zidane 6.2.24 → 6.2.27

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 (114) hide show
  1. package/dist/{acp-CTn1kFvP.js → acp-R4-zVIud.js} +4 -4
  2. package/dist/{acp-CTn1kFvP.js.map → acp-R4-zVIud.js.map} +1 -1
  3. package/dist/acp-cli.js +5 -5
  4. package/dist/acp.d.ts +1 -1
  5. package/dist/acp.js +1 -1
  6. package/dist/{agent-tLfGSSQw.js → agent-DIe8yzcb.js} +343 -30
  7. package/dist/agent-DIe8yzcb.js.map +1 -0
  8. package/dist/agent.d.ts +1 -1
  9. package/dist/agent.js +2 -2
  10. package/dist/{anthropic-uKvj1yle.js → anthropic-BEYY0hVK.js} +66 -10
  11. package/dist/anthropic-BEYY0hVK.js.map +1 -0
  12. package/dist/{auth-CpdQebUy.js → auth-C4YPRDN4.js} +3 -3
  13. package/dist/{auth-CpdQebUy.js.map → auth-C4YPRDN4.js.map} +1 -1
  14. package/dist/chat/pure.d.ts +3 -3
  15. package/dist/chat/pure.js +1 -1
  16. package/dist/chat.d.ts +4 -4
  17. package/dist/chat.d.ts.map +1 -1
  18. package/dist/chat.js +5 -5
  19. package/dist/contexts/daytona.d.ts +1 -1
  20. package/dist/contexts/daytona.d.ts.map +1 -1
  21. package/dist/contexts/daytona.js +9 -2
  22. package/dist/contexts/daytona.js.map +1 -1
  23. package/dist/contexts/docker.js +1 -1
  24. package/dist/contexts/e2b.d.ts +1 -1
  25. package/dist/contexts/e2b.d.ts.map +1 -1
  26. package/dist/contexts/e2b.js +3 -4
  27. package/dist/contexts/e2b.js.map +1 -1
  28. package/dist/{contexts-A4ep-iJs.js → contexts-0rmrBhTK.js} +8 -5
  29. package/dist/contexts-0rmrBhTK.js.map +1 -0
  30. package/dist/contexts.js +1 -1
  31. package/dist/eval.d.ts +1 -1
  32. package/dist/eval.js +2 -2
  33. package/dist/extensions.d.ts +1 -1
  34. package/dist/extensions.js +1 -1
  35. package/dist/{output-cap-kBUHBlnq.js → glob-DluQFSw1.js} +2 -50
  36. package/dist/glob-DluQFSw1.js.map +1 -0
  37. package/dist/headless.d.ts +1 -1
  38. package/dist/headless.js +5 -5
  39. package/dist/history-search-worker.js +6 -1
  40. package/dist/history-search-worker.js.map +1 -1
  41. package/dist/{index-8zm_hwdY.d.ts → index-B6eBI_OD.d.ts} +312 -96
  42. package/dist/index-B6eBI_OD.d.ts.map +1 -0
  43. package/dist/index.d.ts +2 -2
  44. package/dist/index.js +12 -9
  45. package/dist/index.js.map +1 -1
  46. package/dist/{inline-autocomplete-BAwBcZtD.js → inline-autocomplete-CbcdZpfC.js} +2 -1
  47. package/dist/inline-autocomplete-CbcdZpfC.js.map +1 -0
  48. package/dist/{login-DfJGuHUj.js → login-afiadwf0.js} +34 -18
  49. package/dist/login-afiadwf0.js.map +1 -0
  50. package/dist/mcp.d.ts +1 -1
  51. package/dist/{messages-CvHfYB7f.js → messages-Dy9t7DbQ.js} +97 -6
  52. package/dist/messages-Dy9t7DbQ.js.map +1 -0
  53. package/dist/output/stream-json.d.ts +1 -1
  54. package/dist/output/terminal.d.ts +1 -1
  55. package/dist/output-cap-DM1HrgIp.js +100 -0
  56. package/dist/output-cap-DM1HrgIp.js.map +1 -0
  57. package/dist/presets.d.ts +1 -1
  58. package/dist/presets.js +1 -1
  59. package/dist/providers/anthropic.d.ts +1 -1
  60. package/dist/providers/anthropic.js +1 -1
  61. package/dist/providers/arcee.d.ts +1 -1
  62. package/dist/providers/baseten.d.ts +1 -1
  63. package/dist/providers/cerebras.d.ts +1 -1
  64. package/dist/providers/local.d.ts +1 -1
  65. package/dist/providers/openai-compat.d.ts +1 -1
  66. package/dist/providers/openai.d.ts +1 -1
  67. package/dist/providers/openai.js +4 -3
  68. package/dist/providers/openai.js.map +1 -1
  69. package/dist/providers/openrouter.d.ts +1 -1
  70. package/dist/providers/xai.d.ts +1 -1
  71. package/dist/providers.d.ts +1 -1
  72. package/dist/providers.js +1 -1
  73. package/dist/restate.d.ts +1 -1
  74. package/dist/{run-summary-nD3IQ4ZQ.js → run-summary-BU_NgwUT.js} +17 -1
  75. package/dist/run-summary-BU_NgwUT.js.map +1 -0
  76. package/dist/session/sqlite.d.ts +1 -1
  77. package/dist/{session-jLsc_kMH.js → session-DkVnYGm9.js} +2 -2
  78. package/dist/{session-jLsc_kMH.js.map → session-DkVnYGm9.js.map} +1 -1
  79. package/dist/session.d.ts +1 -1
  80. package/dist/session.js +2 -2
  81. package/dist/skills.d.ts +1 -1
  82. package/dist/{tool-formatters-CZJYzP4x.d.ts → tool-formatters-DdchId_3.d.ts} +2 -2
  83. package/dist/{tool-formatters-CZJYzP4x.d.ts.map → tool-formatters-DdchId_3.d.ts.map} +1 -1
  84. package/dist/tools/fetch-url.d.ts +1 -1
  85. package/dist/tools/web-search.d.ts +1 -1
  86. package/dist/tools.d.ts +1 -1
  87. package/dist/tools.js +1 -1
  88. package/dist/{transcript-anchors-Ded7iJjG.js → transcript-anchors-DOuzgpJx.js} +88 -12
  89. package/dist/transcript-anchors-DOuzgpJx.js.map +1 -0
  90. package/dist/{transcript-anchors-CJ2dmpQk.d.ts → transcript-anchors-DvXLpbIc.d.ts} +2 -2
  91. package/dist/transcript-anchors-DvXLpbIc.d.ts.map +1 -0
  92. package/dist/tui.d.ts +44 -7
  93. package/dist/tui.d.ts.map +1 -1
  94. package/dist/tui.js +1387 -1106
  95. package/dist/tui.js.map +1 -1
  96. package/dist/{turn-operations-ClGa1vnb.js → turn-operations-4tnvKwfJ.js} +77 -13
  97. package/dist/turn-operations-4tnvKwfJ.js.map +1 -0
  98. package/dist/{turn-operations-DZYpXcU1.d.ts → turn-operations-HRTRJIcy.d.ts} +3 -3
  99. package/dist/{turn-operations-DZYpXcU1.d.ts.map → turn-operations-HRTRJIcy.d.ts.map} +1 -1
  100. package/dist/types-BIarq1qE.js.map +1 -1
  101. package/dist/types.d.ts +1 -1
  102. package/package.json +2 -1
  103. package/dist/agent-tLfGSSQw.js.map +0 -1
  104. package/dist/anthropic-uKvj1yle.js.map +0 -1
  105. package/dist/contexts-A4ep-iJs.js.map +0 -1
  106. package/dist/index-8zm_hwdY.d.ts.map +0 -1
  107. package/dist/inline-autocomplete-BAwBcZtD.js.map +0 -1
  108. package/dist/login-DfJGuHUj.js.map +0 -1
  109. package/dist/messages-CvHfYB7f.js.map +0 -1
  110. package/dist/output-cap-kBUHBlnq.js.map +0 -1
  111. package/dist/run-summary-nD3IQ4ZQ.js.map +0 -1
  112. package/dist/transcript-anchors-CJ2dmpQk.d.ts.map +0 -1
  113. package/dist/transcript-anchors-Ded7iJjG.js.map +0 -1
  114. package/dist/turn-operations-ClGa1vnb.js.map +0 -1
package/dist/tui.js CHANGED
@@ -1,25 +1,25 @@
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-DOuzgpJx.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-DIe8yzcb.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";
9
9
  import { i as sniffImageMediaType } from "./media-sniff-Bn76JxAu.js";
10
10
  import { d as McpOAuthProvider, i as nonOwningMcpView, n as connectMcpServers, p as hasAuthorizationHeader } from "./mcp-H6bqyM94.js";
11
11
  import { replaceDynamicSection } from "./prompt.js";
12
- import { a as getExtensionSessionState, c as setExtensionSessionState, i as demoteStaleRunningStatus, o as loadSession, t as createSession } from "./session-jLsc_kMH.js";
12
+ import { a as getExtensionSessionState, c as setExtensionSessionState, i as demoteStaleRunningStatus, o as loadSession, t as createSession } from "./session-DkVnYGm9.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-C4YPRDN4.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,26 +1295,20 @@ 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
- })
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
+ ] })
1018
1312
  ]
1019
1313
  });
1020
1314
  }
@@ -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
  }
@@ -1606,273 +1909,79 @@ function pickHelper() {
1606
1909
  * Returns `true` if the spawn looked plausibly successful, `false`
1607
1910
  * if we couldn't even attach to the child's stdin (binary missing,
1608
1911
  * 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.
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.
1780
1916
  */
1781
- function dispatchHintKey(renderer, spec) {
1782
- const parsed = parseHintKeySpec(spec);
1783
- if (!parsed) return false;
1784
- renderer.keyInput.processParsedKey(parsed);
1785
- return true;
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
+ }
1786
1933
  }
1787
1934
  /**
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.
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.
1798
1938
  */
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
- ] });
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
+ }
1828
1949
  }
1829
- /**
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.
1833
- */
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
- });
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,
@@ -2894,7 +3048,72 @@ function StatusSpinner({ color }) {
2894
3048
  children: useSpinnerFrame()
2895
3049
  });
2896
3050
  }
2897
- function Transcript({ events, settings, selectedTurnId = null, busy = false, searchQuery, searchPending = false, onLoadEarlier }) {
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(() => {
2922
- const direct = /* @__PURE__ */ new Set();
2923
- const callIds = /* @__PURE__ */ new Set();
2924
- const childIds = /* @__PURE__ */ new Set();
2925
- if (!searchQuery) return {
2926
- direct,
2927
- callIds,
2928
- childIds
2929
- };
3141
+ if (!searchQuery) return NO_SEARCH_MATCHES;
3142
+ const direct = /* @__PURE__ */ new Set();
3143
+ const callIds = /* @__PURE__ */ new Set();
3144
+ const childIds = /* @__PURE__ */ new Set();
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)]);
@@ -19795,6 +20039,14 @@ function AppShell() {
19795
20039
  setLastInputTokens(ctx.effectiveTokens);
19796
20040
  lastInputTokensRef.current = ctx.effectiveTokens;
19797
20041
  });
20042
+ agent.hooks.hook("history:repair", (ctx) => {
20043
+ if (sessionRef.current?.id !== session.id) return;
20044
+ const blocks = `${ctx.removedBlocks} stale reasoning block${ctx.removedBlocks === 1 ? "" : "s"}`;
20045
+ stream.appendImmediate({
20046
+ kind: "info",
20047
+ text: `🩹 Repaired history: removed ${blocks} the provider rejected${ctx.persisted ? "" : " (session not yet persisted)"}.`
20048
+ });
20049
+ });
19798
20050
  const spawnStartedAt = /* @__PURE__ */ new Map();
19799
20051
  agent.hooks.hook("spawn:before", ({ id, task, depth, model, resumesChildId }) => {
19800
20052
  const startedAt = Date.now();
@@ -19925,6 +20177,7 @@ function AppShell() {
19925
20177
  });
19926
20178
  agent.hooks.hook("agent:done", () => {
19927
20179
  pendingAnnotationsRef.current.clear();
20180
+ pruneTodosByRun(session);
19928
20181
  });
19929
20182
  subscribeMcpToolsCache(agent.hooks, { dataDir });
19930
20183
  return agent;
@@ -19991,7 +20244,9 @@ function AppShell() {
19991
20244
  const cachedLive = liveSessionSearchIndexRef.current;
19992
20245
  if (cachedLive?.sessionId === meta.id && cachedLive.updatedAt === meta.updatedAt && cachedLive.turnCount === live.turns.length) index = cachedLive.index;
19993
20246
  else {
20247
+ const staleLive = liveSessionSearchIndexRef.current;
19994
20248
  index = await (historySearchWorkerRef.current ??= new HistorySearchWorkerClient()).build(live.turns, abortController.signal);
20249
+ if (staleLive) historySearchWorkerRef.current?.release(staleLive.index);
19995
20250
  liveSessionSearchIndexRef.current = {
19996
20251
  sessionId: meta.id,
19997
20252
  updatedAt: meta.updatedAt,
@@ -20006,15 +20261,25 @@ function AppShell() {
20006
20261
  } else {
20007
20262
  const turns = await store.getTurns(meta.id);
20008
20263
  index = await (historySearchWorkerRef.current ??= new HistorySearchWorkerClient()).build(turns, abortController.signal);
20264
+ historySearchWorkerRef.current?.slim(index);
20009
20265
  const cache = sessionSearchCacheRef.current;
20266
+ const stale = cache.get(meta.id);
20267
+ if (stale) historySearchWorkerRef.current?.release(stale.index);
20010
20268
  cache.set(meta.id, {
20011
20269
  updatedAt: meta.updatedAt,
20012
20270
  index
20013
20271
  });
20014
- while (cache.size > SESSION_SEARCH_INDEX_CACHE_MAX) {
20272
+ let totalBytes = 0;
20273
+ for (const entry of cache.values()) totalBytes += entry.index.bytes;
20274
+ while (cache.size > 1 && (cache.size > SESSION_SEARCH_INDEX_CACHE_MAX || totalBytes > SESSION_SEARCH_INDEX_CACHE_MAX_BYTES)) {
20015
20275
  const oldest = cache.keys().next().value;
20016
20276
  if (oldest === void 0) break;
20277
+ const evicted = cache.get(oldest);
20017
20278
  cache.delete(oldest);
20279
+ if (evicted) {
20280
+ totalBytes -= evicted.index.bytes;
20281
+ historySearchWorkerRef.current?.release(evicted.index);
20282
+ }
20018
20283
  }
20019
20284
  }
20020
20285
  if (generation !== sessionSearchGenerationRef.current) return;
@@ -20022,7 +20287,24 @@ function AppShell() {
20022
20287
  if (match) matches.set(meta.id, match);
20023
20288
  } catch (error) {
20024
20289
  if (abortController.signal.aborted) throw abortController.signal.reason instanceof DOMException ? abortController.signal.reason : error;
20025
- debugLog("session content search skipped a session", error);
20290
+ const broken = sessionSearchCacheRef.current.get(meta.id);
20291
+ if (broken) {
20292
+ sessionSearchCacheRef.current.delete(meta.id);
20293
+ historySearchWorkerRef.current?.release(broken.index);
20294
+ try {
20295
+ const turns = await store.getTurns(meta.id);
20296
+ const retry = await (historySearchWorkerRef.current ??= new HistorySearchWorkerClient()).build(turns, abortController.signal);
20297
+ try {
20298
+ const match = await historySearchWorkerRef.current.history(retry, query, 3, abortController.signal);
20299
+ if (match) matches.set(meta.id, match);
20300
+ } finally {
20301
+ historySearchWorkerRef.current?.release(retry);
20302
+ }
20303
+ } catch (retryError) {
20304
+ if (abortController.signal.aborted) throw abortController.signal.reason instanceof DOMException ? abortController.signal.reason : retryError;
20305
+ debugLog("session content search retry failed", retryError);
20306
+ }
20307
+ } else debugLog("session content search skipped a session", error);
20026
20308
  }
20027
20309
  done++;
20028
20310
  if (done === sessions.length || done % 4 === 0) setSessionContentSearchProgress({
@@ -21172,6 +21454,7 @@ function AppShell() {
21172
21454
  const ctx = {
21173
21455
  args: invocation.args,
21174
21456
  argv: invocation.argv,
21457
+ invokedAs: invocation.invokedAs,
21175
21458
  ...attachments.length > 0 ? { attachments: attachments.map((a) => ({
21176
21459
  name: a.name,
21177
21460
  mediaType: a.mediaType,
@@ -21921,6 +22204,10 @@ function AppShell() {
21921
22204
  const onLoginMcp = useCallback(async (name) => {
21922
22205
  const entry = mcpsCatalogRef.current.find((d) => d.config.name === name);
21923
22206
  if (!entry) return;
22207
+ for (const [other, ctrl] of mcpLoginAbortsRef.current) if (other !== name && !ctrl.signal.aborted) {
22208
+ debugLog(`mcp-login: refusing concurrent login for "${name}"`, `"${other}" is still authorizing`);
22209
+ return;
22210
+ }
21924
22211
  mcpLoginAbortsRef.current.get(name)?.abort();
21925
22212
  const ac = new AbortController();
21926
22213
  mcpLoginAbortsRef.current.set(name, ac);
@@ -21932,6 +22219,7 @@ function AppShell() {
21932
22219
  await loginMcpServer(entry.config, {
21933
22220
  store: mcpCredentialStore,
21934
22221
  signal: ac.signal,
22222
+ renderCallbackPage: renderDefaultCallbackPage,
21935
22223
  hooks: agentRef.current?.hooks,
21936
22224
  onAuthorizationUrl: (url) => tryOpenBrowser(url.toString())
21937
22225
  });
@@ -22024,6 +22312,7 @@ function AppShell() {
22024
22312
  (async () => {
22025
22313
  const cached = transcriptSearchIndexRef.current;
22026
22314
  const index = cached?.sessionId === session.id && cached.events === events ? cached.index : await (historySearchWorkerRef.current ??= new HistorySearchWorkerClient()).build(turns, abortController.signal);
22315
+ if (cached && cached.index !== index) historySearchWorkerRef.current?.release(cached.index);
22027
22316
  transcriptSearchIndexRef.current = {
22028
22317
  sessionId: session.id,
22029
22318
  events,
@@ -22151,7 +22440,14 @@ function AppShell() {
22151
22440
  store,
22152
22441
  ...source.projectRoot ? { projectRoot: source.projectRoot } : {}
22153
22442
  });
22154
- fork.setTurns(slice);
22443
+ const rehomed = await rehomePersistedStubs(slice, resolvePersistDir({
22444
+ userDir: cacheDir,
22445
+ sessionId: source.id
22446
+ }), resolvePersistDir({
22447
+ userDir: cacheDir,
22448
+ sessionId: fork.id
22449
+ }));
22450
+ fork.setTurns(rehomed);
22155
22451
  fork.setRuns(inheritedRuns);
22156
22452
  try {
22157
22453
  await fork.save();
@@ -22164,7 +22460,8 @@ function AppShell() {
22164
22460
  }, [
22165
22461
  picked,
22166
22462
  store,
22167
- activateSession
22463
+ activateSession,
22464
+ cacheDir
22168
22465
  ]);
22169
22466
  const onDeleteTurn = useCallback(async (turnId) => {
22170
22467
  const session = sessionRef.current;
@@ -22508,9 +22805,7 @@ function AppShell() {
22508
22805
  onExportSession,
22509
22806
  keybindings
22510
22807
  ]);
22511
- const openSelectedTurn = useCallback(() => {
22512
- const id = selectedTurnId;
22513
- if (!id) return;
22808
+ const openTurnDetails = useCallback((id) => {
22514
22809
  const session = sessionRef.current;
22515
22810
  if (!session) return;
22516
22811
  const turn = session.turns.find((t) => t.id === id);
@@ -22541,7 +22836,6 @@ function AppShell() {
22541
22836
  }));
22542
22837
  }, [
22543
22838
  modal,
22544
- selectedTurnId,
22545
22839
  turnIds,
22546
22840
  transcriptSearchMode,
22547
22841
  transcriptSearchQuery,
@@ -22551,7 +22845,54 @@ function AppShell() {
22551
22845
  onEditTurn,
22552
22846
  keybindings
22553
22847
  ]);
22848
+ const openSelectedTurn = useCallback(() => {
22849
+ if (selectedTurnId) openTurnDetails(selectedTurnId);
22850
+ }, [selectedTurnId, openTurnDetails]);
22851
+ const onTranscriptSelectTurn = useCallback((id) => {
22852
+ if (!(transcriptSearchMode ? transcriptSearchTurnIds : turnIds).includes(id)) return false;
22853
+ setSelectedTurnId(id);
22854
+ return true;
22855
+ }, [
22856
+ transcriptSearchMode,
22857
+ transcriptSearchTurnIds,
22858
+ turnIds
22859
+ ]);
22860
+ const onTranscriptOpenTurn = useCallback((id) => {
22861
+ if (!(transcriptSearchMode ? transcriptSearchTurnIds : turnIds).includes(id)) return false;
22862
+ setSelectedTurnId(id);
22863
+ openTurnDetails(id);
22864
+ return true;
22865
+ }, [
22866
+ transcriptSearchMode,
22867
+ transcriptSearchTurnIds,
22868
+ turnIds,
22869
+ openTurnDetails
22870
+ ]);
22554
22871
  useKeyboard((key) => {
22872
+ if (showQuitConfirmRef.current) {
22873
+ if (key.name === "escape") {
22874
+ setShowQuitConfirm(false);
22875
+ setQuitConfirmSelected("cancel");
22876
+ return;
22877
+ }
22878
+ if (key.name === "c" && key.ctrl && !key.shift) {
22879
+ renderer.destroy();
22880
+ return;
22881
+ }
22882
+ if (key.name === "return") {
22883
+ if (quitConfirmSelectedRef.current === "quit") renderer.destroy();
22884
+ else {
22885
+ setShowQuitConfirm(false);
22886
+ setQuitConfirmSelected("cancel");
22887
+ }
22888
+ return;
22889
+ }
22890
+ if (key.name === "tab" || key.name === "left" || key.name === "right") {
22891
+ setQuitConfirmSelected((prev) => prev === "cancel" ? "quit" : "cancel");
22892
+ return;
22893
+ }
22894
+ return;
22895
+ }
22555
22896
  if (modal.isOpen) return;
22556
22897
  if (transcriptSearchMode && screen === "chat") {
22557
22898
  if (key.name === "escape") onTranscriptSearchModeChange(false);
@@ -22763,7 +23104,7 @@ function AppShell() {
22763
23104
  }));
22764
23105
  return;
22765
23106
  }
22766
- if (key.ctrl && !key.shift && !key.meta && (key.name === "c" || key.name === "x") && !showQuitConfirmRef.current) {
23107
+ if (key.ctrl && !key.shift && !key.meta && (key.name === "c" || key.name === "x")) {
22767
23108
  const editor = renderer.currentFocusedEditor;
22768
23109
  if (editor?.hasSelection()) {
22769
23110
  const text = editor.getSelectedText();
@@ -22772,30 +23113,6 @@ function AppShell() {
22772
23113
  return;
22773
23114
  }
22774
23115
  }
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
23116
  if (key.name === "c" && key.ctrl && !key.shift) {
22800
23117
  const mode = settingsRef.current.ctrlCBehavior;
22801
23118
  if (mode === "quit") {
@@ -23049,6 +23366,8 @@ function AppShell() {
23049
23366
  enterSelectsCompletion: (providerId) => providerId === "chats",
23050
23367
  onPopupOpenChange,
23051
23368
  selectedTurnId,
23369
+ onSelectTurn: onTranscriptSelectTurn,
23370
+ onOpenTurn: onTranscriptOpenTurn,
23052
23371
  transcriptSearchMode,
23053
23372
  transcriptSearchQuery,
23054
23373
  transcriptSearchEvents,
@@ -23072,15 +23391,16 @@ function AppShell() {
23072
23391
  status: transcriptSearchMode && transcriptSearchSearching ? "searching" : pendingInteraction?.kind === "question" ? "asking" : compacting ? "compacting" : retryInfo && busy ? "retrying" : busy ? "busy" : null,
23073
23392
  compactionTokens,
23074
23393
  compactionStartedAt,
23075
- retry: retryInfo
23394
+ retry: retryInfo,
23395
+ contextKey: keybindings.openContextPanel
23076
23396
  }),
23077
23397
  showQuitConfirm && /* @__PURE__ */ jsxs("box", {
23078
23398
  position: "absolute",
23079
23399
  top: "50%",
23080
23400
  left: "50%",
23081
23401
  marginTop: -3,
23082
- marginLeft: -18,
23083
- width: 36,
23402
+ marginLeft: -21,
23403
+ width: 42,
23084
23404
  height: 7,
23085
23405
  borderStyle: "rounded",
23086
23406
  borderColor: COLOR.warn,
@@ -23089,35 +23409,52 @@ function AppShell() {
23089
23409
  alignItems: "center",
23090
23410
  justifyContent: "center",
23091
23411
  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");
23412
+ children: [
23413
+ /* @__PURE__ */ jsx$1("text", { children: "Quit zidane?" }),
23414
+ /* @__PURE__ */ jsxs("box", {
23415
+ flexDirection: "row",
23416
+ gap: 2,
23417
+ children: [/* @__PURE__ */ jsx$1("box", {
23418
+ backgroundColor: quitConfirmSelected === "cancel" ? COLOR.accent : void 0,
23419
+ onMouseDown: (event) => {
23420
+ event.stopPropagation();
23421
+ event.preventDefault();
23422
+ if (quitConfirmSelectedRef.current === "cancel") setShowQuitConfirm(false);
23423
+ else setQuitConfirmSelected("cancel");
23424
+ },
23425
+ children: /* @__PURE__ */ jsx$1("text", {
23426
+ fg: quitConfirmSelected === "cancel" ? SURFACE.modal : COLOR.dim,
23427
+ children: " Cancel "
23428
+ })
23429
+ }), /* @__PURE__ */ jsx$1("box", {
23430
+ backgroundColor: quitConfirmSelected === "quit" ? COLOR.warn : void 0,
23431
+ onMouseDown: (event) => {
23432
+ event.stopPropagation();
23433
+ event.preventDefault();
23434
+ if (quitConfirmSelectedRef.current === "quit") renderer.destroy();
23435
+ else setQuitConfirmSelected("quit");
23436
+ },
23437
+ children: /* @__PURE__ */ jsx$1("text", {
23438
+ fg: quitConfirmSelected === "quit" ? SURFACE.modal : COLOR.dim,
23439
+ children: " Quit "
23440
+ })
23441
+ })]
23442
+ }),
23443
+ /* @__PURE__ */ jsx$1(ModalHints, { hints: [
23444
+ {
23445
+ key: "tab",
23446
+ label: "switch"
23102
23447
  },
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");
23448
+ {
23449
+ key: "↵",
23450
+ label: "confirm"
23114
23451
  },
23115
- children: /* @__PURE__ */ jsx$1("text", {
23116
- fg: quitConfirmSelected === "quit" ? SURFACE.modal : COLOR.dim,
23117
- children: " Quit "
23118
- })
23119
- })]
23120
- })]
23452
+ {
23453
+ key: "esc",
23454
+ label: "cancel"
23455
+ }
23456
+ ] })
23457
+ ]
23121
23458
  })
23122
23459
  ]
23123
23460
  });
@@ -23534,30 +23871,27 @@ function EffortPickerModal({ current, supportsAdaptive, onPick }) {
23534
23871
  highlightBg: SURFACE.selection
23535
23872
  }, level.id))
23536
23873
  }),
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
- })
23874
+ /* @__PURE__ */ jsx$1(ModalHints, { hints: [
23875
+ {
23876
+ key: "↑↓",
23877
+ label: "navigate",
23878
+ labelColor: COLOR.dim
23879
+ },
23880
+ {
23881
+ key: "↵",
23882
+ label: "select",
23883
+ labelColor: COLOR.dim
23884
+ },
23885
+ {
23886
+ key: "esc",
23887
+ label: "close",
23888
+ labelColor: COLOR.dim
23889
+ },
23890
+ {
23891
+ key: "",
23892
+ label: `${filtered.length} / ${levels.length} level${levels.length === 1 ? "" : "s"}`
23893
+ }
23894
+ ] })
23561
23895
  ]
23562
23896
  });
23563
23897
  }
@@ -23648,20 +23982,13 @@ function ToggleListModal({ catalog, keyOf, settingKey, gcKeys, title, renderDeta
23648
23982
  children: [
23649
23983
  preamble,
23650
23984
  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
- })
23985
+ /* @__PURE__ */ jsx$1(ModalHints, { hints: [onRefresh && {
23986
+ key: "ctrl+r",
23987
+ label: "refresh"
23988
+ }, {
23989
+ key: "esc",
23990
+ label: "close"
23991
+ }] })
23665
23992
  ]
23666
23993
  });
23667
23994
  return /* @__PURE__ */ jsxs(Modal, {
@@ -23697,30 +24024,24 @@ function ToggleListModal({ catalog, keyOf, settingKey, gcKeys, title, renderDeta
23697
24024
  }, name);
23698
24025
  })
23699
24026
  }),
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
- })
24027
+ /* @__PURE__ */ jsx$1(ModalHints, { hints: [
24028
+ {
24029
+ key: "↑↓",
24030
+ label: "navigate"
24031
+ },
24032
+ {
24033
+ key: "↵",
24034
+ label: "toggle"
24035
+ },
24036
+ onRefresh && {
24037
+ key: "ctrl+r",
24038
+ label: "refresh"
24039
+ },
24040
+ {
24041
+ key: "esc",
24042
+ label: "close"
24043
+ }
24044
+ ] })
23724
24045
  ]
23725
24046
  });
