Commit Graph

127 Commits

Author SHA1 Message Date
teknium1
9f077100cc fix(desktop): enable New Group Chat with one local bot plus remote-connection bots
The roster menu gated "New Group Chat" on `activeSourceRoster.length < 2`
(local-source rows only) while the dialog it opens seats the FULL
multi-source roster (`selectableRoster = roster.filter(bot => !bot.ghost)`,
`canCreate = selected.length >= 2`). With one local bot and any number of
remote-connection bots the door stayed shut on a room the dialog would
happily create; users added a throwaway local bot just to click through.

The gate now counts the same selectable set as the dialog.

Live repro (real Electron + a real second `hermes serve` registered as a
remote connection): e2e/group-create-gate-remote-roster — base: menu entry
`aria-disabled="true"` with 1 local + 2 remote rows in the roster; head:
enabled, dialog seats Hermes + the remote Inbox, "Create Group (2)" enabled.

Co-authored-by: FalconOrtiz <falcon.ortiz11@gmail.com>
Co-authored-by: neil771 <neil@lannscapital.com>
2026-09-17 00:22:51 -07:00
teknium1
d62036acb8 test(desktop): e2e — pre-split hosted rooms stay reachable from the Desktop backend
Seeds the pre-isolation layout (hosted_room* rows in the root state.db,
written by the store's own writers), launches the REAL Electron app and asks
its spawned `hermes serve` backend over JSON-RPC: groups.list / groups.state /
groups.log for the old room. On origin/main the store is created empty and
every call answers 4114 "hosted room not found"; with the one-shot import the
room, its cursor and its event come back, shared-state.db exists and the
legacy state.db is left intact.

Docs: one sentence on where room state lives and that pre-existing rooms are
copied across once on update. Contributor mapping for L4XB, whose #109917
proposed the same import a little earlier (ATTACH + INSERT OR IGNORE into an
empty store); the marker-row variant from #109979 is the one landed here
because it also reaches installs that already created new rooms after the
upgrade.

Refs #109775
Co-authored-by: L4XB <lukas.buck@e-mail.de>
2026-09-17 00:15:35 -07:00
teknium1
2b267f9467 fix(desktop): introduce a renamed primary bot by its own @tag in group-turn prompts
The primary profile renamed to a Bot Mode title ("Bobby") is @bobby to the
roster, autocomplete and the mention resolver, but buildGroupChatTurnPrompt
introduced it — to itself and to its peers — as @hermes. A member told
"You are @hermes" read "@bobby …" as someone else's message and replied
"(pass)", so the room settled with the addressed bot silent.

Use botMentionTag() (title-derived tag, falls back to the @handle) for the
viewer and the peer list; an unrenamed primary still reads @hermes.

Live repro: e2e/group-prompt-renamed-primary-handle.spec.ts fails on
origin/main (room log carries only the user line) and passes here (entry
authored by `default` saying "B"). Prompt hunk proposed by @chilledtonic
in #89720.
2026-09-16 22:25:33 -07:00
teknium1
f879fead28 test(desktop): live Bot Chat spec for message_agent friendly-name targets
Mock inference gains a one-shot `E2E_CALL(<tool>)[<json>]` script so a spec can make
the REAL tool run from a real Bot Chat; the new spec creates a sender bot, seeds a
`writer` profile titled "Scribe", sends a DM to "Scribe" and asserts the rendered
tool result says "Message dispatched to @writer" (base: "No teammate named
'Scribe'"), plus the sender's state.db tool row.
2026-09-16 22:25:33 -07:00
teknium1
d4625b593d test(desktop): re-select the room before pressing Stop in the @all re-engage spec
Same background intro-turn tab yank as the other room specs: the room's
Stop button is hidden behind a fronted bot chat tab and the click times out.
2026-09-16 21:55:50 -07:00
teknium1
abbcdc0104 test(desktop): re-select the room before reading room content in group specs
A bot created moments before the room runs its canonical-chat intro turn in
the background (create-dialog.tsx createCanonicalChat kickoff); when it
lands, the roster fronts that bot's chat tab and yanks the center away from
the room, so a room-content assertion sees the reply as hidden or the
Activity button as missing and fails on an unrelated visibility check. Both
specs now re-select the room tab inside the wait, like the approval spec.
2026-09-16 21:55:50 -07:00
teknium1
84b79b3787 test(desktop): assert the group approval click inside the room's own card
The #91706 spec located the "once" button page-wide. On a build without the
fix the Desktop is no longer on the room when the approval arrives (the
same server request also lands on the session-level approval surface and a
fresh draft tab takes the foreground), so the spec failed on "button not
visible" instead of on the contract it exists to prove — one
approval.respond per click, no footer Respond. The spec now re-selects the
room tab while waiting and scopes the card, its choice and the (absent)
Respond button to the room's approval card, so a base leg fails on the
respond-count assertion.
2026-09-16 21:55:50 -07:00
teknium1
b466a87bd6 fix(bot-mode): group rooms wake on @all, ignore prose stop words, report dead turns, approve on click
Three group-room turn-loop defects, each live-reproduced in the real
Electron app against a real gateway with mock inference:

- Hold classifier (group-rounds.ts): a stop/halt/pause token holds a member
  only within two words of an @mention, so "@impl go, das ist halt ein Test"
  is delivered instead of setting a sticky hold (#103893); addressing the
  whole room (@all/@everyone) without a stop word releases every hold, so a
  Stopped room wakes on "@all <task>" without the literal "resume" (#97740).
- Retained failure (group-turns.ts): the gateway keeps a failed turn under
  session.resume.inflight as {status:'error'}; the room read that as live
  work and slid the deadline to the 20-minute cap while the member looked
  busy. groupSessionBusy/retainedGroupTurnError classify it as finished;
  the foreground poll throws it into the failed-turn path (activity +
  roster badge) and the harvester consumes the stranded marker (#92760,
  diagnosis from #95103 by @hrnbld).
- Approval card (group-chat-parts.tsx): approval choices submit on click;
  the clip-prone footer Respond button is gone for approvals (#91706).
  Room message code blocks wrap instead of overflowing (#91857 by
  @piskooooo).

Tests: invariant vitest cases in group-rounds/group-turns/group-chat-parts
(all red on base); three Electron specs under apps/desktop/e2e using two
new mock-inference triggers (gated rm -rf for a real approval prompt, a
non-retryable 401 for a retained failure). Docs: bot-mode.md § Groups.

Co-authored-by: hrnbld <hrnbld@users.noreply.github.com>
Co-authored-by: piskooooo <piskooooo@users.noreply.github.com>
2026-09-16 21:55:50 -07:00
teknium1
00bae45b2e chore(desktop): bump the desktop workspace lock entry; front the room tab in the owner-meta spec
package-lock.json records the apps/desktop workspace version; siblings bump
it with package.json. The spec's createAgent opens the new Bot's canonical
Bot Chat, whose late hydration fronted a "Draft" tab over the room and hid
the reply the spec waits on; bring the room tab back before sending.
2026-09-16 21:52:29 -07:00
teknium1
f9cc4ce975 fix(desktop): resolve Group Chat member identity by owning connection and profile
Group rooms label members through two paths that predate owner-qualified
bot meta: the member header read `allMeta[b.name]` (and cleared remote
titles outright), and `groupSpeakerLabel(name)` — the "X is thinking…"
line, Activity rows and the round prompt — looked a raw profile name up
in `$botMeta`, whose keys are `connectionId::profile` (botMetaKey). Both
either missed (raw id / stale "Hermes") or hit a stale v1 relic, so
re-titling a Bot repainted the roster but not its rooms, remote members
lost their titles, and two same-named `default`s on different gateways
produced identical Activity rows.

Now the header title goes through `botRosterMeta(member)`, Activity
records `groupMemberKey(member)` (the route-qualified key), and
`groupSpeakerLabel` resolves a member key — or a raw name that exactly
one roster row carries — through the same `displayName(row,
botRosterMeta(row))` pipeline the Bots tab renders. When two same-named
members still resolve to one label, the connection label is appended.
The clarify/approval card badge deliberately keeps the plain name (it
matches its member through `memberKey`).

Live repro (real Electron, mock inference): e2e/group-transcript-owner-meta
— re-title a room member through Edit profile; base kept "Programmer" on
the transcript row and Activity, head shows "Infra Ops" on both.

Co-authored-by: FalconOrtiz <falcon.ortiz11@gmail.com>
Co-authored-by: fridde01 <carl@carltaylor.com.au>
Co-authored-by: zhongwater123 <805041391@qq.com>
2026-09-16 21:52:29 -07:00
addelh
755781341e feat(desktop): auto-grow the Bot group composer for long prompts
The shared GroupMentionInput textarea was fixed at one row (rows=1,
max-h-40, resize disabled), so drafting or reviewing a long brief meant
scrolling through a tiny field (#95300). Size it from its content with
`field-sizing: content` — the idiom the Kanban drawer already uses —
bounded at min(50vh, 24rem) with internal scrolling past the cap, so the
transcript keeps its space. Applies to both the new-thread and the
reply-in-thread composer through the shared component; Enter-to-send,
Shift+Enter, IME guards and @mention completion are untouched.

Ported from PR #95308 (its target was the pre-TypeScript plugin.js) onto
the current group-chat-parts.tsx; one invariant test on the mounted
textarea and a Playwright spec that measures the real Electron composer
at one row, eight lines, and past the cap.
2026-09-16 21:49:19 -07:00
teknium1
8168bf1d35 test(desktop): count mention refs by data-ref, not by the shared .ref class
Through the shell renderer an e-mail address autolinks to a mailto rendered
by the shell's link component, which is also .ref-styled; only mention spans
carry data-ref, so the mention assertion counts those.
2026-09-16 21:39:30 -07:00
teknium1
48e0157b60 test(bot-mode): live Electron spec for mention rendering and Reply to @bot
Real Electron + hermes serve + mock inference: send a message mixing a
seated-bot mention, @user, an e-mail address and an unknown @token; assert
exactly two .ref spans (agent, human) on the rendered line, the stored log
text untouched, then click the bot reply's "Reply to Programmer" action and
assert the composer reads "@programmer " (idempotent on a second click).
2026-09-16 21:39:30 -07:00
teknium1
92760b936f test(desktop): routines pane spec writes screenshots to the test output dir
The spec hardcoded a /tmp rig directory for its four screenshots and left
console.log tracing in place. Screenshots now go through
testInfo.outputPath() like chat.spec.ts so they land in test-results/ on
any machine and CI; the debug logging and its helper are gone.
2026-09-16 17:14:45 -07:00
teknium1
8717a0bd39 fix(bot-mode): Routines rows fit the 250px pane and a closed Scheduled jobs pane comes back
A job row in the Scheduled jobs (Routines) pane is a grid item, and grid items
default to `min-width: auto`, so a long nowrap title made the row as wide as
its text (~530px inside the 250px pane). The pane's overflow clipped the
enable/disable Switch and the delete control clean off the right edge and the
title was hard-cut with no ellipsis (#91623); the next-run label on the
metadata line lost its tail the same way (#89534). `min-w-0` on the card and
on both lines lets the row shrink to the pane, the title ellipsizes, and the
schedule pill / next-run label wrap onto two lines instead of being cut
mid-word.

Closing the pane with its ✕ remembered the dismissal in
`hermes.desktop.dismissedPanes.v1`, and nothing ever showed the pane again —
it only exists by being registered while Bot Mode is on screen, so the pane
was gone until a full layout reset (#102224). The plugin now drops that
remembered Close each time it registers the pane (new `host.undismissPane`
door — un-dismiss + adopt only, unlike `revealPane` it never fronts the pane
or un-collapses its zone), so re-entering the Bots tab brings the pane back
as the collapsed right-edge tab it first arrived as.

Live-tested in the real Electron app with e2e/bot-routines-pane-narrow.spec.ts
(fails on origin/main, passes here); the same run confirms the row's Switch
toggles the job in place (#95031 already fixed by 253b9d78c).

Co-authored-by: MicroWearld <MicroWearld@users.noreply.github.com>
Co-authored-by: worlldz <worlldz@users.noreply.github.com>
Co-authored-by: 686f6c61 <686f6c61@users.noreply.github.com>
2026-09-16 17:14:45 -07:00
teknium1
71c5a0bd0a fix(desktop): localize the Bot Mode row context menu and attention hints
The roster's right-click menu on a bot row still spelled `Pin to top` /
`Unpin`, `Hide` / `Unhide`, `Groups: …` / `Manage groups…`, the pin/hide
toasts, the metadata-load error toasts, the `This device` gateway label
and the attention-badge tooltips as English literals, so a zh/ja reader
saw English items inside an otherwise translated menu (#91667, residual of
#88798 / #91336 after the bundle landed in a7db531a2a).

Route every one of them through the plugin bundle (`b.bot.*`, all four
locales) and turn `BOT_ATTENTION_HINTS` into `botAttentionHint(reason)`,
which reads `botsText()` at render so the tooltip follows the active
locale. No behaviour change; strings only.

Tests: bot-row.test.tsx renders the menu under `zh` and asserts the
catalog strings appear and no English literal survives (red on base).
e2e/bot-mode-roster-localized.spec.ts drives the real Electron app with
`display.language: zh`: right-click menu + a fresh group row (red on base,
green here).
2026-09-16 17:13:41 -07:00
teknium1
39c859185b test(desktop): read the code-block reply from the room body after re-selecting the room tab
A bot created moments before the room runs its intro turn in the background;
when it lands the roster fronts that bot's chat tab and the reply text found
by getByText sits outside any room body (null closest()). Scope the locator
to a room body and re-select the room inside the wait.
2026-09-16 17:11:44 -07:00
teknium1
c27d67a934 fix(bot-mode): resolve room MEDIA only for members on this gateway; make the code-block spec bite
`MessageTextContent` resolves `MEDIA:` paths against the ACTIVE gateway, so
a Connections Bot's `MEDIA:/path` in a cross-machine room was read from the
wrong machine (broken image, or a same-path local file). The room now passes
`media={false}` for remote-source members, leaving the directive as prose.

The live spec's LONG_LINE (~150 chars) never overflowed the message column,
so its "no clipped ancestor" assertion held on base too. The fixture is now a
600+ char unbroken token and the probe first asserts a scrolling `pre` with
scrollWidth > clientWidth exists before asserting nothing clips it.

Room-level vitest: the SDK mock exports a stub `MessageTextContent` and the
test asserts every body renders through it, with media resolution on for
local members and off for remote ones.
2026-09-16 17:11:44 -07:00
teknium1
9073bfb77c fix(bot-mode): group-chat replies render through the chat's message renderer
A bot's reply in a group room went through raw Streamdown. Its stock code
block lays the header and the body out as inline siblings, so the code sat
shifted right and its tail was clipped with no horizontal scrollbar (#91878);
the same raw path never ran the Desktop `MEDIA:` transform, so a bot's
`MEDIA:/path/file.png` showed as a plain path instead of an inline image or
player (#93728). The room now renders each entry with the SDK's
`MessageTextContent` (the 1:1 chat renderer plus `renderMediaTags`) — the
door the previous commit exports — behind a feature check, so an older shell
keeps the Streamdown fallback.

Live-tested in the real Electron app with
e2e/group-chat-code-block-and-media.spec.ts (fails on origin/main: the
`MEDIA:` directive is visible as text and the code block clips; passes here).
The SDK mock used by the group tests gains the new export so the feature
check exercises the fallback there.
2026-09-16 17:11:44 -07:00
teknium1
13f18198bd feat(desktop): open a bot's most recent session from its roster row
Add "Open recent session" to the bot row's context menu (#93054). The
left click stays on the canonical Bot Chat — that is the bot's stable
identity — but multi-task users keep their live work in ordinary
sessions (cron runs, delegated jobs, `+` side threads), and reaching the
freshest one meant hunting the mixed sidebar.

No new backend surface: `profiles.list` already ships `last_session`
(the newest row session.list would show; hidden Bot Chat / group member
sessions never win, tool/kanban workers are denied server-side). The
item opens that id through the same host.openSession contract the
canonical open uses, as a tab in the bot's workspace on its own
connection route, and falls back to the row click when the profile has
nothing listable yet. No tabTitle is passed: the stored row titles the
tab (a tabTitle equal to the title would caption it as the bot, the
canonical-chat rule, hiding which session it is).

Two invariant tests on the open path plus a Playwright spec against the
real Electron app; docs paragraph; bundled-plugin version bump.
2026-09-16 17:10:31 -07:00
teknium1
2679f47727 feat(desktop): pin a group chat to the top of the Bots roster
Group rows gain the Pin to top / Unpin context item bot rows already
have. The roster sort has honoured `room.pinned` since the room-order
work, but nothing ever wrote the flag, so a daily-driver room kept
sinking under fresher 1:1 chats (#89813). The pin lives on the room
record and rides the local group-chats persistence like `rosterOrder`
(sync: false) — presentation state stays out of the gateway mirror.

Also a Playwright spec that drives the real Electron app through both
group-row organisation paths (pin/unpin ordering + storage truth, and
filing into a user section via the row menu), plus the user-guide
paragraph, the contributor mapping for the salvaged commit, and the
bundled-plugin version bump the recent history uses.
2026-09-16 17:09:53 -07:00
teknium1
e803f16b6d test(bot-mode): front the room tab before typing in the member-editing spec
createAgent opens the new Bot's canonical Bot Chat, whose late hydration can
front a "Draft" tab over the room; the composer was then hidden at the send.
Bring the room tab back before each interaction with its composer.
2026-09-16 17:07:46 -07:00
teknium1
c94a959732 test(bot-mode): live spec for editing a room's members + docs
Playwright spec against the real Electron app and `hermes serve` with mock
inference: create a three-Bot roster, seat two in a room, open Group
settings → Manage members, prove the checklist is pre-checked, that Cancel
changes nothing, swap one member for another via the header door, then
verify storage truth (durable room descriptors AND each Bot's groups[]
metadata) and that the next unaddressed round seats exactly the saved
roster. Docs: the user guide describes the new Manage members door.
2026-09-16 17:07:46 -07:00
teknium1
b5a9efea06 fix(cli): inherit the launch profile's custom provider gateway with its model
A profile created without a clone source copies the launch profile's
`model` block so it can run. When that block points at a custom
`providers:` gateway (self-hosted / local endpoint), `model.provider:
my-gateway` alone is "Unknown provider 'my-gateway'" on the new bot's very
first turn — created, but unable to run, the same on-arrival failure
#101885 / #94071 describe for the built-in case (fixed in 01a3e9a44c).

launch_model_seed() returns the model block plus exactly that provider's
definition; both the CLI seed (_seed_model_config, also behind the
Desktop's profiles.create) and the gateway inheritance path
(_inherit_launch_model) write the same shape.

Live repro: e2e/new-bot-config-on-disk.spec.ts (New Bot → Fresh profile →
Create; asserts the created profile's config.yaml on disk and that the
intro turn completes) — red on origin/main with the banner "Unknown
provider 'mock'", green here.
2026-09-16 17:02:38 -07:00
teknium1
87a83d9225 test(desktop): e2e — hosted room turn on the Desktop backend releases the bot_room slot
Drives the REAL Electron app's spawned `hermes serve` backend over its
JSON-RPC socket (groups.create → groups.send → groups.log until the round
settles) and asserts storage truth: `profiles/sentinel/runtime/
active_sessions.json` holds no `bot_room` entry once the turn settled. On
the pre-fix build the entry stays (updated_at == started_at) until the
backend exits — the state that locked a second room worker out (#106847).
The Bots pane screenshot is the rendered-surface control.

Docs: note that several room workers can share a home and that a member's
room session is held only for its turn.

Refs #106847
2026-09-16 17:00:44 -07:00
teknium1
3e09f65e68 test(desktop): teammate DM survives an unreadable mailbox ticket beside it (Electron e2e)
Real Electron + real serve backend + mock inference: the Desktop holds alpha's Bot Chat open,
a chmod-000 ticket is planted in alpha's bot_live_delivery mailbox, then a real DM is admitted
through tools.bot_live_delivery. Fails on the pre-fix backend (admission degrades to
PermissionError / ambiguous, the gateway poll crashes every cycle, nothing renders); passes
with the tolerant scan (the DM renders as an inter-agent card).

Part of #109820
2026-09-16 16:55:05 -07:00
teknium1
a49865e263 test(desktop): e2e — Bots roster never lists profiles/ infra dirs or tombstones (#99392) 2026-09-16 16:48:42 -07:00
teknium1
9f959d68ae test(desktop): e2e — Bots roster loads with timestamp metadata in profile.yaml
Durable form of the live repro for #92506: seeds a profile whose profile.yaml
carries an unquoted ISO timestamp under ui_meta and asserts the Bots roster
renders rows instead of spinning forever. Fails on origin/main (0 rows after
60 s, profiles.list never answers); passes with the serialization guard and
producer coercion in this PR (3 rows in 2.2 s).
2026-09-16 16:48:05 -07:00
teknium1
51fc09ddd1 test(desktop): live e2e for CJK bot names in the New Bot dialog
Drives the real Electron app: type 小助手, Create Bot enables, the roster
row reads 小助手 and the profile lands at profiles/u5c0f-u52a9-u624b with
the entered name as its title. Red on origin/main (button stays disabled).
2026-09-16 16:47:24 -07:00
teknium1
61ab27e9f0 fix(desktop): name the Settings scope note by the same label as its chip
The "Applies to <profile>" note under the chips still printed the canonical
slug after the chips switched to the Bot title / display_name, so a bot
called "Atlas Prime" in the roster and on its chip was announced as
"alpha" one line below. Resolve the selected profile's presentation label
through the same profileLabel() precedence; the slug remains the identity.

Adds the Playwright e2e that drives the real Electron app with two seeded
profiles (one Bot Mode title, one display_name), opens Settings and asserts
the chips and the note read the presentation names (#109686).
2026-09-16 16:44:53 -07:00
teknium1
89f277020c fix(desktop): bot-workspace tiles drop the whole ambient composer selection; live Electron proof
Gate reasoning_effort and fast behind the same switch as model/provider in
desktopSessionCreateParams (includeComposerSelection): a Bot-workspace tile
targets a different profile without switching the window's composer, so all
four fields of an unrelated session's pick would otherwise ride into its
session.create. Omitting them lets the bot profile's configured defaults apply.

The vitest added for this PR left the hoisted requestGatewayForAgent mock
with a recorded call (restoreAllMocks only restores spies), which failed the
next test's not-called assertion in CI ("keeps an unlisted named local
legacy-profile tile owned by its bare profile"). The test now resets that mock
and the composer atoms it set, and asserts the two new omitted fields.

New Playwright spec bot-tile-ignores-ambient-composer-model.spec.ts drives the
real app: open a bot's canonical chat, pick a second model from the composer
menu (the mock provider now lists extraModels; receivedModels records the
model of every completion request), Ctrl+T a side chat, send a turn, and
assert the inference request carried the profile default. Fails on main's
index.ts (request carried the ambient pick), passes on head.
2026-09-16 16:44:13 -07:00
teknium1
0e746b213b test(desktop): e2e — archived+hidden session is listed in Settings → Sessions
Real Electron + real `hermes serve`: seed one archived+hidden row and one
plain archived control through SessionDB, open the archived view and
assert both rows render with their Unarchive button. Fails on the
previous `list_sessions_rich` (only the control row lists), passes with
the archived-only listing fix.
2026-09-16 16:43:32 -07:00
jango
783a06d070 [verified] test(desktop): trim runtime probe coverage
(cherry picked from commit 83b7fed46834988f66051f73dab5c65dbf47a672)
2026-09-15 10:50:53 +05:30
jango
fa0f1986dc test(desktop): verify actual Electron browser identity across platforms
(cherry picked from commit 6cd0a15ed98ced17fe1bf1d6e81e2201aefc1fd2)
2026-09-15 10:50:53 +05:30
jango
e270766fe5 test(desktop): detect transient stale spawns and native path separators
(cherry picked from commit 807ef07f71a3ddceb63c8ee1d688739ab6ab9d62)
2026-09-15 10:50:53 +05:30
jango
78399c88da test(desktop): verify probe responsiveness and stale-start cancellation
(cherry picked from commit c12f7e0f677a234e7e5057a13485e39346da0d03)
2026-09-15 10:50:53 +05:30
teknium1
9326d9cdc4 fix(bot-mode): show the current thread and newest unresolved failure
Attribute drive-level errors to the thread being drained, not the send
that created the queue. Reinsert repeat member failures in recency order
so the collapsed activity row cannot show an older sibling failure.

Cover both invariants and the repeated-refusal sequence in native Desktop.
Thanks to @kvnloo for identifying both review findings.
2026-09-14 17:35:04 -07:00
teknium1
3040c87ac1 fix(bot-mode): do not retry failures from already queued sends
Keep failed-member exclusion across the room queue, rather than resetting
it per pending thread. A new user action after failure still permits a new
attempt. Share the drain activity epoch so a skipped queued thread cannot
hide the preceding member failure.

Proven red in real Electron: hold transport refusal, enqueue same-thread
and cross-thread sends, then release; old head submits three times, fixed
head once. Strengthen follow-up evidence with distinct provider replies,
exact public log order/count, and per-input inference counts.
2026-09-14 17:35:04 -07:00
teknium1
e80642df60 fix(bot-mode): keep group follow-ups ordered and late answers visible
Serialize room drives through their actual member completion, freeze input
watermarks by retained entry identity, and share the same completion path
with handoff continuations. Stop discards queued work without releasing an
active owner early; rename follows the existing room binding.

Observe stranded replies for the hard-cap duration plus grace after the
foreground wait, and retain unresolved failures in collapsed Activity.
Never automatically retry an ambiguous failed submit within the same drive.

Adapted from the queue and boundary approach in #92041 by @enwaiax and
harvest-budget approach in #107193 by @Finn763; #106502 by @wadib identified
failed-submit watermark consumption. The implementation retains current
numeric watermark storage, room lifecycle bindings and serial round limits.

Related: #92003, #105247, #100026
2026-09-14 17:35:04 -07:00
teknium1
1f1f02e354 fix(desktop): preserve qualified bot identity in primary handoffs
Keep the salvaged botHandle normalization, but remove the unconditional
hermes alias on remote default profiles: the parser's last-wins map
otherwise retargets a local @hermes handoff by roster order.

Consolidate regression coverage into two invariants for persisted primary
handles, both handoff directions and three-source qualified identity.
Capture real Electron screenshots, durable logs and source receipts;
exercise the reverse live handoff too. Document the repair and bump the
bundled Desktop patch version.
2026-09-14 17:04:27 -07:00
teknium1
52972d15f3 test(desktop): live e2e for a teammate's @hermes handoff driving the primary bot
Real-Electron Playwright spec for #100406: a two-member room (primary
profile + code-farmer). The user addresses only @code-farmer; its
scripted reply @mentions hermes; the assertion is a `default`-authored
"B" entry in the persisted room log. On origin/main the room settles
after Code Farmer's line and the spec fails at that assertion; with the
mention-alias fix it passes.

The mock inference server gains a per-speaker script for group rooms:
`E2E_SAY(<handle>)[<line>]` tokens in the user's send answer the member
whose turn prompt opens with `You are @<handle>`; unscripted members
reply "(pass)". `{at}` stands for `@` so the script itself never
mentions anyone and round one only drives the member the user tagged.
2026-09-14 17:04:27 -07:00
Kuxin
a318772f51 fix(test): raise mock model context window 2026-09-12 08:30:27 -07:00
brooklyn!
4f1966edac feat(desktop): connector cards that wait for the sign-in, and a guided first launch that holds together (#108292)
* feat(desktop): give Button a loading prop that swaps label for spinner without layout shift

The label stays in the box, invisible, and the spinner is absolutely
centred over it, so a Connect or Approve button keeps its width while it
works instead of collapsing to a spinner. The approval bar had the same
thrash and moves onto it.

* refactor(desktop): one consent card for connectors and MCP setup

McpSetupTool rendered its own copy of the connector card's markup. It now
renders ConnectorCard for the pending question and ConnectorSummary once
settled, and the card gains what MCP needed: keyboard accelerators, a
source line, a question heading. The card also gets an avatar variant
(40px mark in the left gutter, text and buttons on one column) and a
collapseWhenSettled switch so a connector can stay a full card with a
green Connected pill in the action slot while MCP keeps its one-line
summary. Brand marks for Gmail, Calendar, Drive, Discord, Telegram and
Spotify; Slack via Tabler because simple-icons dropped the mark.

* feat(desktop): connector card drives the agent through manage_connections wait

The offer used to end in a Continue in chat button, and the agent, seeing
an unconnected status, would improvise around the app. Now the card does
what the TUI does. Clicking Connect opens the browser and sends one hidden
line telling the agent to park in manage_connections action=wait for that
slug and to never call connect again (a second link cancels the one being
signed into). Not now sends its own line. A hidden request that lands
while the turn is busy steers it, or queues if the turn just ended.

Which call owns the live card changes too: consecutive calls naming the
same apps are one exchange (connect, the wait, the status that follows),
and the first of the last exchange is the card, so the agent's wait no
longer demotes the card mid-authorization and mints a fresh one below it.
A targeted ask renders one or two bare cards; only a real catalog gets the
header, search and refresh.

* feat(desktop): onboarding connects apps in chat and keeps tasks finishable without them

The welcome chat knew connectors only as preferences to pick and wire up
later, so asked to connect Gmail it invented a Settings page that does not
exist. Both scripts now carry one rule set: status once, one batched
connect for every app named, the card is the ask so write a line and end
the turn, never route around a declined app with another client or
credential. The build handoff checks real connection status instead of
asserting none are connected, and the first task must be finishable, not
free of, the apps they picked. The connectors card explains what
connecting means and reports the count on its Continue button.

* fix(tools): resolve the Nous identity for share_auth profiles in the connector gate

A profile created with share_auth has no auth.json of its own and signs
in through the root store. Every other credential reader falls back to
the global root; the connector gate read HERMES_HOME/auth.json directly,
saw nothing, and stripped manage_connections from the profile's tool
list, so the welcome chat's agent truthfully reported the tool missing.
The gate now goes through get_provider_auth_state.

* fix(agent): name a provider retry backoff on the live status line

The retry status is buffered and replays only when every retry fails, so
during a 60s backoff after a 5xx the user saw a bare spinner. Right after
a connector sign-in landed this read as the agent going silent. The
backoff now also rewrites the live wait notice, which the desktop already
renders in the thread status row; it is transient and clears on recovery.

* test(desktop): connector rehearsal launcher and flagged connector spec

connector-rehearsal.mjs starts the real desktop and backend under a fresh
HERMES_HOME with no copied credentials, a fixed Vite port and CDP on 9344,
so the onboarding connector flow can be driven end to end by hand or from
outside. The Playwright spec covers the flagged connector step.

* fix(desktop): send the agent back into wait when the user keeps waiting after a timeout

The card's Keep waiting re-entered the poll but the agent's own wait had
timed out too and nothing told it to go back in, so it would start
talking mid-authorization. keepWaiting now fires onWaiting like connect
does. Tests also pin that an expired or revoked grant asks the gateway
for reconnect, not connect.

* style(desktop): blank lines in connector-flow test per lint

* feat(desktop): HERMES_SKIP_INTRO=1 / --skip-intro skips the first-run film

The intro is a one-time reveal, so anyone rehearsing the guided chat behind
it sits through it on every fresh HERMES_HOME. The flag rides the existing
launch-flags path (main → preload → renderer) next to guestOnboarding and
only gates isIntroRevealEnabled; the backend never sees it. The rehearsal
launcher sets it.

* fix(desktop): onboarding card Continue stays Done after the transcript rebuilds

The card kept its Done flag in component state. The hidden submit and the
turn-end hydrate both rebuild the message list, so the card remounted with
the flag false and Continue came back live, letting a step be answered
twice. The committed steps now live with the other onboarding answers,
keyed by step, and the first-build chip pick rides the same store.
remember_onboarding projects by key, so the new field never reaches USER.md.

* fix(desktop): no provider picker or free-tier chip over the guided first launch

Two sign-in surfaces leaked into the guide. A credential probe on the
setup profile (a free-tier token mid refresh, a session before its runtime
settled) hit requestDesktopOnboarding and dropped the provider picker over
the chat the user was in; and the statusbar free-tier chip sat there
offering a second sign-in the whole time. Both now yield while the gate
phase is cinematic, guided or handoff. The free tier is the provider for
those phases, and the guide offers sign-in on its own ready screen.

* fix(desktop): onboarding connector picks are real catalog slugs

The picker offered Spotify, GitHub and Stripe, none of which the deployed
connector catalog carries, and spelled Calendar and Drive with hyphens the
gateway does not use. A pick the build chat could not honour ended as
"Spotify isn't in the connector list" after the user had been told to
expect it. The list is now twelve slugs from the live status catalog,
spelled as the gateway spells them; GitHub is out (the terminal has git
and gh), chat channels stay on Messaging. Marks for the new entries; the
Google marks answer both spellings. The build runbook offers the picked
connections in its first turn rather than after the work is underway.

* fix(desktop): the free-tier ready screen never interrupts the guided chat

A readiness round fires when the layout pick assembles the window, and it
raised the free-tier ready screen over the conversation: the user was
dropped into the main app, dismissed it, and came back to a card they had
already answered. The guide is the introduction. The ready screen now
yields while the gate is cinematic, guided or handoff, and the notice is
acked the moment the guided chat takes the screen, not only when the film
does, so a skipped film no longer leaves it pending.

* feat(desktop): tour options that lead to building, and a fork that follows the tour

"Just the basics" and "Show me around" read as a click-through with no
exit; "I'll figure it out" read as declining help. Now Quick tour, Show me
everything, and Skip, let's build something. The script also folds the
fork into the same turn as the tour, so when the user closes the overlay
the next ask is already waiting instead of a transcript that ends on the
tour call.

* feat(desktop): the onboarding connector picker reads the live catalog

A hardcoded list, however carefully copied from today's catalog, is the
next drift. The picker now asks connectors.list through the same
session-owned RPC the connector cards use and offers exactly what the
gateway carries: a curated lead order puts the everyday apps first, chat
channels stay on Messaging, everything else is reachable by search. The
picks are gateway slugs, handed straight to manage_connections. No
catalog (toolset off, gateway unreachable) ends the step honestly with
Skip instead of inventing apps.

* test(desktop): the guided first launch never forces a sign-in

The acceptance criterion the guided onboarding was built to, as a test:
while the gate is cinematic, guided or handoff, the provider picker does
not open and a credential warning is dropped rather than deferred to the
next send. Outside the guide the picker opens as before. Red against the
tree before the guards landed (6 of 9).

* fix(desktop): a relaunch mid-guide resumes the guide, in the guide's shape

Closing the app during the guided first launch and reopening it booted the
normal shell around the persisted solo layout: the connecting splash, the
stock composer and model picker, a small window whose sidebars would not
open, while the gate still read guided. The gate now queues a kickoff for
the guided phase too (the kickoff adopts the existing guide chat by title),
takes the solo shape before the gateway opens rather than after, and the
connecting overlay yields to the guide's own opening. A typed reply in the
composer now closes an ask card and the first-build chips the same way a
click does; the layout card's Continue comes back Done.

* style(desktop): one answeredAfter helper for the ask card and first-build chips

* fix(desktop): the guide takes its shape on the tick the film ends, not after the window shows

Between the film and the greeting the full-size shell painted for a beat:
finishIntroReveal showed the main window, then the kickoff shrank it once
the setup profile answered. The listener on the intro's hidden edge now
takes the guide's shape (solo layout + small centred window) synchronously,
so the window is already the guide when it is shown. One takeGuideShape
owns the pair; kickoff and the boot gate call it idempotently.

* style(desktop): the 'nothing connects yet' line reads first on the connectors card
2026-09-12 10:00:06 +05:30
emozilla
4e865d42d1 fix(desktop): restore flat glass surfaces without text bleed 2026-09-11 13:41:05 -05:00
brooklyn!
8d092c2f71 fix(desktop): validate remote OAuth through ticket minting
Handle truncated OAuth responses and keep confirmed auth recovery stable. Preserve the current attempt guard before latching failures; the older pre-guard latch proposal is not carried forward.

Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
Co-authored-by: FalconOrtiz <falcon.ortiz11@gmail.com>
Co-authored-by: Ugo Enyioha <ugo.enyioha@outlook.com>
Co-authored-by: Bartok9 <259807879+Bartok9@users.noreply.github.com>
2026-09-11 07:14:55 -05:00
kshitijk4poor
0dd6bee93a refactor(desktop): route every REST status error through httpStatusError
fetchJsonViaOauthSession and finalizeGatewayDownload still hand-rolled the
`Error("<status>: <body>")` + statusCode shape the helper was introduced to
consolidate, so the "shared by all three paths" claim was false on landing.
The download-transport source test now asserts the helper call.
2026-09-11 12:53:07 +05:30
Zeus-Deus
8d22781b1c fix(desktop): latch an expired remote OAuth session on the first confirmed 401 instead of flickering the Sign in overlay (#95701)
Cold-launching against a gated remote gateway whose stored session has been
invalidated server-side alternated between the connecting state and the
recovery overlay; the Sign in button was only intermittently clickable.

Root cause: fetchJson() built a bare Error("401: ...") for HTTP failures on
the native-bearer path, dropping res.statusCode. Every downstream classifier
is shape-based (isGatewayAuthRejection, isServerSideHttpError, the
ensureNativeAccessToken 401 check), so the confirmed rejection looked like a
transport blip: withTransientRetries hammered it, gatewayTicketFailure used
the transport copy, startHermes tagged the boot retryable, and the renderer's
bounded boot-retry loop re-emitted running:true over the overlay on every
attempt. Separately, gatewayTicketFailure only ever set needsOauthLogin,
which isReauthRequiredError ignores, so even a structured 401 from the cookie
path never latched.

- api-transport: httpStatusError() is the one HTTP-error shape; fetchJson and
  fetchPublicJson use it, matching fetchJsonViaOauthSession.
- mintGatewayWsTicket: the gateway never rotates a native bearer server-side
  (dashboard_auth/middleware.py), so a bearer 401 gets ONE forced
  /auth/native/refresh; a live refresh token retries the mint once, a dead one
  drops the stored tokens and the rejection is confirmed.
- gatewayTicketFailure: a confirmed 401/403 is tagged isReauthRequired so
  startHermes latches it and marks the boot non-retryable.
- startHermes: latches are set before the first await in the failure path;
  updateBootProgress holds every update that is not a re-emit of the latched
  failure until a recovery path clears the latch.

Tests: composition + main.ts source pins (remote-reauth-latch.test.ts), unit
coverage for the new helpers, and a Playwright e2e that boots the real app
against a fake gateway with a dead session and proves the overlay latches
once (one mint, one refresh, retryable:false, Sign in stays clickable).
2026-09-11 12:53:07 +05:30
brooklyn!
3cad0f312b fix(desktop): keep visible renderer animations running on blur 2026-09-08 21:53:33 -05:00
ethernet
cf9d341e4b fix(install-e2e): verify input readiness and tolerate transient windows 2026-09-06 12:55:05 -04:00
ethernet
7cfe82a2a7 fix(install-e2e): handle startup zoom restore and legacy Unicode output 2026-09-06 00:36:02 -04:00