Commit Graph

7570 Commits

Author SHA1 Message Date
Brooklyn Nicholson
60bf909af9 fix(desktop): forbid .only in all Playwright configs
A committed test.only/describe.only shrinks the Playwright run to a
subset while the workflow still reports success — a silent partial
green. None of the three desktop Playwright configs set forbidOnly.

- Root config: forbidOnly: !!process.env.CI, mirroring the existing
  retries: process.env.CI ? 1 : 0 CI switch — local devs keep the
  ability to focus a test interactively.
- e2e/core and e2e/update configs: forbidOnly: true unconditionally.
  These lanes already hard-set retries: 0 because a required job must
  expose problems rather than hide them; the same contract forbids a
  focused test quietly shrinking a required job.

Verified on base (red): CI=1 npx playwright test with a temporary
test.only probe passed and reported '1 passed'. After the fix (green):
the same probe fails with "item focused with '.only' is not allowed
due to the 'forbidOnly' option" under CI=1 for the root config and
without CI for the core/update lanes; without CI the root config still
runs the focused probe (local behavior unchanged).

Fixes #93142
2026-10-02 18:37:44 -05:00
Brooklyn Nicholson
4fba38e2da fix(desktop): drop a settled live reply the page still carries as its streaming copy
The switch-back hydrate can be composed from the baseline that still held the
reply's streaming row, under the same id the store later settled. The overlay
replaced that row in place and never checked whether the page already had the
reply committed, so the answer painted twice until the next refresh (about one
run in three of the tool-turn race). The represented-on-page check now runs
before the in-place replace and drops the stale copy; a folded tool bubble is
also matched by its answer text when the completion carried no row receipt.
2026-10-02 18:31:06 -05:00
Brooklyn Nicholson
feca9757b7 fix(desktop): a finished tool turn stays one bubble when the switch-back hydrate races it
The tool-turn switch-back race still painted the answer twice, frozen on its
first chunk, in two places:

- appendLiveSessionProjection only stood the stale activate snapshot down for
  a tool-free committed reply. A tool turn's snapshot carries the turn's whole
  text and history folds narration, tools and answer into one bubble, so it
  never matched. It now stands down when the committed bubble ends in a reply
  that covers the snapshot; a mid-turn commit ends on a tool and still projects.
- overlayConcurrentMessageChanges only adds and replaces. A page composed from
  the older baseline could carry a live row the store had already retired into
  the committed reply. A baseline live row missing from the current store now
  stays retired.

The steer matrix also covers the steered tool finishing in the background:
the oracle skips backend notice rows (process_complete and friends) the way
hydration draws them.
2026-10-02 18:31:06 -05:00
Brooklyn Nicholson
3cf1b4197b fix(desktop): a tool turn completing during the switch-back hydrate renders once
The new switch-back-race tool-turn case caught it: when message.complete
settles the live reply while the switch-back REST page is in flight, the
overlay kept the settled stream row unless the page carried an assistant
message with the same words. History folds a tool turn's narration, tool and
reply into one bubble, so its text never equals the reply and the answer
painted twice. The completion already binds the reply to its stored row;
match that row first and keep the text rule as the fallback.

The oracle now compares a steer stored at a tool boundary by the user's
words, not the out-of-band wrapper the model sees. The review-row scenario
does not reproduce #131626's duplicate on main, so it is a plain guard
instead of a KNOWN entry.
2026-10-02 18:31:06 -05:00
Brooklyn Nicholson
72103912e0 test(desktop): sampler fails a transiently misordered transcript
The in-page sampler already sees every frame; it now also fails a reply
rendered above its own prompt and two messages that swap places between
frames, so a wrong order that heals by the final state still fails.
2026-10-02 18:31:06 -05:00
Brooklyn Nicholson
deceec1aa8 test(desktop): core e2e for the timings that render one reply twice
reply-settle-paths.spec.ts forces the duplicate-reply shapes under the
transcript oracle: an answer the backend repeats as the final reply, with
a review.summary row delivered before that turn's message.complete
(#131626, KNOWN), a steer at each point of a tool turn, then reload and
switch-back over folded tool turns. switch-back-race.spec.ts runs both
orders with a tool turn too (#128809). The scripted provider can now
script marker-less traffic such as the background review fork.
2026-10-02 18:31:06 -05:00
ethernet
91b17917e6 test(bot-mode): pin the roster busy flag across a stop/restart overlap
A stale roster run finishing after a restart must not clear the new
run's rosterBusy flag. Dropping the generation check in
syncRelayRosters' finally left every existing test green, but let the
next 60s tick start a third concurrent roster writer while the
restarted run was still in flight (expected 4 profiles.list calls, got
6). Add a regression for that overlap, and apply eslint
padding-line-between-statements fixes to the lines the salvaged commit
added so the touched files stay at zero warnings.

Follow-up to #131028; mutation gap reported in review there.
2026-10-02 19:00:44 -04:00
Gille
8dc8292883 fix(bot-mode): discard stopped relay roster refreshes 2026-10-02 19:00:44 -04:00
ethernet
cb326e32c7 fix(desktop): activate a tray-hidden window when anything but the tray restores it
On Windows, restore() or showInactive() on a window that the tray hid
paints the window, but it does not become the foreground window. The
window then drops all mouse and key input until the app restarts. A
relaunch from the taskbar or Start menu, a deep link, and a notification
click all restore the window this way. Only the tray menu and tray click
ran show() and focus().

The restore and show handlers now finish the activation for a window
that the tray hid: show(), then focus(). The call is deferred past the
native restore. Windows that the tray did not hide are not changed.

Fixes #127349
2026-10-02 18:43:06 -04:00
Brooklyn Nicholson
c3b4a30eff fix(relay): hand each streamed chunk over before the provider sends the next
With shared metrics on, every provider stream runs through Relay's managed
pipeline. Relay pulls the next provider chunk before it delivers the current
one, and the provider read blocked the stream's private loop, so each chunk
was withheld until the next arrived. On Desktop the reply looked stopped while
the timer kept going and queued follow-ups waited on a turn that was still
running; a steer aborted text the user never saw, stacking user bubbles with
no output between them.

The off-loop read from the previous commit fixes it. Correct its comment to
the real cause, replace the two-loop starvation test (production gives every
stream its own loop, and it bounded wall-clock at 150ms) with a single-stream
contract that deadlocks on the old code, and add a core e2e spec that steers
and queues against a paused provider with Relay managed execution on.
2026-10-02 16:29:18 -05:00
Brooklyn Nicholson
50a6abca7c test(desktop): keep background replies single across refresh 2026-10-02 12:34:28 -05:00
Brooklyn Nicholson
c24c8c2731 fix(desktop): merge folded replies by stored source identity 2026-10-02 12:34:28 -05:00
teknium1
83097ba008 fix(desktop): keep the hermes://skill/install deep link through the catalog revert
The plugin deep link (hermes://plugin/install?catalog=) and the skill
deep link are separate features: one per kind. Reverting #119381's card
catalog should not leave skills without a link, so this restores the
skill route on its own:

- Desktop: skill-deeplink-install store, the skill route in
  deeplink-routes, its use-desktop-integrations wiring, and the
  confirm-store onConfirm/details/busy/done phases its dialog uses
  (restored from main with their tests). Its nine strings move from the
  deleted catalog.* namespace to skillDeepLink.* (en + the six locales
  that translated them).
- Website: Skills Hub "Install in Hermes" button, plus the
  source-qualified installIdentifier from extract-skills.py so the link,
  the embedded picker and the CLI command all name the exact skill
  (apps/shared/src/catalog-install.ts keeps only the skill helpers).
- Docs: skills.md "Install from the website" section, desktop.md pointer.

The card catalog UI stays reverted.
2026-10-02 10:29:28 -07:00
teknium1
6cb7a64e59 revert: Skills and Plugins card catalog browser, third landing (reverts #119381)
Reverts all seven commits of #119381 (59c54c78..476ff4dd), which
re-landed the native card-based Skills/Plugins catalog browser after it
was reverted twice (#113037 reverting #112669, #115787 reverting
#113128). Capabilities -> Skills and Plugins return to the list/detail
tabs with the embedded catalog picker they had before #119381.

Removed with it:
- Desktop catalog/ module (browser, cards, detail dialog, filters,
  reels/masonry, install switch) and skill deep-link install
  (hermes://skill/install); skills/embedded-hub-picker.tsx comes back.
- apps/shared catalog-browse.ts and catalog-install.ts.
- The `catalog` i18n namespace in every locale (+ regenerated
  locales/_keys.desktop.json); the two i18n pack tests and the keys
  test now exercise connectorsPage.searchPlaceholder instead.
- Website /skills "Open in Hermes Desktop" skill links and the
  extract-skills install identifiers; docs return to the pre-#119381 text.
- Later fixes that only patched the reverted catalog code go with it:
  5832a6eb93 + 7aba2cbbe9 (catalog download deadline),
  9e63cdc4b8 (catalog lookalike rows), c756986db2 (catalog counts),
  the catalog half of c13232e90c / f265273347.

Kept on purpose (later work that depends on code #119381 introduced):
- DialogContent `chrome` / `overlayClassName` / nested-dialog layering
  (the image lightbox uses them).
- vitest.setup.ts / src/test/jsdom.ts stubs (other suites rely on them).
- Kanban toolset wiring on the Plugins Desktop switch (cd30fdadf6,
  7ca2f5665f) and its tests, the `hermes_not_connected` server tone,
  the SearchField flex-1 fix, skill provenance helpers. SkillDetail shows
  a provenance pill only for agent/hub skills, since the pre-#119381
  labels have no `external` entry.
2026-10-02 10:29:28 -07:00
hermes-seaeye[bot]
0f9e758088 fmt(js): npm run fix on merge (#131680)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-02 17:29:05 +00:00
ethernet
386e31a56f fix(desktop): ship exact-size, theme-aware MSIX icons instead of one 44px tile
The MSIX package carried a single Square44x44Logo.png, so every taskbar and
Start slot on a HiDPI Windows 11 machine was Windows upscaling a 44px bitmap,
and with no altform variants Windows plated the icon itself. The generator now
emits the qualified catalog Windows resolves by itself: <Logo>.scale-{125,150,
200,400} for all four manifest logos at Microsoft's pixel sizes, and
Square44x44Logo.targetsize-{16..256} in the three required theme forms —
unqualified and altform-lightunplated carry the light tile, altform-unplated
(dark theme) the dark tile, the same split macOS dark mode gets. before-build
stages the whole assets/appx directory; app-builder-lib runs makepri and
`makeappx /l` as soon as it sees a qualified asset, so nothing else changes.
The hand-listed *-dark.png logos, generated but never staged, are gone.

Exposed along the way: the commit badge sat outside the portrait's silhouette
clip, and at 16–30px its anti-aliased edge stacked coverage on the plate's
corner pixels and changed the alpha channel between build flavors. The badge
now shares the clip, is plain black, and is left off direct renders below
32px where no SHA is legible anyway.
2026-10-02 12:51:02 -04:00
Brooklyn Nicholson
5654863ccd fix(desktop,tui): /browser use [off] works outside the CLI
The Desktop and TUI subcommand pickers offer /browser use (it is in the
registry's subcommands), but both handlers only knew connect, disconnect
and status, so picking it printed the usage line.

browser.manage gains a use action (enabled: bool) that writes the session
profile's browser.backend through the same set_browser_use_mode helper the
CLI now calls, and status/use report browser_use. The handler is profile
scoped, so a worker session writes its own profile. A live agent keeps its
tools (prompt cache), so Desktop and TUI say the switch applies to new
chats instead of swapping tools mid-conversation. use and status also run
on remote backends; connect/disconnect stay local-only.
2026-10-02 11:24:58 -05:00
Brooklyn Nicholson
b3059921bc feat(desktop): show when a message was sent on hover
Hovering a message reveals its time with the hover actions ("3:27 PM",
"Yesterday, 3:27 PM", "Sep 28, 3:27 PM"); the tip adds the age for same-day
messages and the full date. User bubbles show it beside Restore/Stop, replies
at the head of the action bar. It stands down while display.timestamps is on,
since the transcript already prints every stamp (#118405).
2026-10-02 10:59:29 -05:00
Brooklyn Nicholson
69fdbb7cdc fix(desktop): model picker keeps names whole; provider labels its favorites once
The Favorites section put a provider chip on every row beside the variant,
fast and effort chips, and with names as the only element allowed to shrink
they collapsed to a letter (#130349). A mixed Favorites section now names
each provider once over its rows; the variant tag stays a filled chip after
the name, while fast and effort move to outlined chips by the caret that
edits them. An inherited effort is no longer stamped on every row: it shows
on the active model and on rows whose preset chose one.
2026-10-02 10:59:29 -05:00
ethernet
9a71d5a8b5 Merge pull request #122976 from NousResearch/ethie/macos-icon
feat(desktop): macOS 26 layered app icon, ring dropped from every icon
2026-10-02 11:37:47 -04:00
hermes-seaeye[bot]
b78931e3b0 fmt(js): npm run fix on merge (#131603)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-02 15:16:58 +00:00
ethernet
2bf86dd20c fix(desktop): run the Windows update checkers from bundled core code
A packaged Windows build reported "Microsoft Store returned invalid
JSON" on every update check. The Store checker, the App Installer
checker and the relaunch waiter were resolved under the payload's repo
snapshot (<payload>/hermes-agent/apps/desktop/scripts/), but
INERT_SNAPSHOT_DIRS has excluded apps/ and scripts/ from that snapshot
since f6713f2762. The installed 2026.6523.3313.0 package has no apps/
directory at all. The payload python then failed to open the script,
exited 2 with empty stdout, and parseResult reported invalid JSON.

The two checkers are python that runs under the payload interpreter, so
they belong with the core code, like the other hermes_cli helpers the
desktop runs with -m:

- apps/desktop/scripts/check-store-update.py becomes
  hermes_cli/windows_store_update.py and check-appinstaller-update.py
  becomes hermes_cli/windows_appinstaller_update.py (git mv).
- The runner spawns `python -P -m <module>` with PYTHONPATH set to the
  bundled core code, then the venv dependencies: the order the bin
  launchers give sys.path. -P keeps the working directory off sys.path.
- The relaunch waiter is PowerShell run outside the package, not core
  code. It ships as an app resource (win.extraResources) and resolves
  from process.resourcesPath.

The Python tests now import the modules, and a real `python -P -m`
subprocess test proves a checker failure still reaches the desktop as
JSON. A vitest checks the packaging config ships the waiter.
2026-10-02 10:42:07 -04:00
ethernet
baf6313149 test(desktop): copy scripts/mac-icon.cjs into the channel-manifest fixture 2026-10-02 10:33:44 -04:00
ethernet
0217a2a324 feat(icons): custom drive artwork as the volume icon of both installer DMGs
The desktop DMG's volume icon defaulted to the packager's icns, which is
actool's 256px fallback whenever mac.icon is the Icon Composer package, and
dmgbuild's badge option can only paste onto the stock removable-drive icon.
Ship our own drive-with-the-girl artwork instead: assets/dmg-volume.png
(hand-made, one artwork since volume icons have no appearance variants) is
emitted as apps/desktop/packaging/dmg-volume.icns, beside the DMG background
so the `files` whitelist keeps it out of the app bundle, and dmg.icon points
at it.

Tauri always uses the app icns as the volume icon and offers no override, so
the bootstrap workflow runs scripts/bundles/set_dmg_volume_icon.sh on the
built DMG before signing (the same .VolumeIcon.icns + custom-icon flag
dmgbuild sets), and the verify step checks the icon is on the mounted image.
Verified on macOS 26.6.2: both DMGs render the identical volume icon.
2026-10-02 10:33:44 -04:00
ethernet
383916b845 feat(icons): layered macOS 26 icon for the bootstrap installer and the DMGs
Tauri's bundler (cli 2.11.4) compiles an Icon Composer package listed in
bundle.icon into Assets.car with actool >= 26 and sets CFBundleIconName, so
the bootstrap installer gets an unbranded twin of the desktop package under
src-tauri/icons/icon.icon (generator target; the bundle stays the plain
brand because flavours only apply under apps/desktop/). Tauri only logs when
actool is too old, so the macOS job selects Xcode 26 and the verify step
asserts Assets.car and the plist key are in the built .app.

The desktop DMG's volume icon defaulted to the packager's icns, which is
actool's 256px fallback whenever mac.icon is the .icon package; point
dmg.icon at the full-resolution assets/icon.icns. Tauri's DMG already takes
--volicon from the bundle's .icns, which the regenerated outputs cover.
2026-10-02 10:33:44 -04:00
ethernet
c2b69de66d feat(icons): drop the brand ring everywhere and frost the mono icon
Next to macOS 26's own icons the flat plate + girl reads better than the
ringed tile, so the ring is off in every output: the tiles' inward stroke,
the layered icon's border layer and its targets. BORDER_ENABLED keeps the
ring code one flag away.

Without a ring the girl's lowest nodes are dragged past the tile edge
(EDGE_OVERSHOOT) so the outline crops her instead of leaving a gap.

Clear light/dark and Tinted light/dark now come from one grayscale mono
layer with Liquid Glass on: the parts of the girl that are black in light
mode become a near-black frosted ink (0.13 @ 90%), her white parts stay
white, each pixel painted exactly once so materials never stack. The art
layer hides under "tinted" and the mono layer shows only there.

Mac girl at 1.06x (between main's 1.12x and the layers' 1.0x), for the icns
and the layered icon alike. Chosen from a six-appearance matrix of real
IconServices renders next to Finder.
2026-10-02 10:33:44 -04:00
ethernet
e967eff52c fix(icons): register the layered icon's girl on the mac plate box
main scaled the girl 1.12x for every mac target (a white tile with a ring
reads small beside full-colour peers), but the macOS 26 layers were still
registered on the full-bleed squircle box, so macOS 26 would have shown a
smaller girl than macOS 15.

The layered icon's canvas is the plate itself, so derive its portrait box
from the mac box in plate coordinates. Ink overlap with the icns girl goes
from 0.65 to 0.996 IoU; only the two art layers change.
2026-10-02 10:33:44 -04:00
ethernet
02ed9f2649 fix(icons): give the macOS 26 icon its mono annotation for Clear and Tinted
Clear light/dark and Tinted light/dark all render from one "tinted"
specialization: the system reads the layer as luminance (white = visible)
and tints it. Without one they fall back to the light layers, whose black
girl and ring have no luminance, so those four modes showed an empty plate.

Point "tinted" at the white layers already used for dark mode; the manifest
test now requires it and pins it to the dark image. Verified in the Dock on
macOS 26.6.2 in Clear and Tinted, light and dark: white girl and ring as
glass / in the tint colour.
2026-10-02 10:33:44 -04:00
ethernet
cfc05e70b9 fix(icons): let actool pick the dark layer images for the macOS 26 icon
Each layer carried both a fixed "image-name" and per-appearance
"image-name-specializations". actool treats the fixed name as the image for
every appearance and drops the specializations, so Assets.car held only the
light PNGs and dark mode showed the black girl and black ring on the dark
fill: a black squircle. Icon Composer itself writes one or the other, never
both.

Drop the fixed name; the manifest test now rejects it and requires a dark
specialization on every layer. Verified on macOS 26.6.2 in dark mode: the
catalog gains art-dark/border-dark and the Dock shows the white girl.
2026-10-02 10:33:44 -04:00
ethernet
26960b1afb build(desktop): compile the layered icon to Assets.car on macOS 26 hosts
electron-builder (app-builder-lib 27.0.0-alpha.6) compiles a `.icon` given as
`mac.icon` into Contents/Resources/Assets.car and sets CFBundleIconName, but
only with actool >= 26 (Xcode 26); on any other host it throws and the build
fails. scripts/mac-icon.cjs picks the Icon Composer package when that actool
is present and the legacy .icns otherwise, so a dev Mac on Xcode 16 keeps
building the app it always built.

The release runners keep Xcode 16.4 selected although 26.x is installed, so
the darwin job now selects the newest Xcode 26 via DEVELOPER_DIR and asserts
the actool version before packaging; a silent fallback cannot ship from CI.

electron-builder's `.icon` path also replaces the bundled icon.icns with
actool's 256px fallback. macOS <= 15 shows that file, so after-pack restores
our 16..1024 assets/icon.icns.
2026-10-02 10:33:43 -04:00
ethernet
c179041466 feat(icons): macOS 26 layered icon whose border follows Apple's mask
macOS 26 clips every app icon to its own squircle and shows legacy .icns art
inside a glass plate. Our border is a stroke on the tile's rounded rect, so on
26 the ring sits inside a grey rim and thins toward the corners where Apple's
curve and ours diverge.

Add an Icon Composer package (apps/desktop/assets/icon.icon) of canvas-filling
layers the system masks itself: a border band whose outer edge is the canvas
(the mask supplies the outline) and whose inner edge is Apple's mask offset
inward by the brand border thickness, so the band is the same width everywhere
along the visible outline; and the girl (+ commit badge) as a second layer.
Build flavours recolour the fill in icon.json only.

Apple's mask is a circular arc flanked by two cubic easing segments (the
Figma corner-smoothing construction). Fitted to macOS 26.6.2's own render of
a system icon on the 824-on-1024 grid: radius 214px, smoothing 0.645, rms
0.32px / worst 0.81px; a superellipse cannot get under 0.54px rms.

The .icns targets are unchanged: Apple's own 26-era .icns still sit on the
824 grid, and macOS <= 15 keeps showing that artwork.

The new contract test probes the border layer 640 points along the mask:
opaque at thickness-1.5px, transparent at thickness+1.5px. Rendering the
layers with the old r=185.4 corner violates it at 1448 probe points.
2026-10-02 10:33:43 -04:00
teknium1
4097709b0c fix(desktop): report and mute a Browser guest that attaches after its session is hidden
The off-screen hook only re-ran when hidden/parked changed, and a guest has
no webContents id until it attaches. A page whose session was parked between
mount and attach was never reported hidden to main (so mouse back/reload
could still target it) and never muted. The pane now feeds the guest's
dom-ready to the hook, which re-reports and re-mutes for the attached id.
2026-10-02 02:53:03 -07:00
teknium1
248091cd83 refactor(desktop): move preview tab ownership into store/preview-ownership.ts
preview.ts had grown to ~1450 lines. Who-owns-this-tab logic — compression
rotation aliases, the pending-runtime map, owner identity, ownsTab /
bucketTabsFor, the drawer visibility predicate — now lives in a topical
sibling. preview.ts keeps the tab list, its per-profile storage and the bucket
writers (adopt/rekey/prune), which need that storage. bucketTabsFor takes the
viewed bucket as a parameter instead of reading preview.ts's module state, so
the sibling has no import cycle back into preview.ts.

Behaviour-identical: importers of PreviewOwner and forgetPendingRuntimeTabs
now import from preview-ownership (no re-export shim).
2026-10-02 02:53:03 -07:00
teknium1
ab713f7c2b test(desktop): prove the kept Browser body survives A -> B -> A, not the registration identity
The keep-alive test compared the pane registration object across a session
switch — an implementation detail. It now renders the layout tree and asserts
what the user gets: A's body element stays connected (hidden, inert) while B
is shown, is the same node with the same input value when A returns, mounted
once and never disconnected. Red on the pre-keep-alive head f5f54a2b6c9.
2026-10-02 02:53:03 -07:00
teknium1
263fe5ccbd fix(desktop): mute a hidden session's kept Browser and restore its sound on return
A hidden session's live Browser stays mounted so returning shows the same
page, but it kept playing audio into the chat the user had switched to. The
parked-pane set is now an atom; a preview pane whose tab is parked mutes its
guest (webview.setAudioMuted) and unmutes on return only if it was the one that
muted it, so a page muted beforehand stays muted. Inactive tabs in the session
on screen keep playing, like background tabs in a browser.

Timers/rendering: a CSS-hidden webview still reports visible to Chromium and
Electron has no public API to mark a guest hidden without detaching it (any
detach destroys the guest, losing its state), so throttling is left as is.
2026-10-02 02:53:03 -07:00
teknium1
3cb409bed8 fix(desktop): mouse back/forward and reload never act on a hidden session's Browser
After a session switch, Chromium still tracked the hidden session's kept
Browser guest as the focused webContents, so main's focused-guest gestures
(mouse back/forward app-command, swipe, Ctrl/Cmd+R) navigated or reloaded a
page the user could not see. The keep-alive host also left focus on an inert
hidden guest, where keystrokes could still land.

- A keep-alive host going hidden blurs whatever inside it holds focus.
- The preview pane tells main when its guest leaves / returns to the screen
  (hermes:preview-guest-hidden; only the embedding renderer is believed), and
  commandFocusedGuest (moved to electron/preview-guest-offscreen.ts) skips a
  hidden guest so the gesture falls through to the renderer's visible pane.
2026-10-02 02:53:03 -07:00
teknium1
c2b049241d fix(desktop): a fresh draft's drawer no longer shows another runtime's pending tab
tabVisibleTo matched every ownerless tab when no session was focused, so a
runtime's pending tab (opened before its stored id bound) leaked into a fresh
draft's drawer and into any other runtime's. A pending tab is now visible only
while its own runtime is the one on screen; a true draft tab (no runtime) stays
in the draft.
2026-10-02 02:53:03 -07:00
teknium1
30b27de069 fix(desktop): keep a hidden session's live Browser mounted across session switches
Switching A -> B -> A unmounted A's Browser (its pane contribution was
disposed), so returning made a new Electron guest: unsaved input and page
JS state were lost, where main kept them. A preview tab whose session leaves
the screen now leaves the layout tree but keeps its contribution, so its
keep-alive host stays mounted (hidden, inert, aria-hidden, no pointer
events) and the same guest re-docks on return. Parked panes are excluded
from tree adoption (parked-panes.ts). Retention is capped at 8 hidden live
pages, longest-hidden first; past the cap a page unmounts and reloads from
its saved row when its session returns. Agent tools are unaffected: they
resolve tabs by owner, never by what is mounted.
2026-10-02 02:53:03 -07:00
teknium1
d481030dff fix(desktop): agent preview tools see only their own profile's pins and runtime's tabs
Two ownership holes in the per-session preview scoping:

- Cross-profile pins (P1): previewTabsFor read the viewed profile's bucket
  and admitted its pins unconditionally, and previewTabIdsVisibleTo
  authorized them for the pop-out bridge, so profile X's tile could
  preview.read profile Y's pinned page. The requester's owner now carries
  the profile its runtime belongs to (previewScopeForRuntime, the same
  resolution the rail scope uses); pins count only in that profile's
  bucket, own tabs count wherever they live. Agent opens and closes use the
  same identity, so X can neither navigate nor close Y's pinned Browser.

- Pending-runtime pool (P2): an ownerless tab was visible to every requester
  with no stored id, so runtime B read, reused (navigated) and closed runtime
  A's pre-stored-id Browser. Ownerless tabs are now either a runtime's pending
  tab (that runtime only) or a true draft tab (the draft on screen only:
  the primary's runtime, or a request naming no runtime) across reads,
  reuse and closes.

Human UI paths ($visiblePreviewTabs, the focused drawer) are unchanged.
2026-10-02 02:53:03 -07:00
teknium1
db91e4d993 fix(desktop): deleting a session prunes its preview tabs in every profile bucket
prunePreviewTabsForSession only filtered the viewed profile's tabs, so
deleting profile X's session while Y was on screen left X's unpinned tabs in
X's bucket and in storage. Prune every bucket (pins survive) and persist.
2026-10-02 02:53:03 -07:00
teknium1
f3747bc1e7 fix(desktop): preview tab menu reads the live pin state so Pin toggles back to Unpin
The zone menu's pin row captured `pinned` when the pane registered, and the
mirror keeps a same-title registration for the tab's whole life, so after the
first Pin the row kept offering Pin and kept setting true. Read the flag when
the menu renders and again on select.
2026-10-02 02:53:03 -07:00
teknium1
8685505b0b fix(desktop): third-review follow-ups for per-session preview tabs
- A popped-out Browser now answers a background session in another
  profile: previewTabIdsVisibleTo reads every profile bucket, not only the
  one in view. A background bucket counts only when the requester has a tab
  of its own there, so one profile's pins never answer for another
  profile's session.
- Agent preview tools act for the requesting runtime as well as its stored
  id (PreviewOwner): a tab that runtime opened before its stored id bound
  stays drivable/readable once the selection already names that id, locally
  and through the pop-out bridge.
- A runtime whose session state is dropped before it binds a stored id
  drops its pending notes (dropSessionState / clearAllSessionStates), so
  the next draft's first send adopts those tabs instead of skipping them.
- The drawer's pending-tab clause skips a LISTED selection: a cold resume
  selects an existing session a beat before it unbinds the old runtime, and
  that runtime's pending tab no longer joins the resumed drawer. An unlisted
  id (the runtime's own stored id arriving) still shows it.
- server-requests.test pop-out mocks forward the owner and assert it.
2026-10-02 02:53:03 -07:00
teknium1
59c9dea932 test(desktop): statusbar test mock exposes $focusedSessionIsTile
The preview store now derives visible tabs from session-focus's
$focusedSessionIsTile; the statusbar test's session-focus mock lacked it,
so the suite failed at import.
2026-10-02 02:53:03 -07:00
teknium1
bc636d37a3 fix(desktop): scope every agent preview tool to the requesting session
Second-pass review of the per-session preview tabs:

- drive_preview, tour, nav, input and the script runner resolve the
  requester's tab (storedSessionIdForRuntimeId of the event's runtime) via
  the shared resolveActivePreviewTab, not the focused session's; an id that
  does not resolve reads the ownerless/draft view, never the focused one.
- Pre-stored-id agent tabs are handed over when THAT runtime's stored id
  binds (handleTransition / ensureSessionState), and openPreview stamps the
  event's runtime rather than whichever runtime is active.
- Requests forwarded to a popped-out Browser carry the requester's tab ids;
  a pop-out showing another session's tab stays silent.
- Runtime notes are dropped with their tabs (close, prune, profile drop) and
  draft adoption skips tabs pending for a live runtime.
- The rotation-alias walk is bounded by the map size.
2026-10-02 02:53:03 -07:00
teknium1
bdba2705f4 fix(desktop): hand a pre-stored-id agent preview to the runtime that opened it
With focus-driven adoption gone (F1), main's "keeps the tab when the
stored session id arrives afterwards" regressed: an agent's open_preview
right after a first send lands before the stored id, so the tab was
ownerless and vanished from the drawer once the id arrived.

openPreview now notes the runtime an ownerless tab was opened under
(memory-only; a fresh draft has no runtime, so its file-browser / "+"
Browser tabs carry no note). The tab is adopted only when THAT runtime's
stored id arrives, never when another session or tile takes the screen.
2026-10-02 02:53:03 -07:00
teknium1
cb60c05506 refactor(desktop): import the focused-session atoms from session-focus, drop the re-export shim
$focusedStoredSessionId / $focusedSessionIsTile moved to session-focus.ts
(to break the session-states <-> preview load cycle) but session-states
kept re-exporting them for its old importers - an internal-move compat
shim (independent review F5). Every importer now reads
'@/store/session-focus' and the re-export is gone.

Test plumbing follows the symbol: suites that stubbed the atom on the
session-states mock now stub it on session-focus (profile-routing) or use
the real derivation (open-session-refresh-in-place), and two
session-states assertions read the atom from the module that defines it.
session-sidebar-focus loads session-states for the primary-follow wiring
its last case asserts, as its old import did implicitly.

Also drops the dead $focusedStoredSessionId stub this PR had added to the
close-tab.test.ts mock (F8); the file is back to main.
2026-10-02 02:53:03 -07:00
teknium1
8e737698bc fix(desktop): keep the agent's read_preview / drive_preview inside the session's tabs
resolveActivePreviewTab / readActivePreview, activePreviewInput and
activePreviewNav still read every session's tabs. With no visible
selection the tabs[0] fallback handed read_preview another session's
hidden tab, and the multi-tab meta listed hidden tabs' labels and URLs to
the model (independent review F3).

They now read previewTabsFor(session): the tabs that session can see (its
own plus pins). read_preview passes the requesting session's stored id;
drive_preview (active session only) uses the focused one. A popped-out
Browser renderer holds only its own tab, so it keeps reading that.
2026-10-02 02:53:03 -07:00
teknium1
8bb014d6a9 fix(desktop): resolve preview tab owners through compression rotation; one write per agent close
Independent review F2/F4/F9 on the session-scoped preview rail:

- F2 (hang): rekey(A,B) then a replayed rekey(B,A) put a cycle in the
  memory-only alias map and the latestSessionId walk spun forever, freezing
  $visiblePreviewTabs. Both ends of a rekey now resolve to their current
  tips first and a rekey that resolves to the same tip is ignored, so the
  map only ever links one tip to a different tip and stays acyclic.
- F4: a tab opened in the window between a rotation and focus following it
  was stamped with the rotated-away id; the alias map does not survive a
  relaunch, so the tab was orphaned. Opens, the strip "+" Browser and an
  unpin now stamp the resolved tip, and a deleted conversation is pruned by
  any of its ids.
- F9: close_preview with no url called closeRightRailTab per tab (N tab-list
  writes, N persists, N selection fix-ups). It now closes the session's
  unpinned tabs in one write and re-homes the selection once.
2026-10-02 02:53:03 -07:00
teknium1
4fc8c909cb fix(desktop): keep a draft's preview tabs in the draft until its first send
A fresh draft has no stored id, so the tabs it opens are ownerless. The
focus listener adopted every ownerless tab on ANY null -> id focus change,
so a side tile taking focus, or an existing session clicked in the
sidebar, swallowed the draft's file / Browser tabs and the draft's drawer
came back empty (independent review F1, same symptom as M1).

Adoption is now an explicit call made where the draft's first send
assigns its stored id, beside the composer draft's own hand-over
(announceNewSessionDraftKey). Focus changes never re-own a tab.
2026-10-02 02:53:03 -07:00
teknium1
6666be3b60 i18n(desktop): translate the preview tab pin/unpin labels in de, es, fr, ru
The re-landed pin-to-workspace menu shipped en/ja/zh/zh-hant/ar strings;
the other locales fell back to English.
2026-10-02 02:53:03 -07:00