pikiloop 0.4.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/LICENSE +21 -0
- package/README.md +353 -0
- package/README.v2.md +287 -0
- package/README.zh-CN.md +352 -0
- package/dashboard/dist/assets/AgentTab-UZPIhlkr.js +1 -0
- package/dashboard/dist/assets/DirBrowser-Ckcmi-Pi.js +1 -0
- package/dashboard/dist/assets/ExtensionsTab-KZhEDrdu.js +1 -0
- package/dashboard/dist/assets/IMAccessTab-Bd_IY1GQ.js +1 -0
- package/dashboard/dist/assets/Modal-CTeL0y7P.js +1 -0
- package/dashboard/dist/assets/Modals-axftHasy.js +1 -0
- package/dashboard/dist/assets/Select-C8tOdPhe.js +1 -0
- package/dashboard/dist/assets/SessionPanel-C1geSRxw.js +1 -0
- package/dashboard/dist/assets/SystemTab-DBDkaPiO.js +1 -0
- package/dashboard/dist/assets/anthropic-BAdojD7P.ico +0 -0
- package/dashboard/dist/assets/codex-DYadqqp0.png +0 -0
- package/dashboard/dist/assets/deepseek-BeYNZEk0.ico +0 -0
- package/dashboard/dist/assets/doubao-DloFDuFR.png +0 -0
- package/dashboard/dist/assets/feishu-C4OMrjCW.ico +0 -0
- package/dashboard/dist/assets/gemini-BYkEpiWr.svg +1 -0
- package/dashboard/dist/assets/hermes-BAarh-tH.png +0 -0
- package/dashboard/dist/assets/index-CpM4CqZJ.js +23 -0
- package/dashboard/dist/assets/index-DXSohzrE.js +3 -0
- package/dashboard/dist/assets/index-reSbuley.css +1 -0
- package/dashboard/dist/assets/markdown-DxQYQFeH.js +29 -0
- package/dashboard/dist/assets/minimax-PuEGTfrF.ico +0 -0
- package/dashboard/dist/assets/mlx-DhWwjtMw.png +0 -0
- package/dashboard/dist/assets/ollama-Bt9O-2K_.png +0 -0
- package/dashboard/dist/assets/openrouter-CsJ_bD5Q.ico +0 -0
- package/dashboard/dist/assets/playwright-BldPFZgC.ico +0 -0
- package/dashboard/dist/assets/qwen-xykkX0_y.png +0 -0
- package/dashboard/dist/assets/react-vendor-C7Sl8SE7.js +9 -0
- package/dashboard/dist/assets/router-DHISdpPk.js +3 -0
- package/dashboard/dist/assets/shared-BIP_4k4I.js +1 -0
- package/dashboard/dist/favicon.svg +28 -0
- package/dashboard/dist/index.html +17 -0
- package/dist/agent/acp-client.js +261 -0
- package/dist/agent/auto-update.js +432 -0
- package/dist/agent/await-resume.js +50 -0
- package/dist/agent/cli/auth.js +325 -0
- package/dist/agent/cli/catalog.js +40 -0
- package/dist/agent/cli/detector.js +136 -0
- package/dist/agent/cli/index.js +7 -0
- package/dist/agent/cli/registry.js +33 -0
- package/dist/agent/driver.js +39 -0
- package/dist/agent/drivers/claude-tui.js +2297 -0
- package/dist/agent/drivers/claude.js +2689 -0
- package/dist/agent/drivers/codex.js +2210 -0
- package/dist/agent/drivers/gemini.js +1059 -0
- package/dist/agent/drivers/hermes.js +795 -0
- package/dist/agent/goal.js +274 -0
- package/dist/agent/handover.js +130 -0
- package/dist/agent/images.js +355 -0
- package/dist/agent/index.js +50 -0
- package/dist/agent/mcp/bridge.js +791 -0
- package/dist/agent/mcp/extensions.js +637 -0
- package/dist/agent/mcp/oauth.js +353 -0
- package/dist/agent/mcp/registry.js +119 -0
- package/dist/agent/mcp/session-server.js +229 -0
- package/dist/agent/mcp/tools/ask-user.js +113 -0
- package/dist/agent/mcp/tools/await-resume.js +77 -0
- package/dist/agent/mcp/tools/goal.js +144 -0
- package/dist/agent/mcp/tools/types.js +12 -0
- package/dist/agent/mcp/tools/workspace.js +212 -0
- package/dist/agent/npm.js +31 -0
- package/dist/agent/session.js +1206 -0
- package/dist/agent/skill-installer.js +160 -0
- package/dist/agent/skills.js +257 -0
- package/dist/agent/stream.js +743 -0
- package/dist/agent/types.js +13 -0
- package/dist/agent/utils.js +687 -0
- package/dist/bot/bot.js +2499 -0
- package/dist/bot/command-ui.js +633 -0
- package/dist/bot/commands.js +513 -0
- package/dist/bot/headless-bot.js +36 -0
- package/dist/bot/host.js +192 -0
- package/dist/bot/human-loop.js +168 -0
- package/dist/bot/menu.js +48 -0
- package/dist/bot/orchestration.js +79 -0
- package/dist/bot/render-shared.js +309 -0
- package/dist/bot/session-hub.js +361 -0
- package/dist/bot/session-status.js +55 -0
- package/dist/bot/streaming.js +309 -0
- package/dist/browser-profile.js +579 -0
- package/dist/browser-supervisor.js +249 -0
- package/dist/catalog/cli-tools.js +421 -0
- package/dist/catalog/index.js +21 -0
- package/dist/catalog/local-models.js +94 -0
- package/dist/catalog/mcp-servers.js +315 -0
- package/dist/catalog/skill-repos.js +173 -0
- package/dist/channels/base.js +55 -0
- package/dist/channels/dingtalk/bot.js +549 -0
- package/dist/channels/dingtalk/channel.js +268 -0
- package/dist/channels/discord/bot.js +552 -0
- package/dist/channels/discord/channel.js +245 -0
- package/dist/channels/feishu/bot.js +1275 -0
- package/dist/channels/feishu/channel.js +911 -0
- package/dist/channels/feishu/markdown.js +91 -0
- package/dist/channels/feishu/render.js +619 -0
- package/dist/channels/health.js +109 -0
- package/dist/channels/slack/bot.js +554 -0
- package/dist/channels/slack/channel.js +283 -0
- package/dist/channels/states.js +6 -0
- package/dist/channels/telegram/bot.js +1310 -0
- package/dist/channels/telegram/channel.js +820 -0
- package/dist/channels/telegram/directory.js +111 -0
- package/dist/channels/telegram/live-preview.js +220 -0
- package/dist/channels/telegram/render.js +384 -0
- package/dist/channels/wecom/bot.js +558 -0
- package/dist/channels/wecom/channel.js +479 -0
- package/dist/channels/weixin/api.js +520 -0
- package/dist/channels/weixin/bot.js +1000 -0
- package/dist/channels/weixin/channel.js +222 -0
- package/dist/cli/autostart.js +262 -0
- package/dist/cli/channel-supervisor.js +313 -0
- package/dist/cli/channels.js +54 -0
- package/dist/cli/main.js +726 -0
- package/dist/cli/onboarding.js +227 -0
- package/dist/cli/run.js +308 -0
- package/dist/cli/setup-wizard.js +235 -0
- package/dist/core/config/runtime-config.js +201 -0
- package/dist/core/config/user-config.js +510 -0
- package/dist/core/config/validation.js +521 -0
- package/dist/core/constants.js +400 -0
- package/dist/core/git.js +145 -0
- package/dist/core/legacy-compat.js +60 -0
- package/dist/core/logging.js +101 -0
- package/dist/core/platform.js +59 -0
- package/dist/core/process-control.js +315 -0
- package/dist/core/secrets/index.js +42 -0
- package/dist/core/secrets/inline-seal.js +60 -0
- package/dist/core/secrets/ref.js +33 -0
- package/dist/core/secrets/resolver.js +65 -0
- package/dist/core/secrets/store.js +63 -0
- package/dist/core/utils.js +233 -0
- package/dist/core/version.js +15 -0
- package/dist/dashboard/platform.js +219 -0
- package/dist/dashboard/routes/agents.js +450 -0
- package/dist/dashboard/routes/cli.js +174 -0
- package/dist/dashboard/routes/config.js +523 -0
- package/dist/dashboard/routes/extensions.js +745 -0
- package/dist/dashboard/routes/local-models.js +290 -0
- package/dist/dashboard/routes/models.js +324 -0
- package/dist/dashboard/routes/sessions.js +838 -0
- package/dist/dashboard/runtime.js +410 -0
- package/dist/dashboard/server.js +237 -0
- package/dist/dashboard/session-control.js +347 -0
- package/dist/model/catalog.js +104 -0
- package/dist/model/index.js +20 -0
- package/dist/model/injector.js +272 -0
- package/dist/model/provider-models.js +112 -0
- package/dist/model/store.js +212 -0
- package/dist/model/types.js +13 -0
- package/dist/model/validation.js +203 -0
- package/package.json +82 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime session status helpers for dashboard polling.
|
|
3
|
+
*/
|
|
4
|
+
import { isRunningSessionStale } from '../agent/index.js';
|
|
5
|
+
/** Age threshold used when the owning PID is unknown or unverifiable. */
|
|
6
|
+
const STALE_RUNNING_AGE_MS = 30 * 60_000; // 30 minutes
|
|
7
|
+
function getSessionRuntime(bot, session) {
|
|
8
|
+
const sessionId = session.sessionId || null;
|
|
9
|
+
if (!sessionId)
|
|
10
|
+
return null;
|
|
11
|
+
return bot.sessionStates.get(`${session.agent}:${sessionId}`) || null;
|
|
12
|
+
}
|
|
13
|
+
function resolveIsRunning(session, runtime) {
|
|
14
|
+
if (runtime?.runningTaskIds.size)
|
|
15
|
+
return { isRunning: true, isStale: false };
|
|
16
|
+
if (!session.running)
|
|
17
|
+
return { isRunning: false, isStale: false };
|
|
18
|
+
const stale = isRunningSessionStale({
|
|
19
|
+
runState: session.runState ?? 'running',
|
|
20
|
+
runPid: session.runPid ?? null,
|
|
21
|
+
runUpdatedAt: session.runUpdatedAt ?? null,
|
|
22
|
+
}, STALE_RUNNING_AGE_MS);
|
|
23
|
+
return stale ? { isRunning: false, isStale: true } : { isRunning: true, isStale: false };
|
|
24
|
+
}
|
|
25
|
+
export function getSessionStatusForChat(bot, chat, session) {
|
|
26
|
+
const runtime = getSessionRuntime(bot, session);
|
|
27
|
+
const sessionId = session.sessionId || null;
|
|
28
|
+
const isCurrent = !!sessionId && (runtime
|
|
29
|
+
? chat.activeSessionKey === runtime.key
|
|
30
|
+
: chat.agent === session.agent && chat.sessionId === sessionId);
|
|
31
|
+
const { isRunning, isStale } = resolveIsRunning(session, runtime);
|
|
32
|
+
return { runtime, isCurrent, isRunning, isStale };
|
|
33
|
+
}
|
|
34
|
+
export function getSessionStatusForBot(bot, session) {
|
|
35
|
+
const runtime = getSessionRuntime(bot, session);
|
|
36
|
+
const sessionId = session.sessionId || null;
|
|
37
|
+
let isCurrent = false;
|
|
38
|
+
if (sessionId) {
|
|
39
|
+
for (const [, chat] of bot.chats) {
|
|
40
|
+
if (runtime) {
|
|
41
|
+
if (chat.activeSessionKey === runtime.key) {
|
|
42
|
+
isCurrent = true;
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (chat.agent === session.agent && chat.sessionId === sessionId) {
|
|
48
|
+
isCurrent = true;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const { isRunning, isStale } = resolveIsRunning(session, runtime);
|
|
54
|
+
return { runtime, isCurrent, isRunning, isStale };
|
|
55
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stream preview parsing helpers for live message updates.
|
|
3
|
+
*
|
|
4
|
+
* Used by IM channels (Telegram / Feishu / WeChat). The dashboard reads the
|
|
5
|
+
* raw `StreamSnapshot.activity` string directly and renders independently —
|
|
6
|
+
* none of the compaction in this file flows to it. Keep changes that improve
|
|
7
|
+
* IM legibility free to touch this surface; if a change would also alter what
|
|
8
|
+
* the dashboard ultimately shows, do it in the dashboard layer instead.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Shrink absolute paths that bloat IM cards on small screens. Any absolute
|
|
12
|
+
* path with 4+ segments collapses to `…/<last-two-segments>` so directory
|
|
13
|
+
* context is kept while the leading `/Users/…/long/project/root/` noise is
|
|
14
|
+
* dropped. Length-based gating made the output inconsistent — borderline
|
|
15
|
+
* paths (47 chars) sat next to compacted ones (52 chars), making the activity
|
|
16
|
+
* list look broken. Relative paths and short paths (<4 segments, e.g.
|
|
17
|
+
* `~/foo`, `/tmp/x.log`) are passed through unchanged.
|
|
18
|
+
*/
|
|
19
|
+
function compactActivityPath(token) {
|
|
20
|
+
if (!token.includes('/'))
|
|
21
|
+
return token;
|
|
22
|
+
const segments = token.split('/').filter(Boolean);
|
|
23
|
+
if (segments.length < 4)
|
|
24
|
+
return token;
|
|
25
|
+
const tail = segments.slice(-2).join('/');
|
|
26
|
+
return `…/${tail}`;
|
|
27
|
+
}
|
|
28
|
+
function compactPathsInActivityLine(line) {
|
|
29
|
+
// Conservative: only target obviously absolute paths starting with `/` or
|
|
30
|
+
// `~/`. Inline file:line references (`foo/bar.ts:42`) keep their structure
|
|
31
|
+
// since the prefix is short anyway.
|
|
32
|
+
return line.replace(/(^|\s)([~/][^\s]+)/g, (_match, lead, raw) => {
|
|
33
|
+
const trailingPunct = raw.match(/[)\],.;!?]+$/)?.[0] ?? '';
|
|
34
|
+
const path = trailingPunct ? raw.slice(0, -trailingPunct.length) : raw;
|
|
35
|
+
return `${lead}${compactActivityPath(path)}${trailingPunct}`;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const TOOL_DONE_RE = /^(.+?)\s+(done|failed)$/;
|
|
39
|
+
/** "X -> Y" pattern produced by `summarizeClaudeToolResult` for tools whose
|
|
40
|
+
* result has body text (im_ask_user, ToolSearch, MCP tools, …). The Y half
|
|
41
|
+
* is the tool's response — capturing it lets us collapse the pre-event
|
|
42
|
+
* (`Ask user: q`) and the post-event (`Ask user: q -> A: …`) into a single
|
|
43
|
+
* line in the narrative instead of leaving both sitting around. */
|
|
44
|
+
const TOOL_ARROW_RE = /^(.+?)\s*->\s*(.+)$/;
|
|
45
|
+
const INJECTED_PROMPT_MARKERS = [
|
|
46
|
+
'\n[Session Workspace]',
|
|
47
|
+
'\n[Telegram Artifact Return]',
|
|
48
|
+
'\n[Artifact Return]',
|
|
49
|
+
];
|
|
50
|
+
export function stripInjectedPrompts(text) {
|
|
51
|
+
for (const marker of INJECTED_PROMPT_MARKERS) {
|
|
52
|
+
const idx = text.indexOf(marker);
|
|
53
|
+
if (idx >= 0)
|
|
54
|
+
return text.slice(0, idx).trim();
|
|
55
|
+
}
|
|
56
|
+
return text.trim();
|
|
57
|
+
}
|
|
58
|
+
export function summarizePromptForStatus(prompt, maxLen = 50) {
|
|
59
|
+
const clean = stripInjectedPrompts(prompt).replace(/\s+/g, ' ').trim();
|
|
60
|
+
if (!clean)
|
|
61
|
+
return '';
|
|
62
|
+
if (clean.length <= maxLen)
|
|
63
|
+
return clean;
|
|
64
|
+
return clean.slice(0, Math.max(0, maxLen - 3)).trimEnd() + '...';
|
|
65
|
+
}
|
|
66
|
+
function parseClaudeShellActivity(line) {
|
|
67
|
+
const prefix = 'Run shell: ';
|
|
68
|
+
if (!line.startsWith(prefix))
|
|
69
|
+
return null;
|
|
70
|
+
const detail = line.slice(prefix.length).trim();
|
|
71
|
+
if (!detail)
|
|
72
|
+
return { key: prefix.trim(), status: 'active' };
|
|
73
|
+
const doneIdx = detail.indexOf(' -> ');
|
|
74
|
+
if (doneIdx > 0) {
|
|
75
|
+
return {
|
|
76
|
+
key: detail.slice(0, doneIdx).trim(),
|
|
77
|
+
status: 'done',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const failed = detail.match(/^(.*)\sfailed(?::.*)?$/);
|
|
81
|
+
if (failed?.[1]?.trim()) {
|
|
82
|
+
return {
|
|
83
|
+
key: failed[1].trim(),
|
|
84
|
+
status: 'failed',
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (detail.endsWith(' done')) {
|
|
88
|
+
const key = detail.slice(0, -' done'.length).trim();
|
|
89
|
+
return { key: key || detail, status: 'done' };
|
|
90
|
+
}
|
|
91
|
+
return { key: detail, status: 'active' };
|
|
92
|
+
}
|
|
93
|
+
export function parseActivitySummary(activity) {
|
|
94
|
+
const narrative = [];
|
|
95
|
+
let failedCommands = 0;
|
|
96
|
+
let activeCommands = 0;
|
|
97
|
+
let completedCommands = 0;
|
|
98
|
+
const activeClaudeShells = new Map();
|
|
99
|
+
// Track narrative indices keyed by their normalized start text so a later
|
|
100
|
+
// "X done" / "X failed" line collapses the prior "X" entry instead of
|
|
101
|
+
// appending. Avoids the double-line spam in IM cards where each tool call
|
|
102
|
+
// shows both its in-progress and completed line.
|
|
103
|
+
const pendingNarrative = new Map();
|
|
104
|
+
const pushPending = (key, index) => {
|
|
105
|
+
const slot = pendingNarrative.get(key);
|
|
106
|
+
if (slot)
|
|
107
|
+
slot.push(index);
|
|
108
|
+
else
|
|
109
|
+
pendingNarrative.set(key, [index]);
|
|
110
|
+
};
|
|
111
|
+
const popPending = (key) => {
|
|
112
|
+
const slot = pendingNarrative.get(key);
|
|
113
|
+
if (!slot || !slot.length)
|
|
114
|
+
return null;
|
|
115
|
+
const idx = slot.shift();
|
|
116
|
+
if (!slot.length)
|
|
117
|
+
pendingNarrative.delete(key);
|
|
118
|
+
return idx;
|
|
119
|
+
};
|
|
120
|
+
for (const rawLine of activity.split('\n')) {
|
|
121
|
+
const line = compactPathsInActivityLine(rawLine.replace(/\s+/g, ' ').trim());
|
|
122
|
+
if (!line)
|
|
123
|
+
continue;
|
|
124
|
+
const claudeShell = parseClaudeShellActivity(line);
|
|
125
|
+
if (claudeShell) {
|
|
126
|
+
const key = claudeShell.key || 'Run shell';
|
|
127
|
+
const current = activeClaudeShells.get(key) || 0;
|
|
128
|
+
if (claudeShell.status === 'active') {
|
|
129
|
+
activeClaudeShells.set(key, current + 1);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
if (current > 0)
|
|
133
|
+
activeClaudeShells.set(key, current - 1);
|
|
134
|
+
if (claudeShell.status === 'done')
|
|
135
|
+
completedCommands++;
|
|
136
|
+
else
|
|
137
|
+
failedCommands++;
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (line.startsWith('$ ')) {
|
|
142
|
+
activeCommands++;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (line.startsWith('Ran: ')) {
|
|
146
|
+
completedCommands++;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const executed = line.match(/^Executed (\d+) command(?:s)?\.$/);
|
|
150
|
+
if (executed) {
|
|
151
|
+
completedCommands = Math.max(completedCommands, parseInt(executed[1], 10) || 0);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const running = line.match(/^Running (\d+) command(?:s)?\.\.\.$/);
|
|
155
|
+
if (running) {
|
|
156
|
+
activeCommands = Math.max(activeCommands, parseInt(running[1], 10) || 0);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const failed = line.match(/^Command failed \((\d+)\):/);
|
|
160
|
+
if (failed) {
|
|
161
|
+
failedCommands++;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (/^Command failed \(\d+\)$/.test(line)) {
|
|
165
|
+
failedCommands++;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
// Pair "X" → "X done"/"X failed": rewrite the prior in-progress entry in
|
|
169
|
+
// place rather than appending a second line. Falls back to a plain append
|
|
170
|
+
// when no matching start exists (e.g. the start line was trimmed off by a
|
|
171
|
+
// history window earlier in the run).
|
|
172
|
+
const doneMatch = line.match(TOOL_DONE_RE);
|
|
173
|
+
if (doneMatch) {
|
|
174
|
+
const baseKey = doneMatch[1].trim();
|
|
175
|
+
const status = doneMatch[2];
|
|
176
|
+
const idx = popPending(baseKey);
|
|
177
|
+
if (idx != null) {
|
|
178
|
+
narrative[idx] = status === 'failed' ? `${baseKey} failed` : baseKey;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
narrative.push(status === 'failed' ? `${baseKey} failed` : baseKey);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
// Pair "X" → "X -> Y" (im_ask_user, ToolSearch, MCP tools, … — any tool
|
|
185
|
+
// whose summarizeClaudeToolResult fell into the arrow branch). Without
|
|
186
|
+
// this, the IM card shows the question and the answered form side by
|
|
187
|
+
// side. We replace the pending entry with the full arrow form so the
|
|
188
|
+
// narrative carries the answer in a single line.
|
|
189
|
+
const arrowMatch = line.match(TOOL_ARROW_RE);
|
|
190
|
+
if (arrowMatch) {
|
|
191
|
+
const baseKey = arrowMatch[1].trim();
|
|
192
|
+
const idx = popPending(baseKey);
|
|
193
|
+
if (idx != null) {
|
|
194
|
+
narrative[idx] = line;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
pushPending(line, narrative.length);
|
|
199
|
+
narrative.push(line);
|
|
200
|
+
}
|
|
201
|
+
for (const pending of activeClaudeShells.values()) {
|
|
202
|
+
activeCommands += pending;
|
|
203
|
+
}
|
|
204
|
+
return { narrative: collapseConsecutiveDuplicates(narrative), failedCommands, completedCommands, activeCommands };
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Walk the narrative and collapse runs of identical lines into `X ×N`. The
|
|
208
|
+
* input narrative often contains repeats when the model calls the same tool
|
|
209
|
+
* multiple times in a row (two consecutive `Edit README.md`, three `Read X`,
|
|
210
|
+
* …) — listing them N times wastes IM card real estate without adding
|
|
211
|
+
* information. Non-adjacent duplicates are preserved to keep the temporal
|
|
212
|
+
* order intact.
|
|
213
|
+
*/
|
|
214
|
+
function collapseConsecutiveDuplicates(narrative) {
|
|
215
|
+
const out = [];
|
|
216
|
+
let runStart = -1;
|
|
217
|
+
let runCount = 0;
|
|
218
|
+
const flush = () => {
|
|
219
|
+
if (runStart < 0)
|
|
220
|
+
return;
|
|
221
|
+
out.push(runCount > 1 ? `${narrative[runStart]} ×${runCount}` : narrative[runStart]);
|
|
222
|
+
runStart = -1;
|
|
223
|
+
runCount = 0;
|
|
224
|
+
};
|
|
225
|
+
for (let i = 0; i < narrative.length; i++) {
|
|
226
|
+
if (runStart >= 0 && narrative[i] === narrative[runStart]) {
|
|
227
|
+
runCount++;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
flush();
|
|
231
|
+
runStart = i;
|
|
232
|
+
runCount = 1;
|
|
233
|
+
}
|
|
234
|
+
flush();
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
export function formatActivityCommandSummary(completedCommands, activeCommands, failedCommands = 0) {
|
|
238
|
+
const parts = [];
|
|
239
|
+
if (failedCommands > 0)
|
|
240
|
+
parts.push(`${failedCommands} failed`);
|
|
241
|
+
if (completedCommands > 0)
|
|
242
|
+
parts.push(`${completedCommands} done`);
|
|
243
|
+
if (activeCommands > 0)
|
|
244
|
+
parts.push(`${activeCommands} running`);
|
|
245
|
+
return parts.length ? `commands: ${parts.join(', ')}` : '';
|
|
246
|
+
}
|
|
247
|
+
export function summarizeActivityForPreview(activity) {
|
|
248
|
+
const summary = parseActivitySummary(activity);
|
|
249
|
+
const lines = [...summary.narrative];
|
|
250
|
+
const commandSummary = formatActivityCommandSummary(summary.completedCommands, summary.activeCommands, summary.failedCommands);
|
|
251
|
+
if (commandSummary)
|
|
252
|
+
lines.push(commandSummary);
|
|
253
|
+
return lines.join('\n');
|
|
254
|
+
}
|
|
255
|
+
export function hasPreviewMeta(meta) {
|
|
256
|
+
return meta?.contextPercent != null;
|
|
257
|
+
}
|
|
258
|
+
export function samePreviewMeta(a, b) {
|
|
259
|
+
return (a?.contextPercent ?? null) === (b?.contextPercent ?? null);
|
|
260
|
+
}
|
|
261
|
+
export function samePreviewPlan(a, b) {
|
|
262
|
+
if ((a?.explanation ?? null) !== (b?.explanation ?? null))
|
|
263
|
+
return false;
|
|
264
|
+
const aSteps = a?.steps ?? [];
|
|
265
|
+
const bSteps = b?.steps ?? [];
|
|
266
|
+
if (aSteps.length !== bSteps.length)
|
|
267
|
+
return false;
|
|
268
|
+
for (let i = 0; i < aSteps.length; i++) {
|
|
269
|
+
if (aSteps[i].status !== bSteps[i].status)
|
|
270
|
+
return false;
|
|
271
|
+
if (aSteps[i].step !== bSteps[i].step)
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
function normalizePlanStep(step) {
|
|
277
|
+
return step.replace(/\s+/g, ' ').trim();
|
|
278
|
+
}
|
|
279
|
+
export function renderPlanForPreview(plan) {
|
|
280
|
+
if (!plan?.steps.length)
|
|
281
|
+
return '';
|
|
282
|
+
const total = plan.steps.length;
|
|
283
|
+
const completed = plan.steps.filter(step => step.status === 'completed').length;
|
|
284
|
+
const lines = [`Plan ${completed}/${total}`];
|
|
285
|
+
// Show the most recent / currently-active slice of the plan. Live viewers
|
|
286
|
+
// care about the in-progress + upcoming steps; the dozen already-completed
|
|
287
|
+
// ones at the top of the list are just visual ballast (the `completed/total`
|
|
288
|
+
// header already conveys the overall progress).
|
|
289
|
+
const WINDOW = 4;
|
|
290
|
+
let startIdx = 0;
|
|
291
|
+
if (total > WINDOW) {
|
|
292
|
+
// Center the window on the in-progress step when one exists; otherwise
|
|
293
|
+
// anchor to the tail so the next pending steps are visible.
|
|
294
|
+
const inProgressIdx = plan.steps.findIndex(step => step.status === 'inProgress');
|
|
295
|
+
const anchor = inProgressIdx >= 0 ? inProgressIdx : total - 1;
|
|
296
|
+
startIdx = Math.max(0, Math.min(total - WINDOW, anchor - Math.floor(WINDOW / 2)));
|
|
297
|
+
}
|
|
298
|
+
const dropped = startIdx;
|
|
299
|
+
if (dropped > 0)
|
|
300
|
+
lines.push(`... +${dropped} earlier`);
|
|
301
|
+
for (const step of plan.steps.slice(startIdx, startIdx + WINDOW)) {
|
|
302
|
+
const prefix = step.status === 'completed' ? '[x]' : step.status === 'inProgress' ? '[>]' : '[ ]';
|
|
303
|
+
lines.push(`${prefix} ${normalizePlanStep(step.step)}`);
|
|
304
|
+
}
|
|
305
|
+
const remaining = total - (startIdx + WINDOW);
|
|
306
|
+
if (remaining > 0)
|
|
307
|
+
lines.push(`... +${remaining} more`);
|
|
308
|
+
return lines.join('\n');
|
|
309
|
+
}
|