zidane 6.5.3 → 6.5.5

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 (137) hide show
  1. package/dist/{acp-CIpD8UIw.js → acp-BPJbXS2J.js} +6 -6
  2. package/dist/{acp-CIpD8UIw.js.map → acp-BPJbXS2J.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-gZjHsUft.js} +176 -166
  7. package/dist/agent-gZjHsUft.js.map +1 -0
  8. package/dist/agent.d.ts +1 -1
  9. package/dist/agent.js +1 -1
  10. package/dist/{anthropic-_Hn1YiWr.js → anthropic-D3le2d-s.js} +75 -14
  11. package/dist/anthropic-D3le2d-s.js.map +1 -0
  12. package/dist/{auth-CS_e8ng1.js → auth-DCiXCwGa.js} +8 -6
  13. package/dist/{auth-CS_e8ng1.js.map → auth-DCiXCwGa.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 +29 -8
  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/cursor-iojRXHF9.js +139 -0
  27. package/dist/cursor-iojRXHF9.js.map +1 -0
  28. package/dist/{errors-BwBxu3zD.js → errors-DKiMrOKq.js} +61 -2
  29. package/dist/{errors-BwBxu3zD.js.map → errors-DKiMrOKq.js.map} +1 -1
  30. package/dist/errors.js +2 -2
  31. package/dist/eval.d.ts +1 -1
  32. package/dist/eval.js +1 -1
  33. package/dist/extensions.d.ts +1 -1
  34. package/dist/extensions.js +1 -1
  35. package/dist/{fetch-url-B7aSg6Bp.js → fetch-url-ma-5_p0p.js} +2 -2
  36. package/dist/{fetch-url-B7aSg6Bp.js.map → fetch-url-ma-5_p0p.js.map} +1 -1
  37. package/dist/headless.d.ts +1 -1
  38. package/dist/headless.js +3 -3
  39. package/dist/{index-BKyWhILQ.d.ts → index-BYKzzYzi.d.ts} +85 -22
  40. package/dist/index-BYKzzYzi.d.ts.map +1 -0
  41. package/dist/index.d.ts +2 -2
  42. package/dist/index.js +14 -14
  43. package/dist/{login-D3CKhgbI.js → login-CB23LyI1.js} +3 -3
  44. package/dist/{login-D3CKhgbI.js.map → login-CB23LyI1.js.map} +1 -1
  45. package/dist/{mcp-D9fqOQo5.js → mcp-CVtSQTWg.js} +3 -3
  46. package/dist/{mcp-D9fqOQo5.js.map → mcp-CVtSQTWg.js.map} +1 -1
  47. package/dist/mcp.d.ts +1 -1
  48. package/dist/mcp.js +1 -1
  49. package/dist/{messages-C_H9soi5.js → messages-CVVg5jUL.js} +2 -2
  50. package/dist/{messages-C_H9soi5.js.map → messages-CVVg5jUL.js.map} +1 -1
  51. package/dist/{oauth-BYjlFR-J.js → oauth-C4GvY_p1.js} +50 -11
  52. package/dist/oauth-C4GvY_p1.js.map +1 -0
  53. package/dist/{openai-compat-CnHeo4Lz.js → openai-compat-hmZzpDrB.js} +2 -2
  54. package/dist/{openai-compat-CnHeo4Lz.js.map → openai-compat-hmZzpDrB.js.map} +1 -1
  55. package/dist/output/stream-json.d.ts +1 -1
  56. package/dist/output/terminal.d.ts +1 -1
  57. package/dist/pkce-t8BcPyEi.js +30 -0
  58. package/dist/pkce-t8BcPyEi.js.map +1 -0
  59. package/dist/{plugins-2PMTVcp-.js → plugins-onDvTHEm.js} +2 -2
  60. package/dist/{plugins-2PMTVcp-.js.map → plugins-onDvTHEm.js.map} +1 -1
  61. package/dist/plugins.d.ts +1 -1
  62. package/dist/plugins.js +1 -1
  63. package/dist/presets.d.ts +1 -1
  64. package/dist/presets.js +1 -1
  65. package/dist/providers/anthropic.d.ts +1 -1
  66. package/dist/providers/anthropic.js +1 -1
  67. package/dist/providers/arcee.d.ts +1 -1
  68. package/dist/providers/arcee.js +1 -1
  69. package/dist/providers/baseten.d.ts +1 -1
  70. package/dist/providers/baseten.js +1 -1
  71. package/dist/providers/cerebras.d.ts +1 -1
  72. package/dist/providers/cerebras.js +1 -1
  73. package/dist/providers/local.d.ts +1 -1
  74. package/dist/providers/local.js +1 -1
  75. package/dist/providers/openai-compat.d.ts +1 -1
  76. package/dist/providers/openai-compat.js +1 -1
  77. package/dist/providers/openai.d.ts +1 -1
  78. package/dist/providers/openai.js +4 -4
  79. package/dist/providers/openrouter.d.ts +1 -1
  80. package/dist/providers/openrouter.js +1 -1
  81. package/dist/providers/xai.d.ts +1 -1
  82. package/dist/providers/xai.js +3 -3
  83. package/dist/providers-Cf0AXIIZ.js +137 -0
  84. package/dist/providers-Cf0AXIIZ.js.map +1 -0
  85. package/dist/providers.d.ts +2 -2
  86. package/dist/providers.js +5 -5
  87. package/dist/restate.d.ts +1 -1
  88. package/dist/restate.js +2 -2
  89. package/dist/session/sqlite.d.ts +1 -1
  90. package/dist/session/sqlite.js +1 -1
  91. package/dist/{session-VpLrNaFQ.js → session-cqN5NbO-.js} +2 -2
  92. package/dist/{session-VpLrNaFQ.js.map → session-cqN5NbO-.js.map} +1 -1
  93. package/dist/session.d.ts +1 -1
  94. package/dist/session.js +2 -2
  95. package/dist/{skills-Cp9vnBwd.js → skills-BMGH_KK1.js} +2 -2
  96. package/dist/{skills-Cp9vnBwd.js.map → skills-BMGH_KK1.js.map} +1 -1
  97. package/dist/skills.d.ts +1 -1
  98. package/dist/skills.js +1 -1
  99. package/dist/{timeout-BDetbuN6.js → timeout-CjOG2mea.js} +29 -2
  100. package/dist/timeout-CjOG2mea.js.map +1 -0
  101. package/dist/timeout-DEaqslLO.d.ts.map +1 -1
  102. package/dist/{tool-formatters-DMr62Hpy.d.ts → tool-formatters-B5WSHMCU.d.ts} +2 -2
  103. package/dist/{tool-formatters-DMr62Hpy.d.ts.map → tool-formatters-B5WSHMCU.d.ts.map} +1 -1
  104. package/dist/tools/fetch-url.d.ts +1 -1
  105. package/dist/tools/fetch-url.js +1 -1
  106. package/dist/tools/web-search.d.ts +1 -1
  107. package/dist/tools/web-search.js +2 -2
  108. package/dist/tools.d.ts +1 -1
  109. package/dist/tools.js +1 -1
  110. package/dist/{transcript-anchors-BHihk9nf.d.ts → transcript-anchors-BPnWHLyr.d.ts} +2 -2
  111. package/dist/{transcript-anchors-BHihk9nf.d.ts.map → transcript-anchors-BPnWHLyr.d.ts.map} +1 -1
  112. package/dist/{transcript-anchors-uU_KZLsp.js → transcript-anchors-BvC8t7dt.js} +177 -63
  113. package/dist/transcript-anchors-BvC8t7dt.js.map +1 -0
  114. package/dist/tui.d.ts +18 -29
  115. package/dist/tui.d.ts.map +1 -1
  116. package/dist/tui.js +737 -236
  117. package/dist/tui.js.map +1 -1
  118. package/dist/{turn-operations-iGu5J08j.js → turn-operations-7_Ff7INF.js} +2 -2
  119. package/dist/{turn-operations-iGu5J08j.js.map → turn-operations-7_Ff7INF.js.map} +1 -1
  120. package/dist/{turn-operations-BgqVUjtl.d.ts → turn-operations-BvP3_HsU.d.ts} +3 -3
  121. package/dist/{turn-operations-BgqVUjtl.d.ts.map → turn-operations-BvP3_HsU.d.ts.map} +1 -1
  122. package/dist/types.d.ts +1 -1
  123. package/dist/types.js +1 -1
  124. package/dist/{xai-CfmI5Y5k.js → xai-DILdFGzN.js} +3 -29
  125. package/dist/xai-DILdFGzN.js.map +1 -0
  126. package/docs/CHAT.md +1 -1
  127. package/docs/TUI.md +4 -4
  128. package/package.json +2 -1
  129. package/dist/agent-8kfGIDb_.js.map +0 -1
  130. package/dist/anthropic-_Hn1YiWr.js.map +0 -1
  131. package/dist/index-BKyWhILQ.d.ts.map +0 -1
  132. package/dist/oauth-BYjlFR-J.js.map +0 -1
  133. package/dist/providers-VypZh7rD.js +0 -264
  134. package/dist/providers-VypZh7rD.js.map +0 -1
  135. package/dist/timeout-BDetbuN6.js.map +0 -1
  136. package/dist/transcript-anchors-uU_KZLsp.js.map +0 -1
  137. package/dist/xai-CfmI5Y5k.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-hmZzpDrB.js";
2
+ import { t as buildContextSegments } from "./context-segments-CJCtJ9sU.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-gZjHsUft.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-cqN5NbO-.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-CS_e8ng1.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 { 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-DCiXCwGa.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-uU_KZLsp.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-BvC8t7dt.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";
@@ -11738,7 +11836,7 @@ const EMPTY_QUEUED_MESSAGES = [];
11738
11836
  function shouldRenderTranscriptSearch(searchMode, query) {
11739
11837
  return searchMode && query.trim().length > 0;
11740
11838
  }
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 }) {
11839
+ 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
11840
  const COLOR = useColors();
11743
11841
  const SURFACE = useSurfaces();
11744
11842
  const titleText = session?.title ?? "New session";
@@ -11806,6 +11904,7 @@ function ChatScreen({ cwd, events, busy, compacting = false, queuedMessages = EM
11806
11904
  }, [onPopupOpenChange]);
11807
11905
  const fileEditPending = pending && isFileEditTool(pending.tool) ? pending : null;
11808
11906
  const planPending = !pending && pendingInteraction?.kind === "plan" ? pendingInteraction : null;
11907
+ const approvalPending = pending && !fileEditPending ? pending : null;
11809
11908
  const modal = useModal();
11810
11909
  useEffect(() => {
11811
11910
  if (!fileEditPending) return;
@@ -11861,47 +11960,58 @@ function ChatScreen({ cwd, events, busy, compacting = false, queuedMessages = EM
11861
11960
  request: fileEditPending,
11862
11961
  onDecide: onApproval
11863
11962
  }, fileEditPending.id),
11864
- /* @__PURE__ */ jsx$1("box", {
11963
+ /* @__PURE__ */ jsxs("box", {
11865
11964
  style: {
11866
11965
  flexDirection: "column",
11867
11966
  flexShrink: 0,
11868
11967
  backgroundColor: SURFACE.background
11869
11968
  },
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
11969
+ children: [
11970
+ approvalPending && /* @__PURE__ */ jsx$1(ApprovalBlock, {
11971
+ request: approvalPending,
11972
+ onPick: onApproval
11881
11973
  }),
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
11974
+ pendingInteraction && !approvalPending && /* @__PURE__ */ jsx$1(InteractionBlock, {
11975
+ request: pendingInteraction,
11976
+ onResolve: onInteraction
11977
+ }, pendingInteraction.id),
11978
+ /* @__PURE__ */ jsxs("box", {
11979
+ style: {
11980
+ flexDirection: "column",
11981
+ flexShrink: 0,
11982
+ visible: approvalPending == null && pendingInteraction == null
11983
+ },
11984
+ children: [
11985
+ queuedMessages.length > 0 && !completionPopupOpen && /* @__PURE__ */ jsx$1(QueuedMessagesBlock, {
11986
+ messages: queuedMessages,
11987
+ selectionIndex: queueSelectionIndex,
11988
+ shortcuts: queueShortcuts
11989
+ }),
11990
+ /* @__PURE__ */ jsx$1(ExtensionBanners, {}),
11991
+ /* @__PURE__ */ jsx$1(PromptBlock, {
11992
+ userPrompts,
11993
+ onSubmit,
11994
+ completionProviders,
11995
+ inlineAutocompleteProviders,
11996
+ onCompletionTab,
11997
+ enterSelectsCompletion,
11998
+ onPopupOpenChange: handlePopupOpenChange,
11999
+ searchMode: transcriptSearchMode,
12000
+ onSearchModeChange: onTranscriptSearchModeChange,
12001
+ onSearchQueryChange: onTranscriptSearchQueryChange,
12002
+ onSearchNavigate: onTranscriptSearchNavigate,
12003
+ onSearchOpen: onTranscriptSearchOpen,
12004
+ selectMode: queueSelectionIndex != null ? "queue" : null,
12005
+ triggerHints: promptTriggerHints,
12006
+ busy,
12007
+ onEnterQueueFromEmpty: onEnterQueueFromEmptyPrompt,
12008
+ attachImageBinding,
12009
+ editorBinding,
12010
+ suspended: approvalPending != null || pendingInteraction != null
12011
+ })
12012
+ ]
11903
12013
  })
11904
- ] })
12014
+ ]
11905
12015
  }),
11906
12016
  !planPending && !fileEditPending && /* @__PURE__ */ jsx$1(TitleOverlay, {
11907
12017
  title: titleText,
@@ -12071,9 +12181,23 @@ function queueRowId(index) {
12071
12181
  * (where committed messages live) and the prompt (where the user is
12072
12182
  * typing the next one). Dropped entirely when the queue is empty.
12073
12183
  */
12184
+ /**
12185
+ * Badge text for a queued message's attachment count, or `null` when there is
12186
+ * nothing to show.
12187
+ *
12188
+ * Split out from the row so the logic — pluralization, and the distinction
12189
+ * between "no attachments" and "this preview predates the field" — is testable
12190
+ * without standing up `ChatScreen`'s provider stack. Padded with spaces because
12191
+ * it renders as a filled chip and needs breathing room inside its background.
12192
+ */
12193
+ function attachmentBadgeLabel(count) {
12194
+ if (count === void 0 || count <= 0) return null;
12195
+ return ` +${count} ${count === 1 ? "attachment" : "attachments"} `;
12196
+ }
12074
12197
  function QueuedMessagesBlock({ messages, selectionIndex, shortcuts }) {
12075
12198
  const COLOR = useColors();
12076
12199
  const SURFACE = useSurfaces();
12200
+ const attachmentChip = resolveChipColor(SURFACE.chips, "file");
12077
12201
  const scrollboxRef = useRef(null);
12078
12202
  const enterHintKey = shortcuts?.enter ? formatBindingForDisplay(shortcuts.enter) : "↑";
12079
12203
  useEffect(() => {
@@ -12136,6 +12260,11 @@ function QueuedMessagesBlock({ messages, selectionIndex, shortcuts }) {
12136
12260
  fg: COLOR.mute,
12137
12261
  children: "❯ "
12138
12262
  }),
12263
+ attachmentBadgeLabel(msg.attachments) !== null && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx$1("span", {
12264
+ fg: attachmentChip.fg,
12265
+ bg: attachmentChip.bg,
12266
+ children: attachmentBadgeLabel(msg.attachments)
12267
+ }), /* @__PURE__ */ jsx$1("span", { children: " " })] }),
12139
12268
  /* @__PURE__ */ jsx$1("span", {
12140
12269
  fg: focused ? COLOR.brand : COLOR.dim,
12141
12270
  children: previewText(msg.text)
@@ -12225,7 +12354,7 @@ const INLINE_GHOST_DEBOUNCE_MS = 120;
12225
12354
  function isTranscriptSearchExitKey(event, cursorOffset) {
12226
12355
  return event.name === "escape" || (event.name === "backspace" || event.name === "delete") && !event.ctrl && !event.meta && !event.shift && cursorOffset === 0;
12227
12356
  }
12228
- function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocompleteProviders, onCompletionTab, enterSelectsCompletion, onPopupOpenChange, searchMode = false, onSearchModeChange, onSearchQueryChange, onSearchNavigate, onSearchOpen, selectMode = null, triggerHints, busy = false, onEnterQueueFromEmpty, attachImageBinding, editorBinding }) {
12357
+ 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
12358
  const focused = useModalAwareFocus();
12230
12359
  const renderer = useRenderer();
12231
12360
  const COLOR = useColors();
@@ -12488,30 +12617,12 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
12488
12617
  syncBuffer();
12489
12618
  return true;
12490
12619
  }, [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();
12620
+ /** Apply decoded clipboard text through the shared file/attachment/inline policy. */
12621
+ const applyTextPaste = useCallback((text) => {
12508
12622
  const ta = textareaRef.current;
12509
12623
  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
- }
12624
+ const normalized = stripAnsiSequences(text).replace(/\r\n?/g, "\n");
12625
+ if (normalized.length === 0) return;
12515
12626
  if (attachmentsEnabled && !normalized.includes("\n")) {
12516
12627
  const trimmed = normalized.trim().replace(/^["']|["']$/g, "");
12517
12628
  let resolved = trimmed.startsWith("file://") ? decodeURIComponent(trimmed.slice(7)) : trimmed;
@@ -12535,15 +12646,12 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
12535
12646
  }
12536
12647
  } catch {}
12537
12648
  }
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
- }]);
12649
+ const pastePlan = planTextPaste(normalized, attachmentsEnabled);
12650
+ if (pastePlan.kind === "attachment") {
12651
+ setAttachments((prev) => [...prev, pastePlan.attachment]);
12545
12652
  return;
12546
12653
  }
12654
+ const lines = pastePlan.text.trim().split("\n");
12547
12655
  for (let i = 0; i < lines.length; i++) {
12548
12656
  if (i > 0) ta.newLine();
12549
12657
  const part = lines[i];
@@ -12551,9 +12659,64 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
12551
12659
  }
12552
12660
  syncBuffer();
12553
12661
  }, [
12554
- syncBuffer,
12555
12662
  attachmentsEnabled,
12556
- attachClipboardImage,
12663
+ showNotice,
12664
+ syncBuffer
12665
+ ]);
12666
+ /**
12667
+ * Prevent the textarea's one-shot insert and explicitly preserve multiline
12668
+ * paste. Empty terminal paste remains the image-clipboard probe.
12669
+ */
12670
+ const onPaste = useCallback((event) => {
12671
+ event.preventDefault();
12672
+ const raw = decodePasteBytes(event.bytes);
12673
+ if (stripAnsiSequences(raw).replace(/\r\n?/g, "\n").length === 0) {
12674
+ if (attachmentsEnabled) attachClipboardImage();
12675
+ return;
12676
+ }
12677
+ applyTextPaste(raw);
12678
+ }, [
12679
+ applyTextPaste,
12680
+ attachmentsEnabled,
12681
+ attachClipboardImage
12682
+ ]);
12683
+ /**
12684
+ * Read the OS clipboard on the app-owned keybinding. Unlike terminal paste,
12685
+ * this moves only the shortcut bytes through the PTY, avoiding large-paste
12686
+ * backpressure bugs in integrated terminals. Text is re-injected through the
12687
+ * exact same paste handler as bracketed paste; images retain the existing
12688
+ * attachment behavior.
12689
+ */
12690
+ const pasteClipboard = useCallback(async () => {
12691
+ const result = await clipboardReader();
12692
+ if (!result.ok) {
12693
+ showNotice(result.reason === "empty" ? "clipboard has no text or image" : result.detail ?? "could not read the clipboard");
12694
+ return;
12695
+ }
12696
+ if (result.kind === "text") {
12697
+ applyTextPaste(result.text);
12698
+ return;
12699
+ }
12700
+ if (!attachmentsEnabled) {
12701
+ showNotice("attachments are disabled — enable them in Settings → Agent");
12702
+ return;
12703
+ }
12704
+ if (result.data.length > MAX_IMAGE_BYTES) {
12705
+ showNotice(`clipboard image too large (${formatBytes(result.data.length)}) — max ${formatBytes(MAX_IMAGE_BYTES)}`);
12706
+ return;
12707
+ }
12708
+ setAttachments((prev) => {
12709
+ const n = prev.filter((a) => a.name.startsWith("clipboard-")).length + 1;
12710
+ return [...prev, {
12711
+ name: `clipboard-${n}.${extForImageMediaType(result.mediaType)}`,
12712
+ content: result.data,
12713
+ mediaType: result.mediaType
12714
+ }];
12715
+ });
12716
+ }, [
12717
+ applyTextPaste,
12718
+ attachmentsEnabled,
12719
+ clipboardReader,
12557
12720
  showNotice
12558
12721
  ]);
12559
12722
  /**
@@ -12620,7 +12783,7 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
12620
12783
  const onKeyDown = useCallback((event) => {
12621
12784
  if (attachImageBinding && matchesBinding(event, attachImageBinding)) {
12622
12785
  event.preventDefault();
12623
- attachClipboardImage();
12786
+ pasteClipboard();
12624
12787
  return;
12625
12788
  }
12626
12789
  if (editorBinding && matchesBinding(event, editorBinding)) {
@@ -12752,7 +12915,7 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
12752
12915
  enterSelectsCompletion,
12753
12916
  syncBuffer,
12754
12917
  attachImageBinding,
12755
- attachClipboardImage,
12918
+ pasteClipboard,
12756
12919
  editorBinding,
12757
12920
  openExternalEditor,
12758
12921
  attachments,
@@ -12848,7 +13011,7 @@ function PromptBlock({ userPrompts, onSubmit, completionProviders, inlineAutocom
12848
13011
  },
12849
13012
  children: [/* @__PURE__ */ jsx$1("textarea", {
12850
13013
  ref: textareaRef,
12851
- focused: focused && !selectMode,
13014
+ focused: focused && !selectMode && !suspended,
12852
13015
  keyBindings: TEXTAREA_BINDINGS,
12853
13016
  wrapMode: "word",
12854
13017
  placeholder: selectMode === "queue" ? "— queue-select mode — press ⎋ to resume typing —" : searchMode ? "Search this conversation…" : shellMode ? "Run a shell command…" : "Ask zidane…",
@@ -18496,7 +18659,109 @@ function restoreTerminalTitle(out = process.stdout) {
18496
18659
  } catch {}
18497
18660
  }
18498
18661
  //#endregion
18662
+ //#region src/chat/todo-edits.ts
18663
+ /**
18664
+ * Prefix for ids minted here, distinguishing user-authored rows from the
18665
+ * model's own ids in a session file. Purely diagnostic — nothing branches on
18666
+ * it — but it makes "who added this" answerable after the fact.
18667
+ */
18668
+ const USER_TODO_ID_PREFIX = "user-";
18669
+ /**
18670
+ * Mint an id that cannot collide with an existing row.
18671
+ *
18672
+ * Counter-based rather than random: a stable, readable id keeps session diffs
18673
+ * legible, and the collision check makes it safe against ids the model already
18674
+ * used (including previous `user-*` ones after a reload).
18675
+ */
18676
+ function nextTodoId(items) {
18677
+ const taken = new Set(items.map((i) => i.id));
18678
+ for (let n = 1;; n++) {
18679
+ const id = `${USER_TODO_ID_PREFIX}${n}`;
18680
+ if (!taken.has(id)) return id;
18681
+ }
18682
+ }
18683
+ /**
18684
+ * Append a todo. Returns the list unchanged when `content` is blank — an empty
18685
+ * row is never what the user meant, and rejecting it here means every caller
18686
+ * (keyboard, click, future GUI) gets the same guard.
18687
+ */
18688
+ function addTodo(items, content) {
18689
+ const trimmed = content.trim();
18690
+ if (!trimmed) return [...items];
18691
+ return [...items, {
18692
+ id: nextTodoId(items),
18693
+ content: trimmed,
18694
+ status: "pending"
18695
+ }];
18696
+ }
18697
+ /**
18698
+ * Replace one todo's text. Blank content is rejected (see {@link addTodo}); an
18699
+ * unknown id is a no-op rather than an error, because the list can be replaced
18700
+ * by a `todowrite` between render and keypress.
18701
+ */
18702
+ function editTodo(items, id, content) {
18703
+ const trimmed = content.trim();
18704
+ if (!trimmed) return [...items];
18705
+ return items.map((item) => item.id === id ? {
18706
+ ...item,
18707
+ content: trimmed
18708
+ } : item);
18709
+ }
18710
+ /** Drop one todo. Unknown id is a no-op, same rationale as {@link editTodo}. */
18711
+ function removeTodo(items, id) {
18712
+ return items.filter((item) => item.id !== id);
18713
+ }
18714
+ /** Drop everything. Separate from `removeTodo` so the caller's intent is explicit at the call site. */
18715
+ function clearTodos() {
18716
+ return [];
18717
+ }
18718
+ /**
18719
+ * Advance a todo's status one step: pending → in_progress → completed →
18720
+ * pending. `cancelled` also lands on `pending`, so a dropped item can be
18721
+ * revived without a dedicated key.
18722
+ *
18723
+ * Cycling INTO `in_progress` demotes any other in-progress row to `pending`:
18724
+ * the list's whole purpose is "what is being worked on right now", and two
18725
+ * live rows make that unanswerable — `TodoIndicator`'s successor
18726
+ * (`todoProgressLabel`) reads the first one and would silently ignore the rest.
18727
+ */
18728
+ function cycleTodoStatus(items, id) {
18729
+ const target = items.find((item) => item.id === id);
18730
+ if (!target) return [...items];
18731
+ const next = nextStatus(target.status);
18732
+ return items.map((item) => {
18733
+ if (item.id === id) return {
18734
+ ...item,
18735
+ status: next
18736
+ };
18737
+ if (next === "in_progress" && item.status === "in_progress") return {
18738
+ ...item,
18739
+ status: "pending"
18740
+ };
18741
+ return item;
18742
+ });
18743
+ }
18744
+ function nextStatus(status) {
18745
+ switch (status) {
18746
+ case "pending": return "in_progress";
18747
+ case "in_progress": return "completed";
18748
+ default: return "pending";
18749
+ }
18750
+ }
18751
+ //#endregion
18499
18752
  //#region src/tui/todos-modal.tsx
18753
+ /**
18754
+ * Steering text for a "retry this todo" request.
18755
+ *
18756
+ * Quotes the content so a model reading it can match the row verbatim against
18757
+ * its own list, and names the status because "retry" means different things
18758
+ * for a row it believes is `completed` versus one it abandoned as `cancelled`.
18759
+ * Pure + exported so the wording is pinned by a test rather than living only
18760
+ * inside a JSX callback.
18761
+ */
18762
+ function buildTodoRetryMessage(item) {
18763
+ return `Retry this todo (currently ${item.status}): "${item.content}"`;
18764
+ }
18500
18765
  /** Floor on the modal height — keeps the header + at least a few rows visible on tiny terminals. */
18501
18766
  const MIN_MODAL_HEIGHT = 12;
18502
18767
  /** Ceiling on the modal height — prevents a giant list from blanket-filling a tall window. */
@@ -18534,11 +18799,16 @@ function statusColors(status, COLOR) {
18534
18799
  };
18535
18800
  }
18536
18801
  }
