mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-12 01:52:17 -10:00
* feat(serve): expose workflow tasks and controls * feat(web-shell): visualize workflow runs * fix(serve): align workflow capability gating * fix(cli): reject live workflow history deletion * fix(web-shell): harden workflow history UX * fix(serve): start controlled workflows in background * fix(serve): address workflow control review blockers * fix(serve): make workflow history deletion race-safe * fix(serve): enforce workspace trust on the daemon Workflow surfaces (#9546) * feat(web-shell): add workflow creation entry * style(web-shell): flatten the workflow execution graph Rework the Workflow run card as a flat surface: one background, one hairline, status carried by a single accent per node and edge. Drops the radial/dot-grid canvas, gradient node fills, drop shadows and glow filters, and raises the 9–10px type to 11–12px. - Lanes share the viewport width (196–336px) so a three-phase run fills the card instead of ending in a blank strip; the last lane absorbs any remaining width. Nodes grow with their lane up to 300px. - Node: 3px status bar on the left, lucide status icon, label on its own line with status and elapsed time beneath it. Queued nodes are dashed. - Run controls (pause / stop / retry / rerun) move from above the card into the graph's metrics strip so they sit next to what they act on. - Inspector drops its tinted panel and boxed key/value cells for a hairline-separated key/value list; callouts keep only a 2px bar. - Saved workflows list becomes one bordered list with row dividers instead of individually bordered, striped cards. Claude-Session: https://claude.ai/code/session_01Umbt74AQ1QDT8bGwFhRYks * feat(web-shell): open a saved workflow's definition from the Saved tab A saved workflow row was name + scope + Run; the definition itself was unreachable from the browser because the daemon only forwarded `{ name, source }` and the `/file` route stops at the workspace boundary, which excludes user-scope `~/.qwen/workflows`. Add a read-only definition surface end to end: - daemon: `GET /session/:id/saved-workflows/:name` → `_qwen/session/saved_workflow` → `qwen/status/session/saved_workflow`. The ACP child resolves the name through `resolveSavedWorkflowScript` (so the existing saved-workflow-directory guard applies), parses the `export const meta` block with `extractAndStripMeta`, and returns `{ source, scriptPath, script, meta, metaError? }`. Every miss — unknown name, illegal name, unreadable file, Workflow controls unavailable, untrusted workspace — fails closed to `workflow: null` with the same envelope on both transports, so the route cannot be used to probe the filesystem. - sdk / webui: `DaemonSessionSavedWorkflowStatus`, route-table entry, `DaemonSessionClient.savedWorkflow`, and a `readSavedWorkflow` action. - web-shell: the Saved row's identity block is now a disclosure button. Expanding it shows the description, "when to use", the phase list with optional model badges, the script path, the definition's recent runs in this session (with a jump to History), and a folded, highlighted copy of the source. Stale reads across session switches are dropped, and a definition that disappears from the list closes its detail. - e2e: the mock daemon learns `/session/:id/tasks`, a configurable `supported-commands`, and `/session/:id/saved-workflows/:name`, and a `workflow-page` visuals spec captures Saved, the expanded definition, Running, and History in both themes for the CI preview. Claude-Session: https://claude.ai/code/session_01Umbt74AQ1QDT8bGwFhRYks * fix(serve): close cross-session workflow deletion races and untrusted task reads (#9546) - Session history merge now treats a persisted snapshot as authoritative over a stale callback cache, and retires the cache once the runner confirms the snapshot write (new registry snapshot-persisted hook), so a sibling's deletion is not resurrected on refresh. - History deletion consults every sibling session's run registry (live entries and settling handles) before deleting from the shared store, and a successful deletion purges sibling terminal entries so retries cannot re-persist a deleted run. - Workflow holds mirror the registry's hasRunningEntries: paused runs no longer pin the session indefinitely. - The includeWorkflows opt-in is gated on workspace trust at the daemon boundary in both the ACP-HTTP dispatch and the REST tasks route, matching the fail-closed shape of the other workflow surfaces. * fix(cli): close the four cross-session workflow-history consistency holes Addresses R5-9, R7-4, R7-5 and R7-10 — all four Criticals open on this PR. They are one family: a run's history can be deleted, retried, or listed from any session, and each gate had a different idea of which runs exist. R5-9 — the mutation claim is task-global, not session-scoped. Keyed `sessionId\0taskId`, it serialized nothing that mattered: every session shares one snapshot store. A sibling's retry passed canStart (`failed`, no handle), took its own per-session claim, then awaited journal load/compile before `register()`; a delete-history landing in that structural window found the run terminal and handle-less in every registry, removed the journal directory and snapshot, and answered `{changed: true}` — after which the retry re-registered and its settlement re-persisted the history the user was told was deleted. The claim is now keyed by taskId alone and taken by delete-history, retry, rerun and run-saved. `run-saved` keys off a saved workflow's NAME, so it claims in its own `saved\0` namespace rather than colliding with runIds. R7-4 — deletion tests membership against the uncapped merged set. `buildSessionTasksStatus` serializes every registry entry unconditionally while `refreshWorkflowHistory` truncates to MAX_RETAINED_SNAPSHOTS by startTime, so a long run that settled after ~30 newer ones started stayed listed via the registry but fell out of the window — terminal, handle-free, live in no sibling, and permanently undeletable. `refreshWorkflowHistory` now records the merged id set before the cap, and deletion gates on that, the registry, or the unpersisted cache. `deleteWorkflowSnapshot` already tolerates an absent target, so the wider gate cannot delete what is not there. R7-5 — snapshot retirement is a latch. The registry's dispatch-drain callbacks emit status changes on TERMINAL entries with no status gate, and in-flight dispatches keep draining across the snapshot write, so a terminal emission routinely landed after `notifySnapshotPersisted` had retired the cache entry — re-inserting the run as "never persisted". A sibling's deletion was then undone by the next refresh, which reads "absent on disk, present in cache" as a pending write and republishes. Persistence is now remembered per runId and `#rememberWorkflowHistory` returns early for members; the latch releases when the runId goes active again, so a genuine re-run is still cached. R7-10 — the liveness gate sees runs whose session is gone. It iterated `this.sessions` only, but close/kill/shutdown use force semantics and a background run owns a detached controller, so after `removeStoredSessionEntry` a still-settling run was invisible to the gate and unreachable by the delete handler's sibling `removeTerminal` loop: a sibling delete-history removed the LIVE run's journal and snapshot, and the orphan's settlement write recreated it. Two halves — `Session.dispose()` now aborts its workflow registry the way it already aborts the agent registry (before the callbacks are torn down), and the registry of a removed session is retained here until its runs drain, so the gate still answers across the settlement window an abort cannot compress to zero. Retention is bookkeeping: a Config that cannot answer is logged, never turned into a shutdown failure. Regressions, each mutation-checked against its own repair: - acpAgent: a sibling's parked retry makes delete-history answer `{changed: false}` without reaching the store, and the deletion goes through once the claim releases - acpAgent: a live run's registry stays visible to the gate across its session's close, and is dropped once the handle is released - Session: a status emission after `snapshotPersisted` no longer resurrects a sibling-deleted run (reverting the latch reproduces the reviewer's probe verbatim), and a re-registered runId is remembered again - Session: a run with the oldest startTime behind 30 newer snapshots is deletable - Session: dispose aborts the workflow registry before clearing its callbacks Verification: Session.test.ts 733 passed, acpAgent.test.ts 514 passed, `tsc --noEmit -p packages/cli` clean, eslint and prettier clean. The 14 `packages/cli/src/serve` failures (fast-path import boundary, capabilities-docs contract, workspace fs/agents/memory, conversation runtime ownership) reproduce identically on the unmodified head — base skew, untouched by this change. Claude-Session: https://claude.ai/code/session_01M7z4PccYfDPyyfg3oGr8V1 * fix(core): drop the duplicated telemetry-swap mock property Merging upstream/main (053f17b775) clears the 9 `client.telemetrySwap` failures this branch had from predating #10220, but main carries its own TS1117 on the same file: two commits added `getToolRegistry` to the same object literal independently and neither saw the other.032b9078e6feat(serve): backfill session PR bindings ... (#9729)8241905392test(core): give the telemetry-swap client mock a getToolRegistry (#10220) Checking that file out from upstream/main here and running `tsc --noEmit -p packages/core` reproduces `client.telemetrySwap.test.ts(103,5): error TS1117` verbatim, so taking the merge unmodified would have traded 9 test failures for a build that never reaches the tests at all. Removed as part of the merge rather than left for a follow-up: keeps #10220's copy, which was added for this purpose and carries the explanation, and drops #9729's incidental one. main still needs the same removal — this only keeps it out of the branch. Verified after the merge: `tsc --noEmit` clean for both packages/core and packages/cli; client.telemetrySwap 10 passed, Session.test.ts and acpAgent.test.ts 1260 passed together. Claude-Session: https://claude.ai/code/session_01M7z4PccYfDPyyfg3oGr8V1 * fix(workflows): guard pending run lifecycle * fix(workflows): let a starting run be cancelled, and stop two slow leaks Four of the behavioural items the review deferred across rounds 8-9; the test-coverage-only entries stay deferred. - Cancel during the start window (R9 acpAgent.ts:10884 + workflow-runner.ts:181). Between `reserveStart` and `register` the runner loads the script and replays the journal — seconds, for a resume of a large one — and the registry has no entry yet. `sessionTaskCancel` answered `not_found` for a run the client could see starting, and `registry.cancel` could not reach the reserved controller either. New `cancelStarting` aborts it (the reservation stays the runner's to release, as after `abortAll`), and the cancel handler routes there when the liveness gate would say "starting". Doing that exposed the second half: the runner threw a bare `Error` for an abort during start, and the tool's catch only recognised the CALLER's signal — a registry-side abort surfaced as an unexplained failure. It is now a typed `WorkflowStartCancelledError`, mapped to the same "cancelled before it could start" result. - `detachedWorkflowRegistries` (R8 acpAgent.ts:3386) was pruned only inside the delete-history liveness check. A daemon that closes sessions mid-run and never deletes history retained every registry for its lifetime. Prune on session close as well. - Refresh/delete race (R9 Session.ts:3428). `refreshWorkflowHistory` reads the directory and merges without a claim; a delete landing between the read and the merge was overwritten by the stale listing and the run reappeared until the next refresh. Deletions are now sequenced, and a refresh drops any run deleted after its read began — keyed by runId and compared against the refresh's own mark, so a later retry that reuses the id is not suppressed. - The "Register a new run" JSDoc sat on `reserveStart` (R9 registry:594). Mutation-verified, all four at once against the full suites: disabling the starting-window branch, the prune-on-close, the deletion filter, and the typed-error mapping reddens exactly the four new tests and nothing else. Claude-Session: https://claude.ai/code/session_01VXsC4f71S6U6YkW82NRw7m * fix(core): close the round-5 review findings on workflow task controls - Key the starting-window cancel on a live reservation rather than on the absence of an entry: a retry reuses its runId, so its terminal entry shadowed the reservation and cancel answered `not_running` about a run that was actively starting. - Answer `changed: false` from retry when `execute()` reports a start that never registered (no `workflowRunId`), mirroring rerun and run-saved. - Classify a registry-side abort of the reserved controller as a cancel in foreground starts too, not only background ones; the tool maps `WorkflowStartCancelledError` in either mode. - Report reserved-but-unregistered runs as workflow active-work holds (`WorkflowRunRegistry.listStartingRunIds`), so a daemon conditional close cannot dispose the session under a start it just accepted. - Propagate a successful history deletion into every sibling session's deletion marker and cached history, symmetric to the `removeTerminal` sweep, so a sibling refresh that had already read the directory cannot republish the deleted run. Claude-Session: https://claude.ai/code/session_018dYE4LwSMeMPFchXk5UBdM * fix(serve): keep workflow retry and the workflowsEnabled flag consistent across sessions Two cross-session gaps in the workflow control surface: - A retry consulted only the requesting session's registry. Every session shares one journal/snapshot store and the task-global claim is released as soon as the background start returns, so a sibling whose registry still showed the run `failed` started a second runner under the same runId. Retry now refuses while the runId is live in any session (or in its own starting window), checked synchronously beside canStart so the answer cannot go stale before the claim is taken. - Workspace reload updated `tools.workflowsEnabled` for `/capabilities` but never told existing sessions; `Config.workflowsEnabled` was set once at construction. The reload's `tools` branch now propagates the flag and pushes an available-commands update when it flips. Claude-Session: https://claude.ai/code/session_01NkW1J2aBKcsKS62dkPcWbT * fix(serve): refuse to report a workflow history deletion whose registry entry survived `deleteWorkflowHistory` ignored `removeTerminal()`'s answer. The registry refuses to remove a live or handle-held entry — its own last word on whether the run is still active in this session — so a `false` for an entry that exists meant the run re-registered under the deletion and would re-persist the history the client was just told was gone. The entry is now retired before the store is touched, and a refusal fails the deletion; a persisted-only run has no entry and is unaffected. Claude-Session: https://claude.ai/code/session_01NkW1J2aBKcsKS62dkPcWbT * fix: settle the merge with main after #10411 landed The daemon layer merged as #10411 with later review fixes this branch's copy predated, and main renamed surfaces underneath the Web Shell layer: - drop the leftover options.beforeSessionCreate call in acpAgent.ts; main renamed the hook to beforeSessionPublish (#10269) and invokes it at its new location - rename @qwen-code/webui/daemon-react-sdk imports in the three new workflow client files to @qwen-code/web-shell/daemon-react-sdk - count the saved-workflow route in the telemetry catalog assertions (63 routes, 61 handler_resolved) - bump the SDK browser daemon bundle budget 215KB -> 216KB for the saved-workflow read surface (measured ~110 bytes over the old cap) Claude-Session: https://claude.ai/code/session_01NkW1J2aBKcsKS62dkPcWbT * fix(web-shell): remove stale task helper import * fix(web-shell): restore the collapsed-group style and pin workflow fixtures Review round 1 findings R1-1, R1-5, R1-7 and R1-11. `ToolGroup` stopped hiding collapsed groups with an unconditional inline `display: none` and moved workflow groups onto `styles.chatSummaryContentCollapsed`, but that class was dropped from `ToolChrome.module.css` by #9914. A CSS module miss resolves to `undefined`, so the collapsed workflow group rendered its content in full. Restore the class with the `grid-template-rows: 0fr` collapse it had before, which keeps the group mounted — what the workflow detail view needs — instead of unmounting it. `WorkflowRunsPage` references `styles.detailRunTime` for the flexible middle column of `.detailRuns li`; the class was never defined, leaving a long timestamp free to widen the row. Define it with the truncation the `minmax(0, 1fr)` column implies. The workflow task fixtures declare `DaemonSessionWorkflowTaskStatus` but omit `tokenBudgetTotal` (and `currentPhase` in `composerTasks`), which a trailing `Partial<...>` spread cannot satisfy, and `workflowTasks` annotates its array `DaemonSessionTaskStatus[]` — the agent | shell | monitor union that has no workflow member. Both are TS2322 under the package's own strict options today, masked only by the test-file typecheck exclusion. * fix(web-shell): address workflow review blockers * fix(web-shell): close the five Critical findings from review round 3 Each fix was mutation-probed: the test named below fails on the reverted behaviour and passes with it. R3-1 useBackgroundTasks read the activity fact by re-parsing the rendered taskActivityKey with a regex. `callId` is unconstrained text and the key is `callId:status` joined with '|', so the rendering is not injective: a completed call whose id contains ':in_progress|' reads as active, which pins BOTH polling stop conditions open and leaves the session polling every 3s for as long as the view is mounted. taskActivity.ts now exports hasActiveTaskActivity(), derived by the same walk that builds the key, and the hook takes the boolean instead. The key stays, for change detection only. (taskActivity.test.ts) R3-2 ToolGroup passed `inert={true}` as a JSX prop. react-dom 18 — inside this package's peer range — has no `inert` property entry and drops a boolean on an unknown attribute, so no attribute rendered at all: the collapsed content stays mounted, and its focusable ToolLine rows stayed in the tab order while aria-hidden hid them from assistive tech. Now set on the node via toggleAttribute, the pattern ParallelAgentsGroup already uses. R1-43 Rows became focusable in this PR while the global `x` shortcut still acts on the selection, so focus and selection were two independent cursors: Tab to task B, press x, and task A stops. Focus now syncs the selection for embedded rows too, except while a row is expanded — the pinned "focus does not expand" behaviour must not change. R1-44 tasksPanelActive was latched per hook instance and never reset on session change. Split view keeps the old pane mounted, so switching primary fires no `active: false`, and when that pane's panel finally closes its event carries the OLD sessionId and is filtered out — the new session's polling stayed blocked for good. Reset it in the existing [connected, owner, sessionId] effect; the same-session pause contract is unchanged. R2-1 isComposerTask counted every workflow task, including the retained history getWorkflowTasks() merges in. One poll was enough to make the status-bar pill announce the project's whole saved history ("30 tasks done") for the rest of the session, uncleareable, and to list those runs in the environment panel. History is excluded here and at the two inline filters, which now reuse the predicate. Consumers that need the history (the workflow details provider) still read the unfiltered list. * test(web-shell): align the useBackgroundTasks mock with its new signature The R3-1 fix threaded a `taskActivityActive` boolean into `useBackgroundTasks` between `taskActivityKey` and `connected`, but App.test.tsx mocks the hook by position and its stub was not updated — so `refreshTrigger` received `connected` (true) and seven assertions on the captured trigger failed with "expected true to be 0/1". Caught by CI, not locally: the earlier verification ran `vitest run client/`, and that path filter selects 234 of the package's 245 test files — App.test.tsx is one of the eleven it leaves out. * fix(web-shell): keep the workflow runs page loadable, current and per-session Three production findings from review round 3 on #10594, each pinned by a test verified to fail with the fix reverted. R1-41 `reload` had the auxiliary `refreshCommands()` in the same Promise.all as the task fetch, so a supported-commands failure rejected the whole load, discarded an already-fetched snapshot and hid all three tabs behind the failure banner — a persistently failing refresh made the page unusable while task status worked. Only the task fetch gates the page now; refreshCommands still reports itself through its own notice. R1-42 The session-switch reset cleared five states but not `loadError`, and reload clears it only on success — so session A's failure banner rendered over session B until B's load settled, or indefinitely if it hung. R2-8 The snapshot's only live update path is the poller inside the active/history panes, which Radix unmounts while inactive — so on the default Saved tab the Active/History badges and the saved cards' recent-runs list froze at whatever the last reload saw. Both panes are force-mounted now. Note Radix's forceMount does NOT hide inactive content, contrary to the finding's suggestion (verified: the forced pane renders with data-state="inactive" and no hidden attribute), so the hiding is done by a page-scoped CSS rule; the panes stay mounted, so their effects keep running while nothing renders. The tab-separation test asserted on the whole container, which now also holds the hidden pane. It scopes to the pane Radix marks active — a signal independent of how the hiding is implemented, which matters because jsdom does not apply the CSS module rule. * test(web-shell): make the workflow runs page assertions able to fail Five findings from review round 3 on #10594, all of them assertions that passed no matter what the page did. R1-22 The suite bound getTasks and getWorkflowTasks to one shared mock, so it could not see which route the page reads. That difference is load-bearing: the legacy route builds no workflow tasks at all, so a flip to it empties every tab in production. Split into two mocks with a mount-time assertion on the workflow route; the no-session path now checks both. Verified: flipping reload() to getTasks reds 12 of 14 tests, where the aliased suite passed all 12. R1-15 'opens directly on compact workflow navigation' guarded two strings that exist nowhere in the repo, so both negatives passed unconditionally and the only live assertion (the Refresh button) is satisfied by the toolbar alone. Asserts rendered tab content instead. R1-16 The stale-completion test checked for 'Workflow could not be started', but the copy is 'The saved workflow could not be started' — toContain is case-sensitive, so it passed with the banner up. It also resolved the stale start with started:true, leaving the error path unexercised: the banner is only reachable through !started or the catch. Corrected string, and the stale start now resolves as a failure. R1-38 The same test clicked the Run button through an optional chain with no assertion that it existed and none that a start was ever issued — a missing button made every later assertion pass vacuously. R1-19 The rich start payload was programmed before renderPage, which resets that mock, so taskId/status never reached the component. Moved after the mount. * fix(web-shell): remove dead workflow render paths and unify the tool predicate Four findings from review round 3 on #10594. R3-3 The workflow token entry pushed into `compactFields` never rendered: `compactFields` has one read site, and the same diff makes `headerContent` short-circuit to null for workflow tasks before reaching it. Built and pushed on every render for no effect, and misleading to anyone editing the inline workflow row. The live `subtitleParts` push stays. R1-20 `toolName.toLowerCase() === 'workflow'` was hand-inlined three times in ToolGroup — gating the detail view, the ToolLine route and the collapsed keep-mounted behaviour. A rename that missed one would make routing disagree with mounting, with no compile error. Extracted `isWorkflowToolName` beside `isShellToolName` and used at all three. R1-46 The toolbar comment promised colour was left to the owner while the rule below flattened every non-primary variant's border and background. The override is deliberate — the strip should read as one toolbar — so the comment now says that instead of the opposite. R1-47 Three `opacity: 1` declarations were dead: the queued/cancelled, related and dimmed rules have identical specificity and come later, so they win wherever they co-occur, and neither `.node` nor `.edge` sets a base opacity, so there is nothing to restore otherwise. Dropped rather than promoted — making an approval hold full opacity through those states is a visual change, not a cleanup, and is noted as such at the site. * fix: stop unbounded polling, misattributed runs and stolen selections Five more findings from review round 3 on #10594, each with a test verified to fail on the reverted behaviour. R2-2 `getTaskActivityKey` counts in-flight workflow tool calls, but with the workflows endpoint off the hook polls `getTasks()`, whose response structurally excludes workflow tasks — so the call could never appear in a snapshot and BOTH stop conditions became unsatisfiable: the 3s poll ran for the rest of the run (foreground runs cap at 30 minutes). The activity fact is now gated on the same flag that picks the endpoint, so a workflow call is simply unobservable in that mode and the empty-poll budget ends the poll. Both call sites pass the flag. R3-6 The approval auto-focus tracked one last-observed id, which cannot tell "a new approval arrived" from "the newest was settled and an older one is now last" — answering the newest of two re-stole the inspector from the node the user had deliberately selected. Tracks an acknowledged set instead, seeded at mount and reset with the rest of the run-change state, so polling ticks never re-fire it. R3-12 The `resumeFromRunId` fallback paired a resume that never launched — a script compile error, or cancelled before start — with the SOURCE run, rendering that run's graph as the failed call's detail instead of its error text. Gated to live calls, which is the branch's argued use. R1-12 `_qwen/session/saved_workflow` was missing from WS_READ_METHODS, so definition reads were billed to the mutation tier (30/min) and shared a bucket with real mutations — ~30 reads a minute then had the user's next pause/stop/cancel rejected as rate-limited. R3-11 The `resumeFromRunId` branch had no coverage at all; both its live and never-launched cases are pinned now. Not addressed here: R3-5 (ChatPane's poller duplicates App's for the same session when the default split view is open). The fix is architectural — lift polling to App, or share it through a per-session subscription cache — and panes can host sessions other than the current one, so it needs its own slice rather than being folded into this batch. * test(web-shell): cover the task panel's refusal, release and switch paths Four findings from review round 3 on #10594. Each new assertion was verified against the mutant the finding names. R3-13 No `changed: false` appears anywhere in the suite, so both refusal branches were uncovered. The daemon answers that when a transition is no longer valid — Retry clicked while a concurrent poll finalizes the run. Dropping the guards would fire onWorkflowRunStarted for a run that never started (App bumps its refresh trigger on it) and the user would never see why the click did nothing. Retry and delete-history cases added; with the guards removed both go red. R3-14 Nothing pinned that `busy` is released after an action settles in the same session, so inverting the session-guarded finally left every control permanently disabled with the suite green. Asserted through Stop rather than Pause: once the run reports 'pausing', Pause is disabled by status, so a `pause.disabled === false` assertion would fail for a reason that has nothing to do with `busy`. R3-8 The stale-action test never pinned the [snapshot.sessionId] reset, which is the only thing freeing the new session's panel — the stale action's own finally skips the release because its session guard no longer matches. It now expands the session-B row, asserts Pause is enabled and that a second control call is accepted. R3-9 The stale-polling test stopped at "the stale response is dropped". The polling effect keeps sessionId out of its deps on purpose and re-reads the ref per tick, so a refactor that fixes staleness by not re-arming leaves the mounted panel never refreshing again. One more interval after the switch now asserts a second fetch and an onTasksChange carrying the new session. * fix(workflows): address review blockers * fix(web-shell): remove duplicate ChatEditor test bindings * fix(cli): name running saved workflows * fix(cli): preserve saved workflow labels in history * fix(workflows): address review blockers --------- Co-authored-by: qqqys <[email protected]> Co-authored-by: Qwen Code Autofix <[email protected]> Co-authored-by: qwen-code-dev-bot <[email protected]> Co-authored-by: qqqys <[email protected]> Co-authored-by: Shaojin Wen <[email protected]>
380 lines
6.4 KiB
CSS
380 lines
6.4 KiB
CSS
.root {
|
|
width: min(100%, 1160px);
|
|
margin: 0 auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.tabs {
|
|
min-height: 0;
|
|
}
|
|
|
|
.toolbar {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 2px 0 9px;
|
|
}
|
|
|
|
.toolbarActions {
|
|
display: flex;
|
|
flex: 0 0 auto;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
/*
|
|
* The two run panes are force-mounted so their pollers keep the badge
|
|
* counts and the saved cards' recent-runs list live while the user browses
|
|
* the Saved tab. Radix's forceMount does NOT hide inactive content — it
|
|
* only keeps it mounted — so the hiding is ours to do. Mounted and hidden
|
|
* is exactly what is wanted here: effects keep running, nothing renders.
|
|
*/
|
|
.content[data-state='inactive'] {
|
|
display: none;
|
|
}
|
|
|
|
.content {
|
|
padding-top: 10px;
|
|
}
|
|
|
|
.loading,
|
|
.error,
|
|
.emptyState {
|
|
padding: 24px 4px;
|
|
color: var(--muted-foreground);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.error {
|
|
color: var(--error-color);
|
|
}
|
|
|
|
.savedList {
|
|
display: grid;
|
|
overflow: hidden;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
background: var(--background);
|
|
}
|
|
|
|
.savedEntry {
|
|
min-width: 0;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.savedEntry:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.savedCard {
|
|
min-width: 0;
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) auto auto;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 6px 12px 6px 6px;
|
|
}
|
|
|
|
/* The identity block is the disclosure control for the definition detail. */
|
|
.savedIdentity {
|
|
min-width: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 4px 6px;
|
|
border: 0;
|
|
border-radius: 6px;
|
|
background: transparent;
|
|
color: inherit;
|
|
cursor: pointer;
|
|
font: inherit;
|
|
text-align: left;
|
|
}
|
|
|
|
.savedIdentity:hover {
|
|
background: var(--secondary);
|
|
}
|
|
|
|
.savedIdentity:focus-visible {
|
|
outline: 2px solid color-mix(in srgb, var(--primary) 55%, transparent);
|
|
outline-offset: 1px;
|
|
}
|
|
|
|
.savedChevron {
|
|
width: 14px;
|
|
height: 14px;
|
|
flex: 0 0 auto;
|
|
color: var(--muted-foreground);
|
|
transition: transform 120ms ease;
|
|
}
|
|
|
|
.savedEntry[data-expanded] .savedChevron {
|
|
transform: rotate(90deg);
|
|
}
|
|
|
|
.savedIdentityText {
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.savedDetail {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
padding: 4px 14px 14px 32px;
|
|
border-top: 1px solid var(--border);
|
|
font-size: 12px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.detailDescription {
|
|
margin: 10px 0 0;
|
|
color: var(--foreground);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.detailSection {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
|
|
.detailLabel {
|
|
margin: 0;
|
|
color: var(--muted-foreground);
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
letter-spacing: 0.06em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.detailText,
|
|
.detailMuted {
|
|
margin: 0;
|
|
color: var(--muted-foreground);
|
|
}
|
|
|
|
.detailWarning {
|
|
padding: 6px 10px;
|
|
border-radius: 0 4px 4px 0;
|
|
box-shadow: inset 2px 0 0 var(--warning-color);
|
|
background: color-mix(in srgb, var(--warning-color) 8%, transparent);
|
|
color: var(--foreground);
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
.detailPhases {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
}
|
|
|
|
.detailPhases li {
|
|
display: flex;
|
|
min-width: 0;
|
|
align-items: baseline;
|
|
gap: 10px;
|
|
}
|
|
|
|
.detailPhaseIndex {
|
|
flex: 0 0 auto;
|
|
color: var(--muted-foreground);
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
}
|
|
|
|
.detailPhaseCopy {
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
column-gap: 8px;
|
|
}
|
|
|
|
.detailPhaseCopy strong {
|
|
font-weight: 600;
|
|
}
|
|
|
|
.detailPhaseCopy span {
|
|
color: var(--muted-foreground);
|
|
}
|
|
|
|
.detailPhaseModel {
|
|
flex: 0 0 auto;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.detailRuns {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
}
|
|
|
|
.detailRuns li {
|
|
display: grid;
|
|
grid-template-columns: 84px minmax(0, 1fr) auto;
|
|
gap: 12px;
|
|
color: var(--muted-foreground);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.detailRunStatus {
|
|
color: var(--foreground);
|
|
}
|
|
|
|
/* Flexible middle column of `.detailRuns li` — truncate rather than widen the
|
|
row when a timestamp renders long. */
|
|
.detailRunTime {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.detailRuns li[data-status='running'] .detailRunStatus {
|
|
color: var(--agent-blue-500);
|
|
}
|
|
|
|
.detailRuns li[data-status='completed'] .detailRunStatus {
|
|
color: var(--success-color);
|
|
}
|
|
|
|
.detailRuns li[data-status='failed'] .detailRunStatus {
|
|
color: var(--error-color);
|
|
}
|
|
|
|
.detailRuns li[data-status='paused'] .detailRunStatus,
|
|
.detailRuns li[data-status='pausing'] .detailRunStatus,
|
|
.detailRuns li[data-status='cancelled'] .detailRunStatus {
|
|
color: var(--warning-color);
|
|
}
|
|
|
|
.detailRunRuntime {
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.detailLink {
|
|
align-self: flex-start;
|
|
margin-left: -8px;
|
|
}
|
|
|
|
.detailSourceBar {
|
|
display: flex;
|
|
min-width: 0;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.detailPath {
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
color: var(--muted-foreground);
|
|
font-family: var(--font-mono);
|
|
font-size: 11px;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.detailSource {
|
|
min-width: 0;
|
|
overflow-x: auto;
|
|
}
|
|
|
|
.detailSource pre {
|
|
margin: 0;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.savedName {
|
|
overflow: hidden;
|
|
color: var(--foreground);
|
|
font-family: var(--font-mono);
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.savedDescription,
|
|
.savedEmptyDescription {
|
|
color: var(--muted-foreground);
|
|
font-size: 12px;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.savedEmpty {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
padding: 22px 16px;
|
|
border: 1px dashed var(--border);
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.savedEmpty > svg {
|
|
width: 18px;
|
|
height: 18px;
|
|
flex: 0 0 auto;
|
|
margin-top: 1px;
|
|
color: var(--muted-foreground);
|
|
}
|
|
|
|
.savedEmptyTitle {
|
|
margin-bottom: 3px;
|
|
color: var(--foreground);
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.startingIcon {
|
|
animation: workflow-saved-spin 900ms linear infinite;
|
|
}
|
|
|
|
@keyframes workflow-saved-spin {
|
|
to {
|
|
transform: rotate(360deg);
|
|
}
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.startingIcon {
|
|
animation: none;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.root {
|
|
gap: 10px;
|
|
}
|
|
|
|
.toolbar {
|
|
align-items: flex-end;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.toolbar > [role='tablist'] {
|
|
min-width: 0;
|
|
overflow-x: auto;
|
|
overflow-y: hidden;
|
|
}
|
|
|
|
.savedCard {
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
}
|
|
|
|
.savedCard > button:not(.savedIdentity) {
|
|
grid-column: 1 / -1;
|
|
width: 100%;
|
|
}
|
|
}
|