@esso0428/pi-subagents 0.15.17 → 0.16.0
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/CHANGELOG.md +12 -0
- package/dist/agent-history-list.d.ts.map +1 -1
- package/dist/agent-history-list.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +47 -6
- package/dist/index.js.map +1 -1
- package/dist/output-file.js +3 -3
- package/dist/output-file.js.map +1 -1
- package/dist/ui/agent-widget.d.ts +11 -4
- package/dist/ui/agent-widget.d.ts.map +1 -1
- package/dist/ui/agent-widget.js +85 -19
- package/dist/ui/agent-widget.js.map +1 -1
- package/dist/ui/ccstyle/diff/ansi-utils.js +2 -2
- package/dist/ui/ccstyle/diff/ansi-utils.js.map +1 -1
- package/dist/ui/ccstyle/diff/diff-presentation.d.ts.map +1 -1
- package/dist/ui/ccstyle/diff/diff-presentation.js +0 -1
- package/dist/ui/ccstyle/diff/diff-presentation.js.map +1 -1
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts +1 -1
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts.map +1 -1
- package/dist/ui/ccstyle/diff/diff-renderer.js +15 -10
- package/dist/ui/ccstyle/diff/diff-renderer.js.map +1 -1
- package/dist/ui/ccstyle/diff/line-width-safety.d.ts.map +1 -1
- package/dist/ui/ccstyle/diff/line-width-safety.js +1 -2
- package/dist/ui/ccstyle/diff/line-width-safety.js.map +1 -1
- package/dist/ui/ccstyle/diff/shiki-highlight.d.ts +19 -0
- package/dist/ui/ccstyle/diff/shiki-highlight.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/shiki-highlight.js +85 -0
- package/dist/ui/ccstyle/diff/shiki-highlight.js.map +1 -0
- package/dist/ui/ccstyle/tool-renderer.d.ts +1 -1
- package/dist/ui/ccstyle/tool-renderer.d.ts.map +1 -1
- package/dist/ui/ccstyle/tool-renderer.js +1 -1
- package/dist/ui/ccstyle/tool-renderer.js.map +1 -1
- package/dist/ui/ccstyle/tool-result.d.ts +9 -1
- package/dist/ui/ccstyle/tool-result.d.ts.map +1 -1
- package/dist/ui/ccstyle/tool-result.js +137 -5
- package/dist/ui/ccstyle/tool-result.js.map +1 -1
- package/dist/ui/conversation-blocks.d.ts.map +1 -1
- package/dist/ui/conversation-blocks.js +14 -13
- package/dist/ui/conversation-blocks.js.map +1 -1
- package/dist/ui/conversation-role.d.ts +1 -1
- package/dist/ui/conversation-role.d.ts.map +1 -1
- package/dist/ui/conversation-search.js +4 -4
- package/dist/ui/conversation-search.js.map +1 -1
- package/dist/ui/conversation-timeline.d.ts +17 -1
- package/dist/ui/conversation-timeline.d.ts.map +1 -1
- package/dist/ui/conversation-timeline.js +116 -43
- package/dist/ui/conversation-timeline.js.map +1 -1
- package/dist/ui/conversation-viewer.d.ts +5 -1
- package/dist/ui/conversation-viewer.d.ts.map +1 -1
- package/dist/ui/conversation-viewer.js +67 -26
- package/dist/ui/conversation-viewer.js.map +1 -1
- package/dist/ui/deferred-ui-refresh.d.ts +15 -0
- package/dist/ui/deferred-ui-refresh.d.ts.map +1 -0
- package/dist/ui/deferred-ui-refresh.js +50 -0
- package/dist/ui/deferred-ui-refresh.js.map +1 -0
- package/dist/ui/fleet-list.d.ts +55 -14
- package/dist/ui/fleet-list.d.ts.map +1 -1
- package/dist/ui/fleet-list.js +178 -44
- package/dist/ui/fleet-list.js.map +1 -1
- package/dist/write-execution.d.ts.map +1 -1
- package/dist/write-execution.js +1 -1
- package/dist/write-execution.js.map +1 -1
- package/package.json +6 -5
- package/src/agent-history-list.ts +1 -1
- package/src/index.ts +3 -3
- package/src/output-file.ts +3 -3
- package/src/ui/ccstyle/diff/ansi-utils.ts +2 -2
- package/src/ui/ccstyle/diff/diff-presentation.ts +0 -1
- package/src/ui/ccstyle/diff/diff-renderer.ts +22 -21
- package/src/ui/ccstyle/diff/line-width-safety.ts +1 -2
- package/src/ui/ccstyle/diff/shiki-highlight.ts +97 -0
- package/src/ui/ccstyle/tool-renderer.ts +3 -3
- package/src/ui/ccstyle/tool-result.ts +175 -7
- package/src/ui/conversation-role.ts +1 -1
- package/src/ui/conversation-search.ts +4 -4
- package/src/ui/conversation-timeline.ts +7 -7
- package/src/ui/conversation-viewer.ts +16 -7
- package/src/ui/fleet-list.ts +3 -4
- package/src/write-execution.ts +2 -2
- package/test/agent-history-list.test.ts +0 -121
- package/test/agent-manager-history.test.ts +0 -88
- package/test/agent-menu-history-navigation.test.ts +0 -86
- package/test/output-file-history.test.ts +0 -50
- package/test/ui/agent-widget.test.ts +0 -172
- package/test/ui/conversation-blocks.test.ts +0 -103
- package/test/ui/conversation-diff.test.ts +0 -91
- package/test/ui/conversation-search.test.ts +0 -55
- package/test/ui/conversation-viewer.test.ts +0 -438
- package/test/ui/deferred-ui-refresh.test.ts +0 -55
- package/test/ui/fleet-list.test.ts +0 -389
- package/test/write-execution.test.ts +0 -83
- package/tsconfig.json +0 -16
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import {
|
|
4
|
-
renderEditDiffResult,
|
|
5
|
-
renderWriteDiffResult,
|
|
6
|
-
} from "../../src/ui/ccstyle/diff/diff-renderer.js";
|
|
7
|
-
import { renderRichToolResult } from "../../src/ui/ccstyle/tool-result.js";
|
|
8
|
-
import { DEFAULT_TOOL_DISPLAY_CONFIG } from "../../src/ui/ccstyle/diff/types.js";
|
|
9
|
-
|
|
10
|
-
const theme = {
|
|
11
|
-
fg(color: string, text: string) {
|
|
12
|
-
return `<${color}>${text}</${color}>`;
|
|
13
|
-
},
|
|
14
|
-
bg(color: string, text: string) {
|
|
15
|
-
return `<bg:${color}>${text}</bg:${color}>`;
|
|
16
|
-
},
|
|
17
|
-
bold(text: string) {
|
|
18
|
-
return `<bold>${text}</bold>`;
|
|
19
|
-
},
|
|
20
|
-
getBgAnsi() {
|
|
21
|
-
return "";
|
|
22
|
-
},
|
|
23
|
-
getFgAnsi() {
|
|
24
|
-
return "";
|
|
25
|
-
},
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
const config = {
|
|
29
|
-
...DEFAULT_TOOL_DISPLAY_CONFIG,
|
|
30
|
-
diffViewMode: "unified" as const,
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
describe("viewer-local ccstyle diff renderer", () => {
|
|
34
|
-
it("renders persisted write diff after the underlying file changes", () => {
|
|
35
|
-
const persisted = {
|
|
36
|
-
diff: "-1 old\n+1 new",
|
|
37
|
-
patch: "--- file.txt\n+++ file.txt\n@@ -1,1 +1,1 @@\n-old\n+new\n",
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const first = renderWriteDiffResult(undefined, {
|
|
41
|
-
expanded: true,
|
|
42
|
-
filePath: "file.txt",
|
|
43
|
-
persistedDiff: persisted,
|
|
44
|
-
}, config, theme, "");
|
|
45
|
-
const second = renderWriteDiffResult(undefined, {
|
|
46
|
-
expanded: true,
|
|
47
|
-
filePath: "file.txt",
|
|
48
|
-
persistedDiff: persisted,
|
|
49
|
-
}, config, theme, "");
|
|
50
|
-
|
|
51
|
-
expect(second.render(100).join("\n")).toContain("new");
|
|
52
|
-
expect(second.render(100).join("\n")).toBe(first.render(100).join("\n"));
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("prefers persisted write diff over the current content fallback", () => {
|
|
56
|
-
const component = renderWriteDiffResult("later\n", {
|
|
57
|
-
expanded: true,
|
|
58
|
-
filePath: "file.txt",
|
|
59
|
-
previousContent: "old\n",
|
|
60
|
-
fileExistedBeforeWrite: true,
|
|
61
|
-
persistedDiff: { diff: "-1 old\n+1 new", patch: "--- file.txt\n+++ file.txt\n@@ -1,1 +1,1 @@\n-old\n+new\n" },
|
|
62
|
-
}, config, theme, "");
|
|
63
|
-
|
|
64
|
-
const rendered = component.render(100).join("\n");
|
|
65
|
-
expect(rendered).toContain("new");
|
|
66
|
-
expect(rendered).not.toContain("later");
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("returns an explicit unavailable row instead of an empty success card", () => {
|
|
70
|
-
const component = renderRichToolResult("write", {
|
|
71
|
-
content: [{ type: "text", text: "Successfully wrote" }],
|
|
72
|
-
details: { diffUnavailableReason: "durable diff exceeds limit" },
|
|
73
|
-
}, { expanded: true }, theme, { args: { path: "large.txt" } });
|
|
74
|
-
|
|
75
|
-
expect(component?.render(100).join("\n")).toContain("diff unavailable");
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("keeps edit diff and all output rows within the requested width", () => {
|
|
79
|
-
const component = renderEditDiffResult(
|
|
80
|
-
{ diff: "-1 a very long removed line\n+1 a very long added line" },
|
|
81
|
-
{ expanded: true, filePath: "file.ts" },
|
|
82
|
-
config,
|
|
83
|
-
theme,
|
|
84
|
-
"",
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
for (const width of [20, 40, 100]) {
|
|
88
|
-
expect(component.render(width).every((line) => visibleWidth(line) <= width)).toBe(true);
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
});
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
findConversationMatches,
|
|
4
|
-
highlightConversationLine,
|
|
5
|
-
nearestConversationMatchAfter,
|
|
6
|
-
nearestConversationMatchBefore,
|
|
7
|
-
stripAnsi,
|
|
8
|
-
} from "../../src/ui/conversation-search.js";
|
|
9
|
-
|
|
10
|
-
describe("conversation search helpers", () => {
|
|
11
|
-
it("uses smartcase substring matching", () => {
|
|
12
|
-
const lines = [{ plain: "const Answer = 42;" }, { plain: "answer again" }];
|
|
13
|
-
expect(findConversationMatches(lines, "answer")).toEqual([
|
|
14
|
-
{ lineIdx: 0, col: 6, len: 6 },
|
|
15
|
-
{ lineIdx: 1, col: 0, len: 6 },
|
|
16
|
-
]);
|
|
17
|
-
expect(findConversationMatches(lines, "Answer")).toEqual([
|
|
18
|
-
{ lineIdx: 0, col: 6, len: 6 },
|
|
19
|
-
]);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("highlights matches without consuming ANSI escape sequences", () => {
|
|
23
|
-
const styled = "\x1b[31mAnswer\x1b[39m";
|
|
24
|
-
const result = highlightConversationLine(
|
|
25
|
-
styled,
|
|
26
|
-
[{ lineIdx: 0, col: 0, len: 6, current: true }],
|
|
27
|
-
"<m>",
|
|
28
|
-
"<c>",
|
|
29
|
-
"</>",
|
|
30
|
-
);
|
|
31
|
-
expect(result).toContain("<c>");
|
|
32
|
-
expect(result).toContain("Answer");
|
|
33
|
-
expect(result).toContain("\x1b[31m");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("strips CSI and OSC ANSI sequences", () => {
|
|
37
|
-
expect(stripAnsi("\x1b[31mred\x1b[39m")).toBe("red");
|
|
38
|
-
expect(stripAnsi("before\x1b]0;title\x07after")).toBe("beforeafter");
|
|
39
|
-
expect(stripAnsi("before\x1b]0;title\x1b\\after")).toBe("beforeafter");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("wraps nearest-match navigation", () => {
|
|
43
|
-
const matches = [
|
|
44
|
-
{ lineIdx: 2, col: 0, len: 3 },
|
|
45
|
-
{ lineIdx: 5, col: 1, len: 3 },
|
|
46
|
-
{ lineIdx: 9, col: 2, len: 3 },
|
|
47
|
-
];
|
|
48
|
-
expect(nearestConversationMatchAfter(matches, 5)).toBe(1);
|
|
49
|
-
expect(nearestConversationMatchAfter(matches, 10)).toBe(0);
|
|
50
|
-
expect(nearestConversationMatchBefore(matches, 5)).toBe(1);
|
|
51
|
-
expect(nearestConversationMatchBefore(matches, 1)).toBe(2);
|
|
52
|
-
expect(nearestConversationMatchAfter([], 0)).toBe(-1);
|
|
53
|
-
expect(nearestConversationMatchBefore([], 0)).toBe(-1);
|
|
54
|
-
});
|
|
55
|
-
});
|
|
@@ -1,438 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import type { TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
3
|
-
import { ConversationViewer, createStaticConversationSource } from "../../src/ui/conversation-viewer.js";
|
|
4
|
-
|
|
5
|
-
const theme = {
|
|
6
|
-
fg: (name: string, text: string) => name === "text" ? `\x1b[36m${text}\x1b[39m` : text,
|
|
7
|
-
bold: (text: string) => `\x1b[1m${text}\x1b[22m`,
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
function record(extra: Record<string, unknown> = {}): any {
|
|
11
|
-
return {
|
|
12
|
-
id: "agent-1",
|
|
13
|
-
type: "general-purpose",
|
|
14
|
-
description: "test agent",
|
|
15
|
-
status: "completed",
|
|
16
|
-
toolUses: 0,
|
|
17
|
-
startedAt: 0,
|
|
18
|
-
lifetimeUsage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
19
|
-
...extra,
|
|
20
|
-
};
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function tui() {
|
|
24
|
-
return {
|
|
25
|
-
terminal: { rows: 30 },
|
|
26
|
-
requestRender: vi.fn(),
|
|
27
|
-
stop: vi.fn(),
|
|
28
|
-
start: vi.fn(),
|
|
29
|
-
} as any;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function mouse(type: TuiMouseEvent["type"], y: number, overrides: Partial<TuiMouseEvent> = {}): TuiMouseEvent {
|
|
33
|
-
return {
|
|
34
|
-
type,
|
|
35
|
-
button: "left",
|
|
36
|
-
x: 2,
|
|
37
|
-
y,
|
|
38
|
-
screenX: 2,
|
|
39
|
-
screenY: y,
|
|
40
|
-
width: 120,
|
|
41
|
-
height: 30,
|
|
42
|
-
shift: false,
|
|
43
|
-
alt: false,
|
|
44
|
-
ctrl: false,
|
|
45
|
-
...overrides,
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// The test record has the default invocation row, so timeline line 0 is viewer row 4.
|
|
50
|
-
function viewerY(timelineLine: number): number {
|
|
51
|
-
return 4 + timelineLine;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
describe("ConversationViewer", () => {
|
|
55
|
-
it("renders assistant fenced code through the Markdown renderer", () => {
|
|
56
|
-
const ui = tui();
|
|
57
|
-
const viewer = new ConversationViewer(
|
|
58
|
-
ui,
|
|
59
|
-
createStaticConversationSource([
|
|
60
|
-
{ role: "assistant", content: [{ type: "text", text: "```ts\nconst answer: number = 42;\n```" }] },
|
|
61
|
-
] as any),
|
|
62
|
-
record(),
|
|
63
|
-
undefined,
|
|
64
|
-
theme,
|
|
65
|
-
vi.fn(),
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
const rendered = viewer.render(100).join("\n");
|
|
69
|
-
expect(rendered).toContain("answer");
|
|
70
|
-
expect(rendered).toMatch(/\x1b\[/);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("toggles compact tool rows without changing the transcript", () => {
|
|
74
|
-
const ui = tui();
|
|
75
|
-
const viewer = new ConversationViewer(
|
|
76
|
-
ui,
|
|
77
|
-
createStaticConversationSource([
|
|
78
|
-
{ role: "assistant", content: [{ type: "toolCall", name: "read", arguments: { path: "src/index.ts" } }] },
|
|
79
|
-
{ role: "toolResult", toolName: "read", content: [{ type: "text", text: "contents" }] },
|
|
80
|
-
] as any),
|
|
81
|
-
record(),
|
|
82
|
-
undefined,
|
|
83
|
-
theme,
|
|
84
|
-
vi.fn(),
|
|
85
|
-
);
|
|
86
|
-
|
|
87
|
-
expect(viewer.render(100).join("\n")).toContain("read");
|
|
88
|
-
viewer.handleInput("t");
|
|
89
|
-
expect(viewer.render(100).join("\n")).not.toContain("contents");
|
|
90
|
-
viewer.handleInput("t");
|
|
91
|
-
expect(viewer.render(100).join("\n")).toContain("contents");
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it("shows pi-peek hints for live viewers and hides O in history", () => {
|
|
95
|
-
const live = new ConversationViewer(
|
|
96
|
-
tui(),
|
|
97
|
-
createStaticConversationSource([{ role: "assistant", content: "live" }] as any),
|
|
98
|
-
record({ status: "running", session: {} }),
|
|
99
|
-
undefined,
|
|
100
|
-
theme,
|
|
101
|
-
vi.fn(),
|
|
102
|
-
vi.fn(),
|
|
103
|
-
undefined,
|
|
104
|
-
vi.fn(),
|
|
105
|
-
{ pi: {} as any, ctx: {} as any, readOnly: false },
|
|
106
|
-
);
|
|
107
|
-
const liveFooter = live.render(120).join("\n");
|
|
108
|
-
expect(liveFooter).toContain("j/k scroll · J/K messages · [/] tools · w preview · g/G t M o O /n N q");
|
|
109
|
-
expect(liveFooter).toContain("e steer");
|
|
110
|
-
expect(liveFooter).toContain("x stop");
|
|
111
|
-
live.handleInput("e");
|
|
112
|
-
expect(live.render(120).join("\n")).toContain("Enter send");
|
|
113
|
-
live.handleInput("\x1b");
|
|
114
|
-
|
|
115
|
-
const history = new ConversationViewer(
|
|
116
|
-
tui(),
|
|
117
|
-
createStaticConversationSource([{ role: "assistant", content: "history" }] as any),
|
|
118
|
-
record({ session: {} }),
|
|
119
|
-
undefined,
|
|
120
|
-
theme,
|
|
121
|
-
vi.fn(),
|
|
122
|
-
undefined,
|
|
123
|
-
undefined,
|
|
124
|
-
undefined,
|
|
125
|
-
{ pi: {} as any, ctx: {} as any, readOnly: true },
|
|
126
|
-
);
|
|
127
|
-
const historyFooter = history.render(120).join("\n");
|
|
128
|
-
expect(historyFooter).toContain("j/k scroll · J/K messages · [/] tools · w preview · g/G t M o /n N q");
|
|
129
|
-
expect(historyFooter).not.toContain("M o O");
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("shows semantic role headers, effective invocation metadata, and the search row", () => {
|
|
133
|
-
const semanticTheme = {
|
|
134
|
-
fg: (name: string, text: string) => `<${name}>${text}</${name}>`,
|
|
135
|
-
bold: (text: string) => `<bold>${text}</bold>`,
|
|
136
|
-
};
|
|
137
|
-
const viewer = new ConversationViewer(
|
|
138
|
-
tui(),
|
|
139
|
-
createStaticConversationSource([
|
|
140
|
-
{ role: "user", content: "find model" },
|
|
141
|
-
{ role: "assistant", content: [{ type: "text", text: "model found" }] },
|
|
142
|
-
] as any),
|
|
143
|
-
record({ invocation: { effectiveModelName: "claude-sonnet", effectiveThinking: "high" } }),
|
|
144
|
-
undefined,
|
|
145
|
-
semanticTheme,
|
|
146
|
-
vi.fn(),
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
const initial = viewer.render(120).join("\n");
|
|
150
|
-
expect(initial).toContain("<userMessageText>USER</userMessageText>");
|
|
151
|
-
expect(initial).toContain("<accent>ASSISTANT</accent>");
|
|
152
|
-
expect(initial).toContain("model: claude-sonnet");
|
|
153
|
-
expect(initial).toContain("thinking: high");
|
|
154
|
-
|
|
155
|
-
viewer.handleInput("/");
|
|
156
|
-
const searchRow = viewer.render(120).join("\n");
|
|
157
|
-
expect(searchRow).toContain("<accent>/</accent>");
|
|
158
|
-
expect(searchRow).toContain("Enter apply · Esc cancel");
|
|
159
|
-
for (const character of "model") viewer.handleInput(character);
|
|
160
|
-
expect(viewer.render(120).join("\n")).toContain("model");
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("focuses and expands a bounded fallback tool card", () => {
|
|
164
|
-
const viewer = new ConversationViewer(
|
|
165
|
-
tui(),
|
|
166
|
-
createStaticConversationSource([
|
|
167
|
-
{ role: "assistant", content: [{ type: "toolCall", name: "read", arguments: { path: "x" } }] },
|
|
168
|
-
{ role: "toolResult", toolName: "read", content: [{ type: "text", text: "one\ntwo\nthree\nfour\nfive\nsix" }] },
|
|
169
|
-
] as any),
|
|
170
|
-
record(),
|
|
171
|
-
undefined,
|
|
172
|
-
theme,
|
|
173
|
-
vi.fn(),
|
|
174
|
-
);
|
|
175
|
-
|
|
176
|
-
expect(viewer.render(120).join("\n")).not.toContain("six");
|
|
177
|
-
viewer.handleInput("\t");
|
|
178
|
-
viewer.handleInput("\r");
|
|
179
|
-
expect(viewer.render(120).join("\n")).toContain("six");
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it("toggles mouse expansion only on the normalized click event", () => {
|
|
183
|
-
const viewer = new ConversationViewer(
|
|
184
|
-
tui(),
|
|
185
|
-
createStaticConversationSource([
|
|
186
|
-
{ role: "assistant", content: [{ type: "toolCall", name: "read", arguments: { path: "x" } }] },
|
|
187
|
-
{ role: "toolResult", toolName: "read", content: [{ type: "text", text: "one\ntwo\nthree\nfour\nfive\nsix" }] },
|
|
188
|
-
] as any),
|
|
189
|
-
record(),
|
|
190
|
-
undefined,
|
|
191
|
-
theme,
|
|
192
|
-
vi.fn(),
|
|
193
|
-
);
|
|
194
|
-
expect(viewer.render(120).join("\n")).not.toContain("six");
|
|
195
|
-
const span = (viewer as any).timeline.getSnapshot().toolSpans[0];
|
|
196
|
-
expect(span.clickStartLine).toEqual(expect.any(Number));
|
|
197
|
-
const hintY = viewerY(span.clickStartLine);
|
|
198
|
-
expect(viewer.handleMouse(mouse("press", hintY))?.handled).toBe(true);
|
|
199
|
-
expect(viewer.render(120).join("\n")).not.toContain("six");
|
|
200
|
-
viewer.handleMouse(mouse("click", hintY, { clickCount: 1 }));
|
|
201
|
-
expect(viewer.render(120).join("\n")).toContain("six");
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it("focuses USER and ASSISTANT blocks from their rendered spans", () => {
|
|
205
|
-
const viewer = new ConversationViewer(
|
|
206
|
-
tui(),
|
|
207
|
-
createStaticConversationSource([
|
|
208
|
-
{ role: "user", content: "user text" },
|
|
209
|
-
{ role: "assistant", content: [{ type: "text", text: "assistant text" }] },
|
|
210
|
-
] as any),
|
|
211
|
-
record(),
|
|
212
|
-
undefined,
|
|
213
|
-
theme,
|
|
214
|
-
vi.fn(),
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
viewer.render(120);
|
|
218
|
-
const timeline = (viewer as any).timeline;
|
|
219
|
-
const spans = timeline.getSnapshot().blockSpans;
|
|
220
|
-
expect(spans).toHaveLength(2);
|
|
221
|
-
viewer.handleMouse(mouse("click", viewerY(spans[1].startLine), { clickCount: 1 }));
|
|
222
|
-
expect((viewer as any).selectedMessageIdx).toBe(1);
|
|
223
|
-
expect(timeline.getFocusedToolCallId()).toBeUndefined();
|
|
224
|
-
viewer.handleMouse(mouse("click", viewerY(spans[0].startLine), { clickCount: 1 }));
|
|
225
|
-
expect((viewer as any).selectedMessageIdx).toBe(0);
|
|
226
|
-
expect(timeline.getFocusedToolCallId()).toBeUndefined();
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it("focuses normal tool rows without expanding, while hint clicks expand", () => {
|
|
230
|
-
const viewer = new ConversationViewer(
|
|
231
|
-
tui(),
|
|
232
|
-
createStaticConversationSource([
|
|
233
|
-
{ role: "assistant", content: [{ type: "toolCall", name: "read", arguments: { path: "x" } }] },
|
|
234
|
-
{ role: "toolResult", toolName: "read", content: [{ type: "text", text: "one\ntwo\nthree\nfour\nfive\nsix" }] },
|
|
235
|
-
] as any),
|
|
236
|
-
record(),
|
|
237
|
-
undefined,
|
|
238
|
-
theme,
|
|
239
|
-
vi.fn(),
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
expect(viewer.render(120).join("\n")).not.toContain("six");
|
|
243
|
-
const timeline = (viewer as any).timeline;
|
|
244
|
-
const initialSpan = timeline.getSnapshot().toolSpans[0];
|
|
245
|
-
const toolId = initialSpan.toolCallId;
|
|
246
|
-
const hintLine = initialSpan.clickStartLine as number;
|
|
247
|
-
const normalOutputLine = hintLine - 1;
|
|
248
|
-
|
|
249
|
-
const press = viewer.handleMouse(mouse("press", viewerY(hintLine)));
|
|
250
|
-
expect(press).toMatchObject({ handled: true, capture: true });
|
|
251
|
-
expect(timeline.getFocusedToolCallId()).toBeUndefined();
|
|
252
|
-
expect(viewer.render(120).join("\n")).not.toContain("six");
|
|
253
|
-
|
|
254
|
-
viewer.handleMouse(mouse("click", viewerY(normalOutputLine), { clickCount: 1 }));
|
|
255
|
-
expect(timeline.getFocusedToolCallId()).toBe(toolId);
|
|
256
|
-
expect((viewer as any).selectedMessageIdx).toBe(initialSpan.blockIndex);
|
|
257
|
-
expect(viewer.render(120).join("\n")).not.toContain("six");
|
|
258
|
-
|
|
259
|
-
const collapsedSpan = timeline.getSnapshot().toolSpans[0];
|
|
260
|
-
viewer.handleMouse(mouse("click", viewerY(collapsedSpan.clickStartLine as number), { clickCount: 1 }));
|
|
261
|
-
expect(timeline.getFocusedToolCallId()).toBe(toolId);
|
|
262
|
-
expect(viewer.render(120).join("\n")).toContain("six");
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it("synchronizes J/K message focus and anchors [/] around the selected message", () => {
|
|
266
|
-
const viewer = new ConversationViewer(
|
|
267
|
-
tui(),
|
|
268
|
-
createStaticConversationSource([
|
|
269
|
-
{ role: "user", content: "user one" },
|
|
270
|
-
{ role: "assistant", content: [{ type: "text", text: "assistant one" }] },
|
|
271
|
-
{ role: "assistant", content: [{ type: "toolCall", id: "tool-1", name: "read", arguments: { path: "one" } }] },
|
|
272
|
-
{ role: "toolResult", toolCallId: "tool-1", toolName: "read", content: [{ type: "text", text: "one" }] },
|
|
273
|
-
{ role: "assistant", content: [{ type: "text", text: "assistant two" }] },
|
|
274
|
-
{ role: "assistant", content: [{ type: "toolCall", id: "tool-2", name: "read", arguments: { path: "two" } }] },
|
|
275
|
-
{ role: "toolResult", toolCallId: "tool-2", toolName: "read", content: [{ type: "text", text: "two" }] },
|
|
276
|
-
] as any),
|
|
277
|
-
record(),
|
|
278
|
-
undefined,
|
|
279
|
-
theme,
|
|
280
|
-
vi.fn(),
|
|
281
|
-
);
|
|
282
|
-
|
|
283
|
-
viewer.render(120);
|
|
284
|
-
const timeline = (viewer as any).timeline;
|
|
285
|
-
const snapshot = timeline.getSnapshot();
|
|
286
|
-
viewer.handleInput("]");
|
|
287
|
-
expect((viewer as any).selectedMessageIdx).toBe(snapshot.toolSpans[0].blockIndex);
|
|
288
|
-
expect(timeline.getFocusedToolCallId()).toBe("tool-1");
|
|
289
|
-
|
|
290
|
-
viewer.handleInput("J");
|
|
291
|
-
expect((viewer as any).selectedMessageIdx).toBe(3);
|
|
292
|
-
expect(timeline.getFocusedToolCallId()).toBeUndefined();
|
|
293
|
-
viewer.handleInput("]");
|
|
294
|
-
expect((viewer as any).selectedMessageIdx).toBe(snapshot.toolSpans[1].blockIndex);
|
|
295
|
-
expect(timeline.getFocusedToolCallId()).toBe("tool-2");
|
|
296
|
-
|
|
297
|
-
viewer.handleInput("K");
|
|
298
|
-
expect((viewer as any).selectedMessageIdx).toBe(3);
|
|
299
|
-
expect(timeline.getFocusedToolCallId()).toBeUndefined();
|
|
300
|
-
viewer.handleInput("[");
|
|
301
|
-
expect((viewer as any).selectedMessageIdx).toBe(snapshot.toolSpans[0].blockIndex);
|
|
302
|
-
expect(timeline.getFocusedToolCallId()).toBe("tool-1");
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it("uses the first or last tool for initial [/], before an explicit focus exists", () => {
|
|
306
|
-
const source = createStaticConversationSource([
|
|
307
|
-
{ role: "assistant", content: [{ type: "toolCall", id: "tool-1", name: "read", arguments: { path: "one" } }] },
|
|
308
|
-
{ role: "toolResult", toolCallId: "tool-1", toolName: "read", content: [{ type: "text", text: "one" }] },
|
|
309
|
-
{ role: "assistant", content: [{ type: "toolCall", id: "tool-2", name: "read", arguments: { path: "two" } }] },
|
|
310
|
-
{ role: "toolResult", toolCallId: "tool-2", toolName: "read", content: [{ type: "text", text: "two" }] },
|
|
311
|
-
] as any);
|
|
312
|
-
const makeViewer = () => new ConversationViewer(tui(), source, record(), undefined, theme, vi.fn());
|
|
313
|
-
|
|
314
|
-
const left = makeViewer();
|
|
315
|
-
left.render(120);
|
|
316
|
-
left.handleInput("[");
|
|
317
|
-
expect((left as any).timeline.getFocusedToolCallId()).toBe("tool-2");
|
|
318
|
-
|
|
319
|
-
const right = makeViewer();
|
|
320
|
-
right.render(120);
|
|
321
|
-
right.handleInput("]");
|
|
322
|
-
expect((right as any).timeline.getFocusedToolCallId()).toBe("tool-1");
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
it("highlights the collapsed tool hint and the viewer close control independently", () => {
|
|
326
|
-
const semanticTheme = {
|
|
327
|
-
fg: (name: string, text: string) => `<${name}>${text}</${name}>`,
|
|
328
|
-
bold: (text: string) => `<bold>${text}</bold>`,
|
|
329
|
-
};
|
|
330
|
-
const done = vi.fn();
|
|
331
|
-
const viewer = new ConversationViewer(
|
|
332
|
-
tui(),
|
|
333
|
-
createStaticConversationSource([
|
|
334
|
-
{ role: "assistant", content: [{ type: "toolCall", name: "read", arguments: { path: "x" } }] },
|
|
335
|
-
{ role: "toolResult", toolName: "read", content: [{ type: "text", text: "one\ntwo\nthree\nfour\nfive\nsix" }] },
|
|
336
|
-
] as any),
|
|
337
|
-
record(),
|
|
338
|
-
undefined,
|
|
339
|
-
semanticTheme,
|
|
340
|
-
done,
|
|
341
|
-
);
|
|
342
|
-
const initial = viewer.render(120).join("\n");
|
|
343
|
-
const span = (viewer as any).timeline.getSnapshot().toolSpans[0];
|
|
344
|
-
const hintY = viewerY(span.clickStartLine);
|
|
345
|
-
expect(initial).not.toContain("<text>click to show more</text>");
|
|
346
|
-
viewer.handleMouse(mouse("move", hintY, { button: "none" }));
|
|
347
|
-
expect(viewer.render(120).join("\n")).toContain("<text>click to show more</text>");
|
|
348
|
-
viewer.handleMouse(mouse("move", 1, { button: "none", x: 114, screenX: 114, screenY: 1 }));
|
|
349
|
-
expect(viewer.render(120).join("\n")).toContain("<text><bold>[esc]</bold></text>");
|
|
350
|
-
expect(viewer.handleMouse(mouse("press", 1, { x: 114, screenX: 114 }))?.handled).toBe(true);
|
|
351
|
-
expect(viewer.handleMouse(mouse("click", 1, { x: 114, screenX: 114, clickCount: 1 }))?.handled).toBe(true);
|
|
352
|
-
expect(done).toHaveBeenCalledTimes(1);
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
it("focuses nearby tools with [ and ] and opens a complete read-only preview with w", () => {
|
|
356
|
-
const ui = tui();
|
|
357
|
-
let preview: any;
|
|
358
|
-
const previewDone = vi.fn();
|
|
359
|
-
const custom = vi.fn((factory: any) => {
|
|
360
|
-
preview = factory(ui, theme, undefined, previewDone);
|
|
361
|
-
const initial = preview.render(100).join("\\n");
|
|
362
|
-
expect(initial).toContain("Input");
|
|
363
|
-
expect(initial).toContain("Output");
|
|
364
|
-
expect(initial).toContain("line 1");
|
|
365
|
-
expect(initial).not.toContain("line 40");
|
|
366
|
-
preview.handleInput("G");
|
|
367
|
-
expect(preview.render(100).join("\\n")).toContain("line 40");
|
|
368
|
-
preview.handleInput("k");
|
|
369
|
-
preview.handleInput("j");
|
|
370
|
-
preview.handleInput("g");
|
|
371
|
-
preview.handleInput("g");
|
|
372
|
-
expect(preview.render(100).join("\\n")).toContain("line 1");
|
|
373
|
-
preview.handleInput("q");
|
|
374
|
-
expect(previewDone).toHaveBeenCalledTimes(1);
|
|
375
|
-
return Promise.resolve(undefined);
|
|
376
|
-
});
|
|
377
|
-
const viewer = new ConversationViewer(
|
|
378
|
-
ui,
|
|
379
|
-
createStaticConversationSource([
|
|
380
|
-
{ role: "assistant", content: [{ type: "toolCall", name: "read", arguments: { path: "first" } }] },
|
|
381
|
-
{ role: "toolResult", toolName: "read", content: [{ type: "text", text: Array.from({ length: 40 }, (_, index) => `line ${index + 1}`).join("\n") }] },
|
|
382
|
-
{ role: "assistant", content: [{ type: "toolCall", name: "read", arguments: { path: "second" } }] },
|
|
383
|
-
{ role: "toolResult", toolName: "read", content: [{ type: "text", text: "second output" }] },
|
|
384
|
-
] as any),
|
|
385
|
-
record(),
|
|
386
|
-
undefined,
|
|
387
|
-
theme,
|
|
388
|
-
vi.fn(),
|
|
389
|
-
undefined,
|
|
390
|
-
undefined,
|
|
391
|
-
undefined,
|
|
392
|
-
{ pi: {} as any, ctx: { ui: { custom } } as any, readOnly: true },
|
|
393
|
-
);
|
|
394
|
-
viewer.render(120);
|
|
395
|
-
const timeline = (viewer as any).timeline;
|
|
396
|
-
const ids = timeline.getSnapshot().interactiveToolIds;
|
|
397
|
-
viewer.handleInput("]");
|
|
398
|
-
expect(timeline.getFocusedToolCallId()).toBe(ids[0]);
|
|
399
|
-
viewer.render(120);
|
|
400
|
-
viewer.handleInput("w");
|
|
401
|
-
expect(custom).toHaveBeenCalledWith(expect.any(Function), expect.objectContaining({ overlay: true }));
|
|
402
|
-
preview.handleInput("G");
|
|
403
|
-
preview.handleInput("g");
|
|
404
|
-
preview.handleInput("g");
|
|
405
|
-
preview.handleInput("q");
|
|
406
|
-
viewer.handleInput("]");
|
|
407
|
-
expect(timeline.getFocusedToolCallId()).toBe(ids[1]);
|
|
408
|
-
viewer.handleInput("[");
|
|
409
|
-
expect(timeline.getFocusedToolCallId()).toBe(ids[0]);
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
it("keeps historical viewers read-only and closes on Escape", () => {
|
|
413
|
-
const ui = tui();
|
|
414
|
-
const steer = vi.fn();
|
|
415
|
-
const stop = vi.fn();
|
|
416
|
-
const done = vi.fn();
|
|
417
|
-
const viewer = new ConversationViewer(
|
|
418
|
-
ui,
|
|
419
|
-
createStaticConversationSource([{ role: "assistant", content: "history" }] as any),
|
|
420
|
-
record({ status: "running", session: {} }),
|
|
421
|
-
undefined,
|
|
422
|
-
theme,
|
|
423
|
-
done,
|
|
424
|
-
stop,
|
|
425
|
-
undefined,
|
|
426
|
-
steer,
|
|
427
|
-
{ pi: {} as any, ctx: {} as any, readOnly: true },
|
|
428
|
-
);
|
|
429
|
-
|
|
430
|
-
viewer.handleInput("\r");
|
|
431
|
-
viewer.handleInput("x");
|
|
432
|
-
viewer.handleInput("O");
|
|
433
|
-
expect(steer).not.toHaveBeenCalled();
|
|
434
|
-
expect(stop).not.toHaveBeenCalled();
|
|
435
|
-
viewer.handleInput("\x1b");
|
|
436
|
-
expect(done).toHaveBeenCalledTimes(1);
|
|
437
|
-
});
|
|
438
|
-
});
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { createDeferredUiRefresh } from "../../src/ui/deferred-ui-refresh.js";
|
|
3
|
-
|
|
4
|
-
describe("createDeferredUiRefresh", () => {
|
|
5
|
-
afterEach(() => vi.useRealTimers());
|
|
6
|
-
|
|
7
|
-
it("fires once after the debounce delay", () => {
|
|
8
|
-
vi.useFakeTimers();
|
|
9
|
-
const requestRender = vi.fn();
|
|
10
|
-
const refresh = createDeferredUiRefresh(requestRender);
|
|
11
|
-
|
|
12
|
-
refresh.schedule();
|
|
13
|
-
vi.advanceTimersByTime(49);
|
|
14
|
-
expect(requestRender).not.toHaveBeenCalled();
|
|
15
|
-
vi.advanceTimersByTime(1);
|
|
16
|
-
expect(requestRender).toHaveBeenCalledTimes(1);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("coalesces repeated schedules and respects the max wait", () => {
|
|
20
|
-
vi.useFakeTimers();
|
|
21
|
-
const requestRender = vi.fn();
|
|
22
|
-
const refresh = createDeferredUiRefresh(requestRender);
|
|
23
|
-
|
|
24
|
-
refresh.schedule();
|
|
25
|
-
vi.advanceTimersByTime(40);
|
|
26
|
-
refresh.schedule();
|
|
27
|
-
vi.advanceTimersByTime(40);
|
|
28
|
-
refresh.schedule();
|
|
29
|
-
vi.advanceTimersByTime(40);
|
|
30
|
-
refresh.schedule();
|
|
31
|
-
vi.advanceTimersByTime(40);
|
|
32
|
-
refresh.schedule();
|
|
33
|
-
vi.advanceTimersByTime(39);
|
|
34
|
-
expect(requestRender).not.toHaveBeenCalled();
|
|
35
|
-
vi.advanceTimersByTime(1);
|
|
36
|
-
expect(requestRender).toHaveBeenCalledTimes(1);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("supports cancel and dispose without firing a stale callback", () => {
|
|
40
|
-
vi.useFakeTimers();
|
|
41
|
-
const requestRender = vi.fn();
|
|
42
|
-
const refresh = createDeferredUiRefresh(requestRender);
|
|
43
|
-
|
|
44
|
-
refresh.schedule();
|
|
45
|
-
refresh.cancel();
|
|
46
|
-
vi.advanceTimersByTime(250);
|
|
47
|
-
expect(requestRender).not.toHaveBeenCalled();
|
|
48
|
-
|
|
49
|
-
refresh.schedule();
|
|
50
|
-
refresh.dispose();
|
|
51
|
-
refresh.schedule();
|
|
52
|
-
vi.advanceTimersByTime(250);
|
|
53
|
-
expect(requestRender).not.toHaveBeenCalled();
|
|
54
|
-
});
|
|
55
|
-
});
|