@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.
- package/CHANGELOG.md +37 -0
- package/package.json +26 -22
- package/packages/ask-user/package.json +2 -2
- package/packages/autocomplete/package.json +4 -2
- package/packages/btw/package.json +2 -2
- package/packages/cocoindex/package.json +2 -2
- package/packages/cocoindex/tools.ts +0 -8
- package/packages/compactor/README.md +18 -22
- package/packages/compactor/package.json +3 -3
- package/packages/compactor/src/compaction/build-sections.ts +2 -2
- package/packages/compactor/src/compaction/format.ts +1 -1
- package/packages/compactor/src/config/presets.ts +19 -30
- package/packages/compactor/src/index.ts +39 -56
- package/packages/compactor/src/session/resume-inject.ts +27 -0
- package/packages/compactor/src/tools/ctx-batch-execute.ts +4 -2
- package/packages/compactor/src/tools/ctx-execute-file.ts +4 -2
- package/packages/compactor/src/tools/ctx-execute.ts +4 -2
- package/packages/compactor/src/tools/register.ts +22 -4
- package/packages/compactor/src/tui/settings-overlay.ts +9 -22
- package/packages/compactor/src/types.ts +24 -2
- package/packages/core/events.ts +13 -13
- package/packages/core/package.json +3 -2
- package/packages/footer/README.md +2 -2
- package/packages/footer/package.json +3 -3
- package/packages/footer/src/events.ts +0 -10
- package/packages/footer/src/segments/core.ts +0 -9
- package/packages/image/package.json +2 -2
- package/packages/info-screen/core-groups.ts +0 -14
- package/packages/info-screen/package.json +2 -2
- package/packages/input-shortcuts/package.json +2 -2
- package/packages/kanboard/commands.ts +0 -1
- package/packages/kanboard/package.json +2 -2
- package/packages/mcp/package.json +2 -2
- package/packages/memory/package.json +3 -3
- package/packages/memory/storage.ts +0 -18
- package/packages/memory/tools.ts +0 -1
- package/packages/milestone/hooks.ts +9 -15
- package/packages/milestone/package.json +2 -2
- package/packages/notify/README.md +2 -0
- package/packages/notify/events.ts +23 -13
- package/packages/notify/package.json +2 -2
- package/packages/notify/tools.ts +17 -8
- package/packages/notify/types.ts +5 -1
- package/packages/ralph/package.json +3 -3
- package/packages/ralph/ralph-loop.ts +10 -0
- package/packages/subagents/README.md +3 -1
- package/packages/subagents/package.json +10 -6
- package/packages/subagents/src/agent-manager.ts +46 -11
- package/packages/subagents/src/core-compat.ts +38 -0
- package/packages/subagents/src/global.d.ts +7 -0
- package/packages/subagents/src/index.ts +6 -3
- package/packages/subagents/src/widget.ts +0 -7
- package/packages/unipi/bundled.js +364 -434
- package/packages/updater/package.json +4 -3
- package/packages/utility/README.md +1 -1
- package/packages/utility/package.json +7 -4
- package/packages/utility/src/commands.ts +2 -10
- package/packages/utility/src/diff/theme.ts +0 -3
- package/packages/utility/src/tui/name-badge.ts +0 -9
- package/packages/web-api/package.json +2 -2
- package/packages/web-api/src/engine/extract.ts +0 -26
- package/packages/web-api/src/tools.ts +0 -13
- package/packages/workflow/commands.ts +13 -1
- package/packages/workflow/index.ts +12 -1
- package/packages/workflow/lifecycle.ts +43 -0
- package/packages/workflow/package.json +3 -2
- package/packages/autocomplete/index.ts +0 -7
- package/packages/autocomplete/src/__tests__/command-registry.audit.test.ts +0 -120
- package/packages/autocomplete/src/__tests__/provider.sorting.test.ts +0 -423
- package/packages/compactor/src/compaction/recall-scope.ts +0 -32
- package/packages/compactor/src/compaction/sections.ts +0 -15
- package/packages/compactor/src/display/bash-display.ts +0 -28
- package/packages/compactor/src/display/diff-presentation.ts +0 -25
- package/packages/compactor/src/display/pending-diff-preview.ts +0 -51
- package/packages/compactor/src/display/user-message-box.ts +0 -16
- package/packages/compactor/src/session/analytics.ts +0 -205
- package/packages/image/index.ts +0 -1
- package/packages/input-shortcuts/index.ts +0 -5
- package/packages/input-shortcuts/src/status.ts +0 -35
- package/packages/kanboard/tui/kanboard-overlay.ts +0 -300
- package/packages/mcp/index.ts +0 -1
- package/packages/memory/search.ts +0 -134
- package/packages/milestone/coexist.ts +0 -114
- package/packages/notify/src/__tests__/ask-user-prompt-message.test.ts +0 -159
- package/packages/notify/src/__tests__/event-bus.test.ts +0 -119
- package/packages/notify/src/__tests__/permission-prompt-message.test.ts +0 -172
- package/packages/subagents/src/__tests__/badge-generation.test.ts +0 -315
- package/packages/subagents/src/__tests__/config.test.ts +0 -240
- package/packages/subagents/src/__tests__/esc-propagation.test.ts +0 -162
- package/packages/subagents/src/__tests__/file-lock.test.ts +0 -244
- package/packages/subagents/src/__tests__/shutdown-stale-ctx.test.ts +0 -185
- package/packages/subagents/src/__tests__/workflow-integration.test.ts +0 -334
- package/packages/subagents/src/prompts.ts +0 -39
- package/packages/utility/index.ts +0 -1
- package/packages/web-api/src/tui/progress.ts +0 -168
- 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
|
-
}
|