zidane 6.0.0 → 6.1.0

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 (165) hide show
  1. package/README.md +27 -0
  2. package/dist/{acp-Irby04Us.js → acp-cfR8T19u.js} +8 -8
  3. package/dist/{acp-Irby04Us.js.map → acp-cfR8T19u.js.map} +1 -1
  4. package/dist/acp-cli.js +7 -7
  5. package/dist/acp-cli.js.map +1 -1
  6. package/dist/acp.d.ts +1 -1
  7. package/dist/acp.js +1 -1
  8. package/dist/{agent-DkSmGkDM.js → agent-DZRA7wip.js} +377 -61
  9. package/dist/agent-DZRA7wip.js.map +1 -0
  10. package/dist/agent.d.ts +1 -1
  11. package/dist/agent.js +1 -1
  12. package/dist/{anthropic-CTHsdFG9.js → anthropic-DNJbRMUK.js} +7 -7
  13. package/dist/anthropic-DNJbRMUK.js.map +1 -0
  14. package/dist/atomic-write-B7zr1ddF.js.map +1 -1
  15. package/dist/{auth-BDSu_0t3.js → auth-B3cB5K6d.js} +7 -7
  16. package/dist/auth-B3cB5K6d.js.map +1 -0
  17. package/dist/cache-telemetry-BKAwsNxm.js.map +1 -1
  18. package/dist/chat/pure.d.ts +4 -4
  19. package/dist/chat/pure.js +3 -3
  20. package/dist/chat.d.ts +44 -21
  21. package/dist/chat.d.ts.map +1 -1
  22. package/dist/chat.js +7 -7
  23. package/dist/chat.js.map +1 -1
  24. package/dist/completion-core-CXYSVhZo.js.map +1 -1
  25. package/dist/context-breakdown-Dzo25N45.js.map +1 -1
  26. package/dist/contexts/daytona.d.ts +3 -1
  27. package/dist/contexts/daytona.d.ts.map +1 -1
  28. package/dist/contexts/daytona.js +2 -2
  29. package/dist/contexts/daytona.js.map +1 -1
  30. package/dist/contexts/docker.d.ts.map +1 -1
  31. package/dist/contexts/docker.js +2 -2
  32. package/dist/contexts/docker.js.map +1 -1
  33. package/dist/contexts/e2b.d.ts +3 -1
  34. package/dist/contexts/e2b.d.ts.map +1 -1
  35. package/dist/contexts/e2b.js +1 -1
  36. package/dist/contexts/e2b.js.map +1 -1
  37. package/dist/{contexts-Vone3qQQ.js → contexts-BIbcfBFH.js} +2 -2
  38. package/dist/contexts-BIbcfBFH.js.map +1 -0
  39. package/dist/contexts.js +1 -1
  40. package/dist/edit-utils-EGosADZq.js.map +1 -1
  41. package/dist/eval.d.ts +1 -1
  42. package/dist/eval.js +4 -3
  43. package/dist/eval.js.map +1 -1
  44. package/dist/exec-file-size-PXkr-MGA.js.map +1 -1
  45. package/dist/extensions.d.ts +1 -1
  46. package/dist/extensions.js +1 -1
  47. package/dist/{fetch-url-CWE8X5OD.js → fetch-url-_5WKWpbQ.js} +2 -2
  48. package/dist/{fetch-url-CWE8X5OD.js.map → fetch-url-_5WKWpbQ.js.map} +1 -1
  49. package/dist/{format-BNOXpl-1.js → format-BF745pLL.js} +9 -2
  50. package/dist/format-BF745pLL.js.map +1 -0
  51. package/dist/generate-data-WL_yGVL6.js.map +1 -1
  52. package/dist/hash-DJRzmcPI.js.map +1 -1
  53. package/dist/headless.d.ts +1 -1
  54. package/dist/headless.js +4 -4
  55. package/dist/headless.js.map +1 -1
  56. package/dist/{index-B8UtiZy7.d.ts → index-BGpM2YaU.d.ts} +378 -5
  57. package/dist/index-BGpM2YaU.d.ts.map +1 -0
  58. package/dist/index.d.ts +2 -2
  59. package/dist/index.js +11 -11
  60. package/dist/index.js.map +1 -1
  61. package/dist/logger-DBX9uYOw.js.map +1 -1
  62. package/dist/{login-DOF8pMVM.js → login-DZLG__2f.js} +4 -3
  63. package/dist/{login-DOF8pMVM.js.map → login-DZLG__2f.js.map} +1 -1
  64. package/dist/mcp-CcvuVXB9.js.map +1 -1
  65. package/dist/mcp.d.ts +1 -1
  66. package/dist/media-sniff-Bn76JxAu.js.map +1 -1
  67. package/dist/{messages-CwujiS74.js → messages-C5BDOjUG.js} +2 -2
  68. package/dist/{messages-CwujiS74.js.map → messages-C5BDOjUG.js.map} +1 -1
  69. package/dist/oauth-8LqbeI_Q.js.map +1 -1
  70. package/dist/{openai-compat-BvgYGSR1.js → openai-compat-B0kqvZqN.js} +2 -2
  71. package/dist/openai-compat-B0kqvZqN.js.map +1 -0
  72. package/dist/output/stream-json.d.ts +1 -1
  73. package/dist/output/terminal.d.ts +1 -1
  74. package/dist/output/terminal.js +1 -1
  75. package/dist/output/terminal.js.map +1 -1
  76. package/dist/output-cap-kBUHBlnq.js.map +1 -1
  77. package/dist/presets.d.ts +1 -1
  78. package/dist/presets.js +1 -1
  79. package/dist/providers/anthropic.d.ts +1 -1
  80. package/dist/providers/anthropic.js +1 -1
  81. package/dist/providers/arcee.d.ts +1 -1
  82. package/dist/providers/arcee.js +1 -1
  83. package/dist/providers/baseten.d.ts +1 -1
  84. package/dist/providers/baseten.js +2 -2
  85. package/dist/providers/baseten.js.map +1 -1
  86. package/dist/providers/cerebras.d.ts +1 -1
  87. package/dist/providers/cerebras.js +1 -1
  88. package/dist/providers/local.d.ts +1 -1
  89. package/dist/providers/local.js +1 -1
  90. package/dist/providers/openai-compat.d.ts +1 -1
  91. package/dist/providers/openai-compat.js +1 -1
  92. package/dist/providers/openai.d.ts +1 -1
  93. package/dist/providers/openai.js +8 -8
  94. package/dist/providers/openai.js.map +1 -1
  95. package/dist/providers/openrouter.d.ts +1 -1
  96. package/dist/providers/openrouter.js +1 -1
  97. package/dist/providers/xai.d.ts +1 -1
  98. package/dist/providers/xai.js +1 -1
  99. package/dist/{providers-z6LDMAr3.js → providers-BMlrTr6a.js} +3 -3
  100. package/dist/{providers-z6LDMAr3.js.map → providers-BMlrTr6a.js.map} +1 -1
  101. package/dist/providers.d.ts +1 -1
  102. package/dist/providers.js +4 -4
  103. package/dist/read-state-Dq_TXUX1.js.map +1 -1
  104. package/dist/{resolve-Cr8JSL_O.js → resolve-CC8Lmwcj.js} +3 -3
  105. package/dist/{resolve-Cr8JSL_O.js.map → resolve-CC8Lmwcj.js.map} +1 -1
  106. package/dist/restate.d.ts +1 -1
  107. package/dist/restate.d.ts.map +1 -1
  108. package/dist/restate.js.map +1 -1
  109. package/dist/session/sqlite.d.ts +1 -1
  110. package/dist/session/sqlite.d.ts.map +1 -1
  111. package/dist/session/sqlite.js +75 -20
  112. package/dist/session/sqlite.js.map +1 -1
  113. package/dist/{session-CsdDIPY8.js → session-CsKzK8-P.js} +3 -3
  114. package/dist/session-CsKzK8-P.js.map +1 -0
  115. package/dist/session.d.ts +2 -2
  116. package/dist/session.js +2 -2
  117. package/dist/{skills-Bsb0rvWI.js → skills-BjeXSRlQ.js} +2 -2
  118. package/dist/{skills-Bsb0rvWI.js.map → skills-BjeXSRlQ.js.map} +1 -1
  119. package/dist/skills.d.ts +1 -1
  120. package/dist/skills.js +2 -2
  121. package/dist/stdio-loader-Ce68wUmM.js.map +1 -1
  122. package/dist/{tool-formatters-DY0TDb1r.d.ts → tool-formatters-CMwjDvkp.d.ts} +2 -2
  123. package/dist/tool-formatters-CMwjDvkp.d.ts.map +1 -0
  124. package/dist/tools/fetch-url.d.ts +1 -1
  125. package/dist/tools/fetch-url.d.ts.map +1 -1
  126. package/dist/tools/fetch-url.js +1 -1
  127. package/dist/tools/web-search.d.ts +1 -1
  128. package/dist/tools/web-search.js +1 -1
  129. package/dist/tools/web-search.js.map +1 -1
  130. package/dist/tools.d.ts +2 -2
  131. package/dist/tools.js +2 -2
  132. package/dist/{transcript-anchors-DSRPWjiH.js → transcript-anchors-BTNe1OJi.js} +346 -89
  133. package/dist/transcript-anchors-BTNe1OJi.js.map +1 -0
  134. package/dist/{transcript-anchors-Cb-bDbNH.d.ts → transcript-anchors-D5wvdKQQ.d.ts} +37 -3
  135. package/dist/transcript-anchors-D5wvdKQQ.d.ts.map +1 -0
  136. package/dist/tui.d.ts +21 -6
  137. package/dist/tui.d.ts.map +1 -1
  138. package/dist/tui.js +497 -83
  139. package/dist/tui.js.map +1 -1
  140. package/dist/{turn-operations-Dq9Kx7m2.js → turn-operations-CEt_l3fy.js} +23 -8
  141. package/dist/turn-operations-CEt_l3fy.js.map +1 -0
  142. package/dist/{turn-operations-CgMROLsr.d.ts → turn-operations-JO5MYM9_.d.ts} +27 -9
  143. package/dist/turn-operations-JO5MYM9_.d.ts.map +1 -0
  144. package/dist/types-BIarq1qE.js.map +1 -1
  145. package/dist/types.d.ts +1 -1
  146. package/dist/{xai-B_e6TOA8.js → xai-C_aq671K.js} +3 -3
  147. package/dist/{xai-B_e6TOA8.js.map → xai-C_aq671K.js.map} +1 -1
  148. package/docs/ARCHITECTURE.md +4 -0
  149. package/docs/CHAT.md +12 -10
  150. package/docs/SKILL.md +1 -1
  151. package/docs/TUI.md +3 -3
  152. package/package.json +2 -2
  153. package/dist/agent-DkSmGkDM.js.map +0 -1
  154. package/dist/anthropic-CTHsdFG9.js.map +0 -1
  155. package/dist/auth-BDSu_0t3.js.map +0 -1
  156. package/dist/contexts-Vone3qQQ.js.map +0 -1
  157. package/dist/format-BNOXpl-1.js.map +0 -1
  158. package/dist/index-B8UtiZy7.d.ts.map +0 -1
  159. package/dist/openai-compat-BvgYGSR1.js.map +0 -1
  160. package/dist/session-CsdDIPY8.js.map +0 -1
  161. package/dist/tool-formatters-DY0TDb1r.d.ts.map +0 -1
  162. package/dist/transcript-anchors-Cb-bDbNH.d.ts.map +0 -1
  163. package/dist/transcript-anchors-DSRPWjiH.js.map +0 -1
  164. package/dist/turn-operations-CgMROLsr.d.ts.map +0 -1
  165. package/dist/turn-operations-Dq9Kx7m2.js.map +0 -1
package/dist/tui.js CHANGED
@@ -1,24 +1,24 @@
1
- import { $ as useMcpAuthState, $n as eventsFromTurns, $t as useUpdateCheck, A as runOAuthLogin, Bt as createDiscoverySlot, Cn as TODO_STATUS_GLYPHS, Ct as SETTINGS_CHOICES, D as formatPathForCwd, Dt as clampFps, Et as SettingsProvider, G as buildVisibleMcpRows, Gn as buildPlanSystem, Gt as useConfig, Ht as useDiscovery, J as createFileMcpCredentialStore, Jn as createChatTurnsReadTool, Jt as useCompletion, K as indexOfServerRow, Kn as envSection, Kt as resolveConfig, L as loadMcpToolsCache, M as buildMcpServers, Mt as resolveTheme, Nn as useActiveTodos, O as fetchOAuthRedirect, Ot as useSettings, P as discoverProjectMcps, Q as useMcpAuthDispatch, Qn as deriveSessionTitle, Qt as buildUpdateHint, St as SETTINGS_CATEGORIES, T as suggestSafelistEntries, Tt as SETTINGS_TOGGLES, U as useMcpToolToggleMap, Ut as useDiscoveryOptional, V as subscribeMcpToolsCache, Vt as DiscoveryProvider, Wn as buildBuildSystem, Wt as ConfigProvider, Xn as compactSummaryEvent, Yt as tryOpenBrowser, Z as McpAuthProvider, Zt as bootTick, _ as useSafeModeQueue, _n as accentColor, _t as buildHints, a as useSurfaces, ar as serverToolResultSummary, at as buildResumedToolResultsTurn, b as getSafelist, bt as useEnabledToggleSet, c as useStreamBuffer, cn as chatAutoCompactBehavior, ct as makeRequestInteraction, d as discoverProjectSkills, dr as updateToolEventOutcomes, dt as useInteractionsActions, er as lastContextSizeFromTurns, et as getMcpAuthStatus, ft as useInteractionsQueue, g as useSafeModeActions, gt as truncateTrailing, h as SafeModeProvider, ht as hintsLength, i as useSelectStyle, it as PRESENT_PLAN_TOOL, j as supportsOAuth, jt as resolveChipColor, k as oauthUsesManualCodePaste, l as buildSkillsConfig, ln as discoverAgentsMd, lr as toolCallPreview, lt as pendingInteractionsFromTurns, m as writeSessionExport, mt as clipHintsToWidth, n as ThemeProvider, nr as loadState, nt as ASK_USER_TOOL, or as stripSpawnTokensLine, ot as createInteractionTools, pt as EMPTY_HINTS, q as parentServerName, qn as chatReferenceTokensForPrompt, qt as resolveStoragePaths, r as useColors, rr as marginTopFor, rt as InteractionsProvider, s as useTheme, sr as sumRunCosts, t as computeTurnAnchors, tr as listSessionMeta, u as defaultSkillScanPaths, ur as toolResultText, wt as SETTINGS_INPUTS, x as isOnSafelist, xt as DEFAULT_SETTINGS, y as addToSafelist, yt as listProjectFiles, z as refreshMcpToolsCatalog } from "./transcript-anchors-DSRPWjiH.js";
2
- import { $ as filterExtensionRegistry, B as installExtensionFromSpec, Ft as formatBindingForDisplay, G as updateInstalledExtension, It as groupBindings, J as discoverAndBuildExtensionRegistry, Jt as inlineLoadedExtension, K as createExtensionHud, L as createExtensionPromptBridge, Lt as keybindingsPath, M as summaryToTurn, Nt as KEYBINDING_KEY_COL_WIDTH, Pt as ensureKeybindingsFile, Q as disposeExtensionRegistry, R as ExtensionInstallError, Rt as matchesBinding, Ut as resolveExtensionKeybindings, W as removeInstalledExtension, Xt as parseExtensionScanPathEnv, Z as composeExtensionPreset, _ as selectFilesFromSession, cn as effectiveContextWindow, en as coerceExtensionConfigInput, f as resolveMcpWarningsDir, h as buildPostCompactAttachments, in as resolveExtensionConfigValue, m as resolveTasksDir, nt as createExtensionHostRuntimeBridge, o as applyCompactSummaryCutoff, p as resolvePersistDir, q as registerExtensionHostModules, sn as OUTPUT_RESERVE_TOKENS, st as basic_default, t as createAgent, tt as partitionExtensionCatalog, u as cleanupPersistedSession, y as compactConversation } from "./agent-DkSmGkDM.js";
3
- import { a as formatTaskStatus, c as shortId, i as formatDuration, n as compactPath, o as formatTaskSummary, r as fmtTokens, s as previewLine, t as ageString } from "./format-BNOXpl-1.js";
4
- import { n as createProcessContext } from "./contexts-Vone3qQQ.js";
1
+ import { $ as useMcpAuthState, $n as buildPlanSystem, $t as restoreChainFallbacks, A as runOAuthLogin, Bt as createDiscoverySlot, Ct as SETTINGS_CHOICES, D as formatPathForCwd, Dt as clampFps, Et as SettingsProvider, G as buildVisibleMcpRows, Gt as useConfig, Ht as useDiscovery, J as createFileMcpCredentialStore, Jt as buildRunModel, K as indexOfServerRow, Kt as resolveConfig, L as loadMcpToolsCache, M as buildMcpServers, Mt as resolveTheme, O as fetchOAuthRedirect, Ot as useSettings, P as discoverProjectMcps, Q as useMcpAuthDispatch, Qn as buildBuildSystem, Qt as providerKeySetChanged, R as mcpToolsCachePath, St as SETTINGS_CATEGORIES, T as suggestSafelistEntries, Tn as accentColor, Tt as SETTINGS_TOGGLES, U as useMcpToolToggleMap, Ut as useDiscoveryOptional, V as subscribeMcpToolsCache, Vn as useActiveTodos, Vt as DiscoveryProvider, Wt as ConfigProvider, Xt as chainsEqual, Yt as chainPositionOf, Z as McpAuthProvider, Zt as filterChainFallbacks, _ as useSafeModeQueue, _n as discoverAgentsMd, _r as toolCallPreview, _t as buildHints, a as useSurfaces, an as bootTick, at as buildResumedToolResultsTurn, b as getSafelist, br as usageByModel, bt as useEnabledToggleSet, c as useStreamBuffer, cr as lastContextSizeFromTurns, ct as makeRequestInteraction, d as discoverProjectSkills, dr as marginTopFor, dt as useInteractionsActions, er as envSection, et as getMcpAuthStatus, ft as useInteractionsQueue, g as useSafeModeActions, gn as chatAutoCompactBehavior, gt as truncateTrailing, h as SafeModeProvider, hr as sumRunCosts, ht as hintsLength, i as useSelectStyle, ir as compactSummaryEvent, it as PRESENT_PLAN_TOOL, j as supportsOAuth, jn as TODO_STATUS_GLYPHS, jt as resolveChipColor, k as oauthUsesManualCodePaste, l as buildSkillsConfig, lr as listSessionMeta, lt as pendingInteractionsFromTurns, m as writeSessionExport, mr as stripSpawnTokensLine, mt as clipHintsToWidth, n as ThemeProvider, nn as useCompletion, nr as createChatTurnsReadTool, nt as ASK_USER_TOOL, on as buildUpdateHint, or as deriveSessionTitle, ot as createInteractionTools, pr as serverToolResultSummary, pt as EMPTY_HINTS, q as parentServerName, qt as resolveStoragePaths, r as useColors, rn as tryOpenBrowser, rt as InteractionsProvider, s as useTheme, sn as useUpdateCheck, sr as eventsFromTurns, t as computeTurnAnchors, tn as toggleChainEntry, tr as chatReferenceTokensForPrompt, u as defaultSkillScanPaths, ur as loadState, vr as toolResultText, wt as SETTINGS_INPUTS, x as isOnSafelist, xt as DEFAULT_SETTINGS, y as addToSafelist, yr as updateToolEventOutcomes, yt as listProjectFiles, z as refreshMcpToolsCatalog } from "./transcript-anchors-BTNe1OJi.js";
2
+ import { $ as composeExtensionPreset, $t as inlineLoadedExtension, B as ExtensionInstallError, Bt as formatBindingForDisplay, H as installExtensionFromSpec, Ht as keybindingsPath, J as createExtensionHud, Jt as resolveExtensionKeybindings, K as removeInstalledExtension, P as summaryToTurn, Rt as KEYBINDING_KEY_COL_WIDTH, Tt as createModelSearchTool, Ut as matchesBinding, Vt as groupBindings, X as discoverAndBuildExtensionRegistry, Y as registerExtensionHostModules, _ as buildPostCompactAttachments, an as coerceExtensionConfigInput, c as applyCompactSummaryCutoff, et as disposeExtensionRegistry, f as cleanupPersistedSession, fn as OUTPUT_RESERVE_TOKENS, g as resolveTasksDir, h as resolvePersistDir, ht as isSpawnTool, it as createExtensionHostRuntimeBridge, ln as resolveExtensionConfigValue, lt as basic_default, m as resolveMcpWarningsDir, nn as scanExtensionDirectories, pn as effectiveContextWindow, q as updateInstalledExtension, rt as partitionExtensionCatalog, t as createAgent, tn as parseExtensionScanPathEnv, tt as filterExtensionRegistry, x as compactConversation, y as selectFilesFromSession, z as createExtensionPromptBridge, zt as ensureKeybindingsFile } from "./agent-DZRA7wip.js";
3
+ import { a as formatDuration, c as previewLine, i as fmtTokens, l as shortId, n as compactPath, o as formatTaskStatus, r as fmtCost, s as formatTaskSummary, t as ageString } from "./format-BF745pLL.js";
4
+ import { n as createProcessContext } from "./contexts-BIbcfBFH.js";
5
5
  import { l as errorMessage } from "./errors-DJUxZg9b.js";