23726
24047
  }
@@ -24030,20 +24351,13 @@ function McpsSettingsModal({ catalog, errors, toolsByServer, onLogin, onLogout,
24030
24351
  children: " { \"mcpServers\": { ... } } "
24031
24352
  }), "wrapper from Claude Code / Cursor also works."]
24032
24353
  }),
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
- })
24354
+ /* @__PURE__ */ jsx$1(ModalHints, { hints: [onRefresh && {
24355
+ key: "r",
24356
+ label: "refresh"
24357
+ }, {
24358
+ key: "esc",
24359
+ label: "close"
24360
+ }] })
24047
24361
  ]
24048
24362
  });
24049
24363
  const groups = [];
@@ -24098,8 +24412,7 @@ function McpsSettingsModal({ catalog, errors, toolsByServer, onLogin, onLogout,
24098
24412
  focusedEntry,
24099
24413
  focusedStatus,
24100
24414
  expanded,
24101
- showRefresh: !!onRefresh,
24102
- COLOR
24415
+ showRefresh: !!onRefresh
24103
24416
  })
24104
24417
  ]
24105
24418
  });
@@ -24330,7 +24643,7 @@ function renderDetailPanel(entry, status, COLOR) {
24330
24643
  });
24331
24644
  return null;
24332
24645
  }
24333
- function renderActionHints({ focusedRow, focusedEntry, focusedStatus, expanded, showRefresh, COLOR }) {
24646
+ function renderActionHints({ focusedRow, focusedEntry, focusedStatus, expanded, showRefresh }) {
24334
24647
  const effectiveStatus = focusedStatus ?? { kind: "idle" };
24335
24648
  const onServerRow = focusedRow?.kind === "server";
24336
24649
  const canL = onServerRow && focusedEntry ? canLogin(focusedEntry, effectiveStatus) : false;
@@ -24338,77 +24651,44 @@ function renderActionHints({ focusedRow, focusedEntry, focusedStatus, expanded,
24338
24651
  const canCancel = effectiveStatus.kind === "authorizing";
24339
24652
  const canExpand = onServerRow && focusedEntry ? !expanded.has(focusedEntry.config.name) : false;
24340
24653
  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
- });
24654
+ return /* @__PURE__ */ jsx$1(ModalHints, { hints: [
24655
+ {
24656
+ key: "↑↓",
24657
+ label: "navigate"
24658
+ },
24659
+ {
24660
+ key: "↵",
24661
+ label: "toggle"
24662
+ },
24663
+ canExpand && {
24664
+ key: "→",
24665
+ label: "expand"
24666
+ },
24667
+ canCollapse && {
24668
+ key: "←",
24669
+ label: "collapse"
24670
+ },
24671
+ canL && {
24672
+ key: "l",
24673
+ label: "login"
24674
+ },
24675
+ canO && {
24676
+ key: "o",
24677
+ label: "logout"
24678
+ },
24679
+ showRefresh && {
24680
+ key: "r",
24681
+ label: "refresh"
24682
+ },
24683
+ canCancel && {
24684
+ key: "esc",
24685
+ label: "cancel"
24686
+ },
24687
+ !canCancel && {
24688
+ key: "esc",
24689
+ label: "close"
24690
+ }
24691
+ ] });
24412
24692
  }
24413
24693
  function renderErrors(errors, home, warnColor) {
24414
24694
  if (!errors || errors.length === 0) return null;
@@ -24633,6 +24913,7 @@ async function runTui(options = {}) {
24633
24913
  debounceDelay: 0,
24634
24914
  targetFps: bootFps,
24635
24915
  maxFps: bootFps,
24916
+ useMouse: config.initialSettings.mouseInteractions ?? DEFAULT_SETTINGS.mouseInteractions,
24636
24917
  gatherStats
24637
24918
  });
24638
24919
  bootTick("runTui:after-createCliRenderer");