Commit Graph

21297 Commits

Author SHA1 Message Date
teknium1 cf0c42fa0b fix(agent): never replay chain-of-thought in the active-turn redirect checkpoint
A /steer redirect during a thinking phase serialized the streamed
reasoning into the persisted assistant checkpoint ('Reasoning shown
before the interruption: ...'). An assistant turn exposing its own
chain-of-thought reads to Anthropic's output classifier as
reasoning-injection/prefill jailbreak, so every subsequent call on the
session deterministically returned 'Provider returned an empty
response' — and because the checkpoint is persisted and replayed, no
retry, nudge, or empty-recovery branch could ever escape it. Four
sessions were permanently bricked this way in the week of Jul 21-27
(42+ blocked calls; every reasoning-free checkpoint that week was
untouched — same mechanism as the prefill.json incident).

Class fix: streamed reasoning is now display-only state. The
_current_streamed_reasoning_text accumulator is removed entirely
(producer in _fire_reasoning_delta, resets, and init), so no future
path can serialize CoT into replayable content. The checkpoint keeps
only the visible response text; the model regenerates its reasoning on
the retried turn. Invariant documented in _apply_active_turn_redirect.

Regression tests: CoT never appears in either checkpoint shape,
reasoning-only interrupts produce a bare checkpoint, reasoning deltas
stay display-only.
2026-07-27 16:58:55 -07:00
Brooklyn Nicholson b1f4b95761 fix(desktop): measure reasoning per block instead of per turn
The timer registry hands every caller of a key the same origin, and every
reasoning block in a turn was keyed `reasoning:<messageId>`. So the second and
third blocks measured from the first one's start and each reported the running
total as its own duration — the "6s, 6s, 16s" down a single turn.

Key per block, and move the measurement into `useMeasuredDuration`, which
keeps the number beside the origin that produced it. The thread virtualizes,
so the component that watched a block finish is usually gone by the time
anyone scrolls back to read it; component state forgot the duration on
unmount and the row fell back to having none.

A block that genuinely was never watched running — history from an earlier app
session, or reasoning that arrived already complete — still has no duration to
report, and now says "Thought" rather than sitting in the present tense at a
turn that ended.

Also drops the run summary's aggregate +N/−M: a run can no longer contain a
file edit, so it was always zero. Each edit carries its own count on its card.
2026-07-27 18:50:20 -05:00
Brooklyn Nicholson f08b0e5606 Merge remote-tracking branch 'origin/main' into bb/desktop-activity-grouping 2026-07-27 18:42:49 -05:00
Brooklyn Nicholson a3c46ac09e refactor(desktop): give live tool runs a one-line ticker
A live run was the settled view under a CSS max-height: the real rows,
capped at ~6.75rem, with an escape hatch that let an open diff lift the cap
entirely. So it was neither a single line nor an honest expanded block, and a
run could balloon mid-turn.

Split the two presentations instead. Live, a run is its summary plus a
one-line reel that slides each finished action up and out, so a turn touching
thirty files reads as one line ticking over in place. Settled, the summary is
the whole of it until opened. Cards — file edits, clarify, image_generate —
leave the run entirely and stay on screen where they happened, since the diff
or the question IS the point of the turn.

Drops useToolWindow, the bounded-window threshold, the scroll container, the
fade mask and the :has([data-tool-open]) escape hatch, all of which existed to
make "the real rows, but shorter" work.

Also unifies transcript scaffolding on one colour: thinking headers painted
--ui-text-secondary and tool summaries --ui-text-tertiary under a shared
opacity, which is two greys for one kind of line. Both now render through
ScaffoldRow at a token pitched between them.
2026-07-27 18:42:46 -05:00
Alex Fournier d7b8a63eb7 Merge upstream main into feat/hermes-relay-shared-metrics 2026-07-27 16:24:46 -07:00
hermes-seaeye[bot] 96db67b849 fmt(js): npm run fix on merge (#72967)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-27 23:12:24 +00:00
brooklyn! a19cfa4531 Merge pull request #72960 from NousResearch/bb/desktop-statusbar-toggle
feat(desktop): let the status bar be hidden
2026-07-27 18:10:28 -05:00
Brooklyn Nicholson 6ba2f3e644 fix(desktop): say a sub-second reasoning block was brief
The elapsed timer counts whole seconds, so reasoning that finished
inside one rendered as "Thought for 0s" — accurate and useless, and on a
turn with several short blocks it repeats down the transcript. Drop the
number below a second and say it was brief instead.
2026-07-27 18:10:27 -05:00
Brooklyn Nicholson f96997c36f fix(desktop): settle a tool run whose calls never resolved
A run inferred "still working" from a missing result alone, so a call
left unresolved — by an interrupted turn, or an agent that moved on —
pinned its run as live forever. That stranded the summary in the present
tense ("Exploring 2 files" on a finished turn) and, because a live run
withholds its toggle so approvals can't hide, left the run permanently
expanded with no way to collapse it.

Qualify liveness the way ToolEntry already qualifies a row's: a missing
result only means pending while the run is the tail of a running
message. Liveness is now passed into summarizeToolRun rather than read
off the calls, since it isn't a property of the calls.
2026-07-27 18:10:27 -05:00
Brooklyn Nicholson f7d6c1be8f docs(desktop): explain what UNBOUNDABLE_TOOLS now guards
The list gates two behaviours since the settled-run summary landed, but
the comment still justified it only in terms of the scroll window's
height cap. Record the pending-row rule that keeps approvals visible,
and why file edits stay off the list.
2026-07-27 18:10:26 -05:00
Brooklyn Nicholson a77c75f3e0 feat(desktop): report how long the model thought
A settled reasoning block reads "Thought for 5s" instead of staying
"Thinking" forever. Nothing in the persisted turn records the duration,
so the number is frozen when the block finishes on screen and simply
omitted on a rehydrated turn, rather than reporting whatever a timer that
never ran would say.
2026-07-27 18:10:26 -05:00
Brooklyn Nicholson 7151ea4c77 feat(desktop): collapse a settled tool run to its summary line
A run of two or more tool calls now renders behind its summary once it
has finished, so a long transcript reads as what the agent did rather
than as a wall of rows. The run is keyed by its first tool call instead
of its part index: live and rehydrated turns agree on which calls belong
together but not on the indices they land at, and keying by index is what
made the previous attempt reshuffle on settle.

A run holding anything still pending always renders its rows, which is
what keeps a clarify question or an approval bar out from behind a
chevron. The approval-group tests move to tool-group and grow coverage
for both halves of that rule.
2026-07-27 18:10:26 -05:00
Brooklyn Nicholson 97d790d8b1 feat(desktop): summarize a run of tool calls as one line
Adds the grammar behind "Edited wiring.tsx, explored 3 files, ran 5
commands": one clause per category of work, a name when the category
holds a single thing and a count otherwise, and the present tense for
whichever category is still running.

The continuity test is the load-bearing part. Tool grouping was reverted
once because it reshuffled the moment a turn settled, so this replays the
same turn twice — as the gateway event stream the live view builds
bubbles from, and as the rows toChatMessages rehydrates on resume — and
asserts both produce the same runs.
2026-07-27 18:10:26 -05:00
Alex Fournier c07f2e023a Merge upstream main into feat/hermes-relay-shared-metrics 2026-07-27 16:10:24 -07:00
brooklyn! 9f02bb207d Merge pull request #72965 from NousResearch/bb/resume-freetext-search
fix(desktop): keep /resume's free-text search typeable
2026-07-27 18:09:50 -05:00
Victor Kyriazakos 9864e00fb4 feat(relay): flat-DM liveliness — status anchors to the triggering ts, replies stay flat (QA-8)
Victor's correction: flat DMs CAN have a live thinking status. setStatus
on the triggering message's ts renders '… thinking'/per-tool phrases in
that message's thread-footer space and clears without leaving a message
artifact. Native suppresses this because ITS reply routing could inherit
the activated thread; the relay lane's flat-mode sends strip their
anchors explicitly (QA-6/7), so the status anchor cannot leak into reply
placement — proven by the new leak-guard test.

send_typing/stop_typing now anchor the status in flat mode too, gated by
platforms.relay.extra.slack.flat_dm_status (default ON; false restores
the fully anchorless posture). Thread mode unchanged.
2026-07-27 23:08:11 +00:00
Victor Kyriazakos 71d5c47e21 fix(relay): per-message sessions for fronted Slack DMs — stamp the inbound ts as session thread (QA-3)
A 2nd top-level DM while a turn was in flight resolved to the SAME
session key and steered the running turn ('Redirected current run')
instead of starting its own. Native SlackAdapter stamps thread_ts =
event.thread_ts or ts on EVERY inbound, so build_session_key isolates
each top-level message; the connector normalizes top-level messages with
thread_id=null and the relay lane never reproduced the stamp.

_stamp_slack_session_thread applies native parity on the inbound bridge:
top-level Slack message + thread-per-message mode => source.thread_id =
its own ts (fresh session, parallel turns). Real thread replies and flat
mode untouched (flat keeps the shared rolling DM session on purpose).

Also introduces the enterprise config shape for relay-fronted Slack:
platforms.relay.extra.slack.<subset of native Slack fields> (nested
object wins; legacy flat extra.reply_in_thread still honoured). All
reply_in_thread reads (send/typing/stop_typing/run.py progress) now
route through one resolver.
2026-07-27 23:08:11 +00:00
Ben Barclay 981feb6730 feat(skills): org skills are editable in place; local edits survive org updates
The read-only org mirror broke the learning loop precisely where it matters
most. The system prompt tells every agent to patch a skill the moment it
finds a gap, and shared skills are the ones the most people use — but every
write to _org/ was refused, and the curator was excluded from them outright.
So org skills froze while personal skills kept improving, and the offered
alternative ("fork it into a personal skill, then propose the fork") is not
something an agent does mid-task. The refusal WAS the feature; improvements
were simply lost, and manual forks would have fragmented the shared set.

Edit in place:
- skill_manage patch/edit/write_file now work on org skills. Only delete is
  still refused (the mirror is a view of org HEAD — a local delete returns on
  the next pull; removing a shared skill is an admin action).
- Org skills are curation-eligible again, so the curator can improve the
  highest-leverage skills in the system instead of skipping them.
- The load-time provenance header now says edits are allowed and kept,
  instead of instructing the agent not to edit.

Local edits are never overwritten:
- pull_org_skills previously rmtree'd each skill dir and re-materialized it,
  silently destroying local work on the next session start. It now records a
  content fingerprint per skill (.org-baseline.json) when it writes one, and
  SKIPS any skill whose local content diverges from that baseline.
- When upstream ALSO changed such a skill, it is reported in the pull
  result's "conflicted" list and left untouched for the user to resolve
  deliberately (propose the local version, or delete it and re-pull to take
  theirs). A missing baseline is treated as unmodified so pre-existing
  mirrors do not raise phantom conflicts.
- Fingerprints are content-based (path + bytes, sorted), so a touch/mtime
  change is not mistaken for an edit.

Sharing back:
- Default: the edit stays local and the tool result tells the user to run
  "hermes skills propose <skill>".
- Opt-in sync.org_auto_propose / HERMES_SYNC_ORG_AUTO_PROPOSE submits each
  edit immediately. Defaults OFF — pushing every agent edit to a whole
  organisation is not a safe default. A failed submission never fails the
  edit; the change is saved and can be proposed later.
- "hermes sync status" lists org skills with unshared local edits;
  "hermes sync pull" reports conflicts it declined to overwrite.

Tests: 25 in the namespace suite (was 15). The two that asserted the old
read-only behaviour now assert the opposite. New coverage for edit-applied,
share-back guidance, delete-still-refused, curation-allowed, edit detection,
missing-baseline tolerance, mtime-insensitivity, and the auto-propose
default. 428 passed / 0 failed via scripts/run_tests.sh.

Verified through the REAL pull path against a mock plane: pull v1 -> edit in
place -> upstream ships v2 -> pull leaves the local edit intact, reports the
conflict, and surfaces it in status.
2026-07-28 09:08:04 +10:00
brooklyn! fbd8d1a93d Merge pull request #72963 from NousResearch/bb/artifacts-real-preview
Unify the preview rail onto one tab list
2026-07-27 18:04:18 -05:00
Brooklyn Nicholson 3c74f463d0 fix(desktop): keep /resume's free-text search typeable
/resume was classified as an options command, but its argument is a
free-text query the picker fuzzy-matches against session titles and
previews. Its completion list also always ends in a "Browse all
sessions…" action row, so Space-to-accept never fell through to an
empty list: the first space in a multi-word query emptied the composer
and threw the user into the overlay, query and all.

Classify it as mixed so spaces type through, matches keep narrowing as
you refine, and Tab or arrow-then-Enter still accept a highlighted
session.
2026-07-27 18:03:07 -05:00
Brooklyn Nicholson f4e042f466 style(desktop): sit every pane tab strip on the sidebar surface
The strips painted with `--theme-card-seed` — the raw, unmixed seed rather
than a surface token — so they read as their own band beside the sidebar and
titlebar, which share `--ui-bg-sidebar`.

Fixed at the default in `PaneTab` rather than per strip. The right-rail
preview strip never set the vars at all, so its inactive tabs fell through
to the seed even though its container was already on the sidebar surface;
correcting the fallback fixes that one for free and keeps the next strip
from regressing. With the default right, the two zone strips no longer need
to redeclare the var and paint the token directly.
2026-07-27 18:02:20 -05:00
brooklyn! 7de4fbd493 Merge pull request #72956 from NousResearch/bb/slash-freetext-enter
fix(desktop): don't let Enter swap a free-text slash argument for a completion
2026-07-27 17:56:45 -05:00
Brooklyn Nicholson cd25a9f405 fix(desktop): open a preview without dragging the file tree open
The preview pane shares a collapsible column with the file tree, and
`revealTreePane` un-collapses a column through that column's bound store —
which on the right is `$fileBrowserOpen`, the tree's own ⌘J toggle. So
every preview open literally called `setFileBrowserOpen(true)` and the tree
came with it.

`revealPreview` now un-collapses the column directly and leaves the toggle
alone. The tree pane's visibility binding gains `$fileBrowserOpen` to match,
since its presence was tracking only the column's collapse — without that it
would still render the moment anything opened the column.
2026-07-27 17:55:46 -05:00
Brooklyn Nicholson 003ff53fb4 fix(desktop): scope composer and transcript state to their own session
`$activeSessionId` only ever holds the primary chat's session, but surfaces
that render once per transcript were reading it as if it meant "the session
on screen." A preview produced inside a session tile was recorded under the
main chat's key and surfaced in the main chat's composer, which is what
prompted this.

The tool row now records under its own `SessionView`, and the same fix
applies to the other readers of that atom that render per surface:
attachment pills and inline preview links resolve relative paths against
their session's cwd, composer voice and auto-speak read and subscribe to
their own transcript, and the thread's compaction label, prompt-wait gate
and turn timer follow the session that mounted them. `ComposerScope` now
carries a `$messages` atom rather than a read closure so both the
imperative read and the subscription come from one place.
2026-07-27 17:55:46 -05:00
Brooklyn Nicholson 96999b116b refactor(desktop): put every preview on one rail tab list
The right rail held two things at once: a list of file tabs, and a
privileged "live preview" slot with a hardcoded `preview` tab id backed by
a separate session-keyed registry. The two were written under different
session-id rules and reconciled against each other, so an `open_preview`
from a session whose stored id hadn't landed yet was set and then
immediately cleared — the pane flashed and vanished. Artifacts arrived as
a third list with their own pane and renderers.

Now everything the rail can show is a `PreviewTarget` in `$previewTabs`,
and `openPreview` is the only way in. `$previewTarget` is a computed read
of the active tab, the session registry and its reconciler are gone, and
artifacts render in the real preview pane through the shared mode switcher
and source view instead of a parallel one. Artifact tabs stay memory-only
since the registry rebuilds from the transcript.
2026-07-27 17:55:27 -05:00
Brooklyn Nicholson 43571601aa fix(desktop): don't let Enter swap a free-text slash argument for a completion
`/goal` keeps its completion popover open across arbitrary prose so its
subcommands stay reachable. The popover highlights its first row on open, and
Enter accepted that highlight unconditionally — so pressing Enter to send
`/goal ship the redesign` would replace the sentence with a row the user never
chose. Space was already guarded; Enter and Tab were not.

Enter now accepts only after the user has arrowed to a row deliberately, so
the highlight never lies about what Enter will do. Tab stays an unconditional
accept, since it means nothing else in the composer.

This is latent rather than reproducible today: `/goal` is absent from
`SUBCOMMANDS` (its `args_hint` pipes are spaced, so the extraction regex
misses them), so the backend returns no arg completions and the branch never
runs. Giving `/goal` the subcommands it already advertises would resurrect
the #71963 symptom in a worse form — losing the prose instead of chipping it.
2026-07-27 17:50:18 -05:00
Brooklyn Nicholson e3acdfb21d refactor(desktop): lift the completion-accept decision out of the keydown ladder
Which keys accept the highlighted completion was an inline condition in the
composer's keydown god-function, untestable without a DOM harness. Move it to
a pure helper beside the other slash-query utilities.
2026-07-27 17:50:12 -05:00
Brooklyn Nicholson 58c8d86bd5 feat(desktop): let the status bar be hidden
The bar is always-on chrome today. Hiding it is `⌘⇧S`, the ⌘K palette, or
the bottom row of its own right-click menu — VS Code's set of doors, minus
their unbound default (they ship `toggleStatusbarVisibility` with no
keybinding and Hermes has no chord dispatcher for a `⌘K ⌘S` two-stroke).

Hidden unmounts the bar rather than hiding it, so the 15s status poll and
the per-turn readouts stop with it. Visibility persists per window profile
and defaults on.
2026-07-27 17:47:45 -05:00
Gille b429194478 refactor(desktop): simplify free-text slash mode check (#72815) 2026-07-27 17:45:03 -05:00
Ben Barclay e19ac3b745 docs(sync): point users from personal sync to org sharing
`hermes sync` gave no hint that org sharing exists, so there was no path
from 'I want to share this with my team' to `hermes skills propose` — sync
looked like the only sharing surface while being personal-only (it always
CAS-es refs/user/<owner>/HEAD).

- Bare `hermes sync` usage and the `--help` epilog now state that these
  commands are personal-only and name `hermes skills propose <skill>` as
  the org path, noting the approval step and that org skills arrive
  automatically and are read-only locally.
- Scrubbed the remaining internal jargon from the sync module docstring
  (M1-D, DEV-PHASE, HSP/1) missed by the earlier pass, which only covered
  help= strings.

Tests: 2 new guards asserting both surfaces reference the org command.
373 passed / 0 failed via scripts/run_tests.sh. Both outputs verified by
running the actual commands.
2026-07-28 08:19:56 +10:00
Alex Fournier 5efeb73b9f Merge upstream main into feat/hermes-relay-shared-metrics 2026-07-27 15:13:47 -07:00
brooklyn! 7c532e1006 Merge pull request #72889 from NousResearch/bb/composer-at-paths
Fix `@` path navigation, folder completion, and chip baseline in the composer
2026-07-27 17:13:00 -05:00
brooklyn! 42c308ecdc Merge pull request #72897 from NousResearch/bb/desktop-drift-fixes
Desktop: fix diff color drift, replayed notifications, stall timing, and quit-on-active-work
2026-07-27 17:12:49 -05:00
Alex Fournier 3666e3417e Merge upstream main into feat/hermes-relay-shared-metrics 2026-07-27 15:12:47 -07:00
brooklyn! 14cb0507a7 Merge pull request #72912 from NousResearch/bb/desktop-project-count
Drop the leftover session counts inside an entered project
2026-07-27 17:12:39 -05:00
Alex Fournier 70d8db7409 Merge upstream main into feat/hermes-relay-shared-metrics 2026-07-27 15:08:25 -07:00
Gille dbc18c6d62 fix(desktop): preserve live model after settings save (#72903) 2026-07-27 17:52:37 -04:00
Teknium 731aa0ccc9 fix(browser): stop stale cdp_url from stalling every startup by 10+ seconds
Tool-schema assembly at CLI/Desktop startup runs the browser-family
check_fns (browser, browser_cdp, browser_dialog, browser_vision). Each
of those gates called _get_cdp_override(), which resolves the configured
endpoint over HTTP (GET /json/version, timeout=10) — so a *stale*
browser.cdp_url pointing at a dead debug browser cost ~7 serial blocking
socket connects before the banner rendered. Measured on a real Windows
install with a dead http://[::1]:9222 config: 15.1s of an 18s launch,
with no warning or error — just mystery slowness. The value is easy to
leave behind: /browser connect writes a session-scoped env override, but
'hermes config set browser.cdp_url' persists forever while the debug
Chrome it pointed at dies on the next browser restart.

Split the helper:

- _get_cdp_override_raw() — returns the configured value (env var or
  config.yaml) with zero network I/O. Used by every is-it-configured
  gate: check_browser_requirements, _browser_cdp_check, _is_local_mode,
  _is_local_backend, _navigation_session_key, _should_inject_engine
  (via _is_local_mode), and the hermes doctor chromium-skip check.
- _get_cdp_override() — unchanged contract (raw + /json/version
  resolution), now only called on paths that are about to connect:
  session creation and the dialog-supervisor attach.

This follows the existing rule in check_browser_requirements ('do not
execute agent-browser --version here') and the browser.manage status
path, which already banned _get_cdp_override for exactly this reason
(test_browser_manage_status_does_not_call_get_cdp_override): schema
assembly must not perform blocking I/O.

A/B on the same machine, same dead endpoint: get_tool_definitions()
15.08s unpatched -> 1.89s patched, with browser_cdp/browser_dialog
still advertised (gate now keys off configuration, not reachability —
matching the documented lazy-supervisor contract in
_browser_dialog_check).

Adds a regression test asserting the browser_cdp check_fn never touches
the network.
2026-07-27 14:32:05 -07:00
brooklyn! c63be0daf7 Merge pull request #72900 from NousResearch/bb/desktop-home-project
Add a Home project at the top of the desktop sidebar
2026-07-27 16:29:57 -05:00
Brooklyn Nicholson e04ed64637 style(desktop): drop the session counts inside an entered project
The sidebar's flat session list lost its `x/<total>` chip in #72336, but
the project drill-in kept counting: WorkspaceHeader still rendered a
SidebarCount for every repo and every branch/worktree lane. Entering a
project put a number next to each label again.

Remove the header's count slot and both call sites, along with the now
dead repo total.
2026-07-27 16:25:51 -05:00
Brooklyn Nicholson e00901259c feat(desktop): Tab into a folder from the @ popover
Tab and Enter shared one branch, so picking a folder always committed a
chip and closed the menu — the list could show `apps/` but never open it.
Reaching a nested path meant typing every segment by hand.

Split the two intents. Tab re-types the token as a bare path so the next
completion lists that folder's children; Enter still commits the folder
itself as a chip. Files ignore the distinction — there's nowhere deeper
to go. Backspace mirrors the descent, dropping one path segment per press
instead of one character, so climbing out costs the same as going in.
2026-07-27 16:20:58 -05:00
Brooklyn Nicholson 579b66336f fix(desktop): let automated teardown quit past the active-work prompt
Playwright closes the app with a turn still in flight, so the new quit
confirmation waited on a click nobody was there to make and the E2E
worker died on a 90s teardown timeout.
2026-07-27 16:19:27 -05:00
hermes-seaeye[bot] a88e27e9e5 fmt(js): npm run fix on merge (#72902)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-27 21:15:26 +00:00
brooklyn! 1d76a15188 Merge pull request #72899 from NousResearch/bb/desktop-recede-chrome-seams
Let the desktop chrome recede, and mark the active tab instead
2026-07-27 16:07:08 -05:00
Brooklyn Nicholson 5a5d7b9386 feat(desktop): pin Home to the top of the project sidebar
Home leads the overview above the active project and outside any drag
order, drills in to a flat chat list (it has no repo or worktree
structure), and overlays live sessions so a brand-new detached chat
appears instantly. Starting a chat from inside Home stays detached
instead of picking up the configured default project dir. Rename and
delete are hidden — there's no record behind the row.
2026-07-27 16:05:19 -05:00
Brooklyn Nicholson ef0f4763e3 i18n(desktop): name the project-less bucket "Home"
The sidebar row is a place you enter, not a status line, so it reads as a
destination rather than "No project".
2026-07-27 16:05:10 -05:00
Brooklyn Nicholson 60b6ea237f feat(gateway): group unplaced sessions into a Home bucket in the project tree
Sessions with no cwd — or whose folder can't be promoted to a project (the
bare home dir, a deleted workspace, HERMES state) — were dropped from the
project tree entirely, so the grouped sidebar silently showed fewer chats
than flat Recents. Collect them into a synthetic `__no_project__` node at
the head of the list. It carries one lane purely to hold the rows, and is
omitted when empty so a project-less install stays blank.
2026-07-27 16:04:57 -05:00
Brooklyn Nicholson 2a7da2b549 style(desktop): fade split sashes until hover
The seam hairline sat at full strength on every split, so an empty
workspace read as a wireframe. Hold it at 0.1 and bring it up with the
grab band already on hover.
2026-07-27 15:58:34 -05:00
Brooklyn Nicholson 35a002a441 feat(desktop): mark the active pane tab with a primary underline
The active tab was defined by absence: the strip painted a rule and the
tab covered it, so inactive tabs stopped a pixel short to let it show.
Draw the state instead. The tab carries its own 2px --theme-primary
underline and the strip's rule goes away, which lets tabs run full height
and removes PANE_TAB_STRIP_LINE along with it.
2026-07-27 15:58:22 -05:00
Brooklyn Nicholson d8b5bbf607 style(desktop): drop the titlebar and statusbar edge rules
The window chrome bracketed the workspace with a 1px rule top and bottom.
Both bars already paint the sidebar surface, so the rules divided one
continuous color rather than separating two.
2026-07-27 15:58:12 -05:00