* feat(ui): unify focused presentation routes
/focus/<target> replaces unshipped standalone query links across dashboard, terminal, desktop, and native apps.
Gateway-served index assets are anchored so nested documents resolve their bundles from the Control UI base path.
* test(gateway): narrow emitted asset URLs
Fixes check:test-types TS18048/TS2322 by dropping unmatched optional captures before comparing emitted asset URLs.
* test(docs): follow centralized cloud secret guidance
Fixes the stale current-main docs test after #126132 centralized GCP and Hetzner setup in docker-vm-runtime.
* test(ui): retry missing locator reads
The 500ms locator text read can time out while the menu label is still rendering, causing expect.poll to reject instead of using its owning 10s retry window. Treat only Playwright TimeoutError as a missing value so the outer poll retries while page-closure and arbitrary failures still surface.
* test(android): capture TLS probe coroutine
The TLS probe test inferred its coroutine from mutable scope children, racing unrelated child startup and teardown in CI. Capture the exact Job from inside the probe coroutine and join that owner before asserting the stale-attempt guard.
* fix(gateway): preserve plugin focus routes
Keep approval handling ahead of plugin dispatch, but treat focus documents as an unclaimed Control UI fallback after plugin authentication and routing. Exact and prefix plugin routes therefore retain ownership, while unclaimed reads serve the focus document and other methods return 404.
* fix(ui): migrate released terminal links
Preserve stable v2026.7.1 terminal query compatibility by rewriting the root/base ?view=terminal URL once to the canonical /focus/terminal path with history.replace. Keep URL parsing path-only, and leave the removed desktop and dashboard query forms as a hard cut.
* test(codex): assign run-attempt tools shard
Cached filtered configs caused duplicate ownership, and the test lacked a canonical full-suite owner.
* test(ui): keep cloud recovery proof state-owned
The recovery test should assert owner state and reload identity, while dedicated tests own transient alert visibility.
* test(qa): wait for outbound bus state
* fix(qa): reserve gateway ports through staging
* refactor(qa): keep socket creation in gateway owner
Mark stale terminal tabs exited when the Gateway rejects input or resize, so operators see an actionable outcome instead of silently losing keystrokes.
* Control UI: unify chat rails in a tabbed panel
* fix(ui): compact chat side panel navigation
* Polish rail separators around the active tab
* Soften rail tab separators
* Inset the rail tab strip from the resize handle
* Remove the rail tab strip bottom rule
* Add compact close controls to rail tabs
* Redistribute rail tabs after closing a surface
* Restore bottom docking from the rail terminal
* Fade clipped rail tab labels at the edge
* Add drag reordering to rail tabs
* Polish terminal tab chrome
* Polish rail tab close controls
* Regularize rail tab separators
* Space rail tabs evenly
* Add feedback to inactive rail tabs
* Smooth rail tab selection
* Hide unavailable rail menu items
* Preserve browser multi-tab entry
* Support bottom-docked rail tabs
* Polish rail tab sizing
* Refine side panel actions
* Neutralize rail empty-state icons
* Separate dark rail surfaces
* Fade only clipped rail tab labels
* Compact rail header controls
* Refine the surface picker
* Raise and tighten the surface empty state
* Keep Browser and Tasks in the chat topbar
* Unify surface list cards
* Keep embedded rails inside their dock
* Reveal rail tab close actions on demand
* Show full rail labels only when clipped
* Let rail tabs use available space
* Align chat topbar surface controls
* Unify rail surface empty states
* Center rail tab glyphs
* Fade clipped rail labels smoothly
* Keep rail tab labels visible
* Tighten rail tab separator and label fade
* Keep the new-tab control beside the rail tabs
* Keep rail tab separators from reflowing on activation
* Anchor the new-tab control outside the rail tab scroll
* Scroll rail tabs at the floor instead of clipping them
* Tighten spacing between rail tabs
* Report an unusable terminal open response as a readable failure
* Keep the terminal session menu inside the panel
* Share one dock destination cluster across rails, terminal and browser
* Align the terminal mini rail with its management icons
* Tidy panel pickers, browser chrome and task detail retry
* Type the sidebar callbacks object so tsgo:ui passes
* Space out the panel picker rows
* Measure tab strip edges by rect and cancel superseded installs
* Validate every terminal session field and release unusable sessions
* Fold the terminal rail stretch rules into one
* test(ui): match rail tab checks to the tabbed panel header
Anchoring the new-tab control outside the scroll area wrapped the strip in
.side-panel__header-tabs, and every query written as a direct-child chain
started matching nothing. The assertions did not fail loudly: they compared
an empty label list, so three side-panel journeys and the background-tasks
rail went red on a selector detail rather than on behaviour.
Give the label query one named owner scoped to the panel's own header, so
Terminal and Browser strips in the panel body stay excluded while header
layout can keep moving. Freeing that width also means two short tabs now
fit where they used to be squeezed, so the closing step asserts the honest
invariant - a strip that fits again releases the fade - while the overflow
and tooltip regime stays proven earlier in the same journey.
Settle the shared side-panel helper on whichever surface renders: an empty
panel offers its type list and a populated one offers the header menu, and
probing before either existed left it waiting for a control that never
comes. Also drop a shadowed seed binding and an unused locator that oxlint
flagged in the same file.
* fix(ui): adapt session discussion slot to the unified panel layout
* test(ui): capture the bottom dock evidence with a resting divider
* fix(ui): drop the dead previousDock toggle path
* test(ui): align sibling suites with the tabbed panel decisions
* fix(ui): keep narrow-pane hides above the icon-button styling cascade
* fix(ui): create tooltip descriptions via ownerDocument
* fix(lint): merge duplicate terminal header styles and drop dead branches
* refactor(ui): extract embedded panel templates from the pane render
* refactor(ui): delete rail-era dead exports
* test(ui): repair type drift after the rebase
* fix(ui): keep the workspace toggle module-local and settle test caps
* fix(ui): give side-panel tabs their runtime and their own header actions
The tabbed side panel dropped two things the old rails owned. The discussion
tab lost its only runtime registration, so `openclaw-session-discussion` was
never defined and the tab rendered as a permanently blank box; it now loads
through the per-slot lazy runtime map like the other panel types.
Panels also have no header of their own anymore, so any action on the active
panel's content had to reach the shared header. The header now takes panel
contributed actions instead of a discussion-only open-url special case, which
brings back the side-chat "Clear thread" overflow menu — its gateway reset had
no reachable entry point at all in the embedded rail.
* test(ui): anchor the sidebar scroll proof to the panel host
The bounding contract moved from `.sidebar-column__panel` to
`.side-panel__panel`; mounting the detail panel under the removed class let it
grow instead of scroll, so the test failed for its harness rather than for the
behavior it guards.
* refactor(ui): move side-panel wiring out of the chat render monolith
`chat-pane-render.ts` crossed its line budget. The region callbacks are layout
policy, so they resolve in the layout module with the pane injecting only what
it owns, and the two panel-action producers collapse into the single header
contribution the region consumes.
* fix(ui): keep one terminal intent queue per document
A session route mounts the side-panel terminal beside the shell instance kept
for the bottom dock. Each owned a private action array over one sessionStorage
key, so their whole-array writes erased each other's intents and a freshly
mounted panel drained through a reconnect fence it never saw.
The queue, its persisted record, and the fence now belong to the document;
panels bind as executors while connected. The chat pane records a toggle intent
where it observes it, instead of parking the raw event in memory and handing it
to a panel that may not be mounted yet — a service-worker reload in that window
dropped the request, thread id and all, with nothing explaining why. That makes
the deferred-restore handshake dead weight, so it goes.
* fix(ui): let the panel that starts a terminal intent finish it
Opening a catalog terminal from a chat route mounts the side-panel terminal
right after the request, which swapped the queue's executor mid-flight and
stranded the in-flight action; the panel's own mount restore then opened a
plain session instead. The executor that began an action stays responsible for
it as long as it can still run.
* test(ui): cover closed legacy side docks
* refactor(ui): split side panel controllers
* fix(ui): stabilize embedded panel handoffs
* style(ui): format browser panel refresh
* test(ui): settle catalog prepend paint incrementally
* docs(ui): describe the unified chat side panel
* test(ui): settle catalog clock transitions
---------
Co-authored-by: Jason (Json) <263060202+fuller-stack-dev@users.noreply.github.com>
* fix(ui): standardize scrollbar styling across the Control UI
Give the Control UI one canonical scrollbar definition (a 12px hit
target with a thin, muted-at-rest, 6px painted thumb, derived from
--muted so every theme family and both modes follow with no override)
and delete the sixteen scattered per-surface overrides that used to
make every scroll container look different.
Shadow-DOM parity ships in the same change: a grouped ::part() rule
covers Web Awesome menus/listboxes, and a shared Lit fragment covers
the terminal/browser/desktop panels' own shadow roots, since neither
::-webkit-scrollbar* rules nor scrollbar-width cross a shadow
boundary on their own.
Fixes#124272
* fix(ui): reach every Web Awesome scroll part through generic selectors
The canonical scrollbar group enumerated six menu hosts plus the settings
select, so every unlisted Web Awesome surface kept the raw platform
scrollbar: a Chromium probe against the previous stylesheet reports
width:auto and a fully transparent thumb for wa-dropdown::part(menu),
wa-select::part(listbox), and wa-popover::part(body).
Style the three generic part families instead -- the same host group the
shared corner treatment already uses at base.css:435 -- which covers every
dropdown, listbox, and popover in the UI and removes 23 production lines.
The new-session composer's ten-line cap test asserted thinness as a
<=6px hit target, which was the old shrink-the-target recipe the canonical
profile replaces. It now asserts the real contract: a 12px hit target
painting a 6px thumb through the transparent border. The chat-page e2e
gains a genericity guard that walks every wa-dropdown/wa-select/wa-popover
on the page and requires each scroll part to report the canonical width,
so an unlisted component cannot silently regress.
The light terminal theme reused the dark-background ANSI palette (1.4-2.6:1
contrast on #f7f8fa; brightWhite was 1.06), and the intro banner hardcoded
256-color indices 223/216 that bypass the client theme entirely.
Light mode now gets its own darkened ANSI-16 palette (>=4.5:1, bright
variants darker for emphasis, brightWhite maps to strongest ink), and the
banner emits themable ANSI-16 yellow/bright-red so each mode owns its
rendering. Dark mode is visually unchanged.
* improve(ui): align transient menu chrome with design-system geometry
Use one shared transient-menu recipe across Control UI panels and items.
Match the carapace reference: 6px item radius plus 4px inset yields a 10px panel radius, with restrained motion for fixed menus.
* test(ui): align gateway picker menu expectations
* test(cli): keep benign sqlite diagnostics out of ACP exit stderr assertions
* fix(ui): restore 44px menu touch targets on coarse pointers
Behavior-preserving follow-up cleanup related to #121637.
Make replay a required structured contract, move stream currentness into AbortSignal ownership, keep controller construction private to the session owner, and remove the optional replay fallback, exported factory, tab factory state, and nested currentness callbacks.
* fix(ui): scope the Control UI cursor convention to app-like display modes
The app-chrome cursor convention from #103357/#103411 was applied
unconditionally, so an ordinary browser tab lost the pointer hand on
buttons, menus, tabs, rails, selects and accordion summaries — the only
hover affordance a page owns. The convention is correct for the installed
window the manifest declares ("display": "standalone"), not for every
window the same bundle is served into.
base.css now owns one policy token, --cursor-action, selected by display
mode: pointer by default, the desktop arrow under standalone, minimal-ui
and window-controls-overlay. A low-specificity rule maps generic
actionable controls onto that token, restoring the affordance on the
surfaces #103411 stripped bare, while every component rule that owns a
semantic cursor (not-allowed, disabled, grab, resize, zoom-in, text,
wait) keeps winning without !important.
The 92 cursor: pointer declarations that had drifted back into ui/src
since July now consume the token instead of hardcoding the hand, so they
stop contradicting the policy in an installed window. Real hyperlinks
keep the pointer in every mode. The pre-boot mount fallback repeats the
policy locally because it must render when the bundle fails to load.
Closes#121242
* test(ui): run the cursor policy browser test in the node-driven project
ui/vitest.config.ts routes Playwright-from-Node .browser.test.ts files to the
unit-node project; without registering the new cursor policy test there, the
in-browser chromium project tried to import it and failed on node:fs/playwright.
* fix(ui): keep the desktop arrow in the native app hosts
The macOS dashboard embeds the Control UI in a plain web view, which
reports `display-mode: browser`, so the display-mode-only policy would
have handed it the browser-tab pointer. It already announces itself with
`openclaw-native-macos`/`-nav`/`-web-chrome` on `<html>`, the same markers
`ui/src/styles/layout.css` matches on, so the policy reads those too.
* fix(codex): pin catalog CODEX_HOME onto terminal resume spawns
The Codex session catalog lists threads through the plugin's supervision
app-server connection, which can run on the agent-scoped Codex home
(homeScope: "agent"). The terminal resume plan spawned a bare
'codex resume <threadId>' under the operator login shell, so the CLI
resolved its default ~/.codex and failed with "No saved session found".
Terminal plans now carry the catalog connection's CODEX_HOME (local and
paired-node paths), the gateway merges plan env into the PTY spawn, and
the triplicated home-scope resolution collapses into one shared
resolveCodexAppServerLocalHomeDir.
* feat(ui): add main-content terminal placement
The terminal panel gains a third dock placement, "main", filling the
content region next to the sidebar like the transcript pane. The panel
header gets a bottom/right/main placement switcher (persisted in the
existing openclaw.terminal.panel.v1 store), and opening a Codex/Claude
session from the session catalog selects main mode so resumed sessions
appear as a full window instead of a bottom strip.
Shadow-DOM components carried fallback colors that never matched the
actual tokens: panel-tab-strip fell back to teal #4ec9a8 for the coral
--accent, resizable-divider to Bootstrap blue #007bff, and --border to a
generic #333. Align all fallbacks with the claw-family values in
styles/base.css so token-less rendering matches the product palette.
Also document the intentionally dark image-lightbox chrome and the
terminal theme's hand-mirrored token values, and regenerate the drifted
values in ui/docs/design-system/color-tokens.md (--muted, --muted-strong,
--primary, --danger, --info, light status colors) from base.css truth.
* perf(ci): drop compact bin cap to 190s to split straggler pairings
Fresh census from green run 29633628994: every fan-out job queues 65s,
the pack sits at ~160s, but compact-large-3 ran 195s because the 235s
cap let core-runtime-media-ui (hint 124) and core-unit-src-security
(hint 95) share one bin. A 190s cap forbids such pairings; the plan goes
from 21 to 25 compact bins and the hinted max drops accordingly, so the
run wall tracks the pack instead of one straggler. Costs ~4 extra
runners per run; wall is the optimization target.
* perf(test): run DOM-free Control UI tests in the node environment
35 ui/src test files whose transitive import closure never touches DOM
APIs or the lit runtime carried the ui config's jsdom default anyway; on
CI the per-file jsdom construction dominates the shard (environment
95.2s vs tests 59.4s in run 29633628994). Annotate them with the
repo-standard @vitest-environment node docblock (29 files in ui/ already
use it). Verified: full ui config run passes with the annotations; the
only failing files are three storage tests that fail identically on a
clean checkout under local Node 26 and are untouched here.
* chore: kick dropped push event for PR head ingest
* refactor(ui): share one panel tab strip between terminal and browser
Extract the terminal tab strip (tab + attached close button as one
surface, from #108611) into a shared panel-tab-strip module and migrate
the in-app browser panel onto it. The browser tabs inherit the attached
close design; terminal tabs gain middle-click close and long-label
ellipsis; both get the empty-group workaround and a strip that shrinks
inside the flex header. Net -24 prod LOC; duplicated tp-/bp- strip CSS
deleted.
* fix(ui): keep shared panel new-tab button stable