Skip to content

feat(desktop): split-pane multi-session, kanban live events + attachments, full locale parity - #58338

Open
rod-nxtlevel wants to merge 6 commits into
NousResearch:mainfrom
rod-nxtlevel:feat/desktop-split-pane-live-kanban
Open

feat(desktop): split-pane multi-session, kanban live events + attachments, full locale parity#58338
rod-nxtlevel wants to merge 6 commits into
NousResearch:mainfrom
rod-nxtlevel:feat/desktop-split-pane-live-kanban

Conversation

@rod-nxtlevel

Copy link
Copy Markdown
Contributor

What

Three additions, stacked on #58138 (which stacks on #58109 + #58137):

Split-pane multi-session (2 panes)

Two chat sessions side-by-side in one window — independent composer, streaming, busy state, and profile per pane.

  • Architecture: the existing view singletons keep their exact names and modules, redefined as the active pane's view (~60 consumer files untouched, including the non-React store modules React context can't reach). The split pane runs a factory-created PaneSessionView bundle; the per-session state cache gained a multi-view fan-out (line-for-line legacy behavior with one registered view — its existing test passes unedited).
  • Profile correctness: sends/resumes execute only from the active pane (pointerdown-capture activation); the split's request wrapper awaits the gateway-profile swap, closing the Enter-beats-swap race; background main-pane RPCs ride a profile-pinned socket; split boot-restore uses a non-swapping ensureProfileSocketOpen so it can never steal the active gateway.
  • UX: ⌘⇧\ toggle (rebindable), palette entries, 'Open in split' on session rows; same session never opens in both panes (dispatcher + history-nav enforcement); split state/width persist; narrow windows collapse and restore; socket keep-set + native-notification foreground rules cover both panes.
  • Adversarial review caught and we fixed: background main-pane misroutes over the split's socket, mirror teardown restoring stale identity, boot-restore focus stealing, cross-pane sticky-model writes, profile-tag drops on pane-created sessions.

Kanban live events + attachment transfer (extends #58137)

  • /api/plugins/kanban/events WS with the dashboard's canonical auth (loopback ?token= / gated single-use ?ticket=), ?since= cursor, ?board= pinned at handshake; 250ms-coalesced refetch; healthy socket stretches the 4s poll to a 60s safety net; any drop resumes fast polling; stale preload degrades to polling.
  • Attachments: download with collision-resolved save + Reveal toast; multipart upload with a 25MB pre-check mirroring the backend cap; binary-capable authed fetch in main for both token and OAuth modes.

Locale parity

ja.ts and zh-hant.ts gain every key missing relative to zh coverage (360 keys each) — kanban, badges, overview, split-pane strings, and long-standing gaps. All four locales now move together.

Tests

100+ new cases across split store/mirror/dispatcher/profile-pinning/composer-bus-gating/keep-set/notifications, live-events lifecycle, attachment transfer, and i18n. Typecheck + lint clean; full-sweep failure set byte-identical to the pre-existing baseline (stash-verified).

🤖 Generated with Claude Code

rod-nxtlevel and others added 6 commits July 4, 2026 03:12
Board overlay (CronView pattern) over the existing /api/plugins/kanban
backend: 8 dnd-kit columns in BOARD_COLUMNS order, card drawer with
comments/events/runs/worker-log/attachments/DAG links, new-card dialog,
board switcher, dispatcher nudge.

- HOME-scoped by design: no profileScoped()/?profile= on kanban routes.
- Server-authoritative mutations: optimistic moves reconcile from the
  server response (rule bounces like done→review render where the server
  put them, with an info toast); 409s revert with the backend's detail.
- 'running' is never a drag target (backend 400s direct PATCH; ready→
  running only via dispatcher claim) — Nudge dispatcher instead.
- 4s visibility-gated polling with latest_event_id cursor (atom identity
  preserved when unchanged); overlay wrapped in its own ErrorBoundary.
- parseKanbanApiError unwraps the Electron IPC error prefix so HTTP
  status/detail survive the bridge.
- i18n en+zh (+types); ja/zh-hant fall back to English.
- 22 vitest cases (store rules, move/revert/bounce, cursor identity,
  component smoke via mocked hermes:api).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- resolveGatewayWsUrl maps mint failures to reauth only when auth-shaped
  (401/403/needsOauthLogin); transport errors stay retryable.
- Failed initial boot retries on 5s/15s/30s/60s jittered backoff and on
  wake/online/visibility nudges; genuine auth failures latch with the
  sign-in overlay instead of thrashing.
- After a socket reconnect (primary or secondary), reconcile cached busy
  sessions against session.active_list — clears composers stuck on
  'Thinking…' when message.complete died with the socket.
- Coalesce refreshSessions (1.5s trailing + single-flight, ancillary
  cron/messaging fan-out throttled to 30s) and debounce session.info
  config refetches (5s, gated to config-bearing events).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ross-profile overview, scoped query caches

- Per-profile last-session memo: switching profiles restores each
  profile's last open session (existence-checked, probe fallback,
  token-guarded against races; deleted/archived sessions evicted).
