@esso0428/pi-subagents 0.15.16 → 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 +18 -0
- package/README.md +1 -1
- 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 +49 -7
- package/src/output-file.ts +3 -3
- package/src/ui/agent-widget.ts +18 -10
- 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/deferred-ui-refresh.ts +72 -0
- package/src/ui/fleet-list.ts +185 -50
- 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 -161
- 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/fleet-list.test.ts +0 -135
- package/test/write-execution.test.ts +0 -83
- package/tsconfig.json +0 -16
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
-
import { streamToOutputFile, writeInitialEntry } from "../src/output-file.js";
|
|
6
|
-
|
|
7
|
-
const tempDirs: string[] = [];
|
|
8
|
-
|
|
9
|
-
afterEach(async () => {
|
|
10
|
-
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
describe("streamToOutputFile durable message snapshots", () => {
|
|
14
|
-
it("writes tool result details to both output and project history files", async () => {
|
|
15
|
-
const dir = await mkdtemp(join(tmpdir(), "pi-subagents-output-test-"));
|
|
16
|
-
tempDirs.push(dir);
|
|
17
|
-
const outputPath = join(dir, "agent.output");
|
|
18
|
-
const historyPath = join(dir, "history.jsonl");
|
|
19
|
-
writeInitialEntry(outputPath, "agent-1", "run", dir);
|
|
20
|
-
writeInitialEntry(historyPath, "agent-1", "run", dir);
|
|
21
|
-
|
|
22
|
-
let listener: ((event: any) => void) | undefined;
|
|
23
|
-
const session = {
|
|
24
|
-
messages: [
|
|
25
|
-
{ role: "user", content: "run" },
|
|
26
|
-
{
|
|
27
|
-
role: "toolResult",
|
|
28
|
-
toolCallId: "call-1",
|
|
29
|
-
toolName: "write",
|
|
30
|
-
content: [{ type: "text", text: "Successfully wrote to file.txt" }],
|
|
31
|
-
details: { diff: "+1 new", patch: "+new" },
|
|
32
|
-
},
|
|
33
|
-
],
|
|
34
|
-
subscribe(next: (event: any) => void) {
|
|
35
|
-
listener = next;
|
|
36
|
-
return () => { listener = undefined; };
|
|
37
|
-
},
|
|
38
|
-
} as any;
|
|
39
|
-
|
|
40
|
-
const cleanup = streamToOutputFile(session, outputPath, "agent-1", dir, historyPath);
|
|
41
|
-
listener?.({ type: "turn_end" });
|
|
42
|
-
cleanup();
|
|
43
|
-
|
|
44
|
-
for (const path of [outputPath, historyPath]) {
|
|
45
|
-
const entries = (await readFile(path, "utf8")).trim().split("\n").map((line) => JSON.parse(line));
|
|
46
|
-
expect(entries).toHaveLength(2);
|
|
47
|
-
expect(entries[1].message.details).toEqual({ diff: "+1 new", patch: "+new" });
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
});
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { AgentManager } from "../../src/agent-manager.js";
|
|
3
|
-
import {
|
|
4
|
-
AgentWidget,
|
|
5
|
-
getWidgetLineBudget,
|
|
6
|
-
MAX_WIDGET_LINES,
|
|
7
|
-
type AgentActivity,
|
|
8
|
-
type Theme,
|
|
9
|
-
} from "../../src/ui/agent-widget.js";
|
|
10
|
-
import type { AgentRecord } from "../../src/types.js";
|
|
11
|
-
|
|
12
|
-
const theme: Theme = {
|
|
13
|
-
fg: (_color, text) => text,
|
|
14
|
-
bold: (text) => text,
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const managers: AgentManager[] = [];
|
|
18
|
-
|
|
19
|
-
afterEach(() => {
|
|
20
|
-
for (const manager of managers.splice(0)) manager.dispose();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
function makeRecord(overrides: Partial<AgentRecord> = {}): AgentRecord {
|
|
24
|
-
return {
|
|
25
|
-
id: "agent-1",
|
|
26
|
-
type: "Explore",
|
|
27
|
-
description: "Inspect the repository",
|
|
28
|
-
status: "completed",
|
|
29
|
-
toolUses: 0,
|
|
30
|
-
startedAt: 1_000,
|
|
31
|
-
completedAt: 2_000,
|
|
32
|
-
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
|
|
33
|
-
compactionCount: 0,
|
|
34
|
-
...overrides,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function renderWidget(
|
|
39
|
-
records: AgentRecord[],
|
|
40
|
-
rows = 24,
|
|
41
|
-
activity = new Map<string, AgentActivity>(),
|
|
42
|
-
mode: "all" | "background" | "off" = "all",
|
|
43
|
-
) {
|
|
44
|
-
const manager = { listAgents: () => records } as unknown as AgentManager;
|
|
45
|
-
const widget = new AgentWidget(manager, activity, () => mode);
|
|
46
|
-
const tui = { terminal: { columns: 200, rows } };
|
|
47
|
-
const render = () => (widget as any).renderWidget(tui, theme) as string[];
|
|
48
|
-
return { render, widget };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function createWidgetHarness(records: AgentRecord[]) {
|
|
52
|
-
const manager = { listAgents: () => records } as unknown as AgentManager;
|
|
53
|
-
let factory: ((tui: any, theme: Theme) => { render(): string[]; invalidate(): void }) | undefined;
|
|
54
|
-
const tui = {
|
|
55
|
-
terminal: { columns: 120, rows: 24 },
|
|
56
|
-
requestRender: vi.fn(),
|
|
57
|
-
};
|
|
58
|
-
const ui = {
|
|
59
|
-
setStatus: vi.fn(),
|
|
60
|
-
setWidget: vi.fn((_key: string, content: any) => { factory = content; }),
|
|
61
|
-
};
|
|
62
|
-
const widget = new AgentWidget(manager, new Map(), () => "all");
|
|
63
|
-
widget.setUICtx(ui);
|
|
64
|
-
widget.update();
|
|
65
|
-
factory?.(tui, theme).render();
|
|
66
|
-
tui.requestRender.mockClear();
|
|
67
|
-
return { records, tui, ui, widget, render: () => factory?.(tui, theme).render() ?? [] };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
describe("AgentWidget live records", () => {
|
|
71
|
-
it("keeps restored history in the live widget and manager history", () => {
|
|
72
|
-
const manager = new AgentManager();
|
|
73
|
-
managers.push(manager);
|
|
74
|
-
manager.restoreCompleted([makeRecord({ id: "history", description: "restored history" })]);
|
|
75
|
-
|
|
76
|
-
const widget = new AgentWidget(manager, new Map(), () => "all");
|
|
77
|
-
const tui = { terminal: { columns: 120, rows: 24 } };
|
|
78
|
-
|
|
79
|
-
expect((widget as any).renderWidget(tui, theme).join("\n")).toContain("restored history");
|
|
80
|
-
expect(manager.listAgents().map(record => record.id)).toEqual(["history"]);
|
|
81
|
-
expect(manager.getRecord("history")).toBeDefined();
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("keeps a live completed record visible after markFinished", () => {
|
|
85
|
-
const record = makeRecord({ id: "live", description: "live completion" });
|
|
86
|
-
const { render, widget } = renderWidget([record]);
|
|
87
|
-
|
|
88
|
-
widget.markFinished(record.id);
|
|
89
|
-
|
|
90
|
-
expect(render().join("\n")).toContain("live completion");
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("normalizes multiline rows and bounds output by the responsive ceiling", () => {
|
|
94
|
-
const running = makeRecord({
|
|
95
|
-
id: "running",
|
|
96
|
-
status: "running",
|
|
97
|
-
completedAt: undefined,
|
|
98
|
-
description: "running\nsecond line",
|
|
99
|
-
});
|
|
100
|
-
const finished = makeRecord({
|
|
101
|
-
id: "finished",
|
|
102
|
-
description: "finished\nsecond line",
|
|
103
|
-
});
|
|
104
|
-
const extraFinished = Array.from({ length: 10 }, (_, i) => makeRecord({
|
|
105
|
-
id: `finished-${i}`,
|
|
106
|
-
description: `extra ${i}`,
|
|
107
|
-
}));
|
|
108
|
-
const activity = new Map<string, AgentActivity>([
|
|
109
|
-
[running.id, {
|
|
110
|
-
activeTools: new Map(),
|
|
111
|
-
toolUses: 0,
|
|
112
|
-
responseText: "activity\nsecond line",
|
|
113
|
-
turnCount: 1,
|
|
114
|
-
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
|
|
115
|
-
}],
|
|
116
|
-
]);
|
|
117
|
-
const rows = 9;
|
|
118
|
-
const { render } = renderWidget([running, finished, ...extraFinished], rows, activity);
|
|
119
|
-
const lines = render();
|
|
120
|
-
|
|
121
|
-
expect(lines.every(line => !/[\r\n]/.test(line))).toBe(true);
|
|
122
|
-
expect(lines.length).toBeLessThanOrEqual(getWidgetLineBudget(rows));
|
|
123
|
-
expect(lines.length).toBeLessThanOrEqual(MAX_WIDGET_LINES);
|
|
124
|
-
expect(lines.join("\n")).toContain("running second line");
|
|
125
|
-
expect(lines.some(line => line.includes("more"))).toBe(true);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("counts queued records in the overflow indicator on short terminals", () => {
|
|
129
|
-
const running = makeRecord({ id: "running", status: "running", completedAt: undefined });
|
|
130
|
-
const queued = makeRecord({ id: "queued", status: "queued", completedAt: undefined });
|
|
131
|
-
const { render } = renderWidget([running, queued], 6);
|
|
132
|
-
|
|
133
|
-
const output = render().join("\\n");
|
|
134
|
-
expect(output).toContain("+2 more");
|
|
135
|
-
expect(output).toContain("1 queued");
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("reserves editor space on short terminals while retaining the widget ceiling", () => {
|
|
139
|
-
expect(getWidgetLineBudget(9)).toBe(5);
|
|
140
|
-
expect(getWidgetLineBudget(100)).toBe(MAX_WIDGET_LINES);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("does not request a render when a live snapshot is unchanged", () => {
|
|
144
|
-
vi.useFakeTimers();
|
|
145
|
-
vi.setSystemTime(10_000);
|
|
146
|
-
try {
|
|
147
|
-
const running = makeRecord({ id: "running", status: "running", completedAt: undefined });
|
|
148
|
-
const { widget, tui } = createWidgetHarness([running]);
|
|
149
|
-
|
|
150
|
-
widget.update();
|
|
151
|
-
expect(tui.requestRender).not.toHaveBeenCalled();
|
|
152
|
-
|
|
153
|
-
running.description = "changed description";
|
|
154
|
-
widget.update();
|
|
155
|
-
expect(tui.requestRender).toHaveBeenCalledTimes(1);
|
|
156
|
-
widget.dispose();
|
|
157
|
-
} finally {
|
|
158
|
-
vi.useRealTimers();
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
});
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { formatConversationMessages } from "../../src/ui/conversation-blocks.js";
|
|
3
|
-
|
|
4
|
-
describe("formatConversationMessages", () => {
|
|
5
|
-
it("keeps markdown source for user and assistant blocks", () => {
|
|
6
|
-
const blocks = formatConversationMessages([
|
|
7
|
-
{ role: "user", content: "# Ask\n\nUse `x`." },
|
|
8
|
-
{ role: "assistant", content: [
|
|
9
|
-
{ type: "text", text: "## Answer\n\n```ts\nconst x: number = 1;\n```" },
|
|
10
|
-
] },
|
|
11
|
-
] as any);
|
|
12
|
-
|
|
13
|
-
expect(blocks.filter((b) => b.kind === "text").map((b) => b.markdown)).toEqual([
|
|
14
|
-
"# Ask\n\nUse `x`.",
|
|
15
|
-
"## Answer\n\n```ts\nconst x: number = 1;\n```",
|
|
16
|
-
]);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("separates assistant tool calls and bounds tool results", () => {
|
|
20
|
-
const blocks = formatConversationMessages([
|
|
21
|
-
{ role: "assistant", content: [
|
|
22
|
-
{ type: "toolCall", name: "read", arguments: { path: "src/index.ts" } },
|
|
23
|
-
] },
|
|
24
|
-
{ role: "toolResult", toolName: "read", isError: false, content: [
|
|
25
|
-
{ type: "text", text: "a".repeat(100) },
|
|
26
|
-
] },
|
|
27
|
-
] as any, { maxToolResultChars: 40 });
|
|
28
|
-
|
|
29
|
-
expect(blocks.some((b) => b.toolLine?.includes("read"))).toBe(true);
|
|
30
|
-
const result = blocks.find((b) => b.role === "tool" && b.fullText.includes("read"));
|
|
31
|
-
expect(result?.fullText.length).toBeLessThanOrEqual(40);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("pairs tool results by id, then same-name source order, and preserves orphans", () => {
|
|
35
|
-
const blocks = formatConversationMessages([
|
|
36
|
-
{ role: "assistant", content: [
|
|
37
|
-
{ type: "toolCall", id: "call-read", name: "read", arguments: { path: "a.ts" } },
|
|
38
|
-
{ type: "toolCall", id: "call-write", name: "write", arguments: { path: "b.ts", content: "new" } },
|
|
39
|
-
] },
|
|
40
|
-
{ role: "toolResult", toolCallId: "call-read", toolName: "read", isError: false, details: { source: "id" }, content: [{ type: "text", text: "read output" }] },
|
|
41
|
-
{ role: "toolResult", toolName: "write", isError: true, details: { source: "name" }, content: [{ type: "text", text: "write failed" }] },
|
|
42
|
-
{ role: "toolResult", toolCallId: "missing", toolName: "bash", isError: true, content: [{ type: "text", text: "orphan" }] },
|
|
43
|
-
] as any);
|
|
44
|
-
|
|
45
|
-
const tools = blocks.filter((block) => block.kind === "tool");
|
|
46
|
-
expect(tools).toHaveLength(3);
|
|
47
|
-
expect(tools[0]?.toolCallId).toBe("call-read");
|
|
48
|
-
expect(tools[0]?.toolResult?.details).toEqual({ source: "id" });
|
|
49
|
-
expect(tools[1]?.toolCallId).toBe("call-write");
|
|
50
|
-
expect(tools[1]?.toolResult?.isError).toBe(true);
|
|
51
|
-
expect(tools[2]?.toolResult?.content).toEqual([{ type: "text", text: "orphan" }]);
|
|
52
|
-
expect(tools[2]?.fullText).toContain("orphan");
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("uses a stable synthetic id when a tool call omits its id", () => {
|
|
56
|
-
const blocks = formatConversationMessages([
|
|
57
|
-
{ role: "assistant", content: [{ type: "toolCall", name: "read", arguments: { path: "a.ts" } }] },
|
|
58
|
-
{ role: "toolResult", toolName: "read", content: [{ type: "text", text: "ok" }] },
|
|
59
|
-
] as any);
|
|
60
|
-
|
|
61
|
-
expect(blocks[0]?.toolCallId).toBe("message-0-tool-0");
|
|
62
|
-
expect(blocks[0]?.toolResult?.content).toEqual([{ type: "text", text: "ok" }]);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it("normalizes CRLF and bounds text with a deterministic truncation marker", () => {
|
|
66
|
-
const blocks = formatConversationMessages([
|
|
67
|
-
{ role: "user", content: "one\r\ntwo\rthree\nfour" },
|
|
68
|
-
] as any, { maxEntryChars: 12 });
|
|
69
|
-
|
|
70
|
-
expect(blocks[0]?.markdown).toBe("\n… [truncate");
|
|
71
|
-
expect(blocks[0]?.id).toBe("message-0");
|
|
72
|
-
expect(blocks[0]?.fullText.length).toBeLessThanOrEqual(12);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it("formats bash, images, summaries, and deterministic tool ids", () => {
|
|
76
|
-
const blocks = formatConversationMessages([
|
|
77
|
-
{ role: "assistant", content: [
|
|
78
|
-
{ type: "text", text: "answer" },
|
|
79
|
-
{ type: "toolCall", name: "read", arguments: { path: "a.ts" } },
|
|
80
|
-
{ type: "toolCall", name: "write", arguments: { path: "b.ts" } },
|
|
81
|
-
] },
|
|
82
|
-
{ role: "bashExecution", command: "echo hi", output: "hi" },
|
|
83
|
-
{ role: "custom", customType: "note", content: [{ type: "image" }] },
|
|
84
|
-
{ role: "compactionSummary", summary: "Earlier context" },
|
|
85
|
-
] as any);
|
|
86
|
-
|
|
87
|
-
expect(blocks.map((block) => block.id)).toEqual([
|
|
88
|
-
"message-0", "message-0-tool-0", "message-0-tool-1", "message-1", "message-2", "message-3",
|
|
89
|
-
]);
|
|
90
|
-
expect(blocks[1]?.toolLine).toContain("· read → a.ts");
|
|
91
|
-
expect(blocks[3]?.fullText).toContain("$ echo hi\nhi");
|
|
92
|
-
expect(blocks[4]?.markdown).toBe("[image]");
|
|
93
|
-
expect(blocks[5]?.role).toBe("meta");
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it("skips unsupported or empty messages without throwing", () => {
|
|
97
|
-
expect(formatConversationMessages([
|
|
98
|
-
{ role: "unknown", content: { not: "text" } },
|
|
99
|
-
{ role: "user", content: [{ type: "thinking", thinking: "hidden" }] },
|
|
100
|
-
{ role: "custom", display: false, content: "hidden" },
|
|
101
|
-
] as any)).toEqual([]);
|
|
102
|
-
});
|
|
103
|
-
});
|
|
@@ -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
|
-
});
|