Commit graph

1485 commits

Author SHA1 Message Date
brooklyn!
a606d24cf2
Merge pull request #71679 from NousResearch/bb/default-effort
fix(desktop): honor the configured reasoning effort instead of assuming medium
2026-07-25 20:58:00 -05:00
Brooklyn Nicholson
43d1088ba2 refactor(desktop): give reasoning effort one owner
The seven effort levels were enumerated four times in four shapes: a
labels map, a radio-option list, a settings value array, and an enum
list for the config field. Each carried its own hardcoded medium
fallback, which is how the picker drifted from the configured default in
the first place.

Collapse them into lib/reasoning-effort, which owns the scale, the
labels, and the resolve/fallback rules. Net -80/+26 in source. Dropping
the effortLabelKey cast also means the i18n keys are now type-checked
against the canonical list instead of asserted into place.
2026-07-25 20:51:18 -05:00
brooklyn!
10160a1808
Merge pull request #71672 from NousResearch/bb/tab-session-titles
fix(desktop): name a Cmd+T session from its first message
2026-07-25 20:51:01 -05:00
Brooklyn Nicholson
fab0e60570 fix(desktop): name a Cmd+T session from its first message
A Cmd+T tab's session is created unlisted (openNewSessionTile passes
`listed: false`), so it has no $sessions row until its first turn
persists and a refresh surfaces it. For that whole first exchange the
tab strip and the sidebar read "New session".

Cmd+N has no such gap: its session is created per-send, and
createBackendSessionForSend seeds the optimistic row with the user's
text as the preview. A tile never reaches that path — its runtime id is
already bound, so its createBackendSessionForSend seam is a no-op that
returns the existing id.

Seed the row the same way on a tile's first send. The tab strip already
re-syncs on $sessions (watchSessionTiles lists it in `also`), so the tab
and the sidebar both name the session within the first message; the
server's auto-title supersedes the preview when the turn completes.

Guarded so it only ever fires once per session: no-op on empty text and
on an already-listed row, matching across compression by lineage root so
a re-send can't clobber a real title with a raw message preview.
2026-07-25 20:44:19 -05:00
brooklyn!
a288fc341c
Merge pull request #71678 from NousResearch/bb/code-block-overflow
fix(desktop): keep code and diffs out of the tool overflow window
2026-07-25 20:42:22 -05:00
Brooklyn Nicholson
be1edef5ce fix(desktop): show the real effort on the composer pill
The pill hardcoded 'Med' whenever the surface had no explicit effort, so
a profile configured for high advertised a level the agent would not
use. Take the profile default as a fallback and drop the now-unused
shell.modelMenu.medium key across the locales.
2026-07-25 20:37:13 -05:00
Brooklyn Nicholson
e2122f22b8 fix(desktop): stop the model picker overriding the configured effort
Selecting a model applied 'preset.effort ?? medium', so a user running
agent.reasoning_effort: high was silently downgraded to medium on every
model switch and every new chat that inherited the pick. The Thinking
toggle and the effort radio group defaulted to the same literal.

Resolve all of them from the profile default instead, falling back to
DEFAULT_REASONING_EFFORT only when config has not loaded.
2026-07-25 20:37:09 -05:00
Brooklyn Nicholson
58f5a05655 fix(desktop): publish the profile default reasoning effort to a store
The composer only seeded effort from agent.reasoning_effort when it was
about to reseed the whole selection, which a sticky manual model pick
skips. The configured value was read and discarded, leaving every other
surface with no way to resolve the profile default.

Mirror it into $defaultReasoningEffort on every config load, independent
of the composer reseed, so the manual pick stays sticky without hiding
the default from the surfaces that need it.
2026-07-25 20:37:06 -05:00
Brooklyn Nicholson
7f2971039a fix(desktop): keep code and diffs out of the tool overflow window
A run of 3+ adjacent tool calls collapses into the 6.75rem
`.tool-group-scroll` window. That is right for status rows, but a patch
or execute_code row's body is a code block the user reads, and the
window squeezed it to a ~2-row viewport behind a fade mask.

