pikiclaw 0.3.69 → 0.3.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/dist/agent/drivers/claude-tui.js +40 -0
- package/dist/bot/bot.js +32 -4
- package/dist/bot/command-ui.js +9 -11
- package/dist/bot/commands.js +8 -2
- package/dist/bot/render-shared.js +0 -14
- package/dist/channels/feishu/render.js +0 -2
- package/dist/channels/telegram/render.js +0 -2
- package/package.json +1 -1
|
@@ -356,6 +356,35 @@ export function detectClaudeBypassPrompt(screen) {
|
|
|
356
356
|
&& t.includes('yes,iaccept')
|
|
357
357
|
&& t.includes('no,exit');
|
|
358
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* Capture-only classifier for the stall watchdog. When the turn goes quiet we
|
|
361
|
+
* cannot tell from timing alone whether the TUI is (a) frozen mid-turn (the
|
|
362
|
+
* known CLI bug — PTY dead), (b) just thinking for a long time (PTY repaints a
|
|
363
|
+
* spinner), or (c) blocked on an interactive prompt that bypass mode does NOT
|
|
364
|
+
* suppress and that's waiting for input it will never get (trust-a-new-folder,
|
|
365
|
+
* a "Do you want to proceed?" confirmation, an expired-login prompt, …). The
|
|
366
|
+
* raw PTY screen is the only thing that disambiguates them, and we don't
|
|
367
|
+
* otherwise persist it — so on a stall we record a compact stripped sample plus
|
|
368
|
+
* a conservative "looks like an interactive prompt" flag. Changes no control
|
|
369
|
+
* flow; it exists purely to make the next stall diagnosable from data.
|
|
370
|
+
*/
|
|
371
|
+
export function classifyStallScreen(screen) {
|
|
372
|
+
if (typeof screen !== 'string' || !screen)
|
|
373
|
+
return { looksLikePrompt: false, sample: '' };
|
|
374
|
+
const stripped = stripAnsiEscapes(screen);
|
|
375
|
+
const sample = stripped.replace(/\s+/g, ' ').trim().slice(-400);
|
|
376
|
+
// Claude positions words with cursor moves, so the live screen is spaceless;
|
|
377
|
+
// match against the despaced form (see detectClaudeBypassPrompt).
|
|
378
|
+
const ds = stripped.replace(/\s+/g, '').toLowerCase();
|
|
379
|
+
const looksLikePrompt = ds.includes('esctocancel') // claude's confirm-dialog footer ("Enter to confirm · Esc to cancel")
|
|
380
|
+
|| ds.includes('doyouwant')
|
|
381
|
+
|| ds.includes('wouldyoulike')
|
|
382
|
+
|| ds.includes('trustthisfolder')
|
|
383
|
+
|| ds.includes('yes,iaccept')
|
|
384
|
+
|| ds.includes('(y/n)')
|
|
385
|
+
|| (ds.includes('❯') && ds.includes('1.') && ds.includes('2.')); // numbered select with cursor
|
|
386
|
+
return { looksLikePrompt, sample };
|
|
387
|
+
}
|
|
359
388
|
/**
|
|
360
389
|
* Extract text / thinking blocks from an assistant JSONL event and route them:
|
|
361
390
|
* text → the chunked stream buffer (slow drain), thinking → `s.thinking`
|
|
@@ -1601,6 +1630,9 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1601
1630
|
stallDiagPtyAliveWhileQuiet = true;
|
|
1602
1631
|
if (nowMs - lastStallDiagHeartbeatAt >= STALL_DIAG_HEARTBEAT_INTERVAL_MS) {
|
|
1603
1632
|
lastStallDiagHeartbeatAt = nowMs;
|
|
1633
|
+
// Snapshot the screen so a quiet stretch can later be classified as
|
|
1634
|
+
// a frozen stream vs a long think vs a blocking interactive prompt.
|
|
1635
|
+
const screenInfo = classifyStallScreen(screenTail);
|
|
1604
1636
|
writeStallDiag({
|
|
1605
1637
|
kind: 'quiet',
|
|
1606
1638
|
sessionId: activeSessionId,
|
|
@@ -1616,6 +1648,8 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1616
1648
|
pendingHookTools: pendingHookToolIds.size,
|
|
1617
1649
|
pendingBgAgents: pendingBgForStall,
|
|
1618
1650
|
pendingBgBash: pendingClaudeBackgroundBashCount(s),
|
|
1651
|
+
looksLikePrompt: screenInfo.looksLikePrompt,
|
|
1652
|
+
screenSample: screenInfo.sample,
|
|
1619
1653
|
});
|
|
1620
1654
|
}
|
|
1621
1655
|
}
|
|
@@ -1631,6 +1665,7 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1631
1665
|
const quietMin = Math.round((Date.now() - lastProgressAt) / 60_000);
|
|
1632
1666
|
const ptyQuietS = Math.round((Date.now() - lastPtyDataAt) / 1000);
|
|
1633
1667
|
s.stopReason = 'stalled';
|
|
1668
|
+
const stallScreen = classifyStallScreen(screenTail);
|
|
1634
1669
|
writeStallDiag({
|
|
1635
1670
|
kind: 'stall',
|
|
1636
1671
|
sessionId: activeSessionId,
|
|
@@ -1645,6 +1680,11 @@ export async function doClaudeTuiStream(opts) {
|
|
|
1645
1680
|
lastJsonlType: lastMainJsonlType,
|
|
1646
1681
|
pendingHookTools: pendingHookToolIds.size,
|
|
1647
1682
|
pendingBgAgents: pendingBgForStall,
|
|
1683
|
+
// looksLikePrompt=true here is the signal that the "stall" was really
|
|
1684
|
+
// a blocking interactive prompt waiting for input bypass can't skip —
|
|
1685
|
+
// the mid-turn dialog-hang hypothesis, confirmable from screenSample.
|
|
1686
|
+
looksLikePrompt: stallScreen.looksLikePrompt,
|
|
1687
|
+
screenSample: stallScreen.sample,
|
|
1648
1688
|
});
|
|
1649
1689
|
if (!s.errors) {
|
|
1650
1690
|
s.errors = [`Claude process went silent mid-turn for ${quietMin}m (no JSONL, hook, or sub-agent events; PTY quiet ${ptyQuietS}s) — known claude CLI freeze. Terminated for auto-resume.`];
|
package/dist/bot/bot.js
CHANGED
|
@@ -1886,12 +1886,40 @@ export class Bot {
|
|
|
1886
1886
|
}
|
|
1887
1887
|
switchEffortForChat(chatId, effort) {
|
|
1888
1888
|
const cs = this.chat(chatId);
|
|
1889
|
-
|
|
1889
|
+
// "ultra" is a synthetic top rung in the effort picker, NOT a real --effort
|
|
1890
|
+
// value (the claude CLI rejects anything outside low|medium|high|xhigh|max).
|
|
1891
|
+
// It bundles "max reasoning depth + permit multi-agent Workflow
|
|
1892
|
+
// orchestration" — the same pairing as Claude's own `ultracode` mode. Decode
|
|
1893
|
+
// it here, the single apply choke point, so the rest of the pipeline only
|
|
1894
|
+
// ever sees a concrete effort value plus the orthogonal workflow flag.
|
|
1895
|
+
// Because the rungs are mutually exclusive, picking any concrete level also
|
|
1896
|
+
// clears the workflow opt-in (capability-gated — only claude advertises it).
|
|
1897
|
+
const ultra = effort === 'ultra';
|
|
1898
|
+
const realEffort = ultra ? 'max' : effort;
|
|
1899
|
+
this.setEffortForAgent(cs.agent, realEffort);
|
|
1890
1900
|
const session = this.getSelectedSession(cs);
|
|
1891
1901
|
if (session)
|
|
1892
|
-
session.thinkingEffort =
|
|
1893
|
-
this.persistAgentPreference(cs.agent, 'effort',
|
|
1894
|
-
|
|
1902
|
+
session.thinkingEffort = realEffort;
|
|
1903
|
+
this.persistAgentPreference(cs.agent, 'effort', realEffort);
|
|
1904
|
+
if (getDriverCapabilities(cs.agent).workflow) {
|
|
1905
|
+
this.setWorkflowEnabledForAgent(cs.agent, ultra);
|
|
1906
|
+
this.persistAgentPreference(cs.agent, 'workflow', ultra ? '1' : '0');
|
|
1907
|
+
}
|
|
1908
|
+
this.log(`effort switched to ${effort} (effort=${realEffort}, workflow=${ultra}) for ${cs.agent} chat=${chatId}`);
|
|
1909
|
+
}
|
|
1910
|
+
/**
|
|
1911
|
+
* Effort value to *display* in the picker. Workflow is orthogonal under the
|
|
1912
|
+
* hood, but the UI folds "max depth + workflow on" into the single synthetic
|
|
1913
|
+
* `ultra` rung (see {@link switchEffortForChat}), so report it as current when
|
|
1914
|
+
* the agent has orchestration enabled. Mirrors the decomposition above.
|
|
1915
|
+
*/
|
|
1916
|
+
effortSelectionForAgent(agent) {
|
|
1917
|
+
const effort = this.effortForAgent(agent);
|
|
1918
|
+
if (!effort)
|
|
1919
|
+
return null;
|
|
1920
|
+
if (getDriverCapabilities(agent).workflow && this.workflowEnabledForAgent(agent))
|
|
1921
|
+
return 'ultra';
|
|
1922
|
+
return effort;
|
|
1895
1923
|
}
|
|
1896
1924
|
switchPermissionModeForChat(chatId, mode) {
|
|
1897
1925
|
const cs = this.chat(chatId);
|
package/dist/bot/command-ui.js
CHANGED
|
@@ -387,22 +387,20 @@ export function buildModeCommandView(bot, chatId) {
|
|
|
387
387
|
],
|
|
388
388
|
];
|
|
389
389
|
const metaLines = [];
|
|
390
|
-
if (!isClaude
|
|
390
|
+
if (!isClaude)
|
|
391
391
|
metaLines.push('Permission mode is only available for Claude.');
|
|
392
|
+
// Workflow orchestration is no longer a standalone toggle here — it folded
|
|
393
|
+
// into the effort picker as the top "Ultra" rung (max depth + multi-agent
|
|
394
|
+
// fan-out). Surface its state and point users to /models so the capability
|
|
395
|
+
// stays discoverable.
|
|
392
396
|
if (supportsWorkflow) {
|
|
393
|
-
metaLines.push(`Workflow orchestration: ${workflowOn ? 'On' : 'Off'} —
|
|
394
|
-
rows.push([
|
|
395
|
-
{ label: 'Workflow On', action: { kind: 'workflow.toggle', enabled: true },
|
|
396
|
-
state: workflowOn ? 'current' : 'default', primary: workflowOn },
|
|
397
|
-
{ label: 'Workflow Off', action: { kind: 'workflow.toggle', enabled: false },
|
|
398
|
-
state: workflowOn ? 'default' : 'current', primary: !workflowOn },
|
|
399
|
-
]);
|
|
397
|
+
metaLines.push(`Workflow orchestration: ${workflowOn ? 'On (Ultra effort)' : 'Off'} — pick the Ultra rung in /models to toggle.`);
|
|
400
398
|
}
|
|
401
399
|
return {
|
|
402
400
|
kind: 'mode',
|
|
403
401
|
title: 'Agent Mode',
|
|
404
402
|
detail: `Current: ${isPlanMode ? 'Plan (read-only)' : 'Code (full access)'}`
|
|
405
|
-
+ (supportsWorkflow
|
|
403
|
+
+ (supportsWorkflow && workflowOn ? ' · Ultra (workflow)' : ''),
|
|
406
404
|
metaLines,
|
|
407
405
|
items: [],
|
|
408
406
|
rows,
|
|
@@ -515,7 +513,7 @@ export async function executeCommandAction(bot, chatId, action, opts = {}) {
|
|
|
515
513
|
}
|
|
516
514
|
case 'effort.set': {
|
|
517
515
|
const chat = bot.chat(chatId);
|
|
518
|
-
const currentEffort = bot.
|
|
516
|
+
const currentEffort = bot.effortSelectionForAgent(chat.agent);
|
|
519
517
|
if (action.effort === currentEffort) {
|
|
520
518
|
return { kind: 'noop', message: `Already using ${action.effort} effort` };
|
|
521
519
|
}
|
|
@@ -552,7 +550,7 @@ export async function executeCommandAction(bot, chatId, action, opts = {}) {
|
|
|
552
550
|
if (!draft)
|
|
553
551
|
return { kind: 'noop', message: 'No changes' };
|
|
554
552
|
const currentModel = bot.modelForAgent(chat.agent);
|
|
555
|
-
const currentEffort = bot.
|
|
553
|
+
const currentEffort = bot.effortSelectionForAgent(chat.agent);
|
|
556
554
|
const currentProfileId = bot.activeProfileIdForAgent(chat.agent);
|
|
557
555
|
const profileChanged = (draft.profileId || null) !== (currentProfileId || null);
|
|
558
556
|
const modelChanged = profileChanged
|
package/dist/bot/commands.js
CHANGED
|
@@ -30,7 +30,7 @@ export function getStartData(bot, chatId) {
|
|
|
30
30
|
.map(a => ({
|
|
31
31
|
agent: a.agent,
|
|
32
32
|
model: bot.modelForAgent(a.agent) || '(default)',
|
|
33
|
-
effort: bot.
|
|
33
|
+
effort: bot.effortSelectionForAgent(a.agent),
|
|
34
34
|
}));
|
|
35
35
|
return {
|
|
36
36
|
...intro,
|
|
@@ -373,6 +373,10 @@ const EFFORT_LEVELS = {
|
|
|
373
373
|
{ id: 'high', label: 'High' },
|
|
374
374
|
{ id: 'xhigh', label: 'Very High' },
|
|
375
375
|
{ id: 'max', label: 'Max' },
|
|
376
|
+
// Synthetic top rung: "max depth + multi-agent Workflow orchestration", the
|
|
377
|
+
// same bundle as Claude's `ultracode` mode. Not a real --effort value — the
|
|
378
|
+
// bot decomposes it into (effort=max, workflow=on) at apply time.
|
|
379
|
+
{ id: 'ultra', label: 'Ultra' },
|
|
376
380
|
],
|
|
377
381
|
codex: [
|
|
378
382
|
{ id: 'low', label: 'Low' },
|
|
@@ -389,7 +393,9 @@ const EFFORT_LEVELS = {
|
|
|
389
393
|
],
|
|
390
394
|
};
|
|
391
395
|
function buildEffortData(bot, agent) {
|
|
392
|
-
|
|
396
|
+
// Display value folds workflow into the synthetic `ultra` rung — see
|
|
397
|
+
// Bot.effortSelectionForAgent.
|
|
398
|
+
const currentEffort = bot.effortSelectionForAgent(agent);
|
|
393
399
|
if (!currentEffort)
|
|
394
400
|
return null;
|
|
395
401
|
const levels = EFFORT_LEVELS[agent];
|
|
@@ -276,12 +276,6 @@ function renderSubAgentsForPreview(meta) {
|
|
|
276
276
|
}
|
|
277
277
|
return lines.join('\n');
|
|
278
278
|
}
|
|
279
|
-
/** After this much wall-clock, a still-running turn shows a text-only "still
|
|
280
|
-
* working" banner (see StreamPreviewData.longRunHint) so a long silent
|
|
281
|
-
* operation (held background task, slow command) doesn't read as a frozen
|
|
282
|
-
* card. Deliberately above the chunked-stream cadence so quick turns never
|
|
283
|
-
* flash it. */
|
|
284
|
-
const LONG_RUN_HINT_AFTER_MS = 60_000;
|
|
285
279
|
export function extractStreamPreviewData(input) {
|
|
286
280
|
const maxBody = 2400;
|
|
287
281
|
const display = input.bodyText.trim();
|
|
@@ -299,13 +293,6 @@ export function extractStreamPreviewData(input) {
|
|
|
299
293
|
// freshly-opened card doesn't flash "0s".
|
|
300
294
|
const elapsedMs = Math.max(0, input.elapsedMs);
|
|
301
295
|
const thinkingProgressText = elapsedMs >= 1000 ? fmtCompactUptime(elapsedMs) : null;
|
|
302
|
-
// After a turn has run a while, a long silent operation (a held background
|
|
303
|
-
// task, a slow command) can make the card look frozen. Surface a text-only
|
|
304
|
-
// "still working" line so the user knows it's alive and can switch away. No
|
|
305
|
-
// elapsed time here — the footer keeps the single clock, so no second timer.
|
|
306
|
-
const longRunHint = elapsedMs >= LONG_RUN_HINT_AFTER_MS
|
|
307
|
-
? '⏳ Still working — the result will update in this card'
|
|
308
|
-
: null;
|
|
309
296
|
return {
|
|
310
297
|
display,
|
|
311
298
|
rawThinking,
|
|
@@ -318,6 +305,5 @@ export function extractStreamPreviewData(input) {
|
|
|
318
305
|
thinkSnippet,
|
|
319
306
|
preview,
|
|
320
307
|
thinkingProgressText,
|
|
321
|
-
longRunHint,
|
|
322
308
|
};
|
|
323
309
|
}
|
|
@@ -246,8 +246,6 @@ function buildPreviewMarkdown(input, options) {
|
|
|
246
246
|
// heartbeat, so the card still visibly advances.
|
|
247
247
|
parts.push(`**${data.label}**`);
|
|
248
248
|
}
|
|
249
|
-
if (data.longRunHint)
|
|
250
|
-
parts.push(data.longRunHint);
|
|
251
249
|
if (options?.includeFooter !== false) {
|
|
252
250
|
parts.push(formatPreviewFooter(input.agent, input.elapsedMs, input.meta ?? null, {
|
|
253
251
|
model: input.model,
|
|
@@ -345,8 +345,6 @@ export function buildStreamPreviewHtml(input) {
|
|
|
345
345
|
// heartbeat, so the card still visibly advances.
|
|
346
346
|
parts.push(`<blockquote><b>${escapeHtml(data.label)}</b></blockquote>`);
|
|
347
347
|
}
|
|
348
|
-
if (data.longRunHint)
|
|
349
|
-
parts.push(`<i>${escapeHtml(data.longRunHint)}</i>`);
|
|
350
348
|
parts.push(formatPreviewFooterHtml(input.agent, input.elapsedMs, input.meta ?? null, {
|
|
351
349
|
model: input.model,
|
|
352
350
|
effort: input.effort,
|
package/package.json
CHANGED