@mandipadk7/kavi 0.1.2 → 0.1.4
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 -65
- package/dist/config.js +8 -2
- package/dist/daemon.js +357 -0
- package/dist/doctor.js +33 -0
- package/dist/git.js +101 -2
- package/dist/main.js +171 -23
- package/dist/reviews.js +188 -0
- package/dist/router.js +69 -0
- package/dist/rpc.js +45 -0
- package/dist/session.js +27 -0
- package/dist/task-artifacts.js +28 -1
- package/dist/tui.js +381 -8
- package/package.json +2 -1
package/dist/tui.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import readline from "node:readline";
|
|
3
3
|
import process from "node:process";
|
|
4
|
+
import { cycleReviewAssignee } from "./reviews.js";
|
|
4
5
|
import { extractPromptPathHints, routeTask } from "./router.js";
|
|
5
|
-
import { pingRpc, readSnapshot, rpcEnqueueTask, rpcResolveApproval, rpcShutdown, rpcTaskArtifact, rpcWorktreeDiff, subscribeSnapshotRpc } from "./rpc.js";
|
|
6
|
+
import { pingRpc, rpcAddReviewNote, rpcAddReviewReply, rpcEnqueueReviewFollowUp, readSnapshot, rpcEnqueueTask, rpcResolveApproval, rpcSetReviewNoteStatus, rpcShutdown, rpcTaskArtifact, rpcUpdateReviewNote, rpcWorktreeDiff, subscribeSnapshotRpc } from "./rpc.js";
|
|
6
7
|
const RESET = "\u001b[0m";
|
|
7
8
|
const ANSI_PATTERN = /\u001b\[[0-9;]*m/g;
|
|
8
9
|
const SUBSCRIPTION_RETRY_MS = 1_000;
|
|
@@ -209,6 +210,12 @@ function toneLine(value, tone, selected) {
|
|
|
209
210
|
function countTasks(tasks, status) {
|
|
210
211
|
return tasks.filter((task)=>task.status === status).length;
|
|
211
212
|
}
|
|
213
|
+
function countOpenReviewNotes(snapshot, agent) {
|
|
214
|
+
if (!snapshot) {
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
return snapshot.session.reviewNotes.filter((note)=>note.status === "open" && (agent ? note.agent === agent : true)).length;
|
|
218
|
+
}
|
|
212
219
|
function changedPathCount(diff) {
|
|
213
220
|
return diff?.paths.length ?? 0;
|
|
214
221
|
}
|
|
@@ -517,6 +524,127 @@ function selectedHunkIndex(ui, agent, review) {
|
|
|
517
524
|
const current = ui.hunkSelections[agent] ?? 0;
|
|
518
525
|
return Math.max(0, Math.min(current, hunks.length - 1));
|
|
519
526
|
}
|
|
527
|
+
function reviewDispositionTone(disposition) {
|
|
528
|
+
switch(disposition){
|
|
529
|
+
case "approve":
|
|
530
|
+
return "good";
|
|
531
|
+
case "concern":
|
|
532
|
+
return "bad";
|
|
533
|
+
case "question":
|
|
534
|
+
case "accepted_risk":
|
|
535
|
+
return "warn";
|
|
536
|
+
case "wont_fix":
|
|
537
|
+
return "muted";
|
|
538
|
+
default:
|
|
539
|
+
return "muted";
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function reviewDispositionLabel(disposition) {
|
|
543
|
+
switch(disposition){
|
|
544
|
+
case "approve":
|
|
545
|
+
return "Approve";
|
|
546
|
+
case "concern":
|
|
547
|
+
return "Concern";
|
|
548
|
+
case "question":
|
|
549
|
+
return "Question";
|
|
550
|
+
case "accepted_risk":
|
|
551
|
+
return "Accepted Risk";
|
|
552
|
+
case "wont_fix":
|
|
553
|
+
return "Won't Fix";
|
|
554
|
+
case "note":
|
|
555
|
+
return "Note";
|
|
556
|
+
default:
|
|
557
|
+
return disposition;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function reviewAssigneeLabel(assignee) {
|
|
561
|
+
switch(assignee){
|
|
562
|
+
case "codex":
|
|
563
|
+
return "codex";
|
|
564
|
+
case "claude":
|
|
565
|
+
return "claude";
|
|
566
|
+
case "operator":
|
|
567
|
+
return "operator";
|
|
568
|
+
default:
|
|
569
|
+
return "unassigned";
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
function activeReviewContext(snapshot, ui) {
|
|
573
|
+
const agent = reviewAgentForUi(snapshot, ui);
|
|
574
|
+
if (!agent) {
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
const review = ui.diffReviews[agent]?.review ?? null;
|
|
578
|
+
const filePath = review?.selectedPath ?? selectedDiffPath(snapshot, ui, agent);
|
|
579
|
+
if (!filePath) {
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
const task = ui.activeTab === "tasks" ? selectedTask(snapshot, ui) : null;
|
|
583
|
+
const hunkIndex = selectedHunkIndex(ui, agent, review);
|
|
584
|
+
const hunkHeader = hunkIndex === null ? null : parseDiffHunks(review?.patch ?? "")[hunkIndex]?.header ?? null;
|
|
585
|
+
return {
|
|
586
|
+
agent,
|
|
587
|
+
taskId: task?.owner === agent ? task.id : null,
|
|
588
|
+
filePath,
|
|
589
|
+
hunkIndex,
|
|
590
|
+
hunkHeader
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
function reviewNotesForContext(snapshot, context) {
|
|
594
|
+
if (!snapshot || !context) {
|
|
595
|
+
return [];
|
|
596
|
+
}
|
|
597
|
+
return [
|
|
598
|
+
...snapshot.session.reviewNotes
|
|
599
|
+
].filter((note)=>{
|
|
600
|
+
if (note.agent !== context.agent || note.filePath !== context.filePath) {
|
|
601
|
+
return false;
|
|
602
|
+
}
|
|
603
|
+
if (context.hunkIndex !== null) {
|
|
604
|
+
return note.hunkIndex === context.hunkIndex || note.hunkIndex === null;
|
|
605
|
+
}
|
|
606
|
+
return true;
|
|
607
|
+
}).sort((left, right)=>right.createdAt.localeCompare(left.createdAt));
|
|
608
|
+
}
|
|
609
|
+
function syncSelectedReviewNote(snapshot, ui) {
|
|
610
|
+
const notes = reviewNotesForContext(snapshot, activeReviewContext(snapshot, ui));
|
|
611
|
+
ui.selectedReviewNoteId = notes.some((note)=>note.id === ui.selectedReviewNoteId) ? ui.selectedReviewNoteId : notes[0]?.id ?? null;
|
|
612
|
+
}
|
|
613
|
+
function selectedReviewNote(snapshot, ui) {
|
|
614
|
+
const notes = reviewNotesForContext(snapshot, activeReviewContext(snapshot, ui));
|
|
615
|
+
return notes.find((note)=>note.id === ui.selectedReviewNoteId) ?? notes[0] ?? null;
|
|
616
|
+
}
|
|
617
|
+
function renderReviewNotesSection(notes, selectedNoteId, width) {
|
|
618
|
+
if (notes.length === 0) {
|
|
619
|
+
return [
|
|
620
|
+
"- none"
|
|
621
|
+
];
|
|
622
|
+
}
|
|
623
|
+
return notes.flatMap((note)=>{
|
|
624
|
+
const stateLabel = note.landedAt ? `${note.status}+landed` : note.status;
|
|
625
|
+
const prefix = `${note.id === selectedNoteId ? ">" : "-"} [${reviewDispositionLabel(note.disposition)} ${stateLabel}] ${shortTime(note.createdAt)}`;
|
|
626
|
+
const followUps = note.followUpTaskIds.length > 0 ? ` | follow-ups=${note.followUpTaskIds.length}` : "";
|
|
627
|
+
const replies = note.comments.length > 1 ? ` | replies=${note.comments.length - 1}` : "";
|
|
628
|
+
const assignee = ` | assignee=${reviewAssigneeLabel(note.assignee)}`;
|
|
629
|
+
const landed = note.landedAt ? ` | landed=${shortTime(note.landedAt)}` : "";
|
|
630
|
+
const detail = note.body || note.summary;
|
|
631
|
+
return wrapText(`${prefix} ${detail}${assignee}${followUps}${replies}${landed}`, width).map((line)=>toneLine(line, note.status === "resolved" ? "muted" : reviewDispositionTone(note.disposition), note.id === selectedNoteId));
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
function renderSelectedReviewNoteSection(note, width) {
|
|
635
|
+
if (!note) {
|
|
636
|
+
return [
|
|
637
|
+
"- none"
|
|
638
|
+
];
|
|
639
|
+
}
|
|
640
|
+
return [
|
|
641
|
+
...wrapText(`Status: ${note.status} | Disposition: ${reviewDispositionLabel(note.disposition)} | Updated: ${shortTime(note.updatedAt)}`, width),
|
|
642
|
+
...wrapText(`Assignee: ${reviewAssigneeLabel(note.assignee)}`, width),
|
|
643
|
+
...wrapText(`Landed: ${note.landedAt ? shortTime(note.landedAt) : "-"}`, width),
|
|
644
|
+
...wrapText(`Follow-up tasks: ${note.followUpTaskIds.join(", ") || "-"}`, width),
|
|
645
|
+
...note.comments.flatMap((comment, index)=>wrapText(`${index === 0 ? "Root" : `Reply ${index}`}: ${comment.body} (${shortTime(comment.updatedAt)})`, width))
|
|
646
|
+
];
|
|
647
|
+
}
|
|
520
648
|
function latestPendingApproval(approvals) {
|
|
521
649
|
return [
|
|
522
650
|
...approvals
|
|
@@ -610,7 +738,7 @@ function artifactForTask(ui, task) {
|
|
|
610
738
|
function taskDetailTitle(ui) {
|
|
611
739
|
return `Inspector | Task ${ui.taskDetailSection}`;
|
|
612
740
|
}
|
|
613
|
-
function renderTaskInspector(task, artifactEntry, loading, diffEntry, loadingDiff, ui, width, height) {
|
|
741
|
+
function renderTaskInspector(task, artifactEntry, loading, diffEntry, loadingDiff, snapshot, ui, width, height) {
|
|
614
742
|
if (!task) {
|
|
615
743
|
return renderPanel("Inspector", width, height, [
|
|
616
744
|
styleLine("No task selected.", "muted")
|
|
@@ -698,12 +826,14 @@ function renderTaskInspector(task, artifactEntry, loading, diffEntry, loadingDif
|
|
|
698
826
|
const hunks = parseDiffHunks(review.patch);
|
|
699
827
|
const hunkIndex = agent ? selectedHunkIndex(ui, agent, review) : null;
|
|
700
828
|
const selectedHunk = hunkIndex === null ? null : hunks[hunkIndex] ?? null;
|
|
829
|
+
const reviewNotes = reviewNotesForContext(snapshot, activeReviewContext(snapshot, ui));
|
|
701
830
|
lines.push(...section("Review", [
|
|
702
831
|
`Agent: ${review.agent}`,
|
|
703
832
|
`Selected file: ${review.selectedPath ?? "-"}`,
|
|
704
833
|
`Changed files: ${review.changedPaths.length}`,
|
|
705
834
|
`Hunks: ${hunks.length}`,
|
|
706
835
|
`Selected hunk: ${hunkIndex === null ? "-" : `${hunkIndex + 1}/${hunks.length}`}`,
|
|
836
|
+
`Notes: ${reviewNotes.length}`,
|
|
707
837
|
`Stat: ${review.stat}`
|
|
708
838
|
].flatMap((line)=>wrapText(line, innerWidth))));
|
|
709
839
|
lines.push(...section("Changed Files", review.changedPaths.length ? review.changedPaths.flatMap((filePath)=>wrapText(`${filePath === review.selectedPath ? ">" : "-"} ${filePath}`, innerWidth)) : [
|
|
@@ -718,6 +848,8 @@ function renderTaskInspector(task, artifactEntry, loading, diffEntry, loadingDif
|
|
|
718
848
|
lines.push(...section("Patch", review.patch ? wrapPreformatted(review.patch, innerWidth) : [
|
|
719
849
|
"No textual patch available."
|
|
720
850
|
]));
|
|
851
|
+
lines.push(...section("Review Notes", renderReviewNotesSection(reviewNotes, ui.selectedReviewNoteId, innerWidth)));
|
|
852
|
+
lines.push(...section("Selected Review Note", renderSelectedReviewNoteSection(selectedReviewNote(snapshot, ui), innerWidth)));
|
|
721
853
|
} else {
|
|
722
854
|
lines.push(...section("Diff Review", [
|
|
723
855
|
"No diff review available yet."
|
|
@@ -892,10 +1024,12 @@ function renderWorktreeInspector(snapshot, worktree, diffEntry, loadingDiff, ui,
|
|
|
892
1024
|
const hunks = parseDiffHunks(diffEntry.review.patch);
|
|
893
1025
|
const hunkIndex = selectedHunkIndex(ui, worktree.agent, diffEntry.review);
|
|
894
1026
|
const selectedHunk = hunkIndex === null ? null : hunks[hunkIndex] ?? null;
|
|
1027
|
+
const reviewNotes = reviewNotesForContext(snapshot, activeReviewContext(snapshot, ui));
|
|
895
1028
|
lines.push(...section("Review", [
|
|
896
1029
|
`Selected file: ${diffEntry.review.selectedPath ?? "-"}`,
|
|
897
1030
|
`Hunks: ${hunks.length}`,
|
|
898
1031
|
`Selected hunk: ${hunkIndex === null ? "-" : `${hunkIndex + 1}/${hunks.length}`}`,
|
|
1032
|
+
`Notes: ${reviewNotes.length}`,
|
|
899
1033
|
`Stat: ${diffEntry.review.stat}`
|
|
900
1034
|
].flatMap((line)=>wrapText(line, innerWidth))));
|
|
901
1035
|
if (selectedHunk) {
|
|
@@ -907,6 +1041,8 @@ function renderWorktreeInspector(snapshot, worktree, diffEntry, loadingDiff, ui,
|
|
|
907
1041
|
lines.push(...section("Patch", diffEntry.review.patch ? wrapPreformatted(diffEntry.review.patch, innerWidth) : [
|
|
908
1042
|
"No textual patch available."
|
|
909
1043
|
]));
|
|
1044
|
+
lines.push(...section("Review Notes", renderReviewNotesSection(reviewNotes, ui.selectedReviewNoteId, innerWidth)));
|
|
1045
|
+
lines.push(...section("Selected Review Note", renderSelectedReviewNoteSection(selectedReviewNote(snapshot, ui), innerWidth)));
|
|
910
1046
|
} else {
|
|
911
1047
|
lines.push(...section("Diff Review", [
|
|
912
1048
|
"No diff review available yet."
|
|
@@ -921,7 +1057,7 @@ function renderWorktreeInspector(snapshot, worktree, diffEntry, loadingDiff, ui,
|
|
|
921
1057
|
function renderInspector(snapshot, ui, width, height) {
|
|
922
1058
|
switch(ui.activeTab){
|
|
923
1059
|
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);
|
|
1060
|
+
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
1061
|
case "approvals":
|
|
926
1062
|
return renderApprovalInspector(selectedApproval(snapshot, ui), width, height);
|
|
927
1063
|
case "claims":
|
|
@@ -965,7 +1101,8 @@ function renderLane(snapshot, agent, width, height) {
|
|
|
965
1101
|
`Worktree: ${worktree ? path.basename(worktree.path) : "-"}`,
|
|
966
1102
|
`Branch: ${worktree?.branch ?? "-"}`,
|
|
967
1103
|
`Pending approvals: ${approvals.length}`,
|
|
968
|
-
`Changed paths: ${diff?.paths.length ?? 0}
|
|
1104
|
+
`Changed paths: ${diff?.paths.length ?? 0}`,
|
|
1105
|
+
`Open reviews: ${countOpenReviewNotes(snapshot, agent)}`
|
|
969
1106
|
].flatMap((line)=>wrapText(line, innerWidth))),
|
|
970
1107
|
...section("Summary", wrapText(status.summary ?? "No summary yet.", innerWidth)),
|
|
971
1108
|
...section("Tasks", tasks.length ? tasks.slice(0, 4).flatMap((task)=>wrapText(`- [${task.status}] ${task.title}`, innerWidth)) : [
|
|
@@ -986,7 +1123,7 @@ function renderHeader(view, ui, width) {
|
|
|
986
1123
|
const repoName = path.basename(session?.repoRoot ?? process.cwd());
|
|
987
1124
|
const line1 = fitAnsiLine(`${toneForPanel("Kavi Operator", true)} | session=${session?.id ?? "-"} | repo=${repoName} | rpc=${view.connected ? "connected" : "disconnected"}`, width);
|
|
988
1125
|
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);
|
|
1126
|
+
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
1127
|
const tabs = OPERATOR_TABS.map((tab, index)=>{
|
|
991
1128
|
const count = buildTabItems(snapshot, tab).length;
|
|
992
1129
|
const label = `[${index + 1}] ${tabLabel(tab)} ${count}`;
|
|
@@ -1015,6 +1152,19 @@ function footerSelectionSummary(snapshot, ui, width) {
|
|
|
1015
1152
|
}
|
|
1016
1153
|
function renderFooter(snapshot, ui, width) {
|
|
1017
1154
|
const toast = currentToast(ui);
|
|
1155
|
+
const reviewContext = activeReviewContext(snapshot, ui);
|
|
1156
|
+
if (ui.reviewComposer) {
|
|
1157
|
+
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);
|
|
1158
|
+
const composerLine = fitAnsiLine(`Disposition: ${reviewDispositionLabel(ui.reviewComposer.disposition)} | Enter save | Esc cancel | Ctrl+U clear`, width);
|
|
1159
|
+
const scopeLine = fitAnsiLine(`Scope: ${reviewContext?.agent ?? "-"} | ${reviewContext?.filePath ?? "-"}${reviewContext?.hunkIndex === null || reviewContext?.hunkIndex === undefined ? "" : ` | hunk ${reviewContext.hunkIndex + 1}`}`, width);
|
|
1160
|
+
const promptLine = fitAnsiLine(`> ${ui.reviewComposer.body}`, width);
|
|
1161
|
+
return [
|
|
1162
|
+
composerHeader,
|
|
1163
|
+
composerLine,
|
|
1164
|
+
scopeLine,
|
|
1165
|
+
promptLine
|
|
1166
|
+
];
|
|
1167
|
+
}
|
|
1018
1168
|
if (ui.composer) {
|
|
1019
1169
|
const composerHeader = fitAnsiLine(styleLine("Compose Task", "accent", "strong"), width);
|
|
1020
1170
|
const composerLine = fitAnsiLine(`Route: ${ui.composer.owner} | 1 auto 2 codex 3 claude | Enter submit | Esc cancel | Ctrl+U clear`, width);
|
|
@@ -1028,7 +1178,7 @@ function renderFooter(snapshot, ui, width) {
|
|
|
1028
1178
|
}
|
|
1029
1179
|
return [
|
|
1030
1180
|
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),
|
|
1181
|
+
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 | a cycle assignee | w won't fix | x accepted risk | F fix task | H handoff | g/G top/bottom | s stop daemon | q quit", width),
|
|
1032
1182
|
footerSelectionSummary(snapshot, ui, width),
|
|
1033
1183
|
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
1184
|
];
|
|
@@ -1187,6 +1337,104 @@ async function resolveApprovalSelection(paths, snapshot, ui, decision, remember)
|
|
|
1187
1337
|
});
|
|
1188
1338
|
setToast(ui, "info", `${decision === "allow" ? "Approved" : "Denied"} ${approval.toolName}${remember ? " with remembered rule" : ""}.`);
|
|
1189
1339
|
}
|
|
1340
|
+
async function submitReviewNote(paths, view, ui) {
|
|
1341
|
+
const composer = ui.reviewComposer;
|
|
1342
|
+
const context = activeReviewContext(view.snapshot, ui);
|
|
1343
|
+
if (!composer || !context) {
|
|
1344
|
+
throw new Error("No active diff review context is available.");
|
|
1345
|
+
}
|
|
1346
|
+
const body = composer.body.trim();
|
|
1347
|
+
if (!body) {
|
|
1348
|
+
throw new Error("Review note cannot be empty.");
|
|
1349
|
+
}
|
|
1350
|
+
if (composer.mode === "edit" && composer.noteId) {
|
|
1351
|
+
await rpcUpdateReviewNote(paths, {
|
|
1352
|
+
noteId: composer.noteId,
|
|
1353
|
+
body
|
|
1354
|
+
});
|
|
1355
|
+
} else if (composer.mode === "reply" && composer.noteId) {
|
|
1356
|
+
await rpcAddReviewReply(paths, {
|
|
1357
|
+
noteId: composer.noteId,
|
|
1358
|
+
body
|
|
1359
|
+
});
|
|
1360
|
+
} else {
|
|
1361
|
+
await rpcAddReviewNote(paths, {
|
|
1362
|
+
agent: context.agent,
|
|
1363
|
+
taskId: context.taskId,
|
|
1364
|
+
filePath: context.filePath,
|
|
1365
|
+
hunkIndex: context.hunkIndex,
|
|
1366
|
+
hunkHeader: context.hunkHeader,
|
|
1367
|
+
disposition: composer.disposition,
|
|
1368
|
+
body
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
ui.reviewComposer = null;
|
|
1372
|
+
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}`}.`);
|
|
1373
|
+
}
|
|
1374
|
+
async function toggleSelectedReviewNoteStatus(paths, snapshot, ui) {
|
|
1375
|
+
const note = selectedReviewNote(snapshot, ui);
|
|
1376
|
+
if (!note) {
|
|
1377
|
+
throw new Error("No review note is selected.");
|
|
1378
|
+
}
|
|
1379
|
+
const status = note.status === "resolved" ? "open" : "resolved";
|
|
1380
|
+
await rpcSetReviewNoteStatus(paths, {
|
|
1381
|
+
noteId: note.id,
|
|
1382
|
+
status
|
|
1383
|
+
});
|
|
1384
|
+
setToast(ui, "info", `${status === "resolved" ? "Resolved" : "Reopened"} review note ${note.id}.`);
|
|
1385
|
+
}
|
|
1386
|
+
async function enqueueSelectedReviewFollowUp(paths, snapshot, ui, mode) {
|
|
1387
|
+
const note = selectedReviewNote(snapshot, ui);
|
|
1388
|
+
if (!note) {
|
|
1389
|
+
throw new Error("No review note is selected.");
|
|
1390
|
+
}
|
|
1391
|
+
const owner = mode === "fix" ? note.agent : note.agent === "codex" ? "claude" : "codex";
|
|
1392
|
+
await rpcEnqueueReviewFollowUp(paths, {
|
|
1393
|
+
noteId: note.id,
|
|
1394
|
+
owner,
|
|
1395
|
+
mode
|
|
1396
|
+
});
|
|
1397
|
+
setToast(ui, "info", `Queued ${mode === "fix" ? "fix" : "handoff"} follow-up for review note ${note.id} to ${owner}.`);
|
|
1398
|
+
}
|
|
1399
|
+
async function cycleSelectedReviewNoteAssignee(paths, snapshot, ui) {
|
|
1400
|
+
const note = selectedReviewNote(snapshot, ui);
|
|
1401
|
+
if (!note) {
|
|
1402
|
+
throw new Error("No review note is selected.");
|
|
1403
|
+
}
|
|
1404
|
+
const assignee = cycleReviewAssignee(note.assignee, note.agent);
|
|
1405
|
+
await rpcUpdateReviewNote(paths, {
|
|
1406
|
+
noteId: note.id,
|
|
1407
|
+
assignee
|
|
1408
|
+
});
|
|
1409
|
+
setToast(ui, "info", `Assigned review note ${note.id} to ${reviewAssigneeLabel(assignee)}.`);
|
|
1410
|
+
}
|
|
1411
|
+
async function resolveSelectedReviewNoteWithDisposition(paths, snapshot, ui, disposition) {
|
|
1412
|
+
const note = selectedReviewNote(snapshot, ui);
|
|
1413
|
+
if (!note) {
|
|
1414
|
+
throw new Error("No review note is selected.");
|
|
1415
|
+
}
|
|
1416
|
+
await rpcUpdateReviewNote(paths, {
|
|
1417
|
+
noteId: note.id,
|
|
1418
|
+
disposition,
|
|
1419
|
+
assignee: "operator"
|
|
1420
|
+
});
|
|
1421
|
+
await rpcSetReviewNoteStatus(paths, {
|
|
1422
|
+
noteId: note.id,
|
|
1423
|
+
status: "resolved"
|
|
1424
|
+
});
|
|
1425
|
+
setToast(ui, "info", `Marked review note ${note.id} as ${reviewDispositionLabel(disposition).toLowerCase()}.`);
|
|
1426
|
+
}
|
|
1427
|
+
function cycleSelectedReviewNote(snapshot, ui, delta) {
|
|
1428
|
+
const notes = reviewNotesForContext(snapshot, activeReviewContext(snapshot, ui));
|
|
1429
|
+
if (notes.length === 0) {
|
|
1430
|
+
ui.selectedReviewNoteId = null;
|
|
1431
|
+
return false;
|
|
1432
|
+
}
|
|
1433
|
+
const currentIndex = Math.max(0, notes.findIndex((note)=>note.id === ui.selectedReviewNoteId));
|
|
1434
|
+
const nextIndex = (currentIndex + delta + notes.length) % notes.length;
|
|
1435
|
+
ui.selectedReviewNoteId = notes[nextIndex]?.id ?? notes[0]?.id ?? null;
|
|
1436
|
+
return true;
|
|
1437
|
+
}
|
|
1190
1438
|
async function ensureSelectedTaskArtifact(paths, view, ui, render) {
|
|
1191
1439
|
if (!view.connected || ui.activeTab !== "tasks") {
|
|
1192
1440
|
return;
|
|
@@ -1261,6 +1509,7 @@ async function ensureSelectedDiffReview(paths, view, ui, render) {
|
|
|
1261
1509
|
};
|
|
1262
1510
|
} finally{
|
|
1263
1511
|
ui.loadingDiffReviews[agent] = false;
|
|
1512
|
+
syncSelectedReviewNote(view.snapshot, ui);
|
|
1264
1513
|
render();
|
|
1265
1514
|
}
|
|
1266
1515
|
}
|
|
@@ -1320,6 +1569,7 @@ export async function attachTui(paths) {
|
|
|
1320
1569
|
selectedIds: emptySelectionMap(),
|
|
1321
1570
|
taskDetailSection: "overview",
|
|
1322
1571
|
composer: null,
|
|
1572
|
+
reviewComposer: null,
|
|
1323
1573
|
toast: null,
|
|
1324
1574
|
artifacts: {},
|
|
1325
1575
|
loadingArtifacts: {},
|
|
@@ -1338,7 +1588,8 @@ export async function attachTui(paths) {
|
|
|
1338
1588
|
hunkSelections: {
|
|
1339
1589
|
codex: 0,
|
|
1340
1590
|
claude: 0
|
|
1341
|
-
}
|
|
1591
|
+
},
|
|
1592
|
+
selectedReviewNoteId: null
|
|
1342
1593
|
};
|
|
1343
1594
|
const render = ()=>{
|
|
1344
1595
|
process.stdout.write(renderScreen(view, ui, paths));
|
|
@@ -1346,6 +1597,7 @@ export async function attachTui(paths) {
|
|
|
1346
1597
|
const syncUiForSnapshot = (snapshot)=>{
|
|
1347
1598
|
ui.selectedIds = syncSelections(ui.selectedIds, snapshot);
|
|
1348
1599
|
ui.diffSelections = syncDiffSelections(ui.diffSelections, snapshot, ui.activeTab === "tasks" ? selectedTask(snapshot, ui) : null);
|
|
1600
|
+
syncSelectedReviewNote(snapshot, ui);
|
|
1349
1601
|
};
|
|
1350
1602
|
const applySnapshot = (snapshot, reason)=>{
|
|
1351
1603
|
view.snapshot = snapshot;
|
|
@@ -1476,6 +1728,7 @@ export async function attachTui(paths) {
|
|
|
1476
1728
|
const items = buildTabItems(view.snapshot, ui.activeTab);
|
|
1477
1729
|
ui.selectedIds[ui.activeTab] = moveSelectionId(items, ui.selectedIds[ui.activeTab], delta);
|
|
1478
1730
|
ui.diffSelections = syncDiffSelections(ui.diffSelections, view.snapshot, ui.activeTab === "tasks" ? selectedTask(view.snapshot, ui) : null);
|
|
1731
|
+
syncSelectedReviewNote(view.snapshot, ui);
|
|
1479
1732
|
render();
|
|
1480
1733
|
void ensureSelectedTaskArtifact(paths, view, ui, render);
|
|
1481
1734
|
void ensureSelectedDiffReview(paths, view, ui, render);
|
|
@@ -1484,6 +1737,37 @@ export async function attachTui(paths) {
|
|
|
1484
1737
|
if (closed) {
|
|
1485
1738
|
return;
|
|
1486
1739
|
}
|
|
1740
|
+
if (ui.reviewComposer) {
|
|
1741
|
+
runAction(async ()=>{
|
|
1742
|
+
if (key.name === "escape") {
|
|
1743
|
+
ui.reviewComposer = null;
|
|
1744
|
+
setToast(ui, "info", "Review note capture cancelled.");
|
|
1745
|
+
render();
|
|
1746
|
+
return;
|
|
1747
|
+
}
|
|
1748
|
+
if (key.ctrl && key.name === "u") {
|
|
1749
|
+
ui.reviewComposer.body = "";
|
|
1750
|
+
render();
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
if (key.name === "backspace") {
|
|
1754
|
+
ui.reviewComposer.body = ui.reviewComposer.body.slice(0, -1);
|
|
1755
|
+
render();
|
|
1756
|
+
return;
|
|
1757
|
+
}
|
|
1758
|
+
if (key.name === "return") {
|
|
1759
|
+
await submitReviewNote(paths, view, ui);
|
|
1760
|
+
await refresh();
|
|
1761
|
+
render();
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
if (input.length === 1 && !key.ctrl && !key.meta) {
|
|
1765
|
+
ui.reviewComposer.body += input;
|
|
1766
|
+
render();
|
|
1767
|
+
}
|
|
1768
|
+
});
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1487
1771
|
if (ui.composer) {
|
|
1488
1772
|
runAction(async ()=>{
|
|
1489
1773
|
if (key.name === "escape") {
|
|
@@ -1507,6 +1791,7 @@ export async function attachTui(paths) {
|
|
|
1507
1791
|
await refresh();
|
|
1508
1792
|
ui.selectedIds.tasks = buildTabItems(view.snapshot, "tasks")[0]?.id ?? null;
|
|
1509
1793
|
ui.diffSelections = syncDiffSelections(ui.diffSelections, view.snapshot, selectedTask(view.snapshot, ui));
|
|
1794
|
+
syncSelectedReviewNote(view.snapshot, ui);
|
|
1510
1795
|
render();
|
|
1511
1796
|
void ensureSelectedTaskArtifact(paths, view, ui, render);
|
|
1512
1797
|
void ensureSelectedDiffReview(paths, view, ui, render);
|
|
@@ -1539,7 +1824,7 @@ export async function attachTui(paths) {
|
|
|
1539
1824
|
});
|
|
1540
1825
|
return;
|
|
1541
1826
|
}
|
|
1542
|
-
if (
|
|
1827
|
+
if (input === "q" || key.ctrl && key.name === "c") {
|
|
1543
1828
|
close();
|
|
1544
1829
|
return;
|
|
1545
1830
|
}
|
|
@@ -1574,6 +1859,7 @@ export async function attachTui(paths) {
|
|
|
1574
1859
|
const items = buildTabItems(view.snapshot, ui.activeTab);
|
|
1575
1860
|
ui.selectedIds[ui.activeTab] = items[0]?.id ?? null;
|
|
1576
1861
|
ui.diffSelections = syncDiffSelections(ui.diffSelections, view.snapshot, ui.activeTab === "tasks" ? selectedTask(view.snapshot, ui) : null);
|
|
1862
|
+
syncSelectedReviewNote(view.snapshot, ui);
|
|
1577
1863
|
render();
|
|
1578
1864
|
void ensureSelectedTaskArtifact(paths, view, ui, render);
|
|
1579
1865
|
void ensureSelectedDiffReview(paths, view, ui, render);
|
|
@@ -1583,6 +1869,7 @@ export async function attachTui(paths) {
|
|
|
1583
1869
|
const items = buildTabItems(view.snapshot, ui.activeTab);
|
|
1584
1870
|
ui.selectedIds[ui.activeTab] = items.at(-1)?.id ?? null;
|
|
1585
1871
|
ui.diffSelections = syncDiffSelections(ui.diffSelections, view.snapshot, ui.activeTab === "tasks" ? selectedTask(view.snapshot, ui) : null);
|
|
1872
|
+
syncSelectedReviewNote(view.snapshot, ui);
|
|
1586
1873
|
render();
|
|
1587
1874
|
void ensureSelectedTaskArtifact(paths, view, ui, render);
|
|
1588
1875
|
void ensureSelectedDiffReview(paths, view, ui, render);
|
|
@@ -1597,6 +1884,7 @@ export async function attachTui(paths) {
|
|
|
1597
1884
|
const nextIndex = (currentIndex + delta + TASK_DETAIL_SECTIONS.length) % TASK_DETAIL_SECTIONS.length;
|
|
1598
1885
|
ui.taskDetailSection = TASK_DETAIL_SECTIONS[nextIndex] ?? ui.taskDetailSection;
|
|
1599
1886
|
ui.diffSelections = syncDiffSelections(ui.diffSelections, view.snapshot, selectedTask(view.snapshot, ui));
|
|
1887
|
+
syncSelectedReviewNote(view.snapshot, ui);
|
|
1600
1888
|
render();
|
|
1601
1889
|
void ensureSelectedTaskArtifact(paths, view, ui, render);
|
|
1602
1890
|
void ensureSelectedDiffReview(paths, view, ui, render);
|
|
@@ -1628,12 +1916,95 @@ export async function attachTui(paths) {
|
|
|
1628
1916
|
});
|
|
1629
1917
|
return;
|
|
1630
1918
|
}
|
|
1919
|
+
if (input === "A" || input === "C" || input === "Q" || input === "M") {
|
|
1920
|
+
if (!activeReviewContext(view.snapshot, ui)) {
|
|
1921
|
+
setToast(ui, "error", "No active diff review context is selected.");
|
|
1922
|
+
render();
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
ui.reviewComposer = {
|
|
1926
|
+
mode: "create",
|
|
1927
|
+
disposition: input === "A" ? "approve" : input === "C" ? "concern" : input === "Q" ? "question" : "note",
|
|
1928
|
+
noteId: null,
|
|
1929
|
+
body: ""
|
|
1930
|
+
};
|
|
1931
|
+
render();
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
if (input === "o" || input === "O") {
|
|
1935
|
+
if (!cycleSelectedReviewNote(view.snapshot, ui, input === "O" ? -1 : 1)) {
|
|
1936
|
+
setToast(ui, "error", "No review notes are available in the current diff context.");
|
|
1937
|
+
}
|
|
1938
|
+
render();
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
if (input === "E") {
|
|
1942
|
+
const note = selectedReviewNote(view.snapshot, ui);
|
|
1943
|
+
if (!note) {
|
|
1944
|
+
setToast(ui, "error", "No review note is selected.");
|
|
1945
|
+
render();
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
ui.reviewComposer = {
|
|
1949
|
+
mode: "edit",
|
|
1950
|
+
disposition: note.disposition,
|
|
1951
|
+
noteId: note.id,
|
|
1952
|
+
body: note.body
|
|
1953
|
+
};
|
|
1954
|
+
render();
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
if (input === "T") {
|
|
1958
|
+
const note = selectedReviewNote(view.snapshot, ui);
|
|
1959
|
+
if (!note) {
|
|
1960
|
+
setToast(ui, "error", "No review note is selected.");
|
|
1961
|
+
render();
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
ui.reviewComposer = {
|
|
1965
|
+
mode: "reply",
|
|
1966
|
+
disposition: note.disposition,
|
|
1967
|
+
noteId: note.id,
|
|
1968
|
+
body: ""
|
|
1969
|
+
};
|
|
1970
|
+
render();
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
if (input === "R") {
|
|
1974
|
+
runAction(async ()=>{
|
|
1975
|
+
await toggleSelectedReviewNoteStatus(paths, view.snapshot, ui);
|
|
1976
|
+
await refresh();
|
|
1977
|
+
});
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
if (input === "a") {
|
|
1981
|
+
runAction(async ()=>{
|
|
1982
|
+
await cycleSelectedReviewNoteAssignee(paths, view.snapshot, ui);
|
|
1983
|
+
await refresh();
|
|
1984
|
+
});
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
if (input === "w" || input === "x") {
|
|
1988
|
+
runAction(async ()=>{
|
|
1989
|
+
await resolveSelectedReviewNoteWithDisposition(paths, view.snapshot, ui, input === "w" ? "wont_fix" : "accepted_risk");
|
|
1990
|
+
await refresh();
|
|
1991
|
+
});
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
if (input === "F" || input === "H") {
|
|
1995
|
+
runAction(async ()=>{
|
|
1996
|
+
await enqueueSelectedReviewFollowUp(paths, view.snapshot, ui, input === "F" ? "fix" : "handoff");
|
|
1997
|
+
await refresh();
|
|
1998
|
+
});
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
1631
2001
|
if (input === "," || input === ".") {
|
|
1632
2002
|
const agent = cycleDiffSelection(view.snapshot, ui, input === "," ? -1 : 1);
|
|
1633
2003
|
if (!agent) {
|
|
1634
2004
|
return;
|
|
1635
2005
|
}
|
|
1636
2006
|
render();
|
|
2007
|
+
syncSelectedReviewNote(view.snapshot, ui);
|
|
1637
2008
|
void ensureSelectedDiffReview(paths, view, ui, render);
|
|
1638
2009
|
return;
|
|
1639
2010
|
}
|
|
@@ -1643,12 +2014,14 @@ export async function attachTui(paths) {
|
|
|
1643
2014
|
return;
|
|
1644
2015
|
}
|
|
1645
2016
|
render();
|
|
2017
|
+
syncSelectedReviewNote(view.snapshot, ui);
|
|
1646
2018
|
return;
|
|
1647
2019
|
}
|
|
1648
2020
|
if (key.name === "return" && ui.activeTab === "tasks") {
|
|
1649
2021
|
const currentIndex = TASK_DETAIL_SECTIONS.indexOf(ui.taskDetailSection);
|
|
1650
2022
|
ui.taskDetailSection = TASK_DETAIL_SECTIONS[(currentIndex + 1) % TASK_DETAIL_SECTIONS.length] ?? ui.taskDetailSection;
|
|
1651
2023
|
ui.diffSelections = syncDiffSelections(ui.diffSelections, view.snapshot, selectedTask(view.snapshot, ui));
|
|
2024
|
+
syncSelectedReviewNote(view.snapshot, ui);
|
|
1652
2025
|
render();
|
|
1653
2026
|
void ensureSelectedTaskArtifact(paths, view, ui, render);
|
|
1654
2027
|
void ensureSelectedDiffReview(paths, view, ui, render);
|
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandipadk7/kavi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Managed Codex + Claude collaboration TUI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"preferGlobal": true,
|
|
7
|
+
"homepage": "https://www.npmjs.com/package/@mandipadk7/kavi",
|
|
7
8
|
"publishConfig": {
|
|
8
9
|
"access": "public"
|
|
9
10
|
},
|