Commit Graph

8066 Commits

Author SHA1 Message Date
Teknium
0aa764fedd test(desktop): the main channel hand-off expects no channel-record request
4b351c751a made `main` resolve straight to the git branch without reading
releases/channels/main.json (a WAF 403 on that never-published record stopped
updates, #128295). The Desktop source hand-off test still served that record
and asserted apply() fetched it twice, so JS & TS checks went red on every
main push and PR since.

The fixture no longer serves main.json and the test asserts re-resolving main
makes zero requests -- a regression back to reading the record now 404s and
fails the same assertion.
2026-10-11 00:47:43 -07:00
kshitijk4poor
0faf0938eb refactor(desktop): split the edit deep-cut phases out of runEdit; move the deep-cut tests into a sibling
The repo code-health ratchet blocked the push on two counts:
- runEdit had cyclomatic complexity 31 against a target of 20.
- index.test.tsx grew past its main-branch cap.

The pure steps move to rewind.ts:
- planEditAfterConfirm: plan, confirm, session-ownership check and
  re-aim.
- retryKeepsDeepConfirm: the stale-retry consent rule.

The hook gains retryStaleEdit, which holds the existing #82462 retry
unchanged apart from the consent rule, and rollBackEdit.
isCompressedAwayError becomes module-level. runEdit is now at CC 20.

The usePromptActions test harness moves to prompt-actions-harness.tsx.
The #133716 hook tests move to index.deep-cut.test.tsx, plus a new test
for a later user turn arriving during the dialog, which goes red with
the "moved" outcome removed. index.test.tsx drops below its main-branch
size. No behaviour change otherwise.
2026-10-11 12:14:05 +05:30
kshitijk4poor
183e955bd0 fix(desktop): an edit retry only reuses the deep confirm it covered; a session switch drops the edit
Sol r4 findings:
- P1: the 4018 stale-target edit retry carried the user's deep-cut
  confirm into a refreshed transcript that had gained later user turns,
  so the server gate was skipped for turns nobody confirmed. The retry
  now sends the confirm only when the refreshed transcript has the same
  number of later user turns. Otherwise it goes unconfirmed, and a
  server 4033 asks again.
- P2: the first deep-edit confirm did not re-check the session after
  the dialog. A switch left the busy flags on the new session while the
  edit went to the old one. The edit is now dropped when the session
  (or the tile's runtime) changed.
2026-10-11 12:14:05 +05:30
kshitijk4poor
579d3575c2 fix(desktop): keep the 4033 re-run flag off the public callbacks; an unseen later turn stops an edit
Delta review 2 Lows:
- The internal answeredFor parameter rode on the public onReload/onEdit
  callbacks, and assistant-ui calls onReload(parentId, config), so it
  received the config object on every reload. The 4033 re-run now goes
  through private runReload/runEdit, and the public callbacks take only
  their own arguments.
- A deep edit re-aimed after the confirm accepted a later user turn that
  landed during the dialog, which archived a turn the user never
  answered for. revalidateEditPlan now refuses when the count of later
  user turns changed. Regenerate already asked again in this case.
2026-10-11 12:14:05 +05:30
kshitijk4poor
4f5be98c29 fix(desktop): a 4033 confirm answered after a session switch is dropped
Sol r3: the re-run after an accepted 4033 read the active session
again. If the user had switched sessions while the dialog was open, a
parentId=null regenerate truncated and resubmitted the other session's
last turn without asking. The answer now belongs to the session that
was refused: the re-run happens only if the session (the tile's
runtime) is still the same. Applies to primary and tile, regenerate and
edit.
2026-10-11 12:14:05 +05:30
kshitijk4poor
d4a001341c fix: re-aim a deep edit confirmed mid-stream; count an unstamped carrier's own turn
Delta review findings:
- Medium: a confirmed deep edit during a live turn was silently
  dropped, because the stale check compared the transcript array by
  identity and streaming replaces it on every delta. The edit now
  re-plans against the current transcript (revalidateEditPlan). It
  proceeds when the edited turn keeps its address and the re-planned
  index is applied. If the turn moved, it stops with an "Edit failed"
  notice instead of returning silently. Applies to the primary chat
  and tiles.
- Low: a live carrier with no row id of its own never counted its own
  user turn, so a two-turn cut could pass without 4033. It now counts
  (fail closed).
2026-10-11 12:14:05 +05:30
kshitijk4poor
1b8a09f88d fix(desktop): a server 4033 re-runs the rewind from the top instead of retrying a stale plan
Sol r2 findings on bc1394c357:
- P1: the 4033 fallback dialog skipped the stale-transcript check.
  Accepting retried the captured plan even if the transcript changed
  while the dialog was open, and declining rolled back to the
  pre-submit snapshot over newer messages.
- P2: a slow answer let the optimistic busy arm expire, and the retry
  never re-armed it.

A refusal now rolls back at once (the refused submit wrote nothing).
If the user confirms, regenerate or edit re-runs from the top: the
session is re-read, the busy and stale guards apply again, and the
optimistic state is re-armed. The forced deep cut covers only the
transcript the user answered for, so a change during the dialog asks
again. submitWithDeepTruncateConfirm is removed.
2026-10-11 12:14:05 +05:30
kshitijk4poor
2afbaf10d1 refactor(desktop): one deep-cut confirm helper; drop a plan that went stale during the confirm
Lead fold from the independent review (efficiency Medium; reuse,
quality, thermo and 2c Lows):
- Stale plan: the confirm dialog is a wait as long as the user takes,
  so a turn that started or output that landed meanwhile made the plan
  and its rollback snapshot stale. Regenerate now returns without
  submitting when the session went busy or its transcript changed
  during the confirm. Edit, which interrupts a live turn on purpose,
  returns only when the transcript changed under a confirmed deep cut.
- One helper: planConfirmedReload and planConfirmedEdit share
  confirmIfDeepCut instead of two copies of the check, and the confirm
  thunk is built once per callback.
- Naming: deepReloadConfirmRequest is renamed deepCutConfirmRequest,
  since edit uses it too, and the 4033 code is a named constant.
- Tests: tile hook tests cover the stale-confirm drop and an
  unpredicted 4033 asking once and resubmitting confirmed.
2026-10-11 12:14:05 +05:30
kshitijk4poor
c72f4fa1dd fix(desktop): edit asks before a deep cut; a server 4033 asks once and retries
Review findings on #133748 (teknium1 Medium + Low, kvnloo concurring):
- Edit sent confirm_deep_truncate on every submit, so the server gate
  never applied to Desktop edits. planConfirmedEdit mirrors
  planConfirmedReload: an edit that would archive later user turns asks
  the same destructive confirm first. A tail edit or failed-turn edit
  asks nothing, and a decline sends nothing. The stale-target retry
  re-addresses the same turn and reuses the user's answer.
- The client and server count later user turns differently, so an
  under-asking client turned a deep cut into an opaque "Regenerate
  failed". submitWithDeepTruncateConfirm handles the server's 4033
  refusal by asking once and retrying confirmed. A declined confirm
  rolls back silently instead of showing an error toast.
Both apply to the primary chat and session tiles.
2026-10-11 12:14:05 +05:30
kshitijk4poor
45be34ba9c test(desktop): pin the explicit deep-truncate confirm on edit and tile restore retry
c1077745cc made an omitted confirmDeepTruncate mean not confirmed, so the explicit true at the edit and tile restore-retry submits is load-bearing; dropping either would turn an older-turn edit or restore into a silent 4033. Assert the flag on the existing edit and tile stale-target restore tests.
2026-10-11 12:14:05 +05:30
kshitijk4poor
eba445b402 refactor(desktop): make the deep-truncate confirm explicit at every rewind site
Gate C18 r1 quality [Low]: runRewindSubmit defaulted confirmDeepTruncate
to true while truncateSubmitParams defaulted it to false, so regenerate
had to write `?? false` and restore/edit opted into a destructive server
bypass by omitting a trailing argument. An omitted flag now means not
confirmed; restore/edit and their stale-target retries pass true
explicitly, and ReloadPlan.confirmDeepTruncate is a plain boolean
(planReload false, planConfirmedReload true after the confirm).

The parameter stays optional rather than required: it trails other
optional parameters, which TypeScript forbids for a required one.
2026-10-11 12:14:05 +05:30
kshitijk4poor
2c1f9e4b39 test(desktop): pin the deep-regenerate confirm wiring at both reload sites
Gate C18 r1 2ab [Medium, toothless]: reverting the planConfirmedReload
wiring in index.ts / session-tile-actions.ts left every test green. The
existing deep-regenerate tests now assert the confirm is asked once, a
decline sends no prompt.submit (main surface), and an accept sends
confirm_deep_truncate. Both go red when the wiring reverts to planReload.
2026-10-11 12:14:05 +05:30
kshitijk4poor
57f825763b fix(desktop): count failed later turns when deciding a deep regenerate
Gate C18 r1 2c [Medium]: planConfirmedReload used
visibleUserMessageIndices, which skips failed user turns, but the
gateway persists a failed turn's user row and counts it. Regenerating
before a failed later turn sent no confirm_deep_truncate and got an
unexplained 4033 (it worked before the gate). Any later visible user row
now asks for the confirm.
2026-10-11 12:14:05 +05:30
kshitijk4poor
df927be528 test(desktop): spell out the decline stub in the deep-reload test
Gate C18 r1 quality [Low]: `async () => (asked += 1) < 0` hid "count
and return false" in an always-false comparison.
2026-10-11 12:14:05 +05:30
kshitijk4poor
06559414d3 test: move two existing tests into topical siblings to keep files under their size cap
Code-health ratchet: index.test.tsx (6584 > 6577) and test_tui_gateway_server.py
(23504 > 23503) may only shrink vs main, and the deep-truncate stack grew both.
Pure moves, bodies unchanged:
- the uploadComposerAttachment Windows-image staging test -> upload-composer-attachment.test.ts
- test_prompt_submit_empty_truncation_allowed_with_confirm -> test_tui_gateway_server_deep_truncate.py
  (the sibling now covers both history-cut opt-ins)
2026-10-11 12:14:05 +05:30
kshitijk4poor
0e630b920e fix(tui-gateway): refuse a prompt.submit cut that archives >1 user turn without confirm_deep_truncate
Every existing truncation guard validates that the anchor exists and matches.
In #133716 the anchor was valid but named the wrong row (a stale Desktop
binding), and the gateway archived 107 live messages (430 -> 323) with no
friction — nothing bounded how much history one submit may drop.

_truncate_history_for_submit now refuses (4033, data: archived_messages,
archived_user_turns) when the cut archives more than one user turn and the
submit lacks confirm_deep_truncate. One turn is the legit implicit-target
allowance: tail regenerate and edit-last-message drop exactly that turn.
History and DB stay untouched on refusal.

Desktop: restore and edit name their target explicitly, so runRewindSubmit
sends confirm_deep_truncate by default (via truncateSubmitParams); regenerate
sends it only after the Half-A confirm. The TUI sends no truncation params.
Contract field added to PromptSubmitParams and the generated TS/OpenRPC
regenerated with scripts/gen_gateway_contracts.py.

Trade-off: an older Desktop build against this backend gets 4033 on
restore/edit of an older turn until it updates.

Fixes #133716

Reported-by: @Enferlain
2026-10-11 12:14:05 +05:30
kshitijk4poor
cbb057c0b7 fix(desktop): confirm a regenerate that would archive later user turns
A regenerate's target comes from the bound message scope. In a long-lived
renderer that binding can go stale and name a turn hours back; planReload
trusted it, and the truncating prompt.submit silently archived every later
turn (430 -> 323, 107 live messages, in #133716).

planConfirmedReload (rewind.ts, shared by the primary chat and session tiles)
now routes a reload that would drop later user turns through the same
confirm restore-checkpoint uses (shared ConfirmDialog via store/confirm,
existing restore copy — no new strings). Declining submits nothing. The
normal tail regenerate has no later user turn and is unchanged.

Refs #133716

Reported-by: @Enferlain
2026-10-11 12:14:05 +05:30
Teknium
0e0539c0f0 feat(desktop): plugins run built-in app actions (ctx.runAction)
Plugins that wanted a built-in app action (toggle the browser panel, open
Settings) faked its keyboard shortcut with a synthetic KeyboardEvent; that
silently breaks when the user rebinds or unbinds the shortcut
(browser-toggle catalog plugin).

- ctx.runAction(id) runs the same handler the keybinding dispatcher and
  palette use: useKeybinds publishes its handler table through
  registerBuiltinActionRunner, read at call time, so bindings never matter
- allowlist of view/navigation actions only (PLUGIN_APP_ACTIONS, typed
  PluginAppActionId union); destructive / privileged ids are refused
- result-shaped, never throws: { ok: true } or { ok: false, reason:
  'unknown' | 'denied' | 'unavailable', error } plus a console warning
- ctx.listActions() lists allowed ids with localized labels
- docs: API section, allowlist table, migration-table row; SDK exports
2026-10-10 20:26:37 -07:00
hermes-seaeye[bot]
b5722d6e49 fmt(js): npm run fix on merge (#136337)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-10 23:15:47 +00:00
Teknium
8f08b65538 fmt(desktop): prettier on the channel-display files after main's npm run fix pass 2026-10-10 16:05:48 -07:00
Teknium
cfcb5ea7bc test(desktop): the overlay channel fixture types its partial status 2026-10-10 16:05:48 -07:00
Teknium
26fa3aa079 fix(desktop): Change only where Settings can switch; name the release only on an explicit not-ahead
Review on #136313 (Greptile):
- The overlay offered "Change" to a custom-branch checkout and to runtimes
  older than --set-channel, but Settings shows no selector for either, so
  the link landed nowhere. The label stays; the action is passed only when
  sourceUpdateChannel() allows a choice.
- An older checker omits aheadOfRelease yet may also pin an ahead checkout
  to its own HEAD, so a missing field read as "on the release". source_check
  now always sends the boolean, and the status line names the release only
  on an explicit false.
2026-10-10 16:05:48 -07:00
Teknium
573f514a19 feat(desktop): version details name the update channel; the overlay links to change it
The updates overlay (status-bar version click) showed no channel, so a
source install could not tell it was pinned to stable releases.

- VersionDetails (shared by About and the overlay) gains a Channel row for
  source checkouts: "Stable releases", "Every commit (main)", or
  "Branch: <name>". Packaged installs render no row; their channel is the
  package's.
- The overlay passes a "Change" link that closes the dialog and deep-links
  to Settings > About > Updates, where the selector and its confirm step
  live. The channel is not switchable from the overlay itself.
- Current on stable, the status line names the release ("You're on the
  latest stable release (v0.21.6)."). A forward-only checkout ahead of the
  release keeps the generic line: source_check now reports
  `aheadOfRelease`, since its pinned target is HEAD and would otherwise
  look identical to being on the release.
2026-10-10 16:05:48 -07:00
ethernet
9456670d2f Merge pull request #135743 from NousResearch/harder-linter-fixes-part3
Harder linter fixes part 3
2026-10-10 17:55:39 -04:00
hermes-seaeye[bot]
6e5a56d947 fmt(js): npm run fix on merge (#136310)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-10-10 21:39:44 +00:00
Teknium
1f711b275f test(desktop): the source hand-off fixture installs its loopback transport once
baab4808d0 routed the stable lookup's GitHub reads through
source_check._request, so hermes_cli/source_check.py is now imported twice in
the probe (the CLI entry, then the module source_releases reads through). The
fixture's transport.py runs on every import of that file, so the second copy
wrapped the first and saw the already-rewritten 127.0.0.1 URL:
`assert parsed.hostname in ('hermes-assets.nousresearch.com', 'api.github.com')`
failed and the stable leg returned release-unavailable. main's JS & TS checks
have been red on this since 66605471e9.

transport.py now installs the redirect once (a marker on urllib.request). No
assertion changed. Locally: 1 failed on main, 1 passed with this.
2026-10-10 14:29:51 -07:00
Teknium
1c9135d5c2 test(e2e): the Desktop update harness keeps the seed's channel record
configureProvider() rewrote config.yaml from scratch for every spec, after
the seed had pinned `update.installs.<id>: main`, so the three Desktop update
specs still saw the stable default (release run 38078988253: the update check
answered {"channel":"stable","error":"release-unavailable"}). It now carries
the seed's top-level `update:` block into the provider config it writes.
2026-10-10 14:29:51 -07:00
Teknium
3a4b4b6f83 test(e2e): update sandboxes follow the main channel, not the stable default
#135847 made an official-origin source checkout default to the stable channel,
which resolves only from GitHub's latest published release. The upgrade-network,
Windows install+update and Desktop update harnesses keep the official URL as
origin, publish commits on main and never a release, so every update in them
stopped at the stable default:

- e2e-upgrade (network): "Could not resolve the stable source channel: No
  published stable release could be verified" (9 failed)
- Windows install + update E2E: "Release: stable v0.21.6; this checkout is
  newer and waits for the next release", checkout never moved (12 failed)
- Desktop update E2E (app-update, update-concurrency, build-fail): the update
  check answered {"channel":"stable","error":"release-unavailable"}

These suites test moving between commits, so each seeded install now records
`hermes update --set-channel main` right after its installer (new
_install_helpers.pin_main_channel; Windows Machine.install). Stable resolution
keeps its own coverage: test_release_channel_records sets stable explicitly on
the same seeded install, and tests/hermes_cli/test_source_release_channels.py.

Local: tests/e2e/core/upgrade/network on main plus this change, 21 passed
(4 failed in test_offline_and_flaky.py on main before it).
2026-10-10 14:29:51 -07:00
Maciej Lisiewski
0006a5e229 ci: fix PLW1509, PLW1510, PLW2101 linter errors 2026-10-10 15:20:13 +02:00
Teknium
a7b2ba02e3 fix(desktop): channel selector only where it can save, shows the choice at once, and never calls a custom branch main
Review findings on #135949 (Greptile, @unsupportedpastels):

- An older runtime (v0.21.6) answers checks but rejects --set-channel, so the
  selector failed on every click. source_check now marks its reply
  `channelSelectable`; Desktop shows the row only when the install says so.
- The confirmed choice painted only after the forced check returned. The row
  now shows it at once, and a failure re-reads the install instead of guessing.
- A checkout following a custom branch rendered as "Every commit" (main). The
  row now appears only for stable, or for main itself.
2026-10-10 02:33:22 -07:00
Teknium
354fe11d7e test(desktop): the source-channel handoff fixture answers stable from releases/latest
Since #135847, stable is the latest published GitHub release
(`releases/latest`), not the R2 release-candidates pointer. The fixture still
served only the pointer, so the check reported "No published stable release"
and the test went red on main (the Python-only PR skipped the Desktop lane).
2026-10-10 02:33:22 -07:00
Teknium
9287719a65 feat(desktop): About ▸ Updates picks stable releases or every commit for source installs 2026-10-10 02:33:22 -07:00
Teknium
24051d9084 feat(desktop): drop the "This could run on your computer" strip above the composer
The local-setup card appeared above the input after the first finished turn
on any machine whose catalog recommends a local model, and stayed across
relaunches until dismissed. It read as a nag on every new session.

The offer keeps its quiet homes: the "Run locally" row at the top of the
model menu (until setup completes) and the handoff tour's model-pill step.
The card's state machine (armed/shown/dismissed, the message.complete hook,
the menu-open request) existed only for the card, so it goes with it; the
store keeps the eligibility read both remaining surfaces use.
2026-10-09 14:01:55 -07:00
ethernet
040dbb9053 Merge pull request #135348 from NousResearch/harder-linter-fixes-part2
Harder linter fixes part 2
2026-10-09 13:08:10 -04:00
GrgaPalcic
2af85c331a fix(desktop): make mermaid SVG well-formed via the HTML parser, not a void-tag regex
The void-tag pass from the previous commit fixes `<br>`, but mermaid's
label HTML carries more than void tags: a non-breaking space (`&nbsp;`,
mermaid's `#nbsp;`, or a literal U+00A0 in the source) is serialised as
the `&nbsp;` entity, which XML does not define. Such a label still
decodes to a broken image.

mermaid.render() returns the HTML serialisation of the diagram, so parse
it with the HTML parser it was written for and re-serialise it with
XMLSerializer. That fixes the whole class at once. Against mermaid
11.16 in Chromium, across flowchart, class, state, ER, mindmap, block,
kanban, sequence, journey, gantt, pie, timeline, gitGraph, quadrant and
xychart output, the round-trip yields the same element tree and image
size as the regex wherever the regex output is valid XML, and also loads
the non-breaking-space cases the regex leaves broken.

Drop a label's plain `xmlns` attribute before serialising: the
serializer declares the namespace itself, and a spec-conformant one
(jsdom) writes both, a duplicate attribute.

The tests' fixtures put `<br>` directly in SVG <text>, which mermaid
never emits; replace them with the real 11.16 label shape (XHTML in a
<foreignObject>) and assert the label keeps its line break and its
non-breaking space.

Refs #133089, #134031
2026-10-09 11:57:18 -05:00
flyer
766ab6dd2d fix(desktop): render Mermaid labels that contain <br/>
`mermaid.render()` returns SVG aimed at inline HTML, and its sanitizer
gives back a `<br/>` written in a label as an open `<br>`. That string is
not well-formed XML, so the strict `image/svg+xml` parse in
`normalizeSvgSize` yields the parsererror document instead of an `<svg>`,
takes the `el.tagName !== 'svg'` early return, and hands the *unnormalized*
string back. The diagram `<img>` then gets it as a `data:` URI, which Blink
decodes as strict XML and rejects for the same reason — so the message shows
a broken image whose alt text reads "Mermaid diagram", with nothing thrown
and nothing written to any log.

Close HTML void tags before the markup reaches either XML parse. The pass is
generic rather than a `<br>` special case, because the same class of input
reaches `<hr>`, `<img>`, `<input>`, …, and it is idempotent: already-closed
tags and non-void tags (including names that merely start with a void tag,
such as `<break>`) pass through untouched.

Fixes #133089

Verified by replaying 32 real ```mermaid fences from a session database
through the app's own renderer chunks in Electron 40.10.2 / Chromium 144:
24/32 fail before this change — every failure has `<br/>` in a label, with a
perfect 1:1 correlation — and 31/31 renderable diagrams succeed after it. The
32nd is an invalid Mermaid source that correctly keeps falling back to its
source block.
2026-10-09 11:57:18 -05:00
Maciej Lisiewski
9eab688b42 ci: ruff UP017 fixes 2026-10-09 17:34:36 +02:00
Jeffrey Quesnelle
e0550c97bb Merge pull request #135438 from NousResearch/fix/local-recs-weak-hardware
fix(local-models): stop recommending the 27B on hardware that can't run it well
2026-10-09 11:19:21 -04:00
Teknium
ecf9655b5a refactor(desktop): move plugin-contract type re-exports out of the SDK barrel
Keeps apps/desktop/src/sdk/index.ts under its code-health line cap after the
ctx.pet type exports.
2026-10-09 02:04:56 -07:00
Teknium
ecb66f60dd feat(desktop): plugin SDK pet bubble (ctx.pet.say)
Plugins can now put a short plain-text line in the core pet's speech bubble
instead of locating the pet canvas in the app DOM and floating their own
overlay over it (pet-wallet, #135178).

- ctx.pet.say(text, { id?, tone?, ttlMs? }) -> disposer; ctx.pet.clear(id?);
  ctx.pet.visible (ReadableAtom<boolean>)
- host-owned rendering in PetBubble, in-window (plugin lines only) and in the
  pop-out overlay (lines ride the existing pet-state IPC push)
- plain text, 120-char cap, control/bidi chars stripped, plugin-name label,
  TTL 6 s default (1-30 s), 3 live lines and 10 says/10 s per plugin
- core error / waiting states keep the bubble; nothing shows without a pet
- every line a plugin has up is cleared on unload/disable/reload
- docs: API section, migration-table row for pet-wallet, SDK exports
2026-10-09 02:04:56 -07:00
emozilla
f560c14e73 fix(local-models): recommend nothing when no model reaches the speed floor
When no resident model cleared 20 tok/s, the resolver still recommended the
fastest one (fastest-resident). A "32 GB" machine without a discrete GPU reads
31.5 GB, where only the 27B fits, at a predicted 13 tok/s, so CPU-only and
integrated-GPU machines were steered to a slow dense model. Those machines now
get no recommendation, and every fitting model stays one click away in Browse.
The no-recommendation copy now says the model must run at full speed, the user
guide states the rule, and the unused fastest-resident tooltip string is gone
from every locale.
2026-10-09 00:44:28 -04:00
emozilla
02118fc2de fix(desktop): offer local setup only for the recommended model
The composer's "This could run on your computer" card took the recommended
catalog row, else the first row marked `fits`. A row fits when any build runs,
including one spilled to system RAM, so a GTX 1080 with 16 GB of RAM was
offered the 27B, which the resolver never recommends there. The card now names
the recommended model, and a machine without one gets no card.
2026-10-09 00:19:54 -04:00
Teknium
eb699a158c fix(pm): MSIX plugin installs build on a writable copy of the bundled Python (#135236)
Enabling a plugin with Python dependencies, and `hermes pm install`, failed
in the packaged Windows app: uv lock/sync/pip check exit 101 with "Unable to
create process using ...\venv\Scripts\python.exe".

uv queries a venv by running its Scripts\python.exe. That redirector lives
outside the package and CreateProcess's the interpreter named by
pyvenv.cfg `home`. 6a031bf1b3 moved generations onto the packaged
interpreter, so `home` names a file under WindowsApps that a process without
package identity may not start (exit 101 is the redirector's
RC_CREATE_PROCESS). Restore the verified writable copy of the pinned Python
in the user store as the build interpreter for sealed Windows payloads.

venv_command (also from 6a031bf1b3) stays: Hermes still enters those venvs
on the packaged interpreter, so its own children keep package access to the
bundled git, rg and PM worker.
2026-10-08 20:20:55 -07:00
ethernet
94b1b6d422 refactor(desktop): drive FadeScroll's edge fade with scroll-driven CSS
FadeScroll tracked clipped edges in React state, with a scroll handler and a
resize observer, then rebuilt an inline mask on every change. Move that to a
`scroll-fade-y` utility: the mask's stops are scaled by two registered
<number> properties that the scroller's own scroll position animates
(animation-timeline: scroll(self y)), so an edge only fades while content is
clipped behind it and a list that fits is never dimmed. No listener, no state;
a browser without scroll-driven animations shows no fade.

From each edge inward the mask is `pad` of fully hidden content, then `fade`
of gradient, both optional props. The onboarding apps list sets pad to 1.5rem
so the gradient starts at the chips instead of spending itself on the empty
padding at the box edge. edgeMask/scrollEdges stay for the profile switcher.
2026-10-08 22:10:44 -04:00
ethernet
9809922e04 feat(desktop): fade top/bottom of the onboarding connectors list
The connector/plugin chip picker is height-capped, so the last rows were
clipped with no hint there was more. Wrap the grid in the existing
FadeScroll, which already fades whichever edge has content clipped behind
it and leaves a list that fits untouched.
2026-10-08 22:09:38 -04:00
Teknium
9d05e7ff92 test(desktop): model settings tests clear their toast timers before jsdom tears down
The main-model apply test raises a success toast, which arms a 5 s
auto-dismiss timer. Nothing cleared it, so when it fired after the file's
jsdom window was gone, vitest reported "window is not defined" from
dismissNotification as an unhandled error and failed JS & TS checks with
every test green. afterEach now calls clearNotifications().
2026-10-08 15:20:58 -07:00
kshitijk4poor
d2cbfcf7db test(desktop): the React Compiler cache is its own module with focused tests
The cache lived inline in vite.config.ts, and the renderer builder tests use a bare fixture
config, so nothing exercised it: a broken warm path would still pass every test.

Move it to scripts/compiler-cache.mjs (behaviour unchanged; toolchain files and cache root are
now parameters, and an existing closeBundle hook is chained instead of replaced) and test hit
vs fresh output, toolchain/NODE_ENV generations, corrupt entries, pruning, incremental (watch)
rebuilds and the dev-server bypass. A cold and warm production build of the real app stay
byte-identical. Reported by Greptile on #135092.
2026-10-08 15:20:58 -07:00
kshitijk4poor
1f6d8b6ba8 fix(desktop): a node-pty layout without its macOS spawn-helper is an incomplete native tree
nativeTreeComplete decides whether a later build may skip restaging, but never looked at
node-pty. A partially extracted package with pty.node and no spawn-helper was recorded
complete; repairing the installed package without a lockfile change then left the staged
copy broken, and every later build shipped a terminal that fails on its first spawn.

Check the layout node-pty itself loads (build/Release, build/Debug, then prebuilds, as in
lib/utils.js) for a binding, and on macOS for the spawn-helper beside it (lib/unixTerminal.js).
Reported by Greptile on #135092.
2026-10-08 15:20:58 -07:00
kshitijk4poor
27e0f189ac fix(desktop): the React Compiler cache serves production builds only, keyed by NODE_ENV
The compiler emits HMR cache-reset code when NODE_ENV is development, and the cache key ignored it, so a dev-server session could seed entries a later production build reused. A dev server also wrote one entry per save with no build to prune them. The cache now wraps only 'vite build', NODE_ENV joins the toolchain key, and entries drop the map's sourcesContent (the module text the key already hashes; restored on a hit): 22 MB -> 15 MB. Verified: warm == cold byte-identical without sourcemaps; with --sourcemap two cold builds already differ in one map's __VITE_ASSET__ placeholder order, so that diff is vite's, not the cache's.
2026-10-08 15:20:58 -07:00
kshitijk4poor
3fc37481eb refactor(desktop): one receipt reader, one output list, no native double copy on renderer reuse
rendererCurrent and productCurrent share hostReceipt() for the schema/product/host checks. The stamped outputs come from bundleElectronMain's own electronOutputs list, so a renamed or added bundle can never be copied stale from the previous dist. The reuse copy skips node_modules/ and native/, which copyNativeTree recopies from nativeDeps right after, and a --typecheck build never reuses a renderer that skipped tsc.
2026-10-08 15:20:58 -07:00