18537
- function TodoRow({ item, rowId }) {
18802
+ function TodoRow({ item, rowId, focused, onClick }) {
18538
18803
  const COLOR = useColors();
18539
18804
  const colors = statusColors(item.status, COLOR);
18540
18805
  return /* @__PURE__ */ jsx$1("box", {
18541
18806
  id: rowId,
18807
+ onMouseDown: onClick && ((event) => {
18808
+ event.stopPropagation();
18809
+ event.preventDefault();
18810
+ onClick();
18811
+ }),
18542
18812
  style: {
18543
18813
  flexShrink: 0,
18544
18814
  flexDirection: "row"
@@ -18546,6 +18816,10 @@ function TodoRow({ item, rowId }) {
18546
18816
  children: /* @__PURE__ */ jsxs("text", {
18547
18817
  wrapMode: "word",
18548
18818
  children: [
18819
+ /* @__PURE__ */ jsx$1("span", {
18820
+ fg: focused ? COLOR.brand : COLOR.mute,
18821
+ children: focused ? "▶ " : " "
18822
+ }),
18549
18823
  /* @__PURE__ */ jsx$1("span", {
18550
18824
  fg: colors.glyph,
18551
18825
  children: TODO_STATUS_GLYPHS[item.status]
@@ -18562,10 +18836,14 @@ function TodoRow({ item, rowId }) {
18562
18836
  })
18563
18837
  });
18564
18838
  }
18565
- function TodosModal({ session, agent }) {
18839
+ function TodosModal({ session, agent, onSteer }) {
18566
18840
  const COLOR = useColors();
18841
+ const modal = useModal();
18567
18842
  const { height: termHeight } = useTerminalDimensions();
18568
18843
  const [, setTick] = useState(0);
18844
+ const [cursorRaw, setCursorRaw] = useState(0);
18845
+ const [mode, setMode] = useState("list");
18846
+ const [draft, setDraft] = useState("");
18569
18847
  useEffect(() => {
18570
18848
  if (!agent) return;
18571
18849
  return agent.hooks.hook("tool:after", (ctx) => {
@@ -18588,46 +18866,213 @@ function TodosModal({ session, agent }) {
18588
18866
  const maxHeight = Math.max(MIN_MODAL_HEIGHT, Math.min(MAX_MODAL_HEIGHT$1, idealHeight));
18589
18867
  const display = state.todos.length > 0 ? state.todos : state.archive;
18590
18868
  const total = display.length;
18591
- return /* @__PURE__ */ jsx$1(Modal, {
18869
+ const cursor = total === 0 ? 0 : Math.min(cursorRaw, total - 1);
18870
+ const focusedItem = display[cursor] ?? null;
18871
+ /**
18872
+ * Write a transformed list back to the run.
18873
+ *
18874
+ * Editing while the ARCHIVE is on screen promotes it back to the live slot,
18875
+ * which is the only coherent option: `setTodosForRun` owns the live slot, and
18876
+ * an edit the user just made must be the list that's displayed next.
18877
+ */
18878
+ const commit = useCallback((next) => {
18879
+ if (!session || !state.runId) return;
18880
+ setTodosForRun(session, state.runId, next, { clearArchive: true });
18881
+ setTick((t) => t + 1);
18882
+ }, [session, state.runId]);
18883
+ const editable = session !== null && state.runId !== null;
18884
+ /** Enter add / edit mode, seeding the draft from the focused row for edits. */
18885
+ const beginEdit = useCallback((next) => {
18886
+ setDraft(next === "edit" ? focusedItem?.content ?? "" : "");
18887
+ setMode(next);
18888
+ }, [focusedItem]);
18889
+ useKeyboard((key) => {
18890
+ if (!editable) return;
18891
+ if (mode === "add" || mode === "edit") {
18892
+ if (key.name === "escape") {
18893
+ setDraft("");
18894
+ setMode("list");
18895
+ }
18896
+ return;
18897
+ }
18898
+ if (mode === "confirm-clear") {
18899
+ if (key.name === "y") {
18900
+ commit(clearTodos());
18901
+ setMode("list");
18902
+ } else if (key.name === "n" || key.name === "escape") setMode("list");
18903
+ return;
18904
+ }
18905
+ if (key.name === "up") {
18906
+ setCursorRaw((c) => Math.max(0, c - 1));
18907
+ return;
18908
+ }
18909
+ if (key.name === "down") {
18910
+ setCursorRaw((c) => Math.min(Math.max(0, total - 1), c + 1));
18911
+ return;
18912
+ }
18913
+ if (key.name === "a") {
18914
+ beginEdit("add");
18915
+ return;
18916
+ }
18917
+ if (key.ctrl && key.name === "d") {
18918
+ if (total > 0) setMode("confirm-clear");
18919
+ return;
18920
+ }
18921
+ if (key.name === "d") {
18922
+ if (focusedItem) commit(removeTodo(display, focusedItem.id));
18923
+ return;
18924
+ }
18925
+ if (key.name === "e" || key.name === "return") {
18926
+ if (focusedItem) beginEdit("edit");
18927
+ return;
18928
+ }
18929
+ if (key.name === "space") {
18930
+ if (focusedItem) commit(cycleTodoStatus(display, focusedItem.id));
18931
+ return;
18932
+ }
18933
+ if (key.name === "r") {
18934
+ if (focusedItem && onSteer) {
18935
+ onSteer(buildTodoRetryMessage(focusedItem));
18936
+ modal.close();
18937
+ }
18938
+ }
18939
+ });
18940
+ const submitInput = useCallback(() => {
18941
+ const value = draft.trim();
18942
+ if (value) {
18943
+ if (mode === "add") commit(addTodo(display, value));
18944
+ else if (mode === "edit" && focusedItem) commit(editTodo(display, focusedItem.id, value));
18945
+ }
18946
+ setDraft("");
18947
+ setMode("list");
18948
+ }, [
18949
+ draft,
18950
+ mode,
18951
+ commit,
18952
+ display,
18953
+ focusedItem
18954
+ ]);
18955
+ return /* @__PURE__ */ jsxs(Modal, {
18592
18956
  title: "todos",
18593
18957
  bottomTitle: total > 0 ? `${total} item${total === 1 ? "" : "s"} · esc close` : "esc close",
18594
18958
  rightTitle: display.length > 0 ? /* @__PURE__ */ jsx$1(CountsBadge, { items: display }) : null,
18595
18959
  maxWidth: 100,
18596
18960
  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 "
18961
+ disableEscape: mode !== "list",
18962
+ children: [
18963
+ total === 0 ? /* @__PURE__ */ jsxs("text", { children: [
18964
+ /* @__PURE__ */ jsx$1("span", {
18965
+ fg: COLOR.mute,
18966
+ children: "(no todos yet — the agent hasn't called "
18967
+ }),
18968
+ /* @__PURE__ */ jsx$1("span", {
18969
+ fg: COLOR.warn,
18970
+ children: "todowrite"
18971
+ }),
18972
+ /* @__PURE__ */ jsx$1("span", {
18973
+ fg: COLOR.mute,
18974
+ children: ")"
18975
+ })
18976
+ ] }) : /* @__PURE__ */ jsx$1("box", {
18977
+ style: {
18978
+ flexDirection: "column",
18979
+ flexGrow: 1,
18980
+ flexShrink: 1,
18981
+ overflow: "hidden"
18982
+ },
18983
+ children: /* @__PURE__ */ jsx$1("scrollbox", {
18984
+ ref: scrollRef,
18985
+ focusable: false,
18986
+ stickyScroll: false,
18987
+ style: {
18988
+ flexGrow: 1,
18989
+ flexShrink: 1
18990
+ },
18991
+ children: display.map((item, i) => /* @__PURE__ */ jsx$1(TodoRow, {
18992
+ item,
18993
+ rowId: `todo-row-${item.id}`,
18994
+ focused: editable && i === cursor,
18995
+ onClick: editable ? () => setCursorRaw(i) : void 0
18996
+ }, item.id))
18997
+ })
18601
18998
  }),
18602
- /* @__PURE__ */ jsx$1("span", {
18603
- fg: COLOR.warn,
18604
- children: "todowrite"
18999
+ editable && (mode === "add" || mode === "edit") && /* @__PURE__ */ jsxs("box", {
19000
+ style: {
19001
+ flexShrink: 0,
19002
+ flexDirection: "row",
19003
+ paddingTop: 1
19004
+ },
19005
+ children: [/* @__PURE__ */ jsx$1("text", {
19006
+ wrapMode: "none",
19007
+ children: /* @__PURE__ */ jsx$1("span", {
19008
+ fg: COLOR.warn,
19009
+ children: mode === "add" ? "+ " : "✎ "
19010
+ })
19011
+ }), /* @__PURE__ */ jsx$1("input", {
19012
+ focused: true,
19013
+ value: draft,
19014
+ placeholder: mode === "add" ? "new todo…" : "edit todo…",
19015
+ onInput: setDraft,
19016
+ onSubmit: () => submitInput(),
19017
+ style: { flexGrow: 1 }
19018
+ })]
18605
19019
  }),
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,
19020
+ editable && mode === "confirm-clear" && /* @__PURE__ */ jsxs("box", {
18621
19021
  style: {
18622
- flexGrow: 1,
18623
- flexShrink: 1
19022
+ flexShrink: 0,
19023
+ flexDirection: "column",
19024
+ paddingTop: 1
19025
+ },
19026
+ children: [/* @__PURE__ */ jsx$1("text", {
19027
+ wrapMode: "none",
19028
+ children: /* @__PURE__ */ jsx$1("span", {
19029
+ fg: COLOR.error,
19030
+ children: `delete all ${total} todo${total === 1 ? "" : "s"}?`
19031
+ })
19032
+ }), /* @__PURE__ */ jsx$1(ModalHints, { hints: [{
19033
+ key: "y",
19034
+ label: "yes",
19035
+ keyColor: COLOR.error
19036
+ }, {
19037
+ key: "n",
19038
+ label: "cancel"
19039
+ }] })]
19040
+ }),
19041
+ editable && mode === "list" && /* @__PURE__ */ jsx$1("box", {
19042
+ style: {
19043
+ flexShrink: 0,
19044
+ flexDirection: "column",
19045
+ paddingTop: 1
18624
19046
  },
18625
- children: display.map((item) => /* @__PURE__ */ jsx$1(TodoRow, {
18626
- item,
18627
- rowId: `todo-row-${item.id}`
18628
- }, item.id))
19047
+ children: /* @__PURE__ */ jsx$1(ModalHints, { hints: [
19048
+ {
19049
+ key: "a",
19050
+ label: "add"
19051
+ },
19052
+ total > 0 && {
19053
+ key: "e",
19054
+ label: "edit"
19055
+ },
19056
+ total > 0 && {
19057
+ key: "space",
19058
+ label: "status"
19059
+ },
19060
+ total > 0 && {
19061
+ key: "d",
19062
+ label: "remove"
19063
+ },
19064
+ total > 0 && onSteer !== void 0 && {
19065
+ key: "r",
19066
+ label: "retry"
19067
+ },
19068
+ total > 0 && {
19069
+ key: "ctrl+d",
19070
+ label: "delete all",
19071
+ keyColor: COLOR.error
19072
+ }
19073
+ ] })
18629
19074
  })
18630
- })
19075
+ ]
18631
19076
  });
18632
19077
  }
18633
19078
  /**
@@ -19560,10 +20005,19 @@ function AppShell() {
19560
20005
  useEffect(() => {
19561
20006
  persistToolResultsRef.current = settings.persistToolResults;
19562
20007
  }, [settings.persistToolResults]);
19563
- const allowInteractionRef = useRef(settings.allowInteraction);
20008
+ const interactionModeRef = useRef(settings.interactionMode);
19564
20009
  useEffect(() => {
19565
- allowInteractionRef.current = settings.allowInteraction;
19566
- }, [settings.allowInteraction]);
20010
+ interactionModeRef.current = settings.interactionMode;
20011
+ }, [settings.interactionMode]);
20012
+ useEffect(() => {
20013
+ if (settings.interactionMode !== "skip" || !pendingInteraction) return;
20014
+ if (!isModelInteraction(pendingInteraction)) return;
20015
+ interactions.resolveHead(createAutomaticInteractionResponse(pendingInteraction));
20016
+ }, [
20017
+ interactions,
20018
+ pendingInteraction,
20019
+ settings.interactionMode
20020
+ ]);
19567
20021
  const userInstructionsScopeRef = useRef(settings.userInstructionsScope);
19568
20022
  useEffect(() => {
19569
20023
  userInstructionsScopeRef.current = settings.userInstructionsScope;
@@ -20470,7 +20924,7 @@ function AppShell() {
20470
20924
  const profile = pickedAgentRef.current;
20471
20925
  if (profile.id !== "build" && profile.id !== "plan") return;
20472
20926
  return agent.hooks.hook("system:transform", (ctx) => {
20473
- const allowInteraction = allowInteractionRef.current !== false;
20927
+ const allowInteraction = interactionModeRef.current !== "disabled";
20474
20928
  const envOpts = {
20475
20929
  cwd,
20476
20930
  ...projectDir !== cwd ? { projectRoot: projectDir } : {},
@@ -20814,8 +21268,11 @@ function AppShell() {
20814
21268
  enabled: enabledMcpsRef.current,
20815
21269
  ...disabledMcpToolsRef.current ? { disabledTools: disabledMcpToolsRef.current } : {}
20816
21270
  }), ...pluginContributions.mcpServers];
20817
- const allowInteraction = allowInteractionRef.current !== false;
20818
- const interactionTools = allowInteraction ? createInteractionTools({ requestInteraction: makeRequestInteraction(interactions) }) : {};
21271
+ const allowInteraction = interactionModeRef.current !== "disabled";
21272
+ const interactionTools = allowInteraction ? createInteractionTools({
21273
+ requestInteraction: makeRequestInteraction(interactions),
21274
+ shouldSkipInteractions: () => interactionModeRef.current === "skip"
21275
+ }) : {};
20819
21276
  const actualCwd = cwdRef.current;
20820
21277
  const { agentsMd, systemFile, system: builtInSystem } = composeHostContext(profile, allowInteraction);
20821
21278
  sessionInstructionFilesRef.current = agentsMd.files;
@@ -21716,6 +22173,35 @@ function AppShell() {
21716
22173
  interactions
21717
22174
  ]);
21718
22175
  /**
22176
+ * Persist the matched `tool_result` turn for a resolved batch WITHOUT
22177
+ * running. Automatic mode uses this on activation so opening an old
22178
+ * session repairs its history (every `tool_use` matched) instead of
22179
+ * silently starting a billed run nobody asked for; the next prompt
22180
+ * continues normally.
22181
+ */
22182
+ const persistResumedInteractions = useCallback(async (requests, responses) => {
22183
+ const session = sessionRef.current;
22184
+ if (!session) {
22185
+ debugLog("resume-interaction: missing session", { count: requests.length });
22186
+ return false;
22187
+ }
22188
+ const turnId = await session.generateTurnId();
22189
+ const runId = requests[0]?.runId;
22190
+ let turn;
22191
+ try {
22192
+ turn = buildResumedToolResultsTurn(requests, responses, {
22193
+ turnId,
22194
+ runId
22195
+ });
22196
+ } catch (err) {
22197
+ debugLog("resume-interaction: turn build failed", err);
22198
+ return false;
22199
+ }
22200
+ await session.appendTurns([turn]);
22201
+ if (sessionRef.current === session) setEvents(capTranscriptEvents(eventsFromTurns(session.turns, session.runs)));
22202
+ return true;
22203
+ }, []);
22204
+ /**
21719
22205
  * Continue a session from where pending interactions left it. Appends
21720
22206
  * a single user turn containing one `tool_result` per pending request
21721
22207
  * (in pending order) and triggers a prompt-less `agent.run()` so the
@@ -21830,7 +22316,12 @@ function AppShell() {
21830
22316
  const pending = pendingInteractionsFromTurns(session.turns);
21831
22317
  if (pending.length > 0) {
21832
22318
  const collected = /* @__PURE__ */ new Map();
22319
+ const automatic = interactionModeRef.current === "skip";
21833
22320
  for (const request of pending) {
22321
+ if (automatic) {
22322
+ collected.set(request.id, createAutomaticInteractionResponse(request));
22323
+ continue;
22324
+ }
21834
22325
  const entry = {
21835
22326
  request,
21836
22327
  resolve: (response) => {
@@ -21841,6 +22332,7 @@ function AppShell() {
21841
22332
  };
21842
22333
  interactions.enqueue(entry);
21843
22334
  }
22335
+ if (automatic) persistResumedInteractions(pending, collected).catch((err) => debugLog("resume-interaction: automatic persist failed", err));
21844
22336
  }
21845
22337
  }, [
21846
22338
  teardown,
@@ -21850,6 +22342,7 @@ function AppShell() {
21850
22342
  projectDir,
21851
22343
  interactions,
21852
22344
  continueResumedInteractions,
22345
+ persistResumedInteractions,
21853
22346
  extensionRegistry
21854
22347
  ]);
21855
22348
  /** Ref mirror — same rationale as `refreshSessionsRef` above. */
@@ -22890,7 +23383,8 @@ function AppShell() {
22890
23383
  interaction: {
22891
23384
  confirm: async (prompt, confirmOpts) => {
22892
23385
  const response = await requestCommandInteraction({
22893
- id: `command_${crypto.randomUUID()}`,
23386
+ id: `${HOST_INTERACTION_ID_PREFIX}${crypto.randomUUID()}`,
23387
+ origin: "host",
22894
23388
  kind: "question",
22895
23389
  tool: ASK_USER_TOOL,
22896
23390
  createdAt: Date.now(),
@@ -22910,7 +23404,8 @@ function AppShell() {
22910
23404
  },
22911
23405
  askUser: async (form) => {
22912
23406
  const response = await requestCommandInteraction({
22913
- id: `command_${crypto.randomUUID()}`,
23407
+ id: `${HOST_INTERACTION_ID_PREFIX}${crypto.randomUUID()}`,
23408
+ origin: "host",
22914
23409
  kind: "question",
22915
23410
  tool: ASK_USER_TOOL,
22916
23411
  createdAt: Date.now(),
@@ -22921,7 +23416,8 @@ function AppShell() {
22921
23416
  },
22922
23417
  presentPlan: async (plan) => {
22923
23418
  const response = await requestCommandInteraction({
22924
- id: `command_${crypto.randomUUID()}`,
23419
+ id: `${HOST_INTERACTION_ID_PREFIX}${crypto.randomUUID()}`,
23420
+ origin: "host",
22925
23421
  kind: "plan",
22926
23422
  tool: PRESENT_PLAN_TOOL,
22927
23423
  createdAt: Date.now(),
@@ -23651,7 +24147,7 @@ function AppShell() {
23651
24147
  })();
23652
24148
  }, [modal, config.paths.userDir]);
23653
24149
  const onReadContextSegments = useCallback(async () => {
23654
- const { shadowedSystemFiles, supportsOverride } = composeHostContext(pickedAgentRef.current, allowInteractionRef.current !== false);
24150
+ const { shadowedSystemFiles, supportsOverride } = composeHostContext(pickedAgentRef.current, interactionModeRef.current !== "disabled");
23655
24151
  const shadowedSystemPath = shadowedSystemFiles[0]?.path;
23656
24152
  const scopeLoadsUserOverride = userScopeOverridesLoad(userInstructionsScopeRef.current);
23657
24153
  const canOverrideSystem = supportsOverride && scopeLoadsUserOverride;
@@ -23668,7 +24164,7 @@ function AppShell() {
23668
24164
  ...overrideBlockedBy ? { overrideBlockedBy } : {},
23669
24165
  ...shadowedSystemPath ? { shadowedSystemPath } : {}
23670
24166
  };
23671
- const { agentsMd, systemFile, system } = composeHostContext(pickedAgentRef.current, allowInteractionRef.current !== false);
24167
+ const { agentsMd, systemFile, system } = composeHostContext(pickedAgentRef.current, interactionModeRef.current !== "disabled");
23672
24168
  const enabled = enabledSkillsRef.current;
23673
24169
  const sessionSkills = enabled === void 0 ? [...skillsCatalogRef.current] : skillsCatalogRef.current.filter((s) => enabled.includes(s.name));
23674
24170
  return {
@@ -23705,7 +24201,7 @@ function AppShell() {
23705
24201
  const onCreateSystemPrompt = useCallback(async () => {
23706
24202
  const path = userSystemMdPath({ prefix: config.prefix });
23707
24203
  if (!existsSync(path)) {
23708
- const { doctrineOnlySystem, supportsOverride } = composeHostContext(pickedAgentRef.current, allowInteractionRef.current !== false);
24204
+ const { doctrineOnlySystem, supportsOverride } = composeHostContext(pickedAgentRef.current, interactionModeRef.current !== "disabled");
23709
24205
  if (!supportsOverride) throw new Error(`the "${pickedAgentRef.current.id}" profile supplies its own system prompt, so a SYSTEM.md override would not apply`);
23710
24206
  if (!userScopeOverridesLoad(userInstructionsScopeRef.current)) throw new Error(`userInstructionsScope is "${userInstructionsScopeRef.current}", so a SYSTEM.md in your home directory would not be loaded`);
23711
24207
  const doctrine = splitSystemPrompt(doctrineOnlySystem() ?? "").static;
@@ -24507,7 +25003,8 @@ function AppShell() {
24507
25003
  if (matchesBinding(key, keybindings.openTodos) && screen === "chat" && currentSession) {
24508
25004
  modal.open(/* @__PURE__ */ jsx$1(TodosModal, {
24509
25005
  session: sessionRef.current,
24510
- agent: agentRef.current
25006
+ agent: agentRef.current,
25007
+ onSteer: (message) => onSubmitPrompt(message, [], [])
24511
25008
  }));
24512
25009
  return;
24513
25010
  }
@@ -24695,6 +25192,7 @@ function AppShell() {
24695
25192
  COLOR.warn,
24696
25193
  COLOR.dim
24697
25194
  ]);
25195
+ const todoProgress = todoProgressLabel(useActiveTodos(sessionRef.current));
24698
25196
  const hints = useMemo(() => {
24699
25197
  const base = buildHints({
24700
25198
  screen,
@@ -24720,6 +25218,7 @@ function AppShell() {
24720
25218
  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
25219
  activeSkillCount: activeSkillNames.size,
24722
25220
  skillsChipColor: COLOR.brand,
25221
+ todoProgress: settings.showTodoIndicator ? todoProgress : null,
24723
25222
  updateHint,
24724
25223
  cacheBreakHint,
24725
25224
  sessionsScopeAll: sessionsShowAll
@@ -24741,6 +25240,8 @@ function AppShell() {
24741
25240
  currentSession,
24742
25241
  hasMultipleAgents,
24743
25242
  settings.uiMode,
25243
+ settings.showTodoIndicator,
25244
+ todoProgress,
24744
25245
  picked,
24745
25246
  pickedAgent,
24746
25247
  COLOR,
@@ -24762,7 +25263,8 @@ function AppShell() {
24762
25263
  start: r.start,
24763
25264
  end: r.end,
24764
25265
  providerId: r.providerId
24765
- }))
25266
+ })),
25267
+ attachments: m.attachments.length
24766
25268
  })), [messageQueue]);
24767
25269
  const queueShortcuts = useMemo(() => ({
24768
25270
  enter: keybindings.enterQueueSelection,
@@ -24853,7 +25355,6 @@ function AppShell() {
24853
25355
  settings,
24854
25356
  onSubmit: onSubmitPrompt,
24855
25357
  session: currentSession,
24856
- liveSession: sessionRef.current,
24857
25358
  pending: pendingApproval,
24858
25359
  onApproval: resolveHead,
24859
25360
  pendingInteraction,