Commit graph

1701 commits

Author SHA1 Message Date
Brooklyn Nicholson
6ba2f3e644 fix(desktop): say a sub-second reasoning block was brief
The elapsed timer counts whole seconds, so reasoning that finished
inside one rendered as "Thought for 0s" — accurate and useless, and on a
turn with several short blocks it repeats down the transcript. Drop the
number below a second and say it was brief instead.
2026-07-27 18:10:27 -05:00
Brooklyn Nicholson
f96997c36f fix(desktop): settle a tool run whose calls never resolved
A run inferred "still working" from a missing result alone, so a call
left unresolved — by an interrupted turn, or an agent that moved on —
pinned its run as live forever. That stranded the summary in the present
tense ("Exploring 2 files" on a finished turn) and, because a live run
withholds its toggle so approvals can't hide, left the run permanently
expanded with no way to collapse it.

Qualify liveness the way ToolEntry already qualifies a row's: a missing
result only means pending while the run is the tail of a running
message. Liveness is now passed into summarizeToolRun rather than read
off the calls, since it isn't a property of the calls.
2026-07-27 18:10:27 -05:00
Brooklyn Nicholson
f7d6c1be8f docs(desktop): explain what UNBOUNDABLE_TOOLS now guards
The list gates two behaviours since the settled-run summary landed, but
the comment still justified it only in terms of the scroll window's
height cap. Record the pending-row rule that keeps approvals visible,
and why file edits stay off the list.
2026-07-27 18:10:26 -05:00
Brooklyn Nicholson
a77c75f3e0 feat(desktop): report how long the model thought
A settled reasoning block reads "Thought for 5s" instead of staying
"Thinking" forever. Nothing in the persisted turn records the duration,
so the number is frozen when the block finishes on screen and simply
omitted on a rehydrated turn, rather than reporting whatever a timer that
never ran would say.
2026-07-27 18:10:26 -05:00
Brooklyn Nicholson
7151ea4c77 feat(desktop): collapse a settled tool run to its summary line
A run of two or more tool calls now renders behind its summary once it
has finished, so a long transcript reads as what the agent did rather
than as a wall of rows. The run is keyed by its first tool call instead
of its part index: live and rehydrated turns agree on which calls belong
together but not on the indices they land at, and keying by index is what
made the previous attempt reshuffle on settle.

A run holding anything still pending always renders its rows, which is
what keeps a clarify question or an approval bar out from behind a
chevron. The approval-group tests move to tool-group and grow coverage
for both halves of that rule.
2026-07-27 18:10:26 -05:00
Brooklyn Nicholson
97d790d8b1 feat(desktop): summarize a run of tool calls as one line
Adds the grammar behind "Edited wiring.tsx, explored 3 files, ran 5
commands": one clause per category of work, a name when the category
holds a single thing and a count otherwise, and the present tense for
whichever category is still running.

The continuity test is the load-bearing part. Tool grouping was reverted
once because it reshuffled the moment a turn settled, so this replays the
same turn twice — as the gateway event stream the live view builds
bubbles from, and as the rows toChatMessages rehydrates on resume — and
asserts both produce the same runs.
2026-07-27 18:10:26 -05:00
brooklyn!
9f02bb207d
Merge pull request #72965 from NousResearch/bb/resume-freetext-search
fix(desktop): keep /resume's free-text search typeable
2026-07-27 18:09:50 -05:00
brooklyn!
fbd8d1a93d
Merge pull request #72963 from NousResearch/bb/artifacts-real-preview
Unify the preview rail onto one tab list
2026-07-27 18:04:18 -05:00
Brooklyn Nicholson
3c74f463d0 fix(desktop): keep /resume's free-text search typeable
/resume was classified as an options command, but its argument is a
free-text query the picker fuzzy-matches against session titles and
previews. Its completion list also always ends in a "Browse all
sessions…" action row, so Space-to-accept never fell through to an
empty list: the first space in a multi-word query emptied the composer
and threw the user into the overlay, query and all.

Classify it as mixed so spaces type through, matches keep narrowing as
you refine, and Tab or arrow-then-Enter still accept a highlighted
session.
2026-07-27 18:03:07 -05:00
brooklyn!
7de4fbd493
Merge pull request #72956 from NousResearch/bb/slash-freetext-enter
fix(desktop): don't let Enter swap a free-text slash argument for a completion
2026-07-27 17:56:45 -05:00
Brooklyn Nicholson
cd25a9f405 fix(desktop): open a preview without dragging the file tree open
The preview pane shares a collapsible column with the file tree, and
`revealTreePane` un-collapses a column through that column's bound store —
which on the right is `$fileBrowserOpen`, the tree's own ⌘J toggle. So
every preview open literally called `setFileBrowserOpen(true)` and the tree
came with it.

`revealPreview` now un-collapses the column directly and leaves the toggle
alone. The tree pane's visibility binding gains `$fileBrowserOpen` to match,
since its presence was tracking only the column's collapse — without that it
would still render the moment anything opened the column.
2026-07-27 17:55:46 -05:00
Brooklyn Nicholson
003ff53fb4 fix(desktop): scope composer and transcript state to their own session
`$activeSessionId` only ever holds the primary chat's session, but surfaces
that render once per transcript were reading it as if it meant "the session
on screen." A preview produced inside a session tile was recorded under the
main chat's key and surfaced in the main chat's composer, which is what
prompted this.

The tool row now records under its own `SessionView`, and the same fix
applies to the other readers of that atom that render per surface:
attachment pills and inline preview links resolve relative paths against
their session's cwd, composer voice and auto-speak read and subscribe to
their own transcript, and the thread's compaction label, prompt-wait gate
and turn timer follow the session that mounted them. `ComposerScope` now
carries a `$messages` atom rather than a read closure so both the
imperative read and the subscription come from one place.
2026-07-27 17:55:46 -05:00
Brooklyn Nicholson
96999b116b refactor(desktop): put every preview on one rail tab list
The right rail held two things at once: a list of file tabs, and a
privileged "live preview" slot with a hardcoded `preview` tab id backed by
a separate session-keyed registry. The two were written under different
session-id rules and reconciled against each other, so an `open_preview`
from a session whose stored id hadn't landed yet was set and then
immediately cleared — the pane flashed and vanished. Artifacts arrived as
a third list with their own pane and renderers.

Now everything the rail can show is a `PreviewTarget` in `$previewTabs`,
and `openPreview` is the only way in. `$previewTarget` is a computed read
of the active tab, the session registry and its reconciler are gone, and
artifacts render in the real preview pane through the shared mode switcher
and source view instead of a parallel one. Artifact tabs stay memory-only
since the registry rebuilds from the transcript.
2026-07-27 17:55:27 -05:00
Brooklyn Nicholson
43571601aa fix(desktop): don't let Enter swap a free-text slash argument for a completion
`/goal` keeps its completion popover open across arbitrary prose so its
subcommands stay reachable. The popover highlights its first row on open, and
Enter accepted that highlight unconditionally — so pressing Enter to send
`/goal ship the redesign` would replace the sentence with a row the user never
chose. Space was already guarded; Enter and Tab were not.

Enter now accepts only after the user has arrowed to a row deliberately, so
the highlight never lies about what Enter will do. Tab stays an unconditional
accept, since it means nothing else in the composer.

This is latent rather than reproducible today: `/goal` is absent from
`SUBCOMMANDS` (its `args_hint` pipes are spaced, so the extraction regex
misses them), so the backend returns no arg completions and the branch never
runs. Giving `/goal` the subcommands it already advertises would resurrect
the #71963 symptom in a worse form — losing the prose instead of chipping it.
2026-07-27 17:50:18 -05:00
Brooklyn Nicholson
e3acdfb21d refactor(desktop): lift the completion-accept decision out of the keydown ladder
Which keys accept the highlighted completion was an inline condition in the
composer's keydown god-function, untestable without a DOM harness. Move it to
a pure helper beside the other slash-query utilities.
2026-07-27 17:50:12 -05:00
Gille
b429194478
refactor(desktop): simplify free-text slash mode check (#72815) 2026-07-27 17:45:03 -05:00
brooklyn!
7c532e1006
Merge pull request #72889 from NousResearch/bb/composer-at-paths
Fix `@` path navigation, folder completion, and chip baseline in the composer
2026-07-27 17:13:00 -05:00
brooklyn!
42c308ecdc
Merge pull request #72897 from NousResearch/bb/desktop-drift-fixes
Desktop: fix diff color drift, replayed notifications, stall timing, and quit-on-active-work
2026-07-27 17:12:49 -05:00
brooklyn!
14cb0507a7
Merge pull request #72912 from NousResearch/bb/desktop-project-count
Drop the leftover session counts inside an entered project
2026-07-27 17:12:39 -05:00
Gille
dbc18c6d62
fix(desktop): preserve live model after settings save (#72903) 2026-07-27 17:52:37 -04:00
brooklyn!
c63be0daf7
Merge pull request #72900 from NousResearch/bb/desktop-home-project
Add a Home project at the top of the desktop sidebar
2026-07-27 16:29:57 -05:00
Brooklyn Nicholson
e04ed64637 style(desktop): drop the session counts inside an entered project
The sidebar's flat session list lost its `x/<total>` chip in #72336, but
the project drill-in kept counting: WorkspaceHeader still rendered a
SidebarCount for every repo and every branch/worktree lane. Entering a
project put a number next to each label again.

Remove the header's count slot and both call sites, along with the now
dead repo total.
2026-07-27 16:25:51 -05:00
Brooklyn Nicholson
e00901259c feat(desktop): Tab into a folder from the @ popover
Tab and Enter shared one branch, so picking a folder always committed a
chip and closed the menu — the list could show `apps/` but never open it.
Reaching a nested path meant typing every segment by hand.

Split the two intents. Tab re-types the token as a bare path so the next
completion lists that folder's children; Enter still commits the folder
itself as a chip. Files ignore the distinction — there's nowhere deeper
to go. Backspace mirrors the descent, dropping one path segment per press
instead of one character, so climbing out costs the same as going in.
2026-07-27 16:20:58 -05:00
Brooklyn Nicholson
579b66336f fix(desktop): let automated teardown quit past the active-work prompt
Playwright closes the app with a turn still in flight, so the new quit
confirmation waited on a click nobody was there to make and the E2E
worker died on a 90s teardown timeout.
2026-07-27 16:19:27 -05:00
hermes-seaeye[bot]
a88e27e9e5
fmt(js): npm run fix on merge (#72902)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-27 21:15:26 +00:00
Brooklyn Nicholson
5a5d7b9386 feat(desktop): pin Home to the top of the project sidebar
Home leads the overview above the active project and outside any drag
order, drills in to a flat chat list (it has no repo or worktree
structure), and overlays live sessions so a brand-new detached chat
appears instantly. Starting a chat from inside Home stays detached
instead of picking up the configured default project dir. Rename and
delete are hidden — there's no record behind the row.
2026-07-27 16:05:19 -05:00
Brooklyn Nicholson
ef0f4763e3 i18n(desktop): name the project-less bucket "Home"
The sidebar row is a place you enter, not a status line, so it reads as a
destination rather than "No project".
2026-07-27 16:05:10 -05:00
Brooklyn Nicholson
2a7da2b549 style(desktop): fade split sashes until hover
The seam hairline sat at full strength on every split, so an empty
workspace read as a wireframe. Hold it at 0.1 and bring it up with the
grab band already on hover.
2026-07-27 15:58:34 -05:00
Brooklyn Nicholson
35a002a441 feat(desktop): mark the active pane tab with a primary underline
The active tab was defined by absence: the strip painted a rule and the
tab covered it, so inactive tabs stopped a pixel short to let it show.
Draw the state instead. The tab carries its own 2px --theme-primary
underline and the strip's rule goes away, which lets tabs run full height
and removes PANE_TAB_STRIP_LINE along with it.
2026-07-27 15:58:22 -05:00
Brooklyn Nicholson
d8b5bbf607 style(desktop): drop the titlebar and statusbar edge rules
The window chrome bracketed the workspace with a 1px rule top and bottom.
Both bars already paint the sidebar surface, so the rules divided one
continuous color rather than separating two.
2026-07-27 15:58:12 -05:00
Brooklyn Nicholson
c7ef4c192d refactor(desktop): name the overlay z-index ladder
DESIGN.md already said app-wide surfaces must not compete through ad-hoc
z-index literals, and the code disagreed: three overlapping numbering
schemes, and comments narrating the fight ("defaults to z-130, renders
UNDER the onboarding overlay (z-1300) ... bump it above with z-[1310]").
Picking a number meant reading someone else's near miss.

Name the rungs — modal, over-modal, switcher, and the boot chain — and
point the call sites at them. Every rung keeps the exact value it had, so
nothing moves; what changes is that the next overlay has a name to reach
for instead of a number to guess. Local stacking within a component stays
on plain z-10/z-20.
2026-07-27 15:48:09 -05:00
Brooklyn Nicholson
9ae3bd73c9 feat(desktop): confirm before quitting with a turn in flight
Cmd-Q went straight through to teardown, killing the backend mid-tool-call
— the turn is gone and whatever the agent was part-way through writing
stays part-way written, with nothing on screen to warn about it.

Renderers now report which chats are mid-turn; before-quit merges the
reports and asks, naming them, defaulting to Keep Running. Update, swap,
and uninstall relaunches skip the prompt: those are the app replacing
itself, and a modal there would strand the detached script waiting on a
PID that never exits.
2026-07-27 15:47:57 -05:00
Brooklyn Nicholson
2f5926ed05 fix(desktop): time a stream stall from the last activity
The tail "Hermes is thinking" indicator resets on every flush, but its
timer never did: with no timer key, useElapsedSeconds anchors to mount,
and the indicator mounts with the assistant message. A stall two minutes
into a turn therefore claimed two minutes of silence instead of the two
seconds that had actually passed.

Give the hook an explicit epoch and hand it the timestamp of the activity
the quiet spell followed. Compaction still counts from the turn's start,
which is the span it owns.
2026-07-27 15:47:53 -05:00
Brooklyn Nicholson
e9bb4c3951 fix(desktop): don't alert for prompts a reconnect replayed
A socket opening replays state that already existed — a session parked on
an approval re-emits its request so the UI can draw the prompt. Those
arrive as ordinary events, so launching Hermes, switching profiles, or
riding out a reconnect fired an OS notification for a prompt the user had
known about for an hour.

Hold native notifications for a beat after any gateway opens. The sidebar
row and the inline approval bar still appear immediately; only the OS
notification waits for something that actually just happened.
2026-07-27 15:47:45 -05:00
Brooklyn Nicholson
93477b2a0c fix(desktop): paint diffs from the theme palette
Diff add/remove lines were hardcoded to Tailwind's emerald/rose while the
overview ruler beside them — and the rest of the app — used --ui-green /
--ui-red, so every diff sat slightly off-brand and stayed put when the
semantic palette moved. Derive the tint, gutter, and text from those two
colors instead. One renderer feeds the tool card, the file preview, and
the review pane, so all three follow.
2026-07-27 15:47:42 -05:00
Brooklyn Nicholson
ecd5c79636 fix(desktop): sit composer chips on the text baseline
`align-middle` centers a pill on the x-height midpoint, which sits above
the center of the surrounding text box, so chips rode visibly low against
the words they're nestled between. Measured against the rendered surface,
`-0.12em` lands the chip's own baseline within 0.08px of the line's
(vs 0.79px off before) without growing the line box.

Applies to both the directive and slash chip classes — they share a line,
so fixing one and not the other just moves the mismatch.
2026-07-27 15:24:56 -05:00
Brooklyn Nicholson
de0b376cc9 fix(desktop): keep the @ popover open while typing a path
`AT_TRIGGER_RE` excluded `/` from the query, so the trigger died on the
first separator: `@/desk`, `@./www`, `@~/Desktop` and even `@file:src/foo`
all stopped matching the moment a path appeared. The gateway already
answered those queries correctly — the composer just never asked.

A `/` inside an `@` token is navigation, not a delimiter. The token stays
whitespace-bounded, which is what actually ends it.
2026-07-27 15:24:44 -05:00
Brooklyn Nicholson
bdd75630a7 fix(desktop): paint the titlebar with the sidebar's surface color
The shell titlebar declared no background of its own, so it showed through\nto the wrapper's --ui-bg-chrome and read as a lighter band above the\nsession list. Use --ui-sidebar-surface-background, the token the sidebar\nalready paints with, so the two chrome surfaces meet on the hairline\ninstead of a color change.
2026-07-27 15:22:13 -05:00
Brooklyn Nicholson
97a8034dfd refactor(desktop): resolve profile backend routing from one table
Three helpers each re-derived part of the same decision: which backend
serves profile P, and does its REST path need a `?profile=` scope.
profileUsesPrimaryBackend answered the first half, pathWithGlobalRemoteProfile
answered the second, and ensureBackend re-checked globalRemoteActive() around
both. Splitting one table across three predicates is how the global-remote
case ended up registering reapable pool entries for a backend it never owned.

resolveProfileBackendRoute() states the four routes in one place and returns
the backend, the descriptor scope, and whether the path needs a query
parameter. The call sites read the answer instead of recomputing it.

One behavior change falls out: `hermes:api` now passes the primary profile
through, so the primary no longer sends itself a redundant `?profile=<self>`
on a global remote that already serves it.
2026-07-27 13:44:11 -05:00
Brooklyn Nicholson
f18e50a070 fix(desktop): record main-process faults in desktop.log
Electron pre-installs its own uncaughtException listener and only warns on
unhandled rejections, so a main-process fault usually leaves the app running
with the reason on stderr — which nothing captures when the app is launched
from Finder or the Start menu. The fault never reaches desktop.log, so it is
absent from `hermes debug share` and the user can only describe symptoms.

Record both to desktop.log and flush synchronously, since a fault that does
prove fatal leaves no chance for the batched async flush. Five loadURL calls
were also unhandled, each able to leave a blank window with no explanation
anywhere the user can send us; they now name the surface that failed.

Co-authored-by: Rodrigo Fernandez <rod@nxtlevel.dev>
2026-07-27 13:41:59 -05:00
Brooklyn Nicholson
d7e738af90 fix(desktop): retire pooled remote backends whose host went away
A pooled backend entry pointing at a remote host has no child process, so
the 'exit' handler that clears a dead local backend never fires. The
renderer's 60s keepalive touch also spares it from the idle reaper. Nothing
was left to retire the descriptor, so once the host went away the pool kept
serving it and every profile bound to that host stayed broken until restart.

Pooled remote descriptors now share the primary's liveness policy: probed on
the same revalidate tick, keyed per base URL, and dropped only after the
same consecutive-failure limit, so the next ensureBackend() rebuilds.

Co-authored-by: Rodrigo Fernandez <rod@nxtlevel.dev>
2026-07-27 13:41:59 -05:00
Gille
3884e0eea0 fix(desktop): reuse global remote backend across profiles
Keep non-primary profiles that inherit the app-global remote on the primary connection descriptor instead of creating processless pool entries that the idle reaper repeatedly removes.

Preserve per-profile remote overrides and local pooled backends, and cover the routing policy with behavioral tests.

Co-authored-by: Rodrigo Fernandez <rodrigo@nxtlevelsaas.com>
2026-07-27 13:41:59 -05:00
Vitor Cepeda Lopes
704a321870 fix(desktop): preserve OAuth sessions in sidebar 2026-07-27 13:41:56 -05:00
Brooklyn Nicholson
ac9a10ccbb test(desktop): cover the workspace-page reveal bug class
Both layers, and the classification underneath them: a page route fronts
the pane whether it carries a query or not, moving between two pages
fronts it again even though `$workspaceIsPage` never changes, contributed
routes count, and chat and overlay targets leave the tab alone.
2026-07-27 12:54:49 -05:00
Brooklyn Nicholson
f6ea8b462e fix(desktop): front the workspace pane from the router location
Capabilities, Messaging and Artifacts render inside the `workspace` pane,
so navigating to one has to bring that pane to the front of its group.
Nothing did. With the main zone parked on a session tile, the route and
the page content changed behind the tile and the click looked dead until
the app restarted. Session switches already front the pane in
`store/session-states.ts`; pages had no equivalent.

Front it from the router location, in the effect that already mirrors
`$workspaceIsPage`. One place decides, so every entry point inherits it:
sidebar, keybinds, palette, Command Center, contributed statusbar and
titlebar `to` targets, back/forward, and cold-start restore — which no
longer needs its own call.

`navigateToWorkspacePage` stays for the one case the location can't see:
hitting Capabilities while already on `/skills` leaves the location
untouched, so no effect fires and only an imperative reveal brings the
page back.
2026-07-27 12:54:45 -05:00
Brooklyn Nicholson
8323bf3d71 fix(desktop): classify router targets by pathname, not the raw target
Every route classifier reasoned about the full navigation target, so a
query put them on the wrong branch: `/skills?tab=mcp` failed the reserved
path check and fell through to the session-id parser as the session
`skills?tab=mcp`, which made `appViewForPath` report Capabilities as a
chat. The command palette reaches Capabilities exclusively through those
targets, and Settings redirects old `/settings?tab=mcp` deep links there.

Strip the query and hash once, up front. Session ids are percent-encoded
by `sessionRoute`, so `?`/`#` can only ever start a query or a hash.
2026-07-27 12:54:39 -05:00
alelpoan
5ecc666146 test(desktop): cover workspace pane reveal in selectSidebarItem
Adds a regression test for #72602: navigating to a sidebar route now calls navigate() and fronts the workspace pane (noteActiveTreeGroup(null) + revealTreePane('workspace')).

Mocks @/components/pane-shell/tree/store to assert the calls without depending on real pane-tree DOM state.
2026-07-27 12:54:20 -05:00
alelpoan
9cef5496eb fix(desktop): front workspace pane when navigating sidebar routes
Capabilities/Messaging/Artifacts (and other full-page workspace routes) rendered their content correctly on navigate(), but the workspace pane itself stayed behind an active session tile in the pane tree if one was focused. Clicking the same sidebar item again after switching to a session tile appeared to do nothing.

Session switches already call revealTreePane('workspace') + noteActiveTreeGroup(null) to front the pane (store/session-states.ts), but sidebar/keybind/command-palette navigation to full-page routes did not have an equivalent.

Add navigateToWorkspacePage() helper in routes.ts that wraps navigate() and fronts the workspace pane for non-overlay, non-chat views. Apply it at all 5 call sites that navigate to full-page workspace routes: selectSidebarItem, use-keybinds (nav.skills/messaging/artifacts), command palette 'go', Command Center's onNavigateRoute, and cold-start route restore.

Fixes #72602
2026-07-27 12:54:20 -05:00
Gille
c2e45b555f
fix(desktop): keep free-text slash arguments editable (#72768)
Some checks are pending
CI / Detect affected areas (push) Waiting to run
CI / Python tests (push) Blocked by required conditions
CI / Python lints (push) Blocked by required conditions
CI / JS & TS checks (push) Blocked by required conditions
CI / Desktop E2E (push) Blocked by required conditions
CI / Docs Site (push) Blocked by required conditions
CI / Deny unrelated histories (push) Blocked by required conditions
CI / Check contributors (push) Blocked by required conditions
CI / Check uv.lock (push) Blocked by required conditions
CI / Check no committed infographics (push) Blocked by required conditions
CI / package-lock.json diff (push) Blocked by required conditions
CI / Lint Docker scripts (push) Blocked by required conditions
CI / Build&Test Docker image (push) Blocked by required conditions
CI / Supply-chain scan (push) Blocked by required conditions
CI / Review label gate (push) Blocked by required conditions
CI / OSV scan (push) Waiting to run
CI / CI review comment (live) (push) Blocked by required conditions
CI / All required checks pass (push) Blocked by required conditions
CI / CI timing report (push) Blocked by required conditions
Docker Build, Test, and Publish / build (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest) (push) Waiting to run
Docker Build, Test, and Publish / build (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-24.04-arm) (push) Waiting to run
Docker Build, Test, and Publish / publish (amd64, type=gha,scope=docker-amd64, type=gha,mode=max,scope=docker-amd64, linux/amd64, ubuntu-latest) (push) Blocked by required conditions
Docker Build, Test, and Publish / publish (arm64, type=gha,scope=docker-arm64, type=gha,mode=max,scope=docker-arm64, linux/arm64, ubuntu-24.04-arm) (push) Blocked by required conditions
Docker Build, Test, and Publish / merge (push) Blocked by required conditions
auto-fix lint issues & formatting / Generate eslint --fix patch (push) Waiting to run
auto-fix lint issues & formatting / Apply patch (push) Blocked by required conditions
2026-07-27 12:47:17 -05:00
hermes-seaeye[bot]
846b14ab01
fmt(js): npm run fix on merge (#72703)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-27 14:52:58 +00:00