Commit Graph

6611 Commits

Author SHA1 Message Date
hermes-seaeye[bot]
d0288be5b3 fmt(js): npm run fix on merge (#123423)
Some checks are pending
Live provider canaries / Live provider canaries (push) Waiting to run
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-26 03:51:55 +00:00
hermes-seaeye[bot]
aae6c2044e fmt(js): npm run fix on merge (#123390)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-09-26 03:39:27 +00:00
Hermes Agent
c15ebb1435 fix(desktop): drop stray blank lines inside theme import groups 2026-09-25 20:00:57 -05:00
Hermes Agent
ecaa1f001a test: disentangle the cherry-picked customCSS suites
The salvage cherry-pick landed nested/interleaved describes in
context.test.tsx — split them out so each suite has its own beforeEach,
and rename the local-bridge describe to what it actually covers. Use
the repo's hermes_yaml wrapper (safe_dump) in test_skin_engine.py where
plain 'yaml' isn't a guaranteed dependency.
2026-09-25 20:00:57 -05:00
David Metcalfe
37ee28d789 test: cover customCSS parsing, RPC payload, and style lifecycle
- backend-sync: built-in/default-named skins store CSS without shadowing
  the palette; empty field clears the entry; reset clears the store
- context: style tag insert / same-tag replace / removal lifecycle, plus
  mono-named and default-named user skin CSS application
- skin_engine: passthrough, whitespace strip, 32 KiB cap, empty default
- protocol: end-to-end resolve_skin() payload carries customCSS from YAML
2026-09-25 20:00:57 -05:00
David Metcalfe
c5c27188fa fix(desktop): carry customCSS for built-in-named user skins
ingestBackendSkin() deliberately skips skinToDesktopTheme() for 'default'
and BUILTIN_THEMES names so a user skin never shadows the desktop's
hand-tuned palette. That also dropped the skin's customCSS, contradicting
the documented precedence ('user skins in ~/.hermes/skins/ take precedence
over built-in skins with the same name'). Carry the CSS separately in a
 atom (keyed by the name the desktop resolves, default →
nous) and merge it into the derived theme, so the palette policy is
untouched but the user's CSS still reaches applyTheme.
2026-09-25 20:00:57 -05:00
David Metcalfe
c60ba75199 fix(desktop): pass skin customCSS through to desktop theme context
The web dashboard supports customCSS in theme YAML (PR #14776) but
the desktop app's skin pipeline dropped the field. Users who wanted
custom styling had to hack app.asar, which gets overwritten on every
update.

This adds customCSS passthrough through the full skin pipeline:

- HermesSkin (apps/shared) and DesktopTheme types gain customCSS?: string
- skinToDesktopTheme() passes skin.customCSS through
- applyTheme() injects a scoped <style id="hermes-desktop-custom-css">
  tag on theme apply, and removes it when switching to a CSS-less theme
- SkinConfig gets custom_css field, read from YAML as "customCSS"
- _build_skin_config() caps at 32 KiB (same as the web dashboard)
- resolve_skin() emits "customCSS" in the gateway JSON-RPC payload

Users now put CSS in ~/.hermes/skins/<name>.yaml under the customCSS
key — it persists across updates because the skin dir is outside app.asar.

Closes #53013
Refs #53012
2026-09-25 20:00:57 -05:00
Hermes Agent
66bc259712 fix(gateway): declare cwd_explicit on the session.create contract
The desktop ships cwd_explicit provenance with session.create (#52589),
but the wire contract (extra=forbid) rejected the unknown key, so every
remote-mode create failed with 'out of sync (different versions)' and the
composer never accepted the draft (Desktop core E2E remote-topology).
Local-backend lanes never sent it (a bare new chat is detached, no cwd).

Regenerate the shared contract TS + OpenRPC from the updated registry.
2026-09-25 20:00:33 -05:00
Hermes Agent
02b282a5d4 fix(desktop): stop approval.respond timing out behind stalled WS writes
approval.respond rode the generic 30s RPC deadline while the backend
honors an answer for the whole approvals.timeout window (default 300s).
A WebSocket write stalled behind a long LLM stream killed the client's
respond long before the backend would, surfacing the red
"request timed out: approval.respond" toast and apparently freezing
the session.

The RPC now carries an explicit 330s deadline (300s window + drain
margin), ambientRequestFor forwards per-call deadlines it was silently
dropping, and a deadline failure retries once — resolve_gateway_approval
pops the queue entry before committing, so a duplicate resolve is a
harmless resolved: 0.

Fixes #55433
2026-09-25 20:00:33 -05:00
Hermes Agent
a9972dc3f9 fix: profile terminal.cwd beats the desktop's inherited workspace cwd
A new chat bound to a named profile carried the desktop's app-global
workspace cwd (the launch profile's configured directory or the current
project scope) unconditionally, so a profile with its own terminal.cwd
was silently overridden on both create and resume.

The desktop now ships cwd_explicit provenance with the create RPC — true
only for a deliberate workspace pick (folder picker, an explicit lane or
tile target), false for the inherited default — and the gateway lets a
named profile's configured terminal.cwd win over a non-explicit client
cwd. An explicit pick still wins; a profile without a configured cwd
keeps the inherited workspace.

Fixes #52589

Co-authored-by: Sahil-SS9 <Sahil-SS9@users.noreply.github.com>
2026-09-25 20:00:33 -05:00
Hermes Agent
05426e04f8 fix(desktop): sort the system-ca import in main 2026-09-25 20:00:19 -05:00
Hermes Agent
2bc6cc421a fix(desktop): trust macOS keychain CAs for remote gateways like Windows
installWindowsSystemCaTrust only handled win32, so on macOS a remote
gateway fronted by a keychain-trusted private CA rendered fine but
every main-process Node https call failed with 'unable to get local
issuer certificate'. Generalize it to installSystemCaTrust covering
both stores tls.getCACertificates('system') can enumerate: the Windows
cert store and the macOS keychain (user + System roots, honoring the
SSL 'Always Trust' policy; Node >= 22.15). Linux stays excluded since
its 'system' store is the OpenSSL scan the default trust already
covers.

Fixes https://github.com/NousResearch/hermes-agent/issues/57241
2026-09-25 20:00:19 -05:00
Hermes Agent
a5e949bcef fix(desktop): sort imports in background-complete-event test 2026-09-25 20:00:12 -05:00
Hermes Agent
2658607d54 fix(desktop): import GatewayEvent type in the background.complete test
The salvaged test imported RpcEvent from @/types/hermes; main's event
type lives in @hermes/shared as GatewayEvent.
2026-09-25 20:00:12 -05:00
Hermes Agent
1f20117b33 fix(desktop): keep /bg's local action spec canonical over the catalog
The salvaged /background spec named the alias as canonical; the Python
registry's canonical is bg, so the catalog's exec placeholder (and the
canonical lookup) missed the local action and would have fallen through
to slash.exec. Name the spec /bg with /background as the alias, matching
how /btw sits in the local table. Also drops the /journey and /stop exec
assertions from the salvage (both contradict main's current routing).
2026-09-25 20:00:12 -05:00
liuhao1024
d73a542b77 fix(desktop): deliver /background results to the originating conversation
The desktop routed /background through the slash worker, whose HermesCLI
prints the completion from a fire-and-forget thread after process_command
already returned - past the worker's stdout capture window - so the
result never reached the conversation that started the task (#97635).

Route the command to the gateway's prompt.background RPC (the TUI's
path) and render the background.complete event into the originating
session's transcript, mirroring the TUI's [bg <task_id>] system line.
2026-09-25 20:00:12 -05:00
kshitijk4poor
916e1688ba test(desktop): tidy manual-overlay tests; guard the client no-command title 2026-09-26 06:23:43 +05:30
kshitijk4poor
78ea55eb27 refactor(desktop): drop the Update-everything backend skip; title refusals from the target
Review findings on this branch:

- The skip keyed on status.supported, which is can_apply == (install_method
  == "git"). That silenced docker/nix/apt backends too, which do return a
  real command, and it could act on a stale status after switching remotes.
  With the empty-command fix, a managed backend already lands on an honest
  message-only dialog, so the skip isn't needed.
- Every backend manual state comes from the refusal branch, so the
  apply.error "refused" flag always equalled isBackend. Stop writing a UI
  flag into the error field; ManualView picks the command-less title from
  isBackend.
- Normalise the backend command once instead of `|| null` / `|| undefined`.
2026-09-26 06:23:43 +05:30
kshitijk4poor
e66bc37a81 fix(desktop): translate the backend manual-update copy for de/es/fr/ru; cover it in the overlay test 2026-09-26 06:23:43 +05:30
Larry Pang
745bd737a9 fix(desktop): name the backend when a remote update needs a manual command
When the connected backend refuses a dashboard update and returns a real
command (docker/nix installs), the manual dialog said "You installed
Hermes from the command line" -- untrue for a remote backend. Use
backend-aware copy instead.

Salvaged from #78958. The PR's backend half (returning
recommended_update_command() from the managed-externally refusal) is not
carried: that branch only fires for hosted /opt/data and pip/docker
container installs, where the recommended command is exactly the update
the guard blocks. The earlier commit in this series returns an empty
command there instead.
2026-09-26 06:23:43 +05:30
kshitijk4poor
2e1d976235 fix(desktop): skip self-refusing backends in Update everything; title refusals honestly
- Update everything no longer calls the backend updater when the backend
  already reported supported=false (managed container, commit build); it
  surfaces the backend's own reason and still runs the client leg.
- A command-less backend refusal is titled "Can't update from here"
  instead of "Update from your terminal" (all locales).
2026-09-26 06:23:43 +05:30
kshitijk4poor
4c1fd5bc05 fix(update): stop rendering 'managed outside dashboard' as a shell command
Containerized dashboards refused updates with update_command set to the
prose 'managed outside dashboard'. Desktop renders update_command verbatim
as a copyable '$ <cmd>' line, so users got a fake command that fails with
'command not found: managed'.

- backend: managed-externally refusal and check return update_command=''
  (no runnable command), matching the commit-build refusal.
- desktop: treat an empty update_command as 'no command' (message-only
  manual view); fall back to 'hermes update' only when an older backend
  omits the field. Previously '' || 'hermes update' also showed a wrong
  command for commit builds.
2026-09-26 06:23:43 +05:30
Hermes Agent
8507948c04 fix(desktop): qualify lineage absorption members by profile (#92454)
`_lineage_ids` members were matched bare, so a kept row in another
profile whose stored id merely coincided with an incoming lineage was
evicted. Qualify members with the owning row's profile, matching the
identity and lineage keys in the same predicate.
2026-09-25 19:10:04 -05:00
Hermes Agent
1ec4957d4c fix(desktop): drop ghost sidebar rows absorbed by a reorganized compression chain (#85331)
mergeSessionPage's survivor filter kept any previous row whose id sat in the
keep set and that the incoming page did not return. The tip-rotation
lineage dedup (#43483) only matched through the lineage ROOT key, so
when a compression-chain reorganization minted a FRESH root id (manual
storage-format repair), an old SEGMENT row that was in the keep set (the
working/selected session at refresh time) produced the exact signal of a
legitimately-kept row: absent from the page, unmatched by lineage key.
It survived as a title-less ghost the backend never sent — the sidebar
rendered N+2 rows for a group the endpoints served as N.

Fix: the survivor filter now also drops a row whose id appears anywhere
inside an incoming row's `_lineage_ids` (the full chain the backend
projects, already served on list rows). That is absorption, not
staleness: a genuinely-kept row — pinned aged off the page, an
in-flight first turn — never has its id inside ANOTHER session's
lineage, so the protected survivor behaviors all hold (guards included).
No API change, no extra traffic; the data needed was already on the wire.

Regression tests: the reporter's exact unit repro (old segment in keep
after reorg mints a fresh root) and the pinned-aging-off guard with a
deep lineage on the incoming page.

The official repair-chains maintenance command from the report is a
separate feature (open PR #86743) and stays out of this fix.
2026-09-25 19:10:04 -05:00
Hermes Agent
3521034ed6 fix(desktop): isolate Inbox session actions from the row resume (#85163)
In card (Inbox) mode the actions cluster holding the ⋮ SessionActionsMenu
renders INSIDE SidebarRowBody — the row button whose plain-click onClick
resumes. Radix portals the menu content, but React synthetic events still
bubble through the logical parent, so an Archive menu click also fired the
row's resume: the archive RPC raced a re-open of the chat it was removing,
and the queued resume could transiently restore the row. Flat rows are
unaffected (their actions render outside the row button via the shell
actions column).

Two halves, same bug class:

1. stopPropagation (click + pointerdown) on the actions container in card
   mode. Container-level on purpose: every action owns its gesture; a future
   child that needs row semantics moves outside the boundary.

2. upsertResolvedSession now refuses to recache a resolved row that raced an
   archive/delete: while any identity (stored id, row id, lineage root) is
   tombstoned, when the backend row itself is archived, or when the
   tombstone lifecycle moved since the request started (ABA-safe — a failed
   archive rolls the tombstone back while the by-id response is in flight).
   Tombstone generations in store/session-removal make the ABA cycle
   detectable; membership alone cannot. The resolved row is still returned,
   so an explicit resume-by-id of archived history keeps working.

Component test drives the REAL Radix menu inside the REAL row: without the
stopPropagation, onResume fires twice (the reporter's exact symptom);
with it, onArchive fires once and onResume never.

Salvage of open PR #85166 by Jakub Wolniewicz, rebased onto current main:
tombstone generations live in store/session-removal (their home since the
projects/sessions store split), and the upsert guard keeps the hidden-row
off-list parking intact.

Co-authored-by: Jakub Wolniewicz <4850809+frizikk@users.noreply.github.com>
2026-09-25 19:10:04 -05:00
Hermes Agent
67c6fdc092 fix(desktop): stop home-directory repo scans when no roots are configured
An empty repo_scan_roots silently expanded to a bounded scan of the
user's entire home directory on every Desktop launch, with no way to
restrict the traversal short of disabling discovery outright. Empty
roots are now a safe no-op: users must explicitly configure
desktop.repo_scan_roots for filesystem scanning, and session-derived
projects remain available. The default config comment documents the
opt-in.

Fixes #53328

Co-authored-by: John Kim Querobines <jkim.querobines@gmail.com>
2026-09-25 19:09:03 -05:00
Hermes Agent
f8dddd7151 test(desktop): match the leaf name span now that badge chips wrap the row label
The effort-badge change wraps the model name span in a container span
alongside the badge chips, so SPAN text queries that match on composed
textContent hit two elements. Require a leaf span (no descendant span)
in the affected model-menu-panel queries.
2026-09-25 18:23:43 -05:00
Hermes Agent
29aa6dcca0 fix(desktop): render reasoning effort as a badge chip, not part of the model name
The model catalog menu appended the reasoning effort (and fast mode, and
variant tag) to the model name as plain tertiary text with only a
leading space, so a row read 'Qwen3.7 Max High' — the same model with a
High reasoning setting looked like a differently-named model.

Render each row-meta value as a discrete bordered chip beside the (now
independently truncating) name span, so the setting can't be mistaken
for part of the name.

Fixes https://github.com/NousResearch/hermes-agent/issues/51833

Co-Authored-By: PRATHAMESH75 <prathamesh290504@gmail.com>
2026-09-25 18:23:43 -05:00
Hermes Agent
5dcfd72a96 test(desktop): opt voice conversation sibling tests into read-aloud TTS 2026-09-25 18:19:41 -05:00
Hermes Agent
5d0182c5f0 fix(desktop): voice chat honors the read-aloud toggle (#44263)
The voice-conversation loop spoke every reply unconditionally: the per-turn
drive effect opened a live speech session as soon as a reply appeared,
regardless of the "Read replies aloud" setting. With the toggle off (and
voice.auto_tts false / tts.provider empty) a voice-chat turn still read the
reply aloud, and the mic could pick the speaker back up as phantom input.

Gate the loop at the same setting the toggle writes ($autoSpeakReplies,
seeded once from voice.auto_tts): when it is off, a completed reply is
consumed and the mic re-arms directly — speech recognition still works and
the reply stays text on screen; no TTS stream or sentence playback starts.
2026-09-25 18:19:41 -05:00
Hermes Agent
ab0e060bd7 fix(desktop): sort imports in git-binary-candidates test 2026-09-25 18:17:47 -05:00
Hermes Agent
a5e95fb27e fix(desktop): find UGit's bundled git.exe in the Windows resolver (#61494)
resolveGitBinary() only probed fixed candidate paths and PATH. A UGit
install keeps its Git-for-Windows copy under a versioned Electron app dir
(%LOCALAPPDATA%\UGit\app-<version>\resources\app\git\cmd\git.exe), which no
fixed candidate can name — and while the UGit installer adds that dir to
PATH, an Electron process launched from Explorer inherits the login-time
environment block, which can miss it. The update check's git spawn then
ENOENTs and "Check for updates" fails.

Extract the Windows candidate list into git-binary-candidates.ts with an
injectable fs, and enumerate the UGit app-* glob there (newest version
first, after the Hermes-bundled PortableGit and before the system-wide
defaults). resolveGitBinary() now selects from that list. Proven by a
unit test driving a fake filesystem — the reported layout resolves to the
UGit binary with nothing earlier on disk, and the test fails when the glob
is removed.
2026-09-25 18:17:47 -05:00
Hermes Agent
7131fe3f03 fix(desktop): warn when the GUI is older than the backend (reverse contract check)
reportBackendContract() warned only when the backend reported a contract
LOWER than the GUI's required value. The reverse skew — a GUI build older
than the backend it drives — passed silently. That happens in practice to
any long-running desktop app that survives a backend update without
relaunching; the stale GUI then drives newer gateway code and fails
cryptically downstream.

Warn in both directions:

- backend contract < required (existing): "Backend out of date" toast
  with the one-click backend update action, unchanged.
- backend contract > required (new): "Hermes app out of date" toast
  pointing at the client update overlay (openUpdateOverlayFor on the
  client target).

The new toast mirrors the existing skew toast's ergonomics: persistent
24h snooze on dismiss, immediate clear once the two sides align, and
snooze reset on alignment so a later skew warns right away. Each
direction dismisses the other's toast, so at most one skew warning is
ever live. i18n keys added to all shipped locales;
DESKTOP_BACKEND_CONTRACT docs in tui_gateway/server.py now describe both
directions.

Fixes #60542

Co-authored-by: Hermes Upgrade Staging <alex@vencounsel.com>
2026-09-25 18:06:54 -05:00
Hermes Agent
3273971a59 fix(desktop): keep Close and + on a lone workspace even when tabs are hidden
A lone workspace pane is stranded without its strip: Close still empties it
to a draft and + still opens a tab, so a chromeless workspace is a dead zone
for both handles. Treat any lone main pane as stranded, workspace included,
which also makes the auto-only lone-main clause unnecessary.

Approach from #107444 by @abundantbeing.

Co-authored-by: abundantbeing <beingsabundant@gmail.com>
2026-09-25 18:03:06 -05:00
Hermes Agent
39e20f4d55 fix(desktop): keep the tab strip on a lone workspace in auto mode
Every launch lands on a lone workspace chat, and auto mode hid its strip,
so the session tab and its "+" were gone until the app-wide default was
set to always. A lone main tile now keeps its strip on auto; Hide tabs and
an app-wide "never" still hide it. The siblingMainZone input and the
$mainTileZoneCount store it fed are no longer needed and are removed.
2026-09-25 18:03:06 -05:00
Hermes Agent
32b4acf4c8 fix(desktop): name the connected Telegram bot after QR setup
After a QR apply the success toast only said the gateway was restarting,
and reset() wiped the bot name, so the Quick setup card fell back to the
"already configured, will replace" warning with no sign of which bot was
just connected.

The toast now reads "Connected: @bot · Telegram saved; gateway
restarting…", and the card keeps a Connected badge with the @bot handle
in place of the replace warning until the next setup starts.

Co-authored-by: wangtao <wangtao@wangtaodeMacBook-Pro.local>
2026-09-25 17:41:21 -05:00
Hermes Agent
0fb7c95203 fix(desktop): give preview error states a Close that closes the tab
A preview tab whose file was moved or deleted rendered "Preview
unavailable" with nothing to click, so the only way out was the strip's
close glyph. The file read/PDF error states and the missing-artifact state
now offer a Close button wired to the tile's own closeTabPane, the same
close the strip and Cmd+W run.

Adapts the onClose threading from #93194.

Co-authored-by: Axl Ibiza, MBA <andrexibiza@gmail.com>
2026-09-25 17:39:11 -05:00
Hermes Agent
1f30bdbaff fix(desktop): add runtimeStartedAt to onboarding fixture and sort test import 2026-09-25 17:35:12 -05:00
Hermes Agent
81b0ee6323 test(desktop): resolve runtime timer salvage against current main
Adapt frizikk's runtime-session-timer tests to main's branchStoredSession
routing (branch_stored create), HEAD's statusbar timer harness, and seed
$sessionStates so the runtime cache anchor is observable.
2026-09-25 17:35:12 -05:00
Jakub Wolniewicz
2834a61726 docs(desktop): clarify runtime timer fallback 2026-09-25 17:35:12 -05:00
Jakub Wolniewicz
5769d06aab style(desktop): format runtime timer changes 2026-09-25 17:35:12 -05:00
Jakub Wolniewicz
6d3e852119 test(desktop): harden runtime timer regressions 2026-09-25 17:35:12 -05:00
Jakub Wolniewicz
cd0601a0df fix(desktop): sync runtime timer from session cache 2026-09-25 17:35:12 -05:00
Jakub Wolniewicz
868ca90218 fix(desktop): preserve runtime session timer across warm switches 2026-09-25 17:35:12 -05:00
Hermes Agent
e2ee9a6978 fix(desktop): reveal manual model-name input when /v1/models discovery is empty
The onboarding wizard's Local / custom endpoint path hard-failed on
OpenAI-compatible SaaS that doesn't expose an OpenAI-shaped /v1/models
catalog (Cohere's compatibility endpoint, auth-gated gateways): the probe
came back reachable-but-empty and the wizard rejected the save with
'Start a model on that endpoint' — no way forward except hand-editing
config.yaml, even though the runtime already supports these endpoints
via discover_models: false + an explicit models: list.

Turn the empty-models case into a recoverable flow: the save returns
{ ok: false, needsModelInput: true } and the form reveals a manual
model-name input (hidden on the happy path). A typed name is persisted
verbatim through the same provider=custom + base_url assignment.

Fixes https://github.com/NousResearch/hermes-agent/issues/47006

Co-Authored-By: David Metcalfe <dmetcalfe@users.noreply.github.com>
2026-09-25 17:35:00 -05:00
Hermes Agent
6231398b21 fix(desktop): never remember or restore a delegate subagent session
A delegate child (source='subagent') is invisible in the sidebar
(_LISTABLE_CHILD_SQL), so when its id lands in the remembered-session
slot the next cold start resumes an orphan chat while the sidebar
highlights the parent: every message silently goes to the child
(#56983). The remember path had no source check, and the restore path
validated ownership only — a child row that reached a list slice
(messaging aggregator, optimistic insert) passed both.

Guard both directions, keyed on source (not parent_session_id — /branch
children carry it too and ARE user-facing):

- Remember: a routed delegate row remembers its parent instead; an
  orphan child remembers nothing.
- Restore: a listed non-delegate row restores synchronously exactly as
  before; an unlisted id resolves by id (the by-id endpoint serves
  children the list omits), repairing a child to its parent, clearing
  an orphan/foreign id, and keeping the remembered value when the
  fetch itself fails.

Fixes #56983
Salvages #56988 (resolveRememberedSessionId + repair design, authored by baau)

Co-authored-by: baau <1347825413@qq.com>
2026-09-25 17:33:42 -05:00
Hermes Agent
a00fdda6c5 fix(desktop): skip lone-tilde escaping on directive lines; align prose tests with the unknown-tag escape 2026-09-25 17:33:25 -05:00
Albert.Zhou
25381c17d8 fix(desktop): escape lone tildes and unknown html-like tokens in markdown prose
Streamdown renders `1~10` / `~¥0.089` as GFM strikethrough pairs (two lone
tildes in one paragraph pair up and strike through the text between them),
and unknown HTML-shaped tokens like <tool_call>/<observation> are fed to
parse5 as unclosed tags, silently swallowing the rest of the message.

Escape lone tildes (preserving ~~strikethrough~~, ~~~ fences, autolink
paths, inline & fenced code) and escape unknown html-like tokens to
entities against a 55-element safe-tag allowlist, nested into the
rewriteProseSegment pipeline that already shields URLs/file links/math.
Includes preprocess unit tests and render-level DOM regression tests.
Closes #50871, #53953.
2026-09-25 17:33:25 -05:00
Hermes Agent
6dc19ad3c7 test(desktop): keep rebased checks green 2026-09-25 17:33:06 -05:00
brooklyn!
cc97da2888 fix(preview): refuse unfocused type and abort leftover keystrokes
After the locate click, type now refuses unless the located editable is
document.activeElement, so characters are not delivered as page input when
focus stayed on body. The keystroke loop checks an abort signal between
characters; preview.act cancel (timeout or interrupt) and a local Stop both
set it, so queued keystrokes stop. A printable press on body/html is refused
unless allow_shortcut is set.
2026-09-25 17:33:06 -05:00