- Profile rail badges: working spinner dot + amber needs-input count per
  profile with session tooltips; click opens the neediest session in
  that profile. dnd-kit sortable behavior preserved.
- Command center gains an 'Overview' landing section: per-profile
  working/attention/total/last-activity rows with quick-switch, plus a
  kanban summary strip (per-status counts + hot review/blocked cards)
  that degrades gracefully when the backend lacks the kanban plugin.
- react-query keys are profile-scoped (config, config-schema, skills,
  toolsets, hub sources/search, model-options); the blanket
  invalidateQueries() on profile change is gone — profile hopping hits
  warm caches with no cross-profile bleed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The overlay X floats absolutely at right-3 (w = --titlebar-control-size)
and the panel header rides up into the titlebar band, so right-aligned
PanelHeader actions rendered underneath it (first visible in the kanban
view — 'New card' overlapped the close button).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nt coverage

- Live /api/plugins/kanban/events WebSocket: main-process mints authed WS
  URLs (loopback ?token= / OAuth single-use ?ticket=) with ?since= cursor
  and ?board= pinned at handshake; renderer singleton connection with
  250ms-coalesced refetch on events, catch-up on open, 1s..30s backoff;
  healthy socket stretches the 4s poll to a 60s safety net, any drop
  resumes fast polling. Stale preload degrades to polling.
- Attachments: download to ~/Downloads with collision resolution and a
  Reveal toast; multipart upload with a 25MB pre-check mirroring the
  backend cap; binary-capable authed fetch in main (token + OAuth modes);
  errors surface backend detail via parseKanbanApiError.
- ja.ts and zh-hant.ts gain all 360 keys missing relative to zh coverage
  (kanban, badges, overview, and long-standing gaps).
- PanelHeader actions now clear the floating overlay close button.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two chat sessions side-by-side in one window, each with independent
composer, streaming, busy state, and profile.

- Singletons keep their meaning as 'the active pane's view'; the split
  pane runs a factory-created PaneSessionView bundle fed by the
  per-session state cache's new multi-view fan-out. Single-pane behavior
  is unchanged (pinned suites pass unedited).
- Sends/resumes execute only from the active pane; the split's request
  wrapper awaits the gateway-profile swap (closes the Enter-beats-swap
  race); background main-pane RPCs ride a profile-pinned socket; boot
  restore uses ensureProfileSocketOpen and never steals the active swap.
- Open via ⌘⇧\ (rebindable), command palette, or 'Open in split' on any
  session row; same session never opens in both panes; split state and
  width persist; narrow windows collapse the split and restore it.
- Socket keep-set and keepalive include the split session's profile;
  native notifications treat both visible panes as foreground.
- 50+ new tests (split store, cache fan-out, mirror, dispatcher, profile
  pinning, composer bus gating, keybind, keep-set, notifications).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@rod-nxtlevel
rod-nxtlevel requested a review from a team July 4, 2026 15:35
@alt-glitch alt-glitch added type/feature New feature or request comp/desktop Electron desktop app (apps/desktop/*) comp/cron Cron scheduler and job management P3 Low — cosmetic, nice to have labels Jul 4, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the substantial desktop implementation. The feature premise remains valid on current main: apps/desktop/src/app/desktop-controller.tsx:1112 constructs one ChatView, and both chat routes use that element at :1289-1291.

Problems

  • apps/desktop/src/store/kanban.ts:115-117 has a board-switch race. It reads $kanbanActiveBoard for the request, but unconditionally publishes the response after await. If the user selects another board meanwhile, setKanbanActiveBoard() clears the view (:58-70), while the new refresh returns early because $kanbanRefreshing is already true (:107-110). The old response can then repopulate the new selection with the previous board until polling retries.

Suggested changes

  • Add a board-selection generation/token around refreshKanbanBoard() and discard stale responses; schedule the latest board's refresh when an earlier request settles. Cover a deferred old-board response followed by a board switch.

Automated hermes-sweeper review.

try {
const next = await getKanbanBoard($kanbanActiveBoard.get())

$kanbanBoard.set(reconcileKanbanBoard($kanbanBoard.get(), next))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commits a response even if setKanbanActiveBoard() changed the selected board while line 115 was awaiting. The replacement refresh is suppressed by the single-flight guard, so the old board can repopulate the new selection until polling retries. Please make this request generation/board-scoped and add a deferred-response board-switch test.

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 15, 2026
@teknium1 teknium1 added the area/sessions Session lifecycle, resume, persistence, history label Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/sessions Session lifecycle, resume, persistence, history comp/cron Cron scheduler and job management comp/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants