feat: add real-time permission and model mode synchronization close #21

Implements bidirectional sync of permission/model modes between CLI sessions and web app. Adds Codex-specific permission modes (read-only, safe-yolo, yolo) alongside Claude's modes. Web can now control CLI session state via RPC set-session-config handler, while CLI broadcasts state changes through keep-alive payloads. UI controls are flavor-aware, showing appropriate modes for Claude vs Codex vs Gemini. Type centralization in api/types eliminates circular dependencies.
This commit is contained in:
weishu
2025-12-28 20:51:18 +08:00
parent 2dc09a80db
commit 334cebc2da
21 changed files with 361 additions and 120 deletions
+35 -7
View File
@@ -27,7 +27,7 @@ type SessionSummary = {
metadata: SessionSummaryMetadata | null
todoProgress: { completed: number; total: number } | null
pendingRequestsCount: number
modelMode?: 'default' | 'sonnet' | 'opus' | null
modelMode?: 'default' | 'sonnet' | 'opus'
}
function toSessionSummary(session: Session): SessionSummary {
@@ -55,12 +55,12 @@ function toSessionSummary(session: Session): SessionSummary {
metadata,
todoProgress,
pendingRequestsCount,
modelMode: session.modelMode ?? null
modelMode: session.modelMode
}
}
const permissionModeSchema = z.object({
mode: z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan'])
mode: z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan', 'read-only', 'safe-yolo', 'yolo'])
})
const modelModeSchema = z.object({
@@ -159,8 +159,26 @@ export function createSessionsRoutes(getSyncEngine: () => SyncEngine | null): Ho
return c.json({ error: 'Invalid body' }, 400)
}
await engine.setPermissionMode(sessionResult.sessionId, parsed.data.mode)
return c.json({ ok: true })
const flavor = sessionResult.session.metadata?.flavor ?? 'claude'
const mode = parsed.data.mode
const claudeModes = new Set(['default', 'acceptEdits', 'bypassPermissions', 'plan'])
const codexModes = new Set(['default', 'read-only', 'safe-yolo', 'yolo'])
if (flavor === 'gemini') {
return c.json({ error: 'Permission mode not supported for Gemini sessions' }, 400)
}
if (flavor === 'codex' ? !codexModes.has(mode) : !claudeModes.has(mode)) {
return c.json({ error: 'Invalid permission mode for session flavor' }, 400)
}
try {
await engine.applySessionConfig(sessionResult.sessionId, { permissionMode: mode })
return c.json({ ok: true })
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to apply permission mode'
return c.json({ error: message }, 409)
}
})
app.post('/sessions/:id/model', async (c) => {
@@ -180,8 +198,18 @@ export function createSessionsRoutes(getSyncEngine: () => SyncEngine | null): Ho
return c.json({ error: 'Invalid body' }, 400)
}
await engine.setModelMode(sessionResult.sessionId, parsed.data.model)
return c.json({ ok: true })
const flavor = sessionResult.session.metadata?.flavor ?? 'claude'
if (flavor !== 'claude') {
return c.json({ error: 'Model mode is only supported for Claude sessions' }, 400)
}
try {
await engine.applySessionConfig(sessionResult.sessionId, { modelMode: parsed.data.model })
return c.json({ ok: true })
} catch (error) {
const message = error instanceof Error ? error.message : 'Failed to apply model mode'
return c.json({ error: message }, 409)
}
})
app.get('/sessions/:id/slash-commands', async (c) => {