@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/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 (key.name === "q" || key.ctrl && key.name === "c") {
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandipadk7/kavi",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Managed Codex + Claude collaboration TUI",
5
5
  "type": "module",
6
6
  "preferGlobal": true,