Commit Graph

4089 Commits

Author SHA1 Message Date
Royalaid 3f590e68df perf(desktop): scope streaming-status invalidation below message root
Prong A of the wide-recalc fix (FINDINGS.md round-4 protocol): read status in
leaf components inside message content, hoist MessagePrimitive.Parts so status
flips cannot re-render the parts subtree. Behavior-identical; invalidation
scope only.

The third primary edit -- dropping the data-streaming root attribute -- is NOT
in this commit. The design doc calls it dead based on a CSS grep, and that grep
is correct (every [data-streaming='true'] rule targets [data-slot='code-card']).
But it has a live non-CSS consumer: scripts/run-short-session-hang-repro.mjs
:928 and :1023 count settled assistant rows via
[data-slot="aui_assistant-message-root"]:not([data-streaming="true"]) and gate
the assistant-response wait on that count growing. Deleting the attribute makes
that selector match every row, so the gate would pass at stream start instead of
completion. Held pending a decision rather than silently weakening the repro.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-21 04:23:41 -07:00
Teknium d47252547b fix: lazy session-states import is best-effort under partial module mocks
Test harnesses that vi.mock('@/hermes') without setApiRequestProfile make
the session-states transitive graph unloadable; the deferred reconcile
import then rejected unhandled and failed unrelated suites in shard 2.
Catch and skip — the production graph always loads.
2026-08-21 04:08:43 -07:00
Teknium 3a2a12b752 fix: break session-states import cycle via lazy import in reconnectSecondary
Static gateway.ts -> session-states.ts import closed a module cycle that
left $activeGatewayProfile undefined at session-states init (TypeError:
Cannot read properties of undefined (reading 'get')) — the CI red across
all three UI shards. Dynamic import defers the edge past module init;
reconcile semantics unchanged. Proven by re-adding the static edge:
hud/pet suites reproduce the exact CI failure.
2026-08-21 04:08:43 -07:00
Teknium 4f64807f5d fix(desktop): stale running arcs clear on gateway reconnect (#53902, #73082)
A respawned backend re-mints runtime ids, so a pre-reconnect busy state
never receives its terminal busy:false publish and its session stayed in
$workingSessionIds forever - the sidebar running arc and agents-panel
'running' chrome lied for hours after the turn ended (the stale-flag
half of #53902/#73082; the CSS cost half landed in #91383).

reconcileBusyStatesOnReconnect() downgrades busy/awaitingResponse states
through publishSessionState (watchdogs disarm, stall hints drop, settle/
unread bookkeeping stays consistent), scoped by event-source: the primary
reconnect touches only scope-less runtimes, a secondary (registry)
reconnect touches only its own connection's. needsInput survives - a
blocking prompt is the user's to answer. A genuinely live turn re-asserts
busy on its next post-reconnect event, so the worst case is one arc blink.

Regression tests proven by sabotage run (neutered reconcile -> 5/6 fail).
2026-08-21 04:08:43 -07:00
Teknium 443d4387b5 perf(desktop): idle renderers stop burning CPU on infinite CSS animations (#53902, #73082)
The arc-border running indicator animated background-position (repaint
every frame: ~3,600 main-thread style recalcs/min per arc, ~4,100ms/min
of renderer task time measured over 60s of true idle) and progress-slide
animated left (forced layout every frame). Both now travel via transform
on the compositor: same visuals, 3,617 -> 79 style recalcs/min and
4,108 -> 153 ms/min task time in the same harness (-96%).
2026-08-21 03:04:57 -07:00
Teknium 0a8cdec697 fix(desktop): silence wsl.exe stderr banner + detached explorer relaunch rung
Follow-ups to the salvaged WSL-bridge gating (#66447):
- wsl-path-bridge.ts: discard wsl.exe stderr so the 'WSL is not installed'
  banner can never leak into an attached console on WSL-less machines (#80184).
- scripts/desktop-update/windows.ps1: add an explorer.exe-mediated detached
  relaunch rung between the WMI attempt and the tethered Start-Process
  fallback. When Win32_Process.Create fails (observed ReturnValue 8), the
  Desktop no longer re-attaches to the hand-off console, so its stdout stops
  flooding the window and the console can close.
2026-08-21 00:49:41 -07:00
Tranquil-Flow deec043276 fix(desktop): scope WSL bridge state by profile 2026-08-21 00:49:41 -07:00
Tranquil-Flow b634032fa4 fix(desktop): gate WSL bridge for remote backends
Seed backend-mode state before creating the first window and update it after runtime resolution. Keep remote reconnects gated until a local backend is confirmed.

Cover Windows child-process suppression and bridge state transitions.
2026-08-21 00:49:41 -07:00
Brooklyn Nicholson 524b062289 fix(desktop): don't re-list a just-unpinned session under Pinned
The Pinned section falls back to the server `pinned` flag for rows the
local set doesn't hold, so a backend pin stays reachable when
localStorage is cold (#85969). But an unpin leaves the local set the
instant the user clicks, while the loaded row keeps reporting
`pinned: true` until a page issued after the PATCH lands — so the
fallback read the user's own unpin as a foreign pin, parked the session
at the bottom of Pinned, and only released it a refresh cycle later.

session-pin-sync already knows which rows its own in-flight writes
contradict; publish that fence and have the fallback skip them.
2026-08-21 01:19:15 -05:00
brooklyn! 02e270a47e fix: editing a message in an old session fails (profile DB + window-relative ordinal) (#91302)
* fix(gateway): persist prompt.submit truncation to the session's own profile DB

`_get_db()` returns the LAUNCH profile's SessionDB handle. App-global
remote mode gives a session its own profile (`session["profile_home"]`)
whose transcript lives in that profile's `state.db`, so a write keyed on
`session_key` that goes through `_get_db()` addresses the wrong database.

In the `prompt.submit` truncate branch that has two consequences. The
edit/resend never sticks — `session.resume` reopens the profile db and
resurrects the undone turns — and when the launch profile happens to hold
a row under the same session id, the truncated transcript is inserted
into a profile the session does not belong to.

It also silently voids the branch's own fail-closed contract. The handler
persists before it rewrites `session["history"]` precisely so that a
failed write refuses the turn and leaves memory and DB aligned; that only
holds if the handle it checks is the one that owns the row.

`_session_db(session)` is the profile-aware resolver that already exists
for this: the profile's `state.db` when `profile_home` is set, otherwise
the shared launch handle. Non-profile sessions are unaffected —
`_session_db` borrows the same shared handle and leaves it open.

`active_only=True` and `archive_dropped=True` are carried through
unchanged; only the handle the call is made against changes.

* fix(gateway): resolve the /undo command against the session's own profile DB

`command.dispatch`'s `/undo` branch opened the launch profile's handle via
`_get_db()`, but every read and write under it is scoped by session id:
`list_recent_user_messages`, `rewind_to_message` and the
`get_messages_as_conversation` reload all key on `session_key`.

For a session with its own profile (`session["profile_home"]`) the rows
live in that profile's `state.db`, so against the launch handle
`list_recent_user_messages` returns nothing and the command fails closed
with `4018 "no user messages to undo"` — for the entire session, on every
invocation, even though the transcript is right there in the profile db.

Route the whole branch through `_session_db(session)`, which yields the db
that owns the session's row and closes a profile handle on exit. Sessions
without a profile keep borrowing the shared launch handle exactly as
before, so this is behaviourally identical for them.

* fix(gateway): read /history and /context from the session's own profile DB

`_format_live_history_output` and `_format_live_context_output` rebuild the
transcript from the database rather than from `session["history"]`, because
the in-memory list is empty for a session this process did not run itself.
Both reads are scoped by session id but were issued against `_get_db()`,
the launch profile's handle.

A session with its own profile (`session["profile_home"]`) keeps its rows
in that profile's `state.db`, so both reads come back empty and the
commands under-report: `/history` renders "No conversation history yet."
and `/context` falls back to the empty in-memory list and reports a
conversation of zero messages. Both swallow their exceptions, so there is
no error either — just a wrong answer about the user's own transcript.

Resolve both through `_session_db(session)`, the profile-aware resolver
used by the rest of the session-scoped paths.

* test(gateway): cover session-scoped transcript ops against a profile DB

Regression coverage for the three session-scoped sites that resolved
against the launch profile's handle instead of the db owning the
session's row. Each test drives the real JSON-RPC entry point with a
session carrying `profile_home`, seeds the transcript into the profile's
own `state.db`, and asserts against both databases.

Per site, with the production change reverted to its pre-fix form:

- `prompt.submit` truncation — `test_truncation_persists_to_the_profile_db`
  and `test_truncation_does_not_copy_rows_into_the_launch_profile` fail.
  The second seeds a row under the same session id in the launch db so the
  foreign write succeeds instead of failing a key check, which is the case
  that copies a transcript into a profile it does not belong to.
- `/undo` — `test_undo_rewinds_the_profile_transcript` fails with
  `4018 "no user messages to undo"`.
- `/history` and `/context` — `test_history_reads_the_profile_transcript`
  and `test_context_reads_the_profile_transcript` fail, reporting an empty
  conversation.

`test_undo_still_uses_the_shared_handle_without_a_profile` and
`test_truncation_without_a_profile_uses_the_shared_handle` pin the
unchanged path: with no `profile_home` the resolver must borrow the shared
launch handle and leave it open. Both stay green in every direction, so a
future change cannot satisfy the profile cases by abandoning the shared
one.

* fix(desktop): aim truncations by durable id alone on tail-only transcripts

The cold-open transcript is a newest-first prefetch page
(LATEST_SESSION_MESSAGES_LIMIT = 120) with the resume RPC sent
omit_messages — older rows only arrive via "Show earlier" backfill.
planEdit/planReload/planRestore still counted truncate ordinals over
that windowed list, so every edit/reload/restore in a session longer
than the prefetch page sent a window-relative ordinal alongside the
durable row/message id. The gateway's #82959 cross-check resolved the
durable id to its full-history ordinal, read the offset as drift, and
refused with 4030 — making the Edit affordance permanently dead in
long sessions.

When the transcript may be tail-only (the transcript-tail
bookkeeping's possiblyTruncated), drop the client ordinal and address
the truncation by durable id alone — the same rule runRewindSubmit
already applies to content-resolved row ids (#87059). The ordinal
tripwire stays on whenever the transcript is complete.

Closes #88082

* fix(desktop): drop client rewind ordinal whenever a durable id is present

#88092 gated the drop on tail-only prefetch. After in-place compact the
live scrollback is treated as complete, so Restore still sent a
display-lineage ordinal next to a resolved row id and the gateway
refused with 4030 (#89244). prefix_user_count is structurally 0 on
in-place because get_ancestor_display_prefix is cross-session.

Same choke point: if a durable truncate_before_row_id or a real
truncate_before_message_id is present, omit the client ordinal.
confirm_empty_truncate is still carried from a caller ordinal of 0.
Unknown ids still fail closed at 4018.

Closes #89244

---------

Co-authored-by: briandevans <252620095+briandevans@users.noreply.github.com>
Co-authored-by: zengzheqing <yuntianqing@yahoo.com>
2026-08-21 06:02:19 +00:00
brooklyn! 6e64e6b9c6 fix(desktop): Windows glass windows stop rendering when they lose focus (#91307)
* fix(desktop): stop layering Windows glass windows, and don't make them transparent

A Windows chat window under glass rendered while focused and went dead the
moment it lost it. Two things put it on a compositing path DWM will not draw
acrylic behind, both no-ops that looked free:

`opacity: windowOpacity()` was passed on every window. Under glass on Windows
fade is 0, so the value is always 1 — but Electron's `SetOpacity` calls
`SetLayered()` and `SetLayeredWindowAttributes(..., LWA_ALPHA)` before it looks
at the value, and nothing ever takes `WS_EX_LAYERED` back off. A layered window
composites through the legacy redirection surface, which Windows documents as
mutually exclusive with `UpdateLayeredWindow`. Opacity is now only passed when
the state actually fades, and the runtime path keeps setting it for a window
that is already faded so it can still come back to opaque.

`transparent: true` was set on every glass-capable Windows chat window, on the
premise that DWM materials only reach the client area that way (electron#49443,
which was closed as need-info against an EOL Electron 28). They do not need it:
`IsTranslucent` answers yes off `background_material_` alone, which is what
gives the page its transparent default backing, and `SetBackgroundMaterial`
flips widget translucency live. Its one gate is a frameless window, and
`titleBarStyle: 'hidden'` already satisfies it. What `transparent` did add was
permanent — the widget pinned to kTranslucent for the window's whole life, so
even glass-OFF windows paid a DirectComposition redraw per frame
(electron#39895), plus the documented transparent-window limits, including that
a resizable transparent window is unsupported and breaks (electron#48421).

Both landed latent in #89837 and only surfaced when #90587 turned glass on by
default and dropped the opaque backing that had been hiding them.

* docs(desktop): name the one thing the opacity guard cannot undo

Electron exposes no way back off WS_EX_LAYERED, so a Windows window that has
been faded once keeps the layered compositing path until it is recreated. Not
opening the door on the default path is the whole of the fix; say so where the
guard lives rather than leaving a reviewer to work out the gap.
2026-08-21 05:57:14 +00:00
Teknium 7af4b8ee92 test(bot-mode): drop unused stub param flagged by eslint 2026-08-20 21:54:18 -07:00
Teknium c6745da72b feat(bot-mode): durable room identity + full gateway fan-out for group-room sync
Resolves the room-lifecycle class on top of the salvaged #89369 projection:

- v3 projection keys rooms by immutable roomId (id:<roomId>) with
  name:<name> fallback for legacy rooms; v1/v2 envelopes are normalized
  on read so mixed-version fleets share one merge path
- rename is now a same-key field update — no distributed delete+create,
  no old-name resurrection from lagging gateways
- id tombstones are FINAL (ids are never reused), so a gateway that was
  offline during a disband can never resurrect the room, regardless of
  the revision its stale copy carries; same-name recreation is unaffected
  because it mints a fresh roomId
- the projection fans out to EVERY reachable default-profile gateway
  (per-gateway job queues, CAS revision streams, backoff and retry caps),
  so rooms survive any single gateway dying and surface on gateway-only
  clients without waiting for a Desktop to foreground that gateway
- cold hydrate follows a remote rename via roomId instead of duplicating
  the room under both names

New tests: id-keyed rename continuity, final id-tombstones vs lagging
high-revision copies, rename-job shape (changed+deleted same key),
cold-hydrate re-keying, multi-gateway fan-out. Sabotage-verified: each
new test fails against the pre-class behavior.
2026-08-20 21:54:18 -07:00
Teknium 5d75a46645 fix(bot-mode): stabilize sync entry identity across gateway round-trips
Two follow-ups to the salvaged #89369 base against current main:
- compact projection entries no longer overwrite the local rich copy
  (attachments survive; watermark accounting stays stable)
- synthetic legacy-N thread ids collapse to one bucket in the entry key,
  so id-less entries don't duplicate after a pull and manufacture
  phantom member turns into busy sessions
2026-08-20 21:54:18 -07:00
TheTom 3b83b5e1fa fix(gateway): serialize shared bot room updates 2026-08-20 21:54:18 -07:00
TheTom 7656a2ad46 fix(desktop): hydrate shared bot room previews 2026-08-20 21:54:18 -07:00
TheTom 64d393cb7e fix(desktop): sync bot room previews through profiles 2026-08-20 21:54:18 -07:00
hermes-seaeye[bot] 949aff69a8 fmt(js): npm run fix on merge (#91256)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-21 02:51:33 +00:00
Jaaneek 95057c2a63 feat(desktop,web): enable the React Compiler in both renderer builds
Wire babel-plugin-react-compiler through @vitejs/plugin-react v6's
reactCompilerPreset + @rolldown/plugin-babel in the web and desktop
vite configs, scoped to modules that can actually contain components
or hooks (JSX syntax or a react-ish import — the preset's default
filter babel-parsed every TS module). Both vitest configs run compiled
components, so rules-of-react violations fail in CI.

Also fixes the latent bug the compiler exposed: usePluginI18n kept a
stable translator identity over a mutating locale registry, so
memoized consumers (React.memo today, compiled components tomorrow)
served stale strings after a late bundle registration. The registry
version now keys the translator identity — correct with or without
the compiler.
2026-08-20 19:44:26 -07:00
zhongwater123 ed2d01a3c3 fix(desktop): dedupe profile mention completions 2026-08-20 19:42:09 -07:00
Mengfan ba501f02ce fix(desktop): Bot Mode mention readers miss the connection-suffixed roster cache
useRoster() keys its query on [...ROSTER_KEY, connectionId] so every
connection the window has been on gets its own cache entry, but the two
imperative roster readers — the @mention completions and the mention
middleware — still called getQueryData(ROSTER_KEY). TanStack Query's
getQueryData is an exact-key match, so the bare key matched nothing and
both readers saw an empty roster: the composer offered no agent handles,
and remote @name-device mentions (e.g. @default-vera for a same-named
profile on an SSH connection) fell through to the local profiles.list
fallback, which only knows bare local names — the mention passed through
untouched to the local agent instead of being routed over Connections.

Add a cachedRoster() helper: honor the legacy bare-key write first, then
scan the key family with getQueriesData, preferring the active
connection's entry and falling back to any other entry (a stale roster
beats none). Never throws, matching the readers' existing posture.

Regression tests model the real cache shape — entries under the suffixed
key, exact-key reads missing them — so the key-shape mismatch is pinned;
the previous tests stubbed getQueryData to ignore keys, which is exactly
why this slipped through.
2026-08-20 19:42:00 -07:00
Gille 76952ba54f fix(desktop): follow focused bot for mentions 2026-08-20 19:12:39 -07:00
ArthurFranckPat 770f9a5747 fix(desktop): read the live union roster in mention-completions + mention middleware
useRoster caches under [...ROSTER_KEY, activeConnectionId] — a 3-element
key — but the composer @ autocomplete provider and the mention-routing
middleware both read getQueryData(ROSTER_KEY) with the bare 2-element
key. An exact-match lookup against a 3-element cache key returns
undefined forever, so:

- @ autocomplete offered ZERO bot rows (local and remote alike)
- remote-bot mentions never matched the union roster and fell back to a
  local-only profiles.list, so @name-device mentions did not route

That is issue #89303: the remote-source Bot row tells the user to
`@name-device` the agent, and the completion surface never offers it.

Fix: cachedUnionRoster() — read the live connection's cache entry first
(getQueryData with the full 3-element key), fall back to a prefix scan
(getQueriesData) keeping the freshest snapshot. Never throws; callers
keep their existing cold-cache fallbacks.

The plugin test mocked getQueryData as key-agnostic, which is why this
never failed in CI: the mock now reproduces the real QueryClient v5
exact-key semantics (a bare ROSTER_KEY lookup misses the 3-element
cache), and the tests fail against the old code (verified: 2 failures
before the plugin fix, 9/9 pass after).

Fixes #89303
2026-08-20 19:12:39 -07:00
Teknium e30388e409 fix: retained group panes re-anchor to the latest message on reopen
Keep-alive workspace panes (#89788) stay mounted while hidden, so returning
to an already-open room never remounted GroupChatWorkspace and the
mount-time bottom anchor didn't rerun — reopening group A after visiting
group B left A at the old scroll position. GroupChatMainView now subscribes
to the pane's visibility via feature-detected host.paneVisibility (always-
visible atom fallback for older SDKs) and the workspace scrolls the bottom
sentinel into view on the hidden→visible edge. Repro and fix direction from
the live-audit review comment on #90526.

Also updates the composer shape test to slice only GroupMentionInput
(GroupClarifyCard, merged since the branch was cut, legitimately uses
Input) and teaches the legacy-SDK react proxy about useMemo.
2026-08-20 14:25:50 -07:00
Teknium 3c5fb284e5 fix(bot-mode): group-room UX cluster — Shift+Enter newlines, open-at-latest, late replies harvested (#89884, #89835, #89545)
Composer was a single-line Input (Enter always submitted, newlines
impossible) — now the SDK Textarea with Enter=send / Shift+Enter=newline
and popover-first key handling. Room log had no scroll anchoring (opened
at position 0) — bottom sentinel + near-bottom-guarded anchor effect.
Stranded replies were only harvested inside an active turn loop (stuck
until the user's next send) — the settle path now runs a bounded
background harvest that yields to a live loop.
2026-08-20 14:25:50 -07:00
Bikash Joshi 693d6341b7 fix(desktop): expose group deletion from roster rows 2026-08-20 14:14:51 -07:00
Sara Burke ddc78217fd Merge current Bot Mode group fixes
Carry the reviewed hybrid routing branch through current main at 603d5651b,
preserving fresh room-session identity and prior signed history.
2026-08-20 16:55:00 -04:00
Teknium d1ea11d589 fix: disband tombstones no longer hold the group name or leak into storage
Found in live GUI E2E of the roomId salvage: disbanding a room while a
drive is mid-turn leaves an epoch-bump tombstone under the room's key.
Two defects surfaced: (1) updateGroupChat persists the WHOLE atom map, so
the next unrelated room write persisted the tombstone as an empty room;
(2) create/rename collision sets counted the tombstone's key, so
recreating the group under the same display name silently became
'<name> 2'. Tombstones are now flagged, excluded from the durable map,
from create/rename collision sets (liveGroupChatNames), and from roster
rows. Regression coverage in the existing tombstone test.
2026-08-20 13:53:28 -07:00
Teknium 92b6da23bc fix: disband confirm copy no longer hardcodes the legacy session title
New rooms title member sessions 'Group: <roomId>'; the dialog copy now
describes the kept sessions generically instead of quoting a title that is
wrong for post-roomId rooms.
2026-08-20 13:53:28 -07:00
YuYigeng 3752948bb7 fix(bot-mode): recreating a same-name group mints fresh member sessions 2026-08-20 13:53:28 -07:00
Sara Burke ba42346e99 Merge latest upstream into hybrid remote Bot routing
Carry the reviewed hybrid Bot routing branch through current main at
4a5b6dd45 without rewriting prior signed commits.
2026-08-20 16:52:28 -04:00
Brooklyn Nicholson 4a5b6dd451 feat(desktop): a connector consent card that knows nothing about connectors
The visual half of asking someone to connect a thing: the mark, the card,
the settled scaffold line it collapses to, and the trust badge.

It renders a subject, a state, and an outcome, and calls back. What a
connector IS, how one connects, and where the strings come from all stay
with the caller — the prop types are structural, so a richer type satisfies
them without this layer importing it. That boundary is the point: any
surface that has to ask "connect this?" should look identical without
sharing a data layer.

MarkdownLinkText comes along because setup steps need inline links.
LinkifiedText can't serve them — it finds bare URLs and guesses a label,
and here the label carries the meaning while the URL is a console page
whose own title is useless or, behind a login wall, actively wrong.
2026-08-20 14:07:53 -05:00
Brooklyn Nicholson 73d1041000 feat(desktop): resolve a site's own favicon in the main process
A name with no bundled brand glyph had nothing to render, and the renderer
could not go find one — CORS makes another origin's markup unreadable.

So resolution moves to the main process: read the page, collect every icon
it declares in its head and web manifest, rank them, sniff the bytes to
catch a challenge page served as image/png, and cache the answer per host
on disk. Only ever the site's own marks — a public icon service would
answer for the hosts behind a bot wall, but asking one means telling a
third party which tools someone is wiring up, so an unreadable site keeps
its monogram instead.
2026-08-20 14:07:53 -05:00
Brooklyn Nicholson 9ed308f901 refactor(desktop): one identity chip behind every avatar
Messaging platforms, the MCP servers tab, and the connector surfaces each
drew their own square brand chip, and the three copies had already drifted
on radius, glyph size, and what an unknown name falls back to.

They now share one AvatarChip. Size and radius stay the caller's call;
everything else is fixed in one place, so a Slack row and a Linear card
wear the same mark. brandFor also stops caring how a name is spelled —
catalog slug, registry id, or display name all reach the same glyph.
2026-08-20 14:07:53 -05:00
Brooklyn Nicholson 2e1e3cc91d feat(desktop): let plugins switch the theme from outside React
The SDK could contribute a theme through `THEMES_AREA` but never select one,
and `useTheme().setTheme` needs a component to hang the hook on. A plugin that
repaints on an event — a gateway coming up, a socket message — had nowhere to
call, so shipping one meant patching the app and re-patching it after every
upgrade.

`requestTheme(name)` writes to the same one-shot channel the backend `/skin`
sync already uses, so the ThemeProvider drains it through `setTheme` and an
imperative switch normalizes and persists per profile exactly like a manual
pick — one policy, one owner.

An unresolvable name is refused rather than coerced. `setTheme` falls back to
the default skin, which is right for a person picking off a list and wrong for
code reacting to an event: a gateway naming a theme the user never installed
would silently reset an appearance the caller never meant to touch. The
returned boolean doubles as the availability check.
2026-08-20 13:30:31 -05:00
hermes-seaeye[bot] 63c6d9a45c fmt(js): npm run fix on merge (#91000)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-20 18:15:16 +00:00
fangliquan 065c8bd6c0 test(updater): cover handoff marker edge cases 2026-08-20 12:52:21 -05:00
fangliquanflq fe4515da69 fix(desktop): add curly braces in handoff marker test helpers
ESLint curly rule requires block bodies for if/else in runPosix and runWindows.
2026-08-20 12:52:21 -05:00
fangliquanflq 60f0a4f2db fix(desktop): coerce spawn output to string in handoff marker tests
spawnSync stdout/stderr can be string | Buffer; assert.equal expects string | Error. Wrap with String() so check:lint typecheck passes in CI.
2026-08-20 12:52:21 -05:00
fangliquanflq ee6a9f8326 fix(updater): carry acquisition age through scripts 2026-08-20 12:52:21 -05:00
fangliquan dbc2a9c8e9 fix(updater): preserve holder age across handoffs 2026-08-20 12:51:51 -05:00
Brooklyn Nicholson aa08d0fdf4 fix(desktop): session skeletons stand on the same edge as the rows they become
The loading placeholder had its own copy of the row grid — its own min-height,
its own two columns, `pl-2` and no trailing inset — so its right-hand block sat
several pixels off from where the real rows land. The list stepped sideways as
sessions resolved.

Compose the shared row chrome instead, which is where that inset lives.
2026-08-20 11:35:30 -05:00
Brooklyn Nicholson 91670103fd fix(desktop): pagination ellipses land on the same sidebar edge as the rows
"Load more" and a workspace's "show more" hang off the bottom of a list rather
than sitting in a row, so they never saw the shell's trailing inset and stayed
flush against the edge every row above them now stops short of.
2026-08-20 11:15:34 -05:00
Brooklyn Nicholson 60ff62cb13 refactor(desktop): build the cron sidebar row from the shared row chrome
The cron row had its own copy of the row grid — its own min-height, its own
`grid-cols-[minmax(0,1fr)_auto]`, its own `pl-2 pr-1`, and a comment explaining
that the numbers were chosen by hand to line up with the session rows above it.
They had already drifted apart on the right edge.

Compose SidebarRowShell / SidebarRowBody / SidebarRowLead / SidebarRowLabel
instead, so a cron job and a session share one definition of what a row is and
cannot drift again.
2026-08-20 11:15:30 -05:00
Brooklyn Nicholson b6d21b37b6 fix(desktop): sidebar row trailing inset, so the working arc stops clipping the age
SidebarRowShell owned the row's height and, through the body, its leading
inset — but nothing owned the trailing one. The actions slot rendered with no
padding, so the age, chips and kebab sat on the row's border box. That is the
same pixel a working row paints its arc on (`.arc-row` sets `--arc-standoff:
0rem`), so the animation ran straight through the text.

Give the shell that inset. It is the only box containing both the one-line
row's actions column and the card variant's in-body cluster, so one class
covers every trailing thing a row can render. The card drops the body's
label-to-actions gap in exchange: it has no such column to clear, and keeping
the gap would pull its header in past every line below it.
2026-08-20 11:15:27 -05:00
Sara Burke 052513f65b Merge current upstream into hybrid remote Bot routing
Adopt upstream's one-hidden-forever-chat model and remove the per-bot hidden
session browser while preserving connection-bound remote Bot opening, the full
remote action menu, same-name isolation, and global Sessions navigation.
2026-08-20 10:26:19 -04:00
hermes-seaeye[bot] f43eabee5f fmt(js): npm run fix on merge (#90822)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-20 13:13:15 +00:00
hermes-seaeye[bot] e2c4793efa fmt(js): npm run fix on merge (#90818)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-08-20 12:56:20 +00:00
Teknium 2123a01601 fix: disbanded group chats no longer resurface as empty roster rows (Bot Mode)
Disband removed the room log and each member's group membership, but
BotsPane's mergeServerMeta then overlaid the STALE cached roster snapshot
(fetched before the disband) whose ui_meta['hermes-bots'] still carried the
old groups array — spreading it back over local meta and re-listing the
group as an empty row. Any later meta write for the bot (pin, title,
canonical-chat pointer) re-uploaded the resurrected membership server-side,
making the ghost permanent. Same stale-overlay class could revert renames,
re-group left members, and undo pins/hides.

Fix (class-level, not per-field):
- saveBotMeta stamps a per-bot last-local-write time (re-stamped when the
  profiles.configure write settles).
- useRoster stamps each snapshot with its fetch ISSUE time (fetchedAt).
- mergeServerMeta skips overlaying any bot whose local write post-dates the
  snapshot; the next (fresh) fetch overlays normally, so server truth still
  gets the last word. No-fetchedAt callers behave exactly as before.
- disbandGroupChat / renameGroupChat invalidate the roster query so all
  surfaces converge on a fresh snapshot immediately.

Tests: new regression (stale snapshot cannot resurrect a disbanded
membership; fresh snapshot still wins) proven to fail without the fix via
sabotage run; fence-off compatibility test; hide-bots shape regex updated.
341/341 plugin tests pass.
2026-08-20 05:47:40 -07:00
Teknium d3e124601f test(desktop): parent watchdog env now carries HERMES_SPAWN — update exact-shape assertions
The two deepEqual tests pin the exact env object, so the new spawn tag
field made them red. Assert the tag in both shapes and add direct
spawnTag() coverage (winms-derived seconds, dash fallback, non-winms
markers rejected).
2026-08-20 04:47:38 -07:00