zidane 6.0.1 → 6.1.1

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-DK382vzL.js} +8 -8
  3. package/dist/{acp-Irby04Us.js.map → acp-DK382vzL.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-Bq32ZJSo.js} +392 -63
  9. package/dist/agent-Bq32ZJSo.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-CV_OSYVD.js} +10 -9
  13. package/dist/anthropic-CV_OSYVD.js.map +1 -0
  14. package/dist/atomic-write-B7zr1ddF.js.map +1 -1
  15. package/dist/{auth-BDSu_0t3.js → auth-D5FfJtLf.js} +7 -7
  16. package/dist/auth-D5FfJtLf.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-C6Il4kBD.d.ts} +388 -5
  57. package/dist/index-C6Il4kBD.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-Ce4yRjd3.js} +2 -2
  68. package/dist/{messages-CwujiS74.js.map → messages-Ce4yRjd3.js.map} +1 -1
  69. package/dist/oauth-8LqbeI_Q.js.map +1 -1
  70. package/dist/{openai-compat-BvgYGSR1.js → openai-compat-TA7aihJ-.js} +6 -4
  71. package/dist/openai-compat-TA7aihJ-.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-Dln69sQ_.js} +3 -3
  100. package/dist/{providers-z6LDMAr3.js.map → providers-Dln69sQ_.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-D7JSEMME.js} +3 -3
  114. package/dist/session-D7JSEMME.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-ETbcC9DM.d.ts} +2 -2
  123. package/dist/tool-formatters-ETbcC9DM.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-CBaghfwl.js} +364 -96
  133. package/dist/transcript-anchors-CBaghfwl.js.map +1 -0
  134. package/dist/{transcript-anchors-Cb-bDbNH.d.ts → transcript-anchors-e_30Kvk0.d.ts} +37 -3
  135. package/dist/transcript-anchors-e_30Kvk0.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 +505 -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-D-aaiUOo.d.ts} +27 -9
  143. package/dist/turn-operations-D-aaiUOo.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-kGdhWmds.js} +3 -3
  147. package/dist/{xai-B_e6TOA8.js.map → xai-kGdhWmds.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-CBaghfwl.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-Bq32ZJSo.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-D7JSEMME.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-D5FfJtLf.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" && inp.key !== "activeIcon" || 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,32 @@ 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 reflects the
13842
+ * agent's state, most-urgent first:
13843
+ * 1. `attentionIcon` (⚠️ when empty) while the agent is blocked on
13844
+ * user input (tool approval / `ask_user` / `present_plan`) — a
13845
+ * backgrounded tab flags "zidane needs you". Wins over `active`
13846
+ * because an approval mid-run blocks the stream anyway.
13847
+ * 2. `activeIcon` (⚡ when empty) while a run is streaming — the
13848
+ * tab reads "working" at a glance.
13849
+ * 3. `sessionIcon` (⚽ when empty) — idle.
13850
+ *
13851
+ * Callers still pass the result through {@link sanitizeTitle} — icons
13852
+ * and session titles are both free text.
13853
+ */
13854
+ function composeTerminalTitle(opts) {
13855
+ const sessionTitle = opts.sessionTitle?.trim() || "zidane";
13856
+ if (opts.mode === "app") return "zidane";
13857
+ if (opts.mode === "session") return sessionTitle;
13858
+ return `${opts.awaitingInput ? opts.attentionIcon.trim() || "⚠️" : opts.active ? opts.activeIcon.trim() || "⚡" : opts.sessionIcon.trim() || "⚽"} · ${sessionTitle}`;
13859
+ }
13860
+ /**
13690
13861
  * Push the current terminal title so {@link restoreTerminalTitle} can
13691
13862
  * bring it back later. No-op when `out` isn't a TTY.
13692
13863
  */
@@ -14333,7 +14504,7 @@ function blockSummary(turn) {
14333
14504
  }
14334
14505
  //#endregion
14335
14506
  //#region src/tui/app.tsx
14336
- const BUILTIN_COMPLETION_PROVIDER_IDS = new Set([
14507
+ const BUILTIN_COMPLETION_PROVIDER_IDS = /* @__PURE__ */ new Set([
14337
14508
  "commands",
14338
14509
  "skills",
14339
14510
  "files",
@@ -14346,7 +14517,7 @@ const BUILTIN_COMPLETION_PROVIDER_IDS = new Set([
14346
14517
  * section into the commands+skills popover. Extensions extend `/` via
14347
14518
  * `ctx.registerCommand`; a raw completion provider must pick its own char.
14348
14519
  */
14349
- const BUILTIN_COMPLETION_TRIGGERS = new Set([
14520
+ const BUILTIN_COMPLETION_TRIGGERS = /* @__PURE__ */ new Set([
14350
14521
  "/",
14351
14522
  "@",
14352
14523
  "#"
@@ -14362,6 +14533,12 @@ const BUILTIN_COMPLETION_TRIGGERS = new Set([
14362
14533
  */
14363
14534
  const SKILL_ACTIVATION_TIMEOUT_MS = 60 * 1e3;
14364
14535
  /**
14536
+ * Poll cadence for out-of-band extension install detection (see the
14537
+ * auto-reload effect near `runningRef`). A scan is a handful of readdirs
14538
+ * and lstats — cheap enough that latency, not cost, picked the number.
14539
+ */
14540
+ const EXTENSION_WATCH_POLL_MS = 3e3;
14541
+ /**
14365
14542
  * Surface failures that are normally silenced (teardown / save) when the
14366
14543
  * `ZIDANE_DEBUG` env var is set. Logging via `console.error` would otherwise
14367
14544
  * trigger OpenTUI's error console overlay and clutter the UI for end users.
@@ -14619,6 +14796,10 @@ function AppShell() {
14619
14796
  useEffect(() => {
14620
14797
  userInstructionsScopeRef.current = settings.userInstructionsScope;
14621
14798
  }, [settings.userInstructionsScope]);
14799
+ const subagentModelsRef = useRef(settings.subagentModels);
14800
+ useEffect(() => {
14801
+ subagentModelsRef.current = settings.subagentModels;
14802
+ }, [settings.subagentModels]);
14622
14803
  const autoCompactRef = useRef(settings.autoCompact);
14623
14804
  useEffect(() => {
14624
14805
  autoCompactRef.current = settings.autoCompact;
@@ -14965,6 +15146,14 @@ function AppShell() {
14965
15146
  const [events, setEvents] = useState([]);
14966
15147
  const [busy, setBusy] = useState(false);
14967
15148
  /**
15149
+ * The provider+model the run actually advanced to after a `model:fallback`
15150
+ * — `null` while the primary target is serving. Drives the footer's model
15151
+ * chip (actual model id, warn-colored to read as "temporary") and resets
15152
+ * at every run submission because fallback is run-sticky: each new run
15153
+ * starts from the primary again.
15154
+ */
15155
+ const [activeFallback, setActiveFallback] = useState(null);
15156
+ /**
14968
15157
  * Drives the compaction UI indicator — the small spinner painted over the
14969
15158
  * session title. Toggled by the loop-native `compact:start` / `compact:end`
14970
15159
  * hooks (and the manual `onCompactSession` flow).
@@ -15006,6 +15195,95 @@ function AppShell() {
15006
15195
  */
15007
15196
  const runningRef = useRef(false);
15008
15197
  /**
15198
+ * Auto-`/reload` on out-of-band extension installs. The Settings-panel
15199
+ * lifecycle (ctrl+g/u/x) already ends in a hot reload, but extensions
15200
+ * can also land on disk without the panel: the agent running an
15201
+ * installer through the shell tool mid-conversation, a second terminal,
15202
+ * a plain `git clone` into a scan root. Those used to sit invisible
15203
+ * until a manual `/reload` or restart.
15204
+ *
15205
+ * Detection is a low-frequency poll of `scanExtensionDirectories` — the
15206
+ * SAME entry enumeration discovery uses (trust boundaries, project/user
15207
+ * precedence, `ZIDANE_EXTENSION_DIRS`), so the watcher can't drift from
15208
+ * what a reload would actually see. The signature is the entry set
15209
+ * (name + resolved path), NOT file contents — in-place code edits don't
15210
+ * trip it; this reacts to extensions appearing/disappearing, exactly
15211
+ * the "installed a new extension" event. (fs.watch was rejected: scan
15212
+ * roots routinely don't exist yet, and watching parents-of-missing-dirs
15213
+ * across platforms costs more complexity than a 3s readdir poll.)
15214
+ *
15215
+ * Deliberately skipped while a run is in flight (reload mid-run would
15216
+ * dispose extension instances whose tools the agent may be holding) and
15217
+ * while a modal is open (the Settings install flow does its own reload;
15218
+ * polling under it would race a second discovery pass). Both gates
15219
+ * self-resolve — the next tick catches up. The modal gate is read
15220
+ * through a ref, NOT an effect dep: `useModal()`'s api object is
15221
+ * rebuilt on every open/close, and depending on it would re-seed the
15222
+ * baseline right as a modal closes — silently absorbing anything that
15223
+ * landed on disk while it was open.
15224
+ *
15225
+ * Trust posture matches `/reload` exactly: a newly appeared extension
15226
+ * still goes through the same enable/allowlist + project-trust rules —
15227
+ * auto-reload never auto-enables anything the manual path wouldn't.
15228
+ *
15229
+ * Known accepted gap: the baseline seeds from a fresh disk scan at
15230
+ * mount, so an install landing between boot discovery and shell mount
15231
+ * (seconds, e.g. while on the auth screen) is absorbed unloaded.
15232
+ * Seeding from the registry's own entry set would close it, but the
15233
+ * registry legitimately diverges from raw scan enumeration (trust
15234
+ * rejection, allowlists, selection dedupe) — any systematic divergence
15235
+ * would turn into a permanent reload-every-tick loop. Loop-proof beats
15236
+ * complete here: baseline and tick MUST come from the same function.
15237
+ */
15238
+ const modalRef = useRef(modal);
15239
+ modalRef.current = modal;
15240
+ const extensionAutoReloadInFlightRef = useRef(false);
15241
+ useEffect(() => {
15242
+ const scanSignature = () => {
15243
+ try {
15244
+ return scanExtensionDirectories({
15245
+ cwd: cwdRef.current,
15246
+ prefix: config.prefix,
15247
+ ...config.extensionScanPaths.length > 0 ? { extensionScanPaths: config.extensionScanPaths } : {}
15248
+ }).map((e) => `${e.name}\u0000${e.dir}`).sort().join("\n");
15249
+ } catch (err) {
15250
+ debugLog("extension watch: scan failed", err);
15251
+ return null;
15252
+ }
15253
+ };
15254
+ let lastSignature = scanSignature();
15255
+ const id = setInterval(() => {
15256
+ if (extensionAutoReloadInFlightRef.current || runningRef.current || modalRef.current.isOpen) return;
15257
+ const next = scanSignature();
15258
+ if (next === null) return;
15259
+ if (lastSignature === null) {
15260
+ lastSignature = next;
15261
+ return;
15262
+ }
15263
+ if (next === lastSignature) return;
15264
+ lastSignature = next;
15265
+ extensionAutoReloadInFlightRef.current = true;
15266
+ (async () => {
15267
+ try {
15268
+ const { active, inactive } = await reloadExtensionsRef.current();
15269
+ streamRef.current?.appendImmediate({
15270
+ kind: "info",
15271
+ 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.`
15272
+ });
15273
+ } catch (err) {
15274
+ debugLog("extension watch: auto-reload failed", err);
15275
+ } finally {
15276
+ extensionAutoReloadInFlightRef.current = false;
15277
+ }
15278
+ })();
15279
+ }, EXTENSION_WATCH_POLL_MS);
15280
+ return () => clearInterval(id);
15281
+ }, [
15282
+ extensionRegistry,
15283
+ config.prefix,
15284
+ config.extensionScanPaths
15285
+ ]);
15286
+ /**
15009
15287
  * Auto-wake trigger for background-task notifications. When a
15010
15288
  * backgrounded shell task exits while the agent is IDLE, nothing would
15011
15289
  * otherwise deliver the queued `<task-notification>` until the user
@@ -15106,16 +15384,25 @@ function AppShell() {
15106
15384
  saveTerminalTitle();
15107
15385
  titleSavedRef.current = true;
15108
15386
  }
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;
15387
+ const title = composeTerminalTitle({
15388
+ mode: settings.terminalTitle,
15389
+ sessionTitle: currentSession?.title,
15390
+ sessionIcon: settings.sessionIcon,
15391
+ attentionIcon: settings.attentionIcon,
15392
+ activeIcon: settings.activeIcon,
15393
+ awaitingInput: pendingApproval !== null || pendingInteraction !== null,
15394
+ active: busy
15395
+ });
15114
15396
  renderer.setTerminalTitle(sanitizeTitle(title));
15115
15397
  }, [
15116
15398
  renderer,
15117
15399
  settings.terminalTitle,
15118
15400
  settings.sessionIcon,
15401
+ settings.attentionIcon,
15402
+ settings.activeIcon,
15403
+ busy,
15404
+ pendingApproval,
15405
+ pendingInteraction,
15119
15406
  currentSession?.title,
15120
15407
  restoreTitle
15121
15408
  ]);
@@ -15327,9 +15614,14 @@ function AppShell() {
15327
15614
  const model = modelId ?? remembered ?? descriptor.defaultModel ?? descriptor.factory().meta.defaultModel;
15328
15615
  const effort = effortForModel(descriptor, model, initialState.lastEffortByModel);
15329
15616
  const opts = restoreModelOptions(descriptor, model, initialState.lastModelOptionsByModel);
15617
+ const fallbacks = restoreChainFallbacks(initialState.lastModelChain, {
15618
+ provider: provider.key,
15619
+ model
15620
+ }, (k) => providerRegistry[k] !== void 0);
15330
15621
  return {
15331
15622
  provider,
15332
15623
  model,
15624
+ ...fallbacks.length > 0 ? { fallbacks } : {},
15333
15625
  ...effort ? { effort } : {},
15334
15626
  ...opts ? { modelOptions: opts } : {}
15335
15627
  };
@@ -15343,9 +15635,66 @@ function AppShell() {
15343
15635
  suppressWakeRef.current = true;
15344
15636
  agentRef.current?.abort();
15345
15637
  }, [denyAll, interactions]);
15638
+ /**
15639
+ * `ProviderKey` → runtime `Provider.name` for the providers constructed
15640
+ * into the CURRENT agent. Replaced wholesale on every `buildAgent` so a
15641
+ * run never pins a fallback entry to a provider the live agent doesn't
15642
+ * own (`buildRunModel` drops entries with no mapping). Chain entries
15643
+ * carry chat-level keys; the agent loop matches on instance names —
15644
+ * this map is the bridge.
15645
+ */
15646
+ const providerNameByKeyRef = useRef(/* @__PURE__ */ new Map());
15647
+ /**
15648
+ * The `model` option for chat runs: the bare primary model id when no
15649
+ * fallbacks are configured (byte-identical to single-model behavior),
15650
+ * else the pinned `{ provider, model }` chain — validated against the
15651
+ * current agent's provider map so a stale chain degrades to fewer
15652
+ * targets instead of a run-time resolution error. Each target carries
15653
+ * its model's context window from the chat catalog so the loop re-bases
15654
+ * the auto-compact trigger when a fallback advances (the run-level
15655
+ * `autoCompact.contextWindow` is sized for the primary).
15656
+ */
15657
+ const runModelFor = useCallback((p) => {
15658
+ const fallbacks = p.fallbacks ?? [];
15659
+ if (fallbacks.length === 0) return p.model;
15660
+ return buildRunModel([{
15661
+ provider: p.provider.key,
15662
+ model: p.model
15663
+ }, ...fallbacks], (k) => providerNameByKeyRef.current.get(k), (entry) => {
15664
+ const d = providerRegistry[entry.provider];
15665
+ return d ? getContextWindow(d, entry.model) ?? void 0 : void 0;
15666
+ });
15667
+ }, [providerRegistry]);
15346
15668
  const buildAgent = useCallback((session, key) => {
15347
15669
  const descriptor = providerRegistry[key];
15348
15670
  if (!descriptor) throw new Error(`No provider registered for key "${key}"`);
15671
+ const primaryProvider = descriptor.factory();
15672
+ const nameByKey = /* @__PURE__ */ new Map([[key, primaryProvider.name]]);
15673
+ const extraProviders = [];
15674
+ const seenNames = /* @__PURE__ */ new Set([primaryProvider.name]);
15675
+ for (const entry of pickedRef.current?.fallbacks ?? []) {
15676
+ if (nameByKey.has(entry.provider)) continue;
15677
+ const d = providerRegistry[entry.provider];
15678
+ if (!d) {
15679
+ debugLog("buildAgent: fallback provider not registered, skipping", entry.provider);
15680
+ continue;
15681
+ }
15682
+ let instance;
15683
+ try {
15684
+ instance = d.factory();
15685
+ } catch (err) {
15686
+ debugLog(`buildAgent: fallback provider "${entry.provider}" factory threw, skipping`, err);
15687
+ continue;
15688
+ }
15689
+ if (seenNames.has(instance.name)) {
15690
+ debugLog(`buildAgent: duplicate provider name "${instance.name}", skipping fallback provider`, entry.provider);
15691
+ continue;
15692
+ }
15693
+ seenNames.add(instance.name);
15694
+ nameByKey.set(entry.provider, instance.name);
15695
+ extraProviders.push(instance);
15696
+ }
15697
+ providerNameByKeyRef.current = nameByKey;
15349
15698
  const profile = pickedAgentRef.current;
15350
15699
  const skillsScan = defaultSkillScanPaths({
15351
15700
  cwd: projectDir,
@@ -15356,7 +15705,7 @@ function AppShell() {
15356
15705
  const userEnabledSkills = enabledSkillsRef.current;
15357
15706
  const skillsConfig = buildSkillsConfig({
15358
15707
  scan: skillsScan,
15359
- enabled: userEnabledSkills === void 0 ? void 0 : Array.from(new Set([...userEnabledSkills, ...extensionSkillNames]))
15708
+ enabled: userEnabledSkills === void 0 ? void 0 : Array.from(/* @__PURE__ */ new Set([...userEnabledSkills, ...extensionSkillNames]))
15360
15709
  });
15361
15710
  const projectMcps = buildMcpServers({
15362
15711
  discovered: mcpsCatalogRef.current,
@@ -15391,6 +15740,20 @@ function AppShell() {
15391
15740
  sessionId: session.id
15392
15741
  });
15393
15742
  const composedPreset = composeExtensionPreset(liveExtensionRegistry, profile.preset);
15743
+ const modelCatalogTools = {};
15744
+ if (subagentModelsRef.current) {
15745
+ const catalog = [];
15746
+ for (const [providerKey, providerName] of nameByKey) for (const info of modelsFor(providerKey)) catalog.push({
15747
+ provider: providerName,
15748
+ model: info
15749
+ });
15750
+ const presetSpawn = composedPreset.tools?.spawn;
15751
+ if (catalog.length > 0 && presetSpawn && isSpawnTool(presetSpawn)) {
15752
+ modelCatalogTools.spawn = presetSpawn.withModels(catalog);
15753
+ 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");
15754
+ modelCatalogTools.model_search = createModelSearchTool(catalog);
15755
+ }
15756
+ }
15394
15757
  const execution = launch?.context === "docker" ? createDockerContext({
15395
15758
  image: launch.image,
15396
15759
  cwd: actualCwd
@@ -15413,7 +15776,8 @@ function AppShell() {
15413
15776
  tools: {
15414
15777
  ...interactionTools,
15415
15778
  chatTurnsRead: chatTurnsReadTool,
15416
- ...composedPreset.tools ?? {}
15779
+ ...composedPreset.tools ?? {},
15780
+ ...modelCatalogTools
15417
15781
  },
15418
15782
  contextBreakdownSections: [...liveExtensionRegistry.contextBreakdownSections, ...liveExtensionRegistry.contextPanelSections],
15419
15783
  behavior: {
@@ -15422,7 +15786,7 @@ function AppShell() {
15422
15786
  tasksDir
15423
15787
  },
15424
15788
  execution,
15425
- provider: descriptor.factory(),
15789
+ provider: extraProviders.length > 0 ? [primaryProvider, ...extraProviders] : primaryProvider,
15426
15790
  session,
15427
15791
  mcpConnector: (configs) => connectMcpServers(configs, void 0, agent.hooks, { buildAuthProvider: (cfg) => new McpOAuthProvider({
15428
15792
  name: cfg.name,
@@ -15613,6 +15977,16 @@ function AppShell() {
15613
15977
  turnId
15614
15978
  });
15615
15979
  });
15980
+ agent.hooks.hook("model:fallback", ({ from, to, err }) => {
15981
+ setActiveFallback({
15982
+ provider: to.provider,
15983
+ model: to.model
15984
+ });
15985
+ stream.appendImmediate({
15986
+ kind: "info",
15987
+ text: `⚠ model fallback: ${from.provider}/${from.model} failed (${errorMessage(err)}) — continuing on ${to.provider}/${to.model}`
15988
+ });
15989
+ });
15616
15990
  agent.hooks.hook("tool:before", async ({ callId, name, input, turnId }) => {
15617
15991
  registerInFlightTool({
15618
15992
  callId,
@@ -15736,6 +16110,10 @@ function AppShell() {
15736
16110
  }
15737
16111
  stream.flushAndUpdate(finalizeStreamingMarkdown);
15738
16112
  });
16113
+ agent.hooks.hook("child:turn:after", ({ usage }) => {
16114
+ const turnCost = usage.cost ?? 0;
16115
+ if (turnCost > 0) setSessionCost((prev) => prev + turnCost);
16116
+ });
15739
16117
  agent.hooks.hook("agent:done", () => {
15740
16118
  const reason = lastFinishReasonRef.current;
15741
16119
  lastFinishReasonRef.current = void 0;
@@ -15781,13 +16159,14 @@ function AppShell() {
15781
16159
  setLastInputTokens(ctx.effectiveTokens);
15782
16160
  lastInputTokensRef.current = ctx.effectiveTokens;
15783
16161
  });
15784
- agent.hooks.hook("spawn:before", ({ id, task, depth }) => {
16162
+ agent.hooks.hook("spawn:before", ({ id, task, depth, model }) => {
15785
16163
  const taskPreview = previewLine(task, 80);
15786
16164
  stream.appendImmediate({
15787
16165
  kind: "spawn-start",
15788
16166
  text: taskPreview,
15789
16167
  childId: id,
15790
- depth: depth ?? 1
16168
+ depth: depth ?? 1,
16169
+ ...model ? { model } : {}
15791
16170
  });
15792
16171
  });
15793
16172
  agent.hooks.hook("spawn:complete", ({ id, depth, status, stats }) => {
@@ -15893,6 +16272,7 @@ function AppShell() {
15893
16272
  return agent;
15894
16273
  }, [
15895
16274
  providerRegistry,
16275
+ modelsFor,
15896
16276
  stream,
15897
16277
  gateDecision,
15898
16278
  cancelRunOnDenial,
@@ -15908,7 +16288,8 @@ function AppShell() {
15908
16288
  extensionRegistry
15909
16289
  ]);
15910
16290
  const refreshSessions = useCallback(async () => {
15911
- const list = await listSessionMeta(store, settings.showAllProjects ? void 0 : { projectRoot: projectDir });
16291
+ const filter = settings.showAllProjects ? void 0 : { projectRoot: projectDir };
16292
+ const list = await listSessionMeta(store, filter);
15912
16293
  setSessions(list);
15913
16294
  return list;
15914
16295
  }, [
@@ -16015,10 +16396,11 @@ function AppShell() {
16015
16396
  setEvents(capTranscriptEvents(eventsFromTurns(session.turns, session.runs)));
16016
16397
  runningRef.current = true;
16017
16398
  setBusy(true);
16399
+ setActiveFallback(null);
16018
16400
  try {
16019
16401
  const runModelOptions = enabledModelOptions(currentPicked.modelOptions);
16020
16402
  await agent.run({
16021
- model: currentPicked.model,
16403
+ model: runModelFor(currentPicked),
16022
16404
  ...currentPicked.effort ? { thinking: currentPicked.effort } : {},
16023
16405
  ...Object.keys(runModelOptions).length > 0 ? { modelOptions: runModelOptions } : {},
16024
16406
  behavior: {
@@ -16060,8 +16442,10 @@ function AppShell() {
16060
16442
  sessionRef.current = session;
16061
16443
  agentRef.current = buildAgent(session, key);
16062
16444
  if (extensionRegistrySwapKeyRef.current === null || extensionRegistrySwapKeyRef.current === extensionRegistry) pendingAgentRebuildRef.current = false;
16063
- setActiveSkillNames(readPinnedSkills(session.metadata["zidane.activeSkills"]));
16445
+ const persistedPins = readPinnedSkills(session.metadata["zidane.activeSkills"]);
16446
+ setActiveSkillNames(persistedPins);
16064
16447
  setEvents(capTranscriptEvents(eventsFromTurns(session.turns, session.runs)));
16448
+ setActiveFallback(null);
16065
16449
  const replayedTokens = lastContextSizeFromTurns(session.turns, session.runs);
16066
16450
  setLastInputTokens(replayedTokens);
16067
16451
  lastInputTokensRef.current = replayedTokens;
@@ -16149,7 +16533,8 @@ function AppShell() {
16149
16533
  ]);
16150
16534
  const [availableProviders, setAvailableProviders] = useState([]);
16151
16535
  const refreshAvailableProviders = useCallback(() => {
16152
- setAvailableProviders(detectAuth(config.paths.userDir, providerRegistry).filter((p) => p.available));
16536
+ const detected = detectAuth(config.paths.userDir, providerRegistry);
16537
+ setAvailableProviders(detected.filter((p) => p.available));
16153
16538
  }, [config.paths.userDir, providerRegistry]);
16154
16539
  useEffect(() => {
16155
16540
  refreshAvailableProviders();
@@ -16456,30 +16841,36 @@ function AppShell() {
16456
16841
  else setQueueSelectionIndex(Math.min(pushIdx, messageQueueRef.current.length - 1));
16457
16842
  }, []);
16458
16843
  /**
16459
- * Pick a `{ providerKey, modelId }` tuple from the cross-provider
16460
- * model picker. Two branches:
16844
+ * Apply a model chain from the cross-provider picker's ordered
16845
+ * multi-select. Entry 0 is the PRIMARY (same role as the old
16846
+ * single-pick: provider swap, effort, options); entries 1+ become
16847
+ * run-level fallbacks (`agent.run({ model: [...] })`).
16461
16848
  *
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.
16849
+ * Persistence mirrors the old single-pick path (`lastProvider` +
16850
+ * `lastModelByProvider` track the primary) plus the additive
16851
+ * `lastModelChain`. Fallback entries are re-validated against the
16852
+ * live auth list; stale entries drop with a debug log.
16471
16853
  *
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).
16854
+ * The session re-activates (agent rebuild) when the primary provider
16855
+ * changed OR the chain's provider-key SET changed the agent must
16856
+ * own an instance for every chain provider. A pure model / order
16857
+ * change within the same providers stays a per-run swap, exactly
16858
+ * like same-provider single picks always have.
16475
16859
  */
16476
- const onPickModel = useCallback(async (next) => {
16477
- const nextProvider = availableProviders.find((p) => p.key === next.providerKey);
16860
+ const onApplyModelChain = useCallback(async (chain) => {
16861
+ const primary = chain[0];
16862
+ if (!primary) {
16863
+ modal.close();
16864
+ return;
16865
+ }
16866
+ const nextProvider = availableProviders.find((p) => p.key === primary.provider);
16478
16867
  if (!nextProvider) {
16479
- debugLog("onPickModel: unknown provider key", next.providerKey);
16868
+ debugLog("onApplyModelChain: unknown provider key", primary.provider);
16480
16869
  modal.close();
16481
16870
  return;
16482
16871
  }
16872
+ const fallbacks = filterChainFallbacks(chain.slice(1), primary, (k) => availableProviders.some((p) => p.key === k) && providerRegistry[k] !== void 0);
16873
+ if (fallbacks.length < chain.length - 1) debugLog("onApplyModelChain: dropped unavailable/duplicate fallback entries", chain.length - 1 - fallbacks.length);
16483
16874
  const descriptor = providerRegistry[nextProvider.key];
16484
16875
  const prior = stateStore.load();
16485
16876
  const providerChanged = picked?.provider.key !== nextProvider.key;
@@ -16488,19 +16879,34 @@ function AppShell() {
16488
16879
  ...providerChanged ? { lastProvider: nextProvider.key } : {},
16489
16880
  lastModelByProvider: {
16490
16881
  ...prior.lastModelByProvider,
16491
- [nextProvider.key]: next.modelId
16492
- }
16882
+ [nextProvider.key]: primary.model
16883
+ },
16884
+ lastModelChain: [{
16885
+ provider: primary.provider,
16886
+ model: primary.model
16887
+ }, ...fallbacks]
16493
16888
  });
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({
16889
+ const nextEffort = descriptor ? effortForModel(descriptor, primary.model, prior.lastEffortByModel) : void 0;
16890
+ const nextOptions = descriptor ? restoreModelOptions(descriptor, primary.model, prior.lastModelOptionsByModel) : void 0;
16891
+ const updated = {
16497
16892
  provider: nextProvider,
16498
- model: next.modelId,
16893
+ model: primary.model,
16894
+ ...fallbacks.length > 0 ? { fallbacks } : {},
16499
16895
  ...nextEffort ? { effort: nextEffort } : {},
16500
16896
  ...nextOptions ? { modelOptions: nextOptions } : {}
16501
- });
16897
+ };
16898
+ const prevChain = picked ? [{
16899
+ provider: picked.provider.key,
16900
+ model: picked.model
16901
+ }, ...picked.fallbacks ?? []] : [];
16902
+ setPicked(updated);
16903
+ setActiveFallback(null);
16904
+ pickedRef.current = updated;
16502
16905
  modal.close();
16503
- if (providerChanged && currentSession && !busy && !pendingApproval) await activateSession(currentSession.id, nextProvider.key);
16906
+ if ((providerChanged || providerKeySetChanged(prevChain, [{
16907
+ provider: primary.provider,
16908
+ model: primary.model
16909
+ }, ...fallbacks])) && currentSession && !busy && !pendingApproval) await activateSession(currentSession.id, nextProvider.key);
16504
16910
  }, [
16505
16911
  availableProviders,
16506
16912
  providerRegistry,
@@ -16535,7 +16941,8 @@ function AppShell() {
16535
16941
  modelsFor,
16536
16942
  current,
16537
16943
  onPick: (next) => {
16538
- setSetting("compactionModel", next.providerKey === picked.provider.key && next.modelId === picked.model ? "" : `${next.providerKey}/${next.modelId}`);
16944
+ const isMain = next.providerKey === picked.provider.key && next.modelId === picked.model;
16945
+ setSetting("compactionModel", isMain ? "" : `${next.providerKey}/${next.modelId}`);
16539
16946
  modal.close();
16540
16947
  }
16541
16948
  }));
@@ -16676,7 +17083,7 @@ function AppShell() {
16676
17083
  });
16677
17084
  }
16678
17085
  const newRefs = uniqueSkillNamesFromReferences(references);
16679
- const skillNames = Array.from(new Set([...activeSkillNamesRef.current, ...newRefs]));
17086
+ const skillNames = Array.from(/* @__PURE__ */ new Set([...activeSkillNamesRef.current, ...newRefs]));
16680
17087
  for (const name of skillNames) try {
16681
17088
  await withTimeout(agent.activateSkill(name), {
16682
17089
  operation: `activateSkill(${name})`,
@@ -16713,8 +17120,9 @@ function AppShell() {
16713
17120
  };
16714
17121
  })];
16715
17122
  const runModelOptions = enabledModelOptions(picked.modelOptions);
17123
+ setActiveFallback(null);
16716
17124
  await agent.run({
16717
- model: picked.model,
17125
+ model: runModelFor(picked),
16718
17126
  prompt: runPrompt,
16719
17127
  ...picked.effort ? { thinking: picked.effort } : {},
16720
17128
  ...Object.keys(runModelOptions).length > 0 ? { modelOptions: runModelOptions } : {},
@@ -16848,8 +17256,9 @@ function AppShell() {
16848
17256
  const before = session.turns.length;
16849
17257
  const beforeRuns = session.runs.length;
16850
17258
  const runModelOptions = enabledModelOptions(picked.modelOptions);
17259
+ setActiveFallback(null);
16851
17260
  await agent.run({
16852
- model: picked.model,
17261
+ model: runModelFor(picked),
16853
17262
  prompt: agentPrompt,
16854
17263
  ...picked.effort ? { thinking: picked.effort } : {},
16855
17264
  ...Object.keys(runModelOptions).length > 0 ? { modelOptions: runModelOptions } : {},
@@ -17256,7 +17665,8 @@ function AppShell() {
17256
17665
  if (!session || !p) return;
17257
17666
  const stale = agentRef.current;
17258
17667
  const staleWarnLog = warnLogRef.current;
17259
- agentRef.current = buildAgentRef.current(session, p.provider.key);
17668
+ const fresh = buildAgentRef.current(session, p.provider.key);
17669
+ agentRef.current = fresh;
17260
17670
  if (stale) await stale.destroy().catch((err) => debugLog("rebuildActiveAgent: destroy failed", err));
17261
17671
  if (staleWarnLog) await staleWarnLog.close().catch((err) => debugLog("rebuildActiveAgent: warnLog.close failed", err));
17262
17672
  }, []);
@@ -17353,7 +17763,8 @@ function AppShell() {
17353
17763
  ]);
17354
17764
  const onLogoutMcp = useCallback((name) => {
17355
17765
  mcpCredentialStore.delete(name);
17356
- dispatchAuth(mcpsCatalogRef.current.find((d) => d.config.name === name)?.config.auth === "oauth" ? {
17766
+ const entry = mcpsCatalogRef.current.find((d) => d.config.name === name);
17767
+ dispatchAuth(entry?.config.auth === "oauth" ? {
17357
17768
  type: "auth-required",
17358
17769
  name,
17359
17770
  reason: "no-tokens"
@@ -17415,7 +17826,8 @@ function AppShell() {
17415
17826
  const idx = turnIds.indexOf(prev);
17416
17827
  if (idx === -1) return turnIds[turnIds.length - 1];
17417
17828
  const len = turnIds.length;
17418
- return turnIds[((idx + direction) % len + len) % len];
17829
+ const nextIdx = ((idx + direction) % len + len) % len;
17830
+ return turnIds[nextIdx];
17419
17831
  });
17420
17832
  }, [turnIds]);
17421
17833
  const onForkTurn = useCallback(async (turnId) => {
@@ -17929,7 +18341,11 @@ function AppShell() {
17929
18341
  providerKey: picked.provider.key,
17930
18342
  modelId: picked.model
17931
18343
  },
17932
- onPick: onPickModel
18344
+ chain: [{
18345
+ provider: picked.provider.key,
18346
+ model: picked.model
18347
+ }, ...picked.fallbacks ?? []],
18348
+ onApplyChain: onApplyModelChain
17933
18349
  }));
17934
18350
  return;
17935
18351
  }
@@ -17964,7 +18380,13 @@ function AppShell() {
17964
18380
  autocompactBuffer: OUTPUT_RESERVE_TOKENS,
17965
18381
  ...settings.autoCompact && settings.autoCompactThreshold ? { compactThreshold: settings.autoCompactThreshold } : {}
17966
18382
  }).then((breakdown) => {
17967
- if (breakdown && agentRef.current === agent) modal.open(/* @__PURE__ */ jsx$1(ContextPanelModal, { breakdown }));
18383
+ if (breakdown && agentRef.current === agent) {
18384
+ const modelUsage = usageByModel(sessionRef.current?.runs ?? []);
18385
+ modal.open(/* @__PURE__ */ jsx$1(ContextPanelModal, {
18386
+ breakdown,
18387
+ modelUsage
18388
+ }));
18389
+ }
17968
18390
  }).catch((err) => debugLog("getContextBreakdown failed", err));
17969
18391
  return;
17970
18392
  }
@@ -18135,8 +18557,8 @@ function AppShell() {
18135
18557
  currentSession,
18136
18558
  hasMultipleAgents,
18137
18559
  uiMode: settings.uiMode,
18138
- modelLabel: picked?.model ?? null,
18139
- modelColor: COLOR.model,
18560
+ modelLabel: activeFallback ? activeFallback.model : picked ? `${picked.model}${picked.fallbacks && picked.fallbacks.length > 0 ? ` +${picked.fallbacks.length}` : ""}` : null,
18561
+ modelColor: activeFallback ? COLOR.warn : COLOR.model,
18140
18562
  effortLabel: modelHasReasoning ? picked?.effort ?? "medium" : null,
18141
18563
  effortColor: COLOR.warn,
18142
18564
  modelOptionsLabel: (() => {