|
| 1 | +# Web Shell workspace overview |
| 2 | + |
| 3 | +Issue: https://github.com/QwenLM/qwen-code/issues/10399 |
| 4 | + |
| 5 | +## Goal |
| 6 | + |
| 7 | +Make a workspace a first-class object in the Web Shell sidebar: show what it |
| 8 | +contains and whether it is healthy without opening a session, and put every |
| 9 | +workspace-level action behind its own menu. This is layer A of the plan in the |
| 10 | +issue — a frontend-only change on top of daemon routes that already exist. |
| 11 | + |
| 12 | +## Behavior |
| 13 | + |
| 14 | +### Folder header |
| 15 | + |
| 16 | +- The header keeps its name, badges and git chip. It gains session counts at |
| 17 | + its right edge: sessions waiting on the user (warning tone), sessions with a |
| 18 | + prompt in flight (success tone), and the total. A total from a truncated |
| 19 | + catalog page shows as `N+`. Collapsing a row disables its catalog query, so |
| 20 | + the row keeps the last counts it computed: they stay visible while collapsed |
| 21 | + and refresh on the next expand. While the query is active and a page is |
| 22 | + missing (a session-source switch), no counts are shown rather than stale |
| 23 | + ones above an empty list. |
| 24 | +- The name carries the full path as a tooltip. While the section is expanded |
| 25 | + the path is printed under the header. |
| 26 | +- The Projects label shows the number of registered workspaces once there is |
| 27 | + more than one. |
| 28 | + |
| 29 | +### Facet chips |
| 30 | + |
| 31 | +While a trusted workspace is expanded, a chip row summarizes MCP servers |
| 32 | +(`connected/enabled`), skills (enabled), extensions (active, or |
| 33 | +`active/total` when they differ), channels (`connected/configured`) and |
| 34 | +context files (count). Hooks are available but off by default. |
| 35 | + |
| 36 | +- MCP, skills and hooks are discovered by the workspace's ACP child. Until it |
| 37 | + reports `initialized`, the chip shows `—` and the tooltip says the runtime |
| 38 | + is not initialized yet. A placeholder is never rendered as `0`. |
| 39 | +- Context files are read from disk by the daemon itself, so its answer is |
| 40 | + always definitive: a workspace without a QWEN.md shows `0`. |
| 41 | +- The MCP chip takes the warning tone when a server errored or discovery |
| 42 | + finished with an enabled server still not connected; the channels chip when |
| 43 | + an instance is in the error state. |
| 44 | +- Below the sidebar's tight width the chips drop their text labels. |
| 45 | +- Extensions, channels and context files are daemon-side facets. When they |
| 46 | + are unknown the daemon lacks the route or the fetch failed, so their tooltip |
| 47 | + says "unavailable on this daemon" rather than "not initialized yet". The |
| 48 | + chip row itself appears only once the first fetch round has landed, so a |
| 49 | + round still in flight never reads as a missing route. |
| 50 | +- Chips are read-only. Opening a management page is a menu action, so the |
| 51 | + chips never take the button role and their accessible names cannot collide |
| 52 | + with the navigation buttons that share the same words. |
| 53 | + |
| 54 | +### Workspace menu |
| 55 | + |
| 56 | +The hover `⋮` on a workspace row replaces the single-item removal menu: |
| 57 | + |
| 58 | +- Rename… (dynamic registration daemons, on registration-backed rows only — |
| 59 | + the daemon's bound workspace has no registration to persist a name in; |
| 60 | + opens a dialog; an empty name falls back to the folder name and control |
| 61 | + characters are refused before the request is sent), Copy path, New task, |
| 62 | + New worktree task (only when |
| 63 | + the git poll reports a branch — a worktree needs a repository, and the |
| 64 | + composer would otherwise have no chip to show or undo the armed intent). |
| 65 | +- Manage: MCP servers, Skills, Extensions, Channels, Settings, with the chip |
| 66 | + counts next to the first four (part of the item's accessible name). The |
| 67 | + menu keeps the row's last snapshot while the row is collapsed, so the |
| 68 | + counts do not vanish on collapse; the action area stays visible while its |
| 69 | + menu is open so Escape returns focus to the trigger. |
| 70 | +- Reload runtime (`POST /workspaces/:w/reload`), then Remove workspace. |
| 71 | + |
| 72 | +Each entry appears only when the workspace's state allows it: untrusted rows |
| 73 | +that cannot be removed still show nothing, locked-workspace renderers still |
| 74 | +suppress the whole action area. |
| 75 | + |
| 76 | +The Manage group is offered on the daemon's primary workspace only. The |
| 77 | +management pages read the connection's bound workspace, so a secondary row |
| 78 | +cannot open its own view yet; that is layer B1 of the issue. |
| 79 | + |
| 80 | +## Data flow |
| 81 | + |
| 82 | +`useWorkspaceOverview(client, cwd, { enabled, items })` fans out over |
| 83 | +`client.workspaceByCwd(cwd)` to `/mcp`, `/skills`, `/extensions`, |
| 84 | +`/channels`, `/memory` and `/hooks`, one request per requested facet. Each |
| 85 | +call fails independently — an older daemon without a route, a transient |
| 86 | +error, or a malformed body — leaves that facet `undefined` and keeps the |
| 87 | +others. A facet keeps its last known value across up to three consecutive |
| 88 | +unanswered rounds, then reads as unavailable, so a route that stays gone |
| 89 | +after a rollback cannot freeze a stale count on the chip. Rounds that time |
| 90 | +out after the next tick has already replaced them still count: the SDK's |
| 91 | +request deadline equals the poll cadence, so during a daemon hang every |
| 92 | +round is superseded before it lands, and only their observed misses can |
| 93 | +expire the facet. The budget is scoped to one bookkeeping session: a reset |
| 94 | +boundary (a cwd change, the section collapsing) advances an epoch, and a |
| 95 | +round launched before the boundary can neither book misses into the fresh |
| 96 | +session nor refill it with a stale success. |
| 97 | + |
| 98 | +Fetching is gated on the section being expanded, the workspace trusted and the |
| 99 | +default header rendered (a locked sidebar's custom header has no chip or menu |
| 100 | +to feed), and polls every 30 s only while the document is visible, plus a |
| 101 | +refetch on window focus and on the sidebar's reload token. Collapsed rows cost |
| 102 | +nothing, and a synthetic fallback workspace without a real cwd is never asked. |
| 103 | + |
| 104 | +Measured against the mock daemon (`npm run dev`, React StrictMode, 5 trusted |
| 105 | +workspaces all expanded, tab visible): after the initial round the sidebar |
| 106 | +issued 25 facet requests per 30 s tick — 50 over 60 s — next to the 33 |
| 107 | +session-catalog and git-status requests the same rows already made in that |
| 108 | +window. Without StrictMode the initial round is 25 requests, not 50. That is |
| 109 | +the cost the layer-C overview endpoint collapses to 5 per tick. |
| 110 | + |
| 111 | +Session counts come from the catalog page the row already lists; the primary |
| 112 | +workspace, whose sessions the sidebar lists itself, gets its counts passed in. |
| 113 | + |
| 114 | +## Embedding |
| 115 | + |
| 116 | +- `sidebar.workspaceOverview: false` keeps the plain folder headers; |
| 117 | + `{ items: [...] }` selects the chips. |
| 118 | +- `onOpenWorkspaceManagement(target, workspaceCwd)` and |
| 119 | + `onNewWorktreeSession(workspaceCwd)` are new sidebar callbacks; the app |
| 120 | + wires them to `openPanel` and to `createNewSession` with a worktree git |
| 121 | + intent. The intent is set in the same synchronous step that clears the |
| 122 | + previous one, so it belongs to that draft from the start: a prompt |
| 123 | + submitted while the clear is still in flight gets the worktree, and any |
| 124 | + later session start or draft workspace switch resets it like any other |
| 125 | + intent. An armed intent survives a transient git-status gap — only a |
| 126 | + session, an untrusted workspace, a no-branch answer for the draft's own |
| 127 | + workspace (the status is keyed by `workspaceCwd`, so the answer of a |
| 128 | + workspace being left never clears an intent armed for the new one) or a |
| 129 | + draft workspace switch clears it; re-selecting the draft's own workspace |
| 130 | + from the composer picker is a no-op, and an intent set while a session |
| 131 | + already exists is cleared immediately. |
| 132 | + |
| 133 | +## Follow-ups (layers B and C in the issue) |
| 134 | + |
| 135 | +- A Trust… menu entry. `POST /workspaces/:w/trust/request` only records a |
| 136 | + request that needs operator action and a daemon restart |
| 137 | + (`accepted: false, requiresOperatorAction: true`), so an entry today would |
| 138 | + promise a change it cannot make; it needs an "operator action required" |
| 139 | + feedback surface first. |
| 140 | +- Bind the management pages to a chosen workspace so every row can open its |
| 141 | + own MCP / Skills / Extensions view. |
| 142 | +- A Workspaces overview page with a table across workspaces. |
| 143 | +- `GET /workspaces/:w/overview` on the daemon to collapse the fan-out into one |
| 144 | + request, advertised as `workspace_overview`, once the workspace-runtime |
| 145 | + stack has landed. |
0 commit comments