@muyichengshayu/promptx 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.9
4
+
5
+ - 新增 `PromptX Glass Light` 毛玻璃主题,统一覆盖工作台主容器、任务列表、设置面板、弹窗、下拉、状态标签与图片预览层。
6
+ - 优化执行区视觉层次,补齐提示词 / 执行过程 / 回复三类消息卡片,以及 Markdown 中引用、代码块、表格等内容样式。
7
+ - 单独收敛手机端观感:压低玻璃强度与阴影,收紧任务卡、详情头部、设置页导航与表单布局。
8
+ - 修复 Markdown 表格在新主题下布局错乱的问题,改为使用外层滚动容器承载表格边框与横向滚动。
9
+ - 编辑区右上角主按钮在任务执行中保持“发送”文案并禁用,不再切换成“停止”,减少误触和语义跳变。
10
+
11
+ ## 0.1.8
12
+
13
+ - 新增 OpenCode 执行引擎接入,补齐 runner、契约测试、Doctor 检查与文档说明,并支持在项目配置里直接选择 OpenCode。
14
+ - 项目管理里的执行引擎列表改为服务端下发,避免前后端版本不一致时前端看不到新引擎选项。
15
+ - 优化项目编辑规则:项目未执行前允许切换执行引擎,执行后自动锁定,保持与工作目录一致的配置心智。
16
+ - 强化运行停止逻辑:用户点击停止后会立即进入强制停止流程,并补齐更可靠的跨平台子进程终止处理与“正在停止...”反馈。
17
+
3
18
  ## 0.1.7
4
19
 
5
20
  - 新增多引擎运行器抽象,除了 Codex 之外,现已支持接入 Claude Code,并统一项目层的执行引擎配置与展示。
package/README.md CHANGED
@@ -2,13 +2,14 @@
2
2
 
3
3
  PromptX 是一个面向本机 AI 协作的轻量工作台。
4
4
 
5
- 它适合先整理需求、截图、文本、PDF、禅道 Bug 等上下文,再持续发送给本机 Codex,在同一页里查看执行过程和多轮结果。
5
+ 它适合先整理需求、截图、文本、PDF、禅道 Bug 等上下文,再持续发送给本机 AI Agent,在同一页里查看执行过程和多轮结果。
6
6
 
7
7
  ## 核心能力
8
8
 
9
9
  - 左侧管理任务,中间查看项目执行过程,右侧整理输入内容
10
10
  - 支持文本、图片、`md`、`txt`、`pdf`
11
- - 支持为任务绑定本机项目,并持续复用同一个 Codex 线程
11
+ - 支持为任务绑定本机项目,并持续复用同一个执行引擎线程
12
+ - 支持多执行引擎,当前已接入 `Codex`、`Claude Code`、`OpenCode`
12
13
  - 支持查看执行过程、代码变更和最终回复
13
14
  - 支持公开页与 Raw 导出
14
15
  - 内置禅道 Chrome 扩展,可一键把 Bug 内容带入工作台
@@ -30,8 +31,12 @@ PromptX 是一个面向本机 AI 协作的轻量工作台。
30
31
  ## 运行前提
31
32
 
32
33
  - 已安装 Node,支持 `20`、`22`、`24`,推荐 `22`
33
- - 本机可以正常运行 `codex --version`
34
- - Codex 已开启高权限,并使用满血模式
34
+ - 本机至少安装一个可用执行引擎
35
+ - 当前支持:
36
+ - `codex --version`
37
+ - `claude --version`
38
+ - `opencode --version`
39
+ - 如使用 Codex,建议开启高权限并使用满血模式
35
40
 
36
41
  ## 安装
37
42
 
@@ -67,9 +72,21 @@ promptx doctor
67
72
  1. 打开工作台,新建或选择一个任务
68
73
  2. 在右侧整理文本、图片、文件等上下文
69
74
  3. 在中间选择一个 PromptX 项目
70
- 4. 点击发送,把当前内容交给 Codex
75
+ 4. 为项目选择执行引擎,并点击发送
71
76
  5. 在中间继续查看执行过程,并按需多轮发送
72
77
 
78
+ ## 当前支持的执行引擎
79
+
80
+ - `Codex`
81
+ - `Claude Code`
82
+ - `OpenCode`
83
+
84
+ PromptX 内部已经开始使用统一的 agent run 事件协议,后续继续扩展其他执行引擎时,前端展示层和执行过程面板可以尽量复用。
85
+
86
+ 如需查看这套协议约定,可参考:
87
+
88
+ - `docs/agent-run-protocol.md`
89
+
73
90
  ## 远程访问 Relay(预览)
74
91
 
75
92
  如果你希望在手机上远程访问自己电脑上的 PromptX,或想在云端部署多租户 Relay,请直接查看:
@@ -103,10 +120,10 @@ promptx doctor
103
120
 
104
121
  ## 注意事项
105
122
 
106
- - 当前只支持 Codex,不支持其他模型后端
107
123
  - 当前以本机单用户使用为主,不包含账号体系和团队权限
108
124
  - 默认仅监听本机地址;如需跨设备访问,建议通过 Tailscale
109
- - 如果 Codex 运行在受限权限下,文件读写和自动修改能力会明显受限
125
+ - 如果执行引擎运行在受限权限下,文件读写和自动修改能力会明显受限
126
+ - 不同执行引擎的工具能力、输出事件丰富度和稳定性可能会有差异
110
127
 
111
128
  ## 本地数据目录
112
129
 
@@ -16,6 +16,7 @@ import {
16
16
  createTurnCompletedEvent,
17
17
  getAgentEngineLabel,
18
18
  } from '../../../../packages/shared/src/index.js'
19
+ import { createManagedSpawnOptions, forceStopChildProcess } from '../processControl.js'
19
20
 
20
21
  const CLAUDE_CODE_BIN = process.env.CLAUDE_CODE_BIN || 'claude'
21
22
  const CLAUDE_DEFAULT_ARGS = ['--dangerously-skip-permissions']
@@ -68,16 +69,10 @@ function resolveClaudeCodeBinary() {
68
69
  }
69
70
 
70
71
  function createClaudeSpawn(commandArgs = [], cwd = '') {
71
- const options = {
72
- env: process.env,
72
+ const options = createManagedSpawnOptions({
73
+ cwd,
73
74
  stdio: ['ignore', 'pipe', 'pipe'],
74
- windowsHide: true,
75
- }
76
-
77
- const normalizedCwd = String(cwd || '').trim()
78
- if (normalizedCwd) {
79
- options.cwd = normalizedCwd
80
- }
75
+ })
81
76
 
82
77
  if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(RESOLVED_CLAUDE_CODE_BIN)) {
83
78
  return spawn(
@@ -524,9 +519,7 @@ export function streamPromptToClaudeCodeSession(sessionInput, prompt, callbacks
524
519
  child,
525
520
  result,
526
521
  cancel() {
527
- if (!child.killed) {
528
- child.kill('SIGTERM')
529
- }
522
+ forceStopChildProcess(child)
530
523
  },
531
524
  }
532
525
  }
@@ -6,10 +6,12 @@ import {
6
6
  } from '../../../../packages/shared/src/index.js'
7
7
  import { codexRunner } from './codexRunner.js'
8
8
  import { claudeCodeRunner } from './claudeCodeRunner.js'
9
+ import { openCodeRunner } from './openCodeRunner.js'
9
10
 
10
11
  const runnerRegistry = new Map([
11
12
  [codexRunner.engine, codexRunner],
12
13
  [claudeCodeRunner.engine, claudeCodeRunner],
14
+ [openCodeRunner.engine, openCodeRunner],
13
15
  ])
14
16
 
15
17
  export function getAgentRunner(engine = AGENT_ENGINES.CODEX) {
@@ -0,0 +1,459 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { execFileSync, spawn } from 'node:child_process'
4
+ import {
5
+ AGENT_ENGINES,
6
+ AGENT_RUN_ITEM_TYPES,
7
+ createAgentEventEnvelopeEvent,
8
+ createCompletedEnvelopeEvent,
9
+ createErrorEvent,
10
+ createItemCompletedEvent,
11
+ createItemStartedEvent,
12
+ createStatusEnvelopeEvent,
13
+ createStderrEnvelopeEvent,
14
+ createStdoutEnvelopeEvent,
15
+ createThreadStartedEvent,
16
+ createTurnCompletedEvent,
17
+ getAgentEngineLabel,
18
+ } from '../../../../packages/shared/src/index.js'
19
+ import { createManagedSpawnOptions, forceStopChildProcess } from '../processControl.js'
20
+
21
+ const OPENCODE_BIN = process.env.OPENCODE_BIN || 'opencode'
22
+ const RESOLVED_OPENCODE_BIN = resolveOpenCodeBinary()
23
+
24
+ function resolveOpenCodeBinary() {
25
+ if (process.platform !== 'win32') {
26
+ return OPENCODE_BIN
27
+ }
28
+
29
+ if (path.extname(OPENCODE_BIN)) {
30
+ return OPENCODE_BIN
31
+ }
32
+
33
+ if (fs.existsSync(`${OPENCODE_BIN}.cmd`)) {
34
+ return `${OPENCODE_BIN}.cmd`
35
+ }
36
+
37
+ if (fs.existsSync(`${OPENCODE_BIN}.bat`)) {
38
+ return `${OPENCODE_BIN}.bat`
39
+ }
40
+
41
+ if (fs.existsSync(OPENCODE_BIN)) {
42
+ return OPENCODE_BIN
43
+ }
44
+
45
+ try {
46
+ const output = execFileSync('where.exe', [OPENCODE_BIN], {
47
+ encoding: 'utf8',
48
+ stdio: ['ignore', 'pipe', 'ignore'],
49
+ windowsHide: true,
50
+ }).trim()
51
+
52
+ if (!output) {
53
+ return OPENCODE_BIN
54
+ }
55
+
56
+ const candidates = output
57
+ .split(/\r?\n/g)
58
+ .map((line) => line.trim())
59
+ .filter(Boolean)
60
+
61
+ return candidates.find((item) => /\.(cmd|bat)$/i.test(item))
62
+ || candidates.find((item) => /\.(exe|com)$/i.test(item))
63
+ || candidates[0]
64
+ || OPENCODE_BIN
65
+ } catch {
66
+ return OPENCODE_BIN
67
+ }
68
+ }
69
+
70
+ function createOpenCodeSpawn(commandArgs = [], cwd = '') {
71
+ const options = createManagedSpawnOptions({
72
+ cwd,
73
+ stdio: ['ignore', 'pipe', 'pipe'],
74
+ })
75
+
76
+ if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(RESOLVED_OPENCODE_BIN)) {
77
+ return spawn(
78
+ process.env.ComSpec || 'cmd.exe',
79
+ ['/d', '/s', '/c', RESOLVED_OPENCODE_BIN, ...commandArgs],
80
+ options
81
+ )
82
+ }
83
+
84
+ return spawn(RESOLVED_OPENCODE_BIN, commandArgs, options)
85
+ }
86
+
87
+ function normalizeSpawnError(error) {
88
+ if (error?.code === 'ENOENT') {
89
+ const attempted = RESOLVED_OPENCODE_BIN === OPENCODE_BIN
90
+ ? OPENCODE_BIN
91
+ : `${OPENCODE_BIN} -> ${RESOLVED_OPENCODE_BIN}`
92
+
93
+ return new Error(
94
+ `找不到 OpenCode CLI(尝试执行:${attempted})。请先确认终端里可以运行 \`opencode --version\`,或设置环境变量 \`OPENCODE_BIN\`。`
95
+ )
96
+ }
97
+
98
+ return error
99
+ }
100
+
101
+ function parseJsonLine(line = '') {
102
+ const text = String(line || '').trim()
103
+ if (!text) {
104
+ return null
105
+ }
106
+
107
+ try {
108
+ return JSON.parse(text)
109
+ } catch {
110
+ return null
111
+ }
112
+ }
113
+
114
+ function splitBufferedLines(buffer = '') {
115
+ const text = String(buffer || '')
116
+ if (!text) {
117
+ return { lines: [], rest: '' }
118
+ }
119
+
120
+ const normalized = text.replace(/\r\n/g, '\n')
121
+ const parts = normalized.split('\n')
122
+ const rest = parts.pop() || ''
123
+
124
+ return {
125
+ lines: parts.map((line) => line.trim()).filter(Boolean),
126
+ rest,
127
+ }
128
+ }
129
+
130
+ function flushBufferedText(buffer = '') {
131
+ const { lines, rest } = splitBufferedLines(buffer)
132
+ const tail = String(rest || '').trim()
133
+ return tail ? [...lines, tail] : lines
134
+ }
135
+
136
+ function summarizeOpenCodeInput(input = {}) {
137
+ if (!input || typeof input !== 'object') {
138
+ return ''
139
+ }
140
+
141
+ const command = String(input.command || '').trim()
142
+ if (command) {
143
+ return command
144
+ }
145
+
146
+ const singleValueKeys = ['filePath', 'path', 'pattern', 'query', 'url', 'description']
147
+ for (const key of singleValueKeys) {
148
+ const value = String(input[key] || '').trim()
149
+ if (value) {
150
+ return value
151
+ }
152
+ }
153
+
154
+ try {
155
+ const compact = JSON.stringify(input)
156
+ return compact.length <= 240 ? compact : `${compact.slice(0, 237)}...`
157
+ } catch {
158
+ return ''
159
+ }
160
+ }
161
+
162
+ function stringifyOpenCodeOutput(output) {
163
+ if (typeof output === 'string') {
164
+ return output.trim()
165
+ }
166
+
167
+ if (output == null) {
168
+ return ''
169
+ }
170
+
171
+ try {
172
+ const compact = JSON.stringify(output)
173
+ return compact.length <= 12000 ? compact : `${compact.slice(0, 11997)}...`
174
+ } catch {
175
+ return String(output || '').trim()
176
+ }
177
+ }
178
+
179
+ function buildOpenCodeToolCommand(event = {}) {
180
+ const part = event?.part && typeof event.part === 'object' ? event.part : event
181
+ const toolName = String(part?.tool || 'OpenCode tool').trim() || 'OpenCode tool'
182
+ const input = part?.state?.input && typeof part.state.input === 'object'
183
+ ? part.state.input
184
+ : {}
185
+ const inputSummary = summarizeOpenCodeInput(input)
186
+ return inputSummary ? `${toolName}: ${inputSummary}` : toolName
187
+ }
188
+
189
+ export function extractOpenCodeText(event = {}) {
190
+ return String(event?.part?.text || event?.text || '').trim()
191
+ }
192
+
193
+ export function extractOpenCodeSessionId(event = {}) {
194
+ const candidates = [
195
+ event?.sessionID,
196
+ event?.sessionId,
197
+ event?.part?.sessionID,
198
+ event?.part?.sessionId,
199
+ ]
200
+
201
+ return candidates.map((value) => String(value || '').trim()).find(Boolean) || ''
202
+ }
203
+
204
+ export function extractOpenCodeUsage(event = {}) {
205
+ const tokens = event?.part?.tokens
206
+ if (!tokens || typeof tokens !== 'object') {
207
+ return null
208
+ }
209
+
210
+ return {
211
+ input_tokens: Number(tokens.input) || 0,
212
+ output_tokens: Number(tokens.output) || 0,
213
+ cached_input_tokens: Number(tokens.cache?.read) || 0,
214
+ }
215
+ }
216
+
217
+ function createOpenCodeRunStatusEvent(session = {}) {
218
+ const hasExistingThread = Boolean(
219
+ String(session?.engineSessionId || session?.engineThreadId || session?.codexThreadId || '').trim()
220
+ )
221
+
222
+ return createStatusEnvelopeEvent({
223
+ stage: hasExistingThread ? 'resuming' : 'starting',
224
+ message: hasExistingThread
225
+ ? '已连接 PromptX 项目,正在继续这轮执行。'
226
+ : '已创建 PromptX 项目,正在启动第一轮执行。',
227
+ })
228
+ }
229
+
230
+ export function createOpenCodeNormalizationState() {
231
+ return {
232
+ turnStarted: false,
233
+ }
234
+ }
235
+
236
+ export function normalizeOpenCodeEvents(event = {}, state = createOpenCodeNormalizationState()) {
237
+ const eventType = String(event?.type || '').trim().toLowerCase()
238
+ const normalizedEvents = []
239
+
240
+ if (eventType === 'step_start') {
241
+ if (!state.turnStarted) {
242
+ state.turnStarted = true
243
+ normalizedEvents.push({ type: 'turn.started' })
244
+ }
245
+ return normalizedEvents
246
+ }
247
+
248
+ if (eventType === 'tool_use') {
249
+ const command = buildOpenCodeToolCommand(event)
250
+ const status = String(event?.part?.state?.status || '').trim().toLowerCase()
251
+ const output = stringifyOpenCodeOutput(event?.part?.state?.output)
252
+
253
+ normalizedEvents.push(createItemStartedEvent({
254
+ type: AGENT_RUN_ITEM_TYPES.COMMAND_EXECUTION,
255
+ command,
256
+ status: 'in_progress',
257
+ }))
258
+
259
+ if (status === 'completed' || status === 'failed' || status === 'error') {
260
+ normalizedEvents.push(createItemCompletedEvent({
261
+ type: AGENT_RUN_ITEM_TYPES.COMMAND_EXECUTION,
262
+ command,
263
+ status: status === 'completed' ? 'completed' : 'failed',
264
+ exit_code: status === 'completed' ? 0 : 1,
265
+ aggregated_output: output,
266
+ }))
267
+ }
268
+
269
+ return normalizedEvents
270
+ }
271
+
272
+ if (eventType === 'text') {
273
+ const text = extractOpenCodeText(event)
274
+ if (!text) {
275
+ return normalizedEvents
276
+ }
277
+
278
+ normalizedEvents.push(createItemCompletedEvent({
279
+ type: AGENT_RUN_ITEM_TYPES.AGENT_MESSAGE,
280
+ text,
281
+ }))
282
+ return normalizedEvents
283
+ }
284
+
285
+ if (eventType === 'step_finish') {
286
+ const reason = String(event?.part?.reason || '').trim().toLowerCase()
287
+ if (reason === 'stop') {
288
+ const usage = extractOpenCodeUsage(event)
289
+ normalizedEvents.push(createTurnCompletedEvent(usage ? { usage } : {}))
290
+ return normalizedEvents
291
+ }
292
+
293
+ return normalizedEvents
294
+ }
295
+
296
+ if (eventType === 'error') {
297
+ const message = extractOpenCodeText(event) || String(event?.message || event?.error || '').trim()
298
+ if (message) {
299
+ normalizedEvents.push(createErrorEvent(message))
300
+ }
301
+ return normalizedEvents
302
+ }
303
+
304
+ return [{
305
+ type: `opencode.${eventType || 'event'}`,
306
+ detail: extractOpenCodeText(event),
307
+ }]
308
+ }
309
+
310
+ export function normalizeOpenCodeEvent(event = {}, state = createOpenCodeNormalizationState()) {
311
+ return normalizeOpenCodeEvents(event, state)[0] || null
312
+ }
313
+
314
+ function createExecArgs(session, prompt) {
315
+ const args = [
316
+ 'run',
317
+ '--format',
318
+ 'json',
319
+ ]
320
+
321
+ const sessionId = String(session?.engineSessionId || session?.engineThreadId || session?.codexThreadId || '').trim()
322
+ if (sessionId) {
323
+ args.push('--session', sessionId)
324
+ }
325
+
326
+ if (session?.cwd) {
327
+ args.push('--dir', session.cwd)
328
+ }
329
+
330
+ args.push(String(prompt || ''))
331
+ return args
332
+ }
333
+
334
+ export function streamPromptToOpenCodeSession(sessionInput, prompt, callbacks = {}) {
335
+ const session = sessionInput && typeof sessionInput === 'object' ? sessionInput : null
336
+ const normalizedPrompt = String(prompt || '').trim()
337
+
338
+ if (!session?.id || !session?.cwd) {
339
+ throw new Error('缺少 PromptX 项目。')
340
+ }
341
+
342
+ if (!normalizedPrompt) {
343
+ throw new Error('没有可发送的提示词。')
344
+ }
345
+
346
+ const onEvent = typeof callbacks.onEvent === 'function' ? callbacks.onEvent : () => {}
347
+ const onThreadStarted = typeof callbacks.onThreadStarted === 'function' ? callbacks.onThreadStarted : () => {}
348
+
349
+ const child = createOpenCodeSpawn(createExecArgs(session, normalizedPrompt), session.cwd)
350
+ onEvent(createOpenCodeRunStatusEvent(session))
351
+
352
+ let stdoutBuffer = ''
353
+ let stderrBuffer = ''
354
+ let lastStderrLine = ''
355
+ let finalMessage = ''
356
+ let finalSessionId = String(session.engineSessionId || session.engineThreadId || session.codexThreadId || '').trim()
357
+ const normalizationState = createOpenCodeNormalizationState()
358
+
359
+ const rememberSessionId = (sessionId) => {
360
+ const value = String(sessionId || '').trim()
361
+ if (!value || value === finalSessionId) {
362
+ return
363
+ }
364
+
365
+ finalSessionId = value
366
+ onThreadStarted(value)
367
+ onEvent(createAgentEventEnvelopeEvent(createThreadStartedEvent(value)))
368
+ }
369
+
370
+ const emitOpenCodeJsonLine = (line) => {
371
+ const event = parseJsonLine(line)
372
+ if (!event) {
373
+ onEvent(createStdoutEnvelopeEvent(line))
374
+ return
375
+ }
376
+
377
+ const sessionId = extractOpenCodeSessionId(event)
378
+ if (sessionId) {
379
+ rememberSessionId(sessionId)
380
+ }
381
+
382
+ const normalizedEvents = normalizeOpenCodeEvents(event, normalizationState)
383
+ normalizedEvents.forEach((normalizedEvent) => {
384
+ onEvent(createAgentEventEnvelopeEvent(normalizedEvent))
385
+ })
386
+
387
+ if (String(event?.type || '').trim().toLowerCase() === 'text') {
388
+ const text = extractOpenCodeText(event)
389
+ if (text) {
390
+ finalMessage = `${finalMessage}${finalMessage ? '\n' : ''}${text}`
391
+ }
392
+ }
393
+ }
394
+
395
+ child.stdout.on('data', (chunk) => {
396
+ stdoutBuffer += chunk.toString()
397
+ const { lines, rest } = splitBufferedLines(stdoutBuffer)
398
+ stdoutBuffer = rest
399
+ lines.forEach(emitOpenCodeJsonLine)
400
+ })
401
+
402
+ child.stderr.on('data', (chunk) => {
403
+ stderrBuffer += chunk.toString()
404
+ const { lines, rest } = splitBufferedLines(stderrBuffer)
405
+ stderrBuffer = rest
406
+ lines.forEach((line) => {
407
+ lastStderrLine = line
408
+ onEvent(createStderrEnvelopeEvent(line))
409
+ })
410
+ })
411
+
412
+ const result = new Promise((resolve, reject) => {
413
+ child.on('error', (error) => {
414
+ reject(normalizeSpawnError(error))
415
+ })
416
+
417
+ child.on('close', (code) => {
418
+ flushBufferedText(stdoutBuffer).forEach(emitOpenCodeJsonLine)
419
+ flushBufferedText(stderrBuffer).forEach((line) => {
420
+ lastStderrLine = line
421
+ onEvent(createStderrEnvelopeEvent(line))
422
+ })
423
+
424
+ if (code !== 0) {
425
+ reject(new Error(lastStderrLine || 'OpenCode 执行失败。'))
426
+ return
427
+ }
428
+
429
+ const message = finalMessage.trim()
430
+ onEvent(createCompletedEnvelopeEvent(message))
431
+
432
+ resolve({
433
+ sessionId: session.id,
434
+ threadId: finalSessionId,
435
+ message,
436
+ })
437
+ })
438
+ })
439
+
440
+ return {
441
+ child,
442
+ result,
443
+ cancel() {
444
+ forceStopChildProcess(child)
445
+ },
446
+ }
447
+ }
448
+
449
+ export const openCodeRunner = {
450
+ engine: AGENT_ENGINES.OPENCODE,
451
+ label: getAgentEngineLabel(AGENT_ENGINES.OPENCODE),
452
+ supportsWorkspaceHistory: false,
453
+ listKnownWorkspaces() {
454
+ return []
455
+ },
456
+ streamSessionPrompt(session, prompt, callbacks = {}) {
457
+ return streamPromptToOpenCodeSession(session, prompt, callbacks)
458
+ },
459
+ }
@@ -13,6 +13,7 @@ import {
13
13
  createStderrEnvelopeEvent,
14
14
  createStdoutEnvelopeEvent,
15
15
  } from '../../../packages/shared/src/index.js'
16
+ import { createManagedSpawnOptions, forceStopChildProcess } from './processControl.js'
16
17
 
17
18
  const CODEX_BIN = process.env.CODEX_BIN || 'codex'
18
19
  const CODEX_HOME = process.env.CODEX_HOME || path.join(os.homedir(), '.codex')
@@ -79,16 +80,10 @@ function resolveCodexBinary() {
79
80
  }
80
81
 
81
82
  function createCodexSpawn(commandArgs = [], cwd = '') {
82
- const options = {
83
- env: process.env,
83
+ const options = createManagedSpawnOptions({
84
+ cwd,
84
85
  stdio: ['pipe', 'pipe', 'pipe'],
85
- windowsHide: true,
86
- }
87
- const normalizedCwd = String(cwd || '').trim()
88
-
89
- if (normalizedCwd) {
90
- options.cwd = normalizedCwd
91
- }
86
+ })
92
87
 
93
88
  if (process.platform === 'win32' && /\.(cmd|bat)$/i.test(RESOLVED_CODEX_BIN)) {
94
89
  return spawn(
@@ -550,25 +545,10 @@ export function streamPromptToCodexSession(sessionInput, prompt, callbacks = {})
550
545
  child,
551
546
  result,
552
547
  cancel() {
553
- if (child.killed) {
548
+ if (child.killed || !child.pid) {
554
549
  return
555
550
  }
556
-
557
- if (process.platform === 'win32' && child.pid) {
558
- try {
559
- execFileSync('taskkill.exe', ['/PID', String(child.pid), '/T', '/F'], {
560
- stdio: 'ignore',
561
- windowsHide: true,
562
- })
563
- return
564
- } catch {
565
- // Fall through to the default child kill when taskkill is unavailable.
566
- }
567
- }
568
-
569
- if (!child.killed) {
570
- child.kill('SIGTERM')
571
- }
551
+ forceStopChildProcess(child)
572
552
  },
573
553
  }
574
554
  }