@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/README.md +25 -7
- package/dist/adapters/claude.js +136 -13
- package/dist/adapters/codex.js +235 -30
- package/dist/adapters/shared.js +35 -0
- package/dist/approvals.js +72 -1
- package/dist/codex-app-server.js +310 -0
- package/dist/command-queue.js +1 -0
- package/dist/daemon.js +446 -5
- package/dist/decision-ledger.js +75 -0
- package/dist/git.js +171 -0
- package/dist/main.js +251 -36
- package/dist/paths.js +1 -1
- package/dist/prompts.js +13 -0
- package/dist/router.js +190 -5
- package/dist/rpc.js +226 -0
- package/dist/runtime.js +4 -1
- package/dist/session.js +10 -1
- package/dist/task-artifacts.js +10 -2
- package/dist/tui.js +1653 -72
- package/package.json +7 -12
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 {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
|