Commit graph

5 commits

Author SHA1 Message Date
Shaojin Wen
49497f5076
feat(web-shell): color-code each split pane by workspace (#6971)
* feat(web-shell): color-code each split pane by workspace

On a narrow split (split-screen / mobile), it was hard to tell which
workspace each pane belonged to: a pane's header showed only its session
name, and the sole workspace signal — the composer chip at the bottom —
collapsed to a bare folder icon that looked identical for every
workspace, so the workspace was discoverable only by hovering each one.

Surface the workspace where you actually scan — the pane header — and
give each workspace a stable accent color so panes read apart at a
glance and same-workspace panes read as a group:

- Add a colored workspace tag (dot + basename) at the start of each
  pane header on a multi-workspace daemon, and colorize the header
  divider with the same accent. The dot never shrinks, so panes stay
  distinguishable even when the name and session title ellipsize.
- Derive a stable per-workspace color from the workspace's position in
  the daemon's advertised workspaces[], reusing the sidebar
  session-group palette so the two surfaces speak the same color
  language. Extracted into a shared workspaceAccent.module.css.
- Tint the composer workspace chip with the same accent (folder + faint
  background) so it stays distinguishable even in its icon-only compact
  state, instead of a generic folder.

Single-workspace daemons are unchanged: no tag, and the header divider
falls back to the neutral border.

* refactor(web-shell): address review on split-pane workspace accent

- Rename workspaceAccent.module.css -> WorkspaceAccent.module.css to match the
  PascalCase convention used by every other component .module.css; update both
  import sites.
- Hoist the four raw-hex accent colors (red/orange/yellow/green) into shared
  --accent-* theme tokens in App.module.css, and point the workspace accent
  module, the sidebar group dots, and the overview badges at them. The palette
  now has a single source of truth and can't drift between the four surfaces
  (values are unchanged, so rendering is identical).
- Add a compile-time exhaustiveness guard so adding a
  DaemonSessionGroupPresetColor without extending WORKSPACE_ACCENT_COLORS (and
  its CSS class) fails the build instead of silently dropping that accent.
- Give the pane-header workspace tag role="img" so its "Workspace: <name>"
  aria-label is reliably announced; aria-label on a bare span (generic role)
  is not.

* refactor(web-shell): address follow-up review on workspace accent

- Hoist the four --accent-* tokens out of both theme blocks into the
  theme-independent .app scope, so they are declared once (the values do not
  vary by theme) — a genuine single declaration rather than two kept in sync.
- Add a dev-only runtime check that every accent color has a matching class in
  WorkspaceAccent.module.css, closing the gap the compile-time guard cannot
  cover: CSS modules are typed Record<string, string>, so a renamed/removed
  class would otherwise silently drop that color's accent.
- Rename the "same workspace same color" test to describe what it actually
  asserts (a stable color per cwd, and distinct colors across workspaces).

* refactor(web-shell): address second follow-up review on workspace accent

- WorkspaceIndicator tests: assert on imported CSS-module class names instead of
  string literals, so a CSS-module naming change can't silently make the
  substring checks vacuous; add an expanded-mode (non-compact) accent test so a
  refactor that gated the accent on `compact` would be caught.
- workspaceColor.ts: run the CSS-class contract check unconditionally — throw in
  dev, but console.error in production — so a missing class in a prod build is
  at least diagnosable instead of a silent accent drop.
- WorkspaceAccent.module.css: correct the docstring to state exactly which
  tokens come from where — red/orange/yellow/green from --accent-* in
  App.module.css, blue/purple deliberately reusing the --agent-* brand tokens.

---------

Co-authored-by: wenshao <wenshao@example.com>
2026-07-16 02:38:58 +00:00
Shaojin Wen
93ccdf4070
feat(web-shell): maximize a single split pane (#6951)
* feat(web-shell): maximize a single split pane

Add a per-pane maximize/restore toggle to the split view. Clicking it makes
one pane fill the whole split and hides the others; the hidden panes stay
mounted so their sessions keep streaming (a purely visual solo). Restore via
the header button or Escape — Escape defers to the composer, the add-session
picker, and open dialogs so it never steals their key.

The toggle only appears with 2+ panes, adding a session exits maximize to
reveal the new pane, and the maximize is dropped whenever its pane leaves the
set or the split shrinks to a single pane.

* refactor(web-shell): use lucide icons for the maximize toggle; cover switch + picker-Escape

Address review on #6951:
- Swap the hand-written Maximize2/Minimize2 SVG paths for the named lucide-react
  components, per the web-shell icon convention (README) and matching DialogShell.
- Add tests for moving maximize between panes (guards the toggle's switch branch)
  and for Escape closing the add-session picker without un-maximizing (guards the
  pickerOpen deferral).

---------

Co-authored-by: wenshao <wenshao@example.com>
2026-07-15 12:10:29 +00:00
ytahdn
6e096cf16c
fix(web-shell): align split view chat interactions (#6633)
* fix(web-shell): align split chat interactions

* fix(web-shell): preserve split pane drafts on send failure

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-10 04:19:42 +00:00
Shaojin Wen
55b2886909
fix(web-shell): split-view pane fixes (remove "current" badge, clear composer on send) (#6454)
* fix(web-shell): remove meaningless "current" badge from split-view panes

In the split view every pane is an equal, independently interactive session (its own DaemonSessionProvider, SSE, transcript, and approvals), so tagging one pane as the workspace's "current" session carried no operational meaning. It only leaked a single-view concept into a peer-of-equals layout and reliably prompted "what is this?" questions from users. Panes are already identified by their titles.

Drop the isCurrent badge and its border highlight from ChatPane, and stop passing isCurrent from SplitView. The sidebar and session overview keep their "current" indicators, which are legitimate "you are here" navigation. currentSessionId is retained only to seed the initial pane.

* fix(web-shell): clear the split-view composer on send, not at turn end

A split-view pane kept the just-sent text sitting in its composer until the whole turn finished. handleSubmit committed the draft on the sendPrompt promise resolving, but that promise resolves via waitForAcceptedPromptCompletion (turn end), not at admission. Switch to the onAdmitted hook so the composer clears the moment the daemon accepts the prompt, matching the main view. A prompt rejected before admission still preserves the draft and surfaces the error.

* fix(web-shell): pass commitAccepted directly as onAdmitted; cover admit-then-fail

Review follow-up:
- commitAccepted is already `() => void`, so pass it directly as the onAdmitted option instead of wrapping it in a redundant `() => commitAccepted?.()` closure.
- Add a test for the turn failing after admission: the draft stays cleared (no second commit) and the error is still surfaced to onError.
2026-07-07 14:08:59 +00:00
Shaojin Wen
f41e95ac18
feat(web-shell): add Session Overview panel and in-window split view (#6400)
* feat(web-shell): add Session Overview panel and in-window split view

Add a large-screen "Session Overview" mission-control panel and an
in-window split view so users can monitor and drive multiple daemon
sessions at once.

- SessionOverviewPanel: ranked live cards (needs-approval -> running ->
  idle) merging the workspace session list with the detail=full status
  report. Multi-select opens the selected sessions as a split view in
  the current tab ("Open in split") or in a new browser tab ("Open in
  new tab", via a ?split=a,b URL).
- SplitView + ChatPane: one DaemonWorkspaceProvider hosting N
  DaemonSessionProvider panes, each a self-contained interactive chat
  (transcript, composer, streaming, tool/ask approvals). Browser focus
  scopes the keyboard per pane, so panes never contend over approvals.
- Sidebar entry points gated to large screens; the split view's Back
  returns to the Session Overview.

* refactor(web-shell): address review feedback on the session overview / split view

- SessionOverviewPanel: prune the selection Set when a session leaves the list
  (so a reappearing session isn't silently reselected) and make select-all use
  the intersection rather than prev.size.
- Extract isAskUserPermission into a shared util so App.tsx and ChatPane.tsx no
  longer keep verbatim copies that can drift.
- SplitView: dismiss the "add session" picker on Escape or a click outside it.
- Tests: MAX_PANES cap, popup-blocked path, checkbox-selects-without-navigating,
  stale-selection pruning, and a direct test for the extracted util.

* fix(web-shell): address /review findings on the split view

- ToolApproval: add a `keyboardActive` prop; split panes pass false so global
  Enter/Escape/digit shortcuts can't confirm the wrong session's approval, and
  the outer session's approval overlay is no longer rendered behind the split
  (where it would keep its global shortcuts while hidden).
- ChatPane: defer the composer commit until sendPrompt resolves, so a rejected
  prompt (transcript loading / disconnected / turn active) preserves the draft
  instead of silently dropping it.
- SplitView: include a per-mount nonce in each pane's clientId so two tabs
  opening the same split don't share a client id — which suppressOwnUserEcho
  would treat as a self-echo and drop from the transcript.
- SessionOverviewPanel: cap the split selection to MAX_SPLIT_PANES before
  building the ?split= URL or opening the in-window split, with a hint when more
  are selected; also dismiss the split picker on Escape / click-outside.
- Tests covering each.

* fix(web-shell): address second /review round on the split view

- SplitView: wrap each pane in its own ErrorBoundary, so a render crash in one
  pane (malformed block, unexpected tool shape) shows an inline fallback with a
  close action instead of white-screening the whole split.
- splitUrl / overview: carry the daemon token into the new-tab split URL's
  fragment. The current tab has already stripped the token from its URL, so a
  token-auth (`serve --open`) deployment would otherwise open the split tab
  unauthenticated. The token rides the hash (never sent to the server / logs).
- Tests: per-pane error isolation, token-in-fragment (and none without a token),
  and the overview polling effects (interval fires, document.hidden skips, and
  the in-flight guard prevents overlapping polls).

* fix(web-shell): hide the outer chat under the split and share app-level contexts

- App: hide (display:none) + aria-hide the outer chat subtree whenever
  mainView !== 'chat', not only when a panel is open. Previously the outer
  chat/composer/toolbar stayed reachable by keyboard/AT behind the full-page
  split (it was only covered visually). State is preserved (node stays mounted).
- App: wrap SplitView in the app-level WebShellCustomizationProvider and
  CompactModeContext so split panes render markdown / tool-headers / thinking
  the same way the single-session chat does. Todo contexts stay chat-only —
  they belong to the outer session, not the panes.

* refactor(web-shell): address review suggestions — coverage, dedup, split UX

- ToolApproval: add a dedicated test on the real component that the global
  keyboard shortcut is armed by default and NOT armed when keyboardActive=false
  (the cross-pane approval safety mechanism).
- SplitView: auto-exit to the Session Overview when the last pane is closed
  (guarded so an initial empty seed doesn't bounce straight back out).
- ChatPane: add tests for the cancel action, the empty/whitespace submit guard,
  and error routing to the onError prop.
- Extract the shared session-list page size + organization feature flag into
  constants/sessions.ts, used by the overview, split view, and sidebar, so the
  values can't drift between the three.

* fix(web-shell): surface outer approval + failed refresh in overview/split

- Split view: when the outer (main) session is waiting on an approval
  that's hidden behind the split, show a non-blocking notice banner with
  a "Go to it" button that returns to the chat where the approval lives.
- Auto-close the split (like the overview panel) when the viewport shrinks
  below the large-screen breakpoint, so users aren't stranded.
- Session Overview: surface a failed refresh inline (keeping the last-good
  cards) instead of silently swallowing it once cards are on screen.
- Tests: status-report poll cadence, picker dismiss (Escape / outside /
  inside click), inline refresh-failure banner.

* fix(web-shell): sever window.opener on split tab; tighten hidden-chat test

- openSelectedInNewTab now clears win.opener (the split tab carries a
  daemon token in its URL fragment) to prevent reverse tabnabbing, matching
  the existing bug-report window.open path.
- Strengthen the split-view App test so a missing outer-chat subtree fails
  instead of passing vacuously through an optional chain.

* fix(web-shell): split-view focus/stability/robustness follow-ups

- Refocus the composer after a shrink-driven split close so keyboard users
  aren't dropped onto <body> (skips when an approval or panel takes over).
- Stabilize SplitView onExit via useCallback so its last-pane-close effect
  doesn't re-fire on every App re-render.
- ChatPane: surface a per-pane connection-loss banner instead of silently
  showing stale messages when a pane's daemon connection drops.
- ChatPane: anchor the streaming timer to the active turn's start (last user
  message timestamp) so a pane opened mid-turn shows real elapsed time.
- Tests: split auto-close on shrink, outer-approval split notice + return-to-
  chat, connection banner, and streaming-timer anchoring.
2026-07-07 02:44:18 +00:00