mini-coder 0.5.7 → 0.5.9
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 +5 -2
- package/package.json +1 -1
- package/src/agent.ts +206 -60
- package/src/cli.ts +11 -2
- package/src/headless.ts +185 -47
- package/src/index.ts +78 -10
- package/src/input.ts +1 -0
- package/src/prompt.ts +8 -5
- package/src/session.ts +56 -8
- package/src/submit.ts +0 -5
- package/src/tools.ts +243 -3
- package/src/ui/commands.test.ts +71 -0
- package/src/ui/commands.ts +13 -0
- package/src/ui/conversation.test.ts +91 -0
- package/src/ui/conversation.ts +122 -3
- package/src/ui/help.test.ts +1 -0
- package/src/ui/help.ts +1 -0
- package/src/ui.ts +228 -25
package/src/ui/commands.test.ts
CHANGED
|
@@ -122,6 +122,7 @@ describe("ui/commands", () => {
|
|
|
122
122
|
inputValue = value;
|
|
123
123
|
},
|
|
124
124
|
appendInfoMessage: () => {},
|
|
125
|
+
appendTodoMessage: () => {},
|
|
125
126
|
scrollConversationToBottom: () => {},
|
|
126
127
|
render: () => {},
|
|
127
128
|
reloadPromptContext: async () => {},
|
|
@@ -161,6 +162,7 @@ describe("ui/commands", () => {
|
|
|
161
162
|
inputValue = value;
|
|
162
163
|
},
|
|
163
164
|
appendInfoMessage: () => {},
|
|
165
|
+
appendTodoMessage: () => {},
|
|
164
166
|
scrollConversationToBottom: () => {},
|
|
165
167
|
render: () => {},
|
|
166
168
|
reloadPromptContext: async () => {},
|
|
@@ -206,6 +208,7 @@ describe("ui/commands", () => {
|
|
|
206
208
|
inputValue = value;
|
|
207
209
|
},
|
|
208
210
|
appendInfoMessage: () => {},
|
|
211
|
+
appendTodoMessage: () => {},
|
|
209
212
|
scrollConversationToBottom: () => {},
|
|
210
213
|
render: () => {},
|
|
211
214
|
reloadPromptContext: async () => {},
|
|
@@ -244,6 +247,7 @@ describe("ui/commands", () => {
|
|
|
244
247
|
},
|
|
245
248
|
setInputValue: () => {},
|
|
246
249
|
appendInfoMessage: () => {},
|
|
250
|
+
appendTodoMessage: () => {},
|
|
247
251
|
scrollConversationToBottom: () => {},
|
|
248
252
|
render: () => {},
|
|
249
253
|
reloadPromptContext: async () => {},
|
|
@@ -310,6 +314,7 @@ describe("ui/commands", () => {
|
|
|
310
314
|
},
|
|
311
315
|
setInputValue: () => {},
|
|
312
316
|
appendInfoMessage: () => {},
|
|
317
|
+
appendTodoMessage: () => {},
|
|
313
318
|
scrollConversationToBottom: () => {
|
|
314
319
|
scrollCalls += 1;
|
|
315
320
|
},
|
|
@@ -393,6 +398,7 @@ describe("ui/commands", () => {
|
|
|
393
398
|
dismissOverlay: () => {},
|
|
394
399
|
setInputValue: () => {},
|
|
395
400
|
appendInfoMessage: () => {},
|
|
401
|
+
appendTodoMessage: () => {},
|
|
396
402
|
scrollConversationToBottom: () => {},
|
|
397
403
|
render: () => {},
|
|
398
404
|
reloadPromptContext: async () => {},
|
|
@@ -415,6 +421,7 @@ describe("ui/commands", () => {
|
|
|
415
421
|
dismissOverlay: () => {},
|
|
416
422
|
setInputValue: () => {},
|
|
417
423
|
appendInfoMessage: () => {},
|
|
424
|
+
appendTodoMessage: () => {},
|
|
418
425
|
scrollConversationToBottom: () => {},
|
|
419
426
|
render: () => {},
|
|
420
427
|
reloadPromptContext: async (nextState) => {
|
|
@@ -470,6 +477,7 @@ describe("ui/commands", () => {
|
|
|
470
477
|
dismissOverlay: () => {},
|
|
471
478
|
setInputValue: () => {},
|
|
472
479
|
appendInfoMessage: () => {},
|
|
480
|
+
appendTodoMessage: () => {},
|
|
473
481
|
scrollConversationToBottom: () => {},
|
|
474
482
|
render: () => {},
|
|
475
483
|
reloadPromptContext: async () => {},
|
|
@@ -495,6 +503,7 @@ describe("ui/commands", () => {
|
|
|
495
503
|
dismissOverlay: () => {},
|
|
496
504
|
setInputValue: () => {},
|
|
497
505
|
appendInfoMessage: () => {},
|
|
506
|
+
appendTodoMessage: () => {},
|
|
498
507
|
scrollConversationToBottom: () => {},
|
|
499
508
|
render: () => {},
|
|
500
509
|
reloadPromptContext: async () => {},
|
|
@@ -511,6 +520,66 @@ describe("ui/commands", () => {
|
|
|
511
520
|
}
|
|
512
521
|
});
|
|
513
522
|
|
|
523
|
+
test("/todo appends the current todo list without creating a session", () => {
|
|
524
|
+
const state = createTestState();
|
|
525
|
+
state.messages = [
|
|
526
|
+
{
|
|
527
|
+
role: "toolResult",
|
|
528
|
+
toolCallId: "todo-1",
|
|
529
|
+
toolName: "todoWrite",
|
|
530
|
+
content: [
|
|
531
|
+
{
|
|
532
|
+
type: "text",
|
|
533
|
+
text: JSON.stringify({
|
|
534
|
+
todos: [
|
|
535
|
+
{ content: "Review prompt wording", status: "completed" },
|
|
536
|
+
{ content: "Implement todo tools", status: "in_progress" },
|
|
537
|
+
],
|
|
538
|
+
}),
|
|
539
|
+
},
|
|
540
|
+
],
|
|
541
|
+
isError: false,
|
|
542
|
+
timestamp: 1,
|
|
543
|
+
},
|
|
544
|
+
];
|
|
545
|
+
const appended: Array<{
|
|
546
|
+
todos: Array<{ content: string; status: string }>;
|
|
547
|
+
sessionId: string | null;
|
|
548
|
+
}> = [];
|
|
549
|
+
const controller = createCommandController({
|
|
550
|
+
openOverlay: () => {},
|
|
551
|
+
dismissOverlay: () => {},
|
|
552
|
+
setInputValue: () => {},
|
|
553
|
+
appendInfoMessage: () => {},
|
|
554
|
+
appendTodoMessage: (todos, nextState) => {
|
|
555
|
+
appended.push({
|
|
556
|
+
todos: todos.map((todo) => ({ ...todo })),
|
|
557
|
+
sessionId: nextState.session?.id ?? null,
|
|
558
|
+
});
|
|
559
|
+
},
|
|
560
|
+
scrollConversationToBottom: () => {},
|
|
561
|
+
render: () => {},
|
|
562
|
+
reloadPromptContext: async () => {},
|
|
563
|
+
openInBrowser: () => {},
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
try {
|
|
567
|
+
expect(controller.handleCommand("todo", state)).toBe(true);
|
|
568
|
+
expect(appended).toEqual([
|
|
569
|
+
{
|
|
570
|
+
todos: [
|
|
571
|
+
{ content: "Review prompt wording", status: "completed" },
|
|
572
|
+
{ content: "Implement todo tools", status: "in_progress" },
|
|
573
|
+
],
|
|
574
|
+
sessionId: null,
|
|
575
|
+
},
|
|
576
|
+
]);
|
|
577
|
+
expect(state.session).toBeNull();
|
|
578
|
+
} finally {
|
|
579
|
+
state.db.close();
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
|
|
514
583
|
test("applyModelSelection updates the state and persists the default model", () => {
|
|
515
584
|
const faux = registerFauxProvider();
|
|
516
585
|
const state = createTestState();
|
|
@@ -520,6 +589,7 @@ describe("ui/commands", () => {
|
|
|
520
589
|
dismissOverlay: () => {},
|
|
521
590
|
setInputValue: () => {},
|
|
522
591
|
appendInfoMessage: () => {},
|
|
592
|
+
appendTodoMessage: () => {},
|
|
523
593
|
scrollConversationToBottom: () => {},
|
|
524
594
|
render: () => {},
|
|
525
595
|
reloadPromptContext: async () => {},
|
|
@@ -547,6 +617,7 @@ describe("ui/commands", () => {
|
|
|
547
617
|
dismissOverlay: () => {},
|
|
548
618
|
setInputValue: () => {},
|
|
549
619
|
appendInfoMessage: () => {},
|
|
620
|
+
appendTodoMessage: () => {},
|
|
550
621
|
scrollConversationToBottom: () => {},
|
|
551
622
|
render: () => {},
|
|
552
623
|
reloadPromptContext: async () => {},
|
package/src/ui/commands.ts
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
undoLastTurn,
|
|
30
30
|
} from "../session.ts";
|
|
31
31
|
import { updateSettings } from "../settings.ts";
|
|
32
|
+
import { getTodoItems } from "../tools.ts";
|
|
32
33
|
import { buildHelpText, COMMAND_DESCRIPTIONS } from "./help.ts";
|
|
33
34
|
import { type ActiveOverlay, OVERLAY_MAX_VISIBLE } from "./overlay.ts";
|
|
34
35
|
import { abbreviatePath } from "./status.ts";
|
|
@@ -51,6 +52,11 @@ interface UiCommandRuntime {
|
|
|
51
52
|
setInputValue: (value: string) => void;
|
|
52
53
|
/** Append a UI-only info message to the conversation log. */
|
|
53
54
|
appendInfoMessage: (text: string, state: AppState) => void;
|
|
55
|
+
/** Append a UI-only todo snapshot to the conversation log. */
|
|
56
|
+
appendTodoMessage: (
|
|
57
|
+
todos: ReturnType<typeof getTodoItems>,
|
|
58
|
+
state: AppState,
|
|
59
|
+
) => void;
|
|
54
60
|
/** Re-enable stick-to-bottom behavior for the conversation log. */
|
|
55
61
|
scrollConversationToBottom: () => void;
|
|
56
62
|
/** Trigger a UI re-render. */
|
|
@@ -580,6 +586,10 @@ export function createCommandController(
|
|
|
580
586
|
runtime.appendInfoMessage(buildHelpText(state), state);
|
|
581
587
|
};
|
|
582
588
|
|
|
589
|
+
const handleTodoCommand = (state: AppState): void => {
|
|
590
|
+
runtime.appendTodoMessage(getTodoItems(state.messages), state);
|
|
591
|
+
};
|
|
592
|
+
|
|
583
593
|
const handleCommand = (command: string, state: AppState): boolean => {
|
|
584
594
|
switch (command) {
|
|
585
595
|
case "model":
|
|
@@ -622,6 +632,9 @@ export function createCommandController(
|
|
|
622
632
|
case "verbose":
|
|
623
633
|
handleVerboseCommand(state);
|
|
624
634
|
return true;
|
|
635
|
+
case "todo":
|
|
636
|
+
handleTodoCommand(state);
|
|
637
|
+
return true;
|
|
625
638
|
case "help":
|
|
626
639
|
handleHelpCommand(state);
|
|
627
640
|
return true;
|
|
@@ -1057,6 +1057,33 @@ describe("ui/conversation", () => {
|
|
|
1057
1057
|
expect(text).not.toContain("{");
|
|
1058
1058
|
});
|
|
1059
1059
|
|
|
1060
|
+
test("renderAssistantMessage for a todoWrite tool call shows a todo-count summary instead of raw JSON", () => {
|
|
1061
|
+
// Arrange
|
|
1062
|
+
const assistant = {
|
|
1063
|
+
content: [
|
|
1064
|
+
fauxToolCall(
|
|
1065
|
+
"todoWrite",
|
|
1066
|
+
{
|
|
1067
|
+
todos: [
|
|
1068
|
+
{ content: "Inspect headless JSON output", status: "completed" },
|
|
1069
|
+
{ content: "Update the TUI preview", status: "in_progress" },
|
|
1070
|
+
],
|
|
1071
|
+
},
|
|
1072
|
+
{ id: "tool-1" },
|
|
1073
|
+
),
|
|
1074
|
+
],
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
// Act
|
|
1078
|
+
const text = collectText(renderAssistantMessage(assistant, RENDER_OPTS));
|
|
1079
|
+
|
|
1080
|
+
// Assert
|
|
1081
|
+
expect(text).toContain("todo write ->");
|
|
1082
|
+
expect(text).toContain("Updating todos... 2 todos updated");
|
|
1083
|
+
expect(text).not.toContain("{");
|
|
1084
|
+
expect(text).not.toContain('"todos"');
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1060
1087
|
test("renderAssistantMessage for an edit tool call shows both old and new content without diff prefixes", () => {
|
|
1061
1088
|
// Arrange
|
|
1062
1089
|
const assistant = {
|
|
@@ -1321,6 +1348,70 @@ describe("ui/conversation", () => {
|
|
|
1321
1348
|
expect(text).not.toContain("error 17");
|
|
1322
1349
|
});
|
|
1323
1350
|
|
|
1351
|
+
test("renderToolResult for todoWrite shows the full checklist even when verbose is off", async () => {
|
|
1352
|
+
const snapshot = JSON.stringify({
|
|
1353
|
+
todos: [
|
|
1354
|
+
{ content: "Review prompt wording", status: "completed" },
|
|
1355
|
+
{ content: "Implement todo tools", status: "in_progress" },
|
|
1356
|
+
{ content: "Add /todo command", status: "pending" },
|
|
1357
|
+
{ content: "Run the full verification suite", status: "pending" },
|
|
1358
|
+
],
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
const text = await renderVisibleText(
|
|
1362
|
+
renderToolResult("todoWrite", {}, snapshot, false, RENDER_OPTS),
|
|
1363
|
+
PREVIEW_WIDTH,
|
|
1364
|
+
24,
|
|
1365
|
+
);
|
|
1366
|
+
|
|
1367
|
+
expect(text).toContain("todo write <-");
|
|
1368
|
+
expect(text).toContain("[x] Review prompt wording");
|
|
1369
|
+
expect(text).toContain("[~] Implement todo tools");
|
|
1370
|
+
expect(text).toContain("[ ] Add /todo command");
|
|
1371
|
+
expect(text.join(" ")).toContain("[ ] Run the full verification suite");
|
|
1372
|
+
expect(text.some((line) => line.startsWith("And "))).toBe(false);
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
test("buildConversationLogNodes renders UI todo messages with the shared checklist block", async () => {
|
|
1376
|
+
const state = {
|
|
1377
|
+
messages: [
|
|
1378
|
+
{
|
|
1379
|
+
role: "ui" as const,
|
|
1380
|
+
kind: "todo" as const,
|
|
1381
|
+
todos: [
|
|
1382
|
+
{ content: "Review prompt wording", status: "completed" as const },
|
|
1383
|
+
{ content: "Implement todo tools", status: "in_progress" as const },
|
|
1384
|
+
{ content: "Add /todo command", status: "pending" as const },
|
|
1385
|
+
],
|
|
1386
|
+
timestamp: 1,
|
|
1387
|
+
},
|
|
1388
|
+
],
|
|
1389
|
+
showReasoning: false,
|
|
1390
|
+
verbose: false,
|
|
1391
|
+
theme: DEFAULT_THEME,
|
|
1392
|
+
};
|
|
1393
|
+
|
|
1394
|
+
const text = await renderVisibleText(
|
|
1395
|
+
VStack(
|
|
1396
|
+
{},
|
|
1397
|
+
buildConversationLogNodes(
|
|
1398
|
+
state,
|
|
1399
|
+
{ isStreaming: false, content: [], pendingToolResults: [] },
|
|
1400
|
+
0,
|
|
1401
|
+
PREVIEW_WIDTH,
|
|
1402
|
+
),
|
|
1403
|
+
),
|
|
1404
|
+
PREVIEW_WIDTH,
|
|
1405
|
+
24,
|
|
1406
|
+
);
|
|
1407
|
+
|
|
1408
|
+
expect(text).toContain("todo");
|
|
1409
|
+
expect(text).toContain("[x] Review prompt wording");
|
|
1410
|
+
expect(text).toContain("[~] Implement todo tools");
|
|
1411
|
+
expect(text).toContain("[ ] Add /todo command");
|
|
1412
|
+
expect(text.some((line) => line.includes('"todos"'))).toBe(false);
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1324
1415
|
test("renderToolResult for a generic plugin tool uses the shared result header", () => {
|
|
1325
1416
|
// Arrange
|
|
1326
1417
|
const args = { query: "session persistence sqlite turn numbering" };
|
package/src/ui/conversation.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
import type { AppState } from "../index.ts";
|
|
24
24
|
import type { UiMessage } from "../session.ts";
|
|
25
25
|
import type { Theme } from "../theme.ts";
|
|
26
|
+
import { parseTodoSnapshot, type TodoItem } from "../tools.ts";
|
|
26
27
|
import { APP_NAME, DEV_VERSION_LABEL } from "../version.ts";
|
|
27
28
|
|
|
28
29
|
/** Single blank-line gap used between conversation-level blocks. */
|
|
@@ -164,6 +165,8 @@ interface ToolBlockSpec {
|
|
|
164
165
|
bodyLines: readonly ToolRenderLine[];
|
|
165
166
|
/** Optional syntax-highlighted body rendered from the full unsplit source. */
|
|
166
167
|
highlightedBody?: HighlightedBodySpec;
|
|
168
|
+
/** Optional custom body node rendered as-is. */
|
|
169
|
+
bodyNode?: Node;
|
|
167
170
|
/** Whether `/verbose` preview rules apply to the body. */
|
|
168
171
|
previewBody: boolean;
|
|
169
172
|
}
|
|
@@ -412,6 +415,12 @@ function getToolArgString(args: Record<string, unknown>, key: string): string {
|
|
|
412
415
|
return typeof value === "string" ? value : "";
|
|
413
416
|
}
|
|
414
417
|
|
|
418
|
+
function formatTodoWriteCallSummary(args: Record<string, unknown>): string {
|
|
419
|
+
const todoCount = Array.isArray(args.todos) ? args.todos.length : 0;
|
|
420
|
+
const todoLabel = todoCount === 1 ? "todo" : "todos";
|
|
421
|
+
return `Updating todos... ${todoCount} ${todoLabel} updated`;
|
|
422
|
+
}
|
|
423
|
+
|
|
415
424
|
/** Strip shell execution labels and normalize the exit line for the UI. */
|
|
416
425
|
function normalizeShellOutput(output: string): string {
|
|
417
426
|
return output
|
|
@@ -421,17 +430,18 @@ function normalizeShellOutput(output: string): string {
|
|
|
421
430
|
}
|
|
422
431
|
|
|
423
432
|
function getToolHeaderName(toolName: string): string {
|
|
424
|
-
return toolName
|
|
433
|
+
return toolName.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase();
|
|
425
434
|
}
|
|
426
435
|
|
|
427
436
|
function getToolHeaderColor(toolName: string, theme: Theme) {
|
|
428
437
|
switch (toolName) {
|
|
429
438
|
case "shell":
|
|
439
|
+
case "readImage":
|
|
440
|
+
case "todoWrite":
|
|
441
|
+
case "todoRead":
|
|
430
442
|
return theme.accentText;
|
|
431
443
|
case "edit":
|
|
432
444
|
return theme.secondaryAccentText;
|
|
433
|
-
case "readImage":
|
|
434
|
-
return theme.accentText;
|
|
435
445
|
default:
|
|
436
446
|
return theme.secondaryAccentText ?? theme.toolText;
|
|
437
447
|
}
|
|
@@ -684,6 +694,48 @@ function renderToolLines(
|
|
|
684
694
|
return lines.map((line) => renderToolLine(line, theme));
|
|
685
695
|
}
|
|
686
696
|
|
|
697
|
+
function getTodoMarker(todo: TodoItem): "[ ]" | "[~]" | "[x]" {
|
|
698
|
+
switch (todo.status) {
|
|
699
|
+
case "in_progress":
|
|
700
|
+
return "[~]";
|
|
701
|
+
case "completed":
|
|
702
|
+
return "[x]";
|
|
703
|
+
default:
|
|
704
|
+
return "[ ]";
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function getTodoColor(todo: TodoItem, theme: Theme): Theme["toolText"] {
|
|
709
|
+
switch (todo.status) {
|
|
710
|
+
case "in_progress":
|
|
711
|
+
return theme.accentText;
|
|
712
|
+
case "completed":
|
|
713
|
+
return theme.diffAdded;
|
|
714
|
+
default:
|
|
715
|
+
return theme.toolText;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function renderTodoChecklist(todos: readonly TodoItem[], theme: Theme): Node {
|
|
720
|
+
if (todos.length === 0) {
|
|
721
|
+
return Text("No todo items.", {
|
|
722
|
+
fgColor: theme.mutedText,
|
|
723
|
+
italic: true,
|
|
724
|
+
wrap: "word",
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
return VStack(
|
|
729
|
+
{},
|
|
730
|
+
todos.map((todo) =>
|
|
731
|
+
Text(`${getTodoMarker(todo)} ${todo.content}`, {
|
|
732
|
+
fgColor: getTodoColor(todo, theme),
|
|
733
|
+
wrap: "word",
|
|
734
|
+
}),
|
|
735
|
+
),
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
|
|
687
739
|
function measureToolNodesHeight(lines: readonly Node[], width: number): number {
|
|
688
740
|
if (lines.length === 0) {
|
|
689
741
|
return 0;
|
|
@@ -778,6 +830,10 @@ function renderToolBody(
|
|
|
778
830
|
spec: ToolBlockSpec,
|
|
779
831
|
opts: Pick<ConversationRenderOpts, "previewWidth" | "theme" | "verbose">,
|
|
780
832
|
): { body: Node | null; summary?: ToolRenderLine } {
|
|
833
|
+
if (spec.bodyNode) {
|
|
834
|
+
return { body: spec.bodyNode };
|
|
835
|
+
}
|
|
836
|
+
|
|
781
837
|
if (spec.highlightedBody) {
|
|
782
838
|
return renderToolBodyFromNodes(
|
|
783
839
|
getHighlightedBodyLines(
|
|
@@ -868,6 +924,17 @@ function buildEditToolCallSpec(args: Record<string, unknown>): ToolBlockSpec {
|
|
|
868
924
|
};
|
|
869
925
|
}
|
|
870
926
|
|
|
927
|
+
function buildTodoWriteToolCallSpec(
|
|
928
|
+
args: Record<string, unknown>,
|
|
929
|
+
): ToolBlockSpec {
|
|
930
|
+
return {
|
|
931
|
+
toolName: "todoWrite",
|
|
932
|
+
direction: "->",
|
|
933
|
+
bodyLines: [{ kind: "text", text: formatTodoWriteCallSummary(args) }],
|
|
934
|
+
previewBody: false,
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
|
|
871
938
|
function buildReadImageToolCallSpec(
|
|
872
939
|
args: Record<string, unknown>,
|
|
873
940
|
): ToolBlockSpec {
|
|
@@ -903,6 +970,9 @@ function buildToolCallSpec(
|
|
|
903
970
|
if (toolName === "edit") {
|
|
904
971
|
return buildEditToolCallSpec(args);
|
|
905
972
|
}
|
|
973
|
+
if (toolName === "todoWrite") {
|
|
974
|
+
return buildTodoWriteToolCallSpec(args);
|
|
975
|
+
}
|
|
906
976
|
if (toolName === "readImage") {
|
|
907
977
|
return buildReadImageToolCallSpec(args);
|
|
908
978
|
}
|
|
@@ -956,6 +1026,31 @@ function buildEditToolResultSpec(
|
|
|
956
1026
|
};
|
|
957
1027
|
}
|
|
958
1028
|
|
|
1029
|
+
function buildTodoToolResultSpec(
|
|
1030
|
+
toolName: string,
|
|
1031
|
+
content: ToolResultMessage["content"],
|
|
1032
|
+
isError: boolean,
|
|
1033
|
+
theme: Theme,
|
|
1034
|
+
): ToolBlockSpec {
|
|
1035
|
+
if (isError) {
|
|
1036
|
+
return {
|
|
1037
|
+
toolName,
|
|
1038
|
+
direction: "<-",
|
|
1039
|
+
bodyLines: splitToolTextLines(getToolContentText(content), "error"),
|
|
1040
|
+
previewBody: false,
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
const todos = parseTodoSnapshot(getToolContentText(content)) ?? [];
|
|
1045
|
+
return {
|
|
1046
|
+
toolName,
|
|
1047
|
+
direction: "<-",
|
|
1048
|
+
bodyLines: [],
|
|
1049
|
+
bodyNode: renderTodoChecklist(todos, theme),
|
|
1050
|
+
previewBody: false,
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
|
|
959
1054
|
function buildReadImageToolResultSpec(
|
|
960
1055
|
args: Record<string, unknown>,
|
|
961
1056
|
content: ToolResultMessage["content"],
|
|
@@ -1008,6 +1103,12 @@ function renderToolResultContent(
|
|
|
1008
1103
|
opts,
|
|
1009
1104
|
);
|
|
1010
1105
|
}
|
|
1106
|
+
if (toolName === "todoWrite" || toolName === "todoRead") {
|
|
1107
|
+
return renderToolBlock(
|
|
1108
|
+
buildTodoToolResultSpec(toolName, content, isError, opts.theme),
|
|
1109
|
+
opts,
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1011
1112
|
if (toolName === "readImage") {
|
|
1012
1113
|
return renderToolBlock(
|
|
1013
1114
|
buildReadImageToolResultSpec(args, content, isError),
|
|
@@ -1045,8 +1146,26 @@ export function renderToolResult(
|
|
|
1045
1146
|
return renderToolResultContent(toolName, args, content, isError, opts);
|
|
1046
1147
|
}
|
|
1047
1148
|
|
|
1149
|
+
function renderUiTodoMessage(
|
|
1150
|
+
msg: Extract<UiMessage, { kind: "todo" }>,
|
|
1151
|
+
theme: Theme,
|
|
1152
|
+
): Node {
|
|
1153
|
+
return VStack({ padding: { x: 1 } }, [
|
|
1154
|
+
Text("todo", {
|
|
1155
|
+
fgColor: theme.secondaryAccentText ?? theme.toolText,
|
|
1156
|
+
bold: true,
|
|
1157
|
+
wrap: "word",
|
|
1158
|
+
}),
|
|
1159
|
+
renderTodoChecklist(msg.todos, theme),
|
|
1160
|
+
]);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1048
1163
|
/** Render an internal UI message in the conversation log. */
|
|
1049
1164
|
function renderUiMessage(msg: UiMessage, theme: Theme): Node {
|
|
1165
|
+
if (msg.kind === "todo") {
|
|
1166
|
+
return renderUiTodoMessage(msg, theme);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1050
1169
|
return VStack({ padding: { x: 1 } }, [
|
|
1051
1170
|
Text(msg.content, {
|
|
1052
1171
|
fgColor: theme.mutedText,
|
package/src/ui/help.test.ts
CHANGED
|
@@ -22,6 +22,7 @@ describe("ui/help", () => {
|
|
|
22
22
|
expect(text).toContain(
|
|
23
23
|
"/verbose Toggle verbose tool rendering (currently off)",
|
|
24
24
|
);
|
|
25
|
+
expect(text).toContain("/todo Show the current todo list");
|
|
25
26
|
});
|
|
26
27
|
|
|
27
28
|
test("buildHelpText describes the current Escape behavior", () => {
|
package/src/ui/help.ts
CHANGED
|
@@ -36,6 +36,7 @@ export const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
36
36
|
undo: "Undo last turn",
|
|
37
37
|
reasoning: "Toggle thinking display",
|
|
38
38
|
verbose: "Toggle verbose tool rendering",
|
|
39
|
+
todo: "Show the current todo list",
|
|
39
40
|
login: "OAuth login",
|
|
40
41
|
logout: "OAuth logout",
|
|
41
42
|
help: "Show help",
|