pikiclaw 0.2.69 → 0.2.71
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 +1 -0
- package/dist/bot-command-ui.js +112 -14
- package/dist/bot-commands.js +1 -1
- package/dist/bot-feishu-render.js +42 -11
- package/dist/bot-feishu.js +53 -7
- package/dist/bot-handler.js +1 -1
- package/dist/bot-telegram-render.js +8 -4
- package/dist/bot-telegram.js +62 -10
- package/dist/bot.js +56 -1
- package/dist/channel-feishu.js +4 -2
- package/dist/cli.js +9 -4
- package/dist/code-agent.js +2 -3
- package/dist/driver-claude.js +3 -2
- package/dist/process-control.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -142,6 +142,7 @@ npx pikiclaw@latest --doctor # check environment only
|
|
|
142
142
|
|
|
143
143
|
- Streaming preview with continuous message updates
|
|
144
144
|
- Session switching, resume, and multi-turn conversations
|
|
145
|
+
- Task queue with **Steer** — interrupt the running task and let a queued message jump ahead
|
|
145
146
|
- Working directory browsing and switching
|
|
146
147
|
- File attachments automatically enter the session workspace
|
|
147
148
|
- Long-task sleep prevention, watchdog, and auto-restart
|
package/dist/bot-command-ui.js
CHANGED
|
@@ -31,6 +31,12 @@ export function encodeCommandAction(action) {
|
|
|
31
31
|
return `mod:${action.modelId}`;
|
|
32
32
|
case 'effort.set':
|
|
33
33
|
return `eff:${action.effort}`;
|
|
34
|
+
case 'models.select.model':
|
|
35
|
+
return `md:${action.modelId}`;
|
|
36
|
+
case 'models.select.effort':
|
|
37
|
+
return `ed:${action.effort}`;
|
|
38
|
+
case 'models.confirm':
|
|
39
|
+
return 'mc';
|
|
34
40
|
case 'skill.run':
|
|
35
41
|
return `skr:${action.command}`;
|
|
36
42
|
}
|
|
@@ -70,6 +76,20 @@ export function decodeCommandAction(data) {
|
|
|
70
76
|
return null;
|
|
71
77
|
return { kind: 'effort.set', effort };
|
|
72
78
|
}
|
|
79
|
+
if (data.startsWith('md:')) {
|
|
80
|
+
const modelId = data.slice(3);
|
|
81
|
+
if (!modelId)
|
|
82
|
+
return null;
|
|
83
|
+
return { kind: 'models.select.model', modelId };
|
|
84
|
+
}
|
|
85
|
+
if (data.startsWith('ed:')) {
|
|
86
|
+
const effort = data.slice(3);
|
|
87
|
+
if (!effort)
|
|
88
|
+
return null;
|
|
89
|
+
return { kind: 'models.select.effort', effort };
|
|
90
|
+
}
|
|
91
|
+
if (data === 'mc')
|
|
92
|
+
return { kind: 'models.confirm' };
|
|
73
93
|
if (data.startsWith('skr:')) {
|
|
74
94
|
const command = data.slice(4);
|
|
75
95
|
if (!command)
|
|
@@ -134,24 +154,57 @@ export function buildAgentsCommandView(bot, chatId) {
|
|
|
134
154
|
rows: chunkRows(actions, 3),
|
|
135
155
|
};
|
|
136
156
|
}
|
|
137
|
-
|
|
157
|
+
const modelsDrafts = new Map();
|
|
158
|
+
async function initModelsDraft(bot, chatId) {
|
|
138
159
|
const data = await getModelsListData(bot, chatId);
|
|
139
|
-
const
|
|
160
|
+
const draft = { modelId: data.currentModel, effort: data.effort?.current ?? null };
|
|
161
|
+
modelsDrafts.set(String(chatId), draft);
|
|
162
|
+
return draft;
|
|
163
|
+
}
|
|
164
|
+
export async function buildModelsCommandView(bot, chatId, draft) {
|
|
165
|
+
const data = await getModelsListData(bot, chatId);
|
|
166
|
+
// Initialize draft from current state or use the provided one
|
|
167
|
+
const d = draft ?? {
|
|
168
|
+
modelId: data.currentModel,
|
|
169
|
+
effort: data.effort?.current ?? null,
|
|
170
|
+
};
|
|
171
|
+
modelsDrafts.set(String(chatId), d);
|
|
172
|
+
const isSelected = (modelId) => modelMatchesSelection(data.agent, modelId, d.modelId);
|
|
173
|
+
const models = [...data.models].sort((a, b) => Number(isSelected(b.id)) - Number(isSelected(a.id)));
|
|
140
174
|
const modelButtons = models.map(model => ({
|
|
141
175
|
label: model.alias || model.id,
|
|
142
|
-
action: { kind: 'model
|
|
143
|
-
state: buttonStateFromFlags({ isCurrent: model.
|
|
144
|
-
primary: model.
|
|
176
|
+
action: { kind: 'models.select.model', modelId: model.id },
|
|
177
|
+
state: buttonStateFromFlags({ isCurrent: isSelected(model.id) }),
|
|
178
|
+
primary: isSelected(model.id),
|
|
145
179
|
}));
|
|
146
|
-
const rows = chunkRows(modelButtons,
|
|
180
|
+
const rows = chunkRows(modelButtons, 1);
|
|
147
181
|
if (data.effort) {
|
|
148
|
-
|
|
182
|
+
const effortButtons = data.effort.levels.map(level => ({
|
|
149
183
|
label: level.label,
|
|
150
|
-
action: { kind: 'effort
|
|
151
|
-
state: buttonStateFromFlags({ isCurrent: level.
|
|
152
|
-
primary: level.
|
|
153
|
-
}))
|
|
184
|
+
action: { kind: 'models.select.effort', effort: level.id },
|
|
185
|
+
state: buttonStateFromFlags({ isCurrent: level.id === d.effort }),
|
|
186
|
+
primary: level.id === d.effort,
|
|
187
|
+
}));
|
|
188
|
+
// Section label — clicking it is harmless (triggers confirm, which is noop if nothing changed)
|
|
189
|
+
rows.push([{
|
|
190
|
+
label: '— Thinking Effort —',
|
|
191
|
+
action: { kind: 'models.confirm' },
|
|
192
|
+
state: 'default',
|
|
193
|
+
primary: false,
|
|
194
|
+
}]);
|
|
195
|
+
// ≤3 levels fit in one row; 4+ split into rows of 2 to avoid Feishu truncation
|
|
196
|
+
rows.push(...chunkRows(effortButtons, effortButtons.length <= 3 ? effortButtons.length : 2));
|
|
154
197
|
}
|
|
198
|
+
// Detect whether draft differs from current live values
|
|
199
|
+
const modelChanged = !modelMatchesSelection(data.agent, d.modelId, data.currentModel);
|
|
200
|
+
const effortChanged = !!(data.effort && d.effort !== data.effort.current);
|
|
201
|
+
const hasChanges = modelChanged || effortChanged;
|
|
202
|
+
rows.push([{
|
|
203
|
+
label: hasChanges ? '✓ Apply' : '✓ OK',
|
|
204
|
+
action: { kind: 'models.confirm' },
|
|
205
|
+
state: 'default',
|
|
206
|
+
primary: hasChanges,
|
|
207
|
+
}]);
|
|
155
208
|
return {
|
|
156
209
|
kind: 'models',
|
|
157
210
|
title: 'Models',
|
|
@@ -159,15 +212,15 @@ export async function buildModelsCommandView(bot, chatId) {
|
|
|
159
212
|
metaLines: [
|
|
160
213
|
...(data.sources.length ? [`Source: ${data.sources.join(', ')}`] : []),
|
|
161
214
|
...(data.note ? [data.note] : []),
|
|
162
|
-
...(data.effort ? [`Thinking Effort: ${
|
|
215
|
+
...(data.effort ? [`Thinking Effort: ${d.effort}`] : []),
|
|
163
216
|
],
|
|
164
217
|
items: models.map(model => ({
|
|
165
218
|
label: model.alias || model.id,
|
|
166
219
|
detail: model.alias ? model.id : null,
|
|
167
|
-
state: buttonStateFromFlags({ isCurrent: model.
|
|
220
|
+
state: buttonStateFromFlags({ isCurrent: isSelected(model.id) }),
|
|
168
221
|
})),
|
|
169
222
|
emptyText: 'No discoverable models found.',
|
|
170
|
-
helperText: data.models.length ? '
|
|
223
|
+
helperText: data.models.length ? 'Select model and effort, then tap Apply.' : null,
|
|
171
224
|
rows,
|
|
172
225
|
};
|
|
173
226
|
}
|
|
@@ -286,6 +339,51 @@ export async function executeCommandAction(bot, chatId, action, opts = {}) {
|
|
|
286
339
|
},
|
|
287
340
|
};
|
|
288
341
|
}
|
|
342
|
+
case 'models.select.model': {
|
|
343
|
+
const draft = modelsDrafts.get(String(chatId)) ?? await initModelsDraft(bot, chatId);
|
|
344
|
+
draft.modelId = action.modelId;
|
|
345
|
+
return { kind: 'view', view: await buildModelsCommandView(bot, chatId, draft), callbackText: '' };
|
|
346
|
+
}
|
|
347
|
+
case 'models.select.effort': {
|
|
348
|
+
const draft = modelsDrafts.get(String(chatId)) ?? await initModelsDraft(bot, chatId);
|
|
349
|
+
draft.effort = action.effort;
|
|
350
|
+
return { kind: 'view', view: await buildModelsCommandView(bot, chatId, draft), callbackText: '' };
|
|
351
|
+
}
|
|
352
|
+
case 'models.confirm': {
|
|
353
|
+
const chat = bot.chat(chatId);
|
|
354
|
+
const draft = modelsDrafts.get(String(chatId));
|
|
355
|
+
modelsDrafts.delete(String(chatId));
|
|
356
|
+
if (!draft)
|
|
357
|
+
return { kind: 'noop', message: 'No changes' };
|
|
358
|
+
const currentModel = bot.modelForAgent(chat.agent);
|
|
359
|
+
const currentEffort = bot.effortForAgent(chat.agent);
|
|
360
|
+
const modelChanged = !modelMatchesSelection(chat.agent, draft.modelId, currentModel);
|
|
361
|
+
const effortChanged = draft.effort != null && draft.effort !== currentEffort;
|
|
362
|
+
if (!modelChanged && !effortChanged) {
|
|
363
|
+
return { kind: 'noop', message: 'No changes' };
|
|
364
|
+
}
|
|
365
|
+
const parts = [];
|
|
366
|
+
if (modelChanged) {
|
|
367
|
+
bot.switchModelForChat(chatId, draft.modelId);
|
|
368
|
+
parts.push(`Model: ${draft.modelId}`);
|
|
369
|
+
}
|
|
370
|
+
if (effortChanged) {
|
|
371
|
+
bot.switchEffortForChat(chatId, draft.effort);
|
|
372
|
+
parts.push(`Effort: ${draft.effort}`);
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
kind: 'notice',
|
|
376
|
+
callbackText: parts.join(', '),
|
|
377
|
+
notice: {
|
|
378
|
+
title: 'Configuration Updated',
|
|
379
|
+
value: parts.join('\n'),
|
|
380
|
+
detail: modelChanged
|
|
381
|
+
? `${chat.agent} · session reset`
|
|
382
|
+
: `${chat.agent} · takes effect on next message`,
|
|
383
|
+
valueMode: 'plain',
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
}
|
|
289
387
|
case 'skill.run': {
|
|
290
388
|
const resolved = resolveSkillPrompt(bot, chatId, action.command, '');
|
|
291
389
|
if (!resolved)
|
package/dist/bot-commands.js
CHANGED
|
@@ -78,7 +78,7 @@ export async function getSessionsPageData(bot, chatId, page, pageSize = 5) {
|
|
|
78
78
|
runState: status.isRunning ? 'running' : s.runState,
|
|
79
79
|
runDetail: s.runDetail,
|
|
80
80
|
});
|
|
81
|
-
const title = s.title ? s.title.replace(/\n/g, ' ').slice(0,
|
|
81
|
+
const title = s.title ? s.title.replace(/\n/g, ' ').slice(0, 20) : sessionKey.slice(0, 20);
|
|
82
82
|
const time = s.createdAt
|
|
83
83
|
? new Date(s.createdAt).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })
|
|
84
84
|
: '?';
|
|
@@ -100,6 +100,29 @@ export function renderCommandSelectionCard(view) {
|
|
|
100
100
|
rows: view.rows.map(row => ({ actions: row.map(actionButton) })),
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Strip code-fence markers (``` / ~~~) from text that is not meant to be
|
|
105
|
+
* rendered as full markdown (thinking, activity). Truncation by
|
|
106
|
+
* extractThinkingTail can leave stray fences that open unwanted code blocks.
|
|
107
|
+
*/
|
|
108
|
+
function stripCodeFences(text) {
|
|
109
|
+
return text.replace(/^(`{3,}|~{3,}).*$/gm, '');
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Ensure code fences in markdown text are balanced. If an odd number of
|
|
113
|
+
* fence markers is detected, append a closing fence so partial code blocks
|
|
114
|
+
* do not swallow the rest of the card.
|
|
115
|
+
*/
|
|
116
|
+
function ensureBalancedCodeFences(text) {
|
|
117
|
+
let inCode = false;
|
|
118
|
+
for (const line of text.split('\n')) {
|
|
119
|
+
const trimmed = line.trimStart();
|
|
120
|
+
if (trimmed.startsWith('```') || trimmed.startsWith('~~~')) {
|
|
121
|
+
inCode = !inCode;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return inCode ? text + '\n```' : text;
|
|
125
|
+
}
|
|
103
126
|
function escapeFeishuMarkdownText(text) {
|
|
104
127
|
return text.replace(/([\\`*_{}[\]()#+\-.!|>~])/g, '\\$1');
|
|
105
128
|
}
|
|
@@ -156,10 +179,14 @@ export function buildHumanLoopPromptMarkdown(prompt) {
|
|
|
156
179
|
// ---------------------------------------------------------------------------
|
|
157
180
|
// LivePreview renderer — produces Markdown for Feishu card elements
|
|
158
181
|
// ---------------------------------------------------------------------------
|
|
159
|
-
export function buildInitialPreviewMarkdown(agent, model, effort, waiting = false) {
|
|
182
|
+
export function buildInitialPreviewMarkdown(agent, model, effort, waiting = false, queuePosition = 0) {
|
|
160
183
|
const parts = [];
|
|
161
|
-
if (waiting)
|
|
162
|
-
|
|
184
|
+
if (waiting) {
|
|
185
|
+
const queueLabel = queuePosition > 0
|
|
186
|
+
? `Queued · ${queuePosition} ${queuePosition === 1 ? 'task' : 'tasks'} ahead`
|
|
187
|
+
: 'Waiting in queue...';
|
|
188
|
+
parts.push(queueLabel);
|
|
189
|
+
}
|
|
163
190
|
if (model)
|
|
164
191
|
parts.push(model);
|
|
165
192
|
else
|
|
@@ -172,19 +199,19 @@ function buildPreviewMarkdown(input, options) {
|
|
|
172
199
|
const data = extractStreamPreviewData(input);
|
|
173
200
|
const parts = [];
|
|
174
201
|
if (data.planDisplay) {
|
|
175
|
-
parts.push(`**Plan**\n${data.planDisplay}`);
|
|
202
|
+
parts.push(`**Plan**\n${stripCodeFences(data.planDisplay)}`);
|
|
176
203
|
}
|
|
177
204
|
if (data.activityDisplay) {
|
|
178
|
-
parts.push(`**Activity**\n${trimActivityForPreview(data.activityDisplay, data.maxActivity)}`);
|
|
205
|
+
parts.push(`**Activity**\n${stripCodeFences(trimActivityForPreview(data.activityDisplay, data.maxActivity))}`);
|
|
179
206
|
}
|
|
180
207
|
if (data.thinkDisplay && !data.display) {
|
|
181
|
-
parts.push(`**${data.label}**\n${data.thinkDisplay}`);
|
|
208
|
+
parts.push(`**${data.label}**\n${stripCodeFences(data.thinkDisplay)}`);
|
|
182
209
|
}
|
|
183
210
|
else if (data.display) {
|
|
184
211
|
if (data.rawThinking) {
|
|
185
|
-
parts.push(`**${data.label}**\n${data.thinkSnippet}`);
|
|
212
|
+
parts.push(`**${data.label}**\n${stripCodeFences(data.thinkSnippet)}`);
|
|
186
213
|
}
|
|
187
|
-
parts.push(data.preview);
|
|
214
|
+
parts.push(ensureBalancedCodeFences(data.preview));
|
|
188
215
|
}
|
|
189
216
|
if (options?.includeFooter !== false) {
|
|
190
217
|
parts.push(formatPreviewFooter(input.agent, input.elapsedMs, input.meta ?? null));
|
|
@@ -213,21 +240,21 @@ export function buildFinalReplyRender(agent, result) {
|
|
|
213
240
|
let activityText = '';
|
|
214
241
|
let activityNoteText = '';
|
|
215
242
|
if (data.activityNarrative) {
|
|
216
|
-
activityText = `**Activity**\n${data.activityNarrative}\n\n`;
|
|
243
|
+
activityText = `**Activity**\n${stripCodeFences(data.activityNarrative)}\n\n`;
|
|
217
244
|
}
|
|
218
245
|
if (data.activityCommandSummary) {
|
|
219
246
|
activityNoteText = `*${data.activityCommandSummary}*\n\n`;
|
|
220
247
|
}
|
|
221
248
|
let thinkingText = '';
|
|
222
249
|
if (data.thinkingDisplay) {
|
|
223
|
-
thinkingText = `**${data.thinkLabel}**\n${data.thinkingDisplay}\n\n`;
|
|
250
|
+
thinkingText = `**${data.thinkLabel}**\n${stripCodeFences(data.thinkingDisplay)}\n\n`;
|
|
224
251
|
}
|
|
225
252
|
let statusText = '';
|
|
226
253
|
if (data.statusLines) {
|
|
227
254
|
statusText = `**⚠ Incomplete Response**\n${data.statusLines.join('\n')}\n\n`;
|
|
228
255
|
}
|
|
229
256
|
const headerText = `${activityText}${activityNoteText}${statusText}${thinkingText}`;
|
|
230
|
-
const bodyText = data.bodyMessage;
|
|
257
|
+
const bodyText = ensureBalancedCodeFences(data.bodyMessage);
|
|
231
258
|
return {
|
|
232
259
|
fullText: `${headerText}${bodyText}${footerText}`,
|
|
233
260
|
headerText,
|
|
@@ -583,6 +610,10 @@ function normalizeFeishuMarkdown(lines) {
|
|
|
583
610
|
pendingBlankLine = false;
|
|
584
611
|
out.push(line);
|
|
585
612
|
}
|
|
613
|
+
// Safety: close any unclosed code block so it doesn't swallow the rest of
|
|
614
|
+
// a Feishu card element (e.g. truncated body text ending mid-fence).
|
|
615
|
+
if (inCodeBlock)
|
|
616
|
+
out.push('```');
|
|
586
617
|
return out.join('\n');
|
|
587
618
|
}
|
|
588
619
|
export function adaptMarkdownForFeishu(markdown) {
|
package/dist/bot-feishu.js
CHANGED
|
@@ -201,7 +201,13 @@ export class FeishuBot extends Bot {
|
|
|
201
201
|
}
|
|
202
202
|
resolveIncomingSession(ctx, text, files) {
|
|
203
203
|
const cs = this.chat(ctx.chatId);
|
|
204
|
-
|
|
204
|
+
const replyMessageId = ctx.replyToMessageId || null;
|
|
205
|
+
const repliedSession = this.sessionFromMessage(ctx.chatId, replyMessageId);
|
|
206
|
+
if (repliedSession) {
|
|
207
|
+
this.log(`[resolveSession] reply matched session=${repliedSession.sessionId} chat=${ctx.chatId}`);
|
|
208
|
+
this.applySessionSelection(cs, repliedSession);
|
|
209
|
+
return repliedSession;
|
|
210
|
+
}
|
|
205
211
|
const selected = this.getSelectedSession(cs);
|
|
206
212
|
if (selected)
|
|
207
213
|
return selected;
|
|
@@ -257,9 +263,27 @@ export class FeishuBot extends Bot {
|
|
|
257
263
|
}
|
|
258
264
|
}
|
|
259
265
|
sessionsPageSize = 5;
|
|
260
|
-
buildStopKeyboard(actionId) {
|
|
266
|
+
buildStopKeyboard(actionId, opts) {
|
|
261
267
|
if (!actionId)
|
|
262
268
|
return undefined;
|
|
269
|
+
if (opts?.queued) {
|
|
270
|
+
return {
|
|
271
|
+
rows: [{
|
|
272
|
+
actions: [
|
|
273
|
+
{
|
|
274
|
+
tag: 'button',
|
|
275
|
+
text: { tag: 'plain_text', content: 'Recall' },
|
|
276
|
+
value: { action: `tsk:stop:${actionId}` },
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
tag: 'button',
|
|
280
|
+
text: { tag: 'plain_text', content: 'Steer' },
|
|
281
|
+
value: { action: `tsk:steer:${actionId}` },
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
}],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
263
287
|
return {
|
|
264
288
|
rows: [{
|
|
265
289
|
actions: [{
|
|
@@ -512,7 +536,7 @@ export class FeishuBot extends Bot {
|
|
|
512
536
|
workdir: this.workdir,
|
|
513
537
|
files: msg.files,
|
|
514
538
|
sessionId: session.sessionId,
|
|
515
|
-
title:
|
|
539
|
+
title: undefined,
|
|
516
540
|
});
|
|
517
541
|
session.workspacePath = staged.workspacePath;
|
|
518
542
|
this.syncSelectedChats(session);
|
|
@@ -550,12 +574,13 @@ export class FeishuBot extends Bot {
|
|
|
550
574
|
startedAt: start,
|
|
551
575
|
sourceMessageId: ctx.messageId,
|
|
552
576
|
});
|
|
553
|
-
const
|
|
577
|
+
const queuePosition = waiting ? this.getQueuePosition(session.key, taskId) : 0;
|
|
578
|
+
const placeholderKeyboard = this.buildStopKeyboard(this.actionIdForTask(taskId), { queued: waiting });
|
|
554
579
|
const model = session.modelId || this.modelForAgent(session.agent);
|
|
555
580
|
const effort = this.effortForAgent(session.agent);
|
|
556
|
-
const placeholderId = await this.channel.sendStreamingCard(ctx.chatId, buildInitialPreviewMarkdown(session.agent, model, effort, waiting), {
|
|
581
|
+
const placeholderId = await this.channel.sendStreamingCard(ctx.chatId, buildInitialPreviewMarkdown(session.agent, model, effort, waiting, queuePosition), {
|
|
557
582
|
replyTo: ctx.messageId || undefined,
|
|
558
|
-
keyboard:
|
|
583
|
+
keyboard: placeholderKeyboard,
|
|
559
584
|
});
|
|
560
585
|
if (placeholderId) {
|
|
561
586
|
this.registerSessionMessage(ctx.chatId, placeholderId, session);
|
|
@@ -576,6 +601,14 @@ export class FeishuBot extends Bot {
|
|
|
576
601
|
this.log(`[handleMessage] skipped cancelled queued task chat=${ctx.chatId} msg=${ctx.messageId}`);
|
|
577
602
|
return;
|
|
578
603
|
}
|
|
604
|
+
// Task is now running — update keyboard from Recall/Steer to Stop
|
|
605
|
+
const runningKeyboard = this.buildStopKeyboard(this.actionIdForTask(taskId));
|
|
606
|
+
if (placeholderId && waiting) {
|
|
607
|
+
try {
|
|
608
|
+
await this.channel.editMessage(ctx.chatId, placeholderId, buildInitialPreviewMarkdown(session.agent, model, effort, false), { keyboard: runningKeyboard });
|
|
609
|
+
}
|
|
610
|
+
catch { }
|
|
611
|
+
}
|
|
579
612
|
if (placeholderId) {
|
|
580
613
|
const renderer = this.channel.isStreamingCard(placeholderId)
|
|
581
614
|
? feishuStreamingPreviewRenderer
|
|
@@ -591,7 +624,7 @@ export class FeishuBot extends Bot {
|
|
|
591
624
|
canEditMessages: supportsChannelCapability(this.channel, 'editMessages'),
|
|
592
625
|
canSendTyping: false,
|
|
593
626
|
parseMode: 'Markdown',
|
|
594
|
-
keyboard:
|
|
627
|
+
keyboard: runningKeyboard,
|
|
595
628
|
log: (message) => this.log(message),
|
|
596
629
|
});
|
|
597
630
|
livePreview.start();
|
|
@@ -787,6 +820,7 @@ export class FeishuBot extends Bot {
|
|
|
787
820
|
messageId: ctx.messageId,
|
|
788
821
|
from: ctx.from,
|
|
789
822
|
chatType: 'p2p',
|
|
823
|
+
replyToMessageId: null,
|
|
790
824
|
reply: (text, opts) => ctx.channel.send(ctx.chatId, text, opts),
|
|
791
825
|
editReply: (msgId, text, opts) => ctx.channel.editMessage(ctx.chatId, msgId, text, opts),
|
|
792
826
|
channel: ctx.channel,
|
|
@@ -800,6 +834,8 @@ export class FeishuBot extends Bot {
|
|
|
800
834
|
return;
|
|
801
835
|
if (await this.handleTaskStopCallback(data, ctx))
|
|
802
836
|
return;
|
|
837
|
+
if (await this.handleTaskSteerCallback(data, ctx))
|
|
838
|
+
return;
|
|
803
839
|
if (await this.handleSwitchNavigateCallback(data, ctx))
|
|
804
840
|
return;
|
|
805
841
|
if (await this.handleSwitchSelectCallback(data, ctx))
|
|
@@ -877,6 +913,16 @@ export class FeishuBot extends Bot {
|
|
|
877
913
|
}
|
|
878
914
|
return true;
|
|
879
915
|
}
|
|
916
|
+
async handleTaskSteerCallback(data, ctx) {
|
|
917
|
+
if (!data.startsWith('tsk:steer:'))
|
|
918
|
+
return false;
|
|
919
|
+
const actionId = data.slice('tsk:steer:'.length).trim();
|
|
920
|
+
const result = this.steerTaskByActionId(actionId);
|
|
921
|
+
if (!result.task)
|
|
922
|
+
return true;
|
|
923
|
+
// The queued task will naturally run next after the running task is interrupted
|
|
924
|
+
return true;
|
|
925
|
+
}
|
|
880
926
|
async handleSwitchNavigateCallback(data, ctx) {
|
|
881
927
|
if (!data.startsWith('sw:n:'))
|
|
882
928
|
return false;
|
package/dist/bot-handler.js
CHANGED
|
@@ -258,10 +258,14 @@ function formatFinalFooterHtml(status, agent, elapsedMs, contextPercent) {
|
|
|
258
258
|
export function formatProviderUsageLines(usage) {
|
|
259
259
|
return buildProviderUsageLines(usage).map(line => line.bold ? `<b>${escapeHtml(line.text)}</b>` : escapeHtml(line.text));
|
|
260
260
|
}
|
|
261
|
-
export function buildInitialPreviewHtml(agent, waiting = false) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
261
|
+
export function buildInitialPreviewHtml(agent, waiting = false, queuePosition = 0) {
|
|
262
|
+
if (waiting) {
|
|
263
|
+
const queueLabel = queuePosition > 0
|
|
264
|
+
? `Queued · ${queuePosition} ${queuePosition === 1 ? 'task' : 'tasks'} ahead`
|
|
265
|
+
: 'Waiting in queue...';
|
|
266
|
+
return `<i>${escapeHtml(queueLabel)}</i>\n\n${formatPreviewFooterHtml(agent, 0)}`;
|
|
267
|
+
}
|
|
268
|
+
return formatPreviewFooterHtml(agent, 0);
|
|
265
269
|
}
|
|
266
270
|
export function buildStreamPreviewHtml(input) {
|
|
267
271
|
const data = extractStreamPreviewData(input);
|
package/dist/bot-telegram.js
CHANGED
|
@@ -178,6 +178,7 @@ export class TelegramBot extends Bot {
|
|
|
178
178
|
: null;
|
|
179
179
|
const repliedSession = this.sessionFromMessage(ctx.chatId, replyMessageId);
|
|
180
180
|
if (repliedSession) {
|
|
181
|
+
this.log(`[resolveSession] reply matched session=${repliedSession.sessionId} chat=${ctx.chatId}`);
|
|
181
182
|
this.applySessionSelection(cs, repliedSession);
|
|
182
183
|
return repliedSession;
|
|
183
184
|
}
|
|
@@ -262,9 +263,17 @@ export class TelegramBot extends Bot {
|
|
|
262
263
|
}
|
|
263
264
|
}
|
|
264
265
|
sessionsPageSize = 5;
|
|
265
|
-
buildStopKeyboard(actionId) {
|
|
266
|
+
buildStopKeyboard(actionId, opts) {
|
|
266
267
|
if (!actionId)
|
|
267
268
|
return undefined;
|
|
269
|
+
if (opts?.queued) {
|
|
270
|
+
return {
|
|
271
|
+
inline_keyboard: [[
|
|
272
|
+
{ text: 'Recall', callback_data: `tsk:stop:${actionId}` },
|
|
273
|
+
{ text: 'Steer', callback_data: `tsk:steer:${actionId}` },
|
|
274
|
+
]],
|
|
275
|
+
};
|
|
276
|
+
}
|
|
268
277
|
return {
|
|
269
278
|
inline_keyboard: [[
|
|
270
279
|
{ text: 'Stop', callback_data: `tsk:stop:${actionId}` },
|
|
@@ -462,7 +471,7 @@ export class TelegramBot extends Bot {
|
|
|
462
471
|
workdir: this.workdir,
|
|
463
472
|
files: msg.files,
|
|
464
473
|
sessionId: session.sessionId,
|
|
465
|
-
title:
|
|
474
|
+
title: undefined,
|
|
466
475
|
});
|
|
467
476
|
session.workspacePath = staged.workspacePath;
|
|
468
477
|
this.syncSelectedChats(session);
|
|
@@ -501,11 +510,12 @@ export class TelegramBot extends Bot {
|
|
|
501
510
|
startedAt: start,
|
|
502
511
|
sourceMessageId: ctx.messageId,
|
|
503
512
|
});
|
|
504
|
-
const stopKeyboard = this.buildStopKeyboard(this.actionIdForTask(taskId));
|
|
505
513
|
const waiting = this.sessionHasPendingWork(session);
|
|
514
|
+
const queuePosition = waiting ? this.getQueuePosition(session.key, taskId) : 0;
|
|
515
|
+
const placeholderKeyboard = this.buildStopKeyboard(this.actionIdForTask(taskId), { queued: waiting });
|
|
506
516
|
let phId = null;
|
|
507
517
|
if (canEditMessages) {
|
|
508
|
-
const placeholderId = await ctx.reply(buildInitialPreviewHtml(session.agent, waiting), { parseMode: 'HTML', messageThreadId, keyboard:
|
|
518
|
+
const placeholderId = await ctx.reply(buildInitialPreviewHtml(session.agent, waiting, queuePosition), { parseMode: 'HTML', messageThreadId, keyboard: placeholderKeyboard });
|
|
509
519
|
phId = typeof placeholderId === 'number' ? placeholderId : null;
|
|
510
520
|
if (phId != null) {
|
|
511
521
|
this.registerSessionMessage(ctx.chatId, phId, session);
|
|
@@ -534,6 +544,14 @@ export class TelegramBot extends Bot {
|
|
|
534
544
|
this.log(`[handleMessage] skipped cancelled queued task chat=${ctx.chatId} msg=${ctx.messageId}`);
|
|
535
545
|
return;
|
|
536
546
|
}
|
|
547
|
+
// Task is now running — update keyboard from Recall/Steer to Stop
|
|
548
|
+
const runningKeyboard = this.buildStopKeyboard(this.actionIdForTask(taskId));
|
|
549
|
+
if (phId != null && waiting) {
|
|
550
|
+
try {
|
|
551
|
+
await this.channel.editMessage(ctx.chatId, phId, buildInitialPreviewHtml(session.agent, false), { parseMode: 'HTML', keyboard: runningKeyboard });
|
|
552
|
+
}
|
|
553
|
+
catch { }
|
|
554
|
+
}
|
|
537
555
|
if (phId != null || canSendTyping) {
|
|
538
556
|
livePreview = new LivePreview({
|
|
539
557
|
agent: session.agent,
|
|
@@ -546,7 +564,7 @@ export class TelegramBot extends Bot {
|
|
|
546
564
|
canEditMessages,
|
|
547
565
|
canSendTyping,
|
|
548
566
|
messageThreadId,
|
|
549
|
-
keyboard:
|
|
567
|
+
keyboard: runningKeyboard,
|
|
550
568
|
log: (message) => this.log(message),
|
|
551
569
|
});
|
|
552
570
|
livePreview.start();
|
|
@@ -651,7 +669,8 @@ export class TelegramBot extends Bot {
|
|
|
651
669
|
finalMsgId = await replacePreview(rendered.fullHtml);
|
|
652
670
|
}
|
|
653
671
|
else {
|
|
654
|
-
|
|
672
|
+
// Split: header on first message, footer on last message
|
|
673
|
+
const maxFirst = 3900 - rendered.headerHtml.length;
|
|
655
674
|
let firstBody;
|
|
656
675
|
let remaining;
|
|
657
676
|
if (maxFirst > 200) {
|
|
@@ -665,14 +684,29 @@ export class TelegramBot extends Bot {
|
|
|
665
684
|
firstBody = '';
|
|
666
685
|
remaining = rendered.bodyHtml;
|
|
667
686
|
}
|
|
668
|
-
const firstHtml = `${rendered.headerHtml}${firstBody}${rendered.footerHtml}`;
|
|
669
|
-
finalMsgId = await replacePreview(firstHtml);
|
|
670
687
|
if (remaining.trim()) {
|
|
688
|
+
// Multi-message: header on first, footer on last
|
|
689
|
+
const firstHtml = `${rendered.headerHtml}${firstBody}`;
|
|
690
|
+
finalMsgId = await replacePreview(firstHtml);
|
|
671
691
|
const chunks = splitText(remaining, 3800);
|
|
672
|
-
for (
|
|
673
|
-
|
|
692
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
693
|
+
const isLast = i === chunks.length - 1;
|
|
694
|
+
const chunkText = isLast ? `${chunks[i]}${rendered.footerHtml}` : chunks[i];
|
|
695
|
+
remember(await sendFinalText(chunkText, finalMsgId ?? phId ?? ctx.messageId));
|
|
696
|
+
}
|
|
697
|
+
// Safety: re-clear the Stop keyboard on the placeholder in case the first edit silently failed
|
|
698
|
+
if (phId != null) {
|
|
699
|
+
try {
|
|
700
|
+
await this.channel.editMessage(ctx.chatId, phId, firstHtml || '(done)', { parseMode: 'HTML', keyboard: { inline_keyboard: [] } });
|
|
701
|
+
}
|
|
702
|
+
catch { }
|
|
674
703
|
}
|
|
675
704
|
}
|
|
705
|
+
else {
|
|
706
|
+
// Body fits on first message; only footer pushes it over — keep together
|
|
707
|
+
const firstHtml = `${rendered.headerHtml}${firstBody}${rendered.footerHtml}`;
|
|
708
|
+
finalMsgId = await replacePreview(firstHtml);
|
|
709
|
+
}
|
|
676
710
|
}
|
|
677
711
|
return { primaryMessageId: finalMsgId, messageIds };
|
|
678
712
|
}
|
|
@@ -742,6 +776,22 @@ export class TelegramBot extends Bot {
|
|
|
742
776
|
await ctx.answerCallback('Nothing to stop.');
|
|
743
777
|
return true;
|
|
744
778
|
}
|
|
779
|
+
async handleTaskSteerCallback(data, ctx) {
|
|
780
|
+
if (!data.startsWith('tsk:steer:'))
|
|
781
|
+
return false;
|
|
782
|
+
const actionId = data.slice('tsk:steer:'.length).trim();
|
|
783
|
+
const result = this.steerTaskByActionId(actionId);
|
|
784
|
+
if (!result.task) {
|
|
785
|
+
await ctx.answerCallback('This task already finished.');
|
|
786
|
+
return true;
|
|
787
|
+
}
|
|
788
|
+
if (result.task.status !== 'queued') {
|
|
789
|
+
await ctx.answerCallback('Task is already running.');
|
|
790
|
+
return true;
|
|
791
|
+
}
|
|
792
|
+
await ctx.answerCallback(result.interrupted ? 'Steering — interrupting current task...' : 'No running task to interrupt.');
|
|
793
|
+
return true;
|
|
794
|
+
}
|
|
745
795
|
async handleHumanLoopCallback(data, ctx) {
|
|
746
796
|
if (!data.startsWith('hl:'))
|
|
747
797
|
return false;
|
|
@@ -808,6 +858,8 @@ export class TelegramBot extends Bot {
|
|
|
808
858
|
return;
|
|
809
859
|
if (await this.handleTaskStopCallback(data, ctx))
|
|
810
860
|
return;
|
|
861
|
+
if (await this.handleTaskSteerCallback(data, ctx))
|
|
862
|
+
return;
|
|
811
863
|
if (await this.handleSwitchNavigateCallback(data, ctx))
|
|
812
864
|
return;
|
|
813
865
|
if (await this.handleSwitchSelectCallback(data, ctx))
|
package/dist/bot.js
CHANGED
|
@@ -617,7 +617,7 @@ export class Bot {
|
|
|
617
617
|
workdir: this.workdir,
|
|
618
618
|
files: [],
|
|
619
619
|
sessionId: null,
|
|
620
|
-
title: title ||
|
|
620
|
+
title: title || 'New session',
|
|
621
621
|
});
|
|
622
622
|
const runtime = this.upsertSessionRuntime({
|
|
623
623
|
agent: cs.agent,
|
|
@@ -767,6 +767,61 @@ export class Bot {
|
|
|
767
767
|
}
|
|
768
768
|
return { task, interrupted: false, cancelled: false };
|
|
769
769
|
}
|
|
770
|
+
/**
|
|
771
|
+
* Steer: interrupt the running task so a queued task (identified by actionId) runs next.
|
|
772
|
+
* Returns { task, interrupted } where task is the queued task and interrupted indicates
|
|
773
|
+
* whether a running task was aborted.
|
|
774
|
+
*/
|
|
775
|
+
steerTaskByActionId(actionId) {
|
|
776
|
+
const taskId = this.taskKeysByActionId.get(String(actionId));
|
|
777
|
+
if (!taskId)
|
|
778
|
+
return { task: null, interrupted: false };
|
|
779
|
+
const task = this.activeTasks.get(taskId) || null;
|
|
780
|
+
if (!task || task.status !== 'queued')
|
|
781
|
+
return { task, interrupted: false };
|
|
782
|
+
const interrupted = this.interruptRunningTask(task.sessionKey);
|
|
783
|
+
return { task, interrupted };
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Interrupt only the currently running task for a session, leaving queued tasks intact.
|
|
787
|
+
* Used by the "Steer" action to let a queued task run next.
|
|
788
|
+
*/
|
|
789
|
+
interruptRunningTask(sessionKey) {
|
|
790
|
+
const session = this.getSessionRuntimeByKey(sessionKey, { allowAnyWorkdir: true });
|
|
791
|
+
if (!session)
|
|
792
|
+
return false;
|
|
793
|
+
for (const taskId of session.runningTaskIds) {
|
|
794
|
+
const task = this.activeTasks.get(taskId);
|
|
795
|
+
if (!task || task.status !== 'running')
|
|
796
|
+
continue;
|
|
797
|
+
try {
|
|
798
|
+
task.abort?.();
|
|
799
|
+
}
|
|
800
|
+
catch { }
|
|
801
|
+
return true;
|
|
802
|
+
}
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Return the number of tasks ahead of the given task in its session queue.
|
|
807
|
+
* Counts running + queued (non-cancelled) tasks that were started before this one.
|
|
808
|
+
*/
|
|
809
|
+
getQueuePosition(sessionKey, taskId) {
|
|
810
|
+
const session = this.getSessionRuntimeByKey(sessionKey, { allowAnyWorkdir: true });
|
|
811
|
+
if (!session)
|
|
812
|
+
return 0;
|
|
813
|
+
let ahead = 0;
|
|
814
|
+
for (const otherId of session.runningTaskIds) {
|
|
815
|
+
if (otherId === taskId)
|
|
816
|
+
continue;
|
|
817
|
+
const other = this.activeTasks.get(otherId);
|
|
818
|
+
if (!other || other.cancelled)
|
|
819
|
+
continue;
|
|
820
|
+
if (other.status === 'running' || other.status === 'queued')
|
|
821
|
+
ahead++;
|
|
822
|
+
}
|
|
823
|
+
return ahead;
|
|
824
|
+
}
|
|
770
825
|
sourceMessageKey(chatId, sourceMessageId) {
|
|
771
826
|
return `${String(chatId)}:${String(sourceMessageId)}`;
|
|
772
827
|
}
|
package/dist/channel-feishu.js
CHANGED
|
@@ -423,7 +423,8 @@ class FeishuChannel extends Channel {
|
|
|
423
423
|
this._log(`[recv] skipped: not mentioned in group ${chatId}`);
|
|
424
424
|
return;
|
|
425
425
|
}
|
|
426
|
-
const
|
|
426
|
+
const parentId = typeof msg.parent_id === 'string' && msg.parent_id ? msg.parent_id : null;
|
|
427
|
+
const ctx = this._makeCtx(chatId, messageId, from, chatType, event, parentId);
|
|
427
428
|
// Parse message content
|
|
428
429
|
let text = '';
|
|
429
430
|
const files = [];
|
|
@@ -1031,12 +1032,13 @@ class FeishuChannel extends Channel {
|
|
|
1031
1032
|
// ========================================================================
|
|
1032
1033
|
// Internal helpers
|
|
1033
1034
|
// ========================================================================
|
|
1034
|
-
_makeCtx(chatId, messageId, from, chatType, raw) {
|
|
1035
|
+
_makeCtx(chatId, messageId, from, chatType, raw, replyToMessageId) {
|
|
1035
1036
|
return {
|
|
1036
1037
|
chatId,
|
|
1037
1038
|
messageId,
|
|
1038
1039
|
from,
|
|
1039
1040
|
chatType,
|
|
1041
|
+
replyToMessageId: replyToMessageId || null,
|
|
1040
1042
|
reply: (text, opts) => this.send(chatId, text, { ...opts, replyTo: messageId || opts?.replyTo }),
|
|
1041
1043
|
editReply: (msgId, text, opts) => this.editMessage(chatId, msgId, text, opts),
|
|
1042
1044
|
channel: this,
|
package/dist/cli.js
CHANGED
|
@@ -299,10 +299,15 @@ Docs: https://github.com/xiaotonng/pikiclaw
|
|
|
299
299
|
*/
|
|
300
300
|
function persistWorkdir(args, userConfig) {
|
|
301
301
|
if (!process.env.PIKICLAW_DAEMON_CHILD) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
302
|
+
// Only overwrite persisted workdir when the user explicitly passed -w.
|
|
303
|
+
// Falling back to cwd when no flag is given can clobber a valid saved
|
|
304
|
+
// workdir with a temp directory (e.g. after a non-daemon restart).
|
|
305
|
+
if (args.workdir) {
|
|
306
|
+
const cliWorkdir = path.resolve(args.workdir);
|
|
307
|
+
if (userConfig.workdir !== cliWorkdir) {
|
|
308
|
+
updateUserConfig({ workdir: cliWorkdir });
|
|
309
|
+
return loadUserConfig();
|
|
310
|
+
}
|
|
306
311
|
}
|
|
307
312
|
}
|
|
308
313
|
return userConfig;
|
package/dist/code-agent.js
CHANGED
|
@@ -748,8 +748,7 @@ export function stageSessionFiles(opts) {
|
|
|
748
748
|
const importedFiles = importFilesIntoWorkspace(session.workspacePath, opts.files);
|
|
749
749
|
if (importedFiles.length) {
|
|
750
750
|
session.record.stagedFiles = dedupeStrings([...session.record.stagedFiles, ...importedFiles]);
|
|
751
|
-
|
|
752
|
-
session.record.title = importedFiles[0];
|
|
751
|
+
/* title will be set when the first text prompt arrives */
|
|
753
752
|
saveSessionRecord(opts.workdir, session.record);
|
|
754
753
|
}
|
|
755
754
|
return { sessionId: session.sessionId, workspacePath: session.workspacePath, importedFiles };
|
|
@@ -890,7 +889,7 @@ function prepareStreamOpts(opts) {
|
|
|
890
889
|
const stagedFiles = [...session.record.stagedFiles];
|
|
891
890
|
session.record.stagedFiles = [];
|
|
892
891
|
if (!session.record.title)
|
|
893
|
-
session.record.title = summarizePromptTitle(opts.prompt) ||
|
|
892
|
+
session.record.title = summarizePromptTitle(opts.prompt) || null;
|
|
894
893
|
setSessionRunState(session.record, 'running', null);
|
|
895
894
|
saveSessionRecord(opts.workdir, session.record);
|
|
896
895
|
const attachmentPaths = attachmentRelPaths.map(relPath => path.join(session.workspacePath, relPath));
|
package/dist/driver-claude.js
CHANGED
|
@@ -223,9 +223,10 @@ function getNativeClaudeSessions(workdir) {
|
|
|
223
223
|
const filePath = path.join(projectDir, entry.name);
|
|
224
224
|
try {
|
|
225
225
|
const stat = fs.statSync(filePath);
|
|
226
|
-
// Read
|
|
226
|
+
// Read enough bytes to get past the system_prompt line (can be 20KB+) and
|
|
227
|
+
// reach the first user/assistant events for title and model extraction.
|
|
227
228
|
const fd = fs.openSync(filePath, 'r');
|
|
228
|
-
const buf = Buffer.alloc(
|
|
229
|
+
const buf = Buffer.alloc(65536);
|
|
229
230
|
const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
230
231
|
fs.closeSync(fd);
|
|
231
232
|
const head = buf.toString('utf8', 0, bytesRead);
|
package/dist/process-control.js
CHANGED
package/package.json
CHANGED