zidane 6.6.1 → 6.6.3

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 (75) 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-BGiCne_W.d.ts → index-D0O-QO7Z.d.ts} +60 -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-5zq0YCK0.d.ts → tool-formatters-NrzD6Fkk.d.ts} +20 -3
  50. package/dist/{tool-formatters-5zq0YCK0.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-BfV8T2us.js → transcript-anchors-BjH-68k2.js} +654 -35
  56. package/dist/transcript-anchors-BjH-68k2.js.map +1 -0
  57. package/dist/{transcript-anchors-BaAzmW1d.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 +859 -191
  62. package/dist/tui.js.map +1 -1
  63. package/dist/{turn-operations-DXiTV38z.d.ts → turn-operations-Cuc9pwiB.d.ts} +3 -3
  64. package/dist/{turn-operations-DXiTV38z.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/TUI.md +22 -2
  71. package/package.json +1 -1
  72. package/dist/agent-ut6nQRnI.js.map +0 -1
  73. package/dist/index-BGiCne_W.d.ts.map +0 -1
  74. package/dist/transcript-anchors-BaAzmW1d.d.ts.map +0 -1
  75. package/dist/transcript-anchors-BfV8T2us.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-BfV8T2us.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
  })
@@ -15677,7 +15861,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
15677
15861
  category: "agent",
15678
15862
  id: "compaction-model",
15679
15863
  label: "Compaction model",
15680
- 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`,
15681
15865
  onPick: actions.onPickCompactionModel
15682
15866
  });
15683
15867
  if (actions?.onImportClaudeCode) acts.push({
@@ -16067,14 +16251,14 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
16067
16251
  const { segment } = row;
16068
16252
  if (segment.id === "system" && !segment.path && actions?.onCreateSystemPrompt) {
16069
16253
  if (!instructionsCanOverride) {
16070
- 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.");
16071
16255
  return;
16072
16256
  }
16073
16257
  Promise.resolve(actions.onCreateSystemPrompt()).catch((err) => setInstructionsNotice(`Could not create SYSTEM.md: ${errorMessage(err)}`));
16074
16258
  return;
16075
16259
  }
16076
16260
  if (segment.path) Promise.resolve(actions?.onOpenInstructionsFile?.(segment.path)).catch((err) => setInstructionsNotice(`Could not open ${segment.label}: ${errorMessage(err)}`));
16077
- 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.`);
16078
16262
  return;
16079
16263
  }
16080
16264
  if (activeTab === "keybindings") {
@@ -16279,7 +16463,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
16279
16463
  key.preventDefault();
16280
16464
  const ext = focusedExtension;
16281
16465
  if (ext?.dir) actions?.onEditExtension?.(ext.dir);
16282
- 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.`);
16283
16467
  return;
16284
16468
  }
16285
16469
  if (key.ctrl && key.name === "g" && actions?.onInstallExtension) {
@@ -16401,7 +16585,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
16401
16585
  if (skill.baseDir) actions.onEditSkill(skill);
16402
16586
  else setSkillOp({
16403
16587
  kind: "notice",
16404
- text: `"${skill.name}" is ${skill.source ?? "inline"} — no directory to edit.`
16588
+ text: `"${skill.name}" is ${skill.source ?? "inline"}, no directory to edit.`
16405
16589
  });
16406
16590
  return;
16407
16591
  }
@@ -16473,7 +16657,7 @@ function SettingsModal({ skillsCatalog: skillsCatalogProp, mcpsCatalog: mcpsCata
16473
16657
  setInstructionsNotice("reloading…");
16474
16658
  actions.onRefreshInstructions().then((outcome) => {
16475
16659
  setInstructionsRefreshKey((n) => n + 1);
16476
- 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");
16477
16661
  }).catch((err) => setInstructionsNotice(`Refresh failed: ${errorMessage(err)}`));
16478
16662
  return;
16479
16663
  }
@@ -17891,7 +18075,7 @@ function renderEmptyToolsListRow({ serverName, anchorId, focused, bg, COLOR, ref
17891
18075
  children: focused ? TOOL_ROW_FOCUS_INDENT$1 : TOOL_ROW_INDENT$1
17892
18076
  }), /* @__PURE__ */ jsx$1("span", {
17893
18077
  fg: COLOR.mute,
17894
- 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."
17895
18079
  })]
17896
18080
  }, `tool-empty:${serverName}`);
17897
18081
  }
@@ -17911,11 +18095,11 @@ function InstructionsList({ rows, totalCount, loading, error, live, shadowedSyst
17911
18095
  children: [
17912
18096
  !live && /* @__PURE__ */ jsx$1("text", {
17913
18097
  fg: COLOR.mute,
17914
- 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."
17915
18099
  }),
17916
18100
  shadowedSystemPath !== void 0 && /* @__PURE__ */ jsx$1("text", {
17917
18101
  fg: COLOR.mute,
17918
- 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.`
17919
18103
  }),
