Commit Graph

37299 Commits

Author SHA1 Message Date
teknium1
a4b48064b1 fix(desktop): move the pre-session draft only when the fresh chat is re-homed, keyed on the scope swap
The salvaged guard migrated the `__new__` bucket when the composer's runtime
session id went from empty to set in the same render as the scope change.
That misses the reporter's path and over-reaches on another:

- Cold-start resume-last-session (use-desktop-integrations) navigates to the
  remembered route as soon as the session list loads; the route flips the
  composer scope immediately while `session.resume` publishes the runtime id
  later, so the guard saw an empty id and never migrated — the typed text
  vanished exactly as reported.
- Opening an existing session from a fresh chat also goes empty → set, so the
  guard would carry the new-chat draft into whatever session the user clicked.
  Drafts are per scope by design (the id-rotation migration in chat/index.tsx
  is deliberately same-conversation only); a sidebar click must leave the
  new-chat draft in its bucket.

The composer swap cannot tell the two apart from its props, so the site that
assigns the id says so: `announceNewSessionDraftKey(stored)` at the two seams
where a fresh chat becomes a stored session (first-send `session.create` in
createBackendSessionForSend, cold-start restore of the remembered session /
route), consumed once by the swap effect via `adoptNewSessionDraft(scope)`,
which moves the bucket after the outgoing cleanup stashed the live editor text
under it and before the incoming scope is restored — so the text follows the
chat with no duplicate left in `__new__`. The existing helper still refuses to
overwrite a non-empty destination.

The `'active'` sentinel in use-composer-draft.ts is a focus-routing target
(`markActiveComposer`), never a draft key, so it needs no migration.

Tests: the hook test now flips the scope with the runtime id still unknown
(red on origin/main and on the salvaged guard alone) plus a control that a
plain new-chat → existing-session switch leaves the bucket untouched (red on
the salvaged guard alone). The store-level duplicate of the migration test is
dropped; the non-empty-destination invariant stays.
2026-09-18 11:01:11 -07:00
KoNit-K
ec0f840db8 fix(desktop): migrate pre-session composer drafts 2026-09-18 11:01:11 -07:00
teknium1
fff7c83113 test(shared): pin the interrupted-replay race to the issue's 3-socket sequence
Extend the salvaged regression test so it asserts the exact invariant from
#114048 rather than only "a replay happens": socket 3 asks for
last_seen=1, a live seq=3 racing that replay is parked by the NEW hold
(watermark stays at 1 until the gap is recovered), and the final order is
[1, 2, 3] with the watermark at 3. On origin/main this fails at the first
assertion (no replay is sent on socket 3 because the stale flag is still
set), which is the reporter's observed `replayOnThird: []`.
2026-09-18 11:00:38 -07:00
KoNit-K
93b3aa0674 fix(desktop): restart replay after interrupted reconnect 2026-09-18 11:00:38 -07:00
teknium1
ed45828c8b fix: inject platform into buildCommandScreenshotMonitor instead of faking process.platform in tests
The builder takes an optional `platform` (default process.platform) so the
argv test can drive the darwin and non-darwin branches explicitly. The test
no longer redefines process.platform via Object.defineProperty.
2026-09-18 11:00:02 -07:00
teknium1
b72a2bd802 test(desktop): prove the screenshot-monitor xcrun argv on every host
The salvaged test returned early off macOS, so Linux CI never executed the
assertion. Stub process.platform to 'darwin' (execFileSync is already mocked)
so the `--sdk macosx clang` argv contract is checked wherever vitest runs, and
add the off-macOS no-op as the control case.
2026-09-18 11:00:02 -07:00
Yagna Vudathu
5bb1ded879 fix(desktop): pin macosx SDK for the screenshot-monitor native build
A bare xcrun clang inherits the host default SDK, which may be newer than
the active linker (unknown-arch .tbd stubs at link time). Naming --sdk
macosx explicitly forces the driver and linker to agree on one SDK.

