Files
qwen-code/packages/web-shell/client/utils/gitModeIntent.ts
Shaojin Wen cd5d5af2fb feat(web-shell): show workspace overview and a workspace menu in the sidebar (#10407)
* feat(web-shell): show workspace overview and a workspace menu in the sidebar

Workspace rows gain session counts, the full path and facet chips (MCP,
skills, extensions, channels, context files) while expanded, plus a
workspace menu with rename, copy path, new worktree task, management
entries, reload runtime and remove. Facets are fetched per expanded
trusted workspace through the existing workspace-qualified routes and an
uninitialized runtime is never rendered as zero.

Layer A of #10399.

* fix(web-shell): address review of the workspace overview sidebar

- e2e: call the mock daemon's replay helper with its object form
- stop fetching facets behind a custom (locked) header renderer and for
  the synthetic fallback workspace, which has nothing to display them
- keep a row's last session counts while it is collapsed
- rename: accept the daemon's 256-character display-name cap and treat a
  failed capabilities refresh after a successful rename as converged
- chips: use the sidebar's warning token and say "unavailable" rather than
  "not initialized" for daemon-side facets; drop the unread loading state
- record the Trust… deferral and the measured fan-out cost in the design
  doc; pin the reviewed behaviours with tests (hooks facet, user-question
  attention, poll default, in-flight invalidation, copy path, reload
  feedback, overview opt-out, worktree task)

* fix(web-shell): second review round for the workspace overview sidebar

- context files are answered by the daemon from disk, so a workspace
  without a QWEN.md renders a known 0 instead of "not initialized"
- offer "New worktree task" only when the git poll reports a branch
- keep retained session counts only while the catalog query is disabled,
  not across a source switch on an expanded row
- tests: daemon-rejected rename keeps the dialog open, clipboard stub is
  restored, the display-name cap is pinned exactly, worktree gating

* fix(web-shell): third review round for the workspace overview sidebar

- primary row shows no counts while its session source is switching,
  instead of the previous source's numbers
- poll git whenever header actions are wired, so the worktree entry no
  longer depends on the diff handler being present
- render no chips until the first overview round lands
- tests: primary-removal guard, branch-gated worktree entry on a non-git
  secondary workspace, untrusted rows never fetch, MCP disabled-server
  arithmetic, context file count, in-progress discovery is not a failure,
  and a deterministic wait for Radix's outside-press listener

* fix(web-shell): fourth review round for the workspace overview sidebar

- retained session counts are tagged with the source they were computed
  for, so a collapsed row drops them after a global source switch
- a locked sidebar with a custom header passes no header actions, so the
  section skips the git poll that only feeds them
- tests: primary-row wiring (no session list) for the loading guard,
  attention badge and truncation marker, overview reaches header actions,
  skills/extensions/channels tooltips, locked rows never poll git

* chore: restore teamHelpers.test.ts to main's formatting after merge

* fix(web-shell): fifth review round for the workspace overview sidebar

- an armed branch/worktree intent no longer resets on a transient
  git-status gap; only a session, an untrusted workspace, a no-branch
  answer or a draft workspace switch clears it
- the workspace header action wrapper stays visible while its menu is
  open, so Escape returns focus to the trigger
- the git poll runs only when a consumer of the branch is wired (diff
  chip or worktree entry outside a locked sidebar)
- the worktree entry shares the new-task re-entrancy guard, busy state
  and catalog refresh
- the menu's management section is one flat group; facet summaries are
  isolated per facet so a malformed body cannot blank the whole round
- tests for each of the above plus the items/cwd/re-enable transitions,
  placeholder overwrite on merge, skills placeholder, and action order

* fix(web-shell): sixth review round for the workspace overview sidebar

- the sidebar's worktree arm is dropped when another session start ran
  while the creation was in flight
- header totals honour the daemon's page-level truncated flag as well as
  a next cursor, on both the primary and secondary derivations
- rename is offered on registration-backed rows only; the rename dialog
  refuses control characters before the request is sent
- the header menu keeps the row's last overview snapshot while collapsed
  and its live counts are part of the items' accessible names
- a custom header still fetches the overview when header actions consume
  it; a facet reads as unavailable after three unanswered rounds and a
  cwd change never carries the previous workspace's facets over
- mock daemon: per-workspace overview facets; e2e asserts the secondary
  row's own counts and pins the 30 s poll with a fake clock
- main-boot.test mocks the provider module main.tsx imports (also opened
  separately against main)

* fix(web-shell): count unanswered overview rounds even when superseded

The SDK's request deadline equals the overview poll cadence, so while a
daemon hangs every round times out just after the next tick has replaced
it and never reached the miss bookkeeping; facets could freeze on their
pre-hang counts for the whole hang. Misses are now recorded for every
round that lands, and a superseded round that observes an expiry drops
those facets without merging its stale data. A round from a previous cwd
is ignored entirely.

* fix(web-shell): set the worktree intent with the draft it belongs to

The sidebar's worktree entry armed the intent after createNewSession
settled, guarded by a composer-version token. Two gaps remained: a prompt
submitted while the previous session was still clearing read the reset
intent and got the current branch, and a draft re-targeted through the
sidebar during the await kept the token so the arm still landed.

createNewSession now takes the git intent and writes both the state and
the ref in the same synchronous step that resets the previous one, so the
intent belongs to that draft from the start; the sidebar entry passes
worktree and no post-await arm exists. The invalidation effect also
re-runs on intent changes so an intent set while a session already exists
is cleared at once.

* fix(web-shell): keep an armed git intent across no-op and cross-workspace draft moves

Two paths still dropped a worktree intent that nothing had invalidated.

Re-selecting the draft's own workspace from the composer picker (Radix
fires onValueChange for the checked row too) went through the draft
branch of switchWorkspace and hit the reset added for real switches. The
branch now compares the target with the selected workspace before it
overwrites the ref and skips only the reset when they match; the cwd
re-set and composer version bump stay as before.

Arming a worktree intent from another workspace's row while the draft
workspace had a definitive no-branch answer (a non-repo primary) was
undone in the same commit: activeWorkspaceCwd already pointed at the new
workspace but selectedWorkspaceGitStatus still held the old answer, which
the git-status effect clears only after the commit. The reset check now
keys the status by workspaceCwd, the same rule the git_status_changed
mirror effect uses, so a status for a workspace being left never decides
for the new one. A definitive no-branch answer for the draft's own
workspace still clears the intent.

* fix(web-shell): scope overview miss budgets to one bookkeeping session

The miss bookkeeping in reload() runs for every landed round on purpose —
during a daemon hang every round is superseded before it lands, and only
their observed misses can expire a facet. But it also ran for rounds
launched before a reset boundary: a round held in flight across a cwd
round trip (A→B→A passes the cwd guard) or across a collapse/expand cycle
landed after missesRef was reset and booked into the fresh session,
shortening the three-round grace, or — landing successfully — refilled
the budget so a frozen count outlived it.

Budgets are now scoped by an epoch that advances at the two reset
boundaries (cwd change, disable); a round captures the epoch at launch
and books nothing if a reset happened while it was in flight. Rounds
inside one unbroken session still all count, and the hang-expiry test
pins that a per-round epoch bump would break.
2026-08-31 00:53:13 +00:00

33 lines
1.1 KiB
TypeScript

/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/
import type { DaemonWorkspaceGitStatus } from '@qwen-code/sdk/daemon';
/**
* Whether an armed git-mode intent (new branch / worktree) for the next
* session can no longer apply and must fall back to the current branch.
*
* Only definitive states reset it: a session already exists, the workspace
* is not trusted, or git status answered and reported no branch. A status
* that is merely absent — not fetched yet, or a poll round that failed — is
* transient and leaves the intent alone; otherwise one rejected refetch
* while a draft sits would silently turn a requested worktree task into a
* plain session in the main checkout.
*/
export function gitModeIntentMustReset({
sessionId,
workspaceTrusted,
gitStatus,
}: {
sessionId: string | null | undefined;
workspaceTrusted: boolean | undefined;
gitStatus: Pick<DaemonWorkspaceGitStatus, 'branch'> | undefined;
}): boolean {
if (sessionId) return true;
if (workspaceTrusted !== true) return true;
return gitStatus !== undefined && !gitStatus.branch;
}