17920
18104
  rows.map(({ segment, expanded }, i) => {
17921
18105
  const focused = i === cursor;
@@ -18083,7 +18267,7 @@ function AuthenticationList({ view, providers, rows, cursor, highlightBg, query
18083
18267
  }),
18084
18268
  /* @__PURE__ */ jsx$1("span", {
18085
18269
  fg: view.currentProviderLabel ? COLOR.accent : COLOR.mute,
18086
- children: view.currentProviderLabel ?? "— not selected —"
18270
+ children: view.currentProviderLabel ?? "not selected"
18087
18271
  }),
18088
18272
  view.currentProviderKey && view.currentProviderLabel && view.currentProviderKey !== view.currentProviderLabel.toLowerCase() && /* @__PURE__ */ jsx$1("span", {
18089
18273
  fg: COLOR.dim,
@@ -18098,7 +18282,7 @@ function AuthenticationList({ view, providers, rows, cursor, highlightBg, query
18098
18282
  children: " model "
18099
18283
  }), /* @__PURE__ */ jsx$1("span", {
18100
18284
  fg: view.currentModel ? COLOR.model : COLOR.mute,
18101
- children: view.currentModel ?? "— not selected —"
18285
+ children: view.currentModel ?? "not selected"
18102
18286
  })]
18103
18287
  })
18104
18288
  ]
@@ -18755,12 +18939,12 @@ function canLogout$1(status) {
18755
18939
  return status.kind === "authed" || status.kind === "error" || status.kind === "authorizing";
18756
18940
  }