6
- import { M as findGitRoot, j as projectUserPaths } from "./resolve-Cr8JSL_O.js";
6
+ import { M as findGitRoot, j as projectUserPaths } from "./resolve-CC8Lmwcj.js";
7
7
  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";
8
8
  import { i as sniffImageMediaType } from "./media-sniff-Bn76JxAu.js";
9
9
  import { r as formatTokenUsage, t as effectiveInputFromTurn } from "./stats-DAKBEKjc.js";
10
10
  import { replaceDynamicSection } from "./prompt.js";
11
- import { a as getExtensionSessionState, i as demoteStaleRunningStatus, o as loadSession, s as setExtensionSessionState, t as createSession } from "./session-CsdDIPY8.js";
11
+ import { a as getExtensionSessionState, i as demoteStaleRunningStatus, o as loadSession, s as setExtensionSessionState, t as createSession } from "./session-CsKzK8-P.js";
12
12
  import { o as withTimeout } from "./timeout-BDetbuN6.js";
13
13
  import { n as connectMcpServers, u as McpOAuthProvider } from "./mcp-CcvuVXB9.js";
14
14
  import { c as commandBlockToEvent, d as executeCommand, f as parseCommandInvocation, i as shouldAutoGenerateTitle, l as commandBlocksToContent, r as generateSessionTitle, t as generateStructuredData } from "./generate-data-WL_yGVL6.js";
15
15
  import { s as resolveCompletionSubmit } from "./completion-core-CXYSVhZo.js";
16
- import { i as installSkillFromSpec, r as SkillInstallError } from "./skills-Bsb0rvWI.js";
17
- import { i as ZIDANE_EXTENSION_SKILL_NAME, l as withZidaneExtensionSkill, o as extensionDirForCwd, r as EXTENSION_EDIT_METADATA_KEY, s as extensionScaffold, t as loginMcpServer } from "./login-DOF8pMVM.js";
18
- import { C as restoreModelOptions, S as piIdOf, _ as modelOptionsFor, m as getContextWindow, p as enabledModelOptions, s as setProviderCredential, t as detectAuth, v as modelSupportsReasoning } from "./auth-BDSu_0t3.js";
16
+ import { i as installSkillFromSpec, r as SkillInstallError } from "./skills-BjeXSRlQ.js";
17
+ import { i as ZIDANE_EXTENSION_SKILL_NAME, l as withZidaneExtensionSkill, o as extensionDirForCwd, r as EXTENSION_EDIT_METADATA_KEY, s as extensionScaffold, t as loginMcpServer } from "./login-DZLG__2f.js";
18
+ import { C as restoreModelOptions, S as piIdOf, _ as modelOptionsFor, m as getContextWindow, p as enabledModelOptions, s as setProviderCredential, t as detectAuth, v as modelSupportsReasoning } from "./auth-B3cB5K6d.js";
19
19
  import { createDockerContext } from "./contexts/docker.js";
20
20
  import { createTuiStore } from "./session/sqlite.js";
21
- import { $ as rewriteMultiEditHeader, A as buildLinearRamp, D as createFilesCompletionProvider, F as selectableTurnIds, H as extractEditPayload, I as turnSelectionOwnership, J as buildEditOutcomesAnnotation, K as summarizeEditPayload, M as isEditErrorResult, N as isTurnHighlighted, P as isVisible, Q as resolveApprovalForPayload, R as buildContextualDiff, S as splitMarkdownCodeBlocks, T as uniqueSkillNamesFromReferences, U as filetypeFromPath, W as previewEditPayload, X as mergeApprovalAndBodyOutcomes, Z as parseEditOutcomesFromResult, _ as filterModelCatalog, a as TOOL_DISPLAY, b as chunkMarkdownForRender, d as finalizeStreamingMarkdown, et as stripEditOutcomesAnnotation, f as finalizeStreamingMarkdownForOwner, g as buildModelCatalog, h as splitPromptSegments, i as turnAsText, it as createChatsCompletionProvider, j as EDIT_TOOL_NAMES, k as blendHsl, m as turnContextSize, n as deleteTurnSafely, o as displayNameFor, r as truncateTurnsAt, s as formatToolCall, tt as summarizeOutcomes, u as capTranscriptEvents, v as indexOfEntry, w as createSkillsCompletionProvider, x as normalizeMarkdownCodeFences, z as buildUnifiedDiff } from "./turn-operations-Dq9Kx7m2.js";
21
+ import { $ as rewriteMultiEditHeader, A as buildLinearRamp, D as createFilesCompletionProvider, F as selectableTurnIds, H as extractEditPayload, I as turnSelectionOwnership, J as buildEditOutcomesAnnotation, K as summarizeEditPayload, M as isEditErrorResult, N as isTurnHighlighted, P as isVisible, Q as resolveApprovalForPayload, R as buildContextualDiff, S as splitMarkdownCodeBlocks, T as uniqueSkillNamesFromReferences, U as filetypeFromPath, W as previewEditPayload, X as mergeApprovalAndBodyOutcomes, Z as parseEditOutcomesFromResult, _ as filterModelCatalog, a as TOOL_DISPLAY, b as chunkMarkdownForRender, d as finalizeStreamingMarkdown, et as stripEditOutcomesAnnotation, f as finalizeStreamingMarkdownForOwner, g as buildModelCatalog, h as splitPromptSegments, i as turnAsText, it as createChatsCompletionProvider, j as EDIT_TOOL_NAMES, k as blendHsl, m as turnContextSize, n as deleteTurnSafely, o as displayNameFor, r as truncateTurnsAt, s as formatToolCall, tt as summarizeOutcomes, u as capTranscriptEvents, v as indexOfEntry, w as createSkillsCompletionProvider, x as normalizeMarkdownCodeFences, z as buildUnifiedDiff } from "./turn-operations-CEt_l3fy.js";
22
22
  import { basename, dirname, join, relative, resolve } from "node:path";
23
23
  import { Buffer } from "node:buffer";
24
24
  import * as fs from "node:fs";
@@ -2183,6 +2183,13 @@ function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = fals
2183
2183
  fg: COLOR.mute,
2184
2184
  children: " · "
2185
2185
  })] }),
2186
+ event.model !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx$1("span", {
2187
+ fg: COLOR.model,
2188
+ children: event.model
2189
+ }), /* @__PURE__ */ jsx$1("span", {
2190
+ fg: COLOR.mute,
2191
+ children: " · "
2192
+ })] }),
2186
2193
  /* @__PURE__ */ jsx$1("span", {
2187
2194
  fg: COLOR.dim,
2188
2195
  children: safeText
@@ -3378,7 +3385,7 @@ function pctLabel(tokens, denom) {
3378
3385
  const raw = tokens / denom * 100;
3379
3386
  return raw < 1 ? "<1%" : `${Math.round(raw)}%`;
3380
3387
  }
3381
- function ContextPanelModal({ breakdown }) {
3388
+ function ContextPanelModal({ breakdown, modelUsage }) {
3382
3389
  const COLOR = useColors();
3383
3390
  const [expanded, setExpanded] = useState(() => /* @__PURE__ */ new Set());
3384
3391
  const expandableIds = useMemo(() => breakdown.categories.filter((c) => c.items && c.items.length > 0).map((c) => c.id), [breakdown]);
@@ -3492,6 +3499,10 @@ function ContextPanelModal({ breakdown }) {
3492
3499
  usage: breakdown.usage,
3493
3500
  COLOR
3494
3501
  }),
3502
+ modelUsage && modelUsage.length > 0 && /* @__PURE__ */ jsx$1(ModelUsageRows, {
3503
+ rows: modelUsage,
3504
+ COLOR
3505
+ }),
3495
3506
  breakdown.extensionSections?.map((section) => /* @__PURE__ */ jsx$1(ExtensionSectionRows, {
3496
3507
  section,
3497
3508
  COLOR
@@ -3683,6 +3694,82 @@ function UsageRows({ usage, COLOR }) {
3683
3694
  }, r.label))
3684
3695
  ] });
3685
3696
  }
3697
+ /**
3698
+ * Session-wide token + spend totals per model, one row each — the surface
3699
+ * that makes cost-tiered subagent delegation legible (cheap workers show
3700
+ * as their own rows at their own rates). Costs come from per-turn usage:
3701
+ * provider-reported when available, locally estimated otherwise; `≥`
3702
+ * marks models where some turns had no rate (the figure is a floor).
3703
+ */
3704
+ function ModelUsageRows({ rows, COLOR }) {
3705
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
3706
+ /* @__PURE__ */ jsx$1("box", { style: {
3707
+ height: 1,
3708
+ flexShrink: 0
3709
+ } }),
3710
+ /* @__PURE__ */ jsx$1("box", {
3711
+ style: {
3712
+ height: 1,
3713
+ flexShrink: 0
3714
+ },
3715
+ children: /* @__PURE__ */ jsx$1("text", {
3716
+ wrapMode: "none",
3717
+ fg: COLOR.mute,
3718
+ children: "usage by model"
3719
+ })
3720
+ }),
3721
+ rows.map((r) => {
3722
+ const sent = r.input + r.cacheRead + r.cacheCreation;
3723
+ return /* @__PURE__ */ jsxs("box", {
3724
+ style: {
3725
+ flexDirection: "row",
3726
+ height: 1,
3727
+ flexShrink: 0
3728
+ },
3729
+ children: [
3730
+ /* @__PURE__ */ jsx$1("text", {
3731
+ wrapMode: "none",
3732
+ fg: COLOR.model,
3733
+ children: r.modelId
3734
+ }),
3735
+ /* @__PURE__ */ jsx$1("box", { style: { flexGrow: 1 } }),
3736
+ /* @__PURE__ */ jsxs("text", {
3737
+ wrapMode: "none",
3738
+ children: [
3739
+ /* @__PURE__ */ jsx$1("span", {
3740
+ fg: COLOR.mute,
3741
+ children: `${r.turns} turn${r.turns === 1 ? "" : "s"} · `
3742
+ }),
3743
+ /* @__PURE__ */ jsx$1("span", {
3744
+ fg: COLOR.dim,
3745
+ children: fmtTokens(sent)
3746
+ }),
3747
+ /* @__PURE__ */ jsx$1("span", {
3748
+ fg: COLOR.mute,
3749
+ children: " in · "
3750
+ }),
3751
+ /* @__PURE__ */ jsx$1("span", {
3752
+ fg: COLOR.dim,
3753
+ children: fmtTokens(r.output)
3754
+ }),
3755
+ /* @__PURE__ */ jsx$1("span", {
3756
+ fg: COLOR.mute,
3757
+ children: " out"
3758
+ }),
3759
+ r.cost > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx$1("span", {
3760
+ fg: COLOR.mute,
3761
+ children: ` · ${r.costComplete ? "" : "≥"}`
3762
+ }), /* @__PURE__ */ jsx$1("span", {
3763
+ fg: COLOR.money ?? COLOR.warn,
3764
+ children: fmtCost(r.cost)
3765
+ })] })
3766
+ ]
3767
+ })
3768
+ ]
3769
+ }, r.modelId);
3770
+ })
3771
+ ] });
3772
+ }
3686
3773
  function ExtensionSectionRows({ section, COLOR }) {
3687
3774
  return /* @__PURE__ */ jsxs(Fragment, { children: [
3688
3775
  /* @__PURE__ */ jsx$1("box", { style: {
@@ -3990,7 +4077,7 @@ const HOME = homedir();
3990
4077
  const MAX_ENTRIES = 5e4;
3991
4078
  const MAX_DEPTH = 5;
3992
4079
  const WALK_BATCH_SIZE = 64;
3993
- const SKIP_DIRS = new Set([
4080
+ const SKIP_DIRS = /* @__PURE__ */ new Set([
3994
4081
  "node_modules",
3995
4082
  ".git",
3996
4083
  ".hg",
@@ -4453,8 +4540,12 @@ function DiscoveryShell({ children }) {
4453
4540
  mcpCredentialStore
4454
4541
  ]);
4455
4542
  useEffect(() => {
4543
+ let lastSignature = cacheFileSignature(dataDir);
4456
4544
  const id = setInterval(() => {
4457
4545
  try {
4546
+ const signature = cacheFileSignature(dataDir);
4547
+ if (signature === lastSignature) return;
4548
+ lastSignature = signature;
4458
4549
  setMcpToolsByServer(readToolsByServer(dataDir));
4459
4550
  } catch (err) {
4460
4551
  debugLog$1("mcp-tools-cache refresh failed", err);
@@ -4496,6 +4587,15 @@ function DiscoveryShell({ children }) {
4496
4587
  * only need the schemas. Best-effort — a corrupt cache produces an
4497
4588
  * empty map and the next bootstrap re-populates.
4498
4589
  */
4590
+ /**
4591
+ * Cheap change signature for the MCP tools cache file — `null` when the
4592
+ * file doesn't exist yet. mtime+size catches every write the atomic
4593
+ * save path produces (fresh tmp file + rename ⇒ new inode/mtime).
4594
+ */
4595
+ function cacheFileSignature(dataDir) {
4596
+ const stat = statSync(mcpToolsCachePath(dataDir), { throwIfNoEntry: false });
4597
+ return stat ? `${stat.mtimeMs}:${stat.size}` : null;
4598
+ }
4499
4599
  function readToolsByServer(dataDir) {
4500
4600
  const cache = loadMcpToolsCache({ dataDir });
4501
4601
  const out = {};
@@ -4954,7 +5054,17 @@ function OptionRow({ option, isEnabled, isFocused, highlightBg }) {
4954
5054
  * the chosen provider and model id on commit. Pick a model from a
4955
5055
  * different provider than the current one and the caller is expected
4956
5056
  * to swap the active `ProviderAuth` + rebuild the agent — see
4957
- * `app.tsx`'s `onPickModel` for the canonical wiring.
5057
+ * `app.tsx`'s `onApplyModelChain` for the canonical wiring.
5058
+ *
5059
+ * Two commit modes:
5060
+ * - **Single pick** (`onPick`, default): ↵ commits the highlighted row
5061
+ * and the caller closes the modal — the original behavior, still
5062
+ * used by the compaction-model picker and external hosts.
5063
+ * - **Ordered multi-select** (`onApplyChain`): ↵ toggles the highlighted
5064
+ * row in a numbered fallback chain (the number is the row's 1-based
5065
+ * position — the order rows were picked is the order the agent falls
5066
+ * back in), esc applies the chain and closes. Esc with an unchanged
5067
+ * (or emptied) chain is a plain cancel.
4958
5068
  *
4959
5069
  * Geometry:
4960
5070
  * - Top: single-row search input. The input owns focus so the user
@@ -4973,7 +5083,7 @@ function OptionRow({ option, isEnabled, isFocused, highlightBg }) {
4973
5083
  */
4974
5084
  /** Visible rows in the windowed list. Keeps the modal a stable size on long catalogs. */
4975
5085
  const VISIBLE_ROWS = 10;
4976
- function ModelPickerModal({ providers, modelsFor, current, onPick }) {
5086
+ function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onApplyChain }) {
4977
5087
  const COLOR = useColors();
4978
5088
  const SURFACE = useSurfaces();
4979
5089
  const inputRef = useRef(null);
@@ -4997,13 +5107,35 @@ function ModelPickerModal({ providers, modelsFor, current, onPick }) {
4997
5107
  setSelectedIdx(0);
4998
5108
  }, []);
4999
5109
  const safeIndex = filtered.length === 0 ? 0 : Math.min(selectedIdx, filtered.length - 1);
5110
+ const chainMode = onApplyChain !== void 0;
5111
+ const [initialChain] = useState(() => chain && chain.length > 0 ? [...chain] : [{
5112
+ provider: current.providerKey,
5113
+ model: current.modelId
5114
+ }]);
5115
+ const [chainState, setChainState] = useState(initialChain);
5116
+ const modalApi = useModalState();
5000
5117
  const commit = () => {
5001
5118
  const row = filtered[safeIndex];
5002
- if (row) onPick({
5119
+ if (!row) return;
5120
+ if (chainMode) {
5121
+ setChainState((prev) => toggleChainEntry(prev, {
5122
+ provider: row.providerKey,
5123
+ model: row.model.id
5124
+ }));
5125
+ return;
5126
+ }
5127
+ onPick?.({
5003
5128
  providerKey: row.providerKey,
5004
5129
  modelId: row.model.id
5005
5130
  });
5006
5131
  };
5132
+ const handleClose = () => {
5133
+ if (chainMode && chainState.length > 0 && !chainsEqual(chainState, initialChain)) {
5134
+ onApplyChain?.(chainState);
5135
+ return;
5136
+ }
5137
+ modalApi?.close();
5138
+ };
5007
5139
  const viewport = useMemo(() => {
5008
5140
  if (filtered.length <= VISIBLE_ROWS) return {
5009
5141
  start: 0,
@@ -5035,8 +5167,9 @@ function ModelPickerModal({ providers, modelsFor, current, onPick }) {
5035
5167
  });
5036
5168
  if (providers.length === 0) return /* @__PURE__ */ jsx$1(EmptyProvidersState, {});
5037
5169
  return /* @__PURE__ */ jsxs(Modal, {
5038
- title: "select model",
5170
+ title: chainMode ? "select models — order = fallback priority" : "select model",
5039
5171
  maxWidth: 100,
5172
+ onClose: handleClose,
5040
5173
  children: [
5041
5174
  /* @__PURE__ */ jsx$1("box", {
5042
5175
  style: {
@@ -5074,7 +5207,8 @@ function ModelPickerModal({ providers, modelsFor, current, onPick }) {
5074
5207
  entry,
5075
5208
  isCurrent: entry.providerKey === current.providerKey && entry.model.id === current.modelId,
5076
5209
  isFocused: viewport.start + i === safeIndex,
5077
- highlightBg: SURFACE.selection
5210
+ highlightBg: SURFACE.selection,
5211
+ chainPosition: chainMode ? chainPositionOf(chainState, entry.providerKey, entry.model.id) : null
5078
5212
  }, `${entry.providerKey}:${entry.model.id}`))
5079
5213
  }),
5080
5214
  /* @__PURE__ */ jsxs("text", {
@@ -5089,12 +5223,16 @@ function ModelPickerModal({ providers, modelsFor, current, onPick }) {
5089
5223
  fg: COLOR.warn,
5090
5224
  children: "↵"
5091
5225
  }),
5092
- " select · ",
5226
+ chainMode ? " add/remove · " : " select · ",
5093
5227
  /* @__PURE__ */ jsx$1("span", {
5094
5228
  fg: COLOR.warn,
5095
5229
  children: "esc"
5096
5230
  }),
5097
- " close · ",
5231
+ chainMode ? " apply · " : " close · ",
5232
+ chainMode && /* @__PURE__ */ jsx$1("span", {
5233
+ fg: COLOR.brand,
5234
+ children: `${chainState.length} selected · `
5235
+ }),
5098
5236
  /* @__PURE__ */ jsx$1("span", {
5099
5237
  fg: COLOR.mute,
5100
5238
  children: `${filtered.length} / ${catalog.length} model${catalog.length === 1 ? "" : "s"}`
@@ -5111,9 +5249,10 @@ function ModelPickerModal({ providers, modelsFor, current, onPick }) {
5111
5249
  * select-turn mode in the transcript) so it pops without a separate
5112
5250
  * marker glyph competing with the `●` "current" indicator.
5113
5251
  */
5114
- function ModelRow({ entry, isCurrent, isFocused, highlightBg }) {
5252
+ function ModelRow({ entry, isCurrent, isFocused, highlightBg, chainPosition = null }) {
5115
5253
  const COLOR = useColors();
5116
- const marker = isCurrent ? "●" : " ";
5254
+ const marker = chainPosition !== null ? chainPosition > 9 ? "+" : String(chainPosition) : isCurrent ? "●" : " ";
5255
+ const markerColor = chainPosition !== null || isCurrent ? COLOR.brand : COLOR.mute;
5117
5256
  return /* @__PURE__ */ jsx$1("box", {
5118
5257
  style: {
5119
5258
  height: 1,
@@ -5126,7 +5265,7 @@ function ModelRow({ entry, isCurrent, isFocused, highlightBg }) {
5126
5265
  wrapMode: "none",
5127
5266
  children: [
5128
5267
  /* @__PURE__ */ jsx$1("span", {
5129
- fg: isCurrent ? COLOR.brand : COLOR.mute,
5268
+ fg: markerColor,
5130
5269
  children: marker
5131
5270
  }),
5132
5271
  /* @__PURE__ */ jsx$1("span", {
@@ -5818,7 +5957,7 @@ function ExtensionBanners() {
5818
5957
  }
5819
5958
  //#endregion
5820
5959
  //#region src/tui/file-edit-approval-modal.tsx
5821
- const FILE_EDIT_TOOLS = new Set([
5960
+ const FILE_EDIT_TOOLS = /* @__PURE__ */ new Set([
5822
5961
  "edit",
5823
5962
  "multi_edit",
5824
5963
  "write_file"
@@ -7521,7 +7660,8 @@ function OpenBrowserButton({ authUrl }) {
7521
7660
  const feedbackTimer = useRef(null);
7522
7661
  const trigger = useCallback(() => {
7523
7662
  tryOpenBrowser(authUrl);
7524
- setFeedback(writeToClipboard(authUrl) ? "opened in browser · URL copied to clipboard" : "opened in browser");
7663
+ const copied = writeToClipboard(authUrl);
7664
+ setFeedback(copied ? "opened in browser · URL copied to clipboard" : "opened in browser");
7525
7665
  if (feedbackTimer.current) clearTimeout(feedbackTimer.current);
7526
7666
  feedbackTimer.current = setTimeout(setFeedback, 4e3, null);
7527
7667
  }, [authUrl]);
@@ -8540,7 +8680,8 @@ function SessionsScreen({ sessions, currentId, focusedSessionId, onPick, onCreat
8540
8680
  ]);
8541
8681
  const moveCursor = useCallback((nextIndex) => {
8542
8682
  if (!onFocusChange || rows.length === 0) return;
8543
- const row = rows[(nextIndex % rows.length + rows.length) % rows.length];
8683
+ const clamped = (nextIndex % rows.length + rows.length) % rows.length;
8684
+ const row = rows[clamped];
8544
8685
  onFocusChange(row?.rowId ?? null);
8545
8686
  }, [rows, onFocusChange]);
8546
8687
  const commitCurrent = useCallback(() => {
@@ -10642,7 +10783,8 @@ const LOG_LEVEL_CYCLE = [
10642
10783
  ];
10643
10784
  /** Next min-level in {@link LOG_LEVEL_CYCLE}, wrapping around. */
10644
10785
  function nextLogLevel(level) {
10645
- return LOG_LEVEL_CYCLE[(LOG_LEVEL_CYCLE.indexOf(level) + 1) % LOG_LEVEL_CYCLE.length];
10786
+ const i = LOG_LEVEL_CYCLE.indexOf(level);
10787
+ return LOG_LEVEL_CYCLE[(i + 1) % LOG_LEVEL_CYCLE.length];
10646
10788
  }
10647
10789
  /** Retain only records at or above `minLevel`. Preserves order. */
10648
10790
  function filterLogRecords(records, minLevel) {
@@ -11175,7 +11317,8 @@ function McpAddDialog({ active, existingNames, onSubmit, onCancel }) {
11175
11317
  }
11176
11318
  const cycles = key.name === "left" ? -1 : key.name === "right" || key.name === "space" ? 1 : 0;
11177
11319
  if (cycles !== 0 && row === "transport") {
11178
- setTransport(MCP_TRANSPORTS[(MCP_TRANSPORTS.indexOf(transport) + cycles + MCP_TRANSPORTS.length) % MCP_TRANSPORTS.length]);
11320
+ const t = MCP_TRANSPORTS.indexOf(transport);
11321
+ setTransport(MCP_TRANSPORTS[(t + cycles + MCP_TRANSPORTS.length) % MCP_TRANSPORTS.length]);
11179
11322
  return;
11180
11323
  }
11181
11324
  if (cycles !== 0 && row === "scope") setScope((s) => s === "user" ? "project" : "user");
@@ -11582,7 +11725,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
11582
11725
  description: `current: ${settings.compactionModel || "main model"} — pick any provider's model for compaction summaries (pick the main model to clear)`,
11583
11726
  onPick: actions.onPickCompactionModel
11584
11727
  });
11585
- const inputs = SETTINGS_INPUTS.filter((inp) => inp.key !== "sessionIcon" || settings.terminalTitle === "icon-session").map((inp) => ({
11728
+ const inputs = SETTINGS_INPUTS.filter((inp) => inp.key !== "sessionIcon" && inp.key !== "attentionIcon" || settings.terminalTitle === "icon-session").map((inp) => ({
11586
11729
  kind: "input",
11587
11730
  ...inp
11588
11731
  }));
@@ -11837,7 +11980,8 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
11837
11980
  if (!key.ctrl && !key.meta && !key.shift && (key.name === "left" || key.name === "right")) {
11838
11981
  key.preventDefault();
11839
11982
  const idx = tabOrder.indexOf(activeTab);
11840
- setActiveTab(key.name === "left" ? tabOrder[(idx - 1 + tabOrder.length) % tabOrder.length] : tabOrder[(idx + 1) % tabOrder.length]);
11983
+ const next = key.name === "left" ? tabOrder[(idx - 1 + tabOrder.length) % tabOrder.length] : tabOrder[(idx + 1) % tabOrder.length];
11984
+ setActiveTab(next);
11841
11985
  return;
11842
11986
  }
11843
11987
  if (key.name === "up" || key.ctrl && key.name === "p") {
@@ -13677,8 +13821,9 @@ const TITLE_STACK_PUSH = "\x1B[22;0t";
13677
13821
  const TITLE_STACK_RESTORE = "\x1B]0;\x07\x1B[23;0t";
13678
13822
  /**
13679
13823
  * Make a string safe to hand to the terminal as a window/icon title.
13680
- * Session titles come from user content and `sessionIcon` is free text,
13681
- * so a stray control byte (newline, BEL, ESC, …) could terminate the OSC
13824
+ * Session titles come from user content and the icon settings
13825
+ * (`sessionIcon` / `attentionIcon`) are free text, so a stray control
13826
+ * byte (newline, BEL, ESC, …) could terminate the OSC
13682
13827
  * title sequence early or smuggle escape sequences through it. Strip
13683
13828
  * control characters, collapse whitespace to single spaces, trim, and
13684
13829
  * clamp to a sane length — a title is a one-line label, not a payload.
@@ -13687,6 +13832,28 @@ function sanitizeTitle(raw) {
13687
13832
  return [...raw.replace(/[\u0000-\u001F\u007F-\u009F]/g, " ").replace(/\s+/g, " ").trim()].slice(0, 256).join("");
13688
13833
  }
13689
13834
  /**
13835
+ * Compose the window title for the active (non-`'off'`) `terminalTitle`
13836
+ * modes. Pure — the TUI's title effect owns WHEN to apply it (and the
13837
+ * save/restore dance for `'off'`); this owns WHAT it says:
13838
+ *
13839
+ * - `'app'` — always `zidane`.
13840
+ * - `'session'` — the session title, falling back to `zidane`.
13841
+ * - `'icon-session'` — `{icon} · {title}`, where the icon is
13842
+ * `sessionIcon` (⚽ when empty) — or `attentionIcon` (⚠️ when empty)
13843
+ * while the agent is blocked on user input (tool approval /
13844
+ * `ask_user` / `present_plan`), so a backgrounded tab can flag
13845
+ * "zidane needs you".
13846
+ *
13847
+ * Callers still pass the result through {@link sanitizeTitle} — icons
13848
+ * and session titles are both free text.
13849
+ */
13850
+ function composeTerminalTitle(opts) {
13851
+ const sessionTitle = opts.sessionTitle?.trim() || "zidane";
13852
+ if (opts.mode === "app") return "zidane";
13853
+ if (opts.mode === "session") return sessionTitle;
13854
+ return `${opts.awaitingInput ? opts.attentionIcon.trim() || "⚠️" : opts.sessionIcon.trim() || "⚽"} · ${sessionTitle}`;
13855
+ }
13856
+ /**
13690
13857
  * Push the current terminal title so {@link restoreTerminalTitle} can
13691
13858
  * bring it back later. No-op when `out` isn't a TTY.
13692
13859
  */
@@ -14333,7 +14500,7 @@ function blockSummary(turn) {
14333
14500
  }
14334
14501
  //#endregion
14335
14502
  //#region src/tui/app.tsx
14336
- const BUILTIN_COMPLETION_PROVIDER_IDS = new Set([
14503
+ const BUILTIN_COMPLETION_PROVIDER_IDS = /* @__PURE__ */ new Set([
14337
14504
  "commands",
14338
14505
  "skills",
14339
14506
  "files",
@@ -14346,7 +14513,7 @@ const BUILTIN_COMPLETION_PROVIDER_IDS = new Set([
14346
14513
  * section into the commands+skills popover. Extensions extend `/` via
14347
14514
  * `ctx.registerCommand`; a raw completion provider must pick its own char.
14348
14515
  */
14349
- const BUILTIN_COMPLETION_TRIGGERS = new Set([
14516
+ const BUILTIN_COMPLETION_TRIGGERS = /* @__PURE__ */ new Set([
14350
14517
  "/",
14351
14518
  "@",
14352
14519
  "#"
@@ -14362,6 +14529,12 @@ const BUILTIN_COMPLETION_TRIGGERS = new Set([
14362
14529
  */
14363
14530
  const SKILL_ACTIVATION_TIMEOUT_MS = 60 * 1e3;
14364
14531
  /**
14532
+ * Poll cadence for out-of-band extension install detection (see the
14533
+ * auto-reload effect near `runningRef`). A scan is a handful of readdirs
14534
+ * and lstats — cheap enough that latency, not cost, picked the number.
14535
+ */
14536
+ const EXTENSION_WATCH_POLL_MS = 3e3;
14537
+ /**
14365
14538
  * Surface failures that are normally silenced (teardown / save) when the
14366
14539
  * `ZIDANE_DEBUG` env var is set. Logging via `console.error` would otherwise
14367
14540
  * trigger OpenTUI's error console overlay and clutter the UI for end users.
@@ -14619,6 +14792,10 @@ function AppShell() {
14619
14792
  useEffect(() => {
14620
14793
  userInstructionsScopeRef.current = settings.userInstructionsScope;
14621
14794
  }, [settings.userInstructionsScope]);
14795
+ const subagentModelsRef = useRef(settings.subagentModels);
14796
+ useEffect(() => {
14797
+ subagentModelsRef.current = settings.subagentModels;
14798
+ }, [settings.subagentModels]);
14622
14799
  const autoCompactRef = useRef(settings.autoCompact);
14623
14800
  useEffect(() => {
14624
14801
  autoCompactRef.current = settings.autoCompact;
@@ -14965,6 +15142,14 @@ function AppShell() {
14965
15142
  const [events, setEvents] = useState([]);
14966
15143
  const [busy, setBusy] = useState(false);
14967
15144
  /**
15145
+ * The provider+model the run actually advanced to after a `model:fallback`
15146
+ * — `null` while the primary target is serving. Drives the footer's model
15147
+ * chip (actual model id, warn-colored to read as "temporary") and resets
15148
+ * at every run submission because fallback is run-sticky: each new run
15149
+ * starts from the primary again.
15150
+ */
15151
+ const [activeFallback, setActiveFallback] = useState(null);
15152
+ /**
14968
15153
  * Drives the compaction UI indicator — the small spinner painted over the
14969
15154
  * session title. Toggled by the loop-native `compact:start` / `compact:end`
14970
15155
  * hooks (and the manual `onCompactSession` flow).
@@ -15006,6 +15191,95 @@ function AppShell() {
15006
15191
  */
15007
15192
  const runningRef = useRef(false);
15008
15193
  /**
15194
+ * Auto-`/reload` on out-of-band extension installs. The Settings-panel
15195
+ * lifecycle (ctrl+g/u/x) already ends in a hot reload, but extensions
15196
+ * can also land on disk without the panel: the agent running an
15197
+ * installer through the shell tool mid-conversation, a second terminal,
15198
+ * a plain `git clone` into a scan root. Those used to sit invisible
15199
+ * until a manual `/reload` or restart.
15200
+ *
15201
+ * Detection is a low-frequency poll of `scanExtensionDirectories` — the
15202
+ * SAME entry enumeration discovery uses (trust boundaries, project/user
15203
+ * precedence, `ZIDANE_EXTENSION_DIRS`), so the watcher can't drift from
15204
+ * what a reload would actually see. The signature is the entry set
15205
+ * (name + resolved path), NOT file contents — in-place code edits don't
15206
+ * trip it; this reacts to extensions appearing/disappearing, exactly
15207
+ * the "installed a new extension" event. (fs.watch was rejected: scan
15208
+ * roots routinely don't exist yet, and watching parents-of-missing-dirs
15209
+ * across platforms costs more complexity than a 3s readdir poll.)
15210
+ *
15211
+ * Deliberately skipped while a run is in flight (reload mid-run would
15212
+ * dispose extension instances whose tools the agent may be holding) and
15213
+ * while a modal is open (the Settings install flow does its own reload;
15214
+ * polling under it would race a second discovery pass). Both gates
15215
+ * self-resolve — the next tick catches up. The modal gate is read
15216
+ * through a ref, NOT an effect dep: `useModal()`'s api object is
15217
+ * rebuilt on every open/close, and depending on it would re-seed the
15218
+ * baseline right as a modal closes — silently absorbing anything that
15219
+ * landed on disk while it was open.
15220
+ *
15221
+ * Trust posture matches `/reload` exactly: a newly appeared extension
15222
+ * still goes through the same enable/allowlist + project-trust rules —
15223
+ * auto-reload never auto-enables anything the manual path wouldn't.
15224
+ *
15225
+ * Known accepted gap: the baseline seeds from a fresh disk scan at
15226
+ * mount, so an install landing between boot discovery and shell mount
15227
+ * (seconds, e.g. while on the auth screen) is absorbed unloaded.
15228
+ * Seeding from the registry's own entry set would close it, but the
15229
+ * registry legitimately diverges from raw scan enumeration (trust
15230
+ * rejection, allowlists, selection dedupe) — any systematic divergence
15231
+ * would turn into a permanent reload-every-tick loop. Loop-proof beats
15232
+ * complete here: baseline and tick MUST come from the same function.
15233
+ */
15234
+ const modalRef = useRef(modal);
15235
+ modalRef.current = modal;
15236
+ const extensionAutoReloadInFlightRef = useRef(false);
15237
+ useEffect(() => {
15238
+ const scanSignature = () => {
15239
+ try {
15240
+ return scanExtensionDirectories({
15241
+ cwd: cwdRef.current,
15242
+ prefix: config.prefix,
15243
+ ...config.extensionScanPaths.length > 0 ? { extensionScanPaths: config.extensionScanPaths } : {}
15244
+ }).map((e) => `${e.name}\u0000${e.dir}`).sort().join("\n");
15245
+ } catch (err) {
15246
+ debugLog("extension watch: scan failed", err);
15247
+ return null;
15248
+ }
15249
+ };
15250
+ let lastSignature = scanSignature();
15251
+ const id = setInterval(() => {
15252
+ if (extensionAutoReloadInFlightRef.current || runningRef.current || modalRef.current.isOpen) return;
15253
+ const next = scanSignature();
15254
+ if (next === null) return;
15255
+ if (lastSignature === null) {
15256
+ lastSignature = next;
15257
+ return;
15258
+ }
15259
+ if (next === lastSignature) return;
15260
+ lastSignature = next;
15261
+ extensionAutoReloadInFlightRef.current = true;
15262
+ (async () => {
15263
+ try {
15264
+ const { active, inactive } = await reloadExtensionsRef.current();
15265
+ streamRef.current?.appendImmediate({
15266
+ kind: "info",
15267
+ text: `\u{1F50C} Extensions changed on disk — reloaded: ${active} active${inactive > 0 ? `, ${inactive} inactive (enable in Settings → Extensions)` : ""}. Agent-side contributions apply on the next run.`
15268
+ });
15269
+ } catch (err) {
15270
+ debugLog("extension watch: auto-reload failed", err);
15271
+ } finally {
15272
+ extensionAutoReloadInFlightRef.current = false;
15273
+ }
15274
+ })();
15275
+ }, EXTENSION_WATCH_POLL_MS);
15276
+ return () => clearInterval(id);
15277
+ }, [
15278
+ extensionRegistry,
15279
+ config.prefix,
15280
+ config.extensionScanPaths
15281
+ ]);
15282
+ /**
15009
15283
  * Auto-wake trigger for background-task notifications. When a
15010
15284
  * backgrounded shell task exits while the agent is IDLE, nothing would
15011
15285
  * otherwise deliver the queued `<task-notification>` until the user
@@ -15106,16 +15380,21 @@ function AppShell() {
15106
15380
  saveTerminalTitle();
15107
15381
  titleSavedRef.current = true;
15108
15382
  }
15109
- const sessionTitle = currentSession?.title?.trim() || "zidane";
15110
- let title;
15111
- if (settings.terminalTitle === "app") title = "zidane";
15112
- else if (settings.terminalTitle === "icon-session") title = `${settings.sessionIcon.trim() || "⚽"} · ${sessionTitle}`;
15113
- else title = sessionTitle;
15383
+ const title = composeTerminalTitle({
15384
+ mode: settings.terminalTitle,
15385
+ sessionTitle: currentSession?.title,
15386
+ sessionIcon: settings.sessionIcon,
15387
+ attentionIcon: settings.attentionIcon,
15388
+ awaitingInput: pendingApproval !== null || pendingInteraction !== null
15389
+ });
15114
15390
  renderer.setTerminalTitle(sanitizeTitle(title));
15115
15391
  }, [
15116
15392
  renderer,
15117
15393
  settings.terminalTitle,
15118
15394
  settings.sessionIcon,
15395
+ settings.attentionIcon,
15396
+ pendingApproval,
15397
+ pendingInteraction,
15119
15398
  currentSession?.title,
15120
15399
  restoreTitle
15121
15400
  ]);
@@ -15327,9 +15606,14 @@ function AppShell() {
15327
15606
  const model = modelId ?? remembered ?? descriptor.defaultModel ?? descriptor.factory().meta.defaultModel;
15328
15607
  const effort = effortForModel(descriptor, model, initialState.lastEffortByModel);
15329
15608
  const opts = restoreModelOptions(descriptor, model, initialState.lastModelOptionsByModel);
15609
+ const fallbacks = restoreChainFallbacks(initialState.lastModelChain, {
15610
+ provider: provider.key,
15611
+ model
15612
+ }, (k) => providerRegistry[k] !== void 0);
15330
15613
  return {
15331
15614
  provider,
15332
15615
  model,
15616
+ ...fallbacks.length > 0 ? { fallbacks } : {},
15333
15617
  ...effort ? { effort } : {},
15334
15618
  ...opts ? { modelOptions: opts } : {}
15335
15619
  };
@@ -15343,9 +15627,66 @@ function AppShell() {
15343
15627
  suppressWakeRef.current = true;
15344
15628
  agentRef.current?.abort();
15345
15629
  }, [denyAll, interactions]);
15630
+ /**
15631
+ * `ProviderKey` → runtime `Provider.name` for the providers constructed
15632
+ * into the CURRENT agent. Replaced wholesale on every `buildAgent` so a
15633
+ * run never pins a fallback entry to a provider the live agent doesn't
15634
+ * own (`buildRunModel` drops entries with no mapping). Chain entries
15635
+ * carry chat-level keys; the agent loop matches on instance names —
15636
+ * this map is the bridge.
15637
+ */
15638
+ const providerNameByKeyRef = useRef(/* @__PURE__ */ new Map());
15639
+ /**
15640
+ * The `model` option for chat runs: the bare primary model id when no
15641
+ * fallbacks are configured (byte-identical to single-model behavior),
15642
+ * else the pinned `{ provider, model }` chain — validated against the
15643
+ * current agent's provider map so a stale chain degrades to fewer
15644
+ * targets instead of a run-time resolution error. Each target carries
15645
+ * its model's context window from the chat catalog so the loop re-bases
15646
+ * the auto-compact trigger when a fallback advances (the run-level
15647
+ * `autoCompact.contextWindow` is sized for the primary).
15648
+ */
15649
+ const runModelFor = useCallback((p) => {
15650
+ const fallbacks = p.fallbacks ?? [];
15651
+ if (fallbacks.length === 0) return p.model;
15652
+ return buildRunModel([{
15653
+ provider: p.provider.key,
15654
+ model: p.model
15655
+ }, ...fallbacks], (k) => providerNameByKeyRef.current.get(k), (entry) => {
15656
+ const d = providerRegistry[entry.provider];
15657
+ return d ? getContextWindow(d, entry.model) ?? void 0 : void 0;
15658
+ });
15659
+ }, [providerRegistry]);
15346
15660
  const buildAgent = useCallback((session, key) => {
15347
15661
  const descriptor = providerRegistry[key];
15348
15662
  if (!descriptor) throw new Error(`No provider registered for key "${key}"`);
15663
+ const primaryProvider = descriptor.factory();
15664
+ const nameByKey = /* @__PURE__ */ new Map([[key, primaryProvider.name]]);
15665
+ const extraProviders = [];
15666
+ const seenNames = /* @__PURE__ */ new Set([primaryProvider.name]);
15667
+ for (const entry of pickedRef.current?.fallbacks ?? []) {
15668
+ if (nameByKey.has(entry.provider)) continue;
15669
+ const d = providerRegistry[entry.provider];
15670
+ if (!d) {
15671
+ debugLog("buildAgent: fallback provider not registered, skipping", entry.provider);
15672
+ continue;
15673
+ }
15674
+ let instance;
15675
+ try {
15676
+ instance = d.factory();
15677
+ } catch (err) {
15678
+ debugLog(`buildAgent: fallback provider "${entry.provider}" factory threw, skipping`, err);
15679
+ continue;
15680
+ }
15681
+ if (seenNames.has(instance.name)) {
15682
+ debugLog(`buildAgent: duplicate provider name "${instance.name}", skipping fallback provider`, entry.provider);
15683
+ continue;
15684
+ }
15685
+ seenNames.add(instance.name);
15686
+ nameByKey.set(entry.provider, instance.name);
15687
+ extraProviders.push(instance);
15688
+ }
15689
+ providerNameByKeyRef.current = nameByKey;
15349
15690
  const profile = pickedAgentRef.current;
15350
15691
  const skillsScan = defaultSkillScanPaths({
15351
15692
  cwd: projectDir,
@@ -15356,7 +15697,7 @@ function AppShell() {
15356
15697
  const userEnabledSkills = enabledSkillsRef.current;
15357
15698
  const skillsConfig = buildSkillsConfig({
15358
15699
  scan: skillsScan,
15359
- enabled: userEnabledSkills === void 0 ? void 0 : Array.from(new Set([...userEnabledSkills, ...extensionSkillNames]))
15700
+ enabled: userEnabledSkills === void 0 ? void 0 : Array.from(/* @__PURE__ */ new Set([...userEnabledSkills, ...extensionSkillNames]))
15360
15701
  });
15361
15702
  const projectMcps = buildMcpServers({
15362
15703
  discovered: mcpsCatalogRef.current,
@@ -15391,6 +15732,20 @@ function AppShell() {
15391
15732
  sessionId: session.id
15392
15733
  });
15393
15734
  const composedPreset = composeExtensionPreset(liveExtensionRegistry, profile.preset);
15735
+ const modelCatalogTools = {};
15736
+ if (subagentModelsRef.current) {
15737
+ const catalog = [];
15738
+ for (const [providerKey, providerName] of nameByKey) for (const info of modelsFor(providerKey)) catalog.push({
15739
+ provider: providerName,
15740
+ model: info
15741
+ });
15742
+ const presetSpawn = composedPreset.tools?.spawn;
15743
+ if (catalog.length > 0 && presetSpawn && isSpawnTool(presetSpawn)) {
15744
+ modelCatalogTools.spawn = presetSpawn.withModels(catalog);
15745
+ if (composedPreset.tools && "model_search" in composedPreset.tools) debugLog("buildAgent: preset already registers a \"model_search\" tool — shadowing it with the spawn model catalog", "model_search");
15746
+ modelCatalogTools.model_search = createModelSearchTool(catalog);
15747
+ }
15748
+ }
15394
15749
  const execution = launch?.context === "docker" ? createDockerContext({
15395
15750
  image: launch.image,
15396
15751
  cwd: actualCwd
@@ -15413,7 +15768,8 @@ function AppShell() {
15413
15768
  tools: {
15414
15769
  ...interactionTools,
15415
15770
  chatTurnsRead: chatTurnsReadTool,
15416
- ...composedPreset.tools ?? {}
15771
+ ...composedPreset.tools ?? {},
15772
+ ...modelCatalogTools
15417
15773
  },
15418
15774
  contextBreakdownSections: [...liveExtensionRegistry.contextBreakdownSections, ...liveExtensionRegistry.contextPanelSections],
15419
15775
  behavior: {
@@ -15422,7 +15778,7 @@ function AppShell() {
15422
15778
  tasksDir
15423
15779
  },
15424
15780
  execution,
15425
- provider: descriptor.factory(),
15781
+ provider: extraProviders.length > 0 ? [primaryProvider, ...extraProviders] : primaryProvider,
15426
15782
  session,
15427
15783
  mcpConnector: (configs) => connectMcpServers(configs, void 0, agent.hooks, { buildAuthProvider: (cfg) => new McpOAuthProvider({
15428
15784
  name: cfg.name,
@@ -15613,6 +15969,16 @@ function AppShell() {
15613
15969
  turnId
15614
15970
  });
15615
15971
  });
15972
+ agent.hooks.hook("model:fallback", ({ from, to, err }) => {
15973
+ setActiveFallback({
15974
+ provider: to.provider,
15975
+ model: to.model
15976
+ });
15977
+ stream.appendImmediate({
15978
+ kind: "info",
15979
+ text: `⚠ model fallback: ${from.provider}/${from.model} failed (${errorMessage(err)}) — continuing on ${to.provider}/${to.model}`
15980
+ });
15981
+ });
15616
15982
  agent.hooks.hook("tool:before", async ({ callId, name, input, turnId }) => {
15617
15983
  registerInFlightTool({
15618
15984
  callId,
@@ -15736,6 +16102,10 @@ function AppShell() {
15736
16102
  }
15737
16103
  stream.flushAndUpdate(finalizeStreamingMarkdown);
15738
16104
  });
16105
+ agent.hooks.hook("child:turn:after", ({ usage }) => {
16106
+ const turnCost = usage.cost ?? 0;
16107
+ if (turnCost > 0) setSessionCost((prev) => prev + turnCost);
16108
+ });
15739
16109
  agent.hooks.hook("agent:done", () => {
15740
16110
  const reason = lastFinishReasonRef.current;
15741
16111
  lastFinishReasonRef.current = void 0;
@@ -15781,13 +16151,14 @@ function AppShell() {
15781
16151
  setLastInputTokens(ctx.effectiveTokens);
15782
16152
  lastInputTokensRef.current = ctx.effectiveTokens;
15783
16153
  });
15784
- agent.hooks.hook("spawn:before", ({ id, task, depth }) => {
16154
+ agent.hooks.hook("spawn:before", ({ id, task, depth, model }) => {
15785
16155
  const taskPreview = previewLine(task, 80);
15786
16156
  stream.appendImmediate({
15787
16157
  kind: "spawn-start",
15788
16158
  text: taskPreview,
15789
16159
  childId: id,
15790
- depth: depth ?? 1
16160
+ depth: depth ?? 1,
16161
+ ...model ? { model } : {}
15791
16162
  });
15792
16163
  });
15793
16164
  agent.hooks.hook("spawn:complete", ({ id, depth, status, stats }) => {
@@ -15893,6 +16264,7 @@ function AppShell() {
15893
16264
  return agent;
15894
16265
  }, [
15895
16266
  providerRegistry,
16267
+ modelsFor,
15896
16268
  stream,
15897
16269
  gateDecision,
15898
16270
  cancelRunOnDenial,
@@ -15908,7 +16280,8 @@ function AppShell() {
15908
16280
  extensionRegistry
15909
16281
  ]);
15910
16282
  const refreshSessions = useCallback(async () => {
15911
- const list = await listSessionMeta(store, settings.showAllProjects ? void 0 : { projectRoot: projectDir });
16283
+ const filter = settings.showAllProjects ? void 0 : { projectRoot: projectDir };
16284
+ const list = await listSessionMeta(store, filter);
15912
16285
  setSessions(list);
15913
16286
  return list;
15914
16287
  }, [
@@ -16015,10 +16388,11 @@ function AppShell() {
16015
16388
  setEvents(capTranscriptEvents(eventsFromTurns(session.turns, session.runs)));
16016
16389
  runningRef.current = true;
16017
16390
  setBusy(true);
16391
+ setActiveFallback(null);
16018
16392
  try {
16019
16393
  const runModelOptions = enabledModelOptions(currentPicked.modelOptions);
16020
16394
  await agent.run({
16021
- model: currentPicked.model,
16395
+ model: runModelFor(currentPicked),
16022
16396
  ...currentPicked.effort ? { thinking: currentPicked.effort } : {},
16023
16397
  ...Object.keys(runModelOptions).length > 0 ? { modelOptions: runModelOptions } : {},
16024
16398
  behavior: {
@@ -16060,8 +16434,10 @@ function AppShell() {
16060
16434
  sessionRef.current = session;
16061
16435
  agentRef.current = buildAgent(session, key);
16062
16436
  if (extensionRegistrySwapKeyRef.current === null || extensionRegistrySwapKeyRef.current === extensionRegistry) pendingAgentRebuildRef.current = false;
16063
- setActiveSkillNames(readPinnedSkills(session.metadata["zidane.activeSkills"]));
16437
+ const persistedPins = readPinnedSkills(session.metadata["zidane.activeSkills"]);
16438
+ setActiveSkillNames(persistedPins);
16064
16439
  setEvents(capTranscriptEvents(eventsFromTurns(session.turns, session.runs)));
16440
+ setActiveFallback(null);
16065
16441
  const replayedTokens = lastContextSizeFromTurns(session.turns, session.runs);
16066
16442
  setLastInputTokens(replayedTokens);
16067
16443
  lastInputTokensRef.current = replayedTokens;
@@ -16149,7 +16525,8 @@ function AppShell() {
16149
16525
  ]);
16150
16526
  const [availableProviders, setAvailableProviders] = useState([]);
16151
16527
  const refreshAvailableProviders = useCallback(() => {
16152
- setAvailableProviders(detectAuth(config.paths.userDir, providerRegistry).filter((p) => p.available));
16528
+ const detected = detectAuth(config.paths.userDir, providerRegistry);
16529
+ setAvailableProviders(detected.filter((p) => p.available));
16153
16530
  }, [config.paths.userDir, providerRegistry]);
16154
16531
  useEffect(() => {
16155
16532
  refreshAvailableProviders();
@@ -16456,30 +16833,36 @@ function AppShell() {
16456
16833
  else setQueueSelectionIndex(Math.min(pushIdx, messageQueueRef.current.length - 1));
16457
16834
  }, []);
16458
16835
  /**
16459
- * Pick a `{ providerKey, modelId }` tuple from the cross-provider
16460
- * model picker. Two branches:
16836
+ * Apply a model chain from the cross-provider picker's ordered
16837
+ * multi-select. Entry 0 is the PRIMARY (same role as the old
16838
+ * single-pick: provider swap, effort, options); entries 1+ become
16839
+ * run-level fallbacks (`agent.run({ model: [...] })`).
16461
16840
  *
16462
- * - **Same provider, different model** update `picked.model` +
16463
- * remember it in `lastModelByProvider`. The active agent keeps
16464
- * running; `agent.run({ model })` accepts the model per-call so
16465
- * no session rebuild is needed.
16466
- * - **Different provider** — swap the active `ProviderAuth`,
16467
- * persist BOTH `lastProvider` + `lastModelByProvider`, then
16468
- * re-activate the current session against the new provider's
16469
- * factory. The session id (and conversation history) is
16470
- * preserved; only the bound agent + provider instance change.
16841
+ * Persistence mirrors the old single-pick path (`lastProvider` +
16842
+ * `lastModelByProvider` track the primary) plus the additive
16843
+ * `lastModelChain`. Fallback entries are re-validated against the
16844
+ * live auth list; stale entries drop with a debug log.
16471
16845
  *
16472
- * Either branch re-resolves the reasoning effort: a non-reasoning
16473
- * model loses its `effort`, a reasoning model gets the remembered
16474
- * per-model value (falling back to a sensible default).
16846
+ * The session re-activates (agent rebuild) when the primary provider
16847
+ * changed OR the chain's provider-key SET changed the agent must
16848
+ * own an instance for every chain provider. A pure model / order
16849
+ * change within the same providers stays a per-run swap, exactly
16850
+ * like same-provider single picks always have.
16475
16851
  */
16476
- const onPickModel = useCallback(async (next) => {
16477
- const nextProvider = availableProviders.find((p) => p.key === next.providerKey);
16852
+ const onApplyModelChain = useCallback(async (chain) => {
16853
+ const primary = chain[0];
16854
+ if (!primary) {
16855
+ modal.close();
16856
+ return;
16857
+ }
16858
+ const nextProvider = availableProviders.find((p) => p.key === primary.provider);
16478
16859
  if (!nextProvider) {
16479
- debugLog("onPickModel: unknown provider key", next.providerKey);
16860
+ debugLog("onApplyModelChain: unknown provider key", primary.provider);
16480
16861
  modal.close();
16481
16862
  return;
16482
16863
  }
16864
+ const fallbacks = filterChainFallbacks(chain.slice(1), primary, (k) => availableProviders.some((p) => p.key === k) && providerRegistry[k] !== void 0);
16865
+ if (fallbacks.length < chain.length - 1) debugLog("onApplyModelChain: dropped unavailable/duplicate fallback entries", chain.length - 1 - fallbacks.length);
16483
16866
  const descriptor = providerRegistry[nextProvider.key];
16484
16867
  const prior = stateStore.load();
16485
16868
  const providerChanged = picked?.provider.key !== nextProvider.key;
@@ -16488,19 +16871,34 @@ function AppShell() {
16488
16871
  ...providerChanged ? { lastProvider: nextProvider.key } : {},
16489
16872
  lastModelByProvider: {
16490
16873
  ...prior.lastModelByProvider,
16491
- [nextProvider.key]: next.modelId
16492
- }
16874
+ [nextProvider.key]: primary.model
16875
+ },
16876
+ lastModelChain: [{
16877
+ provider: primary.provider,
16878
+ model: primary.model
16879
+ }, ...fallbacks]
16493
16880
  });
16494
- const nextEffort = descriptor ? effortForModel(descriptor, next.modelId, prior.lastEffortByModel) : void 0;
16495
- const nextOptions = descriptor ? restoreModelOptions(descriptor, next.modelId, prior.lastModelOptionsByModel) : void 0;
16496
- setPicked({
16881
+ const nextEffort = descriptor ? effortForModel(descriptor, primary.model, prior.lastEffortByModel) : void 0;
16882
+ const nextOptions = descriptor ? restoreModelOptions(descriptor, primary.model, prior.lastModelOptionsByModel) : void 0;
16883
+ const updated = {
16497
16884
  provider: nextProvider,
16498
- model: next.modelId,
16885
+ model: primary.model,
16886
+ ...fallbacks.length > 0 ? { fallbacks } : {},
16499
16887
  ...nextEffort ? { effort: nextEffort } : {},
16500
16888
  ...nextOptions ? { modelOptions: nextOptions } : {}
16501
- });
16889
+ };
16890
+ const prevChain = picked ? [{
16891
+ provider: picked.provider.key,
16892
+ model: picked.model
16893
+ }, ...picked.fallbacks ?? []] : [];
16894
+ setPicked(updated);
16895
+ setActiveFallback(null);
16896
+ pickedRef.current = updated;
16502
16897
  modal.close();
16503
- if (providerChanged && currentSession && !busy && !pendingApproval) await activateSession(currentSession.id, nextProvider.key);
16898
+ if ((providerChanged || providerKeySetChanged(prevChain, [{
16899
+ provider: primary.provider,
16900
+ model: primary.model
16901
+ }, ...fallbacks])) && currentSession && !busy && !pendingApproval) await activateSession(currentSession.id, nextProvider.key);
16504
16902
  }, [
16505
16903
  availableProviders,
16506
16904
  providerRegistry,
@@ -16535,7 +16933,8 @@ function AppShell() {
16535
16933
  modelsFor,
16536
16934
  current,
16537
16935
  onPick: (next) => {
16538
- setSetting("compactionModel", next.providerKey === picked.provider.key && next.modelId === picked.model ? "" : `${next.providerKey}/${next.modelId}`);
16936
+ const isMain = next.providerKey === picked.provider.key && next.modelId === picked.model;
16937
+ setSetting("compactionModel", isMain ? "" : `${next.providerKey}/${next.modelId}`);
16539
16938
  modal.close();
16540
16939
  }
16541
16940
  }));
@@ -16676,7 +17075,7 @@ function AppShell() {
16676
17075
  });
16677
17076
  }
16678
17077
  const newRefs = uniqueSkillNamesFromReferences(references);
16679
- const skillNames = Array.from(new Set([...activeSkillNamesRef.current, ...newRefs]));
17078
+ const skillNames = Array.from(/* @__PURE__ */ new Set([...activeSkillNamesRef.current, ...newRefs]));
16680
17079
  for (const name of skillNames) try {
16681
17080
  await withTimeout(agent.activateSkill(name), {
16682
17081
  operation: `activateSkill(${name})`,
@@ -16713,8 +17112,9 @@ function AppShell() {
16713
17112
  };
16714
17113
  })];
16715
17114
  const runModelOptions = enabledModelOptions(picked.modelOptions);
17115
+ setActiveFallback(null);
16716
17116
  await agent.run({
16717
- model: picked.model,
17117
+ model: runModelFor(picked),
16718
17118
  prompt: runPrompt,
16719
17119
  ...picked.effort ? { thinking: picked.effort } : {},
16720
17120
  ...Object.keys(runModelOptions).length > 0 ? { modelOptions: runModelOptions } : {},
@@ -16848,8 +17248,9 @@ function AppShell() {
16848
17248
  const before = session.turns.length;
16849
17249
  const beforeRuns = session.runs.length;
16850
17250
  const runModelOptions = enabledModelOptions(picked.modelOptions);
17251
+ setActiveFallback(null);
16851
17252
  await agent.run({
16852
- model: picked.model,
17253
+ model: runModelFor(picked),
16853
17254
  prompt: agentPrompt,
16854
17255
  ...picked.effort ? { thinking: picked.effort } : {},
16855
17256
  ...Object.keys(runModelOptions).length > 0 ? { modelOptions: runModelOptions } : {},
@@ -17256,7 +17657,8 @@ function AppShell() {
17256
17657
  if (!session || !p) return;
17257
17658
  const stale = agentRef.current;
17258
17659
  const staleWarnLog = warnLogRef.current;
17259
- agentRef.current = buildAgentRef.current(session, p.provider.key);
17660
+ const fresh = buildAgentRef.current(session, p.provider.key);
17661
+ agentRef.current = fresh;
17260
17662
  if (stale) await stale.destroy().catch((err) => debugLog("rebuildActiveAgent: destroy failed", err));
17261
17663
  if (staleWarnLog) await staleWarnLog.close().catch((err) => debugLog("rebuildActiveAgent: warnLog.close failed", err));
17262
17664
  }, []);
@@ -17353,7 +17755,8 @@ function AppShell() {
17353
17755
  ]);
17354
17756
  const onLogoutMcp = useCallback((name) => {
17355
17757
  mcpCredentialStore.delete(name);
17356
- dispatchAuth(mcpsCatalogRef.current.find((d) => d.config.name === name)?.config.auth === "oauth" ? {
17758
+ const entry = mcpsCatalogRef.current.find((d) => d.config.name === name);
17759
+ dispatchAuth(entry?.config.auth === "oauth" ? {
17357
17760
  type: "auth-required",
17358
17761
  name,
17359
17762
  reason: "no-tokens"
@@ -17415,7 +17818,8 @@ function AppShell() {
17415
17818
  const idx = turnIds.indexOf(prev);
17416
17819
  if (idx === -1) return turnIds[turnIds.length - 1];
17417
17820
  const len = turnIds.length;
17418
- return turnIds[((idx + direction) % len + len) % len];
17821
+ const nextIdx = ((idx + direction) % len + len) % len;
17822
+ return turnIds[nextIdx];
17419
17823
  });
17420
17824
  }, [turnIds]);
17421
17825
  const onForkTurn = useCallback(async (turnId) => {
@@ -17929,7 +18333,11 @@ function AppShell() {
17929
18333
  providerKey: picked.provider.key,
17930
18334
  modelId: picked.model
17931
18335
  },
17932
- onPick: onPickModel
18336
+ chain: [{
18337
+ provider: picked.provider.key,
18338
+ model: picked.model
18339
+ }, ...picked.fallbacks ?? []],
18340
+ onApplyChain: onApplyModelChain
17933
18341
  }));
17934
18342
  return;
17935
18343
  }
@@ -17964,7 +18372,13 @@ function AppShell() {
17964
18372
  autocompactBuffer: OUTPUT_RESERVE_TOKENS,
17965
18373
  ...settings.autoCompact && settings.autoCompactThreshold ? { compactThreshold: settings.autoCompactThreshold } : {}
17966
18374
  }).then((breakdown) => {
17967
- if (breakdown && agentRef.current === agent) modal.open(/* @__PURE__ */ jsx$1(ContextPanelModal, { breakdown }));
18375
+ if (breakdown && agentRef.current === agent) {
18376
+ const modelUsage = usageByModel(sessionRef.current?.runs ?? []);
18377
+ modal.open(/* @__PURE__ */ jsx$1(ContextPanelModal, {
18378
+ breakdown,
18379
+ modelUsage
18380
+ }));
18381
+ }
17968
18382
  }).catch((err) => debugLog("getContextBreakdown failed", err));
17969
18383
  return;
17970
18384
  }
@@ -18135,8 +18549,8 @@ function AppShell() {
18135
18549
  currentSession,
18136
18550
  hasMultipleAgents,
18137
18551
  uiMode: settings.uiMode,
18138
- modelLabel: picked?.model ?? null,
18139
- modelColor: COLOR.model,
18552
+ modelLabel: activeFallback ? activeFallback.model : picked ? `${picked.model}${picked.fallbacks && picked.fallbacks.length > 0 ? ` +${picked.fallbacks.length}` : ""}` : null,
18553
+ modelColor: activeFallback ? COLOR.warn : COLOR.model,
18140
18554
  effortLabel: modelHasReasoning ? picked?.effort ?? "medium" : null,
18141
18555
  effortColor: COLOR.warn,
18142
18556
  modelOptionsLabel: (() => {