zidane 6.5.4 → 6.5.6

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 (129) hide show
  1. package/dist/{acp-CIpD8UIw.js → acp-Dm95NwNM.js} +6 -6
  2. package/dist/{acp-CIpD8UIw.js.map → acp-Dm95NwNM.js.map} +1 -1
  3. package/dist/acp-cli.js +6 -6
  4. package/dist/acp.d.ts +1 -1
  5. package/dist/acp.js +1 -1
  6. package/dist/{agent-8kfGIDb_.js → agent-CcTWL-Z6.js} +231 -167
  7. package/dist/agent-CcTWL-Z6.js.map +1 -0
  8. package/dist/agent.d.ts +1 -1
  9. package/dist/agent.js +1 -1
  10. package/dist/{anthropic-qaw8XAc9.js → anthropic-DLc_Kw-1.js} +420 -25
  11. package/dist/anthropic-DLc_Kw-1.js.map +1 -0
  12. package/dist/{auth-BLy7X1SY.js → auth-BAclQnl3.js} +127 -35
  13. package/dist/{auth-BLy7X1SY.js.map → auth-BAclQnl3.js.map} +1 -1
  14. package/dist/chat/pure.d.ts +3 -3
  15. package/dist/chat/pure.js +2 -2
  16. package/dist/chat.d.ts +115 -9
  17. package/dist/chat.d.ts.map +1 -1
  18. package/dist/chat.js +8 -8
  19. package/dist/{context-segments-1mcqSCM3.js → context-segments-CJCtJ9sU.js} +16 -3
  20. package/dist/{context-segments-1mcqSCM3.js.map → context-segments-CJCtJ9sU.js.map} +1 -1
  21. package/dist/{context-transport-Bco9Wo0b.js → context-transport-D8yeGdpd.js} +2 -2
  22. package/dist/{context-transport-Bco9Wo0b.js.map → context-transport-D8yeGdpd.js.map} +1 -1
  23. package/dist/contexts/daytona.d.ts +1 -1
  24. package/dist/contexts/daytona.js +1 -1
  25. package/dist/contexts/e2b.d.ts +1 -1
  26. package/dist/{errors-BwBxu3zD.js → errors-DKiMrOKq.js} +61 -2
  27. package/dist/{errors-BwBxu3zD.js.map → errors-DKiMrOKq.js.map} +1 -1
  28. package/dist/errors.js +2 -2
  29. package/dist/eval.d.ts +1 -1
  30. package/dist/eval.js +1 -1
  31. package/dist/extensions.d.ts +1 -1
  32. package/dist/extensions.js +1 -1
  33. package/dist/{fetch-url-B7aSg6Bp.js → fetch-url-ma-5_p0p.js} +2 -2
  34. package/dist/{fetch-url-B7aSg6Bp.js.map → fetch-url-ma-5_p0p.js.map} +1 -1
  35. package/dist/headless.d.ts +1 -1
  36. package/dist/headless.js +3 -3
  37. package/dist/{index-BSNr0OQn.d.ts → index-ZWibV230.d.ts} +100 -28
  38. package/dist/index-ZWibV230.d.ts.map +1 -0
  39. package/dist/index.d.ts +1 -1
  40. package/dist/index.js +13 -13
  41. package/dist/{login-D3CKhgbI.js → login-CB23LyI1.js} +3 -3
  42. package/dist/{login-D3CKhgbI.js.map → login-CB23LyI1.js.map} +1 -1
  43. package/dist/{mcp-D9fqOQo5.js → mcp-CVtSQTWg.js} +3 -3
  44. package/dist/{mcp-D9fqOQo5.js.map → mcp-CVtSQTWg.js.map} +1 -1
  45. package/dist/mcp.d.ts +1 -1
  46. package/dist/mcp.js +1 -1
  47. package/dist/{messages-C_H9soi5.js → messages-CZMUuKzo.js} +2 -2
  48. package/dist/{messages-C_H9soi5.js.map → messages-CZMUuKzo.js.map} +1 -1
  49. package/dist/{oauth-ZebH9NHb.js → oauth-C4GvY_p1.js} +27 -8
  50. package/dist/oauth-C4GvY_p1.js.map +1 -0
  51. package/dist/{openai-compat-CnHeo4Lz.js → openai-compat-BoTi-9fd.js} +9 -4
  52. package/dist/openai-compat-BoTi-9fd.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/{plugins-2PMTVcp-.js → plugins-onDvTHEm.js} +2 -2
  56. package/dist/{plugins-2PMTVcp-.js.map → plugins-onDvTHEm.js.map} +1 -1
  57. package/dist/plugins.d.ts +1 -1
  58. package/dist/plugins.js +1 -1
  59. package/dist/presets.d.ts +1 -1
  60. package/dist/presets.js +1 -1
  61. package/dist/providers/anthropic.d.ts +1 -1
  62. package/dist/providers/anthropic.js +1 -1
  63. package/dist/providers/arcee.d.ts +1 -1
  64. package/dist/providers/arcee.js +1 -1
  65. package/dist/providers/baseten.d.ts +1 -1
  66. package/dist/providers/baseten.js +1 -1
  67. package/dist/providers/cerebras.d.ts +1 -1
  68. package/dist/providers/cerebras.js +1 -1
  69. package/dist/providers/local.d.ts +1 -1
  70. package/dist/providers/local.js +1 -1
  71. package/dist/providers/openai-compat.d.ts +1 -1
  72. package/dist/providers/openai-compat.js +1 -1
  73. package/dist/providers/openai.d.ts +1 -1
  74. package/dist/providers/openai.js +4 -4
  75. package/dist/providers/openrouter.d.ts +1 -1
  76. package/dist/providers/openrouter.js +1 -1
  77. package/dist/providers/xai.d.ts +1 -1
  78. package/dist/providers/xai.js +2 -2
  79. package/dist/{providers-Bxs-XHir.js → providers-CmhdtD7w.js} +3 -3
  80. package/dist/{providers-Bxs-XHir.js.map → providers-CmhdtD7w.js.map} +1 -1
  81. package/dist/providers.d.ts +1 -1
  82. package/dist/providers.js +4 -4
  83. package/dist/restate.d.ts +1 -1
  84. package/dist/restate.js +2 -2
  85. package/dist/session/sqlite.d.ts +1 -1
  86. package/dist/session/sqlite.js +1 -1
  87. package/dist/{session-VpLrNaFQ.js → session-CyRLSsVa.js} +2 -2
  88. package/dist/{session-VpLrNaFQ.js.map → session-CyRLSsVa.js.map} +1 -1
  89. package/dist/session.d.ts +1 -1
  90. package/dist/session.js +2 -2
  91. package/dist/{skills-Cp9vnBwd.js → skills-BMGH_KK1.js} +2 -2
  92. package/dist/{skills-Cp9vnBwd.js.map → skills-BMGH_KK1.js.map} +1 -1
  93. package/dist/skills.d.ts +1 -1
  94. package/dist/skills.js +1 -1
  95. package/dist/{timeout-BDetbuN6.js → timeout-CjOG2mea.js} +29 -2
  96. package/dist/timeout-CjOG2mea.js.map +1 -0
  97. package/dist/timeout-DEaqslLO.d.ts.map +1 -1
  98. package/dist/{tool-formatters-BfKqWm5N.d.ts → tool-formatters-D3wSS2zz.d.ts} +2 -2
  99. package/dist/{tool-formatters-BfKqWm5N.d.ts.map → tool-formatters-D3wSS2zz.d.ts.map} +1 -1
  100. package/dist/tools/fetch-url.d.ts +1 -1
  101. package/dist/tools/fetch-url.js +1 -1
  102. package/dist/tools/web-search.d.ts +1 -1
  103. package/dist/tools/web-search.js +2 -2
  104. package/dist/tools.d.ts +1 -1
  105. package/dist/tools.js +1 -1
  106. package/dist/{transcript-anchors-Bv8HQ3S2.js → transcript-anchors-D-Yuimif.js} +177 -63
  107. package/dist/transcript-anchors-D-Yuimif.js.map +1 -0
  108. package/dist/{transcript-anchors-CNqDbrc9.d.ts → transcript-anchors-llwMrLRG.d.ts} +2 -2
  109. package/dist/{transcript-anchors-CNqDbrc9.d.ts.map → transcript-anchors-llwMrLRG.d.ts.map} +1 -1
  110. package/dist/tui.d.ts +24 -29
  111. package/dist/tui.d.ts.map +1 -1
  112. package/dist/tui.js +964 -244
  113. package/dist/tui.js.map +1 -1
  114. package/dist/{turn-operations-iGu5J08j.js → turn-operations-7_Ff7INF.js} +2 -2
  115. package/dist/{turn-operations-iGu5J08j.js.map → turn-operations-7_Ff7INF.js.map} +1 -1
  116. package/dist/{turn-operations-CnIpcenJ.d.ts → turn-operations-C_XvfBU3.d.ts} +3 -3
  117. package/dist/{turn-operations-CnIpcenJ.d.ts.map → turn-operations-C_XvfBU3.d.ts.map} +1 -1
  118. package/dist/types.d.ts +1 -1
  119. package/dist/types.js +1 -1
  120. package/docs/CHAT.md +1 -1
  121. package/docs/TUI.md +4 -4
  122. package/package.json +2 -1
  123. package/dist/agent-8kfGIDb_.js.map +0 -1
  124. package/dist/anthropic-qaw8XAc9.js.map +0 -1
  125. package/dist/index-BSNr0OQn.d.ts.map +0 -1
  126. package/dist/oauth-ZebH9NHb.js.map +0 -1
  127. package/dist/openai-compat-CnHeo4Lz.js.map +0 -1
  128. package/dist/timeout-BDetbuN6.js.map +0 -1
  129. package/dist/transcript-anchors-Bv8HQ3S2.js.map +0 -1
package/dist/tui.js CHANGED
@@ -1,29 +1,29 @@
1
- import { C as estimateRebilledCostUsd } from "./openai-compat-CnHeo4Lz.js";
2
- import { t as buildContextSegments } from "./context-segments-1mcqSCM3.js";
3
- import { $ as installExtensionFromSpec, $t as groupBindings, A as selectFilesFromSession, C as resolveLegacySessionDir, Cn as resolveExtensionConfigValue, D as resolveTasksDir, Dn as OUTPUT_RESERVE_TOKENS, E as resolveSessionDir, Et as isSpawnTool, G as summaryToTurn, It as looksBinary, M as compactConversation, O as buildPostCompactAttachments, On as effectiveContextWindow, Pt as tailTruncate, Qt as formatBindingForDisplay, Rt as createModelSearchTool, S as resolveLegacyPersistDir, T as resolvePersistDir, X as createExtensionPromptBridge, Xt as KEYBINDING_KEY_COL_WIDTH, Z as ExtensionInstallError, Zt as ensureKeybindingsFile, at as createExtensionHud, bt as basic_default, cn as registerExtensionHostModules, dt as filterExtensionRegistry, en as keybindingsPath, f as applyCompactSummaryCutoff, fn as inlineLoadedExtension, ft as filterRegistryPlugins, gn as createDebugLog, hn as scanExtensionDirectories, ht as createExtensionHostRuntimeBridge, it as updateInstalledExtension, lt as composeExtensionPreset, mn as parseExtensionScanPathEnv, mt as partitionExtensionCatalog, on as resolveExtensionKeybindings, ot as discoverAndBuildExtensionRegistry, rt as removeInstalledExtension, t as createAgent, tn as matchesBinding, ut as disposeExtensionRegistry, v as cleanupPersistedSession, w as resolveMcpWarningsDir, wn as sanitizeExtensionConfigActionPatch, x as rehomePersistedStubs, yn as coerceExtensionConfigInput } from "./agent-8kfGIDb_.js";
1
+ import { C as estimateRebilledCostUsd } from "./openai-compat-BoTi-9fd.js";
2
+ import { t as buildContextSegments } from "./context-segments-CJCtJ9sU.js";
3
+ import { $ as installExtensionFromSpec, $t as groupBindings, A as selectFilesFromSession, An as OUTPUT_RESERVE_TOKENS, C as resolveLegacySessionDir, D as resolveTasksDir, Dn as sanitizeExtensionConfigActionPatch, E as resolveSessionDir, En as resolveExtensionConfigValue, Et as isSpawnTool, G as summaryToTurn, It as looksBinary, M as compactConversation, O as buildPostCompactAttachments, Pt as tailTruncate, Qt as formatBindingForDisplay, Rt as createModelSearchTool, S as resolveLegacyPersistDir, Sn as coerceExtensionConfigInput, T as resolvePersistDir, X as createExtensionPromptBridge, Xt as KEYBINDING_KEY_COL_WIDTH, Z as ExtensionInstallError, Zt as ensureKeybindingsFile, _n as parseExtensionScanPathEnv, at as createExtensionHud, bt as basic_default, dn as registerExtensionHostModules, dt as filterExtensionRegistry, en as keybindingsPath, f as applyCompactSummaryCutoff, ft as filterRegistryPlugins, hn as inlineLoadedExtension, ht as createExtensionHostRuntimeBridge, it as updateInstalledExtension, jn as effectiveContextWindow, lt as composeExtensionPreset, mt as partitionExtensionCatalog, on as resolveExtensionKeybindings, ot as discoverAndBuildExtensionRegistry, rt as removeInstalledExtension, t as createAgent, tn as matchesBinding, ut as disposeExtensionRegistry, v as cleanupPersistedSession, vn as scanExtensionDirectories, w as resolveMcpWarningsDir, x as rehomePersistedStubs, yn as createDebugLog } from "./agent-CcTWL-Z6.js";
4
4
  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";
5
5
  import { n as appendCapped } from "./output-cap-C9zukcCj.js";
6
6
  import { i as killProcessGroup, n as SUPPORTS_PROCESS_GROUPS, r as createProcessContext } from "./contexts-DWVt7MvF.js";
7
- import { u as errorMessage } from "./errors-BwBxu3zD.js";
8
- import { E as buildCatalog, R as projectUserPaths, c as SkillInstallError, l as installSkillFromSpec, z as findGitRoot } from "./skills-Cp9vnBwd.js";
9
- import { n as defaultPluginDataRoot, r as loadAgentPlugin } from "./plugins-2PMTVcp-.js";
7
+ import { u as errorMessage } from "./errors-DKiMrOKq.js";
8
+ import { E as buildCatalog, R as projectUserPaths, c as SkillInstallError, l as installSkillFromSpec, z as findGitRoot } from "./skills-BMGH_KK1.js";
9
+ import { n as defaultPluginDataRoot, r as loadAgentPlugin } from "./plugins-onDvTHEm.js";
10
10
  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";
11
11
  import { i as sniffImageMediaType } from "./media-sniff-UKijEaiQ.js";
12
- import { _ as hasAuthorizationHeader, h as McpOAuthProvider, n as connectMcpServers, s as nonOwningMcpView } from "./mcp-D9fqOQo5.js";
12
+ import { _ as hasAuthorizationHeader, h as McpOAuthProvider, n as connectMcpServers, s as nonOwningMcpView } from "./mcp-CVtSQTWg.js";
13
13
  import { replaceDynamicSection, splitSystemPrompt } from "./prompt.js";
14
- import { a as getExtensionSessionState, c as setExtensionSessionState, i as demoteStaleRunningStatus, o as loadSession, t as createSession } from "./session-VpLrNaFQ.js";
14
+ import { a as getExtensionSessionState, c as setExtensionSessionState, i as demoteStaleRunningStatus, o as loadSession, t as createSession } from "./session-CyRLSsVa.js";
15
15
  import { n as flattenTurns, r as formatTokenUsage, t as effectiveInputFromTurn } from "./stats-DAKBEKjc.js";
16
- import { o as withTimeout } from "./timeout-BDetbuN6.js";
16
+ import { s as withTimeout } from "./timeout-CjOG2mea.js";
17
17
  import { i as installCacheBreakLogger } from "./cache-telemetry-ehVdWsg2.js";
18
18
  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-D0MPvBaS.js";
19
19
  import { s as resolveCompletionSubmit } from "./completion-core-BRo86R_L.js";
20
- import { a as ZIDANE_EXTENSION_SKILL_NAME, d as extensionScaffold, f as readSkillEditMarker, h as withZidaneSkillEditSkill, i as SKILL_EDIT_METADATA_KEY, l as editSessionMetadata, m as withZidaneExtensionSkill, o as ZIDANE_SKILL_EDIT_SKILL_NAME, t as loginMcpServer, u as extensionDirForCwd } from "./login-D3CKhgbI.js";
21
- 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-BLy7X1SY.js";
20
+ import { a as ZIDANE_EXTENSION_SKILL_NAME, d as extensionScaffold, f as readSkillEditMarker, h as withZidaneSkillEditSkill, i as SKILL_EDIT_METADATA_KEY, l as editSessionMetadata, m as withZidaneExtensionSkill, o as ZIDANE_SKILL_EDIT_SKILL_NAME, t as loginMcpServer, u as extensionDirForCwd } from "./login-CB23LyI1.js";
21
+ import { E as renderDefaultCallbackPage, T as restoreModelOptions, b as modelSupportsReasoning, c as setPreferredCredential, g as getContextWindow, h as enabledModelOptions, l as setProviderCredential, n as applyApiKeyEnv, t as detectAuth, w as piIdOf, y as modelOptionsFor } from "./auth-BAclQnl3.js";
22
22
  import { createDockerContext } from "./contexts/docker.js";
23
23
  import { createTuiStore } from "./session/sqlite.js";
24
- import { $ as buildContextualDiff, A as createSkillsCompletionProvider, B as EDIT_TOOL_NAMES, C as chunkMarkdownForRender, D as splitMarkdownCodeBlocks, E as normalizeMarkdownCodeFences, G as isVisibleDuringSearch, H as isTurnHighlighted, I as createCommandsCompletionProvider, J as selectableTurnIds, K as promptHistoryFromEvents, L as blendHsl, N as createFilesCompletionProvider, O as stripHtmlCommentsOutsideCode, R as buildLinearRamp, T as codeTokenHasClosingFence, U as isUserBashEvent, V as isEditErrorResult, W as isVisible, X as streamEventSearchText, Y as selectionAfterTurnRemoval, Z as turnSelectionOwnership, _ as turnContextSize, _t as summarizeOutcomes, a as TOOL_DISPLAY, at as filetypeFromPath, b as filterModelCatalog, bt as createChatsCompletionProvider, ct as summarizeEditPayload, d as finalizeStreamingMarkdown, et as buildUnifiedDiff, f as finalizeStreamingMarkdownForOwner, ft as mergeApprovalAndBodyOutcomes, g as revealEarlierTranscriptEvents, gt as stripEditOutcomesAnnotation, h as replaceTranscriptTurnEvents, ht as rewriteMultiEditHeader, i as turnAsText, it as extractEditPayload, j as uniqueSkillNamesFromReferences, mt as resolveApprovalForPayload, n as deleteTurnSafely, o as displayNameFor, ot as previewEditPayload, p as hiddenEarlierTranscriptCount, pt as parseEditOutcomesFromResult, q as retagUserPromptEvents, r as truncateTurnsAt, s as formatToolCall, tt as clipDiffForRender, u as capTranscriptEvents, ut as buildEditOutcomesAnnotation, v as splitPromptSegments, w as closeDanglingCodeFence, y as buildModelCatalog, z as BASH_CALL_ID_PREFIX } from "./turn-operations-iGu5J08j.js";
24
+ import { $ as buildContextualDiff, A as createSkillsCompletionProvider, B as EDIT_TOOL_NAMES, C as chunkMarkdownForRender, D as splitMarkdownCodeBlocks, E as normalizeMarkdownCodeFences, G as isVisibleDuringSearch, H as isTurnHighlighted, I as createCommandsCompletionProvider, J as selectableTurnIds, K as promptHistoryFromEvents, L as blendHsl, N as createFilesCompletionProvider, O as stripHtmlCommentsOutsideCode, R as buildLinearRamp, T as codeTokenHasClosingFence, U as isUserBashEvent, V as isEditErrorResult, W as isVisible, X as streamEventSearchText, Y as selectionAfterTurnRemoval, Z as turnSelectionOwnership, _ as turnContextSize, _t as summarizeOutcomes, a as TOOL_DISPLAY, at as filetypeFromPath, b as filterModelCatalog, bt as createChatsCompletionProvider, ct as summarizeEditPayload, d as finalizeStreamingMarkdown, et as buildUnifiedDiff, f as finalizeStreamingMarkdownForOwner, ft as mergeApprovalAndBodyOutcomes, g as revealEarlierTranscriptEvents, gt as stripEditOutcomesAnnotation, h as replaceTranscriptTurnEvents, ht as rewriteMultiEditHeader, i as turnAsText, it as extractEditPayload, j as uniqueSkillNamesFromReferences, mt as resolveApprovalForPayload, n as deleteTurnSafely, o as displayNameFor, ot as previewEditPayload, p as hiddenEarlierTranscriptCount, pt as parseEditOutcomesFromResult, q as retagUserPromptEvents, r as truncateTurnsAt, s as formatToolCall, tt as clipDiffForRender, u as capTranscriptEvents, ut as buildEditOutcomesAnnotation, v as splitPromptSegments, w as closeDanglingCodeFence, y as buildModelCatalog, z as BASH_CALL_ID_PREFIX } from "./turn-operations-7_Ff7INF.js";
25
25
  import { a as searchHighlightRanges, c as searchSessionHistoryIndexAsync, i as highlightSegments, n as buildSessionSearchIndexAsync, r as filterSessionSearchCatalog, u as searchSessionIndexAsync } from "./history-search-C4bk1Wz_.js";
26
- import { $ as McpAuthProvider, $t as filterChainFallbacks, An as discoverAgentsMd, Ar as toolCallPreview, At as useSettings, B as mcpToolsCachePath, C as addToSafelist, Cr as loadState, Ct as DEFAULT_SETTINGS, Dr as stripSpawnTokensLine, Dt as SETTINGS_TOGGLES, Er as serverToolResultSummary, Et as SETTINGS_INPUTS, F as oauthUsesManualCodePaste, G as useMcpToolToggleMap, Gt as useDiscoveryOptional, Ht as createDiscoverySlot, I as runOAuthLogin, J as indexOfServerRow, Jt as resolveConfig, Kt as ConfigProvider, L as supportsOAuth, M as fetchOAuthRedirect, Mr as updateToolEventOutcomes, Nr as usageByModel, Nt as resolveChipColor, Or as sumRunCosts, Ot as SettingsProvider, P as redirectUriFromAuthUrl, Pt as resolveTheme, Qt as chainsEqual, Rn as accentColor, Sr as listSessionMeta, St as useEnabledToggleSet, T as isOnSafelist, Tt as SETTINGS_CHOICES, U as subscribeMcpToolsCache, Ut as DiscoveryProvider, V as refreshMcpToolsCatalog, Wn as TODO_STATUS_GLYPHS, Wt as useDiscovery, X as createFileMcpCredentialStore, Xt as buildRunModel, Y as parentServerName, Yt as resolveStoragePaths, Zn as pruneTodosByRun, Zt as chainPositionOf, _r as compactSummaryEvent, _t as hintsLength, a as useSurfaces, an as countClaudeSessionFiles, at as InteractionsProvider, b as useSafeModeActions, bn as useUpdateCheck, br as eventsFromTurns, c as discoverSystemMd, cn as mcpsWritePath, ct as createInteractionTools, d as userSystemMdPath, dn as buildMcpServers, dr as buildBuildSystem, dt as pendingInteractionsFromTurns, en as providerKeySetChanged, er as useActiveTodos, et as useMcpAuthDispatch, f as useStreamBuffer, fn as dedupMcpServersByName, fr as buildPlanSystem, gn as tryOpenBrowser, gt as clipHintsToWidth, h as discoverProjectSkills, hr as createChatTurnsReadTool, ht as EMPTY_HINTS, i as useSelectStyle, in as useCompletion, it as ASK_USER_TOOL, j as formatPathForCwd, jr as toolResultText, k as suggestSafelistEntries, kn as chatAutoCompactBehavior, kt as clampFps, l as renderSystemMdSeed, ln as removeMcpServer, m as defaultSkillScanPaths, mn as discoverProjectMcps, mr as chatReferenceTokensForPrompt, mt as useInteractionsQueue, n as ThemeProvider, nt as getMcpAuthStatus, on as hasClaudeCodeSetup, ot as PRESENT_PLAN_TOOL, p as buildSkillsConfig, pr as envSection, pt as useInteractionsActions, q as buildVisibleMcpRows, qt as useConfig, r as useColors, rn as toggleChainEntry, s as useTheme, sn as importFromClaudeCode, st as buildResumedToolResultsTurn, t as computeTurnAnchors, tn as restoreChainFallbacks, tt as useMcpAuthState, u as userScopeOverridesLoad, un as upsertMcpServer, ut as makeRequestInteraction, v as writeSessionExport, vn as bootTick, vt as truncateTrailing, w as getSafelist, wr as marginTopFor, wt as SETTINGS_CATEGORIES, x as useSafeModeQueue, xr as lastContextSizeFromTurns, xt as listProjectFiles, y as SafeModeProvider, yn as buildUpdateHint, yr as deriveSessionTitle, yt as buildHints, z as loadMcpToolsCache } from "./transcript-anchors-Bv8HQ3S2.js";
26
+ import { $ as McpAuthProvider, $t as resolveStoragePaths, At as SETTINGS_INPUTS, B as mcpToolsCachePath, C as addToSafelist, Cn as buildUpdateHint, Ct as buildHints, Dr as listSessionMeta, Dt as DEFAULT_SETTINGS, Er as lastContextSizeFromTurns, Et as useEnabledToggleSet, F as oauthUsesManualCodePaste, Fr as toolCallPreview, G as useMcpToolToggleMap, Hn as accentColor, I as runOAuthLogin, Ir as toolResultText, J as indexOfServerRow, Jn as TODO_STATUS_GLYPHS, Jt as useDiscovery, Kt as createDiscoverySlot, L as supportsOAuth, Lr as updateToolEventOutcomes, Lt as resolveChipColor, M as fetchOAuthRedirect, Mr as stripSpawnTokensLine, Mt as SettingsProvider, Nn as chatAutoCompactBehavior, Nr as sumRunCosts, Nt as clampFps, Or as loadState, Ot as SETTINGS_CATEGORIES, P as redirectUriFromAuthUrl, Pn as discoverAgentsMd, Pt as useSettings, Qt as resolveConfig, Rr as usageByModel, Rt as resolveTheme, Sn as bootTick, Sr as compactSummaryEvent, St as truncateTrailing, T as isOnSafelist, Tr as eventsFromTurns, Tt as listProjectFiles, U as subscribeMcpToolsCache, V as refreshMcpToolsCatalog, X as createFileMcpCredentialStore, Xt as ConfigProvider, Y as parentServerName, Yt as useDiscoveryOptional, Zt as useConfig, _r as buildPlanSystem, _t as useInteractionsActions, a as useSurfaces, an as restoreChainFallbacks, ar as useActiveTodos, b as useSafeModeActions, bn as tryOpenBrowser, br as createChatTurnsReadTool, bt as clipHintsToWidth, c as discoverSystemMd, cn as useCompletion, ct as PRESENT_PLAN_TOOL, d as userSystemMdPath, dn as importFromClaudeCode, dt as createInteractionTools, en as buildRunModel, et as useMcpAuthDispatch, f as useStreamBuffer, fn as mcpsWritePath, gn as dedupMcpServersByName, gr as buildBuildSystem, h as discoverProjectSkills, hn as buildMcpServers, ht as pendingInteractionsFromTurns, i as useSelectStyle, in as providerKeySetChanged, ir as todoProgressLabel, it as ASK_USER_TOOL, j as formatPathForCwd, jr as serverToolResultSummary, jt as SETTINGS_TOGGLES, k as suggestSafelistEntries, kr as marginTopFor, kt as SETTINGS_CHOICES, l as renderSystemMdSeed, ln as countClaudeSessionFiles, lt as buildResumedToolResultsTurn, m as defaultSkillScanPaths, mn as upsertMcpServer, mt as makeRequestInteraction, n as ThemeProvider, nn as chainsEqual, nt as getMcpAuthStatus, ot as HOST_INTERACTION_ID_PREFIX, p as buildSkillsConfig, pn as removeMcpServer, pt as isModelInteraction, q as buildVisibleMcpRows, qt as DiscoveryProvider, r as useColors, rn as filterChainFallbacks, rr as setTodosForRun, s as useTheme, sn as toggleChainEntry, st as InteractionsProvider, t as computeTurnAnchors, tn as chainPositionOf, tr as pruneTodosByRun, tt as useMcpAuthState, u as userScopeOverridesLoad, un as hasClaudeCodeSetup, ut as createAutomaticInteractionResponse, v as writeSessionExport, vn as discoverProjectMcps, vr as envSection, vt as useInteractionsQueue, w as getSafelist, wn as useUpdateCheck, wr as deriveSessionTitle, x as useSafeModeQueue, xt as hintsLength, y as SafeModeProvider, yr as chatReferenceTokensForPrompt, yt as EMPTY_HINTS, z as loadMcpToolsCache } from "./transcript-anchors-D-Yuimif.js";
27
27
  import { basename, delimiter, dirname, join, relative, resolve } from "node:path";
28
28
  import { Buffer } from "node:buffer";
29
29
  import * as fs from "node:fs";
@@ -6079,8 +6079,14 @@ function parseEditorCommand(spec) {
6079
6079
  * `<`, `>` and friends carry none of the obvious "needs quoting" signals, so a
6080
6080
  * repo cloned into `C:\work\a&b` would otherwise hand `C:\work\a&b\AGENTS.md`
6081
6081
  * over bare, and cmd would split it at the `&` and run the tail as a second
6082
- * command. `%` and `!` are included so a path containing them can't be mistaken
6083
- * for a variable reference at parse time.
6082
+ * command.
6083
+ *
6084
+ * `%` and `!` are in the class so such a token is quoted rather than passed
6085
+ * bare — but be clear about the limit: quoting does NOT stop cmd.exe expanding
6086
+ * `%VAR%`, nor `!VAR!` where delayed expansion is on. Double quotes suppress
6087
+ * splitting and other syntax, not expansion. That is a property of the shell
6088
+ * which no amount of quoting can close; the worst case is a wrong path, not an
6089
+ * injected command.
6084
6090
  */
6085
6091
  function quoteForShell(token) {
6086
6092
  if (token === "") return "\"\"";
@@ -7500,36 +7506,31 @@ function runEditorOnText(initialText, options = {}) {
7500
7506
  //#endregion
7501
7507
  //#region src/tui/clipboard-image.ts
7502
7508
  /**
7503
- * Read an IMAGE from the OS clipboard.
7504
- *
7505
- * The companion to the write-only {@link import('./clipboard')} module. Pasting
7506
- * a clipboard image into a terminal is fundamentally different from pasting
7507
- * text: terminals deliver text via bracketed paste (`PasteEvent`), but they do
7508
- * NOT transmit binary clipboard image data that way — on macOS a `cmd+V` of a
7509
- * screenshot usually produces no paste event at all. So to support "paste the
7510
- * image I just copied" we have to read the OS clipboard directly via a
7511
- * platform helper, on an explicit trigger.
7509
+ * Read image or text content directly from the OS clipboard.
7512
7510
  *
7513
- * Every outcome is classified (never thrown) so the caller can show the right
7514
- * message:
7515
- * - `ok` → bytes + sniffed media type, ready to attach.
7516
- * - `no-image` → clipboard holds no image (probably text) — soft no-op.
7517
- * - `helper-missing` → the platform helper isn't installed (`detail` carries
7518
- * an install hint).
7519
- * - `unsupported-env` → remote session (SSH): the helper would read the
7520
- * REMOTE box's clipboard, not the user's.
7521
- * - `failed` → helper ran but produced no usable image.
7511
+ * Images are never carried by terminal paste. Text normally is, but some
7512
+ * integrated-terminal PTYs freeze while forwarding large bracketed pastes. The
7513
+ * explicit app-owned shortcut therefore reads either representation through a
7514
+ * platform helper, keeping the clipboard payload out of the terminal stream.
7522
7515
  *
7523
- * SSH note: like OSC 52 on the write side, there's no portable way to reach the
7524
- * user's LOCAL clipboard image over SSH, so we detect it and bail with a clear
7525
- * message rather than silently reading the wrong machine's clipboard.
7516
+ * Every outcome is classified rather than thrown so the composer can surface a
7517
+ * precise notice. Remote sessions are rejected because native helpers would
7518
+ * read the remote machine's clipboard, not the user's local clipboard.
7519
+ */
7520
+ /**
7521
+ * Cap on clipboard TEXT (raw bytes), matching the dragged-file attachment
7522
+ * ceiling in the composer. Native reads deliberately bypass the terminal's
7523
+ * paste backpressure, which used to be the de-facto limit — without a cap a
7524
+ * multi-gigabyte clipboard would be buffered here, folded into `paste.txt`,
7525
+ * persisted in the session, and shipped to the provider.
7526
7526
  */
7527
+ const MAX_CLIPBOARD_TEXT_BYTES = 20 * 1024 * 1024;
7527
7528
  /** True in a remote session, where a local clipboard helper would read the wrong box. */
7528
7529
  function isRemoteSession(env = process.env) {
7529
7530
  return !!(env.SSH_CONNECTION || env.SSH_TTY || env.SSH_CLIENT);
7530
7531
  }
7531
7532
  /** Spawn a helper and capture its (binary) stdout. Never throws. */
7532
- function captureViaSpawn(cmd, args, timeoutMs = 5e3) {
7533
+ function captureViaSpawn(cmd, args, timeoutMs = 5e3, maxBytes = Infinity) {
7533
7534
  return new Promise((resolveP) => {
7534
7535
  let child;
7535
7536
  try {
@@ -7549,8 +7550,19 @@ function captureViaSpawn(cmd, args, timeoutMs = 5e3) {
7549
7550
  }
7550
7551
  const out = [];
7551
7552
  const err = [];
7553
+ let size = 0;
7554
+ let truncated = false;
7552
7555
  const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
7553
- child.stdout?.on("data", (chunk) => out.push(chunk));
7556
+ child.stdout?.on("data", (chunk) => {
7557
+ if (truncated) return;
7558
+ size += chunk.length;
7559
+ if (size > maxBytes) {
7560
+ truncated = true;
7561
+ child.kill("SIGKILL");
7562
+ return;
7563
+ }
7564
+ out.push(chunk);
7565
+ });
7554
7566
  child.stderr?.setEncoding("utf8");
7555
7567
  child.stderr?.on("data", (chunk) => err.push(chunk));
7556
7568
  child.on("error", () => {
@@ -7568,7 +7580,8 @@ function captureViaSpawn(cmd, args, timeoutMs = 5e3) {
7568
7580
  code,
7569
7581
  stdout: Buffer.concat(out),
7570
7582
  stderr: err.join(""),
7571
- spawnError: false
7583
+ spawnError: false,
7584
+ truncated
7572
7585
  });
7573
7586
  });
7574
7587
  });
@@ -7715,6 +7728,143 @@ async function readClipboardImage(hooks = {}) {
7715
7728
  detail: "no clipboard helper available in this environment"
7716
7729
  };
7717
7730
  }
7731
+ async function readClipboardTextWith(capture, platform, env) {
7732
+ let command;
7733
+ let args;
7734
+ let installHint;
7735
+ /**
7736
+ * Second text target to try when the first isn't on the clipboard.
7737
+ * `xclip` has no "any text" alias, so a source offering only the legacy
7738
+ * `STRING` target fails the `UTF8_STRING` read; retrying keeps such
7739
+ * sources working without falling back to an untyped read, which would
7740
+ * hand back image bytes on an image-only clipboard.
7741
+ */
7742
+ let fallbackArgs;
7743
+ /**
7744
+ * Whether the helper was asked for a SPECIFIC text target. Such a helper
7745
+ * exits non-zero when the clipboard doesn't offer that type, which means
7746
+ * "no text here", not "the helper broke" — so the exit classifies as
7747
+ * `no-text` instead of surfacing its stderr as an inline notice.
7748
+ * `pbpaste` / PowerShell are untyped: they exit 0 with empty output on an
7749
+ * empty clipboard and reach the same verdict via the length check below.
7750
+ */
7751
+ let textTargetsExhausted = false;
7752
+ if (platform === "darwin") {
7753
+ command = "pbpaste";
7754
+ args = [];
7755
+ installHint = "pbpaste is required to paste text";
7756
+ } else if (platform === "win32") {
7757
+ command = "powershell";
7758
+ args = [
7759
+ "-NoProfile",
7760
+ "-NonInteractive",
7761
+ "-Command",
7762
+ "Add-Type -AssemblyName System.Windows.Forms; [Console]::OutputEncoding=[System.Text.UTF8Encoding]::new($false); [Console]::Out.Write([System.Windows.Forms.Clipboard]::GetText())"
7763
+ ];
7764
+ installHint = "PowerShell is required to paste text";
7765
+ } else if (env.WAYLAND_DISPLAY) {
7766
+ command = "wl-paste";
7767
+ args = [
7768
+ "--no-newline",
7769
+ "--type",
7770
+ "text"
7771
+ ];
7772
+ textTargetsExhausted = true;
7773
+ installHint = "install wl-clipboard to paste text";
7774
+ } else if (env.DISPLAY) {
7775
+ command = "xclip";
7776
+ args = [
7777
+ "-selection",
7778
+ "clipboard",
7779
+ "-t",
7780
+ "UTF8_STRING",
7781
+ "-o"
7782
+ ];
7783
+ fallbackArgs = [
7784
+ "-selection",
7785
+ "clipboard",
7786
+ "-t",
7787
+ "STRING",
7788
+ "-o"
7789
+ ];
7790
+ textTargetsExhausted = true;
7791
+ installHint = "install xclip to paste text";
7792
+ } else return {
7793
+ ok: false,
7794
+ reason: "helper-missing",
7795
+ detail: "no clipboard helper available in this environment"
7796
+ };
7797
+ let result = await capture(command, args, void 0, MAX_CLIPBOARD_TEXT_BYTES);
7798
+ if (result.spawnError) return {
7799
+ ok: false,
7800
+ reason: "helper-missing",
7801
+ detail: installHint
7802
+ };
7803
+ if (result.code !== 0 && !result.truncated && fallbackArgs) result = await capture(command, fallbackArgs, void 0, MAX_CLIPBOARD_TEXT_BYTES);
7804
+ if (result.truncated || result.stdout.length > 20971520) return {
7805
+ ok: false,
7806
+ reason: "too-large",
7807
+ detail: `clipboard text exceeds ${Math.round(MAX_CLIPBOARD_TEXT_BYTES / (1024 * 1024))} MB — paste it from a file instead`
7808
+ };
7809
+ if (result.code !== 0 && textTargetsExhausted) return {
7810
+ ok: false,
7811
+ reason: "no-text"
7812
+ };
7813
+ if (result.code !== 0) return {
7814
+ ok: false,
7815
+ reason: "failed",
7816
+ detail: result.stderr.trim() || "clipboard helper failed"
7817
+ };
7818
+ if (result.stdout.length === 0) return {
7819
+ ok: false,
7820
+ reason: "no-text"
7821
+ };
7822
+ return {
7823
+ ok: true,
7824
+ text: result.stdout.toString("utf8")
7825
+ };
7826
+ }
7827
+ /** Read text directly from the OS clipboard without sending it through the terminal PTY. */
7828
+ async function readClipboardText(hooks = {}) {
7829
+ const platform = hooks.platform ?? process.platform;
7830
+ const env = hooks.env ?? process.env;
7831
+ if (isRemoteSession(env)) return {
7832
+ ok: false,
7833
+ reason: "unsupported-env",
7834
+ detail: "clipboard paste is unavailable over SSH — paste the text through your local terminal instead"
7835
+ };
7836
+ return readClipboardTextWith(hooks.capture ?? captureViaSpawn, platform, env);
7837
+ }
7838
+ /** Prefer clipboard images, then fall back to text read outside the terminal PTY. */
7839
+ async function readClipboardContent(hooks = {}) {
7840
+ const image = await readClipboardImage(hooks);
7841
+ if (image.ok) return {
7842
+ ok: true,
7843
+ kind: "image",
7844
+ data: image.data,
7845
+ mediaType: image.mediaType
7846
+ };
7847
+ const text = await readClipboardText(hooks);
7848
+ if (text.ok) return {
7849
+ ok: true,
7850
+ kind: "text",
7851
+ text: text.text
7852
+ };
7853
+ if (text.reason === "no-text" && image.reason === "no-image") return {
7854
+ ok: false,
7855
+ reason: "empty"
7856
+ };
7857
+ if (text.reason === "no-text") return {
7858
+ ok: false,
7859
+ reason: image.reason === "no-image" ? "empty" : image.reason,
7860
+ ...image.detail ? { detail: image.detail } : {}
7861
+ };
7862
+ return {
7863
+ ok: false,
7864
+ reason: text.reason,
7865
+ ...text.detail ? { detail: text.detail } : {}
7866
+ };
7867
+ }
7718
7868
  //#endregion
7719
7869
  //#region src/tui/completion-popup.tsx
7720
7870
  /**
@@ -9258,23 +9408,17 @@ function QuestionInteractionBlock({ request, onResolve }) {
9258
9408
  const { width: termCols, height: termRows } = useTerminalDimensions();
9259
9409
  const maxRows = wizardMaxRows(termRows);
9260
9410
  const contentCols = Math.max(1, termCols - 4);
9261
- const measuredRows = useMemo(() => introRows(intro, contentCols) + questions.reduce((sum, q, i) => sum + estimateQuestionRowHeight(q, i < activeIndex ? "done" : i === activeIndex ? "active" : "pending", contentCols), 0), [
9411
+ const contentRows = useMemo(() => {
9412
+ const introHeight = introRows(intro, contentCols);
9413
+ const heightWithFocus = (focus) => introHeight + questions.reduce((sum, q, i) => sum + estimateQuestionRowHeight(q, i < focus ? "done" : i === focus ? "active" : "pending", contentCols), 0);
9414
+ const tallest = questions.length === 0 ? introHeight : questions.reduce((max, _, focus) => Math.max(max, heightWithFocus(focus)), 0);
9415
+ return Math.min(tallest, maxRows);
9416
+ }, [
9262
9417
  intro,
9263
9418
  questions,
9264
- activeIndex,
9265
- contentCols
9419
+ contentCols,
9420
+ maxRows
9266
9421
  ]);
9267
- const highWaterRef = useRef({
9268
- key: "",
9269
- rows: 0
9270
- });
9271
- const highWaterKey = `${request.id}:${contentCols}`;
9272
- if (highWaterRef.current.key !== highWaterKey) highWaterRef.current = {
9273
- key: highWaterKey,
9274
- rows: 0
9275
- };
9276
- highWaterRef.current.rows = Math.max(highWaterRef.current.rows, measuredRows);
9277
- const contentRows = Math.min(highWaterRef.current.rows, maxRows);
9278
9422
  return /* @__PURE__ */ jsxs(InteractionShell, {
9279
9423
  title: questions.length > 1 ? ` answer ${activeIndex + 1}/${questions.length} · esc to abort run ` : " answer · esc to abort run ",
9280
9424
  maxHeight: maxRows,
@@ -10328,72 +10472,6 @@ function McpAuthorizingPanel({ serverName, authUrl, maxLineWidth, chromeWidth, i
10328
10472
  });
10329
10473
  }
10330
10474
  //#endregion
10331
- //#region src/tui/todo-indicator.tsx
10332
- /**
10333
- * Layout budget when truncating the content. The bar never wraps —
10334
- * single-line by contract — so we subtract every column consumed by
10335
- * surrounding chrome before measuring how much room is left for the
10336
- * todo's `content` text.
10337
- *
10338
- * - 2 cols ⇒ `<ChatScreen>`'s `border: true` (1 cell each side).
10339
- * - 2 cols ⇒ this indicator's own `paddingLeft: 1 + paddingRight: 1`
10340
- * (mirrors the queue block's outer padding so the bar
10341
- * and queue read as aligned siblings).
10342
- * - 4 cols ⇒ the indicator's own visible chrome: glyph "◐" (1) +
10343
- * two spaces (2) + 1 col of trailing breathing room.
10344
- *
10345
- * That's 8 cols total of non-content overhead. Below `MIN_VISIBLE_TAIL`
10346
- * the bar bails — a one-or-two-char tail isn't worth painting.
10347
- */
10348
- const SCREEN_BORDER_COLS = 2;
10349
- const INDICATOR_PADDING_COLS = 2;
10350
- const CHROME_COLS = 4;
10351
- const MIN_VISIBLE_TAIL = 8;
10352
- function truncateForWidth(text, max) {
10353
- if (max <= 0) return "";
10354
- if (text.length <= max) return text;
10355
- if (max <= 1) return "…";
10356
- return `${text.slice(0, max - 1)}…`;
10357
- }
10358
- function TodoIndicator({ session }) {
10359
- const { settings } = useSettings();
10360
- const COLOR = useColors();
10361
- const { width: termWidth } = useTerminalDimensions();
10362
- const state = useActiveTodos(session);
10363
- if (!settings.showTodoIndicator) return null;
10364
- const item = state.inProgress;
10365
- if (!item) return null;
10366
- const usable = Math.max(0, termWidth - SCREEN_BORDER_COLS - INDICATOR_PADDING_COLS - CHROME_COLS);
10367
- if (usable < MIN_VISIBLE_TAIL) return null;
10368
- const display = truncateForWidth(item.content.replace(/\s+/g, " ").trim(), usable);
10369
- return /* @__PURE__ */ jsx$1("box", {
10370
- style: {
10371
- marginTop: 1,
10372
- flexShrink: 0,
10373
- flexDirection: "row",
10374
- paddingLeft: 1,
10375
- paddingRight: 1
10376
- },
10377
- children: /* @__PURE__ */ jsxs("text", {
10378
- wrapMode: "none",
10379
- children: [
10380
- /* @__PURE__ */ jsx$1("span", {
10381
- fg: COLOR.warn,
10382
- children: TODO_STATUS_GLYPHS.in_progress
10383
- }),
10384
- /* @__PURE__ */ jsx$1("span", {
10385
- fg: COLOR.mute,
10386
- children: " "
10387
- }),
10388
- /* @__PURE__ */ jsx$1("span", {
10389
- fg: COLOR.dim,
10390
- children: display
10391
- })
10392
- ]
10393
- })
10394
- });
10395
- }
10396
- //#endregion
10397
10475
  //#region src/tui/screens.tsx
10398
10476
  /** How long a clipboard-paste status line stays before self-clearing. */
10399
10477
  const NOTICE_TTL_MS = 5e3;
@@ -10418,6 +10496,26 @@ const MAX_FILE_ATTACHMENT_BYTES = 20 * 1024 * 1024;
10418
10496
  * every subsequent render of the prompt) crawl.
10419
10497
  */
10420
10498
  const PASTE_INLINE_MAX_CHARS = 1e4;
10499
+ /**
10500
+ * Normalize terminal/native clipboard text and decide whether the composer
10501
+ * should insert it or fold it into a text attachment.
10502
+ */
10503
+ function planTextPaste(text, attachmentsEnabled) {
10504
+ const normalized = stripAnsiSequences(text).replace(/\r\n?/g, "\n");
10505
+ const lines = normalized.trim().split("\n");
10506
+ if (attachmentsEnabled && (lines.length > 10 || normalized.length > PASTE_INLINE_MAX_CHARS)) return {
10507
+ kind: "attachment",
10508
+ attachment: {
10509
+ name: "paste.txt",
10510
+ content: Buffer.from(normalized, "utf8"),
10511
+ mediaType: "text/plain"
10512
+ }
10513
+ };
10514
+ return {
10515
+ kind: "inline",
10516
+ text: normalized
10517
+ };
10518
+ }
10421
10519
  /** `image/png` → `png` etc., for naming the synthesized clipboard attachment. */
10422
10520
  function extForImageMediaType(mediaType) {
10423
10521
  const sub = mediaType.split("/")[1] ?? "png";
@@ -11091,6 +11189,7 @@ function OAuthRunningStep({ descriptor, dataDir, onSuccess, onError }) {
11091
11189
  kind: "oauth",
11092
11190
  ...creds
11093
11191
  });
11192
+ if (descriptor.envKey) delete process.env[descriptor.envKey];
11094
11193
  onSuccess();
11095
11194
  } catch (err) {
11096
11195
  if (cancelled) return;
@@ -11738,7 +11837,7 @@ const EMPTY_QUEUED_MESSAGES = [];
11738
11837
  function shouldRenderTranscriptSearch(searchMode, query) {
11739
11838
  return searchMode && query.trim().length > 0;
11740
11839
  }
11741
- 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 }) {
11840
+ 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 }) {
11742
11841
  const COLOR = useColors();
11743
11842
  const SURFACE = useSurfaces();
11744
11843
  const titleText = session?.title ?? "New session";
@@ -11806,6 +11905,7 @@ function ChatScreen({ cwd, events, busy, compacting = false, queuedMessages = EM
11806
11905
  }, [onPopupOpenChange]);
11807
11906
  const fileEditPending = pending && isFileEditTool(pending.tool) ? pending : null;
11808
11907
  const planPending = !pending && pendingInteraction?.kind === "plan" ? pendingInteraction : null;
11908
+ const approvalPending = pending && !fileEditPending ? pending : null;
11809
11909
  const modal = useModal();
11810
11910
  useEffect(() => {
11811
11911
  if (!fileEditPending) return;
@@ -11861,47 +11961,58 @@ function ChatScreen({ cwd, events, busy, compacting = false, queuedMessages = EM
11861
11961
  request: fileEditPending,
11862
11962
  onDecide: onApproval
11863
11963
  }, fileEditPending.id),
11864
- /* @__PURE__ */ jsx$1("box", {
11964
+ /* @__PURE__ */ jsxs("box", {
11865
11965
  style: {
11866
11966
  flexDirection: "column",
11867
11967
  flexShrink: 0,
11868
11968
  backgroundColor: SURFACE.background
11869
11969
  },
11870
- children: pending && !fileEditPending ? /* @__PURE__ */ jsx$1(ApprovalBlock, {
11871
- request: pending,
11872
- onPick: onApproval
11873
- }) : pendingInteraction ? /* @__PURE__ */ jsx$1(InteractionBlock, {
11874
- request: pendingInteraction,
11875
- onResolve: onInteraction
11876
- }, pendingInteraction.id) : /* @__PURE__ */ jsxs(Fragment, { children: [
11877
- queuedMessages.length > 0 && !completionPopupOpen && /* @__PURE__ */ jsx$1(QueuedMessagesBlock, {
11878
- messages: queuedMessages,
11879
- selectionIndex: queueSelectionIndex,
11880
- shortcuts: queueShortcuts
11970
+ children: [
11971
+ approvalPending && /* @__PURE__ */ jsx$1(ApprovalBlock, {
11972
+ request: approvalPending,
11973
+ onPick: onApproval
11881
11974
  }),
11882
- /* @__PURE__ */ jsx$1(TodoIndicator, { session: liveSession }),
11883
- /* @__PURE__ */ jsx$1(ExtensionBanners, {}),
11884
- /* @__PURE__ */ jsx$1(PromptBlock, {
11885
- userPrompts,
11886
- onSubmit,
11887
- completionProviders,
11888
- inlineAutocompleteProviders,
11889
- onCompletionTab,
11890
- enterSelectsCompletion,
11891
- onPopupOpenChange: handlePopupOpenChange,
11892
- searchMode: transcriptSearchMode,
11893
- onSearchModeChange: onTranscriptSearchModeChange,
11894
- onSearchQueryChange: onTranscriptSearchQueryChange,
11895
- onSearchNavigate: onTranscriptSearchNavigate,
11896
- onSearchOpen: onTranscriptSearchOpen,
11897
- selectMode: queueSelectionIndex != null ? "queue" : null,
11898
- triggerHints: promptTriggerHints,
11899
- busy,
11900
- onEnterQueueFromEmpty: onEnterQueueFromEmptyPrompt,
11901
- attachImageBinding,
11902
- editorBinding
11975
+ pendingInteraction && !approvalPending && /* @__PURE__ */ jsx$1(InteractionBlock, {
11976
+ request: pendingInteraction,
11977
+ onResolve: onInteraction
11978
+ }, pendingInteraction.id),
11979
+ /* @__PURE__ */ jsxs("box", {
11980
+ style: {
11981
+ flexDirection: "column",
11982
+ flexShrink: 0,
11983
+ visible: approvalPending == null && pendingInteraction == null
11984
+ },
11985
+ children: [
11986
+ queuedMessages.length > 0 && !completionPopupOpen && /* @__PURE__ */ jsx$1(QueuedMessagesBlock, {
11987
+ messages: queuedMessages,
11988
+ selectionIndex: queueSelectionIndex,
11989
+ shortcuts: queueShortcuts
11990
+ }),
11991
+ /* @__PURE__ */ jsx$1(ExtensionBanners, {}),
11992
+ /* @__PURE__ */ jsx$1(PromptBlock, {
11993
+ userPrompts,
11994
+ onSubmit,
11995
+ completionProviders,
11996
+ inlineAutocompleteProviders,
11997
+ onCompletionTab,
11998
+ enterSelectsCompletion,
11999
+ onPopupOpenChange: handlePopupOpenChange,
12000
+ searchMode: transcriptSearchMode,
12001
+ onSearchModeChange: onTranscriptSearchModeChange,
12002
+ onSearchQueryChange: onTranscriptSearchQueryChange,
12003
+ onSearchNavigate: onTranscriptSearchNavigate,
12004
+ onSearchOpen: onTranscriptSearchOpen,
12005
+ selectMode: queueSelectionIndex != null ? "queue" : null,
12006
+ triggerHints: promptTriggerHints,
12007
+ busy,
12008
+ onEnterQueueFromEmpty: onEnterQueueFromEmptyPrompt,
12009
+ attachImageBinding,
12010
+ editorBinding,
12011
+ suspended: approvalPending != null || pendingInteraction != null
12012
+ })
12013
+ ]
11903
12014
  })
11904
- ] })
12015
+ ]
11905
12016
  }),
11906
12017
  !planPending && !fileEditPending && /* @__PURE__ */ jsx$1(TitleOverlay, {
11907
12018
  title: titleText,
@@ -12071,9 +12182,23 @@ function queueRowId(index) {
12071
12182
  * (where committed messages live) and the prompt (where the user is
12072
12183
  * typing the next one). Dropped entirely when the queue is empty.
12073
12184
  */
12185
+ /**
12186
+ * Badge text for a queued message's attachment count, or `null` when there is
12187
+ * nothing to show.
12188
+ *
12189
+ * Split out from the row so the logic — pluralization, and the distinction
12190
+ * between "no attachments" and "this preview predates the field" — is testable
12191
+ * without standing up `ChatScreen`'s provider stack. Padded with spaces because
12192
+ * it renders as a filled chip and needs breathing room inside its background.
12193
+ */
12194
+ function attachmentBadgeLabel(count) {
12195
+ if (count === void 0 || count <= 0) return null;
12196
+ return ` +${count} ${count === 1 ? "attachment" : "attachments"} `;
12197
+ }
12074
12198
  function QueuedMessagesBlock({ messages, selectionIndex, shortcuts }) {
12075
12199
  const COLOR = useColors();
12076
12200
  const SURFACE = useSurfaces();
12201
+ const attachmentChip = resolveChipColor(SURFACE.chips, "file");
12077
12202
  const scrollboxRef = useRef(null);
12078
12203
  const enterHintKey = shortcuts?.enter ? formatBindingForDisplay(shortcuts.enter) : "↑";
12079
12204
  useEffect(() => {
@@ -12136,6 +12261,11 @@ function QueuedMessagesBlock({ messages, selectionIndex, shortcuts }) {
12136
12261
  fg: COLOR.mute,
12137
12262
  children: "❯ "
12138
12263
  }),
12264
+ attachmentBadgeLabel(msg.attachments) !== null && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx$1("span", {
12265
+ fg: attachmentChip.fg,
12266
+ bg: attachmentChip.bg,
12267
+ children: attachmentBadgeLabel(msg.attachments)
12268
+ }), /* @__PURE__ */ jsx$1("span", { children: " " })] }),
12139
12269
  /* @__PURE__ */ jsx$1("span", {
12140
12270
  fg: focused ? COLOR.brand : COLOR.dim,
12141
12271
  children: previewText(msg.text)
@@ -12225,7 +12355,7 @@ const INLINE_GHOST_DEBOUNCE_MS = 120;
12225
12355
  function isTranscriptSearchExitKey(event, cursorOffset) {
12226
12356
  return event.name === "escape" || (event.name === "backspace" || event.name === "delete") && !event.ctrl && !event.meta && !event.shift && cursorOffset === 0;
12227
12357
  }
12228
- function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocompleteProviders, onCompletionTab, enterSelectsCompletion, onPopupOpenChange, searchMode = false, onSearchModeChange, onSearchQueryChange, onSearchNavigate, onSearchOpen, selectMode = null, triggerHints, busy = false, onEnterQueueFromEmpty, attachImageBinding, editorBinding }) {
12358
+ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocompleteProviders, onCompletionTab, enterSelectsCompletion, onPopupOpenChange, searchMode = false, onSearchModeChange, onSearchQueryChange, onSearchNavigate, onSearchOpen, selectMode = null, triggerHints, busy = false, onEnterQueueFromEmpty, attachImageBinding, editorBinding, clipboardReader = readClipboardContent, suspended = false }) {
12229
12359
  const focused = useModalAwareFocus();
12230
12360
  const renderer = useRenderer();
12231
12361
  const COLOR = useColors();
@@ -12488,30 +12618,12 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
12488
12618
  syncBuffer();
12489
12619
  return true;
12490
12620
  }, [completion, syncBuffer]);
12491
- /**
12492
- * Multi-line paste handler. The textarea's default `handlePaste`
12493
- * forwards the decoded bytes to `insertText` in one call, which the
12494
- * native edit buffer treats as raw characters — embedded `\n` lands
12495
- * as a literal glyph rather than splitting the line, and bracketed
12496
- * paste from macOS Terminal often substitutes CR for LF on top of
12497
- * that. The result was a multi-line paste collapsing to one giant
12498
- * row.
12499
- *
12500
- * We `preventDefault` to skip the textarea's default path, normalize
12501
- * `\r\n` / `\r` to `\n`, then drive the edit buffer explicitly:
12502
- * `insertText` for each segment, `newLine()` between segments. The
12503
- * cursor is positioned at the end of the pasted block (the natural
12504
- * place to keep typing).
12505
- */
12506
- const onPaste = useCallback((event) => {
12507
- event.preventDefault();
12621
+ /** Apply decoded clipboard text through the shared file/attachment/inline policy. */
12622
+ const applyTextPaste = useCallback((text) => {
12508
12623
  const ta = textareaRef.current;
12509
12624
  if (!ta) return;
12510
- const normalized = stripAnsiSequences(decodePasteBytes(event.bytes)).replace(/\r\n?/g, "\n");
12511
- if (normalized.length === 0) {
12512
- if (attachmentsEnabled) attachClipboardImage();
12513
- return;
12514
- }
12625
+ const normalized = stripAnsiSequences(text).replace(/\r\n?/g, "\n");
12626
+ if (normalized.length === 0) return;
12515
12627
  if (attachmentsEnabled && !normalized.includes("\n")) {
12516
12628
  const trimmed = normalized.trim().replace(/^["']|["']$/g, "");
12517
12629
  let resolved = trimmed.startsWith("file://") ? decodeURIComponent(trimmed.slice(7)) : trimmed;
@@ -12535,15 +12647,12 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
12535
12647
  }
12536
12648
  } catch {}
12537
12649
  }
12538
- const lines = normalized.trim().split("\n");
12539
- if (attachmentsEnabled && (lines.length > 10 || normalized.length > PASTE_INLINE_MAX_CHARS)) {
12540
- setAttachments((prev) => [...prev, {
12541
- name: "paste.txt",
12542
- content: Buffer.from(normalized, "utf-8"),
12543
- mediaType: "text/plain"
12544
- }]);
12650
+ const pastePlan = planTextPaste(normalized, attachmentsEnabled);
12651
+ if (pastePlan.kind === "attachment") {
12652
+ setAttachments((prev) => [...prev, pastePlan.attachment]);
12545
12653
  return;
12546
12654
  }
12655
+ const lines = pastePlan.text.trim().split("\n");
12547
12656
  for (let i = 0; i < lines.length; i++) {
12548
12657
  if (i > 0) ta.newLine();
12549
12658
  const part = lines[i];
@@ -12551,9 +12660,64 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
12551
12660
  }
12552
12661
  syncBuffer();
12553
12662
  }, [
12554
- syncBuffer,
12555
12663
  attachmentsEnabled,
12556
- attachClipboardImage,
12664
+ showNotice,
12665
+ syncBuffer
12666
+ ]);
12667
+ /**
12668
+ * Prevent the textarea's one-shot insert and explicitly preserve multiline
12669
+ * paste. Empty terminal paste remains the image-clipboard probe.
12670
+ */
12671
+ const onPaste = useCallback((event) => {
12672
+ event.preventDefault();
12673
+ const raw = decodePasteBytes(event.bytes);
12674
+ if (stripAnsiSequences(raw).replace(/\r\n?/g, "\n").length === 0) {
12675
+ if (attachmentsEnabled) attachClipboardImage();
12676
+ return;
12677
+ }
12678
+ applyTextPaste(raw);
12679
+ }, [
12680
+ applyTextPaste,
12681
+ attachmentsEnabled,
12682
+ attachClipboardImage
12683
+ ]);
12684
+ /**
12685
+ * Read the OS clipboard on the app-owned keybinding. Unlike terminal paste,
12686
+ * this moves only the shortcut bytes through the PTY, avoiding large-paste
12687
+ * backpressure bugs in integrated terminals. Text is re-injected through the
12688
+ * exact same paste handler as bracketed paste; images retain the existing
12689
+ * attachment behavior.
12690
+ */
12691
+ const pasteClipboard = useCallback(async () => {
12692
+ const result = await clipboardReader();
12693
+ if (!result.ok) {
12694
+ showNotice(result.reason === "empty" ? "clipboard has no text or image" : result.detail ?? "could not read the clipboard");
12695
+ return;
12696
+ }
12697
+ if (result.kind === "text") {
12698
+ applyTextPaste(result.text);
12699
+ return;
12700
+ }
12701
+ if (!attachmentsEnabled) {
12702
+ showNotice("attachments are disabled — enable them in Settings → Agent");
12703
+ return;
12704
+ }
12705
+ if (result.data.length > MAX_IMAGE_BYTES) {
12706
+ showNotice(`clipboard image too large (${formatBytes(result.data.length)}) — max ${formatBytes(MAX_IMAGE_BYTES)}`);
12707
+ return;
12708
+ }
12709
+ setAttachments((prev) => {
12710
+ const n = prev.filter((a) => a.name.startsWith("clipboard-")).length + 1;
12711
+ return [...prev, {
12712
+ name: `clipboard-${n}.${extForImageMediaType(result.mediaType)}`,
12713
+ content: result.data,
12714
+ mediaType: result.mediaType
12715
+ }];
12716
+ });
12717
+ }, [
12718
+ applyTextPaste,
12719
+ attachmentsEnabled,
12720
+ clipboardReader,
12557
12721
  showNotice
12558
12722
  ]);
12559
12723
  /**
@@ -12620,7 +12784,7 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
12620
12784
  const onKeyDown = useCallback((event) => {
12621
12785
  if (attachImageBinding && matchesBinding(event, attachImageBinding)) {
12622
12786
  event.preventDefault();
12623
- attachClipboardImage();
12787
+ pasteClipboard();
12624
12788
  return;
12625
12789
  }
12626
12790
  if (editorBinding && matchesBinding(event, editorBinding)) {
@@ -12752,7 +12916,7 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
12752
12916
  enterSelectsCompletion,
12753
12917
  syncBuffer,
12754
12918
  attachImageBinding,
12755
- attachClipboardImage,
12919
+ pasteClipboard,
12756
12920
  editorBinding,
12757
12921
  openExternalEditor,
12758
12922
  attachments,
@@ -12848,7 +13012,7 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
12848
13012
  },
12849
13013
  children: [/* @__PURE__ */ jsx$1("textarea", {
12850
13014
  ref: textareaRef,
12851
- focused: focused && !selectMode,
13015
+ focused: focused && !selectMode && !suspended,
12852
13016
  keyBindings: TEXTAREA_BINDINGS,
12853
13017
  wrapMode: "word",
12854
13018
  placeholder: selectMode === "queue" ? "— queue-select mode — press ⎋ to resume typing —" : searchMode ? "Search this conversation…" : shellMode ? "Run a shell command…" : "Ask zidane…",
@@ -13692,6 +13856,78 @@ function statusColor(status, COLOR) {
13692
13856
  }
13693
13857
  }
13694
13858
  //#endregion
13859
+ //#region src/chat/auth-rows.ts
13860
+ /**
13861
+ * Stored credentials, i.e. the ones a user can actually choose between.
13862
+ *
13863
+ * `env` is excluded: it is observed rather than stored, and is usually just the
13864
+ * mirror `applyApiKeyEnv` wrote from the stored API key, so counting it would
13865
+ * advertise a choice that does not exist.
13866
+ */
13867
+ function selectableSources(provider) {
13868
+ return provider.methods.filter((m) => m.source !== "env");
13869
+ }
13870
+ /** A provider earns a chevron only when there is a real choice to reveal. */
13871
+ function isExpandableProvider(provider) {
13872
+ return selectableSources(provider).length > 1;
13873
+ }
13874
+ /**
13875
+ * Flatten the provider list plus the expanded set into the visible-row list.
13876
+ *
13877
+ * - `providers` — filtered / ordered provider list (post-search).
13878
+ * - `expanded` — keys of providers whose sources are open. Keys for
13879
+ * providers that are no longer expandable are ignored rather
13880
+ * than cleaned up, so stale expansion state (a credential
13881
+ * removed while the modal is open) can't desync the cursor.
13882
+ */
13883
+ function buildVisibleAuthRows(providers, expanded) {
13884
+ const out = [];
13885
+ for (const provider of providers) {
13886
+ const expandable = isExpandableProvider(provider);
13887
+ out.push({
13888
+ kind: "provider",
13889
+ provider,
13890
+ expandable
13891
+ });
13892
+ if (!expandable || !expanded.has(provider.key)) continue;
13893
+ for (const method of selectableSources(provider)) out.push({
13894
+ kind: "source",
13895
+ providerKey: provider.key,
13896
+ method
13897
+ });
13898
+ if (provider.envKey) {
13899
+ const envMethod = provider.methods.find((m) => m.source === "env");
13900
+ out.push({
13901
+ kind: "source",
13902
+ providerKey: provider.key,
13903
+ method: envMethod ?? {
13904
+ source: "env",
13905
+ detail: provider.envKey,
13906
+ active: false
13907
+ }
13908
+ });
13909
+ }
13910
+ }
13911
+ return out;
13912
+ }
13913
+ /**
13914
+ * Provider key a row "belongs to" — its own for a provider header, the parent's
13915
+ * for a source row.
13916
+ */
13917
+ function parentProviderKey(row) {
13918
+ return row.kind === "provider" ? row.provider.key : row.providerKey;
13919
+ }
13920
+ /**
13921
+ * Index in `rows` of the header for the given provider key, or `-1`.
13922
+ *
13923
+ * Collapse handlers snap the cursor up to the parent BEFORE the row list
13924
+ * shrinks; without that the cursor keeps its flat index and silently lands on
13925
+ * whichever sibling row slid into that slot.
13926
+ */
13927
+ function indexOfProviderRow(rows, providerKey) {
13928
+ return rows.findIndex((r) => r.kind === "provider" && r.provider.key === providerKey);
13929
+ }
13930
+ //#endregion
13695
13931
  //#region src/chat/contribution-origin.ts
13696
13932
  /**
13697
13933
  * Union the three skill sources into the list the Skills tab shows, in the
@@ -15132,6 +15368,17 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15132
15368
  const [expandedMcps, setExpandedMcps] = useState(() => /* @__PURE__ */ new Set());
15133
15369
  const [expandedExtensions, setExpandedExtensions] = useState(() => /* @__PURE__ */ new Set());
15134
15370
  const [expandedInstructions, setExpandedInstructions] = useState(() => /* @__PURE__ */ new Set());
15371
+ const [expandedProviders, setExpandedProviders] = useState(() => /* @__PURE__ */ new Set());
15372
+ /**
15373
+ * Provider list after an in-modal credential swap.
15374
+ *
15375
+ * `authentication.providers` is a snapshot frozen when the modal opened, so
15376
+ * promoting a credential would leave the ● marker pointing at the old winner
15377
+ * until the user closed and re-opened — which reads as "nothing happened".
15378
+ * The host returns freshly-detected providers from the action and they land
15379
+ * here, so the swap is visible in place.
15380
+ */
15381
+ const [providersAfterSwap, setProvidersAfterSwap] = useState(null);
15135
15382
  const [instructionsNotice, setInstructionsNotice] = useState(null);
15136
15383
  const [instructionSegments, setInstructionSegments] = useState([]);
15137
15384
  const [instructionsLoading, setInstructionsLoading] = useState(false);
@@ -15496,7 +15743,11 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15496
15743
  expandedMcps,
15497
15744
  mcpToolsByServer
15498
15745
  ]);
15499
- const filteredProviders = useMemo(() => (authentication?.providers ?? []).filter((p) => matchesTerms(providerCorpus(p), queryTerms)), [authentication?.providers, queryTerms]);
15746
+ const filteredProviders = useMemo(() => (providersAfterSwap ?? authentication?.providers ?? []).filter((p) => matchesTerms(providerCorpus(p), queryTerms)), [
15747
+ providersAfterSwap,
15748
+ authentication?.providers,
15749
+ queryTerms
15750
+ ]);
15500
15751
  const instructionVisibleRows = useMemo(() => buildVisibleInstructionRows({
15501
15752
  segments: instructionSegments,
15502
15753
  expanded: expandedInstructions,
@@ -15508,7 +15759,8 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15508
15759
  queryActive,
15509
15760
  queryTerms
15510
15761
  ]);
15511
- const authRowCount = filteredProviders.length;
15762
+ const authVisibleRows = useMemo(() => buildVisibleAuthRows(filteredProviders, expandedProviders), [filteredProviders, expandedProviders]);
15763
+ const authRowCount = authVisibleRows.length;
15512
15764
  const filteredSize = {
15513
15765
  ...Object.fromEntries(SETTINGS_CATEGORIES.map((c) => [c.id, filteredByCategory[c.id].length])),
15514
15766
  skills: filteredSkills.length,
@@ -15622,6 +15874,27 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15622
15874
  });
15623
15875
  }, [instructionVisibleRows]);
15624
15876
  const toggleExpandFocusedInstructionRow = useCallback(() => toggleExpandInstructionRowAt(cursor), [toggleExpandInstructionRowAt, cursor]);
15877
+ const toggleExpandProviderRowAt = useCallback((index) => {
15878
+ const row = authVisibleRows[index];
15879
+ if (!row) return;
15880
+ if (row.kind === "provider" && !row.expandable) return;
15881
+ const providerKey = parentProviderKey(row);
15882
+ setExpandedProviders((prev) => {
15883
+ const next = new Set(prev);
15884
+ if (next.has(providerKey)) {
15885
+ if (row.kind === "source") {
15886
+ const parentIndex = indexOfProviderRow(authVisibleRows, providerKey);
15887
+ if (parentIndex >= 0) setCursorByTab((c) => ({
15888
+ ...c,
15889
+ authentication: parentIndex
15890
+ }));
15891
+ }
15892
+ next.delete(providerKey);
15893
+ } else next.add(providerKey);
15894
+ return next;
15895
+ });
15896
+ }, [authVisibleRows]);
15897
+ const toggleExpandFocusedProviderRow = useCallback(() => toggleExpandProviderRowAt(cursor), [toggleExpandProviderRowAt, cursor]);
15625
15898
  const mcpRowCount = mcpVisibleRows.length;
15626
15899
  const extensionRowCount = extensionVisibleRows.length;
15627
15900
  const instructionRowCount = instructionVisibleRows.length;
@@ -15806,8 +16079,17 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15806
16079
  return;
15807
16080
  }
15808
16081
  if (activeTab === "authentication") {
15809
- const provider = filteredProviders[index];
15810
- if (provider) actions?.onConfigureProvider?.(provider);
16082
+ const row = authVisibleRows[index];
16083
+ if (!row) return;
16084
+ if (row.kind === "provider") {
16085
+ actions?.onConfigureProvider?.(row.provider);
16086
+ return;
16087
+ }
16088
+ if (row.method.source === "env" || row.method.active) return;
16089
+ const provider = filteredProviders.find((p) => p.key === row.providerKey);
16090
+ if (!provider) return;
16091
+ const refreshed = actions?.onSetPreferredCredential?.(provider, row.method.source);
16092
+ if (refreshed) setProvidersAfterSwap(refreshed);
15811
16093
  }
15812
16094
  }, [
15813
16095
  activeTab,
@@ -15828,6 +16110,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15828
16110
  mcpToolToggleMap,
15829
16111
  actions,
15830
16112
  filteredProviders,
16113
+ authVisibleRows,
15831
16114
  instructionVisibleRows,
15832
16115
  instructionsCanOverride,
15833
16116
  instructionsOverrideBlockedBy
@@ -15975,6 +16258,10 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15975
16258
  toggleExpandFocusedInstructionRow();
15976
16259
  return;
15977
16260
  }
16261
+ if (activeTab === "authentication" && key.name === "tab") {
16262
+ toggleExpandFocusedProviderRow();
16263
+ return;
16264
+ }
15978
16265
  if (key.name === "return") {
15979
16266
  activateRowAt(cursor);
15980
16267
  return;
@@ -16359,6 +16646,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
16359
16646
  activeTab === "authentication" && authentication && /* @__PURE__ */ jsx$1(AuthenticationList, {
16360
16647
  view: authentication,
16361
16648
  providers: filteredProviders,
16649
+ rows: authVisibleRows,
16362
16650
  cursor,
16363
16651
  highlightBg: SURFACE.selection,
16364
16652
  query
@@ -17761,7 +18049,7 @@ function LogsList({ buffer, filePath, minLevel, query, scrollboxRef }) {
17761
18049
  ]
17762
18050
  });
17763
18051
  }
17764
- function AuthenticationList({ view, providers, cursor, highlightBg, query }) {
18052
+ function AuthenticationList({ view, providers, rows, cursor, highlightBg, query }) {
17765
18053
  const COLOR = useColors();
17766
18054
  const home = homedir();
17767
18055
  const totalProviders = view.providers?.length ?? 0;
@@ -17842,10 +18130,51 @@ function AuthenticationList({ view, providers, cursor, highlightBg, query }) {
17842
18130
  })
17843
18131
  }),
17844
18132
  totalProviders > 0 && providers.length === 0 && /* @__PURE__ */ jsx$1(EmptyRow, { label: `no providers match "${query}"` }),
17845
- providers.map((p, i) => {
17846
- const isCurrent = view.currentProviderKey === p.key;
18133
+ rows.map((row, i) => {
17847
18134
  const focused = i === cursor;
17848
18135
  const bg = focused ? highlightBg : void 0;
18136
+ if (row.kind === "source") {
18137
+ const { method } = row;
18138
+ const isEnv = method.source === "env";
18139
+ const dotColor = isEnv ? COLOR.model : method.active ? COLOR.accent : COLOR.mute;
18140
+ return /* @__PURE__ */ jsx$1("box", {
18141
+ id: anchorIdFor(i),
18142
+ style: {
18143
+ flexShrink: 0,
18144
+ paddingLeft: 1,
18145
+ paddingRight: 1,
18146
+ backgroundColor: bg
18147
+ },
18148
+ children: /* @__PURE__ */ jsxs("text", {
18149
+ wrapMode: "none",
18150
+ children: [
18151
+ /* @__PURE__ */ jsx$1("span", {
18152
+ fg: focused ? COLOR.brand : COLOR.mute,
18153
+ children: focused ? " ▶ " : " "
18154
+ }),
18155
+ /* @__PURE__ */ jsx$1("span", {
18156
+ fg: dotColor,
18157
+ children: isEnv || method.active ? "● " : "○ "
18158
+ }),
18159
+ /* @__PURE__ */ jsx$1("span", {
18160
+ fg: isEnv ? COLOR.model : method.active ? COLOR.accent : focused ? COLOR.brand : COLOR.dim,
18161
+ children: method.source
18162
+ }),
18163
+ /* @__PURE__ */ jsx$1("span", {
18164
+ fg: COLOR.mute,
18165
+ children: ` ${displayPath$1(method.detail, home)}`
18166
+ }),
18167
+ method.active && /* @__PURE__ */ jsx$1("span", {
18168
+ fg: COLOR.mute,
18169
+ children: " (in use)"
18170
+ })
18171
+ ]
18172
+ })
18173
+ }, `${i}:${row.providerKey}:${method.source}`);
18174
+ }
18175
+ const p = row.provider;
18176
+ const expanded = row.expandable && rows[i + 1]?.kind === "source";
18177
+ const isCurrent = view.currentProviderKey === p.key;
17849
18178
  const marker = focused ? "▶ " : isCurrent ? "▸ " : " ";
17850
18179
  const labelColor = focused || isCurrent ? COLOR.brand : p.available ? COLOR.dim : COLOR.mute;
17851
18180
  return /* @__PURE__ */ jsxs("box", {
@@ -17875,9 +18204,17 @@ function AuthenticationList({ view, providers, cursor, highlightBg, query }) {
17875
18204
  isCurrent && /* @__PURE__ */ jsx$1("span", {
17876
18205
  fg: COLOR.mute,
17877
18206
  children: " (active)"
18207
+ }),
18208
+ row.expandable && /* @__PURE__ */ jsx$1("span", {
18209
+ fg: COLOR.mute,
18210
+ children: ` ${expanded ? "▾" : "▸"} ${selectableSources(p).length} sources`
18211
+ }),
18212
+ row.expandable && focused && /* @__PURE__ */ jsx$1("span", {
18213
+ fg: COLOR.dim,
18214
+ children: " tab"
17878
18215
  })
17879
18216
  ]
17880
- }), p.methods.length > 0 && /* @__PURE__ */ jsx$1("text", {
18217
+ }), p.methods.length > 0 && !expanded && /* @__PURE__ */ jsx$1("text", {
17881
18218
  wrapMode: "none",
17882
18219
  fg: COLOR.mute,
17883
18220
  children: ` ${p.methods.map((m) => `${m.source}: ${displayPath$1(m.detail, home)}`).join(" · ")}`
@@ -18496,7 +18833,109 @@ function restoreTerminalTitle(out = process.stdout) {
18496
18833
  } catch {}
18497
18834
  }
18498
18835
  //#endregion
18836
+ //#region src/chat/todo-edits.ts
18837
+ /**
18838
+ * Prefix for ids minted here, distinguishing user-authored rows from the
18839
+ * model's own ids in a session file. Purely diagnostic — nothing branches on
18840
+ * it — but it makes "who added this" answerable after the fact.
18841
+ */
18842
+ const USER_TODO_ID_PREFIX = "user-";
18843
+ /**
18844
+ * Mint an id that cannot collide with an existing row.
18845
+ *
18846
+ * Counter-based rather than random: a stable, readable id keeps session diffs
18847
+ * legible, and the collision check makes it safe against ids the model already
18848
+ * used (including previous `user-*` ones after a reload).
18849
+ */
18850
+ function nextTodoId(items) {
18851
+ const taken = new Set(items.map((i) => i.id));
18852
+ for (let n = 1;; n++) {
18853
+ const id = `${USER_TODO_ID_PREFIX}${n}`;
18854
+ if (!taken.has(id)) return id;
18855
+ }
18856
+ }
18857
+ /**
18858
+ * Append a todo. Returns the list unchanged when `content` is blank — an empty
18859
+ * row is never what the user meant, and rejecting it here means every caller
18860
+ * (keyboard, click, future GUI) gets the same guard.
18861
+ */
18862
+ function addTodo(items, content) {
18863
+ const trimmed = content.trim();
18864
+ if (!trimmed) return [...items];
18865
+ return [...items, {
18866
+ id: nextTodoId(items),
18867
+ content: trimmed,
18868
+ status: "pending"
18869
+ }];
18870
+ }
18871
+ /**
18872
+ * Replace one todo's text. Blank content is rejected (see {@link addTodo}); an
18873
+ * unknown id is a no-op rather than an error, because the list can be replaced
18874
+ * by a `todowrite` between render and keypress.
18875
+ */
18876
+ function editTodo(items, id, content) {
18877
+ const trimmed = content.trim();
18878
+ if (!trimmed) return [...items];
18879
+ return items.map((item) => item.id === id ? {
18880
+ ...item,
18881
+ content: trimmed
18882
+ } : item);
18883
+ }
18884
+ /** Drop one todo. Unknown id is a no-op, same rationale as {@link editTodo}. */
18885
+ function removeTodo(items, id) {
18886
+ return items.filter((item) => item.id !== id);
18887
+ }
18888
+ /** Drop everything. Separate from `removeTodo` so the caller's intent is explicit at the call site. */
18889
+ function clearTodos() {
18890
+ return [];
18891
+ }
18892
+ /**
18893
+ * Advance a todo's status one step: pending → in_progress → completed →
18894
+ * pending. `cancelled` also lands on `pending`, so a dropped item can be
18895
+ * revived without a dedicated key.
18896
+ *
18897
+ * Cycling INTO `in_progress` demotes any other in-progress row to `pending`:
18898
+ * the list's whole purpose is "what is being worked on right now", and two
18899
+ * live rows make that unanswerable — `TodoIndicator`'s successor
18900
+ * (`todoProgressLabel`) reads the first one and would silently ignore the rest.
18901
+ */
18902
+ function cycleTodoStatus(items, id) {
18903
+ const target = items.find((item) => item.id === id);
18904
+ if (!target) return [...items];
18905
+ const next = nextStatus(target.status);
18906
+ return items.map((item) => {
18907
+ if (item.id === id) return {
18908
+ ...item,
18909
+ status: next
18910
+ };
18911
+ if (next === "in_progress" && item.status === "in_progress") return {
18912
+ ...item,
18913
+ status: "pending"
18914
+ };
18915
+ return item;
18916
+ });
18917
+ }
18918
+ function nextStatus(status) {
18919
+ switch (status) {
18920
+ case "pending": return "in_progress";
18921
+ case "in_progress": return "completed";
18922
+ default: return "pending";
18923
+ }
18924
+ }
18925
+ //#endregion
18499
18926
  //#region src/tui/todos-modal.tsx
18927
+ /**
18928
+ * Steering text for a "retry this todo" request.
18929
+ *
18930
+ * Quotes the content so a model reading it can match the row verbatim against
18931
+ * its own list, and names the status because "retry" means different things
18932
+ * for a row it believes is `completed` versus one it abandoned as `cancelled`.
18933
+ * Pure + exported so the wording is pinned by a test rather than living only
18934
+ * inside a JSX callback.
18935
+ */
18936
+ function buildTodoRetryMessage(item) {
18937
+ return `Retry this todo (currently ${item.status}): "${item.content}"`;
18938
+ }
18500
18939
  /** Floor on the modal height — keeps the header + at least a few rows visible on tiny terminals. */
18501
18940
  const MIN_MODAL_HEIGHT = 12;
18502
18941
  /** Ceiling on the modal height — prevents a giant list from blanket-filling a tall window. */
@@ -18534,11 +18973,16 @@ function statusColors(status, COLOR) {
18534
18973
  };
18535
18974
  }
18536
18975
  }
18537
- function TodoRow({ item, rowId }) {
18976
+ function TodoRow({ item, rowId, focused, onClick }) {
18538
18977
  const COLOR = useColors();
18539
18978
  const colors = statusColors(item.status, COLOR);
18540
18979
  return /* @__PURE__ */ jsx$1("box", {
18541
18980
  id: rowId,
18981
+ onMouseDown: onClick && ((event) => {
18982
+ event.stopPropagation();
18983
+ event.preventDefault();
18984
+ onClick();
18985
+ }),
18542
18986
  style: {
18543
18987
  flexShrink: 0,
18544
18988
  flexDirection: "row"
@@ -18546,6 +18990,10 @@ function TodoRow({ item, rowId }) {
18546
18990
  children: /* @__PURE__ */ jsxs("text", {
18547
18991
  wrapMode: "word",
18548
18992
  children: [
18993
+ /* @__PURE__ */ jsx$1("span", {
18994
+ fg: focused ? COLOR.brand : COLOR.mute,
18995
+ children: focused ? "▶ " : " "
18996
+ }),
18549
18997
  /* @__PURE__ */ jsx$1("span", {
18550
18998
  fg: colors.glyph,
18551
18999
  children: TODO_STATUS_GLYPHS[item.status]
@@ -18562,10 +19010,14 @@ function TodoRow({ item, rowId }) {
18562
19010
  })
18563
19011
  });
18564
19012
  }
18565
- function TodosModal({ session, agent }) {
19013
+ function TodosModal({ session, agent, onSteer }) {
18566
19014
  const COLOR = useColors();
19015
+ const modal = useModal();
18567
19016
  const { height: termHeight } = useTerminalDimensions();
18568
19017
  const [, setTick] = useState(0);
19018
+ const [cursorRaw, setCursorRaw] = useState(0);
19019
+ const [mode, setMode] = useState("list");
19020
+ const [draft, setDraft] = useState("");
18569
19021
  useEffect(() => {
18570
19022
  if (!agent) return;
18571
19023
  return agent.hooks.hook("tool:after", (ctx) => {
@@ -18588,46 +19040,213 @@ function TodosModal({ session, agent }) {
18588
19040
  const maxHeight = Math.max(MIN_MODAL_HEIGHT, Math.min(MAX_MODAL_HEIGHT$1, idealHeight));
18589
19041
  const display = state.todos.length > 0 ? state.todos : state.archive;
18590
19042
  const total = display.length;
18591
- return /* @__PURE__ */ jsx$1(Modal, {
19043
+ const cursor = total === 0 ? 0 : Math.min(cursorRaw, total - 1);
19044
+ const focusedItem = display[cursor] ?? null;
19045
+ /**
19046
+ * Write a transformed list back to the run.
19047
+ *
19048
+ * Editing while the ARCHIVE is on screen promotes it back to the live slot,
19049
+ * which is the only coherent option: `setTodosForRun` owns the live slot, and
19050
+ * an edit the user just made must be the list that's displayed next.
19051
+ */
19052
+ const commit = useCallback((next) => {
19053
+ if (!session || !state.runId) return;
19054
+ setTodosForRun(session, state.runId, next, { clearArchive: true });
19055
+ setTick((t) => t + 1);
19056
+ }, [session, state.runId]);
19057
+ const editable = session !== null && state.runId !== null;
19058
+ /** Enter add / edit mode, seeding the draft from the focused row for edits. */
19059
+ const beginEdit = useCallback((next) => {
19060
+ setDraft(next === "edit" ? focusedItem?.content ?? "" : "");
19061
+ setMode(next);
19062
+ }, [focusedItem]);
19063
+ useKeyboard((key) => {
19064
+ if (!editable) return;
19065
+ if (mode === "add" || mode === "edit") {
19066
+ if (key.name === "escape") {
19067
+ setDraft("");
19068
+ setMode("list");
19069
+ }
19070
+ return;
19071
+ }
19072
+ if (mode === "confirm-clear") {
19073
+ if (key.name === "y") {
19074
+ commit(clearTodos());
19075
+ setMode("list");
19076
+ } else if (key.name === "n" || key.name === "escape") setMode("list");
19077
+ return;
19078
+ }
19079
+ if (key.name === "up") {
19080
+ setCursorRaw((c) => Math.max(0, c - 1));
19081
+ return;
19082
+ }
19083
+ if (key.name === "down") {
19084
+ setCursorRaw((c) => Math.min(Math.max(0, total - 1), c + 1));
19085
+ return;
19086
+ }
19087
+ if (key.name === "a") {
19088
+ beginEdit("add");
19089
+ return;
19090
+ }
19091
+ if (key.ctrl && key.name === "d") {
19092
+ if (total > 0) setMode("confirm-clear");
19093
+ return;
19094
+ }
19095
+ if (key.name === "d") {
19096
+ if (focusedItem) commit(removeTodo(display, focusedItem.id));
19097
+ return;
19098
+ }
19099
+ if (key.name === "e" || key.name === "return") {
19100
+ if (focusedItem) beginEdit("edit");
19101
+ return;
19102
+ }
19103
+ if (key.name === "space") {
19104
+ if (focusedItem) commit(cycleTodoStatus(display, focusedItem.id));
19105
+ return;
19106
+ }
19107
+ if (key.name === "r") {
19108
+ if (focusedItem && onSteer) {
19109
+ onSteer(buildTodoRetryMessage(focusedItem));
19110
+ modal.close();
19111
+ }
19112
+ }
19113
+ });
19114
+ const submitInput = useCallback(() => {
19115
+ const value = draft.trim();
19116
+ if (value) {
19117
+ if (mode === "add") commit(addTodo(display, value));
19118
+ else if (mode === "edit" && focusedItem) commit(editTodo(display, focusedItem.id, value));
19119
+ }
19120
+ setDraft("");
19121
+ setMode("list");
19122
+ }, [
19123
+ draft,
19124
+ mode,
19125
+ commit,
19126
+ display,
19127
+ focusedItem
19128
+ ]);
19129
+ return /* @__PURE__ */ jsxs(Modal, {
18592
19130
  title: "todos",
18593
19131
  bottomTitle: total > 0 ? `${total} item${total === 1 ? "" : "s"} · esc close` : "esc close",
18594
19132
  rightTitle: display.length > 0 ? /* @__PURE__ */ jsx$1(CountsBadge, { items: display }) : null,
18595
19133
  maxWidth: 100,
18596
19134
  maxHeight,
18597
- children: total === 0 ? /* @__PURE__ */ jsxs("text", { children: [
18598
- /* @__PURE__ */ jsx$1("span", {
18599
- fg: COLOR.mute,
18600
- children: "(no todos yet — the agent hasn't called "
19135
+ disableEscape: mode !== "list",
19136
+ children: [
19137
+ total === 0 ? /* @__PURE__ */ jsxs("text", { children: [
19138
+ /* @__PURE__ */ jsx$1("span", {
19139
+ fg: COLOR.mute,
19140
+ children: "(no todos yet — the agent hasn't called "
19141
+ }),
19142
+ /* @__PURE__ */ jsx$1("span", {
19143
+ fg: COLOR.warn,
19144
+ children: "todowrite"
19145
+ }),
19146
+ /* @__PURE__ */ jsx$1("span", {
19147
+ fg: COLOR.mute,
19148
+ children: ")"
19149
+ })
19150
+ ] }) : /* @__PURE__ */ jsx$1("box", {
19151
+ style: {
19152
+ flexDirection: "column",
19153
+ flexGrow: 1,
19154
+ flexShrink: 1,
19155
+ overflow: "hidden"
19156
+ },
19157
+ children: /* @__PURE__ */ jsx$1("scrollbox", {
19158
+ ref: scrollRef,
19159
+ focusable: false,
19160
+ stickyScroll: false,
19161
+ style: {
19162
+ flexGrow: 1,
19163
+ flexShrink: 1
19164
+ },
19165
+ children: display.map((item, i) => /* @__PURE__ */ jsx$1(TodoRow, {
19166
+ item,
19167
+ rowId: `todo-row-${item.id}`,
19168
+ focused: editable && i === cursor,
19169
+ onClick: editable ? () => setCursorRaw(i) : void 0
19170
+ }, item.id))
19171
+ })
18601
19172
  }),
18602
- /* @__PURE__ */ jsx$1("span", {
18603
- fg: COLOR.warn,
18604
- children: "todowrite"
19173
+ editable && (mode === "add" || mode === "edit") && /* @__PURE__ */ jsxs("box", {
19174
+ style: {
19175
+ flexShrink: 0,
19176
+ flexDirection: "row",
19177
+ paddingTop: 1
19178
+ },
19179
+ children: [/* @__PURE__ */ jsx$1("text", {
19180
+ wrapMode: "none",
19181
+ children: /* @__PURE__ */ jsx$1("span", {
19182
+ fg: COLOR.warn,
19183
+ children: mode === "add" ? "+ " : "✎ "
19184
+ })
19185
+ }), /* @__PURE__ */ jsx$1("input", {
19186
+ focused: true,
19187
+ value: draft,
19188
+ placeholder: mode === "add" ? "new todo…" : "edit todo…",
19189
+ onInput: setDraft,
19190
+ onSubmit: () => submitInput(),
19191
+ style: { flexGrow: 1 }
19192
+ })]
18605
19193
  }),
18606
- /* @__PURE__ */ jsx$1("span", {
18607
- fg: COLOR.mute,
18608
- children: ")"
18609
- })
18610
- ] }) : /* @__PURE__ */ jsx$1("box", {
18611
- style: {
18612
- flexDirection: "column",
18613
- flexGrow: 1,
18614
- flexShrink: 1,
18615
- overflow: "hidden"
18616
- },
18617
- children: /* @__PURE__ */ jsx$1("scrollbox", {
18618
- ref: scrollRef,
18619
- focusable: false,
18620
- stickyScroll: false,
19194
+ editable && mode === "confirm-clear" && /* @__PURE__ */ jsxs("box", {
18621
19195
  style: {
18622
- flexGrow: 1,
18623
- flexShrink: 1
19196
+ flexShrink: 0,
19197
+ flexDirection: "column",
19198
+ paddingTop: 1
18624
19199
  },
18625
- children: display.map((item) => /* @__PURE__ */ jsx$1(TodoRow, {
18626
- item,
18627
- rowId: `todo-row-${item.id}`
18628
- }, item.id))
19200
+ children: [/* @__PURE__ */ jsx$1("text", {
19201
+ wrapMode: "none",
19202
+ children: /* @__PURE__ */ jsx$1("span", {
19203
+ fg: COLOR.error,
19204
+ children: `delete all ${total} todo${total === 1 ? "" : "s"}?`
19205
+ })
19206
+ }), /* @__PURE__ */ jsx$1(ModalHints, { hints: [{
19207
+ key: "y",
19208
+ label: "yes",
19209
+ keyColor: COLOR.error
19210
+ }, {
19211
+ key: "n",
19212
+ label: "cancel"
19213
+ }] })]
19214
+ }),
19215
+ editable && mode === "list" && /* @__PURE__ */ jsx$1("box", {
19216
+ style: {
19217
+ flexShrink: 0,
19218
+ flexDirection: "column",
19219
+ paddingTop: 1
19220
+ },
19221
+ children: /* @__PURE__ */ jsx$1(ModalHints, { hints: [
19222
+ {
19223
+ key: "a",
19224
+ label: "add"
19225
+ },
19226
+ total > 0 && {
19227
+ key: "e",
19228
+ label: "edit"
19229
+ },
19230
+ total > 0 && {
19231
+ key: "space",
19232
+ label: "status"
19233
+ },
19234
+ total > 0 && {
19235
+ key: "d",
19236
+ label: "remove"
19237
+ },
19238
+ total > 0 && onSteer !== void 0 && {
19239
+ key: "r",
19240
+ label: "retry"
19241
+ },
19242
+ total > 0 && {
19243
+ key: "ctrl+d",
19244
+ label: "delete all",
19245
+ keyColor: COLOR.error
19246
+ }
19247
+ ] })
18629
19248
  })
18630
- })
19249
+ ]
18631
19250
  });
18632
19251
  }
18633
19252
  /**
@@ -19560,10 +20179,19 @@ function AppShell() {
19560
20179
  useEffect(() => {
19561
20180
  persistToolResultsRef.current = settings.persistToolResults;
19562
20181
  }, [settings.persistToolResults]);
19563
- const allowInteractionRef = useRef(settings.allowInteraction);
20182
+ const interactionModeRef = useRef(settings.interactionMode);
19564
20183
  useEffect(() => {
19565
- allowInteractionRef.current = settings.allowInteraction;
19566
- }, [settings.allowInteraction]);
20184
+ interactionModeRef.current = settings.interactionMode;
20185
+ }, [settings.interactionMode]);
20186
+ useEffect(() => {
20187
+ if (settings.interactionMode !== "skip" || !pendingInteraction) return;
20188
+ if (!isModelInteraction(pendingInteraction)) return;
20189
+ interactions.resolveHead(createAutomaticInteractionResponse(pendingInteraction));
20190
+ }, [
20191
+ interactions,
20192
+ pendingInteraction,
20193
+ settings.interactionMode
20194
+ ]);
19567
20195
  const userInstructionsScopeRef = useRef(settings.userInstructionsScope);
19568
20196
  useEffect(() => {
19569
20197
  userInstructionsScopeRef.current = settings.userInstructionsScope;
@@ -20470,7 +21098,7 @@ function AppShell() {
20470
21098
  const profile = pickedAgentRef.current;
20471
21099
  if (profile.id !== "build" && profile.id !== "plan") return;
20472
21100
  return agent.hooks.hook("system:transform", (ctx) => {
20473
- const allowInteraction = allowInteractionRef.current !== false;
21101
+ const allowInteraction = interactionModeRef.current !== "disabled";
20474
21102
  const envOpts = {
20475
21103
  cwd,
20476
21104
  ...projectDir !== cwd ? { projectRoot: projectDir } : {},
@@ -20814,8 +21442,11 @@ function AppShell() {
20814
21442
  enabled: enabledMcpsRef.current,
20815
21443
  ...disabledMcpToolsRef.current ? { disabledTools: disabledMcpToolsRef.current } : {}
20816
21444
  }), ...pluginContributions.mcpServers];
20817
- const allowInteraction = allowInteractionRef.current !== false;
20818
- const interactionTools = allowInteraction ? createInteractionTools({ requestInteraction: makeRequestInteraction(interactions) }) : {};
21445
+ const allowInteraction = interactionModeRef.current !== "disabled";
21446
+ const interactionTools = allowInteraction ? createInteractionTools({
21447
+ requestInteraction: makeRequestInteraction(interactions),
21448
+ shouldSkipInteractions: () => interactionModeRef.current === "skip"
21449
+ }) : {};
20819
21450
  const actualCwd = cwdRef.current;
20820
21451
  const { agentsMd, systemFile, system: builtInSystem } = composeHostContext(profile, allowInteraction);
20821
21452
  sessionInstructionFilesRef.current = agentsMd.files;
@@ -21716,6 +22347,35 @@ function AppShell() {
21716
22347
  interactions
21717
22348
  ]);
21718
22349
  /**
22350
+ * Persist the matched `tool_result` turn for a resolved batch WITHOUT
22351
+ * running. Automatic mode uses this on activation so opening an old
22352
+ * session repairs its history (every `tool_use` matched) instead of
22353
+ * silently starting a billed run nobody asked for; the next prompt
22354
+ * continues normally.
22355
+ */
22356
+ const persistResumedInteractions = useCallback(async (requests, responses) => {
22357
+ const session = sessionRef.current;
22358
+ if (!session) {
22359
+ debugLog("resume-interaction: missing session", { count: requests.length });
22360
+ return false;
22361
+ }
22362
+ const turnId = await session.generateTurnId();
22363
+ const runId = requests[0]?.runId;
22364
+ let turn;
22365
+ try {
22366
+ turn = buildResumedToolResultsTurn(requests, responses, {
22367
+ turnId,
22368
+ runId
22369
+ });
22370
+ } catch (err) {
22371
+ debugLog("resume-interaction: turn build failed", err);
22372
+ return false;
22373
+ }
22374
+ await session.appendTurns([turn]);
22375
+ if (sessionRef.current === session) setEvents(capTranscriptEvents(eventsFromTurns(session.turns, session.runs)));
22376
+ return true;
22377
+ }, []);
22378
+ /**
21719
22379
  * Continue a session from where pending interactions left it. Appends
21720
22380
  * a single user turn containing one `tool_result` per pending request
21721
22381
  * (in pending order) and triggers a prompt-less `agent.run()` so the
@@ -21830,7 +22490,12 @@ function AppShell() {
21830
22490
  const pending = pendingInteractionsFromTurns(session.turns);
21831
22491
  if (pending.length > 0) {
21832
22492
  const collected = /* @__PURE__ */ new Map();
22493
+ const automatic = interactionModeRef.current === "skip";
21833
22494
  for (const request of pending) {
22495
+ if (automatic) {
22496
+ collected.set(request.id, createAutomaticInteractionResponse(request));
22497
+ continue;
22498
+ }
21834
22499
  const entry = {
21835
22500
  request,
21836
22501
  resolve: (response) => {
@@ -21841,6 +22506,7 @@ function AppShell() {
21841
22506
  };
21842
22507
  interactions.enqueue(entry);
21843
22508
  }
22509
+ if (automatic) persistResumedInteractions(pending, collected).catch((err) => debugLog("resume-interaction: automatic persist failed", err));
21844
22510
  }
21845
22511
  }, [
21846
22512
  teardown,
@@ -21850,6 +22516,7 @@ function AppShell() {
21850
22516
  projectDir,
21851
22517
  interactions,
21852
22518
  continueResumedInteractions,
22519
+ persistResumedInteractions,
21853
22520
  extensionRegistry
21854
22521
  ]);
21855
22522
  /** Ref mirror — same rationale as `refreshSessionsRef` above. */
@@ -22890,7 +23557,8 @@ function AppShell() {
22890
23557
  interaction: {
22891
23558
  confirm: async (prompt, confirmOpts) => {
22892
23559
  const response = await requestCommandInteraction({
22893
- id: `command_${crypto.randomUUID()}`,
23560
+ id: `${HOST_INTERACTION_ID_PREFIX}${crypto.randomUUID()}`,
23561
+ origin: "host",
22894
23562
  kind: "question",
22895
23563
  tool: ASK_USER_TOOL,
22896
23564
  createdAt: Date.now(),
@@ -22910,7 +23578,8 @@ function AppShell() {
22910
23578
  },
22911
23579
  askUser: async (form) => {
22912
23580
  const response = await requestCommandInteraction({
22913
- id: `command_${crypto.randomUUID()}`,
23581
+ id: `${HOST_INTERACTION_ID_PREFIX}${crypto.randomUUID()}`,
23582
+ origin: "host",
22914
23583
  kind: "question",
22915
23584
  tool: ASK_USER_TOOL,
22916
23585
  createdAt: Date.now(),
@@ -22921,7 +23590,8 @@ function AppShell() {
22921
23590
  },
22922
23591
  presentPlan: async (plan) => {
22923
23592
  const response = await requestCommandInteraction({
22924
- id: `command_${crypto.randomUUID()}`,
23593
+ id: `${HOST_INTERACTION_ID_PREFIX}${crypto.randomUUID()}`,
23594
+ origin: "host",
22925
23595
  kind: "plan",
22926
23596
  tool: PRESENT_PLAN_TOOL,
22927
23597
  createdAt: Date.now(),
@@ -23485,6 +24155,50 @@ function AppShell() {
23485
24155
  if (staleWarnLog) await staleWarnLog.close().catch((err) => debugLog("rebuildActiveAgent: warnLog.close failed", err));
23486
24156
  }, []);
23487
24157
  rebuildActiveAgentRef.current = rebuildActiveAgent;
24158
+ /**
24159
+ * Promote one of a provider's stored credentials — Settings → Authentication
24160
+ * → `↵` on a source row, for a provider holding both an API key and an OAuth
24161
+ * grant.
24162
+ *
24163
+ * The preference IS the storage layout (the canonical file key holds the
24164
+ * winner), so persisting it is the entire change — there is no separate
24165
+ * precedence setting to keep in sync. `applyApiKeyEnv` then re-reconciles
24166
+ * `process.env`, which is the step that actually makes it take effect: a
24167
+ * promoted API key has to be injected, and a promoted OAuth grant has to
24168
+ * CLEAR the ambient var so the refresh path runs instead of a stale token.
24169
+ *
24170
+ * Returns freshly-detected providers instead of closing the modal. The
24171
+ * settings modal's provider list is frozen at open time, so an earlier
24172
+ * version closed the modal to avoid showing a stale active marker — which
24173
+ * made a SUCCESSFUL swap look like the key did nothing, since the panel
24174
+ * vanished at the moment of success. Handing detection back lets the modal
24175
+ * re-render the marker in place and stay put.
24176
+ *
24177
+ * Gated like `onConfigureProvider` so a mid-run credential swap can't race
24178
+ * the agent.
24179
+ */
24180
+ const onSetPreferredCredential = useMemo(() => {
24181
+ if (busy || pendingApproval) return void 0;
24182
+ return (provider, source) => {
24183
+ const descriptor = providerRegistry[provider.key];
24184
+ if (!descriptor) return void 0;
24185
+ setPreferredCredential(config.paths.userDir, descriptor, source);
24186
+ applyApiKeyEnv(config.paths.userDir, providerRegistry, { log: (message) => getTuiLogging()?.sink.emit({
24187
+ level: "debug",
24188
+ timestamp: Date.now(),
24189
+ message: message.trimEnd(),
24190
+ attrs: { scope: "chat" }
24191
+ }) });
24192
+ if (provider.key === pickedRef.current?.provider.key) rebuildActiveAgent().catch((err) => debugLog("onSetPreferredCredential: rebuild failed", err));
24193
+ return detectAuth(config.paths.userDir, providerRegistry);
24194
+ };
24195
+ }, [
24196
+ busy,
24197
+ pendingApproval,
24198
+ config.paths.userDir,
24199
+ providerRegistry,
24200
+ rebuildActiveAgent
24201
+ ]);
23488
24202
  useEffect(() => {
23489
24203
  const prev = mcpRuntimeKeyRef.current;
23490
24204
  if (prev === null) {
@@ -23651,7 +24365,7 @@ function AppShell() {
23651
24365
  })();
23652
24366
  }, [modal, config.paths.userDir]);
23653
24367
  const onReadContextSegments = useCallback(async () => {
23654
- const { shadowedSystemFiles, supportsOverride } = composeHostContext(pickedAgentRef.current, allowInteractionRef.current !== false);
24368
+ const { shadowedSystemFiles, supportsOverride } = composeHostContext(pickedAgentRef.current, interactionModeRef.current !== "disabled");
23655
24369
  const shadowedSystemPath = shadowedSystemFiles[0]?.path;
23656
24370
  const scopeLoadsUserOverride = userScopeOverridesLoad(userInstructionsScopeRef.current);
23657
24371
  const canOverrideSystem = supportsOverride && scopeLoadsUserOverride;
@@ -23668,7 +24382,7 @@ function AppShell() {
23668
24382
  ...overrideBlockedBy ? { overrideBlockedBy } : {},
23669
24383
  ...shadowedSystemPath ? { shadowedSystemPath } : {}
23670
24384
  };
23671
- const { agentsMd, systemFile, system } = composeHostContext(pickedAgentRef.current, allowInteractionRef.current !== false);
24385
+ const { agentsMd, systemFile, system } = composeHostContext(pickedAgentRef.current, interactionModeRef.current !== "disabled");
23672
24386
  const enabled = enabledSkillsRef.current;
23673
24387
  const sessionSkills = enabled === void 0 ? [...skillsCatalogRef.current] : skillsCatalogRef.current.filter((s) => enabled.includes(s.name));
23674
24388
  return {
@@ -23705,7 +24419,7 @@ function AppShell() {
23705
24419
  const onCreateSystemPrompt = useCallback(async () => {
23706
24420
  const path = userSystemMdPath({ prefix: config.prefix });
23707
24421
  if (!existsSync(path)) {
23708
- const { doctrineOnlySystem, supportsOverride } = composeHostContext(pickedAgentRef.current, allowInteractionRef.current !== false);
24422
+ const { doctrineOnlySystem, supportsOverride } = composeHostContext(pickedAgentRef.current, interactionModeRef.current !== "disabled");
23709
24423
  if (!supportsOverride) throw new Error(`the "${pickedAgentRef.current.id}" profile supplies its own system prompt, so a SYSTEM.md override would not apply`);
23710
24424
  if (!userScopeOverridesLoad(userInstructionsScopeRef.current)) throw new Error(`userInstructionsScope is "${userInstructionsScopeRef.current}", so a SYSTEM.md in your home directory would not be loaded`);
23711
24425
  const doctrine = splitSystemPrompt(doctrineOnlySystem() ?? "").static;
@@ -24425,6 +25139,7 @@ function AppShell() {
24425
25139
  },
24426
25140
  actions: {
24427
25141
  onConfigureProvider,
25142
+ onSetPreferredCredential,
24428
25143
  onOpenKeybindings: onOpenKeybindingsFile,
24429
25144
  getContextSegments: onReadContextSegments,
24430
25145
  onOpenInstructionsFile,
@@ -24507,7 +25222,8 @@ function AppShell() {
24507
25222
  if (matchesBinding(key, keybindings.openTodos) && screen === "chat" && currentSession) {
24508
25223
  modal.open(/* @__PURE__ */ jsx$1(TodosModal, {
24509
25224
  session: sessionRef.current,
24510
- agent: agentRef.current
25225
+ agent: agentRef.current,
25226
+ onSteer: (message) => onSubmitPrompt(message, [], [])
24511
25227
  }));
24512
25228
  return;
24513
25229
  }
@@ -24695,6 +25411,7 @@ function AppShell() {
24695
25411
  COLOR.warn,
24696
25412
  COLOR.dim
24697
25413
  ]);
25414
+ const todoProgress = todoProgressLabel(useActiveTodos(sessionRef.current));
24698
25415
  const hints = useMemo(() => {
24699
25416
  const base = buildHints({
24700
25417
  screen,
@@ -24720,6 +25437,7 @@ function AppShell() {
24720
25437
  inFlightToolCount: inFlightTools.reduce((n, entry) => entry.childId === void 0 ? n + 1 : n, 0) + backgroundTasks.reduce((n, entry) => entry.childId === void 0 ? n + 1 : n, 0),
24721
25438
  activeSkillCount: activeSkillNames.size,
24722
25439
  skillsChipColor: COLOR.brand,
25440
+ todoProgress: settings.showTodoIndicator ? todoProgress : null,
24723
25441
  updateHint,
24724
25442
  cacheBreakHint,
24725
25443
  sessionsScopeAll: sessionsShowAll
@@ -24741,6 +25459,8 @@ function AppShell() {
24741
25459
  currentSession,
24742
25460
  hasMultipleAgents,
24743
25461
  settings.uiMode,
25462
+ settings.showTodoIndicator,
25463
+ todoProgress,
24744
25464
  picked,
24745
25465
  pickedAgent,
24746
25466
  COLOR,
@@ -24762,7 +25482,8 @@ function AppShell() {
24762
25482
  start: r.start,
24763
25483
  end: r.end,
24764
25484
  providerId: r.providerId
24765
- }))
25485
+ })),
25486
+ attachments: m.attachments.length
24766
25487
  })), [messageQueue]);
24767
25488
  const queueShortcuts = useMemo(() => ({
24768
25489
  enter: keybindings.enterQueueSelection,
@@ -24853,7 +25574,6 @@ function AppShell() {
24853
25574
  settings,
24854
25575
  onSubmit: onSubmitPrompt,
24855
25576
  session: currentSession,
24856
- liveSession: sessionRef.current,
24857
25577
  pending: pendingApproval,
24858
25578
  onApproval: resolveHead,
24859
25579
  pendingInteraction,