Commit Graph

46630 Commits

Author SHA1 Message Date
Halldrix
eec5d2af64 fix(build): record the baked stamp clock, not the live one (#123308)
write-build-stamp.mjs rewrites the stamp's `builtAt` clock on every build, so the
whole-file hash moved under a receipt that had just recorded it. Two builds
overlapping on one tree failed with "Build inputs changed during compilation" for
a difference the build machinery itself made, and the caller turned that into
SystemExit(1).

Hash the stamp's provenance identity instead, key-sorted so a cosmetic
re-serialisation is not a change. The clock is excluded from that comparison but
recorded as `stampClock` — the value the bake actually embedded, captured from
the same read that produces __HERMES_INSTALL_STAMP__ — because packaging copies
the live stamp into the bundle after the receipt is written. Re-reading the stamp
at record time would certify a clock the output never contained, and
productCurrent would then report a bundle current whose packaged clock disagrees
with its baked main, which is exactly what detectBundleSwap must never do.

recordProduct still compares the prepared inputs, so a clock-only restamp cannot
throw. productCurrent additionally requires the recorded clock to match, so a
restamp between bake and packaging is a rebuild rather than a silent divergence.
A receipt predating the field compares by hash alone until the next build
rewrites it; the release packaging path never consults productCurrent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-01 00:15:05 -05:00
Halldrix
ff28c475e3 fix(build): hash the desktop stamp's identity, not its build clock (#123308)
write-build-stamp.mjs rewrites `builtAt` on every build, and build.mjs runs it
before compilation, so the desktop build rewrote a file its own receipt hashed
as a prepared input. Two concurrent `hermes desktop` builds therefore made
recordProduct() fail with "Build inputs changed during compilation" on a change
the build itself caused, and the caller turned that into SystemExit(1).

Hash the stamp's provenance identity instead of its raw bytes. builtAt is the
only field that moves between two stamps (the tree documents it as
"differs on every rebuild"), and it is a build clock, not provenance: the baked
output changes when commit/payload/tag change, never when time passes. A
non-JSON stamp still falls back to the whole-file hash, so content keeps
deciding freshness and only the clock is ignored.

Unlike b3618bda35, the stamp cannot simply leave the hash set:
bundleElectronMain bakes its bytes into electron-main.mjs, and
bundle-swap.test.ts pins "same commit rebuilt (builtAt moved)" as a real swap,
so freezing the clock would break a committed test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-10-01 00:15:05 -05:00
PRATHAMESH75
d436e79bae test(build): seed assets/ input before the junk-write assertion (#122632)
The freshness regression wrote assets/.localized into a hashed web input
dir the fixture never created, so the assertion exercised a
missing->directory transition of the assets input rather than the
junk-in-an-existing-dir case it meant to cover: treeHash goes from
'assets\0missing\0' to 'assets\0', the hash flips, and productCurrent
returns false. Seed assets/logo.svg before buildWeb so assets is an
established hashed dir at baseline; the later .localized is then skipped
by macMetadata and the hash stays byte-identical, keeping assets coverage
in the junk-metadata check.

Reported by @kokhlo.
2026-10-01 00:15:05 -05:00
Brooklyn Nicholson
90390bbd1a style(desktop): sort dead-session-prune import in contrib controller 2026-10-01 00:12:40 -05:00
Brooklyn Nicholson
0c59646fbc fix(desktop): drop the vestigial pruneComposerPopoutZones import
main removed pruneComposerPopoutZones from @/store/composer-popout (the
popout zones were reworked); dead-session drafts are pruned through
composer.ts's stashedDraftScopes/clearSessionDraft instead. The import was
unused and broke the desktop typecheck.
2026-10-01 00:12:40 -05:00
David Metcalfe
a2f0200111 fix(desktop): prune dead session references from renderer storage (#79001)
Pins, composer drafts, and queued prompts persist session ids in
localStorage and re-assert/probe them at boot. Sessions removed from
state.db (retention purge, manual delete) leave dead ids behind:
nothing ever drops them, so every boot re-requests the dead ids and the
gateway answers 404 "Session not found" once per stale reference,
forever.

Add a dead-session prune sweep that runs after the first sidebar list
payload (and re-runs, debounced, on later list changes). A stored id is
only declared dead after a by-id probe 404s on every known profile — a
404 is profile-scoped, so a single miss never prunes anything, and
non-404 probe failures (gateway mid-restart, network) defer the id
instead of dropping it. Dead pins are forgotten from pin-sync
bookkeeping before unpinning so the reconcile does not re-PATCH the
dead id (which would recreate the 404).

Fixes #79001
2026-10-01 00:12:40 -05:00
John Paul Soliva
612d8e44a2 fix(desktop): a fence character inside a code listing no longer ends the block
preprocessMarkdown and normalizeFilePreviewMath told code from prose with
CODE_FENCE_SPLIT_RE, which ends a fenced block at the first ``` or ~~~
anywhere, mid-line included, and whatever the opening marker was. The
rest of the listing then ran through the prose rewrites: citation
stripping turned arr[0] into arr, and $5 became \$5. EMPTY_FENCE_BLOCK_RE
matched an inner closing fence followed by the outer one, so a markdown
example fenced with ```` or ~~~ lost both closers, and the rest of the
message rendered inside the code block.

Both now use one line-based splitter with the CommonMark fence rule that
normalizeFenceBlocks already applies: a fence opens on its own line, and
only a line holding the same character, at least as long, closes it.
2026-10-01 00:07:19 -05:00
Brooklyn Nicholson
b20281dcc0 fix(desktop): preserve stale guard fork protection 2026-10-01 00:04:44 -05:00
Brooklyn Nicholson
46c60181f2 test(desktop): keep the notice/tip guard regressions and pin own-residue vs competing-view semantics 2026-10-01 00:04:44 -05:00
salch-cred
36ff1abd51 fix(desktop): graft this window's own turn residue instead of refusing the send (fixes #124005) 2026-10-01 00:04:44 -05:00
finn763
b236dca1b6 fix(desktop): keep pre-compression transcript reachable after in-place /compress (#105256) 2026-10-01 00:04:27 -05:00
Brooklyn Nicholson
cb75168adf test(tui-gateway): patch the multi-slot skill cache key (hermes-agent 128312) 2026-10-01 00:04:20 -05:00
Brooklyn Nicholson
5ba9db294f fix(tui-gateway): expand stacked /skill tokens in command.dispatch
_dispatch_skill built a single-skill invocation from (name, arg) with no
stacked handling, so on Desktop/TUI '/skill-a /skill-b do XYZ' left every
extra /skill token in the prompt as plain instruction text — unlike the
CLI (cli.py) and messaging-gateway (gateway/run_inbound.py) paths that
route through split_stacked_skill_commands + build_stacked_skill_invocation_message.

After the `key in cmds` gate, the extra leading skill tokens (up to 5)
are split off and one stacked invocation message loads every skill over
the remaining instruction, with a loading notice and the display
projection the UIs render. A non-skill or repeated token stops the stack
and stays in the instruction, exactly like the CLI/gateway paths.

Adapted from #74876 into the current _dispatch_skill body
(suppress/_tools_mod/_session_home_scope shape preserved).

Fixes #74705

Co-authored-by: Alexey <56967387+lifeFedorovAlexey@users.noreply.github.com>
2026-10-01 00:04:20 -05:00
finn763
722bcedb38 fix(desktop): keep one-question clarify answerable on a single-shape request
A backend without the batch wire parks one single-shape gateway request
per question while the tool args carry the canonical one-entry questions
array. The batch card has no qids to answer with, so it sat a
permanently-disabled '0 of 1 answered' preview until the live request
timed out unanswered. Route that skew to the live single card, whose own
question-text match still stands down on disagreement.

Closes #123126
2026-10-01 00:03:20 -05:00
brooklyn
b4ed99f3bb refactor(desktop/i18n): split ar.ts into ar_<topic> siblings
ar.ts had grown to 3,770 lines (2K-law violation, #79963). Split the
Arabic catalog into topic siblings and recompose it in ar.ts via
defineLocale():

  ar.ts -> ar_artifacts.ts     (artifacts, artifactCard, artifactPreview, preview)
          ar_assistant.ts     (assistant)
          ar_boot.ts          (boot, remoteDisplayBanner, updates, guidedGreeting, install, onboarding)
          ar_capabilities.ts   (catalog, skills, agents)
          ar_chat.ts           (composer, statusStack, prompts, desktop, tips)
          ar_chrome.ts         (externalOpenFailed, fileMenu, titlebar, keybinds, sidebar, shell, rightSidebar, zones, contextMenu)
          ar_command_center.ts (commandCenter, messaging, profiles, cron)
          ar_common.ts         (sharedMetrics, common, ui)
          ar_connectors.ts     (sessionImport)
          ar_diagnostics.ts    (sendDiagnostics, notifications, errors)
          ar_settings.ts       (language, settings, modelAssignment, modelPicker, modelVisibility, interfaceMode)

Pure move: every member block moved byte-verbatim; the composed catalog
is value-identical to HEAD's ar.ts (4,935 leaf keys). Members added on
main since the original proposal were assigned to their topic shard.

Fixes #79963
Part of #78647

Co-authored-by: Finn763 <148605355+Finn763@users.noreply.github.com>
2026-10-01 00:01:52 -05:00
Brooklyn Nicholson
374406c2f8 style(desktop): import order in the focused-pane test suites 2026-10-01 00:01:30 -05:00
Brooklyn Nicholson
349372199a fix(desktop): build the focused-pane cwd on main's session focus; keep it through Files/Terminal chrome
Main now derives $focusedStoredSessionId from $focusedTreePaneId, which keeps
the last chat while the sessions list holds focus. Drop this branch's second
derivation and widen that rule instead: any non-chat zone (Files, Terminal,
preview tabs) keeps the remembered chat, so the Files rail no longer jumps to
the primary's project when you click into it.
2026-10-01 00:01:30 -05:00
Brooklyn Nicholson
3d33017cd1 test(desktop): drive the tree-based focused-session derivation in the statusbar suite
$focusedStoredSessionId now derives from $activeTreeGroup + $layoutTree, so
focusing a tile in tests must set the main zone's active pane, not only the
session-focus pane atom.
2026-10-01 00:01:30 -05:00
Brooklyn Nicholson
5285869db4 fix(desktop): import $activeTreeGroup for the focused-session derivation 2026-10-01 00:01:30 -05:00
Mohammad
b90aa65651 fix(desktop): add session-history fallback to $focusedWorkspaceCwd for primary sessions
When switching between multiple open project tabs, the primary workspace
could temporarily report an empty cwd before workspaceCwdOwner caught up.
Fall back to the confirmed session cwd from $sessions / $focusedSessionState
so the right sidebar immediately resolves the workspace directory.
2026-10-01 00:01:30 -05:00
Mohammad
4c9cd25d96 fix(desktop): preserve file git change tints across multi-cwd sessions and sidebar refreshes
- Bind $repoChangeByPath to $repoStatusByCwd so file status decorations
  persist independently of active session ownership or detached chats.
- Add directory-level untracked inheritance to repoChangeKindForPath.
- Register right-sidebar workspace cwd in registerRepoStatusCwd and trigger
  refreshRepoStatus on sidebar tree refresh.
2026-10-01 00:01:30 -05:00
Mohammad
2cfb32447a fix(desktop): retain focused tile cwd when clicking sidebar chrome and expose $focusedWorkspaceCwd 2026-10-01 00:01:30 -05:00
Mohammad
1f44d42daf fix(desktop): sync right sidebar file tree to focused session tile
When switching between session tabs/tiles in the desktop app, the right sidebar file tree retained the primary workspace cwd ($selectedStoredSessionId) instead of updating to the focused session tile's cwd.

Bind RightSidebarPane to $focusedStoredSessionId and $focusedSessionState so the file tree updates immediately upon tab/tile interaction.
2026-10-01 00:01:30 -05:00
686f6c61
d63c6eb874 fix(desktop): keep the Linux tray across a minimize toggle
Tray.destroy() leaves the StatusNotifierItem interface exported, so a later
new Tray() in the same process cannot re-export and the panel icon dies.
Park the instance until quit or host loss instead of destroying it on disable.
2026-09-30 23:57:09 -05:00
Fabio Motta
6e78408e70 test(desktop): cover simple-git host tuple path 2026-09-30 23:57:02 -05:00
Fabio Motta
e7585e05ad fix(desktop): silence trusted simple-git binary warning 2026-09-30 23:57:02 -05:00
Brooklyn Nicholson
be7f7b1af9 test(desktop): voice-prefs mocks export $bargeInEnabled 2026-09-30 23:49:04 -05:00
Brooklyn Nicholson
4b3cbf8c60 fix(desktop): STT filter trailing strip + live barge-in disarm (#127275)
Address review feedback from @kvnloo:

1. The client-direct exact-phrase hallucination check stripped '.'/'!'
   anywhere in the transcript, while the relay only strips trailing
   ('cleaned.rstrip(".!")' in tools/voice_mode_transcript.py). 'thank. you'
   was dropped client-direct but kept relayed. Now a trailing strip
   (/[.!]+$/), with a regression pinning internal punctuation as a real
   turn on both paths.

2. Turning voice.barge_in off after the monitor armed did not disarm it:
   ensureBargeMonitor() gates on $bargeInEnabled only at creation, so a
   live config refresh saying barge-in is off left the armed monitor
   interrupting on speech. An atom subscription now stops + clears the
   live monitor on true->false (a mid-flight capture is dropped and the
   loop resumes normal listening). Focused mid-turn toggle test added.
2026-09-30 23:49:04 -05:00
Brooklyn Nicholson
bd32c718d8 style(desktop): type-only import and named-import order in the pop-out bridge 2026-09-30 23:45:41 -05:00
Brooklyn Nicholson
7c161aa1b4 fix(desktop): sort popout bridge imports in browser-popout-shell 2026-09-30 23:45:41 -05:00
Brooklyn Nicholson
bf4d603da9 fix(desktop): tighten popout bridge request typing
tsc -p found request.id used as string after a truthiness guard; narrow to
typeof string and capture before the async closure. Cast the script-runner
stub in the test to the runner type.
2026-09-30 23:45:41 -05:00
Brooklyn Nicholson
33ebf9f11a fix(desktop): keep agent browser control after pop-out
After pop-out the live webview lives in the Browser window while the chat
window still owns the active-session gate, so drive_preview/read_preview
answered NOTHING_OPEN/empty from the chat window. Forward both over the
shared window relay (IPC in the app, BroadcastChannel as a dev fallback)
and answer with the pop-out's result; a silent pop-out falls back to the
legacy local error. Both pop-out legs now ride one relay surface.

Adapted from #101530 by FalconOrtiz (rebased onto the server-requests
split; transport moved from BroadcastChannel-only to the IPC relay so it
works in packaged file:// windows).

Co-authored-by: FalconOrtiz <falcon.ortiz11@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-30 23:45:41 -05:00
Miguel Chaves
809b1aa50b fix(desktop): route pop-out browser annotations to origin 2026-09-30 23:45:41 -05:00
Brooklyn Nicholson
e27448b231 test(agent): align the mirror merge pin and live-marker fixture with current repair behavior 2026-09-30 23:18:06 -05:00
Brooklyn Nicholson
abdf5496fb fix(tui_gateway): identity reads use the un-repaired durable transcript (#94486)
Co-authored-by: zengzheqing <zengzheqing@users.noreply.github.com>
2026-09-30 23:18:06 -05:00
liuhao1024
696f6e9ff9 fix(agent): keep marker-merged user rows addressable after alternation repair
A model-switch marker is persisted as role=user on purpose (#48338), so
the next real user row forms a user;user pair that repair_message_sequence
Pass 2 merges into the earlier (marker) row — keeping its display_kind and
dropping the plain row's _row_id from the repaired view. Every rewind /
prompt.submit addressing surface indexes non-display user turns, so the
merged pair became unaddressable and the client's next submit with
truncate_before_row_id failed closed with the input silently dropped
(#94486).

Keep the merged pair addressable: drop the display classification and
carry the plain row's durable id. display_kind never reaches providers
(stripped from every outgoing copy), so the wire payload is unchanged.

Fixes #94486
2026-09-30 23:18:06 -05:00
teknium1
406f19e190 test(e2e): fake homes answer the shared-metrics offer
An interactive chat on an undecided profile now stops on the one-time
offer; the shared e2e home writer records an answer like a real user
who already decided, so PTY/console drivers reach the composer.
2026-09-30 21:15:08 -07:00
teknium1
7a06cb50d7 feat(telemetry): shared-metrics opt-in offer on every surface
One answer per profile, asked once, with the Desktop strip's three equal
choices (Send to Nous / Local only / No thanks) now offered by:

- `hermes setup`: at the end of every flow (Quick, Full, Blank Slate,
  Portal, --quick), via the setup-completed record they all funnel into.
- `hermes` / `hermes --tui`: once before an interactive chat on a profile
  that never answered. Skipped for -q, piped/JSON output, spawned actions
  (HERMES_NONINTERACTIVE), Desktop-hosted panes and managed installs.
- Web dashboard: a banner for the profile being managed, backed by
  GET/PUT /api/shared-metrics/consent.

"No thanks" is the terminal default so Enter never opts anyone in; Esc and
the banner's X leave the question open.

Also fixes the answer that could not stick: a CLI "no" equals the shipped
defaults, so save_config stripped it and every surface (Desktop strip
included) kept asking. The answer is now written to config.yaml directly,
and `decided` has one definition shared by the RPC, the CLI and the router.
2026-09-30 21:15:08 -07:00
Adolanium
5a2a300d99 fix(dashboard): remove session files when deleting empty sessions
DELETE /api/sessions/empty called delete_empty_sessions() without a
sessions_dir, so the rows were removed but their files in sessions/ stayed
on disk. Pass the profile's sessions dir, the same way the single delete
route does.
2026-09-30 23:14:09 -05:00
hermes-seaeye[bot]
484ebdf16a fmt(js): npm run fix on merge (#129961)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-01 04:11:49 +00:00
Brooklyn Nicholson
544df42d78 fix(desktop/radio): resume the preserved stream when dictation lands in the composer
Fluid Voice (and dictation tools like it) pauses other audio over the
macOS media session to hear the microphone, then inserts the transcript
into the focused field — a guarded paste in Chromium/Electron. No
media-session resume signal ever follows, which is why preserving the
stream alone (the reporter's own experiment) still left radio paused.

Track the generation that last reached 'playing' (playGeneration); an
external pause keeps the element intact (status 'paused'), and a paste
into an editable field resumes it via a plain element.play(). The
plugin's own Pause stays destructive (toggle() from 'live' runs stop(),
clearing the element), so a paste never resumes an intentionally paused
stream and Play reconnects from scratch. Never resumed on window focus.

Credit: Kokhlo's PR #108115 contributed the non-destructive external
pause and the survival test.

Co-authored-by: kokhlo <konstantin.khlopkov93@gmail.com>
2026-09-30 23:11:19 -05:00
kokhlo
356a80bfe3 fix(desktop/radio): preserve stream on external pause for Fluid Voice resume (#108113)
Browser/media controls pause the element outside our buttons. The old handler
tore down the stream (cleared src, invoked load(), removed the element, and
advanced the generation) before macOS Fluid Voice or any external media
controller could resume it.

The internal Pause control already increments the generation first, so its
resulting media pause is safely ignored by current(). The external-event
branch can therefore be non-destructive: set status to 'paused' instead of
calling stop(). The 'playing' event already transitions the state back to
'live', so resume is possible.

Add a focused lifecycle test: external pause changes status to 'paused',
element and src survive, and a synthetic 'playing' event brings the stream
back to 'live'.
2026-09-30 23:11:19 -05:00
Brooklyn Nicholson
346aab2f8c fix(desktop): fix lint in skill provenance files (hermes-agent 128387) 2026-09-30 23:10:24 -05:00
Brooklyn Nicholson
10990b5a00 test(learning-graph): force the external-mount-first scan order
test_used_local_skill_still_produces_learning_node was reported as failing
under scripts/run_tests.sh (env -i) but passing under bare pytest, which read
as ambient-env leakage. Reproduction on both the reviewed head and the current
one shows the suite green under the scrubbed runner; the flake the reviewer hit
was the 'external' source loop-variable leak (fixed in 71485f5763), whose
manifestation depends on rglob scan order — env -i changes TMPDIR, which
changes the tmpdir layout, which changes the order the mount and the local
skill are yielded in. That env-dependence was coincidental, not causal.

Pin the hostile order directly: patch Path.rglob so the external mount's
SKILL.md always scans before the local skill, and assert the local skill still
classifies 'agent' (red at the reviewed head, green now). Also drop the
no-op monkeypatch of hermes_constants._hermes_home_cache — no such attribute
exists, so it reset nothing and only implied a cache reset that never happened.
2026-09-30 23:10:24 -05:00
Brooklyn Nicholson
0002e60e5c fix(desktop): complete SkillInfo fixture fields in provenance test 2026-09-30 23:10:24 -05:00
Brooklyn Nicholson
993247cee9 fix(learning-graph): don't leak 'external' source to later skills in a root 2026-09-30 23:10:24 -05:00
Brooklyn Nicholson
c13232e90c feat(skills): external mounts get an 'external' provenance tier (#108032)
Skills mounted from skills.external_dirs are externally authored, but every
provenance surface flattened them into 'agent': the usage report, the
dashboard's set-based twin, the desktop catalog/detail UI, and the learning
journey graph (where a used external mount rendered as a learned skill).

- tools/skill_usage: provenance() gains an 'external' tier (external-only:
  a local copy of the same name keeps winning, mirroring is_agent_created's
  external exclusion and _external_skill_names() scans external dirs once
  for the router's set-based twin.
- web_routers/skills: /api/skills classifies hub > bundled > external > agent.
- learning_graph: skills whose path resolves under an external dir are tagged
  source='external' and are never learning milestones — a mount is configured,
  not learned, even when used.
- desktop: SkillInfo.provenance gains 'external'; the catalog labels mounts
  'external' and the detail pane keeps them editable in place. Origin only,
  never mutability: read-only enforcement for external mounts was deliberately
  rejected by maintainers (commit 8c8fc6c1ec / PR #17512 — a refusal just
  breeds silent local duplicates). Provenance decisions extracted into
  skill-provenance.ts pure helpers (testable without source-reading tests).
- curator: usage report/filter knows the 'external' origin.

The desktop edit logic in this commit is the natural reading of 8c8fc6c1ec
(external dirs exist so the user can point Hermes at a vault they curate
elsewhere); if maintainers prefer the read-only half as well, it slots in
behind isEditableProvenance() in one place.
EOF
)
2026-09-30 23:10:24 -05:00
Brooklyn Nicholson
ab910f0c7b refactor(desktop/i18n): split zh-hant.ts into zh-hant_<topic> siblings
Part of #79958

Splits the Traditional Chinese override table (4419 lines on main) into the
same 11 topic files as the source PR, leaving zh-hant.ts as the composing
defineLocale file (177 lines) with the export name zhHant unchanged:

- zh-hant_artifacts.ts (artifacts, artifactCard, artifactPreview, preview)
- zh-hant_assistant.ts (assistant)
- zh-hant_boot.ts (boot, remoteDisplayBanner, updates, guidedGreeting, install, onboarding)
- zh-hant_capabilities.ts (skills, starmap, agents)
- zh-hant_chat.ts (composer, statusStack, prompts, desktop, tips)
- zh-hant_chrome.ts (fileMenu, titlebar, sidebar, shell, rightSidebar, zones, contextMenu)
- zh-hant_command_center.ts (commandCenter, messaging, profiles, cron)
- zh-hant_common.ts (common, billingBlock, ui)
- zh-hant_connectors.ts (sessionImport)
- zh-hant_diagnostics.ts (notifications, sendDiagnostics, errors)
- zh-hant_settings.ts (language, settings, modelPicker, modelVisibility)

Rebuilt against current main: main added six top-level keys since the source
PR branched (externalOpenFailed, sharedMetrics, intro, catalog, modelAssignment,
interfaceMode). They sit outside every topic boundary of the split scheme, so
they stay inline in the composing file at their original positions; the 41
mapped keys moved to the same topics with the same filenames as the source PR.

zh-hant is a sparse override: siblings carry satisfies Pick<TranslationOverrides,...>
and the composition keeps the original top-level key order. No re-export shim.

Co-authored-by: finn763 <165816600+finn763@users.noreply.github.com>
2026-09-30 23:09:55 -05:00
aydnOktay
3e1539ba1d fix(desktop): keep the mid-run composer input full-width (#99728)
Unstacked editor sat in a 1fr grid column with flex-1 + an 8rem min-width floor, so the contenteditable collapsed to the measured 129pt sliver while queue controls crowded the row. Fill the grid track with w-full min-w-0 instead.
2026-09-30 23:09:39 -05:00
David Metcalfe
aed18fdfbf fix(desktop): keep MEDIA attachments and prose when a turn also generated an image
stripGeneratedImageEchoes stripped EVERY markdown image and #media: link
from a final message whenever an image_generate tool result was present
in the same turn's parts. A completed response that carried a MEDIA:
directive for a different file lost its attachment link entirely, and a
message whose only content was such a directive rendered blank after the
empty text part was dropped by dedupeGeneratedImageEchoesInParts
(#80621).

Scope the strip to echoes of the actual generated source:
- markdown images are removed only when the src matches a source,
  with a lookahead so a longer path (a.png.bak) is not eaten by a
  prefix source (a.png)
- #media: links are removed only when the encoded path matches a
  source, anchored to the closing paren so prefix paths survive
- bare-path stripping no longer matches inside markdown link
  destinations, so [label](/tmp/a.png) is not corrupted to [label]()

Also wrap parseMarkdownIntoBlocksCached in a lexer-throw fallback so a
single pathological message cannot unwind through MessageRenderBoundary
to the root boundary and blank the whole transcript.

Closes #80621

Rebased onto current main: the strip keeps main's whitespace semantics — main deliberately stopped normalizing whole-document whitespace through media extraction (3e1b7af21b), so the same-file echo test now expects the two-space gap the removed image leaves instead of the old collapsed form. The scoped strip itself is unchanged.
2026-09-30 23:09:23 -05:00