Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Add a persisted "mode after plan approval" setting - #3728

Closed
haacked wants to merge 2 commits into
mainfrom
posthog-code/default-plan-approval-mode
Closed

haacked wants to merge 2 commits into
mainfrom
posthog-code/default-plan-approval-mode

Conversation

@haacked

@haacked haacked commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Approving a plan pre-selects a continue-mode that's either a sticky "last used" choice or a hardcoded "auto" fallback — there's no way to pin a specific mode as the permanent default. Pick a different mode once (e.g. for one sensitive plan), and every future plan approval silently defaults to that mode too.

Screenshot 2026-07-22 at 4 16 23 PM

Changes

  • Adds defaultPlanApprovalMode to the settings store (packages/ui/src/features/settings/settingsStore.ts), default "last_used" (fully backward compatible).
  • Exposes it as "Mode after plan approval" in Settings → General, right below "Initial task mode" (GeneralSettings.tsx).
  • PlanApprovalSelector now resolves its pre-selected mode from this pinned setting first, falling back to the existing remembered/last-used chain when it's "last_used" or the pinned mode isn't offered.
  • Since both the Claude and Codex adapters route their plan-approval prompt through this same shared UI component, the change covers both without touching packages/agent or packages/core.

How did you test this?

  • pnpm --filter @posthog/ui typecheck and pnpm --filter @posthog/ui test (added cases to settingsStore.test.ts and PlanApprovalSelector.test.tsx covering persistence round-trip and the pinned-mode-overrides-remembered-mode / pinned-mode-not-offered branches) — all passing.
  • biome check on the changed files.

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

Created with PostHog Code

Previously, the mode pre-selected when approving a plan was either the sticky "last used" choice or a hardcoded fallback to "auto" — there was no way to pin a specific mode as the permanent default, so picking a different mode once (e.g. for one sensitive plan) silently became the default for every future approval.

Adds `defaultPlanApprovalMode` to the settings store (default `"last_used"`, fully backward compatible), exposed as "Mode after plan approval" in Settings → General, right below "Initial task mode". `PlanApprovalSelector` now prefers this pinned mode over the remembered last-used mode when set. Since both the Claude and Codex adapters route their plan-approval prompt through the same shared `PlanApprovalSelector` UI, this covers both without touching `packages/agent` or `packages/core`.

Generated-By: PostHog Code
Task-Id: 602d18f7-fc49-4e98-865e-6cbf9e27a91f
@trunk-io

trunk-io Bot commented Jul 22, 2026

Copy link
Copy Markdown

Merging to main in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@github-actions

github-actions Bot commented Jul 22, 2026 •

Copy link
Copy Markdown

React Doctor found no issues in the changed files. 🎉

Reviewed by React Doctor for commit 2a141a7.

Extract PlanApprovalSelector's mode-resolution chain into a standalone, testable `resolveInitialPlanApprovalMode` function, and derive the settings dropdown's mode options from a shared `PLAN_APPROVAL_MODES` list in `modeStyles.tsx` instead of hardcoding them separately in `GeneralSettings.tsx`.

Generated-By: PostHog Code
Task-Id: 602d18f7-fc49-4e98-865e-6cbf9e27a91f
@haacked haacked added the reviewhog ($$$) Reviews pull requests before humans do label Jul 22, 2026
@posthog

posthog Bot commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

🦔 ReviewHog reviewed this pull request

Found 0 must fix, 1 should fix, 0 consider.

Published 1 finding (view the review).

@posthog

posthog Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

ReviewHog Alpha 🦔 If you find any issues helpful - please reply "valid", "invalid", etc., for evaluation purposes 🙏

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ReviewHog Report

Changes

Issues: 1 issue

Files (4)
  • packages/ui/src/features/permissions/PlanApprovalSelector.tsx
  • packages/ui/src/features/sessions/modeStyles.tsx
  • packages/ui/src/features/settings/sections/GeneralSettings.tsx
  • packages/ui/src/features/settings/settingsStore.ts

Comment on lines +70 to +80

// Concrete modes `buildExitPlanModePermissionOptions` (packages/agent) can
// offer when a plan is approved, excluding "plan" itself. Kept here, next to
// the label map settings/approval UI already share, so both pick modes from
// one list.
export const PLAN_APPROVAL_MODES: readonly ExecutionMode[] = [
"auto",
"default",
"acceptEdits",
"bypassPermissions",
];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

