Drag-to-float gestures now default off on a fresh install. A composer that was already floating before the flip keeps its gestures, and a stored choice wins either way.
Co-authored-by: networthexplained <300128320+networthexplained@users.noreply.github.com>
A volume that already owns /workspace skipped the configured working
directory, so tools treated that host path as unmounted. Bind it at a
second mount, or point tools at the volume that already has it, for any
drive path.
Six fixes for the wave-7 picker/input cluster:
DeepSeek -> "Deepseek") and left the gemini- branch's words lowercase
("Gemini 2.5 pro"). Vendor casing + parameter counts now applied after
title-case (GLM, DeepSeek, MiniMax, OpenAI, ERNIE, MiMo, BGE, VL, IT,
FP8, AI; 8b -> 8B, a3b -> A3B), and the gemini branch title-cases like
every other branch.
and was unreachable by keyboard (rows are highlighted, never DOM-focused,
so Radix's own ArrowRight never fires). The chevron is now visible on
every model row and ArrowRight (caret parked at query end) hands focus
to the highlighted trigger and opens its sub; ArrowLeft returns focus
to the search field. Consolidates #86968 + #104532.
-fast/-thinking/-preview ids to the base label. The tag now rides the
display name on every surface, and formatModelPillLabel no longer
doubles Fast for a -fast variant id.
all MoA presets were disabled: manual picks are sticky by design
(d595e636c8), but the virtual moa provider's catalog row disappears
entirely once no preset is enabled, so that one absence is
authoritative (moaPickRemoved) and the pick reseeds from the profile
default. Narrow moa-only exception — no general catalog diff.
token (nimb -> nimb*); none of the CJK routes can honour it (bigram and
trigram routes quote tokens so the star matches literally; LIKE has no
star wildcard at all), so CJK searches returned zero results. The star
is now stripped per token on the CJK path only.
measure() effect deps (stale measurements after toggling Inbox style)
and the card estimate undershot the four-line/wrapped-title worst case
(74px), painting rows over their neighbours on cold start. Card
estimate raised to the worst-case-covering 96px and the deps fixed.
Linux uses apple-touch-icon.png as the window icon and Nix requires it to
match the full-bleed launcher icon, so keep it full-bleed and point the
dev-only app.dock.setIcon at assets/icon-mac.png instead.
Dev runs replace the Dock icon with public/apple-touch-icon.png, which the
generator rendered full-bleed, so it drew ~24% larger than its Dock
neighbors. Render it from the mac-grid master like the icns targets.
On the 824 grid the plate matches peers, but the girl inside a white tile
with a ring read small; scale her 1.12x about the plate center for every
mac target.
A pending clarify card waits on its gateway clarify.request. When that
frame is lost the card sat as a disabled preview until the 300s timeout
with no hint of what went wrong.
After a 4s grace the card now asks the owner socket for
session.events.since, which re-delivers the session's open requests, so a
request the backend still holds parks and the card goes live. If nothing
turns up, single and batch cards show an inline notice (all locales)
pointing at Stop, and drop the dead Skip/Continue actions.
Since the questions[]-only schema (#95907) every single question is a
one-entry batch on both the tool args and the gateway wire, yet no test
exercised that shape (called out in #98645). Lock the behavior down at
both layers:
- unit: a one-entry batch renders the batch card (not a blank/spinner
single card) both with the wire already parked and when the request
lands after the tool row, and answers with the qid-keyed lock
- e2e: a SINGLE_BATCH trigger drives the real chain (composer -> gateway
-> agent -> clarify tool -> clarify.request -> renderer) through mount,
pick, confirm, and settle for questions.length === 1
The e2e mock's trigger routing also learns to scope its has-tool-result
guard to the answering turn's own question text: the existing any-tool-
result check would false-positive once a second scripted clarify shares
the conversation history.
Adapted to main: the mock server now lives in tests-js/scripts, and a batch
confirm answers with clarify.lock.
Electron booted from active-profile.json and ignored argv. hermes
desktop and hermes -p <name> desktop never appended --profile, so
the packaged app kept the stored profile.
Parse both --profile spellings before startHermes, persist that
name, and append the same flag on the packaged launch. A missing
flag does not change the stored profile.
Desktop paste/file attachments land in Hermes-managed staging dirs on the
GATEWAY (composer-pastes/ for large text pastes, attachments/ for dropped
files), but on the Remote SSH topology the workspace root (TERMINAL_CWD) is a
path on the SSH HOST - the two filesystems are fully disjoint, as the issue
thread confirms. Two gaps combined to reject every staged attachment with
"path is outside the allowed workspace":
- _resolve_path admitted only allowed_root + composer-paste roots, so a
gateway-staged attachments/ path was refused outright. Admit the
_CACHE_DIRS staging roots (attachments/, images/, cache/*, composer-pastes/)
via a helper that asks get_cache_directory_mounts - the gateway's OWN
payload is never a workspace escape, and the path-traversal and
credential-deny guards in _ensure_reference_path_allowed still run after.
Anything else outside the workspace stays blocked.
- composer-pastes/ was missing from _CACHE_DIRS, so its bytes never reached
the remote: ssh/daytona/vercel_sandbox sync via iter_sync_files ->
iter_cache_files, and to_agent_visible_cache_path only translates mounted
dirs - a paste attached on a fresh session dangled on the remote host.
Tests cover the disjoint-filesystem SSH topology end-to-end (text inlines,
binary renders the synced ~/.hermes path), the still-refused stranger path,
local-backend unchanged, and the composer-pastes mount+sync enumeration.
Consolidates PR #110387 by Finn763 (the _agent_staged_path guard widening and
the SSH-topology tests, adapted to the current _ensure_reference_path_allowed
ordering) with PR #103412 by ericmaddox (whose mapping insight is subsumed by
the _CACHE_DIRS entry, which fixes both the sync and the translation).
Co-authored-by: ericmaddox <ericmaddox@users.noreply.github.com>
The narrow-viewport overlay for a collapsed zone is `absolute inset-y-0` —
it starts at the viewport's top edge with its tab strip (SESSIONS | BOTS) as
the first child, so on macOS the strip slides under the traffic lights.
Docked zones already reserve that band (TreeGroup: useWindowControlsOverlap
-> paddingTop plus an absolute [-webkit-app-region:drag] spacer; top-edge
zones use usePanelTitlebar), but the overlay used neither.
Reserve it the same way: measure the native controls rect against the
overlay element and pad the strip below it, keeping the band a window-drag
target via the drag-region spacer. The overlay's data attribute now carries
the revealed pane id (it was a constant empty string), which the regression
test uses as its selector.
Salvaged from PR #110034 by Muhammed Emin Boydak (the overlap hookup, the
paddingTop + drag spacer, and the 34px-reservation test), adapted to the
current file (NO_PANE_GROUP import, per-pane data attribute).
Fixes#110033.
Model output can arrive carrying Gemini-style grounding citation markers:
private-use delimiters U+E200/U+E201 wrap a `citeturn<n>search<m>` id list
(U+E202 separates ids) - e.g. `\uE200citeturn0search11\uE202turn2search0\uE201`.
Desktop had no rule for that shape (CITATION_MARKER_RE only strips bare
numeric `[n]` markers), so the private-use code points painted as replacement
glyphs - the reported "triple bars" - and the `turn…search…` ids rendered as
literal prose, wrapping across table cells and obscuring the answer.
Add CITATION_TRANSPORT_MARKER_RE and strip it in rewriteProseSegment, the
same shielded path the numeric marker rule rides: inline code and math spans
split out first, so `$\sqrt[3]{8}$` and quoted marker text are untouched, and
the bare no-delimiter alternative only fires on the `cite` head so plain prose
and stray private-use characters (icon fonts) are left alone. A marker that
cannot be resolved to a source is dropped, never invented into a link -
matching the reporter's own expectation. Mid-stream flushes (closing U+E201
not yet arrived) are covered by the optional-tail shape.
Backend-side emission (which search provider leaks the markers into model
text) remains unisolated, as the report itself notes; the display-layer strip
is justified regardless.
Fixes#120587. No external PR existed (the catalog's linked PR #120592 is a
dead reference).
MarkdownLink nulled fallbackLabel whenever the link text matched the target
URL — exactly the bare-autolink case — so PrettyLink fell through to
urlSlugTitleLabel and rendered a host-only label (`ncpssd.org` for
https://www.ncpssd.org/), with the address readable only via hover/inspect.
The user could not read an address sent in chat.
The link's own text is always a legitimate fallback label; pass it through.
Labeled links are unchanged: their authored label already wins display, and
that shape (a label hiding the address) is PrettyLink's documented contract,
not a bug.
Salvaged from PR #38213 by Phantomthedog (the fallbackLabel change and the
localhost/example.com render tests), reworked to keep the change scoped to
the bare-autolink shape and reshaped into an end-to-end
MarkdownTextContent test alongside the existing session/filelinks suites.
A popped-out Browser window is a fresh renderer: no in-memory atoms cross
the window boundary, and no session ever pushes a rail scope into it (the
controller skips session/preview watching there), so its scoped previewTabs
view started empty and PreviewTilePane rendered null — the shell spawned but
never painted, matching the reported black window. Root cause is the
per-profile rail scoping from c996d1c088, not the GHSA window-open policy
the reporter suspected: the window/IPC handoff itself is fine.
Three coordinated moves in the store:
- adoptPersistedBrowserTab now reads every profile bucket (plus the
pre-scoping single-array shape; the old decode returned [] for bucketed
storage, so even the sibling URL sync was dead) and, when the tab is not
in this renderer's view, re-homes the view onto the OWNING bucket instead
of splicing the tab into 'default' — a splice would duplicate the popped
tab into the primary profile's rail. When the tab IS present (the docked
mirror on pop-out close), it adopts the newer URL/label as before.
- PreviewTilePane calls that adoption from a browser window when its tab is
missing from the view.
- The persist subscriber no longer echoes module-init emissions back over
storage: nanostores fires subscribe immediately, so every renderer used to
clobber its un-adopted record (a legacy single-array store was wiped
before pendingLegacyTabs could adopt it; a 'default'-only bucket store was
removed the same way), and the view is now seeded from the renderer's own
bucket — which also restores the primary profile's rail at boot, since
setPreviewScope('default') early-returns on the initial viewKey.
Salvaged from PR #120110 by finn763 (diagnosis + adoption mechanism +
creation-emission guard, re-homed onto the owner bucket to avoid the
duplicate-bucket write.
EOF
)
The unknown-id shortcut gave any unsaved draft a private OAuth jar, so a
pre-save cloud sign-in wrote persist:hermes-remote-oauth-conn-<id> while the
saved cloud connection reads the shared persist:hermes-remote-oauth. Send the
draft's kind/authMode with the request and only grant a private jar to remote
OAuth drafts; everything else falls back to the legacy jar.
The #99989 salvage threads {connectionId, label} through
oauthLoginConnectionConfig; the pre-existing #89529 test still asserted
the single-argument call shape. Update it to the two-argument contract.
Co-authored-by: Together <BorgWrightpcalac@outlook.com>
The cherry-picked test expected 'conn:<id>' but main's partition prefix is
'persist:hermes-remote-oauth-conn-<id>' (PR #99992 was written against an
older naming). Align the assertion with the shipped prefix.
Co-authored-by: Together <BorgWrightpcalac@outlook.com>
Settings → Connections lets a draft remote gateway be signed in BEFORE it
is saved. The login IPC carried only the URL, so resolveOauthPartition()
matched against the on-disk registry, found no entry, and fell back to the
legacy shared jar: the fresh session was invisible to the saved connection
(which reads its own per-connection jar, #92183), and in the same-host
setup it also evicted the other gateway's cookie in the shared jar.
The login IPC now carries the draft's identity. An explicit connectionId
wins; otherwise the main process mints the id the save will use
(connectionIdForPendingLogin, same derivation as normalizeConnectionInput)
and returns it so the editor pins the id into the draft. The resolver
resolves a named connection by identity: a known entry follows the
existing rules; an unknown id is a pending non-primary oauth remote —
editor saves never promote a fresh entry to primary — and gets its own
partition up front.
Fixes#99989
(cherry picked from commit 054a7af5e49403660c29c152440c0d3bb46d3871)
A republished connection reply carried only the cached backend descriptor,
whose getWindowState() spread was baked in at process cold start
(startHermes caches the backend for its lifetime). A window that entered
fullscreen after the first dial got a stale isFullscreen: false on every
republish — reconnect, sleep/wake, gateway switch, backend restart, ⌘R
reload — so the renderer's live fullscreen flag was overwritten and the
titlebar's traffic-light inset reverted to the windowed offset while still
fullscreen. Toggling fullscreen fired a live push again and "fixed" it,
which is exactly the intermittent behavior reported (#102451).
Read the calling window's state when the reply is built, via the new
connection-window-state helpers (liveWindowState/overlayWindowState), so
every shape connectDesktopProfileRoute returns — registry-scoped,
primary-resolved and bare — carries live values consistent with the
hermes:window-state-changed live-push path. Both connection IPC handlers
now pass their sender through; without it the lookup silently falls back
to mainWindow, so a secondary window would be told the primary's chrome —
the same defect for secondary windows that the primary had for stale
snapshots.
The secondary-window fullscreen leak (bindWindowChromeEvents) is already
fixed on main (window-chrome-events.ts threads the bound window through
sendWindowStateChanged), so this is the remaining surface.
Tests: connection-window-state.test.ts covers sender-window state reads,
primary fallback, destroyed/no-window degradation, and the overlay
behavior on all three reply shapes (DI over BrowserWindow.fromWebContents
and getWindowState — no source-text assertions).
Fixes#102451
Co-authored-by: ryrenz <163799701+ryrenz@users.noreply.github.com>
session.info heartbeats mirror the runtime's resolved model/provider (e.g.
the generic `custom` billing class a named provider resolves to) into the
view through setCurrentModel/setCurrentProvider, which also persist to the
composer's sticky localStorage selection. Every heartbeat therefore
overwrote the user's actual pick, so a later new chat followed the
last-seen runtime class instead of the selection or the Settings default.
Add setCurrentModelTransient/setCurrentProviderTransient (mirroring the
existing setCurrentCwdTransient pattern) and use them in
syncRuntimeMetadataToView, which is the only caller reached from periodic
heartbeats rather than an explicit user pick.
Fixes#102793
(cherry picked from commit d8049b4ba40a59becac59a8f8ccbddcf8b9d58cc)
The failed/cancelled state of the first-launch install overlay only
offered "Copy output" and "Reload and retry" -- retry clears the
latched failure and reloads, re-entering bootstrap. There was no way
to back out of a deliberately cancelled install short of quitting.
Add a Close button (and Escape) that dismisses the overlay locally,
without touching main's bootstrap state or reloading, so the app
falls through to whatever view sits behind it.
(cherry picked from commit 68dffe3db385735cedc26559f0bac695b20bf42a)
Use Electron 40's event-object console-message contract, retain one-argument listener arity, and report malformed signature drift once so renderer logging cannot fail silently.
Co-authored-by: CupaJ12 <108900676+CupaJ12@users.noreply.github.com>
(cherry picked from commit 378701b2b7af1e0a3accfaec7b8daa46c30832d6)
Desktop rebuilds the AIAgent per turn (idle reap -> next message re-mint),
and agent_init.py gives every rebuilt agent a brand-new, empty _credits_latch
via new_credits_latch(). seed_credits_at_session_start() -> _hydrate_seed_state()
then unconditionally primes latch["seen_below_90"] on that fresh latch and
evaluates once -- correct for a genuinely new session opening mid-band, but on
a reap/resume rebuild it makes evaluate_credits_notices() see
shown_band=None vs. current_band=<the same band as before>, so it re-fires
"You've used $X of your $Y cap" on every message even though the user already
saw that exact notice moments ago on the previous incarnation of the same
session (#101578).
agent.session_id is stable across these rebuilds even though the agent object
and its latch are not, so add a small process-lifetime cache
(_seen_usage_bands, bounded to 500 entries, MRU eviction) keyed by session_id
that remembers the last usage_band actually shown. _hydrate_seed_state()
restores it into the fresh latch before evaluating, so a rebuild with unchanged
usage stays quiet, while a rebuild after a genuine crossing (recorded via the
same warm-path write in rate_limit_credits._emit_credits_notices, the single
chokepoint both the seed and live-header paths share) still fires normally.
Deliberately not persisted anywhere durable -- a real process restart is a real
"session open" and should still warn immediately, matching the existing
cold-start seed behavior for a session that opens already in a band. An agent
with no session_id (plain CLI, never rebuilt) falls back to the pre-fix
behavior unchanged.
Tests (tests/agent/test_credits_cold_start.py): a rebuild with the same
session_id and unchanged usage does not re-fire; a rebuild after a genuine
band change still fires (and clears the old key); a different session_id is
never suppressed by another session's history; an agent without a session_id
degrades to the old always-prime behavior without raising.
Fixes#101578
Co-authored-by: Edizzier <umit.ediz@hotmail.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
A Desktop Docker session that registers an absolute host directory
such as /mnt/... or /srv/... as its cwd skipped the /Users|/home/
drive-letter heuristic, so the mount check never ran and commands
were wrapped with cd to that host path. Classify the mounted host
directory as unusable before that heuristic, and remap it to
/workspace in the live-env write and the per-command resolver.
The scoped clear deleted only the root's own key from each cache, but this
PR's listing pass now keys gitRootCache on every listed directory and
nestedRepoCache on every entry the ignore rules probe — all strict
descendants of the root, never the root key itself. The refresh paths in
use-project-tree therefore served stale nested-repo and git-root answers:
git init inside a parent-ignored directory stayed hidden until a full
no-arg clear (connection change or relaunch).
Evict every key at or under the root, for all three caches, matched on a
path boundary via isUnderPath so /repo does not evict /repo2, and only
within the current connection's cache keys. Regression test covers the
refresh-makes-it-visible case and the sibling-root boundary.
The drain-exhaustion drop treated 'hint lookup returned undefined' as
'the session is gone', but getSessionOwnerHint also returns undefined for
TWO OR MORE routes (cloud gateway plus local backend) — a positive
liveness signal misread as absence. Use the plural getSessionOwnerHints
(≥1 route keeps the entry), and refuse to drop while the loaded list page
cannot prove absence either ($sessionsLoadError, or any profile in
$sessionProfilesTruncated — the sidebar list is one page, so a session
below the fold is unknown, not deleted). 'Maybe' never deletes; the
queued prompt stays for a manual send.
A prompt left in the composer queue when the app exits is drained after
relaunch against a session runtime that no longer exists. The submit
path already resumes by stored id (#106986 gated the drain on session
discovery for exactly this), but when the stored resume itself refuses
(deleted from another surface, backend restart rejecting), the drain
retried MAX_AUTO_DRAIN_ATTEMPTS times, showed the queueStuck ERROR
banner ('message not sent' — reading as data loss), and because the
failure counter was process-local while the queue persists in
localStorage, every subsequent launch replayed the whole cycle.
- Persist the per-entry drain-failure budget with the queue
(QueuedPromptEntry.drainFailures): the retry ladder is unchanged
within a process, but an exhausted entry is skipped after restart —
no attempts, no banner. Queueing a fresh prompt lifts the budget off
the session's entries, and a manual send from the queue panel clears
it (clearQueuedPromptDrainFailures), mirroring the composer's
in-process counter reset on the same gestures.
- At exhaustion, distinguish gone from known (discovery has settled,
so the loaded list plus owner hints are authoritative): a session no
row or hint answers to can never accept the queued prompt — drop the
entry and notify quietly (info, new queueDropped strings). A session
that still exists keeps its entry (real data, manually sendable) and
gets the queueStuck notice downgraded from error to info.
Regression tests: gone-session drop with a quiet notice, known-session
retention, and a restored-after-exhaustion queue that neither attempts
nor notifies. i18n: queueDroppedTitle/Body added to all nine locales.
Closes#98015.
Implementation from the issue reporter's live-verified v3 patch
(MichaelZelbel), posted on the tracker after their first patch (and PR
#101115) proved unshippable: the readDir bridge strips `.git` entries
(FS_READDIR_HIDDEN in electron/fs-read-dir.ts), so any implementation
that probes directory listings for a `.git` entry passes mocked tests
and does nothing in production.
Two changes to filterIgnored:
- Detect nested repo/worktree roots via the existing gitRoot bridge
(git rev-parse --show-toplevel): a directory is its own repo's root
when gitRoot(dir) resolves to dir itself. Covers worktrees (a .git
file) for free, works even though listings never show .git, and only
probes entries the parent's ignore rules would actually hide.
- Re-anchor the ignore-rule chain at the nearest git root of the LISTED
directory (falling back to the project root): git applies ignore
rules only inside their own repository, so a parent's `dev/*` must
not empty a nested repo one level down — only that repo's own
.gitignore chain governs.
Probe results are cached like the gitignore reads and cleared with the
same cache-invalidation path. Coexists with the per-project
showIgnoredFiles opt-in (checked first — it skips the whole pass).
Tests: nested clone, worktree-style .git file, browsing inside a
nested repo, plus the existing suite.
Closes#99861.