zidane 5.2.0 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -7
- package/dist/chat.d.ts +4 -138
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +2 -2
- package/dist/{index-BRTRan3z.d.ts → index-D-cTScN3.d.ts} +8 -29
- package/dist/{index-BRTRan3z.d.ts.map → index-D-cTScN3.d.ts.map} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -3
- package/dist/{login-D7Tp-K5f.js → login-BXVt5wuA.js} +2 -2
- package/dist/{login-D7Tp-K5f.js.map → login-BXVt5wuA.js.map} +1 -1
- package/dist/{presets-AgF0RFx1.js → presets-tvD28pCu.js} +10 -29
- package/dist/presets-tvD28pCu.js.map +1 -0
- package/dist/presets.d.ts +1 -1
- package/dist/presets.js +1 -1
- package/dist/{tools-BRbbfdJh.js → tools-CMVruxF0.js} +2 -130
- package/dist/tools-CMVruxF0.js.map +1 -0
- package/dist/tools.d.ts +1 -1
- package/dist/tools.js +1 -1
- package/dist/{transcript-anchors-GXkj_ox7.d.ts → transcript-anchors-eyhlGeBI.d.ts} +2 -2
- package/dist/transcript-anchors-eyhlGeBI.d.ts.map +1 -0
- package/dist/tui.d.ts +1 -1
- package/dist/tui.js +55 -5
- package/dist/tui.js.map +1 -1
- package/dist/{turn-operations-CQTMxSnC.js → turn-operations-Y7e15gJf.js} +7 -330
- package/dist/turn-operations-Y7e15gJf.js.map +1 -0
- package/dist/types.d.ts +1 -1
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/CHAT.md +56 -16
- package/docs/SKILL.md +2 -2
- package/docs/TUI.md +22 -2
- package/package.json +1 -1
- package/dist/presets-AgF0RFx1.js.map +0 -1
- package/dist/tools-BRbbfdJh.js.map +0 -1
- package/dist/transcript-anchors-GXkj_ox7.d.ts.map +0 -1
- package/dist/turn-operations-CQTMxSnC.js.map +0 -1
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as ExecutionHandle, i as ExecutionContext, n as ContextType, o as SpawnConfig, r as ExecResult, t as ContextCapabilities } from "./types-Ce78ds4h.js";
|
|
2
2
|
import { t as SandboxProvider } from "./index-BiO_5Hm4.js";
|
|
3
3
|
import { A as SessionStore, At as SpawnHookContext, Bt as toolOutputByteLength, C as SkillResource, Ct as PromptTextPart, D as Session, Dt as SessionHookContext, E as CreateSessionOptions, Et as SessionEndStatus, Ft as ToolResultContent, G as ProviderCapabilities, Gt as AgentToolNotAllowedError, Ht as AgentAbortedError, It as ToolResultImageContent, J as ToolCall, Jt as ClassifiedErrorKind, K as StreamCallbacks, Kt as CONTEXT_EXCEEDED_MESSAGE_PATTERNS, Lt as ToolResultTextContent, Mt as ThinkingLevel, N as RemoteStoreOptions, Nt as ToolExecutionMode, O as SessionData, Ot as SessionMessage, Pt as ToolHookContext, Q as OpenRouterParams, Rt as TurnFinishReason, St as PromptPart, T as SkillsConfig, Tt as SessionContentBlock, Ut as AgentContextExceededError, Vt as toolResultToText, W as Provider, Wt as AgentProviderError, X as ToolSpec, Xt as matchesContextExceeded, Y as ToolResult, Z as TurnResult, _t as McpToolHookContext, b as ToolMap, bt as PromptDocumentPart, ct as CerebrasParams, ft as AgentBehavior, gt as McpServerConfig, ht as ChildRunStats, i as AgentOptions, jt as StreamHookContext, k as SessionRun, kt as SessionTurn, mt as AgentStats, ot as OpenAIParams, p as McpConnection, pt as AgentRunOptions, q as StreamOptions, qt as ClassifiedError, r as AgentHooks, t as Agent, ut as AnthropicParams, v as ToolContext, wt as RunHookMap, x as SkillConfig, xt as PromptImagePart, y as ToolDef, yt as OAuthRefreshHookContext, zt as TurnUsage } from "./agent-CYpPKn5Z.js";
|
|
4
|
-
import { I as ModelUsage, L as flattenTurns, R as statsByModel, _ as ChildAgent, f as ValidationResult, j as InteractionToolOptions, t as Preset, v as SpawnToolOptions, y as SpawnToolState } from "./index-
|
|
4
|
+
import { I as ModelUsage, L as flattenTurns, R as statsByModel, _ as ChildAgent, f as ValidationResult, j as InteractionToolOptions, t as Preset, v as SpawnToolOptions, y as SpawnToolState } from "./index-D-cTScN3.js";
|
|
5
5
|
export { type Agent, AgentAbortedError, type AgentBehavior, AgentContextExceededError, type AgentHooks, type AgentOptions, AgentProviderError, type AgentRunOptions, type AgentStats, AgentToolNotAllowedError, type AnthropicParams, CONTEXT_EXCEEDED_MESSAGE_PATTERNS, type CerebrasParams, type ChildAgent, type ChildRunStats, type ClassifiedError, type ClassifiedErrorKind, type ContextCapabilities, type ContextType, type CreateSessionOptions, type ExecResult, type ExecutionContext, type ExecutionHandle, type InteractionToolOptions, type McpConnection, type McpServerConfig, type McpToolHookContext, type ModelUsage, type OAuthRefreshHookContext, type OpenAIParams, type OpenRouterParams, type Preset, type PromptDocumentPart, type PromptImagePart, type PromptPart, type PromptTextPart, type Provider, type ProviderCapabilities, type RemoteStoreOptions, 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 StreamCallbacks, type StreamHookContext, type StreamOptions, type ThinkingLevel, type ToolCall, type ToolContext, type ToolDef, type ToolExecutionMode, type ToolHookContext, type ToolMap, type ToolResult, type ToolResultContent, type ToolResultImageContent, type ToolResultTextContent, type ToolSpec, type TurnFinishReason, type TurnResult, type TurnUsage, type ValidationResult, flattenTurns, matchesContextExceeded, statsByModel, toolOutputByteLength, toolResultToText };
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -90,7 +90,7 @@ flowchart TB
|
|
|
90
90
|
S2 --> S3["stream:text hook (per chunk)"]
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
-
STREAM -->|error| CATCH["
|
|
93
|
+
STREAM -->|error| CATCH["stream:error hook (raw err)\nturn:after hook (zero usage)\nRethrow"]
|
|
94
94
|
STREAM -->|ok| S4{"Has text?"}
|
|
95
95
|
S4 -->|yes| S5["stream:end hook"]
|
|
96
96
|
S4 -->|no| AFTER
|
|
@@ -428,6 +428,7 @@ turn:before ← turn starts
|
|
|
428
428
|
oauth:refresh ← (when provider supports it)
|
|
429
429
|
stream:thinking (n) / stream:text (n) ← each streamed chunk
|
|
430
430
|
stream:end ← text complete (if text present)
|
|
431
|
+
stream:error ← provider rejected before stream:end (omitted on user-initiated aborts)
|
|
431
432
|
turn:after ← always fires (incl. errors); SessionTurn + toolCounts
|
|
432
433
|
tool:gate [mutable: block, reason, result?] ← refuse / substitute / run
|
|
433
434
|
tool:unknown [mutable: result?, suppressError] ← no toolDef registered
|
|
@@ -494,7 +495,7 @@ spawn:error ← child run threw
|
|
|
494
495
|
The child's lifecycle also bubbles to the parent hook surface with `childId` + `depth`, so nested UI renders without subscribing on the child:
|
|
495
496
|
|
|
496
497
|
```
|
|
497
|
-
child:stream:text / child:stream:thinking / child:stream:end
|
|
498
|
+
child:stream:text / child:stream:thinking / child:stream:end / child:stream:error
|
|
498
499
|
child:tool:gate / child:mcp:tool:gate ← share the child's ctx — parent mutations propagate
|
|
499
500
|
child:tool:transform ← share the child's ctx — parent mutations propagate
|
|
500
501
|
child:tool:before / child:tool:after / child:tool:error
|
package/docs/CHAT.md
CHANGED
|
@@ -154,7 +154,7 @@ The table below indexes every named export; sections further down dive into the
|
|
|
154
154
|
| `store` | Session reconstruction + persisted UI state + transcript view rules — `eventsFromTurns`, `lastContextSizeFromTurns`, `listSessionMeta`, `deriveSessionTitle`, `titleFromTurns`, `selectableTurnIds`, `stripSpawnTokensLine`, `toolCallPreview`, `toolResultText`, `createStateStore`, `loadState`, `saveState`, `isVisible`, `marginTopFor`, `isEditErrorResult`, `isTurnHighlighted`, `sumRunCosts`, `turnSelectionOwnership`, `TuiState`, `StateStoreApi`. |
|
|
155
155
|
| `streaming` | `useStreamBuffer`, `finalizeStreamingMarkdown`, `finalizeStreamingMarkdownForOwner`, `turnContextSize`. Per-owner finalize handles concurrent parent + child streams. |
|
|
156
156
|
| `theme` + `theme-context` | `Theme`, `BUILTIN_THEMES`, `resolveTheme`, `resolveChipColor`, `DEFAULT_THEME`, themes (`CATPPUCCIN_*`, `VAPORWAVE_THEME`), hooks (`useTheme`, `useColors`, `useSelectStyle`, `useSurfaces`, `useSyntaxStyles`). See **Theme**. |
|
|
157
|
-
| `todos` | `todowrite` / `todoread` tool factory — `createTodoTools` (returns a `Preset`), `isTodoTool`, `getTodosForRun`, `setTodosForRun`, `pruneTodosByRun`, `TODOWRITE_TOOL`, `TODOREAD_TOOL`, `TODOS_METADATA_KEY`, `TODO_WRITE_COUNTS_METADATA_KEY`, `TodoItem`, `TodoStatus`, `CreateTodoToolsOptions`.
|
|
157
|
+
| `todos` | `todowrite` / `todoread` tool factory — `createTodoTools` (returns a `Preset`), `isTodoTool`, `getTodosForRun`, `setTodosForRun`, `pruneTodosByRun`, `TODOWRITE_TOOL`, `TODOREAD_TOOL`, `TODOS_METADATA_KEY`, `TODO_WRITE_COUNTS_METADATA_KEY`, `TodoItem`, `TodoStatus`, `TodosBag`, `CreateTodoToolsOptions`. Persistent task checkpointing — top-level runs share a session-scoped slot (continuity across prompts); subagent runs get their own slot (isolation). Per-run write budget plumbs through `behavior.toolBudgets`; identical-payload dedup lives in the tool body so it resolves against the active slot. Compose with `composePresets` or spread. See **Todos** below. |
|
|
158
158
|
| `tool-formatters` | Per-tool display verbs + curated input formatters — `TOOL_DISPLAY`, `displayNameFor`, `formatToolCall`, `ToolDisplayMeta`, `ToolFormatLine`. Powers the `'formatted'` `toolCallDisplay` view. See **Tool call display**. |
|
|
159
159
|
| `transcript-anchors` | `computeTurnAnchors(items)` → `TranscriptItem[]` — assigns `turn-anchor-<turnId>` ids consumers wire into their renderer's scroll-into-view (TUI: `ScrollBoxRenderable.scrollChildIntoView`; GUI: DOM `element.scrollIntoView`). |
|
|
160
160
|
| `turn-operations` | `deleteTurnSafely`, `truncateTurnsAt`, `turnAsText`, `countNeighbors`. Fork / delete with orphan tool-pair cleanup. |
|
|
@@ -357,6 +357,8 @@ interface Settings {
|
|
|
357
357
|
targetFps: number
|
|
358
358
|
/** Drip-feed streamed text at a smooth cadence (typewriter) instead of in stream bursts. Default: on. */
|
|
359
359
|
smoothStreaming: boolean
|
|
360
|
+
/** Inline gradient throbber at the transcript tail while a run is streaming. Default: off. */
|
|
361
|
+
showThrobber: boolean
|
|
360
362
|
/** Skills allowlist. `undefined` = every discovered skill; `[]` = off. */
|
|
361
363
|
enabledSkills?: readonly string[]
|
|
362
364
|
/** MCPs allowlist. Same semantics. */
|
|
@@ -476,25 +478,48 @@ const pending = queue[0] ?? null
|
|
|
476
478
|
|
|
477
479
|
## Todos
|
|
478
480
|
|
|
479
|
-
`todowrite` and `todoread` give the model a place to plan multi-step work and stream progress as it executes. Monolithic-replacement semantics — every `todowrite` overwrites the active
|
|
481
|
+
`todowrite` and `todoread` give the model a place to plan multi-step work and stream progress as it executes. Monolithic-replacement semantics — every `todowrite` overwrites the active list in full. State lives at `session.metadata.todos: TodosBag` and is split by run kind:
|
|
482
|
+
|
|
483
|
+
```ts
|
|
484
|
+
interface TodosBag {
|
|
485
|
+
/** Top-level slot — shared across runs whose `parentRunId` is unset. */
|
|
486
|
+
session?: TodoItem[]
|
|
487
|
+
/** Per-subagent-run slots, keyed by the child run's id. */
|
|
488
|
+
byRun?: Record<string, TodoItem[]>
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
The keying rule, given a `runId`:
|
|
493
|
+
|
|
494
|
+
- **Top-level run** (no `parentRunId`) → `bag.session`. All top-level runs in the same session share this slot, so a list written in one prompt **survives across the next prompt**. The user aborts a run mid-task, sends a follow-up, and `todoread` returns the same list automatically — the model picks up where it left off without a host-side carry-over hook.
|
|
495
|
+
- **Subagent run** (`parentRunId` set) → `bag.byRun[runId]`. Each child has its own slot; parallel children stay isolated from each other and from the parent. Matches Claude Code's TodoWrite v1 keying (`agentId ?? sessionId`).
|
|
496
|
+
|
|
497
|
+
**Auto-clear on completion**: when every item is `completed`, the slot is wiped to `[]` on write — prevents stale "all done" lists from shadowing the next prompt's context. The model's reply summarizes the close-out (`Marked N items complete — list cleared.`); only the persisted slot is reset. Same rationale as Claude Code's `TodoWriteTool.ts:70`.
|
|
498
|
+
|
|
499
|
+
**Archive sidecar**: every non-empty write also stashes the list under `bag.archive` (mirroring the active slot's routing — `archive.session` for top-level, `archive.byRun[runId]` for subagents). Empty writes — whether from auto-clear or an explicit `setTodosForRun(session, runId, [])` — **preserve** the archive. UI surfaces (the TUI modal in particular) fall back to it when the live slot is empty so users can still glance back at "what was just completed" until the model writes a new list. The archive is invisible to `todoread` — strictly model-facing reads always go to the live slot.
|
|
480
500
|
|
|
481
501
|
Public API (re-exported from `zidane/chat`):
|
|
482
502
|
|
|
483
|
-
- **Factory** — `createTodoTools(options?)` returns a `Preset` carrying `{ tools, behavior }` (tools +
|
|
503
|
+
- **Factory** — `createTodoTools(options?)` returns a `Preset` carrying `{ tools, behavior }` (tools + per-run write budget). Identical-payload dedup lives inside the tool body (resolves against the active slot per the keying rule above) and deliberately does NOT plumb through `behavior.dedupTools`. Compose with `composePresets` or spread it into your agent setup like any other preset.
|
|
484
504
|
- **Identities** — `TODOWRITE_TOOL`, `TODOREAD_TOOL`, `isTodoTool(name)`.
|
|
485
|
-
- **Metadata accessors** — `getTodosForRun(session, runId)`, `setTodosForRun(session, runId, items)`, `pruneTodosByRun(session)`, `TODOS_METADATA_KEY`, `TODO_WRITE_COUNTS_METADATA_KEY`.
|
|
486
|
-
- **
|
|
505
|
+
- **Metadata accessors** — `getTodosForRun(session, runId)`, `setTodosForRun(session, runId, items)`, `getArchivedTodosForRun(session, runId)`, `pruneTodosByRun(session)`, `TODOS_METADATA_KEY`, `TODO_WRITE_COUNTS_METADATA_KEY`. All route through the keying rule above — top-level reads/writes the session slot, subagent reads/writes its own. The archive accessor reads `bag.archive` with the same routing.
|
|
506
|
+
- **UI selectors** (renderer-agnostic, used by the TUI's todo indicator + modal; reusable by any GUI shell):
|
|
507
|
+
- `useActiveTodos(session) → ActiveTodosState` — React hook. Recomputes on every parent re-render; selector is O(runs + todos) so memoization is unnecessary (and was previously incorrect — `Session.runs` is mutated in place by `completeRun` / `abortRun`).
|
|
508
|
+
- `selectActiveTodos(session) → ActiveTodosState` — pure selector, identical contract, no React.
|
|
509
|
+
- `pickActiveRunId(session) → runId | null` — "the run UI surfaces should reflect right now" (latest-running, falling back to most-recently-appended).
|
|
510
|
+
- `TODO_STATUS_GLYPHS: Record<TodoStatus, string>` — single source of truth for status icons across surfaces.
|
|
511
|
+
- **Types** — `TodoItem`, `TodoStatus` (`'pending' | 'in_progress' | 'completed' | 'cancelled'`), `TodosBag` (`{ session?, byRun?, archive? }`), `TodoTally`, `ActiveTodosState` (carries both `todos` and `archive` so UI surfaces can render the close-out batch after auto-clear), `CreateTodoToolsOptions`.
|
|
487
512
|
|
|
488
513
|
`CreateTodoToolsOptions`:
|
|
489
514
|
|
|
490
515
|
| Option | Default | Notes |
|
|
491
516
|
|---|---|---|
|
|
492
517
|
| `maxItems` | `100` | Per-call item cap. Excess items are truncated by the recursive validator and reported in the tool_result. |
|
|
493
|
-
| `maxWritesPerRun` | `
|
|
494
|
-
| `onMaxWrites` | `'steer'` | `'steer'` or `'block'`. |
|
|
495
|
-
| `remindAfter` | `3` | Append a "you've checkpointed N times — slow down" nudge to the tool_result once the
|
|
518
|
+
| `maxWritesPerRun` | `0` | Per-run write cap. **Off by default** — `todowrite` is a state-transition tool, capping it punishes the legitimate "finish the N-item plan" path (which needs ≥ N+1 calls). Set to a positive `N` to opt in; the factory then plumbs `behavior.toolBudgets.todowrite = { max: N, onExceed }`. |
|
|
519
|
+
| `onMaxWrites` | `'steer'` | `'steer'` or `'block'`. Only takes effect when `maxWritesPerRun > 0`. |
|
|
520
|
+
| `remindAfter` | `3` | Append a "you've checkpointed N times — slow down" nudge to the tool_result once the cumulative count reaches this. Count is scoped the same way the data is (session-shared for top-level runs, per-run for subagents) so it reflects use of the *active* slot, not noisy per-prompt resets. Set to `0` to opt out. |
|
|
496
521
|
| `reminderText` | built-in | `(count, items) => string \| undefined`. Return `undefined` / `''` to suppress for this call. |
|
|
497
|
-
| `dedupIdentical` | `true` |
|
|
522
|
+
| `dedupIdentical` | `true` | Short-circuits the tool body when the incoming payload is identical to the current slot (session-shared for top-level, per-run for subagents). Does NOT plumb through `behavior.dedupTools` — that cache lives at the gate, is keyed per session, and would conflate top-level + subagent re-writes. Counter still advances on no-op re-writes so the reminder catches spam. Set `false` to skip the check entirely. |
|
|
498
523
|
| `writeDescription` / `readDescription` | built-in | Override the JSON-schema-level descriptions. |
|
|
499
524
|
|
|
500
525
|
`BUILD_AGENT` opts in via `composePresets`; `PLAN_AGENT` doesn't (read-only mode has nothing to checkpoint). Hosts building custom profiles use the same primitive:
|
|
@@ -509,11 +534,11 @@ const myPreset = composePresets(
|
|
|
509
534
|
tools: { ...basicTools },
|
|
510
535
|
behavior: { ...SHARED_BEHAVIOR },
|
|
511
536
|
}),
|
|
512
|
-
createTodoTools({
|
|
537
|
+
createTodoTools({ remindAfter: 3 }),
|
|
513
538
|
)
|
|
514
539
|
```
|
|
515
540
|
|
|
516
|
-
`composePresets` deep-merges `behavior.dedupTools` and `behavior.toolBudgets` per tool name, so layering order is forgiving: a downstream preset overriding `
|
|
541
|
+
`composePresets` deep-merges `behavior.dedupTools` and `behavior.toolBudgets` per tool name, so layering order is forgiving: a downstream preset overriding `toolBudgets.todowrite` keeps the rest of the factory's wiring intact, and a host wiring its own `dedupTools.todowrite` entry (for telemetry or alternate caching) survives composition. For the simplest case ("just add the tools to an existing config"), plain spread is also valid since the factory returns a `Preset`:
|
|
517
542
|
|
|
518
543
|
```ts
|
|
519
544
|
createAgent({ ...basicPreset, ...createTodoTools(), provider })
|
|
@@ -524,13 +549,28 @@ Spread is shallow-merge, so two presets that both touch `dedupTools` / `toolBudg
|
|
|
524
549
|
Hygiene is layered across the lifecycle:
|
|
525
550
|
|
|
526
551
|
- **Input time** — schema validation drops malformed items; per-call ID dedup keeps last-wins semantics; empty initial writes don't create a slot.
|
|
527
|
-
- **
|
|
528
|
-
- **Per-
|
|
529
|
-
- **
|
|
530
|
-
- **
|
|
552
|
+
- **Auto-clear** — when every item is `completed`, the live slot is wiped on write. Prevents the "all done" list from sitting in the model's context and shadowing the next prompt. The close-out list lands in `bag.archive` so the TUI modal can keep showing it until the model writes a fresh batch.
|
|
553
|
+
- **Per-call reminder** — rides on the tool's own `tool_result`, so the model sees the nudge in the result it just received (no `system:transform` plumbing). Counter is scoped to the active slot (session-shared / per-run).
|
|
554
|
+
- **Per-run write budget (opt-in)** — when the host sets `maxWritesPerRun > 0`, plumbed through `behavior.toolBudgets.todowrite`; same gate, same `tool-budget:exceeded` event. Counts every dispatched call including identical re-writes. Off by default — see the table above for the rationale.
|
|
555
|
+
- **Identical-payload dedup** — handled inside the tool body, against the current slot. The slot resolution naturally splits top-level from subagent, so an identical re-write in the active scope short-circuits to "No change" and a structurally identical write in a different scope (different subagent) writes correctly. Deliberately NOT wired through `behavior.dedupTools` (session-scoped cache would conflate the two).
|
|
556
|
+
- **Session-level reconciliation** — `pruneTodosByRun(session)` drops orphan subagent slots after a `setRuns()` fork or any out-of-band run-list mutation. The top-level `session` slot is unaffected (it's session-scoped, not per-run, so there's no orphan to GC). Not automatic on `save()`.
|
|
557
|
+
- **Subagent isolation** — falls out of `parentRunId`-keyed routing in `getTodosForRun` / `setTodosForRun`; each child run owns its own slot.
|
|
531
558
|
|
|
532
559
|
Renderer integration: the `TOOL_DISPLAY` registry ships entries for both tools (`Todos N items · …` line shape that matches the tool's result summary), and `DEFAULT_PERSIST_EXCLUDE_TOOLS` skips them from disk-persistence (the latest snapshot is the only state of interest).
|
|
533
560
|
|
|
561
|
+
### UI surface — opt-in indicator + always-on modal
|
|
562
|
+
|
|
563
|
+
The chat layer ships two renderer-agnostic primitives any host can build a UI on. The TUI wires both (see **TUI.md**); a GUI shell would reuse `useActiveTodos` + `TODO_STATUS_GLYPHS` and draw its own chrome.
|
|
564
|
+
|
|
565
|
+
- **Inline indicator.** Single-line "▸ <in-progress content>" badge to render near the prompt input. Show iff `selectActiveTodos(session).inProgress` is non-null. Gated on `Settings.showTodoIndicator` (default `true`). When the active run has no `in_progress` item the indicator returns null — never a placeholder, never visual noise.
|
|
566
|
+
- **Todos modal.** Read-only viewer for the active run's slot. Renders the per-status tally header + a scrollable list of rows (`TODO_STATUS_GLYPHS[status]` + content). No interactive controls — the model is the only writer. The host wires it behind the `openTodos` keybinding action (default `ctrl+t` — see **Keybindings** below); the modal closes on `esc`. Resolves the active run via `pickActiveRunId` (latest running, fallback most-recently-appended) so a child subagent's slot surfaces when it's the live run.
|
|
567
|
+
|
|
568
|
+
The hook recomputes on every parent re-render (cheap O(runs + todos) selector) so a `setTodosForRun` write — whether to the session slot or a subagent's — lands in the indicator and modal on the next paint without any memoization-cache invalidation gymnastics.
|
|
569
|
+
|
|
570
|
+
For surfaces that don't sit on the streaming-buffer cascade — typically modal trees rendered above the chat shell — the host should drive an explicit re-render by subscribing to the agent's `tool:after` hook filtered to `TODOWRITE_TOOL` (the hook payload's `name` field). The TUI's `TodosModal` does this so it stays live while open; the indicator gets it for free because it sits inside `ChatScreen` and rides the existing event cascade.
|
|
571
|
+
|
|
572
|
+
`Settings.showTodoIndicator` (boolean, default `true`) hides only the inline indicator. The modal stays accessible regardless — opening it is an explicit user gesture.
|
|
573
|
+
|
|
534
574
|
## Interactions
|
|
535
575
|
|
|
536
576
|
`present_plan` and `ask_user` let the agent pause for explicit user input. **The call IS the persisted state** — both tools land in `session.turns` as regular `tool_call` blocks, and pending entries are derived from disk via `pendingInteractionsFromTurns(turns)`.
|
|
@@ -559,7 +599,7 @@ A user-overridable action catalog. Defaults declared in `KEYBINDING_DEFS`; user
|
|
|
559
599
|
```ts
|
|
560
600
|
type KeyAction
|
|
561
601
|
= | 'openSettings' | 'openSessionDetails' | 'openModelPicker' | 'openEffortPicker'
|
|
562
|
-
| 'cycleAgent' | 'enterSelectTurnMode'
|
|
602
|
+
| 'openTodos' | 'cycleAgent' | 'enterSelectTurnMode'
|
|
563
603
|
| 'enterQueueSelection' | 'pushQueuedMessage' | 'dropQueuedMessage'
|
|
564
604
|
| 'turnFork' | 'turnDelete' | 'turnCopy'
|
|
565
605
|
| 'sessionDelete' | 'sessionCopyId' | 'sessionGenerateTitle'
|
package/docs/SKILL.md
CHANGED
|
@@ -33,9 +33,9 @@ const agent = createAgent({
|
|
|
33
33
|
cache: true, // prompt-cache breakpoints
|
|
34
34
|
toolOutputBudget: 32768, // off by default
|
|
35
35
|
dedupReads: true, // re-read dedup
|
|
36
|
-
dedupTools: {
|
|
36
|
+
dedupTools: { execute_sql: i => typeof i.query === 'string' ? i.query.trim() : undefined },
|
|
37
37
|
requireReadBeforeEdit: false,
|
|
38
|
-
toolBudgets: {
|
|
38
|
+
toolBudgets: { execute_sql: { max: 20, onExceed: 'steer' } },
|
|
39
39
|
compactStrategy: 'off', // 'off' | 'tail' (non-Anthropic compaction)
|
|
40
40
|
compactThreshold: 131_072, // 128 KiB
|
|
41
41
|
compactKeepTurns: 4,
|
package/docs/TUI.md
CHANGED
|
@@ -193,6 +193,7 @@ Every customizable shortcut routes through `<userDir>/keybindings.json` and is p
|
|
|
193
193
|
| `ctrl+m` | `openModelPicker` | chat (idle) | open cross-provider model picker |
|
|
194
194
|
| `ctrl+l` | `openEffortPicker` | chat (idle, model has reasoning) | open reasoning-effort picker |
|
|
195
195
|
| `ctrl+s` | `enterSelectTurnMode` | chat (idle) | enter select-turn mode (transcript navigation) |
|
|
196
|
+
| `ctrl+t` | `openTodos` | chat (session attached) | open the active run's `todowrite` checkpoints (read-only) |
|
|
196
197
|
| `shift+tab` | `cycleAgent` | chat (idle, ≥2 profiles) | cycle to next agent profile |
|
|
197
198
|
| `ctrl+↵` | `pushQueuedMessage` | queue-selection mode | push selected queued message into the live run |
|
|
198
199
|
| `backspace` | `dropQueuedMessage` | queue-selection mode | drop selected queued message |
|
|
@@ -305,6 +306,23 @@ File-edit tools (`edit` / `multi_edit` / `write_file`) get their own approval su
|
|
|
305
306
|
|
|
306
307
|
`isFileEditTool(tool)` (exported from `file-edit-approval-modal.tsx`) is the gate routing predicate. The set is `{'edit', 'multi_edit', 'write_file'}`; everything else stays on the inline `ApprovalBlock` path. For a fully-denied file-edit call, `applyGate` skips the substitute path entirely — sets `ctx.block = true` + emits a synthetic `tool-result` event with body `[fully denied] <edit-outcomes>…</edit-outcomes>` for live display. The persisted result stays the terse `Blocked: User denied this tool call` the harness writes.
|
|
307
308
|
|
|
309
|
+
### Todos surface — indicator + modal
|
|
310
|
+
|
|
311
|
+
Two thin chrome layers on top of the renderer-agnostic `useActiveTodos` hook (see [Todos in CHAT.md](./CHAT.md#todos) for the data contract — the keying rule, the auto-clear behavior, and the legacy-bag migration). Both surfaces share `TODO_STATUS_GLYPHS` so a row in the modal and the inline indicator read as the same visual language.
|
|
312
|
+
|
|
313
|
+
- **`TodoIndicator`** (`src/tui/todo-indicator.tsx`) — single-line `◐ <in-progress content>` badge mounted between the queue block and the prompt in `ChatScreen`. Hides itself (returns `null`) when there's no session, no active run, no `in_progress` item, the terminal is too narrow to render a meaningful tail, or `Settings.showTodoIndicator` is off. Read-only — not focusable, not clickable. Truncates multi-line / oversized content to one visual line so the chat zone stays calm.
|
|
314
|
+
- **`TodosModal`** (`src/tui/todos-modal.tsx`) — opened via `ctrl+t` (`openTodos`), closes on `esc`. Renders the per-status tally header (`3 completed · 1 in progress · 2 pending`) plus a scrollable list of rows. A right-aligned top-border badge mirrors the live counts (`N in progress · M completed · …`) — populated via the new `Modal.rightTitle` slot (absolutely-positioned sibling that rides the top border, same scissor-rect trick as `TitleOverlay` / file-edit modal). Falls back to the archived "last batch" snapshot when the live list is empty (e.g. after the model marked everything `completed` and the slot auto-cleared) with a subtle `· last batch ·` banner so the user can still see what was just finished. Auto-scrolls the first live `in_progress` row into view on open. Capped at ~two-thirds of the terminal height with a floor (`12`) and ceiling (`36`) so the modal never overpowers the transcript. Layout containment matches the file-edit modal — `flexGrow: 1` on the scrollbox + `overflow: 'hidden'` on the panel — so a long list scrolls cleanly without painting over the action footer. The modal subscribes to the agent's `tool:after` hook (filtered to `todowrite`) so it reflects checkpoints written while it's open — `<ModalRoot>` sits above `<AppShell>` in the React tree and doesn't re-render on the streaming-buffer cascade, so the explicit subscription is what drives reactivity.
|
|
315
|
+
|
|
316
|
+
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.
|
|
317
|
+
|
|
318
|
+
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).
|
|
319
|
+
|
|
320
|
+
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.
|
|
321
|
+
|
|
322
|
+
`ChatScreen` forwards the live `Session` reference through a separate `liveSession` prop (distinct from the lightweight `SessionMeta` snapshot) because the hook reads `session.metadata.todos` directly; the snapshot path would miss mutations that don't change message identity. The reference stays stable across activations of the same session — re-renders are driven by the existing `events` cascade on every tool result.
|
|
323
|
+
|
|
324
|
+
`Settings.showTodoIndicator` (default `true`) hides only the inline indicator. The modal is always accessible — opening it is an explicit user gesture.
|
|
325
|
+
|
|
308
326
|
## Settings rows
|
|
309
327
|
|
|
310
328
|
The Settings modal is tabbed. The **General** tab renders three row kinds from the chat-level tables; **Skills** and **MCP servers** render the discovered catalogs as enable/disable checklists.
|
|
@@ -323,7 +341,7 @@ A single shared search input filters the active tab's list (`label + description
|
|
|
323
341
|
|
|
324
342
|
For the live source of truth, see `SETTINGS_TOGGLES` / `SETTINGS_CHOICES` in `src/chat/settings-context.tsx`. The current shape includes:
|
|
325
343
|
|
|
326
|
-
- **Toggles** — `safeMode`, `allowInteraction`, `resumeLastSession`, `hideSubagentOutput`, `persistToolResults`, `autoCompact`, `showAllProjects`, `showThinking`, `showToolResults`, `showEditDiffs`, `smoothStreaming`.
|
|
344
|
+
- **Toggles** — `safeMode`, `allowInteraction`, `resumeLastSession`, `hideSubagentOutput`, `persistToolResults`, `autoCompact`, `showAllProjects`, `showThinking`, `showToolResults`, `showEditDiffs`, `smoothStreaming`, `showTodoIndicator`, `showThrobber`.
|
|
327
345
|
- **Choices** — `toolCallDisplay` (Formatted / Full / Hidden), `autoCompactThreshold` (60% / 70% / 80% / 90%), `theme` (every entry in `BUILTIN_THEMES`), `targetFps` (30 / 60 / 120).
|
|
328
346
|
|
|
329
347
|
`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.
|
|
@@ -488,11 +506,13 @@ src/tui/
|
|
|
488
506
|
session-details-modal.tsx Stats + delete / export / title / compact (ctrl+x)
|
|
489
507
|
turn-details-modal.tsx Fork / delete / copy (opened from select-turn mode)
|
|
490
508
|
file-edit-approval-modal.tsx FileEditApprovalModal + SingleEditApprovalModal + MultiEditApprovalModal + UnresolvedHunkPanel + `originatorSuffix` (` · child-N` attribution). Inline-mounted in ChatScreen's transcript slot, keyed on `request.id` for force-remount on queue advance — bridges ApprovalDecision (including { kind: 'partial', mask }) to the gate via the helpers in `zidane/chat`'s `edit-approval` module.
|
|
509
|
+
todo-indicator.tsx Single-line "in progress todo" badge between the queue block and the prompt (gated on `Settings.showTodoIndicator`; hides itself when nothing's in progress)
|
|
510
|
+
todos-modal.tsx Read-only viewer for the active todowrite list (ctrl+t). Resolves the session-shared slot for top-level runs and the per-run slot for subagents; falls back to the archive snapshot when the live slot is empty so a just-completed batch stays visible — see CHAT.md Todos for the keying + archive contracts.
|
|
491
511
|
interaction-block.tsx InteractionBlock + plan picker + question wizard
|
|
492
512
|
toggle-list-modal.tsx Generic checkbox-list modal (ToggleListModal)
|
|
493
513
|
completion-popup.tsx Provider-agnostic autocomplete popover
|
|
494
514
|
discovery-shell.tsx DiscoveryShell — wires DiscoveryProvider from `zidane/chat` with the three SWR slots (files / skills / mcps)
|
|
495
|
-
crush-throbber.tsx CrushThrobber — gradient activity glyph for the thinking affordance
|
|
515
|
+
crush-throbber.tsx CrushThrobber — gradient activity glyph for the thinking affordance (gated on `Settings.showThrobber`, off by default)
|
|
496
516
|
clipboard.ts OSC 52 writer used by the detail modals
|
|
497
517
|
theme.ts buildMdStyle, useMdStyle, MdStyleProvider, ChipStyleProvider, useChipStyle, useChipHighlights
|
|
498
518
|
tree-sitter.ts Extra grammar registration (registerTreeSitterParsers + initTreeSitterWorker + setupTreeSitter)
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"presets-AgF0RFx1.js","names":[],"sources":["../src/presets/basic.ts","../src/presets/index.ts"],"sourcesContent":["import { definePreset } from '.'\nimport { edit, listFiles, multiEdit, readFile, shell, writeFile } from '../tools'\nimport { createSpawnTool } from '../tools/spawn'\n\n/**\n * Core tools available in every basic preset (without spawn).\n *\n * `edit` and `multi_edit` ship in the basic set because surgical edits are the\n * default modality for production agents — `write_file` is for full overwrites.\n * `glob` and `grep` are exported but opt-in: not every agent needs codebase\n * search, and shipping them by default would force `tool:gate` work onto\n * consumers that prefer the model to use `shell` + classic Unix tools.\n */\nexport const basicTools = { shell, readFile, writeFile, listFiles, edit, multiEdit }\n\nexport default definePreset({\n name: 'basic',\n system: 'You are a helpful assistant with access to shell, file reading, file writing, surgical and multi-edit tools, directory listing, and sub-agent spawning. Prefer `edit` / `multi_edit` for in-place changes and `write_file` for full file overwrites. Use them to accomplish tasks in the project directory.',\n // `persist: true` shares the parent's session with every child agent — child\n // turns land in `session.turns` tagged with their own `runId`, and the run\n // itself is recorded in `session.runs` with `parentRunId` + `depth`. That's\n // what lets a reloaded TUI session reconstruct the full subagent tree (see\n // `eventsFromTurns` in `tui/store.ts`). Hosts that want children in-memory\n // only can construct their own preset with `createSpawnTool()`.\n tools: { ...basicTools, spawn: createSpawnTool({ persist: true }) },\n})\n","import type { AgentHooks, AgentOptions } from '../agent'\n\nexport type { AgentHookMap } from '../agent'\n\n/**\n * A preset is a reusable slice of `AgentOptions` — spread it into `createAgent()`\n * to configure tools, a default system prompt, aliases, behavior defaults, and\n * agent-lifetime hooks.\n *\n * `provider`, `execution`, `session`, and internal fields are excluded so presets\n * remain shareable and composable.\n *\n * ```ts\n * import { basic } from 'zidane/presets'\n * createAgent({ ...basic, provider })\n * ```\n *\n * ### Composing multiple presets\n *\n * Bare `...spread` is shallow — `{ ...a, ...b }` overwrites every key `b`\n * defines, including `hooks`. Use {@link composePresets} when you want\n * field-aware merging (per-event hook concat, tools shallow-merge, etc.):\n *\n * ```ts\n * createAgent({ ...composePresets(basic, telemetry, mine), provider })\n * ```\n */\nexport type Preset = Omit<Partial<AgentOptions>, 'provider' | 'execution' | 'session' | 'mcpConnector'>\n\n/**\n * Identity helper for type inference when defining a preset.\n */\nexport function definePreset(config: Preset): Preset {\n return config\n}\n\n/**\n * Field-aware composition of presets. Right-most preset wins for scalar fields;\n * objects shallow-merge; arrays and hook handler lists concatenate. Designed so\n * stacking presets does the obvious thing without the spread-collision footgun:\n *\n * - `name`, `system`, `eager`, `skills` → last-defined wins\n * - `tools`, `toolAliases`, `behavior` → shallow-merge (later keys override)\n * - `behavior.dedupTools`, `behavior.toolBudgets` → **deep-merge** (per-tool-name; later wins on collision)\n * - `mcpServers` → concat with last-wins on `name` collision\n * - `hooks` → per-event concat; every handler fires\n *\n * `hooks` always emerges as `event → handler[]` so downstream registration\n * (in `createAgent`) sees a uniform shape. Order of handlers within an event\n * follows preset order: earlier presets register first.\n *\n * `mcpServers` is deduped by `name` because shipping two servers with the same\n * name would trip the connector at runtime — a later preset overriding an\n * earlier preset's `github` server is the practical intent.\n *\n * `behavior.dedupTools` and `behavior.toolBudgets` get the same per-key deep-merge\n * because they are tool-name-keyed records — a preset that ships a dedup hasher\n * for one tool should not erase a hasher another preset ships for a different\n * tool. Last-wins still applies on a per-tool collision so a downstream preset\n * can override an upstream preset's policy for one specific tool. Other\n * `behavior` fields keep last-wins semantics.\n */\nexport function composePresets(...presets: Preset[]): Preset {\n const out: Preset = {}\n const hooksByEvent: { [K in keyof AgentHooks]?: AgentHooks[K][] } = {}\n // Keep mcpServers in source-order on first sight, but allow later\n // declarations to override earlier ones with the same `name`. A `Map`\n // keyed by name gives O(1) override + stable iteration.\n const mcpByName = new Map<string, NonNullable<Preset['mcpServers']>[number]>()\n\n for (const p of presets) {\n if (p.name !== undefined)\n out.name = p.name\n if (p.system !== undefined)\n out.system = p.system\n if (p.eager !== undefined)\n out.eager = p.eager\n if (p.skills !== undefined)\n out.skills = p.skills\n if (p.tools)\n out.tools = { ...out.tools, ...p.tools }\n if (p.toolAliases)\n out.toolAliases = { ...out.toolAliases, ...p.toolAliases }\n if (p.behavior) {\n // Top-level shallow-merge first; then deep-merge the two tool-name-keyed\n // sub-records so per-tool entries from earlier presets aren't clobbered.\n const merged: NonNullable<Preset['behavior']> = { ...out.behavior, ...p.behavior }\n if (out.behavior?.dedupTools || p.behavior.dedupTools) {\n merged.dedupTools = { ...out.behavior?.dedupTools, ...p.behavior.dedupTools }\n }\n if (out.behavior?.toolBudgets || p.behavior.toolBudgets) {\n merged.toolBudgets = { ...out.behavior?.toolBudgets, ...p.behavior.toolBudgets }\n }\n out.behavior = merged\n }\n if (p.mcpServers) {\n for (const server of p.mcpServers)\n mcpByName.set(server.name, server)\n }\n if (p.hooks) {\n for (const [event, handler] of Object.entries(p.hooks)) {\n if (handler === undefined)\n continue\n const list = Array.isArray(handler) ? handler : [handler]\n const key = event as keyof AgentHooks\n // Safe cast: we read the loose `AgentHookMap` shape (handler-or-array)\n // and re-emit only as arrays. Each `list` element matches the event's\n // handler signature by construction (the input was typed `AgentHookMap`).\n const bucket = (hooksByEvent[key] ??= []) as unknown[]\n bucket.push(...(list as unknown[]))\n }\n }\n }\n\n if (mcpByName.size > 0)\n out.mcpServers = [...mcpByName.values()]\n\n if (Object.keys(hooksByEvent).length > 0)\n out.hooks = hooksByEvent\n\n return out\n}\n\nexport { default as basic, basicTools } from './basic'\n"],"mappings":";;;;;;;;;;;AAaA,MAAa,aAAa;CAAE;CAAO;CAAU;CAAW;CAAW;CAAM;CAAW;AAEpF,IAAA,gBAAe,aAAa;CAC1B,MAAM;CACN,QAAQ;CAOR,OAAO;EAAE,GAAG;EAAY,OAAO,gBAAgB,EAAE,SAAS,MAAM,CAAC;EAAE;CACpE,CAAC;;;;;;ACOF,SAAgB,aAAa,QAAwB;CACnD,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BT,SAAgB,eAAe,GAAG,SAA2B;CAC3D,MAAM,MAAc,EAAE;CACtB,MAAM,eAA8D,EAAE;CAItE,MAAM,4BAAY,IAAI,KAAwD;CAE9E,KAAK,MAAM,KAAK,SAAS;EACvB,IAAI,EAAE,SAAS,KAAA,GACb,IAAI,OAAO,EAAE;EACf,IAAI,EAAE,WAAW,KAAA,GACf,IAAI,SAAS,EAAE;EACjB,IAAI,EAAE,UAAU,KAAA,GACd,IAAI,QAAQ,EAAE;EAChB,IAAI,EAAE,WAAW,KAAA,GACf,IAAI,SAAS,EAAE;EACjB,IAAI,EAAE,OACJ,IAAI,QAAQ;GAAE,GAAG,IAAI;GAAO,GAAG,EAAE;GAAO;EAC1C,IAAI,EAAE,aACJ,IAAI,cAAc;GAAE,GAAG,IAAI;GAAa,GAAG,EAAE;GAAa;EAC5D,IAAI,EAAE,UAAU;GAGd,MAAM,SAA0C;IAAE,GAAG,IAAI;IAAU,GAAG,EAAE;IAAU;GAClF,IAAI,IAAI,UAAU,cAAc,EAAE,SAAS,YACzC,OAAO,aAAa;IAAE,GAAG,IAAI,UAAU;IAAY,GAAG,EAAE,SAAS;IAAY;GAE/E,IAAI,IAAI,UAAU,eAAe,EAAE,SAAS,aAC1C,OAAO,cAAc;IAAE,GAAG,IAAI,UAAU;IAAa,GAAG,EAAE,SAAS;IAAa;GAElF,IAAI,WAAW;;EAEjB,IAAI,EAAE,YACJ,KAAK,MAAM,UAAU,EAAE,YACrB,UAAU,IAAI,OAAO,MAAM,OAAO;EAEtC,IAAI,EAAE,OACJ,KAAK,MAAM,CAAC,OAAO,YAAY,OAAO,QAAQ,EAAE,MAAM,EAAE;GACtD,IAAI,YAAY,KAAA,GACd;GACF,MAAM,OAAO,MAAM,QAAQ,QAAQ,GAAG,UAAU,CAAC,QAAQ;GACzD,MAAM,MAAM;GAKZ,CADgB,aAAa,SAAS,EAAE,EACjC,KAAK,GAAI,KAAmB;;;CAKzC,IAAI,UAAU,OAAO,GACnB,IAAI,aAAa,CAAC,GAAG,UAAU,QAAQ,CAAC;CAE1C,IAAI,OAAO,KAAK,aAAa,CAAC,SAAS,GACrC,IAAI,QAAQ;CAEd,OAAO"}
|