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,619 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feishu-specific message card rendering.
|
|
3
|
+
*
|
|
4
|
+
* Converts structured data from bot/commands.ts into Feishu Markdown (for interactive cards).
|
|
5
|
+
* Also provides a LivePreviewRenderer for streaming output.
|
|
6
|
+
*/
|
|
7
|
+
import { encodeCommandAction } from '../../bot/command-ui.js';
|
|
8
|
+
import { fmtUptime, fmtTokens, fmtBytes, formatGitStatusLine } from '../../bot/bot.js';
|
|
9
|
+
import { summarizePromptForStatus } from '../../bot/commands.js';
|
|
10
|
+
import { footerStatusSymbol, formatFooterParts, trimActivityForPreview, buildProviderUsageLines, extractFinalReplyData, extractStreamPreviewData, } from '../../bot/render-shared.js';
|
|
11
|
+
export { dispatchImageBlocks } from '../../bot/render-shared.js';
|
|
12
|
+
import { currentHumanLoopQuestion, humanLoopAnsweredCount, isHumanLoopAwaitingText, isHumanLoopQuestionAnswered, summarizeHumanLoopAnswer, } from '../../bot/human-loop.js';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { listSubdirs } from '../../bot/bot.js';
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Helpers
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
function formatPreviewFooter(agent, elapsedMs, meta, decorations) {
|
|
19
|
+
const parts = formatFooterParts(agent, elapsedMs, meta, null, decorations);
|
|
20
|
+
return `${footerStatusSymbol('running')} ${parts.identity}\n*${parts.runtime}*`;
|
|
21
|
+
}
|
|
22
|
+
function formatFinalFooter(status, agent, elapsedMs, contextPercent, decorations) {
|
|
23
|
+
const parts = formatFooterParts(agent, elapsedMs, null, contextPercent ?? null, decorations);
|
|
24
|
+
return `${footerStatusSymbol(status)} ${parts.identity}\n*${parts.runtime}*`;
|
|
25
|
+
}
|
|
26
|
+
function truncateLabel(label, maxChars = 24) {
|
|
27
|
+
return label.length > maxChars ? `${label.slice(0, Math.max(1, maxChars - 1))}…` : label;
|
|
28
|
+
}
|
|
29
|
+
function cardButton(label, action, primary = false) {
|
|
30
|
+
const button = {
|
|
31
|
+
tag: 'button',
|
|
32
|
+
text: { tag: 'plain_text', content: truncateLabel(label) },
|
|
33
|
+
value: { action },
|
|
34
|
+
};
|
|
35
|
+
if (primary)
|
|
36
|
+
button.type = 'primary';
|
|
37
|
+
return button;
|
|
38
|
+
}
|
|
39
|
+
function cardRows(actions, size = 3) {
|
|
40
|
+
const rows = [];
|
|
41
|
+
for (let i = 0; i < actions.length; i += size) {
|
|
42
|
+
const rowActions = actions.slice(i, i + size);
|
|
43
|
+
if (!rowActions.length)
|
|
44
|
+
continue;
|
|
45
|
+
rows.push({ actions: rowActions });
|
|
46
|
+
}
|
|
47
|
+
return rows;
|
|
48
|
+
}
|
|
49
|
+
function selectionStateSymbol(state) {
|
|
50
|
+
switch (state) {
|
|
51
|
+
case 'current': return '●';
|
|
52
|
+
case 'running': return '🟢';
|
|
53
|
+
case 'unavailable': return '❌';
|
|
54
|
+
default: return '○';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function formatCommandItemMarkdown(item, index) {
|
|
58
|
+
const parts = [
|
|
59
|
+
selectionStateSymbol(item.state),
|
|
60
|
+
`**${index + 1}.**`,
|
|
61
|
+
item.label,
|
|
62
|
+
];
|
|
63
|
+
if (item.detail)
|
|
64
|
+
parts.push(item.detail);
|
|
65
|
+
return parts.join(' ');
|
|
66
|
+
}
|
|
67
|
+
function formatCommandButtonLabel(button) {
|
|
68
|
+
const prefix = button.state && button.state !== 'default'
|
|
69
|
+
? `${selectionStateSymbol(button.state)} `
|
|
70
|
+
: '';
|
|
71
|
+
return truncateLabel(`${prefix}${button.label}`.trim());
|
|
72
|
+
}
|
|
73
|
+
function actionButton(button) {
|
|
74
|
+
return cardButton(formatCommandButtonLabel(button), encodeCommandAction(button.action), !!button.primary);
|
|
75
|
+
}
|
|
76
|
+
export function renderCommandNotice(notice) {
|
|
77
|
+
const lines = [`**${notice.title}**`];
|
|
78
|
+
if (notice.value) {
|
|
79
|
+
lines.push(notice.valueMode === 'plain' ? notice.value : `\`${notice.value}\``);
|
|
80
|
+
}
|
|
81
|
+
if (notice.detail)
|
|
82
|
+
lines.push(notice.detail);
|
|
83
|
+
return lines.join('\n\n');
|
|
84
|
+
}
|
|
85
|
+
export function renderCommandSelectionMarkdown(view) {
|
|
86
|
+
const title = view.detail ? `**${view.title}** · \`${view.detail}\`` : `**${view.title}**`;
|
|
87
|
+
const lines = [title];
|
|
88
|
+
if (view.metaLines.length)
|
|
89
|
+
lines.push(...view.metaLines.map(line => `*${line}*`));
|
|
90
|
+
if (view.items.length) {
|
|
91
|
+
lines.push('', ...view.items.map((item, index) => formatCommandItemMarkdown(item, index)));
|
|
92
|
+
}
|
|
93
|
+
else if (view.emptyText) {
|
|
94
|
+
lines.push('', `*${view.emptyText}*`);
|
|
95
|
+
}
|
|
96
|
+
if (view.helperText)
|
|
97
|
+
lines.push('', `*${view.helperText}*`);
|
|
98
|
+
return lines.join('\n');
|
|
99
|
+
}
|
|
100
|
+
export function renderCommandSelectionCard(view) {
|
|
101
|
+
return {
|
|
102
|
+
markdown: renderCommandSelectionMarkdown(view),
|
|
103
|
+
rows: view.rows.map(row => ({ actions: row.map(actionButton) })),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Strip code-fence markers (``` / ~~~) from text that is not meant to be
|
|
108
|
+
* rendered as full markdown (thinking, activity). Truncation by
|
|
109
|
+
* extractThinkingTail can leave stray fences that open unwanted code blocks.
|
|
110
|
+
*/
|
|
111
|
+
function stripCodeFences(text) {
|
|
112
|
+
return text.replace(/^(`{3,}|~{3,}).*$/gm, '');
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Ensure code fences in markdown text are balanced. If an odd number of
|
|
116
|
+
* fence markers is detected, append a closing fence so partial code blocks
|
|
117
|
+
* do not swallow the rest of the card.
|
|
118
|
+
*/
|
|
119
|
+
function ensureBalancedCodeFences(text) {
|
|
120
|
+
let inCode = false;
|
|
121
|
+
for (const line of text.split('\n')) {
|
|
122
|
+
const trimmed = line.trimStart();
|
|
123
|
+
if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) {
|
|
124
|
+
inCode = !inCode;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return inCode ? text + '\n```' : text;
|
|
128
|
+
}
|
|
129
|
+
function escapeFeishuMarkdownText(text) {
|
|
130
|
+
return text.replace(/([\\`*_{}[\]()#+\-.!|>~])/g, '\\$1');
|
|
131
|
+
}
|
|
132
|
+
function renderFeishuQuote(text) {
|
|
133
|
+
return text
|
|
134
|
+
.split('\n')
|
|
135
|
+
.map(line => `> ${escapeFeishuMarkdownText(line)}`)
|
|
136
|
+
.join('\n');
|
|
137
|
+
}
|
|
138
|
+
export function renderSessionTurnMarkdown(userText, assistantText) {
|
|
139
|
+
const parts = [];
|
|
140
|
+
const user = String(userText || '').trim();
|
|
141
|
+
const assistant = String(assistantText || '').trim();
|
|
142
|
+
if (user || assistant)
|
|
143
|
+
parts.push('**Recent Context**');
|
|
144
|
+
if (user)
|
|
145
|
+
parts.push('**User**', renderFeishuQuote(user));
|
|
146
|
+
if (assistant)
|
|
147
|
+
parts.push('**Assistant**', assistant);
|
|
148
|
+
return parts.join('\n\n');
|
|
149
|
+
}
|
|
150
|
+
export function buildHumanLoopPromptMarkdown(prompt) {
|
|
151
|
+
const question = currentHumanLoopQuestion(prompt);
|
|
152
|
+
const lines = [`**${prompt.title}**`];
|
|
153
|
+
if (prompt.detail)
|
|
154
|
+
lines.push(`\`${prompt.detail}\``);
|
|
155
|
+
lines.push(`*${humanLoopAnsweredCount(prompt)}/${prompt.questions.length} answered*`);
|
|
156
|
+
if (!question)
|
|
157
|
+
return lines.join('\n\n');
|
|
158
|
+
if (question.header.trim())
|
|
159
|
+
lines.push(`**${question.header}**`);
|
|
160
|
+
lines.push(question.prompt);
|
|
161
|
+
const options = question.options || [];
|
|
162
|
+
if (options.length) {
|
|
163
|
+
lines.push(options.map((option, index) => {
|
|
164
|
+
const detail = option.description ? `\n ${option.description}` : '';
|
|
165
|
+
return `${index + 1}. ${option.label}${detail}`;
|
|
166
|
+
}).join('\n'));
|
|
167
|
+
}
|
|
168
|
+
if (isHumanLoopAwaitingText(prompt)) {
|
|
169
|
+
lines.push(`*${question.secret ? 'Reply in chat with the secret value.' : 'Reply in chat with text to answer.'}*`);
|
|
170
|
+
}
|
|
171
|
+
if (prompt.hint)
|
|
172
|
+
lines.push(`*${prompt.hint}*`);
|
|
173
|
+
if (prompt.questions.length > 1) {
|
|
174
|
+
lines.push(prompt.questions.map((item, index) => {
|
|
175
|
+
const summary = summarizeHumanLoopAnswer(prompt, item);
|
|
176
|
+
const answered = isHumanLoopQuestionAnswered(prompt, index);
|
|
177
|
+
return `${answered ? '●' : '○'} ${item.header || item.prompt}: ${summary.display}`;
|
|
178
|
+
}).join('\n'));
|
|
179
|
+
}
|
|
180
|
+
return lines.join('\n\n');
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Closed-state rendering for a resolved human-loop card. Collapsed to the
|
|
184
|
+
* title + one row per question showing the chosen answer. No buttons, no
|
|
185
|
+
* pending-state copy.
|
|
186
|
+
*/
|
|
187
|
+
export function buildAnsweredHumanLoopPromptMarkdown(prompt, summary) {
|
|
188
|
+
const symbol = summary.status === 'cancelled' ? '⊘' : '✓';
|
|
189
|
+
const lines = [`**${symbol} ${prompt.title}**`];
|
|
190
|
+
if (summary.status === 'cancelled')
|
|
191
|
+
lines.push('*Cancelled.*');
|
|
192
|
+
for (const row of summary.rows) {
|
|
193
|
+
lines.push(`${row.label}: **${row.display}**`);
|
|
194
|
+
}
|
|
195
|
+
return lines.join('\n');
|
|
196
|
+
}
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
// LivePreview renderer — produces Markdown for Feishu card elements
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
export function buildInitialPreviewMarkdown(agent, model, effort, waiting = false, queuePosition = 0) {
|
|
201
|
+
const parts = [];
|
|
202
|
+
if (waiting) {
|
|
203
|
+
const queueLabel = queuePosition > 0
|
|
204
|
+
? `Queued · ${queuePosition} ${queuePosition === 1 ? 'task' : 'tasks'} ahead`
|
|
205
|
+
: 'Waiting in queue...';
|
|
206
|
+
parts.push(queueLabel);
|
|
207
|
+
}
|
|
208
|
+
if (model)
|
|
209
|
+
parts.push(model);
|
|
210
|
+
else
|
|
211
|
+
parts.push(agent);
|
|
212
|
+
if (effort)
|
|
213
|
+
parts.push(`${effort}`);
|
|
214
|
+
return parts.join(' · ');
|
|
215
|
+
}
|
|
216
|
+
function buildPreviewMarkdown(input, options) {
|
|
217
|
+
const data = extractStreamPreviewData(input);
|
|
218
|
+
const parts = [];
|
|
219
|
+
if (data.planDisplay) {
|
|
220
|
+
// First line of planDisplay is already "Plan N/M" — promote it to the
|
|
221
|
+
// bold header instead of stacking a second "**Plan**" line above it.
|
|
222
|
+
const text = stripCodeFences(data.planDisplay);
|
|
223
|
+
const nl = text.indexOf('\n');
|
|
224
|
+
parts.push(nl >= 0 ? `**${text.slice(0, nl)}**${text.slice(nl)}` : `**${text}**`);
|
|
225
|
+
}
|
|
226
|
+
if (data.activityDisplay) {
|
|
227
|
+
parts.push(`**Activity**\n${stripCodeFences(trimActivityForPreview(data.activityDisplay, data.maxActivity))}`);
|
|
228
|
+
}
|
|
229
|
+
if (data.subAgentsDisplay) {
|
|
230
|
+
parts.push(`**Sub-agent**\n${stripCodeFences(data.subAgentsDisplay)}`);
|
|
231
|
+
}
|
|
232
|
+
if (data.thinkDisplay && !data.display) {
|
|
233
|
+
// Elapsed lives in the footer only (single timer). Header is the bare label.
|
|
234
|
+
parts.push(`**${data.label}**\n${stripCodeFences(data.thinkDisplay)}`);
|
|
235
|
+
}
|
|
236
|
+
else if (data.display) {
|
|
237
|
+
if (data.rawThinking) {
|
|
238
|
+
parts.push(`**${data.label}**\n${stripCodeFences(data.thinkSnippet)}`);
|
|
239
|
+
}
|
|
240
|
+
parts.push(ensureBalancedCodeFences(data.preview));
|
|
241
|
+
}
|
|
242
|
+
else if (data.thinkingProgressText) {
|
|
243
|
+
// Thinking phase with no streamed thinking/body text yet — show the bare
|
|
244
|
+
// "{thinkLabel}" so the card isn't blank. The elapsed tick lives in the
|
|
245
|
+
// footer only (one timer, not two); the footer re-renders on the channel
|
|
246
|
+
// heartbeat, so the card still visibly advances.
|
|
247
|
+
parts.push(`**${data.label}**`);
|
|
248
|
+
}
|
|
249
|
+
if (options?.includeFooter !== false) {
|
|
250
|
+
parts.push(formatPreviewFooter(input.agent, input.elapsedMs, input.meta ?? null, {
|
|
251
|
+
model: input.model,
|
|
252
|
+
effort: input.effort,
|
|
253
|
+
}));
|
|
254
|
+
}
|
|
255
|
+
return parts.join('\n\n');
|
|
256
|
+
}
|
|
257
|
+
export function buildStreamPreviewMarkdown(input) {
|
|
258
|
+
return buildPreviewMarkdown(input, { includeFooter: true });
|
|
259
|
+
}
|
|
260
|
+
export const feishuPreviewRenderer = {
|
|
261
|
+
renderInitial: buildInitialPreviewMarkdown,
|
|
262
|
+
renderStream: buildStreamPreviewMarkdown,
|
|
263
|
+
};
|
|
264
|
+
export function buildFinalReplyRender(agent, result) {
|
|
265
|
+
const data = extractFinalReplyData(agent, result);
|
|
266
|
+
const footerText = `\n\n${formatFinalFooter(data.footerStatus, agent, data.elapsedMs, result.contextPercent ?? null, {
|
|
267
|
+
model: result.model,
|
|
268
|
+
effort: result.thinkingEffort,
|
|
269
|
+
})}`;
|
|
270
|
+
let activityText = '';
|
|
271
|
+
let activityNoteText = '';
|
|
272
|
+
if (data.activityNarrative) {
|
|
273
|
+
activityText = `**Activity**\n${stripCodeFences(data.activityNarrative)}\n\n`;
|
|
274
|
+
}
|
|
275
|
+
if (data.activityCommandSummary) {
|
|
276
|
+
activityNoteText = `*${data.activityCommandSummary}*\n\n`;
|
|
277
|
+
}
|
|
278
|
+
let thinkingText = '';
|
|
279
|
+
if (data.thinkingDisplay) {
|
|
280
|
+
thinkingText = `**${data.thinkLabel}**\n${stripCodeFences(data.thinkingDisplay)}\n\n`;
|
|
281
|
+
}
|
|
282
|
+
let statusText = '';
|
|
283
|
+
if (data.statusLines) {
|
|
284
|
+
statusText = `**⚠ Incomplete Response**\n${data.statusLines.join('\n')}\n\n`;
|
|
285
|
+
}
|
|
286
|
+
const headerText = `${activityText}${activityNoteText}${statusText}${thinkingText}`;
|
|
287
|
+
const bodyText = ensureBalancedCodeFences(data.bodyMessage);
|
|
288
|
+
return {
|
|
289
|
+
fullText: `${headerText}${bodyText}${footerText}`,
|
|
290
|
+
headerText,
|
|
291
|
+
bodyText,
|
|
292
|
+
footerText,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
// Command renderers — produce Markdown for Feishu cards
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
export function renderStart(d) {
|
|
299
|
+
const lines = [
|
|
300
|
+
`**${d.title}** v${d.version}`,
|
|
301
|
+
d.subtitle,
|
|
302
|
+
'',
|
|
303
|
+
`**Agent:** ${d.agent}`,
|
|
304
|
+
`**Workdir:** \`${d.workdir}\``,
|
|
305
|
+
'',
|
|
306
|
+
'**Agents**',
|
|
307
|
+
...d.agentDetails.map(a => {
|
|
308
|
+
let line = ` **${a.agent}**: ${a.model}`;
|
|
309
|
+
if (a.effort)
|
|
310
|
+
line += ` (effort: ${a.effort})`;
|
|
311
|
+
return line;
|
|
312
|
+
}),
|
|
313
|
+
'',
|
|
314
|
+
'**Commands**',
|
|
315
|
+
...d.commands.map(c => `/${c.command} — ${c.description}`),
|
|
316
|
+
];
|
|
317
|
+
return lines.join('\n');
|
|
318
|
+
}
|
|
319
|
+
export function renderSessionsPage(d) {
|
|
320
|
+
const agentChips = Object.entries(d.agentTotals)
|
|
321
|
+
.sort((a, b) => b[1] - a[1])
|
|
322
|
+
.map(([agent, count]) => `${agent}:${count}`)
|
|
323
|
+
.join(' · ');
|
|
324
|
+
const header = d.workspaceName
|
|
325
|
+
? (agentChips ? `${d.workspaceName} · ${agentChips}` : d.workspaceName)
|
|
326
|
+
: (agentChips || 'sessions');
|
|
327
|
+
const lines = [
|
|
328
|
+
`**${header}** (${d.total}) p${d.page + 1}/${d.totalPages}`,
|
|
329
|
+
'',
|
|
330
|
+
];
|
|
331
|
+
if (!d.sessions.length) {
|
|
332
|
+
lines.push('*No sessions found.*');
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
for (let i = 0; i < d.sessions.length; i++) {
|
|
336
|
+
const s = d.sessions[i];
|
|
337
|
+
const icon = s.isRunning ? '🟢' : s.isCurrent ? '●' : '○';
|
|
338
|
+
lines.push(`${icon} **${i + 1}.** [${s.agent}] ${s.title} ${s.time}${s.isCurrent ? ' ← current' : ''}`);
|
|
339
|
+
}
|
|
340
|
+
lines.push('');
|
|
341
|
+
lines.push('*Use the controls below to switch, or reply with session number / "new".*');
|
|
342
|
+
}
|
|
343
|
+
if (d.totalPages > 1) {
|
|
344
|
+
lines.push(`\nPage ${d.page + 1}/${d.totalPages}. Use the page controls below or reply "p2", "p3" etc. to navigate.`);
|
|
345
|
+
}
|
|
346
|
+
return lines.join('\n');
|
|
347
|
+
}
|
|
348
|
+
export function renderAgentsList(d) {
|
|
349
|
+
const lines = ['**Available Agents**', ''];
|
|
350
|
+
for (const a of d.agents) {
|
|
351
|
+
const status = !a.installed ? '❌' : a.isCurrent ? '●' : '○';
|
|
352
|
+
lines.push(`${status} **${a.agent}**${a.isCurrent ? ' (current)' : ''}`);
|
|
353
|
+
if (a.installed) {
|
|
354
|
+
if (a.version)
|
|
355
|
+
lines.push(` Version: \`${a.version}\``);
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
lines.push(' Not installed');
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
lines.push('');
|
|
362
|
+
lines.push('*Use the controls below to switch, or reply with agent name (e.g. "claude", "codex").*');
|
|
363
|
+
return lines.join('\n');
|
|
364
|
+
}
|
|
365
|
+
export function renderModelsList(d) {
|
|
366
|
+
const lines = [`**Models for ${d.agent}**`];
|
|
367
|
+
if (d.sources.length)
|
|
368
|
+
lines.push(`*Source: ${d.sources.join(', ')}*`);
|
|
369
|
+
if (d.note)
|
|
370
|
+
lines.push(`*${d.note}*`);
|
|
371
|
+
lines.push('');
|
|
372
|
+
if (!d.models.length) {
|
|
373
|
+
lines.push('*No discoverable models found.*');
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
for (let i = 0; i < d.models.length; i++) {
|
|
377
|
+
const m = d.models[i];
|
|
378
|
+
const status = m.isCurrent ? '●' : '○';
|
|
379
|
+
const display = m.alias ? `${m.alias} (${m.id})` : m.id;
|
|
380
|
+
lines.push(`${status} **${i + 1}.** \`${display}\`${m.isCurrent ? ' ← current' : ''}`);
|
|
381
|
+
}
|
|
382
|
+
lines.push('');
|
|
383
|
+
lines.push('*Use the controls below to switch, or reply with model number / ID.*');
|
|
384
|
+
}
|
|
385
|
+
if (d.effort) {
|
|
386
|
+
lines.push('');
|
|
387
|
+
lines.push(`**Thinking Effort:** \`${d.effort.current}\``);
|
|
388
|
+
lines.push(d.effort.levels.map(l => l.isCurrent ? `**[${l.label}]**` : l.label).join(' | '));
|
|
389
|
+
}
|
|
390
|
+
return lines.join('\n');
|
|
391
|
+
}
|
|
392
|
+
export function renderSkillsList(d) {
|
|
393
|
+
const lines = [`**Project Skills** (${d.skills.length})`, '', `**Agent:** ${d.agent}`, `**Workdir:** \`${d.workdir}\``];
|
|
394
|
+
if (!d.skills.length) {
|
|
395
|
+
lines.push('', '*No project skills found in `.pikiloop/skills/` or `.claude/commands/`.*');
|
|
396
|
+
return lines.join('\n');
|
|
397
|
+
}
|
|
398
|
+
lines.push('');
|
|
399
|
+
for (const skill of d.skills) {
|
|
400
|
+
lines.push(`**/${skill.command}** — ${skill.label}`);
|
|
401
|
+
if (skill.description)
|
|
402
|
+
lines.push(skill.description);
|
|
403
|
+
}
|
|
404
|
+
lines.push('', '*Tap a button below or send the command directly.*');
|
|
405
|
+
return lines.join('\n');
|
|
406
|
+
}
|
|
407
|
+
export function renderSessionsPageCard(d) {
|
|
408
|
+
const sessionButtons = d.sessions.map(s => {
|
|
409
|
+
const prefix = s.isCurrent ? '● ' : s.isRunning ? '🟢 ' : '';
|
|
410
|
+
return cardButton(`${prefix}${s.title}`, `sess:${s.key}`, s.isCurrent);
|
|
411
|
+
});
|
|
412
|
+
const navButtons = [];
|
|
413
|
+
if (d.page > 0)
|
|
414
|
+
navButtons.push(cardButton(`◀ p${d.page}`, `sp:${d.page - 1}`));
|
|
415
|
+
navButtons.push(cardButton('+ New', 'sess:new'));
|
|
416
|
+
if (d.page < d.totalPages - 1)
|
|
417
|
+
navButtons.push(cardButton(`p${d.page + 2} ▶`, `sp:${d.page + 1}`));
|
|
418
|
+
return {
|
|
419
|
+
markdown: renderSessionsPage(d),
|
|
420
|
+
rows: [
|
|
421
|
+
...cardRows(sessionButtons),
|
|
422
|
+
...(navButtons.length ? [{ actions: navButtons }] : []),
|
|
423
|
+
],
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
export function renderAgentsListCard(d) {
|
|
427
|
+
const actions = d.agents
|
|
428
|
+
.filter(a => a.installed)
|
|
429
|
+
.map(a => cardButton(a.isCurrent ? `● ${a.agent}` : a.agent, `ag:${a.agent}`, a.isCurrent));
|
|
430
|
+
return {
|
|
431
|
+
markdown: renderAgentsList(d),
|
|
432
|
+
rows: cardRows(actions),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
export function renderModelsListCard(d) {
|
|
436
|
+
const modelRows = cardRows(d.models.map(m => cardButton(m.isCurrent ? `● ${m.alias || m.id}` : (m.alias || m.id), `mod:${m.id}`, m.isCurrent)));
|
|
437
|
+
const effortRows = d.effort
|
|
438
|
+
? cardRows(d.effort.levels.map(l => cardButton(l.isCurrent ? `● ${l.label}` : l.label, `eff:${l.id}`, l.isCurrent)))
|
|
439
|
+
: [];
|
|
440
|
+
return {
|
|
441
|
+
markdown: renderModelsList(d),
|
|
442
|
+
rows: [...modelRows, ...effortRows],
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
export function renderSkillsCard(d) {
|
|
446
|
+
return {
|
|
447
|
+
markdown: renderSkillsList(d),
|
|
448
|
+
rows: cardRows(d.skills.map(skill => cardButton(skill.label, `skill:${skill.command}`)), 2),
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
export function renderStatus(d) {
|
|
452
|
+
const gitLine = formatGitStatusLine(d.git);
|
|
453
|
+
const lines = [
|
|
454
|
+
`**pikiloop** v${d.version}`,
|
|
455
|
+
'',
|
|
456
|
+
`**Uptime:** ${fmtUptime(d.uptime)}`,
|
|
457
|
+
`**Memory:** ${(d.memRss / 1024 / 1024).toFixed(0)}MB RSS / ${(d.memHeap / 1024 / 1024).toFixed(0)}MB heap`,
|
|
458
|
+
`**PID:** ${d.pid}`,
|
|
459
|
+
`**Workdir:** \`${d.workdir}\``,
|
|
460
|
+
...(gitLine ? [`**Git:** ${gitLine}`] : []),
|
|
461
|
+
'',
|
|
462
|
+
`**Agent:** ${d.agent}`,
|
|
463
|
+
`**Model:** ${d.model}`,
|
|
464
|
+
`**Session:** ${d.sessionId ? `\`${d.sessionId.slice(0, 16)}\`` : '(new)'}`,
|
|
465
|
+
`**Active Tasks:** ${d.activeTasksCount}`,
|
|
466
|
+
];
|
|
467
|
+
if (d.running) {
|
|
468
|
+
lines.push(`**Running:** ${fmtUptime(Date.now() - d.running.startedAt)} - ${summarizePromptForStatus(d.running.prompt)}`);
|
|
469
|
+
}
|
|
470
|
+
// Provider usage
|
|
471
|
+
const usageLines = buildProviderUsageLines(d.usage);
|
|
472
|
+
if (usageLines.length > 1) {
|
|
473
|
+
lines.push('');
|
|
474
|
+
for (const line of usageLines) {
|
|
475
|
+
lines.push(line.text);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
lines.push('', '**Bot Usage**', ` Turns: ${d.stats.totalTurns}`);
|
|
479
|
+
if (d.stats.totalInputTokens || d.stats.totalOutputTokens) {
|
|
480
|
+
lines.push(` In: ${fmtTokens(d.stats.totalInputTokens)} Out: ${fmtTokens(d.stats.totalOutputTokens)}`);
|
|
481
|
+
if (d.stats.totalCachedTokens)
|
|
482
|
+
lines.push(` Cached: ${fmtTokens(d.stats.totalCachedTokens)}`);
|
|
483
|
+
}
|
|
484
|
+
return lines.join('\n');
|
|
485
|
+
}
|
|
486
|
+
// ---------------------------------------------------------------------------
|
|
487
|
+
// Directory browser (interactive workdir switcher)
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
class PathRegistry {
|
|
490
|
+
pathToId = new Map();
|
|
491
|
+
idToPath = new Map();
|
|
492
|
+
nextId = 1;
|
|
493
|
+
register(dirPath) {
|
|
494
|
+
let id = this.pathToId.get(dirPath);
|
|
495
|
+
if (id != null)
|
|
496
|
+
return id;
|
|
497
|
+
id = this.nextId++;
|
|
498
|
+
this.pathToId.set(dirPath, id);
|
|
499
|
+
this.idToPath.set(id, dirPath);
|
|
500
|
+
if (this.pathToId.size > 500) {
|
|
501
|
+
const oldest = [...this.pathToId.entries()].slice(0, 200);
|
|
502
|
+
for (const [oldPath, oldId] of oldest) {
|
|
503
|
+
this.pathToId.delete(oldPath);
|
|
504
|
+
this.idToPath.delete(oldId);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return id;
|
|
508
|
+
}
|
|
509
|
+
resolve(id) {
|
|
510
|
+
return this.idToPath.get(id);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
const feishuPathRegistry = new PathRegistry();
|
|
514
|
+
const DIR_PAGE_SIZE = 8;
|
|
515
|
+
export function resolveFeishuRegisteredPath(id) {
|
|
516
|
+
return feishuPathRegistry.resolve(id);
|
|
517
|
+
}
|
|
518
|
+
export function buildSwitchWorkdirCard(currentWorkdir, browsePath, page = 0, opts = {}) {
|
|
519
|
+
const dirs = listSubdirs(browsePath);
|
|
520
|
+
const totalPages = Math.max(1, Math.ceil(dirs.length / DIR_PAGE_SIZE));
|
|
521
|
+
const currentPage = Math.min(Math.max(0, page), totalPages - 1);
|
|
522
|
+
const slice = dirs.slice(currentPage * DIR_PAGE_SIZE, (currentPage + 1) * DIR_PAGE_SIZE);
|
|
523
|
+
// Text
|
|
524
|
+
const lines = ['**Workdir**'];
|
|
525
|
+
lines.push(`● \`${currentWorkdir}\``);
|
|
526
|
+
if (browsePath !== currentWorkdir)
|
|
527
|
+
lines.push(`○ \`${browsePath}\``);
|
|
528
|
+
if (opts.savedWorkspaceCount && opts.savedWorkspaceCount > 0) {
|
|
529
|
+
lines.push('', `_Tip: ${opts.savedWorkspaceCount} saved workspace${opts.savedWorkspaceCount === 1 ? '' : 's'} — use /workspaces for one-tap switching._`);
|
|
530
|
+
}
|
|
531
|
+
// Directory buttons (2 per row)
|
|
532
|
+
const dirRows = [];
|
|
533
|
+
for (let i = 0; i < slice.length; i += 2) {
|
|
534
|
+
const rowActions = [];
|
|
535
|
+
for (let j = i; j < Math.min(i + 2, slice.length); j++) {
|
|
536
|
+
const fullPath = path.join(browsePath, slice[j]);
|
|
537
|
+
const id = feishuPathRegistry.register(fullPath);
|
|
538
|
+
rowActions.push(cardButton(slice[j], `sw:n:${id}:0`));
|
|
539
|
+
}
|
|
540
|
+
dirRows.push({ actions: rowActions });
|
|
541
|
+
}
|
|
542
|
+
// Nav row: parent + pagination
|
|
543
|
+
const navActions = [];
|
|
544
|
+
const parent = path.dirname(browsePath);
|
|
545
|
+
if (parent !== browsePath) {
|
|
546
|
+
navActions.push(cardButton('⬆ ..', `sw:n:${feishuPathRegistry.register(parent)}:0`));
|
|
547
|
+
}
|
|
548
|
+
if (totalPages > 1) {
|
|
549
|
+
const browseId = feishuPathRegistry.register(browsePath);
|
|
550
|
+
if (currentPage > 0)
|
|
551
|
+
navActions.push(cardButton(`◀ ${currentPage}/${totalPages}`, `sw:n:${browseId}:${currentPage - 1}`));
|
|
552
|
+
if (currentPage < totalPages - 1)
|
|
553
|
+
navActions.push(cardButton(`${currentPage + 2}/${totalPages} ▶`, `sw:n:${browseId}:${currentPage + 1}`));
|
|
554
|
+
}
|
|
555
|
+
// Select button
|
|
556
|
+
const selectActions = [
|
|
557
|
+
cardButton('✓ Use This', `sw:s:${feishuPathRegistry.register(browsePath)}`, true),
|
|
558
|
+
];
|
|
559
|
+
const rows = [
|
|
560
|
+
...dirRows,
|
|
561
|
+
...(navActions.length ? [{ actions: navActions }] : []),
|
|
562
|
+
{ actions: selectActions },
|
|
563
|
+
];
|
|
564
|
+
return { markdown: lines.join('\n'), rows };
|
|
565
|
+
}
|
|
566
|
+
const WORKSPACES_PAGE_SIZE = 10;
|
|
567
|
+
export function buildWorkspacesCard(data, page = 0) {
|
|
568
|
+
const { workspaces, currentWorkdir } = data;
|
|
569
|
+
const lines = ['**Workspaces**'];
|
|
570
|
+
if (workspaces.length === 0) {
|
|
571
|
+
lines.push('', 'No saved workspaces yet.', '', 'Add workspaces from the Dashboard (Sessions → Add Workspace), then come back to switch with one tap.', '', 'You can still browse the file system with /switch.');
|
|
572
|
+
return { markdown: lines.join('\n'), rows: [] };
|
|
573
|
+
}
|
|
574
|
+
lines.push(`● \`${currentWorkdir}\``);
|
|
575
|
+
const totalPages = Math.max(1, Math.ceil(workspaces.length / WORKSPACES_PAGE_SIZE));
|
|
576
|
+
const currentPage = Math.min(Math.max(0, page), totalPages - 1);
|
|
577
|
+
const slice = workspaces.slice(currentPage * WORKSPACES_PAGE_SIZE, (currentPage + 1) * WORKSPACES_PAGE_SIZE);
|
|
578
|
+
const rows = [];
|
|
579
|
+
for (const ws of slice) {
|
|
580
|
+
const marker = ws.isCurrent ? '✓ ' : ws.exists ? '' : '⚠ ';
|
|
581
|
+
const id = feishuPathRegistry.register(ws.path);
|
|
582
|
+
rows.push({ actions: [cardButton(`${marker}${ws.name}`, `wsp:s:${id}`, ws.isCurrent ? false : true)] });
|
|
583
|
+
}
|
|
584
|
+
if (totalPages > 1) {
|
|
585
|
+
const navActions = [];
|
|
586
|
+
if (currentPage > 0)
|
|
587
|
+
navActions.push(cardButton(`◀ ${currentPage}/${totalPages}`, `wsp:p:${currentPage - 1}`));
|
|
588
|
+
if (currentPage < totalPages - 1)
|
|
589
|
+
navActions.push(cardButton(`${currentPage + 2}/${totalPages} ▶`, `wsp:p:${currentPage + 1}`));
|
|
590
|
+
if (navActions.length)
|
|
591
|
+
rows.push({ actions: navActions });
|
|
592
|
+
}
|
|
593
|
+
lines.push('', `_Tap a workspace to switch. ${workspaces.length} saved._`);
|
|
594
|
+
return { markdown: lines.join('\n'), rows };
|
|
595
|
+
}
|
|
596
|
+
export function renderHost(d) {
|
|
597
|
+
const lines = [
|
|
598
|
+
'**Host**',
|
|
599
|
+
'',
|
|
600
|
+
`**Name:** ${d.hostName}`,
|
|
601
|
+
`**CPU:** ${d.cpuModel} x${d.cpuCount}`,
|
|
602
|
+
d.cpuUsage
|
|
603
|
+
? `**CPU Usage:** ${d.cpuUsage.usedPercent.toFixed(1)}% (${d.cpuUsage.userPercent.toFixed(1)}% user, ${d.cpuUsage.sysPercent.toFixed(1)}% sys, ${d.cpuUsage.idlePercent.toFixed(1)}% idle)`
|
|
604
|
+
: '**CPU Usage:** unavailable',
|
|
605
|
+
`**Memory:** ${fmtBytes(d.memoryUsed)} / ${fmtBytes(d.totalMem)} (${d.memoryPercent.toFixed(0)}%)`,
|
|
606
|
+
`**Available:** ${fmtBytes(d.memoryAvailable)}`,
|
|
607
|
+
`**Battery:** ${d.battery ? `${d.battery.percent} (${d.battery.state})` : 'unavailable'}`,
|
|
608
|
+
];
|
|
609
|
+
if (d.disk)
|
|
610
|
+
lines.push(`**Disk:** ${d.disk.used} used / ${d.disk.total} total (${d.disk.percent})`);
|
|
611
|
+
lines.push(`\n**Process:** PID ${d.selfPid} | RSS ${fmtBytes(d.selfRss)} | Heap ${fmtBytes(d.selfHeap)}`);
|
|
612
|
+
if (d.topProcs.length > 1) {
|
|
613
|
+
lines.push('\n**Top Processes**');
|
|
614
|
+
lines.push('```');
|
|
615
|
+
lines.push(...d.topProcs);
|
|
616
|
+
lines.push('```');
|
|
617
|
+
}
|
|
618
|
+
return lines.join('\n');
|
|
619
|
+
}
|