Skip to content

Web terminal redesign: icon rail, dockable workspace, simple-mode chat#442

Draft
thellert wants to merge 42 commits into
mainfrom
feature/web-terminal-redesign
Draft

Web terminal redesign: icon rail, dockable workspace, simple-mode chat#442
thellert wants to merge 42 commits into
mainfrom
feature/web-terminal-redesign

Conversation

@thellert

@thellert thellert commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

🖥️ 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

# 1. Grab the branch
git clone https://github.com/als-apg/osprey.git
cd osprey
git checkout feature/web-terminal-redesign

# 2. Install it (editable) in a fresh venv
python -m venv .venv && source .venv/bin/activate
uv pip install -e .        # or: pip install -e .

# 3. Build a local control assistant to launch from
#    (first build installs the project's deps — give it a few minutes)
osprey build my-assistant --preset control-assistant
cd my-assistant
# add a provider API key to .env if it isn't already in your environment

# 4. Launch the redesigned web terminal → http://localhost:8087
osprey web

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-ui skill (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.

thellert and others added 10 commits July 21, 2026 06:41
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.
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
Comment thread tests/interfaces/web_terminal/test_chat_routes.py Fixed
Comment thread tests/interfaces/web_terminal/test_chat_routes.py Fixed
Comment thread tests/interfaces/web_terminal/test_chat_routes.py Fixed
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.
Comment thread tests/interfaces/web_terminal/test_chat_browser.py Fixed
Comment thread tests/interfaces/web_terminal/test_chat_browser.py Fixed
thellert added 17 commits July 22, 2026 17:44
…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.
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.
thellert added 14 commits July 24, 2026 06:57
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant