Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 73 additions & 11 deletions sources/components/tools/PermissionFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface PermissionFooterProps {
reason?: string;
mode?: string;
allowedTools?: string[];
decision?: 'approved' | 'approved_for_session' | 'denied' | 'abort';
decision?: 'approved' | 'approved_for_session' | 'approved_execpolicy_amendment' | 'denied' | 'abort';
};
sessionId: string;
toolName: string;
Expand All @@ -26,9 +26,19 @@ export const PermissionFooter: React.FC<PermissionFooterProps> = ({ permission,
const [loadingButton, setLoadingButton] = useState<'allow' | 'deny' | 'abort' | null>(null);
const [loadingAllEdits, setLoadingAllEdits] = useState(false);
const [loadingForSession, setLoadingForSession] = useState(false);
const [loadingExecPolicy, setLoadingExecPolicy] = useState(false);

// Check if this is a Codex session - check both metadata.flavor and tool name prefix
const isCodex = metadata?.flavor === 'codex' || toolName.startsWith('Codex');
// Codex always provides proposed_execpolicy_amendment
const execPolicyCommand = (() => {
const proposedAmendment = toolInput?.proposedExecpolicyAmendment ?? toolInput?.proposed_execpolicy_amendment;
if (Array.isArray(proposedAmendment)) {
return proposedAmendment.filter((part: unknown): part is string => typeof part === 'string' && part.length > 0);
}
return [];
})();
const canApproveExecPolicy = isCodex && execPolicyCommand.length > 0;

const handleApprove = async () => {
if (permission.status !== 'pending' || loadingButton !== null || loadingAllEdits || loadingForSession) return;
Expand Down Expand Up @@ -93,7 +103,7 @@ export const PermissionFooter: React.FC<PermissionFooterProps> = ({ permission,

// Codex-specific handlers
const handleCodexApprove = async () => {
if (permission.status !== 'pending' || loadingButton !== null || loadingForSession) return;
if (permission.status !== 'pending' || loadingButton !== null || loadingForSession || loadingExecPolicy) return;

setLoadingButton('allow');
try {
Expand All @@ -106,7 +116,7 @@ export const PermissionFooter: React.FC<PermissionFooterProps> = ({ permission,
};

const handleCodexApproveForSession = async () => {
if (permission.status !== 'pending' || loadingButton !== null || loadingForSession) return;
if (permission.status !== 'pending' || loadingButton !== null || loadingForSession || loadingExecPolicy) return;

setLoadingForSession(true);
try {
Expand All @@ -117,9 +127,29 @@ export const PermissionFooter: React.FC<PermissionFooterProps> = ({ permission,
setLoadingForSession(false);
}
};

const handleCodexApproveExecPolicy = async () => {
if (permission.status !== 'pending' || loadingButton !== null || loadingForSession || loadingExecPolicy || !canApproveExecPolicy) return;

setLoadingExecPolicy(true);
try {
await sessionAllow(
sessionId,
permission.id,
undefined,
undefined,
'approved_execpolicy_amendment',
{ command: execPolicyCommand }
);
} catch (error) {
console.error('Failed to approve with execpolicy amendment:', error);
} finally {
setLoadingExecPolicy(false);
}
};

const handleCodexAbort = async () => {
if (permission.status !== 'pending' || loadingButton !== null || loadingForSession) return;
if (permission.status !== 'pending' || loadingButton !== null || loadingForSession || loadingExecPolicy) return;

setLoadingButton('abort');
try {
Expand Down Expand Up @@ -159,6 +189,7 @@ export const PermissionFooter: React.FC<PermissionFooterProps> = ({ permission,
// Codex-specific status detection with fallback
const isCodexApproved = isCodex && isApproved && (permission.decision === 'approved' || !permission.decision);
const isCodexApprovedForSession = isCodex && isApproved && permission.decision === 'approved_for_session';
const isCodexApprovedExecPolicy = isCodex && isApproved && permission.decision === 'approved_execpolicy_amendment';
const isCodexAborted = isCodex && isDenied && permission.decision === 'abort';

const styles = StyleSheet.create({
Expand Down Expand Up @@ -268,10 +299,10 @@ export const PermissionFooter: React.FC<PermissionFooterProps> = ({ permission,
styles.button,
isPending && styles.buttonAllow,
isCodexApproved && styles.buttonSelected,
(isCodexAborted || isCodexApprovedForSession) && styles.buttonInactive
(isCodexAborted || isCodexApprovedForSession || isCodexApprovedExecPolicy) && styles.buttonInactive
]}
onPress={handleCodexApprove}
disabled={!isPending || loadingButton !== null || loadingForSession}
disabled={!isPending || loadingButton !== null || loadingForSession || loadingExecPolicy}
activeOpacity={isPending ? 0.7 : 1}
>
{loadingButton === 'allow' && isPending ? (
Expand All @@ -291,16 +322,47 @@ export const PermissionFooter: React.FC<PermissionFooterProps> = ({ permission,
)}
</TouchableOpacity>

{/* Codex: Yes, always allow this command button */}
{canApproveExecPolicy && (
<TouchableOpacity
style={[
styles.button,
isPending && styles.buttonForSession,
isCodexApprovedExecPolicy && styles.buttonSelected,
(isCodexAborted || isCodexApproved || isCodexApprovedForSession) && styles.buttonInactive
]}
onPress={handleCodexApproveExecPolicy}
disabled={!isPending || loadingButton !== null || loadingForSession || loadingExecPolicy}
activeOpacity={isPending ? 0.7 : 1}
>
{loadingExecPolicy && isPending ? (
<View style={[styles.buttonContent, { width: 40, height: 20, justifyContent: 'center' }]}>
<ActivityIndicator size={Platform.OS === 'ios' ? "small" : 14 as any} color={styles.loadingIndicatorForSession.color} />
</View>
) : (
<View style={styles.buttonContent}>
<Text style={[
styles.buttonText,
isPending && styles.buttonTextForSession,
isCodexApprovedExecPolicy && styles.buttonTextSelected
]} numberOfLines={1} ellipsizeMode="tail">
{t('codex.permissions.yesAlwaysAllowCommand')}
</Text>
</View>
)}
</TouchableOpacity>
)}

{/* Codex: Yes, and don't ask for a session button */}
<TouchableOpacity
style={[
styles.button,
isPending && styles.buttonForSession,
isCodexApprovedForSession && styles.buttonSelected,
(isCodexAborted || isCodexApproved) && styles.buttonInactive
(isCodexAborted || isCodexApproved || isCodexApprovedExecPolicy) && styles.buttonInactive
]}
onPress={handleCodexApproveForSession}
disabled={!isPending || loadingButton !== null || loadingForSession}
disabled={!isPending || loadingButton !== null || loadingForSession || loadingExecPolicy}
activeOpacity={isPending ? 0.7 : 1}
>
{loadingForSession && isPending ? (
Expand All @@ -326,10 +388,10 @@ export const PermissionFooter: React.FC<PermissionFooterProps> = ({ permission,
styles.button,
isPending && styles.buttonDeny,
isCodexAborted && styles.buttonSelected,
(isCodexApproved || isCodexApprovedForSession) && styles.buttonInactive
(isCodexApproved || isCodexApprovedForSession || isCodexApprovedExecPolicy) && styles.buttonInactive
]}
onPress={handleCodexAbort}
disabled={!isPending || loadingButton !== null || loadingForSession}
disabled={!isPending || loadingButton !== null || loadingForSession || loadingExecPolicy}
activeOpacity={isPending ? 0.7 : 1}
>
{loadingButton === 'abort' && isPending ? (
Expand Down Expand Up @@ -477,4 +539,4 @@ export const PermissionFooter: React.FC<PermissionFooterProps> = ({ permission,
</View>
</View>
);
};
};
25 changes: 21 additions & 4 deletions sources/sync/ops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ interface SessionPermissionRequest {
reason?: string;
mode?: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan';
allowTools?: string[];
decision?: 'approved' | 'approved_for_session' | 'denied' | 'abort';
decision?: 'approved' | 'approved_for_session' | 'approved_execpolicy_amendment' | 'denied' | 'abort';
execPolicyAmendment?: {
command: string[];
};
}

// Mode change operation types
Expand Down Expand Up @@ -299,8 +302,22 @@ export async function sessionAbort(sessionId: string): Promise<void> {
/**
* Allow a permission request
*/
export async function sessionAllow(sessionId: string, id: string, mode?: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan', allowedTools?: string[], decision?: 'approved' | 'approved_for_session'): Promise<void> {
const request: SessionPermissionRequest = { id, approved: true, mode, allowTools: allowedTools, decision };
export async function sessionAllow(
sessionId: string,
id: string,
mode?: 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan',
allowedTools?: string[],
decision?: 'approved' | 'approved_for_session' | 'approved_execpolicy_amendment',
execPolicyAmendment?: { command: string[] }
): Promise<void> {
const request: SessionPermissionRequest = {
id,
approved: true,
mode,
allowTools: allowedTools,
decision,
execPolicyAmendment
};
await apiSocket.sessionRPC(sessionId, 'permission', request);
}

Expand Down Expand Up @@ -520,4 +537,4 @@ export type {
TreeNode,
SessionRipgrepResponse,
SessionKillResponse
};
};
20 changes: 12 additions & 8 deletions sources/sync/reducer/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ type StoredPermission = {
reason?: string;
mode?: string;
allowedTools?: string[];
decision?: 'approved' | 'approved_for_session' | 'denied' | 'abort';
decision?: 'approved' | 'approved_for_session' | 'approved_execpolicy_amendment' | 'denied' | 'abort';
};

export type ReducerState = {
Expand Down Expand Up @@ -351,16 +351,20 @@ export function reducer(state: ReducerState, messages: NormalizedMessage[], agen
// Check if we already have a message for this permission ID
const existingMessageId = state.toolIdToMessageId.get(permId);
if (existingMessageId) {
// Update existing tool message with permission info
// Update existing tool message with permission info and latest arguments
const message = state.messages.get(existingMessageId);
if (message?.tool && !message.tool.permission) {
if (message?.tool) {
if (ENABLE_LOGGING) {
console.log(`[REDUCER] Updating existing tool ${permId} with permission`);
}
message.tool.permission = {
id: permId,
status: 'pending'
};
// Always update input to get latest arguments (e.g., proposedExecpolicyAmendment)
message.tool.input = request.arguments;
if (!message.tool.permission) {
message.tool.permission = {
id: permId,
status: 'pending'
};
}
changed.add(existingMessageId);
}
} else {
Expand Down Expand Up @@ -1147,4 +1151,4 @@ function convertReducerMessageToMessage(reducerMsg: ReducerMessage, state: Reduc
}

return null;
}
}
4 changes: 2 additions & 2 deletions sources/sync/storageTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export const AgentStateSchema = z.object({
reason: z.string().nullish(),
mode: z.string().nullish(),
allowedTools: z.array(z.string()).nullish(),
decision: z.enum(['approved', 'approved_for_session', 'denied', 'abort']).nullish()
decision: z.enum(['approved', 'approved_for_session', 'approved_execpolicy_amendment', 'denied', 'abort']).nullish()
})).nullish()
});

Expand Down Expand Up @@ -153,4 +153,4 @@ export interface GitStatus {
aheadCount?: number; // Commits ahead of upstream
behindCount?: number; // Commits behind upstream
stashCount?: number; // Number of stash entries
}
}
4 changes: 2 additions & 2 deletions sources/sync/typesMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export type ToolCall = {
reason?: string;
mode?: string;
allowedTools?: string[];
decision?: 'approved' | 'approved_for_session' | 'denied' | 'abort';
decision?: 'approved' | 'approved_for_session' | 'approved_execpolicy_amendment' | 'denied' | 'abort';
date?: number;
};
}
Expand Down Expand Up @@ -59,4 +59,4 @@ export type ToolCallMessage = {
meta?: MessageMeta;
}

export type Message = UserTextMessage | AgentTextMessage | ToolCallMessage | ModeSwitchMessage;
export type Message = UserTextMessage | AgentTextMessage | ToolCallMessage | ModeSwitchMessage;
6 changes: 3 additions & 3 deletions sources/sync/typesRaw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ const rawToolResultContentSchema = z.object({
result: z.enum(['approved', 'denied']),
mode: z.string().optional(),
allowedTools: z.array(z.string()).optional(),
decision: z.enum(['approved', 'approved_for_session', 'denied', 'abort']).optional(),
decision: z.enum(['approved', 'approved_for_session', 'approved_execpolicy_amendment', 'denied', 'abort']).optional(),
}).optional(),
});
export type RawToolResultContent = z.infer<typeof rawToolResultContentSchema>;
Expand Down Expand Up @@ -158,7 +158,7 @@ type NormalizedAgentContent =
result: 'approved' | 'denied';
mode?: string;
allowedTools?: string[];
decision?: 'approved' | 'approved_for_session' | 'denied' | 'abort';
decision?: 'approved' | 'approved_for_session' | 'approved_execpolicy_amendment' | 'denied' | 'abort';
};
} | {
type: 'summary',
Expand Down Expand Up @@ -422,4 +422,4 @@ export function normalizeRawMessage(id: string, localId: string | null, createdA
}
}
return null;
}
}
1 change: 1 addition & 0 deletions sources/text/_default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,7 @@ export const en = {
codex: {
// Codex permission dialog buttons
permissions: {
yesAlwaysAllowCommand: 'Yes, always allow globally',
yesForSession: "Yes, and don't ask for a session",
stopAndExplain: 'Stop, and explain what to do',
}
Expand Down
1 change: 1 addition & 0 deletions sources/text/translations/ca.ts
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,7 @@ export const ca: TranslationStructure = {
codex: {
// Codex permission dialog buttons
permissions: {
yesAlwaysAllowCommand: 'Sí, permet globalment',
yesForSession: 'Sí, i no preguntar per aquesta sessió',
stopAndExplain: 'Atura, i explica què fer',
}
Expand Down
1 change: 1 addition & 0 deletions sources/text/translations/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,7 @@ export const es: TranslationStructure = {
codex: {
// Codex permission dialog buttons
permissions: {
yesAlwaysAllowCommand: 'Sí, permitir globalmente',
yesForSession: 'Sí, y no preguntar por esta sesión',
stopAndExplain: 'Detener, y explicar qué hacer',
}
Expand Down
1 change: 1 addition & 0 deletions sources/text/translations/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,7 @@ export const pl: TranslationStructure = {
codex: {
// Codex permission dialog buttons
permissions: {
yesAlwaysAllowCommand: 'Tak, zezwól globalnie',
yesForSession: 'Tak, i nie pytaj dla tej sesji',
stopAndExplain: 'Zatrzymaj i wyjaśnij, co zrobić',
}
Expand Down
1 change: 1 addition & 0 deletions sources/text/translations/pt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,7 @@ export const pt: TranslationStructure = {
codex: {
// Codex permission dialog buttons
permissions: {
yesAlwaysAllowCommand: 'Sim, permitir globalmente',
yesForSession: 'Sim, e não perguntar para esta sessão',
stopAndExplain: 'Parar, e explicar o que fazer',
}
Expand Down
1 change: 1 addition & 0 deletions sources/text/translations/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,7 @@ export const ru: TranslationStructure = {
codex: {
// Codex permission dialog buttons
permissions: {
yesAlwaysAllowCommand: 'Да, разрешить глобально',
yesForSession: 'Да, и не спрашивать для этой сессии',
stopAndExplain: 'Остановить и объяснить, что делать',
}
Expand Down
1 change: 1 addition & 0 deletions sources/text/translations/zh-Hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,7 @@ export const zhHans: TranslationStructure = {
codex: {
// Codex permission dialog buttons
permissions: {
yesAlwaysAllowCommand: '是,全局永久允许',
yesForSession: '是,并且本次会话不再询问',
stopAndExplain: '停止,并说明该做什么',
}
Expand Down