zidane 6.2.24 → 6.2.26

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 (101) hide show
  1. package/dist/{acp-CTn1kFvP.js → acp-CNZqD1Fw.js} +3 -3
  2. package/dist/{acp-CTn1kFvP.js.map → acp-CNZqD1Fw.js.map} +1 -1
  3. package/dist/acp-cli.js +4 -4
  4. package/dist/acp.d.ts +1 -1
  5. package/dist/acp.js +1 -1
  6. package/dist/{agent-tLfGSSQw.js → agent--VPMZ0nT.js} +267 -16
  7. package/dist/agent--VPMZ0nT.js.map +1 -0
  8. package/dist/agent.d.ts +1 -1
  9. package/dist/agent.js +2 -2
  10. package/dist/{auth-CpdQebUy.js → auth-DIT_DMwa.js} +2 -2
  11. package/dist/{auth-CpdQebUy.js.map → auth-DIT_DMwa.js.map} +1 -1
  12. package/dist/chat/pure.d.ts +3 -3
  13. package/dist/chat/pure.js +1 -1
  14. package/dist/chat.d.ts +4 -4
  15. package/dist/chat.d.ts.map +1 -1
  16. package/dist/chat.js +5 -5
  17. package/dist/contexts/daytona.d.ts +1 -1
  18. package/dist/contexts/daytona.d.ts.map +1 -1
  19. package/dist/contexts/daytona.js +9 -2
  20. package/dist/contexts/daytona.js.map +1 -1
  21. package/dist/contexts/docker.js +1 -1
  22. package/dist/contexts/e2b.d.ts +1 -1
  23. package/dist/contexts/e2b.d.ts.map +1 -1
  24. package/dist/contexts/e2b.js +3 -4
  25. package/dist/contexts/e2b.js.map +1 -1
  26. package/dist/{contexts-A4ep-iJs.js → contexts-0rmrBhTK.js} +8 -5
  27. package/dist/contexts-0rmrBhTK.js.map +1 -0
  28. package/dist/contexts.js +1 -1
  29. package/dist/eval.d.ts +1 -1
  30. package/dist/eval.js +1 -1
  31. package/dist/extensions.d.ts +1 -1
  32. package/dist/extensions.js +1 -1
  33. package/dist/{output-cap-kBUHBlnq.js → glob-DluQFSw1.js} +2 -50
  34. package/dist/glob-DluQFSw1.js.map +1 -0
  35. package/dist/headless.d.ts +1 -1
  36. package/dist/headless.js +3 -3
  37. package/dist/history-search-worker.js +6 -1
  38. package/dist/history-search-worker.js.map +1 -1
  39. package/dist/{index-8zm_hwdY.d.ts → index-AVZ6pYpi.d.ts} +212 -96
  40. package/dist/index-AVZ6pYpi.d.ts.map +1 -0
  41. package/dist/index.d.ts +1 -1
  42. package/dist/index.js +8 -5
  43. package/dist/index.js.map +1 -1
  44. package/dist/{inline-autocomplete-BAwBcZtD.js → inline-autocomplete-CbcdZpfC.js} +2 -1
  45. package/dist/inline-autocomplete-CbcdZpfC.js.map +1 -0
  46. package/dist/{login-DfJGuHUj.js → login-afiadwf0.js} +34 -18
  47. package/dist/login-afiadwf0.js.map +1 -0
  48. package/dist/mcp.d.ts +1 -1
  49. package/dist/output/stream-json.d.ts +1 -1
  50. package/dist/output/terminal.d.ts +1 -1
  51. package/dist/output-cap-DM1HrgIp.js +100 -0
  52. package/dist/output-cap-DM1HrgIp.js.map +1 -0
  53. package/dist/presets.d.ts +1 -1
  54. package/dist/presets.js +1 -1
  55. package/dist/providers/anthropic.d.ts +1 -1
  56. package/dist/providers/arcee.d.ts +1 -1
  57. package/dist/providers/baseten.d.ts +1 -1
  58. package/dist/providers/cerebras.d.ts +1 -1
  59. package/dist/providers/local.d.ts +1 -1
  60. package/dist/providers/openai-compat.d.ts +1 -1
  61. package/dist/providers/openai.d.ts +1 -1
  62. package/dist/providers/openrouter.d.ts +1 -1
  63. package/dist/providers/xai.d.ts +1 -1
  64. package/dist/providers.d.ts +1 -1
  65. package/dist/restate.d.ts +1 -1
  66. package/dist/{run-summary-nD3IQ4ZQ.js → run-summary-BU_NgwUT.js} +17 -1
  67. package/dist/run-summary-BU_NgwUT.js.map +1 -0
  68. package/dist/session/sqlite.d.ts +1 -1
  69. package/dist/session.d.ts +1 -1
  70. package/dist/skills.d.ts +1 -1
  71. package/dist/{tool-formatters-CZJYzP4x.d.ts → tool-formatters-Dual3Hdl.d.ts} +2 -2
  72. package/dist/{tool-formatters-CZJYzP4x.d.ts.map → tool-formatters-Dual3Hdl.d.ts.map} +1 -1
  73. package/dist/tools/fetch-url.d.ts +1 -1
  74. package/dist/tools/web-search.d.ts +1 -1
  75. package/dist/tools.d.ts +1 -1
  76. package/dist/tools.js +1 -1
  77. package/dist/{transcript-anchors-CJ2dmpQk.d.ts → transcript-anchors-CG85bieG.d.ts} +2 -2
  78. package/dist/transcript-anchors-CG85bieG.d.ts.map +1 -0
  79. package/dist/{transcript-anchors-Ded7iJjG.js → transcript-anchors-CnmxZ8QR.js} +88 -12
  80. package/dist/transcript-anchors-CnmxZ8QR.js.map +1 -0
  81. package/dist/tui.d.ts +44 -7
  82. package/dist/tui.d.ts.map +1 -1
  83. package/dist/tui.js +1397 -1124
  84. package/dist/tui.js.map +1 -1
  85. package/dist/{turn-operations-ClGa1vnb.js → turn-operations-4tnvKwfJ.js} +77 -13
  86. package/dist/turn-operations-4tnvKwfJ.js.map +1 -0
  87. package/dist/{turn-operations-DZYpXcU1.d.ts → turn-operations-BUYaSCL3.d.ts} +3 -3
  88. package/dist/{turn-operations-DZYpXcU1.d.ts.map → turn-operations-BUYaSCL3.d.ts.map} +1 -1
  89. package/dist/types-BIarq1qE.js.map +1 -1
  90. package/dist/types.d.ts +1 -1
  91. package/package.json +2 -1
  92. package/dist/agent-tLfGSSQw.js.map +0 -1
  93. package/dist/contexts-A4ep-iJs.js.map +0 -1
  94. package/dist/index-8zm_hwdY.d.ts.map +0 -1
  95. package/dist/inline-autocomplete-BAwBcZtD.js.map +0 -1
  96. package/dist/login-DfJGuHUj.js.map +0 -1
  97. package/dist/output-cap-kBUHBlnq.js.map +0 -1
  98. package/dist/run-summary-nD3IQ4ZQ.js.map +0 -1
  99. package/dist/transcript-anchors-CJ2dmpQk.d.ts.map +0 -1
  100. package/dist/transcript-anchors-Ded7iJjG.js.map +0 -1
  101. package/dist/turn-operations-ClGa1vnb.js.map +0 -1
package/dist/tui.js CHANGED
@@ -1,8 +1,8 @@
1
- import { $ as McpAuthProvider, $t as filterChainFallbacks, A as runOAuthLogin, At as useSettings, B as mcpToolsCachePath, Ct as DEFAULT_SETTINGS, D as formatPathForCwd, Dn as accentColor, Dt as SETTINGS_TOGGLES, Et as SETTINGS_INPUTS, F as parseJsonTolerant, G as useMcpToolToggleMap, Gt as useDiscoveryOptional, Ht as createDiscoverySlot, I as parseMcpsFile, J as indexOfServerRow, Jt as resolveConfig, Kt as ConfigProvider, L as parseClaudeState, M as buildMcpServers, N as defaultMcpsConfigPaths, Nn as TODO_STATUS_GLYPHS, Nt as resolveChipColor, O as fetchOAuthRedirect, Ot as SettingsProvider, P as discoverProjectMcps, Pt as resolveTheme, Qt as chainsEqual, Sr as usageByModel, St as useEnabledToggleSet, T as suggestSafelistEntries, Tt as SETTINGS_CHOICES, U as subscribeMcpToolsCache, Un as useActiveTodos, Ut as DiscoveryProvider, V as refreshMcpToolsCatalog, Wt as useDiscovery, X as createFileMcpCredentialStore, Xt as buildRunModel, Y as parentServerName, Yt as resolveStoragePaths, Zt as chainPositionOf, _ as useSafeModeQueue, _r as sumRunCosts, _t as hintsLength, a as useSurfaces, an as tryOpenBrowser, at as InteractionsProvider, b as getSafelist, br as toolResultText, c as useStreamBuffer, cn as buildUpdateHint, cr as deriveSessionTitle, ct as createInteractionTools, d as discoverProjectSkills, dr as listSessionMeta, dt as pendingInteractionsFromTurns, en as providerKeySetChanged, er as buildBuildSystem, et as useMcpAuthDispatch, fr as loadState, g as useSafeModeActions, gr as stripSpawnTokensLine, gt as clipHintsToWidth, h as SafeModeProvider, hr as serverToolResultSummary, ht as EMPTY_HINTS, i as useSelectStyle, in as useCompletion, ir as createChatTurnsReadTool, it as ASK_USER_TOOL, j as supportsOAuth, k as oauthUsesManualCodePaste, kt as clampFps, l as buildSkillsConfig, ln as useUpdateCheck, lr as eventsFromTurns, m as writeSessionExport, mt as useInteractionsQueue, n as ThemeProvider, nr as envSection, nt as getMcpAuthStatus, or as compactSummaryEvent, ot as PRESENT_PLAN_TOOL, pr as marginTopFor, pt as useInteractionsActions, q as buildVisibleMcpRows, qt as useConfig, r as useColors, rn as toggleChainEntry, rr as chatReferenceTokensForPrompt, s as useTheme, sn as bootTick, st as buildResumedToolResultsTurn, t as computeTurnAnchors, tn as restoreChainFallbacks, tr as buildPlanSystem, tt as useMcpAuthState, u as defaultSkillScanPaths, ur as lastContextSizeFromTurns, ut as makeRequestInteraction, vn as chatAutoCompactBehavior, vt as truncateTrailing, wt as SETTINGS_CATEGORIES, x as isOnSafelist, xr as updateToolEventOutcomes, xt as listProjectFiles, y as addToSafelist, yn as discoverAgentsMd, yr as toolCallPreview, yt as buildHints, z as loadMcpToolsCache } from "./transcript-anchors-Ded7iJjG.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-tLfGSSQw.js";
1
+ import { $ as McpAuthProvider, $t as filterChainFallbacks, A as runOAuthLogin, At as useSettings, B as mcpToolsCachePath, Bn as pruneTodosByRun, Ct as DEFAULT_SETTINGS, D as formatPathForCwd, Dn as accentColor, Dt as SETTINGS_TOGGLES, Et as SETTINGS_INPUTS, F as parseJsonTolerant, G as useMcpToolToggleMap, Gt as useDiscoveryOptional, Ht as createDiscoverySlot, I as parseMcpsFile, J as indexOfServerRow, Jt as resolveConfig, Kt as ConfigProvider, L as parseClaudeState, M as buildMcpServers, N as defaultMcpsConfigPaths, Nn as TODO_STATUS_GLYPHS, Nt as resolveChipColor, O as fetchOAuthRedirect, Ot as SettingsProvider, P as discoverProjectMcps, Pt as resolveTheme, Qt as chainsEqual, Sr as usageByModel, St as useEnabledToggleSet, T as suggestSafelistEntries, Tt as SETTINGS_CHOICES, U as subscribeMcpToolsCache, Un as useActiveTodos, Ut as DiscoveryProvider, V as refreshMcpToolsCatalog, Wt as useDiscovery, X as createFileMcpCredentialStore, Xt as buildRunModel, Y as parentServerName, Yt as resolveStoragePaths, Zt as chainPositionOf, _ as useSafeModeQueue, _r as sumRunCosts, _t as hintsLength, a as useSurfaces, an as tryOpenBrowser, at as InteractionsProvider, b as getSafelist, br as toolResultText, c as useStreamBuffer, cn as buildUpdateHint, cr as deriveSessionTitle, ct as createInteractionTools, d as discoverProjectSkills, dr as listSessionMeta, dt as pendingInteractionsFromTurns, en as providerKeySetChanged, er as buildBuildSystem, et as useMcpAuthDispatch, fr as loadState, g as useSafeModeActions, gr as stripSpawnTokensLine, gt as clipHintsToWidth, h as SafeModeProvider, hr as serverToolResultSummary, ht as EMPTY_HINTS, i as useSelectStyle, in as useCompletion, ir as createChatTurnsReadTool, it as ASK_USER_TOOL, j as supportsOAuth, k as oauthUsesManualCodePaste, kt as clampFps, l as buildSkillsConfig, ln as useUpdateCheck, lr as eventsFromTurns, m as writeSessionExport, mt as useInteractionsQueue, n as ThemeProvider, nr as envSection, nt as getMcpAuthStatus, or as compactSummaryEvent, ot as PRESENT_PLAN_TOOL, pr as marginTopFor, pt as useInteractionsActions, q as buildVisibleMcpRows, qt as useConfig, r as useColors, rn as toggleChainEntry, rr as chatReferenceTokensForPrompt, s as useTheme, sn as bootTick, st as buildResumedToolResultsTurn, t as computeTurnAnchors, tn as restoreChainFallbacks, tr as buildPlanSystem, tt as useMcpAuthState, u as defaultSkillScanPaths, ur as lastContextSizeFromTurns, ut as makeRequestInteraction, vn as chatAutoCompactBehavior, vt as truncateTrailing, wt as SETTINGS_CATEGORIES, x as isOnSafelist, xr as updateToolEventOutcomes, xt as listProjectFiles, y as addToSafelist, yn as discoverAgentsMd, yr as toolCallPreview, yt as buildHints, z as loadMcpToolsCache } from "./transcript-anchors-CnmxZ8QR.js";
2
+ import { B as createExtensionPromptBridge, Bt as KEYBINDING_KEY_COL_WIDTH, Ct as tailTruncate, Dt as createModelSearchTool, F as summaryToTurn, Gt as matchesBinding, Ht as formatBindingForDisplay, J as updateInstalledExtension, S as compactConversation, Tt as looksBinary, U as installExtensionFromSpec, Ut as groupBindings, V as ExtensionInstallError, Vt as ensureKeybindingsFile, Wt as keybindingsPath, X as registerExtensionHostModules, Xt as resolveExtensionKeybindings, Y as createExtensionHud, Z as discoverAndBuildExtensionRegistry, _ as resolveTasksDir, at as createExtensionHostRuntimeBridge, b as selectFilesFromSession, c as applyCompactSummaryCutoff, dn as resolveExtensionConfigValue, et as composeExtensionPreset, f as cleanupPersistedSession, fn as sanitizeExtensionConfigActionPatch, g as resolvePersistDir, gn as effectiveContextWindow, gt as isSpawnTool, h as resolveMcpWarningsDir, hn as OUTPUT_RESERVE_TOKENS, in as scanExtensionDirectories, it as partitionExtensionCatalog, m as rehomePersistedStubs, nt as filterExtensionRegistry, q as removeInstalledExtension, rn as parseExtensionScanPathEnv, sn as coerceExtensionConfigInput, t as createAgent, tn as inlineLoadedExtension, tt as disposeExtensionRegistry, ut as basic_default, v as buildPostCompactAttachments } from "./agent--VPMZ0nT.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
- import { n as appendCapped } from "./output-cap-kBUHBlnq.js";
5
- import { i as killProcessGroup, n as SUPPORTS_PROCESS_GROUPS, r as createProcessContext } from "./contexts-A4ep-iJs.js";
4
+ import { n as appendCapped } from "./output-cap-DM1HrgIp.js";
5
+ import { i as killProcessGroup, n as SUPPORTS_PROCESS_GROUPS, r as createProcessContext } from "./contexts-0rmrBhTK.js";
6
6
  import { l as errorMessage } from "./errors-DJUxZg9b.js";
7
7
  import { M as findGitRoot, j as projectUserPaths } from "./resolve-BPTA1RpX.js";
8
8
  import { a as createRingBufferSink, i as createLoggingHooks, l as logFileTimestamp, n as createFileSink, o as fanoutSink, r as createLogger, s as formatLogLine, t as consoleSink } from "./logger-DBX9uYOw.js";
@@ -12,14 +12,14 @@ import { replaceDynamicSection } from "./prompt.js";
12
12
  import { a as getExtensionSessionState, c as setExtensionSessionState, i as demoteStaleRunningStatus, o as loadSession, t as createSession } from "./session-jLsc_kMH.js";
13
13
  import { n as flattenTurns, r as formatTokenUsage, t as effectiveInputFromTurn } from "./stats-DAKBEKjc.js";
14
14
  import { o as withTimeout } from "./timeout-BDetbuN6.js";
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";
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-CbcdZpfC.js";
16
16
  import { s as resolveCompletionSubmit } from "./completion-core-BRo86R_L.js";
17
17
  import { i as installSkillFromSpec, r as SkillInstallError } from "./skills-C7eIjWYm.js";
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-DfJGuHUj.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-CpdQebUy.js";
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-afiadwf0.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, w as renderDefaultCallbackPage } from "./auth-DIT_DMwa.js";
20
20
  import { createDockerContext } from "./contexts/docker.js";
21
21
  import { createTuiStore } from "./session/sqlite.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-ClGa1vnb.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-4tnvKwfJ.js";
23
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";
24
24
  import { basename, dirname, join, relative, resolve } from "node:path";
25
25
  import { Buffer } from "node:buffer";
