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
+20 -3
View File
@@ -7,7 +7,19 @@ import { backoff } from '@/utils/time'
import { AsyncLock } from '@/utils/lock'
import type { RawJSONLines } from '@/claude/types'
import { configuration } from '@/configuration'
import type { AgentState, ClientToServerEvents, MessageContent, MessageMeta, Metadata, ServerToClientEvents, Session, Update, UserMessage } from './types'
import type {
AgentState,
ClientToServerEvents,
MessageContent,
MessageMeta,
Metadata,
ServerToClientEvents,
Session,
SessionModelMode,
SessionPermissionMode,
Update,
UserMessage
} from './types'
import { AgentStateSchema, MetadataSchema, UserMessageSchema } from './types'
import { RpcHandlerManager } from './rpc/RpcHandlerManager'
import { registerCommonHandlers } from '../modules/common/registerCommonHandlers'
@@ -300,12 +312,17 @@ export class ApiSessionClient extends EventEmitter {
})
}
keepAlive(thinking: boolean, mode: 'local' | 'remote'): void {
keepAlive(
thinking: boolean,
mode: 'local' | 'remote',
runtime?: { permissionMode?: SessionPermissionMode; modelMode?: SessionModelMode }
): void {
this.socket.volatile.emit('session-alive', {
sid: this.sessionId,
time: Date.now(),
thinking,
mode
mode,
...(runtime ?? {})
})
}
+15 -6
View File
@@ -1,6 +1,5 @@
import { z } from 'zod'
import { UsageSchema } from '@/claude/types'
import type { PermissionMode } from '@/claude/loop'
import type {
TerminalClosePayload,
TerminalExitPayload,
@@ -14,6 +13,11 @@ import type {
export type Usage = z.infer<typeof UsageSchema>
export type ClaudePermissionMode = 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan'
export type CodexPermissionMode = 'default' | 'read-only' | 'safe-yolo' | 'yolo'
export type SessionPermissionMode = ClaudePermissionMode | CodexPermissionMode
export type SessionModelMode = 'default' | 'sonnet' | 'opus'
export type Metadata = {
path: string
host: string
@@ -145,8 +149,8 @@ export type Session = {
agentStateVersion: number
thinking?: boolean
thinkingAt?: number
permissionMode?: PermissionMode | null
modelMode?: 'default' | 'sonnet' | 'opus' | null
permissionMode?: SessionPermissionMode
modelMode?: SessionModelMode
}
export const MachineMetadataSchema = z.object({
@@ -273,8 +277,6 @@ export type CreateMachineResponse = z.infer<typeof CreateMachineResponseSchema>
export const MessageMetaSchema = z.object({
sentFrom: z.string().optional(),
permissionMode: z.string().optional(),
model: z.string().nullable().optional(),
fallbackModel: z.string().nullable().optional(),
customSystemPrompt: z.string().nullable().optional(),
appendSystemPrompt: z.string().nullable().optional(),
@@ -323,7 +325,14 @@ export interface ServerToClientEvents {
export interface ClientToServerEvents {
message: (data: { sid: string; message: unknown; localId?: string }) => void
'session-alive': (data: { sid: string; time: number; thinking: boolean; mode?: 'local' | 'remote' }) => void
'session-alive': (data: {
sid: string
time: number
thinking: boolean
mode?: 'local' | 'remote'
permissionMode?: SessionPermissionMode
modelMode?: SessionModelMode
}) => void
'session-end': (data: { sid: string; time: number }) => void
'update-metadata': (data: { sid: string; expectedVersion: number; metadata: unknown }, cb: (answer: {
result: 'error'