mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
release: v1.9.0
This commit is contained in:
@@ -17,12 +17,14 @@ import {
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { useAgentChatUnified } from "./hooks";
|
||||
import { type TaskStatusReason } from "./hooks/agentChatShared";
|
||||
import {
|
||||
buildLiveTaskSnapshot,
|
||||
type TaskStatusReason,
|
||||
} from "./hooks/agentChatShared";
|
||||
import {
|
||||
settleLiveArtifactAfterStreamStops,
|
||||
useArtifactDisplayState,
|
||||
} from "./hooks/useArtifactDisplayState";
|
||||
import { useCompatSubagentRuntime } from "./hooks/useCompatSubagentRuntime";
|
||||
import type { TopicBranchStatus } from "./hooks/useTopicBranchBoard";
|
||||
import { useSessionFiles } from "./hooks/useSessionFiles";
|
||||
import { useContentSync } from "./hooks/useContentSync";
|
||||
@@ -84,6 +86,7 @@ import {
|
||||
|
||||
import type {
|
||||
Message,
|
||||
MessageImage,
|
||||
MessagePreviewTarget,
|
||||
SiteSavedContentTarget,
|
||||
WriteArtifactContext,
|
||||
@@ -100,6 +103,7 @@ import {
|
||||
import { isHiddenInternalArtifactPath } from "./utils/internalArtifactVisibility";
|
||||
import { buildHarnessRequestMetadata } from "./utils/harnessRequestMetadata";
|
||||
import { deriveHarnessSessionState } from "./utils/harnessState";
|
||||
import { resolveWorkflowLayoutBottomSpacing } from "./utils/workflowLayout";
|
||||
import {
|
||||
alignChatToolPreferencesWithExecutionStrategy,
|
||||
loadChatToolPreferences,
|
||||
@@ -110,10 +114,7 @@ import {
|
||||
createSessionRecentPreferencesFromChatToolPreferences,
|
||||
createSessionRecentTeamSelectionFromTeamDefinition,
|
||||
} from "./utils/sessionExecutionRuntime";
|
||||
import {
|
||||
buildRealSubagentTimelineItems,
|
||||
buildSyntheticSubagentTimelineItems,
|
||||
} from "./utils/subagentTimeline";
|
||||
import { buildRealSubagentTimelineItems } from "./utils/subagentTimeline";
|
||||
import {
|
||||
buildGeneralAgentSystemPrompt,
|
||||
resolveAgentChatMode,
|
||||
@@ -148,14 +149,11 @@ import { useWorkspaceHarnessInventoryRuntime } from "./workspace/useWorkspaceHar
|
||||
import { useWorkspaceCanvasWorkflowActions } from "./workspace/useWorkspaceCanvasWorkflowActions";
|
||||
import { useWorkspaceCanvasSceneRuntime } from "./workspace/useWorkspaceCanvasSceneRuntime";
|
||||
import { useWorkspaceCanvasMessageSyncRuntime } from "./workspace/useWorkspaceCanvasMessageSyncRuntime";
|
||||
import { useWorkspaceConversationShellSceneRuntime } from "./workspace/useWorkspaceConversationShellSceneRuntime";
|
||||
import { useWorkspaceDisplayMessagesRuntime } from "./workspace/useWorkspaceDisplayMessagesRuntime";
|
||||
import { useWorkspaceConversationSceneRuntime } from "./workspace/useWorkspaceConversationSceneRuntime";
|
||||
import { useWorkspaceInputbarSceneRuntime } from "./workspace/useWorkspaceInputbarSceneRuntime";
|
||||
import { useWorkspaceNavigationActions } from "./workspace/useWorkspaceNavigationActions";
|
||||
import { useWorkspaceShellChromeRuntime } from "./workspace/useWorkspaceShellChromeRuntime";
|
||||
import { useWorkspaceWriteFileAction } from "./workspace/useWorkspaceWriteFileAction";
|
||||
import { useWorkspaceArtifactPreviewActions } from "./workspace/useWorkspaceArtifactPreviewActions";
|
||||
import { useWorkspaceWorkflowProgressSync } from "./workspace/useWorkspaceWorkflowProgressSync";
|
||||
import { useWorkspaceCanvasLayoutRuntime } from "./workspace/useWorkspaceCanvasLayoutRuntime";
|
||||
import { useWorkspaceCanvasTaskFileSync } from "./workspace/useWorkspaceCanvasTaskFileSync";
|
||||
import { useWorkspaceGeneralResourceSync } from "./workspace/useWorkspaceGeneralResourceSync";
|
||||
@@ -169,26 +167,28 @@ import { buildImageSkillLaunchRequestMetadata } from "./workspace/imageSkillLaun
|
||||
import { useWorkspaceImageTaskPreviewRuntime } from "./workspace/useWorkspaceImageTaskPreviewRuntime";
|
||||
import { useWorkspaceVideoTaskPreviewRuntime } from "./workspace/useWorkspaceVideoTaskPreviewRuntime";
|
||||
import { useWorkspaceVideoTaskActionRuntime } from "./workspace/useWorkspaceVideoTaskActionRuntime";
|
||||
import { useWorkspaceRuntimeTeamDispatchPreviewRuntime } from "./workspace/useWorkspaceRuntimeTeamDispatchPreviewRuntime";
|
||||
import { useWorkspaceSessionRestore } from "./workspace/useWorkspaceSessionRestore";
|
||||
import { useWorkspaceResetRuntime } from "./workspace/useWorkspaceResetRuntime";
|
||||
import { useWorkspaceSendActions } from "./workspace/useWorkspaceSendActions";
|
||||
import { useWorkspaceTeamSessionControlRuntime } from "./workspace/useWorkspaceTeamSessionControlRuntime";
|
||||
import { useWorkspaceTeamWorkbenchAutoOpenRuntime } from "./workspace/useWorkspaceTeamWorkbenchAutoOpenRuntime";
|
||||
import { useWorkspaceGeneralWorkbenchScaffoldRuntime } from "./workspace/useWorkspaceGeneralWorkbenchScaffoldRuntime";
|
||||
import { useWorkspaceGeneralWorkbenchVersionStatusRuntime } from "./workspace/useWorkspaceGeneralWorkbenchVersionStatusRuntime";
|
||||
import { useWorkspaceTopicSwitch } from "./workspace/useWorkspaceTopicSwitch";
|
||||
import { useWorkspaceA2UIRuntime } from "./workspace/useWorkspaceA2UIRuntime";
|
||||
import { useWorkspaceSceneGateRuntime } from "./workspace/useWorkspaceSceneGateRuntime";
|
||||
import { useWorkspaceAutoGuideRuntime } from "./workspace/useWorkspaceAutoGuideRuntime";
|
||||
import { useWorkspaceGeneralWorkbenchSidebarRuntime } from "./workspace/useWorkspaceGeneralWorkbenchSidebarRuntime";
|
||||
import { useWorkspaceGeneralWorkbenchRuntime } from "./workspace/useWorkspaceGeneralWorkbenchRuntime";
|
||||
import { useWorkspaceGeneralWorkbenchShellRuntime } from "./workspace/useWorkspaceGeneralWorkbenchShellRuntime";
|
||||
import { useWorkspaceContextDetailActions } from "./workspace/useWorkspaceContextDetailActions";
|
||||
import { useWorkspaceTeamSessionRuntime } from "./workspace/useWorkspaceTeamSessionRuntime";
|
||||
import { useWorkspaceGeneralWorkbenchDocumentPersistenceRuntime } from "./workspace/useWorkspaceGeneralWorkbenchDocumentPersistenceRuntime";
|
||||
import { useWorkspaceServiceSkillEntryActions } from "./workspace/useWorkspaceServiceSkillEntryActions";
|
||||
import { useWorkspaceArtifactViewModeControl } from "./workspace/useWorkspaceArtifactViewModeControl";
|
||||
import { WorkspaceGeneralWorkbenchSidebar } from "./workspace/WorkspaceGeneralWorkbenchSidebar";
|
||||
import { GeneralWorkbenchHarnessDialogSection } from "./workspace/WorkspaceHarnessDialogs";
|
||||
import { WorkspaceShellScene } from "./workspace/WorkspaceShellScene";
|
||||
import {
|
||||
TEAM_PRIMARY_CHAT_PANEL_MIN_WIDTH,
|
||||
TEAM_PRIMARY_CHAT_PANEL_WIDTH,
|
||||
} from "./workspace/WorkspaceStyles";
|
||||
import { hasNamedGeneralCanvasFilePreview } from "./workspace/generalCanvasPreviewState";
|
||||
import { resolvePreferredServiceSkillResultFileTarget } from "./workspace/serviceSkillResultFileTarget";
|
||||
import {
|
||||
@@ -1605,32 +1605,17 @@ export function AgentChatWorkspace({
|
||||
syncSessionRecentPreferences,
|
||||
]);
|
||||
|
||||
const hasRealTeamGraph =
|
||||
childSubagentSessions.length > 0 || Boolean(subagentParentContext);
|
||||
const {
|
||||
clearRuntimeTeamState: clearPreparedRuntimeTeamState,
|
||||
prepareRuntimeTeamBeforeSend,
|
||||
} = useRuntimeTeamFormation({
|
||||
projectId,
|
||||
sessionId,
|
||||
selectedTeam,
|
||||
subagentEnabled: effectiveChatToolPreferences.subagent,
|
||||
hasRealTeamGraph,
|
||||
});
|
||||
const {
|
||||
runtimeTeamDispatchPreview,
|
||||
runtimeTeamPreviewState,
|
||||
clearRuntimeTeamDispatchPreview,
|
||||
setRuntimeTeamDispatchPreview,
|
||||
} = useWorkspaceRuntimeTeamDispatchPreviewRuntime({
|
||||
messagesLength: messages.length,
|
||||
sessionId,
|
||||
});
|
||||
const teamDispatchPreviewState = runtimeTeamPreviewState;
|
||||
const clearRuntimeTeamState = useCallback(() => {
|
||||
clearPreparedRuntimeTeamState();
|
||||
clearRuntimeTeamDispatchPreview();
|
||||
}, [clearPreparedRuntimeTeamState, clearRuntimeTeamDispatchPreview]);
|
||||
}, [clearPreparedRuntimeTeamState]);
|
||||
const localImageWorkbenchSessionKeyRef = useRef(
|
||||
`__local_image_workbench__:${Date.now().toString(36)}:${Math.random()
|
||||
.toString(36)
|
||||
@@ -1746,17 +1731,13 @@ export function AgentChatWorkspace({
|
||||
liveRuntimeBySessionId: teamSessionRuntime.liveRuntimeBySessionId,
|
||||
stopSending,
|
||||
});
|
||||
const {
|
||||
teamWorkbenchAutoFocusToken,
|
||||
dismissActiveTeamWorkbenchAutoOpen,
|
||||
handleActivateTeamWorkbench,
|
||||
} = useWorkspaceTeamWorkbenchAutoOpenRuntime({
|
||||
hasRealTeamGraph: teamSessionRuntime.hasRealTeamGraph,
|
||||
layoutMode,
|
||||
runtimeTeamRequestId: runtimeTeamDispatchPreview?.key ?? null,
|
||||
sessionId,
|
||||
setLayoutMode,
|
||||
});
|
||||
const [teamWorkbenchAutoFocusToken, setTeamWorkbenchAutoFocusToken] =
|
||||
useState(0);
|
||||
const dismissActiveTeamWorkbenchAutoOpen = useCallback(() => {}, []);
|
||||
const handleActivateTeamWorkbench = useCallback(() => {
|
||||
setTeamWorkbenchAutoFocusToken((current) => current + 1);
|
||||
setLayoutMode((current) => (current === "chat" ? "chat-canvas" : current));
|
||||
}, [setLayoutMode]);
|
||||
useEffect(() => {
|
||||
logAgentDebug(
|
||||
"AgentChatPage",
|
||||
@@ -2021,7 +2002,6 @@ export function AgentChatWorkspace({
|
||||
siteSkillExecutionState?.profileKey,
|
||||
siteSkillExecutionState?.targetId,
|
||||
]);
|
||||
const compatSubagentRuntime = useCompatSubagentRuntime(sessionId);
|
||||
const realSubagentTimelineItems = useMemo(
|
||||
() =>
|
||||
buildRealSubagentTimelineItems({
|
||||
@@ -2031,25 +2011,9 @@ export function AgentChatWorkspace({
|
||||
}),
|
||||
[childSubagentSessions, sessionId, turns],
|
||||
);
|
||||
const syntheticSubagentItems = useMemo(
|
||||
() =>
|
||||
buildSyntheticSubagentTimelineItems({
|
||||
threadId: sessionId,
|
||||
turnId: currentTurnId,
|
||||
events: compatSubagentRuntime.events,
|
||||
}),
|
||||
[compatSubagentRuntime.events, currentTurnId, sessionId],
|
||||
);
|
||||
const effectiveThreadItems = useMemo(
|
||||
() =>
|
||||
mergeThreadItems(
|
||||
threadItems,
|
||||
realSubagentTimelineItems,
|
||||
realSubagentTimelineItems.length > 0
|
||||
? undefined
|
||||
: syntheticSubagentItems,
|
||||
),
|
||||
[realSubagentTimelineItems, syntheticSubagentItems, threadItems],
|
||||
() => mergeThreadItems(threadItems, realSubagentTimelineItems),
|
||||
[realSubagentTimelineItems, threadItems],
|
||||
);
|
||||
const harnessState = useMemo(
|
||||
() =>
|
||||
@@ -2148,7 +2112,6 @@ export function AgentChatWorkspace({
|
||||
isSending,
|
||||
projectMemory,
|
||||
harnessState,
|
||||
compatSubagentRuntime,
|
||||
});
|
||||
const {
|
||||
contextWorkspace,
|
||||
@@ -2259,7 +2222,6 @@ export function AgentChatWorkspace({
|
||||
pendingA2UIForm,
|
||||
pendingA2UISource,
|
||||
pendingActionRequest,
|
||||
pendingLegacyQuestionnaireA2UIForm,
|
||||
pendingPromotedA2UIActionRequest,
|
||||
resolvePendingA2UISubmit,
|
||||
} = useWorkspaceA2UIRuntime({
|
||||
@@ -2315,9 +2277,51 @@ export function AgentChatWorkspace({
|
||||
historyPageSize: GENERAL_WORKBENCH_HISTORY_PAGE_SIZE,
|
||||
});
|
||||
|
||||
const { handleViewContextDetail } = useWorkspaceContextDetailActions({
|
||||
contextWorkspace,
|
||||
});
|
||||
const handleViewContextDetail = useCallback(
|
||||
(contextId: string) => {
|
||||
const detail = contextWorkspace.getContextDetail(contextId);
|
||||
if (!detail) {
|
||||
toast.error("无法找到上下文详情");
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceLabel =
|
||||
detail.source === "material"
|
||||
? "素材库"
|
||||
: detail.source === "content"
|
||||
? "历史内容"
|
||||
: "搜索结果";
|
||||
|
||||
toast.info(
|
||||
<div style={{ maxWidth: "500px" }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: "8px" }}>
|
||||
{detail.name}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
color: "hsl(var(--muted-foreground))",
|
||||
marginBottom: "8px",
|
||||
}}
|
||||
>
|
||||
来源: {sourceLabel} · 约 {detail.estimatedTokens} tokens
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "13px",
|
||||
lineHeight: "1.5",
|
||||
maxHeight: "300px",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{detail.bodyText || detail.previewText}
|
||||
</div>
|
||||
</div>,
|
||||
{ duration: 10000 },
|
||||
);
|
||||
},
|
||||
[contextWorkspace],
|
||||
);
|
||||
|
||||
const harnessRequestMetadata = useMemo(
|
||||
() =>
|
||||
@@ -2384,13 +2388,50 @@ export function AgentChatWorkspace({
|
||||
harnessPendingCount,
|
||||
});
|
||||
|
||||
useWorkspaceGeneralWorkbenchVersionStatusRuntime({
|
||||
isThemeWorkbench,
|
||||
themeWorkbenchRunState,
|
||||
useEffect(() => {
|
||||
if (!isThemeWorkbench || themeWorkbenchRunState !== "idle") {
|
||||
return;
|
||||
}
|
||||
if (!canvasState || canvasState.type !== "document") {
|
||||
return;
|
||||
}
|
||||
|
||||
const latestTerminal = themeWorkbenchBackendRunState?.latest_terminal ?? null;
|
||||
|
||||
setDocumentVersionStatusMap((previous) => {
|
||||
if (latestTerminal) {
|
||||
const terminalVersionId = latestTerminal.run_id;
|
||||
const terminalVersionExists = canvasState.versions.some(
|
||||
(version) => version.id === terminalVersionId,
|
||||
);
|
||||
if (terminalVersionExists) {
|
||||
const terminalStatus: TopicBranchStatus =
|
||||
latestTerminal.status === "success" ? "merged" : "candidate";
|
||||
if (previous[terminalVersionId] !== terminalStatus) {
|
||||
return {
|
||||
...previous,
|
||||
[terminalVersionId]: terminalStatus,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const currentVersionId = canvasState.currentVersionId;
|
||||
if (!currentVersionId || previous[currentVersionId] !== "in_progress") {
|
||||
return previous;
|
||||
}
|
||||
return {
|
||||
...previous,
|
||||
[currentVersionId]: "pending",
|
||||
};
|
||||
});
|
||||
}, [
|
||||
canvasState,
|
||||
latestTerminal: themeWorkbenchBackendRunState?.latest_terminal ?? null,
|
||||
isThemeWorkbench,
|
||||
setDocumentVersionStatusMap,
|
||||
});
|
||||
themeWorkbenchBackendRunState?.latest_terminal,
|
||||
themeWorkbenchRunState,
|
||||
]);
|
||||
|
||||
// 会话文件持久化 hook
|
||||
const {
|
||||
@@ -2622,7 +2663,8 @@ export function AgentChatWorkspace({
|
||||
handleSendRef,
|
||||
webSearchPreferenceRef,
|
||||
isPreparingSend,
|
||||
submissionPreview,
|
||||
displayMessages,
|
||||
teamDispatchPreviewState,
|
||||
} = useWorkspaceSendActions({
|
||||
input,
|
||||
setInput,
|
||||
@@ -2640,6 +2682,7 @@ export function AgentChatWorkspace({
|
||||
prepareActiveContextPrompt: contextWorkspace.prepareActiveContextPrompt,
|
||||
},
|
||||
projectId,
|
||||
sessionId,
|
||||
executionStrategy,
|
||||
accessMode,
|
||||
preferredTeamPresetId,
|
||||
@@ -2654,13 +2697,13 @@ export function AgentChatWorkspace({
|
||||
browserAssistPreferredBackend: browserAssistRequestPreferredBackend,
|
||||
browserAssistAutoLaunch: browserAssistRequestAutoLaunch,
|
||||
workspaceRequestMetadataBase: initialRequestMetadata,
|
||||
messagesCount: messages.length,
|
||||
messages,
|
||||
bootstrapDispatchPreviewMessages,
|
||||
sendMessage,
|
||||
resolveSendBoundary,
|
||||
finalizeAfterSendSuccess,
|
||||
rollbackAfterSendFailure,
|
||||
prepareRuntimeTeamBeforeSend,
|
||||
setRuntimeTeamDispatchPreview,
|
||||
ensureBrowserAssistCanvas,
|
||||
handleAutoLaunchMatchedSiteSkill:
|
||||
workspaceServiceSkillEntryActions.handleAutoLaunchMatchedSiteSkill,
|
||||
@@ -2761,7 +2804,6 @@ export function AgentChatWorkspace({
|
||||
});
|
||||
const { handleInputbarA2UISubmit } = useWorkspaceA2UISubmitActions({
|
||||
handlePermissionResponse,
|
||||
pendingLegacyQuestionnaireA2UIForm,
|
||||
pendingPromotedA2UIActionRequest,
|
||||
resolvePendingA2UISubmit,
|
||||
sendMessage,
|
||||
@@ -2812,19 +2854,31 @@ export function AgentChatWorkspace({
|
||||
console.log("[AgentChatPage] 执行技能命令:", command);
|
||||
handleSend([], false, false, command);
|
||||
}, [pendingSkillKey, isThemeWorkbench, consumePendingSkill, handleSend]);
|
||||
useEffect(() => {
|
||||
if (!sessionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { displayMessages } = useWorkspaceDisplayMessagesRuntime({
|
||||
bootstrapDispatchPreviewMessages,
|
||||
updateTopicSnapshot(
|
||||
sessionId,
|
||||
buildLiveTaskSnapshot({
|
||||
messages: displayMessages,
|
||||
isSending,
|
||||
pendingActionCount: pendingActions.length,
|
||||
queuedTurnCount: queuedTurns.length,
|
||||
workspaceError: Boolean(workspacePathMissing || workspaceHealthError),
|
||||
}),
|
||||
);
|
||||
}, [
|
||||
displayMessages,
|
||||
isSending,
|
||||
messages,
|
||||
pendingActionCount: pendingActions.length,
|
||||
queuedTurnCount: queuedTurns.length,
|
||||
runtimeTeamDispatchPreview,
|
||||
submissionPreview,
|
||||
pendingActions.length,
|
||||
queuedTurns.length,
|
||||
sessionId,
|
||||
updateTopicSnapshot,
|
||||
workspaceError: Boolean(workspacePathMissing || workspaceHealthError),
|
||||
});
|
||||
workspaceHealthError,
|
||||
workspacePathMissing,
|
||||
]);
|
||||
const latestAssistantMessageId = useMemo(
|
||||
() =>
|
||||
[...displayMessages]
|
||||
@@ -2890,7 +2944,7 @@ export function AgentChatWorkspace({
|
||||
shouldBootstrapCanvasOnEntry,
|
||||
canvasState,
|
||||
generalCanvasState,
|
||||
showTeamWorkspaceBoard: teamSessionRuntime.showTeamWorkspaceBoard,
|
||||
teamWorkspaceEnabled: teamSessionRuntime.teamWorkspaceEnabled,
|
||||
hasCurrentCanvasArtifact: Boolean(currentCanvasArtifact),
|
||||
currentCanvasArtifactType: currentCanvasArtifact?.type,
|
||||
hasBrowserAssistArtifact,
|
||||
@@ -3459,61 +3513,229 @@ export function AgentChatWorkspace({
|
||||
setChatMessages,
|
||||
});
|
||||
|
||||
const shellChromeRuntime = useWorkspaceShellChromeRuntime({
|
||||
const shellChromeRuntime = useMemo(() => {
|
||||
const hasUnconsumedInitialDispatch =
|
||||
!shouldUseCompactGeneralWorkbench && isBootstrapDispatchPending;
|
||||
|
||||
const showChatLayout =
|
||||
agentEntry === "claw" ||
|
||||
hasDisplayMessages ||
|
||||
hasPendingA2UIForm ||
|
||||
isThemeWorkbench ||
|
||||
hasUnconsumedInitialDispatch ||
|
||||
isSending ||
|
||||
queuedTurns.length > 0;
|
||||
|
||||
const shouldHideGeneralWorkbenchInputForTheme =
|
||||
shouldUseCompactGeneralWorkbench;
|
||||
const shouldShowGeneralWorkbenchFloatingInputOverlay =
|
||||
isThemeWorkbench &&
|
||||
showChatLayout &&
|
||||
!shouldHideGeneralWorkbenchInputForTheme;
|
||||
const isWorkspaceCompactChrome = topBarChrome === "workspace-compact";
|
||||
const shouldRenderBrandedEmptyState = !showChatLayout;
|
||||
const shouldRenderTopBar = !hideTopBar && !shouldRenderBrandedEmptyState;
|
||||
const shouldRenderInlineA2UI = isSpecializedThemeMode;
|
||||
|
||||
const shouldUseTeamPrimaryChatPanelWidth =
|
||||
layoutMode === "chat-canvas" &&
|
||||
teamSessionRuntime.teamWorkspaceEnabled &&
|
||||
(teamSessionRuntime.hasRuntimeSessions || Boolean(teamDispatchPreviewState));
|
||||
|
||||
return {
|
||||
showChatLayout,
|
||||
isWorkspaceCompactChrome,
|
||||
workflowLayoutBottomSpacing: resolveWorkflowLayoutBottomSpacing({
|
||||
contextWorkspaceEnabled: contextWorkspace.generalWorkbenchEnabled,
|
||||
showFloatingInputOverlay:
|
||||
shouldShowGeneralWorkbenchFloatingInputOverlay,
|
||||
hasCanvasContent: layoutMode !== "chat",
|
||||
workflowRunState: themeWorkbenchRunState,
|
||||
gateStatus: currentGate.status,
|
||||
}),
|
||||
shouldHideGeneralWorkbenchInputForTheme,
|
||||
shouldRenderTopBar,
|
||||
layoutTransitionChatPanelWidth: shouldUseTeamPrimaryChatPanelWidth
|
||||
? TEAM_PRIMARY_CHAT_PANEL_WIDTH
|
||||
: undefined,
|
||||
layoutTransitionChatPanelMinWidth: shouldUseTeamPrimaryChatPanelWidth
|
||||
? TEAM_PRIMARY_CHAT_PANEL_MIN_WIDTH
|
||||
: undefined,
|
||||
shouldShowGeneralWorkbenchFloatingInputOverlay,
|
||||
shouldRenderInlineA2UI,
|
||||
};
|
||||
}, [
|
||||
agentEntry,
|
||||
contextWorkspaceEnabled: contextWorkspace.generalWorkbenchEnabled,
|
||||
contextWorkspace.generalWorkbenchEnabled,
|
||||
currentGate.status,
|
||||
hasDisplayMessages,
|
||||
hasPendingA2UIForm,
|
||||
hideTopBar,
|
||||
isBootstrapDispatchPending,
|
||||
isSpecializedThemeMode,
|
||||
isSending,
|
||||
isSpecializedThemeMode,
|
||||
isThemeWorkbench,
|
||||
layoutMode,
|
||||
queuedTurnCount: queuedTurns.length,
|
||||
queuedTurns.length,
|
||||
shouldUseCompactGeneralWorkbench,
|
||||
showTeamWorkspaceBoard: teamSessionRuntime.showTeamWorkspaceBoard,
|
||||
topBarChrome,
|
||||
themeWorkbenchRunState,
|
||||
currentGateStatus: currentGate.status,
|
||||
hasRealTeamGraph: teamSessionRuntime.hasRealTeamGraph,
|
||||
teamDispatchPreviewState,
|
||||
});
|
||||
const generalWorkbenchShellRuntime = useWorkspaceGeneralWorkbenchShellRuntime(
|
||||
{
|
||||
showChatPanel: effectiveShowChatPanel,
|
||||
showSidebar,
|
||||
hasPendingA2UIForm,
|
||||
contextHarnessRuntime,
|
||||
generalWorkbenchScaffoldRuntime,
|
||||
generalWorkbenchSidebarRuntime,
|
||||
harnessInventoryRuntime,
|
||||
handleCreateVersionSnapshot,
|
||||
handleSwitchBranchVersion,
|
||||
handleSetBranchStatus,
|
||||
handleAddImage,
|
||||
handleImportDocument,
|
||||
handleViewContextDetail,
|
||||
messages,
|
||||
harnessState,
|
||||
compatSubagentRuntime,
|
||||
childSubagentSessions,
|
||||
selectedTeamLabel,
|
||||
selectedTeamSummary,
|
||||
selectedTeamRoles: selectedTeam?.roles,
|
||||
teamMemorySnapshot: resolvedTeamMemoryShadowSnapshot,
|
||||
handleOpenSubagentSession,
|
||||
handleHarnessLoadFilePreview,
|
||||
handleFileClick: handleWorkspaceFileClick,
|
||||
},
|
||||
teamSessionRuntime.hasRuntimeSessions,
|
||||
teamSessionRuntime.teamWorkspaceEnabled,
|
||||
themeWorkbenchRunState,
|
||||
topBarChrome,
|
||||
]);
|
||||
const shouldShowGeneralWorkbenchSidebarForTheme =
|
||||
!generalWorkbenchScaffoldRuntime.shouldUseCompactGeneralWorkbench;
|
||||
const showGeneralWorkbenchSidebar =
|
||||
effectiveShowChatPanel &&
|
||||
showSidebar &&
|
||||
!hasPendingA2UIForm &&
|
||||
isThemeWorkbench &&
|
||||
shouldShowGeneralWorkbenchSidebarForTheme &&
|
||||
(!generalWorkbenchScaffoldRuntime.enableGeneralWorkbenchPanelCollapse ||
|
||||
!generalWorkbenchScaffoldRuntime.generalWorkbenchSidebarCollapsed);
|
||||
const showGeneralWorkbenchLeftExpandButton =
|
||||
effectiveShowChatPanel &&
|
||||
showSidebar &&
|
||||
!hasPendingA2UIForm &&
|
||||
isThemeWorkbench &&
|
||||
shouldShowGeneralWorkbenchSidebarForTheme &&
|
||||
generalWorkbenchScaffoldRuntime.enableGeneralWorkbenchPanelCollapse &&
|
||||
generalWorkbenchScaffoldRuntime.generalWorkbenchSidebarCollapsed;
|
||||
const handleDeleteGeneralWorkbenchVersion = useCallback(
|
||||
() => undefined,
|
||||
[],
|
||||
);
|
||||
const handleCollapseGeneralWorkbenchSidebar = useCallback(() => {
|
||||
generalWorkbenchScaffoldRuntime.setGeneralWorkbenchSidebarCollapsed(true);
|
||||
}, [generalWorkbenchScaffoldRuntime]);
|
||||
const handleExpandGeneralWorkbenchSidebar = useCallback(() => {
|
||||
generalWorkbenchScaffoldRuntime.setGeneralWorkbenchSidebarCollapsed(false);
|
||||
}, [generalWorkbenchScaffoldRuntime]);
|
||||
const generalWorkbenchHarnessDialog = (
|
||||
<GeneralWorkbenchHarnessDialogSection
|
||||
enabled={
|
||||
contextHarnessRuntime.workbenchEnabled &&
|
||||
contextHarnessRuntime.isThemeWorkbench
|
||||
}
|
||||
open={contextHarnessRuntime.harnessPanelVisible}
|
||||
onOpenChange={contextHarnessRuntime.setHarnessPanelVisible}
|
||||
harnessState={harnessState}
|
||||
environment={contextHarnessRuntime.harnessEnvironment}
|
||||
childSubagentSessions={childSubagentSessions}
|
||||
selectedTeamLabel={selectedTeamLabel}
|
||||
selectedTeamSummary={selectedTeamSummary}
|
||||
selectedTeamRoles={selectedTeam?.roles}
|
||||
teamMemorySnapshot={resolvedTeamMemoryShadowSnapshot}
|
||||
toolInventory={harnessInventoryRuntime.toolInventory}
|
||||
toolInventoryLoading={harnessInventoryRuntime.toolInventoryLoading}
|
||||
toolInventoryError={harnessInventoryRuntime.toolInventoryError}
|
||||
onRefreshToolInventory={harnessInventoryRuntime.refreshToolInventory}
|
||||
onOpenSubagentSession={handleOpenSubagentSession}
|
||||
onLoadFilePreview={handleHarnessLoadFilePreview}
|
||||
onOpenFile={handleWorkspaceFileClick}
|
||||
/>
|
||||
);
|
||||
const generalWorkbenchSidebarNode = (
|
||||
<WorkspaceGeneralWorkbenchSidebar
|
||||
visible={showGeneralWorkbenchSidebar}
|
||||
isThemeWorkbench={isThemeWorkbench}
|
||||
enablePanelCollapse={
|
||||
generalWorkbenchScaffoldRuntime.enableGeneralWorkbenchPanelCollapse
|
||||
}
|
||||
onRequestCollapse={handleCollapseGeneralWorkbenchSidebar}
|
||||
generalWorkbenchHarnessSummary={
|
||||
harnessInventoryRuntime.generalWorkbenchHarnessSummary
|
||||
}
|
||||
harnessPanelVisible={contextHarnessRuntime.harnessPanelVisible}
|
||||
onToggleHarnessPanel={contextHarnessRuntime.handleToggleHarnessPanel}
|
||||
workflow={{
|
||||
branchItems: generalWorkbenchScaffoldRuntime.branchItems,
|
||||
onCreateVersionSnapshot: handleCreateVersionSnapshot,
|
||||
onSwitchBranchVersion: handleSwitchBranchVersion,
|
||||
onDeleteTopic: handleDeleteGeneralWorkbenchVersion,
|
||||
onSetBranchStatus: handleSetBranchStatus,
|
||||
workflowSteps:
|
||||
generalWorkbenchSidebarRuntime.generalWorkbenchWorkflowSteps,
|
||||
onAddImage: handleAddImage,
|
||||
onImportDocument: handleImportDocument,
|
||||
activityLogs: generalWorkbenchSidebarRuntime.generalWorkbenchActivityLogs,
|
||||
creationTaskEvents:
|
||||
generalWorkbenchScaffoldRuntime.generalWorkbenchCreationTaskEvents,
|
||||
onViewRunDetail:
|
||||
generalWorkbenchSidebarRuntime.handleViewGeneralWorkbenchRunDetail,
|
||||
activeRunDetail:
|
||||
generalWorkbenchSidebarRuntime.selectedGeneralWorkbenchRunDetail,
|
||||
activeRunDetailLoading:
|
||||
generalWorkbenchSidebarRuntime.generalWorkbenchRunDetailLoading,
|
||||
}}
|
||||
contextWorkspace={contextHarnessRuntime.contextWorkspace}
|
||||
onViewContextDetail={handleViewContextDetail}
|
||||
history={{
|
||||
hasMore: generalWorkbenchSidebarRuntime.generalWorkbenchHistoryHasMore,
|
||||
loading: generalWorkbenchSidebarRuntime.generalWorkbenchHistoryLoading,
|
||||
onLoadMore:
|
||||
generalWorkbenchSidebarRuntime
|
||||
.handleLoadMoreGeneralWorkbenchHistory,
|
||||
skillDetailMap:
|
||||
generalWorkbenchSidebarRuntime.generalWorkbenchSkillDetailMap,
|
||||
messages,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
useWorkspaceWorkflowProgressSync({
|
||||
enabled: isSpecializedThemeMode && hasMessages && steps.length > 0,
|
||||
const workflowProgressEnabled =
|
||||
isSpecializedThemeMode && hasMessages && steps.length > 0;
|
||||
const workflowProgressSignature = useMemo(() => {
|
||||
if (!workflowProgressEnabled) {
|
||||
return "hidden";
|
||||
}
|
||||
|
||||
const stepSignature = steps
|
||||
.map((step) => `${step.id}:${step.status}:${step.title}`)
|
||||
.join("|");
|
||||
return `${currentStepIndex}:${stepSignature}`;
|
||||
}, [currentStepIndex, steps, workflowProgressEnabled]);
|
||||
const lastWorkflowProgressSignatureRef = useRef<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!onWorkflowProgressChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
lastWorkflowProgressSignatureRef.current === workflowProgressSignature
|
||||
) {
|
||||
return;
|
||||
}
|
||||
lastWorkflowProgressSignatureRef.current = workflowProgressSignature;
|
||||
|
||||
if (!workflowProgressEnabled) {
|
||||
onWorkflowProgressChange(null);
|
||||
return;
|
||||
}
|
||||
|
||||
onWorkflowProgressChange({
|
||||
currentIndex: currentStepIndex,
|
||||
steps: steps.map((step) => ({
|
||||
id: step.id,
|
||||
title: step.title,
|
||||
status: step.status,
|
||||
})),
|
||||
});
|
||||
}, [
|
||||
currentStepIndex,
|
||||
steps,
|
||||
onWorkflowProgressChange,
|
||||
});
|
||||
steps,
|
||||
workflowProgressEnabled,
|
||||
workflowProgressSignature,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
onWorkflowProgressChange?.(null);
|
||||
};
|
||||
}, [onWorkflowProgressChange]);
|
||||
const navigationActions = useWorkspaceNavigationActions({
|
||||
applyProjectSelection,
|
||||
compactSession,
|
||||
@@ -3589,14 +3811,38 @@ export function AgentChatWorkspace({
|
||||
selectedTeamSummary,
|
||||
teamDispatchPreviewState,
|
||||
teamMemorySnapshot: resolvedTeamMemoryShadowSnapshot,
|
||||
teamSessionRuntime,
|
||||
teamSessionControlRuntime,
|
||||
currentSessionTitle: teamSessionRuntime.currentSessionTitle,
|
||||
currentSessionRuntimeStatus:
|
||||
teamSessionRuntime.currentSessionRuntimeStatus,
|
||||
currentSessionLatestTurnStatus:
|
||||
teamSessionRuntime.currentSessionLatestTurnStatus,
|
||||
liveRuntimeBySessionId: teamSessionRuntime.liveRuntimeBySessionId,
|
||||
liveActivityBySessionId: teamSessionRuntime.liveActivityBySessionId,
|
||||
activityRefreshVersionBySessionId:
|
||||
teamSessionRuntime.activityRefreshVersionBySessionId,
|
||||
handleSendSubagentInput:
|
||||
teamSessionControlRuntime.handleSendSubagentInput,
|
||||
handleWaitSubagentSession:
|
||||
teamSessionControlRuntime.handleWaitSubagentSession,
|
||||
handleWaitActiveTeamSessions:
|
||||
teamSessionControlRuntime.handleWaitActiveTeamSessions,
|
||||
handleCloseCompletedTeamSessions:
|
||||
teamSessionControlRuntime.handleCloseCompletedTeamSessions,
|
||||
handleCloseSubagentSession:
|
||||
teamSessionControlRuntime.handleCloseSubagentSession,
|
||||
handleResumeSubagentSession:
|
||||
teamSessionControlRuntime.handleResumeSubagentSession,
|
||||
teamWaitSummary: teamSessionControlRuntime.teamWaitSummary,
|
||||
teamControlSummary: teamSessionControlRuntime.teamControlSummary,
|
||||
handleStopSending: teamSessionControlRuntime.handleStopSending,
|
||||
teamWorkspaceEnabled: teamSessionRuntime.teamWorkspaceEnabled,
|
||||
handleOpenSubagentSession,
|
||||
handleReturnToParentSession,
|
||||
input,
|
||||
setInput,
|
||||
currentGate,
|
||||
generalWorkbenchSidebarRuntime,
|
||||
generalWorkbenchWorkflowSteps:
|
||||
generalWorkbenchSidebarRuntime.generalWorkbenchWorkflowSteps,
|
||||
steps,
|
||||
workflowRunState: themeWorkbenchRunState,
|
||||
handleSend,
|
||||
@@ -3649,14 +3895,20 @@ export function AgentChatWorkspace({
|
||||
handleRestartGeneralWorkbenchEntryPrompt,
|
||||
handleContinueGeneralWorkbenchEntryPrompt,
|
||||
generalWorkbenchEnabled: workspaceHarnessEnabled && chatMode === "general",
|
||||
contextHarnessRuntime,
|
||||
harnessPanelVisible: contextHarnessRuntime.harnessPanelVisible,
|
||||
setHarnessPanelVisible: contextHarnessRuntime.setHarnessPanelVisible,
|
||||
harnessState,
|
||||
compatSubagentRuntime,
|
||||
harnessInventoryRuntime,
|
||||
harnessEnvironment: contextHarnessRuntime.harnessEnvironment,
|
||||
toolInventory: harnessInventoryRuntime.toolInventory,
|
||||
toolInventoryLoading: harnessInventoryRuntime.toolInventoryLoading,
|
||||
toolInventoryError: harnessInventoryRuntime.toolInventoryError,
|
||||
refreshToolInventory: harnessInventoryRuntime.refreshToolInventory,
|
||||
mappedTheme,
|
||||
activeRuntimeStatusTitle: contextHarnessRuntime.activeRuntimeStatusTitle,
|
||||
handleHarnessLoadFilePreview,
|
||||
handleFileClick: handleWorkspaceFileClick,
|
||||
shellChromeRuntime,
|
||||
showGeneralWorkbenchFloatingInputOverlay:
|
||||
shellChromeRuntime.shouldShowGeneralWorkbenchFloatingInputOverlay,
|
||||
handleActivateTeamWorkbench,
|
||||
chatToolPreferences: effectiveChatToolPreferences,
|
||||
});
|
||||
@@ -3712,40 +3964,53 @@ export function AgentChatWorkspace({
|
||||
handleDocumentContentReviewRun,
|
||||
handleDocumentTextStylizeRun,
|
||||
preferContentReviewInRightRail,
|
||||
teamSessionRuntime,
|
||||
teamSessionControlRuntime,
|
||||
teamWorkspaceEnabled: teamSessionRuntime.teamWorkspaceEnabled,
|
||||
liveActivityBySessionId: teamSessionRuntime.liveActivityBySessionId,
|
||||
teamWaitSummary: teamSessionControlRuntime.teamWaitSummary,
|
||||
teamControlSummary: teamSessionControlRuntime.teamControlSummary,
|
||||
teamWorkbenchAutoFocusToken,
|
||||
teamDispatchPreviewState,
|
||||
teamMemorySnapshot: resolvedTeamMemoryShadowSnapshot,
|
||||
});
|
||||
|
||||
const workspaceShellSceneRuntime = useWorkspaceConversationShellSceneRuntime({
|
||||
const handleSendFromEmptyState = useCallback(
|
||||
(
|
||||
text: string,
|
||||
sendExecutionStrategy?: "react" | "code_orchestrated" | "auto",
|
||||
images?: MessageImage[],
|
||||
) => {
|
||||
void handleSend(
|
||||
images || [],
|
||||
effectiveChatToolPreferences.webSearch,
|
||||
effectiveChatToolPreferences.thinking,
|
||||
text,
|
||||
sendExecutionStrategy,
|
||||
);
|
||||
},
|
||||
[
|
||||
effectiveChatToolPreferences.thinking,
|
||||
effectiveChatToolPreferences.webSearch,
|
||||
handleSend,
|
||||
],
|
||||
);
|
||||
|
||||
const conversationSceneRuntime = useWorkspaceConversationSceneRuntime({
|
||||
messageListEmptyStateVariant:
|
||||
agentEntry === "claw" ? "task-center" : "default",
|
||||
navbarContextVariant: agentEntry === "claw" ? "task-center" : "default",
|
||||
sidebarContextVariant: agentEntry === "claw" ? "task-center" : "default",
|
||||
navigationActions,
|
||||
inputbarScene,
|
||||
canvasScene,
|
||||
handleSendFromEmptyState,
|
||||
shellChromeRuntime,
|
||||
generalWorkbenchShellRuntime,
|
||||
contextHarnessRuntime,
|
||||
teamSessionRuntime,
|
||||
currentImageWorkbenchState,
|
||||
project,
|
||||
projectId,
|
||||
projectMemory,
|
||||
handleSend,
|
||||
generalCanvasState,
|
||||
showSidebar,
|
||||
topics,
|
||||
switchTopic,
|
||||
handleResumeSidebarTask,
|
||||
deleteTopic,
|
||||
renameTopic,
|
||||
childSubagentSessions,
|
||||
subagentParentContext,
|
||||
handleReturnToParentSession,
|
||||
generalWorkbenchHarnessDialog,
|
||||
teamWorkspaceEnabled: teamSessionRuntime.teamWorkspaceEnabled,
|
||||
currentImageWorkbenchActive: currentImageWorkbenchState.active,
|
||||
projectId: projectId ?? null,
|
||||
projectRootPath: project?.rootPath || null,
|
||||
projectCharacters: projectMemory?.characters || [],
|
||||
generalCanvasContent: generalCanvasState.content,
|
||||
handleToggleHarnessPanel: contextHarnessRuntime.handleToggleHarnessPanel,
|
||||
entryBannerVisible,
|
||||
entryBannerMessage,
|
||||
serviceSkillExecutionCard,
|
||||
@@ -3856,9 +4121,47 @@ export function AgentChatWorkspace({
|
||||
timelineFocusRequestKey,
|
||||
});
|
||||
|
||||
const shellPendingActions = pendingActions ?? [];
|
||||
const shellQueuedTurns = queuedTurns ?? [];
|
||||
const shellHandleBackHome = handleBackHome ?? (() => undefined);
|
||||
const shellHandleOpenSubagentSession =
|
||||
handleOpenSubagentSession ?? (() => undefined);
|
||||
const shellDisplayMessages = displayMessages ?? [];
|
||||
const shellIsSending = isSending ?? false;
|
||||
|
||||
return (
|
||||
<>
|
||||
{workspaceShellSceneRuntime.shellSceneNode}
|
||||
<WorkspaceShellScene
|
||||
compactChrome={shellChromeRuntime.isWorkspaceCompactChrome}
|
||||
isThemeWorkbench={isThemeWorkbench}
|
||||
generalWorkbenchSidebarNode={generalWorkbenchSidebarNode}
|
||||
showChatPanel={effectiveShowChatPanel}
|
||||
showSidebar={showSidebar}
|
||||
sidebarContextVariant={
|
||||
agentEntry === "claw" ? "task-center" : "default"
|
||||
}
|
||||
showGeneralWorkbenchLeftExpandButton={
|
||||
showGeneralWorkbenchLeftExpandButton
|
||||
}
|
||||
onExpandGeneralWorkbenchSidebar={handleExpandGeneralWorkbenchSidebar}
|
||||
mainAreaNode={conversationSceneRuntime.mainAreaNode}
|
||||
currentTopicId={sessionId ?? null}
|
||||
topics={topics}
|
||||
onNewChat={shellHandleBackHome}
|
||||
onSwitchTopic={switchTopic}
|
||||
onResumeTask={handleResumeSidebarTask}
|
||||
onDeleteTopic={deleteTopic}
|
||||
onRenameTopic={renameTopic}
|
||||
currentMessages={shellDisplayMessages}
|
||||
isSending={shellIsSending}
|
||||
pendingActionCount={shellPendingActions.length}
|
||||
queuedTurnCount={shellQueuedTurns.length}
|
||||
workspaceError={conversationSceneRuntime.workspaceAlertVisible}
|
||||
childSubagentSessions={childSubagentSessions}
|
||||
subagentParentContext={subagentParentContext}
|
||||
onOpenSubagentSession={shellHandleOpenSubagentSession}
|
||||
onReturnToParentSession={handleReturnToParentSession}
|
||||
/>
|
||||
<AutomationJobDialog
|
||||
open={workspaceServiceSkillEntryActions.automationDialogOpen}
|
||||
mode="create"
|
||||
|
||||
@@ -7,7 +7,6 @@ import type {
|
||||
} from "@/lib/api/agentRuntime";
|
||||
|
||||
import type { ChatToolPreferences } from "../utils/chatToolPreferences";
|
||||
import type { CompatSubagentRuntimeStatus } from "../utils/compatSubagentRuntime";
|
||||
import type { HarnessSessionState } from "../utils/harnessState";
|
||||
import { getOutputSchemaRuntimeLabel } from "../utils/sessionExecutionRuntime";
|
||||
|
||||
@@ -16,7 +15,6 @@ interface AgentRuntimeStripProps {
|
||||
toolPreferences: ChatToolPreferences;
|
||||
harnessState: HarnessSessionState;
|
||||
childSubagentSessions?: AsterSubagentSessionInfo[];
|
||||
compatSubagentRuntime: CompatSubagentRuntimeStatus;
|
||||
variant?: "standalone" | "embedded";
|
||||
isSending?: boolean;
|
||||
executionRuntime?: AsterSessionExecutionRuntime | null;
|
||||
@@ -47,7 +45,6 @@ export const AgentRuntimeStrip: React.FC<AgentRuntimeStripProps> = ({
|
||||
activeTheme,
|
||||
toolPreferences,
|
||||
harnessState,
|
||||
compatSubagentRuntime,
|
||||
childSubagentSessions = [],
|
||||
variant = "standalone",
|
||||
isSending = false,
|
||||
@@ -72,7 +69,11 @@ export const AgentRuntimeStrip: React.FC<AgentRuntimeStripProps> = ({
|
||||
label: "联网搜索",
|
||||
enabled: toolPreferences.webSearch,
|
||||
},
|
||||
{ key: "subagent", label: "多代理", enabled: toolPreferences.subagent },
|
||||
{
|
||||
key: "subagent",
|
||||
label: "任务拆分",
|
||||
enabled: toolPreferences.subagent,
|
||||
},
|
||||
],
|
||||
[toolPreferences],
|
||||
);
|
||||
@@ -150,26 +151,14 @@ export const AgentRuntimeStrip: React.FC<AgentRuntimeStripProps> = ({
|
||||
key: "team_sessions",
|
||||
label:
|
||||
completedTeamSessions > 0
|
||||
? `协作会话 ${childSubagentSessions.length} · 已完成 ${completedTeamSessions}`
|
||||
: `协作会话 ${childSubagentSessions.length}`,
|
||||
? `子任务 ${childSubagentSessions.length} · 已完成 ${completedTeamSessions}`
|
||||
: `子任务 ${childSubagentSessions.length}`,
|
||||
tone: "outline",
|
||||
});
|
||||
} else if (compatSubagentRuntime.isRunning) {
|
||||
const progressLabel =
|
||||
compatSubagentRuntime.progress &&
|
||||
typeof compatSubagentRuntime.progress.completed === "number" &&
|
||||
typeof compatSubagentRuntime.progress.total === "number"
|
||||
? `协作成员处理中 ${compatSubagentRuntime.progress.completed}/${compatSubagentRuntime.progress.total}`
|
||||
: "协作成员处理中";
|
||||
nextItems.push({
|
||||
key: "subagent_running",
|
||||
label: progressLabel,
|
||||
tone: "secondary",
|
||||
});
|
||||
} else if (harnessState.delegatedTasks.length > 0) {
|
||||
nextItems.push({
|
||||
key: "delegated",
|
||||
label: `最近协作 ${harnessState.delegatedTasks.length}`,
|
||||
label: `最近子任务 ${harnessState.delegatedTasks.length}`,
|
||||
tone: "outline",
|
||||
});
|
||||
}
|
||||
@@ -193,8 +182,6 @@ export const AgentRuntimeStrip: React.FC<AgentRuntimeStripProps> = ({
|
||||
return nextItems;
|
||||
}, [
|
||||
childSubagentSessions,
|
||||
compatSubagentRuntime.isRunning,
|
||||
compatSubagentRuntime.progress,
|
||||
executionRuntime,
|
||||
harnessState,
|
||||
isExecutionRuntimeActive,
|
||||
@@ -216,8 +203,8 @@ export const AgentRuntimeStrip: React.FC<AgentRuntimeStripProps> = ({
|
||||
{toolPreferences.subagent ? (
|
||||
<Badge variant={hasSelectedTeam ? "secondary" : "outline"}>
|
||||
{hasSelectedTeam
|
||||
? `协作 · ${selectedTeamLabel || `${selectedTeamRoleCount} 角色`}`
|
||||
: "协作模式"}
|
||||
? `分工 · ${selectedTeamLabel || `${selectedTeamRoleCount} 角色`}`
|
||||
: "分工模式"}
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -238,14 +225,14 @@ export const AgentRuntimeStrip: React.FC<AgentRuntimeStripProps> = ({
|
||||
</div>
|
||||
{toolPreferences.subagent ? (
|
||||
<div className="mb-3 rounded-xl border border-border/70 bg-background/80 px-3 py-2 text-xs text-muted-foreground">
|
||||
<span className="font-medium text-foreground">当前协作设置</span>
|
||||
<span className="font-medium text-foreground">当前任务分工</span>
|
||||
<span>
|
||||
{" "}
|
||||
·{" "}
|
||||
{selectedTeamSummary?.trim() ||
|
||||
(hasSelectedTeam
|
||||
? `已配置 ${selectedTeamRoleCount} 个角色,系统会按需邀请协作成员。`
|
||||
: "已开启协作模式,本次可选择或自定义协作方案。")}
|
||||
? `已配置 ${selectedTeamRoleCount} 个角色,系统会按需拆出子任务。`
|
||||
: "已开启分工模式,本次可选择或自定义任务方案。")}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { ReactNode } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import type { TurnMemoryPrefetchResult } from "@/lib/api/memoryRuntime";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { normalizeTeamMemoryDisplayText } from "../utils/teamMemoryDisplay";
|
||||
|
||||
type RuntimeMemoryPrefetchStatus = "idle" | "loading" | "ready" | "error";
|
||||
|
||||
@@ -152,7 +153,7 @@ export function AgentThreadMemoryPrefetchPreview({
|
||||
)}
|
||||
>
|
||||
{status === "loading"
|
||||
? "正在按最新回合 prompt 预演 rules / working / durable / team / compaction 五层记忆命中。"
|
||||
? "正在按最新回合 prompt 预演规则 / 工作 / 持久 / 任务影子 / 压缩五层记忆命中。"
|
||||
: status === "error"
|
||||
? error
|
||||
: "下面展示的是当前这轮真实会用到的记忆命中预演,不会改写会话,只帮助判断续接质量。"}
|
||||
@@ -175,7 +176,7 @@ export function AgentThreadMemoryPrefetchPreview({
|
||||
{formatMemoryLayerStatusLabel("持久", result.durable_memories.length)}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="border-sky-200 bg-white text-sky-700">
|
||||
{formatMemoryLayerStatusLabel("Team", result.team_memory_entries.length)}
|
||||
{formatMemoryLayerStatusLabel("任务影子", result.team_memory_entries.length)}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="border-sky-200 bg-white text-sky-700">
|
||||
{formatMemoryLayerStatusLabel(
|
||||
@@ -256,7 +257,7 @@ export function AgentThreadMemoryPrefetchPreview({
|
||||
) : null}
|
||||
</DetailPanel>
|
||||
|
||||
<DetailPanel title="Team Shadow 明细" emptyText="当前没有命中的 Team shadow。">
|
||||
<DetailPanel title="任务影子明细" emptyText="当前没有命中的任务影子。">
|
||||
{result.team_memory_entries.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{result.team_memory_entries.slice(0, 3).map((entry) => (
|
||||
@@ -275,13 +276,16 @@ export function AgentThreadMemoryPrefetchPreview({
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-2 text-sm leading-6 text-slate-700">
|
||||
{truncateText(entry.content, 220)}
|
||||
{truncateText(
|
||||
normalizeTeamMemoryDisplayText(entry.content),
|
||||
220,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{result.team_memory_entries.length > 3 ? (
|
||||
<div className="text-xs text-slate-500">
|
||||
另有 {result.team_memory_entries.length - 3} 条 Team shadow 未展开。
|
||||
另有 {result.team_memory_entries.length - 3} 条任务影子未展开。
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -613,7 +613,7 @@ describe("AgentThreadReliabilityPanel", () => {
|
||||
expect(container.textContent).toContain("规则 2");
|
||||
expect(container.textContent).toContain("工作 已命中");
|
||||
expect(container.textContent).toContain("持久 1");
|
||||
expect(container.textContent).toContain("Team 1");
|
||||
expect(container.textContent).toContain("任务影子 1");
|
||||
expect(container.textContent).toContain("压缩 已命中");
|
||||
expect(container.textContent).toContain("工作记忆摘录");
|
||||
expect(container.textContent).toContain("继续整理风险点与来源摘要");
|
||||
@@ -622,7 +622,7 @@ describe("AgentThreadReliabilityPanel", () => {
|
||||
expect(container.textContent).toContain("持久记忆命中");
|
||||
expect(container.textContent).toContain("研究简报输出偏好");
|
||||
expect(container.textContent).toContain("优先给风险与证据链");
|
||||
expect(container.textContent).toContain("Team Shadow 明细");
|
||||
expect(container.textContent).toContain("任务影子明细");
|
||||
expect(container.textContent).toContain("team.selection");
|
||||
expect(container.textContent).toContain("研究协作队");
|
||||
expect(container.textContent).toContain("压缩续接摘要");
|
||||
@@ -1032,7 +1032,7 @@ describe("AgentThreadReliabilityPanel", () => {
|
||||
expect.stringContaining("规则层:1 个来源"),
|
||||
);
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Team 层:1 条"),
|
||||
expect.stringContaining("任务影子层:1 条"),
|
||||
);
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
|
||||
expect.stringContaining("规则来源:/workspace/AGENTS.md"),
|
||||
@@ -1041,7 +1041,7 @@ describe("AgentThreadReliabilityPanel", () => {
|
||||
expect.stringContaining("持久记忆详情:研究输出格式偏好|先给结论,再列风险"),
|
||||
);
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Team 影子详情:team.selection|研究协作队"),
|
||||
expect.stringContaining("任务影子详情:team.selection|研究协作队"),
|
||||
);
|
||||
expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
|
||||
expect.stringContaining("压缩命中摘要:保留研究目标与最近来源摘要。"),
|
||||
@@ -1077,7 +1077,7 @@ describe("AgentThreadReliabilityPanel", () => {
|
||||
expect.stringContaining("浏览器工具执行失败"),
|
||||
);
|
||||
expect(mockToast.success).toHaveBeenCalledWith("AI 诊断内容已复制");
|
||||
expect(container.textContent).toContain("compat 快速诊断");
|
||||
expect(container.textContent).toContain("线程级快速诊断");
|
||||
expect(container.textContent).toContain("快速复制给 AI");
|
||||
expect(container.textContent).toContain("复制原始 JSON(debug)");
|
||||
expect(container.textContent).toContain("外部分析交接");
|
||||
|
||||
@@ -357,7 +357,7 @@ function buildMemoryPrefetchDiagnosticLines(
|
||||
`- 规则层:${prefetch.rules_source_paths.length} 个来源`,
|
||||
`- 工作层:${prefetch.working_memory_excerpt ? "已命中" : "未命中"}`,
|
||||
`- 持久层:${prefetch.durable_memories.length} 条`,
|
||||
`- Team 层:${prefetch.team_memory_entries.length} 条`,
|
||||
`- 任务影子层:${prefetch.team_memory_entries.length} 条`,
|
||||
`- 压缩层:${prefetch.latest_compaction ? "已命中" : "未命中"}`,
|
||||
];
|
||||
|
||||
@@ -393,13 +393,13 @@ function buildMemoryPrefetchDiagnosticLines(
|
||||
}
|
||||
if (prefetch.team_memory_entries.length > 0) {
|
||||
sections.push(
|
||||
`- Team 影子键:${prefetch.team_memory_entries
|
||||
`- 任务影子键:${prefetch.team_memory_entries
|
||||
.slice(0, 3)
|
||||
.map((entry) => entry.key)
|
||||
.join("|")}`,
|
||||
);
|
||||
sections.push(
|
||||
`- Team 影子详情:${prefetch.team_memory_entries
|
||||
`- 任务影子详情:${prefetch.team_memory_entries
|
||||
.slice(0, 3)
|
||||
.map(
|
||||
(entry) =>
|
||||
@@ -453,7 +453,7 @@ function resolveMemoryPrefetchPreviewChangeLabel(
|
||||
case "durable":
|
||||
return `长期记忆 ${change.previous || "无"} -> ${change.current || "无"}`;
|
||||
case "team":
|
||||
return `Team 影子 ${change.previous || "无"} -> ${change.current || "无"}`;
|
||||
return `任务影子 ${change.previous || "无"} -> ${change.current || "无"}`;
|
||||
case "compaction":
|
||||
return `压缩摘要 ${change.previous || "无"} -> ${change.current || "无"}`;
|
||||
case "user_message":
|
||||
@@ -574,7 +574,7 @@ function buildReliabilityDiagnosticText(params: {
|
||||
`- 模型:${diagnosticRuntimeContext?.model || "未知"}`,
|
||||
`- 执行策略:${diagnosticRuntimeContext?.executionStrategy || "未知"}`,
|
||||
`- 主题:${diagnosticRuntimeContext?.activeTheme || "未知"}`,
|
||||
`- 协作方案:${diagnosticRuntimeContext?.selectedTeamLabel || "未设置"}`,
|
||||
`- 任务方案:${diagnosticRuntimeContext?.selectedTeamLabel || "未设置"}`,
|
||||
`- 工作区根目录:${diagnosticRuntimeContext?.workingDir || "未知"}`,
|
||||
"",
|
||||
"### 当前状态",
|
||||
@@ -735,7 +735,7 @@ function buildReliabilityDiagnosticText(params: {
|
||||
}
|
||||
if (memoryPrefetchComparison.diff.changed) {
|
||||
sections.push(
|
||||
`- 层变化:规则 ${memoryPrefetchComparison.diff.layerChanges.rulesDelta >= 0 ? "+" : ""}${memoryPrefetchComparison.diff.layerChanges.rulesDelta}|工作 ${memoryPrefetchComparison.diff.layerChanges.workingChanged}|持久 ${memoryPrefetchComparison.diff.layerChanges.durableDelta >= 0 ? "+" : ""}${memoryPrefetchComparison.diff.layerChanges.durableDelta}|Team ${memoryPrefetchComparison.diff.layerChanges.teamDelta >= 0 ? "+" : ""}${memoryPrefetchComparison.diff.layerChanges.teamDelta}|压缩 ${memoryPrefetchComparison.diff.layerChanges.compactionChanged}`,
|
||||
`- 层变化:规则 ${memoryPrefetchComparison.diff.layerChanges.rulesDelta >= 0 ? "+" : ""}${memoryPrefetchComparison.diff.layerChanges.rulesDelta}|工作 ${memoryPrefetchComparison.diff.layerChanges.workingChanged}|持久 ${memoryPrefetchComparison.diff.layerChanges.durableDelta >= 0 ? "+" : ""}${memoryPrefetchComparison.diff.layerChanges.durableDelta}|任务影子 ${memoryPrefetchComparison.diff.layerChanges.teamDelta >= 0 ? "+" : ""}${memoryPrefetchComparison.diff.layerChanges.teamDelta}|压缩 ${memoryPrefetchComparison.diff.layerChanges.compactionChanged}`,
|
||||
);
|
||||
if (memoryPrefetchComparison.diff.previewChanges.length > 0) {
|
||||
sections.push(
|
||||
@@ -1238,7 +1238,7 @@ export const AgentThreadReliabilityPanel: React.FC<
|
||||
variant="outline"
|
||||
className="border-amber-200 bg-amber-50 text-amber-700"
|
||||
>
|
||||
compat 快速诊断
|
||||
线程级快速诊断
|
||||
</Badge>
|
||||
<Badge
|
||||
variant="outline"
|
||||
@@ -1294,11 +1294,11 @@ export const AgentThreadReliabilityPanel: React.FC<
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 rounded-xl border border-amber-200 bg-amber-50/80 px-3 py-2 text-[11px] leading-5 text-amber-900">
|
||||
当前入口属于 `compat` 线程级快速诊断,只覆盖当前 thread 的运行信号。
|
||||
正式交给外部模型分析时,请优先使用工作台“交接制品 →
|
||||
外部分析交接”的 `analysis-brief.md / analysis-context.json`
|
||||
主链;这里的“快速复制给 AI”只适合临时排障,“复制原始
|
||||
JSON(debug)”适合程序化分析、存档或二次处理。
|
||||
当前入口只覆盖当前 thread 的运行信号。正式交给外部模型分析时,
|
||||
请优先使用工作台“交接制品 → 外部分析交接”的
|
||||
`analysis-brief.md / analysis-context.json` 主链;这里的“快速复制给
|
||||
AI”只适合临时排障,“复制原始 JSON(debug)”适合程序化分析、
|
||||
存档或二次处理。
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid gap-2 md:grid-cols-3">
|
||||
@@ -1683,7 +1683,7 @@ export const AgentThreadReliabilityPanel: React.FC<
|
||||
variant="outline"
|
||||
className="border-sky-200 bg-white text-sky-700"
|
||||
>
|
||||
Team{" "}
|
||||
任务影子{" "}
|
||||
{memoryPrefetchComparison.diff.layerChanges.teamDelta > 0 ? "+" : ""}
|
||||
{memoryPrefetchComparison.diff.layerChanges.teamDelta}
|
||||
</Badge>
|
||||
|
||||
@@ -990,7 +990,7 @@ describe("AgentThreadTimeline", () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("真实协作成员 item 应支持查看协作详情", () => {
|
||||
it("真实子任务 item 应支持查看子任务详情", () => {
|
||||
const onOpenSubagentSession = vi.fn();
|
||||
const items: AgentThreadItem[] = [
|
||||
{
|
||||
@@ -1012,10 +1012,11 @@ describe("AgentThreadTimeline", () => {
|
||||
|
||||
expect(container.textContent).toContain("图片任务 1");
|
||||
expect(container.textContent).not.toContain("Image #1");
|
||||
expect(container.textContent).toContain("子任务:图片任务 1");
|
||||
|
||||
const button = Array.from(
|
||||
container.querySelectorAll<HTMLButtonElement>("button"),
|
||||
).find((element) => element.textContent?.includes("查看协作详情"));
|
||||
).find((element) => element.textContent?.includes("查看子任务详情"));
|
||||
|
||||
expect(button).toBeTruthy();
|
||||
|
||||
|
||||
@@ -831,13 +831,12 @@ function renderGroupItemDetails(
|
||||
|
||||
if (item.type === "subagent_activity") {
|
||||
const subagentSessionId = item.session_id?.trim();
|
||||
const displayTitle =
|
||||
resolveInternalImageTaskDisplayName(item.title) || "协作任务";
|
||||
const displayTitle = resolveInternalImageTaskDisplayName(item.title) || "子任务";
|
||||
|
||||
return (
|
||||
<SurfaceCard
|
||||
icon={Bot}
|
||||
title={`交给协作成员:${displayTitle}`}
|
||||
title={`子任务:${displayTitle}`}
|
||||
badge={
|
||||
<Badge variant={resolveStatusBadgeVariant(item.status)}>
|
||||
{resolveSubagentStatusLabel(item.status_label, item.status)}
|
||||
@@ -862,7 +861,7 @@ function renderGroupItemDetails(
|
||||
variant="outline"
|
||||
onClick={() => onOpenSubagentSession(subagentSessionId)}
|
||||
>
|
||||
查看协作详情
|
||||
查看子任务详情
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -1113,9 +1112,9 @@ function normalizeBlockPreviewLine(
|
||||
|
||||
if (
|
||||
kind === "subagent" &&
|
||||
!hasAnyPrefix(trimmed, ["分给协作成员", "协作成员"])
|
||||
!hasAnyPrefix(trimmed, ["分给子任务", "子任务", "分给协作成员", "协作成员"])
|
||||
) {
|
||||
return `分给协作成员处理 ${trimmed}`;
|
||||
return `分给子任务处理 ${trimmed}`;
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
@@ -1174,9 +1173,7 @@ function resolveBlockSummaryLines(block: AgentThreadOrderedBlock): string[] {
|
||||
}
|
||||
|
||||
if (block.kind === "subagent") {
|
||||
return [
|
||||
block.status === "completed" ? "协作成员处理完了" : "协作成员在处理",
|
||||
];
|
||||
return [block.status === "completed" ? "子任务已完成" : "子任务处理中"];
|
||||
}
|
||||
|
||||
if (block.kind === "other") {
|
||||
|
||||
@@ -216,12 +216,12 @@ function buildActivityView(item: AgentThreadItem): SessionActivityView | null {
|
||||
case "subagent_activity":
|
||||
return {
|
||||
id: item.id,
|
||||
title: item.title?.trim() || "子 Agent 活动",
|
||||
title: item.title?.trim() || "子任务活动",
|
||||
summary:
|
||||
shortenText(item.summary, 100) ||
|
||||
shortenText(item.role, 100) ||
|
||||
shortenText(item.model, 100) ||
|
||||
"协作成员正在推进任务。",
|
||||
"子任务正在推进当前任务。",
|
||||
timeLabel,
|
||||
statusLabel,
|
||||
tone,
|
||||
|
||||
@@ -1360,12 +1360,12 @@ describe("CanvasWorkbenchLayout", () => {
|
||||
renderPreview,
|
||||
teamView: {
|
||||
enabled: true,
|
||||
title: "Team Workbench",
|
||||
subtitle: "多成员实时协作",
|
||||
title: "任务工作台",
|
||||
subtitle: "任务进行时",
|
||||
badges: [
|
||||
{
|
||||
key: "team-runtime",
|
||||
label: "Team Workbench",
|
||||
label: "任务工作台",
|
||||
tone: "accent",
|
||||
},
|
||||
{
|
||||
@@ -1377,9 +1377,9 @@ describe("CanvasWorkbenchLayout", () => {
|
||||
summaryStats: [
|
||||
{
|
||||
key: "team-status",
|
||||
label: "协作状态",
|
||||
label: "任务状态",
|
||||
value: "处理中",
|
||||
detail: "2 位处理中,1 位排队中。",
|
||||
detail: "2 项处理中,1 项排队中。",
|
||||
tone: "accent",
|
||||
},
|
||||
],
|
||||
@@ -1401,7 +1401,7 @@ describe("CanvasWorkbenchLayout", () => {
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
container.querySelector(
|
||||
'button[aria-label="切换画布标签-Team Workbench"]',
|
||||
'button[aria-label="切换画布标签-任务工作台"]',
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(renderPreview).toHaveBeenCalled();
|
||||
@@ -1441,8 +1441,8 @@ describe("CanvasWorkbenchLayout", () => {
|
||||
renderPreview,
|
||||
teamView: {
|
||||
enabled: true,
|
||||
title: "Team Workbench",
|
||||
subtitle: "多成员实时协作",
|
||||
title: "任务工作台",
|
||||
subtitle: "任务进行时",
|
||||
autoFocusToken: 1,
|
||||
renderPreview: () => <div>unused-team-preview</div>,
|
||||
renderPanel: () => <div data-testid="team-panel">team-panel</div>,
|
||||
|
||||
@@ -1438,7 +1438,7 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
|
||||
}
|
||||
return {
|
||||
kind: "team-workbench",
|
||||
title: teamView.title || "Team Workbench",
|
||||
title: teamView.title || "任务工作台",
|
||||
};
|
||||
}, [teamView]);
|
||||
|
||||
@@ -1580,7 +1580,9 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
|
||||
{
|
||||
key: "team" as const,
|
||||
label:
|
||||
teamView.tabLabel?.trim() || teamView.title?.trim() || "team",
|
||||
teamView.tabLabel?.trim() ||
|
||||
teamView.title?.trim() ||
|
||||
"任务工作台",
|
||||
badge:
|
||||
teamView.tabBadge?.trim() ||
|
||||
teamView.triggerState?.label?.trim() ||
|
||||
@@ -2027,7 +2029,7 @@ export const CanvasWorkbenchLayout = memo(function CanvasWorkbenchLayout({
|
||||
<div data-testid="canvas-workbench-panel-team" className="p-5">
|
||||
<div className={WORKBENCH_MUTED_PANEL_CLASSNAME}>
|
||||
{teamView?.panelCopy?.emptyText ||
|
||||
"当前没有可展示的 Team Workbench。"}
|
||||
"当前没有可展示的任务工作台。"}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -299,7 +299,7 @@ describe("ChatSidebar", () => {
|
||||
expect(container.textContent).toContain("更早记录");
|
||||
});
|
||||
|
||||
it("任务协作和任务列表应处于同一滚动区域", () => {
|
||||
it("子任务和任务列表应处于同一滚动区域", () => {
|
||||
const container = renderSidebar({
|
||||
childSubagentSessions: [
|
||||
{
|
||||
@@ -325,14 +325,120 @@ describe("ChatSidebar", () => {
|
||||
expect(scrollArea).toBeTruthy();
|
||||
expect(teamSection).toBeTruthy();
|
||||
expect(scrollArea?.contains(teamSection)).toBe(true);
|
||||
expect(scrollArea?.textContent).toContain("任务协作");
|
||||
expect(scrollArea?.textContent).toContain("子任务");
|
||||
expect(scrollArea?.textContent).toContain(
|
||||
"这里展示当前任务的协作成员、当前任务和任务节点。",
|
||||
"这里优先展示正在处理的子任务,再回到当前任务和后续节点。",
|
||||
);
|
||||
expect(scrollArea?.textContent).toContain("任务一");
|
||||
});
|
||||
|
||||
it("点击任务协作的任务入口应收起顶部区块并滚动到任务列表", () => {
|
||||
it("父线程子任务应按状态优先级排序并前置当前焦点", () => {
|
||||
const container = renderSidebar({
|
||||
childSubagentSessions: [
|
||||
{
|
||||
id: "child-completed",
|
||||
name: "已完成代理",
|
||||
created_at: 1_742_288_400,
|
||||
updated_at: 1_742_288_560,
|
||||
session_type: "sub_agent",
|
||||
task_summary: "已经输出收尾结果。",
|
||||
role_hint: "writer",
|
||||
runtime_status: "completed",
|
||||
},
|
||||
{
|
||||
id: "child-running",
|
||||
name: "处理中代理",
|
||||
created_at: 1_742_288_390,
|
||||
updated_at: 1_742_288_500,
|
||||
session_type: "sub_agent",
|
||||
task_summary: "正在处理主线回归。",
|
||||
role_hint: "executor",
|
||||
runtime_status: "running",
|
||||
},
|
||||
{
|
||||
id: "child-queued",
|
||||
name: "待开始代理",
|
||||
created_at: 1_742_288_395,
|
||||
updated_at: 1_742_288_540,
|
||||
session_type: "sub_agent",
|
||||
task_summary: "等待前序任务完成后接手。",
|
||||
role_hint: "reviewer",
|
||||
runtime_status: "queued",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const cards = Array.from(
|
||||
container.querySelectorAll('[data-testid^="sidebar-subagent-session-"]'),
|
||||
);
|
||||
|
||||
expect(cards.map((card) => card.getAttribute("data-testid"))).toEqual([
|
||||
"sidebar-subagent-session-child-running",
|
||||
"sidebar-subagent-session-child-queued",
|
||||
"sidebar-subagent-session-child-completed",
|
||||
]);
|
||||
expect(cards[0]?.textContent).toContain("当前焦点");
|
||||
expect(cards[0]?.textContent).toContain("处理中代理");
|
||||
expect(cards[2]?.textContent).not.toContain("当前焦点");
|
||||
});
|
||||
|
||||
it("子线程并行子任务应按状态优先级排序并前置当前焦点", () => {
|
||||
const container = renderSidebar({
|
||||
topics: [
|
||||
{
|
||||
...defaultTopics[0],
|
||||
id: "child-1",
|
||||
title: "当前子任务",
|
||||
sourceSessionId: "child-1",
|
||||
},
|
||||
],
|
||||
currentTopicId: "child-1",
|
||||
subagentParentContext: {
|
||||
parent_session_id: "parent-1",
|
||||
parent_session_name: "主线程",
|
||||
role_hint: "implementer",
|
||||
task_summary: "对齐任务列表排序。",
|
||||
created_from_turn_id: "turn-42",
|
||||
sibling_subagent_sessions: [
|
||||
{
|
||||
id: "child-completed",
|
||||
name: "已完成代理",
|
||||
created_at: 1_742_288_400,
|
||||
updated_at: 1_742_288_560,
|
||||
session_type: "sub_agent",
|
||||
task_summary: "已经输出收尾结果。",
|
||||
role_hint: "writer",
|
||||
runtime_status: "completed",
|
||||
},
|
||||
{
|
||||
id: "child-running",
|
||||
name: "处理中代理",
|
||||
created_at: 1_742_288_390,
|
||||
updated_at: 1_742_288_500,
|
||||
session_type: "sub_agent",
|
||||
task_summary: "正在处理主线回归。",
|
||||
role_hint: "executor",
|
||||
runtime_status: "running",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const cards = Array.from(
|
||||
container.querySelectorAll('[data-testid^="sidebar-subagent-session-"]'),
|
||||
);
|
||||
|
||||
expect(cards.map((card) => card.getAttribute("data-testid"))).toEqual([
|
||||
"sidebar-subagent-session-child-running",
|
||||
"sidebar-subagent-session-child-completed",
|
||||
]);
|
||||
expect(cards[0]?.textContent).toContain("当前焦点");
|
||||
expect(container.textContent).toContain(
|
||||
"当前线程来自主任务,可直接返回主任务并切换其他子任务;正在处理的任务会排在前面。",
|
||||
);
|
||||
});
|
||||
|
||||
it("点击子任务入口应收起顶部区块并滚动到任务列表", () => {
|
||||
const container = renderSidebar({
|
||||
childSubagentSessions: [
|
||||
{
|
||||
@@ -373,7 +479,7 @@ describe("ChatSidebar", () => {
|
||||
block: "start",
|
||||
behavior: "smooth",
|
||||
});
|
||||
expect(container.textContent).toContain("已收起 · 1 个子代理 · 1 个处理中");
|
||||
expect(container.textContent).toContain("已收起 · 1 个子任务 · 1 个处理中");
|
||||
expect(container.textContent).not.toContain("代码审查代理");
|
||||
});
|
||||
|
||||
@@ -622,7 +728,7 @@ describe("ChatSidebar", () => {
|
||||
expect(onResumeTask).toHaveBeenCalledWith("topic-1", "user_action");
|
||||
});
|
||||
|
||||
it("父线程应在侧栏展示真实子代理并支持打开", () => {
|
||||
it("父线程应在侧栏展示真实子任务并支持打开", () => {
|
||||
const onOpenSubagentSession = vi.fn();
|
||||
const container = renderSidebar({
|
||||
childSubagentSessions: [
|
||||
@@ -650,9 +756,10 @@ describe("ChatSidebar", () => {
|
||||
onOpenSubagentSession,
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("任务协作");
|
||||
expect(container.textContent).toContain("子任务");
|
||||
expect(container.textContent).toContain("代码审查代理");
|
||||
expect(container.textContent).toContain("文档校对代理");
|
||||
expect(container.textContent).toContain("子任务");
|
||||
expect(container.textContent).toContain("处理中");
|
||||
expect(container.textContent).toContain("已完成");
|
||||
|
||||
@@ -668,7 +775,7 @@ describe("ChatSidebar", () => {
|
||||
expect(onOpenSubagentSession).toHaveBeenCalledWith("child-1");
|
||||
});
|
||||
|
||||
it("父线程任务协作区域应支持折叠和展开", () => {
|
||||
it("父线程子任务区域应支持折叠和展开", () => {
|
||||
const container = renderSidebar({
|
||||
childSubagentSessions: [
|
||||
{
|
||||
@@ -698,7 +805,7 @@ describe("ChatSidebar", () => {
|
||||
expect(container.textContent).toContain("文档校对代理");
|
||||
|
||||
const collapseButton = container.querySelector(
|
||||
'button[aria-label="收起任务协作"]',
|
||||
'button[aria-label="收起子任务"]',
|
||||
) as HTMLButtonElement | null;
|
||||
expect(collapseButton).toBeTruthy();
|
||||
|
||||
@@ -707,13 +814,13 @@ describe("ChatSidebar", () => {
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain(
|
||||
"已收起 · 2 个子代理 · 1 个处理中 · 1 个已完成",
|
||||
"已收起 · 2 个子任务 · 1 个处理中 · 1 个已完成",
|
||||
);
|
||||
expect(container.textContent).not.toContain("代码审查代理");
|
||||
expect(container.textContent).not.toContain("文档校对代理");
|
||||
|
||||
const expandButton = container.querySelector(
|
||||
'button[aria-label="展开任务协作"]',
|
||||
'button[aria-label="展开子任务"]',
|
||||
) as HTMLButtonElement | null;
|
||||
expect(expandButton).toBeTruthy();
|
||||
|
||||
@@ -725,7 +832,7 @@ describe("ChatSidebar", () => {
|
||||
expect(container.textContent).toContain("文档校对代理");
|
||||
});
|
||||
|
||||
it("父线程任务协作在子代理较多时应默认收起,并支持展开更多子代理", () => {
|
||||
it("父线程子任务较多时应默认收起,并支持展开更多子任务", () => {
|
||||
const container = renderSidebar({
|
||||
childSubagentSessions: [
|
||||
{
|
||||
@@ -772,7 +879,7 @@ describe("ChatSidebar", () => {
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain(
|
||||
"已收起 · 4 个子代理 · 2 个处理中 · 1 个稍后开始 · 1 个已完成",
|
||||
"已收起 · 4 个子任务 · 2 个处理中 · 1 个稍后开始 · 1 个已完成",
|
||||
);
|
||||
expect(container.textContent).not.toContain("代码审查代理");
|
||||
expect(container.textContent).not.toContain("文档校对代理");
|
||||
@@ -780,7 +887,7 @@ describe("ChatSidebar", () => {
|
||||
expect(container.textContent).not.toContain("回归验证代理");
|
||||
|
||||
const expandTeamButton = container.querySelector(
|
||||
'button[aria-label="展开任务协作"]',
|
||||
'button[aria-label="展开子任务"]',
|
||||
) as HTMLButtonElement | null;
|
||||
expect(expandTeamButton).toBeTruthy();
|
||||
|
||||
@@ -791,14 +898,14 @@ describe("ChatSidebar", () => {
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("代码审查代理");
|
||||
expect(container.textContent).toContain("文档校对代理");
|
||||
expect(container.textContent).toContain("回归验证代理");
|
||||
expect(container.textContent).toContain("数据整理代理");
|
||||
expect(container.textContent).not.toContain("回归验证代理");
|
||||
expect(container.textContent).toContain("展开剩余 1 个子代理");
|
||||
expect(container.textContent).not.toContain("文档校对代理");
|
||||
expect(container.textContent).toContain("展开剩余 1 个子任务");
|
||||
|
||||
const expandMoreButton = Array.from(
|
||||
container.querySelectorAll("button"),
|
||||
).find((element) => element.textContent?.includes("展开剩余 1 个子代理"));
|
||||
).find((element) => element.textContent?.includes("展开剩余 1 个子任务"));
|
||||
expect(expandMoreButton).toBeTruthy();
|
||||
|
||||
act(() => {
|
||||
@@ -808,11 +915,12 @@ describe("ChatSidebar", () => {
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("回归验证代理");
|
||||
expect(container.textContent).toContain("收起子代理列表");
|
||||
expect(container.textContent).toContain("文档校对代理");
|
||||
expect(container.textContent).toContain("收起子任务列表");
|
||||
|
||||
const collapseListButton = Array.from(
|
||||
container.querySelectorAll("button"),
|
||||
).find((element) => element.textContent?.includes("收起子代理列表"));
|
||||
).find((element) => element.textContent?.includes("收起子任务列表"));
|
||||
expect(collapseListButton).toBeTruthy();
|
||||
|
||||
act(() => {
|
||||
@@ -821,10 +929,11 @@ describe("ChatSidebar", () => {
|
||||
);
|
||||
});
|
||||
|
||||
expect(container.textContent).not.toContain("回归验证代理");
|
||||
expect(container.textContent).toContain("回归验证代理");
|
||||
expect(container.textContent).not.toContain("文档校对代理");
|
||||
});
|
||||
|
||||
it("子线程同级子代理较多时应默认收起任务协作", () => {
|
||||
it("子线程并行子任务较多时应默认收起子任务", () => {
|
||||
const container = renderSidebar({
|
||||
topics: [
|
||||
{
|
||||
@@ -877,14 +986,14 @@ describe("ChatSidebar", () => {
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain(
|
||||
"已收起 · 3 个同级子代理 · 1 个处理中 · 1 个稍后开始 · 1 个已完成",
|
||||
"已收起 · 3 个并行子任务 · 1 个处理中 · 1 个稍后开始 · 1 个已完成",
|
||||
);
|
||||
expect(container.textContent).not.toContain("研究代理");
|
||||
expect(container.textContent).not.toContain("验证代理");
|
||||
expect(container.textContent).not.toContain("文档代理");
|
||||
|
||||
const expandButton = container.querySelector(
|
||||
'button[aria-label="展开任务协作"]',
|
||||
'button[aria-label="展开子任务"]',
|
||||
) as HTMLButtonElement | null;
|
||||
expect(expandButton).toBeTruthy();
|
||||
|
||||
@@ -893,12 +1002,12 @@ describe("ChatSidebar", () => {
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("验证代理");
|
||||
expect(container.textContent).toContain("文档代理");
|
||||
expect(container.textContent).not.toContain("研究代理");
|
||||
expect(container.textContent).toContain("展开剩余 1 个同级子代理");
|
||||
expect(container.textContent).toContain("研究代理");
|
||||
expect(container.textContent).not.toContain("文档代理");
|
||||
expect(container.textContent).toContain("展开剩余 1 个并行子任务");
|
||||
});
|
||||
|
||||
it("子线程应展示父会话和同级子代理入口", () => {
|
||||
it("子线程应展示父会话和并行子任务入口", () => {
|
||||
const onOpenSubagentSession = vi.fn();
|
||||
const onReturnToParentSession = vi.fn();
|
||||
const container = renderSidebar({
|
||||
@@ -934,9 +1043,9 @@ describe("ChatSidebar", () => {
|
||||
onReturnToParentSession,
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("任务协作");
|
||||
expect(container.textContent).toContain("子任务");
|
||||
expect(container.textContent).toContain("主线程");
|
||||
expect(container.textContent).toContain("当前子代理");
|
||||
expect(container.textContent).toContain("当前子任务");
|
||||
expect(container.textContent).toContain("实现 team sidebar");
|
||||
expect(container.textContent).toContain("研究代理");
|
||||
expect(container.textContent).toContain("稍后开始");
|
||||
@@ -964,7 +1073,7 @@ describe("ChatSidebar", () => {
|
||||
expect(onOpenSubagentSession).toHaveBeenCalledWith("child-2");
|
||||
});
|
||||
|
||||
it("内部图片子代理标题应显示为用户文案", () => {
|
||||
it("内部图片子任务标题应显示为用户文案", () => {
|
||||
const container = renderSidebar({
|
||||
topics: [
|
||||
{
|
||||
|
||||
@@ -43,7 +43,7 @@ const RECENT_TASK_WINDOW_MS = 1000 * 60 * 60 * 24 * 3;
|
||||
const OLDER_TASKS_INITIAL_COUNT = 8;
|
||||
const TEAM_SECTION_INITIAL_CHILD_COUNT = 3;
|
||||
const TEAM_SECTION_INITIAL_SIBLING_COUNT = 2;
|
||||
const TEAM_SECTION_LABEL = "任务协作";
|
||||
const TEAM_SECTION_LABEL = "子任务";
|
||||
const PINNED_TASK_IDS_STORAGE_KEY = "lime_task_sidebar_pinned_ids";
|
||||
|
||||
const STATUS_META: Record<
|
||||
@@ -462,16 +462,67 @@ const TEAM_STATUS_SUMMARY_ORDER: Array<
|
||||
NonNullable<AsterSubagentSessionInfo["runtime_status"]> | "idle"
|
||||
> = ["running", "queued", "completed", "failed", "aborted", "closed", "idle"];
|
||||
|
||||
const SUBAGENT_TASK_PRIORITY: Record<
|
||||
NonNullable<AsterSubagentSessionInfo["runtime_status"]> | "idle",
|
||||
number
|
||||
> = {
|
||||
running: 0,
|
||||
queued: 1,
|
||||
failed: 2,
|
||||
aborted: 2,
|
||||
completed: 3,
|
||||
closed: 4,
|
||||
idle: 5,
|
||||
};
|
||||
|
||||
function resolveSubagentStatusMeta(
|
||||
status?: AsterSubagentSessionInfo["runtime_status"],
|
||||
) {
|
||||
return SUBAGENT_STATUS_META[status ?? "idle"];
|
||||
}
|
||||
|
||||
function sortSubagentSessionsByPriority(
|
||||
sessions: AsterSubagentSessionInfo[],
|
||||
): AsterSubagentSessionInfo[] {
|
||||
return [...sessions].sort((left, right) => {
|
||||
const leftPriority =
|
||||
SUBAGENT_TASK_PRIORITY[left.runtime_status ?? "idle"] ??
|
||||
SUBAGENT_TASK_PRIORITY.idle;
|
||||
const rightPriority =
|
||||
SUBAGENT_TASK_PRIORITY[right.runtime_status ?? "idle"] ??
|
||||
SUBAGENT_TASK_PRIORITY.idle;
|
||||
|
||||
if (leftPriority !== rightPriority) {
|
||||
return leftPriority - rightPriority;
|
||||
}
|
||||
|
||||
if (left.updated_at !== right.updated_at) {
|
||||
return right.updated_at - left.updated_at;
|
||||
}
|
||||
|
||||
if (left.created_at !== right.created_at) {
|
||||
return right.created_at - left.created_at;
|
||||
}
|
||||
|
||||
return left.id.localeCompare(right.id);
|
||||
});
|
||||
}
|
||||
|
||||
function shouldMarkSubagentAsFocus(
|
||||
session: AsterSubagentSessionInfo | undefined,
|
||||
): boolean {
|
||||
if (!session) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const status = session.runtime_status ?? "idle";
|
||||
return status !== "completed" && status !== "closed";
|
||||
}
|
||||
|
||||
function resolveSubagentSessionTypeLabel(value?: string) {
|
||||
switch (value) {
|
||||
case "sub_agent":
|
||||
return "子代理";
|
||||
return "子任务";
|
||||
case "fork":
|
||||
return "分支会话";
|
||||
case "user":
|
||||
@@ -650,16 +701,13 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
Record<string, string>
|
||||
>({});
|
||||
const sortedChildSubagentSessions = useMemo(
|
||||
() =>
|
||||
[...childSubagentSessions].sort(
|
||||
(left, right) => right.updated_at - left.updated_at,
|
||||
),
|
||||
() => sortSubagentSessionsByPriority(childSubagentSessions),
|
||||
[childSubagentSessions],
|
||||
);
|
||||
const siblingSubagentSessions = useMemo(
|
||||
() =>
|
||||
[...(subagentParentContext?.sibling_subagent_sessions ?? [])].sort(
|
||||
(left, right) => right.updated_at - left.updated_at,
|
||||
sortSubagentSessionsByPriority(
|
||||
subagentParentContext?.sibling_subagent_sessions ?? [],
|
||||
),
|
||||
[subagentParentContext?.sibling_subagent_sessions],
|
||||
);
|
||||
@@ -710,8 +758,8 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
buildCollapsedTeamSummary(
|
||||
teamSummarySessions,
|
||||
subagentParentContext
|
||||
? `${siblingSubagentSessions.length} 个同级子代理`
|
||||
: `${sortedChildSubagentSessions.length} 个子代理`,
|
||||
? `${siblingSubagentSessions.length} 个并行子任务`
|
||||
: `${sortedChildSubagentSessions.length} 个子任务`,
|
||||
),
|
||||
[
|
||||
siblingSubagentSessions,
|
||||
@@ -932,6 +980,7 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
const renderSubagentSessionCard = (
|
||||
session: AsterSubagentSessionInfo,
|
||||
options?: {
|
||||
focusLabel?: string;
|
||||
highlightCurrent?: boolean;
|
||||
subtitle?: string;
|
||||
},
|
||||
@@ -944,6 +993,7 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
<button
|
||||
key={session.id}
|
||||
type="button"
|
||||
data-testid={`sidebar-subagent-session-${session.id}`}
|
||||
onClick={() => {
|
||||
if (!canOpen) {
|
||||
return;
|
||||
@@ -952,7 +1002,7 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
}}
|
||||
className={cn(
|
||||
"w-full rounded-[20px] border px-3.5 py-3 text-left shadow-sm shadow-slate-950/5 transition",
|
||||
options?.highlightCurrent
|
||||
options?.highlightCurrent || options?.focusLabel
|
||||
? "border-slate-300 bg-white/98 ring-1 ring-slate-100 dark:border-white/15 dark:bg-white/10"
|
||||
: "border-slate-200/80 bg-white/86 hover:border-slate-300 hover:bg-white dark:border-white/10 dark:bg-white/5 dark:hover:bg-white/10",
|
||||
!canOpen ? "cursor-default" : "",
|
||||
@@ -966,8 +1016,13 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="truncate text-sm font-semibold text-slate-900 dark:text-slate-100">
|
||||
{resolveSidebarDisplayTitle(session.name, "未命名子代理")}
|
||||
{resolveSidebarDisplayTitle(session.name, "未命名子任务")}
|
||||
</div>
|
||||
{options?.focusLabel ? (
|
||||
<Badge className="border border-sky-200 bg-sky-50 text-sky-700">
|
||||
{options.focusLabel}
|
||||
</Badge>
|
||||
) : null}
|
||||
<Badge className={statusMeta.badgeClassName}>
|
||||
{statusMeta.label}
|
||||
</Badge>
|
||||
@@ -1159,16 +1214,16 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
{teamSectionCollapsed
|
||||
? collapsedTeamSummary
|
||||
: subagentParentContext
|
||||
? "当前线程来自主任务,可直接返回主任务并切换其他分工。"
|
||||
: "这里展示当前任务的协作成员、当前任务和任务节点。"}
|
||||
? "当前线程来自主任务,可直接返回主任务并切换其他子任务;正在处理的任务会排在前面。"
|
||||
: "这里优先展示正在处理的子任务,再回到当前任务和后续节点。"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<Badge className="border border-slate-200 bg-white text-slate-600 dark:border-white/10 dark:bg-white/10 dark:text-slate-200">
|
||||
{subagentParentContext
|
||||
? "子线程"
|
||||
: `${sortedChildSubagentSessions.length} 个子代理`}
|
||||
? "子任务线程"
|
||||
: `${sortedChildSubagentSessions.length} 个子任务`}
|
||||
</Badge>
|
||||
{hasAnyTasks ? (
|
||||
<button
|
||||
@@ -1235,7 +1290,7 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-5 text-slate-600 dark:text-slate-300">
|
||||
返回主线程,查看完整 team 视图和原始上下文。
|
||||
返回主线程,查看完整任务视图和原始上下文。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1243,14 +1298,14 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
|
||||
<div className="rounded-[20px] border border-slate-200/80 bg-white/86 px-3.5 py-3 shadow-sm shadow-slate-950/5 dark:border-white/10 dark:bg-white/5">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-sm font-semibold text-slate-900 dark:text-slate-100">
|
||||
{resolveSidebarDisplayTitle(
|
||||
currentTaskItem?.title,
|
||||
"当前子代理",
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm font-semibold text-slate-900 dark:text-slate-100">
|
||||
{resolveSidebarDisplayTitle(
|
||||
currentTaskItem?.title,
|
||||
"当前子任务",
|
||||
)}
|
||||
</div>
|
||||
<Badge className="border border-slate-900 bg-slate-900 text-white dark:border-white dark:bg-white dark:text-slate-900">
|
||||
当前子代理
|
||||
当前子任务
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2 text-[11px] text-slate-500 dark:text-slate-400">
|
||||
@@ -1276,14 +1331,19 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<div className="text-[11px] font-semibold tracking-[0.12em] text-slate-500">
|
||||
同级子代理
|
||||
并行子任务
|
||||
</div>
|
||||
<div className="text-[11px] text-slate-400">
|
||||
{siblingSubagentSessions.length} 个
|
||||
</div>
|
||||
</div>
|
||||
{visibleSiblingSubagentSessions.map((session) =>
|
||||
renderSubagentSessionCard(session),
|
||||
{visibleSiblingSubagentSessions.map((session, index) =>
|
||||
renderSubagentSessionCard(session, {
|
||||
focusLabel:
|
||||
index === 0 && shouldMarkSubagentAsFocus(session)
|
||||
? "当前焦点"
|
||||
: undefined,
|
||||
}),
|
||||
)}
|
||||
{hiddenSiblingSubagentCount > 0 ? (
|
||||
<button
|
||||
@@ -1291,7 +1351,7 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
onClick={() => setShowAllSiblingSubagents(true)}
|
||||
className="w-full rounded-2xl border border-dashed border-slate-200/80 bg-white/70 px-3 py-2 text-xs font-medium text-slate-500 transition hover:border-slate-300 hover:bg-white hover:text-slate-900 dark:border-white/10 dark:bg-white/5 dark:text-slate-300 dark:hover:bg-white/10 dark:hover:text-slate-100"
|
||||
>
|
||||
展开剩余 {hiddenSiblingSubagentCount} 个同级子代理
|
||||
展开剩余 {hiddenSiblingSubagentCount} 个并行子任务
|
||||
</button>
|
||||
) : null}
|
||||
{showAllSiblingSubagents &&
|
||||
@@ -1302,7 +1362,7 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
onClick={() => setShowAllSiblingSubagents(false)}
|
||||
className="w-full rounded-2xl border border-slate-200/80 bg-white/78 px-3 py-2 text-xs font-medium text-slate-500 transition hover:border-slate-300 hover:bg-white hover:text-slate-900 dark:border-white/10 dark:bg-white/5 dark:text-slate-300 dark:hover:bg-white/10 dark:hover:text-slate-100"
|
||||
>
|
||||
收起同级子代理列表
|
||||
收起并行子任务列表
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -1312,6 +1372,11 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
<div className="mt-4 space-y-2">
|
||||
{visibleChildSubagentSessions.map((session) =>
|
||||
renderSubagentSessionCard(session, {
|
||||
focusLabel:
|
||||
session.id === sortedChildSubagentSessions[0]?.id &&
|
||||
shouldMarkSubagentAsFocus(session)
|
||||
? "当前焦点"
|
||||
: undefined,
|
||||
highlightCurrent: session.id === currentTopicId,
|
||||
}),
|
||||
)}
|
||||
@@ -1321,7 +1386,7 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
onClick={() => setShowAllChildSubagents(true)}
|
||||
className="w-full rounded-2xl border border-dashed border-slate-200/80 bg-white/70 px-3 py-2 text-xs font-medium text-slate-500 transition hover:border-slate-300 hover:bg-white hover:text-slate-900 dark:border-white/10 dark:bg-white/5 dark:text-slate-300 dark:hover:bg-white/10 dark:hover:text-slate-100"
|
||||
>
|
||||
展开剩余 {hiddenChildSubagentCount} 个子代理
|
||||
展开剩余 {hiddenChildSubagentCount} 个子任务
|
||||
</button>
|
||||
) : null}
|
||||
{showAllChildSubagents &&
|
||||
@@ -1332,7 +1397,7 @@ export const ChatSidebar: React.FC<ChatSidebarProps> = ({
|
||||
onClick={() => setShowAllChildSubagents(false)}
|
||||
className="w-full rounded-2xl border border-slate-200/80 bg-white/78 px-3 py-2 text-xs font-medium text-slate-500 transition hover:border-slate-300 hover:bg-white hover:text-slate-900 dark:border-white/10 dark:bg-white/5 dark:text-slate-300 dark:hover:bg-white/10 dark:hover:text-slate-100"
|
||||
>
|
||||
收起子代理列表
|
||||
收起子任务列表
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -345,7 +345,7 @@ describe("EmptyState", () => {
|
||||
expect(container.textContent).not.toContain("快速启动");
|
||||
expect(container.textContent).not.toContain("生成配图");
|
||||
expect(container.textContent).not.toContain("浏览器任务起步");
|
||||
expect(container.textContent).not.toContain("Team 冒烟测试");
|
||||
expect(container.textContent).not.toContain("任务拆分冒烟测试");
|
||||
expect(container.textContent).not.toContain("推荐方案");
|
||||
|
||||
const recommendationTitles = Array.from(
|
||||
@@ -500,11 +500,11 @@ describe("EmptyState", () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const expectedLabels = ["我的方法", "持续流程", "多代理", "浏览器接入"];
|
||||
const expectedLabels = ["我的方法", "持续流程", "任务拆分", "浏览器接入"];
|
||||
const expectedAlts = [
|
||||
"方法能力卡占位图",
|
||||
"持续流程能力卡占位图",
|
||||
"多代理协作能力卡占位图",
|
||||
"任务拆分能力卡占位图",
|
||||
"浏览器接入能力卡占位图",
|
||||
];
|
||||
|
||||
@@ -975,7 +975,7 @@ describe("EmptyState", () => {
|
||||
container.querySelector('[data-testid="inputbar-plan-toggle"]'),
|
||||
).toBeNull();
|
||||
expect(
|
||||
container.querySelector('button[title="多代理偏好已关闭"]'),
|
||||
container.querySelector('button[title="任务拆分偏好已关闭"]'),
|
||||
).toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-testid="inputbar-access-mode-select"]'),
|
||||
@@ -1002,7 +1002,7 @@ describe("EmptyState", () => {
|
||||
) as HTMLButtonElement | null;
|
||||
expect(planButton).toBeTruthy();
|
||||
const subagentButton = container.querySelector(
|
||||
'button[title="多代理偏好已关闭"]',
|
||||
'button[title="任务拆分偏好已关闭"]',
|
||||
) as HTMLButtonElement | null;
|
||||
expect(subagentButton).toBeTruthy();
|
||||
const accessModeSelect = container.querySelector(
|
||||
|
||||
@@ -841,7 +841,7 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
{
|
||||
key: "agent-teams",
|
||||
eyebrow: "支撑能力",
|
||||
title: "多代理",
|
||||
title: "任务拆分",
|
||||
value: subagentEnabled
|
||||
? "当前任务支持分工推进"
|
||||
: "复杂任务可拆分并行推进",
|
||||
@@ -849,7 +849,7 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
|
||||
"当一个目标需要调研、方案和执行同时推进时,可以拆成多个分工后统一回收结论。",
|
||||
icon: <Workflow className="h-5 w-5" />,
|
||||
imageSrc: capabilityAgentTeamsPlaceholder,
|
||||
imageAlt: "多代理协作能力卡占位图",
|
||||
imageAlt: "任务拆分能力卡占位图",
|
||||
tone: "lime",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -458,7 +458,7 @@ describe("EmptyStateComposerPanel", () => {
|
||||
),
|
||||
).toBeNull();
|
||||
expect(
|
||||
container.querySelector('button[title="多代理偏好已关闭"]'),
|
||||
container.querySelector('button[title="任务拆分偏好已关闭"]'),
|
||||
).toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-testid="empty-state-advanced-toggle"]'),
|
||||
@@ -510,7 +510,7 @@ describe("EmptyStateComposerPanel", () => {
|
||||
|
||||
expect(enableButton).toBeNull();
|
||||
expect(
|
||||
container.querySelector('button[title="多代理偏好已关闭"]'),
|
||||
container.querySelector('button[title="任务拆分偏好已关闭"]'),
|
||||
).toBeNull();
|
||||
expect(
|
||||
container.querySelector('[data-testid="empty-state-advanced-toggle"]'),
|
||||
@@ -537,7 +537,7 @@ describe("EmptyStateComposerPanel", () => {
|
||||
expandAdvancedControls(container);
|
||||
|
||||
const enableButton = container.querySelector(
|
||||
'button[title="多代理偏好已关闭"]',
|
||||
'button[title="任务拆分偏好已关闭"]',
|
||||
) as HTMLButtonElement | null;
|
||||
|
||||
expect(enableButton).toBeTruthy();
|
||||
@@ -574,7 +574,7 @@ describe("EmptyStateComposerPanel", () => {
|
||||
).toBeTruthy();
|
||||
|
||||
const toggleButton = container.querySelector(
|
||||
'button[title="多代理偏好已开启"]',
|
||||
'button[title="任务拆分偏好已开启"]',
|
||||
) as HTMLButtonElement | null;
|
||||
|
||||
expect(toggleButton).toBeTruthy();
|
||||
@@ -591,7 +591,7 @@ describe("EmptyStateComposerPanel", () => {
|
||||
container.querySelector('[data-testid="empty-state-team-selector"]'),
|
||||
).toBeNull();
|
||||
expect(
|
||||
container.querySelector('button[title="多代理偏好已关闭"]'),
|
||||
container.querySelector('button[title="任务拆分偏好已关闭"]'),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -612,7 +612,7 @@ describe("EmptyStateComposerPanel", () => {
|
||||
),
|
||||
).toBeNull();
|
||||
expect(
|
||||
container.querySelector('button[title="多代理偏好已关闭"]'),
|
||||
container.querySelector('button[title="任务拆分偏好已关闭"]'),
|
||||
).toBeNull();
|
||||
expect(enableButton).toBeTruthy();
|
||||
expect(enableButton?.textContent).toContain("启用任务分工");
|
||||
|
||||
@@ -177,7 +177,7 @@ function renderSidebar(
|
||||
}
|
||||
|
||||
describe("GeneralWorkbenchSidebar", () => {
|
||||
it("任务进行时面板应突出当前任务并按状态优先级排序", () => {
|
||||
it("任务视图面板应突出当前任务并按状态优先级排序", () => {
|
||||
const { container } = renderSidebar({
|
||||
workflowSteps: [
|
||||
{ id: "done", title: "完成提纲", status: "completed" },
|
||||
@@ -188,7 +188,7 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
});
|
||||
|
||||
const workflowTab = container.querySelector(
|
||||
'button[aria-label="打开任务进行时面板"]',
|
||||
'button[aria-label="打开任务视图"]',
|
||||
) as HTMLButtonElement | null;
|
||||
expect(workflowTab).toBeTruthy();
|
||||
expect(workflowTab?.textContent).toContain("任务");
|
||||
@@ -199,15 +199,23 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
}
|
||||
|
||||
expect(container.textContent).toContain("任务工作台");
|
||||
expect(container.textContent).toContain("跟踪当前任务、后续节点与产物版本。");
|
||||
expect(container.textContent).toContain("任务进行时");
|
||||
expect(container.textContent).toContain("聚焦当前任务、后续节点与相关版本。");
|
||||
expect(container.textContent).toContain("任务视图");
|
||||
expect(container.textContent).toContain("当前任务");
|
||||
expect(container.textContent).toContain("撰写主稿");
|
||||
expect(container.textContent).toContain("已完成 1/4");
|
||||
expect(container.textContent).toMatch(/相关分支|相关版本/);
|
||||
|
||||
const stepNodes = Array.from(
|
||||
container.querySelectorAll('[data-testid="workflow-sidebar-step"]'),
|
||||
);
|
||||
const taskSection = container.querySelector(
|
||||
'[data-testid="workflow-sidebar-task-section"]',
|
||||
) as HTMLElement | null;
|
||||
const branchSection = container.querySelector(
|
||||
'[data-testid="workflow-sidebar-branch-section"]',
|
||||
) as HTMLElement | null;
|
||||
|
||||
expect(stepNodes).toHaveLength(4);
|
||||
expect(stepNodes.map((node) => node.getAttribute("data-status"))).toEqual([
|
||||
"active",
|
||||
@@ -215,6 +223,16 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
"pending",
|
||||
"completed",
|
||||
]);
|
||||
expect(taskSection).toBeTruthy();
|
||||
expect(branchSection).toBeTruthy();
|
||||
const taskSectionOrder =
|
||||
taskSection && branchSection
|
||||
? taskSection.compareDocumentPosition(branchSection) &
|
||||
Node.DOCUMENT_POSITION_FOLLOWING
|
||||
: 0;
|
||||
expect(
|
||||
taskSectionOrder,
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
it("传入折叠回调时应显示折叠按钮并可触发", () => {
|
||||
@@ -312,12 +330,22 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
|
||||
it("应展示新的双 tab 与紧凑上下文列表结构", () => {
|
||||
const { container } = renderSidebar();
|
||||
const shell = container.querySelector(
|
||||
'[data-testid="general-workbench-sidebar"]',
|
||||
) as HTMLElement | null;
|
||||
const header = container.querySelector(
|
||||
'[data-testid="general-workbench-sidebar-header"]',
|
||||
) as HTMLElement | null;
|
||||
const tabs = container.querySelector(
|
||||
'[data-testid="general-workbench-sidebar-tabs"]',
|
||||
) as HTMLElement | null;
|
||||
|
||||
expect(container.textContent).toContain("上下文管理");
|
||||
expect(
|
||||
container.querySelector('button[aria-label="打开上下文管理"]'),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
container.querySelector('button[aria-label="打开任务进行时面板"]'),
|
||||
container.querySelector('button[aria-label="打开任务视图"]'),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
container.querySelector('button[aria-label="打开执行日志"]'),
|
||||
@@ -326,6 +354,11 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
expect(container.textContent).toContain("上下文列表");
|
||||
expect(container.textContent).not.toContain("上下文概览");
|
||||
expect(container.textContent).not.toContain("项目资料");
|
||||
expect(shell?.className).toContain("bg-slate-50");
|
||||
expect(shell?.className).not.toContain("linear-gradient");
|
||||
expect(header?.className).toContain("bg-white");
|
||||
expect(header?.className).not.toContain("backdrop-blur");
|
||||
expect(tabs?.className).toContain("bg-slate-100");
|
||||
});
|
||||
|
||||
it("应展示新的搜索上下文输入区", () => {
|
||||
@@ -725,7 +758,7 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
});
|
||||
|
||||
const workflowTab = container.querySelector(
|
||||
'button[aria-label="打开任务进行时面板"]',
|
||||
'button[aria-label="打开任务视图"]',
|
||||
) as HTMLButtonElement | null;
|
||||
if (workflowTab) {
|
||||
act(() => {
|
||||
@@ -734,7 +767,7 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
}
|
||||
|
||||
const mergeButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(button) => button.textContent === "采纳到主稿",
|
||||
(button) => button.textContent === "采纳",
|
||||
);
|
||||
expect(mergeButton).toBeTruthy();
|
||||
if (mergeButton) {
|
||||
@@ -745,7 +778,7 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
expect(onSetBranchStatus).toHaveBeenCalledWith("topic-a", "merged");
|
||||
});
|
||||
|
||||
it("版本模式应展示产物版本语义", () => {
|
||||
it("版本模式应展示相关版本语义", () => {
|
||||
const onSetBranchStatus = vi.fn();
|
||||
const { container } = renderSidebar({
|
||||
branchMode: "version",
|
||||
@@ -753,7 +786,7 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
});
|
||||
|
||||
const workflowTab = container.querySelector(
|
||||
'button[aria-label="打开任务进行时面板"]',
|
||||
'button[aria-label="打开任务视图"]',
|
||||
) as HTMLButtonElement | null;
|
||||
if (workflowTab) {
|
||||
act(() => {
|
||||
@@ -761,8 +794,8 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
});
|
||||
}
|
||||
|
||||
expect(container.textContent).toContain("产物版本");
|
||||
expect(container.textContent).toContain("创建版本快照");
|
||||
expect(container.textContent).toContain("相关版本");
|
||||
expect(container.textContent).toContain("新增版本");
|
||||
|
||||
const setMainButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(button) => button.textContent === "设为主稿",
|
||||
@@ -780,7 +813,7 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
it("活动日志应展示后端闸门与运行标识", () => {
|
||||
const { container } = renderSidebar();
|
||||
const workflowTab = container.querySelector(
|
||||
'button[aria-label="打开任务进行时面板"]',
|
||||
'button[aria-label="打开任务视图"]',
|
||||
) as HTMLButtonElement | null;
|
||||
if (workflowTab) {
|
||||
act(() => {
|
||||
@@ -833,7 +866,7 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
});
|
||||
|
||||
const workflowTab = container.querySelector(
|
||||
'button[aria-label="打开任务进行时面板"]',
|
||||
'button[aria-label="打开任务视图"]',
|
||||
) as HTMLButtonElement | null;
|
||||
if (workflowTab) {
|
||||
act(() => {
|
||||
@@ -866,7 +899,7 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
const onViewRunDetail = vi.fn();
|
||||
const { container } = renderSidebar({ onViewRunDetail });
|
||||
const workflowTab = container.querySelector(
|
||||
'button[aria-label="打开任务进行时面板"]',
|
||||
'button[aria-label="打开任务视图"]',
|
||||
) as HTMLButtonElement | null;
|
||||
if (workflowTab) {
|
||||
act(() => {
|
||||
@@ -916,7 +949,7 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
});
|
||||
|
||||
const workflowTab = container.querySelector(
|
||||
'button[aria-label="打开任务进行时面板"]',
|
||||
'button[aria-label="打开任务视图"]',
|
||||
) as HTMLButtonElement | null;
|
||||
if (workflowTab) {
|
||||
act(() => {
|
||||
@@ -958,7 +991,7 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
});
|
||||
|
||||
const workflowTab = container.querySelector(
|
||||
'button[aria-label="打开任务进行时面板"]',
|
||||
'button[aria-label="打开任务视图"]',
|
||||
) as HTMLButtonElement | null;
|
||||
if (workflowTab) {
|
||||
act(() => {
|
||||
@@ -1033,7 +1066,7 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
});
|
||||
|
||||
const workflowTab = container.querySelector(
|
||||
'button[aria-label="打开任务进行时面板"]',
|
||||
'button[aria-label="打开任务视图"]',
|
||||
) as HTMLButtonElement | null;
|
||||
if (workflowTab) {
|
||||
act(() => {
|
||||
@@ -1139,7 +1172,7 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
});
|
||||
|
||||
const workflowTab = container.querySelector(
|
||||
'button[aria-label="打开任务进行时面板"]',
|
||||
'button[aria-label="打开任务视图"]',
|
||||
) as HTMLButtonElement | null;
|
||||
if (workflowTab) {
|
||||
act(() => {
|
||||
@@ -1222,7 +1255,7 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
});
|
||||
|
||||
const workflowTab = container.querySelector(
|
||||
'button[aria-label="打开任务进行时面板"]',
|
||||
'button[aria-label="打开任务视图"]',
|
||||
) as HTMLButtonElement | null;
|
||||
if (workflowTab) {
|
||||
act(() => {
|
||||
@@ -1231,6 +1264,15 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
}
|
||||
|
||||
expect(container.textContent).toContain("任务提交");
|
||||
const toggleCreationTasksButton = container.querySelector(
|
||||
"button[aria-label='切换任务提交记录']",
|
||||
) as HTMLButtonElement | null;
|
||||
expect(toggleCreationTasksButton).toBeTruthy();
|
||||
if (toggleCreationTasksButton) {
|
||||
act(() => {
|
||||
toggleCreationTasksButton.click();
|
||||
});
|
||||
}
|
||||
expect(container.textContent).toContain("配图生成");
|
||||
expect(container.textContent).toContain("排版优化");
|
||||
expect(container.textContent).toContain("本组 2 条");
|
||||
@@ -1279,7 +1321,7 @@ describe("GeneralWorkbenchSidebar", () => {
|
||||
});
|
||||
|
||||
const workflowTab = container.querySelector(
|
||||
'button[aria-label="打开任务进行时面板"]',
|
||||
'button[aria-label="打开任务视图"]',
|
||||
) as HTMLButtonElement | null;
|
||||
if (workflowTab) {
|
||||
act(() => {
|
||||
|
||||
@@ -27,7 +27,7 @@ function GeneralWorkbenchSidebarComponent({
|
||||
}),
|
||||
);
|
||||
const {
|
||||
branchCount,
|
||||
workflowCount,
|
||||
activeContextCount,
|
||||
visibleExecLogCount,
|
||||
contextPanelProps,
|
||||
@@ -43,7 +43,7 @@ function GeneralWorkbenchSidebarComponent({
|
||||
activeTab={activeTab}
|
||||
isVersionMode={isVersionMode}
|
||||
activeContextCount={activeContextCount}
|
||||
branchCount={branchCount}
|
||||
workflowCount={workflowCount}
|
||||
visibleExecLogCount={visibleExecLogCount}
|
||||
onTabChange={setActiveTab}
|
||||
onRequestCollapse={onRequestCollapse}
|
||||
|
||||
@@ -8,7 +8,7 @@ interface GeneralWorkbenchSidebarShellProps {
|
||||
activeTab: GeneralWorkbenchSidebarTab;
|
||||
isVersionMode: boolean;
|
||||
activeContextCount: number;
|
||||
branchCount: number;
|
||||
workflowCount: number;
|
||||
visibleExecLogCount: number;
|
||||
onTabChange: (tab: GeneralWorkbenchSidebarTab) => void;
|
||||
onRequestCollapse?: () => void;
|
||||
@@ -18,13 +18,13 @@ interface GeneralWorkbenchSidebarShellProps {
|
||||
}
|
||||
|
||||
const SIDEBAR_CONTAINER_CLASSNAME =
|
||||
"relative flex h-full w-[290px] min-w-[290px] flex-col border-r border-slate-200/80 bg-[linear-gradient(180deg,rgba(248,250,252,0.96)_0%,rgba(241,245,249,0.84)_100%)]";
|
||||
"relative flex h-full w-[290px] min-w-[290px] flex-col border-r border-slate-200 bg-slate-50";
|
||||
|
||||
const SIDEBAR_COLLAPSE_HANDLE_CLASSNAME =
|
||||
"absolute right-[-10px] top-1/2 z-[2] inline-flex h-[60px] w-4 -translate-y-1/2 items-center justify-center rounded-r-[10px] border border-l-0 border-slate-200/80 bg-white text-slate-500 transition-colors hover:bg-slate-50 hover:text-slate-900";
|
||||
"absolute right-[-10px] top-1/2 z-[2] inline-flex h-[60px] w-4 -translate-y-1/2 items-center justify-center rounded-r-[10px] border border-l-0 border-slate-200 bg-white text-slate-500 transition-colors hover:bg-slate-50 hover:text-slate-900";
|
||||
|
||||
const SIDEBAR_HEADER_CLASSNAME =
|
||||
"border-b border-slate-200/80 bg-white/88 px-4 py-4 backdrop-blur-sm";
|
||||
"border-b border-slate-200 bg-white px-4 py-4";
|
||||
|
||||
const SIDEBAR_HEADER_META_ROW_CLASSNAME =
|
||||
"flex items-center justify-between gap-2.5";
|
||||
@@ -33,7 +33,7 @@ const SIDEBAR_HEADER_ACTION_SLOT_CLASSNAME =
|
||||
"mt-[-2px] inline-flex shrink-0 items-center justify-center";
|
||||
|
||||
const SIDEBAR_EYEBROW_CLASSNAME =
|
||||
"text-[10px] font-semibold uppercase tracking-[0.06em] text-slate-500";
|
||||
"text-[10px] font-semibold text-slate-500";
|
||||
|
||||
const SIDEBAR_TITLE_CLASSNAME =
|
||||
"mt-2.5 text-base font-semibold leading-6 text-slate-900";
|
||||
@@ -42,7 +42,7 @@ const SIDEBAR_DESCRIPTION_CLASSNAME =
|
||||
"mt-1.5 text-[12px] leading-5 text-slate-500";
|
||||
|
||||
const SIDEBAR_TABS_CLASSNAME =
|
||||
"mt-3 flex gap-1.5 rounded-[18px] border border-slate-200/80 bg-slate-50/80 p-1";
|
||||
"mt-3 flex gap-1.5 rounded-[18px] border border-slate-200 bg-slate-100 p-1";
|
||||
|
||||
const SIDEBAR_TAB_LABEL_CLASSNAME = "min-w-0 truncate";
|
||||
|
||||
@@ -56,7 +56,7 @@ function getSidebarTabButtonClassName(active: boolean) {
|
||||
"flex h-[38px] min-w-0 flex-1 items-center justify-center gap-1 rounded-xl border px-2 text-[11px] font-semibold leading-none transition-colors",
|
||||
active
|
||||
? "border-slate-300 bg-white text-slate-900 shadow-sm shadow-slate-950/5"
|
||||
: "border-transparent text-slate-500 hover:border-slate-200/90 hover:bg-white/80 hover:text-slate-900",
|
||||
: "border-transparent text-slate-500 hover:border-slate-200 hover:bg-white hover:text-slate-900",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ function resolveSidebarTitle(
|
||||
return "上下文管理";
|
||||
}
|
||||
if (activeTab === "workflow") {
|
||||
return "任务进行时";
|
||||
return "任务视图";
|
||||
}
|
||||
return isVersionMode ? "版本记录" : "运行记录";
|
||||
}
|
||||
@@ -89,8 +89,8 @@ function resolveSidebarDescription(
|
||||
}
|
||||
if (activeTab === "workflow") {
|
||||
return isVersionMode
|
||||
? "跟踪当前任务、后续节点与产物版本。"
|
||||
: "跟踪当前任务、后续节点与分支进展。";
|
||||
? "聚焦当前任务、后续节点与相关版本。"
|
||||
: "聚焦当前任务、后续节点与相关分支。";
|
||||
}
|
||||
return "查看技能调用、工具输出与运行记录。";
|
||||
}
|
||||
@@ -99,7 +99,7 @@ export function GeneralWorkbenchSidebarShell({
|
||||
activeTab,
|
||||
isVersionMode,
|
||||
activeContextCount,
|
||||
branchCount,
|
||||
workflowCount,
|
||||
visibleExecLogCount,
|
||||
onTabChange,
|
||||
onRequestCollapse,
|
||||
@@ -112,7 +112,10 @@ export function GeneralWorkbenchSidebarShell({
|
||||
className={SIDEBAR_CONTAINER_CLASSNAME}
|
||||
data-testid="general-workbench-sidebar"
|
||||
>
|
||||
<div className={SIDEBAR_HEADER_CLASSNAME}>
|
||||
<div
|
||||
className={SIDEBAR_HEADER_CLASSNAME}
|
||||
data-testid="general-workbench-sidebar-header"
|
||||
>
|
||||
<div className={SIDEBAR_HEADER_META_ROW_CLASSNAME}>
|
||||
<div className={SIDEBAR_EYEBROW_CLASSNAME}>任务工作台</div>
|
||||
{headerActionSlot ? (
|
||||
@@ -130,7 +133,10 @@ export function GeneralWorkbenchSidebarShell({
|
||||
<div className={SIDEBAR_DESCRIPTION_CLASSNAME}>
|
||||
{resolveSidebarDescription(activeTab, isVersionMode)}
|
||||
</div>
|
||||
<div className={SIDEBAR_TABS_CLASSNAME}>
|
||||
<div
|
||||
className={SIDEBAR_TABS_CLASSNAME}
|
||||
data-testid="general-workbench-sidebar-tabs"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="打开上下文管理"
|
||||
@@ -147,8 +153,8 @@ export function GeneralWorkbenchSidebarShell({
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="打开任务进行时面板"
|
||||
title="任务进行时面板"
|
||||
aria-label="打开任务视图"
|
||||
title="任务视图"
|
||||
className={getSidebarTabButtonClassName(activeTab === "workflow")}
|
||||
onClick={() => onTabChange("workflow")}
|
||||
>
|
||||
@@ -156,7 +162,7 @@ export function GeneralWorkbenchSidebarShell({
|
||||
<span
|
||||
className={getSidebarTabCountClassName(activeTab === "workflow")}
|
||||
>
|
||||
{branchCount}
|
||||
{workflowCount}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
|
||||
@@ -77,30 +77,28 @@ interface GeneralWorkbenchWorkflowPanelProps {
|
||||
const WORKFLOW_SECTION_CLASSNAME = "border-b border-slate-200/70 px-4 py-3";
|
||||
|
||||
const WORKFLOW_SECTION_TITLE_CLASSNAME =
|
||||
"mb-2.5 flex items-center justify-between text-[11px] font-semibold uppercase tracking-[0.06em] text-slate-500";
|
||||
"mb-2.5 flex items-center justify-between text-[11px] font-semibold text-slate-500";
|
||||
|
||||
const WORKFLOW_SECTION_BADGE_CLASSNAME =
|
||||
"inline-flex min-h-4 min-w-4 items-center justify-center rounded-full border border-slate-200 bg-white px-1.5 text-[10px] font-semibold text-slate-500";
|
||||
|
||||
const WORKFLOW_NEW_TOPIC_BUTTON_CLASSNAME =
|
||||
"flex h-10 w-full items-center gap-2 rounded-[14px] border border-dashed border-slate-200/90 bg-white/90 px-3 text-sm font-medium text-slate-700 shadow-sm shadow-slate-950/5 transition-colors hover:border-slate-300 hover:bg-white hover:text-slate-900";
|
||||
|
||||
const WORKFLOW_PROGRESS_TEXT_CLASSNAME = "text-sm leading-5 text-slate-500";
|
||||
|
||||
const WORKFLOW_PROGRESS_BAR_CLASSNAME =
|
||||
"mt-2 h-2 overflow-hidden rounded-full bg-slate-200/80";
|
||||
|
||||
const WORKFLOW_PROGRESS_FILL_CLASSNAME =
|
||||
"h-full rounded-full bg-[linear-gradient(90deg,rgba(14,116,144,0.72)_0%,rgba(16,185,129,0.76)_100%)] transition-[width] duration-200";
|
||||
"inline-flex h-7 shrink-0 items-center gap-1 rounded-full border border-slate-200 bg-slate-50 px-2.5 text-[11px] font-medium text-slate-500 transition-colors hover:border-slate-300 hover:bg-white hover:text-slate-900";
|
||||
|
||||
const WORKFLOW_STEP_LIST_CLASSNAME = "mt-3 flex flex-col gap-2";
|
||||
|
||||
const WORKFLOW_STATUS_CARD_CLASSNAME =
|
||||
"mt-3 rounded-[16px] border border-slate-200/80 bg-white px-3 py-3 shadow-sm shadow-slate-950/5";
|
||||
const WORKFLOW_TASK_SUMMARY_CLASSNAME =
|
||||
"mt-2 rounded-[14px] border border-slate-200 bg-slate-50 px-3 py-2.5";
|
||||
|
||||
const WORKFLOW_TASK_SUMMARY_PILL_CLASSNAME =
|
||||
"inline-flex items-center rounded-full border border-slate-200 bg-white px-2 py-0.5 text-[10px] font-medium text-slate-500";
|
||||
|
||||
const TOGGLE_BUTTON_CLASSNAME =
|
||||
"inline-flex items-center text-slate-500 transition-colors hover:text-slate-900";
|
||||
|
||||
const WORKFLOW_INLINE_LABEL_CLASSNAME =
|
||||
"text-[10px] font-semibold text-slate-500";
|
||||
|
||||
function createDiv(baseClassName: string) {
|
||||
return function ClassedDiv({
|
||||
className,
|
||||
@@ -149,10 +147,10 @@ function BranchItem({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg border p-[7px]",
|
||||
"rounded-[14px] border px-3 py-2.5",
|
||||
$active
|
||||
? "border-sky-300/70 bg-sky-50/70"
|
||||
: "border-slate-200/80 bg-white",
|
||||
? "border-sky-200/80 bg-sky-50/40"
|
||||
: "border-slate-200/80 bg-slate-50/70",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -160,10 +158,10 @@ function BranchItem({
|
||||
);
|
||||
}
|
||||
|
||||
const BranchHead = createDiv("flex items-center gap-[5px]");
|
||||
const BranchHead = createDiv("flex items-start gap-2");
|
||||
|
||||
const BranchTitleButton = createButton(
|
||||
"flex-1 truncate border-0 bg-transparent p-0 text-left text-[11px] text-slate-900",
|
||||
"flex-1 truncate border-0 bg-transparent p-0 text-left text-[12px] font-medium leading-5 text-slate-900",
|
||||
);
|
||||
|
||||
function StatusBadge({
|
||||
@@ -176,14 +174,17 @@ function StatusBadge({
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex rounded-full px-1.5 py-0.5 text-[10px]",
|
||||
$status === "merged" && "bg-emerald-100 text-emerald-700",
|
||||
$status === "in_progress" && "bg-sky-100 text-sky-700",
|
||||
$status === "pending" && "bg-amber-100 text-amber-700",
|
||||
"inline-flex rounded-full border px-1.5 py-0.5 text-[10px] font-medium",
|
||||
$status === "merged" &&
|
||||
"border-emerald-200 bg-emerald-50 text-emerald-700",
|
||||
$status === "in_progress" &&
|
||||
"border-sky-200 bg-sky-50 text-sky-700",
|
||||
$status === "pending" &&
|
||||
"border-amber-200 bg-amber-50 text-amber-700",
|
||||
$status !== "merged" &&
|
||||
$status !== "in_progress" &&
|
||||
$status !== "pending" &&
|
||||
"bg-slate-100 text-slate-500",
|
||||
"border-slate-200 bg-slate-100 text-slate-500",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -191,14 +192,16 @@ function StatusBadge({
|
||||
);
|
||||
}
|
||||
|
||||
const ActionRow = createDiv("mt-1.5 flex gap-[5px]");
|
||||
const BranchMeta = createDiv("mt-1 text-[10px] leading-4 text-slate-500");
|
||||
|
||||
const ActionRow = createDiv("mt-2 flex flex-wrap gap-1.5");
|
||||
|
||||
const TinyButton = createButton(
|
||||
"rounded-md border border-slate-200 bg-white px-[7px] py-[3px] text-[11px] text-slate-700 transition-colors hover:border-slate-300 hover:bg-slate-50 hover:text-slate-900",
|
||||
"rounded-full border border-slate-200 bg-white px-2 py-0.5 text-[10px] font-medium text-slate-500 transition-colors hover:border-slate-300 hover:bg-slate-50 hover:text-slate-900",
|
||||
);
|
||||
|
||||
const DeleteButton = createButton(
|
||||
"rounded p-0.5 text-slate-500 transition-colors hover:bg-rose-50 hover:text-rose-600",
|
||||
"rounded-full p-1 text-slate-400 transition-colors hover:bg-rose-50 hover:text-rose-600",
|
||||
);
|
||||
|
||||
const ActivityList = createDiv("flex flex-col gap-[5px]");
|
||||
@@ -259,15 +262,14 @@ const RunDetailActionButton = createButton(
|
||||
|
||||
function getWorkflowStepRowClassName(status: StepStatus) {
|
||||
return cn(
|
||||
"flex items-start gap-2 rounded-[12px] border px-2.5 py-2 text-sm leading-5",
|
||||
status === "completed" &&
|
||||
"border-emerald-200 bg-emerald-50/80 text-slate-900",
|
||||
status === "error" && "border-rose-200 bg-rose-50/80 text-slate-900",
|
||||
status === "active" && "border-sky-200 bg-sky-50/80 text-slate-900",
|
||||
"flex items-start gap-2.5 rounded-[14px] border bg-white px-3 py-2.5 text-sm leading-5 shadow-sm shadow-slate-950/5",
|
||||
status === "completed" && "border-slate-200 text-slate-700",
|
||||
status === "error" && "border-rose-200 text-slate-900",
|
||||
status === "active" && "border-sky-200 text-slate-900",
|
||||
status !== "completed" &&
|
||||
status !== "error" &&
|
||||
status !== "active" &&
|
||||
"border-slate-200/80 bg-slate-50/70 text-slate-500",
|
||||
"border-slate-200 text-slate-600",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -286,12 +288,24 @@ function getStepIcon(status: StepStatus) {
|
||||
|
||||
function getStatusBadgeClassName(status: StepStatus) {
|
||||
return cn(
|
||||
"inline-flex rounded-full px-2 py-0.5 text-[10px] font-semibold",
|
||||
status === "completed" && "bg-emerald-100 text-emerald-700",
|
||||
status === "error" && "bg-rose-100 text-rose-700",
|
||||
status === "active" && "bg-sky-100 text-sky-700",
|
||||
status === "pending" && "bg-amber-100 text-amber-700",
|
||||
status === "skipped" && "bg-slate-100 text-slate-500",
|
||||
"inline-flex rounded-full border px-2 py-0.5 text-[10px] font-semibold",
|
||||
status === "completed" && "border-emerald-200 bg-emerald-50 text-emerald-700",
|
||||
status === "error" && "border-rose-200 bg-rose-50 text-rose-700",
|
||||
status === "active" && "border-sky-200 bg-sky-50 text-sky-700",
|
||||
status === "pending" && "border-amber-200 bg-amber-50 text-amber-700",
|
||||
status === "skipped" && "border-slate-200 bg-slate-50 text-slate-500",
|
||||
);
|
||||
}
|
||||
|
||||
function getWorkflowStepIconClassName(status: StepStatus) {
|
||||
return cn(
|
||||
status === "completed" && "text-emerald-600",
|
||||
status === "error" && "text-rose-600",
|
||||
status === "active" && "text-sky-600",
|
||||
status !== "completed" &&
|
||||
status !== "error" &&
|
||||
status !== "active" &&
|
||||
"text-slate-400",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -302,6 +316,53 @@ function getBranchStatusText(status: TopicBranchStatus): string {
|
||||
return "备选";
|
||||
}
|
||||
|
||||
function getBranchSectionTitle(isVersionMode: boolean): string {
|
||||
return isVersionMode ? "相关版本" : "相关分支";
|
||||
}
|
||||
|
||||
function getBranchCreateLabel(isVersionMode: boolean): string {
|
||||
return isVersionMode ? "新增版本" : "新增分支";
|
||||
}
|
||||
|
||||
function getBranchPrimaryActionLabel(isVersionMode: boolean): string {
|
||||
return isVersionMode ? "设为主稿" : "采纳";
|
||||
}
|
||||
|
||||
function getBranchSecondaryActionLabel(isVersionMode: boolean): string {
|
||||
return isVersionMode ? "待评审" : "待决策";
|
||||
}
|
||||
|
||||
function getEmptyBranchText(isVersionMode: boolean): string {
|
||||
return isVersionMode
|
||||
? "暂无相关版本,当前任务还没有沉淀出可切换记录"
|
||||
: "暂无相关分支,当前任务还在主线上推进";
|
||||
}
|
||||
|
||||
function getBranchMetaText(
|
||||
item: TopicBranchItem,
|
||||
isVersionMode: boolean,
|
||||
): string {
|
||||
if (item.isCurrent) {
|
||||
return isVersionMode
|
||||
? "当前任务正在围绕这一版内容继续推进"
|
||||
: "当前任务正在围绕这条分支继续推进";
|
||||
}
|
||||
if (item.status === "merged") {
|
||||
return isVersionMode ? "这版内容已经收进主稿" : "这条分支已经并入主稿";
|
||||
}
|
||||
if (item.status === "pending") {
|
||||
return isVersionMode
|
||||
? "等待评审后再决定是否继续推进"
|
||||
: "等待决策后再决定是否继续推进";
|
||||
}
|
||||
if (item.status === "candidate") {
|
||||
return isVersionMode ? "保留为可回看的候选版本" : "保留为可回看的候选分支";
|
||||
}
|
||||
return isVersionMode
|
||||
? "记录一版正在推进中的内容"
|
||||
: "记录一条正在推进中的相关分支";
|
||||
}
|
||||
|
||||
function formatGateLabel(
|
||||
gateKey?: SidebarActivityLog["gateKey"],
|
||||
): string | null {
|
||||
@@ -482,6 +543,23 @@ function GeneralWorkbenchWorkflowPanelComponent({
|
||||
const currentWorkflowStep = workflowSnapshot.leadingStep;
|
||||
const remainingSteps = workflowSnapshot.remainingCount;
|
||||
const sortedWorkflowSteps = workflowSnapshot.sortedSteps;
|
||||
const sortedBranchItems = [...branchItems].sort((left, right) => {
|
||||
if (left.isCurrent !== right.isCurrent) {
|
||||
return left.isCurrent ? -1 : 1;
|
||||
}
|
||||
const statusPriority: Record<TopicBranchStatus, number> = {
|
||||
in_progress: 0,
|
||||
pending: 1,
|
||||
candidate: 2,
|
||||
merged: 3,
|
||||
};
|
||||
const statusDiff =
|
||||
statusPriority[left.status] - statusPriority[right.status];
|
||||
if (statusDiff !== 0) {
|
||||
return statusDiff;
|
||||
}
|
||||
return left.title.localeCompare(right.title, "zh-CN");
|
||||
});
|
||||
const workflowSummaryText = buildWorkflowSummaryText({
|
||||
leadingStep: currentWorkflowStep,
|
||||
remainingCount: remainingSteps,
|
||||
@@ -491,105 +569,86 @@ function GeneralWorkbenchWorkflowPanelComponent({
|
||||
completedCount: completedSteps,
|
||||
totalCount: workflowSteps.length,
|
||||
});
|
||||
const branchSectionTitle = getBranchSectionTitle(isVersionMode);
|
||||
const branchCreateLabel = getBranchCreateLabel(isVersionMode);
|
||||
const branchPrimaryActionLabel = getBranchPrimaryActionLabel(isVersionMode);
|
||||
const branchSecondaryActionLabel = getBranchSecondaryActionLabel(isVersionMode);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className={cn(WORKFLOW_SECTION_CLASSNAME, "relative z-10")}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={WORKFLOW_NEW_TOPIC_BUTTON_CLASSNAME}
|
||||
>
|
||||
<Plus size={14} />
|
||||
{isVersionMode ? "创建版本快照" : "新建分支任务"}
|
||||
<ChevronDown size={12} className="ml-auto" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" style={{ width: "260px" }}>
|
||||
<DropdownMenuItem onClick={onNewTopic}>
|
||||
<GitBranch size={14} />
|
||||
<span>{isVersionMode ? "创建版本快照" : "新建分支任务"}</span>
|
||||
</DropdownMenuItem>
|
||||
{onAddImage ? (
|
||||
<DropdownMenuItem onClick={onAddImage}>
|
||||
<ImageIcon size={14} />
|
||||
<span>添加图片</span>
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{onImportDocument ? (
|
||||
<DropdownMenuItem onClick={onImportDocument}>
|
||||
<FileText size={14} />
|
||||
<span>导入文稿</span>
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</section>
|
||||
|
||||
<section className={WORKFLOW_SECTION_CLASSNAME}>
|
||||
<section
|
||||
className={WORKFLOW_SECTION_CLASSNAME}
|
||||
data-testid="workflow-sidebar-task-section"
|
||||
>
|
||||
<div className={WORKFLOW_SECTION_TITLE_CLASSNAME}>
|
||||
<span>任务进行时</span>
|
||||
<span>任务视图</span>
|
||||
<span className={WORKFLOW_SECTION_BADGE_CLASSNAME}>
|
||||
{remainingSteps}
|
||||
</span>
|
||||
</div>
|
||||
<div className={WORKFLOW_STATUS_CARD_CLASSNAME}>
|
||||
<div className="flex items-start gap-3">
|
||||
<div
|
||||
<div
|
||||
className={WORKFLOW_TASK_SUMMARY_CLASSNAME}
|
||||
data-testid="workflow-sidebar-task-summary"
|
||||
>
|
||||
<div className="flex items-start gap-2.5">
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full",
|
||||
currentWorkflowStep?.status === "error" &&
|
||||
"bg-rose-100 text-rose-600",
|
||||
currentWorkflowStep?.status === "pending" &&
|
||||
"bg-amber-100 text-amber-700",
|
||||
(!currentWorkflowStep ||
|
||||
currentWorkflowStep.status === "active" ||
|
||||
currentWorkflowStep.status === "completed") &&
|
||||
"bg-sky-100 text-sky-700",
|
||||
"mt-0.5",
|
||||
getWorkflowStepIconClassName(
|
||||
currentWorkflowStep?.status ?? "active",
|
||||
),
|
||||
)}
|
||||
>
|
||||
{getStepIcon(currentWorkflowStep?.status ?? "active")}
|
||||
</div>
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-[11px] font-semibold text-slate-500">
|
||||
当前任务
|
||||
</div>
|
||||
<div
|
||||
className="mt-1 text-sm font-semibold leading-5 text-slate-900"
|
||||
data-testid="workflow-sidebar-current-step"
|
||||
>
|
||||
{currentWorkflowStep?.title || "当前流程已完成"}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className={WORKFLOW_INLINE_LABEL_CLASSNAME}>
|
||||
当前任务
|
||||
</span>
|
||||
<span
|
||||
className="break-words text-sm font-semibold leading-5 text-slate-900"
|
||||
data-testid="workflow-sidebar-current-step"
|
||||
>
|
||||
{currentWorkflowStep?.title || "当前流程已完成"}
|
||||
</span>
|
||||
<span
|
||||
className={getStatusBadgeClassName(
|
||||
currentWorkflowStep?.status ?? "completed",
|
||||
)}
|
||||
>
|
||||
{getWorkflowStatusLabel(
|
||||
currentWorkflowStep?.status ?? "completed",
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1 text-[11px] leading-5 text-slate-500">
|
||||
{workflowSummaryText}
|
||||
</div>
|
||||
<div className="mt-1 text-[11px] leading-5 text-slate-400">
|
||||
{workflowProgressLabel}
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2">
|
||||
<span className={WORKFLOW_TASK_SUMMARY_PILL_CLASSNAME}>
|
||||
{workflowProgressLabel}
|
||||
</span>
|
||||
<span className={WORKFLOW_TASK_SUMMARY_PILL_CLASSNAME}>
|
||||
{remainingSteps > 0
|
||||
? `剩余 ${remainingSteps} 项待处理`
|
||||
: "当前流程已全部完成"}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1 text-[10px] text-slate-400">
|
||||
<span className="inline-flex h-1 w-14 overflow-hidden rounded-full bg-slate-200">
|
||||
<span
|
||||
className="h-full rounded-full bg-sky-500/70 transition-[width] duration-200"
|
||||
style={{
|
||||
width: `${Math.max(0, Math.min(100, progressPercent))}%`,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
{Math.max(0, Math.min(100, Math.round(progressPercent)))}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
className={getStatusBadgeClassName(
|
||||
currentWorkflowStep?.status ?? "completed",
|
||||
)}
|
||||
>
|
||||
{getWorkflowStatusLabel(currentWorkflowStep?.status ?? "completed")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={cn(WORKFLOW_PROGRESS_TEXT_CLASSNAME, "mt-3")}>
|
||||
{remainingSteps > 0
|
||||
? `剩余 ${remainingSteps} 项待处理`
|
||||
: "当前流程已全部完成"}
|
||||
</div>
|
||||
<div className={WORKFLOW_PROGRESS_BAR_CLASSNAME}>
|
||||
<div
|
||||
className={WORKFLOW_PROGRESS_FILL_CLASSNAME}
|
||||
style={{
|
||||
width: `${Math.max(0, Math.min(100, progressPercent))}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={WORKFLOW_STEP_LIST_CLASSNAME}>
|
||||
{sortedWorkflowSteps.map((step) => (
|
||||
<div
|
||||
@@ -598,7 +657,11 @@ function GeneralWorkbenchWorkflowPanelComponent({
|
||||
data-testid="workflow-sidebar-step"
|
||||
data-status={step.status}
|
||||
>
|
||||
<span className="mt-0.5">{getStepIcon(step.status)}</span>
|
||||
<span
|
||||
className={cn("mt-0.5", getWorkflowStepIconClassName(step.status))}
|
||||
>
|
||||
{getStepIcon(step.status)}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="break-words text-sm leading-5">{step.title}</div>
|
||||
</div>
|
||||
@@ -610,50 +673,94 @@ function GeneralWorkbenchWorkflowPanelComponent({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className={WORKFLOW_SECTION_CLASSNAME}>
|
||||
<section
|
||||
className={cn(WORKFLOW_SECTION_CLASSNAME, "relative z-10")}
|
||||
data-testid="workflow-sidebar-branch-section"
|
||||
>
|
||||
<div className={WORKFLOW_SECTION_TITLE_CLASSNAME}>
|
||||
<span>{isVersionMode ? "产物版本" : "篇内分支"}</span>
|
||||
<span className={WORKFLOW_SECTION_BADGE_CLASSNAME}>
|
||||
{branchItems.length}
|
||||
<span>{branchSectionTitle}</span>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<span className={WORKFLOW_SECTION_BADGE_CLASSNAME}>
|
||||
{branchItems.length}
|
||||
</span>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={WORKFLOW_NEW_TOPIC_BUTTON_CLASSNAME}
|
||||
>
|
||||
<Plus size={13} />
|
||||
{branchCreateLabel}
|
||||
<ChevronDown size={11} />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" style={{ width: "260px" }}>
|
||||
<DropdownMenuItem onClick={onNewTopic}>
|
||||
<GitBranch size={14} />
|
||||
<span>{branchCreateLabel}</span>
|
||||
</DropdownMenuItem>
|
||||
{onAddImage ? (
|
||||
<DropdownMenuItem onClick={onAddImage}>
|
||||
<ImageIcon size={14} />
|
||||
<span>添加图片</span>
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{onImportDocument ? (
|
||||
<DropdownMenuItem onClick={onImportDocument}>
|
||||
<FileText size={14} />
|
||||
<span>导入文稿</span>
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</span>
|
||||
</div>
|
||||
<BranchList className="custom-scrollbar">
|
||||
{branchItems.length === 0 ? (
|
||||
<ActivityMeta>
|
||||
{isVersionMode
|
||||
? "暂无文稿版本,先生成或创建快照"
|
||||
: "暂无分支任务"}
|
||||
</ActivityMeta>
|
||||
<ActivityMeta>{getEmptyBranchText(isVersionMode)}</ActivityMeta>
|
||||
) : (
|
||||
branchItems.map((item) => (
|
||||
sortedBranchItems.map((item) => (
|
||||
<BranchItem key={item.id} $active={item.isCurrent}>
|
||||
<BranchHead>
|
||||
<GitBranch size={13} />
|
||||
<BranchTitleButton onClick={() => onSwitchTopic(item.id)}>
|
||||
{item.title}
|
||||
</BranchTitleButton>
|
||||
<StatusBadge $status={item.status}>
|
||||
{getBranchStatusText(item.status)}
|
||||
</StatusBadge>
|
||||
{!isVersionMode ? (
|
||||
<DeleteButton
|
||||
onClick={() => onDeleteTopic(item.id)}
|
||||
aria-label="删除分支"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</DeleteButton>
|
||||
) : null}
|
||||
<GitBranch
|
||||
size={13}
|
||||
className={cn(
|
||||
"mt-0.5 shrink-0",
|
||||
item.isCurrent ? "text-sky-600" : "text-slate-400",
|
||||
)}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-start gap-2">
|
||||
<BranchTitleButton onClick={() => onSwitchTopic(item.id)}>
|
||||
{item.title}
|
||||
</BranchTitleButton>
|
||||
<StatusBadge $status={item.status}>
|
||||
{item.isCurrent
|
||||
? "当前焦点"
|
||||
: getBranchStatusText(item.status)}
|
||||
</StatusBadge>
|
||||
{!isVersionMode ? (
|
||||
<DeleteButton
|
||||
onClick={() => onDeleteTopic(item.id)}
|
||||
aria-label="删除分支"
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</DeleteButton>
|
||||
) : null}
|
||||
</div>
|
||||
<BranchMeta>{getBranchMetaText(item, isVersionMode)}</BranchMeta>
|
||||
</div>
|
||||
</BranchHead>
|
||||
<ActionRow>
|
||||
<TinyButton
|
||||
onClick={() => onSetBranchStatus(item.id, "merged")}
|
||||
>
|
||||
{isVersionMode ? "设为主稿" : "采纳到主稿"}
|
||||
{branchPrimaryActionLabel}
|
||||
</TinyButton>
|
||||
<TinyButton
|
||||
onClick={() => onSetBranchStatus(item.id, "pending")}
|
||||
>
|
||||
{isVersionMode ? "标记待评审" : "标记待决策"}
|
||||
{branchSecondaryActionLabel}
|
||||
</TinyButton>
|
||||
</ActionRow>
|
||||
</BranchItem>
|
||||
|
||||
@@ -102,15 +102,6 @@ function renderPanel(
|
||||
root.render(
|
||||
<HarnessStatusPanel
|
||||
harnessState={createHarnessState()}
|
||||
compatSubagentRuntime={{
|
||||
isRunning: false,
|
||||
progress: null,
|
||||
result: null,
|
||||
error: null,
|
||||
summary: null,
|
||||
recentActivity: [],
|
||||
hasSignals: false,
|
||||
}}
|
||||
environment={{
|
||||
skillsCount: 2,
|
||||
skillNames: ["read_file", "write_todos"],
|
||||
@@ -1319,7 +1310,7 @@ describe("HarnessStatusPanel", () => {
|
||||
expect(document.body.textContent).toContain("429 rate limit");
|
||||
});
|
||||
|
||||
it("存在 selectedTeam 时应在工作台展示当前协作设置", () => {
|
||||
it("存在 selectedTeam 时应在工作台展示当前任务分工", () => {
|
||||
renderPanel({
|
||||
selectedTeamLabel: "前端联调团队",
|
||||
selectedTeamSummary: "分析、实现、验证三段式推进。",
|
||||
@@ -1335,8 +1326,8 @@ describe("HarnessStatusPanel", () => {
|
||||
],
|
||||
});
|
||||
|
||||
expect(document.body.textContent).toContain("协作设置");
|
||||
expect(document.body.textContent).toContain("当前协作设置");
|
||||
expect(document.body.textContent).toContain("任务分工");
|
||||
expect(document.body.textContent).toContain("当前任务分工");
|
||||
expect(document.body.textContent).toContain("前端联调团队");
|
||||
expect(document.body.textContent).toContain("分析、实现、验证三段式推进。");
|
||||
expect(document.body.textContent).toContain("模板 code-explorer");
|
||||
@@ -1344,7 +1335,7 @@ describe("HarnessStatusPanel", () => {
|
||||
expect(document.body.textContent).toContain("repo-exploration");
|
||||
});
|
||||
|
||||
it("存在真实 child session 时应优先展示协作会话摘要,并将旧 scheduler 降级为兼容轨迹", () => {
|
||||
it("存在真实 child session 时应优先展示子任务摘要", () => {
|
||||
renderPanel({
|
||||
childSubagentSessions: [
|
||||
{
|
||||
@@ -1370,40 +1361,15 @@ describe("HarnessStatusPanel", () => {
|
||||
role_hint: "executor",
|
||||
},
|
||||
],
|
||||
compatSubagentRuntime: {
|
||||
isRunning: true,
|
||||
progress: {
|
||||
total: 2,
|
||||
completed: 1,
|
||||
failed: 0,
|
||||
running: 1,
|
||||
pending: 0,
|
||||
skipped: 0,
|
||||
cancelled: false,
|
||||
currentTasks: ["legacy-task-1"],
|
||||
percentage: 50,
|
||||
},
|
||||
result: null,
|
||||
error: null,
|
||||
summary: "正在执行:legacy-task-1",
|
||||
recentActivity: [
|
||||
{
|
||||
id: "compat:1:started",
|
||||
summary: "开始调度 2 个子任务",
|
||||
},
|
||||
],
|
||||
hasSignals: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(document.body.textContent).toContain("任务进行中");
|
||||
expect(document.body.textContent).toContain("协作会话");
|
||||
expect(document.body.textContent).toContain("当前协作会话");
|
||||
expect(document.body.textContent).toContain("实时协作会话");
|
||||
expect(document.body.textContent).toContain("兼容模式");
|
||||
expect(document.body.textContent).toContain("旧链路");
|
||||
expect(document.body.textContent).not.toContain("兼容调度进度");
|
||||
expect(document.body.textContent).not.toContain("兼容调度轨迹");
|
||||
expect(document.body.textContent).toContain("子任务");
|
||||
expect(document.body.textContent).toContain("当前子任务");
|
||||
expect(document.body.textContent).toContain("实时子任务");
|
||||
expect(document.body.textContent).toContain("类型:子任务");
|
||||
expect(document.body.textContent).not.toContain("协作回退");
|
||||
expect(document.body.textContent).not.toContain("回退链路");
|
||||
expect(document.body.textContent).toContain("研究代理");
|
||||
expect(document.body.textContent).toContain("实现代理");
|
||||
});
|
||||
|
||||
@@ -109,7 +109,6 @@ import {
|
||||
} from "../utils/workflowStepPresentation";
|
||||
import { buildThreadReliabilityView } from "../utils/threadReliabilityView";
|
||||
import { resolveTeamWorkspaceStableProcessingLabel } from "../utils/teamWorkspaceCopy";
|
||||
import type { CompatSubagentRuntimeDisplaySnapshot } from "../utils/compatSubagentRuntime";
|
||||
import type { TeamRoleDefinition } from "../utils/teamDefinitions";
|
||||
import type { TeamMemorySnapshot } from "@/lib/teamMemorySync";
|
||||
import { AgentThreadReliabilityPanel } from "./AgentThreadReliabilityPanel";
|
||||
@@ -135,7 +134,6 @@ export interface HarnessFilePreviewResult {
|
||||
|
||||
interface HarnessStatusPanelProps {
|
||||
harnessState: HarnessSessionState;
|
||||
compatSubagentRuntime: CompatSubagentRuntimeDisplaySnapshot;
|
||||
environment: HarnessEnvironmentSummary;
|
||||
layout?: "default" | "sidebar" | "dialog";
|
||||
onLoadFilePreview?: (path: string) => Promise<HarnessFilePreviewResult>;
|
||||
@@ -340,12 +338,12 @@ function resolveSubagentRuntimeStatusVariant(
|
||||
function resolveSubagentSessionTypeLabel(value?: string): string {
|
||||
switch (value) {
|
||||
case "sub_agent":
|
||||
return "协作成员";
|
||||
return "子任务";
|
||||
case "fork":
|
||||
return "分支协作";
|
||||
return "分支任务";
|
||||
case "user":
|
||||
default:
|
||||
return value?.trim() || "协作会话";
|
||||
return value?.trim() || "任务会话";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1535,115 +1533,6 @@ function SummaryCard({
|
||||
);
|
||||
}
|
||||
|
||||
function CompatSubagentFallbackCard({
|
||||
snapshot,
|
||||
condensed = false,
|
||||
onOpenUrl,
|
||||
}: {
|
||||
snapshot: CompatSubagentRuntimeDisplaySnapshot;
|
||||
condensed?: boolean;
|
||||
onOpenUrl: (url: string) => void | Promise<void>;
|
||||
}) {
|
||||
if (!snapshot.hasSignals) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const statusVariant: ComponentProps<typeof Badge>["variant"] = snapshot.error
|
||||
? "destructive"
|
||||
: snapshot.isRunning
|
||||
? "secondary"
|
||||
: "outline";
|
||||
const statusLabel = snapshot.error
|
||||
? "异常"
|
||||
: snapshot.isRunning
|
||||
? snapshot.progress
|
||||
? `${snapshot.progress.completed}/${snapshot.progress.total}`
|
||||
: "处理中"
|
||||
: snapshot.result
|
||||
? "已结束"
|
||||
: `${snapshot.recentActivity.length} 条`;
|
||||
const primarySummary = snapshot.progress
|
||||
? `进度 ${snapshot.progress.completed}/${snapshot.progress.total}${
|
||||
snapshot.progress.currentTasks.length > 0
|
||||
? ` · 当前任务 ${snapshot.progress.currentTasks.join("、")}`
|
||||
: ""
|
||||
}`
|
||||
: snapshot.summary ||
|
||||
snapshot.recentActivity[0]?.summary ||
|
||||
snapshot.error ||
|
||||
snapshot.result?.mergedSummary ||
|
||||
"检测到兼容调度信号";
|
||||
const visibleActivity = condensed
|
||||
? snapshot.recentActivity.slice(0, 1)
|
||||
: snapshot.recentActivity.slice(0, 3);
|
||||
|
||||
return (
|
||||
<div className="rounded-xl border border-dashed border-border bg-muted/35 p-3">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<TerminalSquare className="h-4 w-4 text-muted-foreground" />
|
||||
<div className="text-sm font-medium text-foreground">兼容模式</div>
|
||||
<Badge variant="outline">旧链路</Badge>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-muted-foreground">
|
||||
仅用于承接旧调度信号,不作为当前协作视图的主事实源。
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant={statusVariant}>{statusLabel}</Badge>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 space-y-2">
|
||||
<InteractiveText
|
||||
text={primarySummary}
|
||||
className="text-xs text-muted-foreground"
|
||||
onOpenUrl={onOpenUrl}
|
||||
/>
|
||||
|
||||
{visibleActivity.length > 0 &&
|
||||
visibleActivity[0]?.summary !== primarySummary ? (
|
||||
<div className="rounded-lg bg-background/70 px-2.5 py-2">
|
||||
<div className="text-[11px] font-medium text-muted-foreground">
|
||||
最近兼容轨迹
|
||||
</div>
|
||||
<div className="mt-1 space-y-1">
|
||||
{visibleActivity.map((item) => (
|
||||
<InteractiveText
|
||||
key={item.id}
|
||||
text={item.summary}
|
||||
className="text-xs text-muted-foreground"
|
||||
onOpenUrl={onOpenUrl}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!condensed &&
|
||||
snapshot.result?.mergedSummary &&
|
||||
snapshot.result.mergedSummary !== primarySummary ? (
|
||||
<div className="rounded-lg bg-background/70 px-2.5 py-2">
|
||||
<div className="text-[11px] font-medium text-muted-foreground">
|
||||
兼容汇总
|
||||
</div>
|
||||
<InteractiveText
|
||||
text={snapshot.result.mergedSummary}
|
||||
className="mt-1 text-xs text-muted-foreground"
|
||||
onOpenUrl={onOpenUrl}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{!condensed && snapshot.error && snapshot.error !== primarySummary ? (
|
||||
<div className="rounded-lg border border-destructive/30 bg-destructive/10 px-2.5 py-2 text-xs text-destructive">
|
||||
<InteractiveText text={snapshot.error} onOpenUrl={onOpenUrl} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InventoryStatCard({
|
||||
title,
|
||||
value,
|
||||
@@ -1696,7 +1585,6 @@ function Section({
|
||||
|
||||
export function HarnessStatusPanel({
|
||||
harnessState,
|
||||
compatSubagentRuntime,
|
||||
environment,
|
||||
layout = "default",
|
||||
onLoadFilePreview,
|
||||
@@ -2064,7 +1952,6 @@ export function HarnessStatusPanel({
|
||||
() => summarizeChildSubagentSessions(childSubagentSessions),
|
||||
[childSubagentSessions],
|
||||
);
|
||||
const hasCompatSchedulerSignals = compatSubagentRuntime.hasSignals;
|
||||
const hasSelectedTeamConfig =
|
||||
Boolean(selectedTeamLabel?.trim()) ||
|
||||
Boolean(selectedTeamSummary?.trim()) ||
|
||||
@@ -2229,7 +2116,7 @@ export function HarnessStatusPanel({
|
||||
const sections: HarnessSectionNavItem[] = [];
|
||||
|
||||
if (hasSelectedTeamConfig) {
|
||||
sections.push({ key: "team_config", label: "协作设置" });
|
||||
sections.push({ key: "team_config", label: "任务分工" });
|
||||
}
|
||||
|
||||
if (runtimeTaskPresentation) {
|
||||
@@ -2264,10 +2151,9 @@ export function HarnessStatusPanel({
|
||||
}
|
||||
if (
|
||||
realTeamSummary.total > 0 ||
|
||||
harnessState.delegatedTasks.length > 0 ||
|
||||
hasCompatSchedulerSignals
|
||||
harnessState.delegatedTasks.length > 0
|
||||
) {
|
||||
sections.push({ key: "delegation", label: "协作成员" });
|
||||
sections.push({ key: "delegation", label: "子任务" });
|
||||
}
|
||||
if (harnessState.latestContextTrace.length > 0) {
|
||||
sections.push({ key: "context", label: "上下文轨迹" });
|
||||
@@ -2291,7 +2177,6 @@ export function HarnessStatusPanel({
|
||||
harnessState.recentFileEvents.length,
|
||||
hasHandoffSection,
|
||||
hasSelectedTeamConfig,
|
||||
hasCompatSchedulerSignals,
|
||||
realTeamSummary.total,
|
||||
runtimeTaskPresentation,
|
||||
threadReliabilityView.shouldRender,
|
||||
@@ -2337,7 +2222,7 @@ export function HarnessStatusPanel({
|
||||
if (hasSelectedTeamConfig) {
|
||||
cards.push({
|
||||
sectionKey: "team_config",
|
||||
title: "协作设置",
|
||||
title: "任务分工",
|
||||
value:
|
||||
selectedTeamLabel?.trim() ||
|
||||
`${selectedTeamRoles?.length || 0} 个角色`,
|
||||
@@ -2345,7 +2230,7 @@ export function HarnessStatusPanel({
|
||||
selectedTeamSummary?.trim() ||
|
||||
((selectedTeamRoles?.length || 0) > 0
|
||||
? `已配置 ${selectedTeamRoles?.length || 0} 个角色`
|
||||
: "本次已启用协作设置"),
|
||||
: "本次已启用任务分工"),
|
||||
icon: Workflow,
|
||||
});
|
||||
}
|
||||
@@ -2364,7 +2249,7 @@ export function HarnessStatusPanel({
|
||||
if (realTeamSummary.total > 0) {
|
||||
cards.push({
|
||||
sectionKey: "delegation",
|
||||
title: "协作成员",
|
||||
title: "子任务",
|
||||
value:
|
||||
realTeamSummary.active > 0
|
||||
? `${realTeamSummary.active}/${realTeamSummary.total}`
|
||||
@@ -2696,11 +2581,6 @@ export function HarnessStatusPanel({
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
任务进行中
|
||||
</Badge>
|
||||
) : compatSubagentRuntime.isRunning ? (
|
||||
<Badge variant="secondary" className="gap-1">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
兼容调度中
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">{description}</p>
|
||||
@@ -2811,7 +2691,7 @@ export function HarnessStatusPanel({
|
||||
{hasSelectedTeamConfig ? (
|
||||
<Section
|
||||
sectionKey="team_config"
|
||||
title="当前协作设置"
|
||||
title="当前任务分工"
|
||||
badge={
|
||||
selectedTeamRoles && selectedTeamRoles.length > 0
|
||||
? `${selectedTeamRoles.length} 个角色`
|
||||
@@ -2823,7 +2703,7 @@ export function HarnessStatusPanel({
|
||||
<div className="rounded-xl border border-sky-200/80 bg-sky-50/50 p-3">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
|
||||
<Workflow className="h-4 w-4 text-sky-600" />
|
||||
<span>{selectedTeamLabel || "当前已启用协作方案"}</span>
|
||||
<span>{selectedTeamLabel || "当前已启用任务方案"}</span>
|
||||
</div>
|
||||
{selectedTeamSummary ? (
|
||||
<div className="mt-2 text-sm text-muted-foreground">
|
||||
@@ -2831,7 +2711,7 @@ export function HarnessStatusPanel({
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-2 text-sm text-muted-foreground">
|
||||
本次会优先参考所选协作方案的角色分工,按需邀请协作成员一起处理。
|
||||
本次会优先参考所选任务方案的角色分工,按需拆出子任务继续处理。
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -3140,7 +3020,7 @@ export function HarnessStatusPanel({
|
||||
hint={`待开始 ${handoffBundle.todo_pending} · 进行中 ${handoffBundle.todo_in_progress}`}
|
||||
/>
|
||||
<InventoryStatCard
|
||||
title="协作成员"
|
||||
title="子任务"
|
||||
value={`${handoffBundle.active_subagent_count}`}
|
||||
hint={`workspace ${handoffBundle.workspace_id || "未绑定"}`}
|
||||
/>
|
||||
@@ -5712,16 +5592,15 @@ export function HarnessStatusPanel({
|
||||
) : null}
|
||||
|
||||
{realTeamSummary.total > 0 ||
|
||||
harnessState.delegatedTasks.length > 0 ||
|
||||
hasCompatSchedulerSignals ? (
|
||||
harnessState.delegatedTasks.length > 0 ? (
|
||||
<Section
|
||||
sectionKey="delegation"
|
||||
title="协作成员"
|
||||
title="子任务"
|
||||
badge={
|
||||
realTeamSummary.active > 0
|
||||
? `处理中 ${realTeamSummary.active}`
|
||||
: realTeamSummary.total > 0
|
||||
? `${realTeamSummary.total} 个协作`
|
||||
? `${realTeamSummary.total} 个子任务`
|
||||
: harnessState.delegatedTasks.length > 0
|
||||
? `${harnessState.delegatedTasks.length} 条`
|
||||
: undefined
|
||||
@@ -5733,7 +5612,7 @@ export function HarnessStatusPanel({
|
||||
<div className="rounded-xl border border-border bg-background p-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
当前协作会话
|
||||
当前子任务
|
||||
</div>
|
||||
<Badge variant="outline">
|
||||
{realTeamSummary.total} 个
|
||||
@@ -5802,7 +5681,7 @@ export function HarnessStatusPanel({
|
||||
{childSubagentSessions.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
<div className="text-xs font-medium text-muted-foreground">
|
||||
实时协作会话
|
||||
实时子任务
|
||||
</div>
|
||||
{childSubagentSessions.map((session) => (
|
||||
<div
|
||||
@@ -5900,12 +5779,6 @@ export function HarnessStatusPanel({
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<CompatSubagentFallbackCard
|
||||
snapshot={compatSubagentRuntime}
|
||||
condensed={realTeamSummary.total > 0}
|
||||
onOpenUrl={handleOpenExternalLink}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
) : null}
|
||||
|
||||
+10
-5
@@ -14,6 +14,7 @@ import type { BuiltinInputCommand } from "../../../skill-selection/builtinComman
|
||||
import { TeamSelector } from "./TeamSelector";
|
||||
import { InputbarWorkflowStatusPanel } from "./InputbarWorkflowStatusPanel";
|
||||
import { InputbarModelExtra } from "./InputbarModelExtra";
|
||||
import { InputbarPromptCacheNotice } from "./InputbarPromptCacheNotice";
|
||||
import { InputbarVisionCapabilityNotice } from "./InputbarVisionCapabilityNotice";
|
||||
import { InputbarExecutionStrategySelect } from "./InputbarExecutionStrategySelect";
|
||||
import { InputbarAccessModeSelect } from "./InputbarAccessModeSelect";
|
||||
@@ -158,10 +159,14 @@ export const InputbarComposerSection: React.FC<
|
||||
currentPendingImages.length > 0 &&
|
||||
Boolean(resolvedProviderType?.trim()) &&
|
||||
Boolean(resolvedModel?.trim());
|
||||
const shouldShowPromptCacheNotice = Boolean(resolvedProviderType?.trim());
|
||||
const resolvedTopExtra =
|
||||
topExtra || shouldShowVisionNotice ? (
|
||||
topExtra || shouldShowPromptCacheNotice || shouldShowVisionNotice ? (
|
||||
<>
|
||||
{topExtra}
|
||||
{shouldShowPromptCacheNotice && resolvedProviderType ? (
|
||||
<InputbarPromptCacheNotice providerType={resolvedProviderType} />
|
||||
) : null}
|
||||
{shouldShowVisionNotice && resolvedProviderType && resolvedModel ? (
|
||||
<InputbarVisionCapabilityNotice
|
||||
providerType={resolvedProviderType}
|
||||
@@ -233,7 +238,9 @@ export const InputbarComposerSection: React.FC<
|
||||
</Badge>
|
||||
) : null}
|
||||
|
||||
{!showAdvancedControls && shouldShowModelControls && !hasConfiguredModel ? (
|
||||
{!showAdvancedControls &&
|
||||
shouldShowModelControls &&
|
||||
!hasConfiguredModel ? (
|
||||
<InputbarModelExtra
|
||||
isFullscreen={isFullscreen}
|
||||
providerType={resolvedProviderType}
|
||||
@@ -248,9 +255,7 @@ export const InputbarComposerSection: React.FC<
|
||||
|
||||
{showAdvancedControls ? (
|
||||
<>
|
||||
{showSkillSelector ? (
|
||||
<SkillSelector {...skillSelectorProps} />
|
||||
) : null}
|
||||
{showSkillSelector ? <SkillSelector {...skillSelectorProps} /> : null}
|
||||
{shouldShowTeamSelector ? (
|
||||
<TeamSelector
|
||||
activeTheme={activeTheme}
|
||||
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
import React from "react";
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { InputbarPromptCacheNotice } from "./InputbarPromptCacheNotice";
|
||||
|
||||
const mockUseConfiguredProviders = vi.fn();
|
||||
|
||||
vi.mock("@/hooks/useConfiguredProviders", () => ({
|
||||
useConfiguredProviders: (options: unknown) =>
|
||||
mockUseConfiguredProviders(options),
|
||||
findConfiguredProviderBySelection: (
|
||||
providers: Array<{ key: string; providerId?: string }>,
|
||||
selection?: string | null,
|
||||
) => {
|
||||
const normalizedSelection = (selection || "").trim().toLowerCase();
|
||||
const keyMatch =
|
||||
providers.find(
|
||||
(provider) => provider.key.trim().toLowerCase() === normalizedSelection,
|
||||
) ?? null;
|
||||
const providerIdMatch =
|
||||
providers.find(
|
||||
(provider) =>
|
||||
(provider.providerId || "").trim().toLowerCase() ===
|
||||
normalizedSelection,
|
||||
) ?? null;
|
||||
|
||||
if (keyMatch && providerIdMatch && keyMatch !== providerIdMatch) {
|
||||
if (!keyMatch.providerId && providerIdMatch.providerId) {
|
||||
return providerIdMatch;
|
||||
}
|
||||
}
|
||||
|
||||
return keyMatch ?? providerIdMatch ?? null;
|
||||
},
|
||||
resolveConfiguredProviderPromptCacheSupportNotice: (
|
||||
providers: Array<{ key: string; providerId?: string; type?: string }>,
|
||||
selection?: string | null,
|
||||
) => {
|
||||
const normalizedSelection = (selection || "").trim().toLowerCase();
|
||||
const selectedProvider =
|
||||
providers.find(
|
||||
(provider) => provider.key.trim().toLowerCase() === normalizedSelection,
|
||||
) ??
|
||||
providers.find(
|
||||
(provider) =>
|
||||
(provider.providerId || "").trim().toLowerCase() ===
|
||||
normalizedSelection,
|
||||
) ??
|
||||
null;
|
||||
const normalizedConfiguredType = (
|
||||
selectedProvider?.type || ""
|
||||
).trim().toLowerCase();
|
||||
|
||||
if (normalizedConfiguredType === "anthropic-compatible") {
|
||||
return {
|
||||
label: "未声明自动缓存",
|
||||
detail:
|
||||
"当前 Provider 未声明支持自动 Prompt Cache;如需复用前缀,请使用显式 cache_control 标记。",
|
||||
source: "configured_provider" as const,
|
||||
};
|
||||
}
|
||||
|
||||
if (normalizedSelection === "anthropic-compatible") {
|
||||
return {
|
||||
label: "未声明自动缓存",
|
||||
detail:
|
||||
"当前 Provider 未声明支持自动 Prompt Cache;当前提示基于 Provider 选择器回退判断,如需复用前缀,请使用显式 cache_control 标记。",
|
||||
source: "selection_fallback" as const,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
}));
|
||||
|
||||
const mountedRoots: Array<{ root: Root; container: HTMLDivElement }> = [];
|
||||
|
||||
beforeEach(() => {
|
||||
(
|
||||
globalThis as typeof globalThis & {
|
||||
IS_REACT_ACT_ENVIRONMENT?: boolean;
|
||||
}
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
mockUseConfiguredProviders.mockReturnValue({
|
||||
providers: [],
|
||||
loading: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
while (mountedRoots.length > 0) {
|
||||
const mounted = mountedRoots.pop();
|
||||
if (!mounted) break;
|
||||
act(() => {
|
||||
mounted.root.unmount();
|
||||
});
|
||||
mounted.container.remove();
|
||||
}
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
function renderNotice(
|
||||
props?: Partial<React.ComponentProps<typeof InputbarPromptCacheNotice>>,
|
||||
) {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
act(() => {
|
||||
root.render(
|
||||
<InputbarPromptCacheNotice
|
||||
providerType="custom-provider-id"
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
mountedRoots.push({ root, container });
|
||||
return container;
|
||||
}
|
||||
|
||||
describe("InputbarPromptCacheNotice", () => {
|
||||
it("anthropic-compatible 自定义 Provider 应展示发送前提示", () => {
|
||||
mockUseConfiguredProviders.mockReturnValue({
|
||||
providers: [
|
||||
{
|
||||
key: "custom-provider-id",
|
||||
label: "GLM Anthropic",
|
||||
registryId: "custom-provider-id",
|
||||
type: "anthropic-compatible",
|
||||
providerId: "custom-provider-id",
|
||||
},
|
||||
],
|
||||
loading: false,
|
||||
});
|
||||
|
||||
const container = renderNotice();
|
||||
|
||||
expect(
|
||||
container.querySelector('[data-testid="inputbar-prompt-cache-warning"]'),
|
||||
).not.toBeNull();
|
||||
expect(container.textContent).toContain("未声明支持自动 Prompt Cache");
|
||||
expect(container.textContent).toContain("cache_control");
|
||||
});
|
||||
|
||||
it("官方 Anthropic Provider 不应展示发送前提示", () => {
|
||||
mockUseConfiguredProviders.mockReturnValue({
|
||||
providers: [
|
||||
{
|
||||
key: "anthropic",
|
||||
label: "Anthropic",
|
||||
registryId: "anthropic",
|
||||
type: "anthropic",
|
||||
providerId: "anthropic",
|
||||
},
|
||||
],
|
||||
loading: false,
|
||||
});
|
||||
|
||||
const container = renderNotice({
|
||||
providerType: "anthropic",
|
||||
});
|
||||
|
||||
expect(
|
||||
container.querySelector('[data-testid="inputbar-prompt-cache-warning"]'),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("未解析到真实 Provider 时,应允许按选择器回退展示提示", () => {
|
||||
const container = renderNotice({
|
||||
providerType: "anthropic-compatible",
|
||||
});
|
||||
|
||||
const warning = container.querySelector(
|
||||
'[data-testid="inputbar-prompt-cache-warning"]',
|
||||
);
|
||||
expect(warning).not.toBeNull();
|
||||
expect(warning?.getAttribute("title")).toContain("回退判断");
|
||||
});
|
||||
});
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import React, { useMemo } from "react";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
import {
|
||||
resolveConfiguredProviderPromptCacheSupportNotice,
|
||||
useConfiguredProviders,
|
||||
} from "@/hooks/useConfiguredProviders";
|
||||
|
||||
interface InputbarPromptCacheNoticeProps {
|
||||
providerType: string;
|
||||
}
|
||||
|
||||
export const InputbarPromptCacheNotice: React.FC<
|
||||
InputbarPromptCacheNoticeProps
|
||||
> = ({ providerType }) => {
|
||||
const shouldInspectCapability = Boolean(providerType.trim());
|
||||
|
||||
const { providers, loading: providersLoading } = useConfiguredProviders({
|
||||
autoLoad: shouldInspectCapability,
|
||||
});
|
||||
|
||||
const notice = useMemo(() => {
|
||||
if (!shouldInspectCapability || providersLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return resolveConfiguredProviderPromptCacheSupportNotice(
|
||||
providers,
|
||||
providerType,
|
||||
);
|
||||
}, [providerType, providers, providersLoading, shouldInspectCapability]);
|
||||
|
||||
if (!notice) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="inputbar-prompt-cache-warning"
|
||||
className="mx-3 mb-2 flex items-start gap-2 rounded-2xl border border-amber-200/80 bg-amber-50/90 px-3 py-2 text-[11px] leading-5 text-amber-800"
|
||||
title={notice.detail}
|
||||
>
|
||||
<AlertCircle className="mt-0.5 h-3.5 w-3.5 shrink-0 text-amber-600" />
|
||||
<span>
|
||||
当前 Provider 未声明支持自动 Prompt Cache;如需复用前缀,请使用显式{" "}
|
||||
cache_control 标记
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -51,10 +51,10 @@ export const InputbarTools: React.FC<InputbarToolsProps> = ({
|
||||
onClick={() => onToolClick("subagent_mode")}
|
||||
className={activeTools["subagent_mode"] ? "active" : ""}
|
||||
aria-pressed={activeTools["subagent_mode"]}
|
||||
title={`多代理偏好${activeTools["subagent_mode"] ? "已开启" : "已关闭"}`}
|
||||
title={`任务拆分偏好${activeTools["subagent_mode"] ? "已开启" : "已关闭"}`}
|
||||
>
|
||||
<Workflow />
|
||||
<span>多代理</span>
|
||||
<span>任务拆分</span>
|
||||
</ToolButton>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
+1
-1
@@ -366,7 +366,7 @@ export function InputbarWorkflowStatusPanel({
|
||||
<SummaryIcon $tone={summaryTone}>{renderToneIcon(summaryTone)}</SummaryIcon>
|
||||
<SummaryBody>
|
||||
<SummaryEyebrow>
|
||||
任务进行时
|
||||
任务视图
|
||||
</SummaryEyebrow>
|
||||
<SummaryTitle data-testid="workflow-current-title">
|
||||
{summaryTitle}
|
||||
|
||||
@@ -561,7 +561,7 @@ export const TeamSelectorPanel: React.FC<TeamSelectorPanelProps> = ({
|
||||
</div>
|
||||
<div className="mt-2 text-sm leading-6 text-slate-600">
|
||||
{inspectorTeam.description ||
|
||||
"这套分工会作为主代理拆分子任务时的协作参考。"}
|
||||
"这套分工会作为主代理拆分子任务时的分工参考。"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -963,7 +963,7 @@ export const TeamSelectorPanel: React.FC<TeamSelectorPanelProps> = ({
|
||||
{selectedProfile && suggestedSkillIds.length > 0 ? (
|
||||
<>推荐技能:{suggestedSkillIds.join("、")}</>
|
||||
) : (
|
||||
"skillIds 会透传给运行时,用于约束子代理的技能集。"
|
||||
"skillIds 会透传给运行时,用于约束子任务可用的技能集。"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1011,7 +1011,7 @@ export const TeamSelectorPanel: React.FC<TeamSelectorPanelProps> = ({
|
||||
任务分工配置
|
||||
</div>
|
||||
<div className="mt-1 text-sm text-slate-700">
|
||||
只在当前任务适合拆分推进时,为主代理提供分工结构参考。
|
||||
只在当前任务适合拆分推进时,为当前任务提供任务分工结构参考。
|
||||
</div>
|
||||
<div className="mt-2 text-xs text-slate-500">
|
||||
{isProjectScopedCustomTeam
|
||||
|
||||
@@ -90,7 +90,9 @@ export function useInputbarToolState({
|
||||
thinking: thinkingEnabled,
|
||||
subagent: nextSubagent,
|
||||
});
|
||||
toast.info(`多代理${nextSubagent ? "偏好已开启" : "偏好已关闭"}`);
|
||||
toast.info(
|
||||
`任务拆分${nextSubagent ? "偏好已开启" : "偏好已关闭"}`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "attach":
|
||||
@@ -124,7 +126,7 @@ export function useInputbarToolState({
|
||||
thinking: thinkingEnabled,
|
||||
subagent: enabled,
|
||||
});
|
||||
toast.info(`多代理${enabled ? "偏好已开启" : "偏好已关闭"}`);
|
||||
toast.info(`任务拆分${enabled ? "偏好已开启" : "偏好已关闭"}`);
|
||||
},
|
||||
[subagentEnabled, thinkingEnabled, updateToolStates, webSearchEnabled],
|
||||
);
|
||||
|
||||
@@ -45,7 +45,7 @@ const mockInputbarCore = vi.fn(
|
||||
data-testid="toggle-subagent-mode"
|
||||
onClick={() => props.onToolClick?.("subagent_mode")}
|
||||
>
|
||||
切换多代理
|
||||
切换任务拆分
|
||||
</button>
|
||||
<span data-testid="subagent-state">
|
||||
{props.activeTools?.subagent_mode ? "on" : "off"}
|
||||
@@ -139,6 +139,12 @@ vi.mock("../ChatModelSelector", () => ({
|
||||
ChatModelSelector: () => <div data-testid="model-selector" />,
|
||||
}));
|
||||
|
||||
vi.mock("./components/InputbarPromptCacheNotice", () => ({
|
||||
InputbarPromptCacheNotice: (props: { providerType: string }) => (
|
||||
<div data-testid="inputbar-prompt-cache-warning">{props.providerType}</div>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/dev-bridge", () => ({
|
||||
safeInvoke: vi.fn(async () => []),
|
||||
}));
|
||||
@@ -334,7 +340,7 @@ describe("Inputbar", () => {
|
||||
],
|
||||
overlayAccessory: (
|
||||
<button type="button" data-testid="team-inline-toggle">
|
||||
查看协作进展 · 2
|
||||
查看任务进展 · 2
|
||||
</button>
|
||||
),
|
||||
});
|
||||
@@ -472,9 +478,7 @@ describe("Inputbar", () => {
|
||||
expect(
|
||||
container.querySelector('[data-testid="inputbar-plan-toggle"]'),
|
||||
).toBeNull();
|
||||
expect(
|
||||
container.querySelector('select[aria-label="权限模式"]'),
|
||||
).toBeNull();
|
||||
expect(container.querySelector('select[aria-label="权限模式"]')).toBeNull();
|
||||
|
||||
expandAdvancedControls(container);
|
||||
|
||||
@@ -824,6 +828,25 @@ describe("Inputbar", () => {
|
||||
expect(latestCall.leftExtra).toBeDefined();
|
||||
});
|
||||
|
||||
it("已选择 Provider 时应将 prompt cache 提示组件挂到输入区顶部", async () => {
|
||||
const { container } = renderInputbar({
|
||||
providerType: "custom-provider-id",
|
||||
setProviderType: vi.fn(),
|
||||
model: "glm-5.1",
|
||||
setModel: vi.fn(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(
|
||||
container.querySelector('[data-testid="inputbar-prompt-cache-warning"]'),
|
||||
).toBeTruthy();
|
||||
expect(container.textContent).toContain("custom-provider-id");
|
||||
});
|
||||
|
||||
it("任务中心工作区应使用继续推进型输入提示", async () => {
|
||||
renderInputbar({
|
||||
variant: "workspace",
|
||||
@@ -924,7 +947,7 @@ describe("Inputbar", () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("任务进行时");
|
||||
expect(container.textContent).toContain("任务视图");
|
||||
expect(container.textContent).toContain("检索项目素材");
|
||||
expect(container.textContent).toContain("任务队列");
|
||||
expect(container.querySelector('[data-testid="inputbar-core"]')).toBeNull();
|
||||
@@ -956,9 +979,9 @@ describe("Inputbar", () => {
|
||||
|
||||
expect(container.textContent).toContain("检索项目素材");
|
||||
|
||||
const collapseButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(button) => button.getAttribute("aria-label") === "折叠任务队列",
|
||||
);
|
||||
const collapseButton = Array.from(
|
||||
container.querySelectorAll("button"),
|
||||
).find((button) => button.getAttribute("aria-label") === "折叠任务队列");
|
||||
expect(collapseButton).toBeTruthy();
|
||||
|
||||
act(() => {
|
||||
@@ -1028,7 +1051,7 @@ describe("Inputbar", () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("任务进行时");
|
||||
expect(container.textContent).toContain("任务视图");
|
||||
expect(container.textContent).toContain("检索项目素材");
|
||||
expect(container.querySelector('[data-testid="inputbar-core"]')).toBeNull();
|
||||
});
|
||||
@@ -1057,7 +1080,7 @@ describe("Inputbar", () => {
|
||||
expect(
|
||||
container.querySelector('[data-testid="inputbar-core"]'),
|
||||
).toBeTruthy();
|
||||
expect(container.textContent).toContain("任务进行时");
|
||||
expect(container.textContent).toContain("任务视图");
|
||||
expect(container.textContent).toContain("等待用户确认选题");
|
||||
expect(container.textContent).not.toContain("正在生成中");
|
||||
});
|
||||
|
||||
@@ -7,6 +7,77 @@ import type { Message } from "../types";
|
||||
|
||||
const IMAGE_WORKBENCH_FOCUS_EVENT = "lime:image-workbench-focus";
|
||||
const VIDEO_WORKBENCH_TASK_ACTION_EVENT = "lime:video-workbench-task-action";
|
||||
type MockConfiguredProvider = {
|
||||
key: string;
|
||||
label?: string;
|
||||
registryId?: string;
|
||||
type?: string;
|
||||
providerId?: string;
|
||||
};
|
||||
|
||||
const mockUseConfiguredProviders = vi.fn((_options?: unknown) => ({
|
||||
providers: [] as MockConfiguredProvider[],
|
||||
loading: false,
|
||||
}));
|
||||
const mockFindConfiguredProviderBySelection = vi.fn(
|
||||
(
|
||||
_providers: MockConfiguredProvider[],
|
||||
_selection?: string | null,
|
||||
): MockConfiguredProvider | null => null,
|
||||
);
|
||||
const mockTokenUsageDisplay = vi.fn(
|
||||
({
|
||||
promptCacheNotice,
|
||||
}: {
|
||||
promptCacheNotice?: {
|
||||
label?: string;
|
||||
} | null;
|
||||
}) => (
|
||||
<div data-testid="token-usage-display">
|
||||
{promptCacheNotice?.label || "token-usage-display"}
|
||||
</div>
|
||||
),
|
||||
);
|
||||
|
||||
vi.mock("@/hooks/useConfiguredProviders", () => ({
|
||||
useConfiguredProviders: (options?: unknown) =>
|
||||
mockUseConfiguredProviders(options),
|
||||
findConfiguredProviderBySelection: (
|
||||
providers: MockConfiguredProvider[],
|
||||
selection?: string | null,
|
||||
) => mockFindConfiguredProviderBySelection(providers, selection),
|
||||
resolveConfiguredProviderPromptCacheSupportNotice: (
|
||||
providers: MockConfiguredProvider[],
|
||||
selection?: string | null,
|
||||
) => {
|
||||
const selectedProvider =
|
||||
mockFindConfiguredProviderBySelection(providers, selection);
|
||||
const normalizedConfiguredType = (
|
||||
selectedProvider?.type || ""
|
||||
).trim().toLowerCase();
|
||||
const normalizedSelection = (selection || "").trim().toLowerCase();
|
||||
|
||||
if (normalizedConfiguredType === "anthropic-compatible") {
|
||||
return {
|
||||
label: "未声明自动缓存",
|
||||
detail:
|
||||
"当前 Provider 未声明支持自动 Prompt Cache;如需复用前缀,请使用显式 cache_control 标记。",
|
||||
source: "configured_provider" as const,
|
||||
};
|
||||
}
|
||||
|
||||
if (normalizedSelection === "anthropic-compatible") {
|
||||
return {
|
||||
label: "未声明自动缓存",
|
||||
detail:
|
||||
"当前 Provider 未声明支持自动 Prompt Cache;当前提示基于 Provider 选择器回退判断,如需复用前缀,请使用显式 cache_control 标记。",
|
||||
source: "selection_fallback" as const,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./MarkdownRenderer", () => ({
|
||||
MarkdownRenderer: ({ content }: { content: string }) => (
|
||||
@@ -83,7 +154,11 @@ vi.mock("./StreamingRenderer", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./TokenUsageDisplay", () => ({
|
||||
TokenUsageDisplay: () => <div data-testid="token-usage-display" />,
|
||||
TokenUsageDisplay: (props: {
|
||||
promptCacheNotice?: {
|
||||
label?: string;
|
||||
} | null;
|
||||
}) => mockTokenUsageDisplay(props),
|
||||
}));
|
||||
|
||||
vi.mock("./AgentThreadTimeline", () => ({
|
||||
@@ -121,6 +196,11 @@ afterEach(() => {
|
||||
mounted.container.remove();
|
||||
}
|
||||
vi.clearAllMocks();
|
||||
mockUseConfiguredProviders.mockImplementation(() => ({
|
||||
providers: [],
|
||||
loading: false,
|
||||
}));
|
||||
mockFindConfiguredProviderBySelection.mockImplementation(() => null);
|
||||
});
|
||||
|
||||
function render(
|
||||
@@ -235,6 +315,58 @@ describe("MessageList", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("anthropic-compatible 自定义 Provider 无缓存命中时应透传自动缓存提示", () => {
|
||||
const now = new Date();
|
||||
const messages: Message[] = [
|
||||
{
|
||||
id: "msg-assistant-usage",
|
||||
role: "assistant",
|
||||
content: "本轮已完成。",
|
||||
timestamp: now,
|
||||
usage: {
|
||||
input_tokens: 1_500,
|
||||
output_tokens: 500,
|
||||
cached_input_tokens: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockUseConfiguredProviders.mockImplementation(() => ({
|
||||
providers: [
|
||||
{
|
||||
key: "custom-provider-id",
|
||||
label: "GLM Anthropic",
|
||||
registryId: "custom-provider-id",
|
||||
type: "anthropic-compatible",
|
||||
providerId: "custom-provider-id",
|
||||
},
|
||||
],
|
||||
loading: false,
|
||||
}));
|
||||
mockFindConfiguredProviderBySelection.mockImplementation(
|
||||
(
|
||||
providers: MockConfiguredProvider[],
|
||||
selection?: string | null,
|
||||
): MockConfiguredProvider | null =>
|
||||
Array.isArray(providers)
|
||||
? (providers.find((provider) => provider.key === selection) ?? null)
|
||||
: null,
|
||||
);
|
||||
|
||||
const container = render(messages, {
|
||||
providerType: "custom-provider-id",
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("未声明自动缓存");
|
||||
expect(mockTokenUsageDisplay).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
promptCacheNotice: expect.objectContaining({
|
||||
label: "未声明自动缓存",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("图片任务消息卡应在聊天区渲染预览并支持展开图片画布", () => {
|
||||
const now = new Date();
|
||||
const messages: Message[] = [
|
||||
|
||||
@@ -18,6 +18,10 @@ import {
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { toast } from "sonner";
|
||||
import type { Artifact } from "@/lib/artifact/types";
|
||||
import {
|
||||
resolveConfiguredProviderPromptCacheSupportNotice,
|
||||
useConfiguredProviders,
|
||||
} from "@/hooks/useConfiguredProviders";
|
||||
import { resolveArtifactProtocolFilePath } from "@/lib/artifact-protocol";
|
||||
import {
|
||||
MessageListContainer,
|
||||
@@ -68,9 +72,7 @@ import {
|
||||
resolveSiteSavedContentTargetRelativePath,
|
||||
} from "../utils/siteToolResultSummary";
|
||||
import logoImg from "/logo.png";
|
||||
import {
|
||||
type ArtifactTimelineOpenTarget,
|
||||
} from "../utils/artifactTimelineNavigation";
|
||||
import { type ArtifactTimelineOpenTarget } from "../utils/artifactTimelineNavigation";
|
||||
|
||||
interface MessageListProps {
|
||||
messages: Message[];
|
||||
@@ -157,6 +159,8 @@ interface MessageListProps {
|
||||
timelineFocusRequestKey?: number;
|
||||
/** 当前由聊天区底部承载的待处理 A2UI 来源 */
|
||||
activePendingA2UISource?: PendingA2UISource | null;
|
||||
/** 当前会话的 provider 选择器 */
|
||||
providerType?: string;
|
||||
}
|
||||
|
||||
function isDeferredTimelineItem(item: AgentThreadItem): boolean {
|
||||
@@ -403,6 +407,7 @@ const MessageListInner: React.FC<MessageListProps> = ({
|
||||
focusedTimelineItemId = null,
|
||||
timelineFocusRequestKey = 0,
|
||||
activePendingA2UISource = null,
|
||||
providerType,
|
||||
}) => {
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -412,6 +417,34 @@ const MessageListInner: React.FC<MessageListProps> = ({
|
||||
const [isUserScrolling, setIsUserScrolling] = useState(false);
|
||||
const [shouldAutoScroll, setShouldAutoScroll] = useState(true);
|
||||
|
||||
const shouldInspectPromptCacheNotice = useMemo(
|
||||
() =>
|
||||
Boolean(
|
||||
providerType?.trim() &&
|
||||
messages.some(
|
||||
(msg) =>
|
||||
msg.role === "assistant" &&
|
||||
!msg.isThinking &&
|
||||
msg.usage &&
|
||||
(msg.usage.cached_input_tokens ?? 0) <= 0,
|
||||
),
|
||||
),
|
||||
[messages, providerType],
|
||||
);
|
||||
const { providers } = useConfiguredProviders({
|
||||
autoLoad: shouldInspectPromptCacheNotice,
|
||||
});
|
||||
const promptCacheNotice = useMemo(
|
||||
() =>
|
||||
shouldInspectPromptCacheNotice
|
||||
? resolveConfiguredProviderPromptCacheSupportNotice(
|
||||
providers,
|
||||
providerType,
|
||||
)
|
||||
: null,
|
||||
[providerType, providers, shouldInspectPromptCacheNotice],
|
||||
);
|
||||
|
||||
const visibleMessages = useMemo(
|
||||
() =>
|
||||
messages.filter((msg) => {
|
||||
@@ -581,7 +614,9 @@ const MessageListInner: React.FC<MessageListProps> = ({
|
||||
: [];
|
||||
const trailingTimelineItems = timeline
|
||||
? dedupeDeferredTimelineItems(
|
||||
timelineConversationItems.filter((item) => isDeferredTimelineItem(item)),
|
||||
timelineConversationItems.filter((item) =>
|
||||
isDeferredTimelineItem(item),
|
||||
),
|
||||
).filter(
|
||||
(item) =>
|
||||
item.type !== "file_artifact" ||
|
||||
@@ -599,7 +634,8 @@ const MessageListInner: React.FC<MessageListProps> = ({
|
||||
const hasTrailingArtifactTimelineItems = trailingTimelineItems.some(
|
||||
(item) => item.type === "file_artifact",
|
||||
);
|
||||
const timelineActionRequests = inlineProcessCoverage.actionRequestCounts.size
|
||||
const timelineActionRequests = inlineProcessCoverage.actionRequestCounts
|
||||
.size
|
||||
? undefined
|
||||
: msg.actionRequests;
|
||||
const primaryActionRequests =
|
||||
@@ -643,12 +679,13 @@ const MessageListInner: React.FC<MessageListProps> = ({
|
||||
: null;
|
||||
const shouldRenderMessageCanvasShortcut = Boolean(
|
||||
messageSavedSiteContentTarget &&
|
||||
onOpenSavedSiteContent &&
|
||||
!hasTrailingArtifactTimelineItems,
|
||||
onOpenSavedSiteContent &&
|
||||
!hasTrailingArtifactTimelineItems,
|
||||
);
|
||||
const messageCanvasShortcutTitle = messageSavedSiteContentTarget
|
||||
? resolveSiteSavedContentTargetDisplayName(messageSavedSiteContentTarget) ||
|
||||
"导出稿"
|
||||
? resolveSiteSavedContentTargetDisplayName(
|
||||
messageSavedSiteContentTarget,
|
||||
) || "导出稿"
|
||||
: "文件";
|
||||
const messageCanvasShortcutPath = messageSavedSiteContentTarget
|
||||
? resolveSiteSavedContentTargetRelativePath(messageSavedSiteContentTarget)
|
||||
@@ -846,7 +883,14 @@ const MessageListInner: React.FC<MessageListProps> = ({
|
||||
) : null}
|
||||
|
||||
{msg.role === "assistant" && !msg.isThinking && msg.usage && (
|
||||
<TokenUsageDisplay usage={msg.usage} />
|
||||
<TokenUsageDisplay
|
||||
usage={msg.usage}
|
||||
promptCacheNotice={
|
||||
(msg.usage.cached_input_tokens ?? 0) <= 0
|
||||
? promptCacheNotice
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{msg.role === "assistant" &&
|
||||
|
||||
@@ -34,15 +34,19 @@ Agent 聊天界面的 UI 组件集合。
|
||||
| `team-workspace-board/teamWorkspaceCanvasControllerState.ts` | Team 画布控制纯状态工具,统一承载视口更新、lane 布局写回与置顶规则 |
|
||||
| `team-workspace-board/useTeamWorkspaceActivityPreviewSync.ts` | Team 过程预览同步 hook,统一承载请求去重、选中成员轮询与 stale preview 预取 |
|
||||
| `team-workspace-board/useTeamWorkspaceActivityPreviews.ts` | Team 过程预览 hook,统一承载成员最近过程的预取、轮询与 stale preview 预热 |
|
||||
| `team-workspace-board/useTeamWorkspaceBoardActionUiState.ts` | Team Board action UI 状态 hook,统一承载选中成员输入草稿、pending 状态与派生文案输入值 |
|
||||
| `team-workspace-board/useTeamWorkspaceBoardActions.ts` | Team Board 操作控制 hook,统一承载成员发送/等待/关闭与 Team 级等待/收尾的 pending 状态 |
|
||||
| `team-workspace-board/useTeamWorkspaceBoardCanvasRuntime.ts` | Team Board 画布运行态组合 hook,统一承载 lane 拼装、stage hint 与自由画布控制接线 |
|
||||
| `team-workspace-board/useTeamWorkspaceBoardComposer.ts` | Team Board 高阶编排 hook,统一承载 runtime、activity、canvas、presentation 与 shell/empty-shell props 组合 |
|
||||
| `team-workspace-board/useTeamWorkspaceBoardFormationState.ts` | Team Board 分工展示前置 hook,统一承载 selected team 归一化与 runtime formation / plan display 组装 |
|
||||
| `team-workspace-board/useTeamWorkspaceBoardOperationState.ts` | Team Board 运行态派生 hook,统一承载 team operation、wait summary 与 session control 组装 |
|
||||
| `team-workspace-board/useTeamWorkspaceBoardRuntimeState.ts` | Team Board 运行态组合 hook,统一承载 team 轨迹、焦点成员、会话控制与交互 action 组装 |
|
||||
| `team-workspace-board/useTeamWorkspaceBoardSelectedSessionActionState.ts` | Team Board 当前选中成员 capability hook,统一承载 open/resume/send/wait/close 可用性派生 |
|
||||
| `team-workspace-board/useTeamWorkspaceBoardShellProps.ts` | Team Board 壳层 props 组合 hook,统一承载 empty shell / shell props 映射与当前选中成员 inline detail 装配 |
|
||||
| `team-workspace-board/useTeamWorkspaceBoardSessionGraph.ts` | Team Board 会话图组合 hook,统一承载 orchestrator/current child/visible/member canvas/rail 的派生组装 |
|
||||
| `team-workspace-board/useTeamWorkspaceBoardSelectedInlineDetail.tsx` | Team Board 当前选中成员详情组合 hook,统一承载 inline detail 条件渲染与 props 映射 |
|
||||
| `team-workspace-board/useTeamWorkspaceBoardPresentation.tsx` | Team Board 展示态 hook,统一承载 chrome 文案、详情展示与壳层样式拼装 |
|
||||
| `team-workspace-board/teamWorkspaceBoardPresentationSelectors.ts` | Team Board 展示态 selector,统一承载细节可见性、成员画布文案与壳层 className 派生 |
|
||||
| `team-workspace-board/useTeamWorkspaceCanvasController.ts` | Team Board 画布控制 hook,统一承载自由画布的布局状态、持久化、lane 布局派生与交互接线 |
|
||||
| `team-workspace-board/useTeamWorkspaceCanvasInteractionHandlers.ts` | Team 画布交互 hook,统一承载拖拽、resize、平移、缩放、自动布局、fit 与选中行为 |
|
||||
| `team-workspace-board/useTeamWorkspaceCanvasKeyboardShortcuts.ts` | Team 画布键盘快捷键 hook,统一承载 Space 手型、缩放、适应与方向键平移 |
|
||||
|
||||
@@ -356,7 +356,7 @@ describe("StreamingRenderer", () => {
|
||||
thinkingContent: "先检查滚动触发逻辑\n再确认输出展开时机",
|
||||
toolCalls: [
|
||||
{
|
||||
id: "tool-process-group-legacy",
|
||||
id: "tool-process-group-fallback",
|
||||
name: "functions.exec_command",
|
||||
arguments: JSON.stringify({ cmd: "rg -n scrollKey src" }),
|
||||
status: "completed",
|
||||
@@ -431,7 +431,7 @@ describe("StreamingRenderer", () => {
|
||||
thinkingContent: "这段思考应由 timeline 承载",
|
||||
toolCalls: [
|
||||
{
|
||||
id: "tool-suppressed-legacy",
|
||||
id: "tool-suppressed-fallback",
|
||||
name: "functions.exec_command",
|
||||
arguments: JSON.stringify({ cmd: "rg -n duplicate src" }),
|
||||
status: "completed",
|
||||
@@ -442,7 +442,7 @@ describe("StreamingRenderer", () => {
|
||||
],
|
||||
actionRequests: [
|
||||
{
|
||||
requestId: "req-suppressed-legacy",
|
||||
requestId: "req-suppressed-fallback",
|
||||
actionType: "tool_confirmation",
|
||||
status: "pending",
|
||||
prompt: "请确认是否继续",
|
||||
|
||||
@@ -1602,25 +1602,25 @@ export const StreamingRenderer: React.FC<StreamingRendererProps> = memo(
|
||||
);
|
||||
}
|
||||
|
||||
// 回退模式:将思考、工具和确认收敛成同一条执行过程
|
||||
const legacyProcessEntries: StreamingProcessEntry[] = [];
|
||||
// 非交错内容统一收敛到同一条执行过程,避免重复渲染多条过程流。
|
||||
const fallbackProcessEntries: StreamingProcessEntry[] = [];
|
||||
if (finalThinking) {
|
||||
legacyProcessEntries.push({
|
||||
fallbackProcessEntries.push({
|
||||
kind: "thinking",
|
||||
id: "legacy-thinking",
|
||||
id: "fallback-thinking",
|
||||
text: finalThinking,
|
||||
defaultExpanded: isStreaming,
|
||||
});
|
||||
}
|
||||
for (const toolCall of suppressProcessFlow ? [] : toolCalls || []) {
|
||||
legacyProcessEntries.push({
|
||||
fallbackProcessEntries.push({
|
||||
kind: "tool",
|
||||
id: toolCall.id,
|
||||
toolCall,
|
||||
});
|
||||
}
|
||||
for (const request of suppressProcessFlow ? [] : visibleActionRequests) {
|
||||
legacyProcessEntries.push({
|
||||
fallbackProcessEntries.push({
|
||||
kind: "action",
|
||||
id: request.requestId,
|
||||
actionRequired: request,
|
||||
@@ -1634,8 +1634,8 @@ export const StreamingRenderer: React.FC<StreamingRendererProps> = memo(
|
||||
{shouldShowRuntimeStatus && runtimeStatus ? (
|
||||
<AgentRuntimeStatusBlock status={runtimeStatus} />
|
||||
) : null}
|
||||
{legacyProcessEntries.length > 0
|
||||
? renderProcessRun(legacyProcessEntries, "legacy-process")
|
||||
{fallbackProcessEntries.length > 0
|
||||
? renderProcessRun(fallbackProcessEntries, "fallback-process")
|
||||
: null}
|
||||
|
||||
{/* 解析后的内容区域(包括 A2UI、write_file、普通文本) */}
|
||||
|
||||
@@ -55,7 +55,7 @@ describe("TeamMemoryShadowCard", () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("应展示 repo 作用域和关键 Team 记忆条目", () => {
|
||||
it("应展示 repo 作用域和关键任务记忆条目", () => {
|
||||
const container = renderCard({
|
||||
repoScope: "/workspace/lime",
|
||||
entries: {
|
||||
@@ -66,7 +66,7 @@ describe("TeamMemoryShadowCard", () => {
|
||||
},
|
||||
"team.subagents": {
|
||||
key: "team.subagents",
|
||||
content: "子代理:\n- 研究代理 [running] explorer · 梳理主线风险",
|
||||
content: "子任务:\n- 研究代理 [running] explorer · 梳理主线风险",
|
||||
updatedAt: 300,
|
||||
},
|
||||
"team.parent_context": {
|
||||
@@ -80,13 +80,15 @@ describe("TeamMemoryShadowCard", () => {
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-memory-shadow-card"]'),
|
||||
).not.toBeNull();
|
||||
expect(container.textContent).toContain("协作记忆影子");
|
||||
expect(container.textContent).toContain("任务记忆影子");
|
||||
expect(container.textContent).toContain(
|
||||
"当前仓库已缓存 3 条 Team 续接上下文",
|
||||
"当前仓库已缓存 3 条任务续接上下文",
|
||||
);
|
||||
expect(container.textContent).toContain("/workspace/lime");
|
||||
expect(container.textContent).toContain("当前 Team");
|
||||
expect(container.textContent).toContain("子代理概览");
|
||||
expect(container.textContent).toContain("当前任务方案");
|
||||
expect(container.textContent).toContain("任务方案:研究双人组");
|
||||
expect(container.textContent).toContain("子任务概览");
|
||||
expect(container.textContent).toContain("子任务:");
|
||||
expect(container.textContent).toContain("父会话上下文");
|
||||
expect(container.textContent).toContain(
|
||||
"研究代理 [running] explorer · 梳理主线风险",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BrainCircuit, FolderTree, Sparkles, Users } from "lucide-react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import type { TeamMemoryEntry, TeamMemorySnapshot } from "@/lib/teamMemorySync";
|
||||
import { normalizeTeamMemoryDisplayText } from "../utils/teamMemoryDisplay";
|
||||
|
||||
interface TeamMemoryShadowCardProps {
|
||||
snapshot?: TeamMemorySnapshot | null;
|
||||
@@ -16,11 +17,11 @@ interface TeamMemoryDisplayEntry {
|
||||
|
||||
const ENTRY_META: Record<string, { label: string; icon: typeof Users }> = {
|
||||
"team.selection": {
|
||||
label: "当前 Team",
|
||||
label: "当前任务方案",
|
||||
icon: Users,
|
||||
},
|
||||
"team.subagents": {
|
||||
label: "子代理概览",
|
||||
label: "子任务概览",
|
||||
icon: Sparkles,
|
||||
},
|
||||
"team.parent_context": {
|
||||
@@ -54,6 +55,7 @@ function toDisplayEntry(entry: TeamMemoryEntry): TeamMemoryDisplayEntry {
|
||||
icon: meta.icon,
|
||||
lines: entry.content
|
||||
.split("\n")
|
||||
.map((line) => normalizeTeamMemoryDisplayText(line))
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.slice(0, 4),
|
||||
@@ -83,14 +85,13 @@ export function TeamMemoryShadowCard({
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.12em] text-slate-500">
|
||||
<BrainCircuit className="h-3.5 w-3.5" />
|
||||
<span>协作记忆影子</span>
|
||||
<span>任务记忆影子</span>
|
||||
</div>
|
||||
<div className="mt-2 text-sm font-semibold text-slate-900">
|
||||
当前仓库已缓存 {entries.length} 条 Team 续接上下文
|
||||
当前仓库已缓存 {entries.length} 条任务续接上下文
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-5 text-slate-500">
|
||||
仅保存在当前仓库本地作用域,用来续接 Team
|
||||
选择、子代理概览和父会话上下文。
|
||||
仅保存在当前仓库本地作用域,用来续接任务方案选择、子任务概览和父会话上下文。
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
|
||||
@@ -72,10 +72,94 @@ describe("TeamWorkbenchSummaryPanel", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("团队工作台");
|
||||
expect(container.textContent).toContain("协作记忆影子");
|
||||
expect(container.textContent).toContain("任务工作台");
|
||||
expect(container.textContent).toContain("任务记忆影子");
|
||||
expect(container.textContent).toContain("/workspace/lime");
|
||||
expect(container.textContent).toContain("当前 Team");
|
||||
expect(container.textContent).toContain("当前任务方案");
|
||||
expect(container.textContent).toContain("研究双人组");
|
||||
});
|
||||
|
||||
it("任务分工已就绪时应展示当前任务视角", () => {
|
||||
const container = renderPanel({
|
||||
teamDispatchPreviewState: {
|
||||
requestId: "runtime-formed-1",
|
||||
status: "formed",
|
||||
label: "修复 Team",
|
||||
summary: "分析、执行、验证三段式推进。",
|
||||
members: [
|
||||
{
|
||||
id: "task-1",
|
||||
label: "分析",
|
||||
summary: "收敛问题边界。",
|
||||
roleKey: "explorer",
|
||||
profileId: "code-explorer",
|
||||
skillIds: ["repo-exploration"],
|
||||
status: "planned",
|
||||
},
|
||||
{
|
||||
id: "task-2",
|
||||
label: "执行",
|
||||
summary: "落地修复并回传结果。",
|
||||
roleKey: "executor",
|
||||
profileId: "code-executor",
|
||||
skillIds: ["bounded-implementation"],
|
||||
status: "planned",
|
||||
},
|
||||
],
|
||||
blueprint: {
|
||||
label: "代码排障团队",
|
||||
summary: "分析、执行、验证三段式推进。",
|
||||
roles: [],
|
||||
},
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("任务分工已准备好");
|
||||
expect(container.textContent).toContain("当前任务分工");
|
||||
expect(container.textContent).toContain(
|
||||
"当前任务方案已就绪。任务拆出后,这里会从方案视图过渡到任务视图。",
|
||||
);
|
||||
expect(container.textContent).toContain("参考方案:代码排障团队");
|
||||
});
|
||||
|
||||
it("最近动态应使用任务叙事", () => {
|
||||
const container = renderPanel({
|
||||
childSubagentSessions: [
|
||||
{
|
||||
id: "child-1",
|
||||
name: "分析",
|
||||
created_at: 1_710_000_000,
|
||||
updated_at: 1_710_000_100,
|
||||
session_type: "sub_agent",
|
||||
runtime_status: "completed",
|
||||
latest_turn_status: "completed",
|
||||
task_summary: "已整理问题边界",
|
||||
},
|
||||
{
|
||||
id: "child-2",
|
||||
name: "执行",
|
||||
created_at: 1_710_000_010,
|
||||
updated_at: 1_710_000_120,
|
||||
session_type: "sub_agent",
|
||||
runtime_status: "completed",
|
||||
latest_turn_status: "completed",
|
||||
task_summary: "已完成修复",
|
||||
},
|
||||
],
|
||||
teamControlSummary: {
|
||||
action: "close_completed",
|
||||
requestedSessionIds: ["child-1", "child-2"],
|
||||
cascadeSessionIds: [],
|
||||
affectedSessionIds: ["child-1", "child-2"],
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("最近动态");
|
||||
expect(container.textContent).toContain(
|
||||
"最近一次收尾操作收起了 2 项已完成任务。",
|
||||
);
|
||||
expect(container.textContent).toContain("任务轨道");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,11 +13,12 @@ import type {
|
||||
TeamWorkspaceWaitSummary,
|
||||
} from "../teamWorkspaceRuntime";
|
||||
import {
|
||||
resolveRuntimeFormationStatusMeta,
|
||||
resolveRuntimeMemberStatusMeta,
|
||||
summarizeTeamWorkspaceExecution,
|
||||
} from "../teamWorkspaceRuntime";
|
||||
import { buildRuntimeFormationDisplayState } from "../team-workspace-runtime/formationDisplaySelectors";
|
||||
import type { TeamRoleDefinition } from "../utils/teamDefinitions";
|
||||
import { normalizeTeamWorkspaceDisplayValue } from "../utils/teamWorkspaceDisplay";
|
||||
import { TeamMemoryShadowCard } from "./TeamMemoryShadowCard";
|
||||
|
||||
interface TeamWorkbenchSummaryPanelProps {
|
||||
@@ -46,19 +47,19 @@ function buildOperationSummary(params: {
|
||||
const affectedCount = params.teamControlSummary.affectedSessionIds.length;
|
||||
switch (params.teamControlSummary.action) {
|
||||
case "resume":
|
||||
return `最近一次恢复操作影响 ${affectedCount} 个 agent。`;
|
||||
return `最近一次继续操作影响 ${affectedCount} 项任务。`;
|
||||
case "close_completed":
|
||||
return `最近一次收尾操作关闭了 ${affectedCount} 位已完成成员。`;
|
||||
return `最近一次收尾操作收起了 ${affectedCount} 项已完成任务。`;
|
||||
case "close":
|
||||
default:
|
||||
return `最近一次关闭操作影响 ${affectedCount} 个 agent。`;
|
||||
return `最近一次暂停操作影响 ${affectedCount} 项任务。`;
|
||||
}
|
||||
}
|
||||
|
||||
if (params.teamWaitSummary) {
|
||||
return params.teamWaitSummary.timedOut
|
||||
? `最近一次等待超时,仍有 ${params.teamWaitSummary.awaitedSessionIds.length} 位成员在推进。`
|
||||
: "最近一次等待已收到成员结果。";
|
||||
? `最近一次等待超时,仍有 ${params.teamWaitSummary.awaitedSessionIds.length} 项任务在推进。`
|
||||
: "最近一次等待已收到任务结果。";
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -91,21 +92,13 @@ export function TeamWorkbenchSummaryPanel({
|
||||
subagentParentContext,
|
||||
liveRuntimeBySessionId,
|
||||
});
|
||||
const hasRealTeamGraph =
|
||||
childSubagentSessions.length > 0 || Boolean(subagentParentContext);
|
||||
const runtimeFormationMeta = dispatchPreviewState
|
||||
? resolveRuntimeFormationStatusMeta(dispatchPreviewState.status)
|
||||
: null;
|
||||
const runtimeTeamLabel =
|
||||
dispatchPreviewState?.label?.trim() ||
|
||||
dispatchPreviewState?.blueprint?.label?.trim() ||
|
||||
selectedTeamLabel?.trim() ||
|
||||
null;
|
||||
const runtimeSummaryText =
|
||||
dispatchPreviewState?.summary?.trim() ||
|
||||
dispatchPreviewState?.blueprint?.summary?.trim() ||
|
||||
selectedTeamSummary ||
|
||||
null;
|
||||
const hasRuntimeSessions = executionSummary.totalSessionCount > 0;
|
||||
const runtimeFormationDisplay = buildRuntimeFormationDisplayState({
|
||||
teamDispatchPreviewState: dispatchPreviewState,
|
||||
fallbackLabel: selectedTeamLabel,
|
||||
fallbackSummary: selectedTeamSummary,
|
||||
});
|
||||
const runtimeTeamLabel = runtimeFormationDisplay.panelLabel;
|
||||
const operationSummary = buildOperationSummary({
|
||||
teamWaitSummary,
|
||||
teamControlSummary,
|
||||
@@ -113,33 +106,37 @@ export function TeamWorkbenchSummaryPanel({
|
||||
const latestActivity = Object.values(liveActivityBySessionId)
|
||||
.flat()
|
||||
.slice(0, 4);
|
||||
const displayRoleCount = hasRealTeamGraph
|
||||
const selectedRoleCount = (selectedTeamRoles ?? []).filter((role) =>
|
||||
role.label.trim(),
|
||||
).length;
|
||||
const displayRoleCount = hasRuntimeSessions
|
||||
? executionSummary.totalSessionCount
|
||||
: (dispatchPreviewState?.members.length ?? 0);
|
||||
: (dispatchPreviewState?.members.length ?? selectedRoleCount);
|
||||
const roleCards = dispatchPreviewState?.members.length
|
||||
? dispatchPreviewState.members.map((member) => ({
|
||||
id: member.id,
|
||||
label: member.label,
|
||||
label: normalizeTeamWorkspaceDisplayValue(member.label) || member.label,
|
||||
roleKey: member.roleKey,
|
||||
profileId: member.profileId,
|
||||
summary: member.summary,
|
||||
summary:
|
||||
normalizeTeamWorkspaceDisplayValue(member.summary) || member.summary,
|
||||
skillIds: member.skillIds,
|
||||
statusMeta: resolveRuntimeMemberStatusMeta(member.status),
|
||||
}))
|
||||
: (selectedTeamRoles ?? []).map((role) => ({
|
||||
id: role.id,
|
||||
label: role.label,
|
||||
label: normalizeTeamWorkspaceDisplayValue(role.label) || role.label,
|
||||
roleKey: role.roleKey,
|
||||
profileId: role.profileId,
|
||||
summary: role.summary,
|
||||
summary: normalizeTeamWorkspaceDisplayValue(role.summary) || role.summary,
|
||||
skillIds: role.skillIds ?? [],
|
||||
statusMeta: null,
|
||||
}));
|
||||
const summaryCards = [
|
||||
{
|
||||
label: "活跃 Agent",
|
||||
label: "活跃任务",
|
||||
value: String(executionSummary.activeSessionCount),
|
||||
hint: executionSummary.hasActiveRuntime ? "正在协作" : "尚未运行",
|
||||
hint: executionSummary.hasActiveRuntime ? "任务进行中" : "尚未运行",
|
||||
},
|
||||
{
|
||||
label: "处理中",
|
||||
@@ -152,21 +149,25 @@ export function TeamWorkbenchSummaryPanel({
|
||||
hint: executionSummary.queuedSessionCount > 0 ? "按顺序继续" : "暂无",
|
||||
},
|
||||
{
|
||||
label: hasRealTeamGraph
|
||||
label: hasRuntimeSessions
|
||||
? "总会话"
|
||||
: dispatchPreviewState
|
||||
? "当前成员"
|
||||
: "总会话",
|
||||
? "当前任务"
|
||||
: selectedRoleCount > 0
|
||||
? "计划分工"
|
||||
: "总会话",
|
||||
value: String(
|
||||
hasRealTeamGraph
|
||||
hasRuntimeSessions
|
||||
? executionSummary.totalSessionCount
|
||||
: displayRoleCount,
|
||||
),
|
||||
hint: hasRealTeamGraph
|
||||
? "已进入团队图谱"
|
||||
hint: hasRuntimeSessions
|
||||
? "已进入任务轨道"
|
||||
: dispatchPreviewState
|
||||
? "当前编队"
|
||||
: "等待创建",
|
||||
? "当前分工"
|
||||
: selectedRoleCount > 0
|
||||
? "已选方案"
|
||||
: "等待创建",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -175,55 +176,45 @@ export function TeamWorkbenchSummaryPanel({
|
||||
<section className="rounded-[24px] border border-slate-200/80 bg-white p-4 shadow-sm shadow-slate-950/5">
|
||||
<div className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.12em] text-slate-500">
|
||||
<Workflow className="h-3.5 w-3.5" />
|
||||
<span>团队工作台</span>
|
||||
<span>任务工作台</span>
|
||||
{runtimeTeamLabel ? (
|
||||
<span className="rounded-full border border-sky-200 bg-sky-50 px-2 py-0.5 text-[10px] font-medium tracking-normal text-sky-700 normal-case">
|
||||
{runtimeTeamLabel}
|
||||
</span>
|
||||
) : null}
|
||||
{runtimeFormationMeta ? (
|
||||
{runtimeFormationDisplay.panelStatusLabel ? (
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-full px-2 py-0.5 text-[10px] font-medium tracking-normal normal-case",
|
||||
runtimeFormationMeta.badgeClassName,
|
||||
runtimeFormationDisplay.panelStatusBadgeClassName,
|
||||
)}
|
||||
>
|
||||
{runtimeFormationMeta.label}
|
||||
{runtimeFormationDisplay.panelStatusLabel}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-2 text-sm font-semibold text-slate-900">
|
||||
{executionSummary.statusTitle ||
|
||||
runtimeFormationMeta?.title ||
|
||||
"团队已就绪,等待主代理开始编排"}
|
||||
runtimeFormationDisplay.panelHeadline ||
|
||||
"任务工作台已就绪,等待主线程开始编排"}
|
||||
</div>
|
||||
<p className="mt-2 text-xs leading-5 text-slate-500">
|
||||
{dispatchPreviewState?.status === "failed"
|
||||
? dispatchPreviewState.errorMessage?.trim() ||
|
||||
"这次 Team 准备失败,可继续在当前对话中推进。"
|
||||
: runtimeSummaryText ||
|
||||
"这里展示团队总览与运行密度;主对话只保留调度记录,角色执行正文在左侧 Team 画布查看。"}
|
||||
{runtimeFormationDisplay.panelDescription}
|
||||
</p>
|
||||
{!hasRealTeamGraph && dispatchPreviewState ? (
|
||||
{!hasRuntimeSessions && dispatchPreviewState ? (
|
||||
<div
|
||||
className={cn(
|
||||
"mt-3 rounded-2xl border px-3 py-2 text-xs leading-5",
|
||||
runtimeFormationMeta?.badgeClassName ||
|
||||
runtimeFormationDisplay.panelStatusBadgeClassName ||
|
||||
"border border-slate-200 bg-slate-50 text-slate-700",
|
||||
)}
|
||||
>
|
||||
{dispatchPreviewState.status === "forming"
|
||||
? "模型正在依据当前任务准备 Team,真实成员加入后会自动切换到实时协作轨道。"
|
||||
: dispatchPreviewState.status === "formed"
|
||||
? `已准备 ${dispatchPreviewState.members.length} 个成员,当前等待系统分派真实成员进入协作。`
|
||||
: dispatchPreviewState.errorMessage?.trim() ||
|
||||
"暂未成功准备这次 Team。"}
|
||||
{runtimeFormationDisplay.noticeText}
|
||||
</div>
|
||||
) : null}
|
||||
{!hasRealTeamGraph && !dispatchPreviewState ? (
|
||||
{!hasRuntimeSessions && !dispatchPreviewState ? (
|
||||
<div className="mt-3 rounded-2xl border border-sky-100 bg-sky-50 px-3 py-2 text-xs leading-5 text-sky-700">
|
||||
尚未出现真实团队成员。开始分派成员后,左侧 Team
|
||||
画布会自动切换为实时协作视图。
|
||||
尚未接入任务。发送后这里会先展示分工,再过渡到任务视图。
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
@@ -253,7 +244,7 @@ export function TeamWorkbenchSummaryPanel({
|
||||
<section className="rounded-[24px] border border-slate-200/80 bg-white p-4 shadow-sm shadow-slate-950/5">
|
||||
<div className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.12em] text-slate-500">
|
||||
<Bot className="h-3.5 w-3.5" />
|
||||
<span>{dispatchPreviewState ? "当前成员" : "角色分工"}</span>
|
||||
<span>{dispatchPreviewState ? "当前任务分工" : "角色分工"}</span>
|
||||
</div>
|
||||
<div className="mt-3 space-y-2">
|
||||
{roleCards.map((role) => (
|
||||
@@ -304,9 +295,9 @@ export function TeamWorkbenchSummaryPanel({
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{dispatchPreviewState?.blueprint?.label ? (
|
||||
{runtimeFormationDisplay.referenceLabel ? (
|
||||
<div className="mt-3 rounded-2xl border border-slate-200 bg-slate-50 px-3 py-2 text-xs leading-5 text-slate-500">
|
||||
参考蓝图 Team:{dispatchPreviewState.blueprint.label}
|
||||
参考方案:{runtimeFormationDisplay.referenceLabel}
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
@@ -359,7 +350,7 @@ export function TeamWorkbenchSummaryPanel({
|
||||
<span>交互说明</span>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
左侧主画布负责展示成员轨道、实时过程与细节;右侧用于总览、角色结构与快速理解当前协作状态。
|
||||
左侧主画布负责展示任务轨道、实时过程与细节;右侧用于总览、任务结构与快速理解当前进展状态。
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-2 text-sky-700">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
|
||||
@@ -233,9 +233,10 @@ describe("TeamWorkspaceBoard", () => {
|
||||
childSubagentSessions: [],
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("任务协作");
|
||||
expect(container.textContent).toContain("还没有成员接入");
|
||||
expect(container.textContent).toContain("查看任务进行时");
|
||||
expect(container.textContent).toContain("任务工作台");
|
||||
expect(container.textContent).toContain("还没有任务接手");
|
||||
expect(container.textContent).toContain("查看任务视图");
|
||||
expect(container.textContent).not.toContain("任务进行时");
|
||||
expect(container.textContent).not.toContain("spawn_agent");
|
||||
expect(container.textContent).not.toContain("Explorer 槽位");
|
||||
expect(container.textContent).not.toContain("Executor 槽位");
|
||||
@@ -256,8 +257,8 @@ describe("TeamWorkspaceBoard", () => {
|
||||
expandButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("需要时会自动接入协作成员");
|
||||
expect(container.textContent).toContain("邀请协作成员");
|
||||
expect(container.textContent).toContain("需要时会自动拆出任务");
|
||||
expect(container.textContent).toContain("拆出任务");
|
||||
expect(container.textContent).toContain("收起细节");
|
||||
});
|
||||
|
||||
@@ -287,7 +288,7 @@ describe("TeamWorkspaceBoard", () => {
|
||||
],
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("临时修复 Team");
|
||||
expect(container.textContent).toContain("临时修复任务方案");
|
||||
expect(container.textContent).toContain("2 个计划分工");
|
||||
|
||||
const expandButton = container.querySelector(
|
||||
@@ -398,8 +399,10 @@ describe("TeamWorkspaceBoard", () => {
|
||||
],
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("任务协作");
|
||||
expect(container.textContent).toContain("2 位成员协作中");
|
||||
expect(container.textContent).toContain("任务工作台");
|
||||
expect(container.textContent).toContain(
|
||||
"任务进行中 · 1 项处理中 / 1 项稍后开始",
|
||||
);
|
||||
expect(container.textContent).toContain("研究员");
|
||||
expect(container.textContent).toContain("执行器");
|
||||
expect(container.textContent).toContain("代码分析员");
|
||||
@@ -417,13 +420,25 @@ describe("TeamWorkspaceBoard", () => {
|
||||
?.getAttribute("data-expanded"),
|
||||
).toBe("false");
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-canvas-toolbar"]')
|
||||
container.querySelector('[data-testid="team-workspace-compact-summary"]')
|
||||
?.textContent,
|
||||
).toContain("焦点 研究员");
|
||||
).toContain("当前焦点");
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-compact-summary"]')
|
||||
?.textContent,
|
||||
).toContain("当前焦点会优先落在正在处理的任务上。");
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-canvas-toolbar"]')
|
||||
?.textContent,
|
||||
).toContain("缩放 100%");
|
||||
).toContain("处理中");
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-canvas-toolbar"]')
|
||||
?.textContent,
|
||||
).not.toContain("当前焦点 研究员");
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-canvas-toolbar"]')
|
||||
?.textContent,
|
||||
).not.toContain("缩放 100%");
|
||||
expect(container.textContent).toContain("最近进展");
|
||||
expect(container.textContent).toContain(
|
||||
"回复:已完成竞品摘要与数据来源梳理。",
|
||||
@@ -438,7 +453,7 @@ describe("TeamWorkspaceBoard", () => {
|
||||
const executorLane = container.querySelector(
|
||||
'[data-testid="team-workspace-member-lane-child-2"]',
|
||||
);
|
||||
expect(researcherLane?.textContent).toContain("成员进展");
|
||||
expect(researcherLane?.textContent).toContain("任务进展");
|
||||
expect(researcherLane?.textContent).toContain(
|
||||
"回复:已完成竞品摘要与数据来源梳理。",
|
||||
);
|
||||
@@ -465,14 +480,14 @@ describe("TeamWorkspaceBoard", () => {
|
||||
expect(container.textContent).toContain("已对 3 个来源完成去重校验。");
|
||||
|
||||
const openButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(element) => element.textContent?.includes("打开对话"),
|
||||
(element) => element.textContent?.includes("打开任务"),
|
||||
);
|
||||
await clickElement(openButton ?? null);
|
||||
|
||||
expect(onOpenSubagentSession).toHaveBeenCalledWith("child-1");
|
||||
});
|
||||
|
||||
it("真实成员存在时应优先按蓝图角色顺序排列泳道", async () => {
|
||||
it("真实任务存在时应优先把处理中任务排到前面,并在同优先级下保持蓝图顺序", async () => {
|
||||
const container = await renderBoard({
|
||||
childSubagentSessions: [
|
||||
{
|
||||
@@ -512,15 +527,6 @@ describe("TeamWorkspaceBoard", () => {
|
||||
label: "修复 Team",
|
||||
summary: "分析、执行协同推进。",
|
||||
members: [
|
||||
{
|
||||
id: "runtime-explorer",
|
||||
label: "分析",
|
||||
summary: "先定位问题。",
|
||||
roleKey: "explorer",
|
||||
profileId: "code-explorer",
|
||||
skillIds: ["repo-exploration"],
|
||||
status: "planned",
|
||||
},
|
||||
{
|
||||
id: "runtime-executor",
|
||||
label: "执行",
|
||||
@@ -530,6 +536,15 @@ describe("TeamWorkspaceBoard", () => {
|
||||
skillIds: ["bounded-implementation"],
|
||||
status: "planned",
|
||||
},
|
||||
{
|
||||
id: "runtime-explorer",
|
||||
label: "分析",
|
||||
summary: "先定位问题。",
|
||||
roleKey: "explorer",
|
||||
profileId: "code-explorer",
|
||||
skillIds: ["repo-exploration"],
|
||||
status: "planned",
|
||||
},
|
||||
],
|
||||
blueprint: null,
|
||||
updatedAt: Date.now(),
|
||||
@@ -654,13 +669,29 @@ describe("TeamWorkspaceBoard", () => {
|
||||
),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-canvas-toolbar"]')
|
||||
container.querySelector('[data-testid="team-workspace-compact-summary"]')
|
||||
?.textContent,
|
||||
).toContain("焦点 实现代理");
|
||||
).toContain("当前焦点");
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-compact-summary"]')
|
||||
?.textContent,
|
||||
).toContain("并行任务会在各自面板里持续更新进展和结果");
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-canvas-toolbar"]')
|
||||
?.textContent,
|
||||
).toContain("缩放 100%");
|
||||
).toContain("处理中");
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-canvas-toolbar"]')
|
||||
?.textContent,
|
||||
).toContain("当前任务");
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-canvas-toolbar"]')
|
||||
?.textContent,
|
||||
).not.toContain("当前焦点 实现代理");
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-canvas-toolbar"]')
|
||||
?.textContent,
|
||||
).not.toContain("缩放 100%");
|
||||
expect(container.textContent).toContain("最近进展");
|
||||
expect(container.textContent).toContain(
|
||||
"推理:先检查 team runtime 的控制面状态,再决定是否等待。",
|
||||
@@ -672,7 +703,7 @@ describe("TeamWorkspaceBoard", () => {
|
||||
const siblingLane = container.querySelector(
|
||||
'[data-testid="team-workspace-member-lane-child-sibling-1"]',
|
||||
);
|
||||
expect(currentLane?.textContent).toContain("成员进展");
|
||||
expect(currentLane?.textContent).toContain("任务进展");
|
||||
expect(currentLane?.textContent).toContain(
|
||||
"推理:先检查 team runtime 的控制面状态,再决定是否等待。",
|
||||
);
|
||||
@@ -682,7 +713,7 @@ describe("TeamWorkspaceBoard", () => {
|
||||
);
|
||||
|
||||
const returnButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(element) => element.textContent?.includes("返回主助手"),
|
||||
(element) => element.textContent?.includes("返回主任务"),
|
||||
);
|
||||
expect(returnButton).toBeTruthy();
|
||||
|
||||
@@ -761,8 +792,9 @@ describe("TeamWorkspaceBoard", () => {
|
||||
expect(embeddedShell?.className).toContain("pointer-events-auto");
|
||||
expect(embeddedShell?.className).toContain("overflow-hidden");
|
||||
expect(embeddedShell?.className).toContain("flex-col");
|
||||
expect(embeddedShell?.className).toContain("bg-transparent");
|
||||
expect(embeddedShell?.className).toContain("border-0");
|
||||
expect(embeddedShell?.className).toContain("rounded-[24px]");
|
||||
expect(embeddedShell?.className).toContain("border-slate-200");
|
||||
expect(embeddedShell?.className).toContain("bg-white");
|
||||
expect(embeddedShell?.className).not.toContain("backdrop-blur");
|
||||
expect(boardBody?.className).toContain("overflow-y-auto");
|
||||
expect(railList?.getAttribute("data-layout-kind")).toBe("free-canvas");
|
||||
@@ -911,7 +943,7 @@ describe("TeamWorkspaceBoard", () => {
|
||||
{
|
||||
id: "status-child-live-1-running",
|
||||
title: "状态切换",
|
||||
detail: "收到 team 状态事件,已切换为运行中。",
|
||||
detail: "收到任务状态事件,已切换为运行中。",
|
||||
statusLabel: "运行中",
|
||||
badgeClassName: "border border-sky-200 bg-sky-50 text-sky-700",
|
||||
},
|
||||
@@ -1648,11 +1680,11 @@ describe("TeamWorkspaceBoard", () => {
|
||||
onWaitActiveTeamSessions,
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("等待任一成员结果");
|
||||
expect(container.textContent).toContain("2 位处理中");
|
||||
expect(container.textContent).toContain("等待任一任务结果");
|
||||
expect(container.textContent).toContain("2 项处理中");
|
||||
|
||||
const waitAnyButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(element) => element.textContent?.includes("等待任一成员结果"),
|
||||
(element) => element.textContent?.includes("等待任一任务结果"),
|
||||
);
|
||||
expect(waitAnyButton).toBeTruthy();
|
||||
|
||||
@@ -1705,9 +1737,14 @@ describe("TeamWorkspaceBoard", () => {
|
||||
const operations = container.querySelector(
|
||||
'[data-testid="team-workspace-team-operations"]',
|
||||
);
|
||||
const operationList = container.querySelector(
|
||||
'[data-testid="team-workspace-team-operations-list"]',
|
||||
);
|
||||
|
||||
expect(operations).toBeTruthy();
|
||||
expect(operations?.className).toContain("overflow-x-auto");
|
||||
expect(operations?.textContent).toContain("任务进展");
|
||||
expect(operations?.textContent).toContain("最近 1 条");
|
||||
expect(operationList).toBeTruthy();
|
||||
expect(operations?.textContent).toContain("收到结果");
|
||||
expect(operations?.textContent).toContain("刚才等到 执行器 返回了新结果");
|
||||
expect(operations?.textContent).toContain("当前状态为已完成");
|
||||
@@ -1797,8 +1834,10 @@ describe("TeamWorkspaceBoard", () => {
|
||||
);
|
||||
|
||||
expect(operations).toBeTruthy();
|
||||
expect(operations?.textContent).toContain("任务进展");
|
||||
expect(operations?.textContent).toContain("最近 1 条");
|
||||
expect(operations?.textContent).toContain("暂停处理");
|
||||
expect(operations?.textContent).toContain("刚才已暂停 2 位协作成员的处理");
|
||||
expect(operations?.textContent).toContain("刚才已暂停 2 项任务的处理");
|
||||
});
|
||||
|
||||
it("点击 Team 轨迹项时应切换焦点到对应 agent", async () => {
|
||||
@@ -1907,12 +1946,12 @@ describe("TeamWorkspaceBoard", () => {
|
||||
onCloseCompletedTeamSessions,
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("收起已完成成员");
|
||||
expect(container.textContent).toContain("2 位已完成");
|
||||
expect(container.textContent).toContain("收起已完成任务");
|
||||
expect(container.textContent).toContain("2 项已完成");
|
||||
|
||||
const closeCompletedButton = Array.from(
|
||||
container.querySelectorAll("button"),
|
||||
).find((element) => element.textContent?.includes("收起已完成成员"));
|
||||
).find((element) => element.textContent?.includes("收起已完成任务"));
|
||||
expect(closeCompletedButton).toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
@@ -2040,6 +2079,9 @@ describe("TeamWorkspaceBoard", () => {
|
||||
expect(header).toBeTruthy();
|
||||
expect(header?.className).toContain("sticky");
|
||||
expect(header?.className).toContain("top-0");
|
||||
expect(header?.textContent).toContain("任务工作台");
|
||||
expect(header?.textContent).toContain("任务进行中");
|
||||
expect(header?.textContent).not.toContain("任务进行时");
|
||||
});
|
||||
|
||||
it("本轮 Team 准备中时,应在空 shell 展示组建状态", async () => {
|
||||
@@ -2062,7 +2104,7 @@ describe("TeamWorkspaceBoard", () => {
|
||||
|
||||
expect(container.textContent).toContain("正在准备任务分工");
|
||||
expect(container.textContent).toContain("准备中");
|
||||
expect(container.textContent).toContain("任务方案 · 排障 Team");
|
||||
expect(container.textContent).toContain("任务方案 · 排障任务方案");
|
||||
expect(container.textContent).toContain("参考方案 · 代码排障团队");
|
||||
});
|
||||
|
||||
@@ -2142,7 +2184,7 @@ describe("TeamWorkspaceBoard", () => {
|
||||
|
||||
expect(container.textContent).toContain("任务分工准备失败");
|
||||
expect(container.textContent).toContain(
|
||||
"Provider 认证失败,无法生成 Team。",
|
||||
"Provider 认证失败,无法生成任务方案。",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -163,7 +163,7 @@ describe("TeamWorkspaceDock", () => {
|
||||
],
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("任务协作");
|
||||
expect(container.textContent).toContain("任务视图");
|
||||
|
||||
const toggleButton = container.querySelector<HTMLButtonElement>(
|
||||
'[data-testid="team-workspace-dock-toggle"]',
|
||||
@@ -174,15 +174,18 @@ describe("TeamWorkspaceDock", () => {
|
||||
toggleButton?.click();
|
||||
});
|
||||
|
||||
expect(document.body.textContent).toContain("等待成员接入");
|
||||
expect(document.body.textContent).toContain("等待任务接手");
|
||||
expect(document.body.textContent).toContain("任务方案");
|
||||
expect(document.body.textContent).toContain("不遮挡画布");
|
||||
expect(document.body.textContent).toContain("当前任务方案:前端联调团队");
|
||||
expect(document.body.textContent).toContain("分析、实现、验证三段式推进。");
|
||||
expect(document.body.textContent).not.toContain("查看详情");
|
||||
expect(
|
||||
document.body.querySelector('[data-testid="team-workspace-empty-card"]'),
|
||||
).toBeTruthy();
|
||||
const emptyCard = document.body.querySelector<HTMLElement>(
|
||||
'[data-testid="team-workspace-empty-card"]',
|
||||
);
|
||||
expect(emptyCard).toBeTruthy();
|
||||
expect(emptyCard?.textContent).toContain("任务工作台");
|
||||
expect(emptyCard?.textContent).not.toContain("任务进行时");
|
||||
expect(
|
||||
document.body.querySelector(
|
||||
'[data-testid="team-workspace-selected-team"]',
|
||||
@@ -260,7 +263,9 @@ describe("TeamWorkspaceDock", () => {
|
||||
expect(
|
||||
document.body.querySelector('[data-testid="team-workspace-dock-panel"]'),
|
||||
).toBeTruthy();
|
||||
expect(container.textContent).toContain("收起协作面板");
|
||||
expect(container.textContent).toContain("任务视图");
|
||||
expect(container.textContent).toContain("1 项处理中");
|
||||
expect(container.textContent).not.toContain("收起任务视图");
|
||||
expect(document.body.textContent).toContain("研究员");
|
||||
});
|
||||
|
||||
@@ -270,7 +275,7 @@ describe("TeamWorkspaceDock", () => {
|
||||
requestId: "runtime-forming-1",
|
||||
status: "forming",
|
||||
label: "修复 Team",
|
||||
summary: "正在根据任务准备协作成员。",
|
||||
summary: "正在根据当前任务准备分工。",
|
||||
members: [],
|
||||
blueprint: {
|
||||
label: "代码排障团队",
|
||||
@@ -284,7 +289,9 @@ describe("TeamWorkspaceDock", () => {
|
||||
expect(
|
||||
document.body.querySelector('[data-testid="team-workspace-dock-panel"]'),
|
||||
).toBeNull();
|
||||
expect(container.textContent).toContain("查看任务进行时 · 准备中");
|
||||
expect(container.textContent).toContain("任务视图");
|
||||
expect(container.textContent).toContain("准备中");
|
||||
expect(container.textContent).not.toContain("查看任务视图");
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-dock-signal"]'),
|
||||
).toBeTruthy();
|
||||
@@ -320,7 +327,9 @@ describe("TeamWorkspaceDock", () => {
|
||||
expect(
|
||||
document.body.querySelector('[data-testid="team-workspace-dock-panel"]'),
|
||||
).toBeNull();
|
||||
expect(container.textContent).toContain("查看任务进行时 · 1");
|
||||
expect(container.textContent).toContain("任务视图");
|
||||
expect(container.textContent).toContain("1 项任务");
|
||||
expect(container.textContent).not.toContain("查看任务视图");
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-dock-signal"]'),
|
||||
).toBeTruthy();
|
||||
@@ -347,7 +356,9 @@ describe("TeamWorkspaceDock", () => {
|
||||
expect(
|
||||
document.body.querySelector('[data-testid="team-workspace-dock-panel"]'),
|
||||
).toBeNull();
|
||||
expect(container.textContent).toContain("查看任务进行时 · 失败");
|
||||
expect(container.textContent).toContain("任务视图");
|
||||
expect(container.textContent).toContain("准备失败");
|
||||
expect(container.textContent).not.toContain("查看任务视图");
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-dock-signal"]'),
|
||||
).toBeTruthy();
|
||||
@@ -400,9 +411,9 @@ describe("TeamWorkspaceDock", () => {
|
||||
);
|
||||
|
||||
expect(toggleButton).toBeTruthy();
|
||||
expect(toggleButton?.textContent).toContain("修复 Team");
|
||||
expect(toggleButton?.textContent).toContain("1 位处理中");
|
||||
expect(toggleButton?.textContent).not.toContain("查看任务进行时");
|
||||
expect(toggleButton?.textContent).toContain("修复任务方案");
|
||||
expect(toggleButton?.textContent).toContain("1 项处理中");
|
||||
expect(toggleButton?.textContent).not.toContain("查看任务视图");
|
||||
|
||||
act(() => {
|
||||
toggleButton?.click();
|
||||
@@ -433,8 +444,18 @@ describe("TeamWorkspaceDock", () => {
|
||||
const toggleButton = container.querySelector<HTMLButtonElement>(
|
||||
'[data-testid="team-workspace-dock-toggle"]',
|
||||
);
|
||||
const summary = container.querySelector<HTMLElement>(
|
||||
'[data-testid="team-workspace-dock-summary"]',
|
||||
);
|
||||
const badges = container.querySelector<HTMLElement>(
|
||||
'[data-testid="team-workspace-dock-badges"]',
|
||||
);
|
||||
|
||||
expect(toggleButton?.textContent).toContain("查看任务进行时 · 1");
|
||||
expect(toggleButton?.textContent).toContain("任务视图");
|
||||
expect(toggleButton?.textContent).toContain("1 项处理中");
|
||||
expect(toggleButton?.textContent).not.toContain("查看任务视图");
|
||||
expect(summary?.textContent).toContain("任务视图");
|
||||
expect(badges?.textContent).toContain("1 项处理中");
|
||||
expect(
|
||||
container.querySelector('[data-testid="team-workspace-dock-signal"]'),
|
||||
).toBeTruthy();
|
||||
@@ -511,7 +532,9 @@ describe("TeamWorkspaceDock", () => {
|
||||
const toggleButton = container.querySelector<HTMLButtonElement>(
|
||||
'[data-testid="team-workspace-dock-toggle"]',
|
||||
);
|
||||
expect(toggleButton?.textContent).toContain("收起协作面板");
|
||||
expect(toggleButton?.textContent).toContain("任务视图");
|
||||
expect(toggleButton?.textContent).toContain("1 项处理中");
|
||||
expect(toggleButton?.textContent).not.toContain("收起任务视图");
|
||||
|
||||
act(() => {
|
||||
toggleButton?.click();
|
||||
@@ -550,7 +573,9 @@ describe("TeamWorkspaceDock", () => {
|
||||
expect(
|
||||
document.body.querySelector('[data-testid="team-workspace-dock-panel"]'),
|
||||
).toBeNull();
|
||||
expect(toggleButton?.textContent).toContain("查看任务进行时 · 2");
|
||||
expect(toggleButton?.textContent).toContain("任务视图");
|
||||
expect(toggleButton?.textContent).toContain("2 项处理中");
|
||||
expect(toggleButton?.textContent).not.toContain("查看任务视图");
|
||||
});
|
||||
|
||||
it("切换会话后,新的真实成员出现应再次自动展开", async () => {
|
||||
@@ -612,7 +637,9 @@ describe("TeamWorkspaceDock", () => {
|
||||
expect(
|
||||
document.body.querySelector('[data-testid="team-workspace-dock-panel"]'),
|
||||
).toBeTruthy();
|
||||
expect(container.textContent).toContain("收起协作面板");
|
||||
expect(container.textContent).toContain("任务视图");
|
||||
expect(container.textContent).toContain("1 项处理中");
|
||||
expect(container.textContent).not.toContain("收起任务视图");
|
||||
});
|
||||
|
||||
it("旧会话初次挂载时即使已有真实成员,也不应自动展开", async () => {
|
||||
@@ -636,7 +663,9 @@ describe("TeamWorkspaceDock", () => {
|
||||
expect(
|
||||
document.body.querySelector('[data-testid="team-workspace-dock-panel"]'),
|
||||
).toBeNull();
|
||||
expect(container.textContent).toContain("查看任务进行时 · 1");
|
||||
expect(container.textContent).toContain("任务视图");
|
||||
expect(container.textContent).toContain("1 项处理中");
|
||||
expect(container.textContent).not.toContain("查看任务视图");
|
||||
});
|
||||
|
||||
it("已有真实成员的 Dock 切到 inline 时,不应因为布局变化自动展开", async () => {
|
||||
@@ -679,7 +708,9 @@ describe("TeamWorkspaceDock", () => {
|
||||
expect(
|
||||
document.body.querySelector('[data-testid="team-workspace-dock-panel"]'),
|
||||
).toBeNull();
|
||||
expect(container.textContent).toContain("查看任务进行时 · 1");
|
||||
expect(container.textContent).toContain("任务视图");
|
||||
expect(container.textContent).toContain("1 项处理中");
|
||||
expect(container.textContent).not.toContain("查看任务视图");
|
||||
});
|
||||
|
||||
it("本轮任务方案已就绪时,应在空态 Dock 展示成员摘要", async () => {
|
||||
@@ -712,7 +743,17 @@ describe("TeamWorkspaceDock", () => {
|
||||
const toggleButton = container.querySelector<HTMLButtonElement>(
|
||||
'[data-testid="team-workspace-dock-toggle"]',
|
||||
);
|
||||
expect(toggleButton?.textContent).toContain("查看任务进行时 · 1");
|
||||
const summary = container.querySelector<HTMLElement>(
|
||||
'[data-testid="team-workspace-dock-summary"]',
|
||||
);
|
||||
const badges = container.querySelector<HTMLElement>(
|
||||
'[data-testid="team-workspace-dock-badges"]',
|
||||
);
|
||||
expect(toggleButton?.textContent).toContain("任务视图");
|
||||
expect(toggleButton?.textContent).toContain("1 项任务");
|
||||
expect(toggleButton?.textContent).not.toContain("查看任务视图");
|
||||
expect(summary?.textContent).toContain("任务视图");
|
||||
expect(badges?.textContent).toContain("1 项任务");
|
||||
|
||||
act(() => {
|
||||
toggleButton?.click();
|
||||
@@ -728,7 +769,7 @@ describe("TeamWorkspaceDock", () => {
|
||||
});
|
||||
|
||||
expect(document.body.textContent).toContain("任务分工已准备好");
|
||||
expect(document.body.textContent).toContain("修复 Team");
|
||||
expect(document.body.textContent).toContain("修复任务方案");
|
||||
expect(document.body.textContent).toContain("分析");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { ChevronDown, ChevronUp, Workflow } from "lucide-react";
|
||||
import { ChevronDown, ChevronUp, ListTodo } from "lucide-react";
|
||||
import styled, { css, keyframes } from "styled-components";
|
||||
import type {
|
||||
AsterSubagentParentContext,
|
||||
@@ -14,17 +14,18 @@ import type {
|
||||
TeamWorkspaceWaitSummary,
|
||||
} from "../teamWorkspaceRuntime";
|
||||
import {
|
||||
resolveRuntimeFormationStatusMeta,
|
||||
resolveRuntimeMemberStatusMeta,
|
||||
summarizeTeamWorkspaceExecution,
|
||||
} from "../teamWorkspaceRuntime";
|
||||
import { buildRuntimeFormationDisplayState } from "../team-workspace-runtime/formationDisplaySelectors";
|
||||
import { TeamWorkspaceBoard } from "./TeamWorkspaceBoard";
|
||||
import type { TeamRoleDefinition } from "../utils/teamDefinitions";
|
||||
import {
|
||||
TEAM_WORKSPACE_PLAN_LABEL,
|
||||
TEAM_WORKSPACE_REALTIME_BADGE_LABEL,
|
||||
TEAM_WORKSPACE_SURFACE_TITLE,
|
||||
TEAM_WORKSPACE_WAITING_HEADLINE,
|
||||
} from "../utils/teamWorkspaceCopy";
|
||||
import { normalizeTeamWorkspaceDisplayValue } from "../utils/teamWorkspaceDisplay";
|
||||
|
||||
const DockContainer = styled.div<{
|
||||
$withBottomOverlay: boolean;
|
||||
@@ -155,24 +156,29 @@ const DockToggle = styled.button<{
|
||||
}>`
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: ${({ $launcherOnly }) => ($launcherOnly ? "36px" : "42px")};
|
||||
padding: ${({ $launcherOnly }) => ($launcherOnly ? "6px 10px" : "8px 12px")};
|
||||
gap: ${({ $launcherOnly }) => ($launcherOnly ? "8px" : "10px")};
|
||||
min-height: ${({ $launcherOnly }) => ($launcherOnly ? "36px" : "48px")};
|
||||
padding: ${({ $launcherOnly }) =>
|
||||
$launcherOnly ? "6px 10px" : "8px 12px 8px 10px"};
|
||||
border-radius: 999px;
|
||||
border: 1px solid
|
||||
${({ $active, $expanded }) =>
|
||||
$expanded
|
||||
? "rgba(14, 116, 144, 0.28)"
|
||||
? "rgba(14, 116, 144, 0.24)"
|
||||
: $active
|
||||
? "rgba(56, 189, 248, 0.32)"
|
||||
? "rgba(186, 230, 253, 0.96)"
|
||||
: "rgba(203, 213, 225, 0.9)"};
|
||||
background: ${({ $launcherOnly }) =>
|
||||
$launcherOnly ? "rgba(255, 255, 255, 0.98)" : "#ffffff"};
|
||||
background: ${({ $launcherOnly, $expanded }) =>
|
||||
$launcherOnly
|
||||
? "rgba(255, 255, 255, 0.98)"
|
||||
: $expanded
|
||||
? "#ffffff"
|
||||
: "#f8fafc"};
|
||||
color: #0f172a;
|
||||
box-shadow: ${({ $launcherOnly }) =>
|
||||
$launcherOnly
|
||||
? "0 10px 24px -18px rgba(15, 23, 42, 0.18)"
|
||||
: "0 16px 40px -28px rgba(15, 23, 42, 0.28)"};
|
||||
: "0 12px 28px -24px rgba(15, 23, 42, 0.22)"};
|
||||
transition:
|
||||
transform 0.18s ease,
|
||||
box-shadow 0.18s ease,
|
||||
@@ -196,7 +202,7 @@ const DockToggle = styled.button<{
|
||||
box-shadow: ${({ $launcherOnly }) =>
|
||||
$launcherOnly
|
||||
? "0 12px 28px -18px rgba(15, 23, 42, 0.22)"
|
||||
: "0 18px 44px -28px rgba(15, 23, 42, 0.32)"};
|
||||
: "0 14px 30px -24px rgba(15, 23, 42, 0.24)"};
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -206,20 +212,22 @@ const DockIconShell = styled.span<{
|
||||
}>`
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
height: ${({ $launcherOnly }) => ($launcherOnly ? "24px" : "28px")};
|
||||
width: ${({ $launcherOnly }) => ($launcherOnly ? "24px" : "28px")};
|
||||
height: ${({ $launcherOnly }) => ($launcherOnly ? "24px" : "26px")};
|
||||
width: ${({ $launcherOnly }) => ($launcherOnly ? "24px" : "26px")};
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(226, 232, 240, 0.9);
|
||||
background: #ffffff;
|
||||
background: ${({ $launcherOnly }) =>
|
||||
$launcherOnly ? "#ffffff" : "rgba(255, 255, 255, 0.92)"};
|
||||
color: #475569;
|
||||
|
||||
${({ $attention }) =>
|
||||
$attention
|
||||
? css`
|
||||
color: #0369a1;
|
||||
border-color: rgba(125, 211, 252, 0.96);
|
||||
border-color: rgba(125, 211, 252, 0.72);
|
||||
background: #eff6ff;
|
||||
`
|
||||
: ""}
|
||||
`;
|
||||
@@ -231,6 +239,31 @@ const DockTextStack = styled.span`
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
const DockSummaryStack = styled.span`
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 3px;
|
||||
`;
|
||||
|
||||
const DockSummaryEyebrow = styled.span`
|
||||
min-width: 0;
|
||||
color: #64748b;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0.06em;
|
||||
`;
|
||||
|
||||
const DockBadgeRow = styled.span`
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
const DockPrimaryText = styled.span<{ $launcherOnly: boolean }>`
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
@@ -248,27 +281,27 @@ const DockStatusBadge = styled.span<{
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
border-radius: 999px;
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1.4;
|
||||
padding: 1px 7px 1px 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
line-height: 1.35;
|
||||
border: 1px solid
|
||||
${({ $tone }) =>
|
||||
$tone === "success"
|
||||
? "rgba(167, 243, 208, 0.96)"
|
||||
? "rgba(167, 243, 208, 0.72)"
|
||||
: $tone === "error"
|
||||
? "rgba(254, 205, 211, 0.96)"
|
||||
? "rgba(254, 205, 211, 0.72)"
|
||||
: $tone === "active"
|
||||
? "rgba(186, 230, 253, 0.96)"
|
||||
: "rgba(226, 232, 240, 0.96)"};
|
||||
? "rgba(186, 230, 253, 0.72)"
|
||||
: "rgba(226, 232, 240, 0.88)"};
|
||||
background: ${({ $tone }) =>
|
||||
$tone === "success"
|
||||
? "#ecfdf5"
|
||||
? "rgba(236, 253, 245, 0.86)"
|
||||
: $tone === "error"
|
||||
? "#fff1f2"
|
||||
? "rgba(255, 241, 242, 0.86)"
|
||||
: $tone === "active"
|
||||
? "#f0f9ff"
|
||||
: "#f8fafc"};
|
||||
? "rgba(240, 249, 255, 0.86)"
|
||||
: "rgba(248, 250, 252, 0.92)"};
|
||||
color: ${({ $tone }) =>
|
||||
$tone === "success"
|
||||
? "#047857"
|
||||
@@ -277,6 +310,23 @@ const DockStatusBadge = styled.span<{
|
||||
: $tone === "active"
|
||||
? "#0369a1"
|
||||
: "#64748b"};
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: inline-flex;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
margin-right: 6px;
|
||||
border-radius: 999px;
|
||||
background: ${({ $tone }) =>
|
||||
$tone === "success"
|
||||
? "#10b981"
|
||||
: $tone === "error"
|
||||
? "#f43f5e"
|
||||
: $tone === "active"
|
||||
? "#0ea5e9"
|
||||
: "#94a3b8"};
|
||||
}
|
||||
`;
|
||||
|
||||
const DockSignal = styled.span`
|
||||
@@ -509,48 +559,54 @@ export function TeamWorkspaceDock({
|
||||
}: TeamWorkspaceDockProps) {
|
||||
const dispatchPreviewState = teamDispatchPreviewState;
|
||||
const launcherOnly = typeof onActivateWorkbench === "function";
|
||||
const hasRealTeamGraph =
|
||||
childSubagentSessions.length > 0 || Boolean(subagentParentContext);
|
||||
const executionSummary = summarizeTeamWorkspaceExecution({
|
||||
currentSessionId,
|
||||
currentSessionRuntimeStatus,
|
||||
currentSessionLatestTurnStatus,
|
||||
currentSessionQueuedTurnCount,
|
||||
childSubagentSessions,
|
||||
subagentParentContext,
|
||||
liveRuntimeBySessionId,
|
||||
});
|
||||
const hasRuntimeSessions = executionSummary.totalSessionCount > 0;
|
||||
const hasRuntimeFormation = Boolean(dispatchPreviewState);
|
||||
const runtimeFormationMeta = dispatchPreviewState
|
||||
? resolveRuntimeFormationStatusMeta(dispatchPreviewState.status)
|
||||
: null;
|
||||
const runtimeTeamLabel =
|
||||
dispatchPreviewState?.label?.trim() ||
|
||||
dispatchPreviewState?.blueprint?.label?.trim() ||
|
||||
selectedTeamLabel?.trim() ||
|
||||
null;
|
||||
const runtimeFormationDisplay = buildRuntimeFormationDisplayState({
|
||||
teamDispatchPreviewState: dispatchPreviewState,
|
||||
fallbackLabel: selectedTeamLabel,
|
||||
fallbackSummary: selectedTeamSummary,
|
||||
});
|
||||
const runtimeTeamLabel = runtimeFormationDisplay.panelLabel;
|
||||
const runtimeBlueprintSummary = normalizeTeamWorkspaceDisplayValue(
|
||||
dispatchPreviewState?.blueprint?.summary,
|
||||
);
|
||||
const normalizedSelectedTeamSummary = normalizeTeamWorkspaceDisplayValue(
|
||||
selectedTeamSummary,
|
||||
);
|
||||
const runtimeTeamSummary =
|
||||
dispatchPreviewState?.summary?.trim() ||
|
||||
dispatchPreviewState?.blueprint?.summary?.trim() ||
|
||||
selectedTeamSummary?.trim() ||
|
||||
normalizeTeamWorkspaceDisplayValue(dispatchPreviewState?.summary) ||
|
||||
runtimeBlueprintSummary ||
|
||||
normalizedSelectedTeamSummary ||
|
||||
null;
|
||||
const runtimeTaskCount = dispatchPreviewState?.members.length ?? 0;
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [userDismissedAutoExpand, setUserDismissedAutoExpand] = useState(false);
|
||||
const hasInitializedRef = useRef(false);
|
||||
const previousSessionIdRef = useRef<string | null>(currentSessionId ?? null);
|
||||
const previousHasRealGraphRef = useRef(hasRealTeamGraph);
|
||||
const previousHasRuntimeSessionsRef = useRef(hasRuntimeSessions);
|
||||
const toggleRef = useRef<HTMLButtonElement | null>(null);
|
||||
const [inlinePanelLayout, setInlinePanelLayout] =
|
||||
useState<InlinePanelLayout | null>(null);
|
||||
const isCompact = !hasRealTeamGraph && !hasRuntimeFormation;
|
||||
const isCompact = !hasRuntimeSessions && !hasRuntimeFormation;
|
||||
const showAttentionCue =
|
||||
!expanded &&
|
||||
(hasRealTeamGraph ||
|
||||
dispatchPreviewState?.status === "forming" ||
|
||||
dispatchPreviewState?.status === "formed" ||
|
||||
dispatchPreviewState?.status === "failed");
|
||||
const dockCount = subagentParentContext
|
||||
? (subagentParentContext.sibling_subagent_sessions?.length ?? 0) + 1
|
||||
: childSubagentSessions.length;
|
||||
!expanded && (hasRuntimeSessions || hasRuntimeFormation);
|
||||
const shouldPortalPanel = expanded;
|
||||
const [teamDetailExpanded, setTeamDetailExpanded] = useState(false);
|
||||
const hasSelectedTeamDetails =
|
||||
Boolean(runtimeTeamSummary) ||
|
||||
(dispatchPreviewState?.members.length ?? 0) > 0 ||
|
||||
Boolean(dispatchPreviewState?.blueprint?.summary?.trim()) ||
|
||||
runtimeTaskCount > 0 ||
|
||||
Boolean(runtimeBlueprintSummary) ||
|
||||
(dispatchPreviewState?.blueprint?.roles.length ?? 0) > 0 ||
|
||||
Boolean(selectedTeamSummary?.trim()) ||
|
||||
Boolean(normalizedSelectedTeamSummary) ||
|
||||
(selectedTeamRoles?.length ?? 0) > 0;
|
||||
const activeRuntimeSessionCount = childSubagentSessions.filter(
|
||||
(session) =>
|
||||
@@ -575,14 +631,16 @@ export function TeamWorkspaceDock({
|
||||
teamConcurrencySnapshot?.team_parallel_budget !== undefined
|
||||
? `${teamConcurrencySnapshot.team_active_count ?? runningRuntimeSessionCount}/${teamConcurrencySnapshot.team_parallel_budget} 处理中`
|
||||
: runningRuntimeSessionCount > 0
|
||||
? `${runningRuntimeSessionCount} 位处理中`
|
||||
? `${runningRuntimeSessionCount} 项处理中`
|
||||
: null;
|
||||
const teamQueueBadgeText =
|
||||
(teamConcurrencySnapshot?.team_queued_count ?? queuedRuntimeSessionCount) >
|
||||
0
|
||||
? `${teamConcurrencySnapshot?.team_queued_count ?? queuedRuntimeSessionCount} 位等待中`
|
||||
? `${teamConcurrencySnapshot?.team_queued_count ?? queuedRuntimeSessionCount} 项等待中`
|
||||
: null;
|
||||
const teamQueueReason = teamConcurrencySnapshot?.queue_reason?.trim() || null;
|
||||
const teamQueueReason = normalizeTeamWorkspaceDisplayValue(
|
||||
teamConcurrencySnapshot?.queue_reason,
|
||||
);
|
||||
const launcherStatusMeta = useMemo<{
|
||||
label: string;
|
||||
tone: "idle" | "active" | "success" | "error";
|
||||
@@ -599,28 +657,31 @@ export function TeamWorkspaceDock({
|
||||
return { label: "失败", tone: "error" };
|
||||
}
|
||||
|
||||
if (hasRealTeamGraph) {
|
||||
if (hasRuntimeSessions) {
|
||||
if (activeRuntimeSessionCount > 0) {
|
||||
return {
|
||||
label:
|
||||
teamConcurrencyBadgeText || `${activeRuntimeSessionCount} 位处理中`,
|
||||
teamConcurrencyBadgeText || `${activeRuntimeSessionCount} 项处理中`,
|
||||
tone: "active",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
label: `${dockCount} 名成员`,
|
||||
tone: dockCount > 0 ? "success" : "idle",
|
||||
label: `${executionSummary.totalSessionCount} 项任务`,
|
||||
tone: executionSummary.totalSessionCount > 0 ? "success" : "idle",
|
||||
};
|
||||
}
|
||||
|
||||
if (dispatchPreviewState?.status === "formed") {
|
||||
return dispatchPreviewState.members.length > 0
|
||||
return runtimeTaskCount > 0
|
||||
? {
|
||||
label: `${dispatchPreviewState.members.length} 名成员`,
|
||||
label: `${runtimeTaskCount} 项任务`,
|
||||
tone: "success",
|
||||
}
|
||||
: { label: "已就绪", tone: "success" };
|
||||
: {
|
||||
label: runtimeFormationDisplay.panelStatusLabel || "已就绪",
|
||||
tone: "success",
|
||||
};
|
||||
}
|
||||
|
||||
if (runtimeTeamLabel) {
|
||||
@@ -630,77 +691,121 @@ export function TeamWorkspaceDock({
|
||||
return { label: "待开始", tone: "idle" };
|
||||
}, [
|
||||
activeRuntimeSessionCount,
|
||||
dockCount,
|
||||
hasRealTeamGraph,
|
||||
executionSummary.totalSessionCount,
|
||||
hasRuntimeSessions,
|
||||
launcherOnly,
|
||||
teamConcurrencyBadgeText,
|
||||
runtimeFormationDisplay.panelStatusLabel,
|
||||
runtimeTaskCount,
|
||||
runtimeTeamLabel,
|
||||
dispatchPreviewState,
|
||||
]);
|
||||
const toggleLabel = useMemo(() => {
|
||||
if (expanded) {
|
||||
return "收起协作面板";
|
||||
}
|
||||
if (hasRealTeamGraph) {
|
||||
const dockStatusBadges = useMemo<
|
||||
Array<{
|
||||
label: string;
|
||||
tone: "idle" | "active" | "success" | "error";
|
||||
}>
|
||||
>(() => {
|
||||
if (hasRuntimeSessions) {
|
||||
const badges: Array<{
|
||||
label: string;
|
||||
tone: "idle" | "active" | "success" | "error";
|
||||
}> = [];
|
||||
|
||||
if (teamConcurrencyBadgeText || teamQueueBadgeText) {
|
||||
return ["查看任务进行时", teamConcurrencyBadgeText, teamQueueBadgeText]
|
||||
.filter(Boolean)
|
||||
.join(" · ");
|
||||
if (teamConcurrencyBadgeText) {
|
||||
badges.push({ label: teamConcurrencyBadgeText, tone: "active" });
|
||||
}
|
||||
if (teamQueueBadgeText) {
|
||||
badges.push({ label: teamQueueBadgeText, tone: "idle" });
|
||||
}
|
||||
return badges;
|
||||
}
|
||||
return `查看任务进行时 · ${dockCount}`;
|
||||
|
||||
return [
|
||||
{
|
||||
label: `${executionSummary.totalSessionCount} 项任务`,
|
||||
tone:
|
||||
executionSummary.totalSessionCount > 0 ? "success" : "idle",
|
||||
},
|
||||
];
|
||||
}
|
||||
if (dispatchPreviewState?.status === "forming") {
|
||||
return "查看任务进行时 · 准备中";
|
||||
return [
|
||||
{
|
||||
label: runtimeFormationDisplay.panelStatusLabel || "准备中",
|
||||
tone: "active",
|
||||
},
|
||||
];
|
||||
}
|
||||
if (dispatchPreviewState?.status === "formed") {
|
||||
return `查看任务进行时 · ${dispatchPreviewState.members.length}`;
|
||||
return [
|
||||
{
|
||||
label:
|
||||
runtimeTaskCount > 0
|
||||
? `${runtimeTaskCount} 项任务`
|
||||
: runtimeFormationDisplay.panelStatusLabel || "已就绪",
|
||||
tone: "success",
|
||||
},
|
||||
];
|
||||
}
|
||||
if (dispatchPreviewState?.status === "failed") {
|
||||
return "查看任务进行时 · 失败";
|
||||
return [
|
||||
{
|
||||
label: runtimeFormationDisplay.panelStatusLabel || "失败",
|
||||
tone: "error",
|
||||
},
|
||||
];
|
||||
}
|
||||
return TEAM_WORKSPACE_SURFACE_TITLE;
|
||||
|
||||
return runtimeTeamLabel ? [{ label: "已准备", tone: "idle" }] : [];
|
||||
}, [
|
||||
dockCount,
|
||||
expanded,
|
||||
hasRealTeamGraph,
|
||||
executionSummary.totalSessionCount,
|
||||
hasRuntimeSessions,
|
||||
runtimeTaskCount,
|
||||
dispatchPreviewState,
|
||||
runtimeFormationDisplay.panelStatusLabel,
|
||||
teamConcurrencyBadgeText,
|
||||
teamQueueBadgeText,
|
||||
runtimeTeamLabel,
|
||||
]);
|
||||
const launcherPrimaryLabel = runtimeTeamLabel || TEAM_WORKSPACE_SURFACE_TITLE;
|
||||
const launcherPrimaryLabel = runtimeTeamLabel || "任务视图";
|
||||
const dockPrimaryLabel = "任务视图";
|
||||
|
||||
useEffect(() => {
|
||||
const normalizedSessionId = currentSessionId ?? null;
|
||||
|
||||
if (!hasInitializedRef.current) {
|
||||
previousSessionIdRef.current = normalizedSessionId;
|
||||
previousHasRealGraphRef.current = hasRealTeamGraph;
|
||||
previousHasRuntimeSessionsRef.current = hasRuntimeSessions;
|
||||
hasInitializedRef.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (previousSessionIdRef.current !== normalizedSessionId) {
|
||||
previousSessionIdRef.current = normalizedSessionId;
|
||||
previousHasRealGraphRef.current = hasRealTeamGraph;
|
||||
previousHasRuntimeSessionsRef.current = hasRuntimeSessions;
|
||||
setUserDismissedAutoExpand(false);
|
||||
setExpanded(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const graphAppeared = !previousHasRealGraphRef.current && hasRealTeamGraph;
|
||||
const graphCleared = previousHasRealGraphRef.current && !hasRealTeamGraph;
|
||||
const runtimeSessionsAppeared =
|
||||
!previousHasRuntimeSessionsRef.current && hasRuntimeSessions;
|
||||
const runtimeSessionsCleared =
|
||||
previousHasRuntimeSessionsRef.current && !hasRuntimeSessions;
|
||||
|
||||
if (graphCleared) {
|
||||
if (runtimeSessionsCleared) {
|
||||
setUserDismissedAutoExpand(false);
|
||||
}
|
||||
|
||||
if (graphAppeared && !userDismissedAutoExpand) {
|
||||
if (runtimeSessionsAppeared && !userDismissedAutoExpand) {
|
||||
setExpanded(true);
|
||||
}
|
||||
|
||||
previousSessionIdRef.current = normalizedSessionId;
|
||||
previousHasRealGraphRef.current = hasRealTeamGraph;
|
||||
}, [currentSessionId, hasRealTeamGraph, userDismissedAutoExpand]);
|
||||
previousHasRuntimeSessionsRef.current = hasRuntimeSessions;
|
||||
}, [currentSessionId, hasRuntimeSessions, userDismissedAutoExpand]);
|
||||
|
||||
const updateInlinePanelLayout = useCallback(() => {
|
||||
if (
|
||||
@@ -750,11 +855,11 @@ export function TeamWorkspaceDock({
|
||||
};
|
||||
}, [shouldPortalPanel, updateInlinePanelLayout]);
|
||||
|
||||
if (!shellVisible && !hasRealTeamGraph) {
|
||||
if (!shellVisible && !hasRuntimeSessions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const panelContent = hasRealTeamGraph ? (
|
||||
const panelContent = hasRuntimeSessions ? (
|
||||
<TeamWorkspaceBoard
|
||||
embedded={true}
|
||||
shellVisible={shellVisible}
|
||||
@@ -788,31 +893,21 @@ export function TeamWorkspaceDock({
|
||||
<EmptyStateCard data-testid="team-workspace-empty-card" role="status">
|
||||
<EmptyStateBody>
|
||||
<EmptyStateEyebrow>
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<Workflow className="h-3.5 w-3.5" />
|
||||
<span>{TEAM_WORKSPACE_SURFACE_TITLE}已启用</span>
|
||||
</span>
|
||||
<EmptyStateBadge $tone="success">
|
||||
{TEAM_WORKSPACE_REALTIME_BADGE_LABEL}
|
||||
</EmptyStateBadge>
|
||||
{runtimeFormationMeta ? (
|
||||
<EmptyStateBadge>{runtimeFormationMeta.label}</EmptyStateBadge>
|
||||
<EmptyStateBadge>{TEAM_WORKSPACE_SURFACE_TITLE}</EmptyStateBadge>
|
||||
{runtimeFormationDisplay.panelStatusLabel ? (
|
||||
<EmptyStateBadge>
|
||||
{runtimeFormationDisplay.panelStatusLabel}
|
||||
</EmptyStateBadge>
|
||||
) : null}
|
||||
</EmptyStateEyebrow>
|
||||
<EmptyStateTitle>
|
||||
{runtimeFormationMeta?.title || TEAM_WORKSPACE_WAITING_HEADLINE}
|
||||
{runtimeFormationDisplay.panelHeadline ||
|
||||
TEAM_WORKSPACE_WAITING_HEADLINE}
|
||||
</EmptyStateTitle>
|
||||
<EmptyStateDescription>
|
||||
{dispatchPreviewState?.status === "forming" ? (
|
||||
"系统正在按当前任务准备分工,成员接入后会自动展开完整协作面板。"
|
||||
) : dispatchPreviewState?.status === "formed" ? (
|
||||
`已准备 ${dispatchPreviewState.members.length} 位协作成员,后续会自动接入并继续处理。`
|
||||
) : dispatchPreviewState?.status === "failed" ? (
|
||||
dispatchPreviewState.errorMessage?.trim() ||
|
||||
"这次任务分工准备失败,你仍然可以继续在当前对话中处理。"
|
||||
) : (
|
||||
<>需要时系统会自动安排任务分工,这里会切换成完整任务协作面板。</>
|
||||
)}
|
||||
{dispatchPreviewState
|
||||
? runtimeFormationDisplay.noticeText
|
||||
: runtimeFormationDisplay.panelDescription}
|
||||
</EmptyStateDescription>
|
||||
{teamQueueReason ? (
|
||||
<EmptyStateBadge>{teamQueueReason}</EmptyStateBadge>
|
||||
@@ -860,7 +955,10 @@ export function TeamWorkspaceDock({
|
||||
return (
|
||||
<EmptyStateRoleItem key={`dock-runtime-role-${member.id}`}>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<EmptyStateRoleName>{member.label}</EmptyStateRoleName>
|
||||
<EmptyStateRoleName>
|
||||
{normalizeTeamWorkspaceDisplayValue(member.label) ||
|
||||
member.label}
|
||||
</EmptyStateRoleName>
|
||||
<span
|
||||
className={`rounded-full px-2 py-0.5 text-[10px] font-medium ${memberStatusMeta.badgeClassName}`}
|
||||
>
|
||||
@@ -878,7 +976,8 @@ export function TeamWorkspaceDock({
|
||||
) : null}
|
||||
</div>
|
||||
<EmptyStateRoleSummary>
|
||||
{member.summary}
|
||||
{normalizeTeamWorkspaceDisplayValue(member.summary) ||
|
||||
member.summary}
|
||||
</EmptyStateRoleSummary>
|
||||
{member.skillIds.length > 0 ? (
|
||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||
@@ -898,20 +997,24 @@ export function TeamWorkspaceDock({
|
||||
{!dispatchPreviewState
|
||||
? selectedTeamRoles?.map((role) => (
|
||||
<EmptyStateRoleItem key={`dock-role-${role.id}`}>
|
||||
<EmptyStateRoleName>{role.label}</EmptyStateRoleName>
|
||||
<EmptyStateRoleName>
|
||||
{normalizeTeamWorkspaceDisplayValue(role.label) ||
|
||||
role.label}
|
||||
</EmptyStateRoleName>
|
||||
<EmptyStateRoleSummary>
|
||||
{role.summary}
|
||||
{normalizeTeamWorkspaceDisplayValue(role.summary) ||
|
||||
role.summary}
|
||||
</EmptyStateRoleSummary>
|
||||
</EmptyStateRoleItem>
|
||||
))
|
||||
: null}
|
||||
{dispatchPreviewState?.blueprint?.label ? (
|
||||
{runtimeFormationDisplay.referenceLabel ? (
|
||||
<EmptyStateRoleItem>
|
||||
<EmptyStateRoleName>
|
||||
参考方案 · {dispatchPreviewState.blueprint.label}
|
||||
参考方案 · {runtimeFormationDisplay.referenceLabel}
|
||||
</EmptyStateRoleName>
|
||||
<EmptyStateRoleSummary>
|
||||
{dispatchPreviewState.blueprint.summary ||
|
||||
{runtimeBlueprintSummary ||
|
||||
"当前任务分工参考了当前方案里的角色偏好。"}
|
||||
</EmptyStateRoleSummary>
|
||||
</EmptyStateRoleItem>
|
||||
@@ -929,10 +1032,8 @@ export function TeamWorkspaceDock({
|
||||
{runtimeTeamSummary ? (
|
||||
<EmptyStateBadge>{runtimeTeamSummary}</EmptyStateBadge>
|
||||
) : null}
|
||||
{dispatchPreviewState?.members.length ? (
|
||||
<EmptyStateBadge>
|
||||
{dispatchPreviewState.members.length} 个成员
|
||||
</EmptyStateBadge>
|
||||
{runtimeTaskCount > 0 ? (
|
||||
<EmptyStateBadge>{runtimeTaskCount} 项任务</EmptyStateBadge>
|
||||
) : null}
|
||||
<EmptyStateBadge>不遮挡画布</EmptyStateBadge>
|
||||
</EmptyStateFooter>
|
||||
@@ -974,12 +1075,12 @@ export function TeamWorkspaceDock({
|
||||
aria-expanded={launcherOnly ? false : expanded}
|
||||
aria-label={
|
||||
launcherOnly
|
||||
? `打开${TEAM_WORKSPACE_SURFACE_TITLE}`
|
||||
? "打开任务视图"
|
||||
: expanded
|
||||
? `收起${TEAM_WORKSPACE_SURFACE_TITLE}`
|
||||
: `展开${TEAM_WORKSPACE_SURFACE_TITLE}`
|
||||
? "收起任务视图"
|
||||
: "展开任务视图"
|
||||
}
|
||||
$active={hasRealTeamGraph}
|
||||
$active={hasRuntimeSessions}
|
||||
$expanded={expanded}
|
||||
$attention={showAttentionCue}
|
||||
$launcherOnly={launcherOnly}
|
||||
@@ -1004,7 +1105,7 @@ export function TeamWorkspaceDock({
|
||||
$attention={showAttentionCue}
|
||||
$launcherOnly={launcherOnly}
|
||||
>
|
||||
<Workflow className="h-4 w-4" />
|
||||
<ListTodo className="h-4 w-4" />
|
||||
{showAttentionCue ? (
|
||||
<DockSignal
|
||||
aria-hidden="true"
|
||||
@@ -1024,9 +1125,16 @@ export function TeamWorkspaceDock({
|
||||
) : null}
|
||||
</DockTextStack>
|
||||
) : (
|
||||
<span className="min-w-0 truncate text-sm font-semibold text-slate-900">
|
||||
{toggleLabel}
|
||||
</span>
|
||||
<DockSummaryStack data-testid="team-workspace-dock-summary">
|
||||
<DockSummaryEyebrow>{dockPrimaryLabel}</DockSummaryEyebrow>
|
||||
<DockBadgeRow data-testid="team-workspace-dock-badges">
|
||||
{dockStatusBadges.map((badge) => (
|
||||
<DockStatusBadge key={badge.label} $tone={badge.tone}>
|
||||
{badge.label}
|
||||
</DockStatusBadge>
|
||||
))}
|
||||
</DockBadgeRow>
|
||||
</DockSummaryStack>
|
||||
)}
|
||||
</DockToggle>
|
||||
{!launcherOnly &&
|
||||
|
||||
@@ -75,4 +75,56 @@ describe("TokenUsageDisplay", () => {
|
||||
expect(container.textContent).toContain("1.2K tokens");
|
||||
expect(container.textContent).toContain("1.2M tokens");
|
||||
});
|
||||
|
||||
it("存在缓存命中时应附带展示 cached token", () => {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
mountedRoots.push({ container, root });
|
||||
|
||||
act(() => {
|
||||
root.render(
|
||||
<TokenUsageDisplay
|
||||
usage={{
|
||||
input_tokens: 12_000,
|
||||
output_tokens: 19_000,
|
||||
cached_input_tokens: 8_000,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("31.0K tokens");
|
||||
expect(container.textContent).toContain("命中缓存 8.0K");
|
||||
});
|
||||
|
||||
it("未启用自动缓存时应展示轻量诊断提示", () => {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
mountedRoots.push({ container, root });
|
||||
|
||||
act(() => {
|
||||
root.render(
|
||||
<TokenUsageDisplay
|
||||
usage={{
|
||||
input_tokens: 12_000,
|
||||
output_tokens: 19_000,
|
||||
cached_input_tokens: 0,
|
||||
}}
|
||||
promptCacheNotice={{
|
||||
label: "未声明自动缓存",
|
||||
detail:
|
||||
"当前 Provider 未声明支持自动 Prompt Cache;如需复用前缀,请使用显式 cache_control 标记。",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("31.0K tokens");
|
||||
expect(container.textContent).toContain("未声明自动缓存");
|
||||
expect(container.firstElementChild?.getAttribute("title")).toContain(
|
||||
"未声明支持自动 Prompt Cache",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,9 +40,22 @@ const UsageText = styled.span`
|
||||
"Courier New", monospace;
|
||||
`;
|
||||
|
||||
const UsageMeta = styled.span`
|
||||
font-family:
|
||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||
"Courier New", monospace;
|
||||
`;
|
||||
|
||||
export interface TokenUsagePromptCacheNotice {
|
||||
label: string;
|
||||
detail?: string;
|
||||
source?: "configured_provider" | "selection_fallback";
|
||||
}
|
||||
|
||||
interface TokenUsageDisplayProps {
|
||||
usage: TokenUsage;
|
||||
className?: string;
|
||||
promptCacheNotice?: TokenUsagePromptCacheNotice | null;
|
||||
}
|
||||
|
||||
function formatCompactTokenCount(value: number): string {
|
||||
@@ -68,13 +81,28 @@ function formatCompactTokenCount(value: number): string {
|
||||
export const TokenUsageDisplay: React.FC<TokenUsageDisplayProps> = ({
|
||||
usage,
|
||||
className,
|
||||
promptCacheNotice,
|
||||
}) => {
|
||||
const total = usage.input_tokens + usage.output_tokens;
|
||||
const cachedInput = Math.max(0, usage.cached_input_tokens ?? 0);
|
||||
const missingPromptCacheNotice =
|
||||
cachedInput > 0 ? null : (promptCacheNotice ?? null);
|
||||
|
||||
return (
|
||||
<UsageContainer className={className}>
|
||||
<UsageContainer
|
||||
className={className}
|
||||
title={missingPromptCacheNotice?.detail}
|
||||
>
|
||||
<UsageIcon />
|
||||
<UsageText>{formatCompactTokenCount(total)} tokens</UsageText>
|
||||
{cachedInput > 0 ? (
|
||||
<UsageMeta>{`· 命中缓存 ${formatCompactTokenCount(cachedInput)}`}</UsageMeta>
|
||||
) : null}
|
||||
{missingPromptCacheNotice ? (
|
||||
<UsageMeta data-testid="token-usage-prompt-cache-notice">
|
||||
{`· ${missingPromptCacheNotice.label}`}
|
||||
</UsageMeta>
|
||||
) : null}
|
||||
</UsageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -755,7 +755,7 @@ describe("ToolCallDisplay", () => {
|
||||
mountedRoots.push({ container, root });
|
||||
|
||||
expect(container.textContent).toContain("已打开 https://example.com/docs");
|
||||
expect(container.textContent).toContain("协作中 修复登录页");
|
||||
expect(container.textContent).toContain("子任务处理中 修复登录页");
|
||||
expect(container.textContent).toContain("已读取输出 video-task-1");
|
||||
expect(container.textContent).toContain("已加载技能 lime-governance");
|
||||
expect(container.textContent).toContain("已列出 src/**/*.tsx");
|
||||
|
||||
+2
-2
@@ -50,11 +50,11 @@ export function SelectedSessionInlineActivitySection({
|
||||
</p>
|
||||
) : selectedSessionActivityPreview?.status === "ready" ? (
|
||||
<p className="mt-2 text-sm leading-6 text-slate-500">
|
||||
这位成员暂时还没有可展示的新进展。
|
||||
这项任务暂时还没有可展示的新进展。
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-2 text-sm leading-6 text-slate-500">
|
||||
正在同步这位成员的最新进展...
|
||||
正在同步这项任务的最新进展...
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
+4
-4
@@ -44,7 +44,7 @@ export function SelectedSessionInlineCollaborationSection({
|
||||
<div className={inlineDetailSectionClassName}>
|
||||
<div className="flex flex-wrap items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.14em] text-slate-500">
|
||||
<Clock3 className="h-3.5 w-3.5" />
|
||||
<span>继续协作</span>
|
||||
<span>继续处理</span>
|
||||
{canWaitSelectedSession ? (
|
||||
<span className="rounded-full border border-slate-200 bg-white px-2 py-0.5 text-[10px] font-medium tracking-normal text-slate-600 normal-case">
|
||||
可直接查看结果
|
||||
@@ -68,7 +68,7 @@ export function SelectedSessionInlineCollaborationSection({
|
||||
: "等待结果 30 秒"}
|
||||
</Button>
|
||||
<span className="text-xs leading-5 text-slate-500">
|
||||
仅在当前内容确实依赖这位成员结果时使用。
|
||||
仅在当前内容确实依赖这项任务结果时使用。
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
@@ -79,7 +79,7 @@ export function SelectedSessionInlineCollaborationSection({
|
||||
onChange={(event) =>
|
||||
onSelectedSessionInputDraftChange(event.target.value)
|
||||
}
|
||||
placeholder="给这位协作成员补充说明、补充约束,或请它继续推进下一步。"
|
||||
placeholder="给这项任务补充说明、补充约束,或请它继续推进下一步。"
|
||||
className="min-h-[96px] resize-y border-slate-200 bg-white text-sm text-slate-700 placeholder:text-slate-400"
|
||||
data-testid="team-workspace-send-input-textarea"
|
||||
/>
|
||||
@@ -112,7 +112,7 @@ export function SelectedSessionInlineCollaborationSection({
|
||||
: "立即插入说明"}
|
||||
</Button>
|
||||
<span className="text-xs leading-5 text-slate-500">
|
||||
这条说明只会发送给当前成员,不影响其他协作成员。
|
||||
这条说明只会发送给当前任务,不影响其他并行任务。
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -100,7 +100,7 @@ export function SelectedSessionInlineDetail({
|
||||
<div className={inlineDetailSectionClassName}>
|
||||
<div className="flex flex-wrap items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.14em] text-slate-500">
|
||||
<Bot className="h-3.5 w-3.5" />
|
||||
<span>协作设置</span>
|
||||
<span>任务分工</span>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap items-center gap-2 text-xs text-slate-500">
|
||||
{detailDisplay.settingBadges.map((badge) => (
|
||||
|
||||
+2
-2
@@ -51,7 +51,7 @@ export function SelectedSessionInlineHeader({
|
||||
<span>当前查看</span>
|
||||
{selectedSession.isCurrent ? (
|
||||
<span className="rounded-full border border-slate-200 bg-white px-2 py-0.5 text-[10px] font-medium tracking-normal text-slate-600 normal-case">
|
||||
当前对话
|
||||
当前任务
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -103,7 +103,7 @@ export function SelectedSessionInlineHeader({
|
||||
variant="outline"
|
||||
onClick={() => void onOpenSelectedSession?.()}
|
||||
>
|
||||
{isChildSession ? "切换会话" : "打开对话"}
|
||||
{isChildSession ? "切换任务" : "打开任务"}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
+14
-30
@@ -1,19 +1,9 @@
|
||||
import {
|
||||
Activity,
|
||||
ArrowUpLeft,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Workflow,
|
||||
} from "lucide-react";
|
||||
import { ArrowUpLeft, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { TeamWorkspaceBoardChromeDisplayState } from "../../team-workspace-runtime/boardChromeSelectors";
|
||||
import type { TeamWorkspaceRuntimeStatus } from "../../teamWorkspaceRuntime";
|
||||
import {
|
||||
TEAM_WORKSPACE_IDLE_STATUS_LABEL,
|
||||
TEAM_WORKSPACE_REALTIME_BADGE_LABEL,
|
||||
TEAM_WORKSPACE_SURFACE_TITLE,
|
||||
} from "../../utils/teamWorkspaceCopy";
|
||||
import { TEAM_WORKSPACE_IDLE_STATUS_LABEL, TEAM_WORKSPACE_SURFACE_TITLE } from "../../utils/teamWorkspaceCopy";
|
||||
|
||||
interface TeamWorkspaceStatusMeta {
|
||||
badgeClassName: string;
|
||||
@@ -57,31 +47,25 @@ export function TeamWorkspaceBoardHeader({
|
||||
return (
|
||||
<div className={className} data-testid={dataTestId}>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">
|
||||
<Workflow className="h-3.5 w-3.5" />
|
||||
<span>{TEAM_WORKSPACE_SURFACE_TITLE}</span>
|
||||
<span className="ml-1 inline-flex items-center gap-1 rounded-full border border-emerald-200 bg-emerald-50 px-2 py-0.5 text-[10px] font-medium tracking-normal text-emerald-700 normal-case">
|
||||
<Activity className="h-3 w-3" />
|
||||
{TEAM_WORKSPACE_REALTIME_BADGE_LABEL}
|
||||
<div className="flex flex-wrap items-center gap-2 text-[11px] text-slate-500">
|
||||
<span className="rounded-full border border-slate-200 bg-slate-50 px-2.5 py-1 font-medium">
|
||||
{TEAM_WORKSPACE_SURFACE_TITLE}
|
||||
</span>
|
||||
{createdFromTurnId ? (
|
||||
<span className="rounded-full border border-slate-200 bg-white px-2.5 py-1">
|
||||
承接自 {createdFromTurnId}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-1.5 flex flex-wrap items-center gap-2">
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"font-semibold text-slate-900",
|
||||
useCompactCanvasChrome ? "text-sm" : "text-[15px]",
|
||||
)}
|
||||
className={cn("text-[15px] font-semibold leading-6 text-slate-900")}
|
||||
>
|
||||
{boardChromeDisplay.compactBoardHeadline}
|
||||
</span>
|
||||
{createdFromTurnId ? (
|
||||
<span className="rounded-full border border-slate-200 bg-white px-2.5 py-1 text-[11px] text-slate-500">
|
||||
来自之前的任务 {createdFromTurnId}
|
||||
</span>
|
||||
) : null}
|
||||
{!useCompactCanvasChrome && !isChildSession && totalTeamSessions > 0 ? (
|
||||
<span className="rounded-full border border-sky-200 bg-sky-50 px-2.5 py-1 text-[11px] text-sky-700">
|
||||
{totalTeamSessions} 位协作成员
|
||||
{totalTeamSessions} 项任务
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -136,7 +120,7 @@ export function TeamWorkspaceBoardHeader({
|
||||
onClick={() => void onReturnToParentSession()}
|
||||
>
|
||||
<ArrowUpLeft className="mr-1.5 h-3.5 w-3.5" />
|
||||
返回主助手
|
||||
返回主任务
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -193,7 +193,7 @@ export function TeamWorkspaceCanvasLaneCard({
|
||||
<div className="mt-3 flex min-h-0 flex-1 flex-col overflow-hidden rounded-2xl border border-slate-200 bg-slate-50">
|
||||
<div className="flex items-center gap-1.5 border-b border-slate-200 px-3 py-2 text-[10px] font-semibold uppercase tracking-[0.12em] text-slate-500">
|
||||
<PanelTop className="h-3 w-3" />
|
||||
<span>成员进展</span>
|
||||
<span>任务进展</span>
|
||||
<span className="rounded-full border border-slate-200 bg-white px-2 py-0.5 text-[10px] font-medium tracking-normal text-slate-600 normal-case">
|
||||
{lane.kind === "session" ? "最近进展" : "等待接入"}
|
||||
</span>
|
||||
|
||||
+3
-3
@@ -94,9 +94,9 @@ export function TeamWorkspaceCanvasStage({
|
||||
data-testid="team-workspace-canvas-shortcuts"
|
||||
>
|
||||
<span className="rounded-full border border-slate-200 bg-slate-50 px-2 py-0.5 text-[10px] font-semibold tracking-[0.12em] text-slate-500">
|
||||
画布
|
||||
任务视图
|
||||
</span>
|
||||
<span className="truncate">空白处拖拽 · Space 手型 · A 整理 · F 适应</span>
|
||||
<span className="truncate">拖拽调整任务布局 · Space 平移 · A 整理 · F 聚焦</span>
|
||||
</div>
|
||||
{lanes.length > 0 ? (
|
||||
<div
|
||||
@@ -153,7 +153,7 @@ export function TeamWorkspaceCanvasStage({
|
||||
<div className="absolute inset-0 flex items-center justify-center p-8">
|
||||
<div className="max-w-[520px] rounded-[24px] border border-dashed border-slate-300 bg-white/92 px-6 py-5 text-center shadow-sm shadow-slate-950/5">
|
||||
<div className="text-sm font-semibold text-slate-900">
|
||||
暂无协作画布
|
||||
暂无任务视图
|
||||
</div>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-500">
|
||||
{canvasStageHint}
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import { act, type ReactNode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { TeamWorkspaceCanvasStage } from "./TeamWorkspaceCanvasStage";
|
||||
import { TeamWorkspaceCanvasToolbar } from "./TeamWorkspaceCanvasToolbar";
|
||||
import { TeamWorkspaceCanvasViewButtons } from "./TeamWorkspaceTeamOverviewControls";
|
||||
|
||||
function renderIntoDocument(element: ReactNode) {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
act(() => {
|
||||
root.render(element);
|
||||
});
|
||||
|
||||
return {
|
||||
container,
|
||||
unmount: () => {
|
||||
act(() => {
|
||||
root.unmount();
|
||||
});
|
||||
container.remove();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("TeamWorkspaceCanvasSurfaceCopy", () => {
|
||||
beforeEach(() => {
|
||||
(
|
||||
globalThis as typeof globalThis & {
|
||||
IS_REACT_ACT_ENVIRONMENT?: boolean;
|
||||
}
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(
|
||||
globalThis as typeof globalThis & {
|
||||
IS_REACT_ACT_ENVIRONMENT?: boolean;
|
||||
}
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
});
|
||||
|
||||
it("应使用任务视图口径展示任务布局提示", () => {
|
||||
const { container, unmount } = renderIntoDocument(
|
||||
<TeamWorkspaceCanvasStage
|
||||
canvasBoundsHeight={640}
|
||||
canvasBoundsWidth={960}
|
||||
canvasStageHeight="640px"
|
||||
canvasStageHint="当前任务会按状态持续刷新。"
|
||||
expandedSessionId={null}
|
||||
isCanvasPanModifierActive={false}
|
||||
laneLayouts={{}}
|
||||
lanes={[]}
|
||||
onCanvasWheel={vi.fn()}
|
||||
onSelectLane={vi.fn()}
|
||||
onStartCanvasLaneDrag={vi.fn()}
|
||||
onStartCanvasLaneResize={vi.fn()}
|
||||
onStartCanvasPan={vi.fn()}
|
||||
selectedSessionId={null}
|
||||
viewport={{ x: 0, y: 0, zoom: 1 }}
|
||||
viewportRef={{ current: null }}
|
||||
/>,
|
||||
);
|
||||
|
||||
try {
|
||||
expect(container.textContent).toContain("任务视图");
|
||||
expect(container.textContent).toContain("拖拽调整任务布局");
|
||||
expect(container.textContent).toContain("暂无任务视图");
|
||||
expect(container.textContent).not.toContain("暂无任务画布");
|
||||
} finally {
|
||||
unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it("工具栏应使用任务视图与聚焦任务口径", () => {
|
||||
const { container, unmount } = renderIntoDocument(
|
||||
<TeamWorkspaceCanvasToolbar
|
||||
laneCount={3}
|
||||
onAutoArrangeCanvas={vi.fn()}
|
||||
onFitCanvasView={vi.fn()}
|
||||
onResetCanvasView={vi.fn()}
|
||||
onZoomIn={vi.fn()}
|
||||
onZoomOut={vi.fn()}
|
||||
zoom={1.15}
|
||||
/>,
|
||||
);
|
||||
|
||||
try {
|
||||
expect(container.textContent).toContain("任务视图");
|
||||
expect(container.textContent).toContain("3 个任务面板");
|
||||
expect(container.textContent).toContain("聚焦任务");
|
||||
expect(container.textContent).not.toContain("自由画布");
|
||||
expect(container.textContent).not.toContain("适应视图");
|
||||
} finally {
|
||||
unmount();
|
||||
}
|
||||
});
|
||||
|
||||
it("紧凑控制条也应使用聚焦任务口径", () => {
|
||||
const { container, unmount } = renderIntoDocument(
|
||||
<TeamWorkspaceCanvasViewButtons
|
||||
onAutoArrangeCanvas={vi.fn()}
|
||||
onFitCanvasView={vi.fn()}
|
||||
onZoomIn={vi.fn()}
|
||||
onZoomOut={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
try {
|
||||
expect(container.textContent).toContain("整理布局");
|
||||
expect(container.textContent).toContain("聚焦任务");
|
||||
expect(container.textContent).not.toContain("适应视图");
|
||||
} finally {
|
||||
unmount();
|
||||
}
|
||||
});
|
||||
});
|
||||
+3
-3
@@ -23,14 +23,14 @@ export function TeamWorkspaceCanvasToolbar({
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2 text-[11px] text-slate-500">
|
||||
<span className="rounded-full border border-slate-200 bg-white px-2.5 py-1">
|
||||
自由画布
|
||||
任务视图
|
||||
</span>
|
||||
<span className="rounded-full border border-slate-200 bg-slate-50 px-2.5 py-1">
|
||||
缩放 {Math.round(zoom * 100)}%
|
||||
</span>
|
||||
{laneCount > 0 ? (
|
||||
<span className="rounded-full border border-slate-200 bg-slate-50 px-2.5 py-1">
|
||||
{laneCount} 个成员面板
|
||||
{laneCount} 个任务面板
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -64,7 +64,7 @@ export function TeamWorkspaceCanvasToolbar({
|
||||
variant="outline"
|
||||
onClick={onFitCanvasView}
|
||||
>
|
||||
适应视图
|
||||
聚焦任务
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+7
-11
@@ -1,4 +1,4 @@
|
||||
import { Activity, ChevronDown, Workflow } from "lucide-react";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type {
|
||||
@@ -7,7 +7,6 @@ import type {
|
||||
} from "../../team-workspace-runtime/formationDisplaySelectors";
|
||||
import {
|
||||
TEAM_WORKSPACE_IDLE_STATUS_LABEL,
|
||||
TEAM_WORKSPACE_REALTIME_BADGE_LABEL,
|
||||
TEAM_WORKSPACE_SURFACE_TITLE,
|
||||
} from "../../utils/teamWorkspaceCopy";
|
||||
|
||||
@@ -43,19 +42,16 @@ export function TeamWorkspaceEmptyShellState({
|
||||
>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3 px-4 py-3.5 sm:px-5">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-2 text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">
|
||||
<Workflow className="h-3.5 w-3.5" />
|
||||
<span>{TEAM_WORKSPACE_SURFACE_TITLE}</span>
|
||||
<span className="ml-1 inline-flex items-center gap-1 rounded-full border border-emerald-200 bg-emerald-50 px-2 py-0.5 text-[10px] font-medium tracking-normal text-emerald-700 normal-case">
|
||||
<Activity className="h-3 w-3" />
|
||||
{TEAM_WORKSPACE_REALTIME_BADGE_LABEL}
|
||||
<div className="flex flex-wrap items-center gap-2 text-[11px] text-slate-500">
|
||||
<span className="rounded-full border border-slate-200 bg-slate-50 px-2.5 py-1 font-medium">
|
||||
{TEAM_WORKSPACE_SURFACE_TITLE}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-1.5 flex flex-wrap items-center gap-2">
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm font-semibold text-slate-900">
|
||||
{hasRuntimeFormation
|
||||
? runtimeFormationDisplay.panelHeadline
|
||||
: "任务协作已就绪"}
|
||||
: "任务面板已就绪"}
|
||||
</span>
|
||||
<span className="rounded-full border border-slate-200 bg-slate-50 px-2.5 py-1 text-[11px] text-slate-600">
|
||||
{runtimeFormationDisplay.panelStatusLabel ||
|
||||
@@ -85,7 +81,7 @@ export function TeamWorkspaceEmptyShellState({
|
||||
data-testid="team-workspace-detail-toggle"
|
||||
>
|
||||
<ChevronDown className="mr-1.5 h-3.5 w-3.5" />
|
||||
查看任务进行时
|
||||
查看任务视图
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@ export function TeamWorkspaceFallbackDetailSection({
|
||||
) : null}
|
||||
<div className="mt-4 flex flex-wrap items-center gap-2 text-xs text-slate-500">
|
||||
<span className="rounded-full border border-slate-200 bg-white px-2.5 py-1">
|
||||
推荐流程:邀请协作成员 → 查看结果 → 补充说明
|
||||
推荐流程:拆出任务 → 查看结果 → 补充说明
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+82
-103
@@ -22,59 +22,92 @@ export function TeamWorkspaceTeamOperationsPanel({
|
||||
return null;
|
||||
}
|
||||
|
||||
const heading = (
|
||||
<div className="flex flex-wrap items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.14em] text-slate-500">
|
||||
<Activity className="h-3.5 w-3.5" />
|
||||
<span>任务进展</span>
|
||||
<span className="rounded-full border border-slate-200 bg-white px-2 py-0.5 text-[10px] font-medium tracking-normal text-slate-600 normal-case">
|
||||
最近 {teamOperationEntries.length} 条
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderEntry = (entry: TeamOperationDisplayEntry, compact: boolean) => {
|
||||
const content = (
|
||||
<>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-full px-2 py-0.5 text-[10px] font-medium",
|
||||
entry.badgeClassName,
|
||||
)}
|
||||
>
|
||||
{entry.title}
|
||||
</span>
|
||||
<span className="text-[11px] text-slate-500">
|
||||
{formatOperationUpdatedAt(entry.updatedAt)}
|
||||
</span>
|
||||
</div>
|
||||
<p
|
||||
className={cn(
|
||||
"mt-1 whitespace-pre-wrap break-words text-slate-700",
|
||||
compact ? "text-xs leading-5" : "text-sm leading-6",
|
||||
)}
|
||||
>
|
||||
{entry.detail}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
||||
if (entry.targetSessionId) {
|
||||
return (
|
||||
<button
|
||||
key={entry.id}
|
||||
type="button"
|
||||
className={cn(
|
||||
"w-full text-left transition",
|
||||
compact
|
||||
? "rounded-[14px] border border-slate-200 bg-white px-3 py-2.5 hover:border-slate-300"
|
||||
: embedded
|
||||
? "border-l-2 border-slate-200 px-3 py-2 hover:border-slate-300 hover:bg-slate-50"
|
||||
: "rounded-[14px] border border-slate-200 bg-slate-50 px-3 py-2.5 hover:border-slate-300 hover:bg-slate-50",
|
||||
)}
|
||||
onClick={() => onSelectTeamOperationEntry(entry)}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={entry.id}
|
||||
className={cn(
|
||||
compact
|
||||
? "rounded-[14px] border border-slate-200 bg-white px-3 py-2.5"
|
||||
: embedded
|
||||
? "border-l-2 border-slate-200 px-3 py-2"
|
||||
: "rounded-[14px] border border-slate-200 bg-slate-50 px-3 py-2.5",
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
if (useCompactCanvasChrome) {
|
||||
return (
|
||||
<div
|
||||
className="mt-2 flex items-start gap-2 overflow-x-auto pb-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
className="mt-2 rounded-[18px] border border-slate-200 bg-slate-50 p-3"
|
||||
data-testid="team-workspace-team-operations"
|
||||
>
|
||||
<div className="sticky left-0 z-10 flex shrink-0 items-center gap-2 rounded-full border border-slate-200 bg-white px-3 py-1.5 text-[11px] font-medium text-slate-500 shadow-sm shadow-slate-950/5">
|
||||
<Activity className="h-3.5 w-3.5" />
|
||||
<span>协作动态</span>
|
||||
<span className="rounded-full border border-slate-200 bg-slate-50 px-2 py-0.5 text-[10px] text-slate-600">
|
||||
{teamOperationEntries.length}
|
||||
</span>
|
||||
{heading}
|
||||
<div
|
||||
className="mt-3 space-y-2"
|
||||
data-testid="team-workspace-team-operations-list"
|
||||
>
|
||||
{teamOperationEntries.map((entry) => renderEntry(entry, true))}
|
||||
</div>
|
||||
{teamOperationEntries.map((entry) => {
|
||||
const content = (
|
||||
<div className="flex min-w-0 items-start gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"mt-0.5 shrink-0 rounded-full px-2 py-0.5 text-[10px] font-medium",
|
||||
entry.badgeClassName,
|
||||
)}
|
||||
>
|
||||
{entry.title}
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-xs leading-5 text-slate-700">
|
||||
{entry.detail}
|
||||
</div>
|
||||
<div className="mt-0.5 text-[10px] text-slate-500">
|
||||
{formatOperationUpdatedAt(entry.updatedAt)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return entry.targetSessionId ? (
|
||||
<button
|
||||
key={entry.id}
|
||||
type="button"
|
||||
className="inline-flex min-w-[220px] max-w-[340px] shrink-0 rounded-[16px] border border-slate-200 bg-white px-3 py-2 text-left transition hover:border-slate-300 hover:bg-slate-50"
|
||||
onClick={() => onSelectTeamOperationEntry(entry)}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
) : (
|
||||
<div
|
||||
key={entry.id}
|
||||
className="inline-flex min-w-[220px] max-w-[340px] shrink-0 rounded-[16px] border border-slate-200 bg-slate-50 px-3 py-2"
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -88,63 +121,9 @@ export function TeamWorkspaceTeamOperationsPanel({
|
||||
)}
|
||||
data-testid="team-workspace-team-operations"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.14em] text-slate-500">
|
||||
<Activity className="h-3.5 w-3.5" />
|
||||
<span>协作动态</span>
|
||||
<span className="rounded-full border border-slate-200 bg-slate-50 px-2 py-0.5 text-[10px] font-medium tracking-normal text-slate-600 normal-case">
|
||||
最近 {teamOperationEntries.length} 条
|
||||
</span>
|
||||
</div>
|
||||
{heading}
|
||||
<div className="mt-3 space-y-2">
|
||||
{teamOperationEntries.map((entry) => {
|
||||
const content = (
|
||||
<>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-full px-2 py-0.5 text-[10px] font-medium",
|
||||
entry.badgeClassName,
|
||||
)}
|
||||
>
|
||||
{entry.title}
|
||||
</span>
|
||||
<span className="text-[11px] text-slate-500">
|
||||
{formatOperationUpdatedAt(entry.updatedAt)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-sm leading-6 text-slate-700">
|
||||
{entry.detail}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
||||
return entry.targetSessionId ? (
|
||||
<button
|
||||
key={entry.id}
|
||||
type="button"
|
||||
className={cn(
|
||||
"w-full text-left transition",
|
||||
embedded
|
||||
? "border-l-2 border-slate-200 px-3 py-2 hover:border-slate-300 hover:bg-slate-50"
|
||||
: "rounded-[14px] border border-slate-200 bg-slate-50 px-3 py-2.5 hover:border-slate-300 hover:bg-slate-50",
|
||||
)}
|
||||
onClick={() => onSelectTeamOperationEntry(entry)}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
) : (
|
||||
<div
|
||||
key={entry.id}
|
||||
className={cn(
|
||||
embedded
|
||||
? "border-l-2 border-slate-200 px-3 py-2"
|
||||
: "rounded-[14px] border border-slate-200 bg-slate-50 px-3 py-2.5",
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{teamOperationEntries.map((entry) => renderEntry(entry, false))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+187
@@ -0,0 +1,187 @@
|
||||
import { act, type ComponentProps, type ReactNode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { TeamWorkspaceTeamOverviewChrome } from "./TeamWorkspaceTeamOverviewChrome";
|
||||
|
||||
vi.mock("@/components/ui/button", () => ({
|
||||
Button: ({
|
||||
children,
|
||||
className,
|
||||
onClick,
|
||||
type = "button",
|
||||
...props
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
type?: "button" | "submit" | "reset";
|
||||
[key: string]: unknown;
|
||||
}) => (
|
||||
<button type={type} className={className} onClick={onClick} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}));
|
||||
|
||||
const mountedRoots: Array<{
|
||||
root: ReturnType<typeof createRoot>;
|
||||
container: HTMLDivElement;
|
||||
}> = [];
|
||||
|
||||
beforeEach(() => {
|
||||
(
|
||||
globalThis as typeof globalThis & {
|
||||
IS_REACT_ACT_ENVIRONMENT?: boolean;
|
||||
}
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
while (mountedRoots.length > 0) {
|
||||
const mounted = mountedRoots.pop();
|
||||
if (!mounted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
act(() => {
|
||||
mounted.root.unmount();
|
||||
});
|
||||
mounted.container.remove();
|
||||
}
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
function renderChrome(
|
||||
props?: Partial<ComponentProps<typeof TeamWorkspaceTeamOverviewChrome>>,
|
||||
) {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
const defaultProps: ComponentProps<typeof TeamWorkspaceTeamOverviewChrome> =
|
||||
{
|
||||
boardChromeDisplay: {
|
||||
boardHeadline: "任务进行中 · 1 项处理中 / 1 项稍后开始",
|
||||
boardHint: "这里只展示当前有哪些任务在处理、状态如何,以及最近更新到了哪里。",
|
||||
compactBoardHeadline: "任务进行中 · 1 项处理中 / 1 项稍后开始",
|
||||
compactToolbarChips: [
|
||||
{ key: "focus", text: "当前焦点 研究员", tone: "summary" },
|
||||
{ key: "status", text: "处理中", tone: "status", status: "running" },
|
||||
{ key: "updated-at", text: "更新于 刚刚", tone: "muted" },
|
||||
{ key: "current", text: "当前任务", tone: "muted" },
|
||||
{ key: "waitable", text: "2 项处理中", tone: "muted" },
|
||||
],
|
||||
statusSummaryBadges: [],
|
||||
},
|
||||
canCloseCompletedTeamSessions: true,
|
||||
canWaitAnyActiveTeamSession: true,
|
||||
completedCount: 1,
|
||||
embedded: true,
|
||||
formatUpdatedAt: () => "刚刚",
|
||||
memberCanvasSubtitle:
|
||||
"2 项任务已接入,当前焦点会优先落在正在处理的任务上。",
|
||||
memberCanvasTitle: "任务视图",
|
||||
onAutoArrangeCanvas: vi.fn(),
|
||||
onCloseCompletedTeamSessions: vi.fn(),
|
||||
onFitCanvasView: vi.fn(),
|
||||
onSelectTeamOperationEntry: vi.fn(),
|
||||
onWaitAnyActiveTeamSessions: vi.fn(),
|
||||
onZoomIn: vi.fn(),
|
||||
onZoomOut: vi.fn(),
|
||||
pendingTeamAction: null,
|
||||
resolveStatusMeta: () => ({
|
||||
badgeClassName: "border border-sky-200 bg-sky-50 text-sky-700",
|
||||
}),
|
||||
selectedSession: {
|
||||
name: "研究员",
|
||||
isCurrent: true,
|
||||
updatedAt: 1710000000,
|
||||
},
|
||||
teamOperationEntries: [],
|
||||
useCompactCanvasChrome: true,
|
||||
waitableCount: 2,
|
||||
};
|
||||
|
||||
act(() => {
|
||||
root.render(
|
||||
<TeamWorkspaceTeamOverviewChrome {...defaultProps} {...props} />,
|
||||
);
|
||||
});
|
||||
|
||||
mountedRoots.push({ root, container });
|
||||
return container;
|
||||
}
|
||||
|
||||
describe("TeamWorkspaceTeamOverviewChrome", () => {
|
||||
it("紧凑态应把当前焦点放进独立摘要块,并把操作放在下一行", () => {
|
||||
const container = renderChrome();
|
||||
|
||||
const summary = container.querySelector(
|
||||
'[data-testid="team-workspace-compact-summary"]',
|
||||
) as HTMLElement | null;
|
||||
const toolbar = container.querySelector(
|
||||
'[data-testid="team-workspace-canvas-toolbar"]',
|
||||
) as HTMLElement | null;
|
||||
const controls = container.querySelector(
|
||||
'[data-testid="team-workspace-compact-controls"]',
|
||||
) as HTMLElement | null;
|
||||
|
||||
expect(summary?.textContent).toContain("任务视图");
|
||||
expect(summary?.textContent).toContain("研究员");
|
||||
expect(summary?.textContent).toContain("当前焦点");
|
||||
expect(summary?.textContent).toContain(
|
||||
"当前焦点会优先落在正在处理的任务上。",
|
||||
);
|
||||
expect(summary?.textContent).toContain("更新于 刚刚");
|
||||
expect(summary?.textContent).toContain("当前任务");
|
||||
|
||||
expect(toolbar?.textContent).toContain("处理中");
|
||||
expect(toolbar?.textContent).toContain("2 项处理中");
|
||||
expect(toolbar?.textContent).not.toContain("当前焦点 研究员");
|
||||
|
||||
expect(controls?.textContent).toContain("任务处理");
|
||||
expect(controls?.textContent).toContain("视图");
|
||||
expect(controls?.textContent).toContain("等待任一任务结果");
|
||||
expect(controls?.textContent).toContain("收起已完成任务");
|
||||
expect(controls?.textContent).toContain("聚焦任务");
|
||||
expect(controls?.textContent).toContain("整理布局");
|
||||
expect(controls?.textContent).not.toContain("缩小");
|
||||
expect(controls?.textContent).not.toContain("放大");
|
||||
});
|
||||
|
||||
it("紧凑态存在任务进展时,应优先把进展列表放在次级操作之前", () => {
|
||||
const container = renderChrome({
|
||||
teamOperationEntries: [
|
||||
{
|
||||
id: "wait-1",
|
||||
title: "收到结果",
|
||||
detail: "刚才等到 执行器 返回了新结果,当前状态为已完成。",
|
||||
badgeClassName: "border border-emerald-200 bg-emerald-50 text-emerald-700",
|
||||
updatedAt: 1710000000,
|
||||
targetSessionId: "child-1",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const operations = container.querySelector(
|
||||
'[data-testid="team-workspace-team-operations"]',
|
||||
) as HTMLElement | null;
|
||||
const controls = container.querySelector(
|
||||
'[data-testid="team-workspace-compact-controls"]',
|
||||
) as HTMLElement | null;
|
||||
|
||||
expect(operations?.textContent).toContain("任务进展");
|
||||
expect(operations?.textContent).toContain("最近 1 条");
|
||||
expect(operations?.textContent).toContain("收到结果");
|
||||
expect(controls?.textContent).toContain("任务处理");
|
||||
expect(controls?.textContent).toContain("视图");
|
||||
const operationsOrder =
|
||||
operations && controls
|
||||
? operations.compareDocumentPosition(controls) &
|
||||
Node.DOCUMENT_POSITION_FOLLOWING
|
||||
: 0;
|
||||
expect(
|
||||
operationsOrder,
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+121
-41
@@ -14,6 +14,7 @@ interface TeamWorkspaceStatusMeta {
|
||||
}
|
||||
|
||||
interface TeamWorkspaceOverviewSelectedSession {
|
||||
name?: string;
|
||||
isCurrent?: boolean;
|
||||
updatedAt?: number;
|
||||
}
|
||||
@@ -71,47 +72,124 @@ export function TeamWorkspaceTeamOverviewChrome({
|
||||
"rounded-full border border-slate-200 bg-white px-2.5 py-1 text-[11px] text-slate-500";
|
||||
const compactCanvasMutedChipClassName =
|
||||
"rounded-full border border-slate-200 bg-slate-50 px-2.5 py-1 text-[11px] text-slate-500";
|
||||
const compactActionButtonClassName =
|
||||
"h-8 rounded-full border-slate-200 bg-white px-3 text-xs text-slate-700 hover:bg-slate-100";
|
||||
const compactViewButtonClassName =
|
||||
"h-8 rounded-full border-slate-200 bg-slate-50 px-3 text-xs text-slate-600 hover:bg-white";
|
||||
const compactSummaryStatusChips = boardChromeDisplay.compactToolbarChips.filter(
|
||||
(chip) => chip.key !== "focus",
|
||||
);
|
||||
const hasCompactTaskActions =
|
||||
canWaitAnyActiveTeamSession || canCloseCompletedTeamSessions;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
{useCompactCanvasChrome ? (
|
||||
<>
|
||||
<div
|
||||
className="flex flex-wrap items-center gap-2"
|
||||
data-testid="team-workspace-canvas-toolbar"
|
||||
>
|
||||
{boardChromeDisplay.compactToolbarChips.map((chip) => (
|
||||
<TeamWorkspaceCompactToolbarChip
|
||||
key={chip.key}
|
||||
chip={chip}
|
||||
compactCanvasMutedChipClassName={
|
||||
compactCanvasMutedChipClassName
|
||||
}
|
||||
compactCanvasSummaryChipClassName={
|
||||
compactCanvasSummaryChipClassName
|
||||
}
|
||||
resolveStatusMeta={resolveStatusMeta}
|
||||
/>
|
||||
))}
|
||||
{useCompactCanvasChrome ? (
|
||||
<div className="space-y-2.5">
|
||||
<div
|
||||
className="rounded-[20px] border border-slate-200 bg-white px-3.5 py-3 shadow-sm shadow-slate-950/5"
|
||||
data-testid="team-workspace-compact-summary"
|
||||
>
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.14em] text-slate-500">
|
||||
{memberCanvasTitle}
|
||||
</div>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-2">
|
||||
<div className="truncate text-sm font-semibold text-slate-900">
|
||||
{selectedSession?.name?.trim() || "等待任务接手"}
|
||||
</div>
|
||||
<span className="rounded-full border border-sky-200 bg-sky-50 px-2 py-0.5 text-[10px] font-medium text-sky-700">
|
||||
{selectedSession ? "当前焦点" : "等待接手"}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs leading-5 text-slate-500">
|
||||
{memberCanvasSubtitle}
|
||||
</p>
|
||||
</div>
|
||||
{selectedSession ? (
|
||||
<div className="flex flex-wrap items-center gap-2 text-[11px] text-slate-500">
|
||||
<span className="inline-flex items-center gap-1 rounded-full border border-slate-200 bg-slate-50 px-2.5 py-1">
|
||||
<Clock3 className="h-3.5 w-3.5" />
|
||||
更新于 {formatUpdatedAt(selectedSession.updatedAt)}
|
||||
</span>
|
||||
{selectedSession.isCurrent ? (
|
||||
<span className="rounded-full border border-slate-200 bg-slate-50 px-2.5 py-1">
|
||||
当前任务
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<TeamWorkspaceTeamActionButtons
|
||||
canCloseCompletedTeamSessions={canCloseCompletedTeamSessions}
|
||||
canWaitAnyActiveTeamSession={canWaitAnyActiveTeamSession}
|
||||
onCloseCompletedTeamSessions={onCloseCompletedTeamSessions}
|
||||
onWaitAnyActiveTeamSessions={onWaitAnyActiveTeamSessions}
|
||||
pendingTeamAction={pendingTeamAction}
|
||||
/>
|
||||
{compactSummaryStatusChips.length > 0 ? (
|
||||
<div
|
||||
className="mt-3 flex flex-wrap items-center gap-2"
|
||||
data-testid="team-workspace-canvas-toolbar"
|
||||
>
|
||||
{compactSummaryStatusChips.map((chip) => (
|
||||
<TeamWorkspaceCompactToolbarChip
|
||||
key={chip.key}
|
||||
chip={chip}
|
||||
compactCanvasMutedChipClassName={
|
||||
compactCanvasMutedChipClassName
|
||||
}
|
||||
compactCanvasSummaryChipClassName={
|
||||
compactCanvasSummaryChipClassName
|
||||
}
|
||||
resolveStatusMeta={resolveStatusMeta}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<TeamWorkspaceTeamOperationsPanel
|
||||
embedded={embedded}
|
||||
onSelectTeamOperationEntry={onSelectTeamOperationEntry}
|
||||
teamOperationEntries={teamOperationEntries}
|
||||
useCompactCanvasChrome={true}
|
||||
/>
|
||||
<div
|
||||
className="flex flex-wrap items-start justify-between gap-2.5"
|
||||
data-testid="team-workspace-compact-controls"
|
||||
>
|
||||
{hasCompactTaskActions ? (
|
||||
<div
|
||||
className="flex flex-wrap items-center gap-2 rounded-[18px] border border-slate-200 bg-slate-50 px-3 py-2"
|
||||
data-testid="team-workspace-compact-task-actions"
|
||||
>
|
||||
<span className="text-[10px] font-semibold uppercase tracking-[0.12em] text-slate-500">
|
||||
任务处理
|
||||
</span>
|
||||
<TeamWorkspaceTeamActionButtons
|
||||
buttonClassName={compactActionButtonClassName}
|
||||
canCloseCompletedTeamSessions={canCloseCompletedTeamSessions}
|
||||
canWaitAnyActiveTeamSession={canWaitAnyActiveTeamSession}
|
||||
onCloseCompletedTeamSessions={onCloseCompletedTeamSessions}
|
||||
onWaitAnyActiveTeamSessions={onWaitAnyActiveTeamSessions}
|
||||
pendingTeamAction={pendingTeamAction}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
className="flex flex-wrap items-center gap-2 rounded-[18px] border border-slate-200 bg-white px-3 py-2"
|
||||
data-testid="team-workspace-compact-view-actions"
|
||||
>
|
||||
<span className="text-[10px] font-semibold uppercase tracking-[0.12em] text-slate-500">
|
||||
视图
|
||||
</span>
|
||||
<TeamWorkspaceCanvasViewButtons
|
||||
buttonClassName={compactViewButtonClassName}
|
||||
includeZoomControls={false}
|
||||
onAutoArrangeCanvas={onAutoArrangeCanvas}
|
||||
onFitCanvasView={onFitCanvasView}
|
||||
onZoomIn={onZoomIn}
|
||||
onZoomOut={onZoomOut}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<>
|
||||
<div>
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">
|
||||
@@ -129,14 +207,14 @@ export function TeamWorkspaceTeamOverviewChrome({
|
||||
</span>
|
||||
{selectedSession.isCurrent ? (
|
||||
<span className="rounded-full border border-slate-200 bg-slate-50 px-2.5 py-1">
|
||||
当前对话
|
||||
当前任务
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!useCompactCanvasChrome &&
|
||||
(canWaitAnyActiveTeamSession ||
|
||||
canCloseCompletedTeamSessions ||
|
||||
@@ -151,22 +229,24 @@ export function TeamWorkspaceTeamOverviewChrome({
|
||||
/>
|
||||
{canWaitAnyActiveTeamSession ? (
|
||||
<span className="rounded-full border border-slate-200 bg-slate-50 px-2.5 py-1 text-[11px] text-slate-500">
|
||||
{waitableCount} 位成员正在处理中
|
||||
{waitableCount} 项任务正在处理中
|
||||
</span>
|
||||
) : null}
|
||||
{canCloseCompletedTeamSessions ? (
|
||||
<span className="rounded-full border border-slate-200 bg-slate-50 px-2.5 py-1 text-[11px] text-slate-500">
|
||||
{completedCount} 位成员已完成
|
||||
{completedCount} 项任务已完成
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<TeamWorkspaceTeamOperationsPanel
|
||||
embedded={embedded}
|
||||
onSelectTeamOperationEntry={onSelectTeamOperationEntry}
|
||||
teamOperationEntries={teamOperationEntries}
|
||||
useCompactCanvasChrome={useCompactCanvasChrome}
|
||||
/>
|
||||
{!useCompactCanvasChrome ? (
|
||||
<TeamWorkspaceTeamOperationsPanel
|
||||
embedded={embedded}
|
||||
onSelectTeamOperationEntry={onSelectTeamOperationEntry}
|
||||
teamOperationEntries={teamOperationEntries}
|
||||
useCompactCanvasChrome={false}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+42
-11
@@ -9,6 +9,7 @@ interface TeamWorkspaceStatusMeta {
|
||||
}
|
||||
|
||||
interface TeamWorkspaceTeamActionButtonsProps {
|
||||
buttonClassName?: string;
|
||||
canCloseCompletedTeamSessions: boolean;
|
||||
canWaitAnyActiveTeamSession: boolean;
|
||||
onCloseCompletedTeamSessions: () => void | Promise<void>;
|
||||
@@ -17,6 +18,7 @@ interface TeamWorkspaceTeamActionButtonsProps {
|
||||
}
|
||||
|
||||
export function TeamWorkspaceTeamActionButtons({
|
||||
buttonClassName,
|
||||
canCloseCompletedTeamSessions,
|
||||
canWaitAnyActiveTeamSession,
|
||||
onCloseCompletedTeamSessions,
|
||||
@@ -30,6 +32,7 @@ export function TeamWorkspaceTeamActionButtons({
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className={buttonClassName}
|
||||
disabled={pendingTeamAction === "wait_any"}
|
||||
onClick={() => void onWaitAnyActiveTeamSessions()}
|
||||
data-testid="team-workspace-wait-active-button"
|
||||
@@ -39,7 +42,7 @@ export function TeamWorkspaceTeamActionButtons({
|
||||
) : null}
|
||||
{pendingTeamAction === "wait_any"
|
||||
? "等待中..."
|
||||
: "等待任一成员结果"}
|
||||
: "等待任一任务结果"}
|
||||
</Button>
|
||||
) : null}
|
||||
{canCloseCompletedTeamSessions ? (
|
||||
@@ -47,6 +50,7 @@ export function TeamWorkspaceTeamActionButtons({
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className={buttonClassName}
|
||||
disabled={pendingTeamAction === "close_completed"}
|
||||
onClick={() => void onCloseCompletedTeamSessions()}
|
||||
data-testid="team-workspace-close-completed-button"
|
||||
@@ -56,7 +60,7 @@ export function TeamWorkspaceTeamActionButtons({
|
||||
) : null}
|
||||
{pendingTeamAction === "close_completed"
|
||||
? "收起中..."
|
||||
: "收起已完成成员"}
|
||||
: "收起已完成任务"}
|
||||
</Button>
|
||||
) : null}
|
||||
</>
|
||||
@@ -64,6 +68,8 @@ export function TeamWorkspaceTeamActionButtons({
|
||||
}
|
||||
|
||||
interface TeamWorkspaceCanvasViewButtonsProps {
|
||||
buttonClassName?: string;
|
||||
includeZoomControls?: boolean;
|
||||
onAutoArrangeCanvas: () => void;
|
||||
onFitCanvasView: () => void;
|
||||
onZoomIn: () => void;
|
||||
@@ -71,6 +77,8 @@ interface TeamWorkspaceCanvasViewButtonsProps {
|
||||
}
|
||||
|
||||
export function TeamWorkspaceCanvasViewButtons({
|
||||
buttonClassName,
|
||||
includeZoomControls = true,
|
||||
onAutoArrangeCanvas,
|
||||
onFitCanvasView,
|
||||
onZoomIn,
|
||||
@@ -82,20 +90,43 @@ export function TeamWorkspaceCanvasViewButtons({
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className={buttonClassName}
|
||||
onClick={onFitCanvasView}
|
||||
>
|
||||
聚焦任务
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className={buttonClassName}
|
||||
onClick={onAutoArrangeCanvas}
|
||||
data-testid="team-workspace-auto-arrange-button"
|
||||
>
|
||||
整理布局
|
||||
</Button>
|
||||
<Button type="button" size="sm" variant="outline" onClick={onZoomOut}>
|
||||
缩小
|
||||
</Button>
|
||||
<Button type="button" size="sm" variant="outline" onClick={onZoomIn}>
|
||||
放大
|
||||
</Button>
|
||||
<Button type="button" size="sm" variant="outline" onClick={onFitCanvasView}>
|
||||
适应视图
|
||||
</Button>
|
||||
{includeZoomControls ? (
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className={buttonClassName}
|
||||
onClick={onZoomOut}
|
||||
>
|
||||
缩小
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className={buttonClassName}
|
||||
onClick={onZoomIn}
|
||||
>
|
||||
放大
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildTeamWorkspaceBoardSurfaceClassNames,
|
||||
resolveTeamWorkspaceBoardCopyState,
|
||||
} from "./teamWorkspaceBoardPresentationSelectors";
|
||||
|
||||
describe("teamWorkspaceBoardPresentationSelectors", () => {
|
||||
it("应为无真实成员画布的 schedule 状态返回稳定文案", () => {
|
||||
expect(
|
||||
resolveTeamWorkspaceBoardCopyState({
|
||||
detailExpanded: false,
|
||||
dispatchPreviewStatus: "forming",
|
||||
hasRuntimeSessions: false,
|
||||
isChildSession: false,
|
||||
isEmptyShellState: false,
|
||||
shellExpanded: false,
|
||||
visibleSessionsCount: 0,
|
||||
}),
|
||||
).toMatchObject({
|
||||
detailToggleLabel: "查看细节",
|
||||
detailVisible: false,
|
||||
memberCanvasTitle: "任务视图",
|
||||
});
|
||||
|
||||
expect(
|
||||
resolveTeamWorkspaceBoardCopyState({
|
||||
detailExpanded: false,
|
||||
dispatchPreviewStatus: "forming",
|
||||
hasRuntimeSessions: false,
|
||||
isChildSession: false,
|
||||
isEmptyShellState: false,
|
||||
shellExpanded: false,
|
||||
visibleSessionsCount: 0,
|
||||
}).memberCanvasSubtitle,
|
||||
).toContain("正在准备当前任务分工");
|
||||
expect(
|
||||
resolveTeamWorkspaceBoardCopyState({
|
||||
detailExpanded: false,
|
||||
dispatchPreviewStatus: "failed",
|
||||
hasRuntimeSessions: false,
|
||||
isChildSession: false,
|
||||
isEmptyShellState: false,
|
||||
shellExpanded: false,
|
||||
visibleSessionsCount: 0,
|
||||
}).memberCanvasSubtitle,
|
||||
).toContain("任务分工准备失败");
|
||||
});
|
||||
|
||||
it("应为真实任务画布与嵌入态返回紧凑壳层样式", () => {
|
||||
const copyState = resolveTeamWorkspaceBoardCopyState({
|
||||
detailExpanded: false,
|
||||
dispatchPreviewStatus: null,
|
||||
hasRuntimeSessions: true,
|
||||
isChildSession: false,
|
||||
isEmptyShellState: false,
|
||||
shellExpanded: false,
|
||||
visibleSessionsCount: 3,
|
||||
});
|
||||
const classNames = buildTeamWorkspaceBoardSurfaceClassNames({
|
||||
className: "custom-shell",
|
||||
detailVisible: false,
|
||||
embedded: true,
|
||||
selectedSessionStatusCardClassName: "border-sky-200 bg-white",
|
||||
selectedSessionVisible: true,
|
||||
useCompactCanvasChrome: true,
|
||||
});
|
||||
|
||||
expect(copyState.memberCanvasSubtitle).toContain("3 项任务已接入");
|
||||
expect(classNames.boardShellClassName).toContain("rounded-[24px]");
|
||||
expect(classNames.boardShellClassName).toContain("border-slate-200");
|
||||
expect(classNames.boardShellClassName).toContain("bg-white");
|
||||
expect(classNames.boardShellClassName).toContain("custom-shell");
|
||||
expect(classNames.boardHeaderClassName).toContain("sticky top-0");
|
||||
expect(classNames.canvasStageHeight).toBe("clamp(560px, 76vh, 980px)");
|
||||
expect(classNames.detailCardClassName).toContain("rounded-[20px]");
|
||||
expect(classNames.railCardClassName).toContain("space-y-3");
|
||||
});
|
||||
});
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { TeamWorkspaceRuntimeFormationStatus } from "../../teamWorkspaceRuntime";
|
||||
import { TEAM_WORKSPACE_CANVAS_STAGE_HEIGHT } from "../../utils/teamWorkspaceCanvas";
|
||||
|
||||
interface ResolveTeamWorkspaceBoardCopyStateParams {
|
||||
detailExpanded: boolean;
|
||||
dispatchPreviewStatus?: TeamWorkspaceRuntimeFormationStatus | null;
|
||||
hasRuntimeSessions: boolean;
|
||||
isChildSession: boolean;
|
||||
isEmptyShellState: boolean;
|
||||
shellExpanded: boolean;
|
||||
visibleSessionsCount: number;
|
||||
}
|
||||
|
||||
interface TeamWorkspaceBoardCopyState {
|
||||
detailToggleLabel: string;
|
||||
detailVisible: boolean;
|
||||
memberCanvasSubtitle: string;
|
||||
memberCanvasTitle: string;
|
||||
}
|
||||
|
||||
interface BuildTeamWorkspaceBoardSurfaceClassNamesParams {
|
||||
className?: string;
|
||||
detailVisible: boolean;
|
||||
embedded: boolean;
|
||||
selectedSessionStatusCardClassName?: string | null;
|
||||
selectedSessionVisible: boolean;
|
||||
useCompactCanvasChrome: boolean;
|
||||
}
|
||||
|
||||
interface TeamWorkspaceBoardSurfaceClassNames {
|
||||
boardBodyClassName: string;
|
||||
boardHeaderClassName: string;
|
||||
boardShellClassName: string;
|
||||
canvasStageHeight: string;
|
||||
detailCardClassName: string;
|
||||
inlineDetailSectionClassName: string;
|
||||
inlineTimelineEntryClassName: string;
|
||||
inlineTimelineFeedClassName: string;
|
||||
railCardClassName: string;
|
||||
}
|
||||
|
||||
export function resolveTeamWorkspaceBoardCopyState({
|
||||
detailExpanded,
|
||||
dispatchPreviewStatus = null,
|
||||
hasRuntimeSessions,
|
||||
isChildSession,
|
||||
isEmptyShellState,
|
||||
shellExpanded,
|
||||
visibleSessionsCount,
|
||||
}: ResolveTeamWorkspaceBoardCopyStateParams): TeamWorkspaceBoardCopyState {
|
||||
const detailVisible =
|
||||
isEmptyShellState || !hasRuntimeSessions
|
||||
? detailExpanded || shellExpanded
|
||||
: false;
|
||||
|
||||
return {
|
||||
detailToggleLabel: detailVisible ? "收起细节" : "查看细节",
|
||||
detailVisible,
|
||||
memberCanvasTitle: "任务视图",
|
||||
memberCanvasSubtitle: hasRuntimeSessions
|
||||
? isChildSession
|
||||
? "并行任务会在各自面板里持续更新进展和结果,主对话只保留必要摘要。"
|
||||
: `${visibleSessionsCount} 项任务已接入,当前焦点会优先落在正在处理的任务上。`
|
||||
: dispatchPreviewStatus === "forming"
|
||||
? "正在准备当前任务分工,任务拆出后会在这里独立更新进展。"
|
||||
: dispatchPreviewStatus === "formed"
|
||||
? "当前任务分工已经就绪,任务拆出后会在各自面板里开始处理。"
|
||||
: dispatchPreviewStatus === "failed"
|
||||
? "这次任务分工准备失败,暂时无法生成任务面板。"
|
||||
: "任务拆出后,这里会展开为独立的任务视图。",
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTeamWorkspaceBoardSurfaceClassNames({
|
||||
className,
|
||||
detailVisible,
|
||||
embedded,
|
||||
selectedSessionStatusCardClassName = null,
|
||||
selectedSessionVisible,
|
||||
useCompactCanvasChrome,
|
||||
}: BuildTeamWorkspaceBoardSurfaceClassNamesParams): TeamWorkspaceBoardSurfaceClassNames {
|
||||
return {
|
||||
boardBodyClassName: embedded
|
||||
? cn(
|
||||
"min-h-0 flex-1 overflow-y-auto overscroll-contain",
|
||||
useCompactCanvasChrome
|
||||
? "space-y-2.5 p-3 sm:p-3.5"
|
||||
: "space-y-3 p-3 sm:p-4",
|
||||
)
|
||||
: cn(useCompactCanvasChrome ? "p-3 sm:p-3.5" : "p-3 sm:p-4"),
|
||||
boardHeaderClassName: cn(
|
||||
"flex flex-wrap items-start justify-between gap-3",
|
||||
useCompactCanvasChrome ? "px-4 py-2.5 sm:px-4" : "px-4 py-3.5 sm:px-5",
|
||||
embedded
|
||||
? cn(
|
||||
"sticky top-0 z-20 border-b border-slate-200",
|
||||
useCompactCanvasChrome ? "bg-white" : "bg-slate-50",
|
||||
)
|
||||
: cn(
|
||||
"border-b border-slate-200",
|
||||
useCompactCanvasChrome ? "bg-white" : "bg-slate-50",
|
||||
),
|
||||
),
|
||||
boardShellClassName: cn(
|
||||
embedded
|
||||
? "pointer-events-auto flex h-full min-h-0 flex-1 flex-col overflow-hidden rounded-[24px] border border-slate-200 bg-white shadow-sm shadow-slate-950/5"
|
||||
: "overflow-hidden rounded-[26px] border border-slate-200 bg-white shadow-[0_18px_64px_-42px_rgba(15,23,42,0.24)]",
|
||||
embedded ? "mx-0 mt-0" : "mx-3 mt-2",
|
||||
className,
|
||||
),
|
||||
canvasStageHeight:
|
||||
embedded && !detailVisible
|
||||
? "clamp(560px, 76vh, 980px)"
|
||||
: TEAM_WORKSPACE_CANVAS_STAGE_HEIGHT,
|
||||
detailCardClassName: cn(
|
||||
embedded
|
||||
? "rounded-[20px] border border-slate-200 bg-white p-4"
|
||||
: "rounded-[22px] border p-4 shadow-sm shadow-slate-950/5",
|
||||
!embedded &&
|
||||
(selectedSessionVisible
|
||||
? selectedSessionStatusCardClassName
|
||||
: "border-slate-200 bg-white"),
|
||||
),
|
||||
inlineDetailSectionClassName:
|
||||
"mt-3 rounded-[18px] border border-slate-200 bg-slate-50 p-3",
|
||||
inlineTimelineEntryClassName:
|
||||
"rounded-[14px] border border-slate-200 bg-white p-3",
|
||||
inlineTimelineFeedClassName:
|
||||
"mt-3 rounded-[16px] border border-slate-200 bg-white p-3",
|
||||
railCardClassName: embedded
|
||||
? cn(
|
||||
"pointer-events-auto",
|
||||
useCompactCanvasChrome ? "space-y-3" : "space-y-4",
|
||||
)
|
||||
: "rounded-[22px] border border-slate-200 bg-slate-50 p-3.5 shadow-sm shadow-slate-950/5",
|
||||
};
|
||||
}
|
||||
+386
@@ -0,0 +1,386 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
buildTeamWorkspaceBoardCanvasSectionProps,
|
||||
buildTeamWorkspaceBoardEmptyShellProps,
|
||||
buildTeamWorkspaceBoardHeaderProps,
|
||||
buildTeamWorkspaceBoardShellProps,
|
||||
} from "./teamWorkspaceBoardPropBuilders";
|
||||
|
||||
describe("teamWorkspaceBoardPropBuilders", () => {
|
||||
it("应按嵌入态规则组装 empty shell 与 shell props", () => {
|
||||
const onExpand = vi.fn();
|
||||
const onToggleDetail = vi.fn();
|
||||
const onReturnToParentSession = vi.fn();
|
||||
const resolveStatusMeta = vi.fn().mockReturnValue({
|
||||
badgeClassName: "border-sky-200 bg-sky-50 text-sky-700",
|
||||
});
|
||||
|
||||
const emptyShellProps = buildTeamWorkspaceBoardEmptyShellProps({
|
||||
className: "custom-shell",
|
||||
embedded: true,
|
||||
hasRuntimeFormation: true,
|
||||
onExpand,
|
||||
runtimeFormationDisplay: {
|
||||
blueprintRoleCards: [],
|
||||
emptyDetail: "系统正在准备分工。",
|
||||
hint: "正在准备分工",
|
||||
hasRuntimeFormation: true,
|
||||
memberCards: [],
|
||||
noticeText: "系统正在准备当前任务分工。",
|
||||
panelDescription: "当前任务分工正在生成。",
|
||||
panelHeadline: "任务分工准备中",
|
||||
panelLabel: "编辑 Team",
|
||||
panelStatusLabel: "准备中",
|
||||
panelStatusBadgeClassName: "border-sky-200 bg-sky-50 text-sky-700",
|
||||
panelTitle: "当前任务分工",
|
||||
referenceLabel: null,
|
||||
summaryBadges: [],
|
||||
},
|
||||
selectedTeamPlanDisplay: {
|
||||
hasSelectedTeamPlan: true,
|
||||
label: "编辑 Team",
|
||||
roleCards: [],
|
||||
summary: "先拆三段,再并行推进。",
|
||||
summaryBadges: [],
|
||||
},
|
||||
});
|
||||
|
||||
const headerProps = buildTeamWorkspaceBoardHeaderProps({
|
||||
boardChromeDisplay: {
|
||||
boardHeadline: "主任务总览",
|
||||
boardHint: "主线程会持续汇总子任务进展。",
|
||||
compactBoardHeadline: "任务进行中 · 3 项处理中",
|
||||
compactToolbarChips: [],
|
||||
statusSummaryBadges: [],
|
||||
},
|
||||
createdFromTurnId: "turn-42",
|
||||
detailToggleLabel: "查看细节",
|
||||
detailVisible: false,
|
||||
isChildSession: true,
|
||||
isEmptyShellState: false,
|
||||
onReturnToParentSession,
|
||||
onToggleDetail,
|
||||
resolveStatusMeta,
|
||||
runtimeFormationStatusLabel: "准备中",
|
||||
totalTeamSessions: 3,
|
||||
useCompactCanvasChrome: true,
|
||||
});
|
||||
|
||||
const shellProps = buildTeamWorkspaceBoardShellProps({
|
||||
boardBodyClassName: "board-body",
|
||||
boardHeaderClassName: "board-header",
|
||||
boardShellClassName: "board-shell",
|
||||
canvasSectionProps: {
|
||||
canvasStageProps: {
|
||||
canvasBoundsHeight: 720,
|
||||
canvasBoundsWidth: 1080,
|
||||
canvasStageHeight: "720px",
|
||||
canvasStageHint: "拖动查看任务进行时",
|
||||
expandedSessionId: "child-1",
|
||||
isCanvasPanModifierActive: false,
|
||||
laneLayouts: {},
|
||||
lanes: [],
|
||||
onCanvasWheel: vi.fn(),
|
||||
onSelectLane: vi.fn(),
|
||||
onStartCanvasLaneDrag: vi.fn(),
|
||||
onStartCanvasLaneResize: vi.fn(),
|
||||
onStartCanvasPan: vi.fn(),
|
||||
selectedInlineDetail: null,
|
||||
selectedSessionId: "child-1",
|
||||
viewport: { x: 0, y: 0, zoom: 1 },
|
||||
viewportRef: { current: null },
|
||||
},
|
||||
canvasToolbarProps: {
|
||||
laneCount: 0,
|
||||
onAutoArrangeCanvas: vi.fn(),
|
||||
onFitCanvasView: vi.fn(),
|
||||
onResetCanvasView: vi.fn(),
|
||||
onZoomIn: vi.fn(),
|
||||
onZoomOut: vi.fn(),
|
||||
zoom: 1,
|
||||
},
|
||||
overviewChromeProps: {
|
||||
boardChromeDisplay: {
|
||||
boardHeadline: "主任务总览",
|
||||
boardHint: "主线程会持续汇总子任务进展。",
|
||||
compactBoardHeadline: "任务进行中 · 3 项处理中",
|
||||
compactToolbarChips: [],
|
||||
statusSummaryBadges: [],
|
||||
},
|
||||
canCloseCompletedTeamSessions: false,
|
||||
canWaitAnyActiveTeamSession: false,
|
||||
completedCount: 0,
|
||||
formatUpdatedAt: () => "刚刚",
|
||||
memberCanvasSubtitle: "子任务会持续更新进展。",
|
||||
memberCanvasTitle: "任务视图",
|
||||
onAutoArrangeCanvas: vi.fn(),
|
||||
onCloseCompletedTeamSessions: vi.fn(),
|
||||
onFitCanvasView: vi.fn(),
|
||||
onSelectTeamOperationEntry: vi.fn(),
|
||||
onWaitAnyActiveTeamSessions: vi.fn(),
|
||||
onZoomIn: vi.fn(),
|
||||
onZoomOut: vi.fn(),
|
||||
pendingTeamAction: null,
|
||||
resolveStatusMeta,
|
||||
selectedSession: null,
|
||||
teamOperationEntries: [],
|
||||
waitableCount: 0,
|
||||
},
|
||||
fallbackDetailProps: null,
|
||||
railCardClassName: "rail-card",
|
||||
useCompactCanvasChrome: true,
|
||||
},
|
||||
embedded: true,
|
||||
headerProps,
|
||||
});
|
||||
|
||||
expect(emptyShellProps).toMatchObject({
|
||||
className: "custom-shell",
|
||||
embedded: true,
|
||||
hasRuntimeFormation: true,
|
||||
});
|
||||
expect(emptyShellProps.onExpand).toBe(onExpand);
|
||||
expect(headerProps).toMatchObject({
|
||||
createdFromTurnId: "turn-42",
|
||||
detailToggleLabel: "查看细节",
|
||||
runtimeFormationStatusLabel: "准备中",
|
||||
totalTeamSessions: 3,
|
||||
});
|
||||
expect(headerProps.onToggleDetail).toBe(onToggleDetail);
|
||||
expect(headerProps.onReturnToParentSession).toBe(onReturnToParentSession);
|
||||
expect(shellProps).toMatchObject({
|
||||
boardBodyClassName: "board-body",
|
||||
boardHeaderClassName: "board-header",
|
||||
boardShellClassName: "board-shell",
|
||||
embedded: true,
|
||||
style: { maxHeight: "inherit" },
|
||||
});
|
||||
});
|
||||
|
||||
it("应在尚无 runtime sessions 时为 canvas section 透传 fallback detail 与 overview props", () => {
|
||||
const onStartCanvasLaneDrag = vi.fn();
|
||||
const onStartCanvasLaneResize = vi.fn();
|
||||
const onSelectTeamOperationEntry = vi.fn();
|
||||
const resolveStatusMeta = vi.fn().mockReturnValue({
|
||||
badgeClassName: "border-amber-200 bg-amber-50 text-amber-700",
|
||||
});
|
||||
|
||||
const props = buildTeamWorkspaceBoardCanvasSectionProps({
|
||||
boardChromeDisplay: {
|
||||
boardHeadline: "任务进行中 · 1 项处理中 / 1 项稍后开始",
|
||||
boardHint: "当前主任务会继续汇总进展。",
|
||||
compactBoardHeadline: "任务进行中 · 1 项处理中 / 1 项稍后开始",
|
||||
compactToolbarChips: [
|
||||
{
|
||||
key: "running",
|
||||
text: "运行中 1",
|
||||
tone: "status",
|
||||
status: "running",
|
||||
},
|
||||
],
|
||||
statusSummaryBadges: [],
|
||||
},
|
||||
canvasBoundsHeight: 680,
|
||||
canvasBoundsWidth: 960,
|
||||
canvasStageHeight: "680px",
|
||||
canvasStageHint: "正在同步任务进行时",
|
||||
canvasViewportRef: { current: null },
|
||||
canvasZoom: 1.2,
|
||||
canCloseCompletedTeamSessions: true,
|
||||
canWaitAnyActiveTeamSession: true,
|
||||
completedCount: 1,
|
||||
detailCardClassName: "detail-card",
|
||||
detailVisible: true,
|
||||
expandedSessionId: "child-2",
|
||||
formatUpdatedAt: () => "刚刚",
|
||||
hasRuntimeSessions: false,
|
||||
isCanvasPanModifierActive: true,
|
||||
laneLayouts: {},
|
||||
lanes: [
|
||||
{
|
||||
persistKey: "lane-alpha",
|
||||
} as never,
|
||||
],
|
||||
memberCanvasSubtitle: "子任务会持续更新任务进展。",
|
||||
memberCanvasTitle: "任务视图",
|
||||
onAutoArrangeCanvas: vi.fn(),
|
||||
onCanvasWheel: vi.fn(),
|
||||
onCloseCompletedTeamSessions: vi.fn(),
|
||||
onFitCanvasView: vi.fn(),
|
||||
onResetCanvasView: vi.fn(),
|
||||
onSelectCanvasLane: vi.fn(),
|
||||
onSelectTeamOperationEntry,
|
||||
onStartCanvasLaneDrag,
|
||||
onStartCanvasLaneResize,
|
||||
onStartCanvasPan: vi.fn(),
|
||||
onWaitAnyActiveTeamSessions: vi.fn(),
|
||||
onZoomIn: vi.fn(),
|
||||
onZoomOut: vi.fn(),
|
||||
pendingTeamAction: "wait_any",
|
||||
railCardClassName: "rail-card",
|
||||
resolveStatusMeta,
|
||||
runtimeFormationDisplay: {
|
||||
blueprintRoleCards: [],
|
||||
emptyDetail: "系统正在准备分工。",
|
||||
hint: "正在准备分工",
|
||||
hasRuntimeFormation: true,
|
||||
memberCards: [],
|
||||
noticeText: "系统正在准备当前任务分工。",
|
||||
panelDescription: "当前任务分工正在生成。",
|
||||
panelHeadline: "任务分工准备中",
|
||||
panelLabel: "资料整理 Team",
|
||||
panelStatusLabel: "准备中",
|
||||
panelStatusBadgeClassName: "border-sky-200 bg-sky-50 text-sky-700",
|
||||
panelTitle: "当前任务分工",
|
||||
referenceLabel: null,
|
||||
summaryBadges: [],
|
||||
},
|
||||
selectedInlineDetail: "inline-detail",
|
||||
selectedSession: {
|
||||
isCurrent: false,
|
||||
updatedAt: 1710000000,
|
||||
},
|
||||
selectedSessionId: "child-2",
|
||||
selectedTeamPlanDisplay: {
|
||||
hasSelectedTeamPlan: true,
|
||||
label: "资料整理 Team",
|
||||
roleCards: [],
|
||||
summary: "先拆任务,再并行推进。",
|
||||
summaryBadges: [],
|
||||
},
|
||||
teamOperationEntries: [
|
||||
{
|
||||
badgeClassName: "border-sky-200 bg-sky-50 text-sky-700",
|
||||
detail: "资料整理返回了新结果",
|
||||
id: "wait-1",
|
||||
title: "收到结果",
|
||||
updatedAt: 1710000000,
|
||||
targetSessionId: "child-2",
|
||||
},
|
||||
],
|
||||
useCompactCanvasChrome: true,
|
||||
viewport: { x: 12, y: 24, zoom: 1.2 },
|
||||
waitableCount: 2,
|
||||
});
|
||||
|
||||
expect(props.fallbackDetailProps).toMatchObject({
|
||||
detailCardClassName: "detail-card",
|
||||
detailVisible: true,
|
||||
});
|
||||
expect(props.overviewChromeProps).toMatchObject({
|
||||
canCloseCompletedTeamSessions: true,
|
||||
canWaitAnyActiveTeamSession: true,
|
||||
completedCount: 1,
|
||||
memberCanvasTitle: "任务视图",
|
||||
pendingTeamAction: "wait_any",
|
||||
waitableCount: 2,
|
||||
});
|
||||
expect(props.canvasToolbarProps.zoom).toBe(1.2);
|
||||
expect(props.canvasStageProps.selectedInlineDetail).toBe("inline-detail");
|
||||
expect(props.canvasStageProps.viewport).toEqual({ x: 12, y: 24, zoom: 1.2 });
|
||||
|
||||
props.canvasStageProps.onStartCanvasLaneDrag?.(
|
||||
{ persistKey: "lane-alpha" } as never,
|
||||
{ clientX: 10, clientY: 20 } as never,
|
||||
);
|
||||
props.canvasStageProps.onStartCanvasLaneResize?.(
|
||||
{ persistKey: "lane-alpha" } as never,
|
||||
"right" as never,
|
||||
{ clientX: 40, clientY: 60 } as never,
|
||||
);
|
||||
|
||||
expect(onStartCanvasLaneDrag).toHaveBeenCalledWith(
|
||||
"lane-alpha",
|
||||
expect.anything(),
|
||||
);
|
||||
expect(onStartCanvasLaneResize).toHaveBeenCalledWith(
|
||||
"lane-alpha",
|
||||
"right",
|
||||
expect.anything(),
|
||||
);
|
||||
expect(props.overviewChromeProps.onSelectTeamOperationEntry).toBe(
|
||||
onSelectTeamOperationEntry,
|
||||
);
|
||||
expect(props.railCardClassName).toBe("rail-card");
|
||||
expect(props.useCompactCanvasChrome).toBe(true);
|
||||
});
|
||||
|
||||
it("应在已有 runtime sessions 时关闭 fallback detail", () => {
|
||||
const props = buildTeamWorkspaceBoardCanvasSectionProps({
|
||||
boardChromeDisplay: {
|
||||
boardHeadline: "任务进行中",
|
||||
boardHint: "正在持续汇总任务进展。",
|
||||
compactBoardHeadline: "任务进行中",
|
||||
compactToolbarChips: [],
|
||||
statusSummaryBadges: [],
|
||||
},
|
||||
canvasBoundsHeight: 680,
|
||||
canvasBoundsWidth: 960,
|
||||
canvasStageHeight: "680px",
|
||||
canvasStageHint: "正在同步任务进行时",
|
||||
canvasViewportRef: { current: null },
|
||||
canvasZoom: 1,
|
||||
canCloseCompletedTeamSessions: false,
|
||||
canWaitAnyActiveTeamSession: false,
|
||||
completedCount: 0,
|
||||
detailCardClassName: "detail-card",
|
||||
detailVisible: false,
|
||||
expandedSessionId: null,
|
||||
formatUpdatedAt: () => "刚刚",
|
||||
hasRuntimeSessions: true,
|
||||
isCanvasPanModifierActive: false,
|
||||
laneLayouts: {},
|
||||
lanes: [],
|
||||
memberCanvasSubtitle: "已有任务接入。",
|
||||
memberCanvasTitle: "任务视图",
|
||||
onAutoArrangeCanvas: vi.fn(),
|
||||
onCanvasWheel: vi.fn(),
|
||||
onCloseCompletedTeamSessions: vi.fn(),
|
||||
onFitCanvasView: vi.fn(),
|
||||
onResetCanvasView: vi.fn(),
|
||||
onSelectCanvasLane: vi.fn(),
|
||||
onSelectTeamOperationEntry: vi.fn(),
|
||||
onStartCanvasLaneDrag: vi.fn(),
|
||||
onStartCanvasLaneResize: vi.fn(),
|
||||
onStartCanvasPan: vi.fn(),
|
||||
onWaitAnyActiveTeamSessions: vi.fn(),
|
||||
onZoomIn: vi.fn(),
|
||||
onZoomOut: vi.fn(),
|
||||
pendingTeamAction: null,
|
||||
railCardClassName: "rail-card",
|
||||
resolveStatusMeta: vi.fn(),
|
||||
runtimeFormationDisplay: {
|
||||
blueprintRoleCards: [],
|
||||
emptyDetail: "系统正在准备分工。",
|
||||
hint: "正在准备分工",
|
||||
hasRuntimeFormation: true,
|
||||
memberCards: [],
|
||||
noticeText: "系统正在准备当前任务分工。",
|
||||
panelDescription: "当前任务分工正在生成。",
|
||||
panelHeadline: "任务分工准备中",
|
||||
panelLabel: "资料整理 Team",
|
||||
panelStatusLabel: "准备中",
|
||||
panelStatusBadgeClassName: "border-sky-200 bg-sky-50 text-sky-700",
|
||||
panelTitle: "当前任务分工",
|
||||
referenceLabel: null,
|
||||
summaryBadges: [],
|
||||
},
|
||||
selectedInlineDetail: null,
|
||||
selectedSession: null,
|
||||
selectedSessionId: null,
|
||||
selectedTeamPlanDisplay: {
|
||||
hasSelectedTeamPlan: true,
|
||||
label: "资料整理 Team",
|
||||
roleCards: [],
|
||||
summary: "先拆任务,再并行推进。",
|
||||
summaryBadges: [],
|
||||
},
|
||||
teamOperationEntries: [],
|
||||
useCompactCanvasChrome: true,
|
||||
viewport: { x: 0, y: 0, zoom: 1 },
|
||||
waitableCount: 0,
|
||||
});
|
||||
|
||||
expect(props.fallbackDetailProps).toBeNull();
|
||||
});
|
||||
});
|
||||
+139
-111
@@ -30,9 +30,30 @@ interface BuildTeamWorkspaceBoardEmptyShellPropsParams {
|
||||
|
||||
interface BuildTeamWorkspaceBoardShellPropsParams {
|
||||
boardBodyClassName: TeamWorkspaceBoardShellProps["boardBodyClassName"];
|
||||
boardChromeDisplay: TeamWorkspaceBoardHeaderProps["boardChromeDisplay"];
|
||||
boardHeaderClassName: TeamWorkspaceBoardShellProps["boardHeaderClassName"];
|
||||
boardShellClassName: TeamWorkspaceBoardShellProps["boardShellClassName"];
|
||||
canvasSectionProps: TeamWorkspaceBoardShellProps["canvasSectionProps"];
|
||||
embedded: boolean;
|
||||
headerProps: TeamWorkspaceBoardShellProps["headerProps"];
|
||||
}
|
||||
|
||||
interface BuildTeamWorkspaceBoardHeaderPropsParams {
|
||||
boardChromeDisplay: TeamWorkspaceBoardHeaderProps["boardChromeDisplay"];
|
||||
createdFromTurnId?: TeamWorkspaceBoardHeaderProps["createdFromTurnId"];
|
||||
detailToggleLabel: TeamWorkspaceBoardHeaderProps["detailToggleLabel"];
|
||||
detailVisible: TeamWorkspaceBoardHeaderProps["detailVisible"];
|
||||
isChildSession: TeamWorkspaceBoardHeaderProps["isChildSession"];
|
||||
isEmptyShellState: TeamWorkspaceBoardHeaderProps["isEmptyShellState"];
|
||||
onReturnToParentSession?: TeamWorkspaceBoardHeaderProps["onReturnToParentSession"];
|
||||
onToggleDetail: TeamWorkspaceBoardHeaderProps["onToggleDetail"];
|
||||
resolveStatusMeta: TeamWorkspaceBoardHeaderProps["resolveStatusMeta"];
|
||||
runtimeFormationStatusLabel?: TeamWorkspaceBoardHeaderProps["runtimeFormationStatusLabel"];
|
||||
totalTeamSessions: TeamWorkspaceBoardHeaderProps["totalTeamSessions"];
|
||||
useCompactCanvasChrome: TeamWorkspaceBoardHeaderProps["useCompactCanvasChrome"];
|
||||
}
|
||||
|
||||
interface BuildTeamWorkspaceBoardCanvasSectionPropsParams {
|
||||
boardChromeDisplay: TeamWorkspaceBoardOverviewChromeProps["boardChromeDisplay"];
|
||||
canvasBoundsHeight: TeamWorkspaceBoardCanvasStageProps["canvasBoundsHeight"];
|
||||
canvasBoundsWidth: TeamWorkspaceBoardCanvasStageProps["canvasBoundsWidth"];
|
||||
canvasStageHeight: TeamWorkspaceBoardCanvasStageProps["canvasStageHeight"];
|
||||
@@ -44,18 +65,13 @@ interface BuildTeamWorkspaceBoardShellPropsParams {
|
||||
canWaitAnyActiveTeamSession:
|
||||
TeamWorkspaceBoardOverviewChromeProps["canWaitAnyActiveTeamSession"];
|
||||
completedCount: TeamWorkspaceBoardOverviewChromeProps["completedCount"];
|
||||
createdFromTurnId?: TeamWorkspaceBoardHeaderProps["createdFromTurnId"];
|
||||
detailCardClassName: TeamWorkspaceBoardFallbackDetailProps["detailCardClassName"];
|
||||
detailToggleLabel: TeamWorkspaceBoardHeaderProps["detailToggleLabel"];
|
||||
detailVisible: TeamWorkspaceBoardHeaderProps["detailVisible"];
|
||||
embedded: boolean;
|
||||
expandedSessionId?: TeamWorkspaceBoardCanvasStageProps["expandedSessionId"];
|
||||
formatUpdatedAt: TeamWorkspaceBoardOverviewChromeProps["formatUpdatedAt"];
|
||||
hasRealTeamGraph: boolean;
|
||||
hasRuntimeSessions: boolean;
|
||||
isCanvasPanModifierActive:
|
||||
TeamWorkspaceBoardCanvasStageProps["isCanvasPanModifierActive"];
|
||||
isChildSession: TeamWorkspaceBoardHeaderProps["isChildSession"];
|
||||
isEmptyShellState: TeamWorkspaceBoardHeaderProps["isEmptyShellState"];
|
||||
laneLayouts: TeamWorkspaceBoardCanvasStageProps["laneLayouts"];
|
||||
lanes: TeamWorkspaceBoardCanvasStageProps["lanes"];
|
||||
memberCanvasSubtitle: TeamWorkspaceBoardOverviewChromeProps["memberCanvasSubtitle"];
|
||||
@@ -66,7 +82,6 @@ interface BuildTeamWorkspaceBoardShellPropsParams {
|
||||
onCloseCompletedTeamSessions:
|
||||
TeamWorkspaceBoardOverviewChromeProps["onCloseCompletedTeamSessions"];
|
||||
onFitCanvasView: TeamWorkspaceBoardCanvasToolbarProps["onFitCanvasView"];
|
||||
onReturnToParentSession?: TeamWorkspaceBoardHeaderProps["onReturnToParentSession"];
|
||||
onResetCanvasView: TeamWorkspaceBoardCanvasToolbarProps["onResetCanvasView"];
|
||||
onSelectCanvasLane: TeamWorkspaceBoardCanvasStageProps["onSelectLane"];
|
||||
onSelectTeamOperationEntry:
|
||||
@@ -85,15 +100,13 @@ interface BuildTeamWorkspaceBoardShellPropsParams {
|
||||
>[2],
|
||||
) => void;
|
||||
onStartCanvasPan: TeamWorkspaceBoardCanvasStageProps["onStartCanvasPan"];
|
||||
onToggleDetail: TeamWorkspaceBoardHeaderProps["onToggleDetail"];
|
||||
onWaitAnyActiveTeamSessions:
|
||||
TeamWorkspaceBoardOverviewChromeProps["onWaitAnyActiveTeamSessions"];
|
||||
onZoomIn: TeamWorkspaceBoardCanvasToolbarProps["onZoomIn"];
|
||||
onZoomOut: TeamWorkspaceBoardCanvasToolbarProps["onZoomOut"];
|
||||
pendingTeamAction: TeamWorkspaceBoardOverviewChromeProps["pendingTeamAction"];
|
||||
railCardClassName: TeamWorkspaceBoardCanvasSectionProps["railCardClassName"];
|
||||
resolveStatusMeta: TeamWorkspaceBoardHeaderProps["resolveStatusMeta"];
|
||||
runtimeFormationStatusLabel?: TeamWorkspaceBoardHeaderProps["runtimeFormationStatusLabel"];
|
||||
resolveStatusMeta: TeamWorkspaceBoardOverviewChromeProps["resolveStatusMeta"];
|
||||
runtimeFormationDisplay:
|
||||
TeamWorkspaceBoardFallbackDetailProps["runtimeFormationDisplay"];
|
||||
selectedInlineDetail?: TeamWorkspaceBoardCanvasStageProps["selectedInlineDetail"];
|
||||
@@ -103,8 +116,7 @@ interface BuildTeamWorkspaceBoardShellPropsParams {
|
||||
TeamWorkspaceBoardFallbackDetailProps["selectedTeamPlanDisplay"];
|
||||
teamOperationEntries:
|
||||
TeamWorkspaceBoardOverviewChromeProps["teamOperationEntries"];
|
||||
totalTeamSessions: TeamWorkspaceBoardHeaderProps["totalTeamSessions"];
|
||||
useCompactCanvasChrome: TeamWorkspaceBoardHeaderProps["useCompactCanvasChrome"];
|
||||
useCompactCanvasChrome: TeamWorkspaceBoardCanvasSectionProps["useCompactCanvasChrome"];
|
||||
viewport: TeamWorkspaceBoardCanvasStageProps["viewport"];
|
||||
waitableCount: TeamWorkspaceBoardOverviewChromeProps["waitableCount"];
|
||||
}
|
||||
@@ -127,31 +139,53 @@ export function buildTeamWorkspaceBoardEmptyShellProps({
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTeamWorkspaceBoardShellProps({
|
||||
boardBodyClassName,
|
||||
export function buildTeamWorkspaceBoardHeaderProps({
|
||||
boardChromeDisplay,
|
||||
boardHeaderClassName,
|
||||
boardShellClassName,
|
||||
createdFromTurnId,
|
||||
detailToggleLabel,
|
||||
detailVisible,
|
||||
isChildSession,
|
||||
isEmptyShellState,
|
||||
onReturnToParentSession,
|
||||
onToggleDetail,
|
||||
resolveStatusMeta,
|
||||
runtimeFormationStatusLabel,
|
||||
totalTeamSessions,
|
||||
useCompactCanvasChrome,
|
||||
}: BuildTeamWorkspaceBoardHeaderPropsParams): TeamWorkspaceBoardHeaderProps {
|
||||
return {
|
||||
boardChromeDisplay,
|
||||
createdFromTurnId,
|
||||
detailToggleLabel,
|
||||
detailVisible,
|
||||
isChildSession,
|
||||
isEmptyShellState,
|
||||
onReturnToParentSession,
|
||||
onToggleDetail,
|
||||
resolveStatusMeta,
|
||||
runtimeFormationStatusLabel,
|
||||
totalTeamSessions,
|
||||
useCompactCanvasChrome,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTeamWorkspaceBoardCanvasSectionProps({
|
||||
boardChromeDisplay,
|
||||
canCloseCompletedTeamSessions,
|
||||
canWaitAnyActiveTeamSession,
|
||||
completedCount,
|
||||
detailCardClassName,
|
||||
detailVisible,
|
||||
expandedSessionId,
|
||||
formatUpdatedAt,
|
||||
hasRuntimeSessions,
|
||||
canvasBoundsHeight,
|
||||
canvasBoundsWidth,
|
||||
canvasStageHeight,
|
||||
canvasStageHint,
|
||||
canvasViewportRef,
|
||||
canvasZoom,
|
||||
canCloseCompletedTeamSessions,
|
||||
canWaitAnyActiveTeamSession,
|
||||
completedCount,
|
||||
createdFromTurnId,
|
||||
detailCardClassName,
|
||||
detailToggleLabel,
|
||||
detailVisible,
|
||||
embedded,
|
||||
expandedSessionId,
|
||||
formatUpdatedAt,
|
||||
hasRealTeamGraph,
|
||||
isCanvasPanModifierActive,
|
||||
isChildSession,
|
||||
isEmptyShellState,
|
||||
laneLayouts,
|
||||
lanes,
|
||||
memberCanvasSubtitle,
|
||||
@@ -160,114 +194,108 @@ export function buildTeamWorkspaceBoardShellProps({
|
||||
onCanvasWheel,
|
||||
onCloseCompletedTeamSessions,
|
||||
onFitCanvasView,
|
||||
onReturnToParentSession,
|
||||
onResetCanvasView,
|
||||
onSelectCanvasLane,
|
||||
onSelectTeamOperationEntry,
|
||||
onStartCanvasLaneDrag,
|
||||
onStartCanvasLaneResize,
|
||||
onStartCanvasPan,
|
||||
onToggleDetail,
|
||||
onWaitAnyActiveTeamSessions,
|
||||
onZoomIn,
|
||||
onZoomOut,
|
||||
pendingTeamAction,
|
||||
railCardClassName,
|
||||
resolveStatusMeta,
|
||||
runtimeFormationStatusLabel,
|
||||
runtimeFormationDisplay,
|
||||
selectedInlineDetail,
|
||||
selectedSession,
|
||||
selectedSessionId,
|
||||
selectedTeamPlanDisplay,
|
||||
teamOperationEntries,
|
||||
totalTeamSessions,
|
||||
useCompactCanvasChrome,
|
||||
viewport,
|
||||
waitableCount,
|
||||
}: BuildTeamWorkspaceBoardCanvasSectionPropsParams): TeamWorkspaceBoardCanvasSectionProps {
|
||||
return {
|
||||
canvasStageProps: {
|
||||
canvasBoundsHeight,
|
||||
canvasBoundsWidth,
|
||||
canvasStageHeight,
|
||||
canvasStageHint,
|
||||
expandedSessionId,
|
||||
isCanvasPanModifierActive,
|
||||
laneLayouts,
|
||||
lanes,
|
||||
onCanvasWheel,
|
||||
onSelectLane: onSelectCanvasLane,
|
||||
onStartCanvasLaneDrag: (lane, event) =>
|
||||
onStartCanvasLaneDrag(lane.persistKey, event),
|
||||
onStartCanvasLaneResize: (lane, direction, event) =>
|
||||
onStartCanvasLaneResize(lane.persistKey, direction, event),
|
||||
onStartCanvasPan,
|
||||
selectedInlineDetail,
|
||||
selectedSessionId,
|
||||
viewport,
|
||||
viewportRef: canvasViewportRef,
|
||||
},
|
||||
canvasToolbarProps: {
|
||||
laneCount: lanes.length,
|
||||
onAutoArrangeCanvas,
|
||||
onFitCanvasView,
|
||||
onResetCanvasView,
|
||||
onZoomIn,
|
||||
onZoomOut,
|
||||
zoom: canvasZoom,
|
||||
},
|
||||
fallbackDetailProps: hasRuntimeSessions
|
||||
? null
|
||||
: {
|
||||
detailCardClassName,
|
||||
detailVisible,
|
||||
runtimeFormationDisplay,
|
||||
selectedTeamPlanDisplay,
|
||||
},
|
||||
overviewChromeProps: {
|
||||
boardChromeDisplay,
|
||||
canCloseCompletedTeamSessions,
|
||||
canWaitAnyActiveTeamSession,
|
||||
completedCount,
|
||||
formatUpdatedAt,
|
||||
memberCanvasSubtitle,
|
||||
memberCanvasTitle,
|
||||
onAutoArrangeCanvas,
|
||||
onCloseCompletedTeamSessions,
|
||||
onFitCanvasView,
|
||||
onSelectTeamOperationEntry,
|
||||
onWaitAnyActiveTeamSessions,
|
||||
onZoomIn,
|
||||
onZoomOut,
|
||||
pendingTeamAction,
|
||||
resolveStatusMeta,
|
||||
selectedSession,
|
||||
teamOperationEntries,
|
||||
waitableCount,
|
||||
},
|
||||
railCardClassName,
|
||||
useCompactCanvasChrome,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTeamWorkspaceBoardShellProps({
|
||||
boardBodyClassName,
|
||||
boardHeaderClassName,
|
||||
boardShellClassName,
|
||||
canvasSectionProps,
|
||||
embedded,
|
||||
headerProps,
|
||||
}: BuildTeamWorkspaceBoardShellPropsParams): TeamWorkspaceBoardShellProps {
|
||||
return {
|
||||
boardBodyClassName,
|
||||
boardHeaderClassName,
|
||||
boardShellClassName,
|
||||
canvasSectionProps: {
|
||||
canvasStageProps: {
|
||||
canvasBoundsHeight,
|
||||
canvasBoundsWidth,
|
||||
canvasStageHeight,
|
||||
canvasStageHint,
|
||||
expandedSessionId,
|
||||
isCanvasPanModifierActive,
|
||||
laneLayouts,
|
||||
lanes,
|
||||
onCanvasWheel,
|
||||
onSelectLane: onSelectCanvasLane,
|
||||
onStartCanvasLaneDrag: (lane, event) =>
|
||||
onStartCanvasLaneDrag(lane.persistKey, event),
|
||||
onStartCanvasLaneResize: (lane, direction, event) =>
|
||||
onStartCanvasLaneResize(lane.persistKey, direction, event),
|
||||
onStartCanvasPan,
|
||||
selectedInlineDetail,
|
||||
selectedSessionId,
|
||||
viewport,
|
||||
viewportRef: canvasViewportRef,
|
||||
},
|
||||
canvasToolbarProps: {
|
||||
laneCount: lanes.length,
|
||||
onAutoArrangeCanvas,
|
||||
onFitCanvasView,
|
||||
onResetCanvasView,
|
||||
onZoomIn,
|
||||
onZoomOut,
|
||||
zoom: canvasZoom,
|
||||
},
|
||||
fallbackDetailProps: hasRealTeamGraph
|
||||
? null
|
||||
: {
|
||||
detailCardClassName,
|
||||
detailVisible,
|
||||
runtimeFormationDisplay,
|
||||
selectedTeamPlanDisplay,
|
||||
},
|
||||
overviewChromeProps: {
|
||||
boardChromeDisplay,
|
||||
canCloseCompletedTeamSessions,
|
||||
canWaitAnyActiveTeamSession,
|
||||
completedCount,
|
||||
formatUpdatedAt,
|
||||
memberCanvasSubtitle,
|
||||
memberCanvasTitle,
|
||||
onAutoArrangeCanvas,
|
||||
onCloseCompletedTeamSessions,
|
||||
onFitCanvasView,
|
||||
onSelectTeamOperationEntry,
|
||||
onWaitAnyActiveTeamSessions,
|
||||
onZoomIn,
|
||||
onZoomOut,
|
||||
pendingTeamAction,
|
||||
resolveStatusMeta,
|
||||
selectedSession,
|
||||
teamOperationEntries,
|
||||
waitableCount,
|
||||
},
|
||||
railCardClassName,
|
||||
useCompactCanvasChrome,
|
||||
},
|
||||
canvasSectionProps,
|
||||
embedded,
|
||||
headerProps: {
|
||||
boardChromeDisplay,
|
||||
createdFromTurnId,
|
||||
detailToggleLabel,
|
||||
detailVisible,
|
||||
isChildSession,
|
||||
isEmptyShellState,
|
||||
onReturnToParentSession,
|
||||
onToggleDetail,
|
||||
resolveStatusMeta,
|
||||
runtimeFormationStatusLabel,
|
||||
totalTeamSessions,
|
||||
useCompactCanvasChrome,
|
||||
},
|
||||
headerProps,
|
||||
style: embedded ? { maxHeight: "inherit" } : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import type { TeamSessionCard } from "../../utils/teamWorkspaceSessions";
|
||||
|
||||
export type TeamWorkspacePendingSessionAction =
|
||||
| "close"
|
||||
| "interrupt_send"
|
||||
| "resume"
|
||||
| "send"
|
||||
| "wait";
|
||||
|
||||
export type TeamWorkspacePendingTeamAction =
|
||||
| "close_completed"
|
||||
| "wait_any";
|
||||
|
||||
interface UseTeamWorkspaceBoardActionUiStateParams {
|
||||
selectedSession?: Pick<TeamSessionCard, "id"> | null;
|
||||
}
|
||||
|
||||
export function useTeamWorkspaceBoardActionUiState({
|
||||
selectedSession,
|
||||
}: UseTeamWorkspaceBoardActionUiStateParams) {
|
||||
const [pendingSessionAction, setPendingSessionAction] = useState<{
|
||||
sessionId: string;
|
||||
action: TeamWorkspacePendingSessionAction;
|
||||
} | null>(null);
|
||||
const [pendingTeamAction, setPendingTeamAction] =
|
||||
useState<TeamWorkspacePendingTeamAction | null>(null);
|
||||
const [sessionInputDraftById, setSessionInputDraftById] = useState<
|
||||
Record<string, string>
|
||||
>({});
|
||||
|
||||
const selectedActionPending = Boolean(
|
||||
selectedSession && pendingSessionAction?.sessionId === selectedSession.id,
|
||||
);
|
||||
const selectedSessionInputDraft = selectedSession
|
||||
? (sessionInputDraftById[selectedSession.id] ?? "")
|
||||
: "";
|
||||
const selectedSessionInputMessage = useMemo(
|
||||
() => selectedSessionInputDraft.trim(),
|
||||
[selectedSessionInputDraft],
|
||||
);
|
||||
|
||||
const handleSelectedSessionInputDraftChange = useCallback(
|
||||
(value: string) => {
|
||||
if (!selectedSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSessionInputDraftById((previous) => {
|
||||
if (previous[selectedSession.id] === value) {
|
||||
return previous;
|
||||
}
|
||||
return {
|
||||
...previous,
|
||||
[selectedSession.id]: value,
|
||||
};
|
||||
});
|
||||
},
|
||||
[selectedSession],
|
||||
);
|
||||
|
||||
const clearSelectedSessionInputDraft = useCallback((sessionId: string) => {
|
||||
setSessionInputDraftById((previous) => {
|
||||
if (!previous[sessionId]) {
|
||||
return previous;
|
||||
}
|
||||
return {
|
||||
...previous,
|
||||
[sessionId]: "",
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
clearSelectedSessionInputDraft,
|
||||
handleSelectedSessionInputDraftChange,
|
||||
pendingSessionAction,
|
||||
pendingTeamAction,
|
||||
selectedActionPending,
|
||||
selectedSessionInputDraft,
|
||||
selectedSessionInputMessage,
|
||||
setPendingSessionAction,
|
||||
setPendingTeamAction,
|
||||
};
|
||||
}
|
||||
+37
-63
@@ -1,16 +1,8 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { useCallback } from "react";
|
||||
import type { TeamSessionCard } from "../../utils/teamWorkspaceSessions";
|
||||
|
||||
export type TeamWorkspacePendingSessionAction =
|
||||
| "close"
|
||||
| "interrupt_send"
|
||||
| "resume"
|
||||
| "send"
|
||||
| "wait";
|
||||
|
||||
export type TeamWorkspacePendingTeamAction =
|
||||
| "close_completed"
|
||||
| "wait_any";
|
||||
import {
|
||||
useTeamWorkspaceBoardActionUiState,
|
||||
} from "./useTeamWorkspaceBoardActionUiState";
|
||||
|
||||
interface UseTeamWorkspaceBoardActionsParams {
|
||||
completedTeamSessionIds: string[];
|
||||
@@ -47,26 +39,19 @@ export function useTeamWorkspaceBoardActions({
|
||||
waitTimeoutMs = 30_000,
|
||||
waitableTeamSessionIds,
|
||||
}: UseTeamWorkspaceBoardActionsParams) {
|
||||
const [pendingSessionAction, setPendingSessionAction] = useState<{
|
||||
sessionId: string;
|
||||
action: TeamWorkspacePendingSessionAction;
|
||||
} | null>(null);
|
||||
const [pendingTeamAction, setPendingTeamAction] =
|
||||
useState<TeamWorkspacePendingTeamAction | null>(null);
|
||||
const [sessionInputDraftById, setSessionInputDraftById] = useState<
|
||||
Record<string, string>
|
||||
>({});
|
||||
|
||||
const selectedActionPending = Boolean(
|
||||
selectedSession && pendingSessionAction?.sessionId === selectedSession.id,
|
||||
);
|
||||
const selectedSessionInputDraft = selectedSession
|
||||
? (sessionInputDraftById[selectedSession.id] ?? "")
|
||||
: "";
|
||||
const selectedSessionInputMessage = useMemo(
|
||||
() => selectedSessionInputDraft.trim(),
|
||||
[selectedSessionInputDraft],
|
||||
);
|
||||
const {
|
||||
clearSelectedSessionInputDraft,
|
||||
handleSelectedSessionInputDraftChange,
|
||||
pendingSessionAction,
|
||||
pendingTeamAction,
|
||||
selectedActionPending,
|
||||
selectedSessionInputDraft,
|
||||
selectedSessionInputMessage,
|
||||
setPendingSessionAction,
|
||||
setPendingTeamAction,
|
||||
} = useTeamWorkspaceBoardActionUiState({
|
||||
selectedSession,
|
||||
});
|
||||
|
||||
const handleWaitAnyActiveTeamSessions = useCallback(async () => {
|
||||
if (!onWaitActiveTeamSessions || waitableTeamSessionIds.length <= 1) {
|
||||
@@ -79,7 +64,12 @@ export function useTeamWorkspaceBoardActions({
|
||||
} finally {
|
||||
setPendingTeamAction(null);
|
||||
}
|
||||
}, [onWaitActiveTeamSessions, waitTimeoutMs, waitableTeamSessionIds]);
|
||||
}, [
|
||||
onWaitActiveTeamSessions,
|
||||
setPendingTeamAction,
|
||||
waitTimeoutMs,
|
||||
waitableTeamSessionIds,
|
||||
]);
|
||||
|
||||
const handleCloseCompletedTeamSessions = useCallback(async () => {
|
||||
if (!onCloseCompletedTeamSessions || completedTeamSessionIds.length === 0) {
|
||||
@@ -92,7 +82,11 @@ export function useTeamWorkspaceBoardActions({
|
||||
} finally {
|
||||
setPendingTeamAction(null);
|
||||
}
|
||||
}, [completedTeamSessionIds, onCloseCompletedTeamSessions]);
|
||||
}, [
|
||||
completedTeamSessionIds,
|
||||
onCloseCompletedTeamSessions,
|
||||
setPendingTeamAction,
|
||||
]);
|
||||
|
||||
const handleSelectedSessionAction = useCallback(
|
||||
async (action: "close" | "resume" | "wait") => {
|
||||
@@ -122,29 +116,11 @@ export function useTeamWorkspaceBoardActions({
|
||||
onResumeSubagentSession,
|
||||
onWaitSubagentSession,
|
||||
selectedSession,
|
||||
setPendingSessionAction,
|
||||
waitTimeoutMs,
|
||||
],
|
||||
);
|
||||
|
||||
const handleSelectedSessionInputDraftChange = useCallback(
|
||||
(value: string) => {
|
||||
if (!selectedSession) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSessionInputDraftById((previous) => {
|
||||
if (previous[selectedSession.id] === value) {
|
||||
return previous;
|
||||
}
|
||||
return {
|
||||
...previous,
|
||||
[selectedSession.id]: value,
|
||||
};
|
||||
});
|
||||
},
|
||||
[selectedSession],
|
||||
);
|
||||
|
||||
const handleSelectedSessionSendInput = useCallback(
|
||||
async (interrupt: boolean) => {
|
||||
if (!selectedSession || !selectedSessionInputMessage) {
|
||||
@@ -158,22 +134,20 @@ export function useTeamWorkspaceBoardActions({
|
||||
await onSendSubagentInput?.(sessionId, selectedSessionInputMessage, {
|
||||
interrupt,
|
||||
});
|
||||
setSessionInputDraftById((previous) => {
|
||||
if (!previous[sessionId]) {
|
||||
return previous;
|
||||
}
|
||||
return {
|
||||
...previous,
|
||||
[sessionId]: "",
|
||||
};
|
||||
});
|
||||
clearSelectedSessionInputDraft(sessionId);
|
||||
} finally {
|
||||
setPendingSessionAction((current) =>
|
||||
current?.sessionId === sessionId ? null : current,
|
||||
);
|
||||
}
|
||||
},
|
||||
[onSendSubagentInput, selectedSession, selectedSessionInputMessage],
|
||||
[
|
||||
clearSelectedSessionInputDraft,
|
||||
onSendSubagentInput,
|
||||
selectedSession,
|
||||
selectedSessionInputMessage,
|
||||
setPendingSessionAction,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
+335
@@ -0,0 +1,335 @@
|
||||
import React from "react";
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { TeamWorkspaceRuntimeFormationState } from "../../teamWorkspaceRuntime";
|
||||
import { useTeamWorkspaceBoardCanvasRuntime } from "./useTeamWorkspaceBoardCanvasRuntime";
|
||||
|
||||
type HookProps = Parameters<typeof useTeamWorkspaceBoardCanvasRuntime>[0];
|
||||
|
||||
const { mockUseTeamWorkspaceCanvasController } = vi.hoisted(() => ({
|
||||
mockUseTeamWorkspaceCanvasController: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./useTeamWorkspaceCanvasController", () => ({
|
||||
useTeamWorkspaceCanvasController: mockUseTeamWorkspaceCanvasController,
|
||||
}));
|
||||
|
||||
const mountedRoots: Array<{ root: Root; container: HTMLDivElement }> = [];
|
||||
|
||||
function createFormationState(
|
||||
overrides?: Partial<TeamWorkspaceRuntimeFormationState>,
|
||||
): TeamWorkspaceRuntimeFormationState {
|
||||
return {
|
||||
requestId: overrides?.requestId ?? "dispatch-1",
|
||||
status: overrides?.status ?? "forming",
|
||||
label: overrides?.label ?? null,
|
||||
summary: overrides?.summary ?? null,
|
||||
members: overrides?.members ?? [],
|
||||
blueprint: overrides?.blueprint ?? null,
|
||||
errorMessage: overrides?.errorMessage ?? null,
|
||||
updatedAt: overrides?.updatedAt ?? 1,
|
||||
};
|
||||
}
|
||||
|
||||
function createControllerState(
|
||||
overrides?: Partial<
|
||||
ReturnType<typeof useTeamWorkspaceBoardCanvasRuntime> &
|
||||
Record<string, unknown>
|
||||
>,
|
||||
) {
|
||||
return {
|
||||
canvasBounds: {
|
||||
height: 720,
|
||||
width: 1080,
|
||||
},
|
||||
canvasLaneLayouts: {},
|
||||
canvasViewportRef: { current: null },
|
||||
handleAutoArrangeCanvas: vi.fn(),
|
||||
handleCanvasWheel: vi.fn(),
|
||||
handleFitCanvasView: vi.fn(),
|
||||
handleResetCanvasView: vi.fn(),
|
||||
handleSelectCanvasLane: vi.fn(),
|
||||
handleStartCanvasLaneDrag: vi.fn(),
|
||||
handleStartCanvasLaneResize: vi.fn(),
|
||||
handleStartCanvasPan: vi.fn(),
|
||||
handleZoomIn: vi.fn(),
|
||||
handleZoomOut: vi.fn(),
|
||||
isCanvasPanModifierActive: false,
|
||||
viewport: { x: 0, y: 0, zoom: 1.15 },
|
||||
zoom: 1.15,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function renderHook(props?: Partial<HookProps>) {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
let latestValue: ReturnType<typeof useTeamWorkspaceBoardCanvasRuntime> | null =
|
||||
null;
|
||||
|
||||
const defaultProps: HookProps = {
|
||||
activityTimelineEntryLimit: 4,
|
||||
canvasStorageScopeId: "scope-default",
|
||||
canvasViewportFallbackHeight: 560,
|
||||
embedded: false,
|
||||
expandedSessionId: null,
|
||||
focusSession: vi.fn(),
|
||||
hasRealTeamGraph: false,
|
||||
hasRuntimeFormation: false,
|
||||
hasSelectedTeamPlan: false,
|
||||
liveActivityBySessionId: {},
|
||||
memberCanvasSessions: [],
|
||||
plannedRoles: [],
|
||||
previewBySessionId: {},
|
||||
teamDispatchPreviewState: null,
|
||||
};
|
||||
|
||||
function Probe(currentProps: HookProps) {
|
||||
latestValue = useTeamWorkspaceBoardCanvasRuntime(currentProps);
|
||||
return null;
|
||||
}
|
||||
|
||||
const render = async (nextProps?: Partial<HookProps>) => {
|
||||
await act(async () => {
|
||||
root.render(<Probe {...defaultProps} {...props} {...nextProps} />);
|
||||
await Promise.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
mountedRoots.push({ root, container });
|
||||
|
||||
return {
|
||||
getValue: () => {
|
||||
if (!latestValue) {
|
||||
throw new Error("hook 尚未初始化");
|
||||
}
|
||||
return latestValue;
|
||||
},
|
||||
render,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
(
|
||||
globalThis as typeof globalThis & {
|
||||
IS_REACT_ACT_ENVIRONMENT?: boolean;
|
||||
}
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
mockUseTeamWorkspaceCanvasController.mockReturnValue(createControllerState());
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
while (mountedRoots.length > 0) {
|
||||
const mounted = mountedRoots.pop();
|
||||
if (!mounted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
mounted.root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
mounted.container.remove();
|
||||
}
|
||||
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("useTeamWorkspaceBoardCanvasRuntime", () => {
|
||||
it("应把真实成员任务进行时投影为 session lanes 并透传给 controller", async () => {
|
||||
const focusSession = vi.fn();
|
||||
const harness = renderHook({
|
||||
canvasStorageScopeId: "scope-team-board",
|
||||
canvasViewportFallbackHeight: 620,
|
||||
expandedSessionId: "child-1",
|
||||
focusSession,
|
||||
hasRealTeamGraph: true,
|
||||
hasRuntimeFormation: true,
|
||||
hasSelectedTeamPlan: true,
|
||||
liveActivityBySessionId: {
|
||||
"child-1": [
|
||||
{
|
||||
id: "live-1",
|
||||
title: "工具 检查",
|
||||
detail: "主线回归已补齐。",
|
||||
statusLabel: "完成",
|
||||
badgeClassName:
|
||||
"border border-emerald-200 bg-emerald-50 text-emerald-700",
|
||||
},
|
||||
],
|
||||
},
|
||||
memberCanvasSessions: [
|
||||
{
|
||||
id: "child-1",
|
||||
name: "执行代理",
|
||||
runtimeStatus: "running",
|
||||
taskSummary: "补齐 task schedule 主线回归。",
|
||||
blueprintRoleId: "executor",
|
||||
blueprintRoleLabel: "执行",
|
||||
profileId: "code-executor",
|
||||
profileName: "代码执行员",
|
||||
roleKey: "executor",
|
||||
teamPresetId: "code-triage-team",
|
||||
latestTurnStatus: "running",
|
||||
queuedTurnCount: 1,
|
||||
teamParallelBudget: 2,
|
||||
teamActiveCount: 1,
|
||||
skills: [
|
||||
{
|
||||
id: "repo-exploration",
|
||||
name: "仓库探索",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
plannedRoles: [
|
||||
{
|
||||
id: "executor",
|
||||
label: "执行",
|
||||
summary: "负责落地主线改动。",
|
||||
profileId: "code-executor",
|
||||
roleKey: "executor",
|
||||
},
|
||||
],
|
||||
previewBySessionId: {
|
||||
"child-1": {
|
||||
preview: "回复:旧内容",
|
||||
entries: [
|
||||
{
|
||||
id: "stored-1",
|
||||
title: "回复",
|
||||
detail: "历史里的旧进度。",
|
||||
statusLabel: "消息",
|
||||
badgeClassName:
|
||||
"border border-slate-200 bg-slate-50 text-slate-600",
|
||||
},
|
||||
],
|
||||
status: "ready",
|
||||
},
|
||||
},
|
||||
teamDispatchPreviewState: createFormationState({
|
||||
requestId: "dispatch-runtime-1",
|
||||
status: "formed",
|
||||
members: [
|
||||
{
|
||||
id: "executor",
|
||||
label: "执行",
|
||||
summary: "负责落地主线改动。",
|
||||
profileId: "code-executor",
|
||||
roleKey: "executor",
|
||||
skillIds: [],
|
||||
status: "running",
|
||||
sessionId: "child-1",
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
await harness.render();
|
||||
|
||||
const controllerArgs =
|
||||
mockUseTeamWorkspaceCanvasController.mock.calls.at(-1)?.[0];
|
||||
|
||||
expect(controllerArgs).toMatchObject({
|
||||
canvasStorageScopeId: "scope-team-board",
|
||||
canvasViewportFallbackHeight: 620,
|
||||
embedded: false,
|
||||
expandedSessionId: "child-1",
|
||||
});
|
||||
expect(controllerArgs?.onSelectSession).toBe(focusSession);
|
||||
expect(controllerArgs?.canvasLanes).toEqual(harness.getValue().canvasLanes);
|
||||
expect(harness.getValue().canvasLanes).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "child-1",
|
||||
kind: "session",
|
||||
persistKey: "session:child-1",
|
||||
fallbackPersistKeys: ["runtime:executor", "planned:executor"],
|
||||
title: "执行代理",
|
||||
previewText: "工具 检查:主线回归已补齐。",
|
||||
}),
|
||||
]);
|
||||
expect(harness.getValue().canvasStageHint).toBe(
|
||||
"当前任务会按状态持续刷新,焦点会优先落在正在处理的任务上;需要时可调整任务布局或缩放视图。",
|
||||
);
|
||||
});
|
||||
|
||||
it("应在成员尚未接入时先把 runtime formation 投影为任务进行时预备 lane", async () => {
|
||||
const harness = renderHook({
|
||||
hasRealTeamGraph: false,
|
||||
hasRuntimeFormation: true,
|
||||
hasSelectedTeamPlan: false,
|
||||
plannedRoles: [
|
||||
{
|
||||
id: "writer",
|
||||
label: "写作",
|
||||
summary: "把主线结果整理成说明。",
|
||||
roleKey: "writer",
|
||||
},
|
||||
],
|
||||
teamDispatchPreviewState: createFormationState({
|
||||
requestId: "dispatch-runtime-2",
|
||||
status: "formed",
|
||||
members: [
|
||||
{
|
||||
id: "writer",
|
||||
label: "写作",
|
||||
summary: "负责整理主线结果。",
|
||||
roleKey: "writer",
|
||||
skillIds: [],
|
||||
status: "running",
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
await harness.render();
|
||||
|
||||
expect(harness.getValue().canvasLanes).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "writer",
|
||||
kind: "runtime",
|
||||
persistKey: "runtime:writer",
|
||||
fallbackPersistKeys: ["planned:writer"],
|
||||
title: "写作",
|
||||
previewText: "负责整理主线结果。",
|
||||
}),
|
||||
]);
|
||||
expect(harness.getValue().canvasStageHint).toBe(
|
||||
"当前任务分工已经准备好,任务拆出后会依次开始处理。",
|
||||
);
|
||||
});
|
||||
|
||||
it("应在仅有计划分工时生成 planned lane 并提示即将切换为任务进行时", async () => {
|
||||
const harness = renderHook({
|
||||
hasRealTeamGraph: false,
|
||||
hasRuntimeFormation: false,
|
||||
hasSelectedTeamPlan: true,
|
||||
plannedRoles: [
|
||||
{
|
||||
id: "reviewer",
|
||||
label: "复核",
|
||||
summary: "确认任务进行时语义没有跑偏。",
|
||||
roleKey: "reviewer",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await harness.render();
|
||||
|
||||
expect(harness.getValue().canvasLanes).toEqual([
|
||||
expect.objectContaining({
|
||||
id: "reviewer",
|
||||
kind: "planned",
|
||||
persistKey: "planned:reviewer",
|
||||
title: "复核",
|
||||
previewText: "确认任务进行时语义没有跑偏。",
|
||||
}),
|
||||
]);
|
||||
expect(harness.getValue().canvasStageHint).toBe(
|
||||
"这里会先展示当前任务分工,任务拆出后会切换为独立的任务视图。",
|
||||
);
|
||||
});
|
||||
});
|
||||
+385
@@ -0,0 +1,385 @@
|
||||
import React from "react";
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useTeamWorkspaceBoardComposer } from "./useTeamWorkspaceBoardComposer";
|
||||
|
||||
type HookProps = Parameters<typeof useTeamWorkspaceBoardComposer>[0];
|
||||
|
||||
const {
|
||||
mockUseTeamWorkspaceActivityPreviews,
|
||||
mockUseTeamWorkspaceBoardCanvasRuntime,
|
||||
mockUseTeamWorkspaceBoardPresentation,
|
||||
mockUseTeamWorkspaceBoardRuntimeState,
|
||||
mockUseTeamWorkspaceBoardShellProps,
|
||||
} = vi.hoisted(() => ({
|
||||
mockUseTeamWorkspaceActivityPreviews: vi.fn(),
|
||||
mockUseTeamWorkspaceBoardCanvasRuntime: vi.fn(),
|
||||
mockUseTeamWorkspaceBoardPresentation: vi.fn(),
|
||||
mockUseTeamWorkspaceBoardRuntimeState: vi.fn(),
|
||||
mockUseTeamWorkspaceBoardShellProps: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./useTeamWorkspaceActivityPreviews", () => ({
|
||||
useTeamWorkspaceActivityPreviews: mockUseTeamWorkspaceActivityPreviews,
|
||||
}));
|
||||
|
||||
vi.mock("./useTeamWorkspaceBoardCanvasRuntime", () => ({
|
||||
useTeamWorkspaceBoardCanvasRuntime: mockUseTeamWorkspaceBoardCanvasRuntime,
|
||||
}));
|
||||
|
||||
vi.mock("./useTeamWorkspaceBoardPresentation", () => ({
|
||||
useTeamWorkspaceBoardPresentation: mockUseTeamWorkspaceBoardPresentation,
|
||||
}));
|
||||
|
||||
vi.mock("./useTeamWorkspaceBoardRuntimeState", () => ({
|
||||
useTeamWorkspaceBoardRuntimeState: mockUseTeamWorkspaceBoardRuntimeState,
|
||||
}));
|
||||
|
||||
vi.mock("./useTeamWorkspaceBoardShellProps", () => ({
|
||||
useTeamWorkspaceBoardShellProps: mockUseTeamWorkspaceBoardShellProps,
|
||||
}));
|
||||
|
||||
const mountedRoots: Array<{ root: Root; container: HTMLDivElement }> = [];
|
||||
|
||||
function createFormationState() {
|
||||
return {
|
||||
hasRuntimeFormation: true,
|
||||
hasSelectedTeamPlan: true,
|
||||
plannedRoles: [{ id: "researcher", label: "资料整理" }],
|
||||
runtimeFormationDisplay: {
|
||||
panelStatusLabel: "准备中",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createSessionGraphState() {
|
||||
return {
|
||||
basePreviewableRailSessions: [{ id: "base-1" }],
|
||||
baseRailSessions: [{ id: "base-1" }],
|
||||
canvasStorageScopeId: "scope-1",
|
||||
currentChildSession: { id: "child-current" },
|
||||
hasRealTeamGraph: true,
|
||||
isChildSession: true,
|
||||
isEmptyShellState: false,
|
||||
memberCanvasSessions: [{ id: "child-1" }, { id: "child-2" }],
|
||||
orchestratorSession: { id: "orchestrator-1" },
|
||||
railSessions: [{ id: "child-1" }, { id: "child-2" }],
|
||||
siblingCount: 1,
|
||||
totalTeamSessions: 2,
|
||||
visibleSessions: [{ id: "child-1" }, { id: "child-2" }],
|
||||
};
|
||||
}
|
||||
|
||||
function renderHook(props?: Partial<HookProps>) {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
let latestValue: ReturnType<typeof useTeamWorkspaceBoardComposer> | null =
|
||||
null;
|
||||
|
||||
const defaultProps: HookProps = {
|
||||
canvasViewportFallbackHeight: 560,
|
||||
className: "board-shell",
|
||||
currentSessionId: "child-2",
|
||||
detailExpanded: true,
|
||||
embedded: false,
|
||||
formationState: createFormationState() as unknown as HookProps["formationState"],
|
||||
onCloseCompletedTeamSessions: vi.fn(),
|
||||
onCloseSubagentSession: vi.fn(),
|
||||
onExpandEmptyShell: vi.fn(),
|
||||
onOpenSubagentSession: vi.fn(),
|
||||
onResumeSubagentSession: vi.fn(),
|
||||
onReturnToParentSession: vi.fn(),
|
||||
onSendSubagentInput: vi.fn(),
|
||||
onToggleDetail: vi.fn(),
|
||||
onWaitActiveTeamSessions: vi.fn(),
|
||||
onWaitSubagentSession: vi.fn(),
|
||||
sessionGraphState: createSessionGraphState() as unknown as HookProps["sessionGraphState"],
|
||||
shellExpanded: true,
|
||||
subagentParentContext: {
|
||||
parent_session_name: "主线程",
|
||||
} as HookProps["subagentParentContext"],
|
||||
teamControlSummary: {
|
||||
action: "close_completed",
|
||||
affectedSessionIds: ["child-done"],
|
||||
cascadeSessionIds: [],
|
||||
requestedSessionIds: ["child-done"],
|
||||
updatedAt: 1710000000,
|
||||
},
|
||||
teamDispatchPreviewState: {
|
||||
status: "running",
|
||||
} as unknown as HookProps["teamDispatchPreviewState"],
|
||||
teamWaitSummary: {
|
||||
awaitedSessionIds: ["child-2"],
|
||||
resolvedSessionId: "child-2",
|
||||
resolvedStatus: "completed",
|
||||
timedOut: false,
|
||||
updatedAt: 1710000100,
|
||||
},
|
||||
activityRefreshVersionBySessionId: {
|
||||
"child-2": 3,
|
||||
},
|
||||
liveActivityBySessionId: {
|
||||
"child-2": [],
|
||||
},
|
||||
};
|
||||
|
||||
function Probe(currentProps: HookProps) {
|
||||
latestValue = useTeamWorkspaceBoardComposer(currentProps);
|
||||
return null;
|
||||
}
|
||||
|
||||
const render = async (nextProps?: Partial<HookProps>) => {
|
||||
await act(async () => {
|
||||
root.render(<Probe {...defaultProps} {...props} {...nextProps} />);
|
||||
await Promise.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
mountedRoots.push({ root, container });
|
||||
|
||||
return {
|
||||
getValue: () => {
|
||||
if (!latestValue) {
|
||||
throw new Error("hook 尚未初始化");
|
||||
}
|
||||
return latestValue;
|
||||
},
|
||||
render,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
(
|
||||
globalThis as typeof globalThis & {
|
||||
IS_REACT_ACT_ENVIRONMENT?: boolean;
|
||||
}
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
mockUseTeamWorkspaceBoardRuntimeState.mockReturnValue({
|
||||
canCloseCompletedTeamSessions: true,
|
||||
canWaitAnyActiveTeamSession: true,
|
||||
completedTeamSessionIds: ["child-done-1", "child-done-2"],
|
||||
expandedSessionId: "child-2",
|
||||
focusSession: { id: "child-2" },
|
||||
selectedBaseSession: { id: "base-1" },
|
||||
selectedSession: { id: "child-2", runtimeStatus: "running" },
|
||||
statusSummary: { queued: 1, running: 1 },
|
||||
waitableTeamSessionIds: ["child-1"],
|
||||
});
|
||||
mockUseTeamWorkspaceActivityPreviews.mockReturnValue({
|
||||
selectedSessionActivityState: {
|
||||
entries: [],
|
||||
previewState: null,
|
||||
previewText: "资料整理刚更新进度",
|
||||
shouldPoll: true,
|
||||
supportsPreview: true,
|
||||
},
|
||||
sessionActivityPreviewById: {
|
||||
"child-2": {
|
||||
previewText: "资料整理刚更新进度",
|
||||
},
|
||||
},
|
||||
});
|
||||
mockUseTeamWorkspaceBoardCanvasRuntime.mockReturnValue({
|
||||
zoom: 1.25,
|
||||
});
|
||||
mockUseTeamWorkspaceBoardPresentation.mockReturnValue({
|
||||
boardShellClassName: "board-shell",
|
||||
});
|
||||
mockUseTeamWorkspaceBoardShellProps.mockReturnValue({
|
||||
emptyShellProps: {
|
||||
embedded: false,
|
||||
},
|
||||
shellProps: {
|
||||
embedded: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
while (mountedRoots.length > 0) {
|
||||
const mounted = mountedRoots.pop();
|
||||
if (!mounted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
mounted.root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
mounted.container.remove();
|
||||
}
|
||||
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("useTeamWorkspaceBoardComposer", () => {
|
||||
it("应把 schedule 主线所需的 runtime、activity、canvas、presentation 与 shell 串成同一条编排链", async () => {
|
||||
const harness = renderHook();
|
||||
|
||||
await harness.render();
|
||||
|
||||
expect(mockUseTeamWorkspaceBoardRuntimeState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
currentSessionId: "child-2",
|
||||
isChildSession: true,
|
||||
onCloseCompletedTeamSessions: expect.any(Function),
|
||||
orchestratorSessionId: "orchestrator-1",
|
||||
teamControlSummary: expect.objectContaining({
|
||||
action: "close_completed",
|
||||
}),
|
||||
teamWaitSummary: expect.objectContaining({
|
||||
resolvedSessionId: "child-2",
|
||||
}),
|
||||
visibleSessions: [{ id: "child-1" }, { id: "child-2" }],
|
||||
waitTimeoutMs: 30_000,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockUseTeamWorkspaceActivityPreviews).toHaveBeenCalledWith({
|
||||
activityRefreshVersionBySessionId: {
|
||||
"child-2": 3,
|
||||
},
|
||||
activityTimelineEntryLimit: 4,
|
||||
basePreviewableRailSessions: [{ id: "base-1" }],
|
||||
liveActivityBySessionId: {
|
||||
"child-2": [],
|
||||
},
|
||||
selectedBaseSession: { id: "base-1" },
|
||||
selectedSession: { id: "child-2", runtimeStatus: "running" },
|
||||
});
|
||||
|
||||
expect(mockUseTeamWorkspaceBoardCanvasRuntime).toHaveBeenCalledWith({
|
||||
activityTimelineEntryLimit: 4,
|
||||
canvasStorageScopeId: "scope-1",
|
||||
canvasViewportFallbackHeight: 560,
|
||||
embedded: false,
|
||||
expandedSessionId: "child-2",
|
||||
focusSession: { id: "child-2" },
|
||||
hasRealTeamGraph: true,
|
||||
hasRuntimeFormation: true,
|
||||
hasSelectedTeamPlan: true,
|
||||
liveActivityBySessionId: {
|
||||
"child-2": [],
|
||||
},
|
||||
memberCanvasSessions: [{ id: "child-1" }, { id: "child-2" }],
|
||||
plannedRoles: [{ id: "researcher", label: "资料整理" }],
|
||||
previewBySessionId: {
|
||||
"child-2": {
|
||||
previewText: "资料整理刚更新进度",
|
||||
},
|
||||
},
|
||||
teamDispatchPreviewState: {
|
||||
status: "running",
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockUseTeamWorkspaceBoardPresentation).toHaveBeenCalledWith({
|
||||
canCloseCompletedTeamSessions: true,
|
||||
canWaitAnyActiveTeamSession: true,
|
||||
className: "board-shell",
|
||||
completedCount: 2,
|
||||
detailExpanded: true,
|
||||
dispatchPreviewStatus: "running",
|
||||
embedded: false,
|
||||
hasRuntimeFormation: true,
|
||||
isChildSession: true,
|
||||
isEmptyShellState: false,
|
||||
parentSessionName: "主线程",
|
||||
runtimeFormationDisplay: {
|
||||
panelStatusLabel: "准备中",
|
||||
},
|
||||
selectedSession: { id: "child-2", runtimeStatus: "running" },
|
||||
shellExpanded: true,
|
||||
siblingCount: 1,
|
||||
statusSummary: { queued: 1, running: 1 },
|
||||
totalTeamSessions: 2,
|
||||
visibleSessionsCount: 2,
|
||||
waitableCount: 1,
|
||||
zoom: 1.25,
|
||||
});
|
||||
|
||||
expect(mockUseTeamWorkspaceBoardShellProps).toHaveBeenCalledWith({
|
||||
className: "board-shell",
|
||||
embedded: false,
|
||||
canvasRuntimeState: { zoom: 1.25 },
|
||||
formationState: expect.objectContaining({
|
||||
hasRuntimeFormation: true,
|
||||
}),
|
||||
onExpandEmptyShell: expect.any(Function),
|
||||
onOpenSubagentSession: expect.any(Function),
|
||||
onReturnToParentSession: expect.any(Function),
|
||||
onToggleDetail: expect.any(Function),
|
||||
presentationState: { boardShellClassName: "board-shell" },
|
||||
runtimeState: expect.objectContaining({
|
||||
completedTeamSessionIds: ["child-done-1", "child-done-2"],
|
||||
}),
|
||||
selectedSessionActivityState: expect.objectContaining({
|
||||
previewText: "资料整理刚更新进度",
|
||||
}),
|
||||
sessionGraphState: expect.objectContaining({
|
||||
canvasStorageScopeId: "scope-1",
|
||||
}),
|
||||
subagentParentContext: {
|
||||
parent_session_name: "主线程",
|
||||
},
|
||||
});
|
||||
|
||||
expect(harness.getValue()).toEqual({
|
||||
emptyShellProps: {
|
||||
embedded: false,
|
||||
},
|
||||
shellProps: {
|
||||
embedded: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("应在可选输入缺省时回退到稳定默认值", async () => {
|
||||
const harness = renderHook({
|
||||
activityRefreshVersionBySessionId: undefined,
|
||||
currentSessionId: undefined,
|
||||
liveActivityBySessionId: undefined,
|
||||
subagentParentContext: undefined,
|
||||
teamControlSummary: undefined,
|
||||
teamDispatchPreviewState: undefined,
|
||||
teamWaitSummary: undefined,
|
||||
});
|
||||
|
||||
await harness.render();
|
||||
|
||||
expect(mockUseTeamWorkspaceBoardRuntimeState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
currentSessionId: undefined,
|
||||
teamControlSummary: null,
|
||||
teamWaitSummary: null,
|
||||
}),
|
||||
);
|
||||
expect(mockUseTeamWorkspaceActivityPreviews).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
activityRefreshVersionBySessionId: {},
|
||||
liveActivityBySessionId: {},
|
||||
}),
|
||||
);
|
||||
expect(mockUseTeamWorkspaceBoardCanvasRuntime).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
liveActivityBySessionId: {},
|
||||
teamDispatchPreviewState: null,
|
||||
}),
|
||||
);
|
||||
expect(mockUseTeamWorkspaceBoardPresentation).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
dispatchPreviewStatus: undefined,
|
||||
parentSessionName: undefined,
|
||||
}),
|
||||
);
|
||||
expect(mockUseTeamWorkspaceBoardShellProps).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
subagentParentContext: null,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
-1
@@ -136,7 +136,6 @@ export function useTeamWorkspaceBoardComposer({
|
||||
detailExpanded,
|
||||
dispatchPreviewStatus: teamDispatchPreviewState?.status,
|
||||
embedded,
|
||||
hasRealTeamGraph: sessionGraphState.hasRealTeamGraph,
|
||||
hasRuntimeFormation: formationState.hasRuntimeFormation,
|
||||
isChildSession: sessionGraphState.isChildSession,
|
||||
isEmptyShellState: sessionGraphState.isEmptyShellState,
|
||||
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
import React from "react";
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { useTeamWorkspaceBoardOperationState } from "./useTeamWorkspaceBoardOperationState";
|
||||
|
||||
type HookProps = Parameters<typeof useTeamWorkspaceBoardOperationState>[0];
|
||||
|
||||
const mountedRoots: Array<{ root: Root; container: HTMLDivElement }> = [];
|
||||
|
||||
function renderHook(props?: Partial<HookProps>) {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
let latestValue: ReturnType<typeof useTeamWorkspaceBoardOperationState> | null =
|
||||
null;
|
||||
|
||||
const defaultProps: HookProps = {
|
||||
currentChildSession: null,
|
||||
currentSessionId: null,
|
||||
isChildSession: false,
|
||||
railSessions: [],
|
||||
teamControlSummary: null,
|
||||
teamWaitSummary: null,
|
||||
visibleSessions: [],
|
||||
};
|
||||
|
||||
function Probe(currentProps: HookProps) {
|
||||
latestValue = useTeamWorkspaceBoardOperationState(currentProps);
|
||||
return null;
|
||||
}
|
||||
|
||||
const render = async (nextProps?: Partial<HookProps>) => {
|
||||
await act(async () => {
|
||||
root.render(<Probe {...defaultProps} {...props} {...nextProps} />);
|
||||
await Promise.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
mountedRoots.push({ root, container });
|
||||
|
||||
return {
|
||||
getValue: () => {
|
||||
if (!latestValue) {
|
||||
throw new Error("hook 尚未初始化");
|
||||
}
|
||||
return latestValue;
|
||||
},
|
||||
render,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
(
|
||||
globalThis as typeof globalThis & {
|
||||
IS_REACT_ACT_ENVIRONMENT?: boolean;
|
||||
}
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
while (mountedRoots.length > 0) {
|
||||
const mounted = mountedRoots.pop();
|
||||
if (!mounted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
mounted.root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
mounted.container.remove();
|
||||
}
|
||||
});
|
||||
|
||||
describe("useTeamWorkspaceBoardOperationState", () => {
|
||||
it("应从当前可见成员与主轨道派生 schedule 状态", async () => {
|
||||
const harness = renderHook({
|
||||
currentChildSession: {
|
||||
id: "child-1",
|
||||
name: "当前成员",
|
||||
runtimeStatus: "running",
|
||||
sessionType: "sub_agent",
|
||||
},
|
||||
currentSessionId: "child-1",
|
||||
isChildSession: true,
|
||||
railSessions: [
|
||||
{
|
||||
id: "child-1",
|
||||
name: "当前成员",
|
||||
runtimeStatus: "running",
|
||||
sessionType: "sub_agent",
|
||||
},
|
||||
{
|
||||
id: "child-2",
|
||||
name: "资料整理",
|
||||
runtimeStatus: "queued",
|
||||
sessionType: "sub_agent",
|
||||
},
|
||||
{
|
||||
id: "child-3",
|
||||
name: "已完成成员",
|
||||
runtimeStatus: "completed",
|
||||
sessionType: "sub_agent",
|
||||
},
|
||||
{
|
||||
id: "user-main",
|
||||
name: "主线程",
|
||||
runtimeStatus: "running",
|
||||
sessionType: "user",
|
||||
},
|
||||
],
|
||||
visibleSessions: [
|
||||
{
|
||||
id: "child-1",
|
||||
name: "当前成员",
|
||||
runtimeStatus: "running",
|
||||
sessionType: "sub_agent",
|
||||
},
|
||||
{
|
||||
id: "child-2",
|
||||
name: "资料整理",
|
||||
runtimeStatus: "queued",
|
||||
sessionType: "sub_agent",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await harness.render();
|
||||
|
||||
expect(harness.getValue().statusSummary).toEqual({
|
||||
queued: 1,
|
||||
running: 1,
|
||||
});
|
||||
expect(harness.getValue().waitableTeamSessionIds).toEqual([
|
||||
"child-1",
|
||||
"child-2",
|
||||
]);
|
||||
expect(harness.getValue().completedTeamSessionIds).toEqual(["child-3"]);
|
||||
});
|
||||
|
||||
it("应只展示当前可见成员相关的等待动态", async () => {
|
||||
const harness = renderHook({
|
||||
railSessions: [
|
||||
{
|
||||
id: "child-1",
|
||||
name: "资料整理",
|
||||
runtimeStatus: "running",
|
||||
sessionType: "sub_agent",
|
||||
},
|
||||
],
|
||||
teamControlSummary: {
|
||||
action: "close_completed",
|
||||
affectedSessionIds: ["hidden-child"],
|
||||
cascadeSessionIds: [],
|
||||
requestedSessionIds: ["hidden-child"],
|
||||
updatedAt: 100,
|
||||
},
|
||||
teamWaitSummary: {
|
||||
awaitedSessionIds: ["child-1"],
|
||||
resolvedSessionId: "child-1",
|
||||
resolvedStatus: "completed",
|
||||
timedOut: false,
|
||||
updatedAt: 200,
|
||||
},
|
||||
visibleSessions: [
|
||||
{
|
||||
id: "child-1",
|
||||
name: "资料整理",
|
||||
runtimeStatus: "running",
|
||||
sessionType: "sub_agent",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await harness.render();
|
||||
|
||||
expect(harness.getValue().visibleTeamWaitSummary).toEqual({
|
||||
awaitedSessionIds: ["child-1"],
|
||||
resolvedSessionId: "child-1",
|
||||
resolvedStatus: "completed",
|
||||
timedOut: false,
|
||||
updatedAt: 200,
|
||||
});
|
||||
expect(harness.getValue().teamOperationEntries).toHaveLength(1);
|
||||
expect(harness.getValue().teamOperationEntries[0]).toMatchObject({
|
||||
targetSessionId: "child-1",
|
||||
title: "收到结果",
|
||||
});
|
||||
expect(harness.getValue().teamOperationEntries[0]?.detail).toContain(
|
||||
"资料整理",
|
||||
);
|
||||
});
|
||||
});
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import { useMemo } from "react";
|
||||
import type {
|
||||
TeamWorkspaceControlSummary,
|
||||
TeamWorkspaceWaitSummary,
|
||||
} from "../../teamWorkspaceRuntime";
|
||||
import {
|
||||
buildTeamWorkspaceSessionControlState,
|
||||
} from "../../team-workspace-runtime/sessionStateSelectors";
|
||||
import {
|
||||
buildVisibleTeamOperationState,
|
||||
type TeamOperationDisplayEntry,
|
||||
} from "../../team-workspace-runtime/teamOperationSelectors";
|
||||
import type { TeamSessionCard } from "../../utils/teamWorkspaceSessions";
|
||||
|
||||
interface UseTeamWorkspaceBoardOperationStateParams {
|
||||
currentChildSession?: TeamSessionCard | null;
|
||||
currentSessionId?: string | null;
|
||||
isChildSession: boolean;
|
||||
railSessions: TeamSessionCard[];
|
||||
teamControlSummary?: TeamWorkspaceControlSummary | null;
|
||||
teamWaitSummary?: TeamWorkspaceWaitSummary | null;
|
||||
visibleSessions: TeamSessionCard[];
|
||||
}
|
||||
|
||||
interface TeamWorkspaceBoardOperationState {
|
||||
completedTeamSessionIds: string[];
|
||||
statusSummary: Record<string, number>;
|
||||
teamOperationEntries: TeamOperationDisplayEntry[];
|
||||
visibleTeamWaitSummary: TeamWorkspaceWaitSummary | null;
|
||||
waitableTeamSessionIds: string[];
|
||||
}
|
||||
|
||||
export function useTeamWorkspaceBoardOperationState({
|
||||
currentChildSession = null,
|
||||
currentSessionId,
|
||||
isChildSession,
|
||||
railSessions,
|
||||
teamControlSummary = null,
|
||||
teamWaitSummary = null,
|
||||
visibleSessions,
|
||||
}: UseTeamWorkspaceBoardOperationStateParams): TeamWorkspaceBoardOperationState {
|
||||
const teamOperationState = useMemo(
|
||||
() =>
|
||||
buildVisibleTeamOperationState({
|
||||
railSessions,
|
||||
teamWaitSummary,
|
||||
teamControlSummary,
|
||||
}),
|
||||
[railSessions, teamControlSummary, teamWaitSummary],
|
||||
);
|
||||
const sessionControlState = useMemo(
|
||||
() =>
|
||||
buildTeamWorkspaceSessionControlState({
|
||||
visibleSessions,
|
||||
railSessions,
|
||||
currentChildSession,
|
||||
isChildSession,
|
||||
currentSessionId,
|
||||
}),
|
||||
[
|
||||
currentChildSession,
|
||||
currentSessionId,
|
||||
isChildSession,
|
||||
railSessions,
|
||||
visibleSessions,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
completedTeamSessionIds: sessionControlState.completedSessionIds,
|
||||
statusSummary: sessionControlState.statusSummary,
|
||||
teamOperationEntries: teamOperationState.entries,
|
||||
visibleTeamWaitSummary: teamOperationState.visibleTeamWaitSummary,
|
||||
waitableTeamSessionIds: sessionControlState.waitableSessionIds,
|
||||
};
|
||||
}
|
||||
+43
-78
@@ -1,5 +1,4 @@
|
||||
import { useMemo } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { buildTeamWorkspaceBoardChromeDisplayState } from "../../team-workspace-runtime/boardChromeSelectors";
|
||||
import {
|
||||
buildSelectedSessionDetailDisplayState,
|
||||
@@ -12,7 +11,10 @@ import {
|
||||
resolveStatusMeta,
|
||||
type TeamSessionCard,
|
||||
} from "../../utils/teamWorkspaceSessions";
|
||||
import { TEAM_WORKSPACE_CANVAS_STAGE_HEIGHT } from "../../utils/teamWorkspaceCanvas";
|
||||
import {
|
||||
buildTeamWorkspaceBoardSurfaceClassNames,
|
||||
resolveTeamWorkspaceBoardCopyState,
|
||||
} from "./teamWorkspaceBoardPresentationSelectors";
|
||||
|
||||
interface UseTeamWorkspaceBoardPresentationParams {
|
||||
canCloseCompletedTeamSessions: boolean;
|
||||
@@ -22,7 +24,6 @@ interface UseTeamWorkspaceBoardPresentationParams {
|
||||
detailExpanded: boolean;
|
||||
dispatchPreviewStatus?: TeamWorkspaceRuntimeFormationStatus | null;
|
||||
embedded: boolean;
|
||||
hasRealTeamGraph: boolean;
|
||||
hasRuntimeFormation: boolean;
|
||||
isChildSession: boolean;
|
||||
isEmptyShellState: boolean;
|
||||
@@ -66,7 +67,6 @@ export function useTeamWorkspaceBoardPresentation({
|
||||
detailExpanded,
|
||||
dispatchPreviewStatus = null,
|
||||
embedded,
|
||||
hasRealTeamGraph,
|
||||
hasRuntimeFormation,
|
||||
isChildSession,
|
||||
isEmptyShellState,
|
||||
@@ -81,30 +81,27 @@ export function useTeamWorkspaceBoardPresentation({
|
||||
zoom,
|
||||
runtimeFormationDisplay,
|
||||
}: UseTeamWorkspaceBoardPresentationParams): TeamWorkspaceBoardPresentationState {
|
||||
const useCompactCanvasChrome = hasRealTeamGraph;
|
||||
const detailVisible =
|
||||
isEmptyShellState || !hasRealTeamGraph
|
||||
? detailExpanded || shellExpanded
|
||||
: false;
|
||||
const detailToggleLabel = detailVisible ? "收起细节" : "查看细节";
|
||||
const memberCanvasTitle = "协作进展画布";
|
||||
const memberCanvasSubtitle = hasRealTeamGraph
|
||||
? isChildSession
|
||||
? "当前协作成员会在各自面板里持续更新进展和结果,主对话只保留必要摘要。"
|
||||
: `${visibleSessionsCount} 位协作成员已加入,每位成员都会在自己的面板里持续更新进展和结果。`
|
||||
: dispatchPreviewStatus === "forming"
|
||||
? "正在准备当前任务分工,成员接入后会在这里独立更新进展。"
|
||||
: dispatchPreviewStatus === "formed"
|
||||
? "当前任务分工已经就绪,成员接入后会在各自面板里开始处理。"
|
||||
: dispatchPreviewStatus === "failed"
|
||||
? "这次任务分工准备失败,暂时无法生成成员面板。"
|
||||
: "成员加入后,这里会展开为独立的任务进行时面板。";
|
||||
const hasRuntimeSessions = isChildSession || totalTeamSessions > 0;
|
||||
const useCompactCanvasChrome = hasRuntimeSessions;
|
||||
const {
|
||||
detailToggleLabel,
|
||||
detailVisible,
|
||||
memberCanvasSubtitle,
|
||||
memberCanvasTitle,
|
||||
} = resolveTeamWorkspaceBoardCopyState({
|
||||
detailExpanded,
|
||||
dispatchPreviewStatus,
|
||||
hasRuntimeSessions,
|
||||
isChildSession,
|
||||
isEmptyShellState,
|
||||
shellExpanded,
|
||||
visibleSessionsCount,
|
||||
});
|
||||
|
||||
const boardChromeDisplay = useMemo(
|
||||
() =>
|
||||
buildTeamWorkspaceBoardChromeDisplayState({
|
||||
hasRealTeamGraph,
|
||||
hasRuntimeFormation,
|
||||
hasRuntimeSessions,
|
||||
runtimeFormationTitle: hasRuntimeFormation
|
||||
? runtimeFormationDisplay.panelHeadline
|
||||
: null,
|
||||
@@ -125,7 +122,7 @@ export function useTeamWorkspaceBoardPresentation({
|
||||
canCloseCompletedTeamSessions,
|
||||
canWaitAnyActiveTeamSession,
|
||||
completedCount,
|
||||
hasRealTeamGraph,
|
||||
hasRuntimeSessions,
|
||||
hasRuntimeFormation,
|
||||
isChildSession,
|
||||
parentSessionName,
|
||||
@@ -143,7 +140,7 @@ export function useTeamWorkspaceBoardPresentation({
|
||||
const detailSummary =
|
||||
selectedSession?.taskSummary ||
|
||||
buildFallbackSummary({
|
||||
hasRealTeamGraph,
|
||||
hasRuntimeSessions,
|
||||
isChildSession,
|
||||
selectedSession,
|
||||
});
|
||||
@@ -157,53 +154,24 @@ export function useTeamWorkspaceBoardPresentation({
|
||||
[isChildSession, parentSessionName, selectedSession],
|
||||
);
|
||||
const selectedStatusMeta = resolveStatusMeta(selectedSession?.runtimeStatus);
|
||||
const boardShellClassName = cn(
|
||||
embedded
|
||||
? "pointer-events-auto flex h-full min-h-0 flex-1 flex-col overflow-hidden rounded-none border-0 bg-transparent shadow-none"
|
||||
: "overflow-hidden rounded-[26px] border border-slate-200 bg-white shadow-[0_18px_64px_-42px_rgba(15,23,42,0.24)]",
|
||||
embedded ? "mx-0 mt-0" : "mx-3 mt-2",
|
||||
const {
|
||||
boardBodyClassName,
|
||||
boardHeaderClassName,
|
||||
boardShellClassName,
|
||||
canvasStageHeight,
|
||||
detailCardClassName,
|
||||
inlineDetailSectionClassName,
|
||||
inlineTimelineEntryClassName,
|
||||
inlineTimelineFeedClassName,
|
||||
railCardClassName,
|
||||
} = buildTeamWorkspaceBoardSurfaceClassNames({
|
||||
className,
|
||||
);
|
||||
const boardHeaderClassName = cn(
|
||||
"flex flex-wrap items-start justify-between gap-3",
|
||||
useCompactCanvasChrome ? "px-4 py-2.5 sm:px-4" : "px-4 py-3.5 sm:px-5",
|
||||
embedded
|
||||
? cn(
|
||||
"sticky top-0 z-20 border-b border-slate-200",
|
||||
useCompactCanvasChrome ? "bg-white" : "bg-slate-50",
|
||||
)
|
||||
: cn(
|
||||
"border-b border-slate-200",
|
||||
useCompactCanvasChrome ? "bg-white" : "bg-slate-50",
|
||||
),
|
||||
);
|
||||
const boardBodyClassName = embedded
|
||||
? cn(
|
||||
"min-h-0 flex-1 overflow-y-auto overscroll-contain",
|
||||
useCompactCanvasChrome
|
||||
? "space-y-2.5 p-3 sm:p-3.5"
|
||||
: "space-y-3 p-3 sm:p-4",
|
||||
)
|
||||
: cn(useCompactCanvasChrome ? "p-3 sm:p-3.5" : "p-3 sm:p-4");
|
||||
const canvasStageHeight =
|
||||
embedded && !detailVisible
|
||||
? "clamp(560px, 76vh, 980px)"
|
||||
: TEAM_WORKSPACE_CANVAS_STAGE_HEIGHT;
|
||||
const railCardClassName = embedded
|
||||
? cn(
|
||||
"pointer-events-auto",
|
||||
useCompactCanvasChrome ? "space-y-3" : "space-y-4",
|
||||
)
|
||||
: "rounded-[22px] border border-slate-200 bg-slate-50 p-3.5 shadow-sm shadow-slate-950/5";
|
||||
const detailCardClassName = cn(
|
||||
embedded
|
||||
? "rounded-[20px] border border-slate-200 bg-white p-4"
|
||||
: "rounded-[22px] border p-4 shadow-sm shadow-slate-950/5",
|
||||
!embedded &&
|
||||
(selectedSession
|
||||
? selectedStatusMeta.cardClassName
|
||||
: "border-slate-200 bg-white"),
|
||||
);
|
||||
detailVisible,
|
||||
embedded,
|
||||
selectedSessionStatusCardClassName: selectedStatusMeta.cardClassName,
|
||||
selectedSessionVisible: Boolean(selectedSession),
|
||||
useCompactCanvasChrome,
|
||||
});
|
||||
|
||||
return {
|
||||
boardBodyClassName,
|
||||
@@ -215,12 +183,9 @@ export function useTeamWorkspaceBoardPresentation({
|
||||
detailSummary,
|
||||
detailToggleLabel,
|
||||
detailVisible,
|
||||
inlineDetailSectionClassName:
|
||||
"mt-3 rounded-[18px] border border-slate-200 bg-slate-50 p-3",
|
||||
inlineTimelineEntryClassName:
|
||||
"rounded-[14px] border border-slate-200 bg-white p-3",
|
||||
inlineTimelineFeedClassName:
|
||||
"mt-3 rounded-[16px] border border-slate-200 bg-white p-3",
|
||||
inlineDetailSectionClassName,
|
||||
inlineTimelineEntryClassName,
|
||||
inlineTimelineFeedClassName,
|
||||
memberCanvasSubtitle,
|
||||
memberCanvasTitle,
|
||||
railCardClassName,
|
||||
|
||||
+40
-83
@@ -1,18 +1,13 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useCallback } from "react";
|
||||
import type {
|
||||
TeamWorkspaceControlSummary,
|
||||
TeamWorkspaceWaitSummary,
|
||||
} from "../../teamWorkspaceRuntime";
|
||||
import {
|
||||
buildTeamWorkspaceSelectedSessionActionState,
|
||||
buildTeamWorkspaceSessionControlState,
|
||||
} from "../../team-workspace-runtime/sessionStateSelectors";
|
||||
import {
|
||||
buildVisibleTeamOperationState,
|
||||
type TeamOperationDisplayEntry,
|
||||
} from "../../team-workspace-runtime/teamOperationSelectors";
|
||||
import type { TeamOperationDisplayEntry } from "../../team-workspace-runtime/teamOperationSelectors";
|
||||
import type { TeamSessionCard } from "../../utils/teamWorkspaceSessions";
|
||||
import { useTeamWorkspaceBoardActions } from "./useTeamWorkspaceBoardActions";
|
||||
import { useTeamWorkspaceBoardOperationState } from "./useTeamWorkspaceBoardOperationState";
|
||||
import { useTeamWorkspaceBoardSelectedSessionActionState } from "./useTeamWorkspaceBoardSelectedSessionActionState";
|
||||
import { useTeamWorkspaceSessionFocus } from "./useTeamWorkspaceSessionFocus";
|
||||
|
||||
interface UseTeamWorkspaceBoardRuntimeStateParams {
|
||||
@@ -66,17 +61,21 @@ export function useTeamWorkspaceBoardRuntimeState({
|
||||
visibleSessions,
|
||||
waitTimeoutMs = 30_000,
|
||||
}: UseTeamWorkspaceBoardRuntimeStateParams) {
|
||||
const teamOperationState = useMemo(
|
||||
() =>
|
||||
buildVisibleTeamOperationState({
|
||||
railSessions,
|
||||
teamWaitSummary,
|
||||
teamControlSummary,
|
||||
}),
|
||||
[railSessions, teamControlSummary, teamWaitSummary],
|
||||
);
|
||||
const visibleTeamWaitSummary = teamOperationState.visibleTeamWaitSummary;
|
||||
const teamOperationEntries = teamOperationState.entries;
|
||||
const {
|
||||
completedTeamSessionIds,
|
||||
statusSummary,
|
||||
teamOperationEntries,
|
||||
visibleTeamWaitSummary,
|
||||
waitableTeamSessionIds,
|
||||
} = useTeamWorkspaceBoardOperationState({
|
||||
currentChildSession,
|
||||
currentSessionId,
|
||||
isChildSession,
|
||||
railSessions,
|
||||
teamControlSummary,
|
||||
teamWaitSummary,
|
||||
visibleSessions,
|
||||
});
|
||||
|
||||
const {
|
||||
expandedSessionId,
|
||||
@@ -93,69 +92,27 @@ export function useTeamWorkspaceBoardRuntimeState({
|
||||
visibleTeamWaitSummary,
|
||||
});
|
||||
|
||||
const sessionControlState = useMemo(
|
||||
() =>
|
||||
buildTeamWorkspaceSessionControlState({
|
||||
visibleSessions,
|
||||
railSessions,
|
||||
currentChildSession,
|
||||
isChildSession,
|
||||
currentSessionId,
|
||||
}),
|
||||
[
|
||||
currentChildSession,
|
||||
currentSessionId,
|
||||
isChildSession,
|
||||
railSessions,
|
||||
visibleSessions,
|
||||
],
|
||||
);
|
||||
const statusSummary = sessionControlState.statusSummary;
|
||||
const waitableTeamSessionIds = sessionControlState.waitableSessionIds;
|
||||
const completedTeamSessionIds = sessionControlState.completedSessionIds;
|
||||
|
||||
const sessionActionState = useMemo(
|
||||
() =>
|
||||
buildTeamWorkspaceSelectedSessionActionState({
|
||||
completedTeamSessionIds,
|
||||
currentSessionId,
|
||||
hasCloseCompletedTeamSessionsHandler: Boolean(
|
||||
onCloseCompletedTeamSessions,
|
||||
),
|
||||
hasCloseSubagentSessionHandler: Boolean(onCloseSubagentSession),
|
||||
hasOpenSubagentSessionHandler: Boolean(onOpenSubagentSession),
|
||||
hasResumeSubagentSessionHandler: Boolean(onResumeSubagentSession),
|
||||
hasSendSubagentInputHandler: Boolean(onSendSubagentInput),
|
||||
hasWaitActiveTeamSessionsHandler: Boolean(onWaitActiveTeamSessions),
|
||||
hasWaitSubagentSessionHandler: Boolean(onWaitSubagentSession),
|
||||
selectedSession,
|
||||
waitableTeamSessionIds,
|
||||
}),
|
||||
[
|
||||
completedTeamSessionIds,
|
||||
currentSessionId,
|
||||
onCloseCompletedTeamSessions,
|
||||
onCloseSubagentSession,
|
||||
onOpenSubagentSession,
|
||||
onResumeSubagentSession,
|
||||
onSendSubagentInput,
|
||||
onWaitActiveTeamSessions,
|
||||
onWaitSubagentSession,
|
||||
selectedSession,
|
||||
waitableTeamSessionIds,
|
||||
],
|
||||
);
|
||||
const canCloseCompletedTeamSessions =
|
||||
sessionActionState.canCloseCompletedTeamSessions;
|
||||
const canOpenSelectedSession = sessionActionState.canOpenSelectedSession;
|
||||
const canResumeSelectedSession =
|
||||
sessionActionState.canResumeSelectedSession;
|
||||
const canSendSelectedSessionInput =
|
||||
sessionActionState.canSendSelectedSessionInput;
|
||||
const canStopSelectedSession = sessionActionState.canStopSelectedSession;
|
||||
const canWaitAnyActiveTeamSession =
|
||||
sessionActionState.canWaitAnyActiveTeamSession;
|
||||
const canWaitSelectedSession = sessionActionState.canWaitSelectedSession;
|
||||
const {
|
||||
canCloseCompletedTeamSessions,
|
||||
canOpenSelectedSession,
|
||||
canResumeSelectedSession,
|
||||
canSendSelectedSessionInput,
|
||||
canStopSelectedSession,
|
||||
canWaitAnyActiveTeamSession,
|
||||
canWaitSelectedSession,
|
||||
} = useTeamWorkspaceBoardSelectedSessionActionState({
|
||||
completedTeamSessionIds,
|
||||
currentSessionId,
|
||||
onCloseCompletedTeamSessions,
|
||||
onCloseSubagentSession,
|
||||
onOpenSubagentSession,
|
||||
onResumeSubagentSession,
|
||||
onSendSubagentInput,
|
||||
onWaitActiveTeamSessions,
|
||||
onWaitSubagentSession,
|
||||
selectedSession,
|
||||
waitableTeamSessionIds,
|
||||
});
|
||||
|
||||
const {
|
||||
handleCloseCompletedTeamSessions,
|
||||
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
import React from "react";
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import { useTeamWorkspaceBoardSelectedSessionActionState } from "./useTeamWorkspaceBoardSelectedSessionActionState";
|
||||
|
||||
type HookProps = Parameters<typeof useTeamWorkspaceBoardSelectedSessionActionState>[0];
|
||||
|
||||
const mountedRoots: Array<{ root: Root; container: HTMLDivElement }> = [];
|
||||
|
||||
function renderHook(props?: Partial<HookProps>) {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
let latestValue:
|
||||
| ReturnType<typeof useTeamWorkspaceBoardSelectedSessionActionState>
|
||||
| null = null;
|
||||
|
||||
const defaultProps: HookProps = {
|
||||
completedTeamSessionIds: [],
|
||||
currentSessionId: "parent-1",
|
||||
onCloseCompletedTeamSessions: undefined,
|
||||
onCloseSubagentSession: undefined,
|
||||
onOpenSubagentSession: undefined,
|
||||
onResumeSubagentSession: undefined,
|
||||
onSendSubagentInput: undefined,
|
||||
onWaitActiveTeamSessions: undefined,
|
||||
onWaitSubagentSession: undefined,
|
||||
selectedSession: null,
|
||||
waitableTeamSessionIds: [],
|
||||
};
|
||||
|
||||
function Probe(currentProps: HookProps) {
|
||||
latestValue = useTeamWorkspaceBoardSelectedSessionActionState(currentProps);
|
||||
return null;
|
||||
}
|
||||
|
||||
const render = async (nextProps?: Partial<HookProps>) => {
|
||||
await act(async () => {
|
||||
root.render(<Probe {...defaultProps} {...props} {...nextProps} />);
|
||||
await Promise.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
mountedRoots.push({ root, container });
|
||||
|
||||
return {
|
||||
getValue: () => {
|
||||
if (!latestValue) {
|
||||
throw new Error("hook 尚未初始化");
|
||||
}
|
||||
return latestValue;
|
||||
},
|
||||
render,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
(
|
||||
globalThis as typeof globalThis & {
|
||||
IS_REACT_ACT_ENVIRONMENT?: boolean;
|
||||
}
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
while (mountedRoots.length > 0) {
|
||||
const mounted = mountedRoots.pop();
|
||||
if (!mounted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
mounted.root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
mounted.container.remove();
|
||||
}
|
||||
});
|
||||
|
||||
describe("useTeamWorkspaceBoardSelectedSessionActionState", () => {
|
||||
it("应为运行中成员开放当前可用操作", async () => {
|
||||
const noop = async () => {};
|
||||
const harness = renderHook({
|
||||
completedTeamSessionIds: ["done-1"],
|
||||
onCloseCompletedTeamSessions: noop,
|
||||
onCloseSubagentSession: noop,
|
||||
onOpenSubagentSession: noop,
|
||||
onResumeSubagentSession: noop,
|
||||
onSendSubagentInput: noop,
|
||||
onWaitActiveTeamSessions: noop,
|
||||
onWaitSubagentSession: noop,
|
||||
selectedSession: {
|
||||
id: "child-1",
|
||||
name: "资料整理",
|
||||
runtimeStatus: "running",
|
||||
sessionType: "sub_agent",
|
||||
},
|
||||
waitableTeamSessionIds: ["child-1", "child-2"],
|
||||
});
|
||||
|
||||
await harness.render();
|
||||
|
||||
expect(harness.getValue()).toMatchObject({
|
||||
canCloseCompletedTeamSessions: true,
|
||||
canOpenSelectedSession: true,
|
||||
canResumeSelectedSession: false,
|
||||
canSendSelectedSessionInput: true,
|
||||
canStopSelectedSession: true,
|
||||
canWaitAnyActiveTeamSession: true,
|
||||
canWaitSelectedSession: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("应为已关闭成员只保留恢复能力", async () => {
|
||||
const noop = async () => {};
|
||||
const harness = renderHook({
|
||||
onCloseSubagentSession: noop,
|
||||
onOpenSubagentSession: noop,
|
||||
onResumeSubagentSession: noop,
|
||||
onSendSubagentInput: noop,
|
||||
onWaitSubagentSession: noop,
|
||||
selectedSession: {
|
||||
id: "child-closed",
|
||||
name: "封面定稿",
|
||||
runtimeStatus: "closed",
|
||||
sessionType: "sub_agent",
|
||||
},
|
||||
waitableTeamSessionIds: ["child-2"],
|
||||
});
|
||||
|
||||
await harness.render();
|
||||
|
||||
expect(harness.getValue()).toMatchObject({
|
||||
canOpenSelectedSession: true,
|
||||
canResumeSelectedSession: true,
|
||||
canSendSelectedSessionInput: false,
|
||||
canStopSelectedSession: false,
|
||||
canWaitAnyActiveTeamSession: false,
|
||||
canWaitSelectedSession: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { useMemo } from "react";
|
||||
import { buildTeamWorkspaceSelectedSessionActionState } from "../../team-workspace-runtime/sessionStateSelectors";
|
||||
import type { TeamSessionCard } from "../../utils/teamWorkspaceSessions";
|
||||
|
||||
interface UseTeamWorkspaceBoardSelectedSessionActionStateParams {
|
||||
completedTeamSessionIds: string[];
|
||||
currentSessionId?: string | null;
|
||||
onCloseCompletedTeamSessions?: (sessionIds: string[]) => void | Promise<void>;
|
||||
onCloseSubagentSession?: (sessionId: string) => void | Promise<void>;
|
||||
onOpenSubagentSession?: (sessionId: string) => void | Promise<void>;
|
||||
onResumeSubagentSession?: (sessionId: string) => void | Promise<void>;
|
||||
onSendSubagentInput?: (
|
||||
sessionId: string,
|
||||
message: string,
|
||||
options?: { interrupt?: boolean },
|
||||
) => void | Promise<void>;
|
||||
onWaitActiveTeamSessions?: (
|
||||
sessionIds: string[],
|
||||
timeoutMs?: number,
|
||||
) => void | Promise<void>;
|
||||
onWaitSubagentSession?: (
|
||||
sessionId: string,
|
||||
timeoutMs?: number,
|
||||
) => void | Promise<void>;
|
||||
selectedSession: TeamSessionCard | null;
|
||||
waitableTeamSessionIds: string[];
|
||||
}
|
||||
|
||||
export function useTeamWorkspaceBoardSelectedSessionActionState({
|
||||
completedTeamSessionIds,
|
||||
currentSessionId,
|
||||
onCloseCompletedTeamSessions,
|
||||
onCloseSubagentSession,
|
||||
onOpenSubagentSession,
|
||||
onResumeSubagentSession,
|
||||
onSendSubagentInput,
|
||||
onWaitActiveTeamSessions,
|
||||
onWaitSubagentSession,
|
||||
selectedSession,
|
||||
waitableTeamSessionIds,
|
||||
}: UseTeamWorkspaceBoardSelectedSessionActionStateParams) {
|
||||
return useMemo(
|
||||
() =>
|
||||
buildTeamWorkspaceSelectedSessionActionState({
|
||||
completedTeamSessionIds,
|
||||
currentSessionId,
|
||||
hasCloseCompletedTeamSessionsHandler: Boolean(
|
||||
onCloseCompletedTeamSessions,
|
||||
),
|
||||
hasCloseSubagentSessionHandler: Boolean(onCloseSubagentSession),
|
||||
hasOpenSubagentSessionHandler: Boolean(onOpenSubagentSession),
|
||||
hasResumeSubagentSessionHandler: Boolean(onResumeSubagentSession),
|
||||
hasSendSubagentInputHandler: Boolean(onSendSubagentInput),
|
||||
hasWaitActiveTeamSessionsHandler: Boolean(onWaitActiveTeamSessions),
|
||||
hasWaitSubagentSessionHandler: Boolean(onWaitSubagentSession),
|
||||
selectedSession,
|
||||
waitableTeamSessionIds,
|
||||
}),
|
||||
[
|
||||
completedTeamSessionIds,
|
||||
currentSessionId,
|
||||
onCloseCompletedTeamSessions,
|
||||
onCloseSubagentSession,
|
||||
onOpenSubagentSession,
|
||||
onResumeSubagentSession,
|
||||
onSendSubagentInput,
|
||||
onWaitActiveTeamSessions,
|
||||
onWaitSubagentSession,
|
||||
selectedSession,
|
||||
waitableTeamSessionIds,
|
||||
],
|
||||
);
|
||||
}
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
import React from "react";
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import type {
|
||||
AsterSubagentParentContext,
|
||||
AsterSubagentSessionInfo,
|
||||
} from "@/lib/api/agentRuntime";
|
||||
import type {
|
||||
TeamWorkspaceLiveRuntimeState,
|
||||
TeamWorkspaceRuntimeFormationState,
|
||||
} from "../../teamWorkspaceRuntime";
|
||||
import { useTeamWorkspaceBoardSessionGraph } from "./useTeamWorkspaceBoardSessionGraph";
|
||||
|
||||
type HookProps = Parameters<typeof useTeamWorkspaceBoardSessionGraph>[0];
|
||||
|
||||
const mountedRoots: Array<{ root: Root; container: HTMLDivElement }> = [];
|
||||
|
||||
function createSubagentSessionInfo(
|
||||
overrides: Partial<AsterSubagentSessionInfo> &
|
||||
Pick<AsterSubagentSessionInfo, "id" | "name">,
|
||||
): AsterSubagentSessionInfo {
|
||||
return {
|
||||
created_at: overrides.created_at ?? 1,
|
||||
updated_at: overrides.updated_at ?? 2,
|
||||
session_type: overrides.session_type ?? "sub_agent",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createFormationState(
|
||||
overrides?: Partial<TeamWorkspaceRuntimeFormationState>,
|
||||
): TeamWorkspaceRuntimeFormationState {
|
||||
return {
|
||||
requestId: overrides?.requestId ?? "dispatch-1",
|
||||
status: overrides?.status ?? "formed",
|
||||
label: overrides?.label ?? null,
|
||||
summary: overrides?.summary ?? null,
|
||||
members: overrides?.members ?? [],
|
||||
blueprint: overrides?.blueprint ?? null,
|
||||
errorMessage: overrides?.errorMessage ?? null,
|
||||
updatedAt: overrides?.updatedAt ?? 1,
|
||||
};
|
||||
}
|
||||
|
||||
function renderHook(props?: Partial<HookProps>) {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
let latestValue: ReturnType<typeof useTeamWorkspaceBoardSessionGraph> | null =
|
||||
null;
|
||||
|
||||
const defaultProps: HookProps = {
|
||||
childSubagentSessions: [],
|
||||
currentSessionId: "parent-1",
|
||||
currentSessionLatestTurnStatus: "running",
|
||||
currentSessionName: "主线程",
|
||||
currentSessionQueuedTurnCount: 0,
|
||||
currentSessionRuntimeStatus: "running",
|
||||
liveRuntimeBySessionId: {},
|
||||
shellVisible: false,
|
||||
subagentParentContext: null,
|
||||
teamDispatchPreviewState: null,
|
||||
};
|
||||
|
||||
function Probe(currentProps: HookProps) {
|
||||
latestValue = useTeamWorkspaceBoardSessionGraph(currentProps);
|
||||
return null;
|
||||
}
|
||||
|
||||
const render = async (nextProps?: Partial<HookProps>) => {
|
||||
await act(async () => {
|
||||
root.render(<Probe {...defaultProps} {...props} {...nextProps} />);
|
||||
await Promise.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
mountedRoots.push({ root, container });
|
||||
|
||||
return {
|
||||
getValue: () => {
|
||||
if (!latestValue) {
|
||||
throw new Error("hook 尚未初始化");
|
||||
}
|
||||
return latestValue;
|
||||
},
|
||||
render,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
(
|
||||
globalThis as typeof globalThis & {
|
||||
IS_REACT_ACT_ENVIRONMENT?: boolean;
|
||||
}
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
while (mountedRoots.length > 0) {
|
||||
const mounted = mountedRoots.pop();
|
||||
if (!mounted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
mounted.root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
mounted.container.remove();
|
||||
}
|
||||
});
|
||||
|
||||
describe("useTeamWorkspaceBoardSessionGraph", () => {
|
||||
it("应在子成员上下文里用 live runtime 覆盖当前任务进行时图谱", async () => {
|
||||
const subagentParentContext = {
|
||||
parent_session_id: "parent-1",
|
||||
parent_session_name: "主线程",
|
||||
task_summary: "拆解主线并推进。",
|
||||
role_hint: "analysis",
|
||||
blueprint_role_id: "role-analysis",
|
||||
blueprint_role_label: "分析",
|
||||
role_key: "analysis",
|
||||
sibling_subagent_sessions: [
|
||||
createSubagentSessionInfo({
|
||||
id: "child-verifier",
|
||||
name: "复核成员",
|
||||
runtime_status: "queued",
|
||||
latest_turn_status: "queued",
|
||||
queued_turn_count: 2,
|
||||
blueprint_role_id: "role-verify",
|
||||
blueprint_role_label: "验证",
|
||||
role_key: "verifier",
|
||||
}),
|
||||
createSubagentSessionInfo({
|
||||
id: "child-executor",
|
||||
name: "执行成员",
|
||||
runtime_status: "running",
|
||||
latest_turn_status: "running",
|
||||
queued_turn_count: 1,
|
||||
blueprint_role_id: "role-executor",
|
||||
blueprint_role_label: "执行",
|
||||
role_key: "executor",
|
||||
}),
|
||||
],
|
||||
} as AsterSubagentParentContext;
|
||||
const liveRuntimeBySessionId: Record<
|
||||
string,
|
||||
TeamWorkspaceLiveRuntimeState
|
||||
> = {
|
||||
"child-current": {
|
||||
runtimeStatus: "running",
|
||||
latestTurnStatus: "running",
|
||||
queuedTurnCount: 3,
|
||||
teamActiveCount: 1,
|
||||
teamParallelBudget: 2,
|
||||
baseFingerprint: "child-current:live",
|
||||
},
|
||||
"child-executor": {
|
||||
runtimeStatus: "completed",
|
||||
latestTurnStatus: "completed",
|
||||
queuedTurnCount: 0,
|
||||
baseFingerprint: "child-executor:live",
|
||||
},
|
||||
};
|
||||
const harness = renderHook({
|
||||
childSubagentSessions: [
|
||||
createSubagentSessionInfo({
|
||||
id: "ignored-root-child",
|
||||
name: "不会进入当前子成员视图",
|
||||
runtime_status: "running",
|
||||
}),
|
||||
],
|
||||
currentSessionId: "child-current",
|
||||
currentSessionLatestTurnStatus: "queued",
|
||||
currentSessionName: "分析成员",
|
||||
currentSessionQueuedTurnCount: 1,
|
||||
currentSessionRuntimeStatus: "queued",
|
||||
liveRuntimeBySessionId,
|
||||
subagentParentContext,
|
||||
teamDispatchPreviewState: createFormationState({
|
||||
blueprint: {
|
||||
label: "主线 Team",
|
||||
summary: "分析、执行、验证并行推进。",
|
||||
roles: [
|
||||
{
|
||||
id: "role-analysis",
|
||||
label: "分析",
|
||||
summary: "收敛当前主线边界。",
|
||||
roleKey: "analysis",
|
||||
},
|
||||
{
|
||||
id: "role-executor",
|
||||
label: "执行",
|
||||
summary: "落实当前改动。",
|
||||
roleKey: "executor",
|
||||
},
|
||||
{
|
||||
id: "role-verify",
|
||||
label: "验证",
|
||||
summary: "确认任务进行时语义。",
|
||||
roleKey: "verifier",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await harness.render();
|
||||
|
||||
expect(harness.getValue().isChildSession).toBe(true);
|
||||
expect(harness.getValue().canvasStorageScopeId).toBe("child-current");
|
||||
expect(harness.getValue().hasRealTeamGraph).toBe(true);
|
||||
expect(harness.getValue().isEmptyShellState).toBe(false);
|
||||
expect(harness.getValue().siblingCount).toBe(2);
|
||||
expect(harness.getValue().totalTeamSessions).toBe(3);
|
||||
expect(harness.getValue().currentChildSession).toMatchObject({
|
||||
id: "child-current",
|
||||
name: "分析成员",
|
||||
runtimeStatus: "running",
|
||||
latestTurnStatus: "running",
|
||||
queuedTurnCount: 3,
|
||||
teamActiveCount: 1,
|
||||
teamParallelBudget: 2,
|
||||
isCurrent: true,
|
||||
});
|
||||
expect(harness.getValue().visibleSessions).toMatchObject([
|
||||
{
|
||||
id: "child-verifier",
|
||||
runtimeStatus: "queued",
|
||||
latestTurnStatus: "queued",
|
||||
queuedTurnCount: 2,
|
||||
},
|
||||
{
|
||||
id: "child-executor",
|
||||
runtimeStatus: "completed",
|
||||
latestTurnStatus: "completed",
|
||||
queuedTurnCount: 0,
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
harness.getValue().visibleSessions.map((session) => session.id),
|
||||
).toEqual(["child-verifier", "child-executor"]);
|
||||
expect(
|
||||
harness.getValue().visibleSessions.some(
|
||||
(session) => session.id === "ignored-root-child",
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
harness.getValue().railSessions.map((session) => session.id),
|
||||
).toEqual(["child-current", "child-verifier", "child-executor"]);
|
||||
expect(
|
||||
harness.getValue().memberCanvasSessions.map((session) => session.id),
|
||||
).toEqual(["child-current", "child-verifier", "child-executor"]);
|
||||
expect(
|
||||
harness.getValue().basePreviewableRailSessions.map((session) => session.id),
|
||||
).toEqual(["child-current", "child-verifier", "child-executor"]);
|
||||
});
|
||||
|
||||
it("应在空 shell 态下回退到 dispatch preview request id 作为 schedule 作用域", async () => {
|
||||
const harness = renderHook({
|
||||
currentSessionId: " ",
|
||||
currentSessionName: "主线程",
|
||||
shellVisible: true,
|
||||
teamDispatchPreviewState: createFormationState({
|
||||
requestId: "dispatch-preview-42",
|
||||
status: "forming",
|
||||
}),
|
||||
});
|
||||
|
||||
await harness.render();
|
||||
|
||||
expect(harness.getValue().isChildSession).toBe(false);
|
||||
expect(harness.getValue().hasRealTeamGraph).toBe(false);
|
||||
expect(harness.getValue().isEmptyShellState).toBe(true);
|
||||
expect(harness.getValue().canvasStorageScopeId).toBe(
|
||||
"dispatch-preview-42",
|
||||
);
|
||||
expect(harness.getValue().currentChildSession).toBeNull();
|
||||
expect(harness.getValue().visibleSessions).toEqual([]);
|
||||
expect(harness.getValue().baseRailSessions).toEqual([]);
|
||||
expect(harness.getValue().railSessions).toEqual([]);
|
||||
expect(harness.getValue().memberCanvasSessions).toEqual([]);
|
||||
expect(harness.getValue().basePreviewableRailSessions).toEqual([]);
|
||||
expect(harness.getValue().totalTeamSessions).toBe(0);
|
||||
});
|
||||
});
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
import React from "react";
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useTeamWorkspaceBoardShellProps } from "./useTeamWorkspaceBoardShellProps";
|
||||
|
||||
type HookProps = Parameters<typeof useTeamWorkspaceBoardShellProps>[0];
|
||||
|
||||
const { mockUseTeamWorkspaceBoardSelectedInlineDetail } = vi.hoisted(() => ({
|
||||
mockUseTeamWorkspaceBoardSelectedInlineDetail: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./useTeamWorkspaceBoardSelectedInlineDetail", () => ({
|
||||
useTeamWorkspaceBoardSelectedInlineDetail:
|
||||
mockUseTeamWorkspaceBoardSelectedInlineDetail,
|
||||
}));
|
||||
|
||||
const mountedRoots: Array<{ root: Root; container: HTMLDivElement }> = [];
|
||||
const inlineDetailNode = (
|
||||
<div data-testid="inline-detail">inline detail</div>
|
||||
);
|
||||
|
||||
function createRuntimeFormationDisplay() {
|
||||
return {
|
||||
blueprintRoleCards: [],
|
||||
emptyDetail: "系统正在准备分工。",
|
||||
hint: "正在准备分工",
|
||||
hasRuntimeFormation: true,
|
||||
memberCards: [],
|
||||
noticeText: "系统正在准备当前任务分工。",
|
||||
panelDescription: "当前任务分工正在生成。",
|
||||
panelHeadline: "任务分工准备中",
|
||||
panelLabel: "编辑 Team",
|
||||
panelStatusLabel: "准备中",
|
||||
panelStatusBadgeClassName: "border-sky-200 bg-sky-50 text-sky-700",
|
||||
panelTitle: "当前任务分工",
|
||||
referenceLabel: null,
|
||||
summaryBadges: [],
|
||||
};
|
||||
}
|
||||
|
||||
function createSelectedTeamPlanDisplay() {
|
||||
return {
|
||||
hasSelectedTeamPlan: true,
|
||||
label: "编辑 Team",
|
||||
roleCards: [],
|
||||
summary: "先拆三段,再并行推进。",
|
||||
summaryBadges: [],
|
||||
};
|
||||
}
|
||||
|
||||
function renderHook(props?: Partial<HookProps>) {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
const root = createRoot(container);
|
||||
|
||||
let latestValue: ReturnType<typeof useTeamWorkspaceBoardShellProps> | null =
|
||||
null;
|
||||
|
||||
const defaultProps: HookProps = {
|
||||
canvasRuntimeState: {
|
||||
canvasBounds: {
|
||||
height: 680,
|
||||
width: 960,
|
||||
},
|
||||
canvasLaneLayouts: {},
|
||||
canvasLanes: [{ persistKey: "lane-alpha" } as never],
|
||||
canvasStageHint: "正在同步任务进行时",
|
||||
canvasViewportRef: { current: null },
|
||||
handleAutoArrangeCanvas: vi.fn(),
|
||||
handleCanvasWheel: vi.fn(),
|
||||
handleFitCanvasView: vi.fn(),
|
||||
handleResetCanvasView: vi.fn(),
|
||||
handleSelectCanvasLane: vi.fn(),
|
||||
handleStartCanvasLaneDrag: vi.fn(),
|
||||
handleStartCanvasLaneResize: vi.fn(),
|
||||
handleStartCanvasPan: vi.fn(),
|
||||
handleZoomIn: vi.fn(),
|
||||
handleZoomOut: vi.fn(),
|
||||
isCanvasPanModifierActive: false,
|
||||
viewport: { x: 12, y: 24, zoom: 1.1 },
|
||||
zoom: 1.1,
|
||||
} as unknown as HookProps["canvasRuntimeState"],
|
||||
className: "custom-shell",
|
||||
embedded: true,
|
||||
formationState: {
|
||||
hasRuntimeFormation: true,
|
||||
runtimeFormationDisplay: createRuntimeFormationDisplay(),
|
||||
selectedTeamPlanDisplay: createSelectedTeamPlanDisplay(),
|
||||
} as unknown as HookProps["formationState"],
|
||||
onExpandEmptyShell: vi.fn(),
|
||||
onOpenSubagentSession: vi.fn(),
|
||||
onReturnToParentSession: vi.fn(),
|
||||
onToggleDetail: vi.fn(),
|
||||
presentationState: {
|
||||
boardBodyClassName: "board-body",
|
||||
boardChromeDisplay: {
|
||||
boardHeadline: "主任务总览",
|
||||
boardHint: "主线程会持续汇总子任务进展。",
|
||||
compactBoardHeadline: "任务进行中 · 2 项处理中",
|
||||
compactToolbarChips: [],
|
||||
statusSummaryBadges: [],
|
||||
},
|
||||
boardHeaderClassName: "board-header",
|
||||
boardShellClassName: "board-shell",
|
||||
canvasStageHeight: "680px",
|
||||
detailCardClassName: "detail-card",
|
||||
detailSummary: "子任务会持续更新任务进展。",
|
||||
detailToggleLabel: "查看细节",
|
||||
detailVisible: true,
|
||||
inlineDetailSectionClassName: "inline-detail",
|
||||
inlineTimelineEntryClassName: "inline-entry",
|
||||
inlineTimelineFeedClassName: "inline-feed",
|
||||
memberCanvasSubtitle: "子任务会持续更新任务进展。",
|
||||
memberCanvasTitle: "任务视图",
|
||||
railCardClassName: "rail-card",
|
||||
selectedSessionDetailDisplay: {
|
||||
summary: "资料整理正在推进。",
|
||||
},
|
||||
useCompactCanvasChrome: true,
|
||||
} as unknown as HookProps["presentationState"],
|
||||
runtimeState: {
|
||||
canCloseCompletedTeamSessions: true,
|
||||
canOpenSelectedSession: true,
|
||||
canResumeSelectedSession: false,
|
||||
canSendSelectedSessionInput: true,
|
||||
canStopSelectedSession: true,
|
||||
canWaitAnyActiveTeamSession: true,
|
||||
canWaitSelectedSession: true,
|
||||
completedTeamSessionIds: ["child-done"],
|
||||
expandedSessionId: "child-2",
|
||||
handleCloseCompletedTeamSessions: vi.fn(),
|
||||
handleSelectTeamOperationEntry: vi.fn(),
|
||||
handleSelectedSessionAction: vi.fn(),
|
||||
handleSelectedSessionInputDraftChange: vi.fn(),
|
||||
handleSelectedSessionSendInput: vi.fn(),
|
||||
handleWaitAnyActiveTeamSessions: vi.fn(),
|
||||
pendingSessionAction: {
|
||||
action: "interrupt_send",
|
||||
sessionId: "child-2",
|
||||
},
|
||||
pendingTeamAction: "wait_any",
|
||||
selectedActionPending: true,
|
||||
selectedSession: {
|
||||
id: "child-2",
|
||||
name: "资料整理",
|
||||
runtimeStatus: "running",
|
||||
sessionType: "sub_agent",
|
||||
updatedAt: 1710000000,
|
||||
},
|
||||
selectedSessionInputDraft: "继续推进下一步",
|
||||
selectedSessionInputMessage: "继续推进下一步",
|
||||
teamOperationEntries: [
|
||||
{
|
||||
badgeClassName: "border-sky-200 bg-sky-50 text-sky-700",
|
||||
detail: "资料整理返回了新结果",
|
||||
id: "wait-1",
|
||||
targetSessionId: "child-2",
|
||||
title: "收到结果",
|
||||
updatedAt: 1710000000,
|
||||
},
|
||||
],
|
||||
waitableTeamSessionIds: ["child-1", "child-2"],
|
||||
} as unknown as HookProps["runtimeState"],
|
||||
selectedSessionActivityState: {
|
||||
entries: [],
|
||||
previewState: null,
|
||||
previewText: "",
|
||||
shouldPoll: false,
|
||||
supportsPreview: false,
|
||||
} as unknown as HookProps["selectedSessionActivityState"],
|
||||
sessionGraphState: {
|
||||
hasRealTeamGraph: true,
|
||||
isChildSession: true,
|
||||
isEmptyShellState: false,
|
||||
totalTeamSessions: 2,
|
||||
} as unknown as HookProps["sessionGraphState"],
|
||||
subagentParentContext: {
|
||||
created_from_turn_id: "turn-42",
|
||||
parent_session_name: "主线程",
|
||||
} as HookProps["subagentParentContext"],
|
||||
};
|
||||
|
||||
function Probe(currentProps: HookProps) {
|
||||
latestValue = useTeamWorkspaceBoardShellProps(currentProps);
|
||||
return null;
|
||||
}
|
||||
|
||||
const render = async (nextProps?: Partial<HookProps>) => {
|
||||
await act(async () => {
|
||||
root.render(<Probe {...defaultProps} {...props} {...nextProps} />);
|
||||
await Promise.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
mountedRoots.push({ root, container });
|
||||
|
||||
return {
|
||||
getValue: () => {
|
||||
if (!latestValue) {
|
||||
throw new Error("hook 尚未初始化");
|
||||
}
|
||||
return latestValue;
|
||||
},
|
||||
render,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
(
|
||||
globalThis as typeof globalThis & {
|
||||
IS_REACT_ACT_ENVIRONMENT?: boolean;
|
||||
}
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
mockUseTeamWorkspaceBoardSelectedInlineDetail.mockReturnValue(inlineDetailNode);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
while (mountedRoots.length > 0) {
|
||||
const mounted = mountedRoots.pop();
|
||||
if (!mounted) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await act(async () => {
|
||||
mounted.root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
mounted.container.remove();
|
||||
}
|
||||
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("useTeamWorkspaceBoardShellProps", () => {
|
||||
it("应在已有 runtime sessions 时关闭 fallback detail 并组装完整壳层 props", async () => {
|
||||
const harness = renderHook();
|
||||
|
||||
await harness.render();
|
||||
|
||||
expect(
|
||||
mockUseTeamWorkspaceBoardSelectedInlineDetail,
|
||||
).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
canOpenSelectedSession: true,
|
||||
canWaitSelectedSession: true,
|
||||
isChildSession: true,
|
||||
selectedActionPending: true,
|
||||
selectedSession: expect.objectContaining({
|
||||
id: "child-2",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(harness.getValue().emptyShellProps).toMatchObject({
|
||||
className: "custom-shell",
|
||||
embedded: true,
|
||||
hasRuntimeFormation: true,
|
||||
});
|
||||
expect(harness.getValue().shellProps).toMatchObject({
|
||||
boardBodyClassName: "board-body",
|
||||
boardHeaderClassName: "board-header",
|
||||
boardShellClassName: "board-shell",
|
||||
embedded: true,
|
||||
style: { maxHeight: "inherit" },
|
||||
});
|
||||
expect(harness.getValue().shellProps.headerProps).toMatchObject({
|
||||
createdFromTurnId: "turn-42",
|
||||
detailToggleLabel: "查看细节",
|
||||
runtimeFormationStatusLabel: "准备中",
|
||||
totalTeamSessions: 2,
|
||||
});
|
||||
expect(
|
||||
harness.getValue().shellProps.canvasSectionProps.canvasStageProps
|
||||
.selectedSessionId,
|
||||
).toBe("child-2");
|
||||
expect(
|
||||
harness.getValue().shellProps.canvasSectionProps.canvasStageProps
|
||||
.selectedInlineDetail,
|
||||
).toBe(inlineDetailNode);
|
||||
expect(
|
||||
harness.getValue().shellProps.canvasSectionProps.overviewChromeProps,
|
||||
).toMatchObject({
|
||||
completedCount: 1,
|
||||
pendingTeamAction: "wait_any",
|
||||
waitableCount: 2,
|
||||
});
|
||||
expect(
|
||||
harness.getValue().shellProps.canvasSectionProps.fallbackDetailProps,
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("应在尚无 runtime sessions 时保留 fallback detail 并保留普通 shell 高度", async () => {
|
||||
const harness = renderHook({
|
||||
embedded: false,
|
||||
sessionGraphState: {
|
||||
hasRealTeamGraph: false,
|
||||
isChildSession: false,
|
||||
isEmptyShellState: false,
|
||||
totalTeamSessions: 0,
|
||||
} as unknown as HookProps["sessionGraphState"],
|
||||
subagentParentContext: null,
|
||||
});
|
||||
|
||||
await harness.render();
|
||||
|
||||
expect(
|
||||
harness.getValue().shellProps.canvasSectionProps.fallbackDetailProps,
|
||||
).toMatchObject({
|
||||
detailCardClassName: "detail-card",
|
||||
detailVisible: true,
|
||||
});
|
||||
expect(harness.getValue().shellProps.style).toBeUndefined();
|
||||
expect(harness.getValue().shellProps.headerProps.createdFromTurnId).toBe(
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
+27
-15
@@ -5,7 +5,9 @@ import {
|
||||
resolveStatusMeta,
|
||||
} from "../../utils/teamWorkspaceSessions";
|
||||
import {
|
||||
buildTeamWorkspaceBoardCanvasSectionProps,
|
||||
buildTeamWorkspaceBoardEmptyShellProps,
|
||||
buildTeamWorkspaceBoardHeaderProps,
|
||||
buildTeamWorkspaceBoardShellProps,
|
||||
} from "./teamWorkspaceBoardPropBuilders";
|
||||
import { useTeamWorkspaceBoardSelectedInlineDetail } from "./useTeamWorkspaceBoardSelectedInlineDetail";
|
||||
@@ -148,11 +150,11 @@ export function useTeamWorkspaceBoardShellProps({
|
||||
zoom: canvasZoom,
|
||||
} = canvasRuntimeState;
|
||||
const {
|
||||
hasRealTeamGraph,
|
||||
isChildSession,
|
||||
isEmptyShellState,
|
||||
totalTeamSessions,
|
||||
} = sessionGraphState;
|
||||
const hasRuntimeSessions = isChildSession || totalTeamSessions > 0;
|
||||
|
||||
const selectedInlineDetail = useTeamWorkspaceBoardSelectedInlineDetail({
|
||||
canOpenSelectedSession,
|
||||
@@ -187,11 +189,22 @@ export function useTeamWorkspaceBoardShellProps({
|
||||
runtimeFormationDisplay,
|
||||
selectedTeamPlanDisplay,
|
||||
});
|
||||
const shellProps = buildTeamWorkspaceBoardShellProps({
|
||||
boardBodyClassName,
|
||||
const headerProps = buildTeamWorkspaceBoardHeaderProps({
|
||||
boardChromeDisplay,
|
||||
createdFromTurnId: subagentParentContext?.created_from_turn_id,
|
||||
detailToggleLabel,
|
||||
detailVisible,
|
||||
isChildSession,
|
||||
isEmptyShellState,
|
||||
onReturnToParentSession,
|
||||
onToggleDetail,
|
||||
resolveStatusMeta,
|
||||
runtimeFormationStatusLabel: runtimeFormationDisplay.panelStatusLabel,
|
||||
totalTeamSessions,
|
||||
useCompactCanvasChrome,
|
||||
});
|
||||
const canvasSectionProps = buildTeamWorkspaceBoardCanvasSectionProps({
|
||||
boardChromeDisplay,
|
||||
boardHeaderClassName,
|
||||
boardShellClassName,
|
||||
canvasBoundsHeight: canvasBounds.height,
|
||||
canvasBoundsWidth: canvasBounds.width,
|
||||
canvasStageHeight,
|
||||
@@ -201,17 +214,12 @@ export function useTeamWorkspaceBoardShellProps({
|
||||
canCloseCompletedTeamSessions,
|
||||
canWaitAnyActiveTeamSession,
|
||||
completedCount: completedTeamSessionIds.length,
|
||||
createdFromTurnId: subagentParentContext?.created_from_turn_id,
|
||||
detailCardClassName,
|
||||
detailToggleLabel,
|
||||
detailVisible,
|
||||
embedded,
|
||||
expandedSessionId,
|
||||
formatUpdatedAt,
|
||||
hasRealTeamGraph,
|
||||
hasRuntimeSessions,
|
||||
isCanvasPanModifierActive,
|
||||
isChildSession,
|
||||
isEmptyShellState,
|
||||
laneLayouts: canvasLaneLayouts,
|
||||
lanes: canvasLanes,
|
||||
memberCanvasSubtitle,
|
||||
@@ -220,32 +228,36 @@ export function useTeamWorkspaceBoardShellProps({
|
||||
onCanvasWheel: handleCanvasWheel,
|
||||
onCloseCompletedTeamSessions: handleCloseCompletedTeamSessions,
|
||||
onFitCanvasView: handleFitCanvasView,
|
||||
onReturnToParentSession,
|
||||
onResetCanvasView: handleResetCanvasView,
|
||||
onSelectCanvasLane: handleSelectCanvasLane,
|
||||
onSelectTeamOperationEntry: handleSelectTeamOperationEntry,
|
||||
onStartCanvasLaneDrag: handleStartCanvasLaneDrag,
|
||||
onStartCanvasLaneResize: handleStartCanvasLaneResize,
|
||||
onStartCanvasPan: handleStartCanvasPan,
|
||||
onToggleDetail,
|
||||
onWaitAnyActiveTeamSessions: handleWaitAnyActiveTeamSessions,
|
||||
onZoomIn: handleZoomIn,
|
||||
onZoomOut: handleZoomOut,
|
||||
pendingTeamAction,
|
||||
railCardClassName,
|
||||
resolveStatusMeta,
|
||||
runtimeFormationStatusLabel: runtimeFormationDisplay.panelStatusLabel,
|
||||
runtimeFormationDisplay,
|
||||
selectedInlineDetail,
|
||||
selectedSession,
|
||||
selectedSessionId: selectedSession?.id ?? null,
|
||||
selectedTeamPlanDisplay,
|
||||
teamOperationEntries,
|
||||
totalTeamSessions,
|
||||
useCompactCanvasChrome,
|
||||
viewport: canvasViewport,
|
||||
waitableCount: waitableTeamSessionIds.length,
|
||||
});
|
||||
const shellProps = buildTeamWorkspaceBoardShellProps({
|
||||
boardBodyClassName,
|
||||
boardHeaderClassName,
|
||||
boardShellClassName,
|
||||
canvasSectionProps,
|
||||
embedded,
|
||||
headerProps,
|
||||
});
|
||||
|
||||
return {
|
||||
emptyShellProps,
|
||||
|
||||
@@ -16,7 +16,7 @@ interface UseGeneralWorkbenchSidebarOrchestrationParams {
|
||||
}
|
||||
|
||||
export interface GeneralWorkbenchSidebarOrchestration {
|
||||
branchCount: number;
|
||||
workflowCount: number;
|
||||
isVersionMode: boolean;
|
||||
activeContextCount: number;
|
||||
visibleExecLogCount: number;
|
||||
@@ -31,6 +31,9 @@ export function useGeneralWorkbenchSidebarOrchestration({
|
||||
}: UseGeneralWorkbenchSidebarOrchestrationParams): GeneralWorkbenchSidebarOrchestration {
|
||||
const { isVersionMode, context, workflow, execLog } = input;
|
||||
const branchCount = workflow.branchItems.length;
|
||||
const workflowCount = workflow.workflowSteps.filter(
|
||||
(step) => step.status !== "completed" && step.status !== "skipped",
|
||||
).length;
|
||||
const runDetailSessionId =
|
||||
workflow.activeRunDetail?.session_id?.trim() || null;
|
||||
const { handleRevealArtifactInFinder, handleOpenArtifactWithDefaultApp } =
|
||||
@@ -76,7 +79,7 @@ export function useGeneralWorkbenchSidebarOrchestration({
|
||||
});
|
||||
|
||||
return {
|
||||
branchCount,
|
||||
workflowCount,
|
||||
isVersionMode,
|
||||
activeContextCount: contextPanelState.activeContextItems.length,
|
||||
visibleExecLogCount: execLogState.visibleExecLogEntries.length,
|
||||
|
||||
@@ -43,7 +43,7 @@ export function useGeneralWorkbenchWorkflowPanelState({
|
||||
activeRunMetadata,
|
||||
}: UseGeneralWorkbenchWorkflowPanelStateParams): GeneralWorkbenchWorkflowPanelState {
|
||||
const [showActivityLogs, setShowActivityLogs] = useState(false);
|
||||
const [showCreationTasks, setShowCreationTasks] = useState(true);
|
||||
const [showCreationTasks, setShowCreationTasks] = useState(false);
|
||||
|
||||
const completedSteps = useMemo(
|
||||
() => workflowSteps.filter((step) => step.status === "completed").length,
|
||||
|
||||
@@ -172,6 +172,7 @@ describe("agentChatHistory", () => {
|
||||
usage: {
|
||||
input_tokens: 12000,
|
||||
output_tokens: 19000,
|
||||
cached_input_tokens: 4000,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -182,6 +183,7 @@ describe("agentChatHistory", () => {
|
||||
expect(messages[0]?.usage).toEqual({
|
||||
input_tokens: 12000,
|
||||
output_tokens: 19000,
|
||||
cached_input_tokens: 4000,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -433,6 +435,7 @@ describe("agentChatHistory", () => {
|
||||
usage: {
|
||||
input_tokens: 20480,
|
||||
output_tokens: 10240,
|
||||
cached_input_tokens: 8192,
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -459,6 +462,7 @@ describe("agentChatHistory", () => {
|
||||
expect(mergedMessages[1]?.usage).toEqual({
|
||||
input_tokens: 20480,
|
||||
output_tokens: 10240,
|
||||
cached_input_tokens: 8192,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -74,6 +74,8 @@ const normalizeHistoryUsage = (usage: unknown): AgentTokenUsage | undefined => {
|
||||
|
||||
const inputTokens = (usage as { input_tokens?: unknown }).input_tokens;
|
||||
const outputTokens = (usage as { output_tokens?: unknown }).output_tokens;
|
||||
const cachedInputTokens = (usage as { cached_input_tokens?: unknown })
|
||||
.cached_input_tokens;
|
||||
if (
|
||||
typeof inputTokens !== "number" ||
|
||||
typeof outputTokens !== "number" ||
|
||||
@@ -88,6 +90,12 @@ const normalizeHistoryUsage = (usage: unknown): AgentTokenUsage | undefined => {
|
||||
return {
|
||||
input_tokens: inputTokens,
|
||||
output_tokens: outputTokens,
|
||||
cached_input_tokens:
|
||||
typeof cachedInputTokens === "number" &&
|
||||
Number.isFinite(cachedInputTokens) &&
|
||||
cachedInputTokens >= 0
|
||||
? cachedInputTokens
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -896,7 +904,7 @@ const buildAssistantHydrationSignature = (message: Message): string => {
|
||||
|
||||
const buildHistoryMessageSignature = (message: Message): string => {
|
||||
const usageSignature = message.usage
|
||||
? `${message.usage.input_tokens}:${message.usage.output_tokens}`
|
||||
? `${message.usage.input_tokens}:${message.usage.output_tokens}:${message.usage.cached_input_tokens ?? ""}`
|
||||
: "";
|
||||
return [
|
||||
message.role,
|
||||
|
||||
@@ -48,6 +48,7 @@ describe("agentStreamRuntimeHandler", () => {
|
||||
usage: {
|
||||
input_tokens: 12_000,
|
||||
output_tokens: 19_000,
|
||||
cached_input_tokens: 8_000,
|
||||
},
|
||||
} as AgentEvent,
|
||||
requestState: {
|
||||
@@ -100,6 +101,7 @@ describe("agentStreamRuntimeHandler", () => {
|
||||
usage: {
|
||||
input_tokens: 12_000,
|
||||
output_tokens: 19_000,
|
||||
cached_input_tokens: 8_000,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,6 +39,5 @@ export function useAgentChatUnified(options: UseAgentChatUnifiedOptions) {
|
||||
export { useAsterAgentChat } from "./useAsterAgentChat";
|
||||
export { useRuntimeTeamFormation } from "./useRuntimeTeamFormation";
|
||||
export { useTeamWorkspaceRuntime } from "./useTeamWorkspaceRuntime";
|
||||
// compat subagent 适配仅允许内部直连,不继续从统一 hooks 入口扩散。
|
||||
export { useThemeContextWorkspace } from "./useThemeContextWorkspace";
|
||||
export { useTopicBranchBoard } from "./useTopicBranchBoard";
|
||||
|
||||
@@ -4565,7 +4565,7 @@ describe("useAsterAgentChat action_required 渲染链路", () => {
|
||||
stream.emit({
|
||||
type: "tool_start",
|
||||
tool_id: "tool-meta-1",
|
||||
tool_name: "SubAgentTask",
|
||||
tool_name: "Agent",
|
||||
arguments: JSON.stringify({
|
||||
prompt: "检查 harness 缺口",
|
||||
}),
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { useSubAgentScheduler } from "@/hooks/useSubAgentScheduler";
|
||||
|
||||
import {
|
||||
buildCompatSubagentRuntimeSnapshot,
|
||||
type CompatSubagentRuntimeSnapshot,
|
||||
} from "../utils/compatSubagentRuntime";
|
||||
|
||||
export function useCompatSubagentRuntime(
|
||||
sessionId?: string | null,
|
||||
): CompatSubagentRuntimeSnapshot {
|
||||
const compatSchedulerState = useSubAgentScheduler(sessionId);
|
||||
|
||||
return buildCompatSubagentRuntimeSnapshot(compatSchedulerState);
|
||||
}
|
||||
@@ -37,10 +37,8 @@ function renderHook(props?: Partial<HookProps>) {
|
||||
|
||||
const defaultProps: HookProps = {
|
||||
projectId: "project-1",
|
||||
sessionId: "session-1",
|
||||
selectedTeam: createSelectedTeam(),
|
||||
subagentEnabled: true,
|
||||
hasRealTeamGraph: false,
|
||||
createRequestId: () => "request-1",
|
||||
now: () => 1_710_000_000_000,
|
||||
};
|
||||
|
||||
@@ -14,10 +14,8 @@ interface PrepareRuntimeTeamBeforeSendParams {
|
||||
|
||||
interface UseRuntimeTeamFormationOptions {
|
||||
projectId?: string | null;
|
||||
sessionId?: string | null;
|
||||
selectedTeam?: TeamDefinition | null;
|
||||
subagentEnabled: boolean;
|
||||
hasRealTeamGraph: boolean;
|
||||
createRequestId?: () => string;
|
||||
now?: () => number;
|
||||
}
|
||||
|
||||
@@ -152,6 +152,9 @@ describe("useTeamMemoryShadowSync", () => {
|
||||
"Team:研究双人组",
|
||||
);
|
||||
expect(snapshot?.entries["team.selection"]?.content).toContain("角色:");
|
||||
expect(snapshot?.entries["team.subagents"]?.content).toContain(
|
||||
"子任务:",
|
||||
);
|
||||
expect(snapshot?.entries["team.subagents"]?.content).toContain(
|
||||
"研究代理 [running] explorer · 梳理主线风险",
|
||||
);
|
||||
|
||||
@@ -91,7 +91,7 @@ function buildSubagentMemoryContent(params: {
|
||||
|
||||
return [
|
||||
params.sessionId ? `会话:${params.sessionId}` : null,
|
||||
"子代理:",
|
||||
"子任务:",
|
||||
...sessions.map(summarizeSubagentSession),
|
||||
]
|
||||
.filter((item): item is string => Boolean(item))
|
||||
|
||||
@@ -23,7 +23,6 @@ const {
|
||||
mockUseThemeContextWorkspace,
|
||||
mockUseTopicBranchBoard,
|
||||
mockUseTeamWorkspaceRuntime,
|
||||
mockUseCompatSubagentRuntime,
|
||||
mockUseSessionFiles,
|
||||
mockGetProject,
|
||||
mockGetDefaultProject,
|
||||
@@ -68,7 +67,6 @@ const {
|
||||
mockUseThemeContextWorkspace: vi.fn(),
|
||||
mockUseTopicBranchBoard: vi.fn(),
|
||||
mockUseTeamWorkspaceRuntime: vi.fn(),
|
||||
mockUseCompatSubagentRuntime: vi.fn(),
|
||||
mockUseSessionFiles: vi.fn(),
|
||||
mockGetProject: vi.fn(),
|
||||
mockGetDefaultProject: vi.fn(),
|
||||
@@ -197,10 +195,6 @@ vi.mock("./hooks", () => ({
|
||||
useTeamWorkspaceRuntime: mockUseTeamWorkspaceRuntime,
|
||||
}));
|
||||
|
||||
vi.mock("./hooks/useCompatSubagentRuntime", () => ({
|
||||
useCompatSubagentRuntime: mockUseCompatSubagentRuntime,
|
||||
}));
|
||||
|
||||
vi.mock("@/hooks/useDeveloperFeatureFlags", () => ({
|
||||
useDeveloperFeatureFlags: mockUseDeveloperFeatureFlags,
|
||||
}));
|
||||
@@ -476,7 +470,7 @@ vi.mock("./components/TeamWorkspaceDock", () => ({
|
||||
onActivateWorkbench();
|
||||
}}
|
||||
>
|
||||
打开任务协作
|
||||
打开任务工作台
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -1237,15 +1231,6 @@ beforeEach(() => {
|
||||
liveActivityBySessionId: {},
|
||||
activityRefreshVersionBySessionId: {},
|
||||
});
|
||||
mockUseCompatSubagentRuntime.mockReturnValue({
|
||||
isRunning: false,
|
||||
progress: null,
|
||||
events: [],
|
||||
result: null,
|
||||
error: null,
|
||||
recentActivity: [],
|
||||
hasSignals: false,
|
||||
});
|
||||
mockUseSessionFiles.mockReturnValue({
|
||||
saveFile: vi.fn(async () => undefined),
|
||||
files: [],
|
||||
@@ -1897,6 +1882,9 @@ describe("AgentChatPage 通用工作台", { timeout: 20_000 }, () => {
|
||||
|
||||
expect(document.body.textContent).toContain("处理工作台");
|
||||
expect(document.body.textContent).toContain("通用助手");
|
||||
expect(document.body.textContent).toContain(
|
||||
"集中查看计划、待确认事项、任务进展、文件活动和处理结果。",
|
||||
);
|
||||
});
|
||||
|
||||
it("处理工作台开关关闭时不应显示入口,也不应触发工具库存读取", async () => {
|
||||
@@ -5883,8 +5871,8 @@ describe("AgentChatPage 服务技能 A2UI", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("AgentChatPage legacy 问卷 A2UI", () => {
|
||||
it("工作区编排出现待补充 A2UI 时应保持聊天主区域,不展示左侧工作台侧栏", async () => {
|
||||
describe("AgentChatPage 当前 A2UI 事实源", () => {
|
||||
it("历史问卷正文不应再提升为输入区 A2UI", async () => {
|
||||
mockUseThemeContextWorkspace.mockReturnValue(
|
||||
createMockThemeContextWorkspaceState({
|
||||
enabled: true,
|
||||
@@ -5918,7 +5906,7 @@ describe("AgentChatPage legacy 问卷 A2UI", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const container = renderPage({
|
||||
renderPage({
|
||||
projectId: "project-theme-a2ui",
|
||||
contentId: "content-theme-a2ui",
|
||||
theme: "general",
|
||||
@@ -5926,222 +5914,48 @@ describe("AgentChatPage legacy 问卷 A2UI", () => {
|
||||
});
|
||||
await flushEffects(10);
|
||||
|
||||
expect(
|
||||
container
|
||||
.querySelector('[data-testid="layout-transition"]')
|
||||
?.getAttribute("data-mode"),
|
||||
).toBe("chat");
|
||||
expect(
|
||||
container.querySelector('[data-testid="general-workbench-sidebar"]'),
|
||||
).toBeNull();
|
||||
|
||||
const latestPendingPanelProps =
|
||||
mockWorkspacePendingA2UIPanel.mock.calls.at(-1)?.[0] as
|
||||
| {
|
||||
pendingA2UIForm?: {
|
||||
data?: Record<string, unknown>;
|
||||
} | null;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
expect(latestPendingPanelProps?.pendingA2UIForm?.data).toMatchObject({
|
||||
source: "legacy_questionnaire",
|
||||
questionCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it("应将结构化问卷提升到输入区浮层,并按字段标签提交摘要", async () => {
|
||||
mockUseAgentChatUnified.mockImplementation(
|
||||
({ workspaceId }: { workspaceId: string }) => {
|
||||
observedWorkspaceIds.push(workspaceId);
|
||||
return {
|
||||
providerType: "kiro",
|
||||
setProviderType: vi.fn(),
|
||||
model: "mock-model",
|
||||
setModel: vi.fn(),
|
||||
executionStrategy: "auto",
|
||||
setExecutionStrategy: vi.fn(),
|
||||
messages: [
|
||||
{
|
||||
id: "msg-legacy-user",
|
||||
role: "user",
|
||||
content: "帮我先梳理需求",
|
||||
timestamp: new Date("2026-03-15T09:00:00.000Z"),
|
||||
},
|
||||
{
|
||||
id: "msg-legacy-assistant",
|
||||
role: "assistant",
|
||||
content: `为了继续推进,我需要你先补充以下信息:
|
||||
|
||||
1. 目标与对象
|
||||
- 这次内容主要面向谁?(客户 / 上级 / 同事)
|
||||
- 这次最想达成的目标是什么?
|
||||
|
||||
2. 风格与限制
|
||||
- 语气偏好:正式严谨 / 友好专业 / 直接高效
|
||||
- 是否需要加入明确行动号召?`,
|
||||
timestamp: new Date("2026-03-15T09:00:01.000Z"),
|
||||
},
|
||||
],
|
||||
isSending: false,
|
||||
sendMessage: sharedSendMessageMock,
|
||||
stopSending: vi.fn(async () => undefined),
|
||||
clearMessages: vi.fn(),
|
||||
deleteMessage: vi.fn(),
|
||||
editMessage: vi.fn(),
|
||||
handlePermissionResponse: vi.fn(),
|
||||
triggerAIGuide: sharedTriggerAIGuideMock,
|
||||
topics: [
|
||||
{
|
||||
id: "topic-a",
|
||||
title: "话题 A",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
],
|
||||
sessionId: "session-1",
|
||||
switchTopic: sharedSwitchTopicMock,
|
||||
deleteTopic: vi.fn(),
|
||||
renameTopic: vi.fn(),
|
||||
workspacePathMissing: false,
|
||||
fixWorkspacePathAndRetry: vi.fn(),
|
||||
dismissWorkspacePathError: vi.fn(),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
renderPage({
|
||||
projectId: "project-legacy-a2ui",
|
||||
theme: "general",
|
||||
lockTheme: true,
|
||||
});
|
||||
await flushEffects(10);
|
||||
|
||||
const latestMessageListProps = mockMessageList.mock.calls.at(-1)?.[0] as
|
||||
| {
|
||||
messages?: Array<Record<string, unknown>>;
|
||||
}
|
||||
| undefined;
|
||||
expect(latestMessageListProps?.messages?.[1]?.content).toBe(
|
||||
"已整理为补充信息表单,请在输入区完成填写。",
|
||||
expect(latestMessageListProps?.messages?.[1]?.content).toContain(
|
||||
"为了继续推进,我需要你先补充以下信息",
|
||||
);
|
||||
expect(latestMessageListProps?.messages?.[1]?.content).toContain(
|
||||
"这次内容主要面向谁?",
|
||||
);
|
||||
|
||||
const latestPendingPanelProps =
|
||||
mockWorkspacePendingA2UIPanel.mock.calls.at(-1)?.[0] as
|
||||
| {
|
||||
pendingA2UIForm?: {
|
||||
data?: Record<string, unknown>;
|
||||
components?: Array<Record<string, unknown>>;
|
||||
id?: string;
|
||||
} | null;
|
||||
a2uiSubmissionNotice?: {
|
||||
title?: string;
|
||||
summary?: string;
|
||||
} | null;
|
||||
onA2UISubmit?: (formData: Record<string, unknown>) => void;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
expect(latestPendingPanelProps?.pendingA2UIForm).toBeTruthy();
|
||||
expect(latestPendingPanelProps?.pendingA2UIForm?.data).toMatchObject({
|
||||
source: "legacy_questionnaire",
|
||||
questionCount: 1,
|
||||
governance: expect.objectContaining({
|
||||
originalQuestionCount: 4,
|
||||
deferredQuestionCount: 3,
|
||||
}),
|
||||
});
|
||||
expect(latestPendingPanelProps?.pendingA2UIForm?.components || []).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
component: "ChoicePicker",
|
||||
label: "这次内容主要面向谁?",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(
|
||||
latestPendingPanelProps?.pendingA2UIForm?.components || [],
|
||||
).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
label: "这次最想达成的目标是什么?",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
label: "语气偏好",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
label: "是否需要加入明确行动号召?",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
const componentIdByLabel = Object.fromEntries(
|
||||
(latestPendingPanelProps?.pendingA2UIForm?.components || [])
|
||||
.filter(
|
||||
(component) =>
|
||||
(component.component === "ChoicePicker" ||
|
||||
component.component === "TextField") &&
|
||||
typeof component.label === "string" &&
|
||||
typeof component.id === "string",
|
||||
)
|
||||
.map((component) => [component.label, component.id]),
|
||||
);
|
||||
|
||||
act(() => {
|
||||
latestPendingPanelProps?.onA2UISubmit?.({
|
||||
[componentIdByLabel["这次内容主要面向谁?"]]: ["客户"],
|
||||
});
|
||||
});
|
||||
|
||||
expect(sharedSendMessageMock).toHaveBeenCalledWith(
|
||||
`我的选择:
|
||||
- 这次内容主要面向谁?: 客户`,
|
||||
[],
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
requestMetadata: {
|
||||
elicitation_context: expect.objectContaining({
|
||||
source: "legacy_questionnaire",
|
||||
mode: "compatibility_bridge",
|
||||
question_count: 1,
|
||||
governance: expect.objectContaining({
|
||||
originalQuestionCount: 4,
|
||||
deferredQuestionCount: 3,
|
||||
}),
|
||||
entries: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
label: "这次内容主要面向谁?",
|
||||
value: "客户",
|
||||
summary: "客户",
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(latestPendingPanelProps?.pendingA2UIForm ?? null).toBeNull();
|
||||
expect(latestPendingPanelProps?.a2uiSubmissionNotice ?? null).toBeNull();
|
||||
});
|
||||
|
||||
it("ask/tool_calls 残留问卷正文也应提升为输入区 A2UI,而不是原样展示", async () => {
|
||||
mockUseAgentChatUnified.mockImplementation(
|
||||
({ workspaceId }: { workspaceId: string }) => {
|
||||
observedWorkspaceIds.push(workspaceId);
|
||||
return {
|
||||
providerType: "kiro",
|
||||
setProviderType: vi.fn(),
|
||||
model: "mock-model",
|
||||
setModel: vi.fn(),
|
||||
executionStrategy: "auto",
|
||||
setExecutionStrategy: vi.fn(),
|
||||
messages: [
|
||||
{
|
||||
id: "msg-legacy-user",
|
||||
role: "user",
|
||||
content: "请先做网页研究简报",
|
||||
timestamp: new Date("2026-03-15T09:00:00.000Z"),
|
||||
},
|
||||
{
|
||||
id: "msg-legacy-assistant-compat-ask",
|
||||
role: "assistant",
|
||||
content: `我注意到您想让我做“网页研究简报”,但您没有指定具体的研究主题。
|
||||
it("ask/tool_calls 残留问卷正文应原样显示且不生成 pending A2UI", async () => {
|
||||
installMockAgentChatUnifiedState(
|
||||
createMockAgentChatUnifiedState({
|
||||
messages: [
|
||||
{
|
||||
id: "msg-legacy-user",
|
||||
role: "user",
|
||||
content: "请先做网页研究简报",
|
||||
timestamp: new Date("2026-03-15T09:00:00.000Z"),
|
||||
},
|
||||
{
|
||||
id: "msg-legacy-assistant-compat-ask",
|
||||
role: "assistant",
|
||||
content: `我注意到您想让我做“网页研究简报”,但您没有指定具体的研究主题。
|
||||
|
||||
我注意到您想让我做“网页研究简报”,但您没有指定具体的研究主题。
|
||||
|
||||
@@ -6156,33 +5970,10 @@ ask<arg_key>question</arg_key><arg_key>arg_value>请提供您希望我研究的
|
||||
- 其他您关心的主题
|
||||
|
||||
另外,请告诉我该研究的主要目的是什么?</arg_value></tool_calls>`,
|
||||
timestamp: new Date("2026-03-15T09:00:01.000Z"),
|
||||
},
|
||||
],
|
||||
isSending: false,
|
||||
sendMessage: sharedSendMessageMock,
|
||||
stopSending: vi.fn(async () => undefined),
|
||||
clearMessages: vi.fn(),
|
||||
deleteMessage: vi.fn(),
|
||||
editMessage: vi.fn(),
|
||||
handlePermissionResponse: vi.fn(),
|
||||
triggerAIGuide: sharedTriggerAIGuideMock,
|
||||
topics: [
|
||||
{
|
||||
id: "topic-a",
|
||||
title: "话题 A",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
],
|
||||
sessionId: "session-1",
|
||||
switchTopic: sharedSwitchTopicMock,
|
||||
deleteTopic: vi.fn(),
|
||||
renameTopic: vi.fn(),
|
||||
workspacePathMissing: false,
|
||||
fixWorkspacePathAndRetry: vi.fn(),
|
||||
dismissWorkspacePathError: vi.fn(),
|
||||
};
|
||||
},
|
||||
timestamp: new Date("2026-03-15T09:00:01.000Z"),
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
renderPage({
|
||||
@@ -6197,185 +5988,38 @@ ask<arg_key>question</arg_key><arg_key>arg_value>请提供您希望我研究的
|
||||
messages?: Array<Record<string, unknown>>;
|
||||
}
|
||||
| undefined;
|
||||
expect(latestMessageListProps?.messages?.[1]?.content).toBe(
|
||||
"已整理为补充信息表单,请在输入区完成填写。",
|
||||
expect(latestMessageListProps?.messages?.[1]?.content).toContain(
|
||||
"请提供您希望我研究的具体主题",
|
||||
);
|
||||
expect(latestMessageListProps?.messages?.[1]?.content).toContain(
|
||||
"</tool_calls>",
|
||||
);
|
||||
|
||||
const latestPendingPanelProps =
|
||||
mockWorkspacePendingA2UIPanel.mock.calls.at(-1)?.[0] as
|
||||
| {
|
||||
pendingA2UIForm?: {
|
||||
data?: Record<string, unknown>;
|
||||
components?: Array<Record<string, unknown>>;
|
||||
id?: string;
|
||||
} | null;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
expect(latestPendingPanelProps?.pendingA2UIForm?.data).toMatchObject({
|
||||
source: "legacy_questionnaire",
|
||||
sectionCount: 1,
|
||||
questionCount: 1,
|
||||
governance: expect.objectContaining({
|
||||
originalQuestionCount: 2,
|
||||
deferredQuestionCount: 1,
|
||||
}),
|
||||
});
|
||||
expect(
|
||||
(latestPendingPanelProps?.pendingA2UIForm?.components || []).some(
|
||||
(component) =>
|
||||
component.component === "TextField" &&
|
||||
component.label === "请提供您希望我研究的具体主题",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(latestPendingPanelProps?.pendingA2UIForm ?? null).toBeNull();
|
||||
});
|
||||
|
||||
it("普通中文澄清问题也应提升为输入区 A2UI,而不是继续停留在正文里", async () => {
|
||||
mockUseAgentChatUnified.mockImplementation(
|
||||
({ workspaceId }: { workspaceId: string }) => {
|
||||
observedWorkspaceIds.push(workspaceId);
|
||||
return {
|
||||
providerType: "kiro",
|
||||
setProviderType: vi.fn(),
|
||||
model: "mock-model",
|
||||
setModel: vi.fn(),
|
||||
executionStrategy: "auto",
|
||||
setExecutionStrategy: vi.fn(),
|
||||
messages: [
|
||||
{
|
||||
id: "msg-plain-legacy-user",
|
||||
role: "user",
|
||||
content: "围绕这个主题做研究",
|
||||
timestamp: new Date("2026-03-15T09:00:00.000Z"),
|
||||
},
|
||||
{
|
||||
id: "msg-plain-legacy-assistant",
|
||||
role: "assistant",
|
||||
content: `我需要先明确一下:您希望我研究哪个主题?您的消息中提到了“围绕这个主题”,但没有具体说明主题内容。请告诉我:
|
||||
|
||||
- 具体的研究主题(例如:某个产品、技术、公司、市场趋势、政策、事件等)
|
||||
- 研究目的(例如:投资决策、技术选型、竞争分析、学习了解等)
|
||||
- 是否有特定关注点(例如:风险、机会、对比、最新动态等)
|
||||
|
||||
一旦您明确了主题,我会:
|
||||
1. 使用联网搜索获取最新信息
|
||||
2. 整理关键来源和核心发现
|
||||
3. 识别风险点和待追踪问题
|
||||
4. 输出一版结构化的研究简报`,
|
||||
timestamp: new Date("2026-03-15T09:00:01.000Z"),
|
||||
},
|
||||
],
|
||||
isSending: false,
|
||||
sendMessage: sharedSendMessageMock,
|
||||
stopSending: vi.fn(async () => undefined),
|
||||
clearMessages: vi.fn(),
|
||||
deleteMessage: vi.fn(),
|
||||
editMessage: vi.fn(),
|
||||
handlePermissionResponse: vi.fn(),
|
||||
triggerAIGuide: sharedTriggerAIGuideMock,
|
||||
topics: [
|
||||
{
|
||||
id: "topic-a",
|
||||
title: "话题 A",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
],
|
||||
sessionId: "session-1",
|
||||
switchTopic: sharedSwitchTopicMock,
|
||||
deleteTopic: vi.fn(),
|
||||
renameTopic: vi.fn(),
|
||||
workspacePathMissing: false,
|
||||
fixWorkspacePathAndRetry: vi.fn(),
|
||||
dismissWorkspacePathError: vi.fn(),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
renderPage({
|
||||
projectId: "project-plain-legacy-a2ui",
|
||||
theme: "general",
|
||||
lockTheme: true,
|
||||
});
|
||||
await flushEffects(10);
|
||||
|
||||
const latestMessageListProps = mockMessageList.mock.calls.at(-1)?.[0] as
|
||||
| {
|
||||
messages?: Array<Record<string, unknown>>;
|
||||
}
|
||||
| undefined;
|
||||
expect(latestMessageListProps?.messages?.[1]?.content).toBe(
|
||||
"已整理为补充信息表单,请在输入区完成填写。",
|
||||
);
|
||||
|
||||
const latestPendingPanelProps =
|
||||
mockWorkspacePendingA2UIPanel.mock.calls.at(-1)?.[0] as
|
||||
| {
|
||||
pendingA2UIForm?: {
|
||||
data?: Record<string, unknown>;
|
||||
components?: Array<Record<string, unknown>>;
|
||||
} | null;
|
||||
}
|
||||
| undefined;
|
||||
|
||||
expect(latestPendingPanelProps?.pendingA2UIForm?.data).toMatchObject({
|
||||
source: "legacy_questionnaire",
|
||||
sectionCount: 1,
|
||||
questionCount: 1,
|
||||
governance: expect.objectContaining({
|
||||
originalQuestionCount: 3,
|
||||
deferredQuestionCount: 2,
|
||||
}),
|
||||
});
|
||||
expect(latestPendingPanelProps?.pendingA2UIForm?.components || []).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
component: "TextField",
|
||||
label: "具体的研究主题",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(
|
||||
(latestPendingPanelProps?.pendingA2UIForm?.components || []).filter(
|
||||
(component) => component.component === "TextField",
|
||||
),
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
latestPendingPanelProps?.pendingA2UIForm?.components || [],
|
||||
).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
component: "TextField",
|
||||
label: "研究目的",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
component: "TextField",
|
||||
label: "是否有特定关注点",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it("问卷已提交后,消息区应折叠为简短确认而不是继续展示完整题面", async () => {
|
||||
mockUseAgentChatUnified.mockImplementation(
|
||||
({ workspaceId }: { workspaceId: string }) => {
|
||||
observedWorkspaceIds.push(workspaceId);
|
||||
return {
|
||||
providerType: "kiro",
|
||||
setProviderType: vi.fn(),
|
||||
model: "mock-model",
|
||||
setModel: vi.fn(),
|
||||
executionStrategy: "auto",
|
||||
setExecutionStrategy: vi.fn(),
|
||||
messages: [
|
||||
{
|
||||
id: "msg-legacy-user",
|
||||
role: "user",
|
||||
content: "帮我先梳理需求",
|
||||
timestamp: new Date("2026-03-15T09:00:00.000Z"),
|
||||
},
|
||||
{
|
||||
id: "msg-legacy-assistant",
|
||||
role: "assistant",
|
||||
content: `为了继续推进,我需要你先补充以下信息:
|
||||
it("历史问卷已出现用户补充摘要时也不应折叠原正文", async () => {
|
||||
installMockAgentChatUnifiedState(
|
||||
createMockAgentChatUnifiedState({
|
||||
messages: [
|
||||
{
|
||||
id: "msg-legacy-user",
|
||||
role: "user",
|
||||
content: "帮我先梳理需求",
|
||||
timestamp: new Date("2026-03-15T09:00:00.000Z"),
|
||||
},
|
||||
{
|
||||
id: "msg-legacy-assistant",
|
||||
role: "assistant",
|
||||
content: `为了继续推进,我需要你先补充以下信息:
|
||||
|
||||
1. 目标与对象
|
||||
- 这次内容主要面向谁?(客户 / 上级 / 同事)
|
||||
@@ -6384,41 +6028,18 @@ ask<arg_key>question</arg_key><arg_key>arg_value>请提供您希望我研究的
|
||||
2. 风格与限制
|
||||
- 语气偏好:正式严谨 / 友好专业 / 直接高效
|
||||
- 是否需要加入明确行动号召?`,
|
||||
timestamp: new Date("2026-03-15T09:00:01.000Z"),
|
||||
},
|
||||
{
|
||||
id: "msg-legacy-summary",
|
||||
role: "user",
|
||||
content: `我的选择:
|
||||
timestamp: new Date("2026-03-15T09:00:01.000Z"),
|
||||
},
|
||||
{
|
||||
id: "msg-legacy-summary",
|
||||
role: "user",
|
||||
content: `我的选择:
|
||||
- 这次内容主要面向谁?: 客户
|
||||
- 这次最想达成的目标是什么?: 帮助市场团队统一宣传口径`,
|
||||
timestamp: new Date("2026-03-15T09:01:00.000Z"),
|
||||
},
|
||||
],
|
||||
isSending: false,
|
||||
sendMessage: sharedSendMessageMock,
|
||||
stopSending: vi.fn(async () => undefined),
|
||||
clearMessages: vi.fn(),
|
||||
deleteMessage: vi.fn(),
|
||||
editMessage: vi.fn(),
|
||||
handlePermissionResponse: vi.fn(),
|
||||
triggerAIGuide: sharedTriggerAIGuideMock,
|
||||
topics: [
|
||||
{
|
||||
id: "topic-a",
|
||||
title: "话题 A",
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
],
|
||||
sessionId: "session-1",
|
||||
switchTopic: sharedSwitchTopicMock,
|
||||
deleteTopic: vi.fn(),
|
||||
renameTopic: vi.fn(),
|
||||
workspacePathMissing: false,
|
||||
fixWorkspacePathAndRetry: vi.fn(),
|
||||
dismissWorkspacePathError: vi.fn(),
|
||||
};
|
||||
},
|
||||
timestamp: new Date("2026-03-15T09:01:00.000Z"),
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
renderPage({
|
||||
@@ -6433,12 +6054,30 @@ ask<arg_key>question</arg_key><arg_key>arg_value>请提供您希望我研究的
|
||||
messages?: Array<Record<string, unknown>>;
|
||||
}
|
||||
| undefined;
|
||||
expect(latestMessageListProps?.messages?.[1]?.content).toBe(
|
||||
expect(latestMessageListProps?.messages?.[1]?.content).toContain(
|
||||
"为了继续推进,我需要你先补充以下信息",
|
||||
);
|
||||
expect(latestMessageListProps?.messages?.[1]?.content).not.toBe(
|
||||
"补充信息表单已提交。",
|
||||
);
|
||||
|
||||
const latestPendingPanelProps =
|
||||
mockWorkspacePendingA2UIPanel.mock.calls.at(-1)?.[0] as
|
||||
| {
|
||||
pendingA2UIForm?: {
|
||||
id?: string;
|
||||
} | null;
|
||||
a2uiSubmissionNotice?: {
|
||||
title?: string;
|
||||
summary?: string;
|
||||
} | null;
|
||||
}
|
||||
| undefined;
|
||||
expect(latestPendingPanelProps?.pendingA2UIForm ?? null).toBeNull();
|
||||
expect(latestPendingPanelProps?.a2uiSubmissionNotice ?? null).toBeNull();
|
||||
});
|
||||
|
||||
it("真实 action_required 存在时,不应被 legacy 折叠逻辑覆盖", async () => {
|
||||
it("真实 action_required 存在时,应优先提升当前 A2UI", async () => {
|
||||
mockUseAgentChatUnified.mockImplementation(
|
||||
({ workspaceId }: { workspaceId: string }) => {
|
||||
observedWorkspaceIds.push(workspaceId);
|
||||
@@ -6543,7 +6182,7 @@ ask<arg_key>question</arg_key><arg_key>arg_value>请提供您希望我研究的
|
||||
);
|
||||
});
|
||||
|
||||
it("真实 action_required 已提交后,输入区应显示补充信息确认提示而不是继续停留在表单态", async () => {
|
||||
it("真实 action_required 已提交后,不应残留表单或旧兼容提示", async () => {
|
||||
mockUseAgentChatUnified.mockImplementation(
|
||||
({ workspaceId }: { workspaceId: string }) => {
|
||||
observedWorkspaceIds.push(workspaceId);
|
||||
|
||||
@@ -4,8 +4,7 @@ import { buildTeamWorkspaceBoardChromeDisplayState } from "./boardChromeSelector
|
||||
describe("boardChromeSelectors", () => {
|
||||
it("真实成员图应产出紧凑 chrome 标题与工具条 chips", () => {
|
||||
const state = buildTeamWorkspaceBoardChromeDisplayState({
|
||||
hasRealTeamGraph: true,
|
||||
hasRuntimeFormation: false,
|
||||
hasRuntimeSessions: true,
|
||||
isChildSession: false,
|
||||
totalTeamSessions: 2,
|
||||
siblingCount: 0,
|
||||
@@ -26,19 +25,20 @@ describe("boardChromeSelectors", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(state.boardHeadline).toBe("2 位成员协作中");
|
||||
expect(state.boardHeadline).toBe("任务进行中 · 1 项处理中 / 1 项稍后开始");
|
||||
expect(state.boardHint).toBe(
|
||||
"这里只展示谁在帮你处理什么、处理到哪一步,以及接下来会给你什么结果。",
|
||||
"这里只展示当前有哪些任务在处理、状态如何,以及最近更新到了哪里。",
|
||||
);
|
||||
expect(state.compactBoardHeadline).toBe(
|
||||
"任务进行中 · 1 项处理中 / 1 项稍后开始",
|
||||
);
|
||||
expect(state.compactBoardHeadline).toBe("2 位成员协作中");
|
||||
expect(state.compactToolbarChips).toEqual([
|
||||
{ key: "focus", text: "焦点 研究员", tone: "summary" },
|
||||
{ key: "focus", text: "当前焦点 研究员", tone: "summary" },
|
||||
{ key: "status", text: "处理中", tone: "status", status: "running" },
|
||||
{ key: "updated-at", text: "更新于 刚刚", tone: "muted" },
|
||||
{ key: "current", text: "当前对话", tone: "muted" },
|
||||
{ key: "zoom", text: "缩放 100%", tone: "muted" },
|
||||
{ key: "waitable", text: "2 位处理中", tone: "muted" },
|
||||
{ key: "completed", text: "1 位已完成", tone: "muted" },
|
||||
{ key: "current", text: "当前任务", tone: "muted" },
|
||||
{ key: "waitable", text: "2 项处理中", tone: "muted" },
|
||||
{ key: "completed", text: "1 项已完成", tone: "muted" },
|
||||
]);
|
||||
expect(state.statusSummaryBadges).toEqual([
|
||||
{ key: "running", text: "处理中 1", status: "running" },
|
||||
@@ -48,10 +48,9 @@ describe("boardChromeSelectors", () => {
|
||||
|
||||
it("forming 阶段应优先显示 runtime formation 的标题与提示", () => {
|
||||
const state = buildTeamWorkspaceBoardChromeDisplayState({
|
||||
hasRealTeamGraph: false,
|
||||
hasRuntimeFormation: true,
|
||||
hasRuntimeSessions: false,
|
||||
runtimeFormationTitle: "任务分工已准备好",
|
||||
runtimeFormationHint: "当前任务的分工已经准备好,成员加入后会继续接手处理。",
|
||||
runtimeFormationHint: "当前任务的分工已经准备好,任务拆出后会继续接手处理。",
|
||||
isChildSession: false,
|
||||
totalTeamSessions: 0,
|
||||
siblingCount: 0,
|
||||
@@ -65,18 +64,16 @@ describe("boardChromeSelectors", () => {
|
||||
});
|
||||
|
||||
expect(state.boardHeadline).toBe("任务分工已准备好");
|
||||
expect(state.boardHint).toBe("当前任务的分工已经准备好,成员加入后会继续接手处理。");
|
||||
expect(state.boardHint).toBe("当前任务的分工已经准备好,任务拆出后会继续接手处理。");
|
||||
expect(state.compactBoardHeadline).toBe("任务分工已准备好");
|
||||
expect(state.compactToolbarChips).toEqual([
|
||||
{ key: "focus", text: "等待成员接入", tone: "summary" },
|
||||
{ key: "zoom", text: "缩放 88%", tone: "muted" },
|
||||
{ key: "focus", text: "等待任务接手", tone: "summary" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("子线程应优先显示父会话标题与 sibling 提示", () => {
|
||||
const state = buildTeamWorkspaceBoardChromeDisplayState({
|
||||
hasRealTeamGraph: true,
|
||||
hasRuntimeFormation: false,
|
||||
hasRuntimeSessions: true,
|
||||
isChildSession: true,
|
||||
parentSessionName: "主线程总览",
|
||||
totalTeamSessions: 1,
|
||||
@@ -93,7 +90,7 @@ describe("boardChromeSelectors", () => {
|
||||
});
|
||||
|
||||
expect(state.boardHeadline).toBe("主线程总览");
|
||||
expect(state.boardHint).toBe("当前正与 2 位协作成员并行推进");
|
||||
expect(state.boardHint).toBe("当前任务正与 2 项并行子任务一起推进");
|
||||
expect(state.compactBoardHeadline).toBe("主线程总览");
|
||||
expect(state.statusSummaryBadges).toEqual([
|
||||
{ key: "completed", text: "已完成 1", status: "completed" },
|
||||
|
||||
@@ -41,50 +41,79 @@ function formatUpdatedAt(updatedAt?: number) {
|
||||
}
|
||||
|
||||
function buildBoardHeadline(params: {
|
||||
hasRealTeamGraph: boolean;
|
||||
hasRuntimeSessions: boolean;
|
||||
isChildSession: boolean;
|
||||
parentSessionName?: string | null;
|
||||
statusSummary: Record<string, number>;
|
||||
totalTeamSessions: number;
|
||||
}) {
|
||||
const {
|
||||
hasRealTeamGraph,
|
||||
hasRuntimeSessions,
|
||||
isChildSession,
|
||||
parentSessionName,
|
||||
statusSummary,
|
||||
totalTeamSessions,
|
||||
} = params;
|
||||
const runningCount = statusSummary.running ?? 0;
|
||||
const queuedCount = statusSummary.queued ?? 0;
|
||||
const completedCount = statusSummary.completed ?? 0;
|
||||
const retryCount =
|
||||
(statusSummary.failed ?? 0) + (statusSummary.aborted ?? 0);
|
||||
|
||||
if (isChildSession) {
|
||||
return parentSessionName?.trim() || "主助手协作区";
|
||||
return parentSessionName?.trim() || "主任务总览";
|
||||
}
|
||||
if (!hasRealTeamGraph) {
|
||||
return "需要时会自动接入协作成员";
|
||||
if (!hasRuntimeSessions) {
|
||||
return "需要时会自动拆出任务";
|
||||
}
|
||||
if (runningCount > 0) {
|
||||
if (queuedCount > 0) {
|
||||
return `任务进行中 · ${runningCount} 项处理中 / ${queuedCount} 项稍后开始`;
|
||||
}
|
||||
return totalTeamSessions > 1
|
||||
? `任务进行中 · ${runningCount} 项处理中`
|
||||
: "任务进行中";
|
||||
}
|
||||
if (queuedCount > 0) {
|
||||
return totalTeamSessions > 1
|
||||
? `任务准备中 · ${queuedCount} 项稍后开始`
|
||||
: "任务准备中";
|
||||
}
|
||||
if (completedCount > 0 && completedCount === totalTeamSessions) {
|
||||
return totalTeamSessions > 1
|
||||
? `${completedCount} 项任务已完成`
|
||||
: "任务已完成";
|
||||
}
|
||||
if (retryCount > 0 && retryCount === totalTeamSessions) {
|
||||
return totalTeamSessions > 1
|
||||
? `${retryCount} 项任务需重试`
|
||||
: "任务需重试";
|
||||
}
|
||||
return totalTeamSessions > 0
|
||||
? `${totalTeamSessions} 位成员协作中`
|
||||
? `${totalTeamSessions} 项任务已接入`
|
||||
: TEAM_WORKSPACE_SURFACE_TITLE;
|
||||
}
|
||||
|
||||
function buildBoardHint(params: {
|
||||
hasRealTeamGraph: boolean;
|
||||
hasRuntimeSessions: boolean;
|
||||
isChildSession: boolean;
|
||||
siblingCount: number;
|
||||
}) {
|
||||
const { hasRealTeamGraph, isChildSession, siblingCount } = params;
|
||||
const { hasRuntimeSessions, isChildSession, siblingCount } = params;
|
||||
|
||||
if (isChildSession) {
|
||||
return siblingCount > 0
|
||||
? `当前正与 ${siblingCount} 位协作成员并行推进`
|
||||
: "当前由你和这位协作成员一起推进";
|
||||
? `当前任务正与 ${siblingCount} 项并行子任务一起推进`
|
||||
: "当前正在处理这项子任务,结果会回流到主任务。";
|
||||
}
|
||||
if (!hasRealTeamGraph) {
|
||||
return "系统会在需要时自动安排任务分工并接入协作成员,你只需要关注当前任务和结果。";
|
||||
if (!hasRuntimeSessions) {
|
||||
return "系统会在需要时自动拆出任务、安排处理顺序,并把结果回流到当前任务。";
|
||||
}
|
||||
return "这里只展示谁在帮你处理什么、处理到哪一步,以及接下来会给你什么结果。";
|
||||
return "这里只展示当前有哪些任务在处理、状态如何,以及最近更新到了哪里。";
|
||||
}
|
||||
|
||||
export function buildTeamWorkspaceBoardChromeDisplayState(params: {
|
||||
hasRealTeamGraph: boolean;
|
||||
hasRuntimeFormation: boolean;
|
||||
hasRuntimeSessions: boolean;
|
||||
runtimeFormationTitle?: string | null;
|
||||
runtimeFormationHint?: string | null;
|
||||
isChildSession: boolean;
|
||||
@@ -100,36 +129,29 @@ export function buildTeamWorkspaceBoardChromeDisplayState(params: {
|
||||
statusSummary: Record<string, number>;
|
||||
}): TeamWorkspaceBoardChromeDisplayState {
|
||||
const boardHeadline =
|
||||
!params.hasRealTeamGraph && params.runtimeFormationTitle
|
||||
!params.hasRuntimeSessions && params.runtimeFormationTitle
|
||||
? params.runtimeFormationTitle
|
||||
: buildBoardHeadline({
|
||||
hasRealTeamGraph: params.hasRealTeamGraph,
|
||||
hasRuntimeSessions: params.hasRuntimeSessions,
|
||||
isChildSession: params.isChildSession,
|
||||
parentSessionName: params.parentSessionName,
|
||||
statusSummary: params.statusSummary,
|
||||
totalTeamSessions: params.totalTeamSessions,
|
||||
});
|
||||
const boardHint =
|
||||
!params.hasRealTeamGraph &&
|
||||
params.hasRuntimeFormation &&
|
||||
params.runtimeFormationHint
|
||||
!params.hasRuntimeSessions && params.runtimeFormationHint
|
||||
? params.runtimeFormationHint
|
||||
: buildBoardHint({
|
||||
hasRealTeamGraph: params.hasRealTeamGraph,
|
||||
hasRuntimeSessions: params.hasRuntimeSessions,
|
||||
isChildSession: params.isChildSession,
|
||||
siblingCount: params.siblingCount,
|
||||
});
|
||||
const compactBoardHeadline =
|
||||
params.hasRealTeamGraph &&
|
||||
!params.isChildSession &&
|
||||
params.totalTeamSessions > 0
|
||||
? `${params.totalTeamSessions} 位成员协作中`
|
||||
: boardHeadline;
|
||||
const compactToolbarChips: TeamWorkspaceBoardChromeChip[] = [
|
||||
{
|
||||
key: "focus",
|
||||
text: params.selectedSession
|
||||
? `焦点 ${params.selectedSession.name}`
|
||||
: "等待成员接入",
|
||||
? `当前焦点 ${params.selectedSession.name}`
|
||||
: "等待任务接手",
|
||||
tone: "summary",
|
||||
},
|
||||
...(params.selectedSession?.runtimeStatus
|
||||
@@ -157,21 +179,16 @@ export function buildTeamWorkspaceBoardChromeDisplayState(params: {
|
||||
? [
|
||||
{
|
||||
key: "current",
|
||||
text: "当前对话",
|
||||
text: "当前任务",
|
||||
tone: "muted" as const,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
key: "zoom",
|
||||
text: `缩放 ${Math.round(params.zoom * 100)}%`,
|
||||
tone: "muted",
|
||||
},
|
||||
...(params.canWaitAnyActiveTeamSession
|
||||
? [
|
||||
{
|
||||
key: "waitable",
|
||||
text: `${params.waitableCount} 位处理中`,
|
||||
text: `${params.waitableCount} 项处理中`,
|
||||
tone: "muted" as const,
|
||||
},
|
||||
]
|
||||
@@ -180,7 +197,7 @@ export function buildTeamWorkspaceBoardChromeDisplayState(params: {
|
||||
? [
|
||||
{
|
||||
key: "completed",
|
||||
text: `${params.completedCount} 位已完成`,
|
||||
text: `${params.completedCount} 项已完成`,
|
||||
tone: "muted" as const,
|
||||
},
|
||||
]
|
||||
@@ -190,13 +207,15 @@ export function buildTeamWorkspaceBoardChromeDisplayState(params: {
|
||||
return {
|
||||
boardHeadline,
|
||||
boardHint,
|
||||
compactBoardHeadline,
|
||||
compactBoardHeadline: boardHeadline,
|
||||
compactToolbarChips,
|
||||
statusSummaryBadges: Object.entries(params.statusSummary)
|
||||
.filter(([, count]) => count > 0)
|
||||
.map(([status, count]) => {
|
||||
const normalizedStatus =
|
||||
status === "idle" ? undefined : (status as TeamWorkspaceRuntimeStatus);
|
||||
status === "idle"
|
||||
? undefined
|
||||
: (status as TeamWorkspaceRuntimeStatus);
|
||||
|
||||
return {
|
||||
key: status,
|
||||
|
||||
@@ -147,8 +147,8 @@ describe("canvasLaneSelectors", () => {
|
||||
title: "写作",
|
||||
badgeLabel: "正在处理",
|
||||
roleLabel: "撰写",
|
||||
statusHint: "这位协作成员正在处理",
|
||||
updatedAtLabel: "等待成员加入",
|
||||
statusHint: "这项任务正在处理",
|
||||
updatedAtLabel: "等待任务接手",
|
||||
previewText: "负责整理路线图结论。",
|
||||
}),
|
||||
]);
|
||||
@@ -179,7 +179,7 @@ describe("canvasLaneSelectors", () => {
|
||||
title: "复核",
|
||||
badgeLabel: "待开始",
|
||||
roleLabel: "复核",
|
||||
statusHint: "等待系统邀请协作成员加入",
|
||||
statusHint: "等待系统把这项任务拆出来",
|
||||
updatedAtLabel: "计划分工",
|
||||
previewText: "补测试并确认风险。",
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { AsterSubagentSkillInfo } from "@/lib/api/agentRuntime";
|
||||
import { formatRelativeTime } from "@/lib/api/project";
|
||||
import type { TeamRoleDefinition } from "../utils/teamDefinitions";
|
||||
import { normalizeTeamWorkspaceDisplayValue } from "../utils/teamWorkspaceDisplay";
|
||||
import { getTeamPresetOption } from "../utils/teamPresets";
|
||||
import {
|
||||
buildTeamWorkspaceSkillDisplayName,
|
||||
@@ -186,19 +187,22 @@ function buildSessionLaneEmptyState(params: {
|
||||
const { session, previewState } = params;
|
||||
|
||||
if (previewState?.status === "error") {
|
||||
return previewState.errorMessage?.trim() || "同步最新内容失败";
|
||||
return (
|
||||
normalizeTeamWorkspaceDisplayValue(previewState.errorMessage) ||
|
||||
"同步最新内容失败"
|
||||
);
|
||||
}
|
||||
|
||||
if (previewState?.status === "loading") {
|
||||
return "正在同步这位协作成员的最新内容...";
|
||||
return "正在同步这项任务的最新内容...";
|
||||
}
|
||||
|
||||
if (session?.runtimeStatus === "queued") {
|
||||
return "这位协作成员已经收到任务,马上开始处理。";
|
||||
return "这项任务已经收到安排,马上开始处理。";
|
||||
}
|
||||
|
||||
if (session?.runtimeStatus === "running") {
|
||||
return "这位协作成员正在处理,最新进展会持续刷新到这里。";
|
||||
return "这项任务正在处理,最新进展会持续刷新到这里。";
|
||||
}
|
||||
|
||||
if (session?.runtimeStatus === "completed") {
|
||||
@@ -212,7 +216,7 @@ function buildSessionLaneEmptyState(params: {
|
||||
return "这一步没有顺利完成,你可以在下方查看细节并决定是否继续。";
|
||||
}
|
||||
|
||||
return "这位协作成员暂时还没有产出可展示的内容。";
|
||||
return "这项任务暂时还没有产出可展示的内容。";
|
||||
}
|
||||
|
||||
function buildCanvasLaneTitleSummary(
|
||||
@@ -221,9 +225,9 @@ function buildCanvasLaneTitleSummary(
|
||||
const memberMeta = resolveRuntimeMemberStatusMeta(member.status);
|
||||
const statusHint =
|
||||
member.status === "spawning"
|
||||
? "正在接入协作成员"
|
||||
? "正在接入这项任务"
|
||||
: member.status === "running"
|
||||
? "这位协作成员正在处理"
|
||||
? "这项任务正在处理"
|
||||
: member.status === "waiting"
|
||||
? "等待继续补充说明"
|
||||
: member.status === "completed"
|
||||
@@ -231,8 +235,8 @@ function buildCanvasLaneTitleSummary(
|
||||
: member.status === "failed"
|
||||
? "这一步需要重试"
|
||||
: member.sessionId
|
||||
? "已连接到真实成员"
|
||||
: "等待成员接入";
|
||||
? "已连接到真实任务"
|
||||
: "等待任务接手";
|
||||
|
||||
return {
|
||||
badgeLabel: memberMeta.label,
|
||||
@@ -245,7 +249,7 @@ function buildCanvasLaneTitleSummary(
|
||||
: member.status === "waiting"
|
||||
? "bg-amber-400"
|
||||
: "bg-sky-500",
|
||||
summary: member.summary,
|
||||
summary: normalizeTeamWorkspaceDisplayValue(member.summary) || member.summary,
|
||||
statusHint,
|
||||
};
|
||||
}
|
||||
@@ -255,8 +259,8 @@ function buildPlannedRoleLaneSummary(role: TeamRoleDefinition) {
|
||||
badgeLabel: "待开始",
|
||||
badgeClassName: "border border-slate-200 bg-slate-50 text-slate-600",
|
||||
dotClassName: "bg-slate-300",
|
||||
summary: role.summary,
|
||||
statusHint: "等待系统邀请协作成员加入",
|
||||
summary: normalizeTeamWorkspaceDisplayValue(role.summary) || role.summary,
|
||||
statusHint: "等待系统把这项任务拆出来",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -466,7 +470,7 @@ function buildSessionCanvasLane(params: {
|
||||
kind: "session",
|
||||
title: session.name,
|
||||
summary:
|
||||
session.taskSummary ||
|
||||
normalizeTeamWorkspaceDisplayValue(session.taskSummary) ||
|
||||
"暂时还没有任务摘要,打开详情后可查看完整上下文。",
|
||||
badgeLabel: meta.label,
|
||||
badgeClassName: meta.badgeClassName,
|
||||
@@ -537,9 +541,10 @@ export function buildTeamWorkspaceCanvasLanes(params: {
|
||||
resolveTeamWorkspaceRoleHintLabel(member.roleKey) || undefined,
|
||||
profileLabel: undefined,
|
||||
statusHint: laneSummary.statusHint,
|
||||
updatedAtLabel: "等待成员加入",
|
||||
updatedAtLabel: "等待任务接手",
|
||||
skillLabels: [],
|
||||
previewText: member.summary,
|
||||
previewText:
|
||||
normalizeTeamWorkspaceDisplayValue(member.summary) || member.summary,
|
||||
previewEntries: [],
|
||||
};
|
||||
});
|
||||
@@ -563,7 +568,7 @@ export function buildTeamWorkspaceCanvasLanes(params: {
|
||||
statusHint: laneSummary.statusHint,
|
||||
updatedAtLabel: "计划分工",
|
||||
skillLabels: [],
|
||||
previewText: role.summary,
|
||||
previewText: normalizeTeamWorkspaceDisplayValue(role.summary) || role.summary,
|
||||
previewEntries: [],
|
||||
};
|
||||
});
|
||||
|
||||
@@ -32,7 +32,7 @@ describe("formationDisplaySelectors", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("runtime formation 已就绪时,应产出状态、成员与参考分工", () => {
|
||||
it("runtime formation 已就绪时,应产出状态、任务与参考分工", () => {
|
||||
const state = buildRuntimeFormationDisplayState({
|
||||
teamDispatchPreviewState: {
|
||||
requestId: "runtime-1",
|
||||
@@ -66,9 +66,9 @@ describe("formationDisplaySelectors", () => {
|
||||
expect(state.hasRuntimeFormation).toBe(true);
|
||||
expect(state.hint).toContain("当前任务的分工已经准备好");
|
||||
expect(state.summaryBadges.map((badge) => badge.text)).toEqual([
|
||||
"任务方案 · 修复 Team",
|
||||
"任务方案 · 修复任务方案",
|
||||
"已就绪",
|
||||
"1 位当前成员",
|
||||
"1 项当前任务",
|
||||
"参考方案 · 代码排障团队",
|
||||
]);
|
||||
expect(state.panelHeadline).toBe("任务分工已准备好");
|
||||
@@ -81,6 +81,8 @@ describe("formationDisplaySelectors", () => {
|
||||
summary: "先定位问题与影响面。",
|
||||
});
|
||||
expect(state.noticeText).toContain("当前任务方案已就绪");
|
||||
expect(state.noticeText).toContain("任务拆出后");
|
||||
expect(state.noticeText).toContain("任务视图");
|
||||
});
|
||||
|
||||
it("runtime formation 失败时,应优先使用失败原因", () => {
|
||||
@@ -97,8 +99,8 @@ describe("formationDisplaySelectors", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(state.panelDescription).toBe("Provider 认证失败,无法生成 Team。");
|
||||
expect(state.emptyDetail).toBe("Provider 认证失败,无法生成 Team。");
|
||||
expect(state.noticeText).toBe("Provider 认证失败,无法生成 Team。");
|
||||
expect(state.panelDescription).toBe("Provider 认证失败,无法生成任务方案。");
|
||||
expect(state.emptyDetail).toBe("Provider 认证失败,无法生成任务方案。");
|
||||
expect(state.noticeText).toBe("Provider 认证失败,无法生成任务方案。");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { TeamRoleDefinition } from "../utils/teamDefinitions";
|
||||
import { normalizeTeamWorkspaceDisplayValue } from "../utils/teamWorkspaceDisplay";
|
||||
import {
|
||||
TEAM_WORKSPACE_IDLE_STATUS_LABEL,
|
||||
TEAM_WORKSPACE_PLAN_LABEL,
|
||||
@@ -60,31 +61,32 @@ export function buildRuntimeFormationHint(
|
||||
) {
|
||||
switch (teamDispatchPreviewState?.status) {
|
||||
case "forming":
|
||||
return "系统正在准备当前任务的分工,成员接入后会自动开始处理。";
|
||||
return "系统正在准备当前任务的分工,任务拆出后会自动开始处理。";
|
||||
case "formed":
|
||||
return "当前任务的分工已经准备好,成员加入后会继续接手处理。";
|
||||
return "当前任务的分工已经准备好,任务拆出后会继续接手处理。";
|
||||
case "failed":
|
||||
return "当前任务的分工准备失败,但你仍然可以继续在当前对话里推进。";
|
||||
return "当前任务的分工准备失败,但你仍然可以继续在当前任务里推进。";
|
||||
default:
|
||||
return "需要时这里会自动展开成任务协作面板。";
|
||||
return "需要时这里会自动展开成任务面板。";
|
||||
}
|
||||
}
|
||||
|
||||
export function buildRuntimeFormationEmptyDetail(
|
||||
teamDispatchPreviewState?: TeamWorkspaceRuntimeFormationState | null,
|
||||
) {
|
||||
const errorMessage = normalizeTeamWorkspaceDisplayValue(
|
||||
teamDispatchPreviewState?.errorMessage,
|
||||
);
|
||||
|
||||
switch (teamDispatchPreviewState?.status) {
|
||||
case "forming":
|
||||
return "系统正在根据当前任务准备分工。完成后,这里会先展示当前成员卡片,再接入真实处理进展。";
|
||||
return "系统正在根据当前任务准备分工。完成后,这里会先展示任务卡片,再接入真实处理进展。";
|
||||
case "formed":
|
||||
return "当前任务方案已经准备好。画布会先展示当前分工,等成员真正开始处理后,再自动切换为实时进展。";
|
||||
return "当前任务方案已经准备好。画布会先展示当前分工,等任务真正开始处理后,再自动切换为任务视图。";
|
||||
case "failed":
|
||||
return (
|
||||
teamDispatchPreviewState.errorMessage?.trim() ||
|
||||
"当前任务分工准备失败,暂时无法展示当前成员。"
|
||||
);
|
||||
return errorMessage || "当前任务分工准备失败,暂时无法展示当前任务。";
|
||||
default:
|
||||
return `${TEAM_WORKSPACE_IDLE_STATUS_LABEL}。系统开始分工后,详情区会切换为成员摘要视图。`;
|
||||
return `${TEAM_WORKSPACE_IDLE_STATUS_LABEL}。系统开始分工后,详情区会切换为任务摘要视图。`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,13 +95,19 @@ export function buildSelectedTeamPlanDisplayState(params: {
|
||||
selectedTeamSummary?: string | null;
|
||||
selectedTeamRoles?: TeamRoleDefinition[] | null;
|
||||
}): TeamWorkspaceSelectedTeamPlanDisplayState {
|
||||
const label = params.selectedTeamLabel?.trim() || null;
|
||||
const summary = params.selectedTeamSummary?.trim() || null;
|
||||
const label = normalizeTeamWorkspaceDisplayValue(params.selectedTeamLabel);
|
||||
const summary = normalizeTeamWorkspaceDisplayValue(params.selectedTeamSummary);
|
||||
const roleCards = (params.selectedTeamRoles ?? [])
|
||||
.filter((role) => role.label.trim())
|
||||
.map((role) => ({
|
||||
id: role.id,
|
||||
label: role.label,
|
||||
label: normalizeTeamWorkspaceDisplayValue(role.label),
|
||||
summary:
|
||||
normalizeTeamWorkspaceDisplayValue(role.summary) || role.summary.trim(),
|
||||
}))
|
||||
.filter((role) => Boolean(role.label))
|
||||
.map((role) => ({
|
||||
id: role.id,
|
||||
label: role.label || "",
|
||||
summary: role.summary,
|
||||
}));
|
||||
|
||||
@@ -141,41 +149,41 @@ export function buildRuntimeFormationDisplayState(params: {
|
||||
}): TeamWorkspaceRuntimeFormationDisplayState {
|
||||
const state = params.teamDispatchPreviewState ?? null;
|
||||
const meta = state ? resolveRuntimeFormationStatusMeta(state.status) : null;
|
||||
const label =
|
||||
state?.label?.trim() ||
|
||||
state?.blueprint?.label?.trim() ||
|
||||
params.fallbackLabel?.trim() ||
|
||||
null;
|
||||
const summary =
|
||||
state?.summary?.trim() ||
|
||||
state?.blueprint?.summary?.trim() ||
|
||||
params.fallbackSummary?.trim() ||
|
||||
null;
|
||||
const label = normalizeTeamWorkspaceDisplayValue(
|
||||
state?.label || state?.blueprint?.label || params.fallbackLabel,
|
||||
);
|
||||
const summary = normalizeTeamWorkspaceDisplayValue(
|
||||
state?.summary || state?.blueprint?.summary || params.fallbackSummary,
|
||||
);
|
||||
const errorMessage = normalizeTeamWorkspaceDisplayValue(state?.errorMessage);
|
||||
const referenceLabel = normalizeTeamWorkspaceDisplayValue(
|
||||
state?.blueprint?.label,
|
||||
);
|
||||
const memberCards = (state?.members ?? []).map((member) => {
|
||||
const memberMeta = resolveRuntimeMemberStatusMeta(member.status);
|
||||
return {
|
||||
id: member.id,
|
||||
label: member.label,
|
||||
summary: member.summary,
|
||||
label: normalizeTeamWorkspaceDisplayValue(member.label) || member.label,
|
||||
summary:
|
||||
normalizeTeamWorkspaceDisplayValue(member.summary) || member.summary,
|
||||
badgeLabel: memberMeta.label,
|
||||
badgeClassName: memberMeta.badgeClassName,
|
||||
};
|
||||
});
|
||||
const blueprintRoleCards = (state?.blueprint?.roles ?? []).map((role) => ({
|
||||
id: role.id,
|
||||
label: role.label,
|
||||
summary: role.summary,
|
||||
label: normalizeTeamWorkspaceDisplayValue(role.label) || role.label,
|
||||
summary: normalizeTeamWorkspaceDisplayValue(role.summary) || role.summary,
|
||||
}));
|
||||
|
||||
const noticeText =
|
||||
state?.status === "forming"
|
||||
? "系统正在准备当前任务分工,完成后会先展示成员卡片,后续再切换为独立的实时进展面板。"
|
||||
? "系统正在准备当前任务分工,完成后会先展示任务卡片,后续再切换为独立的任务视图。"
|
||||
: state?.status === "formed"
|
||||
? "当前任务方案已就绪。系统开始分工后,这里会从方案视图过渡到实时协作画布。"
|
||||
? "当前任务方案已就绪。任务拆出后,这里会从方案视图过渡到任务视图。"
|
||||
: state?.status === "failed"
|
||||
? state.errorMessage?.trim() ||
|
||||
"当前任务分工准备失败,暂时还没有成员接入。"
|
||||
: `${TEAM_WORKSPACE_IDLE_STATUS_LABEL}。系统开始分工后,这里会生成独立的成员进展画布。`;
|
||||
? errorMessage || "当前任务分工准备失败,暂时还没有任务接手。"
|
||||
: `${TEAM_WORKSPACE_IDLE_STATUS_LABEL}。系统开始分工后,这里会生成独立的任务视图。`;
|
||||
|
||||
return {
|
||||
hasRuntimeFormation: Boolean(state),
|
||||
@@ -206,17 +214,17 @@ export function buildRuntimeFormationDisplayState(params: {
|
||||
? [
|
||||
{
|
||||
key: "runtime-member-count",
|
||||
text: `${memberCards.length} 位当前成员`,
|
||||
text: `${memberCards.length} 项当前任务`,
|
||||
className:
|
||||
"rounded-full border border-slate-200 bg-white px-2.5 py-1",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(state?.blueprint?.label
|
||||
...(referenceLabel
|
||||
? [
|
||||
{
|
||||
key: "runtime-blueprint-label",
|
||||
text: `参考方案 · ${state.blueprint.label}`,
|
||||
text: `参考方案 · ${referenceLabel}`,
|
||||
className:
|
||||
"rounded-full border border-slate-200 bg-white px-2.5 py-1",
|
||||
},
|
||||
@@ -230,9 +238,9 @@ export function buildRuntimeFormationDisplayState(params: {
|
||||
panelHeadline: meta?.title || TEAM_WORKSPACE_WAITING_HEADLINE,
|
||||
panelDescription:
|
||||
state?.status === "failed"
|
||||
? state.errorMessage?.trim() || "当前任务分工准备失败,暂时无法展示更多内容。"
|
||||
: summary || "这里会先展示当前任务方案,成员接入后再切换成实时进展。",
|
||||
referenceLabel: state?.blueprint?.label?.trim() || null,
|
||||
? errorMessage || "当前任务分工准备失败,暂时无法展示更多内容。"
|
||||
: summary || "这里会先展示当前任务方案,任务拆出后再切换成任务视图。",
|
||||
referenceLabel,
|
||||
memberCards,
|
||||
blueprintRoleCards,
|
||||
};
|
||||
|
||||
@@ -121,4 +121,37 @@ describe("liveRuntimeProjector", () => {
|
||||
retryableOverload: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("queue 与 turn 生命周期提示应使用子任务口径", () => {
|
||||
const queuedProjection = projectRuntimeStreamEvent({
|
||||
sessionId: "child-1",
|
||||
session: createSessionSnapshot(),
|
||||
event: {
|
||||
type: "queue_added",
|
||||
} as AgentEvent,
|
||||
});
|
||||
|
||||
const startedProjection = projectRuntimeStreamEvent({
|
||||
sessionId: "child-1",
|
||||
session: createSessionSnapshot(),
|
||||
event: {
|
||||
type: "queue_started",
|
||||
} as AgentEvent,
|
||||
});
|
||||
|
||||
const turnProjection = projectRuntimeStreamEvent({
|
||||
sessionId: "child-1",
|
||||
session: createSessionSnapshot({
|
||||
runtimeStatus: "running",
|
||||
latestTurnStatus: "running",
|
||||
}),
|
||||
event: {
|
||||
type: "turn_started",
|
||||
} as AgentEvent,
|
||||
});
|
||||
|
||||
expect(queuedProjection?.entry?.detail).toContain("这项子任务会在前一项完成后继续处理");
|
||||
expect(startedProjection?.entry?.detail).toContain("这项子任务已经开始处理当前任务");
|
||||
expect(turnProjection?.entry?.detail).toContain("这项子任务正在推进当前内容");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -584,7 +584,7 @@ export function projectRuntimeStreamEvent(params: {
|
||||
sessionId,
|
||||
key: "queue",
|
||||
title: "稍后开始",
|
||||
detail: "新的说明已经收到,这位协作成员会在前一项完成后继续处理。",
|
||||
detail: "新的说明已经收到,这项子任务会在前一项完成后继续处理。",
|
||||
statusLabel: resolveTeamWorkspaceDisplayRuntimeStatusLabel("queued"),
|
||||
badgeClassName: QUEUED_BADGE_CLASS_NAME,
|
||||
}),
|
||||
@@ -600,7 +600,7 @@ export function projectRuntimeStreamEvent(params: {
|
||||
sessionId,
|
||||
key: "queue",
|
||||
title: "开始处理",
|
||||
detail: "这位协作成员已经开始处理当前任务。",
|
||||
detail: "这项子任务已经开始处理当前任务。",
|
||||
statusLabel: resolveTeamWorkspaceDisplayRuntimeStatusLabel("running"),
|
||||
badgeClassName: IN_PROGRESS_BADGE_CLASS_NAME,
|
||||
}),
|
||||
@@ -646,7 +646,7 @@ export function projectRuntimeStreamEvent(params: {
|
||||
sessionId,
|
||||
key: "turn",
|
||||
title: "继续处理",
|
||||
detail: "这位协作成员正在推进当前内容。",
|
||||
detail: "这项子任务正在推进当前内容。",
|
||||
statusLabel: resolveTeamWorkspaceDisplayRuntimeStatusLabel("running"),
|
||||
badgeClassName: IN_PROGRESS_BADGE_CLASS_NAME,
|
||||
}),
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ describe("selectedSessionDetailSelectors", () => {
|
||||
expect(state.queueReason).toBe("等待上游并发窗口。");
|
||||
expect(state.metadata).toEqual([
|
||||
"分工 执行",
|
||||
"协作成员",
|
||||
"子任务",
|
||||
"服务 OpenAI",
|
||||
"模型 gpt-5.4",
|
||||
"来源 spawn_agent",
|
||||
|
||||
@@ -80,6 +80,10 @@ describe("teamOperationSelectors", () => {
|
||||
"control-resume-1710000200000",
|
||||
"wait-1710000100000",
|
||||
]);
|
||||
expect(entries[0]?.detail).toBe("刚才已继续 研究员 的处理。");
|
||||
expect(entries[1]?.detail).toBe(
|
||||
"刚才等待结果时超时了,还有 1 项任务仍在处理中。",
|
||||
);
|
||||
expect(formatOperationUpdatedAt()).toBe("刚刚");
|
||||
});
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user