@undefineds.co/linx 0.2.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 (100) hide show
  1. package/README.md +91 -0
  2. package/dist/index.js +578 -0
  3. package/dist/index.js.map +1 -0
  4. package/dist/lib/account-api.js +100 -0
  5. package/dist/lib/account-api.js.map +1 -0
  6. package/dist/lib/account-session.js +30 -0
  7. package/dist/lib/account-session.js.map +1 -0
  8. package/dist/lib/ai-command.js +411 -0
  9. package/dist/lib/ai-command.js.map +1 -0
  10. package/dist/lib/chat-api.js +122 -0
  11. package/dist/lib/chat-api.js.map +1 -0
  12. package/dist/lib/codex-plugin/bridge.js +109 -0
  13. package/dist/lib/codex-plugin/bridge.js.map +1 -0
  14. package/dist/lib/codex-plugin/codex-native-proxy.js +358 -0
  15. package/dist/lib/codex-plugin/codex-native-proxy.js.map +1 -0
  16. package/dist/lib/codex-plugin/index.js +4 -0
  17. package/dist/lib/codex-plugin/index.js.map +1 -0
  18. package/dist/lib/codex-plugin/runner.js +38 -0
  19. package/dist/lib/codex-plugin/runner.js.map +1 -0
  20. package/dist/lib/credentials-store.js +74 -0
  21. package/dist/lib/credentials-store.js.map +1 -0
  22. package/dist/lib/default-model.js +8 -0
  23. package/dist/lib/default-model.js.map +1 -0
  24. package/dist/lib/login-command.js +51 -0
  25. package/dist/lib/login-command.js.map +1 -0
  26. package/dist/lib/models.js +6 -0
  27. package/dist/lib/models.js.map +1 -0
  28. package/dist/lib/oidc-auth.js +451 -0
  29. package/dist/lib/oidc-auth.js.map +1 -0
  30. package/dist/lib/oidc-session-storage.js +37 -0
  31. package/dist/lib/oidc-session-storage.js.map +1 -0
  32. package/dist/lib/pi-adapter/auth.js +38 -0
  33. package/dist/lib/pi-adapter/auth.js.map +1 -0
  34. package/dist/lib/pi-adapter/branding.js +239 -0
  35. package/dist/lib/pi-adapter/branding.js.map +1 -0
  36. package/dist/lib/pi-adapter/index.js +4 -0
  37. package/dist/lib/pi-adapter/index.js.map +1 -0
  38. package/dist/lib/pi-adapter/interactive.js +63 -0
  39. package/dist/lib/pi-adapter/interactive.js.map +1 -0
  40. package/dist/lib/pi-adapter/runtime.js +271 -0
  41. package/dist/lib/pi-adapter/runtime.js.map +1 -0
  42. package/dist/lib/pi-adapter/stream.js +314 -0
  43. package/dist/lib/pi-adapter/stream.js.map +1 -0
  44. package/dist/lib/pi-adapter/theme.js +84 -0
  45. package/dist/lib/pi-adapter/theme.js.map +1 -0
  46. package/dist/lib/pod-chat-store.js +200 -0
  47. package/dist/lib/pod-chat-store.js.map +1 -0
  48. package/dist/lib/profile-identity.js +116 -0
  49. package/dist/lib/profile-identity.js.map +1 -0
  50. package/dist/lib/prompt.js +82 -0
  51. package/dist/lib/prompt.js.map +1 -0
  52. package/dist/lib/runtime-target.js +12 -0
  53. package/dist/lib/runtime-target.js.map +1 -0
  54. package/dist/lib/solid-auth.js +55 -0
  55. package/dist/lib/solid-auth.js.map +1 -0
  56. package/dist/lib/thread-utils.js +12 -0
  57. package/dist/lib/thread-utils.js.map +1 -0
  58. package/dist/lib/watch/archive.js +110 -0
  59. package/dist/lib/watch/archive.js.map +1 -0
  60. package/dist/lib/watch/auth.js +56 -0
  61. package/dist/lib/watch/auth.js.map +1 -0
  62. package/dist/lib/watch/codex-composer.js +219 -0
  63. package/dist/lib/watch/codex-composer.js.map +1 -0
  64. package/dist/lib/watch/codex-footer.js +88 -0
  65. package/dist/lib/watch/codex-footer.js.map +1 -0
  66. package/dist/lib/watch/codex-overlay.js +154 -0
  67. package/dist/lib/watch/codex-overlay.js.map +1 -0
  68. package/dist/lib/watch/codex-request-form.js +341 -0
  69. package/dist/lib/watch/codex-request-form.js.map +1 -0
  70. package/dist/lib/watch/codex-request-input.js +187 -0
  71. package/dist/lib/watch/codex-request-input.js.map +1 -0
  72. package/dist/lib/watch/display.js +1514 -0
  73. package/dist/lib/watch/display.js.map +1 -0
  74. package/dist/lib/watch/format.js +161 -0
  75. package/dist/lib/watch/format.js.map +1 -0
  76. package/dist/lib/watch/hooks/claude.js +12 -0
  77. package/dist/lib/watch/hooks/claude.js.map +1 -0
  78. package/dist/lib/watch/hooks/codebuddy.js +18 -0
  79. package/dist/lib/watch/hooks/codebuddy.js.map +1 -0
  80. package/dist/lib/watch/hooks/codex.js +13 -0
  81. package/dist/lib/watch/hooks/codex.js.map +1 -0
  82. package/dist/lib/watch/hooks/index.js +24 -0
  83. package/dist/lib/watch/hooks/index.js.map +1 -0
  84. package/dist/lib/watch/hooks/shared.js +19 -0
  85. package/dist/lib/watch/hooks/shared.js.map +1 -0
  86. package/dist/lib/watch/index.js +10 -0
  87. package/dist/lib/watch/index.js.map +1 -0
  88. package/dist/lib/watch/pod-ai.js +168 -0
  89. package/dist/lib/watch/pod-ai.js.map +1 -0
  90. package/dist/lib/watch/pod-approval.js +578 -0
  91. package/dist/lib/watch/pod-approval.js.map +1 -0
  92. package/dist/lib/watch/pod-persistence.js +226 -0
  93. package/dist/lib/watch/pod-persistence.js.map +1 -0
  94. package/dist/lib/watch/runner.js +1124 -0
  95. package/dist/lib/watch/runner.js.map +1 -0
  96. package/dist/lib/watch/types.js +2 -0
  97. package/dist/lib/watch/types.js.map +1 -0
  98. package/dist/watch-cli.js +142 -0
  99. package/dist/watch-cli.js.map +1 -0
  100. package/package.json +38 -0
@@ -0,0 +1,1514 @@
1
+ import { emitKeypressEvents } from 'node:readline';
2
+ import { stdin, stdout } from 'node:process';
3
+ import { CodexComposer } from './codex-composer.js';
4
+ import { renderFooterLine } from './codex-footer.js';
5
+ import { CodexRequestForm } from './codex-request-form.js';
6
+ import { renderCodexOverlay } from './codex-overlay.js';
7
+ import { renderCodexRequestInputDetailed, } from './codex-request-input.js';
8
+ const ANSI = {
9
+ reset: '\x1b[0m',
10
+ bold: '\x1b[1m',
11
+ dim: '\x1b[2m',
12
+ red: '\x1b[31m',
13
+ green: '\x1b[32m',
14
+ magenta: '\x1b[35m',
15
+ cyan: '\x1b[36m',
16
+ };
17
+ function createAbortError() {
18
+ const error = new Error('The operation was aborted.');
19
+ error.name = 'AbortError';
20
+ return error;
21
+ }
22
+ function supportsWatchTui() {
23
+ if (process.env.LINX_WATCH_PLAIN === '1') {
24
+ return false;
25
+ }
26
+ if (!stdout.isTTY || !stdin.isTTY) {
27
+ return false;
28
+ }
29
+ return process.env.TERM !== 'dumb';
30
+ }
31
+ function clipLine(text, width) {
32
+ if (width <= 0) {
33
+ return '';
34
+ }
35
+ if (text.length <= width) {
36
+ return text.padEnd(width, ' ');
37
+ }
38
+ if (width === 1) {
39
+ return text.slice(0, 1);
40
+ }
41
+ if (width <= 3) {
42
+ return text.slice(0, width);
43
+ }
44
+ return `${text.slice(0, width - 3)}...`;
45
+ }
46
+ function wrapText(text, width) {
47
+ if (width <= 1) {
48
+ return [text.slice(0, Math.max(width, 0))];
49
+ }
50
+ const lines = [];
51
+ for (const rawLine of text.split('\n')) {
52
+ let remaining = rawLine || ' ';
53
+ while (remaining.length > width) {
54
+ lines.push(remaining.slice(0, width));
55
+ remaining = remaining.slice(width);
56
+ }
57
+ lines.push(remaining);
58
+ }
59
+ return lines;
60
+ }
61
+ function shortSessionId(id) {
62
+ if (id.length <= 24) {
63
+ return id;
64
+ }
65
+ return `${id.slice(0, 12)}...${id.slice(-8)}`;
66
+ }
67
+ function applyAnsi(text, ...styles) {
68
+ if (!text) {
69
+ return text;
70
+ }
71
+ return `${styles.join('')}${text}${ANSI.reset}`;
72
+ }
73
+ export function formatWatchElapsed(elapsedMs) {
74
+ const totalSeconds = Math.max(0, Math.floor(elapsedMs / 1000));
75
+ if (totalSeconds < 60) {
76
+ return `${totalSeconds}s`;
77
+ }
78
+ if (totalSeconds < 3600) {
79
+ const minutes = Math.floor(totalSeconds / 60);
80
+ const seconds = totalSeconds % 60;
81
+ return `${minutes}m ${seconds.toString().padStart(2, '0')}s`;
82
+ }
83
+ const hours = Math.floor(totalSeconds / 3600);
84
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
85
+ const seconds = totalSeconds % 60;
86
+ return `${hours}h ${minutes.toString().padStart(2, '0')}m ${seconds.toString().padStart(2, '0')}s`;
87
+ }
88
+ function statusGlyph(phase, elapsedMs) {
89
+ if (phase === 'starting' || phase === 'running') {
90
+ const frames = ['-', '\\', '|', '/'];
91
+ return frames[Math.floor(elapsedMs / 125) % frames.length] ?? '-';
92
+ }
93
+ if (phase === 'approval' || phase === 'question') {
94
+ return '?';
95
+ }
96
+ return '>';
97
+ }
98
+ function statusLabel(phase) {
99
+ switch (phase) {
100
+ case 'starting':
101
+ return 'Starting';
102
+ case 'ready':
103
+ return 'Ready';
104
+ case 'running':
105
+ return 'Working';
106
+ case 'approval':
107
+ return 'Approval required';
108
+ case 'question':
109
+ return 'Input required';
110
+ }
111
+ }
112
+ function statusHint(phase) {
113
+ switch (phase) {
114
+ case 'starting':
115
+ return 'Ctrl+C to exit';
116
+ case 'ready':
117
+ return 'Enter to send | /help | /exit';
118
+ case 'running':
119
+ return 'Ctrl+C to exit';
120
+ case 'approval':
121
+ return 'y/s/n/c';
122
+ case 'question':
123
+ return 'answer + Enter';
124
+ }
125
+ }
126
+ function compactStatusDetail(detail) {
127
+ if (!detail) {
128
+ return undefined;
129
+ }
130
+ const normalized = detail.trim();
131
+ if (!normalized) {
132
+ return undefined;
133
+ }
134
+ if (normalized.length <= 24) {
135
+ return normalized;
136
+ }
137
+ return `${normalized.slice(0, 21)}...`;
138
+ }
139
+ function shortPath(value, width = 28) {
140
+ if (value.length <= width) {
141
+ return value;
142
+ }
143
+ const parts = value.split('/').filter(Boolean);
144
+ if (parts.length === 0) {
145
+ return value.slice(-width);
146
+ }
147
+ const tail = parts.slice(-2).join('/');
148
+ if (tail.length + 4 <= width) {
149
+ return `.../${tail}`;
150
+ }
151
+ return `...${value.slice(-(width - 3))}`;
152
+ }
153
+ function footerHintForPhase(phase) {
154
+ switch (phase) {
155
+ case 'starting':
156
+ return {
157
+ emptyHint: 'Starting',
158
+ draftHint: 'Starting',
159
+ };
160
+ case 'ready':
161
+ return {
162
+ emptyHint: '/help · /exit · /model <id> · /debug on|off',
163
+ draftHint: 'Enter send · Shift+Enter newline · Alt+Enter follow-up',
164
+ };
165
+ case 'running':
166
+ return {
167
+ emptyHint: '/help · /exit · /model <id> · /debug on|off',
168
+ draftHint: 'Enter steer · Shift+Enter newline · Alt+Enter follow-up',
169
+ };
170
+ case 'approval':
171
+ return {
172
+ emptyHint: 'Approve: y · session: s · reject: n · cancel: c',
173
+ draftHint: 'Approve: y · session: s · reject: n · cancel: c',
174
+ };
175
+ case 'question':
176
+ return {
177
+ emptyHint: 'Answer and press Enter',
178
+ draftHint: 'Answer and press Enter',
179
+ };
180
+ }
181
+ }
182
+ function summarizeToolField(value) {
183
+ if (typeof value === 'string') {
184
+ const trimmed = value.trim();
185
+ return trimmed || undefined;
186
+ }
187
+ if (Array.isArray(value)) {
188
+ const parts = value
189
+ .map((entry) => summarizeToolField(entry))
190
+ .filter((entry) => Boolean(entry));
191
+ return parts.length > 0 ? parts.join(' ') : undefined;
192
+ }
193
+ if (typeof value === 'number' || typeof value === 'boolean') {
194
+ return String(value);
195
+ }
196
+ return undefined;
197
+ }
198
+ export function summarizeWatchToolCall(name, args) {
199
+ const detail = args
200
+ ? summarizeToolField(args.command
201
+ ?? args.cmd
202
+ ?? args.pattern
203
+ ?? args.query
204
+ ?? args.path
205
+ ?? args.filePath
206
+ ?? args.file
207
+ ?? args.url
208
+ ?? args.cwd)
209
+ : undefined;
210
+ if (!detail) {
211
+ return name;
212
+ }
213
+ const summarizedDetail = /^(\/|~\/|\.\.?\/)/.test(detail) ? shortPath(detail, 40) : detail;
214
+ return `${name} · ${summarizedDetail}`;
215
+ }
216
+ export function summarizeWatchDebugPayload(raw) {
217
+ if (typeof raw === 'string') {
218
+ const trimmed = raw.trim();
219
+ if (!trimmed) {
220
+ return { text: 'raw event' };
221
+ }
222
+ if (trimmed.length <= 96) {
223
+ return { text: trimmed };
224
+ }
225
+ return {
226
+ text: `${trimmed.slice(0, 93)}...`,
227
+ detail: trimmed,
228
+ };
229
+ }
230
+ const serialized = JSON.stringify(raw);
231
+ if (!serialized) {
232
+ return { text: 'raw event' };
233
+ }
234
+ try {
235
+ const parsed = typeof raw === 'object' && raw !== null ? raw : null;
236
+ const method = typeof parsed?.method === 'string' ? parsed.method : undefined;
237
+ const update = typeof parsed?.params === 'object' && parsed.params !== null
238
+ ? parsed.params.update
239
+ : undefined;
240
+ const updateType = typeof update?.sessionUpdate === 'string'
241
+ ? update.sessionUpdate
242
+ : typeof update?.type === 'string'
243
+ ? update.type
244
+ : undefined;
245
+ const summary = method ?? updateType ?? 'raw event';
246
+ if (serialized.length <= 120) {
247
+ return { text: `${summary} · ${serialized}` };
248
+ }
249
+ return {
250
+ text: summary,
251
+ detail: serialized,
252
+ };
253
+ }
254
+ catch {
255
+ if (serialized.length <= 96) {
256
+ return { text: serialized };
257
+ }
258
+ return {
259
+ text: `${serialized.slice(0, 93)}...`,
260
+ detail: serialized,
261
+ };
262
+ }
263
+ }
264
+ function editorBorderColor(phase) {
265
+ switch (phase) {
266
+ case 'starting':
267
+ return ANSI.dim;
268
+ case 'ready':
269
+ return ANSI.cyan;
270
+ case 'running':
271
+ return ANSI.magenta;
272
+ case 'approval':
273
+ return ANSI.cyan;
274
+ case 'question':
275
+ return ANSI.green;
276
+ }
277
+ }
278
+ function renderBorderBox(lines, width, title, color) {
279
+ const innerWidth = Math.max(1, width - 4);
280
+ const label = title.trim() ? ` ${title.trim()} ` : '';
281
+ const topPlain = `┌${label}${'─'.repeat(Math.max(0, width - label.length - 2))}┐`;
282
+ const bottomPlain = `└${'─'.repeat(Math.max(0, width - 2))}┘`;
283
+ return [
284
+ applyAnsi(clipLine(topPlain, width), color),
285
+ ...lines.map((line) => {
286
+ const plain = line.replace(/\x1b\[[0-9;]*m/g, '');
287
+ const padded = clipLine(plain, innerWidth);
288
+ return `${applyAnsi('│ ', color)}${line}${' '.repeat(Math.max(0, innerWidth - plain.length))}${applyAnsi(' │', color)}`;
289
+ }),
290
+ applyAnsi(clipLine(bottomPlain, width), color),
291
+ ];
292
+ }
293
+ export function formatWatchHeaderLine(record, width) {
294
+ const source = record.resolvedCredentialSource ?? record.credentialSource;
295
+ return clipLine(` LinX watch | ${record.backend} | ${record.status} | mode=${record.mode} | runtime=${record.runtime} | source=${source} | ${shortSessionId(record.id)} `, width);
296
+ }
297
+ export function formatWatchStatusLine(state, width, now = Date.now()) {
298
+ const elapsedMs = Math.max(0, now - state.since);
299
+ const parts = [
300
+ `${statusGlyph(state.phase, elapsedMs)} ${statusLabel(state.phase)} (${formatWatchElapsed(elapsedMs)})`,
301
+ compactStatusDetail(state.detail),
302
+ statusHint(state.phase),
303
+ ].filter((part) => Boolean(part));
304
+ return clipLine(` ${parts.join(' | ')} `, width);
305
+ }
306
+ export function formatWatchQueueLine(queueState, width) {
307
+ const parts = [];
308
+ if (queueState.steeringCount > 0) {
309
+ parts.push(`steer ${queueState.steeringCount}`);
310
+ }
311
+ if (queueState.followUpCount > 0) {
312
+ parts.push(`follow-up ${queueState.followUpCount}`);
313
+ }
314
+ if (parts.length === 0) {
315
+ return clipLine(' Queue empty ', width);
316
+ }
317
+ return clipLine(` Queued | ${parts.join(' | ')} `, width);
318
+ }
319
+ export function formatWatchFooterContext(record) {
320
+ const source = record.resolvedCredentialSource ?? record.credentialSource;
321
+ return [
322
+ shortPath(record.cwd),
323
+ `session=${shortSessionId(record.id)}`,
324
+ record.model ? `model=${shortSessionId(record.model)}` : null,
325
+ `source=${source}`,
326
+ ]
327
+ .filter((part) => Boolean(part))
328
+ .join(' | ');
329
+ }
330
+ export function formatWatchFooterLine(input) {
331
+ const hints = footerHintForPhase(input.phase);
332
+ return renderFooterLine({
333
+ mode: input.hasDraft ? 'ComposerHasDraft' : 'ComposerEmpty',
334
+ emptyHint: hints.emptyHint,
335
+ draftHint: hints.draftHint,
336
+ context: formatWatchFooterContext(input.record),
337
+ }, input.width);
338
+ }
339
+ export function formatWatchActivityPanelLines(input) {
340
+ if (input.maxHeight <= 2) {
341
+ return [];
342
+ }
343
+ const visibleEntries = input.hideToolOutput
344
+ ? input.entries.filter((entry) => entry.kind !== 'tool')
345
+ : input.entries;
346
+ const innerWidth = Math.max(1, input.width - 4);
347
+ const toolEntries = visibleEntries.filter((entry) => entry.kind === 'tool');
348
+ const debugEntries = visibleEntries.filter((entry) => entry.kind === 'debug');
349
+ const statusEntries = visibleEntries.filter((entry) => entry.kind !== 'tool' && entry.kind !== 'debug');
350
+ const groups = [];
351
+ const pushGroup = (label, lines) => {
352
+ if (lines.length === 0) {
353
+ return;
354
+ }
355
+ const group = [
356
+ applyAnsi(clipLine(label, innerWidth), ANSI.bold, ANSI.dim),
357
+ ...lines,
358
+ ];
359
+ groups.push(group);
360
+ };
361
+ const renderToolLines = (entry) => {
362
+ return [applyAnsi(clipLine(`[tool] ${entry.text}`, innerWidth), ANSI.green)];
363
+ };
364
+ const renderStatusLines = (entry) => {
365
+ if (entry.kind === 'success') {
366
+ return [applyAnsi(clipLine(`[session] ${entry.text}`, innerWidth), ANSI.green)];
367
+ }
368
+ if (entry.kind === 'error') {
369
+ return wrapText(`[error] ${entry.text}`, innerWidth).map((line) => applyAnsi(clipLine(line, innerWidth), ANSI.red));
370
+ }
371
+ const label = /approval/i.test(entry.text)
372
+ ? '[approval]'
373
+ : /input/i.test(entry.text)
374
+ ? '[input]'
375
+ : '[note]';
376
+ return wrapText(`${label} ${entry.text}`, innerWidth).map((line) => applyAnsi(clipLine(line, innerWidth), ANSI.dim));
377
+ };
378
+ const renderDebugLines = (entry) => {
379
+ if (entry.kind !== 'debug') {
380
+ return [];
381
+ }
382
+ const debugLines = wrapText(`[debug] ${entry.text}`, innerWidth);
383
+ if (entry.detail) {
384
+ debugLines.push(...wrapText(` ${entry.detail}`, innerWidth));
385
+ }
386
+ return debugLines.map((line) => applyAnsi(clipLine(line, innerWidth), ANSI.dim));
387
+ };
388
+ pushGroup('status', statusEntries.slice(-2).flatMap((entry) => renderStatusLines(entry)));
389
+ pushGroup('tools', toolEntries.slice(-2).flatMap((entry) => renderToolLines(entry)));
390
+ pushGroup('debug', debugEntries.slice(-1).flatMap((entry) => renderDebugLines(entry)));
391
+ const maxContentLines = Math.max(0, input.maxHeight - 2);
392
+ const selectedGroups = [];
393
+ let remaining = maxContentLines;
394
+ for (let index = 0; index < groups.length; index += 1) {
395
+ const group = groups[index] ?? [];
396
+ const separatorCost = selectedGroups.length > 0 ? 1 : 0;
397
+ const required = group.length + separatorCost;
398
+ if (required > remaining) {
399
+ continue;
400
+ }
401
+ selectedGroups.push(group);
402
+ remaining -= required;
403
+ }
404
+ const contentLines = selectedGroups.flatMap((group, index) => {
405
+ const result = [];
406
+ if (index > 0) {
407
+ result.push(applyAnsi(clipLine('·'.repeat(Math.min(innerWidth, 12)), innerWidth), ANSI.dim));
408
+ }
409
+ result.push(...group);
410
+ return result;
411
+ });
412
+ if (contentLines.length === 0) {
413
+ return [];
414
+ }
415
+ const title = input.debugMode ? 'activity | debug' : 'activity';
416
+ return renderBorderBox(contentLines, input.width, title, ANSI.dim);
417
+ }
418
+ export function selectWatchFooterSectionCounts(input) {
419
+ let remaining = Math.max(0, input.totalHeight - input.headerCount - input.promptCount - 1);
420
+ const statusCount = input.showStatus && remaining > 0 ? 1 : 0;
421
+ remaining -= statusCount;
422
+ const queueCount = Math.min(input.queueCount, remaining);
423
+ remaining -= queueCount;
424
+ const contextCount = Math.min(input.contextCount, remaining);
425
+ return {
426
+ contextCount,
427
+ statusCount,
428
+ queueCount,
429
+ };
430
+ }
431
+ export function formatWatchTranscriptLine(line, width) {
432
+ if (line.startsWith('you> ')) {
433
+ return wrapPrefixedLine('you ', line.slice(5), width, ANSI.cyan);
434
+ }
435
+ if (line.startsWith('linx> ')) {
436
+ return wrapPrefixedLine('linx ', line.slice(6), width, ANSI.magenta);
437
+ }
438
+ if (line.startsWith('assistant> ')) {
439
+ return wrapPrefixedLine('linx ', line.slice(11), width, ANSI.magenta);
440
+ }
441
+ if (line.startsWith('[tool] ')) {
442
+ return [applyAnsi(clipLine(line, width), ANSI.green)];
443
+ }
444
+ if (line.startsWith('[approval] ') || line.startsWith('[input] ')) {
445
+ return [applyAnsi(clipLine(line, width), ANSI.cyan)];
446
+ }
447
+ if (line.startsWith('[note] ')) {
448
+ return [applyAnsi(clipLine(line, width), ANSI.dim)];
449
+ }
450
+ if (line.startsWith('[session] completed')) {
451
+ return [applyAnsi(clipLine(line, width), ANSI.green)];
452
+ }
453
+ if (line.startsWith('[session] failed') || line.startsWith('stderr> ') || line.startsWith('[error] ')) {
454
+ return [applyAnsi(clipLine(line, width), ANSI.red)];
455
+ }
456
+ return wrapText(line, width).map((entry) => clipLine(entry, width));
457
+ }
458
+ function wrapPrefixedLine(prefix, content, width, color) {
459
+ const safeWidth = Math.max(width, prefix.length + 1);
460
+ const firstWidth = Math.max(1, safeWidth - prefix.length);
461
+ const continuationPrefix = ' '.repeat(prefix.length);
462
+ const plainLines = wrapText(content || ' ', firstWidth);
463
+ return plainLines.map((entry, index) => {
464
+ const prefixText = index === 0 ? prefix : continuationPrefix;
465
+ const padded = clipLine(`${prefixText}${entry}`, safeWidth);
466
+ const visiblePrefix = index === 0 ? applyAnsi(prefix, ANSI.bold, color) : continuationPrefix;
467
+ return `${visiblePrefix}${padded.slice(prefixText.length)}`;
468
+ });
469
+ }
470
+ function styleStatusLine(line) {
471
+ return applyAnsi(line, ANSI.cyan);
472
+ }
473
+ function styleContextLine(line, width) {
474
+ if (/^\s+\d+\./.test(line)) {
475
+ return applyAnsi(clipLine(line, width), ANSI.dim);
476
+ }
477
+ return applyAnsi(clipLine(line, width), ANSI.cyan);
478
+ }
479
+ function styleComposerLine(line, width) {
480
+ const raw = clipLine(`${line.prefix}${line.text}`, width);
481
+ const prefix = applyAnsi(line.prefix, ANSI.cyan, ANSI.bold);
482
+ const value = raw.slice(line.prefix.length);
483
+ if (line.isPlaceholder) {
484
+ return `${prefix}${applyAnsi(value, ANSI.dim)}`;
485
+ }
486
+ return `${prefix}${value}`;
487
+ }
488
+ function centerLine(text, width) {
489
+ const visible = text.length;
490
+ if (visible >= width) {
491
+ return clipLine(text, width);
492
+ }
493
+ const leftPadding = Math.max(0, Math.floor((width - visible) / 2));
494
+ return `${' '.repeat(leftPadding)}${text}`.padEnd(width, ' ');
495
+ }
496
+ function styleOverlayLine(line) {
497
+ const leftPadding = line.match(/^\s*/u)?.[0] ?? '';
498
+ const content = line.slice(leftPadding.length);
499
+ if (content.startsWith('┌') || content.startsWith('└')) {
500
+ return `${leftPadding}${applyAnsi(content, ANSI.dim)}`;
501
+ }
502
+ if (content.includes('› ')) {
503
+ return `${leftPadding}${content.replace('› ', applyAnsi('› ', ANSI.cyan, ANSI.bold))}`;
504
+ }
505
+ if (content.includes('> ')) {
506
+ return `${leftPadding}${content.replace('> ', applyAnsi('> ', ANSI.cyan, ANSI.bold))}`;
507
+ }
508
+ return line;
509
+ }
510
+ function withRequestInputComposer(state, edit) {
511
+ const composer = new CodexComposer();
512
+ const answerValue = state.answerValue ?? '';
513
+ composer.beginPrompt('answer> ');
514
+ composer.setText(answerValue, state.answerCursor ?? answerValue.length);
515
+ edit(composer);
516
+ state.answerValue = composer.text();
517
+ state.answerCursor = composer.cursorIndex();
518
+ }
519
+ class PlainWatchDisplay {
520
+ renderState = { hasAssistantOutput: false };
521
+ record;
522
+ prompt;
523
+ queueState = { steeringCount: 0, followUpCount: 0 };
524
+ constructor(record, prompt) {
525
+ this.record = record;
526
+ this.prompt = prompt;
527
+ }
528
+ start() {
529
+ stdout.write(`LinX watch\nsession: ${this.record.id}\nbackend: ${this.record.backend}\nruntime: ${this.record.runtime}\nmode: ${this.record.mode}\ncmd: ${this.record.command} ${this.record.args.join(' ')}\n\n`);
530
+ }
531
+ updateRecord(record) {
532
+ this.record = record;
533
+ }
534
+ updateQueue(state) {
535
+ this.queueState = state;
536
+ }
537
+ bindInputController() { }
538
+ setPhase() { }
539
+ showActivity(text, tone = 'note') {
540
+ const prefix = tone === 'success'
541
+ ? '[ok]'
542
+ : tone === 'error'
543
+ ? '[error]'
544
+ : tone === 'debug'
545
+ ? '[debug]'
546
+ : '[note]';
547
+ stdout.write(`${prefix} ${text}\n`);
548
+ }
549
+ setDebugMode(enabled) {
550
+ this.showActivity(`Debug protocol view ${enabled ? 'enabled' : 'disabled'}`);
551
+ }
552
+ async chooseOption(title, lines, options, signal) {
553
+ stdout.write(`${title}\n`);
554
+ for (const line of lines) {
555
+ stdout.write(`${line}\n`);
556
+ }
557
+ options.forEach((option, index) => {
558
+ const suffix = option.description ? ` - ${option.description}` : '';
559
+ stdout.write(` ${index + 1}. ${option.label}${suffix}\n`);
560
+ });
561
+ const raw = (await this.prompt('select> ', signal)).trim();
562
+ if (/^\d+$/u.test(raw)) {
563
+ const option = options[Number(raw) - 1];
564
+ if (option) {
565
+ return option.value;
566
+ }
567
+ }
568
+ return raw;
569
+ }
570
+ async chooseQuestion(state) {
571
+ stdout.write(`${state.header}\n`);
572
+ stdout.write(`Question ${state.questionIndex + 1}/${state.questionCount}\n`);
573
+ stdout.write(`${state.question}\n`);
574
+ state.options.forEach((option, index) => {
575
+ const suffix = option.description ? ` - ${option.description}` : '';
576
+ stdout.write(` ${index + 1}. ${option.label}${suffix}\n`);
577
+ });
578
+ const raw = (await this.prompt(state.options.length > 0 ? 'select> ' : 'answer> ')).trim();
579
+ if (/^\d+$/u.test(raw)) {
580
+ const option = state.options[Number(raw) - 1];
581
+ if (option) {
582
+ return option.value;
583
+ }
584
+ }
585
+ return raw;
586
+ }
587
+ async chooseQuestions(questions) {
588
+ const answers = {};
589
+ for (const [index, question] of questions.entries()) {
590
+ const raw = await this.chooseQuestion({
591
+ header: question.header,
592
+ question: question.question,
593
+ options: question.options.map((option, optionIndex) => ({
594
+ label: option.label,
595
+ value: option.label,
596
+ description: option.description,
597
+ shortcuts: [`${optionIndex + 1}`],
598
+ })),
599
+ questionIndex: index,
600
+ questionCount: questions.length,
601
+ unansweredCount: Math.max(1, questions.length - index),
602
+ });
603
+ answers[question.id] = {
604
+ answers: raw.trim() ? [raw.trim()] : [],
605
+ };
606
+ }
607
+ return answers;
608
+ }
609
+ showUserTurn(text) {
610
+ stdout.write(`you> ${text}\n`);
611
+ }
612
+ showHelp() {
613
+ stdout.write('/help 查看帮助\n/exit 退出当前 watch 会话\n\n');
614
+ }
615
+ showQuestion(lines) {
616
+ for (const line of lines) {
617
+ stdout.write(`${line}\n`);
618
+ }
619
+ }
620
+ flushAssistantLine() {
621
+ if (this.renderState.hasAssistantOutput) {
622
+ stdout.write('\n');
623
+ this.renderState.hasAssistantOutput = false;
624
+ }
625
+ }
626
+ renderEvents(events) {
627
+ for (const event of events) {
628
+ if (event.type === 'assistant.delta') {
629
+ stdout.write(event.text);
630
+ this.renderState.hasAssistantOutput = true;
631
+ continue;
632
+ }
633
+ if (event.type === 'assistant.done') {
634
+ if (event.text && !this.renderState.hasAssistantOutput) {
635
+ stdout.write(`${event.text}\n`);
636
+ }
637
+ else {
638
+ this.flushAssistantLine();
639
+ }
640
+ continue;
641
+ }
642
+ this.flushAssistantLine();
643
+ if (event.type === 'tool.call') {
644
+ stdout.write(`[tool] ${summarizeWatchToolCall(event.name, event.arguments)}\n`);
645
+ continue;
646
+ }
647
+ if (event.type === 'approval.required') {
648
+ stdout.write(`[approval] ${event.message}\n`);
649
+ continue;
650
+ }
651
+ if (event.type === 'input.required') {
652
+ stdout.write(`[input] ${event.message}\n`);
653
+ continue;
654
+ }
655
+ this.showActivity(event.message);
656
+ }
657
+ }
658
+ renderRawLine(stream, line) {
659
+ this.flushAssistantLine();
660
+ if (!line.trim()) {
661
+ return;
662
+ }
663
+ const target = stream === 'stderr' ? process.stderr : stdout;
664
+ target.write(`${line}\n`);
665
+ }
666
+ async promptInput(prompt) {
667
+ return {
668
+ text: await this.prompt(prompt),
669
+ mode: 'send',
670
+ };
671
+ }
672
+ finish(status, record, error) {
673
+ this.updateRecord(record);
674
+ this.flushAssistantLine();
675
+ if (status === 'completed') {
676
+ stdout.write(`\n[session] completed ${record.id}\n`);
677
+ }
678
+ else {
679
+ process.stderr.write(`\n[session] failed ${record.id}${error ? `: ${error}` : ''}\n`);
680
+ }
681
+ }
682
+ }
683
+ class TuiWatchDisplay {
684
+ record;
685
+ promptFallback;
686
+ transcript = [];
687
+ activityEntries = [];
688
+ contextLines = [];
689
+ assistantLine = '';
690
+ queueState = { steeringCount: 0, followUpCount: 0 };
691
+ active = false;
692
+ originalRawMode = false;
693
+ renderTicker = null;
694
+ composer = new CodexComposer();
695
+ promptResolver = null;
696
+ requestFormResolver = null;
697
+ overlay = null;
698
+ overlayResolver = null;
699
+ overlayRejecter = null;
700
+ inputController = null;
701
+ hideToolOutput = false;
702
+ debugMode = false;
703
+ lastCtrlCTime = 0;
704
+ state = {
705
+ phase: 'starting',
706
+ detail: 'Booting watch session',
707
+ since: Date.now(),
708
+ };
709
+ constructor(record, promptFallback) {
710
+ this.record = record;
711
+ this.promptFallback = promptFallback;
712
+ }
713
+ start() {
714
+ if (this.active) {
715
+ return;
716
+ }
717
+ this.active = true;
718
+ this.pushActivityEntry({ kind: 'note', text: 'LinX watch ready' });
719
+ this.pushActivityEntry({ kind: 'note', text: 'Use /help for commands. Type /exit to leave this session.' });
720
+ emitKeypressEvents(stdin);
721
+ if ('setRawMode' in stdin && typeof stdin.setRawMode === 'function') {
722
+ this.originalRawMode = Boolean(stdin.isRaw);
723
+ stdin.setRawMode(true);
724
+ }
725
+ stdin.resume();
726
+ stdin.on('keypress', this.handleKeypress);
727
+ stdout.on('resize', this.handleResize);
728
+ this.renderTicker = setInterval(() => {
729
+ this.render();
730
+ }, 125);
731
+ stdout.write('\x1b[?1049h\x1b[2J\x1b[H\x1b[?25l');
732
+ this.render();
733
+ }
734
+ updateRecord(record) {
735
+ this.record = record;
736
+ this.render();
737
+ }
738
+ updateQueue(state) {
739
+ this.queueState = state;
740
+ this.render();
741
+ }
742
+ bindInputController(controller) {
743
+ this.inputController = controller;
744
+ }
745
+ setPhase(phase, detail) {
746
+ if (this.state.phase === phase && this.state.detail === detail) {
747
+ this.render();
748
+ return;
749
+ }
750
+ this.state = {
751
+ phase,
752
+ detail,
753
+ since: Date.now(),
754
+ };
755
+ this.render();
756
+ }
757
+ showActivity(text, tone = 'note') {
758
+ this.pushActivityEntry({
759
+ kind: tone,
760
+ text,
761
+ });
762
+ this.render();
763
+ }
764
+ setDebugMode(enabled) {
765
+ this.debugMode = enabled;
766
+ this.pushActivityEntry({
767
+ kind: 'note',
768
+ text: `Debug protocol view ${enabled ? 'enabled' : 'disabled'}`,
769
+ });
770
+ this.render();
771
+ }
772
+ chooseOption(title, lines, options, signal) {
773
+ if (!this.active) {
774
+ return this.promptFallback('select> ', signal);
775
+ }
776
+ this.flushAssistant();
777
+ this.contextLines = [];
778
+ this.overlay = {
779
+ kind: 'selection',
780
+ value: {
781
+ title,
782
+ body: lines,
783
+ options,
784
+ selectedIndex: 0,
785
+ },
786
+ };
787
+ this.render();
788
+ return new Promise((resolve, reject) => {
789
+ this.overlayResolver = resolve;
790
+ this.overlayRejecter = reject;
791
+ if (signal) {
792
+ const onAbort = () => {
793
+ if (this.overlayResolver !== resolve) {
794
+ return;
795
+ }
796
+ this.overlayResolver = null;
797
+ this.overlayRejecter = null;
798
+ this.overlay = null;
799
+ this.render();
800
+ reject(createAbortError());
801
+ };
802
+ if (signal.aborted) {
803
+ onAbort();
804
+ return;
805
+ }
806
+ signal.addEventListener('abort', onAbort, { once: true });
807
+ }
808
+ });
809
+ }
810
+ chooseQuestion(state) {
811
+ if (!this.active) {
812
+ return this.promptFallback('select> ');
813
+ }
814
+ this.flushAssistant();
815
+ this.contextLines = [];
816
+ this.overlay = {
817
+ kind: 'request-input',
818
+ value: {
819
+ ...state,
820
+ selectedIndex: 0,
821
+ answerValue: '',
822
+ answerCursor: 0,
823
+ },
824
+ };
825
+ this.render();
826
+ return new Promise((resolve) => {
827
+ this.overlayResolver = resolve;
828
+ this.overlayRejecter = null;
829
+ });
830
+ }
831
+ chooseQuestions(questions) {
832
+ if (!this.active) {
833
+ const plain = new PlainWatchDisplay(this.record, this.promptFallback);
834
+ return plain.chooseQuestions(questions);
835
+ }
836
+ this.flushAssistant();
837
+ this.contextLines = [];
838
+ this.overlay = {
839
+ kind: 'request-form',
840
+ value: new CodexRequestForm(questions),
841
+ };
842
+ this.render();
843
+ return new Promise((resolve) => {
844
+ this.requestFormResolver = resolve;
845
+ });
846
+ }
847
+ showUserTurn(text) {
848
+ this.flushAssistant();
849
+ this.composer.recordSubmission(text);
850
+ this.pushTranscriptEntry({ kind: 'user', text });
851
+ this.render();
852
+ }
853
+ showHelp() {
854
+ this.showActivity('Commands: /help, /exit');
855
+ }
856
+ showQuestion(lines) {
857
+ this.flushAssistant();
858
+ this.contextLines = lines.filter((line) => line.trim().length > 0);
859
+ this.render();
860
+ }
861
+ renderEvents(events) {
862
+ for (const event of events) {
863
+ if (event.type === 'assistant.delta') {
864
+ this.assistantLine += event.text;
865
+ continue;
866
+ }
867
+ if (event.type === 'assistant.done') {
868
+ if (event.text && !this.assistantLine) {
869
+ this.pushTranscriptEntry({ kind: 'assistant', text: event.text });
870
+ }
871
+ else {
872
+ this.flushAssistant();
873
+ }
874
+ continue;
875
+ }
876
+ this.flushAssistant();
877
+ if (event.type === 'tool.call') {
878
+ this.pushActivityEntry({
879
+ kind: 'tool',
880
+ text: summarizeWatchToolCall(event.name, event.arguments),
881
+ });
882
+ if (this.debugMode && event.raw) {
883
+ const debugPayload = summarizeWatchDebugPayload(event.raw);
884
+ this.pushActivityEntry({
885
+ kind: 'debug',
886
+ text: debugPayload.text,
887
+ ...(debugPayload.detail ? { detail: debugPayload.detail } : {}),
888
+ });
889
+ }
890
+ continue;
891
+ }
892
+ if (event.type === 'approval.required') {
893
+ continue;
894
+ }
895
+ if (event.type === 'input.required') {
896
+ continue;
897
+ }
898
+ this.pushActivityEntry({
899
+ kind: 'note',
900
+ text: event.message,
901
+ });
902
+ if (this.debugMode && event.raw) {
903
+ const debugPayload = summarizeWatchDebugPayload(event.raw);
904
+ this.pushActivityEntry({
905
+ kind: 'debug',
906
+ text: debugPayload.text,
907
+ ...(debugPayload.detail ? { detail: debugPayload.detail } : {}),
908
+ });
909
+ }
910
+ }
911
+ this.render();
912
+ }
913
+ renderRawLine(stream, line) {
914
+ this.flushAssistant();
915
+ const trimmed = line.trim();
916
+ if (trimmed) {
917
+ if (stream === 'stdout' && !this.debugMode) {
918
+ this.render();
919
+ return;
920
+ }
921
+ const debugPayload = stream === 'stdout' && this.debugMode
922
+ ? summarizeWatchDebugPayload(trimmed)
923
+ : null;
924
+ this.pushActivityEntry({
925
+ kind: stream === 'stderr' ? 'error' : this.debugMode ? 'debug' : 'note',
926
+ text: stream === 'stderr'
927
+ ? trimmed
928
+ : debugPayload?.text ?? trimmed,
929
+ ...(debugPayload?.detail ? { detail: debugPayload.detail } : {}),
930
+ });
931
+ }
932
+ this.render();
933
+ }
934
+ promptInput(prompt) {
935
+ if (!this.active) {
936
+ return this.promptFallback(prompt).then((text) => ({ text, mode: 'send' }));
937
+ }
938
+ this.composer.beginPrompt(prompt);
939
+ this.overlay = null;
940
+ this.render();
941
+ return new Promise((resolve) => {
942
+ this.promptResolver = resolve;
943
+ });
944
+ }
945
+ finish(status, record, error) {
946
+ this.updateRecord(record);
947
+ this.flushAssistant();
948
+ if (status === 'completed') {
949
+ this.pushActivityEntry({ kind: 'success', text: `Session completed | ${record.id}` });
950
+ }
951
+ else {
952
+ this.pushActivityEntry({ kind: 'error', text: `Session failed | ${record.id}${error ? `: ${error}` : ''}` });
953
+ }
954
+ this.render();
955
+ this.teardown();
956
+ if (status === 'completed') {
957
+ stdout.write(`[session] completed ${record.id}\n`);
958
+ }
959
+ else {
960
+ process.stderr.write(`[session] failed ${record.id}${error ? `: ${error}` : ''}\n`);
961
+ }
962
+ }
963
+ handleResize = () => {
964
+ this.render();
965
+ };
966
+ handleCtrlC() {
967
+ const now = Date.now();
968
+ const hasDraft = this.composer.text().length > 0;
969
+ if (hasDraft) {
970
+ this.composer.setText('');
971
+ this.contextLines = [];
972
+ this.lastCtrlCTime = now;
973
+ this.render();
974
+ return;
975
+ }
976
+ if (now - this.lastCtrlCTime <= 800) {
977
+ this.teardown();
978
+ process.exit(1);
979
+ return;
980
+ }
981
+ this.lastCtrlCTime = now;
982
+ this.pushActivityEntry({ kind: 'note', text: 'Press Ctrl+C again to quit' });
983
+ this.render();
984
+ }
985
+ restoreQueuedSubmission() {
986
+ const restored = this.inputController?.restoreQueuedSubmission() ?? null;
987
+ if (!restored) {
988
+ this.pushActivityEntry({ kind: 'note', text: 'Queue is empty' });
989
+ this.render();
990
+ return;
991
+ }
992
+ this.composer.setText(restored.text);
993
+ this.contextLines = [
994
+ restored.mode === 'follow-up'
995
+ ? 'Restored queued follow-up message'
996
+ : 'Restored queued steering message',
997
+ ];
998
+ this.pushActivityEntry({
999
+ kind: 'note',
1000
+ text: restored.mode === 'follow-up'
1001
+ ? 'Restored queued follow-up message'
1002
+ : 'Restored queued steering message',
1003
+ });
1004
+ this.render();
1005
+ }
1006
+ toggleToolOutput() {
1007
+ this.hideToolOutput = !this.hideToolOutput;
1008
+ this.pushActivityEntry({
1009
+ kind: 'note',
1010
+ text: this.hideToolOutput ? 'Tool output collapsed' : 'Tool output expanded',
1011
+ });
1012
+ this.render();
1013
+ }
1014
+ handleKeypress = (value, key) => {
1015
+ if (this.requestFormResolver && this.overlay?.kind === 'request-form') {
1016
+ if (key.ctrl && key.name === 'c') {
1017
+ this.teardown();
1018
+ process.exit(1);
1019
+ }
1020
+ const result = this.overlay.value.applyKey(value, key);
1021
+ if (result.kind === 'submitted') {
1022
+ const resolve = this.requestFormResolver;
1023
+ this.requestFormResolver = null;
1024
+ this.overlay = null;
1025
+ this.render();
1026
+ resolve(result.answers);
1027
+ return;
1028
+ }
1029
+ this.render();
1030
+ return;
1031
+ }
1032
+ if (this.overlayResolver && this.overlay) {
1033
+ if (key.ctrl && key.name === 'c') {
1034
+ this.teardown();
1035
+ process.exit(1);
1036
+ }
1037
+ if (this.overlay.kind === 'request-input' && this.overlay.value.options.length === 0) {
1038
+ if (key.name === 'return' || key.name === 'enter') {
1039
+ const resolve = this.overlayResolver;
1040
+ const answer = this.overlay.value.answerValue ?? '';
1041
+ this.overlayResolver = null;
1042
+ this.overlayRejecter = null;
1043
+ this.overlay = null;
1044
+ this.render();
1045
+ resolve(answer);
1046
+ return;
1047
+ }
1048
+ if (key.name === 'backspace') {
1049
+ withRequestInputComposer(this.overlay.value, (composer) => composer.backspace());
1050
+ this.render();
1051
+ return;
1052
+ }
1053
+ if (key.name === 'delete') {
1054
+ withRequestInputComposer(this.overlay.value, (composer) => composer.deleteForward());
1055
+ this.render();
1056
+ return;
1057
+ }
1058
+ if (key.name === 'left' || (key.ctrl && key.name === 'b')) {
1059
+ withRequestInputComposer(this.overlay.value, (composer) => composer.moveLeft());
1060
+ this.render();
1061
+ return;
1062
+ }
1063
+ if (key.name === 'right' || (key.ctrl && key.name === 'f')) {
1064
+ withRequestInputComposer(this.overlay.value, (composer) => composer.moveRight());
1065
+ this.render();
1066
+ return;
1067
+ }
1068
+ if (key.name === 'home' || (key.ctrl && key.name === 'a')) {
1069
+ withRequestInputComposer(this.overlay.value, (composer) => composer.moveToStart());
1070
+ this.render();
1071
+ return;
1072
+ }
1073
+ if (key.name === 'end' || (key.ctrl && key.name === 'e')) {
1074
+ withRequestInputComposer(this.overlay.value, (composer) => composer.moveToEnd());
1075
+ this.render();
1076
+ return;
1077
+ }
1078
+ if (key.ctrl && key.name === 'u') {
1079
+ withRequestInputComposer(this.overlay.value, (composer) => composer.deleteToStart());
1080
+ this.render();
1081
+ return;
1082
+ }
1083
+ if (key.ctrl && key.name === 'k') {
1084
+ withRequestInputComposer(this.overlay.value, (composer) => composer.deleteToEnd());
1085
+ this.render();
1086
+ return;
1087
+ }
1088
+ if (key.name === 'escape' || key.name === 'tab' || key.name === 'up' || key.name === 'down') {
1089
+ return;
1090
+ }
1091
+ if (typeof value === 'string' && value.length > 0 && !key.ctrl && !key.meta) {
1092
+ withRequestInputComposer(this.overlay.value, (composer) => composer.insert(value));
1093
+ this.render();
1094
+ }
1095
+ return;
1096
+ }
1097
+ const currentOverlay = this.overlay;
1098
+ if (currentOverlay.kind === 'request-form') {
1099
+ return;
1100
+ }
1101
+ const shortcut = typeof value === 'string' ? value.toLowerCase() : '';
1102
+ const overlayOptions = currentOverlay.value.options;
1103
+ const matchedIndex = shortcut
1104
+ ? overlayOptions.findIndex((option) => option.shortcuts?.includes(shortcut))
1105
+ : -1;
1106
+ if (key.name === 'escape' && currentOverlay.kind === 'selection') {
1107
+ const cancelIndex = overlayOptions.findIndex((option) => (option.value === 'c'
1108
+ || option.shortcuts?.includes('c')
1109
+ || /cancel/i.test(option.label)));
1110
+ if (cancelIndex !== -1) {
1111
+ const resolve = this.overlayResolver;
1112
+ const matched = overlayOptions[cancelIndex];
1113
+ this.overlayResolver = null;
1114
+ this.overlayRejecter = null;
1115
+ this.overlay = null;
1116
+ this.render();
1117
+ resolve(matched?.value ?? '');
1118
+ return;
1119
+ }
1120
+ }
1121
+ if (matchedIndex !== -1) {
1122
+ const resolve = this.overlayResolver;
1123
+ const matched = overlayOptions[matchedIndex];
1124
+ this.overlayResolver = null;
1125
+ this.overlayRejecter = null;
1126
+ this.overlay = null;
1127
+ this.render();
1128
+ resolve(matched.value);
1129
+ return;
1130
+ }
1131
+ if (key.name === 'up' || key.name === 'k') {
1132
+ currentOverlay.value.selectedIndex = Math.max(0, currentOverlay.value.selectedIndex - 1);
1133
+ this.render();
1134
+ return;
1135
+ }
1136
+ if (key.name === 'down' || key.name === 'j' || key.name === 'tab') {
1137
+ currentOverlay.value.selectedIndex = Math.min(overlayOptions.length - 1, currentOverlay.value.selectedIndex + 1);
1138
+ this.render();
1139
+ return;
1140
+ }
1141
+ if (key.name === 'return' || key.name === 'enter') {
1142
+ const resolve = this.overlayResolver;
1143
+ const selected = overlayOptions[currentOverlay.value.selectedIndex];
1144
+ this.overlayResolver = null;
1145
+ this.overlayRejecter = null;
1146
+ this.overlay = null;
1147
+ this.render();
1148
+ resolve(selected?.value ?? '');
1149
+ return;
1150
+ }
1151
+ return;
1152
+ }
1153
+ if (!this.promptResolver) {
1154
+ if (key.ctrl && key.name === 'c') {
1155
+ this.handleCtrlC();
1156
+ }
1157
+ return;
1158
+ }
1159
+ if (key.ctrl && key.name === 'c') {
1160
+ this.handleCtrlC();
1161
+ return;
1162
+ }
1163
+ if (key.ctrl && key.name === 'o') {
1164
+ this.toggleToolOutput();
1165
+ return;
1166
+ }
1167
+ if (key.meta && key.name === 'up') {
1168
+ this.restoreQueuedSubmission();
1169
+ return;
1170
+ }
1171
+ if ((key.name === 'return' || key.name === 'enter') && key.shift) {
1172
+ this.composer.insert('\n');
1173
+ this.render();
1174
+ return;
1175
+ }
1176
+ if (key.name === 'return' || key.name === 'enter') {
1177
+ const resolve = this.promptResolver;
1178
+ const text = this.composer.text();
1179
+ const mode = key.meta ? 'follow-up' : 'send';
1180
+ this.promptResolver = null;
1181
+ this.composer.setText('');
1182
+ this.contextLines = [];
1183
+ this.render();
1184
+ resolve({ text, mode });
1185
+ return;
1186
+ }
1187
+ if (key.name === 'backspace') {
1188
+ this.composer.backspace();
1189
+ this.render();
1190
+ return;
1191
+ }
1192
+ if (key.name === 'delete') {
1193
+ this.composer.deleteForward();
1194
+ this.render();
1195
+ return;
1196
+ }
1197
+ if (key.name === 'left' || (key.ctrl && key.name === 'b')) {
1198
+ this.composer.moveLeft();
1199
+ this.render();
1200
+ return;
1201
+ }
1202
+ if (key.name === 'right' || (key.ctrl && key.name === 'f')) {
1203
+ this.composer.moveRight();
1204
+ this.render();
1205
+ return;
1206
+ }
1207
+ if (key.name === 'home' || (key.ctrl && key.name === 'a')) {
1208
+ this.composer.moveToStart();
1209
+ this.render();
1210
+ return;
1211
+ }
1212
+ if (key.name === 'end' || (key.ctrl && key.name === 'e')) {
1213
+ this.composer.moveToEnd();
1214
+ this.render();
1215
+ return;
1216
+ }
1217
+ if (key.name === 'up' || (key.ctrl && key.name === 'p')) {
1218
+ if (this.composer.navigateHistory('up')) {
1219
+ this.render();
1220
+ }
1221
+ return;
1222
+ }
1223
+ if (key.name === 'down' || (key.ctrl && key.name === 'n')) {
1224
+ if (this.composer.navigateHistory('down')) {
1225
+ this.render();
1226
+ }
1227
+ return;
1228
+ }
1229
+ if (key.ctrl && key.name === 'u') {
1230
+ this.composer.deleteToStart();
1231
+ this.render();
1232
+ return;
1233
+ }
1234
+ if (key.ctrl && key.name === 'k') {
1235
+ this.composer.deleteToEnd();
1236
+ this.render();
1237
+ return;
1238
+ }
1239
+ if (key.name === 'escape' || key.name === 'tab') {
1240
+ return;
1241
+ }
1242
+ if (typeof value === 'string' && value.length > 0 && !key.ctrl && !key.meta) {
1243
+ this.composer.insert(value);
1244
+ this.render();
1245
+ }
1246
+ };
1247
+ flushAssistant() {
1248
+ if (!this.assistantLine.trim()) {
1249
+ this.assistantLine = '';
1250
+ return;
1251
+ }
1252
+ this.pushTranscriptEntry({ kind: 'assistant', text: this.assistantLine });
1253
+ this.assistantLine = '';
1254
+ }
1255
+ pushTranscriptEntry(entry) {
1256
+ if (!entry.text.trim()) {
1257
+ return;
1258
+ }
1259
+ const nextEntry = {
1260
+ ...entry,
1261
+ text: entry.text.trimEnd(),
1262
+ };
1263
+ this.transcript.push(nextEntry);
1264
+ if (this.transcript.length > 400) {
1265
+ this.transcript.splice(0, this.transcript.length - 400);
1266
+ }
1267
+ }
1268
+ pushActivityEntry(entry) {
1269
+ if (!entry.text.trim()) {
1270
+ return;
1271
+ }
1272
+ const nextEntry = {
1273
+ ...entry,
1274
+ text: entry.text.trimEnd(),
1275
+ };
1276
+ this.activityEntries.push(nextEntry);
1277
+ if (this.activityEntries.length > 120) {
1278
+ this.activityEntries.splice(0, this.activityEntries.length - 120);
1279
+ }
1280
+ }
1281
+ buildMainLines(width) {
1282
+ const lines = this.transcript.map((entry) => {
1283
+ if (entry.kind === 'user') {
1284
+ return `you> ${entry.text}`;
1285
+ }
1286
+ return `assistant> ${entry.text}`;
1287
+ });
1288
+ if (this.assistantLine) {
1289
+ lines.push(`linx> ${this.assistantLine}`);
1290
+ }
1291
+ return lines.flatMap((line) => formatWatchTranscriptLine(line, width));
1292
+ }
1293
+ buildActivityLines(width, maxLines) {
1294
+ if (maxLines <= 0) {
1295
+ return [];
1296
+ }
1297
+ const sourceEntries = this.hideToolOutput
1298
+ ? this.activityEntries.filter((entry) => entry.kind !== 'tool')
1299
+ : this.activityEntries;
1300
+ return sourceEntries
1301
+ .flatMap((entry) => {
1302
+ if (entry.kind === 'tool') {
1303
+ return [applyAnsi(clipLine(`[tool] ${entry.text}`, width), ANSI.green)];
1304
+ }
1305
+ if (entry.kind === 'success') {
1306
+ return [applyAnsi(clipLine(`[ok] ${entry.text}`, width), ANSI.green)];
1307
+ }
1308
+ if (entry.kind === 'error') {
1309
+ return wrapText(`[error] ${entry.text}`, width).map((line) => applyAnsi(clipLine(line, width), ANSI.red));
1310
+ }
1311
+ if (entry.kind === 'debug') {
1312
+ const debugLines = wrapText(`[debug] ${entry.text}`, width);
1313
+ if (entry.detail) {
1314
+ debugLines.push(...wrapText(` ${entry.detail}`, width));
1315
+ }
1316
+ return debugLines.map((line) => applyAnsi(clipLine(line, width), ANSI.dim));
1317
+ }
1318
+ return wrapText(`[note] ${entry.text}`, width).map((line) => applyAnsi(clipLine(line, width), ANSI.dim));
1319
+ })
1320
+ .slice(-maxLines);
1321
+ }
1322
+ buildActivityPanel(width, maxHeight) {
1323
+ return formatWatchActivityPanelLines({
1324
+ width,
1325
+ maxHeight,
1326
+ entries: this.activityEntries,
1327
+ hideToolOutput: this.hideToolOutput,
1328
+ debugMode: this.debugMode,
1329
+ });
1330
+ }
1331
+ buildContextLines(width, maxLines) {
1332
+ return this.contextLines
1333
+ .flatMap((line) => wrapText(line, width))
1334
+ .slice(0, Math.max(0, maxLines));
1335
+ }
1336
+ buildHeaderLines(width) {
1337
+ return [
1338
+ styleStatusLine(formatWatchHeaderLine(this.record, width)),
1339
+ applyAnsi(clipLine(' Alt+Up restore | Ctrl+O tools | Ctrl+C clear / double Ctrl+C quit ', width), ANSI.dim),
1340
+ ];
1341
+ }
1342
+ buildQueueLines(width) {
1343
+ if (this.queueState.steeringCount === 0 && this.queueState.followUpCount === 0) {
1344
+ return [];
1345
+ }
1346
+ return [applyAnsi(formatWatchQueueLine(this.queueState, width), ANSI.dim)];
1347
+ }
1348
+ buildFooterLine(width) {
1349
+ return applyAnsi(formatWatchFooterLine({
1350
+ width,
1351
+ phase: this.state.phase,
1352
+ record: this.record,
1353
+ hasDraft: this.composer.hasDraft(),
1354
+ }), ANSI.dim);
1355
+ }
1356
+ buildPromptLines(width) {
1357
+ const innerWidth = Math.max(16, width - 4);
1358
+ const rendered = this.composer.render(innerWidth);
1359
+ const title = this.state.phase === 'running'
1360
+ ? `editor | running ${this.record.backend}`
1361
+ : `editor | ${this.record.backend}`;
1362
+ const framedLines = renderBorderBox(rendered.lines.map((line) => styleComposerLine(line, innerWidth)), width, title, editorBorderColor(this.state.phase));
1363
+ return {
1364
+ lines: framedLines,
1365
+ cursorRow: rendered.cursorRow + 1,
1366
+ cursorCol: rendered.cursorCol + 2,
1367
+ };
1368
+ }
1369
+ buildOverlayLines(width, maxHeight) {
1370
+ if (!this.overlay) {
1371
+ return { lines: [] };
1372
+ }
1373
+ if (this.overlay.kind === 'selection') {
1374
+ const lines = renderCodexOverlay(this.overlay.value, Math.max(24, Math.min(width - 6, 88)), maxHeight);
1375
+ return {
1376
+ lines: lines.map((line) => styleOverlayLine(centerLine(line, width))),
1377
+ };
1378
+ }
1379
+ if (this.overlay.kind === 'request-form') {
1380
+ if (this.overlay.value.confirmUnansweredActive()) {
1381
+ const confirmState = this.overlay.value.confirmationOverlayState();
1382
+ if (confirmState) {
1383
+ const lines = renderCodexOverlay(confirmState, Math.max(24, Math.min(width - 6, 88)), maxHeight);
1384
+ return {
1385
+ lines: lines.map((line) => styleOverlayLine(centerLine(line, width))),
1386
+ };
1387
+ }
1388
+ }
1389
+ const rendered = renderCodexRequestInputDetailed(this.overlay.value.currentState(), Math.max(28, Math.min(width - 6, 88)), maxHeight);
1390
+ const rawCenteredLines = rendered.lines.map((line) => centerLine(line, width));
1391
+ const cursorLine = rendered.cursorLineIndex;
1392
+ const cursorRawLine = cursorLine === undefined ? undefined : rawCenteredLines[cursorLine];
1393
+ const leftPadding = cursorRawLine?.match(/^\s*/u)?.[0]?.length ?? 0;
1394
+ return {
1395
+ lines: rawCenteredLines.map((line) => styleOverlayLine(line)),
1396
+ cursorRow: cursorLine,
1397
+ cursorCol: cursorLine === undefined || rendered.cursorCol === undefined
1398
+ ? undefined
1399
+ : leftPadding + rendered.cursorCol,
1400
+ };
1401
+ }
1402
+ const rendered = renderCodexRequestInputDetailed(this.overlay.value, Math.max(28, Math.min(width - 6, 88)), maxHeight);
1403
+ const rawCenteredLines = rendered.lines.map((line) => centerLine(line, width));
1404
+ const cursorLine = rendered.cursorLineIndex;
1405
+ const cursorRawLine = cursorLine === undefined ? undefined : rawCenteredLines[cursorLine];
1406
+ const leftPadding = cursorRawLine?.match(/^\s*/u)?.[0]?.length ?? 0;
1407
+ return {
1408
+ lines: rawCenteredLines.map((line) => styleOverlayLine(line)),
1409
+ cursorRow: cursorLine,
1410
+ cursorCol: cursorLine === undefined || rendered.cursorCol === undefined
1411
+ ? undefined
1412
+ : leftPadding + rendered.cursorCol,
1413
+ };
1414
+ }
1415
+ render() {
1416
+ if (!this.active) {
1417
+ return;
1418
+ }
1419
+ const totalWidth = Math.max(stdout.columns ?? 100, 60);
1420
+ const totalHeight = Math.max(stdout.rows ?? 24, 4);
1421
+ const headerLines = this.buildHeaderLines(totalWidth);
1422
+ const showStatusLine = this.state.phase !== 'ready';
1423
+ const maxContextLines = Math.min(5, Math.max(0, totalHeight - (showStatusLine ? 5 : 4)));
1424
+ const contextLines = this.buildContextLines(totalWidth, maxContextLines);
1425
+ const queueLines = this.buildQueueLines(totalWidth);
1426
+ const showPromptLine = Boolean(this.promptResolver);
1427
+ const promptRender = showPromptLine ? this.buildPromptLines(totalWidth) : null;
1428
+ const footerSectionCounts = selectWatchFooterSectionCounts({
1429
+ totalHeight,
1430
+ headerCount: headerLines.length,
1431
+ contextCount: contextLines.length,
1432
+ showStatus: showStatusLine,
1433
+ queueCount: queueLines.length,
1434
+ promptCount: promptRender?.lines.length ?? 0,
1435
+ });
1436
+ const footerLines = [
1437
+ ...contextLines
1438
+ .slice(-footerSectionCounts.contextCount)
1439
+ .map((line) => styleContextLine(line, totalWidth)),
1440
+ ...(footerSectionCounts.statusCount > 0 ? [styleStatusLine(formatWatchStatusLine(this.state, totalWidth))] : []),
1441
+ ...queueLines.slice(0, footerSectionCounts.queueCount),
1442
+ ...(promptRender ? promptRender.lines : []),
1443
+ this.buildFooterLine(totalWidth),
1444
+ ];
1445
+ const overlayRender = this.buildOverlayLines(totalWidth, Math.max(0, totalHeight - headerLines.length - footerLines.length));
1446
+ const overlayLines = overlayRender.lines;
1447
+ const contentHeight = Math.max(0, totalHeight - headerLines.length - footerLines.length - overlayLines.length);
1448
+ const activityBudget = Math.min(6, Math.max(0, Math.floor(contentHeight / 3)));
1449
+ const mainLines = this.buildMainLines(totalWidth);
1450
+ const activityPanelLines = this.buildActivityPanel(totalWidth, activityBudget);
1451
+ const mainBudget = Math.max(0, contentHeight - activityPanelLines.length);
1452
+ const visibleMain = mainLines.slice(-mainBudget);
1453
+ const rows = [];
1454
+ rows.push(...headerLines);
1455
+ for (const line of visibleMain) {
1456
+ rows.push(line);
1457
+ }
1458
+ for (const line of activityPanelLines) {
1459
+ rows.push(line);
1460
+ }
1461
+ while (rows.length < headerLines.length + contentHeight) {
1462
+ rows.push(' '.repeat(totalWidth));
1463
+ }
1464
+ rows.push(...overlayLines);
1465
+ rows.push(...footerLines);
1466
+ const visibleRows = rows.slice(0, totalHeight);
1467
+ stdout.write('\x1b[H');
1468
+ stdout.write(visibleRows.join('\n'));
1469
+ stdout.write('\x1b[J');
1470
+ if (overlayRender.cursorRow !== undefined && overlayRender.cursorCol !== undefined) {
1471
+ const cursorRow = Math.min(totalHeight, headerLines.length + contentHeight + overlayRender.cursorRow + 1);
1472
+ const cursorCol = Math.min(totalWidth, overlayRender.cursorCol);
1473
+ stdout.write(`\x1b[?25h\x1b[${cursorRow};${cursorCol}H`);
1474
+ }
1475
+ else if (this.promptResolver) {
1476
+ const cursorRow = Math.min(totalHeight, headerLines.length
1477
+ + contentHeight
1478
+ + overlayLines.length
1479
+ + footerSectionCounts.contextCount
1480
+ + footerSectionCounts.statusCount
1481
+ + footerSectionCounts.queueCount
1482
+ + (promptRender?.cursorRow ?? 0)
1483
+ + 1);
1484
+ const cursorCol = Math.min(totalWidth, promptRender?.cursorCol ?? 1);
1485
+ stdout.write(`\x1b[?25h\x1b[${cursorRow};${cursorCol}H`);
1486
+ }
1487
+ else {
1488
+ stdout.write('\x1b[?25l');
1489
+ }
1490
+ }
1491
+ teardown() {
1492
+ if (!this.active) {
1493
+ return;
1494
+ }
1495
+ this.active = false;
1496
+ stdin.off('keypress', this.handleKeypress);
1497
+ stdout.off('resize', this.handleResize);
1498
+ if (this.renderTicker) {
1499
+ clearInterval(this.renderTicker);
1500
+ this.renderTicker = null;
1501
+ }
1502
+ if ('setRawMode' in stdin && typeof stdin.setRawMode === 'function') {
1503
+ stdin.setRawMode(this.originalRawMode);
1504
+ }
1505
+ stdout.write('\x1b[?25h\x1b[?1049l');
1506
+ }
1507
+ }
1508
+ export function createWatchDisplay(record, prompt) {
1509
+ if (supportsWatchTui()) {
1510
+ return new TuiWatchDisplay(record, prompt);
1511
+ }
1512
+ return new PlainWatchDisplay(record, prompt);
1513
+ }
1514
+ //# sourceMappingURL=display.js.map