Skip to content

Comments

Fix: Restore views properly, model selection for commit and pr and speed up some cli models with session resume#801

Merged
gsxdsm merged 4 commits intoAutoMaker-Org:v0.15.0rcfrom
gsxdsm:fix/restoring-view
Feb 22, 2026
Merged

Fix: Restore views properly, model selection for commit and pr and speed up some cli models with session resume#801
gsxdsm merged 4 commits intoAutoMaker-Org:v0.15.0rcfrom
gsxdsm:fix/restoring-view

Conversation

@gsxdsm
Copy link
Collaborator

@gsxdsm gsxdsm commented Feb 22, 2026

Summary

This PR fixes the issue where the selected view/worktree was not being restored after app refresh, and adds several related improvements:

  • Fix worktree view restoration: The UI now correctly persists and restores the selected feature worktree (not just the main branch) across page refreshes, PWA memory eviction, and tab discards. The previous implementation dropped all non-null worktree paths from the cache as a crash-loop prevention measure; this PR replaces that blunt sanitization with runtime validation in use-worktrees.ts that gracefully resets to the main branch if a cached worktree no longer exists.
  • Fix agent model selection persistence: The selected AI model in the Agent view is now persisted per-session and restored when switching back to a previous session or after a refresh. Model selection state is lifted into useAgentSession and stored in the app store (agentModelBySession).
  • Fix board-view worktree reactivity: currentWorktreeInfo now subscribes directly to the store slice instead of calling a getter function, so worktree switches immediately re-render the kanban/list without requiring an extra render cycle.
  • Add prDescriptionModel phase model: Adds a dedicated configurable model for PR description generation (defaults to claude-sonnet), alongside the existing commitMessageModel.
  • Expose model override UI for commit messages and PR descriptions: Both the commit worktree dialog and the create-PR dialog now include a ModelOverrideTrigger so users can change the model used for AI generation on the fly, and a regenerate button in the commit dialog.
  • Refactor commit message generation: Moves from an Electron-IPC-only path to the HTTP API client, making it work in web mode as well. The generation function is now extracted into a reusable generateCommitMessage callback.
  • Add session resume support for all CLI providers: Codex, Copilot, Cursor, and Gemini providers now forward sdkSessionId to their respective resume/--resume flags so multi-turn agent conversations continue in the same provider session across Automaker turns. The AgentExecutor tracks the live session_id emitted by the stream and threads it through all sub-queries.

Test plan

  • Refresh the page while a feature worktree is selected — verify the board restores to the same worktree instead of resetting to main branch
  • Delete a worktree, then refresh — verify the board gracefully falls back to the main branch without a crash loop
  • Open the Agent view, select a session, change the model, switch away and back — verify the model selection is restored
  • Open the Commit dialog with AI commit messages enabled — verify the model override trigger and regenerate button are present and functional
  • Open the Create PR dialog — verify the model override trigger works for description generation
  • Run server unit tests: npm run test:server -- tests/unit/providers/codex-provider.test.ts tests/unit/providers/copilot-provider.test.ts tests/unit/services/agent-service.test.ts
  • Run all tests: npm run test:all

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Session resumption across multiple AI providers (resume via sdkSessionId)
    • Per-session agent model persistence and restoration (modelSelection persisted per session)
    • AI-driven commit message generation with Regenerate button and commit/PR model override
    • HTTP API and client methods accept model/thinkingLevel/provider for AI generation
    • Broader worktree persistence/restoration for main and feature worktrees
  • Bug Fixes

    • Improved discard flow: handle renamed paths and stronger untracked-file removal
  • Tests

    • Added unit tests covering resume flows and CLI argument construction for providers

@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

📝 Walkthrough

Walkthrough

Adds optional sdkSessionId-driven session resumption across server providers and agent execution; introduces per-session model persistence and model-override UI controls; broadens worktree cache acceptance and updates related HTTP/electron API signatures and tests.

Changes

Cohort / File(s) Summary
Server: Session resume & providers
apps/server/src/providers/codex-provider.ts, apps/server/src/providers/copilot-provider.ts, apps/server/src/providers/cursor-provider.ts, apps/server/src/providers/gemini-provider.ts
Introduce resume-aware flows controlled by optional sdkSessionId: Codex uses exec+resume path and disables schema/image steps; Copilot attempts resumeSession before createSession; Cursor/Gemini append --resume <id> to CLI args.
Server: Agent execution & types
apps/server/src/services/agent-executor-types.ts, apps/server/src/services/agent-executor.ts, apps/server/src/services/agent-service.ts
Add sdkSessionId to agent options and propagate/update it across streaming/per-task flows; conversationHistory construction moved later and included conditionally in ExecuteOptions.
Server: Provider tests
apps/server/tests/unit/providers/codex-provider.test.ts, apps/server/tests/unit/providers/copilot-provider.test.ts, apps/server/tests/unit/providers/cursor-provider.test.ts, apps/server/tests/unit/providers/gemini-provider.test.ts
Add unit tests verifying resume behavior: Codex exec/resume args, Copilot resume vs fallback createSession, Cursor/Gemini CLI --resume handling.
Server: Agent-service tests
apps/server/tests/unit/services/agent-service.test.ts
Adds test ensuring Gemini provider path triggers context/history preparation and that options are propagated.
UI: Per-session model persistence & store
apps/ui/src/store/app-store.ts, apps/ui/src/store/types/state-types.ts, libs/types/src/settings.ts, apps/ui/src/hooks/use-settings-sync.ts
Add agentModelBySession state, getters/setters, sync/migration and size cap; include field in settings sync and migration.
UI: useAgentSession hook & AgentView
apps/ui/src/components/views/agent-view/hooks/use-agent-session.ts, apps/ui/src/components/views/agent-view.tsx
Expose modelSelection and setModelSelection from the session hook; persist per-session model selection and remove local modelSelection state from AgentView.
UI: Model-override & dialogs
apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx, apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx
Add ModelOverrideTrigger/useModelOverride; wire model/thinkingLevel/provider override into commit/PR generation flows; add regenerate UI and stable generation ref handling using HTTP API.
UI: HTTP client & electron types
apps/ui/src/lib/http-api-client.ts, apps/ui/src/types/electron.d.ts
Extend generateCommitMessage and generatePRDescription signatures to accept optional model, thinkingLevel, providerId and include them in request payloads.
UI: Worktree cache & utils
apps/ui/src/lib/settings-utils.ts, apps/ui/src/store/ui-cache-store.ts, apps/ui/src/components/views/board-view.tsx
Broaden sanitization/restore to accept both main (null path) and feature (string path) worktrees; switch BoardView to selector-based currentWorktree lookup (currentWorktreeByProject).
Misc: Discard changes logic
apps/server/src/routes/worktree/routes/discard-changes.ts
Use git clean -ffd and add path parsing for renamed entries; treat unmatched requested files as untracked for removal; adjust related parsing and messages.
Misc: Tests & small updates
apps/server/tests/unit/providers/*, apps/ui/tests/*
Add/adjust unit tests for resume/session flows, model restoration, and provider argument construction.

Sequence Diagram(s)

sequenceDiagram
  participant UI as Client/UI
  participant Agent as AgentService
  participant Provider as Provider (Copilot/Codex/Gemini)
  participant SDK as External SDK / CLI

  UI->>Agent: Execute request (options, sdkSessionId?)
  Agent->>Provider: executeQuery(options with sdkSessionId)
  Provider->>SDK: resolve model / build prompt
  alt sdkSessionId provided
    Provider->>SDK: attempt resume (resumeSession / exec resume)
    alt resume succeeds
      SDK-->>Provider: resumed session stream
    else resume fails
      SDK-->>Provider: resume error
      Provider->>SDK: createSession / exec (normal)
      SDK-->>Provider: new session stream
    end
  else no sdkSessionId
    Provider->>SDK: createSession / exec (normal)
    SDK-->>Provider: new session stream
  end
  Provider-->>Agent: forward stream events (include session_id)
  Agent-->>UI: aggregated streaming responses (updates sdkSessionId as received)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

Enhancement, scope: ui, Refactor, Testers-Requested

Poem

🐰 I hopped through code to save the thread,

Sessions resumed where conversations led,
Models tucked per-chat, prompts anew,
Commit notes sing with AI in view,
Tiny rabbit cheers — persistent and fled.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: view restoration, model selection persistence for commit/PR, and session resume for CLI models.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share

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

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @gsxdsm, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the user experience by improving the persistence and reactivity of several key UI elements. It ensures that user selections for worktrees and AI models are reliably restored across sessions and refreshes, preventing data loss and improving workflow continuity. Additionally, it introduces greater flexibility in AI model selection for code generation tasks and streamlines the underlying architecture for AI-powered features, making them more robust and accessible in different environments.

Highlights

  • Worktree View Restoration: The UI now correctly persists and restores the selected feature worktree across page refreshes, PWA memory eviction, and tab discards. Runtime validation gracefully resets to the main branch if a cached worktree no longer exists, preventing crash loops.
  • Agent Model Selection Persistence: The selected AI model in the Agent view is now persisted per-session and restored when switching back to a previous session or after a refresh, with state lifted into useAgentSession and stored in the app store.
  • Board-View Worktree Reactivity: currentWorktreeInfo now directly subscribes to the store slice, ensuring immediate re-rendering of kanban/list views upon worktree switches without extra render cycles.
  • Dedicated PR Description Model: A new configurable prDescriptionModel (defaulting to claude-sonnet) has been added for generating pull request descriptions, alongside the existing commitMessageModel.
  • Model Override UI for AI Generation: Both the commit worktree dialog and the create-PR dialog now include a ModelOverrideTrigger and a regenerate button, allowing users to change the AI model used for commit message and PR description generation on the fly.
  • Refactored Commit Message Generation: Commit message generation has been moved from an Electron-IPC-only path to the HTTP API client, enabling it to work in web mode and extracting the generation function into a reusable callback.
  • CLI Provider Session Resume Support: Codex, Copilot, Cursor, and Gemini providers now support session resumption by forwarding sdkSessionId to their respective CLI flags, allowing multi-turn agent conversations to continue in the same provider session across Automaker turns.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • apps/server/src/providers/codex-provider.ts
    • Added CODEX_RESUME_SUBCOMMAND constant.
    • Modified executeQuery to conditionally write output schema and add directories based on sdkSessionId.
    • Updated executeQuery to use exec resume subcommand when sdkSessionId is provided.
  • apps/server/src/providers/copilot-provider.ts
    • Added types for CopilotSession and ResumableCopilotClient to support session resumption.
    • Refactored executeQuery to attempt resuming an existing Copilot session using sdkSessionId or create a new one if resumption fails.
  • apps/server/src/providers/cursor-provider.ts
    • Added --resume CLI argument to buildCliArgs when sdkSessionId is present.
  • apps/server/src/providers/gemini-provider.ts
    • Added --resume CLI argument to buildCliArgs when sdkSessionId is present.
  • apps/server/src/services/agent-executor-types.ts
    • Added sdkSessionId property to AgentExecutionOptions interface.
  • apps/server/src/services/agent-executor.ts
    • Propagated sdkSessionId through AgentExecutor options.
    • Updated sdkSessionId in execution options if a new session ID is received from the provider stream.
  • apps/server/src/services/agent-service.ts
    • Removed redundant conversation history mapping before adding the current message.
    • Introduced combinedSystemPrompt variable for better prompt management.
    • Modified conversation history extraction to slice messages before the current one and filter empty content.
    • Passed sdkSessionId to ExecuteOptions for provider calls.
  • apps/server/tests/unit/providers/codex-provider.test.ts
    • Added a test case to verify codex-provider uses exec resume and passes sdkSessionId when provided.
  • apps/server/tests/unit/providers/copilot-provider.test.ts
    • Added mocks for createSession and resumeSession functions of CopilotClient.
    • Introduced test cases to verify CopilotProvider uses resumeSession when sdkSessionId is provided and falls back to createSession if resumption fails.
  • apps/server/tests/unit/providers/cursor-provider.test.ts
    • Added a new test file to verify CursorProvider includes the --resume flag when sdkSessionId is provided.
  • apps/server/tests/unit/providers/gemini-provider.test.ts
    • Added a new test file to verify GeminiProvider includes the --resume flag when sdkSessionId is provided.
  • apps/server/tests/unit/services/agent-service.test.ts
    • Added a test case to ensure context and history preparation are included for Gemini requests.
  • apps/ui/src/components/views/agent-view.tsx
    • Removed local modelSelection state and integrated model persistence directly into useAgentSession.
  • apps/ui/src/components/views/agent-view/hooks/use-agent-session.ts
    • Added modelSelection and setModelSelection to the hook's return value and state.
    • Implemented logic to persist and restore agent model selection per session using setAgentModelForSession and getAgentModelForSession from the app store.
    • Improved useEffect logic to handle worktree/project changes by resetting session and model selection state.
  • apps/ui/src/components/views/board-view.tsx
    • Removed getCurrentWorktree from useAppStore selectors.
    • Modified currentWorktreeInfo to directly subscribe to the currentWorktreeByProject slice in the app store for improved reactivity.
  • apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx
    • Imported RefreshCw icon and ModelOverrideTrigger and useModelOverride components.
    • Integrated useModelOverride for commit message model selection.
    • Refactored generateCommitMessage to use getHttpApiClient instead of getElectronAPI and accept model parameters.
    • Added a 'Regenerate' button and ModelOverrideTrigger to the commit message section.
  • apps/ui/src/components/views/board-view/dialogs/create-pr-dialog.tsx
    • Imported ModelOverrideTrigger and useModelOverride components.
    • Integrated useModelOverride for PR description model selection.
    • Updated handleGenerateDescription to pass model override parameters to generatePRDescription API call.
    • Added ModelOverrideTrigger to the PR title/description generation section.
  • apps/ui/src/lib/http-api-client.ts
    • Updated generateCommitMessage method to accept optional model, thinkingLevel, and providerId parameters.
    • Updated generatePRDescription method to accept optional model, thinkingLevel, and providerId parameters.
  • apps/ui/src/lib/settings-utils.ts
    • Modified sanitizeWorktreeByProject to allow persistence of both null (main branch) and non-null (feature worktree) paths, validating only the object structure.
  • apps/ui/src/store/app-store.ts
    • Added agentModelBySession to the initial state.
    • Implemented setAgentModelForSession and getAgentModelForSession actions to manage agent model selections per session.
  • apps/ui/src/store/types/state-types.ts
    • Added agentModelBySession property to AppState interface.
    • Added setAgentModelForSession and getAgentModelForSession methods to AppActions interface.
  • apps/ui/src/store/ui-cache-store.ts
    • Modified syncUICache to persist all valid currentWorktreeByProject entries, including feature worktrees.
    • Modified restoreFromUICache to restore all valid currentWorktreeByProject entries, relying on runtime validation in use-worktrees.ts for deleted worktrees.
  • libs/types/src/settings.ts
    • Added prDescriptionModel property to PhaseModelConfig interface.
    • Set prDescriptionModel to claude-sonnet as its default value in DEFAULT_PHASE_MODELS.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces several improvements to view restoration, model selection persistence, and session resume support for CLI providers. The refactoring of commit message generation to use the HTTP API client enables better support for web mode. The addition of session resume for Codex, Copilot, Cursor, and Gemini is a significant performance optimization for multi-turn conversations. Feedback is provided regarding potential issues with image handling and schema validation during resumed Codex sessions, as well as redundant context transmission.

Comment on lines +798 to +800
const schemaPath = isResumeQuery
? null
: await writeOutputSchemaFile(options.cwd, options.outputFormat);
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Skipping the output schema file generation when isResumeQuery is true might cause issues if a resumed turn still requires structured JSON output (e.g., during an interrupted feature generation task). If the CLI expects the --output-schema flag to enforce the response format on every turn, omitting it could lead to unparseable output.

Comment on lines 839 to 853
if (
!isResumeQuery &&
codexSettings.additionalDirs &&
codexSettings.additionalDirs.length > 0
) {
for (const dir of codexSettings.additionalDirs) {
preExecArgs.push(CODEX_ADD_DIR_FLAG, dir);
}
}

// If images were written to disk, add the image directory so the CLI can access them
if (imagePaths.length > 0) {
if (!isResumeQuery && imagePaths.length > 0) {
const imageDir = path.join(options.cwd, CODEX_INSTRUCTIONS_DIR, IMAGE_TEMP_DIR);
preExecArgs.push(CODEX_ADD_DIR_FLAG, imageDir);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The !isResumeQuery check prevents adding the image directory and additional directories to the CLI arguments when resuming a session. If a follow-up turn in a multi-turn conversation includes new images or requires access to these directories, the Codex CLI may fail to access them because the --add-dir flags are omitted. Consider including these flags even for resumed queries if the CLI is stateless regarding directory permissions.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/server/src/providers/copilot-provider.ts (1)

567-597: ⚠️ Potential issue | 🟠 Major

Resolve model aliases before Copilot SDK calls.

Line 568 passes bareModel directly into the SDK. Per repo rules, model aliases must be normalized via resolveModelString() before API calls. This avoids alias-only values like “sonnet” being sent to the SDK.

🛠️ Suggested fix
+import { resolveModelString } from '@automaker/model-resolver';
...
-    const bareModel = options.model || DEFAULT_BARE_MODEL;
+    const bareModel = options.model || DEFAULT_BARE_MODEL;
+    const resolvedModel = resolveModelString(bareModel);
...
-      const sessionOptions: CopilotSessionOptions = {
-        model: bareModel,
+      const sessionOptions: CopilotSessionOptions = {
+        model: resolvedModel,

As per coding guidelines: "Use resolveModelString() from @automaker/model-resolver to convert model aliases (haiku, sonnet, opus) to full model names before making API calls."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/providers/copilot-provider.ts` around lines 567 - 597, The
Copilot SDK is receiving the un-normalized model alias in sessionOptions (see
bareModel used when constructing CopilotSessionOptions and passed to
client.createSession / resumableClient.resumeSession); call resolveModelString()
from `@automaker/model-resolver` on bareModel and use the returned normalized
model string in the sessionOptions.model before any SDK calls (both when
creating a new session and when resuming via resumeSession when
options.sdkSessionId is present).
apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx (2)

594-601: ⚠️ Potential issue | 🟡 Minor

handleOpenChange should also block closure during active AI generation.

The Cancel button is correctly disabled while isGenerating, but overlay clicks and the Escape key route through handleOpenChange, which only guards against isPushing. Closing mid-generation allows the in-flight API call to complete and attempt to update state (setMessage) after the dialog is closed.

🔧 Suggested fix
   const handleOpenChange = (nextOpen: boolean) => {
-    if (!nextOpen && isPushing) {
+    if (!nextOpen && (isPushing || isGenerating)) {
       return;
     }
     onOpenChange(nextOpen);
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx`
around lines 594 - 601, handleOpenChange currently prevents closing only when
isPushing; extend its guard to also block closure while isGenerating so overlay
clicks/Escape don't close the dialog mid-generation. In the handleOpenChange
function (referencing handleOpenChange, isPushing, isGenerating, and
onOpenChange), return early when either isPushing or isGenerating is true and
only call onOpenChange(nextOpen) when neither is active; this keeps the UI open
until generation completes and prevents in-flight API callbacks (e.g.,
setMessage) from running after the dialog is closed.

541-586: ⚠️ Potential issue | 🟠 Major

Object identity instability in useModelOverride causes repeated API calls.

useModelOverride returns a new object literal on every render, even though its individual properties (effectiveModelEntry, effectiveModel) are memoized. Including commitModelOverride in the useCallback dependency array makes generateCommitMessage unstable. Since generateCommitMessage is also a dependency of the useEffect below, the effect re-fires on every render while open && worktree && enableAiCommitMessages are truthy — calling generateCommitMessage() (and thus the API) repeatedly. Each re-run also calls setMessage('') first, wiping any in-progress result.

Extract the stable memoized values from commitModelOverride so generateCommitMessage only changes when actual model selection changes:

Suggested fix
+ const {
+   effectiveModel: commitEffectiveModel,
+   effectiveModelEntry: commitEffectiveModelEntry,
+ } = commitModelOverride;
+
  const generateCommitMessage = useCallback(async () => {
    if (!worktree) return;
    setIsGenerating(true);
    try {
      const api = getHttpApiClient();
      const result = await api.worktree.generateCommitMessage(
        worktree.path,
-       commitModelOverride.effectiveModel,
-       commitModelOverride.effectiveModelEntry.thinkingLevel,
-       commitModelOverride.effectiveModelEntry.providerId
+       commitEffectiveModel,
+       commitEffectiveModelEntry.thinkingLevel,
+       commitEffectiveModelEntry.providerId
      );
      // ...
    }
-  }, [worktree, commitModelOverride]);
+  }, [worktree, commitEffectiveModel, commitEffectiveModelEntry]);

Also, prevent dialog close via Escape or overlay click during generation by updating handleOpenChange to guard isGenerating:

  const handleOpenChange = (nextOpen: boolean) => {
-   if (!nextOpen && isPushing) {
+   if (!nextOpen && (isPushing || isGenerating)) {
      return;
    }
    onOpenChange(nextOpen);
  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx`
around lines 541 - 586, The commit dialog repeatedly triggers
generateCommitMessage because commitModelOverride is a new object each render;
fix by pulling out the stable memoized values from useModelOverride (e.g., const
{ effectiveModel, effectiveModelEntry } = commitModelOverride or have
useModelOverride return memoized primitives) and use those primitives in the
generateCommitMessage useCallback dependency array instead of
commitModelOverride so generateCommitMessage only changes when the actual model
selection changes; also ensure the opening useEffect depends on those primitives
(effectiveModel, effectiveModelEntry) rather than the whole override object and
remove the setMessage('') wipe if unnecessary while generation is in progress;
finally update handleOpenChange to ignore close requests when isGenerating is
true so the dialog cannot be closed via Escape or overlay click during
generation.
🧹 Nitpick comments (9)
apps/ui/src/lib/settings-utils.ts (1)

22-28: 'path' in worktree presence guard (line 25) is redundant

The value check on line 28 already covers the absent-key case: when path is not present it is undefined, which is neither null nor typeof 'string', so the entry is rejected. By contrast, branch is validated only via typeof worktree.branch === 'string' with no separate 'in' guard — the two conditions are inconsistently structured.

♻️ Proposed cleanup for symmetry
   if (
     typeof worktree === 'object' &&
     worktree !== null &&
-    'path' in worktree &&
     'branch' in worktree &&
     typeof worktree.branch === 'string' &&
     (worktree.path === null || typeof worktree.path === 'string')
   ) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/lib/settings-utils.ts` around lines 22 - 28, The guard "'path' in
worktree" is redundant and causes asymmetric checks vs. branch; update the
conditional in the worktree validation (the if that inspects worktree) to remove
the explicit "'path' in worktree" check and instead rely on the existing type
checks (typeof worktree.branch === 'string' and (worktree.path === null ||
typeof worktree.path === 'string')) so both keys are validated consistently;
ensure the condition still checks worktree is an object and not null before the
typeof checks on branch and path.
apps/server/tests/unit/providers/codex-provider.test.ts (1)

173-192: Assertions for --output-schema and --add-dir exclusion are tautologically true.

Lines 190–191 check that --output-schema and --add-dir are absent, but since the test provides no outputFormat or additionalDirs, these flags wouldn't appear even in a non-resume invocation. To truly validate the resume-specific guards, supply these options and confirm they're excluded only when sdkSessionId is present:

💡 Suggested improvement
       await collectAsyncGenerator(
         provider.executeQuery({
           prompt: 'Continue',
           model: 'gpt-5.2',
           cwd: '/tmp',
           sdkSessionId: 'codex-session-123',
+          outputFormat: { type: 'json_schema', schema: { type: 'object' } },
+          codexSettings: { additionalDirs: ['/extra'] },
         })
       );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/tests/unit/providers/codex-provider.test.ts` around lines 173 -
192, The test currently asserts that '--output-schema' and '--add-dir' are
absent but never supplies outputFormat or additionalDirs, making those
assertions meaningless; update the test that calls provider.executeQuery (the
"uses exec resume when sdkSessionId is provided" case) to pass outputFormat and
additionalDirs (e.g., outputFormat: {...} and additionalDirs: ['...']) so the
spawnJSONLProcess invocation would normally include '--output-schema' and
'--add-dir', then verify via vi.mocked(spawnJSONLProcess).mock.calls[0][0].args
that when sdkSessionId is provided the args still exclude '--output-schema' and
'--add-dir' while keeping 'exec', 'resume', '--json', and the session id; this
ensures spawnJSONLProcess, provider.executeQuery, and collectAsyncGenerator
behavior are correctly tested.
apps/server/tests/unit/providers/cursor-provider.test.ts (1)

6-22: Consider adding a negative test and using a standard constructor.

Two observations:

  1. Missing negative case: The Gemini test suite includes a test for when sdkSessionId is absent. Adding the same here would improve consistency and coverage.

  2. Object.create bypasses the constructor: If buildCliArgs ever relies on constructor-initialized state beyond cliPath, this test will silently break. A beforeEach with proper mocking (as in the Codex and Gemini tests) would be more robust.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/tests/unit/providers/cursor-provider.test.ts` around lines 6 -
22, Add a negative test that verifies buildCliArgs (on CursorProvider) does not
include "--resume" when sdkSessionId is omitted, and replace the
Object.create(...) instantiation with a real instance created via the
CursorProvider constructor (or use a beforeEach to construct and set
provider.cliPath) so constructor-initialized state is honored; refer to
CursorProvider, buildCliArgs, cliPath, and sdkSessionId when updating the tests.
apps/ui/src/store/types/state-types.ts (1)

674-676: Consider pruning stale entries when a session is deleted

agentModelBySession is never cleaned up when deleteChatSession is called, so removed sessions leave orphaned entries in the map. This is negligible in practice (entries are tiny) but a tidy implementation would remove the entry in the delete action.

♻️ Suggested cleanup in app-store.ts (outside this file)
// In the deleteChatSession action implementation:
 deleteChatSession: (sessionId) =>
   set((state) => {
+    const { [sessionId]: _removed, ...remainingModels } = state.agentModelBySession;
     return {
       chatSessions: state.chatSessions.filter((s) => s.id !== sessionId),
+      agentModelBySession: remainingModels,
     };
   }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/store/types/state-types.ts` around lines 674 - 676,
agentModelBySession is never pruned when sessions are removed; update the
session deletion logic (the deleteChatSession action in app-store.ts) to also
remove the corresponding entry from the agentModelBySession map (use the
sessionId passed to deleteChatSession). Ensure the cleanup matches the store API
(works with setAgentModelForSession/getAgentModelForSession semantics) and
handles missing keys gracefully (no-ops if the sessionId is not present).
apps/ui/src/store/ui-cache-store.ts (1)

122-135: Optional: extract duplicated validation into a shared helper

The worktree-entry guard (typeof worktree === 'object' && … branch check … path check) appears verbatim in both syncUICache (line 124) and restoreFromUICache (line 193). A small inline helper keeps future constraint changes in one place.

♻️ Proposed refactor
+// Minimal structural guard for a cached worktree entry
+function isValidCachedWorktreeEntry(
+  w: unknown
+): w is { path: string | null; branch: string } {
+  return (
+    typeof w === 'object' &&
+    w !== null &&
+    'path' in w &&
+    'branch' in w &&
+    typeof (w as Record<string, unknown>).branch === 'string' &&
+    ((w as Record<string, unknown>).path === null ||
+      typeof (w as Record<string, unknown>).path === 'string')
+  );
+}

Then replace both inline guards with if (isValidCachedWorktreeEntry(worktree)).

Also applies to: 189-207

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/store/ui-cache-store.ts` around lines 122 - 135, Extract the
duplicated runtime guard for worktree entries into a small helper function
(e.g., isValidCachedWorktreeEntry) that accepts the unknown worktree value and
returns a boolean after checking the same conditions currently repeated
(object/non-null, has 'path' and 'branch', branch is string, path is null or
string); then replace the inline guards in syncUICache (where sanitized is
built) and restoreFromUICache (the corresponding guard around lines ~189-207)
with if (isValidCachedWorktreeEntry(worktree)) so all validation logic lives in
one place and future changes only require updating that helper.
apps/ui/src/components/views/agent-view/hooks/use-agent-session.ts (3)

108-118: Duplicated model-restoration logic — consider extracting a helper.

The model-restoration logic (check persisted model → set or fallback to default) appears both here (lines 112-117) and in handleSelectSession (lines 52-61). A small helper like restoreModelForSession(sessionId) would DRY this up and prevent future drift between the two paths.

Not blocking, just a readability improvement.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/agent-view/hooks/use-agent-session.ts` around
lines 108 - 118, The model-restoration logic is duplicated between the
initialization block (using getLastSelectedSession, getAgentModelForSession,
setModelSelectionState, setCurrentSessionId) and the interactive path in
handleSelectSession; extract that behavior into a small helper function (e.g.,
restoreModelForSession(sessionId)) that encapsulates "fetch persisted model for
sessionId and setModelSelectionState or fallback", then call that helper from
both the init code and handleSelectSession to DRY up the logic and keep behavior
consistent.

52-62: Info-level logging may be noisy in production.

Line 56 logs the full persistedModel object at info level on every session switch. Consider downgrading to debug to reduce noise, especially since this fires on every session selection.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/agent-view/hooks/use-agent-session.ts` around
lines 52 - 62, The code currently logs the full persistedModel at info level on
every session switch; change the logger.info call in the session restoration
block (the one that calls getAgentModelForSession and setModelSelectionState
when sessionId is present) to logger.debug and avoid logging the entire
persistedModel object (log sessionId and a concise marker like "restored model"
or a small identifier instead) so normal production logs are quieter while
preserving useful debug output; ensure the else branch still uses
DEFAULT_MODEL_SELECTION and setModelSelectionState as before.

28-33: Consider using a Zustand selector to avoid full-store subscription.

useAppStore() without a selector subscribes to the entire store, so any state change (even unrelated) will re-render the component using this hook. Since you only need four stable action references, a shallow selector would eliminate unnecessary re-renders:

const { setLastSelectedSession, getLastSelectedSession, setAgentModelForSession, getAgentModelForSession } =
  useAppStore(
    useShallow(s => ({
      setLastSelectedSession: s.setLastSelectedSession,
      getLastSelectedSession: s.getLastSelectedSession,
      setAgentModelForSession: s.setAgentModelForSession,
      getAgentModelForSession: s.getAgentModelForSession,
    }))
  );

This is a pre-existing pattern, so not blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/agent-view/hooks/use-agent-session.ts` around
lines 28 - 33, Replace the full-store subscription call to useAppStore() with a
selector that picks only the four action/getter functions to avoid unnecessary
re-renders; specifically, select setLastSelectedSession, getLastSelectedSession,
setAgentModelForSession and getAgentModelForSession via a selector (and use a
shallow comparator) so the hook only subscribes to those stable references
instead of the entire store.
apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx (1)

547-553: Add optional chaining on effectiveModelEntry access.

Same potential null dereference as flagged in create-pr-dialog.tsxcommitModelOverride.effectiveModelEntry is accessed without a null guard.

🛡️ Suggested fix
-        commitModelOverride.effectiveModelEntry.thinkingLevel,
-        commitModelOverride.effectiveModelEntry.providerId
+        commitModelOverride.effectiveModelEntry?.thinkingLevel,
+        commitModelOverride.effectiveModelEntry?.providerId
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx`
around lines 547 - 553, The call to api.worktree.generateCommitMessage uses
commitModelOverride.effectiveModelEntry without null checks and can crash if
that entry is null; update the arguments to use optional chaining on
effectiveModelEntry (e.g.,
commitModelOverride.effectiveModelEntry?.thinkingLevel and
commitModelOverride.effectiveModelEntry?.providerId) before passing them into
generateCommitMessage (keeping the existing commitModelOverride.effectiveModel
argument as-is) so the call safely handles a missing effectiveModelEntry.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/server/src/providers/codex-provider.ts`:
- Around line 797-802: The code writes image files even when resuming
(isResumeQuery based on options.sdkSessionId) by calling extractImageBlocks and
writeImageFiles, but the resume path does not add the image directory to the
Codex CLI args (the --add-dir logic), making those images inaccessible; fix by
either skipping image extraction/writing when options.sdkSessionId is truthy
(short-circuit extractImageBlocks/writeImageFiles when isResumeQuery) or ensure
the resume CLI arg construction still includes the image directory when
imagePaths is non-empty (add the same --add-dir behavior used for non-resume
flows); update the branches around isResumeQuery, extractImageBlocks,
writeImageFiles, and the code that builds the CLI args to keep behavior
consistent.

In `@apps/ui/src/lib/settings-utils.ts`:
- Line 12: Update the documentation comment above the settings entry filtering
function in settings-utils.ts to accurately reflect all rejected cases: state
that entries are dropped when they are not an object, when branch is missing or
not a string, and when path is present but is neither null nor a string; change
the parenthetical to something like "(not an object, missing/invalid path or
branch)" so the doc matches the actual guard logic.

In `@apps/ui/src/store/app-store.ts`:
- Around line 1602-1611: The store field agentModelBySession is not being
persisted; add 'agentModelBySession' to the SETTINGS_FIELDS_TO_SYNC array in
use-settings-sync.ts and update refreshSettingsFromServer() to restore
agentModelBySession from the server response (similar to how
lastSelectedSessionByProject is restored) so model selections survive page
refreshes; reference the SETTINGS_FIELDS_TO_SYNC constant and the
refreshSettingsFromServer() function when making the change.

---

Outside diff comments:
In `@apps/server/src/providers/copilot-provider.ts`:
- Around line 567-597: The Copilot SDK is receiving the un-normalized model
alias in sessionOptions (see bareModel used when constructing
CopilotSessionOptions and passed to client.createSession /
resumableClient.resumeSession); call resolveModelString() from
`@automaker/model-resolver` on bareModel and use the returned normalized model
string in the sessionOptions.model before any SDK calls (both when creating a
new session and when resuming via resumeSession when options.sdkSessionId is
present).

In `@apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx`:
- Around line 594-601: handleOpenChange currently prevents closing only when
isPushing; extend its guard to also block closure while isGenerating so overlay
clicks/Escape don't close the dialog mid-generation. In the handleOpenChange
function (referencing handleOpenChange, isPushing, isGenerating, and
onOpenChange), return early when either isPushing or isGenerating is true and
only call onOpenChange(nextOpen) when neither is active; this keeps the UI open
until generation completes and prevents in-flight API callbacks (e.g.,
setMessage) from running after the dialog is closed.
- Around line 541-586: The commit dialog repeatedly triggers
generateCommitMessage because commitModelOverride is a new object each render;
fix by pulling out the stable memoized values from useModelOverride (e.g., const
{ effectiveModel, effectiveModelEntry } = commitModelOverride or have
useModelOverride return memoized primitives) and use those primitives in the
generateCommitMessage useCallback dependency array instead of
commitModelOverride so generateCommitMessage only changes when the actual model
selection changes; also ensure the opening useEffect depends on those primitives
(effectiveModel, effectiveModelEntry) rather than the whole override object and
remove the setMessage('') wipe if unnecessary while generation is in progress;
finally update handleOpenChange to ignore close requests when isGenerating is
true so the dialog cannot be closed via Escape or overlay click during
generation.

---

Nitpick comments:
In `@apps/server/tests/unit/providers/codex-provider.test.ts`:
- Around line 173-192: The test currently asserts that '--output-schema' and
'--add-dir' are absent but never supplies outputFormat or additionalDirs, making
those assertions meaningless; update the test that calls provider.executeQuery
(the "uses exec resume when sdkSessionId is provided" case) to pass outputFormat
and additionalDirs (e.g., outputFormat: {...} and additionalDirs: ['...']) so
the spawnJSONLProcess invocation would normally include '--output-schema' and
'--add-dir', then verify via vi.mocked(spawnJSONLProcess).mock.calls[0][0].args
that when sdkSessionId is provided the args still exclude '--output-schema' and
'--add-dir' while keeping 'exec', 'resume', '--json', and the session id; this
ensures spawnJSONLProcess, provider.executeQuery, and collectAsyncGenerator
behavior are correctly tested.

In `@apps/server/tests/unit/providers/cursor-provider.test.ts`:
- Around line 6-22: Add a negative test that verifies buildCliArgs (on
CursorProvider) does not include "--resume" when sdkSessionId is omitted, and
replace the Object.create(...) instantiation with a real instance created via
the CursorProvider constructor (or use a beforeEach to construct and set
provider.cliPath) so constructor-initialized state is honored; refer to
CursorProvider, buildCliArgs, cliPath, and sdkSessionId when updating the tests.

In `@apps/ui/src/components/views/agent-view/hooks/use-agent-session.ts`:
- Around line 108-118: The model-restoration logic is duplicated between the
initialization block (using getLastSelectedSession, getAgentModelForSession,
setModelSelectionState, setCurrentSessionId) and the interactive path in
handleSelectSession; extract that behavior into a small helper function (e.g.,
restoreModelForSession(sessionId)) that encapsulates "fetch persisted model for
sessionId and setModelSelectionState or fallback", then call that helper from
both the init code and handleSelectSession to DRY up the logic and keep behavior
consistent.
- Around line 52-62: The code currently logs the full persistedModel at info
level on every session switch; change the logger.info call in the session
restoration block (the one that calls getAgentModelForSession and
setModelSelectionState when sessionId is present) to logger.debug and avoid
logging the entire persistedModel object (log sessionId and a concise marker
like "restored model" or a small identifier instead) so normal production logs
are quieter while preserving useful debug output; ensure the else branch still
uses DEFAULT_MODEL_SELECTION and setModelSelectionState as before.
- Around line 28-33: Replace the full-store subscription call to useAppStore()
with a selector that picks only the four action/getter functions to avoid
unnecessary re-renders; specifically, select setLastSelectedSession,
getLastSelectedSession, setAgentModelForSession and getAgentModelForSession via
a selector (and use a shallow comparator) so the hook only subscribes to those
stable references instead of the entire store.

In `@apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx`:
- Around line 547-553: The call to api.worktree.generateCommitMessage uses
commitModelOverride.effectiveModelEntry without null checks and can crash if
that entry is null; update the arguments to use optional chaining on
effectiveModelEntry (e.g.,
commitModelOverride.effectiveModelEntry?.thinkingLevel and
commitModelOverride.effectiveModelEntry?.providerId) before passing them into
generateCommitMessage (keeping the existing commitModelOverride.effectiveModel
argument as-is) so the call safely handles a missing effectiveModelEntry.

In `@apps/ui/src/lib/settings-utils.ts`:
- Around line 22-28: The guard "'path' in worktree" is redundant and causes
asymmetric checks vs. branch; update the conditional in the worktree validation
(the if that inspects worktree) to remove the explicit "'path' in worktree"
check and instead rely on the existing type checks (typeof worktree.branch ===
'string' and (worktree.path === null || typeof worktree.path === 'string')) so
both keys are validated consistently; ensure the condition still checks worktree
is an object and not null before the typeof checks on branch and path.

In `@apps/ui/src/store/types/state-types.ts`:
- Around line 674-676: agentModelBySession is never pruned when sessions are
removed; update the session deletion logic (the deleteChatSession action in
app-store.ts) to also remove the corresponding entry from the
agentModelBySession map (use the sessionId passed to deleteChatSession). Ensure
the cleanup matches the store API (works with
setAgentModelForSession/getAgentModelForSession semantics) and handles missing
keys gracefully (no-ops if the sessionId is not present).

In `@apps/ui/src/store/ui-cache-store.ts`:
- Around line 122-135: Extract the duplicated runtime guard for worktree entries
into a small helper function (e.g., isValidCachedWorktreeEntry) that accepts the
unknown worktree value and returns a boolean after checking the same conditions
currently repeated (object/non-null, has 'path' and 'branch', branch is string,
path is null or string); then replace the inline guards in syncUICache (where
sanitized is built) and restoreFromUICache (the corresponding guard around lines
~189-207) with if (isValidCachedWorktreeEntry(worktree)) so all validation logic
lives in one place and future changes only require updating that helper.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx (1)

576-588: ⚠️ Potential issue | 🟠 Major

Model override change silently resets and re-generates the commit message

generateCommitMessage is listed in this effect's dependency array and is recreated every time commitEffectiveModel or commitEffectiveModelEntry changes (its own useCallback deps). As a result, when the user changes the model override while the dialog is open, the effect re-fires: it calls setMessage('') (erasing any user edits) and then calls generateCommitMessage() again — an unintended auto-regeneration that the user did not request.

Fix by stabilising the callback reference with a ref so the effect only fires on meaningful state changes (open, worktree, enableAiCommitMessages):

🐛 Proposed fix
+  const generateCommitMessageRef = useRef(generateCommitMessage);
+  useEffect(() => {
+    generateCommitMessageRef.current = generateCommitMessage;
+  });

   // Generate AI commit message when dialog opens (if enabled)
   useEffect(() => {
     if (open && worktree) {
       // Reset state
       setMessage('');
       setError(null);

       if (!enableAiCommitMessages) {
         return;
       }

-      generateCommitMessage();
+      generateCommitMessageRef.current();
     }
-  }, [open, worktree, enableAiCommitMessages, generateCommitMessage]);
+  }, [open, worktree, enableAiCommitMessages]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx`
around lines 576 - 588, The effect currently depends on generateCommitMessage,
which is recreated when commitEffectiveModel or commitEffectiveModelEntry change
and causes the dialog to reset and auto-regenerate a message unexpectedly;
stabilize the callback by storing generateCommitMessage in a ref (e.g.
generateCommitMessageRef) and update that ref whenever the callback changes,
then call generateCommitMessageRef.current() from the useEffect and remove
generateCommitMessage from the effect dependency array so the effect only runs
on open, worktree, and enableAiCommitMessages; keep the existing setMessage('')
and setError(null) behavior but ensure user edits aren't erased by model
override changes.
🧹 Nitpick comments (5)
apps/ui/src/hooks/use-settings-sync.ts (1)

810-812: Apply migratePhaseModelEntry() to each value when restoring agentModelBySession.

Every other PhaseModelEntry restored in this function is passed through migratePhaseModelEntry() — including all 11 fields in migratedPhaseModels (lines 694–718) and defaultFeatureModel (line 768). The raw pass-through here means stored model aliases from old sessions are never migrated, and could silently break model resolution.

♻️ Proposed fix
-      agentModelBySession:
-        serverSettings.agentModelBySession ?? currentAppState.agentModelBySession,
+      agentModelBySession: serverSettings.agentModelBySession
+        ? Object.fromEntries(
+            Object.entries(serverSettings.agentModelBySession as Record<string, unknown>).map(
+              ([sessionId, entry]) => [sessionId, migratePhaseModelEntry(entry)]
+            )
+          )
+        : currentAppState.agentModelBySession,

setAgentModelForSession in app-store.ts correctly performs an immutable update via object spread, so reference-based change detection will work as expected once this migration is in place.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/hooks/use-settings-sync.ts` around lines 810 - 812, The restore
logic for agentModelBySession should map each stored PhaseModelEntry through
migratePhaseModelEntry before assigning it so old session model aliases are
migrated; update the agentModelBySession assignment to take
serverSettings.agentModelBySession ?? currentAppState.agentModelBySession and
transform each value with migratePhaseModelEntry(...) (preserving keys and
null-path filtering already present), ensuring migrated objects are returned so
setAgentModelForSession's object-spread updates will detect changes.
apps/ui/src/store/ui-cache-store.ts (1)

138-144: Duplicate sanitization logic — consider extracting a shared helper

The same loop/validate/accumulate pattern appears in both syncUICache and restoreFromUICache. Extracting it keeps the guard in one place and makes future changes to the validation logic automatic in both call sites.

♻️ Suggested refactor

Add a private helper immediately after isValidCachedWorktreeEntry:

+function sanitizeWorktreeByProject(
+  raw: Record<string, unknown>
+): Record<string, { path: string | null; branch: string }> {
+  const sanitized: Record<string, { path: string | null; branch: string }> = {};
+  for (const [key, worktree] of Object.entries(raw)) {
+    if (isValidCachedWorktreeEntry(worktree)) {
+      sanitized[key] = worktree;
+    }
+  }
+  return sanitized;
+}

Then simplify both call sites:

-    const sanitized: Record<string, { path: string | null; branch: string }> = {};
-    for (const [projectPath, worktree] of Object.entries(appState.currentWorktreeByProject)) {
-      if (isValidCachedWorktreeEntry(worktree)) {
-        sanitized[projectPath] = worktree;
-      }
-    }
-    update.cachedCurrentWorktreeByProject = sanitized;
+    update.cachedCurrentWorktreeByProject = sanitizeWorktreeByProject(
+      appState.currentWorktreeByProject as Record<string, unknown>
+    );
-    const sanitized: Record<string, { path: string | null; branch: string }> = {};
-    for (const [projectPath, worktree] of Object.entries(cache.cachedCurrentWorktreeByProject)) {
-      if (isValidCachedWorktreeEntry(worktree)) {
-        sanitized[projectPath] = worktree;
-      }
-    }
-    if (Object.keys(sanitized).length > 0) {
-      stateUpdate.currentWorktreeByProject = sanitized;
-    }
+    const sanitized = sanitizeWorktreeByProject(
+      cache.cachedCurrentWorktreeByProject as Record<string, unknown>
+    );
+    if (Object.keys(sanitized).length > 0) {
+      stateUpdate.currentWorktreeByProject = sanitized;
+    }

Also applies to: 198-209

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/store/ui-cache-store.ts` around lines 138 - 144, Duplicate
sanitization of appState.currentWorktreeByProject exists in syncUICache and
restoreFromUICache; extract the loop/validate/accumulate logic into a private
helper (e.g., sanitizeWorktreeMap) placed near isValidCachedWorktreeEntry, which
takes a Record<string, ...> and returns the sanitized Record<string, ...>; then
replace the inline loops in syncUICache and restoreFromUICache to call
sanitizeWorktreeMap and assign its result to
update.cachedCurrentWorktreeByProject (and any similar target), ensuring all
call sites share the same validation path.
apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx (1)

829-851: Regenerate button and model override are shown/enabled regardless of enableAiCommitMessages

When a user has explicitly disabled AI commit messages (the enableAiCommitMessages setting), the Regenerate button and ModelOverrideTrigger are still fully visible and functional. Clicking Regenerate will call the HTTP API and overwrite whatever the user typed — which feels inconsistent with having opted out of AI generation.

Consider hiding or disabling both controls when enableAiCommitMessages is false, or at minimum gate the generateCommitMessage call:

-  disabled={isGenerating || isLoading}
+  disabled={isGenerating || isLoading || !enableAiCommitMessages}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx`
around lines 829 - 851, The Regenerate button and ModelOverrideTrigger ignore
the user's enableAiCommitMessages preference; update the UI and handler so AI
controls are disabled/hidden and the API call is blocked when
enableAiCommitMessages is false. Specifically, wrap or conditionally render the
Button (the onClick handler generateCommitMessage) and the ModelOverrideTrigger
(commitModelOverride.* props) based on enableAiCommitMessages, or disable the
Button and ModelOverrideTrigger when enableAiCommitMessages is false;
additionally guard generateCommitMessage itself to no-op if
enableAiCommitMessages is false to prevent accidental HTTP calls.
apps/server/src/providers/codex-provider.ts (1)

851-854: Redundant !isResumeQuery guard — imagePaths is already empty in resume mode.

Since imagePaths is set to [] unconditionally when isResumeQuery is true (line 803), imagePaths.length > 0 will always be false in resume mode. The !isResumeQuery outer check is superfluous.

♻️ Proposed simplification
-      if (!isResumeQuery && imagePaths.length > 0) {
+      if (imagePaths.length > 0) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/providers/codex-provider.ts` around lines 851 - 854, The
outer !isResumeQuery check is redundant because imagePaths is set to [] when
isResumeQuery is true; update the condition in the block that pushes
CODEX_ADD_DIR_FLAG to only check imagePaths.length > 0 and remove the
!isResumeQuery guard, leaving the imageDir computation (path.join(options.cwd,
CODEX_INSTRUCTIONS_DIR, IMAGE_TEMP_DIR)) and the
preExecArgs.push(CODEX_ADD_DIR_FLAG, imageDir) logic intact so the flag is only
added when imagePaths actually contains entries.
apps/server/src/providers/copilot-provider.ts (1)

600-601: Log message says "Session created" for both new and resumed sessions.

🔧 Suggested fix
-    const sessionId = session.sessionId;
-    logger.debug(`Session created: ${sessionId}`);
+    const sessionId = session.sessionId;
+    const isResumed = !!(options.sdkSessionId && typeof resumableClient.resumeSession === 'function');
+    logger.debug(`Session ${isResumed ? 'resumed' : 'created'}: ${sessionId}`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/providers/copilot-provider.ts` around lines 600 - 601, The
log always says "Session created" even when a session is resumed; update the
logger.debug call to distinguish new vs resumed sessions by inspecting the
session object (e.g., check a flag like session.isNew or session.resumed, or
compare created/lastAccess timestamps) and log "Session created: <sessionId>"
for new sessions and "Session resumed: <sessionId>" for resumed ones; locate the
code around sessionId / session.sessionId and replace the single message with a
conditional that picks the correct text before calling logger.debug.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/server/src/providers/copilot-provider.ts`:
- Around line 583-598: The resumed/create session may leak if an error occurs
after session creation (e.g., during session.send()); update the outer try/catch
around resumeSession/createSession to track the CopilotSession instance and
ensure session.destroy() is called in the catch (before/alongside client.stop())
when session is truthy; specifically, after calling
resumableClient.resumeSession(...) or client.createSession(...) ensure any later
thrown error triggers session.destroy() (reference symbols: CopilotSession,
resumableClient.resumeSession, client.createSession, session.send,
session.destroy, client.stop).
- Line 526: resolveModelString(options.model || DEFAULT_BARE_MODEL) can return
dash-separated canonical names (e.g., claude-sonnet-4-6) which the Copilot SDK
rejects; normalize the resolved string before assigning to
CopilotSessionOptions.model by converting the version-related dashes to dots
(e.g., transform claude-sonnet-4-6 → claude-sonnet-4.6). Update the code that
sets bareModel (the variable assigned from resolveModelString) so it maps
dash-separated canonical outputs from resolveModelString (and entries coming
from CLAUDE_CANONICAL_MAP) into the dot-separated format expected by
CopilotSessionOptions.model while preserving DEFAULT_BARE_MODEL behavior.

In `@apps/ui/src/components/views/agent-view/hooks/use-agent-session.ts`:
- Around line 85-94: The setModelSelection callback currently writes to local
state but silently skips persistence when currentSessionId is null; make this
intent explicit by adding explicit handling: either persist the choice to a new
"pending" slot (e.g., add a pendingModelSelection state and
setPendingModelSelection(model) when currentSessionId is null) and update
restoreModelForSession to prefer pendingModelSelection before falling back to
DEFAULT_MODEL_SELECTION, or add a clear inline comment above setModelSelection
that the picker must be disabled when there is no session (and enforce that via
UI) so skipping setAgentModelForSession(currentSessionId, model) is intentional.
Ensure changes reference setModelSelection, currentSessionId,
setAgentModelForSession, restoreModelForSession, and DEFAULT_MODEL_SELECTION.
- Around line 55-66: The debug log in restoreModelForSession mistakenly logs the
string 'restored model' instead of the actual persistedModel; update the
logger.debug call inside restoreModelForSession to include the persistedModel
(or its relevant property) along with sessionId so the restored model value is
emitted; keep the rest of the logic intact (getAgentModelForSession,
setModelSelectionState, DEFAULT_MODEL_SELECTION).

In `@apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx`:
- Around line 597-601: The dialog close handler handleOpenChange currently
blocks closing only when isPushing or isGenerating are true; include the
commit-in-flight flag isLoading in that same guard so Escape/overlay clicks are
ignored while a commit is ongoing. Update the conditional inside
handleOpenChange to check (!nextOpen && (isPushing || isGenerating ||
isLoading)) and keep the existing comment/early return behavior so the dialog
cannot be closed mid-commit.

In `@apps/ui/src/hooks/use-settings-sync.ts`:
- Line 112: agentModelBySession is unbounded and is serialized on every
sync/cache write; cap or prune it to avoid bloat by trimming to a recent window
(e.g., 50 most-recently-used) or applying TTL-based pruning before
serializing/restoring. Update getSettingsFieldValue (and any merge/restore paths
that read/write agentModelBySession) to produce a pruned version of
agentModelBySession: sort or track recency (LRU) and keep only the N most recent
sessionId entries (suggest N=50), and apply the same pruning when saving to
localStorage and when bundling the sync payload so both sync and cache only
carry the trimmed map. Ensure the pruning logic references agentModelBySession
and is run in the serialization/restore routines (getSettingsFieldValue and the
sync payload/cache write hooks).

In `@apps/ui/src/store/ui-cache-store.ts`:
- Around line 88-98: The validator is allowing empty strings for branch; update
isValidCachedWorktreeEntry to require a non-empty branch by adding a check that
(worktree as Record<string, unknown>).branch is a string with length > 0 (e.g.,
trim().length > 0) while keeping the existing path null-or-string logic; modify
the function isValidCachedWorktreeEntry accordingly so corrupted cache entries
with branch === "" are rejected.

---

Outside diff comments:
In `@apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx`:
- Around line 576-588: The effect currently depends on generateCommitMessage,
which is recreated when commitEffectiveModel or commitEffectiveModelEntry change
and causes the dialog to reset and auto-regenerate a message unexpectedly;
stabilize the callback by storing generateCommitMessage in a ref (e.g.
generateCommitMessageRef) and update that ref whenever the callback changes,
then call generateCommitMessageRef.current() from the useEffect and remove
generateCommitMessage from the effect dependency array so the effect only runs
on open, worktree, and enableAiCommitMessages; keep the existing setMessage('')
and setError(null) behavior but ensure user edits aren't erased by model
override changes.

---

Nitpick comments:
In `@apps/server/src/providers/codex-provider.ts`:
- Around line 851-854: The outer !isResumeQuery check is redundant because
imagePaths is set to [] when isResumeQuery is true; update the condition in the
block that pushes CODEX_ADD_DIR_FLAG to only check imagePaths.length > 0 and
remove the !isResumeQuery guard, leaving the imageDir computation
(path.join(options.cwd, CODEX_INSTRUCTIONS_DIR, IMAGE_TEMP_DIR)) and the
preExecArgs.push(CODEX_ADD_DIR_FLAG, imageDir) logic intact so the flag is only
added when imagePaths actually contains entries.

In `@apps/server/src/providers/copilot-provider.ts`:
- Around line 600-601: The log always says "Session created" even when a session
is resumed; update the logger.debug call to distinguish new vs resumed sessions
by inspecting the session object (e.g., check a flag like session.isNew or
session.resumed, or compare created/lastAccess timestamps) and log "Session
created: <sessionId>" for new sessions and "Session resumed: <sessionId>" for
resumed ones; locate the code around sessionId / session.sessionId and replace
the single message with a conditional that picks the correct text before calling
logger.debug.

In `@apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx`:
- Around line 829-851: The Regenerate button and ModelOverrideTrigger ignore the
user's enableAiCommitMessages preference; update the UI and handler so AI
controls are disabled/hidden and the API call is blocked when
enableAiCommitMessages is false. Specifically, wrap or conditionally render the
Button (the onClick handler generateCommitMessage) and the ModelOverrideTrigger
(commitModelOverride.* props) based on enableAiCommitMessages, or disable the
Button and ModelOverrideTrigger when enableAiCommitMessages is false;
additionally guard generateCommitMessage itself to no-op if
enableAiCommitMessages is false to prevent accidental HTTP calls.

In `@apps/ui/src/hooks/use-settings-sync.ts`:
- Around line 810-812: The restore logic for agentModelBySession should map each
stored PhaseModelEntry through migratePhaseModelEntry before assigning it so old
session model aliases are migrated; update the agentModelBySession assignment to
take serverSettings.agentModelBySession ?? currentAppState.agentModelBySession
and transform each value with migratePhaseModelEntry(...) (preserving keys and
null-path filtering already present), ensuring migrated objects are returned so
setAgentModelForSession's object-spread updates will detect changes.

In `@apps/ui/src/store/ui-cache-store.ts`:
- Around line 138-144: Duplicate sanitization of
appState.currentWorktreeByProject exists in syncUICache and restoreFromUICache;
extract the loop/validate/accumulate logic into a private helper (e.g.,
sanitizeWorktreeMap) placed near isValidCachedWorktreeEntry, which takes a
Record<string, ...> and returns the sanitized Record<string, ...>; then replace
the inline loops in syncUICache and restoreFromUICache to call
sanitizeWorktreeMap and assign its result to
update.cachedCurrentWorktreeByProject (and any similar target), ensuring all
call sites share the same validation path.

'projectHistory',
'projectHistoryIndex',
'lastSelectedSessionByProject',
'agentModelBySession',
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

agentModelBySession has unbounded growth — add a size cap or TTL-based pruning.

agentModelBySession is a Record<sessionId, PhaseModelEntry>. Unlike lastSelectedSessionByProject which is bounded by project count (one entry per project), this map gains a new entry for every agent session ever started. Nothing prunes it.

The full map is serialized into every sync payload (lines 304–306) and into the localStorage cache write (line 338). Active users will accumulate hundreds of entries, progressively bloating settings.json, every debounced write, and every page-reload cache read.

Consider capping the map to a rolling window (e.g., the 50 most-recently-used session IDs) when writing or restoring it, similar to how projectHistory is often capped:

♻️ Suggested pruning in getSettingsFieldValue
 if (field === 'autoModeByWorktree') {
   // ...
 }
+if (field === 'agentModelBySession') {
+  const map = appState.agentModelBySession as Record<string, unknown>;
+  const MAX_ENTRIES = 50;
+  const entries = Object.entries(map);
+  if (entries.length <= MAX_ENTRIES) return map;
+  // Keep the last MAX_ENTRIES entries (insertion-order approximation)
+  return Object.fromEntries(entries.slice(-MAX_ENTRIES));
+}
 return appState[field as keyof typeof appState];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/hooks/use-settings-sync.ts` at line 112, agentModelBySession is
unbounded and is serialized on every sync/cache write; cap or prune it to avoid
bloat by trimming to a recent window (e.g., 50 most-recently-used) or applying
TTL-based pruning before serializing/restoring. Update getSettingsFieldValue
(and any merge/restore paths that read/write agentModelBySession) to produce a
pruned version of agentModelBySession: sort or track recency (LRU) and keep only
the N most recent sessionId entries (suggest N=50), and apply the same pruning
when saving to localStorage and when bundling the sync payload so both sync and
cache only carry the trimmed map. Ensure the pruning logic references
agentModelBySession and is run in the serialization/restore routines
(getSettingsFieldValue and the sync payload/cache write hooks).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
apps/ui/src/components/views/agent-view/hooks/use-agent-session.ts (1)

29-41: useShallow is unnecessary for stable store action references.

The four functions selected here (setLastSelectedSession, getLastSelectedSession, etc.) are created once inside the Zustand create() call and never change between renders. useShallow adds a shallow-comparison overhead on every store update to produce the same result as a direct selector. Not a bug, but unnecessary work.

♻️ Suggested simplification
- const {
-   setLastSelectedSession,
-   getLastSelectedSession,
-   setAgentModelForSession,
-   getAgentModelForSession,
- } = useAppStore(
-   useShallow((state) => ({
-     setLastSelectedSession: state.setLastSelectedSession,
-     getLastSelectedSession: state.getLastSelectedSession,
-     setAgentModelForSession: state.setAgentModelForSession,
-     getAgentModelForSession: state.getAgentModelForSession,
-   }))
- );
+ const setLastSelectedSession = useAppStore((s) => s.setLastSelectedSession);
+ const getLastSelectedSession = useAppStore((s) => s.getLastSelectedSession);
+ const setAgentModelForSession = useAppStore((s) => s.setAgentModelForSession);
+ const getAgentModelForSession = useAppStore((s) => s.getAgentModelForSession);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/agent-view/hooks/use-agent-session.ts` around
lines 29 - 41, The selector is wrapping stable Zustand action references with
useShallow which is unnecessary; change the useAppStore invocation in
use-agent-session to directly select setLastSelectedSession,
getLastSelectedSession, setAgentModelForSession, and getAgentModelForSession
without the useShallow wrapper so you avoid the extra shallow-comparison
overhead while still getting the same stable function references.
apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx (1)

583-595: No cancellation for in-flight generation when the dialog is closed and re-opened.

If the dialog closes and re-opens rapidly, the effect on line 593 calls generateCommitMessageRef.current() without cancelling any prior in-flight HTTP request. Both calls could race to setMessage, and the first (stale) response could overwrite the second. Consider adding an AbortController or a cancelled flag similar to the loadDiffs pattern on line 318.

♻️ Suggested cancellation pattern
  useEffect(() => {
    if (open && worktree) {
      setMessage('');
      setError(null);

      if (!enableAiCommitMessages) {
        return;
      }

-     generateCommitMessageRef.current();
+     let cancelled = false;
+     generateCommitMessageRef.current().then(() => {
+       // noop — state is set inside the callback
+     });
+     return () => {
+       cancelled = true;
+     };
    }
  }, [open, worktree, enableAiCommitMessages]);

Note: Full cancellation would require threading an AbortSignal into generateCommitMessage and checking cancelled before setMessage. The above is a minimal sketch — the real fix should guard setMessage inside generateCommitMessage itself.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx`
around lines 583 - 595, The effect that calls generateCommitMessageRef.current()
can start overlapping HTTP work when the dialog is closed/re-opened; update the
logic so in-flight generation is cancelled before starting a new one (mirror the
loadDiffs pattern): create a local cancelled flag or AbortController inside the
useEffect, pass an AbortSignal or check cancelled inside generateCommitMessage
(and/or generateCommitMessageRef.current) before calling setMessage, and ensure
the effect cleanup sets cancelled/aborts the controller so stale responses do
not overwrite state when the dialog is reopened.
apps/server/src/providers/copilot-provider.ts (1)

660-672: Potential double-destroy if client.stop() throws on the happy path.

If activeSession.destroy() (line 660) succeeds but client.stop() (line 661) throws, control transfers to the catch block. Since session === activeSession, the catch block will attempt session.destroy() on an already-destroyed session (lines 668–670). The inner try-catch swallows any resulting error, so there's no functional breakage — but setting session = undefined after the successful destroy prevents the redundant call.

🛡️ Optional: clear `session` after successful destroy
       // Cleanup
-      await activeSession.destroy();
+      await activeSession.destroy();
+      session = undefined; // prevent redundant destroy in catch if client.stop() throws
       await client.stop();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/providers/copilot-provider.ts` around lines 660 - 672, The
try block may call await activeSession.destroy() and then await client.stop(),
but if client.stop() throws the catch block will attempt to destroy the same
session again because session still references activeSession; after successfully
destroying the session in the happy path (the call to activeSession.destroy() in
the try), set session = undefined (or null) before calling client.stop() so the
catch cleanup won't double-destroy the same session; update references to
session and activeSession accordingly so the cleanup logic in the catch only
tries to destroy if session still exists.
apps/server/src/providers/codex-provider.ts (1)

810-810: Consider whether full conversation history should be sent via stdin in resume mode.

buildCombinedPrompt (line 810) serialises conversationHistory into the stdin payload for every invocation, including resume turns. If exec resume loads the session's existing history from its store and then appends everything from stdin, the exchanged history will be duplicated in the model's context window.

If exec resume expects only the new user turn (not the full reconstructed history), consider passing a reduced prompt for resume queries:

💡 Possible approach
- const promptText = buildCombinedPrompt(options, combinedSystemPrompt);
+ const promptText = isResumeQuery
+   ? buildResumePrompt(options, combinedSystemPrompt)   // new user turn only
+   : buildCombinedPrompt(options, combinedSystemPrompt);

Confirm whether codex exec resume expects the full conversation or only the new user turn via stdin.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/providers/codex-provider.ts` at line 810, The prompt
currently built by calling buildCombinedPrompt (assigned to promptText) always
serialises the full conversationHistory into the stdin payload, which can
duplicate history when running `codex exec resume`; detect when the request is a
resume turn (e.g., check options.mode / options.isResume or the presence of a
resume flag on the incoming options) and change the call so resume invocations
send a reduced prompt containing only the new user turn (or call a dedicated
function like buildResumePrompt) instead of the full reconstructed
conversationHistory; update the logic around buildCombinedPrompt/promptText to
conditionally build the minimized stdin payload for resume mode so the session
store’s history is not re-appended by the model.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx`:
- Around line 543-573: The generateCommitMessage callback is passing
commitEffectiveModel (which may be an alias) directly to
api.worktree.generateCommitMessage; resolve the alias first by calling
resolveModelString(commitEffectiveModel) from `@automaker/model-resolver` and pass
that resolved string to api.worktree.generateCommitMessage instead, keeping the
rest of the call intact (place the resolveModelString call inside
generateCommitMessage before invoking
getHttpApiClient()/api.worktree.generateCommitMessage and update the dependency
array to include commitEffectiveModel if needed).

---

Duplicate comments:
In `@apps/ui/src/components/views/agent-view/hooks/use-agent-session.ts`:
- Around line 55-66: The debug log in restoreModelForSession currently should
log the actual persisted model instead of a literal string; update the
logger.debug call inside function restoreModelForSession (referencing
getAgentModelForSession and setModelSelectionState) to include the
persistedModel variable (and sessionId) so it logs the real object (e.g.,
logger.debug('Restoring model selection for session:', sessionId,
persistedModel)); ensure DEFAULT_MODEL_SELECTION remains used in the else
branch.
- Around line 84-98: The comment confirms the behavior in setModelSelection is
intentional: when currentSessionId is null we only update local state via
setModelSelectionState and intentionally skip persistence via
setAgentModelForSession; no code change is required. Leave the useCallback
implementation (setModelSelection), its dependency array ([currentSessionId,
setAgentModelForSession]), and the explanatory inline comment as-is.

In `@apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx`:
- Around line 604-607: The dialog close-prevention needed to account for all
active operations; update handleOpenChange to check isLoading, isPushing, and
isGenerating and return early when nextOpen is false and any of those flags are
true (i.e., keep the existing guard using isLoading || isPushing || isGenerating
inside handleOpenChange) — no further changes required once that condition is
present.

In `@apps/ui/src/hooks/use-settings-sync.ts`:
- Around line 177-187: No change required: the pruning logic in
use-settings-sync.ts that caps agentModelBySession to the last 50 entries (the
block checking field === 'agentModelBySession' and returning
Object.fromEntries(entries.slice(-MAX_ENTRIES))) is acceptable and should be
left as-is in the useSettingsSync hook.

In `@apps/ui/src/store/ui-cache-store.ts`:
- Around line 84-99: The previous bug where empty or whitespace-only branch
strings could pass validation has been fixed in isValidCachedWorktreeEntry by
adding trim().length > 0; no further changes are required—keep the current
checks on branch and path (function isValidCachedWorktreeEntry) as they
correctly reject invalid cached worktree entries.

---

Nitpick comments:
In `@apps/server/src/providers/codex-provider.ts`:
- Line 810: The prompt currently built by calling buildCombinedPrompt (assigned
to promptText) always serialises the full conversationHistory into the stdin
payload, which can duplicate history when running `codex exec resume`; detect
when the request is a resume turn (e.g., check options.mode / options.isResume
or the presence of a resume flag on the incoming options) and change the call so
resume invocations send a reduced prompt containing only the new user turn (or
call a dedicated function like buildResumePrompt) instead of the full
reconstructed conversationHistory; update the logic around
buildCombinedPrompt/promptText to conditionally build the minimized stdin
payload for resume mode so the session store’s history is not re-appended by the
model.

In `@apps/server/src/providers/copilot-provider.ts`:
- Around line 660-672: The try block may call await activeSession.destroy() and
then await client.stop(), but if client.stop() throws the catch block will
attempt to destroy the same session again because session still references
activeSession; after successfully destroying the session in the happy path (the
call to activeSession.destroy() in the try), set session = undefined (or null)
before calling client.stop() so the catch cleanup won't double-destroy the same
session; update references to session and activeSession accordingly so the
cleanup logic in the catch only tries to destroy if session still exists.

In `@apps/ui/src/components/views/agent-view/hooks/use-agent-session.ts`:
- Around line 29-41: The selector is wrapping stable Zustand action references
with useShallow which is unnecessary; change the useAppStore invocation in
use-agent-session to directly select setLastSelectedSession,
getLastSelectedSession, setAgentModelForSession, and getAgentModelForSession
without the useShallow wrapper so you avoid the extra shallow-comparison
overhead while still getting the same stable function references.

In `@apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx`:
- Around line 583-595: The effect that calls generateCommitMessageRef.current()
can start overlapping HTTP work when the dialog is closed/re-opened; update the
logic so in-flight generation is cancelled before starting a new one (mirror the
loadDiffs pattern): create a local cancelled flag or AbortController inside the
useEffect, pass an AbortSignal or check cancelled inside generateCommitMessage
(and/or generateCommitMessageRef.current) before calling setMessage, and ensure
the effect cleanup sets cancelled/aborts the controller so stale responses do
not overwrite state when the dialog is reopened.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
apps/server/src/routes/worktree/routes/discard-changes.ts (1)

55-69: Deduplicate rename-path parsing by using parseGitStatus() from @automaker/git-utils.

The parseFilePath function (lines 55–69) duplicates the rename-handling logic that already exists in parseGitStatus() (exported from @automaker/git-utils). Both implementations use identical logic:

if (indexStatus === 'R' || workTreeStatus === 'R') {
  const arrowIndex = filePath.indexOf(' -> ');
  if (arrowIndex !== -1) {
    filePath = filePath.slice(arrowIndex + 4);
  }
}

Your comments on lines 58 and 117 acknowledge this. Instead of reimplementing it locally, import parseGitStatus (already imported elsewhere in the codebase via common.ts) and replace the manual parsing in lines 110–120:

const allFiles = parseGitStatus(status).map(f => ({ status: f.status, path: f.path }));

This eliminates the duplication and ensures consistency if rename handling ever changes in git-utils.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/routes/worktree/routes/discard-changes.ts` around lines 55 -
69, Remove the local parseFilePath duplication and instead use the existing
parseGitStatus helper: import parseGitStatus (via the same export used in
common.ts or directly from `@automaker/git-utils`) and replace the manual rename
handling where you build allFiles with const allFiles =
parseGitStatus(status).map(f => ({ status: f.status, path: f.path })); also
delete the parseFilePath function and update any call sites that used it to
consume the parsed objects from parseGitStatus so rename logic is centralized.
apps/server/src/providers/codex-provider.ts (1)

436-444: Session ID constraint is dead code in CLI resume mode but informational in SDK mode.

buildCodexConstraintsPrompt adds the session ID text when options.sdkSessionId is truthy. However, in CLI resume mode, buildResumePrompt is used (line 821) instead of buildCombinedPrompt, so the constraint-laden combinedSystemPrompt is never sent to the model. The session ID flows correctly as a CLI argument instead (line 884).

In SDK mode (lines 779-787), combinedSystemPrompt IS passed to executeCodexSdkQuery, making the session ID text present in the system prompt. However, actual session resumption is handled separately by codex.resumeThread(options.sdkSessionId), so the text is redundant but not missing from the SDK request.

Removing the session ID constraint block would be safe but changes SDK system prompt behavior. If cleaned up, ensure SDK documentation doesn't rely on this informational context. The CONSTRAINTS_SESSION_ID_LABEL constant at line 143 is only used here and can be removed together with the constraint block.

SDK session resumption is properly implemented: executeCodexSdkQuery correctly calls codex.resumeThread() when options.sdkSessionId is present, with fallback to a new thread on failure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/providers/codex-provider.ts` around lines 436 - 444, The
session ID constraint added in buildCodexConstraintsPrompt (using
CONSTRAINTS_SESSION_ID_LABEL) is dead in CLI resume mode and redundant in SDK
mode because SDK resumption is handled by codex.resumeThread in
executeCodexSdkQuery; remove the session ID block from
buildCodexConstraintsPrompt (and delete the unused CONSTRAINTS_SESSION_ID_LABEL
constant) so the system prompt no longer includes the SDK session ID text, but
keep codex.resumeThread usage in executeCodexSdkQuery and ensure
buildResumePrompt/buildCombinedPrompt behavior is unchanged; also update any SDK
documentation/comments that previously relied on the session ID appearing in the
system prompt.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/server/src/providers/codex-provider.ts`:
- Around line 807-813: When isResumeQuery is true the provided
options.outputFormat is currently ignored silently; update the code around the
isResumeQuery/schemaPath logic to emit a clear warning or assert when
options.outputFormat (or other output-related options) is non-null in resume
mode so callers know their value is dropped. Specifically, add a conditional
after computing isResumeQuery (check isResumeQuery && options.outputFormat) and
call the module's logger (or throw/assert in the surrounding function) with a
message indicating outputFormat is ignored for resumed queries; keep
writeOutputSchemaFile and schemaPath behavior unchanged when not resuming.

In `@apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx`:
- Around line 838-854: The Regenerate button's disabled condition currently
checks only isGenerating || isLoading; update its disabled prop to also include
isPushing so it becomes disabled when a push is in flight. Locate the Regenerate
Button in commit-worktree-dialog.tsx (the JSX that references
generateCommitMessage and shows Spinner or RefreshCw) and change its disabled to
combine isGenerating, isLoading, and isPushing (e.g., disabled={isGenerating ||
isLoading || isPushing}) so generation cannot start while
handleCommit/onOpenChange push flows are active.

---

Nitpick comments:
In `@apps/server/src/providers/codex-provider.ts`:
- Around line 436-444: The session ID constraint added in
buildCodexConstraintsPrompt (using CONSTRAINTS_SESSION_ID_LABEL) is dead in CLI
resume mode and redundant in SDK mode because SDK resumption is handled by
codex.resumeThread in executeCodexSdkQuery; remove the session ID block from
buildCodexConstraintsPrompt (and delete the unused CONSTRAINTS_SESSION_ID_LABEL
constant) so the system prompt no longer includes the SDK session ID text, but
keep codex.resumeThread usage in executeCodexSdkQuery and ensure
buildResumePrompt/buildCombinedPrompt behavior is unchanged; also update any SDK
documentation/comments that previously relied on the session ID appearing in the
system prompt.

In `@apps/server/src/routes/worktree/routes/discard-changes.ts`:
- Around line 55-69: Remove the local parseFilePath duplication and instead use
the existing parseGitStatus helper: import parseGitStatus (via the same export
used in common.ts or directly from `@automaker/git-utils`) and replace the manual
rename handling where you build allFiles with const allFiles =
parseGitStatus(status).map(f => ({ status: f.status, path: f.path })); also
delete the parseFilePath function and update any call sites that used it to
consume the parsed objects from parseGitStatus so rename logic is centralized.

Comment on lines +807 to +813
const isResumeQuery = Boolean(options.sdkSessionId);
const schemaPath = isResumeQuery
? null
: await writeOutputSchemaFile(options.cwd, options.outputFormat);
const imageBlocks =
!isResumeQuery && codexSettings.enableImages ? extractImageBlocks(options.prompt) : [];
const imagePaths = isResumeQuery ? [] : await writeImageFiles(options.cwd, imageBlocks);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Image handling in resume mode correctly addressed.

imageBlocks and imagePaths are set to [] when isResumeQuery is true, so the image dir is never added to preExecArgs and there are no orphaned files — this properly resolves the previously flagged issue.

Note: outputFormat (and thus schemaPath) is silently dropped in resume mode (line 808–810). If a caller supplies outputFormat for a resumed query, there's no warning. Consider logging or asserting if options.outputFormat is non-null in resume mode so the caller knows it's ignored.

🔍 Suggested silent-drop guard
+      if (isResumeQuery && options.outputFormat) {
+        logger.warn('[executeQuery] outputFormat is ignored in resume mode');
+      }
       const schemaPath = isResumeQuery
         ? null
         : await writeOutputSchemaFile(options.cwd, options.outputFormat);
📝 Committable suggestion

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

Suggested change
const isResumeQuery = Boolean(options.sdkSessionId);
const schemaPath = isResumeQuery
? null
: await writeOutputSchemaFile(options.cwd, options.outputFormat);
const imageBlocks =
!isResumeQuery && codexSettings.enableImages ? extractImageBlocks(options.prompt) : [];
const imagePaths = isResumeQuery ? [] : await writeImageFiles(options.cwd, imageBlocks);
const isResumeQuery = Boolean(options.sdkSessionId);
if (isResumeQuery && options.outputFormat) {
logger.warn('[executeQuery] outputFormat is ignored in resume mode');
}
const schemaPath = isResumeQuery
? null
: await writeOutputSchemaFile(options.cwd, options.outputFormat);
const imageBlocks =
!isResumeQuery && codexSettings.enableImages ? extractImageBlocks(options.prompt) : [];
const imagePaths = isResumeQuery ? [] : await writeImageFiles(options.cwd, imageBlocks);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/server/src/providers/codex-provider.ts` around lines 807 - 813, When
isResumeQuery is true the provided options.outputFormat is currently ignored
silently; update the code around the isResumeQuery/schemaPath logic to emit a
clear warning or assert when options.outputFormat (or other output-related
options) is non-null in resume mode so callers know their value is dropped.
Specifically, add a conditional after computing isResumeQuery (check
isResumeQuery && options.outputFormat) and call the module's logger (or
throw/assert in the surrounding function) with a message indicating outputFormat
is ignored for resumed queries; keep writeOutputSchemaFile and schemaPath
behavior unchanged when not resuming.

Comment on lines +110 to 121
// For renamed files: XY OLD_PATH -> NEW_PATH
const statusLines = status.trim().split('\n').filter(Boolean);
const allFiles = statusLines.map((line) => {
const fileStatus = line.substring(0, 2);
const filePath = line.slice(3).trim();
const rawPath = line.slice(3);
const indexStatus = fileStatus.charAt(0);
const workTreeStatus = fileStatus.charAt(1);
// Parse path consistently with parseGitStatus() in git-utils,
// which extracts the new path for renames
const filePath = parseFilePath(rawPath, indexStatus, workTreeStatus);
return { status: fileStatus, path: filePath };
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Rename handling improvement looks correct, but selective discard of renames may only partially unstage.

The switch to parseFilePath is a clear improvement — previously the raw "old_path -> new_path" string would be passed to git commands verbatim, which would fail. Now the new path is correctly extracted.

However, for staged renames (R index status), only the new path is tracked. Running git reset HEAD -- <new_path> unstages the addition of the new path but leaves the deletion of the old path staged. To fully unstage a rename you'd need both paths (or git reset HEAD without file args). This is a pre-existing limitation, but the rename-aware parsing makes it more relevant now that renames are actually handled.

Comment on lines +838 to +854
{enableAiCommitMessages && (
<>
<Button
variant="ghost"
size="sm"
onClick={generateCommitMessage}
disabled={isGenerating || isLoading}
className="h-6 px-2 text-xs"
title="Regenerate commit message"
>
{isGenerating ? (
<Spinner size="xs" className="mr-1" />
) : (
<RefreshCw className="w-3 h-3 mr-1" />
)}
Regenerate
</Button>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add isPushing to Regenerate button's disabled check.

The Cancel and Commit buttons both guard on isPushing, but the Regenerate button only checks isGenerating || isLoading. When a push is in flight, the user can still click Regenerate, producing two concurrent spinner states and a wasted API call (the push's handleCommit path calls onOpenChange(false) directly, closing the dialog while generation is still in-flight). This is also confusing in the push-retry state where generating a new commit message is meaningless.

🛡️ Proposed fix
-              disabled={isGenerating || isLoading}
+              disabled={isGenerating || isLoading || isPushing}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/ui/src/components/views/board-view/dialogs/commit-worktree-dialog.tsx`
around lines 838 - 854, The Regenerate button's disabled condition currently
checks only isGenerating || isLoading; update its disabled prop to also include
isPushing so it becomes disabled when a push is in flight. Locate the Regenerate
Button in commit-worktree-dialog.tsx (the JSX that references
generateCommitMessage and shows Spinner or RefreshCw) and change its disabled to
combine isGenerating, isLoading, and isPushing (e.g., disabled={isGenerating ||
isLoading || isPushing}) so generation cannot start while
handleCommit/onOpenChange push flows are active.

@gsxdsm gsxdsm merged commit 9305ecc into AutoMaker-Org:v0.15.0rc Feb 22, 2026
7 checks passed
@gsxdsm gsxdsm deleted the fix/restoring-view branch February 22, 2026 18:45
gsxdsm added a commit to gsxdsm/automaker that referenced this pull request Feb 23, 2026
…eed up some cli models with session resume (AutoMaker-Org#801)

* Changes from fix/restoring-view

* feat: Add resume query safety checks and optimize store selectors

* feat: Improve session management and model normalization

* refactor: Extract prompt building logic and handle file path parsing for renames
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant