zidane 6.2.12 → 6.2.14

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 (77) hide show
  1. package/dist/{acp-C-auiVTk.js → acp-DxYjSCQW.js} +5 -5
  2. package/dist/{acp-C-auiVTk.js.map → acp-DxYjSCQW.js.map} +1 -1
  3. package/dist/acp-cli.js +6 -6
  4. package/dist/acp.js +1 -1
  5. package/dist/{agent-DhfdXgL2.js → agent-DoEx_WD3.js} +6 -5
  6. package/dist/{agent-DhfdXgL2.js.map → agent-DoEx_WD3.js.map} +1 -1
  7. package/dist/agent.js +2 -2
  8. package/dist/aliasing-lVFbm81n.js +217 -0
  9. package/dist/aliasing-lVFbm81n.js.map +1 -0
  10. package/dist/{anthropic-uXfsormU.js → anthropic-BYsc6qWj.js} +3 -3
  11. package/dist/{anthropic-uXfsormU.js.map → anthropic-BYsc6qWj.js.map} +1 -1
  12. package/dist/{auth-TZg3MVi2.js → auth-Bb479DFB.js} +4 -4
  13. package/dist/{auth-TZg3MVi2.js.map → auth-Bb479DFB.js.map} +1 -1
  14. package/dist/chat/pure.d.ts +3 -3
  15. package/dist/chat/pure.js +3 -2
  16. package/dist/chat.d.ts +3 -3
  17. package/dist/chat.js +6 -5
  18. package/dist/chat.js.map +1 -1
  19. package/dist/eval.js +1 -1
  20. package/dist/extensions.js +1 -1
  21. package/dist/headless.js +3 -3
  22. package/dist/history-search-C4bk1Wz_.js +486 -0
  23. package/dist/history-search-C4bk1Wz_.js.map +1 -0
  24. package/dist/history-search-worker.d.ts +1 -0
  25. package/dist/history-search-worker.js +86 -0
  26. package/dist/history-search-worker.js.map +1 -0
  27. package/dist/index-l0wehkWU.d.ts.map +1 -1
  28. package/dist/index.js +7 -7
  29. package/dist/{messages-BDpXLFoe.js → messages-CvHfYB7f.js} +3 -3
  30. package/dist/{messages-BDpXLFoe.js.map → messages-CvHfYB7f.js.map} +1 -1
  31. package/dist/{openai-compat-J_JZ7qsw.js → openai-compat-pHI0kBWH.js} +37 -8
  32. package/dist/openai-compat-pHI0kBWH.js.map +1 -0
  33. package/dist/presets.js +1 -1
  34. package/dist/providers/anthropic.js +1 -1
  35. package/dist/providers/arcee.js +1 -1
  36. package/dist/providers/baseten.js +1 -1
  37. package/dist/providers/cerebras.js +1 -1
  38. package/dist/providers/local.js +1 -1
  39. package/dist/providers/openai-compat.js +1 -1
  40. package/dist/providers/openai.js +27 -10
  41. package/dist/providers/openai.js.map +1 -1
  42. package/dist/providers/openrouter.js +1 -1
  43. package/dist/providers/xai.js +1 -1
  44. package/dist/{providers-CclOzGxc.js → providers-D9Y2RndM.js} +3 -3
  45. package/dist/{providers-CclOzGxc.js.map → providers-D9Y2RndM.js.map} +1 -1
  46. package/dist/providers.js +4 -4
  47. package/dist/{read-state-Dq_TXUX1.js → read-state-rDmfeLmz.js} +3 -196
  48. package/dist/read-state-rDmfeLmz.js.map +1 -0
  49. package/dist/restate.js +2 -1
  50. package/dist/restate.js.map +1 -1
  51. package/dist/{session-CIXE-Bzp.js → session-DzrXKLw-.js} +2 -2
  52. package/dist/{session-CIXE-Bzp.js.map → session-DzrXKLw-.js.map} +1 -1
  53. package/dist/session.js +2 -2
  54. package/dist/{tool-formatters-BSwVkSX2.d.ts → tool-formatters-DdgS-xE2.d.ts} +90 -4
  55. package/dist/tool-formatters-DdgS-xE2.d.ts.map +1 -0
  56. package/dist/tools.js +2 -2
  57. package/dist/{transcript-anchors-BzcuKnq8.js → transcript-anchors-DE6D0dNo.js} +4 -4
  58. package/dist/transcript-anchors-DE6D0dNo.js.map +1 -0
  59. package/dist/transcript-anchors-DKjXpQ0h.d.ts.map +1 -1
  60. package/dist/tui.d.ts +52 -10
  61. package/dist/tui.d.ts.map +1 -1
  62. package/dist/tui.js +1344 -249
  63. package/dist/tui.js.map +1 -1
  64. package/dist/{turn-operations-CXjRaltC.js → turn-operations-CokV32wf.js} +82 -11
  65. package/dist/turn-operations-CokV32wf.js.map +1 -0
  66. package/dist/{turn-operations-CjkgQKmR.d.ts → turn-operations-sE-Sli9H.d.ts} +2 -2
  67. package/dist/{turn-operations-CjkgQKmR.d.ts.map → turn-operations-sE-Sli9H.d.ts.map} +1 -1
  68. package/dist/{xai-D-wALLuZ.js → xai-BwhzoehX.js} +2 -2
  69. package/dist/{xai-D-wALLuZ.js.map → xai-BwhzoehX.js.map} +1 -1
  70. package/package.json +1 -1
  71. package/dist/hash-DJRzmcPI.js +0 -24
  72. package/dist/hash-DJRzmcPI.js.map +0 -1
  73. package/dist/openai-compat-J_JZ7qsw.js.map +0 -1
  74. package/dist/read-state-Dq_TXUX1.js.map +0 -1
  75. package/dist/tool-formatters-BSwVkSX2.d.ts.map +0 -1
  76. package/dist/transcript-anchors-BzcuKnq8.js.map +0 -1
  77. package/dist/turn-operations-CXjRaltC.js.map +0 -1
package/dist/tui.js CHANGED
@@ -1,5 +1,5 @@
1
- import { $ as useMcpAuthState, $n as buildPlanSystem, $t as restoreChainFallbacks, A as runOAuthLogin, Bt as createDiscoverySlot, Ct as SETTINGS_CHOICES, D as formatPathForCwd, Dt as clampFps, Et as SettingsProvider, G as buildVisibleMcpRows, Gt as useConfig, Ht as useDiscovery, J as createFileMcpCredentialStore, Jt as buildRunModel, K as indexOfServerRow, Kt as resolveConfig, L as loadMcpToolsCache, M as buildMcpServers, Mt as resolveTheme, O as fetchOAuthRedirect, Ot as useSettings, P as discoverProjectMcps, Q as useMcpAuthDispatch, Qn as buildBuildSystem, Qt as providerKeySetChanged, R as mcpToolsCachePath, St as SETTINGS_CATEGORIES, T as suggestSafelistEntries, Tn as accentColor, Tt as SETTINGS_TOGGLES, U as useMcpToolToggleMap, Ut as useDiscoveryOptional, V as subscribeMcpToolsCache, Vn as useActiveTodos, Vt as DiscoveryProvider, Wt as ConfigProvider, Xt as chainsEqual, Yt as chainPositionOf, Z as McpAuthProvider, Zt as filterChainFallbacks, _ as useSafeModeQueue, _n as discoverAgentsMd, _r as toolCallPreview, _t as buildHints, a as useSurfaces, an as bootTick, at as buildResumedToolResultsTurn, b as getSafelist, br as usageByModel, bt as useEnabledToggleSet, c as useStreamBuffer, cr as lastContextSizeFromTurns, ct as makeRequestInteraction, d as discoverProjectSkills, dr as marginTopFor, dt as useInteractionsActions, er as envSection, et as getMcpAuthStatus, ft as useInteractionsQueue, g as useSafeModeActions, gn as chatAutoCompactBehavior, gt as truncateTrailing, h as SafeModeProvider, hr as sumRunCosts, ht as hintsLength, i as useSelectStyle, ir as compactSummaryEvent, it as PRESENT_PLAN_TOOL, j as supportsOAuth, jn as TODO_STATUS_GLYPHS, jt as resolveChipColor, k as oauthUsesManualCodePaste, l as buildSkillsConfig, lr as listSessionMeta, lt as pendingInteractionsFromTurns, m as writeSessionExport, mr as stripSpawnTokensLine, mt as clipHintsToWidth, n as ThemeProvider, nn as useCompletion, nr as createChatTurnsReadTool, nt as ASK_USER_TOOL, on as buildUpdateHint, or as deriveSessionTitle, ot as createInteractionTools, pr as serverToolResultSummary, pt as EMPTY_HINTS, q as parentServerName, qt as resolveStoragePaths, r as useColors, rn as tryOpenBrowser, rt as InteractionsProvider, s as useTheme, sn as useUpdateCheck, sr as eventsFromTurns, t as computeTurnAnchors, tn as toggleChainEntry, tr as chatReferenceTokensForPrompt, u as defaultSkillScanPaths, ur as loadState, vr as toolResultText, wt as SETTINGS_INPUTS, x as isOnSafelist, xt as DEFAULT_SETTINGS, y as addToSafelist, yr as updateToolEventOutcomes, yt as listProjectFiles, z as refreshMcpToolsCatalog } from "./transcript-anchors-BzcuKnq8.js";
2
- import { $ as composeExtensionPreset, B as ExtensionInstallError, Bt as ensureKeybindingsFile, Et as createModelSearchTool, H as installExtensionFromSpec, Ht as groupBindings, J as createExtensionHud, K as removeInstalledExtension, P as summaryToTurn, St as tailTruncate, Ut as keybindingsPath, Vt as formatBindingForDisplay, Wt as matchesBinding, X as discoverAndBuildExtensionRegistry, Y as registerExtensionHostModules, Yt as resolveExtensionKeybindings, _ as buildPostCompactAttachments, c as applyCompactSummaryCutoff, dn as sanitizeExtensionConfigActionPatch, en as inlineLoadedExtension, et as disposeExtensionRegistry, f as cleanupPersistedSession, g as resolveTasksDir, h as resolvePersistDir, hn as effectiveContextWindow, ht as isSpawnTool, it as createExtensionHostRuntimeBridge, lt as basic_default, m as resolveMcpWarningsDir, mn as OUTPUT_RESERVE_TOKENS, nn as parseExtensionScanPathEnv, on as coerceExtensionConfigInput, q as updateInstalledExtension, rn as scanExtensionDirectories, rt as partitionExtensionCatalog, t as createAgent, tt as filterExtensionRegistry, un as resolveExtensionConfigValue, wt as looksBinary, x as compactConversation, y as selectFilesFromSession, z as createExtensionPromptBridge, zt as KEYBINDING_KEY_COL_WIDTH } from "./agent-DhfdXgL2.js";
1
+ import { $ as useMcpAuthState, $n as buildPlanSystem, $t as restoreChainFallbacks, A as runOAuthLogin, Bt as createDiscoverySlot, Ct as SETTINGS_CHOICES, D as formatPathForCwd, Dt as clampFps, Et as SettingsProvider, G as buildVisibleMcpRows, Gt as useConfig, Ht as useDiscovery, J as createFileMcpCredentialStore, Jt as buildRunModel, K as indexOfServerRow, Kt as resolveConfig, L as loadMcpToolsCache, M as buildMcpServers, Mt as resolveTheme, O as fetchOAuthRedirect, Ot as useSettings, P as discoverProjectMcps, Q as useMcpAuthDispatch, Qn as buildBuildSystem, Qt as providerKeySetChanged, R as mcpToolsCachePath, St as SETTINGS_CATEGORIES, T as suggestSafelistEntries, Tn as accentColor, Tt as SETTINGS_TOGGLES, U as useMcpToolToggleMap, Ut as useDiscoveryOptional, V as subscribeMcpToolsCache, Vn as useActiveTodos, Vt as DiscoveryProvider, Wt as ConfigProvider, Xt as chainsEqual, Yt as chainPositionOf, Z as McpAuthProvider, Zt as filterChainFallbacks, _ as useSafeModeQueue, _n as discoverAgentsMd, _r as toolCallPreview, _t as buildHints, a as useSurfaces, an as bootTick, at as buildResumedToolResultsTurn, b as getSafelist, br as usageByModel, bt as useEnabledToggleSet, c as useStreamBuffer, cr as lastContextSizeFromTurns, ct as makeRequestInteraction, d as discoverProjectSkills, dr as marginTopFor, dt as useInteractionsActions, er as envSection, et as getMcpAuthStatus, ft as useInteractionsQueue, g as useSafeModeActions, gn as chatAutoCompactBehavior, gt as truncateTrailing, h as SafeModeProvider, hr as sumRunCosts, ht as hintsLength, i as useSelectStyle, ir as compactSummaryEvent, it as PRESENT_PLAN_TOOL, j as supportsOAuth, jn as TODO_STATUS_GLYPHS, jt as resolveChipColor, k as oauthUsesManualCodePaste, l as buildSkillsConfig, lr as listSessionMeta, lt as pendingInteractionsFromTurns, m as writeSessionExport, mr as stripSpawnTokensLine, mt as clipHintsToWidth, n as ThemeProvider, nn as useCompletion, nr as createChatTurnsReadTool, nt as ASK_USER_TOOL, on as buildUpdateHint, or as deriveSessionTitle, ot as createInteractionTools, pr as serverToolResultSummary, pt as EMPTY_HINTS, q as parentServerName, qt as resolveStoragePaths, r as useColors, rn as tryOpenBrowser, rt as InteractionsProvider, s as useTheme, sn as useUpdateCheck, sr as eventsFromTurns, t as computeTurnAnchors, tn as toggleChainEntry, tr as chatReferenceTokensForPrompt, u as defaultSkillScanPaths, ur as loadState, vr as toolResultText, wt as SETTINGS_INPUTS, x as isOnSafelist, xt as DEFAULT_SETTINGS, y as addToSafelist, yr as updateToolEventOutcomes, yt as listProjectFiles, z as refreshMcpToolsCatalog } from "./transcript-anchors-DE6D0dNo.js";
2
+ import { $ as composeExtensionPreset, B as ExtensionInstallError, Bt as ensureKeybindingsFile, Et as createModelSearchTool, H as installExtensionFromSpec, Ht as groupBindings, J as createExtensionHud, K as removeInstalledExtension, P as summaryToTurn, St as tailTruncate, Ut as keybindingsPath, Vt as formatBindingForDisplay, Wt as matchesBinding, X as discoverAndBuildExtensionRegistry, Y as registerExtensionHostModules, Yt as resolveExtensionKeybindings, _ as buildPostCompactAttachments, c as applyCompactSummaryCutoff, dn as sanitizeExtensionConfigActionPatch, en as inlineLoadedExtension, et as disposeExtensionRegistry, f as cleanupPersistedSession, g as resolveTasksDir, h as resolvePersistDir, hn as effectiveContextWindow, ht as isSpawnTool, it as createExtensionHostRuntimeBridge, lt as basic_default, m as resolveMcpWarningsDir, mn as OUTPUT_RESERVE_TOKENS, nn as parseExtensionScanPathEnv, on as coerceExtensionConfigInput, q as updateInstalledExtension, rn as scanExtensionDirectories, rt as partitionExtensionCatalog, t as createAgent, tt as filterExtensionRegistry, un as resolveExtensionConfigValue, wt as looksBinary, x as compactConversation, y as selectFilesFromSession, z as createExtensionPromptBridge, zt as KEYBINDING_KEY_COL_WIDTH } from "./agent-DoEx_WD3.js";
3
3
  import { a as formatDuration, c as previewLine, i as fmtTokens, l as shortId, n as compactPath, o as formatTaskStatus, r as fmtCost, s as formatTaskSummary, t as ageString } from "./format-BF745pLL.js";
4
4
  import { n as appendCapped } from "./output-cap-kBUHBlnq.js";
5
5
  import { i as killProcessGroup, n as SUPPORTS_PROCESS_GROUPS, r as createProcessContext } from "./contexts-A4ep-iJs.js";
