pikiloom 0.4.53 → 0.4.55

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 (25) hide show
  1. package/dashboard/dist/assets/{AgentTab-DSpqOkYp.js → AgentTab-CvghrxSK.js} +1 -1
  2. package/dashboard/dist/assets/{ConnectionModal-BFcN8nJr.js → ConnectionModal-DHw2M8z4.js} +1 -1
  3. package/dashboard/dist/assets/{DirBrowser-Dk-TadnX.js → DirBrowser-hui6YZ3V.js} +1 -1
  4. package/dashboard/dist/assets/{ExtensionsTab-wiaVcnfS.js → ExtensionsTab-DAqHco47.js} +1 -1
  5. package/dashboard/dist/assets/{IMAccessTab-C4ltstAX.js → IMAccessTab-BFKhRG8w.js} +1 -1
  6. package/dashboard/dist/assets/{Modal-BeyxM41U.js → Modal-oPg6drTY.js} +1 -1
  7. package/dashboard/dist/assets/{Modals-B2a8IIvO.js → Modals-DwMaDCJc.js} +1 -1
  8. package/dashboard/dist/assets/{Select-DxYPQ-41.js → Select-Dlz8-JdF.js} +1 -1
  9. package/dashboard/dist/assets/SessionPanel-DPb40DDF.js +1 -0
  10. package/dashboard/dist/assets/{SystemTab-1frRjK4R.js → SystemTab-CzexSRMC.js} +1 -1
  11. package/dashboard/dist/assets/{index-dR0w8M7K.js → index-BIX62DbC.js} +2 -2
  12. package/dashboard/dist/assets/{index-C-9wwAiw.js → index-CRE3Clam.js} +15 -15
  13. package/dashboard/dist/assets/{shared-BbgVZfU_.js → shared-BATHuwRO.js} +1 -1
  14. package/dashboard/dist/index.html +1 -1
  15. package/dist/agent/drivers/claude.js +37 -45
  16. package/dist/agent/index.js +1 -1
  17. package/dist/agent/kernel-bridge.js +6 -1
  18. package/dist/agent/utils.js +33 -0
  19. package/dist/bot/commands.js +4 -2
  20. package/package.json +1 -1
  21. package/packages/kernel/dist/drivers/claude.d.ts +3 -0
  22. package/packages/kernel/dist/drivers/claude.js +53 -4
  23. package/packages/kernel/dist/index.d.ts +1 -1
  24. package/packages/kernel/dist/index.js +1 -1
  25. package/dashboard/dist/assets/SessionPanel-CYO6NGSt.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-dR0w8M7K.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-BIX62DbC.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-dR0w8M7K.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-BIX62DbC.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-BQ_R1F_G.css">
@@ -3,7 +3,7 @@ import path from 'node:path';
3
3
  import { execSync, spawn } from 'node:child_process';
4
4
  import { createInterface } from 'node:readline';
5
5
  import { registerDriver } from '../driver.js';
