Add app-server thread Goal mode - #211
Conversation
📝 WalkthroughWalkthroughThread 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. ChangesThread goal mode
Windows browse path handling
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
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
/review |
Code Review by Qodo
1. Missing TestChat browse-link screenshot
|
| :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%); | ||
| } |
There was a problem hiding this comment.
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
| 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 |
There was a problem hiding this comment.
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
| 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 | ||
| } |
There was a problem hiding this comment.
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
| 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 |
There was a problem hiding this comment.
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
|
/review |
| const goalCommand = imageUrls.length === 0 && skills.length === 0 && fileAttachments.length === 0 | ||
| ? parseGoalCommand(nextText) | ||
| : null | ||
| if (goalCommand) { | ||
| await executeGoalCommand(threadId, goalCommand) | ||
| return | ||
| } |
There was a problem hiding this comment.
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
| 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 | ||
| } |
There was a problem hiding this comment.
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
| async function runThreadGoalUpdate(update: () => Promise<void>): Promise<void> { | ||
| if (isUpdatingThreadGoal.value) return | ||
| isUpdatingThreadGoal.value = true | ||
| try { |
There was a problem hiding this comment.
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
|
Code review by qodo was updated up to the latest commit 8bdef19 |
PR Summary by QodoAdd app-server-backed thread Goal mode with slash commands and goal bar
AI Description
Diagram
High-Level Assessment
Files changed (14)
|
| 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 | ||
| } |
There was a problem hiding this comment.
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
|
Code review by qodo was updated up to the latest commit 8bdef19 |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/utils/goalCommand.ts (1)
18-67: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider deriving the lifecycle-action list from a single source.
The set of secondary actions (
edit,pause,resume,clear) is hard-coded both inGOAL_COMMAND_OPTIONS(viaid) and in theparseGoalCommandsubcommand 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 inuseDesktopState.ts'sexecuteGoalCommand).♻️ 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 winWire 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 (seeApp.vue) consistently routes user-facing text throught()viauseUiLanguage(). 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
📒 Files selected for processing (14)
src/App.vuesrc/api/codexGateway.test.tssrc/api/codexGateway.tssrc/components/content/ThreadComposer.vuesrc/components/content/ThreadGoalBar.vuesrc/composables/useDesktopState.test.tssrc/composables/useDesktopState.tssrc/style.csssrc/types/codex.tssrc/utils/goalCommand.test.tssrc/utils/goalCommand.tstests.mdtests/chat-composer-rendering/index.mdtests/chat-composer-rendering/thread-goal-mode.md
| 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' }) | ||
| } |
There was a problem hiding this comment.
🩺 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) andpause/resume/edit(1791-1812) allawait loadThreadGoal(...)with no catch; if the RPC fails, the rejection propagates uncaught up throughsendMessageToSelectedThread, which is invoked asvoid sendMessageToSelectedThread(...)in App.vue — this becomes an unhandled promise rejection and the user gets noerror.valuefeedback (contrast with the normal message-send path, which wrapsstartTurnForThreadin try/catch and sets a turn error).clear(1785-1790) duplicatesclearSelectedThreadGoal's RPC call inline instead of calling it, and also has no try/catch, so a failed/goal clearsilently does nothing visible to the user.view's success path never clears a staleerror.valuefrom 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.
| 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.
|
/review |
|
/review |
| 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 |
There was a problem hiding this comment.
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
| ) | ||
| activeReasoningItemId = '' | ||
| shouldAutoScrollOnNextAgentEvent = false | ||
| if (nextThreadId) void loadThreadGoal(nextThreadId).catch(() => {}) | ||
| } |
There was a problem hiding this comment.
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
|
Code review by qodo was updated up to the latest commit edc7aaa |
|
/review |
| 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` |
There was a problem hiding this comment.
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
|
Code review by qodo was updated up to the latest commit 1c24377 |
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
src/server/localBrowseUi.test.tssrc/server/localBrowseUi.tstests/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
| 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}` |
There was a problem hiding this comment.
🎯 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 #.
|
/review |
There was a problem hiding this comment.
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
📒 Files selected for processing (8)
src/api/codexGateway.test.tssrc/api/codexGateway.tssrc/components/content/ThreadComposer.vuesrc/composables/useDesktopState.test.tssrc/composables/useDesktopState.tssrc/utils/slashCommand.test.tssrc/utils/slashCommand.tstests/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
| 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' } } |
There was a problem hiding this comment.
🎯 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.
| 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.
| 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 |
There was a problem hiding this comment.
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
|
Code review by qodo was updated up to the latest commit d715f7e |
Summary - add app-server-backed thread Goal state through
thread/goal/get,thread/goal/set, andthread/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 - consumethread/goal/updatedandthread/goal/clearednotifications without broad thread/message refreshes ## Protocol behaviorthread/goal/setowns and starts the autonomous goal loop. CodexApp does not send a secondturn/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 against127.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 POSIX0600mode reporting, which this Windows environment does not provide. ## Manual coverage Addedtests/chat-composer-rendering/thread-goal-mode.mdand 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
- 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
## 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)/goalslash-command autocomplete with in-input previews and keyboard selection./goalparsing/suggestions, thread goal RPCs, and local-browse path decoding.Native Codex slash commands
/plan,/review,/compact,/model,/rename,/fork, and/archiveAdditional verification: