mirror of
https://github.com/turnstonelabs/turnstone.git
synced 2026-08-12 23:12:23 -06:00
main
45 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
998271b016 | feat: convert large pastes to attachments | ||
|
|
480a1426b3 |
Fail-closed history-commit handoff (#1005)
* fix(session): fail-closed history-commit handoff (#981) The deleted-workstream discovery is now a terminal, ws_id-keyed latch: keyed conversation commits refuse admission once the durable parent is gone (convergence finalizers and force-abandon are exempt), history handoff refuses to mint a proof token so /history fails closed with a 503 instead of silently wiping the pane, and the SSE stream carries a workstream_gone resync reason. Discarded commits leave a forensic log of commit keys and roles, never content. Conversation rows gain a commit_key (migration 071): keyed saves are idempotent under retry, validated against the full commit identity, and refused when they would cross a workstream deletion. The prune orphan category now requires a NULL alias plus a two-hour updated grace, with cutoffs computed at discovery time and carried into both dialects' rechecks. The mid-turn interjection queue is owner-partitioned with no per-site mode flags: pops take the acting principal's and unowned rows, other participants' rows are structurally retained, and enforcement lives at queue admission plus the shared before_spawn gates. The retraction ledger is bounded by open pop windows: pops open a window atomically with the queue delete, restores close their ids atomically with the ledger consume, every other exit closes through one helper, and misses for unheld ids record nothing. The workstream-gone latch refuses unattended wakes at all three gates (watcher spawn, claim, delivery pre-pop), and the retry dispatcher regained its pre-envelope cancel/error convergence net. Persistence-state reporting derives through the session bound to each UI instead of a registry lookup by id that failed open to healthy during tombstone retention. The dashboard roster no longer re-inserts ghost entries from trailing activity events, the history tool-outcome scan tolerates interleaved non-turn rows, and the shared handoff-deadline handle owns its own retirement. Single-sourced across call sites: keyed-commit row values, attachment save wrappers, tail-truncation and conflict-resolution bodies for both storage dialects; worker-slot lifecycle field sets; the direct-commit admission frame; queued-row layout accessors; the string-aware comment stripper shared by every JS harness suite. Refs #981 #964 * fix(session): sweep handoff fixes to their sibling surfaces The interactive replay loop treated a system row as a tool-batch boundary, so every tool result after an interleaved row vanished from that pane while the coordinator rendered the same history correctly. Only a conversational turn ends the batch window now, matching the shared outcome index. Accepted user turns clear the composer's attachment chips on the same viewer policy that settles optimistic bubbles rather than on having matched a local bubble, so a workstream created with an upload no longer keeps a chip for an attachment the create dispatch already consumed. The coordinator's raced-Stop arm emits the stream-end hook it inherits alongside the idle state, leaving no unfinalized bubble or unflushed tool output. Ending a session surfaces a failure toast when the request never lands or answers with a non-JSON body. The per-second persistence reconcile now probes each session without blocking: a workstream whose generation and handoff locks are held is skipped until the next pass instead of contending the locks every commit needs. The one-shot repair that gates workstream creation at capacity keeps a definite probe — it has no next pass, and the sessions likeliest to be contended are the ones whose unresolved journals emptied its candidate list. Single-sourced: the attachment lane builds its conversation row through the shared commit-identity builder; the ordinary worker exit releases its slot through the lifecycle owner; both operator surfaces snapshot their counters through one non-consuming helper; the replay preamble loses its per-kind wrappers and its config hook; the browser harness suites share one brace walker; and each in-flight history attempt is one record carrying both its abort controller and its deadline. Refs #981 #964 |
||
|
|
729a02a833 |
feat(ui): copy-to-clipboard for messages and rendered blocks
Three idle-only affordances on every chat surface: a persistent copy button in each assistant bubble's actions bar, a pointer-only floating button over the hovered markdown block (fence, mermaid diagram, table), and Enter on a focused block for keyboard users, with the outcome flashed on the block itself. Copy resolves to SOURCE, not rendered text. The renderer stashes each table's raw markdown in data-md-source at render time — span sentinels restored in reverse mask order, footnote-definition bodies restored to raw before their recursive render — and whole-message copy reads the streaming pipeline's per-frame stash. The clipboard transport falls back to the legacy execCommand path for plain-HTTP LAN nodes, cloning and restoring the user's selection and focus. Outcomes surface button-local only: flash + title + one live-region announcement through the shared makeAnnouncer factory (also adopted by the interactive voice/tool announcers, whose lazily created regions swallowed their first announcement). Busy refusals answer with their own message. Coordinator retry and admin token-copy keep zero-module-dependency degrade paths. |
||
|
|
6f194d338f |
fix(console): restore back-to-console from a proxied node view
The console proxies a node's web UI at /node/{id}/ and injects a shim into
the page it fetches upstream. The only way back was a node-picker menu the
shim built into #ui-header — an element the L-shell renovation (
|
||
|
|
984a10307e |
feat(coordinator): MCP tool surface for coordinator sessions (#725)
Coordinator-kind workstreams get the same MCP surface as interactive sessions — tools, resources, and prompts (read_resource/use_prompt go dual-kind) — gated per-persona exactly like interactive, with no separate feature flag. The console hosts its manager with node parity end to end: boot calls create_mcp_client inline (same catalog resolution: DB rows, then mcp.config_path, then this host's config.toml), the admin reload fan-out lazily constructs and reconciles it under a lock (the node's unlocked equivalent is #873), per-server refresh/reconnect and the admin MCP status view cover it under the collector's console pseudo-node id, and shutdown follows LIFO teardown. Sessions read the live manager through a per-construction getter — the console counterpart of the node factory's mcp_ref[0] read; client presence is the session-level contract, and the kind-aware tool assembly runs the same listener/prime/rebind skeleton as interactive. bind_acting_user re-scopes listeners and per-user pools, which is security-critical for multi-sender coordinators. The wire-safety status projections move verbatim to core/mcp_utils so both hosts present one schema (node endpoint bodies byte-identical); the console's per-server action classification is a pinned COPY of the node endpoints', with a parity test driving both sides across the outcome matrix that fails if either drifts. The shared MCP error card (consent / re-consent / forbidden / operator) moves to mcp_error.js + mcp_error.css, linked by all three card hosts and pinned by className→rule and host→link parity tests; the module joins the whole-file sink-scan and var-ratchet lists. Reload reporting is honest about the console entry: excluded from the unreached-node warning's list and denominator, and the toast claims "+ console" only for a real reconcile, with an explicit note on failure. The pending-consent badge (#874's console half) ships too: the console defines the same onConsentDetected seam the node dashboard exposes — lighting up the shared pane host's existing bridge for hosted interactive panes — and the coordinator pane threads its card's detections through the single MCP-error helper. The badge rides the Admin > MCP Servers rail row, hydrates at boot from the Phase 9 pending-consent endpoint the console already serves, re-syncs to DB truth when the operator views the MCP panel, and the rail-less standalone page carries a status-bar chip instead. A coordinator that hits a consent wall unattended now has a persistent, glanceable signal. Pre-existing bugs fixed along the way: create_mcp_client returned None on pool-only installs, leaving any host managerless after restart until the next admin MCP write; admin_import_mcp_config never scheduled the reload fan-out (stale catalogs after import); the admin settings UI rendered the coordinator settings section unordered and unlabeled. Follow-ups: #873 (node reload double-construct race); #874 narrows to the admin-MCP-view per-server indicator. |
||
|
|
ace9e034f9 |
fix(ui): ephemeral panes close on split-dismiss instead of orphaning a tab
The preview pane opens beside the conversation as a split cell. Dismissing
that cell — the per-cell chip, or Unsplit from the other pane — ran
closeCell(), which hides the pane but keeps it in _panes/_order, leaving an
orphan tab with no meaningful reopen (the reopen affordance is the transcript
chip, not the tab bar).
Add an `ephemeral` flag on ShellPane. For an ephemeral pane the cell chip and
Unsplit route to close() — destroying the pane and its tab — and the chip's
glyph/label read as a destructive close rather than a reversible hide. Unsplit
still spares the focused survivor even when it is ephemeral ("keep the focused
pane"). The preview pane sets the flag; conversational panes do not, so an
all-conversation split is unchanged (Unsplit reduces to the prior
_exitLayout(_activeId)).
|
||
|
|
e010124008 |
feat(preview): rich preview pane + open_preview tool
Tool results only ever rendered as plain text in the transcript. This
adds the model-driven rich-preview lane every comparable surface has,
in turnstone's developer-tool idiom: a preview pane that opens BESIDE
the conversation, keyboard-operable, sandboxed, never replacing the
transcript that spawned it.
Backend
- New built-in open_preview(target, kind?, title?): resolves an http(s)
URL, a file path, or attachment:<id> to bytes; classifies into
web/pdf/image/table/text/markdown (magic bytes > MIME hint >
extension > UTF-8 fallback, legacy-charset pages transcoded); caps
size per kind; persists content-addressed with kind="preview" —
refcounted and GC'd with the workstream, skipped by trajectory
reconstruction so preview bytes can never materialize onto the wire.
URL targets gate like web_fetch (network egress); paths/attachments
run unprompted like read_file.
- New core.web.fetch_with_ssrf_guard: manual redirect walk that
SSRF-screens every hop BEFORE requesting it (follow_redirects=True
checked nothing between hops); adopted by both open_preview and
web_fetch. URL userinfo is stripped before the descriptor or the
stored bytes see it; <base href> is injected doctype-safely so
relative assets resolve without quirks mode.
- The preview descriptor rides the tool turn's meta side channel with
ONE shape on every boundary: the live tool_result SSE event, the
conversations.meta column, and the /history projection. Cancelled
batches commit an already-announced preview (blob + meta) instead of
stranding the open pane on a permanent 404.
- New GET {ws}/attachments/{id}/preview (read scope, same ownership
gate as /content) serves the STORED type with per-MIME hardening:
bare CSP sandbox for text/html (renderable, scriptless, opaque
origin), no CSP for application/pdf (Chromium's viewer refuses
sandboxed contexts), full default-src 'none' otherwise; filenames
fold to latin-1-safe ASCII. The console /node proxy now forwards
CSP/nosniff/disposition/cache-control instead of dropping them.
- History loads exclude preview blobs from the bulk content fetch at
the query (they were read and discarded on every load).
Frontend
- New "preview" pane type registered in the shared shell (server +
console): openPaneBeside placement, per-kind renderers — fully
sandboxed iframe for pages, browser PDF viewer, sortable tables
(CSV/TSV/JSON, ragged-file safe, 5k-row cap), rendered markdown,
text — plus back/forward history with arrow keys, reload persistence
via pane meta, and backoff auto-retry (0.9s..7.2s) bridging the gap
between the live descriptor and the batch fold that commits its blob.
- Tool results carrying a descriptor render a credential-redacted
preview chip (the reopen + replay affordance); live results auto-open
the pane only while the originating pane holds focus.
Docs: docs/tools.md + prompts/tools.md. Tests: policy unit tests, tool
prepare/exec (mocked fetch), serving route + proxy header pass-through,
storage exclusion on both backends, cancel-path commit, JS static
guards; a headless-Chrome harness drives the real module graph (32 DOM
assertions).
|
||
|
|
93a7486cc2 |
Add client-side credential redaction for tool call cards
New shared ES6+ module (redact_credentials.js) provides comprehensive visual credential censorship matching the backend output guard patterns: - PEM private key blocks, connection strings, Bearer tokens - OpenAI / GitHub / AWS / Google API key formats - Query-string and JSON-style credential values - JSON secret keys (api_key, password, token, authorization, etc.) - ENV secret lines (SECRET_KEY=, DATABASE_URL=, etc.) Integrated into both frontend surfaces: - interactive.js: replaces legacy minimal _redactApiKeys function - conversation.js::buildConvResult (shared substrate, used by coordinator) - coordinator.js::renderToolOutput fallback paths Backend parity: added 'authorization' to the JSON secret regex in output_guard.py so the output guard flags and redacts Authorization headers in JSON tool output. Tests: ported the runtime smoke test from the removed _redactApiKeys to import the new module directly; added redact_credentials.js to the var-free and const-reassign guard bundles. |
||
|
|
2d4cb6fea9 |
fix(ui): make pane hotkeys work off macOS and match across surfaces
The pane/workstream accelerators only worked on macOS. They were bound to Ctrl, which on Windows/Linux IS the browser's own accelerator: Ctrl+T, Ctrl+W and Ctrl+1-9 were swallowed by the browser (new tab / close tab / switch tab) and never reached the page. macOS browsers own Cmd instead, so Ctrl was free there and everything appeared to work. On top of that the shortcuts were declared in three places that had drifted apart — the "?" overlay, each app.js keydown handler, and the tab-menu badges in shell.js. The console fell to convTabMenu's node-proxy fallback lane, which dropped every shortcut badge (and Fork), so its tab menu showed no accelerators and Ctrl+W there just closed the browser tab. Choose the modifier per platform (Ctrl on macOS, Alt on Windows/Linux) and make shell.js the single source of truth for the per-pane accelerators: a stable accel registry drives both the platform-aware badge and one shared keydown handler that invokes the ACTIVE pane's own menu item, so a badge can't advertise a chord the handler ignores and each surface contributes only what it supports (the console omits Fork; it has no fork surface yet). Each surface's app.js keeps only its global accels (new / switch / dashboard); the console regains switch + dashboard to match. Mod+W now uniformly means Close pane (drop the tab, session keeps running), matching its badge and the universal Ctrl+W convention — previously the standalone's Ctrl+W stopped the session. The previously-dead "Refresh title / Ctrl+Shift+R" is wired, and Ctrl+T / Ctrl+D yield to text editing while a field is focused (macOS transpose / delete-forward). |
||
|
|
09c05733c6 |
test(personas): harden the guard suite — real paths over scripted events
The rank guard now derives needs_approval through the real _prepare_tool on a bash call under an allowlisting persona instead of scripting the flag, and asserts the approval gate actually fires. The row-shape guard's source-grep is replaced with behavioral collector tests driving both ws_created lanes (poll-diff and SSE relay), plus a proxy-forward twin and a saved-list value assertion that would catch positional column mix-ups. Receiving-side stamping gets its first HTTP coverage: create with an explicit persona under workstreams.create only (selection needs no persona perm), kind-mismatch and unknown-name 400s, omitted-persona default stamping, the 503 on a failed default lookup, and the clean-None legacy lane. Resume adoption is pinned end to end: a corrupt target stamp leaves the session fully intact, an MCP-on stamp is refused when the client was persona-gated at construction, and an MCP-off stamp drops the live surface (listeners deregistered, toolsets reset). Soft-set tool_search expansion recomposes the prompt exactly once; legacy sessions never recompose. Compaction legs run real flows now: spill plus the recall-pointer variant under memory-off with recall visible vs hidden, and a full stamp surviving compaction-then-resume. Migration 063 gains the downgrade config-cleanup case (stamps removed, creative_mode preserved) and the conversion idempotency case (already-stamped creative rows don't crash the upgrade). RBAC coverage goes cross-perm: read-only and write-only principals hit every verb (a wrong-perm-name regression in any handler is now visible), archive and default-flip succeed through PATCH, persona.* strings round-trip the role editors and the overrides overlay, and the production route table is asserted directly (no DELETE registered). Endpoint/storage fixtures move off migration-seed names; storage hardening tests cover the size caps, corrupt-row reads, the TypeError-to-ValueError ordering, the duplicate-name race mapping, and the single-default backstop. Shell asserts pin the new picker surfaces and drop the last persona-as-kind wording. |
||
|
|
e2dcd2bd6b |
fix(personas): apply review findings — stamp adoption on fork/restore, PATCH semantics, gating
Review pass over the branch surfaced real defects, all fixed here with regression guards: - Fork-resume (resume_ws) adopts the SOURCE workstream's stamp, resolved pre-construction so all four levers (including the construction-time MCP gate) bind the fork; a corrupt source stamp is a loud 400, an unstamped legacy source forks unstamped — never the kind default. Watch-restore and CLI --resume thread the stamp the same way, closing an MCP leak where a restored MCP-off workstream re-merged the catalog. - SessionManager.open parses the stamp inside the install guard so a corrupt stamp releases the reserved slot; a retry reproduces the loud error instead of 'already tracked'. - Mid-session resume() adopting a stamp rebuilds the tool_search pathway to match (hard set drops it, soft set force-constructs it); soft persona sets survive the global tool-search setting being off. - Memory nudges gate on actual memory-tool VISIBILITY, not just the memory lever, so an allowlist that hides the tool also silences the nudges that point at it; post-compaction resume gets a no-recall nudge variant when the pointer would dangle. - Console PATCH: explicit null flags from UpdatePersonaRequest no longer archive the persona or flip levers on a rename; multi-kind personas survive a shelf edit; admin list ships the per-kind tool_inventory so the shelf checklist tracks the server inventory instead of a hardcoded JS list; admin CRUD moved off the event loop. - Migration 063 converts legacy creative_mode workstreams to the full writer stamp (downgrade removes all persona keys). - REPL: /new passes the persona; /workstreams unpacks the widened row. - Shared resolve_persona_for_kind is the single eligibility rule for the HTTP handler, CLI, and spawn precheck; spawn_batch memoizes the persona lookup; ToolSearchManager.is_expanded gives the visibility tail an O(1) probe. |
||
|
|
cc0fa53077 |
feat(coordinator): port Regenerate/Edit title to coordinators
Coordinators carry LLM/auto titles like interactive workstreams but had no way to regenerate or rename them. Port the interactive "Refresh title" (LLM regenerate) + "Edit title" (manual alias) dropdown actions by lifting the two handlers — the last shared verbs that weren't yet lifted — and opting coordinators in. - session_routes.py: add make_refresh_title_handler / make_set_title_handler factories (cfg pattern, mirroring make_close_handler). set_title resolves the workstream BEFORE the alias write and 404s when the kind has no tenant_check storage gate and the in-memory manager doesn't own it: set_workstream_alias is a global, kind-unscoped UPDATE, so this prevents an operator renaming a workstream the coord manager doesn't own (e.g. an interactive ws via the coord route) and the silent-200 on a bogus id. - server.py: re-point the interactive bundle to the lifted handlers; drop the standalone refresh_workstream_title / set_workstream_title. - console/server.py: wire refresh_title / set_title into the coord bundle (gated by the existing admin.coordinator operator check). - shell.js: enable titleVerbs on the coordinator pane's tab menu; the base-aware lane posts to the console-origin coord routes. Tests: coord refresh/set-title (regenerate, operator-gate, 404 unknown, alias store + broadcast, empty, conflict, cross-kind reject); interactive title tests re-pointed to the lifted handlers for lift-parity; shell.js coord-menu assertion. |
||
|
|
531913ec03 |
refactor(attachments): address branch self-review
- DRY the launcher create body: the multipart (meta + file parts) vs JSON framing was duplicated in _createCoordinator and _createInteractive — extract _createWorkstreamFetchOpts so the create wire shape lives in one place. - Correct the proxy comment: the forwarded owner uid comes from the authenticated ws_body (as on the JSON path), not the caller's meta; the proxy token source is console-proxy, not console. |
||
|
|
9c15bb035c |
fix(attachments): forward create-time attachments for console interactive sessions
The console creates interactive sessions by proxying to the owning node via /v1/api/cluster/workstreams/new, which only forwarded JSON — so a file staged in the launcher was blocked with "Attachments aren't supported for interactive sessions yet". The node create endpoint already accepts multipart (meta JSON + file parts) on interactive_endpoint_config; only the proxy lacked it. Teach create_workstream to accept multipart: parse meta + files (same caps as the node), pick the node exactly as before (auto / pool / pinned), and forward the files instead of re-serialising JSON. _createInteractive sends multipart when files are staged (mirroring _createCoordinator) and the launcher gate is removed. The files-need-a-task guard already ensures an initial turn to dispatch them on. |
||
|
|
f7500261e2 |
fix(attachments): base-prefix interactive pane attachment requests
A console interactive pane is node-proxied — every request rides the pane's
transport base ("/node/{id}"). The attachment controller hardcoded bare
/v1/api/workstreams/... paths, so upload / list / delete / preview landed on
the console's OWN coord route, which resolves ws_id via coord_mgr.get() and
404s as "coordinator not found". The standalone server (base="") was
unaffected, which masked the bug.
Thread the pane base through: createAttachmentController and
buildAttachmentPreview take an optional getBase / base, and the interactive
pane wires this._base into both. Coordinator panes and the standalone server
pass "" and stay origin-mounted as before.
|
||
|
|
ce105c4ed1 |
fix(ui): split separator ARIA range reflects the real clamp, not 10–90
_buildHandle hard-coded aria-valuemin/max at 10/90 (inherited from the old ui/static implementation) while the actual drag/keyboard clamp is _ratioBounds — the cell minimums against the split node's OWN px region (a 1200px host really clamps at ~17/83; nested splits sit tighter), so assistive tech was told a wider range than the separator allows. aria-valuenow/min/max are now all written in _applyLayout's handle loop from _ratioBounds(h.node) — one writer, refreshed on every drag, keyboard nudge, and structural change. A bare window resize can stale the advertised range until the next interaction (no resize listener by design — % insets make resizes free), still strictly truer than a constant. The max>=min guard covers a host shrunk below two cell minimums, where the bounds legitimately cross. |
||
|
|
482e6648ca |
fix(ui): drop the pane-hosted coordinator sidebar below the corner chip
The per-pane ✕/− chip floats at the pane's top-right — exactly where the coordinator sidebar's toggle row and Children refresh button sit, so the chip covered them. Pane-hosted coordinators now start the sidebar content 44px down (padding, not margin, so the column's left border still runs the full pane height); the standalone coordinator page has no chip and keeps the 14px default. |
||
|
|
ed08986d93 |
fix(ui): split-view pre-push review round — mode-distinct chip, anchoring, light-theme AA
Dual designer review (one primed on the branch context, one cold), all measured findings applied: - The per-pane chip was a mode-error trap: identical glyph at the identical locus, reversible in split mode (hide cell) but destructive single-pane (close pane). Now − hides, ✕ closes, and the close mode wears a danger hover/focus ring so the irreversible action telegraphs before the click lands. - Single-pane chip anchored to the VIEWPORT: an unpositioned section resolves absolutes to <body>, so the chip only coincidentally landed near the pane corner. .panes > section.pane is now position:relative in both modes (all pane-content absolutes verified to anchor to their own local relative parents). - Light-theme AA (measured): .shown tab underline 55% mix composited to 2.34:1 -> 80% (~3.7:1 light / ~5:1 dark); focused-cell ring 2.60:1 on light -> 75% mix override there (dark keeps 55% at 3.75:1). - Chip: border --hair-2 measured ~1.3:1 (invisible) -> --ink-4; 22px target under WCAG 2.5.8's 24px floor -> 28px; right offset clears the message scrollbar gutter; light resting glyph one ink step up. - Focus bar inset 1px from cell sides (no doubled-accent stripe where it butted a separator at the T-junction); greyscale font smoothing on the tail glyphs (subpixel RGB fringed the box-drawing characters). Rejected with rationale: aria-pressed on the split buttons (they are one-shot verbs — splitting again nests — not mode toggles). |
||
|
|
44c11efb53 |
feat(ui): split-view follow-ups — per-pane ✕, child-opens-beside, close-on-ws_closed
Four refinements from first live use: - Per-pane ✕ chip, top-right of every visible pane. Split mode: hide that cell (closeCell — the tab stays, the sibling absorbs the space). Single-pane: close the pane outright (withheld from the unclosable Dashboard). The click decides at click time; the label tracks the mode. Manager-injected into the pane section — content untouched. - Coordinator child links open BESIDE the coordinator (openPaneBeside: split right of the focused cell, seeded with the child pane) instead of replacing it — the parent stays on screen. Degrades to the plain focused-cell swap when the split is denied (cap / narrow viewport). splitFocused() gained an optional explicit-fill parameter for this. - Tier-1 ws_closed now CLOSES the open interactive pane (tab gone, a split cell collapses) — the coordinator-closes-its-child flow, matching the standalone's pane-auto-close. The dead-banner lane stays for streams that die without a ws_closed (node crash/network), where the session may still be revivable. - Paint bug: the focused-cell ring was an inset box-shadow on the section, which paints in the element's own background layer — UNDER opaque children touching the edges, so the status bar / composer strip occluded it. The ring now rides a click-transparent ::after overlay above pane content; the 2px top bar sits above the ring line. The livepass shell surface's demo panes grew a .ws-status-bar footer so the occlusion bug class stays visible to future passes. |
||
|
|
f8f7152d63 |
feat(ui): split view returns to the L-shell — PaneManager layout tree
Revives the split-pane feature retired with ui/static (step 6), rebuilt on PaneManager: an optional binary layout tree (null = the one-pane-per- tab behaviour, unchanged) renders visible panes as %-inset cells — no reparenting, so live stream DOM, scroll state and media survive layout changes. Tabs stay global: the active tab is the focused cell, a backgrounded tab swaps into it, clicking inside a visible pane focuses its cell, .shown marks visible-unfocused tabs. Separators resize by pointer-capture drag and arrow keys (role=separator + aria-value*); the tree persists in the working-set blob and rehydrate prunes leaves whose pane did not restore. Limits: 6 cells, 200x150 cell minimums, denials toast the manager's reason. Affordance: Split right / Split down / Unsplit buttons in the tab-bar tail replace the redundant [+] (the permanent Dashboard tab is the launcher) — deliberately no contextmenu override this time. The dead TS_APP.focusLauncher seam goes with it. Measured chrome: the focused cell wears a 2px accent top bar (no thin tinted ring clears 3:1 in both themes) plus a 55%-mix inset ring; separators rest at --ink-4 with solid-accent hover/drag/focus; .shown tabs carry an accent underline; the tail cluster is fenced and lifted to --ink-3. scripts/livepass.py grows a third surface: shell/livepass.html boots the real shell.js + pane.js and drives ?split=right|down|three|none (+ &theme=light), stamping SPLIT-READY-<cells> / SPLIT-FAILED-<reason>. |
||
|
|
ef7fdb3a26 |
fix(ui): re-home MCP consent badge on the Manage Connections row (#657)
* fix(ui): re-home MCP consent badge on the Manage Connections row The L-shell renovation retired the standalone settings gear (#settings-btn). The MCP pending-consent badge anchored to that gear via _refreshConsentBadge, which null-guarded silently — so since the renovation pending consent requests had no indicator (the badge was invisible). Re-home the badge on the rail's Manage row where the MCP/connections surface lives in both deployments: - rail.js gains a generic setRowBadge(tabKey, count, label?) hook + a `badge` builder: a small ⚠-glyph + count chip (never colour alone) using the DS warn tokens. mountManage registers row + owning-group-head refs and re-applies live counts across a (re)mount. When the owning group is collapsed, the count also mirrors onto the group head so a hidden row never hides the signal. rail.js stays agnostic — it owns the mechanism, the caller owns the meaning. - shell.js (the ESM bridge) re-exports setRowBadge on window.TS_SHELL so the classic ui/static/app.js subsystem can drive it without importing the module. - The standalone consent subsystem keeps its shell-level ownership: _refresh- ConsentBadge now drives setRowBadge on the Connections tab, fed by both the loadPendingConsents hydrate/poll load and live onConsentDetected notifications. - The shared interactive pane host bridges onConsentDetected to the new window.TS_APP.onConsentDetected seam (undefined on the console, so the console pane stays a no-op there); panes only notify. - The dead colour-only gear badge CSS (.settings-consent-badge, red dot) is removed; the new chip lives in shell.css as token-only .rail-badge so it flips themes by construction. Console MCP tab (Extensions > mcp) and standalone Connections tab (Extensions > connections) both badge correctly. Pins extended in test_shell_js.py + test_app_js.py. * fix(ui): drop the unused head ref from the rail badge row map Review feedback: _rowEls stored each row's group-head element but every head consumer resolves it through _groupEls; keeping the duplicate DOM ref made the remount state shape harder to reason about. |
||
|
|
b1c526a170 | style: ruff-format the appended drawer test guard | ||
|
|
539ed3ccbf |
test(ui): pin the collapse/drawer/popup-menu behavior + breakpoint pair
Review follow-ups: the new rail collapse and mobile drawer had no committed guards (the repo pattern is per-step string assertions in test_shell_js.py) and openPopupMenu — now load-bearing for both the tab dropdown and the footer user menu — was unpinned. - test_rail_collapse_glyph_strip: persistence key, toggle + aria-controls, class-flip seam, cpill-label/manage-glyph companions, 52px desktop-scoped CSS. - test_mobile_drawer_off_canvas: burger, scrim, rail-open flip, pane-activation auto-close, off-canvas translateX + visibility:hidden. - test_popup_menu_shared_helper: the export + both consumers (the user menu's prefer-up path included). - shell.css: the 769/768 media blocks are a matched pair CSS cannot express as a shared token — both now carry a cross-referencing change-both comment. |
||
|
|
2320c6d13c |
refactor(ui): migrate the shared_static substrate to ES modules
utils/toast/kb/cards/auth/renderer/composer/composer_attachments/ composer_queue/status_bar convert from classic scripts (implicit globals, IIFE wrappers) to ES modules with explicit exports. Parse-time cross-dependencies become real imports (auth/kb/cards -> utils, auth/cards -> toast, cards -> auth, renderer -> utils), which deletes the implicit script-order contract those files relied on. utils stays import-free (bottom of the graph); its two upward calls (setMarkdown -> renderer, export -> toast/auth) late-bind through window at call time to avoid import cycles. Each module installs a transitional window bridge for the still-classic bundles (console app/admin/governance, ui app, inline onclick=), which only touch the globals at boot/event time — verified by a column-0 / IIFE-body audit of all four consumers, and including the audit-missed initLogin() that both app.js boot paths call. theme.js stays classic: deferring it would flash the wrong theme before first paint. Vendored katex/hljs/mermaid stay classic and lazily typeof-guarded. interactive.js and shell.js drop their bare-global reads for real imports (authFetch, showToast, Composer, StatusBar, queue/attachment controllers, streaming renderer, setMarkdown). The three HTML entries load the substrate as module tags (same positions, same version_html stamping); classic admin/governance/app still parse first, modules evaluate before shell.js calls TS_APP.boot(). Tests: auth/kb/utils move from test_app_js's classic node-check sweep to test_shell_js's module-semantics sweep, which now covers all 15 shared modules (sink scan excludes renderer.js, the sanctioned HTML producer; the no-var ratchet covers the var-free subset). The const-reassign guard re-includes the converted files plus the shell modules. |
||
|
|
3fc65577f5 |
fix(ui): tab-menu verbs follow the live node when the controller is dead
A dead interactive controller's base goes stale once its node loses or re-homes the ws, but menuBase() returned it first — so the close/delete 404-as-success lanes could silently drop a tab whose session is alive on the node it re-homed to. Mirror the revive path: when isDead(), lead with the live Tier-1 node and fall back to the stale base only when the ws is gone cluster-wide (its 404 then correctly reads as "already closed"). |
||
|
|
4b1536be2c |
fix(ui): ws lifecycle round 2 — dead-session revive + proxied tab-menu verbs
Two reported console bugs, one shared root: a pane can outlive its session, and nothing brought the two back together. Reconnect: an interactive pane whose stream died (ws closed/evicted elsewhere, node restart, re-home) could never reconnect while its tab existed — openPane() on an existing pane was focus-only, the controller's connect() is one-shot, and its 5s recovery loop re-dialed the SAME node forever (infinite 404 polling through the console proxy). The only workaround was closing the tab before resuming. - createInteractivePane now tracks terminal failure: 3 consecutive CLOSED recovery beats -> give up (stream closed, timers + any pending history load invalidated, status bar "Disconnected", opts.onDead fired once). host.onStreamOpen (new hook) resets the counter; isDead()/markDead()/base join the controller surface; onLogin ignores a dead controller — revive owns recovery, so a deliberately closed session is never resurrected by a timer. - PaneManager.openPane fires pane.onReopen(extra) when it targets an ALREADY-OPEN pane — the explicit-intent signal (saved-list resume, rail row, child link) that activate() can't carry (hooks no-op on the active pane, and onActivate also fires on plain tab switches). getPane() added for cross-cutting lifecycle signals. - The shell paints a click-to-reconnect banner on give-up — and immediately on Tier-1 ws_closed via the new TS_SHELL.notifySessionClosed seam (the console keeps the tab, unlike the standalone's auto-close, so the conversation stays readable). Reopen/banner-click revives: tear down the dead controller, re-resolve through the origin-first POST /open lane, rebuild. The forced resolve skips BOTH beginConnect fast paths (a stale Tier-1 row must not bypass /open) while a live node leads the hint chain (an origin-first /open then reuses a genuinely-live session instead of loading a duplicate on the old meta node). The standalone lane POSTs its local /open on revive too — /events 404s on an unloaded ws. - Coordinator parity: the factory exposes reconnect() (acts only on a missing/CLOSED stream; OPEN is healthy, CONNECTING is already being worked) and the pane's onReopen drives it — the saved-list resume POSTs /open before openPane, so a fresh stream is all it needs. Tab menu: a node-proxied interactive pane's dropdown gated every verb on classic globals that only exist in ui/static/app.js, so the console got a nearly-empty menu whose one surviving verb (Export) hit the console origin and 404'd. convTabMenu gains a base-aware fallback lane: verbs POST against the pane's OWN transport base (controller's exact base -> persisted node hint -> live Tier-1 node; a verb is omitted while no base is resolvable — never aimed at the wrong origin). Close/Delete confirm first (window.confirm, the coordinator precedent) and treat 404 as intent-satisfied (nothing left to stop/delete -> drop the tab). exportWorkstreamDownload takes the base. The standalone keeps its globals lane (incl. Fork) byte-identical, and an empty verb section no longer renders a leading separator. Verified: 189 JS-pin tests; two headless-Chrome live-DOM harnesses driving the real modules — console 16/16 (connect -> ws_closed -> banner -> reopen revives on a new node with the fresh hint -> give-up stops retrying -> live-node-led resolve), standalone 10/10 (globals menu intact, revive POSTs /open exactly once, no cluster resolve). |
||
|
|
c4dec213de |
fix(ui): address /review of the workstream-lifecycle change
Multi-stage review (find → verify → sanity) of
|
||
|
|
71d3ed6abe |
fix(ui): repair interactive/proxied workstream lifecycle (reload, create, launcher)
Workstream-lifecycle bugfixes on the L-shell: - Node-proxied interactive panes now SURVIVE a browser reload. On first activate a pane resolves its owning node and (re)opens the session there before streaming — the node /events stream 404s on a ws not loaded on its node, so a rehydrated pane could not just connect blind. Resolution is origin-first via the new TS_APP.resolveInteractiveNode seam (POST /open with a rendezvous /route fallback). PaneManager now persists a pane's resolved nodeId as opaque meta and hands it back on rehydrate, so a reload restores the pane onto the SAME node even before the Tier-1 snapshot has populated — the exact timing that used to strand it on base="" (the console, not a node). - Both launcher personas open the new session as a PANE, not a full-page nav (coordinator -> coordinator pane; interactive -> node-proxied pane); the full-page nav stays only as the shell-absent fallback. Every interactive entry point (create, active row, rail, saved row, child link, reload) now funnels through one resolve-open-connect path, folding away the bespoke restoreInteractiveSession helper. - The interactive launcher gains a node-selection strategy (Least loaded | Specific node, with a live node picker fed from the cluster snapshot) and a persona-aware task hint — the shared composer no longer shows "...coordinator orchestrate?" when the interactive persona is selected. Guards updated to pin the new wiring; the stale console landing test (asserting the renovation-retired bottom-bar node picker) is corrected to the rail. |
||
|
|
b14cc93d37 |
chore(ui): apply /review findings — guard menu listener, fold tab repaint, drop dead status-bar code
From the multi-stage review of this session's changes (all minor): - bug-1: the footer user-menu's deferred document-listener attach now bails if the menu was already closed (closeUserMenu nulls the cleanup ref), closing a latent listener-leak window. - perf-1: fold paintConvTabGlyphs + paintConvTabTitles into one paintConvTabs — a single findWs per stateful tab per Tier-1 render instead of two scans. - q-2: remove the dead StatusBar.paint modelEl branch + its modelInfo arg (both callers dropped it when the model moved to the composer chip) and the orphaned .ws-sb-model CSS rules. (q-1, the parallel-head string-helper extraction, follows separately.) |
||
|
|
0f137f570f |
feat(ui): L-shell step 7 — auth-gate openPane (coordinator scope)
openPane now auth-gates pane CREATION via an optional per-type canOpen predicate (deny -> no pane; focusing an already-open pane is never re-gated). PaneManager stays generic — it holds a _gates map and consults canOpen/onDeny; the shell supplies the gate. The coordinator type gates on the admin.coordinator scope — the SAME sessionStorage-backed _hasCoordPermission helper the launcher + saved-list use. Because every coordinator open path (rail click, child-link, rehydrate, [+] launcher) routes through openPane, this gates them all at once — closing the gap where a rail click opened a coordinator pane a user lacked scope for (it then 404'd server-side). Perms live in sessionStorage so they survive a refresh → rehydrate gates correctly (an operator's persisted coord pane restores, a non-operator's is skipped). The backend enforces the scope too; this just avoids opening a doomed pane. Verified: 31/31 mechanism harness (gate allow/deny/onDeny) + real-stack console wiring (authorized operator opens the coord pane; a no-permission stub denies a new coord pane, gateDenied:true, errs:[]) + a shell JS guard + CSS audit baseline. |
||
|
|
9f10e8e81d |
feat(ui): L-shell step 7 — [+] new-session button in the tab-bar tail
The right-floated tabbar tail (empty since the scaffold) gets a [+] button that
focuses the persona launcher — the Dashboard pane hosts the unified
coordinator/interactive launcher, and a new session needs a task prompt, so "new
session" composes there. Cross-deployment via window.showHome (both the console
and standalone expose it) with a pm.openPane("dashboard") fallback; reuses the
scaffold's .tab-add styling. Auth stays the launcher's concern (it gates each
persona option), so focusing it is always safe.
Verified: renders in the real shell (standalone harness DOM) + a shell JS guard.
|
||
|
|
e60f5a3108 |
feat(ui): L-shell step 7 — live tab state-glyphs (Tier-1, shape+colour)
Conversational tabs now show a live shape+colour state glyph (● ◐ ⚠ ✗ ○) instead of the static ◆/○ placeholders the header removal (5e.2e) left behind — driven by the SAME Tier-1 source + builder the rail uses, so tab and rail always agree. - rail.js: export the glyph() builder (one source of truth for the mapping). - pane.js: ShellPane.stateful + PaneManager.setTabGlyph/statefulTabs — generic (PaneManager owns no glyph vocabulary; the shell passes the built element). A stateful pane builds no static glyph; the shell paints a live .ui-glyph. - shell.js: stateForWs() reads the Tier-1 snapshot; paintConvTabGlyphs() repaints every stateful tab on each Tier-1 render (subscribed to TS_APP.onRender) + per pane on activate. Coordinator + interactive panes are now stateful. - shell.css: .tab .tab-glyph spacing (static + live); live glyphs keep their own .ui-glyph-* state colour (no .tab .glyph override). SINGLE WRITER: the tab glyph is written only by the Tier-1 path (the pane's Tier-2 stream drives its body, not the tab) — no two-tier race, no stale open-time placeholder on reconnect (BRIEFING L144-147). A coordinator-telemetry-parity gap (open Q#2) would stale tab + rail equally, consistently. Verified: 27/27 mechanism harness (8 new glyph asserts) + real-stack wiring harnesses (console coord ui-glyph-running / int ui-glyph-idle; standalone int ui-glyph-running — matching the stubbed Tier-1 state, errs:[]) + 26 shell JS guards + CSS audit at baseline. |
||
|
|
7448792251 |
feat(ui): L-shell step 7 — tab-action dropdown (three-verb close + per-persona verbs)
PaneManager tabs gain a caret opening a generic, keyboard-navigable action
dropdown — recovering the affordances the pane-header removal (5e.2e) dropped.
The mechanism is generic; the item set is pane-type AND deployment derived.
- pane.js: the caret (a <span>, not a nested <button>) + _openTabMenu/_closeTabMenu
— singleton, right-anchored under the caret with overflow flip + viewport clamp,
Arrow/Home/End/Esc/Tab nav, ContextMenu/Shift+F10 + right-click open.
- shell.css: the .tab-menu chrome promoted to the SHARED sheet (both deployments),
recovered from the retired .ws-tab-dropdown design but translated onto the DS
token vocabulary (--panel-2/--hair-2/--ink-*/--err).
- shell.js: convTabMenu wires each type by capability/feature-detection —
coordinator: Export · Close pane · Close workstream (its controller's
closeSession — the Export + end removed from its header land here)
standalone interactive: Refresh/Edit/Fork · Export · Close pane ·
Close workstream · Delete (classic ui/static globals)
console interactive: Export · Close pane (those globals are standalone-only)
admin: Close pane
Three-verb close is load-bearing: Close pane (drop tab) != Close workstream
(stop session) != Delete (destroy + unsave).
Designer-reviewed both personas, dark+light: resting danger cue on Delete (never
colour-alone), elevated --panel-2 surface, accent-wash hover, light key-hint AA,
viewport y-clamp + max-height.
Verified: 19/19 mechanism harness + real-stack wiring harnesses (all three menus,
errs:[]) + 25 shell JS guards + CSS audit at baseline (zero new flips).
|
||
|
|
f3a0954b76 |
refactor(ui): L-shell step 6.0 — make shell.js deployment-agnostic
Two changes so the SAME shell mounts on a standalone turnstone-server (step 6's META-GOAL: collapse the console/static vs ui/static fork): - The coordinator pane import is LAZY + gated on caps.orchestration. A static `import ... from "/static/coordinator/coordinator.js"` 404s on a standalone server (whose /static is ui/static, no coordinator file) and aborts the whole shell module. It's now `await import()` inside mountShell, before rehydrate, registered only when the deployment has orchestration (the console); a persisted coordinator pane then degrades to a rehydrate skip. mountShell is now async. - The interactive pane's nodeId is gated on caps.cluster. Node-proxy transport only exists in a cluster deployment; on a single-node standalone every session is LOCAL, so nodeId stays null -> the pane uses base="" (no /node/<id> hop), even though the synthesized one-node clusterState names a node. Console behaviour is identical (orchestration:true -> the import runs + the coordinator registers; cluster:true -> the nodeId ternary's true branch is the original expression). Verified both personas build clean in a headless harness: console = [Cluster, Workspaces, Manage] + coordinator registered, no errors; standalone = caps off, no Cluster, no coordinator, no errors. |
||
|
|
6614b4a549 |
refactor(ui): L-shell step 5e.0 — migrate coordinator.js to an ES module
Lift coordinator.js off the window.createCoordinatorPane bridge onto a real ESM export, so the upcoming shared conversational module (5e) is import-consumed on both sides rather than through a classic window global. The console shell and the standalone page's bootstrap both import the factory now; zero behaviour change. - coordinator.js: export the factory, drop the window bridge — no classic consumer remains (unlike interactive.js, whose ui/static app.js still uses its global). - shell.js: import the coordinator factory by URL (mirrors the interactive import) and call it directly. - console index.html: stop script-tagging coordinator.js; shell.js's import loads it (a classic tag chokes on the top-level export). - coordinator/index.html: the standalone bootstrap becomes a module that imports the factory — a classic eager IIFE ran before the deferred module loaded it. - tests: pin the new ESM seam (export, shell import, module bootstrap). |
||
|
|
1d399703b1 |
fix(ui): L-shell step 5d — designer-review fixes (rail active-marker, tab glyph)
Designer pass on the new console interactive pane. Two clean fixes; the rest of the findings are scoped to their planned steps (see below). - Rail Workspaces `.open` marker now tracks the ACTIVE pane instead of being hardcoded to Dashboard — the rail map and the tab bar were disagreeing about what's focused (opening a session never moved the rail highlight). PaneManager gains getActive() + onActiveChange() and fans out on activate/close; the rail keys `.open` off the active pane's rawId and re-renders on activation (not just on the next Tier-1 snapshot). - The active tab's glyph brightens (--ink-4 -> --ink-2) so an open session's `○` placeholder doesn't read permanently "idle" beside its live (running ●) rail row. (Tab glyphs go fully live in step 7.) Deferred (planned elsewhere, not regressions): the tab CLOSE affordance is step 7 (the brief's three-verb `.ws-tab-dropdown`); the interactive/coordinator HEADER consistency is what the step-5e base lift unifies (a shared header parameterized by affordances), so partial coordinator-header surgery now would be a half-measure. Verified: a headless screenshot (rail `.open` now on the active session, slim header + persona tag render clean) + 107 JS-guard tests (test_shell_js 5d guard), node --check, prettier, ruff. |
||
|
|
b9ba542ce6 |
feat(ui): L-shell step 5b — register the interactive pane in the console shell
Wire the shared interactive Pane (5a) into the console L-shell as a ws_id-keyed,
node-proxied conversational pane.
- shell.js IMPORTS createInteractivePane (interactive is a real ES module, so
the shell consumes it the modern way; the legacy coordinator pane stays on the
window.* seam — the incremental "pulled by the adopting pane" modernization).
registerType('interactive') mirrors the coordinator: build on mount, connect
on activate (idempotent) + login re-arm, deactivate on tab-away (stops
focus-stealing while the stream stays live), destroy on close.
- The node-proxy target is DERIVED from the Tier-1 snapshot (nodeForWs), so a
rehydrated pane needs no persisted node_id; a rail click / child link can pass
{nodeId} as an open-time hint. openPane(type, id, extra) threads that hint to
the factory (not persisted).
- rail.js: interactive session clicks now openPane('interactive', ws.id,
{nodeId: ws.node}) instead of full-page nav to /node/{id}/.
- interactive.css (new, shared): the embedded slim-header layout (scoped to
.pane--embedded so it never collides with the ShellPane's own .pane section —
the brief's namespace watch-out) + the conversational rendering (tool output /
media / MCP-error / verdict / output-guard cards) COPIED from ui/static. The
shared chat.css .msg/.ts-approval base is left untouched, so the coordinator
pane is unaffected; step 5e unifies the vocabularies, and ui/static keeps its
copy for the standalone until step 6.
Verified: an integration harness running the REAL shell.js + rail.js +
interactive.js (register -> rail-open -> embedded chrome -> node-proxy SSE
/node/{id}/.../events -> /history replay into real .msg turns -> destroy, zero
errors) + a screenshot; 105 JS-guard tests (test_shell_js step-5 guard),
node --check, prettier, ruff.
|
||
|
|
6e0901ee1b |
fix(ui): L-shell step 4 — designer-review fixes (pane-aware close, overflow, glyph, a11y)
Four findings from the step-4 designer pass on the coordinator pane.
- P1 (bug): the `end` button ran `window.location.href = "/"`, which inside the
L-shell reloaded the WHOLE console — every other pane destroyed, all their
Tier-2 streams dropped. Thread an `onClose` through the factory; the console
pane passes `() => pm.close(pane.id)` so `end` closes that tab (and runs the
controller teardown via onClose→destroy); the standalone page passes none and
keeps the console redirect.
- P2: the pane root carries both `.pane-body` (overflow:auto) and
`.coord-chrome-root` (flex column), so the generic pane scroller redundantly
wrapped the sticky appbar. `.pane-body.coord-chrome-root { overflow: hidden }`
(scoped to this pane type) — the coord chrome owns its own scroll regions.
- P3: the coordinator tab glyph `●` collided with the rail's running state-dot
vocabulary (a static dot reading as "live"); swap to `◆` (a shape marker that
pairs with dashboard's `◇`), pending the real state-glyph in step 7.
- P3: the destructive `end` button had only a title; add aria-label
"End coordinator session".
Verified via the harness (clicking `end` closes the pane without reloading;
glyph `◆`; aria-label present; zero errors); guards pin the pane-aware close.
test_shell_js + test_coordinator_page (97 green), ruff.
|
||
|
|
05dbcf3818 |
feat(ui): L-shell step 4b — coordinator sessions as console panes
The console can now host coordinator sessions as ws_id-keyed panes alongside
dashboard/admin — step 4 complete (the de-globalization landed in 4a).
- coordinator.js: `buildCoordChrome(root, opts)` builds the coordinator chrome
programmatically (createElement, no innerHTML); the factory builds it on
instantiate, so the SAME factory serves the standalone page and a console pane.
`opts.standalone` adds the page-level bits a pane doesn't want (the Console
back-link, the theme toggle, the shared #toast).
- index.html (standalone): goes thin — a bootstrap calling
createCoordinatorPane(document.body, ws_id, {standalone:true}); the ~500-line
inline <style> is migrated to coord-chrome.css (its lone page-level body rule
scoped to .coord-chrome-root) so the console can load the same chrome CSS.
- shell.js: registerType('coordinator') keyed by ws_id — onMount builds the
controller into the pane body, onActivate opens its Tier-2 SSE once, onClose
destroys it. Plus a window.TS_LOGIN fan-out registry so every pane re-arms its
own stream on re-auth (app.js's single onLoginSuccess becomes one subscriber).
- rail.js: coordinator clicks → openPane('coordinator', ws_id) instead of
full-page nav (interactive sessions stay interim full-page until step 5).
- console/index.html: loads the coordinator controller + chrome CSS + the shared
composer/renderer deps it needs.
Child links → openPane('interactive', ws_id) are deferred to step 5 (the
interactive pane doesn't exist yet); coordinator transport stays console-local
inline (parameterized only when the shared ConversationalPane base is lifted).
Verified end-to-end with a headless harness running the real shell.js + rail.js +
coordinator.js: opening a coordinator pane registers the type, the rail row opens
it, buildCoordChrome populates the pane, the Tier-2 SSE connects, destroy() tears
down — zero uncaught errors; renders cleanly (appbar + chat + children/tasks
sidebar + status bar). test_shell_js + test_coordinator_page (97 green), ruff.
|
||
|
|
1f254d764a |
fix(ui): L-shell step 3 — designer-review fixes (admin-pane inset, rail seed, a11y)
Five findings from the step-3 designer pass; the P3 chevron-rotation (taste) was skipped — the text-swap is already motion-safe. - P1: the adopted #view-admin had no inset, so the first admin section-header butted the tab-bar hairline + rail edge. Add `padding:16px 0 0 16px` on `.pane-body > #view-admin` (.admin-content keeps its right pad). - P2: the rail Manage active-marker never seeded from getActiveTab(), so a PaneManager.rehydrate-restored Admin pane showed no active group/row until a re-click. mountManage now takes the PaneManager, seeds the marker + expands the owning group when the Admin pane is already open (new PaneManager.hasPane()). - P2: the active-row band was byte-identical to the amber `.row.open` of live sessions (distinct only by a 2px stripe). Give it its own neutral idiom — `--panel-2` fill + a hairline `inset 2px` marker — so "which admin tab" reads as different in kind from "which session is live". - P3: `.gcount` pinned right with `margin-left:auto` (was incidental via flex). - P3: strip the dangling `role="tabpanel"`/`aria-labelledby="tab-*"` from the 18 adopted admin panels (their sidebar buttons were deleted in 3b); the 9 legit tabpanels elsewhere are untouched. Verified via the headless harness (rail / admin-open / rehydrate states) + test_shell_js.py guards (the aria strip is now pinned). |
||
|
|
1001293217 |
refactor(ui): L-shell step 3b — delete the retired admin sidebar + mobile drawer
The rail's Manage groups replaced the in-pane admin sidebar in 3a; this removes the now-dead markup, JS, and CSS that it leaves behind. - index.html: drop the #admin-sidebar nav (6 groups / 18 buttons) + the mobile #admin-sidebar-backdrop; #admin-layout now wraps #admin-content alone. - admin.js: delete the mobile off-canvas drawer (_mobileSidebarOpen, _injectMobileToggle, _toggleMobileSidebar, the Escape-to-close + arrow-nav + resize-sync handlers) and switchAdminTab's now-dead .admin-nav active loop + breadcrumb write. - style.css: remove the .admin-sidebar* / .admin-nav* / .admin-mobile-toggle* rules, the mobile off-canvas @media block, and the dead reduced-motion entries. - shell.css: drop the .pane-body .admin-sidebar hide rule (nothing to hide now). .admin-layout / .admin-content / #view-admin stay (the Admin pane adopts them). Verified: no residual sidebar/mobile refs, CSS braces balanced, admin.js parses, headless render unchanged, and test_shell_js.py pins the removal. |
||
|
|
9c4ec4855d |
feat(ui): L-shell step 3a — Admin pane + rail Manage groups
Admin becomes a singleton pane and the rail's Manage section becomes its
navigation; the in-pane sidebar is retired.
- shell.js registers an `admin` pane type that adopts #view-admin (the 18
tabpanels) on first open; the dashboard pane keeps #main.
- admin.js: new ADMIN_IA seam (window.TS_ADMIN) — the group→tab map, a shared
adminTabAllowed() gate (mirrors the legacy showAdmin permission gate, incl.
the ungated node list), an active-tab subscription, and openTab. showAdmin is
now a thin delegator (openPane('admin') + switchAdminTab); the in-#main view
toggle, breadcrumb write, history push, and mobile-hamburger injection go.
- rail.js: mountManage() builds the six collapsible .grp groups from the seam,
permission-filtered, routing a row click through openTab — never touching
admin DOM.
- app.js: home/drill re-focus the Dashboard pane instead of blanking the moved
#view-admin.
- shell.css: the .grp vocabulary + admin-pane layout (in-pane sidebar hidden,
#view-admin fills the pane).
The legacy #admin-sidebar is hidden via CSS pending its deletion in 3b; this is
the additive, independently-runnable half. Verified with a headless-Chrome
harness driving the real shell.js + rail.js over a stubbed seam, plus the
test_shell_js.py guards (19 passing).
|
||
|
|
b453ac5bb3 |
feat(ui): L-shell step 2b — persona-unified dashboard launcher + both-kinds saved list
The dashboard body becomes a persona-unified launcher (start a coordinator OR an interactive session from one composer) and the saved list spans both kinds; the redundant active-coordinators table is dropped (the rail covers it now).
Backend — the console /v1/api/workstreams/saved now returns both kinds: session_routes.py extracts _collect_saved_rows (shared by the refactored, behaviour-preserving make_saved_handler) + adds make_unified_saved_handler (merges per-kind queries — run concurrently via asyncio.gather — sorted by updated desc). The operator gate (admin.coordinator) is applied once; no new exposure (operators already see every session). console/server.py mounts it with [coordinator, interactive] cfgs.
Frontend — a persona toggle routes submit by kind: coordinator -> console-local POST /v1/api/workstreams/new; interactive -> node-proxy POST /v1/api/cluster/workstreams/new (auto placement). Each option is scope-gated (admin.coordinator / workstreams.create); attachments stay coordinator-only. The saved list gains a KIND tag column + kind-routed activation (coordinator -> /open + /coordinator; interactive -> /node/{id}/?ws_id=) and stays operator-gated. The active-coordinators table + _renderHomeView/_activeCoordsFromClusterState are removed.
Tests: make_unified_saved_handler coverage (tests/test_saved_handler_unified.py, synthetic fixtures, no DB) + a console-launcher static guard (tests/test_shell_js.py). Reviewed via the multi-stage pipeline; findings applied (client/server gate match, concurrent queries, chip-CSS dedup, static guards, stale-comment cleanup).
|
||
|
|
6a2f83f829 |
feat(ui): L-shell step 2a — rail Cluster + Workspaces go live, retire bottom bar
The rail's Cluster + Workspaces sections (step-1 stub labels) now render live from the Tier-1 clusterState, and the legacy bottom #cluster-status-bar is retired — the rail replaces it (the L-shell has no bottom bar). New shared_static/rail.js (ESM): renders Cluster (health pills wired to drillDownByState + a node list with version/drift) and Workspaces (the session tree — coordinators with children nested via the shared _bucketByParent, COORD/INT persona tags, state = shape+colour via ui-base .ui-glyph-*). app.js exposes a minimal Tier-1 seam on window.TS_APP (getClusterState + onRender + the rail's nav actions); renderFromState fires subscribers. No physical clusterState extraction — the seam closures see the live binding. shell.js builds the Cluster/Workspaces render targets and mounts rail.js before boot so it catches the first snapshot. Retire the bottom bar: delete the #cluster-status-bar markup + renderStatusBar / renderNodePicker / the node-picker helpers + STATE_ORDER (~310 lines), and the .stale toggles in connectSSE (the rail-conn #status-bar carries connection state now). buildNodeInfoFromSnapshot / recomputeOverview / _bucketByParent stay — the rail reuses them. The dashboard body still carries its active-coordinator table transiently; 2b reshapes it into the persona launcher + unified saved list. |
||
|
|
96762c2874 |
feat(ui): L-shell scaffold — rail + tab bar + PaneManager pane host
Step 1 of the console renovation: a full-height left rail, a top tab bar, and a generic pane host that shows one pane per tab. Existing console content is hosted unchanged inside it as the default Dashboard pane. New shared_static ES modules (the first ESM citizens; classic scripts keep loading alongside them): pane.js (PaneManager + ShellPane — typed-window host with openPane/activate/close, sessionStorage rehydrate, a WAI-ARIA tablist with roving tabindex + arrow-key nav, reconcile-in-place tabs); shell.js (builds the rail/tab-bar/pane-host, reparents #main + #status-bar with ids preserved so connectSSE needs no rewire, relocates the header controls into the rail footer, drives the app boot); shell.css (chrome ported from the layout mock to base.css tokens). console index.html loads the shell module + capability flags + stylesheet; app.js's bottom init is wrapped into window.TS_APP.boot, which the deferred shell module drives (it runs after the classic scripts). Cluster health, the Workspaces tree, admin, and conversational pane types arrive in later steps; the rail sections are labelled stubs. |