@pi-unipi/unipi 2.4.1 → 2.4.2

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.
Files changed (96) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/package.json +26 -22
  3. package/packages/ask-user/package.json +2 -2
  4. package/packages/autocomplete/package.json +4 -2
  5. package/packages/btw/package.json +2 -2
  6. package/packages/cocoindex/package.json +2 -2
  7. package/packages/cocoindex/tools.ts +0 -8
  8. package/packages/compactor/README.md +18 -22
  9. package/packages/compactor/package.json +3 -3
  10. package/packages/compactor/src/compaction/build-sections.ts +2 -2
  11. package/packages/compactor/src/compaction/format.ts +1 -1
  12. package/packages/compactor/src/config/presets.ts +19 -30
  13. package/packages/compactor/src/index.ts +39 -56
  14. package/packages/compactor/src/session/resume-inject.ts +27 -0
  15. package/packages/compactor/src/tools/ctx-batch-execute.ts +4 -2
  16. package/packages/compactor/src/tools/ctx-execute-file.ts +4 -2
  17. package/packages/compactor/src/tools/ctx-execute.ts +4 -2
  18. package/packages/compactor/src/tools/register.ts +22 -4
  19. package/packages/compactor/src/tui/settings-overlay.ts +9 -22
  20. package/packages/compactor/src/types.ts +24 -2
  21. package/packages/core/events.ts +13 -13
  22. package/packages/core/package.json +3 -2
  23. package/packages/footer/README.md +2 -2
  24. package/packages/footer/package.json +3 -3
  25. package/packages/footer/src/events.ts +0 -10
  26. package/packages/footer/src/segments/core.ts +0 -9
  27. package/packages/image/package.json +2 -2
  28. package/packages/info-screen/core-groups.ts +0 -14
  29. package/packages/info-screen/package.json +2 -2
  30. package/packages/input-shortcuts/package.json +2 -2
  31. package/packages/kanboard/commands.ts +0 -1
  32. package/packages/kanboard/package.json +2 -2
  33. package/packages/mcp/package.json +2 -2
  34. package/packages/memory/package.json +3 -3
  35. package/packages/memory/storage.ts +0 -18
  36. package/packages/memory/tools.ts +0 -1
  37. package/packages/milestone/hooks.ts +9 -15
  38. package/packages/milestone/package.json +2 -2
  39. package/packages/notify/README.md +2 -0
  40. package/packages/notify/events.ts +23 -13
  41. package/packages/notify/package.json +2 -2
  42. package/packages/notify/tools.ts +17 -8
  43. package/packages/notify/types.ts +5 -1
  44. package/packages/ralph/package.json +3 -3
  45. package/packages/ralph/ralph-loop.ts +10 -0
  46. package/packages/subagents/README.md +3 -1
  47. package/packages/subagents/package.json +10 -6
  48. package/packages/subagents/src/agent-manager.ts +46 -11
  49. package/packages/subagents/src/core-compat.ts +38 -0
  50. package/packages/subagents/src/global.d.ts +7 -0
  51. package/packages/subagents/src/index.ts +6 -3
  52. package/packages/subagents/src/widget.ts +0 -7
  53. package/packages/unipi/bundled.js +364 -434
  54. package/packages/updater/package.json +4 -3
  55. package/packages/utility/README.md +1 -1
  56. package/packages/utility/package.json +7 -4
  57. package/packages/utility/src/commands.ts +2 -10
  58. package/packages/utility/src/diff/theme.ts +0 -3
  59. package/packages/utility/src/tui/name-badge.ts +0 -9
  60. package/packages/web-api/package.json +2 -2
  61. package/packages/web-api/src/engine/extract.ts +0 -26
  62. package/packages/web-api/src/tools.ts +0 -13
  63. package/packages/workflow/commands.ts +13 -1
  64. package/packages/workflow/index.ts +12 -1
  65. package/packages/workflow/lifecycle.ts +43 -0
  66. package/packages/workflow/package.json +3 -2
  67. package/packages/autocomplete/index.ts +0 -7
  68. package/packages/autocomplete/src/__tests__/command-registry.audit.test.ts +0 -120
  69. package/packages/autocomplete/src/__tests__/provider.sorting.test.ts +0 -423
  70. package/packages/compactor/src/compaction/recall-scope.ts +0 -32
  71. package/packages/compactor/src/compaction/sections.ts +0 -15
  72. package/packages/compactor/src/display/bash-display.ts +0 -28
  73. package/packages/compactor/src/display/diff-presentation.ts +0 -25
  74. package/packages/compactor/src/display/pending-diff-preview.ts +0 -51
  75. package/packages/compactor/src/display/user-message-box.ts +0 -16
  76. package/packages/compactor/src/session/analytics.ts +0 -205
  77. package/packages/image/index.ts +0 -1
  78. package/packages/input-shortcuts/index.ts +0 -5
  79. package/packages/input-shortcuts/src/status.ts +0 -35
  80. package/packages/kanboard/tui/kanboard-overlay.ts +0 -300
  81. package/packages/mcp/index.ts +0 -1
  82. package/packages/memory/search.ts +0 -134
  83. package/packages/milestone/coexist.ts +0 -114
  84. package/packages/notify/src/__tests__/ask-user-prompt-message.test.ts +0 -159
  85. package/packages/notify/src/__tests__/event-bus.test.ts +0 -119
  86. package/packages/notify/src/__tests__/permission-prompt-message.test.ts +0 -172
  87. package/packages/subagents/src/__tests__/badge-generation.test.ts +0 -315
  88. package/packages/subagents/src/__tests__/config.test.ts +0 -240
  89. package/packages/subagents/src/__tests__/esc-propagation.test.ts +0 -162
  90. package/packages/subagents/src/__tests__/file-lock.test.ts +0 -244
  91. package/packages/subagents/src/__tests__/shutdown-stale-ctx.test.ts +0 -185
  92. package/packages/subagents/src/__tests__/workflow-integration.test.ts +0 -334
  93. package/packages/subagents/src/prompts.ts +0 -39
  94. package/packages/utility/index.ts +0 -1
  95. package/packages/web-api/src/tui/progress.ts +0 -168
  96. package/packages/web-api/src/tui/result.ts +0 -173
@@ -1,334 +0,0 @@
1
- /**
2
- * Test: Workflow integration — `/unipi:work` with subagent support
3
- *
4
- * Verifies:
5
- * - spawn_helper and get_helper_result tools are properly defined
6
- * - Agent types (explore, work) are correctly configured
7
- * - Concurrency limit is respected
8
- * - Custom agent type loading works
9
- * - System prompt builder generates correct prompts
10
- */
11
-
12
- import { describe, it } from "node:test";
13
- import assert from "node:assert/strict";
14
-
15
- // Test type definitions
16
- const BUILTIN_TYPES = ["explore", "work"] as const;
17
-
18
- interface AgentConfig {
19
- name: string;
20
- displayName?: string;
21
- description: string;
22
- builtinToolNames?: string[];
23
- disallowedTools?: string[];
24
- extensions: true | string[] | false;
25
- skills: true | string[] | false;
26
- model?: string;
27
- thinking?: string;
28
- maxTurns?: number;
29
- systemPrompt: string;
30
- promptMode: "replace" | "append";
31
- inheritContext?: boolean;
32
- runInBackground?: boolean;
33
- isolated?: boolean;
34
- memory?: string;
35
- isDefault?: boolean;
36
- enabled?: boolean;
37
- source?: "builtin" | "project" | "global";
38
- }
39
-
40
- // Test prompt builder
41
- function buildAgentPrompt(
42
- config: AgentConfig,
43
- cwd: string,
44
- env: { isGitRepo: boolean; branch: string; platform: string },
45
- parentSystemPrompt: string,
46
- ): string {
47
- if (config.promptMode === "append") {
48
- return [
49
- parentSystemPrompt,
50
- "",
51
- "---",
52
- "",
53
- `## Agent Role: ${config.displayName ?? config.name}`,
54
- config.systemPrompt,
55
- ].join("\n");
56
- }
57
-
58
- return [
59
- `# ${config.displayName ?? config.name}`,
60
- "",
61
- config.systemPrompt,
62
- "",
63
- "---",
64
- "",
65
- `Working directory: ${cwd}`,
66
- `Git: ${env.isGitRepo ? `${env.branch} on ${env.platform}` : "not a git repo"}`,
67
- ].join("\n");
68
- }
69
-
70
- // Test concurrency manager
71
- class ConcurrencyManager {
72
- private maxConcurrent: number;
73
- private running: number = 0;
74
- private queue: Array<{ id: string; resolve: () => void }> = [];
75
-
76
- constructor(maxConcurrent: number) {
77
- this.maxConcurrent = maxConcurrent;
78
- }
79
-
80
- async acquire(id: string): Promise<() => void> {
81
- if (this.running >= this.maxConcurrent) {
82
- await new Promise<void>((resolve) => {
83
- this.queue.push({ id, resolve });
84
- });
85
- }
86
-
87
- this.running++;
88
-
89
- let released = false;
90
- return () => {
91
- if (released) return;
92
- released = true;
93
- this.running--;
94
-
95
- // Start next in queue
96
- if (this.queue.length > 0) {
97
- const next = this.queue.shift()!;
98
- next.resolve();
99
- }
100
- };
101
- }
102
-
103
- getRunning(): number {
104
- return this.running;
105
- }
106
-
107
- getQueueLength(): number {
108
- return this.queue.length;
109
- }
110
- }
111
-
112
- describe("Workflow Integration", () => {
113
- describe("Tool Definitions", () => {
114
- it("should define spawn_helper tool with correct parameters", () => {
115
- const toolDef = {
116
- name: "spawn_helper",
117
- description: "Launch a sub-agent for parallel work.",
118
- parameters: {
119
- type: "object",
120
- required: ["type", "prompt", "description"],
121
- properties: {
122
- type: { type: "string" },
123
- prompt: { type: "string" },
124
- description: { type: "string" },
125
- run_in_background: { type: "boolean" },
126
- max_turns: { type: "number" },
127
- model: { type: "string" },
128
- thinking: { type: "string" },
129
- },
130
- },
131
- };
132
-
133
- assert.equal(toolDef.name, "spawn_helper");
134
- assert.equal(toolDef.parameters.required.length, 3);
135
- assert.ok(toolDef.parameters.properties.type);
136
- assert.ok(toolDef.parameters.properties.prompt);
137
- assert.ok(toolDef.parameters.properties.description);
138
- });
139
-
140
- it("should define get_helper_result tool with correct parameters", () => {
141
- const toolDef = {
142
- name: "get_helper_result",
143
- description: "Check status and retrieve results from a background agent.",
144
- parameters: {
145
- type: "object",
146
- required: ["agent_id"],
147
- properties: {
148
- agent_id: { type: "string" },
149
- wait: { type: "boolean" },
150
- },
151
- },
152
- };
153
-
154
- assert.equal(toolDef.name, "get_helper_result");
155
- assert.equal(toolDef.parameters.required.length, 1);
156
- assert.ok(toolDef.parameters.properties.agent_id);
157
- });
158
- });
159
-
160
- describe("Agent Types", () => {
161
- it("should have explore and work as builtin types", () => {
162
- assert.deepEqual(BUILTIN_TYPES, ["explore", "work"]);
163
- });
164
-
165
- it("should define explore agent with read-only tools", () => {
166
- const exploreConfig: AgentConfig = {
167
- name: "explore",
168
- description: "Fast parallel codebase exploration",
169
- builtinToolNames: ["read", "bash", "grep", "find", "ls"],
170
- systemPrompt: "You are a read-only exploration agent.",
171
- promptMode: "replace",
172
- extensions: true,
173
- skills: true,
174
- };
175
-
176
- assert.ok(!exploreConfig.builtinToolNames?.includes("write"));
177
- assert.ok(!exploreConfig.builtinToolNames?.includes("edit"));
178
- assert.ok(exploreConfig.builtinToolNames?.includes("read"));
179
- });
180
-
181
- it("should define work agent with read-write tools", () => {
182
- const workConfig: AgentConfig = {
183
- name: "work",
184
- description: "Parallel file writes with transparent locking",
185
- builtinToolNames: ["read", "write", "edit", "bash", "grep", "find", "ls"],
186
- systemPrompt: "You are a read-write work agent.",
187
- promptMode: "replace",
188
- extensions: true,
189
- skills: true,
190
- };
191
-
192
- assert.ok(workConfig.builtinToolNames?.includes("write"));
193
- assert.ok(workConfig.builtinToolNames?.includes("edit"));
194
- assert.ok(workConfig.builtinToolNames?.includes("read"));
195
- });
196
- });
197
-
198
- describe("System Prompt Builder", () => {
199
- const env = {
200
- isGitRepo: true,
201
- branch: "main",
202
- platform: "GitHub",
203
- };
204
-
205
- it("should build replace mode prompt", () => {
206
- const config: AgentConfig = {
207
- name: "explore",
208
- displayName: "Explorer",
209
- description: "Test",
210
- systemPrompt: "Find all authentication files.",
211
- promptMode: "replace",
212
- extensions: true,
213
- skills: true,
214
- };
215
-
216
- const prompt = buildAgentPrompt(config, "/workspace", env, "Parent prompt");
217
-
218
- assert.ok(prompt.startsWith("# Explorer"));
219
- assert.ok(prompt.includes("Find all authentication files."));
220
- assert.ok(prompt.includes("Working directory: /workspace"));
221
- assert.ok(!prompt.includes("Parent prompt"));
222
- });
223
-
224
- it("should build append mode prompt", () => {
225
- const config: AgentConfig = {
226
- name: "work",
227
- displayName: "Worker",
228
- description: "Test",
229
- systemPrompt: "Refactor the auth module.",
230
- promptMode: "append",
231
- extensions: true,
232
- skills: true,
233
- };
234
-
235
- const prompt = buildAgentPrompt(config, "/workspace", env, "Parent prompt");
236
-
237
- assert.ok(prompt.startsWith("Parent prompt"));
238
- assert.ok(prompt.includes("## Agent Role: Worker"));
239
- assert.ok(prompt.includes("Refactor the auth module."));
240
- });
241
- });
242
-
243
- describe("Concurrency Limit", () => {
244
- it("should respect max concurrent limit", async () => {
245
- const manager = new ConcurrencyManager(2);
246
- const events: string[] = [];
247
-
248
- // Start 3 agents, only 2 should run immediately
249
- const release1 = await manager.acquire("agent-1");
250
- events.push("agent-1-started");
251
-
252
- const release2 = await manager.acquire("agent-2");
253
- events.push("agent-2-started");
254
-
255
- assert.equal(manager.getRunning(), 2);
256
- assert.deepEqual(events, ["agent-1-started", "agent-2-started"]);
257
-
258
- // Third agent should queue
259
- const acquire3Promise = manager.acquire("agent-3").then((release) => {
260
- events.push("agent-3-started");
261
- return release;
262
- });
263
-
264
- assert.equal(manager.getQueueLength(), 1);
265
-
266
- // Release first agent
267
- release1();
268
- const release3 = await acquire3Promise;
269
-
270
- assert.equal(manager.getRunning(), 2);
271
- assert.deepEqual(events, ["agent-1-started", "agent-2-started", "agent-3-started"]);
272
-
273
- release2();
274
- release3();
275
- });
276
-
277
- it("should queue agents in order", async () => {
278
- const manager = new ConcurrencyManager(1);
279
- const events: string[] = [];
280
-
281
- const release1 = await manager.acquire("agent-1");
282
- events.push("1");
283
-
284
- const p2 = manager.acquire("agent-2").then(r => { events.push("2"); return r; });
285
- const p3 = manager.acquire("agent-3").then(r => { events.push("3"); return r; });
286
-
287
- release1();
288
- const release2 = await p2;
289
- release2();
290
- const release3 = await p3;
291
-
292
- assert.deepEqual(events, ["1", "2", "3"]);
293
- release3();
294
- });
295
- });
296
-
297
- describe("Custom Agent Loading", () => {
298
- it("should parse agent markdown frontmatter", () => {
299
- const markdown = `---
300
- name: code-checker
301
- description: Code quality checker
302
- tools: read, grep, find, bash
303
- thinking: high
304
- ---
305
- You are a code quality checker. Review code for issues.`;
306
-
307
- // Simple frontmatter parser
308
- const match = markdown.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
309
- assert.ok(match);
310
-
311
- const frontmatter = match![1];
312
- const body = match![2];
313
-
314
- assert.ok(frontmatter.includes("name: code-checker"));
315
- assert.ok(frontmatter.includes("tools: read, grep, find, bash"));
316
- assert.ok(body.includes("You are a code quality checker."));
317
- });
318
-
319
- it("should validate required fields", () => {
320
- const validAgent = {
321
- name: "test-agent",
322
- description: "Test agent",
323
- systemPrompt: "Do something.",
324
- promptMode: "replace" as const,
325
- extensions: true as const,
326
- skills: true as const,
327
- };
328
-
329
- assert.ok(validAgent.name);
330
- assert.ok(validAgent.description);
331
- assert.ok(validAgent.systemPrompt);
332
- });
333
- });
334
- });
@@ -1,39 +0,0 @@
1
- /**
2
- * @pi-unipi/subagents — System prompt builder
3
- */
4
-
5
- import type { AgentConfig } from "./types.js";
6
-
7
- /**
8
- * Build system prompt for an agent.
9
- */
10
- export function buildAgentPrompt(
11
- config: AgentConfig,
12
- cwd: string,
13
- env: { isGitRepo: boolean; branch: string; platform: string },
14
- parentSystemPrompt: string,
15
- ): string {
16
- if (config.promptMode === "append") {
17
- // Append mode: parent prompt + agent additions
18
- return [
19
- parentSystemPrompt,
20
- "",
21
- "---",
22
- "",
23
- `## Agent Role: ${config.displayName ?? config.name}`,
24
- config.systemPrompt,
25
- ].join("\n");
26
- }
27
-
28
- // Replace mode: standalone prompt
29
- return [
30
- `# ${config.displayName ?? config.name}`,
31
- "",
32
- config.systemPrompt,
33
- "",
34
- "---",
35
- "",
36
- `Working directory: ${cwd}`,
37
- `Git: ${env.isGitRepo ? `${env.branch} on ${env.platform}` : "not a git repo"}`,
38
- ].join("\n");
39
- }
@@ -1 +0,0 @@
1
- export { default } from "./src/index.js";
@@ -1,168 +0,0 @@
1
- /**
2
- * @unipi/web-api — TUI Progress Renderer
3
- *
4
- * Renders batch fetch progress for TUI display.
5
- */
6
-
7
- import type { FetchProgress, FetchProgressStatus } from "../engine/types.js";
8
-
9
- /** Spinner frames for animation */
10
- const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
11
-
12
- /** Status glyphs */
13
- const STATUS_GLYPHS: Record<FetchProgressStatus, string> = {
14
- queued: "○",
15
- connecting: SPINNER_FRAMES[0],
16
- waiting: SPINNER_FRAMES[0],
17
- loading: SPINNER_FRAMES[0],
18
- processing: SPINNER_FRAMES[0],
19
- done: "✓",
20
- error: "✗",
21
- };
22
-
23
- /**
24
- * Get a spinner frame for the given index.
25
- * Cycles through spinner frames for animation.
26
- *
27
- * @param index - Animation frame index
28
- * @returns Spinner character
29
- */
30
- export function getSpinnerFrame(index: number): string {
31
- return SPINNER_FRAMES[index % SPINNER_FRAMES.length];
32
- }
33
-
34
- /**
35
- * Render a progress bar.
36
- *
37
- * @param percent - Progress percentage (0-100)
38
- * @param width - Bar width in characters
39
- * @returns Progress bar string
40
- */
41
- export function renderProgressBar(percent: number, width: number = 10): string {
42
- const filled = Math.round((percent / 100) * width);
43
- const empty = width - filled;
44
- return "█".repeat(filled) + "░".repeat(empty);
45
- }
46
-
47
- /**
48
- * Truncate a URL for display.
49
- *
50
- * @param url - URL to truncate
51
- * @param maxLength - Maximum length
52
- * @returns Truncated URL
53
- */
54
- function truncateUrl(url: string, maxLength: number): string {
55
- if (url.length <= maxLength) {
56
- return url;
57
- }
58
-
59
- // Try to keep the domain
60
- try {
61
- const parsed = new URL(url);
62
- const domain = parsed.host;
63
- const path = parsed.pathname + parsed.search;
64
-
65
- if (domain.length + 3 >= maxLength) {
66
- return url.slice(0, maxLength - 1) + "…";
67
- }
68
-
69
- const remaining = maxLength - domain.length - 3;
70
- if (path.length <= remaining) {
71
- return domain + path;
72
- }
73
-
74
- return domain + path.slice(0, remaining - 1) + "…";
75
- } catch {
76
- return url.slice(0, maxLength - 1) + "…";
77
- }
78
- }
79
-
80
- /**
81
- * Render a single progress item line.
82
- *
83
- * @param progress - Progress object
84
- * @param width - Available width
85
- * @param spinnerIndex - Animation frame index
86
- * @returns Formatted line
87
- */
88
- export function renderProgressLine(
89
- progress: FetchProgress,
90
- width: number = 80,
91
- spinnerIndex: number = 0
92
- ): string {
93
- // Status glyph
94
- let glyph = STATUS_GLYPHS[progress.status];
95
- if (["connecting", "waiting", "loading", "processing"].includes(progress.status)) {
96
- glyph = getSpinnerFrame(spinnerIndex);
97
- }
98
-
99
- // Truncate URL
100
- const urlMax = Math.min(40, width - 30);
101
- const url = truncateUrl(progress.url, urlMax);
102
-
103
- // Progress bar
104
- const bar = renderProgressBar(progress.percent, 8);
105
-
106
- // Status text
107
- const statusText = progress.phase || progress.status;
108
-
109
- // Format line
110
- return `${glyph} ${url.padEnd(urlMax)} ${statusText.padEnd(12)} [${bar}]`;
111
- }
112
-
113
- /**
114
- * Render batch progress header.
115
- *
116
- * @param progress - All progress items
117
- * @param concurrency - Current concurrency
118
- * @returns Header line
119
- */
120
- export function renderBatchProgressHeader(
121
- progress: FetchProgress[],
122
- concurrency: number
123
- ): string {
124
- const total = progress.length;
125
- const done = progress.filter((p) => p.status === "done").length;
126
- const error = progress.filter((p) => p.status === "error").length;
127
- const active = progress.filter(
128
- (p) => !["queued", "done", "error"].includes(p.status)
129
- ).length;
130
-
131
- return `batch_web_content_read ${done}/${total} done · ok ${done - error} · err ${error} · concurrency ${concurrency}`;
132
- }
133
-
134
- /**
135
- * Render full batch progress display.
136
- *
137
- * @param progress - All progress items
138
- * @param concurrency - Current concurrency
139
- * @param width - Available width
140
- * @param spinnerIndex - Animation frame index
141
- * @returns Formatted string
142
- */
143
- export function renderBatchProgress(
144
- progress: FetchProgress[],
145
- concurrency: number = 8,
146
- width: number = 80,
147
- spinnerIndex: number = 0
148
- ): string {
149
- const lines: string[] = [];
150
-
151
- // Header
152
- lines.push(renderBatchProgressHeader(progress, concurrency));
153
- lines.push("");
154
-
155
- // Progress items (show up to 10)
156
- const maxItems = 10;
157
- const itemsToShow = progress.slice(0, maxItems);
158
-
159
- for (const item of itemsToShow) {
160
- lines.push(renderProgressLine(item, width, spinnerIndex));
161
- }
162
-
163
- if (progress.length > maxItems) {
164
- lines.push(` ... and ${progress.length - maxItems} more`);
165
- }
166
-
167
- return lines.join("\n");
168
- }
@@ -1,173 +0,0 @@
1
- /**
2
- * @unipi/web-api — TUI Result Renderer
3
- *
4
- * Renders single and batch results for TUI display.
5
- */
6
-
7
- import type { FetchResult, BatchFetchResult, FetchError } from "../engine/types.js";
8
-
9
- /** Maximum preview lines */
10
- const PREVIEW_LINES = 7;
11
-
12
- /**
13
- * Truncate content for preview.
14
- *
15
- * @param content - Content to preview
16
- * @param maxLines - Maximum lines
17
- * @returns Preview string
18
- */
19
- function truncatePreview(content: string, maxLines: number = PREVIEW_LINES): string {
20
- const lines = content.split("\n").slice(0, maxLines);
21
- return lines.join("\n");
22
- }
23
-
24
- /**
25
- * Render a single result for display.
26
- *
27
- * @param result - Fetch result
28
- * @param verbose - Include metadata header
29
- * @returns Formatted string
30
- */
31
- export function renderSingleResult(
32
- result: FetchResult,
33
- verbose: boolean = true
34
- ): string {
35
- const lines: string[] = [];
36
-
37
- if (verbose) {
38
- // Title
39
- lines.push(`# ${result.title || "Untitled"}`);
40
- lines.push("");
41
-
42
- // Metadata
43
- const meta: string[] = [];
44
- if (result.author) {
45
- meta.push(`Author: ${result.author}`);
46
- }
47
- if (result.published) {
48
- meta.push(`Published: ${result.published}`);
49
- }
50
- if (result.site) {
51
- meta.push(`Site: ${result.site}`);
52
- }
53
- if (result.language) {
54
- meta.push(`Language: ${result.language}`);
55
- }
56
- if (result.wordCount) {
57
- meta.push(`Words: ${result.wordCount}`);
58
- }
59
-
60
- if (meta.length > 0) {
61
- lines.push(meta.join(" · "));
62
- }
63
-
64
- // URL
65
- lines.push(`URL: ${result.url}`);
66
- if (result.finalUrl !== result.url) {
67
- lines.push(`Final URL: ${result.finalUrl}`);
68
- }
69
-
70
- lines.push("");
71
- lines.push("---");
72
- lines.push("");
73
- }
74
-
75
- // Content preview
76
- const preview = truncatePreview(result.content);
77
- lines.push(preview);
78
-
79
- // Expand hint
80
- if (result.content.split("\n").length > PREVIEW_LINES) {
81
- lines.push("");
82
- lines.push(`... [${result.wordCount} words total · Ctrl+O to expand]`);
83
- }
84
-
85
- return lines.join("\n");
86
- }
87
-
88
- /**
89
- * Render a batch result for display.
90
- *
91
- * @param result - Batch fetch result
92
- * @returns Formatted string
93
- */
94
- export function renderBatchResult(result: BatchFetchResult): string {
95
- const lines: string[] = [];
96
-
97
- // Summary header
98
- lines.push(`# Batch Read Results`);
99
- lines.push("");
100
- lines.push(
101
- `Total: ${result.total} · Succeeded: ${result.succeeded} · Failed: ${result.failed}`
102
- );
103
- lines.push("");
104
-
105
- // Per-item results
106
- for (let i = 0; i < result.items.length; i++) {
107
- const item = result.items[i];
108
- const status = item.status === "done" ? "✓" : "✗";
109
-
110
- lines.push(`## [${i + 1}/${result.total}] ${status}`);
111
-
112
- if (item.status === "done") {
113
- lines.push(`**${item.result.title}**`);
114
- lines.push(`URL: ${item.result.url}`);
115
- lines.push(`Words: ${item.result.wordCount}`);
116
- lines.push("");
117
-
118
- // Content preview
119
- const preview = truncatePreview(item.result.content);
120
- lines.push(preview);
121
-
122
- if (item.result.content.split("\n").length > PREVIEW_LINES) {
123
- lines.push("...");
124
- }
125
- } else {
126
- lines.push(`URL: ${item.error.url || "unknown"}`);
127
- lines.push(`Error: ${item.error.error}`);
128
- }
129
-
130
- lines.push("");
131
- }
132
-
133
- return lines.join("\n");
134
- }
135
-
136
- /**
137
- * Render an error result for display.
138
- *
139
- * @param error - Fetch error
140
- * @returns Formatted string
141
- */
142
- export function renderErrorResult(error: FetchError): string {
143
- const lines: string[] = [];
144
-
145
- lines.push(`# Fetch Error`);
146
- lines.push("");
147
- lines.push(`**${error.error}**`);
148
- lines.push("");
149
- lines.push(`Code: \`${error.code}\``);
150
- lines.push(`Phase: \`${error.phase}\``);
151
-
152
- if (error.url) {
153
- lines.push("");
154
- lines.push(`URL: ${error.url}`);
155
- if (error.finalUrl && error.finalUrl !== error.url) {
156
- lines.push(`Final URL: ${error.finalUrl}`);
157
- }
158
- }
159
-
160
- if (error.statusCode) {
161
- lines.push("");
162
- lines.push(
163
- `HTTP Status: ${error.statusCode}${error.statusText ? ` ${error.statusText}` : ""}`
164
- );
165
- }
166
-
167
- if (error.retryable) {
168
- lines.push("");
169
- lines.push(`*This error may be retried.*`);
170
- }
171
-
172
- return lines.join("\n");
173
- }