@@ -198,33 +198,70 @@ function claudeProjectSlug(path) {
198
198
  * `projectRoot` is derived.
199
199
  */
200
200
  function listClaudeSessionFiles(opts = {}) {
201
+ const out = [];
202
+ walkClaudeSessionFiles(opts, opts.allProjects ? (file) => out.push(file) : (file, scoped) => {
203
+ if (scoped()) out.push(file);
204
+ });
205
+ return out;
206
+ }
207
+ /**
208
+ * Detection-preview counts — scoped and all-projects figures from ONE
209
+ * directory walk (the naive two-listing approach reads every project dir
210
+ * twice). The scoped classification (with its per-file cwd peek for
211
+ * candidate dirs) runs at most once per file via the lazy `scoped()`
212
+ * thunk.
213
+ */
214
+ function countClaudeSessionFiles(opts = {}) {
215
+ let project = 0;
216
+ let all = 0;
217
+ walkClaudeSessionFiles({
218
+ ...opts,
219
+ allProjects: false
220
+ }, (_file, scoped) => {
221
+ all++;
222
+ if (scoped()) project++;
223
+ }, true);
224
+ return {
225
+ project,
226
+ all
227
+ };
228
+ }
229
+ /**
230
+ * Shared walk under `~/.claude/projects`. `visit` receives every
231
+ * transcript of every CANDIDATE dir (scoped mode) or every dir
232
+ * (`allProjects` / `visitAllDirs`), plus a lazy `scoped()` thunk that
233
+ * answers "does this file belong to the current project?" — lazy because
234
+ * the answer can cost a `transcriptProjectRoot` peek (up to the whole
235
+ * file), which pure all-projects listings never need.
236
+ */
237
+ function walkClaudeSessionFiles(opts, visit, visitAllDirs = false) {
201
238
  const { projectRoot, home } = roots(opts);
202
239
  const projectsDir = resolve(home, ".claude/projects");
203
- if (!existsSync(projectsDir)) return [];
240
+ if (!existsSync(projectsDir)) return;
204
241
  const slugs = [claudeProjectSlug(projectRoot), claudeProjectSlug(realpathOr(projectRoot))];
205
242
  const exact = new Set(slugs);
206
243
  const prefixes = [...new Set(slugs.map((s) => `${s}-`))];
207
244
  const wantedRoots = /* @__PURE__ */ new Set([projectRoot, realpathOr(projectRoot)]);
208
- const out = [];
209
245
  for (const entry of readdirSync(projectsDir, { withFileTypes: true })) {
210
246
  if (!entry.isDirectory()) continue;
211
247
  const exactMatch = exact.has(entry.name);
212
- if (!opts.allProjects && !exactMatch && !prefixes.some((p) => entry.name.startsWith(p))) continue;
248
+ const candidate = exactMatch || prefixes.some((p) => entry.name.startsWith(p));
249
+ if (!opts.allProjects && !visitAllDirs && !candidate) continue;
213
250
  const projectDir = join(projectsDir, entry.name);
214
251
  for (const file of readdirSync(projectDir)) {
215
252
  if (!file.endsWith(".jsonl")) continue;
216
253
  const path = join(projectDir, file);
217
- if (!opts.allProjects) {
254
+ const scoped = () => {
255
+ if (!candidate) return false;
218
256
  const root = transcriptProjectRoot(path);
219
- if (root === null ? !exactMatch : !(wantedRoots.has(root) || wantedRoots.has(realpathOr(root)))) continue;
220
- }
221
- out.push({
257
+ return root === null ? exactMatch : wantedRoots.has(root) || wantedRoots.has(realpathOr(root));
258
+ };
259
+ visit({
222
260
  path,
223
261
  projectDir
224
- });
262
+ }, scoped);
225
263
  }
226
264
  }
227
- return out;
228
265
  }
229
266
  /**
230
267
  * Peek a transcript's project root: scan the first chunk for the first
@@ -363,7 +400,7 @@ function convertClaudeSession(jsonl, opts = {}) {
363
400
  });
364
401
  }
365
402
  if (turns.length === 0) return null;
366
- const id = claudeSessionId ?? opts.fallbackId ?? crypto.randomUUID();
403
+ const id = opts.fallbackId ?? claudeSessionId ?? crypto.randomUUID();
367
404
  const projectRoot = cwd ? findGitRoot(cwd) ?? cwd : void 0;
368
405
  return {
369
406
  session: {
@@ -375,7 +412,8 @@ function convertClaudeSession(jsonl, opts = {}) {
375
412
  metadata: {
376
413
  ...title ? { title } : {},
377
414
  importedFrom: "claude-code",
378
- claudeSessionId: id
415
+ claudeSessionId: id,
416
+ ...claudeSessionId && claudeSessionId !== id ? { claudeLineageId: claudeSessionId } : {}
379
417
  },
380
418
  createdAt: turns[0].createdAt,
381
419
  updatedAt: turns[turns.length - 1].createdAt
@@ -493,10 +531,6 @@ async function importClaudeSessions(store, opts = {}) {
493
531
  result.empty++;
494
532
  continue;
495
533
  }
496
- if (await store.load(converted.session.id)) {
497
- result.skipped++;
498
- continue;
499
- }
500
534
  await store.save(converted.session);
501
535
  result.imported++;
502
536
  } catch (err) {
@@ -524,7 +558,8 @@ function importClaudeMcps(opts = {}) {
524
558
  const result = {
525
559
  copied: [],
526
560
  skipped: [],
527
- failed: []
561
+ failed: [],
562
+ duplicates: []
528
563
  };
529
564
  const nativeNames = /* @__PURE__ */ new Set();
530
565
  for (const candidate of defaultMcpsConfigPaths({
@@ -567,7 +602,10 @@ function importClaudeMcps(opts = {}) {
567
602
  }
568
603
  const seen = /* @__PURE__ */ new Set();
569
604
  for (const { servers, scope } of sources) for (const server of servers) {
570
- if (seen.has(server.name)) continue;
605
+ if (seen.has(server.name)) {
606
+ result.duplicates.push(server.name);
607
+ continue;
608
+ }
571
609
  seen.add(server.name);
572
610
  if (nativeNames.has(server.name)) {
573
611
  result.skipped.push(server.name);
@@ -684,13 +722,28 @@ async function importFromClaudeCode(store, opts = {}) {
684
722
  if (opts.skills !== false) summary.skills = importClaudeSkills(opts);
685
723
  return summary;
686
724
  }
725
+ /**
726
+ * Per-catalog fzf instance, keyed on catalog reference identity —
727
+ * `suggest()` runs per keystroke and `new Fzf(...)` walks the whole
728
+ * command list to build its match structures. The getter returns a
729
+ * stable array until a registry rebuild swaps it.
730
+ */
731
+ const fzfByCatalog = /* @__PURE__ */ new WeakMap();
732
+ function fzfFor(catalog) {
733
+ let fzf = fzfByCatalog.get(catalog);
734
+ if (!fzf) {
735
+ fzf = new Fzf(catalog, {
736
+ selector: (c) => c.name,
737
+ casing: "case-insensitive"
738
+ });
739
+ fzfByCatalog.set(catalog, fzf);
740
+ }
741
+ return fzf;
742
+ }
687
743
  /** Filter + rank commands against a query (name-priority, then description, then fuzzy on name). */
688
744
  function scoreCommands(catalog, query) {
689
745
  const q = query.trim().toLowerCase();
690
- const fuzzyHits = q.length > 0 ? new Set(new Fzf([...catalog], {
691
- selector: (c) => c.name,
692
- casing: "case-insensitive"
693
- }).find(q).map((r) => r.item)) : null;
746
+ const fuzzyHits = q.length > 0 ? new Set(fzfFor(catalog).find(q).map((r) => r.item)) : null;
694
747
  const scored = [];
695
748
  for (const command of catalog) {
696
749
  const name = command.name.toLowerCase();
@@ -810,12 +863,252 @@ function attachMcpWarnLog(hooks, opts) {
810
863
  } };
811
864
  }
812
865
  //#endregion
866
+ //#region src/tui/clickable.tsx
867
+ /** Glyph / word aliases → OpenTUI key names (`parse.keypress` conventions). */
868
+ const KEY_NAME_ALIASES = {
869
+ "esc": "escape",
870
+ "↵": "return",
871
+ "enter": "return",
872
+ "tab": "tab",
873
+ "space": "space",
874
+ "⌫": "backspace",
875
+ "backspace": "backspace",
876
+ "pgup": "pageup",
877
+ "pgdn": "pagedown",
878
+ "↑": "up",
879
+ "↓": "down",
880
+ "←": "left",
881
+ "→": "right",
882
+ "↑↓": "down",
883
+ "←→": "right",
884
+ "del": "delete"
885
+ };
886
+ /** Decorative pseudo-keys that must never dispatch (chips, markers). */
887
+ const NON_KEYS = /* @__PURE__ */ new Set([
888
+ "✦",
889
+ "·",
890
+ "…"
891
+ ]);
892
+ /** Mac-style modifier glyphs → word modifiers (`⌘k`, `⌘+k`, `⇧↵`, …). */
893
+ const GLYPH_MODIFIERS = {
894
+ "⌘": "meta",
895
+ "⌃": "ctrl",
896
+ "⌥": "option",
897
+ "⇧": "shift"
898
+ };
899
+ /**
900
+ * Parse a hint key spec (`ctrl+g`, `shift+↵`, `esc`, `a`, `↑↓`) into a
901
+ * dispatchable {@link ParsedKey}, or `null` when the spec is decorative
902
+ * or ambiguous (the hint then renders non-clickable). Modifier segments
903
+ * are order-insensitive; the final segment is the key itself.
904
+ */
905
+ function parseHintKeySpec(spec) {
906
+ const trimmed = spec.trim().toLowerCase();
907
+ if (!trimmed || NON_KEYS.has(trimmed)) return null;
908
+ let first = trimmed.split("/")[0] ?? "";
909
+ first = first.replace(/[⌘⌃⌥⇧]/g, (glyph) => `${GLYPH_MODIFIERS[glyph]}+`).replace(/\+\+/g, "+");
910
+ if (first.startsWith("^") && first.length > 1) first = `ctrl+${first.slice(1)}`;
911
+ const segments = first.split("+").map((s) => s.trim()).filter(Boolean);
912
+ if (segments.length === 0) return null;
913
+ const keyPart = segments[segments.length - 1] ?? "";
914
+ const modifiers = segments.slice(0, -1);
915
+ const mods = {
916
+ ctrl: false,
917
+ meta: false,
918
+ shift: false,
919
+ option: false
920
+ };
921
+ for (const mod of modifiers) if (mod === "ctrl" || mod === "control") mods.ctrl = true;
922
+ else if (mod === "shift") mods.shift = true;
923
+ else if (mod === "alt" || mod === "option") mods.option = true;
924
+ else if (mod === "meta" || mod === "cmd" || mod === "super") mods.meta = true;
925
+ else return null;
926
+ const name = KEY_NAME_ALIASES[keyPart] ?? (/^f(?:[1-9]|1[0-2])$/.test(keyPart) ? keyPart : null) ?? (keyPart.length === 1 ? keyPart : null);
927
+ if (!name) return null;
928
+ const sequence = keySequence(name, mods);
929
+ return {
930
+ name,
931
+ ...mods,
932
+ sequence,
933
+ number: /^\d$/.test(name),
934
+ raw: sequence,
935
+ eventType: "press",
936
+ source: "raw"
937
+ };
938
+ }
939
+ /** Byte sequences real terminals send for the named specials we synthesize. */
940
+ const NAMED_KEY_SEQUENCES = {
941
+ return: "\r",
942
+ tab: " ",
943
+ escape: "\x1B",
944
+ space: " ",
945
+ backspace: "",
946
+ delete: "\x1B[3~",
947
+ up: "\x1B[A",
948
+ down: "\x1B[B",
949
+ right: "\x1B[C",
950
+ left: "\x1B[D",
951
+ pageup: "\x1B[5~",
952
+ pagedown: "\x1B[6~"
953
+ };
954
+ /**
955
+ * The `sequence`/`raw` a REAL keypress would carry — without it the
956
+ * synthetic event is second-class: focused inputs insert text from
957
+ * `key.sequence` (so a clicked `@ files` hint must actually type `@`),
958
+ * and some handlers match on `sequence` rather than `name`. Modifier
959
+ * chords return `''`: their handlers dispatch on `name` + flags, and
960
+ * fabricating control bytes would risk double interpretation.
961
+ */
962
+ function keySequence(name, mods) {
963
+ if (mods.ctrl || mods.meta || mods.option) return "";
964
+ if (mods.shift) return name === "tab" ? "\x1B[Z" : "";
965
+ if (name.length === 1) return name;
966
+ return NAMED_KEY_SEQUENCES[name] ?? "";
967
+ }
968
+ /**
969
+ * Dispatch a hint key spec through the renderer's key pipeline. Returns
970
+ * `false` for unparseable specs (no-op). Exposed for non-hint surfaces
971
+ * that want the same click==keypress contract.
972
+ *
973
+ * CONTRACT NOTE: `processParsedKey` wraps the parsed key in a real
974
+ * `KeyEvent`, so handlers receive the same object type as terminal
975
+ * input — but synthesized modifier chords carry an empty
976
+ * `sequence`/`raw` (fabricating control bytes would risk double
977
+ * interpretation by byte-level consumers). Every handler in this app
978
+ * discriminates on `name` + modifier flags, which is the contract this
979
+ * dispatch guarantees; a future handler matching on non-empty
980
+ * `sequence` for chords would see synthesized clicks differ from real
981
+ * keystrokes.
982
+ */
983
+ function dispatchHintKey(renderer, spec) {
984
+ const parsed = parseHintKeySpec(spec);
985
+ if (!parsed) return false;
986
+ renderer.keyInput.processParsedKey(parsed);
987
+ return true;
988
+ }
989
+ /**
990
+ * True when a row click landed on the row's `[✓] ` checkbox cell — the
991
+ * columns `[startCol, startCol + width)` of the FIRST line of the target
992
+ * renderable. Rows that must stay single `<text>` renderables (the MCP
993
+ * list documents an OpenTUI layout regression forbidding box wrappers)
994
+ * can't make the checkbox its own renderable, so zone-splitting works by
995
+ * coordinate instead: `event.x/y` are absolute, the target renderable
996
+ * knows its own absolute origin, and the difference is the glyph column.
997
+ * The `y` check pins line 0 so wrapped continuation lines can't false-
998
+ * positive into the checkbox range. Degrades to `false` (row semantics)
999
+ * when the target's geometry is unavailable.
1000
+ */
1001
+ function inCheckboxZone(event, startCol, width = 4) {
1002
+ const target = event.target;
1003
+ if (!target || typeof target.x !== "number" || typeof target.y !== "number") return false;
1004
+ if (event.y - target.y !== 0) return false;
1005
+ const col = event.x - target.x;
1006
+ return col >= startCol && col < startCol + width;
1007
+ }
1008
+ /** Spans for one hint's text content — shared by hover and rest states. */
1009
+ function hintSpans(hint, COLOR, hovered) {
1010
+ const keyColor = hovered ? COLOR.accent : hint.keyColor ?? COLOR.warn;
1011
+ const labelColor = hovered ? COLOR.accent : hint.labelColor ?? COLOR.dim;
1012
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1013
+ hint.key !== "" && /* @__PURE__ */ jsx$1("span", {
1014
+ fg: keyColor,
1015
+ children: hint.key
1016
+ }),
1017
+ hint.extra && /* @__PURE__ */ jsx$1("span", {
1018
+ fg: hovered ? COLOR.accent : hint.extra.keyColor ?? COLOR.mute,
1019
+ children: hint.extra.key
1020
+ }),
1021
+ /* @__PURE__ */ jsx$1("span", {
1022
+ fg: labelColor,
1023
+ children: hint.key === "" ? hint.label : ` ${hint.label}`
1024
+ }),
1025
+ hint.extra && /* @__PURE__ */ jsx$1("span", {
1026
+ fg: hovered ? COLOR.accent : hint.extra.labelColor ?? COLOR.dim,
1027
+ children: ` ${hint.extra.label}`
1028
+ })
1029
+ ] });
1030
+ }
1031
+ /**
1032
+ * One hint as its own `<text>` renderable — clickable when its key spec
1033
+ * parses, hover-highlighted in the theme accent. Non-parseable hints
1034
+ * render identically but inert, so decorative chips cost nothing.
1035
+ */
1036
+ function ClickableHint({ hint }) {
1037
+ const COLOR = useColors();
1038
+ const renderer = useRenderer();
1039
+ const [hovered, setHovered] = useState(false);
1040
+ if (!(parseHintKeySpec(hint.key) !== null)) return /* @__PURE__ */ jsx$1("text", {
1041
+ selectable: false,
1042
+ children: hintSpans(hint, COLOR, false)
1043
+ });
1044
+ return /* @__PURE__ */ jsx$1("text", {
1045
+ selectable: false,
1046
+ onMouseDown: (event) => {
1047
+ event.stopPropagation();
1048
+ event.preventDefault();
1049
+ dispatchHintKey(renderer, hint.key);
1050
+ },
1051
+ onMouseOver: () => setHovered(true),
1052
+ onMouseOut: () => setHovered(false),
1053
+ children: hintSpans(hint, COLOR, hovered)
1054
+ });
1055
+ }
1056
+ /**
1057
+ * Modal-footer flavor of {@link HintRow}: same clickable hints, but with
1058
+ * the modal convention's mute labels (footers historically rendered as
1059
+ * `<text fg={mute}>` prose) and falsy-entry filtering so call sites can
1060
+ * express conditional hints inline:
1061
+ *
1062
+ * ```tsx
1063
+ * <ModalHints hints={[
1064
+ * { key: '↑↓', label: 'navigate' },
1065
+ * onRefresh && { key: 'ctrl+r', label: 'refresh' },
1066
+ * { key: 'esc', label: 'close' },
1067
+ * ]}
1068
+ * />
1069
+ * ```
1070
+ *
1071
+ * Per-hint `keyColor`/`labelColor` overrides still win over the defaults.
1072
+ */
1073
+ function ModalHints({ hints }) {
1074
+ const COLOR = useColors();
1075
+ return /* @__PURE__ */ jsx$1(HintRow, { hints: hints.filter((h) => Boolean(h)).map((h) => ({
1076
+ labelColor: COLOR.mute,
1077
+ ...h
1078
+ })) });
1079
+ }
1080
+ /**
1081
+ * A row of clickable hints with ` · ` separators — the renderable-per-hint
1082
+ * replacement for the span-based `renderHintSpans`. Text content is
1083
+ * byte-identical to the span renderer, so existing `hintsLength` width
1084
+ * math keeps holding.
1085
+ */
1086
+ function HintRow({ hints }) {
1087
+ const COLOR = useColors();
1088
+ return /* @__PURE__ */ jsx$1("box", {
1089
+ style: {
1090
+ flexDirection: "row",
1091
+ flexWrap: "wrap"
1092
+ },
1093
+ children: hints.map((hint, i) => /* @__PURE__ */ jsxs("box", {
1094
+ style: { flexDirection: "row" },
1095
+ children: [i > 0 && /* @__PURE__ */ jsx$1("text", {
1096
+ selectable: false,
1097
+ fg: COLOR.mute,
1098
+ children: " · "
1099
+ }), /* @__PURE__ */ jsx$1(ClickableHint, { hint })]
1100
+ }, `${hint.key}\u0000${hint.label}\u0000${i}`))
1101
+ });
1102
+ }
1103
+ //#endregion
813
1104
  //#region src/tui/modal.tsx
814
1105
  const ModalContext = createContext(null);
815
1106
  function ModalRoot({ children }) {
816
1107
  const [active, setActive] = useState(null);
817
1108
  const [lockCount, setLockCount] = useState(0);
818
1109
  const [escLockCount, setEscLockCount] = useState(0);
1110
+ const backdropRef = useRef(null);
1111
+ const renderer = useRenderer();
819
1112
  const api = useMemo(() => ({
820
1113
  open: (node) => setActive(node),
821
1114
  close: () => setActive(null),
@@ -842,6 +1135,7 @@ function ModalRoot({ children }) {
842
1135
  },
843
1136
  children
844
1137
  }), active && /* @__PURE__ */ jsx$1("box", {
1138
+ ref: backdropRef,
845
1139
  style: {
846
1140
  position: "absolute",
847
1141
  top: 0,
@@ -852,6 +1146,12 @@ function ModalRoot({ children }) {
852
1146
  justifyContent: "center",
853
1147
  zIndex: 100
854
1148
  },
1149
+ onMouseDown: (event) => {
1150
+ if (event.target !== backdropRef.current) return;
1151
+ event.stopPropagation();
1152
+ event.preventDefault();
1153
+ dispatchHintKey(renderer, "esc");
1154
+ },
855
1155
  children: active
856
1156
  })]
857
1157
  });
@@ -995,31 +1295,25 @@ function AgentPickerModal({ agents, currentAgentId, onPick }) {
995
1295
  if (option) onPick(option.value);
996
1296
  }
997
1297
  }),
998
- /* @__PURE__ */ jsxs("text", {
999
- fg: COLOR.mute,
1000
- children: [
1001
- /* @__PURE__ */ jsx$1("span", {
1002
- fg: COLOR.warn,
1003
- children: "↑↓"
1004
- }),
1005
- " navigate · ",
1006
- /* @__PURE__ */ jsx$1("span", {
1007
- fg: COLOR.warn,
1008
- children: "↵"
1009
- }),
1010
- " select · ",
1011
- /* @__PURE__ */ jsx$1("span", {
1012
- fg: COLOR.warn,
1013
- children: "esc"
1014
- }),
1015
- " close"
1016
- ]
1017
- })
1018
- ]
1019
- });
1020
- }
1021
- function EmptyState$1() {
1022
- const COLOR = useColors();
1298
+ /* @__PURE__ */ jsx$1(ModalHints, { hints: [
1299
+ {
1300
+ key: "↑↓",
1301
+ label: "navigate"
1302
+ },
1303
+ {
1304
+ key: "↵",
1305
+ label: "select"
1306
+ },
1307
+ {
1308
+ key: "esc",
1309
+ label: "close"
1310
+ }
1311
+ ] })
1312
+ ]
1313
+ });
1314
+ }
1315
+ function EmptyState$1() {
1316
+ const COLOR = useColors();
1023
1317
  return /* @__PURE__ */ jsxs(Modal, {
1024
1318
  title: "select agent",
1025
1319
  children: [/* @__PURE__ */ jsx$1("text", {
@@ -1222,31 +1516,28 @@ function CancelToolModal({ inFlight, onCancel, onCancelAll, onClose }) {
1222
1516
  callIdColWidth,
1223
1517
  childColWidth
1224
1518
  }, row.callId))
1225
- }), /* @__PURE__ */ jsxs("text", {
1226
- fg: COLOR.dim,
1227
- children: [
1228
- /* @__PURE__ */ jsx$1("span", {
1229
- fg: COLOR.warn,
1230
- children: "↑↓"
1231
- }),
1232
- " navigate · ",
1233
- /* @__PURE__ */ jsx$1("span", {
1234
- fg: COLOR.warn,
1235
- children: "↵"
1236
- }),
1237
- " cancel selected · ",
1238
- /* @__PURE__ */ jsx$1("span", {
1239
- fg: COLOR.warn,
1240
- children: "a"
1241
- }),
1242
- " cancel all · ",
1243
- /* @__PURE__ */ jsx$1("span", {
1244
- fg: COLOR.warn,
1245
- children: "esc"
1246
- }),
1247
- " close"
1248
- ]
1249
- })]
1519
+ }), /* @__PURE__ */ jsx$1(ModalHints, { hints: [
1520
+ {
1521
+ key: "↑↓",
1522
+ label: "navigate",
1523
+ labelColor: COLOR.dim
1524
+ },
1525
+ {
1526
+ key: "↵",
1527
+ label: "cancel selected",
1528
+ labelColor: COLOR.dim
1529
+ },
1530
+ {
1531
+ key: "a",
1532
+ label: "cancel all",
1533
+ labelColor: COLOR.dim
1534
+ },
1535
+ {
1536
+ key: "esc",
1537
+ label: "close",
1538
+ labelColor: COLOR.dim
1539
+ }
1540
+ ] })]
1250
1541
  });
1251
1542
  }
1252
1543
  function CancelToolRow({ row, isFocused, highlightBg, elapsedColWidth, callIdColWidth, childColWidth }) {
@@ -1359,13 +1650,7 @@ function ClaudeImportModal({ store, cwd, prefix, onDone }) {
1359
1650
  const [summary, setSummary] = useState(null);
1360
1651
  const [error, setError] = useState(null);
1361
1652
  const runningRef = useRef(false);
1362
- const counts = useMemo(() => ({
1363
- project: listClaudeSessionFiles({ cwd }).length,
1364
- all: listClaudeSessionFiles({
1365
- cwd,
1366
- allProjects: true
1367
- }).length
1368
- }), [cwd]);
1653
+ const counts = useMemo(() => countClaudeSessionFiles({ cwd }), [cwd]);
1369
1654
  const rows = [
1370
1655
  {
1371
1656
  key: "sessions",
@@ -1420,18 +1705,19 @@ function ClaudeImportModal({ store, cwd, prefix, onDone }) {
1420
1705
  await onDone?.(result);
1421
1706
  } catch {}
1422
1707
  };
1708
+ /** ↵/space/click on a row: the Import row runs, every other row toggles. */
1709
+ const activateRow = (row) => {
1710
+ if (row.key === "run") runImport();
1711
+ else setOptions((prev) => ({
1712
+ ...prev,
1713
+ [row.key]: !prev[row.key]
1714
+ }));
1715
+ };
1423
1716
  useKeyboard((key) => {
1424
1717
  if (phase !== "pick") return;
1425
1718
  if (key.name === "up" || key.ctrl && key.name === "p") setCursor((c) => (c - 1 + rows.length) % rows.length);
1426
1719
  else if (key.name === "down" || key.ctrl && key.name === "n") setCursor((c) => (c + 1) % rows.length);
1427
- else if (key.name === "return" || key.name === "space") {
1428
- const row = rows[cursor];
1429
- if (row.key === "run") runImport();
1430
- else setOptions((prev) => ({
1431
- ...prev,
1432
- [row.key]: !prev[row.key]
1433
- }));
1434
- }
1720
+ else if (key.name === "return" || key.name === "space") activateRow(rows[cursor]);
1435
1721
  });
1436
1722
  if (phase === "running") return /* @__PURE__ */ jsx$1(Modal, {
1437
1723
  title: "Import from Claude Code",
@@ -1458,26 +1744,35 @@ function ClaudeImportModal({ store, cwd, prefix, onDone }) {
1458
1744
  summary.sessions.failed.length > 0 ? `, ${summary.sessions.failed.length} failed` : ""
1459
1745
  ]
1460
1746
  }),
1747
+ summary?.sessions && failLine(summary.sessions.failed.map((f) => f.error)) !== null && /* @__PURE__ */ jsx$1("text", {
1748
+ fg: COLOR.error,
1749
+ children: failLine(summary.sessions.failed.map((f) => f.error))
1750
+ }),
1461
1751
  summary?.mcps && /* @__PURE__ */ jsx$1("text", {
1462
1752
  fg: COLOR.dim,
1463
1753
  children: copyLine("mcp servers", summary.mcps)
1464
1754
  }),
1755
+ summary?.mcps && failLine(summary.mcps.failed.map((f) => `${f.name}: ${f.error}`)) !== null && /* @__PURE__ */ jsx$1("text", {
1756
+ fg: COLOR.error,
1757
+ children: failLine(summary.mcps.failed.map((f) => `${f.name}: ${f.error}`))
1758
+ }),
1465
1759
  summary?.skills && /* @__PURE__ */ jsx$1("text", {
1466
1760
  fg: COLOR.dim,
1467
1761
  children: copyLine("skills", summary.skills)
1468
1762
  }),
1763
+ summary?.skills && failLine(summary.skills.failed.map((f) => `${f.name}: ${f.error}`)) !== null && /* @__PURE__ */ jsx$1("text", {
1764
+ fg: COLOR.error,
1765
+ children: failLine(summary.skills.failed.map((f) => `${f.name}: ${f.error}`))
1766
+ }),
1469
1767
  summary?.notes.map((note) => /* @__PURE__ */ jsx$1("text", {
1470
1768
  fg: COLOR.mute,
1471
1769
  children: `· ${note}`
1472
1770
  }, note))
1473
1771
  ]
1474
- }), /* @__PURE__ */ jsxs("text", {
1475
- fg: COLOR.mute,
1476
- children: [/* @__PURE__ */ jsx$1("span", {
1477
- fg: COLOR.warn,
1478
- children: "esc"
1479
- }), " close"]
1480
- })]
1772
+ }), /* @__PURE__ */ jsx$1(ModalHints, { hints: [{
1773
+ key: "esc",
1774
+ label: "close"
1775
+ }] })]
1481
1776
  });
1482
1777
  const enabledFor = (key) => key === "run" ? void 0 : options[key];
1483
1778
  return /* @__PURE__ */ jsxs(Modal, {
@@ -1489,6 +1784,14 @@ function ClaudeImportModal({ store, cwd, prefix, onDone }) {
1489
1784
  const enabled = enabledFor(row.key);
1490
1785
  return /* @__PURE__ */ jsxs("text", {
1491
1786
  fg: focused ? COLOR.brand : COLOR.dim,
1787
+ selectable: false,
1788
+ onMouseOver: () => setCursor(i),
1789
+ onMouseDown: (event) => {
1790
+ event.stopPropagation();
1791
+ event.preventDefault();
1792
+ setCursor(i);
1793
+ activateRow(row);
1794
+ },
1492
1795
  children: [
1493
1796
  /* @__PURE__ */ jsx$1("span", {
1494
1797
  fg: focused ? COLOR.brand : COLOR.mute,
@@ -1509,32 +1812,32 @@ function ClaudeImportModal({ store, cwd, prefix, onDone }) {
1509
1812
  ]
1510
1813
  }, row.key);
1511
1814
  })
1512
- }), /* @__PURE__ */ jsxs("text", {
1513
- fg: COLOR.mute,
1514
- children: [
1515
- /* @__PURE__ */ jsx$1("span", {
1516
- fg: COLOR.warn,
1517
- children: "↑↓"
1518
- }),
1519
- " navigate · ",
1520
- /* @__PURE__ */ jsx$1("span", {
1521
- fg: COLOR.warn,
1522
- children: "↵"
1523
- }),
1524
- " toggle / run · ",
1525
- /* @__PURE__ */ jsx$1("span", {
1526
- fg: COLOR.warn,
1527
- children: "esc"
1528
- }),
1529
- " close"
1530
- ]
1531
- })]
1815
+ }), /* @__PURE__ */ jsx$1(ModalHints, { hints: [
1816
+ {
1817
+ key: "↑↓",
1818
+ label: "navigate"
1819
+ },
1820
+ {
1821
+ key: "↵",
1822
+ label: "toggle / run"
1823
+ },
1824
+ {
1825
+ key: "esc",
1826
+ label: "close"
1827
+ }
1828
+ ] })]
1532
1829
  });
1533
1830
  }
1831
+ /** First failure reason (+count of the rest), or null when nothing failed. */
1832
+ function failLine(errors) {
1833
+ if (errors.length === 0) return null;
1834
+ return errors.length > 1 ? ` ⚠ ${errors[0]} (+${errors.length - 1} more)` : ` ⚠ ${errors[0]}`;
1835
+ }
1534
1836
  function copyLine(label, result) {
1535
1837
  const parts = [];
1536
1838
  parts.push(result.copied.length > 0 ? `${result.copied.length} copied (${result.copied.join(", ")})` : "none copied");
1537
1839
  if (result.skipped.length > 0) parts.push(`${result.skipped.length} skipped as native (${result.skipped.join(", ")})`);
1840
+ if (result.duplicates && result.duplicates.length > 0) parts.push(`${result.duplicates.length} duplicate source entr${result.duplicates.length === 1 ? "y" : "ies"} dropped (${result.duplicates.join(", ")})`);
1538
1841
  if (result.failed.length > 0) parts.push(`${result.failed.length} failed`);
1539
1842
  return `${label}: ${parts.join(" · ")}`;
1540
1843
  }
@@ -1589,290 +1892,96 @@ function pickHelper() {
1589
1892
  if (process.env.WAYLAND_DISPLAY) return {
1590
1893
  cmd: "wl-copy",
1591
1894
  args: []
1592
- };
1593
- if (process.env.DISPLAY) return {
1594
- cmd: "xclip",
1595
- args: ["-selection", "clipboard"]
1596
- };
1597
- return null;
1598
- }
1599
- /**
1600
- * Spawn the helper and pipe `text` over stdin. Fire-and-forget: we
1601
- * don't await the child's exit because the OS clipboard is populated
1602
- * synchronously by `pbcopy` / `clip.exe` / `wl-copy` / `xclip` once
1603
- * stdin closes, and a typical drag-select is human-scale latency we
1604
- * don't need to gate the next frame on.
1605
- *
1606
- * Returns `true` if the spawn looked plausibly successful, `false`
1607
- * if we couldn't even attach to the child's stdin (binary missing,
1608
- * permission denied, etc.). An `error` event after spawn flips the
1609
- * returned value to `false` retroactively — but by then the caller
1610
- * has already seen `true` and moved on. We rely on the boolean only
1611
- * for the synchronous "should we report progress to the user?" hint,
1612
- * not as a hard contract.
1613
- */
1614
- function spawnHelper(text) {
1615
- const helper = pickHelper();
1616
- if (!helper) return false;
1617
- try {
1618
- const child = spawn(helper.cmd, [...helper.args], { stdio: [
1619
- "pipe",
1620
- "ignore",
1621
- "ignore"
1622
- ] });
1623
- child.on("error", () => {});
1624
- child.stdin?.on("error", () => {});
1625
- child.stdin?.end(text, "utf8");
1626
- return true;
1627
- } catch {
1628
- return false;
1629
- }
1630
- }
1631
- /**
1632
- * Emit the OSC 52 sequence on stdout. Skips silently when stdout
1633
- * isn't a TTY so `bun run app | tee log` doesn't dump literal escape
1634
- * bytes into a pipe.
1635
- */
1636
- function writeOsc52(text) {
1637
- if (typeof process === "undefined" || !process.stdout?.write) return false;
1638
- if (!process.stdout.isTTY) return false;
1639
- try {
1640
- const encoded = Buffer.from(text, "utf8").toString("base64");
1641
- process.stdout.write(`\x1B]52;c;${encoded}\x07`);
1642
- return true;
1643
- } catch {
1644
- return false;
1645
- }
1646
- }
1647
- function writeToClipboard(text) {
1648
- const osc = writeOsc52(text);
1649
- const helper = spawnHelper(text);
1650
- return osc || helper;
1651
- }
1652
- //#endregion
1653
- //#region src/chat/diff-stat.ts
1654
- /**
1655
- * Split a line into segments: `+N` runs are `add`, `−N`/`-N` runs are `del`,
1656
- * everything else `plain`. Counts only match at a word boundary (start or
1657
- * after whitespace/`|`/`·`), so `file-42.ts` stays plain.
1658
- */
1659
- function segmentDiffStatLine(line) {
1660
- const segments = [];
1661
- const re = /(?<=^|[\s|·])(\+\d+|[−-]\d+)(?=$|[\s,·])/g;
1662
- let last = 0;
1663
- for (let m = re.exec(line); m !== null; m = re.exec(line)) {
1664
- if (m.index > last) segments.push({
1665
- text: line.slice(last, m.index),
1666
- tone: "plain"
1667
- });
1668
- segments.push({
1669
- text: m[0],
1670
- tone: m[0].startsWith("+") ? "add" : "del"
1671
- });
1672
- last = m.index + m[0].length;
1673
- }
1674
- if (last < line.length) segments.push({
1675
- text: line.slice(last),
1676
- tone: "plain"
1677
- });
1678
- return segments.length > 0 ? segments : [{
1679
- text: line,
1680
- tone: "plain"
1681
- }];
1682
- }
1683
- //#endregion
1684
- //#region src/tui/clickable.tsx
1685
- /** Glyph / word aliases → OpenTUI key names (`parse.keypress` conventions). */
1686
- const KEY_NAME_ALIASES = {
1687
- "esc": "escape",
1688
- "↵": "return",
1689
- "enter": "return",
1690
- "tab": "tab",
1691
- "space": "space",
1692
- "⌫": "backspace",
1693
- "backspace": "backspace",
1694
- "pgup": "pageup",
1695
- "pgdn": "pagedown",
1696
- "↑": "up",
1697
- "↓": "down",
1698
- "←": "left",
1699
- "→": "right",
1700
- "↑↓": "down",
1701
- "←→": "right",
1702
- "del": "delete"
1703
- };
1704
- /** Decorative pseudo-keys that must never dispatch (chips, markers). */
1705
- const NON_KEYS = /* @__PURE__ */ new Set([
1706
- "✦",
1707
- "·",
1708
- "…"
1709
- ]);
1710
- /**
1711
- * Parse a hint key spec (`ctrl+g`, `shift+↵`, `esc`, `a`, `↑↓`) into a
1712
- * dispatchable {@link ParsedKey}, or `null` when the spec is decorative
1713
- * or ambiguous (the hint then renders non-clickable). Modifier segments
1714
- * are order-insensitive; the final segment is the key itself.
1715
- */
1716
- function parseHintKeySpec(spec) {
1717
- const trimmed = spec.trim().toLowerCase();
1718
- if (!trimmed || NON_KEYS.has(trimmed)) return null;
1719
- const segments = (trimmed.split("/")[0] ?? "").split("+").map((s) => s.trim()).filter(Boolean);
1720
- if (segments.length === 0) return null;
1721
- const keyPart = segments[segments.length - 1] ?? "";
1722
- const modifiers = segments.slice(0, -1);
1723
- const mods = {
1724
- ctrl: false,
1725
- meta: false,
1726
- shift: false,
1727
- option: false
1728
- };
1729
- for (const mod of modifiers) if (mod === "ctrl" || mod === "control") mods.ctrl = true;
1730
- else if (mod === "shift") mods.shift = true;
1731
- else if (mod === "alt" || mod === "option") mods.option = true;
1732
- else if (mod === "meta" || mod === "cmd" || mod === "super") mods.meta = true;
1733
- else return null;
1734
- const name = KEY_NAME_ALIASES[keyPart] ?? (keyPart.length === 1 ? keyPart : null);
1735
- if (!name) return null;
1736
- const sequence = keySequence(name, mods);
1737
- return {
1738
- name,
1739
- ...mods,
1740
- sequence,
1741
- number: /^\d$/.test(name),
1742
- raw: sequence,
1743
- eventType: "press",
1744
- source: "raw"
1745
- };
1746
- }
1747
- /** Byte sequences real terminals send for the named specials we synthesize. */
1748
- const NAMED_KEY_SEQUENCES = {
1749
- return: "\r",
1750
- tab: " ",
1751
- escape: "\x1B",
1752
- space: " ",
1753
- backspace: "",
1754
- delete: "\x1B[3~",
1755
- up: "\x1B[A",
1756
- down: "\x1B[B",
1757
- right: "\x1B[C",
1758
- left: "\x1B[D",
1759
- pageup: "\x1B[5~",
1760
- pagedown: "\x1B[6~"
1761
- };
1762
- /**
1763
- * The `sequence`/`raw` a REAL keypress would carry — without it the
1764
- * synthetic event is second-class: focused inputs insert text from
1765
- * `key.sequence` (so a clicked `@ files` hint must actually type `@`),
1766
- * and some handlers match on `sequence` rather than `name`. Modifier
1767
- * chords return `''`: their handlers dispatch on `name` + flags, and
1768
- * fabricating control bytes would risk double interpretation.
1769
- */
1770
- function keySequence(name, mods) {
1771
- if (mods.ctrl || mods.meta || mods.option) return "";
1772
- if (mods.shift) return name === "tab" ? "\x1B[Z" : "";
1773
- if (name.length === 1) return name;
1774
- return NAMED_KEY_SEQUENCES[name] ?? "";
1775
- }
1776
- /**
1777
- * Dispatch a hint key spec through the renderer's key pipeline. Returns
1778
- * `false` for unparseable specs (no-op). Exposed for non-hint surfaces
1779
- * that want the same click==keypress contract.
1780
- */
1781
- function dispatchHintKey(renderer, spec) {
1782
- const parsed = parseHintKeySpec(spec);
1783
- if (!parsed) return false;
1784
- renderer.keyInput.processParsedKey(parsed);
1785
- return true;
1895
+ };
1896
+ if (process.env.DISPLAY) return {
1897
+ cmd: "xclip",
1898
+ args: ["-selection", "clipboard"]
1899
+ };
1900
+ return null;
1786
1901
  }
1787
1902
  /**
1788
- * True when a row click landed on the row's `[✓] ` checkbox cell — the
1789
- * columns `[startCol, startCol + width)` of the FIRST line of the target
1790
- * renderable. Rows that must stay single `<text>` renderables (the MCP
1791
- * list documents an OpenTUI layout regression forbidding box wrappers)
1792
- * can't make the checkbox its own renderable, so zone-splitting works by
1793
- * coordinate instead: `event.x/y` are absolute, the target renderable
1794
- * knows its own absolute origin, and the difference is the glyph column.
1795
- * The `y` check pins line 0 so wrapped continuation lines can't false-
1796
- * positive into the checkbox range. Degrades to `false` (row semantics)
1797
- * when the target's geometry is unavailable.
1903
+ * Spawn the helper and pipe `text` over stdin. Fire-and-forget: we
1904
+ * don't await the child's exit because the OS clipboard is populated
1905
+ * synchronously by `pbcopy` / `clip.exe` / `wl-copy` / `xclip` once
1906
+ * stdin closes, and a typical drag-select is human-scale latency we
1907
+ * don't need to gate the next frame on.
1908
+ *
1909
+ * Returns `true` if the spawn looked plausibly successful, `false`
1910
+ * if we couldn't even attach to the child's stdin (binary missing,
1911
+ * permission denied, etc.). An `error` event after spawn flips the
1912
+ * returned value to `false` retroactively — but by then the caller
1913
+ * has already seen `true` and moved on. We rely on the boolean only
1914
+ * for the synchronous "should we report progress to the user?" hint,
1915
+ * not as a hard contract.
1798
1916
  */