Fixes #113708.

(cherry picked from commit d5fcb6c6ab3de96602528dade2d7f75b7e47ddd5)
2026-09-18 11:00:02 -07:00
teknium1
cdc5034209 fix(desktop): retire "Show earlier" when the complete index lists no prompt before the window
When the backend counts rows before an open history page but the prompt
index is complete and names no predecessor, revealOlder returned false
without touching state, so the button and the top-edge auto-page kept
offering a page that could never arrive. Retire the page's reach in that
case; an incomplete index still leaves the page untouched for a retry.

Tests: one new history-window case pins the retire; the session-top
control (already covered by the reachability case's final assertions)
and the separate lookup-ceiling case are folded away, and the two
padding-line lint warnings in history-window.test.tsx are fixed.
2026-09-18 10:59:30 -07:00
teknium1
edff958060 fix(desktop): only merge an older history page that actually touches the window
The around route reads forward from a prompt, so when a single turn holds
more display rows than the page limit, the previous prompt's page ends before
the open window's first row. Merging the two would paint one continuous
transcript with a silent hole. Each page now carries `pagination.offset`
(display rows before its first row); an older page that does not reach the
current offset replaces the display page instead of being prepended — the
reader still moves backwards, nothing is omitted without notice — and the
prepend scroll anchor is not spent for a replacement.

Docs: the desktop guide now says Show earlier keeps paging back after a
timeline jump.
2026-09-18 10:59:30 -07:00
finn763
41ce41cda8 fix(desktop): open history windows keep their earlier prompts reachable
2efbbef981 read "there is more above" from the live-tail flags alone:

    const olderAvailable = !history.page && (windowed || restBackfillAvailable)

A rail jump replaces the display with a bounded around page, so every row that
page does not hold reads as exhausted. The transcript's "Show earlier" button,
the top-edge auto-page, and the parked-offset regrow all key off that single
`olderAvailable`, so a reader who clicked an older mark was pinned to that mark
and everything after it — while the around window itself had already reported
`has_older: true`.

The window now carries its own reach (`pagination.has_older`), and "Show
earlier" pages it backwards through the same prompt index the rail draws
(previousPromptRowId; cached and coalesced with the rail's own loadMore)
instead of refusing every prepend. One range, so the rail's marks and the
transcript's entry point cannot disagree about what is still up there.

The rail's earlier mark also stops depending on the live window flag for its
reach: with the index not yet loaded it pages the transcript instead of
falling through to a no-op reveal.
2026-09-18 10:59:30 -07:00
teknium1
c3d2e108f1 fix(desktop): keep no-op tail re-records as LRU touches
A suppressed identical re-record still bumps the key to the MRU end of
transcriptTailOrder (no store publish), so the constantly re-read active
session is not the first eviction candidate once 256 distinct tails were
hydrated. Also pads the statements eslint flagged in the tail tests.
2026-09-18 10:58:58 -07:00
teknium1
28e0eab9ac fix(desktop): no-op re-reads keep identity; a genuine scroll ends a pending restore
Two more atoms of the class #113845 opened (#113842):

- useStatusSnapshot published a fresh StatusResponse object on every 60s tick
  even when the content was unchanged, re-rendering every consumer for
  nothing. Content-equal snapshots now keep the previous reference.
- The transcript restore effect in thread/list.tsx re-pinned a `bottom` target
  on EVERY ResizeObserver tick once settled (every composer keystroke resizes
  the content box), and its cancel set (wheel/pointerdown/keydown) did not
  include `scroll`, so a rail jump, a find-in-page reveal or a scrollbar drag
  never ended the restore — the next tick yanked the view back to the bottom.
  The bottom branch now re-pins only on real transcript growth
  (shouldReapplyFrozenThreadScrollOffset handles both target kinds), and a
  `scroll` whose position neither this effect nor use-stick-to-bottom wrote
  cancels the restore. The effect reads back every scrollTop it writes so its
  own scroll events cannot cancel it.

Tests: content-equal status re-read keeps identity (red on base); jsdom
list-restore cases for the scroll cancel and the composer-only resize gate
(red on base); the pure predicate's bottom contract.
2026-09-18 10:58:58 -07:00
Yagna Vudathu
9568f614c9 fix(desktop): skip tail atom writes when the entry is unchanged
Heartbeat re-records identical tail entries; each unconditional .set()
re-rendered the whole chat tree through the tail atom. Content-compare and
return early on no-ops.

Fixes #113842.
2026-09-18 10:58:58 -07:00
teknium1
11a12f201e fix(desktop): mark the immediate active-route redial too, via one reopen helper
disposeSecondariesForConnection re-dials an unleased active scope straight
away, through the same dispose → evict → ensureGatewayForAgent sequence the
drain path uses, so it has the same window: the entry is out of the map while
ensureGatewayForAgent awaits its shared-remote probe, and a prune in that gap
takes the activation through setActive(primary) and the redial's epoch check
discards it. Route both call sites through reopenAfterRedial so the
reactivating marker covers the whole class, and pin the sibling with a second
invariant test.
2026-09-18 10:56:33 -07:00
John Paul Soliva
c382333e25 fix(desktop): a prune no longer cancels the redial of the route it is pruning
Editing the connection you are viewing defers its redial until the last lease drops, then disposes
the entry, evicts it and re-activates the same scope asynchronously. While that is in flight the
entry is gone from the map, so restoreActiveToPrimaryIfEvicted sees an active scope with no entry
and calls setActive(primary) — which bumps the activation epoch, and the redial's own
applyActive(epoch) is then refused. The socket it dialed is fine; the window just never goes back
to it. A connection edit silently re-homes the window to the primary backend.

Mark the scope while its re-activation is in flight and let the safety net skip it. The marker is
cleared in a finally on both paths, so a redial that never lands still falls back.

The test holds the redial inside a gated dial, runs the pruner in that window, and asserts the
window ends up on the re-dialed socket rather than the primary. Letting the pruner ignore the
marker fails it.
2026-09-18 10:56:33 -07:00
teknium1
85f5f560c3 fix(agent): a paste-only opener's expansion footer must not demote the title preview
The titler receives the opening message AFTER @-reference expansion, so
the Desktop's generated pasted_content @file: ref arrives with a
'--- Context Warnings ---' (or '--- Attached Context ---') footer. That
footer made the ref-only check in build_title_input fail, so the live
wire capture showed the path + warning leading the title-model input
with 'Pasted content: ...' appended after it. Strip the expander footer
when a preview is present, so a paste-only opener lets the preview lead.

Invariant: test_expanded_paste_ref_footer_does_not_demote_the_preview
(red on a5dac801, green here). Re-checked on the wire against the stub
model: title input now starts with the pasted topic, no @file:/warning.
2026-09-18 10:56:00 -07:00
teknium1
490bd2093b fix: cover the title_preview wire seam and the widened _run_after_agent_ready call
The PR widened tui_gateway/methods_prompt.py::_run_after_agent_ready with a
display_metadata positional; tests/tui_gateway/test_submit_time_user_row.py
(already on main) still called the six-arg form and failed CI with a
TypeError. Update the call.

Add one turn_context-level invariant: a user message carrying
display_metadata.title_preview reaches maybe_auto_title(title_preview=...).
Red with agent/turn_context.py swapped to origin/main, green on this head;
without it the whole prompt.submit -> display_metadata -> titler seam had
no test.
2026-09-18 10:56:00 -07:00
teknium1
34ba61bf67 fix(agent): paste title hint reaches the instant title and the prompt.submit contract
Build on #114129 (@KoNit-K), which carries a Desktop-generated large-paste
preview from the composer through `prompt.submit` -> `display_metadata` ->
turn context -> the shared title input. Two gaps closed:

- `apply_instant_title` never received the preview, so the instant title of a
  paste-only opener was the generated `@file:` path — and stayed that way,
  because the upgrade thread's `derive_title` fallback writes `derived`
  provenance, which never replaces the `derived` title already stored.
  Thread the hint into the instant stage too.
- `build_title_input` let the `@file:` ref lead when the opener was nothing
  but the generated attachment ref; the preview now leads for a ref-only
  opener (an instruction still leads when the user typed one).
- `prompt.submit` gains `title_preview` in the contract (regenerated shared
  TS/OpenRPC); documented as title-only input in the configuration guide.
- Tests trimmed to two invariants (shared input reaches both stages; budget +
  manual attachments stay unread).
2026-09-18 10:56:00 -07:00
KoNit-K
8d25e69b6e fix(desktop): use generated paste previews for titles 2026-09-18 10:56:00 -07:00
teknium1
996c18274b fix(desktop): the creating desktop backfills section names onto bots filed before names rode along
Bots filed before sectionName existed carry only sectionId in their profile
ui_meta. The creating desktop knows the section record but never wrote the
name back, so a second desktop on the same gateway had nothing to rebuild
the section from and still drew a flat list.

backfillBotSectionNames runs beside adoptBotSectionsFromMeta in the roster
pane effect: members whose sectionId matches a locally known section but
have no sectionName are re-stamped through moveBotsToSection (one write per
profile, sequential). Once written the name is present, so it is a one-time
pass per member. Sections nobody here knows are left alone.

Docs no longer tell users to re-file or rename to stamp the name.
2026-09-18 10:55:24 -07:00
teknium1
1197afc2b9 fix(desktop): bot roster sections follow the roster to every desktop
Section RECORDS live in the creating desktop's plugin storage
(user-sections.ts, BOT_SECTIONS_KEY) while membership (`sectionId`) rides
each bot's profile ui_meta over the gateway. A second desktop on the same
backend therefore held sectionIds whose names it had never seen, and
renderUserSections fell back to the flat list — the "no section headings"
half of #114355.

- every filing writes `sectionName` beside `sectionId` (moveBotsToSection),
  so the name travels with the membership
- adoptBotSectionsFromMeta rebuilds records this desktop never created from
  its members' meta, and takes a rename once every member agrees on the new
  name; the roster pane runs it on each roster/meta change
- renameBotSection re-stamps the members so the new name reaches other
  desktops; order and empty sections remain per desktop
- two invariants in user-sections.test.ts (red on origin/main); docs updated

The other half of the report — no "New section" entry in the + menu — is a
stale Linux build: roster-pane-toolbar.tsx and bot-row.tsx render the
entries unconditionally and nothing under plugins/hermes-bots/ reads
process.platform / navigator.platform.
2026-09-18 10:55:24 -07:00
teknium1
08ea0cc3fc chore(contributors): map xiajiahang@weyland-x.com to @xiajiahang 2026-09-18 10:54:46 -07:00
teknium1
e6fa1fd474 test(desktop): pin the group mention popover to the elevated surface in vitest
Replace the Playwright spec from #104065 with one jsdom invariant in the
existing GroupMentionInput suite: the popover carries `bg-(--ui-bg-elevated)`
and never a `--ui-bg-{primary,secondary,tertiary}` fill token. The Desktop
E2E job is `if: false` in ci.yaml, so the 96-line spec (two bots, a group,
a 32-paragraph post, a theme toggle) would never run; the token → opaque
surface chain was verified once in headless Chromium (alpha 62 → 255).
2026-09-18 10:54:46 -07:00
xiajiahang
e0f0d2f539 fix(desktop): use an opaque surface for group mention menus 2026-09-18 10:54:46 -07:00
teknium1
4cde8f2ced fix: answer window-owned requests for sessions hosted in tiles
WINDOW_OWNED_REQUESTS (preview.act, preview.read, terminal.read,
window.read) treated only the active session as owned by this window, so
a session shown in one of this window's session tiles got its pane reads
'ignore'd by every attached window and the tool stalled until the backend
deadline. Ownership is now "this window hosts the session": the primary
view OR any open tile's runtimeId (same rungs foregroundSessionScopes
walks), read synchronously from $sessionTiles.

Also make the table the single owner rule: 'tour' joins the set and the
duplicated inner `sessionId && !isActiveSession` guards in previewAct and
tour go away (they were unreachable behind the table route; after the
tile widening they would have re-introduced the silent stall for a hosted
but non-active tile session, which now gets the fail-fast refusal
instead).
2026-09-18 10:54:13 -07:00
teknium1
3c4ac7cc42 fix(desktop): every per-window pane bridge stays silent for another window's session
Widen the owner rule to the sibling bridges answered from THIS window's
panes: terminal.read and window.read alongside preview.act / preview.read.
Every attached window observes the same server request; a window showing
another session has no pane for it and its empty answer would win the
race, so the tool reported "No preview tab is open / No in-app terminal is
open" while the owner's pane was open. The route table lives in one set so
the four bridges cannot drift apart again. Tests trimmed to two invariants.
2026-09-18 10:54:13 -07:00
KoNit-K
4db8521f25 fix(desktop): retry replayed preview owner routing
(cherry picked from commit e3348ca07ebd4fc2cd500c830338a7d48ab5fda6)
2026-09-18 10:54:13 -07:00
teknium1
aee7de4db5 fix(desktop): point the remote-backend desktop-half tooltip at the install path
The honest "unavailable (remote backend)" state (previous commit) tells the
user the copy will never happen; the tooltip now also says what does work
against a remote backend — Install from Git with the Desktop target checked
clones the desktop half onto this machine (the install modal already takes
that branch for connection.mode === 'remote'). Docs note the new state next to
the existing remote-backend paragraph.
2026-09-18 10:53:41 -07:00
liuhao1024
2bc2c50df3 fix(desktop): mark a remote-backend desktop half unavailable instead of forever copying
Electron's desktop-half reconcile (reconcileUnifiedDesktopHalves) only
walks this machine's hermes homes, so a unified package installed on a
remote backend can never materialize into the app's desktop-plugins
root. The renderer still derived desktopMissing from the backend's
has_desktop_half, painting an eternal 'copying…' state that reads as a
transfer in progress (#114079).

Show an honest 'unavailable (remote backend)' state (with an explanatory
tip) whenever the live resolved connection mode is remote; keep the
pending state for local backends, where the reconcile really can catch
up on rescan or restart.

Fixes #114079

(cherry picked from commit 2e35b47495c699b619f9899da988dea24f2b6632)
2026-09-18 10:53:41 -07:00
teknium1
e052500722 docs(desktop): the Kanban board switcher sits in the page header, not the window title bar
/kanban is a workspace page route: the titlebar slot is hidden there and the
switcher is projected into the Kanban page's header row (WORKSPACE_PAGE_HEADER_AREA
after #114960). Point readers, the i18n comment and the test comment at that row.
2026-09-18 10:53:00 -07:00
teknium1
d235ae0991 fix(desktop): Kanban board switcher reads as a control — icon, tooltip, i18n
Follow-up to the salvaged #114647 hunk (visible "Board" label +
`Board: <name>` accessible name):

- replace the native `title` with the app's `Tip` ("Switch board"), the
  same tooltip every other dropdown trigger uses, so hover names the
  ACTION instead of repeating the board name
- lead the trigger with the plugin's `project` icon so the title-bar
  text reads as the Kanban board control, not a static window title
- add `switchBoard` to the kanban plugin bundle (en/ja/zh/zh-hant)
- test registers the real locale bundle and asserts the rendered label,
  accessible name and tooltip (the raw-key fallback matched
  `/board:/i` by coincidence)
- docs: describe the Desktop switcher and its local selection

Fixes #114642
2026-09-18 10:53:00 -07:00
KoNit-K
8667e98293 fix(desktop): clarify kanban board switcher 2026-09-18 10:53:00 -07:00
teknium1
66ba4e5114 test(tui_gateway): pin that a live-turn rewind is not redirected; document the 4009 contract
The contributor test covers the queue fall-through; the Desktop's normal case is
a redirect-capable AIAgent, where busy_input_mode=interrupt turned the edit into
a mid-turn redirect and left the un-edited transcript in place. Pin that branch
too, and document in the rewind section that a truncating submit refuses with
4009 while a turn runs so hosts interrupt + retry (the Desktop already does).
2026-09-18 10:52:27 -07:00
chelsealong
00d41aa52c fix(gateway): signal busy instead of queueing a mid-turn edit/rewind
prompt.submit's busy path (_handle_busy_submit) steers/redirects or
queues a mid-turn submit as a plain follow-up, which is correct for
an ordinary message but silently drops the truncation intent of an
edit/restore/regenerate: the original (un-edited) turn keeps running
and its reply lands untouched, reading to the user as "my edit was
rejected" (#113942 — editing a message while Hermes is still
thinking).

