@mandipadk7/kavi 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.
package/dist/tui.js CHANGED
@@ -1,91 +1,1672 @@
1
+ import path from "node:path";
1
2
  import readline from "node:readline";
2
3
  import process from "node:process";
3
- import { listApprovalRequests, resolveApprovalRequest } from "./approvals.js";
4
- import { appendCommand } from "./command-queue.js";
5
- import { loadSessionRecord, readRecentEvents } from "./session.js";
6
- function divider(title) {
7
- return `\n=== ${title} ${"=".repeat(Math.max(1, 60 - title.length))}`;
8
- }
9
- function render(session, events, approvals) {
10
- const agentLines = Object.values(session.agentStatus).map((status)=>`- ${status.agent}: ${status.transport} | last_exit=${status.lastExitCode ?? "-"} | last_run=${status.lastRunAt ?? "-"}`).join("\n");
11
- const worktreeLines = session.worktrees.map((worktree)=>`- ${worktree.agent}: ${worktree.path} (${worktree.branch})`).join("\n");
12
- const taskLines = session.tasks.map((task)=>`- ${task.id} | ${task.owner} | ${task.status} | ${task.title}${task.summary ? ` | ${task.summary}` : ""}`).join("\n");
13
- const messageLines = session.peerMessages.slice(-8).map((message)=>`- ${message.from} -> ${message.to} [${message.intent}] ${message.subject}`).join("\n");
14
- const approvalLines = approvals.slice(-6).map((request)=>`- ${request.id} | ${request.agent} | ${request.summary}`).join("\n");
15
- const eventLines = events.slice(-8).map((event)=>`- ${event.timestamp} ${event.type}`).join("\n");
4
+ import { extractPromptPathHints, routeTask } from "./router.js";
5
+ import { pingRpc, readSnapshot, rpcEnqueueTask, rpcResolveApproval, rpcShutdown, rpcTaskArtifact, rpcWorktreeDiff, subscribeSnapshotRpc } from "./rpc.js";
6
+ const RESET = "\u001b[0m";
7
+ const ANSI_PATTERN = /\u001b\[[0-9;]*m/g;
8
+ const SUBSCRIPTION_RETRY_MS = 1_000;
9
+ const TOAST_DURATION_MS = 4_500;
10
+ const STYLES = {
11
+ accent: "\u001b[36m",
12
+ muted: "\u001b[90m",
13
+ good: "\u001b[32m",
14
+ warn: "\u001b[33m",
15
+ bad: "\u001b[31m",
16
+ strong: "\u001b[1m",
17
+ reverse: "\u001b[7m"
18
+ };
19
+ export const OPERATOR_TABS = [
20
+ "tasks",
21
+ "approvals",
22
+ "claims",
23
+ "decisions",
24
+ "events",
25
+ "messages",
26
+ "worktrees"
27
+ ];
28
+ const TASK_DETAIL_SECTIONS = [
29
+ "overview",
30
+ "prompt",
31
+ "replay",
32
+ "output",
33
+ "diff"
34
+ ];
35
+ function styleLine(text, ...tones) {
36
+ if (tones.length === 0) {
37
+ return text;
38
+ }
39
+ return `${tones.map((tone)=>STYLES[tone]).join("")}${text}${RESET}`;
40
+ }
41
+ function stripAnsi(value) {
42
+ return value.replaceAll(ANSI_PATTERN, "");
43
+ }
44
+ function visibleLength(value) {
45
+ return stripAnsi(value).length;
46
+ }
47
+ function sliceAnsi(value, width) {
48
+ if (width <= 0) {
49
+ return "";
50
+ }
51
+ let output = "";
52
+ let visible = 0;
53
+ let index = 0;
54
+ while(index < value.length && visible < width){
55
+ if (value[index] === "\u001b") {
56
+ const remainder = value.slice(index);
57
+ const match = remainder.match(/^\u001b\[[0-9;]*m/);
58
+ if (match) {
59
+ output += match[0];
60
+ index += match[0].length;
61
+ continue;
62
+ }
63
+ }
64
+ output += value[index];
65
+ visible += 1;
66
+ index += 1;
67
+ }
68
+ if (output.includes("\u001b[") && !output.endsWith(RESET)) {
69
+ output += RESET;
70
+ }
71
+ return output;
72
+ }
73
+ function fitAnsiLine(value, width) {
74
+ if (width <= 0) {
75
+ return "";
76
+ }
77
+ const trimmed = value.replaceAll(/\r/g, "");
78
+ const length = visibleLength(trimmed);
79
+ if (length === width) {
80
+ return trimmed;
81
+ }
82
+ if (length > width) {
83
+ return sliceAnsi(trimmed, width);
84
+ }
85
+ return `${trimmed}${" ".repeat(width - length)}`;
86
+ }
87
+ export function wrapText(value, width) {
88
+ const targetWidth = Math.max(8, width);
89
+ const source = value.replaceAll("\r", "");
90
+ if (!source.trim()) {
91
+ return [
92
+ ""
93
+ ];
94
+ }
95
+ const lines = [];
96
+ for (const paragraph of source.split("\n")){
97
+ if (!paragraph.trim()) {
98
+ lines.push("");
99
+ continue;
100
+ }
101
+ let current = "";
102
+ for (const word of paragraph.split(/\s+/)){
103
+ if (!word) {
104
+ continue;
105
+ }
106
+ if (word.length > targetWidth) {
107
+ if (current) {
108
+ lines.push(current);
109
+ current = "";
110
+ }
111
+ for(let index = 0; index < word.length; index += targetWidth){
112
+ lines.push(word.slice(index, index + targetWidth));
113
+ }
114
+ continue;
115
+ }
116
+ const candidate = current ? `${current} ${word}` : word;
117
+ if (candidate.length > targetWidth) {
118
+ lines.push(current);
119
+ current = word;
120
+ } else {
121
+ current = candidate;
122
+ }
123
+ }
124
+ if (current) {
125
+ lines.push(current);
126
+ }
127
+ }
128
+ return lines.length > 0 ? lines : [
129
+ ""
130
+ ];
131
+ }
132
+ function wrapPreformatted(value, width) {
133
+ const targetWidth = Math.max(8, width);
134
+ const lines = [];
135
+ for (const sourceLine of value.replaceAll("\r", "").split("\n")){
136
+ if (sourceLine.length === 0) {
137
+ lines.push("");
138
+ continue;
139
+ }
140
+ if (sourceLine.length <= targetWidth) {
141
+ lines.push(sourceLine);
142
+ continue;
143
+ }
144
+ for(let index = 0; index < sourceLine.length; index += targetWidth){
145
+ lines.push(sourceLine.slice(index, index + targetWidth));
146
+ }
147
+ }
148
+ return lines.length > 0 ? lines : [
149
+ ""
150
+ ];
151
+ }
152
+ function section(title, lines) {
153
+ return [
154
+ `-- ${title} --`,
155
+ ...lines
156
+ ];
157
+ }
158
+ function truncateValue(value, width) {
159
+ if (width <= 0) {
160
+ return "";
161
+ }
162
+ if (value.length <= width) {
163
+ return value;
164
+ }
165
+ if (width <= 3) {
166
+ return ".".repeat(width);
167
+ }
168
+ return `${value.slice(0, width - 3)}...`;
169
+ }
170
+ function shortTime(value) {
171
+ if (!value) {
172
+ return "-";
173
+ }
174
+ return value.replace("T", " ").replace(/\.\d+Z$/, "Z");
175
+ }
176
+ function statusTone(status) {
177
+ switch(status){
178
+ case "completed":
179
+ case "approved":
180
+ return "good";
181
+ case "pending":
182
+ case "running":
183
+ case "blocked":
184
+ return "warn";
185
+ case "failed":
186
+ case "denied":
187
+ return "bad";
188
+ default:
189
+ return "muted";
190
+ }
191
+ }
192
+ function toneLine(value, tone, selected) {
193
+ if (selected) {
194
+ return styleLine(value, "reverse");
195
+ }
196
+ switch(tone){
197
+ case "good":
198
+ return styleLine(value, "good");
199
+ case "warn":
200
+ return styleLine(value, "warn");
201
+ case "bad":
202
+ return styleLine(value, "bad");
203
+ case "muted":
204
+ return styleLine(value, "muted");
205
+ default:
206
+ return value;
207
+ }
208
+ }
209
+ function countTasks(tasks, status) {
210
+ return tasks.filter((task)=>task.status === status).length;
211
+ }
212
+ function changedPathCount(diff) {
213
+ return diff?.paths.length ?? 0;
214
+ }
215
+ function findWorktreeDiff(snapshot, agent) {
216
+ return snapshot.worktreeDiffs.find((diff)=>diff.agent === agent);
217
+ }
218
+ function taskPriority(task) {
219
+ switch(task.status){
220
+ case "running":
221
+ return 0;
222
+ case "blocked":
223
+ return 1;
224
+ case "pending":
225
+ return 2;
226
+ case "failed":
227
+ return 3;
228
+ case "completed":
229
+ return 4;
230
+ default:
231
+ return 5;
232
+ }
233
+ }
234
+ export function buildTabItems(snapshot, tab) {
235
+ if (!snapshot) {
236
+ return [];
237
+ }
238
+ const { session } = snapshot;
239
+ switch(tab){
240
+ case "tasks":
241
+ return [
242
+ ...session.tasks
243
+ ].sort((left, right)=>{
244
+ const priority = taskPriority(left) - taskPriority(right);
245
+ if (priority !== 0) {
246
+ return priority;
247
+ }
248
+ return right.updatedAt.localeCompare(left.updatedAt);
249
+ }).map((task)=>({
250
+ id: task.id,
251
+ title: `[${task.status}] ${task.owner} ${task.title}`,
252
+ detail: task.summary ?? task.routeReason ?? task.prompt,
253
+ tone: statusTone(task.status)
254
+ }));
255
+ case "approvals":
256
+ return [
257
+ ...snapshot.approvals
258
+ ].sort((left, right)=>{
259
+ const pendingDelta = Number(right.status === "pending") - Number(left.status === "pending");
260
+ if (pendingDelta !== 0) {
261
+ return pendingDelta;
262
+ }
263
+ return right.updatedAt.localeCompare(left.updatedAt);
264
+ }).map((approval)=>({
265
+ id: approval.id,
266
+ title: `[${approval.status}] ${approval.agent} ${approval.toolName}`,
267
+ detail: approval.summary,
268
+ tone: statusTone(approval.status === "approved" ? "approved" : approval.status === "denied" ? "denied" : approval.status)
269
+ }));
270
+ case "claims":
271
+ return [
272
+ ...session.pathClaims
273
+ ].sort((left, right)=>{
274
+ const activeDelta = Number(right.status === "active") - Number(left.status === "active");
275
+ if (activeDelta !== 0) {
276
+ return activeDelta;
277
+ }
278
+ return right.updatedAt.localeCompare(left.updatedAt);
279
+ }).map((claim)=>({
280
+ id: claim.id,
281
+ title: `[${claim.status}] ${claim.agent} ${claim.source}`,
282
+ detail: claim.paths.join(", ") || "(no paths)",
283
+ tone: claim.status === "active" ? "warn" : "muted"
284
+ }));
285
+ case "decisions":
286
+ return [
287
+ ...session.decisions
288
+ ].sort((left, right)=>right.createdAt.localeCompare(left.createdAt)).map((decision)=>({
289
+ id: decision.id,
290
+ title: `[${decision.kind}] ${decision.summary}`,
291
+ detail: decision.detail,
292
+ tone: decision.kind === "integration" ? "warn" : "normal"
293
+ }));
294
+ case "events":
295
+ return [
296
+ ...snapshot.events
297
+ ].sort((left, right)=>right.timestamp.localeCompare(left.timestamp)).map((event)=>({
298
+ id: event.id,
299
+ title: event.type,
300
+ detail: shortTime(event.timestamp),
301
+ tone: "muted"
302
+ }));
303
+ case "messages":
304
+ return [
305
+ ...session.peerMessages
306
+ ].sort((left, right)=>right.createdAt.localeCompare(left.createdAt)).map((message)=>({
307
+ id: message.id,
308
+ title: `${message.from} -> ${message.to} [${message.intent}]`,
309
+ detail: message.subject,
310
+ tone: "normal"
311
+ }));
312
+ case "worktrees":
313
+ return session.worktrees.map((worktree)=>({
314
+ id: worktree.agent,
315
+ title: `${worktree.agent} ${worktree.branch}`,
316
+ detail: `${changedPathCount(findWorktreeDiff(snapshot, worktree.agent))} changed | ${path.basename(worktree.path)}`,
317
+ tone: changedPathCount(findWorktreeDiff(snapshot, worktree.agent)) > 0 ? "warn" : "good"
318
+ }));
319
+ default:
320
+ return [];
321
+ }
322
+ }
323
+ function emptySelectionMap() {
324
+ return {
325
+ tasks: null,
326
+ approvals: null,
327
+ claims: null,
328
+ decisions: null,
329
+ events: null,
330
+ messages: null,
331
+ worktrees: null
332
+ };
333
+ }
334
+ export function moveSelectionId(items, currentId, delta) {
335
+ if (items.length === 0) {
336
+ return null;
337
+ }
338
+ const currentIndex = Math.max(0, items.findIndex((item)=>item.id === currentId));
339
+ const nextIndex = (currentIndex + delta + items.length) % items.length;
340
+ return items[nextIndex]?.id ?? items[0]?.id ?? null;
341
+ }
342
+ export function nextTab(current, delta) {
343
+ const index = OPERATOR_TABS.indexOf(current);
344
+ const nextIndex = (index + delta + OPERATOR_TABS.length) % OPERATOR_TABS.length;
345
+ return OPERATOR_TABS[nextIndex] ?? current;
346
+ }
347
+ function defaultSelectionForTab(snapshot, tab) {
348
+ const items = buildTabItems(snapshot, tab);
349
+ return items[0]?.id ?? null;
350
+ }
351
+ function syncSelections(selectedIds, snapshot) {
352
+ if (!snapshot) {
353
+ return emptySelectionMap();
354
+ }
355
+ const next = {
356
+ ...selectedIds
357
+ };
358
+ for (const tab of OPERATOR_TABS){
359
+ const items = buildTabItems(snapshot, tab);
360
+ next[tab] = items.some((item)=>item.id === selectedIds[tab]) ? selectedIds[tab] : defaultSelectionForTab(snapshot, tab);
361
+ }
362
+ return next;
363
+ }
364
+ function selectedItem(snapshot, ui) {
365
+ const items = buildTabItems(snapshot, ui.activeTab);
366
+ return items.find((item)=>item.id === ui.selectedIds[ui.activeTab]) ?? items[0] ?? null;
367
+ }
368
+ function selectedTask(snapshot, ui) {
369
+ if (!snapshot) {
370
+ return null;
371
+ }
372
+ const selectedId = ui.selectedIds.tasks;
373
+ return snapshot.session.tasks.find((task)=>task.id === selectedId) ?? null;
374
+ }
375
+ function selectedApproval(snapshot, ui) {
376
+ if (!snapshot) {
377
+ return null;
378
+ }
379
+ const selectedId = ui.selectedIds.approvals;
380
+ return snapshot.approvals.find((approval)=>approval.id === selectedId) ?? latestPendingApproval(snapshot.approvals);
381
+ }
382
+ function selectedClaim(snapshot, ui) {
383
+ if (!snapshot) {
384
+ return null;
385
+ }
386
+ const selectedId = ui.selectedIds.claims;
387
+ return snapshot.session.pathClaims.find((claim)=>claim.id === selectedId) ?? null;
388
+ }
389
+ function selectedDecision(snapshot, ui) {
390
+ if (!snapshot) {
391
+ return null;
392
+ }
393
+ const selectedId = ui.selectedIds.decisions;
394
+ return snapshot.session.decisions.find((decision)=>decision.id === selectedId) ?? null;
395
+ }
396
+ function selectedEvent(snapshot, ui) {
397
+ if (!snapshot) {
398
+ return null;
399
+ }
400
+ const selectedId = ui.selectedIds.events;
401
+ return snapshot.events.find((event)=>event.id === selectedId) ?? null;
402
+ }
403
+ function selectedMessage(snapshot, ui) {
404
+ if (!snapshot) {
405
+ return null;
406
+ }
407
+ const selectedId = ui.selectedIds.messages;
408
+ return snapshot.session.peerMessages.find((message)=>message.id === selectedId) ?? null;
409
+ }
410
+ function selectedWorktree(snapshot, ui) {
411
+ if (!snapshot) {
412
+ return null;
413
+ }
414
+ const selectedId = ui.selectedIds.worktrees;
415
+ return snapshot.session.worktrees.find((worktree)=>worktree.agent === selectedId) ?? snapshot.session.worktrees[0] ?? null;
416
+ }
417
+ function managedAgentForTask(task) {
418
+ if (task?.owner === "codex" || task?.owner === "claude") {
419
+ return task.owner;
420
+ }
421
+ return null;
422
+ }
423
+ function reviewAgentForUi(snapshot, ui) {
424
+ if (!snapshot) {
425
+ return null;
426
+ }
427
+ if (ui.activeTab === "worktrees") {
428
+ return selectedWorktree(snapshot, ui)?.agent ?? null;
429
+ }
430
+ if (ui.activeTab === "tasks" && ui.taskDetailSection === "diff") {
431
+ return managedAgentForTask(selectedTask(snapshot, ui));
432
+ }
433
+ return null;
434
+ }
435
+ function changedPathsForAgent(snapshot, agent) {
436
+ return findWorktreeDiff(snapshot ?? null, agent)?.paths ?? [];
437
+ }
438
+ function changedPathSignature(paths) {
439
+ return paths.join("\n");
440
+ }
441
+ function firstMatchingPath(candidates, availablePaths) {
442
+ for (const candidate of candidates){
443
+ if (availablePaths.includes(candidate)) {
444
+ return candidate;
445
+ }
446
+ }
447
+ return null;
448
+ }
449
+ export function syncDiffSelections(current, snapshot, task = null) {
450
+ const next = {
451
+ ...current
452
+ };
453
+ for (const agent of [
454
+ "codex",
455
+ "claude"
456
+ ]){
457
+ const changedPaths = changedPathsForAgent(snapshot, agent);
458
+ const currentSelection = current[agent];
459
+ const taskPreferred = task?.owner === agent ? firstMatchingPath(task.claimedPaths, changedPaths) : null;
460
+ if (changedPaths.length === 0) {
461
+ next[agent] = null;
462
+ continue;
463
+ }
464
+ if (taskPreferred) {
465
+ next[agent] = taskPreferred;
466
+ continue;
467
+ }
468
+ next[agent] = currentSelection && changedPaths.includes(currentSelection) ? currentSelection : changedPaths[0] ?? null;
469
+ }
470
+ return next;
471
+ }
472
+ function diffEntryForAgent(ui, agent) {
473
+ return agent ? ui.diffReviews[agent] ?? null : null;
474
+ }
475
+ function selectedDiffPath(snapshot, ui, agent) {
476
+ const changedPaths = changedPathsForAgent(snapshot, agent);
477
+ if (changedPaths.length === 0) {
478
+ return null;
479
+ }
480
+ return ui.diffSelections[agent] && changedPaths.includes(ui.diffSelections[agent]) ? ui.diffSelections[agent] : changedPaths[0] ?? null;
481
+ }
482
+ function diffFooter(agent) {
483
+ if (!agent) {
484
+ return styleLine("Diff review is available for managed Codex and Claude worktrees.", "muted");
485
+ }
486
+ return styleLine(`Diff keys: , previous file | . next file | { previous hunk | } next hunk | current agent ${agent}`, "muted");
487
+ }
488
+ export function parseDiffHunks(patch) {
489
+ const lines = patch.replaceAll("\r", "").split("\n");
490
+ const hunks = [];
491
+ let current = null;
492
+ for (const line of lines){
493
+ if (line.startsWith("@@")) {
494
+ if (current) {
495
+ hunks.push(current);
496
+ }
497
+ current = {
498
+ header: line,
499
+ lines: []
500
+ };
501
+ continue;
502
+ }
503
+ if (current) {
504
+ current.lines.push(line);
505
+ }
506
+ }
507
+ if (current) {
508
+ hunks.push(current);
509
+ }
510
+ return hunks;
511
+ }
512
+ function selectedHunkIndex(ui, agent, review) {
513
+ const hunks = parseDiffHunks(review?.patch ?? "");
514
+ if (hunks.length === 0) {
515
+ return null;
516
+ }
517
+ const current = ui.hunkSelections[agent] ?? 0;
518
+ return Math.max(0, Math.min(current, hunks.length - 1));
519
+ }
520
+ function latestPendingApproval(approvals) {
521
+ return [
522
+ ...approvals
523
+ ].filter((request)=>request.status === "pending").sort((left, right)=>left.createdAt.localeCompare(right.createdAt)).pop() ?? null;
524
+ }
525
+ function tabLabel(tab) {
526
+ switch(tab){
527
+ case "tasks":
528
+ return "Tasks";
529
+ case "approvals":
530
+ return "Approvals";
531
+ case "claims":
532
+ return "Claims";
533
+ case "decisions":
534
+ return "Decisions";
535
+ case "events":
536
+ return "Events";
537
+ case "messages":
538
+ return "Messages";
539
+ case "worktrees":
540
+ return "Worktrees";
541
+ default:
542
+ return tab;
543
+ }
544
+ }
545
+ function toneForPanel(title, focused) {
546
+ if (focused) {
547
+ return styleLine(title, "accent", "strong");
548
+ }
549
+ return styleLine(title, "strong");
550
+ }
551
+ function renderPanel(title, width, height, content, options = {}) {
552
+ const safeWidth = Math.max(12, width);
553
+ const safeHeight = Math.max(3, height);
554
+ const titleLine = ` ${truncateValue(title, safeWidth - 6)} `;
555
+ const border = "-".repeat(Math.max(0, safeWidth - 2 - titleLine.length));
556
+ const top = `+${titleLine}${border}+`;
557
+ const bottom = `+${"-".repeat(safeWidth - 2)}+`;
558
+ const visibleRows = safeHeight - 2;
559
+ const paddedContent = [
560
+ ...content
561
+ ];
562
+ while(paddedContent.length < visibleRows){
563
+ paddedContent.push("");
564
+ }
565
+ const body = paddedContent.slice(0, visibleRows).map((line)=>`|${fitAnsiLine(line, safeWidth - 2)}|`);
566
+ const styledTop = options.focused ? styleLine(top, "accent") : options.muted ? styleLine(top, "muted") : top;
567
+ const styledBottom = options.focused ? styleLine(bottom, "accent") : options.muted ? styleLine(bottom, "muted") : bottom;
16
568
  return [
17
- "\u001bc",
18
- `Kavi Session ${session.id}`,
19
- `Repo: ${session.repoRoot}`,
20
- `Goal: ${session.goal ?? "-"}`,
21
- `Status: ${session.status}`,
22
- divider("Agents"),
23
- agentLines || "- none",
24
- divider("Worktrees"),
25
- worktreeLines || "- none",
26
- divider("Tasks"),
27
- taskLines || "- none",
28
- divider("Approvals"),
29
- approvalLines || "- none",
30
- divider("Peer Messages"),
31
- messageLines || "- none",
32
- divider("Events"),
33
- eventLines || "- none",
34
- "\nKeys: q quit | r refresh | y approve latest | n deny latest | s shutdown daemon"
35
- ].join("\n");
569
+ styledTop,
570
+ ...body,
571
+ styledBottom
572
+ ];
573
+ }
574
+ function combineColumns(columns) {
575
+ const height = Math.max(...columns.map((column)=>column.lines.length), 0);
576
+ const rows = [];
577
+ for(let index = 0; index < height; index += 1){
578
+ rows.push(columns.map((column)=>column.lines[index] ?? " ".repeat(column.width)).join(" "));
579
+ }
580
+ return rows;
581
+ }
582
+ function renderListPanel(snapshot, ui, width, height) {
583
+ const items = buildTabItems(snapshot, ui.activeTab);
584
+ const currentId = ui.selectedIds[ui.activeTab];
585
+ const selectedIndex = Math.max(0, items.findIndex((item)=>item.id === currentId));
586
+ const title = `Board | ${tabLabel(ui.activeTab)} (${items.length})`;
587
+ const visibleRows = Math.max(1, height - 2);
588
+ const start = Math.max(0, Math.min(selectedIndex - Math.floor(visibleRows / 2), Math.max(0, items.length - visibleRows)));
589
+ const content = items.length === 0 ? [
590
+ styleLine("No items in this view.", "muted")
591
+ ] : items.slice(start, start + visibleRows).map((item)=>{
592
+ const selected = item.id === currentId;
593
+ const prefix = selected ? ">" : " ";
594
+ const plain = `${prefix} ${item.title}${item.detail ? ` | ${item.detail}` : ""}`;
595
+ return toneLine(plain, item.tone, selected);
596
+ });
597
+ return renderPanel(title, width, height, content, {
598
+ focused: true
599
+ });
600
+ }
601
+ function formatJson(value, width) {
602
+ return wrapPreformatted(JSON.stringify(value, null, 2), width);
603
+ }
604
+ function artifactForTask(ui, task) {
605
+ if (!task) {
606
+ return null;
607
+ }
608
+ return ui.artifacts[task.id] ?? null;
609
+ }
610
+ function taskDetailTitle(ui) {
611
+ return `Inspector | Task ${ui.taskDetailSection}`;
612
+ }
613
+ function renderTaskInspector(task, artifactEntry, loading, diffEntry, loadingDiff, ui, width, height) {
614
+ if (!task) {
615
+ return renderPanel("Inspector", width, height, [
616
+ styleLine("No task selected.", "muted")
617
+ ]);
618
+ }
619
+ const innerWidth = Math.max(16, width - 2);
620
+ const artifact = artifactEntry?.artifact ?? null;
621
+ const lines = [];
622
+ if (ui.taskDetailSection === "overview") {
623
+ lines.push(...section("Task", [
624
+ `Id: ${task.id}`,
625
+ `Owner: ${task.owner}`,
626
+ `Status: ${task.status}`,
627
+ `Updated: ${shortTime(task.updatedAt)}`,
628
+ `Route: ${task.routeReason ?? "-"}`,
629
+ `Claimed paths: ${task.claimedPaths.join(", ") || "-"}`,
630
+ `Summary: ${task.summary ?? "-"}`
631
+ ].flatMap((line)=>wrapText(line, innerWidth))));
632
+ if (loading) {
633
+ lines.push(...section("Artifact", [
634
+ "Loading task artifact..."
635
+ ]));
636
+ } else if (artifactEntry?.error) {
637
+ lines.push(...section("Artifact", wrapText(`Artifact load failed: ${artifactEntry.error}`, innerWidth)));
638
+ } else if (artifact) {
639
+ lines.push(...section("Artifact", [
640
+ `Started: ${shortTime(artifact.startedAt)}`,
641
+ `Finished: ${shortTime(artifact.finishedAt)}`,
642
+ `Envelope status: ${artifact.envelope?.status ?? "-"}`,
643
+ `Next recommendation: ${artifact.envelope?.nextRecommendation ?? "-"}`,
644
+ `Error: ${artifact.error ?? "-"}`
645
+ ].flatMap((line)=>wrapText(line, innerWidth))));
646
+ lines.push(...section("Blockers", artifact.envelope?.blockers.length ? artifact.envelope.blockers.flatMap((blocker)=>wrapText(`- ${blocker}`, innerWidth)) : [
647
+ "- none"
648
+ ]));
649
+ lines.push(...section("Peer Messages", artifact.envelope?.peerMessages.length ? artifact.envelope.peerMessages.flatMap((message)=>wrapText(`- ${message.to} [${message.intent}] ${message.subject}`, innerWidth)) : [
650
+ "- none"
651
+ ]));
652
+ } else {
653
+ lines.push(...section("Artifact", [
654
+ "No artifact recorded for this task yet."
655
+ ]));
656
+ }
657
+ } else if (ui.taskDetailSection === "prompt") {
658
+ lines.push(...section("Prompt", wrapText(task.prompt, innerWidth)));
659
+ } else if (ui.taskDetailSection === "replay") {
660
+ if (loading) {
661
+ lines.push("Loading task artifact...");
662
+ } else if (artifactEntry?.error) {
663
+ lines.push(...wrapText(`Artifact load failed: ${artifactEntry.error}`, innerWidth));
664
+ } else if (artifact?.decisionReplay.length) {
665
+ lines.push(...artifact.decisionReplay.flatMap((line)=>wrapText(line, innerWidth)));
666
+ } else {
667
+ lines.push("No decision replay available.");
668
+ }
669
+ } else if (ui.taskDetailSection === "output") {
670
+ if (loading) {
671
+ lines.push("Loading task artifact...");
672
+ } else if (artifactEntry?.error) {
673
+ lines.push(...wrapText(`Artifact load failed: ${artifactEntry.error}`, innerWidth));
674
+ } else if (artifact?.rawOutput) {
675
+ lines.push(...wrapPreformatted(artifact.rawOutput, innerWidth));
676
+ } else {
677
+ lines.push("No raw output captured.");
678
+ }
679
+ } else {
680
+ const agent = managedAgentForTask(task);
681
+ lines.push(...section("Task Scope", [
682
+ `Owner: ${task.owner}`,
683
+ `Claimed paths: ${task.claimedPaths.join(", ") || "-"}`,
684
+ `Route reason: ${task.routeReason ?? "-"}`
685
+ ].flatMap((line)=>wrapText(line, innerWidth))));
686
+ if (!agent) {
687
+ lines.push(...section("Diff Review", [
688
+ "Diff review is only available for Codex and Claude managed tasks."
689
+ ]));
690
+ } else if (loadingDiff) {
691
+ lines.push(...section("Diff Review", [
692
+ "Loading worktree diff..."
693
+ ]));
694
+ } else if (diffEntry?.error) {
695
+ lines.push(...section("Diff Review", wrapText(`Diff load failed: ${diffEntry.error}`, innerWidth)));
696
+ } else if (diffEntry?.review) {
697
+ const review = diffEntry.review;
698
+ const hunks = parseDiffHunks(review.patch);
699
+ const hunkIndex = agent ? selectedHunkIndex(ui, agent, review) : null;
700
+ const selectedHunk = hunkIndex === null ? null : hunks[hunkIndex] ?? null;
701
+ lines.push(...section("Review", [
702
+ `Agent: ${review.agent}`,
703
+ `Selected file: ${review.selectedPath ?? "-"}`,
704
+ `Changed files: ${review.changedPaths.length}`,
705
+ `Hunks: ${hunks.length}`,
706
+ `Selected hunk: ${hunkIndex === null ? "-" : `${hunkIndex + 1}/${hunks.length}`}`,
707
+ `Stat: ${review.stat}`
708
+ ].flatMap((line)=>wrapText(line, innerWidth))));
709
+ lines.push(...section("Changed Files", review.changedPaths.length ? review.changedPaths.flatMap((filePath)=>wrapText(`${filePath === review.selectedPath ? ">" : "-"} ${filePath}`, innerWidth)) : [
710
+ "- clean"
711
+ ]));
712
+ if (selectedHunk) {
713
+ lines.push(...section("Current Hunk", wrapPreformatted([
714
+ selectedHunk.header,
715
+ ...selectedHunk.lines
716
+ ].join("\n"), innerWidth)));
717
+ }
718
+ lines.push(...section("Patch", review.patch ? wrapPreformatted(review.patch, innerWidth) : [
719
+ "No textual patch available."
720
+ ]));
721
+ } else {
722
+ lines.push(...section("Diff Review", [
723
+ "No diff review available yet."
724
+ ]));
725
+ }
726
+ }
727
+ const footerLines = [
728
+ styleLine("Detail keys: [ ] cycle task sections", "muted")
729
+ ];
730
+ if (ui.taskDetailSection === "diff") {
731
+ footerLines.push(diffFooter(managedAgentForTask(task)));
732
+ }
733
+ return renderPanel(taskDetailTitle(ui), width, height, [
734
+ ...lines,
735
+ "",
736
+ ...footerLines
737
+ ], {
738
+ focused: true
739
+ });
740
+ }
741
+ function renderApprovalInspector(approval, width, height) {
742
+ if (!approval) {
743
+ return renderPanel("Inspector | Approval", width, height, [
744
+ styleLine("No approval selected.", "muted")
745
+ ]);
746
+ }
747
+ const innerWidth = Math.max(16, width - 2);
748
+ const lines = [
749
+ ...section("Approval", [
750
+ `Id: ${approval.id}`,
751
+ `Agent: ${approval.agent}`,
752
+ `Status: ${approval.status}`,
753
+ `Tool: ${approval.toolName}`,
754
+ `Remembered: ${approval.remember ? "yes" : "no"}`,
755
+ `Created: ${shortTime(approval.createdAt)}`,
756
+ `Updated: ${shortTime(approval.updatedAt)}`,
757
+ `Match key: ${approval.matchKey}`,
758
+ `Summary: ${approval.summary}`
759
+ ].flatMap((line)=>wrapText(line, innerWidth))),
760
+ ...section("Payload", formatJson(approval.payload, innerWidth)),
761
+ "",
762
+ styleLine("Actions: y allow | Y allow+remember | n deny | N deny+remember", "muted")
763
+ ];
764
+ return renderPanel("Inspector | Approval", width, height, lines, {
765
+ focused: true
766
+ });
767
+ }
768
+ function renderClaimInspector(claim, width, height) {
769
+ if (!claim) {
770
+ return renderPanel("Inspector | Claim", width, height, [
771
+ styleLine("No claim selected.", "muted")
772
+ ]);
773
+ }
774
+ const innerWidth = Math.max(16, width - 2);
775
+ const lines = [
776
+ ...section("Claim", [
777
+ `Id: ${claim.id}`,
778
+ `Task: ${claim.taskId}`,
779
+ `Agent: ${claim.agent}`,
780
+ `Source: ${claim.source}`,
781
+ `Status: ${claim.status}`,
782
+ `Created: ${shortTime(claim.createdAt)}`,
783
+ `Updated: ${shortTime(claim.updatedAt)}`,
784
+ `Note: ${claim.note ?? "-"}`
785
+ ].flatMap((line)=>wrapText(line, innerWidth))),
786
+ ...section("Paths", claim.paths.length > 0 ? claim.paths.flatMap((filePath)=>wrapText(`- ${filePath}`, innerWidth)) : [
787
+ "- none"
788
+ ])
789
+ ];
790
+ return renderPanel("Inspector | Claim", width, height, lines, {
791
+ focused: true
792
+ });
793
+ }
794
+ function renderDecisionInspector(decision, width, height) {
795
+ if (!decision) {
796
+ return renderPanel("Inspector | Decision", width, height, [
797
+ styleLine("No decision selected.", "muted")
798
+ ]);
799
+ }
800
+ const innerWidth = Math.max(16, width - 2);
801
+ const lines = [
802
+ ...section("Decision", [
803
+ `Id: ${decision.id}`,
804
+ `Kind: ${decision.kind}`,
805
+ `Agent: ${decision.agent ?? "-"}`,
806
+ `Task: ${decision.taskId ?? "-"}`,
807
+ `Created: ${shortTime(decision.createdAt)}`,
808
+ `Summary: ${decision.summary}`,
809
+ `Detail: ${decision.detail}`
810
+ ].flatMap((line)=>wrapText(line, innerWidth))),
811
+ ...section("Metadata", formatJson(decision.metadata, innerWidth))
812
+ ];
813
+ return renderPanel("Inspector | Decision", width, height, lines, {
814
+ focused: true
815
+ });
816
+ }
817
+ function renderEventInspector(event, width, height) {
818
+ if (!event) {
819
+ return renderPanel("Inspector | Event", width, height, [
820
+ styleLine("No event selected.", "muted")
821
+ ]);
822
+ }
823
+ const innerWidth = Math.max(16, width - 2);
824
+ const lines = [
825
+ ...section("Event", [
826
+ `Id: ${event.id}`,
827
+ `Type: ${event.type}`,
828
+ `Timestamp: ${shortTime(event.timestamp)}`
829
+ ].flatMap((line)=>wrapText(line, innerWidth))),
830
+ ...section("Payload", formatJson(event.payload, innerWidth))
831
+ ];
832
+ return renderPanel("Inspector | Event", width, height, lines, {
833
+ focused: true
834
+ });
835
+ }
836
+ function renderMessageInspector(message, width, height) {
837
+ if (!message) {
838
+ return renderPanel("Inspector | Message", width, height, [
839
+ styleLine("No peer message selected.", "muted")
840
+ ]);
841
+ }
842
+ const innerWidth = Math.max(16, width - 2);
843
+ const lines = [
844
+ ...section("Message", [
845
+ `Id: ${message.id}`,
846
+ `Task: ${message.taskId}`,
847
+ `From: ${message.from}`,
848
+ `To: ${message.to}`,
849
+ `Intent: ${message.intent}`,
850
+ `Created: ${shortTime(message.createdAt)}`,
851
+ `Subject: ${message.subject}`
852
+ ].flatMap((line)=>wrapText(line, innerWidth))),
853
+ ...section("Body", wrapText(message.body, innerWidth))
854
+ ];
855
+ return renderPanel("Inspector | Message", width, height, lines, {
856
+ focused: true
857
+ });
858
+ }
859
+ function renderWorktreeInspector(snapshot, worktree, diffEntry, loadingDiff, ui, width, height) {
860
+ if (!snapshot || !worktree) {
861
+ return renderPanel("Inspector | Worktree", width, height, [
862
+ styleLine("No worktree selected.", "muted")
863
+ ]);
864
+ }
865
+ const diff = findWorktreeDiff(snapshot, worktree.agent);
866
+ const status = snapshot.session.agentStatus[worktree.agent];
867
+ const ownedTasks = snapshot.session.tasks.filter((task)=>task.owner === worktree.agent).sort((left, right)=>right.updatedAt.localeCompare(left.updatedAt)).slice(0, 4);
868
+ const innerWidth = Math.max(16, width - 2);
869
+ const lines = [
870
+ ...section("Worktree", [
871
+ `Agent: ${worktree.agent}`,
872
+ `Branch: ${worktree.branch}`,
873
+ `Path: ${worktree.path}`,
874
+ `Transport: ${status.transport}`,
875
+ `Last run: ${shortTime(status.lastRunAt)}`,
876
+ `Last exit: ${status.lastExitCode ?? "-"}`
877
+ ].flatMap((line)=>wrapText(line, innerWidth))),
878
+ ...section("Changed Paths", diff?.paths.length ? diff.paths.flatMap((filePath)=>wrapText(`${filePath === diffEntry?.review?.selectedPath ? ">" : "-"} ${filePath}`, innerWidth)) : [
879
+ "- clean"
880
+ ]),
881
+ ...section("Recent Tasks", ownedTasks.length ? ownedTasks.flatMap((task)=>wrapText(`- [${task.status}] ${task.title}`, innerWidth)) : [
882
+ "- none"
883
+ ])
884
+ ];
885
+ if (loadingDiff) {
886
+ lines.push(...section("Diff Review", [
887
+ "Loading worktree diff..."
888
+ ]));
889
+ } else if (diffEntry?.error) {
890
+ lines.push(...section("Diff Review", wrapText(`Diff load failed: ${diffEntry.error}`, innerWidth)));
891
+ } else if (diffEntry?.review) {
892
+ const hunks = parseDiffHunks(diffEntry.review.patch);
893
+ const hunkIndex = selectedHunkIndex(ui, worktree.agent, diffEntry.review);
894
+ const selectedHunk = hunkIndex === null ? null : hunks[hunkIndex] ?? null;
895
+ lines.push(...section("Review", [
896
+ `Selected file: ${diffEntry.review.selectedPath ?? "-"}`,
897
+ `Hunks: ${hunks.length}`,
898
+ `Selected hunk: ${hunkIndex === null ? "-" : `${hunkIndex + 1}/${hunks.length}`}`,
899
+ `Stat: ${diffEntry.review.stat}`
900
+ ].flatMap((line)=>wrapText(line, innerWidth))));
901
+ if (selectedHunk) {
902
+ lines.push(...section("Current Hunk", wrapPreformatted([
903
+ selectedHunk.header,
904
+ ...selectedHunk.lines
905
+ ].join("\n"), innerWidth)));
906
+ }
907
+ lines.push(...section("Patch", diffEntry.review.patch ? wrapPreformatted(diffEntry.review.patch, innerWidth) : [
908
+ "No textual patch available."
909
+ ]));
910
+ } else {
911
+ lines.push(...section("Diff Review", [
912
+ "No diff review available yet."
913
+ ]));
914
+ }
915
+ lines.push("");
916
+ lines.push(diffFooter(worktree.agent));
917
+ return renderPanel("Inspector | Worktree", width, height, lines, {
918
+ focused: true
919
+ });
920
+ }
921
+ function renderInspector(snapshot, ui, width, height) {
922
+ switch(ui.activeTab){
923
+ case "tasks":
924
+ return renderTaskInspector(selectedTask(snapshot, ui), artifactForTask(ui, selectedTask(snapshot, ui)), selectedTask(snapshot, ui) ? ui.loadingArtifacts[selectedTask(snapshot, ui)?.id ?? ""] === true : false, diffEntryForAgent(ui, managedAgentForTask(selectedTask(snapshot, ui))), managedAgentForTask(selectedTask(snapshot, ui)) ? ui.loadingDiffReviews[managedAgentForTask(selectedTask(snapshot, ui)) ?? "codex"] === true : false, ui, width, height);
925
+ case "approvals":
926
+ return renderApprovalInspector(selectedApproval(snapshot, ui), width, height);
927
+ case "claims":
928
+ return renderClaimInspector(selectedClaim(snapshot, ui), width, height);
929
+ case "decisions":
930
+ return renderDecisionInspector(selectedDecision(snapshot, ui), width, height);
931
+ case "events":
932
+ return renderEventInspector(selectedEvent(snapshot, ui), width, height);
933
+ case "messages":
934
+ return renderMessageInspector(selectedMessage(snapshot, ui), width, height);
935
+ case "worktrees":
936
+ return renderWorktreeInspector(snapshot, selectedWorktree(snapshot, ui), diffEntryForAgent(ui, selectedWorktree(snapshot, ui)?.agent ?? null), selectedWorktree(snapshot, ui) ? ui.loadingDiffReviews[selectedWorktree(snapshot, ui)?.agent ?? "codex"] === true : false, ui, width, height);
937
+ default:
938
+ return renderPanel("Inspector", width, height, [
939
+ styleLine("No inspector data.", "muted")
940
+ ]);
941
+ }
942
+ }
943
+ function renderLane(snapshot, agent, width, height) {
944
+ if (!snapshot) {
945
+ return renderPanel(`${agent === "codex" ? "Codex" : "Claude"} Lane`, width, height, [
946
+ styleLine("No session snapshot available.", "muted")
947
+ ]);
948
+ }
949
+ const status = snapshot.session.agentStatus[agent];
950
+ const worktree = snapshot.session.worktrees.find((item)=>item.agent === agent);
951
+ const diff = findWorktreeDiff(snapshot, agent);
952
+ const tasks = snapshot.session.tasks.filter((task)=>task.owner === agent).sort((left, right)=>right.updatedAt.localeCompare(left.updatedAt));
953
+ const approvals = snapshot.approvals.filter((approval)=>approval.agent === agent && approval.status === "pending");
954
+ const claims = snapshot.session.pathClaims.filter((claim)=>claim.agent === agent && claim.status === "active");
955
+ const innerWidth = Math.max(16, width - 2);
956
+ const lines = [
957
+ ...section("Status", [
958
+ `Transport: ${status.transport}`,
959
+ `Available: ${status.available ? "yes" : "no"}`,
960
+ `Last run: ${shortTime(status.lastRunAt)}`,
961
+ `Last exit: ${status.lastExitCode ?? "-"}`
962
+ ].flatMap((line)=>wrapText(line, innerWidth))),
963
+ ...section("Session", [
964
+ `Runtime session: ${status.sessionId ?? "-"}`,
965
+ `Worktree: ${worktree ? path.basename(worktree.path) : "-"}`,
966
+ `Branch: ${worktree?.branch ?? "-"}`,
967
+ `Pending approvals: ${approvals.length}`,
968
+ `Changed paths: ${diff?.paths.length ?? 0}`
969
+ ].flatMap((line)=>wrapText(line, innerWidth))),
970
+ ...section("Summary", wrapText(status.summary ?? "No summary yet.", innerWidth)),
971
+ ...section("Tasks", tasks.length ? tasks.slice(0, 4).flatMap((task)=>wrapText(`- [${task.status}] ${task.title}`, innerWidth)) : [
972
+ "- none"
973
+ ]),
974
+ ...section("Claims", claims.length ? claims.slice(0, 3).flatMap((claim)=>wrapText(`- ${claim.paths.join(", ")}`, innerWidth)) : [
975
+ "- none"
976
+ ]),
977
+ ...section("Diff", diff?.paths.length ? diff.paths.slice(0, 4).flatMap((filePath)=>wrapText(`- ${filePath}`, innerWidth)) : [
978
+ "- clean"
979
+ ])
980
+ ];
981
+ return renderPanel(`${agent === "codex" ? "Codex" : "Claude"} Lane`, width, height, lines);
982
+ }
983
+ function renderHeader(view, ui, width) {
984
+ const snapshot = view.snapshot;
985
+ const session = snapshot?.session ?? null;
986
+ const repoName = path.basename(session?.repoRoot ?? process.cwd());
987
+ const line1 = fitAnsiLine(`${toneForPanel("Kavi Operator", true)} | session=${session?.id ?? "-"} | repo=${repoName} | rpc=${view.connected ? "connected" : "disconnected"}`, width);
988
+ const line2 = fitAnsiLine(`Goal: ${session?.goal ?? "-"} | status=${session?.status ?? "-"} | refresh=${shortTime(view.refreshedAt)}`, width);
989
+ const line3 = fitAnsiLine(session ? `Tasks P:${countTasks(session.tasks, "pending")} R:${countTasks(session.tasks, "running")} B:${countTasks(session.tasks, "blocked")} C:${countTasks(session.tasks, "completed")} F:${countTasks(session.tasks, "failed")} | approvals=${snapshot?.approvals.filter((approval)=>approval.status === "pending").length ?? 0} | claims=${session.pathClaims.filter((claim)=>claim.status === "active").length} | decisions=${session.decisions.length}` : "Waiting for session snapshot...", width);
990
+ const tabs = OPERATOR_TABS.map((tab, index)=>{
991
+ const count = buildTabItems(snapshot, tab).length;
992
+ const label = `[${index + 1}] ${tabLabel(tab)} ${count}`;
993
+ return tab === ui.activeTab ? styleLine(label, "reverse") : label;
994
+ }).join(" ");
995
+ const line4 = fitAnsiLine(tabs, width);
996
+ return [
997
+ line1,
998
+ line2,
999
+ line3,
1000
+ line4
1001
+ ];
1002
+ }
1003
+ function currentToast(ui) {
1004
+ if (!ui.toast) {
1005
+ return null;
1006
+ }
1007
+ return ui.toast.expiresAt > Date.now() ? ui.toast : null;
1008
+ }
1009
+ function footerSelectionSummary(snapshot, ui, width) {
1010
+ const item = selectedItem(snapshot, ui);
1011
+ if (!item) {
1012
+ return fitAnsiLine("Selection: none", width);
1013
+ }
1014
+ return fitAnsiLine(`Selection: ${item.title}${item.detail ? ` | ${item.detail}` : ""}`, width);
1015
+ }
1016
+ function renderFooter(snapshot, ui, width) {
1017
+ const toast = currentToast(ui);
1018
+ if (ui.composer) {
1019
+ const composerHeader = fitAnsiLine(styleLine("Compose Task", "accent", "strong"), width);
1020
+ const composerLine = fitAnsiLine(`Route: ${ui.composer.owner} | 1 auto 2 codex 3 claude | Enter submit | Esc cancel | Ctrl+U clear`, width);
1021
+ const promptLine = fitAnsiLine(`> ${ui.composer.prompt}`, width);
1022
+ return [
1023
+ composerHeader,
1024
+ composerLine,
1025
+ promptLine,
1026
+ fitAnsiLine(toast ? styleLine(toast.message, toast.level === "error" ? "bad" : "good") : styleLine("Composer accepts free text; tasks are routed or assigned when you press Enter.", "muted"), width)
1027
+ ];
1028
+ }
1029
+ return [
1030
+ fitAnsiLine("Keys: 1-7 tabs | h/l or Tab cycle tabs | j/k move | [ ] task detail | ,/. diff file | { } diff hunk | c compose | r refresh", width),
1031
+ fitAnsiLine("Actions: y/Y allow approval | n/N deny approval | g/G top/bottom | s stop daemon | q quit", width),
1032
+ footerSelectionSummary(snapshot, ui, width),
1033
+ fitAnsiLine(toast ? styleLine(toast.message, toast.level === "error" ? "bad" : "good") : styleLine("Operator surface is live over the daemon socket with pushed snapshots.", "muted"), width)
1034
+ ];
1035
+ }
1036
+ function buildLayout(width, height) {
1037
+ if (width >= 120) {
1038
+ const left = Math.max(32, Math.min(44, Math.floor(width * 0.34)));
1039
+ const middle = Math.max(28, Math.min(38, Math.floor(width * 0.26)));
1040
+ const right = Math.max(36, width - left - middle - 2);
1041
+ return [
1042
+ {
1043
+ kind: "wide",
1044
+ columns: [
1045
+ left,
1046
+ middle,
1047
+ right
1048
+ ]
1049
+ }
1050
+ ];
1051
+ }
1052
+ if (width >= 88) {
1053
+ const left = Math.max(28, Math.floor(width * 0.4));
1054
+ const right = Math.max(36, width - left - 1);
1055
+ return [
1056
+ {
1057
+ kind: "narrow",
1058
+ columns: [
1059
+ left,
1060
+ right
1061
+ ]
1062
+ }
1063
+ ];
1064
+ }
1065
+ return [
1066
+ {
1067
+ kind: "compact"
1068
+ }
1069
+ ];
1070
+ }
1071
+ function renderBody(view, ui, width, height) {
1072
+ const layout = buildLayout(width, height)[0];
1073
+ if (!layout) {
1074
+ return [];
1075
+ }
1076
+ if (layout.kind === "wide" && layout.columns) {
1077
+ const [leftWidth, middleWidth, rightWidth] = layout.columns;
1078
+ const topHeight = Math.max(7, Math.floor(height / 2));
1079
+ const bottomHeight = Math.max(7, height - topHeight);
1080
+ return combineColumns([
1081
+ {
1082
+ width: leftWidth,
1083
+ lines: renderListPanel(view.snapshot, ui, leftWidth, height)
1084
+ },
1085
+ {
1086
+ width: middleWidth,
1087
+ lines: [
1088
+ ...renderLane(view.snapshot, "codex", middleWidth, topHeight),
1089
+ ...renderLane(view.snapshot, "claude", middleWidth, bottomHeight)
1090
+ ]
1091
+ },
1092
+ {
1093
+ width: rightWidth,
1094
+ lines: renderInspector(view.snapshot, ui, rightWidth, height)
1095
+ }
1096
+ ]);
1097
+ }
1098
+ if (layout.kind === "narrow" && layout.columns) {
1099
+ const [leftWidth, rightWidth] = layout.columns;
1100
+ const inspectorHeight = Math.max(9, Math.floor(height * 0.58));
1101
+ const laneHeight = Math.max(6, Math.floor((height - inspectorHeight) / 2));
1102
+ const remaining = Math.max(6, height - inspectorHeight - laneHeight);
1103
+ return combineColumns([
1104
+ {
1105
+ width: leftWidth,
1106
+ lines: renderListPanel(view.snapshot, ui, leftWidth, height)
1107
+ },
1108
+ {
1109
+ width: rightWidth,
1110
+ lines: [
1111
+ ...renderInspector(view.snapshot, ui, rightWidth, inspectorHeight),
1112
+ ...renderLane(view.snapshot, "codex", rightWidth, laneHeight),
1113
+ ...renderLane(view.snapshot, "claude", rightWidth, remaining)
1114
+ ]
1115
+ }
1116
+ ]);
1117
+ }
1118
+ const listHeight = Math.max(8, Math.floor(height * 0.34));
1119
+ const inspectorHeight = Math.max(8, Math.floor(height * 0.34));
1120
+ const laneHeight = Math.max(6, Math.floor((height - listHeight - inspectorHeight) / 2));
1121
+ const finalLaneHeight = Math.max(6, height - listHeight - inspectorHeight - laneHeight);
1122
+ return [
1123
+ ...renderListPanel(view.snapshot, ui, width, listHeight),
1124
+ ...renderInspector(view.snapshot, ui, width, inspectorHeight),
1125
+ ...renderLane(view.snapshot, "codex", width, laneHeight),
1126
+ ...renderLane(view.snapshot, "claude", width, finalLaneHeight)
1127
+ ];
1128
+ }
1129
+ function renderScreen(view, ui, paths) {
1130
+ const width = process.stdout.columns ?? 120;
1131
+ const height = process.stdout.rows ?? 36;
1132
+ const header = renderHeader(view, ui, width);
1133
+ const footer = renderFooter(view.snapshot, ui, width);
1134
+ const bodyHeight = Math.max(12, height - header.length - footer.length);
1135
+ const body = renderBody(view, ui, width, bodyHeight);
1136
+ return `\u001b[?25l\u001b[H\u001b[2J${[
1137
+ ...header,
1138
+ ...body,
1139
+ ...footer
1140
+ ].slice(0, height).join("\n")}`;
1141
+ }
1142
+ function setToast(ui, level, message) {
1143
+ ui.toast = {
1144
+ level,
1145
+ message,
1146
+ expiresAt: Date.now() + TOAST_DURATION_MS
1147
+ };
1148
+ }
1149
+ async function queueManualTask(paths, view, ui) {
1150
+ const snapshot = view.snapshot;
1151
+ const composer = ui.composer;
1152
+ if (!snapshot || !composer) {
1153
+ throw new Error("No live session snapshot is available for task composition.");
1154
+ }
1155
+ const prompt = composer.prompt.trim();
1156
+ if (!prompt) {
1157
+ throw new Error("Task prompt cannot be empty.");
1158
+ }
1159
+ const routeDecision = composer.owner === "auto" ? await routeTask(prompt, snapshot.session, paths) : {
1160
+ owner: composer.owner,
1161
+ strategy: "manual",
1162
+ confidence: 1,
1163
+ reason: `Operator manually assigned the task to ${composer.owner}.`,
1164
+ claimedPaths: extractPromptPathHints(prompt)
1165
+ };
1166
+ await rpcEnqueueTask(paths, {
1167
+ owner: routeDecision.owner,
1168
+ prompt,
1169
+ routeReason: routeDecision.reason,
1170
+ claimedPaths: routeDecision.claimedPaths,
1171
+ routeStrategy: routeDecision.strategy,
1172
+ routeConfidence: routeDecision.confidence
1173
+ });
1174
+ ui.composer = null;
1175
+ ui.activeTab = "tasks";
1176
+ setToast(ui, "info", `Queued ${routeDecision.owner} task via ${routeDecision.strategy}: ${routeDecision.reason}`);
1177
+ }
1178
+ async function resolveApprovalSelection(paths, snapshot, ui, decision, remember) {
1179
+ const approval = ui.activeTab === "approvals" ? selectedApproval(snapshot, ui) : snapshot ? latestPendingApproval(snapshot.approvals) : null;
1180
+ if (!approval) {
1181
+ throw new Error("No approval request is available to resolve.");
1182
+ }
1183
+ await rpcResolveApproval(paths, {
1184
+ requestId: approval.id,
1185
+ decision,
1186
+ remember
1187
+ });
1188
+ setToast(ui, "info", `${decision === "allow" ? "Approved" : "Denied"} ${approval.toolName}${remember ? " with remembered rule" : ""}.`);
1189
+ }
1190
+ async function ensureSelectedTaskArtifact(paths, view, ui, render) {
1191
+ if (!view.connected || ui.activeTab !== "tasks") {
1192
+ return;
1193
+ }
1194
+ const task = selectedTask(view.snapshot, ui);
1195
+ if (!task) {
1196
+ return;
1197
+ }
1198
+ const existing = ui.artifacts[task.id];
1199
+ if (existing && existing.taskUpdatedAt === task.updatedAt) {
1200
+ return;
1201
+ }
1202
+ if (ui.loadingArtifacts[task.id]) {
1203
+ return;
1204
+ }
1205
+ ui.loadingArtifacts[task.id] = true;
1206
+ render();
1207
+ try {
1208
+ const artifact = await rpcTaskArtifact(paths, task.id);
1209
+ ui.artifacts[task.id] = {
1210
+ taskUpdatedAt: task.updatedAt,
1211
+ artifact,
1212
+ error: null
1213
+ };
1214
+ } catch (error) {
1215
+ ui.artifacts[task.id] = {
1216
+ taskUpdatedAt: task.updatedAt,
1217
+ artifact: null,
1218
+ error: error instanceof Error ? error.message : String(error)
1219
+ };
1220
+ } finally{
1221
+ delete ui.loadingArtifacts[task.id];
1222
+ render();
1223
+ }
1224
+ }
1225
+ async function ensureSelectedDiffReview(paths, view, ui, render) {
1226
+ if (!view.connected) {
1227
+ return;
1228
+ }
1229
+ const agent = reviewAgentForUi(view.snapshot, ui);
1230
+ if (!agent) {
1231
+ return;
1232
+ }
1233
+ const changedPaths = changedPathsForAgent(view.snapshot, agent);
1234
+ const selectedPath = selectedDiffPath(view.snapshot, ui, agent);
1235
+ const changedSignature = changedPathSignature(changedPaths);
1236
+ const existing = ui.diffReviews[agent];
1237
+ if (existing && existing.selectedPath === selectedPath && existing.changedSignature === changedSignature) {
1238
+ return;
1239
+ }
1240
+ if (ui.loadingDiffReviews[agent]) {
1241
+ return;
1242
+ }
1243
+ ui.loadingDiffReviews[agent] = true;
1244
+ render();
1245
+ try {
1246
+ const review = await rpcWorktreeDiff(paths, agent, selectedPath);
1247
+ ui.diffSelections[agent] = review.selectedPath;
1248
+ ui.hunkSelections[agent] = 0;
1249
+ ui.diffReviews[agent] = {
1250
+ selectedPath: review.selectedPath,
1251
+ changedSignature: changedPathSignature(review.changedPaths),
1252
+ review,
1253
+ error: null
1254
+ };
1255
+ } catch (error) {
1256
+ ui.diffReviews[agent] = {
1257
+ selectedPath,
1258
+ changedSignature,
1259
+ review: null,
1260
+ error: error instanceof Error ? error.message : String(error)
1261
+ };
1262
+ } finally{
1263
+ ui.loadingDiffReviews[agent] = false;
1264
+ render();
1265
+ }
1266
+ }
1267
+ function cycleDiffSelection(snapshot, ui, delta) {
1268
+ const agent = reviewAgentForUi(snapshot, ui);
1269
+ if (!agent) {
1270
+ return null;
1271
+ }
1272
+ const changedPaths = changedPathsForAgent(snapshot, agent);
1273
+ if (changedPaths.length === 0) {
1274
+ ui.diffSelections[agent] = null;
1275
+ return agent;
1276
+ }
1277
+ const current = selectedDiffPath(snapshot, ui, agent);
1278
+ const currentIndex = Math.max(0, changedPaths.findIndex((filePath)=>filePath === current));
1279
+ const nextIndex = (currentIndex + delta + changedPaths.length) % changedPaths.length;
1280
+ ui.diffSelections[agent] = changedPaths[nextIndex] ?? changedPaths[0] ?? null;
1281
+ ui.hunkSelections[agent] = 0;
1282
+ return agent;
1283
+ }
1284
+ function cycleDiffHunk(snapshot, ui, delta) {
1285
+ const agent = reviewAgentForUi(snapshot, ui);
1286
+ if (!agent) {
1287
+ return null;
1288
+ }
1289
+ const review = ui.diffReviews[agent]?.review ?? null;
1290
+ const hunks = parseDiffHunks(review?.patch ?? "");
1291
+ if (hunks.length === 0) {
1292
+ ui.hunkSelections[agent] = 0;
1293
+ return agent;
1294
+ }
1295
+ const currentIndex = selectedHunkIndex(ui, agent, review) ?? 0;
1296
+ ui.hunkSelections[agent] = (currentIndex + delta + hunks.length) % hunks.length;
1297
+ return agent;
36
1298
  }
37
1299
  export async function attachTui(paths) {
38
- readline.emitKeypressEvents(process.stdin);
39
- if (process.stdin.isTTY) {
40
- process.stdin.setRawMode(true);
1300
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1301
+ throw new Error("The operator UI requires an interactive terminal.");
41
1302
  }
1303
+ readline.emitKeypressEvents(process.stdin);
1304
+ process.stdin.setRawMode(true);
42
1305
  let closed = false;
43
- const refresh = async ()=>{
44
- const session = await loadSessionRecord(paths);
45
- const events = await readRecentEvents(paths, 30);
46
- const approvals = await listApprovalRequests(paths);
47
- process.stdout.write(render(session, events, approvals));
48
- };
49
- const interval = setInterval(()=>{
50
- void refresh();
51
- }, 1500);
52
- await refresh();
53
- await new Promise((resolve)=>{
54
- process.stdin.on("keypress", async (_str, key)=>{
55
- if (key.name === "q" || key.ctrl && key.name === "c") {
56
- closed = true;
57
- clearInterval(interval);
58
- if (process.stdin.isTTY) {
59
- process.stdin.setRawMode(false);
60
- }
61
- process.stdout.write("\n");
62
- resolve();
63
- return;
1306
+ let refreshing = false;
1307
+ let subscribing = false;
1308
+ let actionQueue = Promise.resolve();
1309
+ let closeResolver = null;
1310
+ let reconnectTimer = null;
1311
+ let snapshotSubscription = null;
1312
+ const view = {
1313
+ snapshot: null,
1314
+ connected: false,
1315
+ error: null,
1316
+ refreshedAt: null
1317
+ };
1318
+ const ui = {
1319
+ activeTab: "tasks",
1320
+ selectedIds: emptySelectionMap(),
1321
+ taskDetailSection: "overview",
1322
+ composer: null,
1323
+ toast: null,
1324
+ artifacts: {},
1325
+ loadingArtifacts: {},
1326
+ diffSelections: {
1327
+ codex: null,
1328
+ claude: null
1329
+ },
1330
+ diffReviews: {
1331
+ codex: null,
1332
+ claude: null
1333
+ },
1334
+ loadingDiffReviews: {
1335
+ codex: false,
1336
+ claude: false
1337
+ },
1338
+ hunkSelections: {
1339
+ codex: 0,
1340
+ claude: 0
1341
+ }
1342
+ };
1343
+ const render = ()=>{
1344
+ process.stdout.write(renderScreen(view, ui, paths));
1345
+ };
1346
+ const syncUiForSnapshot = (snapshot)=>{
1347
+ ui.selectedIds = syncSelections(ui.selectedIds, snapshot);
1348
+ ui.diffSelections = syncDiffSelections(ui.diffSelections, snapshot, ui.activeTab === "tasks" ? selectedTask(snapshot, ui) : null);
1349
+ };
1350
+ const applySnapshot = (snapshot, reason)=>{
1351
+ view.snapshot = snapshot;
1352
+ view.connected = true;
1353
+ view.error = null;
1354
+ view.refreshedAt = new Date().toISOString();
1355
+ syncUiForSnapshot(snapshot);
1356
+ render();
1357
+ void ensureSelectedTaskArtifact(paths, view, ui, render);
1358
+ void ensureSelectedDiffReview(paths, view, ui, render);
1359
+ if (reason !== "subscribe") {
1360
+ ui.toast = currentToast(ui);
1361
+ }
1362
+ };
1363
+ const scheduleReconnect = ()=>{
1364
+ if (closed || reconnectTimer) {
1365
+ return;
1366
+ }
1367
+ reconnectTimer = setTimeout(()=>{
1368
+ reconnectTimer = null;
1369
+ runAction(connectSubscription);
1370
+ }, SUBSCRIPTION_RETRY_MS);
1371
+ };
1372
+ const markDisconnected = (message)=>{
1373
+ view.connected = false;
1374
+ view.error = message;
1375
+ view.refreshedAt = new Date().toISOString();
1376
+ render();
1377
+ scheduleReconnect();
1378
+ };
1379
+ const connectSubscription = async ()=>{
1380
+ if (closed || subscribing || snapshotSubscription) {
1381
+ return;
1382
+ }
1383
+ subscribing = true;
1384
+ const candidate = subscribeSnapshotRpc(paths, {
1385
+ onSnapshot: (event)=>{
1386
+ if (snapshotSubscription !== candidate || closed) {
1387
+ return;
1388
+ }
1389
+ applySnapshot(event.snapshot, event.reason);
1390
+ },
1391
+ onError: (error)=>{
1392
+ if (snapshotSubscription !== candidate || closed) {
1393
+ return;
1394
+ }
1395
+ snapshotSubscription = null;
1396
+ markDisconnected(error.message);
1397
+ },
1398
+ onDisconnect: ()=>{
1399
+ if (snapshotSubscription !== candidate || closed) {
1400
+ return;
1401
+ }
1402
+ snapshotSubscription = null;
1403
+ markDisconnected("RPC subscription disconnected.");
64
1404
  }
65
- if (key.name === "r") {
66
- await refresh();
67
- return;
1405
+ });
1406
+ snapshotSubscription = candidate;
1407
+ try {
1408
+ await candidate.connected;
1409
+ } catch (error) {
1410
+ if (snapshotSubscription === candidate && !closed) {
1411
+ snapshotSubscription = null;
1412
+ markDisconnected(error instanceof Error ? error.message : String(error));
68
1413
  }
69
- if (key.name === "s") {
70
- await appendCommand(paths, "shutdown", {});
71
- await refresh();
72
- return;
1414
+ } finally{
1415
+ subscribing = false;
1416
+ render();
1417
+ }
1418
+ };
1419
+ const refresh = async ()=>{
1420
+ if (refreshing || closed) {
1421
+ return;
1422
+ }
1423
+ refreshing = true;
1424
+ try {
1425
+ const snapshot = await readSnapshot(paths);
1426
+ applySnapshot(snapshot, "manual.refresh");
1427
+ } catch (error) {
1428
+ view.connected = await pingRpc(paths);
1429
+ view.error = error instanceof Error ? error.message : String(error);
1430
+ view.refreshedAt = new Date().toISOString();
1431
+ if (!view.connected && snapshotSubscription) {
1432
+ snapshotSubscription.close();
1433
+ snapshotSubscription = null;
1434
+ scheduleReconnect();
73
1435
  }
74
- if (key.name === "y" || key.name === "n") {
75
- const approvals = await listApprovalRequests(paths);
76
- const latest = approvals.sort((left, right)=>left.createdAt.localeCompare(right.createdAt)).pop();
77
- if (!latest) {
1436
+ render();
1437
+ } finally{
1438
+ refreshing = false;
1439
+ }
1440
+ };
1441
+ const runAction = (fn)=>{
1442
+ actionQueue = actionQueue.then(async ()=>{
1443
+ try {
1444
+ await fn();
1445
+ } catch (error) {
1446
+ setToast(ui, "error", error instanceof Error ? error.message : String(error));
1447
+ render();
1448
+ }
1449
+ });
1450
+ };
1451
+ const close = ()=>{
1452
+ if (closed) {
1453
+ return;
1454
+ }
1455
+ closed = true;
1456
+ if (reconnectTimer) {
1457
+ clearTimeout(reconnectTimer);
1458
+ reconnectTimer = null;
1459
+ }
1460
+ snapshotSubscription?.close();
1461
+ snapshotSubscription = null;
1462
+ process.stdin.setRawMode(false);
1463
+ process.stdin.off("keypress", keypressHandler);
1464
+ process.stdout.off("resize", resizeHandler);
1465
+ process.stdout.write("\u001b[0m\u001b[?25h\n");
1466
+ closeResolver?.();
1467
+ };
1468
+ const selectTab = (tab)=>{
1469
+ ui.activeTab = tab;
1470
+ syncUiForSnapshot(view.snapshot);
1471
+ render();
1472
+ void ensureSelectedTaskArtifact(paths, view, ui, render);
1473
+ void ensureSelectedDiffReview(paths, view, ui, render);
1474
+ };
1475
+ const moveSelection = (delta)=>{
1476
+ const items = buildTabItems(view.snapshot, ui.activeTab);
1477
+ ui.selectedIds[ui.activeTab] = moveSelectionId(items, ui.selectedIds[ui.activeTab], delta);
1478
+ ui.diffSelections = syncDiffSelections(ui.diffSelections, view.snapshot, ui.activeTab === "tasks" ? selectedTask(view.snapshot, ui) : null);
1479
+ render();
1480
+ void ensureSelectedTaskArtifact(paths, view, ui, render);
1481
+ void ensureSelectedDiffReview(paths, view, ui, render);
1482
+ };
1483
+ const keypressHandler = (input, key)=>{
1484
+ if (closed) {
1485
+ return;
1486
+ }
1487
+ if (ui.composer) {
1488
+ runAction(async ()=>{
1489
+ if (key.name === "escape") {
1490
+ ui.composer = null;
1491
+ setToast(ui, "info", "Task composition cancelled.");
1492
+ render();
1493
+ return;
1494
+ }
1495
+ if (key.ctrl && key.name === "u") {
1496
+ ui.composer.prompt = "";
1497
+ render();
1498
+ return;
1499
+ }
1500
+ if (key.name === "backspace") {
1501
+ ui.composer.prompt = ui.composer.prompt.slice(0, -1);
1502
+ render();
1503
+ return;
1504
+ }
1505
+ if (key.name === "return") {
1506
+ await queueManualTask(paths, view, ui);
78
1507
  await refresh();
1508
+ ui.selectedIds.tasks = buildTabItems(view.snapshot, "tasks")[0]?.id ?? null;
1509
+ ui.diffSelections = syncDiffSelections(ui.diffSelections, view.snapshot, selectedTask(view.snapshot, ui));
1510
+ render();
1511
+ void ensureSelectedTaskArtifact(paths, view, ui, render);
1512
+ void ensureSelectedDiffReview(paths, view, ui, render);
1513
+ return;
1514
+ }
1515
+ if (key.name === "tab" || input === "\t") {
1516
+ ui.composer.owner = ui.composer.owner === "auto" ? "codex" : ui.composer.owner === "codex" ? "claude" : "auto";
1517
+ render();
1518
+ return;
1519
+ }
1520
+ if (input === "1") {
1521
+ ui.composer.owner = "auto";
1522
+ render();
1523
+ return;
1524
+ }
1525
+ if (input === "2") {
1526
+ ui.composer.owner = "codex";
1527
+ render();
79
1528
  return;
80
1529
  }
81
- await resolveApprovalRequest(paths, latest.id, key.name === "y" ? "allow" : "deny", false);
1530
+ if (input === "3") {
1531
+ ui.composer.owner = "claude";
1532
+ render();
1533
+ return;
1534
+ }
1535
+ if (input.length === 1 && !key.ctrl && !key.meta) {
1536
+ ui.composer.prompt += input;
1537
+ render();
1538
+ }
1539
+ });
1540
+ return;
1541
+ }
1542
+ if (key.name === "q" || key.ctrl && key.name === "c") {
1543
+ close();
1544
+ return;
1545
+ }
1546
+ if (input >= "1" && input <= "7") {
1547
+ const tab = OPERATOR_TABS[Number(input) - 1];
1548
+ if (tab) {
1549
+ selectTab(tab);
1550
+ }
1551
+ return;
1552
+ }
1553
+ if (key.name === "tab") {
1554
+ selectTab(nextTab(ui.activeTab, key.shift ? -1 : 1));
1555
+ return;
1556
+ }
1557
+ if (key.name === "left" || input === "h") {
1558
+ selectTab(nextTab(ui.activeTab, -1));
1559
+ return;
1560
+ }
1561
+ if (key.name === "right" || input === "l") {
1562
+ selectTab(nextTab(ui.activeTab, 1));
1563
+ return;
1564
+ }
1565
+ if (key.name === "down" || input === "j") {
1566
+ moveSelection(1);
1567
+ return;
1568
+ }
1569
+ if (key.name === "up" || input === "k") {
1570
+ moveSelection(-1);
1571
+ return;
1572
+ }
1573
+ if (input === "g" && !key.shift) {
1574
+ const items = buildTabItems(view.snapshot, ui.activeTab);
1575
+ ui.selectedIds[ui.activeTab] = items[0]?.id ?? null;
1576
+ ui.diffSelections = syncDiffSelections(ui.diffSelections, view.snapshot, ui.activeTab === "tasks" ? selectedTask(view.snapshot, ui) : null);
1577
+ render();
1578
+ void ensureSelectedTaskArtifact(paths, view, ui, render);
1579
+ void ensureSelectedDiffReview(paths, view, ui, render);
1580
+ return;
1581
+ }
1582
+ if (key.name === "g" && key.shift) {
1583
+ const items = buildTabItems(view.snapshot, ui.activeTab);
1584
+ ui.selectedIds[ui.activeTab] = items.at(-1)?.id ?? null;
1585
+ ui.diffSelections = syncDiffSelections(ui.diffSelections, view.snapshot, ui.activeTab === "tasks" ? selectedTask(view.snapshot, ui) : null);
1586
+ render();
1587
+ void ensureSelectedTaskArtifact(paths, view, ui, render);
1588
+ void ensureSelectedDiffReview(paths, view, ui, render);
1589
+ return;
1590
+ }
1591
+ if (input === "[" || input === "]") {
1592
+ if (ui.activeTab !== "tasks") {
1593
+ return;
1594
+ }
1595
+ const currentIndex = TASK_DETAIL_SECTIONS.indexOf(ui.taskDetailSection);
1596
+ const delta = input === "[" ? -1 : 1;
1597
+ const nextIndex = (currentIndex + delta + TASK_DETAIL_SECTIONS.length) % TASK_DETAIL_SECTIONS.length;
1598
+ ui.taskDetailSection = TASK_DETAIL_SECTIONS[nextIndex] ?? ui.taskDetailSection;
1599
+ ui.diffSelections = syncDiffSelections(ui.diffSelections, view.snapshot, selectedTask(view.snapshot, ui));
1600
+ render();
1601
+ void ensureSelectedTaskArtifact(paths, view, ui, render);
1602
+ void ensureSelectedDiffReview(paths, view, ui, render);
1603
+ return;
1604
+ }
1605
+ if (input === "r") {
1606
+ runAction(refresh);
1607
+ return;
1608
+ }
1609
+ if (input === "c") {
1610
+ ui.composer = {
1611
+ owner: "auto",
1612
+ prompt: ""
1613
+ };
1614
+ render();
1615
+ return;
1616
+ }
1617
+ if (input === "s") {
1618
+ runAction(async ()=>{
1619
+ await rpcShutdown(paths);
1620
+ close();
1621
+ });
1622
+ return;
1623
+ }
1624
+ if (input === "y" || input === "n") {
1625
+ runAction(async ()=>{
1626
+ await resolveApprovalSelection(paths, view.snapshot, ui, input === "y" ? "allow" : "deny", key.shift === true);
82
1627
  await refresh();
1628
+ });
1629
+ return;
1630
+ }
1631
+ if (input === "," || input === ".") {
1632
+ const agent = cycleDiffSelection(view.snapshot, ui, input === "," ? -1 : 1);
1633
+ if (!agent) {
1634
+ return;
83
1635
  }
84
- });
85
- });
86
- if (!closed) {
87
- clearInterval(interval);
1636
+ render();
1637
+ void ensureSelectedDiffReview(paths, view, ui, render);
1638
+ return;
1639
+ }
1640
+ if (input === "{" || input === "}") {
1641
+ const agent = cycleDiffHunk(view.snapshot, ui, input === "{" ? -1 : 1);
1642
+ if (!agent) {
1643
+ return;
1644
+ }
1645
+ render();
1646
+ return;
1647
+ }
1648
+ if (key.name === "return" && ui.activeTab === "tasks") {
1649
+ const currentIndex = TASK_DETAIL_SECTIONS.indexOf(ui.taskDetailSection);
1650
+ ui.taskDetailSection = TASK_DETAIL_SECTIONS[(currentIndex + 1) % TASK_DETAIL_SECTIONS.length] ?? ui.taskDetailSection;
1651
+ ui.diffSelections = syncDiffSelections(ui.diffSelections, view.snapshot, selectedTask(view.snapshot, ui));
1652
+ render();
1653
+ void ensureSelectedTaskArtifact(paths, view, ui, render);
1654
+ void ensureSelectedDiffReview(paths, view, ui, render);
1655
+ }
1656
+ };
1657
+ const resizeHandler = ()=>{
1658
+ render();
1659
+ };
1660
+ process.stdin.on("keypress", keypressHandler);
1661
+ process.stdout.on("resize", resizeHandler);
1662
+ await connectSubscription();
1663
+ if (!view.snapshot) {
1664
+ await refresh();
88
1665
  }
1666
+ render();
1667
+ await new Promise((resolve)=>{
1668
+ closeResolver = resolve;
1669
+ });
89
1670
  }
90
1671
 
91
1672