1799
- function inCheckboxZone(event, startCol, width = 4) {
1800
- const target = event.target;
1801
- if (!target || typeof target.x !== "number" || typeof target.y !== "number") return false;
1802
- if (event.y - target.y !== 0) return false;
1803
- const col = event.x - target.x;
1804
- return col >= startCol && col < startCol + width;
1805
- }
1806
- /** Spans for one hint's text content — shared by hover and rest states. */
1807
- function hintSpans(hint, COLOR, hovered) {
1808
- const keyColor = hovered ? COLOR.accent : hint.keyColor ?? COLOR.warn;
1809
- const labelColor = hovered ? COLOR.accent : hint.labelColor ?? COLOR.dim;
1810
- return /* @__PURE__ */ jsxs(Fragment, { children: [
1811
- hint.key !== "" && /* @__PURE__ */ jsx$1("span", {
1812
- fg: keyColor,
1813
- children: hint.key
1814
- }),
1815
- hint.extra && /* @__PURE__ */ jsx$1("span", {
1816
- fg: hint.extra.keyColor ?? COLOR.mute,
1817
- children: hint.extra.key
1818
- }),
1819
- /* @__PURE__ */ jsx$1("span", {
1820
- fg: labelColor,
1821
- children: hint.key === "" ? hint.label : ` ${hint.label}`
1822
- }),
1823
- hint.extra && /* @__PURE__ */ jsx$1("span", {
1824
- fg: hint.extra.labelColor ?? COLOR.dim,
1825
- children: ` ${hint.extra.label}`
1826
- })
1827
- ] });
1917
+ function spawnHelper(text) {
1918
+ const helper = pickHelper();
1919
+ if (!helper) return false;
1920
+ try {
1921
+ const child = spawn(helper.cmd, [...helper.args], { stdio: [
1922
+ "pipe",
1923
+ "ignore",
1924
+ "ignore"
1925
+ ] });
1926
+ child.on("error", () => {});
1927
+ child.stdin?.on("error", () => {});
1928
+ child.stdin?.end(text, "utf8");
1929
+ return true;
1930
+ } catch {
1931
+ return false;
1932
+ }
1828
1933
  }
1829
1934
  /**
1830
- * One hint as its own `<text>` renderable — clickable when its key spec
1831
- * parses, hover-highlighted in the theme accent. Non-parseable hints
1832
- * render identically but inert, so decorative chips cost nothing.
1935
+ * Emit the OSC 52 sequence on stdout. Skips silently when stdout
1936
+ * isn't a TTY so `bun run app | tee log` doesn't dump literal escape
1937
+ * bytes into a pipe.
1833
1938
  */
1834
- function ClickableHint({ hint }) {
1835
- const COLOR = useColors();
1836
- const renderer = useRenderer();
1837
- const [hovered, setHovered] = useState(false);
1838
- if (!(parseHintKeySpec(hint.key) !== null)) return /* @__PURE__ */ jsx$1("text", {
1839
- selectable: false,
1840
- children: hintSpans(hint, COLOR, false)
1841
- });
1842
- return /* @__PURE__ */ jsx$1("text", {
1843
- selectable: false,
1844
- onMouseDown: (event) => {
1845
- event.stopPropagation();
1846
- event.preventDefault();
1847
- dispatchHintKey(renderer, hint.key);
1848
- },
1849
- onMouseOver: () => setHovered(true),
1850
- onMouseOut: () => setHovered(false),
1851
- children: hintSpans(hint, COLOR, hovered)
1852
- });
1939
+ function writeOsc52(text) {
1940
+ if (typeof process === "undefined" || !process.stdout?.write) return false;
1941
+ if (!process.stdout.isTTY) return false;
1942
+ try {
1943
+ const encoded = Buffer.from(text, "utf8").toString("base64");
1944
+ process.stdout.write(`\x1B]52;c;${encoded}\x07`);
1945
+ return true;
1946
+ } catch {
1947
+ return false;
1948
+ }
1949
+ }
1950
+ function writeToClipboard(text) {
1951
+ const osc = writeOsc52(text);
1952
+ const helper = spawnHelper(text);
1953
+ return osc || helper;
1853
1954
  }
1955
+ //#endregion
1956
+ //#region src/chat/diff-stat.ts
1854
1957
  /**
1855
- * A row of clickable hints with ` · ` separators — the renderable-per-hint
1856
- * replacement for the span-based `renderHintSpans`. Text content is
1857
- * byte-identical to the span renderer, so existing `hintsLength` width
1858
- * math keeps holding.
1958
+ * Split a line into segments: `+N` runs are `add`, `−N`/`-N` runs are `del`,
1959
+ * everything else `plain`. Counts only match at a word boundary (start or
1960
+ * after whitespace/`|`/`·`), so `file-42.ts` stays plain.
1859
1961
  */
1860
- function HintRow({ hints }) {
1861
- const COLOR = useColors();
1862
- return /* @__PURE__ */ jsx$1("box", {
1863
- style: {
1864
- flexDirection: "row",
1865
- flexWrap: "wrap"
1866
- },
1867
- children: hints.map((hint, i) => /* @__PURE__ */ jsxs("box", {
1868
- style: { flexDirection: "row" },
1869
- children: [i > 0 && /* @__PURE__ */ jsx$1("text", {
1870
- selectable: false,
1871
- fg: COLOR.mute,
1872
- children: " · "
1873
- }), /* @__PURE__ */ jsx$1(ClickableHint, { hint })]
1874
- }, `${hint.key}\u0000${hint.label}\u0000${i}`))
1962
+ function segmentDiffStatLine(line) {
1963
+ const segments = [];
1964
+ const re = /(?<=^|[\s|·])(\+\d+|[−-]\d+)(?=$|[\s,·])/g;
1965
+ let last = 0;
1966
+ for (let m = re.exec(line); m !== null; m = re.exec(line)) {
1967
+ if (m.index > last) segments.push({
1968
+ text: line.slice(last, m.index),
1969
+ tone: "plain"
1970
+ });
1971
+ segments.push({
1972
+ text: m[0],
1973
+ tone: m[0].startsWith("+") ? "add" : "del"
1974
+ });
1975
+ last = m.index + m[0].length;
1976
+ }
1977
+ if (last < line.length) segments.push({
1978
+ text: line.slice(last),
1979
+ tone: "plain"
1875
1980
  });
1981
+ return segments.length > 0 ? segments : [{
1982
+ text: line,
1983
+ tone: "plain"
1984
+ }];
1876
1985
  }
1877
1986
  //#endregion
1878
1987
  //#region src/tui/crush-throbber.tsx
@@ -2510,7 +2619,7 @@ function onInputSubmit(handler) {
2510
2619
  * Width tiering is driven by plain-text length estimates — close enough
2511
2620
  * since the segments are ASCII-heavy.
2512
2621
  */
2513
- function Footer({ hints, context, cost = null, status = null, compactionTokens = null, compactionStartedAt = null, retry = null }) {
2622
+ function Footer({ hints, context, cost = null, status = null, compactionTokens = null, compactionStartedAt = null, retry = null, contextKey = null }) {
2514
2623
  const { width } = useTerminalDimensions();
2515
2624
  const showCost = typeof cost === "number" && cost > 0;
2516
2625
  const inner = Math.max(0, width - 2);
@@ -2542,7 +2651,8 @@ function Footer({ hints, context, cost = null, status = null, compactionTokens =
2542
2651
  /* @__PURE__ */ jsx$1("box", { style: { flexGrow: 1 } }),
2543
2652
  /* @__PURE__ */ jsx$1(RightStatus, {
2544
2653
  cost: showCost ? cost : null,
2545
- context
2654
+ context,
2655
+ contextKey: contextKey ?? null
2546
2656
  })
2547
2657
  ]
2548
2658
  });
@@ -2569,22 +2679,43 @@ function Footer({ hints, context, cost = null, status = null, compactionTokens =
2569
2679
  },
2570
2680
  children: [/* @__PURE__ */ jsx$1("box", { style: { flexGrow: 1 } }), /* @__PURE__ */ jsx$1(RightStatus, {
2571
2681
  cost: showCost ? cost : null,
2572
- context
2682
+ context,
2683
+ contextKey: contextKey ?? null
2573
2684
  })]
2574
2685
  })]
2575
2686
  });
2576
2687
  }
2577
- function RightStatus({ cost, context }) {
2688
+ function RightStatus({ cost, context, contextKey }) {
2578
2689
  const COLOR = useColors();
2690
+ const renderer = useRenderer();
2691
+ const [hovered, setHovered] = useState(false);
2579
2692
  if (cost == null && !context) return null;
2580
- return /* @__PURE__ */ jsxs("text", { children: [
2581
- cost != null && /* @__PURE__ */ jsx$1(CostIndicator, { cost }),
2693
+ const body = /* @__PURE__ */ jsxs(Fragment, { children: [
2694
+ cost != null && /* @__PURE__ */ jsx$1(CostIndicator, {
2695
+ cost,
2696
+ accent: hovered
2697
+ }),
2582
2698
  cost != null && context && /* @__PURE__ */ jsx$1("span", {
2583
2699
  fg: COLOR.mute,
2584
2700
  children: " · "
2585
2701
  }),
2586
- context && /* @__PURE__ */ jsx$1(ContextIndicator, { context })
2702
+ context && /* @__PURE__ */ jsx$1(ContextIndicator, {
2703
+ context,
2704
+ accent: hovered
2705
+ })
2587
2706
  ] });
2707
+ if (!(contextKey !== null && parseHintKeySpec(contextKey) !== null)) return /* @__PURE__ */ jsx$1("text", { children: body });
2708
+ return /* @__PURE__ */ jsx$1("text", {
2709
+ selectable: false,
2710
+ onMouseDown: (event) => {
2711
+ event.stopPropagation();
2712
+ event.preventDefault();
2713
+ dispatchHintKey(renderer, contextKey);
2714
+ },
2715
+ onMouseOver: () => setHovered(true),
2716
+ onMouseOut: () => setHovered(false),
2717
+ children: body
2718
+ });
2588
2719
  }
2589
2720
  /**
2590
2721
  * Right-edge status in the Footer's left section. Busy stays glyph-only;
@@ -2666,14 +2797,15 @@ function renderHintSpans(hints, COLOR) {
2666
2797
  })
2667
2798
  ] }, i));
2668
2799
  }
2669
- function ContextIndicator({ context }) {
2800
+ function ContextIndicator({ context, accent = false }) {
2670
2801
  const COLOR = useColors();
2671
2802
  const ratio = context.max > 0 ? context.used / context.max : 0;
2672
2803
  const pct = Math.round(ratio * 100);
2673
- const color = ratio >= .85 ? COLOR.error : ratio >= .6 ? COLOR.warn : COLOR.dim;
2804
+ const color = accent ? COLOR.accent : ratio >= .85 ? COLOR.error : ratio >= .6 ? COLOR.warn : COLOR.dim;
2805
+ const muted = accent ? COLOR.accent : COLOR.mute;
2674
2806
  return /* @__PURE__ */ jsxs("span", { children: [
2675
2807
  /* @__PURE__ */ jsx$1("span", {
2676
- fg: COLOR.mute,
2808
+ fg: muted,
2677
2809
  children: "ctx "
2678
2810
  }),
2679
2811
  /* @__PURE__ */ jsx$1("span", {
@@ -2681,7 +2813,7 @@ function ContextIndicator({ context }) {
2681
2813
  children: fmtTokens(context.used)
2682
2814
  }),
2683
2815
  /* @__PURE__ */ jsx$1("span", {
2684
- fg: COLOR.mute,
2816
+ fg: muted,
2685
2817
  children: ` / ${fmtTokens(context.max)} `
2686
2818
  }),
2687
2819
  /* @__PURE__ */ jsx$1("span", {
@@ -2690,11 +2822,11 @@ function ContextIndicator({ context }) {
2690
2822
  })
2691
2823
  ] });
2692
2824
  }
2693
- function CostIndicator({ cost }) {
2825
+ function CostIndicator({ cost, accent = false }) {
2694
2826
  const COLOR = useColors();
2695
- const fg = COLOR.money ?? COLOR.warn;
2827
+ const fg = accent ? COLOR.accent : COLOR.money ?? COLOR.warn;
2696
2828
  return /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx$1("span", {
2697
- fg: COLOR.mute,
2829
+ fg: accent ? COLOR.accent : COLOR.mute,
2698
2830
  children: "$"
2699
2831
  }), /* @__PURE__ */ jsx$1("span", {
2700
2832
  fg,
@@ -2763,8 +2895,11 @@ function renderGradientText(text, from, to) {
2763
2895
  * </box>
2764
2896
  * ```
2765
2897
  */
2766
- function TitleOverlay({ title, meta = null, titleColor, parentWidth, statusIcon = null, statusIconCells = 1 }) {
2898
+ function TitleOverlay({ title, meta = null, titleColor, parentWidth, statusIcon = null, statusIconCells = 1, clickKey = null }) {
2767
2899
  const COLOR = useColors();
2900
+ const renderer = useRenderer();
2901
+ const [titleHovered, setTitleHovered] = useState(false);
2902
+ const clickable = clickKey !== null && parseHintKeySpec(clickKey) !== null;
2768
2903
  const { width: termWidth } = useTerminalDimensions();
2769
2904
  const useGradient = titleColor === void 0;
2770
2905
  const gradientFrom = COLOR.throbber?.to ?? COLOR.accent;
@@ -2777,32 +2912,51 @@ function TitleOverlay({ title, meta = null, titleColor, parentWidth, statusIcon
2777
2912
  const showMeta = meta != null && metaLen > 0 && title.length + iconReserve + TITLE_OVERLAY_WRAP + TITLE_OVERLAY_GAP + metaLen + TITLE_OVERLAY_META_WRAP <= inner;
2778
2913
  const titleBudget = (showMeta ? inner - (metaLen + TITLE_OVERLAY_META_WRAP) - TITLE_OVERLAY_GAP - TITLE_OVERLAY_WRAP : inner - TITLE_OVERLAY_WRAP) - iconReserve;
2779
2914
  const visibleTitle = titleBudget <= 0 ? "" : truncateTrailing(title, titleBudget);
2780
- return /* @__PURE__ */ jsxs(Fragment, { children: [(visibleTitle || statusIcon) && /* @__PURE__ */ jsxs("text", {
2915
+ const titleBody = /* @__PURE__ */ jsxs(Fragment, { children: [
2916
+ /* @__PURE__ */ jsx$1("span", {
2917
+ fg: COLOR.mute,
2918
+ children: " "
2919
+ }),
2920
+ titleHovered ? /* @__PURE__ */ jsx$1("span", {
2921
+ fg: COLOR.accent,
2922
+ children: visibleTitle
2923
+ }) : useGradient && visibleTitle.length > 0 ? renderGradientText(visibleTitle, gradientFrom, gradientTo) : /* @__PURE__ */ jsx$1("span", {
2924
+ fg,
2925
+ children: visibleTitle
2926
+ }),
2927
+ statusIcon && /* @__PURE__ */ jsx$1("span", {
2928
+ fg: COLOR.mute,
2929
+ children: " "
2930
+ }),
2931
+ statusIcon,
2932
+ /* @__PURE__ */ jsx$1("span", {
2933
+ fg: COLOR.mute,
2934
+ children: " "
2935
+ })
2936
+ ] });
2937
+ return /* @__PURE__ */ jsxs(Fragment, { children: [(visibleTitle || statusIcon) && (clickable ? /* @__PURE__ */ jsx$1("text", {
2781
2938
  style: {
2782
2939
  position: "absolute",
2783
2940
  top: 0,
2784
2941
  left: 1
2785
2942
  },
2786
- children: [
2787
- /* @__PURE__ */ jsx$1("span", {
2788
- fg: COLOR.mute,
2789
- children: " "
2790
- }),
2791
- useGradient && visibleTitle.length > 0 ? renderGradientText(visibleTitle, gradientFrom, gradientTo) : /* @__PURE__ */ jsx$1("span", {
2792
- fg,
2793
- children: visibleTitle
2794
- }),
2795
- statusIcon && /* @__PURE__ */ jsx$1("span", {
2796
- fg: COLOR.mute,
2797
- children: " "
2798
- }),
2799
- statusIcon,
2800
- /* @__PURE__ */ jsx$1("span", {
2801
- fg: COLOR.mute,
2802
- children: " "
2803
- })
2804
- ]
2805
- }), showMeta && meta && /* @__PURE__ */ jsxs("text", {
2943
+ selectable: false,
2944
+ onMouseDown: (event) => {
2945
+ event.stopPropagation();
2946
+ event.preventDefault();
2947
+ dispatchHintKey(renderer, clickKey);
2948
+ },
2949
+ onMouseOver: () => setTitleHovered(true),
2950
+ onMouseOut: () => setTitleHovered(false),
2951
+ children: titleBody
2952
+ }) : /* @__PURE__ */ jsx$1("text", {
2953
+ style: {
2954
+ position: "absolute",
2955
+ top: 0,
2956
+ left: 1
2957
+ },
2958
+ children: titleBody
2959
+ })), showMeta && meta && /* @__PURE__ */ jsxs("text", {
2806
2960
  style: {
2807
2961
  position: "absolute",
2808
2962
  top: 0,
@@ -2893,8 +3047,73 @@ function StatusSpinner({ color }) {
2893
3047
  fg: color,
2894
3048
  children: useSpinnerFrame()
2895
3049
  });
2896
- }
2897
- function Transcript({ events, settings, selectedTurnId = null, busy = false, searchQuery, searchPending = false, onLoadEarlier }) {
3050
+ }
3051
+ /**
3052
+ * Bordered container for one run of subagent events. The box's border +
3053
+ * left padding give the visual "this is a subagent" affordance, so events
3054
+ * inside render with `depthOffset: 1` — a direct child of the parent
3055
+ * (depth 1) sits flush against the box's inner padding rather than being
3056
+ * indented twice. Grandchildren (depth ≥ 2) still indent further, so
3057
+ * nested subagents remain visually distinct.
3058
+ *
3059
+ * Memoized like {@link EventLine} so completed historical runs don't
3060
+ * re-execute per drain tick — but with a custom comparator:
3061
+ * `partitionTranscript` / `computeTurnAnchors` rebuild the per-run
3062
+ * `events` / `anchorIds` arrays on every transcript change, so the default
3063
+ * shallow compare would never bail while streaming. The inner event
3064
+ * references (and the deterministic anchor-id strings) ARE stable until
3065
+ * their content changes, so an element-wise compare recovers the bail-out.
3066
+ */
3067
+ const SubagentBlock = memo(({ events, previous, selectedTurnId = null, anchorIds, codeSettleTargets, codeSettleEpoch = 0, live, searchQuery, searchTitleMatches }) => {
3068
+ const COLOR = useColors();
3069
+ const childIds = useMemo(() => {
3070
+ const set = /* @__PURE__ */ new Set();
3071
+ for (const e of events) if (e.childId) set.add(e.childId);
3072
+ return Array.from(set);
3073
+ }, [events]);
3074
+ const title = childIds.length === 0 ? " subagent " : childIds.length === 1 ? ` ${childIds[0]} ` : ` subagents · ${childIds.join(", ")} `;
3075
+ const marginTop = previous ? 1 : 0;
3076
+ const hideChildLabel = childIds.length === 1;
3077
+ return /* @__PURE__ */ jsx$1("box", {
3078
+ title,
3079
+ style: {
3080
+ border: true,
3081
+ borderColor: COLOR.brand,
3082
+ paddingLeft: 1,
3083
+ paddingRight: 1,
3084
+ paddingTop: 0,
3085
+ paddingBottom: 0,
3086
+ marginTop,
3087
+ flexDirection: "column",
3088
+ flexShrink: 0,
3089
+ alignSelf: "stretch"
3090
+ },
3091
+ children: events.map((evt, i) => /* @__PURE__ */ jsx$1(EventLine, {
3092
+ event: evt,
3093
+ previous: events[i - 1],
3094
+ depthOffset: 1,
3095
+ selected: selectedTurnId !== null && evt.turnId === selectedTurnId,
3096
+ anchorId: anchorIds?.[i],
3097
+ hideChildLabel,
3098
+ settleCodeEpoch: codeSettleTargets?.has(evt) ? codeSettleEpoch : 0,
3099
+ liveSpawn: evt.kind === "spawn-start" ? live?.find((spawn) => spawn.id === evt.childId) : void 0,
3100
+ searchQuery,
3101
+ 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)
3102
+ }, i))
3103
+ });
3104
+ }, (prev, next) => sameElements(prev.events, next.events) && sameElements(prev.anchorIds, next.anchorIds) && prev.previous === next.previous && prev.selectedTurnId === next.selectedTurnId && prev.codeSettleTargets === next.codeSettleTargets && prev.codeSettleEpoch === next.codeSettleEpoch && prev.live === next.live && prev.searchQuery === next.searchQuery && prev.searchTitleMatches === next.searchTitleMatches);
3105
+ /** Element-wise `===` over possibly-rebuilt arrays — see {@link SubagentBlock}. */
3106
+ function sameElements(a, b) {
3107
+ if (a === b) return true;
3108
+ if (!a || !b || a.length !== b.length) return false;
3109
+ return a.every((value, i) => value === b[i]);
3110
+ }
3111
+ const NO_SEARCH_MATCHES = {
3112
+ direct: /* @__PURE__ */ new Set(),
3113
+ callIds: /* @__PURE__ */ new Set(),
3114
+ childIds: /* @__PURE__ */ new Set()
3115
+ };
3116
+ function Transcript({ events, settings, selectedTurnId = null, busy = false, searchQuery, searchPending = false, onLoadEarlier, onSelectTurn, onOpenTurn }) {
2898
3117
  const COLOR = useColors();
2899
3118
  const showThrobber = busy && settings.showThrobber;
2900
3119
  const codeSettle = useRef({
@@ -2919,14 +3138,10 @@ function Transcript({ events, settings, selectedTurnId = null, busy = false, sea
2919
3138
  const throbberLabel = events.length > 0 && events[events.length - 1].kind === "thinking" ? "Thinking" : void 0;
2920
3139
  const ownership = useMemo(() => turnSelectionOwnership(events), [events]);
2921
3140
  const searchMatches = useMemo(() => {
3141
+ if (!searchQuery) return NO_SEARCH_MATCHES;
2922
3142
  const direct = /* @__PURE__ */ new Set();
2923
3143
  const callIds = /* @__PURE__ */ new Set();
2924
3144
  const childIds = /* @__PURE__ */ new Set();
2925
- if (!searchQuery) return {
2926
- direct,
2927
- callIds,
2928
- childIds
2929
- };
2930
3145
  const stack = [];
2931
3146
  for (const event of events) {
2932
3147
  if (event.kind === "spawn-start") stack.push(event);
@@ -2955,9 +3170,13 @@ function Transcript({ events, settings, selectedTurnId = null, busy = false, sea
2955
3170
  searchMatches
2956
3171
  ]);
2957
3172
  const scrollboxRef = useRef(null);
3173
+ const mouseSelectionRef = useRef(false);
3174
+ const pressedTurnRef = useRef(null);
2958
3175
  const rowVisibility = useRef(createRowVisibilityStore());
2959
3176
  const loadEarlierRef = useRef(onLoadEarlier);
2960
3177
  loadEarlierRef.current = onLoadEarlier;
3178
+ const eventsRef = useRef(events);
3179
+ eventsRef.current = events;
2961
3180
  const earlierLoad = useRef(null);
2962
3181
  const earlierLoadArmed = useRef(false);
2963
3182
  useEffect(() => {
@@ -2970,7 +3189,7 @@ function Transcript({ events, settings, selectedTurnId = null, busy = false, sea
2970
3189
  });
2971
3190
  const topThreshold = Math.max(4, Math.floor(sb.viewport.height / 4));
2972
3191
  if (sb.scrollTop > topThreshold) earlierLoadArmed.current = true;
2973
- else if (earlierLoadArmed.current && !earlierLoad.current && loadEarlierRef.current && hiddenEarlierTranscriptCount(events) > 0) {
3192
+ else if (earlierLoadArmed.current && !earlierLoad.current && loadEarlierRef.current && hiddenEarlierTranscriptCount(eventsRef.current) > 0) {
2974
3193
  earlierLoadArmed.current = false;
2975
3194
  const snapshot = {
2976
3195
  oldHeight: sb.scrollHeight,
@@ -2984,7 +3203,7 @@ function Transcript({ events, settings, selectedTurnId = null, busy = false, sea
2984
3203
  }
2985
3204
  }, 200);
2986
3205
  return () => clearInterval(timer);
2987
- }, [events]);
3206
+ }, []);
2988
3207
  useEffect(() => {
2989
3208
  const pending = earlierLoad.current;
2990
3209
  if (!pending) return;
@@ -3038,6 +3257,10 @@ function Transcript({ events, settings, selectedTurnId = null, busy = false, sea
3038
3257
  }, [items, events]);
3039
3258
  useEffect(() => {
3040
3259
  if (!selectedTurnId) return;
3260
+ if (mouseSelectionRef.current) {
3261
+ mouseSelectionRef.current = false;
3262
+ return;
3263
+ }
3041
3264
  let handle = 0;
3042
3265
  let attempts = 0;
3043
3266
  const align = () => {
@@ -3098,7 +3321,7 @@ function Transcript({ events, settings, selectedTurnId = null, busy = false, sea
3098
3321
  children: [items.map((item, i) => {
3099
3322
  if (item.kind === "event") {
3100
3323
  const live = liveByItem.get(i);
3101
- return /* @__PURE__ */ jsx$1(EventLine, {
3324
+ const line = /* @__PURE__ */ jsx$1(EventLine, {
3102
3325
  event: item.event,
3103
3326
  previous: item.previous,
3104
3327
  selected: isTurnHighlighted(item.event, selectedTurnId, ownership),
@@ -3107,6 +3330,33 @@ function Transcript({ events, settings, selectedTurnId = null, busy = false, sea
3107
3330
  liveSpawn: live?.[0],
3108
3331
  searchQuery,
3109
3332
  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)
3333
+ });
3334
+ const selectActive = selectedTurnId !== null && (onSelectTurn !== void 0 || onOpenTurn !== void 0);
3335
+ const targetTurn = item.event.turnId && !item.event.childId ? ownership.get(item.event.turnId) ?? item.event.turnId : null;
3336
+ const active = selectActive && targetTurn !== null;
3337
+ return /* @__PURE__ */ jsx$1("box", {
3338
+ style: { flexDirection: "column" },
3339
+ onMouseOver: active ? () => {
3340
+ if (targetTurn === selectedTurnId) return;
3341
+ if (onSelectTurn?.(targetTurn) !== false) mouseSelectionRef.current = true;
3342
+ } : void 0,
3343
+ onMouseDown: active && onOpenTurn ? (event) => {
3344
+ pressedTurnRef.current = event.button === 0 ? {
3345
+ turnId: targetTurn,
3346
+ x: event.x,
3347
+ y: event.y
3348
+ } : null;
3349
+ } : void 0,
3350
+ onMouseUp: active && onOpenTurn ? (event) => {
3351
+ const pressed = pressedTurnRef.current;
3352
+ pressedTurnRef.current = null;
3353
+ if (event.button !== 0 || pressed === null || pressed.turnId !== targetTurn) return;
3354
+ if (pressed.x !== event.x || pressed.y !== event.y) return;
3355
+ event.stopPropagation();
3356
+ event.preventDefault();
3357
+ if (onOpenTurn(targetTurn) !== false && targetTurn !== selectedTurnId) mouseSelectionRef.current = true;
3358
+ } : void 0,
3359
+ children: line
3110
3360
  }, i);
3111
3361
  }
3112
3362
  return /* @__PURE__ */ jsx$1(SubagentBlock, {
@@ -3254,52 +3504,6 @@ function partitionTranscript(events, settings, searchReveal) {
3254
3504
  return items;
3255
3505
  }
3256
3506
  /**
3257
- * Bordered container for one run of subagent events. The box's border +
3258
- * left padding give the visual "this is a subagent" affordance, so events
3259
- * inside render with `depthOffset: 1` — a direct child of the parent
3260
- * (depth 1) sits flush against the box's inner padding rather than being
3261
- * indented twice. Grandchildren (depth ≥ 2) still indent further, so
3262
- * nested subagents remain visually distinct.
3263
- */
3264
- function SubagentBlock({ events, previous, selectedTurnId = null, anchorIds, codeSettleTargets, codeSettleEpoch = 0, live, searchQuery, searchTitleMatches }) {
3265
- const COLOR = useColors();
3266
- const childIds = useMemo(() => {
3267
- const set = /* @__PURE__ */ new Set();
3268
- for (const e of events) if (e.childId) set.add(e.childId);
3269
- return Array.from(set);
3270
- }, [events]);
3271
- const title = childIds.length === 0 ? " subagent " : childIds.length === 1 ? ` ${childIds[0]} ` : ` subagents · ${childIds.join(", ")} `;
3272
- const marginTop = previous ? 1 : 0;
3273
- const hideChildLabel = childIds.length === 1;
3274
- return /* @__PURE__ */ jsx$1("box", {
3275
- title,
3276
- style: {
3277
- border: true,
3278
- borderColor: COLOR.brand,
3279
- paddingLeft: 1,
3280
- paddingRight: 1,
3281
- paddingTop: 0,
3282
- paddingBottom: 0,
3283
- marginTop,
3284
- flexDirection: "column",
3285
- flexShrink: 0,
3286
- alignSelf: "stretch"
3287
- },
3288
- children: events.map((evt, i) => /* @__PURE__ */ jsx$1(EventLine, {
3289
- event: evt,
3290
- previous: events[i - 1],
3291
- depthOffset: 1,
3292
- selected: selectedTurnId !== null && evt.turnId === selectedTurnId,
3293
- anchorId: anchorIds?.[i],
3294
- hideChildLabel,
3295
- settleCodeEpoch: codeSettleTargets?.has(evt) ? codeSettleEpoch : 0,
3296
- liveSpawn: evt.kind === "spawn-start" ? live?.find((spawn) => spawn.id === evt.childId) : void 0,
3297
- searchQuery,
3298
- 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)
3299
- }, i))
3300
- });
3301
- }
3302
- /**
3303
3507
  * Stable one-row subagent lifecycle layout. Identity/model/task stay in
3304
3508
  * the left lane for the whole run; only the right tail changes:
3305
3509
  * active `2 turns · 14s · ⠋ tool call`, done `4 turns`.
@@ -4919,29 +5123,23 @@ function ContextPanelModal({ breakdown, modelUsage }) {
4919
5123
  children: "~"
4920
5124
  }), " estimated — exact counts unavailable for this provider"]
4921
5125
  }),
4922
- /* @__PURE__ */ jsx$1("text", {
4923
- fg: COLOR.dim,
4924
- children: expandableIds.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
4925
- /* @__PURE__ */ jsx$1("span", {
4926
- fg: COLOR.warn,
4927
- children: "↑↓"
4928
- }),
4929
- " navigate · ",
4930
- /* @__PURE__ */ jsx$1("span", {
4931
- fg: COLOR.warn,
4932
- children: "↵/space"
4933
- }),
4934
- " expand · ",
4935
- /* @__PURE__ */ jsx$1("span", {
4936
- fg: COLOR.warn,
4937
- children: "esc"
4938
- }),
4939
- " close"
4940
- ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx$1("span", {
4941
- fg: COLOR.warn,
4942
- children: "esc"
4943
- }), " close"] })
4944
- })
5126
+ /* @__PURE__ */ jsx$1(ModalHints, { hints: [
5127
+ expandableIds.length > 0 && {
5128
+ key: "↑↓",
5129
+ label: "navigate",
5130
+ labelColor: COLOR.dim
5131
+ },
5132
+ expandableIds.length > 0 && {
5133
+ key: "↵/space",
5134
+ label: "expand",
5135
+ labelColor: COLOR.dim
5136
+ },
5137
+ {
5138
+ key: "esc",
5139
+ label: "close",
5140
+ labelColor: COLOR.dim
5141
+ }
5142
+ ] })
4945
5143
  ]
4946
5144
  });
4947
5145
  }
@@ -5450,26 +5648,23 @@ function CreateExtensionModal({ userRoot, projectRoot, loadProjectExtensions, on
5450
5648
  fg: COLOR.error,
5451
5649
  children: error
5452
5650
  }),
5453
- /* @__PURE__ */ jsxs("text", {
5454
- fg: COLOR.dim,
5455
- children: [
5456
- /* @__PURE__ */ jsx$1("span", {
5457
- fg: COLOR.warn,
5458
- children: "tab"
5459
- }),
5460
- " scope · ",
5461
- /* @__PURE__ */ jsx$1("span", {
5462
- fg: COLOR.warn,
5463
- children: "↵"
5464
- }),
5465
- " create · ",
5466
- /* @__PURE__ */ jsx$1("span", {
5467
- fg: COLOR.warn,
5468
- children: "esc"
5469
- }),
5470
- " cancel"
5471
- ]
5472
- })
5651
+ /* @__PURE__ */ jsx$1(ModalHints, { hints: [
5652
+ {
5653
+ key: "tab",
5654
+ label: "scope",
5655
+ labelColor: COLOR.dim
5656
+ },
5657
+ {
5658
+ key: "↵",
5659
+ label: "create",
5660
+ labelColor: COLOR.dim
5661
+ },
5662
+ {
5663
+ key: "esc",
5664
+ label: "cancel",
5665
+ labelColor: COLOR.dim
5666
+ }
5667
+ ] })
5473
5668
  ]
5474
5669
  });
5475
5670
  }
@@ -5778,40 +5973,37 @@ function CwdPickerModal({ currentCwd, onPick }) {
5778
5973
  }, result.item.path);
5779
5974
  })
5780
5975
  }),
5781
- /* @__PURE__ */ jsxs("text", {
5782
- fg: COLOR.dim,
5783
- children: [
5784
- /* @__PURE__ */ jsx$1("span", {
5785
- fg: COLOR.warn,
5786
- children: "↑↓"
5787
- }),
5788
- " navigate · ",
5789
- /* @__PURE__ */ jsx$1("span", {
5790
- fg: COLOR.warn,
5791
- children: "tab"
5792
- }),
5793
- " enter dir · ",
5794
- /* @__PURE__ */ jsx$1("span", {
5795
- fg: COLOR.warn,
5796
- children: "←"
5797
- }),
5798
- " parent · ",
5799
- /* @__PURE__ */ jsx$1("span", {
5800
- fg: COLOR.warn,
5801
- children: "↵"
5802
- }),
5803
- " select · ",
5804
- /* @__PURE__ */ jsx$1("span", {
5805
- fg: COLOR.warn,
5806
- children: "esc"
5807
- }),
5808
- " close · ",
5809
- /* @__PURE__ */ jsx$1("span", {
5810
- fg: COLOR.mute,
5811
- children: `${results.length} match${results.length === 1 ? "" : "es"}`
5812
- })
5813
- ]
5814
- })
5976
+ /* @__PURE__ */ jsx$1(ModalHints, { hints: [
5977
+ {
5978
+ key: "↑↓",
5979
+ label: "navigate",
5980
+ labelColor: COLOR.dim
5981
+ },
5982
+ {
5983
+ key: "tab",
5984
+ label: "enter dir",
5985
+ labelColor: COLOR.dim
5986
+ },
5987
+ {
5988
+ key: "←",
5989
+ label: "parent",
5990
+ labelColor: COLOR.dim
5991
+ },
5992
+ {
5993
+ key: "↵",
5994
+ label: "select",
5995
+ labelColor: COLOR.dim
5996
+ },
5997
+ {
5998
+ key: "esc",
5999
+ label: "close",
6000
+ labelColor: COLOR.dim
6001
+ },
6002
+ {
6003
+ key: "",
6004
+ label: `${results.length} match${results.length === 1 ? "" : "es"}`
6005
+ }
6006
+ ] })
5815
6007
  ]
5816
6008
  });
5817
6009
  }
@@ -6098,6 +6290,21 @@ async function resolveExtensionModalNode(node) {
6098
6290
  let nextClientId = 1;
6099
6291
  /** Mirror of the TUI's ZIDANE_DEBUG-gated logger — silent by default. */
6100
6292
  const debugFallback = process.env.ZIDANE_DEBUG ? (op, error) => console.error(`[zidane/tui] history search ${op} fell back to the main thread:`, error) : () => {};
6293
+ /**
6294
+ * Rough searchable-size estimate for degraded builds — string block
6295
+ * payloads only, mirroring what index extraction keeps (attachments /
6296
+ * base64 are stripped there). Cheap enough to run inline; only feeds the
6297
+ * byte budget, so precision doesn't matter.
6298
+ */
6299
+ function estimateTurnsBytes(turns) {
6300
+ let total = 0;
6301
+ for (const turn of turns) for (const block of turn.content) {
6302
+ const b = block;
6303
+ if (typeof b.text === "string") total += b.text.length;
6304
+ if (typeof b.output === "string") total += b.output.length;
6305
+ }
6306
+ return total;
6307
+ }
6101
6308
  function workerUrl() {
6102
6309
  if (typeof Bun !== "undefined" && Bun.isStandaloneExecutable) return new URL("./history-search-worker.ts", import.meta.url);
6103
6310
  return new URL(import.meta.url.endsWith(".ts") ? "./history-search-worker.ts" : "./history-search-worker.js", import.meta.url);
@@ -6112,6 +6319,12 @@ var HistorySearchWorkerClient = class {
6112
6319
  this.createWorker = createWorker;
6113
6320
  }
6114
6321
  indexes = /* @__PURE__ */ new WeakMap();
6322
+ /**
6323
+ * Handles whose index lives in the worker (a successful `build`). Only
6324
+ * these are safe to `slim()` — a degraded handle's turns are the sole
6325
+ * source for its main-thread fallback index.
6326
+ */
6327
+ workerBuilt = /* @__PURE__ */ new WeakSet();
6115
6328
  registrations = /* @__PURE__ */ new Map();
6116
6329
  rebuilds = /* @__PURE__ */ new Map();
6117
6330
  pending = /* @__PURE__ */ new Map();
@@ -6129,10 +6342,11 @@ var HistorySearchWorkerClient = class {
6129
6342
  if (this.unavailable) return {
6130
6343
  indexId,
6131
6344
  length: turns.length,
6345
+ bytes: estimateTurnsBytes(turns),
6132
6346
  turns
6133
6347
  };
6134
6348
  try {
6135
- return {
6349
+ const handle = {
6136
6350
  ...await this.request({
6137
6351
  type: "build",
6138
6352
  indexId,
@@ -6140,16 +6354,47 @@ var HistorySearchWorkerClient = class {
6140
6354
  }, signal),
6141
6355
  turns
6142
6356
  };
6357
+ this.workerBuilt.add(handle);
6358
+ return handle;
6143
6359
  } catch (error) {
6144
6360
  if (signal?.aborted) throw signal.reason;
6145
6361
  debugFallback("build", error);
6146
6362
  return {
6147
6363
  indexId,
6148
6364
  length: turns.length,
6365
+ bytes: estimateTurnsBytes(turns),
6149
6366
  turns
6150
6367
  };
6151
6368
  }
6152
6369
  }
6370
+ /**
6371
+ * Drop the handle's main-thread turn retention once the worker owns the
6372
+ * index. Callers whose turns are alive anyway (live session, transcript
6373
+ * search) skip this; the cross-session cache calls it so 64 cached
6374
+ * handles don't pin 64 full sessions on the main thread. A slimmed
6375
+ * handle can't rebuild after a worker-side eviction — the caller is
6376
+ * expected to self-heal by rebuilding from the store on failure.
6377
+ */
6378
+ slim(index) {
6379
+ if (!this.workerBuilt.has(index)) return;
6380
+ index.turns = void 0;
6381
+ }
6382
+ /**
6383
+ * Main-thread eviction notice: free the handle's retained data AND the
6384
+ * worker's cached index. Without this the worker kept its copy until
6385
+ * its own LRU happened to cycle it out. Fire-and-forget by design.
6386
+ */
6387
+ release(index) {
6388
+ index.turns = void 0;
6389
+ index.fallback = void 0;
6390
+ if (!this.worker) return;
6391
+ try {
6392
+ this.worker.postMessage({
6393
+ type: "release",
6394
+ indexId: index.indexId
6395
+ });
6396
+ } catch {}
6397
+ }
6153
6398
  async search(index, query, limit, signal) {
6154
6399
  if (this.unavailable) return searchSessionIndexAsync(await this.fallbackIndex(index, signal), query, {
6155
6400
  limit,
@@ -6219,6 +6464,7 @@ var HistorySearchWorkerClient = class {
6219
6464
  rebuild(index) {
6220
6465
  const inFlight = this.rebuilds.get(index.indexId);
6221
6466
  if (inFlight) return inFlight;
6467
+ if (!index.turns) return Promise.reject(/* @__PURE__ */ new Error(`History search index released: ${index.indexId}`));
6222
6468
  const rebuild = this.request({
6223
6469
  type: "build",
6224
6470
  indexId: index.indexId,
@@ -6229,10 +6475,13 @@ var HistorySearchWorkerClient = class {
6229
6475
  }
6230
6476
  fallbackIndex(index, signal) {
6231
6477
  if (!("indexId" in index)) return Promise.resolve(index);
6232
- if (!index.fallback) index.fallback = buildSessionSearchIndexAsync(index.turns).catch((error) => {
6233
- index.fallback = void 0;
6234
- throw error;
6235
- });
6478
+ if (!index.fallback) {
6479
+ if (!index.turns) return Promise.reject(/* @__PURE__ */ new Error(`History search index released: ${index.indexId}`));
6480
+ index.fallback = buildSessionSearchIndexAsync(index.turns).catch((error) => {
6481
+ index.fallback = void 0;
6482
+ throw error;
6483
+ });
6484
+ }
6236
6485
  signal?.throwIfAborted();
6237
6486
  return index.fallback;
6238
6487
  }
@@ -6288,6 +6537,10 @@ var HistorySearchWorkerClient = class {
6288
6537
  }
6289
6538
  });
6290
6539
  this.registrations.set(indexId, registration);
6540
+ registration.catch(() => {
6541
+ this.registrations.delete(indexId);
6542
+ this.indexes.delete(index);
6543
+ });
6291
6544
  return registration.then(() => indexId);
6292
6545
  }
6293
6546
  request(payload, signal) {
@@ -6658,26 +6911,23 @@ function ModelOptionsPickerModal({ supportsReasoning, supportsAdaptive, currentE
6658
6911
  })]
6659
6912
  }, `option:${row.option.id}`);
6660
6913
  })]
6661
- }), /* @__PURE__ */ jsxs("text", {
6662
- fg: COLOR.dim,
6663
- children: [
6664
- /* @__PURE__ */ jsx$1("span", {
6665
- fg: COLOR.warn,
6666
- children: "↑↓"
6667
- }),
6668
- " navigate · ",
6669
- /* @__PURE__ */ jsx$1("span", {
6670
- fg: COLOR.warn,
6671
- children: "↵/space"
6672
- }),
6673
- " select/toggle · ",
6674
- /* @__PURE__ */ jsx$1("span", {
6675
- fg: COLOR.warn,
6676
- children: "esc"
6677
- }),
6678
- " close"
6679
- ]
6680
- })]
6914
+ }), /* @__PURE__ */ jsx$1(ModalHints, { hints: [
6915
+ {
6916
+ key: "↑↓",
6917
+ label: "navigate",
6918
+ labelColor: COLOR.dim
6919
+ },
6920
+ {
6921
+ key: "↵/space",
6922
+ label: "select/toggle",
6923
+ labelColor: COLOR.dim
6924
+ },
6925
+ {
6926
+ key: "esc",
6927
+ label: "close",
6928
+ labelColor: COLOR.dim
6929
+ }
6930
+ ] })]
6681
6931
  });
6682
6932
  }
6683
6933
  function SectionHeader({ label, color }) {
@@ -6998,38 +7248,37 @@ function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onAppl
6998
7248
  chainPosition: chainMode ? chainPositionOf(chainState, entry.providerKey, entry.model.id) : null
6999
7249
  }, `${entry.providerKey}:${entry.model.id}`))
7000
7250
  }),
7001
- /* @__PURE__ */ jsxs("text", {
7002
- fg: COLOR.dim,
7003
- children: [
7004
- /* @__PURE__ */ jsx$1("span", {
7005
- fg: COLOR.warn,
7006
- children: "↑↓"
7007
- }),
7008
- " navigate · ",
7009
- /* @__PURE__ */ jsx$1("span", {
7010
- fg: COLOR.warn,
7011
- children: "↵"
7012
- }),
7013
- chainMode ? " add/remove · " : " select · ",
7014
- chainMode && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx$1("span", {
7015
- fg: COLOR.warn,
7016
- children: "ctrl+shift+↑↓"
7017
- }), " reorder · "] }),
7018
- /* @__PURE__ */ jsx$1("span", {
7019
- fg: COLOR.warn,
7020
- children: "esc"
7021
- }),
7022
- chainMode ? " apply · " : " close · ",
7023
- chainMode && /* @__PURE__ */ jsx$1("span", {
7024
- fg: COLOR.brand,
7025
- children: `${chainState.length} selected · `
7026
- }),
7027
- /* @__PURE__ */ jsx$1("span", {
7028
- fg: COLOR.mute,
7029
- children: `${filtered.length} / ${catalog.length} model${catalog.length === 1 ? "" : "s"}`
7030
- })
7031
- ]
7032
- })
7251
+ /* @__PURE__ */ jsx$1(ModalHints, { hints: [
7252
+ {
7253
+ key: "↑↓",
7254
+ label: "navigate",
7255
+ labelColor: COLOR.dim
7256
+ },
7257
+ {
7258
+ key: "↵",
7259
+ label: chainMode ? "add/remove" : "select",
7260
+ labelColor: COLOR.dim
7261
+ },
7262
+ chainMode && {
7263
+ key: "ctrl+shift+↑↓",
7264
+ label: "reorder",
7265
+ labelColor: COLOR.dim
7266
+ },
7267
+ {
7268
+ key: "esc",
7269
+ label: chainMode ? "apply" : "close",
7270
+ labelColor: COLOR.dim
7271
+ },
7272
+ chainMode && {
7273
+ key: "",
7274
+ label: `${chainState.length} selected`,
7275
+ labelColor: COLOR.brand
7276
+ },
7277
+ {
7278
+ key: "",
7279
+ label: `${filtered.length} / ${catalog.length} model${catalog.length === 1 ? "" : "s"}`
7280
+ }
7281
+ ] })
7033
7282
  ]
7034
7283
  });
7035
7284
  }
@@ -7190,38 +7439,37 @@ function ModelSelectList({ catalog, multi = false, initial, onApply, onCancel, v
7190
7439
  chainPosition: multi ? picks.includes(entry.model.id) ? picks.indexOf(entry.model.id) + 1 : null : null
7191
7440
  }, `${entry.providerKey}:${entry.model.id}`))
7192
7441
  }),
7193
- /* @__PURE__ */ jsxs("text", {
7194
- fg: COLOR.dim,
7195
- children: [
7196
- /* @__PURE__ */ jsx$1("span", {
7197
- fg: COLOR.warn,
7198
- children: "↑↓"
7199
- }),
7200
- " navigate · ",
7201
- /* @__PURE__ */ jsx$1("span", {
7202
- fg: COLOR.warn,
7203
- children: "↵"
7204
- }),
7205
- multi ? " add/remove · " : " select · ",
7206
- multi && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx$1("span", {
7207
- fg: COLOR.warn,
7208
- children: "ctrl+shift+↑↓"
7209
- }), " reorder · "] }),
7210
- /* @__PURE__ */ jsx$1("span", {
7211
- fg: COLOR.warn,
7212
- children: "esc"
7213
- }),
7214
- multi ? " apply · " : " cancel · ",
7215
- multi && /* @__PURE__ */ jsx$1("span", {
7216
- fg: COLOR.brand,
7217
- children: `${picks.length} selected · `
7218
- }),
7219
- /* @__PURE__ */ jsx$1("span", {
7220
- fg: COLOR.mute,
7221
- children: `${filtered.length} / ${deduped.length} model${deduped.length === 1 ? "" : "s"}`
7222
- })
7223
- ]
7224
- })
7442
+ /* @__PURE__ */ jsx$1(ModalHints, { hints: [
7443
+ {
7444
+ key: "↑↓",
7445
+ label: "navigate",
7446
+ labelColor: COLOR.dim
7447
+ },
7448
+ {
7449
+ key: "↵",
7450
+ label: multi ? "add/remove" : "select",
7451
+ labelColor: COLOR.dim
7452
+ },
7453
+ multi && {
7454
+ key: "ctrl+shift+↑↓",
7455
+ label: "reorder",
7456
+ labelColor: COLOR.dim
7457
+ },
7458
+ {
7459
+ key: "esc",
7460
+ label: multi ? "apply" : "cancel",
7461
+ labelColor: COLOR.dim
7462
+ },
7463
+ multi && {
7464
+ key: "",
7465
+ label: `${picks.length} selected`,
7466
+ labelColor: COLOR.brand
7467
+ },
7468
+ {
7469
+ key: "",
7470
+ label: `${filtered.length} / ${deduped.length} model${deduped.length === 1 ? "" : "s"}`
7471
+ }
7472
+ ] })
7225
7473
  ]
7226
7474
  });
7227
7475
  }
@@ -7768,33 +8016,27 @@ function CompletionPopup({ state, visibleRows = 6 }) {
7768
8016
  })
7769
8017
  }, item.id);
7770
8018
  })
7771
- }), /* @__PURE__ */ jsxs("text", {
7772
- fg: COLOR.mute,
7773
- children: [
7774
- /* @__PURE__ */ jsx$1("span", {
7775
- fg: COLOR.warn,
7776
- children: "↑↓"
7777
- }),
7778
- " navigate · ",
7779
- /* @__PURE__ */ jsx$1("span", {
7780
- fg: COLOR.warn,
7781
- children: enterSelects ? "↵" : "tab"
7782
- }),
7783
- " select · ",
7784
- enterSelects ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx$1("span", {
7785
- fg: COLOR.warn,
7786
- children: "tab"
7787
- }), " scope · "] }) : /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx$1("span", {
7788
- fg: COLOR.warn,
7789
- children: "↵"
7790
- }), " submit · "] }),
7791
- /* @__PURE__ */ jsx$1("span", {
7792
- fg: COLOR.warn,
7793
- children: "esc"
7794
- }),
7795
- " close"
7796
- ]
7797
- })] });
8019
+ }), /* @__PURE__ */ jsx$1(ModalHints, { hints: [
8020
+ {
8021
+ key: "↑↓",
8022
+ label: "navigate"
8023
+ },
8024
+ {
8025
+ key: enterSelects ? "↵" : "tab",
8026
+ label: "select"
8027
+ },
8028
+ enterSelects ? {
8029
+ key: "tab",
8030
+ label: "scope"
8031
+ } : {
8032
+ key: "↵",
8033
+ label: "submit"
8034
+ },
8035
+ {
8036
+ key: "esc",
8037
+ label: "close"
8038
+ }
8039
+ ] })] });
7798
8040
  }
7799
8041
  return /* @__PURE__ */ jsxs("box", {
7800
8042
  style: {
@@ -9919,11 +10161,11 @@ function toneColor(tone, COLOR) {
9919
10161
  * the parent picker's `useKeyboard` (via a ref) to suppress single-key
9920
10162
  * shortcuts (`l` / `o` / `r`) while the user is typing into the input.
9921
10163
  */
9922
- function McpAuthorizingPanel({ serverName, authUrl, maxLineWidth, chromeWidth, inputFocused }) {
10164
+ function McpAuthorizingPanel({ serverName, authUrl, maxLineWidth, chromeWidth, inputFocused, fill = false }) {
9923
10165
  const COLOR = useColors();
9924
10166
  const inputRef = useRef(null);
9925
10167
  const [hint, setHint] = useState(null);
9926
- const status = hint && hint.tone !== "error" ? hint.text : "complete the login in your browser — waiting for the callback…";
10168
+ const status = hint && hint.tone !== "error" ? hint.text : authUrl === null ? "starting login flow…" : "complete the login in your browser — waiting for the callback…";
9927
10169
  const onSubmit = useCallback(() => {
9928
10170
  const value = inputRef.current?.value?.trim() ?? "";
9929
10171
  if (!value) return;
@@ -9953,7 +10195,7 @@ function McpAuthorizingPanel({ serverName, authUrl, maxLineWidth, chromeWidth, i
9953
10195
  return /* @__PURE__ */ jsxs("box", {
9954
10196
  style: {
9955
10197
  flexDirection: "column",
9956
- flexShrink: 0
10198
+ ...fill ? { flexGrow: 1 } : { flexShrink: 0 }
9957
10199
  },
9958
10200
  children: [/* @__PURE__ */ jsxs("box", {
9959
10201
  style: {
@@ -9961,11 +10203,12 @@ function McpAuthorizingPanel({ serverName, authUrl, maxLineWidth, chromeWidth, i
9961
10203
  border: true,
9962
10204
  borderColor: COLOR.border,
9963
10205
  padding: 1,
9964
- gap: 1
10206
+ gap: 1,
10207
+ ...fill ? { flexGrow: 1 } : {}
9965
10208
  },
9966
10209
  children: [
9967
10210
  /* @__PURE__ */ jsx$1(Spinner, { label: status }),
9968
- /* @__PURE__ */ jsx$1(OAuthAuthBlock, {
10211
+ authUrl !== null && /* @__PURE__ */ jsx$1(OAuthAuthBlock, {
9969
10212
  authUrl,
9970
10213
  maxLineWidth,
9971
10214
  chromeWidth: (chromeWidth ?? 14) + 4,
@@ -11347,7 +11590,7 @@ const EMPTY_QUEUED_MESSAGES = [];
11347
11590
  function shouldRenderTranscriptSearch(searchMode, query) {
11348
11591
  return searchMode && query.trim().length > 0;
11349
11592
  }
11350
- 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 }) {
11593
+ 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, onSelectTurn, onOpenTurn, transcriptSearchMode = false, transcriptSearchQuery = "", transcriptSearchEvents = [], transcriptSearchSearching = false, onTranscriptSearchModeChange, onTranscriptSearchQueryChange, onTranscriptSearchNavigate, onTranscriptSearchOpen, onLoadEarlierTranscript, promptTriggerHints, attachImageBinding, editorBinding, sessionDetailsBinding, liveSession = null }) {
11351
11594
  const COLOR = useColors();
11352
11595
  const titleText = session?.title ?? "New session";
11353
11596
  const sessionShortcutKey = formatBindingForDisplay(sessionDetailsBinding);
@@ -11437,6 +11680,8 @@ function ChatScreen({ cwd, events, busy, compacting = false, queuedMessages = EM
11437
11680
  events: displayedEvents,
11438
11681
  settings,
11439
11682
  selectedTurnId: selectedTurnId ?? null,
11683
+ onSelectTurn,
11684
+ onOpenTurn,
11440
11685
  searchQuery: transcriptSearchActive ? transcriptSearchQuery : void 0,
11441
11686
  searchPending: transcriptSearchSearching,
11442
11687
  busy: !transcriptSearchActive && busy && !pending && !pendingInteraction,
@@ -11485,7 +11730,8 @@ function ChatScreen({ cwd, events, busy, compacting = false, queuedMessages = EM
11485
11730
  ] }),
11486
11731
  /* @__PURE__ */ jsx$1(TitleOverlay, {
11487
11732
  title: titleText,
11488
- meta: metaSegments
11733
+ meta: metaSegments,
11734
+ clickKey: sessionDetailsBinding ?? null
11489
11735
  })
11490
11736
  ]
11491
11737
  });
@@ -13159,81 +13405,64 @@ function ActionRow$2({ pending, copyStatus, titleStatus, titleError, hasGenerate
13159
13405
  })]
13160
13406
  });
13161
13407
  }
13162
- if (copyStatus === "copied") return /* @__PURE__ */ jsxs("text", {
13163
- fg: COLOR.dim,
13164
- children: [
13165
- /* @__PURE__ */ jsx$1("span", {
13166
- fg: COLOR.accent,
13167
- children: "✓ session id copied"
13168
- }),
13169
- " · ",
13170
- /* @__PURE__ */ jsx$1("span", {
13171
- fg: COLOR.warn,
13172
- children: keys.delete
13173
- }),
13174
- " delete · ",
13175
- /* @__PURE__ */ jsx$1("span", {
13176
- fg: COLOR.warn,
13177
- children: "esc"
13178
- }),
13179
- " close"
13180
- ]
13181
- });
13182
- if (copyStatus === "failed") return /* @__PURE__ */ jsxs("text", {
13183
- fg: COLOR.dim,
13184
- children: [
13185
- /* @__PURE__ */ jsx$1("span", {
13186
- fg: COLOR.error,
13187
- children: "copy failed (terminal may not support OSC 52)"
13188
- }),
13189
- " · ",
13190
- /* @__PURE__ */ jsx$1("span", {
13191
- fg: COLOR.warn,
13192
- children: "esc"
13193
- }),
13194
- " close"
13195
- ]
13196
- });
13197
- return /* @__PURE__ */ jsxs("text", {
13198
- fg: COLOR.dim,
13199
- children: [
13200
- hasGenerate && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx$1("span", {
13201
- fg: COLOR.warn,
13202
- children: keys.generate
13203
- }), " title · "] }),
13204
- hasExport && /* @__PURE__ */ jsxs(Fragment, { children: [
13205
- /* @__PURE__ */ jsx$1("span", {
13206
- fg: COLOR.warn,
13207
- children: keys.markdown
13208
- }),
13209
- "/",
13210
- /* @__PURE__ */ jsx$1("span", {
13211
- fg: COLOR.warn,
13212
- children: keys.json
13213
- }),
13214
- " export · "
13215
- ] }),
13216
- hasCompact && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx$1("span", {
13217
- fg: COLOR.warn,
13218
- children: keys.compact
13219
- }), " compact · "] }),
13220
- /* @__PURE__ */ jsx$1("span", {
13221
- fg: COLOR.warn,
13222
- children: keys.delete
13223
- }),
13224
- " delete · ",
13225
- /* @__PURE__ */ jsx$1("span", {
13226
- fg: COLOR.warn,
13227
- children: keys.copy
13228
- }),
13229
- " copy · ",
13230
- /* @__PURE__ */ jsx$1("span", {
13231
- fg: COLOR.warn,
13232
- children: "esc"
13233
- }),
13234
- " close"
13235
- ]
13236
- });
13408
+ if (copyStatus === "copied") return /* @__PURE__ */ jsx$1(ModalHints, { hints: [
13409
+ {
13410
+ key: "",
13411
+ label: "✓ session id copied",
13412
+ labelColor: COLOR.accent
13413
+ },
13414
+ {
13415
+ key: keys.delete,
13416
+ label: "delete",
13417
+ labelColor: COLOR.dim
13418
+ },
13419
+ {
13420
+ key: "esc",
13421
+ label: "close",
13422
+ labelColor: COLOR.dim
13423
+ }
13424
+ ] });
13425
+ if (copyStatus === "failed") return /* @__PURE__ */ jsx$1(ModalHints, { hints: [{
13426
+ key: "",
13427
+ label: "copy failed (terminal may not support OSC 52)",
13428
+ labelColor: COLOR.error
13429
+ }, {
13430
+ key: "esc",
13431
+ label: "close",
13432
+ labelColor: COLOR.dim
13433
+ }] });
13434
+ return /* @__PURE__ */ jsx$1(ModalHints, { hints: [
13435
+ hasGenerate && {
13436
+ key: keys.generate,
13437
+ label: "title",
13438
+ labelColor: COLOR.dim
13439
+ },
13440
+ hasExport && {
13441
+ key: `${keys.markdown}/${keys.json}`,
13442
+ label: "export",
13443
+ labelColor: COLOR.dim
13444
+ },
13445
+ hasCompact && {
13446
+ key: keys.compact,
13447
+ label: "compact",
13448
+ labelColor: COLOR.dim
13449
+ },
13450
+ {
13451
+ key: keys.delete,
13452
+ label: "delete",
13453
+ labelColor: COLOR.dim
13454
+ },
13455
+ {
13456
+ key: keys.copy,
13457
+ label: "copy",
13458
+ labelColor: COLOR.dim
13459
+ },
13460
+ {
13461
+ key: "esc",
13462
+ label: "close",
13463
+ labelColor: COLOR.dim
13464
+ }
13465
+ ] });
13237
13466
  }
13238
13467
  /**
13239
13468
  * Sum token + cost figures across every {@link SessionRun}. Preference
@@ -14852,10 +15081,18 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
14852
15081
  useEffect(() => {
14853
15082
  setMcpRemoveArmed(null);
14854
15083
  }, [focusedMcp?.config.name, activeTab]);
14855
- const oauthPasteActive = activeTab === "mcps" && focusedMcpStatus?.kind === "authorizing" && !!focusedMcpStatus.url;
15084
+ const authorizingMcp = activeTab === "mcps" ? mcpsCatalog.map((entry) => ({
15085
+ entry,
15086
+ status: getMcpAuthStatus(authState, entry.config.name)
15087
+ })).find(({ status }) => status.kind === "authorizing") : void 0;
15088
+ const oauthPasteActive = !!authorizingMcp && authorizingMcp.status.kind === "authorizing" && !!authorizingMcp.status.url;
14856
15089
  useEffect(() => {
14857
- if (!oauthPasteActive && !editingInputKey) inputRef.current?.focus();
14858
- }, [oauthPasteActive, editingInputKey]);
15090
+ if (!oauthPasteActive && !editingInputKey && !authorizingMcp) inputRef.current?.focus();
15091
+ }, [
15092
+ oauthPasteActive,
15093
+ editingInputKey,
15094
+ authorizingMcp
15095
+ ]);
14859
15096
  useEffect(() => {
14860
15097
  if (editingInputKey) editInputRef.current?.focus();
14861
15098
  }, [editingInputKey]);
@@ -15008,8 +15245,8 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15008
15245
  }
15009
15246
  if (extInstallActive || skillInstallActive || skillImportAllActive) return;
15010
15247
  if (mcpAddActive) return;
15011
- if (key.name === "escape" && oauthPasteActive && focusedMcp) {
15012
- actions?.onCancelLoginMcp?.(focusedMcp.config.name);
15248
+ if (key.name === "escape" && oauthPasteActive && authorizingMcp) {
15249
+ actions?.onCancelLoginMcp?.(authorizingMcp.entry.config.name);
15013
15250
  return;
15014
15251
  }
15015
15252
  if (oauthPasteActive) return;
@@ -15147,7 +15384,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15147
15384
  actions?.onLogoutMcp?.(focusedMcp.config.name);
15148
15385
  return;
15149
15386
  }
15150
- if (key.name === "escape" && focusedMcpStatus.kind === "authorizing") actions?.onCancelLoginMcp?.(focusedMcp.config.name);
15387
+ if (key.name === "escape" && authorizingMcp) actions?.onCancelLoginMcp?.(authorizingMcp.entry.config.name);
15151
15388
  }
15152
15389
  if (key.ctrl && !key.shift && key.name === "l" && activeTab === "logs") {
15153
15390
  key.preventDefault();
@@ -15272,7 +15509,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15272
15509
  onSelect: clickTab
15273
15510
  })
15274
15511
  }),
15275
- /* @__PURE__ */ jsx$1("box", {
15512
+ !authorizingMcp && /* @__PURE__ */ jsx$1("box", {
15276
15513
  style: {
15277
15514
  border: true,
15278
15515
  borderColor: COLOR.borderActive,
@@ -15290,7 +15527,19 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15290
15527
  style: { flexGrow: 1 }
15291
15528
  })
15292
15529
  }),
15293
- /* @__PURE__ */ jsxs("scrollbox", {
15530
+ authorizingMcp ? /* @__PURE__ */ jsx$1("box", {
15531
+ style: {
15532
+ flexGrow: 1,
15533
+ flexDirection: "column"
15534
+ },
15535
+ children: /* @__PURE__ */ jsx$1(McpAuthorizingPanel, {
15536
+ serverName: authorizingMcp.entry.config.name,
15537
+ authUrl: authorizingMcp.status.kind === "authorizing" ? authorizingMcp.status.url ?? null : null,
15538
+ maxLineWidth: 154,
15539
+ inputFocused: true,
15540
+ fill: true
15541
+ })
15542
+ }) : /* @__PURE__ */ jsxs("scrollbox", {
15294
15543
  ref: scrollboxRef,
15295
15544
  focusable: false,
15296
15545
  style: {
@@ -15385,7 +15634,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15385
15634
  highlightBg: SURFACE.selection
15386
15635
  })
15387
15636
  }),
15388
- activeTab === "mcps" && focusedMcp && focusedMcpStatus && /* @__PURE__ */ jsx$1("box", {
15637
+ activeTab === "mcps" && !authorizingMcp && focusedMcp && focusedMcpStatus && /* @__PURE__ */ jsx$1("box", {
15389
15638
  style: { flexShrink: 0 },
15390
15639
  children: renderMcpDetailPanel(focusedMcp, focusedMcpStatus, COLOR)
15391
15640
  }),
@@ -15455,27 +15704,27 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15455
15704
  style: { flexGrow: 1 }
15456
15705
  })
15457
15706
  }),
15458
- /* @__PURE__ */ jsxs("text", {
15459
- fg: COLOR.mute,
15460
- children: [
15461
- /* @__PURE__ */ jsx$1("span", {
15462
- fg: COLOR.warn,
15463
- children: "↵"
15464
- }),
15465
- " save · ",
15466
- /* @__PURE__ */ jsx$1("span", {
15467
- fg: COLOR.warn,
15468
- children: "esc"
15469
- }),
15470
- ` cancel · empty = ${editingInputItem.placeholder}`
15471
- ]
15472
- })
15707
+ /* @__PURE__ */ jsx$1(ModalHints, { hints: [
15708
+ {
15709
+ key: "↵",
15710
+ label: "save"
15711
+ },
15712
+ {
15713
+ key: "esc",
15714
+ label: "cancel"
15715
+ },
15716
+ {
15717
+ key: "",
15718
+ label: `empty = ${editingInputItem.placeholder}`
15719
+ }
15720
+ ] })
15473
15721
  ]
15474
15722
  }),
15475
15723
  /* @__PURE__ */ jsx$1("box", {
15476
15724
  style: { flexShrink: 0 },
15477
15725
  children: /* @__PURE__ */ jsx$1(Hints, {
15478
15726
  activeTab,
15727
+ authorizing: !!authorizingMcp,
15479
15728
  focusedMcp,
15480
15729
  focusedMcpRow,
15481
15730
  focusedMcpStatus,
@@ -16816,29 +17065,7 @@ function EmptyRow({ label }) {
16816
17065
  });
16817
17066
  }
16818
17067
  function renderMcpDetailPanel(entry, status, COLOR) {
16819
- if (status.kind === "authorizing") {
16820
- if (!status.url) return /* @__PURE__ */ jsxs("box", {
16821
- style: {
16822
- flexDirection: "column",
16823
- border: ["top"],
16824
- borderColor: COLOR.border,
16825
- paddingTop: 1
16826
- },
16827
- children: [/* @__PURE__ */ jsx$1("text", {
16828
- fg: COLOR.brand,
16829
- children: `Authorizing ${entry.config.name}`
16830
- }), /* @__PURE__ */ jsx$1("text", {
16831
- fg: COLOR.dim,
16832
- children: "Starting login flow…"
16833
- })]
16834
- });
16835
- return /* @__PURE__ */ jsx$1(McpAuthorizingPanel, {
16836
- serverName: entry.config.name,
16837
- authUrl: status.url,
16838
- maxLineWidth: 154,
16839
- inputFocused: true
16840
- });
16841
- }
17068
+ if (status.kind === "authorizing") return null;
16842
17069
  if (status.kind === "error") return /* @__PURE__ */ jsxs("box", {
16843
17070
  style: {
16844
17071
  flexDirection: "column",
@@ -16938,12 +17165,12 @@ function renderMcpErrors(errors, home, warnColor, dimColor) {
16938
17165
  }, err.path))
16939
17166
  });
16940
17167
  }
16941
- function Hints({ activeTab, focusedMcp, focusedMcpRow, focusedMcpStatus, canRefreshSkills, canRefreshMcps, canEditExtension, canCreateExtension, canUpdateExtension, canRemoveExtension, canInstallExtension, canInstallSkill, canAddMcp, canRemoveSkill, canRemoveMcp, focusedExtensionExpandable }) {
17168
+ function Hints({ activeTab, authorizing = false, focusedMcp, focusedMcpRow, focusedMcpStatus, canRefreshSkills, canRefreshMcps, canEditExtension, canCreateExtension, canUpdateExtension, canRemoveExtension, canInstallExtension, canInstallSkill, canAddMcp, canRemoveSkill, canRemoveMcp, focusedExtensionExpandable }) {
16942
17169
  const COLOR = useColors();
16943
17170
  const onServerRow = focusedMcpRow?.kind === "server";
16944
17171
  const showLogin = activeTab === "mcps" && onServerRow && !!focusedMcp && !!focusedMcpStatus && canLogin$1(focusedMcp, focusedMcpStatus);
16945
17172
  const showLogout = activeTab === "mcps" && onServerRow && !!focusedMcpStatus && canLogout$1(focusedMcpStatus);
16946
- const showCancel = activeTab === "mcps" && focusedMcpStatus?.kind === "authorizing";
17173
+ const showCancel = activeTab === "mcps" && authorizing;
16947
17174
  const showRefresh = activeTab === "skills" && canRefreshSkills || activeTab === "mcps" && canRefreshMcps;
16948
17175
  return /* @__PURE__ */ jsx$1(HintRow, { hints: [
16949
17176
  {
@@ -17677,26 +17904,23 @@ function TurnDetailsModal({ turn, toolResultTurns = [], searchQuery, index, tota
17677
17904
  children: "— no previewable content —"
17678
17905
  })
17679
17906
  }),
17680
- editing ? /* @__PURE__ */ jsxs("text", {
17681
- fg: COLOR.dim,
17682
- children: [
17683
- /* @__PURE__ */ jsx$1("span", {
17684
- fg: COLOR.warn,
17685
- children: "↵"
17686
- }),
17687
- " save · ",
17688
- /* @__PURE__ */ jsx$1("span", {
17689
- fg: COLOR.warn,
17690
- children: "shift+↵"
17691
- }),
17692
- " newline · ",
17693
- /* @__PURE__ */ jsx$1("span", {
17694
- fg: COLOR.warn,
17695
- children: "esc"
17696
- }),
17697
- " cancel"
17698
- ]
17699
- }) : /* @__PURE__ */ jsx$1(ActionRow, {
17907
+ editing ? /* @__PURE__ */ jsx$1(ModalHints, { hints: [
17908
+ {
17909
+ key: "↵",
17910
+ label: "save",
17911
+ labelColor: COLOR.dim
17912
+ },
17913
+ {
17914
+ key: "shift+↵",
17915
+ label: "newline",
17916
+ labelColor: COLOR.dim
17917
+ },
17918
+ {
17919
+ key: "esc",
17920
+ label: "cancel",
17921
+ labelColor: COLOR.dim
17922
+ }
17923
+ ] }) : /* @__PURE__ */ jsx$1(ActionRow, {
17700
17924
  pending,
17701
17925
  copyStatus,
17702
17926
  canCopy: fullText.length > 0,
@@ -17758,81 +17982,72 @@ function ActionRow({ pending, copyStatus, canCopy, canEdit, forkKey, deleteKey,
17758
17982
  " cancel"
17759
17983
  ]
17760
17984
  });
17761
- if (copyStatus === "copied") return /* @__PURE__ */ jsxs("text", {
17762
- fg: COLOR.dim,
17763
- children: [
17764
- /* @__PURE__ */ jsx$1("span", {
17765
- fg: COLOR.accent,
17766
- children: "✓ copied"
17767
- }),
17768
- " · ",
17769
- /* @__PURE__ */ jsx$1("span", {
17770
- fg: COLOR.warn,
17771
- children: forkKey
17772
- }),
17773
- " fork · ",
17774
- /* @__PURE__ */ jsx$1("span", {
17775
- fg: COLOR.warn,
17776
- children: deleteKey
17777
- }),
17778
- " delete · ",
17779
- /* @__PURE__ */ jsx$1("span", {
17780
- fg: canEdit ? COLOR.warn : COLOR.mute,
17781
- children: editKey
17782
- }),
17783
- canEdit ? " edit · " : " (no text) · ",
17784
- /* @__PURE__ */ jsx$1("span", {
17785
- fg: COLOR.warn,
17786
- children: "esc"
17787
- }),
17788
- " close"
17789
- ]
17790
- });
17791
- if (copyStatus === "failed") return /* @__PURE__ */ jsxs("text", {
17792
- fg: COLOR.dim,
17793
- children: [
17794
- /* @__PURE__ */ jsx$1("span", {
17795
- fg: COLOR.error,
17796
- children: "copy failed (terminal may not support OSC 52)"
17797
- }),
17798
- " · ",
17799
- /* @__PURE__ */ jsx$1("span", {
17800
- fg: COLOR.warn,
17801
- children: "esc"
17802
- }),
17803
- " close"
17804
- ]
17805
- });
17806
- return /* @__PURE__ */ jsxs("text", {
17807
- fg: COLOR.dim,
17808
- children: [
17809
- /* @__PURE__ */ jsx$1("span", {
17810
- fg: COLOR.warn,
17811
- children: forkKey
17812
- }),
17813
- " fork · ",
17814
- /* @__PURE__ */ jsx$1("span", {
17815
- fg: COLOR.warn,
17816
- children: deleteKey
17817
- }),
17818
- " delete · ",
17819
- /* @__PURE__ */ jsx$1("span", {
17820
- fg: canCopy ? COLOR.warn : COLOR.mute,
17821
- children: copyKey
17822
- }),
17823
- canCopy ? " copy · " : " (nothing to copy) · ",
17824
- /* @__PURE__ */ jsx$1("span", {
17825
- fg: canEdit ? COLOR.warn : COLOR.mute,
17826
- children: editKey
17827
- }),
17828
- canEdit ? " edit · " : " (no text) · ",
17829
- /* @__PURE__ */ jsx$1("span", {
17830
- fg: COLOR.warn,
17831
- children: "esc"
17832
- }),
17833
- " close"
17834
- ]
17835
- });
17985
+ if (copyStatus === "copied") return /* @__PURE__ */ jsx$1(ModalHints, { hints: [
17986
+ {
17987
+ key: "",
17988
+ label: "✓ copied",
17989
+ labelColor: COLOR.accent
17990
+ },
17991
+ {
17992
+ key: forkKey,
17993
+ label: "fork",
17994
+ labelColor: COLOR.dim
17995
+ },
17996
+ {
17997
+ key: deleteKey,
17998
+ label: "delete",
17999
+ labelColor: COLOR.dim
18000
+ },
18001
+ {
18002
+ key: editKey,
18003
+ label: canEdit ? "edit" : "(no text)",
18004
+ keyColor: canEdit ? COLOR.warn : COLOR.mute,
18005
+ labelColor: COLOR.dim
18006
+ },
18007
+ {
18008
+ key: "esc",
18009
+ label: "close",
18010
+ labelColor: COLOR.dim
18011
+ }
18012
+ ] });
18013
+ if (copyStatus === "failed") return /* @__PURE__ */ jsx$1(ModalHints, { hints: [{
18014
+ key: "",
18015
+ label: "copy failed (terminal may not support OSC 52)",
18016
+ labelColor: COLOR.error
18017
+ }, {
18018
+ key: "esc",
18019
+ label: "close",
18020
+ labelColor: COLOR.dim
18021
+ }] });
18022
+ return /* @__PURE__ */ jsx$1(ModalHints, { hints: [
18023
+ {
18024
+ key: forkKey,
18025
+ label: "fork",
18026
+ labelColor: COLOR.dim
18027
+ },
18028
+ {
18029
+ key: deleteKey,
18030
+ label: "delete",
18031
+ labelColor: COLOR.dim
18032
+ },
18033
+ {
18034
+ key: copyKey,
18035
+ label: canCopy ? "copy" : "(nothing to copy)",
18036
+ keyColor: canCopy ? COLOR.warn : COLOR.mute,
18037
+ labelColor: COLOR.dim
18038
+ },
18039
+ {
18040
+ key: editKey,
18041
+ label: canEdit ? "edit" : "(no text)",
18042
+ keyColor: canEdit ? COLOR.warn : COLOR.mute,
18043
+ labelColor: COLOR.dim
18044
+ },
18045
+ {
18046
+ key: "esc",
18047
+ label: "close",
18048
+ labelColor: COLOR.dim
18049
+ }
18050
+ ] });
17836
18051
  }
17837
18052
  function buildPreviewEntries(turns, limit) {
17838
18053
  const entries = [];
@@ -17969,10 +18184,20 @@ const BASH_PAINT_INTERVAL_MS = 100;
17969
18184
  const BASH_RESULT_MAX_BYTES = 32 * 1024;
17970
18185
  /** Rolling-tail window while output streams — matches the renderer's bash display cap. */
17971
18186
  const BASH_STREAM_TAIL_LINES = 10;
17972
- /** Last `n` lines of `text` (terminal-style rolling tail for live bash output). */
18187
+ /**
18188
+ * Last `n` lines of `text` (terminal-style rolling tail for live bash
18189
+ * output). Backward newline scan, not `split` — this runs per output
18190
+ * chunk against the full accumulation (up to 1MB), and re-splitting the
18191
+ * whole snapshot every chunk is O(n²) in total streamed bytes.
18192
+ */
17973
18193
  function lastLines(text, n) {
17974
- const lines = text.split("\n");
17975
- return lines.length <= n ? text : lines.slice(lines.length - n).join("\n");
18194
+ let idx = text.length;
18195
+ for (let i = 0; i < n; i++) {
18196
+ if (idx === 0) return text;
18197
+ idx = text.lastIndexOf("\n", idx - 1);
18198
+ if (idx === -1) return text;
18199
+ }
18200
+ return text.slice(idx + 1);
17976
18201
  }
17977
18202
  /**
17978
18203
  * Poll cadence for out-of-band extension install detection (see the
@@ -17983,6 +18208,14 @@ const EXTENSION_WATCH_POLL_MS = 3e3;
17983
18208
  /** LRU bound for cross-session search indexes (full lowercased text per session). */
17984
18209
  const SESSION_SEARCH_INDEX_CACHE_MAX = 64;
17985
18210
  /**
18211
+ * Byte budget for the same cache, using each handle's indexed-text size
18212
+ * (reported by the worker build). The count cap alone lets 64 huge
18213
+ * sessions pin hundreds of MB (index copy in the worker, plus any
18214
+ * main-thread fallback indexes); the budget evicts LRU-first once the
18215
+ * summed estimate crosses it, keeping worst-case retention predictable.
18216
+ */
18217
+ const SESSION_SEARCH_INDEX_CACHE_MAX_BYTES = 64 * 1024 * 1024;
18218
+ /**
17986
18219
  * Surface failures that are normally silenced (teardown / save) when the
17987
18220
  * `ZIDANE_DEBUG` env var is set. Logging via `console.error` would otherwise
17988
18221
  * trigger OpenTUI's error console overlay and clutter the UI for end users.
@@ -18224,6 +18457,9 @@ function AppShell() {
18224
18457
  if (renderer.targetFps !== fps) renderer.targetFps = fps;
18225
18458
  if (renderer.maxFps !== fps) renderer.maxFps = fps;
18226
18459
  }, [renderer, settings.targetFps]);
18460
+ useEffect(() => {
18461
+ if (renderer.useMouse !== settings.mouseInteractions) renderer.useMouse = settings.mouseInteractions;
18462
+ }, [renderer, settings.mouseInteractions]);
18227
18463
  const lastCopiedRef = useRef("");
18228
18464
  useSelectionHandler((selection) => {
18229
18465
  if (selection.isDragging) return;
@@ -18970,9 +19206,17 @@ function AppShell() {
18970
19206
  store,
18971
19207
  getAllowedTokens: () => chatReferenceTokensRef.current
18972
19208
  }), [store]);
19209
+ const visibleChatCatalogCache = useRef(/* @__PURE__ */ new WeakMap());
18973
19210
  const visibleChatCatalog = useCallback((catalog) => {
18974
- const currentId = currentSessionIdRef.current;
18975
- return currentId ? catalog.filter((entry) => entry.id !== currentId) : [...catalog];
19211
+ const currentId = currentSessionIdRef.current ?? null;
19212
+ const hit = visibleChatCatalogCache.current.get(catalog);
19213
+ if (hit && hit.currentId === currentId) return hit.result;
19214
+ const result = currentId ? catalog.filter((entry) => entry.id !== currentId) : [...catalog];
19215
+ visibleChatCatalogCache.current.set(catalog, {
19216
+ currentId,
19217
+ result
19218
+ });
19219
+ return result;
18976
19220
  }, []);
18977
19221
  const ensureChatCatalog = useCallback(async () => {
18978
19222
  const [projectList, allList] = await Promise.all([listSessionMeta(store, { projectRoot: projectDir }), listSessionMeta(store)]);
@@ -19925,6 +20169,7 @@ function AppShell() {
19925
20169
  });
19926
20170
  agent.hooks.hook("agent:done", () => {
19927
20171
  pendingAnnotationsRef.current.clear();
20172
+ pruneTodosByRun(session);
19928
20173
  });
19929
20174
  subscribeMcpToolsCache(agent.hooks, { dataDir });
19930
20175
  return agent;
@@ -19991,7 +20236,9 @@ function AppShell() {
19991
20236
  const cachedLive = liveSessionSearchIndexRef.current;
19992
20237
  if (cachedLive?.sessionId === meta.id && cachedLive.updatedAt === meta.updatedAt && cachedLive.turnCount === live.turns.length) index = cachedLive.index;
19993
20238
  else {
20239
+ const staleLive = liveSessionSearchIndexRef.current;
19994
20240
  index = await (historySearchWorkerRef.current ??= new HistorySearchWorkerClient()).build(live.turns, abortController.signal);
20241
+ if (staleLive) historySearchWorkerRef.current?.release(staleLive.index);
19995
20242
  liveSessionSearchIndexRef.current = {
19996
20243
  sessionId: meta.id,
19997
20244
  updatedAt: meta.updatedAt,
@@ -20006,15 +20253,25 @@ function AppShell() {
20006
20253
  } else {
20007
20254
  const turns = await store.getTurns(meta.id);
20008
20255
  index = await (historySearchWorkerRef.current ??= new HistorySearchWorkerClient()).build(turns, abortController.signal);
20256
+ historySearchWorkerRef.current?.slim(index);
20009
20257
  const cache = sessionSearchCacheRef.current;
20258
+ const stale = cache.get(meta.id);
20259
+ if (stale) historySearchWorkerRef.current?.release(stale.index);
20010
20260
  cache.set(meta.id, {
20011
20261
  updatedAt: meta.updatedAt,
20012
20262
  index
20013
20263
  });
20014
- while (cache.size > SESSION_SEARCH_INDEX_CACHE_MAX) {
20264
+ let totalBytes = 0;
20265
+ for (const entry of cache.values()) totalBytes += entry.index.bytes;
20266
+ while (cache.size > 1 && (cache.size > SESSION_SEARCH_INDEX_CACHE_MAX || totalBytes > SESSION_SEARCH_INDEX_CACHE_MAX_BYTES)) {
20015
20267
  const oldest = cache.keys().next().value;
20016
20268
  if (oldest === void 0) break;
20269
+ const evicted = cache.get(oldest);
20017
20270
  cache.delete(oldest);
20271
+ if (evicted) {
20272
+ totalBytes -= evicted.index.bytes;
20273
+ historySearchWorkerRef.current?.release(evicted.index);
20274
+ }
20018
20275
  }
20019
20276
  }
20020
20277
  if (generation !== sessionSearchGenerationRef.current) return;
@@ -20022,7 +20279,24 @@ function AppShell() {
20022
20279
  if (match) matches.set(meta.id, match);
20023
20280
  } catch (error) {
20024
20281
  if (abortController.signal.aborted) throw abortController.signal.reason instanceof DOMException ? abortController.signal.reason : error;
20025
- debugLog("session content search skipped a session", error);
20282
+ const broken = sessionSearchCacheRef.current.get(meta.id);
20283
+ if (broken) {
20284
+ sessionSearchCacheRef.current.delete(meta.id);
20285
+ historySearchWorkerRef.current?.release(broken.index);
20286
+ try {
20287
+ const turns = await store.getTurns(meta.id);
20288
+ const retry = await (historySearchWorkerRef.current ??= new HistorySearchWorkerClient()).build(turns, abortController.signal);
20289
+ try {
20290
+ const match = await historySearchWorkerRef.current.history(retry, query, 3, abortController.signal);
20291
+ if (match) matches.set(meta.id, match);
20292
+ } finally {
20293
+ historySearchWorkerRef.current?.release(retry);
20294
+ }
20295
+ } catch (retryError) {
20296
+ if (abortController.signal.aborted) throw abortController.signal.reason instanceof DOMException ? abortController.signal.reason : retryError;
20297
+ debugLog("session content search retry failed", retryError);
20298
+ }
20299
+ } else debugLog("session content search skipped a session", error);
20026
20300
  }
20027
20301
  done++;
20028
20302
  if (done === sessions.length || done % 4 === 0) setSessionContentSearchProgress({
@@ -21172,6 +21446,7 @@ function AppShell() {
21172
21446
  const ctx = {
21173
21447
  args: invocation.args,
21174
21448
  argv: invocation.argv,
21449
+ invokedAs: invocation.invokedAs,
21175
21450
  ...attachments.length > 0 ? { attachments: attachments.map((a) => ({
21176
21451
  name: a.name,
21177
21452
  mediaType: a.mediaType,
@@ -21921,6 +22196,10 @@ function AppShell() {
21921
22196
  const onLoginMcp = useCallback(async (name) => {
21922
22197
  const entry = mcpsCatalogRef.current.find((d) => d.config.name === name);
21923
22198
  if (!entry) return;
22199
+ for (const [other, ctrl] of mcpLoginAbortsRef.current) if (other !== name && !ctrl.signal.aborted) {
22200
+ debugLog(`mcp-login: refusing concurrent login for "${name}"`, `"${other}" is still authorizing`);
22201
+ return;
22202
+ }
21924
22203
  mcpLoginAbortsRef.current.get(name)?.abort();
21925
22204
  const ac = new AbortController();
21926
22205
  mcpLoginAbortsRef.current.set(name, ac);
@@ -21932,6 +22211,7 @@ function AppShell() {
21932
22211
  await loginMcpServer(entry.config, {
21933
22212
  store: mcpCredentialStore,
21934
22213
  signal: ac.signal,
22214
+ renderCallbackPage: renderDefaultCallbackPage,
21935
22215
  hooks: agentRef.current?.hooks,
21936
22216
  onAuthorizationUrl: (url) => tryOpenBrowser(url.toString())
21937
22217
  });
@@ -22024,6 +22304,7 @@ function AppShell() {
22024
22304
  (async () => {
22025
22305
  const cached = transcriptSearchIndexRef.current;
22026
22306
  const index = cached?.sessionId === session.id && cached.events === events ? cached.index : await (historySearchWorkerRef.current ??= new HistorySearchWorkerClient()).build(turns, abortController.signal);
22307
+ if (cached && cached.index !== index) historySearchWorkerRef.current?.release(cached.index);
22027
22308
  transcriptSearchIndexRef.current = {
22028
22309
  sessionId: session.id,
22029
22310
  events,
@@ -22151,7 +22432,14 @@ function AppShell() {
22151
22432
  store,
22152
22433
  ...source.projectRoot ? { projectRoot: source.projectRoot } : {}
22153
22434
  });
22154
- fork.setTurns(slice);
22435
+ const rehomed = await rehomePersistedStubs(slice, resolvePersistDir({
22436
+ userDir: cacheDir,
22437
+ sessionId: source.id
22438
+ }), resolvePersistDir({
22439
+ userDir: cacheDir,
22440
+ sessionId: fork.id
22441
+ }));
22442
+ fork.setTurns(rehomed);
22155
22443
  fork.setRuns(inheritedRuns);
22156
22444
  try {
22157
22445
  await fork.save();
@@ -22164,7 +22452,8 @@ function AppShell() {
22164
22452
  }, [
22165
22453
  picked,
22166
22454
  store,
22167
- activateSession
22455
+ activateSession,
22456
+ cacheDir
22168
22457
  ]);
22169
22458
  const onDeleteTurn = useCallback(async (turnId) => {
22170
22459
  const session = sessionRef.current;
@@ -22508,9 +22797,7 @@ function AppShell() {
22508
22797
  onExportSession,
22509
22798
  keybindings
22510
22799
  ]);
22511
- const openSelectedTurn = useCallback(() => {
22512
- const id = selectedTurnId;
22513
- if (!id) return;
22800
+ const openTurnDetails = useCallback((id) => {
22514
22801
  const session = sessionRef.current;
22515
22802
  if (!session) return;
22516
22803
  const turn = session.turns.find((t) => t.id === id);
@@ -22541,7 +22828,6 @@ function AppShell() {
22541
22828
  }));
22542
22829
  }, [
22543
22830
  modal,
22544
- selectedTurnId,
22545
22831
  turnIds,
22546
22832
  transcriptSearchMode,
22547
22833
  transcriptSearchQuery,
@@ -22551,7 +22837,54 @@ function AppShell() {
22551
22837
  onEditTurn,
22552
22838
  keybindings
22553
22839
  ]);
22840
+ const openSelectedTurn = useCallback(() => {
22841
+ if (selectedTurnId) openTurnDetails(selectedTurnId);
22842
+ }, [selectedTurnId, openTurnDetails]);
22843
+ const onTranscriptSelectTurn = useCallback((id) => {
22844
+ if (!(transcriptSearchMode ? transcriptSearchTurnIds : turnIds).includes(id)) return false;
22845
+ setSelectedTurnId(id);
22846
+ return true;
22847
+ }, [
22848
+ transcriptSearchMode,
22849
+ transcriptSearchTurnIds,
22850
+ turnIds
22851
+ ]);
22852
+ const onTranscriptOpenTurn = useCallback((id) => {
22853
+ if (!(transcriptSearchMode ? transcriptSearchTurnIds : turnIds).includes(id)) return false;
22854
+ setSelectedTurnId(id);
22855
+ openTurnDetails(id);
22856
+ return true;
22857
+ }, [
22858
+ transcriptSearchMode,
22859
+ transcriptSearchTurnIds,
22860
+ turnIds,
22861
+ openTurnDetails
22862
+ ]);
22554
22863
  useKeyboard((key) => {
22864
+ if (showQuitConfirmRef.current) {
22865
+ if (key.name === "escape") {
22866
+ setShowQuitConfirm(false);
22867
+ setQuitConfirmSelected("cancel");
22868
+ return;
22869
+ }
22870
+ if (key.name === "c" && key.ctrl && !key.shift) {
22871
+ renderer.destroy();
22872
+ return;
22873
+ }
22874
+ if (key.name === "return") {
22875
+ if (quitConfirmSelectedRef.current === "quit") renderer.destroy();
22876
+ else {
22877
+ setShowQuitConfirm(false);
22878
+ setQuitConfirmSelected("cancel");
22879
+ }
22880
+ return;
22881
+ }
22882
+ if (key.name === "tab" || key.name === "left" || key.name === "right") {
22883
+ setQuitConfirmSelected((prev) => prev === "cancel" ? "quit" : "cancel");
22884
+ return;
22885
+ }
22886
+ return;
22887
+ }
22555
22888
  if (modal.isOpen) return;
22556
22889
  if (transcriptSearchMode && screen === "chat") {
22557
22890
  if (key.name === "escape") onTranscriptSearchModeChange(false);
@@ -22763,7 +23096,7 @@ function AppShell() {
22763
23096
  }));
22764
23097
  return;
22765
23098
  }
22766
- if (key.ctrl && !key.shift && !key.meta && (key.name === "c" || key.name === "x") && !showQuitConfirmRef.current) {
23099
+ if (key.ctrl && !key.shift && !key.meta && (key.name === "c" || key.name === "x")) {
22767
23100
  const editor = renderer.currentFocusedEditor;
22768
23101
  if (editor?.hasSelection()) {
22769
23102
  const text = editor.getSelectedText();
@@ -22772,30 +23105,6 @@ function AppShell() {
22772
23105
  return;
22773
23106
  }
22774
23107
  }
22775
- if (showQuitConfirmRef.current) {
22776
- if (key.name === "escape") {
22777
- setShowQuitConfirm(false);
22778
- setQuitConfirmSelected("cancel");
22779
- return;
22780
- }
22781
- if (key.name === "c" && key.ctrl && !key.shift) {
22782
- renderer.destroy();
22783
- return;
22784
- }
22785
- if (key.name === "return") {
22786
- if (quitConfirmSelectedRef.current === "quit") renderer.destroy();
22787
- else {
22788
- setShowQuitConfirm(false);
22789
- setQuitConfirmSelected("cancel");
22790
- }
22791
- return;
22792
- }
22793
- if (key.name === "tab" || key.name === "left" || key.name === "right") {
22794
- setQuitConfirmSelected((prev) => prev === "cancel" ? "quit" : "cancel");
22795
- return;
22796
- }
22797
- return;
22798
- }
22799
23108
  if (key.name === "c" && key.ctrl && !key.shift) {
22800
23109
  const mode = settingsRef.current.ctrlCBehavior;
22801
23110
  if (mode === "quit") {
@@ -23049,6 +23358,8 @@ function AppShell() {
23049
23358
  enterSelectsCompletion: (providerId) => providerId === "chats",
23050
23359
  onPopupOpenChange,
23051
23360
  selectedTurnId,
23361
+ onSelectTurn: onTranscriptSelectTurn,
23362
+ onOpenTurn: onTranscriptOpenTurn,
23052
23363
  transcriptSearchMode,
23053
23364
  transcriptSearchQuery,
23054
23365
  transcriptSearchEvents,
@@ -23072,15 +23383,16 @@ function AppShell() {
23072
23383
  status: transcriptSearchMode && transcriptSearchSearching ? "searching" : pendingInteraction?.kind === "question" ? "asking" : compacting ? "compacting" : retryInfo && busy ? "retrying" : busy ? "busy" : null,
23073
23384
  compactionTokens,
23074
23385
  compactionStartedAt,
23075
- retry: retryInfo
23386
+ retry: retryInfo,
23387
+ contextKey: keybindings.openContextPanel
23076
23388
  }),
23077
23389
  showQuitConfirm && /* @__PURE__ */ jsxs("box", {
23078
23390
  position: "absolute",
23079
23391
  top: "50%",
23080
23392
  left: "50%",
23081
23393
  marginTop: -3,
23082
- marginLeft: -18,
23083
- width: 36,
23394
+ marginLeft: -21,
23395
+ width: 42,
23084
23396
  height: 7,
23085
23397
  borderStyle: "rounded",
23086
23398
  borderColor: COLOR.warn,
@@ -23089,35 +23401,52 @@ function AppShell() {
23089
23401
  alignItems: "center",
23090
23402
  justifyContent: "center",
23091
23403
  gap: 1,
23092
- children: [/* @__PURE__ */ jsx$1("text", { children: "Quit zidane?" }), /* @__PURE__ */ jsxs("box", {
23093
- flexDirection: "row",
23094
- gap: 2,
23095
- children: [/* @__PURE__ */ jsx$1("box", {
23096
- backgroundColor: quitConfirmSelected === "cancel" ? COLOR.accent : void 0,
23097
- onMouseDown: (event) => {
23098
- event.stopPropagation();
23099
- event.preventDefault();
23100
- if (quitConfirmSelectedRef.current === "cancel") setShowQuitConfirm(false);
23101
- else setQuitConfirmSelected("cancel");
23404
+ children: [
23405
+ /* @__PURE__ */ jsx$1("text", { children: "Quit zidane?" }),
23406
+ /* @__PURE__ */ jsxs("box", {
23407
+ flexDirection: "row",
23408
+ gap: 2,
23409
+ children: [/* @__PURE__ */ jsx$1("box", {
23410
+ backgroundColor: quitConfirmSelected === "cancel" ? COLOR.accent : void 0,
23411
+ onMouseDown: (event) => {
23412
+ event.stopPropagation();
23413
+ event.preventDefault();
23414
+ if (quitConfirmSelectedRef.current === "cancel") setShowQuitConfirm(false);
23415
+ else setQuitConfirmSelected("cancel");
23416
+ },
23417
+ children: /* @__PURE__ */ jsx$1("text", {
23418
+ fg: quitConfirmSelected === "cancel" ? SURFACE.modal : COLOR.dim,
23419
+ children: " Cancel "
23420
+ })
23421
+ }), /* @__PURE__ */ jsx$1("box", {
23422
+ backgroundColor: quitConfirmSelected === "quit" ? COLOR.warn : void 0,
23423
+ onMouseDown: (event) => {
23424
+ event.stopPropagation();
23425
+ event.preventDefault();
23426
+ if (quitConfirmSelectedRef.current === "quit") renderer.destroy();
23427
+ else setQuitConfirmSelected("quit");
23428
+ },
23429
+ children: /* @__PURE__ */ jsx$1("text", {
23430
+ fg: quitConfirmSelected === "quit" ? SURFACE.modal : COLOR.dim,
23431
+ children: " Quit "
23432
+ })
23433
+ })]
23434
+ }),
23435
+ /* @__PURE__ */ jsx$1(ModalHints, { hints: [
23436
+ {
23437
+ key: "tab",
23438
+ label: "switch"
23102
23439
  },
23103
- children: /* @__PURE__ */ jsx$1("text", {
23104
- fg: quitConfirmSelected === "cancel" ? SURFACE.modal : COLOR.dim,
23105
- children: " Cancel "
23106
- })
23107
- }), /* @__PURE__ */ jsx$1("box", {
23108
- backgroundColor: quitConfirmSelected === "quit" ? COLOR.warn : void 0,
23109
- onMouseDown: (event) => {
23110
- event.stopPropagation();
23111
- event.preventDefault();
23112
- if (quitConfirmSelectedRef.current === "quit") renderer.destroy();
23113
- else setQuitConfirmSelected("quit");
23440
+ {
23441
+ key: "↵",
23442
+ label: "confirm"
23114
23443
  },
23115
- children: /* @__PURE__ */ jsx$1("text", {
23116
- fg: quitConfirmSelected === "quit" ? SURFACE.modal : COLOR.dim,
23117
- children: " Quit "
23118
- })
23119
- })]
23120
- })]
23444
+ {
23445
+ key: "esc",
23446
+ label: "cancel"
23447
+ }
23448
+ ] })
23449
+ ]
23121
23450
  })
23122
23451
  ]
23123
23452
  });
@@ -23534,30 +23863,27 @@ function EffortPickerModal({ current, supportsAdaptive, onPick }) {
23534
23863
  highlightBg: SURFACE.selection
23535
23864
  }, level.id))
23536
23865
  }),
23537
- /* @__PURE__ */ jsxs("text", {
23538
- fg: COLOR.dim,
23539
- children: [
23540
- /* @__PURE__ */ jsx$1("span", {
23541
- fg: COLOR.warn,
23542
- children: "↑↓"
23543
- }),
23544
- " navigate · ",
23545
- /* @__PURE__ */ jsx$1("span", {
23546
- fg: COLOR.warn,
23547
- children: "↵"
23548
- }),
23549
- " select · ",
23550
- /* @__PURE__ */ jsx$1("span", {
23551
- fg: COLOR.warn,
23552
- children: "esc"
23553
- }),
23554
- " close · ",
23555
- /* @__PURE__ */ jsx$1("span", {
23556
- fg: COLOR.mute,
23557
- children: `${filtered.length} / ${levels.length} level${levels.length === 1 ? "" : "s"}`
23558
- })
23559
- ]
23560
- })
23866
+ /* @__PURE__ */ jsx$1(ModalHints, { hints: [
23867
+ {
23868
+ key: "↑↓",
23869
+ label: "navigate",
23870
+ labelColor: COLOR.dim
23871
+ },
23872
+ {
23873
+ key: "↵",
23874
+ label: "select",
23875
+ labelColor: COLOR.dim
23876
+ },
23877
+ {
23878
+ key: "esc",
23879
+ label: "close",
23880
+ labelColor: COLOR.dim
23881
+ },
23882
+ {
23883
+ key: "",
23884
+ label: `${filtered.length} / ${levels.length} level${levels.length === 1 ? "" : "s"}`
23885
+ }
23886
+ ] })
23561
23887
  ]
23562
23888
  });
23563
23889
  }
@@ -23648,20 +23974,13 @@ function ToggleListModal({ catalog, keyOf, settingKey, gcKeys, title, renderDeta
23648
23974
  children: [
23649
23975
  preamble,
23650
23976
  emptyState,
23651
- /* @__PURE__ */ jsxs("text", {
23652
- fg: COLOR.mute,
23653
- children: [
23654
- onRefresh && /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx$1("span", {
23655
- fg: COLOR.warn,
23656
- children: "ctrl+r"
23657
- }), " refresh · "] }),
23658
- /* @__PURE__ */ jsx$1("span", {
23659
- fg: COLOR.warn,
23660
- children: "esc"
23661
- }),
23662
- " close"
23663
- ]
23664
- })
23977
+ /* @__PURE__ */ jsx$1(ModalHints, { hints: [onRefresh && {
23978
+ key: "ctrl+r",
23979
+ label: "refresh"
23980
+ }, {
23981
+ key: "esc",
23982
+ label: "close"
23983
+ }] })
23665
23984
  ]
23666
23985
  });
23667
23986
  return /* @__PURE__ */ jsxs(Modal, {
@@ -23697,30 +24016,24 @@ function ToggleListModal({ catalog, keyOf, settingKey, gcKeys, title, renderDeta
23697
24016
  }, name);
23698
24017
  })
23699
24018
  }),
23700
- /* @__PURE__ */ jsxs("text", {
23701
- fg: COLOR.mute,
23702
- children: [
23703
- /* @__PURE__ */ jsx$1("span", {
23704
- fg: COLOR.warn,
23705
- children: "↑↓"
23706
- }),
23707
- " navigate · ",
23708
- /* @__PURE__ */ jsx$1("span", {
23709
- fg: COLOR.warn,
23710
- children: "↵"
23711
- }),
23712
- " toggle · ",
23713
- onRefresh && /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx$1("span", {
23714
- fg: COLOR.warn,
23715
- children: "ctrl+r"
23716
- }), " refresh · "] }),
23717
- /* @__PURE__ */ jsx$1("span", {
23718
- fg: COLOR.warn,
23719
- children: "esc"
23720
- }),
23721
- " close"
23722
- ]
23723
- })
24019
+ /* @__PURE__ */ jsx$1(ModalHints, { hints: [
24020
+ {
24021
+ key: "↑↓",
24022
+ label: "navigate"
24023
+ },
24024
+ {
24025
+ key: "↵",
24026
+ label: "toggle"
24027
+ },
24028
+ onRefresh && {
24029
+ key: "ctrl+r",
24030
+ label: "refresh"
24031
+ },
24032
+ {
24033
+ key: "esc",
24034
+ label: "close"
24035
+ }
24036
+ ] })
23724
24037
  ]
23725
24038
  });
23726
24039
  }
@@ -24030,20 +24343,13 @@ function McpsSettingsModal({ catalog, errors, toolsByServer, onLogin, onLogout,
24030
24343
  children: " { \"mcpServers\": { ... } } "
24031
24344
  }), "wrapper from Claude Code / Cursor also works."]
24032
24345
  }),
24033
- /* @__PURE__ */ jsxs("text", {
24034
- fg: COLOR.mute,
24035
- children: [
24036
- onRefresh && /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx$1("span", {
24037
- fg: COLOR.warn,
24038
- children: "r"
24039
- }), " refresh · "] }),
24040
- /* @__PURE__ */ jsx$1("span", {
24041
- fg: COLOR.warn,
24042
- children: "esc"
24043
- }),
24044
- " close"
24045
- ]
24046
- })
24346
+ /* @__PURE__ */ jsx$1(ModalHints, { hints: [onRefresh && {
24347
+ key: "r",
24348
+ label: "refresh"
24349
+ }, {
24350
+ key: "esc",
24351
+ label: "close"
24352
+ }] })
24047
24353
  ]
24048
24354
  });
24049
24355
  const groups = [];
@@ -24098,8 +24404,7 @@ function McpsSettingsModal({ catalog, errors, toolsByServer, onLogin, onLogout,
24098
24404
  focusedEntry,
24099
24405
  focusedStatus,
24100
24406
  expanded,
24101
- showRefresh: !!onRefresh,
24102
- COLOR
24407
+ showRefresh: !!onRefresh
24103
24408
  })
24104
24409
  ]
24105
24410
  });
@@ -24330,7 +24635,7 @@ function renderDetailPanel(entry, status, COLOR) {
24330
24635
  });
24331
24636
  return null;
24332
24637
  }
24333
- function renderActionHints({ focusedRow, focusedEntry, focusedStatus, expanded, showRefresh, COLOR }) {
24638
+ function renderActionHints({ focusedRow, focusedEntry, focusedStatus, expanded, showRefresh }) {
24334
24639
  const effectiveStatus = focusedStatus ?? { kind: "idle" };
24335
24640
  const onServerRow = focusedRow?.kind === "server";
24336
24641
  const canL = onServerRow && focusedEntry ? canLogin(focusedEntry, effectiveStatus) : false;
@@ -24338,77 +24643,44 @@ function renderActionHints({ focusedRow, focusedEntry, focusedStatus, expanded,
24338
24643
  const canCancel = effectiveStatus.kind === "authorizing";
24339
24644
  const canExpand = onServerRow && focusedEntry ? !expanded.has(focusedEntry.config.name) : false;
24340
24645
  const canCollapse = focusedRow?.kind === "tool" || (onServerRow && focusedEntry ? expanded.has(focusedEntry.config.name) : false);
24341
- return /* @__PURE__ */ jsxs("text", {
24342
- fg: COLOR.mute,
24343
- children: [
24344
- /* @__PURE__ */ jsx$1("span", {
24345
- fg: COLOR.warn,
24346
- children: "↑↓"
24347
- }),
24348
- " navigate · ",
24349
- /* @__PURE__ */ jsx$1("span", {
24350
- fg: COLOR.warn,
24351
- children: "↵"
24352
- }),
24353
- " toggle",
24354
- canExpand && /* @__PURE__ */ jsxs("span", { children: [
24355
- " · ",
24356
- /* @__PURE__ */ jsx$1("span", {
24357
- fg: COLOR.warn,
24358
- children: "→"
24359
- }),
24360
- " expand"
24361
- ] }),
24362
- canCollapse && /* @__PURE__ */ jsxs("span", { children: [
24363
- " · ",
24364
- /* @__PURE__ */ jsx$1("span", {
24365
- fg: COLOR.warn,
24366
- children: "←"
24367
- }),
24368
- " collapse"
24369
- ] }),
24370
- canL && /* @__PURE__ */ jsxs("span", { children: [
24371
- " · ",
24372
- /* @__PURE__ */ jsx$1("span", {
24373
- fg: COLOR.warn,
24374
- children: "l"
24375
- }),
24376
- " login"
24377
- ] }),
24378
- canO && /* @__PURE__ */ jsxs("span", { children: [
24379
- " · ",
24380
- /* @__PURE__ */ jsx$1("span", {
24381
- fg: COLOR.warn,
24382
- children: "o"
24383
- }),
24384
- " logout"
24385
- ] }),
24386
- showRefresh && /* @__PURE__ */ jsxs("span", { children: [
24387
- " · ",
24388
- /* @__PURE__ */ jsx$1("span", {
24389
- fg: COLOR.warn,
24390
- children: "r"
24391
- }),
24392
- " refresh"
24393
- ] }),
24394
- canCancel && /* @__PURE__ */ jsxs("span", { children: [
24395
- " · ",
24396
- /* @__PURE__ */ jsx$1("span", {
24397
- fg: COLOR.warn,
24398
- children: "esc"
24399
- }),
24400
- " cancel"
24401
- ] }),
24402
- !canCancel && /* @__PURE__ */ jsxs("span", { children: [
24403
- " · ",
24404
- /* @__PURE__ */ jsx$1("span", {
24405
- fg: COLOR.warn,
24406
- children: "esc"
24407
- }),
24408
- " close"
24409
- ] })
24410
- ]
24411
- });
24646
+ return /* @__PURE__ */ jsx$1(ModalHints, { hints: [
24647
+ {
24648
+ key: "↑↓",
24649
+ label: "navigate"
24650
+ },
24651
+ {
24652
+ key: "↵",
24653
+ label: "toggle"
24654
+ },
24655
+ canExpand && {
24656
+ key: "→",
24657
+ label: "expand"
24658
+ },
24659
+ canCollapse && {
24660
+ key: "←",
24661
+ label: "collapse"
24662
+ },
24663
+ canL && {
24664
+ key: "l",
24665
+ label: "login"
24666
+ },
24667
+ canO && {
24668
+ key: "o",
24669
+ label: "logout"
24670
+ },
24671
+ showRefresh && {
24672
+ key: "r",
24673
+ label: "refresh"
24674
+ },
24675
+ canCancel && {
24676
+ key: "esc",
24677
+ label: "cancel"
24678
+ },
24679
+ !canCancel && {
24680
+ key: "esc",
24681
+ label: "close"
24682
+ }
24683
+ ] });
24412
24684
  }
24413
24685
  function renderErrors(errors, home, warnColor) {
24414
24686
  if (!errors || errors.length === 0) return null;
@@ -24633,6 +24905,7 @@ async function runTui(options = {}) {
24633
24905
  debounceDelay: 0,
24634
24906
  targetFps: bootFps,
24635
24907
  maxFps: bootFps,
24908
+ useMouse: config.initialSettings.mouseInteractions ?? DEFAULT_SETTINGS.mouseInteractions,
24636
24909
  gatherStats
24637
24910
  });
24638
24911
  bootTick("runTui:after-createCliRenderer");