The CSS break-out (`:has([data-tool-row][data-tool-open])`) already
lifts the cap once a row is expanded, which is why most tools are safe
to bound. It can't help here: the user has to notice the clipped block
and expand it before the code is legible.

Extend the existing UNBOUNDABLE_TOOLS opt-out to the code-bearing tools
behind an `isUnboundableTool` predicate, deriving the file-edit names
from `isFileEditTool` so a newly supported edit tool can't be exempt in
one place and clipped in the other. `terminal` stays boundable: console
output is a log tail whose last lines are the ones that matter, which
is exactly what the window pins.

Verified in Chrome against the app's compiled CSS: a 3-call run
carrying a 520px diff rendered at 108px before, full height after.
2026-07-25 20:35:01 -05:00
hermes-seaeye[bot]
ba159d6fa9
fmt(js): npm run fix on merge (#71667)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-26 01:21:38 +00:00
teknium1
751c15f9eb lint(desktop): exempt benign ref writes flagged by the new atom-mirror guard
The guard matches any `.current` write inside a `useEffect`, so it also
flags refs that are not mirroring a reactive value. Seven such sites
landed on main after the rule was written:

  - wiring.tsx: one-shot request-seen sentinel
  - billing/plans-view.tsx: previous-confirming tracker for focus return
  - config-settings.tsx: autosave bookkeeping
  - gateway-settings.tsx: monotonic request-sequence counters
  - toolset-config-panel.tsx: mount flag + one-shot provider-choice claim
  - voice-provider-fields.tsx: one-shot config seed flag

None mirrors an atom/prop/state value, so none can produce the
one-render-lag stale read the rule exists to prevent. Each gets an
eslint-disable with the specific reason rather than widening the rule,
which would reopen the hole for real mirrors.

Verified the guard still catches the real antipattern: a probe file
mirroring `useStore($activeSessionId)` into a ref via useEffect is
reported. eslint over apps/desktop/src is 0 errors, and the 24 remaining
warnings match the pre-change baseline exactly.
2026-07-25 18:12:48 -07:00
ethernet
e026fd61d8 lint(desktop): ban atom-mirrored refs via no-restricted-syntax eslint rule
A ref synced from a nanostores atom via useEffect lags the atom by one
render. Callbacks that read the ref instead of the atom get stale values —
cancelRun sent session.interrupt to the wrong session (#66485), and
steerPrompt/restoreToMessage/editMessage had closure-priority stale reads.

The rule catches the three shapes of this antipattern:
  - useEffect(() => { ref.current = value }, [value])
  - useEffect(() => { ref.current = value; ... }, [value])
  - useEffect(() => { setMutableRef(ref, value) }, [value])

All 79 existing hits get eslint-disable-next-line with a comment. The
legitimate ref writes (DOM instances, mount flags, request tokens,
prev-value tracking, prop mirrors) stay suppressed; the 11 real bug
sites will be fixed in the next commit so their suppressions can be
removed.

Rule is scoped to apps/desktop only — ui-tui and tests-js don't use
nanostores and don't have this pattern.
2026-07-25 18:12:48 -07:00
hermes-seaeye[bot]
44480617ff
fmt(js): npm run fix on merge (#71638)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-25 23:26:29 +00:00
hermes-seaeye[bot]
2ba1e028e4
fmt(js): npm run fix on merge (#71634)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-25 23:18:02 +00:00
SHL0MS
6b26b409cf fix(desktop): /goal arg stays editable, kickoff queues when busy, slash header stops echoing long args
Four symptoms from the same /goal flow on desktop:

- Typing '/goal <text>' sealed the command into a directive chip on
  Space because /goal was registered without args:true, so the goal
  prose rendered awkwardly after a pill. The registry row now matches
  /personality and /tools: the arg stays editable text.

- The slash status header echoed the ENTIRE invocation ('slash:/goal
  <whole goal prose>') in mono, immediately above the backend notice
  that repeats the goal text again, and the kickoff user bubble that
  repeats it a third time. The header now carries just the command
  token (slash:/goal).

- When the session was busy, handleDispatch rendered 'session busy'
  and dropped the dispatch message. For /goal that message is the
  kickoff prompt, and the backend has ALREADY set the goal by then —
  the goal existed but the agent never heard about it, and later turns
  looked goal-unaware (#63352). The busy path now queues the kickoff
  on the composer queue: it sends on settle and is visible/editable in
  the queue panel meanwhile. Falls back to the old message if the
  queue rejects the entry.

- A slash command issued on a fresh draft created the backend session
  with no preview, so the sidebar row sat as 'Untitled session' —
  and when the kickoff was dropped, auto-title never fired either
  (it needs a completed user->assistant exchange). ensureSessionId now
  seeds the preview with the typed command.

Tests: registry row contract, busy-path queueing (kickoff neither
sends mid-turn nor vanishes), and the header-token assertion.
2026-07-25 16:09:38 -07:00
hermes-seaeye[bot]
ec734b6658
fmt(js): npm run fix on merge (#71626)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-25 23:00:16 +00:00
teknium1
584b274495 fix(desktop): slash commands target the user's chat, not a new session
`/goal status` reported "No active goal" for a goal that was live: the
desktop's slash pipeline resolved its target session differently than the
submit pipeline, so the command ran against a different session than the
chat on screen.

`/goal` state is persisted per-session in SessionDB (`state_meta` key
`goal:<session_id>`). slash.ts resolved with a bare
`hint || activeRef || createBackendSessionForSend()`, so whenever the
runtime binding was momentarily absent — profile swap, reconnect,
orphan-reap, request timeout — it silently MINTED A NEW SESSION and ran
there. submit.ts already handles this case by resuming the routed stored
session on its owning profile (#55578, #67603).

Extract that ladder into one shared resolver both pipelines use, per the
"one resolver owns each policy" rule in apps/desktop/AGENTS.md. This fixes
the whole class, not just `/goal`: every exec/rpc slash command
(`/usage`, `/status`, `/tools`, …) had the same hole.

A targeted durable conversation whose runtime cannot be rebound now
returns null instead of forking the chat — reporting that a command could
not run beats running it against the wrong session.
2026-07-25 15:44:39 -07:00
teknium1
78c06525e8 fix(desktop): keep optional action handlers optional through the latest-actions adapters
The adapters wrapped every field in an arrow function, including the
optional ones. That makes an absent handler unconditionally truthy, and
several children gate on a handler's PRESENCE rather than just calling
it:

  - onDismissError    -> assistant-message.tsx renders the dismiss button
                         only when defined
  - onRestoreToMessage -> thread/index.tsx gates the restore-confirm flow
  - onTranscribeAudio  -> use-voice-recorder / use-voice-conversation gate
                         recording on it
  - onLoadMoreMessaging / onLoadMoreProfileSessions -> sidebar paging

So the adapter would paint a dead dismiss button and let voice recording
proceed into a no-op transcription path even when the controller had
deliberately left those handlers off.

Wrap an optional field only when it is currently present, and re-read the
latest value inside the wrapper so the stale-closure fix still applies.
Presence is stable for a given actions object (the controller mutates
fields in place rather than toggling a handler between defined and
undefined), while the closure is what churns — which is exactly what the
indirection re-reads.

Adds two regression tests: absent optional handlers stay undefined, and a
present optional handler still late-binds to the latest closure. The
first was verified to fail against the unconditional-wrapper form.
2026-07-25 13:01:14 -07:00
eagle-nyp
344976773a test(desktop): cover stale steer action binding 2026-07-25 13:01:14 -07:00
eagle
8f53429651 fix(desktop): avoid stale actions in memoized surfaces 2026-07-25 13:01:14 -07:00
hermes-seaeye[bot]
5baf174781
fmt(js): npm run fix on merge (#71549)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-25 19:53:26 +00:00
teknium1
771f1b7f21 fix(desktop): route live-turn and history actions by the current session, not a stale closure
Redirect/steer, regenerate, restore-checkpoint, edit-message, and
change-cwd read `activeSessionId || activeSessionIdRef.current`, which
prefers the closure-captured prop whenever it is non-null and only falls
back to the ref once the prop is null.

That precedence is backwards. The actions bag is a stable ref that
wiring.tsx mutates in place (Object.assign), and the pane surfaces are
memoized on that stable ref, so a surface does not re-render when the
active session changes and keeps whichever closure was current when it
last rendered. `activeSessionIdRef` is the authority: it is mirrored
during render in use-session-state-cache, and submit.ts /
use-session-actions pin it imperatively mid-flight without touching the
source prop. The prop is stale by design. `cancelRun` in the same file
already reads the ref exclusively and documents exactly this hazard.

User-visible effect after switching chats: a typed correction was
delivered into the previously focused conversation's live turn (the
"session suddenly working on another chat's task" report), and rewinds
truncated the wrong session's transcript — real data loss, since a
truncating resubmit deletes history after the target ordinal. Nothing
crosses over in stored state, which is why a DB/transcript audit of the
affected session comes back clean.

Also fixes the same defect in `changeSessionCwd`, where a stale target
re-anchored another conversation's workspace, pointing that agent's
terminal/file tools at the wrong project. The now-unused
`activeSessionId` option is dropped from useCwdActions rather than left
as a footgun for the next caller.

Not changed, verified not affected: model-edit-submenu reads the runtime
id via `useStore` (live subscription, re-renders on change), and
use-composer-actions guards on `attachedSessionId === activeSessionId`,
so a stale value there only skips a detach instead of writing
cross-session.

Tests: 4 regression cases pin each action to the current session when
the prop and the ref disagree. Verified to fail against the pre-fix code
(all four reported the stale `rt-abc123` instead of the current
session), including a scripted revert of all four sites.

Co-authored-by: Drew Donaldson <49219012+Automata-intelligentsia@users.noreply.github.com>
2026-07-25 12:45:46 -07:00
Perseus Computing
ac327dfa38
fix(desktop): resolve unpacked spawn-helper before chmod (#71171) 2026-07-25 15:19:29 -04:00
hermes-seaeye[bot]
46815f4910
fmt(js): npm run fix on merge (#71532)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-25 19:11:39 +00:00
brooklyn!
5349c7c280
Merge pull request #71162 from NousResearch/bb/session-link-titles
feat(desktop): resolve @session links to titles you can click
2026-07-25 14:03:39 -05:00
Brooklyn Nicholson
9edbc68d0f feat(desktop): open the session a @session ref names
An agent-written ref rendered as a chip that went nowhere on click. It now
renders as an ordinary inline link — the agent wrote it mid-sentence, so it
should read like one — with the funnel icon leading the resolved title.
Clicking either surface (that link, or the chip in the user's own message)
opens the session as a tab, the way its sidebar row does.

The tile store loads on click rather than at import: the composer's rich
editor pulls this module in, so a static import would boot the profile store
and its REST routing along with every transcript render.
2026-07-25 12:09:36 -05:00
hermes-seaeye[bot]
760112adb6
fmt(js): npm run fix on merge (#71236)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-25 06:30:23 +00:00
teknium1
0c48d2bda9 style: sort Harness props in the leak-guard test (perfectionist/sort-jsx-props) 2026-07-24 23:22:04 -07:00
teknium1
fd29ce5102 fix: widen requestGateway mock signature for typecheck (follow-up for salvaged PR #69689) 2026-07-24 23:22:04 -07:00
theone139344
062d261955 fix(desktop): prevent cross-session leak in background queue drain
A background queue drain (fromQueue: true) whose runtime binding was
reaped by the gateway fires with sessionId=null. The expression

  options?.sessionId ?? activeSessionIdRef.current

falls back to whichever runtime id the foreground happens to hold,
landing the queued prompt in the session the user is currently viewing
instead of the session that owns the queue entry — a cross-session
message leak.

Guard the fallback: only inherit the foreground runtime when the drain
targets the current view (no storedSessionId, or it matches the
foreground). A background drain (storedSessionId differs) is left with
sessionId=null so the existing session.resume path rebinds the correct
runtime before prompt.submit fires.

Includes a regression test: "a fromQueue drain with null runtime id
does NOT land in the foreground session (cross-session leak guard)".
All 53 existing tests pass.
2026-07-24 23:22:04 -07:00
brooklyn!
4ba71e6aa4
Merge pull request #71202 from NousResearch/bb/desktop-session-drop-target
fix(desktop): resolve drop targets and focus against the visible tab
2026-07-25 00:24:21 -05:00
Brooklyn Nicholson
0e1332abbb test(desktop): cover hidden-tab resolution for drops, focus, and timeline
Each case mounts a stacked group with a hidden tab whose geometry
matches the visible one — the arrangement that made the original bug
invisible to selector order.
2026-07-25 00:09:30 -05:00
Brooklyn Nicholson
771dfcc083 fix(desktop): keep background tabs out of blur, timeline, and key routing
Same ambiguity in three more lookups: blurComposerInput could blur a
hidden input and leave the focused one alone, the timeline scrolled
whichever viewport it found first, and a clarify card waiting in a
background thread swallowed the foreground composer's letter keys.
2026-07-25 00:09:30 -05:00
Brooklyn Nicholson
3fbc9bebe1 fix(desktop): drop a dragged session on the tab the user can see
The drag snapshotted every chat surface and composer in the document,
so with a stacked tab group the link/split resolved against whichever
pane came first — usually a hidden one — and the @session chip landed
in a composer nobody was looking at.
2026-07-25 00:09:30 -05:00
Brooklyn Nicholson
9285de4a41 fix(desktop): mark kept-alive panes so document lookups can skip them
Inactive tabs stay mounted and hidden with `visibility`, which preserves
their layout box — so a background tab answers a document-wide selector
with a rect identical to the visible tab's. Tag hidden layers and route
those lookups through visible-scoped query helpers.
2026-07-25 00:09:30 -05:00
hermes-seaeye[bot]
9823f15f6a
fmt(js): npm run fix on merge (#71196)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-07-25 04:51:19 +00:00
brooklyn!
62e07223d6
Merge pull request #71184 from NousResearch/bb/desktop-stream-resume
fix(desktop): survive and resume turns interrupted mid-stream
2026-07-24 23:42:57 -05:00
teknium1
36703753e0 test(desktop): pin visible agent-init-failure surfacing + optimistic-message survival (#63078)
Client half of leg 2. The issue's failure mode was the desktop clearing the
optimistic first message and showing nothing when the backend dropped the
turn. With the gateway now preserving the message across slow builds and
emitting a real error event on genuine failure, these tests pin the desktop
contract that makes that visible:

- an agent-init error event renders an in-transcript assistant error bubble,
  keeps the user's optimistic first message (never silently cleared), fires
  the global error toast, and releases busy/awaitingResponse so the composer
  is usable again;
- the #65567 pre-ready cancel emit renders the same way.

Covers failAssistantMessage + the gateway-event error branch, which no test
exercised at the session-state level (todo-cleanup only asserted todo
eviction).
2026-07-24 21:40:08 -07:00
Dr Kennedy Umege
b20b235206 test(desktop): pin busy-gateway churn tolerance end-to-end through the submit pipeline (#64327)
From PR #64327 (@Kenmege), whose target-aware drift predicate (compare route
tokens by their routed chat target; ignore selection null-resets, search/hash
churn, and background active-ref retargets; never count a move onto the
submit's own target) already reached main via the #69578 salvage
(1bdd478efa, Co-authored-by Kennedy Umege) and gained the #70986 composerScope
prong. What main covered only at the unit level (session-context-drift.test.ts)
is here pinned at the pipeline level, both directions:

- a send from a second chat rides out simultaneous programmatic churn
  (selection null-reset from a gateway/profile reconnect, active-ref retarget
  from a background event, search/hash-only route change from an overlay)
  mid-session.resume and still reaches prompt.submit;
- a genuine user switch (selection AND route moving to another real chat)
  mid-submit still aborts before prompt.submit.

Part of the #63078 fix branch.
2026-07-24 21:40:08 -07:00
杨子聪
83333c6cf3 test(desktop): pin the genuine post-create switch abort during attachment sync (#62805)
Salvaged from PR #62805 (@floatingrain), whose diagnosis of the deterministic
frontend self-abort (createBackendSessionForSend mutating the selected ref +
route, then the caller's drift guard reading its own re-home as a user switch)
was correct — and correct about the sweeper misread that closed it: the
sweeper's implemented_on_main verdict cited submit.ts:245, which was inside
the session.resume block, while the raw post-create guard at the
createBackendSessionForSend site was still aborting every new chat on the
then-current main (4281151ae8). The mechanism itself has since landed via
8c288760d0 + 1bdd478efa, so what remains distinct is this regression: after
the pipeline adopts the created chat as its pinned target, a GENUINE user
switch (selection and route both moving to another chat during the
attachment-sync await) must still abort instead of being masked by the
re-baseline.

Part of the #63078 fix branch.
2026-07-24 21:40:08 -07:00
Roger Han
eb2f648628 test(desktop): pin first-send delivery across a late React Router route commit (#62990)
Salvaged from PR #62990 (@Roger--Han), a competing leg-1 fix for #63078. Its
mechanism (a pinned-route-token set accepting both the pre-commit and the
deterministic created-session token) was superseded by main's target-aware
drift predicate (1bdd478efa), but the PR pinned a timing case nothing on main
covered: React Router exposing the STALE new-chat route to the submit
continuation after create returns, committing the created session's URL only
before the next await settles. Both snapshots are the pipeline's own
transition — the first prompt must still reach prompt.submit.

Includes the contributors/emails mapping (added directly:
scripts/add_contributor.py's login regex rejects the consecutive hyphen in
the real GitHub login Roger--Han).

Part of the #63078 fix branch.
2026-07-24 21:40:08 -07:00
giggling-ginger
620d5801d4 test(desktop): pin first-message delivery through the new-chat route transition (#62562)
Salvaged from PR #62562 (@giggling-ginger). The mechanism that PR proposed
(adopt the session identity a first-message submit creates as the pipeline's
new pinned target; distinguish the expected new-chat route replacement from a
genuine concurrent switch) has since landed on main via 8c288760d0 and the
target-aware drift predicate (1bdd478efa / #69578). What main still lacked was
this PR's stronger regression coverage:

- The positive leg now asserts the FULL RPC transcript — exactly one
  prompt.submit addressed to the created runtime id with the user's text, no
  session.resume detour — instead of only 'not resume', and makes the creator
  stub faithful to the real one (re-homes selection AND route, receives the
  preview text used to seed the sidebar row).
- A new abort leg pins the route-moves-first sidebar-navigation race: the
  route changes to a different chat while the selected ref still points at
  the just-created session (navigate() commits before resumeSession() updates
  the ref), which must still abort rather than deliver into the wrong chat.

Part of the #63078 fix branch.
2026-07-24 21:40:08 -07:00
Brooklyn Nicholson
082bd17122 feat(desktop): auto-continue turns interrupted by a crash
Mid-turn progress lives only in process memory — the agent flushes to
SQLite at turn end — so an app/backend/machine death mid-turn lost the
turn entirely: reopening the session showed the recovered partial, but
the work never finished and the prompt itself survived nowhere durable.

Turns now write a durable marker (bounded per-profile sidecar) when they
start running and clear it when they conclude; success, handled error,
and interrupt all clear it, so a surviving marker is positive proof of a
process death. session.resume reads the marker: a fresh interruption
(desktop.auto_continue.freshness_minutes, default 15) is re-submitted
automatically as a continuation turn carrying the original prompt in an
interruption note, streams live to the client that just resumed, and
renders as a "resumed interrupted turn" event row. Stale markers are
cleared and the recovered partial speaks for itself; a turn that keeps
crashing stops auto-continuing after max_attempts (default 2) — the same
freshness + crash-loop-breaker posture as the messaging gateway's
restart auto-resume.
2026-07-24 23:31:56 -05:00
Brooklyn Nicholson
b8675a1899 fix(desktop): surface terminal error frames as failed bubbles
message.complete frames with status "error" were detected only by a text
regex heuristic, which misses the gateway's "Error: <detail>" texts and
partial-text failures — a failed turn rendered as a healthy reply. The
structured error/partial fields now drive the failure state: the bubble is
marked failed from the frame's error field, and a partial failure keeps
its streamed text visible instead of stripping it. session.resume's
inflight projection likewise carries a retained failure's error onto the
projected assistant row, so a failed turn recovered after a disconnect
renders as failed rather than as a healthy partial answer.

Co-authored-by: Reza Sayar <rsayar@uvic.ca>
2026-07-24 23:31:56 -05:00
Brooklyn Nicholson
8d8d1d61fe feat(desktop): crash-survivable in-flight turn journal
The renderer's session-state cache is memory-only and the backend's
inflight snapshot dies with the backend process, so nothing survived a
full app or machine death mid-turn: reopening the session showed the
transcript up to the last committed turn and silently dropped everything
the crashed turn had streamed.

While a turn runs, the visible tail (user prompt + streamed assistant
rows, tool calls included) is now journaled to localStorage — throttled
off the delta-flush hot path, bounded (24 entries / 7 days), cleared the
moment the turn settles. Session resume folds the journaled tail back
onto the restored transcript. When the backend also has a live text-only
inflight projection for the same turn, the journal overlays its richer
structure onto that row (longer text wins, base row id kept so live
deltas keep landing) instead of treating it as caught up — the ordering
defect that dropped locally recorded tool progress in the original PR.

Co-authored-by: Omar Baradei <omar@kostudios.io>
2026-07-24 23:31:55 -05:00
Brooklyn Nicholson
99ab036291 feat(session-search): give the agent a link to hand back
Asked to link to a session, the agent had no way to know the @session
reference syntax exists — every mention in the tool schema described
consuming a link the user dropped, never writing one — so it answered
with the title and timestamp as prose and the desktop had nothing to
render.

Every result now carries a ready-to-copy `link`, and the schema says to
write it inline instead of restating the title around it. The profile
segment is omitted when the active profile can't be named confidently;
a bare id still resolves.

Also skip linkifying a ref a model already wrapped in a markdown link,
which would otherwise rewrite into a nested link.
2026-07-24 23:30:25 -05:00
Brooklyn Nicholson
92439be351 feat(desktop): render agent-written @session links as chips
Assistant text goes through the markdown renderer, not DirectiveContent,
so a session reference an agent wrote came out as literal text. Rewrite
bare refs into `#session/<value>` links during markdown preprocessing and
dispatch that href to the shared chip in MarkdownLink, alongside the
existing media and preview hrefs.

Preprocessing already skips code fences and inline code, so a ref being
discussed in code stays literal. The pure parsing/href helpers move to
session-refs.ts to keep the resolver's React and API imports out of the
per-flush preprocess path.
2026-07-24 22:52:45 -05:00
Brooklyn Nicholson
dfbc9dbb15 feat(desktop): show resolved titles on @session chips
Route session refs in the transcript through the title resolver so a
dropped session reads as its title instead of a truncated id, and use
Tabler's funnel for the session chip icon.
2026-07-24 22:52:34 -05:00
Brooklyn Nicholson
cbad98e04e feat(desktop): add session link title resolver
Resolve @session:<profile>/<id> reference values to the session's title:
the in-memory sidebar list answers most lookups, and an unknown id falls
back to GET /api/sessions/{id}. Cache, in-flight dedupe, and subscriber
fan-out mirror the external-link title resolver.

An untitled row resolves to empty rather than "Untitled session" so the
caller's short-id fallback stays the chip label.
2026-07-24 22:52:16 -05:00
brooklyn!
666824261a
Merge pull request #71121 from NousResearch/bb/desktop-image-persist
fix(desktop): keep attached images renderable across session switches and restarts
2026-07-24 22:23:42 -05:00