resolveStoredSession() is a probe ladder — cache, then a by-id GET on the
active backend, then the same GET scoped to each other profile until one
hits. A miss on rungs 2 and 3 is expected and already handled: the backend
answers 404 {"detail":"Session not found"} and the renderer falls to the
next rung.
But ipcMain.handle logs "Error occurred in handler for 'hermes:api'" with a
full stack for every rejected invoke, with no per-handler opt-out — so each
designed fall-through printed a multi-line stack on startup and on every
session switch, burying real hermes:api failures.
Extract the handler body, and for that one expected case resolve with a
sentinel instead of rejecting; preload — our own code on the other side of
the same seam — rethrows it as an Error with the identical '404: <body>'
message. The renderer contract is byte-identical (the ladder and its tests
are untouched); only Electron's logging is bypassed. Every other failure
still rejects and still logs in full.
The sentinel check is deliberately narrow (a plain object whose only key is
the sentinel, with a string value) so no backend payload can be mistaken
for one.
Fixes#79265
/bg registers the interactive approval panel, which no longer has a deadline;
without the binding its batch deadline and authorization-gate accounting still
assumed approvals.timeout.
A Bot Mode group turn blocked on the user's clarify/approval answer pushes its
runaway hard cap out instead of expiring the card under the room; the offline
toast and the Settings description stop promising an approval timeout; the
approval.respond deadline comments describe what it bounds now.
tui_gateway (the TUI's and the Desktop app's backend) binds the no-deadline
policy around every prompt turn and sends clarify requests with no deadline.
The classic CLI binds the no-deadline policy around each turn and drops its own
approvals.timeout / clarify_timeout deadlines; the hint row hides the countdown.
Removes the now-unreachable timeout copy from every locale.
set_prompts_wait_for_answer() binds, per turn, that built-in approval prompts
have no deadline; approval_wait_seconds() is the one resolver the gateway poll
loop, the CLI prompt and the human-wait ceiling read. approvals.timeout keeps
governing messaging platforms, ACP and approval transport plugins.
A human-wait window now fixes its ceiling when its waiters enter it, so the
batch-deadline loop reading it from an unbound context still excludes an
unbounded prompt in full.
Main's closeDockedPreviewMatching filtered popped-out tabs; carry that onto
the session-visible tab list. Re-add preview.pin/unpin in the split locale
modules and the key manifest, and let the model-picker suite derive the
focused session from its mocked tree focus.
parseTabList rekeyed restored URL tabs onto url:<address>, breaking the
pop-out handoff (tabs are looked up by their persisted minted id) and the
never-reuse id contract. Only FILE ids need the session-scoped rekey.
The session-focus module's new imports broke tests whose vi.mock factories
omitted them: tree/store mocks now export $activeTreeGroup/$layoutTree, and
the statusbar mock keeps the real $focusedStoredSessionId derivation.
Review feedback on #87288 (Enough1122 + cross-vendor validation):
- openPreview: reusing a pinned file row from another session keeps the
owner's id (no id/sessionId split); the singleton Browser and artifacts
reached from another session re-own to the navigating session so the new
URL is not invisible in the session that opened it; the rail selection
follows the row's final id instead of the pre-computed one.
- preview-tile: merge the visible-list reveal/rehome listeners into a
single rehome-then-reveal listener so ordering is explicit.
- preview-strip-tools: the pin toggle reads the current store state at
click time instead of the render-time closure.
- Tests: pinned-reuse owner id, focused-tile-over-selection, and Browser
re-own coverage — each mutation-verified (fails with the fix reverted).
42 targeted tests pass; renderer typecheck and eslint clean.
Preview tabs were window-global: a file/URL/artifact opened in one
conversation stayed visible above the composer in every other
conversation — including duplicates of the same file (file targets were
keyed by the raw URL, so file:// and plain-path entry points stacked).
Per the design proposed on #73890, tabs now belong to the session that
opened them (sessionId stamped at open), and the drawer renders only the
active session's tabs plus an explicit pinned set:
- pinned tabs render in every session (the deliberate cross-session
workspace); a pin toggle rides on each preview tab's strip glyphs
- file tab ids are canonicalized (file:// stripped) so the same file
dedupes across entry points; restored legacy rows are rekeyed and
deduped on decode
- deleting a session drops its tabs (pins survive)
- legacy unscoped tabs migrate to pinned on load; ownerless tabs opened
on a fresh draft are adopted into the session when one appears
- the terminal pane is untouched (process state stays global)
Tests: store unit tests for stamping/visibility/pin/prune/adoption/
canonicalization/migration, plus a mirror integration test proving the
drawer swaps panes on session switch. Full UI suite 561 files / 5529
tests green; typecheck and lint clean.
Partially addresses #73890 (the preview-tab scoping gap; the Artifacts
pane and focused-session fallback paths remain separate).
_failed_turn_display_metadata stored result["error"] and the error_surface
raw into the boundary row's display_metadata, which SessionDB persists
without redaction — a secret echoed by a provider or tool error reached the
'store' and 'export' sinks verbatim (test_secret_redaction_gateway
[tool_reads_env_file-store/export]). Force-redact every string in the
payload (nested dicts/lists included) at the producer, matching the
compressor's persistence boundary.
Ensure `--session <name>` is passed alongside `--cdp <ws_url>` in `tools/browser_tool_session.py:_run_browser_command()`.
Previously, `--session` was omitted when `session_info["cdp_url"]` was set, causing `agent-browser` CLI invocations to run statelessly without connecting to a session daemon. Consequently, the accessibility ref map built during `browser_snapshot` was not retained across CLI calls, causing subsequent ref actions like `browser_click("@e4")` or `browser_type("@e4", text)` to treat `@e4` as an unescaped CSS selector and fail with Playwright parser errors.
Chat embeds are plain iframes in the default session, and Electron allows a
single onBeforeSendHeaders listener per webRequest hook — installRemoteHeaderRules
registered its own listener on session.defaultSession, so a separate stamp there
was silently discarded. Compose the stamp into the shared listener (belt-and-braces
alongside the loopback renderer origin from the previous commit).
Co-authored-by: Konstantin Khlopkov <konstantin.khlopkov93@gmail.com>
Packaged builds load the renderer over `file:`, whose origin is opaque
("null"). YouTube rejects a null-origin embed, so every YouTube embed was
dead in a packaged app while working in dev (where the renderer is served
over http from the Vite dev server).
Serve the packaged renderer from a loopback HTTP origin instead, so embeds
get a real origin in packaged builds too. `rendererBaseUrl()` becomes the
single place that resolves the renderer location -- dev server, packaged
HTTP origin, or the file:// fallback -- and the main window, session
windows and the pet overlay all route through it.
Addresses both problems raised in review of #63492:
- Navigation guard: `will-navigate` previously admitted any `file:` URL when
packaged, and the earlier revision of this PR compared URL strings against
the base. Both are unsound. A string compare accepts
`http://127.0.0.1:47891@attacker.example/` -- everything before the `@` is
userinfo, so it shares our prefix while its real origin is
attacker.example -- which would load attacker content into a window
carrying the `hermesDesktop` preload bridge. Navigation is now checked by
PARSED ORIGIN in a pure, unit-tested helper (`renderer-url.ts`), with a
regression that asserts the old prefix check would have let the hostile URL
through. `file:` bases compare by resolved path, since their origins are
all "null".
- Port collision: the server bound a fixed 47891 and rejected on any bind
error, while `whenReady` awaits it before `createWindow()` -- so an
occupied port meant startup never reached window creation. It now falls
back to an OS-assigned ephemeral port, covered by a test that binds a
squatter on the requested port first. The fixed port is still preferred
because the renderer's origin keys its localStorage, so a port that
changed per launch would orphan persisted renderer state.
Session windows keep upstream's `rendererIndexPath` path for the file://
fallback: passing a file:// URL as `devServer` would have produced
`index.html/?win=secondary`.
Scoped to the embed fix only. The OAuth/boot-recovery changes that were
mixed into this branch are split out into a separate change.
15 locales double-escaped the newlines in gateway.fast.picker_title and
gateway.reasoning.picker_title, so YAML loaded a visible backslash-n and
Telegram showed literal \n text. Every catalog value is now checked for
an escaped newline.
The RPC session.history half of #104754 landed separately as 9d1eec0ce0.
Co-authored-by: Ahmad Al-Faqih <ahmadalfakeh973@gmail.com>
A rejected import('./shiki-block') re-throws at render, past Suspense, to
the markdown-render boundary — whose HugeTextFallback collapsed the whole
reply into a raw-Markdown panel because one fence couldn't load its
highlighter (#95995: 229 such catches in one desktop.log). Catch the
chunk-load failure class in a block-local boundary that degrades the fence
to the plain pre-wrapping the app already shows while the chunk loads,
retry the dynamic import once (transient fetch failures), and warn once so
desktop.log stays diagnosable. Genuine render errors still re-throw and
reach markdown-render with their component stack.
Keep the 20px visible box and add a transparent 2px ::before ring, so the
pointer target reaches 24px (WCAG 2.5.8) while rest and hover render
pixel-identical to before.
The comment cited WCAG 2.5.5 Target Size (Enhanced) for the 24px floor,
but 2.5.5 is the 44px enhanced target. 24px meets 2.5.8 Target Size
(Minimum) (AA, WCAG 2.2). Fix the reference so future audits measure
against the right success criterion.
ICON_BUTTON (pane headers, list-strip menus, MCP server actions) was a
20px target, below the 24px WCAG 2.5.5 pointer-target floor. Grow it to
size-6 and adjust the MCP config row's top-margin centering (mt-3 ->
mt-2.5) so the larger control stays vertically centered in the h-11 rows
that clone McpRow geometry.
Titlebar buttons are already 24px (TITLEBAR_CONTROL_SIZE) and the
session-row controls belong to finding 3 (#38072), so they are out of
scope here. The composer coding-row git-branch glyph stays small by
deliberate density.
Refs #38072
Fit-on-open used the stage's full width, so a wide diagram landed about 4px
from the overlay edges while leaving hundreds of px empty above and below.
Fit into the stage minus a 32px side and 64px top/bottom inset. The vertical
inset also keeps fitted content clear of the floating toolbar, and the
symmetric insets keep the grid centering exact.
Main 0aa13a5e4b dropped the native title= from the Zoomable trigger
button (aria-label only) and moved the sibling zoomable.test.tsx to
getByRole, but zoomable-fit.test.tsx still queried getByTitle, so all
five fit tests failed on a rebased head. Same fix: query the accessible
button name.
The overlay opened at identity scale, so diagrams wider than the
viewport rendered off-canvas and looked unrendered until the user
zoomed out manually. The fit-to-stage transform was also unreachable
interactively: the reporter’s 4758px ERD needs a ~0.21 scale, below
the old MIN_SCALE floor of 0.25.
- useZoomPan: compute a fit transform (never upscale) from stage vs
content geometry, clamp it through the scale bounds, and expose
setter refs so portal-mounted stages subscribe correctly
- overlay: fit on open and on later geometry changes while the view
is still the fitted one; manual zoom/pan opts out until reset
- reset and smart-zoom now return to the fitted view
- MIN_SCALE 0.25 → 0.05 so fitted giant diagrams stay reachable by
wheel and buttons
- normalizeSvgSize: neutralise mermaid’s inline pixel max-width,
which beat the container width caps (max-w-full / 85vw) and forced
wide diagrams to render at intrinsic width in the inline preview
Closes#110336
Rebase resolution: main split the ar and zh-hant catalogs into
per-domain files, so the two hint keys move into ar_assistant.ts and
zh-hant_assistant.ts. Regenerated locales/_keys.desktop.json.
main gained de/es/fr locale catalogs after #100631 was drafted; add
the two new clarify hint keys there so every bundled locale carries
the full Translations shape.
Desktop rendered the same clarify affordance for both modes, so users
had to trial-click to learn whether more than one answer was allowed.
Fixes#100041
Main's menu template is MenuItemConstructorOptions[], so the local
WindowMenuItem interface (role?: string, submenu: Array<{role: string}>)
no longer satisfies it and tsc rejects the pushed item. Derive the type
from MenuItemConstructorOptions instead: role comes from Electron's own
role union (which includes 'windowMenu'), and submenu items are full
MenuItemConstructorOptions so the click-only Close item fits.
Also carry main's #65457 behaviour into the extracted template: the
non-macOS Close item is click-only with registerAccelerator: false, not
role: 'close' (which would claim CommandOrControl+W before the
before-input-event run that routes terminal-focused Ctrl+W to word
erase). Test updated to match.
Co-authored-by: victorjmilagres <294917029+victorjmilagres@users.noreply.github.com>
Re-apply of the import-order fix-up from the superseded branch: on today's
main the window-focus-policy import sits between the hunks, so the
perfectionist/sort-imports resolution is window-focus-policy →
window-menu → window-open-policy (eslint --fix verified clean).
Wide markdown content forced horizontal panning: fenced code in tool
bodies, user bubbles and the bot group room grew sideways scrollbars (the
group room clipped with no scroll affordance at all), and huge plain-text
fallbacks never wrapped.
- markdown-text: min-w-0 on the container so the flex chain can't grow
from its content; HugeTextFallback chunks soft-wrap
- compact-markdown (tool bodies): fenced code soft-wraps
- user-message-text: fences soft-wrap inside the bubble
- group-chat-view: room bodies soft-wrap pre instead of overflow-x-auto
(the clip with no scroll affordance, #91706 surface)
- tables keep content-sized auto layout, so cells wrap at word
boundaries; an unbreakable token scrolls inside the table card, like
code cards do, instead of squeezing every column letter by letter
Co-authored-by: yuexiong <xiongyue_hnu@163.com>
Co-authored-by: FalconOrtiz <falcon.ortiz11@gmail.com>
Co-authored-by: networthexplained <networthexplained@users.noreply.github.com>
The stale-fullscreen heuristic inferred staleness from geometry alone
(near-origin + >=90% of the work area), so a valid normal window the user
sized on purpose was rewritten to a centered 80% window with its position
dropped (review on window-state.ts:161).
Recovery no longer guesses:
- persistWindowState records boundsCapturedFullScreen at save time via
the existing save path (sanitizeWindowState schema, backward
compatible: absent on legacy snapshots, strict boolean like
isMaximized).
- Flagged snapshots recover on the work area they cover, as before.
- Legacy unflagged snapshots recover only on an unambiguous match:
bounds equal to a display's work area or full bounds within 2px,
never the >=90% shape.
The reviewer's exact case (1824x988 at (0,0) on a 1920x1040 work area) is
now preserved verbatim; the motivating legacy 1920x1040 snapshot and an
exact full-display-bounds snapshot still recover.
The dynamic imports inside 'lets the provider resolve and keep default'
deadlock under vitest's module runner (hangs at both the old PR head
and on main); import ./context and ./user-themes statically like
context.test.tsx does. 'keys default-named skin CSS' still asserted the
pre-#76579 semantics (default never registered); now the classic
default registers as its own palette carrying its customCSS and the
separate built-in CSS store stays empty for it.