@mandipadk7/kavi 0.1.2 → 0.1.3
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 +11 -4
- package/dist/daemon.js +322 -0
- package/dist/git.js +101 -2
- package/dist/main.js +168 -23
- package/dist/reviews.js +159 -0
- package/dist/rpc.js +36 -0
- package/dist/session.js +25 -0
- package/dist/task-artifacts.js +26 -1
- package/dist/tui.js +317 -8
- package/package.json +1 -1
package/dist/tui.js
CHANGED
|
@@ -2,7 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import readline from "node:readline";
|
|
3
3
|
import process from "node:process";
|
|
4
4
|
import { extractPromptPathHints, routeTask } from "./router.js";
|
|
5
|
-
import { pingRpc, readSnapshot, rpcEnqueueTask, rpcResolveApproval, rpcShutdown, rpcTaskArtifact, rpcWorktreeDiff, subscribeSnapshotRpc } from "./rpc.js";
|
|
5
|
+
import { pingRpc, rpcAddReviewNote, rpcAddReviewReply, rpcEnqueueReviewFollowUp, readSnapshot, rpcEnqueueTask, rpcResolveApproval, rpcSetReviewNoteStatus, rpcShutdown, rpcTaskArtifact, rpcUpdateReviewNote, rpcWorktreeDiff, subscribeSnapshotRpc } from "./rpc.js";
|
|
6
6
|
const RESET = "\u001b[0m";
|
|
7
7
|
const ANSI_PATTERN = /\u001b\[[0-9;]*m/g;
|
|
8
8
|
const SUBSCRIPTION_RETRY_MS = 1_000;
|
|
@@ -209,6 +209,12 @@ function toneLine(value, tone, selected) {
|
|
|
209
209
|
function countTasks(tasks, status) {
|
|
210
210
|
return tasks.filter((task)=>task.status === status).length;
|
|
211
211
|
}
|
|
212
|
+
function countOpenReviewNotes(snapshot, agent) {
|
|
213
|
+
if (!snapshot) {
|
|
214
|
+
return 0;
|
|
215
|
+
}
|
|
216
|
+
return snapshot.session.reviewNotes.filter((note)=>note.status === "open" && (agent ? note.agent === agent : true)).length;
|
|
217
|
+
}
|
|
212
218
|
function changedPathCount(diff) {
|
|
213
219
|
return diff?.paths.length ?? 0;
|
|
214
220
|
}
|
|
@@ -517,6 +523,106 @@ function selectedHunkIndex(ui, agent, review) {
|
|
|
517
523
|
const current = ui.hunkSelections[agent] ?? 0;
|
|
518
524
|
return Math.max(0, Math.min(current, hunks.length - 1));
|
|
519
525
|
}
|
|
526
|
+
function reviewDispositionTone(disposition) {
|
|
527
|
+
switch(disposition){
|
|
528
|
+
case "approve":
|
|
529
|
+
return "good";
|
|
530
|
+
case "concern":
|
|
531
|
+
return "bad";
|
|
532
|
+
case "question":
|
|
533
|
+
return "warn";
|
|
534
|
+
default:
|
|
535
|
+
return "muted";
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function reviewDispositionLabel(disposition) {
|
|
539
|
+
switch(disposition){
|
|
540
|
+
case "approve":
|
|
541
|
+
return "Approve";
|
|
542
|
+
case "concern":
|
|
543
|
+
return "Concern";
|
|
544
|
+
case "question":
|
|
545
|
+
return "Question";
|
|
546
|
+
case "note":
|
|
547
|
+
return "Note";
|
|
548
|
+
default:
|
|
549
|
+
return disposition;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
function activeReviewContext(snapshot, ui) {
|
|
553
|
+
const agent = reviewAgentForUi(snapshot, ui);
|
|
554
|
+
if (!agent) {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
const review = ui.diffReviews[agent]?.review ?? null;
|
|
558
|
+
const filePath = review?.selectedPath ?? selectedDiffPath(snapshot, ui, agent);
|
|
559
|
+
if (!filePath) {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
const task = ui.activeTab === "tasks" ? selectedTask(snapshot, ui) : null;
|
|
563
|
+
const hunkIndex = selectedHunkIndex(ui, agent, review);
|
|
564
|
+
const hunkHeader = hunkIndex === null ? null : parseDiffHunks(review?.patch ?? "")[hunkIndex]?.header ?? null;
|
|
565
|
+
return {
|
|
566
|
+
agent,
|
|
567
|
+
taskId: task?.owner === agent ? task.id : null,
|
|
568
|
+
filePath,
|
|
569
|
+
hunkIndex,
|
|
570
|
+
hunkHeader
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
function reviewNotesForContext(snapshot, context) {
|
|
574
|
+
if (!snapshot || !context) {
|
|
575
|
+
return [];
|
|
576
|
+
}
|
|
577
|
+
return [
|
|
578
|
+
...snapshot.session.reviewNotes
|
|
579
|
+
].filter((note)=>{
|
|
580
|
+
if (note.agent !== context.agent || note.filePath !== context.filePath) {
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
if (context.hunkIndex !== null) {
|
|
584
|
+
return note.hunkIndex === context.hunkIndex || note.hunkIndex === null;
|
|
585
|
+
}
|
|
586
|
+
return true;
|
|
587
|
+
}).sort((left, right)=>right.createdAt.localeCompare(left.createdAt));
|
|
588
|
+
}
|
|
589
|
+
function syncSelectedReviewNote(snapshot, ui) {
|
|
590
|
+
const notes = reviewNotesForContext(snapshot, activeReviewContext(snapshot, ui));
|
|
591
|
+
ui.selectedReviewNoteId = notes.some((note)=>note.id === ui.selectedReviewNoteId) ? ui.selectedReviewNoteId : notes[0]?.id ?? null;
|
|
592
|
+
}
|
|
593
|
+
function selectedReviewNote(snapshot, ui) {
|
|
594
|
+
const notes = reviewNotesForContext(snapshot, activeReviewContext(snapshot, ui));
|
|
595
|
+
return notes.find((note)=>note.id === ui.selectedReviewNoteId) ?? notes[0] ?? null;
|
|
596
|
+
}
|
|
597
|
+
function renderReviewNotesSection(notes, selectedNoteId, width) {
|
|
598
|
+
if (notes.length === 0) {
|
|
599
|
+
return [
|
|
600
|
+
"- none"
|
|
601
|
+
];
|
|
602
|
+
}
|
|
603
|
+
return notes.flatMap((note)=>{
|
|
604
|
+
const stateLabel = note.landedAt ? `${note.status}+landed` : note.status;
|
|
605
|
+
const prefix = `${note.id === selectedNoteId ? ">" : "-"} [${reviewDispositionLabel(note.disposition)} ${stateLabel}] ${shortTime(note.createdAt)}`;
|
|
606
|
+
const followUps = note.followUpTaskIds.length > 0 ? ` | follow-ups=${note.followUpTaskIds.length}` : "";
|
|
607
|
+
const replies = note.comments.length > 1 ? ` | replies=${note.comments.length - 1}` : "";
|
|
608
|
+
const landed = note.landedAt ? ` | landed=${shortTime(note.landedAt)}` : "";
|
|
609
|
+
const detail = note.body || note.summary;
|
|
610
|
+
return wrapText(`${prefix} ${detail}${followUps}${replies}${landed}`, width).map((line)=>toneLine(line, note.status === "resolved" ? "muted" : reviewDispositionTone(note.disposition), note.id === selectedNoteId));
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
function renderSelectedReviewNoteSection(note, width) {
|
|
614
|
+
if (!note) {
|
|
615
|
+
return [
|
|
616
|
+
"- none"
|
|
617
|
+
];
|
|
618
|
+
}
|
|
619
|
+
return [
|
|
620
|
+
...wrapText(`Status: ${note.status} | Disposition: ${reviewDispositionLabel(note.disposition)} | Updated: ${shortTime(note.updatedAt)}`, width),
|
|
621
|
+
...wrapText(`Landed: ${note.landedAt ? shortTime(note.landedAt) : "-"}`, width),
|
|
622
|
+
...wrapText(`Follow-up tasks: ${note.followUpTaskIds.join(", ") || "-"}`, width),
|
|
623
|
+
...note.comments.flatMap((comment, index)=>wrapText(`${index === 0 ? "Root" : `Reply ${index}`}: ${comment.body} (${shortTime(comment.updatedAt)})`, width))
|
|
624
|
+
];
|
|
625
|
+
}
|
|
520
626
|
function latestPendingApproval(approvals) {
|
|
521
627
|
return [
|
|
522
628
|
...approvals
|
|
@@ -610,7 +716,7 @@ function artifactForTask(ui, task) {
|
|
|
610
716
|
function taskDetailTitle(ui) {
|
|
611
717
|
return `Inspector | Task ${ui.taskDetailSection}`;
|
|
612
718
|
}
|
|
613
|
-
function renderTaskInspector(task, artifactEntry, loading, diffEntry, loadingDiff, ui, width, height) {
|
|
719
|
+
function renderTaskInspector(task, artifactEntry, loading, diffEntry, loadingDiff, snapshot, ui, width, height) {
|
|
614
720
|
if (!task) {
|
|
615
721
|
return renderPanel("Inspector", width, height, [
|
|
616
722
|
styleLine("No task selected.", "muted")
|
|
@@ -698,12 +804,14 @@ function renderTaskInspector(task, artifactEntry, loading, diffEntry, loadingDif
|
|
|
698
804
|
const hunks = parseDiffHunks(review.patch);
|
|
699
805
|
const hunkIndex = agent ? selectedHunkIndex(ui, agent, review) : null;
|
|
700
806
|
const selectedHunk = hunkIndex === null ? null : hunks[hunkIndex] ?? null;
|
|
807
|
+
const reviewNotes = reviewNotesForContext(snapshot, activeReviewContext(snapshot, ui));
|
|
701
808
|
lines.push(...section("Review", [
|
|
702
809
|
`Agent: ${review.agent}`,
|
|
703
810
|
`Selected file: ${review.selectedPath ?? "-"}`,
|
|
704
811
|
`Changed files: ${review.changedPaths.length}`,
|
|
705
812
|
`Hunks: ${hunks.length}`,
|
|
706
813
|
`Selected hunk: ${hunkIndex === null ? "-" : `${hunkIndex + 1}/${hunks.length}`}`,
|
|
814
|
+
`Notes: ${reviewNotes.length}`,
|
|
707
815
|
`Stat: ${review.stat}`
|
|
708
816
|
].flatMap((line)=>wrapText(line, innerWidth))));
|
|
709
817
|
lines.push(...section("Changed Files", review.changedPaths.length ? review.changedPaths.flatMap((filePath)=>wrapText(`${filePath === review.selectedPath ? ">" : "-"} ${filePath}`, innerWidth)) : [
|
|
@@ -718,6 +826,8 @@ function renderTaskInspector(task, artifactEntry, loading, diffEntry, loadingDif
|
|
|
718
826
|
lines.push(...section("Patch", review.patch ? wrapPreformatted(review.patch, innerWidth) : [
|
|
719
827
|
"No textual patch available."
|
|
720
828
|
]));
|
|
829
|
+
lines.push(...section("Review Notes", renderReviewNotesSection(reviewNotes, ui.selectedReviewNoteId, innerWidth)));
|
|
830
|
+
lines.push(...section("Selected Review Note", renderSelectedReviewNoteSection(selectedReviewNote(snapshot, ui), innerWidth)));
|
|
721
831
|
} else {
|
|
722
832
|
lines.push(...section("Diff Review", [
|
|
723
833
|
"No diff review available yet."
|
|
@@ -892,10 +1002,12 @@ function renderWorktreeInspector(snapshot, worktree, diffEntry, loadingDiff, ui,
|
|
|
892
1002
|
const hunks = parseDiffHunks(diffEntry.review.patch);
|
|
893
1003
|
const hunkIndex = selectedHunkIndex(ui, worktree.agent, diffEntry.review);
|
|
894
1004
|
const selectedHunk = hunkIndex === null ? null : hunks[hunkIndex] ?? null;
|
|
1005
|
+
const reviewNotes = reviewNotesForContext(snapshot, activeReviewContext(snapshot, ui));
|
|
895
1006
|
lines.push(...section("Review", [
|
|
896
1007
|
`Selected file: ${diffEntry.review.selectedPath ?? "-"}`,
|
|
897
1008
|
`Hunks: ${hunks.length}`,
|
|
898
1009
|
`Selected hunk: ${hunkIndex === null ? "-" : `${hunkIndex + 1}/${hunks.length}`}`,
|
|
1010
|
+
`Notes: ${reviewNotes.length}`,
|
|
899
1011
|
`Stat: ${diffEntry.review.stat}`
|
|
900
1012
|
].flatMap((line)=>wrapText(line, innerWidth))));
|
|
901
1013
|
if (selectedHunk) {
|
|
@@ -907,6 +1019,8 @@ function renderWorktreeInspector(snapshot, worktree, diffEntry, loadingDiff, ui,
|
|
|
907
1019
|
lines.push(...section("Patch", diffEntry.review.patch ? wrapPreformatted(diffEntry.review.patch, innerWidth) : [
|
|
908
1020
|
"No textual patch available."
|
|
909
1021
|
]));
|
|
1022
|
+
lines.push(...section("Review Notes", renderReviewNotesSection(reviewNotes, ui.selectedReviewNoteId, innerWidth)));
|
|
1023
|
+
lines.push(...section("Selected Review Note", renderSelectedReviewNoteSection(selectedReviewNote(snapshot, ui), innerWidth)));
|
|
910
1024
|
} else {
|
|
911
1025
|
lines.push(...section("Diff Review", [
|
|
912
1026
|
"No diff review available yet."
|
|
@@ -921,7 +1035,7 @@ function renderWorktreeInspector(snapshot, worktree, diffEntry, loadingDiff, ui,
|
|
|
921
1035
|
function renderInspector(snapshot, ui, width, height) {
|
|
922
1036
|
switch(ui.activeTab){
|
|
923
1037
|
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);
|
|
1038
|
+
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, snapshot, ui, width, height);
|
|
925
1039
|
case "approvals":
|
|
926
1040
|
return renderApprovalInspector(selectedApproval(snapshot, ui), width, height);
|
|
927
1041
|
case "claims":
|
|
@@ -965,7 +1079,8 @@ function renderLane(snapshot, agent, width, height) {
|
|
|
965
1079
|
`Worktree: ${worktree ? path.basename(worktree.path) : "-"}`,
|
|
966
1080
|
`Branch: ${worktree?.branch ?? "-"}`,
|
|
967
1081
|
`Pending approvals: ${approvals.length}`,
|
|
968
|
-
`Changed paths: ${diff?.paths.length ?? 0}
|
|
1082
|
+
`Changed paths: ${diff?.paths.length ?? 0}`,
|
|
1083
|
+
`Open reviews: ${countOpenReviewNotes(snapshot, agent)}`
|
|
969
1084
|
].flatMap((line)=>wrapText(line, innerWidth))),
|
|
970
1085
|
...section("Summary", wrapText(status.summary ?? "No summary yet.", innerWidth)),
|
|
971
1086
|
...section("Tasks", tasks.length ? tasks.slice(0, 4).flatMap((task)=>wrapText(`- [${task.status}] ${task.title}`, innerWidth)) : [
|
|
@@ -986,7 +1101,7 @@ function renderHeader(view, ui, width) {
|
|
|
986
1101
|
const repoName = path.basename(session?.repoRoot ?? process.cwd());
|
|
987
1102
|
const line1 = fitAnsiLine(`${toneForPanel("Kavi Operator", true)} | session=${session?.id ?? "-"} | repo=${repoName} | rpc=${view.connected ? "connected" : "disconnected"}`, width);
|
|
988
1103
|
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);
|
|
1104
|
+
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} | reviews=${countOpenReviewNotes(snapshot)} | claims=${session.pathClaims.filter((claim)=>claim.status === "active").length} | decisions=${session.decisions.length}` : "Waiting for session snapshot...", width);
|
|
990
1105
|
const tabs = OPERATOR_TABS.map((tab, index)=>{
|
|
991
1106
|
const count = buildTabItems(snapshot, tab).length;
|
|
992
1107
|
const label = `[${index + 1}] ${tabLabel(tab)} ${count}`;
|
|
@@ -1015,6 +1130,19 @@ function footerSelectionSummary(snapshot, ui, width) {
|
|
|
1015
1130
|
}
|
|
1016
1131
|
function renderFooter(snapshot, ui, width) {
|
|
1017
1132
|
const toast = currentToast(ui);
|
|
1133
|
+
const reviewContext = activeReviewContext(snapshot, ui);
|
|
1134
|
+
if (ui.reviewComposer) {
|
|
1135
|
+
const composerHeader = fitAnsiLine(styleLine(ui.reviewComposer.mode === "edit" ? "Edit Review Note" : ui.reviewComposer.mode === "reply" ? "Reply To Review Note" : "Capture Review Note", "accent", "strong"), width);
|
|
1136
|
+
const composerLine = fitAnsiLine(`Disposition: ${reviewDispositionLabel(ui.reviewComposer.disposition)} | Enter save | Esc cancel | Ctrl+U clear`, width);
|
|
1137
|
+
const scopeLine = fitAnsiLine(`Scope: ${reviewContext?.agent ?? "-"} | ${reviewContext?.filePath ?? "-"}${reviewContext?.hunkIndex === null || reviewContext?.hunkIndex === undefined ? "" : ` | hunk ${reviewContext.hunkIndex + 1}`}`, width);
|
|
1138
|
+
const promptLine = fitAnsiLine(`> ${ui.reviewComposer.body}`, width);
|
|
1139
|
+
return [
|
|
1140
|
+
composerHeader,
|
|
1141
|
+
composerLine,
|
|
1142
|
+
scopeLine,
|
|
1143
|
+
promptLine
|
|
1144
|
+
];
|
|
1145
|
+
}
|
|
1018
1146
|
if (ui.composer) {
|
|
1019
1147
|
const composerHeader = fitAnsiLine(styleLine("Compose Task", "accent", "strong"), width);
|
|
1020
1148
|
const composerLine = fitAnsiLine(`Route: ${ui.composer.owner} | 1 auto 2 codex 3 claude | Enter submit | Esc cancel | Ctrl+U clear`, width);
|
|
@@ -1028,7 +1156,7 @@ function renderFooter(snapshot, ui, width) {
|
|
|
1028
1156
|
}
|
|
1029
1157
|
return [
|
|
1030
1158
|
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),
|
|
1159
|
+
fitAnsiLine("Actions: y/Y allow approval | n/N deny approval | A/C/Q/M add note | o/O select note | T reply | E edit | R resolve | F fix task | H handoff | g/G top/bottom | s stop daemon | q quit", width),
|
|
1032
1160
|
footerSelectionSummary(snapshot, ui, width),
|
|
1033
1161
|
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
1162
|
];
|
|
@@ -1187,6 +1315,76 @@ async function resolveApprovalSelection(paths, snapshot, ui, decision, remember)
|
|
|
1187
1315
|
});
|
|
1188
1316
|
setToast(ui, "info", `${decision === "allow" ? "Approved" : "Denied"} ${approval.toolName}${remember ? " with remembered rule" : ""}.`);
|
|
1189
1317
|
}
|
|
1318
|
+
async function submitReviewNote(paths, view, ui) {
|
|
1319
|
+
const composer = ui.reviewComposer;
|
|
1320
|
+
const context = activeReviewContext(view.snapshot, ui);
|
|
1321
|
+
if (!composer || !context) {
|
|
1322
|
+
throw new Error("No active diff review context is available.");
|
|
1323
|
+
}
|
|
1324
|
+
const body = composer.body.trim();
|
|
1325
|
+
if (!body) {
|
|
1326
|
+
throw new Error("Review note cannot be empty.");
|
|
1327
|
+
}
|
|
1328
|
+
if (composer.mode === "edit" && composer.noteId) {
|
|
1329
|
+
await rpcUpdateReviewNote(paths, {
|
|
1330
|
+
noteId: composer.noteId,
|
|
1331
|
+
body
|
|
1332
|
+
});
|
|
1333
|
+
} else if (composer.mode === "reply" && composer.noteId) {
|
|
1334
|
+
await rpcAddReviewReply(paths, {
|
|
1335
|
+
noteId: composer.noteId,
|
|
1336
|
+
body
|
|
1337
|
+
});
|
|
1338
|
+
} else {
|
|
1339
|
+
await rpcAddReviewNote(paths, {
|
|
1340
|
+
agent: context.agent,
|
|
1341
|
+
taskId: context.taskId,
|
|
1342
|
+
filePath: context.filePath,
|
|
1343
|
+
hunkIndex: context.hunkIndex,
|
|
1344
|
+
hunkHeader: context.hunkHeader,
|
|
1345
|
+
disposition: composer.disposition,
|
|
1346
|
+
body
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
ui.reviewComposer = null;
|
|
1350
|
+
setToast(ui, "info", composer.mode === "reply" ? `Added reply to review note ${composer.noteId ?? "-"}.` : `${reviewDispositionLabel(composer.disposition)} note ${composer.mode === "edit" ? "updated" : "saved"} for ${context.filePath}${context.hunkIndex === null ? "" : ` hunk ${context.hunkIndex + 1}`}.`);
|
|
1351
|
+
}
|
|
1352
|
+
async function toggleSelectedReviewNoteStatus(paths, snapshot, ui) {
|
|
1353
|
+
const note = selectedReviewNote(snapshot, ui);
|
|
1354
|
+
if (!note) {
|
|
1355
|
+
throw new Error("No review note is selected.");
|
|
1356
|
+
}
|
|
1357
|
+
const status = note.status === "resolved" ? "open" : "resolved";
|
|
1358
|
+
await rpcSetReviewNoteStatus(paths, {
|
|
1359
|
+
noteId: note.id,
|
|
1360
|
+
status
|
|
1361
|
+
});
|
|
1362
|
+
setToast(ui, "info", `${status === "resolved" ? "Resolved" : "Reopened"} review note ${note.id}.`);
|
|
1363
|
+
}
|
|
1364
|
+
async function enqueueSelectedReviewFollowUp(paths, snapshot, ui, mode) {
|
|
1365
|
+
const note = selectedReviewNote(snapshot, ui);
|
|
1366
|
+
if (!note) {
|
|
1367
|
+
throw new Error("No review note is selected.");
|
|
1368
|
+
}
|
|
1369
|
+
const owner = mode === "fix" ? note.agent : note.agent === "codex" ? "claude" : "codex";
|
|
1370
|
+
await rpcEnqueueReviewFollowUp(paths, {
|
|
1371
|
+
noteId: note.id,
|
|
1372
|
+
owner,
|
|
1373
|
+
mode
|
|
1374
|
+
});
|
|
1375
|
+
setToast(ui, "info", `Queued ${mode === "fix" ? "fix" : "handoff"} follow-up for review note ${note.id} to ${owner}.`);
|
|
1376
|
+
}
|
|
1377
|
+
function cycleSelectedReviewNote(snapshot, ui, delta) {
|
|
1378
|
+
const notes = reviewNotesForContext(snapshot, activeReviewContext(snapshot, ui));
|
|
1379
|
+
if (notes.length === 0) {
|
|
1380
|
+
ui.selectedReviewNoteId = null;
|
|
1381
|
+
return false;
|
|
1382
|
+
}
|
|
1383
|
+
const currentIndex = Math.max(0, notes.findIndex((note)=>note.id === ui.selectedReviewNoteId));
|
|
1384
|
+
const nextIndex = (currentIndex + delta + notes.length) % notes.length;
|
|
1385
|
+
ui.selectedReviewNoteId = notes[nextIndex]?.id ?? notes[0]?.id ?? null;
|
|
1386
|
+
return true;
|
|
1387
|
+
}
|
|
1190
1388
|
async function ensureSelectedTaskArtifact(paths, view, ui, render) {
|
|
1191
1389
|
if (!view.connected || ui.activeTab !== "tasks") {
|
|
1192
1390
|
return;
|
|
@@ -1261,6 +1459,7 @@ async function ensureSelectedDiffReview(paths, view, ui, render) {
|
|
|
1261
1459
|
};
|
|
1262
1460
|
} finally{
|
|
1263
1461
|
ui.loadingDiffReviews[agent] = false;
|
|
1462
|
+
syncSelectedReviewNote(view.snapshot, ui);
|
|
1264
1463
|
render();
|
|
1265
1464
|
}
|
|
1266
1465
|
}
|
|
@@ -1320,6 +1519,7 @@ export async function attachTui(paths) {
|
|
|
1320
1519
|
selectedIds: emptySelectionMap(),
|
|
1321
1520
|
taskDetailSection: "overview",
|
|
1322
1521
|
composer: null,
|
|
1522
|
+
reviewComposer: null,
|
|
1323
1523
|
toast: null,
|
|
1324
1524
|
artifacts: {},
|
|
1325
1525
|
loadingArtifacts: {},
|
|
@@ -1338,7 +1538,8 @@ export async function attachTui(paths) {
|
|
|
1338
1538
|
hunkSelections: {
|
|
1339
1539
|
codex: 0,
|
|
1340
1540
|
claude: 0
|
|
1341
|
-
}
|
|
1541
|
+
},
|
|
1542
|
+
selectedReviewNoteId: null
|
|
1342
1543
|
};
|
|
1343
1544
|
const render = ()=>{
|
|
1344
1545
|
process.stdout.write(renderScreen(view, ui, paths));
|
|
@@ -1346,6 +1547,7 @@ export async function attachTui(paths) {
|
|
|
1346
1547
|
const syncUiForSnapshot = (snapshot)=>{
|
|
1347
1548
|
ui.selectedIds = syncSelections(ui.selectedIds, snapshot);
|
|
1348
1549
|
ui.diffSelections = syncDiffSelections(ui.diffSelections, snapshot, ui.activeTab === "tasks" ? selectedTask(snapshot, ui) : null);
|
|
1550
|
+
syncSelectedReviewNote(snapshot, ui);
|
|
1349
1551
|
};
|
|
1350
1552
|
const applySnapshot = (snapshot, reason)=>{
|
|
1351
1553
|
view.snapshot = snapshot;
|
|
@@ -1476,6 +1678,7 @@ export async function attachTui(paths) {
|
|
|
1476
1678
|
const items = buildTabItems(view.snapshot, ui.activeTab);
|
|
1477
1679
|
ui.selectedIds[ui.activeTab] = moveSelectionId(items, ui.selectedIds[ui.activeTab], delta);
|
|
1478
1680
|
ui.diffSelections = syncDiffSelections(ui.diffSelections, view.snapshot, ui.activeTab === "tasks" ? selectedTask(view.snapshot, ui) : null);
|
|
1681
|
+
syncSelectedReviewNote(view.snapshot, ui);
|
|
1479
1682
|
render();
|
|
1480
1683
|
void ensureSelectedTaskArtifact(paths, view, ui, render);
|
|
1481
1684
|
void ensureSelectedDiffReview(paths, view, ui, render);
|
|
@@ -1484,6 +1687,37 @@ export async function attachTui(paths) {
|
|
|
1484
1687
|
if (closed) {
|
|
1485
1688
|
return;
|
|
1486
1689
|
}
|
|
1690
|
+
if (ui.reviewComposer) {
|
|
1691
|
+
runAction(async ()=>{
|
|
1692
|
+
if (key.name === "escape") {
|
|
1693
|
+
ui.reviewComposer = null;
|
|
1694
|
+
setToast(ui, "info", "Review note capture cancelled.");
|
|
1695
|
+
render();
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
if (key.ctrl && key.name === "u") {
|
|
1699
|
+
ui.reviewComposer.body = "";
|
|
1700
|
+
render();
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
if (key.name === "backspace") {
|
|
1704
|
+
ui.reviewComposer.body = ui.reviewComposer.body.slice(0, -1);
|
|
1705
|
+
render();
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
if (key.name === "return") {
|
|
1709
|
+
await submitReviewNote(paths, view, ui);
|
|
1710
|
+
await refresh();
|
|
1711
|
+
render();
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
if (input.length === 1 && !key.ctrl && !key.meta) {
|
|
1715
|
+
ui.reviewComposer.body += input;
|
|
1716
|
+
render();
|
|
1717
|
+
}
|
|
1718
|
+
});
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1487
1721
|
if (ui.composer) {
|
|
1488
1722
|
runAction(async ()=>{
|
|
1489
1723
|
if (key.name === "escape") {
|
|
@@ -1507,6 +1741,7 @@ export async function attachTui(paths) {
|
|
|
1507
1741
|
await refresh();
|
|
1508
1742
|
ui.selectedIds.tasks = buildTabItems(view.snapshot, "tasks")[0]?.id ?? null;
|
|
1509
1743
|
ui.diffSelections = syncDiffSelections(ui.diffSelections, view.snapshot, selectedTask(view.snapshot, ui));
|
|
1744
|
+
syncSelectedReviewNote(view.snapshot, ui);
|
|
1510
1745
|
render();
|
|
1511
1746
|
void ensureSelectedTaskArtifact(paths, view, ui, render);
|
|
1512
1747
|
void ensureSelectedDiffReview(paths, view, ui, render);
|
|
@@ -1539,7 +1774,7 @@ export async function attachTui(paths) {
|
|
|
1539
1774
|
});
|
|
1540
1775
|
return;
|
|
1541
1776
|
}
|
|
1542
|
-
if (
|
|
1777
|
+
if (input === "q" || key.ctrl && key.name === "c") {
|
|
1543
1778
|
close();
|
|
1544
1779
|
return;
|
|
1545
1780
|
}
|
|
@@ -1574,6 +1809,7 @@ export async function attachTui(paths) {
|
|
|
1574
1809
|
const items = buildTabItems(view.snapshot, ui.activeTab);
|
|
1575
1810
|
ui.selectedIds[ui.activeTab] = items[0]?.id ?? null;
|
|
1576
1811
|
ui.diffSelections = syncDiffSelections(ui.diffSelections, view.snapshot, ui.activeTab === "tasks" ? selectedTask(view.snapshot, ui) : null);
|
|
1812
|
+
syncSelectedReviewNote(view.snapshot, ui);
|
|
1577
1813
|
render();
|
|
1578
1814
|
void ensureSelectedTaskArtifact(paths, view, ui, render);
|
|
1579
1815
|
void ensureSelectedDiffReview(paths, view, ui, render);
|
|
@@ -1583,6 +1819,7 @@ export async function attachTui(paths) {
|
|
|
1583
1819
|
const items = buildTabItems(view.snapshot, ui.activeTab);
|
|
1584
1820
|
ui.selectedIds[ui.activeTab] = items.at(-1)?.id ?? null;
|
|
1585
1821
|
ui.diffSelections = syncDiffSelections(ui.diffSelections, view.snapshot, ui.activeTab === "tasks" ? selectedTask(view.snapshot, ui) : null);
|
|
1822
|
+
syncSelectedReviewNote(view.snapshot, ui);
|
|
1586
1823
|
render();
|
|
1587
1824
|
void ensureSelectedTaskArtifact(paths, view, ui, render);
|
|
1588
1825
|
void ensureSelectedDiffReview(paths, view, ui, render);
|
|
@@ -1597,6 +1834,7 @@ export async function attachTui(paths) {
|
|
|
1597
1834
|
const nextIndex = (currentIndex + delta + TASK_DETAIL_SECTIONS.length) % TASK_DETAIL_SECTIONS.length;
|
|
1598
1835
|
ui.taskDetailSection = TASK_DETAIL_SECTIONS[nextIndex] ?? ui.taskDetailSection;
|
|
1599
1836
|
ui.diffSelections = syncDiffSelections(ui.diffSelections, view.snapshot, selectedTask(view.snapshot, ui));
|
|
1837
|
+
syncSelectedReviewNote(view.snapshot, ui);
|
|
1600
1838
|
render();
|
|
1601
1839
|
void ensureSelectedTaskArtifact(paths, view, ui, render);
|
|
1602
1840
|
void ensureSelectedDiffReview(paths, view, ui, render);
|
|
@@ -1628,12 +1866,81 @@ export async function attachTui(paths) {
|
|
|
1628
1866
|
});
|
|
1629
1867
|
return;
|
|
1630
1868
|
}
|
|
1869
|
+
if (input === "A" || input === "C" || input === "Q" || input === "M") {
|
|
1870
|
+
if (!activeReviewContext(view.snapshot, ui)) {
|
|
1871
|
+
setToast(ui, "error", "No active diff review context is selected.");
|
|
1872
|
+
render();
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
ui.reviewComposer = {
|
|
1876
|
+
mode: "create",
|
|
1877
|
+
disposition: input === "A" ? "approve" : input === "C" ? "concern" : input === "Q" ? "question" : "note",
|
|
1878
|
+
noteId: null,
|
|
1879
|
+
body: ""
|
|
1880
|
+
};
|
|
1881
|
+
render();
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
if (input === "o" || input === "O") {
|
|
1885
|
+
if (!cycleSelectedReviewNote(view.snapshot, ui, input === "O" ? -1 : 1)) {
|
|
1886
|
+
setToast(ui, "error", "No review notes are available in the current diff context.");
|
|
1887
|
+
}
|
|
1888
|
+
render();
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
if (input === "E") {
|
|
1892
|
+
const note = selectedReviewNote(view.snapshot, ui);
|
|
1893
|
+
if (!note) {
|
|
1894
|
+
setToast(ui, "error", "No review note is selected.");
|
|
1895
|
+
render();
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
ui.reviewComposer = {
|
|
1899
|
+
mode: "edit",
|
|
1900
|
+
disposition: note.disposition,
|
|
1901
|
+
noteId: note.id,
|
|
1902
|
+
body: note.body
|
|
1903
|
+
};
|
|
1904
|
+
render();
|
|
1905
|
+
return;
|
|
1906
|
+
}
|
|
1907
|
+
if (input === "T") {
|
|
1908
|
+
const note = selectedReviewNote(view.snapshot, ui);
|
|
1909
|
+
if (!note) {
|
|
1910
|
+
setToast(ui, "error", "No review note is selected.");
|
|
1911
|
+
render();
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
ui.reviewComposer = {
|
|
1915
|
+
mode: "reply",
|
|
1916
|
+
disposition: note.disposition,
|
|
1917
|
+
noteId: note.id,
|
|
1918
|
+
body: ""
|
|
1919
|
+
};
|
|
1920
|
+
render();
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
if (input === "R") {
|
|
1924
|
+
runAction(async ()=>{
|
|
1925
|
+
await toggleSelectedReviewNoteStatus(paths, view.snapshot, ui);
|
|
1926
|
+
await refresh();
|
|
1927
|
+
});
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
if (input === "F" || input === "H") {
|
|
1931
|
+
runAction(async ()=>{
|
|
1932
|
+
await enqueueSelectedReviewFollowUp(paths, view.snapshot, ui, input === "F" ? "fix" : "handoff");
|
|
1933
|
+
await refresh();
|
|
1934
|
+
});
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1631
1937
|
if (input === "," || input === ".") {
|
|
1632
1938
|
const agent = cycleDiffSelection(view.snapshot, ui, input === "," ? -1 : 1);
|
|
1633
1939
|
if (!agent) {
|
|
1634
1940
|
return;
|
|
1635
1941
|
}
|
|
1636
1942
|
render();
|
|
1943
|
+
syncSelectedReviewNote(view.snapshot, ui);
|
|
1637
1944
|
void ensureSelectedDiffReview(paths, view, ui, render);
|
|
1638
1945
|
return;
|
|
1639
1946
|
}
|
|
@@ -1643,12 +1950,14 @@ export async function attachTui(paths) {
|
|
|
1643
1950
|
return;
|
|
1644
1951
|
}
|
|
1645
1952
|
render();
|
|
1953
|
+
syncSelectedReviewNote(view.snapshot, ui);
|
|
1646
1954
|
return;
|
|
1647
1955
|
}
|
|
1648
1956
|
if (key.name === "return" && ui.activeTab === "tasks") {
|
|
1649
1957
|
const currentIndex = TASK_DETAIL_SECTIONS.indexOf(ui.taskDetailSection);
|
|
1650
1958
|
ui.taskDetailSection = TASK_DETAIL_SECTIONS[(currentIndex + 1) % TASK_DETAIL_SECTIONS.length] ?? ui.taskDetailSection;
|
|
1651
1959
|
ui.diffSelections = syncDiffSelections(ui.diffSelections, view.snapshot, selectedTask(view.snapshot, ui));
|
|
1960
|
+
syncSelectedReviewNote(view.snapshot, ui);
|
|
1652
1961
|
render();
|
|
1653
1962
|
void ensureSelectedTaskArtifact(paths, view, ui, render);
|
|
1654
1963
|
void ensureSelectedDiffReview(paths, view, ui, render);
|