Skip to content

Add app-server thread Goal mode - #211

Open
Nul-led wants to merge 5 commits into
friuns2:mainfrom
Nul-led:agent/add-goal-mode
Open

Add app-server thread Goal mode#211
Nul-led wants to merge 5 commits into
friuns2:mainfrom
Nul-led:agent/add-goal-mode

Conversation

@Nul-led

@Nul-led Nul-led commented Jul 20, 2026

Copy link
Copy Markdown

Summary - add app-server-backed thread Goal state through thread/goal/get, thread/goal/set, and thread/goal/clear - support /goal <objective>, /goal, /goal edit <objective>, /goal pause, /goal resume, and /goal clear - make Goal mode discoverable with a slash-command picker, prefix filtering, keyboard navigation, syntax completion, and execution previews - show a responsive goal bar with status, optional token budget, inline editing, pause/resume, and clear controls - consume thread/goal/updated and thread/goal/cleared notifications without broad thread/message refreshes ## Protocol behavior thread/goal/set owns and starts the autonomous goal loop. CodexApp does not send a second turn/start, avoiding duplicate work. ## Performance audit - goal reads are cached per thread and concurrent reads are deduplicated - selecting a cached thread does not issue another goal read - goal notifications update the keyed local record directly and bypass the existing broad thread refresh path - slash discovery filters a fixed six-item local array and adds no requests, polling, or cache invalidation - no unbounded fanout, blocking loop, or large payload was added ## Verification - npm run test:unit -- --run src/utils/goalCommand.test.ts src/api/codexGateway.test.ts src/composables/useDesktopState.test.ts (54 passed) - npm run build:frontend (passed) - Playwright against 127.0.0.1:4173: desktop and 375x812 phone in light and dark themes; all six commands found, keyboard completion passed, no page errors or horizontal overflow - live app-server smoke: goal get/set/edit/clear and notification-backed UI updates The full unit suite has 153/155 passing on Windows. The two unrelated failures require unprivileged symlink creation and POSIX 0600 mode reporting, which this Windows environment does not provide. ## Manual coverage Added tests/chat-composer-rendering/thread-goal-mode.md and linked it from the manual test index.

Summary by CodeRabbit

Summary

  • New Features
    • Added a per-thread Goal bar with status/usage plus edit (save/cancel), pause/resume, and clear.
    • Enhanced /goal slash-command autocomplete with in-input previews and keyboard selection.
  • Bug Fixes
    • Improved goal state syncing and reduced unnecessary thread refreshes during updates.
    • Improved Windows absolute path handling for local-browse/edit links.
  • Documentation
    • Added manual test coverage for thread goal mode and Windows local-browse link behavior.
  • Tests
    • Added/extended automated tests for /goal parsing/suggestions, thread goal RPCs, and local-browse path decoding.
## Windows local browse follow-up - normalize `/C:/...` routes on Windows so agent-generated local file links resolve correctly - verify exact PowerShell and MP4 paths, including MP4 byte-range responses - propose the focused upstream fix separately in #212 ## Windows directory navigation follow-up - normalize generated Windows browse and edit links to canonical forward-slash routes such as `/codex-local-browse/C:/Users/...` - preserve Unix and UNC paths plus project-picker query encoding - verify real parent -> child -> parent folder navigation with Playwright Additional verification: - `npm run test:unit -- --run src/server/localBrowseUi.test.ts src/utils/goalCommand.test.ts` (11 passed) - `npm run build` (passed) - focused local-browse suite (6 passed)

Native Codex slash commands

  • expand the composer picker beyond Goal mode with /plan, /review, /compact, /model, /rename, /fork, and /archive
  • execute each command through the corresponding app-server or local UI action instead of sending slash text to the model
  • keep terminal-only CLI commands out of the web picker so every advertised command works on this surface
  • filter a fixed 13-item local list; no command-discovery request, polling, fanout, or cache invalidation is added

Additional verification:

  • focused Vitest: 61 passed
  • production build: passed
  • Playwright: desktop/phone, light/dark; all 13 commands, filtering, Tab completion, previews, no overflow, no page errors
  • full Vitest: 166/168; only the existing Windows symlink and POSIX mode assertions fail

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Thread goal support adds RPC access, slash-command parsing, per-thread synchronization, goal controls, and a thread-view status bar. Windows browse-path decoding and href normalization are also updated with automated and manual test coverage.

Changes

Thread goal mode

Layer / File(s) Summary
Goal types and RPC gateway
src/types/codex.ts, src/api/codexGateway.ts, src/api/codexGateway.test.ts
Defines thread-goal data and statuses, normalizes responses, and adds tested get/set/clear RPC wrappers.
Goal commands and composer interaction
src/utils/goalCommand.ts, src/utils/slashCommand.ts, src/components/content/ThreadComposer.vue, src/style.css
Adds goal and Codex command parsing, suggestions, previews, keyboard selection, and composer styling.
Goal state and command orchestration
src/composables/useDesktopState.ts, src/composables/useDesktopState.test.ts
Adds per-thread caching, command execution, new-thread handling, notifications, cleanup, polling, and composable controls.
Goal bar integration and verification
src/components/content/ThreadGoalBar.vue, src/App.vue, tests.md, tests/chat-composer-rendering/*
Adds goal display and editing controls, wires asynchronous actions, and documents manual verification.

Windows browse path handling

Layer / File(s) Summary
Windows browse-path normalization and verification
src/server/localBrowseUi.ts, src/server/localBrowseUi.test.ts, tests/chat-composer-rendering/index.md, tests/chat-composer-rendering/windows-absolute-file-links-open-local-browse.md
Normalizes Windows drive-letter paths in decoding and generated hrefs, with automated and manual coverage.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ThreadComposer
  participant useDesktopState
  participant codexGateway
  ThreadComposer->>useDesktopState: submit /goal command
  useDesktopState->>codexGateway: invoke thread goal RPC
  codexGateway-->>useDesktopState: return normalized goal
  codexGateway-->>useDesktopState: send goal notification
  useDesktopState-->>ThreadComposer: update selectedThreadGoal
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding app-server-backed thread Goal mode.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Nul-led

Nul-led commented Jul 20, 2026

Copy link
Copy Markdown
Author

/review

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jul 20, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (10) 📘 Rule violations (2) 📜 Skill insights (0)

Grey Divider


Action required

1. Missing TestChat browse-link screenshot 📘 Rule violation ☼ Reliability
Description
This PR changes browse-link/path decoding behavior but does not include the mandatory TestChat
validation artifact output/playwright/testchat--cjs.png. Without the required screenshot evidence,
browse/file-link rendering regressions can ship unnoticed.
Code

src/server/localBrowseUi.ts[R72-88]

+export function decodeBrowsePath(rawPath: string, platform: NodeJS.Platform = process.platform): string {
if (!rawPath) return ''
+  let decoded: string
try {
-    return decodeURIComponent(rawPath)
+    decoded = decodeURIComponent(rawPath)
} catch {
-    return rawPath
+    decoded = rawPath
}
+
+  // Browse URLs keep an absolute-path slash after the route prefix. On Windows,
+  // that turns `C:/path` into `/C:/path`, which Node resolves as `C:\C:\path`.
+  // Remove only that synthetic slash; Unix and UNC absolute paths stay intact.
+  if (platform === 'win32' && /^\/[A-Za-z]:[\\/]/u.test(decoded)) {
+    return decoded.slice(1)
+  }
+
+  return decoded
Evidence
The PR changes browse-route path decoding for Windows drive-letter paths, which falls under
browse-link handling changes that require TestChat validation and an
output/playwright/testchat--cjs.png screenshot artifact (PR Compliance ID 14). The added manual
test entry describes link checks but does not provide the mandated TestChat
hrefOk/titleOk/textOk validation + screenshot artifact.

AGENTS.md: TestChat Validation Is Mandatory for Chat Parsing/File-Link/Browse-Link Changes: AGENTS.md: TestChat Validation Is Mandatory for Chat Parsing/File-Link/Browse-Link Changes: AGENTS.md: TestChat Validation Is Mandatory for Chat Parsing/File-Link/Browse-Link Changes: AGENTS.md: TestChat Validation Is Mandatory for Chat Parsing/File-Link/Browse-Link Changes: AGENTS.md: TestChat Validation Is Mandatory for Chat Parsing/File-Link/Browse-Link Changes: AGENTS.md: TestChat Validation Is Mandatory for Chat Parsing/File-Link/Browse-Link Changes: AGENTS.md: TestChat Validation Is Mandatory for Chat Parsing/File-Link/Browse-Link Changes: AGENTS.md: TestChat Validation Is Mandatory for Chat Parsing/File-Link/Browse-Link Changes: AGENTS.md: TestChat Validation Is Mandatory for Chat Parsing/File-Link/Browse-Link Changes: AGENTS.md: TestChat Validation Is Mandatory for Chat Parsing/File-Link/Browse-Link Changes: AGENTS.md: TestChat Validation Is Mandatory for Chat Parsing/File-Link/Browse-Link Changes: AGENTS.md: TestChat Validation Is Mandatory for Chat Parsing/File-Link/Browse-Link Changes: AGENTS.md: TestChat Validation Is Mandatory for Chat Parsing/File-Link/Browse-Link Changes: AGENTS.md: TestChat Validation Is Mandatory for Chat Parsing/File-Link/Browse-Link Changes: AGENTS.md: TestChat Validation Is Mandatory for Chat Parsing/File-Link/Browse-Link Changes: AGENTS.md: TestChat Validation Is Mandatory for Chat Parsing/File-Link/Browse-Link Changes
src/server/localBrowseUi.ts[72-88]
tests/chat-composer-rendering/windows-absolute-file-links-open-local-browse.md[1-15]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR modifies browse-link handling (`decodeBrowsePath`) but does not include the required TestChat validation screenshot artifact under `output/playwright/testchat-<feature>-cjs.png`.
## Issue Context
Compliance requires a specific TestChat validation for chat parsing/file-link/browse-link changes: send a unique marker containing representative link content, verify the rendered row with `hrefOk`, `titleOk`, and `textOk`, and save the screenshot as `output/playwright/testchat-<feature>-cjs.png`.
## Fix Focus Areas
- src/server/localBrowseUi.ts[72-88]
- tests/chat-composer-rendering/windows-absolute-file-links-open-local-browse.md[1-15]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Windows browse href missing ✓ Resolved 🐞 Bug ≡ Correctness
Description
After decodeBrowsePath strips the synthetic leading slash on Windows drive paths, directory listings
can generate hrefs like /codex-local-browseC:/... (missing the / separator) because
toBrowseHref/toEditHref simply concatenates the route prefix with encodeURI(pathValue). These
malformed URLs won’t match the /codex-local-browse/*path and /codex-local-edit/*path routes,
breaking navigation/edit links in the local browse UI on Windows.
Code

src/server/localBrowseUi.ts[R81-86]

+  // Browse URLs keep an absolute-path slash after the route prefix. On Windows,
+  // that turns `C:/path` into `/C:/path`, which Node resolves as `C:\C:\path`.
+  // Remove only that synthetic slash; Unix and UNC absolute paths stay intact.
+  if (platform === 'win32' && /^\/[A-Za-z]:[\\/]/u.test(decoded)) {
+    return decoded.slice(1)
+  }
Evidence
The PR changes decodeBrowsePath to return Windows drive paths without the leading slash; directory
listings then use toBrowseHref(item.path) and toEditHref(item.path) which concatenate the route
prefix directly with encodeURI(pathValue), producing a URL without the / delimiter required by
the server’s /codex-local-browse/*path and /codex-local-edit/*path routes.

src/server/localBrowseUi.ts[72-89]
src/server/localBrowseUi.ts[144-154]
src/server/localBrowseUi.ts[253-269]
src/server/httpServer.ts[152-167]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`decodeBrowsePath()` now returns Windows drive paths without a leading slash (e.g. `C:/...`). But `toBrowseHref()` / `toEditHref()` build URLs via string concatenation: ``/codex-local-browse${encodeURI(pathValue)}``, which produces malformed URLs for drive-letter paths (e.g. `/codex-local-browseC:/...`). Those URLs do not match the Express routes declared as `/codex-local-browse/*path` and `/codex-local-edit/*path`.
## Issue Context
This is triggered when rendering `createDirectoryListingHtml()` (directory view) because it calls `toBrowseHref(item.path)` / `toEditHref(item.path)` where `item.path` is a filesystem path (`C:\...` / `C:/...`) without a leading `/`.
## Fix Focus Areas
- src/server/localBrowseUi.ts[72-154]
- src/server/localBrowseUi.ts[253-269]
### Suggested approach
- Normalize the path for URL composition, then ensure a leading `/` before concatenating with the route prefix.
- Recommended snippet:
- Convert backslashes to forward slashes for URL paths: `pathValue.replace(/\\/g, '/')`
- Ensure the encoded path starts with `/` (or `//` for UNC):
- `const encoded = encodeURI(normalizedPath)`
- `const suffix = encoded.startsWith('/') ? encoded : `/${encoded}``
- `return `/codex-local-browse${suffix}${query}``
- Apply the same logic to `toEditHref()`.
### Add/adjust tests
- Extend `localBrowseUi.test.ts` (or add a new test) to assert that `toBrowseHref('C:/Users/x.txt')` yields `/codex-local-browse/C:/Users/x.txt` (and similarly for backslash input).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Goal start hides thread 🐞 Bug ☼ Reliability
Description
In sendMessageToNewThread(), the /goal  path awaits updateThreadGoal() before inserting the
optimistic thread into the thread list; if setThreadGoal fails, updateThreadGoal rethrows and the UI
never inserts/selects the newly created thread. This can leave a server-created thread that the user
cannot navigate to from the UI to retry or clean up.
Code

src/composables/useDesktopState.ts[R5150-5163]

+      if (goalCommand?.action === 'set') {
+        await updateThreadGoal(threadId, { objective: goalCommand.objective, status: 'active' })
+        insertOptimisticThread(threadId, targetCwd, goalCommand.objective)
+        resumedThreadById.value = {
+          ...resumedThreadById.value,
+          [threadId]: true,
+        }
+        setSelectedThreadId(threadId)
+        pendingThreadsRefresh = true
+        pendingThreadsRefreshForce = true
+        await syncFromNotifications()
+        isSendingMessage.value = false
+        return threadId
+      }
Evidence
The goal-start branch inserts the thread only after updateThreadGoal(); since updateThreadGoal()
rethrows, any failure prevents insertOptimisticThread() from running. insertOptimisticThread()
is what adds the thread to sourceGroups (and thus projectGroups), so skipping it makes the new
thread not appear in the UI.

src/composables/useDesktopState.ts[5150-5163]
src/composables/useDesktopState.ts[1737-1749]
src/composables/useDesktopState.ts[2307-2345]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `/goal <objective>` Home flow creates a new thread, then calls `updateThreadGoal()`, and only after that inserts the thread into local state. If the goal RPC fails, `updateThreadGoal()` rethrows, so `insertOptimisticThread()` never runs and the new thread is invisible/unreachable in the UI.
## Issue Context
`insertOptimisticThread()` is the mechanism that makes newly created threads visible by updating `sourceGroups`/`projectGroups`. `updateThreadGoal()` explicitly rethrows on failure.
## Fix Focus Areas
- src/composables/useDesktopState.ts[5101-5218]
- src/composables/useDesktopState.ts[1737-1749]
- src/composables/useDesktopState.ts[2307-2345]
## Implementation notes
- After `startThread()` succeeds (and `threadId` is known), call `insertOptimisticThread(threadId, targetCwd, goalCommand.objective)` and `setSelectedThreadId(threadId)` **before** awaiting `updateThreadGoal()`.
- If `updateThreadGoal()` fails, keep the optimistic thread visible/selected and show the error; optionally mark it as not in-progress.
- Add/extend a unit test where `startThread` succeeds but `setThreadGoal` rejects, and assert the thread still exists in `sourceGroups` / is selectable.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
4. Unhandled goal command failures 🐞 Bug ☼ Reliability
Description
In sendMessageToSelectedThread, the new goal-command path awaits executeGoalCommand, but
executeGoalCommand can throw on RPC failures (notably /goal view and /goal clear) without reliably
setting the shared error state, and App.vue dispatches sendMessageToSelectedThread via void so the
rejection can become an unhandled promise rejection. This can leave users with a silently failed
goal command and noisy unhandled-rejection errors when the app-server is unavailable.
Code

src/composables/useDesktopState.ts[R1779-1789]

+  async function executeGoalCommand(threadId: string, command: GoalCommand): Promise<void> {
+    if (command.action === 'view') {
+      const goal = await loadThreadGoal(threadId, { force: true })
+      if (!goal) error.value = 'No goal is set for this thread. Use /goal <objective> to start one.'
+      return
+    }
+    if (command.action === 'clear') {
+      await clearThreadGoal(threadId)
+      storeThreadGoal(threadId, null)
+      error.value = ''
+      return
Evidence
App.vue submits messages by calling sendMessageToSelectedThread without awaiting or catching, while
the new goal-command branch in sendMessageToSelectedThread awaits executeGoalCommand which calls
RPCs that can reject (clear/view) and is not wrapped in a local try/catch that sets error state.

src/App.vue[3429-3445]
src/composables/useDesktopState.ts[4987-5008]
src/composables/useDesktopState.ts[1779-1789]
src/composables/useDesktopState.ts[1737-1749]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `/goal` slash-command path can throw and surface as an unhandled promise rejection because `sendMessageToSelectedThread()` awaits `executeGoalCommand()` but the caller often fire-and-forgets the returned promise (App.vue uses `void`). Additionally, `executeGoalCommand()` performs RPC calls (notably `view` via `loadThreadGoal()` and `clear` via `clearThreadGoal()`) without a try/catch that sets `error.value`, so failures may not be reflected in the UI.
### Issue Context
- Goal-bar button actions are wrapped in `runThreadGoalUpdate()` in App.vue which swallows errors, but slash commands go through `sendMessageToSelectedThread()`.
- `updateThreadGoal()` already centralizes try/catch + `error.value` behavior; `executeGoalCommand()` should behave similarly for all branches.
### Fix Focus Areas
- src/composables/useDesktopState.ts[1779-1815]
- src/composables/useDesktopState.ts[4987-5008]
- src/App.vue[3429-3445]
### Suggested change
- Wrap the `goalCommand` branch in `sendMessageToSelectedThread()` with try/catch (set `error.value` to a normalized message and return).
- In `executeGoalCommand()`:
- For `clear`, use the existing `clearSelectedThreadGoal()` logic pattern (or add try/catch around `clearThreadGoal()` that sets `error.value` then rethrows/returns).
- For `view`, catch `loadThreadGoal(..., { force: true })` failures and set `error.value` (and clear `error.value` on success so stale errors don’t persist).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

5. Slash enter case mismatch 🐞 Bug ≡ Correctness ⭐ New
Description
ThreadComposer.onInputKeydown blocks Enter for runnable slash commands typed with different casing
(e.g. "/GOAL pause") because it compares the canonical option insertText (lowercase) against the raw
draft string. This prevents the command from being submitted while the slash menu is open, despite
slash-command parsing being case-insensitive.
Code

src/components/content/ThreadComposer.vue[R1637-1646]

+    if (event.key === 'Enter') {
+      const selected = slashCommandSuggestions.value[slashCommandHighlightedIndex.value]
+      const normalizedDraft = draft.value.trim()
+      const exactOption = describeSlashCommand(normalizedDraft)
+      const isRunnableExact = exactOption?.requiresArgument === false
+        && exactOption.insertText.trim() === normalizedDraft
+      if (!isRunnableExact) {
+        event.preventDefault()
+        if (selected) applySlashCommandOption(selected)
+        return
Evidence
The composer decides whether Enter should submit by checking `exactOption.insertText.trim() ===
normalizedDraft` (case-sensitive) and prevents Enter otherwise. But goal/slash parsing is
case-insensitive (regex /iu + toLowerCase()), and tests confirm /GOAL pause is a valid
command; therefore the composer can incorrectly block submission for valid uppercase/mixed-case
commands.

src/components/content/ThreadComposer.vue[1637-1646]
src/utils/goalCommand.test.ts[10-14]
src/utils/slashCommand.ts[116-127]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Runnable slash commands entered with different casing (e.g. `/GOAL pause`, `/PLAN`) are parsed correctly, but `onInputKeydown` prevents `Enter` because it checks exact equality between `exactOption.insertText` (canonical lowercase) and the user’s `draft`.

### Issue Context
`describeSlashCommand()` / `parseSlashCommand()` are case-insensitive, and unit tests explicitly expect `/GOAL pause` to parse as a valid goal command. The composer’s “runnable exact command” check should therefore be case-insensitive as well.

### Fix Focus Areas
- src/components/content/ThreadComposer.vue[1637-1646]

### Implementation notes
- Update the runnable-exact comparison to use a normalized form on both sides (e.g. `toLowerCase()`), such as:
 - `exactOption.insertText.trim().toLowerCase() === normalizedDraft.toLowerCase()`
- Keep the existing `requiresArgument === false` guard.
- (Optional) Add a small unit test around composer key handling if there’s an existing test harness for it; otherwise, ensure existing slash parsing tests remain valid.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Goal read errors hidden 🐞 Bug ☼ Reliability
Description
useDesktopState triggers goal reads on thread selection and on polling startup but swallows
loadThreadGoal failures via .catch(() => {}), and loadThreadGoal itself doesn’t update the shared
error state on RPC failure. When thread/goal/get fails, the goal bar can disappear with no
user-visible error, making the UI look like “no goal exists” rather than “goal load failed.”
Code

src/composables/useDesktopState.ts[R1701-1705]

)
activeReasoningItemId = ''
shouldAutoScrollOnNextAgentEvent = false
+    if (nextThreadId) void loadThreadGoal(nextThreadId).catch(() => {})
+  }
Evidence
The new goal-load calls explicitly discard failures in both the thread-selection path and the
polling-start path, and the load function lacks any error-state update, unlike the update path which
sets error.value on failure.

src/composables/useDesktopState.ts[1691-1735]
src/composables/useDesktopState.ts[1737-1749]
src/composables/useDesktopState.ts[5697-5703]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Goal reads (`getThreadGoal`) are initiated automatically (thread selection + polling startup) but failures are swallowed and never populate the shared desktop `error` state. This makes goal state appear missing when the app-server is unreachable/errored.
## Issue Context
- `setSelectedThreadId()` calls `loadThreadGoal(...).catch(() => {})`.
- `startPolling()` calls `loadThreadGoal(...).catch(() => {})`.
- `loadThreadGoal()` does not `try/catch` to set `error.value` on rejection (contrast with `updateThreadGoal()` which does).
## Fix Focus Areas
- src/composables/useDesktopState.ts[1691-1735]
- src/composables/useDesktopState.ts[1737-1749]
- src/composables/useDesktopState.ts[5697-5703]
### Suggested approach
Pick one (consistent) strategy:
1) Handle errors inside `loadThreadGoal()`:
- wrap `await getThreadGoal(...)` in `try/catch`
- on error, set `error.value` (e.g. `Failed to load goal`) and rethrow (or return cached value if you prefer)
OR
2) Keep `loadThreadGoal()` pure but don’t swallow errors at call sites:
- replace `.catch(() => {})` with `.catch((e) => { error.value = ... })`
- optionally guard so you only set error if the failing thread is still selected.
### Add/adjust tests
- Add a unit test where `getThreadGoal` rejects during thread selection and assert `state.error.value` (or equivalent exposed error signal) is updated.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Goal commands block tool reply 🐞 Bug ≡ Correctness
Description
In sendMessageToSelectedThread, valid /goal commands are executed before checking
maybeReplyToPendingUserInputRequest, so a user cannot answer an active item/tool/requestUserInput
prompt with a message that parses as a goal command (it will run the goal RPC instead). This can
prevent the pending tool request from ever receiving the intended reply.
Code

src/composables/useDesktopState.ts[R5002-5008]

+    const goalCommand = imageUrls.length === 0 && skills.length === 0 && fileAttachments.length === 0
+      ? parseGoalCommand(nextText)
+      : null
+    if (goalCommand) {
+      await executeGoalCommand(threadId, goalCommand)
+      return
+    }
Evidence
The function returns early to execute a goal command before it checks the existing
pending-user-input reply path, so goal-command strings can no longer be used as replies when a tool
input request is active.

src/composables/useDesktopState.ts[4963-4985]
src/composables/useDesktopState.ts[4987-5012]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`sendMessageToSelectedThread` executes parsed `/goal` commands before attempting to satisfy a pending `item/tool/requestUserInput` request via `maybeReplyToPendingUserInputRequest`. This causes misrouting: a valid goal command string cannot be used as the answer to a pending tool input request.
### Issue Context
Tool user-input replies are correctness-critical; command parsing should not preempt responding to a pending tool request.
### Fix Focus Areas
- Ensure `maybeReplyToPendingUserInputRequest(...)` is evaluated *before* `/goal` command parsing/execution, **or** gate goal-command parsing behind a check that there is no eligible pending user-input request.
- file/path[start_line-end_line]
- src/composables/useDesktopState.ts[4987-5012]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (3)
8. Goal updates block other threads 🐞 Bug ☼ Reliability
Description
App.vue uses a single global isUpdatingThreadGoal flag and runThreadGoalUpdate returns early when it
is set; if a goal update is in-flight and the user switches threads, the newly selected thread’s
goal bar stays disabled and new goal actions are dropped. This makes goal controls appear
unresponsive during cross-thread navigation.
Code

src/App.vue[R3447-3450]

+async function runThreadGoalUpdate(update: () => Promise<void>): Promise<void> {
+  if (isUpdatingThreadGoal.value) return
+  isUpdatingThreadGoal.value = true
+  try {
Evidence
The disabled state is driven by a single ref, and runThreadGoalUpdate short-circuits globally; this
couples all threads’ goal actions to one in-flight flag.

src/App.vue[1520-1523]
src/App.vue[1015-1022]
src/App.vue[3447-3456]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`isUpdatingThreadGoal` is global in App.vue and used to disable the goal bar. While an update is in-flight for thread A, switching to thread B keeps the bar disabled and `runThreadGoalUpdate` drops new updates (`return`), blocking valid actions on thread B.
### Issue Context
Goal updates are thread-scoped, but the in-flight lock is app-scoped.
### Fix Focus Areas
- Track in-flight goal updates per-thread (e.g., `updatingThreadGoalById: Record<string, boolean>` or `updatingThreadGoalId: string | null`).
- Disable the goal bar only when the selected thread is the one being updated.
- Only suppress duplicate updates for the same thread, not all threads.
- file/path[start_line-end_line]
- src/App.vue[1520-1523]
- src/App.vue[1015-1022]
- src/App.vue[3447-3456]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


9. Dark overrides in component scope 📘 Rule violation ⚙ Maintainability
Description
ThreadGoalBar.vue adds dark-theme overrides using component-scoped :global(:root.dark ...)
selectors instead of placing decisive shared-surface overrides in src/style.css. This can fragment
theme behavior and violates the requirement to centralize dark-theme overrides for shared/large UI
surfaces.
Code

src/components/content/ThreadGoalBar.vue[R241-279]

+:global(:root.dark .thread-goal-bar) {
+  border-color: rgb(245 158 11 / 18%);
+  background: rgb(120 53 15 / 13%);
+  color: #d4d4d8;
+}
+
+:global(:root.dark .thread-goal-heading),
+:global(:root.dark .thread-goal-action) {
+  color: #a1a1aa;
+}
+
+:global(:root.dark .thread-goal-heading strong),
+:global(:root.dark .thread-goal-action.is-primary),
+:global(:root.dark .thread-goal-action:hover:not(:disabled)) {
+  color: #fbbf24;
+}
+
+:global(:root.dark .thread-goal-objective),
+:global(:root.dark .thread-goal-input) {
+  color: #f4f4f5;
+}
+
+:global(:root.dark .thread-goal-input) {
+  border-color: rgb(245 158 11 / 25%);
+  background: rgb(24 24 27 / 70%);
+}
+
+:global(:root.dark .thread-goal-bar.is-paused),
+:global(:root.dark .thread-goal-bar.is-blocked),
+:global(:root.dark .thread-goal-bar.is-usageLimited),
+:global(:root.dark .thread-goal-bar.is-budgetLimited) {
+  border-color: rgb(161 161 170 / 16%);
+  background: rgb(39 39 42 / 70%);
+}
+
+:global(:root.dark .thread-goal-bar.is-complete) {
+  border-color: rgb(74 222 128 / 18%);
+  background: rgb(20 83 45 / 20%);
+}
Evidence
PR Compliance ID 5 requires decisive dark-theme overrides for shared/large UI surfaces to be
implemented in src/style.css. The new ThreadGoalBar instead defines multiple dark-theme rules in
component-scoped CSS using :global(:root.dark ...), indicating the overrides are not centralized
globally.

AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css: AGENTS.md: Shared/Large UI Surfaces Must Prefer Dark-Theme Overrides in src/style.css
src/components/content/ThreadGoalBar.vue[241-279]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new Thread Goal bar implements dark-theme styling via component-scoped `:global(:root.dark ...)` blocks, but the compliance rule requires decisive dark-theme overrides for shared/large UI surfaces to live in `src/style.css`.
## Issue Context
`ThreadGoalBar` renders in the main thread UI (a shared surface), and its dark theme behavior is currently defined inside `src/components/content/ThreadGoalBar.vue`.
## Fix Focus Areas
- src/components/content/ThreadGoalBar.vue[241-279]
- src/style.css[1-1]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Null goals cached indefinitely 🐞 Bug ≡ Correctness
Description
storeThreadGoal marks a thread as "loaded" even when its goal is null, so subsequent loadThreadGoal
calls return cached null and will not refetch unless forced. If goal state changes while the
notification stream is disconnected (missed events) or across polling restarts, the UI can remain
stuck showing no goal until a manual forced refresh (e.g., /goal view) or a full state reset.
Code

src/composables/useDesktopState.ts[R1707-1720]

+  function storeThreadGoal(threadId: string, goal: UiThreadGoal | null): void {
+    threadGoalByThreadId.value = {
+      ...threadGoalByThreadId.value,
+      [threadId]: goal,
+    }
+    loadedThreadGoalIds.add(threadId)
+  }
+
+  async function loadThreadGoal(threadId: string, options: { force?: boolean } = {}): Promise<UiThreadGoal | null> {
+    const normalizedThreadId = threadId.trim()
+    if (!normalizedThreadId) return null
+    if (!options.force && loadedThreadGoalIds.has(normalizedThreadId)) {
+      return threadGoalByThreadId.value[normalizedThreadId] ?? null
+    }
Evidence
The cache key set is updated for both null and non-null results, and loadThreadGoal returns early
when the id is present; startPolling triggers a non-forced load, so a cached null will be reused
instead of refetching.

src/composables/useDesktopState.ts[1707-1720]
src/composables/useDesktopState.ts[1715-1735]
src/composables/useDesktopState.ts[5697-5703]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The goal cache treats `null` (no goal) as a permanently loaded result by adding the thread to `loadedThreadGoalIds`. This prevents re-fetching on later selections and on `startPolling()` unless the call is forced, so a missed notification (offline/reconnect) can leave the goal bar stale.
### Issue Context
- `startPolling()` calls `loadThreadGoal(selectedThreadId)` without `{ force: true }`.
- `loadThreadGoal()` returns cached values when the thread id is present in `loadedThreadGoalIds`, regardless of whether the cached value is `null`.
### Fix Focus Areas
- src/composables/useDesktopState.ts[1707-1735]
- src/composables/useDesktopState.ts[5697-5712]
### Suggested change
Implement one of:
- Don’t add `threadId` to `loadedThreadGoalIds` when `goal === null` (so absence is revalidated later), or
- Track a timestamp/TTL for `null` entries and refetch after a short interval, or
- On polling start/reconnect (`startPolling()` / `ready`), force-refresh the selected thread goal: `loadThreadGoal(selectedThreadId.value, { force: true })`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

11. Token label rounds wrong 🐞 Bug ≡ Correctness
Description
ThreadGoalBar.formatTokens() can render values just below 1,000,000 as "1000k" (e.g., 999,950 →
1000k) even though ≥1,000,000 switches to "m", producing inconsistent usage labels around the
threshold.
Code

src/components/content/ThreadGoalBar.vue[R77-80]

+function formatTokens(value: number): string {
+  if (value < 1000) return String(value)
+  if (value < 1_000_000) return `${Math.round(value / 100) / 10}k`
+  return `${Math.round(value / 100_000) / 10}m`
Evidence
The component uses formatTokens() to build the on-screen usage string; the current rounding logic
can produce "1000k" for values that are still in the < 1_000_000 branch.

src/components/content/ThreadGoalBar.vue[68-81]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`formatTokens()` can round thousands to `1000k` for values just below one million, which is inconsistent with the `m` branch and misleading in the goal usage label.
### Issue Context
This affects `usageLabel`, which is shown whenever `goal.tokenBudget !== null`.
### Fix Focus Areas
- src/components/content/ThreadGoalBar.vue[68-81]
### Suggested fix
Compute the display unit based on the *rounded* value (or compute in `k` first and promote to `m` when the rounded `k` value reaches 1000). For example:
- Compute `k = value / 1000`.
- If `k < 999.95`, show `${round1(k)}k`.
- Else show `${round1(value / 1_000_000)}m`.
(Optionally add a small unit test for boundary cases like `999950`, `999999`, and `1_000_000`.)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


12. Goal view keeps stale error 🐞 Bug ≡ Correctness
Description
executeGoalCommand(action='view') sets the shared error state only when no goal exists, but never
clears it on a successful goal load, so prior desktopError messages can remain visible after a
successful /goal view. This leaves the UI showing an error even though the operation succeeded.
Code

src/composables/useDesktopState.ts[R1779-1784]

+  async function executeGoalCommand(threadId: string, command: GoalCommand): Promise<void> {
+    if (command.action === 'view') {
+      const goal = await loadThreadGoal(threadId, { force: true })
+      if (!goal) error.value = 'No goal is set for this thread. Use /goal <objective> to start one.'
+      return
+    }
Evidence
The view branch only sets an error on the no-goal path and returns without clearing it on success,
while other successful goal updates do clear error and App.vue surfaces desktopError as a visible
feedback error.

src/composables/useDesktopState.ts[1779-1784]
src/composables/useDesktopState.ts[1737-1746]
src/App.vue[1706-1719]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In `executeGoalCommand`, the `view` branch returns without clearing `error.value` when a goal is successfully loaded, so a previously-set desktop error can remain visible.
### Issue Context
Other successful goal lifecycle operations clear `error.value`, and App.vue includes `desktopError` in `visibleFeedbackErrors`, so stale errors remain user-visible.
### Fix Focus Areas
- After `loadThreadGoal(..., { force: true })`, if `goal` is non-null, set `error.value = ''`.
- Do not clear the error before the RPC completes; only clear on confirmed success.
- file/path[start_line-end_line]
- src/composables/useDesktopState.ts[1779-1784]
- src/composables/useDesktopState.ts[1737-1746]
- src/App.vue[1706-1719]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


13. Goal notification id mismatch 🐞 Bug ≡ Correctness
Description
The thread/goal/updated notification handler stores the normalized goal under params.threadId
without verifying it matches goal.threadId, so a malformed notification can associate a goal with
the wrong thread in the local cache. This can show or mutate the wrong goal for a selected thread if
the server ever emits inconsistent payloads.
Code

src/composables/useDesktopState.ts[R3884-3889]

+    if (notification.method === 'thread/goal/updated') {
+      const params = asRecord(notification.params)
+      const threadId = readString(params?.threadId)
+      const goal = normalizeThreadGoal(params?.goal)
+      if (threadId && goal) storeThreadGoal(threadId, goal)
+      return
Evidence
The notification handler chooses the cache key from params.threadId, while normalizeThreadGoal
returns an object that includes its own threadId; without a consistency check, mismatches can
corrupt the per-thread goal map.

src/composables/useDesktopState.ts[3884-3889]
src/api/codexGateway.ts[569-588]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
On `thread/goal/updated`, the handler uses `params.threadId` as the cache key, ignoring `goal.threadId` returned by `normalizeThreadGoal()`. If the notification payload is inconsistent, the app can store the goal under the wrong thread id.
### Issue Context
`normalizeThreadGoal()` already parses and returns a `UiThreadGoal` containing `threadId`.
### Fix Focus Areas
- src/composables/useDesktopState.ts[3884-3889]
- src/api/codexGateway.ts[569-588]
### Suggested change
Before calling `storeThreadGoal(threadId, goal)`:
- Require `threadId === goal.threadId` (else ignore + optionally log), or
- Prefer `storeThreadGoal(goal.threadId, goal)` and only accept notifications where `params.threadId` is missing/consistent.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines +241 to +279
:global(:root.dark .thread-goal-bar) {
border-color: rgb(245 158 11 / 18%);
background: rgb(120 53 15 / 13%);
color: #d4d4d8;
}

:global(:root.dark .thread-goal-heading),
:global(:root.dark .thread-goal-action) {
color: #a1a1aa;
}

:global(:root.dark .thread-goal-heading strong),
:global(:root.dark .thread-goal-action.is-primary),
:global(:root.dark .thread-goal-action:hover:not(:disabled)) {
color: #fbbf24;
}

:global(:root.dark .thread-goal-objective),
:global(:root.dark .thread-goal-input) {
color: #f4f4f5;
}

:global(:root.dark .thread-goal-input) {
border-color: rgb(245 158 11 / 25%);
background: rgb(24 24 27 / 70%);
}

:global(:root.dark .thread-goal-bar.is-paused),
:global(:root.dark .thread-goal-bar.is-blocked),
:global(:root.dark .thread-goal-bar.is-usageLimited),
:global(:root.dark .thread-goal-bar.is-budgetLimited) {
border-color: rgb(161 161 170 / 16%);
background: rgb(39 39 42 / 70%);
}

:global(:root.dark .thread-goal-bar.is-complete) {
border-color: rgb(74 222 128 / 18%);
background: rgb(20 83 45 / 20%);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Dark overrides in component scope 📘 Rule violation ⚙ Maintainability

ThreadGoalBar.vue adds dark-theme overrides using component-scoped :global(:root.dark ...)
selectors instead of placing decisive shared-surface overrides in src/style.css. This can fragment
theme behavior and violates the requirement to centralize dark-theme overrides for shared/large UI
surfaces.
Agent Prompt
## Issue description
The new Thread Goal bar implements dark-theme styling via component-scoped `:global(:root.dark ...)` blocks, but the compliance rule requires decisive dark-theme overrides for shared/large UI surfaces to live in `src/style.css`.

## Issue Context
`ThreadGoalBar` renders in the main thread UI (a shared surface), and its dark theme behavior is currently defined inside `src/components/content/ThreadGoalBar.vue`.

## Fix Focus Areas
- src/components/content/ThreadGoalBar.vue[241-279]
- src/style.css[1-1]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +1779 to +1789
async function executeGoalCommand(threadId: string, command: GoalCommand): Promise<void> {
if (command.action === 'view') {
const goal = await loadThreadGoal(threadId, { force: true })
if (!goal) error.value = 'No goal is set for this thread. Use /goal <objective> to start one.'
return
}
if (command.action === 'clear') {
await clearThreadGoal(threadId)
storeThreadGoal(threadId, null)
error.value = ''
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Unhandled goal command failures 🐞 Bug ☼ Reliability

In sendMessageToSelectedThread, the new goal-command path awaits executeGoalCommand, but
executeGoalCommand can throw on RPC failures (notably /goal view and /goal clear) without reliably
setting the shared error state, and App.vue dispatches sendMessageToSelectedThread via void so the
rejection can become an unhandled promise rejection. This can leave users with a silently failed
goal command and noisy unhandled-rejection errors when the app-server is unavailable.
Agent Prompt
### Issue description
The `/goal` slash-command path can throw and surface as an unhandled promise rejection because `sendMessageToSelectedThread()` awaits `executeGoalCommand()` but the caller often fire-and-forgets the returned promise (App.vue uses `void`). Additionally, `executeGoalCommand()` performs RPC calls (notably `view` via `loadThreadGoal()` and `clear` via `clearThreadGoal()`) without a try/catch that sets `error.value`, so failures may not be reflected in the UI.

### Issue Context
- Goal-bar button actions are wrapped in `runThreadGoalUpdate()` in App.vue which swallows errors, but slash commands go through `sendMessageToSelectedThread()`.
- `updateThreadGoal()` already centralizes try/catch + `error.value` behavior; `executeGoalCommand()` should behave similarly for all branches.

### Fix Focus Areas
- src/composables/useDesktopState.ts[1779-1815]
- src/composables/useDesktopState.ts[4987-5008]
- src/App.vue[3429-3445]

### Suggested change
- Wrap the `goalCommand` branch in `sendMessageToSelectedThread()` with try/catch (set `error.value` to a normalized message and return).
- In `executeGoalCommand()`:
  - For `clear`, use the existing `clearSelectedThreadGoal()` logic pattern (or add try/catch around `clearThreadGoal()` that sets `error.value` then rethrows/returns).
  - For `view`, catch `loadThreadGoal(..., { force: true })` failures and set `error.value` (and clear `error.value` on success so stale errors don’t persist).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +1707 to +1720
function storeThreadGoal(threadId: string, goal: UiThreadGoal | null): void {
threadGoalByThreadId.value = {
...threadGoalByThreadId.value,
[threadId]: goal,
}
loadedThreadGoalIds.add(threadId)
}

async function loadThreadGoal(threadId: string, options: { force?: boolean } = {}): Promise<UiThreadGoal | null> {
const normalizedThreadId = threadId.trim()
if (!normalizedThreadId) return null
if (!options.force && loadedThreadGoalIds.has(normalizedThreadId)) {
return threadGoalByThreadId.value[normalizedThreadId] ?? null
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Null goals cached indefinitely 🐞 Bug ≡ Correctness

storeThreadGoal marks a thread as "loaded" even when its goal is null, so subsequent loadThreadGoal
calls return cached null and will not refetch unless forced. If goal state changes while the
notification stream is disconnected (missed events) or across polling restarts, the UI can remain
stuck showing no goal until a manual forced refresh (e.g., /goal view) or a full state reset.
Agent Prompt
### Issue description
The goal cache treats `null` (no goal) as a permanently loaded result by adding the thread to `loadedThreadGoalIds`. This prevents re-fetching on later selections and on `startPolling()` unless the call is forced, so a missed notification (offline/reconnect) can leave the goal bar stale.

### Issue Context
- `startPolling()` calls `loadThreadGoal(selectedThreadId)` without `{ force: true }`.
- `loadThreadGoal()` returns cached values when the thread id is present in `loadedThreadGoalIds`, regardless of whether the cached value is `null`.

### Fix Focus Areas
- src/composables/useDesktopState.ts[1707-1735]
- src/composables/useDesktopState.ts[5697-5712]

### Suggested change
Implement one of:
- Don’t add `threadId` to `loadedThreadGoalIds` when `goal === null` (so absence is revalidated later), or
- Track a timestamp/TTL for `null` entries and refetch after a short interval, or
- On polling start/reconnect (`startPolling()` / `ready`), force-refresh the selected thread goal: `loadThreadGoal(selectedThreadId.value, { force: true })`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +3884 to +3889
if (notification.method === 'thread/goal/updated') {
const params = asRecord(notification.params)
const threadId = readString(params?.threadId)
const goal = normalizeThreadGoal(params?.goal)
if (threadId && goal) storeThreadGoal(threadId, goal)
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Informational

4. Goal notification id mismatch 🐞 Bug ≡ Correctness

The thread/goal/updated notification handler stores the normalized goal under params.threadId
without verifying it matches goal.threadId, so a malformed notification can associate a goal with
the wrong thread in the local cache. This can show or mutate the wrong goal for a selected thread if
the server ever emits inconsistent payloads.
Agent Prompt
### Issue description
On `thread/goal/updated`, the handler uses `params.threadId` as the cache key, ignoring `goal.threadId` returned by `normalizeThreadGoal()`. If the notification payload is inconsistent, the app can store the goal under the wrong thread id.

### Issue Context
`normalizeThreadGoal()` already parses and returns a `UiThreadGoal` containing `threadId`.

### Fix Focus Areas
- src/composables/useDesktopState.ts[3884-3889]
- src/api/codexGateway.ts[569-588]

### Suggested change
Before calling `storeThreadGoal(threadId, goal)`:
- Require `threadId === goal.threadId` (else ignore + optionally log), or
- Prefer `storeThreadGoal(goal.threadId, goal)` and only accept notifications where `params.threadId` is missing/consistent.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@Nul-led

Nul-led commented Jul 20, 2026

Copy link
Copy Markdown
Author

/review

Comment thread src/composables/useDesktopState.ts Outdated
Comment on lines +5002 to +5008
const goalCommand = imageUrls.length === 0 && skills.length === 0 && fileAttachments.length === 0
? parseGoalCommand(nextText)
: null
if (goalCommand) {
await executeGoalCommand(threadId, goalCommand)
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Goal commands block tool reply 🐞 Bug ≡ Correctness

In sendMessageToSelectedThread, valid /goal commands are executed before checking
maybeReplyToPendingUserInputRequest, so a user cannot answer an active item/tool/requestUserInput
prompt with a message that parses as a goal command (it will run the goal RPC instead). This can
prevent the pending tool request from ever receiving the intended reply.
Agent Prompt
### Issue description
`sendMessageToSelectedThread` executes parsed `/goal` commands before attempting to satisfy a pending `item/tool/requestUserInput` request via `maybeReplyToPendingUserInputRequest`. This causes misrouting: a valid goal command string cannot be used as the answer to a pending tool input request.

### Issue Context
Tool user-input replies are correctness-critical; command parsing should not preempt responding to a pending tool request.

### Fix Focus Areas
- Ensure `maybeReplyToPendingUserInputRequest(...)` is evaluated *before* `/goal` command parsing/execution, **or** gate goal-command parsing behind a check that there is no eligible pending user-input request.
- file/path[start_line-end_line]
  - src/composables/useDesktopState.ts[4987-5012]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +1779 to +1784
async function executeGoalCommand(threadId: string, command: GoalCommand): Promise<void> {
if (command.action === 'view') {
const goal = await loadThreadGoal(threadId, { force: true })
if (!goal) error.value = 'No goal is set for this thread. Use /goal <objective> to start one.'
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Informational

2. Goal view keeps stale error 🐞 Bug ≡ Correctness

executeGoalCommand(action='view') sets the shared error state only when no goal exists, but never
clears it on a successful goal load, so prior desktopError messages can remain visible after a
successful /goal view. This leaves the UI showing an error even though the operation succeeded.
Agent Prompt
### Issue description
In `executeGoalCommand`, the `view` branch returns without clearing `error.value` when a goal is successfully loaded, so a previously-set desktop error can remain visible.

### Issue Context
Other successful goal lifecycle operations clear `error.value`, and App.vue includes `desktopError` in `visibleFeedbackErrors`, so stale errors remain user-visible.

### Fix Focus Areas
- After `loadThreadGoal(..., { force: true })`, if `goal` is non-null, set `error.value = ''`.
- Do not clear the error before the RPC completes; only clear on confirmed success.
- file/path[start_line-end_line]
  - src/composables/useDesktopState.ts[1779-1784]
  - src/composables/useDesktopState.ts[1737-1746]
  - src/App.vue[1706-1719]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/App.vue
Comment on lines +3447 to +3450
async function runThreadGoalUpdate(update: () => Promise<void>): Promise<void> {
if (isUpdatingThreadGoal.value) return
isUpdatingThreadGoal.value = true
try {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Goal updates block other threads 🐞 Bug ☼ Reliability

App.vue uses a single global isUpdatingThreadGoal flag and runThreadGoalUpdate returns early when it
is set; if a goal update is in-flight and the user switches threads, the newly selected thread’s
goal bar stays disabled and new goal actions are dropped. This makes goal controls appear
unresponsive during cross-thread navigation.
Agent Prompt
### Issue description
`isUpdatingThreadGoal` is global in App.vue and used to disable the goal bar. While an update is in-flight for thread A, switching to thread B keeps the bar disabled and `runThreadGoalUpdate` drops new updates (`return`), blocking valid actions on thread B.

### Issue Context
Goal updates are thread-scoped, but the in-flight lock is app-scoped.

### Fix Focus Areas
- Track in-flight goal updates per-thread (e.g., `updatingThreadGoalById: Record<string, boolean>` or `updatingThreadGoalId: string | null`).
- Disable the goal bar only when the selected thread is the one being updated.
- Only suppress duplicate updates for the same thread, not all threads.
- file/path[start_line-end_line]
  - src/App.vue[1520-1523]
  - src/App.vue[1015-1022]
  - src/App.vue[3447-3456]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 8bdef19

@Nul-led
Nul-led marked this pull request as ready for review July 20, 2026 13:19
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add app-server-backed thread Goal mode with slash commands and goal bar

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add app-server RPC-backed per-thread goal state with targeted notification updates.
• Introduce /goal slash commands with picker, previews, and keyboard completion.
• Render an inline goal bar with edit/pause/resume/clear controls and tests/docs.
Diagram

graph TD
  TC["ThreadComposer"] --> GC["goalCommand utils"] --> DS(["useDesktopState"]) --> GW["codexGateway RPC"] --> AS{{"Codex app-server"}} --> DS --> AV["App.vue"] --> GB["ThreadGoalBar"]

  subgraph Legend
    direction LR
    _ui["Vue component"] ~~~ _state(["State/composable"]) ~~~ _ext{{"External service"}}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Build a generic slash-command framework (not goal-specific)
  • ➕ Reusable for future commands beyond /goal (e.g., /model, /prompt, /rate-limit).
  • ➕ Could unify picker UI/behavior with other composer menus.
  • ➖ Bigger scope and more architectural churn for a single feature.
  • ➖ Harder to land incrementally; higher review/maintenance burden initially.
2. Treat /goal as a normal message and let the server intercept it
  • ➕ Less client logic: no command parsing/execution branches in state layer.
  • ➕ Simplifies analytics/audit by keeping commands in the transcript.
  • ➖ Pollutes chat history with control messages users don’t expect to persist.
  • ➖ Harder to guarantee “no duplicate turn/start” semantics and UI responsiveness.

Recommendation: Keep the current approach: explicit client-side command handling with server-authoritative goal lifecycle. It preserves clean transcripts, enables immediate UI updates, and avoids broad refresh paths; the goal-specific picker can be generalized later if more slash commands emerge.

Files changed (14) +1114 / -4

Enhancement (7) +866 / -2
App.vueRender ThreadGoalBar and wire goal lifecycle actions +39/-0

Render ThreadGoalBar and wire goal lifecycle actions

• Adds the goal bar above the composer when a selected thread has a goal. Wires edit/pause/resume/clear handlers and guards updates with a local in-flight flag to prevent concurrent mutations.

src/App.vue

codexGateway.tsAdd thread goal RPC APIs with runtime normalization +53/-0

Add thread goal RPC APIs with runtime normalization

• Defines goal status validation and adds normalizeThreadGoal plus getThreadGoal/setThreadGoal/clearThreadGoal RPC functions. Enforces server response shape by rejecting invalid goal payloads on set.

src/api/codexGateway.ts

ThreadComposer.vueAdd /goal slash-command picker, previews, and keyboard controls +161/-0

Add /goal slash-command picker, previews, and keyboard controls

• Adds a slash-command suggestion menu for goal commands with prefix filtering, arrow navigation, Tab completion, Escape dismissal, and an execution preview when the command is runnable. Includes styling for the new menu/preview and resets highlight/dismissal state on draft/thread changes.

src/components/content/ThreadComposer.vue

ThreadGoalBar.vueNew goal bar UI with edit/pause/resume/clear controls +303/-0

New goal bar UI with edit/pause/resume/clear controls

• Introduces a responsive goal bar component that displays objective, status, optional token budget usage, and provides inline editing with save/cancel. Exposes events for pause/resume and clear, and includes light/dark + mobile styling.

src/components/content/ThreadGoalBar.vue

useDesktopState.tsAdd per-thread goal cache, command execution, and notification handling +189/-2

Add per-thread goal cache, command execution, and notification handling

• Introduces per-thread goal storage with loaded flags and in-flight load deduplication, exposes selectedThreadGoal plus goal mutation actions, and parses /goal messages to run goal lifecycle operations without sending a chat message. Handles thread/goal updated/cleared notifications by directly updating keyed goal state and explicitly bypasses the broad refresh path for these events.

src/composables/useDesktopState.ts

codex.tsAdd thread goal types and status enum +13/-0

Add thread goal types and status enum

• Defines ThreadGoalStatus and UiThreadGoal to represent app-server goal state, including status, objective, token budget, and timestamps.

src/types/codex.ts

goalCommand.tsAdd /goal parser, suggestions, and preview metadata +108/-0

Add /goal parser, suggestions, and preview metadata

• Implements a small goal command catalog plus helpers to filter suggestions by typed prefix, generate an execution preview for runnable commands, and parse /goal subcommands with conservative fallbacks (treating ambiguous arguments as objectives).

src/utils/goalCommand.ts

Tests (3) +163 / -1
codexGateway.test.tsAdd RPC coverage for thread goal get/set/clear +49/-1

Add RPC coverage for thread goal get/set/clear

• Introduces unit tests verifying method names, params, and basic response normalization expectations for thread goal RPCs.

src/api/codexGateway.test.ts

useDesktopState.test.tsAdd state tests for goal caching, notifications, and /goal execution +63/-0

Add state tests for goal caching, notifications, and /goal execution

• Extends mocks and adds tests verifying goals load once per selected thread, notifications update local goal state without broad thread refresh, and /goal start flows avoid calling startThreadTurn (app-server owns the loop).

src/composables/useDesktopState.test.ts

goalCommand.test.tsAdd tests for /goal parsing and discovery behavior +51/-0

Add tests for /goal parsing and discovery behavior

• Covers parsing for lifecycle commands and objectives (including multiline), ignores near-matches, and verifies suggestion filtering plus execution preview transitions.

src/utils/goalCommand.test.ts

Documentation (3) +38 / -1
tests.mdUpdate manual test index to include goals +1/-1

Update manual test index to include goals

• Increments the Chat Composer and Message Rendering section count and mentions goal coverage in the index summary.

tests.md

index.mdLink new thread goal mode manual test +1/-0

Link new thread goal mode manual test

• Adds a new entry for the thread goal mode manual test document.

tests/chat-composer-rendering/index.md

thread-goal-mode.mdAdd manual test plan for goal mode +36/-0

Add manual test plan for goal mode

• Documents prerequisites, step-by-step verification, expected results, and cleanup for goal commands, picker behavior, goal bar responsiveness, and notification-driven updates.

tests/chat-composer-rendering/thread-goal-mode.md

Other (1) +47 / -0
style.cssAdd dark-theme styles for slash command menu and preview +47/-0

Add dark-theme styles for slash command menu and preview

• Defines dark-mode Tailwind utility overrides for the new slash picker and command preview to ensure contrast and consistent hover/active states.

src/style.css

Comment on lines +5150 to +5163
if (goalCommand?.action === 'set') {
await updateThreadGoal(threadId, { objective: goalCommand.objective, status: 'active' })
insertOptimisticThread(threadId, targetCwd, goalCommand.objective)
resumedThreadById.value = {
...resumedThreadById.value,
[threadId]: true,
}
setSelectedThreadId(threadId)
pendingThreadsRefresh = true
pendingThreadsRefreshForce = true
await syncFromNotifications()
isSendingMessage.value = false
return threadId
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Goal start hides thread 🐞 Bug ☼ Reliability

In sendMessageToNewThread(), the /goal <objective> path awaits updateThreadGoal() before inserting
the optimistic thread into the thread list; if setThreadGoal fails, updateThreadGoal rethrows and
the UI never inserts/selects the newly created thread. This can leave a server-created thread that
the user cannot navigate to from the UI to retry or clean up.
Agent Prompt
## Issue description
The `/goal <objective>` Home flow creates a new thread, then calls `updateThreadGoal()`, and only after that inserts the thread into local state. If the goal RPC fails, `updateThreadGoal()` rethrows, so `insertOptimisticThread()` never runs and the new thread is invisible/unreachable in the UI.

## Issue Context
`insertOptimisticThread()` is the mechanism that makes newly created threads visible by updating `sourceGroups`/`projectGroups`. `updateThreadGoal()` explicitly rethrows on failure.

## Fix Focus Areas
- src/composables/useDesktopState.ts[5101-5218]
- src/composables/useDesktopState.ts[1737-1749]
- src/composables/useDesktopState.ts[2307-2345]

## Implementation notes
- After `startThread()` succeeds (and `threadId` is known), call `insertOptimisticThread(threadId, targetCwd, goalCommand.objective)` and `setSelectedThreadId(threadId)` **before** awaiting `updateThreadGoal()`.
- If `updateThreadGoal()` fails, keep the optimistic thread visible/selected and show the error; optionally mark it as not in-progress.
- Add/extend a unit test where `startThread` succeeds but `setThreadGoal` rejects, and assert the thread still exists in `sourceGroups` / is selectable.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 8bdef19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/utils/goalCommand.ts (1)

18-67: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider deriving the lifecycle-action list from a single source.

The set of secondary actions (edit, pause, resume, clear) is hard-coded both in GOAL_COMMAND_OPTIONS (via id) and in the parseGoalCommand subcommand regex (/^(edit|pause|resume|clear).../). A future addition/removal of a lifecycle action only needs to touch one of these to compile, but silently breaks parsing or suggestions if the other is forgotten (there's also a third mirror in useDesktopState.ts's executeGoalCommand).

♻️ Suggested direction
-const subcommand = argument.match(/^(edit|pause|resume|clear)(?:\s+([\s\S]*))?$/iu)
+const KNOWN_SUBCOMMAND_IDS = GOAL_COMMAND_OPTIONS.map((option) => option.id).filter((id) => id !== 'start' && id !== 'view')
+const subcommand = argument.match(new RegExp(`^(${KNOWN_SUBCOMMAND_IDS.join('|')})(?:\\s+([\\s\\S]*))?$`, 'iu'))

Also applies to: 91-108

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/goalCommand.ts` around lines 18 - 67, Define a single shared source
for the lifecycle actions edit, pause, resume, and clear, then derive
GOAL_COMMAND_OPTIONS entries, parseGoalCommand subcommand matching, and
useDesktopState.ts executeGoalCommand handling from it. Update these consumers
to avoid duplicating the action list while preserving each action’s existing
behavior and argument requirements.
src/components/content/ThreadGoalBar.vue (1)

1-38: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Wire up translation (t()) for the goal bar's user-facing strings.

Every visible label here ("Goal", status text, "Edit"/"Pause"/"Resume"/"Clear"/"Save"/"Cancel", aria-labels) is a hard-coded English literal. The rest of the app (see App.vue) consistently routes user-facing text through t() via useUiLanguage(). As written, the goal bar won't be localized when the user switches UI language.

♻️ Suggested direction
 <script setup lang="ts">
 import { computed, nextTick, ref, watch } from 'vue'
 import type { UiThreadGoal } from '../../types/codex'
+import { useUiLanguage } from '../../composables/useUiLanguage'
+
+const { t } = useUiLanguage()

and wrap each literal, e.g. {{ t('Active') }}, {{ t('Edit') }}, :aria-label="t('Goal objective')".

Also applies to: 40-103

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/content/ThreadGoalBar.vue` around lines 1 - 38, Update the
ThreadGoalBar template to obtain t from useUiLanguage and route every
user-facing literal through it, including the heading, statusLabel output,
action labels, and section/input aria-labels. Replace hard-coded English text
and static accessibility labels with translated bindings while preserving the
existing edit, pause/resume, save, cancel, and clear behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/composables/useDesktopState.ts`:
- Around line 1779-1815: Update executeGoalCommand to wrap all goal command
operations in top-level try/catch handling that sets error.value with the
failure and prevents unhandled rejections. Reuse clearSelectedThreadGoal for the
clear action when threadId is the selected thread, and clear stale error.value
after a successful view command while preserving the existing no-goal validation
messages.

---

Nitpick comments:
In `@src/components/content/ThreadGoalBar.vue`:
- Around line 1-38: Update the ThreadGoalBar template to obtain t from
useUiLanguage and route every user-facing literal through it, including the
heading, statusLabel output, action labels, and section/input aria-labels.
Replace hard-coded English text and static accessibility labels with translated
bindings while preserving the existing edit, pause/resume, save, cancel, and
clear behavior.

In `@src/utils/goalCommand.ts`:
- Around line 18-67: Define a single shared source for the lifecycle actions
edit, pause, resume, and clear, then derive GOAL_COMMAND_OPTIONS entries,
parseGoalCommand subcommand matching, and useDesktopState.ts executeGoalCommand
handling from it. Update these consumers to avoid duplicating the action list
while preserving each action’s existing behavior and argument requirements.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8b5f42d8-00d5-43e7-be82-af92149ac4a9

📥 Commits

Reviewing files that changed from the base of the PR and between fac2291 and 8bdef19.

📒 Files selected for processing (14)
  • src/App.vue
  • src/api/codexGateway.test.ts
  • src/api/codexGateway.ts
  • src/components/content/ThreadComposer.vue
  • src/components/content/ThreadGoalBar.vue
  • src/composables/useDesktopState.test.ts
  • src/composables/useDesktopState.ts
  • src/style.css
  • src/types/codex.ts
  • src/utils/goalCommand.test.ts
  • src/utils/goalCommand.ts
  • tests.md
  • tests/chat-composer-rendering/index.md
  • tests/chat-composer-rendering/thread-goal-mode.md

Comment on lines +1779 to 1815
async function executeGoalCommand(threadId: string, command: GoalCommand): Promise<void> {
if (command.action === 'view') {
const goal = await loadThreadGoal(threadId, { force: true })
if (!goal) error.value = 'No goal is set for this thread. Use /goal <objective> to start one.'
return
}
if (command.action === 'clear') {
await clearThreadGoal(threadId)
storeThreadGoal(threadId, null)
error.value = ''
return
}
if (command.action === 'pause' || command.action === 'resume') {
const goal = await loadThreadGoal(threadId)
if (!goal) {
error.value = 'No goal is set for this thread.'
return
}
await updateThreadGoal(threadId, { status: command.action === 'pause' ? 'paused' : 'active' })
return
}
if (command.action === 'edit') {
if (!command.objective) {
error.value = 'Use /goal edit <objective>, or choose Edit in the goal bar.'
return
}
const goal = await loadThreadGoal(threadId)
if (!goal) {
error.value = 'No goal is set for this thread.'
return
}
await updateThreadGoal(threadId, { objective: command.objective })
return
}

await updateThreadGoal(threadId, { objective: command.objective, status: 'active' })
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

executeGoalCommand needs consistent error handling and should reuse clearSelectedThreadGoal.

Unlike updateThreadGoal/clearSelectedThreadGoal, this function has no top-level try/catch:

  • view (1780-1784) and pause/resume/edit (1791-1812) all await loadThreadGoal(...) with no catch; if the RPC fails, the rejection propagates uncaught up through sendMessageToSelectedThread, which is invoked as void sendMessageToSelectedThread(...) in App.vue — this becomes an unhandled promise rejection and the user gets no error.value feedback (contrast with the normal message-send path, which wraps startTurnForThread in try/catch and sets a turn error).
  • clear (1785-1790) duplicates clearSelectedThreadGoal's RPC call inline instead of calling it, and also has no try/catch, so a failed /goal clear silently does nothing visible to the user.
  • view's success path never clears a stale error.value from a previous failed command (e.g. a prior "No goal is set" message stays on screen even after a goal is confirmed to exist).
🐛 Suggested fix
 async function executeGoalCommand(threadId: string, command: GoalCommand): Promise<void> {
-    if (command.action === 'view') {
-      const goal = await loadThreadGoal(threadId, { force: true })
-      if (!goal) error.value = 'No goal is set for this thread. Use /goal <objective> to start one.'
-      return
-    }
-    if (command.action === 'clear') {
-      await clearThreadGoal(threadId)
-      storeThreadGoal(threadId, null)
-      error.value = ''
-      return
-    }
-    if (command.action === 'pause' || command.action === 'resume') {
-      const goal = await loadThreadGoal(threadId)
-      if (!goal) {
-        error.value = 'No goal is set for this thread.'
-        return
-      }
-      await updateThreadGoal(threadId, { status: command.action === 'pause' ? 'paused' : 'active' })
-      return
-    }
-    if (command.action === 'edit') {
-      if (!command.objective) {
-        error.value = 'Use /goal edit <objective>, or choose Edit in the goal bar.'
-        return
-      }
-      const goal = await loadThreadGoal(threadId)
-      if (!goal) {
-        error.value = 'No goal is set for this thread.'
-        return
-      }
-      await updateThreadGoal(threadId, { objective: command.objective })
-      return
-    }
-
-    await updateThreadGoal(threadId, { objective: command.objective, status: 'active' })
+    try {
+      if (command.action === 'view') {
+        const goal = await loadThreadGoal(threadId, { force: true })
+        error.value = goal ? '' : 'No goal is set for this thread. Use /goal <objective> to start one.'
+        return
+      }
+      if (command.action === 'clear') {
+        await clearSelectedThreadGoal()
+        return
+      }
+      if (command.action === 'pause' || command.action === 'resume') {
+        const goal = await loadThreadGoal(threadId)
+        if (!goal) {
+          error.value = 'No goal is set for this thread.'
+          return
+        }
+        await updateThreadGoal(threadId, { status: command.action === 'pause' ? 'paused' : 'active' })
+        return
+      }
+      if (command.action === 'edit') {
+        if (!command.objective) {
+          error.value = 'Use /goal edit <objective>, or choose Edit in the goal bar.'
+          return
+        }
+        const goal = await loadThreadGoal(threadId)
+        if (!goal) {
+          error.value = 'No goal is set for this thread.'
+          return
+        }
+        await updateThreadGoal(threadId, { objective: command.objective })
+        return
+      }
+
+      await updateThreadGoal(threadId, { objective: command.objective, status: 'active' })
+    } catch (unknownError) {
+      error.value = unknownError instanceof Error ? unknownError.message : 'Failed to update goal'
+    }
   }

Note: clearSelectedThreadGoal reads selectedThreadId.value, so this only works cleanly if threadId passed to executeGoalCommand is always the selected thread (true for the current call site) — otherwise keep the inline call but wrap it in try/catch.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function executeGoalCommand(threadId: string, command: GoalCommand): Promise<void> {
if (command.action === 'view') {
const goal = await loadThreadGoal(threadId, { force: true })
if (!goal) error.value = 'No goal is set for this thread. Use /goal <objective> to start one.'
return
}
if (command.action === 'clear') {
await clearThreadGoal(threadId)
storeThreadGoal(threadId, null)
error.value = ''
return
}
if (command.action === 'pause' || command.action === 'resume') {
const goal = await loadThreadGoal(threadId)
if (!goal) {
error.value = 'No goal is set for this thread.'
return
}
await updateThreadGoal(threadId, { status: command.action === 'pause' ? 'paused' : 'active' })
return
}
if (command.action === 'edit') {
if (!command.objective) {
error.value = 'Use /goal edit <objective>, or choose Edit in the goal bar.'
return
}
const goal = await loadThreadGoal(threadId)
if (!goal) {
error.value = 'No goal is set for this thread.'
return
}
await updateThreadGoal(threadId, { objective: command.objective })
return
}
await updateThreadGoal(threadId, { objective: command.objective, status: 'active' })
}
async function executeGoalCommand(threadId: string, command: GoalCommand): Promise<void> {
try {
if (command.action === 'view') {
const goal = await loadThreadGoal(threadId, { force: true })
error.value = goal ? '' : 'No goal is set for this thread. Use /goal <objective> to start one.'
return
}
if (command.action === 'clear') {
await clearSelectedThreadGoal()
return
}
if (command.action === 'pause' || command.action === 'resume') {
const goal = await loadThreadGoal(threadId)
if (!goal) {
error.value = 'No goal is set for this thread.'
return
}
await updateThreadGoal(threadId, { status: command.action === 'pause' ? 'paused' : 'active' })
return
}
if (command.action === 'edit') {
if (!command.objective) {
error.value = 'Use /goal edit <objective>, or choose Edit in the goal bar.'
return
}
const goal = await loadThreadGoal(threadId)
if (!goal) {
error.value = 'No goal is set for this thread.'
return
}
await updateThreadGoal(threadId, { objective: command.objective })
return
}
await updateThreadGoal(threadId, { objective: command.objective, status: 'active' })
} catch (unknownError) {
error.value = unknownError instanceof Error ? unknownError.message : 'Failed to update goal'
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/composables/useDesktopState.ts` around lines 1779 - 1815, Update
executeGoalCommand to wrap all goal command operations in top-level try/catch
handling that sets error.value with the failure and prevents unhandled
rejections. Reuse clearSelectedThreadGoal for the clear action when threadId is
the selected thread, and clear stale error.value after a successful view command
while preserving the existing no-goal validation messages.

@Nul-led

Nul-led commented Jul 21, 2026

Copy link
Copy Markdown
Author

/review

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jul 21, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@Nul-led

Nul-led commented Jul 21, 2026

Copy link
Copy Markdown
Author

/review

Comment on lines +72 to +88
export function decodeBrowsePath(rawPath: string, platform: NodeJS.Platform = process.platform): string {
if (!rawPath) return ''
let decoded: string
try {
return decodeURIComponent(rawPath)
decoded = decodeURIComponent(rawPath)
} catch {
return rawPath
decoded = rawPath
}

// Browse URLs keep an absolute-path slash after the route prefix. On Windows,
// that turns `C:/path` into `/C:/path`, which Node resolves as `C:\C:\path`.
// Remove only that synthetic slash; Unix and UNC absolute paths stay intact.
if (platform === 'win32' && /^\/[A-Za-z]:[\\/]/u.test(decoded)) {
return decoded.slice(1)
}

return decoded

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Missing testchat browse-link screenshot 📘 Rule violation ☼ Reliability

This PR changes browse-link/path decoding behavior but does not include the mandatory TestChat
validation artifact output/playwright/testchat-<feature>-cjs.png. Without the required screenshot
evidence, browse/file-link rendering regressions can ship unnoticed.
Agent Prompt
## Issue description
The PR modifies browse-link handling (`decodeBrowsePath`) but does not include the required TestChat validation screenshot artifact under `output/playwright/testchat-<feature>-cjs.png`.

## Issue Context
Compliance requires a specific TestChat validation for chat parsing/file-link/browse-link changes: send a unique marker containing representative link content, verify the rendered row with `hrefOk`, `titleOk`, and `textOk`, and save the screenshot as `output/playwright/testchat-<feature>-cjs.png`.

## Fix Focus Areas
- src/server/localBrowseUi.ts[72-88]
- tests/chat-composer-rendering/windows-absolute-file-links-open-local-browse.md[1-15]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/server/localBrowseUi.ts
Comment on lines 1701 to +1705
)
activeReasoningItemId = ''
shouldAutoScrollOnNextAgentEvent = false
if (nextThreadId) void loadThreadGoal(nextThreadId).catch(() => {})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Goal read errors hidden 🐞 Bug ☼ Reliability

useDesktopState triggers goal reads on thread selection and on polling startup but swallows
loadThreadGoal failures via .catch(() => {}), and loadThreadGoal itself doesn’t update the shared
error state on RPC failure. When thread/goal/get fails, the goal bar can disappear with no
user-visible error, making the UI look like “no goal exists” rather than “goal load failed.”
Agent Prompt
## Issue description
Goal reads (`getThreadGoal`) are initiated automatically (thread selection + polling startup) but failures are swallowed and never populate the shared desktop `error` state. This makes goal state appear missing when the app-server is unreachable/errored.

## Issue Context
- `setSelectedThreadId()` calls `loadThreadGoal(...).catch(() => {})`.
- `startPolling()` calls `loadThreadGoal(...).catch(() => {})`.
- `loadThreadGoal()` does not `try/catch` to set `error.value` on rejection (contrast with `updateThreadGoal()` which does).

## Fix Focus Areas
- src/composables/useDesktopState.ts[1691-1735]
- src/composables/useDesktopState.ts[1737-1749]
- src/composables/useDesktopState.ts[5697-5703]

### Suggested approach
Pick one (consistent) strategy:
1) Handle errors inside `loadThreadGoal()`:
   - wrap `await getThreadGoal(...)` in `try/catch`
   - on error, set `error.value` (e.g. `Failed to load goal`) and rethrow (or return cached value if you prefer)

OR

2) Keep `loadThreadGoal()` pure but don’t swallow errors at call sites:
   - replace `.catch(() => {})` with `.catch((e) => { error.value = ... })`
   - optionally guard so you only set error if the failing thread is still selected.

### Add/adjust tests
- Add a unit test where `getThreadGoal` rejects during thread selection and assert `state.error.value` (or equivalent exposed error signal) is updated.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit edc7aaa

@Nul-led

Nul-led commented Jul 27, 2026

Copy link
Copy Markdown
Author

/review

Comment on lines +77 to +80
function formatTokens(value: number): string {
if (value < 1000) return String(value)
if (value < 1_000_000) return `${Math.round(value / 100) / 10}k`
return `${Math.round(value / 100_000) / 10}m`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Informational

1. Token label rounds wrong 🐞 Bug ≡ Correctness

ThreadGoalBar.formatTokens() can render values just below 1,000,000 as "1000k" (e.g., 999,950 →
1000k) even though ≥1,000,000 switches to "m", producing inconsistent usage labels around the
threshold.
Agent Prompt
### Issue description
`formatTokens()` can round thousands to `1000k` for values just below one million, which is inconsistent with the `m` branch and misleading in the goal usage label.

### Issue Context
This affects `usageLabel`, which is shown whenever `goal.tokenBudget !== null`.

### Fix Focus Areas
- src/components/content/ThreadGoalBar.vue[68-81]

### Suggested fix
Compute the display unit based on the *rounded* value (or compute in `k` first and promote to `m` when the rounded `k` value reaches 1000). For example:
- Compute `k = value / 1000`.
- If `k < 999.95`, show `${round1(k)}k`.
- Else show `${round1(value / 1_000_000)}m`.

(Optionally add a small unit test for boundary cases like `999950`, `999999`, and `1_000_000`.)

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 1c24377

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/server/localBrowseUi.ts`:
- Around line 144-158: Update toBrowseHref and toEditHref so filesystem path
delimiters such as ? and # are percent-encoded before constructing the route,
while preserving path separators and existing normalization/query behavior.
Reuse the shared normalizeLocalRoutePath flow, apply path-safe encoding to both
hrefs, and add regression coverage for paths containing ? and #.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: aae84f1c-1eea-4be6-b5da-f42d443783c9

📥 Commits

Reviewing files that changed from the base of the PR and between edc7aaa and 1c24377.

📒 Files selected for processing (3)
  • src/server/localBrowseUi.test.ts
  • src/server/localBrowseUi.ts
  • tests/chat-composer-rendering/windows-absolute-file-links-open-local-browse.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/chat-composer-rendering/windows-absolute-file-links-open-local-browse.md

Comment on lines +144 to +158
function normalizeLocalRoutePath(pathValue: string): string {
const normalized = pathValue.replace(/\\/gu, '/')
return normalized.startsWith('/') ? normalized : `/${normalized}`
}

export function toBrowseHref(pathValue: string, newProjectName = ''): string {
const normalizedName = normalizeNewProjectName(newProjectName)
const query = normalizedName ? `?newProjectName=${encodeURIComponent(normalizedName)}` : ''
return `/codex-local-browse${encodeURI(pathValue)}${query}`
return `/codex-local-browse${encodeURI(normalizeLocalRoutePath(pathValue))}${query}`
}

function toEditHref(pathValue: string, newProjectName = ''): string {
export function toEditHref(pathValue: string, newProjectName = ''): string {
const normalizedName = normalizeNewProjectName(newProjectName)
const query = normalizedName ? `?newProjectName=${encodeURIComponent(normalizedName)}` : ''
return `/codex-local-edit${encodeURI(pathValue)}${query}`
return `/codex-local-edit${encodeURI(normalizeLocalRoutePath(pathValue))}${query}`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Escape URL delimiters in filesystem paths.

encodeURI leaves ? and # unescaped, so valid paths such as report#1.txt or report?draft.txt become a fragment/query instead of part of the route. Encode path components (or explicitly escape these delimiters) before building both hrefs, and add regression cases for them.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/localBrowseUi.ts` around lines 144 - 158, Update toBrowseHref and
toEditHref so filesystem path delimiters such as ? and # are percent-encoded
before constructing the route, while preserving path separators and existing
normalization/query behavior. Reuse the shared normalizeLocalRoutePath flow,
apply path-safe encoding to both hrefs, and add regression coverage for paths
containing ? and #.

@Nul-led

Nul-led commented Jul 28, 2026

Copy link
Copy Markdown
Author

/review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/utils/slashCommand.ts`:
- Around line 120-131: Update the slash-command parser around the action
dispatch so compact, review, fork, and archive require an empty argument; reject
or return a usage error when trailing text is present instead of executing the
control and discarding it. Preserve argument handling for plan, model, and
rename, and add regression coverage for trailing text on each no-argument
action.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 83cf9f9d-7877-465f-9d54-2a32c74417c3

📥 Commits

Reviewing files that changed from the base of the PR and between 1c24377 and d715f7e.

📒 Files selected for processing (8)
  • src/api/codexGateway.test.ts
  • src/api/codexGateway.ts
  • src/components/content/ThreadComposer.vue
  • src/composables/useDesktopState.test.ts
  • src/composables/useDesktopState.ts
  • src/utils/slashCommand.test.ts
  • src/utils/slashCommand.ts
  • tests/chat-composer-rendering/thread-goal-mode.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • tests/chat-composer-rendering/thread-goal-mode.md
  • src/api/codexGateway.ts
  • src/api/codexGateway.test.ts
  • src/components/content/ThreadComposer.vue
  • src/composables/useDesktopState.ts

Comment thread src/utils/slashCommand.ts
Comment on lines +120 to +131
const match = value.trim().match(/^\/(plan|compact|review|model|rename|fork|archive)(?:\s+([\s\S]*))?$/iu)
if (!match) return null

const action = match[1].toLowerCase()
const argument = (match[2] ?? '').trim()
if (action === 'plan') return { kind: 'codex', command: { action: 'plan', prompt: argument } }
if (action === 'model') return { kind: 'codex', command: { action: 'model', model: argument } }
if (action === 'rename') return { kind: 'codex', command: { action: 'rename', name: argument } }
if (action === 'compact') return { kind: 'codex', command: { action: 'compact' } }
if (action === 'review') return { kind: 'codex', command: { action: 'review' } }
if (action === 'fork') return { kind: 'codex', command: { action: 'fork' } }
return { kind: 'codex', command: { action: 'archive' } }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject trailing text for no-argument controls.

/compact notes, /review notes, /fork notes, and /archive notes execute the control while silently discarding notes; the submission path then returns without sending it. Require an empty argument for these actions (or surface a usage error) and add regression coverage.

Proposed fix
   const action = match[1].toLowerCase()
   const argument = (match[2] ?? '').trim()
+  const requiresNoArgument =
+    action === 'compact' ||
+    action === 'review' ||
+    action === 'fork' ||
+    action === 'archive'
+  if (requiresNoArgument && argument) return null
+
   if (action === 'plan') return { kind: 'codex', command: { action: 'plan', prompt: argument } }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const match = value.trim().match(/^\/(plan|compact|review|model|rename|fork|archive)(?:\s+([\s\S]*))?$/iu)
if (!match) return null
const action = match[1].toLowerCase()
const argument = (match[2] ?? '').trim()
if (action === 'plan') return { kind: 'codex', command: { action: 'plan', prompt: argument } }
if (action === 'model') return { kind: 'codex', command: { action: 'model', model: argument } }
if (action === 'rename') return { kind: 'codex', command: { action: 'rename', name: argument } }
if (action === 'compact') return { kind: 'codex', command: { action: 'compact' } }
if (action === 'review') return { kind: 'codex', command: { action: 'review' } }
if (action === 'fork') return { kind: 'codex', command: { action: 'fork' } }
return { kind: 'codex', command: { action: 'archive' } }
const match = value.trim().match(/^\/(plan|compact|review|model|rename|fork|archive)(?:\s+([\s\S]*))?$/iu)
if (!match) return null
const action = match[1].toLowerCase()
const argument = (match[2] ?? '').trim()
const requiresNoArgument =
action === 'compact' ||
action === 'review' ||
action === 'fork' ||
action === 'archive'
if (requiresNoArgument && argument) return null
if (action === 'plan') return { kind: 'codex', command: { action: 'plan', prompt: argument } }
if (action === 'model') return { kind: 'codex', command: { action: 'model', model: argument } }
if (action === 'rename') return { kind: 'codex', command: { action: 'rename', name: argument } }
if (action === 'compact') return { kind: 'codex', command: { action: 'compact' } }
if (action === 'review') return { kind: 'codex', command: { action: 'review' } }
if (action === 'fork') return { kind: 'codex', command: { action: 'fork' } }
return { kind: 'codex', command: { action: 'archive' } }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/slashCommand.ts` around lines 120 - 131, Update the slash-command
parser around the action dispatch so compact, review, fork, and archive require
an empty argument; reject or return a usage error when trailing text is present
instead of executing the control and discarding it. Preserve argument handling
for plan, model, and rename, and add regression coverage for trailing text on
each no-argument action.

Comment on lines +1637 to +1646
if (event.key === 'Enter') {
const selected = slashCommandSuggestions.value[slashCommandHighlightedIndex.value]
const normalizedDraft = draft.value.trim()
const exactOption = describeSlashCommand(normalizedDraft)
const isRunnableExact = exactOption?.requiresArgument === false
&& exactOption.insertText.trim() === normalizedDraft
if (!isRunnableExact) {
event.preventDefault()
if (selected) applySlashCommandOption(selected)
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Slash enter case mismatch 🐞 Bug ≡ Correctness

ThreadComposer.onInputKeydown blocks Enter for runnable slash commands typed with different casing
(e.g. "/GOAL pause") because it compares the canonical option insertText (lowercase) against the raw
draft string. This prevents the command from being submitted while the slash menu is open, despite
slash-command parsing being case-insensitive.
Agent Prompt
### Issue description
Runnable slash commands entered with different casing (e.g. `/GOAL pause`, `/PLAN`) are parsed correctly, but `onInputKeydown` prevents `Enter` because it checks exact equality between `exactOption.insertText` (canonical lowercase) and the user’s `draft`.

### Issue Context
`describeSlashCommand()` / `parseSlashCommand()` are case-insensitive, and unit tests explicitly expect `/GOAL pause` to parse as a valid goal command. The composer’s “runnable exact command” check should therefore be case-insensitive as well.

### Fix Focus Areas
- src/components/content/ThreadComposer.vue[1637-1646]

### Implementation notes
- Update the runnable-exact comparison to use a normalized form on both sides (e.g. `toLowerCase()`), such as:
  - `exactOption.insertText.trim().toLowerCase() === normalizedDraft.toLowerCase()`
- Keep the existing `requiresArgument === false` guard.
- (Optional) Add a small unit test around composer key handling if there’s an existing test harness for it; otherwise, ensure existing slash parsing tests remain valid.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit d715f7e

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant