From 24c0d62c1c6e2ae6370287db2c9e7f12feb6db64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=92=E6=8B=BE=E5=85=86=E5=AD=97=E8=8A=82?= <3311482378@qq.com> Date: Tue, 20 Oct 2026 12:13:10 +0800 Subject: [PATCH 01/19] feat(web): migrate DFSC conversation and summary presentation Render Markdown tables, preserve timed thinking history, settle response actions per turn, and project structured subagent states into read-only inspectors. Add the shared right rail and floating Summary with canonical task progress, agent records, activity overview and accessible motion. Make existing Web validation fixtures portable on Windows and exclude generated artifacts from source linting. --- apps/web/AGENTS.md | 29 +++ apps/web/client/src/App.tsx | 51 +++- .../features/conversation/ChildAgentView.tsx | 141 ++++++++-- .../conversation/ConversationStream.tsx | 12 +- .../src/features/conversation/MessageItem.tsx | 40 +-- .../features/conversation/SubagentLane.tsx | 51 ++++ .../conversation/SubagentStatusIcon.tsx | 54 ++++ .../src/features/conversation/TaskBanner.tsx | 117 --------- .../conversation/ThinkingDisclosure.tsx | 92 +++++++ .../src/features/conversation/ToolCall.tsx | 129 +++++----- .../src/features/conversation/actions.ts | 31 ++- .../src/features/conversation/child-agent.css | 197 +++++++++++++- .../src/features/conversation/index.tsx | 5 +- .../features/conversation/message-actions.css | 8 + .../src/features/conversation/stream.css | 225 ++++++++++++++-- .../src/features/conversation/task-banner.css | 158 ------------ .../src/features/conversation/task-counts.ts | 77 ------ .../conversation/tool-presentation.ts | 26 ++ .../src/features/conversation/turn-footers.ts | 41 +++ .../client/src/features/files/ChangesView.tsx | 3 +- .../client/src/features/files/PreviewBody.tsx | 5 +- .../src/features/files/PreviewPanel.tsx | 10 +- apps/web/client/src/features/files/actions.ts | 39 ++- .../src/features/inspector/ContextPanel.tsx | 154 +++++++++++ .../src/features/inspector/RightPanel.tsx | 203 +++++++++++++++ .../src/features/inspector/SummaryAgents.tsx | 56 ++++ .../src/features/inspector/SummaryBoard.tsx | 195 ++++++++++++++ .../src/features/inspector/SummaryHeader.tsx | 49 ++++ .../features/inspector/SummaryTaskStatus.tsx | 73 ++++++ .../client/src/features/inspector/index.tsx | 3 + .../src/features/inspector/inspector.css | 241 ++++++++++++++++++ .../client/src/features/inspector/model.ts | 90 +++++++ .../src/features/shell/ShellEffects.tsx | 5 +- apps/web/client/src/i18n/messages.ts | 176 +++++++++++++ apps/web/client/src/lib/markdown.ts | 175 +++++++------ .../web/client/src/state/assistant-content.ts | 30 +++ apps/web/client/src/state/right-panel.ts | 54 ++++ apps/web/client/src/state/session.ts | 217 +++++++++++++--- apps/web/client/src/state/store.ts | 10 + apps/web/client/src/state/subagent-tasks.ts | 124 +++++++++ apps/web/client/src/state/thinking-timing.ts | 34 +++ apps/web/client/src/state/types.ts | 74 +++++- apps/web/client/src/styles/global.css | 56 ++++ .../client/src/ui/IconSprite/IconSprite.tsx | 188 +++++++++++++- apps/web/openapi.yaml | 103 ++++++++ apps/web/src/inspector.ts | 47 ++++ apps/web/src/runtime.ts | 52 +++- apps/web/src/server.ts | 2 + apps/web/src/subagent-notification.ts | 24 ++ apps/web/src/thinking-timing.ts | 121 +++++++++ apps/web/test/child-sessions.test.ts | 5 +- apps/web/test/client-right-panel.test.ts | 86 +++++++ apps/web/test/client-subagent-tasks.test.ts | 103 ++++++++ apps/web/test/client-summary-activity.test.ts | 50 ++++ apps/web/test/client-summary-agents.test.ts | 62 +++++ apps/web/test/client-thinking-timing.test.ts | 33 +++ apps/web/test/client-thinking.test.ts | 121 +++++++++ .../web/test/client-tool-presentation.test.ts | 50 ++++ apps/web/test/client-turn-footers.test.ts | 105 ++++++++ apps/web/test/client-upstream-sync.test.ts | 107 ++++++++ apps/web/test/files.test.ts | 2 +- apps/web/test/inspector.test.ts | 36 +++ apps/web/test/markdown.test.ts | 93 ++++++- apps/web/test/session-search.test.ts | 7 +- apps/web/test/settings.test.ts | 7 +- apps/web/test/subagent-cli.test.ts | 8 +- apps/web/test/subagent-notification.test.ts | 20 ++ apps/web/test/thinking-timing.test.ts | 120 +++++++++ biome.json | 1 + docs/THIRD_PARTY_PROVENANCE.md | 4 + docs/stepcode-web.md | 152 ++++++++++- 71 files changed, 4595 insertions(+), 674 deletions(-) create mode 100644 apps/web/client/src/features/conversation/SubagentLane.tsx create mode 100644 apps/web/client/src/features/conversation/SubagentStatusIcon.tsx delete mode 100644 apps/web/client/src/features/conversation/TaskBanner.tsx create mode 100644 apps/web/client/src/features/conversation/ThinkingDisclosure.tsx delete mode 100644 apps/web/client/src/features/conversation/task-banner.css delete mode 100644 apps/web/client/src/features/conversation/task-counts.ts create mode 100644 apps/web/client/src/features/conversation/tool-presentation.ts create mode 100644 apps/web/client/src/features/conversation/turn-footers.ts create mode 100644 apps/web/client/src/features/inspector/ContextPanel.tsx create mode 100644 apps/web/client/src/features/inspector/RightPanel.tsx create mode 100644 apps/web/client/src/features/inspector/SummaryAgents.tsx create mode 100644 apps/web/client/src/features/inspector/SummaryBoard.tsx create mode 100644 apps/web/client/src/features/inspector/SummaryHeader.tsx create mode 100644 apps/web/client/src/features/inspector/SummaryTaskStatus.tsx create mode 100644 apps/web/client/src/features/inspector/index.tsx create mode 100644 apps/web/client/src/features/inspector/inspector.css create mode 100644 apps/web/client/src/features/inspector/model.ts create mode 100644 apps/web/client/src/state/assistant-content.ts create mode 100644 apps/web/client/src/state/right-panel.ts create mode 100644 apps/web/client/src/state/subagent-tasks.ts create mode 100644 apps/web/client/src/state/thinking-timing.ts create mode 100644 apps/web/src/inspector.ts create mode 100644 apps/web/src/subagent-notification.ts create mode 100644 apps/web/src/thinking-timing.ts create mode 100644 apps/web/test/client-right-panel.test.ts create mode 100644 apps/web/test/client-subagent-tasks.test.ts create mode 100644 apps/web/test/client-summary-activity.test.ts create mode 100644 apps/web/test/client-summary-agents.test.ts create mode 100644 apps/web/test/client-thinking-timing.test.ts create mode 100644 apps/web/test/client-thinking.test.ts create mode 100644 apps/web/test/client-tool-presentation.test.ts create mode 100644 apps/web/test/client-turn-footers.test.ts create mode 100644 apps/web/test/client-upstream-sync.test.ts create mode 100644 apps/web/test/inspector.test.ts create mode 100644 apps/web/test/subagent-notification.test.ts create mode 100644 apps/web/test/thinking-timing.test.ts diff --git a/apps/web/AGENTS.md b/apps/web/AGENTS.md index b1189120..453f8a1d 100644 --- a/apps/web/AGENTS.md +++ b/apps/web/AGENTS.md @@ -172,6 +172,35 @@ When to add or change one: and add a unit test. - Anything a reloaded page must show again belongs in the session snapshot (e.g. `pendingApprovals`), not only in an SSE event. +- Thinking elapsed time is host-observed presentation metadata (`src/thinking-timing.ts`), persisted through + the session manager's `stepcode-web.thinking-timing` custom entries outside model context. Snapshots and agent + envelopes project validated `thinkingTimings` by original content index. Never infer durations for unobserved + history or start clocks from browser receipt times. +- Subagent rows project structured per-task results and use the existing read-only child transcript API. + Planned arguments mean queued, not running; missing legacy statuses stay unknown. Background custom messages + expose only validated agentId/event/status fields through `src/subagent-notification.ts`. Do not parse + notification prose, infer per-task success from the parent tool, or add child execution controls in the inspector. +- `features/inspector` owns the shared right rail, Summary and Context views. + Summary uses DFSC's independent content-height floating board with a close-only + header, fixed compact width and an optional reading-space reservation; do not + wrap it in the full-height inspector chrome used by the other panels. + Summary's DFSC colors are scoped to that surface. Keep grid disclosures, + fixed-slot completion motion and reduced-motion support; never replay a + live completion animation for initially loaded completed history. + Its MCP switch-shaped indicators are decorative, not configuration controls. + Latest-turn task/subagent activity can reveal Summary once per selection/turn; + respect manual dismissal and never displace an already active inspector. + Its task progress uses canonical completed/total tasks, not tool-call counts. + Agent rows below the task list reuse validated timeline statuses and existing read-only + inspector actions; count records, never infer distinct live agents or success. + The header repeats canonical task completion and agent-record counts; its + activity dot uses explicit running states and preserves unknown history. + File and Subagent entry points select the shared panel through `state/right-panel.ts`. + The read-only `/sessions/{id}/inspector` route projects canonical `step-tasks` + snapshots and SDK statistics; task-tool completion is not task completion. + MCP status is process-scoped, unlike the session's skill count. Do not label + recorded messages as assembled model context or turn patches as Git net changes. + Conversation navigation is excluded; terminal and browser bridges need separate Web adapters. - Workspace navigation metadata is separate from sessions: `web-workspaces.json` stores registered `workspaces` and explicit `removedWorkspaces`. Removed entries are returned with `hidden: true`; clients must not recreate their project groups from session cwd values. Re-adding restores the group; removal never deletes chats or files. diff --git a/apps/web/client/src/App.tsx b/apps/web/client/src/App.tsx index 8dbb1dc7..da0216d7 100644 --- a/apps/web/client/src/App.tsx +++ b/apps/web/client/src/App.tsx @@ -1,7 +1,8 @@ -import type { CSSProperties } from "react"; +import { type CSSProperties, useLayoutEffect, useRef, useState } from "react"; import { Composer, ImagePreview, SelectionDialog } from "./features/composer"; -import { ChildAgentView, ConversationStream, FeedbackModal, TaskBanner } from "./features/conversation"; -import { FileTreePanel, PreviewPanel, PreviewShowButton } from "./features/files"; +import { ConversationStream, FeedbackModal } from "./features/conversation"; +import { FileTreePanel } from "./features/files"; +import { RightPanel, RightToolRail } from "./features/inspector"; import { LoginGate } from "./features/login"; import { CronView, MarketView } from "./features/market"; import { SettingsView } from "./features/settings"; @@ -10,6 +11,7 @@ import { SearchModal, SidebarNav, SidebarResizer } from "./features/sidebar"; import { RemoveWorkspaceModal, WorkspaceBrowserModal, WorkspaceControl } from "./features/workspace"; import { I18nProvider } from "./i18n"; import { cx } from "./lib/format"; +import { activeRightPanel } from "./state/right-panel"; import { shallowEqual, useAppStore } from "./state/store"; import { IconSprite } from "./ui"; @@ -18,14 +20,15 @@ import { IconSprite } from "./ui"; * original DOM position: * * svg.icon-sprite - * div.app[.sidebar-hidden][.preview-open][.resizing] (--sidebar-width, --preview-width) + * div.app[.sidebar-hidden][.right-panel-open][.resizing] (--sidebar-width, --preview-width) * aside.sidebar sidebar nav | files tree | shell user area (account, remote control, settings gear) * #sidebarResizer sidebar - * main.main shell show button | files show button | conversation banner | market cron/market views - * | conversation #childAgentView (read-only child transcript over stream and composer) | stream + * main.main shell show button | files show button | market cron/market views + * | conversation stream * | .composer-zone > composer form (workspace pill above the card) * | shell toast + auth banner - * aside#previewPanel files + * aside#rightPanel floating Summary, or context/files/changes/child inspector / embedded browser + * nav.right-tool-rail * #settingsView settings: full-window settings page (menu + page) over the workspace * #feedbackModal, #searchModal, #workspaceBrowserModal, #removeWorkspaceModal, #selectionDialog, * #imagePreview (composer: enlarged image attachment), #loggedOutOverlay @@ -33,16 +36,35 @@ import { IconSprite } from "./ui"; * #workspaceMenu, #sessionMenu, #tagPicker: rendered by their sidebar trigger, in a portal on body, only while open */ export function App() { + const appRef = useRef(null); + const [summarySpace, setSummarySpace] = useState(false); const layout = useAppStore( (state) => ({ sidebarHidden: state.ui.sidebarHidden, sidebarWidth: state.ui.sidebarWidth, previewWidth: state.ui.previewWidth, resizing: state.ui.resizing, - previewOpen: state.preview.open, + rightOpen: Boolean(activeRightPanel(state)), + summaryOpen: activeRightPanel(state) === "summary", + rightExpanded: state.ui.rightPanelExpanded, }), shallowEqual, ); + useLayoutEffect(() => { + const app = appRef.current; + if (!app) return; + const sidebar = app.querySelector(".sidebar"); + const measure = () => { + const sidebarWidth = layout.sidebarHidden ? 0 : (sidebar?.getBoundingClientRect().width ?? 0); + const railWidth = Number.parseFloat(getComputedStyle(app).getPropertyValue("--rail-width")) || 44; + setSummarySpace(app.clientWidth - sidebarWidth - railWidth >= 1120); + }; + measure(); + const observer = new ResizeObserver(measure); + observer.observe(app); + if (sidebar) observer.observe(sidebar); + return () => observer.disconnect(); + }, [layout.sidebarHidden]); const style: Record = {}; if (layout.sidebarWidth !== null) style["--sidebar-width"] = `${layout.sidebarWidth}px`; if (layout.previewWidth !== null) style["--preview-width"] = `${layout.previewWidth}px`; @@ -50,10 +72,15 @@ export function App() {
- - -
} /> @@ -78,7 +102,8 @@ export function App() {
- + +
diff --git a/apps/web/client/src/features/conversation/ChildAgentView.tsx b/apps/web/client/src/features/conversation/ChildAgentView.tsx index cca0dea6..fa3da9c2 100644 --- a/apps/web/client/src/features/conversation/ChildAgentView.tsx +++ b/apps/web/client/src/features/conversation/ChildAgentView.tsx @@ -2,13 +2,16 @@ import { useEffect, useMemo, useState } from "react"; import { useT } from "../../i18n"; import { api, errorMessage } from "../../lib/api"; import { cx } from "../../lib/format"; +import { activeRightPanel } from "../../state/right-panel"; import { timelineFromEntries } from "../../state/session"; import { setUi, useAppStore } from "../../state/store"; import type { ChildAgentSummary, ChildSessionSnapshot } from "../../state/types"; import { Icon, IconButton, Tooltip } from "../../ui"; -import { closeChildAgent } from "./actions"; +import { closeChildAgent, openSubagentTask } from "./actions"; import { MessageItem } from "./MessageItem"; import { RetryRow } from "./RetryRow"; +import { SubagentStatusIcon } from "./SubagentStatusIcon"; +import { ThinkingDisclosure } from "./ThinkingDisclosure"; import { ToolRow } from "./TimelineRows"; /** A running parent may still have children writing; their transcripts are re-read this often. */ @@ -25,35 +28,46 @@ function usePolled(key: string | null, live: boolean, load: () => Promise) useEffect(() => { if (!key) return; let cancelled = false; - const run = () => - load().then( + let timer: ReturnType | undefined; + const run = async () => { + await load().then( (value) => !cancelled && setState({ key, value }), (error: unknown) => !cancelled && setState({ key, error: errorMessage(error) }), ); + if (!cancelled && live) timer = setTimeout(run, REFRESH_MS); + }; void run(); - const timer = live ? setInterval(run, REFRESH_MS) : undefined; return () => { cancelled = true; - if (timer) clearInterval(timer); + if (timer) clearTimeout(timer); }; }, [key, live]); return state.key === key ? state : {}; } /** - * `#childAgentView`: a child agent's transcript over the parent conversation. + * `#childAgentView`: a responsive child transcript inspector beside the parent. * Strictly read-only: there is no composer, and messages carry no vote or fork * actions. Opened from a subagent call (one child) or a workflow call (the * run's agents, as tabs). The parent stays selected in the sidebar. */ -export function ChildAgentView() { +export function ChildAgentView({ embedded = false }: { readonly embedded?: boolean }) { const t = useT(); const view = useAppStore((state) => state.ui.childView); const currentId = useAppStore((state) => state.current?.id); + const panel = useAppStore(activeRightPanel); + const selectedTool = useAppStore((state) => state.timelineItems.find((item) => item.itemId === view?.toolItemId)); + const tasks = selectedTool?.kind === "tool" ? selectedTool.subagentTasks : undefined; + const task = view?.taskIndex === undefined ? undefined : tasks?.[view.taskIndex]; + const dispatchError = + !task && selectedTool?.kind === "tool" && selectedTool.state === "failed" + ? selectedTool.outputDetail || selectedTool.outputSummary + : ""; + const [expanded, setExpanded] = useState(false); const parentBusy = useAppStore( (state) => state.current?.status === "running" || state.current?.status === "waiting", ); - const open = Boolean(view && view.parentId === currentId); + const open = Boolean(view && view.parentId === currentId && (!embedded || panel === "subagent")); const parentId = view?.parentId ?? ""; const workflowRun = open ? view?.workflowRun : undefined; @@ -62,25 +76,42 @@ export function ChildAgentView() { `/api/v1/sessions/${encodeURIComponent(parentId)}/children?${new URLSearchParams({ workflowRun: workflowRun ?? "" })}`, ).then((body) => body.children), ); - const childId = view?.childId ?? agents.value?.[0]?.id ?? null; - const transcript = usePolled(open && childId ? `${parentId}:${childId}` : null, parentBusy, () => - api(childPath(parentId, childId ?? "")), + const childId = task?.sessionId ?? view?.childId ?? agents.value?.[0]?.id ?? null; + const transcript = usePolled( + open && childId ? `${parentId}:${childId}` : null, + parentBusy || task?.status === "running", + () => api(childPath(parentId, childId ?? "")), ); const items = useMemo(() => timelineFromEntries(transcript.value?.entries ?? []), [transcript.value]); const selectAgent = (id: string) => { if (view) setUi({ childView: { ...view, childId: id } }); }; - const emptyText = transcript.error - ? transcript.error - : workflowRun && agents.value?.length === 0 - ? t("childAgentNoneInRun") - : transcript.value - ? t("childAgentEmpty") - : t("childAgentLoading"); + const emptyText = dispatchError + ? dispatchError + : view?.toolItemId && !task + ? t("subagentNoTranscript") + : transcript.error || agents.error + ? transcript.error || agents.error + : workflowRun && agents.value?.length === 0 + ? t("childAgentNoneInRun") + : task && !childId + ? t( + task.status === "queued" || task.status === "running" + ? "subagentWaitingTranscript" + : "subagentNoTranscript", + ) + : transcript.value + ? t("childAgentEmpty") + : t("childAgentLoading"); return ( -