pikiclaw 0.2.62 → 0.2.64
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/README.md +40 -1
- package/dist/agent-auto-update.js +5 -4
- package/dist/bot-feishu-render.js +27 -100
- package/dist/bot-feishu.js +2 -1
- package/dist/bot-orchestration.js +2 -1
- package/dist/bot-render-shared.js +162 -0
- package/dist/bot-telegram-live-preview.js +4 -3
- package/dist/bot-telegram-render.js +25 -123
- package/dist/bot.js +4 -3
- package/dist/channel-feishu.js +5 -4
- package/dist/channel-telegram.js +5 -4
- package/dist/cli.js +191 -115
- package/dist/code-agent.js +8 -5
- package/dist/config-validation.js +3 -2
- package/dist/constants.js +227 -0
- package/dist/dashboard-platform.js +333 -0
- package/dist/dashboard-routes-agent.js +375 -0
- package/dist/dashboard-routes-config.js +342 -0
- package/dist/dashboard-ui.js +7 -7
- package/dist/dashboard.js +65 -877
- package/dist/driver-claude.js +3 -2
- package/dist/driver-codex.js +234 -198
- package/dist/driver-gemini.js +5 -4
- package/dist/mcp-bridge.js +10 -9
- package/dist/setup-wizard.js +2 -1
- package/dist/user-config.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
> npx pikiclaw@latest
|
|
10
10
|
|
|
11
|
+
<img src="docs/promo-install.gif" alt="Quick install" width="700">
|
|
12
|
+
|
|
11
13
|
<p align="center">
|
|
12
14
|
<a href="https://www.npmjs.com/package/pikiclaw"><img src="https://img.shields.io/npm/v/pikiclaw" alt="npm"></a>
|
|
13
15
|
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
|
|
@@ -45,7 +47,20 @@ pikiclaw 的目标很直接:
|
|
|
45
47
|
你的电脑
|
|
46
48
|
```
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
它适合的不是”演示一次 AI”,而是你离开电脑以后,Agent 还能继续在本机把事做完。
|
|
51
|
+
|
|
52
|
+
### 在 Telegram 里长这样
|
|
53
|
+
|
|
54
|
+
<table>
|
|
55
|
+
<tr>
|
|
56
|
+
<td align=”center”><b>命令与 Agent 切换</b><br><img src=”docs/promo-tg-commands.png” alt=”Commands” width=”320”></td>
|
|
57
|
+
<td align=”center”><b>代码审查</b><br><img src=”docs/promo-tg-task.png” alt=”Code review” width=”320”></td>
|
|
58
|
+
</tr>
|
|
59
|
+
<tr>
|
|
60
|
+
<td align=”center”><b>多轮编码 + 文件回传</b><br><img src=”docs/promo-tg-complex.png” alt=”Complex task” width=”320”></td>
|
|
61
|
+
<td align=”center”><b>状态监控 + 会话管理</b><br><img src=”docs/promo-tg-sessions.png” alt=”Sessions” width=”320”></td>
|
|
62
|
+
</tr>
|
|
63
|
+
</table>
|
|
49
64
|
|
|
50
65
|
---
|
|
51
66
|
|
|
@@ -76,6 +91,23 @@ npx pikiclaw@latest
|
|
|
76
91
|
- 工作目录切换
|
|
77
92
|
- 会话和运行状态查看
|
|
78
93
|
|
|
94
|
+
<details>
|
|
95
|
+
<summary>Dashboard 截图</summary>
|
|
96
|
+
|
|
97
|
+
**配置管理** — IM 接入、AI Agent、系统权限
|
|
98
|
+
|
|
99
|
+
<img src="docs/promo-dashboard-config.png" alt="Config" width="700">
|
|
100
|
+
|
|
101
|
+
**插件中心** — 浏览器操控、桌面自动化
|
|
102
|
+
|
|
103
|
+
<img src="docs/promo-dashboard-extensions.png" alt="Extensions" width="700">
|
|
104
|
+
|
|
105
|
+
**会话管理** — 按 Agent 分组的会话泳道
|
|
106
|
+
|
|
107
|
+
<img src="docs/promo-dashboard-sessions.png" alt="Sessions" width="700">
|
|
108
|
+
|
|
109
|
+
</details>
|
|
110
|
+
|
|
79
111
|
如果你更喜欢终端向导:
|
|
80
112
|
|
|
81
113
|
```bash
|
|
@@ -152,6 +184,13 @@ npx pikiclaw@latest --doctor
|
|
|
152
184
|
|
|
153
185
|
普通文本消息会直接转给当前 Agent。
|
|
154
186
|
|
|
187
|
+
<details>
|
|
188
|
+
<summary>Telegram 命令效果预览</summary>
|
|
189
|
+
|
|
190
|
+
<img src="docs/promo-tg-commands.png" alt="Commands in Telegram" width="360">
|
|
191
|
+
|
|
192
|
+
</details>
|
|
193
|
+
|
|
155
194
|
---
|
|
156
195
|
|
|
157
196
|
## Config And Setup Notes
|
|
@@ -3,8 +3,9 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { getAgentLabel, getAgentPackage } from './agent-npm.js';
|
|
6
|
-
|
|
7
|
-
const
|
|
6
|
+
import { AGENT_UPDATE_TIMEOUTS } from './constants.js';
|
|
7
|
+
const AGENT_UPDATE_LOCK_STALE_MS = AGENT_UPDATE_TIMEOUTS.lockStale;
|
|
8
|
+
const AGENT_UPDATE_COMMAND_TIMEOUT_MS = AGENT_UPDATE_TIMEOUTS.commandTimeout;
|
|
8
9
|
function updaterLockPath() {
|
|
9
10
|
return path.join(os.homedir(), '.pikiclaw', 'agent-auto-update.lock');
|
|
10
11
|
}
|
|
@@ -95,11 +96,11 @@ async function runCommand(cmd, args, opts = {}) {
|
|
|
95
96
|
});
|
|
96
97
|
}
|
|
97
98
|
async function getNpmGlobalPrefix() {
|
|
98
|
-
const result = await runCommand('npm', ['prefix', '-g'], { timeoutMs:
|
|
99
|
+
const result = await runCommand('npm', ['prefix', '-g'], { timeoutMs: AGENT_UPDATE_TIMEOUTS.npmPrefix });
|
|
99
100
|
return result.ok ? result.stdout.trim().split('\n')[0] || null : null;
|
|
100
101
|
}
|
|
101
102
|
async function getLatestPackageVersion(pkg) {
|
|
102
|
-
const result = await runCommand('npm', ['view', pkg, 'version', '--json'], { timeoutMs:
|
|
103
|
+
const result = await runCommand('npm', ['view', pkg, 'version', '--json'], { timeoutMs: AGENT_UPDATE_TIMEOUTS.npmView });
|
|
103
104
|
if (!result.ok)
|
|
104
105
|
return null;
|
|
105
106
|
const raw = result.stdout.trim();
|
|
@@ -5,64 +5,21 @@
|
|
|
5
5
|
* Also provides a LivePreviewRenderer for streaming output.
|
|
6
6
|
*/
|
|
7
7
|
import { encodeCommandAction } from './bot-command-ui.js';
|
|
8
|
-
import { fmtUptime, fmtTokens, fmtBytes
|
|
8
|
+
import { fmtUptime, fmtTokens, fmtBytes } from './bot.js';
|
|
9
9
|
import { summarizePromptForStatus } from './bot-commands.js';
|
|
10
|
-
import {
|
|
11
|
-
import { formatActivityCommandSummary, parseActivitySummary, renderPlanForPreview, summarizeActivityForPreview } from './bot-streaming.js';
|
|
10
|
+
import { footerStatusSymbol, formatFooterSummary, trimActivityForPreview, buildProviderUsageLines, extractFinalReplyData, extractStreamPreviewData, } from './bot-render-shared.js';
|
|
12
11
|
import { currentHumanLoopQuestion, humanLoopAnsweredCount, isHumanLoopAwaitingText, isHumanLoopQuestionAnswered, summarizeHumanLoopAnswer, } from './human-loop.js';
|
|
13
12
|
import path from 'node:path';
|
|
14
13
|
import { listSubdirs } from './bot.js';
|
|
15
14
|
// ---------------------------------------------------------------------------
|
|
16
15
|
// Helpers
|
|
17
16
|
// ---------------------------------------------------------------------------
|
|
18
|
-
function fmtCompactUptime(ms) {
|
|
19
|
-
return fmtUptime(ms).replace(/\s+/g, '');
|
|
20
|
-
}
|
|
21
|
-
function footerStatusSymbol(status) {
|
|
22
|
-
switch (status) {
|
|
23
|
-
case 'running': return '●';
|
|
24
|
-
case 'done': return '✓';
|
|
25
|
-
case 'failed': return '✗';
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
function formatFooterSummary(agent, elapsedMs, meta, contextPercent) {
|
|
29
|
-
const parts = [agent];
|
|
30
|
-
const ctx = contextPercent ?? meta?.contextPercent ?? null;
|
|
31
|
-
if (ctx != null)
|
|
32
|
-
parts.push(`${ctx}%`);
|
|
33
|
-
parts.push(fmtCompactUptime(Math.max(0, Math.round(elapsedMs))));
|
|
34
|
-
return parts.join(' · ');
|
|
35
|
-
}
|
|
36
17
|
function formatPreviewFooter(agent, elapsedMs, meta) {
|
|
37
18
|
return `${footerStatusSymbol('running')} ${formatFooterSummary(agent, elapsedMs, meta)}`;
|
|
38
19
|
}
|
|
39
20
|
function formatFinalFooter(status, agent, elapsedMs, contextPercent) {
|
|
40
21
|
return `${footerStatusSymbol(status)} ${formatFooterSummary(agent, elapsedMs, null, contextPercent ?? null)}`;
|
|
41
22
|
}
|
|
42
|
-
function trimActivityForPreview(text, maxChars = 900) {
|
|
43
|
-
if (text.length <= maxChars)
|
|
44
|
-
return text;
|
|
45
|
-
const lines = text.split('\n').filter(l => l.trim());
|
|
46
|
-
if (lines.length <= 1)
|
|
47
|
-
return text.slice(0, Math.max(0, maxChars - 3)).trimEnd() + '...';
|
|
48
|
-
const tailCount = Math.min(2, Math.max(1, lines.length - 1));
|
|
49
|
-
const tail = lines.slice(-tailCount);
|
|
50
|
-
const headCandidates = lines.slice(0, Math.max(0, lines.length - tailCount));
|
|
51
|
-
const reserved = tail.join('\n').length + 5;
|
|
52
|
-
const budget = Math.max(0, maxChars - reserved);
|
|
53
|
-
const head = [];
|
|
54
|
-
let used = 0;
|
|
55
|
-
for (const line of headCandidates) {
|
|
56
|
-
const extra = line.length + (head.length ? 1 : 0);
|
|
57
|
-
if (used + extra > budget)
|
|
58
|
-
break;
|
|
59
|
-
head.push(line);
|
|
60
|
-
used += extra;
|
|
61
|
-
}
|
|
62
|
-
if (!head.length)
|
|
63
|
-
return text.slice(0, Math.max(0, maxChars - 3)).trimEnd() + '...';
|
|
64
|
-
return [...head, '...', ...tail].join('\n');
|
|
65
|
-
}
|
|
66
23
|
function truncateLabel(label, maxChars = 24) {
|
|
67
24
|
return label.length > maxChars ? `${label.slice(0, Math.max(1, maxChars - 1))}…` : label;
|
|
68
25
|
}
|
|
@@ -212,31 +169,22 @@ export function buildInitialPreviewMarkdown(agent, model, effort, waiting = fals
|
|
|
212
169
|
return parts.join(' · ');
|
|
213
170
|
}
|
|
214
171
|
function buildPreviewMarkdown(input, options) {
|
|
215
|
-
const
|
|
216
|
-
const display = input.bodyText.trim();
|
|
217
|
-
const rawThinking = input.thinking.trim();
|
|
218
|
-
const thinkDisplay = formatThinkingForDisplay(input.thinking, maxBody);
|
|
219
|
-
const planDisplay = renderPlanForPreview(input.plan ?? null);
|
|
220
|
-
const activityDisplay = summarizeActivityForPreview(input.activity);
|
|
221
|
-
const maxActivity = !display && !thinkDisplay && !planDisplay ? 2400 : 1400;
|
|
172
|
+
const data = extractStreamPreviewData(input);
|
|
222
173
|
const parts = [];
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
parts.push(`**Plan**\n${planDisplay}`);
|
|
174
|
+
if (data.planDisplay) {
|
|
175
|
+
parts.push(`**Plan**\n${data.planDisplay}`);
|
|
226
176
|
}
|
|
227
|
-
if (activityDisplay) {
|
|
228
|
-
parts.push(`**Activity**\n${trimActivityForPreview(activityDisplay, maxActivity)}`);
|
|
177
|
+
if (data.activityDisplay) {
|
|
178
|
+
parts.push(`**Activity**\n${trimActivityForPreview(data.activityDisplay, data.maxActivity)}`);
|
|
229
179
|
}
|
|
230
|
-
if (thinkDisplay && !display) {
|
|
231
|
-
parts.push(`**${label}**\n${thinkDisplay}`);
|
|
180
|
+
if (data.thinkDisplay && !data.display) {
|
|
181
|
+
parts.push(`**${data.label}**\n${data.thinkDisplay}`);
|
|
232
182
|
}
|
|
233
|
-
else if (display) {
|
|
234
|
-
if (rawThinking) {
|
|
235
|
-
|
|
236
|
-
parts.push(`**${label}**\n${thinkSnippet}`);
|
|
183
|
+
else if (data.display) {
|
|
184
|
+
if (data.rawThinking) {
|
|
185
|
+
parts.push(`**${data.label}**\n${data.thinkSnippet}`);
|
|
237
186
|
}
|
|
238
|
-
|
|
239
|
-
parts.push(preview);
|
|
187
|
+
parts.push(data.preview);
|
|
240
188
|
}
|
|
241
189
|
if (options?.includeFooter !== false) {
|
|
242
190
|
parts.push(formatPreviewFooter(input.agent, input.elapsedMs, input.meta ?? null));
|
|
@@ -260,46 +208,26 @@ export const feishuStreamingPreviewRenderer = {
|
|
|
260
208
|
renderStream: buildStreamingBodyMarkdown,
|
|
261
209
|
};
|
|
262
210
|
export function buildFinalReplyRender(agent, result) {
|
|
263
|
-
const
|
|
264
|
-
const footerText = `\n\n${formatFinalFooter(footerStatus, agent,
|
|
211
|
+
const data = extractFinalReplyData(agent, result);
|
|
212
|
+
const footerText = `\n\n${formatFinalFooter(data.footerStatus, agent, data.elapsedMs, result.contextPercent ?? null)}`;
|
|
265
213
|
let activityText = '';
|
|
266
214
|
let activityNoteText = '';
|
|
267
|
-
if (
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (display.length > 1600)
|
|
273
|
-
display = '...\n' + display.slice(-1600);
|
|
274
|
-
activityText = `**Activity**\n${display}\n\n`;
|
|
275
|
-
}
|
|
276
|
-
const commandSummary = formatActivityCommandSummary(summary.completedCommands, summary.activeCommands, summary.failedCommands);
|
|
277
|
-
if (commandSummary)
|
|
278
|
-
activityNoteText = `*${commandSummary}*\n\n`;
|
|
215
|
+
if (data.activityNarrative) {
|
|
216
|
+
activityText = `**Activity**\n${data.activityNarrative}\n\n`;
|
|
217
|
+
}
|
|
218
|
+
if (data.activityCommandSummary) {
|
|
219
|
+
activityNoteText = `*${data.activityCommandSummary}*\n\n`;
|
|
279
220
|
}
|
|
280
221
|
let thinkingText = '';
|
|
281
|
-
if (
|
|
282
|
-
thinkingText = `**${thinkLabel
|
|
222
|
+
if (data.thinkingDisplay) {
|
|
223
|
+
thinkingText = `**${data.thinkLabel}**\n${data.thinkingDisplay}\n\n`;
|
|
283
224
|
}
|
|
284
225
|
let statusText = '';
|
|
285
|
-
if (
|
|
286
|
-
|
|
287
|
-
if (result.stopReason === 'max_tokens')
|
|
288
|
-
statusLines.push('Output limit reached. Response may be truncated.');
|
|
289
|
-
if (result.stopReason === 'timeout') {
|
|
290
|
-
statusLines.push(`Timed out after ${fmtUptime(Math.max(0, Math.round(result.elapsedS * 1000)))} before the agent reported completion.`);
|
|
291
|
-
}
|
|
292
|
-
if (!result.ok) {
|
|
293
|
-
const detail = result.error?.trim();
|
|
294
|
-
if (detail && detail !== result.message.trim() && !statusLines.includes(detail))
|
|
295
|
-
statusLines.push(detail);
|
|
296
|
-
else if (result.stopReason !== 'timeout')
|
|
297
|
-
statusLines.push('Agent exited before reporting completion.');
|
|
298
|
-
}
|
|
299
|
-
statusText = `**⚠ Incomplete Response**\n${statusLines.join('\n')}\n\n`;
|
|
226
|
+
if (data.statusLines) {
|
|
227
|
+
statusText = `**⚠ Incomplete Response**\n${data.statusLines.join('\n')}\n\n`;
|
|
300
228
|
}
|
|
301
229
|
const headerText = `${activityText}${activityNoteText}${statusText}${thinkingText}`;
|
|
302
|
-
const bodyText =
|
|
230
|
+
const bodyText = data.bodyMessage;
|
|
303
231
|
return {
|
|
304
232
|
fullText: `${headerText}${bodyText}${footerText}`,
|
|
305
233
|
headerText,
|
|
@@ -474,12 +402,11 @@ export function renderStatus(d) {
|
|
|
474
402
|
lines.push(`**Running:** ${fmtUptime(Date.now() - d.running.startedAt)} - ${summarizePromptForStatus(d.running.prompt)}`);
|
|
475
403
|
}
|
|
476
404
|
// Provider usage
|
|
477
|
-
const usageLines =
|
|
405
|
+
const usageLines = buildProviderUsageLines(d.usage);
|
|
478
406
|
if (usageLines.length > 1) {
|
|
479
407
|
lines.push('');
|
|
480
|
-
// Strip HTML tags from usage lines (they're HTML-formatted)
|
|
481
408
|
for (const line of usageLines) {
|
|
482
|
-
lines.push(line.
|
|
409
|
+
lines.push(line.text);
|
|
483
410
|
}
|
|
484
411
|
}
|
|
485
412
|
lines.push('', '**Bot Usage**', ` Turns: ${d.stats.totalTurns}`);
|
package/dist/bot-feishu.js
CHANGED
|
@@ -24,6 +24,7 @@ import { currentHumanLoopQuestion, humanLoopOptionSelected } from './human-loop.
|
|
|
24
24
|
import { FeishuChannel } from './channel-feishu.js';
|
|
25
25
|
import { splitText, supportsChannelCapability } from './channel-base.js';
|
|
26
26
|
import { getActiveUserConfig } from './user-config.js';
|
|
27
|
+
import { FEISHU_BOT_CARD_MAX } from './constants.js';
|
|
27
28
|
const SHUTDOWN_EXIT_CODE = {
|
|
28
29
|
SIGINT: 130,
|
|
29
30
|
SIGTERM: 143,
|
|
@@ -640,7 +641,7 @@ export class FeishuBot extends Bot {
|
|
|
640
641
|
async sendFinalReply(ctx, placeholderId, agent, result) {
|
|
641
642
|
const rendered = buildFinalReplyRender(agent, result);
|
|
642
643
|
const messageIds = [];
|
|
643
|
-
const MAX_CARD =
|
|
644
|
+
const MAX_CARD = FEISHU_BOT_CARD_MAX;
|
|
644
645
|
if (rendered.fullText.length <= MAX_CARD) {
|
|
645
646
|
// Fits in one card — edit the placeholder
|
|
646
647
|
if (placeholderId) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buildDefaultMenuCommands } from './bot-menu.js';
|
|
2
|
-
|
|
2
|
+
import { BOT_SHUTDOWN_FORCE_EXIT_MS as _BOT_SHUTDOWN_FORCE_EXIT_MS } from './constants.js';
|
|
3
|
+
export const BOT_SHUTDOWN_FORCE_EXIT_MS = _BOT_SHUTDOWN_FORCE_EXIT_MS;
|
|
3
4
|
export function buildBotMenuState(bot) {
|
|
4
5
|
const agents = bot.fetchAgents().agents;
|
|
5
6
|
const installedCount = agents.filter(agent => agent.installed).length;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bot-render-shared.ts — Shared rendering logic used by both Telegram and Feishu renderers.
|
|
3
|
+
*
|
|
4
|
+
* Contains types, pure-data helpers, and functions that are identical across platforms.
|
|
5
|
+
* Platform-specific formatting (HTML vs Markdown) stays in the respective render files.
|
|
6
|
+
*/
|
|
7
|
+
import { fmtUptime, formatThinkingForDisplay, thinkLabel } from './bot.js';
|
|
8
|
+
import { formatActivityCommandSummary, parseActivitySummary, renderPlanForPreview, summarizeActivityForPreview } from './bot-streaming.js';
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Footer helpers
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
export function fmtCompactUptime(ms) {
|
|
13
|
+
return fmtUptime(ms).replace(/\s+/g, '');
|
|
14
|
+
}
|
|
15
|
+
export function footerStatusSymbol(status) {
|
|
16
|
+
switch (status) {
|
|
17
|
+
case 'running': return '●';
|
|
18
|
+
case 'done': return '✓';
|
|
19
|
+
case 'failed': return '✗';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function formatFooterSummary(agent, elapsedMs, meta, contextPercent) {
|
|
23
|
+
const parts = [agent];
|
|
24
|
+
const ctx = contextPercent ?? meta?.contextPercent ?? null;
|
|
25
|
+
if (ctx != null)
|
|
26
|
+
parts.push(`${ctx}%`);
|
|
27
|
+
parts.push(fmtCompactUptime(Math.max(0, Math.round(elapsedMs))));
|
|
28
|
+
return parts.join(' · ');
|
|
29
|
+
}
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Activity trimming
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
export function trimActivityForPreview(text, maxChars = 900) {
|
|
34
|
+
if (text.length <= maxChars)
|
|
35
|
+
return text;
|
|
36
|
+
const lines = text.split('\n').filter(line => line.trim());
|
|
37
|
+
if (lines.length <= 1)
|
|
38
|
+
return text.slice(0, Math.max(0, maxChars - 3)).trimEnd() + '...';
|
|
39
|
+
const tailCount = Math.min(2, Math.max(1, lines.length - 1));
|
|
40
|
+
const tail = lines.slice(-tailCount);
|
|
41
|
+
const headCandidates = lines.slice(0, Math.max(0, lines.length - tailCount));
|
|
42
|
+
const reserved = tail.join('\n').length + 5;
|
|
43
|
+
const budget = Math.max(0, maxChars - reserved);
|
|
44
|
+
const head = [];
|
|
45
|
+
let used = 0;
|
|
46
|
+
for (const line of headCandidates) {
|
|
47
|
+
const extra = line.length + (head.length ? 1 : 0);
|
|
48
|
+
if (used + extra > budget)
|
|
49
|
+
break;
|
|
50
|
+
head.push(line);
|
|
51
|
+
used += extra;
|
|
52
|
+
}
|
|
53
|
+
if (!head.length)
|
|
54
|
+
return text.slice(0, Math.max(0, maxChars - 3)).trimEnd() + '...';
|
|
55
|
+
return [...head, '...', ...tail].join('\n');
|
|
56
|
+
}
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Provider usage (plain-text builder — caller wraps as needed)
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
function rawUsageLine(parts) {
|
|
61
|
+
return parts.filter(part => !!part && String(part).trim()).join(' ');
|
|
62
|
+
}
|
|
63
|
+
export function buildProviderUsageLines(usage) {
|
|
64
|
+
const lines = [
|
|
65
|
+
{ text: '', bold: false },
|
|
66
|
+
{ text: 'Provider Usage', bold: true },
|
|
67
|
+
];
|
|
68
|
+
if (!usage.ok) {
|
|
69
|
+
lines.push({ text: ` Unavailable: ${usage.error || 'No recent usage data found.'}` });
|
|
70
|
+
return lines;
|
|
71
|
+
}
|
|
72
|
+
if (usage.capturedAt) {
|
|
73
|
+
const capturedAtMs = Date.parse(usage.capturedAt);
|
|
74
|
+
if (Number.isFinite(capturedAtMs)) {
|
|
75
|
+
lines.push({ text: ` Updated: ${fmtUptime(Math.max(0, Date.now() - capturedAtMs))} ago` });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!usage.windows.length) {
|
|
79
|
+
lines.push({ text: ` ${usage.status ? `status=${usage.status}` : 'No window data'}` });
|
|
80
|
+
return lines;
|
|
81
|
+
}
|
|
82
|
+
for (const window of usage.windows) {
|
|
83
|
+
const details = rawUsageLine([
|
|
84
|
+
window.usedPercent != null ? `${window.usedPercent}% used` : null,
|
|
85
|
+
window.status ? `status=${window.status}` : null,
|
|
86
|
+
window.resetAfterSeconds != null ? `resetAfterSeconds=${window.resetAfterSeconds}` : null,
|
|
87
|
+
]);
|
|
88
|
+
lines.push({ text: ` ${window.label}: ${details || 'No details'}` });
|
|
89
|
+
}
|
|
90
|
+
return lines;
|
|
91
|
+
}
|
|
92
|
+
export function extractFinalReplyData(agent, result) {
|
|
93
|
+
const footerStatus = result.incomplete || !result.ok ? 'failed' : 'done';
|
|
94
|
+
const elapsedMs = result.elapsedS * 1000;
|
|
95
|
+
const footerSummary = formatFooterSummary(agent, elapsedMs, null, result.contextPercent ?? null);
|
|
96
|
+
let activityNarrative = null;
|
|
97
|
+
let activityCommandSummary = null;
|
|
98
|
+
if (result.activity) {
|
|
99
|
+
const summary = parseActivitySummary(result.activity);
|
|
100
|
+
const narrative = summary.narrative.join('\n');
|
|
101
|
+
if (narrative) {
|
|
102
|
+
activityNarrative = narrative.length > 1600 ? '...\n' + narrative.slice(-1600) : narrative;
|
|
103
|
+
}
|
|
104
|
+
const cmdSummary = formatActivityCommandSummary(summary.completedCommands, summary.activeCommands, summary.failedCommands);
|
|
105
|
+
if (cmdSummary)
|
|
106
|
+
activityCommandSummary = cmdSummary;
|
|
107
|
+
}
|
|
108
|
+
let thinkingDisplay = null;
|
|
109
|
+
if (result.thinking) {
|
|
110
|
+
thinkingDisplay = formatThinkingForDisplay(result.thinking, 1600);
|
|
111
|
+
}
|
|
112
|
+
let statusLines = null;
|
|
113
|
+
if (result.incomplete) {
|
|
114
|
+
statusLines = [];
|
|
115
|
+
if (result.stopReason === 'max_tokens')
|
|
116
|
+
statusLines.push('Output limit reached. Response may be truncated.');
|
|
117
|
+
if (result.stopReason === 'timeout') {
|
|
118
|
+
statusLines.push(`Timed out after ${fmtUptime(Math.max(0, Math.round(elapsedMs)))} before the agent reported completion.`);
|
|
119
|
+
}
|
|
120
|
+
if (!result.ok) {
|
|
121
|
+
const detail = result.error?.trim();
|
|
122
|
+
if (detail && detail !== result.message.trim() && !statusLines.includes(detail))
|
|
123
|
+
statusLines.push(detail);
|
|
124
|
+
else if (result.stopReason !== 'timeout')
|
|
125
|
+
statusLines.push('Agent exited before reporting completion.');
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
footerStatus,
|
|
130
|
+
footerSummary,
|
|
131
|
+
activityNarrative,
|
|
132
|
+
activityCommandSummary,
|
|
133
|
+
thinkingDisplay,
|
|
134
|
+
thinkLabel: thinkLabel(agent),
|
|
135
|
+
statusLines,
|
|
136
|
+
bodyMessage: result.message,
|
|
137
|
+
elapsedMs,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export function extractStreamPreviewData(input) {
|
|
141
|
+
const maxBody = 2400;
|
|
142
|
+
const display = input.bodyText.trim();
|
|
143
|
+
const rawThinking = input.thinking.trim();
|
|
144
|
+
const thinkDisplay = formatThinkingForDisplay(input.thinking, maxBody);
|
|
145
|
+
const planDisplay = renderPlanForPreview(input.plan ?? null);
|
|
146
|
+
const activityDisplay = summarizeActivityForPreview(input.activity);
|
|
147
|
+
const maxActivity = !display && !thinkDisplay && !planDisplay ? 2400 : 1400;
|
|
148
|
+
const label = thinkLabel(input.agent);
|
|
149
|
+
const thinkSnippet = rawThinking ? formatThinkingForDisplay(input.thinking, 600) : '';
|
|
150
|
+
const preview = display.length > maxBody ? '(...truncated)\n' + display.slice(-maxBody) : display;
|
|
151
|
+
return {
|
|
152
|
+
display,
|
|
153
|
+
rawThinking,
|
|
154
|
+
thinkDisplay,
|
|
155
|
+
planDisplay,
|
|
156
|
+
activityDisplay,
|
|
157
|
+
maxActivity,
|
|
158
|
+
label,
|
|
159
|
+
thinkSnippet,
|
|
160
|
+
preview,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { hasPreviewMeta, samePreviewMeta, samePreviewPlan } from './bot-streaming.js';
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
const
|
|
2
|
+
import { STREAM_PREVIEW_TIMEOUTS } from './constants.js';
|
|
3
|
+
const STREAM_PREVIEW_HEARTBEAT_MS = STREAM_PREVIEW_TIMEOUTS.heartbeat;
|
|
4
|
+
const STREAM_TYPING_HEARTBEAT_MS = STREAM_PREVIEW_TIMEOUTS.typing;
|
|
5
|
+
const STREAM_STALLED_NOTICE_MS = STREAM_PREVIEW_TIMEOUTS.stalledNotice;
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
7
|
// LivePreview — generic streaming preview controller
|
|
7
8
|
// ---------------------------------------------------------------------------
|