Commit Graph

2806 Commits

Author SHA1 Message Date
Nicolas Formenton
c75d835555 feat(desktop): mark a session as unread/read with a persisted watermark 2026-08-15 01:21:40 -07:00
OpenClaw Agent
71e8c35285 fix(desktop): clear unread for the whole conversation family, don't re-light read sessions
The sidebar lights the finished-unread dot for every alias of a
conversation lineage (branch children + compression root), but reading
a session cleared only the exact row id — a branched/compressed
conversation kept dots lit on sibling rows no matter how often they
were opened. And any settled completion re-lit the dot even when the
user had already viewed the session since it finished.

- setSelectedStoredSessionId now clears unread for the whole family via
  lineageAliases, not just the selected id
- handleTransition only re-arms unread when the completion settles
  strictly after the user's last read of that session (new last-read
  baseline), so an already-viewed completion never re-lights
- openSession marks read at the very top, before any focus
  short-circuit, so re-clicking an already-visible session clears its
  dot (the original gap the sidebar click could not reach)

4635 desktop tests pass (incl. new family-clear, read-baseline, and
openSession-short-circuit cases); tsc typecheck clean.
2026-08-15 01:21:40 -07:00
gitong
1abeddacc1 fix(desktop): clear unread dot on tile open + add mark-as-read actions
The green 'finished-unread' dot only cleared when a session was opened via
main-thread resume (setSelectedStoredSessionId). Opening a session in a
tab/tile (middle-click / Cmd-click / tile strip) never cleared it, so the
dot stayed while the user was actively reading the session in a tile.

With a remote hermes serve backend the effect is amplified: session.info
transitions for every backend session (CLI/cron/kanban) mark unread in the
desktop client, so unread dots accumulate from sessions the user never
opened.

Changes:
- openSessionTile now calls markSessionRead(), so tile/tab open marks the
  session read (same as main-thread resume)
- new markSessionRead / markAllSessionsRead helpers in store/session,
  reused by setSelectedStoredSessionId
- 'Mark as read' per-row action in the session context menu (shown only
  while the row is unread)
- 'Mark all as read' header action in the recents sidebar (shown only
  when unread sessions exist)
- i18n: markRead (row scope), markAllRead (sidebar scope) in en/zh + types
2026-08-15 01:21:40 -07:00
Zeus-Deus
26b4315e09 fix(desktop): profile-scope persisted unread and stop cold-boot storage clobber
Address review on the persisted unread dots, plus a latent data-loss bug
in the shared persistence helper that the restart e2e exposed.

Review findings:

- Session ids are caller-supplied and each profile backend is its own
  namespace, while the desktop's lists routinely mix profiles (cron and
  messaging slices are always cross-profile; recents are too in
  all-profiles mode). Both persisted records are now bucketed per
  profile - nested records keyed by the ROW's own profile
  (normalizeProfileKey, absent -> default), never the live gateway's,
  except the live busy->idle edge with no loaded row, which can only
  come from the active gateway. Same-id sessions in different profiles
  no longer share watermarks or markers.
- Markers are now bounded (200 per profile, oldest evicted) and cleaned
  up when a session leaves the user's world: forgetSessionUnread() is
  wired into removeSession, archiveSession, and the settings
  permanent-delete path (which bypasses the other two).

Cold-boot clobber (found by the restart e2e after the refactor):

- persistentAtom wrote its value back to storage immediately at
  creation. On a cold boot the bundle can evaluate against a storage
  snapshot that has not caught up yet, so that echo overwrote real
  records with the fallback. Creation is now read-only; only actual
  changes persist. Regression-tested in persisted.test.ts.
- The read side of the same race is handled in session-unread.ts: the
  first list arrival re-reads both records from storage (readable by
  then) and merges them under the in-memory state, so a boot that
  seeded empty atoms adopts the disk state instead of re-seeding every
  row and burying the unread gap. Unread listeners are also disabled in
  secondary windows - their partial list view must not write the
  primary's whole-record state (same isolation rule as session tiles).

Tests: cross-profile same-id regression, live-edge profile fallback,
forgetSessionUnread cleanup, marker cap, persistentAtom creation
read-only; the restart e2e passes again end to end.
2026-08-15 01:21:40 -07:00
Zeus-Deus
d2c2b0b6d4 fix(desktop): persist sidebar unread dots across app restarts
The green "finished — unread" session dot lived only in the transient
$unreadFinishedSessionIds atom, written by a live busy->idle edge the
renderer had to witness. Closing and reopening the app grayed out every
dot, and a session that finished while the app was closed could never
be flagged at all.

Add a persisted layer (session-unread.ts), ported from the webui's
proven design:

- Seen watermarks (hermes.desktop.sessionSeenCounts): the message_count
  last acknowledged per session, keyed by the durable lineage id (same
  rule as session colors). A row whose live count exceeds its watermark
  paints unread on every list refresh - this reconstructs dots after a
  restart AND surfaces sessions that finished while the app was closed.
  First sight of an unknown session seeds the watermark so a fresh
  install doesn't light up every row.
- Explicit finish markers (hermes.desktop.unreadFinishedSessions): the
  live edge, persisted, covering the gap before the sidebar list
  refreshes its counts.

Opening a session acks both; the selected session's watermark tracks
its live count so on-screen activity never reads as unread. Chat and
cron rows get full watermark treatment; messaging rows keep explicit
markers only, so inbound messages don't paint false completion dots.
Profile switches keep persisted markers (keyed by durable id) and only
wipe the transient paint layer, so a round-trip repaints them.

Covered by store unit tests and an e2e spec that boots the app three
times: dot appears on a background finish, survives a restart, clears
on open, and stays cleared after another restart.
2026-08-15 01:21:40 -07:00
Jefftree
a5b50437e4 fix(desktop): key the completed-unread dot on the focused session, not the selected one 2026-08-15 01:21:40 -07:00
hermes-seaeye[bot]
77be513de1 fmt(js): npm run fix on merge (#86813)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-15 08:13:01 +00:00
Teknium
77fcc2ea31 feat(display): honor display.timestamps across desktop transcript and TUI
One config key everywhere (#41531): the same display.timestamps that stamps
[HH:MM] on classic-CLI labels now gates the desktop transcript's timeline
timestamps and renders dim [HH:MM] labels on TUI user/assistant rows.

- desktop: $displayTimestamps store fed from config.yaml via
  use-hermes-config; TimelineTimestamp renders nothing while the key is off
  (the default). Hover tooltips with the exact time stay ungated (#70450).
- TUI: tui_gateway forwards each persisted row's timestamp in the display
  projection; toTranscriptMessages threads it as Msg.createdAt; live rows
  are stamped at append (the #82840 rule); MessageLine shows a dim [HH:MM]
  above user/assistant rows when display.timestamps is on.
- No new config keys, no HERMES_* env vars; display-only, prompt-cache safe.
2026-08-15 01:04:19 -07:00
Chen Jin
4a663dd693 fix(desktop): sort imports to satisfy perfectionist lint (#84508) 2026-08-15 01:04:19 -07:00
Chen Jin
1dbeb47497 fix(desktop): timestamp every desktop.log / RECENT LOGS line
rememberLog prepended only '[hermes] ' to each line, so desktop.log and
the in-app RECENT LOGS view carried no timestamps while agent.log and
gateway.log (Python logging) did.

Extract the line format into a small pure helper (desktop-log-line.ts)
and prefix each line with an ISO-8601 UTC timestamp shared per chunk,
matching the Python-side convention. Regression tests assert the shape
contract: timestamp + [hermes] tag + verbatim message. Fixes #84405.
2026-08-15 01:04:19 -07:00
liguoyu
c9d0cbaba3 fix(desktop): show exact timestamps in tooltips (#70450) 2026-08-15 01:04:19 -07:00
Nick Morales
5f2a15a669 fix(desktop): separate system timestamp text 2026-08-15 01:04:19 -07:00
Nick Morales
a896322b46 feat(desktop): show detailed transcript timestamps 2026-08-15 01:04:19 -07:00
Teknium
aaaea589f6 test+style: align session.new binding pin with #76185 and eslint --fix 2026-08-15 01:04:06 -07:00
Teknium
a9361cfaae fix(desktop): add zh locale strings for the Disable F12 toggle
zh.ts is typed strictly against Translations, so the new
disableF12Title/Desc keys must exist there (other locales fall back via
defineLocale).
2026-08-15 01:04:06 -07:00
Teknium
dacb99d5b8 fix(desktop): keep the deliberate mod+N chord for session.new
#76185 removed both defaults; only the bare shift+n chord hijacks normal
typing (uppercase N / IME input outside an input field). Cmd/Ctrl+N is a
deliberate two-key chord that matches every browser and chat app, so it
stays. Follow-up narrowing of the salvaged fix.
2026-08-15 01:04:06 -07:00
kas-cor
3b5d5e48d1 fix: address review feedback on Disable F12 DevTools PR
- Add disableF12Title/disableF12Desc to i18n/types.ts contract
- Use focused BrowserWindow from menu click callback
- Persist and restore disable-f12 from main process (cold-launch)
2026-08-15 01:04:06 -07:00
Hermes Agent
d4fa3f7352 feat(desktop): add option to disable F12 DevTools shortcut
- Replace built-in menu role 'toggleDevTools' (which had F12 accelerator)
  with explicit menu item using Ctrl+Shift+I / Cmd+Opt+I only
- Add f12Blocked flag in main process, controllable via IPC
- Add 'Disable F12 DevTools' toggle in Settings → Advanced
- F12 still opens DevTools by default; toggle blocks it
- Ctrl+Shift+I (or Cmd+Opt+I on Mac) always works regardless
2026-08-15 01:04:06 -07:00
Brian Sweatt
89f9375bc0 feat(desktop): archive the current session via hotkey and ⌥+⇧-click
Adds a rebindable 'session.archive' keybind action (shipped unbound, like
session.togglePin) plus an ⌥+⇧-click gesture on sidebar session rows,
extracted into a pure, unit-tested click resolver so modifier precedence
(⌥⇧ archive vs ⇧ pin vs ⌘/⌃⇧ new window) stays correct.

Salvaged from #59759. Closes #59308.
2026-08-15 01:04:06 -07:00
273-B_L0
34f484542e fix(desktop): show platform Kanban modifier 2026-08-15 01:04:06 -07:00
273-B_L0
f61fb52f94 fix(desktop): show platform layout modifier 2026-08-15 01:04:06 -07:00
273-B_L0
a590321de2 fix(desktop): show platform commit shortcut 2026-08-15 01:04:06 -07:00
pierrenode
245efdaaa3 fix(desktop): dispose the HUD snap shortcut on native window close
9c75e4863f added the global ⌘⇧G snap-to-cursor shortcut and wired its
dispose() into closeHudWindow() and before-quit. It missed the HUD's
own 'closed' listener (spawnHudWindow's win.on('closed', ...)), which
fires when the window is closed from its own side — e.g. ⌘W — without
going through closeHudWindow() first.

After a ⌘W close, the shortcut stays registered with no HUD left to
apply it to: harmless (applyHudSnapToPointer guards on a destroyed/null
hudWindow) but it keeps CommandOrControl+Shift+G claimed until the HUD
is reopened (register() releases first) or the app quits.

dispose() is idempotent (guards on its own `active` chord), so calling
it unconditionally in the 'closed' handler is safe even on paths where
closeHudWindow() already released it.
2026-08-15 01:04:06 -07:00
Vladimir Rogozhin Assistant
101dfd782d Add PageUp and PageDown keybind support 2026-08-15 01:04:06 -07:00
BAS Lam
13acf7759e fix(desktop): remove session.new keybind hijacking typed N keys
The session.new default keybindings included 'mod+n' and 'shift+n',
which fired when users typed uppercase N (Shift+N) or accidentally
pressed Ctrl+N while using an IME to type Chinese — silently creating
new sessions mid-conversation.

Remove both combos so New Session is only reachable via the sidebar
button. Ctrl+Shift+N (session.newWindow) is unaffected.
2026-08-15 01:04:06 -07:00
Teknium
1c9de87ffb fix: drop duplicate composingRef declaration from rebase weave 2026-08-15 01:03:57 -07:00
Teknium
89df391580 fix(desktop): keybinds skip IME composition keys; pin draft survival across Settings navigation
- use-keybinds: bail out of the global keydown dispatcher while an IME
  composition is active. Windows Chinese IMEs use Ctrl+, as their
  punctuation toggle, which also matched nav.settings and navigated away
  mid-word — destroying the unsent composer draft (#41079).
- use-composer-draft.test: regression-pin the unmount-stash/remount-restore
  contract so route navigation (Settings) can never again drop an unsent
  draft with the React tree.
- use-composer-actions.test: the bounded-preview pipeline keys attachments
  to the durable path and resolves thumbnails asynchronously; the dropped-
  screenshot test now asserts the durable-path contract instead of the
  retired full-res previewUrl field.
2026-08-15 01:03:57 -07:00
Jakub Wolniewicz
d0642e61e5 fix(desktop): preserve legacy attachment replacements 2026-08-15 01:03:57 -07:00
Jakub Wolniewicz
e2a2fba4aa fix(desktop): preserve in-flight attachment replacements 2026-08-15 01:03:57 -07:00
Jakub Wolniewicz
462e7d8e6b fix(desktop): preserve tile attachment ownership 2026-08-15 01:03:57 -07:00
Jakub Wolniewicz
193c199b23 fix(desktop): make image attachment updates occurrence-safe 2026-08-15 01:03:57 -07:00
Jakub Wolniewicz
7d5e9757ec fix(desktop): preserve image preview ownership across drafts 2026-08-15 01:03:57 -07:00
Jakub Wolniewicz
6dfe63e1e2 fix(desktop): preserve image attachment occurrence ownership 2026-08-15 01:03:57 -07:00
Jakub Wolniewicz
5e09b4008b fix(desktop): serialize composer image previews 2026-08-15 01:03:57 -07:00
Jakub Wolniewicz
d345f0831f fix(desktop): bound multi-image thumbnail raster cost 2026-08-15 01:03:57 -07:00
David Metcalfe
485e1e8f69 fix(desktop): keep full-res previewUrl separate from downscaled thumbnail
Addresses review feedback on #68744: downscaling inside
attachmentPreviewDataUrl made previewUrl the 2048px PNG, but current main
(d8cb73b4ab) feeds that field to ImageLightbox and useImageDownload, so
large attachments would open and download at reduced resolution.

- attachmentPreviewDataUrl returns the full-resolution data URL again
- ComposerAttachment gains thumbnailUrl?: string (store/composer.ts)
- attachImagePath stores previewUrl (full-res) + thumbnailUrl (downscaled)
- Attachment pill renders thumbnailUrl ?? previewUrl; lightbox/download
  keep the full-res previewUrl
- optimisticAttachmentRef prefers thumbnailUrl for the in-flight bubble
  display ref (same main-thread decode freeze at send time)
- Attachment-level regression test in use-composer-actions.test.ts:
  full-res previewUrl preserved while thumbnailUrl is a separate
  downscaled value (4000x3000 -> 2048x1536)
2026-08-15 01:03:57 -07:00
David Metcalfe
24fc613318 fix(desktop): downscale large images in composer preview to prevent UI freeze 2026-08-15 01:03:57 -07:00
Johnny
0d447712a0 fix(desktop): keep composer stable across compression 2026-08-15 01:03:57 -07:00
Gille
224530211e fix(desktop): let composer status titles use row width 2026-08-15 01:03:57 -07:00
xrwang8
47970a9e4c fix(desktop): clear edit-composer submitting latch after 200ms cooldown
- Add submitting state + IME composition guard to prevent double-submit
- submitEdit() sets latch, then clears after 200ms timeout
- handleKeyDown() guards on composing so IME Enter doesn't submit
- Shift+Enter inserts newline without submitting
- Test validates Enter calls onEdit, latch clears for second session

Fixes #70771

Signed-off-by: xrwang8 <xrwang8@gmail.com>
2026-08-15 01:03:57 -07:00
mollusk
652ae4877a fix(desktop): let the edit composer scroll long prompts
The inline edit composer caps height at max-h-48 but had no overflow
rule, so long prompts were clipped with no way to reach the tail.
Add overflow-y-auto to match the main composer editor.
2026-08-15 01:03:57 -07:00
Lester Liang
49fbca1880 fix(desktop): prevent accidental composer popout 2026-08-15 01:03:57 -07:00
Denis
354abd7c57 fix(desktop): keep empty composer from collapsing
Fixes #68134\nFixes #68095
2026-08-15 01:03:57 -07:00
infinitycrew39
2a82c6ffbc style(desktop): align empty-state width with full chat column 2026-08-15 01:03:57 -07:00
infinitycrew39
006a4a8873 fix(desktop): restore full-width chat composer column 2026-08-15 01:03:57 -07:00
hermes-seaeye[bot]
af585de28e fmt(js): npm run fix on merge (#86801)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-15 07:46:57 +00:00
Teknium
03c85a1c7c test(desktop): port gateway-file-download tests to vitest electron project
The salvaged branch predates the electron test project's node:test -> vitest
migration (test:desktop:platforms is now `vitest run --project electron`).
Import `test` from vitest so the suites are collected; assertions stay on
node:assert/strict per the existing electron test convention.
2026-08-15 00:37:00 -07:00
Paul Armbruster
27b003996a fix(desktop): stream native gateway downloads + 404 data-url fallback
Addresses both review findings on the remote-gateway download PR:

1. Unbounded buffering (finding #1). fetchBuffer / fetchBufferViaOauthSession
   accumulated the entire response (then copied it again via Buffer.concat)
   before saveGatewayFile even opened the save dialog, so a large gateway file
   could exhaust the native process. Both auth paths now stream: once response
   headers arrive the connect timeout is cleared, the filename is derived, the
   save dialog is shown, and the body is piped to the chosen destination with
   backpressure. A read/write error tears down the stream and unlinks the
   partial file. The byte-moving, data-URL decoding, and filename/path helpers
   are extracted into gateway-file-download.ts so they're unit-testable without
   Electron.

2. No fallback for older gateways (finding #2). saveGatewayFile required the new
   /api/fs/download route. Desktop and the remote gateway update independently,
   so a gateway predating this PR 404s. Added a 404-only compatibility fallback
   to the existing capped /api/fs/read-data-url route (bounded, so it only
   serves smaller files — enough to keep older backends working).

Tests: gateway-file-download.test.ts covers streaming, backpressure,
error-cleanup (unlink on write/response error), data-URL decoding, filename
derivation (incl. traversal reduction), and 404 detection;
gateway-file-download-transport.test.ts asserts both transports stream (no
whole-body Buffer.concat) and that the 404 fallback is wired. Both registered
in the desktop platform test list. Server-side /api/fs/download tests
(streaming + sensitive-file reject) already pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-15 00:37:00 -07:00
Paul BlackSwan
d5a865882a fix(desktop): save remote gateway files natively 2026-08-15 00:37:00 -07:00
Dean Chen
0bc7070cf8 fix(desktop): gate image copy on decoded contents 2026-08-15 00:36:15 -07:00