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.
Files changed (23) hide show
  1. package/dashboard/dist/assets/{AgentTab-0iSS-Bby.js → AgentTab-KSanVg_l.js} +1 -1
  2. package/dashboard/dist/assets/{ConnectionModal-DRg8X2s1.js → ConnectionModal-BSoA2mZL.js} +1 -1
  3. package/dashboard/dist/assets/{DirBrowser-Br3AnAYT.js → DirBrowser-zKX7wmty.js} +1 -1
  4. package/dashboard/dist/assets/{ExtensionsTab-CHO2uMeN.js → ExtensionsTab-DKLjCr4u.js} +1 -1
  5. package/dashboard/dist/assets/{IMAccessTab-7v8e2UqN.js → IMAccessTab-BhvB7Hes.js} +1 -1
  6. package/dashboard/dist/assets/{Modal-bvNc9zVs.js → Modal-Qwlap_Fl.js} +1 -1
  7. package/dashboard/dist/assets/{Modals-CXBXc4Xx.js → Modals-2Zw2FSLo.js} +1 -1
  8. package/dashboard/dist/assets/{Select-D-rJwrhF.js → Select-dIxoi_TN.js} +1 -1
  9. package/dashboard/dist/assets/SessionPanel--D5NGYgd.js +1 -0
  10. package/dashboard/dist/assets/{SystemTab-NTIGkssg.js → SystemTab-CtjiZa5s.js} +1 -1
  11. package/dashboard/dist/assets/{index-h42cuqnj.js → index-BDSh0L9U.js} +3 -3
  12. package/dashboard/dist/assets/{index-CPmFmj6P.js → index-DN0a6ZnO.js} +16 -16
  13. package/dashboard/dist/assets/{shared-CUTx2Dth.js → shared-D2sGZJ9f.js} +1 -1
  14. package/dashboard/dist/index.html +1 -1
  15. package/dist/agent/drivers/claude.js +28 -1
  16. package/dist/agent/turn-snapshot.js +141 -0
  17. package/dist/bot/bot.js +11 -0
  18. package/package.json +1 -1
  19. package/packages/kernel/dist/drivers/claude.d.ts +3 -0
  20. package/packages/kernel/dist/drivers/claude.js +59 -0
  21. package/packages/kernel/dist/index.d.ts +1 -1
  22. package/packages/kernel/dist/index.js +1 -1
  23. 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-h42cuqnj.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};
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-h42cuqnj.js"></script>
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
- pendingBlocks.push({ type: 'system_notice', content: displayText });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pikiloom",
3
- "version": "0.4.59",
3
+ "version": "0.4.61",
4
4
  "description": "Put the world's smartest AI agents in your pocket. Command local Claude & Gemini via IM. | 让最好用的 IM 变成你电脑上的顶级 Agent 控制台",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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};