codeply-cli 0.2.0 → 0.3.0
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/THIRD_PARTY_NOTICES.md +63 -0
- package/bin/codeply.js +562 -370
- package/lib/agent.mjs +1627 -160
- package/lib/ai.js +1107 -387
- package/lib/apply-limit.js +33 -51
- package/lib/apply-patch.js +217 -0
- package/lib/arity-table.json +138 -0
- package/lib/arity.js +70 -0
- package/lib/auth.js +118 -112
- package/lib/commands.js +103 -0
- package/lib/config.js +392 -136
- package/lib/design-library/artifact-template.html +500 -0
- package/lib/design-library/assemble-artifact.mjs +21 -0
- package/lib/design-library/build.mjs +226 -0
- package/lib/design-library/cleanup.mjs +45 -0
- package/lib/design-library/index.json +22380 -0
- package/lib/design-library/make-table-data.mjs +39 -0
- package/lib/design-library/query.mjs +104 -0
- package/lib/diagnostics.js +192 -0
- package/lib/edit-engine.js +171 -14
- package/lib/github-agent.mjs +290 -0
- package/lib/history.js +75 -0
- package/lib/mcp.js +258 -0
- package/lib/native-tools.mjs +189 -0
- package/lib/oauth-connectors.js +478 -0
- package/lib/permissions.js +109 -0
- package/lib/plugins.js +353 -0
- package/lib/render-md.mjs +617 -617
- package/lib/server.mjs +457 -0
- package/lib/session-db.js +220 -0
- package/lib/share.js +168 -0
- package/lib/skills.js +80 -22
- package/lib/snapshot.js +146 -0
- package/lib/subagents.js +164 -0
- package/lib/terminal-hints.js +154 -0
- package/lib/textarea.mjs +258 -258
- package/lib/tools.mjs +1658 -79
- package/lib/tui.js +7 -7
- package/lib/tui.mjs +1086 -992
- package/lib/typecheck-worker.js +147 -0
- package/lib/web-tools.js +158 -0
- package/package.json +4 -3
- package/skills/premium-web-design/SKILL.md +154 -0
package/lib/tui.mjs
CHANGED
|
@@ -1,992 +1,1086 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Codeply TUI
|
|
3
|
-
*
|
|
4
|
-
* Rendering model (important):
|
|
5
|
-
* Everything that is *finished* (banner + past messages + tool activity) is
|
|
6
|
-
* emitted through Ink's <Static>, which writes each item to stdout exactly
|
|
7
|
-
* once and never touches it again. Only the live area
|
|
8
|
-
* prompt, command palette, composer, hints
|
|
9
|
-
*
|
|
10
|
-
* This is what kills the flicker/duplicate-screen bug: Ink redraws a frame by
|
|
11
|
-
* erasing the previous one, but it can only erase lines still on screen. Once
|
|
12
|
-
* the frame grows taller than the terminal, the erase is incomplete and the
|
|
13
|
-
* frame gets appended instead of replaced
|
|
14
|
-
* Keeping the live frame a handful of lines tall makes the redraw exact.
|
|
15
|
-
*
|
|
16
|
-
* Rule for future edits: nothing tall goes outside <Static>.
|
|
17
|
-
*
|
|
18
|
-
* ┌ scrollback (banner + conversation, via <Static>) ──┐
|
|
19
|
-
* ├ live frame ────────────────────────────────────────┤
|
|
20
|
-
* │ spinner · approval · palette · composer · hints │
|
|
21
|
-
* └────────────────────────────────────────────────────┘
|
|
22
|
-
*/
|
|
23
|
-
import React from 'react';
|
|
24
|
-
import { Box, Text, Static, useInput, useApp, useStdout } from 'ink';
|
|
25
|
-
import Spinner from 'ink-spinner';
|
|
26
|
-
import { createRequire } from 'module';
|
|
27
|
-
import path from 'path';
|
|
28
|
-
import os from 'os';
|
|
29
|
-
import fs from 'fs';
|
|
30
|
-
|
|
31
|
-
const require = createRequire(import.meta.url);
|
|
32
|
-
const auth = require('../lib/auth.js');
|
|
33
|
-
const appConfig = require('../lib/config.js');
|
|
34
|
-
const skills = require('../lib/skills.js');
|
|
35
|
-
const VERSION = require('../package.json').version;
|
|
36
|
-
import {
|
|
37
|
-
renderMarkdown, Panel, hexToAnsi, hexToAnsiBg, fillRow, SURFACE,
|
|
38
|
-
ANSI_RESET, ANSI_BOLD, ANSI_DIM,
|
|
39
|
-
} from '../lib/render-md.mjs';
|
|
40
|
-
import { runAgent } from '../lib/agent.mjs';
|
|
41
|
-
import { TOOL_DISPLAY } from '../lib/tools.mjs';
|
|
42
|
-
import TextArea, { enableModifiedEnter, isTerminalReport } from '../lib/textarea.mjs';
|
|
43
|
-
|
|
44
|
-
// ─── Theme (Google brand) ───────────────────────────────────────────────────
|
|
45
|
-
const G = {
|
|
46
|
-
blue: '#4285F4',
|
|
47
|
-
red: '#EA4335',
|
|
48
|
-
yellow: '#FBBC05',
|
|
49
|
-
green: '#34A853',
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const T = {
|
|
53
|
-
accent: G.blue,
|
|
54
|
-
text: '#e8eaed',
|
|
55
|
-
secondary: '#9aa0a6',
|
|
56
|
-
dim: '#80868b',
|
|
57
|
-
muted: '#5f6368',
|
|
58
|
-
faint: '#3c4043',
|
|
59
|
-
border: '#3c4043',
|
|
60
|
-
cycle: [G.blue, G.red, G.yellow, G.green],
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
{ t: '
|
|
82
|
-
{ t: '
|
|
83
|
-
{ t: '
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
{ t: '
|
|
88
|
-
{ t: '
|
|
89
|
-
{ t: '
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
{ t: '██║
|
|
94
|
-
{ t: '
|
|
95
|
-
{ t: '
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
{ t: '
|
|
100
|
-
{ t: '
|
|
101
|
-
{ t: '
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
{ t: '
|
|
106
|
-
{ t: '
|
|
107
|
-
{ t: '
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
{ name: '/
|
|
127
|
-
{ name: '/
|
|
128
|
-
{ name: '/
|
|
129
|
-
{ name: '/
|
|
130
|
-
{ name: '/
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
]
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
//
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
*
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
React.createElement(
|
|
312
|
-
Box,
|
|
313
|
-
{ flexDirection: '
|
|
314
|
-
React.createElement(Text,
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
const
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
React.
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
React.
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
React.
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
React.
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
});
|
|
516
|
-
}, []);
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
push({ role: 'system', content:
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
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
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
if (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
if (
|
|
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
|
-
|
|
840
|
-
|
|
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
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
);
|
|
906
|
-
|
|
907
|
-
const
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Codeply TUI - Google-brand interactive shell.
|
|
3
|
+
*
|
|
4
|
+
* Rendering model (important):
|
|
5
|
+
* Everything that is *finished* (banner + past messages + tool activity) is
|
|
6
|
+
* emitted through Ink's <Static>, which writes each item to stdout exactly
|
|
7
|
+
* once and never touches it again. Only the live area - spinner, approval
|
|
8
|
+
* prompt, command palette, composer, hints - lives in the re-rendered frame.
|
|
9
|
+
*
|
|
10
|
+
* This is what kills the flicker/duplicate-screen bug: Ink redraws a frame by
|
|
11
|
+
* erasing the previous one, but it can only erase lines still on screen. Once
|
|
12
|
+
* the frame grows taller than the terminal, the erase is incomplete and the
|
|
13
|
+
* frame gets appended instead of replaced - you see the conversation twice.
|
|
14
|
+
* Keeping the live frame a handful of lines tall makes the redraw exact.
|
|
15
|
+
*
|
|
16
|
+
* Rule for future edits: nothing tall goes outside <Static>.
|
|
17
|
+
*
|
|
18
|
+
* ┌ scrollback (banner + conversation, via <Static>) ──┐
|
|
19
|
+
* ├ live frame ────────────────────────────────────────┤
|
|
20
|
+
* │ spinner · approval · palette · composer · hints │
|
|
21
|
+
* └────────────────────────────────────────────────────┘
|
|
22
|
+
*/
|
|
23
|
+
import React from 'react';
|
|
24
|
+
import { Box, Text, Static, useInput, useApp, useStdout } from 'ink';
|
|
25
|
+
import Spinner from 'ink-spinner';
|
|
26
|
+
import { createRequire } from 'module';
|
|
27
|
+
import path from 'path';
|
|
28
|
+
import os from 'os';
|
|
29
|
+
import fs from 'fs';
|
|
30
|
+
|
|
31
|
+
const require = createRequire(import.meta.url);
|
|
32
|
+
const auth = require('../lib/auth.js');
|
|
33
|
+
const appConfig = require('../lib/config.js');
|
|
34
|
+
const skills = require('../lib/skills.js');
|
|
35
|
+
const VERSION = require('../package.json').version;
|
|
36
|
+
import {
|
|
37
|
+
renderMarkdown, Panel, hexToAnsi, hexToAnsiBg, fillRow, SURFACE,
|
|
38
|
+
ANSI_RESET, ANSI_BOLD, ANSI_DIM,
|
|
39
|
+
} from '../lib/render-md.mjs';
|
|
40
|
+
import { runAgent } from '../lib/agent.mjs';
|
|
41
|
+
import { TOOL_DISPLAY } from '../lib/tools.mjs';
|
|
42
|
+
import TextArea, { enableModifiedEnter, isTerminalReport } from '../lib/textarea.mjs';
|
|
43
|
+
|
|
44
|
+
// ─── Theme (Google brand) ───────────────────────────────────────────────────
|
|
45
|
+
const G = {
|
|
46
|
+
blue: '#4285F4',
|
|
47
|
+
red: '#EA4335',
|
|
48
|
+
yellow: '#FBBC05',
|
|
49
|
+
green: '#34A853',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const T = {
|
|
53
|
+
accent: G.blue,
|
|
54
|
+
text: '#e8eaed',
|
|
55
|
+
secondary: '#9aa0a6',
|
|
56
|
+
dim: '#80868b',
|
|
57
|
+
muted: '#5f6368',
|
|
58
|
+
faint: '#3c4043',
|
|
59
|
+
border: '#3c4043',
|
|
60
|
+
cycle: [G.blue, G.red, G.yellow, G.green],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// The mascot's own colour, from mascot.png - the home screen's box border and
|
|
64
|
+
// headings pick this up too, so the splash reads as one themed piece instead
|
|
65
|
+
// of the mascot looking like a sticker dropped on top of an unrelated blue box.
|
|
66
|
+
const MASCOT_ORANGE = '#E8821E';
|
|
67
|
+
|
|
68
|
+
const MODES = ['Build', 'Plan', 'Ask'];
|
|
69
|
+
const MODE_COLOR = { Build: G.blue, Plan: G.yellow, Ask: G.green };
|
|
70
|
+
const MODE_HINT = {
|
|
71
|
+
Build: 'reads, edits and verifies files',
|
|
72
|
+
Plan: 'investigates, then writes a plan',
|
|
73
|
+
Ask: 'answers from the codebase, read-only',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// CODEPLY block letters - fixed per-letter widths, one Google color each.
|
|
77
|
+
// C=blue O=red D=yellow E=green P=blue L=red Y=yellow
|
|
78
|
+
const LOGO_SEGMENTS = [
|
|
79
|
+
[
|
|
80
|
+
{ t: ' ██████╗ ', c: G.blue }, { t: ' ██████╗ ', c: G.red },
|
|
81
|
+
{ t: '██████╗ ', c: G.yellow }, { t: '███████╗ ', c: G.green },
|
|
82
|
+
{ t: '██████╗ ', c: G.blue }, { t: '██╗ ', c: G.red },
|
|
83
|
+
{ t: '██╗ ██╗', c: G.yellow },
|
|
84
|
+
],
|
|
85
|
+
[
|
|
86
|
+
{ t: '██╔════╝ ', c: G.blue }, { t: '██╔═══██╗', c: G.red },
|
|
87
|
+
{ t: '██╔══██╗ ', c: G.yellow }, { t: '██╔════╝ ', c: G.green },
|
|
88
|
+
{ t: '██╔══██╗ ', c: G.blue }, { t: '██║ ', c: G.red },
|
|
89
|
+
{ t: '╚██╗ ██╔╝', c: G.yellow },
|
|
90
|
+
],
|
|
91
|
+
[
|
|
92
|
+
{ t: '██║ ', c: G.blue }, { t: '██║ ██║', c: G.red },
|
|
93
|
+
{ t: '██║ ██║ ', c: G.yellow }, { t: '█████╗ ', c: G.green },
|
|
94
|
+
{ t: '██████╔╝ ', c: G.blue }, { t: '██║ ', c: G.red },
|
|
95
|
+
{ t: ' ╚████╔╝ ', c: G.yellow },
|
|
96
|
+
],
|
|
97
|
+
[
|
|
98
|
+
{ t: '██║ ', c: G.blue }, { t: '██║ ██║', c: G.red },
|
|
99
|
+
{ t: '██║ ██║ ', c: G.yellow }, { t: '██╔══╝ ', c: G.green },
|
|
100
|
+
{ t: '██╔═══╝ ', c: G.blue }, { t: '██║ ', c: G.red },
|
|
101
|
+
{ t: ' ╚██╔╝ ', c: G.yellow },
|
|
102
|
+
],
|
|
103
|
+
[
|
|
104
|
+
{ t: '╚██████╗ ', c: G.blue }, { t: '╚██████╔╝', c: G.red },
|
|
105
|
+
{ t: '██████╔╝ ', c: G.yellow }, { t: '███████╗ ', c: G.green },
|
|
106
|
+
{ t: '██║ ', c: G.blue }, { t: '███████╗ ', c: G.red },
|
|
107
|
+
{ t: ' ██║ ', c: G.yellow },
|
|
108
|
+
],
|
|
109
|
+
[
|
|
110
|
+
{ t: ' ╚═════╝ ', c: G.blue }, { t: ' ╚═════╝ ', c: G.red },
|
|
111
|
+
{ t: '╚═════╝ ', c: G.yellow }, { t: '╚══════╝ ', c: G.green },
|
|
112
|
+
{ t: '╚═╝ ', c: G.blue }, { t: '╚══════╝ ', c: G.red },
|
|
113
|
+
{ t: ' ╚═╝ ', c: G.yellow },
|
|
114
|
+
],
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
// Pre-flattened to one ANSI string per row: 6 <Text> nodes instead of 42.
|
|
118
|
+
const LOGO_ROWS = LOGO_SEGMENTS.map((row) =>
|
|
119
|
+
row.map((s) => hexToAnsi(s.c) + ANSI_BOLD + s.t + ANSI_RESET).join('')
|
|
120
|
+
);
|
|
121
|
+
const LOGO_WIDTH = LOGO_SEGMENTS[0].reduce((n, s) => n + s.t.length, 0);
|
|
122
|
+
|
|
123
|
+
const WORDMARK = hexToAnsi(G.yellow) + ANSI_BOLD + 'codeply' + ANSI_RESET;
|
|
124
|
+
|
|
125
|
+
const COMMANDS = [
|
|
126
|
+
{ name: '/login', description: 'Sign in with your Codeply account' },
|
|
127
|
+
{ name: '/logout', description: 'Sign out of current session' },
|
|
128
|
+
{ name: '/whoami', description: 'Show currently signed-in account' },
|
|
129
|
+
{ name: '/help', description: 'Show available commands' },
|
|
130
|
+
{ name: '/clear', description: 'Clear conversation and context' },
|
|
131
|
+
{ name: '/mode', description: 'Cycle Build → Plan → Ask' },
|
|
132
|
+
{ name: '/provider', description: 'Show or switch model provider (BYOK: openrouter, groq, anthropic, openai)' },
|
|
133
|
+
{ name: '/skill', description: 'List, install, or remove agent skills' },
|
|
134
|
+
{ name: '/keys', description: 'Show what your terminal sends for a key' },
|
|
135
|
+
{ name: '/quit', description: 'Exit Codeply' },
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
const TIPS = [
|
|
139
|
+
['ctrl+j', 'inserts a new line - or end a line with \\ and press enter'],
|
|
140
|
+
['Press tab', 'to cycle Build → Plan → Ask without leaving the composer'],
|
|
141
|
+
['Press esc', 'while it is working to interrupt the run'],
|
|
142
|
+
['Ask it to fix a failing test', 'it can run commands and iterate on the output'],
|
|
143
|
+
['Run /keys', 'to see exactly what your terminal sends for a key'],
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
const WHATS_NEW = [
|
|
147
|
+
'Verification gate - the agent now has to check its changes (and can no longer claim tests or deploys it never ran) before it finishes',
|
|
148
|
+
'Added shift+tab to toggle bypass permissions without leaving the composer',
|
|
149
|
+
'/skill install now accepts a bare GitHub owner/repo, not just a full URL',
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
// A tiny pixel mascot echoing mascot.png's blocky orange creature - pointy
|
|
153
|
+
// ear notches, a solid head, two eyes, comb-like legs. The terminal's own
|
|
154
|
+
// background already stands in for every "black" pixel in the source art, so
|
|
155
|
+
// only the orange pixels need marking; everything else is a plain space.
|
|
156
|
+
const MASCOT_ROWS = [
|
|
157
|
+
' █ █ ',
|
|
158
|
+
'████████',
|
|
159
|
+
'██ █ ███',
|
|
160
|
+
'████████',
|
|
161
|
+
'█ █ █ █',
|
|
162
|
+
];
|
|
163
|
+
|
|
164
|
+
function homePath(cwd) {
|
|
165
|
+
const home = os.homedir();
|
|
166
|
+
if (cwd.startsWith(home)) return '~' + cwd.slice(home.length);
|
|
167
|
+
return cwd;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Shorten a path from the left so it never blows out the footer. */
|
|
171
|
+
function fitPath(p, max) {
|
|
172
|
+
if (p.length <= max) return p;
|
|
173
|
+
return '…' + p.slice(p.length - max + 1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ─── Home screen ────────────────────────────────────────────────────────────
|
|
177
|
+
//
|
|
178
|
+
// A single bordered box - title baked into the top border, a left column
|
|
179
|
+
// (identity: mascot, account, mode, cwd) and a right column (tips / what's
|
|
180
|
+
// new) split by one interior divider that runs the full height. Everything
|
|
181
|
+
// is pre-flattened to one ANSI string per row, same discipline as the rest of
|
|
182
|
+
// this file: a handful of <Text> nodes, not one per glyph.
|
|
183
|
+
|
|
184
|
+
const BOX_MIN_COLS = 74; // below this, two columns can't breathe - fall back to the compact mark
|
|
185
|
+
|
|
186
|
+
/** Pad/truncate plain text to exactly `width` visible columns, then colour it. */
|
|
187
|
+
function cell(text, width, color, opts = {}) {
|
|
188
|
+
const t = String(text ?? '');
|
|
189
|
+
const fit = t.length > width ? (width > 1 ? t.slice(0, width - 1) + '…' : t.slice(0, width)) : t;
|
|
190
|
+
const pad = ' '.repeat(Math.max(0, width - fit.length));
|
|
191
|
+
const styled = (color ? hexToAnsi(color) : '') + (opts.bold ? ANSI_BOLD : '') + (opts.dim ? ANSI_DIM : '') + fit + ANSI_RESET;
|
|
192
|
+
return styled + pad;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Centre plain text within `width` columns before it goes through cell(). */
|
|
196
|
+
function centered(text, width) {
|
|
197
|
+
const t = String(text ?? '');
|
|
198
|
+
if (t.length >= width) return t;
|
|
199
|
+
const left = Math.floor((width - t.length) / 2);
|
|
200
|
+
return ' '.repeat(left) + t;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Shared by BoxHome and its caller - the caller needs leftInner/rightInner
|
|
204
|
+
* up front to centre text at the right width before the rows are built. */
|
|
205
|
+
function boxHomeMetrics(cols) {
|
|
206
|
+
const boxWidth = Math.max(60, Math.min(cols - 2, 96));
|
|
207
|
+
const leftColW = Math.round(boxWidth * 0.44);
|
|
208
|
+
const rightColW = boxWidth - leftColW - 3; // 3 = two side borders + one divider
|
|
209
|
+
return { boxWidth, leftColW, rightColW, leftInner: leftColW - 2, rightInner: rightColW - 2 };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function BoxHome({ cols, title, left, right, borderColor }) {
|
|
213
|
+
const { boxWidth, leftInner, rightInner } = boxHomeMetrics(cols);
|
|
214
|
+
const rowCount = Math.max(left.length, right.length);
|
|
215
|
+
const border = hexToAnsi(borderColor);
|
|
216
|
+
|
|
217
|
+
const topPrefix = `── ${title} `;
|
|
218
|
+
const topDashes = Math.max(0, boxWidth - topPrefix.length - 2);
|
|
219
|
+
const top = border + '┌' + topPrefix + '─'.repeat(topDashes) + '┐' + ANSI_RESET;
|
|
220
|
+
const bottom = border + '└' + '─'.repeat(boxWidth - 2) + '┘' + ANSI_RESET;
|
|
221
|
+
|
|
222
|
+
const rows = [];
|
|
223
|
+
for (let i = 0; i < rowCount; i++) {
|
|
224
|
+
const l = left[i] || { text: '' };
|
|
225
|
+
const r = right[i] || { text: '' };
|
|
226
|
+
rows.push(
|
|
227
|
+
border + '│' + ANSI_RESET + ' ' +
|
|
228
|
+
cell(l.text, leftInner, l.color, l) +
|
|
229
|
+
' ' + border + '│' + ANSI_RESET + ' ' +
|
|
230
|
+
cell(r.text, rightInner, r.color, r) +
|
|
231
|
+
' ' + border + '│' + ANSI_RESET
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return React.createElement(
|
|
236
|
+
Box,
|
|
237
|
+
{ flexDirection: 'column' },
|
|
238
|
+
React.createElement(Text, { key: 'top' }, top),
|
|
239
|
+
...rows.map((row, i) => React.createElement(Text, { key: `r${i}` }, row)),
|
|
240
|
+
React.createElement(Text, { key: 'bot' }, bottom)
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function CompactMark({ cols }) {
|
|
245
|
+
const wide = cols >= LOGO_WIDTH + 4;
|
|
246
|
+
return React.createElement(
|
|
247
|
+
Box,
|
|
248
|
+
{ flexDirection: 'column', width: cols, alignItems: 'center' },
|
|
249
|
+
wide
|
|
250
|
+
? LOGO_ROWS.map((row, i) => React.createElement(Text, { key: `lg-${i}` }, row))
|
|
251
|
+
: React.createElement(Text, { key: 'lg' }, WORDMARK),
|
|
252
|
+
React.createElement(
|
|
253
|
+
Box,
|
|
254
|
+
{ key: 'tag', marginTop: 1, flexDirection: 'column', alignItems: 'center' },
|
|
255
|
+
React.createElement(
|
|
256
|
+
Text,
|
|
257
|
+
null,
|
|
258
|
+
hexToAnsi(T.secondary) + 'An agent that reads your code, edits it, and checks its work.' + ANSI_RESET
|
|
259
|
+
),
|
|
260
|
+
React.createElement(
|
|
261
|
+
Text,
|
|
262
|
+
null,
|
|
263
|
+
hexToAnsi(T.muted) + 'interactive shell' + ANSI_RESET +
|
|
264
|
+
hexToAnsi(T.faint) + ' · ' + ANSI_RESET +
|
|
265
|
+
hexToAnsi(T.muted) + 'v' + VERSION + ANSI_RESET
|
|
266
|
+
)
|
|
267
|
+
)
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ─── Message blocks ─────────────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
/** One line of tool activity in the transcript. */
|
|
274
|
+
function ToolLine({ msg }) {
|
|
275
|
+
const display = TOOL_DISPLAY[msg.name] || { verb: msg.name, icon: '▸' };
|
|
276
|
+
const failed = !msg.ok;
|
|
277
|
+
const wrote = msg.name === 'write_file' || msg.name === 'edit_file';
|
|
278
|
+
const ran = msg.name === 'run';
|
|
279
|
+
|
|
280
|
+
const iconColor = failed ? G.red : wrote ? G.yellow : ran ? G.green : G.blue;
|
|
281
|
+
const detail = [msg.label, msg.detail].filter(Boolean).join(' · ');
|
|
282
|
+
|
|
283
|
+
return React.createElement(
|
|
284
|
+
Text,
|
|
285
|
+
{ wrap: 'truncate-end' },
|
|
286
|
+
hexToAnsi(iconColor) + display.icon + ' ' + ANSI_RESET +
|
|
287
|
+
hexToAnsi(failed ? G.red : T.secondary) + display.verb.padEnd(6) + ANSI_RESET +
|
|
288
|
+
hexToAnsi(T.dim) + ' ' + detail + ANSI_RESET
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function MessageBlock({ msg, cols }) {
|
|
293
|
+
if (msg.role === 'user') {
|
|
294
|
+
return React.createElement(
|
|
295
|
+
Box,
|
|
296
|
+
{ flexDirection: 'row', marginBottom: 1, gap: 1 },
|
|
297
|
+
React.createElement(Text, { color: G.blue }, '▌'),
|
|
298
|
+
React.createElement(
|
|
299
|
+
Box,
|
|
300
|
+
{ flexDirection: 'column', flexGrow: 1 },
|
|
301
|
+
React.createElement(Text, null, hexToAnsi(T.dim) + ANSI_DIM + 'you' + ANSI_RESET),
|
|
302
|
+
React.createElement(Text, { color: T.text, wrap: 'wrap' }, msg.content)
|
|
303
|
+
)
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (msg.role === 'assistant') {
|
|
308
|
+
// A plan is a deliverable, not chatter - give it its own panel so it stands
|
|
309
|
+
// apart from the running commentary above and below it.
|
|
310
|
+
if (msg.plan) {
|
|
311
|
+
return React.createElement(
|
|
312
|
+
Box,
|
|
313
|
+
{ flexDirection: 'row', marginBottom: 1, gap: 1 },
|
|
314
|
+
React.createElement(Text, { color: G.yellow }, '▌'),
|
|
315
|
+
React.createElement(
|
|
316
|
+
Box,
|
|
317
|
+
{ flexDirection: 'column', flexGrow: 1 },
|
|
318
|
+
React.createElement(Panel, {
|
|
319
|
+
title: 'plan',
|
|
320
|
+
lines: msg.content.split('\n'),
|
|
321
|
+
width: cols - 4,
|
|
322
|
+
accent: G.yellow,
|
|
323
|
+
})
|
|
324
|
+
)
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
return React.createElement(
|
|
328
|
+
Box,
|
|
329
|
+
{ flexDirection: 'row', marginBottom: 1, gap: 1 },
|
|
330
|
+
React.createElement(Text, { color: G.green }, '▌'),
|
|
331
|
+
React.createElement(
|
|
332
|
+
Box,
|
|
333
|
+
{ flexDirection: 'column', flexGrow: 1 },
|
|
334
|
+
React.createElement(Text, null, WORDMARK),
|
|
335
|
+
...renderMarkdown(msg.content, cols - 4)
|
|
336
|
+
)
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (msg.role === 'tool') {
|
|
341
|
+
return React.createElement(
|
|
342
|
+
Box,
|
|
343
|
+
{ flexDirection: 'row', marginBottom: 0, paddingLeft: 2 },
|
|
344
|
+
React.createElement(ToolLine, { msg })
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (msg.role === 'system') {
|
|
349
|
+
return React.createElement(
|
|
350
|
+
Box,
|
|
351
|
+
{ flexDirection: 'row', marginBottom: 1, gap: 1 },
|
|
352
|
+
React.createElement(Text, { color: G.yellow }, '◆'),
|
|
353
|
+
React.createElement(Text, { color: T.secondary, wrap: 'wrap' }, msg.content)
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (msg.role === 'error') {
|
|
358
|
+
return React.createElement(
|
|
359
|
+
Box,
|
|
360
|
+
{ flexDirection: 'row', marginBottom: 1, gap: 1 },
|
|
361
|
+
React.createElement(Text, { color: G.red }, '✗'),
|
|
362
|
+
React.createElement(Text, { color: G.red, wrap: 'wrap' }, msg.content)
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ─── Command palette ────────────────────────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
function CommandsMenu({ commands, selectedIndex }) {
|
|
372
|
+
return React.createElement(
|
|
373
|
+
Box,
|
|
374
|
+
{
|
|
375
|
+
flexDirection: 'column',
|
|
376
|
+
borderStyle: 'round',
|
|
377
|
+
borderColor: T.border,
|
|
378
|
+
paddingX: 1,
|
|
379
|
+
marginBottom: 1,
|
|
380
|
+
flexShrink: 0,
|
|
381
|
+
},
|
|
382
|
+
React.createElement(
|
|
383
|
+
Text,
|
|
384
|
+
null,
|
|
385
|
+
hexToAnsi(T.muted) + '↑↓' + ANSI_RESET + hexToAnsi(T.faint) + ' select ' + ANSI_RESET +
|
|
386
|
+
hexToAnsi(T.muted) + 'tab' + ANSI_RESET + hexToAnsi(T.faint) + ' complete ' + ANSI_RESET +
|
|
387
|
+
hexToAnsi(T.muted) + 'esc' + ANSI_RESET + hexToAnsi(T.faint) + ' dismiss' + ANSI_RESET
|
|
388
|
+
),
|
|
389
|
+
...commands.map((cmd, i) => {
|
|
390
|
+
const selected = i === selectedIndex;
|
|
391
|
+
const nameColor = selected ? T.cycle[i % T.cycle.length] : T.secondary;
|
|
392
|
+
return React.createElement(
|
|
393
|
+
Text,
|
|
394
|
+
{ key: cmd.name, wrap: 'truncate-end' },
|
|
395
|
+
(selected ? hexToAnsi(G.blue) + '› ' + ANSI_RESET : ' ') +
|
|
396
|
+
hexToAnsi(nameColor) + (selected ? ANSI_BOLD : '') + cmd.name.padEnd(10) + ANSI_RESET +
|
|
397
|
+
hexToAnsi(selected ? T.dim : T.faint) + ' ' + cmd.description + ANSI_RESET
|
|
398
|
+
);
|
|
399
|
+
})
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ─── Approval prompt ────────────────────────────────────────────────────────
|
|
404
|
+
|
|
405
|
+
function ApprovalPrompt({ request }) {
|
|
406
|
+
const accent = request.danger ? G.red : G.yellow;
|
|
407
|
+
const lines = [];
|
|
408
|
+
|
|
409
|
+
if (request.diff) {
|
|
410
|
+
const removed = request.diff.search.split('\n').slice(0, 6);
|
|
411
|
+
const added = request.diff.replace.split('\n').slice(0, 6);
|
|
412
|
+
for (const l of removed) lines.push(hexToAnsi(G.red) + '- ' + l + ANSI_RESET);
|
|
413
|
+
for (const l of added) lines.push(hexToAnsi(G.green) + '+ ' + l + ANSI_RESET);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return React.createElement(
|
|
417
|
+
Box,
|
|
418
|
+
{
|
|
419
|
+
flexDirection: 'column',
|
|
420
|
+
borderStyle: 'round',
|
|
421
|
+
borderColor: accent,
|
|
422
|
+
paddingX: 1,
|
|
423
|
+
marginBottom: 1,
|
|
424
|
+
flexShrink: 0,
|
|
425
|
+
},
|
|
426
|
+
React.createElement(
|
|
427
|
+
Text,
|
|
428
|
+
{ wrap: 'truncate-end' },
|
|
429
|
+
hexToAnsi(accent) + ANSI_BOLD + request.title + ANSI_RESET +
|
|
430
|
+
(request.danger ? hexToAnsi(G.red) + ' ⚠ looks destructive' + ANSI_RESET : '')
|
|
431
|
+
),
|
|
432
|
+
request.detail && React.createElement(
|
|
433
|
+
Text,
|
|
434
|
+
{ wrap: 'truncate-end' },
|
|
435
|
+
hexToAnsi(T.secondary) + request.detail + ANSI_RESET
|
|
436
|
+
),
|
|
437
|
+
...lines.map((l, i) => React.createElement(Text, { key: i, wrap: 'truncate-end' }, l)),
|
|
438
|
+
React.createElement(
|
|
439
|
+
Text,
|
|
440
|
+
null,
|
|
441
|
+
hexToAnsi(G.green) + 'y' + ANSI_RESET + hexToAnsi(T.faint) + ' approve ' + ANSI_RESET +
|
|
442
|
+
hexToAnsi(G.blue) + 'a' + ANSI_RESET + hexToAnsi(T.faint) + ' approve all this session ' + ANSI_RESET +
|
|
443
|
+
hexToAnsi(G.red) + 'n' + ANSI_RESET + hexToAnsi(T.faint) + ' decline' + ANSI_RESET
|
|
444
|
+
)
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ─── Status bar ─────────────────────────────────────────────────────────────
|
|
449
|
+
|
|
450
|
+
/** Permission-bypass state and the running-agent count, pinned bottom-left. */
|
|
451
|
+
function StatusBar({ bypassMode, agentCount }) {
|
|
452
|
+
const glyphColor = bypassMode ? G.yellow : T.muted;
|
|
453
|
+
return React.createElement(
|
|
454
|
+
Text,
|
|
455
|
+
{ wrap: 'truncate-end' },
|
|
456
|
+
hexToAnsi(glyphColor) + ANSI_BOLD + '⏵⏵ ' + ANSI_RESET +
|
|
457
|
+
hexToAnsi(bypassMode ? G.yellow : T.secondary) + `bypass permissions ${bypassMode ? 'on' : 'off'}` + ANSI_RESET +
|
|
458
|
+
hexToAnsi(T.faint) + ' (shift+tab to cycle)' + ANSI_RESET +
|
|
459
|
+
hexToAnsi(T.faint) + ' · ' + ANSI_RESET +
|
|
460
|
+
hexToAnsi(T.muted) + '← ' + agentCount + (agentCount === 1 ? ' agent' : ' agents') + ANSI_RESET
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ─── App ────────────────────────────────────────────────────────────────────
|
|
465
|
+
|
|
466
|
+
const App = () => {
|
|
467
|
+
const { exit } = useApp();
|
|
468
|
+
const { stdout } = useStdout();
|
|
469
|
+
const [cols, setCols] = React.useState(stdout?.columns || 80);
|
|
470
|
+
const [rows, setRows] = React.useState(stdout?.rows || 24);
|
|
471
|
+
|
|
472
|
+
const [input, setInput] = React.useState('');
|
|
473
|
+
const [modeIndex, setModeIndex] = React.useState(0);
|
|
474
|
+
const [session, setSession] = React.useState(null);
|
|
475
|
+
const [running, setRunning] = React.useState(false);
|
|
476
|
+
const [statusLine, setStatusLine] = React.useState('');
|
|
477
|
+
const [elapsed, setElapsed] = React.useState(0);
|
|
478
|
+
const [steps, setSteps] = React.useState(0);
|
|
479
|
+
const [chatHistory, setChatHistory] = React.useState([]);
|
|
480
|
+
const [staticKey, setStaticKey] = React.useState(0);
|
|
481
|
+
const [showCommands, setShowCommands] = React.useState(false);
|
|
482
|
+
const [selectedCommand, setSelectedCommand] = React.useState(0);
|
|
483
|
+
const [filteredCommands, setFilteredCommands] = React.useState(COMMANDS);
|
|
484
|
+
const [escArmed, setEscArmed] = React.useState(false);
|
|
485
|
+
const [approval, setApproval] = React.useState(null);
|
|
486
|
+
const [bypassMode, setBypassMode] = React.useState(false);
|
|
487
|
+
const [activeAgents, setActiveAgents] = React.useState([]); // [{id,label}] - running subagents, this session's own run excluded
|
|
488
|
+
// Only true once the terminal answers our capability query. Windows Terminal
|
|
489
|
+
// never does, so there we advertise ctrl+j instead of lying about shift+enter.
|
|
490
|
+
const [modifiedEnterWorks, setModifiedEnterWorks] = React.useState(false);
|
|
491
|
+
const [keyProbe, setKeyProbe] = React.useState(false);
|
|
492
|
+
|
|
493
|
+
const nextId = React.useRef(1);
|
|
494
|
+
// A real AbortController, not a plain {aborted} object: its .signal ends up
|
|
495
|
+
// handed straight to fetch() (see ai.js), which requires an actual
|
|
496
|
+
// AbortSignal instance - a look-alike object throws a TypeError there
|
|
497
|
+
// ("Expected signal ... to be an instance of AbortSignal"), and that
|
|
498
|
+
// message contains the word "AbortSignal", which the fetch error classifier
|
|
499
|
+
// then mistakes for a real abort. Net effect: every single turn "aborted"
|
|
500
|
+
// immediately, before the model was ever called.
|
|
501
|
+
const abortRef = React.useRef(new AbortController());
|
|
502
|
+
const alwaysAllow = React.useRef(new Set());
|
|
503
|
+
const convo = React.useRef([]); // {role,content} pairs handed to the agent
|
|
504
|
+
|
|
505
|
+
const cwd = process.cwd();
|
|
506
|
+
const mode = MODES[modeIndex];
|
|
507
|
+
const modeColor = MODE_COLOR[mode];
|
|
508
|
+
const tip = React.useMemo(() => TIPS[Math.floor(Math.random() * TIPS.length)], []);
|
|
509
|
+
|
|
510
|
+
const push = React.useCallback((msg) => {
|
|
511
|
+
setChatHistory((prev) => [...prev, { id: `m-${nextId.current++}`, ...msg }]);
|
|
512
|
+
}, []);
|
|
513
|
+
|
|
514
|
+
React.useEffect(() => {
|
|
515
|
+
auth.getSession().then((s) => setSession(s)).catch(() => {});
|
|
516
|
+
}, []);
|
|
517
|
+
|
|
518
|
+
// Ask the terminal to disambiguate shift+enter from enter (see textarea.mjs).
|
|
519
|
+
React.useEffect(() => enableModifiedEnter(stdout), [stdout]);
|
|
520
|
+
|
|
521
|
+
React.useEffect(() => {
|
|
522
|
+
if (!stdout) return;
|
|
523
|
+
const onResize = () => { setCols(stdout.columns || 80); setRows(stdout.rows || 24); };
|
|
524
|
+
stdout.on('resize', onResize);
|
|
525
|
+
return () => stdout.off('resize', onResize);
|
|
526
|
+
}, [stdout]);
|
|
527
|
+
|
|
528
|
+
React.useEffect(() => {
|
|
529
|
+
if (input.startsWith('/') && !input.includes('\n')) {
|
|
530
|
+
const query = input.toLowerCase().split(/\s/)[0];
|
|
531
|
+
const filtered = COMMANDS.filter((cmd) => cmd.name.startsWith(query));
|
|
532
|
+
setFilteredCommands(filtered);
|
|
533
|
+
setShowCommands(filtered.length > 0);
|
|
534
|
+
setSelectedCommand(0);
|
|
535
|
+
} else {
|
|
536
|
+
setShowCommands(false);
|
|
537
|
+
}
|
|
538
|
+
}, [input]);
|
|
539
|
+
|
|
540
|
+
React.useEffect(() => {
|
|
541
|
+
if (!escArmed) return;
|
|
542
|
+
const t = setTimeout(() => setEscArmed(false), 1500);
|
|
543
|
+
return () => clearTimeout(t);
|
|
544
|
+
}, [escArmed]);
|
|
545
|
+
|
|
546
|
+
// Elapsed counter while the agent works. The spinner already repaints the
|
|
547
|
+
// live frame several times a second, so this ticker costs nothing extra.
|
|
548
|
+
React.useEffect(() => {
|
|
549
|
+
if (!running) { setElapsed(0); return; }
|
|
550
|
+
const startedAt = Date.now();
|
|
551
|
+
const t = setInterval(() => setElapsed(Math.floor((Date.now() - startedAt) / 1000)), 1000);
|
|
552
|
+
return () => clearInterval(t);
|
|
553
|
+
}, [running]);
|
|
554
|
+
|
|
555
|
+
// ── approval bridge: tools await this promise, the keypress handler settles it ──
|
|
556
|
+
const requestApproval = React.useCallback((request) => {
|
|
557
|
+
if (bypassModeRef.current) return Promise.resolve('always');
|
|
558
|
+
if (alwaysAllow.current.has(request.tool)) return Promise.resolve('always');
|
|
559
|
+
return new Promise((resolve) => {
|
|
560
|
+
setApproval({ request, resolve });
|
|
561
|
+
});
|
|
562
|
+
}, []);
|
|
563
|
+
|
|
564
|
+
// Read inside the approval callback without re-creating it on every toggle -
|
|
565
|
+
// requestApproval is captured once per run and handed down through several
|
|
566
|
+
// layers of nested subagent calls, so it must see the current value, not a
|
|
567
|
+
// stale one closed over when the run started.
|
|
568
|
+
const bypassModeRef = React.useRef(false);
|
|
569
|
+
React.useEffect(() => { bypassModeRef.current = bypassMode; }, [bypassMode]);
|
|
570
|
+
|
|
571
|
+
// ── subagent bridge: nested runs report in here so the status bar can show them ──
|
|
572
|
+
const onSubagentEvent = React.useCallback((evt) => {
|
|
573
|
+
if (evt.type === 'start') {
|
|
574
|
+
setActiveAgents((prev) => [...prev, { id: evt.id, label: evt.label }]);
|
|
575
|
+
} else if (evt.type === 'end') {
|
|
576
|
+
setActiveAgents((prev) => prev.filter((a) => a.id !== evt.id));
|
|
577
|
+
}
|
|
578
|
+
}, []);
|
|
579
|
+
|
|
580
|
+
const settleApproval = React.useCallback((verdict) => {
|
|
581
|
+
setApproval((current) => {
|
|
582
|
+
if (!current) return null;
|
|
583
|
+
if (verdict === 'always') alwaysAllow.current.add(current.request.tool);
|
|
584
|
+
current.resolve(verdict);
|
|
585
|
+
return null;
|
|
586
|
+
});
|
|
587
|
+
}, []);
|
|
588
|
+
|
|
589
|
+
useInput((char, key) => {
|
|
590
|
+
// The terminal's answer to our kitty query - proof that modified enter is
|
|
591
|
+
// really distinguishable here. Swallow it so it never reaches the draft.
|
|
592
|
+
if (isTerminalReport(char)) { setModifiedEnterWorks(true); return; }
|
|
593
|
+
|
|
594
|
+
// /keys diagnostic: report exactly what the terminal sent for one keypress.
|
|
595
|
+
if (keyProbe) {
|
|
596
|
+
setKeyProbe(false);
|
|
597
|
+
const seq = JSON.stringify(char);
|
|
598
|
+
const flags = ['ctrl', 'shift', 'meta', 'return', 'tab', 'escape', 'backspace', 'delete']
|
|
599
|
+
.filter((f) => key[f]).join(', ') || 'none';
|
|
600
|
+
push({ role: 'system', content: `Key sent: ${seq}\nFlags: ${flags}` });
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// The approval prompt owns the keyboard while it is up.
|
|
605
|
+
if (approval) {
|
|
606
|
+
if (char === 'y' || key.return) { settleApproval('once'); return; }
|
|
607
|
+
if (char === 'a') { settleApproval('always'); return; }
|
|
608
|
+
if (char === 'n' || key.escape) { settleApproval('reject'); return; }
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (key.escape) {
|
|
613
|
+
if (running) { abortRef.current.abort(); setStatusLine('interrupting…'); return; }
|
|
614
|
+
if (showCommands) { setShowCommands(false); return; }
|
|
615
|
+
if (input.length > 0) {
|
|
616
|
+
if (escArmed) { setInput(''); setEscArmed(false); }
|
|
617
|
+
else { setEscArmed(true); }
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (escArmed) { exit(); } else { setEscArmed(true); }
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (key.ctrl && char === 'c') {
|
|
625
|
+
if (running) { abortRef.current.abort(); return; }
|
|
626
|
+
if (input.length > 0) { setInput(''); return; }
|
|
627
|
+
exit();
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (key.tab && key.shift) {
|
|
632
|
+
// Deliberately no chat message here - this is a status-bar toggle, not
|
|
633
|
+
// conversation content, and pushing one would knock a bare "/"-free home
|
|
634
|
+
// screen straight into the compact conversation layout for no reason.
|
|
635
|
+
setBypassMode((b) => !b);
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (key.tab) {
|
|
640
|
+
if (showCommands && filteredCommands.length > 0) {
|
|
641
|
+
const sel = filteredCommands[selectedCommand];
|
|
642
|
+
if (sel) { setInput(sel.name + ' '); setShowCommands(false); }
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
setModeIndex((i) => (i + 1) % MODES.length);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (showCommands) {
|
|
650
|
+
if (key.upArrow) { setSelectedCommand((p) => (p > 0 ? p - 1 : filteredCommands.length - 1)); return; }
|
|
651
|
+
if (key.downArrow) { setSelectedCommand((p) => (p < filteredCommands.length - 1 ? p + 1 : 0)); return; }
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
// ── slash commands ──
|
|
656
|
+
const runCommand = async (cmd, args = '') => {
|
|
657
|
+
if (cmd === '/login') {
|
|
658
|
+
push({ role: 'system', content: 'Opening sign-in…' });
|
|
659
|
+
try {
|
|
660
|
+
await auth.login();
|
|
661
|
+
const s = await auth.getSession();
|
|
662
|
+
setSession(s);
|
|
663
|
+
push({ role: 'system', content: s ? `Signed in as ${s.user.email}` : 'Login cancelled or failed' });
|
|
664
|
+
} catch (e) {
|
|
665
|
+
push({ role: 'error', content: e.message || 'Login failed' });
|
|
666
|
+
}
|
|
667
|
+
} else if (cmd === '/logout') {
|
|
668
|
+
await auth.logout();
|
|
669
|
+
setSession(null);
|
|
670
|
+
push({ role: 'system', content: 'Signed out' });
|
|
671
|
+
} else if (cmd === '/whoami') {
|
|
672
|
+
const s = await auth.getSession();
|
|
673
|
+
push({ role: 'system', content: s ? s.user.email : 'Not signed in' });
|
|
674
|
+
} else if (cmd === '/help') {
|
|
675
|
+
push({
|
|
676
|
+
role: 'system',
|
|
677
|
+
content: 'Commands\n' + COMMANDS.map((c) => `${c.name.padEnd(10)} ${c.description}`).join('\n') +
|
|
678
|
+
'\n\nKeys\nshift+enter / ctrl+j new line\ntab switch mode\nesc interrupt a run\n' +
|
|
679
|
+
`\nModel\n${appConfig.describeProvider()} - change with \`codeply provider\`\n`,
|
|
680
|
+
});
|
|
681
|
+
} else if (cmd === '/clear') {
|
|
682
|
+
// <Static> never re-renders what it already printed, so a clear means
|
|
683
|
+
// wiping the terminal and remounting it with a fresh key. Empty history
|
|
684
|
+
// also drops us back to the home splash.
|
|
685
|
+
convo.current = [];
|
|
686
|
+
if (stdout) stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
687
|
+
setChatHistory([]);
|
|
688
|
+
setStaticKey((k) => k + 1);
|
|
689
|
+
} else if (cmd === '/mode') {
|
|
690
|
+
const next = (modeIndex + 1) % MODES.length;
|
|
691
|
+
setModeIndex(next);
|
|
692
|
+
push({ role: 'system', content: `Mode → ${MODES[next]} - ${MODE_HINT[MODES[next]]}` });
|
|
693
|
+
} else if (cmd === '/provider') {
|
|
694
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
695
|
+
const name = parts[0];
|
|
696
|
+
|
|
697
|
+
if (!name) {
|
|
698
|
+
const cfg = appConfig.getConfig();
|
|
699
|
+
let msg = `Provider: ${appConfig.describeProvider(cfg)}\n` +
|
|
700
|
+
`Providers: ${appConfig.PROVIDERS.join(', ')}\n` +
|
|
701
|
+
'Set with /provider <name> [key] - e.g. /provider openrouter sk-or-v1-...';
|
|
702
|
+
if (cfg.provider === 'codeply') msg += `\n\n${appConfig.byokHint()}`;
|
|
703
|
+
push({ role: 'system', content: msg });
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (!appConfig.PROVIDERS.includes(name)) {
|
|
708
|
+
push({ role: 'error', content: `Unknown provider "${name}". Use one of: ${appConfig.PROVIDERS.join(', ')}.` });
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const key = parts[1];
|
|
713
|
+
const patch = { provider: name };
|
|
714
|
+
if (key && (appConfig.BYOK_PROVIDERS.includes(name) || name === 'ollama')) {
|
|
715
|
+
patch[name] = { apiKey: key };
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const saved = appConfig.saveConfig(patch);
|
|
719
|
+
if (!saved.ok) {
|
|
720
|
+
push({ role: 'error', content: `Could not save config: ${saved.error}` });
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
push({ role: 'system', content: `Provider → ${appConfig.describeProvider(appConfig.getConfig())}` });
|
|
724
|
+
} else if (cmd === '/keys') {
|
|
725
|
+
push({ role: 'system', content: 'Press any key (or combination)…' });
|
|
726
|
+
setKeyProbe(true);
|
|
727
|
+
} else if (cmd === '/skill') {
|
|
728
|
+
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
729
|
+
const sub = parts[0];
|
|
730
|
+
|
|
731
|
+
if (!sub || sub === 'list') {
|
|
732
|
+
const list = skills.listSkills();
|
|
733
|
+
if (list.length === 0) {
|
|
734
|
+
push({ role: 'system', content: 'No skills available.' });
|
|
735
|
+
} else {
|
|
736
|
+
const lines = list.map((s) => `${s.name.padEnd(28)} [${s.source}] ${s.description}`);
|
|
737
|
+
push({ role: 'system', content: `Skills (${list.length})\n` + lines.join('\n') +
|
|
738
|
+
'\n\n/skill install <path-or-github-url> add one\n/skill remove <name> remove a user-installed one' });
|
|
739
|
+
}
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (sub === 'install') {
|
|
744
|
+
const source = parts.slice(1).join(' ');
|
|
745
|
+
if (!source) { push({ role: 'error', content: 'Usage: /skill install <local-path-or-github-url>' }); return; }
|
|
746
|
+
push({ role: 'system', content: `Installing from ${source}…` });
|
|
747
|
+
try {
|
|
748
|
+
const isLocal = fs.existsSync(source);
|
|
749
|
+
const result = isLocal
|
|
750
|
+
? skills.installFromLocalDir(path.resolve(cwd, source))
|
|
751
|
+
: await skills.installFromGitHub(source);
|
|
752
|
+
if (isLocal) {
|
|
753
|
+
if (!result.ok) push({ role: 'error', content: result.error });
|
|
754
|
+
else push({ role: 'system', content: `Installed "${result.name}" → ${result.dest}` });
|
|
755
|
+
} else if (!result.ok) {
|
|
756
|
+
push({ role: 'error', content: result.error || `No skills installed from ${source}.` });
|
|
757
|
+
} else {
|
|
758
|
+
const names = result.installed.map((i) => i.name).join(', ');
|
|
759
|
+
push({ role: 'system', content: `Installed ${result.installed.length} skill(s) from ${result.source}: ${names}` });
|
|
760
|
+
if (result.failed.length) {
|
|
761
|
+
push({ role: 'error', content: `${result.failed.length} failed: ${result.failed.map((f) => f.dir).join(', ')}` });
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
} catch (e) {
|
|
765
|
+
push({ role: 'error', content: `Install failed: ${e.message}` });
|
|
766
|
+
}
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (sub === 'remove') {
|
|
771
|
+
const name = parts[1];
|
|
772
|
+
if (!name) { push({ role: 'error', content: 'Usage: /skill remove <name>' }); return; }
|
|
773
|
+
const result = skills.removeSkill(name);
|
|
774
|
+
push(result.ok
|
|
775
|
+
? { role: 'system', content: `Removed "${name}".` }
|
|
776
|
+
: { role: 'error', content: result.error });
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
push({ role: 'error', content: `Unknown /skill subcommand "${sub}". Use list, install, or remove.` });
|
|
781
|
+
} else if (cmd === '/quit' || cmd === '/exit') {
|
|
782
|
+
exit();
|
|
783
|
+
} else {
|
|
784
|
+
push({ role: 'error', content: `Unknown command: ${cmd}. Type /help for available commands.` });
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
|
|
788
|
+
// ── main turn ──
|
|
789
|
+
const handleSubmit = async (value) => {
|
|
790
|
+
const text = value.trim();
|
|
791
|
+
if (!text || running) return;
|
|
792
|
+
|
|
793
|
+
let submitValue = text;
|
|
794
|
+
if (showCommands && filteredCommands[selectedCommand]) {
|
|
795
|
+
const raw = text.split(/\s+/)[0].toLowerCase();
|
|
796
|
+
if (!COMMANDS.some((c) => c.name === raw)) {
|
|
797
|
+
submitValue = filteredCommands[selectedCommand].name;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
setShowCommands(false);
|
|
801
|
+
setInput('');
|
|
802
|
+
|
|
803
|
+
if (submitValue.startsWith('/')) {
|
|
804
|
+
const [first, ...rest] = submitValue.split(/\s+/);
|
|
805
|
+
await runCommand(first.toLowerCase(), rest.join(' '));
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
push({ role: 'user', content: submitValue });
|
|
810
|
+
setRunning(true);
|
|
811
|
+
setStatusLine('thinking…');
|
|
812
|
+
setSteps(0);
|
|
813
|
+
abortRef.current = new AbortController();
|
|
814
|
+
|
|
815
|
+
const turn = [];
|
|
816
|
+
try {
|
|
817
|
+
const stream = runAgent({
|
|
818
|
+
userMessage: submitValue,
|
|
819
|
+
history: convo.current.slice(-20),
|
|
820
|
+
mode,
|
|
821
|
+
cwd,
|
|
822
|
+
approve: requestApproval,
|
|
823
|
+
signal: abortRef.current.signal,
|
|
824
|
+
onSubagentEvent,
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
for await (const event of stream) {
|
|
828
|
+
if (event.type === 'text') {
|
|
829
|
+
// In Plan mode the substantial reply is the plan itself; short
|
|
830
|
+
// interstitial notes ("let me check X") stay as ordinary prose.
|
|
831
|
+
const isPlan = mode === 'Plan' && event.text.split('\n').length >= 3;
|
|
832
|
+
push({ role: 'assistant', content: event.text, plan: isPlan });
|
|
833
|
+
turn.push({ role: 'assistant', content: event.text });
|
|
834
|
+
setStatusLine('working…');
|
|
835
|
+
} else if (event.type === 'tool_start') {
|
|
836
|
+
const display = TOOL_DISPLAY[event.name] || { verb: event.name };
|
|
837
|
+
setSteps((n) => n + 1);
|
|
838
|
+
setStatusLine(`${display.verb} ${event.args.path || event.args.command || event.args.pattern || ''}`.trim());
|
|
839
|
+
} else if (event.type === 'tool_end') {
|
|
840
|
+
const meta = event.meta || {};
|
|
841
|
+
const unit = event.name === 'search' ? 'matches'
|
|
842
|
+
: event.name === 'list_dir' ? 'entries'
|
|
843
|
+
: 'lines';
|
|
844
|
+
let detail = '';
|
|
845
|
+
if (meta.count != null) detail = `${meta.count} ${unit}`;
|
|
846
|
+
if (meta.added != null) detail = `-${meta.removed || 0} +${meta.added}`;
|
|
847
|
+
if (meta.exitCode != null) detail = `exit ${meta.exitCode}`;
|
|
848
|
+
if (meta.noop) detail = 'no change';
|
|
849
|
+
if (meta.rejected) detail = 'declined';
|
|
850
|
+
push({
|
|
851
|
+
role: 'tool',
|
|
852
|
+
name: event.name,
|
|
853
|
+
ok: event.ok && !meta.rejected && (meta.exitCode == null || meta.exitCode === 0),
|
|
854
|
+
label: event.summary || '',
|
|
855
|
+
detail,
|
|
856
|
+
});
|
|
857
|
+
setStatusLine('thinking…');
|
|
858
|
+
} else if (event.type === 'aborted') {
|
|
859
|
+
push({ role: 'system', content: 'Interrupted.' });
|
|
860
|
+
} else if (event.type === 'error') {
|
|
861
|
+
push({ role: 'error', content: event.error });
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
} catch (e) {
|
|
865
|
+
push({ role: 'error', content: e.message });
|
|
866
|
+
} finally {
|
|
867
|
+
convo.current = [...convo.current, { role: 'user', content: submitValue }, ...turn];
|
|
868
|
+
setRunning(false);
|
|
869
|
+
setStatusLine('');
|
|
870
|
+
// A run that ended while a prompt was up must not leave a dangling promise.
|
|
871
|
+
setApproval((current) => { if (current) current.resolve('reject'); return null; });
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
// Ollama needs no Codeply sign-in, so showing "not signed in" there would be
|
|
876
|
+
// a false alarm - surface the model that is actually answering instead.
|
|
877
|
+
const provider = React.useMemo(() => appConfig.getConfig(), []);
|
|
878
|
+
const usingOllama = provider.provider === 'ollama';
|
|
879
|
+
const account = usingOllama
|
|
880
|
+
? appConfig.describeProviderShort(provider)
|
|
881
|
+
: session ? session.user.email : 'not signed in';
|
|
882
|
+
|
|
883
|
+
// Advertise the key that actually inserts a newline in THIS terminal.
|
|
884
|
+
const newlineKey = modifiedEnterWorks ? 'shift+enter' : 'ctrl+j';
|
|
885
|
+
|
|
886
|
+
const isHome = chatHistory.length === 0;
|
|
887
|
+
// The splash needs real estate; in a short or narrow window fall straight
|
|
888
|
+
// through to the compact layout rather than rendering a cramped one.
|
|
889
|
+
const showSplash = isHome && rows >= 22 && cols >= 44;
|
|
890
|
+
// 1 for this session's own run, plus whatever subagents it has spawned and
|
|
891
|
+
// are still going - matches what the status bar's "← N agent" is counting.
|
|
892
|
+
const agentCount = 1 + activeAgents.length;
|
|
893
|
+
|
|
894
|
+
// On home the composer is a centred column like an app window; in
|
|
895
|
+
// conversation it spans the width so long messages have room.
|
|
896
|
+
const shellWidth = showSplash ? Math.min(cols - 4, 92) : cols - 2;
|
|
897
|
+
const inner = Math.max(20, shellWidth);
|
|
898
|
+
|
|
899
|
+
// Two-column rows are laid out by hand: Ink truncates each child against the
|
|
900
|
+
// width it was given, so without explicit widths the columns run into each
|
|
901
|
+
// other on narrow terminals instead of truncating cleanly.
|
|
902
|
+
const rightHint = escArmed
|
|
903
|
+
? `esc again to ${input.length > 0 ? 'clear' : 'quit'}`
|
|
904
|
+
: running ? 'esc interrupts' : 'enter to send';
|
|
905
|
+
const footRight = fitPath(homePath(cwd), Math.max(12, Math.floor(inner * 0.45))) + ' codeply ' + VERSION;
|
|
906
|
+
const hintsWidth = Math.max(10, inner - rightHint.length - 2);
|
|
907
|
+
const tipWidth = inner - footRight.length - 2;
|
|
908
|
+
const showTip = tipWidth >= 24;
|
|
909
|
+
|
|
910
|
+
const composer = React.createElement(
|
|
911
|
+
Box,
|
|
912
|
+
{
|
|
913
|
+
key: 'composer',
|
|
914
|
+
flexDirection: 'column',
|
|
915
|
+
borderStyle: 'bold',
|
|
916
|
+
borderTop: false,
|
|
917
|
+
borderRight: false,
|
|
918
|
+
borderBottom: false,
|
|
919
|
+
borderLeftColor: running ? T.muted : modeColor,
|
|
920
|
+
paddingLeft: 1,
|
|
921
|
+
width: shellWidth,
|
|
922
|
+
marginTop: showSplash ? 2 : 0,
|
|
923
|
+
flexShrink: 0,
|
|
924
|
+
},
|
|
925
|
+
React.createElement(TextArea, {
|
|
926
|
+
value: input,
|
|
927
|
+
onChange: setInput,
|
|
928
|
+
onSubmit: handleSubmit,
|
|
929
|
+
width: inner - 3,
|
|
930
|
+
focus: !running && !approval,
|
|
931
|
+
allowVertical: !showCommands,
|
|
932
|
+
placeholder: running
|
|
933
|
+
? 'working - esc to interrupt'
|
|
934
|
+
: 'Ask for a change, a fix, or an explanation… (/ for commands)',
|
|
935
|
+
}),
|
|
936
|
+
React.createElement(
|
|
937
|
+
Text,
|
|
938
|
+
{ wrap: 'truncate-end' },
|
|
939
|
+
hexToAnsi(modeColor) + ANSI_BOLD + mode + ANSI_RESET +
|
|
940
|
+
hexToAnsi(T.faint) + ' · ' + ANSI_RESET +
|
|
941
|
+
hexToAnsi(T.muted) + MODE_HINT[mode] + ANSI_RESET +
|
|
942
|
+
hexToAnsi(T.faint) + ' · ' + ANSI_RESET +
|
|
943
|
+
hexToAnsi(usingOllama || session ? T.dim : G.yellow) + account + ANSI_RESET
|
|
944
|
+
)
|
|
945
|
+
);
|
|
946
|
+
|
|
947
|
+
const hints = React.createElement(
|
|
948
|
+
Box,
|
|
949
|
+
{ key: 'hints', flexDirection: 'row', justifyContent: 'space-between', width: shellWidth, marginTop: 1, flexShrink: 0 },
|
|
950
|
+
React.createElement(
|
|
951
|
+
Box,
|
|
952
|
+
{ width: hintsWidth, flexShrink: 0 },
|
|
953
|
+
React.createElement(
|
|
954
|
+
Text,
|
|
955
|
+
{ wrap: 'truncate-end' },
|
|
956
|
+
hexToAnsi(G.blue) + 'tab' + ANSI_RESET + hexToAnsi(T.faint) + ' mode ' + ANSI_RESET +
|
|
957
|
+
hexToAnsi(G.red) + newlineKey + ANSI_RESET + hexToAnsi(T.faint) + ' newline ' + ANSI_RESET +
|
|
958
|
+
hexToAnsi(G.yellow) + '/' + ANSI_RESET + hexToAnsi(T.faint) + ' commands ' + ANSI_RESET +
|
|
959
|
+
hexToAnsi(G.green) + 'esc' + ANSI_RESET + hexToAnsi(T.faint) + ' interrupt' + ANSI_RESET
|
|
960
|
+
)
|
|
961
|
+
),
|
|
962
|
+
React.createElement(
|
|
963
|
+
Text,
|
|
964
|
+
null,
|
|
965
|
+
(escArmed ? hexToAnsi(G.yellow) : hexToAnsi(T.faint)) + rightHint + ANSI_RESET
|
|
966
|
+
)
|
|
967
|
+
);
|
|
968
|
+
|
|
969
|
+
const tipLine = React.createElement(
|
|
970
|
+
Box,
|
|
971
|
+
{ key: 'tip', width: shellWidth, flexShrink: 0 },
|
|
972
|
+
showTip && React.createElement(
|
|
973
|
+
Text,
|
|
974
|
+
{ wrap: 'truncate-end' },
|
|
975
|
+
hexToAnsi(G.yellow) + '• ' + ANSI_RESET +
|
|
976
|
+
hexToAnsi(T.muted) + tip[0] + ANSI_RESET +
|
|
977
|
+
hexToAnsi(T.faint) + ' ' + tip[1] + ANSI_RESET
|
|
978
|
+
)
|
|
979
|
+
);
|
|
980
|
+
|
|
981
|
+
const footer = React.createElement(
|
|
982
|
+
Box,
|
|
983
|
+
{ key: 'footer', flexDirection: 'row', justifyContent: 'space-between', width: shellWidth, flexShrink: 0 },
|
|
984
|
+
React.createElement(
|
|
985
|
+
Box,
|
|
986
|
+
{ width: Math.max(0, tipWidth), flexShrink: 0 },
|
|
987
|
+
showTip && React.createElement(
|
|
988
|
+
Text,
|
|
989
|
+
{ wrap: 'truncate-end' },
|
|
990
|
+
hexToAnsi(G.yellow) + '• ' + ANSI_RESET +
|
|
991
|
+
hexToAnsi(T.muted) + tip[0] + ANSI_RESET +
|
|
992
|
+
hexToAnsi(T.faint) + ' ' + tip[1] + ANSI_RESET
|
|
993
|
+
)
|
|
994
|
+
),
|
|
995
|
+
React.createElement(Text, null, hexToAnsi(T.faint) + footRight + ANSI_RESET)
|
|
996
|
+
);
|
|
997
|
+
|
|
998
|
+
const statusBar = React.createElement(
|
|
999
|
+
Box,
|
|
1000
|
+
{ key: 'statusbar', width: shellWidth, marginTop: 1, flexShrink: 0 },
|
|
1001
|
+
React.createElement(StatusBar, { bypassMode, agentCount })
|
|
1002
|
+
);
|
|
1003
|
+
|
|
1004
|
+
// ── Home: one full-viewport screen, centred like an app ──
|
|
1005
|
+
if (showSplash) {
|
|
1006
|
+
const avail = rows - 1;
|
|
1007
|
+
const canBox = cols >= BOX_MIN_COLS;
|
|
1008
|
+
|
|
1009
|
+
const { leftInner, rightInner } = boxHomeMetrics(cols);
|
|
1010
|
+
const welcomeName = (session?.user?.email || '').split('@')[0] || null;
|
|
1011
|
+
const leftRows = [
|
|
1012
|
+
{ text: '' },
|
|
1013
|
+
{ text: centered(welcomeName ? `Welcome back, ${welcomeName}!` : 'Welcome to Codeply', leftInner), bold: true, color: T.text },
|
|
1014
|
+
{ text: '' },
|
|
1015
|
+
...MASCOT_ROWS.map((row) => ({ text: centered(row, leftInner), color: T.accent })),
|
|
1016
|
+
{ text: '' },
|
|
1017
|
+
{ text: centered(`${mode} mode · v${VERSION}`, leftInner), color: T.muted },
|
|
1018
|
+
{ text: centered(usingOllama || session ? account : 'not signed in - /login', leftInner), color: usingOllama || session ? T.secondary : G.yellow },
|
|
1019
|
+
{ text: centered(fitPath(homePath(cwd), Math.max(10, leftInner - 2)), leftInner), color: T.faint },
|
|
1020
|
+
];
|
|
1021
|
+
const rightRows = [
|
|
1022
|
+
{ text: 'Tips for getting started', bold: true, color: T.accent },
|
|
1023
|
+
{ text: `${tip[0]} ${tip[1]}` },
|
|
1024
|
+
{ text: '' },
|
|
1025
|
+
{ text: 'What\'s new', bold: true, color: T.accent },
|
|
1026
|
+
...WHATS_NEW.map((line) => ({ text: line, color: T.secondary })),
|
|
1027
|
+
].map((r) => ({ color: T.secondary, ...r }));
|
|
1028
|
+
|
|
1029
|
+
return React.createElement(
|
|
1030
|
+
Box,
|
|
1031
|
+
{ flexDirection: 'column', width: cols, height: avail, alignItems: 'center', justifyContent: 'center' },
|
|
1032
|
+
canBox
|
|
1033
|
+
? React.createElement(BoxHome, {
|
|
1034
|
+
key: 'home', cols, title: `Codeply CLI v${VERSION}`,
|
|
1035
|
+
left: leftRows, right: rightRows, borderColor: T.accent,
|
|
1036
|
+
})
|
|
1037
|
+
: React.createElement(CompactMark, { key: 'home', cols }),
|
|
1038
|
+
composer,
|
|
1039
|
+
hints,
|
|
1040
|
+
tipLine,
|
|
1041
|
+
statusBar
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// ── Conversation: scrollback + a deliberately short live frame ──
|
|
1046
|
+
return React.createElement(
|
|
1047
|
+
Box,
|
|
1048
|
+
{ flexDirection: 'column', width: cols, paddingX: 1 },
|
|
1049
|
+
|
|
1050
|
+
// ── Scrollback: printed once, never redrawn. Keeps the live frame short. ──
|
|
1051
|
+
React.createElement(
|
|
1052
|
+
Static,
|
|
1053
|
+
{ key: staticKey, items: chatHistory },
|
|
1054
|
+
(msg) => React.createElement(MessageBlock, { key: msg.id, msg, cols: cols - 2 })
|
|
1055
|
+
),
|
|
1056
|
+
|
|
1057
|
+
// ── Live frame below ──
|
|
1058
|
+
|
|
1059
|
+
running && React.createElement(
|
|
1060
|
+
Box,
|
|
1061
|
+
{ flexDirection: 'row', gap: 1, marginBottom: 1, flexShrink: 0 },
|
|
1062
|
+
React.createElement(Text, { color: T.accent }, React.createElement(Spinner, { type: 'dots' })),
|
|
1063
|
+
React.createElement(
|
|
1064
|
+
Text,
|
|
1065
|
+
{ wrap: 'truncate-end' },
|
|
1066
|
+
hexToAnsi(T.dim) + (statusLine || 'working…') + ANSI_RESET +
|
|
1067
|
+
hexToAnsi(T.faint) + ' ' + elapsed + 's' + ANSI_RESET +
|
|
1068
|
+
(steps > 0 ? hexToAnsi(T.faint) + ' · step ' + steps + ANSI_RESET : '')
|
|
1069
|
+
)
|
|
1070
|
+
),
|
|
1071
|
+
|
|
1072
|
+
approval && React.createElement(ApprovalPrompt, { request: approval.request }),
|
|
1073
|
+
|
|
1074
|
+
showCommands && React.createElement(CommandsMenu, {
|
|
1075
|
+
commands: filteredCommands,
|
|
1076
|
+
selectedIndex: selectedCommand,
|
|
1077
|
+
}),
|
|
1078
|
+
|
|
1079
|
+
composer,
|
|
1080
|
+
hints,
|
|
1081
|
+
footer,
|
|
1082
|
+
statusBar
|
|
1083
|
+
);
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
export default App;
|