Commit Graph

7385 Commits

Author SHA1 Message Date
hermes-seaeye[bot]
f7da80073a fmt(js): npm run fix on merge (#131251)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-02 06:22:10 +00:00
teknium1
b33c32fcf3 feat(desktop-sdk): data-session-switching marker on the chat surface
Theme CSS had no supported way to style a chat switch: t3-code-theme's
installSwitchFade watched the focus store, polled transcript rows and
scroll position in a rAF loop and toggled its own root attribute — DOM
reads catalog rule 8 now refuses.

Core already knows both phases of a switch, so it publishes them: the
chat surface root ([data-chat-surface], per pane) carries
data-session-switching="true" while a routed session is loading
(Thread loading === 'session') and while the thread list's scroll
restore places the new rows, removed on settle, user input, failed
resume or unmount. Each phase holds a token; the attribute goes when the
last holder releases, so the load ending never clears the restore's
mark mid-switch. An attribute (not a view-transition or motion token)
because the need is "know when", the duration is core's real settle
time, and CSS composes it with any theme without JS.
2026-10-01 23:09:43 -07:00
teknium1
c0c029c376 feat(desktop-sdk): model menu row decorations (MODEL_MENU_ROW_AREA)
Plugins could relabel the model pill (COMPOSER_AREAS.modelPill) but the
menu it opens was core-only, so a theme wanting provider marks next to
each model (t3-code-theme, catalog #129600) had to MutationObserve the
open menu and paint into its rows, which catalog rule 8 now refuses.

A `modelMenu.row` data contribution supplies decorate({ provider, model,
label }) -> { icon?, badge? } | null. ModelFamilyRow paints the answer in
two fixed slots it owns: a leading 1rem icon box before the name and a
trailing chip after core's own chips. Per slot the first usable answer in
registry order wins, so an icon plugin and a badge plugin compose; a
throwing decorator or a non-element/non-string value declines, and the
icon renders inside its own error boundary so a crashing plugin component
blanks only its slot, never the menu. Applies to every ModelCatalogMenu
host (composer pill, kanban override), since a mark belongs to the model.
2026-10-01 23:09:43 -07:00
teknium1
e9d7a18dc5 fix(desktop): embed host answers malformed paths with 404 instead of throwing
`new URL("//", origin)` threw inside the http request handler, so a
request like `GET //` escaped as an uncaught main-process TypeError and
was never answered. Reject unparseable targets up front.
2026-10-01 22:27:30 -07:00
teknium1
9b6e759751 fix(desktop): one Classic Hermes card after the reverted build, and a light Classic
A Desktop that ran the reverted #130015 build has the CLI `default` skin
cached under `hermes-desktop-backend-themes-v1`, labelled "Classic Hermes".
readCached only dropped built-in names, so that entry survived next to the
new `classic` theme: two Classic cards, the stale one previewing gold and
applying Nous, and two Classic rows in /skin list. readCached now also drops
every retired name (`default`, `gold`, `nous-light`, moved to presets.ts as
RETIRED_SKINS so the store and the theme provider share it) and writes the
cleaned map back, so the entry is gone from disk on the first launch. The
same rule keeps ingestBackendSkin from registering a theme under a retired
name.

Classic also ignored light mode: it took the single-mode converter output,
dark palette in both slots. Its light variant now comes from the CLI
default skin's `light_colors` ladder (#F5F5F5 surface, goldenrod accent)
with a deeper ink of the same brown, so Desktop's 74% / 54% secondary and
tertiary text land at Nous Light's contrast (6.5:1 / 3.5:1; body 12.3:1).
Dark is unchanged. The description now reads "Gold on navy, the CLI's
original look" and the `as DesktopTheme` cast is gone.
2026-10-01 22:27:00 -07:00
teknium1
77f54c5c35 feat(desktop): offer Classic Hermes gold/navy as an explicit theme pick (#76579)
Desktop users can now choose the CLI's classic gold-on-navy look in
Settings > Appearance (and via /skin classic, gold or hermes).

It ships as a Desktop built-in under its own id `classic`, converted from
the CLI `default` skin colors through the same skinToDesktopTheme path a
backend skin takes. It is deliberately NOT registered under `default`:
stock config ships `display.skin: default`, and backend sync / the boot
fallback adopt that value, so un-retiring `default` (the reverted #130015)
repainted every user who never picked a theme in Classic gold and cached
it. `default` stays retired and aliased to Nous; Classic is only painted
when someone picks it. Desktop never writes its pick to display.skin, so
the pick stays Desktop-local like every other Desktop theme.

Re-lands #130015 (reverted in #130852).

Co-authored-by: 686f6c61 <github@00b.tech>
2026-10-01 22:27:00 -07:00
teknium1
b5be2a3c8b refactor(desktop): render shell menu items directly
Call renderActionItem instead of declaring an Item component inside
shellSections, which minted a new component type on every render and
remounted the menu rows. Restore the tree-group file doc comment above
its imports.
2026-10-01 22:15:59 -07:00
unsupportedpastels
63867308bb test(desktop): consolidate pane ownership invariant coverage 2026-10-01 22:15:59 -07:00
unsupportedpastels
40a2637fae fix(desktop): scope pane fallback menus to the clicked content
Keep explicit nested menus and bare pane operations while restoring text, editable, link, and image menus. Reuse the app actions in a fallback section of the pane-body menu.
2026-10-01 22:15:59 -07:00
Yuan Li
49f5f78e50 test(desktop): cover editable targets inside context triggers
(cherry picked from commit 9b77c43048a040e77084efc376fde1530ddee617)
2026-10-01 22:15:59 -07:00
Yuan Li
f3f6a4d068 fix(desktop): prioritize edit targets in pane context menus
(cherry picked from commit 316d17c89e08a3d1f1e39e42d903de4c40874c3f)
2026-10-01 22:15:59 -07:00
Brooklyn Nicholson
7817bf522a fix(desktop): play YouTube embeds through a loopback player host
YouTube shows error 153 unless the page embedding its player has an http(s)
origin matching the player's `origin` param; the packaged renderer is file://
and Electron drops stamped Referer headers. Serve one wrapper page per video
from a lazily started 127.0.0.1 host in the main process and point the
file:// renderer's YouTube iframe at it. The renderer itself stays on file://,
so settings and backend connections don't move. Dev/web renderers, which
already have an http origin, keep embedding directly, and so does a renderer
bundle running on a main process without the host.
2026-10-01 18:32:06 -05:00
teknium1
e05b16348b refactor(desktop): drop the chunk checker's unreachable empty-assets return
checkDistBuilt already fails an assets/ directory with no .js before it calls
verifyChunksParse, so the early return could never run.
2026-10-01 15:28:55 -07:00
teknium1
0954ad4b9b fix(desktop): verify the renderer bundle once per build, not twice
npm runs the `postbuild` hook after every `npm run build`, and the desktop
compiler (scripts/build/desktop.mjs) already runs checkDistBuilt on the
scratch product before it publishes dist/. Every update and every pack
therefore re-verified the bytes it had just verified; with the per-chunk
syntax check that was a second ~960-process pass (#123216).

Drop the hook; nix's checkPhase calls the guard directly.
2026-10-01 15:28:55 -07:00
funky-xamarin
c7d193a127 fix(desktop): reuse running Node for renderer syntax checks 2026-10-01 15:28:55 -07:00
Chip
94c15c7b70 fix(desktop): syntax-check every renderer chunk in one process
`assert-dist-built.mjs` validated the emitted renderer chunks by spawning
`node --input-type=module --check` once per chunk. A real bundle here emits
~960 `.js` chunks (31 MB), so the postbuild step performed ~960 sequential
process creations: 47-80 s wall clock measured on an idle machine (median
77 ms per spawn, worst 446 ms). That made the update's build step depend on
~960 successful process creations.

On Windows one spawn stalled past its 60 s budget and returned ETIMEDOUT
(`could not run node to syntax-check nord-D92QzZWU.js`), which failed
`npm run build` and aborted the entire update (receipt outcome "failed",
exit 1) even though vite had reported `built in 54.73s`. Nothing was wrong
with the bundle or with node - a single process creation was slow.

Parse every chunk in one child instead: `scripts/check-chunks-parse.mjs`
uses `vm.SourceTextModule` - the in-process module parser, hence
`--experimental-vm-modules` - and writes a JSON verdict on stdout. It
reports the same early errors as `node --check`: invalid destructuring
patterns and duplicate declarations still fail, so the guard keeps catching
the corrupted-silent-fail bundle it was written for. Same verification now
costs one process creation and ~1.5 s instead of ~960 spawns and 47-80 s.

The verdict carries `kind`, and the guard reports `harness` when the check
could not run versus `bundle` when the build is defective. The line is drawn
at the checker: only a SyntaxError is a bundle defect; an unreadable chunk or
an unavailable vm API is a failure of the check itself. Previously every
failure printed "The renderer bundle is missing or incomplete ... Re-run the
build", so a blocked or stalled node process was reported as a bundle defect
and sent at least one investigation after a problem that did not exist.
2026-10-01 15:28:55 -07:00
hermes-seaeye[bot]
6d49922875 fmt(js): npm run fix on merge (#130953)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-01 21:54:23 +00:00
kshitijk4poor
f9f132915f fix(desktop): remount social embeds per descriptor and share the default height
The social renderer keeps its measured height in state, so React reusing
the instance for a different X/Instagram descriptor carried the previous
post's height over; keying it on descriptor.id gives each post a fresh
frame and height.

Also tighten the module: SOCIAL_FRAME_SANDBOX, socialFrameSrc and
reportedHeight have no outside users, so they are no longer exported;
FRAME_ORIGIN is typed by EmbedProvider so a misspelled key fails to
compile; and the 320px fallback duplicated across social-embed,
embed-consent and url-embed now lives once in embed-size.ts as
EMBED_DEFAULT_H.
2026-10-02 03:12:45 +05:30
kshitijk4poor
72d8350707 test(desktop): add a positive control to the social embed origin test
The origin/source rejection test only asserted that the height stayed at
450px, which also passes if the message listener is never attached. A
valid MEASURE from the frame's own origin and window must now resize it
to 999px, so the test goes red when the listener is removed.
2026-10-02 03:12:45 +05:30
kshitijk4poor
8dfcfc02c7 test(desktop): trim social embed tests to the isolation invariants
Keep the two tests that guard the security boundary: X/Instagram render in
a sandboxed cross-origin iframe with no script injected into the app
document, and height messages from a foreign origin or window are ignored.
Theme/resize-shape/parser cases pin vendor message details rather than the
fix.
2026-10-02 03:12:45 +05:30
FroRaut
e1bc6064f9 fix(desktop): render X and Instagram embeds in isolated iframes
The tweet and Instagram renderers injected the vendors' widgets.js/embed.js
into the app document, which also hosts the preload bridge
(window.hermesDesktop). A compromised or hostile vendor script would run with
full access to that bridge. Render both in a sandboxed cross-origin iframe
instead and size it from the embed page's own height messages, accepting
them only from the vendor origin and the frame's own window.

Hand rebuild of #129637 onto current main: keeps the LazyRenderer autoplay
signature, deletes the now-unused escape-html.ts.

Salvage of #129637.
2026-10-02 03:12:45 +05:30
kshitijk4poor
c03dc30158 fix(desktop): key the redirect header strip on the request, not on header values
The out-of-scope strip matched any request header whose value the session
had ever injected, so an unrelated request that happened to carry the same
name+value (e.g. an identity provider using the same API key header) had it
silently removed, and the value set grew without bound for the session's
lifetime.

Track the header names injected per webRequest id instead: an out-of-scope
hop of that same request drops them, every other request is untouched.
Entries are forgotten on webRequest.onCompleted/onErrorOccurred so the map
stays bounded by in-flight requests.

Also: the tracking map is now a required parameter (no silent per-call
default that disables stripping), requests with nothing to add and nothing
tracked take a callback({}) fast path, redundant String() coercions are
gone, and the configured headers are walked once. The test swaps the
production-forbidden Authorization header for X-Api-Key and pins that an
unrelated request with the same name and value is not stripped, including
after the original request id completes.
2026-10-02 02:51:57 +05:30
FroRaut
12c1b80ec7 fix(desktop): strip configured remote headers from out-of-scope redirect hops
Chromium carries app-set request headers (Authorization, CF-Access-*) across
redirects, so a gateway that 30x-redirects to another origin or a path outside
the connection's scope leaked those credentials to the redirect target.
applyRemoteRequestHeaders now remembers every configured header value the
session injected and removes it from hops that are outside its gateway scope,
re-applying only the headers resolved for the current URL. Unrelated requests
that happen to use the same header name with a different value are untouched.

Hand rebuild of #129625 onto current main: keeps the withEmbedRefererStamp
wrapper on the single onBeforeSendHeaders listener, drops the live Electron
fixture/test.

Salvage of #129625.
2026-10-02 02:51:57 +05:30
teknium1
eb049af69c Revert "feat(desktop): copy and Bot Mode group turns follow no-deadline prompts"
This reverts commit 0ea07255e0.
2026-10-01 13:23:12 -07:00
teknium1
eb481d0cd9 Revert "test(desktop): a group turn blocked on the user's approval outlives the hard cap"
This reverts commit 58dffc74ef.
2026-10-01 13:23:12 -07:00
teknium1
2bde65f825 Revert "style(desktop): prettier wrap for the longer settings descriptions"
This reverts commit 714d859171.
2026-10-01 13:23:12 -07:00
teknium1
423eef696e Revert "chore: review follow-ups — stale timeout comments, softer docs claim, group-turn cap credits only user-blocked intervals"
This reverts commit bdf4a5d632.
2026-10-01 13:23:12 -07:00
teknium1
03477e9ae2 Revert "fix(desktop): restore YouTube embeds in the packaged app"
This reverts commit d428df2a65.
2026-10-01 13:00:19 -07:00
teknium1
47c3a6c5e8 Revert "fix(desktop): compose the YouTube embed Referer stamp into the default session listener"
This reverts commit 9abeb829df.
2026-10-01 13:00:19 -07:00
teknium1
ad94cc0f7f Revert "chore(desktop): sort main.ts imports for perfectionist/sort-imports"
This reverts commit 587e673e2a.
2026-10-01 13:00:19 -07:00
teknium1
7cfaff9a17 Revert "fix(desktop): register classic default skin palette in Appearance"
This reverts commit 743d3a4119.
2026-10-01 13:00:19 -07:00
teknium1
a45aa85d52 Revert "fix(desktop): allow setTheme(default) — drop default from RETIRED_SKINS"
This reverts commit d753222ee7.
2026-10-01 13:00:19 -07:00
teknium1
eec3864a67 Revert "test(desktop): ThemeProvider setTheme(default) paints Classic Hermes"
This reverts commit c356d1df82.
2026-10-01 13:00:19 -07:00
teknium1
88d1672f1b Revert "test(desktop): fix default-skin tests against current main"
This reverts commit ac6079b329.
2026-10-01 13:00:19 -07:00
teknium1
a1e867f58c Revert "style(desktop): blank-line nits in default-skin theme files"
This reverts commit aeff051a18.
2026-10-01 13:00:19 -07:00
teknium1
c1c065567f Revert "fix(desktop): scope preview tabs to the session that opened them"
This reverts commit b3e896246f.
2026-10-01 13:00:19 -07:00
teknium1
f5cab618c1 Revert "fix(desktop): harden preview session-scoping reuse paths"
This reverts commit 824e107cb7.
2026-10-01 13:00:19 -07:00
teknium1
b2aeb0009e Revert "fix(desktop): keep minted Browser tab ids across restore; complete test mocks for session-focus"
This reverts commit 310c7ae9e5.
2026-10-01 13:00:19 -07:00
teknium1
bed3ae12ff Revert "fix(desktop): keep popped Browser tabs out of docked closes after the session-scope rebase"
This reverts commit cb9cb968ee.
2026-10-01 13:00:19 -07:00
teknium1
c1946935be Revert "test(desktop): read the session-scoped tab id in the tombstone test"
This reverts commit 572e4f4fad.
2026-10-01 13:00:19 -07:00
kshitijk4poor
d022320b1c test(desktop): reuse shared deferred helper in projects store tests
projects.test.ts carried a file-local deferred() that only exposed
resolve, so the missing-RPC test hand-rolled its own captured-reject
promise. Import the shared @/test/deferred helper (resolve + reject)
instead, drop the duplicate, and settle the stale-owner projects.create
rejection through pendingCreate.reject. No behaviour change; the seven
existing deferred callers work unchanged.
2026-10-02 00:49:01 +05:30
kshitijk4poor
b6f9255a2c refactor(desktop): single owner predicate and live edit scope key
activeProjectsContext inlined the same gateway/profile comparison as
stillOnWritableProjectOwner; reuse the helper so the writable-owner rule
lives in one place (behaviour-equivalent).

fileEditScopeKey mixed a render snapshot (beginEdit passed fsCacheKey)
with a live API-route read; read both halves live so begin and save
compare like with like. The save-scope test now asserts the exact scope
error instead of accepting "edit mode exited" as an alternative.
2026-10-02 00:49:01 +05:30
kshitijk4poor
525f7b9910 test(desktop): pin owner guard on missing projects.create RPC
The catch branch only latches $projectsRpcAvailable=false when the
rejection still belongs to the current gateway/profile, but nothing
exercised it (forcing the guard to `if (true)` kept everything green).
Extend the existing missing-method test with an owner-switch case: a
late missing-method rejection after a profile switch still surfaces the
stale-backend error but must not mark the new owner's backend stale.

Also trim the owner-switch createProject test: the writer is asserted
never to be called, so the temp IDEA.md files and routing mock were dead.
2026-10-02 00:49:01 +05:30
FroRaut
f0f98aea35 fix(desktop): keep project idea writes on their owning connection
(cherry picked from commit f968d4ca8d57644eb35eb1b0ac7bca6a26cd6f27)
2026-10-02 00:49:01 +05:30
FroRaut
6e5013f343 fix(desktop): prevent preview saves crossing connection boundaries
(cherry picked from commit d92993cabc033ea11582899faf6feba0b08748a3)
2026-10-02 00:49:01 +05:30
kshitijk4poor
6afef023c8 test(desktop): pin the grafted residue rows and fix the guard-loop comment
The own-residue delegate test only checked the seeded transcript had two
messages, which would also pass for any two wrong rows; assert the exact
server row ids [1, 2] instead (drops the redundant objectContaining line).

The stale-guard-loop header still said both submit paths refuse on any
non-null refresh; they now refuse only a competingView surplus and graft
own residue then send, so say that.
2026-10-02 00:43:17 +05:30
kshitijk4poor
07f1dfdc76 refactor(desktop): make transcriptRefreshIfBehind's doc stand alone
Its doc opened "Same read, plus …", pointing at refreshIfTranscriptStale,
which the previous commit deleted. That deletion also dropped the only
statement of the null contract (null when current or when the read fails,
so a missing profile or down backend cannot soft-lock send). Restate both
here so the surviving export documents itself.
2026-10-02 00:43:17 +05:30
kshitijk4poor
ff5b4ba6ef refactor(desktop): drop dead refreshIfTranscriptStale export
Its last caller (the tile delegate) now uses transcriptRefreshIfBehind,
which distinguishes own residue from a competing view. Keeping the
residue-blind variant exported invites the same #130031 regression.
2026-10-02 00:43:17 +05:30
JoaoMarcos44
345483b8c0 fix(desktop): let Quick Entry submit after its own stale residue
Quick Entry / tile submitToSession still used refreshIfTranscriptStale,
which refuses on ANY surplus in the authoritative transcript - including
this window's own server-side turn residue (a turn that died on an
approval timeout). The composer path (submit.ts) already moved to
transcriptRefreshIfBehind in #124005; the tile delegate did not, so a
Quick Entry follow-up was refused forever with a stale-session warning.

Use transcriptRefreshIfBehind: a competing view still refuses with the
unprovable {runtimeSessionId, storedSessionId: null} identity, while own
residue is grafted into the cached state and prompt.submit proceeds.

Fixes #130031. Hand rebuild of #130035 (its merge-base predates the
refusal identity return, so a cherry-pick would revert it).
2026-10-02 00:43:17 +05:30
Austin Pickett
44a1ce9724 test(desktop): pin the preview URL scheme boundary
Public http:// and LAN hosts navigate; data:, javascript:, file: and garbage
still return null. The matrix kvnloo asked for on #129758, now that the
host allowlist is gone and the protocol check is the only gate.
2026-10-01 11:56:40 -04:00