@tintinweb/pi-subagents 0.6.2 → 0.7.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 +28 -0
- package/README.md +54 -10
- package/dist/agent-manager.d.ts +23 -1
- package/dist/agent-manager.js +33 -2
- package/dist/agent-runner.d.ts +27 -0
- package/dist/agent-runner.js +35 -17
- package/dist/default-agents.js +2 -9
- package/dist/index.js +199 -50
- package/dist/schedule-store.d.ts +36 -0
- package/dist/schedule-store.js +144 -0
- package/dist/schedule.d.ts +109 -0
- package/dist/schedule.js +338 -0
- package/dist/settings.d.ts +10 -0
- package/dist/settings.js +5 -0
- package/dist/types.d.ts +46 -0
- package/dist/ui/agent-widget.d.ts +15 -8
- package/dist/ui/agent-widget.js +28 -7
- package/dist/ui/conversation-viewer.js +6 -8
- package/dist/ui/schedule-menu.d.ts +16 -0
- package/dist/ui/schedule-menu.js +95 -0
- package/dist/usage.d.ts +50 -0
- package/dist/usage.js +49 -0
- package/package.json +10 -6
- package/src/agent-manager.ts +55 -2
- package/src/agent-runner.ts +49 -18
- package/src/default-agents.ts +2 -9
- package/src/index.ts +207 -41
- package/src/schedule-store.ts +143 -0
- package/src/schedule.ts +365 -0
- package/src/settings.ts +14 -0
- package/src/types.ts +52 -0
- package/src/ui/agent-widget.ts +36 -6
- package/src/ui/conversation-viewer.ts +6 -6
- package/src/ui/schedule-menu.ts +104 -0
- package/src/usage.ts +60 -0
- package/.github/workflows/ci.yml +0 -21
- package/biome.json +0 -26
- package/dist/ui/conversation-viewer.test.d.ts +0 -1
- package/dist/ui/conversation-viewer.test.js +0 -254
package/src/usage.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/** usage.ts — Token usage: shapes, accumulator operators, session-stats readers. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lifetime usage components, accumulated via `message_end` events. Survives
|
|
5
|
+
* compaction (which replaces session.state.messages and would reset any
|
|
6
|
+
* stats-derived sum). cacheRead is excluded because each turn's cacheRead is
|
|
7
|
+
* the cumulative cached prefix re-read on that one call — summing across
|
|
8
|
+
* turns counts the prefix N times. See issue #38.
|
|
9
|
+
*/
|
|
10
|
+
export type LifetimeUsage = { input: number; output: number; cacheWrite: number };
|
|
11
|
+
|
|
12
|
+
/** Sum of lifetime usage components, or 0 if undefined. */
|
|
13
|
+
export function getLifetimeTotal(u?: LifetimeUsage): number {
|
|
14
|
+
return u ? u.input + u.output + u.cacheWrite : 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Add a usage delta into a target accumulator (mutates target). */
|
|
18
|
+
export function addUsage(into: LifetimeUsage, delta: LifetimeUsage): void {
|
|
19
|
+
into.input += delta.input;
|
|
20
|
+
into.output += delta.output;
|
|
21
|
+
into.cacheWrite += delta.cacheWrite;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Minimal shape we read from upstream `getSessionStats()`. */
|
|
25
|
+
export type SessionStatsLike = {
|
|
26
|
+
tokens: { input: number; output: number; cacheWrite: number };
|
|
27
|
+
contextUsage?: { percent: number | null };
|
|
28
|
+
};
|
|
29
|
+
export type SessionLike = { getSessionStats(): SessionStatsLike };
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Session-scoped token count: input + output + cacheWrite as reported by
|
|
33
|
+
* upstream `getSessionStats().tokens` for the *current* session window.
|
|
34
|
+
*
|
|
35
|
+
* RESETS at compaction — upstream replaces `session.state.messages` and the
|
|
36
|
+
* stats are derived from that array. For a lifetime total that survives
|
|
37
|
+
* compaction, use `getLifetimeTotal(lifetimeUsage)` instead, which reads
|
|
38
|
+
* from an independent accumulator fed by `message_end` events.
|
|
39
|
+
*
|
|
40
|
+
* Avoids upstream's `tokens.total` field, which sums per-turn `cacheRead`
|
|
41
|
+
* and so counts the cumulative cached prefix N times across N turns
|
|
42
|
+
* (issue #38).
|
|
43
|
+
*/
|
|
44
|
+
export function getSessionTokens(session: SessionLike | undefined): number {
|
|
45
|
+
if (!session) return 0;
|
|
46
|
+
try {
|
|
47
|
+
const t = session.getSessionStats().tokens;
|
|
48
|
+
return t.input + t.output + t.cacheWrite;
|
|
49
|
+
} catch { return 0; }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Context-window utilization (0–100), or null when unavailable
|
|
54
|
+
* (no model contextWindow, or post-compaction before the next response).
|
|
55
|
+
*/
|
|
56
|
+
export function getSessionContextPercent(session: SessionLike | undefined): number | null {
|
|
57
|
+
if (!session) return null;
|
|
58
|
+
try { return session.getSessionStats().contextUsage?.percent ?? null; }
|
|
59
|
+
catch { return null; }
|
|
60
|
+
}
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [master]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [master]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
build:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
steps:
|
|
13
|
-
- uses: actions/checkout@v4
|
|
14
|
-
- uses: actions/setup-node@v4
|
|
15
|
-
with:
|
|
16
|
-
node-version: 20
|
|
17
|
-
cache: npm
|
|
18
|
-
- run: npm ci
|
|
19
|
-
- run: npm run lint
|
|
20
|
-
- run: npm run typecheck
|
|
21
|
-
- run: npm run test
|
package/biome.json
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://biomejs.dev/schemas/2.4.8/schema.json",
|
|
3
|
-
"linter": {
|
|
4
|
-
"enabled": true,
|
|
5
|
-
"rules": {
|
|
6
|
-
"recommended": true,
|
|
7
|
-
"style": {
|
|
8
|
-
"recommended": false
|
|
9
|
-
},
|
|
10
|
-
"suspicious": {
|
|
11
|
-
"noExplicitAny": "off",
|
|
12
|
-
"noControlCharactersInRegex": "off",
|
|
13
|
-
"noEmptyInterface": "off"
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
},
|
|
17
|
-
"formatter": {
|
|
18
|
-
"enabled": false
|
|
19
|
-
},
|
|
20
|
-
"files": {
|
|
21
|
-
"includes": [
|
|
22
|
-
"src/**/*.ts",
|
|
23
|
-
"test/**/*.ts"
|
|
24
|
-
]
|
|
25
|
-
}
|
|
26
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,254 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
// ── Mock wrapTextWithAnsi ──────────────────────────────────────────────
|
|
3
|
-
// We need to control what wrapTextWithAnsi returns to simulate the
|
|
4
|
-
// upstream bug (returning lines wider than requested width).
|
|
5
|
-
// vi.mock is hoisted and intercepts before conversation-viewer.ts binds
|
|
6
|
-
// its import.
|
|
7
|
-
let wrapOverride = null;
|
|
8
|
-
vi.mock("@mariozechner/pi-tui", async (importOriginal) => {
|
|
9
|
-
const original = await importOriginal();
|
|
10
|
-
return {
|
|
11
|
-
...original,
|
|
12
|
-
wrapTextWithAnsi: (...args) => {
|
|
13
|
-
if (wrapOverride)
|
|
14
|
-
return wrapOverride(...args);
|
|
15
|
-
return original.wrapTextWithAnsi(...args);
|
|
16
|
-
},
|
|
17
|
-
};
|
|
18
|
-
});
|
|
19
|
-
// Must import AFTER vi.mock declaration (vitest hoists vi.mock but the
|
|
20
|
-
// dynamic import of the test subject must happen after)
|
|
21
|
-
const { visibleWidth } = await import("@mariozechner/pi-tui");
|
|
22
|
-
const { ConversationViewer } = await import("./conversation-viewer.js");
|
|
23
|
-
// ── Helpers ────────────────────────────────────────────────────────────
|
|
24
|
-
function mockTui(rows = 40, columns = 80) {
|
|
25
|
-
return {
|
|
26
|
-
terminal: { rows, columns },
|
|
27
|
-
requestRender: vi.fn(),
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
function mockSession(messages = []) {
|
|
31
|
-
return {
|
|
32
|
-
messages,
|
|
33
|
-
subscribe: vi.fn(() => vi.fn()),
|
|
34
|
-
dispose: vi.fn(),
|
|
35
|
-
getSessionStats: () => ({ tokens: { total: 0 } }),
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
function mockRecord(overrides = {}) {
|
|
39
|
-
return {
|
|
40
|
-
id: "test-1",
|
|
41
|
-
type: "general-purpose",
|
|
42
|
-
description: "test agent",
|
|
43
|
-
status: "running",
|
|
44
|
-
toolUses: 0,
|
|
45
|
-
startedAt: Date.now(),
|
|
46
|
-
...overrides,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
function ansiTheme() {
|
|
50
|
-
return {
|
|
51
|
-
fg: (_color, text) => `\x1b[38;5;240m${text}\x1b[0m`,
|
|
52
|
-
bold: (text) => `\x1b[1m${text}\x1b[22m`,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
function assertAllLinesFit(lines, width) {
|
|
56
|
-
for (let i = 0; i < lines.length; i++) {
|
|
57
|
-
const vw = visibleWidth(lines[i]);
|
|
58
|
-
expect(vw, `line ${i} exceeds width (${vw} > ${width}): ${JSON.stringify(lines[i])}`).toBeLessThanOrEqual(width);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
// ── Tests ──────────────────────────────────────────────────────────────
|
|
62
|
-
beforeEach(() => {
|
|
63
|
-
wrapOverride = null;
|
|
64
|
-
});
|
|
65
|
-
describe("ConversationViewer", () => {
|
|
66
|
-
describe("render width safety", () => {
|
|
67
|
-
const widths = [40, 80, 120, 216];
|
|
68
|
-
it("no line exceeds width with empty messages", () => {
|
|
69
|
-
for (const w of widths) {
|
|
70
|
-
const viewer = new ConversationViewer(mockTui(30, w), mockSession([]), mockRecord(), undefined, ansiTheme(), vi.fn());
|
|
71
|
-
assertAllLinesFit(viewer.render(w), w);
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
it("no line exceeds width with plain text messages", () => {
|
|
75
|
-
const messages = [
|
|
76
|
-
{ role: "user", content: "Hello, how are you?" },
|
|
77
|
-
{ role: "assistant", content: [{ type: "text", text: "I am fine, thank you for asking." }] },
|
|
78
|
-
];
|
|
79
|
-
for (const w of widths) {
|
|
80
|
-
const viewer = new ConversationViewer(mockTui(30, w), mockSession(messages), mockRecord(), undefined, ansiTheme(), vi.fn());
|
|
81
|
-
assertAllLinesFit(viewer.render(w), w);
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
it("no line exceeds width when text is longer than viewport", () => {
|
|
85
|
-
const longLine = "A".repeat(500);
|
|
86
|
-
const messages = [
|
|
87
|
-
{ role: "user", content: longLine },
|
|
88
|
-
{ role: "assistant", content: [{ type: "text", text: longLine }] },
|
|
89
|
-
{ role: "toolResult", toolUseId: "t1", content: [{ type: "text", text: longLine }] },
|
|
90
|
-
];
|
|
91
|
-
for (const w of widths) {
|
|
92
|
-
const viewer = new ConversationViewer(mockTui(30, w), mockSession(messages), mockRecord(), undefined, ansiTheme(), vi.fn());
|
|
93
|
-
assertAllLinesFit(viewer.render(w), w);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
it("no line exceeds width with embedded ANSI escape codes in content", () => {
|
|
97
|
-
const ansiText = `\x1b[1mBold heading\x1b[22m and \x1b[31mred text\x1b[0m ${"X".repeat(300)}`;
|
|
98
|
-
const messages = [
|
|
99
|
-
{ role: "toolResult", toolUseId: "t1", content: [{ type: "text", text: ansiText }] },
|
|
100
|
-
];
|
|
101
|
-
for (const w of widths) {
|
|
102
|
-
const viewer = new ConversationViewer(mockTui(30, w), mockSession(messages), mockRecord(), undefined, ansiTheme(), vi.fn());
|
|
103
|
-
assertAllLinesFit(viewer.render(w), w);
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
it("no line exceeds width with long URLs", () => {
|
|
107
|
-
const url = "https://example.com/" + "a/b/c/d/e/".repeat(30) + "?q=" + "x".repeat(100);
|
|
108
|
-
const messages = [
|
|
109
|
-
{ role: "assistant", content: [{ type: "text", text: `Check this link: ${url}` }] },
|
|
110
|
-
];
|
|
111
|
-
for (const w of widths) {
|
|
112
|
-
const viewer = new ConversationViewer(mockTui(30, w), mockSession(messages), mockRecord(), undefined, ansiTheme(), vi.fn());
|
|
113
|
-
assertAllLinesFit(viewer.render(w), w);
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
it("no line exceeds width with wide table-like content", () => {
|
|
117
|
-
const header = "| " + Array.from({ length: 20 }, (_, i) => `Column${i}`).join(" | ") + " |";
|
|
118
|
-
const dataRow = "| " + Array.from({ length: 20 }, () => "value123").join(" | ") + " |";
|
|
119
|
-
const table = [header, dataRow, dataRow, dataRow].join("\n");
|
|
120
|
-
const messages = [
|
|
121
|
-
{ role: "toolResult", toolUseId: "t1", content: [{ type: "text", text: table }] },
|
|
122
|
-
];
|
|
123
|
-
for (const w of widths) {
|
|
124
|
-
const viewer = new ConversationViewer(mockTui(30, w), mockSession(messages), mockRecord(), undefined, ansiTheme(), vi.fn());
|
|
125
|
-
assertAllLinesFit(viewer.render(w), w);
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
it("no line exceeds width with bashExecution messages", () => {
|
|
129
|
-
const messages = [
|
|
130
|
-
{
|
|
131
|
-
role: "bashExecution", command: "cat " + "/very/long/path/".repeat(20) + "file.txt",
|
|
132
|
-
output: "O".repeat(600),
|
|
133
|
-
exitCode: 0, cancelled: false, truncated: false, timestamp: Date.now(),
|
|
134
|
-
},
|
|
135
|
-
];
|
|
136
|
-
for (const w of widths) {
|
|
137
|
-
const viewer = new ConversationViewer(mockTui(30, w), mockSession(messages), mockRecord(), undefined, ansiTheme(), vi.fn());
|
|
138
|
-
assertAllLinesFit(viewer.render(w), w);
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
it("no line exceeds width with running activity indicator", () => {
|
|
142
|
-
const activity = {
|
|
143
|
-
activeTools: new Map([["read", "file.ts"], ["grep", "pattern"]]),
|
|
144
|
-
toolUses: 5, tokens: "10k", responseText: "R".repeat(400),
|
|
145
|
-
session: { getSessionStats: () => ({ tokens: { total: 50000 } }) },
|
|
146
|
-
};
|
|
147
|
-
const messages = [
|
|
148
|
-
{ role: "user", content: "do the thing" },
|
|
149
|
-
{ role: "assistant", content: [{ type: "text", text: "working on it" }] },
|
|
150
|
-
];
|
|
151
|
-
for (const w of widths) {
|
|
152
|
-
const viewer = new ConversationViewer(mockTui(30, w), mockSession(messages), mockRecord({ status: "running" }), activity, ansiTheme(), vi.fn());
|
|
153
|
-
assertAllLinesFit(viewer.render(w), w);
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
it("no line exceeds width with tool calls", () => {
|
|
157
|
-
const messages = [
|
|
158
|
-
{
|
|
159
|
-
role: "assistant",
|
|
160
|
-
content: [
|
|
161
|
-
{ type: "text", text: "Let me check that." },
|
|
162
|
-
{ type: "toolCall", toolUseId: "t1", name: "very_long_tool_name_" + "x".repeat(200), input: {} },
|
|
163
|
-
],
|
|
164
|
-
},
|
|
165
|
-
];
|
|
166
|
-
for (const w of widths) {
|
|
167
|
-
const viewer = new ConversationViewer(mockTui(30, w), mockSession(messages), mockRecord(), undefined, ansiTheme(), vi.fn());
|
|
168
|
-
assertAllLinesFit(viewer.render(w), w);
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
it("no line exceeds width at narrow terminal", () => {
|
|
172
|
-
const messages = [
|
|
173
|
-
{ role: "user", content: "Hello world, this is a normal sentence." },
|
|
174
|
-
{ role: "assistant", content: [{ type: "text", text: "Sure, here's the answer." }] },
|
|
175
|
-
];
|
|
176
|
-
for (const w of [8, 10, 15, 20]) {
|
|
177
|
-
const viewer = new ConversationViewer(mockTui(30, w), mockSession(messages), mockRecord(), undefined, ansiTheme(), vi.fn());
|
|
178
|
-
assertAllLinesFit(viewer.render(w), w);
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
it("no line exceeds width with mixed ANSI + unicode content", () => {
|
|
182
|
-
const text = `\x1b[32m✓\x1b[0m Test passed — 日本語テスト ${"あ".repeat(50)} \x1b[33m⚠\x1b[0m`;
|
|
183
|
-
const messages = [
|
|
184
|
-
{ role: "toolResult", toolUseId: "t1", content: [{ type: "text", text }] },
|
|
185
|
-
];
|
|
186
|
-
for (const w of widths) {
|
|
187
|
-
const viewer = new ConversationViewer(mockTui(30, w), mockSession(messages), mockRecord(), undefined, ansiTheme(), vi.fn());
|
|
188
|
-
assertAllLinesFit(viewer.render(w), w);
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
describe("safety net against upstream wrapTextWithAnsi bugs", () => {
|
|
193
|
-
// These tests call buildContentLines() directly (via the private method)
|
|
194
|
-
// because render() has its own truncation via row(). The safety net in
|
|
195
|
-
// buildContentLines is what prevents the TUI crash — it must clamp
|
|
196
|
-
// independently of render().
|
|
197
|
-
/** Call the private buildContentLines method directly. */
|
|
198
|
-
function callBuildContentLines(viewer, width) {
|
|
199
|
-
return viewer.buildContentLines(width);
|
|
200
|
-
}
|
|
201
|
-
it("mock is intercepting wrapTextWithAnsi", async () => {
|
|
202
|
-
const { wrapTextWithAnsi } = await import("@mariozechner/pi-tui");
|
|
203
|
-
wrapOverride = () => ["MOCK_SENTINEL"];
|
|
204
|
-
expect(wrapTextWithAnsi("anything", 10)).toEqual(["MOCK_SENTINEL"]);
|
|
205
|
-
wrapOverride = null;
|
|
206
|
-
});
|
|
207
|
-
it("clamps overwidth lines from toolResult content", () => {
|
|
208
|
-
const w = 80;
|
|
209
|
-
wrapOverride = () => ["X".repeat(w + 50)];
|
|
210
|
-
const messages = [
|
|
211
|
-
{ role: "toolResult", toolUseId: "t1", content: [{ type: "text", text: "output" }] },
|
|
212
|
-
];
|
|
213
|
-
const viewer = new ConversationViewer(mockTui(30, w), mockSession(messages), mockRecord(), undefined, ansiTheme(), vi.fn());
|
|
214
|
-
assertAllLinesFit(callBuildContentLines(viewer, w), w);
|
|
215
|
-
});
|
|
216
|
-
it("clamps overwidth lines from user message content", () => {
|
|
217
|
-
const w = 80;
|
|
218
|
-
wrapOverride = () => ["Y".repeat(w + 100)];
|
|
219
|
-
const messages = [{ role: "user", content: "hello" }];
|
|
220
|
-
const viewer = new ConversationViewer(mockTui(30, w), mockSession(messages), mockRecord(), undefined, ansiTheme(), vi.fn());
|
|
221
|
-
assertAllLinesFit(callBuildContentLines(viewer, w), w);
|
|
222
|
-
});
|
|
223
|
-
it("clamps overwidth lines from assistant message content", () => {
|
|
224
|
-
const w = 80;
|
|
225
|
-
wrapOverride = () => ["Z".repeat(w + 100)];
|
|
226
|
-
const messages = [
|
|
227
|
-
{ role: "assistant", content: [{ type: "text", text: "response" }] },
|
|
228
|
-
];
|
|
229
|
-
const viewer = new ConversationViewer(mockTui(30, w), mockSession(messages), mockRecord(), undefined, ansiTheme(), vi.fn());
|
|
230
|
-
assertAllLinesFit(callBuildContentLines(viewer, w), w);
|
|
231
|
-
});
|
|
232
|
-
it("clamps overwidth lines from bashExecution output", () => {
|
|
233
|
-
const w = 80;
|
|
234
|
-
wrapOverride = () => ["B".repeat(w + 100)];
|
|
235
|
-
const messages = [
|
|
236
|
-
{
|
|
237
|
-
role: "bashExecution", command: "ls", output: "out",
|
|
238
|
-
exitCode: 0, cancelled: false, truncated: false, timestamp: Date.now(),
|
|
239
|
-
},
|
|
240
|
-
];
|
|
241
|
-
const viewer = new ConversationViewer(mockTui(30, w), mockSession(messages), mockRecord(), undefined, ansiTheme(), vi.fn());
|
|
242
|
-
assertAllLinesFit(callBuildContentLines(viewer, w), w);
|
|
243
|
-
});
|
|
244
|
-
it("clamps overwidth lines that also contain ANSI codes", () => {
|
|
245
|
-
const w = 80;
|
|
246
|
-
wrapOverride = () => [`\x1b[1m\x1b[31m${"W".repeat(w + 30)}\x1b[0m`];
|
|
247
|
-
const messages = [
|
|
248
|
-
{ role: "toolResult", toolUseId: "t1", content: [{ type: "text", text: "output" }] },
|
|
249
|
-
];
|
|
250
|
-
const viewer = new ConversationViewer(mockTui(30, w), mockSession(messages), mockRecord(), undefined, ansiTheme(), vi.fn());
|
|
251
|
-
assertAllLinesFit(callBuildContentLines(viewer, w), w);
|
|
252
|
-
});
|
|
253
|
-
});
|
|
254
|
-
});
|