18757
18941
  function searchPlaceholder(tab) {
18758
- if (tab === "skills") return "search skills — name, description…";
18759
- if (tab === "mcps") return "search MCP servers — name, transport, command/URL…";
18760
- if (tab === "extensions") return "search extensions — name, version, description, settings…";
18761
- if (tab === "keybindings") return "search keybindings — action, key, description…";
18762
- if (tab === "authentication") return "search providers — name, method…";
18763
- 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…`;
18764
18948
  }
18765
18949
  function displayPath$1(path, home) {
18766
18950
  if (home && path.startsWith(`${home}/`)) return `~/${path.slice(home.length + 1)}`;
@@ -19852,6 +20036,322 @@ function blockSummary(turns) {
19852
20036
  return parts.length === 0 ? "(empty)" : parts.join(" · ");
19853
20037
  }
19854
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
19855
20355
  //#region src/tui/app.tsx
19856
20356
  const BUILTIN_COMPLETION_PROVIDER_IDS = /* @__PURE__ */ new Set([
19857
20357
  "commands",
@@ -21216,6 +21716,51 @@ function AppShell() {
21216
21716
  getSmooth: useCallback(() => smoothStreamingRef.current, []),
21217
21717
  maxTranscriptEvents: transcriptRevealHoldRef.current ? null : void 0
21218
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]);
21219
21764
  streamRef.current = stream;
21220
21765
  const makePicked = useCallback((provider, modelId) => {
21221
21766
  const descriptor = providerRegistry[provider.key];
@@ -21785,10 +22330,23 @@ function AppShell() {
21785
22330
  provider: to.provider,
21786
22331
  model: to.model
21787
22332
  });
21788
- stream.appendImmediate({
21789
- kind: "info",
21790
- text: `⚠ model fallback: ${from.provider}/${from.model} failed (${errorMessage(err)}) — continuing on ${to.provider}/${to.model}`
21791
- });
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();
21792
22350
  });
21793
22351
  agent.hooks.hook("tool:before", async ({ callId, name, input, turnId }) => {
21794
22352
  registerInFlightTool({
@@ -21933,18 +22491,18 @@ function AppShell() {
21933
22491
  const reason = lastFinishReasonRef.current;
21934
22492
  lastFinishReasonRef.current = void 0;
21935
22493
  if (sessionRef.current?.id !== session.id) return;
21936
- if (reason === "length") stream.appendImmediate({
21937
- kind: "info",
21938
- 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."
21939
- });
21940
- else if (reason === "pause") stream.appendImmediate({
21941
- kind: "info",
21942
- text: "⚠ Stopped after repeated empty pause turns from the provider (maxConsecutivePauseTurns cap is armed)."
21943
- });
21944
- else if (reason === "content-filter") stream.appendImmediate({
21945
- kind: "info",
21946
- 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."
21947
- });
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.");
21948
22506
  });
21949
22507
  agent.hooks.hook("compact:start", ({ usedFraction }) => {
21950
22508
  setCompacting(true);
@@ -21966,10 +22524,10 @@ function AppShell() {
21966
22524
  if (!(sessionRef.current?.id === session.id)) return;
21967
22525
  if (ctx.status === "error") {
21968
22526
  const msg = errorMessage(ctx.error);
21969
- if (!/abort/i.test(msg)) stream.appendImmediate({
21970
- kind: "error",
21971
- text: `Auto-compaction failed: ${msg}`
21972
- });
22527
+ if (!/abort/i.test(msg)) emitNotice({
22528
+ severity: "error",
22529
+ label: "auto-compaction"
22530
+ }, msg);
21973
22531
  return;
21974
22532
  }
21975
22533
  const markerTurn = [...session.turns].reverse().find((t) => t.content.some((b) => b.type === "compact-summary"));
@@ -21981,10 +22539,10 @@ function AppShell() {
21981
22539
  agent.hooks.hook("history:repair", (ctx) => {
21982
22540
  if (sessionRef.current?.id !== session.id) return;
21983
22541
  const blocks = `${ctx.removedBlocks} stale reasoning block${ctx.removedBlocks === 1 ? "" : "s"}`;
21984
- stream.appendImmediate({
21985
- kind: "info",
21986
- text: `🩹 Repaired history: removed ${blocks} the provider rejected${ctx.persisted ? "" : " (session not yet persisted)"}.`
21987
- });
22542
+ emitNotice({
22543
+ severity: "warn",
22544
+ label: "history repair"
22545
+ }, `removed ${blocks} the provider rejected${ctx.persisted ? "" : " (session not yet persisted)"}`);
21988
22546
  });
21989
22547
  const spawnStartedAt = /* @__PURE__ */ new Map();
21990
22548
  agent.hooks.hook("spawn:before", ({ id, task, depth, model, resumesChildId }) => {
@@ -22026,11 +22584,13 @@ function AppShell() {
22026
22584
  });
22027
22585
  });
22028
22586
  agent.hooks.hook("spawn:error", ({ id, depth, error }) => {
22029
- stream.appendImmediate({
22030
- kind: "error",
22031
- text: `[${id}] ${error.message}`,
22587
+ emitNotice({
22588
+ severity: "error",
22589
+ label: "subagent"
22590
+ }, `[${id}] ${error.message}`, {
22032
22591
  childId: id,
22033
- depth: depth ?? 1
22592
+ depth: depth ?? 1,
22593
+ session
22034
22594
  });
22035
22595
  });
22036
22596
  agent.hooks.hook("child:stream:thinking", ({ delta, childId, depth, turnId }) => {
@@ -22441,10 +23001,10 @@ function AppShell() {
22441
23001
  updatedAt: Date.now()
22442
23002
  } : prev);
22443
23003
  } catch (err) {
22444
- stream.appendImmediate({
22445
- kind: "error",
22446
- text: errorMessage(err)
22447
- });
23004
+ emitNotice({
23005
+ severity: "error",
23006
+ label: "run"
23007
+ }, errorMessage(err), { session });
22448
23008
  } finally {
22449
23009
  stream.flushAndUpdate(finalizeStreamingMarkdown);
22450
23010
  await drainFollowUpsRef.current().catch((err) => debugLog("resume-interaction: follow-up drain failed", err));
@@ -22565,6 +23125,7 @@ function AppShell() {
22565
23125
  ]);
22566
23126
  const [availableProviders, setAvailableProviders] = useState([]);
22567
23127
  const refreshAvailableProviders = useCallback(() => {
23128
+ clearProviderUsageCache();
22568
23129
  const detected = detectAuth(config.paths.userDir, providerRegistry);
22569
23130
  setAvailableProviders(detected.filter((p) => p.available));
22570
23131
  }, [config.paths.userDir, providerRegistry]);
@@ -23366,10 +23927,10 @@ function AppShell() {
23366
23927
  } : prev);
23367
23928
  maybeAutoGenerateTitleRef.current(session);
23368
23929
  } catch (err) {
23369
- stream.appendImmediate({
23370
- kind: "error",
23371
- text: errorMessage(err)
23372
- });
23930
+ emitNotice({
23931
+ severity: "error",
23932
+ label: "run"
23933
+ }, errorMessage(err), { session });
23373
23934
  } finally {
23374
23935
  chatReferenceTokensRef.current = [];
23375
23936
  stream.flushAndUpdate(finalizeStreamingMarkdown);
@@ -23729,15 +24290,15 @@ function AppShell() {
23729
24290
  });
23730
24291
  emitBlock({ text: `\`❯ /${invocation.name}${invocation.args ? ` ${invocation.args}` : ""}\`` });
