release: v1.9.0

This commit is contained in:
coso
2026-04-13 02:50:53 +08:00
parent 52624b1488
commit 0f15b8a71e
313 changed files with 19875 additions and 9864 deletions
+459 -156
View File
@@ -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}
@@ -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}
@@ -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("回退判断");
});
});
@@ -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}
@@ -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");
@@ -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>
)}
@@ -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>
@@ -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) => (
@@ -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>
@@ -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>
@@ -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>
@@ -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}
@@ -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();
}
});
});
@@ -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>
@@ -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>
@@ -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>
@@ -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>
);
@@ -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();
});
});
@@ -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}
</>
);
}
@@ -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}
</>
);
}
@@ -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");
});
});
@@ -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",
};
}
@@ -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();
});
});
@@ -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,
};
}
@@ -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,
};
}
@@ -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 {
@@ -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(
"这里会先展示当前任务分工,任务拆出后会切换为独立的任务视图。",
);
});
});
@@ -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,
}),
);
});
});
@@ -136,7 +136,6 @@ export function useTeamWorkspaceBoardComposer({
detailExpanded,
dispatchPreviewStatus: teamDispatchPreviewState?.status,
embedded,
hasRealTeamGraph: sessionGraphState.hasRealTeamGraph,
hasRuntimeFormation: formationState.hasRuntimeFormation,
isChildSession: sessionGraphState.isChildSession,
isEmptyShellState: sessionGraphState.isEmptyShellState,
@@ -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(
"资料整理",
);
});
});
@@ -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,
};
}
@@ -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,
@@ -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,
@@ -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,
});
});
});
@@ -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,
],
);
}
@@ -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);
});
});
@@ -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,
);
});
});
@@ -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,
},
});
});
-1
View File
@@ -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))
+89 -450
View File
@@ -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,
}),
@@ -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