Web terminal redesign: icon rail, dockable workspace, simple-mode chat#442
Draft
thellert wants to merge 42 commits into
Draft
Web terminal redesign: icon rail, dockable workspace, simple-mode chat#442thellert wants to merge 42 commits into
thellert wants to merge 42 commits into
Conversation
Restyle the web terminal and its embedded interface panels to a flat look with a left icon rail and simple/expert UI modes, on a neutral-gray canvas with an azure accent driven by design-system tokens and a pre-paint UI-mode boot. Extends the flat restyle and simple-mode variants across the ARIEL, Workspace, Channel Finder, Lattice, Knowledge, scan, and Events panels. Adds theme-by-mode visual baselines and a contact-sheet renderer for reviewing every theme/mode variant, plus supporting changelog and lint updates.
Replace the fixed terminal/panel split with a dockview grid: service panels dock as tabs through an overlay iframe adapter (iframes are never reparented, so panel state survives layout changes), human dock gestures sync back to server-owned panel state with an echo guard, and the arrangement persists per project. A pure reconcile module repairs stale stored layouts against the currently-registered panels and is unit tested standalone. Expert and simple UI modes flip between the free grid and a locked layout; visual baselines wait for the dock grid and overlay to settle.
Add a REST chat API (SSE and buffered) backed by pooled SDK sessions. OperatorSession.run_turn owns the per-turn event machine — SDK-silence deadline, heartbeat markers, terminal detection, and the quiesce/release guard discipline — so both transports are thin adapters over one loop. A ChatSessionPool manages chat-keyed sessions with shared-creation dedupe, LRU capacity eviction, and idle reaping. The chat UI splits controller/transport/renderer, renders sanitized markdown only, and gets interrupt and delete endpoints for the turn lifecycle.
Update the web-terminal operate guide for the docked panel workspace, teach the contact-sheet harness to capture the docked shell across the theme and mode matrix, and note the changes in the changelog.
Add an apiRequest helper to api.js so every mutating fetch flows through the withPrefix multi-user chokepoint, replacing 13 copies of the error-extraction idiom and 4 hand-rolled prefix reads. Route the scaffold edit flows through the gallery's one reload pipeline, fixing stale untracked-file and summary state after save/claim. Move the hooks-tree renderer into config-render-helpers.js (one textContent-only implementation, removing an unescaped innerHTML sink) and pin it with unit tests. Give the session-status DOM single writers in terminal.js so sessions.js no longer touches terminal-owned elements. Drop dead re-exports and a duplicated byte formatter.
Extract naming.py as the one authority for dot-path to CSS custom property names: the validator no longer imports the emitter's private promoted-groups tuple or re-derives promoted names inline, so validator and emitter cannot disagree about an emitted name. Resolve the $extensions.default flag through a shared model.default_flagged_stem consumed by both the CSS and JS emitters, and validate the previously unchecked flag fail-closed (boolean, at most one theme, dark only). Replace the fail-closed error idiom hand-copied across the token, manifest, and panel validators with shared bases in errors.py; concrete class names and rendering are unchanged. Add onModeChange() to frame-params.js — one owned, tested receive side for the hub's osprey-mode-change broadcast.
…urface Extract md-render.js as the single marked + hljs + KaTeX pipeline: the standalone rendered-markdown page now imports it as a module instead of carrying an inline ES5 copy that had already drifted (error class names, weaker escaping) and was invisible to the JS toolchain — the unit suite's pipeline coverage now applies to both surfaces. Move the theme-to-Plotly restyle into timeseries.js next to the initial-render mapping, ending gallery.js's reach through the module's private theme seam. Delete the consumer-less interactive-notebook route (hardcoded port) and the unwired sendToTerminal sender. Adopt the design system's onModeChange helper in the gallery and the three bluesky panels, each keeping only its page-specific hook.
…refix The simple-mode chat client fetched /api/chat bare, and six head/body asset URLs (mode-boot, DOMPurify, dockview css/bridge/script, safety link) bypassed the template's prefixed() helper — all dead under a multi-user /u/<user>/ mount. Route every one through the existing prefix chokepoints; single-user output is byte-identical.
…e comments The dockview workspace replaced the fixed resize-handle split, leaving the .resize-handle/.body.resizing rule cluster with no remaining markup or JS writers. Also align comment/docs wording with the shipped design (icon rail, dockable tiles) in the dock modules, the spike driver, and the operate how-to.
for more information, see https://pre-commit.ci
| preview.locator(".plotly-graph-div").first.wait_for( | ||
| state="attached", timeout=_PLOTLY_MS | ||
| ) | ||
| except Exception: |
| await self._response_task | ||
| except asyncio.CancelledError: | ||
| await self._client.interrupt() | ||
| except Exception: |
| # hard-cancel below owns that. | ||
| try: | ||
| await asyncio.wait_for(asyncio.shield(task), timeout=_QUIESCE_TIMEOUT_S) | ||
| except TimeoutError: |
| pass | ||
| except asyncio.CancelledError: | ||
| raise | ||
| except Exception: |
| task.cancel() | ||
| try: | ||
| await task | ||
| except (asyncio.CancelledError, Exception): |
| try: | ||
| self._client.interrupt() | ||
| await asyncio.wait_for(asyncio.shield(quiesce), timeout=_QUIESCE_TIMEOUT_S) | ||
| except TimeoutError: |
|
|
||
| reaper_task.cancel() | ||
| with suppress(asyncio.CancelledError): | ||
| await reaper_task |
Add a client-side command palette to the web terminal: a header search button and a Cmd/Ctrl+K hotkey open a fuzzy-searchable overlay over settings, panels, layouts, and actions. Selecting a setting jumps to it in the settings drawer through the existing write-arming warning gate rather than editing inline, so the drawer stays the single config write path. The palette is expert-mode only, and the non-macOS Ctrl+K binding stays out of the terminal so it does not shadow readline kill-line. Supporting changes: - Promisify the settings warning gate as runWarningGate() and add the gated openDrawerTab()/revealSetting() seams the palette builds on. - Extract the iframe state-sync helpers from panel-manager into panel-iframe-sync, and add read-only panel getters plus a bootstrap module for the palette wiring. - Add a whitespace-tokenized fuzzy matcher and a pure, dependency- injected registry builder, both unit-tested.
…redesign # Conflicts: # src/osprey/interfaces/web_terminal/static/js/panel-manager.js
Token-only glow keyframe (agent-flash) and persistent attention-dot badge (agent-attention), plus a flashElement() helper with reflow-restart and animationend self-clean, for highlighting agent-touched UI elements.
agent_activity frames glow the target rail entry and set a persistent attention badge that clears only when the panel actually surfaces; frames tagged source:'agent' get a transient glow. Backend-direct actions (channel writes, run launches, artifact focus) render in a slim status-bar activity strip with latest-wins coalescing, auto-clear, and suppression while the self-signaling target panel is active. All agent-supplied strings render as text nodes.
Unbound plan panels follow agent-origin drafts (literal mcp-agent match) and flash the applied fields; unsaved local edits suppress the switch in favor of a clickable banner that self-clears when the draft moves or the operator navigates. The reset path switches only on first-ever reset or an agent-authored updated_at advance, so reconnect hellos and resyncs never steal the view. The local draft-flash styling migrates to the shared design-system utility.
Chromium coverage of the SSE-to-DOM chains: rail badge set and clear-on-activate, activity-strip entry with auto-clear, 422 with no DOM change, and the guarded auto-switch against a live bridge draft (agent origin switches and flashes; operator origin provably arrives and does nothing). Suite added to the CI browser lane.
…rames
POST /api/agent-activity validates {tool, target:{kind, panel?, detail?}}
(422 on unknown kinds, length-bounded fields) and broadcasts typed
agent_activity frames over the file-events SSE. The panel focus/visibility/
register routes accept an optional source:'agent' tag passed through to their
broadcasts; browser-originated posts stay untagged and byte-identical. A
fire-and-forget notify_agent_activity() helper posts off-loop with a 1s
timeout, swallows all errors, and clamps detail to the route bound.
…ools set_draft/clear_draft, channel_write, launch_run, and the artifact focus tools notify the web terminal after successful operations only: channel_write names just the executed channels and emits nothing on refusals or blocked writes. All emits dispatch off the event loop and never change tool results, including when the terminal is down.
The buffered chat endpoint keys its session pool on a caller-supplied chat_id, so the container smoke test's one-turn probe must send it or the request is rejected as malformed.
…redesign # Conflicts: # CHANGELOG.md # src/osprey/services/bluesky_panels/panels/health/index.html # src/osprey/services/bluesky_panels/panels/health/panel.css # src/osprey/services/bluesky_panels/panels/health/panel.js # tests/interfaces/design_system/test_visual.py
The buffered chat payload is reduced to {text, events, is_error} and
deliberately withholds turn counts, so the container smoke test asserted a
field the endpoint never returns. Check the terminal result event instead.
…redesign # Conflicts: # CHANGELOG.md
Restores the pre-redesign navy/teal palette as an opt-in family (retro-dark/retro-light): the theme sources preserved verbatim, wt-crt interface tokens carrying the live CRT gates that every flat theme holds at 0/none, and the other interfaces inheriting their existing dark/light groups. The theme switcher, server web.theme resolution, and boot registries pick the family up from the regenerated artifacts with no code changes.
The web terminal header's Expert/Simple segmented toggle and always- visible theme switcher are replaced by a single display-menu dot that opens a popover card with appearance (light/dark), view (Expert/Simple), and theme-family pickers. The dot's core is filled with the active theme's accent token; family pills render from the THEMES registry so new families appear automatically. The View row keeps the #mode-toggle contract, so mode persistence, panel broadcast, and the pre-paint CSS gating are unchanged. Standalone fleet pages keep the shared osprey-theme-switcher component.
Bundle a skill that runs short scripted demonstrations of the agent driving the web workspace: a panel tour, an artifact hand-off, and a layout switch, individually or as one continuous tour. Showing this off previously meant spelling out each step by hand every time. The script reads the live panel inventory rather than assuming a fixed tab set, so it adapts to whichever panels a deployment enables, and it restores the starting active tab and visibility set — a demo is often run twice, and often against someone's real working session. The write-approval beat is opt-in and never bypasses the approval prompt. Wire the skill through the artifact catalog, the regen-tracked file list, and the control-assistant preset, and cover those points plus the panel/artifact tool names it choreographs with an install test, so a rename in the workspace tools fails in CI rather than mid-demo.
Adds web.rail_position (left | top): the panel rail can render as a horizontal strip under the header, the arrangement operators know from the pre-redesign tab bar. Resolution mirrors the ui_mode axis (config -> SSR attribute -> pre-paint rail-boot.js -> localStorage -> runtime setter), with controls in the command palette and the panel "+" menu; the top strip is pure CSS over the same rail DOM, so the rail contract selectors are unchanged. A one-time dismissible hint points operators from the old tab bar to the rail, with a one-click flip to the top arrangement. The CRT scanline/bezel/glow effects return as token-gated pseudo-element overlays reading --wt-crt-* values that are 0/none outside the retro family. The docs contact sheet gains retro and top-rail showcase variants, and browser suites pre-dismiss the hint so it cannot intercept their dock interactions.
The web terminal launches Claude sessions from two surfaces — the PTY
terminal (expert) and the operator chat (simple) — but the agent had no
way to know which one it serves. Each launcher now marks its child env
with OSPREY_WEB_UX, and the panels-context SessionStart hook injects a
matching surface line into agent context: simple sessions are told the
workspace starts hidden and to show_panel("artifacts") whenever they
produce something the operator should see.
The surface line is env-only and survives a down web terminal; the panel
inventory remains an independent, best-effort part of the same hook.
With an empty agent workspace, the simple UI now first-paints as pure chat: nothing auto-claims the workspace slot, so the chat keeps the full width. The workspace appears when the agent shows or switches to a panel (show_panel/switch_panel over SSE), when the operator opens one, or on a flip to expert; a workspace that already holds artifacts opens as before. GET /api/panels reports workspace_has_artifacts (first non-hidden file under the agent workspace, recomputed per request) so the client can make the boot decision. The empty-state card and the embed-src builder move to their own modules (panel-empty-state.js, panel-iframe-sync.js), deduping the URL construction and keeping panel-manager inside the lint size cap.
…nto feature/simple-session-context # Conflicts: # CHANGELOG.md
Null-guard the rail-position attribute read and the rail-hint button lookups, and cast the nonstandard vitest-worker probe — tsc checkJs rejects all three as written. No behavior change; the vitest suite covers all touched paths.
…redesign # Conflicts: # src/osprey/mcp_server/workspace/tools/focus_tools.py
artifact_focus now reports gallery failures as tool errors, so the emit-site tests — which exercise the notify seam, not the gallery — acknowledge the gallery POST the same way the focus tool's own tests do.
feat(web): chat-first simple mode with agent-driven workspace reveal
…redesign # Conflicts: # src/osprey/interfaces/web_terminal/operator_session.py # src/osprey/interfaces/web_terminal/routes/chat.py # tests/interfaces/bluesky_panels/draft-client.test.mjs # tests/interfaces/web_terminal/test_chat_routes.py # tests/interfaces/web_terminal/test_operator_session.py
…l-header-toggles Keep both boot registrations in app.js: the display-menu popover and the one-shot ?rail= adoption are independent axes whose init calls compose. The card's #mode-toggle handler only closes the popover, so initModeToggle() remains the sole owner of the mode flip.
feat(web): collapse header display controls into a popover dot
The hub's always-visible osprey-theme-switcher was replaced by the header display-menu dot, so the behavioral theming tests can no longer click the switcher directly. Route the light/dark flip through a shared _flip_hub_theme helper that opens the popover and clicks the appearance option opposite the current theme.
The panels app moved from osprey.services.bluesky_panels to osprey.interfaces.bluesky_panels; update the agent-activity browser test's sidecar import to the new path so the plan-panel stack fixture imports cleanly.
- non-occlusion: wait for #pipeline-switcher visibility before reading its bounding box, so a not-yet-laid-out (width/height 0) capture can't fail the in-viewport assertion on a loaded runner. - drag restack: give the two post-drag layout-settle waits the 10s structural-settle budget used elsewhere in this file (was 5s); the drop registers reliably but dockview's DOM regroup can exceed 5s under CPU load.
Playwright's synthetic drag_to drives dockview's native HTML5 drag-and-drop through a single drop hit-test that a CPU-contended CI runner rarely loses; the regroup then never happens and the post-drag wait times out. Absorb that rare stochastic miss with a scoped rerun — only_rerun=["TimeoutError"] keeps a genuine restack-logic break failing hard via AssertionError. The post-drag waits stay at the file's 5s convention.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🖥️ Web terminal redesign — exploration branch
Hey! This is a parked working branch where we're exploring a more modern front-end for the OSPREY web terminal — a flat icon-rail layout, a dockable panel workspace, and an Expert/Simple mode toggle. It's a design playground, not a merge candidate yet, and the direction is still wide open.
Try it locally
Then, in the terminal that opens in the browser, just ask the agent for a demo — e.g. "give me a demo of the web workspace". That triggers the
demo-uiskill (bundled with this preset), and the agent walks you through the panels, artifacts, and layouts while you watch the UI move.💬 Feedback wanted
This is a shared sandbox — please open feature requests, drop ideas, and give feedback (PR comments or issues). Nothing here is settled; tell us what a great operator front-end should feel like.