@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/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 (key.name === "q" || key.ctrl && key.name === "c") {
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.2",
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
  },