The desktop client already has a robust interrupt-then-retry loop for
exactly this race (session.interrupt() is async, so prompt.submit can
still observe running=True right after it) — it just needs the
gateway to report "session busy" (4009) instead of accepting the
submit. Route truncating submits around the queue/steer path so they
hit that existing retry loop instead of being silently absorbed.

(cherry picked from commit 982767f723607870804c73a3e0322a50ccbae08b)
2026-09-18 10:52:27 -07:00
teknium1
cd62ab3415 fix(desktop): kinded inline references keep their hue instead of the raw primary (#114612)
The `.ref` fallback declared `--ref-color: var(--dt-primary)` AFTER every
`[data-ref='<kind>']` rule at the same specificity, so source order made it
win for any element carrying both — which is every inline reference: Bot Mode
group @mentions (agent/human/broadcast), `/skill`, `@file:`, pasted URLs.
Every kind painted the raw primary in both modes; on a dark surface that is
the thinnest colour in the palette and the reporter saw @mentions vanish.

Declare the fallback first (`.ref, [data-ref]`) so the kind rules outrank it
by order, exactly as the INLINE REFERENCES block documents. An unkinded
reference still keeps the primary link colour.

Test resolves the cascade for a real `<span class="ref" data-ref=…>` against
the compiled stylesheet (Tailwind's `@supports` color-mix fallback included).
2026-09-18 10:51:45 -07:00
teknium1
f81ac9a45b fix(desktop): approval card hands focus back to the pane the user was in
Answering an approval with the pointer moves focus onto the card's button,
and the card then unmounts, which parks focus on <body>. From <body> the
type-to-focus keybind routes the next printable keystrokes into the chat
composer. In a computer_use flow the agent has typically just clicked the
embedded terminal (or preview) pane, so its follow-up `type` action landed
in the composer while the tool reported success (#113839).

The card records document.activeElement on pointerdown (before Chromium
moves focus to the button) and, once the reply is committed, focuses that
element again — only when focus is still stranded on the card or <body>,
never when the user has since moved to another surface. The modal prompts
(sudo / secret / vault) already get this from Radix Dialog's focus return;
the in-transcript approval card is the one surface that did not.
2026-09-18 10:51:11 -07:00
teknium1
8dff4e9bc5 fix(desktop): unarchive from Settings into the session's own sidebar slice
The Settings unarchive prepended the row into $sessions regardless of
source, so a messaging or cron session showed under SESSIONS until the
next refresh. Route it through restoreListedSession, which picks the slice
from the row's source.
2026-09-18 10:50:39 -07:00
teknium1
f1e89cbba1 fix(desktop): keep untouched sidebar slices identity-stable on resolve eviction
upsertResolvedSession now evicts the moved row from the slices it does
not belong to. `prev.filter` always returns a fresh array, so every
row open would have repainted the messaging and cron sections even when
neither held the row; return `prev` when nothing matched, matching the
signature-gated swaps the list refresh already uses.
2026-09-18 10:50:39 -07:00
Yagna Vudathu
52066564f2 fix(desktop): route resolved sessions into their current slice
upsertResolvedSession always prepended to the regular sessions slice, so a
resolve that observes a source move (cross-room resume rewrites the row to
source=matrix) showed the session twice. Share the slice targeting with
restoreListedSession and evict the stale copy from other slices.

Fixes #113827.
2026-09-18 10:50:39 -07:00
teknium1
4a833f1c53 test(desktop): trim empty-prose pairing tests to the two reproducer invariants
The three "still preserves" cases are already pinned on main by
resume-structural-parts.test.ts (live flat row keeps cached structure,
#76444 non-extending dump) and utils.test.ts (#75825 empty-shell
preference), so they were change-detectors here. Also drops an
unrelated blank-line hunk in appendLiveSessionProjection.

Co-authored-by: KoNit-K <konit.block@protonmail.com>
2026-09-18 10:49:18 -07:00
kokhlo
fabb6da606 fix(desktop): empty-prose and settled-target guards in resume reconciler 2026-09-18 10:49:18 -07:00
teknium1
5f1e6a8278 fix(desktop): theme inline code at the markdown renderer root
The inline-code token rule keyed on the assistant-message slot, so every
other consumer of the same `aui-md prose` renderer — system-message
background reports (#107486) and the session-import preview — fell
through to Tailwind Typography's fixed near-black ink on dark themes.
Widen the selector to `.aui-md :not(pre) > code` so each consumer
inherits the tokens from the renderer root; its (0,1,2) specificity
still beats `.prose :where(code)`, and the unlayered rule still beats
Streamdown's layered utility background. The room selector stays for
the plugin's raw-Streamdown fallback, which has no `.aui-md` root.

Adds one cascade test on the system-message surface (real stylesheet,
getComputedStyle on the opened report's <code>): red before, green after.

Salvages the renderer-root selector from #107492 (@KoNit-K).
2026-09-18 10:48:46 -07:00
teknium1
f50384cf65 test(desktop): room inline-code coverage runs the real stylesheet cascade
Replace the salvaged source-text regex test with a jsdom render of the
room workspace: styles.css is injected as a real <style> sheet and the
assertion reads getComputedStyle() of the <code> the shell renderer emits
inside a room body. A regex over the CSS source passes for any rule that
mentions the tokens, whether or not its selector reaches the room's DOM;
the cascade test is red when the room-owned selector is missing and green
only when it actually matches (verified by swapping styles.css to
origin/main).
2026-09-18 10:48:46 -07:00
KoNit-K
2354782b6b fix(desktop): style inline code in group chats 2026-09-18 10:48:46 -07:00
teknium1
0aa13a5e4b fix(desktop): drop native title= from all 24 button sites the fixed guard flags
Convert every `<button>`/`<Button>` that carried a native `title=` under
apps/desktop/src, per the DESIGN.md rule (tips only where hover teaches
something new; otherwise `aria-label` for a11y):

- `aria-label` swap where the visible affordance already teaches the action
  (zoom/lightbox triggers, delete icon, install "+", reference thumbnail).
- `title=` deleted where an equal `aria-label` or visible text already exists
  (remove/disconnect trash icons, send arrow, stop glyph, Download button,
  MCP tool chips with `aria-pressed`, Lock face + its caption, pet toggles,
  spectator bubble with `aria-expanded`, Create Group whose dialog copy already
  says "Pick 2–N bots" — a tip on a `disabled` Button never opens anyway).
- `<Tip>` where hover reveals something not on screen: full path on changed
  files rows (`key` on the Tip inside the map), "Restore checkpoint — rerun
  from this prompt", "Disconnect (runs the removal command in the terminal)",
  group-chat attach hint and the speaker-handle toggle.

Guard hardening on top of the salvaged scanner: skip `//` and `/* */` comments
inside the opening tag (an apostrophe in `// Don't steal focus…` between
attributes otherwise opens a phantom string and hides `title=` — two shipped
sites were invisible that way), skip `*.test.tsx` fixtures, and match `title=`
as its own attribute (not `data-title=`). One fixture test pins those cases.

Co-authored-by: wnuuee1 <poli.koltsova@gmail.com>
2026-09-18 10:48:15 -07:00
Yagna Vudathu
b2fd69b1f7 fix(desktop): no-native-title guard scans src/ and ends tags at a depth-0 ">"
The guard's tag regex stopped at the first ">" (inside `onClick={() =>`)
and its scan root resolved to src/components, so no site was ever flagged.
Salvaged from #113717 (guard hunk only).
2026-09-18 10:48:15 -07:00
teknium1
26a7a95ceb fix: pin Browser tile keep-alive through the mirror; document Hide for Terminal
The lifecycleKeepAlive gate in watchPreviewTileMirror had no test: swapping
preview-tile.tsx back to main left the suite green, so a regression that
dropped the flag (and turned Hide back into an unmounting Minimize for the
in-app browser) would have shipped silently. preview-tile.test.ts now drives
the real mirror and asserts a url tile registers with lifecycleKeepAlive=true
while a text file peek stays evictable.

The Hide label and kept-mounted hidden body key on lifecycleKeepAlive for every
pane, and the Terminal pane sets it too; the user guide only mentioned the
browser, so the sentence now covers both.
2026-09-18 10:47:00 -07:00
teknium1
63916134e9 chore: map varora1406@gmail.com to @vaibhavarora14
Attribution mapping for the salvaged #114251 commit.
2026-09-18 10:47:00 -07:00
teknium1
a0c61500bc fix(desktop): gate the kept-mounted hidden body on lifecycleKeepAlive
The salvaged kernel kept EVERY minimized zone's panes mounted, contradicting
the deliberate park-on-hide budget documented in tree-group.tsx. Only panes
that declare `lifecycleKeepAlive` (embedded Browser / HTML preview, terminal)
stay mounted while their zone is hidden; everything else unmounts exactly as
before, and a hidden zone with no keep-alive tenant renders no body at all.

- keep the reconciler's lifecycle value for visible zones; a hidden zone
  reports `hot-hidden` to its kept panes instead of a hard-coded rewrite
- PaneBody rides the app's ONE shared ResizeObserver
  (hooks/use-resize-observer.ts) instead of a private observer per zone
- one invariant test: keep-alive pane survives hide/restore with state and
  the "Hide" label, plain sibling still parks (red on origin/main)
- docs: Hide/Restore vs Close for the in-app browser
2026-09-18 10:47:00 -07:00
Vaibhav Arora
73d4a4e9cb fix(desktop): keep hidden browser pages and automation alive
Separate Hide/Restore from Close for the embedded Browser: a minimized
zone keeps its body mounted (PaneBody retains the last visible viewport,
visibility:hidden + inert) instead of unmounting the <webview>, url/html
preview tiles opt into lifecycleKeepAlive via PaneMirror, the collapse
action reads "Hide" for keep-alive panes, commandFocusedPreview ignores a
hidden guest with stale native focus, and drive_preview's focus() no
longer steals the host composer while the pane is hidden.

Salvage-TRIM of #114251 (kernel only). Dropped from the original:
DESIGN.md, the apps/desktop/scripts/browser-hide-restore/ Electron smoke
harness, the three-layer host-focus bounce around executeJavaScript /
sendInputEvent / focusin in preview-pane.tsx, and the extra test files
(pane-body.test.tsx, preview-script-runner.test.ts, preview-tile.test.ts,
two of three tree-group cases).
2026-09-18 10:47:00 -07:00