@@ -9,17 +9,18 @@ import { a as createRingBufferSink, i as createLoggingHooks, l as logFileTimesta
9
9
  import { i as sniffImageMediaType } from "./media-sniff-Bn76JxAu.js";
10
10
  import { n as flattenTurns, r as formatTokenUsage, t as effectiveInputFromTurn } from "./stats-DAKBEKjc.js";
11
11
  import { replaceDynamicSection } from "./prompt.js";
12
- import { a as getExtensionSessionState, i as demoteStaleRunningStatus, o as loadSession, s as setExtensionSessionState, t as createSession } from "./session-CIXE-Bzp.js";
12
+ import { a as getExtensionSessionState, i as demoteStaleRunningStatus, o as loadSession, s as setExtensionSessionState, t as createSession } from "./session-DzrXKLw-.js";
13
13
  import { o as withTimeout } from "./timeout-BDetbuN6.js";
14
14
  import { n as connectMcpServers, u as McpOAuthProvider } from "./mcp-BCaN_Gyg.js";
15
15
  import { a as generateStructuredData, c as shouldAutoGenerateTitle, f as commandBlockToEvent, g as parseCommandInvocation, h as executeCommand, n as patchInlineGhost, p as commandBlocksToContent, r as resolveInlineSuggestion, s as generateSessionTitle, t as createPathAutocompleteProvider } from "./inline-autocomplete-BAwBcZtD.js";
16
16
  import { s as resolveCompletionSubmit } from "./completion-core-BRo86R_L.js";
17
17
  import { i as installSkillFromSpec, r as SkillInstallError } from "./skills-DKWqoaYH.js";
18
18
  import { i as ZIDANE_EXTENSION_SKILL_NAME, l as withZidaneExtensionSkill, o as extensionDirForCwd, r as EXTENSION_EDIT_METADATA_KEY, s as extensionScaffold, t as loginMcpServer } from "./login-Clf-R27F.js";
19
- import { C as restoreModelOptions, S as piIdOf, _ as modelOptionsFor, m as getContextWindow, p as enabledModelOptions, s as setProviderCredential, t as detectAuth, v as modelSupportsReasoning } from "./auth-TZg3MVi2.js";
19
+ import { C as restoreModelOptions, S as piIdOf, _ as modelOptionsFor, m as getContextWindow, p as enabledModelOptions, s as setProviderCredential, t as detectAuth, v as modelSupportsReasoning } from "./auth-Bb479DFB.js";
20
20
  import { createDockerContext } from "./contexts/docker.js";
21
21
  import { createTuiStore } from "./session/sqlite.js";
22
- import { $ as buildEditOutcomesAnnotation, A as blendHsl, B as turnSelectionOwnership, C as stripHtmlCommentsOutsideCode, E as uniqueSkillNamesFromReferences, F as isTurnHighlighted, H as buildContextualDiff, I as isUserBashEvent, J as filetypeFromPath, L as isVisible, M as BASH_CALL_ID_PREFIX, N as EDIT_TOOL_NAMES, O as createFilesCompletionProvider, P as isEditErrorResult, R as promptHistoryFromEvents, S as splitMarkdownCodeBlocks, T as createSkillsCompletionProvider, U as buildUnifiedDiff, W as clipDiffForRender, Y as previewEditPayload, Z as summarizeEditPayload, _ as filterModelCatalog, a as TOOL_DISPLAY, at as stripEditOutcomesAnnotation, b as chunkMarkdownForRender, d as finalizeStreamingMarkdown, f as finalizeStreamingMarkdownForOwner, g as buildModelCatalog, h as splitPromptSegments, i as turnAsText, it as rewriteMultiEditHeader, j as buildLinearRamp, lt as createChatsCompletionProvider, m as turnContextSize, n as deleteTurnSafely, nt as parseEditOutcomesFromResult, o as displayNameFor, ot as summarizeOutcomes, q as extractEditPayload, r as truncateTurnsAt, rt as resolveApprovalForPayload, s as formatToolCall, tt as mergeApprovalAndBodyOutcomes, u as capTranscriptEvents, v as indexOfEntry, x as normalizeMarkdownCodeFences, z as selectableTurnIds } from "./turn-operations-CXjRaltC.js";
22
+ import { $ as extractEditPayload, B as isVisible, C as chunkMarkdownForRender, E as stripHtmlCommentsOutsideCode, F as BASH_CALL_ID_PREFIX, G as streamEventSearchText, H as promptHistoryFromEvents, I as EDIT_TOOL_NAMES, J as buildContextualDiff, K as turnSelectionOwnership, L as isEditErrorResult, N as blendHsl, O as createSkillsCompletionProvider, P as buildLinearRamp, R as isTurnHighlighted, T as splitMarkdownCodeBlocks, U as selectableTurnIds, V as isVisibleDuringSearch, W as selectionAfterTurnRemoval, X as clipDiffForRender, Y as buildUnifiedDiff, _ as turnContextSize, a as TOOL_DISPLAY, at as buildEditOutcomesAnnotation, b as filterModelCatalog, ct as parseEditOutcomesFromResult, d as finalizeStreamingMarkdown, dt as stripEditOutcomesAnnotation, et as filetypeFromPath, f as finalizeStreamingMarkdownForOwner, ft as summarizeOutcomes, g as revealEarlierTranscriptEvents, h as replaceTranscriptTurnEvents, ht as createChatsCompletionProvider, i as turnAsText, j as createFilesCompletionProvider, k as uniqueSkillNamesFromReferences, lt as resolveApprovalForPayload, n as deleteTurnSafely, o as displayNameFor, p as hiddenEarlierTranscriptCount, r as truncateTurnsAt, rt as summarizeEditPayload, s as formatToolCall, st as mergeApprovalAndBodyOutcomes, tt as previewEditPayload, u as capTranscriptEvents, ut as rewriteMultiEditHeader, v as splitPromptSegments, w as normalizeMarkdownCodeFences, y as buildModelCatalog, z as isUserBashEvent } from "./turn-operations-CokV32wf.js";
23
+ import { a as searchHighlightRanges, c as searchSessionHistoryIndexAsync, i as highlightSegments, n as buildSessionSearchIndexAsync, r as filterSessionSearchCatalog, u as searchSessionIndexAsync } from "./history-search-C4bk1Wz_.js";
23
24
  import { basename, dirname, join, relative, resolve } from "node:path";
24
25
  import { Buffer } from "node:buffer";
25
26
  import * as fs from "node:fs";
@@ -1505,15 +1506,16 @@ const BASH_RESULT_MAX_LINES = 10;
1505
1506
  * same-turn events read as one continuous highlighted block instead of a
1506
1507
  * striped list.
1507
1508
  */
1508
- const EventLine = memo(({ event, previous, depthOffset = 0, selected = false, anchorId, hideChildLabel = false, settleCodeEpoch = 0, liveSpawn }) => {
1509
+ const EventLine = memo(({ event, previous, depthOffset = 0, selected = false, anchorId, hideChildLabel = false, settleCodeEpoch = 0, liveSpawn, searchQuery, searchTitleMatch = false }) => {
1509
1510
  const SURFACE = useSurfaces();
1510
1511
  const gap = marginTopFor(event, previous);
1512
+ const highlighted = selected;
1511
1513
  return /* @__PURE__ */ jsx$1("box", {
1512
1514
  id: anchorId,
1513
1515
  style: {
1514
- marginTop: selected ? 0 : gap,
1515
- paddingTop: selected ? gap : 0,
1516
- backgroundColor: selected ? SURFACE.selection : void 0,
1516
+ marginTop: 0,
1517
+ paddingTop: gap,
1518
+ backgroundColor: highlighted ? SURFACE.selection : SURFACE.background,
1517
1519
  alignSelf: "stretch",
1518
1520
  flexShrink: 0,
1519
1521
  flexDirection: "column"
@@ -1523,9 +1525,11 @@ const EventLine = memo(({ event, previous, depthOffset = 0, selected = false, an
1523
1525
  previous,
1524
1526
  depthOffset,
1525
1527
  hideChildLabel,
1526
- selected,
1528
+ selected: highlighted,
1527
1529
  settleCodeEpoch,
1528
- liveSpawn
1530
+ liveSpawn,
1531
+ searchQuery,
1532
+ searchTitleMatch
1529
1533
  })
1530
1534
  });
1531
1535
  });
@@ -1558,8 +1562,8 @@ function Footer({ hints, context, cost = null, status = null, compactionTokens =
1558
1562
  const timer = setInterval(() => setCompactTick((t) => t + 1), 1e3);
1559
1563
  return () => clearInterval(timer);
1560
1564
  }, [status]);
1561
- const compactLabel = status === "compacting" ? compactingLabel(compactionTokens, compactionStartedAt) : "";
1562
- const sW = status === "compacting" ? 2 + compactLabel.length : status === "asking" ? 3 : status ? 2 : 0;
1565
+ const statusLabel = status === "compacting" ? compactingLabel(compactionTokens, compactionStartedAt) : status === "searching" ? " Searching messages…" : "";
1566
+ const sW = status === "compacting" || status === "searching" ? 2 + statusLabel.length : status === "asking" ? 3 : status ? 2 : 0;
1563
1567
  if (hW + sW + (rightW > 0 ? rightW + 1 : 0) <= inner) return /* @__PURE__ */ jsxs("box", {
1564
1568
  style: {
1565
1569
  flexDirection: "row",
@@ -1571,7 +1575,7 @@ function Footer({ hints, context, cost = null, status = null, compactionTokens =
1571
1575
  /* @__PURE__ */ jsx$1(HintsText, { hints }),
1572
1576
  /* @__PURE__ */ jsx$1(FooterStatusIcon, {
1573
1577
  status,
1574
- compactLabel
1578
+ label: statusLabel
1575
1579
  }),
1576
1580
  /* @__PURE__ */ jsx$1("box", { style: { flexGrow: 1 } }),
1577
1581
  /* @__PURE__ */ jsx$1(RightStatus, {
@@ -1594,7 +1598,7 @@ function Footer({ hints, context, cost = null, status = null, compactionTokens =
1594
1598
  },
1595
1599
  children: [/* @__PURE__ */ jsx$1(HintsText, { hints: stackedHints }), /* @__PURE__ */ jsx$1(FooterStatusIcon, {
1596
1600
  status,
1597
- compactLabel
1601
+ label: statusLabel
1598
1602
  })]
1599
1603
  }), rightW > 0 && /* @__PURE__ */ jsxs("box", {
1600
1604
  style: {
@@ -1625,16 +1629,16 @@ function RightStatus({ cost, context }) {
1625
1629
  * compacting adds a short label because background compaction can otherwise
1626
1630
  * look like an idle spinner.
1627
1631
  */
1628
- function FooterStatusIcon({ status, compactLabel = "" }) {
1632
+ function FooterStatusIcon({ status, label = "" }) {
1629
1633
  const COLOR = useColors();
1630
1634
  if (!status) return null;
1631
1635
  const glyph = status === "asking" ? /* @__PURE__ */ jsx$1("span", { children: "❓" }) : status === "busy" ? /* @__PURE__ */ jsx$1(StatusSpinner, { color: COLOR.warn }) : /* @__PURE__ */ jsx$1(StatusSpinner, { color: COLOR.accent });
1632
1636
  return /* @__PURE__ */ jsxs("text", { children: [
1633
1637
  /* @__PURE__ */ jsx$1("span", { children: " " }),
1634
1638
  glyph,
1635
- status === "compacting" && /* @__PURE__ */ jsx$1("span", {
1639
+ label && /* @__PURE__ */ jsx$1("span", {
1636
1640
  fg: COLOR.accent,
1637
- children: compactLabel
1641
+ children: label
1638
1642
  })
1639
1643
  ] });
1640
1644
  }
@@ -1924,9 +1928,8 @@ function StatusSpinner({ color }) {
1924
1928
  children: useSpinnerFrame()
1925
1929
  });
1926
1930
  }
1927
- function Transcript({ events, settings, selectedTurnId = null, busy = false }) {
1931
+ function Transcript({ events, settings, selectedTurnId = null, busy = false, searchQuery, searchPending = false, onLoadEarlier }) {
1928
1932
  const COLOR = useColors();
1929
- const items = useMemo(() => partitionTranscript(events, settings), [events, settings]);
1930
1933
  const showThrobber = busy && settings.showThrobber;
1931
1934
  const codeSettle = useRef({
1932
1935
  wasBusy: false,
@@ -1949,8 +1952,48 @@ function Transcript({ events, settings, selectedTurnId = null, busy = false }) {
1949
1952
  codeSettle.current.wasBusy = busy;
1950
1953
  const throbberLabel = events.length > 0 && events[events.length - 1].kind === "thinking" ? "Thinking" : void 0;
1951
1954
  const ownership = useMemo(() => turnSelectionOwnership(events), [events]);
1955
+ const searchMatches = useMemo(() => {
1956
+ const direct = /* @__PURE__ */ new Set();
1957
+ const callIds = /* @__PURE__ */ new Set();
1958
+ const childIds = /* @__PURE__ */ new Set();
1959
+ if (!searchQuery) return {
1960
+ direct,
1961
+ callIds,
1962
+ childIds
1963
+ };
1964
+ const stack = [];
1965
+ for (const event of events) {
1966
+ if (event.kind === "spawn-start") stack.push(event);
1967
+ if (searchHighlightRanges(streamEventSearchText(event), searchQuery).length > 0) {
1968
+ direct.add(event);
1969
+ if (event.callId) callIds.add(event.callId);
1970
+ if ((event.depth ?? 0) > 0) {
1971
+ for (const parent of stack) if (parent.childId) childIds.add(parent.childId);
1972
+ }
1973
+ }
1974
+ if (event.kind === "spawn-end") {
1975
+ const index = stack.findLastIndex((parent) => parent.childId === event.childId);
1976
+ if (index >= 0) stack.splice(index);
1977
+ }
1978
+ }
1979
+ return {
1980
+ direct,
1981
+ callIds,
1982
+ childIds
1983
+ };
1984
+ }, [events, searchQuery]);
1985
+ const items = useMemo(() => partitionTranscript(events, settings, searchQuery ? (event) => searchMatches.direct.has(event) : void 0), [
1986
+ events,
1987
+ settings,
1988
+ searchQuery,
1989
+ searchMatches
1990
+ ]);
1952
1991
  const scrollboxRef = useRef(null);
1953
1992
  const rowVisibility = useRef(createRowVisibilityStore());
1993
+ const loadEarlierRef = useRef(onLoadEarlier);
1994
+ loadEarlierRef.current = onLoadEarlier;
1995
+ const earlierLoad = useRef(null);
1996
+ const earlierLoadArmed = useRef(false);
1954
1997
  useEffect(() => {
1955
1998
  const timer = setInterval(() => {
1956
1999
  const sb = scrollboxRef.current;
@@ -1959,9 +2002,45 @@ function Transcript({ events, settings, selectedTurnId = null, busy = false }) {
1959
2002
  top: sb.viewport.y,
1960
2003
  height: sb.viewport.height
1961
2004
  });
2005
+ const topThreshold = Math.max(4, Math.floor(sb.viewport.height / 4));
2006
+ if (sb.scrollTop > topThreshold) earlierLoadArmed.current = true;
2007
+ else if (earlierLoadArmed.current && !earlierLoad.current && loadEarlierRef.current && hiddenEarlierTranscriptCount(events) > 0) {
2008
+ earlierLoadArmed.current = false;
2009
+ const snapshot = {
2010
+ oldHeight: sb.scrollHeight,
2011
+ oldTop: sb.scrollTop
2012
+ };
2013
+ earlierLoad.current = snapshot;
2014
+ if (!loadEarlierRef.current()) {
2015
+ earlierLoad.current = null;
2016
+ earlierLoadArmed.current = true;
2017
+ }
2018
+ }
1962
2019
  }, 200);
1963
2020
  return () => clearInterval(timer);
1964
- }, []);
2021
+ }, [events]);
2022
+ useEffect(() => {
2023
+ const pending = earlierLoad.current;
2024
+ if (!pending) return;
2025
+ let cancelled = false;
2026
+ let attempts = 0;
2027
+ const restore = () => {
2028
+ if (cancelled) return;
2029
+ const sb = scrollboxRef.current;
2030
+ if (attempts < 5 && (!sb || sb.scrollHeight <= pending.oldHeight)) {
2031
+ attempts++;
2032
+ requestAnimationFrame(restore);
2033
+ return;
2034
+ }
2035
+ if (sb) sb.scrollTop = pending.oldTop + Math.max(0, sb.scrollHeight - pending.oldHeight);
2036
+ earlierLoad.current = null;
2037
+ };
2038
+ const handle = requestAnimationFrame(restore);
2039
+ return () => {
2040
+ cancelled = true;
2041
+ cancelAnimationFrame(handle);
2042
+ };
2043
+ }, [events]);
1965
2044
  const modal = useModalState();
1966
2045
  useKeyboard(useCallback((key) => {
1967
2046
  if (modal?.isOpen) return;
@@ -1993,36 +2072,56 @@ function Transcript({ events, settings, selectedTurnId = null, busy = false }) {
1993
2072
  }, [items, events]);
1994
2073
  useEffect(() => {
1995
2074
  if (!selectedTurnId) return;
1996
- const scrollbox = scrollboxRef.current;
1997
- if (!scrollbox) return;
1998
- const handle = requestAnimationFrame(() => {
2075
+ let handle = 0;
2076
+ let attempts = 0;
2077
+ const align = () => {
2078
+ const scrollbox = scrollboxRef.current;
2079
+ if (!scrollbox) return;
2080
+ const id = anchors.idByTurn.get(selectedTurnId);
2081
+ if (searchQuery && id) {
2082
+ const child = scrollbox.content.findDescendantById(id);
2083
+ if (child && scrollbox.scrollHeight > scrollbox.viewport.height) {
2084
+ scrollbox.scrollTop = child.y - scrollbox.content.y;
2085
+ return;
2086
+ }
2087
+ if (attempts++ < 5) handle = requestAnimationFrame(align);
2088
+ return;
2089
+ }
1999
2090
  const ownsLast = anchors.lastTurnId !== void 0 && ownership.get(anchors.lastTurnId) === selectedTurnId;
2000
2091
  if (selectedTurnId === anchors.lastTurnId || ownsLast) {
2001
2092
  scrollbox.scrollTop = scrollbox.scrollHeight;
2002
2093
  return;
2003
2094
  }
2004
- const id = anchors.idByTurn.get(selectedTurnId);
2005
2095
  if (id) scrollbox.scrollChildIntoView(id);
2006
- });
2096
+ };
2097
+ handle = requestAnimationFrame(align);
2007
2098
  return () => cancelAnimationFrame(handle);
2008
2099
  }, [
2009
2100
  selectedTurnId,
2010
2101
  anchors,
2011
- ownership
2102
+ ownership,
2103
+ searchQuery
2012
2104
  ]);
2013
- if (items.length === 0 && !showThrobber) return /* @__PURE__ */ jsx$1(EmptyState, {});
2105
+ if (items.length === 0 && !showThrobber) {
2106
+ if (searchQuery) return searchPending ? /* @__PURE__ */ jsx$1("box", { style: { flexGrow: 1 } }) : /* @__PURE__ */ jsx$1("text", {
2107
+ fg: COLOR.mute,
2108
+ children: `No messages match “${searchQuery.trim()}”`
2109
+ });
2110
+ return /* @__PURE__ */ jsx$1(EmptyState, {});
2111
+ }
2014
2112
  return /* @__PURE__ */ jsx$1(RowVisibilityContext.Provider, {
2015
2113
  value: rowVisibility.current.store,
2016
2114
  children: /* @__PURE__ */ jsxs("scrollbox", {
2017
2115
  ref: scrollboxRef,
2116
+ id: "transcript-scrollbox",
2018
2117
  focusable: false,
2019
2118
  style: {
2020
2119
  flexGrow: 1,
2021
2120
  paddingLeft: 1,
2022
2121
  paddingRight: 1
2023
2122
  },
2024
- stickyScroll: true,
2025
- stickyStart: "bottom",
2123
+ stickyScroll: !searchQuery,
2124
+ stickyStart: searchQuery ? void 0 : "bottom",
2026
2125
  verticalScrollbarOptions: {
2027
2126
  width: 1,
2028
2127
  trackOptions: {
@@ -2039,7 +2138,9 @@ function Transcript({ events, settings, selectedTurnId = null, busy = false }) {
2039
2138
  selected: isTurnHighlighted(item.event, selectedTurnId, ownership),
2040
2139
  anchorId: anchors.ids[i][0],
2041
2140
  settleCodeEpoch: codeSettle.current.targets.has(item.event) ? codeSettle.current.epoch : 0,
2042
- liveSpawn: live?.[0]
2141
+ liveSpawn: live?.[0],
2142
+ searchQuery,
2143
+ searchTitleMatch: item.event.kind === "tool" && item.event.callId !== void 0 && searchMatches.callIds.has(item.event.callId) || (item.event.kind === "spawn-start" || item.event.kind === "spawn-end") && item.event.childId !== void 0 && searchMatches.childIds.has(item.event.childId)
2043
2144
  }, i);
2044
2145
  }
2045
2146
  return /* @__PURE__ */ jsx$1(SubagentBlock, {
@@ -2049,7 +2150,9 @@ function Transcript({ events, settings, selectedTurnId = null, busy = false }) {
2049
2150
  anchorIds: anchors.ids[i],
2050
2151
  codeSettleTargets: codeSettle.current.targets,
2051
2152
  codeSettleEpoch: codeSettle.current.epoch,
2052
- live: liveByItem.get(i)
2153
+ live: liveByItem.get(i),
2154
+ searchQuery,
2155
+ searchTitleMatches: searchMatches
2053
2156
  }, i);
2054
2157
  }), showThrobber && /* @__PURE__ */ jsx$1("box", {
2055
2158
  style: { marginTop: items.length > 0 ? 1 : 0 },
@@ -2145,8 +2248,8 @@ function collapseCompletedSpawnStarts(events) {
2145
2248
  }
2146
2249
  return events.filter((_, i) => keep[i]);
2147
2250
  }
2148
- function partitionTranscript(events, settings) {
2149
- const visible = collapseCompletedSpawnStarts(events).filter((e) => isVisible(e, settings));
2251
+ function partitionTranscript(events, settings, searchReveal) {
2252
+ const visible = collapseCompletedSpawnStarts(events).filter((event) => isVisible(event, settings) || searchReveal !== void 0 && (event.depth ?? 0) === 0 && searchReveal(event));
2150
2253
  if (visible.length === 0) return [];
2151
2254
  if (settings.hideSubagentOutput) return visible.map((event, i) => ({
2152
2255
  kind: "event",
@@ -2192,7 +2295,7 @@ function partitionTranscript(events, settings) {
2192
2295
  * indented twice. Grandchildren (depth ≥ 2) still indent further, so
2193
2296
  * nested subagents remain visually distinct.
2194
2297
  */
2195
- function SubagentBlock({ events, previous, selectedTurnId = null, anchorIds, codeSettleTargets, codeSettleEpoch = 0, live }) {
2298
+ function SubagentBlock({ events, previous, selectedTurnId = null, anchorIds, codeSettleTargets, codeSettleEpoch = 0, live, searchQuery, searchTitleMatches }) {
2196
2299
  const COLOR = useColors();
2197
2300
  const childIds = useMemo(() => {
2198
2301
  const set = /* @__PURE__ */ new Set();
@@ -2224,7 +2327,9 @@ function SubagentBlock({ events, previous, selectedTurnId = null, anchorIds, cod
2224
2327
  anchorId: anchorIds?.[i],
2225
2328
  hideChildLabel,
2226
2329
  settleCodeEpoch: codeSettleTargets?.has(evt) ? codeSettleEpoch : 0,
2227
- liveSpawn: evt.kind === "spawn-start" ? live?.find((spawn) => spawn.id === evt.childId) : void 0
2330
+ liveSpawn: evt.kind === "spawn-start" ? live?.find((spawn) => spawn.id === evt.childId) : void 0,
2331
+ searchQuery,
2332
+ searchTitleMatch: evt.kind === "tool" && evt.callId !== void 0 && searchTitleMatches?.callIds.has(evt.callId) || (evt.kind === "spawn-start" || evt.kind === "spawn-end") && evt.childId !== void 0 && searchTitleMatches?.childIds.has(evt.childId)
2228
2333
  }, i))
2229
2334
  });
2230
2335
  }
@@ -2233,13 +2338,15 @@ function SubagentBlock({ events, previous, selectedTurnId = null, anchorIds, cod
2233
2338
  * the left lane for the whole run; only the right tail changes:
2234
2339
  * active `2 turns · 14s · ⠋ tool call`, done `4 turns`.
2235
2340
  */
2236
- function SpawnLifecycleMarker({ row, dim, event, hideChildLabel, live }) {
2341
+ function SpawnLifecycleMarker({ row, dim, event, hideChildLabel, live, searchMatch = false }) {
2237
2342
  const COLOR = useColors();
2343
+ const matchColor = resolveChipColor(useSurfaces().chips, "default");
2238
2344
  const frame = useSpinnerFrame();
2239
2345
  const terminal = event.kind === "spawn-end";
2240
2346
  const failed = terminal && (event.text.startsWith("error") || event.text.startsWith("aborted"));
2241
2347
  const turns = terminal ? event.spawn?.turns : live?.turns;
2242
2348
  const turnLabel = `${turns ?? 0} turn${turns === 1 ? "" : "s"}`;
2349
+ const showTail = live !== void 0 || terminal && turns !== void 0;
2243
2350
  const elapsed = live ? formatDuration(Math.max(0, Date.now() - live.startedAt)) : null;
2244
2351
  return /* @__PURE__ */ jsxs("box", {
2245
2352
  style: {
@@ -2258,7 +2365,8 @@ function SpawnLifecycleMarker({ row, dim, event, hideChildLabel, live }) {
2258
2365
  children: terminal ? failed ? "✗ " : "🌳 " : "🌱 "
2259
2366
  }),
2260
2367
  !hideChildLabel && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx$1("span", {
2261
- fg: COLOR.brand,
2368
+ fg: searchMatch ? matchColor.fg : COLOR.brand,
2369
+ bg: searchMatch ? matchColor.bg : void 0,
2262
2370
  children: event.childId ?? "child"
2263
2371
  }), /* @__PURE__ */ jsx$1("span", {
2264
2372
  fg: COLOR.mute,
@@ -2278,7 +2386,7 @@ function SpawnLifecycleMarker({ row, dim, event, hideChildLabel, live }) {
2278
2386
  ]
2279
2387
  }),
2280
2388
  /* @__PURE__ */ jsx$1("box", { style: { flexGrow: 1 } }),
2281
- /* @__PURE__ */ jsxs("text", {
2389
+ showTail && /* @__PURE__ */ jsxs("text", {
2282
2390
  fg: COLOR.mute,
2283
2391
  wrapMode: "none",
2284
2392
  style: { flexShrink: 0 },
@@ -2333,7 +2441,7 @@ function rowStyle(paddingLeft) {
2333
2441
  alignSelf: "stretch"
2334
2442
  };
2335
2443
  }
2336
- function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = false, selected = false, settleCodeEpoch = 0, liveSpawn }) {
2444
+ function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = false, selected = false, settleCodeEpoch = 0, liveSpawn, searchQuery, searchTitleMatch = false }) {
2337
2445
  const COLOR = useColors();
2338
2446
  const { settings } = useSettings();
2339
2447
  const cleanText = stripTerminalControlChars(event.text);
@@ -2346,7 +2454,8 @@ function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = fals
2346
2454
  case "user-prompt": return /* @__PURE__ */ jsx$1(UserPromptBlock, {
2347
2455
  text: safeText,
2348
2456
  refs: event.refs,
2349
- attachments: event.attachments
2457
+ attachments: event.attachments,
2458
+ searchQuery
2350
2459
  });
2351
2460
  case "info": return /* @__PURE__ */ jsx$1("box", {
2352
2461
  style: row,
@@ -2404,7 +2513,8 @@ function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = fals
2404
2513
  children: [/* @__PURE__ */ jsx$1(ToolCallBlock, {
2405
2514
  event,
2406
2515
  display: settings.toolCallDisplay === "full" ? "full" : "formatted",
2407
- dim: child
2516
+ dim: child,
2517
+ searchMatch: searchTitleMatch
2408
2518
  }), event.tool === "todowrite" && /* @__PURE__ */ jsx$1(TodoInProgressList, {
2409
2519
  input: event.input,
2410
2520
  dim: child
@@ -2416,7 +2526,8 @@ function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = fals
2416
2526
  renderDefault: () => /* @__PURE__ */ jsx$1(ToolResultBlock, {
2417
2527
  text: event.text,
2418
2528
  indent: row.paddingLeft,
2419
- maxLines: isUserBashEvent(event) ? settings.showFullShellOutput ? Number.MAX_SAFE_INTEGER : BASH_RESULT_MAX_LINES : void 0
2529
+ maxLines: isUserBashEvent(event) ? settings.showFullShellOutput ? Number.MAX_SAFE_INTEGER : BASH_RESULT_MAX_LINES : void 0,
2530
+ searchQuery
2420
2531
  })
2421
2532
  });
2422
2533
  case "custom": return /* @__PURE__ */ jsx$1("box", {
@@ -2461,13 +2572,15 @@ function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = fals
2461
2572
  dim: COLOR.dim,
2462
2573
  event,
2463
2574
  hideChildLabel,
2464
- live: liveSpawn
2575
+ live: liveSpawn,
2576
+ searchMatch: searchTitleMatch
2465
2577
  });
2466
2578
  case "spawn-end": return /* @__PURE__ */ jsx$1(SpawnLifecycleMarker, {
2467
2579
  row,
2468
2580
  dim: COLOR.dim,
2469
2581
  event,
2470
- hideChildLabel
2582
+ hideChildLabel,
2583
+ searchMatch: searchTitleMatch
2471
2584
  });
2472
2585
  case "compact-summary": return /* @__PURE__ */ jsx$1(CompactSummaryBlock, {
2473
2586
  event,
@@ -2635,9 +2748,10 @@ function CompactSummaryBlock({ event, indent }) {
2635
2748
  */
2636
2749
  /** Prompt chevron rendered ahead of every user-prompt block. */
2637
2750
  const USER_PROMPT_PREFIX = "❯ ";
2638
- function UserPromptBlock({ text, refs, attachments }) {
2751
+ function UserPromptBlock({ text, refs, attachments, searchQuery }) {
2639
2752
  const COLOR = useColors();
2640
2753
  const SURFACE = useSurfaces();
2754
+ const matchColor = resolveChipColor(SURFACE.chips, "default");
2641
2755
  const boxStyle = {
2642
2756
  border: true,
2643
2757
  borderColor: COLOR.borderActive,
@@ -2670,20 +2784,30 @@ function UserPromptBlock({ text, refs, attachments }) {
2670
2784
  })] }, `att-${idx}`);
2671
2785
  })
2672
2786
  }) : null;
2673
- if (!refs || refs.length === 0) return /* @__PURE__ */ jsxs("box", {
2674
- style: boxStyle,
2675
- children: [
2676
- text.length > 0 && /* @__PURE__ */ jsxs("text", { children: [/* @__PURE__ */ jsx$1("span", {
2677
- fg: COLOR.brand,
2678
- children: USER_PROMPT_PREFIX
2679
- }), text] }),
2680
- !text.length && attachmentChips && /* @__PURE__ */ jsx$1("text", {
2681
- fg: COLOR.brand,
2682
- children: USER_PROMPT_PREFIX
2683
- }),
2684
- attachmentChips
2685
- ]
2686
- });
2787
+ if (!refs || refs.length === 0) {
2788
+ const ranges = searchQuery ? searchHighlightRanges(text, searchQuery) : [];
2789
+ return /* @__PURE__ */ jsxs("box", {
2790
+ style: boxStyle,
2791
+ children: [
2792
+ text.length > 0 && /* @__PURE__ */ jsxs("text", {
2793
+ wrapMode: "word",
2794
+ children: [/* @__PURE__ */ jsx$1("span", {
2795
+ fg: COLOR.brand,
2796
+ children: USER_PROMPT_PREFIX
2797
+ }), highlightSegments(text, ranges).map((segment, i) => /* @__PURE__ */ jsx$1("span", {
2798
+ fg: segment.highlighted ? matchColor.fg : void 0,
2799
+ bg: segment.highlighted ? matchColor.bg : void 0,
2800
+ children: segment.text
2801
+ }, i))]
2802
+ }),
2803
+ !text.length && attachmentChips && /* @__PURE__ */ jsx$1("text", {
2804
+ fg: COLOR.brand,
2805
+ children: USER_PROMPT_PREFIX
2806
+ }),
2807
+ attachmentChips
2808
+ ]
2809
+ });
2810
+ }
2687
2811
  const segments = splitPromptSegments(text, refs);
2688
2812
  return /* @__PURE__ */ jsxs("box", {
2689
2813
  style: boxStyle,
@@ -2696,7 +2820,14 @@ function UserPromptBlock({ text, refs, attachments }) {
2696
2820
  fg: COLOR.brand,
2697
2821
  children: USER_PROMPT_PREFIX
2698
2822
  }), segments.map((seg, i) => {
2699
- if (seg.kind === "plain") return /* @__PURE__ */ jsx$1("text", { children: seg.text }, i);
2823
+ if (seg.kind === "plain") {
2824
+ const ranges = searchQuery ? searchHighlightRanges(seg.text, searchQuery) : [];
2825
+ return /* @__PURE__ */ jsx$1("text", { children: highlightSegments(seg.text, ranges).map((part, j) => /* @__PURE__ */ jsx$1("span", {
2826
+ fg: part.highlighted ? matchColor.fg : void 0,
2827
+ bg: part.highlighted ? matchColor.bg : void 0,
2828
+ children: part.text
2829
+ }, j)) }, i);
2830
+ }
2700
2831
  const chip = resolveChipColor(SURFACE.chips, seg.providerId);
2701
2832
  return /* @__PURE__ */ jsx$1("text", {
2702
2833
  fg: chip.fg,
@@ -3131,33 +3262,64 @@ function CommandDetailBlock({ label, text, indent, diff }) {
3131
3262
  })]
3132
3263
  });
3133
3264
  }
3134
- function ToolResultBlock({ text, indent, diff, maxLines = TOOL_RESULT_MAX_LINES }) {
3265
+ function ToolResultBlock({ text, indent, diff, maxLines = TOOL_RESULT_MAX_LINES, searchQuery }) {
3135
3266
  const COLOR = useColors();
3136
- const rawLines = stripAnsiSequences(text).split("\n");
3137
- let end = rawLines.length;
3138
- while (end > 0 && rawLines[end - 1].trim() === "") end--;
3139
- if (end === 0) return null;
3140
- const lines = rawLines.slice(0, end);
3141
- const visible = lines.slice(0, maxLines);
3142
- const omitted = Math.max(0, lines.length - maxLines);
3267
+ const matchColor = resolveChipColor(useSurfaces().chips, "default");
3268
+ const { lines, start } = useMemo(() => {
3269
+ const rawLines = stripAnsiSequences(text).split("\n");
3270
+ let end = rawLines.length;
3271
+ while (end > 0 && rawLines[end - 1].trim() === "") end--;
3272
+ const trimmed = rawLines.slice(0, end);
3273
+ let windowStart = 0;
3274
+ if (searchQuery && trimmed.length > maxLines) {
3275
+ const matchIdx = trimmed.findIndex((line) => searchHighlightRanges(line, searchQuery).length > 0);
3276
+ if (matchIdx >= maxLines) windowStart = Math.min(Math.max(0, matchIdx - Math.floor(maxLines / 2)), trimmed.length - maxLines);
3277
+ }
3278
+ return {
3279
+ lines: trimmed,
3280
+ start: windowStart
3281
+ };
3282
+ }, [
3283
+ text,
3284
+ searchQuery,
3285
+ maxLines
3286
+ ]);
3287
+ if (lines.length === 0) return null;
3288
+ const visible = lines.slice(start, start + maxLines);
3289
+ const omittedBefore = start;
3290
+ const omitted = Math.max(0, lines.length - start - maxLines);
3143
3291
  return /* @__PURE__ */ jsxs("box", {
3144
3292
  style: {
3145
3293
  paddingLeft: indent,
3146
3294
  flexDirection: "column"
3147
3295
  },
3148
- children: [visible.map((line, i) => /* @__PURE__ */ jsxs("text", {
3149
- fg: COLOR.mute,
3150
- children: [/* @__PURE__ */ jsx$1("span", {
3151
- fg: COLOR.borderActive,
3152
- children: "┃ "
3153
- }), diff && line ? /* @__PURE__ */ jsx$1(DiffStatLine, { line }) : line || " "]
3154
- }, i)), omitted > 0 && /* @__PURE__ */ jsxs("text", {
3155
- fg: COLOR.mute,
3156
- children: [/* @__PURE__ */ jsx$1("span", {
3157
- fg: COLOR.borderActive,
3158
- children: " "
3159
- }), `… ${omitted} more line${omitted === 1 ? "" : "s"}`]
3160
- })]
3296
+ children: [
3297
+ omittedBefore > 0 && /* @__PURE__ */ jsxs("text", {
3298
+ fg: COLOR.mute,
3299
+ children: [/* @__PURE__ */ jsx$1("span", {
3300
+ fg: COLOR.borderActive,
3301
+ children: " "
3302
+ }), `… ${omittedBefore} earlier line${omittedBefore === 1 ? "" : "s"}`]
3303
+ }),
3304
+ visible.map((line, i) => /* @__PURE__ */ jsxs("text", {
3305
+ fg: COLOR.mute,
3306
+ children: [/* @__PURE__ */ jsx$1("span", {
3307
+ fg: COLOR.borderActive,
3308
+ children: "┃ "
3309
+ }), diff && line ? /* @__PURE__ */ jsx$1(DiffStatLine, { line }) : highlightSegments(line || " ", searchQuery ? searchHighlightRanges(line, searchQuery) : []).map((segment, j) => /* @__PURE__ */ jsx$1("span", {
3310
+ fg: segment.highlighted ? matchColor.fg : COLOR.mute,
3311
+ bg: segment.highlighted ? matchColor.bg : void 0,
3312
+ children: segment.text
3313
+ }, j))]
3314
+ }, i)),
3315
+ omitted > 0 && /* @__PURE__ */ jsxs("text", {
3316
+ fg: COLOR.mute,
3317
+ children: [/* @__PURE__ */ jsx$1("span", {
3318
+ fg: COLOR.borderActive,
3319
+ children: "┃ "
3320
+ }), `… ${omitted} more line${omitted === 1 ? "" : "s"}`]
3321
+ })
3322
+ ]
3161
3323
  });
3162
3324
  }
3163
3325
  function EditDiffBlock({ payload, dim }) {
@@ -3514,8 +3676,9 @@ function HunkBlock({ index, hunk, outcome, tool, path, filetype, mdStyle, COLOR,
3514
3676
  });
3515
3677
  }
3516
3678
  const FULL_VIEW_MAX_INPUT_BYTES = 32 * 1024;
3517
- function ToolCallBlock({ event, display, dim }) {
3679
+ function ToolCallBlock({ event, display, dim, searchMatch = false }) {
3518
3680
  const COLOR = useColors();
3681
+ const matchColor = resolveChipColor(useSurfaces().chips, "default");
3519
3682
  const mdStyle = useMdStyle();
3520
3683
  const name = event.tool ?? "";
3521
3684
  const verb = displayNameFor(name, event.input);
@@ -3543,7 +3706,8 @@ function ToolCallBlock({ event, display, dim }) {
3543
3706
  children: "↳ "
3544
3707
  }),
3545
3708
  /* @__PURE__ */ jsx$1("span", {
3546
- fg: dim ? COLOR.dim : COLOR.model,
3709
+ fg: searchMatch ? matchColor.fg : dim ? COLOR.dim : COLOR.model,
3710
+ bg: searchMatch ? matchColor.bg : void 0,
3547
3711
  children: verb
3548
3712
  }),
3549
3713
  /* @__PURE__ */ jsx$1("span", {
@@ -3575,7 +3739,8 @@ function ToolCallBlock({ event, display, dim }) {
3575
3739
  children: "↳ "
3576
3740
  }),
3577
3741
  /* @__PURE__ */ jsx$1("span", {
3578
- fg: dim ? COLOR.dim : COLOR.model,
3742
+ fg: searchMatch ? matchColor.fg : dim ? COLOR.dim : COLOR.model,
3743
+ bg: searchMatch ? matchColor.bg : void 0,
3579
3744
  children: verb
3580
3745
  }),
3581
3746
  line?.target && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx$1("span", {
@@ -4945,6 +5110,255 @@ async function resolveExtensionModalNode(node) {
4945
5110
  return loaded;
4946
5111
  }
4947
5112
  //#endregion
5113
+ //#region src/tui/history-search-worker-client.ts
5114
+ let nextClientId = 1;
5115
+ /** Mirror of the TUI's ZIDANE_DEBUG-gated logger — silent by default. */
5116
+ const debugFallback = process.env.ZIDANE_DEBUG ? (op, error) => console.error(`[zidane/tui] history search ${op} fell back to the main thread:`, error) : () => {};
5117
+ function workerUrl() {
5118
+ if (typeof Bun !== "undefined" && Bun.isStandaloneExecutable) return new URL("./history-search-worker.ts", import.meta.url);
5119
+ return new URL(import.meta.url.endsWith(".ts") ? "./history-search-worker.ts" : "./history-search-worker.js", import.meta.url);
5120
+ }
5121
+ var HistorySearchWorkerClient = class {
5122
+ createWorker;
5123
+ worker = null;
5124
+ constructor(createWorker = () => new Worker(workerUrl(), {
5125
+ type: "module",
5126
+ smol: true
5127
+ })) {
5128
+ this.createWorker = createWorker;
5129
+ }
5130
+ indexes = /* @__PURE__ */ new WeakMap();
5131
+ registrations = /* @__PURE__ */ new Map();
5132
+ rebuilds = /* @__PURE__ */ new Map();
5133
+ pending = /* @__PURE__ */ new Map();
5134
+ nextRequestId = 1;
5135
+ unavailable = false;
5136
+ prewarm() {
5137
+ if (this.unavailable) return;
5138
+ try {
5139
+ this.ensureWorker();
5140
+ } catch {}
5141
+ }
5142
+ async build(turns, signal) {
5143
+ signal?.throwIfAborted();
5144
+ const indexId = `search-index-${nextClientId++}`;
5145
+ if (this.unavailable) return {
5146
+ indexId,
5147
+ length: turns.length,
5148
+ turns
5149
+ };
5150
+ try {
5151
+ return {
5152
+ ...await this.request({
5153
+ type: "build",
5154
+ indexId,
5155
+ turns
5156
+ }, signal),
5157
+ turns
5158
+ };
5159
+ } catch (error) {
5160
+ if (signal?.aborted) throw signal.reason;
5161
+ debugFallback("build", error);
5162
+ return {
5163
+ indexId,
5164
+ length: turns.length,
5165
+ turns
5166
+ };
5167
+ }
5168
+ }
5169
+ async search(index, query, limit, signal) {
5170
+ if (this.unavailable) return searchSessionIndexAsync(await this.fallbackIndex(index, signal), query, {
5171
+ limit,
5172
+ signal
5173
+ });
5174
+ try {
5175
+ const indexId = "indexId" in index ? index.indexId : await this.register(index);
5176
+ return await this.request({
5177
+ type: "search",
5178
+ indexId,
5179
+ query,
5180
+ limit
5181
+ }, signal);
5182
+ } catch (error) {
5183
+ if (signal?.aborted) throw signal.reason;
5184
+ if ("indexId" in index && error instanceof Error && error.message.startsWith("Unknown search index:")) {
5185
+ await this.rebuild(index);
5186
+ return this.request({
5187
+ type: "search",
5188
+ indexId: index.indexId,
5189
+ query,
5190
+ limit
5191
+ }, signal);
5192
+ }
5193
+ debugFallback("search", error);
5194
+ return searchSessionIndexAsync(await this.fallbackIndex(index, signal), query, {
5195
+ limit,
5196
+ signal
5197
+ });
5198
+ }
5199
+ }
5200
+ async history(index, query, maxExcerpts, signal) {
5201
+ if (this.unavailable) return searchSessionHistoryIndexAsync(await this.fallbackIndex(index, signal), query, maxExcerpts, void 0, signal);
5202
+ try {
5203
+ const indexId = "indexId" in index ? index.indexId : await this.register(index);
5204
+ return await this.request({
5205
+ type: "history",
5206
+ indexId,
5207
+ query,
5208
+ maxExcerpts
5209
+ }, signal);
5210
+ } catch (error) {
5211
+ if (signal?.aborted) throw signal.reason;
5212
+ if ("indexId" in index && error instanceof Error && error.message.startsWith("Unknown search index:")) {
5213
+ await this.rebuild(index);
5214
+ return this.request({
5215
+ type: "history",
5216
+ indexId: index.indexId,
5217
+ query,
5218
+ maxExcerpts
5219
+ }, signal);
5220
+ }
5221
+ debugFallback("history", error);
5222
+ return searchSessionHistoryIndexAsync(await this.fallbackIndex(index, signal), query, maxExcerpts, void 0, signal);
5223
+ }
5224
+ }
5225
+ terminate() {
5226
+ this.worker?.terminate();
5227
+ this.worker = null;
5228
+ const error = /* @__PURE__ */ new Error("History search worker terminated");
5229
+ for (const pending of this.pending.values()) pending.reject(error);
5230
+ this.pending.clear();
5231
+ this.registrations.clear();
5232
+ this.rebuilds.clear();
5233
+ }
5234
+ /** Re-register an evicted worker index; concurrent callers share one build. */
5235
+ rebuild(index) {
5236
+ const inFlight = this.rebuilds.get(index.indexId);
5237
+ if (inFlight) return inFlight;
5238
+ const rebuild = this.request({
5239
+ type: "build",
5240
+ indexId: index.indexId,
5241
+ turns: index.turns
5242
+ }).then(() => {}).finally(() => this.rebuilds.delete(index.indexId));
5243
+ this.rebuilds.set(index.indexId, rebuild);
5244
+ return rebuild;
5245
+ }
5246
+ fallbackIndex(index, signal) {
5247
+ if (!("indexId" in index)) return Promise.resolve(index);
5248
+ if (!index.fallback) index.fallback = buildSessionSearchIndexAsync(index.turns).catch((error) => {
5249
+ index.fallback = void 0;
5250
+ throw error;
5251
+ });
5252
+ signal?.throwIfAborted();
5253
+ return index.fallback;
5254
+ }
5255
+ ensureWorker() {
5256
+ if (this.worker) return this.worker;
5257
+ let worker;
5258
+ try {
5259
+ worker = this.createWorker();
5260
+ } catch (error) {
5261
+ this.disable(error);
5262
+ throw error;
5263
+ }
5264
+ worker.onmessage = (event) => this.onMessage(event.data);
5265
+ worker.onerror = (event) => this.disable(event.error ?? new Error(event.message));
5266
+ this.worker = worker;
5267
+ return worker;
5268
+ }
5269
+ register(index) {
5270
+ const known = this.indexes.get(index);
5271
+ if (known) {
5272
+ const pending = this.registrations.get(known);
5273
+ return pending ? pending.then(() => known) : Promise.resolve(known);
5274
+ }
5275
+ const indexId = `search-index-${nextClientId++}`;
5276
+ this.indexes.set(index, indexId);
5277
+ const registration = new Promise((resolve, reject) => {
5278
+ const worker = this.ensureWorker();
5279
+ let onRegistered;
5280
+ const onError = () => {
5281
+ worker.removeEventListener("message", onRegistered);
5282
+ this.registrations.delete(indexId);
5283
+ reject(/* @__PURE__ */ new Error("History search worker registration failed"));
5284
+ };
5285
+ onRegistered = (event) => {
5286
+ if (event.data.type !== "registered" || event.data.indexId !== indexId) return;
5287
+ worker.removeEventListener("message", onRegistered);
5288
+ worker.removeEventListener("error", onError);
5289
+ this.registrations.delete(indexId);
5290
+ resolve();
5291
+ };
5292
+ worker.addEventListener("message", onRegistered);
5293
+ worker.addEventListener("error", onError, { once: true });
5294
+ try {
5295
+ worker.postMessage({
5296
+ type: "register",
5297
+ indexId,
5298
+ index
5299
+ });
5300
+ } catch (error) {
5301
+ worker.removeEventListener("message", onRegistered);
5302
+ worker.removeEventListener("error", onError);
5303
+ reject(error instanceof Error ? error : new Error(String(error)));
5304
+ }
5305
+ });
5306
+ this.registrations.set(indexId, registration);
5307
+ return registration.then(() => indexId);
5308
+ }
5309
+ request(payload, signal) {
5310
+ signal?.throwIfAborted();
5311
+ const requestId = this.nextRequestId++;
5312
+ const worker = this.ensureWorker();
5313
+ return new Promise((resolve, reject) => {
5314
+ const abort = () => {
5315
+ worker.postMessage({
5316
+ type: "cancel",
5317
+ requestId
5318
+ });
5319
+ this.pending.delete(requestId);
5320
+ reject(signal?.reason ?? new DOMException("Aborted", "AbortError"));
5321
+ };
5322
+ signal?.addEventListener("abort", abort, { once: true });
5323
+ this.pending.set(requestId, {
5324
+ resolve: (value) => {
5325
+ signal?.removeEventListener("abort", abort);
5326
+ resolve(value);
5327
+ },
5328
+ reject: (error) => {
5329
+ signal?.removeEventListener("abort", abort);
5330
+ reject(error);
5331
+ }
5332
+ });
5333
+ try {
5334
+ worker.postMessage({
5335
+ ...payload,
5336
+ requestId
5337
+ });
5338
+ } catch (error) {
5339
+ this.pending.delete(requestId);
5340
+ signal?.removeEventListener("abort", abort);
5341
+ reject(error instanceof Error ? error : new Error(String(error)));
5342
+ }
5343
+ });
5344
+ }
5345
+ onMessage(response) {
5346
+ if (response.requestId === void 0) return;
5347
+ const pending = this.pending.get(response.requestId);
5348
+ if (!pending) return;
5349
+ this.pending.delete(response.requestId);
5350
+ if (response.type === "error") pending.reject(new Error(response.message ?? "History search worker failed"));
5351
+ else pending.resolve(response.result);
5352
+ }
5353
+ disable(error) {
5354
+ this.unavailable = true;
5355
+ const reason = error instanceof Error ? error : new Error(String(error));
5356
+ for (const pending of this.pending.values()) pending.reject(reason);
5357
+ this.pending.clear();
5358
+ this.terminate();
5359
+ }
5360
+ };
5361
+ //#endregion
4948
5362
  //#region src/tui/keybindings-modal.tsx
4949
5363
  function KeybindingsModal({ bindings, extensionRows, filePath, onEditFile, onClose }) {
4950
5364
  const SURFACE = useSurfaces();
@@ -5412,6 +5826,38 @@ function OptionRow({ option, isEnabled, isFocused, highlightBg }) {
5412
5826
  */
5413
5827
  /** Visible rows in the windowed list. Keeps the modal a stable size on long catalogs. */
5414
5828
  const VISIBLE_ROWS = 10;
5829
+ /**
5830
+ * Promote selected entries in SELECTION order, then append every unselected
5831
+ * row in its original catalog order. Pure/non-mutating and collision-safe:
5832
+ * duplicate/missing selection keys are ignored. Call only for an empty query
5833
+ * — search results must retain `filterModelCatalog`'s relevance order.
5834
+ */
5835
+ function selectedModelsFirst(entries, selectedKeys, keyOf) {
5836
+ if (selectedKeys.length === 0) return [...entries];
5837
+ const byKey = new Map(entries.map((entry) => [keyOf(entry), entry]));
5838
+ const selected = [];
5839
+ const promoted = /* @__PURE__ */ new Set();
5840
+ for (const key of selectedKeys) {
5841
+ const entry = byKey.get(key);
5842
+ if (!entry || promoted.has(key)) continue;
5843
+ selected.push(entry);
5844
+ promoted.add(key);
5845
+ }
5846
+ return [...selected, ...entries.filter((entry) => !promoted.has(keyOf(entry)))];
5847
+ }
5848
+ function providerModelKey(providerKey, modelId) {
5849
+ return `${providerKey}\u0000${modelId}`;
5850
+ }
5851
+ /** Move one selected entry by one priority slot; boundary/missing = identity. */
5852
+ function reorderSelectedModel(selected, key, delta, keyOf) {
5853
+ const from = selected.findIndex((entry) => keyOf(entry) === key);
5854
+ const to = from + delta;
5855
+ if (from < 0 || to < 0 || to >= selected.length) return selected;
5856
+ const next = [...selected];
5857
+ const [entry] = next.splice(from, 1);
5858
+ next.splice(to, 0, entry);
5859
+ return next;
5860
+ }
5415
5861
  function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onApplyChain }) {
5416
5862
  const COLOR = useColors();
5417
5863
  const SURFACE = useSurfaces();
@@ -5429,13 +5875,6 @@ function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onAppl
5429
5875
  modelsFor,
5430
5876
  current
5431
5877
  ]);
5432
- const filtered = useMemo(() => filterModelCatalog(catalog, query), [catalog, query]);
5433
- const [selectedIdx, setSelectedIdx] = useState(() => Math.max(0, indexOfEntry(catalog, current)));
5434
- const handleQueryChange = useCallback((next) => {
5435
- setQuery(next);
5436
- setSelectedIdx(0);
5437
- }, []);
5438
- const safeIndex = filtered.length === 0 ? 0 : Math.min(selectedIdx, filtered.length - 1);
5439
5878
  const chainMode = onApplyChain !== void 0;
5440
5879
  const [initialChain] = useState(() => chain && chain.length > 0 ? [...chain] : [{
5441
5880
  provider: current.providerKey,
@@ -5443,6 +5882,21 @@ function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onAppl
5443
5882
  }]);
5444
5883
  const [chainState, setChainState] = useState(initialChain);
5445
5884
  const modalApi = useModalState();
5885
+ const filtered = useMemo(() => {
5886
+ const matches = filterModelCatalog(catalog, query);
5887
+ if (query.trim()) return matches;
5888
+ return selectedModelsFirst(matches, chainState.map((pick) => providerModelKey(pick.provider, pick.model)), (row) => providerModelKey(row.providerKey, row.model.id));
5889
+ }, [
5890
+ catalog,
5891
+ query,
5892
+ chainState
5893
+ ]);
5894
+ const [selectedIdx, setSelectedIdx] = useState(0);
5895
+ const handleQueryChange = useCallback((next) => {
5896
+ setQuery(next);
5897
+ setSelectedIdx(0);
5898
+ }, []);
5899
+ const safeIndex = filtered.length === 0 ? 0 : Math.min(selectedIdx, filtered.length - 1);
5446
5900
  const commit = () => {
5447
5901
  const row = filtered[safeIndex];
5448
5902
  if (!row) return;
@@ -5478,6 +5932,26 @@ function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onAppl
5478
5932
  };
5479
5933
  }, [filtered, safeIndex]);
5480
5934
  useKeyboard((key) => {
5935
+ if (key.ctrl && key.shift && (key.name === "up" || key.name === "down")) {
5936
+ if (chainMode) {
5937
+ const row = filtered[safeIndex];
5938
+ if (row) {
5939
+ const keyOf = (entry) => providerModelKey(entry.provider, entry.model);
5940
+ const selectedKey = providerModelKey(row.providerKey, row.model.id);
5941
+ const delta = key.name === "up" ? -1 : 1;
5942
+ const reordered = reorderSelectedModel(chainState, selectedKey, delta, keyOf);
5943
+ if (reordered !== chainState) {
5944
+ setChainState([...reordered]);
5945
+ if (!query.trim()) {
5946
+ const served = new Set(catalog.map((entry) => providerModelKey(entry.providerKey, entry.model.id)));
5947
+ const next = reordered.filter((entry) => served.has(keyOf(entry))).findIndex((entry) => keyOf(entry) === selectedKey);
5948
+ if (next >= 0) setSelectedIdx(next);
5949
+ }
5950
+ }
5951
+ }
5952
+ }
5953
+ return;
5954
+ }
5481
5955
  if (key.name === "up") {
5482
5956
  setSelectedIdx((i) => {
5483
5957
  if (filtered.length === 0) return i;
@@ -5553,6 +6027,10 @@ function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onAppl
5553
6027
  children: "↵"
5554
6028
  }),
5555
6029
  chainMode ? " add/remove · " : " select · ",
6030
+ chainMode && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx$1("span", {
6031
+ fg: COLOR.warn,
6032
+ children: "ctrl+shift+↑↓"
6033
+ }), " reorder · "] }),
5556
6034
  /* @__PURE__ */ jsx$1("span", {
5557
6035
  fg: COLOR.warn,
5558
6036
  children: "esc"
@@ -5605,23 +6083,41 @@ function ModelSelectList({ catalog, multi = false, initial, onApply, onCancel, v
5605
6083
  return true;
5606
6084
  });
5607
6085
  }, [catalog]);
5608
- const filtered = useMemo(() => filterModelCatalog(deduped, query), [deduped, query]);
5609
- const [selectedIdx, setSelectedIdx] = useState(() => {
5610
- const first = initial[0];
5611
- const idx = first ? deduped.findIndex((e) => e.model.id === first) : -1;
5612
- return Math.max(0, idx);
6086
+ const [initialPicks] = useState(() => {
6087
+ const known = new Set(deduped.map((e) => e.model.id));
6088
+ return initial.filter((id) => known.has(id));
5613
6089
  });
6090
+ const [picks, setPicks] = useState(initialPicks);
6091
+ const filtered = useMemo(() => {
6092
+ const matches = filterModelCatalog(deduped, query);
6093
+ return query.trim() ? matches : selectedModelsFirst(matches, picks, (row) => row.model.id);
6094
+ }, [
6095
+ deduped,
6096
+ query,
6097
+ picks
6098
+ ]);
6099
+ const [selectedIdx, setSelectedIdx] = useState(0);
5614
6100
  const handleQueryChange = useCallback((next) => {
5615
6101
  setQuery(next);
5616
6102
  setSelectedIdx(0);
5617
6103
  }, []);
5618
6104
  const safeIndex = filtered.length === 0 ? 0 : Math.min(selectedIdx, filtered.length - 1);
5619
- const [initialPicks] = useState(() => {
5620
- const known = new Set(deduped.map((e) => e.model.id));
5621
- return initial.filter((id) => known.has(id));
5622
- });
5623
- const [picks, setPicks] = useState(initialPicks);
5624
6105
  useKeyboard((key) => {
6106
+ if (key.ctrl && key.shift && (key.name === "up" || key.name === "down")) {
6107
+ if (multi) {
6108
+ const row = filtered[safeIndex];
6109
+ if (row) {
6110
+ const delta = key.name === "up" ? -1 : 1;
6111
+ const from = picks.indexOf(row.model.id);
6112
+ const reordered = reorderSelectedModel(picks, row.model.id, delta, (id) => id);
6113
+ if (reordered !== picks) {
6114
+ setPicks(reordered);
6115
+ if (!query.trim()) setSelectedIdx(from + delta);
6116
+ }
6117
+ }
6118
+ }
6119
+ return;
6120
+ }
5625
6121
  if (key.name === "up" || key.name === "down") {
5626
6122
  setSelectedIdx((i) => {
5627
6123
  if (filtered.length === 0) return i;
@@ -5723,6 +6219,10 @@ function ModelSelectList({ catalog, multi = false, initial, onApply, onCancel, v
5723
6219
  children: "↵"
5724
6220
  }),
5725
6221
  multi ? " add/remove · " : " select · ",
6222
+ multi && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx$1("span", {
6223
+ fg: COLOR.warn,
6224
+ children: "ctrl+shift+↑↓"
6225
+ }), " reorder · "] }),
5726
6226
  /* @__PURE__ */ jsx$1("span", {
5727
6227
  fg: COLOR.warn,
5728
6228
  children: "esc"
@@ -9293,21 +9793,27 @@ function isSessionRowId(rowId) {
9293
9793
  }
9294
9794
  /** Page-up / page-down jump size — half a typical visible window. */
9295
9795
  const PAGE_JUMP = 6;
9296
- function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreate, onFocusChange, showAllProjects = false, onToggleScope, currentProjectRoot }) {
9796
+ function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreate, onFocusChange, contentMatches = /* @__PURE__ */ new Map(), contentSearching = false, contentSearchProgress = {
9797
+ done: 0,
9798
+ total: 0
9799
+ }, onSearchQueryChange, showAllProjects = false, onToggleScope, currentProjectRoot }) {
9297
9800
  const focused = useModalAwareFocus();
9298
9801
  const COLOR = useColors();
9299
9802
  const inputRef = useRef(null);
9300
- const [query, setQuery] = useState("");
9301
- const filteredSessions = useMemo(() => {
9302
- const trimmed = query.trim().toLowerCase();
9303
- if (!trimmed) return sessions;
9304
- const terms = trimmed.split(/\s+/);
9305
- return sessions.filter((meta) => {
9306
- const projectBasename = meta.projectRoot?.split("/").pop() ?? "";
9307
- const corpus = `${meta.title} ${projectBasename} ${meta.projectRoot ?? ""}`.toLowerCase();
9308
- return terms.every((t) => corpus.includes(t));
9309
- });
9310
- }, [sessions, query]);
9803
+ const scrollboxRef = useRef(null);
9804
+ const [query, setQueryState] = useState("");
9805
+ const searchChangeRef = useRef(onSearchQueryChange);
9806
+ searchChangeRef.current = onSearchQueryChange;
9807
+ useEffect(() => () => searchChangeRef.current?.(""), []);
9808
+ const setQuery = useCallback((next) => {
9809
+ setQueryState(next);
9810
+ onSearchQueryChange?.(next);
9811
+ }, [onSearchQueryChange]);
9812
+ const filteredSessions = useMemo(() => filterSessionSearchCatalog(sessions, query, contentMatches), [
9813
+ sessions,
9814
+ query,
9815
+ contentMatches
9816
+ ]);
9311
9817
  const rows = useMemo(() => [{
9312
9818
  kind: "new",
9313
9819
  rowId: NEW_SESSION_ROW_ID,
@@ -9315,8 +9821,9 @@ function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreat
9315
9821
  }, ...filteredSessions.map((meta) => ({
9316
9822
  kind: "session",
9317
9823
  rowId: meta.id,
9318
- meta
9319
- }))], [filteredSessions]);
9824
+ meta,
9825
+ ...query.trim() && contentMatches.get(meta.id) ? { contentMatch: contentMatches.get(meta.id) } : {}
9826
+ }))], [filteredSessions, contentMatches]);
9320
9827
  useEffect(() => {
9321
9828
  if (focused) inputRef.current?.focus();
9322
9829
  }, [focused]);
@@ -9328,6 +9835,33 @@ function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreat
9328
9835
  }
9329
9836
  return rows.length > 1 ? 1 : 0;
9330
9837
  }, [rows, focusedSessionId]);
9838
+ const lastScrolledCursorRef = useRef(-1);
9839
+ useEffect(() => {
9840
+ if (!focused) return;
9841
+ if (lastScrolledCursorRef.current === cursorIndex) return;
9842
+ const scrollbox = scrollboxRef.current;
9843
+ if (!scrollbox) return;
9844
+ let handle = 0;
9845
+ let attempts = 0;
9846
+ const align = () => {
9847
+ const child = scrollbox.content.findDescendantById(`session-row-${cursorIndex}`);
9848
+ if (child) {
9849
+ const childTop = child.y - scrollbox.content.y;
9850
+ const childBottom = childTop + child.height;
9851
+ if (childTop < scrollbox.scrollTop) scrollbox.scrollTop = childTop;
9852
+ else if (childBottom > scrollbox.scrollTop + scrollbox.viewport.height) scrollbox.scrollTop = childBottom - scrollbox.viewport.height;
9853
+ lastScrolledCursorRef.current = cursorIndex;
9854
+ return;
9855
+ }
9856
+ if (attempts++ < 5) handle = requestAnimationFrame(align);
9857
+ };
9858
+ handle = requestAnimationFrame(align);
9859
+ return () => cancelAnimationFrame(handle);
9860
+ }, [
9861
+ cursorIndex,
9862
+ focused,
9863
+ rows
9864
+ ]);
9331
9865
  useEffect(() => {
9332
9866
  if (!onFocusChange) return;
9333
9867
  if (focusedSessionId === "__new__") return;
@@ -9448,44 +9982,66 @@ function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreat
9448
9982
  flexDirection: "column",
9449
9983
  flexGrow: 1
9450
9984
  },
9451
- children: [/* @__PURE__ */ jsx$1("box", {
9452
- style: {
9453
- border: true,
9454
- borderColor: COLOR.border,
9455
- paddingLeft: 1,
9456
- paddingRight: 1,
9457
- height: 3,
9458
- flexShrink: 0,
9459
- marginBottom: 1
9460
- },
9461
- children: /* @__PURE__ */ jsx$1("input", {
9462
- ref: inputRef,
9463
- focused,
9464
- placeholder: "filter sessions — title or project…",
9465
- onInput: setQuery,
9466
- onSubmit: () => {},
9467
- style: { flexGrow: 1 }
9468
- })
9469
- }), /* @__PURE__ */ jsxs("scrollbox", {
9470
- focusable: false,
9471
- style: { flexGrow: 1 },
9472
- children: [rows.map((row, idx) => /* @__PURE__ */ jsx$1(SessionRow, {
9473
- row,
9474
- focused: idx === cursorIndex && focused,
9475
- isCurrent: row.kind === "session" && row.rowId === currentId,
9476
- showProject: showAllProjects,
9477
- currentProjectRoot
9478
- }, row.rowId)), filteredSessions.length === 0 && query.trim().length > 0 && /* @__PURE__ */ jsxs("text", {
9479
- wrapMode: "none",
9480
- children: [/* @__PURE__ */ jsx$1("span", {
9481
- fg: COLOR.mute,
9482
- children: " no sessions match "
9483
- }), /* @__PURE__ */ jsx$1("span", {
9484
- fg: COLOR.warn,
9485
- children: query.trim()
9985
+ children: [
9986
+ /* @__PURE__ */ jsx$1("box", {
9987
+ style: {
9988
+ border: true,
9989
+ borderColor: COLOR.border,
9990
+ paddingLeft: 1,
9991
+ paddingRight: 1,
9992
+ height: 3,
9993
+ flexShrink: 0,
9994
+ marginBottom: 1
9995
+ },
9996
+ children: /* @__PURE__ */ jsx$1("input", {
9997
+ id: "session-search-input",
9998
+ ref: inputRef,
9999
+ focused,
10000
+ placeholder: "search sessions — title, project, or message content…",
10001
+ onInput: setQuery,
10002
+ onSubmit: () => {},
10003
+ style: { flexGrow: 1 }
10004
+ })
10005
+ }),
10006
+ /* @__PURE__ */ jsxs("scrollbox", {
10007
+ ref: scrollboxRef,
10008
+ id: "sessions-scrollbox",
10009
+ focusable: false,
10010
+ style: { flexGrow: 1 },
10011
+ children: [rows.map((row, idx) => /* @__PURE__ */ jsx$1(SessionRow, {
10012
+ anchorId: `session-row-${idx}`,
10013
+ row,
10014
+ separator: idx > 0,
10015
+ focused: idx === cursorIndex && focused,
10016
+ isCurrent: row.kind === "session" && row.rowId === currentId,
10017
+ showProject: showAllProjects,
10018
+ currentProjectRoot
10019
+ }, row.rowId)), filteredSessions.length === 0 && query.trim().length > 0 && /* @__PURE__ */ jsxs("text", {
10020
+ wrapMode: "none",
10021
+ children: [
10022
+ /* @__PURE__ */ jsx$1("span", {
10023
+ fg: COLOR.mute,
10024
+ children: contentSearching ? " searching conversation history for " : " no sessions match "
10025
+ }),
10026
+ /* @__PURE__ */ jsx$1("span", {
10027
+ fg: COLOR.warn,
10028
+ children: query.trim()
10029
+ }),
10030
+ contentSearching && /* @__PURE__ */ jsx$1("span", {
10031
+ fg: COLOR.mute,
10032
+ children: "…"
10033
+ })
10034
+ ]
9486
10035
  })]
9487
- })]
9488
- })]
10036
+ }),
10037
+ /* @__PURE__ */ jsx$1("box", {
10038
+ style: {
10039
+ height: 1,
10040
+ flexShrink: 0
10041
+ },
10042
+ children: contentSearching && /* @__PURE__ */ jsx$1(Spinner, { label: `searching conversation history ${contentSearchProgress.done}/${contentSearchProgress.total}` })
10043
+ })
10044
+ ]
9489
10045
  }), /* @__PURE__ */ jsx$1(TitleOverlay, {
9490
10046
  title: "sessions",
9491
10047
  meta: titleMeta
@@ -9501,18 +10057,25 @@ function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreat
9501
10057
  * exactly the width of the focus + current markers above — so the first
9502
10058
  * stat (the turn count) sits flush under the title's first letter.
9503
10059
  */
9504
- function SessionRow({ row, focused, isCurrent, showProject = false, currentProjectRoot }) {
10060
+ function SessionRow({ row, anchorId, focused, isCurrent, separator = false, showProject = false, currentProjectRoot }) {
9505
10061
  const COLOR = useColors();
10062
+ const SURFACE = useSurfaces();
9506
10063
  const STATS_INDENT = " ";
9507
10064
  const focusMark = focused ? "▶ " : " ";
9508
10065
  const focusColor = focused ? COLOR.brand : COLOR.mute;
9509
10066
  const titleColor = focused ? COLOR.brand : COLOR.dim;
10067
+ const rowStyle = {
10068
+ flexDirection: "column",
10069
+ flexShrink: 0,
10070
+ alignSelf: "stretch",
10071
+ ...separator ? {
10072
+ border: ["top"],
10073
+ borderColor: COLOR.border
10074
+ } : {}
10075
+ };
9510
10076
  if (row.kind === "new") return /* @__PURE__ */ jsxs("box", {
9511
- style: {
9512
- flexDirection: "column",
9513
- flexShrink: 0,
9514
- alignSelf: "stretch"
9515
- },
10077
+ id: anchorId,
10078
+ style: rowStyle,
9516
10079
  children: [/* @__PURE__ */ jsxs("text", {
9517
10080
  wrapMode: "none",
9518
10081
  children: [
@@ -9544,11 +10107,8 @@ function SessionRow({ row, focused, isCurrent, showProject = false, currentProje
9544
10107
  const currentMark = isCurrent ? "● " : " ";
9545
10108
  const currentColor = isCurrent ? COLOR.accent : COLOR.mute;
9546
10109
  return /* @__PURE__ */ jsxs("box", {
9547
- style: {
9548
- flexDirection: "column",
9549
- flexShrink: 0,
9550
- alignSelf: "stretch"
9551
- },
10110
+ id: anchorId,
10111
+ style: rowStyle,
9552
10112
  children: [
9553
10113
  /* @__PURE__ */ jsxs("text", {
9554
10114
  wrapMode: "none",
@@ -9610,6 +10170,38 @@ function SessionRow({ row, focused, isCurrent, showProject = false, currentProje
9610
10170
  fg: COLOR.mute,
9611
10171
  children: STATS_INDENT
9612
10172
  }), renderProjectLabel(meta.projectRoot, currentProjectRoot, COLOR)]
10173
+ }),
10174
+ row.contentMatch && /* @__PURE__ */ jsxs("box", {
10175
+ style: {
10176
+ flexDirection: "column",
10177
+ flexShrink: 0,
10178
+ marginTop: 1,
10179
+ marginLeft: 4,
10180
+ paddingLeft: 1,
10181
+ border: ["left"],
10182
+ borderColor: COLOR.border
10183
+ },
10184
+ children: [/* @__PURE__ */ jsxs("text", {
10185
+ wrapMode: "none",
10186
+ children: [/* @__PURE__ */ jsx$1("span", {
10187
+ fg: COLOR.brand,
10188
+ children: "matches"
10189
+ }), /* @__PURE__ */ jsx$1("span", {
10190
+ fg: COLOR.mute,
10191
+ children: ` · ${row.contentMatch.matchedTurnIds.length} message${row.contentMatch.matchedTurnIds.length === 1 ? "" : "s"}`
10192
+ })]
10193
+ }), row.contentMatch.excerpts.slice(0, 3).map((excerpt, i) => /* @__PURE__ */ jsxs("text", {
10194
+ fg: COLOR.dim,
10195
+ wrapMode: "none",
10196
+ children: [/* @__PURE__ */ jsx$1("span", {
10197
+ fg: COLOR.mute,
10198
+ children: `${excerpt.role === "user" ? "you" : excerpt.role} · `
10199
+ }), highlightSegments(excerpt.text, excerpt.highlighted).map((segment, j) => /* @__PURE__ */ jsx$1("span", {
10200
+ fg: segment.highlighted ? COLOR.warn : COLOR.dim,
10201
+ ...segment.highlighted ? { bg: SURFACE.selection } : {},
10202
+ children: segment.text
10203
+ }, j))]
10204
+ }, `${excerpt.turnId}:${i}`))]
9613
10205
  })
9614
10206
  ]
9615
10207
  });
@@ -9707,7 +10299,10 @@ const MIME_BY_EXT = {
9707
10299
  * default-value reference at the prop destructure.
9708
10300
  */
9709
10301
  const EMPTY_QUEUED_MESSAGES = [];
9710
- function ChatScreen({ cwd, events, busy, compacting = false, queuedMessages = EMPTY_QUEUED_MESSAGES, queueSelectionIndex = null, queueShortcuts, onEnterQueueFromEmptyPrompt, settings, onSubmit, session, pending, onApproval, pendingInteraction, onInteraction, completionProviders, inlineAutocompleteProviders, onCompletionTab, enterSelectsCompletion, onPopupOpenChange, selectedTurnId, promptTriggerHints, attachImageBinding, editorBinding, sessionDetailsBinding, liveSession = null }) {
10302
+ function shouldRenderTranscriptSearch(searchMode, query) {
10303
+ return searchMode && query.trim().length > 0;
10304
+ }
10305
+ function ChatScreen({ cwd, events, busy, compacting = false, queuedMessages = EMPTY_QUEUED_MESSAGES, queueSelectionIndex = null, queueShortcuts, onEnterQueueFromEmptyPrompt, settings, onSubmit, session, pending, onApproval, pendingInteraction, onInteraction, completionProviders, inlineAutocompleteProviders, onCompletionTab, enterSelectsCompletion, onPopupOpenChange, selectedTurnId, transcriptSearchMode = false, transcriptSearchQuery = "", transcriptSearchEvents = [], transcriptSearchSearching = false, onTranscriptSearchModeChange, onTranscriptSearchQueryChange, onTranscriptSearchNavigate, onTranscriptSearchOpen, onLoadEarlierTranscript, promptTriggerHints, attachImageBinding, editorBinding, sessionDetailsBinding, liveSession = null }) {
9711
10306
  const COLOR = useColors();
9712
10307
  const titleText = session?.title ?? "New session";
9713
10308
  const sessionShortcutKey = formatBindingForDisplay(sessionDetailsBinding);
@@ -9765,6 +10360,8 @@ function ChatScreen({ cwd, events, busy, compacting = false, queuedMessages = EM
9765
10360
  hasStatusIcon
9766
10361
  ]);
9767
10362
  const userPrompts = useMemo(() => promptHistoryFromEvents(events), [events]);
10363
+ const transcriptSearchActive = shouldRenderTranscriptSearch(transcriptSearchMode, transcriptSearchQuery);
10364
+ const displayedEvents = transcriptSearchActive ? transcriptSearchEvents : events;
9768
10365
  const [completionPopupOpen, setCompletionPopupOpen] = useState(false);
9769
10366
  const handlePopupOpenChange = useCallback((open) => {
9770
10367
  setCompletionPopupOpen(open);
@@ -9792,10 +10389,13 @@ function ChatScreen({ cwd, events, busy, compacting = false, queuedMessages = EM
9792
10389
  visible: !fileEditPending
9793
10390
  },
9794
10391
  children: /* @__PURE__ */ jsx$1(Transcript, {
9795
- events,
10392
+ events: displayedEvents,
9796
10393
  settings,
9797
10394
  selectedTurnId: selectedTurnId ?? null,
9798
- busy: busy && !pending && !pendingInteraction
10395
+ searchQuery: transcriptSearchActive ? transcriptSearchQuery : void 0,
10396
+ searchPending: transcriptSearchSearching,
10397
+ busy: !transcriptSearchActive && busy && !pending && !pendingInteraction,
10398
+ onLoadEarlier: transcriptSearchActive ? void 0 : onLoadEarlierTranscript
9799
10399
  })
9800
10400
  }),
9801
10401
  /* @__PURE__ */ jsx$1(ExtensionStatusLine, {}),
@@ -9825,7 +10425,12 @@ function ChatScreen({ cwd, events, busy, compacting = false, queuedMessages = EM
9825
10425
  onCompletionTab,
9826
10426
  enterSelectsCompletion,
9827
10427
  onPopupOpenChange: handlePopupOpenChange,
9828
- selectMode: queueSelectionIndex != null ? "queue" : selectedTurnId != null ? "turn" : null,
10428
+ searchMode: transcriptSearchMode,
10429
+ onSearchModeChange: onTranscriptSearchModeChange,
10430
+ onSearchQueryChange: onTranscriptSearchQueryChange,
10431
+ onSearchNavigate: onTranscriptSearchNavigate,
10432
+ onSearchOpen: onTranscriptSearchOpen,
10433
+ selectMode: queueSelectionIndex != null ? "queue" : null,
9829
10434
  triggerHints: promptTriggerHints,
9830
10435
  busy,
9831
10436
  onEnterQueueFromEmpty: onEnterQueueFromEmptyPrompt,
@@ -10151,7 +10756,10 @@ const EMPTY_INLINE_PROVIDERS = [];
10151
10756
  * ghost feels immediate on a pause.
10152
10757
  */
10153
10758
  const INLINE_GHOST_DEBOUNCE_MS = 120;
10154
- function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocompleteProviders, onCompletionTab, enterSelectsCompletion, onPopupOpenChange, selectMode = null, triggerHints, busy = false, onEnterQueueFromEmpty, attachImageBinding, editorBinding }) {
10759
+ function isTranscriptSearchExitKey(event, cursorOffset) {
10760
+ return event.name === "escape" || (event.name === "backspace" || event.name === "delete") && !event.ctrl && !event.meta && !event.shift && cursorOffset === 0;
10761
+ }
10762
+ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocompleteProviders, onCompletionTab, enterSelectsCompletion, onPopupOpenChange, searchMode = false, onSearchModeChange, onSearchQueryChange, onSearchNavigate, onSearchOpen, selectMode = null, triggerHints, busy = false, onEnterQueueFromEmpty, attachImageBinding, editorBinding }) {
10155
10763
  const focused = useModalAwareFocus();
10156
10764
  const renderer = useRenderer();
10157
10765
  const COLOR = useColors();
@@ -10216,7 +10824,7 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
10216
10824
  * Once the user enters history (up at top), we snapshot the draft and cycle.
10217
10825
  */
10218
10826
  const historyRef = useRef(null);
10219
- const completion = useCompletion(bufferState, completionProviders ?? EMPTY_PROVIDERS);
10827
+ const completion = useCompletion(bufferState, searchMode ? EMPTY_PROVIDERS : completionProviders ?? EMPTY_PROVIDERS);
10220
10828
  const popupOpen = completion.active != null && completion.items.length > 0;
10221
10829
  useEffect(() => {
10222
10830
  onPopupOpenChange?.(popupOpen);
@@ -10225,9 +10833,38 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
10225
10833
  useChipHighlights(textareaRef, completion.references, chipStyle);
10226
10834
  const [shellMode, setShellMode] = useState(false);
10227
10835
  const shellModeRef = useRef(false);
10228
- shellModeRef.current = shellMode;
10229
- /** Last text pushed into bufferState — distinguishes "typed `!` on empty" from bulk sets. */
10836
+ const searchModeRef = useRef(searchMode);
10837
+ const previousSearchModeRef = useRef(searchMode);
10838
+ const preSearchTextRef = useRef(null);
10839
+ /** Last text pushed into bufferState — distinguishes typed sentinels from bulk sets. */
10230
10840
  const lastSyncedTextRef = useRef("");
10841
+ searchModeRef.current = searchMode;
10842
+ shellModeRef.current = shellMode;
10843
+ useEffect(() => {
10844
+ const textarea = textareaRef.current;
10845
+ if (!previousSearchModeRef.current && searchMode) {
10846
+ if (preSearchTextRef.current === null) {
10847
+ preSearchTextRef.current = textarea?.plainText ?? "";
10848
+ textarea?.clear();
10849
+ lastSyncedTextRef.current = "";
10850
+ setBufferState({
10851
+ text: "",
10852
+ cursor: 0
10853
+ });
10854
+ onSearchQueryChange?.("");
10855
+ }
10856
+ } else if (previousSearchModeRef.current && !searchMode) {
10857
+ const restored = preSearchTextRef.current ?? "";
10858
+ textarea?.setText(restored);
10859
+ lastSyncedTextRef.current = restored;
10860
+ setBufferState({
10861
+ text: restored,
10862
+ cursor: restored.length
10863
+ });
10864
+ preSearchTextRef.current = null;
10865
+ }
10866
+ previousSearchModeRef.current = searchMode;
10867
+ }, [searchMode, onSearchQueryChange]);
10231
10868
  /**
10232
10869
  * Flip shell mode with the ref mirror updated SYNCHRONOUSLY — callers
10233
10870
  * that mutate the buffer and re-run `syncBuffer` in the same tick
@@ -10237,6 +10874,15 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
10237
10874
  shellModeRef.current = on;
10238
10875
  setShellMode(on);
10239
10876
  }, []);
10877
+ const exitSearchMode = useCallback(() => {
10878
+ searchModeRef.current = false;
10879
+ onSearchQueryChange?.("");
10880
+ onSearchModeChange?.(false);
10881
+ }, [onSearchModeChange, onSearchQueryChange]);
10882
+ useKeyboard((key) => {
10883
+ if (!focused) return;
10884
+ if (searchModeRef.current && key.name === "escape") exitSearchMode();
10885
+ });
10240
10886
  const [ghost, setGhost] = useState(null);
10241
10887
  const ghostRef = useRef(null);
10242
10888
  ghostRef.current = ghost;
@@ -10248,11 +10894,19 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
10248
10894
  const syncBuffer = useCallback(() => {
10249
10895
  const ta = textareaRef.current;
10250
10896
  if (!ta) return;
10251
- if (!shellModeRef.current && ta.plainText === "!" && lastSyncedTextRef.current === "") {
10897
+ if (!searchModeRef.current && !shellModeRef.current && ta.plainText === "?" && lastSyncedTextRef.current === "") {
10898
+ ta.clear();
10899
+ preSearchTextRef.current = "";
10900
+ searchModeRef.current = true;
10901
+ onSearchModeChange?.(true);
10902
+ onSearchQueryChange?.("");
10903
+ }
10904
+ if (!searchModeRef.current && !shellModeRef.current && ta.plainText === "!" && lastSyncedTextRef.current === "") {
10252
10905
  ta.clear();
10253
10906
  setShellModeNow(true);
10254
10907
  }
10255
10908
  lastSyncedTextRef.current = ta.plainText;
10909
+ if (searchModeRef.current) onSearchQueryChange?.(ta.plainText);
10256
10910
  setBufferState({
10257
10911
  text: ta.plainText,
10258
10912
  cursor: ta.cursorOffset
@@ -10265,7 +10919,7 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
10265
10919
  const inlineEnabled = settings.inlineAutocomplete && inlineProviders.length > 0;
10266
10920
  useEffect(() => {
10267
10921
  const atWordBoundary = bufferState.cursor === bufferState.text.length || /\s/.test(bufferState.text[bufferState.cursor]);
10268
- if (!inlineEnabled || popupOpen || selectMode !== null || shellMode || bufferState.text.length === 0 || !atWordBoundary) {
10922
+ if (!inlineEnabled || popupOpen || selectMode !== null || shellMode || searchMode || bufferState.text.length === 0 || !atWordBoundary) {
10269
10923
  setGhost(null);
10270
10924
  return;
10271
10925
  }
@@ -10298,6 +10952,7 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
10298
10952
  popupOpen,
10299
10953
  selectMode,
10300
10954
  shellMode,
10955
+ searchMode,
10301
10956
  inlineProviders
10302
10957
  ]);
10303
10958
  /**
@@ -10333,6 +10988,7 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
10333
10988
  ]);
10334
10989
  const submit = useCallback(() => {
10335
10990
  const value = textareaRef.current?.plainText ?? "";
10991
+ if (searchModeRef.current) return;
10336
10992
  if (shellMode) {
10337
10993
  if (!value.trim()) return;
10338
10994
  onSubmit(`!${value}`, completion.references, attachments);
@@ -10500,6 +11156,24 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
10500
11156
  openExternalEditor();
10501
11157
  return;
10502
11158
  }
11159
+ if (searchModeRef.current) {
11160
+ if (!event.ctrl && !event.meta && !event.shift && (event.name === "up" || event.name === "down")) {
11161
+ event.preventDefault();
11162
+ onSearchNavigate?.(event.name === "up" ? -1 : 1);
11163
+ return;
11164
+ }
11165
+ if (!event.ctrl && !event.meta && !event.shift && event.name === "return") {
11166
+ event.preventDefault();
11167
+ onSearchOpen?.();
11168
+ return;
11169
+ }
11170
+ if (isTranscriptSearchExitKey(event, textareaRef.current?.cursorOffset ?? 0)) {
11171
+ event.preventDefault();
11172
+ exitSearchMode();
11173
+ return;
11174
+ }
11175
+ return;
11176
+ }
10503
11177
  if (event.name === "backspace" && !event.ctrl && !event.meta && !event.shift && shellModeRef.current && (textareaRef.current?.cursorOffset ?? 0) === 0) {
10504
11178
  event.preventDefault();
10505
11179
  setShellModeNow(false);
@@ -10610,7 +11284,10 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
10610
11284
  editorBinding,
10611
11285
  openExternalEditor,
10612
11286
  attachments,
10613
- showNotice
11287
+ showNotice,
11288
+ onSearchNavigate,
11289
+ onSearchOpen,
11290
+ exitSearchMode
10614
11291
  ]);
10615
11292
  const ghostPosition = useMemo(() => {
10616
11293
  if (!ghost) return null;
@@ -10691,7 +11368,7 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
10691
11368
  children: [/* @__PURE__ */ jsxs("box", {
10692
11369
  style: {
10693
11370
  border: true,
10694
- borderColor: selectMode ? COLOR.warn : shellMode ? COLOR.accent : COLOR.borderActive,
11371
+ borderColor: selectMode ? COLOR.warn : searchMode ? COLOR.warn : shellMode ? COLOR.accent : COLOR.borderActive,
10695
11372
  paddingLeft: 1,
10696
11373
  paddingRight: 1,
10697
11374
  height: boxHeight,
@@ -10702,7 +11379,7 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
10702
11379
  focused: focused && !selectMode,
10703
11380
  keyBindings: TEXTAREA_BINDINGS,
10704
11381
  wrapMode: "word",
10705
- placeholder: selectMode === "turn" ? "— turn-select mode — press ⎋ to resume typing —" : selectMode === "queue" ? " queue-select mode — press ⎋ to resume typing —" : shellMode ? "Run a shell command…" : "Ask zidane…",
11382
+ placeholder: selectMode === "queue" ? "— queue-select mode — press ⎋ to resume typing —" : searchMode ? "Search this conversation…" : shellMode ? "Run a shell command…" : "Ask zidane…",
10706
11383
  syntaxStyle: chipStyle,
10707
11384
  style: {
10708
11385
  flexGrow: 1,
@@ -10732,12 +11409,13 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
10732
11409
  })]
10733
11410
  }), /* @__PURE__ */ jsx$1(PromptHints, {
10734
11411
  selectMode,
11412
+ searchMode,
10735
11413
  triggerHints,
10736
11414
  busy
10737
11415
  })]
10738
11416
  })
10739
11417
  ]
10740
- }), !selectMode && /* @__PURE__ */ jsx$1("box", {
11418
+ }), !selectMode && !searchMode && /* @__PURE__ */ jsx$1("box", {
10741
11419
  style: {
10742
11420
  position: "absolute",
10743
11421
  bottom: "100%",
@@ -10764,6 +11442,10 @@ const PROMPT_HINTS_NORMAL = [
10764
11442
  key: "↑↓",
10765
11443
  label: "history"
10766
11444
  },
11445
+ {
11446
+ key: "?",
11447
+ label: "search"
11448
+ },
10767
11449
  {
10768
11450
  key: "ctrl+s",
10769
11451
  label: "messages"
@@ -10792,8 +11474,8 @@ const PROMPT_HINTS_BUSY = [
10792
11474
  label: "abort"
10793
11475
  }
10794
11476
  ];
10795
- /** Prompt-box shortcuts in select-turn mode only the selection actions are valid. */
10796
- const PROMPT_HINTS_SELECT_TURN = [
11477
+ /** Conversation search is the message selector with an optional filter. */
11478
+ const PROMPT_HINTS_SEARCH = [
10797
11479
  {
10798
11480
  key: "↑↓",
10799
11481
  label: "navigate"
@@ -10838,18 +11520,19 @@ const PROMPT_HINTS_SELECT_QUEUE = [{
10838
11520
  * Swaps the hint set based on `selectMode` so the user sees only the
10839
11521
  * shortcuts that actually do something in the active mode.
10840
11522
  */
10841
- function PromptHints({ selectMode, triggerHints, busy = false }) {
11523
+ function PromptHints({ selectMode, searchMode = false, triggerHints, busy = false }) {
10842
11524
  const COLOR = useColors();
10843
11525
  const { settings } = useSettings();
10844
11526
  const { width: termWidth } = useTerminalDimensions();
10845
- const minimalResting = !selectMode && !busy && settings.uiMode === "minimal";
10846
- const primary = selectMode === "turn" ? PROMPT_HINTS_SELECT_TURN : selectMode === "queue" ? PROMPT_HINTS_SELECT_QUEUE : busy ? PROMPT_HINTS_BUSY : minimalResting ? EMPTY_HINTS : PROMPT_HINTS_NORMAL;
11527
+ const minimalResting = !selectMode && !searchMode && !busy && settings.uiMode === "minimal";
11528
+ const primary = searchMode ? PROMPT_HINTS_SEARCH : selectMode === "queue" ? PROMPT_HINTS_SELECT_QUEUE : busy ? PROMPT_HINTS_BUSY : minimalResting ? EMPTY_HINTS : PROMPT_HINTS_NORMAL;
10847
11529
  const hints = useMemo(() => {
10848
11530
  const budget = Math.max(0, termWidth - 5);
10849
11531
  const tooLongCombined = !!triggerHints && triggerHints.length > 0 && !selectMode && primary.length > 0 && hintsLength(primary) + 3 + hintsLength(triggerHints) > budget;
10850
11532
  return clipHintsToWidth(selectMode || !triggerHints || triggerHints.length === 0 || tooLongCombined ? primary : [...primary, ...triggerHints], budget);
10851
11533
  }, [
10852
11534
  selectMode,
11535
+ searchMode,
10853
11536
  primary,
10854
11537
  triggerHints,
10855
11538
  termWidth
@@ -15495,7 +16178,8 @@ const PREVIEW_CHAR_MAX = 8e3;
15495
16178
  * the Modal's own clamp; this is the cap on wide terminals so the modal
15496
16179
  * keeps a comfortable shape rather than stretching to the full height.
15497
16180
  */
15498
- const MAX_MODAL_HEIGHT = 28;
16181
+ const MAX_MODAL_HEIGHT = 46;
16182
+ const MAX_MODAL_WIDTH = 148;
15499
16183
  const EDIT_TEXTAREA_BINDINGS = (() => {
15500
16184
  return [
15501
16185
  ...defaultTextareaKeyBindings.filter((b) => b.name !== "return" && !(b.name === "a" && b.ctrl && !b.shift && !b.meta)),
@@ -15523,12 +16207,18 @@ const EDIT_TEXTAREA_BINDINGS = (() => {
15523
16207
  function editableText(turn) {
15524
16208
  return turn.content.filter((b) => b.type === "text").map((b) => b.text).join("\n\n");
15525
16209
  }
15526
- function TurnDetailsModal({ turn, index, total, actions, keybindings }) {
16210
+ function TurnDetailsModal({ turn, toolResultTurns = [], searchQuery, index, total, actions, keybindings }) {
15527
16211
  const COLOR = useColors();
16212
+ const SURFACE = useSurfaces();
15528
16213
  const modal = useModal();
15529
- const fullText = turnAsText(turn);
15530
- const preview = fullText.length > PREVIEW_CHAR_MAX ? `${fullText.slice(0, PREVIEW_CHAR_MAX)}\n\n…(${fullText.length - PREVIEW_CHAR_MAX} more chars)` : fullText;
15531
- const summary = blockSummary(turn);
16214
+ const { fullText, previewEntries, summary } = useMemo(() => {
16215
+ const previewTurns = [turn, ...toolResultTurns];
16216
+ return {
16217
+ fullText: previewTurns.map(turnAsText).filter(Boolean).join("\n\n"),
16218
+ previewEntries: buildPreviewEntries(previewTurns, PREVIEW_CHAR_MAX),
16219
+ summary: blockSummary(previewTurns)
16220
+ };
16221
+ }, [turn, toolResultTurns]);
15532
16222
  const bottomTitle = `${index - 1} before · ${total - index} after`;
15533
16223
  const [pending, setPending] = useState(null);
15534
16224
  const [copyStatus, setCopyStatus] = useState("idle");
@@ -15594,6 +16284,10 @@ function TurnDetailsModal({ turn, index, total, actions, keybindings }) {
15594
16284
  title: editing ? `edit turn ${index} / ${total} · ${turn.role}` : `turn ${index} / ${total} · ${turn.role}`,
15595
16285
  bottomTitle: editing ? void 0 : bottomTitle,
15596
16286
  maxHeight: MAX_MODAL_HEIGHT,
16287
+ maxWidth: MAX_MODAL_WIDTH,
16288
+ minWidth: 64,
16289
+ horizontalMargin: 2,
16290
+ verticalMargin: 1,
15597
16291
  disableEscape: editing || pending !== null,
15598
16292
  children: [
15599
16293
  !editing && /* @__PURE__ */ jsxs("text", {
@@ -15673,7 +16367,8 @@ function TurnDetailsModal({ turn, index, total, actions, keybindings }) {
15673
16367
  title: " preview ",
15674
16368
  style: {
15675
16369
  border: true,
15676
- borderColor: COLOR.mute,
16370
+ borderColor: COLOR.borderActive,
16371
+ backgroundColor: SURFACE.background,
15677
16372
  paddingLeft: 1,
15678
16373
  paddingRight: 1,
15679
16374
  flexDirection: "column",
@@ -15681,17 +16376,38 @@ function TurnDetailsModal({ turn, index, total, actions, keybindings }) {
15681
16376
  flexShrink: 1,
15682
16377
  minHeight: 5
15683
16378
  },
15684
- children: preview ? /* @__PURE__ */ jsx$1("scrollbox", {
16379
+ children: previewEntries.length > 0 ? /* @__PURE__ */ jsx$1("scrollbox", {
15685
16380
  focusable: false,
15686
- style: { flexGrow: 1 },
16381
+ style: {
16382
+ flexGrow: 1,
16383
+ rootOptions: { backgroundColor: SURFACE.background },
16384
+ contentOptions: { flexDirection: "column" }
16385
+ },
15687
16386
  stickyScroll: false,
15688
- children: /* @__PURE__ */ jsx$1("text", {
15689
- fg: COLOR.dim,
15690
- children: preview
15691
- })
16387
+ children: previewEntries.map((entry, i) => /* @__PURE__ */ jsxs("box", {
16388
+ style: {
16389
+ flexDirection: "column",
16390
+ marginBottom: i === previewEntries.length - 1 ? 0 : 1
16391
+ },
16392
+ children: [/* @__PURE__ */ jsx$1("text", {
16393
+ fg: entry.error ? COLOR.error : entry.kind === "tool-result" ? COLOR.accent : entry.kind === "tool-call" ? COLOR.warn : COLOR.model,
16394
+ children: /* @__PURE__ */ jsx$1(HighlightedPreviewText, {
16395
+ text: entry.label,
16396
+ query: searchQuery,
16397
+ foreground: entry.error ? COLOR.error : entry.kind === "tool-result" ? COLOR.accent : entry.kind === "tool-call" ? COLOR.warn : COLOR.model
16398
+ })
16399
+ }), entry.text && /* @__PURE__ */ jsx$1("text", {
16400
+ fg: entry.error ? COLOR.error : COLOR.dim,
16401
+ children: /* @__PURE__ */ jsx$1(HighlightedPreviewText, {
16402
+ text: entry.text,
16403
+ query: searchQuery,
16404
+ foreground: entry.error ? COLOR.error : COLOR.dim
16405
+ })
16406
+ })]
16407
+ }, `${entry.kind}:${i}`))
15692
16408
  }) : /* @__PURE__ */ jsx$1("text", {
15693
16409
  fg: COLOR.mute,
15694
- children: "— no text content —"
16410
+ children: "— no previewable content —"
15695
16411
  })
15696
16412
  }),
15697
16413
  editing ? /* @__PURE__ */ jsxs("text", {
@@ -15851,14 +16567,97 @@ function ActionRow({ pending, copyStatus, canCopy, canEdit, forkKey, deleteKey,
15851
16567
  ]
15852
16568
  });
15853
16569
  }
15854
- /**
15855
- * Human-readable per-kind block tally — e.g. `1 text · 2 tool_call`. Skips
15856
- * zero-count kinds so the line stays scannable; uses canonical block-type
15857
- * names so they line up with what the LLM and persistence layer see.
15858
- */
15859
- function blockSummary(turn) {
16570
+ function buildPreviewEntries(turns, limit) {
16571
+ const entries = [];
16572
+ let remaining = limit;
16573
+ let skipped = 0;
16574
+ const add = (entry) => {
16575
+ if (remaining <= 0) {
16576
+ skipped++;
16577
+ return;
16578
+ }
16579
+ const text = entry.text.length > remaining ? `${entry.text.slice(0, remaining)}\n… (${entry.text.length - remaining} more chars)` : entry.text;
16580
+ entries.push({
16581
+ ...entry,
16582
+ text
16583
+ });
16584
+ remaining -= Math.min(entry.text.length, remaining);
16585
+ };
16586
+ for (const turn of turns) for (const block of turn.content) if (block.type === "text") add({
16587
+ kind: "text",
16588
+ label: turn.role === "user" ? "you" : turn.role,
16589
+ text: block.text
16590
+ });
16591
+ else if (block.type === "thinking") add({
16592
+ kind: "thinking",
16593
+ label: "thinking",
16594
+ text: block.text
16595
+ });
16596
+ else if (block.type === "tool_call") add({
16597
+ kind: "tool-call",
16598
+ label: `tool call · ${block.name}`,
16599
+ text: stringifyPreview(block.input)
16600
+ });
16601
+ else if (block.type === "tool_result") add({
16602
+ kind: "tool-result",
16603
+ label: block.isError ? "tool result · error" : "tool result",
16604
+ text: toolResultPreview(block.output),
16605
+ error: block.isError
16606
+ });
16607
+ else if (block.type === "server_tool_use") add({
16608
+ kind: "tool-call",
16609
+ label: `server tool call · ${block.name}`,
16610
+ text: stringifyPreview(block.input)
16611
+ });
16612
+ else if (block.type === "server_tool_result") add({
16613
+ kind: "tool-result",
16614
+ label: `server tool result · ${block.toolName}`,
16615
+ text: stringifyPreview(block.content)
16616
+ });
16617
+ else if (block.type === "compact-summary") add({
16618
+ kind: "summary",
16619
+ label: `compaction summary · ${block.replacesTurnIds.length} turns`,
16620
+ text: block.summary
16621
+ });
16622
+ else if (block.type === "image" || block.type === "audio" || block.type === "video" || block.type === "document") add({
16623
+ kind: "attachment",
16624
+ label: `${block.type} attachment`,
16625
+ text: block.name ?? block.mediaType
16626
+ });
16627
+ else skipped++;
16628
+ if (skipped > 0) entries.push({
16629
+ kind: "summary",
16630
+ label: `… ${skipped} more block${skipped === 1 ? "" : "s"} not shown`,
16631
+ text: ""
16632
+ });
16633
+ return entries;
16634
+ }
16635
+ function HighlightedPreviewText({ text, query, foreground }) {
16636
+ const matchColor = resolveChipColor(useSurfaces().chips, "default");
16637
+ return /* @__PURE__ */ jsx$1(Fragment, { children: highlightSegments(text, query ? searchHighlightRanges(text, query) : []).map((segment, i) => /* @__PURE__ */ jsx$1("span", {
16638
+ fg: segment.highlighted ? matchColor.fg : foreground,
16639
+ bg: segment.highlighted ? matchColor.bg : void 0,
16640
+ children: segment.text
16641
+ }, i)) });
16642
+ }
16643
+ function toolResultPreview(output) {
16644
+ if (typeof output === "string") return output;
16645
+ return output.map((part) => {
16646
+ if (part.type === "text") return part.text;
16647
+ const name = "name" in part && part.name ? `: ${part.name}` : "";
16648
+ return `[${part.type} attachment${name}]`;
16649
+ }).join("\n");
16650
+ }
16651
+ function stringifyPreview(value) {
16652
+ try {
16653
+ return JSON.stringify(value, null, 2);
16654
+ } catch {
16655
+ return String(value);
16656
+ }
16657
+ }
16658
+ function blockSummary(turns) {
15860
16659
  const counts = {};
15861
- for (const block of turn.content) counts[block.type] = (counts[block.type] ?? 0) + 1;
16660
+ for (const turn of turns) for (const block of turn.content) counts[block.type] = (counts[block.type] ?? 0) + 1;
15862
16661
  const parts = [];
15863
16662
  for (const [type, n] of Object.entries(counts)) parts.push(`${n} ${type}`);
15864
16663
  return parts.length === 0 ? "(empty)" : parts.join(" · ");
@@ -15914,6 +16713,8 @@ function lastLines(text, n) {
15914
16713
  * and lstats — cheap enough that latency, not cost, picked the number.
15915
16714
  */
15916
16715
  const EXTENSION_WATCH_POLL_MS = 3e3;
16716
+ /** LRU bound for cross-session search indexes (full lowercased text per session). */
16717
+ const SESSION_SEARCH_INDEX_CACHE_MAX = 64;
15917
16718
  /**
15918
16719
  * Surface failures that are normally silenced (teardown / save) when the
15919
16720
  * `ZIDANE_DEBUG` env var is set. Logging via `console.error` would otherwise
@@ -16513,6 +17314,19 @@ function AppShell() {
16513
17314
  const pickedRef = useRef(picked);
16514
17315
  pickedRef.current = picked;
16515
17316
  const [sessions, setSessions] = useState([]);
17317
+ const [sessionContentMatches, setSessionContentMatches] = useState(/* @__PURE__ */ new Map());
17318
+ const [sessionContentSearching, setSessionContentSearching] = useState(false);
17319
+ const [sessionContentSearchProgress, setSessionContentSearchProgress] = useState({
17320
+ done: 0,
17321
+ total: 0
17322
+ });
17323
+ const sessionSearchTimerRef = useRef(null);
17324
+ const sessionSearchQueryRef = useRef("");
17325
+ const sessionSearchGenerationRef = useRef(0);
17326
+ const sessionSearchAbortRef = useRef(null);
17327
+ const historySearchWorkerRef = useRef(null);
17328
+ const sessionSearchCacheRef = useRef(/* @__PURE__ */ new Map());
17329
+ const liveSessionSearchIndexRef = useRef(null);
16516
17330
  const [currentSession, setCurrentSession] = useState(null);
16517
17331
  const [chatReferenceScope, setChatReferenceScope] = useState("project");
16518
17332
  const [projectChatCatalog, setProjectChatCatalog] = useState([]);
@@ -16522,6 +17336,16 @@ function AppShell() {
16522
17336
  const allChatCatalogRef = useRef(allChatCatalog);
16523
17337
  allChatCatalogRef.current = allChatCatalog;
16524
17338
  const [events, setEvents] = useState([]);
17339
+ const eventsRef = useRef(events);
17340
+ eventsRef.current = events;
17341
+ /**
17342
+ * True while the user is browsing revealed history (`onLoadEarlierTranscript`)
17343
+ * and no new run has started. Suspends the stream buffer's display cap so a
17344
+ * trailing smooth-mode drain (typewriter backlog outliving `runningRef`)
17345
+ * can't re-trim the page that was just loaded. Cleared wherever a run
17346
+ * starts and on session switch (`stream.reset()`), where the cap resumes.
17347
+ */
17348
+ const transcriptRevealHoldRef = useRef(false);
16525
17349
  const [busy, setBusy] = useState(false);
16526
17350
  /**
16527
17351
  * The provider+model the run actually advanced to after a `model:fallback`
@@ -16765,12 +17589,17 @@ function AppShell() {
16765
17589
  */
16766
17590
  const lastInputTokensRef = useRef(0);
16767
17591
  /**
16768
- * Active turn id when the user is in "select turn" mode (ctrl+s on the
16769
- * chat screen). `null` means normal mode typing is enabled, transcript
16770
- * has no highlight. When set, the prompt textarea is unfocused so up/down
16771
- * navigate the turn list, ↵ opens the details modal, ⎋ exits the mode.
17592
+ * Active turn in conversation-search mode. The prompt remains focused for
17593
+ * filtering while plain arrows navigate and Enter opens turn details.
16772
17594
  */
16773
17595
  const [selectedTurnId, setSelectedTurnId] = useState(null);
17596
+ const [transcriptSearchMode, setTranscriptSearchMode] = useState(false);
17597
+ const [transcriptSearchQuery, setTranscriptSearchQuery] = useState("");
17598
+ const [transcriptSearchResults, setTranscriptSearchResults] = useState([]);
17599
+ const [transcriptSearchSearching, setTranscriptSearchSearching] = useState(false);
17600
+ const transcriptSearchGenerationRef = useRef(0);
17601
+ const transcriptSearchAbortRef = useRef(null);
17602
+ const transcriptSearchIndexRef = useRef(null);
16774
17603
  /**
16775
17604
  * Highlighted entry in the type-ahead queue box. `null` means the user
16776
17605
  * is in normal mode (textarea focused, queue purely informational);
@@ -17042,7 +17871,10 @@ function AppShell() {
17042
17871
  * writes a title to a torn-down session.
17043
17872
  */
17044
17873
  const autoTitleAbortRef = useRef(null);
17045
- const stream = useStreamBuffer(setEvents, { getSmooth: useCallback(() => smoothStreamingRef.current, []) });
17874
+ const stream = useStreamBuffer(setEvents, {
17875
+ getSmooth: useCallback(() => smoothStreamingRef.current, []),
17876
+ maxTranscriptEvents: transcriptRevealHoldRef.current ? null : void 0
17877
+ });
17046
17878
  streamRef.current = stream;
17047
17879
  const makePicked = useCallback((provider, modelId) => {
17048
17880
  const descriptor = providerRegistry[provider.key];
@@ -17801,6 +18633,113 @@ function AppShell() {
17801
18633
  const toggleSessionsScope = useCallback(() => {
17802
18634
  setSessionsScopeAll(!sessionsShowAll);
17803
18635
  }, [sessionsShowAll]);
18636
+ /** Debounced, cached full-history search for the conversations list. */
18637
+ const onSessionSearchQueryChange = useCallback((raw) => {
18638
+ sessionSearchQueryRef.current = raw;
18639
+ const query = raw.trim();
18640
+ const generation = ++sessionSearchGenerationRef.current;
18641
+ sessionSearchAbortRef.current?.abort();
18642
+ const abortController = new AbortController();
18643
+ sessionSearchAbortRef.current = abortController;
18644
+ if (sessionSearchTimerRef.current) clearTimeout(sessionSearchTimerRef.current);
18645
+ if (!query) {
18646
+ setSessionContentMatches(/* @__PURE__ */ new Map());
18647
+ setSessionContentSearching(false);
18648
+ setSessionContentSearchProgress({
18649
+ done: 0,
18650
+ total: 0
18651
+ });
18652
+ return;
18653
+ }
18654
+ setSessionContentMatches(/* @__PURE__ */ new Map());
18655
+ setSessionContentSearching(true);
18656
+ setSessionContentSearchProgress({
18657
+ done: 0,
18658
+ total: sessions.length
18659
+ });
18660
+ sessionSearchTimerRef.current = setTimeout(() => {
18661
+ (async () => {
18662
+ const matches = /* @__PURE__ */ new Map();
18663
+ let cursor = 0;
18664
+ let done = 0;
18665
+ const worker = async () => {
18666
+ while (cursor < sessions.length && generation === sessionSearchGenerationRef.current) {
18667
+ const meta = sessions[cursor++];
18668
+ try {
18669
+ let index;
18670
+ const live = sessionRef.current;
18671
+ const cached = sessionSearchCacheRef.current.get(meta.id);
18672
+ if (live?.id === meta.id) {
18673
+ const cachedLive = liveSessionSearchIndexRef.current;
18674
+ if (cachedLive?.sessionId === meta.id && cachedLive.updatedAt === meta.updatedAt && cachedLive.turnCount === live.turns.length) index = cachedLive.index;
18675
+ else {
18676
+ index = await (historySearchWorkerRef.current ??= new HistorySearchWorkerClient()).build(live.turns, abortController.signal);
18677
+ liveSessionSearchIndexRef.current = {
18678
+ sessionId: meta.id,
18679
+ updatedAt: meta.updatedAt,
18680
+ turnCount: live.turns.length,
18681
+ index
18682
+ };
18683
+ }
18684
+ } else if (cached?.updatedAt === meta.updatedAt) {
18685
+ index = cached.index;
18686
+ sessionSearchCacheRef.current.delete(meta.id);
18687
+ sessionSearchCacheRef.current.set(meta.id, cached);
18688
+ } else {
18689
+ const turns = await store.getTurns(meta.id);
18690
+ index = await (historySearchWorkerRef.current ??= new HistorySearchWorkerClient()).build(turns, abortController.signal);
18691
+ const cache = sessionSearchCacheRef.current;
18692
+ cache.set(meta.id, {
18693
+ updatedAt: meta.updatedAt,
18694
+ index
18695
+ });
18696
+ while (cache.size > SESSION_SEARCH_INDEX_CACHE_MAX) {
18697
+ const oldest = cache.keys().next().value;
18698
+ if (oldest === void 0) break;
18699
+ cache.delete(oldest);
18700
+ }
18701
+ }
18702
+ if (generation !== sessionSearchGenerationRef.current) return;
18703
+ const match = await (historySearchWorkerRef.current ??= new HistorySearchWorkerClient()).history(index, query, 3, abortController.signal);
18704
+ if (match) matches.set(meta.id, match);
18705
+ } catch (error) {
18706
+ if (abortController.signal.aborted) throw abortController.signal.reason instanceof DOMException ? abortController.signal.reason : error;
18707
+ debugLog("session content search skipped a session", error);
18708
+ }
18709
+ done++;
18710
+ if (done === sessions.length || done % 4 === 0) setSessionContentSearchProgress({
18711
+ done,
18712
+ total: sessions.length
18713
+ });
18714
+ }
18715
+ };
18716
+ try {
18717
+ await Promise.all(Array.from({ length: Math.min(6, sessions.length) }, worker));
18718
+ } catch (error) {
18719
+ if (!(error instanceof DOMException && error.name === "AbortError")) throw error;
18720
+ return;
18721
+ }
18722
+ if (generation !== sessionSearchGenerationRef.current) return;
18723
+ setSessionContentMatches(matches);
18724
+ setSessionContentSearchProgress({
18725
+ done: sessions.length,
18726
+ total: sessions.length
18727
+ });
18728
+ setSessionContentSearching(false);
18729
+ })().catch((error) => {
18730
+ debugLog("session history search failed", error);
18731
+ if (generation === sessionSearchGenerationRef.current) setSessionContentSearching(false);
18732
+ });
18733
+ }, 120);
18734
+ }, [sessions, store]);
18735
+ useEffect(() => {
18736
+ if (sessionSearchQueryRef.current.trim()) onSessionSearchQueryChange(sessionSearchQueryRef.current);
18737
+ }, [sessions, onSessionSearchQueryChange]);
18738
+ useEffect(() => () => {
18739
+ if (sessionSearchTimerRef.current) clearTimeout(sessionSearchTimerRef.current);
18740
+ sessionSearchAbortRef.current?.abort();
18741
+ historySearchWorkerRef.current?.terminate();
18742
+ }, []);
17804
18743
  const refreshSessions = useCallback(async () => {
17805
18744
  const list = await listSessionMeta(store, sessionsShowAll ? void 0 : { projectRoot: projectDir });
17806
18745
  setSessions(list);
@@ -17845,6 +18784,7 @@ function AppShell() {
17845
18784
  debugLog("teardown: auto-title abort failed", err);
17846
18785
  }
17847
18786
  autoTitleAbortRef.current = null;
18787
+ transcriptRevealHoldRef.current = false;
17848
18788
  stream.reset();
17849
18789
  await agentRef.current?.destroy().catch((err) => debugLog("agent.destroy failed", err));
17850
18790
  agentRef.current = null;
@@ -17908,6 +18848,7 @@ function AppShell() {
17908
18848
  await session.appendTurns([turn]);
17909
18849
  setEvents(capTranscriptEvents(eventsFromTurns(session.turns, session.runs)));
17910
18850
  runningRef.current = true;
18851
+ transcriptRevealHoldRef.current = false;
17911
18852
  setBusy(true);
17912
18853
  setActiveFallback(null);
17913
18854
  try {
@@ -18309,6 +19250,25 @@ function AppShell() {
18309
19250
  const onPopupOpenChange = useCallback((open) => {
18310
19251
  popupOpenRef.current = open;
18311
19252
  }, []);
19253
+ /**
19254
+ * Reveal one persisted-history page when Transcript reaches its top marker.
19255
+ * No full-history array is retained: materialize from Session on demand,
19256
+ * slice, then let the temporary array go. A running stream deliberately
19257
+ * declines — its first next append would re-cap anyway, and changing the
19258
+ * head while buckets are draining makes scroll preservation misleading.
19259
+ */
19260
+ const onLoadEarlierTranscript = useCallback(() => {
19261
+ if (runningRef.current) return false;
19262
+ const session = sessionRef.current;
19263
+ const displayed = eventsRef.current;
19264
+ if (!session || hiddenEarlierTranscriptCount(displayed) <= 0) return false;
19265
+ const next = revealEarlierTranscriptEvents(eventsFromTurns(session.turns, session.runs), displayed);
19266
+ if (next === displayed) return false;
19267
+ transcriptRevealHoldRef.current = true;
19268
+ eventsRef.current = next;
19269
+ setEvents(next);
19270
+ return true;
19271
+ }, []);
18312
19272
  const onAbort = useCallback(() => {
18313
19273
  if (popupOpenRef.current) return;
18314
19274
  commandAbortRef.current?.abort();
@@ -19248,6 +20208,7 @@ function AppShell() {
19248
20208
  if (!agent || !session || !picked) return;
19249
20209
  const wasRunning = runningRef.current;
19250
20210
  if (!wasRunning) runningRef.current = true;
20211
+ transcriptRevealHoldRef.current = false;
19251
20212
  suppressWakeRef.current = false;
19252
20213
  (async () => {
19253
20214
  let effectivePrompt = prompt;
@@ -19398,6 +20359,7 @@ function AppShell() {
19398
20359
  if (suppressWakeRef.current) return;
19399
20360
  if (!agent.hasPendingTaskNotifications) return;
19400
20361
  runningRef.current = true;
20362
+ transcriptRevealHoldRef.current = false;
19401
20363
  (async () => {
19402
20364
  setBusy(true);
19403
20365
  try {
@@ -19606,25 +20568,143 @@ function AppShell() {
19606
20568
  }, [modal, config.paths.userDir]);
19607
20569
  const hasMultipleAgents = useMemo(() => Object.keys(liveAgentRegistry).length > 1, [liveAgentRegistry]);
19608
20570
  const turnIds = useMemo(() => selectableTurnIds(events, settings), [events, settings]);
20571
+ useEffect(() => {
20572
+ const query = transcriptSearchQuery.trim();
20573
+ const generation = ++transcriptSearchGenerationRef.current;
20574
+ transcriptSearchAbortRef.current?.abort();
20575
+ if (!transcriptSearchMode || !query) {
20576
+ setTranscriptSearchResults([]);
20577
+ setTranscriptSearchSearching(false);
20578
+ return;
20579
+ }
20580
+ const session = sessionRef.current;
20581
+ if (!session) {
20582
+ setTranscriptSearchResults([]);
20583
+ setTranscriptSearchSearching(false);
20584
+ return;
20585
+ }
20586
+ const turns = session.turns;
20587
+ const abortController = new AbortController();
20588
+ transcriptSearchAbortRef.current = abortController;
20589
+ setTranscriptSearchSearching(true);
20590
+ (async () => {
20591
+ const cached = transcriptSearchIndexRef.current;
20592
+ const index = cached?.sessionId === session.id && cached.events === events ? cached.index : await (historySearchWorkerRef.current ??= new HistorySearchWorkerClient()).build(turns, abortController.signal);
20593
+ transcriptSearchIndexRef.current = {
20594
+ sessionId: session.id,
20595
+ events,
20596
+ index
20597
+ };
20598
+ const matches = await (historySearchWorkerRef.current ??= new HistorySearchWorkerClient()).search(index, query, turns.length, abortController.signal);
20599
+ if (generation !== transcriptSearchGenerationRef.current) return;
20600
+ const order = new Map(turns.map((turn, i) => [turn.id, i]));
20601
+ matches.sort((a, b) => (order.get(a.turn.id) ?? 0) - (order.get(b.turn.id) ?? 0));
20602
+ setTranscriptSearchResults(matches);
20603
+ setTranscriptSearchSearching(false);
20604
+ })().catch((error) => {
20605
+ if (!(error instanceof DOMException && error.name === "AbortError")) {
20606
+ debugLog("conversation search failed", error);
20607
+ if (generation === transcriptSearchGenerationRef.current) setTranscriptSearchSearching(false);
20608
+ }
20609
+ });
20610
+ return () => abortController.abort();
20611
+ }, [
20612
+ transcriptSearchMode,
20613
+ transcriptSearchQuery,
20614
+ events
20615
+ ]);
20616
+ const transcriptSearchOwnerIds = useMemo(() => {
20617
+ const owners = /* @__PURE__ */ new Map();
20618
+ if (!transcriptSearchQuery.trim() || transcriptSearchResults.length === 0) return owners;
20619
+ const session = sessionRef.current;
20620
+ if (!session) return owners;
20621
+ const callOwner = /* @__PURE__ */ new Map();
20622
+ for (const turn of session.turns) for (const block of turn.content) if (block.type === "tool_call") callOwner.set(block.id, turn.id);
20623
+ for (const result of transcriptSearchResults) {
20624
+ const resultOwners = [];
20625
+ for (const block of result.turn.content) {
20626
+ if (block.type !== "tool_result") continue;
20627
+ const owner = callOwner.get(block.callId);
20628
+ if (owner && owner !== result.turn.id && !resultOwners.includes(owner)) resultOwners.push(owner);
20629
+ }
20630
+ if (resultOwners.length > 0) owners.set(result.turn.id, resultOwners);
20631
+ }
20632
+ return owners;
20633
+ }, [
20634
+ transcriptSearchQuery,
20635
+ transcriptSearchResults,
20636
+ events
20637
+ ]);
20638
+ const transcriptSearchEvents = useMemo(() => {
20639
+ if (!transcriptSearchMode || !transcriptSearchQuery.trim()) return [];
20640
+ const session = sessionRef.current;
20641
+ if (!session) return [];
20642
+ const shown = transcriptSearchResults.slice(-200);
20643
+ const contextIds = new Set(shown.map((result) => result.turn.id));
20644
+ for (const result of shown) for (const owner of transcriptSearchOwnerIds.get(result.turn.id) ?? []) contextIds.add(owner);
20645
+ const searchEvents = eventsFromTurns(session.turns.filter((turn) => contextIds.has(turn.id)), session.runs);
20646
+ const hidden = transcriptSearchResults.length - shown.length;
20647
+ if (hidden > 0) searchEvents.unshift({
20648
+ kind: "info",
20649
+ text: `… ${hidden} earlier matched message${hidden === 1 ? "" : "s"} not shown — refine the query to narrow results.`
20650
+ });
20651
+ return searchEvents;
20652
+ }, [
20653
+ transcriptSearchMode,
20654
+ transcriptSearchQuery,
20655
+ transcriptSearchResults,
20656
+ transcriptSearchOwnerIds,
20657
+ events
20658
+ ]);
20659
+ const transcriptSearchTurnIds = useMemo(() => {
20660
+ if (!transcriptSearchQuery.trim()) return turnIds;
20661
+ return selectableTurnIds(transcriptSearchEvents, settings, (event) => isVisibleDuringSearch(event, settings, transcriptSearchQuery));
20662
+ }, [
20663
+ transcriptSearchQuery,
20664
+ transcriptSearchEvents,
20665
+ turnIds,
20666
+ settings
20667
+ ]);
19609
20668
  /** Drop the selection if its turn disappeared (session swap, history reset). */
19610
20669
  useEffect(() => {
20670
+ if (transcriptSearchMode) return;
19611
20671
  if (selectedTurnId && !turnIds.includes(selectedTurnId)) setSelectedTurnId(null);
19612
- }, [selectedTurnId, turnIds]);
19613
- const inSelectMode = selectedTurnId !== null;
19614
- const enterSelectMode = useCallback(() => {
19615
- if (turnIds.length === 0) return;
19616
- setSelectedTurnId(turnIds[turnIds.length - 1]);
20672
+ }, [
20673
+ selectedTurnId,
20674
+ turnIds,
20675
+ transcriptSearchMode
20676
+ ]);
20677
+ useEffect(() => {
20678
+ if (!transcriptSearchMode) return;
20679
+ if (transcriptSearchTurnIds.length === 0) {
20680
+ setSelectedTurnId(null);
20681
+ return;
20682
+ }
20683
+ if (!selectedTurnId || !transcriptSearchTurnIds.includes(selectedTurnId)) setSelectedTurnId(transcriptSearchTurnIds[transcriptSearchTurnIds.length - 1] ?? null);
20684
+ }, [
20685
+ transcriptSearchMode,
20686
+ transcriptSearchTurnIds,
20687
+ selectedTurnId
20688
+ ]);
20689
+ const onTranscriptSearchModeChange = useCallback((enabled) => {
20690
+ setTranscriptSearchMode(enabled);
20691
+ setTranscriptSearchQuery("");
20692
+ if (enabled) (historySearchWorkerRef.current ??= new HistorySearchWorkerClient()).prewarm();
20693
+ if (!enabled) setSelectedTurnId(null);
20694
+ else if (turnIds.length > 0) setSelectedTurnId(turnIds[turnIds.length - 1] ?? null);
19617
20695
  }, [turnIds]);
19618
- const cycleSelectedTurn = useCallback((direction) => {
20696
+ const cycleSelectedTurn = useCallback((direction, candidates = turnIds) => {
19619
20697
  setSelectedTurnId((prev) => {
19620
- if (!prev || turnIds.length === 0) return prev;
19621
- const idx = turnIds.indexOf(prev);
19622
- if (idx === -1) return turnIds[turnIds.length - 1];
19623
- const len = turnIds.length;
19624
- const nextIdx = ((idx + direction) % len + len) % len;
19625
- return turnIds[nextIdx];
20698
+ if (!prev || candidates.length === 0) return prev;
20699
+ const idx = candidates.indexOf(prev);
20700
+ if (idx === -1) return candidates[candidates.length - 1];
20701
+ const len = candidates.length;
20702
+ return candidates[((idx + direction) % len + len) % len];
19626
20703
  });
19627
20704
  }, [turnIds]);
20705
+ const onTranscriptSearchNavigate = useCallback((direction) => {
20706
+ cycleSelectedTurn(direction, transcriptSearchTurnIds);
20707
+ }, [cycleSelectedTurn, transcriptSearchTurnIds]);
19628
20708
  const onForkTurn = useCallback(async (turnId) => {
19629
20709
  const source = sessionRef.current;
19630
20710
  if (!source || !picked) return;
@@ -19664,7 +20744,10 @@ function AppShell() {
19664
20744
  debugLog("delete: save failed", err);
19665
20745
  return;
19666
20746
  }
19667
- setEvents(capTranscriptEvents(eventsFromTurns(session.turns, session.runs)));
20747
+ const nextEvents = capTranscriptEvents(eventsFromTurns(session.turns, session.runs));
20748
+ const nextIds = selectableTurnIds(nextEvents, settings);
20749
+ setEvents(nextEvents);
20750
+ setSelectedTurnId((prev) => prev ? selectionAfterTurnRemoval(turnIds, prev, nextIds) : prev);
19668
20751
  const replayedTokens = lastContextSizeFromTurns(session.turns, session.runs);
19669
20752
  setLastInputTokens(replayedTokens);
19670
20753
  lastInputTokensRef.current = replayedTokens;
@@ -19675,11 +20758,7 @@ function AppShell() {
19675
20758
  userMessageCount: session.turns.reduce((n, t) => t.role === "user" ? n + 1 : n, 0),
19676
20759
  updatedAt: Date.now()
19677
20760
  } : prev);
19678
- setSelectedTurnId((prev) => {
19679
- if (!prev) return prev;
19680
- return nextTurns.some((t) => t.id === prev) ? prev : null;
19681
- });
19682
- }, []);
20761
+ }, [settings, turnIds]);
19683
20762
  const onEditTurn = useCallback(async (turnId, newText) => {
19684
20763
  const session = sessionRef.current;
19685
20764
  if (!session) return;
@@ -19701,7 +20780,10 @@ function AppShell() {
19701
20780
  debugLog("edit: save failed", err);
19702
20781
  return;
19703
20782
  }
19704
- setEvents(capTranscriptEvents(eventsFromTurns(session.turns, session.runs)));
20783
+ const rebuilt = eventsFromTurns(session.turns, session.runs);
20784
+ const patched = replaceTranscriptTurnEvents(eventsRef.current, rebuilt, turnId);
20785
+ setEvents(patched ?? capTranscriptEvents(rebuilt));
20786
+ setSelectedTurnId(turnId);
19705
20787
  const replayedTokens = lastContextSizeFromTurns(session.turns, session.runs);
19706
20788
  setLastInputTokens(replayedTokens);
19707
20789
  lastInputTokensRef.current = replayedTokens;
@@ -19999,11 +21081,23 @@ function AppShell() {
19999
21081
  if (!session) return;
20000
21082
  const turn = session.turns.find((t) => t.id === id);
20001
21083
  if (!turn) return;
20002
- const index = turnIds.indexOf(id) + 1;
21084
+ const candidates = transcriptSearchMode ? transcriptSearchTurnIds : turnIds;
21085
+ const index = candidates.indexOf(id) + 1;
21086
+ const callIds = new Set(turn.content.filter((block) => block.type === "tool_call").map((block) => block.id));
21087
+ const toolResultTurns = callIds.size === 0 ? [] : session.turns.flatMap((candidate) => {
21088
+ if (candidate.id === turn.id) return [];
21089
+ const content = candidate.content.filter((block) => block.type === "tool_result" && callIds.has(block.callId));
21090
+ return content.length > 0 ? [{
21091
+ ...candidate,
21092
+ content
21093
+ }] : [];
21094
+ });
20003
21095
  modal.open(/* @__PURE__ */ jsx$1(TurnDetailsModal, {
20004
21096
  turn,
21097
+ toolResultTurns,
21098
+ searchQuery: transcriptSearchMode ? transcriptSearchQuery : void 0,
20005
21099
  index,
20006
- total: turnIds.length,
21100
+ total: candidates.length,
20007
21101
  actions: {
20008
21102
  onFork: onForkTurn,
20009
21103
  onDelete: onDeleteTurn,
@@ -20015,6 +21109,9 @@ function AppShell() {
20015
21109
  modal,
20016
21110
  selectedTurnId,
20017
21111
  turnIds,
21112
+ transcriptSearchMode,
21113
+ transcriptSearchQuery,
21114
+ transcriptSearchTurnIds,
20018
21115
  onForkTurn,
20019
21116
  onDeleteTurn,
20020
21117
  onEditTurn,
@@ -20022,23 +21119,8 @@ function AppShell() {
20022
21119
  ]);
20023
21120
  useKeyboard((key) => {
20024
21121
  if (modal.isOpen) return;
20025
- if (inSelectMode && screen === "chat") {
20026
- if (key.name === "up") {
20027
- cycleSelectedTurn(-1);
20028
- return;
20029
- }
20030
- if (key.name === "down") {
20031
- cycleSelectedTurn(1);
20032
- return;
20033
- }
20034
- if (key.name === "return") {
20035
- openSelectedTurn();
20036
- return;
20037
- }
20038
- if (key.name === "escape") {
20039
- setSelectedTurnId(null);
20040
- return;
20041
- }
21122
+ if (transcriptSearchMode && screen === "chat") {
21123
+ if (key.name === "escape") onTranscriptSearchModeChange(false);
20042
21124
  return;
20043
21125
  }
20044
21126
  if (queueSelectionIndex != null && screen === "chat") {
@@ -20203,8 +21285,8 @@ function AppShell() {
20203
21285
  }));
20204
21286
  return;
20205
21287
  }
20206
- if (matchesBinding(key, keybindings.enterSelectTurnMode) && screen === "chat" && !busy && !pendingApproval && !pendingInteraction) {
20207
- enterSelectMode();
21288
+ if (matchesBinding(key, keybindings.enterSelectTurnMode) && screen === "chat" && turnIds.length > 0 && !busy && !pendingApproval && !pendingInteraction) {
21289
+ onTranscriptSearchModeChange(true);
20208
21290
  return;
20209
21291
  }
20210
21292
  if (matchesBinding(key, keybindings.cycleAgent) && screen === "chat" && hasMultipleAgents && !busy && !pendingApproval && !pendingInteraction) {
@@ -20495,6 +21577,10 @@ function AppShell() {
20495
21577
  onPick: onSwitchSession,
20496
21578
  onCreate: onCreateSession,
20497
21579
  onFocusChange: setFocusedSessionId,
21580
+ contentMatches: sessionContentMatches,
21581
+ contentSearching: sessionContentSearching,
21582
+ contentSearchProgress: sessionContentSearchProgress,
21583
+ onSearchQueryChange: onSessionSearchQueryChange,
20498
21584
  showAllProjects: sessionsShowAll,
20499
21585
  onToggleScope: toggleSessionsScope,
20500
21586
  currentProjectRoot: projectDir
@@ -20526,6 +21612,15 @@ function AppShell() {
20526
21612
  enterSelectsCompletion: (providerId) => providerId === "chats",
20527
21613
  onPopupOpenChange,
20528
21614
  selectedTurnId,
21615
+ transcriptSearchMode,
21616
+ transcriptSearchQuery,
21617
+ transcriptSearchEvents,
21618
+ transcriptSearchSearching,
21619
+ onTranscriptSearchModeChange,
21620
+ onTranscriptSearchQueryChange: setTranscriptSearchQuery,
21621
+ onTranscriptSearchNavigate,
21622
+ onTranscriptSearchOpen: openSelectedTurn,
21623
+ onLoadEarlierTranscript,
20529
21624
  promptTriggerHints,
20530
21625
  attachImageBinding: keybindings.attachClipboardImage,
20531
21626
  editorBinding: settings.externalEditor ? keybindings.openExternalEditor : void 0,
@@ -20537,7 +21632,7 @@ function AppShell() {
20537
21632
  hints,
20538
21633
  context: contextUsage,
20539
21634
  cost: footerCost,
20540
- status: pendingInteraction?.kind === "question" ? "asking" : compacting ? "compacting" : busy ? "busy" : null,
21635
+ status: transcriptSearchMode && transcriptSearchSearching ? "searching" : pendingInteraction?.kind === "question" ? "asking" : compacting ? "compacting" : busy ? "busy" : null,
20541
21636
  compactionTokens,
20542
21637
  compactionStartedAt
20543
21638
  }),