@esso0428/pi-subagents 0.15.5 → 0.15.7
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 +10 -19
- package/README.md +5 -5
- package/dist/agent-history-list.d.ts +19 -0
- package/dist/agent-history-list.d.ts.map +1 -0
- package/dist/agent-history-list.js +69 -0
- package/dist/agent-history-list.js.map +1 -0
- package/dist/agent-history.d.ts +2 -1
- package/dist/agent-history.d.ts.map +1 -0
- package/dist/agent-history.js +32 -34
- package/dist/agent-history.js.map +1 -0
- package/dist/agent-manager.d.ts +24 -8
- package/dist/agent-manager.d.ts.map +1 -0
- package/dist/agent-manager.js +171 -93
- package/dist/agent-manager.js.map +1 -0
- package/dist/agent-runner.d.ts +1 -0
- package/dist/agent-runner.d.ts.map +1 -0
- package/dist/agent-runner.js +75 -86
- package/dist/agent-runner.js.map +1 -0
- package/dist/agent-types.d.ts +1 -0
- package/dist/agent-types.d.ts.map +1 -0
- package/dist/agent-types.js +28 -45
- package/dist/agent-types.js.map +1 -0
- package/dist/context.d.ts +1 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +3 -6
- package/dist/context.js.map +1 -0
- package/dist/cross-extension-rpc.d.ts +1 -0
- package/dist/cross-extension-rpc.d.ts.map +1 -0
- package/dist/cross-extension-rpc.js +6 -9
- package/dist/cross-extension-rpc.js.map +1 -0
- package/dist/custom-agents.d.ts +1 -0
- package/dist/custom-agents.d.ts.map +1 -0
- package/dist/custom-agents.js +16 -18
- package/dist/custom-agents.js.map +1 -0
- package/dist/default-agents.d.ts +1 -0
- package/dist/default-agents.d.ts.map +1 -0
- package/dist/default-agents.js +2 -4
- package/dist/default-agents.js.map +1 -0
- package/dist/enabled-models.d.ts +1 -0
- package/dist/enabled-models.d.ts.map +1 -0
- package/dist/enabled-models.js +12 -16
- package/dist/enabled-models.js.map +1 -0
- package/dist/env.d.ts +1 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +2 -4
- package/dist/env.js.map +1 -0
- package/dist/group-join.d.ts +1 -0
- package/dist/group-join.d.ts.map +1 -0
- package/dist/group-join.js +2 -5
- package/dist/group-join.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +306 -292
- package/dist/index.js.map +1 -0
- package/dist/invocation-config.d.ts +1 -0
- package/dist/invocation-config.d.ts.map +1 -0
- package/dist/invocation-config.js +3 -6
- package/dist/invocation-config.js.map +1 -0
- package/dist/memory.d.ts +1 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +25 -34
- package/dist/memory.js.map +1 -0
- package/dist/model-resolver.d.ts +1 -0
- package/dist/model-resolver.d.ts.map +1 -0
- package/dist/model-resolver.js +2 -4
- package/dist/model-resolver.js.map +1 -0
- package/dist/nico-overrides.d.ts +1 -0
- package/dist/nico-overrides.d.ts.map +1 -0
- package/dist/nico-overrides.js +15 -20
- package/dist/nico-overrides.js.map +1 -0
- package/dist/output-file.d.ts +1 -0
- package/dist/output-file.d.ts.map +1 -0
- package/dist/output-file.js +17 -22
- package/dist/output-file.js.map +1 -0
- package/dist/prompts.d.ts +1 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +2 -4
- package/dist/prompts.js.map +1 -0
- package/dist/schedule-store.d.ts +1 -0
- package/dist/schedule-store.d.ts.map +1 -0
- package/dist/schedule-store.js +17 -21
- package/dist/schedule-store.js.map +1 -0
- package/dist/schedule.d.ts +1 -0
- package/dist/schedule.d.ts.map +1 -0
- package/dist/schedule.js +9 -12
- package/dist/schedule.js.map +1 -0
- package/dist/settings.d.ts +1 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +16 -23
- package/dist/settings.js.map +1 -0
- package/dist/skill-loader.d.ts +1 -0
- package/dist/skill-loader.d.ts.map +1 -0
- package/dist/skill-loader.js +21 -23
- package/dist/skill-loader.js.map +1 -0
- package/dist/status-note.d.ts +1 -0
- package/dist/status-note.d.ts.map +1 -0
- package/dist/status-note.js +2 -4
- package/dist/status-note.js.map +1 -0
- package/dist/types.d.ts +5 -1
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -4
- package/dist/types.js.map +1 -0
- package/dist/ui/agent-widget.d.ts +2 -5
- package/dist/ui/agent-widget.d.ts.map +1 -0
- package/dist/ui/agent-widget.js +29 -51
- package/dist/ui/agent-widget.js.map +1 -0
- package/dist/ui/ccstyle/diff/ansi-utils.d.ts +11 -0
- package/dist/ui/ccstyle/diff/ansi-utils.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/ansi-utils.js +145 -0
- package/dist/ui/ccstyle/diff/ansi-utils.js.map +1 -0
- package/dist/ui/ccstyle/diff/diff-presentation.d.ts +12 -0
- package/dist/ui/ccstyle/diff/diff-presentation.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/diff-presentation.js +49 -0
- package/dist/ui/ccstyle/diff/diff-presentation.js.map +1 -0
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts +46 -0
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/diff-renderer.js +2044 -0
- package/dist/ui/ccstyle/diff/diff-renderer.js.map +1 -0
- package/dist/ui/ccstyle/diff/line-width-safety.d.ts +12 -0
- package/dist/ui/ccstyle/diff/line-width-safety.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/line-width-safety.js +59 -0
- package/dist/ui/ccstyle/diff/line-width-safety.js.map +1 -0
- package/dist/ui/ccstyle/diff/render-utils.d.ts +6 -0
- package/dist/ui/ccstyle/diff/render-utils.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/render-utils.js +25 -0
- package/dist/ui/ccstyle/diff/render-utils.js.map +1 -0
- package/dist/ui/ccstyle/diff/types.d.ts +22 -0
- package/dist/ui/ccstyle/diff/types.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/types.js +10 -0
- package/dist/ui/ccstyle/diff/types.js.map +1 -0
- package/dist/ui/ccstyle/diff/write-display-utils.d.ts +2 -0
- package/dist/ui/ccstyle/diff/write-display-utils.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/write-display-utils.js +12 -0
- package/dist/ui/ccstyle/diff/write-display-utils.js.map +1 -0
- package/dist/ui/ccstyle/tool-renderer.d.ts +17 -0
- package/dist/ui/ccstyle/tool-renderer.d.ts.map +1 -0
- package/dist/ui/ccstyle/tool-renderer.js +58 -0
- package/dist/ui/ccstyle/tool-renderer.js.map +1 -0
- package/dist/ui/ccstyle/tool-result.d.ts +24 -0
- package/dist/ui/ccstyle/tool-result.d.ts.map +1 -0
- package/dist/ui/ccstyle/tool-result.js +193 -0
- package/dist/ui/ccstyle/tool-result.js.map +1 -0
- package/dist/ui/conversation-blocks.d.ts +46 -0
- package/dist/ui/conversation-blocks.d.ts.map +1 -0
- package/dist/ui/conversation-blocks.js +306 -0
- package/dist/ui/conversation-blocks.js.map +1 -0
- package/dist/ui/conversation-nvim.d.ts +8 -0
- package/dist/ui/conversation-nvim.d.ts.map +1 -0
- package/dist/ui/conversation-nvim.js +117 -0
- package/dist/ui/conversation-nvim.js.map +1 -0
- package/dist/ui/conversation-role.d.ts +13 -0
- package/dist/ui/conversation-role.d.ts.map +1 -0
- package/dist/ui/conversation-role.js +39 -0
- package/dist/ui/conversation-role.js.map +1 -0
- package/dist/ui/conversation-search.d.ts +39 -0
- package/dist/ui/conversation-search.d.ts.map +1 -0
- package/dist/ui/conversation-search.js +124 -0
- package/dist/ui/conversation-search.js.map +1 -0
- package/dist/ui/conversation-timeline.d.ts +62 -0
- package/dist/ui/conversation-timeline.d.ts.map +1 -0
- package/dist/ui/conversation-timeline.js +263 -0
- package/dist/ui/conversation-timeline.js.map +1 -0
- package/dist/ui/conversation-viewer.d.ts +62 -20
- package/dist/ui/conversation-viewer.d.ts.map +1 -0
- package/dist/ui/conversation-viewer.js +449 -224
- package/dist/ui/conversation-viewer.js.map +1 -0
- package/dist/ui/fleet-list.d.ts +24 -11
- package/dist/ui/fleet-list.d.ts.map +1 -0
- package/dist/ui/fleet-list.js +86 -78
- package/dist/ui/fleet-list.js.map +1 -0
- package/dist/ui/schedule-menu.d.ts +1 -0
- package/dist/ui/schedule-menu.d.ts.map +1 -0
- package/dist/ui/schedule-menu.js +2 -4
- package/dist/ui/schedule-menu.js.map +1 -0
- package/dist/ui/viewer-keys.d.ts +1 -0
- package/dist/ui/viewer-keys.d.ts.map +1 -0
- package/dist/ui/viewer-keys.js +8 -10
- package/dist/ui/viewer-keys.js.map +1 -0
- package/dist/usage.d.ts +1 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +5 -10
- package/dist/usage.js.map +1 -0
- package/dist/worktree.d.ts +1 -0
- package/dist/worktree.d.ts.map +1 -0
- package/dist/worktree.js +27 -31
- package/dist/worktree.js.map +1 -0
- package/dist/write-execution.d.ts +42 -0
- package/dist/write-execution.d.ts.map +1 -0
- package/dist/write-execution.js +138 -0
- package/dist/write-execution.js.map +1 -0
- package/package.json +1 -1
- package/src/agent-history-list.ts +91 -0
- package/src/agent-history.ts +12 -17
- package/src/agent-manager.ts +169 -81
- package/src/agent-runner.ts +12 -13
- package/src/index.ts +117 -98
- package/src/output-file.ts +1 -1
- package/src/types.ts +4 -1
- package/src/ui/agent-widget.ts +3 -9
- package/src/ui/ccstyle/ATTRIBUTION.md +37 -0
- package/src/ui/ccstyle/diff/ansi-utils.ts +168 -0
- package/src/ui/ccstyle/diff/diff-presentation.ts +70 -0
- package/src/ui/ccstyle/diff/diff-renderer.ts +3148 -0
- package/src/ui/ccstyle/diff/line-width-safety.ts +92 -0
- package/src/ui/ccstyle/diff/render-utils.ts +32 -0
- package/src/ui/ccstyle/diff/types.ts +33 -0
- package/src/ui/ccstyle/diff/write-display-utils.ts +12 -0
- package/src/ui/ccstyle/tool-renderer.ts +71 -0
- package/src/ui/ccstyle/tool-result.ts +238 -0
- package/src/ui/conversation-blocks.ts +404 -0
- package/src/ui/conversation-nvim.ts +127 -0
- package/src/ui/conversation-role.ts +45 -0
- package/src/ui/conversation-search.ts +161 -0
- package/src/ui/conversation-timeline.ts +315 -0
- package/src/ui/conversation-viewer.ts +418 -210
- package/src/ui/fleet-list.ts +73 -52
- package/src/write-execution.ts +180 -0
- package/test/agent-history-list.test.ts +121 -0
- package/test/agent-manager-history.test.ts +59 -58
- package/test/agent-menu-history-navigation.test.ts +86 -0
- package/test/output-file-history.test.ts +50 -0
- package/test/ui/conversation-blocks.test.ts +103 -0
- package/test/ui/conversation-diff.test.ts +91 -0
- package/test/ui/conversation-search.test.ts +55 -0
- package/test/ui/conversation-viewer.test.ts +182 -0
- package/test/ui/fleet-list.test.ts +131 -0
- package/test/write-execution.test.ts +83 -0
- package/tsconfig.json +16 -0
- package/vitest.temp.config.mts +6 -0
- package/dist/ui/markdown-result.d.ts +0 -3
- package/dist/ui/markdown-result.js +0 -53
- package/src/ui/markdown-result.ts +0 -56
- package/test/agent-history.test.ts +0 -63
- package/test/ui/markdown-result.test.ts +0 -45
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { ConversationViewer, createStaticConversationSource } from "../../src/ui/conversation-viewer.js";
|
|
3
|
+
|
|
4
|
+
const theme = {
|
|
5
|
+
fg: (name: string, text: string) => name === "text" ? `\x1b[36m${text}\x1b[39m` : text,
|
|
6
|
+
bold: (text: string) => `\x1b[1m${text}\x1b[22m`,
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function record(extra: Record<string, unknown> = {}): any {
|
|
10
|
+
return {
|
|
11
|
+
id: "agent-1",
|
|
12
|
+
type: "general-purpose",
|
|
13
|
+
description: "test agent",
|
|
14
|
+
status: "completed",
|
|
15
|
+
toolUses: 0,
|
|
16
|
+
startedAt: 0,
|
|
17
|
+
lifetimeUsage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
18
|
+
...extra,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function tui() {
|
|
23
|
+
return {
|
|
24
|
+
terminal: { rows: 30 },
|
|
25
|
+
requestRender: vi.fn(),
|
|
26
|
+
stop: vi.fn(),
|
|
27
|
+
start: vi.fn(),
|
|
28
|
+
} as any;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("ConversationViewer", () => {
|
|
32
|
+
it("renders assistant fenced code through the Markdown renderer", () => {
|
|
33
|
+
const ui = tui();
|
|
34
|
+
const viewer = new ConversationViewer(
|
|
35
|
+
ui,
|
|
36
|
+
createStaticConversationSource([
|
|
37
|
+
{ role: "assistant", content: [{ type: "text", text: "```ts\nconst answer: number = 42;\n```" }] },
|
|
38
|
+
] as any),
|
|
39
|
+
record(),
|
|
40
|
+
undefined,
|
|
41
|
+
theme,
|
|
42
|
+
vi.fn(),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const rendered = viewer.render(100).join("\n");
|
|
46
|
+
expect(rendered).toContain("answer");
|
|
47
|
+
expect(rendered).toMatch(/\x1b\[/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("toggles compact tool rows without changing the transcript", () => {
|
|
51
|
+
const ui = tui();
|
|
52
|
+
const viewer = new ConversationViewer(
|
|
53
|
+
ui,
|
|
54
|
+
createStaticConversationSource([
|
|
55
|
+
{ role: "assistant", content: [{ type: "toolCall", name: "read", arguments: { path: "src/index.ts" } }] },
|
|
56
|
+
{ role: "toolResult", toolName: "read", content: [{ type: "text", text: "contents" }] },
|
|
57
|
+
] as any),
|
|
58
|
+
record(),
|
|
59
|
+
undefined,
|
|
60
|
+
theme,
|
|
61
|
+
vi.fn(),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect(viewer.render(100).join("\n")).toContain("read");
|
|
65
|
+
viewer.handleInput("t");
|
|
66
|
+
expect(viewer.render(100).join("\n")).not.toContain("contents");
|
|
67
|
+
viewer.handleInput("t");
|
|
68
|
+
expect(viewer.render(100).join("\n")).toContain("contents");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("shows pi-peek hints for live viewers and hides O in history", () => {
|
|
72
|
+
const live = new ConversationViewer(
|
|
73
|
+
tui(),
|
|
74
|
+
createStaticConversationSource([{ role: "assistant", content: "live" }] as any),
|
|
75
|
+
record({ status: "running", session: {} }),
|
|
76
|
+
undefined,
|
|
77
|
+
theme,
|
|
78
|
+
vi.fn(),
|
|
79
|
+
vi.fn(),
|
|
80
|
+
undefined,
|
|
81
|
+
vi.fn(),
|
|
82
|
+
{ pi: {} as any, ctx: {} as any, readOnly: false },
|
|
83
|
+
);
|
|
84
|
+
const liveFooter = live.render(120).join("\n");
|
|
85
|
+
expect(liveFooter).toContain("j/k · J/K · g/G t M o O /n N q");
|
|
86
|
+
expect(liveFooter).toContain("Enter steer");
|
|
87
|
+
expect(liveFooter).toContain("x stop");
|
|
88
|
+
|
|
89
|
+
const history = new ConversationViewer(
|
|
90
|
+
tui(),
|
|
91
|
+
createStaticConversationSource([{ role: "assistant", content: "history" }] as any),
|
|
92
|
+
record({ session: {} }),
|
|
93
|
+
undefined,
|
|
94
|
+
theme,
|
|
95
|
+
vi.fn(),
|
|
96
|
+
undefined,
|
|
97
|
+
undefined,
|
|
98
|
+
undefined,
|
|
99
|
+
{ pi: {} as any, ctx: {} as any, readOnly: true },
|
|
100
|
+
);
|
|
101
|
+
const historyFooter = history.render(120).join("\n");
|
|
102
|
+
expect(historyFooter).toContain("j/k · J/K · g/G t M o /n N q");
|
|
103
|
+
expect(historyFooter).not.toContain("M o O");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("shows semantic role headers, effective invocation metadata, and the search row", () => {
|
|
107
|
+
const semanticTheme = {
|
|
108
|
+
fg: (name: string, text: string) => `<${name}>${text}</${name}>`,
|
|
109
|
+
bold: (text: string) => `<bold>${text}</bold>`,
|
|
110
|
+
};
|
|
111
|
+
const viewer = new ConversationViewer(
|
|
112
|
+
tui(),
|
|
113
|
+
createStaticConversationSource([
|
|
114
|
+
{ role: "user", content: "find model" },
|
|
115
|
+
{ role: "assistant", content: [{ type: "text", text: "model found" }] },
|
|
116
|
+
] as any),
|
|
117
|
+
record({ invocation: { effectiveModelName: "claude-sonnet", effectiveThinking: "high" } }),
|
|
118
|
+
undefined,
|
|
119
|
+
semanticTheme,
|
|
120
|
+
vi.fn(),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const initial = viewer.render(120).join("\n");
|
|
124
|
+
expect(initial).toContain("<userMessageText>USER</userMessageText>");
|
|
125
|
+
expect(initial).toContain("<accent>ASSISTANT</accent>");
|
|
126
|
+
expect(initial).toContain("model: claude-sonnet");
|
|
127
|
+
expect(initial).toContain("thinking: high");
|
|
128
|
+
|
|
129
|
+
viewer.handleInput("/");
|
|
130
|
+
const searchRow = viewer.render(120).join("\n");
|
|
131
|
+
expect(searchRow).toContain("<accent>/</accent>");
|
|
132
|
+
expect(searchRow).toContain("Enter apply · Esc cancel");
|
|
133
|
+
for (const character of "model") viewer.handleInput(character);
|
|
134
|
+
expect(viewer.render(120).join("\n")).toContain("model");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("focuses and expands a bounded fallback tool card", () => {
|
|
138
|
+
const viewer = new ConversationViewer(
|
|
139
|
+
tui(),
|
|
140
|
+
createStaticConversationSource([
|
|
141
|
+
{ role: "assistant", content: [{ type: "toolCall", name: "read", arguments: { path: "x" } }] },
|
|
142
|
+
{ role: "toolResult", toolName: "read", content: [{ type: "text", text: "one\ntwo\nthree\nfour\nfive\nsix" }] },
|
|
143
|
+
] as any),
|
|
144
|
+
record(),
|
|
145
|
+
undefined,
|
|
146
|
+
theme,
|
|
147
|
+
vi.fn(),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
expect(viewer.render(120).join("\n")).not.toContain("six");
|
|
151
|
+
viewer.handleInput("\t");
|
|
152
|
+
viewer.handleInput("\r");
|
|
153
|
+
expect(viewer.render(120).join("\n")).toContain("six");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("keeps historical viewers read-only and closes on Escape", () => {
|
|
157
|
+
const ui = tui();
|
|
158
|
+
const steer = vi.fn();
|
|
159
|
+
const stop = vi.fn();
|
|
160
|
+
const done = vi.fn();
|
|
161
|
+
const viewer = new ConversationViewer(
|
|
162
|
+
ui,
|
|
163
|
+
createStaticConversationSource([{ role: "assistant", content: "history" }] as any),
|
|
164
|
+
record({ status: "running", session: {} }),
|
|
165
|
+
undefined,
|
|
166
|
+
theme,
|
|
167
|
+
done,
|
|
168
|
+
stop,
|
|
169
|
+
undefined,
|
|
170
|
+
steer,
|
|
171
|
+
{ pi: {} as any, ctx: {} as any, readOnly: true },
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
viewer.handleInput("\r");
|
|
175
|
+
viewer.handleInput("x");
|
|
176
|
+
viewer.handleInput("O");
|
|
177
|
+
expect(steer).not.toHaveBeenCalled();
|
|
178
|
+
expect(stop).not.toHaveBeenCalled();
|
|
179
|
+
viewer.handleInput("\x1b");
|
|
180
|
+
expect(done).toHaveBeenCalledTimes(1);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
calculateFleetAgentWindow,
|
|
4
|
+
FLEET_MAX_RENDER_ROWS,
|
|
5
|
+
FleetList,
|
|
6
|
+
} from "../../src/ui/fleet-list.js";
|
|
7
|
+
import { canOpenActiveAgent } from "../../src/agent-history-list.js";
|
|
8
|
+
|
|
9
|
+
const theme = {
|
|
10
|
+
fg: (_color: string, text: string) => text,
|
|
11
|
+
bold: (text: string) => text,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function makeAgents(count: number): any[] {
|
|
15
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
16
|
+
id: `agent-${i}`,
|
|
17
|
+
type: "Explore",
|
|
18
|
+
description: `agent ${i}`,
|
|
19
|
+
status: "running",
|
|
20
|
+
session: { messages: [], subscribe: () => () => {} },
|
|
21
|
+
toolUses: 0,
|
|
22
|
+
startedAt: i + 1,
|
|
23
|
+
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
|
|
24
|
+
compactionCount: 0,
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function createFleetHarness(records: any[]) {
|
|
29
|
+
const manager: any = {
|
|
30
|
+
records,
|
|
31
|
+
listAgents() { return this.records; },
|
|
32
|
+
abort: vi.fn(() => false),
|
|
33
|
+
steer: vi.fn(() => false),
|
|
34
|
+
};
|
|
35
|
+
const tui = { terminal: { rows: 24 }, requestRender: vi.fn() } as any;
|
|
36
|
+
let terminalInput: ((data: string) => { consume?: boolean } | undefined) | undefined;
|
|
37
|
+
let factory: any;
|
|
38
|
+
const ui: any = {
|
|
39
|
+
setWidget: vi.fn((_key: string, content: any) => { factory = content; }),
|
|
40
|
+
onTerminalInput: (handler: typeof terminalInput) => {
|
|
41
|
+
terminalInput = handler;
|
|
42
|
+
return () => { terminalInput = undefined; };
|
|
43
|
+
},
|
|
44
|
+
getEditorText: () => "",
|
|
45
|
+
notify: vi.fn(),
|
|
46
|
+
custom: vi.fn(async () => undefined),
|
|
47
|
+
};
|
|
48
|
+
const fleet = new FleetList(manager, new Map());
|
|
49
|
+
fleet.setUICtx(ui);
|
|
50
|
+
fleet.update();
|
|
51
|
+
const render = (width = 120) => factory ? factory(tui, theme).render(width) : [];
|
|
52
|
+
const input = (data: string) => terminalInput?.(data);
|
|
53
|
+
render(); // The real TUI invokes the widget factory before keyboard input arrives.
|
|
54
|
+
return { manager, fleet, tui, ui, render, input };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe("canOpenActiveAgent", () => {
|
|
58
|
+
it("does not treat terminal records as active FleetView rows", () => {
|
|
59
|
+
expect(canOpenActiveAgent({ status: "completed", session: {} } as any)).toBe(false);
|
|
60
|
+
expect(canOpenActiveAgent({ status: "error", session: {} } as any)).toBe(false);
|
|
61
|
+
expect(canOpenActiveAgent({ status: "running", session: {} } as any)).toBe(true);
|
|
62
|
+
expect(canOpenActiveAgent({ status: "queued", session: undefined } as any)).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("FleetList bounded window", () => {
|
|
67
|
+
it("keeps every FleetView state within six total rows", () => {
|
|
68
|
+
for (const agentCount of [0, 1, 2, 4, 5, 12]) {
|
|
69
|
+
for (let selected = 0; selected <= agentCount; selected++) {
|
|
70
|
+
const window = calculateFleetAgentWindow(agentCount, selected);
|
|
71
|
+
const rows = 2 + (window.hiddenAbove > 0 ? 1 : 0)
|
|
72
|
+
+ (window.end - window.start)
|
|
73
|
+
+ (window.hiddenBelow > 0 ? 1 : 0);
|
|
74
|
+
expect(rows).toBeLessThanOrEqual(FLEET_MAX_RENDER_ROWS);
|
|
75
|
+
if (agentCount > 0 && selected > 0) {
|
|
76
|
+
expect(selected - 1).toBeGreaterThanOrEqual(window.start);
|
|
77
|
+
expect(selected - 1).toBeLessThan(window.end);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("reserves indicator rows while keeping the selected middle agent visible", () => {
|
|
84
|
+
const window = calculateFleetAgentWindow(12, 6);
|
|
85
|
+
expect(window.hiddenAbove).toBeGreaterThan(0);
|
|
86
|
+
expect(window.hiddenBelow).toBeGreaterThan(0);
|
|
87
|
+
expect(5).toBeGreaterThanOrEqual(window.start);
|
|
88
|
+
expect(5).toBeLessThan(window.end);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("slides the visible window while navigating the full roster", () => {
|
|
92
|
+
const { render, input } = createFleetHarness(makeAgents(12));
|
|
93
|
+
input("\u001b[B");
|
|
94
|
+
for (let i = 0; i < 6; i++) input("\u001b[B");
|
|
95
|
+
const middle = render();
|
|
96
|
+
expect(middle.length).toBeLessThanOrEqual(FLEET_MAX_RENDER_ROWS);
|
|
97
|
+
expect(middle.some(line => /↑ \d+ more/.test(line))).toBe(true);
|
|
98
|
+
expect(middle.some(line => /↓ \d+ more/.test(line))).toBe(true);
|
|
99
|
+
expect(middle.some(line => line.includes("●"))).toBe(true);
|
|
100
|
+
|
|
101
|
+
input("\u001b[A");
|
|
102
|
+
const afterUp = render();
|
|
103
|
+
expect(afterUp.length).toBeLessThanOrEqual(FLEET_MAX_RENDER_ROWS);
|
|
104
|
+
expect(afterUp.some(line => line.includes("●"))).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("keeps the first and last windows bounded", () => {
|
|
108
|
+
const { render, input } = createFleetHarness(makeAgents(12));
|
|
109
|
+
expect(render().length).toBeLessThanOrEqual(FLEET_MAX_RENDER_ROWS);
|
|
110
|
+
input("\u001b[B");
|
|
111
|
+
for (let i = 0; i < 12; i++) input("\u001b[B");
|
|
112
|
+
const last = render();
|
|
113
|
+
expect(last.length).toBeLessThanOrEqual(FLEET_MAX_RENDER_ROWS);
|
|
114
|
+
expect(last.some(line => /↓ \d+ more/.test(line))).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("forces a full render once when the active roster changes", () => {
|
|
118
|
+
const { manager, fleet, tui } = createFleetHarness(makeAgents(5));
|
|
119
|
+
tui.requestRender.mockClear();
|
|
120
|
+
manager.records.pop();
|
|
121
|
+
fleet.update();
|
|
122
|
+
expect(tui.requestRender).toHaveBeenCalledWith(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("clears the widget when the active roster becomes empty", () => {
|
|
126
|
+
const { manager, fleet, ui } = createFleetHarness(makeAgents(1));
|
|
127
|
+
manager.records.splice(0);
|
|
128
|
+
fleet.update();
|
|
129
|
+
expect(ui.setWidget).toHaveBeenLastCalledWith("fleet", undefined);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm, writeFile } 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 {
|
|
6
|
+
executeWriteWithMetadata,
|
|
7
|
+
MAX_DURABLE_DIFF_BYTES,
|
|
8
|
+
WriteExecutionMetadataStore,
|
|
9
|
+
} from "../src/write-execution.js";
|
|
10
|
+
|
|
11
|
+
const tempDirs: string[] = [];
|
|
12
|
+
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true })));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
async function tempDir(): Promise<string> {
|
|
18
|
+
const dir = await mkdtemp(join(tmpdir(), "pi-subagents-write-test-"));
|
|
19
|
+
tempDirs.push(dir);
|
|
20
|
+
return dir;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("executeWriteWithMetadata", () => {
|
|
24
|
+
it("writes a new file and returns a durable display diff and patch", async () => {
|
|
25
|
+
const cwd = await tempDir();
|
|
26
|
+
const store = new WriteExecutionMetadataStore();
|
|
27
|
+
const result = await executeWriteWithMetadata(store, "call-new", {
|
|
28
|
+
path: "nested/file.txt",
|
|
29
|
+
content: "new\n",
|
|
30
|
+
}, undefined, cwd);
|
|
31
|
+
|
|
32
|
+
expect(await readFile(join(cwd, "nested/file.txt"), "utf8")).toBe("new\n");
|
|
33
|
+
expect(result.content[0]?.text).toContain("Successfully wrote to nested/file.txt");
|
|
34
|
+
expect(result.details?.diff).toContain("+1 new");
|
|
35
|
+
expect(result.details?.patch).toContain("+new");
|
|
36
|
+
expect(store.get("call-new")?.diff).toBe(result.details?.diff);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("returns durable details that remain independent of later file changes", async () => {
|
|
40
|
+
const cwd = await tempDir();
|
|
41
|
+
await writeFile(join(cwd, "file.txt"), "old\n", "utf8");
|
|
42
|
+
const store = new WriteExecutionMetadataStore();
|
|
43
|
+
const result = await executeWriteWithMetadata(store, "call-existing", {
|
|
44
|
+
path: "file.txt",
|
|
45
|
+
content: "new\n",
|
|
46
|
+
}, undefined, cwd);
|
|
47
|
+
const persisted = { ...result.details };
|
|
48
|
+
|
|
49
|
+
await writeFile(join(cwd, "file.txt"), "later\n", "utf8");
|
|
50
|
+
|
|
51
|
+
expect(persisted.diff).toContain("-1 old");
|
|
52
|
+
expect(persisted.diff).toContain("+1 new");
|
|
53
|
+
expect(persisted.patch).toContain("-old");
|
|
54
|
+
expect(persisted.patch).toContain("+new");
|
|
55
|
+
expect(persisted).not.toHaveProperty("previousContent");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("writes successfully but marks an oversized durable diff unavailable", async () => {
|
|
59
|
+
const cwd = await tempDir();
|
|
60
|
+
const store = new WriteExecutionMetadataStore();
|
|
61
|
+
const content = "x".repeat(MAX_DURABLE_DIFF_BYTES + 1);
|
|
62
|
+
const result = await executeWriteWithMetadata(store, "call-large", {
|
|
63
|
+
path: "large.txt",
|
|
64
|
+
content,
|
|
65
|
+
}, undefined, cwd);
|
|
66
|
+
|
|
67
|
+
expect((await readFile(join(cwd, "large.txt"), "utf8")).length).toBe(content.length);
|
|
68
|
+
expect(result.details?.diffUnavailableReason).toContain("durable diff exceeds");
|
|
69
|
+
expect(result.details).not.toHaveProperty("diff");
|
|
70
|
+
expect(store.get("call-large")?.previousContent).toBe("");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("does not fabricate metadata when the write fails", async () => {
|
|
74
|
+
const cwd = await tempDir();
|
|
75
|
+
const store = new WriteExecutionMetadataStore();
|
|
76
|
+
|
|
77
|
+
await expect(executeWriteWithMetadata(store, "call-fail", {
|
|
78
|
+
path: "/dev/null/pi-subagents-impossible/file.txt",
|
|
79
|
+
content: "nope",
|
|
80
|
+
}, undefined, cwd)).rejects.toBeTruthy();
|
|
81
|
+
expect(store.get("call-fail")).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"declarationMap": true,
|
|
9
|
+
"sourceMap": true,
|
|
10
|
+
"rootDir": "src",
|
|
11
|
+
"outDir": "dist",
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"noEmitOnError": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*.ts"]
|
|
16
|
+
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createMarkdownResult = createMarkdownResult;
|
|
4
|
-
const pi_coding_agent_1 = require("@earendil-works/pi-coding-agent");
|
|
5
|
-
const pi_tui_1 = require("@earendil-works/pi-tui");
|
|
6
|
-
const TRUNCATION_MARKER = "… (more output available)";
|
|
7
|
-
/**
|
|
8
|
-
* Render a child result as Markdown while keeping notification output bounded.
|
|
9
|
-
*
|
|
10
|
-
* Markdown is rendered at paint time so the TUI's current width controls wrapping.
|
|
11
|
-
* If the host's Markdown implementation is unavailable or throws, the original
|
|
12
|
-
* text is rendered through Text instead of making the result disappear.
|
|
13
|
-
*/
|
|
14
|
-
class CappedMarkdownResult {
|
|
15
|
-
text;
|
|
16
|
-
theme;
|
|
17
|
-
maxLines;
|
|
18
|
-
markdown;
|
|
19
|
-
constructor(text, theme, maxLines) {
|
|
20
|
-
this.text = text;
|
|
21
|
-
this.theme = theme;
|
|
22
|
-
this.maxLines = Math.max(0, Math.floor(maxLines));
|
|
23
|
-
try {
|
|
24
|
-
this.markdown = new pi_tui_1.Markdown(text, 0, 0, (0, pi_coding_agent_1.getMarkdownTheme)());
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
// A mismatched host Pi version should still leave the child output visible.
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
invalidate() {
|
|
31
|
-
this.markdown?.invalidate();
|
|
32
|
-
}
|
|
33
|
-
render(width) {
|
|
34
|
-
let lines;
|
|
35
|
-
try {
|
|
36
|
-
lines = this.markdown?.render(width) ?? new pi_tui_1.Text(this.text, 0, 0).render(width);
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
lines = new pi_tui_1.Text(this.text, 0, 0).render(width);
|
|
40
|
-
}
|
|
41
|
-
if (lines.length <= this.maxLines)
|
|
42
|
-
return lines;
|
|
43
|
-
if (this.maxLines === 0)
|
|
44
|
-
return [];
|
|
45
|
-
return [
|
|
46
|
-
...lines.slice(0, this.maxLines - 1),
|
|
47
|
-
this.theme.fg("muted", TRUNCATION_MARKER),
|
|
48
|
-
];
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
function createMarkdownResult(text, theme, maxLines) {
|
|
52
|
-
return new CappedMarkdownResult(text, theme, maxLines);
|
|
53
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Markdown, Text, type Component } from "@earendil-works/pi-tui";
|
|
3
|
-
import type { Theme } from "./agent-widget.js";
|
|
4
|
-
|
|
5
|
-
const TRUNCATION_MARKER = "… (more output available)";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Render a child result as Markdown while keeping notification output bounded.
|
|
9
|
-
*
|
|
10
|
-
* Markdown is rendered at paint time so the TUI's current width controls wrapping.
|
|
11
|
-
* If the host's Markdown implementation is unavailable or throws, the original
|
|
12
|
-
* text is rendered through Text instead of making the result disappear.
|
|
13
|
-
*/
|
|
14
|
-
class CappedMarkdownResult implements Component {
|
|
15
|
-
private readonly text: string;
|
|
16
|
-
private readonly theme: Theme;
|
|
17
|
-
private readonly maxLines: number;
|
|
18
|
-
private markdown: Component | undefined;
|
|
19
|
-
|
|
20
|
-
constructor(text: string, theme: Theme, maxLines: number) {
|
|
21
|
-
this.text = text;
|
|
22
|
-
this.theme = theme;
|
|
23
|
-
this.maxLines = Math.max(0, Math.floor(maxLines));
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
this.markdown = new Markdown(text, 0, 0, getMarkdownTheme());
|
|
27
|
-
} catch {
|
|
28
|
-
// A mismatched host Pi version should still leave the child output visible.
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
invalidate(): void {
|
|
33
|
-
this.markdown?.invalidate();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
render(width: number): string[] {
|
|
37
|
-
let lines: string[];
|
|
38
|
-
try {
|
|
39
|
-
lines = this.markdown?.render(width) ?? new Text(this.text, 0, 0).render(width);
|
|
40
|
-
} catch {
|
|
41
|
-
lines = new Text(this.text, 0, 0).render(width);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (lines.length <= this.maxLines) return lines;
|
|
45
|
-
if (this.maxLines === 0) return [];
|
|
46
|
-
|
|
47
|
-
return [
|
|
48
|
-
...lines.slice(0, this.maxLines - 1),
|
|
49
|
-
this.theme.fg("muted", TRUNCATION_MARKER),
|
|
50
|
-
];
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function createMarkdownResult(text: string, theme: Theme, maxLines: number): Component {
|
|
55
|
-
return new CappedMarkdownResult(text, theme, maxLines);
|
|
56
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
-
import {
|
|
6
|
-
agentHistoryLocator,
|
|
7
|
-
createAgentHistoryPath,
|
|
8
|
-
ensureSubagentsGitignore,
|
|
9
|
-
hasAgentHistory,
|
|
10
|
-
readAgentHistory,
|
|
11
|
-
resolveAgentHistoryPath,
|
|
12
|
-
} from "../src/agent-history.js";
|
|
13
|
-
import { writeInitialEntry } from "../src/output-file.js";
|
|
14
|
-
|
|
15
|
-
const tempDirectories: string[] = [];
|
|
16
|
-
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
for (const directory of tempDirectories.splice(0)) {
|
|
19
|
-
rmSync(directory, { recursive: true, force: true });
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
function tempProject(): string {
|
|
24
|
-
const path = mkdtempSync(join(tmpdir(), "pi-subagents-history-test-"));
|
|
25
|
-
tempDirectories.push(path);
|
|
26
|
-
return path;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
describe("project-local subagent history", () => {
|
|
30
|
-
it("creates a gitignore rule and preserves existing rules", () => {
|
|
31
|
-
const cwd = tempProject();
|
|
32
|
-
const root = join(cwd, ".pi-subagents");
|
|
33
|
-
const gitignore = join(root, ".gitignore");
|
|
34
|
-
const path = ensureSubagentsGitignore(cwd);
|
|
35
|
-
|
|
36
|
-
expect(path).toBe(gitignore);
|
|
37
|
-
expect(readFileSync(gitignore, "utf8")).toBe("*\n");
|
|
38
|
-
|
|
39
|
-
writeFileSync(gitignore, "keep-this\n", "utf8");
|
|
40
|
-
ensureSubagentsGitignore(cwd);
|
|
41
|
-
ensureSubagentsGitignore(cwd);
|
|
42
|
-
expect(readFileSync(gitignore, "utf8")).toBe("keep-this\n*\n");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("creates a bounded project-relative transcript locator", () => {
|
|
46
|
-
const cwd = tempProject();
|
|
47
|
-
const historyPath = createAgentHistoryPath(cwd, "agent-1", "Explore");
|
|
48
|
-
const locator = agentHistoryLocator(cwd, historyPath);
|
|
49
|
-
|
|
50
|
-
expect(locator).toBe(".pi-subagents/agent-transcripts/agent-1_Explore_transcript.jsonl");
|
|
51
|
-
expect(resolveAgentHistoryPath(cwd, locator)).toBe(historyPath);
|
|
52
|
-
expect(hasAgentHistory(cwd, locator)).toBe(false);
|
|
53
|
-
expect(hasAgentHistory(cwd, ".pi-subagents/other.jsonl")).toBe(false);
|
|
54
|
-
expect(resolveAgentHistoryPath(cwd, ".pi-subagents/other.jsonl")).toBeUndefined();
|
|
55
|
-
expect(resolveAgentHistoryPath(cwd, ".pi-subagents/agent-transcripts/../other.jsonl")).toBeUndefined();
|
|
56
|
-
|
|
57
|
-
writeInitialEntry(historyPath, "agent-1", "hello", cwd);
|
|
58
|
-
expect(hasAgentHistory(cwd, locator)).toBe(true);
|
|
59
|
-
const messages = readAgentHistory(cwd, locator);
|
|
60
|
-
expect(messages).toHaveLength(1);
|
|
61
|
-
expect(messages?.[0]).toMatchObject({ role: "user", content: "hello" });
|
|
62
|
-
});
|
|
63
|
-
});
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { createMarkdownResult } from "../../src/ui/markdown-result.js";
|
|
3
|
-
|
|
4
|
-
const theme = {
|
|
5
|
-
fg: (_name: string, text: string) => text,
|
|
6
|
-
bold: (text: string) => text,
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
describe("createMarkdownResult", () => {
|
|
10
|
-
it("renders headings, lists, inline code, and fenced code", () => {
|
|
11
|
-
const component = createMarkdownResult(
|
|
12
|
-
[
|
|
13
|
-
"# Result",
|
|
14
|
-
"",
|
|
15
|
-
"- first",
|
|
16
|
-
"- `inline()`",
|
|
17
|
-
"",
|
|
18
|
-
"```ts",
|
|
19
|
-
"const answer: number = 42;",
|
|
20
|
-
"```",
|
|
21
|
-
].join("\n"),
|
|
22
|
-
theme,
|
|
23
|
-
50,
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
const rendered = component.render(100).join("\n");
|
|
27
|
-
expect(rendered).toContain("Result");
|
|
28
|
-
expect(rendered).toContain("first");
|
|
29
|
-
expect(rendered).toContain("inline()");
|
|
30
|
-
expect(rendered).toContain("answer");
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("caps rendered lines and adds a truncation marker", () => {
|
|
34
|
-
const component = createMarkdownResult("line\n".repeat(20), theme, 5);
|
|
35
|
-
const rendered = component.render(80);
|
|
36
|
-
|
|
37
|
-
expect(rendered).toHaveLength(5);
|
|
38
|
-
expect(rendered.at(-1)).toContain("more");
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("keeps unlabelled code blocks safe without language guessing", () => {
|
|
42
|
-
const component = createMarkdownResult("```\nplain text\n```", theme, 20);
|
|
43
|
-
expect(component.render(80).join("\n")).toContain("plain text");
|
|
44
|
-
});
|
|
45
|
-
});
|