6
- import { Q, run, agentError, agentLog, agentWarn, buildStreamPreviewMeta, computeContext, pushRecentActivity, summarizeClaudeToolUse, summarizeClaudeToolResult, joinErrorMessages, parseTodoWriteAsPlan, previewToolCallInput, previewToolCallResult, detectClaudeApiError, isRetryableClaudeApiError, detectClaudeModelError, claudeModelErrorMessage, emitSessionIdUpdate, IMAGE_EXTS, mimeForExt, listPikiloomSessions, mergeManagedAndNativeSessions, managedRecordToSessionInfo, readTailLines, stripInjectedPrompts, sanitizeSessionUserPreviewText, SESSION_PREVIEW_IMAGE_PLACEHOLDER_RE, CLAUDE_AT_MENTION_IMAGE_RE, extractClaudeAtMentionImagePaths, attachAgentImage, applyTurnWindow, shortValue, roundPercent, toIsoFromEpochSeconds, modelFamily, normalizeClaudeModelId, emptyUsage, normalizeUsageStatus, collapseSkillPrompt, } from '../index.js';
6
+ import { Q, run, agentError, agentLog, agentWarn, buildStreamPreviewMeta, computeContext, pushRecentActivity, summarizeClaudeToolUse, summarizeClaudeToolResult, joinErrorMessages, parseTodoWriteAsPlan, previewToolCallInput, previewToolCallResult, detectClaudeApiError, isRetryableClaudeApiError, detectClaudeModelError, claudeModelErrorMessage, emitSessionIdUpdate, IMAGE_EXTS, mimeForExt, listPikiloomSessions, mergeManagedAndNativeSessions, managedRecordToSessionInfo, readTailLines, stripInjectedPrompts, sanitizeSessionUserPreviewText, isSystemInjectedUserText, SESSION_PREVIEW_IMAGE_PLACEHOLDER_RE, CLAUDE_AT_MENTION_IMAGE_RE, extractClaudeAtMentionImagePaths, attachAgentImage, applyTurnWindow, shortValue, roundPercent, toIsoFromEpochSeconds, modelFamily, normalizeClaudeModelId, emptyUsage, normalizeUsageStatus, collapseSkillPrompt, } from '../index.js';
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';
@@ -1343,12 +1343,16 @@ function getClaudeSessionTail(opts) {
1343
1343
  try {
1344
1344
  const ev = JSON.parse(raw);
1345
1345
  if (ev.type === 'user') {
1346
+ if (ev.isMeta === true)
1347
+ continue;
1346
1348
  const text = stripInjectedPrompts(extractClaudeText(ev.message?.content, true));
1347
- if (text)
1349
+ if (text && !isSystemInjectedUserText(text))
1348
1350
  allMsgs.push({ role: 'user', text });
1349
1351
  }
1350
1352
  else if (ev.type === 'assistant') {
1351
1353
  const text = extractClaudeText(ev.message?.content, true);
1354
+ if (ev.message?.model === '<synthetic>' && isClaudeSyntheticResumeNoise(text))
1355
+ continue;
1352
1356
  if (text)
1353
1357
  allMsgs.push({ role: 'assistant', text });
1354
1358
  }
@@ -1425,26 +1429,6 @@ function extractClaudeBlocks(content, skipSystemBlocks = false, todoWriteToolIds
1425
1429
  }
1426
1430
  return blocks;
1427
1431
  }
1428
- const SYSTEM_INJECTED_USER_TAGS = new Set([
1429
- 'task-notification',
1430
- 'system-reminder',
1431
- 'persisted-output',
1432
- 'local-command-stdout',
1433
- 'local-command-caveat',
1434
- 'local-command-stderr',
1435
- 'ide_opened_file',
1436
- 'ide_diagnostics',
1437
- 'ide_selection',
1438
- 'event',
1439
- 'analysis',
1440
- 'case_id',
1441
- 'tool-use-id',
1442
- 'output-file',
1443
- // Kernel claude driver's in-process truncated-turn recovery prompt (see packages/kernel
1444
- // drivers/claude.ts): injected on the CLI's stdin, so it lands in the jsonl as a real user
1445
- // message — hide it from the transcript like other system-injected turns.
1446
- 'pikiloom-recover',
1447
- ]);
1448
1432
  // The CLI's resume-time repair placeholder for a turn that never concluded (paired with an
1449
1433
  // isMeta "Continue from where you left off." user record). It is not model output — but it IS
1450
1434
  // the only durable marker that the previous reply was cut off before its closing message.
@@ -1455,19 +1439,6 @@ function isClaudeSyntheticResumeNoise(text) {
1455
1439
  return t === 'no response requested.' || t === 'no response requested';
1456
1440
  }
1457
1441
  const CLAUDE_INCOMPLETE_TURN_NOTICE = '⚠️ This reply ended before a closing message was delivered (interrupted, or the model returned an empty final response).';
1458
- function isSystemInjectedUserEvent(text) {
1459
- const trimmed = (text || '').trim();
1460
- if (!trimmed)
1461
- return true;
1462
- if (/^\[Request interrupted by user(?: for tool use)?\]$/i.test(trimmed))
1463
- return true;
1464
- const leading = trimmed.match(/^<([a-z][a-z0-9_-]*)\b/i);
1465
- if (leading && SYSTEM_INJECTED_USER_TAGS.has(leading[1].toLowerCase()))
1466
- return true;
1467
- const lower = trimmed.toLowerCase();
1468
- const markers = ['continued from a previous', 'summary below covers', 'earlier portion of the conversation', 'here is a summary of', 'conversation summary'];
1469
- return markers.some(m => lower.includes(m));
1470
- }
1471
1442
  function getClaudeSessionMessages(opts) {
1472
1443
  const projectDir = path.join(getHome(), '.claude', 'projects', claudeProjectDirName(opts.workdir));
1473
1444
  const filePath = path.join(projectDir, `${opts.sessionId}.jsonl`);
@@ -1600,11 +1571,14 @@ function getClaudeSessionMessages(opts) {
1600
1571
  }
1601
1572
  continue;
1602
1573
  }
1574
+ const probe = extractClaudeText(ev.message?.content, true);
1603
1575
  if (pendingRole === 'assistant') {
1604
- const probe = extractClaudeText(ev.message?.content, true);
1605
- if (isSystemInjectedUserEvent(probe))
1576
+ if (isSystemInjectedUserText(probe))
1606
1577
  continue;
1607
1578
  }
1579
+ else if (probe.trim() && isSystemInjectedUserText(probe)) {
1580
+ continue;
1581
+ }
1608
1582
  flush();
1609
1583
  const rawText = stripInjectedPrompts(extractClaudeText(ev.message?.content, true));
1610
1584
  const userBlocks = extractClaudeBlocks(ev.message?.content, true);
@@ -2005,6 +1979,13 @@ async function fetchClaudeUsageFromOAuth(tokenOverride) {
2005
1979
  // one request. `force` is for explicit user-clicked refreshes only: it bypasses both the TTL and
2006
1980
  // the failure backoff (human-paced clicks cannot stampede the endpoint), with a short floor to
2007
1981
  // absorb double-clicks.
1982
+ //
1983
+ // The 429 budget is PER ACCOUNT and shared with every other Claude Code process on the machine,
1984
+ // so the endpoint can stay rate-limited for long stretches through no fault of our cadence. When
1985
+ // it fails and there is no last-good to serve, degrade to the inference-header probe (the same
1986
+ // channel setup-token accounts use): coarser — 5h/7d only, no Extra spend or per-model weekly —
1987
+ // but reliably available, so the native row never goes blank. A later successful oauth read
1988
+ // replaces it with the full-fidelity snapshot.
2008
1989
  const NATIVE_USAGE_FRESH_TTL_MS = 15_000;
2009
1990
  const NATIVE_USAGE_RETRY_TTL_MS = 60_000;
2010
1991
  const NATIVE_USAGE_FORCE_FLOOR_MS = 3_000;
@@ -2022,20 +2003,31 @@ export function claudeNativeUsage(opts) {
2022
2003
  }
2023
2004
  if (claudeNativeUsageLive.inflight)
2024
2005
  return claudeNativeUsageLive.inflight;
2025
- const p = fetchClaudeUsageFromOAuth()
2026
- .then(fresh => {
2027
- if (fresh) {
2028
- claudeUsageCache.lastGood = fresh;
2006
+ const p = (async () => {
2007
+ let result = await fetchClaudeUsageFromOAuth();
2008
+ const oauthOk = !!result;
2009
+ // Degrade to the header probe when oauth fails and there is no full-fidelity snapshot to
2010
+ // serve — including when last-good is itself a header-probe result (keep it refreshing
2011
+ // through a long 429 stretch instead of freezing on the first degraded read).
2012
+ if (!result && (!claudeUsageCache.lastGood || claudeUsageCache.lastGood.source === 'ratelimit-headers')) {
2013
+ const token = getClaudeOAuthToken();
2014
+ if (token)
2015
+ result = await claudeUsageForToken(token, opts);
2016
+ agentLog(`[usage] native oauth usage read failed; header-probe fallback -> ${result ? result.source : 'null'}`);
2017
+ }
2018
+ if (result) {
2019
+ claudeUsageCache.lastGood = result;
2029
2020
  claudeUsageCache.lastAttemptAt = Date.now();
2030
2021
  claudeNativeUsageLive.at = Date.now();
2031
- claudeNativeUsageLive.failedAt = 0;
2022
+ // A header-probe fallback is a degraded read: keep failedAt so the oauth endpoint is
2023
+ // retried on the normal backoff cadence instead of idling out the full ok-TTL.
2024
+ claudeNativeUsageLive.failedAt = oauthOk ? 0 : Date.now();
2032
2025
  }
2033
2026
  else {
2034
2027
  claudeNativeUsageLive.failedAt = Date.now();
2035
2028
  }
2036
- return fresh ?? claudeUsageCache.lastGood;
2037
- })
2038
- .finally(() => { claudeNativeUsageLive.inflight = null; });
2029
+ return result ?? claudeUsageCache.lastGood;
2030
+ })().finally(() => { claudeNativeUsageLive.inflight = null; });
2039
2031
  claudeNativeUsageLive.inflight = p;
2040
2032
  return p;
2041
2033
  }
@@ -5,7 +5,7 @@ import './drivers/hermes.js';
5
5
  export { IMAGE_EXTS } from './types.js';
6
6
  export { attachAgentImage, attachInlineImage, materializeImage, rewriteAttachmentBlocksForTransport, attachmentUrl, resolveAllowedAttachmentPath, allowedAttachmentRoots, decodeAttachmentPathParam, sessionAttachmentsDir, codexHome, } from './images.js';
7
7
  export { deliverArtifact, readDeliveredArtifacts, deliveredArtifactBlocks, tailDeliveredBlocks, latestDeliveredTaskId, mimeForArtifact, } from './artifacts.js';
8
- export { Q, agentLog, agentWarn, agentError, dedupeStrings, numberOrNull, normalizeStreamPreviewPlan, parseTodoWriteAsPlan, normalizeActivityLine, pushRecentActivity, detectClaudeApiError, isRetryableClaudeApiError, detectClaudeModelError, claudeModelErrorMessage, firstNonEmptyLine, shortValue, normalizeErrorMessage, joinErrorMessages, appendSystemPrompt, mimeForExt, computeContext, buildStreamPreviewMeta, summarizeClaudeToolUse, summarizeClaudeToolResult, previewToolCallInput, previewToolCallResult, roundPercent, toIsoFromEpochSeconds, normalizeUsageStatus, labelFromWindowMinutes, usageWindowFromRateLimit, parseJsonTail, modelFamily, normalizeClaudeModelId, emptyUsage, readTailLines, stripInjectedPrompts, sanitizeSessionUserPreviewText, SESSION_PREVIEW_IMAGE_PLACEHOLDER_RE, CLAUDE_AT_MENTION_IMAGE_RE, extractClaudeAtMentionImagePaths, stripClaudeAtMentionImages, isPendingSessionId, emitSessionIdUpdate, sessionListDisplayTitle, } from './utils.js';
8
+ export { Q, agentLog, agentWarn, agentError, dedupeStrings, numberOrNull, normalizeStreamPreviewPlan, parseTodoWriteAsPlan, normalizeActivityLine, pushRecentActivity, detectClaudeApiError, isRetryableClaudeApiError, detectClaudeModelError, claudeModelErrorMessage, firstNonEmptyLine, shortValue, normalizeErrorMessage, joinErrorMessages, appendSystemPrompt, mimeForExt, computeContext, buildStreamPreviewMeta, summarizeClaudeToolUse, summarizeClaudeToolResult, previewToolCallInput, previewToolCallResult, roundPercent, toIsoFromEpochSeconds, normalizeUsageStatus, labelFromWindowMinutes, usageWindowFromRateLimit, parseJsonTail, modelFamily, normalizeClaudeModelId, emptyUsage, readTailLines, stripInjectedPrompts, sanitizeSessionUserPreviewText, isSystemInjectedUserText, SESSION_PREVIEW_IMAGE_PLACEHOLDER_RE, CLAUDE_AT_MENTION_IMAGE_RE, extractClaudeAtMentionImagePaths, stripClaudeAtMentionImages, isPendingSessionId, emitSessionIdUpdate, sessionListDisplayTitle, } from './utils.js';
9
9
  export { updateSessionMeta, promoteSessionId, recordFork, resolveCanonicalSessionId, getSessionPromotions, listPikiloomSessions, findPikiloomSession, getSessionStoredConfig, ensureManagedSession, findManagedThreadSession, stageSessionFiles, mergeManagedAndNativeSessions, managedRecordToSessionInfo, getSessions, getSessionTail, getSessionMessages, applyTurnWindow, applyTurnFilter, classifySession, deriveUserStatus, exportSession, importSession, deleteAgentSession, isProcessAlive, isRunningSessionStale, reconcileOrphanedRunningSessions, } from './session.js';
10
10
  export { detectAgentBin, listAgents, resolveDefaultAgent, run, doStream, recoverProfileIdForModel, listModels, resolveAgentModels, dropNativeShadowedByProfiles, getUsage, getAgentBoundModelId, setAgentBoundModelId, } from './stream.js';
11
11
  export { registerDriver, getDriver, getDriverCapabilities, allDrivers, allDriverIds, hasDriver, shutdownAllDrivers, } from './driver.js';
@@ -150,7 +150,12 @@ export function composeKernelFinalPresentation(input) {
150
150
  ? 'The turn stopped responding after tool use without producing a reply (the model or provider may have stalled). Send any message to continue.'
151
151
  : input.stopReason === 'truncated'
152
152
  ? 'The reply ended after the last tool call without a closing message (the model returned an empty final response). Send any message to continue.'
153
- : null;
153
+ // 'background' with narration = the hold cap closed the turn while background work was
154
+ // still running (a silent stop from the user's view). With NO narration it is the
155
+ // intentional detached-launch shape, which keeps the friendlier substitution below.
156
+ : (input.stopReason === 'background' && input.bodyText)
157
+ ? 'Background work was still running when this turn closed (hold limit reached). Send any message to check on it or continue.'
158
+ : null;
154
159
  // Empty-text fallback (mirrors the legacy driver): a clean turn with no prose reads
155
160
  // "(no textual response)", not "(no output)" (which the kernel path used to show for every
156
161
  // textless turn — including a background-launch turn that intentionally ends without prose).
@@ -522,6 +522,39 @@ export function stripInjectedPrompts(text) {
522
522
  export const SESSION_PREVIEW_IGNORED_USER_PATTERNS = [
523
523
  /^\[Request interrupted by user(?: for tool use)?\]$/i,
524
524
  ];
525
+ const SYSTEM_INJECTED_USER_TAGS = new Set([
526
+ 'task-notification',
527
+ 'system-reminder',
528
+ 'persisted-output',
529
+ 'local-command-stdout',
530
+ 'local-command-caveat',
531
+ 'local-command-stderr',
532
+ 'ide_opened_file',
533
+ 'ide_diagnostics',
534
+ 'ide_selection',
535
+ 'event',
536
+ 'analysis',
537
+ 'case_id',
538
+ 'tool-use-id',
539
+ 'output-file',
540
+ // Kernel claude driver's in-process truncated-turn recovery prompt (see packages/kernel
541
+ // drivers/claude.ts): injected on the CLI's stdin, so it lands in the jsonl as a real user
542
+ // message — hide it from the transcript like other system-injected turns.
543
+ 'pikiloom-recover',
544
+ ]);
545
+ export function isSystemInjectedUserText(text) {
546
+ const trimmed = (text || '').trim();
547
+ if (!trimmed)
548
+ return true;
549
+ if (/^\[Request interrupted by user(?: for tool use)?\]$/i.test(trimmed))
550
+ return true;
551
+ const leading = trimmed.match(/^<([a-z][a-z0-9_-]*)\b/i);
552
+ if (leading && SYSTEM_INJECTED_USER_TAGS.has(leading[1].toLowerCase()))
553
+ return true;
554
+ const lower = trimmed.toLowerCase();
555
+ const markers = ['continued from a previous', 'summary below covers', 'earlier portion of the conversation', 'here is a summary of', 'conversation summary'];
556
+ return markers.some(m => lower.includes(m));
557
+ }
525
558
  export const SESSION_PREVIEW_IMAGE_PLACEHOLDER_RE = /\[Image:[^\]]+\]/gi;
526
559
  export const SESSION_PREVIEW_FILE_PLACEHOLDER_RE = /\[Attached file:[^\]]+\]/gi;
527
560
  export const CLAUDE_AT_MENTION_IMAGE_RE = /(^|\s)@(\/[^\s@\n]+\.(?:png|jpe?g|gif|webp|svg))(?=\s|$)/gi;
@@ -1,7 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs';
3
3
  import { fmtTokens, fmtUptime, fmtBytes } from './bot.js';
4
- import { getProjectSkillPaths, normalizeClaudeModelId, sessionListDisplayTitle, listAllMcpExtensions, listSkills as listAllSkills, } from '../agent/index.js';
4
+ import { getProjectSkillPaths, normalizeClaudeModelId, sessionListDisplayTitle, listAllMcpExtensions, listSkills as listAllSkills, isSystemInjectedUserText, } from '../agent/index.js';
5
5
  import { getDriver } from '../agent/driver.js';
6
6
  import { accountAgentSupported, listAccounts, getActiveAccountId, getCachedAccountUsage, getAccountsUsageSnapshot, } from '../agent/accounts.js';
7
7
  import { withTimeoutFallback } from '../core/utils.js';
@@ -272,9 +272,11 @@ export function formatSessionsDigestText(data) {
272
272
  export function extractLastSessionTurn(messages) {
273
273
  if (!messages.length)
274
274
  return null;
275
+ // Harness-injected user records (<task-notification>, <system-reminder>, …) can trail the
276
+ // real question in the session file — the preview must anchor on what the user actually asked.
275
277
  let lastUserIndex = -1;
276
278
  for (let i = messages.length - 1; i >= 0; i--) {
277
- if (messages[i].role === 'user') {
279
+ if (messages[i].role === 'user' && !isSystemInjectedUserText(messages[i].text)) {
278
280
  lastUserIndex = i;
279
281
  break;
280
282
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pikiloom",
3
- "version": "0.4.53",
3
+ "version": "0.4.55",
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": {
@@ -33,6 +33,9 @@ export declare function claudeContextWindowFromModel(model: unknown): number | n
33
33
  export declare function claudeEffectiveContextWindow(advertised: number | null): number | null;
34
34
  export declare function handleClaudeEvent(ev: any, s: any, emit: (e: DriverEvent) => void): void;
35
35
  export declare function claudeBgHoldCapMs(): number;
36
+ export declare function claudeBgAgentHoldCapMs(): number;
37
+ export declare function claudeTurnHasAgentBackground(s: any): boolean;
38
+ export declare function claudeBgHoldRecheckMs(): number;
36
39
  export declare function claudeBgSettleQuietMs(): number;
37
40
  export declare function claudeModelStallMs(): number;
38
41
  export declare const CLAUDE_TRUNCATED_RECOVERY_PROMPT: string;
@@ -59,6 +59,8 @@ export class ClaudeDriver {
59
59
  // Dangling-tool-loop tracking: sawToolResult flips on the first tool_result; textSinceToolResult
60
60
  // goes false at every tool_result and true again once the model streams visible text.
61
61
  sawToolResult: false, textSinceToolResult: false,
62
+ // Wall-clock of the last parsed stream event — the hold cap defers while this is fresh.
63
+ lastEventAt: Date.now(),
62
64
  sessionId: null, model: null,
63
65
  stopReason: null, error: null,
64
66
  input: null, output: null, cached: null,
@@ -138,13 +140,29 @@ export class ClaudeDriver {
138
140
  ok: opts.ok ?? !state.error, text: state.text, reasoning: state.reasoning || undefined,
139
141
  error: state.error, stopReason: opts.stopReason ?? state.stopReason, sessionId: state.sessionId, usage: usageOf(),
140
142
  }, opts.kill ?? true);
141
- // Absolute cap while holding for a still-running background task (stopReason marks it as
142
- // "still running in the background" so the empty-text fallback reads right). Idempotent.
143
+ // Cap while holding for a still-running background task (stopReason marks it as
144
+ // "still running in the background" so the terminal presentation reads right). Idempotent
145
+ // the countdown is absolute from the first arm. Sub-agent-backed holds use the longer
146
+ // agent cap, and a cap that fires while events are still flowing defers instead of
147
+ // cutting an actively-working turn mid-generation (the 2026-07-06 "停止不再继续生成":
148
+ // the 10-min cap yanked a live research turn 22s after its last tool_result and the
149
+ // graceful-close leak guard then killed the 4 still-running Explore agents).
143
150
  const armHoldCap = () => {
144
151
  if (holdCapTimer)
145
152
  return;
146
- holdCapTimer = setTimeout(() => { if (!settled)
147
- settleResult({ stopReason: 'background', kill: false }); }, claudeBgHoldCapMs());
153
+ const capMs = claudeTurnHasAgentBackground(state) ? claudeBgAgentHoldCapMs() : claudeBgHoldCapMs();
154
+ const fire = () => {
155
+ holdCapTimer = null;
156
+ if (settled)
157
+ return;
158
+ if (Date.now() - (state.lastEventAt ?? 0) < claudeBgSettleQuietMs()) {
159
+ holdCapTimer = setTimeout(fire, claudeBgHoldRecheckMs());
160
+ unref(holdCapTimer);
161
+ return;
162
+ }
163
+ settleResult({ stopReason: 'background', kill: false });
164
+ };
165
+ holdCapTimer = setTimeout(fire, capMs);
148
166
  unref(holdCapTimer);
149
167
  };
150
168
  // Grace close once all background work is done: settle gracefully (no kill) if Claude stays
@@ -225,6 +243,7 @@ export class ClaudeDriver {
225
243
  catch {
226
244
  continue;
227
245
  }
246
+ state.lastEventAt = Date.now();
228
247
  handleClaudeEvent(ev, state, ctx.emit);
229
248
  const pending = pendingClaudeBackgroundTasks(state);
230
249
  // Any model output means the model is alive and streaming — cancel the post-tool stall watchdog.
@@ -689,6 +708,27 @@ export function claudeBgHoldCapMs() {
689
708
  const raw = Number(process.env.PIKILOOM_CLAUDE_BG_HOLD_MS);
690
709
  return Number.isFinite(raw) && raw > 0 ? raw : CLAUDE_BG_HOLD_CAP_DEFAULT_MS;
691
710
  }
711
+ // Sub-agent-backed background work (Task/Agent tool launches) gets a much longer hold: these
712
+ // are finite model-driven jobs whose results the turn is genuinely waiting on — a research
713
+ // fleet mapping two repos legitimately runs past the 10-minute daemon cap, and capping it
714
+ // there discarded the agents' work and cut the turn mid-flight ("停止不再继续生成").
715
+ // Override with PIKILOOM_CLAUDE_BG_AGENT_HOLD_MS.
716
+ const CLAUDE_BG_AGENT_HOLD_CAP_DEFAULT_MS = 45 * 60_000;
717
+ export function claudeBgAgentHoldCapMs() {
718
+ const raw = Number(process.env.PIKILOOM_CLAUDE_BG_AGENT_HOLD_MS);
719
+ return Number.isFinite(raw) && raw > 0 ? raw : CLAUDE_BG_AGENT_HOLD_CAP_DEFAULT_MS;
720
+ }
721
+ export function claudeTurnHasAgentBackground(s) {
722
+ return !!s?.bgAgentTasks?.size;
723
+ }
724
+ // When the hold cap fires while events are still flowing, defer and re-check on this cadence
725
+ // instead of yanking an actively-working turn (the cap bounds SILENT stuck holds, nothing else).
726
+ // Override with PIKILOOM_CLAUDE_BG_HOLD_RECHECK_MS (tests need sub-second rechecks).
727
+ const CLAUDE_BG_HOLD_RECHECK_DEFAULT_MS = 30_000;
728
+ export function claudeBgHoldRecheckMs() {
729
+ const raw = Number(process.env.PIKILOOM_CLAUDE_BG_HOLD_RECHECK_MS);
730
+ return Number.isFinite(raw) && raw > 0 ? raw : CLAUDE_BG_HOLD_RECHECK_DEFAULT_MS;
731
+ }
692
732
  // Once every KNOWN background task has finished, how long Claude must stay quiet (no further
693
733
  // output at all) before we close a background turn. A completed task's status races AHEAD of the
694
734
  // wake-up turn that reports it — with N parallel agents finishing together, the last agent's
@@ -743,6 +783,15 @@ export function trackClaudeBackgroundTask(ev, s) {
743
783
  const subtype = ev?.subtype;
744
784
  if (subtype !== 'task_started' && subtype !== 'task_updated' && subtype !== 'task_notification')
745
785
  return;
786
+ // Sub-agent-backed background tasks (Task/Agent tool launches) are FINITE model-driven jobs,
787
+ // unlike a detached shell that may daemonize forever — they earn a much longer hold cap
788
+ // (see armHoldCap). The task_started's tool_use_id points at the launching tool call, which
789
+ // for sub-agents lives in s.subAgents.
790
+ if (subtype === 'task_started') {
791
+ const tui = String(ev?.tool_use_id ?? '').trim();
792
+ if (tui && s?.subAgents?.has?.(tui))
793
+ (s.bgAgentTasks ||= new Set()).add(String(ev?.task_id ?? ev?.id ?? tui));
794
+ }
746
795
  const id = String(ev?.task_id ?? ev?.tool_use_id ?? '').trim();
747
796
  if (!id)
748
797
  return;
@@ -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, 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, } 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, 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, } 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 t,j as s}from"./react-vendor-C7Sl8SE7.js";import{c as _t,I as $t,S as Ue,m as Fe,a as R,u as rt,d as Kt,g as zt,l as Wt,e as Yt,j as Xt,f as Gt,s as Vt,Y as Jt}from"./index-dR0w8M7K.js";import{s as Zt,l as er,n as St,m as tr,a as rr,o as nr,d as sr,b as lr,p as or,c as Ce,e as ar,f as bt,g as qe,T as ur,R as cr,U as vt,h as ir,i as It,j as dr,L as fr,I as mr}from"./index-C-9wwAiw.js";import{M as Tt,a as Rt}from"./Modal-BeyxM41U.js";import"./router-DHISdpPk.js";import"./Select-DxYPQ-41.js";import"./DirBrowser-Dk-TadnX.js";import"./markdown-DxQYQFeH.js";import"./ExtensionsTab-wiaVcnfS.js";function gr({snapshot:o}){const[r,f]=t.useState(o.currentIndex??0),[L,I]=t.useState(""),[x,g]=t.useState(!1),[E,k]=t.useState(null);t.useEffect(()=>{f(o.currentIndex??0),I(""),k(null)},[o.promptId,o.currentIndex]);const O=o.questions||[],y=O[r]||null,z=O.length,C=!!(y?.options&&y.options.length),M=C?!!y?.allowFreeform:!0,oe=u=>{u&&(f(c=>c+1),I(""))},ve=async u=>{if(!x){g(!0),k(null);try{const c=await R.interactionSelectOption(o.promptId,u);if(!c.ok){k(c.error||"Failed to submit selection.");return}oe(c.advanced)}catch(c){k(c?.message||"Network error.")}finally{g(!1)}}},ae=async()=>{if(x)return;const u=L.trim();if(!u&&!y?.allowEmpty){k("Please enter a response.");return}g(!0),k(null);try{const c=await R.interactionSubmitText(o.promptId,u);if(!c.ok){k(c.error||"Failed to submit answer.");return}oe(c.advanced)}catch(c){k(c?.message||"Network error.")}finally{g(!1)}},p=async()=>{if(!x){g(!0),k(null);try{const u=await R.interactionSkip(o.promptId);if(!u.ok){k(u.error||"Failed to skip.");return}oe(u.advanced)}catch(u){k(u?.message||"Network error.")}finally{g(!1)}}},ue=async()=>{if(!x){g(!0);try{await R.interactionCancel(o.promptId)}catch{}}},Ie=t.useMemo(()=>{const u=[];return o.hint&&u.push(o.hint),z>1&&u.push(`Question ${r+1} of ${z}`),u.join(" · ")||void 0},[o.hint,r,z]);return s.jsxs(Tt,{open:!0,onClose:ue,wide:C&&(y?.options?.length||0)>3,children:[s.jsx(Rt,{title:o.title||"Pikiloom needs your input",description:Ie,onClose:ue}),y?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:y.header||"Question"}),s.jsx("div",{className:"mt-1 whitespace-pre-wrap text-sm leading-relaxed text-fg",children:y.prompt})]}),C&&s.jsx("div",{className:"grid grid-cols-1 gap-2 sm:grid-cols-2",children:(y.options||[]).map(u=>s.jsxs("button",{type:"button",disabled:x,onClick:()=>ve(u.value||u.label),className:_t("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:u.label}),u.description&&s.jsx("div",{className:"mt-0.5 text-xs leading-snug text-fg-4",children:u.description})]},u.value||u.label))}),M&&s.jsx("div",{children:s.jsx($t,{value:L,onChange:u=>I(u.target.value),onKeyDown:u=>{u.key==="Enter"&&!u.shiftKey&&!x&&(u.preventDefault(),ae())},placeholder:C?"Or type a custom answer…":"Type your answer…",disabled:x,autoFocus:!C})}),E&&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:E}),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(Ue,{})," 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(Fe,{variant:"ghost",size:"sm",onClick:p,disabled:x,children:"Skip"}),M&&s.jsx(Fe,{variant:"primary",size:"sm",onClick:ae,disabled:x||!L.trim()&&!y.allowEmpty,children:"Submit"})]})]})]}):s.jsxs("div",{className:"py-6 text-center text-sm text-fg-5",children:[s.jsx(Ue,{className:"mr-2 inline-block"})," Waiting for the agent…"]})]})}function pr(o){return o.isNull?o.localStreamPending||o.holdsActiveState?"reject-null":"apply":(typeof o.updatedAt=="number"?o.updatedAt:0)<o.lastAppliedUpdatedAt?"reject-stale":"apply"}function hr(o,r){return typeof r!="number"?o:r>o?r:o}function xr(o,r){return!o||!o.length?[]:r.size?o.filter(f=>!r.has(f)):o.slice()}function kr(o,r,f,L){if(!o.size)return o;const I=new Set(r);let x=!1;const g=new Map;for(const[E,k]of o){if(f-k>L||!I.has(E)){x=!0;continue}g.set(E,k)}return x?g:o}const nt=12,yt=160,Sr=96,br=[],vr=[],Ir=[],yr=20,Tr=6e4,le=new Map;function Rr(o,r){return`${o}:${r}`}function jr(o,r){for(le.delete(o),le.set(o,r);le.size>yr;)le.delete(le.keys().next().value)}const qr=t.memo(function({session:r,workdir:f,active:L=!0,onSessionChange:I,initialPendingPrompt:x,initialPendingImageUrls:g,onPendingPromptConsumed:E}){const k=rt(e=>e.locale),O=rt(e=>e.agentStatus?.agents?.find(n=>n.agent===r.agent)??null),y=O?.selectedEffort??null,z=O?.selectedModel??null,C=rt(e=>e.modelLayer),M=r.profileId?C?.profiles.find(e=>e.id===r.profileId)??null:null,oe=M?C?.providers.find(e=>e.id===M.providerId)??null:null,ve=r.profileId===void 0?O?.byokProviderName??null:oe?.name??null,ae=r.profileId===void 0?O?.byokProfileName??null:M&&M.name.trim().toLowerCase()!==M.modelId.trim().toLowerCase()?M.name:null,p=t.useMemo(()=>Kt(k),[k]),ue=zt(r.agent||""),Ie=Wt(r),u=!!x||!!(g&&g.length),[c,He]=t.useState(null),[Be,ce]=t.useState(!u),[ie,st]=t.useState(!1),[i,U]=t.useState(null),[de,fe]=t.useState(!1),[W,De]=t.useState(null),[jt,lt]=t.useState(0),[wt,Qe]=t.useState(null),[Y,ye]=t.useState([]),[Nt,Te]=t.useState([]),[me,_e]=t.useState([]),[S,X]=t.useState(x||null),[P,G]=t.useState(g||[]),[At,V]=t.useState(null),F=t.useRef(null);F.current=At;const ot=t.useRef(S);ot.current=S;const[at,q]=t.useState([]),Re=t.useRef([]);Re.current=at;const ge=t.useRef(null),[Lt,ut]=t.useState(null),[je,pe]=t.useState(null),[he,$e]=t.useState(""),[H,Ke]=t.useState(!1),Ot=!!O?.capabilities?.fork,ze=t.useRef(null),A=t.useRef(g||[]),B=t.useRef(i),We=t.useRef(de);B.current=i,We.current=de;const Ye=t.useRef(Y);Ye.current=Y;const ct=t.useRef(W);ct.current=W;const D=t.useRef(null),we=t.useRef(null),J=t.useRef(!0),Z=t.useRef(!1),Ne=t.useRef(null),Xe=t.useRef(!1),Q=t.useRef(u),xe=t.useRef(!1),ee=t.useRef(!1),Ge=t.useRef(!1),Ve=t.useRef(!1),_=t.useRef(null),Ae=t.useRef(0),ke=t.useRef(new Map),it=t.useRef({model:null,effort:null}),Mt=t.useCallback(e=>{it.current=e},[]);t.useEffect(()=>{Ge.current||!u||(Ge.current=!0,x&&!S&&X(x),g&&g.length&&!P.length&&(G(g),A.current=g),ce(!1),lt(e=>e+1),E?.())},[u,E]);const j=t.useCallback(()=>{X(null),G(e=>{for(const n of e)URL.revokeObjectURL(n);return[]}),A.current=[],V(null)},[]),te=t.useCallback(()=>{q(e=>{if(!e.length)return e;for(const n of e)for(const l of n.imageUrls)URL.revokeObjectURL(l);return[]}),ge.current=null},[]),dt=t.useCallback((e,n)=>{const l=Zt({streaming:We.current,liveStreamPhase:B.current?.phase??null,streamPhase:ct.current,queuedTaskCount:Ye.current.length,pendingQueuedCount:Re.current.length}),a=n||[];if(l){const d=`local-${Date.now().toString(36)}-${Math.random().toString(36).slice(2,8)}`;ge.current=d,q(h=>[...h,{localId:d,taskId:null,prompt:e||"",imageUrls:a}]);return}B.current?.phase==="done"&&U(null);for(const d of A.current)URL.revokeObjectURL(d);ge.current=null,X(e||null),G(a),A.current=a,V(null)},[]),Pt=t.useCallback(e=>{const n=ge.current;if(n){ge.current=null,q(l=>{const a=l.findIndex(h=>h.localId===n);if(a<0)return l;const d=l.slice();return d[a]={...d[a],taskId:e},d});return}V(e)},[]),Et=t.useCallback(async()=>{if(!je)return;const e=he.trim();if(e){Ke(!0);try{const n=await R.forkSession(f,r.agent||"",r.sessionId,je.atTurn,e,{});if(!n.ok||!n.sessionKey){Ke(!1);return}const[l,a]=n.sessionKey.split(":");pe(null),$e(""),I?.({agent:l,sessionId:a,workdir:f})}finally{Ke(!1)}}},[je,he,f,r.agent,r.sessionId,I]);ze.current=Et;const Le=t.useCallback(async(e,n={})=>{try{const l=await er({workdir:f,agent:r.agent||"",sessionId:r.sessionId,rich:!0,turnOffset:e.turnOffset,turnLimit:e.turnLimit,lastNTurns:e.lastNTurns},{force:n.force});return l.ok?St(l):null}catch{return null}},[f,r.agent,r.sessionId]),w=t.useCallback(async({keepOlder:e,force:n=!1,scrollToBottom:l=!1})=>{const a=r.sessionId;if(Ne.current===a)return!1;Ne.current=a;try{const d=await Le({turnOffset:0,turnLimit:nt},{force:n});if(!d||r.sessionId!==a)return!1;if(l&&(Z.current=!0),He(h=>!h||!e?d:tr(h,d)),ce(!1),xe.current&&(xe.current=!1,j()),ee.current){const h=ee.current;ee.current=!1;const ne=h!==!0?h.taskId:null;B.current&&(h===!0||B.current.taskId===ne)&&U(null)}return!0}finally{Ne.current===a&&(Ne.current=null)}},[Le,j,r.sessionId]),Oe=t.useCallback(async()=>{if(!c?.hasOlder||Xe.current)return;const e=D.current;e&&(we.current={scrollHeight:e.scrollHeight,scrollTop:e.scrollTop}),Xe.current=!0,st(!0);try{const n=await Le({turnOffset:Math.max(0,c.totalTurns-c.startTurn),turnLimit:nt});n?He(l=>l?rr(l,n):n):we.current=null}finally{Xe.current=!1,st(!1)}},[Le,c]),Me=t.useRef(null),re=t.useCallback((e,n="ws")=>{if(pr({updatedAt:e?.updatedAt,isNull:!e,lastAppliedUpdatedAt:Ae.current,localStreamPending:Q.current,holdsActiveState:We.current||Ye.current.length>0})!=="apply")return;if(e&&(Ae.current=hr(Ae.current,e.updatedAt)),e?.sessionId&&e.sessionId!==r.sessionId&&(Ve.current=!0,Se.current=`${r.agent}:${e.sessionId}`,I?.({agent:r.agent||"",sessionId:e.sessionId,workdir:f})),!e){const m=Me.current;fe(!1),m==="streaming"?(xe.current=!0,ee.current=!0,w({keepOlder:!0,force:!0,scrollToBottom:J.current})):U(null),Q.current&&m!=="streaming"?w({keepOlder:!0,force:!0}):(m==="done"&&(j(),te()),m!==null&&(Q.current=!1)),Qe(null),De(null),ye([]),Te([]),_e([]),Me.current=null;return}De(e.phase),Qe(e.taskId||null);const a=[];e.taskId&&a.push(e.taskId),Array.isArray(e.queuedTaskIds)&&a.push(...e.queuedTaskIds),ke.current=kr(ke.current,a,Date.now(),Tr);const d=ke.current,h=xr(e.queuedTaskIds,d),ne=(Array.isArray(e.queuedTasks)?e.queuedTasks:[]).filter(m=>!d.has(m.taskId));if(ye(h.length?h:br),Te(ne.length?ne:vr),_e(Array.isArray(e.interactions)&&e.interactions.length?e.interactions:Ir),nr({pendingTaskId:F.current,streamTaskId:e.taskId||null,queuedTaskIds:e.queuedTaskIds})){const m=F.current,T=ot.current||"",v=A.current;q(b=>b.some(se=>se.taskId===m)?b:[...b,{localId:`demote-${m}`,taskId:m,prompt:T,imageUrls:v}]),X(null),G([]),A.current=[],V(null),F.current=null}if(e.phase==="streaming"){if(U({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}),fe(!0),e.taskId&&e.taskId!==F.current){const m=Re.current,T=m.findIndex(v=>v.taskId===e.taskId);if(T>=0){const v=m[T];for(const b of A.current)URL.revokeObjectURL(b);X(v.prompt||null),G(v.imageUrls),A.current=v.imageUrls,V(e.taskId),q(b=>b.filter((se,K)=>K!==T))}}J.current&&(Z.current=!0)}else if(e.phase==="queued")U(null),fe(!1);else if(e.phase==="done"){const m=sr(B.current?.taskId??null,e.taskId||null),T=h.length>0;if(m){fe(!1),U(K=>K?{...K,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}:K);const v=B.current,b=!!v&&lr(v),se=!!e.incomplete&&b&&!T;if(Me.current!=="done"){T||(xe.current=!0),ee.current=se?!1:{taskId:e.taskId||null},w({keepOlder:!0,force:!0,scrollToBottom:J.current});const K=r.agent||"",Bt=r.sessionId,Dt=Se.current;_.current&&clearTimeout(_.current),_.current=setTimeout(()=>{_.current=null,R.getSessionStreamState(K,Bt).then(Qt=>{Se.current===Dt&&Je.current(Qt.state,"seed")}).catch(()=>{})},900)}}T||(Q.current=!1)}const $=new Set;if(e.taskId&&$.add(e.taskId),Array.isArray(e.queuedTaskIds))for(const m of e.queuedTaskIds)$.add(m);q(m=>{let T=!1;const v=[];for(const b of m)if(!b.taskId||$.has(b.taskId))v.push(b);else{for(const se of b.imageUrls)URL.revokeObjectURL(se);T=!0}return T?v:m}),Me.current=e.phase},[j,te,w,r.sessionId,r.agent,I,f]),Je=t.useRef(re);Je.current=re;const ft=t.useCallback(()=>{Q.current=!0,lt(e=>e+1)},[]);t.useEffect(()=>()=>{_.current&&(clearTimeout(_.current),_.current=null)},[]);const Ct=t.useCallback(async e=>{try{ke.current.set(e,Date.now()),await R.recallSessionMessage(e),F.current===e&&j(),q(n=>{let l=!1;const a=[];for(const d of n)if(d.taskId===e){for(const h of d.imageUrls)URL.revokeObjectURL(h);l=!0}else a.push(d);return l?a:n}),ye(n=>n.filter(l=>l!==e)),Te(n=>n.filter(l=>l.taskId!==e)),Qe(n=>n===e?null:n)}catch{}},[j]),qt=t.useCallback(async e=>{const n=Re.current.find(l=>l.taskId===e)||null;if(n&&n.imageUrls.length>0){for(const l of A.current)URL.revokeObjectURL(l);X(n.prompt||null),G(n.imageUrls),A.current=n.imageUrls,V(e),F.current=e,q(l=>l.filter(a=>a.taskId!==e))}try{await R.steerSession(e)}catch{}},[]),Ut=t.useCallback(async()=>{try{await R.stopSession(r.agent||"",r.sessionId)}catch{}},[r.agent,r.sessionId]),Pe=Rr(r.agent||"",r.sessionId);t.useEffect(()=>{if(Ve.current){Ve.current=!1;let d=!1;return w({keepOlder:!0,force:!0}).finally(()=>{d||ce(!1)}),()=>{d=!0}}let e=!1;const n=or({workdir:f,agent:r.agent||"",sessionId:r.sessionId,rich:!0,turnOffset:0,turnLimit:nt},{allowStale:!0}),l=u&&!Ge.current,a=n?.ok?St(n):le.get(Pe)||null;return ce(l?!1:!a),He(a),U(null),fe(!1),De(null),ye([]),Te([]),_e([]),Ae.current=0,ke.current=new Map,l||(j(),te(),Q.current=!1,xe.current=!1,ee.current=!1),J.current=!0,Z.current=!0,l||w({keepOlder:!1,force:!0}).finally(()=>{e||ce(!1)}),()=>{e=!0}},[w,r.agent,r.sessionId,f,Pe,j,te]),t.useEffect(()=>{c&&c.turns.length>0&&jr(Pe,c)},[Pe,c]),t.useEffect(()=>{L&&w({keepOlder:!0,force:!0})},[L,w]);const Se=t.useRef(`${r.agent}:${r.sessionId}`);Se.current=`${r.agent}:${r.sessionId}`,Yt("stream-update",t.useCallback(e=>{e.key===Se.current&&re(e.snapshot??null)},[re])),t.useEffect(()=>{let e=!0;return R.getSessionStreamState(r.agent||"",r.sessionId).then(n=>{e&&Je.current(n.state,"seed")}).catch(()=>{}),()=>{e=!1}},[r.agent,r.sessionId,jt]),Xt(t.useCallback(()=>{R.getSessionStreamState(r.agent||"",r.sessionId).then(e=>{re(e.state,"seed")}).catch(()=>{}),w({keepOlder:!0,force:!0})},[re,r.agent,r.sessionId,w])),t.useEffect(()=>{!Q.current&&Ie!=="running"&&!de&&!i&&!W&&Y.length===0&&(j(),te())},[Ie,de,i,W,Y.length,j,te]),t.useLayoutEffect(()=>{const e=we.current,n=D.current;!e||!n||(we.current=null,n.scrollTop=e.scrollTop+(n.scrollHeight-e.scrollHeight))},[c?.turns.length]),t.useLayoutEffect(()=>{if(!Z.current)return;const e=D.current;e&&(Z.current=!1,e.scrollTop=e.scrollHeight,requestAnimationFrame(()=>{J.current&&(e.scrollTop=e.scrollHeight)}))},[c,i]),t.useLayoutEffect(()=>{if(!S)return;const e=D.current;e&&(e.scrollTop=e.scrollHeight)},[S]),t.useEffect(()=>{if(!c?.hasOlder||Be||ie)return;const e=D.current;e&&e.scrollHeight<=e.clientHeight+yt&&Oe()},[c?.hasOlder,c?.turns.length,Oe,Be,ie]);const Ft=t.useCallback(()=>{const e=D.current;if(!e)return;const n=e.scrollHeight-e.scrollTop-e.clientHeight;J.current=n<=Sr,e.scrollTop<=yt&&Oe()},[Oe]),be=i?.model||r.model||z||null,Ze=Gt(r.agent||"",i?.effort||r.thinkingEffort||y||null,r.workflowEnabled??O?.workflowEnabled)||null,mt=ae&&(!be||be===z)?ae:be?Vt(be):null,et=Jt(r,{streaming:de,hasLiveStream:!!i,streamPhase:W,queuedTaskCount:Y.length}),N=c?.turns||[],tt=t.useMemo(()=>{if(!P.length||!N.length)return!1;const e=N[N.length-1];return!e.user||!Ce(e.user.text,S)?!1:e.user.blocks.filter(l=>l.type==="image").length<P.length},[N,S,P.length]),gt=t.useMemo(()=>P.length?P.map(e=>({type:"image",content:e})):!S||!i?.questionBlocks?.length?[]:Ce(S,i.question)?i.questionBlocks:[],[P,S,i]),pt=i?.question||null,Ee=ar(S,pt),ht=bt(pt,S),xt=N.length>0?N[N.length-1]?.user?.text:null,Ht=!!Ee&&N.length>0&&qe(xt,Ee),kt=t.useMemo(()=>{let e=N;if(tt){const $=e[e.length-1];e=[...e.slice(0,-1),{...$,user:null}]}if(!i||!e.length)return e;const n=e[e.length-1],l=Ee;if(!n.assistant)return!!n.user&&!!l&&!Ce(n.user.text,l)&&(qe(n.user.text,l)||bt(l,n.user.text))?[...e.slice(0,-1),{...n,user:{...n.user,text:l}}]:e;const a=(i.text||"").trim(),d=n.assistant.text?.trim()||"";if(!(l!=null?qe(n.user?.text,l):!!d&&!!a&&(a.startsWith(d)||d.startsWith(a))))return e;const ne=n.user&&l&&!Ce(n.user.text,l)?{...n.user,text:l}:n.user;return[...e.slice(0,-1),{...n,user:ne,assistant:null}]},[N,i,Ee,tt]);return s.jsxs("div",{className:"flex flex-col h-full overflow-hidden",children:[s.jsx("div",{ref:D,onScroll:Ft,className:"flex-1 overflow-y-auto overscroll-contain",children:Be&&!S&&!P.length&&!i?s.jsx("div",{className:"flex items-center justify-center py-20",children:s.jsx(Ue,{className:"h-5 w-5 text-fg-4"})}):kt.length===0&&!S&&!P.length&&!i&&!et?s.jsx("div",{className:"py-20 text-center text-[13px] text-fg-5",children:p("hub.noMessages")}):s.jsxs("div",{className:"max-w-[900px] mx-auto px-6 py-6 space-y-0",children:[(c?.hasOlder||ie)&&s.jsxs("div",{className:"mb-4 flex items-center justify-center gap-2 text-[11px] text-fg-5",children:[ie?s.jsx(Ue,{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:p(ie?"hub.loadingOlderTurns":"hub.loadOlderTurnsHint")})]}),r.migratedFrom?.kind==="fork"&&r.migratedFrom.sessionId&&s.jsxs("button",{type:"button",onClick:()=>I?.({agent:r.migratedFrom.agent||r.agent||"",sessionId:r.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:`#${r.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:p("hub.forkBadge")}),s.jsxs("span",{className:"font-mono",children:["#",r.migratedFrom.sessionId.slice(0,8)]}),typeof r.migratedFrom.forkedAtTurn=="number"&&s.jsxs("span",{className:"text-fg-5/70",children:["· ",p("hub.forkBadgeAt").replace("{turn}",String(r.migratedFrom.forkedAtTurn+1))]})]}),kt.map((e,n)=>{const l=(c?.startTurn||0)+n;return s.jsx(ur,{turn:e,turnIndex:l,agent:r.agent||"",meta:ue,model:mt,effort:Ze,providerName:ve,t:p,workdir:f,onResend:a=>{Z.current=!0,dt(a);const d=it.current;R.sendSessionMessage(f,r.agent||"",r.sessionId,a,{model:d.model||be||void 0,effort:d.effort||Ze||void 0}).then(h=>{h.ok&&ft()}).catch(()=>{j()})},onEdit:a=>ut(a),onFork:Ot?a=>{$e(""),pe({atTurn:a})}:void 0},`${c?.startTurn||0}:${n}`)}),et&&s.jsx("div",{className:"mb-5 animate-in",children:s.jsx(cr,{detail:et,t:p})}),(S||gt.length>0)&&!ht&&(tt||!Ht)&&s.jsxs("div",{className:"session-turn",children:[s.jsx(vt,{text:S||"",blocks:gt,t:p}),!i&&s.jsx("div",{className:"mt-3 mb-5 animate-in",children:s.jsx(ir,{className:"text-fg-5"})})]}),i&&It(i)&&i.question&&(!S||ht)&&!(N.length>0&&qe(xt,i.question))&&s.jsx("div",{className:"session-turn",children:s.jsx(vt,{text:i.question,blocks:i.questionBlocks||void 0,t:p})}),i&&It(i)&&s.jsxs("div",{className:"mb-6",children:[s.jsx(dr,{agent:r.agent||"",meta:ue,model:mt,effort:Ze,providerName:ve,previewMeta:i.previewMeta,hideContextUsage:!0}),s.jsx(fr,{stream:i,t:p,workdir:f})]}),s.jsx("div",{className:"h-4"})]})}),s.jsx(mr,{session:r,workdir:f,onStreamQueued:ft,onSendStart:dt,onSendTaskAssigned:Pt,onSessionChange:I,t:p,streamPhase:W,streamTaskId:wt,queuedTaskIds:Y,queuedTasks:Nt,pendingQueuedSends:at,onRecall:Ct,onSteer:qt,onStopAll:Ut,editDraft:Lt,onEditDraftConsumed:()=>ut(null),onSelectionChange:Mt}),je&&s.jsxs(Tt,{open:!0,onClose:()=>{H||pe(null)},children:[s.jsx(Rt,{title:p("hub.forkPromptTitle"),description:p("hub.forkPromptHint"),onClose:()=>{H||pe(null)}}),s.jsx("textarea",{autoFocus:!0,value:he,disabled:H,onChange:e=>$e(e.target.value),onKeyDown:e=>{e.key==="Enter"&&(e.metaKey||e.ctrlKey)&&he.trim()&&!H&&(e.preventDefault(),ze.current?.())},placeholder:p("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(Fe,{variant:"ghost",disabled:H,onClick:()=>pe(null),children:p("modal.cancel")}),s.jsx(Fe,{variant:"primary",disabled:H||!he.trim(),onClick:()=>{ze.current?.()},children:p(H?"hub.forkSubmitting":"hub.forkSubmit")})]})]}),L&&me.length>0&&s.jsx(gr,{snapshot:me[me.length-1]},me[me.length-1].promptId)]})});export{qr as SessionPanel};