23731
24292
  const result = await executeCommand(command, ctx);
23732
- if (result.error) stream.appendImmediate({
23733
- kind: "error",
23734
- text: `/${invocation.name}: ${errorMessage(result.error)}`
23735
- });
24293
+ if (result.error) emitNotice({
24294
+ severity: "error",
24295
+ label: "command"
24296
+ }, `/${invocation.name}: ${errorMessage(result.error)}`, { session });
23736
24297
  } catch (err) {
23737
- stream.appendImmediate({
23738
- kind: "error",
23739
- text: `/${invocation.name}: ${errorMessage(err)}`
23740
- });
24298
+ emitNotice({
24299
+ severity: "error",
24300
+ label: "command"
24301
+ }, `/${invocation.name}: ${errorMessage(err)}`, { session });
23741
24302
  } finally {
23742
24303
  commandAbortRef.current = null;
23743
24304
  await flushCommandTurn();
@@ -23850,11 +24411,10 @@ function AppShell() {
23850
24411
  }]
23851
24412
  }]).catch((err) => debugLog("bash: appendTurns failed", err));
23852
24413
  } catch (err) {
23853
- stream.appendImmediate({
23854
- kind: "error",
23855
- text: `!${command}: ${errorMessage(err)}`,
23856
- callId
23857
- });
24414
+ emitNotice({
24415
+ severity: "error",
24416
+ label: "shell"
24417
+ }, `!${command}: ${errorMessage(err)}`, { session });
23858
24418
  } finally {
23859
24419
  commandAbortRef.current = null;
23860
24420
  setCurrentSession((prev) => prev ? {
@@ -25251,6 +25811,15 @@ function AppShell() {
25251
25811
  }).catch((err) => debugLog("getContextBreakdown failed", err));
25252
25812
  return;
25253
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
+ }
25254
25823
  if (matchesBinding(key, keybindings.openKeybindings) && screen !== "auth") {
25255
25824
  modal.open(/* @__PURE__ */ jsx$1(KeybindingsModal, {
25256
25825
  bindings: keybindings,
@@ -25528,6 +26097,22 @@ function AppShell() {
25528
26097
  providerRegistry
25529
26098
  ]);
25530
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]);
25531
26116
  useEffect(() => () => {
25532
26117
  teardown();
25533
26118
  }, [teardown]);
@@ -25617,7 +26202,9 @@ function AppShell() {
25617
26202
  compactionTokens,
25618
26203
  compactionStartedAt,
25619
26204
  retry: retryInfo,
25620
- contextKey: keybindings.openContextPanel
26205
+ contextKey: keybindings.openContextPanel,
26206
+ usage: footerUsage,
26207
+ usageKey: keybindings.openUsagePanel
25621
26208
  }),
25622
26209
  showQuitConfirm && /* @__PURE__ */ jsxs("box", {
25623
26210
  position: "absolute",
@@ -25695,6 +26282,67 @@ function effortForModel(descriptor, modelId, remembered) {
25695
26282
  return remembered?.[modelId] ?? "medium";
25696
26283
  }
25697
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
25698
26346
  //#region src/tui/tree-sitter.ts
25699
26347
  /**
25700
26348
  * Register Tree-sitter parsers for the languages we'd like highlighted
@@ -27099,65 +27747,85 @@ async function runTui(options = {}) {
27099
27747
  const inlineExtensions = (options.extensions ?? []).map(inlineLoadedExtension);
27100
27748
  const trustProjectExtensions = options.trustProjectExtensions ?? (preState.settings?.loadProjectExtensions === true || process.env.ZIDANE_TRUST_PROJECT_EXTENSIONS === "1");
27101
27749
  const logging = initTuiLogging({ stateDir: paths.stateDir });
27102
- const extensionSink = logging.sink;
27103
- registerTuiHostModules();
27104
- const hud = initTuiHud();
27105
- const promptBridge = initTuiPromptBridge();
27106
- const hostRuntime = initTuiHostRuntime();
27107
- const extensionRegistry = await discoverAndBuildExtensionRegistry({
27108
- ...options.cwd !== void 0 ? { cwd: options.cwd } : {},
27109
- prefix,
27110
- ...extensionScanPaths.length > 0 ? { extensionScanPaths } : {},
27111
- ...enabledAllowlist !== void 0 ? { enabledAllowlist } : {},
27112
- trustProjectExtensions,
27113
- inlineExtensions,
27114
- sink: extensionSink,
27115
- hud,
27116
- promptBridge,
27117
- hostRuntime,
27118
- readSettings: readLiveSettings
27119
- });
27120
- bootTick("runTui:after-buildExtensionRegistry");
27121
- const config = resolveConfig({
27122
- ...options,
27123
- extensionScanPaths,
27124
- pluginScanPaths,
27125
- extensionRegistry,
27126
- trustProjectExtensions,
27127
- ...options.launch && !options.preset && !options.agents ? { preset: basic_default } : {},
27128
- store: options.store ?? ((resolved) => createTuiStore(options.launch?.sessionDb ?? resolved.db))
27129
- });
27130
- bootTick("runTui:after-resolveConfig");
27131
- let done = () => {};
27132
- const exited = new Promise((resolve) => {
27133
- done = resolve;
27134
- });
27135
- const bootFps = clampFps(config.initialSettings.targetFps ?? DEFAULT_SETTINGS.targetFps);
27136
- const gatherStats = !!process.env.ZIDANE_DEBUG;
27137
- const renderer = await createCliRenderer({
27138
- exitOnCtrlC: false,
27139
- onDestroy: () => done(),
27140
- debounceDelay: 0,
27141
- targetFps: bootFps,
27142
- maxFps: bootFps,
27143
- useMouse: config.initialSettings.mouseInteractions ?? DEFAULT_SETTINGS.mouseInteractions,
27144
- gatherStats
27145
- });
27146
- 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
+ };
27147
27764
  try {
27148
- createRoot(renderer).render(/* @__PURE__ */ jsx(App, { config }));
27149
- } catch (err) {
27150
- renderer.destroy();
27151
- throw err;
27152
- }
27153
- bootTick("runTui:after-mount");
27154
- await exited;
27155
- logging.close();
27156
- if (gatherStats) try {
27157
- const s = renderer.getStats();
27158
- 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`);
27159
- } catch (err) {
27160
- 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();
27161
27829
  }
27162
27830
  process.exit(0);
27163
27831
  }