PLAN_APPROVAL_MODES hardcodes Claude-only mode ids, breaking the pinned setting for the Codex adapter

should_fix bug

Why we think it's a valid issue
  • Checked: PR-head PLAN_APPROVAL_MODES (modeStyles.tsx:75-80) and its MODE_LABELS/MODE_STYLES entries for full-access (lines 42-43, 68); the pinned branch if (defaultApprovalMode !== "last_used" && has(defaultApprovalMode)) in resolveInitialPlanApprovalMode (PlanApprovalSelector.tsx:48-50); Claude's buildExitPlanModePermissionOptions (permission-options.ts:107-136); and Codex's requestPlanImplementation (codex-app-server-agent.ts:1063-1099).
  • Found: Codex offers exactly auto plus (ALLOW_BYPASS-gated) full-access (codex-app-server-agent.ts:1078-1092) — never default/acceptEdits/bypassPermissions. So for a Codex session has() is false for three of the four picklist entries, and the pinned branch falls through to the last-used/auto chain — the pin silently no-ops. full-access is a valid ExecutionMode with existing label/style entries but is absent from PLAN_APPROVAL_MODES, so Codex's one meaningful elevated mode is unpinnable. The picker is rendered globally in General settings (unconditional PLAN_APPROVAL_MODES.map), not adapter-scoped, so Codex-only users see dead options.
  • Impact: A real, reproducible defect directly from this PR: concrete trigger (Codex session + pin any of three Claude-only modes → no-op; or want to pin full-access → impossible) and concrete consequence (explicit user preference silently ignored; relevant mode unreachable). Contradicts the PR's stated Codex coverage. Clears the keep bar — not speculative, not style, premise verified.
  • Priority: Lowering must_fix → should_fix. It fails safe: the no-op degrades to the exact prior last-used/auto behavior (no dangerous or data-losing action), the default last_used is backward compatible so untouched users are unaffected, and only Codex users who actively select a Claude-only mode are hit. A completeness gap in a new convenience feature, not a regression, crash, security, or data-loss bug — meaningful to fix but not release-blocking.
Issue description

