codemini-cli 0.1.1

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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/OPERATIONS.md +202 -0
  3. package/README.md +138 -0
  4. package/bin/coder.js +7 -0
  5. package/deployment.md +205 -0
  6. package/package.json +54 -0
  7. package/skills/brainstorming-lite/SKILL.md +37 -0
  8. package/skills/executing-plan-lite/SKILL.md +41 -0
  9. package/skills/superpowers-lite/SKILL.md +44 -0
  10. package/souls/anime.md +3 -0
  11. package/souls/default.md +3 -0
  12. package/souls/playful.md +3 -0
  13. package/souls/professional.md +3 -0
  14. package/src/cli.js +62 -0
  15. package/src/commands/chat.js +106 -0
  16. package/src/commands/config.js +61 -0
  17. package/src/commands/doctor.js +87 -0
  18. package/src/commands/run.js +64 -0
  19. package/src/commands/skill.js +264 -0
  20. package/src/core/agent-loop.js +281 -0
  21. package/src/core/chat-runtime.js +2075 -0
  22. package/src/core/checkpoint-store.js +66 -0
  23. package/src/core/command-loader.js +201 -0
  24. package/src/core/command-policy.js +71 -0
  25. package/src/core/config-store.js +196 -0
  26. package/src/core/context-compact.js +90 -0
  27. package/src/core/default-system-prompt.js +5 -0
  28. package/src/core/fs-utils.js +16 -0
  29. package/src/core/input-history-store.js +48 -0
  30. package/src/core/input-parser.js +15 -0
  31. package/src/core/paths.js +109 -0
  32. package/src/core/provider/openai-compatible.js +228 -0
  33. package/src/core/session-store.js +178 -0
  34. package/src/core/shell-profile.js +122 -0
  35. package/src/core/shell.js +71 -0
  36. package/src/core/skill-registry.js +55 -0
  37. package/src/core/soul.js +55 -0
  38. package/src/core/task-store.js +116 -0
  39. package/src/core/tools.js +237 -0
  40. package/src/tui/chat-app.js +2007 -0
  41. package/src/tui/input-escape.js +21 -0
@@ -0,0 +1,2007 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
2
+ import { Box, Text, useApp, useInput } from 'ink';
3
+ import { shouldCaptureEscapeSequence } from './input-escape.js';
4
+
5
+ const h = React.createElement;
6
+ const BANNER = [
7
+ ' ██████ ██████ ██████ ███████ ███ ███ ██ ███ ██ ██ ',
8
+ '██ ██ ██ ██ ██ ██ ████ ████ ██ ████ ██ ██ ',
9
+ '██ ██ ██ ██ ██ █████ ██ ████ ██ ██ ██ ██ ██ ██ ',
10
+ '██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ',
11
+ ' ██████ ██████ ██████ ███████ ██ ██ ██ ██ ████ ██ '
12
+ ];
13
+ const BANNER_COLORS = ['magentaBright', 'redBright', 'yellowBright', 'cyanBright', 'magentaBright'];
14
+ const ROLE_STYLES = {
15
+ you: {
16
+ accent: 'blueBright',
17
+ border: 'blue',
18
+ text: 'white',
19
+ badgeBg: 'blue',
20
+ badgeText: 'white',
21
+ chrome: 'gray'
22
+ },
23
+ coder: {
24
+ accent: 'greenBright',
25
+ border: 'cyan',
26
+ text: 'greenBright',
27
+ badgeBg: 'cyan',
28
+ badgeText: 'black',
29
+ chrome: 'gray'
30
+ },
31
+ system: {
32
+ accent: 'yellowBright',
33
+ border: 'yellow',
34
+ text: 'yellowBright',
35
+ badgeBg: 'yellow',
36
+ badgeText: 'black',
37
+ chrome: 'gray'
38
+ },
39
+ error: {
40
+ accent: 'redBright',
41
+ border: 'red',
42
+ text: 'redBright',
43
+ badgeBg: 'red',
44
+ badgeText: 'white',
45
+ chrome: 'gray'
46
+ },
47
+ pending: {
48
+ accent: 'cyanBright',
49
+ border: 'cyan',
50
+ text: 'cyanBright',
51
+ badgeBg: 'cyan',
52
+ badgeText: 'black',
53
+ chrome: 'gray'
54
+ }
55
+ };
56
+
57
+ const TUI_COPY = {
58
+ zh: {
59
+ roleLabels: { you: '你', coder: 'CODER', system: '系统', error: '错误', pending: '等待中' },
60
+ generic: {
61
+ waitingForInput: '等待输入',
62
+ ready: '就绪',
63
+ noMessagesYet: '还没有消息',
64
+ code: '代码',
65
+ live: '运行中',
66
+ idle: '空闲',
67
+ plan: '计划',
68
+ active: '进行中',
69
+ attention: '注意',
70
+ model: '模型',
71
+ mode: '模式',
72
+ session: '会话',
73
+ internalCockpit: '内部编码控制台',
74
+ commandBar: '命令栏',
75
+ safeMode: '安全模式',
76
+ queued: '排队',
77
+ tools: '工具',
78
+ open: '展开',
79
+ collapsed: '收起',
80
+ pendingQueue: '等待队列',
81
+ commandPaletteGroupedSelect: '命令面板 | 分组选择模式',
82
+ commandPaletteGroupedSuggestions: '命令面板 | 分组候选',
83
+ startupHint: '使用 /help、/commands、/exit、!<shell>。Tab 可自动补全 slash 命令。',
84
+ toolSummaryExpanded: '工具摘要:已展开',
85
+ toolSummaryCollapsed: '工具摘要:已收起',
86
+ toggleToolSummary: 'Ctrl+T 切换',
87
+ scrollHint: '使用终端自己的滚动条或 scrollback',
88
+ keyboardDebugEnabled: '键盘调试已开启',
89
+ keyboardDebugDisabled: '键盘调试已关闭',
90
+ keyboardDebugStatus: (on) => `键盘调试当前${on ? '开启' : '关闭'}`,
91
+ debugKeys: (value) => `按键调试:${value || '(无)'}`
92
+ },
93
+ stageTags: {
94
+ sending: '发送中',
95
+ thinking: '思考中',
96
+ streaming: '输出中',
97
+ tooling: '工具中',
98
+ running: '运行中',
99
+ idle: '空闲'
100
+ },
101
+ toolActivity: {
102
+ blocked: '工具被拦截',
103
+ doneRead: '已读取文件',
104
+ doingRead: '正在读取文件',
105
+ doneWrite: '已写入文件',
106
+ doingWrite: '正在写入文件',
107
+ doneList: '已查看目录',
108
+ doingList: '正在查看目录',
109
+ doneCommand: '已执行命令',
110
+ doingCommand: '正在执行命令',
111
+ doneCreateTask: '已创建任务',
112
+ doingCreateTask: '正在创建任务',
113
+ doneUpdateTask: '已更新任务',
114
+ doingUpdateTask: '正在更新任务',
115
+ doneGeneric: '已完成工具',
116
+ doingGeneric: '正在执行工具',
117
+ doneSkill: '已完成技能',
118
+ doingSkill: '正在执行技能',
119
+ toolFailed: (name) => `工具执行失败: ${name}`,
120
+ waitingModelContinue: (detail) => `${detail},等待模型继续`,
121
+ waitingModelAdjust: (detail) => `${detail},等待模型调整`
122
+ },
123
+ suggestion: {
124
+ singleTab: 'Tab 补全当前命令',
125
+ navFill: 'Tab 保持切换模式,↑↓选择,Enter 填入',
126
+ navEnter: 'Tab 进入切换模式,再用 ↑↓ 选择',
127
+ noSuggestions: '/ 查看命令,Tab 自动补全,↑↓ 历史,Ctrl+T 展开工具',
128
+ oneNav: 'Tab 或 Enter 填入当前命令,↑↓ 历史',
129
+ oneIdle: 'Tab 补全当前唯一候选,Enter 直接发送,↑↓ 历史',
130
+ manyNav: (count) => `Tab 切换候选,↑↓选择,Enter 填入 (${count} 项)`,
131
+ manyIdle: (count) => `Tab 进入候选切换 (${count} 项),↑↓ 历史`
132
+ },
133
+ runtime: {
134
+ sendingToGateway: '正在发送到网关',
135
+ preparingRequest: '准备本轮请求',
136
+ modelThinking: '模型正在思考',
137
+ requestDelivered: '请求已送达,等待首个 token',
138
+ generatingReply: '正在生成回复',
139
+ streamingReply: '回复正在流式输出',
140
+ replyCompleted: '回复已完成',
141
+ outputFinished: '本轮输出结束',
142
+ toolRunning: '工具执行中',
143
+ toolCompleted: '工具已完成',
144
+ toolBlocked: '工具被拦截',
145
+ toolFailed: '工具执行失败',
146
+ skillRunning: '技能执行中',
147
+ skillCompleted: '技能已完成',
148
+ skillFailed: '技能执行失败',
149
+ compactingContext: '正在压缩上下文',
150
+ autoCompactTriggered: (mode, threshold) => `自动压缩已触发(${mode},阈值 ${threshold}%)`,
151
+ requestFailed: '请求失败',
152
+ localCommandRunning: '正在执行本地命令',
153
+ queuedWaiting: '排队中,等待上一轮完成',
154
+ idleReady: '等待输入',
155
+ idleReadyDetail: '就绪',
156
+ idleAfterTurn: '空闲',
157
+ idleAfterTurnDetail: '等待下一轮输入'
158
+ }
159
+ },
160
+ en: {
161
+ roleLabels: { you: 'YOU', coder: 'CODER', system: 'SYSTEM', error: 'ERROR', pending: 'PENDING' },
162
+ generic: {
163
+ waitingForInput: 'waiting for input',
164
+ ready: 'ready',
165
+ noMessagesYet: 'No messages yet',
166
+ code: 'code',
167
+ live: 'LIVE',
168
+ idle: 'IDLE',
169
+ plan: 'PLAN',
170
+ active: 'ACTIVE',
171
+ attention: 'ATTENTION',
172
+ model: 'MODEL',
173
+ mode: 'MODE',
174
+ session: 'SESSION',
175
+ internalCockpit: 'internal coding cockpit',
176
+ commandBar: 'COMMAND BAR',
177
+ safeMode: 'SAFE MODE',
178
+ queued: 'QUEUED',
179
+ tools: 'TOOLS',
180
+ open: 'OPEN',
181
+ collapsed: 'COLLAPSED',
182
+ pendingQueue: 'pending queue',
183
+ commandPaletteGroupedSelect: 'command palette | grouped select mode',
184
+ commandPaletteGroupedSuggestions: 'command palette | grouped suggestions',
185
+ startupHint: 'Use /help, /commands, /exit, !<shell>. Tab for slash autocomplete.',
186
+ toolSummaryExpanded: 'Tool summary: expanded',
187
+ toolSummaryCollapsed: 'Tool summary: collapsed',
188
+ toggleToolSummary: 'Ctrl+T to toggle',
189
+ scrollHint: 'Scroll with your terminal scrollbar or scrollback',
190
+ keyboardDebugEnabled: 'Keyboard debug enabled',
191
+ keyboardDebugDisabled: 'Keyboard debug disabled',
192
+ keyboardDebugStatus: (on) => `Keyboard debug is ${on ? 'ON' : 'OFF'}`,
193
+ debugKeys: (value) => `debug keys: ${value || '(none)'}`
194
+ },
195
+ stageTags: {
196
+ sending: 'SENDING',
197
+ thinking: 'THINKING',
198
+ streaming: 'STREAMING',
199
+ tooling: 'TOOLING',
200
+ running: 'RUNNING',
201
+ idle: 'IDLE'
202
+ },
203
+ toolActivity: {
204
+ blocked: 'Tool blocked',
205
+ doneRead: 'Read file',
206
+ doingRead: 'Reading file',
207
+ doneWrite: 'Wrote file',
208
+ doingWrite: 'Writing file',
209
+ doneList: 'Listed directory',
210
+ doingList: 'Listing directory',
211
+ doneCommand: 'Ran command',
212
+ doingCommand: 'Running command',
213
+ doneCreateTask: 'Created task',
214
+ doingCreateTask: 'Creating task',
215
+ doneUpdateTask: 'Updated task',
216
+ doingUpdateTask: 'Updating task',
217
+ doneGeneric: 'Completed tool',
218
+ doingGeneric: 'Running tool',
219
+ doneSkill: 'Completed skill',
220
+ doingSkill: 'Running skill',
221
+ toolFailed: (name) => `Tool failed: ${name}`,
222
+ waitingModelContinue: (detail) => `${detail}, waiting for model to continue`,
223
+ waitingModelAdjust: (detail) => `${detail}, waiting for model to adjust`
224
+ },
225
+ suggestion: {
226
+ singleTab: 'Tab completes the current command',
227
+ navFill: 'Tab stays in pick mode, ↑↓ select, Enter applies',
228
+ navEnter: 'Tab enters pick mode, then use ↑↓ to choose',
229
+ noSuggestions: '/ shows commands, Tab autocompletes, ↑↓ history, Ctrl+T tools',
230
+ oneNav: 'Tab or Enter applies the current command, ↑↓ history',
231
+ oneIdle: 'Tab completes the only candidate, Enter sends, ↑↓ history',
232
+ manyNav: (count) => `Tab cycles candidates, ↑↓ select, Enter applies (${count} items)`,
233
+ manyIdle: (count) => `Tab enters candidate mode (${count} items), ↑↓ history`
234
+ },
235
+ runtime: {
236
+ sendingToGateway: 'sending to gateway',
237
+ preparingRequest: 'preparing this turn',
238
+ modelThinking: 'model is thinking',
239
+ requestDelivered: 'request sent, waiting for first token',
240
+ generatingReply: 'generating reply',
241
+ streamingReply: 'reply is streaming',
242
+ replyCompleted: 'reply completed',
243
+ outputFinished: 'turn output finished',
244
+ toolRunning: 'tool running',
245
+ toolCompleted: 'tool completed',
246
+ toolBlocked: 'tool blocked',
247
+ toolFailed: 'tool failed',
248
+ skillRunning: 'skill running',
249
+ skillCompleted: 'skill completed',
250
+ skillFailed: 'skill failed',
251
+ compactingContext: 'compacting context',
252
+ autoCompactTriggered: (mode, threshold) => `auto-compact triggered (${mode}, threshold ${threshold}%)`,
253
+ requestFailed: 'request failed',
254
+ localCommandRunning: 'running local command',
255
+ queuedWaiting: 'queued, waiting for current turn',
256
+ idleReady: 'waiting for input',
257
+ idleReadyDetail: 'ready',
258
+ idleAfterTurn: 'idle',
259
+ idleAfterTurnDetail: 'ready for next input'
260
+ }
261
+ }
262
+ };
263
+
264
+ function normalizeLanguage(language) {
265
+ return String(language || '').toLowerCase().startsWith('en') ? 'en' : 'zh';
266
+ }
267
+
268
+ function getCopy(language) {
269
+ return TUI_COPY[normalizeLanguage(language)] || TUI_COPY.zh;
270
+ }
271
+
272
+ function messageLabel(label, copy) {
273
+ return copy.roleLabels[label] || String(label || '').toUpperCase();
274
+ }
275
+
276
+ function roleStyle(label) {
277
+ return ROLE_STYLES[label] || ROLE_STYLES.system;
278
+ }
279
+
280
+ function StatusPill({ label, value, color = 'cyanBright', textColor = 'black' }) {
281
+ return h(
282
+ Box,
283
+ { marginRight: 1 },
284
+ h(Text, { color: 'gray' }, `${label} `),
285
+ h(Text, { color: textColor, backgroundColor: color }, ` ${value} `)
286
+ );
287
+ }
288
+
289
+ function trimText(value, maxLen = 88) {
290
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
291
+ if (!text) return '';
292
+ if (text.length <= maxLen) return text;
293
+ return `${text.slice(0, maxLen - 3)}...`;
294
+ }
295
+
296
+ function parseToolDisplayName(name) {
297
+ const raw = String(name || '').trim();
298
+ const match = raw.match(/^([^(]+)\((.*)\)$/);
299
+ return {
300
+ raw,
301
+ base: match ? match[1] : raw,
302
+ target: match ? match[2] : ''
303
+ };
304
+ }
305
+
306
+ function getActivityDisplayParts(activity) {
307
+ if ((activity?.type || 'tool') === 'skill') {
308
+ return {
309
+ primary: `Skill`,
310
+ secondary: `(${activity?.name || 'unknown'})`
311
+ };
312
+ }
313
+ const parsed = parseToolDisplayName(activity?.name);
314
+ const labels = {
315
+ read_file: 'Read',
316
+ write_file: 'Write',
317
+ run_command: 'Command',
318
+ list_files: 'Glob',
319
+ create_task: 'Task',
320
+ update_task: 'Task'
321
+ };
322
+ return {
323
+ primary: labels[parsed.base] || parsed.base || 'Tool',
324
+ secondary: parsed.target ? `(${parsed.target})` : ''
325
+ };
326
+ }
327
+
328
+ function describeToolActivity(name, copy, { done = false, blocked = false } = {}) {
329
+ const { raw, base, target } = parseToolDisplayName(name);
330
+ const safeTarget = trimText(target, 72);
331
+ if (base === 'read_file') {
332
+ return blocked
333
+ ? `${copy.toolActivity.blocked}: read_file(${safeTarget || '.'})`
334
+ : done
335
+ ? `${copy.toolActivity.doneRead}: ${safeTarget || '.'}`
336
+ : `${copy.toolActivity.doingRead}: ${safeTarget || '.'}`;
337
+ }
338
+ if (base === 'write_file') {
339
+ return blocked
340
+ ? `${copy.toolActivity.blocked}: write_file(${safeTarget || '.'})`
341
+ : done
342
+ ? `${copy.toolActivity.doneWrite}: ${safeTarget || '.'}`
343
+ : `${copy.toolActivity.doingWrite}: ${safeTarget || '.'}`;
344
+ }
345
+ if (base === 'list_files') {
346
+ return blocked
347
+ ? `${copy.toolActivity.blocked}: list_files(${safeTarget || '.'})`
348
+ : done
349
+ ? `${copy.toolActivity.doneList}: ${safeTarget || '.'}`
350
+ : `${copy.toolActivity.doingList}: ${safeTarget || '.'}`;
351
+ }
352
+ if (base === 'run_command') {
353
+ return blocked
354
+ ? `${copy.toolActivity.blocked}: ${safeTarget || 'run_command'}`
355
+ : done
356
+ ? `${copy.toolActivity.doneCommand}: ${safeTarget || 'run_command'}`
357
+ : `${copy.toolActivity.doingCommand}: ${safeTarget || 'run_command'}`;
358
+ }
359
+ if (base === 'create_task') {
360
+ return blocked ? `${copy.toolActivity.blocked}: create_task` : done ? copy.toolActivity.doneCreateTask : copy.toolActivity.doingCreateTask;
361
+ }
362
+ if (base === 'update_task') {
363
+ return blocked ? `${copy.toolActivity.blocked}: update_task` : done ? copy.toolActivity.doneUpdateTask : copy.toolActivity.doingUpdateTask;
364
+ }
365
+ return blocked ? `${copy.toolActivity.blocked}: ${raw}` : done ? `${copy.toolActivity.doneGeneric}: ${raw}` : `${copy.toolActivity.doingGeneric}: ${raw}`;
366
+ }
367
+
368
+ function describeSkillActivity(name, copy, { done = false, failed = false } = {}) {
369
+ if (failed) return `${copy.runtime.skillFailed}: /${name}`;
370
+ if (done) return `${copy.toolActivity.doneSkill}: /${name}`;
371
+ return `${copy.toolActivity.doingSkill}: /${name}`;
372
+ }
373
+
374
+ function normalizeRuntimeStatus(status, copy) {
375
+ if (status && typeof status === 'object') {
376
+ return {
377
+ title: trimText(status.title || copy.generic.waitingForInput, 64),
378
+ detail: trimText(status.detail || '', 120),
379
+ color: status.color || 'gray'
380
+ };
381
+ }
382
+ return {
383
+ title: trimText(status || copy.generic.waitingForInput, 64),
384
+ detail: '',
385
+ color: 'gray'
386
+ };
387
+ }
388
+
389
+ function stageDescriptor(inputStage, busy, runtimeStatus, copy) {
390
+ const normalized = normalizeRuntimeStatus(runtimeStatus, copy);
391
+ const tag =
392
+ inputStage === 'sending'
393
+ ? copy.stageTags.sending
394
+ : inputStage === 'thinking'
395
+ ? copy.stageTags.thinking
396
+ : inputStage === 'streaming'
397
+ ? copy.stageTags.streaming
398
+ : inputStage === 'tooling'
399
+ ? copy.stageTags.tooling
400
+ : busy
401
+ ? copy.stageTags.running
402
+ : copy.stageTags.idle;
403
+ const color =
404
+ inputStage === 'sending'
405
+ ? 'yellowBright'
406
+ : inputStage === 'thinking'
407
+ ? 'cyanBright'
408
+ : inputStage === 'streaming'
409
+ ? 'greenBright'
410
+ : inputStage === 'tooling'
411
+ ? 'magentaBright'
412
+ : 'gray';
413
+ return {
414
+ tag,
415
+ color,
416
+ title: normalized.title,
417
+ detail: normalized.detail
418
+ };
419
+ }
420
+
421
+ function RuntimeStrip({ busy, runtimeStatus, loaderTick, copy }) {
422
+ const status = normalizeRuntimeStatus(runtimeStatus, copy);
423
+ const dots = '●○○'.slice(loaderTick % 3, (loaderTick % 3) + 1) || '●';
424
+ return h(
425
+ Box,
426
+ {
427
+ marginBottom: 1,
428
+ borderStyle: 'round',
429
+ borderColor: busy ? 'green' : 'gray',
430
+ paddingX: 1,
431
+ paddingY: 0
432
+ },
433
+ h(Text, { color: busy ? 'greenBright' : 'gray' }, busy ? copy.generic.live : copy.generic.idle),
434
+ h(Text, { color: 'gray' }, ' '),
435
+ h(Text, { color: busy ? 'cyanBright' : 'gray' }, dots),
436
+ h(Text, { color: 'gray' }, ' '),
437
+ h(Text, { color: busy ? 'white' : 'gray' }, status.title || copy.generic.waitingForInput)
438
+ );
439
+ }
440
+
441
+ function PlanStrip({ planState, copy }) {
442
+ if (!planState || !planState.total) return null;
443
+ const progress = `${planState.current}/${planState.total}`;
444
+ return h(
445
+ Box,
446
+ {
447
+ marginBottom: 1,
448
+ borderStyle: 'round',
449
+ borderColor: planState.failed ? 'red' : 'cyan',
450
+ paddingX: 1,
451
+ paddingY: 0,
452
+ flexDirection: 'column'
453
+ },
454
+ h(
455
+ Box,
456
+ { justifyContent: 'space-between' },
457
+ h(
458
+ Box,
459
+ null,
460
+ h(Text, { color: 'black', backgroundColor: planState.failed ? 'red' : 'cyanBright' }, ` ${copy.generic.plan} ${progress} `),
461
+ h(Text, { color: 'gray' }, ' '),
462
+ h(Text, { color: 'magentaBright' }, String(planState.role || 'agent').toUpperCase())
463
+ ),
464
+ h(Text, { color: planState.failed ? 'redBright' : 'greenBright' }, planState.failed ? copy.generic.attention : copy.generic.active)
465
+ ),
466
+ h(Text, { color: 'white' }, planState.title || 'running plan step'),
467
+ planState.steps.length > 0
468
+ ? h(
469
+ Box,
470
+ { marginTop: 1, flexDirection: 'column' },
471
+ ...planState.steps.slice(-4).map((step, idx) =>
472
+ h(
473
+ Box,
474
+ { key: `plan-step-${idx}` },
475
+ h(Text, { color: step.status === 'active' ? 'cyanBright' : step.status === 'failed' ? 'redBright' : 'gray' }, `${step.status === 'active' ? '>' : step.status === 'failed' ? 'x' : '·'} `),
476
+ h(Text, { color: step.status === 'active' ? 'white' : step.status === 'failed' ? 'redBright' : 'gray' }, `${step.index}/${step.total} ${step.role}: ${step.title}`)
477
+ )
478
+ )
479
+ )
480
+ : null
481
+ );
482
+ }
483
+
484
+ function Header({ sessionId, model, shellName }) {
485
+ const shortSession = String(sessionId || '').slice(-12) || '-';
486
+ return h(
487
+ Box,
488
+ { width: '100%', justifyContent: 'center', marginTop: 1, marginBottom: 2 },
489
+ h(
490
+ Box,
491
+ {
492
+ flexDirection: 'column',
493
+ borderStyle: 'round',
494
+ borderColor: 'cyan',
495
+ paddingX: 4,
496
+ paddingY: 1,
497
+ alignItems: 'center',
498
+ minWidth: 88
499
+ },
500
+ h(
501
+ Box,
502
+ { width: '100%', justifyContent: 'space-between', marginBottom: 1 },
503
+ h(Text, { color: 'cyan' }, 'CLI'),
504
+ h(Text, { color: 'greenBright' }, 'SAFE')
505
+ ),
506
+ ...BANNER.map((line, idx) =>
507
+ h(
508
+ Box,
509
+ { key: `b-${idx}`, justifyContent: 'center' },
510
+ h(Text, { color: BANNER_COLORS[idx] || 'cyanBright' }, line)
511
+ )
512
+ ),
513
+ h(Box, { height: 1 }),
514
+ h(Text, { color: 'gray' }, 'optimized for small-model workflows'),
515
+ h(Box, { height: 1 }),
516
+ h(
517
+ Box,
518
+ { flexDirection: 'row', justifyContent: 'center' },
519
+ h(StatusPill, { label: 'MODEL', value: model, color: 'cyanBright', textColor: 'black' }),
520
+ h(StatusPill, { label: 'SHELL', value: shellName || 'powershell', color: 'greenBright', textColor: 'black' }),
521
+ h(StatusPill, { label: 'SESSION', value: shortSession, color: 'magentaBright', textColor: 'black' })
522
+ )
523
+ )
524
+ );
525
+ }
526
+
527
+ function renderInlineCode(line, baseColor) {
528
+ const parts = line.split(/(`[^`]+`)/g);
529
+ return parts.map((part, idx) => {
530
+ if (part.startsWith('`') && part.endsWith('`') && part.length >= 2) {
531
+ return h(
532
+ Text,
533
+ { key: `ic-${idx}`, color: 'black', backgroundColor: 'yellow' },
534
+ part.slice(1, -1)
535
+ );
536
+ }
537
+ return h(Text, { key: `tx-${idx}`, color: baseColor }, part);
538
+ });
539
+ }
540
+
541
+ function renderTextLine(msg, line, idx, color) {
542
+ return h(
543
+ Box,
544
+ { key: `ln-wrap-${msg.id}-${idx}` },
545
+ h(Text, { key: `ln-${msg.id}-${idx}`, color }, ...renderInlineCode(line, color))
546
+ );
547
+ }
548
+
549
+ const BUBBLE_CHROME_ROWS = 4;
550
+
551
+ function charDisplayWidth(ch) {
552
+ const code = ch.codePointAt(0) || 0;
553
+ if (code === 0) return 0;
554
+ if (
555
+ code >= 0x1100 &&
556
+ (
557
+ code <= 0x115f ||
558
+ code === 0x2329 ||
559
+ code === 0x232a ||
560
+ (code >= 0x2e80 && code <= 0xa4cf && code !== 0x303f) ||
561
+ (code >= 0xac00 && code <= 0xd7a3) ||
562
+ (code >= 0xf900 && code <= 0xfaff) ||
563
+ (code >= 0xfe10 && code <= 0xfe19) ||
564
+ (code >= 0xfe30 && code <= 0xfe6f) ||
565
+ (code >= 0xff00 && code <= 0xff60) ||
566
+ (code >= 0xffe0 && code <= 0xffe6)
567
+ )
568
+ ) {
569
+ return 2;
570
+ }
571
+ return 1;
572
+ }
573
+
574
+ function wrapTextChunks(text, width = 72) {
575
+ const safeWidth = Math.max(8, width);
576
+ const chars = Array.from(String(text || ''));
577
+ if (chars.length === 0) return [''];
578
+ const lines = [];
579
+ let current = '';
580
+ let used = 0;
581
+ for (const ch of chars) {
582
+ const chWidth = charDisplayWidth(ch);
583
+ if (used > 0 && used + chWidth > safeWidth) {
584
+ lines.push(current);
585
+ current = ch;
586
+ used = chWidth;
587
+ continue;
588
+ }
589
+ current += ch;
590
+ used += chWidth;
591
+ }
592
+ if (current || lines.length === 0) lines.push(current);
593
+ return lines;
594
+ }
595
+
596
+ function pushWrappedRow(rows, baseRow, contentWidth) {
597
+ const chunks = wrapTextChunks(baseRow.text || '', contentWidth);
598
+ chunks.forEach((chunk, index) => {
599
+ rows.push({
600
+ ...baseRow,
601
+ text: chunk || ' ',
602
+ continuation: index > 0
603
+ });
604
+ });
605
+ }
606
+
607
+ function buildMessageRows(msg, showToolDetails, contentWidth = 72) {
608
+ const rows = [];
609
+ const pushTextRows = (text) => {
610
+ const lines = String(text || '').split('\n');
611
+ let codeFence = false;
612
+ for (const line of lines) {
613
+ const trimmed = line.trim();
614
+ if (trimmed.startsWith('```')) {
615
+ codeFence = !codeFence;
616
+ continue;
617
+ }
618
+ if (codeFence) {
619
+ pushWrappedRow(rows, { kind: 'code', text: line || ' ', color: 'gray' }, contentWidth);
620
+ continue;
621
+ }
622
+ let color = msg.color || roleStyle(msg.label).text || 'white';
623
+ if (line.startsWith('#')) color = 'cyanBright';
624
+ else if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) color = 'magentaBright';
625
+ else if (trimmed.startsWith('>')) color = 'yellow';
626
+ else if (/^[|└├│]/.test(trimmed)) color = 'gray';
627
+ pushWrappedRow(
628
+ rows,
629
+ {
630
+ kind: trimmed.startsWith('>') ? 'quote' : /^[|└├│]/.test(trimmed) ? 'tree' : 'text',
631
+ text: line || ' ',
632
+ color
633
+ },
634
+ trimmed.startsWith('>') ? Math.max(8, contentWidth - 3) : contentWidth
635
+ );
636
+ }
637
+ };
638
+
639
+ const pushActivityRows = (tool, idx, total) => {
640
+ const statusIcon = tool.status === 'done' ? '✓' : tool.status === 'blocked' || tool.status === 'error' ? '×' : '…';
641
+ const statusColor =
642
+ tool.status === 'done' ? 'greenBright' : tool.status === 'blocked' || tool.status === 'error' ? 'redBright' : 'yellow';
643
+ const durationText =
644
+ typeof tool.durationMs === 'number' ? `${(tool.durationMs / 1000).toFixed(1)}s` : '';
645
+ rows.push({
646
+ kind: 'activity',
647
+ activityType: tool.type || 'tool',
648
+ name: tool.name,
649
+ statusIcon,
650
+ statusColor,
651
+ status: tool.status,
652
+ durationText,
653
+ isLatestTool: idx === total - 1
654
+ });
655
+ if ((showToolDetails || idx === total - 1) && tool.summary && tool.status !== 'running') {
656
+ for (const line of String(tool.summary).split('\n')) {
657
+ pushWrappedRow(rows, { kind: 'activity-summary', text: line || ' ', color: 'gray' }, Math.max(8, contentWidth - 4));
658
+ }
659
+ }
660
+ };
661
+
662
+ if (Array.isArray(msg?.segments) && msg.segments.length > 0) {
663
+ const totalTools = msg.segments.filter((segment) => segment.type === 'tool' || segment.type === 'skill').length;
664
+ let toolIndex = 0;
665
+ for (const segment of msg.segments) {
666
+ if (segment.type === 'tool' || segment.type === 'skill') {
667
+ pushActivityRows(segment, toolIndex, totalTools);
668
+ toolIndex += 1;
669
+ } else {
670
+ pushTextRows(segment.text || '');
671
+ }
672
+ }
673
+ } else {
674
+ pushTextRows(msg?.text || '');
675
+ const toolCalls = Array.isArray(msg?.toolCalls) ? msg.toolCalls : [];
676
+ toolCalls.forEach((tool, idx) => pushActivityRows(tool, idx, toolCalls.length));
677
+ }
678
+
679
+ if (msg?.loading && (msg?.liveStatus || msg?.phase)) {
680
+ pushWrappedRow(
681
+ rows,
682
+ {
683
+ kind: 'status',
684
+ text: trimText(msg.liveStatus || msg.phase, 144)
685
+ },
686
+ Math.max(8, contentWidth - 2)
687
+ );
688
+ }
689
+
690
+ return rows;
691
+ }
692
+
693
+
694
+ function groupCommandSuggestions(items) {
695
+ const categoryMap = {
696
+ help: 'Core',
697
+ exit: 'Core',
698
+ commands: 'Core',
699
+ status: 'Runtime',
700
+ mode: 'Runtime',
701
+ compact: 'Runtime',
702
+ retry: 'Runtime',
703
+ tasks: 'Workspace',
704
+ checkpoint: 'Workspace',
705
+ history: 'Workspace',
706
+ config: 'Config',
707
+ debug: 'Config',
708
+ spec: 'Planning',
709
+ plan: 'Planning',
710
+ agents: 'Planning'
711
+ };
712
+ const grouped = new Map();
713
+ for (const item of items) {
714
+ const value = typeof item === 'string' ? item : String(item?.value || '');
715
+ const root = String(value || '').trim().slice(1).split(/\s+/)[0] || 'other';
716
+ const category = categoryMap[root] || 'Other';
717
+ if (!grouped.has(category)) grouped.set(category, []);
718
+ grouped.get(category).push(item);
719
+ }
720
+ return Array.from(grouped.entries());
721
+ }
722
+
723
+ function getSuggestionValue(item) {
724
+ return typeof item === 'string' ? item : String(item?.value || '');
725
+ }
726
+
727
+ function getSuggestionDisplay(item) {
728
+ return typeof item === 'string' ? item : String(item?.display || item?.value || '');
729
+ }
730
+
731
+ function MessageBubble({ msg, loaderTick, showToolDetails, rowWindow = null, contentWidth = 72, copy }) {
732
+ const theme = roleStyle(msg.label);
733
+ const allRows = buildMessageRows(msg, showToolDetails, contentWidth);
734
+ const start = rowWindow ? Math.max(0, rowWindow.start || 0) : 0;
735
+ const end = rowWindow ? Math.max(start, rowWindow.end || allRows.length) : allRows.length;
736
+ const visibleRows = allRows.slice(start, end);
737
+ const rendered = visibleRows.map((row, idx) => {
738
+ if (row.kind === 'activity') {
739
+ const activity = { type: row.activityType, name: row.name, status: row.status };
740
+ const display = getActivityDisplayParts(activity);
741
+ const dotColor =
742
+ activity.type === 'skill'
743
+ ? row.status === 'error'
744
+ ? 'redBright'
745
+ : 'blueBright'
746
+ : row.status === 'error' || row.status === 'blocked'
747
+ ? 'redBright'
748
+ : 'greenBright';
749
+ const textColor =
750
+ activity.type === 'skill'
751
+ ? row.status === 'error'
752
+ ? 'redBright'
753
+ : 'cyanBright'
754
+ : row.status === 'error' || row.status === 'blocked'
755
+ ? 'redBright'
756
+ : 'greenBright';
757
+ return h(
758
+ Box,
759
+ { key: `row-tool-${msg.id}-${idx}` },
760
+ h(Text, { color: 'gray' }, ' '),
761
+ h(Text, { color: dotColor }, '●'),
762
+ h(Text, { color: 'gray' }, ' '),
763
+ h(Text, { color: textColor }, display.primary),
764
+ h(Text, { color: 'gray' }, display.secondary),
765
+ row.durationText ? h(Text, { color: row.statusColor }, ` ${row.durationText}`) : null
766
+ );
767
+ }
768
+ if (row.kind === 'activity-summary') {
769
+ return h(
770
+ Box,
771
+ { key: `row-tool-summary-${msg.id}-${idx}`, marginLeft: 2 },
772
+ h(Text, { color: 'gray' }, `└ ${row.text}`)
773
+ );
774
+ }
775
+ if (row.kind === 'status') {
776
+ const dots = '.'.repeat((loaderTick % 3) + 1);
777
+ const phase = msg.phase;
778
+ const color =
779
+ phase === 'sending'
780
+ ? 'yellowBright'
781
+ : phase === 'queued'
782
+ ? 'cyanBright'
783
+ : phase === 'tooling'
784
+ ? 'magentaBright'
785
+ : phase === 'generating'
786
+ ? 'greenBright'
787
+ : 'cyanBright';
788
+ return h(
789
+ Box,
790
+ { key: `row-status-${msg.id}-${idx}`, marginTop: 1 },
791
+ h(Text, { color: 'gray' }, ' '),
792
+ h(Text, { color }, `${row.text}${dots}`)
793
+ );
794
+ }
795
+ if (row.kind === 'quote') {
796
+ return h(
797
+ Box,
798
+ { key: `row-quote-${msg.id}-${idx}`, marginTop: 1, marginLeft: 1, paddingLeft: 1 },
799
+ h(Text, { color: 'yellow' }, '▍ '),
800
+ h(Text, { color: row.color }, ...renderInlineCode(row.text, row.color))
801
+ );
802
+ }
803
+ if (row.kind === 'tree') {
804
+ return h(
805
+ Box,
806
+ { key: `row-tree-${msg.id}-${idx}`, marginLeft: 1 },
807
+ h(Text, { color: row.color }, row.text)
808
+ );
809
+ }
810
+ if (row.kind === 'code') {
811
+ return h(
812
+ Box,
813
+ { key: `row-code-${msg.id}-${idx}`, marginLeft: 1 },
814
+ h(Text, { color: 'gray' }, row.text)
815
+ );
816
+ }
817
+ return renderTextLine(msg, row.text, idx, row.color);
818
+ });
819
+
820
+ return h(
821
+ Box,
822
+ { marginBottom: 1, flexDirection: 'row' },
823
+ h(Box, { width: 2 }, h(Text, { color: theme.accent }, '│')),
824
+ h(
825
+ Box,
826
+ {
827
+ flexDirection: 'column',
828
+ borderStyle: 'round',
829
+ borderColor: theme.border,
830
+ paddingX: 1,
831
+ paddingY: 0,
832
+ width: '100%'
833
+ },
834
+ h(
835
+ Box,
836
+ { justifyContent: 'space-between', marginBottom: rendered.length > 0 ? 1 : 0 },
837
+ h(
838
+ Box,
839
+ null,
840
+ h(Text, { color: theme.badgeText, backgroundColor: theme.badgeBg }, ` ${messageLabel(msg.label, copy)} `)
841
+ ),
842
+ h(Text, { color: theme.chrome }, ' ')
843
+ ),
844
+ ...rendered
845
+ )
846
+ );
847
+ }
848
+
849
+ function MessageList({ messages, loaderTick, showToolDetails, contentWidth = 72, copy }) {
850
+ return h(
851
+ Box,
852
+ {
853
+ flexDirection: 'column',
854
+ paddingX: 0,
855
+ paddingY: 0
856
+ },
857
+ messages.length === 0 ? h(Text, { color: 'gray' }, copy.generic.noMessagesYet) : null,
858
+ ...messages.map((message) =>
859
+ h(MessageBubble, {
860
+ key: message.id,
861
+ msg: message,
862
+ loaderTick,
863
+ showToolDetails,
864
+ contentWidth,
865
+ copy
866
+ })
867
+ )
868
+ );
869
+ }
870
+
871
+ function SuggestionPanel({ commandSuggestions, suggestionNav, menuIndex, copy }) {
872
+ if (commandSuggestions.length === 0) return null;
873
+ const grouped = groupCommandSuggestions(commandSuggestions);
874
+ let flatIndex = -1;
875
+ const panelHint =
876
+ commandSuggestions.length === 1
877
+ ? copy.suggestion.singleTab
878
+ : suggestionNav
879
+ ? copy.suggestion.navFill
880
+ : copy.suggestion.navEnter;
881
+ return h(
882
+ Box,
883
+ {
884
+ marginTop: 1,
885
+ flexDirection: 'column',
886
+ borderStyle: 'round',
887
+ borderColor: 'magenta',
888
+ paddingX: 1,
889
+ paddingY: 0
890
+ },
891
+ h(
892
+ Box,
893
+ { marginBottom: 1 },
894
+ h(Text, { color: 'magentaBright' }, suggestionNav ? copy.generic.commandPaletteGroupedSelect : copy.generic.commandPaletteGroupedSuggestions),
895
+ h(Text, { color: 'gray' }, ` ${panelHint}`)
896
+ ),
897
+ ...grouped.flatMap(([group, items]) => {
898
+ const nodes = [
899
+ h(
900
+ Box,
901
+ { key: `group-${group}`, marginBottom: 0 },
902
+ h(Text, { color: 'gray' }, `${group.toUpperCase()} `),
903
+ h(Text, { color: 'black', backgroundColor: 'gray' }, ` ${items.length} `)
904
+ )
905
+ ];
906
+ items.forEach((c) => {
907
+ flatIndex += 1;
908
+ const active = suggestionNav && menuIndex === flatIndex;
909
+ const label = getSuggestionDisplay(c);
910
+ nodes.push(
911
+ h(
912
+ Box,
913
+ { key: `opt-${group}-${getSuggestionValue(c)}` },
914
+ h(Text, { color: active ? 'black' : 'magenta', backgroundColor: active ? 'magentaBright' : undefined }, `${active ? ' > ' : ' '}${label}`)
915
+ )
916
+ );
917
+ });
918
+ return nodes;
919
+ })
920
+ );
921
+ }
922
+
923
+ function PendingPanel({ pendingQueue, copy }) {
924
+ if (pendingQueue.length === 0) return null;
925
+ return h(
926
+ Box,
927
+ {
928
+ marginTop: 1,
929
+ flexDirection: 'column',
930
+ borderStyle: 'round',
931
+ borderColor: 'cyan',
932
+ paddingX: 1,
933
+ paddingY: 0
934
+ },
935
+ h(Text, { color: 'cyanBright' }, `${copy.generic.pendingQueue} | ${pendingQueue.length}`),
936
+ ...pendingQueue
937
+ .slice(0, 3)
938
+ .map((p, idx) =>
939
+ h(Text, { key: `pending-${idx}`, color: 'cyan' }, `- ${typeof p === 'string' ? p : p.line}`)
940
+ )
941
+ );
942
+ }
943
+
944
+ function describeCommandHint(commandSuggestions, suggestionNav, copy) {
945
+ const count = Array.isArray(commandSuggestions) ? commandSuggestions.length : 0;
946
+ if (count === 0) {
947
+ return copy.suggestion.noSuggestions;
948
+ }
949
+ if (count === 1) {
950
+ return suggestionNav ? copy.suggestion.oneNav : copy.suggestion.oneIdle;
951
+ }
952
+ return suggestionNav ? copy.suggestion.manyNav(count) : copy.suggestion.manyIdle(count);
953
+ }
954
+
955
+ function buildHistoryMatches(history, needle) {
956
+ const source = Array.isArray(history) ? history : [];
957
+ const query = String(needle || '').toLowerCase();
958
+ const items = [];
959
+ const seen = new Set();
960
+ for (let i = source.length - 1; i >= 0; i -= 1) {
961
+ const entry = String(source[i] || '');
962
+ const key = entry.toLowerCase();
963
+ if (query && !key.startsWith(query)) continue;
964
+ if (seen.has(entry)) continue;
965
+ seen.add(entry);
966
+ items.push(entry);
967
+ }
968
+ return items;
969
+ }
970
+
971
+ function InputBar({
972
+ beforeCursor,
973
+ underCursor,
974
+ afterCursor,
975
+ cursorVisible,
976
+ busy,
977
+ inputStage,
978
+ pendingQueueLength,
979
+ showToolDetails,
980
+ runtimeStatus,
981
+ commandSuggestions,
982
+ suggestionNav,
983
+ copy
984
+ }) {
985
+ const status = stageDescriptor(inputStage, busy, runtimeStatus, copy);
986
+ const commandHint = describeCommandHint(commandSuggestions, suggestionNav, copy);
987
+ return h(
988
+ Box,
989
+ {
990
+ marginTop: 1,
991
+ flexDirection: 'column',
992
+ borderStyle: 'round',
993
+ borderColor: 'cyan',
994
+ paddingX: 1,
995
+ paddingY: 0
996
+ },
997
+ h(
998
+ Box,
999
+ { justifyContent: 'space-between', marginBottom: 1 },
1000
+ h(
1001
+ Box,
1002
+ null,
1003
+ h(Text, { color: 'cyanBright' }, copy.generic.commandBar),
1004
+ h(Text, { color: 'gray' }, ` ${commandHint}`)
1005
+ ),
1006
+ h(
1007
+ Box,
1008
+ null,
1009
+ h(Text, { color: 'black', backgroundColor: 'greenBright' }, ` ${copy.generic.safeMode} `),
1010
+ inputStage !== 'idle' || busy ? h(Text, { color: status.color }, ` ${status.tag}`) : null,
1011
+ pendingQueueLength > 0 ? h(Text, { color: 'cyanBright' }, ` ${copy.generic.queued} ${pendingQueueLength}`) : null,
1012
+ h(Text, { color: showToolDetails ? 'greenBright' : 'gray' }, ` ${copy.generic.tools} ${showToolDetails ? copy.generic.open : copy.generic.collapsed}`)
1013
+ )
1014
+ ),
1015
+ h(
1016
+ Box,
1017
+ null,
1018
+ h(Text, { color: 'cyan' }, 'codemini> '),
1019
+ h(Text, { color: 'white' }, beforeCursor),
1020
+ h(
1021
+ Text,
1022
+ {
1023
+ color: cursorVisible ? 'black' : 'white',
1024
+ backgroundColor: cursorVisible ? 'cyanBright' : undefined
1025
+ },
1026
+ underCursor || ' '
1027
+ ),
1028
+ h(Text, { color: 'white' }, afterCursor)
1029
+ )
1030
+ );
1031
+ }
1032
+
1033
+ function SignatureBar({ version = '' }) {
1034
+ return h(
1035
+ Box,
1036
+ {
1037
+ marginTop: 1,
1038
+ width: '100%',
1039
+ justifyContent: 'space-between'
1040
+ },
1041
+ h(Text, { color: 'gray' }, ' '),
1042
+ h(
1043
+ Box,
1044
+ { flexGrow: 1, justifyContent: 'center' },
1045
+ h(Text, { color: 'gray' }, 'developed by '),
1046
+ h(Text, { color: 'magentaBright' }, '@havingautism')
1047
+ ),
1048
+ h(Text, { color: 'gray' }, `v${version}`)
1049
+ );
1050
+ }
1051
+
1052
+ function makeStatus(title, detail = '', color = 'gray') {
1053
+ return { title, detail, color };
1054
+ }
1055
+
1056
+ function formatRuntimeSnapshot(snapshot) {
1057
+ if (!snapshot || typeof snapshot !== 'object') return '';
1058
+ return [
1059
+ `mode=${snapshot.mode || '-'}`,
1060
+ `model=${snapshot.model || '-'}`,
1061
+ `max_ctx=${snapshot.maxContextTokens || '-'}`,
1062
+ `session=${snapshot.sessionId || '-'}`
1063
+ ].join(' | ');
1064
+ }
1065
+
1066
+ function makeIdleStatus(copy, snapshot, variant = 'ready') {
1067
+ return makeStatus(
1068
+ variant === 'after' ? copy.runtime.idleAfterTurn : copy.runtime.idleReady,
1069
+ formatRuntimeSnapshot(snapshot) || (variant === 'after' ? copy.runtime.idleAfterTurnDetail : copy.runtime.idleReadyDetail),
1070
+ 'gray'
1071
+ );
1072
+ }
1073
+
1074
+ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName = 'powershell', version = '' }) {
1075
+ const copy = getCopy(language);
1076
+ const stdoutCols = Number(process.stdout?.columns || 120);
1077
+ const [inputValue, setInputValue] = useState('');
1078
+ const [cursorIndex, setCursorIndex] = useState(0);
1079
+ const [messages, setMessages] = useState([]);
1080
+ const [busy, setBusy] = useState(false);
1081
+ const [history, setHistory] = useState([]);
1082
+ const [historyIndex, setHistoryIndex] = useState(null);
1083
+ const [draftBeforeHistory, setDraftBeforeHistory] = useState('');
1084
+ const [historyMatches, setHistoryMatches] = useState([]);
1085
+ const [menuIndex, setMenuIndex] = useState(0);
1086
+ const [suggestionNav, setSuggestionNav] = useState(false);
1087
+ const [cursorVisible, setCursorVisible] = useState(true);
1088
+ const [displaySessionId, setDisplaySessionId] = useState(sessionId);
1089
+ const [displayModel, setDisplayModel] = useState(model);
1090
+ const [pendingQueue, setPendingQueue] = useState([]);
1091
+ const [loaderTick, setLoaderTick] = useState(0);
1092
+ const [runtimeStatus, setRuntimeStatus] = useState(
1093
+ makeIdleStatus(copy, runtime.getRuntimeState?.(), 'ready')
1094
+ );
1095
+ const [inputStage, setInputStage] = useState('idle');
1096
+ const [planState, setPlanState] = useState({
1097
+ current: 0,
1098
+ total: 0,
1099
+ role: '',
1100
+ title: '',
1101
+ failed: false,
1102
+ steps: []
1103
+ });
1104
+ const [debugKeys, setDebugKeys] = useState(false);
1105
+ const [lastKeyDebug, setLastKeyDebug] = useState('');
1106
+ const [showToolDetails, setShowToolDetails] = useState(false);
1107
+ const activeAssistantIdRef = useRef(null);
1108
+ const activeUserMessageIdRef = useRef(null);
1109
+ const cursorIndexRef = useRef(0);
1110
+ const inFlightRef = useRef(false);
1111
+ const pendingQueueRef = useRef([]);
1112
+ const deltaBufferRef = useRef('');
1113
+ const deltaFlushTimerRef = useRef(null);
1114
+ const escSeqRef = useRef('');
1115
+ const planTextBufferRef = useRef('');
1116
+ const { exit } = useApp();
1117
+ const startupHint = copy.generic.startupHint;
1118
+ const isBackspaceKey = (value, key) =>
1119
+ Boolean(key?.backspace) || value === '\u0008' || value === '\u007f' || (key?.ctrl && value === 'h');
1120
+ const isDeleteKey = (value, key) =>
1121
+ Boolean(key?.delete) ||
1122
+ (key?.ctrl && value === 'd') ||
1123
+ value === '\u001b[3~' ||
1124
+ value === '\u001b[3;2~' ||
1125
+ value === '\u001b[3;5~';
1126
+ const isPrintableInput = (value, key) => {
1127
+ if (!value || key?.ctrl || key?.meta) return false;
1128
+ if (value.includes('\u001b')) return false;
1129
+ for (const ch of value) {
1130
+ const code = ch.codePointAt(0) || 0;
1131
+ if (code < 32 || code === 127) return false;
1132
+ }
1133
+ return true;
1134
+ };
1135
+
1136
+ const nextId = useMemo(() => {
1137
+ let id = 0;
1138
+ return () => {
1139
+ id += 1;
1140
+ return `m-${id}`;
1141
+ };
1142
+ }, []);
1143
+
1144
+ const commandSuggestions =
1145
+ inputValue.startsWith('/')
1146
+ ? (runtime.getCompletionOptions(inputValue) || []).slice(0, 8)
1147
+ : [];
1148
+ const hasTransientPanels =
1149
+ commandSuggestions.length > 0 || pendingQueue.length > 0 || debugKeys || Boolean(planState?.total);
1150
+ const messageContentWidth = Math.max(24, stdoutCols - 18);
1151
+
1152
+ const syncRuntimeVisualState = (variant = 'ready') => {
1153
+ const snapshot = runtime.getRuntimeState?.();
1154
+ if (!snapshot) return;
1155
+ setDisplaySessionId(snapshot.sessionId || sessionId);
1156
+ setDisplayModel(snapshot.model || model);
1157
+ setRuntimeStatus(makeIdleStatus(copy, snapshot, variant));
1158
+ };
1159
+
1160
+ useEffect(() => {
1161
+ syncRuntimeVisualState('ready');
1162
+ }, []);
1163
+
1164
+ const updatePlanProgressFromText = (chunk) => {
1165
+ if (!chunk) return;
1166
+ planTextBufferRef.current = `${planTextBufferRef.current}${chunk}`.slice(-1200);
1167
+ const pattern = /\[plan\]\s+Step\s+(\d+)\/(\d+)\s+->\s+([^:]+):\s+([^\n\r]+)/gi;
1168
+ let match;
1169
+ let last = null;
1170
+ while ((match = pattern.exec(planTextBufferRef.current))) {
1171
+ last = match;
1172
+ }
1173
+ if (!last) return;
1174
+ const current = Number(last[1]);
1175
+ const total = Number(last[2]);
1176
+ const role = String(last[3] || '').trim();
1177
+ const title = String(last[4] || '').trim();
1178
+ setActiveAssistantMeta({
1179
+ planStep: `${current}/${total} · ${role}: ${title}`
1180
+ });
1181
+ setPlanState((prev) => {
1182
+ const steps = (prev.steps || [])
1183
+ .map((step) => (step.index === current ? { ...step, status: 'done' } : step))
1184
+ .filter((step, idx, arr) => arr.findIndex((x) => x.index === step.index) === idx);
1185
+ const withoutCurrent = steps.filter((step) => step.index !== current);
1186
+ return {
1187
+ current,
1188
+ total,
1189
+ role,
1190
+ title,
1191
+ failed: false,
1192
+ steps: [...withoutCurrent, { index: current, total, role, title, status: 'active' }].sort((a, b) => a.index - b.index)
1193
+ };
1194
+ });
1195
+ };
1196
+
1197
+ const flushAssistantDelta = () => {
1198
+ const targetId = activeAssistantIdRef.current;
1199
+ const delta = deltaBufferRef.current;
1200
+ if (!targetId || !delta) return;
1201
+ deltaBufferRef.current = '';
1202
+ setMessages((prev) =>
1203
+ prev.map((m) => {
1204
+ if (m.id !== targetId) return m;
1205
+ const segments = Array.isArray(m.segments) ? [...m.segments] : [];
1206
+ const last = segments.at(-1);
1207
+ if (last?.type === 'text') {
1208
+ segments[segments.length - 1] = { ...last, text: `${last.text || ''}${delta}` };
1209
+ } else {
1210
+ segments.push({ type: 'text', text: delta });
1211
+ }
1212
+ return { ...m, text: `${m.text}${delta}`, segments };
1213
+ })
1214
+ );
1215
+ };
1216
+
1217
+ const queueAssistantDelta = (chunk) => {
1218
+ if (!chunk) return;
1219
+ deltaBufferRef.current += chunk;
1220
+ if (deltaFlushTimerRef.current) return;
1221
+ deltaFlushTimerRef.current = setTimeout(() => {
1222
+ deltaFlushTimerRef.current = null;
1223
+ flushAssistantDelta();
1224
+ }, 40);
1225
+ };
1226
+
1227
+ const updateActivityStatusOnActiveAssistant = (toolEvent) => {
1228
+ const targetId = activeAssistantIdRef.current;
1229
+ if (!targetId) return;
1230
+ setMessages((prev) =>
1231
+ prev.map((m) => {
1232
+ if (m.id !== targetId) return m;
1233
+ const toolCalls = Array.isArray(m.toolCalls) ? [...m.toolCalls] : [];
1234
+ const activityType = toolEvent.type || 'tool';
1235
+ const byId = toolEvent.id
1236
+ ? toolCalls.findIndex((t) => t.type === activityType && t.id && t.id === toolEvent.id)
1237
+ : -1;
1238
+ const byNameRunning = toolCalls.findIndex(
1239
+ (t) => (t.type || 'tool') === activityType && t.name === toolEvent.name && t.status !== 'done'
1240
+ );
1241
+ const idx = byId !== -1 ? byId : byNameRunning;
1242
+
1243
+ if (idx === -1) {
1244
+ toolCalls.push({
1245
+ type: activityType,
1246
+ id: toolEvent.id || '',
1247
+ name: toolEvent.name,
1248
+ status: toolEvent.status,
1249
+ ...(toolEvent.durationMs !== undefined ? { durationMs: toolEvent.durationMs } : {}),
1250
+ ...(toolEvent.summary ? { summary: toolEvent.summary } : {})
1251
+ });
1252
+ } else {
1253
+ toolCalls[idx] = {
1254
+ ...toolCalls[idx],
1255
+ type: activityType,
1256
+ id: toolEvent.id || toolCalls[idx].id,
1257
+ status: toolEvent.status,
1258
+ ...(toolEvent.durationMs !== undefined ? { durationMs: toolEvent.durationMs } : {}),
1259
+ ...(toolEvent.summary ? { summary: toolEvent.summary } : {})
1260
+ };
1261
+ }
1262
+ const segments = Array.isArray(m.segments) ? [...m.segments] : [];
1263
+ const bySegmentId = toolEvent.id
1264
+ ? segments.findIndex((segment) => segment.type === activityType && segment.id === toolEvent.id)
1265
+ : -1;
1266
+ const bySegmentName = segments.findIndex(
1267
+ (segment) => segment.type === activityType && segment.name === toolEvent.name && segment.status !== 'done'
1268
+ );
1269
+ const segmentIdx = bySegmentId !== -1 ? bySegmentId : bySegmentName;
1270
+ const patch = {
1271
+ type: activityType,
1272
+ id: toolEvent.id || '',
1273
+ name: toolEvent.name,
1274
+ status: toolEvent.status,
1275
+ ...(toolEvent.durationMs !== undefined ? { durationMs: toolEvent.durationMs } : {}),
1276
+ ...(toolEvent.summary ? { summary: toolEvent.summary } : {})
1277
+ };
1278
+ if (segmentIdx === -1) {
1279
+ segments.push(patch);
1280
+ } else {
1281
+ segments[segmentIdx] = {
1282
+ ...segments[segmentIdx],
1283
+ ...patch
1284
+ };
1285
+ }
1286
+ return { ...m, toolCalls, segments };
1287
+ })
1288
+ );
1289
+ };
1290
+
1291
+ const updateMessageMeta = (messageId, patch) => {
1292
+ if (!messageId) return;
1293
+ setMessages((prev) => prev.map((m) => (m.id === messageId ? { ...m, ...patch } : m)));
1294
+ };
1295
+
1296
+ const appendResultMessage = (result) => {
1297
+ if (result.type === 'noop') return;
1298
+ if (
1299
+ result.type === 'system' &&
1300
+ typeof result.text === 'string' &&
1301
+ result.text.startsWith('[debug:keys:')
1302
+ ) {
1303
+ if (result.text === '[debug:keys:on]') {
1304
+ setDebugKeys(true);
1305
+ setMessages((prev) => [
1306
+ ...prev,
1307
+ { id: nextId(), label: 'system', text: copy.generic.keyboardDebugEnabled, color: 'yellowBright' }
1308
+ ]);
1309
+ return;
1310
+ }
1311
+ if (result.text === '[debug:keys:off]') {
1312
+ setDebugKeys(false);
1313
+ setMessages((prev) => [
1314
+ ...prev,
1315
+ { id: nextId(), label: 'system', text: copy.generic.keyboardDebugDisabled, color: 'yellowBright' }
1316
+ ]);
1317
+ return;
1318
+ }
1319
+ if (result.text === '[debug:keys:status]') {
1320
+ setMessages((prev) => [
1321
+ ...prev,
1322
+ {
1323
+ id: nextId(),
1324
+ label: 'system',
1325
+ text: copy.generic.keyboardDebugStatus(debugKeys),
1326
+ color: 'yellowBright'
1327
+ }
1328
+ ]);
1329
+ return;
1330
+ }
1331
+ }
1332
+ if (result.type === 'assistant') {
1333
+ if (!activeAssistantIdRef.current && result.text) {
1334
+ setMessages((prev) => [
1335
+ ...prev,
1336
+ { id: nextId(), label: 'coder', text: result.text, color: 'greenBright' }
1337
+ ]);
1338
+ }
1339
+ return;
1340
+ }
1341
+ setMessages((prev) => [
1342
+ ...prev,
1343
+ { id: nextId(), label: 'system', text: result.text || '', color: 'yellowBright' }
1344
+ ]);
1345
+ };
1346
+
1347
+ const setActiveAssistantMeta = (patch) => {
1348
+ const targetId = activeAssistantIdRef.current;
1349
+ if (!targetId) return;
1350
+ setMessages((prev) => prev.map((m) => (m.id === targetId ? { ...m, ...patch } : m)));
1351
+ };
1352
+
1353
+ const finalizeActiveAssistant = () => {
1354
+ setActiveAssistantMeta({ loading: false, phase: undefined, liveStatus: undefined, planStep: undefined });
1355
+ };
1356
+
1357
+ const ensureActiveAssistant = () => {
1358
+ if (activeAssistantIdRef.current) return activeAssistantIdRef.current;
1359
+ const aid = nextId();
1360
+ activeAssistantIdRef.current = aid;
1361
+ setMessages((prev) => [
1362
+ ...prev,
1363
+ {
1364
+ id: aid,
1365
+ label: 'coder',
1366
+ text: '',
1367
+ color: 'greenBright',
1368
+ toolCalls: [],
1369
+ segments: [],
1370
+ loading: true,
1371
+ phase: 'thinking',
1372
+ liveStatus: copy.runtime.modelThinking
1373
+ }
1374
+ ]);
1375
+ return aid;
1376
+ };
1377
+
1378
+ const runSubmission = (line, userMessageId = null) => {
1379
+ inFlightRef.current = true;
1380
+ activeUserMessageIdRef.current = userMessageId;
1381
+ setBusy(true);
1382
+ setInputStage('sending');
1383
+ setRuntimeStatus(makeStatus(copy.runtime.sendingToGateway, copy.runtime.preparingRequest, 'yellowBright'));
1384
+ setPlanState({ current: 0, total: 0, role: '', title: '', failed: false, steps: [] });
1385
+ planTextBufferRef.current = '';
1386
+ activeAssistantIdRef.current = null;
1387
+ deltaBufferRef.current = '';
1388
+
1389
+ runtime
1390
+ .submit(line, (event) => {
1391
+ if (event?.type === 'assistant:start') {
1392
+ setRuntimeStatus(makeStatus(copy.runtime.modelThinking, copy.runtime.requestDelivered, 'cyanBright'));
1393
+ setInputStage('thinking');
1394
+ updateMessageMeta(activeUserMessageIdRef.current, {
1395
+ loading: false,
1396
+ phase: undefined,
1397
+ liveStatus: undefined
1398
+ });
1399
+ ensureActiveAssistant();
1400
+ setActiveAssistantMeta({ loading: true, phase: 'thinking', liveStatus: copy.runtime.modelThinking });
1401
+ }
1402
+ if (event?.type === 'assistant:delta') {
1403
+ ensureActiveAssistant();
1404
+ updatePlanProgressFromText(event.text);
1405
+ setRuntimeStatus(makeStatus(copy.runtime.generatingReply, copy.runtime.streamingReply, 'greenBright'));
1406
+ setInputStage('streaming');
1407
+ setActiveAssistantMeta({ loading: true, phase: 'generating', liveStatus: copy.runtime.generatingReply });
1408
+ queueAssistantDelta(event.text);
1409
+ }
1410
+ if (event?.type === 'assistant:response') {
1411
+ setRuntimeStatus(makeStatus(copy.runtime.replyCompleted, copy.runtime.outputFinished, 'greenBright'));
1412
+ setInputStage('idle');
1413
+ flushAssistantDelta();
1414
+ finalizeActiveAssistant();
1415
+ if (!activeAssistantIdRef.current && event.text) {
1416
+ setMessages((prev) => [
1417
+ ...prev,
1418
+ { id: nextId(), label: 'coder', text: event.text, color: 'greenBright' }
1419
+ ]);
1420
+ }
1421
+ }
1422
+ if (event?.type === 'tool:start') {
1423
+ ensureActiveAssistant();
1424
+ const detail = describeToolActivity(event.name, copy);
1425
+ setRuntimeStatus(makeStatus(copy.runtime.toolRunning, detail, 'magentaBright'));
1426
+ setInputStage('tooling');
1427
+ setActiveAssistantMeta({ loading: true, phase: 'tooling', liveStatus: detail });
1428
+ updateActivityStatusOnActiveAssistant({
1429
+ type: 'tool',
1430
+ id: event.id,
1431
+ name: event.name,
1432
+ status: 'running'
1433
+ });
1434
+ }
1435
+ if (event?.type === 'tool:end') {
1436
+ const detail = describeToolActivity(event.name, copy, { done: true });
1437
+ setRuntimeStatus(makeStatus(copy.runtime.toolCompleted, copy.toolActivity.waitingModelContinue(detail), 'cyanBright'));
1438
+ setInputStage('thinking');
1439
+ setActiveAssistantMeta({ loading: true, phase: 'thinking', liveStatus: copy.toolActivity.waitingModelContinue(detail) });
1440
+ updateActivityStatusOnActiveAssistant({
1441
+ type: 'tool',
1442
+ id: event.id,
1443
+ name: event.name,
1444
+ status: 'done',
1445
+ durationMs: event.durationMs,
1446
+ summary: event.summary
1447
+ });
1448
+ }
1449
+ if (event?.type === 'tool:blocked') {
1450
+ const detail = describeToolActivity(event.name, copy, { blocked: true });
1451
+ setRuntimeStatus(makeStatus(copy.runtime.toolBlocked, detail, 'redBright'));
1452
+ setInputStage('thinking');
1453
+ setActiveAssistantMeta({ loading: true, phase: 'thinking', liveStatus: copy.toolActivity.waitingModelAdjust(detail) });
1454
+ setPlanState((prev) => ({
1455
+ ...prev,
1456
+ failed: prev.total > 0,
1457
+ steps: (prev.steps || []).map((step) =>
1458
+ step.index === prev.current ? { ...step, status: 'failed' } : step
1459
+ )
1460
+ }));
1461
+ updateActivityStatusOnActiveAssistant({
1462
+ type: 'tool',
1463
+ id: event.id,
1464
+ name: event.name,
1465
+ status: 'blocked'
1466
+ });
1467
+ }
1468
+ if (event?.type === 'tool:error') {
1469
+ const detail = copy.toolActivity.toolFailed(event.name);
1470
+ setRuntimeStatus(makeStatus(copy.runtime.toolFailed, event.summary || detail, 'redBright'));
1471
+ setInputStage('thinking');
1472
+ setActiveAssistantMeta({ loading: true, phase: 'thinking', liveStatus: copy.toolActivity.waitingModelAdjust(detail) });
1473
+ setPlanState((prev) => ({
1474
+ ...prev,
1475
+ failed: prev.total > 0,
1476
+ steps: (prev.steps || []).map((step) =>
1477
+ step.index === prev.current ? { ...step, status: 'failed' } : step
1478
+ )
1479
+ }));
1480
+ updateActivityStatusOnActiveAssistant({
1481
+ type: 'tool',
1482
+ id: event.id,
1483
+ name: event.name,
1484
+ status: 'error',
1485
+ durationMs: event.durationMs,
1486
+ summary: event.summary
1487
+ });
1488
+ }
1489
+ if (event?.type === 'skill:start') {
1490
+ ensureActiveAssistant();
1491
+ const detail = describeSkillActivity(event.name, copy);
1492
+ setRuntimeStatus(makeStatus(copy.runtime.skillRunning, detail, 'blueBright'));
1493
+ setInputStage('tooling');
1494
+ setActiveAssistantMeta({ loading: true, phase: 'tooling', liveStatus: detail });
1495
+ updateActivityStatusOnActiveAssistant({
1496
+ type: 'skill',
1497
+ name: event.name,
1498
+ status: 'running'
1499
+ });
1500
+ }
1501
+ if (event?.type === 'skill:end') {
1502
+ const detail = describeSkillActivity(event.name, copy, { done: true });
1503
+ setRuntimeStatus(makeStatus(copy.runtime.skillCompleted, detail, 'blueBright'));
1504
+ setInputStage('thinking');
1505
+ setActiveAssistantMeta({ loading: true, phase: 'thinking', liveStatus: detail });
1506
+ updateActivityStatusOnActiveAssistant({
1507
+ type: 'skill',
1508
+ name: event.name,
1509
+ status: 'done'
1510
+ });
1511
+ }
1512
+ if (event?.type === 'skill:error') {
1513
+ const detail = describeSkillActivity(event.name, copy, { failed: true });
1514
+ setRuntimeStatus(makeStatus(copy.runtime.skillFailed, event.summary || detail, 'redBright'));
1515
+ setInputStage('thinking');
1516
+ setActiveAssistantMeta({ loading: true, phase: 'thinking', liveStatus: detail });
1517
+ updateActivityStatusOnActiveAssistant({
1518
+ type: 'skill',
1519
+ name: event.name,
1520
+ status: 'error',
1521
+ summary: event.summary
1522
+ });
1523
+ }
1524
+ if (event?.type === 'compact:auto') {
1525
+ setRuntimeStatus(makeStatus(copy.runtime.compactingContext, `auto compact ${event.mode}`, 'yellowBright'));
1526
+ setMessages((prev) => [
1527
+ ...prev,
1528
+ {
1529
+ id: nextId(),
1530
+ label: 'system',
1531
+ text: copy.runtime.autoCompactTriggered(event.mode, event.threshold),
1532
+ color: 'yellowBright'
1533
+ }
1534
+ ]);
1535
+ }
1536
+ })
1537
+ .then((result) => {
1538
+ try {
1539
+ syncRuntimeVisualState('after');
1540
+ } catch {
1541
+ setDisplaySessionId(sessionId);
1542
+ }
1543
+ updateMessageMeta(activeUserMessageIdRef.current, {
1544
+ loading: false,
1545
+ phase: undefined,
1546
+ liveStatus: undefined
1547
+ });
1548
+ if (result.type === 'exit') {
1549
+ exit();
1550
+ return;
1551
+ }
1552
+ if (result.type !== 'noop') setInputStage('idle');
1553
+ if (planTextBufferRef.current && planState.total > 0) {
1554
+ setPlanState((prev) => ({
1555
+ ...prev,
1556
+ steps: (prev.steps || []).map((step) =>
1557
+ step.index === prev.current && step.status === 'active' ? { ...step, status: prev.failed ? 'failed' : 'done' } : step
1558
+ )
1559
+ }));
1560
+ }
1561
+ syncRuntimeVisualState(result.type === 'noop' ? 'ready' : 'after');
1562
+ if (result.type === 'noop') return;
1563
+ appendResultMessage(result);
1564
+ })
1565
+ .catch((err) => {
1566
+ setRuntimeStatus(makeStatus(copy.runtime.requestFailed, err.message, 'redBright'));
1567
+ setInputStage('idle');
1568
+ updateMessageMeta(activeUserMessageIdRef.current, {
1569
+ loading: false,
1570
+ phase: undefined,
1571
+ liveStatus: undefined
1572
+ });
1573
+ setPlanState((prev) => ({
1574
+ ...prev,
1575
+ failed: prev.total > 0,
1576
+ steps: (prev.steps || []).map((step) =>
1577
+ step.index === prev.current ? { ...step, status: 'failed' } : step
1578
+ )
1579
+ }));
1580
+ setMessages((prev) => [
1581
+ ...prev,
1582
+ { id: nextId(), label: 'error', text: err.message, color: 'redBright' }
1583
+ ]);
1584
+ })
1585
+ .finally(() => {
1586
+ flushAssistantDelta();
1587
+ finalizeActiveAssistant();
1588
+ activeAssistantIdRef.current = null;
1589
+ activeUserMessageIdRef.current = null;
1590
+ if (deltaFlushTimerRef.current) {
1591
+ clearTimeout(deltaFlushTimerRef.current);
1592
+ deltaFlushTimerRef.current = null;
1593
+ }
1594
+ inFlightRef.current = false;
1595
+ setBusy(false);
1596
+ if (pendingQueueRef.current.length === 0) {
1597
+ setInputStage('idle');
1598
+ }
1599
+ if (pendingQueueRef.current.length === 0) {
1600
+ syncRuntimeVisualState('ready');
1601
+ }
1602
+
1603
+ if (pendingQueueRef.current.length > 0) {
1604
+ const [next, ...rest] = pendingQueueRef.current;
1605
+ pendingQueueRef.current = rest;
1606
+ setPendingQueue(rest);
1607
+ updateMessageMeta(next.messageId, {
1608
+ loading: true,
1609
+ phase: 'sending',
1610
+ liveStatus: copy.runtime.sendingToGateway
1611
+ });
1612
+ runSubmission(next.line, next.messageId);
1613
+ }
1614
+ });
1615
+ };
1616
+
1617
+ const runImmediateLocalCommand = (line, userMessageId) => {
1618
+ updateMessageMeta(userMessageId, {
1619
+ loading: true,
1620
+ phase: 'sending',
1621
+ liveStatus: copy.runtime.localCommandRunning
1622
+ });
1623
+ runtime
1624
+ .submit(line)
1625
+ .then((result) => {
1626
+ updateMessageMeta(userMessageId, {
1627
+ loading: false,
1628
+ phase: undefined,
1629
+ liveStatus: undefined
1630
+ });
1631
+ try {
1632
+ syncRuntimeVisualState('after');
1633
+ } catch {
1634
+ setDisplaySessionId(sessionId);
1635
+ }
1636
+ if (result.type === 'exit') {
1637
+ exit();
1638
+ return;
1639
+ }
1640
+ appendResultMessage(result);
1641
+ })
1642
+ .catch((err) => {
1643
+ updateMessageMeta(userMessageId, {
1644
+ loading: false,
1645
+ phase: undefined,
1646
+ liveStatus: undefined
1647
+ });
1648
+ setMessages((prev) => [
1649
+ ...prev,
1650
+ { id: nextId(), label: 'error', text: err.message, color: 'redBright' }
1651
+ ]);
1652
+ });
1653
+ };
1654
+
1655
+ useInput((value, key) => {
1656
+ if (debugKeys) {
1657
+ const printable = JSON.stringify(value ?? '');
1658
+ const flags = Object.entries(key || {})
1659
+ .filter(([, v]) => Boolean(v))
1660
+ .map(([k]) => k)
1661
+ .join(',');
1662
+ setLastKeyDebug(`key=${printable} flags=${flags || '-'}`);
1663
+ }
1664
+
1665
+ if (shouldCaptureEscapeSequence(value, escSeqRef.current)) {
1666
+ escSeqRef.current += value || '';
1667
+ const seq = escSeqRef.current;
1668
+ if (seq === '\u001b[3~' || seq === '\u001b[3;2~' || seq === '\u001b[3;5~') {
1669
+ const idxSnapshot = cursorIndexRef.current;
1670
+ setInputValue((prev) => {
1671
+ const idx = Math.min(Math.max(idxSnapshot, 0), prev.length);
1672
+ if (idx <= 0) return prev;
1673
+ return `${prev.slice(0, idx - 1)}${prev.slice(idx)}`;
1674
+ });
1675
+ const next = Math.max(0, idxSnapshot - 1);
1676
+ cursorIndexRef.current = next;
1677
+ setCursorIndex(next);
1678
+ escSeqRef.current = '';
1679
+ return;
1680
+ }
1681
+ if (seq.length > 8) {
1682
+ escSeqRef.current = '';
1683
+ }
1684
+ return;
1685
+ } else {
1686
+ escSeqRef.current = '';
1687
+ }
1688
+
1689
+ if (key.upArrow) {
1690
+ if (suggestionNav && commandSuggestions.length > 0) {
1691
+ setMenuIndex((prev) => Math.max(0, prev - 1));
1692
+ return;
1693
+ }
1694
+ if (history.length === 0) return;
1695
+ if (historyIndex === null) {
1696
+ const matches = buildHistoryMatches(history, inputValue);
1697
+ if (matches.length === 0) return;
1698
+ setDraftBeforeHistory(inputValue);
1699
+ setHistoryMatches(matches);
1700
+ setHistoryIndex(0);
1701
+ setInputValue(matches[0]);
1702
+ cursorIndexRef.current = matches[0].length;
1703
+ setCursorIndex(matches[0].length);
1704
+ return;
1705
+ }
1706
+ if (historyMatches.length === 0) return;
1707
+ const idx = Math.min(historyMatches.length - 1, historyIndex + 1);
1708
+ setHistoryIndex(idx);
1709
+ setInputValue(historyMatches[idx]);
1710
+ cursorIndexRef.current = historyMatches[idx].length;
1711
+ setCursorIndex(historyMatches[idx].length);
1712
+ return;
1713
+ }
1714
+
1715
+ if (key.downArrow) {
1716
+ if (suggestionNav && commandSuggestions.length > 0) {
1717
+ setMenuIndex((prev) => Math.min(commandSuggestions.length - 1, prev + 1));
1718
+ return;
1719
+ }
1720
+ if (history.length === 0 || historyIndex === null) return;
1721
+ const idx = historyIndex - 1;
1722
+ if (idx < 0) {
1723
+ setHistoryIndex(null);
1724
+ setHistoryMatches([]);
1725
+ setInputValue(draftBeforeHistory);
1726
+ cursorIndexRef.current = draftBeforeHistory.length;
1727
+ setCursorIndex(draftBeforeHistory.length);
1728
+ return;
1729
+ }
1730
+ if (historyMatches.length === 0) return;
1731
+ setHistoryIndex(idx);
1732
+ setInputValue(historyMatches[idx]);
1733
+ cursorIndexRef.current = historyMatches[idx].length;
1734
+ setCursorIndex(historyMatches[idx].length);
1735
+ return;
1736
+ }
1737
+ if (key.leftArrow) {
1738
+ setSuggestionNav(false);
1739
+ const next = Math.max(0, cursorIndexRef.current - 1);
1740
+ cursorIndexRef.current = next;
1741
+ setCursorIndex(next);
1742
+ return;
1743
+ }
1744
+ if (key.rightArrow) {
1745
+ setSuggestionNav(false);
1746
+ const next = Math.min(inputValue.length, cursorIndexRef.current + 1);
1747
+ cursorIndexRef.current = next;
1748
+ setCursorIndex(next);
1749
+ return;
1750
+ }
1751
+ if (key.home) {
1752
+ setSuggestionNav(false);
1753
+ cursorIndexRef.current = 0;
1754
+ setCursorIndex(0);
1755
+ return;
1756
+ }
1757
+ if (key.end) {
1758
+ setSuggestionNav(false);
1759
+ cursorIndexRef.current = inputValue.length;
1760
+ setCursorIndex(inputValue.length);
1761
+ return;
1762
+ }
1763
+
1764
+ if (key.return) {
1765
+ if (suggestionNav && commandSuggestions.length > 0) {
1766
+ const selected = commandSuggestions[Math.min(menuIndex, commandSuggestions.length - 1)];
1767
+ const selectedValue = getSuggestionValue(selected);
1768
+ const current = inputValue.trim();
1769
+ if (selectedValue && current !== selectedValue.trim()) {
1770
+ setInputValue(selectedValue);
1771
+ cursorIndexRef.current = selectedValue.length;
1772
+ setCursorIndex(selectedValue.length);
1773
+ setSuggestionNav(false);
1774
+ return;
1775
+ }
1776
+ }
1777
+
1778
+ const line = inputValue.trim();
1779
+ setInputValue('');
1780
+ setSuggestionNav(false);
1781
+ cursorIndexRef.current = 0;
1782
+ setCursorIndex(0);
1783
+ if (!line) return;
1784
+
1785
+ setHistory((prev) => [...prev, line]);
1786
+ setHistoryIndex(null);
1787
+ setDraftBeforeHistory('');
1788
+ setHistoryMatches([]);
1789
+
1790
+ const messageId = nextId();
1791
+ const immediateLocal =
1792
+ typeof runtime.isImmediateLocalInput === 'function' &&
1793
+ runtime.isImmediateLocalInput(line);
1794
+ setMessages((prev) => [
1795
+ ...prev,
1796
+ {
1797
+ id: messageId,
1798
+ label: 'you',
1799
+ text: line,
1800
+ color: 'white',
1801
+ loading: true,
1802
+ phase: immediateLocal ? 'sending' : inFlightRef.current ? 'queued' : 'sending',
1803
+ liveStatus: immediateLocal
1804
+ ? copy.runtime.localCommandRunning
1805
+ : inFlightRef.current
1806
+ ? copy.runtime.queuedWaiting
1807
+ : copy.runtime.sendingToGateway
1808
+ }
1809
+ ]);
1810
+ if (immediateLocal) {
1811
+ runImmediateLocalCommand(line, messageId);
1812
+ } else if (inFlightRef.current) {
1813
+ pendingQueueRef.current = [...pendingQueueRef.current, { line, messageId }];
1814
+ setPendingQueue([...pendingQueueRef.current]);
1815
+ } else {
1816
+ runSubmission(line, messageId);
1817
+ }
1818
+ return;
1819
+ }
1820
+
1821
+ if (isBackspaceKey(value, key) || isDeleteKey(value, key)) {
1822
+ setSuggestionNav(false);
1823
+ const backspace = true;
1824
+ const idxSnapshot = cursorIndexRef.current;
1825
+ setInputValue((prev) => {
1826
+ const idx = Math.min(Math.max(idxSnapshot, 0), prev.length);
1827
+ if (idx <= 0) return prev;
1828
+ return `${prev.slice(0, idx - 1)}${prev.slice(idx)}`;
1829
+ });
1830
+ const next = Math.max(0, idxSnapshot - 1);
1831
+ cursorIndexRef.current = next;
1832
+ setCursorIndex(next);
1833
+ setHistoryIndex(null);
1834
+ setHistoryMatches([]);
1835
+ return;
1836
+ }
1837
+
1838
+ if (key.tab) {
1839
+ if (!inputValue.startsWith('/')) return;
1840
+ if (commandSuggestions.length === 0) return;
1841
+ if (commandSuggestions.length === 1) {
1842
+ const selected = getSuggestionValue(commandSuggestions[0]);
1843
+ setInputValue(selected);
1844
+ cursorIndexRef.current = selected.length;
1845
+ setCursorIndex(selected.length);
1846
+ setSuggestionNav(false);
1847
+ return;
1848
+ }
1849
+ setSuggestionNav(true);
1850
+ return;
1851
+ }
1852
+
1853
+ if (key.ctrl && value === 'c') {
1854
+ exit();
1855
+ return;
1856
+ }
1857
+ if (key.ctrl && value === 't') {
1858
+ setShowToolDetails((prev) => !prev);
1859
+ return;
1860
+ }
1861
+
1862
+ if (isPrintableInput(value, key)) {
1863
+ setSuggestionNav(false);
1864
+ const idxSnapshot = cursorIndexRef.current;
1865
+ setInputValue((prev) => `${prev.slice(0, idxSnapshot)}${value}${prev.slice(idxSnapshot)}`);
1866
+ const next = idxSnapshot + value.length;
1867
+ cursorIndexRef.current = next;
1868
+ setCursorIndex(next);
1869
+ setHistoryIndex(null);
1870
+ setHistoryMatches([]);
1871
+ return;
1872
+ }
1873
+
1874
+ if (key.ctrl && value === 'j') {
1875
+ setSuggestionNav(false);
1876
+ const idxSnapshot = cursorIndexRef.current;
1877
+ setInputValue((prev) => `${prev.slice(0, idxSnapshot)}\n${prev.slice(idxSnapshot)}`);
1878
+ const next = idxSnapshot + 1;
1879
+ cursorIndexRef.current = next;
1880
+ setCursorIndex(next);
1881
+ setHistoryIndex(null);
1882
+ setHistoryMatches([]);
1883
+ }
1884
+ });
1885
+
1886
+ useEffect(() => {
1887
+ setMessages([
1888
+ {
1889
+ id: nextId(),
1890
+ label: 'system',
1891
+ text: startupHint,
1892
+ color: 'yellowBright'
1893
+ }
1894
+ ]);
1895
+ }, [nextId, runtime]);
1896
+
1897
+ useEffect(() => {
1898
+ let alive = true;
1899
+ if (typeof runtime.getInputHistory !== 'function') return () => {};
1900
+ runtime
1901
+ .getInputHistory()
1902
+ .then((items) => {
1903
+ if (!alive || !Array.isArray(items) || items.length === 0) return;
1904
+ setHistory(items.map((v) => String(v)));
1905
+ })
1906
+ .catch(() => {});
1907
+ return () => {
1908
+ alive = false;
1909
+ };
1910
+ }, [runtime]);
1911
+
1912
+ useEffect(() => {
1913
+ setCursorVisible(true);
1914
+ }, []);
1915
+
1916
+ useEffect(() => {
1917
+ const hasLoadingMessage = messages.some((m) => m.loading);
1918
+ if (!busy && !hasLoadingMessage) return () => {};
1919
+ const timer = setInterval(() => {
1920
+ setLoaderTick((prev) => prev + 1);
1921
+ }, 500);
1922
+ return () => clearInterval(timer);
1923
+ }, [busy, messages]);
1924
+
1925
+ useEffect(() => {
1926
+ if (commandSuggestions.length === 0) {
1927
+ setSuggestionNav(false);
1928
+ if (menuIndex !== 0) setMenuIndex(0);
1929
+ return;
1930
+ }
1931
+ if (menuIndex >= commandSuggestions.length) setMenuIndex(0);
1932
+ }, [menuIndex, commandSuggestions.length]);
1933
+
1934
+ useEffect(() => {
1935
+ const safe = Math.min(Math.max(cursorIndexRef.current, 0), inputValue.length);
1936
+ cursorIndexRef.current = safe;
1937
+ setCursorIndex(safe);
1938
+ }, [inputValue.length]);
1939
+
1940
+ useEffect(
1941
+ () => () => {
1942
+ if (deltaFlushTimerRef.current) {
1943
+ clearTimeout(deltaFlushTimerRef.current);
1944
+ deltaFlushTimerRef.current = null;
1945
+ }
1946
+ },
1947
+ []
1948
+ );
1949
+
1950
+ const beforeCursor = inputValue.slice(0, cursorIndex);
1951
+ const underCursor = inputValue.slice(cursorIndex, cursorIndex + 1);
1952
+ const afterCursor = inputValue.slice(cursorIndex + 1);
1953
+ const hasConversationStarted = messages.some((m) =>
1954
+ ['you', 'coder', 'pending', 'error'].includes(m.label)
1955
+ );
1956
+ const visibleMessages = hasConversationStarted
1957
+ ? messages.filter((m) => !(m.label === 'system' && m.text === startupHint))
1958
+ : messages;
1959
+
1960
+ return h(
1961
+ Box,
1962
+ { flexDirection: 'column' },
1963
+ h(Header, { sessionId: displaySessionId, model: displayModel, shellName }),
1964
+ h(RuntimeStrip, { busy, runtimeStatus, loaderTick, copy }),
1965
+ h(PlanStrip, { planState, copy }),
1966
+ h(MessageList, {
1967
+ messages: visibleMessages,
1968
+ loaderTick,
1969
+ showToolDetails,
1970
+ contentWidth: messageContentWidth,
1971
+ copy
1972
+ }),
1973
+ h(
1974
+ Box,
1975
+ { marginTop: 1 },
1976
+ h(
1977
+ Text,
1978
+ { color: 'gray' },
1979
+ `${showToolDetails ? copy.generic.toolSummaryExpanded : copy.generic.toolSummaryCollapsed} (${copy.generic.toggleToolSummary}) · ${copy.generic.scrollHint}`
1980
+ )
1981
+ ),
1982
+ h(SuggestionPanel, { commandSuggestions, suggestionNav, menuIndex, copy }),
1983
+ h(PendingPanel, { pendingQueue, copy }),
1984
+ debugKeys
1985
+ ? h(
1986
+ Box,
1987
+ { marginTop: 1 },
1988
+ h(Text, { color: 'yellow' }, copy.generic.debugKeys(lastKeyDebug))
1989
+ )
1990
+ : null,
1991
+ h(InputBar, {
1992
+ beforeCursor,
1993
+ underCursor,
1994
+ afterCursor,
1995
+ cursorVisible,
1996
+ busy,
1997
+ inputStage,
1998
+ pendingQueueLength: pendingQueue.length,
1999
+ showToolDetails,
2000
+ runtimeStatus,
2001
+ commandSuggestions,
2002
+ suggestionNav,
2003
+ copy
2004
+ }),
2005
+ h(SignatureBar, { version })
2006
+ );
2007
+ }