Commit graph

14 commits

Author SHA1 Message Date
callmeYe
727c2d580c
fix(web-shell): prevent sidebar footer overflow (#6522) 2026-07-08 08:28:12 +00:00
Shaojin Wen
045bbee6ce
fix(web-shell): hide sidebar settings text when width is insufficient (#6494)
Prevent the 'Settings' label from wrapping to a new line when the
sidebar is narrow. Instead, the text is clipped via overflow:hidden
and only the gear icon remains visible.
2026-07-08 02:41:41 +00:00
Shaojin Wen
f7296d0333
feat(web-shell): add Qwen logo beside the sidebar new-chat button (#6437)
Place the Qwen brand mark to the left of the sidebar's New chat button.
The artwork is the same SVG used for the browser-tab favicon (and the
QwenLM GitHub avatar), inlined rather than hot-linked because the Web
Shell CSP is `img-src 'self' data: blob:`, which blocks remote images.
When the sidebar is collapsed there is no room beside the compact
button, so the mark is hidden and only the New chat button remains.
2026-07-07 08:36:48 +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
Shaojin Wen
9a63c03224
feat(web-shell): add a Scheduled Tasks management page (#6348)
* feat(web-shell): add scheduled tasks management page

Add a "Scheduled tasks" page to the Web Shell for managing durable cron tasks against the current workspace.

- Sidebar entry opens a full-pane page (replaces the chat area, not a modal) listing tasks with enable/disable toggle, delete, run-now, and human-readable schedules.
- "New scheduled task" opens a modal with a schedule builder (daily / weekdays / weekly / hourly / every-N-minutes / custom cron) and a live preview.
- "Create via chat" returns to the chat and primes the composer so the agent creates the task through its cron_create tool.
- Daemon CRUD routes (GET/POST/PATCH/DELETE /scheduled-tasks) read/write the existing per-project scheduled_tasks.json; task firing stays with the session-side scheduler.
- Extend DurableCronTask with optional name/enabled (backward compatible); the scheduler skips tasks with enabled:false.
- Add /scheduled-tasks to the vite dev-server proxy allowlist so the page works under npm run dev:daemon.

* chore(web-shell): address review feedback on scheduled tasks

- cron_list: surface name/enabled so the agent can tell a disabled durable task from an active one (a disabled task no longer looks identical to an active one).
- core: export only the tasks-file functions the daemon route actually uses (drop unused addCronTask / getCronFilePath / CRON_TASKS_DISPLAY_PATH from the public barrel).
- CronScheduler: warn when a durable reload fails and the prior view is kept, since a just-disabled or -deleted task can keep firing until the next successful reload.
- Extract the schedule helpers (buildCron / describeCron / parseHhmm / describeLastRun) into a pure module and add unit tests for them.
- Add route tests for PATCH cron/prompt/recurring, empty-patch rejection, and POST field-length / boolean-type validation.

* chore(web-shell): address second review round on scheduled tasks

- Log CRUD errors server-side (writeStderrLine) in each route catch block, matching the other daemon routes.
- Share one id generator (generateCronTaskId in cronTasksFile) between the scheduler and the daemon route instead of duplicating it.
- describeCron: recognize cron day-of-week 7 as an alternate notation for Sunday.
- Reset the builder time to :00 when switching to the hourly frequency (its time picker is hidden, so it no longer silently carries the daily minute).
- Tests: cron_list name/disabled output; route Feb-30 impossible-cron and corrupt-file 500 read-failure; describeCron dow=7.

* chore(web-shell): address third review round on scheduled tasks

- Run now: report sendPrompt rejections via the toast/error path instead of dropping the promise.
- Block chat interaction while the full-pane Scheduled Tasks view is open, so the covered composer can't receive keystrokes/Escape.
- Guard reload() with a request-sequence id so a slow load can't overwrite a newer list after a mutation.
- Re-enabling a task that had genuinely fired resumes from now instead of catching up work paused while it was disabled.
- Restrict "every N minutes" to divisors of 60 (a non-divisor */N fires more often than the label claims).
- Show a Repeats / Runs once label on each card so tool-created one-shots aren't mistaken for repeating schedules.
- Return generic 500 client messages (no internal file path); the detail is logged server-side.
- Tests: SDK scheduled-task methods (method/URL/id-encoding/headers/errors); route re-enable behavior both ways.

* chore(web-shell): address fourth review round (minor suggestions)

- Route error logs interpolate the actual task id instead of the literal ":id".
- cron_list returnDisplay includes the task name (matching llmContent) so terminal /cron list shows UI-assigned names.
- Truncate the delete-confirm label so an unnamed task's long prompt doesn't blow up the confirm() dialog.
- Cap the create-form prompt textarea at MAX_PROMPT_LENGTH and drop the dead typeof-window guard.
- Test generateCronTaskId (format + near-uniqueness).

* chore(web-shell): address fifth review round on scheduled tasks

- Re-enable now resumes any recurring task from now (stamp on every false→true), not only ones that had already fired — a task disabled before its first run no longer catch-up-fires the slot it was paused through.
- describeCron applies the same divisor-of-60 check as buildCron, so a hand-edited/persisted */45 falls back to the raw expression instead of a misleading "every 45 minutes".
- Strengthen the corrupt-file route test to assert the generic client message and no leaked file path.
- Tests: recurring-disabled-before-first-run and one-shot re-enable; describeCron non-divisor fallback.

* test(cli): cover legacy scheduled-task normalization on GET

Seed a pre-fields task (no name/enabled) directly to disk and assert the GET response normalizes it to name:null / enabled:true, guarding backward compatibility with existing scheduled_tasks.json files.

* fix(core): cap durable cron loads against a durable-only budget

The daemon route accepts up to MAX_JOBS durable tasks on disk, but the scheduler previously capped durable loads against its combined job map (session-only + durable). A session holding session-only cron jobs could push the map to MAX_JOBS and make loadFileTasks silently skip durable tasks the route had already accepted — a create that returned 201 would then never fire.

Cap durable installs against a durable-only count instead, and share one MAX_JOBS constant between the scheduler and the daemon route, so a successful create is always loadable. Adds a scheduler test that 40 session-only jobs no longer crowd out 20 durable loads.
2026-07-06 03:47:17 +00:00
Shaojin Wen
edc0555ed1
feat(web-shell): named session groups and color tags in the sidebar (#6350)
* feat(web-shell): named session groups and color tags in the sidebar

Extend web-shell session organization with named groups (create / rename /
delete, assign a session to a group) alongside quick color tags, and surface
pin / archive state. The grouping data is plumbed end-to-end through the
daemon.

- core: session-organization-service carries group id / name / color and
  pin / archive metadata on organized-list entries
- sdk / acp-bridge: session-list entries gain groupId / groupName /
  groupColor / archivedAt; add SessionGroupColor and list-session-groups
  result types
- cli/serve: dispatch + session routes expose listing and assigning groups
- web-shell: sidebar group management UI (create / rename / delete groups,
  color picker, pin, archive) and reuse the shared "Group" label for the
  group action, dropping the redundant "Move to group" string

* fix(cli): exclude color-tagged sessions from the ungrouped filter

Color / named group / recent are mutually exclusive buckets in the web-shell
sidebar — a color-tagged session shows in its color section, not "recent".
But the organized session-list `group=ungrouped` filter only checked
`groupId == null`, so a color-tagged session with no named group leaked into
ungrouped results for REST/ACP consumers, disagreeing with the UI taxonomy.

Align the server filter: ungrouped means no named group and no color tag.
Adds an ACP session/list test asserting a color-tagged session is excluded
from group=ungrouped (fails on the old filter, passes on the new one).

* fix(web-shell): clear color tag when creating a group for a session

saveGroupEditor's create-with-target path assigned the new group but left any
existing color tag in place, unlike the sibling assignSessionGroup /
assignSessionColor paths that keep color and named group mutually exclusive.
Because color takes precedence in the sidebar's section bucketing, the session
stayed in its color section and the group assignment had no visible effect.

Send `color: null` alongside `groupId` on that path, and extend the
create-group dialog test to assert the assignment clears the color.

* fix(cli): exclude color-tagged sessions from the named-group filter

Follow-up to the ungrouped filter fix: the per-group filter (group=<id>) also
ignored color precedence. Core and the REST/ACP update paths can persist both
groupId and color, and the sidebar renders such a session in its color bucket,
so group=<id> API consumers saw a session the web-shell shows elsewhere.
Require `color == null` there too, matching the sidebar taxonomy
(color > group > recent).

Adds an ACP session/list test for a session with both groupId and color set.
2026-07-06 02:19:16 +00:00
ytahdn
7605c8bd15
feat(web-shell): add onSessionChange and onSubmitBefore callbacks (#6333)
* feat(web-shell): add onSessionChange and onSubmitBefore callbacks

Add session-level event callbacks and a pre-submit interception hook
to WebShellProps, enabling external consumers to observe session
lifecycle events and gate prompt submissions.

New APIs:
- onSessionChange: fires on rename (SSE-driven), submit (direct and
  queued), and turn_complete (streamingState transition with error
  context including block ID).
- onSubmitBefore: async hook called before prompt submission; reject
  cancels the prompt with full retry-state rollback (lastSubmittedPrompt,
  lastSubmittedImages, retriedTurnErrorId, showRetryHint).

Sidebar integration:
- sessionListReloadToken triggers sidebar reload on session events
  with pollInFlightRef + document.hidden guards.
- Delayed 2s reload after submit to account for daemon registration lag.

Safety:
- isPreparingPrompt loading state during onSubmitBefore prevents
  duplicate submissions.
- streamingSessionIdRef prevents spurious turn_complete on session switch.
- All slash commands (including internal /language, /model) go through
  onSubmitBefore; queued prompts intentionally bypass it.

* fix(web-shell): add null initial value to delayedReloadTimerRef

React 19's useRef requires an explicit initial value argument.
Match the existing escapeTimerRef pattern: | null + null.

* fix(web-shell): move clearFollowup after onSubmitBefore gate and add tests

- Move clearFollowup() to after onSubmitBefore succeeds so that
  followup context is preserved when the before hook rejects
- Add null guard for clearTimeout on delayedReloadTimerRef
- Add 5 unit tests for sidebar sessionListReloadToken effect
  covering: token change, undefined, unchanged, document.hidden,
  and poll-in-flight gate conditions

Addresses PR #6333 review feedback.

* feat(web-shell): call onSubmitBefore for queued prompts

Previously enqueuePrompt bypassed onSubmitBefore entirely. Now the
before hook is also invoked for queued prompts — if it rejects, the
prompt is cancelled and not added to the queue. The composer still
clears synchronously (fire-and-forget) since the Composer's onSubmit
contract is synchronous (boolean | void).

Also updates the onSubmitBefore JSDoc to reflect this behavior.

Addresses PR #6333 review feedback on security gap.

* test(web-shell): cover session callback behavior

* fix(web-shell): preserve rejected queued prompts

* fix(web-shell): preserve rejected direct prompts

---------

Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-07-05 12:10:47 +00:00
jinye
7a528d078a
feat(daemon): Add session organization (#6305)
* feat(daemon): add session organization

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(daemon): address session organization review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(daemon): cover session organization review cases

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6305)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6305)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6305)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6305)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6305)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6305)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(web-shell): Address session organization review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): Harden session organization review edge cases

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(core): Address session organization review feedback

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-07-05 07:52:56 +00:00
jinye
59e771cef6
feat(daemon): Add session export endpoint (#6297)
* feat(daemon): add session export endpoint

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address PR review feedback (#6297)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: fix PR integration capability baseline (#6297)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* codex: address export tool call id review (#6297)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
2026-07-04 09:33:44 +00:00
Shaojin Wen
c37cb23ccc
feat(web-shell): manage sessions from the sidebar (archive, unarchive, delete) (#6293)
Add an Archive quick action and a "..." overflow menu (Rename / Archive / Delete) to each session row in the web-shell sidebar, plus a collapsible "Archived" section that lazily lists archived sessions with Restore / Delete. Thread the daemon's existing archiveState filter and archive/unarchive endpoints through the webui workspace facade and the useDaemonSessions hook; rename stays limited to the current live session.
2026-07-04 04:27:22 +00:00
Shaojin Wen
9b2fb30cb0
feat(web-shell): add a daemon status page backed by GET /daemon/status (#6272)
* feat(web-shell): add a daemon status page backed by GET /daemon/status

Surface the consolidated daemon status API (#5174) in the Web Shell as a
dashboard dialog opened from a sidebar footer button.

- @qwen-code/sdk: DaemonClient.daemonStatus(detail) plus DaemonStatusReport*
  wire types for the /daemon/status envelope (summary and full detail).
- @qwen-code/webui: loadDaemonStatus workspace action and a
  useDaemonStatusReport hook (exported as useDaemonStatus from
  daemon-react-sdk).
- web-shell: DaemonStatusDialog rendering one dashboard — overall status
  badge, issues list, daemon/runtime/transport/security/limits/capabilities
  cards, plus per-session, workspace-diagnostics, and auth sections. The
  daemon's summary/full cost split is hidden from the operator rather than
  exposed as a toggle: the cheap summary rides a 5s auto-refresh while the
  expensive full report (which may spawn the ACP child and aggregate
  workspace diagnostics) is fetched only on open and on manual refresh, so
  parking the dialog open never rehits that path. Capabilities are sorted,
  counted, and height-capped; the long workspace path stays on one line,
  front-truncated so the tail remains visible. New pulse-icon sidebar entry;
  EN/zh-CN strings.
- vite dev proxy: forward /daemon to the daemon; without it the SPA fallback
  answered /daemon/status with index.html and the dialog failed JSON parsing
  under npm run dev:daemon.

* fix(web-shell): address review on the daemon status dashboard

- Drive the status badge and issues list off the full report when it is
  available, not the summary. The daemon only rolls workspace/preflight/MCP
  problems into status+issues for detail=full, so the summary can read "ok"
  with no issues while a loaded full report is degraded — the dashboard now
  reflects the full rollup (live counters still come from the summary).
- Guard the 5s poll with an in-flight ref so a slow/degraded daemon cannot
  accumulate overlapping status calls (useDaemonResource discards stale
  completions but does not abort; the client timeout is 30s).
- Fix the public DaemonStatusReport wire type: runtime.channelWorker.channels
  is string[] (ChannelWorkerSnapshot), not an array of objects; mirror the
  remaining optional snapshot fields.

* fix(web-shell): translate workspace section status badges

WorkspaceSectionRow rendered the raw wire status (`ok`/`warning`/`error`/
`unavailable`) while every other badge in the dialog goes through `t()`, so
under a Chinese UI these badges showed lowercase English. Route the badge
through `t('daemon.level.<status>')` and add the missing `daemon.level.unavailable`
key to both dictionaries.

* fix(web-shell): scope toolbar error to summary; broaden dashboard test coverage

- The toolbar "failed to load" banner now keys on the summary fetch only. A
  failed full fetch is already surfaced in the diagnostics section, so it no
  longer makes an otherwise-healthy summary (fresh cards + timestamp) read as
  broken.
- Use the ASCII "..." ellipsis in the diagnostics-loading string to match the
  rest of the i18n dictionary.
- Add tests: summary-healthy/full-failed degraded state, the ACP-disabled
  transport branch, uptime/memory/duration formatting across unit boundaries
  (day, GB, sub-second, fractional-second), and sidebar Daemon Status button
  click (expanded + collapsed) — the feature's only entry point.

* fix(web-shell): pause polling on hidden tab; scope dev proxy; fix test mock

- Skip the 5s status poll while document.hidden, matching the sidebar poll —
  a backgrounded tab no longer hits the daemon every 5s.
- Narrow the vite dev proxy to the exact /daemon/status route instead of a
  bare /daemon prefix, mirroring the scoped /voice/stream entry; verified the
  dashboard still proxies (summary + detail=full) in dev.
- Add a message field to the DaemonStatusReport issue mock in the webui
  provider test so it matches the required DaemonStatusReportIssue shape.

* feat(web-shell): surface runtime/channel-worker diagnostics; a11y + polish

Address the daemon-status review round:
- Render the runtime startup/failure state (runtime.loading / runtime.error)
  in the Runtime card so the plausible-looking zero counters during startup
  are not mistaken for a healthy idle daemon.
- Surface channel-worker diagnostics (state, exit code/signal, error, restart
  count) when the worker is enabled — these fields were fetched and typed but
  never shown, leaving a bare "down" with no context.
- Include full.error.message in the diagnostics-failure line (matching the
  summary error path) so a failed detail fetch is actionable.
- Show "N/A" instead of a literal "null" chip for null workspace summary
  values (the wire type allows null).
- Add role="status" + aria-label to the health badge for screen readers.
- Rename the public hook alias useDaemonStatus -> useStatusReport, matching
  the Daemon-prefix-stripping convention of the other re-exports.
- Add tests: runtime startup/failure, channel-worker diagnostics, and the
  empty/disabled placeholders (sessions, rate limit, capabilities, ACP),
  toolbar-banner-with-data, and pure-loading branches.

* fix(web-shell): contain daemon status crashes; workspace empty-state

- Wrap the dashboard in a local ErrorBoundary so a malformed/partial daemon
  response (e.g. an older daemon omitting an additive field like
  channelWorker) — most likely exactly when the daemon is sick and the
  dashboard is most needed — shows a contained fallback instead of throwing
  to the root boundary and white-screening the whole web shell.
- Add an empty-state to the Workspace Diagnostics card (parity with the
  Sessions card) for when full.workspace is empty.
- Tests: error-boundary containment on a malformed report, and the workspace
  empty-state.

* fix(web-shell): fix error-boundary recovery; contain detail crashes

Address the review round (one Critical):
- ErrorBoundary recovery was broken: the comment claimed resetKeys cleared the
  fallback, but none was passed. Switch to a function-form fallback that
  surfaces the actual render error (distinct from a network failure) and fix
  the comment — recovery happens on re-open, since the parent only mounts the
  dialog while open.
- Wrap FullDetail in its own ErrorBoundary so a malformed detail=full payload
  is contained to the detail region instead of taking down the healthy summary
  cards with it; add a catch-all branch so a fetch that resolves without a
  `full` section shows a failed state instead of hanging on "Loading...".
- Toolbar failure banner now shows only when the summary errored AND still has
  data on screen (`summary.error && summary.report`), so it no longer
  misrepresents a dashboard that is rendering from the full fallback.
- SDK type: drop `& Record<string, unknown>` on DaemonStatusReport.daemon and
  add the typed optional `startup` field, matching the DaemonCapabilities
  convention the interface JSDoc claims.
- Add a real useDaemonStatusReport hook test asserting the `report` alias maps
  from `data` — the dialog test mocks the whole hook, so nothing else guarded
  it.

* feat(web-shell): surface runtime.activity in the daemon status dashboard

PR #6270 added a runtime.activity sub-object to GET /daemon/status
(activePrompts, lastActivityAt, idleSinceMs). Type it as an additive optional
on the SDK DaemonStatusReport and render it in the Runtime card: active-prompt
count and an idle duration ("no activity yet" when the daemon has seen none).
Gated on the field's presence so older daemons that omit it still render.
Verified end-to-end against a real qwen serve --web that emits the field.

* fix(web-shell): daemon status polish — i18n count, negative clamp, coverage

Address the review round (all minor):
- Move the capabilities count into the i18n string (daemon.capabilities.titleCount
  with a {count} placeholder) so locales can reorder it.
- Clamp negative durations in formatDurationMs (clock-skew defense).
- Re-export the hook options type as StatusReportOptions for consumers wrapping
  useStatusReport.
- Tests: use the real rate-limit tier keys (prompt/mutation/read) in the fixture,
  and cover the session-id display fallback, the channel-worker signal branch,
  and a healthy workspace section's chip/status rendering.

* feat(web-shell): name the failing checks behind a workspace section status

A "warning"/"error" workspace-diagnostics section only showed a rollup badge
plus count chips, so e.g. a warning preflight was opaque — the operator
couldn't tell it was the auth check without curling the API. Extract the
individual warning/error cells from the section's raw data (across cells /
servers / skills / tools / providers / hooks / extensions) and render each with
its label and message (e.g. "auth: No auth method configured."). OK and other
non-problem cells stay hidden. Verified end-to-end: a real daemon with no
credentials now shows the auth warning inline under preflight.
2026-07-04 00:35:51 +00:00
Shaojin Wen
da22360c25
feat(web-shell): show the qwen-code version in the sidebar footer (#6222)
* feat(web-shell): show the qwen-code version in the sidebar footer

The Web Shell had no visible version. Show the running qwen-code version (from the daemon capabilities) in the sidebar footer, inline with the Settings button so it stays visible without taking its own row.

Render the version consistently wherever it appears:
- Prefix "v" only for a real semver release; a non-semver fallback such as "unknown" is shown as-is, so we never render a bogus "vunknown". Applied to the Web Shell badge and the TUI header.
- Dev builds (scripts/dev.js) now report the real package version instead of the "dev" sentinel, matching scripts/start.js, so the UI shows the actual version (e.g. v0.19.4). DEV=true / NODE_ENV=development remain the signals that mark a dev build.

* test: add readFileSync to node:fs mock in dev.test.js

scripts/dev.js now reads package.json via readFileSync at module load to
report the real CLI_VERSION, but the node:fs mock in dev.test.js did not
export readFileSync, causing vitest to throw "No readFileSync export is
defined on the node:fs mock" and failing the suite.
2026-07-03 08:56:08 +00:00
pomelo
7b9e31885b
feat(web-shell): add mobile sidebar drawer with session list (#6003)
Some checks are pending
E2E Tests / E2E Test (Linux) - sandbox:docker (push) Waiting to run
E2E Tests / E2E Test (Linux) - sandbox:none (push) Waiting to run
E2E Tests / E2E Test - macOS (push) Waiting to run
* feat(web-shell): add mobile sidebar drawer with session list

Replace the display:none behavior at viewport <=760px with an overlay
drawer pattern. A hamburger menu button appears on mobile, tapping it
slides the existing WebShellSidebar in as a fixed overlay with a
semi-transparent backdrop. Selecting or creating a session auto-closes
the drawer. Desktop layout (>=761px) is unaffected.

Closes #6000

* fix(web-shell): address review feedback for mobile sidebar drawer

- Use display:contents for desktop wrapper transparency (Critical: sidebar was hidden)
- Fix z-index stacking so sidebar renders above backdrop in drawer
- Force sidebar expand when mobile drawer is open (collapsed state)
- Hide resizeHandle on mobile to prevent touch scroll conflicts
- Reset drawer state on viewport resize via matchMedia listener
- Add role=dialog, aria-modal, Escape key dismissal, body scroll lock
- Add aria-expanded to hamburger button
- Close drawer when opening Settings or resuming sessions

* fix(web-shell): address second round of review feedback

- Remove dead :global(.sidebar) selector (CSS Modules hash class names)
- Fix Escape key capture-phase handler to not intercept sidebar inputs
- Conditionally apply role=dialog/aria-modal only when drawer is open
- Stop toggling collapsed prop on drawer open/close to preserve sidebar state
- Add closeMobileDrawer() for bare /resume command path
- Fix hamburger button vertical centering in empty chat state on mobile

* fix(web-shell): fix stacking context and escape handler in mobile drawer

* fix(web-shell): prevent iOS Safari background scroll when drawer is open

* chore: remove accidentally committed .qwen-session and gitignore it

The .qwen-session file is a developer-local session UUID generated by
qwen serve. It was accidentally committed to the repo and should never
be tracked.

* fix(web-shell): address review feedback for mobile drawer

- Don't preventDefault touchmove inside the drawer so the session list
  can scroll natively; only block scrolling on the page behind it.
- Defer Escape to a pending tool/permission approval (reject) instead of
  closing the drawer when a prompt is visible.
- Reuse isEditableTarget from utils/dom and only bail out for editable
  targets outside the drawer, so the drawer search input still closes on
  the first Escape.
- Close the drawer before awaiting loadSession so it doesn't linger over
  the old transcript, matching the other session-switch paths.
- Keep the drawer panel visible until the backdrop finishes fading out to
  avoid a one-frame flicker on close.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(web-shell): mobile drawer ignores collapsed rail + block backdrop scroll

- collapsed: a user who collapsed the desktop sidebar got a mobile drawer that
  still rendered as the icon rail (no session list — the whole point of the
  drawer). Force the expanded layout while the drawer is open.
- touchmove: the allowlist matched the outer [data-mobile-drawer] wrapper, which
  also contains the full-screen backdrop, so a touchmove starting on the dim
  backdrop skipped preventDefault and let iOS Safari scroll the page behind.
  Exclude the backdrop so only the panel keeps native scroll.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

* fix(web-shell): harden mobile drawer collapse, error path, and width cap

- Hide the sidebar collapse button while the mobile drawer is open so its
  no-op toggle can no longer silently persist desktop collapsed state.
- Close the drawer before awaiting createSession() so a failed create no
  longer leaves the drawer stuck open with page scroll locked.
- Drop redundant width/min-width/position from .sidebar.mobileOpen and cap
  it with max-width:100vw so a wide persisted width can't overflow phones.

Co-Authored-By: Qwen-Coder <noreply@qwen.ai>

---------

Co-authored-by: pomelo-nwu <czynwu@gmail.com>
Co-authored-by: Qwen-Coder <noreply@qwen.ai>
2026-06-30 15:34:10 +00:00
ytahdn
22389247d5
feat(web-shell): add optional session sidebar with navigation (#5931)
Co-authored-by: ytahdn <ytahdn@gmail.com>
2026-06-27 12:39:46 +00:00