Users pasting a Tailscale IP or LAN host as 'host:port' (no http://) hit
either a hard 'URL is not valid' error in the main process or, worse, a
silent dead probe in the renderer: the ^https?:// gates in the settings
and first-run forms never fired, so the field sat idle with no feedback.
- normalizeRemoteBaseUrl() (electron/connection-config.ts) now prepends
http:// when the input has no scheme:// prefix; explicit non-http
schemes (ws://, ftp://) still reach the protocol check and get a clear
rejection.
- New renderer twin coerceRemoteUrlScheme() (src/lib/remote-url.ts),
wired into both probe gates (gateway-settings.tsx and
first-run-remote-form.tsx) so the debounced /api/status probe, sign-in,
test, and save all see the coerced URL.
- Tests for both sides (electron/connection-config.test.ts,
src/lib/remote-url.test.ts).
The logs pane no longer exists as standing chrome: it isn't registered in
any default layout or preset, never rides the terminal strip as a secondary
tab, and never opens automatically. The contribution is registered only
while the palette's "Toggle logs" command has it summoned, and closing it
(toggle, tab ✕, ⌘W) removes it from the registry and the tree entirely —
including sweeping it out of persisted layouts from before it was
summon-only. The open state is session-only, so a fresh boot always starts
without it.
useAgentTerminal hardcoded resolveSurfaceColor('#ffffff'), so any read that
misses the surface token — a pre-paint mount — flashed a white slab in dark
mode. The user terminal already passes the palette's own background; the
agent mirror is a copy of it that lost that detail.
Pass the same fallback, so both siblings degrade to their mode's surface.
The terminal borrowed --ui-editor-surface-background, the same token the
preview pane, diff gutters, and pane tabs wear. Nothing could retint the
terminal without moving all of them, and it was pinned to the editor surface
rather than the conversation it sits beside.
Give it --ui-terminal-surface-background, aliased to the chat surface, and
point every layer that paints the terminal at it: the pane wrapper, the
persistent overlay, the rail, the instance chrome, the xterm host, and the
probe that feeds xterm's canvas. One knob, and a skin can now move the
terminal alone.
No visual change today — both tokens resolve to --ui-bg-chrome.
The two xterm host divs carried an identical class string; hoist it to
HOST_CLASS so the user terminal and the agent mirror can't drift again.
The terminal row still lived under Go to as a one-way open. It never showed
live state and couldn't hide. Move it through paletteToggle next to logs,
yolo, status bar, and layout edit so every binary ⌘K toggle shares the same
underlined on/off note.
The hit targets are display:contents buttons now, so the branch label and the
counts stay the same flex children of the row with the same classes; the glyph
button fills the existing 3.5 leading slot. Only the hover background is gone.
Evict the runtime the backend just reclaimed instead of waiting for a
resume to 404, and refresh the lists whose ended_at moved. The stored
row is untouched, so reopening resumes from the DB.
The composer's coding strip made the whole bar a button, so a click anywhere
along it — including the dead space between the branch and the counts — opened
the review pane. Only the two things that name the diff are clickable now: the
branch glyph + label, and the ahead/behind + ±lines cluster. The strip itself
is inert and no longer paints a hover state.
Two things made a tool panel tab feel unclosable.
Cmd-W was a dead key over the terminal and the logs pane. The keyboard
close ladder resolved its target with focusedSessionGroup, which only
matches zones hosting a CHAT strip, so a focused tool panel fell through
every rung and Cmd-W emptied the main tab instead. Add a tool rung that
resolves through the same hover/focus ladder the number keys use.
Right-click Close was missing or inert. The zone menu's target was only
resolved by the tab strip's own onContextMenu, so a right-click anywhere
else in the zone (pane body, collapsed rail, edit veil) reused the
PREVIOUS target -- landing on the uncloseable workspace dropped Close
from the menu entirely. Resolve the target on the zone instead, so every
surface that opens the menu names the chip under the pointer.
Close on a tool panel now takes the tab out of the strip and syncs its
owning store, so the ctrl-backtick toggle and the Cmd-K row stay
truthful and bring the pane back; the toggle's open path reveals
(un-dismiss + re-adopt) rather than un-collapsing a pane that has left
the tree.
The logs (and terminal) tab ✕ dismissed the pane from the layout but
never synced the owning store — so the ⌘K toggle was stale and its open
listener called setPaneCollapsed, a no-op when the pane isn't in the
tree. The tab was gone with no way back short of a layout reset.
Route the tab ✕ through closeCollapsePane (dismiss + store sync) so the
toggle stays truthful, and make bindPaneCollapse's open listener call
revealTreePane (un-dismiss + re-adopt) instead of setPaneCollapsed.
pin brace-expansion to 5.0.8
update concurrently to 10.0.4
update electron-builder to 26.15.3
update eslint to 10.8.0
update eslint-plugin-perfectionist to 5.10.0
update @assistant-ui/react to 0.15.0
update @assistant-ui/react-streamdown to 0.3.8
update radix-ui to 1.6.7
update react-router-dom to react-router@8.3.0 - react-router-dom is no longer a standalone package, it just reexports react-router
remove @radix-ui/react-slot: we import this from `radix-ui`
remove eslint-plugin-react: we imported it, but never actually used it!
The statusbar is now opt-in. Existing users with a stored preference
keep their choice; new users get a clean bottom edge. The way back is
the view.toggleStatusbar keybind or the ⌘K row, unchanged.
powerMonitor's AC/battery state is mirrored to the renderers
(store/power.ts) and visiblePoll quadruples its cadence on battery. Only
the safety-net refreshes slow down — event-driven refreshes and live
streaming are untouched.
It loads arbitrary user-linked pages offscreen; unthrottled, a heavy page
burns full CPU for the window's whole lifetime. Title resolution rides
load events and main-process timers, which throttling doesn't touch.
The process-wide disable-background-timer-throttling /
disable-backgrounding-occluded-windows switches plus a static
backgroundThrottling: false on every chat window pinned each renderer's
document.visibilityState to 'visible' for the life of the window. Every
visibility-gated backstop poll and clock tick in the renderer became an
always-on timer: an idle, minimized Hermes burned ~20% CPU around the
clock, on battery too.
Throttling is now a runtime dial. A small controller (stream-throttle.ts)
rides the merged hermes:active-work reports the quit guard already
receives: while any turn is in flight every chat window gets
setBackgroundThrottling(false) — a live answer keeps painting while
blurred, occluded, or minimized, exactly as before — and once all turns
settle (plus a 5s trailing window so the final flush lands at full
cadence) Chromium's default throttling returns and hidden windows go
quiet.
disable-renderer-backgrounding stays: process priority only, no timer
semantics, and it keeps hidden streaming fast.
Select-all + Cut emptied the text and left the composer blank — no draft,
no prompt. Delete had the same hole.
The placeholder is painted on `:empty`, and a cleared editor keeps a
scaffolding <br> so the contenteditable can't collapse to a sliver. Those
two facts collide: the moment the break lands the editor has a child,
`:empty` goes false, and the prompt never comes back.
CSS can't infer emptiness on its own either. A text node is invisible to
selectors, so `one<br>` and a lone `<br>` are the same shape — a structural
rule like `:has(> br:only-child)` paints the placeholder straight over the
user's text. The code that empties the editor is what knows, so it marks
the root and the condition reads `:is(:empty, [data-empty])`.
Both writers that reshape that root maintain the marker through one helper:
the normalizer, and renderComposerContents for a restored draft or an undo.
The message-edit composer shares the slot and the rule, so it takes the
same shared class instead of drifting on its own copy.
#74815 fixed the draft this stashed; the placeholder is a separate seam.
The tab strip decided the close gesture from the `uncloseable` flag, which the
workspace sets to keep its pane in the tree — so the one tab whose close now
does something couldn't be ⌘-clicked or middle-clicked, and its right-click
menu had no Close.
Read the gesture off the pane's registered closer instead, with the workspace
registering closeWorkspaceTab. An atom rather than a lookup, since that closer
comes from a wiring effect that lands after the strip's first paint.
The workspace pane can't leave the tree, so "close the main tab" only ever had
one answer wired: shift the next stacked session in. With main as the only tab
there was nothing to shift and ⌘W dead-ended on the tab the user was looking
at.
closeWorkspaceTab is now the one answer for every entry point — stacked
session still wins, and with nothing stacked main drops to a fresh New session
draft. A blank draft and a full-page view stay no-ops: a blank draft already
IS the post-close state.
Chromium on Windows and Linux answers a middle press inside a scroller by
starting the autoscroll pan, and the mouseup that ends the pan never becomes
an auxclick. Every surface carrying the gesture — tab strips, the session
list, the terminal rail — is a scroller, so middle-click only ever worked on
macOS, where autoscroll doesn't exist.
Arm on pointerdown, spend on the pointerup over the same element (press one
tab, release on another and nothing happens), and cancel the middle mousedown
on every press so the pan widget can't appear on a surface that owns the
button. One helper, four call sites.
⌥-drag is the app's force-selection gesture over mouse-mode TUIs, but
xterm's default alt-click-moves-cursor claims the same click and emits one
cursor left/right escape per column of travel. Shells that don't consume
them echo the raw `^[[D` burst into the buffer. One gesture, one meaning.
Both of xterm's link paths activate through `window.open()`, which the
window's setWindowOpenHandler denies, so ⌘-clicking a URL did nothing but
log "Opening link blocked as opener could not be cleared" — and the OSC 8
path fronted that dead end with a raw confirm() dialog. Route both through
the desktop bridge, the path every other external link in the app takes.
⌘-click on macOS, Ctrl-click elsewhere, matching VS Code's integrated
terminal, Terminal.app, and iTerm2. A bare click stays with the selection so
a misclick on a URL can't launch a browser.