zidane 6.6.0 → 6.6.2

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 (76) hide show
  1. package/dist/{acp-EFzMJPxa.js → acp-BQqhozmt.js} +2 -2
  2. package/dist/{acp-EFzMJPxa.js.map → acp-BQqhozmt.js.map} +1 -1
  3. package/dist/acp-cli.js +3 -3
  4. package/dist/acp.d.ts +1 -1
  5. package/dist/acp.js +1 -1
  6. package/dist/{agent-ut6nQRnI.js → agent-DTxH7RHW.js} +40 -7
  7. package/dist/agent-DTxH7RHW.js.map +1 -0
  8. package/dist/agent.d.ts +1 -1
  9. package/dist/agent.js +1 -1
  10. package/dist/{auth-BaLBiz0r.js → auth-7rv6ZfWk.js} +2 -2
  11. package/dist/{auth-BaLBiz0r.js.map → auth-7rv6ZfWk.js.map} +1 -1
  12. package/dist/chat/pure.d.ts +3 -3
  13. package/dist/chat/pure.js +1 -1
  14. package/dist/chat.d.ts +88 -5
  15. package/dist/chat.d.ts.map +1 -1
  16. package/dist/chat.js +5 -5
  17. package/dist/contexts/daytona.d.ts +1 -1
  18. package/dist/contexts/e2b.d.ts +1 -1
  19. package/dist/eval.d.ts +1 -1
  20. package/dist/extensions.d.ts +1 -1
  21. package/dist/extensions.js +1 -1
  22. package/dist/headless.d.ts +1 -1
  23. package/dist/headless.js +1 -1
  24. package/dist/{index-A1qz0l55.d.ts → index-D0O-QO7Z.d.ts} +72 -1
  25. package/dist/index-D0O-QO7Z.d.ts.map +1 -0
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.js +1 -1
  28. package/dist/mcp.d.ts +1 -1
  29. package/dist/output/stream-json.d.ts +1 -1
  30. package/dist/output/terminal.d.ts +1 -1
  31. package/dist/plugins.d.ts +1 -1
  32. package/dist/presets.d.ts +1 -1
  33. package/dist/presets.js +1 -1
  34. package/dist/providers/anthropic.d.ts +1 -1
  35. package/dist/providers/arcee.d.ts +1 -1
  36. package/dist/providers/baseten.d.ts +1 -1
  37. package/dist/providers/cerebras.d.ts +1 -1
  38. package/dist/providers/local.d.ts +1 -1
  39. package/dist/providers/openai-compat.d.ts +1 -1
  40. package/dist/providers/openai.d.ts +1 -1
  41. package/dist/providers/openrouter.d.ts +1 -1
  42. package/dist/providers/xai.d.ts +1 -1
  43. package/dist/providers.d.ts +1 -1
  44. package/dist/restate.d.ts +1 -1
  45. package/dist/restate.js +1 -1
  46. package/dist/session/sqlite.d.ts +1 -1
  47. package/dist/session.d.ts +1 -1
  48. package/dist/skills.d.ts +1 -1
  49. package/dist/{tool-formatters-OobhXAK1.d.ts → tool-formatters-NrzD6Fkk.d.ts} +20 -3
  50. package/dist/{tool-formatters-OobhXAK1.d.ts.map → tool-formatters-NrzD6Fkk.d.ts.map} +1 -1
  51. package/dist/tools/fetch-url.d.ts +1 -1
  52. package/dist/tools/web-search.d.ts +1 -1
  53. package/dist/tools.d.ts +1 -1
  54. package/dist/tools.js +1 -1
  55. package/dist/{transcript-anchors-ChTtfDdM.js → transcript-anchors-BjH-68k2.js} +661 -37
  56. package/dist/transcript-anchors-BjH-68k2.js.map +1 -0
  57. package/dist/{transcript-anchors-0midxNxf.d.ts → transcript-anchors-UhC58Tmj.d.ts} +3 -3
  58. package/dist/transcript-anchors-UhC58Tmj.d.ts.map +1 -0
  59. package/dist/tui.d.ts +33 -5
  60. package/dist/tui.d.ts.map +1 -1
  61. package/dist/tui.js +864 -193
  62. package/dist/tui.js.map +1 -1
  63. package/dist/{turn-operations-JtKQfSFC.d.ts → turn-operations-Cuc9pwiB.d.ts} +3 -3
  64. package/dist/{turn-operations-JtKQfSFC.d.ts.map → turn-operations-Cuc9pwiB.d.ts.map} +1 -1
  65. package/dist/{turn-operations-7_Ff7INF.js → turn-operations-mMxDRgLY.js} +65 -3
  66. package/dist/{turn-operations-7_Ff7INF.js.map → turn-operations-mMxDRgLY.js.map} +1 -1
  67. package/dist/types.d.ts +1 -1
  68. package/docs/ARCHITECTURE.md +1 -1
  69. package/docs/CHAT.md +30 -5
  70. package/docs/INTERACTIONS.md +11 -0
  71. package/docs/TUI.md +22 -2
  72. package/package.json +1 -1
  73. package/dist/agent-ut6nQRnI.js.map +0 -1
  74. package/dist/index-A1qz0l55.d.ts.map +0 -1
  75. package/dist/transcript-anchors-0midxNxf.d.ts.map +0 -1
  76. package/dist/transcript-anchors-ChTtfDdM.js.map +0 -1
package/dist/tui.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { C as estimateRebilledCostUsd } from "./openai-compat-BoTi-9fd.js";
2
2
  import { t as buildContextSegments } from "./context-segments-CJCtJ9sU.js";
3
- import { $ as installExtensionFromSpec, $t as groupBindings, A as selectFilesFromSession, An as OUTPUT_RESERVE_TOKENS, C as resolveLegacySessionDir, D as resolveTasksDir, Dn as sanitizeExtensionConfigActionPatch, E as resolveSessionDir, En as resolveExtensionConfigValue, Et as isSpawnTool, G as summaryToTurn, It as looksBinary, M as compactConversation, O as buildPostCompactAttachments, Pt as tailTruncate, Qt as formatBindingForDisplay, Rt as createModelSearchTool, S as resolveLegacyPersistDir, Sn as coerceExtensionConfigInput, T as resolvePersistDir, X as createExtensionPromptBridge, Xt as KEYBINDING_KEY_COL_WIDTH, Z as ExtensionInstallError, Zt as ensureKeybindingsFile, _n as parseExtensionScanPathEnv, at as createExtensionHud, bt as basic_default, dn as registerExtensionHostModules, dt as filterExtensionRegistry, en as keybindingsPath, f as applyCompactSummaryCutoff, ft as filterRegistryPlugins, hn as inlineLoadedExtension, ht as createExtensionHostRuntimeBridge, it as updateInstalledExtension, jn as effectiveContextWindow, lt as composeExtensionPreset, mt as partitionExtensionCatalog, on as resolveExtensionKeybindings, ot as discoverAndBuildExtensionRegistry, rt as removeInstalledExtension, t as createAgent, tn as matchesBinding, ut as disposeExtensionRegistry, v as cleanupPersistedSession, vn as scanExtensionDirectories, w as resolveMcpWarningsDir, x as rehomePersistedStubs, yn as createDebugLog } from "./agent-ut6nQRnI.js";
3
+ import { $ as installExtensionFromSpec, $t as groupBindings, A as selectFilesFromSession, C as resolveLegacySessionDir, Cn as coerceExtensionConfigInput, D as resolveTasksDir, Dn as resolveExtensionConfigValue, E as resolveSessionDir, Et as isSpawnTool, G as summaryToTurn, It as looksBinary, M as compactConversation, Mn as effectiveContextWindow, O as buildPostCompactAttachments, On as sanitizeExtensionConfigActionPatch, Pt as tailTruncate, Qt as formatBindingForDisplay, Rt as createModelSearchTool, S as resolveLegacyPersistDir, T as resolvePersistDir, X as createExtensionPromptBridge, Xt as KEYBINDING_KEY_COL_WIDTH, Z as ExtensionInstallError, Zt as ensureKeybindingsFile, _n as parseExtensionScanPathEnv, at as createExtensionHud, bn as setDebugLogSink, bt as basic_default, dn as registerExtensionHostModules, dt as filterExtensionRegistry, en as keybindingsPath, f as applyCompactSummaryCutoff, ft as filterRegistryPlugins, hn as inlineLoadedExtension, ht as createExtensionHostRuntimeBridge, it as updateInstalledExtension, jn as OUTPUT_RESERVE_TOKENS, lt as composeExtensionPreset, mt as partitionExtensionCatalog, on as resolveExtensionKeybindings, ot as discoverAndBuildExtensionRegistry, rt as removeInstalledExtension, t as createAgent, tn as matchesBinding, ut as disposeExtensionRegistry, v as cleanupPersistedSession, vn as scanExtensionDirectories, w as resolveMcpWarningsDir, x as rehomePersistedStubs, yn as createDebugLog } from "./agent-DTxH7RHW.js";
4
4
  import { a as formatDuration, c as previewLine, i as fmtTokens, l as shortId, n as compactPath, o as formatTaskStatus, r as fmtCost, s as formatTaskSummary, t as ageString } from "./format-BF745pLL.js";
5
5
  import { n as appendCapped } from "./output-cap-C9zukcCj.js";
6
6
  import { i as killProcessGroup, n as SUPPORTS_PROCESS_GROUPS, r as createProcessContext } from "./contexts-BSFQVclW.js";
@@ -18,12 +18,12 @@ import { i as installCacheBreakLogger } from "./cache-telemetry-ehVdWsg2.js";
18
18
  import { a as generateStructuredData, c as shouldAutoGenerateTitle, f as commandBlockToEvent, g as parseCommandInvocation, h as executeCommand, n as patchInlineGhost, p as commandBlocksToContent, r as resolveInlineSuggestion, s as generateSessionTitle, t as createPathAutocompleteProvider } from "./inline-autocomplete-D0MPvBaS.js";
19
19
  import { s as resolveCompletionSubmit } from "./completion-core-BRo86R_L.js";
20
20
  import { a as ZIDANE_EXTENSION_SKILL_NAME, d as extensionScaffold, f as readSkillEditMarker, h as withZidaneSkillEditSkill, i as SKILL_EDIT_METADATA_KEY, l as editSessionMetadata, m as withZidaneExtensionSkill, o as ZIDANE_SKILL_EDIT_SKILL_NAME, t as loginMcpServer, u as extensionDirForCwd } from "./login-CB23LyI1.js";
21
- import { E as renderDefaultCallbackPage, T as restoreModelOptions, b as modelSupportsReasoning, c as setPreferredCredential, g as getContextWindow, h as enabledModelOptions, l as setProviderCredential, n as applyApiKeyEnv, t as detectAuth, w as piIdOf, y as modelOptionsFor } from "./auth-BaLBiz0r.js";
21
+ import { E as renderDefaultCallbackPage, T as restoreModelOptions, b as modelSupportsReasoning, c as setPreferredCredential, g as getContextWindow, h as enabledModelOptions, l as setProviderCredential, n as applyApiKeyEnv, t as detectAuth, w as piIdOf, y as modelOptionsFor } from "./auth-7rv6ZfWk.js";
22
22
  import { createDockerContext } from "./contexts/docker.js";
23
23
  import { createTuiStore } from "./session/sqlite.js";
