- 添加内存消息缓冲区,解决 tab 切换后用户消息丢失问题 - prompt() 去除重复 waitReady(),减少不必要的等待 - 新增 kill_on_drop 工具(FinalizationRegistry),待后续启用 - 新增权限模式 hooks 自动配置(仅在显式指定 permission_mode 时生效) - 清除 serve 模式下的调试 timing 日志
765 lines
22 KiB
TypeScript
765 lines
22 KiB
TypeScript
import { type ChildProcess, spawn } from 'child_process'
|
|
import { createInterface } from 'readline'
|
|
import { jsonParse, jsonStringify } from '../utils/slowOperations.js'
|
|
import { logError } from '../utils/log.js'
|
|
import type { EventBus } from './eventBus.js'
|
|
import { INIT_TIMEOUT_MS, getScriptArgsForChild, loadChildSpawnPrefix } from './childSpawn.js'
|
|
import { ControlChannel } from './sessionControlChannel.js'
|
|
import { routeMessage, type StdoutMessage, type MessageRouterCtx } from './sessionMessageRouter.js'
|
|
import type { SessionBusyStatus, SessionState, InitData, PendingPermission, PendingQuestion } from './types.js'
|
|
import type { DisposableChildProcess } from '../utils/killOnDrop.js'
|
|
import type { SessionMessage } from './transcriptReader.js'
|
|
|
|
export type { InitData, PendingPermission, PendingQuestion }
|
|
export { getScriptArgsForChild }
|
|
export { getChildSpawnArgs, saveChildSpawnPrefix } from './childSpawn.js'
|
|
|
|
type MessageListener = (msg: StdoutMessage) => void
|
|
|
|
export type SessionHandleOptions = {
|
|
sessionId: string
|
|
cwd: string
|
|
model?: string
|
|
agent?: string
|
|
permissionMode?: string
|
|
systemPrompt?: string
|
|
resumeSessionId?: string
|
|
resumeSessionAt?: string
|
|
hooks?: Record<string, unknown>
|
|
eventBus: EventBus
|
|
execPath: string
|
|
scriptArgs: string[]
|
|
verbose?: boolean
|
|
silent?: boolean
|
|
onInit?: (data: InitData) => void
|
|
}
|
|
|
|
export class SessionHandle implements DisposableChildProcess {
|
|
readonly sessionId: string
|
|
readonly cwd: string
|
|
private _spawnCwd: string | undefined
|
|
private child: ChildProcess | null = null
|
|
private _status: SessionState = 'starting'
|
|
private _busyStatus: SessionBusyStatus = { type: 'idle' }
|
|
private _model?: string
|
|
private _agent?: string
|
|
private _providerId?: string
|
|
private _permissionMode?: string
|
|
private _title?: string
|
|
private _costUsd = 0
|
|
private _inputTokens = 0
|
|
private _outputTokens = 0
|
|
private createdAt = Date.now()
|
|
private lastActiveAt = Date.now()
|
|
private eventBus: EventBus
|
|
private pendingPermissions = new Map<string, PendingPermission>()
|
|
private pendingQuestions = new Map<string, PendingQuestion>()
|
|
private promptResolve:
|
|
| ((value: { done: boolean; error?: string }) => void)
|
|
| null = null
|
|
private promptReject: ((reason: unknown) => void) | null = null
|
|
private verbose: boolean
|
|
private lastStderr: string[] = []
|
|
private listeners = new Set<MessageListener>()
|
|
private _initData: InitData | null = null
|
|
private initRequestId: string | null = null
|
|
private initResolve: ((data: InitData) => void) | null = null
|
|
private initReject: ((reason: unknown) => void) | null = null
|
|
private _prompting = false
|
|
private _lastMessageUuid: string | null = null
|
|
private _controlChannel = new ControlChannel()
|
|
private _titleGenerationAttempted = false
|
|
private _firstPromptContent?: string
|
|
private _messageBuffer: SessionMessage[] = []
|
|
|
|
get status(): SessionState {
|
|
return this._status
|
|
}
|
|
get busyStatus(): SessionBusyStatus {
|
|
return this._busyStatus
|
|
}
|
|
get model(): string | undefined {
|
|
return this._model
|
|
}
|
|
get agent(): string | undefined {
|
|
return this._agent
|
|
}
|
|
get permissionMode(): string | undefined {
|
|
return this._permissionMode
|
|
}
|
|
get title(): string | undefined {
|
|
return this._title
|
|
}
|
|
get costUsd(): number {
|
|
return this._costUsd
|
|
}
|
|
get inputTokens(): number {
|
|
return this._inputTokens
|
|
}
|
|
get messageCount(): number {
|
|
return 0
|
|
}
|
|
get usage() {
|
|
return {
|
|
input_tokens: this._inputTokens,
|
|
output_tokens: this._outputTokens,
|
|
}
|
|
}
|
|
get initData(): InitData | null {
|
|
return this._initData
|
|
}
|
|
get prompting(): boolean {
|
|
return this._prompting
|
|
}
|
|
get messageBuffer(): ReadonlyArray<SessionMessage> {
|
|
return this._messageBuffer
|
|
}
|
|
|
|
pushMessage(msg: SessionMessage): void {
|
|
if (this._messageBuffer.length > 0) {
|
|
const last = this._messageBuffer[this._messageBuffer.length - 1]
|
|
if (last.uuid === msg.uuid) return
|
|
}
|
|
this._messageBuffer.push(msg)
|
|
}
|
|
|
|
get lastMessageUuid(): string | null {
|
|
return this._lastMessageUuid
|
|
}
|
|
|
|
get ready(): boolean {
|
|
return this._status === 'running'
|
|
}
|
|
|
|
get silent(): boolean {
|
|
return this.opts.silent ?? false
|
|
}
|
|
|
|
get spawnCwd(): string {
|
|
return this._spawnCwd ?? this.cwd
|
|
}
|
|
|
|
async waitReady(timeoutMs = INIT_TIMEOUT_MS): Promise<void> {
|
|
if (this._status === 'running') return
|
|
if (this._status === 'stopped') {
|
|
throw new Error(`Session ${this.sessionId} is stopped`)
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
const timer = setTimeout(() => {
|
|
reject(new Error(`Session ${this.sessionId} init timed out`))
|
|
}, timeoutMs)
|
|
const origResolve = this.initResolve
|
|
const origReject = this.initReject
|
|
this.initResolve = (data: InitData) => {
|
|
clearTimeout(timer)
|
|
origResolve?.(data)
|
|
resolve()
|
|
}
|
|
this.initReject = (err: unknown) => {
|
|
clearTimeout(timer)
|
|
origReject?.(err)
|
|
reject(err)
|
|
}
|
|
})
|
|
}
|
|
|
|
constructor(private opts: SessionHandleOptions) {
|
|
this.sessionId = opts.sessionId
|
|
this.cwd = opts.cwd
|
|
this.eventBus = opts.eventBus
|
|
this._model = opts.model
|
|
this._agent = opts.agent
|
|
this._permissionMode = opts.permissionMode ?? 'acceptEdits'
|
|
this.verbose = opts.verbose ?? false
|
|
}
|
|
|
|
onMessage(listener: MessageListener): () => void {
|
|
this.listeners.add(listener)
|
|
return () => { this.listeners.delete(listener) }
|
|
}
|
|
|
|
spawn(): void {
|
|
const printArgs = [
|
|
'--print',
|
|
'--input-format',
|
|
'stream-json',
|
|
'--output-format',
|
|
'stream-json',
|
|
...(this.opts.resumeSessionId ? [] : ['--session-id', this.sessionId]),
|
|
...(this.opts.model ? ['--model', this.opts.model] : []),
|
|
...(this._agent ? ['--agent', this._agent] : []),
|
|
'--permission-mode',
|
|
this._permissionMode,
|
|
'--permission-prompt-tool',
|
|
'stdio',
|
|
...(this.opts.resumeSessionId
|
|
? ['--resume', this.opts.resumeSessionId]
|
|
: []),
|
|
...(this.opts.resumeSessionAt
|
|
? ['--resume-session-at', this.opts.resumeSessionAt]
|
|
: []),
|
|
'--verbose',
|
|
]
|
|
|
|
const env: NodeJS.ProcessEnv = {
|
|
...process.env,
|
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
|
|
CSC_SERVE_MODE: '1',
|
|
}
|
|
|
|
if (this.opts.systemPrompt) {
|
|
env.CLAUDE_CODE_SYSTEM_PROMPT = this.opts.systemPrompt
|
|
}
|
|
|
|
const saved = loadChildSpawnPrefix()
|
|
const defineArgs = saved?.defineArgs ?? []
|
|
const featureArgs = saved?.featureArgs ?? []
|
|
const spawnArgs = [...defineArgs, ...featureArgs, ...this.opts.scriptArgs, ...printArgs.filter((x): x is string => x != null)]
|
|
|
|
this._spawnCwd = this.opts.cwd
|
|
this.child = spawn(this.opts.execPath, spawnArgs, {
|
|
cwd: this.opts.cwd,
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
env,
|
|
windowsHide: true,
|
|
})
|
|
|
|
// registerKillOnDrop(this, this.child)
|
|
this.setupStdout()
|
|
this.setupStderr()
|
|
|
|
this.child!.on('close', (code, signal) => {
|
|
// unregisterKillOnDrop(this)
|
|
if (this._status !== 'stopped') {
|
|
this._status = 'stopped'
|
|
this._busyStatus = { type: 'idle' }
|
|
this.emitBusyStatus()
|
|
this.emitEvent('deleted', {
|
|
status: 'stopped',
|
|
exit_code: code,
|
|
signal: signal ?? null,
|
|
})
|
|
}
|
|
if (this.initReject) {
|
|
const reject = this.initReject
|
|
this.initResolve = null
|
|
this.initReject = null
|
|
process.stderr.write(`[serve:${this.sessionId}] exit_code=${code} cwd=${this.cwd} stderr:\n${this.lastStderr.slice(-5).join('\n')}\n`)
|
|
reject(new Error(`Process exited with code ${code}`))
|
|
}
|
|
if (this.promptReject) {
|
|
this.promptReject(new Error(`Process exited with code ${code}`))
|
|
}
|
|
})
|
|
|
|
this.child!.on('error', err => {
|
|
logError(err)
|
|
this._status = 'stopped'
|
|
this._busyStatus = { type: 'idle' }
|
|
this.emitBusyStatus()
|
|
if (this.initReject) {
|
|
const reject = this.initReject
|
|
this.initResolve = null
|
|
this.initReject = null
|
|
reject(err)
|
|
}
|
|
if (this.promptReject) {
|
|
this.promptReject(err)
|
|
}
|
|
})
|
|
|
|
this.initRequestId = crypto.randomUUID()
|
|
this.sendInitialize()
|
|
|
|
const timeout = setTimeout(() => {
|
|
if (this.initReject) {
|
|
const reject = this.initReject
|
|
this.initResolve = null
|
|
this.initReject = null
|
|
this._status = 'stopped'
|
|
this.emitEvent('deleted', {
|
|
status: 'stopped',
|
|
reason: 'init_timeout',
|
|
})
|
|
reject(new Error(`Session ${this.sessionId} init timed out`))
|
|
}
|
|
}, INIT_TIMEOUT_MS)
|
|
|
|
this.initResolve = (data: InitData) => {
|
|
clearTimeout(timeout)
|
|
this.initReject = null
|
|
}
|
|
this.initReject = (_err: unknown) => {
|
|
clearTimeout(timeout)
|
|
this.initResolve = null
|
|
}
|
|
}
|
|
|
|
async start(): Promise<void> {
|
|
if (!this.child) {
|
|
this.spawn()
|
|
}
|
|
if (this._status === 'running') return
|
|
return new Promise((resolve, reject) => {
|
|
const origResolve = this.initResolve
|
|
const origReject = this.initReject
|
|
this.initResolve = (data: InitData) => {
|
|
origResolve?.(data)
|
|
resolve()
|
|
}
|
|
this.initReject = (err: unknown) => {
|
|
origReject?.(err)
|
|
reject(err)
|
|
}
|
|
})
|
|
}
|
|
|
|
private sendInitialize(): void {
|
|
const request: Record<string, unknown> = {
|
|
subtype: 'initialize',
|
|
}
|
|
if (this.opts.hooks) {
|
|
request.hooks = this.opts.hooks
|
|
}
|
|
const msg = jsonStringify({
|
|
type: 'control_request',
|
|
request_id: this.initRequestId,
|
|
request,
|
|
})
|
|
this.writeStdin(msg)
|
|
}
|
|
|
|
private setupStdout(): void {
|
|
if (!this.child?.stdout) return
|
|
const rl = createInterface({ input: this.child.stdout })
|
|
rl.on('line', line => {
|
|
if (!line.trim()) return
|
|
let msg: StdoutMessage
|
|
try {
|
|
msg = jsonParse(line) as StdoutMessage
|
|
} catch {
|
|
return
|
|
}
|
|
routeMessage(msg, this.createRouterCtx())
|
|
})
|
|
}
|
|
|
|
private setupStderr(): void {
|
|
if (!this.child?.stderr) return
|
|
const rl = createInterface({ input: this.child.stderr })
|
|
rl.on('line', line => {
|
|
if (this.verbose) {
|
|
process.stderr.write(`[serve:${this.sessionId}] ${line}\n`)
|
|
}
|
|
if (this.lastStderr.length >= 20) {
|
|
this.lastStderr.shift()
|
|
}
|
|
this.lastStderr.push(line)
|
|
})
|
|
}
|
|
|
|
private emitEvent(event: string, data: Record<string, unknown>): void {
|
|
if (this.opts.silent) return
|
|
this.eventBus.publishSessionEvent(this.sessionId, event, data)
|
|
}
|
|
|
|
private emitOpencodeEvent(event: string, properties: Record<string, unknown>): void {
|
|
if (this.opts.silent) return
|
|
if (properties.type === event && 'properties' in properties) {
|
|
this.eventBus.publish(event, properties as Record<string, unknown>)
|
|
return
|
|
}
|
|
this.eventBus.publish(event, {
|
|
session_id: properties.sessionID,
|
|
...properties,
|
|
})
|
|
}
|
|
|
|
private emitBusyStatus(): void {
|
|
if (this.opts.silent) return
|
|
this.eventBus.publish('session.status', {
|
|
sessionID: this.sessionId,
|
|
status: this._busyStatus,
|
|
})
|
|
}
|
|
|
|
private emitMessage(msg: StdoutMessage): void {
|
|
for (const listener of this.listeners) {
|
|
try {
|
|
listener(msg)
|
|
} catch {}
|
|
}
|
|
}
|
|
|
|
private writeStdin(data: string): void {
|
|
if (this.child?.stdin && !this.child.stdin.destroyed) {
|
|
const flushed = this.child.stdin.write(data + '\n')
|
|
if (!flushed) {
|
|
this.child.stdin.once('drain', () => {})
|
|
}
|
|
}
|
|
}
|
|
|
|
private createRouterCtx(): MessageRouterCtx {
|
|
return {
|
|
sessionId: this.sessionId,
|
|
silent: this.opts.silent ?? false,
|
|
getStatus: () => this._status,
|
|
getBusyStatus: () => this._busyStatus,
|
|
getModel: () => this._model,
|
|
getProviderId: () => this._providerId,
|
|
getInitRequestId: () => this.initRequestId,
|
|
getLastMessageUuid: () => this._lastMessageUuid,
|
|
setStatus: (s) => { this._status = s },
|
|
setBusyStatus: (s) => { this._busyStatus = s },
|
|
setModel: (m) => { this._model = m },
|
|
setProviderId: (id) => { this._providerId = id },
|
|
setInitData: (d) => { this._initData = d },
|
|
setLastMessageUuid: (u) => { this._lastMessageUuid = u },
|
|
setLastActiveAt: (t) => { this.lastActiveAt = t },
|
|
addCost: (c) => { this._costUsd += c },
|
|
addInputTokens: (t) => { this._inputTokens += t },
|
|
addOutputTokens: (t) => { this._outputTokens += t },
|
|
getControlChannel: () => this._controlChannel,
|
|
getPendingPermissions: () => this.pendingPermissions,
|
|
getPendingQuestions: () => this.pendingQuestions,
|
|
resolveInit: (data) => {
|
|
const resolve = this.initResolve
|
|
this.initResolve = null
|
|
this.initReject = null
|
|
resolve?.(data)
|
|
this.opts.onInit?.(data)
|
|
},
|
|
resolvePrompt: (value) => {
|
|
this._busyStatus = { type: 'idle' }
|
|
this.emitBusyStatus()
|
|
if (this.promptResolve) {
|
|
this.promptResolve(value)
|
|
this.promptResolve = null
|
|
this.promptReject = null
|
|
}
|
|
if (this._firstPromptContent && !this._titleGenerationAttempted) {
|
|
this._titleGenerationAttempted = true
|
|
const content = this._firstPromptContent
|
|
void this.sendControlRequest(
|
|
{ subtype: 'generate_session_title', description: content, persist: true },
|
|
30_000,
|
|
).then((res) => {
|
|
const title = (res.title as string) || content.slice(0, 100)
|
|
this.setTitle(title)
|
|
}).catch(() => {
|
|
this.setTitle(content.slice(0, 100))
|
|
})
|
|
}
|
|
},
|
|
emitEvent: (event, data) => this.emitEvent(event, data),
|
|
emitOpencodeEvent: (event, props) => this.emitOpencodeEvent(event, props),
|
|
emitBusyStatus: () => this.emitBusyStatus(),
|
|
emitMessage: (msg) => this.emitMessage(msg),
|
|
writeStdin: (data) => this.writeStdin(data),
|
|
pushBufferMessage: (msg) => this.pushMessage(msg),
|
|
}
|
|
}
|
|
|
|
setTitle(title: string): void {
|
|
this._title = title
|
|
this.emitEvent('ready', this.getInfo())
|
|
}
|
|
|
|
async sendControlRequest(
|
|
request: Record<string, unknown>,
|
|
timeoutMs = 10000,
|
|
): Promise<Record<string, unknown>> {
|
|
const requestId = crypto.randomUUID()
|
|
this.writeStdin(
|
|
jsonStringify({
|
|
type: 'control_request',
|
|
request_id: requestId,
|
|
request,
|
|
}),
|
|
)
|
|
return this._controlChannel.register(requestId, timeoutMs)
|
|
}
|
|
|
|
async setModel(model: string): Promise<void> {
|
|
this._model = model
|
|
await this.sendControlRequest({ subtype: 'set_model', model })
|
|
}
|
|
|
|
async setAgent(agent: string): Promise<void> {
|
|
this._agent = agent
|
|
await this.sendControlRequest({ subtype: 'set_agent', agent })
|
|
}
|
|
|
|
async setPermissionMode(mode: string): Promise<void> {
|
|
this._permissionMode = mode
|
|
await this.sendControlRequest({ subtype: 'set_permission_mode', mode })
|
|
}
|
|
|
|
async getMcpStatus(): Promise<unknown[]> {
|
|
const response = await this.sendControlRequest({ subtype: 'mcp_status' })
|
|
return (response.mcpServers as unknown[]) ?? []
|
|
}
|
|
|
|
async prompt(content: string, opts?: { parts?: Array<Record<string, unknown>>; messageID?: string }): Promise<{ done: boolean; error?: string }> {
|
|
if (this._status === 'stopped') {
|
|
throw new Error(
|
|
`Session ${this.sessionId} is stopped`,
|
|
)
|
|
}
|
|
if (this._prompting) {
|
|
throw new Error(
|
|
`Session ${this.sessionId} is already processing a prompt`,
|
|
)
|
|
}
|
|
this._prompting = true
|
|
|
|
const alreadyBusy = this._busyStatus?.type === 'busy'
|
|
|
|
if (!alreadyBusy) {
|
|
this._busyStatus = { type: 'busy' }
|
|
this.emitBusyStatus()
|
|
}
|
|
this.lastActiveAt = Date.now()
|
|
|
|
const uuid = opts?.messageID ?? crypto.randomUUID()
|
|
|
|
const userMsg = jsonStringify({
|
|
type: 'user',
|
|
content,
|
|
uuid,
|
|
session_id: this.sessionId,
|
|
message: { role: 'user', content },
|
|
parent_tool_use_id: null,
|
|
})
|
|
|
|
this.emitEvent('message', {
|
|
type: 'user',
|
|
content,
|
|
uuid,
|
|
session_id: this.sessionId,
|
|
model: this._model ?? '',
|
|
provider_id: this._providerId ?? '',
|
|
})
|
|
|
|
this.pushMessage({
|
|
uuid,
|
|
type: 'user',
|
|
role: 'user',
|
|
content,
|
|
timestamp: Date.now(),
|
|
parent_uuid: null,
|
|
})
|
|
|
|
this.writeStdin(userMsg)
|
|
|
|
if (!this._title && !this._titleGenerationAttempted) {
|
|
this._firstPromptContent = content
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
this.promptResolve = (value) => {
|
|
this._prompting = false
|
|
resolve(value)
|
|
}
|
|
this.promptReject = (reason) => {
|
|
this._prompting = false
|
|
reject(reason)
|
|
}
|
|
})
|
|
}
|
|
|
|
async abort(): Promise<void> {
|
|
if (!this.child || this.child.killed) return
|
|
|
|
const interrupt = jsonStringify({
|
|
type: 'control_request',
|
|
request_id: crypto.randomUUID(),
|
|
request: { subtype: 'interrupt' },
|
|
})
|
|
this.writeStdin(interrupt)
|
|
|
|
this._busyStatus = { type: 'idle' }
|
|
this.emitBusyStatus()
|
|
|
|
if (!this.promptResolve) return
|
|
|
|
await new Promise<void>(resolve => {
|
|
const timeout = setTimeout(() => {
|
|
this.kill()
|
|
resolve()
|
|
}, 2000)
|
|
const originalResolve = this.promptResolve
|
|
this.promptResolve = (value) => {
|
|
clearTimeout(timeout)
|
|
originalResolve?.(value)
|
|
resolve()
|
|
}
|
|
})
|
|
}
|
|
|
|
replyPermission(
|
|
requestId: string,
|
|
behavior: 'allow' | 'deny',
|
|
opts?: {
|
|
updatedInput?: Record<string, unknown>
|
|
updatedPermissions?: Record<string, unknown>[]
|
|
message?: string
|
|
interrupt?: boolean
|
|
decisionClassification?: string
|
|
},
|
|
): void {
|
|
const response = {
|
|
type: 'control_response',
|
|
response: {
|
|
subtype: 'success',
|
|
request_id: requestId,
|
|
response:
|
|
behavior === 'allow'
|
|
? {
|
|
behavior: 'allow',
|
|
updatedInput: opts?.updatedInput ?? {},
|
|
...(opts?.updatedPermissions
|
|
? { updatedPermissions: opts.updatedPermissions }
|
|
: {}),
|
|
...(opts?.decisionClassification
|
|
? { decisionClassification: opts.decisionClassification }
|
|
: {}),
|
|
}
|
|
: {
|
|
behavior: 'deny',
|
|
message: opts?.message ?? 'Denied',
|
|
...(opts?.interrupt ? { interrupt: true } : {}),
|
|
},
|
|
},
|
|
}
|
|
this.writeStdin(jsonStringify(response))
|
|
const perm = this.getPendingPermission(requestId)
|
|
const toolName = perm?.toolName
|
|
this.pendingPermissions.delete(requestId)
|
|
this.emitEvent('permission_replied', {
|
|
request_id: requestId,
|
|
behavior,
|
|
})
|
|
if (toolName === 'AskUserQuestion') {
|
|
const eventType = behavior === 'allow' ? 'question.replied' : 'question.rejected'
|
|
this.emitOpencodeEvent(eventType, {
|
|
sessionID: this.sessionId,
|
|
requestID: requestId,
|
|
})
|
|
} else {
|
|
this.emitOpencodeEvent('permission.replied', {
|
|
sessionID: this.sessionId,
|
|
requestID: requestId,
|
|
})
|
|
}
|
|
}
|
|
|
|
replyQuestion(
|
|
requestId: string,
|
|
action: 'accept' | 'decline',
|
|
content?: Record<string, unknown>,
|
|
): void {
|
|
const response = {
|
|
type: 'control_response',
|
|
response: {
|
|
subtype: 'success',
|
|
request_id: requestId,
|
|
response:
|
|
action === 'accept'
|
|
? { action: 'accept', content }
|
|
: { action: 'decline' },
|
|
},
|
|
}
|
|
this.writeStdin(jsonStringify(response))
|
|
this.pendingQuestions.delete(requestId)
|
|
this.emitEvent('question_replied', {
|
|
request_id: requestId,
|
|
action,
|
|
})
|
|
const eventType = action === 'accept' ? 'question.replied' : 'question.rejected'
|
|
this.emitOpencodeEvent(eventType, {
|
|
sessionID: this.sessionId,
|
|
requestID: requestId,
|
|
})
|
|
}
|
|
|
|
kill(): void {
|
|
if (this.child && !this.child.killed) {
|
|
if (process.platform === 'win32') {
|
|
this.child.kill()
|
|
} else {
|
|
this.child.kill('SIGTERM')
|
|
}
|
|
}
|
|
this._status = 'stopped'
|
|
this._controlChannel.rejectAll(new Error('Session killed'))
|
|
}
|
|
|
|
forceKill(): void {
|
|
if (this.child && !this.child.killed) {
|
|
if (process.platform === 'win32') {
|
|
this.child.kill()
|
|
} else {
|
|
this.child.kill('SIGKILL')
|
|
}
|
|
}
|
|
this._status = 'stopped'
|
|
this._controlChannel.rejectAll(new Error('Session force killed'))
|
|
}
|
|
|
|
[Symbol.dispose](): void {
|
|
this.forceKill()
|
|
}
|
|
|
|
getPendingPermissions(): PendingPermission[] {
|
|
return [...this.pendingPermissions.values()]
|
|
}
|
|
|
|
getPendingPermission(requestId: string): PendingPermission | undefined {
|
|
return this.pendingPermissions.get(requestId)
|
|
}
|
|
|
|
getPendingQuestions(): PendingQuestion[] {
|
|
return [...this.pendingQuestions.values()]
|
|
}
|
|
|
|
getPendingQuestion(requestId: string): PendingQuestion | undefined {
|
|
return this.pendingQuestions.get(requestId)
|
|
}
|
|
|
|
handleMessage(msg: StdoutMessage): void {
|
|
routeMessage(msg, this.createRouterCtx())
|
|
}
|
|
|
|
getLastStderr(): string[] {
|
|
return [...this.lastStderr]
|
|
}
|
|
|
|
getInfo() {
|
|
return {
|
|
id: this.sessionId,
|
|
session_id: this.sessionId,
|
|
slug: this.sessionId,
|
|
projectID: '',
|
|
directory: this.cwd,
|
|
cwd: this.cwd,
|
|
title: this._title,
|
|
version: '',
|
|
time: {
|
|
created: this.createdAt,
|
|
updated: this.lastActiveAt,
|
|
},
|
|
status: this._status,
|
|
model: this._model,
|
|
permission_mode: this._permissionMode,
|
|
created_at: this.createdAt,
|
|
last_active_at: this.lastActiveAt,
|
|
cost_usd: this._costUsd,
|
|
input_tokens: this._inputTokens,
|
|
output_tokens: this._outputTokens,
|
|
last_message_uuid: this._lastMessageUuid,
|
|
}
|
|
}
|
|
}
|