@awak-app/simy-cli 0.1.0 → 0.1.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.
@@ -0,0 +1,1042 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { stripVTControlCharacters } from "node:util";
4
+ import { homedir } from "node:os";
5
+ import { resolve as resolvePath } from "node:path";
6
+ import { Box, Spacer, Text, useApp, useInput, useStdout } from "ink";
7
+
8
+ import { consoleHelpLines, parseConsoleCommand } from "./commands.js";
9
+ import { summarizeCodingLoopEvent } from "../orchestrator/presentation.js";
10
+ import { isLocalRepositoryApprovalPending } from "../runner.js";
11
+
12
+ const TERMINAL_STATES = new Set([
13
+ "merge_ready",
14
+ "pr_ready_for_review",
15
+ "waiting_human",
16
+ "blocked",
17
+ "failed",
18
+ "stopped",
19
+ ]);
20
+ const NEW_TASK = "__new_task__";
21
+
22
+ export function CodingLoopConsole({ agent, onQuit = () => {} }) {
23
+ const { exit } = useApp();
24
+ const { stdout } = useStdout();
25
+ const [terminal, setTerminal] = useState(() => terminalSize(stdout));
26
+ const [runs, setRuns] = useState(() => agent.registry.list());
27
+ const [selectedId, setSelectedId] = useState(() => runs[0]?.id ?? NEW_TASK);
28
+ const [inputMode, setInputMode] = useState(() => runs.length === 0);
29
+ const [input, setInput] = useState("");
30
+ const [busy, setBusy] = useState(false);
31
+ const [notice, setNotice] = useState("");
32
+ const [showHelp, setShowHelp] = useState(false);
33
+ const [confirmStop, setConfirmStop] = useState(false);
34
+ const [detailOpen, setDetailOpen] = useState(false);
35
+ const [detailScrollFromEnd, setDetailScrollFromEnd] = useState(0);
36
+ const [scanPrompt, setScanPrompt] = useState(null);
37
+ const [repositoryPicker, setRepositoryPicker] = useState(null);
38
+ const [draft, setDraft] = useState(() => initialDraft(agent));
39
+
40
+ useEffect(() => {
41
+ const update = (nextRuns) => {
42
+ setRuns([...nextRuns]);
43
+ setSelectedId((current) => {
44
+ if (current === NEW_TASK) return current;
45
+ return nextRuns.some((run) => run.id === current)
46
+ ? current
47
+ : nextRuns[0]?.id ?? NEW_TASK;
48
+ });
49
+ };
50
+ agent.registry.on("change", update);
51
+ return () => agent.registry.off("change", update);
52
+ }, [agent.registry]);
53
+
54
+ useEffect(() => {
55
+ const resize = () => setTerminal(terminalSize(stdout));
56
+ stdout.on("resize", resize);
57
+ return () => stdout.off("resize", resize);
58
+ }, [stdout]);
59
+
60
+ const selectedRun = runs.find((run) => run.id === selectedId) ?? null;
61
+ const selectableIds = [NEW_TASK, ...runs.map((run) => run.id)];
62
+ const selectedIndex = Math.max(0, selectableIds.indexOf(selectedId));
63
+ const waitingForHuman = Boolean(
64
+ selectedRun &&
65
+ (selectedRun.status === "waiting_human" || selectedRun.controlState === "waiting_human"),
66
+ );
67
+ const actionRows =
68
+ 1 +
69
+ (waitingForHuman ? 1 : 0) +
70
+ (!selectedRun ? 1 : 0) +
71
+ (notice || busy ? 1 : 0);
72
+ const mainHeight = Math.max(8, terminal.rows - 5 - actionRows);
73
+ const detailInnerWidth = Math.max(20, terminal.columns - 4);
74
+ const detailRows = useMemo(
75
+ () => buildRunDetailRows(selectedRun, detailInnerWidth),
76
+ [selectedRun, detailInnerWidth, runs],
77
+ );
78
+ const detailViewportRows = Math.max(1, mainHeight - 3);
79
+ const maxDetailScroll = Math.max(0, detailRows.length - detailViewportRows);
80
+ const effectiveDetailScroll = Math.min(detailScrollFromEnd, maxDetailScroll);
81
+
82
+ useEffect(() => {
83
+ setDetailScrollFromEnd(0);
84
+ }, [selectedId]);
85
+
86
+ useEffect(() => {
87
+ if (detailOpen && !selectedRun) setDetailOpen(false);
88
+ }, [detailOpen, selectedRun]);
89
+
90
+ useEffect(() => {
91
+ if (selectedRun?.status === "pr_ready_for_review") {
92
+ setNotice("Review the pull request, then use /recheck to refresh readiness.");
93
+ } else if (
94
+ selectedRun?.status === "waiting_human" ||
95
+ selectedRun?.controlState === "waiting_human"
96
+ ) {
97
+ setNotice("SIMY is waiting for your guidance.");
98
+ }
99
+ }, [selectedRun?.status, selectedRun?.controlState]);
100
+
101
+ async function execute(value) {
102
+ const command = parseConsoleCommand(value);
103
+ if (command.type === "empty") return;
104
+ if (command.type === "error") {
105
+ setNotice(command.message);
106
+ if (!selectedRun) setInputMode(true);
107
+ return;
108
+ }
109
+ if (command.type === "help") {
110
+ setShowHelp(true);
111
+ return;
112
+ }
113
+ if (command.type === "new") {
114
+ setSelectedId(NEW_TASK);
115
+ setDetailOpen(false);
116
+ setInputMode(true);
117
+ setNotice("New task composer ready.");
118
+ return;
119
+ }
120
+ if (command.type === "scan") {
121
+ requestRepositoryScan(command.root);
122
+ return;
123
+ }
124
+ if (command.type === "repositories") {
125
+ const repositories = agent.controls.repositoryInventory?.() || [];
126
+ if (repositories.length === 0) {
127
+ requestRepositoryScan();
128
+ } else {
129
+ openRepositoryPicker(repositories);
130
+ }
131
+ return;
132
+ }
133
+ if (command.type === "configure") {
134
+ setDraft((current) => updateDraft(current, command));
135
+ setNotice(configurationNotice(command));
136
+ setInputMode(true);
137
+ return;
138
+ }
139
+ if (!selectedRun) {
140
+ if (command.type !== "guidance") {
141
+ setNotice("That command requires a selected coding run.");
142
+ return;
143
+ }
144
+ setBusy(true);
145
+ setNotice("Creating the Web ledger and starting the local executor...");
146
+ try {
147
+ const run = await agent.controls.create({
148
+ requirement: command.message,
149
+ repository: draft.repository,
150
+ baseBranch: draft.branch,
151
+ backend: draft.backend,
152
+ localPath: draft.localPath,
153
+ attachmentPaths: draft.attachmentPaths,
154
+ });
155
+ setSelectedId(run.id);
156
+ setDetailOpen(true);
157
+ setDetailScrollFromEnd(0);
158
+ setNotice("Task created from CLI chat. Local execution is starting.");
159
+ } catch (error) {
160
+ setNotice(error instanceof Error ? error.message : String(error));
161
+ setInputMode(true);
162
+ } finally {
163
+ setBusy(false);
164
+ }
165
+ return;
166
+ }
167
+
168
+ setBusy(true);
169
+ setNotice("");
170
+ try {
171
+ switch (command.type) {
172
+ case "guidance":
173
+ if (selectedRun.operation || selectedRun.child) {
174
+ agent.controls.queueGuidance(selectedRun, command.message);
175
+ setNotice("Guidance queued for the next executor handoff.");
176
+ } else {
177
+ await agent.controls.continue(selectedRun, command.message);
178
+ setNotice("Human guidance accepted. The coding loop is continuing.");
179
+ }
180
+ break;
181
+ case "decision":
182
+ await agent.controls.applyDecision(selectedRun, command);
183
+ setNotice("Human decision recorded. The coding loop is continuing.");
184
+ break;
185
+ case "recheck":
186
+ await agent.controls.recheck(selectedRun);
187
+ setNotice("Pull request evidence refreshed.");
188
+ break;
189
+ case "pause":
190
+ agent.controls.pause(selectedRun);
191
+ setNotice("Executor paused.");
192
+ break;
193
+ case "resume":
194
+ agent.controls.resume(selectedRun);
195
+ setNotice("Executor resumed.");
196
+ break;
197
+ case "stop":
198
+ await agent.controls.stop(selectedRun);
199
+ setNotice("Stop requested.");
200
+ break;
201
+ }
202
+ } catch (error) {
203
+ setNotice(error instanceof Error ? error.message : String(error));
204
+ } finally {
205
+ setBusy(false);
206
+ setConfirmStop(false);
207
+ }
208
+ }
209
+
210
+ function quit() {
211
+ onQuit();
212
+ exit();
213
+ }
214
+
215
+ function requestRepositoryScan(root = null) {
216
+ const requestedRoot = String(root || agent.repositoryScanRoot || process.cwd());
217
+ const expandedRoot = requestedRoot === "~"
218
+ ? homedir()
219
+ : requestedRoot.startsWith("~/")
220
+ ? resolvePath(homedir(), requestedRoot.slice(2))
221
+ : resolvePath(requestedRoot);
222
+ setInputMode(false);
223
+ setDetailOpen(false);
224
+ setRepositoryPicker(null);
225
+ setScanPrompt({ root: expandedRoot });
226
+ setNotice("");
227
+ }
228
+
229
+ function openRepositoryPicker(repositories) {
230
+ const expected = selectedRun?.request.repository || draft.repository;
231
+ const selected = Math.max(
232
+ 0,
233
+ repositories.findIndex(
234
+ (item) => item.repository.toLowerCase() === String(expected || "").toLowerCase(),
235
+ ),
236
+ );
237
+ setInputMode(false);
238
+ setDetailOpen(false);
239
+ setRepositoryPicker({ repositories, selected });
240
+ setNotice("");
241
+ }
242
+
243
+ async function authorizeRepositoryScan() {
244
+ if (!scanPrompt || busy) return;
245
+ const root = scanPrompt.root;
246
+ setScanPrompt(null);
247
+ setBusy(true);
248
+ setNotice(`Scanning authorized root ${root}...`);
249
+ try {
250
+ const result = await agent.controls.scanRepositories(root);
251
+ const discoveredRepositories = result.discoveredRepositories || result.repositories;
252
+ const match = selectedRun
253
+ ? discoveredRepositories.find(
254
+ (item) =>
255
+ item.repository.toLowerCase() === selectedRun.request.repository.toLowerCase(),
256
+ )
257
+ : null;
258
+ if (selectedRun && match) {
259
+ await agent.controls.selectRepository(selectedRun, match);
260
+ setDetailOpen(true);
261
+ setDetailScrollFromEnd(0);
262
+ setNotice(`Authorized ${match.repository}. The same coding run is continuing.`);
263
+ } else if (selectedRun) {
264
+ setNotice(
265
+ `${selectedRun.request.repository} was not found under ${result.root}. Run /scan <path> for another authorized root.`,
266
+ );
267
+ } else if (discoveredRepositories.length > 0) {
268
+ openRepositoryPicker(discoveredRepositories);
269
+ setNotice(
270
+ `Found ${discoveredRepositories.length} local repositories under ${result.root}. Choose one with Enter.`,
271
+ );
272
+ } else {
273
+ setNotice(`No GitHub repositories were found under ${result.root}.`);
274
+ setInputMode(true);
275
+ }
276
+ } catch (error) {
277
+ setNotice(error instanceof Error ? error.message : String(error));
278
+ } finally {
279
+ setBusy(false);
280
+ }
281
+ }
282
+
283
+ async function selectRepositoryFromPicker() {
284
+ const selected = repositoryPicker?.repositories[repositoryPicker.selected];
285
+ if (!selected || busy) return;
286
+ setBusy(true);
287
+ try {
288
+ if (selectedRun) {
289
+ await agent.controls.selectRepository(selectedRun, selected);
290
+ setRepositoryPicker(null);
291
+ setDetailOpen(true);
292
+ setDetailScrollFromEnd(0);
293
+ setNotice(`Authorized ${selected.repository}. The same coding run is continuing.`);
294
+ } else {
295
+ const verified = await agent.controls.selectRepository(null, selected);
296
+ setDraft((current) => ({
297
+ ...current,
298
+ repository: verified.repository,
299
+ branch: verified.branch,
300
+ localPath: verified.local_path,
301
+ }));
302
+ setRepositoryPicker(null);
303
+ setInputMode(true);
304
+ setNotice(`Repository set to ${verified.repository} at ${verified.local_path}.`);
305
+ }
306
+ } catch (error) {
307
+ setNotice(error instanceof Error ? error.message : String(error));
308
+ } finally {
309
+ setBusy(false);
310
+ }
311
+ }
312
+
313
+ const inputHandlerRef = useRef(() => {});
314
+ inputHandlerRef.current = (character, key) => {
315
+ if (scanPrompt) {
316
+ if (key.escape || String(character).toLowerCase() === "n") {
317
+ setScanPrompt(null);
318
+ setNotice("Repository scan cancelled. No filesystem access was granted.");
319
+ return;
320
+ }
321
+ if (String(character).toLowerCase() === "y") void authorizeRepositoryScan();
322
+ return;
323
+ }
324
+
325
+ if (repositoryPicker) {
326
+ if (key.escape) {
327
+ setRepositoryPicker(null);
328
+ setNotice("Repository selection cancelled.");
329
+ return;
330
+ }
331
+ if (key.upArrow || character === "k") {
332
+ setRepositoryPicker((current) => ({
333
+ ...current,
334
+ selected: Math.max(0, current.selected - 1),
335
+ }));
336
+ return;
337
+ }
338
+ if (key.downArrow || character === "j") {
339
+ setRepositoryPicker((current) => ({
340
+ ...current,
341
+ selected: Math.min(current.repositories.length - 1, current.selected + 1),
342
+ }));
343
+ return;
344
+ }
345
+ if (key.return) void selectRepositoryFromPicker();
346
+ return;
347
+ }
348
+
349
+ if (inputMode) {
350
+ if (key.escape || (key.ctrl && character === "c")) {
351
+ setInputMode(false);
352
+ setInput("");
353
+ return;
354
+ }
355
+ if (key.return) {
356
+ const submitted = input;
357
+ setInput("");
358
+ setInputMode(false);
359
+ void execute(submitted);
360
+ return;
361
+ }
362
+ if (key.backspace || key.delete) {
363
+ setInput((current) => current.slice(0, -1));
364
+ return;
365
+ }
366
+ if (key.ctrl && character === "u") {
367
+ setInput("");
368
+ return;
369
+ }
370
+ if (!key.ctrl && !key.meta && character) setInput((current) => current + character);
371
+ return;
372
+ }
373
+
374
+ if (key.ctrl && character === "c") {
375
+ quit();
376
+ return;
377
+ }
378
+
379
+ if (showHelp && key.escape) {
380
+ setShowHelp(false);
381
+ return;
382
+ }
383
+
384
+ if (detailOpen && selectedRun) {
385
+ if (key.escape) {
386
+ setDetailOpen(false);
387
+ setDetailScrollFromEnd(0);
388
+ return;
389
+ }
390
+ if (key.upArrow || character === "k") {
391
+ setDetailScrollFromEnd((current) => Math.min(maxDetailScroll, current + 1));
392
+ return;
393
+ }
394
+ if (key.downArrow || character === "j") {
395
+ setDetailScrollFromEnd((current) => Math.max(0, current - 1));
396
+ return;
397
+ }
398
+ if (key.pageUp) {
399
+ setDetailScrollFromEnd((current) =>
400
+ Math.min(maxDetailScroll, current + Math.max(1, detailViewportRows - 2)),
401
+ );
402
+ return;
403
+ }
404
+ if (key.pageDown) {
405
+ setDetailScrollFromEnd((current) =>
406
+ Math.max(0, current - Math.max(1, detailViewportRows - 2)),
407
+ );
408
+ return;
409
+ }
410
+ if (key.home || character === "g") {
411
+ setDetailScrollFromEnd(maxDetailScroll);
412
+ return;
413
+ }
414
+ if (key.end || character === "G") {
415
+ setDetailScrollFromEnd(0);
416
+ return;
417
+ }
418
+ }
419
+
420
+ if (key.upArrow || character === "k") {
421
+ const next = Math.max(0, selectedIndex - 1);
422
+ setSelectedId(selectableIds[next] ?? selectedId);
423
+ return;
424
+ }
425
+ if (key.downArrow || character === "j") {
426
+ const next = Math.min(selectableIds.length - 1, selectedIndex + 1);
427
+ setSelectedId(selectableIds[next] ?? selectedId);
428
+ return;
429
+ }
430
+ if (key.return && selectedRun) {
431
+ setDetailOpen(true);
432
+ setDetailScrollFromEnd(maxDetailScroll);
433
+ return;
434
+ }
435
+ if (character === "i" || key.return) {
436
+ setInputMode(true);
437
+ return;
438
+ }
439
+ if (character === "/") {
440
+ setInput("/");
441
+ setInputMode(true);
442
+ return;
443
+ }
444
+ if (character === "?") {
445
+ setShowHelp((current) => !current);
446
+ return;
447
+ }
448
+ if (character === "s") {
449
+ requestRepositoryScan();
450
+ return;
451
+ }
452
+ if (character === "p" && selectedRun) {
453
+ void execute(selectedRun.controlState === "paused" ? "/resume" : "/pause");
454
+ return;
455
+ }
456
+ if (character === "r" && selectedRun) {
457
+ void execute("/recheck");
458
+ return;
459
+ }
460
+ if (character === "x" && selectedRun) {
461
+ if (confirmStop) void execute("/stop");
462
+ else {
463
+ setConfirmStop(true);
464
+ setNotice("Press x again to stop the selected coding run.");
465
+ }
466
+ return;
467
+ }
468
+ if (character === "q") quit();
469
+ };
470
+ const handleInput = useCallback(
471
+ (character, key) => inputHandlerRef.current(character, key),
472
+ [],
473
+ );
474
+ useInput(handleInput);
475
+
476
+ return jsxs(Box, {
477
+ flexDirection: "column",
478
+ width: terminal.columns,
479
+ children: [
480
+ jsx(Header, { agent, runCount: runs.length }),
481
+ jsx(Box, {
482
+ marginTop: 1,
483
+ height: mainHeight,
484
+ children: showHelp
485
+ ? jsx(Help, { width: terminal.columns })
486
+ : scanPrompt
487
+ ? jsx(RepositoryScanConsent, {
488
+ root: scanPrompt.root,
489
+ width: terminal.columns,
490
+ height: mainHeight,
491
+ })
492
+ : repositoryPicker
493
+ ? jsx(RepositoryPicker, {
494
+ repositories: repositoryPicker.repositories,
495
+ selected: repositoryPicker.selected,
496
+ requiredRepository: selectedRun?.request.repository || null,
497
+ width: terminal.columns,
498
+ height: mainHeight,
499
+ })
500
+ : detailOpen && selectedRun
501
+ ? jsx(RunDetail, {
502
+ run: selectedRun,
503
+ rows: detailRows,
504
+ scrollFromEnd: effectiveDetailScroll,
505
+ width: terminal.columns,
506
+ height: mainHeight,
507
+ })
508
+ : selectedRun
509
+ ? jsx(RunList, {
510
+ runs,
511
+ selectedId,
512
+ width: terminal.columns,
513
+ height: mainHeight,
514
+ })
515
+ : jsx(NewTask, {
516
+ draft,
517
+ loginUrl: agent.loginUrl,
518
+ width: terminal.columns,
519
+ }),
520
+ }),
521
+ jsx(ActionBar, {
522
+ run: selectedRun,
523
+ detailOpen,
524
+ inputMode,
525
+ input,
526
+ busy,
527
+ notice,
528
+ confirmStop,
529
+ draft,
530
+ repositoryMissing: isRepositoryMissing(selectedRun),
531
+ modalOpen: Boolean(scanPrompt || repositoryPicker),
532
+ }),
533
+ ],
534
+ });
535
+ }
536
+
537
+ function Header({ agent, runCount }) {
538
+ return jsxs(Box, {
539
+ borderStyle: "single",
540
+ borderColor: "cyan",
541
+ paddingX: 1,
542
+ children: [
543
+ jsx(Text, { bold: true, color: "cyan", children: "SIMY" }),
544
+ jsx(Text, { bold: true, children: " Coding Chat" }),
545
+ jsx(Text, { dimColor: true, children: ` localhost:${agent.port}` }),
546
+ jsx(Spacer, {}),
547
+ jsx(Text, { color: runCount > 0 ? "green" : "yellow", children: `${runCount} runs` }),
548
+ jsx(Text, { dimColor: true, children: ` ${agent.webOrigin}` }),
549
+ ],
550
+ });
551
+ }
552
+
553
+ function NewTask({ draft, loginUrl, width }) {
554
+ return jsxs(Box, {
555
+ width,
556
+ flexDirection: "column",
557
+ borderStyle: "single",
558
+ borderColor: "cyan",
559
+ paddingX: 1,
560
+ children: [
561
+ jsx(Text, { bold: true, color: "cyan", children: "What do you want to build?" }),
562
+ jsx(Text, {
563
+ dimColor: true,
564
+ children: "Describe the change in the composer. SIMY will create the ledger and run it here.",
565
+ }),
566
+ jsx(Text, { children: `Repository ${draft.repository || "not set"}` }),
567
+ jsx(Text, { children: `Branch ${draft.branch}` }),
568
+ jsx(Text, { children: `Executor ${executorLabel(draft.backend)}` }),
569
+ jsx(Text, {
570
+ children: `Attachments ${draft.attachmentPaths.length === 0 ? "none" : draft.attachmentPaths.join(", ")}`,
571
+ }),
572
+ loginUrl ? jsx(Text, { color: "cyan", children: `Sign in: ${loginUrl}` }) : null,
573
+ ],
574
+ });
575
+ }
576
+
577
+ function RepositoryScanConsent({ root, width, height }) {
578
+ return jsxs(Box, {
579
+ width,
580
+ height,
581
+ flexDirection: "column",
582
+ borderStyle: "single",
583
+ borderColor: "yellow",
584
+ paddingX: 1,
585
+ children: [
586
+ jsx(Text, { bold: true, color: "yellow", children: "Allow local repository scan?" }),
587
+ jsx(Text, { children: " " }),
588
+ jsx(Text, { bold: true, children: "Authorized root" }),
589
+ jsx(Text, { color: "cyan", wrap: "wrap", children: root }),
590
+ jsx(Text, { children: " " }),
591
+ jsx(Text, {
592
+ wrap: "wrap",
593
+ children:
594
+ "SIMY will recursively look for Git repositories below this folder. It reads directory names plus each repository's Git origin and current branch.",
595
+ }),
596
+ jsx(Text, {
597
+ wrap: "wrap",
598
+ children:
599
+ "Source files and file contents are not indexed or uploaded. Hidden, dependency, cache, and symlink directories are skipped.",
600
+ }),
601
+ jsx(Text, { children: " " }),
602
+ jsx(Text, { bold: true, color: "green", children: "Press y to allow this scan" }),
603
+ jsx(Text, { dimColor: true, children: "Press n or Esc to cancel. Use /scan <path> to choose a narrower root." }),
604
+ ],
605
+ });
606
+ }
607
+
608
+ function RepositoryPicker({ repositories, selected, requiredRepository, width, height }) {
609
+ const maxVisible = Math.max(1, height - 5);
610
+ const start = Math.min(
611
+ Math.max(0, selected - Math.floor(maxVisible / 2)),
612
+ Math.max(0, repositories.length - maxVisible),
613
+ );
614
+ const visible = repositories.slice(start, start + maxVisible);
615
+ return jsxs(Box, {
616
+ width,
617
+ height,
618
+ flexDirection: "column",
619
+ borderStyle: "single",
620
+ borderColor: "cyan",
621
+ paddingX: 1,
622
+ children: [
623
+ jsxs(Box, {
624
+ children: [
625
+ jsx(Text, { bold: true, color: "cyan", children: "LOCAL GIT REPOSITORIES" }),
626
+ jsx(Spacer, {}),
627
+ jsx(Text, { dimColor: true, children: `${repositories.length} indexed` }),
628
+ ],
629
+ }),
630
+ requiredRepository
631
+ ? jsx(Text, {
632
+ color: "yellow",
633
+ children: `This run requires ${requiredRepository}`,
634
+ })
635
+ : jsx(Text, { dimColor: true, children: "Choose the repository for the next coding task." }),
636
+ ...visible.map((repository, offset) => {
637
+ const index = start + offset;
638
+ const active = index === selected;
639
+ const eligible =
640
+ !requiredRepository ||
641
+ repository.repository.toLowerCase() === requiredRepository.toLowerCase();
642
+ return jsxs(Box, {
643
+ flexDirection: "column",
644
+ borderStyle: active ? "round" : undefined,
645
+ borderColor: active ? (eligible ? "cyan" : "yellow") : undefined,
646
+ paddingX: active ? 1 : 2,
647
+ children: [
648
+ jsx(Text, {
649
+ bold: active,
650
+ color: eligible ? (active ? "cyan" : undefined) : "yellow",
651
+ children: `${active ? "> " : " "}${repository.repository} ${repository.branch}`,
652
+ }),
653
+ jsx(Text, { dimColor: true, wrap: "wrap", children: repository.local_path }),
654
+ ],
655
+ }, repository.local_path);
656
+ }),
657
+ jsx(Text, { dimColor: true, children: "Up/Down select Enter use repository Esc cancel" }),
658
+ ],
659
+ });
660
+ }
661
+
662
+ function RunList({ runs, selectedId, width, height }) {
663
+ return jsxs(Box, {
664
+ width,
665
+ height,
666
+ flexDirection: "column",
667
+ borderStyle: "single",
668
+ borderColor: "gray",
669
+ paddingX: 1,
670
+ children: [
671
+ jsxs(Box, {
672
+ children: [
673
+ jsx(Text, { bold: true, children: "RUNS" }),
674
+ jsx(Spacer, {}),
675
+ jsx(Text, { dimColor: true, children: "Enter opens the selected run full screen" }),
676
+ ],
677
+ }),
678
+ jsxs(Box, {
679
+ borderStyle: selectedId === NEW_TASK ? "round" : undefined,
680
+ borderColor: selectedId === NEW_TASK ? "cyan" : undefined,
681
+ paddingX: selectedId === NEW_TASK ? 1 : 2,
682
+ children: [
683
+ jsx(Text, { color: "cyan", bold: selectedId === NEW_TASK, children: "+ New task" }),
684
+ ],
685
+ }),
686
+ ...runs.slice(0, 10).map((run) => {
687
+ const selected = run.id === selectedId;
688
+ return jsxs(Box, {
689
+ flexDirection: "column",
690
+ borderStyle: selected ? "round" : undefined,
691
+ borderColor: selected ? "cyan" : undefined,
692
+ paddingX: selected ? 1 : 2,
693
+ children: [
694
+ jsxs(Text, {
695
+ children: [
696
+ jsx(Text, { color: statusColor(run), children: `${statusGlyph(run)} ` }),
697
+ jsx(Text, { bold: selected, children: shortId(run.id) }),
698
+ jsx(Text, { dimColor: true, children: ` ${run.request.backend}` }),
699
+ ],
700
+ }),
701
+ jsx(Text, { dimColor: true, children: truncate(run.request.requirement, width - 6) }),
702
+ ],
703
+ }, run.id);
704
+ }),
705
+ ],
706
+ });
707
+ }
708
+
709
+ function RunDetail({ run, rows, scrollFromEnd, width, height }) {
710
+ if (!run) return null;
711
+ const viewportRows = Math.max(1, height - 3);
712
+ const maxScroll = Math.max(0, rows.length - viewportRows);
713
+ const scroll = Math.min(scrollFromEnd, maxScroll);
714
+ const start = Math.max(0, rows.length - viewportRows - scroll);
715
+ const visibleRows = rows.slice(start, start + viewportRows);
716
+ const end = Math.min(rows.length, start + visibleRows.length);
717
+ return jsxs(Box, {
718
+ width,
719
+ height,
720
+ flexDirection: "column",
721
+ borderStyle: "single",
722
+ borderColor: "gray",
723
+ paddingX: 1,
724
+ children: [
725
+ jsxs(Box, {
726
+ children: [
727
+ jsxs(Text, {
728
+ children: [
729
+ jsx(Text, { bold: true, children: run.id }),
730
+ jsx(Text, { color: statusColor(run), children: ` ${statusLabel(run)}` }),
731
+ ],
732
+ }),
733
+ jsx(Spacer, {}),
734
+ jsx(Text, {
735
+ dimColor: true,
736
+ children: `${start + 1}-${end} / ${rows.length}`,
737
+ }),
738
+ jsx(Text, {
739
+ color: scroll > 0 ? "yellow" : run.child ? "green" : "gray",
740
+ children: scroll > 0 ? " ● scroll paused" : run.child ? " ● live" : " ○ idle",
741
+ }),
742
+ ],
743
+ }),
744
+ ...visibleRows.map((row, index) =>
745
+ jsx(Text, {
746
+ bold: row.bold,
747
+ color: row.color,
748
+ dimColor: row.dimColor,
749
+ children: row.text || " ",
750
+ }, `${start + index}-${row.text}`),
751
+ ),
752
+ ],
753
+ });
754
+ }
755
+
756
+ function Help({ width }) {
757
+ return jsxs(Box, {
758
+ width,
759
+ borderStyle: "single",
760
+ borderColor: "blue",
761
+ paddingX: 1,
762
+ flexDirection: "column",
763
+ children: [
764
+ jsx(Text, { bold: true, color: "blue", children: "COMMANDS" }),
765
+ ...consoleHelpLines().map((line) => jsx(Text, { children: line }, line)),
766
+ ],
767
+ });
768
+ }
769
+
770
+ function ActionBar({
771
+ run,
772
+ detailOpen,
773
+ inputMode,
774
+ input,
775
+ busy,
776
+ notice,
777
+ confirmStop,
778
+ draft,
779
+ repositoryMissing,
780
+ modalOpen,
781
+ }) {
782
+ const waiting = run && (run.controlState === "waiting_human" || run.status === "waiting_human");
783
+ return jsxs(Box, {
784
+ flexDirection: "column",
785
+ marginTop: 1,
786
+ children: [
787
+ waiting
788
+ ? jsx(Text, {
789
+ bold: true,
790
+ color: "yellow",
791
+ children: repositoryMissing
792
+ ? "LOCAL REPOSITORY REQUIRED Press s to review and authorize a local Git repository scan."
793
+ : "HUMAN INPUT REQUIRED Press i, type guidance, then Enter.",
794
+ })
795
+ : null,
796
+ modalOpen
797
+ ? null
798
+ : inputMode
799
+ ? jsxs(Text, {
800
+ children: [
801
+ jsx(Text, { color: "cyan", bold: true, children: "simy> " }),
802
+ input,
803
+ jsx(Text, { inverse: true, children: " " }),
804
+ ],
805
+ })
806
+ : jsx(Text, {
807
+ dimColor: true,
808
+ children: detailOpen
809
+ ? "↑/↓ scroll PgUp/PgDn page Home/End Esc runs i guidance / command s scan repos p pause/resume r recheck x stop ? help q quit"
810
+ : "↑/↓ select Enter open i guidance / command s scan repos ? help q quit",
811
+ }),
812
+ !run
813
+ ? jsx(Text, {
814
+ dimColor: true,
815
+ children: `${draft.repository || "repo not set"} ${draft.branch} ${executorLabel(draft.backend)}`,
816
+ })
817
+ : null,
818
+ notice
819
+ ? jsx(Text, {
820
+ color: confirmStop ? "yellow" : notice.includes("could not") || notice.includes("requires") ? "red" : "cyan",
821
+ children: `${busy ? "Working... " : ""}${notice}`,
822
+ })
823
+ : busy
824
+ ? jsx(Text, { color: "cyan", children: "Working..." })
825
+ : null,
826
+ ],
827
+ });
828
+ }
829
+
830
+ function initialDraft(agent) {
831
+ const backends = agent.capabilities?.backends ?? {};
832
+ return {
833
+ repository: agent.workspace?.repository ?? "",
834
+ branch: agent.workspace?.branch ?? "dev",
835
+ localPath: agent.workspace?.localPath ?? process.cwd(),
836
+ backend: backends.claude ? "claude" : "codex",
837
+ attachmentPaths: [],
838
+ };
839
+ }
840
+
841
+ function isRepositoryMissing(run) {
842
+ return isLocalRepositoryApprovalPending(run);
843
+ }
844
+
845
+ function updateDraft(current, command) {
846
+ if (command.field === "repository") {
847
+ return { ...current, repository: command.value, localPath: null };
848
+ }
849
+ if (command.field === "branch") return { ...current, branch: command.value };
850
+ if (command.field === "backend") return { ...current, backend: command.value };
851
+ if (command.field === "attachment") {
852
+ return current.attachmentPaths.includes(command.value)
853
+ ? current
854
+ : { ...current, attachmentPaths: [...current.attachmentPaths, command.value] };
855
+ }
856
+ if (command.field === "clearAttachments") return { ...current, attachmentPaths: [] };
857
+ return current;
858
+ }
859
+
860
+ function configurationNotice(command) {
861
+ if (command.field === "clearAttachments") return "Attachments cleared.";
862
+ if (command.field === "attachment") return `Attached local path: ${command.value}`;
863
+ return `${command.field} set to ${command.value}.`;
864
+ }
865
+
866
+ function executorLabel(value) {
867
+ return value === "claude" ? "Claude Code" : "Codex";
868
+ }
869
+
870
+ function buildRunDetailRows(run, width) {
871
+ if (!run) return [];
872
+ const latestEvent = run.snapshot.events.at(-1);
873
+ const presentation = latestEvent ? summarizeCodingLoopEvent(run.snapshot, latestEvent) : null;
874
+ const rows = [];
875
+ const add = (value = "", style = {}) => {
876
+ for (const text of wrapTerminalText(value, width)) rows.push({ text, ...style });
877
+ };
878
+
879
+ add(
880
+ `PID ${run.child?.pid ?? "-"} attempt ${run.snapshot.attempts.length + (run.child ? 1 : 0)}`,
881
+ { dimColor: true },
882
+ );
883
+ add(`Repository: ${run.request.repository}`, { dimColor: true });
884
+ if (run.request.attachments?.length) {
885
+ add(`Attachments: ${run.request.attachments.map((item) => item.name).join(", ")}`, {
886
+ dimColor: true,
887
+ });
888
+ }
889
+ add();
890
+ add("YOU", { bold: true, color: "cyan" });
891
+ add(run.request.requirement);
892
+ add();
893
+ add("SIMY", { bold: true, color: "green" });
894
+ add(statusLabel(run), { color: statusColor(run) });
895
+
896
+ if (presentation) {
897
+ add();
898
+ add("STEP SUMMARY", { bold: true, color: "cyan" });
899
+ add(presentation.summary);
900
+ if (presentation.result) add(`Result: ${presentation.result}`);
901
+ if (presentation.next) add(`Next: ${presentation.next}`);
902
+ }
903
+
904
+ add();
905
+ add(`LIVE TRANSCRIPT ${run.child ? "● streaming" : "○ idle"}`, {
906
+ bold: true,
907
+ color: run.child ? "green" : undefined,
908
+ });
909
+ for (const line of runLogLines(run)) add(line);
910
+ return rows;
911
+ }
912
+
913
+ function runLogLines(run) {
914
+ const source =
915
+ run.logs.length > 0
916
+ ? run.logs.map((entry) => entry.text)
917
+ : run.snapshot.events.map((event) => {
918
+ const presentation = summarizeCodingLoopEvent(run.snapshot, event);
919
+ return `[${event.state}] ${event.message} - ${presentation.summary}`;
920
+ });
921
+ return source.map((line) => stripVTControlCharacters(String(line)));
922
+ }
923
+
924
+ function wrapTerminalText(value, maxWidth) {
925
+ const width = Math.max(1, maxWidth);
926
+ const source = stripVTControlCharacters(String(value ?? "")).replaceAll("\t", " ");
927
+ const output = [];
928
+
929
+ for (const paragraph of source.split(/\r?\n/)) {
930
+ if (!paragraph) {
931
+ output.push("");
932
+ continue;
933
+ }
934
+ let line = "";
935
+ let lineWidth = 0;
936
+ const pushLine = () => {
937
+ if (line) output.push(line);
938
+ line = "";
939
+ lineWidth = 0;
940
+ };
941
+ for (const word of paragraph.trim().split(/\s+/u)) {
942
+ const wordWidth = terminalTextWidth(word);
943
+ if (wordWidth <= width) {
944
+ const separatorWidth = line ? 1 : 0;
945
+ if (line && lineWidth + separatorWidth + wordWidth > width) pushLine();
946
+ if (line) {
947
+ line += " ";
948
+ lineWidth += 1;
949
+ }
950
+ line += word;
951
+ lineWidth += wordWidth;
952
+ continue;
953
+ }
954
+
955
+ pushLine();
956
+ for (const character of word) {
957
+ const characterWidth = terminalCharacterWidth(character);
958
+ if (line && lineWidth + characterWidth > width) pushLine();
959
+ line += character;
960
+ lineWidth += characterWidth;
961
+ }
962
+ }
963
+ pushLine();
964
+ }
965
+ return output.length > 0 ? output : [""];
966
+ }
967
+
968
+ function terminalTextWidth(value) {
969
+ let width = 0;
970
+ for (const character of value) width += terminalCharacterWidth(character);
971
+ return width;
972
+ }
973
+
974
+ function terminalCharacterWidth(character) {
975
+ const codePoint = character.codePointAt(0) ?? 0;
976
+ if (codePoint === 0 || codePoint < 32 || (codePoint >= 0x7f && codePoint < 0xa0)) return 0;
977
+ if (/\p{Mark}/u.test(character)) return 0;
978
+ return isFullWidthCodePoint(codePoint) ? 2 : 1;
979
+ }
980
+
981
+ function isFullWidthCodePoint(codePoint) {
982
+ return (
983
+ codePoint >= 0x1100 &&
984
+ (codePoint <= 0x115f ||
985
+ codePoint === 0x2329 ||
986
+ codePoint === 0x232a ||
987
+ (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
988
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
989
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
990
+ (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
991
+ (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
992
+ (codePoint >= 0xff00 && codePoint <= 0xff60) ||
993
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
994
+ (codePoint >= 0x1f300 && codePoint <= 0x1faff) ||
995
+ (codePoint >= 0x20000 && codePoint <= 0x3fffd))
996
+ );
997
+ }
998
+
999
+ function terminalSize(stdout) {
1000
+ return {
1001
+ columns: Math.max(72, stdout.columns || 120),
1002
+ rows: Math.max(24, stdout.rows || 32),
1003
+ };
1004
+ }
1005
+
1006
+ function shortId(value) {
1007
+ const id = String(value || "");
1008
+ return id.length > 28 ? `${id.slice(0, 25)}...` : id;
1009
+ }
1010
+
1011
+ function truncate(value, max) {
1012
+ const text = stripVTControlCharacters(String(value || "")).replace(/\s+/g, " ");
1013
+ return text.length > max ? `${text.slice(0, Math.max(1, max - 3))}...` : text;
1014
+ }
1015
+
1016
+ function statusGlyph(run) {
1017
+ if (run.controlState === "paused") return "Ⅱ";
1018
+ if (run.controlState === "waiting_human") return "!";
1019
+ if (run.controlState === "stopped" || run.status === "stopped") return "■";
1020
+ if (["blocked", "failed"].includes(run.status)) return "×";
1021
+ if (run.status === "merge_ready") return "✓";
1022
+ return run.child || !TERMINAL_STATES.has(run.status) ? "●" : "○";
1023
+ }
1024
+
1025
+ function statusLabel(run) {
1026
+ if (run.controlState === "paused") return "PAUSED";
1027
+ if (run.controlState === "stopping") return "STOPPING";
1028
+ if (run.controlState === "stopped" || run.status === "stopped") return "STOPPED";
1029
+ if (run.status === "pr_ready_for_review") return "PR READY - HUMAN REVIEW";
1030
+ if (run.controlState === "waiting_human" || run.status === "waiting_human") {
1031
+ return "WAITING FOR HUMAN";
1032
+ }
1033
+ if (run.status === "merge_ready") return "READY TO MERGE";
1034
+ return String(run.status || "queued").replaceAll("_", " ").toUpperCase();
1035
+ }
1036
+
1037
+ function statusColor(run) {
1038
+ if (run.controlState === "waiting_human" || run.controlState === "paused") return "yellow";
1039
+ if (["failed", "blocked", "stopped"].includes(run.status)) return "red";
1040
+ if (run.controlState === "complete") return "green";
1041
+ return "cyan";
1042
+ }