@@ -2,7 +2,8 @@ export * from "./CodexStatusIcon";
22
33import type { ComposerControl } from "@/renderer/components/thread/ThreadComposer" ;
44import { CodexStatusIcon } from "./CodexStatusIcon" ;
5- import { fullAccessToggle , planWorkToggle } from "../composerControlBuilders" ;
5+ import { planWorkToggle } from "../composerControlBuilders" ;
6+ import type { AgentCapability , ThreadConfig } from "@/shared/contracts" ;
67import {
78 registerCommitGenDefaults ,
89 registerComposerControls ,
@@ -96,8 +97,8 @@ const CODEX_PERMISSION_PRESETS = [
9697 {
9798 id : "auto-review" ,
9899 label : "Auto-review" ,
99- hint : "Review failures " ,
100- approvalPolicies : [ "on-failure " ] ,
100+ hint : "Review on request " ,
101+ approvalPolicies : [ "on-request " ] ,
101102 sandboxModes : [ "workspace-write" ] ,
102103 } ,
103104 {
@@ -137,8 +138,45 @@ function isCodexPermissionPresetSelected(
137138 ) ;
138139}
139140
141+ function buildCodexPermissionControl (
142+ capabilities : AgentCapability ,
143+ config : ThreadConfig ,
144+ isDisabled : boolean ,
145+ onConfigChange : ( patch : Partial < ThreadConfig > ) => void ,
146+ ) : ComposerControl | null {
147+ const approvalIds = new Set ( capabilities . approvalPolicies . map ( ( policy ) => policy . id ) ) ;
148+ const sandboxIds = new Set ( capabilities . sandboxModes . map ( ( mode ) => mode . id ) ) ;
149+ const permissionPresets = CODEX_PERMISSION_PRESETS . flatMap ( ( preset ) => {
150+ const resolved = resolveCodexPermissionPreset ( preset , approvalIds , sandboxIds ) ;
151+ return resolved ? [ { ...preset , ...resolved } ] : [ ] ;
152+ } ) ;
153+ if ( permissionPresets . length === 0 ) return null ;
154+ const current =
155+ permissionPresets . find ( ( preset ) => isCodexPermissionPresetSelected ( preset , config ) ) ??
156+ permissionPresets [ 0 ] ! ;
157+ return {
158+ iconKind : "permission" ,
159+ options : permissionPresets . map ( ( preset ) => ( {
160+ id : preset . id ,
161+ label : preset . label ,
162+ hint : preset . hint ,
163+ } ) ) ,
164+ hideLabelOnWrap : true ,
165+ value : current . id ,
166+ isDisabled,
167+ onChange : ( value : string ) => {
168+ const preset = permissionPresets . find ( ( option ) => option . id === value ) ;
169+ if ( ! preset ) return ;
170+ onConfigChange ( {
171+ approvalPolicy : preset . approvalPolicy ,
172+ sandboxMode : preset . sandboxMode ,
173+ } ) ;
174+ } ,
175+ } ;
176+ }
177+
140178registerComposerControls ( "codex" , {
141- // ACP exposes plan mode and the coupled approval/sandbox preset selector.
179+ // ACP exposes plan mode in addition to the preset selector.
142180 gui : ( { capabilities, config, isDisabled, onConfigChange } ) => {
143181 const isPlanMode = ( config . mode ?? "agent" ) !== "agent" ;
144182 const controls : ComposerControl [ ] = [
@@ -148,59 +186,24 @@ registerComposerControls("codex", {
148186 onChange : ( isSelected ) => onConfigChange ( { mode : isSelected ? "plan" : "agent" } ) ,
149187 } ) ,
150188 ] ;
151-
152- const approvalIds = new Set ( capabilities . approvalPolicies . map ( ( policy ) => policy . id ) ) ;
153- const sandboxIds = new Set ( capabilities . sandboxModes . map ( ( mode ) => mode . id ) ) ;
154- const permissionPresets = CODEX_PERMISSION_PRESETS . flatMap ( ( preset ) => {
155- const resolved = resolveCodexPermissionPreset ( preset , approvalIds , sandboxIds ) ;
156- return resolved ? [ { ...preset , ...resolved } ] : [ ] ;
157- } ) ;
158- if ( permissionPresets . length > 0 ) {
159- const currentPermissionPreset =
160- permissionPresets . find ( ( preset ) => isCodexPermissionPresetSelected ( preset , config ) ) ??
161- permissionPresets [ 0 ] ! ;
162- controls . push ( {
163- iconKind : "permission" ,
164- options : permissionPresets . map ( ( preset ) => ( {
165- id : preset . id ,
166- label : preset . label ,
167- hint : preset . hint ,
168- } ) ) ,
169- hideLabelOnWrap : true ,
170- value : currentPermissionPreset . id ,
171- isDisabled,
172- onChange : ( value ) => {
173- const preset = permissionPresets . find ( ( option ) => option . id === value ) ;
174- if ( ! preset ) return ;
175- onConfigChange ( {
176- approvalPolicy : preset . approvalPolicy ,
177- sandboxMode : preset . sandboxMode ,
178- } ) ;
179- } ,
180- } ) ;
181- }
189+ const permission = buildCodexPermissionControl (
190+ capabilities ,
191+ config ,
192+ isDisabled ,
193+ onConfigChange ,
194+ ) ;
195+ if ( permission ) controls . push ( permission ) ;
182196 return controls ;
183197 } ,
184- // Terminal CLI ignores `mode: "plan"` and exposes a single Full Access
185- // toggle instead of the paired approval/sandbox selector .
198+ // Terminal CLI ignores `mode: "plan"` but uses the same preset selector
199+ // as GUI for permissions so both surfaces stay in lockstep .
186200 terminal : ( { capabilities, config, isDisabled, onConfigChange } ) => {
187- const hasPermissions =
188- capabilities . approvalPolicies . length > 0 || capabilities . sandboxModes . length > 0 ;
189- if ( ! hasPermissions ) return [ ] ;
190- const isFullAccess =
191- config . approvalPolicy === "never" && config . sandboxMode === "danger-full-access" ;
192- return [
193- fullAccessToggle ( {
194- isFullAccess,
195- isDisabled,
196- onChange : ( selected ) => {
197- if ( selected ) {
198- onConfigChange ( { approvalPolicy : "never" , sandboxMode : "danger-full-access" } ) ;
199- } else {
200- onConfigChange ( { approvalPolicy : "" , sandboxMode : "" } ) ;
201- }
202- } ,
203- } ) ,
204- ] ;
201+ const permission = buildCodexPermissionControl (
202+ capabilities ,
203+ config ,
204+ isDisabled ,
205+ onConfigChange ,
206+ ) ;
207+ return permission ? [ permission ] : [ ] ;
205208 } ,
206209} ) ;
0 commit comments