Commit Graph

4183 Commits

Author SHA1 Message Date
Teknium b51c055a12 fix(desktop): harden window-open deny and cover the link-title window
Follow-ups on the cherry-picked handler:
- a throwing observer can no longer change the decision; the handler returns
  an explicit deny regardless of logging failures
- the denied-URL log carries origin only, so query tokens / signed URLs from
  attacker-controlled content never reach the persisted desktop log
- the hidden link-title window (loads arbitrary user-linked pages on render,
  had no window-open handler at all) now denies too
- tests trimmed to two invariants (proven red against the pre-fix shape)
2026-09-04 23:19:11 -07:00
teknium1 77ca6a6d12 fix(desktop): deny window-open side-effect opens (GHSA-9f4c-93c8-jc8g)
setWindowOpenHandler opened details.url as a side effect before denying.
Per GHSA-9f4c-93c8-jc8g (CVE-2026-70608, High 7.2), a sandboxed iframe
with no allow-popups and no user gesture can reach this handler via the
OpenURL path -- and the desktop renders untrusted artifact HTML in
<iframe sandbox="allow-scripts">. A malicious artifact could therefore
force the OS browser to an attacker URL with zero interaction. Electron
ships no fixed 40.x release (fix is 41.10.3+/42.0.1), so we close it at
the seam, version-independently.

- electron/window-open-policy.ts: pure decideWindowOpen (always deny) +
  createWindowOpenHandler(onDenied) that denies and never opens a URL;
  the hook is logging-only.
- main.ts: wireCommonWindowHandlers uses it (covers primary + all
  secondary/quick windows); the deny is logged, no side-effect open.
- Trusted external links are unaffected: they already route through the
  audited hermes:openExternal IPC channel (openExternalUrl, http/https/
  mailto allowlist). Converted the one remaining bare window.open on the
  Electron path (env-var docs menu) to openExternalLink; other
  window.open sites are bridge-absent web fallbacks.
- tests-js/window-open-policy.test.ts: 4 tests pinning always-deny, the
  logging-only hook, and that a throwing hook never degrades to allow.
2026-09-04 23:19:11 -07:00
hermes-seaeye[bot] 71f8c60f6a fmt(js): npm run fix on merge (#103240)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-04 22:20:13 +00:00
Hao Zhe 64534b4dd4 fix(desktop): use shared tooltip for listing gallery 2026-09-04 15:14:49 -07:00
Brooklyn Nicholson b0ab2e163a test(desktop): pin that a multi-tab zone can still hide its strip
The resolver's own suite only ever asserted `never` against a LONE pane, so
the whole-stack stranding check passed every existing case while Hide tabs was
dead in the one zone users keep tabs in. Adds the missing count: an explicit
`never` is honored once a closeable tile has a neighbor to cycle to, and those
same stacks still paint their strip on auto (the rung resolves before `mode`,
so a stack it stops claiming has to reach the auto rule).

Verified as a tripwire — reverting the resolver fails the first case.
2026-09-04 15:01:10 -05:00
Brooklyn Nicholson 2c52945974 fix(desktop): Hide tabs works again in a zone holding more than one tab
The reachability rung that outranks an explicit `never` ran over the whole
stack: any closeable `placement: 'main'` pane pinned the strip visible at any
tab count. Session tabs and bot chats are exactly that, so one session tab in
main was enough to make Hide tabs a no-op — the zone menu row and ⌘⌥T both
wrote `tabStrip: 'never'` and the resolver overrode it on the next line.

Stranding is about the last handle, not about tabs existing. A stack of two or
more answers tab cycling and ⌘1…⌘9, so hiding its strip costs chrome and no
handle. The tile and tool-panel rungs now apply to a lone pane; hide-only
chrome (sessions / Bots) keeps firing at every count, since it has no close
verb and its Show/Hide rows live on the strip itself.
2026-09-04 15:01:10 -05:00
Brooklyn Nicholson cb6cb74696 Keep renderer-owned fences out of artifact promotion
Artifact detection runs before the rich-fence registry, so a fence long
enough to look like a code document was promoted to a code card and never
reached its own renderer. A multi-listing set crosses that threshold
easily, which made the same content render two different ways depending on
how many properties it held.

`listing` joins the languages that are never artifacts, next to `mermaid`
for the same reason.
2026-09-04 15:00:43 -05:00
Brooklyn Nicholson 9b16719e17 Render property listings as browsable transcript cards
Adds a `listing` fence to the rich-embed registry: JSON in, a native card
out — photo mosaic, address and price, specs, and the catches worth
verifying before a tour.

The rental portals publish no embeddable surface (public APIs deprecated or
broker-gated, no oEmbed, X-Frame-Options on every listing page), so a URL
cannot be framed. The data the agent already gathered can be, and the
portals' image CDNs hotlink fine — so the card is authored rather than
fetched, alongside the existing mermaid and svg fences.

Parsing treats every field as untrusted model output: one object, a bare
array, or `{listings: []}` all work, beds accept `3` and `"3.5"`, non-http
URLs are rejected so a card cannot smuggle a javascript: href, and lists
are capped. Anything unparseable falls back to the plain code block.
2026-09-04 15:00:43 -05:00
abundantbeing 6b1e12c7f4 feat(desktop): drag to create sessions from New session, project + controls, and profile groups 2026-09-04 14:57:37 -05:00
Brooklyn Nicholson 96e1e3f921 feat(desktop): Message Bubble transparency slider in Appearance
One 0-100 lever beside Window Translucency: 0 keeps your bubble solid (the
default), 100 leaves only the outline. The store paints a single root var,
--user-bubble-keep, that --dt-user-bubble mixes in, so the read-only bubble
and the inline edit composer follow together and an untouched lever yields
byte-identical CSS. Reuses TranslucencySlider (with its overlay peek), has a
Cmd-K search entry, and copy in all six locales.
2026-09-04 14:40:54 -05:00
Brooklyn Nicholson 59302ece0d fix(desktop): transcript timestamps were painted at 30% ink in both modes
TimelineTimestamp layered text-muted-foreground/55 on a token that is already
a 54% mix of the base ink, so the stamp rendered near 30% alpha: 2.3:1 on a
dark chrome, 1.9:1 on a light one. Point it at --conversation-scaffold-meta,
the token the tool-row stamps already use, so every stamp in the transcript
reads from one place.
2026-09-04 14:40:54 -05:00
hermes-seaeye[bot] 59d330fd15 fmt(js): npm run fix on merge (#103110)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-04 18:28:32 +00:00
Brooklyn Nicholson 1d1411df9c fix(desktop): keep the saved skin name until the registry can resolve it
normalizeSkin ran at boot, before the gateway seeded backend skins, and flattened an unresolvable name to the default for the whole session. The connect-time seed is apply: false by design, so nothing repainted. Keep the raw pick in provider state and normalize against the live registry instead.
2026-09-04 13:19:19 -05:00
Brooklyn Nicholson 413d5ab863 fix(desktop): cache backend skins so the boot paint can resolve the saved one
$backendThemes only lived in memory, so a relaunch on a skin from ~/.hermes/skins painted the default until the next skin.changed. Hydrate the registry from localStorage like user themes, and write it back on every change.
2026-09-04 13:19:19 -05:00
Brooklyn Nicholson 9403ad2656 refactor(desktop): share the theme shape check between registries 2026-09-04 13:19:19 -05:00
Brooklyn Nicholson 433f719676 fix(desktop): tab ✕ fade lands on the visible field under Glass
Under Window Translucency → Glass, the glass block sets the surface
tokens transparent so <body> is the sole painter. A tab's `--tab-face`
read those tokens, so the ✕ runway resolved to transparent→transparent
and the chip's `bg-(--tab-face)` painted nothing: hovering showed a bare
✕ over un-faded text, with no gradient at all.

Add a `data-glass-field` contract beside the block's existing
`data-glass-opaque` / `data-glass-raised`: a declarer receives
`--glass-field`, the same body mix glass already paints. The tab prefers
it and falls back to its own surface token (`--tab-surface`, which
TAB_ACTIVE / TAB_IDLE now set instead of `--tab-bg` so they feed the
chain rather than bypass it). Face, hover-darken, and the selection wash
all inherit the one base. Inert with glass off: the fallback resolves to
the exact opaque surface it did before.
2026-09-04 13:09:07 -05:00
Brooklyn Nicholson ff75ff2a18 fix(desktop): closeable tab labels clip so the ✕ fade has text to dissolve
The label span used `truncate`, whose ellipsis stops the text ~8px short
of the overlay. The hover gradient then faded over empty tab surface —
surface to the same surface, invisible by construction — and the ✕ read
as floating in a gap beside three dots.

A closeable tab publishes `data-closeable`; the label reads it via
`group-data-[closeable]/tab:text-clip` so overflow runs under the fade
and the gradient dissolves real letterforms. Vertical rails and
uncloseable tabs have no ✕, so they keep the ellipsis.
2026-09-04 13:09:07 -05:00
Brooklyn Nicholson 87e78d668b fix(desktop): pane tab ✕ costs a min-width floor, not 36px of dead padding
#96880 reserved `pr-9` on every closeable tab so the hover ✕ could never
sit over a label — 36px of empty runway at rest on every tab, nearly
doubling FILES (40→76px) and taxing tabs whose label already cleared the
overlay by a mile.

Restore the overlay as designed (✕ paints over the label's right edge,
gradient-blended, zero layout shift) and protect short labels with a
`min-w-13` floor instead. Sized from the chrome it has to survive: 8px
label inset + the ~19px opaque chip, so the shortest labels clear the
chip and pass only under the translucent fade. A floor bills only tabs
that need it; TERMINAL, BROWSER, NEW CHAT pay nothing.

The test that pinned `pr-9` by class string is rewritten as a set-diff
contract: adding `onClose` must add a `min-w-*` and never a `pr-*`.
2026-09-04 13:09:07 -05:00
Brooklyn Nicholson d353931dd9 test(desktop): assert tooltip labels stay in inline flow, not inline-flex
The preview-row and terminal-rail tests pinned the label's `inline-flex`
class — the very shape that broke multi-line tips. Assert the invariant
instead: no flex box under the decoration, and the preview row splits its
two lines with a hard break.
2026-09-04 12:51:08 -05:00
Brooklyn Nicholson a22b295dbd fix(desktop): paint every line of a multi-line tooltip, not just the first
Tip's chip is an inline span with `box-decoration-break: clone`, so its
background only paints the line boxes of inline flow. The #62022 hardening
forced every label child to `inline-flex`, which is an atomic inline: it sits
on the baseline and hangs any extra lines below the chip, dark-on-dark. The
preview status row's two-line label (target path + click hint) showed the
path lit and the hint invisible, and long paths wrapped into unreadable rows.

Force label children `inline` instead — still no block collapse, and wrapped
content stays in flow. TipHintLabel and the preview row drop their flex
boxes for plain inline text (a `<br />` for the hard break).
2026-09-04 12:51:08 -05:00
Teknium 2c6c645803 style(desktop): eslint/prettier pass on the plugin-compat notice (sort-imports, curly) 2026-09-04 02:45:43 -07:00
Teknium 4441a2a28d docs(agents): split AGENTS.md into root + per-area files (≤8k each, the subdirectory-hint cap)
Root AGENTS.md 100,797 → 29,295 chars: what applies everywhere (invariants, rubric, footprint ladder, layout + shape rules, commit/PR, testing) plus a routing table. Area rules move to agent/, hermes_cli/, gateway/, tools/, plugins/, tui_gateway/, web/, skills/, cron/, apps/desktop/src/ AGENTS.md (3–9k each; ceiling is now 32k after d61cff60e3, target ~8k). Long-form process-identity and skin key tables go to website/docs/developer-guide/cli-internals.md. Zero rule loss; map in /tmp/rf/agents_md_zero_loss.md. Stale Bot Mode test paths corrected to apps/desktop/src/plugins/hermes-bots/*.test.ts.
2026-09-04 02:12:35 -07:00
Teknium 0a5164cebe compat(plugins): tell users which installed plugins break on 2026-09-14, and stop loading them after
hermes_cli/plugin_compat.py is now the single source of truth for the compat window:
  COMPAT_REMOVAL_DATE = 2026-09-14; scan_plugin() statically finds `from F import n`, `import F` + `F.n`,
  alias forms and string targets against compat_manifest.json; compat_report() aggregates over the user's
  ENABLED external (non-bundled) plugins; disable_reason() decides the loader's skip.

Surfaces (all read from that one report):
  * CLI: yellow block under the banner naming plugins + date + `hermes plugins compat` (red + DISABLED after)
  * `hermes plugins compat [--json] [path]`: file:line, old -> new per hit; exit 1 while anything remains;
    `path` lets a plugin author scan their own checkout
  * `hermes doctor`: "Plugin import paths (removed Sep 14, 2026)" section next to the xAI retirement check
  * `hermes update`: post-update notice alongside the FTS/curator notices
  * Desktop: compat_report() writes HERMES_HOME/.plugin-compat-report.json (deleted when clean); Electron
    shows ONE warning dialog per distinct report after the backend is up and persists the dismissal in
    userData/plugin-compat-dismissed.json. A new affected plugin, or the date passing, is a new report.

From the date, PluginManager skips a hitting external plugin before importing it, with the reason in
LoadedPlugin.error ("uses N import path(s) removed on 2026-09-14; run `hermes plugins compat` ...") — the
same path a plugin with a broken register() takes, so nothing else is affected. Escape hatch:
plugins.allow_deprecated_imports: true (config_defaults), which only helps until the compat commit is
actually reverted.

Docs: COMPAT_MANIFEST.md (removal date, what-happens table, author instructions), plugin dev guide section.
Tests: tests/test_plugin_compat_notice.py (scanner forms, report scope, date gate + escape hatch, summary
text, report file lifecycle, loader skip via a real PluginManager), electron/plugin-compat-notice.test.ts
(show once, re-show on a different set or on the date passing, malformed file ignored).

Live A/B on this box with a demo plugin on old paths: before the date it loads and the banner/doctor/report
name it; with today=2026-09-14 it is skipped with the reason and the banner turns red; with the escape
hatch it loads again.
2026-09-04 01:28:31 -07:00
Teknium f43b976209 fix(ci-fallout): repoint the in-container docker test, two windows-only tests and the desktop exe-integrity patches to the defining modules; name bot_relay turn timeout/attempts as constants so the Desktop mirror test reads them instead of counting subprocess.run sites 2026-09-03 17:28:02 -07:00
liuhao1024 05f548f35d fix(desktop): declare rememberLog state before the top-level pool-limits read
readPersistedPoolLimits() runs at module evaluation and logs through
rememberLog() on every branch, but hermesLog / desktopLogBuffer /
desktopLogFlushTimer / desktopLogFlushPromise were declared ~110 lines
later. esbuild lowers const/let to var, so the packaged desktop died on
every launch with "Cannot read properties of undefined (reading 'push')"
(#101941, #101960). Moving the four declarations above the read fixes the
crash and keeps the early [pool-limits] line in desktop.log.

Salvaged from #101945 (test dropped: Desktop E2E lane is disabled in CI).
2026-09-03 01:14:37 -07:00
cmyyy 3ea71a47b3 fix(desktop): refresh Bot Chat transcript when a roster click fronts an already-open tab
A roster click on a bot whose canonical Bot Chat is already open only
fronted the tile: the pane kept whatever transcript it last painted,
which can predate rows the bot wrote while the user was elsewhere (a
cron delivery, a teammate's message_agent, another bot's turn). The
stale snapshot persisted until the next user turn — #95600's forceResume
only covered the not-yet-open registry path.

Reuse refreshOpenBotChat (the #99393 reclaim mechanism) on the fronted
branch so forceResume re-pulls the latest transcript. Regression test
pins the behavior: fronting an open Bot Chat now requests the canonical
registry open.
2026-09-03 01:10:16 -07:00
Edder Talmor 37fd6eea97 fix(desktop): toast action is a real button, not a hairline text link
The notification action (`NotificationItem`) rendered as
`variant="textStrong" size="xs"` — an 11px underlined muted-grey text link
with a ~44x20px hit target. On the data-training confirm toast raised by
`surfaceModelSwitchConfirm` / `confirmModelWarning` (e.g. picking
`muse-spark-1.2-contributor`) it read as a footnote, not the one action
the toast exists for, and users reported not being able to "press to
accept".

Promote it to the SDK's `default` variant at `size="sm"`: a filled
primary button, larger hit target, obvious affordance. No new styles.

Salvaged from #96562 (toast half only). Refs #96563.
2026-09-03 00:58:46 -07:00
Teknium b6041240d1 test(desktop): trim Bot Chat pane-focus regression to the two invariant cases
Salvage follow-up to #101639 (@helix4u): keep the re-adopt-after-overlay and
miss-propagation cases, drop the hidden-pane and healthy-path controls.
2026-09-03 00:36:09 -07:00
Gille b6d549d002 fix(desktop): restore missing Bot Chat panes before claiming focus 2026-09-03 00:36:09 -07:00
hermes-seaeye[bot] e629c900a8 fmt(js): npm run fix on merge (#101952)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-03 07:24:01 +00:00
Teknium bd6cc48b94 fix(desktop): annotate the resolvable target for aliased LOCAL rows too
The contributor fix covers remote rows. The reporter's video shows the
sibling shape: with a remote gateway active, the LOCAL twin carries the
'default-this-device' alias, and message_agent's local resolver only
knows bare profile names / 'hermes'. Emit the same target annotation
whenever a local row's alias differs from its resolvable handle.
2026-09-03 00:18:31 -07:00
liuhao1024 2e542c92e6 fix(desktop): annotate canonical relay targets for remote @mentions
The Bot Mode mention middleware built message_agent targets from
botHandle(), which prefers a roster row's source-qualified UI alias
("default-vera"). Neither resolver accepts that form — the relay matches
canonical handle/profile (± @connection-id) and the local path a bare
profile name or "hermes" — so remote handoffs died with "No teammate
named" before enqueue.

Annotate the canonical form instead: profile@connection-id for remote
rows, canonical bare handle (default→hermes) for local ones. Pin the
profile@connection form on the relay side too, so the emitted target
stays inside the documented resolver contract (#97678).
2026-09-03 00:18:31 -07:00
Finn763 e245e40f73 fix(desktop): warm session switch pegs renderer main thread (#95595)
Switching to an already-open session remounted the incoming transcript and
re-tokenized every fenced code block from scratch on the main thread (N blocks
x full shiki tokenization per switch, 96-100% CPU for seconds).

- shiki-block: content-keyed LRU cache of highlighted HTML (theme scope +
  language + code); remounts of unchanged blocks paint cached markup with
  zero highlighter calls; misses debounced, failures degrade to plain text
- use-session-actions: warm resume keeps the session-slice array when the
  reconciled content is equivalent (same guard as the cold path), so the
  runtime repository and every row keep identity
- transcript-window: per-session sticky window memos; a warm re-visit with an
  unchanged transcript reuses the windowed slice by reference (no re-index,
  no repository rebuild); sticky cut survives switches for sessions that grew
- perf regression guards: remount must not re-tokenize (codeToHtml called
  once per unique block), windowed slice reference preserved across switches,
  messageComponents identity stable across session switches
2026-09-03 12:19:11 +05:30
kshitijk4poor f260ea5347 fix(desktop): let the spawn coordinator follow the live pool max
Composition of #92581 on #100985: the hard cap is a constructor constant,
so raising the pool max in Settings would have left new spawns queued
behind the launch-time value. Add setLimit(); slot hand-off now goes
through a single #drain that respects the current cap, which also fixes
the original release path handing a slot to the next waiter even when the
cap had just been lowered (test: lowering never revokes granted slots; new
requests queue until under cap). main.ts constructs from
poolLimits.maxBackends and pushes changes from setPoolLimits(); pinned by
a wiring test.
2026-09-03 12:19:05 +05:30
ClintonEmok c401756a6a fix(desktop): pool sizing as a live device preference in Settings (#91545)
Hover-intent prewarm sweeps across the Bots rail spawned past the pool cap,
LRU-evicting the backend the user was about to click — an evict/respawn
cascade that made profile switching progressively slower (#91545).

- prewarmProfileBackend skips speculative spawns once every pool slot holds
  an open socket; the real click still spawns on demand.
- Pool max/idle become a device preference (Settings -> Advanced), persisted
  atomically in userData (pool-limits.json) and applied live over IPC; the
  HERMES_DESKTOP_POOL_* env vars remain the initial fallback. Defaults are
  unchanged (3 backends / 10 min idle).

Squash of the 3-commit PR #92581 branch (a00dc088c5..783899d12f) applied
via diff onto the spawn-coordinator salvage; import + constant-block
conflicts resolved so the coordinator is constructed from, and follows,
the live preference (setLimit added in the next commit).
2026-09-03 12:19:05 +05:30
kshitijk4poor 5810172f52 fix(desktop): bound the pool-slot wait below the renderer boot budget
Follow-up to the spawn coordinator: the queued ticket waited up to
POOL_IDLE_MS (10 min) for a free local slot, but the renderer gives up on
a backend boot after 45 s. A user clicking a 4th profile with 3 fresh
backends open would see the generic "backend didn't come up" error while
the ticket kept the pool key hostage, so every later click joined the same
stale wait. Cap the wait at 30 s, log the slot pressure when it happens, and
pin the relationship to BACKEND_BOOT_WAIT_TIMEOUT_MS with a wiring test
(fails when the timeout is reverted). Also eslint --fix on the salvaged
files (import order was a lint error).
2026-09-03 12:19:05 +05:30
Kryptonator e924615bb1 fix(desktop): cap concurrent local profile backend spawns
Desktop could spawn a local hermes serve per profile with no hard cap on
starting+running children: LRU eviction spares keepalive-fresh entries, so
a roster refresh across many profiles became a process wave (40+ backends,
load 30-50 reported).

LocalBackendSpawnCoordinator: at most POOL_MAX_BACKENDS local backends may
be starting or running. Remote descriptors never take a slot. Queue tickets
are per request; a slot is released only after process exit is proven
(exitCode/signalCode). A rejected wait keeps the slot occupied. Pool entries
re-assert ownership at each await so an evicted entry cannot spawn a zombie.

Squash of PR #100985 (6017abbbc4 + merge), applied via diff onto current
main. Original commits were authored as 'Motor (Hermes AI) <ceo@xtremagency.com>';
attributed here to the PR author's GitHub identity.
2026-09-03 12:19:05 +05:30
cez0060405 bb319d0d78 fix(desktop): bound orphan-reap sweep so slow probes cannot stall boot
The ownership file accumulates one record per profile per launch, and each
record can cost up to two identity probes (parent + backend) plus a stop.
On Windows those shell out to PowerShell, whose 5.1 cold starts are slow.
Without a bound, a large roster could stall boot for minutes while the
renderer's 45s backend-boot budget expires and the user stares at the
connecting screen.

- Add REAP_PROBE_TIMEOUT_MS (5s) for the orphan-reap path; the claim path
  keeps the full 30s headroom for a freshly spawned backend's marker.
- Add reapDeadlineMs (5s default) as an overall budget for one reap sweep;
  when exhausted, unprocessed records are preserved for the next launch.
- stopOwnedBackend now throws when the identity probe fails (not confirmed
  gone) so the record is preserved instead of leaking the backend.

Verified: two cold starts complete in ~29s (was 5+ min); renderer connects
immediately after backend ready.
2026-09-03 12:17:54 +05:30
hermes-seaeye[bot] 97f3229dfd fmt(js): npm run fix on merge (#101836)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-03 03:17:48 +00:00
ethernet 75aaf56904 enable local models by default on win32 and darwin 2026-09-02 22:40:22 -04:00
hermes-seaeye[bot] 4e66da6eeb fmt(js): npm run fix on merge (#101497)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-02 17:45:59 +00:00
Brooklyn Nicholson 6ef6691960 fix(desktop): a saved comment crop shows its own marker, and only its own
Two faults in the crop that "Add N comments" attaches, both visible in a
two-comment batch on one page.

The marker was missing. showDraft sets the marker's style and resolves, but
resolving only means the property is set — the compositor has not drawn it.
capturePage then photographed the frame before the marker existed, so crops
arrived outlined in blue with no number, while the prompt line said "Image N
marks the target in blue". beginCapture now waits two animation frames, which
puts the shot after the paint.

The wrong marker could appear. Saved pins stay drawn on the page, so any pin
within the crop padding of the new element landed inside the shot: a comment
on a heading came back carrying the marker belonging to the comment on the
paragraph below it, pointing the agent at the wrong element. Saved pins and
hover chrome are hidden for the duration of the shot and restored after.

Restoring runs in a finally, so a capture that throws cannot leave every saved
pin invisible on the page, and the guest calls are best-effort — a torn-down
overlay degrades to the old unbracketed shot rather than failing the capture.
2026-09-02 12:40:00 -05:00
Brooklyn Nicholson 06a4f4ab31 feat(desktop): group a comment batch by page region so it lands as a few tasks
Twenty-three comments arrived as twenty-three flat blocks, so the agent made
twenty-three todos and ground through them one at a time. They now arrive
grouped by where they sit in the page, with a line telling the agent to work
the groups rather than the comments.

The renderer groups on structure, not meaning. Whether a comment is a UI nit
or a functional bug is a judgment only the model can make, and prose-matching
it here would be wrong constantly; which pins share a DOM subtree is something
the selector already answers. That split is also the one that makes parallel
work safe — grouping by theme instead ("all the spacing ones") cuts across the
same components and puts several workers in the same files, so the guidance
says to hand out whole groups and never to regroup by theme.

Grouping compares ancestor paths, so a heading and a paragraph in one card
stay together instead of becoming two singletons. Depth is derived rather than
tuned: descend the shared prefix until it stops being shared, then sub-split
any group still holding more than a third of the batch — without that pass a
normal page buries every section under `main`. Batches under four comments,
and batches that all land in one region, stay flat.

Grouping is advice in the prompt, never an action: the renderer does not spawn
or delegate anything. That stays the agent's call.
2026-09-02 12:29:25 -05:00
Brooklyn Nicholson e4bda3ff77 feat(desktop): browser comments carry the element's selector, markup, and styles
Comment mode shipped the crop and the note, so an agent got a picture of the
problem and had to grep for the element it showed. Each element comment now
also names its CSS selector, its markup, and the computed styles that decide
layout, which is what the agent needs to land in the right file.

The target line stays prose — it is what the user pointed at — and the DOM
detail rides labelled lines beneath it. Area pins have no element, so they
still get only the crop and the note.

Markup is redacted in the guest before it crosses to the host: password and
hidden input values, and any attribute reading as a key/token/secret, are
replaced with [redacted] on a clone, so a page's secrets never reach the
composer or the model. It is clipped to a 600-char budget so one comment
cannot paste a whole section.

AnnotateIdentity was a hand-copy of CompactIdentity that had already drifted;
it is now an alias, so the guest, the pin, and the packer cannot disagree
about the shape again.
2026-09-02 12:29:25 -05:00
hermes-seaeye[bot] bcef556b00 fmt(js): npm run fix on merge (#101481)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-02 17:18:47 +00:00
Teknium 7840a0e2d9 feat: delegation batch tags read "set N" instead of a hex id slice
Interleaved subagent fan-outs were tagged with the first 4 hex chars of the
delegation id ([b2ac 3/9]), which is attributable but unreadable. Batches are
now numbered in order of appearance per process: [set 1 · 3/9], [set 2 · 1/7].
Desktop /agents already labels groups "Delegation N", so its duplicate hex
badge is dropped.
2026-09-02 10:12:54 -07:00
Brooklyn Nicholson 195c3e5a3b fix(desktop): refuse every resume for a chat the user is deleting
The leftover-4001 fix stopped the dispatcher from CREATING a rebind
after a tombstone, but a request queued before it still fired, and the
push path (markRuntimeGone) never checked at all. Both funnel through
requestSessionResume, so guard there: a removal-pending id never gets
queued, and no consumer has to re-derive whether an id is doomed.

isSessionRemovalPending is now the single predicate. resumeSession keeps
its entry guard for requests queued before the tombstone; the session
tile gets the same guard via shouldResumeSessionTile, closing the tile
twin where a 4001 racing a delete unbound the runtime and re-armed the
resume effect against a dead id.

Drops the !freshDraftReady clause on explicitlyRequested: a gateway
switch also stages a fresh draft while leaving the URL on /:sid, where
an explicit request is the only remaining resume lever, so that clause
silently dropped plugin and SDK reselects after a connection apply.

Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
2026-09-02 11:23:50 -05:00
Brooklyn Nicholson 86acfee949 refactor(desktop): give session tombstones their own store
The delete/archive tombstone atoms lived in store/projects.ts, which
imports store/session. Resume lives on the other side of that edge, so
consulting the tombstones from the resume path would have closed an
import cycle. Move the atoms and their mutators to store/session-removal
and repoint every consumer; no behavior change.
2026-09-02 11:23:50 -05:00
xxxigm 66109d3bbd test(desktop): leftover 4001 rebind must not revive a deleted session
Cover the delete-transition race and the tombstone early-return so the Resume failed toast cannot come back through those paths.
2026-09-02 11:23:50 -05:00
xxxigm 990879d688 fix(desktop): don't rebind a deleted chat from a leftover 4001 resume
A queued requestSessionResume still fired during the /:sid -> /new tick after delete, re-selected the doomed id, and toasted Resume failed / Session not found.
2026-09-02 11:23:50 -05:00