Files
qwen-code/packages/web-shell/client/components/workspaces/WorkspacesOverviewPanel.module.css
Shaojin Wen 24b53add50 feat(web-shell): add a Workspaces overview panel (#10589)
* feat(web-shell): add a Workspaces overview panel

Layer B2 of #10399: a full-page table of every registered workspace —
name with primary/untrusted badges, path, active session counts, MCP
health (unknown while the runtime is not initialized, never zero),
branch with a dirty counter, and last activity — with New task and
Remove actions per row. Entries: a Manage workspaces row at the end of
the Projects section (hidden for a locked sidebar) and an opt-in
workspacesOverview footer item outside the default set. Daemon-owned
live runtimes are not rows.

The sidebar's inline removal state machine moves verbatim into a shared
useWorkspaceRemoval hook plus WorkspaceRemovalDialog so both surfaces
run the same confirm/busy/force/mismatch/in-progress flow; the sidebar
keeps its reconciliation as the hook's onRemoved callback. Cells reuse
layer-A plumbing: catalog-store session counts deduplicated with the
sidebar's queries, useWorkspaceOverview for MCP, and the git chip's
fetch discipline for the branch column. Untrusted rows fetch nothing.

* fix(web-shell): align the Workspaces panel's cells and gates with review round 1

Three behavioural fixes: a capabilities-refresh rejection after a
daemon-confirmed removal is no longer misreported as a removal failure
(the panel's onRemoved guards the refresh the way the sidebar's
reconcile does); the MCP fraction excludes disabled servers from its
denominator, matching the sidebar chip's connected/enabled convention;
and last activity falls back to createdAt like the daemon's
getSummaryActivityTime, so a live session that has not published a
terminal still counts.

Smaller alignments: session counts treat a nextCursor page as a lower
bound (the sidebar's union); the opt-in footer entry honours the locked
gate and carries a testid; the panel gets its aria-label and heading
focus when opened; the dedup comment states what actually dedupes.

Coverage the review asked for: the in-progress retry loop (converges,
busy mid-retry, dismissed, exhausted), submitting reset across
sequential removals, the active-session force block through the panel,
the per-row query and facet shape, the New-task double-submit guard and
its App wiring, the footer entry, an MCP-cell-scoped unknown assertion,
and a cross-workspace facet witness in the e2e spec.

* fix(web-shell): stabilize the removal controller and pin review round 2

The one behavioural change: useWorkspaceRemoval reads its options
through a ref and memoizes the controller it returns, and App passes
createNewSession directly, so the panel's column definitions stop
rebuilding on every render — a rebuilt column set is a new component
type per cell, which unmounted every row cell and re-fired their git
fetches on any App re-render while the panel was open.

The rest is coverage round 2 asked for: both remaining retry-loop
exits (mid-retry mismatch reconciles, a non-transient error surfaces
itself rather than the synthetic exhaustion message), the dismissed
flag reset and stale busy-state reset in request(), the
reconcile-before-clear ordering (a hung reconcile keeps the dialog
open), the allow direction of the panel's force gate, hook-level
blockForce at both call sites past the disabled attribute (invoking
the button's props directly, as a stale reference would), the global
cross-row New-task guard, the wait:true git contract, the
truncated-without-cursor lower bound, the unwired footer opt-in
rendering nothing, heading focus on panel open, and an honest scope
note on the e2e cross-wiring witness.

* test(web-shell): pin fresh option reads and submitting across the reconcile

Two review-round-3 pins on the removal hook. Options are read through a
ref so blockForce observes the connection state at confirm time — a
busy dialog whose candidate gains the active session between renders
must refuse the forced retry; the new test re-renders the probe with a
flipped blockForce and goes red if the ref assignment is dropped. And
submitting stays true for the whole reconcile window, keeping every
dialog gate locked during the caller's capability round-trip; the
hung-reconcile test now asserts it mid-flight and goes red if confirm
stops awaiting handleRemoved.

* test(web-shell): pin the busy report across a blocked forced confirm

force derives from activity, so a block branch that cleared it would
silently downgrade the next click to a non-forced attempt and restart
the consent flow; the blocked-confirm test now asserts the report and
candidate both survive the refusal.
2026-09-01 16:55:44 +00:00

106 lines
1.5 KiB
CSS

.page {
display: flex;
min-height: 100%;
flex-direction: column;
margin: -16px -20px 0;
}
.pageHeader {
display: flex;
min-height: 46px;
flex: 0 0 auto;
align-items: center;
gap: 10px;
padding: 10px 20px;
border-block-end: 1px solid var(--border);
}
.backButton {
width: 30px;
height: 30px;
flex: 0 0 30px;
color: var(--muted-foreground);
}
.title {
min-width: 0;
color: var(--foreground);
font-size: 14px;
font-weight: 600;
line-height: 1.35;
outline: none;
}
.pageBody {
display: flex;
min-width: 0;
flex-direction: column;
gap: 16px;
padding: 20px 24px 32px;
}
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.count {
min-width: 0;
color: var(--muted-foreground);
font-size: 13px;
line-height: 1.4;
}
.toolbarActions {
display: flex;
flex: 0 0 auto;
align-items: center;
gap: 8px;
}
.nameCell {
display: flex;
min-width: 0;
align-items: center;
gap: 6px;
}
.nameLabel {
min-width: 0;
overflow: hidden;
font-weight: 500;
text-overflow: ellipsis;
white-space: nowrap;
}
.pathCell {
display: block;
min-width: 0;
overflow: hidden;
color: var(--muted-foreground);
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.statCell {
display: flex;
min-width: 0;
align-items: center;
gap: 6px;
white-space: nowrap;
}
.muted {
color: var(--muted-foreground);
}
.actionsCell {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 6px;
}