Skip to content

🤖 feat: iterate goals UI with completed-goals history and editable objective#3301

Merged
ammario merged 45 commits into
mainfrom
goals-ui-qfdv
May 18, 2026
Merged

🤖 feat: iterate goals UI with completed-goals history and editable objective#3301
ammario merged 45 commits into
mainfrom
goals-ui-qfdv

Conversation

@ammar-agent
Copy link
Copy Markdown
Collaborator

@ammar-agent ammar-agent commented May 16, 2026

Summary

Iterates the right-sidebar Goal tab into the long-lived home for goal
configuration. Builds on the original "completed-goals history + in-place
objective editing" change with three follow-on iterations: always-visible
tab, active-goal accent, in-tab create form, and a new defaults panel
that owns both the per-workspace override and the global defaults that
used to live in Settings → Experiments.

Background

After the first round, the Goal tab still:

  • Hid itself when there was no current goal or history, so a fresh
    workspace had to learn /goal to start one.
  • Looked identical whether or not the workspace had a live goal.
  • Required users to go to Settings → Experiments to configure budget /
    turn-cap defaults, and offered no way to keep different defaults per
    workspace (e.g., higher budget for the workspace handling a tough
    refactor, low budget for everything else).

The user asked us to consolidate goal configuration into the Goal tab
itself — the natural home for goal lifecycle.

Implementation

Original change set (kept)

  • goal-history.jsonl — Append-only per-workspace history file.
    workspace.getGoalHistory returns newest-first, capped at 200, with
    corrupt-line tolerance.
  • WorkspaceGoalService hooksclearGoal and the archive+replace
    branch of setGoalImmediately write history under the file lock before
    the canonical goal.json is rewritten.
  • editInPlace flag on GoalSetInputSchema so the objective can be
    renamed in place (preserves goalId + accounting). Inline objective
    editor on the active-goal card.
  • Clear demotion — Pause / Resume / Mark complete remain primary;
    Clear is a small underlined link, relabeled to "Archive this goal" for
    completed goals.
  • GoalHistorySection under the present goal (or under the empty
    state), expandable read-only rows with end-reason + accounting +
    completion summary.

Iteration 1: Always-visible Goal tab + active-goal accent + in-tab create form

  • Goal tab is now visible whenever the goals experiment is enabled — no
    longer gated on goal != null || goalHistory.length > 0. Surfaces an
    empty-state with a create form so users don't need /goal to begin.
  • GoalTabLabel subscribes to the workspace sidebar state and applies
    the canonical text-success accent when a live (status === "active",
    non-pending) goal exists — mirrors the useActiveGoalCount predicate.
  • GoalCreateForm in the empty state with slash-command parity
    (objective + optional budget + optional turn cap). Cmd/Ctrl+Enter
    submits. Blank budget/turn-cap defers to defaults so /goal, palette,
    and tab paths produce identical goals.

Iteration 2: Per-workspace overrides + Goal-tab defaults panel

  • Sparse override schemaWorkspaceGoalDefaultsOverrideSchema on
    both WorkspaceConfigSchema and WorkspaceMetadataSchema. Each field
    (defaultBudgetCents, defaultTurnCap, alwaysRequireExplicitBudget)
    is independently nullable; null means "inherit from global".
  • workspace.goalDefaults.{get,set} oRPC modeled on
    workspace.heartbeat.{get,set}. The service drops the whole record
    when every field is null, keeping ~/.mux/config.json minimal.
  • mergeGoalDefaults(global, override) layers the override on top
    of the global. loadGoalDefaults(api, workspaceId?) now accepts an
    optional workspace; the slash command (chatCommands.ts), command
    palette (sources.ts), and in-tab create form (RightSidebar.tsx)
    all pass workspaceId so workspace rules win over the global default.
  • GoalDefaultsSection at the bottom of the Goal tab (empty + active
    states). Collapsible <details> with summary showing the effective
    budget + turn cap and a "Workspace" badge when any override is active.
    Two stacked sub-panels:
    • This workspace — three rows with Inherit/Override toggles. When
      inheriting, copy reads "Inherits $X from All workspaces". Toggle on
      seeds the input with the inherited value; toggle off persists null.
    • All workspaces — reuses GoalDefaultsControls from Settings
      (now with an onPersist callback so the workspace panel's inherit
      labels re-pull on every global edit).
  • Settings → Experiments no longer mounts GoalDefaultsControls; it
    emits a small pointer toward the Goal tab as the new home.

Validation

make static-check clean (lint + format + typecheck + docs link check).

Targeted tests (all pass):

  • src/browser/utils/goals/resolveGoalSetIntent.test.ts15 pass,
    including 7 new mergeGoalDefaults precedence tests.
  • src/node/services/workspaceService.goalDefaults.test.ts8 pass
    (round-trip, partial, all-null clears, no-op short-circuit,
    validation, not-found).
  • src/node/services/workspaceGoalService.test.ts — 91 pass.
  • src/browser/features/RightSidebar/GoalTab.test.tsx17 pass,
    including 5 create-form tests.
  • src/browser/features/RightSidebar/Tabs/TabLabels.goal.test.tsx
    5 pass (label accent matrix).
  • src/browser/features/Settings/Sections/ExperimentsSection.test.tsx
    9 pass (updated to assert goal defaults moved out of the panel).
  • tests/ui/layout/rightSidebar.test.ts — 2 new layout-visibility
    guards (tab hidden when GOALS off; always present when GOALS on).

Risks

  • New write path on setGoal replace + clearGoal is best-effort: a
    history write failure logs but never aborts the lifecycle mutation.
  • editInPlace is opt-in; existing /goal, palette, and kickoff paths
    continue to take archive+recreate unchanged.
  • Per-workspace override is sparse and all-null clears: a workspace
    with no override is indistinguishable from one whose override was
    never set, so reverting an override never strands stale data on disk.
  • The loadGoalDefaults(workspaceId?) signature change is backwards
    compatible — callers that don't yet know the workspace (Settings
    global panel) still get global-only defaults.

Generated with mux • Model: anthropic:claude-opus-4-7 • Thinking: high • Cost: $16.93

@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

Please review this iteration of the workspace Goal UI:

  • backend: append-only goal-history JSONL + getGoalHistory IPC + editInPlace setGoal rename
  • frontend: editable objective, de-prominent Clear, expandable completed-goals list under the present goal

Reads + writes happen under the per-workspace file lock; history is best-effort (failures logged, lifecycle mutation never aborted). Curious for feedback on UX wording ("Archive this goal" vs "Clear goal") and the editInPlace flag design vs a dedicated endpoint.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ed6b31e0b7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/workspaceGoalService.ts
Comment thread src/node/services/workspaceGoalService.ts Outdated
@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

Pushed a fix: added getGoal + getGoalHistory mocks to src/browser/stories/mocks/orpc.ts so the RightSidebar mount-time history fetch doesn't throw against the storybook/integration-test mock client (this was the root cause of the Storybook + Integration CI failures on the previous push).

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4f4fbff035

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/workspaceGoalService.ts
@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

Addressed your two pieces of feedback:

  • P2: forwarded editInPlace into PendingGoalMutation and the mid-stream deferral path so an inline rename during streaming still takes the rename branch when it drains. Added a regression test.
  • P3: wrapped getGoalHistory in fileLocks.withLock so a concurrent append cannot leave a partial line that the reader silently skips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f4bfc593db

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/browser/features/RightSidebar/RightSidebar.tsx
@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

Pushed two more fixes addressing your latest feedback:

  • P2 (Honor editInPlace while streams are active): already addressed in f4bfc59 before the latest review snapshot — PendingGoalMutation carries editInPlace and applyPendingAfterStreamEnd replays it through the in-place branch. Resolved that thread.
  • P2 (Preserve the Goal tab for history-only workspaces): updated the right-sidebar layout effect to keep the Goal tab visible whenever goalHistory.length > 0 as well, with goalHistory.length added to the dependency array so the effect re-runs once the history fetch populates. This makes the new empty-state + history branch reachable after clearing the last goal or on a fresh launch of a history-only workspace.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8b771205e0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/workspaceGoalService.ts Outdated
@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

Addressed your tie-breaker feedback: getGoalHistory now retains the JSONL append index and uses it as the DESC tie-breaker when endedAtMs is equal, so the latest line in the file always wins regardless of how fast the appends happened. Added a regression test that pins Date.now to force same-ms timestamps across two consecutive clears.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ammar-agent ammar-agent force-pushed the goals-ui-qfdv branch 2 times, most recently from e6614a7 to 921cff4 Compare May 17, 2026 20:45
@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

Three new iterations on the Goal UX:

  1. Goal tab is now always visible when the GOALS experiment is enabled (no longer gated on goal != null || goalHistory.length > 0).
  2. Goal tab label uses text-success accent when the workspace has a live active goal (mirrors useActiveGoalCount predicate).
  3. Empty-state GoalTab now exposes an in-tab create form with full slash-command parity (objective + optional budget + optional turn cap), routed through the same loadGoalDefaults + resolveGoalSetIntent + unpriced-model gate as the palette and slash paths.

Plus 12 new tests (5 create-form, 5 label-accent, 2 layout-visibility integration).

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1eb4b3d602

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/browser/features/RightSidebar/RightSidebar.tsx Outdated
@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

Iteration 3 just landed (commit ddcf9d811): consolidated goal defaults into the Goal tab.

Key things to look at:

  1. Schema layering — new WorkspaceGoalDefaultsOverrideSchema is sparse-nullable ({ defaultBudgetCents: nullable, defaultTurnCap: nullable, alwaysRequireExplicitBudget: nullable }). Carried on both WorkspaceConfigSchema and WorkspaceMetadataSchema. Persisted via workspaceService.setWorkspaceGoalDefaults which drops the record when every field is null (modeled on setHeartbeatSettings).

  2. Resolver precedencemergeGoalDefaults(global, override) in resolveGoalSetIntent.ts. Used ?? for inherit fallback (null/undefined override field → global). The tricky case is defaultTurnCap which is itself nullable (null = "no cap" at the global layer); the ?? short-circuit correctly treats override null as "inherit", not as "explicit no cap", because the override schema has no way to express "explicit no cap" — that's the global-layer responsibility.

  3. Three create surfaces still in locksteploadGoalDefaults(api, workspaceId?) now layers the workspace override. Callers updated: chatCommands.ts (slash), commands/sources.ts (palette), RightSidebar.tsx (in-tab create form). All three pass workspaceId so workspace rules win.

  4. UI — new GoalDefaultsSection.tsx at the bottom of the Goal tab. Collapsible <details> with summary badge. Per-field inherit/override toggles. Inherit labels re-pull when global is edited via the new GoalDefaultsControls#onPersist callback.

  5. Settings cleanupExperimentsSection no longer mounts GoalDefaultsControls; replaced with a small pointer to the Goal tab. Tests updated.

Risks called out in the PR body.

@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

Addressed P2 in commit b19d5b4ec: handleGoalCreate now fetches api.providers.getConfig() at submit time instead of reading the useProvidersConfig() hook value, matching the slash command + palette pattern. Falls back to the hook value on fetch failure so transient network errors don't leak through.

Iteration 3 (goal defaults consolidation) is also up for review on top of the previous always-visible/accent/create-form work — please cover the new code as well:

  • Sparse WorkspaceGoalDefaultsOverrideSchema on WorkspaceConfigSchema + WorkspaceMetadataSchema
  • workspace.goalDefaults.{get,set} oRPC + workspaceService.set/getWorkspaceGoalDefaults (modeled on heartbeat; drops record when all-null)
  • mergeGoalDefaults(global, override) + loadGoalDefaults(api, workspaceId?) precedence
  • GoalDefaultsSection (collapsible per-workspace overrides + reused global controls)
  • Settings → Experiments now just points to the Goal tab

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b19d5b4ece

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/browser/features/RightSidebar/GoalDefaultsSection.tsx
@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

Addressed your P2 in 18e48a939: GoalDefaultsSection now tracks an explicit isLoading flag.

  • persistOverride() is a no-op while loading (would have otherwise written the synthesized all-null shape on top of saved overrides).
  • The override panel shows a Loading… label and visually + a11y-disables the toggles (opacity-60, pointer-events-none, aria-busy, <button disabled>).
  • isLoading resets to true whenever workspaceId changes so workspace switches can't let new-workspace edits race the old read.

Please re-review iteration 3 (goal defaults consolidation).

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 18e48a9395

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/browser/features/Settings/Sections/GoalsSection.tsx Outdated
@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

Addressed your latest P2 in 36343404b:

  • persistGoalDefaults (Settings GoalsSection) now awaits updateGoalDefaults before firing onPersist.
  • GoalDefaultsSection dropped the globalReloadKey + refetch pattern entirely. onPersist(next) already receives the freshly normalized GoalDefaults; we push that straight into local state, so the race window is closed.

Tests still pass:

  • GoalsSection.test.tsx — 2 pass
  • resolveGoalSetIntent.test.ts — 15 pass
  • workspaceService.goalDefaults.test.ts — 8 pass

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