24
- import { $ as buildContextualDiff, A as createSkillsCompletionProvider, B as EDIT_TOOL_NAMES, C as chunkMarkdownForRender, D as splitMarkdownCodeBlocks, E as normalizeMarkdownCodeFences, G as isVisibleDuringSearch, H as isTurnHighlighted, I as createCommandsCompletionProvider, J as selectableTurnIds, K as promptHistoryFromEvents, L as blendHsl, N as createFilesCompletionProvider, O as stripHtmlCommentsOutsideCode, R as buildLinearRamp, T as codeTokenHasClosingFence, U as isUserBashEvent, V as isEditErrorResult, W as isVisible, X as streamEventSearchText, Y as selectionAfterTurnRemoval, Z as turnSelectionOwnership, _ as turnContextSize, _t as summarizeOutcomes, a as TOOL_DISPLAY, at as filetypeFromPath, b as filterModelCatalog, bt as createChatsCompletionProvider, ct as summarizeEditPayload, d as finalizeStreamingMarkdown, et as buildUnifiedDiff, f as finalizeStreamingMarkdownForOwner, ft as mergeApprovalAndBodyOutcomes, g as revealEarlierTranscriptEvents, gt as stripEditOutcomesAnnotation, h as replaceTranscriptTurnEvents, ht as rewriteMultiEditHeader, i as turnAsText, it as extractEditPayload, j as uniqueSkillNamesFromReferences, mt as resolveApprovalForPayload, n as deleteTurnSafely, o as displayNameFor, ot as previewEditPayload, p as hiddenEarlierTranscriptCount, pt as parseEditOutcomesFromResult, q as retagUserPromptEvents, r as truncateTurnsAt, s as formatToolCall, tt as clipDiffForRender, u as capTranscriptEvents, ut as buildEditOutcomesAnnotation, v as splitPromptSegments, w as closeDanglingCodeFence, y as buildModelCatalog, z as BASH_CALL_ID_PREFIX } from "./turn-operations-7_Ff7INF.js";
24
+ import { B as BASH_CALL_ID_PREFIX, D as normalizeMarkdownCodeFences, E as codeTokenHasClosingFence, G as isVisible, H as isEditErrorResult, J as retagUserPromptEvents, K as isVisibleDuringSearch, L as createCommandsCompletionProvider, M as uniqueSkillNamesFromReferences, O as splitMarkdownCodeBlocks, P as createFilesCompletionProvider, Q as turnSelectionOwnership, R as blendHsl, S as parseCustomModelQuery, T as closeDanglingCodeFence, U as isTurnHighlighted, V as EDIT_TOOL_NAMES, W as isUserBashEvent, X as selectionAfterTurnRemoval, Y as selectableTurnIds, Z as streamEventSearchText, _ as turnContextSize, _t as stripEditOutcomesAnnotation, a as TOOL_DISPLAY, at as extractEditPayload, b as filterModelCatalog, d as finalizeStreamingMarkdown, dt as buildEditOutcomesAnnotation, et as buildContextualDiff, f as finalizeStreamingMarkdownForOwner, g as revealEarlierTranscriptEvents, gt as rewriteMultiEditHeader, h as replaceTranscriptTurnEvents, ht as resolveApprovalForPayload, i as turnAsText, j as createSkillsCompletionProvider, k as stripHtmlCommentsOutsideCode, lt as summarizeEditPayload, mt as parseEditOutcomesFromResult, n as deleteTurnSafely, nt as clipDiffForRender, o as displayNameFor, ot as filetypeFromPath, p as hiddenEarlierTranscriptCount, pt as mergeApprovalAndBodyOutcomes, q as promptHistoryFromEvents, r as truncateTurnsAt, s as formatToolCall, st as previewEditPayload, tt as buildUnifiedDiff, u as capTranscriptEvents, v as splitPromptSegments, vt as summarizeOutcomes, w as chunkMarkdownForRender, xt as createChatsCompletionProvider, y as buildModelCatalog, z as buildLinearRamp } from "./turn-operations-mMxDRgLY.js";
25
25
  import { a as searchHighlightRanges, c as searchSessionHistoryIndexAsync, i as highlightSegments, n as buildSessionSearchIndexAsync, r as filterSessionSearchCatalog, u as searchSessionIndexAsync } from "./history-search-C4bk1Wz_.js";
26
- import { $ as McpAuthProvider, $t as resolveStoragePaths, At as SETTINGS_INPUTS, B as mcpToolsCachePath, C as addToSafelist, Cn as buildUpdateHint, Ct as buildHints, Dr as listSessionMeta, Dt as DEFAULT_SETTINGS, Er as lastContextSizeFromTurns, Et as useEnabledToggleSet, F as oauthUsesManualCodePaste, Fr as toolCallPreview, G as useMcpToolToggleMap, Hn as accentColor, I as runOAuthLogin, Ir as toolResultText, J as indexOfServerRow, Jn as TODO_STATUS_GLYPHS, Jt as useDiscovery, Kt as createDiscoverySlot, L as supportsOAuth, Lr as updateToolEventOutcomes, Lt as resolveChipColor, M as fetchOAuthRedirect, Mr as stripSpawnTokensLine, Mt as SettingsProvider, Nn as chatAutoCompactBehavior, Nr as sumRunCosts, Nt as clampFps, Or as loadState, Ot as SETTINGS_CATEGORIES, P as redirectUriFromAuthUrl, Pn as discoverAgentsMd, Pt as useSettings, Qt as resolveConfig, Rr as usageByModel, Rt as resolveTheme, Sn as bootTick, Sr as compactSummaryEvent, St as truncateTrailing, T as isOnSafelist, Tr as eventsFromTurns, Tt as listProjectFiles, U as subscribeMcpToolsCache, V as refreshMcpToolsCatalog, X as createFileMcpCredentialStore, Xt as ConfigProvider, Y as parentServerName, Yt as useDiscoveryOptional, Zt as useConfig, _r as buildPlanSystem, _t as useInteractionsActions, a as useSurfaces, an as restoreChainFallbacks, ar as useActiveTodos, b as useSafeModeActions, bn as tryOpenBrowser, br as createChatTurnsReadTool, bt as clipHintsToWidth, c as discoverSystemMd, cn as useCompletion, ct as PRESENT_PLAN_TOOL, d as userSystemMdPath, dn as importFromClaudeCode, dt as createInteractionTools, en as buildRunModel, et as useMcpAuthDispatch, f as useStreamBuffer, fn as mcpsWritePath, gn as dedupMcpServersByName, gr as buildBuildSystem, h as discoverProjectSkills, hn as buildMcpServers, ht as pendingInteractionsFromTurns, i as useSelectStyle, in as providerKeySetChanged, ir as todoProgressLabel, it as ASK_USER_TOOL, j as formatPathForCwd, jr as serverToolResultSummary, jt as SETTINGS_TOGGLES, k as suggestSafelistEntries, kr as marginTopFor, kt as SETTINGS_CHOICES, l as renderSystemMdSeed, ln as countClaudeSessionFiles, lt as buildResumedToolResultsTurn, m as defaultSkillScanPaths, mn as upsertMcpServer, mt as makeRequestInteraction, n as ThemeProvider, nn as chainsEqual, nt as getMcpAuthStatus, ot as HOST_INTERACTION_ID_PREFIX, p as buildSkillsConfig, pn as removeMcpServer, pt as isModelInteraction, q as buildVisibleMcpRows, qt as DiscoveryProvider, r as useColors, rn as filterChainFallbacks, rr as setTodosForRun, s as useTheme, sn as toggleChainEntry, st as InteractionsProvider, t as computeTurnAnchors, tn as chainPositionOf, tr as pruneTodosByRun, tt as useMcpAuthState, u as userScopeOverridesLoad, un as hasClaudeCodeSetup, ut as createAutomaticInteractionResponse, v as writeSessionExport, vn as discoverProjectMcps, vr as envSection, vt as useInteractionsQueue, w as getSafelist, wn as useUpdateCheck, wr as deriveSessionTitle, x as useSafeModeQueue, xt as hintsLength, y as SafeModeProvider, yr as chatReferenceTokensForPrompt, yt as EMPTY_HINTS, z as loadMcpToolsCache } from "./transcript-anchors-ChTtfDdM.js";
26
+ import { At as hintsLength, B as formatPathForCwd, Bt as SETTINGS_TOGGLES, C as addToSafelist, Cn as upsertMcpServer, Ct as makeRequestInteraction, Dn as discoverProjectMcps, Dr as envSection, Dt as useInteractionsQueue, Er as buildPlanSystem, Et as useInteractionsActions, F as formatUsagePercent, Fr as lastContextSizeFromTurns, Ft as useEnabledToggleSet, G as runOAuthLogin, Gr as toolCallPreview, Hn as chatAutoCompactBehavior, Hr as stripSpawnTokensLine, Ht as clampFps, I as mostConstrainedWindow, Ir as listSessionMeta, It as DEFAULT_SETTINGS, J as loadMcpToolsCache, Jr as usageByModel, K as supportsOAuth, Kr as toolResultText, Kt as resolveChipColor, L as providerUsageSupported, Lr as loadState, Lt as SETTINGS_CATEGORIES, M as clearProviderUsageCache, Mn as buildUpdateHint, Mt as buildHints, N as fetchAllProviderUsage, Nn as useUpdateCheck, Nr as deriveSessionTitle, Or as chatReferenceTokensForPrompt, Ot as EMPTY_HINTS, P as fetchProviderUsage, Pr as eventsFromTurns, Pt as listProjectFiles, Q as subscribeMcpToolsCache, R as usageChecksOptedOut, Rr as marginTopFor, Rt as SETTINGS_CHOICES, Sn as removeMcpServer, St as isModelInteraction, T as isOnSafelist, Tn as dedupMcpServersByName, Tr as buildBuildSystem, U as redirectUriFromAuthUrl, Un as discoverAgentsMd, Ur as sumRunCosts, Ut as useSettings, V as fetchOAuthRedirect, Vr as serverToolResultSummary, Vt as SettingsProvider, W as oauthUsesManualCodePaste, X as refreshMcpToolsCatalog, Y as mcpToolsCachePath, Zn as accentColor, _n as useCompletion, _t as PRESENT_PLAN_TOOL, a as useSurfaces, an as ConfigProvider, at as createFileMcpCredentialStore, b as useSafeModeActions, bn as importFromClaudeCode, bt as createInteractionTools, c as discoverSystemMd, cn as resolveStoragePaths, ct as McpAuthProvider, d as userSystemMdPath, dn as chainsEqual, dt as getMcpAuthStatus, et as useMcpToolToggleMap, f as useStreamBuffer, fn as filterChainFallbacks, fr as setTodosForRun, gn as toggleChainEntry, gt as InteractionsProvider, h as discoverProjectSkills, ht as HOST_INTERACTION_ID_PREFIX, i as useSelectStyle, in as useDiscoveryOptional, it as parentServerName, jn as bootTick, jr as compactSummaryEvent, jt as truncateTrailing, k as suggestSafelistEntries, kn as tryOpenBrowser, kr as createChatTurnsReadTool, kt as clipHintsToWidth, l as renderSystemMdSeed, ln as buildRunModel, lt as useMcpAuthDispatch, m as defaultSkillScanPaths, mn as restoreChainFallbacks, mr as useActiveTodos, n as ThemeProvider, nn as DiscoveryProvider, nt as buildVisibleMcpRows, on as useConfig, p as buildSkillsConfig, pn as providerKeySetChanged, pr as todoProgressLabel, pt as ASK_USER_TOOL, qr as updateToolEventOutcomes, qt as resolveTheme, r as useColors, rn as useDiscovery, rr as TODO_STATUS_GLYPHS, rt as indexOfServerRow, s as useTheme, sn as resolveConfig, t as computeTurnAnchors, tn as createDiscoverySlot, u as userScopeOverridesLoad, un as chainPositionOf, ur as pruneTodosByRun, ut as useMcpAuthState, v as writeSessionExport, vn as countClaudeSessionFiles, vt as buildResumedToolResultsTurn, w as getSafelist, wn as buildMcpServers, wt as pendingInteractionsFromTurns, x as useSafeModeQueue, xn as mcpsWritePath, y as SafeModeProvider, yn as hasClaudeCodeSetup, yt as createAutomaticInteractionResponse, zr as noticeTurnContent, zt as SETTINGS_INPUTS } from "./transcript-anchors-BjH-68k2.js";
27
27
  import { basename, delimiter, dirname, join, relative, resolve } from "node:path";
28
28
  import { Buffer } from "node:buffer";
29
29
  import * as fs from "node:fs";
@@ -2037,7 +2037,7 @@ function onInputSubmit(handler) {
2037
2037
  * Width tiering is driven by plain-text length estimates — close enough
2038
2038
  * since the segments are ASCII-heavy.
2039
2039
  */
2040
- function Footer({ hints, context, cost = null, status = null, compactionTokens = null, compactionStartedAt = null, retry = null, contextKey = null }) {
2040
+ function Footer({ hints, context, cost = null, status = null, compactionTokens = null, compactionStartedAt = null, retry = null, contextKey = null, usage = null, usageKey = null }) {
2041
2041
  const { width } = useTerminalDimensions();
2042
2042
  const SURFACE = useSurfaces();
2043
2043
  const showCost = typeof cost === "number" && cost > 0;
@@ -2045,7 +2045,9 @@ function Footer({ hints, context, cost = null, status = null, compactionTokens =
2045
2045
  const hW = hintsLength(hints);
2046
2046
  const ctxW = context ? contextIndicatorLength(context) : 0;
2047
2047
  const costW = showCost ? costIndicatorLength(cost) : 0;
2048
- const rightW = costW + ctxW + (costW > 0 && ctxW > 0 ? 3 : 0);
2048
+ const usageW = usage ? usageIndicatorLength(usage) : 0;
2049
+ const costCtxW = costW + ctxW + (costW > 0 && ctxW > 0 ? 3 : 0);
2050
+ const rightW = usageW + costCtxW + (usageW > 0 && costCtxW > 0 ? 3 : 0);
2049
2051
  const [, setCompactTick] = useState(0);
2050
2052
  useEffect(() => {
2051
2053
  if (status !== "compacting") return;
@@ -2072,7 +2074,9 @@ function Footer({ hints, context, cost = null, status = null, compactionTokens =
2072
2074
  /* @__PURE__ */ jsx$1(RightStatus, {
2073
2075
  cost: showCost ? cost : null,
2074
2076
  context,
2075
- contextKey: contextKey ?? null
2077
+ contextKey: contextKey ?? null,
2078
+ usage,
2079
+ usageKey: usageKey ?? null
2076
2080
  })
2077
2081
  ]
2078
2082
  });
@@ -2101,41 +2105,77 @@ function Footer({ hints, context, cost = null, status = null, compactionTokens =
2101
2105
  children: [/* @__PURE__ */ jsx$1("box", { style: { flexGrow: 1 } }), /* @__PURE__ */ jsx$1(RightStatus, {
2102
2106
  cost: showCost ? cost : null,
2103
2107
  context,
2104
- contextKey: contextKey ?? null
2108
+ contextKey: contextKey ?? null,
2109
+ usage,
2110
+ usageKey: usageKey ?? null
2105
2111
  })]
2106
2112
  })]
2107
2113
  });
2108
2114
  }
2109
- function RightStatus({ cost, context, contextKey }) {
2115
+ function RightStatus({ cost, context, contextKey, usage = null, usageKey = null }) {
2110
2116
  const COLOR = useColors();
2117
+ const showContextGroup = cost != null || context !== null;
2118
+ if (!usage && !showContextGroup) return null;
2119
+ return /* @__PURE__ */ jsxs("box", {
2120
+ style: {
2121
+ flexDirection: "row",
2122
+ height: 1
2123
+ },
2124
+ children: [
2125
+ usage && /* @__PURE__ */ jsx$1(ClickableStatus, {
2126
+ keySpec: usageKey,
2127
+ render: (accent) => /* @__PURE__ */ jsx$1(UsageIndicator, {
2128
+ usage,
2129
+ accent
2130
+ })
2131
+ }),
2132
+ usage && showContextGroup && /* @__PURE__ */ jsx$1("text", { children: /* @__PURE__ */ jsx$1("span", {
2133
+ fg: COLOR.mute,
2134
+ children: " · "
2135
+ }) }),
2136
+ showContextGroup && /* @__PURE__ */ jsx$1(ClickableStatus, {
2137
+ keySpec: contextKey,
2138
+ render: (accent) => /* @__PURE__ */ jsxs(Fragment, { children: [
2139
+ cost != null && /* @__PURE__ */ jsx$1(CostIndicator, {
2140
+ cost,
2141
+ accent
2142
+ }),
2143
+ cost != null && context && /* @__PURE__ */ jsx$1("span", {
2144
+ fg: COLOR.mute,
2145
+ children: " · "
2146
+ }),
2147
+ context && /* @__PURE__ */ jsx$1(ContextIndicator, {
2148
+ context,
2149
+ accent
2150
+ })
2151
+ ] })
2152
+ })
2153
+ ]
2154
+ });
2155
+ }
2156
+ /**
2157
+ * One right-edge status segment. Click == the advertised keypress (see
2158
+ * `clickable.tsx`): the panel opens through the exact keyboard handler, so its
2159
+ * gating (chat screen only, agent picked) can't be bypassed by the mouse. A
2160
+ * missing or unparseable spec degrades to inert text.
2161
+ *
2162
+ * Takes `render(accent)` rather than children so the hover highlight reaches
2163
+ * the nested spans without each indicator subscribing to mouse events itself.
2164
+ */
2165
+ function ClickableStatus({ keySpec, render }) {
2111
2166
  const renderer = useRenderer();
2112
2167
  const [hovered, setHovered] = useState(false);
2113
- if (cost == null && !context) return null;
2114
- const body = /* @__PURE__ */ jsxs(Fragment, { children: [
2115
- cost != null && /* @__PURE__ */ jsx$1(CostIndicator, {
2116
- cost,
2117
- accent: hovered
2118
- }),
2119
- cost != null && context && /* @__PURE__ */ jsx$1("span", {
2120
- fg: COLOR.mute,
2121
- children: " · "
2122
- }),
2123
- context && /* @__PURE__ */ jsx$1(ContextIndicator, {
2124
- context,
2125
- accent: hovered
2126
- })
2127
- ] });
2128
- if (!(contextKey !== null && parseHintKeySpec(contextKey) !== null)) return /* @__PURE__ */ jsx$1("text", { children: body });
2168
+ if (!(keySpec !== null && parseHintKeySpec(keySpec) !== null)) return /* @__PURE__ */ jsx$1("text", { children: render(false) });
2129
2169
  return /* @__PURE__ */ jsx$1("text", {
2130
2170
  selectable: false,
2131
2171
  onMouseDown: (event) => {
2132
2172
  event.stopPropagation();
2133
2173
  event.preventDefault();
2134
- dispatchHintKey(renderer, contextKey);
2174
+ dispatchHintKey(renderer, keySpec);
2135
2175
  },
2136
2176
  onMouseOver: () => setHovered(true),
2137
2177
  onMouseOut: () => setHovered(false),
2138
- children: body
2178
+ children: render(hovered)
2139
2179
  });
2140
2180
  }
2141
2181
  /**
@@ -2243,6 +2283,31 @@ function ContextIndicator({ context, accent = false }) {
2243
2283
  })
2244
2284
  ] });
2245
2285
  }
2286
+ /** Gauge marker for the footer's usage segment. */
2287
+ const USAGE_GLYPH = "▰";
2288
+ /**
2289
+ * `▰ 5h 43%`: the active provider's tightest usage window. The ▰ marker is the
2290
+ * same glyph the panel's bars and the footer spinner use, so it reads as a
2291
+ * gauge without spending cells on one.
2292
+ */
2293
+ function UsageIndicator({ usage, accent = false }) {
2294
+ const COLOR = useColors();
2295
+ const color = accent ? COLOR.accent : usage.usedPercent >= 85 ? COLOR.error : usage.usedPercent >= 60 ? COLOR.warn : COLOR.dim;
2296
+ return /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx$1("span", {
2297
+ fg: accent ? COLOR.accent : COLOR.mute,
2298
+ children: `${USAGE_GLYPH} ${usage.label} `
2299
+ }), /* @__PURE__ */ jsx$1("span", {
2300
+ fg: color,
2301
+ children: `${footerUsagePercent(usage.usedPercent)}%`
2302
+ })] });
2303
+ }
2304
+ /**
2305
+ * Whole percent for the status bar, floored so it never rounds 99.9 up to a
2306
+ * ceiling the user hasn't hit. The panel keeps the decimal.
2307
+ */
2308
+ function footerUsagePercent(usedPercent) {
2309
+ return String(Math.floor(usedPercent));
2310
+ }
2246
2311
  function CostIndicator({ cost, accent = false }) {
2247
2312
  const COLOR = useColors();
2248
2313
  const fg = accent ? COLOR.accent : COLOR.money ?? COLOR.warn;
@@ -2448,6 +2513,9 @@ function formatCost(cost) {
2448
2513
  function costIndicatorLength(cost) {
2449
2514
  return 1 + formatCost(cost).length;
2450
2515
  }
2516
+ function usageIndicatorLength(usage) {
2517
+ return 2 + usage.label.length + 1 + footerUsagePercent(usage.usedPercent).length + 1;
2518
+ }
2451
2519
  const SPINNER_FRAMES = [
2452
2520
  "▰▱▱▱▱",
2453
2521
  "▰▰▱▱▱",
@@ -3230,6 +3298,22 @@ function EmptyState() {
3230
3298
  /** Left-pad applied per depth level (in columns). */
3231
3299
  const INDENT_PER_DEPTH = 2;
3232
3300
  const THINKING_GUTTER_WIDTH = 4;
3301
+ /**
3302
+ * Notice rows: `<glyph> <label padded> <message>`. The label column is padded
3303
+ * so a stack of notices lines up down the transcript; 15 fits the longest
3304
+ * label in use (`auto-compaction`) without pushing short ones far from their
3305
+ * text.
3306
+ *
3307
+ * Single-width glyphs only. Emoji (the `🔌` / `🗜` / `🩹` these rows replaced)
3308
+ * render two cells wide in most terminals and one in some, which is exactly
3309
+ * the ambiguity an aligned gutter cannot absorb.
3310
+ */
3311
+ const NOTICE_LABEL_WIDTH = 15;
3312
+ const NOTICE_GLYPH = {
3313
+ info: "·",
3314
+ warn: "!",
3315
+ error: "✗"
3316
+ };
3233
3317
  function indentFor(depth) {
3234
3318
  return depth && depth > 0 ? depth * INDENT_PER_DEPTH : 0;
3235
3319
  }
@@ -3253,6 +3337,46 @@ function rowStyle(paddingLeft) {
3253
3337
  alignSelf: "stretch"
3254
3338
  };
3255
3339
  }
3340
+ /**
3341
+ * One-line row for a harness event: model fallback, repeat-guard trip, history
3342
+ * repair, run error. Glyph and label carry the severity color, the message is
3343
+ * dim, and the whole row is clipped rather than wrapped so a long provider
3344
+ * error can't push a wall of text through the transcript.
3345
+ *
3346
+ * Multi-line messages collapse to their first non-empty line plus a `+N lines`
3347
+ * counter. The full text is what gets persisted, so nothing is lost.
3348
+ */
3349
+ function NoticeRow({ row, notice, text }) {
3350
+ const COLOR = useColors();
3351
+ const accent = notice.severity === "error" ? COLOR.error : notice.severity === "warn" ? COLOR.warn : COLOR.mute;
3352
+ const lines = text.split("\n").map((line) => line.trim()).filter((line) => line !== "");
3353
+ const head = lines[0] ?? "";
3354
+ const extra = lines.length - 1;
3355
+ return /* @__PURE__ */ jsx$1("box", {
3356
+ style: row,
3357
+ children: /* @__PURE__ */ jsxs("text", {
3358
+ wrapMode: "none",
3359
+ children: [
3360
+ /* @__PURE__ */ jsx$1("span", {
3361
+ fg: accent,
3362
+ children: `${NOTICE_GLYPH[notice.severity]} `
3363
+ }),
3364
+ /* @__PURE__ */ jsx$1("span", {
3365
+ fg: accent,
3366
+ children: `${notice.label.padEnd(NOTICE_LABEL_WIDTH)} `
3367
+ }),
3368
+ /* @__PURE__ */ jsx$1("span", {
3369
+ fg: COLOR.dim,
3370
+ children: head
3371
+ }),
3372
+ extra > 0 ? /* @__PURE__ */ jsx$1("span", {
3373
+ fg: COLOR.mute,
3374
+ children: ` +${extra} line${extra === 1 ? "" : "s"}`
3375
+ }) : null
3376
+ ]
3377
+ })
3378
+ });
3379
+ }
3256
3380
  function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = false, selected = false, settleCodeEpoch = 0, liveSpawn, searchQuery, searchTitleMatch = false }) {
3257
3381
  const COLOR = useColors();
3258
3382
  const { settings } = useSettings();
@@ -3269,13 +3393,19 @@ function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = fals
3269
3393
  attachments: event.attachments,
3270
3394
  searchQuery
3271
3395
  });
3272
- case "info": return /* @__PURE__ */ jsx$1("box", {
3273
- style: row,
3274
- children: /* @__PURE__ */ jsx$1("text", {
3275
- fg: COLOR.dim,
3276
- children: safeText
3277
- })
3278
- });
3396
+ case "info":
3397
+ if (event.notice) return /* @__PURE__ */ jsx$1(NoticeRow, {
3398
+ row,
3399
+ notice: event.notice,
3400
+ text: safeText
3401
+ });
3402
+ return /* @__PURE__ */ jsx$1("box", {
3403
+ style: row,
3404
+ children: /* @__PURE__ */ jsx$1("text", {
3405
+ fg: COLOR.dim,
3406
+ children: safeText
3407
+ })
3408
+ });
3279
3409
  case "thinking": {
3280
3410
  const startsRun = !(previous?.kind === "thinking" && previous.childId === event.childId);
3281
3411
  const thinkingText = safeText.replace(/<!--[\s\S]*?-->/g, "").replace(/\*\*([^*]+)\*\*/g, "$1").trimEnd();
@@ -3360,16 +3490,22 @@ function EventLineImpl({ event, previous, depthOffset = 0, hideChildLabel = fals
3360
3490
  indent: row.paddingLeft,
3361
3491
  diff: event.syntax === "diff"
3362
3492
  });
3363
- case "error": return /* @__PURE__ */ jsx$1("box", {
3364
- style: row,
3365
- children: /* @__PURE__ */ jsxs("text", {
3366
- fg: COLOR.error,
3367
- children: [/* @__PURE__ */ jsx$1("span", {
3493
+ case "error":
3494
+ if (event.notice) return /* @__PURE__ */ jsx$1(NoticeRow, {
3495
+ row,
3496
+ notice: event.notice,
3497
+ text: safeText
3498
+ });
3499
+ return /* @__PURE__ */ jsx$1("box", {
3500
+ style: row,
3501
+ children: /* @__PURE__ */ jsxs("text", {
3368
3502
  fg: COLOR.error,
3369
- children: "✗ "
3370
- }), safeText]
3371
- })
3372
- });
3503
+ children: [/* @__PURE__ */ jsx$1("span", {
3504
+ fg: COLOR.error,
3505
+ children: "✗ "
3506
+ }), safeText]
3507
+ })
3508
+ });
3373
3509
  case "markdown": return /* @__PURE__ */ jsx$1("box", {
3374
3510
  style: row,
3375
3511
  children: /* @__PURE__ */ jsx$1(MarkdownBlock, {
@@ -6876,8 +7012,14 @@ function OptionRow({ option, isEnabled, isFocused, highlightBg }) {
6876
7012
  *
6877
7013
  * Empty states:
6878
7014
  * - No available providers → "no providers configured" notice.
6879
- * - Query has no matches → "no matches" row, keep the search input
6880
- * live so the user can backspace out.
7015
+ * - Query has no matches → a selectable "use anyway" row carrying the
7016
+ * typed id (see `parseCustomModelQuery`), so a model the local catalog
7017
+ * doesn't know can still be picked. The search input stays live.
7018
+ *
7019
+ * Off-catalog models: the active pick and every chain entry are passed to
7020
+ * `buildModelCatalog` as `include`, so a model the provider has since
7021
+ * retired still gets a row (marked `not in catalog`) and can be selected
7022
+ * away from. Without it such an entry is invisible and re-persists forever.
6881
7023
  */
6882
7024
  /** Visible rows in the windowed list. Keeps the modal a stable size on long catalogs. */
6883
7025
  const VISIBLE_ROWS = 10;
@@ -6921,22 +7063,36 @@ function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onAppl
6921
7063
  useEffect(() => {
6922
7064
  inputRef.current?.focus();
6923
7065
  }, []);
6924
- const catalog = useMemo(() => buildModelCatalog({
6925
- providers,
6926
- modelsFor,
6927
- current
6928
- }), [
6929
- providers,
6930
- modelsFor,
6931
- current
6932
- ]);
6933
7066
  const chainMode = onApplyChain !== void 0;
6934
7067
  const [initialChain] = useState(() => chain && chain.length > 0 ? [...chain] : [{
6935
7068
  provider: current.providerKey,
6936
7069
  model: current.modelId
6937
7070
  }]);
6938
7071
  const [chainState, setChainState] = useState(initialChain);
7072
+ const [typedEntries, setTypedEntries] = useState([]);
6939
7073
  const modalApi = useModalState();
7074
+ const [includeSeed] = useState(() => [{
7075
+ providerKey: current.providerKey,
7076
+ modelId: current.modelId
7077
+ }, ...(chain ?? []).map((entry) => ({
7078
+ providerKey: entry.provider,
7079
+ modelId: entry.model
7080
+ }))]);
7081
+ const catalog = useMemo(() => buildModelCatalog({
7082
+ providers,
7083
+ modelsFor,
7084
+ current,
7085
+ include: [...includeSeed, ...typedEntries.map((entry) => ({
7086
+ providerKey: entry.provider,
7087
+ modelId: entry.model
7088
+ }))]
7089
+ }), [
7090
+ providers,
7091
+ modelsFor,
7092
+ current,
7093
+ includeSeed,
7094
+ typedEntries
7095
+ ]);
6940
7096
  const filtered = useMemo(() => {
6941
7097
  const matches = filterModelCatalog(catalog, query);
6942
7098
  if (query.trim()) return matches;
@@ -6952,19 +7108,40 @@ function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onAppl
6952
7108
  setSelectedIdx(0);
6953
7109
  }, []);
6954
7110
  const safeIndex = filtered.length === 0 ? 0 : Math.min(selectedIdx, filtered.length - 1);
7111
+ const custom = useMemo(() => filtered.length === 0 ? parseCustomModelQuery(query, providers, current.providerKey) : null, [
7112
+ filtered.length,
7113
+ query,
7114
+ providers,
7115
+ current.providerKey
7116
+ ]);
7117
+ const customEntry = custom ? {
7118
+ providerKey: custom.providerKey,
7119
+ providerLabel: custom.providerLabel,
7120
+ model: {
7121
+ id: custom.modelId,
7122
+ contextWindow: 0
7123
+ },
7124
+ offCatalog: true,
7125
+ searchCorpus: ""
7126
+ } : null;
6955
7127
  const commit = () => {
6956
7128
  const row = filtered[safeIndex];
6957
- if (!row) return;
7129
+ const target = row ? {
7130
+ provider: row.providerKey,
7131
+ model: row.model.id
7132
+ } : custom ? {
7133
+ provider: custom.providerKey,
7134
+ model: custom.modelId
7135
+ } : null;
7136
+ if (!target) return;
6958
7137
  if (chainMode) {
6959
- setChainState((prev) => toggleChainEntry(prev, {
6960
- provider: row.providerKey,
6961
- model: row.model.id
6962
- }));
7138
+ if (!row) setTypedEntries((prev) => prev.some((e) => e.provider === target.provider && e.model === target.model) ? prev : [...prev, target]);
7139
+ setChainState((prev) => toggleChainEntry(prev, target));
6963
7140
  return;
6964
7141
  }
6965
7142
  onPick?.({
6966
- providerKey: row.providerKey,
6967
- modelId: row.model.id
7143
+ providerKey: target.provider,
7144
+ modelId: target.model
6968
7145
  });
6969
7146
  };
6970
7147
  const handleClose = () => {
@@ -7052,7 +7229,7 @@ function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onAppl
7052
7229
  height: VISIBLE_ROWS,
7053
7230
  flexShrink: 0
7054
7231
  },
7055
- children: filtered.length === 0 ? /* @__PURE__ */ jsxs("text", {
7232
+ children: filtered.length === 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("text", {
7056
7233
  fg: COLOR.dim,
7057
7234
  children: [/* @__PURE__ */ jsx$1("span", {
7058
7235
  fg: COLOR.mute,
@@ -7061,7 +7238,13 @@ function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onAppl
7061
7238
  fg: COLOR.warn,
7062
7239
  children: query.trim()
7063
7240
  })]
7064
- }) : viewport.slice.map((entry, i) => /* @__PURE__ */ jsx$1(ModelRow, {
7241
+ }), customEntry && /* @__PURE__ */ jsx$1(ModelRow, {
7242
+ entry: customEntry,
7243
+ isCurrent: false,
7244
+ isFocused: true,
7245
+ highlightBg: SURFACE.selection,
7246
+ chainPosition: chainMode ? chainPositionOf(chainState, customEntry.providerKey, customEntry.model.id) : null
7247
+ })] }) : viewport.slice.map((entry, i) => /* @__PURE__ */ jsx$1(ModelRow, {
7065
7248
  entry,
7066
7249
  isCurrent: entry.providerKey === current.providerKey && entry.model.id === current.modelId,
7067
7250
  isFocused: viewport.start + i === safeIndex,
@@ -7077,7 +7260,7 @@ function ModelPickerModal({ providers, modelsFor, current, onPick, chain, onAppl
7077
7260
  },
7078
7261
  {
7079
7262
  key: "↵",
7080
- label: chainMode ? "add/remove" : "select",
7263
+ label: customEntry ? "use anyway" : chainMode ? "add/remove" : "select",
7081
7264
  labelColor: COLOR.dim
7082
7265
  },
7083
7266
  chainMode && {
@@ -7305,6 +7488,7 @@ function ModelRow({ entry, isCurrent, isFocused, highlightBg, chainPosition = nu
7305
7488
  const COLOR = useColors();
7306
7489
  const marker = chainPosition !== null ? chainPosition > 9 ? "+" : String(chainPosition) : isCurrent ? "●" : " ";
7307
7490
  const markerColor = chainPosition !== null || isCurrent ? COLOR.brand : COLOR.mute;
7491
+ const detail = entry.offCatalog ? "not in catalog" : describeModel(entry.model);
7308
7492
  return /* @__PURE__ */ jsx$1("box", {
7309
7493
  style: {
7310
7494
  height: 1,
@@ -7341,8 +7525,8 @@ function ModelRow({ entry, isCurrent, isFocused, highlightBg, chainPosition = nu
7341
7525
  children: " · "
7342
7526
  }),
7343
7527
  /* @__PURE__ */ jsx$1("span", {
7344
- fg: COLOR.mute,
7345
- children: describeModel(entry.model)
7528
+ fg: entry.offCatalog ? COLOR.warn : COLOR.mute,
7529
+ children: detail
7346
7530
  })
7347
7531
  ]
7348
7532
  })
@@ -9761,8 +9945,11 @@ function MultiSelectQuestionInput({ question, initialChoiceIds, onSubmit }) {
9761
9945
  const required = question.required ?? true;
9762
9946
  const [cursor, setCursor] = useState(0);
9763
9947
  const [picked, setPicked] = useState(() => {
9764
- const valid = new Set(question.choices.map((c) => c.id));
9765
- return new Set((initialChoiceIds ?? []).filter((id) => valid.has(id)));
9948
+ if (initialChoiceIds) {
9949
+ const valid = new Set(question.choices.map((c) => c.id));
9950
+ return new Set(initialChoiceIds.filter((id) => valid.has(id)));
9951
+ }
9952
+ return new Set(question.choices.filter((c) => c.selected).map((c) => c.id));
9766
9953
  });
9767
9954
  const moveCursor = useCallback((delta) => {
9768
9955
  setCursor((c) => {
@@ -15674,7 +15861,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15674
15861
  category: "agent",
15675
15862
  id: "compaction-model",
15676
15863
  label: "Compaction model",
15677
- description: `current: ${settings.compactionModel || "main model"} — pick any provider's model for compaction summaries (pick the main model to clear)`,
15864
+ description: `current: ${settings.compactionModel || "main model"}. Pick any provider's model for compaction summaries, or the main model to clear`,
15678
15865
  onPick: actions.onPickCompactionModel
15679
15866
  });
15680
15867
  if (actions?.onImportClaudeCode) acts.push({
@@ -16064,14 +16251,14 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
16064
16251
  const { segment } = row;
16065
16252
  if (segment.id === "system" && !segment.path && actions?.onCreateSystemPrompt) {
16066
16253
  if (!instructionsCanOverride) {
16067
- setInstructionsNotice(instructionsOverrideBlockedBy === "scope" ? "Your instruction scope excludes user-scope files, so a SYSTEM.md in your home directory would not be loaded. Change it on the Agent tab. Press tab to read the prompt inline." : "This profile supplies its own system prompt — a SYSTEM.md override would not apply. Press tab to read it inline.");
16254
+ setInstructionsNotice(instructionsOverrideBlockedBy === "scope" ? "Your instruction scope excludes user-scope files, so a SYSTEM.md in your home directory would not be loaded. Change it on the Agent tab. Press tab to read the prompt inline." : "This profile supplies its own system prompt, so a SYSTEM.md override would not apply. Press tab to read it inline.");
16068
16255
  return;
16069
16256
  }
16070
16257
  Promise.resolve(actions.onCreateSystemPrompt()).catch((err) => setInstructionsNotice(`Could not create SYSTEM.md: ${errorMessage(err)}`));
16071
16258
  return;
16072
16259
  }
16073
16260
  if (segment.path) Promise.resolve(actions?.onOpenInstructionsFile?.(segment.path)).catch((err) => setInstructionsNotice(`Could not open ${segment.label}: ${errorMessage(err)}`));
16074
- else setInstructionsNotice(`"${segment.label}" is ${instructionOriginBadge(segment)} — no file to open. Press tab to read it inline.`);
16261
+ else setInstructionsNotice(`"${segment.label}" is ${instructionOriginBadge(segment)}, no file to open. Press tab to read it inline.`);
16075
16262
  return;
16076
16263
  }
16077
16264
  if (activeTab === "keybindings") {
@@ -16276,7 +16463,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
16276
16463
  key.preventDefault();
16277
16464
  const ext = focusedExtension;
16278
16465
  if (ext?.dir) actions?.onEditExtension?.(ext.dir);
16279
- else if (ext) setExtEditNotice(`"${ext.name}" is ${ext.source} — no directory to edit. Press ctrl+a to scaffold an editable extension.`);
16466
+ else if (ext) setExtEditNotice(`"${ext.name}" is ${ext.source}, no directory to edit. Press ctrl+a to scaffold an editable extension.`);
16280
16467
  return;
16281
16468
  }
16282
16469
  if (key.ctrl && key.name === "g" && actions?.onInstallExtension) {
@@ -16398,7 +16585,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
16398
16585
  if (skill.baseDir) actions.onEditSkill(skill);
16399
16586
  else setSkillOp({
16400
16587
  kind: "notice",
16401
- text: `"${skill.name}" is ${skill.source ?? "inline"} — no directory to edit.`
16588
+ text: `"${skill.name}" is ${skill.source ?? "inline"}, no directory to edit.`
16402
16589
  });
16403
16590
  return;
16404
16591
  }
@@ -16470,7 +16657,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
16470
16657
  setInstructionsNotice("reloading…");
16471
16658
  actions.onRefreshInstructions().then((outcome) => {
16472
16659
  setInstructionsRefreshKey((n) => n + 1);
16473
- setInstructionsNotice(outcome === "rebuilt" ? "reloaded — agent rebuilt; MCP servers reconnect on the next turn" : outcome === "queued" ? "reloaded — agent rebuild queued until the current run finishes" : "reloaded from disk");
16660
+ setInstructionsNotice(outcome === "rebuilt" ? "reloaded, agent rebuilt. MCP servers reconnect on the next turn" : outcome === "queued" ? "reloaded, agent rebuild queued until the current run finishes" : "reloaded from disk");
16474
16661
  }).catch((err) => setInstructionsNotice(`Refresh failed: ${errorMessage(err)}`));
16475
16662
  return;
16476
16663
  }
@@ -17888,7 +18075,7 @@ function renderEmptyToolsListRow({ serverName, anchorId, focused, bg, COLOR, ref
17888
18075
  children: focused ? TOOL_ROW_FOCUS_INDENT$1 : TOOL_ROW_INDENT$1
17889
18076
  }), /* @__PURE__ */ jsx$1("span", {
17890
18077
  fg: COLOR.mute,
17891
- children: refreshing ? "⟲ Fetching tool list…" : "⟲ No tools reported — check the server command or login; ctrl+r retries."
18078
+ children: refreshing ? "⟲ Fetching tool list…" : "⟲ No tools reported. Check the server command or login, ctrl+r retries."
17892
18079
  })]
17893
18080
  }, `tool-empty:${serverName}`);
17894
18081
  }
@@ -17908,11 +18095,11 @@ function InstructionsList({ rows, totalCount, loading, error, live, shadowedSyst
17908
18095
  children: [
17909
18096
  !live && /* @__PURE__ */ jsx$1("text", {
17910
18097
  fg: COLOR.mute,
17911
- children: " No active session — showing what your next one will load. MCP and extension context is added at run time."
18098
+ children: " No active session, showing what your next one will load. MCP and extension context is added at run time."
17912
18099
  }),
17913
18100
  shadowedSystemPath !== void 0 && /* @__PURE__ */ jsx$1("text", {
17914
18101
  fg: COLOR.mute,
17915
- children: canOverrideSystem ? ` ${displayPath$1(shadowedSystemPath, homedir())} is shadowed by the project SYSTEM.md and is not loaded.` : overrideBlockedBy === "scope" ? ` ${displayPath$1(shadowedSystemPath, homedir())} is not loaded — your instruction scope excludes it.` : ` ${displayPath$1(shadowedSystemPath, homedir())} is not loaded — this profile supplies its own system prompt.`
18102
+ children: canOverrideSystem ? ` ${displayPath$1(shadowedSystemPath, homedir())} is shadowed by the project SYSTEM.md and is not loaded.` : overrideBlockedBy === "scope" ? ` ${displayPath$1(shadowedSystemPath, homedir())} is not loaded, your instruction scope excludes it.` : ` ${displayPath$1(shadowedSystemPath, homedir())} is not loaded, this profile supplies its own system prompt.`
17916
18103
  }),
17917
18104
  rows.map(({ segment, expanded }, i) => {
17918
18105
  const focused = i === cursor;
@@ -18080,7 +18267,7 @@ function AuthenticationList({ view, providers, rows, cursor, highlightBg, query
18080
18267
  }),
18081
18268
  /* @__PURE__ */ jsx$1("span", {
18082
18269
  fg: view.currentProviderLabel ? COLOR.accent : COLOR.mute,
18083
- children: view.currentProviderLabel ?? "— not selected —"
18270
+ children: view.currentProviderLabel ?? "not selected"
18084
18271
  }),
18085
18272
  view.currentProviderKey && view.currentProviderLabel && view.currentProviderKey !== view.currentProviderLabel.toLowerCase() && /* @__PURE__ */ jsx$1("span", {
18086
18273
  fg: COLOR.dim,
@@ -18095,7 +18282,7 @@ function AuthenticationList({ view, providers, rows, cursor, highlightBg, query
18095
18282
  children: " model "
18096
18283
  }), /* @__PURE__ */ jsx$1("span", {
18097
18284
  fg: view.currentModel ? COLOR.model : COLOR.mute,
18098
- children: view.currentModel ?? "— not selected —"
18285
+ children: view.currentModel ?? "not selected"
18099
18286
  })]
18100
18287
  })
18101
18288
  ]
@@ -18752,12 +18939,12 @@ function canLogout$1(status) {
18752
18939
  return status.kind === "authed" || status.kind === "error" || status.kind === "authorizing";
18753
18940
  }
18754
18941
  function searchPlaceholder(tab) {
18755
- if (tab === "skills") return "search skills — name, description…";
18756
- if (tab === "mcps") return "search MCP servers — name, transport, command/URL…";
18757
- if (tab === "extensions") return "search extensions — name, version, description, settings…";
18758
- if (tab === "keybindings") return "search keybindings — action, key, description…";
18759
- if (tab === "authentication") return "search providers — name, method…";
18760
- return `search ${TAB_LABELS[tab].toLowerCase()} settings — name, description…`;
18942
+ if (tab === "skills") return "search skills by name or description…";
18943
+ if (tab === "mcps") return "search MCP servers by name, transport or command…";
18944
+ if (tab === "extensions") return "search extensions by name, version or description…";
18945
+ if (tab === "keybindings") return "search keybindings by action, key or description…";
18946
+ if (tab === "authentication") return "search providers by name or method…";
18947
+ return `search ${TAB_LABELS[tab].toLowerCase()} settings by name or description…`;
18761
18948
  }
18762
18949
  function displayPath$1(path, home) {
18763
18950
  if (home && path.startsWith(`${home}/`)) return `~/${path.slice(home.length + 1)}`;
@@ -19849,6 +20036,322 @@ function blockSummary(turns) {
19849
20036
  return parts.length === 0 ? "(empty)" : parts.join(" · ");
19850
20037
  }
19851
20038
  //#endregion
20039
+ //#region src/tui/use-provider-usage.ts
20040
+ /**
20041
+ * Floor on run-completion refetches. Back-to-back short turns shouldn't each
20042
+ * cost a request, and usage windows don't move meaningfully inside 15s.
20043
+ */
20044
+ const RUN_REFRESH_MIN_AGE_MS = 15e3;
20045
+ /**
20046
+ * Usage for the active provider, refetched when the provider changes and when
20047
+ * a run finishes. Stays `null` for providers with no usage surface or no stored
20048
+ * credential, which is what hides the footer indicator.
20049
+ */
20050
+ function useActiveProviderUsage({ dataDir, descriptor, enabled, busy }) {
20051
+ const [snapshot, setSnapshot] = useState(null);
20052
+ const generationRef = useRef(0);
20053
+ const mountedRef = useRef(true);
20054
+ useEffect(() => () => {
20055
+ mountedRef.current = false;
20056
+ }, []);
20057
+ const snapshotRef = useRef(snapshot);
20058
+ snapshotRef.current = snapshot;
20059
+ const providerKey = descriptor?.key ?? null;
20060
+ const active = enabled && descriptor !== null && providerUsageSupported(descriptor);
20061
+ const load = useCallback((refresh) => {
20062
+ if (!enabled || !descriptor || !providerUsageSupported(descriptor)) return;
20063
+ const generation = ++generationRef.current;
20064
+ fetchProviderUsage(dataDir, descriptor, { refresh }).then((next) => {
20065
+ if (mountedRef.current && generationRef.current === generation) setSnapshot(next);
20066
+ }).catch(() => {});
20067
+ }, [
20068
+ dataDir,
20069
+ descriptor,
20070
+ enabled
20071
+ ]);
20072
+ useEffect(() => {
20073
+ generationRef.current++;
20074
+ setSnapshot(null);
20075
+ if (active) load(false);
20076
+ }, [providerKey, active]);
20077
+ const prevBusyRef = useRef(busy);
20078
+ useEffect(() => {
20079
+ const finished = prevBusyRef.current && !busy;
20080
+ prevBusyRef.current = busy;
20081
+ if (!finished || !active) return;
20082
+ const current = snapshotRef.current;
20083
+ if (current && Date.now() - current.fetchedAt < RUN_REFRESH_MIN_AGE_MS) return;
20084
+ load(true);
20085
+ }, [busy, active]);
20086
+ return snapshot;
20087
+ }
20088
+ /**
20089
+ * Usage for every registered provider that has a usage surface and a stored
20090
+ * credential. Loads once on mount unless `autoLoad` is off; `refresh`
20091
+ * re-probes them all either way.
20092
+ */
20093
+ function useAllProviderUsage({ dataDir, registry, autoLoad = true }) {
20094
+ const [snapshots, setSnapshots] = useState([]);
20095
+ const [loading, setLoading] = useState(autoLoad);
20096
+ const [loaded, setLoaded] = useState(false);
20097
+ const generationRef = useRef(0);
20098
+ const mountedRef = useRef(true);
20099
+ useEffect(() => () => {
20100
+ mountedRef.current = false;
20101
+ }, []);
20102
+ const registryRef = useRef(registry);
20103
+ registryRef.current = registry;
20104
+ const load = useCallback((refresh) => {
20105
+ const generation = ++generationRef.current;
20106
+ setLoading(true);
20107
+ fetchAllProviderUsage(dataDir, registryRef.current, { refresh }).then((next) => {
20108
+ if (!mountedRef.current || generationRef.current !== generation) return;
20109
+ setSnapshots(next);
20110
+ setLoading(false);
20111
+ setLoaded(true);
20112
+ }).catch(() => {
20113
+ if (mountedRef.current && generationRef.current === generation) setLoading(false);
20114
+ });
20115
+ }, [dataDir]);
20116
+ useEffect(() => {
20117
+ if (autoLoad) load(false);
20118
+ }, [load, autoLoad]);
20119
+ return {
20120
+ snapshots,
20121
+ loading,
20122
+ loaded,
20123
+ refresh: useCallback(() => load(true), [load])
20124
+ };
20125
+ }
20126
+ //#endregion
20127
+ //#region src/tui/usage-panel.tsx
20128
+ /** Cells in a window's bar. Fixed, so every row's numbers line up. */
20129
+ const BAR_WIDTH = 12;
20130
+ /** Right-hand columns, padded so percents and resets align down the panel. */
20131
+ const PERCENT_COL = 6;
20132
+ const TRAILING_COL = 16;
20133
+ /** Percent thresholds, shared with the footer indicator. */
20134
+ function usageColor(usedPercent, COLOR) {
20135
+ if (usedPercent >= 85) return COLOR.error;
20136
+ if (usedPercent >= 60) return COLOR.warn;
20137
+ return COLOR.brand;
20138
+ }
20139
+ /**
20140
+ * Time to reset, coarsened to whole days past 24h. `null` when the provider
20141
+ * reported no reset, `resetting` once it is due.
20142
+ *
20143
+ * Every step rounds DOWN, like the footer's percent: 36h reads as `1d`, not
20144
+ * `2d`. Overstating the headroom left on a window is the wrong direction to
20145
+ * be wrong in.
20146
+ */
20147
+ function resetLabel(resetsAt, now = Date.now()) {
20148
+ if (resetsAt === null) return null;
20149
+ const remaining = resetsAt - now;
20150
+ if (remaining <= 0) return "resetting";
20151
+ const minutes = Math.floor(remaining / 6e4);
20152
+ if (minutes < 60) return `resets in ${Math.max(1, minutes)}m`;
20153
+ const hours = Math.floor(minutes / 60);
20154
+ if (hours < 24) {
20155
+ const remMinutes = minutes % 60;
20156
+ return `resets in ${hours}h${remMinutes > 0 ? `${remMinutes}m` : ""}`;
20157
+ }
20158
+ return `resets in ${Math.max(1, Math.floor(hours / 24))}d`;
20159
+ }
20160
+ function UsagePanelModal({ dataDir, registry, activeProviderKey, autoLoad = true }) {
20161
+ const COLOR = useColors();
20162
+ const envOptedOut = usageChecksOptedOut();
20163
+ const { snapshots, loading, loaded, refresh } = useAllProviderUsage({
20164
+ dataDir,
20165
+ registry,
20166
+ autoLoad: autoLoad && !envOptedOut
20167
+ });
20168
+ useKeyboard((key) => {
20169
+ if (key.ctrl && key.name === "r" && !envOptedOut && !loading) refresh();
20170
+ });
20171
+ const ordered = [...snapshots].sort((a, b) => {
20172
+ return (a.providerKey === activeProviderKey ? 0 : 1) - (b.providerKey === activeProviderKey ? 0 : 1);
20173
+ });
20174
+ const { height: termHeight } = useTerminalDimensions();
20175
+ return /* @__PURE__ */ jsxs(Modal, {
20176
+ title: "subscription usage",
20177
+ maxWidth: 72,
20178
+ children: [/* @__PURE__ */ jsx$1("box", {
20179
+ style: {
20180
+ flexDirection: "column",
20181
+ maxHeight: Math.max(6, termHeight - 11),
20182
+ flexShrink: 1,
20183
+ overflow: "hidden"
20184
+ },
20185
+ children: /* @__PURE__ */ jsx$1("scrollbox", {
20186
+ focusable: false,
20187
+ stickyScroll: false,
20188
+ style: {
20189
+ flexGrow: 1,
20190
+ flexShrink: 1
20191
+ },
20192
+ children: ordered.length === 0 ? /* @__PURE__ */ jsx$1("text", {
20193
+ wrapMode: "word",
20194
+ fg: COLOR.mute,
20195
+ children: emptyStateLine({
20196
+ loading,
20197
+ loaded,
20198
+ envOptedOut,
20199
+ autoLoad
20200
+ })
20201
+ }) : ordered.map((snapshot, index) => /* @__PURE__ */ jsx$1(ProviderSection, {
20202
+ snapshot,
20203
+ isActive: snapshot.providerKey === activeProviderKey,
20204
+ first: index === 0,
20205
+ COLOR
20206
+ }, snapshot.providerKey))
20207
+ })
20208
+ }), /* @__PURE__ */ jsx$1(ModalHints, { hints: [...envOptedOut ? [] : [{
20209
+ key: "ctrl+r",
20210
+ label: loading ? "refreshing" : "refresh",
20211
+ labelColor: COLOR.dim
20212
+ }], {
20213
+ key: "esc",
20214
+ label: "close",
20215
+ labelColor: COLOR.dim
20216
+ }] })]
20217
+ });
20218
+ }
20219
+ function ProviderSection({ snapshot, isActive, first, COLOR }) {
20220
+ const SURFACE = useSurfaces();
20221
+ const looseCredits = snapshot.windows.some((win) => win.id === "credits") ? null : snapshot.credits;
20222
+ return /* @__PURE__ */ jsxs("box", {
20223
+ style: {
20224
+ flexDirection: "column",
20225
+ flexShrink: 0
20226
+ },
20227
+ children: [
20228
+ !first && /* @__PURE__ */ jsx$1("box", { style: {
20229
+ height: 1,
20230
+ flexShrink: 0
20231
+ } }),
20232
+ /* @__PURE__ */ jsxs("box", {
20233
+ style: {
20234
+ flexDirection: "row",
20235
+ height: 1,
20236
+ flexShrink: 0,
20237
+ backgroundColor: isActive ? SURFACE.selection : void 0
20238
+ },
20239
+ children: [
20240
+ /* @__PURE__ */ jsxs("text", {
20241
+ wrapMode: "none",
20242
+ children: [/* @__PURE__ */ jsx$1("span", {
20243
+ fg: isActive ? COLOR.brand : COLOR.model,
20244
+ children: snapshot.label
20245
+ }), snapshot.plan ? /* @__PURE__ */ jsx$1("span", {
20246
+ fg: COLOR.mute,
20247
+ children: ` · ${snapshot.plan}`
20248
+ }) : null]
20249
+ }),
20250
+ /* @__PURE__ */ jsx$1("box", { style: { flexGrow: 1 } }),
20251
+ isActive && /* @__PURE__ */ jsx$1("text", {
20252
+ wrapMode: "none",
20253
+ fg: COLOR.brand,
20254
+ children: "active"
20255
+ })
20256
+ ]
20257
+ }),
20258
+ snapshot.status === "ok" ? /* @__PURE__ */ jsxs(Fragment, { children: [snapshot.windows.map((win) => /* @__PURE__ */ jsx$1(WindowRow, {
20259
+ win,
20260
+ trailing: win.id === "credits" && snapshot.credits ? formatBalance(snapshot.credits) : resetLabel(win.resetsAt),
20261
+ COLOR
20262
+ }, win.id)), looseCredits && /* @__PURE__ */ jsxs("box", {
20263
+ style: {
20264
+ flexDirection: "row",
20265
+ height: 1,
20266
+ flexShrink: 0
20267
+ },
20268
+ children: [
20269
+ /* @__PURE__ */ jsx$1("text", {
20270
+ wrapMode: "none",
20271
+ fg: COLOR.dim,
20272
+ children: "credits"
20273
+ }),
20274
+ /* @__PURE__ */ jsx$1("box", { style: { flexGrow: 1 } }),
20275
+ /* @__PURE__ */ jsx$1("text", {
20276
+ wrapMode: "none",
20277
+ fg: COLOR.mute,
20278
+ children: formatBalance(looseCredits).padStart(22)
20279
+ })
20280
+ ]
20281
+ })] }) : /* @__PURE__ */ jsx$1("box", {
20282
+ style: {
20283
+ flexDirection: "row",
20284
+ height: 1,
20285
+ flexShrink: 0
20286
+ },
20287
+ children: /* @__PURE__ */ jsx$1("text", {
20288
+ wrapMode: "none",
20289
+ fg: snapshot.status === "unauthorized" ? COLOR.warn : COLOR.mute,
20290
+ children: `${snapshot.status === "unauthorized" ? "not authorized" : "unavailable"} · ${snapshot.error ?? "no data"}`
20291
+ })
20292
+ })
20293
+ ]
20294
+ });
20295
+ }
20296
+ /** One window: `▰▰▰▱▱ 5h 43% resets in 2h14m`. */
20297
+ function WindowRow({ win, trailing, COLOR }) {
20298
+ const filled = Math.min(BAR_WIDTH, Math.max(win.usedPercent > 0 ? 1 : 0, Math.round(win.usedPercent / 100 * BAR_WIDTH)));
20299
+ const color = usageColor(win.usedPercent, COLOR);
20300
+ return /* @__PURE__ */ jsxs("box", {
20301
+ style: {
20302
+ flexDirection: "row",
20303
+ height: 1,
20304
+ flexShrink: 0
20305
+ },
20306
+ children: [
20307
+ /* @__PURE__ */ jsxs("text", {
20308
+ wrapMode: "none",
20309
+ children: [
20310
+ /* @__PURE__ */ jsx$1("span", {
20311
+ fg: color,
20312
+ children: "▰".repeat(filled)
20313
+ }),
20314
+ /* @__PURE__ */ jsx$1("span", {
20315
+ fg: COLOR.mute,
20316
+ children: "▱".repeat(BAR_WIDTH - filled)
20317
+ }),
20318
+ /* @__PURE__ */ jsx$1("span", {
20319
+ fg: COLOR.dim,
20320
+ children: ` ${win.label}`
20321
+ })
20322
+ ]
20323
+ }),
20324
+ /* @__PURE__ */ jsx$1("box", { style: { flexGrow: 1 } }),
20325
+ /* @__PURE__ */ jsxs("text", {
20326
+ wrapMode: "none",
20327
+ children: [/* @__PURE__ */ jsx$1("span", {
20328
+ fg: color,
20329
+ children: `${formatUsagePercent(win.usedPercent)}%`.padStart(PERCENT_COL)
20330
+ }), /* @__PURE__ */ jsx$1("span", {
20331
+ fg: COLOR.mute,
20332
+ children: (trailing ?? "").padStart(TRAILING_COL)
20333
+ })]
20334
+ })
20335
+ ]
20336
+ });
20337
+ }
20338
+ /** What an empty body says, in precedence order. */
20339
+ function emptyStateLine({ loading, loaded, envOptedOut, autoLoad }) {
20340
+ if (envOptedOut) return "usage checks are disabled in this environment (ZIDANE_NO_USAGE_CHECK / CI)";
20341
+ if (loading) return "loading…";
20342
+ if (!autoLoad && !loaded) return "usage checks are off · ctrl+r fetches once";
20343
+ return "no signed-in provider reports usage";
20344
+ }
20345
+ /** `20.00 / 25.00`, or just the remainder when no total is reported. */
20346
+ function formatBalance(credits) {
20347
+ const remaining = formatCredits(credits.remaining);
20348
+ return credits.total === null ? remaining : `${remaining} / ${formatCredits(credits.total)}`;
20349
+ }
20350
+ /** Balances are dollars for OpenRouter and opaque units elsewhere. */
20351
+ function formatCredits(value) {
20352
+ return value >= 100 ? value.toFixed(0) : value.toFixed(2);
20353
+ }
20354
+ //#endregion
19852
20355
  //#region src/tui/app.tsx
19853
20356
  const BUILTIN_COMPLETION_PROVIDER_IDS = /* @__PURE__ */ new Set([
19854
20357
  "commands",
@@ -21213,6 +21716,51 @@ function AppShell() {
21213
21716
  getSmooth: useCallback(() => smoothStreamingRef.current, []),
21214
21717
  maxTranscriptEvents: transcriptRevealHoldRef.current ? null : void 0
21215
21718
  });
21719
+ /**
21720
+ * Append a harness notice: the aligned one-line rows for things the harness
21721
+ * did rather than things the model said (run error, model fallback,
21722
+ * repeat-guard trip, history repair).
21723
+ *
21724
+ * Persists by default as a display-only `role: 'command'` turn, sharing the
21725
+ * pre-minted `turnId` with the live event so the reloaded row IS the live
21726
+ * row. Pass `persist: false` for notices a reload makes meaningless or
21727
+ * duplicative: progress banners, hot-reload chatter, anything already
21728
+ * superseded by a persisted boundary card.
21729
+ *
21730
+ * Callers whose work outlives a session switch (run hooks, awaited command
21731
+ * handlers) pass their own `session`, captured when the work started. That
21732
+ * splits the two halves rather than dropping both:
21733
+ *
21734
+ * - the live row is skipped when that session is no longer the open one,
21735
+ * so a background run can't paint into the conversation the user just
21736
+ * opened;
21737
+ * - the turn is still persisted to the run's OWN session, because a run
21738
+ * that errors while you are looking elsewhere is exactly when a durable
21739
+ * record is worth the most. Reopening it replays the row in place.
21740
+ */
21741
+ const emitNotice = useCallback((notice, text, opts = {}) => {
21742
+ const open = sessionRef.current;
21743
+ const turnId = crypto.randomUUID();
21744
+ if (!opts.session || open?.id === opts.session.id) stream.appendImmediate({
21745
+ kind: notice.severity === "error" ? "error" : "info",
21746
+ text,
21747
+ notice,
21748
+ turnId,
21749
+ ...opts.childId ? { childId: opts.childId } : {},
21750
+ ...opts.depth !== void 0 ? { depth: opts.depth } : {}
21751
+ });
21752
+ const target = opts.session ?? open;
21753
+ if (opts.persist === false || !target) return;
21754
+ target.appendTurns([{
21755
+ id: turnId,
21756
+ role: "command",
21757
+ createdAt: Date.now(),
21758
+ content: noticeTurnContent(notice, text, {
21759
+ ...opts.childId ? { childId: opts.childId } : {},
21760
+ ...opts.depth !== void 0 ? { depth: opts.depth } : {}
21761
+ })
21762
+ }]).catch((err) => debugLog("emitNotice: appendTurns failed", err));
21763
+ }, [stream]);
21216
21764
  streamRef.current = stream;
21217
21765
  const makePicked = useCallback((provider, modelId) => {
21218
21766
  const descriptor = providerRegistry[provider.key];
@@ -21782,10 +22330,23 @@ function AppShell() {
21782
22330
  provider: to.provider,
21783
22331
  model: to.model
21784
22332
  });
21785
- stream.appendImmediate({
21786
- kind: "info",
21787
- text: `⚠ model fallback: ${from.provider}/${from.model} failed (${errorMessage(err)}) — continuing on ${to.provider}/${to.model}`
21788
- });
22333
+ emitNotice({
22334
+ severity: "warn",
22335
+ label: "model fallback"
22336
+ }, `${from.provider}/${from.model} failed (${errorMessage(err)}), continuing on ${to.provider}/${to.model}`, { session });
22337
+ });
22338
+ const guardNoticed = /* @__PURE__ */ new Set();
22339
+ agent.hooks.hook("repeat-guard:exceeded", ({ tool, count, action }) => {
22340
+ const key = `${tool}\u0000${action}`;
22341
+ if (guardNoticed.has(key)) return;
22342
+ guardNoticed.add(key);
22343
+ emitNotice({
22344
+ severity: action === "abort" ? "error" : "warn",
22345
+ label: "repeat guard"
22346
+ }, action === "abort" ? `${tool} repeated ${count} times with the same input, run stopped` : `${tool} repeated ${count} times with the same input, asked the model to change approach`, { session });
22347
+ });
22348
+ agent.hooks.hook("agent:done", () => {
22349
+ guardNoticed.clear();
21789
22350
  });
21790
22351
  agent.hooks.hook("tool:before", async ({ callId, name, input, turnId }) => {
21791
22352
  registerInFlightTool({
@@ -21930,18 +22491,18 @@ function AppShell() {
21930
22491
  const reason = lastFinishReasonRef.current;
21931
22492
  lastFinishReasonRef.current = void 0;
21932
22493
  if (sessionRef.current?.id !== session.id) return;
21933
- if (reason === "length") stream.appendImmediate({
21934
- kind: "info",
21935
- text: "⚠ Stopped at the model output limit (max_tokens). Turn on \"Auto-resume on output limit\" or raise \"Max output tokens\" in Settings to continue past it."
21936
- });
21937
- else if (reason === "pause") stream.appendImmediate({
21938
- kind: "info",
21939
- text: "⚠ Stopped after repeated empty pause turns from the provider (maxConsecutivePauseTurns cap is armed)."
21940
- });
21941
- else if (reason === "content-filter") stream.appendImmediate({
21942
- kind: "info",
21943
- text: "⚠ The provider refused to continue (safety filter / refusal stop) despite automatic retries. Rephrase the request, or start a fresh session if it keeps happening."
21944
- });
22494
+ if (reason === "length") emitNotice({
22495
+ severity: "warn",
22496
+ label: "output limit"
22497
+ }, "stopped at the model output limit (max_tokens). Turn on \"Auto-resume on output limit\" or raise \"Max output tokens\" in Settings to continue past it.");
22498
+ else if (reason === "pause") emitNotice({
22499
+ severity: "warn",
22500
+ label: "pause cap"
22501
+ }, "stopped after repeated empty pause turns from the provider (maxConsecutivePauseTurns cap is armed).");
22502
+ else if (reason === "content-filter") emitNotice({
22503
+ severity: "warn",
22504
+ label: "refusal"
22505
+ }, "the provider refused to continue (safety filter / refusal stop) despite automatic retries. Rephrase the request, or start a fresh session if it keeps happening.");
21945
22506
  });
21946
22507
  agent.hooks.hook("compact:start", ({ usedFraction }) => {
21947
22508
  setCompacting(true);
@@ -21963,10 +22524,10 @@ function AppShell() {
21963
22524
  if (!(sessionRef.current?.id === session.id)) return;
21964
22525
  if (ctx.status === "error") {
21965
22526
  const msg = errorMessage(ctx.error);
21966
- if (!/abort/i.test(msg)) stream.appendImmediate({
21967
- kind: "error",
21968
- text: `Auto-compaction failed: ${msg}`
21969
- });
22527
+ if (!/abort/i.test(msg)) emitNotice({
22528
+ severity: "error",
22529
+ label: "auto-compaction"
22530
+ }, msg);
21970
22531
  return;
21971
22532
  }
21972
22533
  const markerTurn = [...session.turns].reverse().find((t) => t.content.some((b) => b.type === "compact-summary"));
@@ -21978,10 +22539,10 @@ function AppShell() {
21978
22539
  agent.hooks.hook("history:repair", (ctx) => {
21979
22540
  if (sessionRef.current?.id !== session.id) return;
21980
22541
  const blocks = `${ctx.removedBlocks} stale reasoning block${ctx.removedBlocks === 1 ? "" : "s"}`;
21981
- stream.appendImmediate({
21982
- kind: "info",
21983
- text: `🩹 Repaired history: removed ${blocks} the provider rejected${ctx.persisted ? "" : " (session not yet persisted)"}.`
21984
- });
22542
+ emitNotice({
22543
+ severity: "warn",
22544
+ label: "history repair"
22545
+ }, `removed ${blocks} the provider rejected${ctx.persisted ? "" : " (session not yet persisted)"}`);
21985
22546
  });
21986
22547
  const spawnStartedAt = /* @__PURE__ */ new Map();
21987
22548
  agent.hooks.hook("spawn:before", ({ id, task, depth, model, resumesChildId }) => {
@@ -22023,11 +22584,13 @@ function AppShell() {
22023
22584
  });
22024
22585
  });
22025
22586
  agent.hooks.hook("spawn:error", ({ id, depth, error }) => {
22026
- stream.appendImmediate({
22027
- kind: "error",
22028
- text: `[${id}] ${error.message}`,
22587
+ emitNotice({
22588
+ severity: "error",
22589
+ label: "subagent"
22590
+ }, `[${id}] ${error.message}`, {
22029
22591
  childId: id,
22030
- depth: depth ?? 1
22592
+ depth: depth ?? 1,
22593
+ session
22031
22594
  });
22032
22595
  });
22033
22596
  agent.hooks.hook("child:stream:thinking", ({ delta, childId, depth, turnId }) => {
@@ -22438,10 +23001,10 @@ function AppShell() {
22438
23001
  updatedAt: Date.now()
22439
23002
  } : prev);
22440
23003
  } catch (err) {
22441
- stream.appendImmediate({
22442
- kind: "error",
22443
- text: errorMessage(err)
22444
- });
23004
+ emitNotice({
23005
+ severity: "error",
23006
+ label: "run"
23007
+ }, errorMessage(err), { session });
22445
23008
  } finally {
22446
23009
  stream.flushAndUpdate(finalizeStreamingMarkdown);
22447
23010
  await drainFollowUpsRef.current().catch((err) => debugLog("resume-interaction: follow-up drain failed", err));
@@ -22562,6 +23125,7 @@ function AppShell() {
22562
23125
  ]);
22563
23126
  const [availableProviders, setAvailableProviders] = useState([]);
22564
23127
  const refreshAvailableProviders = useCallback(() => {
23128
+ clearProviderUsageCache();
22565
23129
  const detected = detectAuth(config.paths.userDir, providerRegistry);
22566
23130
  setAvailableProviders(detected.filter((p) => p.available));
22567
23131
  }, [config.paths.userDir, providerRegistry]);
@@ -23363,10 +23927,10 @@ function AppShell() {
23363
23927
  } : prev);
23364
23928
  maybeAutoGenerateTitleRef.current(session);
23365
23929
  } catch (err) {
23366
- stream.appendImmediate({
23367
- kind: "error",
23368
- text: errorMessage(err)
23369
- });
23930
+ emitNotice({
23931
+ severity: "error",
23932
+ label: "run"
23933
+ }, errorMessage(err), { session });
23370
23934
  } finally {
23371
23935
  chatReferenceTokensRef.current = [];
23372
23936
  stream.flushAndUpdate(finalizeStreamingMarkdown);
@@ -23726,15 +24290,15 @@ function AppShell() {
23726
24290
  });
23727
24291
  emitBlock({ text: `\`❯ /${invocation.name}${invocation.args ? ` ${invocation.args}` : ""}\`` });
23728
24292
  const result = await executeCommand(command, ctx);
23729
- if (result.error) stream.appendImmediate({
23730
- kind: "error",
23731
- text: `/${invocation.name}: ${errorMessage(result.error)}`
23732
- });
24293
+ if (result.error) emitNotice({
24294
+ severity: "error",
24295
+ label: "command"
24296
+ }, `/${invocation.name}: ${errorMessage(result.error)}`, { session });
23733
24297
  } catch (err) {
23734
- stream.appendImmediate({
23735
- kind: "error",
23736
- text: `/${invocation.name}: ${errorMessage(err)}`
23737
- });
24298
+ emitNotice({
24299
+ severity: "error",
24300
+ label: "command"
24301
+ }, `/${invocation.name}: ${errorMessage(err)}`, { session });
23738
24302
  } finally {
23739
24303
  commandAbortRef.current = null;
23740
24304
  await flushCommandTurn();
@@ -23847,11 +24411,10 @@ function AppShell() {
23847
24411
  }]
23848
24412
  }]).catch((err) => debugLog("bash: appendTurns failed", err));
23849
24413
  } catch (err) {
23850
- stream.appendImmediate({
23851
- kind: "error",
23852
- text: `!${command}: ${errorMessage(err)}`,
23853
- callId
23854
- });
24414
+ emitNotice({
24415
+ severity: "error",
24416
+ label: "shell"
24417
+ }, `!${command}: ${errorMessage(err)}`, { session });
23855
24418
  } finally {
23856
24419
  commandAbortRef.current = null;
23857
24420
  setCurrentSession((prev) => prev ? {
@@ -25248,6 +25811,15 @@ function AppShell() {
25248
25811
  }).catch((err) => debugLog("getContextBreakdown failed", err));
25249
25812
  return;
25250
25813
  }
25814
+ if (matchesBinding(key, keybindings.openUsagePanel) && screen === "chat") {
25815
+ modal.open(/* @__PURE__ */ jsx$1(UsagePanelModal, {
25816
+ dataDir: config.paths.userDir,
25817
+ registry: providerRegistry,
25818
+ activeProviderKey: picked?.provider.key ?? null,
25819
+ autoLoad: settings.subscriptionUsage
25820
+ }));
25821
+ return;
25822
+ }
25251
25823
  if (matchesBinding(key, keybindings.openKeybindings) && screen !== "auth") {
25252
25824
  modal.open(/* @__PURE__ */ jsx$1(KeybindingsModal, {
25253
25825
  bindings: keybindings,
@@ -25525,6 +26097,22 @@ function AppShell() {
25525
26097
  providerRegistry
25526
26098
  ]);
25527
26099
  const footerCost = screen === "chat" ? sessionCost : null;
26100
+ const activeUsageDescriptor = useMemo(() => picked ? providerRegistry[picked.provider.key] ?? null : null, [picked, providerRegistry]);
26101
+ const providerUsage = useActiveProviderUsage({
26102
+ dataDir: config.paths.userDir,
26103
+ descriptor: activeUsageDescriptor,
26104
+ enabled: screen === "chat" && settings.subscriptionUsage && settings.showUsageIndicator,
26105
+ busy
26106
+ });
26107
+ const footerUsage = useMemo(() => {
26108
+ if (!settings.showUsageIndicator) return null;
26109
+ if (!providerUsage || providerUsage.status !== "ok") return null;
26110
+ const window = mostConstrainedWindow(providerUsage);
26111
+ return window ? {
26112
+ label: window.label,
26113
+ usedPercent: window.usedPercent
26114
+ } : null;
26115
+ }, [providerUsage, settings.showUsageIndicator]);
25528
26116
  useEffect(() => () => {
25529
26117
  teardown();
25530
26118
  }, [teardown]);
@@ -25614,7 +26202,9 @@ function AppShell() {
25614
26202
  compactionTokens,
25615
26203
  compactionStartedAt,
25616
26204
  retry: retryInfo,
25617
- contextKey: keybindings.openContextPanel
26205
+ contextKey: keybindings.openContextPanel,
26206
+ usage: footerUsage,
26207
+ usageKey: keybindings.openUsagePanel
25618
26208
  }),
25619
26209
  showQuitConfirm && /* @__PURE__ */ jsxs("box", {
25620
26210
  position: "absolute",
@@ -25692,6 +26282,67 @@ function effortForModel(descriptor, modelId, remembered) {
25692
26282
  return remembered?.[modelId] ?? "medium";
25693
26283
  }
25694
26284
  //#endregion
26285
+ //#region src/tui/stderr-guard.ts
26286
+ /** Lines kept for the abnormal-exit replay. Bounded so a chatty loop can't grow unbounded. */
26287
+ const REPLAY_CAPACITY = 200;
26288
+ /**
26289
+ * Redirect `process.stderr` into `sink` until the returned function is called.
26290
+ * Idempotent per install; call the returned uninstall before the renderer is
26291
+ * torn down so post-teardown output (render stats, fatal rethrows) reaches the
26292
+ * terminal normally.
26293
+ */
26294
+ function installStderrGuard({ sink, level = "warn" }) {
26295
+ const original = process.stderr.write.bind(process.stderr);
26296
+ const replay = [];
26297
+ let pending = "";
26298
+ let emitting = false;
26299
+ const emit = (line) => {
26300
+ if (line.length === 0 || emitting) return;
26301
+ replay.push(line);
26302
+ if (replay.length > REPLAY_CAPACITY) replay.splice(0, replay.length - REPLAY_CAPACITY);
26303
+ emitting = true;
26304
+ try {
26305
+ sink.emit({
26306
+ level,
26307
+ timestamp: Date.now(),
26308
+ message: line,
26309
+ attrs: { source: "stderr" }
26310
+ });
26311
+ } catch {} finally {
26312
+ emitting = false;
26313
+ }
26314
+ };
26315
+ const flushPending = () => {
26316
+ if (pending.length > 0) {
26317
+ const line = pending;
26318
+ pending = "";
26319
+ emit(line);
26320
+ }
26321
+ };
26322
+ const capture = ((chunk, ...rest) => {
26323
+ const done = rest.find((arg) => typeof arg === "function");
26324
+ const text = typeof chunk === "string" ? chunk : chunk instanceof Uint8Array ? new TextDecoder().decode(chunk) : String(chunk);
26325
+ pending += text;
26326
+ const lines = pending.split("\n");
26327
+ pending = lines.pop() ?? "";
26328
+ for (const line of lines) emit(line.replace(/\r$/, ""));
26329
+ done?.(null);
26330
+ return true;
26331
+ });
26332
+ const onExit = (code) => {
26333
+ process.stderr.write = original;
26334
+ if (code === 0 || replay.length === 0) return;
26335
+ original(`${replay.join("\n")}\n`);
26336
+ };
26337
+ process.stderr.write = capture;
26338
+ process.on("exit", onExit);
26339
+ return () => {
26340
+ process.off("exit", onExit);
26341
+ flushPending();
26342
+ process.stderr.write = original;
26343
+ };
26344
+ }
26345
+ //#endregion
25695
26346
  //#region src/tui/tree-sitter.ts
25696
26347
  /**
25697
26348
  * Register Tree-sitter parsers for the languages we'd like highlighted
@@ -27096,65 +27747,85 @@ async function runTui(options = {}) {
27096
27747
  const inlineExtensions = (options.extensions ?? []).map(inlineLoadedExtension);
27097
27748
  const trustProjectExtensions = options.trustProjectExtensions ?? (preState.settings?.loadProjectExtensions === true || process.env.ZIDANE_TRUST_PROJECT_EXTENSIONS === "1");
27098
27749
  const logging = initTuiLogging({ stateDir: paths.stateDir });
27099
- const extensionSink = logging.sink;
27100
- registerTuiHostModules();
27101
- const hud = initTuiHud();
27102
- const promptBridge = initTuiPromptBridge();
27103
- const hostRuntime = initTuiHostRuntime();
27104
- const extensionRegistry = await discoverAndBuildExtensionRegistry({
27105
- ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
27106
- prefix,
27107
- ...extensionScanPaths.length > 0 ? { extensionScanPaths } : {},
27108
- ...enabledAllowlist !== void 0 ? { enabledAllowlist } : {},
27109
- trustProjectExtensions,
27110
- inlineExtensions,
27111
- sink: extensionSink,
27112
- hud,
27113
- promptBridge,
27114
- hostRuntime,
27115
- readSettings: readLiveSettings
27116
- });
27117
- bootTick("runTui:after-buildExtensionRegistry");
27118
- const config = resolveConfig({
27119
- ...options,
27120
- extensionScanPaths,
27121
- pluginScanPaths,
27122
- extensionRegistry,
27123
- trustProjectExtensions,
27124
- ...options.launch && !options.preset && !options.agents ? { preset: basic_default } : {},
27125
- store: options.store ?? ((resolved) => createTuiStore(options.launch?.sessionDb ?? resolved.db))
27126
- });
27127
- bootTick("runTui:after-resolveConfig");
27128
- let done = () => {};
27129
- const exited = new Promise((resolve) => {
27130
- done = resolve;
27131
- });
27132
- const bootFps = clampFps(config.initialSettings.targetFps ?? DEFAULT_SETTINGS.targetFps);
27133
- const gatherStats = !!process.env.ZIDANE_DEBUG;
27134
- const renderer = await createCliRenderer({
27135
- exitOnCtrlC: false,
27136
- onDestroy: () => done(),
27137
- debounceDelay: 0,
27138
- targetFps: bootFps,
27139
- maxFps: bootFps,
27140
- useMouse: config.initialSettings.mouseInteractions ?? DEFAULT_SETTINGS.mouseInteractions,
27141
- gatherStats
27142
- });
27143
- bootTick("runTui:after-createCliRenderer");
27750
+ setDebugLogSink((scope, line) => logging.sink.emit({
27751
+ level: "debug",
27752
+ timestamp: Date.now(),
27753
+ message: line,
27754
+ attrs: { scope }
27755
+ }));
27756
+ const releaseStderr = installStderrGuard({ sink: logging.sink });
27757
+ let logRedirectsReleased = false;
27758
+ const releaseLogRedirects = () => {
27759
+ if (logRedirectsReleased) return;
27760
+ logRedirectsReleased = true;
27761
+ releaseStderr();
27762
+ setDebugLogSink(null);
27763
+ };
27144
27764
  try {
27145
- createRoot(renderer).render(/* @__PURE__ */ jsx(App, { config }));
27146
- } catch (err) {
27147
- renderer.destroy();
27148
- throw err;
27149
- }
27150
- bootTick("runTui:after-mount");
27151
- await exited;
27152
- logging.close();
27153
- if (gatherStats) try {
27154
- const s = renderer.getStats();
27155
- process.stderr.write(`[zidane/tui] render stats: fps=${s.fps.toFixed(1)} avgFrame=${s.averageFrameTime.toFixed(2)}ms min=${s.minFrameTime.toFixed(2)}ms max=${s.maxFrameTime.toFixed(2)}ms frames=${s.frameCount}\n`);
27156
- } catch (err) {
27157
- process.stderr.write(`[zidane/tui] render stats unavailable: ${errorMessage(err)}\n`);
27765
+ const extensionSink = logging.sink;
27766
+ registerTuiHostModules();
27767
+ const hud = initTuiHud();
27768
+ const promptBridge = initTuiPromptBridge();
27769
+ const hostRuntime = initTuiHostRuntime();
27770
+ const extensionRegistry = await discoverAndBuildExtensionRegistry({
27771
+ ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
27772
+ prefix,
27773
+ ...extensionScanPaths.length > 0 ? { extensionScanPaths } : {},
27774
+ ...enabledAllowlist !== void 0 ? { enabledAllowlist } : {},
27775
+ trustProjectExtensions,
27776
+ inlineExtensions,
27777
+ sink: extensionSink,
27778
+ hud,
27779
+ promptBridge,
27780
+ hostRuntime,
27781
+ readSettings: readLiveSettings
27782
+ });
27783
+ bootTick("runTui:after-buildExtensionRegistry");
27784
+ const config = resolveConfig({
27785
+ ...options,
27786
+ extensionScanPaths,
27787
+ pluginScanPaths,
27788
+ extensionRegistry,
27789
+ trustProjectExtensions,
27790
+ ...options.launch && !options.preset && !options.agents ? { preset: basic_default } : {},
27791
+ store: options.store ?? ((resolved) => createTuiStore(options.launch?.sessionDb ?? resolved.db))
27792
+ });
27793
+ bootTick("runTui:after-resolveConfig");
27794
+ let done = () => {};
27795
+ const exited = new Promise((resolve) => {
27796
+ done = resolve;
27797
+ });
27798
+ const bootFps = clampFps(config.initialSettings.targetFps ?? DEFAULT_SETTINGS.targetFps);
27799
+ const gatherStats = !!process.env.ZIDANE_DEBUG;
27800
+ const renderer = await createCliRenderer({
27801
+ exitOnCtrlC: false,
27802
+ onDestroy: () => done(),
27803
+ debounceDelay: 0,
27804
+ targetFps: bootFps,
27805
+ maxFps: bootFps,
27806
+ useMouse: config.initialSettings.mouseInteractions ?? DEFAULT_SETTINGS.mouseInteractions,
27807
+ gatherStats
27808
+ });
27809
+ bootTick("runTui:after-createCliRenderer");
27810
+ try {
27811
+ createRoot(renderer).render(/* @__PURE__ */ jsx(App, { config }));
27812
+ } catch (err) {
27813
+ renderer.destroy();
27814
+ throw err;
27815
+ }
27816
+ bootTick("runTui:after-mount");
27817
+ await exited;
27818
+ releaseLogRedirects();
27819
+ logging.close();
27820
+ if (gatherStats) try {
27821
+ const s = renderer.getStats();
27822
+ process.stderr.write(`[zidane/tui] render stats: fps=${s.fps.toFixed(1)} avgFrame=${s.averageFrameTime.toFixed(2)}ms min=${s.minFrameTime.toFixed(2)}ms max=${s.maxFrameTime.toFixed(2)}ms frames=${s.frameCount}\n`);
27823
+ } catch (err) {
27824
+ process.stderr.write(`[zidane/tui] render stats unavailable: ${errorMessage(err)}\n`);
27825
+ }
27826
+ } finally {
27827
+ releaseLogRedirects();
27828
+ logging.close();
27158
27829
  }
27159
27830
  process.exit(0);
27160
27831
  }