Skip to content

fix(desktop): /goal arg stays editable, kickoff queues when busy, header stops echoing long args - #68776

Closed
SHL0MS wants to merge 1 commit into
NousResearch:mainfrom
SHL0MS:fix/desktop-goal-slash-ux
Closed

fix(desktop): /goal arg stays editable, kickoff queues when busy, header stops echoing long args#68776
SHL0MS wants to merge 1 commit into
NousResearch:mainfrom
SHL0MS:fix/desktop-goal-slash-ux

Conversation

@SHL0MS

@SHL0MS SHL0MS commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Running /goal <text> on desktop misbehaves four ways at once, and they compound into "the goal didn't stick":

  1. The typed text renders weirdly. /goal was registered without args: true, so the trigger engine treated it as a no-arg command: pressing Space sealed it into a non-editable directive chip and the goal prose sat awkwardly after a pill. /personality and /tools already carry args: true; /goal now matches.

  2. The goal text was echoed three times. The slash status header used the full invocation (slash:/goal <entire goal prose> in mono), directly above the backend notice that repeats the goal ("⊙ Goal set (20-turn budget): ...") and the kickoff user bubble that repeats it again. The header now carries just the command token.

  3. The kickoff was silently dropped when the session was busy — the real "goal didn't register" bug ([Bug]: Desktop submit fails after /goal; local session row created but message never reaches backend #63352). By the time slash.exec returns its send dispatch, the backend has already set the goal (GoalManager.set runs server-side). handleDispatch then hit busyRef.current, rendered "session busy — /interrupt ...", and returned — discarding the kickoff message. Result: the goal exists in the judge's state, but the agent never hears about it, so every subsequent turn looks goal-unaware. The busy path now queues the kickoff on the composer queue: it sends when the running turn settles, and it's visible and editable in the queue panel meanwhile. If the queue rejects the entry, the old message is preserved as fallback.

  4. A /goal (or any slash) on a fresh chat left the session named "Untitled session". The slash path created the backend session with no preview seed, and auto-title needs a completed user→assistant exchange — which never happened when the kickoff was dropped. ensureSessionId now seeds the sidebar preview with the typed command, and fixing (3) restores the auto-title path.

Related but out of scope: #54985 (gateway /goal lacks draft/show/wait/unwait parity — backend), #48236 (goal status bar — feature), #62202 (gateway post-turn goal continuation — backend). #63376 addresses a different submit race after /goal (stale activeSessionId prop in useSubmitPrompt) and composes with this change rather than overlapping it.

Related Issue

Fixes the kickoff-drop half of #63352.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • apps/desktop/src/lib/desktop-slash-commands.ts: args: true on the /goal registry row.
  • apps/desktop/src/app/session/hooks/use-prompt-actions/slash.ts:
    • withSlashOutput renders the status header with the command token (/${ctx.name}) instead of the full invocation.
    • ensureSessionId accepts a preview and withSlashOutput passes the typed command, so slash-created sessions seed a sidebar row name.
    • the handleDispatch busy guard enqueues the dispatch message on the composer queue (keyed via resolveComposerSessionKey, mirroring the ChatBar queue scope) instead of dropping it; the inline status line reports the queue.
  • Tests:
    • desktop-slash-commands.test.ts: /goal registry contract (exec + args).
    • use-prompt-actions/index.test.tsx: busy-path test proving the kickoff neither submits mid-turn nor vanishes (it lands on the queue and the notice still renders), and a header-token test proving long args are no longer echoed in the header.

How to Test

  1. Start a long turn, then type /goal finish the readme and press Enter while the agent is busy.
  2. Before: "session busy — /interrupt the current turn before sending this command", and the kickoff is gone — the goal is set backend-side but the agent never receives it. After: the notice renders, the status line says the message is queued, the queue panel shows it, and it fires when the turn settles.
  3. Type /goal (with a space) in the composer: the text after the command stays editable prose instead of collapsing into a chip.
  4. On a brand-new chat, run /goal <text>: the sidebar row is named from the command immediately, and auto-title runs after the kickoff exchange completes.
  5. Renderer checks: cd apps/desktop && npx tsc -p . --noEmit && npx vitest run --project ui (1745 passed, 209 files), eslint clean on touched files.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run the desktop renderer suite (npx vitest run --project ui) and all tests pass; pytest N/A, no Python changes
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 26.5

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A (no user-facing docs describe the broken behaviors)
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A (renderer-only)
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

…sh 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 (NousResearch#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.
@teknium1

Copy link
Copy Markdown
Contributor

Salvaged onto current main in #71632 with your commit and authorship intact — thanks for both the diagnosis and the fix.

Your read on #63352 was the key one: you spotted that the '/goal then nothing reaches the backend' symptom had a mechanism nobody had isolated, and correctly called it separate from #63376's stale-prop bug rather than a duplicate. That turned out to be right, and it was the last of three independent failures in the same flow:

  1. stale activeSessionId prop minting a second runtime session — fix(desktop): reuse slash-created runtime session after /goal #63376's payload, on main
  2. slash.ts / submit.ts resolving the target session differently — fix(desktop): slash commands target the user's chat, not a new session #71605
  3. kickoff dropped when busyRef is true — yours

Without your note that (3) was distinct, it would have been closed as fixed by the other two and the kickoff would still be vanishing mid-turn.

Two things about the salvage:

  • The branch predated both fix(desktop): slash commands target the user's chat, not a new session #71605 and the /compressaction migration, so slash.ts and desktop-slash-commands.test.ts conflicted. Resolved in favor of main plus your intent — the shared resolveTargetSessionId ladder is kept and now threads your preview argument through to createBackendSessionForSend, so the 'Untitled session' fix still works.
  • One refinement on top: the queue key now prefers the storedSessionId resolved at invocation time over re-reading $sessionStates / $selectedStoredSessionId inside the busy branch. A session switch between dispatch and that branch would otherwise park the kickoff on whichever chat is now in front (same cross-session leak class as [Desktop] Chat tab messages leak across sessions — cross-tab content mixing #59305). Your globals remain as the fallback for a session whose cache entry hasn't landed yet.

Your busy-queue test earned its keep, too — I sabotage-checked it by reverting the branch to the old drop-and-return behavior, and it failed alone and precisely. Closing this in favor of the salvage PR.

@teknium1 teknium1 closed this Jul 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/desktop Electron desktop app (apps/desktop/*) P3 Low — cosmetic, nice to have type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants