zidane 6.6.1 → 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 (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/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { d as ClassifiedErrorKind, i as AgentContextExceededError, l as CONTEXT_EXCEEDED_MESSAGE_PATTERNS, n as AgentAbortedError, o as AgentProviderError, p as matchesContextExceeded, r as AgentBudgetExceededError, s as AgentToolNotAllowedError, u as ClassifiedError } from "./timeout-DEaqslLO.js";
2
- import { Ah as ToolResultAudioContent, Bh as TurnUsage, Bm as ChildRunStats, C as SuccessfulMutationOutcome, Cg as SessionStore, Eh as ThinkingLevel, Fh as ToolResultImageRefContent, Fm as AgentRunOptions, Gh as ToolDef, Gi as AgentHookMap, Gm as DEFAULT_AGENT_CLOCK, Gp as CerebrasParams, Hh as toolOutputByteLength, Ih as ToolResultMetadata, Im as AgentStats, Jh as ReadStateEntry, Kf as StreamCallbacks, Kh as ToolMap, Ki as AgentHooks, Lh as ToolResultTextContent, Mh as ToolResultDocumentContent, Ng as RemoteStoreOptions, Nh as ToolResultDocumentRefContent, Nm as AgentBehavior, Np as OpenAIParams, Ns as SpawnToolOptions, Oh as ToolHookContext, Os as ChildAgent, Ph as ToolResultImageContent, Pm as AgentClock, Ps as SpawnToolState, Qf as TurnResult, Rh as ToolResultVideoContent, Rr as InteractionToolOptions, S as MutationValidationResult, Sg as SessionRun, Uf as Provider, Uh as toolResultToText, Vh as toolOutputBudgetByteLength, Wf as ProviderCapabilities, Wh as ToolContext, Wi as Agent, Xf as ToolResult, Xm as McpServerConfig, Yf as ToolCall, Yh as ReadStateMap, Zf as ToolSpec, Zm as McpToolHookContext, _ as statsByModel, _g as CreateSessionOptions, _h as SessionEndStatus, ag as ToolStateOp, ah as PromptDocumentPart, b as MutationValidationHooksOptions, bg as SessionData, bh as SessionTurn, ch as PromptTextPart, dh as RepeatGuardToolMatcher, em as AnthropicParams, f_ as McpConnection, fg as SkillConfig, fh as ResolveContentRef, gg as SkillsConfig, gh as SessionContentBlock, h as flattenTurns, hh as RunHookMap, ig as ToolStateController, ih as PromptAudioPart, jh as ToolResultContent, kh as ToolOutcome, lh as PromptVideoPart, lp as OpenRouterParams, mg as SkillResource, mh as RetryConfig, nh as ProgressNudgeKind, og as ToolStateStore, oh as PromptImagePart, p as ModelUsage, ph as ResolveContentRefBlock, qf as StreamOptions, qi as AgentOptions, rg as StagedToolState, rh as ProgressNudgeMessage, sg as ToolStateView, sh as PromptPart, t as Preset, th as OAuthRefreshHookContext, uh as RepeatGuardConfig, v as MutationValidationBatch, vh as SessionHookContext, vr as ValidationResult, wh as StreamHookContext, x as MutationValidationMutation, xh as SpawnHookContext, y as MutationValidationFailure, yg as Session, yh as SessionMessage, zh as TurnFinishReason } from "./index-BGiCne_W.js";
2
+ import { Ah as ToolResultAudioContent, Bh as TurnUsage, Bm as ChildRunStats, C as SuccessfulMutationOutcome, Cg as SessionStore, Eh as ThinkingLevel, Fh as ToolResultImageRefContent, Fm as AgentRunOptions, Gh as ToolDef, Gi as AgentHookMap, Gm as DEFAULT_AGENT_CLOCK, Gp as CerebrasParams, Hh as toolOutputByteLength, Ih as ToolResultMetadata, Im as AgentStats, Jh as ReadStateEntry, Kf as StreamCallbacks, Kh as ToolMap, Ki as AgentHooks, Lh as ToolResultTextContent, Mh as ToolResultDocumentContent, Ng as RemoteStoreOptions, Nh as ToolResultDocumentRefContent, Nm as AgentBehavior, Np as OpenAIParams, Ns as SpawnToolOptions, Oh as ToolHookContext, Os as ChildAgent, Ph as ToolResultImageContent, Pm as AgentClock, Ps as SpawnToolState, Qf as TurnResult, Rh as ToolResultVideoContent, Rr as InteractionToolOptions, S as MutationValidationResult, Sg as SessionRun, Uf as Provider, Uh as toolResultToText, Vh as toolOutputBudgetByteLength, Wf as ProviderCapabilities, Wh as ToolContext, Wi as Agent, Xf as ToolResult, Xm as McpServerConfig, Yf as ToolCall, Yh as ReadStateMap, Zf as ToolSpec, Zm as McpToolHookContext, _ as statsByModel, _g as CreateSessionOptions, _h as SessionEndStatus, ag as ToolStateOp, ah as PromptDocumentPart, b as MutationValidationHooksOptions, bg as SessionData, bh as SessionTurn, ch as PromptTextPart, dh as RepeatGuardToolMatcher, em as AnthropicParams, f_ as McpConnection, fg as SkillConfig, fh as ResolveContentRef, gg as SkillsConfig, gh as SessionContentBlock, h as flattenTurns, hh as RunHookMap, ig as ToolStateController, ih as PromptAudioPart, jh as ToolResultContent, kh as ToolOutcome, lh as PromptVideoPart, lp as OpenRouterParams, mg as SkillResource, mh as RetryConfig, nh as ProgressNudgeKind, og as ToolStateStore, oh as PromptImagePart, p as ModelUsage, ph as ResolveContentRefBlock, qf as StreamOptions, qi as AgentOptions, rg as StagedToolState, rh as ProgressNudgeMessage, sg as ToolStateView, sh as PromptPart, t as Preset, th as OAuthRefreshHookContext, uh as RepeatGuardConfig, v as MutationValidationBatch, vh as SessionHookContext, vr as ValidationResult, wh as StreamHookContext, x as MutationValidationMutation, xh as SpawnHookContext, y as MutationValidationFailure, yg as Session, yh as SessionMessage, zh as TurnFinishReason } from "./index-D0O-QO7Z.js";
3
3
  import { _ as TaskHandle, a as ContextType, c as ExecutionContext, g as TaskExitInfo, h as TaskEntry, l as ExecutionHandle, n as ContextCapabilities, o as DetachedTasksCapability, p as SpawnConfig, r as ContextHardening, s as ExecResult, t as BackgroundTaskStatus, v as TaskStallInfo } from "./types-U-rIVUDw.js";
4
4
  import { t as SandboxProvider } from "./sandbox-CDl9tfTo.js";
5
5
  export { type Agent, AgentAbortedError, type AgentBehavior, AgentBudgetExceededError, type AgentClock, AgentContextExceededError, type AgentHookMap, type AgentHooks, type AgentOptions, AgentProviderError, type AgentRunOptions, type AgentStats, AgentToolNotAllowedError, type AnthropicParams, type BackgroundTaskStatus, CONTEXT_EXCEEDED_MESSAGE_PATTERNS, type CerebrasParams, type ChildAgent, type ChildRunStats, type ClassifiedError, type ClassifiedErrorKind, type ContextCapabilities, type ContextHardening, type ContextType, type CreateSessionOptions, DEFAULT_AGENT_CLOCK, type DetachedTasksCapability, type ExecResult, type ExecutionContext, type ExecutionHandle, type InteractionToolOptions, type McpConnection, type McpServerConfig, type McpToolHookContext, type ModelUsage, type MutationValidationBatch, type MutationValidationFailure, type MutationValidationHooksOptions, type MutationValidationMutation, type MutationValidationResult, type OAuthRefreshHookContext, type OpenAIParams, type OpenRouterParams, type Preset, type ProgressNudgeKind, type ProgressNudgeMessage, type PromptAudioPart, type PromptDocumentPart, type PromptImagePart, type PromptPart, type PromptTextPart, type PromptVideoPart, type Provider, type ProviderCapabilities, type ReadStateEntry, type ReadStateMap, type RemoteStoreOptions, type RepeatGuardConfig, type RepeatGuardToolMatcher, type ResolveContentRef, type ResolveContentRefBlock, type RetryConfig, type RunHookMap, type SandboxProvider, type Session, type SessionContentBlock, type SessionData, type SessionEndStatus, type SessionHookContext, type SessionMessage, type SessionRun, type SessionStore, type SessionTurn, type SkillConfig, type SkillResource, type SkillsConfig, type SpawnConfig, type SpawnHookContext, type SpawnToolOptions, type SpawnToolState, type StagedToolState, type StreamCallbacks, type StreamHookContext, type StreamOptions, type SuccessfulMutationOutcome, type TaskEntry, type TaskExitInfo, type TaskHandle, type TaskStallInfo, type ThinkingLevel, type ToolCall, type ToolContext, type ToolDef, type ToolHookContext, type ToolMap, type ToolOutcome, type ToolResult, type ToolResultAudioContent, type ToolResultContent, type ToolResultDocumentContent, type ToolResultDocumentRefContent, type ToolResultImageContent, type ToolResultImageRefContent, type ToolResultMetadata, type ToolResultTextContent, type ToolResultVideoContent, type ToolSpec, type ToolStateController, type ToolStateOp, type ToolStateStore, type ToolStateView, type TurnFinishReason, type TurnResult, type TurnUsage, type ValidationResult, flattenTurns, matchesContextExceeded, statsByModel, toolOutputBudgetByteLength, toolOutputByteLength, toolResultToText };
@@ -813,7 +813,7 @@ Merge rules:
813
813
 
814
814
  `run.behavior` > `agent.behavior` > defaults (field-by-field merge).
815
815
 
816
- Defaults: unlimited turns (set `maxTurns` as a safety net), model-driven output (`maxTokens` unset → Anthropic sends the model's max output since the field is mandatory, OpenAI-style providers omit it so the model runs to a natural stop; set a number for a hard per-turn cap), level-based thinking; `cache: true`, `dedupReads: true`, `requireReadBeforeEdit: false`, `maxConcurrentTools: 10`, `compactStrategy: 'off'`, `compactThreshold: 128 KiB`, `compactKeepTurns: 4`, `toolDisclosure: 'eager'`, `readLineNumbers: true`, `elideStaleReads: false`, `surfaceMcpInstructions: true`, `maxConsecutivePauseTurns: 0` (unlimited — empty-pause exhaustion no longer ends a live run by default), `persistTurns: true`, `providerStreamStartTimeoutMs: 300000`, `providerStreamTimeoutMs: 1800000`, and `mcpConnectTimeoutMs: 120000` for custom connectors. `persistTurns: true` means incremental per-turn store sync; set `false` to persist once at run end instead — trades mid-run crash granularity for O(1) writes per run on stores whose `appendTurns` re-serializes the session. All other knobs (`toolOutputBudget`, `dedupTools`, `toolBudgets`, `repeatGuard`, `thinkingDecay`, `thinkingOverrunPolicy`, `readFileDefaults`, `writeFileExistingPolicy`, `textToolCallRescue`, `unknownToolRecovery`, `progressNudges`, `maxTurnsWarningMessage`, `toolSearch`, `mediaKeepTurns`, `autoCompact`, `persistThreshold`, `persistDir`, `persistExcludeTools`, `persistMaxBytes`, `maxCostUsd`, `maxTotalTokens`, `resumeOnMaxTokens`, `retry`, `toolBatchTimeoutMs`) are `undefined` (`retry` defaults to 3 attempts with 1s → 30s exponential backoff when absent). The chat-layer profiles (`BUILD_AGENT` / `PLAN_AGENT`) override several of these (including turning `requireReadBeforeEdit` **on**, enabling `repeatGuard`, and setting `mediaKeepTurns: 8`) — see CHAT.md → Agents.
816
+ Defaults: unlimited turns (set `maxTurns` as a safety net), model-driven output (`maxTokens` unset → Anthropic sends the model's max output since the field is mandatory, OpenAI-style providers omit it so the model runs to a natural stop; set a number for a hard per-turn cap), level-based thinking; `cache: true`, `dedupReads: true`, `requireReadBeforeEdit: false`, `maxConcurrentTools: 10`, `compactStrategy: 'off'`, `compactThreshold: 128 KiB`, `compactKeepTurns: 4`, `toolDisclosure: 'eager'`, `readLineNumbers: true`, `elideStaleReads: false`, `surfaceMcpInstructions: true`, `maxConsecutivePauseTurns: 0` (unlimited — empty-pause exhaustion no longer ends a live run by default), `persistTurns: true`, `providerStreamStartTimeoutMs: 300000`, `providerStreamTimeoutMs: 1800000`, and `mcpConnectTimeoutMs: 120000` for custom connectors. `persistTurns: true` means incremental per-turn store sync; set `false` to persist once at run end instead — trades mid-run crash granularity for O(1) writes per run on stores whose `appendTurns` re-serializes the session. All other knobs (`toolOutputBudget`, `dedupTools`, `toolBudgets`, `repeatGuard`, `thinkingDecay`, `thinkingOverrunPolicy`, `readFileDefaults`, `writeFileExistingPolicy`, `textToolCallRescue`, `unknownToolRecovery`, `progressNudges`, `maxTurnsWarningMessage`, `toolSearch`, `mediaKeepTurns`, `autoCompact`, `persistThreshold`, `persistDir`, `persistExcludeTools`, `persistMaxBytes`, `maxCostUsd`, `maxTotalTokens`, `resumeOnMaxTokens`, `retry`, `toolBatchTimeoutMs`) are `undefined` (`retry` defaults to 3 attempts with 1s → 30s exponential backoff when absent). The chat-layer profiles (`BUILD_AGENT` / `PLAN_AGENT`) override several of these (including turning `requireReadBeforeEdit` **on**, enabling `repeatGuard` in `mode: 'both'`, and setting `mediaKeepTurns: 8`) — see CHAT.md → Agents.
817
817
 
818
818
  **`toolOutputBudget`** (off by default). The loop sums every tool result's `outputBytes` after `tool:transform`, so consumer truncation counts. On overshoot, a synthetic user message is appended:
819
819
 
package/docs/CHAT.md CHANGED
@@ -145,7 +145,7 @@ The table below indexes every named export; sections further down dive into the
145
145
  | `mcp-tool-toggle` | Per-tool deny-list state machine over `Settings.disabledMcpTools[serverName]`. `useMcpToolToggleSet({ serverName, catalog })` for a single server; `useMcpToolToggleMap({ catalogByServer })` for the multi-server settings UI (stable hook count across React renders). `buildToolToggle` for renderer-agnostic factory use. |
146
146
  | `mcp-tools-cache` | Per-server tool catalog persistence at `<dataDir>/mcp-tools-cache.json`. `loadMcpToolsCache`, `saveMcpToolsCache`, `clearMcpToolsCache`, `mcpToolsCachePath`, `CachedMcpToolList`, `McpToolsCache`. `subscribeMcpToolsCache(agent.hooks, { dataDir, onChange? })` listens on `mcp:tools:list` and writes the file; `refreshMcpToolsCatalog({ servers, hooks, ... })` opens transient connections to repopulate without a full agent run. |
147
147
  | `mcps-discovery` | `discoverProjectMcps`, `parseMcpsFile`, `buildMcpServers`, `defaultMcpsConfigPaths`, `DiscoveredMcp`, `DiscoveryError`, `DiscoveryResult`. `buildMcpServers` now also accepts `disabledTools: Settings['disabledMcpTools']` and unions it into each config's `disabledTools` (the JSON-side deny-list still wins on collisions). See **Project MCP servers**. |
148
- | `model-catalog` | Cross-provider unified model picker assembly — `buildModelCatalog`, `filterModelCatalog`, `indexOfEntry`, `CatalogEntry`. |
148
+ | `model-catalog` | Cross-provider unified model picker assembly — `buildModelCatalog`, `filterModelCatalog`, `indexOfEntry`, `parseCustomModelQuery`, `CatalogEntry`. |
149
149
  | `model-chain` | Ordered model fallback chains (pure helpers behind the picker's multi-select + the host's run-option assembly) — `ModelChainEntry`, `toggleChainEntry`, `chainPositionOf`, `chainsEqual`, `filterChainFallbacks`, `restoreChainFallbacks`, `sanitizeModelChain`, `providerKeySetChanged`, `buildRunModel`. |
150
150
  | `oauth` | `runOAuthLogin(descriptor, { onUrl, onPrompt, onProgress, signal })` + `supportsOAuth(descriptor)` + `oauthUsesManualCodePaste(descriptor)` — AI-provider OAuth (distinct from MCP OAuth). |
151
151
  | `oauth-redirect` | `fetchOAuthRedirect(pastedUrl, { authUrl? })` — SSH escape hatch. Validates `pasted` is a loopback URL, fires a GET so the in-process callback server (pi-ai's or `startOAuthCallback`'s) receives the request through its normal handler. Used by the TUI to accept a redirect-URL paste when the browser couldn't reach the callback server directly. `normalizeRedirectPaste(pasted, authUrl)` widens what counts as a paste (bare code / query / path); `redirectUriFromAuthUrl(authUrl)` reads back the loopback callback the request was minted with, which the TUI also displays. |
@@ -153,6 +153,7 @@ The table below indexes every named export; sections further down dive into the
153
153
  | `project-root` | `findGitRoot(cwd)` — walks upward looking for `.git`, returns absolute path or `null`. Used for session scope tagging + export anchors. |
154
154
  | `prompt-segments` | `splitPromptSegments(text, refs)` → `PromptSegment[]`. GUI maps the same segments to inline-block chip pills. |
155
155
  | `project-user-paths` | `projectUserPaths({ subPath, cwd, home, prefix })` — shared search-order builder for project + user config discovery. Powers `defaultSkillScanPaths` (`'skills'`) and `defaultMcpsConfigPaths` (calls it twice, once per `mcps.json` / `mcp.json`); the same convention extends to any new discovery surface. |
156
+ | `provider-usage` | Subscription usage per provider: `fetchProviderUsage`, `fetchAllProviderUsage`, `providerUsageSupported`, `usageSourceFor`, `mostConstrainedWindow`, `formatUsagePercent`, `clearProviderUsageCache`, `USAGE_CACHE_TTL_MS`. Types: `ProviderUsageSnapshot`, `ProviderUsageWindow`, `ProviderUsageCredits`, `ProviderUsageStatus`. Read-only and fail-soft (never refreshes tokens, never throws). See **Subscription usage**. |
156
157
  | `providers` | `ProviderDescriptor` registry + helpers — `BUILTIN_PROVIDERS` (`anthropicDescriptor`, `openaiDescriptor`, `openrouterDescriptor`, `cerebrasDescriptor`, `arceeDescriptor`, `localDescriptor`), `modelsForDescriptor`, `getModelInfo`, `getContextWindow`, `effectiveContextWindow`, `modelSupportsReasoning`, `OUTPUT_RESERVE_TOKENS`, `credKeyOf`, `piIdOf`. `cursorDescriptor` is exported but NOT in `BUILTIN_PROVIDERS` (OAuth works, inference not wired). See **Local provider**. |
157
158
  | `safe-mode` | Per-project safelist + matchers — `IMPLICITLY_SAFE_TOOLS`, `suggestSafelistEntries`, `addToSafelist`, `getSafelist`, `isOnSafelist`, `matchesSafelistEntry`, `readProjects`, `writeProjects`, `projectsFilePath`. |
158
159
  | `safe-mode-context` | `SafeModeProvider` owns a FIFO approval queue. `useSafeModeQueue()` returns the live array; `useSafeModeActions()` returns stable `{ requestApproval, resolveHead, denyAll }`. |
@@ -377,8 +378,9 @@ Both share `SHARED_BEHAVIOR`:
377
378
  grep: discoveryDedupHash,
378
379
  list_files: discoveryDedupHash,
379
380
  },
380
- repeatGuard: {}, // chain guard: shell blocks @4 / aborts @8;
381
- // read_file blocks @6 / aborts @12
381
+ repeatGuard: { mode: 'both', windowSize: 40 },
382
+ // chain AND sliding window: shell blocks @4 /
383
+ // aborts @8; read_file blocks @6 / aborts @12
382
384
  thinkingDecay: { afterTurn: 10, floor: 1024 }, // step form: ONE budget change
383
385
  // per run = one cache invalidation (the budget
384
386
  // is part of Anthropic's cache key); no-op
@@ -388,7 +390,7 @@ Both share `SHARED_BEHAVIOR`:
388
390
 
389
391
  `DEFAULT_PERSIST_EXCLUDE_TOOLS` = `['read_file', 'tool_search', 'skills_use', 'skills_read', 'present_plan', 'ask_user', 'spawn', 'todowrite', 'todoread']`. SDK consumers building custom profiles can extend this for any host-defined tool whose output should stay inline regardless of size. `DEFAULT_BUDGET_EXCLUDE_TOOLS` = `['tool_search', 'skills_use', 'skills_read']` (context-loading tools exempt from the per-turn output budget).
390
392
 
391
- The loop-prevention knobs are the one place the chat profiles diverge from the SDK core's conservative defaults. `requireReadBeforeEdit` is the only one that can *reject* a previously-succeeding call — but the refusal is a recoverable `tool_result` (`Edit error: <path> has not been read in this session. Call read_file first …`), not a hard error, so a model self-corrects in one turn. Content drift after a read is anchor-tolerant (the edit proceeds with a note when `old_string` still matches the current bytes verbatim; refusal covers anchors the drift broke and fuzzy-only matches, escalating on repeat to name the likely external writer) so churny workflows — a formatter or codegen touching files between edits — don't turn the guard into a standing "re-read everything" tax that pushes models toward shell-based edits. `repeatGuard` defaults to strict chain mode: it blocks the 4th back-to-back identical `shell` call (6th for `read_file`) and aborts at the 8th (12th for `read_file`) via the agent's `AbortController`, preceded by a `repeat-guard:exceeded` event. Consumers can opt into `mode: 'window' | 'both'`, custom `steeringMessage`, or `thresholdAction: 'steer'` to append guidance to the tool result instead of blocking at the soft threshold. `dedupTools` replay and `thinkingDecay` are free / inert until triggered.
393
+ The loop-prevention knobs are the one place the chat profiles diverge from the SDK core's conservative defaults. `requireReadBeforeEdit` is the only one that can *reject* a previously-succeeding call — but the refusal is a recoverable `tool_result` (`Edit error: <path> has not been read in this session. Call read_file first …`), not a hard error, so a model self-corrects in one turn. Content drift after a read is anchor-tolerant (the edit proceeds with a note when `old_string` still matches the current bytes verbatim; refusal covers anchors the drift broke and fuzzy-only matches, escalating on repeat to name the likely external writer) so churny workflows — a formatter or codegen touching files between edits — don't turn the guard into a standing "re-read everything" tax that pushes models toward shell-based edits. `repeatGuard` blocks the 4th identical `shell` call (6th for `read_file`) and aborts at the 8th (12th for `read_file`) via the agent's `AbortController`, preceded by a `repeat-guard:exceeded` event. The SDK core default is strict chain mode; **the chat profiles run `mode: 'both'` with a 40-call window**, because a chain resets on any intervening call and a spinning model always emits one. A real run burned 123 turns and 10M input tokens re-issuing one byte-identical `read_file` 59 times, alternating it with `grep`s on the same path: the longest consecutive streak was 5 against a threshold of 6, so nothing fired and the user had to cancel. The interleaved calls were `dedupTools` cache replays, which are free for the model and (by design) reset the chain, leaving the guard structurally unreachable. The window counts a payload among that tool's own recent calls, so dilution slows the trip instead of erasing it; the same run aborts at call 28 of 123. Consumers can still pick `mode: 'chain' | 'window'`, a custom `steeringMessage`, or `thresholdAction: 'steer'` to append guidance to the tool result instead of blocking at the soft threshold. `dedupTools` replay and `thinkingDecay` are free / inert until triggered.
392
394
 
393
395
  Other reliability levers stay off unless a profile opts in: `writeFileExistingPolicy: 'block-with-edit-recipe'` converts existing-file `write_file` rewrites into a recoverable `tool:gate` refusal; `readFileDefaults` sets omitted `read_file` slice defaults without changing explicit calls; `thinkingOverrunPolicy` queues a non-interrupting next-turn nudge when streamed thinking crosses a threshold; `textToolCallRescue` steers after a model writes JSON/XML/Liquid-style tool-call markup as plain text; `unknownToolRecovery` queues closest currently-advertised tool-name suggestions after an unknown native tool call; `progressNudges` emits bounded pivot nudges for repeated read-only turns, failed/no-op mutations, validation rejects, or tool errors; `maxTurnsWarningMessage` customizes the built-in turn-cap warning.
394
396
 
@@ -730,7 +732,7 @@ A user-overridable action catalog. Defaults declared in `KEYBINDING_DEFS`; user
730
732
  ```ts
731
733
  type KeyAction
732
734
  = | 'openSettings' | 'openSessionDetails' | 'openModelPicker' | 'openEffortPicker'
733
- | 'openTodos' | 'openContextPanel' | 'openKeybindings' | 'cycleAgent' | 'enterSelectTurnMode'
735
+ | 'openTodos' | 'openContextPanel' | 'openUsagePanel' | 'openKeybindings' | 'cycleAgent' | 'enterSelectTurnMode'
734
736
  | 'enterQueueSelection' | 'pushQueuedMessage' | 'dropQueuedMessage'
735
737
  | 'turnFork' | 'turnDelete' | 'turnCopy'
736
738
  | 'sessionDelete' | 'sessionCopyId' | 'sessionGenerateTitle'
@@ -787,6 +789,20 @@ A categorized view of what's consuming the context window — system prompt, rul
787
789
 
788
790
  `ContextBreakdown`, `ContextBreakdownOptions`, `ContextSnapshot`, `ContextCategory`, `ContextItem`, `ContextUsageSplit`, `ContextExactCounts`, `ContextMcpGroup`, and `buildContextBreakdown` are all exported from both `zidane/chat` and `zidane/chat/pure`.
789
791
 
792
+ ## Subscription usage
793
+
794
+ How much of a *subscription* the user has burned, as opposed to the context window (above) or session cost. `chat/provider-usage` probes the providers' own usage endpoints and normalizes the answers into one shape. Surfaced as the TUI footer indicator (`▰ 5h 43%`) plus the `ctrl+p` panel; see [Subscription-usage panel in TUI.md](./TUI.md#subscription-usage-panel).
795
+
796
+ - **`fetchProviderUsage(dataDir, descriptor, { refresh? })`** → `ProviderUsageSnapshot | null`. `null` means there is nothing to show: the provider has no usage surface, or no credential of the kind its probe needs. Otherwise the snapshot carries `status` (`'ok' | 'unauthorized' | 'unavailable'`), `plan`, `windows` (`{ id, label, usedPercent, resetsAt }`, percent 0-100, reset in epoch ms), `credits` (`{ remaining, total }`), `fetchedAt`, and a short `error` whenever `status !== 'ok'`.
797
+ - **`fetchAllProviderUsage(dataDir, registry, opts?)`** → one snapshot per credentialed provider, registry order. `providerUsageSupported(descriptor)` and `usageSourceFor(descriptor)` answer whether a provider reports usage at all, and `mostConstrainedWindow(snapshot)` picks the window closest to its ceiling, which is what a one-line indicator should show.
798
+ - **Credentials come from the file, then the environment for API keys.** `credentials.json` is checked first (it holds the credential `detectAuth` marks active); an API-key probe falls back to `process.env[descriptor.envKey]`, since `detectAuth` marks a provider available on a shell-set key alone and the launcher copies stored keys into env anyway. An entry whose `value` is empty (customFields-only) falls through to env too. OAuth probes stay file-only: those tokens have no env equivalent.
799
+ - **Probes are keyed by credential-file key**, not provider key, so a host descriptor reusing a builtin credential slot (`credentialFileKey: 'anthropic'`) inherits its probe: `anthropic` → `api.anthropic.com/api/oauth/usage`, `openai-codex` → `chatgpt.com/backend-api/wham/usage` (with the stored `accountId` as `ChatGPT-Account-Id`), `xai-oauth` → the `grok.com` gRPC-web billing probe, `openrouter` → `openrouter.ai/api/v1/credits`.
800
+ - **Fail-soft, never throwing.** Only the OpenRouter endpoint is official; the rest are unofficial and can change without notice. Transport errors, HTTP errors, gRPC out-of-band status (`grpc-status: 16` behind an HTTP 200) and unreadable payloads all normalize into a snapshot with a reason. The Grok payload is a protobuf with no schema available, recovered by a tolerant wire-format scan (`parseGrokBillingResponse`) rather than a vendored client, and it commonly refuses bearer tokens, so treat it as best-effort.
801
+ - **Read-only: tokens are never refreshed here.** The run path owns OAuth refresh and refresh tokens are single-use, so a background usage read must not spend one. An expired access token short-circuits to `status: 'unauthorized'` with no request issued.
802
+ - **Consent is two-tiered**, mirroring the update check. Hosts gate the *automatic* probes on `Settings.subscriptionUsage` (default on): the footer hook stops firing and the panel opens without loading, leaving `ctrl+r` inside it as an explicit fetch. `usageChecksOptedOut()` reads `ZIDANE_NO_USAGE_CHECK` / `CI` (same truthiness rule as `ZIDANE_NO_UPDATE`) and is enforced inside `fetchProviderUsage`, so it disables every probe including that refresh. Signing in to a provider is consent to talk to its inference endpoint, not to `chatgpt.com/backend-api` or `grok.com`.
803
+ - **Unauthorized reasons stay in one register**: `not signed in`, `token expired` (local checks), `token rejected` (401/403), `token not accepted` (Grok's gRPC rejection). Other non-2xx keep `HTTP <status>`, where the code is the useful detail.
804
+ - **Cached for `USAGE_CACHE_TTL_MS` (60s)** per credential key, with in-flight coalescing so a footer read and a panel open share one request. Snapshot identity (`providerKey` / `label`) is re-stamped per caller, since two descriptors can share a credential key. `refresh: true` bypasses both the cache and the coalescing map, so probes can overlap; each carries a monotonic ticket and only the newest may write the cache, which also means a probe spanning `clearProviderUsageCache()` cannot repopulate what it dropped. Keys are credential KEYS, not tokens, so a re-auth on the same provider would otherwise serve the previous account for a full TTL: the TUI calls `clearProviderUsageCache()` from its credential-refresh hook (`refreshAvailableProviders`).
805
+
790
806
  ## `SYSTEM.md` — overriding the built-in prompt
791
807
 
792
808
  `discoverSystemMd` (`chat/system-md`) finds a `SYSTEM.md` that **replaces** the built-in doctrine. Deliberately inverted semantics vs `AGENTS.md`:
@@ -1475,6 +1491,15 @@ const cursor = indexOfEntry(filtered, currentPick)
1475
1491
 
1476
1492
  Each `CatalogEntry` carries the provider key, model id, display name, capability flags (`reasoning`, `input`), context window, and a search corpus.
1477
1493
 
1494
+ ### Models the catalog doesn't serve
1495
+
1496
+ Persisted picks outlive catalogs: a provider retires an id, or the user names one the local list never had. Restoring such a pick is deliberate (an id typed by hand is legitimately off-catalog, and the bundled list often lags a provider's real lineup), but it must stay visible and selectable.
1497
+
1498
+ - **`include`** — `buildModelCatalog({ providers, modelsFor, current, include })` guarantees a row for each `{ providerKey, modelId }` pair, synthesizing a stub `ModelInfo` flagged `offCatalog: true` when the provider doesn't serve it. Stubs lead their provider's section and join the search corpus. A provider that serves nothing still appears when `include` names it. Pass the active pick plus every fallback-chain entry: without a row, a stale chain entry can't be toggled off and re-persists on every close.
1499
+ - **`parseCustomModelQuery(query, providers, fallbackProviderKey)`** — reads a query that matched nothing as "use this id anyway". A leading provider key or label is honored (`xai grok-9`, `OpenAI Codex gpt-9`, longest prefix wins) and stripped; otherwise the id is attributed to `fallbackProviderKey`, since a bare model id is meaningless without a provider. Returns `null` when nothing usable remains.
1500
+
1501
+ Renderers should print the `offCatalog` flag rather than the stub's zeroed metadata — the TUI picker shows `not in catalog` where it would otherwise show `ctx …`.
1502
+
1478
1503
  For an effort picker on top, gate visibility on `modelSupportsReasoning(descriptor, modelId)` and offer the `ThinkingLevel` values: `'off' | 'minimal' | 'low' | 'medium' | 'high' | 'adaptive'` (adaptive is Anthropic-only). Persist the user's choice keyed by **model id** (not provider) under `TuiState.lastEffortByModel` so switching the same model across providers preserves intent.
1479
1504
 
1480
1505
  ## Effective context window
package/docs/TUI.md CHANGED
@@ -97,7 +97,7 @@ import {
97
97
  | `AuthScreen` | First-run + re-config provider picker. Drives the OAuth / API-key wizard. |
98
98
  | `SessionsScreen` | Session list. Sorted by recency via the store. |
99
99
  | `Modal`, `ModalRoot`, `useModal`, `useModalAwareFocus` | Overlay layer. `Modal` owns its own esc-to-close handler. `useModalAwareFocus(preferred)` returns `false` while a modal is open so background inputs blur. **Single-slot:** `modal.open(node)` replaces the active node; modals do not stack. |
100
- | `ModelPickerModal` | Cross-provider searchable model picker. Two commit modes: **single pick** (`onPick`) — ↵ returns `{ providerKey, modelId }`, used by the compaction-model picker; **ordered multi-select** (`onApplyChain`) — ↵ toggles the row in a numbered fallback chain (the number is the row's 1-based position; pick order = fallback order), esc applies. Entry 1 is the primary (provider swap, effort, options); entries 2+ become run-level fallback targets (`agent.run({ model: […] })`). Esc with an unchanged or emptied chain is a plain cancel. Cross-provider chains trigger a provider swap + session re-activation in the host; the footer model chip shows `+N` while fallbacks are configured. When a run actually falls back (`model:fallback`), the chip switches to the model NOW serving the run, warn-colored as the "temporary" cue, and a transcript info line records the switch; both snap back at the next run (fallback is run-sticky per run). |
100
+ | `ModelPickerModal` | Cross-provider searchable model picker. Two commit modes: **single pick** (`onPick`) — ↵ returns `{ providerKey, modelId }`, used by the compaction-model picker; **ordered multi-select** (`onApplyChain`) — ↵ toggles the row in a numbered fallback chain (the number is the row's 1-based position; pick order = fallback order), esc applies. Entry 1 is the primary (provider swap, effort, options); entries 2+ become run-level fallback targets (`agent.run({ model: […] })`). Esc with an unchanged or emptied chain is a plain cancel. Cross-provider chains trigger a provider swap + session re-activation in the host; the footer model chip shows `+N` while fallbacks are configured. When a run actually falls back (`model:fallback`), the chip switches to the model NOW serving the run, warn-colored as the "temporary" cue, and a transcript info line records the switch; both snap back at the next run (fallback is run-sticky per run). **Off-catalog models:** the active pick and every seeded chain entry are passed to `buildModelCatalog` as `include`, so a model the provider has retired (or one typed by hand) still gets a row, marked `not in catalog` in warn color instead of a capability blurb. Without it such an entry is invisible, un-deselectable, and re-persists on every close. When a query matches nothing, the empty state is a **selectable** row offering the typed id (`parseCustomModelQuery`: a leading provider key/label routes it, otherwise the current provider owns it) and the `↵` hint reads `use anyway`; committing one in chain mode keeps it listed for the rest of the session so its position marker stays visible. |
101
101
  | `EffortPickerModal` | Reasoning-effort picker. Lists `'off' / 'minimal' / 'low' / 'medium' / 'high'` plus `'adaptive'` when `supportsAdaptive` is true (Anthropic only). Surfaced via `ctrl+l` when `modelSupportsReasoning(descriptor, model)`. |
102
102
  | `SettingsModal` | Tabbed: **Agent** (behaviour, persistence), **UI** (display, theming, animations), **Keybindings** (read-only catalog · `↵` opens `keybindings.json`), **Authentication** (current provider + detected providers · `↵` pops the AuthScreen), **Instructions** (live default-context segments · `tab` reads inline, `↵` opens file-backed sources), **Skills**, **MCP servers**. Category tab order is data-driven by `SETTINGS_CATEGORIES` from `zidane/chat`. Keybindings + Authentication tabs only appear when the host wires the `keybindings` / `authentication` data props; otherwise the legacy "open keybindings file" / "re-authenticate" rows render in the UI / Agent tabs as a fallback. One shared search input filters the active tab; `←/→` cycle tabs (preventDefault keeps the input cursor put), `↑/↓` navigate, `↵` toggle/cycle/pick/edit. MCP tab adds `ctrl+L` login, `ctrl+O` logout, and `esc` cancel-authorizing (Modal's esc-close still fires alongside). Sized at `maxWidth: 160, maxHeight: 50` — wider than the legacy modal so the Keybindings catalog + Authentication status block sit comfortably. |
103
103
  | `AgentPickerModal` | Multi-agent profile picker. Hidden when only one profile is registered. |
@@ -110,6 +110,7 @@ import {
110
110
  | `TitleOverlay` + `MetaSegment` | Colored title (left, defaults to `COLOR.brand`) + optional right-aligned meta on a bordered-box's top border. Plain string → dim label; `MetaSegment[]` → per-segment colors. Render as a SIBLING of the bordered box (not a child) so the border row isn't clipped. Every span carries an explicit background (`bg`, default `SURFACE.background`): OpenTUI keeps a cell's existing char when a span paints a space with NO background, so a transparent overlay lets the border `─` bleed through the title's gaps (reads as a strikethrough) and lets a native `title=` on the same row interleave characters with it (`" plan "` under `"New session"` → `Nplane`). Opaque cuts both ways — `bg` must MATCH the surface under the decorated box, so pass `SURFACE.modal` for modal content (`McpAuthorizingPanel` does) or the overlay stamps a mismatched run along the border row. One title per row, therefore: the chat screen drops this overlay while the plan / file-edit panel owns the transcript slot and paints its own. Pinned in `test/tui/title-overlay-collision.test.tsx`. |
111
111
  | `renderHintSpans`, `hintsLength`, `Hint` | Footer + prompt-hint primitives. A `Hint` is `{ key, label, keyColor?, labelColor? }`; renderers compose them via `renderHintSpans(hints, COLOR)`. |
112
112
  | `ContextUsage` | `{ used, max }` shape passed to `<Footer context>`. Drives the right-aligned context indicator. |
113
+ | `FooterUsage` | `{ label, usedPercent }` shape passed to `<Footer usage>`. Drives the subscription-usage segment left of the cost (`▰ 5h 43%`): the active provider's tightest rate-limit window, resolved by the host via `mostConstrainedWindow` (`zidane/chat`). Rendered as a floored whole percent, so `99.9` never reads as a ceiling the user hasn't hit. `null` hides it. Pair with `<Footer usageKey>` (a keybinding spec, default `ctrl+p`) to make it a click target. |
113
114
  | `buildMdStyle(theme)`, `useMdStyle()` | Convert a `Theme` to an OpenTUI `SyntaxStyle`. Hook returns the shared instance for the active theme. |
114
115
  | `onInputSubmit(handler)` | Cast helper — bypasses an OpenTUI / DOM type collision on `<input onSubmit>`. |
115
116
  | `isVisible`, `marginTopFor`, `isEditErrorResult` | Re-exported from `zidane/chat` (`store` module). Pure transcript-view rules — a GUI shell renders against the same matrix. |
@@ -196,6 +197,7 @@ Every customizable shortcut routes through `<userDir>/keybindings.json` and is p
196
197
  | `ctrl+s` | `enterSelectTurnMode` | chat (idle) | enter select-turn mode (transcript navigation) |
197
198
  | `ctrl+t` | `openTodos` | chat (session attached) | open the active run's `todowrite` checkpoints (read-only) |
198
199
  | `ctrl+shift+g` | `openContextPanel` | chat | open the context-usage breakdown panel (read-only). Needs a terminal with the kitty keyboard protocol to stay distinct from `ctrl+g`; rebind on legacy terminals like Terminal.app |
200
+ | `ctrl+p` | `openUsagePanel` | chat | open the subscription usage panel (read-only): per-provider rate-limit windows and credit balances. `ctrl+r` refetches inside it. Also reachable by clicking the footer's `▰ 5h 43%` segment. Plain `ctrl+p` rather than a `ctrl+shift+…` chord, so it behaves the same on kitty and legacy terminals, and it is free in OpenTUI's textarea keymap |
199
201
  | `ctrl+y` | `openKeybindings` | non-auth | open the keybindings panel — read-only catalog grouped by surface, with `↵` to edit `~/.zidane/keybindings.json` |
200
202
  | `ctrl+k` | `cancelToolCall` | chat (busy, ≥1 in-flight call) | open the cancel-tool picker — pick one call to flip via `agent.cancelTool(callId)`, or press `a` to cancel every in-flight call. Distinct from `esc abort` (whole-run abort); siblings keep running |
201
203
  | `ctrl+v` | `attachClipboardImage` | textarea | paste **text or an image** from the OS clipboard, read by the process itself rather than through the terminal's paste path. Images become attachments; text goes through the same policy as a normal paste (large / multiline folds into `paste.txt`). Because only the keystroke crosses the PTY, this is the way out of integrated terminals whose PTY layer freezes on large `cmd+V` pastes (e.g. Cursor). Reads via a platform helper (`pngpaste` or an `osascript` fallback on macOS; `wl-paste`/`xclip` on Linux; PowerShell on Windows). An empty bracketed-paste event also probes the clipboard for an image. Soft-fails with an inline notice (nothing on the clipboard / helper missing / over SSH / too large). Image attaching is gated on `attachmentsEnabled`; the action id stays `attachClipboardImage` so existing keybinding overrides keep working |
@@ -362,10 +364,28 @@ Two thin chrome layers on top of the renderer-agnostic `useActiveTodos` hook (se
362
364
 
363
365
  Both surfaces resolve "what list to show" via `useActiveTodos(session)`, which carries both the live `todos` (model-facing) and the `archive` (sidecar — see [Todos in CHAT.md](./CHAT.md#todos) for the archive contract). The indicator only renders when there's a live `in_progress` item (an archived in-progress item is by definition stale and hidden). The modal renders `todos` when non-empty, falling back to `archive` when the live slot has been auto-cleared.
364
366
 
367
+ ### Notice rows — harness events in the transcript
368
+
369
+ Things the *harness* did, as opposed to things the model said: a run error, a model fallback, a repeat-guard trip, history repair, a stop-reason explanation. Before these rows existed each producer hand-rolled its own emoji prefix and prose, errors dumped a raw multi-line provider message into the transcript, and none of it survived a reload.
370
+
371
+ - **Shape.** `StreamEvent.notice = { severity: 'info' | 'warn' | 'error', label }` on an ordinary `info` / `error` event. An event without it keeps the previous free-text rendering, which is also why the GUI timeline (it switches on `kind`) needs no change.
372
+ - **Row.** `NoticeRow` (`src/tui/components.tsx`) draws `<glyph> <label padded to 15> <message>`: glyph and label in the severity color (`error` / `warn` / `mute`), message dim. Single-width glyphs only (`✗ ! ·`) because the emoji they replaced render two cells wide in some terminals and one in others, which an aligned gutter cannot absorb. The row is clipped, never wrapped, so a 400-character provider error stays one line; a multi-line message collapses to its first line plus `+N lines`, with the full text still persisted. Consecutive notices stack tight (`marginTopFor`), so a burst reads as one block.
373
+ - **Persistence.** `emitNotice` (`app.tsx`) pre-mints a `turnId`, appends the live event, and writes a display-only `role: 'command'` turn carrying `metadata.command.notice` (`noticeTurnContent` in `chat/store.ts`). `eventsFromTurns` replays it into the same event, so the reloaded row IS the live row. Holding that invariant takes two exemptions from the surrounding command-turn handling: the marker also carries the live `childId` / `depth` (a subagent notice is persisted by the parent's depth-0 run, so the run-derived tag alone would reload it flat), and a notice-only turn skips the delimiting `separator` (live there is none, and `marginTopFor` collapses consecutive notices, so a separator would reload a burst as a double-spaced ladder). A malformed marker degrades to an ordinary `command-detail` row rather than throwing.
374
+ - **Session scoping.** Callers whose work outlives a session switch (run hooks, awaited command handlers) pass their own `session`, captured when the work started, and `emitNotice` splits the two halves: the live row is skipped when that session is no longer the open one, while the turn is still persisted to the run's own session. A run keeps streaming after the user opens another conversation, so without the first half its late fallback or error row lands in the wrong transcript and appends a durable turn that replays there forever; without the second half a run that fails while you are looking elsewhere leaves no record anywhere, which is exactly when one is worth the most. Reopening the original session replays the row in place.
375
+ - **What is not persisted.** Progress banners (`compact:start`), extension hot-reload chatter, and the system-prompt-override warning pass `persist: false`: after a reload they are either meaningless, duplicative of a boundary card that persists on its own, or would accumulate one copy per load.
376
+ - **Repeat guard.** `repeat-guard:exceeded` emits once per `(tool, action)` per run: one row when the guard first blocks and steers, one when it aborts. Without it the intervention is invisible, because a gate-blocked call never reaches `tool:before` and a guard abort otherwise looks byte-identical to the user pressing esc. **Parent runs only** — `repeat-guard:exceeded` is not in `BUBBLED_EVENTS` (`src/tools/spawn.ts`), so a subagent's trip reaches no bus the TUI listens on and produces no row; an aborted child shows up only as the generic `subagent` error row from `spawn:error`. Closing that gap means bubbling the event as `child:repeat-guard:exceeded` and hooking it separately, where the ctx would carry the `childId` / `depth` `emitNotice` already accepts.
377
+
365
378
  ### Context-usage panel
366
379
 
367
380
  - **`ContextPanelModal`** (`src/tui/context-panel.tsx`) — opened via `ctrl+shift+g` (`openContextPanel`), closes on `esc`. Read-only, on-demand: the `App.tsx` handler calls `agent.getContextBreakdown({ model, effectiveWindow, autocompactBuffer, compactThreshold })` (see [Context-usage breakdown in CHAT.md](./CHAT.md#context-usage-breakdown)) and opens the modal with the result — guarded so a stale async resolve (session switched mid-fetch) doesn't pop a panel for the wrong agent. Renders a single full-width stacked bar (one fixed-hue segment per live category, the autocompact-buffer zone pinned at the tail from the threshold to the window end, free space as the remaining track) above per-category rows (swatch · label · token count · percent). Estimated rows carry a `~` prefix; the footnote `~ estimated — exact counts unavailable for this provider` shows only when ≥1 live category is heuristic. Below the categories: an "active skills" section (informational — an activated skill's body lives in the conversation, already counted there) a "last turn" section with the exact cache-read / cache-write / fresh-input / output split from `TurnUsage`, and a "usage by model" section (session-wide per-model turn/token/cost totals across parent + subagent runs, via `usageByModel` from `zidane/chat`; `≥` marks costs floored by turns with no known rate; hidden while the session has no runs). Expandable rows (MCP tools, per-rule files) toggle with `↵`/`space`; `↑↓` move the cursor across expandable groups. The panel works before the first message via the agent's skills-only pre-run assembly. The bar palette is shared verbatim with the GUI popover so both surfaces read identically.
368
381
 
382
+ ### Subscription-usage panel
383
+
384
+ - **`UsagePanelModal`** (`src/tui/usage-panel.tsx`) opens via `ctrl+p` (`openUsagePanel`) or by clicking the footer's `▰ 5h 43%` segment, closes on `esc`; `ctrl+r` refetches. One section per authenticated provider that exposes usage (see [Subscription usage in CHAT.md](./CHAT.md#subscription-usage)), the **active** provider sorted first and marked `active`. Each section renders `label · plan` then one row per rate-limit window: 12-cell `▰▱` bar, short window label, `used%`, then either `resets in 2h14m` or, for a credits window, the remaining balance (`20.00 / 25.00`). Providers whose credits sit alongside rate-limit windows (Codex) get a separate `credits` row. A provider whose probe failed keeps its section and shows `not authorized · <reason>` or `unavailable · <reason>` rather than disappearing, so a broken token is visible instead of silent. Percent thresholds match the footer's `ctx` gauge (warn ≥60%, error ≥85%), and reset labels round down at every step (36h reads `1d`), like the footer percent: overstating the headroom left is the wrong direction to be wrong in.
385
+ - **Opt-outs.** `Settings.subscriptionUsage` (Agent tab, default on) gates every automatic probe: the footer indicator stops fetching and the panel opens on `usage checks are off · ctrl+r fetches once`, so an explicit refresh still works (that line is replaced by the ordinary empty state once a probe has actually run, so the user can tell the fetch happened). `ZIDANE_NO_USAGE_CHECK` / `CI` (any truthy value, same rule as `ZIDANE_NO_UPDATE`) disable the feature outright, including that refresh; the panel then says so and drops the `ctrl+r` hint. `ctrl+r` is ignored while a probe is in flight, so key repeat can't fan out one request per provider per keypress.
386
+ - **`Settings.showUsageIndicator`** (UI tab, default on) hides the footer gauge only; `ctrl+p` still opens the panel, same contract as `showTodoIndicator`. The footer hook exists solely to feed that gauge, so it stands down with it and the panel's on-open fetch becomes the only caller. This is the display switch; `Settings.subscriptionUsage` (Agent tab) is the network switch and decides whether providers are asked at all.
387
+ - **The panel loads its own data.** `<ModalRoot>` sits above `<AppShell>`, so a parent-owned snapshot would freeze at open time and `ctrl+r` would have nowhere to write; the modal calls `useAllProviderUsage` itself (`src/tui/use-provider-usage.ts`). The footer indicator uses the sibling `useActiveProviderUsage` hook, which fetches on provider switch and on run completion (throttled to ≥15s apart) and **never polls on a timer**, so an idle TUI issues no requests. Both hooks read through the shared 60s cache in `chat/provider-usage`, so opening the panel right after a footer refresh costs nothing.
388
+
369
389
  In addition, every `todowrite` tool call in the transcript renders a small "what's in progress" sub-list directly beneath the formatted call line. The sub-list reads the call's `input.todos` payload (the checkpoint the model just wrote), filters to `status === 'in_progress'`, and prints one indented `◐ <content>` row per item. Hidden when the payload has no in-progress items (e.g. all-completed close-out) so the transcript stays tight. This is purely declarative — it reads the event's own payload, not session state, so it's stable across re-renders and matches what the model intended at that checkpoint (not the latest list state, which the indicator + modal already surface).
370
390
 
371
391
  Because top-level runs share `session.metadata.todos.session`, a list written in one prompt **stays visible across the next prompt** — the user aborts mid-task, sends a follow-up, and the indicator + modal continue to show the same list until the model rewrites or auto-clears it. Subagent runs surface their own slot when they're the live run (`pickActiveRunId` picks the latest running run); when a subagent completes, the active resolution reflows to the parent's session slot on the next paint.
@@ -417,7 +437,7 @@ A single shared search input filters the active tab's list (`label + description
417
437
 
418
438
  For the live source of truth, see `SETTINGS_TOGGLES` / `SETTINGS_CHOICES` in `src/chat/settings-context.tsx`. The current shape includes:
419
439
 
420
- - **Toggles** — `safeMode`, `resumeLastSession`, `hideSubagentOutput`, `persistToolResults`, `autoCompact`, `showAllProjects`, `showThinking`, `showToolResults`, `showFullShellOutput`, `showEditDiffs`, `smoothStreaming`, `showTodoIndicator`, `showThrobber`.
440
+ - **Toggles** — `safeMode`, `resumeLastSession`, `hideSubagentOutput`, `persistToolResults`, `autoCompact`, `showAllProjects`, `showThinking`, `showToolResults`, `showFullShellOutput`, `showEditDiffs`, `smoothStreaming`, `showTodoIndicator`, `showUsageIndicator`, `showThrobber`.
421
441
  - **Choices** — `interactionMode` (Ask normally / Skip interactions / Disable tools), `toolCallDisplay` (Formatted / Full / Hidden), `autoCompactThreshold` (60% / 70% / 80% / 90%), `theme` (every entry in `BUILTIN_THEMES`), `targetFps` (30 / 60 / 120).
422
442
 
423
443
  `enabledSkills` / `enabledMcps` deliberately default to `undefined` (every discovered entry enabled). The Settings modal seeds the allowlist with the current discovery on first toggle so newly-added entries don't silently flip on — the user opts them in.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zidane",
3
- "version": "6.6.1",
3
+ "version": "6.6.2",
4
4
  "description": "an agent that goes straight to the goal",
5
5
  "type": "module",
6
6
  "private": false,