PLAN_APPROVAL_MODES is ["auto", "default", "acceptEdits", "bypassPermissions"], and GeneralSettings.tsx uses exactly this list to populate the new "Mode after plan approval" picker. But the two adapters that route through PlanApprovalSelector offer completely different approve-option sets: Claude's buildExitPlanModePermissionOptions (packages/agent/src/adapters/claude/permissions/permission-options.ts) offers bypassPermissions/auto/acceptEdits/default, while Codex's requestPlanImplementation (packages/agent/src/adapters/codex-app-server/codex-app-server-agent.ts:1063-1099) only ever offers auto and full-access (gated by ALLOW_BYPASS) — never default, acceptEdits, or bypassPermissions. In resolveInitialPlanApprovalMode (PlanApprovalSelector.tsx), the pinned-mode branch is if (defaultApprovalMode !== "last_used" && has(defaultApprovalMode)). For a Codex session, has() will be false for default/acceptEdits/bypassPermissions (they're never in approveOptions), so pinning to any of those three Settings choices silently falls through to the old last-used/auto fallback chain — a no-op that contradicts what the user configured. Worse, Codex's actual bypass-equivalent mode, "full-access", isn't in PLAN_APPROVAL_MODES at all, so a Codex user can never pin the one mode that would be meaningful for them, even though "full-access" is a valid ExecutionMode (packages/shared/src/exec-types.ts) and already has an entry in this same file's MODE_LABELS/MODE_STYLES maps. This directly contradicts the PR's stated claim that "both the Claude and Codex adapters route their plan-approval prompt through this same shared UI component, [so] the change covers both." In practice, for Codex users the setting only works for the "auto" choice; the other three choices are dead options and the relevant elevated-trust mode is unreachable.

Suggested fix

Make the offered pin choices adapter-agnostic and derived from what can actually be offered, e.g. include "full-access" alongside the Claude modes (PLAN_APPROVAL_MODES = ["auto", "default", "acceptEdits", "bypassPermissions", "full-access"]), or better, have GeneralSettings.tsx/resolveInitialPlanApprovalMode only surface/select from modes that are relevant per-adapter. At minimum, add "full-access" to the list so Codex users have a working pinned option, and document (or filter) that default/acceptEdits/bypassPermissions are Claude-only and will silently no-op under Codex.

Prompt to fix with AI (copy-paste)
## Context
@packages/ui/src/features/sessions/modeStyles.tsx#L70-80

<issue_description>
`PLAN_APPROVAL_MODES` is `["auto", "default", "acceptEdits", "bypassPermissions"]`, and `GeneralSettings.tsx` uses exactly this list to populate the new "Mode after plan approval" picker. But the two adapters that route through `PlanApprovalSelector` offer completely different approve-option sets: Claude's `buildExitPlanModePermissionOptions` (packages/agent/src/adapters/claude/permissions/permission-options.ts) offers `bypassPermissions`/`auto`/`acceptEdits`/`default`, while Codex's `requestPlanImplementation` (packages/agent/src/adapters/codex-app-server/codex-app-server-agent.ts:1063-1099) only ever offers `auto` and `full-access` (gated by `ALLOW_BYPASS`) — never `default`, `acceptEdits`, or `bypassPermissions`. In `resolveInitialPlanApprovalMode` (PlanApprovalSelector.tsx), the pinned-mode branch is `if (defaultApprovalMode !== "last_used" && has(defaultApprovalMode))`. For a Codex session, `has()` will be false for `default`/`acceptEdits`/`bypassPermissions` (they're never in `approveOptions`), so pinning to any of those three Settings choices silently falls through to the old last-used/auto fallback chain — a no-op that contradicts what the user configured. Worse, Codex's actual bypass-equivalent mode, `"full-access"`, isn't in `PLAN_APPROVAL_MODES` at all, so a Codex user can never pin the one mode that would be meaningful for them, even though `"full-access"` is a valid `ExecutionMode` (packages/shared/src/exec-types.ts) and already has an entry in this same file's `MODE_LABELS`/`MODE_STYLES` maps. This directly contradicts the PR's stated claim that "both the Claude and Codex adapters route their plan-approval prompt through this same shared UI component, [so] the change covers both." In practice, for Codex users the setting only works for the "auto" choice; the other three choices are dead options and the relevant elevated-trust mode is unreachable.
</issue_description>

<issue_validation>
- **Checked:** PR-head `PLAN_APPROVAL_MODES` (modeStyles.tsx:75-80) and its `MODE_LABELS`/`MODE_STYLES` entries for `full-access` (lines 42-43, 68); the pinned branch `if (defaultApprovalMode !== "last_used" && has(defaultApprovalMode))` in `resolveInitialPlanApprovalMode` (PlanApprovalSelector.tsx:48-50); Claude's `buildExitPlanModePermissionOptions` (permission-options.ts:107-136); and Codex's `requestPlanImplementation` (codex-app-server-agent.ts:1063-1099).
- **Found:** Codex offers exactly `auto` plus (ALLOW_BYPASS-gated) `full-access` (codex-app-server-agent.ts:1078-1092) — never `default`/`acceptEdits`/`bypassPermissions`. So for a Codex session `has()` is false for three of the four picklist entries, and the pinned branch falls through to the last-used/auto chain — the pin silently no-ops. `full-access` is a valid `ExecutionMode` with existing label/style entries but is absent from `PLAN_APPROVAL_MODES`, so Codex's one meaningful elevated mode is unpinnable. The picker is rendered globally in General settings (unconditional `PLAN_APPROVAL_MODES.map`), not adapter-scoped, so Codex-only users see dead options.
- **Impact:** A real, reproducible defect directly from this PR: concrete trigger (Codex session + pin any of three Claude-only modes → no-op; or want to pin `full-access` → impossible) and concrete consequence (explicit user preference silently ignored; relevant mode unreachable). Contradicts the PR's stated Codex coverage. Clears the keep bar — not speculative, not style, premise verified.
- **Priority:** Lowering must_fix → should_fix. It fails safe: the no-op degrades to the exact prior last-used/auto behavior (no dangerous or data-losing action), the default `last_used` is backward compatible so untouched users are unaffected, and only Codex users who actively select a Claude-only mode are hit. A completeness gap in a new convenience feature, not a regression, crash, security, or data-loss bug — meaningful to fix but not release-blocking.
</issue_validation>

## Task
Investigate the issue and solve it

<potential_solution>
Make the offered pin choices adapter-agnostic and derived from what can actually be offered, e.g. include `"full-access"` alongside the Claude modes (`PLAN_APPROVAL_MODES = ["auto", "default", "acceptEdits", "bypassPermissions", "full-access"]`), or better, have `GeneralSettings.tsx`/`resolveInitialPlanApprovalMode` only surface/select from modes that are relevant per-adapter. At minimum, add `"full-access"` to the list so Codex users have a working pinned option, and document (or filter) that `default`/`acceptEdits`/`bypassPermissions` are Claude-only and will silently no-op under Codex.
</potential_solution>

@haacked
haacked marked this pull request as ready for review July 22, 2026 23:18
@greptile-apps

greptile-apps Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
packages/ui/src/features/permissions/PlanApprovalSelector.tsx:113-121
**Resolved Mode Leaves State Stale**

When persisted settings hydrate after mount, or the mounted selector receives a new approval with different options, `initialMode` recomputes but the following `useState(initialMode)` retains the old selection. Approving can therefore submit the pre-hydration mode or a mode that is not offered by the current approval request.

### Issue 2 of 2
packages/ui/src/features/sessions/modeStyles.tsx:75-80
**Codex Full Access Is Missing**

Codex offers `full-access` after plan approval when bypass is allowed, but this list controls the new settings menu and omits that mode. Codex users can select Full access during approval but cannot pin it as their default.

```suggestion
export const PLAN_APPROVAL_MODES: readonly ExecutionMode[] = [
  "auto",
  "default",
  "acceptEdits",
  "bypassPermissions",
  "full-access",
];
```

Reviews (1): Last reviewed commit: "Simplify plan-approval mode resolution a..." | Re-trigger Greptile

Comment on lines +113 to +121
const initialMode = useMemo(
() =>
resolveInitialPlanApprovalMode(
approveOptions,
defaultApprovalMode,
lastApprovalMode,
),
[approveOptions, lastApprovalMode, defaultApprovalMode],
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Resolved Mode Leaves State Stale

When persisted settings hydrate after mount, or the mounted selector receives a new approval with different options, initialMode recomputes but the following useState(initialMode) retains the old selection. Approving can therefore submit the pre-hydration mode or a mode that is not offered by the current approval request.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/features/permissions/PlanApprovalSelector.tsx
Line: 113-121

Comment:
**Resolved Mode Leaves State Stale**

When persisted settings hydrate after mount, or the mounted selector receives a new approval with different options, `initialMode` recomputes but the following `useState(initialMode)` retains the old selection. Approving can therefore submit the pre-hydration mode or a mode that is not offered by the current approval request.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +75 to +80
export const PLAN_APPROVAL_MODES: readonly ExecutionMode[] = [
"auto",
"default",
"acceptEdits",
"bypassPermissions",
];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Codex Full Access Is Missing

Codex offers full-access after plan approval when bypass is allowed, but this list controls the new settings menu and omits that mode. Codex users can select Full access during approval but cannot pin it as their default.

Suggested change
export const PLAN_APPROVAL_MODES: readonly ExecutionMode[] = [
"auto",
"default",
"acceptEdits",
"bypassPermissions",
];
export const PLAN_APPROVAL_MODES: readonly ExecutionMode[] = [
"auto",
"default",
"acceptEdits",
"bypassPermissions",
"full-access",
];

Rule Used: When implementing new features, ensure that the UI... (source)

Learned From
PostHog/posthog#32595
PostHog/posthog#32677

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/features/sessions/modeStyles.tsx
Line: 75-80

Comment:
**Codex Full Access Is Missing**

Codex offers `full-access` after plan approval when bypass is allowed, but this list controls the new settings menu and omits that mode. Codex users can select Full access during approval but cannot pin it as their default.

```suggestion
export const PLAN_APPROVAL_MODES: readonly ExecutionMode[] = [
  "auto",
  "default",
  "acceptEdits",
  "bypassPermissions",
  "full-access",
];
```

**Rule Used:** When implementing new features, ensure that the UI... ([source](https://app.greptile.com/posthog-org-19734/-/custom-context?memory=5d57f0af-0be1-44de-8885-055f27e2885f))

**Learned From**
[PostHog/posthog#32595](https://github.com/PostHog/posthog/pull/32595)
[PostHog/posthog#32677](https://github.com/PostHog/posthog/pull/32677)

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@haacked
haacked marked this pull request as draft July 22, 2026 23:26
@haacked

haacked commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

I didn't realize the current behavior is supposed to be last used by default. I think there's a bug in that. If we fix that bug, I don't think we need this PR.

@haacked haacked closed this Jul 22, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

reviewhog ($$$) Reviews pull requests before humans do

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant