Dragging a folder from Explorer/Finder into the composer failed with "file
not found on gateway and no data_url provided", on local gateways too.
extractDroppedFiles tagged every OS drop as a File-bearing entry, so
partitionDroppedFiles routed the folder to the upload pipeline and
file.attach tried to read a directory's bytes — a directory has none, and
there is no data_url to send. This regressed in 4906dcfc25, which routed
OS drops through file.attach to reach a remote gateway but did not exclude
directories, which also carry a File handle.
Detect directories at drop time with DataTransferItem.webkitGetAsEntry(),
the only synchronous way to tell a dropped folder from a file. A dropped
directory now becomes a path-only entry with isDirectory set, which routes
to a @folder: ref exactly like the folder picker, instead of the file
upload path that cannot stage a directory.
Process transfer.items before transfer.files: webkitGetAsEntry lives only
on items, and claiming the folder's path there first lets the files
fallback dedup skip the same entry (Chromium lists a dropped folder in
both). Path-based dedup and the getPathForFile resolution are preserved.
Dark-mode connector lines and ring outlines read too faint. Double the
two live knobs: MODE_DEFAULTS.dark.lineAlpha 0.12->0.24 and
RING_PARAMS.dark.ringAlpha 0.03->0.06. (MODE_DEFAULTS.ringAlpha is dead;
the outline is drawn from RING_PARAMS.)
The last welded composer engine. The `@`/`/` trigger state, detection
(refreshTrigger), the adapter-driven item list + its effects, popover selection,
closeTrigger, commitTypedSlashDirective, and the contentEditable chip insertion
(replaceTriggerWithChip) move verbatim into hooks/use-composer-trigger.ts behind a
hook that takes the editor refs + the two completion sources (at/slash). ChatBar's
input/keydown/keyup paths + the popover render consume the returned API; the
keydown navigation block stays in place (no key-handling restructure), and
triggerKeyConsumedRef is exposed so keyup still skips its post-consume refresh.
ChatBar 1,248 → 1,047. Behaviour-preserving: typecheck 0 errors, eslint clean,
and the composer DOM repro suite (slash-nav, enter-submit, IME composition,
trigger-popover) is green — the documented IME/caret/focus edge fixes ride along
verbatim. (The 1 attachments.test.tsx failure is pre-existing on main.)
fallback-model.ts (1,696) folded into assistant-ui/tool/fallback-model/ with
three cohesive, self-contained leaf modules extracted (verbatim moves):
- types.ts (83) — the shared tool-view types/interfaces.
- format.ts (133) — pure value formatting/parsing (isRecord, compactPreview,
clampForDisplay, prettyJson, parseMaybeObject, unwrapToolPayload, numberValue,
contextValue, formatDurationSeconds).
- targets.ts (75) — url/path/preview detection + disclosure ids (looksLikeUrl,
findFirstUrl, hostnameOf, isPreviewableTarget, toolPart/GroupDisclosureId).
index.ts (1,434) keeps the tool-specific assembler (TOOL_META, titles, the count
machinery, subtitle/detail/diff, buildToolView) and re-exports the leaf modules,
so consumers importing `./fallback-model` are unchanged (folder index resolution)
— no importer or channel edits needed. The count/result/detail helpers reach
across each other around buildToolView, so they stay together to avoid a circular
split; the three leaves are the clean cut.
Behaviour-preserving: typecheck 0 errors, eslint clean, fallback-model test 24/26
(the 2 browser_navigate title failures are pre-existing on main — `hostnameOf`
intentionally includes the pathname; verified identical on the un-split file).
The merged #55859 left the star-map NodeContextMenu import and the
canvas onContextMenu prop out of perfectionist's required order, failing
`npm run lint` in the desktop workspace. Reorder both.
TUI /journey gets d/e with confirm + $EDITOR; desktop gets a right-click
context menu with inline edit modal. Both refresh the graph after mutation.
Extract openInEditor into the shared TUI editor helper.
Bring Hermes-Setup.exe's UI onto the shared design tokens (self-contained, no
desktop-component coupling) and add two capabilities:
- design: flat stage rows (running step opaque, rest muted), neutral check /
destructive cross, running fourier-flow Loader, hairline --stroke-nous
borders, fill-less log panel; ported BrandMark (nous-girl) + HackeryButton +
Loader standalone; re-synced button variants; de-boxed success/failure.
- theme: follow the OS light/dark via the authoritative Tauri window theme
(theme.ts + onThemeChanged, core🪟allow-theme), with Nous dark seed
colors in styles.css so the --ui-*/--dt-* chain derives correctly.
- updates: split the monolithic "Updating" bar into handoff -> download ->
rebuild (+ install on macOS) stages via a shared update_stages() builder, a
live elapsed timer on the running stage, and a dev-only fake-boot preview
(gated on import.meta.env.DEV, stripped from the shipped bundle).
Adds hooks/use-composer-url-dialog.test.tsx (renderHook): @url: directive
fallback, host onAddUrl preference + clear/close, and the blank-input no-op.
First unit coverage for an extracted composer engine — previously none of this
logic was testable while welded into the DOM-coupled ChatBar.
Moves the docked↔floating state, dock/float/toggle actions, drag-gesture wiring,
and the on-screen re-clamp effect out of ChatBar into
hooks/use-composer-popout.ts, verbatim. ChatBar passes its composerRef in and
consumes the returned popout state/handlers; the secondary-window gate and the
shared persisted atom stay encapsulated in the hook.
Moves the resting-placeholder state + the conversation-change re-roll effect +
the disabled/reconnecting/starting derivation out of ChatBar into
hooks/use-composer-placeholder.ts, verbatim. The hook owns its own i18n + browse
reset; ChatBar just reads the derived string.
Moves the URL dialog's open/value state, autofocus-on-open effect, and submit
(host onAddUrl or an @url: directive) out of ChatBar into
hooks/use-composer-url-dialog.ts, verbatim. ChatBar just wires the returned
openUrlDialog into the context menu and the state into <UrlDialog>.
Moves the chat-focused Esc-cancel listener (the latest-handler ref + the
register-once window keydown effect) out of ChatBar into
hooks/use-composer-esc-cancel.ts, verbatim. Encapsulating the latest-closure ref
inside its own hook is the first of the plan's "delete the latest-closure refs"
cleanups: it's no longer a loose ref in the 1.4k-line component, just an
implementation detail of a focused side-effect hook keyed on busy/awaitingInput/
onCancel.
Moves the CodingStatusRow hand-offs (openInWorktree + branch-off / convert /
list / switch) out of ChatBar into hooks/use-composer-branch.ts, verbatim. The
hook depends only on cwd + draftRef + clearDraft (backend coupling via the
projects store); nothing about ChatBar's render. Dead projects/composer-store
imports drop out of index.tsx.
Probe the projects.* RPC surface, block create with a clear update hint,
and avoid the raw "unknown method" toast. Includes i18n for en, zh, ja,
and zh-hant.
FixesNousResearch/hermes-agent#54999
Update the shared queued-edit ref synchronously with React state so draft persistence sees the correct edit mode while loading and restoring queued prompts. Also drop the accidental node_modules symlink from the PR.
Ensure Windows desktop and local terminal teardown kill full process trees so Git Bash descendants cannot survive wrapper exits and accumulate across retries.
Audit follow-up. ChatBar subscribed to the whole `$statusItemsBySession` (a
computed that rebuilds the entire map) + `$previewStatusBySession` maps just to
derive a boolean, so every per-item status mutation (a subagent tick, the 5s
background poll) and every OTHER session's change re-rendered the ~1.4k
component. The queue hook likewise subscribed to the whole `$queuedPromptsBySession`
map.
- Add `useSessionStatusPresence` — a coarse edge (useSyncExternalStore) that
flips only when the stack shows/hides; ChatBar uses it for the styling
data-attr instead of the two map subscriptions.
- Add generic `useSessionSlice(store, key)` — subscribes to one session's array,
bailing out when other sessions churn (the plain atom keeps per-key refs
stable). The queue hook now reads its slice through it.
Result: ChatBar re-renders only when the stack's presence flips or this session's
queue changes — not on background/subagent status streaming or other sessions.
Verified: typecheck clean, 0 lint errors, composer tests 39/40 (pre-existing
attachments failure unrelated).
Two composer fixes:
- **Paste/input lag** — `flushEditorToDraft` serializes the whole editor
(`composerPlainText` is O(n)); running it on every event during a burst
(holding a key, or holding Cmd+V into a growing editor) was O(n²). Coalesce
the input/paste path to one flush per animation frame. Lossless: the
contentEditable DOM is the source of truth and submit + the compositionend /
keydown paths re-read it synchronously (those stay immediate).
- **Detached-composer dock glow** — was `fixed inset-x-0` (full viewport, spilled
under the sessions sidebar). Switched to `absolute inset-x-0`, so it anchors to
the chat-column root the docked composer centers in — the glow now spans only
the thread area, matching the actual dock target.
Verified: typecheck clean, 0 lint errors, composer DOM repro tests pass.
Lift the submit orchestration out of ChatBar into
composer/hooks/use-composer-submit.ts: `submitDraft` (the one decision tree —
queue-edit save · slash-now-while-busy · queue · drain · send · stop),
`dispatchSubmit` (the shared send-with-restore primitive + the external-submit
listener), and `steerDraft`.
This is the seam where the draft and queue engines meet; it now reads both clean
APIs as explicit inputs instead of closing over inline state. ChatBar is left as
a thin coordinator that owns the shared `queueEditRef` and wires the four engines
(draft · queue · submit · metrics/voice/drop) into render.
Behaviour-identical (verbatim move). Verified: typecheck clean, composer DOM
repro tests (enter-submit, IME, slash-now, steer, drain) pass.
Lift the queue subsystem out of ChatBar into composer/hooks/use-composer-queue.ts:
the per-session queue-store binding + queuedPrompts, in-place queued-prompt
editing (begin/step/exit), the shared drain lock + send-then-remove sequence,
manual send-now, bounded auto-drain, and the three queue effects (re-key migrate,
idle auto-drain, queue-edit cleanup).
It consumes the draft API (draftRef/clearDraft/loadIntoComposer/focusInput) and
writes the coordinator-owned `queueEditRef` the draft engine reads — so the
draft↔queue coupling is two explicit deps, not an inline tangle. `steerDraft`
and the chat-focus Esc-cancel stay in ChatBar (not queue-internal).
Behaviour-identical (verbatim move). Verified: typecheck clean, composer DOM
repro tests + queue/edit paths pass.
De-entangle the draft spine: lift the source-of-truth engine (the imperative
composer-runtime subscription, edit primitives, focus, edge selectors, and
per-session load/clear/stash/restore) out of ChatBar into
composer/hooks/use-composer-draft.ts.
The draft↔queue cycle is broken by making `queueEditRef` a coordinator-owned
ref ChatBar threads into the hook (explicit dep, not an implicit shared global).
The contentEditable *event* handlers stay in ChatBar (they bridge into the
trigger engine) and drive the primitives the hook exposes.
Behaviour-preserving (verbatim move); typing perf preserved. Verified: typecheck
clean, composer DOM repro tests (enter-submit, IME, slash-nav) + text-guard pass.
Lift the attachment drop engine (dragActive + the 7 drag/drop handlers + the
in-app-ref vs OS-upload split) out of ChatBar into
composer/hooks/use-composer-drop.ts. Self-contained, off the keystroke path —
consumes insertInlineRefs + onAttachDroppedItems + requestMainFocus. Verbatim
move, behaviour-preserving.
Lift the dictation + voice-conversation + auto-speak subsystem out of ChatBar
into composer/hooks/use-composer-voice.ts. It owns voiceConversationActive,
lastSpokenIdRef, the pending-reply readers, submitVoiceTurn, the voice
hooks (recorder/conversation/auto-speak), the Ctrl+B toggle event, and
handleToggleAutoSpeak; it exposes dictate/voiceStatus/voiceActivityState/
conversation/start+endConversation/handleToggleAutoSpeak for the controls.
Self-contained: consumes the draft/submit primitives (insertText, clearDraft,
focusInput, onSubmit) passed in, nothing depends back on it — so unlike the
queue subsystem (which is circularly coupled to the draft helpers) it lifts
cleanly. Behaviour-preserving; verbatim move.
The real composer state-engine fix. ChatBar subscribed to the full draft string
(`useAuiState(s => s.composer.text)`), so every keystroke re-rendered the whole
~2k-line component even though the contentEditable DOM already owns the text.
Replace that with:
- an imperative composer-runtime subscription (useComposerRuntime().subscribe)
that mirrors text into draftRef, repaints the editor ONLY on external changes
(clear/restore/insert; the focused editor is the source otherwise), and drives
the debounced per-session stash — all without a React render. This folds the
old `[draft]` sync effect and the `[draft]` debounced-stash effect into one
place keyed off the runtime, surviving core rebinds via the effect dep.
- coarse edge selectors (hasText / isHelpHint / isSteerableText, plus
isEmpty / hasHardNewline in useComposerMetrics) for the chrome, which only
re-render when an edge actually flips.
Net: typing within a line does zero ChatBar re-renders / style invalidations;
work happens only on real edges. Behaviour-preserving — draftRef + editor are
already kept current by every mutation path; verified by the composer DOM repro
tests (enter-submit, IME composition, slash-nav) + text-guard.
First step of decomposing the ChatBar god component (composer/index.tsx). Pull
the self-contained *sizing* engine — stacked/inline layout + the measured-height
CSS vars the thread reads for clearance — into composer/hooks/use-composer-metrics.ts.
The hook owns: the media-query `narrow`, `expanded`/`tight`, the 8px height
bucketing (so per-keystroke growth never invalidates the tree's computed style),
the ResizeObserver, the popout re-sync, and the CSS-var cleanup. ChatBar now
just calls `useComposerMetrics(...)` and consumes `stacked`.
Behaviour-preserving (no keystroke/IME/contentEditable path touched): code moved
verbatim. Deliberately a low-risk first slice on the app's most fragile file;
the draft/state-engine spine is the next, dogfood-heavy step
(see desktop-composer-plan.md).
Two status-stack icon nits:
- Subagents used `hubot`; switch to the dedicated `agent` codicon.
- The queue section had no icon while every other group (todos, subagents,
background) has one. Give it `layers` (a stack of pending turns), matched to
the group-icon styling so all four sections read consistently.
Preview links (detected HTML files / localhost dev URLs) were rendered as
CHILDREN of the background StatusSection, which is collapsed by default — so the
moment a background task appeared, the previews got swallowed into the collapsed
"N Background" expandable and vanished until you manually expanded it. With no
background group they rendered as a standalone always-visible block, so the bug
only showed once a bg task was running.
Render the preview links as their own always-visible block right after the
background section instead of as collapsible children. They stay visually
associated with the background group (a localhost dev server and its preview are
the same thing) but are no longer hidden by its collapse — a one-tap open is the
whole point.
- Rework share/import into one Dialog (matches rename/create): a single code
field (copy to share, paste + Load to import) with a hover copy button, a
Reset link beside the upload icon when viewing an imported map, and plainer
copy.
- Core orb: scales with the world zoom (~1.25× the inner shell), backdrop wash
behind it; on focus/hover the scene composites above the orb so the active
tooltip + lit lines are never covered.
- fitViewport floors zoom at the reference (5-ring) extent, so big maps render
at a constant scale and pan instead of shrinking every node to fit.
- Light mode: flip inter-ring band shading to read as depth (not a mound),
fade the core ring in from t=0, drop the timeline star glow.
- Timeline: filled play glyph, crisper constellation, date moved into the legend.
After the slash dispatcher, the next-largest body unit was submitPromptText —
a ~280-line submit pipeline. Lift it into a colocated useSubmitPrompt sub-hook
(use-prompt-actions/submit.ts) with a typed SubmitPromptDeps object; body moves
verbatim. SubmitTextOptions moves to utils.ts (shared by submit + submitText).
Pure restructuring, no behaviour change (full use-prompt-actions suite green).
index.ts: 1,212 -> 937.
The remaining bulk of useMessageStream was handleGatewayEvent — a ~550-line
event-type dispatcher. Lift it into a colocated useGatewayEventHandler sub-hook
(use-message-stream/gateway-event.ts): the values it closed over (sibling
streaming callbacks + the 3 stable refs the deps array omitted + options)
become a typed GatewayEventDeps object; the dispatcher body moves verbatim.
Pure restructuring, no behaviour change (utils tests still green). index.ts:
1,120 -> 540.
The usePromptActions body's largest unit was executeSlashCommand — a ~530-line
`/command` dispatcher. Lift it into a colocated useSlashCommand sub-hook
(use-prompt-actions/slash.ts): the ~13 values it closed over become a typed
SlashCommandDeps object the parent passes in; the dispatcher body (and its inner
runSlash recursion) moves verbatim. SlashActionCtx (slash-only) moves with it.
Pure restructuring, no behaviour change (verified: full use-prompt-actions test
suite still green). index.ts: 1,772 -> ~1,250.
Extract the standalone gateway-event helpers (session-info patch derivation,
completion-error detection, todo-payload routing, delegate_task -> subagent
spec mapping, + the stream-flush/subagent-event constants) out of the
1,285-line hook into a colocated, tested use-message-stream/utils.ts. index.ts
keeps the stateful streaming hook and consumes the helpers.
Pure restructuring, no behaviour change; folder index keeps the import path
intact. index.ts: 1,285 -> ~1,120. Adds unit tests for the pure helpers.
Extract the ~16 standalone helpers (message reconciliation, optimistic/resolved
session upserts, stored-session resolution, runtime-info application, error
classification) out of the 1,254-line god hook into a colocated, tested
use-session-actions/utils.ts. index.ts keeps the hook orchestrator (the
stateful action callbacks) and consumes the helpers.
Pure restructuring, no behaviour change; folder index keeps the import path
(`@/app/session/hooks/use-session-actions`) intact. index.ts: 1,254 -> ~950.
Adds unit tests for the pure helpers.