if (
hasGoalBudgetLimit(budgetCents) &&
!modelHasPricingData(sendMessageOptions.model, providersConfig)
) {
throw new Error(UNPRICED_CURRENT_MODEL_GOAL_MESSAGE);

P2 Badge Fetch provider config before rejecting budget edits

When the user edits an existing goal's budget while useProvidersConfig() is still loading, providersConfig can be null; for custom/provider-mapped models this makes modelHasPricingData(sendMessageOptions.model, providersConfig) return false even though the backend config would resolve pricing. The create path just below fetches api.providers.getConfig() at submit time for this exact case, but inline budget edits still throw UNPRICED_CURRENT_MODEL_GOAL_MESSAGE and block a valid budget until the hook catches up.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1285584a12

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/workspaceGoalService.ts
Comment thread src/node/services/workspaceGoalService.ts
@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

P1 (mid-stream promote) addressed by rejecting promote when isWorkspaceStreaming is true, with a typed WorkspaceGoalTransitionError. The UI surfaces the message in the Upcoming section.

P2 (queued-goal defaults) addressed by applying resolveGoalSetIntent in UpcomingAdder.submit before the IPC call, matching the create-form / slash command / palette paths.

New test promoteUpcomingGoal rejects mid-stream covers P1.

Please take another look.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2e7ce9b869

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/browser/features/RightSidebar/GoalTab.tsx Outdated
Comment thread src/node/services/workspaceGoalService.ts Outdated
@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

Both P2s addressed:

  • Refresh board after auto-promotionuseGoalBoard now takes an activeGoalKey parameter and the GoalTab passes ${goalId}:${status}. Setting/clearing the active goal re-fetches the board.
  • Avoid demoting completed goals into UpcomingpromoteUpcomingGoal now archives a completed previous-active to history under "completed" instead of pushing it back into the queue.

New test: promoteUpcomingGoal archives a completed active goal instead of demoting to upcoming.

Please take another look.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 97ca0ba8bb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/workspaceGoalService.ts Outdated
@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

P2 (gate promoted budgeted goals on pricing data) addressed by adding canRunBudgetedGoalOnKickoffModel checks to both promote paths:

  • promoteUpcomingGoal throws WorkspaceGoalTransitionError(UNPRICED_TARGET_MODEL_GOAL_MESSAGE) — board UI surfaces the message via its existing catch.
  • promoteNextUpcomingUnlocked (auto on complete) logs + returns null, keeping the upcoming head in place so the user can resolve and retry.

Please take another look.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0f5c70b11e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/workspaceGoalService.ts
@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

P2 (double-archive on skipped auto-promote) fixed by hoisting the pricing gate check above the history append in maybeAutoPromoteOnComplete. When the next queued goal is budgeted but the kickoff model is unpriced, we now skip both the history append AND the goal.json write, leaving the completed goal in place so a later manual promote archives it exactly once.

Please take another look.

`promoteUpcomingGoal` and `promoteNextUpcomingUnlocked` set
`status: "active"` and wrote the record without running
`applyBudgetDrivenStatus`. A previously-active goal that was demoted
back into upcoming may already have exhausted limits
(cost >= budget or turnsUsed >= turnCap), so re-promoting it would
let the user fire one extra send before the next chunk's accounting
flipped it back to `budget_limited`.

Fix: run `applyBudgetDrivenStatus` on the parsed record before
persisting. Matches `createGoal` (which has the same conditional
for new active goals). Both promote paths benefit because
`maybeAutoPromoteOnComplete` and `runDeferredAutoPromoteAfterStreamEnd`
delegate to `promoteNextUpcomingUnlocked`.

---

_Generated with `mux` • Model: `anthropic:claude-opus-4-7` • Thinking: `max` • Cost: `$55.32`_

<!-- mux-attribution: model=anthropic:claude-opus-4-7 thinking=max costs=55.32 -->
…mpleted goals (Codex P3)

The 'Archive this goal' link for completed goals called `clearGoal()`,
which records the goal as 'completed' in `goal-history.jsonl`. After
the goal-board work, the renderer surfaces those entries under the
Completed section — not Archived. Label and behavior were
contradicting.

Fix: route the click to `workspace.archiveGoal` when the goal is
lifecycle-complete; everything else still uses `clearGoal()`. The
backend's `archiveGoal` handles the active-goal source path
(append history under 'cleared' end-reason + write to archived),
and the board's existing dedup keeps it in the Archived section
only.

Test updated to assert the new `Archive goal` aria-label.

---

_Generated with `mux` • Model: `anthropic:claude-opus-4-7` • Thinking: `max` • Cost: `$55.32`_

<!-- mux-attribution: model=anthropic:claude-opus-4-7 thinking=max costs=55.32 -->
Adding `useAPI()` directly in GoalTab (for the archive-on-complete
handler) broke storybook smoke tests — the stories render the
GoalTab in isolation without an APIProvider, and `useAPI` throws
when the context is null.

Fix: mirror the same opt-in pattern already used by `useGoalDefaults`
and `useGoalBoard` — read `APIContext` directly with `useContext`,
fall back to a null `api`, and have the archive handler short-circuit
when `api` isn't available. Same defensive posture, same code path.

---

_Generated with `mux` • Model: `anthropic:claude-opus-4-7` • Thinking: `max` • Cost: `$55.32`_

<!-- mux-attribution: model=anthropic:claude-opus-4-7 thinking=max costs=55.32 -->
When an archived goal that was originally completed gets revived and
later promoted, the promote paths spread the goal's existing fields
into the new active record — including `completionSummary`. The
agent's `get_goal` reads `goal.json` directly and would see the
stale 'done' message, potentially confusing the next turn.

Fix: explicitly set `completionSummary: undefined` in both promote
paths (`promoteUpcomingGoal` and `promoteNextUpcomingUnlocked`) so
the active record always lands with a clean summary. Matches the
existing `completionSummaryPatch` invariant in `applyMutableFields`
which clears the summary for any non-complete status transition.

---

_Generated with `mux` • Model: `anthropic:claude-opus-4-7` • Thinking: `max` • Cost: `$55.32`_

<!-- mux-attribution: model=anthropic:claude-opus-4-7 thinking=max costs=55.32 -->
Child task workspaces (those with `parentWorkspaceId`) can't run any
goal action — `WorkspaceGoalService.assertParentWorkspace` rejects
every write (setGoal / clearGoal / setGoalDefaults / all board ops).
Before this change the GOALS experiment unconditionally injected the
Goal tab on every workspace, including child tasks, so users on a
child workspace would see the create / queue form whose submits
silently failed (`child_workspace` typed error).

Fix: read `parentWorkspaceId` from `useWorkspaceMetadata` in
RightSidebar and gate Goal tab insertion on it being null. Existing
'remove tab' branch still runs for non-experiment workspaces; child
workspaces with the experiment now also fall through removeTab if
the tab was previously inserted (handles the case where the user
turned the experiment on at a parent, then jumped to a child).

---

_Generated with `mux` • Model: `anthropic:claude-opus-4-7` • Thinking: `max` • Cost: `$55.32`_

<!-- mux-attribution: model=anthropic:claude-opus-4-7 thinking=max costs=55.32 -->
…x P2)

A goal that was completed → archived → revived → promoted →
completed-again has BOTH a stale 'completed' history entry AND is the
currently-active goal. The previous archiveGoal source order
(archived → upcoming → history → active) hit the history branch
first, snapshotted the stale row, and returned — leaving the live
active goal in place. Result: archive appeared to silently fail
for users who had revived a previously-completed goal.

Fix: move the active-slot branch to the top of the source-priority
ladder (after the already-archived idempotent check). The current
ACTIVE record is used for the archived snapshot, so we get the
latest accounting rather than a stale history row.

All 111 service tests pass; static check passes.

---

_Generated with `mux` • Model: `anthropic:claude-opus-4-7` • Thinking: `max` • Cost: `$55.32`_

<!-- mux-attribution: model=anthropic:claude-opus-4-7 thinking=max costs=55.32 -->
A goal completed → archived → revived → promoted → completed-again
has TWO 'completed' rows in goal-history.jsonl with the same id.
The board's Completed section was rendering both, surfacing the goal
as two cards.

Fix: in `getGoalBoard`, track seen goalIds in a `Set<string>` while
iterating the (already newest-first sorted) history list, and skip
duplicates. We keep only the most recent 'completed' row per
goalId. The other dedup passes (active / archived / upcoming) are
unchanged.

---

_Generated with `mux` • Model: `anthropic:claude-opus-4-7` • Thinking: `max` • Cost: `$55.32`_

<!-- mux-attribution: model=anthropic:claude-opus-4-7 thinking=max costs=55.32 -->
@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…inline edit

Three UX improvements to the multi-goal queue:

1. **Promote mid-stream**: User can now promote a queued goal even while
   the current goal is streaming. The active stream is interrupted first
   (via `WorkspaceService.interruptStream` wired into the goal service
   as `streamInterrupter`), then promotion proceeds under the file lock.
   This preserves cost attribution — the in-flight turn's tokens belong
   to the goal that ran them; the next user message starts fresh under
   the promoted goal.

2. **Surfaced backend errors**: `WorkspaceGoalTransitionError` /
   `WorkspaceGoalChildWorkspaceError` are translated to `ORPCError`
   `BAD_REQUEST` at the router boundary so the message reaches the
   renderer instead of being collapsed to a generic "Internal server
   error". The GoalBoardSections error banner now shows the real
   reason a click had no effect.

3. **Inline edit + DnD reorder**: Upcoming rows expose a Pencil button
   that opens an inline objective+budget editor (new
   `updateUpcomingGoal` endpoint), and reordering uses `@dnd-kit`
   (same pattern as Settings → Providers route-priority list) instead
   of up/down arrow buttons. The "Archive" action is relabeled
   "Remove" to better match the user's mental model.

Backend additions:
- `WorkspaceGoalService.setStreamInterrupter` + `waitForStreamSettled`
- `WorkspaceGoalService.updateUpcomingGoal`
- `GoalBoardUpdateUpcomingInputSchema` + `workspace.updateUpcomingGoal`
- Router-side `withGoalErrorTranslation` wrapper for all 7 board handlers
- `coreServices.ts` wires the interrupter via `workspaceService.interruptStream`

5 new service tests (116 pass total).

---

_Generated with `mux` • Model: `anthropic:claude-opus-4-7` • Thinking: `xhigh` • Cost: `$179.68`_

<!-- mux-attribution: model=anthropic:claude-opus-4-7 thinking=xhigh costs=179.68 -->
@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

Follow-up to address three reviewer points:

  1. Promote mid-stream: removed the WorkspaceGoalTransitionError throw and instead interrupt the active stream (via wired interruptStream callback) before promoting. Preserves cost attribution since the in-flight turn's tokens stay with the goal that ran them. Falls open if interrupter is missing or errors.

  2. Frontend error surface: router translates WorkspaceGoalTransitionError / WorkspaceGoalChildWorkspaceError to ORPCError("BAD_REQUEST", { message }) so the real reason reaches the user instead of "Internal server error".

  3. DnD reorder + inline edit: replaced up/down arrows with @dnd-kit/sortable grip handle (mirrors ProvidersSection). New workspace.updateUpcomingGoal endpoint backs an inline editor on Upcoming rows. "Archive" relabeled "Remove".

5 new service tests (116 total pass).

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 261e3734d5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/workspaceGoalService.ts
… snapshot (Codex P2)

When an inline objective edit arrives while the workspace is streaming
and `editInPlace: true`, the projected snapshot published to the UI was
built via `createGoal(...)`, which minted a fresh goalId + zeroed cost/
turns + cleared budget/turnCap until stream end drained the queued
mutation. The persisted mutation renames in place, so the panel briefly
showed inconsistent accounting that disappeared on drain.

Fix: overlay the rename onto the current record (preserving goalId,
costCents, turnsUsed, attributedChildren, budgetCents, turnCap) for
the editInPlace branch only. Non-editInPlace branches (replace / new
goal) keep `createGoal` semantics.

Test: new "mid-stream editInPlace rename returns an optimistic snapshot
that preserves goalId + accounting" guards the projection. 117 service
tests pass.

---

_Generated with `mux` • Model: `anthropic:claude-opus-4-7` • Thinking: `xhigh` • Cost: `$179.68`_

<!-- mux-attribution: model=anthropic:claude-opus-4-7 thinking=xhigh costs=179.68 -->
@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

Addressed the P2 on mid-stream editInPlace optimistic snapshot:

setGoalInternal now overlays the rename onto the current record (preserving goalId, costCents, turnsUsed, attributedChildren, budgetCents, turnCap) for the editInPlace branch. Non-editInPlace branches still take the createGoal path. New test guards the projection.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5b3de54041

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/workspaceGoalService.ts Outdated
…+ project drain semantics on mid-stream rename (Codex P2)

Two goal-lifecycle hygiene fixes:

1. **Hide "Goal cleared" synthetic messages from the chat transcript.**
   `appendClearSummary` had been persisting an assistant-role synthetic
   message with `uiVisible: true` whenever a goal was cleared, replaced,
   or auto-promoted on complete. The renderer stripped the `Goal cleared:`
   prefix but still showed the rest as a full assistant chat bubble
   ("X" — spent $0.00 over N turns (status: paused)) — pure noise, since
   the right-sidebar Goal Board already surfaces the same information in
   its Completed / Archived sections. Drop `uiVisible: true` so the
   message stays in the AI request payload (the model still knows the
   previous goal was cleared) but no longer renders as a chat bubble.
   Updated `workspaceGoalService.test.ts` assertion accordingly. The
   `StreamingMessageAggregator` legacy-prefix strip stays — it still
   handles older chat.jsonl entries that have `uiVisible: true`.

2. **Mid-stream editInPlace optimistic snapshot mirrors drain semantics.**
   Codex P2 follow-up: when a rename arrives mid-stream and also
   tightens `budgetCents` below the already-accrued cost, the previous
   projection applied raw mutable fields, so the UI would show an
   `active` snapshot until stream end while the eventual persisted record
   immediately becomes `budget_limited`. Project through
   `applyMutableFields` (which itself ends in `applyBudgetDrivenStatus`)
   so the optimistic state matches the drain state. Added a guarding
   test that asserts a rename + tightening budget projects directly to
   `budget_limited`.

118 service tests pass.

---

_Generated with `mux` • Model: `anthropic:claude-opus-4-7` • Thinking: `xhigh` • Cost: `$179.68`_

<!-- mux-attribution: model=anthropic:claude-opus-4-7 thinking=xhigh costs=179.68 -->
@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

Addressed both items in one push:

  1. P2 mutable-field normalization on editInPlace optimistic snapshot: projection now flows through applyMutableFields (→ applyBudgetDrivenStatus), mirroring the drain. New test asserts that a mid-stream rename + tightening budgetCents projects directly to budget_limited instead of a stale active.

  2. Chat-transcript noise from synthetic "Goal cleared" messages: appendClearSummary no longer sets uiVisible: true, so the persisted summary still reaches the model (for "what did we just finish" follow-ups) but no longer renders as a full assistant chat bubble. The right-sidebar Goal Board's Completed/Archived sections already surface the same info visually. User noticed this as "random goal updates in chat transcript".

118 service tests pass; static-check green.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 354c21f175

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/browser/features/RightSidebar/GoalTab.tsx Outdated
…eExplicitBudget (Codex P2)

When `alwaysRequireExplicitBudget` is OFF in goal defaults, blank budget
input is intentionally resolved to `null` (no budget) by
`resolveGoalSetIntent`, not to `defaultBudgetCents`. But both the empty-
state create form (`GoalTab.tsx`) and the queued-goal Adder
(`GoalBoardSections.tsx`) were advertising "$X (default)" as the
placeholder unconditionally, and the helper text said blank uses the
shown defaults — so users with that setting off could submit a blank
budget expecting the default to apply when in fact an unbudgeted goal
would be created.

Fix: gate the placeholder + helper text on `defaults.alwaysRequireExplicitBudget`.

- Placeholder when ON: `$X (default)`
- Placeholder when OFF: `no budget (default)`
- Helper text adjusts to match: when ON, the existing "Leave Budget /
  Turn cap blank to use the defaults shown above" wording is correct;
  when OFF, the text now clarifies that blank Budget creates an
  unbudgeted goal while blank Turn cap still uses the default.

---

_Generated with `mux` • Model: `anthropic:claude-opus-4-7` • Thinking: `xhigh` • Cost: `$179.68`_

<!-- mux-attribution: model=anthropic:claude-opus-4-7 thinking=xhigh costs=179.68 -->
@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

Addressed P2 on budget placeholder/helper text. Both GoalTab create form and GoalBoardSections UpcomingAdder now gate placeholder + helper text on defaults.alwaysRequireExplicitBudget:

  • ON → $X (default) placeholder + "Leave Budget / Turn cap blank to use the defaults shown above"
  • OFF → no budget (default) placeholder + "Leave Budget blank to create an unbudgeted goal; Turn cap blank uses the default shown above"

This matches resolveGoalSetIntent's actual behavior.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a78e245e91

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/browser/features/RightSidebar/GoalBoardSections.tsx Outdated
…dex P2)

The Upcoming Adder only rendered objective + budget fields, so
`resolveGoalSetIntent` silently inherited the workspace/global
`defaultTurnCap` for every queued goal — users couldn't see or override
the turn cap before queuing, and an auto-promoted goal could hit an
unexpected turn limit.

Added an explicit turn-cap field to both:

- `UpcomingAdder` (the "Queue another goal" form): three-column row of
  budget / turn-cap / Queue button. Placeholder uses the same
  default-aware pattern as the budget field — "no cap (default)" when
  the workspace has no default turn cap, "N turns (default)" otherwise.
  Explicit blank still falls through `resolveGoalSetIntent` (which then
  applies the default); explicit non-positive integers surface a form
  error.

- `UpcomingRowEditor` (the inline edit form on existing queued rows):
  new turn-cap input alongside budget, with the same blank-means-no-cap
  semantics the budget field already uses on this surface (an existing
  goal's defaults were resolved when it was queued, so blank-on-edit
  is "clear the cap", not "re-apply the default").

Plumbing: `update` callback in `UpcomingSection` and `onSubmit` shape
on `UpcomingRowEditorProps` now carry `turnCap`. The
`workspace.updateUpcomingGoal` endpoint already accepted `turnCap`.

---

_Generated with `mux` • Model: `anthropic:claude-opus-4-7` • Thinking: `xhigh` • Cost: `$179.68`_

<!-- mux-attribution: model=anthropic:claude-opus-4-7 thinking=xhigh costs=179.68 -->
@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

Addressed P2 on hidden default turn cap. Added an explicit turn-cap field to both the queue Adder (UpcomingAdder) and the inline editor (UpcomingRowEditor), with default-aware placeholders matching the budget field's pattern. Blank in the Adder still falls through resolveGoalSetIntent (applies default); blank in the Editor means "no cap" (the default was already resolved at queue time, identical to the budget field's existing semantics).

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 626414bbbd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/browser/features/RightSidebar/GoalBoardSections.tsx Outdated
Comment thread src/browser/features/RightSidebar/GoalDefaultsSection.tsx Outdated
…ace-default saves (Codex P2)

Two follow-ups:

1. **Reject partial turn-cap strings.** The new turn-cap inputs in the
   Upcoming Adder and the inline UpcomingRowEditor used
   `Number.parseInt`, which silently truncates inputs like `1.5` → 1
   and `12abc` → 12. The user would type one value and the queued goal
   would persist a different cap.

   Extracted `parseGoalTurnCapInput` (full-string `Number` +
   `Number.isSafeInteger` validation) to `src/common/utils/goals/
   budgetParser.ts` so every entry point — slash command, GoalTab inline
   editor, Adder, inline editor — validates the same way.
   `GoalTab.tsx`'s local `parseTurnCapInput` is now an alias for the
   shared canonical parser (eliminates the duplicated definition that
   the Codex note flagged as drifting risk).

2. **Serialize workspace-default saves.** Codex P2: rapid edits to two
   workspace defaults fired multiple concurrent full-record `set` calls
   to `workspace.goalDefaults.set`. The backend reads → mutates →
   writes the full config record per request, so a slower first
   request could complete after a later edit and overwrite it on disk
   while the UI showed the optimistic latest state — silent reverts
   on next refetch or restart.

   Fix in `GoalDefaultsSection.tsx`: chain saves through a
   `useRef<Promise<void>>` so each request awaits the previous before
   issuing its own write. The optimistic local `setOverride` still fires
   immediately so the UI feels snappy, but on-disk state is now
   guaranteed to match the user's final edit.

---

_Generated with `mux` • Model: `anthropic:claude-opus-4-7` • Thinking: `xhigh` • Cost: `$179.68`_

<!-- mux-attribution: model=anthropic:claude-opus-4-7 thinking=xhigh costs=179.68 -->
@ammar-agent
Copy link
Copy Markdown
Collaborator Author

@codex review

Addressed both P2s:

  1. Reject partial turn-cap strings: extracted parseGoalTurnCapInput (full-string Number + Number.isSafeInteger) to src/common/utils/goals/budgetParser.ts and routed both the Adder and the inline editor through it. GoalTab.tsx's parseTurnCapInput is now an alias for the same shared parser. 1.5 / 12abc now correctly fail.

  2. Serialize workspace-default saves: chain saves through a useRef<Promise<void>> in GoalDefaultsSection.tsx so each set request awaits the previous before issuing its own write. Local optimistic state still updates immediately so the UI feels snappy; on-disk state is guaranteed to match the final edit.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Chef's kiss.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@ammario ammario merged commit 4f519e0 into main May 18, 2026
40 of 42 checks passed
@ammario ammario deleted the goals-ui-qfdv branch May 18, 2026 17:16
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.

2 participants