pikiloom 0.4.59 → 0.4.61
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/dashboard/dist/assets/{AgentTab-0iSS-Bby.js → AgentTab-KSanVg_l.js} +1 -1
- package/dashboard/dist/assets/{ConnectionModal-DRg8X2s1.js → ConnectionModal-BSoA2mZL.js} +1 -1
- package/dashboard/dist/assets/{DirBrowser-Br3AnAYT.js → DirBrowser-zKX7wmty.js} +1 -1
- package/dashboard/dist/assets/{ExtensionsTab-CHO2uMeN.js → ExtensionsTab-DKLjCr4u.js} +1 -1
- package/dashboard/dist/assets/{IMAccessTab-7v8e2UqN.js → IMAccessTab-BhvB7Hes.js} +1 -1
- package/dashboard/dist/assets/{Modal-bvNc9zVs.js → Modal-Qwlap_Fl.js} +1 -1
- package/dashboard/dist/assets/{Modals-CXBXc4Xx.js → Modals-2Zw2FSLo.js} +1 -1
- package/dashboard/dist/assets/{Select-D-rJwrhF.js → Select-dIxoi_TN.js} +1 -1
- package/dashboard/dist/assets/SessionPanel--D5NGYgd.js +1 -0
- package/dashboard/dist/assets/{SystemTab-NTIGkssg.js → SystemTab-CtjiZa5s.js} +1 -1
- package/dashboard/dist/assets/{index-h42cuqnj.js → index-BDSh0L9U.js} +3 -3
- package/dashboard/dist/assets/{index-CPmFmj6P.js → index-DN0a6ZnO.js} +16 -16
- package/dashboard/dist/assets/{shared-CUTx2Dth.js → shared-D2sGZJ9f.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dist/agent/drivers/claude.js +28 -1
- package/dist/agent/turn-snapshot.js +141 -0
- package/dist/bot/bot.js +11 -0
- package/package.json +1 -1
- package/packages/kernel/dist/drivers/claude.d.ts +3 -0
- package/packages/kernel/dist/drivers/claude.js +59 -0
- package/packages/kernel/dist/index.d.ts +1 -1
- package/packages/kernel/dist/index.js +1 -1
- package/dashboard/dist/assets/SessionPanel-CoIrjkQt.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{j as s}from"./react-vendor-C7Sl8SE7.js";import{O as t,m as i,S as a}from"./index-
|
|
1
|
+
import{j as s}from"./react-vendor-C7Sl8SE7.js";import{O as t,m as i,S as a}from"./index-BDSh0L9U.js";function o({primary:e,secondary:l,tertiary:n}){return s.jsxs("div",{className:"flex flex-col gap-2 pt-1 sm:flex-row sm:flex-wrap sm:items-center sm:justify-between",children:[s.jsxs("div",{className:"flex flex-wrap items-center gap-2",children:[e&&s.jsxs(i,{tone:"primary",onClick:e.onClick,disabled:e.disabled,children:[e.loading&&s.jsx(a,{}),e.label]}),l&&s.jsx(i,{tone:"secondary",onClick:l.onClick,disabled:l.disabled,children:l.label})]}),n&&s.jsx("div",{className:"text-xs leading-relaxed text-fg-4",children:n})]})}function c({children:e,className:l}){return s.jsx(t,{padding:"md",elevation:"flat",className:l,children:e})}export{o as A,c as S};
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<link rel="icon" type="image/png" href="/logo.png">
|
|
7
7
|
<title>Pikiloom</title>
|
|
8
8
|
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-BDSh0L9U.js"></script>
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/react-vendor-C7Sl8SE7.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/router-DHISdpPk.js">
|
|
12
12
|
<link rel="stylesheet" crossorigin href="/assets/index-BO-FYAny.css">
|
|
@@ -7,6 +7,7 @@ import { Q, run, agentError, agentLog, agentWarn, buildStreamPreviewMeta, comput
|
|
|
7
7
|
import { AGENT_STREAM_HARD_KILL_GRACE_MS, AGENT_GRACEFUL_ABORT_GRACE_MS, SESSION_RUNNING_THRESHOLD_MS } from '../../core/constants.js';
|
|
8
8
|
import { terminateProcessTree } from '../../core/process-control.js';
|
|
9
9
|
import { getHome, IS_MAC, encodePathAsDirName } from '../../core/platform.js';
|
|
10
|
+
import { loadDeliveredTurns, buildRecoveryQueue, normalizeSnapshotPrompt } from '../turn-snapshot.js';
|
|
10
11
|
function buildClaudeUserMessage(prompt, attachments) {
|
|
11
12
|
const content = [];
|
|
12
13
|
for (const filePath of attachments) {
|
|
@@ -1516,6 +1517,9 @@ function isClaudeSyntheticResumeNoise(text) {
|
|
|
1516
1517
|
return t === 'no response requested.' || t === 'no response requested';
|
|
1517
1518
|
}
|
|
1518
1519
|
const CLAUDE_INCOMPLETE_TURN_NOTICE = '⚠️ This reply ended before a closing message was delivered (interrupted, or the model returned an empty final response).';
|
|
1520
|
+
// Shown under a reply we restored from pikiloom's delivery snapshot because the agent transcript
|
|
1521
|
+
// lost it (see turn-snapshot.ts). The content above is what was actually streamed to the user.
|
|
1522
|
+
const CLAUDE_RECOVERED_TURN_NOTICE = 'ℹ️ Restored by pikiloom from the delivered reply — the agent’s own transcript dropped this turn.';
|
|
1519
1523
|
function getClaudeSessionMessages(opts) {
|
|
1520
1524
|
const projectDir = path.join(getHome(), '.claude', 'projects', claudeProjectDirName(opts.workdir));
|
|
1521
1525
|
const filePath = path.join(projectDir, `${opts.sessionId}.jsonl`);
|
|
@@ -1527,6 +1531,13 @@ function getClaudeSessionMessages(opts) {
|
|
|
1527
1531
|
const lines = content.split('\n').filter(l => l.trim());
|
|
1528
1532
|
const allMsgs = [];
|
|
1529
1533
|
const richMsgs = [];
|
|
1534
|
+
// Delivered-reply snapshots keyed by prompt, consumed at tombstones to restore swallowed turns.
|
|
1535
|
+
const recoveryQueue = buildRecoveryQueue(loadDeliveredTurns(opts.sessionId));
|
|
1536
|
+
let lastUserPromptNorm = '';
|
|
1537
|
+
const takeRecoveredTurn = (promptNorm) => {
|
|
1538
|
+
const list = promptNorm ? recoveryQueue.get(promptNorm) : undefined;
|
|
1539
|
+
return list && list.length ? list.shift() : null;
|
|
1540
|
+
};
|
|
1530
1541
|
let pendingRole = null;
|
|
1531
1542
|
let pendingTextParts = [];
|
|
1532
1543
|
let pendingBlocks = [];
|
|
@@ -1699,6 +1710,8 @@ function getClaudeSessionMessages(opts) {
|
|
|
1699
1710
|
pendingRole = 'user';
|
|
1700
1711
|
pendingTextParts = text ? [text] : [];
|
|
1701
1712
|
pendingBlocks = text ? [{ type: 'text', content: text }, ...imageBlocks] : [...imageBlocks];
|
|
1713
|
+
if (text)
|
|
1714
|
+
lastUserPromptNorm = normalizeSnapshotPrompt(text);
|
|
1702
1715
|
}
|
|
1703
1716
|
}
|
|
1704
1717
|
else if (ev.type === 'assistant') {
|
|
@@ -1715,7 +1728,21 @@ function getClaudeSessionMessages(opts) {
|
|
|
1715
1728
|
if (pendingRole === 'user')
|
|
1716
1729
|
flush();
|
|
1717
1730
|
pendingRole = 'assistant';
|
|
1718
|
-
|
|
1731
|
+
// If this tombstone is the whole assistant turn (nothing landed in the jsonl) and we
|
|
1732
|
+
// hold a delivered-reply snapshot for the preceding prompt, restore the real reply
|
|
1733
|
+
// instead of the "ended before a closing message" notice. When partial content did
|
|
1734
|
+
// land (pending blocks already present) we leave it — partial + notice is truthful.
|
|
1735
|
+
const turnEmpty = pendingBlocks.length === 0 && pendingTextParts.length === 0;
|
|
1736
|
+
const recovered = (displayText === CLAUDE_INCOMPLETE_TURN_NOTICE && turnEmpty)
|
|
1737
|
+
? takeRecoveredTurn(lastUserPromptNorm) : null;
|
|
1738
|
+
if (recovered) {
|
|
1739
|
+
pendingTextParts.push(recovered);
|
|
1740
|
+
pendingBlocks.push({ type: 'text', content: recovered });
|
|
1741
|
+
pendingBlocks.push({ type: 'system_notice', content: CLAUDE_RECOVERED_TURN_NOTICE });
|
|
1742
|
+
}
|
|
1743
|
+
else {
|
|
1744
|
+
pendingBlocks.push({ type: 'system_notice', content: displayText });
|
|
1745
|
+
}
|
|
1719
1746
|
continue;
|
|
1720
1747
|
}
|
|
1721
1748
|
if (pendingRole === 'user')
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getUserConfigPath } from '../core/config/user-config.js';
|
|
4
|
+
// Durable per-turn snapshot of the content pikiloom actually delivered.
|
|
5
|
+
//
|
|
6
|
+
// The agent CLIs are the source of truth for transcript history, but their session jsonl is
|
|
7
|
+
// lossy under two upstream conditions we cannot prevent: (1) a turn's closing reply dies before
|
|
8
|
+
// the CLI flushes it to disk (kill/flush race), and (2) the CLI's resume rewrites the jsonl in
|
|
9
|
+
// place and drops a turn that never got a synthetic-free conclusion. In both cases pikiloom has
|
|
10
|
+
// already streamed the complete reply to the user (turn-audit confirms ok/end_turn), yet a later
|
|
11
|
+
// history render reads only a "No response requested." tombstone — the swallow.
|
|
12
|
+
//
|
|
13
|
+
// This sidecar is the missing durable copy: one JSON line per delivered turn, keyed by the user
|
|
14
|
+
// prompt, so the claude history renderer can restore the real reply at a tombstone instead of
|
|
15
|
+
// showing a bare "ended before a closing message" notice. It complements turn-audit.jsonl (which
|
|
16
|
+
// records how a turn ended, not what it said). See [[posttool-empty-result-no-response-requested]].
|
|
17
|
+
const MAX_SNAPSHOT_BYTES = 512 * 1024; // per-session cap, single-slot rotation
|
|
18
|
+
const MAX_TEXT_CHARS = 40_000; // don't archive megabyte replies verbatim
|
|
19
|
+
const STALE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
20
|
+
// Match the raw prompt pikiloom ran against the user text reconstructed from the jsonl. Both
|
|
21
|
+
// sides normalize identically: collapse whitespace, trim, cap length. Case is preserved (matching
|
|
22
|
+
// is mostly CJK / mixed-script prose).
|
|
23
|
+
export function normalizeSnapshotPrompt(s) {
|
|
24
|
+
return (s || '').replace(/\s+/g, ' ').trim().slice(0, 240);
|
|
25
|
+
}
|
|
26
|
+
function snapshotDir() {
|
|
27
|
+
return path.join(path.dirname(getUserConfigPath()), 'turn-snapshots');
|
|
28
|
+
}
|
|
29
|
+
function sessionSnapshotPath(sessionId) {
|
|
30
|
+
// sessionId is a uuid / codex rollout id — safe as a filename, but strip separators defensively.
|
|
31
|
+
const safe = sessionId.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
32
|
+
return path.join(snapshotDir(), `${safe}.jsonl`);
|
|
33
|
+
}
|
|
34
|
+
// A finished turn whose delivered message is only a placeholder or an appended incomplete-notice
|
|
35
|
+
// carries nothing worth restoring. Strip a trailing "⚠️ …" note (composeKernelFinalPresentation
|
|
36
|
+
// and the legacy driver append one to real prose) and drop the empty/placeholder shapes.
|
|
37
|
+
function bodyWorthSaving(message) {
|
|
38
|
+
let text = (message || '').replace(/\n\n⚠️[^\n]*$/u, '').trim();
|
|
39
|
+
if (!text)
|
|
40
|
+
return null;
|
|
41
|
+
if (text === '(no textual response)' || text === '(no output)')
|
|
42
|
+
return null;
|
|
43
|
+
if (text.length > MAX_TEXT_CHARS)
|
|
44
|
+
text = text.slice(0, MAX_TEXT_CHARS);
|
|
45
|
+
return text;
|
|
46
|
+
}
|
|
47
|
+
let cleanedThisProcess = false;
|
|
48
|
+
function cleanupStaleSnapshots() {
|
|
49
|
+
if (cleanedThisProcess)
|
|
50
|
+
return;
|
|
51
|
+
cleanedThisProcess = true;
|
|
52
|
+
try {
|
|
53
|
+
const dir = snapshotDir();
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
for (const name of fs.readdirSync(dir)) {
|
|
56
|
+
const p = path.join(dir, name);
|
|
57
|
+
try {
|
|
58
|
+
if (now - fs.statSync(p).mtimeMs > STALE_MS)
|
|
59
|
+
fs.unlinkSync(p);
|
|
60
|
+
}
|
|
61
|
+
catch { /* skip */ }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch { /* no dir yet */ }
|
|
65
|
+
}
|
|
66
|
+
export function recordDeliveredTurn(entry) {
|
|
67
|
+
try {
|
|
68
|
+
if (!entry.sessionId)
|
|
69
|
+
return;
|
|
70
|
+
const text = bodyWorthSaving(entry.message);
|
|
71
|
+
if (!text)
|
|
72
|
+
return;
|
|
73
|
+
const prompt = (entry.prompt || '').slice(0, 500);
|
|
74
|
+
const dir = snapshotDir();
|
|
75
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
76
|
+
cleanupStaleSnapshots();
|
|
77
|
+
const file = sessionSnapshotPath(entry.sessionId);
|
|
78
|
+
try {
|
|
79
|
+
if (fs.statSync(file).size > MAX_SNAPSHOT_BYTES)
|
|
80
|
+
fs.renameSync(file, `${file}.1`);
|
|
81
|
+
}
|
|
82
|
+
catch { /* no file yet */ }
|
|
83
|
+
const line = JSON.stringify({
|
|
84
|
+
ts: new Date().toISOString(),
|
|
85
|
+
prompt,
|
|
86
|
+
promptNorm: normalizeSnapshotPrompt(prompt),
|
|
87
|
+
text,
|
|
88
|
+
model: entry.model ?? null,
|
|
89
|
+
ok: entry.ok,
|
|
90
|
+
stopReason: entry.stopReason ?? null,
|
|
91
|
+
});
|
|
92
|
+
fs.appendFileSync(file, line + '\n');
|
|
93
|
+
}
|
|
94
|
+
catch { /* snapshotting is best-effort by design */ }
|
|
95
|
+
}
|
|
96
|
+
// Ordered list (append order == turn order) of delivered turns for a session, for render merge.
|
|
97
|
+
export function loadDeliveredTurns(sessionId) {
|
|
98
|
+
if (!sessionId)
|
|
99
|
+
return [];
|
|
100
|
+
try {
|
|
101
|
+
const content = fs.readFileSync(sessionSnapshotPath(sessionId), 'utf-8');
|
|
102
|
+
const out = [];
|
|
103
|
+
for (const raw of content.split('\n')) {
|
|
104
|
+
if (!raw.trim() || raw[0] !== '{')
|
|
105
|
+
continue;
|
|
106
|
+
try {
|
|
107
|
+
const ev = JSON.parse(raw);
|
|
108
|
+
if (typeof ev.text !== 'string' || !ev.text)
|
|
109
|
+
continue;
|
|
110
|
+
out.push({
|
|
111
|
+
prompt: typeof ev.prompt === 'string' ? ev.prompt : '',
|
|
112
|
+
promptNorm: typeof ev.promptNorm === 'string' ? ev.promptNorm : normalizeSnapshotPrompt(ev.prompt || ''),
|
|
113
|
+
text: ev.text,
|
|
114
|
+
model: typeof ev.model === 'string' ? ev.model : null,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch { /* skip bad line */ }
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// A per-prompt FIFO of delivered replies. A tombstone consumes the next reply recorded for its
|
|
126
|
+
// preceding prompt. Front-pop is exact for the common case (a unique recent prompt was swallowed);
|
|
127
|
+
// for a prompt repeated across turns with mixed outcomes the match is by order and may attach a
|
|
128
|
+
// sibling occurrence's reply — still strictly better than a bare tombstone.
|
|
129
|
+
export function buildRecoveryQueue(turns) {
|
|
130
|
+
const q = new Map();
|
|
131
|
+
for (const t of turns) {
|
|
132
|
+
if (!t.promptNorm)
|
|
133
|
+
continue;
|
|
134
|
+
const list = q.get(t.promptNorm);
|
|
135
|
+
if (list)
|
|
136
|
+
list.push(t.text);
|
|
137
|
+
else
|
|
138
|
+
q.set(t.promptNorm, [t.text]);
|
|
139
|
+
}
|
|
140
|
+
return q;
|
|
141
|
+
}
|
package/dist/bot/bot.js
CHANGED
|
@@ -11,6 +11,7 @@ import { resolveGuiIntegrationConfig } from '../agent/mcp/bridge.js';
|
|
|
11
11
|
import { composeSessionToolPrompt } from '../agent/mcp/capabilities.js';
|
|
12
12
|
import { terminateProcessTree } from '../core/process-control.js';
|
|
13
13
|
import { appendTurnAudit } from '../core/turn-audit.js';
|
|
14
|
+
import { recordDeliveredTurn } from '../agent/turn-snapshot.js';
|
|
14
15
|
import { expandTilde } from '../core/platform.js';
|
|
15
16
|
import { VERSION } from '../core/version.js';
|
|
16
17
|
import { buildHumanLoopResponse, createEmptyHumanLoopAnswer, currentHumanLoopQuestion, isHumanLoopAwaitingText, setHumanLoopOption, setHumanLoopText, skipHumanLoopQuestion, summarizeResolvedHumanLoopAnswers, } from './human-loop.js';
|
|
@@ -2132,6 +2133,16 @@ export class Bot {
|
|
|
2132
2133
|
error: result.error ? String(result.error).slice(0, 500) : null,
|
|
2133
2134
|
elapsedS: result.elapsedS, model: result.model ?? resolvedModel, promptPreview: prompt.slice(0, 120),
|
|
2134
2135
|
});
|
|
2136
|
+
// Durable copy of what we just delivered, so the history render can restore it if the CLI's
|
|
2137
|
+
// jsonl later loses this turn to a flush race or an in-place resume rewrite (the swallow).
|
|
2138
|
+
// claude-only: it is the driver whose renderer knows how to merge these at a tombstone.
|
|
2139
|
+
if (cs.agent === 'claude') {
|
|
2140
|
+
recordDeliveredTurn({
|
|
2141
|
+
sessionId: result.sessionId || cs.sessionId || null,
|
|
2142
|
+
prompt, message: result.message, model: result.model ?? resolvedModel ?? null,
|
|
2143
|
+
ok: result.ok, stopReason: result.stopReason ?? null,
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2135
2146
|
if (cs.agent === 'claude' && workflowEnabled && result.thinkingEffort) {
|
|
2136
2147
|
result.thinkingEffort = 'ultra';
|
|
2137
2148
|
}
|
package/package.json
CHANGED
|
@@ -40,6 +40,9 @@ export declare function claudeBgSettleQuietMs(): number;
|
|
|
40
40
|
export declare function claudeModelStallMs(): number;
|
|
41
41
|
export declare const CLAUDE_TRUNCATED_RECOVERY_PROMPT: string;
|
|
42
42
|
export declare function claudeTruncatedRecoveryEnabled(): boolean;
|
|
43
|
+
export declare function isClaudeSyntheticResumeNoise(text: string): boolean;
|
|
44
|
+
export declare function claudeProducedRealOutput(s: any): boolean;
|
|
45
|
+
export declare function claudeResumeNoopRetryLimit(): number;
|
|
43
46
|
export declare function claudeUserEventHasToolResult(ev: any): boolean;
|
|
44
47
|
export declare function isTerminalTaskStatus(status: unknown): boolean;
|
|
45
48
|
export declare function trackClaudeBackgroundTask(ev: any, s: any): void;
|
|
@@ -79,6 +79,10 @@ export class ClaudeDriver {
|
|
|
79
79
|
let settled = false;
|
|
80
80
|
// One-shot guard for the truncated-turn recovery injection (see the result handler).
|
|
81
81
|
let truncatedRecoveryAttempted = false;
|
|
82
|
+
// Bounded counter for the no-op-resume recovery re-injection (see the result handler): a
|
|
83
|
+
// resume of a session left incomplete by a prior turn can no-op several times before the
|
|
84
|
+
// CLI's repair clears and it runs our prompt for real.
|
|
85
|
+
let noopResumeRetries = 0;
|
|
82
86
|
// holdCap: hard backstop while a background task is still running (never-completing daemon).
|
|
83
87
|
// quiet: fires once all known background tasks finished AND Claude has gone quiet, so trailing
|
|
84
88
|
// wake-up turns can still land before we close (see claudeBgSettleQuietMs).
|
|
@@ -278,6 +282,29 @@ export class ClaudeDriver {
|
|
|
278
282
|
settleResult({ stopReason: 'truncated', kill: false });
|
|
279
283
|
return;
|
|
280
284
|
}
|
|
285
|
+
// No-op resume repair: resuming a session whose previous turn was left incomplete (a
|
|
286
|
+
// background hold that reclaimed its sub-agents/workflow — the ultra "no response"
|
|
287
|
+
// report — an interrupt, a stall) makes the CLI answer with a synthetic "No response
|
|
288
|
+
// requested." no-op that ran NONE of our prompt (no model turn at all), instead of
|
|
289
|
+
// processing the message. Settling here delivers a silent "(no textual response)" and
|
|
290
|
+
// drops the user's send. stdin is still open: re-issue the prompt so the CLI drives
|
|
291
|
+
// through its repair to a real answer within this one turn. Bounded (the repair can
|
|
292
|
+
// no-op more than once); the post-tool stall watchdog is the backstop if the CLI never
|
|
293
|
+
// engages. Scoped to resumes (input.sessionId) — a fresh session has no dangling turn.
|
|
294
|
+
if (!hasError && !!input.sessionId && !claudeProducedRealOutput(state)
|
|
295
|
+
&& noopResumeRetries < claudeResumeNoopRetryLimit()) {
|
|
296
|
+
noopResumeRetries++;
|
|
297
|
+
let injected = false;
|
|
298
|
+
try {
|
|
299
|
+
child.stdin.write(claudeUserMessage(input.prompt, input.attachments) + '\n');
|
|
300
|
+
injected = true;
|
|
301
|
+
}
|
|
302
|
+
catch { /* fall through to settle */ }
|
|
303
|
+
if (injected) {
|
|
304
|
+
armModelStall();
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
281
308
|
// kill=false: the CLI persists the turn into the session jsonl AFTER emitting
|
|
282
309
|
// `result`, and on a large session that flush (a whole-file rewrite) takes long
|
|
283
310
|
// enough that an immediate SIGTERM kills the process mid-write — the reply was
|
|
@@ -523,6 +550,13 @@ export function handleClaudeEvent(ev, s, emit) {
|
|
|
523
550
|
return;
|
|
524
551
|
}
|
|
525
552
|
if (t === 'assistant') {
|
|
553
|
+
// Synthetic resume-repair no-op: resuming a session whose previous turn was left incomplete
|
|
554
|
+
// makes the CLI emit an assistant message with model '<synthetic>' whose only text is
|
|
555
|
+
// "No response requested." (paired with an isMeta "Continue from where you left off." user
|
|
556
|
+
// record). It is NOT model output — mirror the legacy driver and drop it, so it neither shows
|
|
557
|
+
// as the reply nor counts as real output (the no-op-resume recovery keys off that emptiness).
|
|
558
|
+
if (ev.message?.model === '<synthetic>' && isClaudeSyntheticResumeNoise(claudeContentText(ev.message?.content)))
|
|
559
|
+
return;
|
|
526
560
|
const contents = ev.message?.content || [];
|
|
527
561
|
for (const b of contents) {
|
|
528
562
|
if (b?.type !== 'tool_use')
|
|
@@ -791,6 +825,31 @@ export function claudeTruncatedRecoveryEnabled() {
|
|
|
791
825
|
const v = String(process.env.PIKILOOM_CLAUDE_TRUNCATED_RECOVERY ?? '').trim().toLowerCase();
|
|
792
826
|
return v !== '0' && v !== 'false' && v !== 'off';
|
|
793
827
|
}
|
|
828
|
+
// The CLI's resume-repair placeholder for a turn that never concluded: an assistant message with
|
|
829
|
+
// model '<synthetic>' whose only text is "No response requested." (paired with an isMeta "Continue
|
|
830
|
+
// from where you left off." user record). It is NOT model output. Mirrors the legacy driver.
|
|
831
|
+
export function isClaudeSyntheticResumeNoise(text) {
|
|
832
|
+
const t = (text || '').trim().toLowerCase();
|
|
833
|
+
return t === 'no response requested.' || t === 'no response requested';
|
|
834
|
+
}
|
|
835
|
+
// True once the turn produced ANY real model output — streamed or whole-message text/reasoning, a
|
|
836
|
+
// tool use, or a spawned sub-agent. False for a pure no-op (a synthetic resume-repair result that
|
|
837
|
+
// ran none of the prompt), which is exactly what the no-op-resume recovery keys off. Pure +
|
|
838
|
+
// exported for hermetic testing.
|
|
839
|
+
export function claudeProducedRealOutput(s) {
|
|
840
|
+
return !!(s?.streamedText || s?.streamedReasoning
|
|
841
|
+
|| (s?.tools?.size ?? 0) > 0 || (s?.subAgents?.size ?? 0) > 0
|
|
842
|
+
|| (typeof s?.text === 'string' && s.text.trim().length > 0)
|
|
843
|
+
|| (typeof s?.reasoning === 'string' && s.reasoning.trim().length > 0));
|
|
844
|
+
}
|
|
845
|
+
// How many times to re-issue the prompt when a resume comes back a pure no-op before giving up and
|
|
846
|
+
// settling (see the result handler). Bounded so a genuinely dead session can't loop forever; 0
|
|
847
|
+
// disables the recovery. Override with PIKILOOM_CLAUDE_RESUME_NOOP_RETRIES.
|
|
848
|
+
const CLAUDE_RESUME_NOOP_RETRY_DEFAULT = 3;
|
|
849
|
+
export function claudeResumeNoopRetryLimit() {
|
|
850
|
+
const raw = Number(process.env.PIKILOOM_CLAUDE_RESUME_NOOP_RETRIES);
|
|
851
|
+
return Number.isFinite(raw) && raw >= 0 ? raw : CLAUDE_RESUME_NOOP_RETRY_DEFAULT;
|
|
852
|
+
}
|
|
794
853
|
// True when a claude `type:'user'` stream event carries at least one tool_result block — i.e. the
|
|
795
854
|
// tool loop just handed control back to the model. Pure + exported for hermetic testing.
|
|
796
855
|
export function claudeUserEventHasToolResult(ev) {
|
|
@@ -9,7 +9,7 @@ export type { SessionStore, CoreSessionRecord, ModelResolver, ModelInjection, To
|
|
|
9
9
|
export type { LoomIO, PromptInput, Surface, SurfaceCapabilities, Plugin, SpawnContribution, } from './contracts/surface.js';
|
|
10
10
|
export { FsSessionStore, NullModelResolver, NoopToolProvider, PassthroughSystemPromptBuilder, AutoCancelInteractionHandler, DeferToTerminalInteractionHandler, NoopCatalog, defaultBaseDir, } from './ports/defaults.js';
|
|
11
11
|
export { EchoDriver } from './drivers/echo.js';
|
|
12
|
-
export { ClaudeDriver, isTerminalTaskStatus, trackClaudeBackgroundTask, pendingClaudeBackgroundTasks, markClaudeTaskNotificationTerminal, decideClaudeResultSettle, claudeBgHoldCapMs, claudeBgAgentHoldCapMs, claudeTurnHasAgentBackground, claudeBgHoldRecheckMs, claudeBgSettleQuietMs, type ClaudeResultSettleDecision, claudeModelStallMs, claudeUserEventHasToolResult, handleClaudeEvent, claudeTurnEndedDangling, claudeTruncatedRecoveryEnabled, CLAUDE_TRUNCATED_RECOVERY_PROMPT, } from './drivers/claude.js';
|
|
12
|
+
export { ClaudeDriver, isTerminalTaskStatus, trackClaudeBackgroundTask, pendingClaudeBackgroundTasks, markClaudeTaskNotificationTerminal, decideClaudeResultSettle, claudeBgHoldCapMs, claudeBgAgentHoldCapMs, claudeTurnHasAgentBackground, claudeBgHoldRecheckMs, claudeBgSettleQuietMs, type ClaudeResultSettleDecision, claudeModelStallMs, claudeUserEventHasToolResult, handleClaudeEvent, claudeTurnEndedDangling, claudeTruncatedRecoveryEnabled, CLAUDE_TRUNCATED_RECOVERY_PROMPT, isClaudeSyntheticResumeNoise, claudeProducedRealOutput, claudeResumeNoopRetryLimit, } from './drivers/claude.js';
|
|
13
13
|
export { CodexDriver } from './drivers/codex.js';
|
|
14
14
|
export { GeminiDriver } from './drivers/gemini.js';
|
|
15
15
|
export { AcpDriver, type AcpDriverConfig } from './drivers/acp.js';
|
|
@@ -19,7 +19,7 @@ export { attachTui } from './runtime/tui.js';
|
|
|
19
19
|
export { FsSessionStore, NullModelResolver, NoopToolProvider, PassthroughSystemPromptBuilder, AutoCancelInteractionHandler, DeferToTerminalInteractionHandler, NoopCatalog, defaultBaseDir, } from './ports/defaults.js';
|
|
20
20
|
// Drivers & surfaces (also available via subpath exports)
|
|
21
21
|
export { EchoDriver } from './drivers/echo.js';
|
|
22
|
-
export { ClaudeDriver, isTerminalTaskStatus, trackClaudeBackgroundTask, pendingClaudeBackgroundTasks, markClaudeTaskNotificationTerminal, decideClaudeResultSettle, claudeBgHoldCapMs, claudeBgAgentHoldCapMs, claudeTurnHasAgentBackground, claudeBgHoldRecheckMs, claudeBgSettleQuietMs, claudeModelStallMs, claudeUserEventHasToolResult, handleClaudeEvent, claudeTurnEndedDangling, claudeTruncatedRecoveryEnabled, CLAUDE_TRUNCATED_RECOVERY_PROMPT, } from './drivers/claude.js';
|
|
22
|
+
export { ClaudeDriver, isTerminalTaskStatus, trackClaudeBackgroundTask, pendingClaudeBackgroundTasks, markClaudeTaskNotificationTerminal, decideClaudeResultSettle, claudeBgHoldCapMs, claudeBgAgentHoldCapMs, claudeTurnHasAgentBackground, claudeBgHoldRecheckMs, claudeBgSettleQuietMs, claudeModelStallMs, claudeUserEventHasToolResult, handleClaudeEvent, claudeTurnEndedDangling, claudeTruncatedRecoveryEnabled, CLAUDE_TRUNCATED_RECOVERY_PROMPT, isClaudeSyntheticResumeNoise, claudeProducedRealOutput, claudeResumeNoopRetryLimit, } from './drivers/claude.js';
|
|
23
23
|
export { CodexDriver } from './drivers/codex.js';
|
|
24
24
|
export { GeminiDriver } from './drivers/gemini.js';
|
|
25
25
|
export { AcpDriver } from './drivers/acp.js';
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{r as n,j as s}from"./react-vendor-C7Sl8SE7.js";import{c as on,I as un,S as ze,m as We,a as T,u as ht,d as cn,g as dn,l as fn,e as mn,j as gn,f as pn,s as hn,Y as xn}from"./index-h42cuqnj.js";import{s as kn,l as Sn,n as Et,m as bn,a as vn,o as In,d as Tn,b as yn,p as Rn,h as Mt,c as Qe,e as jn,f as wn,g as Ct,i as $e,j as Ke,k as Nn,q as An,T as Ln,R as On,U as Ut,r as Pn,t as qt,L as Dt,u as En,I as Mn}from"./index-CPmFmj6P.js";import{M as Ft,a as Bt}from"./Modal-bvNc9zVs.js";import"./router-DHISdpPk.js";import"./Select-D-rJwrhF.js";import"./DirBrowser-Br3AnAYT.js";import"./markdown-DxQYQFeH.js";import"./ExtensionsTab-CHO2uMeN.js";function Cn({snapshot:a}){const[t,f]=n.useState(a.currentIndex??0),[N,y]=n.useState(""),[x,h]=n.useState(!1),[C,S]=n.useState(null);n.useEffect(()=>{f(a.currentIndex??0),y(""),S(null)},[a.promptId,a.currentIndex]);const O=a.questions||[],R=O[t]||null,Z=O.length,U=!!(R?.options&&R.options.length),P=U?!!R?.allowFreeform:!0,de=c=>{c&&(f(i=>i+1),y(""))},fe=async c=>{if(!x){h(!0),S(null);try{const i=await T.interactionSelectOption(a.promptId,c);if(!i.ok){S(i.error||"Failed to submit selection.");return}de(i.advanced)}catch(i){S(i?.message||"Network error.")}finally{h(!1)}}},me=async()=>{if(x)return;const c=N.trim();if(!c&&!R?.allowEmpty){S("Please enter a response.");return}h(!0),S(null);try{const i=await T.interactionSubmitText(a.promptId,c);if(!i.ok){S(i.error||"Failed to submit answer.");return}de(i.advanced)}catch(i){S(i?.message||"Network error.")}finally{h(!1)}},m=async()=>{if(!x){h(!0),S(null);try{const c=await T.interactionSkip(a.promptId);if(!c.ok){S(c.error||"Failed to skip.");return}de(c.advanced)}catch(c){S(c?.message||"Network error.")}finally{h(!1)}}},ee=async()=>{if(!x){h(!0);try{await T.interactionCancel(a.promptId)}catch{}}},te=n.useMemo(()=>{const c=[];return a.hint&&c.push(a.hint),Z>1&&c.push(`Question ${t+1} of ${Z}`),c.join(" · ")||void 0},[a.hint,t,Z]);return s.jsxs(Ft,{open:!0,onClose:ee,wide:U&&(R?.options?.length||0)>3,children:[s.jsx(Bt,{title:a.title||"Pikiloom needs your input",description:te,onClose:ee}),R?s.jsxs("div",{className:"space-y-4",children:[s.jsxs("div",{children:[s.jsx("div",{className:"text-xs font-medium uppercase tracking-wide text-fg-5",children:R.header||"Question"}),s.jsx("div",{className:"mt-1 whitespace-pre-wrap text-sm leading-relaxed text-fg",children:R.prompt})]}),U&&s.jsx("div",{className:"grid grid-cols-1 gap-2 sm:grid-cols-2",children:(R.options||[]).map(c=>s.jsxs("button",{type:"button",disabled:x,onClick:()=>fe(c.value||c.label),className:on("group rounded-lg border border-edge bg-panel-alt px-3 py-2 text-left text-sm transition","hover:border-control-border-h hover:bg-control-h hover:shadow-sm","focus:outline-none focus:ring-2 focus:ring-[var(--th-glow-a)]","disabled:cursor-not-allowed disabled:opacity-50"),children:[s.jsx("div",{className:"font-medium text-fg group-hover:text-fg",children:c.label}),c.description&&s.jsx("div",{className:"mt-0.5 text-xs leading-snug text-fg-4",children:c.description})]},c.value||c.label))}),P&&s.jsx("div",{children:s.jsx(un,{value:N,onChange:c=>y(c.target.value),onKeyDown:c=>{c.key==="Enter"&&!c.shiftKey&&!x&&(c.preventDefault(),me())},placeholder:U?"Or type a custom answer…":"Type your answer…",disabled:x,autoFocus:!U})}),C&&s.jsx("div",{className:"rounded-md border border-red-300/40 bg-red-500/10 px-3 py-2 text-xs text-red-600",children:C}),s.jsxs("div",{className:"flex items-center justify-between gap-3",children:[s.jsx("div",{className:"text-xs text-fg-5",children:x?s.jsxs("span",{className:"inline-flex items-center gap-2",children:[s.jsx(ze,{})," Submitting…"]}):s.jsxs("span",{children:["Press ",s.jsx("kbd",{className:"rounded border border-edge bg-panel-alt px-1.5 py-0.5 text-[10px] uppercase",children:"Enter"})," to send"]})}),s.jsxs("div",{className:"flex items-center gap-2",children:[s.jsx(We,{variant:"ghost",size:"sm",onClick:m,disabled:x,children:"Skip"}),P&&s.jsx(We,{variant:"primary",size:"sm",onClick:me,disabled:x||!N.trim()&&!R.allowEmpty,children:"Submit"})]})]})]}):s.jsxs("div",{className:"py-6 text-center text-sm text-fg-5",children:[s.jsx(ze,{className:"mr-2 inline-block"})," Waiting for the agent…"]})]})}function Un(a){return a.isNull?(a.localStreamPending||a.holdsActiveState)&&!a.holdExpired?"reject-null":"apply":(typeof a.updatedAt=="number"?a.updatedAt:0)<a.lastAppliedUpdatedAt?"reject-stale":"apply"}function qn(a,t){return typeof t!="number"?a:t>a?t:a}function Dn(a,t){return!a||!a.length?[]:t.size?a.filter(f=>!t.has(f)):a.slice()}function Hn(a,t,f,N){if(!a.size)return a;const y=new Set(t);let x=!1;const h=new Map;for(const[C,S]of a){if(f-S>N||!y.has(C)){x=!0;continue}h.set(C,S)}return x?h:a}const xt=12,Ht=160,Fn=96,Bn=[],_n=[],Qn=[],$n={phase:"streaming",text:"",thinking:""},Kn=20,zn=6e4,Wn=15e3,Yn=7e3,ie=new Map;function Gn(a,t){return`${a}:${t}`}function Vn(a,t){for(ie.delete(a),ie.set(a,t);ie.size>Kn;)ie.delete(ie.keys().next().value)}const or=n.memo(function({session:t,workdir:f,active:N=!0,onSessionChange:y,initialPendingPrompt:x,initialPendingImageUrls:h,onPendingPromptConsumed:C}){const S=ht(e=>e.locale),O=ht(e=>e.agentStatus?.agents?.find(r=>r.agent===t.agent)??null),R=O?.selectedEffort??null,Z=O?.selectedModel??null,U=ht(e=>e.modelLayer),P=t.profileId?U?.profiles.find(e=>e.id===t.profileId)??null:null,de=P?U?.providers.find(e=>e.id===P.providerId)??null:null,fe=t.profileId===void 0?O?.byokProviderName??null:de?.name??null,me=t.profileId===void 0?O?.byokProfileName??null:P&&P.name.trim().toLowerCase()!==P.modelId.trim().toLowerCase()?P.name:null,m=n.useMemo(()=>cn(S),[S]),ee=dn(t.agent||""),te=fn(t),c=!!x||!!(h&&h.length),[i,Ye]=n.useState(null),[Ge,ge]=n.useState(!c),[pe,kt]=n.useState(!1),[o,B]=n.useState(null),[_,he]=n.useState(!1),[q,Ve]=n.useState(null),[_t,St]=n.useState(0),[Qt,Xe]=n.useState(null),[D,Re]=n.useState([]),[$t,je]=n.useState([]),[xe,Je]=n.useState([]),[g,ne]=n.useState(x||null),[E,re]=n.useState(h||[]),[Kt,se]=n.useState(null),Q=n.useRef(null);Q.current=Kt;const bt=n.useRef(g);bt.current=g;const[we,H]=n.useState([]),Ne=n.useRef([]);Ne.current=we;const ke=n.useRef(null),[zt,vt]=n.useState(null),[Ae,Se]=n.useState(null),[be,Ze]=n.useState(""),[$,et]=n.useState(!1),Wt=!!O?.capabilities?.fork,tt=n.useRef(null),L=n.useRef(h||[]),K=n.useRef(o),nt=n.useRef(_);K.current=o,nt.current=_;const rt=n.useRef(D);rt.current=D;const It=n.useRef(q);It.current=q;const z=n.useRef(null),Le=n.useRef(null),W=n.useRef(!0),le=n.useRef(!1),Oe=n.useRef(null),st=n.useRef(!1),M=n.useRef(c),ae=n.useRef(!1),Y=n.useRef(!1),lt=n.useRef(!1),at=n.useRef(!1),G=n.useRef(null),Pe=n.useRef(0),ve=n.useRef(Date.now()),Ie=n.useRef(new Map),Tt=n.useRef({model:null,effort:null}),Yt=n.useCallback(e=>{Tt.current=e},[]);n.useEffect(()=>{lt.current||!c||(lt.current=!0,x&&!g&&ne(x),h&&h.length&&!E.length&&(re(h),L.current=h),ge(!1),St(e=>e+1),C?.())},[c,C]);const A=n.useCallback(()=>{ne(null),re(e=>{for(const r of e)URL.revokeObjectURL(r);return[]}),L.current=[],se(null)},[]),V=n.useCallback(()=>{H(e=>{if(!e.length)return e;for(const r of e)for(const l of r.imageUrls)URL.revokeObjectURL(l);return[]}),ke.current=null},[]),yt=n.useCallback((e,r)=>{ve.current=Date.now();const l=kn({streaming:nt.current,liveStreamPhase:K.current?.phase??null,streamPhase:It.current,queuedTaskCount:rt.current.length,pendingQueuedCount:Ne.current.length}),u=r||[];if(l){const d=`local-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,8)}`;ke.current=d,H(k=>[...k,{localId:d,taskId:null,prompt:e||"",imageUrls:u}]);return}K.current?.phase==="done"&&B(null);for(const d of L.current)URL.revokeObjectURL(d);ke.current=null,ne(e||null),re(u),L.current=u,se(null)},[]),Gt=n.useCallback(e=>{const r=ke.current;if(r){ke.current=null,H(l=>{const u=l.findIndex(k=>k.localId===r);if(u<0)return l;const d=l.slice();return d[u]={...d[u],taskId:e},d});return}se(e)},[]),Vt=n.useCallback(async()=>{if(!Ae)return;const e=be.trim();if(e){et(!0);try{const r=await T.forkSession(f,t.agent||"",t.sessionId,Ae.atTurn,e,{});if(!r.ok||!r.sessionKey){et(!1);return}const[l,u]=r.sessionKey.split(":");Se(null),Ze(""),y?.({agent:l,sessionId:u,workdir:f})}finally{et(!1)}}},[Ae,be,f,t.agent,t.sessionId,y]);tt.current=Vt;const Ee=n.useCallback(async(e,r={})=>{try{const l=await Sn({workdir:f,agent:t.agent||"",sessionId:t.sessionId,rich:!0,turnOffset:e.turnOffset,turnLimit:e.turnLimit,lastNTurns:e.lastNTurns},{force:r.force});return l.ok?Et(l):null}catch{return null}},[f,t.agent,t.sessionId]),j=n.useCallback(async({keepOlder:e,force:r=!1,scrollToBottom:l=!1})=>{const u=t.sessionId;if(Oe.current===u)return!1;Oe.current=u;try{const d=await Ee({turnOffset:0,turnLimit:xt},{force:r});if(!d||t.sessionId!==u)return!1;if(l&&(le.current=!0),Ye(k=>!k||!e?d:bn(k,d)),ge(!1),ae.current&&(ae.current=!1,A()),Y.current){const k=Y.current;Y.current=!1;const X=k!==!0?k.taskId:null;K.current&&(k===!0||K.current.taskId===X)&&B(null)}return!0}finally{Oe.current===u&&(Oe.current=null)}},[Ee,A,t.sessionId]),Me=n.useCallback(async()=>{if(!i?.hasOlder||st.current)return;const e=z.current;e&&(Le.current={scrollHeight:e.scrollHeight,scrollTop:e.scrollTop}),st.current=!0,kt(!0);try{const r=await Ee({turnOffset:Math.max(0,i.totalTurns-i.startTurn),turnLimit:xt});r?Ye(l=>l?vn(l,r):r):Le.current=null}finally{st.current=!1,kt(!1)}},[Ee,i]),Ce=n.useRef(null),oe=n.useCallback((e,r="ws")=>{const l=nt.current||rt.current.length>0,u=Date.now()-ve.current>Wn;if(Un({updatedAt:e?.updatedAt,isNull:!e,lastAppliedUpdatedAt:Pe.current,localStreamPending:M.current,holdsActiveState:l,holdExpired:u})!=="apply")return;const k=!e&&u&&(M.current||l);if(e&&(ve.current=Date.now(),Pe.current=qn(Pe.current,e.updatedAt)),e?.sessionId&&e.sessionId!==t.sessionId&&(at.current=!0,Te.current=`${t.agent}:${e.sessionId}`,y?.({agent:t.agent||"",sessionId:e.sessionId,workdir:f})),!e){const p=Ce.current;he(!1),k?(M.current=!1,ae.current=!0,Y.current=!0,V(),j({keepOlder:!0,force:!0,scrollToBottom:W.current})):p==="streaming"?(ae.current=!0,Y.current=!0,j({keepOlder:!0,force:!0,scrollToBottom:W.current})):B(null),k||(M.current&&p!=="streaming"?j({keepOlder:!0,force:!0}):(p==="done"&&(A(),V()),p!==null&&(M.current=!1))),Xe(null),Ve(null),Re([]),je([]),Je([]),Ce.current=null;return}Ve(e.phase),Xe(e.taskId||null);const X=[];e.taskId&&X.push(e.taskId),Array.isArray(e.queuedTaskIds)&&X.push(...e.queuedTaskIds),Ie.current=Hn(Ie.current,X,Date.now(),zn);const ue=Ie.current,gt=Dn(e.queuedTaskIds,ue),Pt=(Array.isArray(e.queuedTasks)?e.queuedTasks:[]).filter(p=>!ue.has(p.taskId));if(Re(gt.length?gt:Bn),je(Pt.length?Pt:_n),Je(Array.isArray(e.interactions)&&e.interactions.length?e.interactions:Qn),In({pendingTaskId:Q.current,streamTaskId:e.taskId||null,queuedTaskIds:e.queuedTaskIds})){const p=Q.current,w=bt.current||"",I=L.current;H(v=>v.some(ce=>ce.taskId===p)?v:[...v,{localId:`demote-${p}`,taskId:p,prompt:w,imageUrls:I}]),ne(null),re([]),L.current=[],se(null),Q.current=null}if(e.phase==="streaming"){if(B({taskId:e.taskId||null,phase:"streaming",text:e.text||"",thinking:e.thinking||"",activity:e.activity,plan:e.plan??null,model:e.model??null,effort:e.effort??null,previewMeta:e.previewMeta??null,subAgents:e.previewMeta?.subAgents??null,generatingImages:e.previewMeta?.generatingImages??0,artifacts:e.artifacts??null,startedAt:typeof e.startedAt=="number"?e.startedAt:null,error:null,question:e.question??null,questionBlocks:e.questionBlocks??null}),he(!0),e.taskId&&e.taskId!==Q.current){const p=Ne.current,w=p.findIndex(I=>I.taskId===e.taskId);if(w>=0){const I=p[w];for(const v of L.current)URL.revokeObjectURL(v);ne(I.prompt||null),re(I.imageUrls),L.current=I.imageUrls,se(e.taskId),H(v=>v.filter((ce,J)=>J!==w))}}W.current&&(le.current=!0)}else if(e.phase==="queued")B(null),he(!1);else if(e.phase==="done"){const p=Tn(K.current?.taskId??null,e.taskId||null),w=gt.length>0;if(p){he(!1),B(J=>J?{...J,phase:"done",error:e.error??null}:e.error?{taskId:e.taskId||null,phase:"done",text:"",thinking:"",activity:"",plan:null,model:e.model??null,effort:e.effort??null,previewMeta:e.previewMeta??null,subAgents:e.previewMeta?.subAgents??null,generatingImages:e.previewMeta?.generatingImages??0,artifacts:e.artifacts??null,error:e.error,question:e.question??null,questionBlocks:e.questionBlocks??null}:J);const I=K.current,v=!!I&&yn(I),ce=!!e.incomplete&&v&&!w;if(Ce.current!=="done"){w||(ae.current=!0),Y.current=ce?!1:{taskId:e.taskId||null},j({keepOlder:!0,force:!0,scrollToBottom:W.current});const J=t.agent||"",sn=t.sessionId,ln=Te.current;G.current&&clearTimeout(G.current),G.current=setTimeout(()=>{G.current=null,T.getSessionStreamState(J,sn).then(an=>{Te.current===ln&&Ue.current(an.state,"seed")}).catch(()=>{})},900)}}w||(M.current=!1)}const pt=new Set;if(e.taskId&&pt.add(e.taskId),Array.isArray(e.queuedTaskIds))for(const p of e.queuedTaskIds)pt.add(p);H(p=>{let w=!1;const I=[];for(const v of p)if(!v.taskId||pt.has(v.taskId))I.push(v);else{for(const ce of v.imageUrls)URL.revokeObjectURL(ce);w=!0}return w?I:p}),Ce.current=e.phase},[A,V,j,t.sessionId,t.agent,y,f]),Ue=n.useRef(oe);Ue.current=oe;const Rt=n.useCallback(()=>{M.current=!0,ve.current=Date.now(),St(e=>e+1)},[]);n.useEffect(()=>()=>{G.current&&(clearTimeout(G.current),G.current=null)},[]);const Xt=n.useCallback(async e=>{try{Ie.current.set(e,Date.now()),await T.recallSessionMessage(e),Q.current===e&&A(),H(r=>{let l=!1;const u=[];for(const d of r)if(d.taskId===e){for(const k of d.imageUrls)URL.revokeObjectURL(k);l=!0}else u.push(d);return l?u:r}),Re(r=>r.filter(l=>l!==e)),je(r=>r.filter(l=>l.taskId!==e)),Xe(r=>r===e?null:r)}catch{}},[A]),Jt=n.useCallback(async e=>{const r=Ne.current.find(l=>l.taskId===e)||null;if(r&&r.imageUrls.length>0){for(const l of L.current)URL.revokeObjectURL(l);ne(r.prompt||null),re(r.imageUrls),L.current=r.imageUrls,se(e),Q.current=e,H(l=>l.filter(u=>u.taskId!==e))}try{await T.steerSession(e)}catch{}},[]),Zt=n.useCallback(async()=>{try{await T.stopSession(t.agent||"",t.sessionId)}catch{}},[t.agent,t.sessionId]),qe=Gn(t.agent||"",t.sessionId);n.useEffect(()=>{if(at.current){at.current=!1;let d=!1;return j({keepOlder:!0,force:!0}).finally(()=>{d||ge(!1)}),()=>{d=!0}}let e=!1;const r=Rn({workdir:f,agent:t.agent||"",sessionId:t.sessionId,rich:!0,turnOffset:0,turnLimit:xt},{allowStale:!0}),l=c&&!lt.current,u=r?.ok?Et(r):ie.get(qe)||null;return ge(l?!1:!u),Ye(u),B(null),he(!1),Ve(null),Re([]),je([]),Je([]),Pe.current=0,ve.current=Date.now(),Ie.current=new Map,l||(A(),V(),M.current=!1,ae.current=!1,Y.current=!1),W.current=!0,le.current=!0,l||j({keepOlder:!1,force:!0}).finally(()=>{e||ge(!1)}),()=>{e=!0}},[j,t.agent,t.sessionId,f,qe,A,V]),n.useEffect(()=>{i&&i.turns.length>0&&Vn(qe,i)},[qe,i]),n.useEffect(()=>{N&&j({keepOlder:!0,force:!0})},[N,j]);const Te=n.useRef(`${t.agent}:${t.sessionId}`);Te.current=`${t.agent}:${t.sessionId}`,mn("stream-update",n.useCallback(e=>{e.key===Te.current&&oe(e.snapshot??null)},[oe])),n.useEffect(()=>{let e=!0;return T.getSessionStreamState(t.agent||"",t.sessionId).then(r=>{e&&Ue.current(r.state,"seed")}).catch(()=>{}),()=>{e=!1}},[t.agent,t.sessionId,_t]),gn(n.useCallback(()=>{T.getSessionStreamState(t.agent||"",t.sessionId).then(e=>{oe(e.state,"seed")}).catch(()=>{}),j({keepOlder:!0,force:!0})},[oe,t.agent,t.sessionId,j]));const jt=_||!!q||D.length>0||!!g||we.length>0;n.useEffect(()=>{if(!N||!jt)return;const e=setInterval(()=>{typeof document<"u"&&document.visibilityState!=="visible"||T.getSessionStreamState(t.agent||"",t.sessionId).then(r=>Ue.current(r.state,"seed")).catch(()=>{})},Yn);return()=>clearInterval(e)},[N,jt,t.agent,t.sessionId]),n.useEffect(()=>{!M.current&&te!=="running"&&!_&&!o&&!q&&D.length===0&&(A(),V())},[te,_,o,q,D.length,A,V]),n.useLayoutEffect(()=>{const e=Le.current,r=z.current;!e||!r||(Le.current=null,r.scrollTop=e.scrollTop+(r.scrollHeight-e.scrollHeight))},[i?.turns.length]),n.useLayoutEffect(()=>{if(!le.current)return;const e=z.current;e&&(le.current=!1,e.scrollTop=e.scrollHeight,requestAnimationFrame(()=>{W.current&&(e.scrollTop=e.scrollHeight)}))},[i,o]),n.useLayoutEffect(()=>{if(!g)return;const e=z.current;e&&(e.scrollTop=e.scrollHeight)},[g]),n.useEffect(()=>{if(!i?.hasOlder||Ge||pe)return;const e=z.current;e&&e.scrollHeight<=e.clientHeight+Ht&&Me()},[i?.hasOlder,i?.turns.length,Me,Ge,pe]);const en=n.useCallback(()=>{const e=z.current;if(!e)return;const r=e.scrollHeight-e.scrollTop-e.clientHeight;W.current=r<=Fn,e.scrollTop<=Ht&&Me()},[Me]),ye=o?.model||t.model||Z||null,De=pn(t.agent||"",o?.effort||t.thinkingEffort||R||null,t.workflowEnabled??O?.workflowEnabled)||null,ot=me&&(!ye||ye===Z)?me:ye?hn(ye):null,ut=xn(t,{streaming:_,hasLiveStream:!!o,streamPhase:q,queuedTaskCount:D.length}),b=i?.turns||[],wt=n.useMemo(()=>{for(let e=b.length-1;e>=0;e--){const r=b[e].assistant?.blocks;if(r)for(let l=r.length-1;l>=0;l--){const u=r[l];if(u.type==="plan"&&Mt(u.plan))return u.plan}}return null},[b]),He=n.useMemo(()=>{if(!E.length||!b.length)return!1;const e=b[b.length-1];return!e.user||!Qe(e.user.text,g)?!1:e.user.blocks.filter(l=>l.type==="image").length<E.length},[b,g,E.length]),ct=n.useMemo(()=>E.length?E.map(e=>({type:"image",content:e})):!g||!o?.questionBlocks?.length?[]:Qe(g,o.question)?o.questionBlocks:[],[E,g,o]),it=o?.question||null,Fe=jn(g,it),Nt=o&&Mt(o.plan)?o.plan:o&&wn(g,it)?wt:null,dt=Ct(it,g),At=b.length>0?b[b.length-1]?.user?.text:null,Lt=!!Fe&&b.length>0&&$e(At,Fe),F=n.useMemo(()=>{let e=b;if(He){const ue=e[e.length-1];e=[...e.slice(0,-1),{...ue,user:null}]}if(!o||!e.length)return e;const r=e[e.length-1],l=Fe;if(!r.assistant)return!!r.user&&!!l&&!Qe(r.user.text,l)&&($e(r.user.text,l)||Ct(l,r.user.text))?[...e.slice(0,-1),{...r,user:{...r.user,text:l}}]:e;const u=(o.text||"").trim(),d=r.assistant.text?.trim()||"";if(!(l!=null?$e(r.user?.text,l):!!d&&!!u&&(u.startsWith(d)||d.startsWith(u))))return e;const X=r.user&&l&&!Qe(r.user.text,l)?{...r.user,text:l}:r.user;return[...e.slice(0,-1),{...r,user:X,assistant:null}]},[b,o,Fe,He]),tn=(!!g||ct.length>0)&&!dt&&(He||!Lt)&&!o,nn=!!o&&Ke(o)&&o.phase==="streaming",ft=Nn({sessionRunning:te==="running",streaming:_,streamPhase:q,queuedTaskCount:D.length,pendingQueuedCount:we.length,liveTurnStreaming:nn,pendingBubbleDots:tn}),Be=ft&&F.length>0?F[F.length-1]:null,_e=!!Be?.assistant&&An(Be.assistant),mt=_e?Be.assistant.usage??null:null,rn=_e?Be.assistant.blocks.filter(e=>e.type==="tool_use").length:0,Ot=(()=>{if(!ft||te!=="running")return null;const e=t.runUpdatedAt?Date.parse(t.runUpdatedAt):NaN;return Number.isFinite(e)?e:null})();return s.jsxs("div",{className:"flex flex-col h-full overflow-hidden",children:[s.jsx("div",{ref:z,onScroll:en,className:"flex-1 overflow-y-auto overscroll-contain",children:Ge&&!g&&!E.length&&!o?s.jsx("div",{className:"flex items-center justify-center py-20",children:s.jsx(ze,{className:"h-5 w-5 text-fg-4"})}):F.length===0&&!g&&!E.length&&!o&&!ut?s.jsx("div",{className:"py-20 text-center text-[13px] text-fg-5",children:m("hub.noMessages")}):s.jsxs("div",{className:"max-w-[900px] mx-auto px-6 py-6 space-y-0",children:[(i?.hasOlder||pe)&&s.jsxs("div",{className:"mb-4 flex items-center justify-center gap-2 text-[11px] text-fg-5",children:[pe?s.jsx(ze,{className:"h-3 w-3 text-fg-5"}):s.jsx("span",{className:"h-1.5 w-1.5 rounded-full bg-fg-5/35"}),s.jsx("span",{children:m(pe?"hub.loadingOlderTurns":"hub.loadOlderTurnsHint")})]}),t.migratedFrom?.kind==="fork"&&t.migratedFrom.sessionId&&s.jsxs("button",{type:"button",onClick:()=>y?.({agent:t.migratedFrom.agent||t.agent||"",sessionId:t.migratedFrom.sessionId,workdir:f}),className:"mb-4 inline-flex items-center gap-1.5 rounded-md border border-edge bg-panel-alt px-2.5 py-1 text-[11px] text-fg-5 transition hover:border-edge-h hover:text-fg-2",title:`#${t.migratedFrom.sessionId.slice(0,8)}`,children:[s.jsxs("svg",{width:"10",height:"10",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2",children:[s.jsx("circle",{cx:"6",cy:"6",r:"2"}),s.jsx("circle",{cx:"18",cy:"6",r:"2"}),s.jsx("circle",{cx:"12",cy:"20",r:"2"}),s.jsx("path",{d:"M6 8v3a3 3 0 0 0 3 3h6a3 3 0 0 0 3-3V8"}),s.jsx("path",{d:"M12 14v4"})]}),s.jsx("span",{children:m("hub.forkBadge")}),s.jsxs("span",{className:"font-mono",children:["#",t.migratedFrom.sessionId.slice(0,8)]}),typeof t.migratedFrom.forkedAtTurn=="number"&&s.jsxs("span",{className:"text-fg-5/70",children:["· ",m("hub.forkBadgeAt").replace("{turn}",String(t.migratedFrom.forkedAtTurn+1))]})]}),F.map((e,r)=>{const l=(i?.startTurn||0)+r;return s.jsx(Ln,{turn:e,turnIndex:l,hideHeaderUsage:_e&&r===F.length-1,isTail:r===F.length-1,fallbackPlan:r===F.length-1&&!(o&&Ke(o))?wt:void 0,agent:t.agent||"",meta:ee,model:ot,effort:De,providerName:fe,t:m,workdir:f,onResend:u=>{le.current=!0,yt(u);const d=Tt.current;T.sendSessionMessage(f,t.agent||"",t.sessionId,u,{model:d.model||ye||void 0,effort:d.effort||De||void 0}).then(k=>{k.ok&&Rt()}).catch(()=>{A()})},onEdit:u=>vt(u),onFork:Wt?u=>{Ze(""),Se({atTurn:u})}:void 0},`${i?.startTurn||0}:${r}`)}),ut&&s.jsx("div",{className:"mb-5 animate-in",children:s.jsx(On,{detail:ut,t:m})}),(g||ct.length>0)&&!dt&&(He||!Lt)&&s.jsxs("div",{className:"session-turn session-turn--live",children:[s.jsx(Ut,{text:g||"",blocks:ct,t:m}),!o&&s.jsx("div",{className:"mt-3 mb-5 animate-in",children:s.jsx(Pn,{className:"text-fg-5"})})]}),o&&Ke(o)&&o.question&&(!g||dt)&&!(b.length>0&&$e(At,o.question))&&s.jsx("div",{className:"session-turn session-turn--live",children:s.jsx(Ut,{text:o.question,blocks:o.questionBlocks||void 0,t:m})}),o&&Ke(o)&&s.jsxs("div",{className:"mb-6",children:[s.jsx(qt,{agent:t.agent||"",meta:ee,model:ot,effort:De,providerName:fe,previewMeta:o.previewMeta,hideContextUsage:!0}),s.jsx(Dt,{stream:Nt===o.plan?o:{...o,plan:Nt},t:m,workdir:f})]}),ft&&(_e?s.jsx("div",{className:"-mt-3 mb-6 animate-in",children:s.jsx(En,{toolCount:rn,ctxPct:mt?.contextPercent??null,ctxTokens:mt?.contextUsedTokens??0,turnOutTokens:mt?.turnOutputTokens??0,startedAt:Ot})}):s.jsxs("div",{className:"mb-6",children:[s.jsx(qt,{agent:t.agent||"",meta:ee,model:ot,effort:De,providerName:fe,hideContextUsage:!0}),s.jsx(Dt,{stream:{...$n,startedAt:Ot},t:m,workdir:f})]})),s.jsx("div",{className:"h-4"})]})}),s.jsx(Mn,{session:t,workdir:f,onStreamQueued:Rt,onSendStart:yt,onSendTaskAssigned:Gt,onSessionChange:y,t:m,streamPhase:q,streamTaskId:Qt,queuedTaskIds:D,queuedTasks:$t,pendingQueuedSends:we,onRecall:Xt,onSteer:Jt,onStopAll:Zt,editDraft:zt,onEditDraftConsumed:()=>vt(null),onSelectionChange:Yt}),Ae&&s.jsxs(Ft,{open:!0,onClose:()=>{$||Se(null)},children:[s.jsx(Bt,{title:m("hub.forkPromptTitle"),description:m("hub.forkPromptHint"),onClose:()=>{$||Se(null)}}),s.jsx("textarea",{autoFocus:!0,value:be,disabled:$,onChange:e=>Ze(e.target.value),onKeyDown:e=>{e.key==="Enter"&&(e.metaKey||e.ctrlKey)&&be.trim()&&!$&&(e.preventDefault(),tt.current?.())},placeholder:m("hub.forkPromptPlaceholder"),className:"w-full min-h-[120px] resize-y rounded-md border border-edge bg-panel-alt px-3 py-2 text-[13px] leading-relaxed text-fg outline-none focus:border-edge-h"}),s.jsxs("div",{className:"mt-4 flex items-center justify-end gap-2",children:[s.jsx(We,{variant:"ghost",disabled:$,onClick:()=>Se(null),children:m("modal.cancel")}),s.jsx(We,{variant:"primary",disabled:$||!be.trim(),onClick:()=>{tt.current?.()},children:m($?"hub.forkSubmitting":"hub.forkSubmit")})]})]}),N&&xe.length>0&&s.jsx(Cn,{snapshot:xe[xe.length-1]},xe[xe.length-1].promptId)]})});export{or as SessionPanel};
|