Commit Graph

1827 Commits

Author SHA1 Message Date
brooklyn! 41e55679ee Merge pull request #75848 from NousResearch/bb/toggle-terminal-persist
Toggle any pane wherever you put it, and keep the header hidden
2026-08-01 01:32:36 -05:00
Brooklyn Nicholson feaa325033 Merge origin/main into bb/toggle-terminal-persist
main reworked the same surface while this was open, so three hunks needed
deciding rather than accepting.

Logs became summon-only (#75862): the contribution only exists while $logsOpen
is on, docked as its OWN zone beside the terminal instead of a tab in its
strip. That supersedes the static logs pane and the bindToolPaneCollapse call
here — main already registers logs' closer/opener directly, so both were
dropped in favour of its version.

main also added a ⌘K "Toggle terminal" row reading $terminalTakeover, and
gave logs back a 7.5rem minHeight under a comment claiming the terminal's
sizing rule. Both are the bugs this branch fixes, so they move onto the
shared behaviour: the palette row reads isPaneVisible/togglePaneVisible like
every other pane toggle, and logs loses the floor so the comment is true —
the sash folds its zone to the rail instead of stranding a sliver.
2026-08-01 01:25:33 -05:00
Brooklyn Nicholson 0b8a3582c6 fix(desktop): cap the changed-files card and fade its overflow
A turn that rewrote twenty files grew a twenty-row card, so the summary
that is supposed to close the turn became the thing you scroll past to
reach the composer. Cap the rows at ~5 and let the clipped edge fade,
the way every other overflow in the app reads.

The horizontal padding moves onto the scroller so a row's hover fill
still bleeds to the card's edge instead of stopping at a scroll gutter.
2026-08-01 01:24:05 -05:00
Brooklyn Nicholson 15b0b95413 refactor(desktop): one edge-faded scroller for the whole app
The kanban drawer had grown the only edge-aware masked scroller in the
tree, and the next surface that wants one would have copied it. Lift it
to components/ui as FadeScroll, export it on the plugin SDK, and leave
kanban's ScrollFade as a name its call sites already pass `max` to.

The mask math comes out as two pure functions. jsdom's CSS parser drops
any gradient containing calc(), so a rendered mask-image can't be read
back off the style attribute -- edgeMask/scrollEdges are testable for
real where the component's inline style is not.
2026-08-01 01:23:59 -05:00
Brooklyn Nicholson 73b8847d7b fix(desktop): toggle every pane off the tree, not off its own boolean
The terminal fix was only one instance. An audit of the other pane toggles
found ⌘G and ⌘J diverging the same way, proven with a probe: with review
stacked behind files in the right column, or either pane inside a minimized
zone, the store reads open while nothing is on screen, so the press
re-asserts a value it already held and the key does nothing.

isPaneVisible / togglePaneVisible replace the tool-panel-only pair and now
back every toggle. Close still routes through closeTreePane, so each pane
keeps its own semantics: a tool panel collapses to its rail, files and
review close through their store, anything else is dismissed.

files and review were bound with a closer and no opener, so the boolean went
stale as soon as anything but the toggle revealed them. bindPaneVisibility
moves into the tree store beside bindToolPaneCollapse, documents the two as a
pair, and both panes now pass both halves. Keeping the binding in the store
also means the tests drive the real function — the earlier copy in the test
file passed with the fix reverted, which is how the missing opener survived
the first pass.

setTreePaneHidden keeps its quiet path: a reactive unhide (a cwd arriving)
must not front or un-minimize over what the user is looking at. Only user
intent goes through the reveal path.
2026-08-01 01:17:45 -05:00
Brooklyn Nicholson 41e4b96233 fix(desktop): restore reactive message and composer runtime hooks
The 0.15 migration swapped the `useMessageRuntime()` hook for a plain
`useAui().message` accessor read. The hook subscribes and re-renders on
change; the accessor does not, so message components paint once and then
go stale until the window is reloaded. `useComposerRuntime().subscribe`
was widened to `aui.subscribe` the same way.

Restores the 0.14 call form, including the test mock whose added
`getState` stub let the API break through CI unnoticed.
2026-08-01 00:58:32 -05:00
Brooklyn Nicholson 031b0a6786 fix(desktop): pin @assistant-ui/react back to 0.14.24
The 0.15 bump rode along with the npm audit pass in #75037, but it was
not required by any advisory — `npm audit` reports 0 vulnerabilities with
0.14.24 pinned. It is a breaking major that costs us a working transcript,
so take the API stability instead.
2026-08-01 00:58:32 -05:00
brooklyn! c74f4c5335 Merge pull request #75890 from NousResearch/bb/disk-full-toast
Toast when a send fails because the disk is full
2026-08-01 00:36:21 -05:00
teknium1 087b2230c4 fix(desktop): accept scheme-less host:port in the remote gateway URL field
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).
2026-07-31 22:28:43 -07:00
Brooklyn Nicholson 69902c203e fix(desktop): toast when a send fails because the disk is full
Map ENOSPC / SQLITE_FULL / "disk full" error strings through notifyError to a
clear free-space toast, and fire it from rejected prompt.submit, gateway error
events, and terminal failure frames so a full disk never looks like silence.
2026-08-01 00:27:17 -05:00
brooklyn! 0454b370d1 Merge pull request #75862 from NousResearch/bb/cmdk-on-off
fix(desktop): put Toggle terminal on the ⌘K on/off pattern
2026-08-01 00:21:11 -05:00
brooklyn! e25aa3a338 Merge pull request #75873 from NousResearch/bb/logs-cmdk-only
Make the logs pane cmd+K-only — never auto-opened, never a standing tab
2026-08-01 00:18:59 -05:00
Brooklyn Nicholson 75fb139977 fix(desktop): make the logs pane summon-only via the command palette
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.
2026-08-01 00:09:43 -05:00
Brooklyn Nicholson 6f756272f8 fix(desktop): stop the agent terminal falling back to a white background
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.
2026-08-01 00:06:48 -05:00
Brooklyn Nicholson 1a2eee0d63 feat(desktop): paint the integrated terminal with the transcript background
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.
2026-08-01 00:06:42 -05:00
Brooklyn Nicholson 3572424402 fix(desktop): put Toggle terminal on the ⌘K on/off pattern
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.
2026-07-31 23:59:36 -05:00
Brooklyn Nicholson 16431b8ab2 test(desktop): cover the tool-panel toggle and hidden-header regressions 2026-07-31 23:49:28 -05:00
Brooklyn Nicholson aca571ef49 feat(desktop): let a tool panel shrink to its collapsed header
The terminal and logs carried a 7.5rem minHeight, so dragging the seam down
jammed against a floor with a sliver of unusable terminal still showing.

Tool-panel zones now floor at COLLAPSED_ZONE_PX (the h-7 header strip)
instead of the generic 80px, and releasing at that floor minimizes the zone
rather than persisting the sliver — it folds to its collapsed header,
vertical rail or horizontal strip depending on the parent axis. The sliver
size is never written, so restoring returns the height it had before.
2026-07-31 23:49:28 -05:00
Brooklyn Nicholson 4802d2d88d fix(desktop): keep a hidden zone header hidden
Two independent clobbers threw the choice away, so "hide header" never
survived a close/reopen cycle.

normalize() deleted headerHidden whenever a zone dropped to one pane, on
the grounds that a lone zone is headerless by default. It is — but the flag
is the user's standing preference for that zone, not a redundant value, and
dropping it meant the bar returned the moment a pane rejoined (close the
stacked logs pane, toggle it back).

Re-adoption then pinned headerHidden false unconditionally. That is right
for a pane arriving somewhere new, where zero chrome leaves no handle to
drag or close, and wrong for a zone whose bar the user deliberately hid.
Adoption now carries the destination zone's own setting, read before the
insert since insertAtGroup pins the flag itself on a center drop.
2026-07-31 23:49:11 -05:00
Brooklyn Nicholson de46fb0365 fix(desktop): make the terminal toggle work wherever the terminal sits
Dragging the terminal to the bottom stacks logs into its zone, and every
tool-panel toggle broke in that stack.

Boot revealed instead of leaving the tree alone: bindPaneCollapse ran
setPaneCollapsed(id, !open), and `false` there fronts the pane. Logs binds
last, so it stole the active tab from the persisted tree. Ctrl-` then asked
to collapse a terminal that was no longer the active tab, the shared-zone
branch declined by design, and the key did nothing until the stack was
broken up. Boot now only ever collapses.

The toggles also asked the wrong question. !$terminalTakeover.get() flips a
boolean that has no idea what is on screen, so once anything else moved the
pane the press spent itself re-asserting a value the store already held.
toggleToolPane derives from the tree instead, and ctrl-`, Cmd+J's terminal
fallback, the statusbar button and the logs palette row all route through
it. The terminal cycle/close keys drop the same stale boolean.

bindPaneCollapse moves into the tree store as bindToolPaneCollapse so the
boot rule is testable against the real function rather than a copy.
2026-07-31 23:48:56 -05:00
brooklyn! e08870f62c Merge pull request #75831 from NousResearch/bb/coding-row-hitbox
Composer coding strip opens the diff only from the branch and the counts
2026-07-31 23:43:35 -05:00
brooklyn! 4579838fd3 Merge pull request #75836 from NousResearch/bb/session-reap-notify
A reclaimed session tells the client instead of vanishing
2026-07-31 23:11:32 -05:00
Brooklyn Nicholson 54ddc96b69 fix(desktop): keep the coding strip's layout byte-identical
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.
2026-07-31 23:09:23 -05:00
Brooklyn Nicholson 9824e9585c fix(desktop): drop cached state for a reclaimed session
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.
2026-07-31 23:04:45 -05:00
Brooklyn Nicholson 6b3d8cf2f1 fix(desktop): open the review pane from the branch and diff counts only
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.
2026-07-31 23:00:57 -05:00
Brooklyn Nicholson 696c2e8265 feat(desktop): open Plugins from the command palette
Settings Plugins was missing from cmd+K Settings group. Add the deep-link
row next to the other non-config tabs so typing plugins lands on the page.
2026-07-31 22:49:07 -05:00
brooklyn! 4b60979dc1 Merge pull request #75583 from NousResearch/bb/hide-kanban-worker-sessions
Kanban worker runs stop appearing as chats in the sidebar
2026-07-31 14:09:01 -05:00
hermes-seaeye[bot] 17e5f7244a fmt(js): npm run fix on merge (#75582)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-31 18:54:27 +00:00
Brooklyn Nicholson e41d2029b7 fix(sessions): keep kanban worker runs out of the session lists
Exclude the source from the desktop sidebar and project tree, the TUI
resume picker, session_search, and the CLI session listings.
2026-07-31 13:53:04 -05:00
brooklyn! 890e3c34c4 Merge pull request #75563 from NousResearch/bb/logs-tab-close
Close the terminal and logs tabs like any other tab
2026-07-31 13:44:49 -05:00
Brooklyn Nicholson 2286ee58c5 fix(desktop): drop imports left unused by the shared close routing 2026-07-31 13:36:53 -05:00
Brooklyn Nicholson c25e4fe687 fix(desktop): close the terminal and logs tabs like any other tab
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.
2026-07-31 13:21:57 -05:00
Brooklyn Nicholson cfae306ab6 Revert "fix(desktop): tool panel tab ✕ can close and re-open"
This reverts commit dd48d9a816.
2026-07-31 13:07:51 -05:00
ethernet 6ecd335aa8 Merge pull request #75037 from NousResearch/sec-fixes
fix(sec): patch vulnerable deps + add publication-age floors and npm script allow-list

Co-authored-by: Kingsley Wong <7207924+datanerdie@users.noreply.github.com>
Co-authored-by: viky <vikyw89@gmail.com>
Co-authored-by: FT_IOxCS <237263164+ft-ioxcs@users.noreply.github.com>
Co-authored-by: 方明元 <fmy3@qq.com>
Co-authored-by: Yorkstone Supplies <58149681+sycamoregroupltd@users.noreply.github.com>
Co-authored-by: Steven Cuz Leath <Steven.Leath@gmail.com>
Co-authored-by: Kyle French <248366920+Dadmin88@users.noreply.github.com>
Co-authored-by: Eugeniusz Gilewski <egilewski@egilewski.com>
Co-authored-by: Christopher Gara <79837758+christopherrobin88@users.noreply.github.com>
Co-authored-by: LironTTG <147833337+LironTTG@users.noreply.github.com>
Co-authored-by: Austin Porada <bbasketballer75@gmail.com>
Co-authored-by: cresslank <9219265+cresslank@users.noreply.github.com>
Co-authored-by: Ion Mudreac <mudreac@gmail.com>
Co-authored-by: martinramos002 <262243228+martinramos002-bot@users.noreply.github.com>
Co-authored-by: Sensie-Agents <agents@joinsensie.com>
Co-authored-by: alexwill87 <173086651+alexwill87@users.noreply.github.com>
Co-authored-by: BullishMomentum56 <218643122+BullishMomentum56@users.noreply.github.com>
Co-authored-by: pintadoai <240097310+pintadoai@users.noreply.github.com>
Co-authored-by: Alfred Sahlberg <dinmail@gmail.com>
Co-authored-by: liuhao1024 <sunsky.lau@gmail.com>
Co-authored-by: Richard Ham <richard.ham@live.com>
Co-authored-by: jrcrittenden <jrcrittenden@gmail.com>
Co-authored-by: 峯岸 亮 <1920071390@campus.ouj.ac.jp>
Co-authored-by: Marcus Martini <6473852+napoleonmm83@users.noreply.github.com>
2026-07-31 14:07:32 -04:00
brooklyn! 0324849fe4 Merge pull request #61173 from NousResearch/bb/desktop-kanban
feat(desktop): Kanban — the founding plugin on the desktop SDK
2026-07-31 13:00:10 -05:00
Brooklyn Nicholson dd48d9a816 fix(desktop): tool panel tab ✕ can close and re-open
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.
2026-07-31 12:54:41 -05:00
ethernet 390a1771b2 fix(sec): update npm deps to resolve npm audit warnings
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!
2026-07-31 13:42:03 -04:00
Brooklyn Nicholson d399c1644b fix(desktop): statusbar off by default
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.
2026-07-31 12:11:23 -05:00
hermes-seaeye[bot] daa1befaf6 fmt(js): npm run fix on merge (#75517)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-31 16:22:58 +00:00
Carbon c31c27e03a fix(desktop): preserve voice stop across speech setup 2026-07-31 09:44:53 -04:00
Brooklyn Nicholson 044800e358 perf(desktop): stretch backstop polls while on battery
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.
2026-07-31 00:38:11 -05:00
Brooklyn Nicholson 8ccb4c2cee perf(desktop): scope background-throttling opt-out to live streaming
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.
2026-07-31 00:38:11 -05:00
brooklyn! cdca247424 Merge pull request #75180 from NousResearch/bb/composer-cut-placeholder
The placeholder comes back when you clear the composer
2026-07-30 23:54:19 -05:00
Brooklyn Nicholson 0b4bd3c7c7 fix(desktop): the placeholder comes back when you clear the composer
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.
2026-07-30 22:54:20 -05:00
hermes-seaeye[bot] b1858f33a1 fmt(js): npm run fix on merge (#75159)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-31 03:08:09 +00:00
brooklyn! ab158e8088 Merge pull request #75127 from NousResearch/bb/close-last-tab
desktop: closing the last main tab lands on New session, and middle-click works on a real mouse
2026-07-30 21:58:43 -05:00
Brooklyn Nicholson 193e5f84f7 fix(desktop): the main tab can be closed by gesture and menu
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.
2026-07-30 21:43:15 -05:00
Brooklyn Nicholson c7b021ca48 fix(desktop): closing the last main tab lands on New session
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.
2026-07-30 21:43:15 -05:00
Brooklyn Nicholson 463fbf5b16 fix(desktop): middle-click works on a real three-button mouse
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.
2026-07-30 21:43:14 -05:00
Brooklyn Nicholson 4d6589c69c fix(desktop): stop ⌥-click spraying cursor escapes into the terminal
⌥-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.
2026-07-30 20:51:40 -05:00