@yanzc/claude-pro 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/api.js +59 -8
- package/src/codex/client.js +7 -0
- package/src/commands.js +44 -10
- package/src/gateway.js +48 -0
- package/src/peers.js +157 -156
- package/src/tools/effort.js +1 -1
- package/src/tools/peers.js +3 -1
- package/src/ui/repl.js +1459 -1450
package/src/ui/repl.js
CHANGED
|
@@ -1,1450 +1,1459 @@
|
|
|
1
|
-
// Interactive REPL: owns the terminal, renders the transcript and live region, and
|
|
2
|
-
// implements the agent's IO callbacks (streaming, tool display, permission prompts).
|
|
3
|
-
|
|
4
|
-
import fs from 'node:fs';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import os from 'node:os';
|
|
7
|
-
import readline from 'node:readline';
|
|
8
|
-
import { spawn } from 'node:child_process';
|
|
9
|
-
import { c, ESC, stringWidth, truncate, padEnd, wordWrap } from './ansi.js';
|
|
10
|
-
import { Screen } from './screen.js';
|
|
11
|
-
import { Editor } from './editor.js';
|
|
12
|
-
import { SelectDialog } from './dialogs.js';
|
|
13
|
-
import { MarkdownStream, renderMarkdown } from './markdown.js';
|
|
14
|
-
import {
|
|
15
|
-
DOT, toolHeader, renderToolResult, renderTodos, renderUserMessage, resultLines, formatTokens, formatDuration, toolLabel,
|
|
16
|
-
} from './render.js';
|
|
17
|
-
import { createRuntime } from '../runtime.js';
|
|
18
|
-
import { SessionStore, firstUserText } from '../session.js';
|
|
19
|
-
import { defaultEffort, effortSupported, quickQuery } from '../api.js';
|
|
20
|
-
import { CONFIG_DIR, VERSION, APP_NAME, INSTALL_DIR, rememberChoice } from '../config.js';
|
|
21
|
-
import { readGatewayLogin } from '../gateway.js';
|
|
22
|
-
import { TOPIC_TITLE_SYSTEM, topicTitlePrompt } from '../prompts.js';
|
|
23
|
-
import { buildCommands } from '../commands.js';
|
|
24
|
-
import { canRestart, writeRestartState, runSelfCheck, watchSource, restartNote, RESTART_EXIT_CODE, MAX_RESTARTS, RESTART_WINDOW_MS } from '../restart.js';
|
|
25
|
-
import { resolvePath, displayPath } from '../utils/paths.js';
|
|
26
|
-
import { ripgrepPath } from '../utils/shell.js';
|
|
27
|
-
import { describePause, formatClock, humanDuration, shouldAnnounce } from '../ratelimit.js';
|
|
28
|
-
import { shutdownBackgroundShells, listBackgroundShells } from '../tools/bash.js';
|
|
29
|
-
import { estimateTokens } from '../agent.js';
|
|
30
|
-
import { readClipboardImage, imageFromFile, watchClipboard } from '../utils/clipboard.js';
|
|
31
|
-
import { checkForUpdate } from '../updater.js';
|
|
32
|
-
import { PeerNode, peersEnabled, findPeer, peerMessagePrompt } from '../peers.js';
|
|
33
|
-
|
|
34
|
-
// A model id as people say it: claude-sonnet-5-5 -> sonnet-5-5, claude-haiku-4-5-20251001 -> haiku-4-5.
|
|
35
|
-
export const modelTag = (model) => String(model?.id ?? '').replace(/^claude-/, '').replace(/-\d{8}$/, '');
|
|
36
|
-
|
|
37
|
-
// The model and effort level that requests use now, for the footer and the terminal title. The effort
|
|
38
|
-
// is left out when none is sent (auto) and for a model that does not take one.
|
|
39
|
-
export function modelInfo(config) {
|
|
40
|
-
const model = config.mainModel;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
.replace(
|
|
57
|
-
.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
child
|
|
95
|
-
|
|
96
|
-
child.on('
|
|
97
|
-
|
|
98
|
-
(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
else if (base.
|
|
130
|
-
else if (lower.
|
|
131
|
-
if (
|
|
132
|
-
if (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
this.
|
|
143
|
-
this.
|
|
144
|
-
this.
|
|
145
|
-
this.
|
|
146
|
-
this.
|
|
147
|
-
this.
|
|
148
|
-
this.
|
|
149
|
-
this.
|
|
150
|
-
this.
|
|
151
|
-
this.
|
|
152
|
-
this.
|
|
153
|
-
this.
|
|
154
|
-
this.
|
|
155
|
-
this.
|
|
156
|
-
this.
|
|
157
|
-
this.
|
|
158
|
-
this.
|
|
159
|
-
this.
|
|
160
|
-
this.
|
|
161
|
-
this.
|
|
162
|
-
this.
|
|
163
|
-
this.
|
|
164
|
-
this.
|
|
165
|
-
this.
|
|
166
|
-
this.
|
|
167
|
-
this.
|
|
168
|
-
this.
|
|
169
|
-
this.
|
|
170
|
-
this.
|
|
171
|
-
this.
|
|
172
|
-
this.
|
|
173
|
-
this.
|
|
174
|
-
this.
|
|
175
|
-
this.
|
|
176
|
-
this.
|
|
177
|
-
this.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
get
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
stdin.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
process.on('
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
this.
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
this.
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
else if (this.
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
if (
|
|
282
|
-
|
|
283
|
-
if (title
|
|
284
|
-
this.
|
|
285
|
-
|
|
286
|
-
this.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const
|
|
297
|
-
const
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
this.
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
this.
|
|
320
|
-
|
|
321
|
-
this.
|
|
322
|
-
|
|
323
|
-
this.
|
|
324
|
-
|
|
325
|
-
this.
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
this.
|
|
333
|
-
this.
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
//
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
this.peers =
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
if (!
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
//
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
this.
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
this.
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
if (
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
// a restart,
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
if (
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
if (this.exiting) return;
|
|
503
|
-
if (
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
this.
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
if (this.
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
if (key.
|
|
595
|
-
|
|
596
|
-
if (
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
if (key.
|
|
604
|
-
|
|
605
|
-
if (key.name === '
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
if (this.
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
this.
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
if (
|
|
628
|
-
this.
|
|
629
|
-
this.
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
if (
|
|
680
|
-
|
|
681
|
-
if (
|
|
682
|
-
if (
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
this.
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
//
|
|
749
|
-
//
|
|
750
|
-
//
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
this.
|
|
840
|
-
this.
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
this.
|
|
868
|
-
this.
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
this.
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
const
|
|
908
|
-
this.
|
|
909
|
-
this.
|
|
910
|
-
this.
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
this.
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
}
|
|
924
|
-
this.active
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
}
|
|
933
|
-
this.
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
this.
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
this.
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
this.
|
|
992
|
-
this.
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
if (
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
if (
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
this.
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
this.
|
|
1132
|
-
break;
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
this.
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
this.
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
},
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
this.
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
this.
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
if (
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
const
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
const
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
if (this.
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
return {
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
const
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
const
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
if (
|
|
1427
|
-
else if (this.
|
|
1428
|
-
else if (this.
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
const
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
const
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1
|
+
// Interactive REPL: owns the terminal, renders the transcript and live region, and
|
|
2
|
+
// implements the agent's IO callbacks (streaming, tool display, permission prompts).
|
|
3
|
+
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import readline from 'node:readline';
|
|
8
|
+
import { spawn } from 'node:child_process';
|
|
9
|
+
import { c, ESC, stringWidth, truncate, padEnd, wordWrap } from './ansi.js';
|
|
10
|
+
import { Screen } from './screen.js';
|
|
11
|
+
import { Editor } from './editor.js';
|
|
12
|
+
import { SelectDialog } from './dialogs.js';
|
|
13
|
+
import { MarkdownStream, renderMarkdown } from './markdown.js';
|
|
14
|
+
import {
|
|
15
|
+
DOT, toolHeader, renderToolResult, renderTodos, renderUserMessage, resultLines, formatTokens, formatDuration, toolLabel,
|
|
16
|
+
} from './render.js';
|
|
17
|
+
import { createRuntime } from '../runtime.js';
|
|
18
|
+
import { SessionStore, firstUserText } from '../session.js';
|
|
19
|
+
import { defaultEffort, effortSupported, effortToSend, quickQuery } from '../api.js';
|
|
20
|
+
import { CONFIG_DIR, VERSION, APP_NAME, INSTALL_DIR, rememberChoice } from '../config.js';
|
|
21
|
+
import { readGatewayLogin } from '../gateway.js';
|
|
22
|
+
import { TOPIC_TITLE_SYSTEM, topicTitlePrompt } from '../prompts.js';
|
|
23
|
+
import { buildCommands } from '../commands.js';
|
|
24
|
+
import { canRestart, writeRestartState, runSelfCheck, watchSource, restartNote, RESTART_EXIT_CODE, MAX_RESTARTS, RESTART_WINDOW_MS } from '../restart.js';
|
|
25
|
+
import { resolvePath, displayPath } from '../utils/paths.js';
|
|
26
|
+
import { ripgrepPath } from '../utils/shell.js';
|
|
27
|
+
import { describePause, formatClock, humanDuration, shouldAnnounce } from '../ratelimit.js';
|
|
28
|
+
import { shutdownBackgroundShells, listBackgroundShells } from '../tools/bash.js';
|
|
29
|
+
import { estimateTokens } from '../agent.js';
|
|
30
|
+
import { readClipboardImage, imageFromFile, watchClipboard } from '../utils/clipboard.js';
|
|
31
|
+
import { checkForUpdate } from '../updater.js';
|
|
32
|
+
import { PeerNode, peersEnabled, findPeer, peerMessagePrompt } from '../peers.js';
|
|
33
|
+
|
|
34
|
+
// A model id as people say it: claude-sonnet-5-5 -> sonnet-5-5, claude-haiku-4-5-20251001 -> haiku-4-5.
|
|
35
|
+
export const modelTag = (model) => String(model?.id ?? '').replace(/^claude-/, '').replace(/-\d{8}$/, '');
|
|
36
|
+
|
|
37
|
+
// The model and effort level that requests use now, for the footer and the terminal title. The effort
|
|
38
|
+
// is left out when none is sent (auto) and for a model that does not take one.
|
|
39
|
+
export function modelInfo(config) {
|
|
40
|
+
const model = config.mainModel;
|
|
41
|
+
// What the requests carry: the nearest level the model takes (none goes out as low on gpt-6.1-sol, ultra as max on Claude).
|
|
42
|
+
const sent = effortToSend(config, model.id, config.effort);
|
|
43
|
+
const effort = sent && effortSupported(model.id) ? sent : null;
|
|
44
|
+
return { model: modelTag(model), effort };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The small model's answer about the topic (see topicTitlePrompt) as a clean title, or null when it is not
|
|
48
|
+
// a new topic or the answer cannot be used. `required`: there is no title yet, so any title will do.
|
|
49
|
+
export function parseTopicTitle(answer, { required = false } = {}) {
|
|
50
|
+
const json = /\{[\s\S]*\}/.exec(String(answer ?? ''))?.[0];
|
|
51
|
+
if (!json) return null;
|
|
52
|
+
let data;
|
|
53
|
+
try { data = JSON.parse(json); } catch { return null; }
|
|
54
|
+
if ((!required && data?.isNewTopic !== true) || typeof data?.title !== 'string') return null;
|
|
55
|
+
const title = data.title
|
|
56
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]+/g, ' ')
|
|
57
|
+
.replace(/\s+/g, ' ')
|
|
58
|
+
.replace(/^["'`“”‘’「」『』《》\s]+|["'`“”‘’「」『』《》\s.。!!??,,::;;]+$/g, '')
|
|
59
|
+
.trim();
|
|
60
|
+
return title ? truncate(title, 40).replace(/\x1b\[0m$/, '') : null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const imageBlock = (img) => ({ type: 'image', source: { type: 'base64', media_type: img.media_type, data: img.data } });
|
|
64
|
+
|
|
65
|
+
const FOCUS_EVENTS_ON = '\x1b[?1004h';
|
|
66
|
+
const FOCUS_EVENTS_OFF = '\x1b[?1004l';
|
|
67
|
+
|
|
68
|
+
const SPINNER = ['·', '✢', '✳', '✶', '✻', '✽', '✽', '✻', '✶', '✳', '✢', '·'];
|
|
69
|
+
const VERBS = ['Thinking', 'Pondering', 'Crafting', 'Brewing', 'Computing', 'Conjuring', 'Cooking', 'Musing', 'Noodling', 'Percolating', 'Ruminating', 'Synthesizing', 'Working', 'Tinkering', 'Mulling', 'Deliberating', 'Churning', 'Forging', 'Hatching', 'Simmering'];
|
|
70
|
+
const HISTORY_FILE = path.join(CONFIG_DIR, 'history.jsonl');
|
|
71
|
+
|
|
72
|
+
function loadHistory(cwd) {
|
|
73
|
+
try {
|
|
74
|
+
return fs.readFileSync(HISTORY_FILE, 'utf8').split('\n').filter(Boolean).map((l) => { try { return JSON.parse(l); } catch { return null; } })
|
|
75
|
+
.filter((e) => e && e.project === cwd).map((e) => e.display).slice(-500);
|
|
76
|
+
} catch { return []; }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function appendHistory(cwd, text) {
|
|
80
|
+
try {
|
|
81
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
82
|
+
fs.appendFileSync(HISTORY_FILE, JSON.stringify({ display: text, project: cwd, timestamp: Date.now() }) + '\n');
|
|
83
|
+
} catch { /* ignore */ }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
class FileIndex {
|
|
87
|
+
constructor(cwd, getConfig = () => null) { this.cwd = cwd; this.getConfig = getConfig; this.files = []; this.at = 0; this.loading = false; }
|
|
88
|
+
refresh() {
|
|
89
|
+
if (this.loading || Date.now() - this.at < 30_000) return;
|
|
90
|
+
this.loading = true;
|
|
91
|
+
const done = (list) => { this.files = list.slice(0, 50_000); this.at = Date.now(); this.loading = false; };
|
|
92
|
+
const rg = ripgrepPath(this.getConfig());
|
|
93
|
+
if (rg) {
|
|
94
|
+
const child = spawn(rg, ['--files', '--hidden', '--glob', '!.git'], { cwd: this.cwd, windowsHide: true });
|
|
95
|
+
let out = '';
|
|
96
|
+
child.stdout.on('data', (d) => { if (out.length < 10_000_000) out += d; });
|
|
97
|
+
child.on('close', () => done(out.split(/\r?\n/).filter(Boolean).map((f) => f.replace(/\\/g, '/'))));
|
|
98
|
+
child.on('error', () => done([]));
|
|
99
|
+
} else {
|
|
100
|
+
(async () => {
|
|
101
|
+
const list = [];
|
|
102
|
+
try {
|
|
103
|
+
for await (const f of fs.promises.glob('**/*', { cwd: this.cwd, exclude: (p) => /(^|[\\/])(node_modules|\.git)$/.test(p) })) {
|
|
104
|
+
list.push(f.replace(/\\/g, '/'));
|
|
105
|
+
if (list.length > 50_000) break;
|
|
106
|
+
}
|
|
107
|
+
} catch { /* ignore */ }
|
|
108
|
+
done(list);
|
|
109
|
+
})();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
search(q) {
|
|
113
|
+
this.refresh();
|
|
114
|
+
const query = q.toLowerCase();
|
|
115
|
+
const dirs = new Set();
|
|
116
|
+
for (const f of this.files) {
|
|
117
|
+
const parts = f.split('/');
|
|
118
|
+
for (let i = 1; i < parts.length; i++) dirs.add(parts.slice(0, i).join('/') + '/');
|
|
119
|
+
if (dirs.size > 5000) break;
|
|
120
|
+
}
|
|
121
|
+
// Without ripgrep the listing comes from a glob that also returns directories (as plain names): they are already in `dirs`, with their slash.
|
|
122
|
+
const all = [...dirs, ...this.files.filter((f) => !dirs.has(f + '/'))];
|
|
123
|
+
const scored = [];
|
|
124
|
+
for (const f of all) {
|
|
125
|
+
const lower = f.toLowerCase();
|
|
126
|
+
const base = lower.replace(/\/$/, '').split('/').pop();
|
|
127
|
+
let score = -1;
|
|
128
|
+
if (!query) score = f.split('/').length;
|
|
129
|
+
else if (base.startsWith(query)) score = 0;
|
|
130
|
+
else if (lower.startsWith(query)) score = 1;
|
|
131
|
+
else if (base.includes(query)) score = 2;
|
|
132
|
+
else if (lower.includes(query)) score = 3;
|
|
133
|
+
if (score >= 0) scored.push({ f, score: score * 1000 + f.length });
|
|
134
|
+
if (scored.length > 2000) break;
|
|
135
|
+
}
|
|
136
|
+
return scored.sort((a, b) => a.score - b.score).slice(0, 12).map((s) => s.f);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export class App {
|
|
141
|
+
constructor({ flags, cwd, stdin = process.stdin, stdout = process.stdout, noProcessExit = false }) {
|
|
142
|
+
this.flags = flags;
|
|
143
|
+
this.cwd = cwd;
|
|
144
|
+
this.stdin = stdin;
|
|
145
|
+
this.stdout = stdout;
|
|
146
|
+
this.noProcessExit = noProcessExit;
|
|
147
|
+
this.screen = new Screen(stdout);
|
|
148
|
+
this.busy = false;
|
|
149
|
+
this.controller = null;
|
|
150
|
+
this.queue = [];
|
|
151
|
+
this.dialogs = [];
|
|
152
|
+
this.active = []; // running tool entries, in call order
|
|
153
|
+
this.pendingToolBlocks = new Map(); // streaming tool_use blocks, by index
|
|
154
|
+
this.md = null;
|
|
155
|
+
this.thinking = null;
|
|
156
|
+
this.verbose = !!flags.verbose;
|
|
157
|
+
this.showTodos = true;
|
|
158
|
+
this.turnStart = 0;
|
|
159
|
+
this.turnTokens = 0;
|
|
160
|
+
this.verb = 'Thinking';
|
|
161
|
+
this.status = null; // override spinner text (e.g. compacting)
|
|
162
|
+
this.rateWait = null; // {resumeAt, label, polling} while paused for a rate limit
|
|
163
|
+
this.exitArmed = 0;
|
|
164
|
+
this.notices = [];
|
|
165
|
+
this.topic = null; // what the conversation is about now, named by the small model (see refreshTopic)
|
|
166
|
+
this.firstPrompt = null; // the session's first message: the title until there is a topic
|
|
167
|
+
this.topicSeq = 0; // topic requests sent ...
|
|
168
|
+
this.topicApplied = 0; // ... and the newest one whose answer was used
|
|
169
|
+
this.restarts = []; // when this session was restarted lately (carried over by each restart)
|
|
170
|
+
this.restartRequest = null; // {reason, then, continueTurn}: restart as soon as nothing is running
|
|
171
|
+
this.sourceChanged = null; // the program's own source changed since it was last checked
|
|
172
|
+
this.reloading = false;
|
|
173
|
+
this.skipAutoReload = false;
|
|
174
|
+
this.peers = null; // this session as seen by the other sessions on the machine (see peers.js)
|
|
175
|
+
this.turnPeers = []; // tasks from other sessions that the running turn answers: [{msg}]
|
|
176
|
+
this.userSentToPeers = new Set(); // ids of messages the user sent to other sessions with @name
|
|
177
|
+
this.fileIndex = new FileIndex(cwd, () => this.runtime?.config);
|
|
178
|
+
this.store = new SessionStore(cwd);
|
|
179
|
+
this.editor = new Editor({
|
|
180
|
+
history: loadHistory(cwd),
|
|
181
|
+
onSubmit: (value, raw, images) => { try { this.onSubmit(value, raw, images); } catch (e) { this.logError(e); } },
|
|
182
|
+
imageFromPath: (p) => {
|
|
183
|
+
const r = imageFromFile(resolvePath(p, this.cwd));
|
|
184
|
+
if (r.error && !/not found|Not a file/.test(r.error)) this.flash(r.error, 'warn');
|
|
185
|
+
if (r.image) this.flashMessage = null;
|
|
186
|
+
return r.image || null;
|
|
187
|
+
},
|
|
188
|
+
getSuggestions: (text, cursor) => this.suggest(text, cursor),
|
|
189
|
+
onChange: () => this.screen.schedule(),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
get agent() { return this.runtime.agent; }
|
|
194
|
+
get config() { return this.runtime.config; }
|
|
195
|
+
get width() { return this.screen.width; }
|
|
196
|
+
|
|
197
|
+
// ---------------------------------------------------------------- lifecycle
|
|
198
|
+
|
|
199
|
+
async start(initialPrompt) {
|
|
200
|
+
const stdin = this.stdin;
|
|
201
|
+
readline.emitKeypressEvents(stdin);
|
|
202
|
+
stdin.setRawMode(true);
|
|
203
|
+
stdin.setEncoding('utf8');
|
|
204
|
+
this.stdout.write(ESC.bracketedPasteOn);
|
|
205
|
+
this.watchCopies();
|
|
206
|
+
const cleanup = () => this.restoreTerminal();
|
|
207
|
+
process.on('exit', cleanup);
|
|
208
|
+
process.on('SIGTERM', () => this.exit(143));
|
|
209
|
+
process.on('SIGHUP', () => this.exit(129));
|
|
210
|
+
|
|
211
|
+
await this.startPeer();
|
|
212
|
+
this.runtime = await createRuntime({ cwd: this.cwd, flags: this.flags, io: this });
|
|
213
|
+
this.commands = buildCommands(this);
|
|
214
|
+
// Keys typed while starting up stay buffered in stdin until we listen.
|
|
215
|
+
stdin.on('keypress', (str, key) => {
|
|
216
|
+
try { this.onKey(str, key || {}); } catch (e) { this.logError(e); }
|
|
217
|
+
});
|
|
218
|
+
stdin.resume();
|
|
219
|
+
this.runtime.modes.listeners.add(() => this.screen.schedule());
|
|
220
|
+
// Started by a restart of this session: the output of the process before it is still on screen, so no
|
|
221
|
+
// welcome box and no transcript, just a line saying so.
|
|
222
|
+
const restart = this.flags.restart || null;
|
|
223
|
+
const resumable = Boolean(restart?.sessionFile && fs.existsSync(restart.sessionFile));
|
|
224
|
+
if (!resumable) this.printWelcome();
|
|
225
|
+
// The tab label, as in the official client: what this session is about, then where and with what.
|
|
226
|
+
this.screen.setTitle(() => this.terminalTitle(), path.basename(this.cwd) || this.cwd, { inherited: Boolean(restart) });
|
|
227
|
+
|
|
228
|
+
if (restart) this.resumeAfterRestart(restart, resumable);
|
|
229
|
+
else if (this.flags.resumeFile) this.loadSession(this.flags.resumeFile, this.flags.resumeId);
|
|
230
|
+
else if (this.flags.resume) await this.commands.get('resume').run('');
|
|
231
|
+
|
|
232
|
+
this.screen.setLive((w) => this.renderLive(w));
|
|
233
|
+
this.spinnerTimer = setInterval(() => { if (this.busy || this.active.length) this.screen.schedule(); }, 120);
|
|
234
|
+
if (restart) this.continueAfterRestart(restart);
|
|
235
|
+
else if (initialPrompt?.trim()) this.onSubmit(initialPrompt, initialPrompt);
|
|
236
|
+
// Nothing configured yet: the gateway is the default, and its login comes first, as in Claude Code.
|
|
237
|
+
else if (this.config.usesGateway && !readGatewayLogin(this.config.gatewayUrl)) {
|
|
238
|
+
Promise.resolve(this.commands.get('login').run('')).catch((e) => this.logError(e)).finally(() => this.screen.schedule());
|
|
239
|
+
}
|
|
240
|
+
this.fileIndex.refresh();
|
|
241
|
+
this.startSourceWatch();
|
|
242
|
+
if (!this.noProcessExit && !this.flags.restart) this.autoUpdate();
|
|
243
|
+
return new Promise((resolve) => { this.resolveExit = resolve; });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// What the session is about now: the topic the small model named last, or else the first thing asked
|
|
247
|
+
// (kept when the conversation is compacted afterwards). Cleared by /clear, replaced by /resume.
|
|
248
|
+
sessionLabel() {
|
|
249
|
+
if (this.topic) return this.topic;
|
|
250
|
+
if (!this.firstPrompt) {
|
|
251
|
+
const first = this.agent?.messages.find((m) => m.role === 'user' && firstUserText(m));
|
|
252
|
+
this.firstPrompt = first ? firstUserText(first) : null;
|
|
253
|
+
}
|
|
254
|
+
return this.firstPrompt;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// The newest message the user typed in this conversation (not a tool result, a reminder or a summary).
|
|
258
|
+
latestPrompt() {
|
|
259
|
+
const messages = this.agent?.messages || [];
|
|
260
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
261
|
+
if (messages[i].role !== 'user') continue;
|
|
262
|
+
const text = firstUserText(messages[i]);
|
|
263
|
+
if (text && !text.startsWith('This session is being continued')) return text;
|
|
264
|
+
}
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Keep the title on the topic of the conversation: ask the small model, in the background, whether
|
|
269
|
+
// `message` starts a new topic and what to call it (as the official client does). Nothing waits for the
|
|
270
|
+
// answer, and when there is none (a rate limit, no network) the title just stays as it is.
|
|
271
|
+
refreshTopic(message) {
|
|
272
|
+
const text = String(message ?? '').trim();
|
|
273
|
+
if (!text || /^(1|true|yes)$/i.test(this.config.env.CLAUDE_PRO_DISABLE_TOPIC_TITLE || '')) return;
|
|
274
|
+
const store = this.store;
|
|
275
|
+
const seq = ++this.topicSeq;
|
|
276
|
+
const current = this.topic;
|
|
277
|
+
quickQuery(this.config, {
|
|
278
|
+
model: this.config.smallModel, system: TOPIC_TITLE_SYSTEM, prompt: topicTitlePrompt(current, text.slice(0, 2000)), maxTokens: 2048,
|
|
279
|
+
}).then((answer) => {
|
|
280
|
+
// another session meanwhile, or an answer about a newer message already in use
|
|
281
|
+
if (store !== this.store || this.exiting || seq < this.topicApplied) return;
|
|
282
|
+
const title = parseTopicTitle(answer, { required: !current });
|
|
283
|
+
if (!title) return;
|
|
284
|
+
this.topicApplied = seq;
|
|
285
|
+
if (title === this.topic) return;
|
|
286
|
+
this.topic = title;
|
|
287
|
+
store.setTopic(title);
|
|
288
|
+
this.screen.schedule();
|
|
289
|
+
}, () => { /* only the title: not worth a word */ });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// "<state> <session> | <directory> · <model> · <effort>". The session is cut short so that the model and
|
|
293
|
+
// effort, which are what tells tabs apart, are not what a narrow tab loses. While it works the state mark
|
|
294
|
+
// blinks, so that a tab that is busy can be told from one that waits for the user.
|
|
295
|
+
terminalTitle() {
|
|
296
|
+
const dir = path.basename(this.cwd) || this.cwd;
|
|
297
|
+
const { model, effort } = modelInfo(this.config);
|
|
298
|
+
const state = this.rateWait ? '⏸' : this.busy ? (Math.floor(Date.now() / 1000) % 2 ? '⠐' : '⠂') : '✳';
|
|
299
|
+
const session = this.sessionLabel();
|
|
300
|
+
const where = [dir, model, effort].filter(Boolean).join(' · ');
|
|
301
|
+
return `${state} ${session ? `${truncate(session, 40).replace(/\x1b\[0m$/, '')} | ` : ''}${where}`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// `handOver`: another process takes the terminal over at once (a restart). It is left the way the program
|
|
305
|
+
// had it (raw mode, bracketed paste, the tab's title), so that keys typed in between are not echoed or lost.
|
|
306
|
+
restoreTerminal({ handOver = false } = {}) {
|
|
307
|
+
if (this.restored) return;
|
|
308
|
+
this.restored = true;
|
|
309
|
+
try {
|
|
310
|
+
this.screen.close({ keepTitle: handOver });
|
|
311
|
+
this.clipboardWatch?.close();
|
|
312
|
+
if (this.clipboardWatch) this.stdout.write(FOCUS_EVENTS_OFF);
|
|
313
|
+
this.stdout.write(handOver ? ESC.showCursor : ESC.bracketedPasteOff + ESC.showCursor);
|
|
314
|
+
if (!handOver && this.stdin.isTTY) this.stdin.setRawMode(false);
|
|
315
|
+
} catch { /* ignore */ }
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
exit(code = 0, { restart = false } = {}) {
|
|
319
|
+
if (this.exiting) return;
|
|
320
|
+
this.exiting = true;
|
|
321
|
+
this.controller?.abort();
|
|
322
|
+
clearInterval(this.spinnerTimer);
|
|
323
|
+
this.sourceWatch?.close();
|
|
324
|
+
shutdownBackgroundShells();
|
|
325
|
+
this.runtime?.tasks.killAll();
|
|
326
|
+
this.runtime?.mcp.close();
|
|
327
|
+
this.peers?.close();
|
|
328
|
+
const u = this.agent?.totalUsage();
|
|
329
|
+
if (!restart && u && u.requests) {
|
|
330
|
+
this.screen.log(c.dim(`Total usage: ${formatTokens(u.input + u.cacheRead + u.cacheWrite)} input (${formatTokens(u.cacheRead)} cached), ${formatTokens(u.output)} output, ${u.requests + u.compactions} requests`));
|
|
331
|
+
}
|
|
332
|
+
if (!restart && this.agent?.messages.length) this.screen.log(c.dim(`Resume this session with: ${APP_NAME} --resume ${this.store.id}`));
|
|
333
|
+
this.restoreTerminal({ handOver: restart });
|
|
334
|
+
this.stdin.pause();
|
|
335
|
+
this.resolveExit?.(code);
|
|
336
|
+
if (!this.noProcessExit) setTimeout(() => process.exit(code), 50).unref();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ---------------------------------------------------------------- other sessions (see peers.js)
|
|
340
|
+
|
|
341
|
+
// Register this session so that the other sessions on the machine can send it tasks. Not in the tests
|
|
342
|
+
// (which drive the App themselves) and not with CLAUDE_PRO_PEERS=0.
|
|
343
|
+
async startPeer() {
|
|
344
|
+
if (this.noProcessExit || !peersEnabled()) return;
|
|
345
|
+
try {
|
|
346
|
+
this.peers = await new PeerNode({ cwd: this.cwd, name: this.flags.restart?.peerName, id: this.flags.restart?.peerId, onMessage: (msg) => this.onPeerMessage(msg) }).start();
|
|
347
|
+
} catch (e) {
|
|
348
|
+
this.peers = null;
|
|
349
|
+
this.notice(`Other sessions cannot reach this one: ${e.message}`, 'warn');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// `byModel`: the MessagePeer tool sent it, so the answer goes to the model; otherwise the user typed
|
|
354
|
+
// `@name ...` and the answer is only shown (and noted for the model).
|
|
355
|
+
async sendToPeer(name, text, { byModel = false } = {}) {
|
|
356
|
+
if (!this.peers) throw new Error('Talking to other sessions is off in this session.');
|
|
357
|
+
const r = await this.peers.send(name, { text });
|
|
358
|
+
if (!byModel) this.userSentToPeers.add(`${r.peer.id}:${r.id}`);
|
|
359
|
+
return r;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
onPeerMessage(msg) {
|
|
363
|
+
if (this.exiting) return;
|
|
364
|
+
const from = c.bold(`@${msg.from.name}`);
|
|
365
|
+
const width = this.width - 1;
|
|
366
|
+
if (msg.kind === 'reply' && this.userSentToPeers.delete(`${msg.from.id}:${msg.inReplyTo}`)) {
|
|
367
|
+
// the answer to something the user sent: shown, and noted for the model, without a turn of its own
|
|
368
|
+
this.screen.log(`${c.blue('✉')} ${from} ${c.dim('answered:')}`, ...renderMarkdown(msg.text, { width: width - 2 }).map((l) => ' ' + l), '');
|
|
369
|
+
this.agent.addReminder(`The user sent a message to another claude-pro session, ${msg.from.name} (${msg.from.cwd}), which answered:\n${msg.text}`);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const display = `✉ ${msg.kind === 'reply' ? 'answer from' : 'task from'} @${msg.from.name}: ${msg.text}`;
|
|
373
|
+
const item = { text: peerMessagePrompt(msg), images: [], display, peer: msg.kind === 'task' ? msg : null };
|
|
374
|
+
if (this.busy) { this.queue.push(item); this.screen.schedule(); return; }
|
|
375
|
+
this.startTurn(item.text, { display, peers: item.peer ? [item.peer] : [] });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// The turn that handled tasks of other sessions ended: send each its answer.
|
|
379
|
+
answerPeers(result, error) {
|
|
380
|
+
const tasks = this.turnPeers.splice(0);
|
|
381
|
+
if (!tasks.length || !this.peers) return;
|
|
382
|
+
const text = error ? `The session failed before finishing: ${error.message || error}`
|
|
383
|
+
: result?.interrupted ? `The user of ${this.peers.name} interrupted the work before it was finished.${result.text ? `\n\nWhat it had written:\n${result.text}` : ''}`
|
|
384
|
+
: (result?.text || '(no answer text)');
|
|
385
|
+
// One answer per session, to its last message: several messages from it that waited for this turn were all
|
|
386
|
+
// handled in it, and sending the whole answer once for each would hand that session the same text again and again.
|
|
387
|
+
const lastOfEach = new Map();
|
|
388
|
+
for (const msg of tasks) lastOfEach.set(`${msg.from.id}|${msg.from.name}|${msg.from.cwd}`, msg);
|
|
389
|
+
for (const msg of lastOfEach.values()) {
|
|
390
|
+
// by id, or by name: a session that was restarted by older code comes back with a new id
|
|
391
|
+
const running = this.peers.peers();
|
|
392
|
+
const peer = findPeer(running, msg.from.id) || running.find((p) => p.name === msg.from.name && p.cwd === msg.from.cwd);
|
|
393
|
+
if (!peer) { this.notice(`@${msg.from.name} is no longer running: its answer was not sent.`, 'warn'); continue; }
|
|
394
|
+
this.peers.send(peer, { text, kind: 'reply', inReplyTo: msg.id })
|
|
395
|
+
.then(() => this.screen.log(c.dim(` ⎿ Answer sent to @${msg.from.name}`)), (e) => this.notice(`Could not send the answer to @${msg.from.name}: ${e.message}`, 'warn'));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ---------------------------------------------------------------- restarting (see restart.js)
|
|
400
|
+
|
|
401
|
+
// What a restart would kill: agents and shells that are still running in the background.
|
|
402
|
+
restartBlockers() {
|
|
403
|
+
const found = [];
|
|
404
|
+
for (const t of this.runtime.tasks.running()) found.push(`background agent ${t.id} "${t.description}"`);
|
|
405
|
+
for (const s of listBackgroundShells()) if (s.status === 'running') found.push(`background shell ${s.id} (${String(s.command).slice(0, 40)})`);
|
|
406
|
+
return found;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Restart as soon as nothing is running: at once when idle, otherwise when the current turn ends (the
|
|
410
|
+
// turn that asked for it still has to be answered and saved). `continueTurn`: the model gets a note in
|
|
411
|
+
// the new process and goes on by itself. Returns {ok, message}.
|
|
412
|
+
requestRestart({ reason = '', then = '', continueTurn = false, force = false } = {}) {
|
|
413
|
+
if (!canRestart()) return { ok: false, message: 'This session cannot restart itself.' };
|
|
414
|
+
const now = Date.now();
|
|
415
|
+
const recent = this.restarts.filter((t) => now - t < RESTART_WINDOW_MS);
|
|
416
|
+
if (recent.length >= MAX_RESTARTS) {
|
|
417
|
+
return { ok: false, message: `The program was restarted ${recent.length} times in the last ${Math.round(RESTART_WINDOW_MS / 60_000)} minutes. That looks like a loop: stop restarting and tell the user what is going on.` };
|
|
418
|
+
}
|
|
419
|
+
const blockers = this.restartBlockers();
|
|
420
|
+
if (blockers.length && !force) return { ok: false, message: `A restart would kill: ${blockers.join('; ')}.` };
|
|
421
|
+
this.restartRequest = { reason, then, continueTurn };
|
|
422
|
+
this.screen.schedule();
|
|
423
|
+
if (!this.busy) this.restartNow();
|
|
424
|
+
return { ok: true };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Leave the state of the session for the next process and go; the supervisor starts it again.
|
|
428
|
+
restartNow() {
|
|
429
|
+
const request = this.restartRequest || {};
|
|
430
|
+
this.restartRequest = null;
|
|
431
|
+
const cfg = this.config;
|
|
432
|
+
const now = Date.now();
|
|
433
|
+
const state = {
|
|
434
|
+
at: now,
|
|
435
|
+
reason: request.reason || '',
|
|
436
|
+
continueWith: request.continueTurn ? restartNote(request) : '',
|
|
437
|
+
sessionId: this.store.id,
|
|
438
|
+
sessionFile: this.store.file,
|
|
439
|
+
modelOverride: cfg.modelOverride,
|
|
440
|
+
effortOverride: cfg.effortOverride,
|
|
441
|
+
effortBoost: cfg.effortBoost,
|
|
442
|
+
mode: this.runtime.modes.mode,
|
|
443
|
+
todos: this.agent.todos,
|
|
444
|
+
draft: this.editor.text,
|
|
445
|
+
draftCursor: this.editor.cursor,
|
|
446
|
+
queue: this.queue.map((q) => ({ text: q.text, images: q.images || [] })),
|
|
447
|
+
checkpoints: this.runtime.checkpoints.snapshot(),
|
|
448
|
+
restarts: [...this.restarts.filter((t) => now - t < RESTART_WINDOW_MS), now],
|
|
449
|
+
peerName: this.peers?.name,
|
|
450
|
+
peerId: this.peers?.id,
|
|
451
|
+
};
|
|
452
|
+
try {
|
|
453
|
+
writeRestartState(state);
|
|
454
|
+
} catch (e) {
|
|
455
|
+
this.notice(`Could not restart: ${e.message}`, 'warn');
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
this.exit(RESTART_EXIT_CODE, { restart: true });
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// In the process a restart started: take up the session where the one before it left it.
|
|
462
|
+
resumeAfterRestart(state, resumable) {
|
|
463
|
+
const cfg = this.config;
|
|
464
|
+
const kept = {};
|
|
465
|
+
if (state.modelOverride) cfg.modelOverride = kept.model = state.modelOverride;
|
|
466
|
+
if (state.effortOverride) cfg.effortOverride = kept.effort = state.effortOverride;
|
|
467
|
+
// The Effort tool's level holds for the request the restart continues, not for the user's next one.
|
|
468
|
+
if (state.effortBoost) { cfg.effortBoost = state.effortBoost; cfg.effortBoostKeepNext = Boolean(state.continueWith); }
|
|
469
|
+
// These were chosen with /model and /effort, which are kept for later sessions. One made by a version of
|
|
470
|
+
// the program that did not keep them yet is saved now.
|
|
471
|
+
try { rememberChoice(cfg.provider, kept); } catch { /* it still holds for this session */ }
|
|
472
|
+
if (state.mode === 'plan') this.runtime.modes.setMode('plan');
|
|
473
|
+
this.restarts = (Array.isArray(state.restarts) ? state.restarts : []).filter((t) => Number.isFinite(t));
|
|
474
|
+
if (resumable) {
|
|
475
|
+
this.loadSession(state.sessionFile, state.sessionId, { quiet: true });
|
|
476
|
+
if (Array.isArray(state.todos)) this.agent.todos = state.todos;
|
|
477
|
+
if (state.checkpoints) this.runtime.checkpoints.adopt(state.checkpoints);
|
|
478
|
+
}
|
|
479
|
+
if (typeof state.draft === 'string' && state.draft) this.editor.set(state.draft, state.draftCursor);
|
|
480
|
+
this.screen.log(c.dim(`↻ Restarted${state.reason ? `: ${state.reason}` : ''}${state.continueWith ? ' · continuing' : ''}`), '');
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// The turn that goes on after a restart: the note for the model, and what the user had queued meanwhile.
|
|
484
|
+
continueAfterRestart(state) {
|
|
485
|
+
const queued = (Array.isArray(state.queue) ? state.queue : []).filter((q) => q && typeof q.text === 'string' && q.text.trim());
|
|
486
|
+
if (!state.continueWith && !queued.length) return;
|
|
487
|
+
const typed = queued.map((q) => q.text).join('\n\n');
|
|
488
|
+
this.startTurn([state.continueWith, typed].filter(Boolean).join('\n\n'), { echo: queued.length > 0, display: typed, topic: typed, images: queued.flatMap((q) => q.images || []) });
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Follow the program's own source: once a turn is over (or at once when idle) a change to it is loaded by
|
|
492
|
+
// a restart, if the new code starts.
|
|
493
|
+
startSourceWatch() {
|
|
494
|
+
if (!canRestart() || /^(0|false|no|off)$/i.test(this.config.env.CLAUDE_PRO_AUTO_RESTART || '')) return;
|
|
495
|
+
this.sourceWatch = watchSource(INSTALL_DIR, (file) => {
|
|
496
|
+
this.sourceChanged = file || true;
|
|
497
|
+
if (!this.busy) this.maybeAutoReload();
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async maybeAutoReload() {
|
|
502
|
+
if (!this.sourceChanged || this.updating || this.reloading || this.busy || this.exiting || this.restartRequest || this.dialogs.length || this.queue.length) return;
|
|
503
|
+
if (this.skipAutoReload) { this.skipAutoReload = false; return; } // the user just interrupted: not now
|
|
504
|
+
if (this.restartBlockers().length) return; // looked at again whenever the program is next idle
|
|
505
|
+
const changed = this.sourceChanged;
|
|
506
|
+
this.sourceChanged = null;
|
|
507
|
+
this.reloading = true;
|
|
508
|
+
this.flash('Source changed: checking the new code…');
|
|
509
|
+
let check;
|
|
510
|
+
try { check = await runSelfCheck(); } finally { this.reloading = false; }
|
|
511
|
+
if (this.exiting) return;
|
|
512
|
+
if (!check.ok) {
|
|
513
|
+
this.notice(`The program's source changed but the new code does not start, so it was not restarted:\n${check.output.split('\n').slice(0, 12).join('\n')}\nFix it; the restart follows by itself (or run /restart).`, 'warn');
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
if (this.busy || this.dialogs.length) { this.sourceChanged = changed; return; } // something started meanwhile: later
|
|
517
|
+
this.requestRestart({ reason: 'source changed', force: true });
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// A newer version on npm is installed in the background (see updater.js) and loaded by a restart once the
|
|
521
|
+
// session is idle. `force`: the /update command, which also says when there is nothing to do.
|
|
522
|
+
async autoUpdate({ force = false } = {}) {
|
|
523
|
+
if (this.updating) { if (force) this.notice('An update is already being installed.'); return; }
|
|
524
|
+
let r;
|
|
525
|
+
try {
|
|
526
|
+
r = await checkForUpdate({ force, env: { ...process.env, ...this.config.env }, onInstalling: (v) => { this.updating = true; this.flash(`Installing ${APP_NAME} ${v}…`); } });
|
|
527
|
+
} catch (e) {
|
|
528
|
+
r = { status: 'failed', error: e.message };
|
|
529
|
+
} finally {
|
|
530
|
+
this.updating = false;
|
|
531
|
+
}
|
|
532
|
+
if (this.exiting) return;
|
|
533
|
+
if (r.status === 'updated') {
|
|
534
|
+
this.notice(`${APP_NAME} was updated to ${r.version} (from ${VERSION}); it is loaded by a restart as soon as the session is idle.`);
|
|
535
|
+
if (canRestart()) { this.sourceChanged = true; if (!this.busy) this.maybeAutoReload(); }
|
|
536
|
+
else this.notice('Restart the program to use it.');
|
|
537
|
+
} else if (r.status === 'failed') {
|
|
538
|
+
if (force || r.version) this.notice(`Could not update ${APP_NAME}${r.version ? ` to ${r.version}` : ''}: ${r.error}`, 'warn');
|
|
539
|
+
} else if (force) {
|
|
540
|
+
this.notice(r.status === 'dev' ? `This copy runs from a source checkout (${INSTALL_DIR}): update it with git.` : `${APP_NAME} ${VERSION} is the latest version.`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
printWelcome() {
|
|
545
|
+
const cfg = this.config;
|
|
546
|
+
const w = Math.min(this.width - 1, 76);
|
|
547
|
+
const inner = w - 4;
|
|
548
|
+
const row = (s) => c.claude('│') + ' ' + padEnd(truncate(s, inner), inner) + ' ' + c.claude('│');
|
|
549
|
+
const model = cfg.mainModel;
|
|
550
|
+
const lines = [
|
|
551
|
+
c.claude('╭' + '─'.repeat(w - 2) + '╮'),
|
|
552
|
+
row(`${c.claude('✻')} Welcome to ${c.bold(APP_NAME)}! ${c.dim(`v${VERSION} · a Claude Code style agent`)}`),
|
|
553
|
+
row(''),
|
|
554
|
+
row(c.dim(' /help for help, /status for your current setup')),
|
|
555
|
+
row(''),
|
|
556
|
+
row(c.dim(` cwd: ${this.cwd}`)),
|
|
557
|
+
row(c.dim(` model: ${model.label}${cfg.effort ? ` · effort ${cfg.effort}` : ''} · ${formatTokens(model.contextWindow)} context`)),
|
|
558
|
+
row(c.dim(` backend: ${cfg.endpointLabel}`)),
|
|
559
|
+
c.claude('╰' + '─'.repeat(w - 2) + '╯'),
|
|
560
|
+
];
|
|
561
|
+
lines.push('', c.dim(' Full access: every tool runs immediately, nothing is ever asked. File edits are checkpointed;'), c.dim(' /rewind puts files and/or the conversation back (shell-command changes are not tracked).'));
|
|
562
|
+
if (cfg.usesGateway && !readGatewayLogin(cfg.gatewayUrl)) {
|
|
563
|
+
lines.push('', c.yellow(` ⚠ Not logged in to the gateway ${cfg.gatewayUrl}. Run /login.`));
|
|
564
|
+
} else if (cfg.provider === 'api' && !cfg.authToken && !cfg.apiKey) {
|
|
565
|
+
lines.push('', c.yellow(' ⚠ No API credentials found. Set ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY in .env, ~/.claude-pro/settings.json or the environment.'));
|
|
566
|
+
}
|
|
567
|
+
const mcp = this.runtime.mcp.status();
|
|
568
|
+
if (mcp.length) {
|
|
569
|
+
const ok = mcp.filter((m) => m.status === 'connected').length;
|
|
570
|
+
lines.push(c.dim(` MCP: ${ok}/${mcp.length} servers connected${ok < mcp.length ? ' · /mcp for details' : ''}`));
|
|
571
|
+
}
|
|
572
|
+
lines.push('', c.dim(' Tips for getting started:'), c.dim(' 1. Ask questions, edit files, or run commands'), c.dim(' 2. Be as specific as you would with another engineer for the best results'), c.dim(` 3. Run /init to create a CLAUDE.md file with instructions for ${APP_NAME}`), '');
|
|
573
|
+
this.screen.log(lines);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// ---------------------------------------------------------------- input
|
|
577
|
+
|
|
578
|
+
// Say "copied" when the user copies text selected in this terminal, as Claude Code does. The terminal does the
|
|
579
|
+
// selecting and copying and tells nobody, so the clipboard is watched; focus events (which the terminal sends
|
|
580
|
+
// once asked to) keep copies made in other windows from being announced. Off with CLAUDE_PRO_COPY_NOTICE=0,
|
|
581
|
+
// and when not writing to the real terminal (the tests).
|
|
582
|
+
watchCopies() {
|
|
583
|
+
if (this.stdout !== process.stdout || !this.stdout.isTTY || /^(0|false|no)$/i.test(process.env.CLAUDE_PRO_COPY_NOTICE || '')) return;
|
|
584
|
+
this.focused = true;
|
|
585
|
+
this.clipboardWatch = watchClipboard((chars) => {
|
|
586
|
+
if (!this.focused || this.exiting) return;
|
|
587
|
+
this.flash(chars ? `Copied ${chars.toLocaleString()} characters to the clipboard` : 'Copied to the clipboard');
|
|
588
|
+
});
|
|
589
|
+
this.stdout.write(FOCUS_EVENTS_ON);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
onKey(str, key) {
|
|
593
|
+
if (this.exiting) return;
|
|
594
|
+
if (key.sequence === '\x1b[I') { this.focused = true; return; }
|
|
595
|
+
if (key.sequence === '\x1b[O') { this.focused = false; return; }
|
|
596
|
+
if (key.ctrl && key.name === 'c') { this.onCtrlC(); return; }
|
|
597
|
+
const dialog = this.dialogs[0];
|
|
598
|
+
if (dialog) {
|
|
599
|
+
dialog.handleKey(str, key);
|
|
600
|
+
this.screen.schedule();
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
if (key.name === 'tab' && key.shift) { this.runtime.modes.toggle(); this.screen.schedule(); return; }
|
|
604
|
+
// alt+v like Claude Code on Windows (ctrl+v is the terminal's own text paste there).
|
|
605
|
+
if ((key.meta || key.ctrl) && key.name === 'v') { this.pasteImage(); return; }
|
|
606
|
+
if (key.ctrl && key.name === 'o') {
|
|
607
|
+
this.verbose = !this.verbose;
|
|
608
|
+
this.notice(this.verbose ? 'Verbose mode on: showing thinking and full tool output' : 'Verbose mode off', 'info');
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (key.ctrl && key.name === 't') { this.showTodos = !this.showTodos; this.screen.schedule(); return; }
|
|
612
|
+
if (key.ctrl && key.name === 'l') { this.screen.clearScreen(); return; }
|
|
613
|
+
if (key.ctrl && key.name === 'd' && !this.editor.text) { this.exit(0); return; }
|
|
614
|
+
if (key.name === 'escape' && this.busy) { this.interrupt(); return; }
|
|
615
|
+
this.exitArmed = 0;
|
|
616
|
+
this.editor.handleKey(str, key);
|
|
617
|
+
this.screen.schedule();
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
onCtrlC() {
|
|
621
|
+
if (this.dialogs.length) {
|
|
622
|
+
// Cancel the dialog and interrupt the run.
|
|
623
|
+
for (const d of this.dialogs.splice(0)) d.resolve(d.cancelValue);
|
|
624
|
+
this.interrupt();
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (this.busy) { this.interrupt(); return; }
|
|
628
|
+
if (this.editor.text) { this.editor.clear(); this.screen.schedule(); return; }
|
|
629
|
+
if (Date.now() - this.exitArmed < 1500) { this.exit(0); return; }
|
|
630
|
+
this.exitArmed = Date.now();
|
|
631
|
+
this.screen.schedule();
|
|
632
|
+
setTimeout(() => this.screen.schedule(), 1600);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
interrupt() {
|
|
636
|
+
if (!this.controller || this.controller.signal.aborted) return;
|
|
637
|
+
this.controller.abort();
|
|
638
|
+
this.screen.schedule();
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
suggest(text, cursor) {
|
|
642
|
+
if (text.startsWith('/') && !/\s/.test(text) && cursor === text.length) {
|
|
643
|
+
const q = text.slice(1).toLowerCase();
|
|
644
|
+
const all = [...this.commands.values()].filter((cmd) => !cmd.hidden);
|
|
645
|
+
const pref = all.filter((cmd) => cmd.name.startsWith(q) || cmd.aliases?.some((a) => a.startsWith(q)));
|
|
646
|
+
const incl = q ? all.filter((cmd) => !pref.includes(cmd) && cmd.name.includes(q)) : [];
|
|
647
|
+
const items = [...pref, ...incl].map((cmd) => ({
|
|
648
|
+
value: '/' + cmd.name,
|
|
649
|
+
label: '/' + cmd.name + (cmd.argHint ? ' ' + cmd.argHint : ''),
|
|
650
|
+
description: cmd.description,
|
|
651
|
+
submitOnEnter: !cmd.argRequired,
|
|
652
|
+
}));
|
|
653
|
+
if (items.length === 1 && items[0].value === text && !items[0].submitOnEnter) return null;
|
|
654
|
+
return { items, from: 0, to: text.length };
|
|
655
|
+
}
|
|
656
|
+
// The mention being typed: `@name`, or `@"name with spaces` once a quote has been opened.
|
|
657
|
+
const before = text.slice(0, cursor);
|
|
658
|
+
const m = /(^|\s)@(?:"([^"@]*)|([^\s@"]*))$/.exec(before);
|
|
659
|
+
if (m) {
|
|
660
|
+
const quoted = m[2] !== undefined;
|
|
661
|
+
const q = quoted ? m[2] : m[3];
|
|
662
|
+
const files = this.fileIndex.search(q);
|
|
663
|
+
// At the start of the message `@name` sends it to another session.
|
|
664
|
+
const peers = !quoted && m.index === 0 && this.peers
|
|
665
|
+
? this.peers.peers().filter((p) => p.name.toLowerCase().startsWith(q.toLowerCase()))
|
|
666
|
+
.map((p) => ({ value: '@' + p.name, label: `@${p.name}`, description: `session · ${displayPath(p.cwd, this.cwd)}` }))
|
|
667
|
+
: [];
|
|
668
|
+
if (!files.length && !peers.length) return null;
|
|
669
|
+
// A name with spaces has to be quoted for the mention to be read back as one file. A directory
|
|
670
|
+
// leaves the quote open, so that completion goes on inside it; a file closes it.
|
|
671
|
+
const value = (f) => (quoted || /\s/.test(f) ? (f.endsWith('/') ? `@"${f}` : `@"${f}"`) : '@' + f);
|
|
672
|
+
return { items: [...peers, ...files.map((f) => ({ value: value(f), label: f, noSpace: f.endsWith('/') }))], from: cursor - q.length - (quoted ? 2 : 1), to: cursor };
|
|
673
|
+
}
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
onSubmit(value, raw, images = []) {
|
|
678
|
+
const text = value.trim();
|
|
679
|
+
if (!text) { this.screen.schedule(); return; }
|
|
680
|
+
appendHistory(this.cwd, raw);
|
|
681
|
+
if (this.sendTypedToPeer(text)) return;
|
|
682
|
+
if (this.busy) {
|
|
683
|
+
if (text.startsWith('/')) { this.notice('Wait for the current task to finish (or press Esc) before running commands.', 'warn'); return; }
|
|
684
|
+
this.queue.push({ text: value, images });
|
|
685
|
+
this.screen.schedule();
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (images.length) { this.startTurn(value, { images }); return; }
|
|
689
|
+
if (text === '?') { this.screen.log(this.shortcutsHelp()); return; }
|
|
690
|
+
if (raw.startsWith('!')) { this.runBashMode(value.slice(1).trim()); return; }
|
|
691
|
+
if (raw.startsWith('#')) { this.addMemory(value.slice(1).trim()); return; }
|
|
692
|
+
if (text.startsWith('/')) {
|
|
693
|
+
const [name, ...rest] = text.slice(1).split(/\s+/);
|
|
694
|
+
const args = text.slice(1 + name.length).trim();
|
|
695
|
+
const cmd = this.commands.get(name.toLowerCase()) || [...this.commands.values()].find((x) => x.aliases?.includes(name.toLowerCase()));
|
|
696
|
+
if (cmd) {
|
|
697
|
+
this.screen.log(renderUserMessage(text, this.width));
|
|
698
|
+
Promise.resolve(cmd.run(args, rest)).catch((e) => this.logError(e)).finally(() => this.screen.schedule());
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if (!/^\/[\w:-]+$/.test(text.split(/\s/)[0])) { this.startTurn(value); return; } // e.g. a path like /usr/bin
|
|
702
|
+
this.notice(`Unknown command: /${name}. Type /help for the list of commands.`, 'warn');
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
this.startTurn(value);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// `@name message`, name being another session: the message goes to that session, as a task for it.
|
|
709
|
+
sendTypedToPeer(text) {
|
|
710
|
+
const m = /^@([\w.-]+)\s+([\s\S]+)$/.exec(text);
|
|
711
|
+
if (!m || !this.peers) return false;
|
|
712
|
+
const peer = findPeer(this.peers.peers(), m[1]);
|
|
713
|
+
if (!peer || peer.name.toLowerCase() !== m[1].toLowerCase()) return false;
|
|
714
|
+
this.screen.log(renderUserMessage(text, this.width));
|
|
715
|
+
this.sendToPeer(peer, m[2].trim()).then(
|
|
716
|
+
() => this.screen.log(c.dim(` ⎿ Sent to @${peer.name} (${displayPath(peer.cwd, this.cwd)}); its answer will be shown here`), ''),
|
|
717
|
+
(e) => this.notice(e.message, 'warn'),
|
|
718
|
+
);
|
|
719
|
+
return true;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
shortcutsHelp() {
|
|
723
|
+
const rows = [
|
|
724
|
+
['! for bash mode', 'double tap esc to clear input'],
|
|
725
|
+
['/ for commands', 'shift + tab to cycle permission modes'],
|
|
726
|
+
['@ for file paths or sessions', 'ctrl + o to toggle verbose output'],
|
|
727
|
+
['# to memorize', 'ctrl + t to toggle todos'],
|
|
728
|
+
['\\⏎ or alt+⏎ for newline', 'ctrl + l to redraw, ctrl + d to exit'],
|
|
729
|
+
['alt + v to paste an image', ''],
|
|
730
|
+
];
|
|
731
|
+
return rows.map(([a, b]) => c.dim(' ' + padEnd(a, 30) + b));
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// ---------------------------------------------------------------- turns
|
|
735
|
+
|
|
736
|
+
// Where a mentioned path is (null when there is no such file): relative to the project root, which is
|
|
737
|
+
// where the autocomplete lists files from, or else relative to where the shell is now (a `cd` in a
|
|
738
|
+
// Bash tool call moves that). Nothing at all is not a path: it would mean the directory itself.
|
|
739
|
+
resolveMention(ref) {
|
|
740
|
+
if (!ref.trim()) return null;
|
|
741
|
+
for (const base of [this.cwd, this.agent.cwd]) {
|
|
742
|
+
const file = resolvePath(ref, base);
|
|
743
|
+
if (fs.existsSync(file)) return file;
|
|
744
|
+
}
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// The file a mention `@...` stands for. A quoted name is exactly what is inside the quotes. A plain name
|
|
749
|
+
// that exists as written is that file; otherwise the punctuation prose puts behind a mention ("see
|
|
750
|
+
// @a.js, then", "(@a.js)") is not part of it.
|
|
751
|
+
mentionedFile(token) {
|
|
752
|
+
if (token.startsWith('"')) return this.resolveMention(token.slice(1, -1));
|
|
753
|
+
return this.resolveMention(token) || this.resolveMention(token.replace(/[,.;:!?)]+$/, ''));
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Expand @file mentions into attached file contents, like Claude Code: what the model would have
|
|
757
|
+
// seen had it called Read itself. Going through the Read tool is what keeps this honest: binary files
|
|
758
|
+
// are refused, images become images, and the file is recorded as read, as only partly read when it
|
|
759
|
+
// was cut (so Write will not overwrite lines the model was never shown).
|
|
760
|
+
async attachments(text) {
|
|
761
|
+
const blocks = [];
|
|
762
|
+
const seen = new Set();
|
|
763
|
+
const read = this.runtime.tools.find((t) => t.name === 'Read');
|
|
764
|
+
const skipped = (file, why) => this.screen.log(c.dim(` ⎿ Skipped ${displayPath(file, this.cwd)}: ${why}`));
|
|
765
|
+
for (const m of text.matchAll(/(?:^|\s)@("[^"]+"|[^\s"]+)/g)) {
|
|
766
|
+
const file = this.mentionedFile(m[1]);
|
|
767
|
+
if (!file || seen.has(file)) continue; // @someone, @..., a file that is not there: not a mention
|
|
768
|
+
seen.add(file);
|
|
769
|
+
let st;
|
|
770
|
+
try { st = fs.statSync(file); } catch { continue; }
|
|
771
|
+
if (st.isDirectory()) {
|
|
772
|
+
let entries;
|
|
773
|
+
try {
|
|
774
|
+
const all = fs.readdirSync(file, { withFileTypes: true });
|
|
775
|
+
entries = all.slice(0, 200).map((e) => (e.isDirectory() ? e.name + '/' : e.name));
|
|
776
|
+
if (all.length > 200) entries.push(`(${all.length - 200} more entries not shown)`);
|
|
777
|
+
} catch (e) { skipped(file, e.message); continue; }
|
|
778
|
+
blocks.push({ type: 'text', text: `<system-reminder>Called the Bash tool with the following input: {"command":"ls ${file}"}\nResult:\n${entries.join('\n')}</system-reminder>` });
|
|
779
|
+
this.screen.log(c.dim(` ⎿ Listed directory ${displayPath(file, this.cwd)}/`));
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
if (!read || !st.isFile()) continue;
|
|
783
|
+
const isImage = /\.(png|jpe?g|gif|webp)$/i.test(file);
|
|
784
|
+
if (!isImage && st.size >= 512 * 1024) { skipped(file, `${Math.round(st.size / 1024)}KB is too large to attach; ask for the part you need`); continue; }
|
|
785
|
+
const input = { file_path: file, limit: 2000 };
|
|
786
|
+
let result;
|
|
787
|
+
try { result = await read.call(input, this.agent.makeCtx()); } catch (e) { skipped(file, e.message); continue; }
|
|
788
|
+
if (result.isError) { skipped(file, String(result.content).split('\n')[0]); continue; }
|
|
789
|
+
blocks.push(typeof result.content === 'string'
|
|
790
|
+
? { type: 'text', text: `<system-reminder>Called the Read tool with the following input: ${JSON.stringify(input)}\nResult of calling the Read tool:\n${result.content}</system-reminder>` }
|
|
791
|
+
: { type: 'text', text: `<system-reminder>The user attached ${file}:</system-reminder>` });
|
|
792
|
+
if (typeof result.content !== 'string') blocks.push(...result.content);
|
|
793
|
+
this.screen.log(c.dim(` ⎿ ${result.display?.summary || 'Read'}: ${displayPath(file, this.cwd)}`));
|
|
794
|
+
}
|
|
795
|
+
return blocks;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// `topic`: what to name the conversation's topic after (default: the message as shown).
|
|
799
|
+
// `peers`: tasks of other sessions that this turn answers.
|
|
800
|
+
async startTurn(prompt, { echo = true, display, images = [], topic, peers = [] } = {}) {
|
|
801
|
+
if (echo) this.screen.log(renderUserMessage(display ?? prompt, this.width));
|
|
802
|
+
this.refreshTopic(topic ?? display ?? prompt);
|
|
803
|
+
let content;
|
|
804
|
+
try {
|
|
805
|
+
content = [{ type: 'text', text: prompt }, ...images.map(imageBlock), ...(await this.attachments(prompt))];
|
|
806
|
+
} catch (e) {
|
|
807
|
+
this.logError(e); // nobody awaits this method: an error left to escape would end the program
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
await this.runAgent(content, { peers });
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// alt+v (ctrl+v where the terminal passes it through): attach an image from the clipboard.
|
|
814
|
+
async pasteImage() {
|
|
815
|
+
if (this.readingClipboard) return;
|
|
816
|
+
this.readingClipboard = true;
|
|
817
|
+
this.flash('Reading image from clipboard…');
|
|
818
|
+
try {
|
|
819
|
+
const r = await readClipboardImage();
|
|
820
|
+
if (r.image) {
|
|
821
|
+
const label = this.editor.insertImage(r.image);
|
|
822
|
+
const size = r.image.width ? `${r.image.width}×${r.image.height}, ` : '';
|
|
823
|
+
this.flash(`${label} attached (${size}${formatTokens(r.image.bytes).replace(/k$/, 'KB')})`);
|
|
824
|
+
} else {
|
|
825
|
+
this.flash(r.error || 'No image found in clipboard. Copy an image (e.g. Win+Shift+S) and press alt+v again.', 'warn');
|
|
826
|
+
}
|
|
827
|
+
} catch (e) {
|
|
828
|
+
this.flash(`Could not read the clipboard: ${e.message}`, 'warn');
|
|
829
|
+
} finally {
|
|
830
|
+
this.readingClipboard = false;
|
|
831
|
+
this.screen.schedule();
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// A short-lived message in the footer (not written to the transcript).
|
|
836
|
+
flash(text, kind = 'info') {
|
|
837
|
+
this.flashMessage = { text, kind, until: Date.now() + 4000 };
|
|
838
|
+
this.screen.schedule();
|
|
839
|
+
clearTimeout(this.flashTimer);
|
|
840
|
+
this.flashTimer = setTimeout(() => this.screen.schedule(), 4100);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
async runAgent(content, { peers = [] } = {}) {
|
|
844
|
+
this.busy = true;
|
|
845
|
+
this.turnPeers.push(...peers);
|
|
846
|
+
let result = null;
|
|
847
|
+
let error = null;
|
|
848
|
+
this.controller = new AbortController();
|
|
849
|
+
this.turnStart = Date.now();
|
|
850
|
+
this.turnTokens = 0;
|
|
851
|
+
this.verb = VERBS[Math.floor(Math.random() * VERBS.length)];
|
|
852
|
+
this.screen.schedule();
|
|
853
|
+
try {
|
|
854
|
+
result = await this.agent.run(content, { signal: this.controller.signal });
|
|
855
|
+
if (result.interrupted) {
|
|
856
|
+
this.flushStreaming();
|
|
857
|
+
this.flushActive(true);
|
|
858
|
+
this.screen.log(resultLines([c.red('Interrupted by user')]), '');
|
|
859
|
+
if (this.restartRequest) {
|
|
860
|
+
this.restartRequest = null;
|
|
861
|
+
this.notice('The restart was cancelled with the turn. Run /restart to load the new code.');
|
|
862
|
+
}
|
|
863
|
+
if (this.sourceChanged) this.skipAutoReload = true;
|
|
864
|
+
}
|
|
865
|
+
} catch (e) {
|
|
866
|
+
error = e;
|
|
867
|
+
this.flushStreaming();
|
|
868
|
+
this.logError(e);
|
|
869
|
+
} finally {
|
|
870
|
+
this.flushStreaming();
|
|
871
|
+
this.flushActive(true);
|
|
872
|
+
this.answerPeers(result, error);
|
|
873
|
+
this.busy = false;
|
|
874
|
+
this.controller = null;
|
|
875
|
+
this.status = null;
|
|
876
|
+
this.rateWait = null;
|
|
877
|
+
this.screen.schedule();
|
|
878
|
+
}
|
|
879
|
+
this.sendWhatWaited();
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// The app has stopped being busy (a turn, a compaction, a command): send what was typed in the
|
|
883
|
+
// meantime as the next turn, or else tell the model about background agents that finished after the
|
|
884
|
+
// last check. Everything that makes the app busy has to end with this, or what waited stays queued.
|
|
885
|
+
sendWhatWaited() {
|
|
886
|
+
if (this.busy || this.exiting) return;
|
|
887
|
+
// A restart that was asked for goes first; what waited is carried over and sent by the new process.
|
|
888
|
+
if (this.restartRequest) { this.restartNow(); return; }
|
|
889
|
+
if (this.queue.length) {
|
|
890
|
+
const next = this.queue.splice(0);
|
|
891
|
+
this.startTurn(next.map((q) => q.text).join('\n\n'), {
|
|
892
|
+
images: next.flatMap((q) => q.images),
|
|
893
|
+
display: next.map((q) => q.display ?? q.text).join('\n\n'),
|
|
894
|
+
peers: next.map((q) => q.peer).filter(Boolean),
|
|
895
|
+
});
|
|
896
|
+
} else if (this.agent.notifications.length) {
|
|
897
|
+
this.runAgent(this.agent.drainNotifications());
|
|
898
|
+
} else {
|
|
899
|
+
this.maybeAutoReload();
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// A background agent ended. If nothing is running, start a turn so the model sees its
|
|
904
|
+
// report right away; otherwise the running turn picks it up at its next step.
|
|
905
|
+
backgroundTaskDone(task) {
|
|
906
|
+
const secs = Math.round((task.ended - task.started) / 1000);
|
|
907
|
+
const color = task.status === 'completed' ? c.green : c.red;
|
|
908
|
+
this.screen.log(`${color(DOT)} ${c.bold('Background agent')} ${task.id} ${c.dim(`"${task.description}"`)} ${task.status} ${c.dim(`(${formatDuration(secs * 1000)})`)}`, '');
|
|
909
|
+
if (!this.busy && !this.exiting && this.agent.notifications.length) this.runAgent(this.agent.drainNotifications());
|
|
910
|
+
else this.screen.schedule();
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
async runBashMode(command) {
|
|
914
|
+
if (!command) return;
|
|
915
|
+
this.screen.log(c.magenta('! ') + command);
|
|
916
|
+
const tool = this.runtime.tools.find((t) => t.name === 'Bash');
|
|
917
|
+
this.busy = true;
|
|
918
|
+
this.controller = new AbortController();
|
|
919
|
+
this.status = 'Running';
|
|
920
|
+
this.turnStart = Date.now();
|
|
921
|
+
this.turnTokens = 0;
|
|
922
|
+
const ctx = this.agent.makeCtx(this.controller.signal);
|
|
923
|
+
const entry = { tool, input: { command }, status: 'running', progress: '', sub: [], subCount: 0, ctx, started: Date.now() };
|
|
924
|
+
this.active.push(entry);
|
|
925
|
+
let result;
|
|
926
|
+
try {
|
|
927
|
+
ctx.onProgress = (p) => { entry.progress = p; };
|
|
928
|
+
result = await tool.call({ command }, ctx);
|
|
929
|
+
} catch {
|
|
930
|
+
const partial = (entry.progress || '').replace(/\r\n/g, '\n').trimEnd();
|
|
931
|
+
result = { isError: true, content: `${partial ? partial + '\n' : ''}Interrupted by user`, interrupted: true };
|
|
932
|
+
}
|
|
933
|
+
this.active = this.active.filter((e) => e !== entry);
|
|
934
|
+
const out = typeof result.content === 'string' ? result.content : '';
|
|
935
|
+
let lines = out.split('\n');
|
|
936
|
+
if (lines.length > 40) lines = [...lines.slice(0, 20), c.dim(`… +${lines.length - 40} lines`), ...lines.slice(-20)];
|
|
937
|
+
this.screen.log(resultLines(lines.map((l) => (result.isError && l === lines[lines.length - 1] ? c.red(l) : c.dim(l)))), '');
|
|
938
|
+
this.agent.pushMessage({
|
|
939
|
+
role: 'user',
|
|
940
|
+
content: [{ type: 'text', text: `<bash-input>${command}</bash-input>\n<bash-stdout>${out}</bash-stdout>\n<system-reminder>The user ran this command directly in bash mode. Take its output into account in later answers.</system-reminder>` }],
|
|
941
|
+
});
|
|
942
|
+
this.busy = false;
|
|
943
|
+
this.controller = null;
|
|
944
|
+
this.status = null;
|
|
945
|
+
this.screen.schedule();
|
|
946
|
+
this.sendWhatWaited();
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
addMemory(text) {
|
|
950
|
+
if (!text) return;
|
|
951
|
+
const files = {
|
|
952
|
+
project: path.join(this.cwd, 'CLAUDE.md'),
|
|
953
|
+
local: path.join(this.cwd, 'CLAUDE.local.md'),
|
|
954
|
+
user: path.join(CONFIG_DIR, 'CLAUDE.md'),
|
|
955
|
+
};
|
|
956
|
+
this.showDialog(new SelectDialog({
|
|
957
|
+
title: 'Save memory',
|
|
958
|
+
body: [c.dim(truncate(text, this.width - 10))],
|
|
959
|
+
question: 'Where should this memory be saved?',
|
|
960
|
+
color: c.blue,
|
|
961
|
+
options: [
|
|
962
|
+
{ label: `Project memory ${c.dim(displayPath(files.project, this.cwd))}`, value: 'project', description: 'Checked in and shared with your team' },
|
|
963
|
+
{ label: `Project memory (local) ${c.dim(displayPath(files.local, this.cwd))}`, value: 'local', description: 'Just for you in this project' },
|
|
964
|
+
{ label: `User memory ${c.dim(displayPath(files.user, this.cwd))}`, value: 'user', description: 'Your instructions for all projects' },
|
|
965
|
+
],
|
|
966
|
+
})).then((choice) => {
|
|
967
|
+
if (!choice) return;
|
|
968
|
+
const file = files[choice];
|
|
969
|
+
// This runs outside any handler: an error here (a read-only directory) would end the program.
|
|
970
|
+
try {
|
|
971
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
972
|
+
let existing = '';
|
|
973
|
+
try { existing = fs.readFileSync(file, 'utf8'); } catch { /* new */ }
|
|
974
|
+
fs.appendFileSync(file, `${existing && !existing.endsWith('\n') ? '\n' : ''}- ${text}\n`);
|
|
975
|
+
// The system prompt stays as it is until the conversation starts over (/clear or a compaction): a prompt
|
|
976
|
+
// that changes in the middle of it cannot be read from the prompt cache.
|
|
977
|
+
this.runtime.deferSystemPromptRefresh();
|
|
978
|
+
this.screen.log(resultLines([c.dim(`Saved to ${displayPath(file, this.cwd)} · the model sees it from the next /clear or /compact`)]));
|
|
979
|
+
} catch (e) {
|
|
980
|
+
this.notice(`Could not save the memory to ${displayPath(file, this.cwd)}: ${e.message}`, 'error');
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// `quiet`: do not print the transcript (a restart: it is still on screen).
|
|
986
|
+
loadSession(file, id, { quiet = false } = {}) {
|
|
987
|
+
const loaded = SessionStore.load(file);
|
|
988
|
+
this.store = new SessionStore(this.cwd, id);
|
|
989
|
+
this.topic = loaded.topic;
|
|
990
|
+
this.firstPrompt = loaded.title;
|
|
991
|
+
this.agent.messages = loaded.messages;
|
|
992
|
+
this.agent.restoreFileState(loaded.messages, loaded.times);
|
|
993
|
+
this.agent.lastActivityAt = loaded.lastAt; // a long pause before this turn clears old tool results, as it would have
|
|
994
|
+
this.agent.lastContextTokens = estimateTokens(loaded.messages);
|
|
995
|
+
// Continue behind the system prompt the conversation had (and its cache), not a freshly built one.
|
|
996
|
+
this.store.systemPrompt = loaded.systemPrompt;
|
|
997
|
+
this.runtime.resumeSystemPrompt(loaded.systemPrompt);
|
|
998
|
+
if (!this.topic) this.refreshTopic(this.latestPrompt()); // a session from before topics were kept
|
|
999
|
+
if (quiet) return;
|
|
1000
|
+
this.screen.log(c.dim(`Resumed session ${id} (${loaded.messages.length} messages)`), '');
|
|
1001
|
+
this.printTranscript(loaded.messages.slice(-30));
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
printTranscript(messages) {
|
|
1005
|
+
const toolMap = new Map(this.runtime.tools.map((t) => [t.name, t]));
|
|
1006
|
+
const ctx = this.agent.makeCtx();
|
|
1007
|
+
for (const m of messages) {
|
|
1008
|
+
const blocks = typeof m.content === 'string' ? [{ type: 'text', text: m.content }] : m.content;
|
|
1009
|
+
for (const b of blocks) {
|
|
1010
|
+
if (m.role === 'user' && b.type === 'text' && !b.text.startsWith('<system-reminder>')) this.screen.log(renderUserMessage(b.text.slice(0, 2000), this.width));
|
|
1011
|
+
if (m.role === 'assistant' && b.type === 'text') this.screen.log(renderMarkdown(b.text, { width: this.width - 1, firstPrefix: `${DOT} `, indent: ' ' }), '');
|
|
1012
|
+
if (m.role === 'assistant' && b.type === 'tool_use') {
|
|
1013
|
+
const tool = toolMap.get(b.name) || { name: b.name };
|
|
1014
|
+
this.screen.log(toolHeader(tool, b.input, ctx, { width: this.width }));
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
this.screen.log('');
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
newSession() {
|
|
1022
|
+
this.store = new SessionStore(this.cwd);
|
|
1023
|
+
this.topic = null;
|
|
1024
|
+
this.firstPrompt = null;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// ---------------------------------------------------------------- dialogs
|
|
1028
|
+
|
|
1029
|
+
showDialog(dialog) {
|
|
1030
|
+
return new Promise((resolve) => {
|
|
1031
|
+
const prev = dialog.onResolve;
|
|
1032
|
+
dialog.onResolve = (v) => {
|
|
1033
|
+
this.dialogs = this.dialogs.filter((d) => d !== dialog);
|
|
1034
|
+
prev?.(v);
|
|
1035
|
+
resolve(v);
|
|
1036
|
+
this.screen.schedule();
|
|
1037
|
+
};
|
|
1038
|
+
this.dialogs.push(dialog);
|
|
1039
|
+
this.screen.schedule();
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
async approvePlan(plan) {
|
|
1044
|
+
this.flushStreaming();
|
|
1045
|
+
const md = renderMarkdown(plan, { width: this.width - 8 });
|
|
1046
|
+
const max = Math.max(8, this.screen.height - 14);
|
|
1047
|
+
const body = md.length > max ? [...md.slice(0, max), c.dim(`… +${md.length - max} lines (the full plan is printed above)`)] : md;
|
|
1048
|
+
if (md.length > max) this.screen.log(c.plan(`${DOT} Plan:`), ...md.map((l) => ' ' + l), '');
|
|
1049
|
+
const choice = await this.showDialog(new SelectDialog({
|
|
1050
|
+
title: 'Ready to code?',
|
|
1051
|
+
body: ["Here is the plan:", '', ...body],
|
|
1052
|
+
question: 'Would you like to proceed?',
|
|
1053
|
+
color: c.plan,
|
|
1054
|
+
cancelValue: 'reject',
|
|
1055
|
+
options: [
|
|
1056
|
+
{ label: 'Yes, start coding', value: 'full' },
|
|
1057
|
+
{ label: 'No, keep planning', value: 'reject' },
|
|
1058
|
+
],
|
|
1059
|
+
}));
|
|
1060
|
+
return choice;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
async askQuestion(q) {
|
|
1064
|
+
const answer = await this.showDialog(new SelectDialog({
|
|
1065
|
+
title: q.header || 'Question',
|
|
1066
|
+
question: c.bold(q.question),
|
|
1067
|
+
color: c.blue,
|
|
1068
|
+
multi: !!q.multiSelect,
|
|
1069
|
+
otherOption: true,
|
|
1070
|
+
options: (q.options || []).map((o) => ({ label: o.label, value: o.label, description: o.description })),
|
|
1071
|
+
}));
|
|
1072
|
+
if (answer === null || answer === undefined) return null;
|
|
1073
|
+
return Array.isArray(answer) ? answer.join(', ') : answer;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// ---------------------------------------------------------------- agent IO
|
|
1077
|
+
|
|
1078
|
+
isMain(agent) { return !agent || agent === this.runtime?.agent; }
|
|
1079
|
+
|
|
1080
|
+
streamEvent(ev, agent) {
|
|
1081
|
+
if (!this.isMain(agent)) return;
|
|
1082
|
+
const w = this.width - 1;
|
|
1083
|
+
switch (ev.type) {
|
|
1084
|
+
case 'block_start':
|
|
1085
|
+
if (ev.block.type === 'text') {
|
|
1086
|
+
this.flushThinking();
|
|
1087
|
+
this.md = new MarkdownStream({ width: w, firstPrefix: `${DOT} `, indent: ' ' });
|
|
1088
|
+
} else if (ev.block.type === 'thinking') {
|
|
1089
|
+
this.thinking = { start: Date.now(), text: '', md: this.verbose ? new MarkdownStream({ width: w, firstPrefix: c.dim('✻ '), indent: ' ', style: (s) => c.dim(c.italic(s)) }) : null };
|
|
1090
|
+
} else if (ev.block.type === 'tool_use') {
|
|
1091
|
+
this.flushThinking();
|
|
1092
|
+
this.pendingToolBlocks.set(ev.index, { name: ev.block.name, partial: '' });
|
|
1093
|
+
}
|
|
1094
|
+
break;
|
|
1095
|
+
case 'text':
|
|
1096
|
+
this.turnTokens += estimateTokens(ev.text);
|
|
1097
|
+
if (this.md) {
|
|
1098
|
+
const lines = this.md.write(ev.text);
|
|
1099
|
+
if (lines.length) this.screen.log(lines);
|
|
1100
|
+
}
|
|
1101
|
+
break;
|
|
1102
|
+
case 'thinking':
|
|
1103
|
+
this.turnTokens += estimateTokens(ev.text);
|
|
1104
|
+
if (this.thinking) {
|
|
1105
|
+
this.thinking.text += ev.text;
|
|
1106
|
+
if (this.thinking.md) {
|
|
1107
|
+
const lines = this.thinking.md.write(ev.text);
|
|
1108
|
+
if (lines.length) this.screen.log(lines);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
break;
|
|
1112
|
+
case 'tool_input': {
|
|
1113
|
+
this.turnTokens += 1;
|
|
1114
|
+
const p = this.pendingToolBlocks.get(ev.index);
|
|
1115
|
+
if (p) p.partial = ev.partial;
|
|
1116
|
+
break;
|
|
1117
|
+
}
|
|
1118
|
+
case 'block_stop':
|
|
1119
|
+
if (ev.block.type === 'text' && this.md) {
|
|
1120
|
+
const lines = this.md.end();
|
|
1121
|
+
if (lines.length) this.screen.log(lines);
|
|
1122
|
+
this.screen.log('');
|
|
1123
|
+
this.md = null;
|
|
1124
|
+
} else if (ev.block.type === 'thinking') {
|
|
1125
|
+
this.flushThinking();
|
|
1126
|
+
} else if (ev.block.type === 'tool_use') {
|
|
1127
|
+
this.pendingToolBlocks.delete(ev.index);
|
|
1128
|
+
}
|
|
1129
|
+
break;
|
|
1130
|
+
case 'retry':
|
|
1131
|
+
this.notice(`API error: ${truncate(ev.error.message, 200)} · retrying in ${Math.round(ev.delayMs / 1000)}s (attempt ${ev.attempt}${ev.maxRetries ? `/${ev.maxRetries}` : ''})`, 'warn');
|
|
1132
|
+
break;
|
|
1133
|
+
case 'rate_limit_wait':
|
|
1134
|
+
case 'rate_limit_resume':
|
|
1135
|
+
this.rateLimitEvent(ev);
|
|
1136
|
+
break;
|
|
1137
|
+
case 'reset':
|
|
1138
|
+
this.md = null;
|
|
1139
|
+
this.thinking = null;
|
|
1140
|
+
this.pendingToolBlocks.clear();
|
|
1141
|
+
break;
|
|
1142
|
+
default:
|
|
1143
|
+
break;
|
|
1144
|
+
}
|
|
1145
|
+
this.screen.schedule();
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// A rate limit pause or its end, from a model request of the main agent (a turn or a compaction).
|
|
1149
|
+
rateLimitEvent(ev) {
|
|
1150
|
+
if (ev.type === 'rate_limit_resume') {
|
|
1151
|
+
this.rateWait = null;
|
|
1152
|
+
this.notice('The limit has cleared; continuing.');
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
// Say it once; later waits of the same pause only move the countdown (unless the reset moved a lot).
|
|
1156
|
+
const prev = this.rateWait;
|
|
1157
|
+
this.rateWait = { resumeAt: ev.resumeAt, label: ev.label, polling: ev.polling, waitingForLogin: Boolean(ev.waitingForLogin) };
|
|
1158
|
+
if (shouldAnnounce(prev, ev)) this.notice(`${describePause(ev)} Esc cancels.`, 'warn');
|
|
1159
|
+
this.screen.schedule();
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
flushThinking() {
|
|
1163
|
+
const t = this.thinking;
|
|
1164
|
+
if (!t) return;
|
|
1165
|
+
this.thinking = null;
|
|
1166
|
+
if (t.md) {
|
|
1167
|
+
const lines = t.md.end();
|
|
1168
|
+
if (lines.length) this.screen.log(lines);
|
|
1169
|
+
this.screen.log('');
|
|
1170
|
+
} else if (t.text.trim()) {
|
|
1171
|
+
const secs = Math.max(1, Math.round((Date.now() - t.start) / 1000));
|
|
1172
|
+
this.screen.log(c.dim(c.italic(`✻ Thought for ${secs}s (ctrl+o to show thinking)`)), '');
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
flushStreaming() {
|
|
1177
|
+
this.flushThinking();
|
|
1178
|
+
if (this.md) {
|
|
1179
|
+
const lines = this.md.end();
|
|
1180
|
+
if (lines.length) this.screen.log(lines, '');
|
|
1181
|
+
this.md = null;
|
|
1182
|
+
}
|
|
1183
|
+
this.pendingToolBlocks.clear();
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
toolBatchStart() {
|
|
1187
|
+
this.flushStreaming();
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
toolStart(tu, tool, input, agent) {
|
|
1191
|
+
if (!this.isMain(agent)) return {};
|
|
1192
|
+
const entry = { tu, tool, input, status: 'running', progress: '', sub: [], subCount: 0, ctx: agent.makeCtx(), started: Date.now() };
|
|
1193
|
+
this.active.push(entry);
|
|
1194
|
+
this.screen.schedule();
|
|
1195
|
+
return {
|
|
1196
|
+
progress: (p) => {
|
|
1197
|
+
if (typeof p === 'string') entry.progress = p;
|
|
1198
|
+
else if (p?.type === 'subtool') { entry.sub.push(p.header); entry.subCount++; }
|
|
1199
|
+
this.screen.schedule();
|
|
1200
|
+
},
|
|
1201
|
+
done: (result) => {
|
|
1202
|
+
entry.result = result;
|
|
1203
|
+
entry.status = result.isError ? 'error' : 'done';
|
|
1204
|
+
this.flushActive();
|
|
1205
|
+
},
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
toolRejected(tu, tool, message, agent) {
|
|
1210
|
+
if (!this.isMain(agent)) return;
|
|
1211
|
+
const ctx = agent.makeCtx();
|
|
1212
|
+
const entry = { tu, tool, input: tu.input, status: 'rejected', rejected: true, rejectMessage: message, ctx };
|
|
1213
|
+
this.screen.log(toolHeader(tool, tu.input, ctx, { status: 'rejected', width: this.width }), renderToolResult(entry, { width: this.width, cwd: this.cwd }), '');
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// Move finished tool entries (in call order) from the live region to the transcript.
|
|
1217
|
+
flushActive(all = false) {
|
|
1218
|
+
while (this.active.length && (all || this.active[0].status !== 'running')) {
|
|
1219
|
+
const e = this.active.shift();
|
|
1220
|
+
if (e.status === 'running') { e.status = 'error'; e.result = { isError: true, content: 'Interrupted', interrupted: true }; }
|
|
1221
|
+
const status = e.result?.isError ? 'error' : 'done';
|
|
1222
|
+
this.screen.log(toolHeader(e.tool, e.input, e.ctx, { status, width: this.width }), renderToolResult(e, { verbose: this.verbose, width: this.width, cwd: this.cwd }), '');
|
|
1223
|
+
}
|
|
1224
|
+
this.screen.schedule();
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// IO object for a sub-agent: reports its tool calls into the parent's Task entry.
|
|
1228
|
+
subagentIo({ def, parentCtx }) {
|
|
1229
|
+
const app = this;
|
|
1230
|
+
let paused = null; // the rate limit wait last announced for this agent
|
|
1231
|
+
// The spinner belongs to the main agent (a background agent can be paused while it keeps working),
|
|
1232
|
+
// so a sub-agent's pause is only announced, once, and its end too. This covers the agent's turns
|
|
1233
|
+
// and the compaction of its own conversation alike.
|
|
1234
|
+
const rateLimit = (ev) => {
|
|
1235
|
+
if (ev.type === 'rate_limit_wait') {
|
|
1236
|
+
if (shouldAnnounce(paused, ev)) app.notice(`[${def.name}] ${describePause(ev)}`, 'warn');
|
|
1237
|
+
paused = { resumeAt: ev.resumeAt, waitingForLogin: Boolean(ev.waitingForLogin) };
|
|
1238
|
+
}
|
|
1239
|
+
if (ev.type === 'rate_limit_resume') {
|
|
1240
|
+
paused = null;
|
|
1241
|
+
app.notice(`[${def.name}] The limit has cleared; continuing.`);
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
return {
|
|
1245
|
+
approvePlan: async () => 'reject',
|
|
1246
|
+
askQuestion: async () => null,
|
|
1247
|
+
notice: (text, kind) => app.notice(`[${def.name}] ${text}`, kind),
|
|
1248
|
+
toolStart(tu, tool, input, agent) {
|
|
1249
|
+
parentCtx.onProgress?.({ type: 'subtool', header: toolHeader(tool, input, agent.makeCtx(), { status: 'done', width: app.width - 8, dot: false }) });
|
|
1250
|
+
return {};
|
|
1251
|
+
},
|
|
1252
|
+
streamEvent(ev) {
|
|
1253
|
+
if (ev.type === 'text' || ev.type === 'thinking') app.turnTokens += estimateTokens(ev.text);
|
|
1254
|
+
if (ev.type === 'retry') app.notice(`[${def.name}] API error, retrying: ${truncate(ev.error.message, 120)}`, 'warn');
|
|
1255
|
+
rateLimit(ev);
|
|
1256
|
+
},
|
|
1257
|
+
compactProgress: rateLimit,
|
|
1258
|
+
takeQueued: () => [],
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
takeQueued() {
|
|
1263
|
+
if (!this.queue.length) return [];
|
|
1264
|
+
const items = this.queue.splice(0);
|
|
1265
|
+
for (const q of items) this.screen.log(renderUserMessage(q.display ?? q.text, this.width));
|
|
1266
|
+
this.turnPeers.push(...items.map((q) => q.peer).filter(Boolean));
|
|
1267
|
+
this.refreshTopic(items.map((q) => q.display ?? q.text).join('\n\n'));
|
|
1268
|
+
// Content blocks for the agent: each queued message's text followed by its images.
|
|
1269
|
+
return items.flatMap((q) => [{ type: 'text', text: q.text }, ...q.images.map(imageBlock)]);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
messageAdded(msg, agent) { if (this.isMain(agent)) this.store.message(msg); }
|
|
1273
|
+
systemPrompt(text, agent) { if (this.isMain(agent)) this.store.setSystemPrompt(text); }
|
|
1274
|
+
messagesReplaced(msgs, agent) { if (this.isMain(agent)) this.store.reset(msgs); }
|
|
1275
|
+
usage() { this.screen.schedule(); }
|
|
1276
|
+
|
|
1277
|
+
compactStart(auto, agent) {
|
|
1278
|
+
if (!this.isMain(agent)) return;
|
|
1279
|
+
this.flushStreaming();
|
|
1280
|
+
this.status = auto ? 'Auto-compacting conversation' : 'Compacting conversation';
|
|
1281
|
+
this.compactTokens = 0;
|
|
1282
|
+
this.screen.schedule();
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
compactProgress(ev) {
|
|
1286
|
+
if (ev.type === 'text' || ev.type === 'thinking') { this.turnTokens += estimateTokens(ev.text); this.screen.schedule(); }
|
|
1287
|
+
// A compaction can hit the limit too: say so instead of sitting on "Compacting conversation" for hours.
|
|
1288
|
+
else if (ev.type === 'rate_limit_wait' || ev.type === 'rate_limit_resume') this.rateLimitEvent(ev);
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
compactEnd(summary, agent, err) {
|
|
1292
|
+
if (!this.isMain(agent)) return;
|
|
1293
|
+
this.status = null;
|
|
1294
|
+
if (err) return;
|
|
1295
|
+
this.lastSummary = summary;
|
|
1296
|
+
this.screen.log(c.dim('✻ Conversation compacted (the summary is in context; /context to see usage)'), '');
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
notice(text, kind = 'info') {
|
|
1300
|
+
const color = kind === 'warn' ? c.yellow : kind === 'error' ? c.red : c.dim;
|
|
1301
|
+
// Long notices (the rate limit ones are) break between words, not wherever the terminal width falls.
|
|
1302
|
+
// A notice of several lines (a hook's warning, say) keeps its lines, each one indented and wrapped.
|
|
1303
|
+
const width = Math.max(20, this.width - 4);
|
|
1304
|
+
this.screen.log(String(text).split('\n').flatMap((line) => wordWrap(line, width)).map((line) => color(` ${line.trimEnd()}`)));
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
logError(e) {
|
|
1308
|
+
const msg = e?.message || String(e);
|
|
1309
|
+
this.screen.log(c.red(`${DOT} API Error: ${msg}`), '');
|
|
1310
|
+
if (process.env.CLAUDE_PRO_DEBUG && e?.stack) this.screen.log(c.dim(e.stack));
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// ---------------------------------------------------------------- live region
|
|
1314
|
+
|
|
1315
|
+
spinnerLine() {
|
|
1316
|
+
const frame = SPINNER[Math.floor(Date.now() / 120) % SPINNER.length];
|
|
1317
|
+
if (this.rateWait) {
|
|
1318
|
+
const w = this.rateWait;
|
|
1319
|
+
const left = humanDuration(w.resumeAt - Date.now()); // the same wording as the notice above it
|
|
1320
|
+
const esc = c.dim(`(${this.controller?.signal.aborted ? 'cancelling…' : 'esc to cancel'})`);
|
|
1321
|
+
if (w.waitingForLogin) return c.yellow(`${frame} Waiting for the Claude Code login to be renewed · checking again in ${left}`) + ' ' + esc;
|
|
1322
|
+
const label = w.label.charAt(0).toUpperCase() + w.label.slice(1);
|
|
1323
|
+
const when = w.polling ? `checking again in ${left}` : `resumes at ${formatClock(w.resumeAt)} (in ${left})`;
|
|
1324
|
+
return c.yellow(`${frame} ${label} reached · ${when}`) + ' ' + esc;
|
|
1325
|
+
}
|
|
1326
|
+
const secs = formatDuration(Date.now() - this.turnStart);
|
|
1327
|
+
const verb = this.status || (this.thinking ? 'Thinking' : this.verb);
|
|
1328
|
+
const aborted = this.controller?.signal.aborted;
|
|
1329
|
+
const parts = [secs];
|
|
1330
|
+
if (this.turnTokens) parts.push(`↓ ${formatTokens(this.turnTokens)} tokens`);
|
|
1331
|
+
parts.push(aborted ? 'interrupting…' : 'esc to interrupt');
|
|
1332
|
+
return c.claude(`${frame} ${verb}…`) + ' ' + c.dim(`(${parts.join(' · ')})`);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
renderActiveEntry(e, w) {
|
|
1336
|
+
const lines = [toolHeader(e.tool, e.input, e.ctx, { status: 'running', width: w })];
|
|
1337
|
+
if (e.tool.name === 'Task' || e.sub.length) {
|
|
1338
|
+
const shown = e.sub.slice(-3);
|
|
1339
|
+
const more = e.subCount - shown.length;
|
|
1340
|
+
const sub = shown.map((h) => c.dim(truncate(h, w - 8)));
|
|
1341
|
+
if (more > 0) sub.push(c.dim(`+${more} more tool use${more === 1 ? '' : 's'}`));
|
|
1342
|
+
if (!sub.length) sub.push(c.dim('Initializing…'));
|
|
1343
|
+
lines.push(...resultLines(sub));
|
|
1344
|
+
} else if (e.progress) {
|
|
1345
|
+
const out = e.progress.replace(/\r\n/g, '\n').trimEnd().split('\n');
|
|
1346
|
+
const tail = out.slice(-5).map((l) => c.dim(truncate(l.replace(/\r/g, ''), w - 8)));
|
|
1347
|
+
if (out.length > 5) tail.unshift(c.dim(`… +${out.length - 5} lines`));
|
|
1348
|
+
lines.push(...resultLines(tail));
|
|
1349
|
+
} else {
|
|
1350
|
+
const secs = Math.floor((Date.now() - e.started) / 1000);
|
|
1351
|
+
if (secs >= 2) lines.push(...resultLines([c.dim(`Running… (${secs}s)`)]));
|
|
1352
|
+
}
|
|
1353
|
+
return lines;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
renderLive(width) {
|
|
1357
|
+
const w = width - 1;
|
|
1358
|
+
const lines = [];
|
|
1359
|
+
let cursor = null;
|
|
1360
|
+
if (this.busy) {
|
|
1361
|
+
// In-progress assistant text.
|
|
1362
|
+
if (this.md) {
|
|
1363
|
+
const partial = this.md.partial();
|
|
1364
|
+
lines.push(...partial.slice(-Math.floor(this.screen.height / 2)));
|
|
1365
|
+
}
|
|
1366
|
+
if (this.thinking?.md) lines.push(...this.thinking.md.partial().slice(-3));
|
|
1367
|
+
else if (this.thinking && this.thinking.text) {
|
|
1368
|
+
const last = this.thinking.text.trimEnd().split('\n').pop() || '';
|
|
1369
|
+
lines.push(c.dim(c.italic(truncate(`✻ ${last}`, w))));
|
|
1370
|
+
}
|
|
1371
|
+
for (const [, p] of this.pendingToolBlocks) {
|
|
1372
|
+
const m = /"(?:file_path|command|pattern|url|description|query)"\s*:\s*"((?:\\.|[^"\\])*)/.exec(p.partial);
|
|
1373
|
+
const tool = this.runtime.tools.find((t) => t.name === p.name) || { name: p.name };
|
|
1374
|
+
const arg = m ? m[1].replace(/\\n/g, ' ').replace(/\\(.)/g, '$1') : '';
|
|
1375
|
+
lines.push(`${c.gray(DOT)} ${c.bold(toolLabel(tool, {}))}${arg ? `(${truncate(arg, w - 20)})` : ''} ${c.dim(p.partial.length > 200 ? `(${formatTokens(Math.round(p.partial.length / 4))} tokens…)` : '…')}`);
|
|
1376
|
+
}
|
|
1377
|
+
for (const e of this.active) lines.push(...this.renderActiveEntry(e, w));
|
|
1378
|
+
if (!this.dialogs.length) {
|
|
1379
|
+
if (lines.length) lines.push('');
|
|
1380
|
+
lines.push(this.spinnerLine());
|
|
1381
|
+
const todos = this.agent.todos;
|
|
1382
|
+
if (this.showTodos && todos.length) lines.push(...renderTodos(todos).map((l) => truncate(l, w)));
|
|
1383
|
+
}
|
|
1384
|
+
} else if (this.active.length) {
|
|
1385
|
+
for (const e of this.active) lines.push(...this.renderActiveEntry(e, w));
|
|
1386
|
+
}
|
|
1387
|
+
if (this.dialogs.length) {
|
|
1388
|
+
if (lines.length) lines.push('');
|
|
1389
|
+
const r = this.dialogs[0].render(width, this.screen.height - lines.length - 1);
|
|
1390
|
+
const top = lines.length;
|
|
1391
|
+
lines.push(...r.lines);
|
|
1392
|
+
if (r.cursor) cursor = { row: top + r.cursor.row, col: r.cursor.col };
|
|
1393
|
+
return { lines, cursor };
|
|
1394
|
+
}
|
|
1395
|
+
if (this.queue.length) {
|
|
1396
|
+
for (const q of this.queue) lines.push(c.dim(' ⎿ queued: ') + c.gray(truncate((q.display ?? q.text).replace(/\s+/g, ' '), w - 14)));
|
|
1397
|
+
}
|
|
1398
|
+
if (lines.length) lines.push('');
|
|
1399
|
+
const placeholder = this.busy ? 'Type a message to queue it for the agent…' : (this.agent.messages.length ? '' : 'Try "explain this codebase" or "fix the failing test"');
|
|
1400
|
+
const r = this.editor.render(width, { placeholder, title: this.boxTitle() });
|
|
1401
|
+
const top = lines.length;
|
|
1402
|
+
lines.push(...r.lines);
|
|
1403
|
+
if (r.cursor) cursor = { row: top + r.cursor.row, col: r.cursor.col };
|
|
1404
|
+
if (this.editor.suggestions) lines.push(...this.editor.renderSuggestions(w));
|
|
1405
|
+
else if (this.editor.text === '?') lines.push(...this.shortcutsHelp());
|
|
1406
|
+
else lines.push(this.footer(w));
|
|
1407
|
+
return { lines, cursor };
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// In the input box's top border: the directory the agent is in now (a `cd` moves it), and this
|
|
1411
|
+
// session's name for the other sessions.
|
|
1412
|
+
boxTitle() {
|
|
1413
|
+
const dir = this.agent?.cwd || this.cwd;
|
|
1414
|
+
const home = os.homedir();
|
|
1415
|
+
const shown = dir.toLowerCase().startsWith(home.toLowerCase()) ? '~' + dir.slice(home.length) : dir;
|
|
1416
|
+
return { left: shown.replace(/\\/g, '/'), right: this.peers ? `@${this.peers.name}` : '' };
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
footer(w) {
|
|
1420
|
+
const mode = this.runtime.modes.mode;
|
|
1421
|
+
let left;
|
|
1422
|
+
const flash = this.flashMessage && Date.now() < this.flashMessage.until ? this.flashMessage : null;
|
|
1423
|
+
if (this.exitArmed && Date.now() - this.exitArmed < 1500) left = c.dim(' Press Ctrl-C again to exit');
|
|
1424
|
+
else if (flash) left = (flash.kind === 'warn' ? c.yellow : c.dim)(' ' + truncate(flash.text, w - 4));
|
|
1425
|
+
else if (this.restartRequest) left = c.dim(' ↻ restarting when this turn ends');
|
|
1426
|
+
else if (mode === 'plan') left = ' ' + c.plan('⏸ plan mode on (read-only)') + c.dim(' (shift+tab to leave)');
|
|
1427
|
+
else if (this.editor.mode === 'bash') left = c.magenta(' ! for bash mode');
|
|
1428
|
+
else if (this.editor.mode === 'memory') left = c.blue(' # to memorize');
|
|
1429
|
+
else left = c.dim(' ? for shortcuts');
|
|
1430
|
+
const cfg = this.config;
|
|
1431
|
+
const threshold = cfg.autoCompactThreshold(cfg.mainModel);
|
|
1432
|
+
const used = this.agent.contextEstimate;
|
|
1433
|
+
let notice = '';
|
|
1434
|
+
const pctLeft = Math.max(0, Math.round((1 - used / threshold) * 100));
|
|
1435
|
+
if (pctLeft <= 30 && cfg.autoCompactEnabled) notice = c.yellow(`Context left until auto-compact: ${pctLeft}%`);
|
|
1436
|
+
else if (this.runtime.tasks.running().length) notice = c.dim(`${this.runtime.tasks.running().length} background agent${this.runtime.tasks.running().length === 1 ? '' : 's'} running · /tasks`);
|
|
1437
|
+
else if (this.verbose) notice = c.dim('verbose');
|
|
1438
|
+
// The model and effort in use always have a place at the right: with the notice after it when both fit,
|
|
1439
|
+
// and the notice alone when they do not (a context warning matters more than the model's name).
|
|
1440
|
+
const { model, effort } = modelInfo(cfg);
|
|
1441
|
+
// An effort that is not set is not sent: the API then uses the model's default, which is shown instead.
|
|
1442
|
+
const level = effort || defaultEffort(cfg.mainModel.id, cfg);
|
|
1443
|
+
const info = c.dim(level ? `${model} · effort ${level}` : model);
|
|
1444
|
+
const room = w - stringWidth(left) - 1;
|
|
1445
|
+
const right = [notice && info ? `${notice}${c.dim(' · ')}${info}` : '', notice, info].find((r) => r && stringWidth(r) < room) || '';
|
|
1446
|
+
const gap = room - stringWidth(right);
|
|
1447
|
+
return right ? left + ' '.repeat(gap) + right : left;
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
export async function runRepl({ flags, cwd, initialPrompt }) {
|
|
1452
|
+
const app = new App({ flags, cwd });
|
|
1453
|
+
process.on('uncaughtException', (e) => {
|
|
1454
|
+
app.restoreTerminal();
|
|
1455
|
+
process.stderr.write(`\nUnexpected error: ${e?.stack || e}\n`);
|
|
1456
|
+
process.exit(1);
|
|
1457
|
+
});
|
|
1458
|
+
return await app.start(initialPrompt);
|
|
1459
|
+
}
|