zidane 4.1.8 → 4.1.9

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 (59) hide show
  1. package/dist/{index-bgh-k8Mv.d.ts → agent-CMIhYhDz.d.ts} +2032 -1993
  2. package/dist/agent-CMIhYhDz.d.ts.map +1 -0
  3. package/dist/chat.d.ts +7 -6
  4. package/dist/chat.d.ts.map +1 -1
  5. package/dist/chat.js +2 -2
  6. package/dist/contexts.d.ts +1 -1
  7. package/dist/{index-BB4kuRh3.d.ts → index-CXVvqTQj.d.ts} +1 -1
  8. package/dist/{index-BB4kuRh3.d.ts.map → index-CXVvqTQj.d.ts.map} +1 -1
  9. package/dist/{index-Ds5YpvfZ.d.ts → index-D6Dd6Kc0.d.ts} +3 -3
  10. package/dist/{index-Ds5YpvfZ.d.ts.map → index-D6Dd6Kc0.d.ts.map} +1 -1
  11. package/dist/{index-DRoG_udt.d.ts → index-DAaKyadO.d.ts} +2 -2
  12. package/dist/{index-DRoG_udt.d.ts.map → index-DAaKyadO.d.ts.map} +1 -1
  13. package/dist/index.d.ts +4 -4
  14. package/dist/index.js +6 -6
  15. package/dist/{interpolate-CukJwP2G.js → interpolate-BydkV1eT.js} +3 -1
  16. package/dist/interpolate-BydkV1eT.js.map +1 -0
  17. package/dist/{mcp-8wClKY-3.js → mcp-Dw-fRPVk.js} +61 -65
  18. package/dist/mcp-Dw-fRPVk.js.map +1 -0
  19. package/dist/mcp.d.ts +1 -1
  20. package/dist/mcp.js +1 -1
  21. package/dist/{presets-BzkJDW1K.js → presets-4zCJzCYw.js} +2 -2
  22. package/dist/{presets-BzkJDW1K.js.map → presets-4zCJzCYw.js.map} +1 -1
  23. package/dist/presets.d.ts +1 -1
  24. package/dist/presets.js +1 -1
  25. package/dist/providers.d.ts +1 -1
  26. package/dist/session/sqlite.d.ts +13 -2
  27. package/dist/session/sqlite.d.ts.map +1 -1
  28. package/dist/session/sqlite.js +70 -27
  29. package/dist/session/sqlite.js.map +1 -1
  30. package/dist/{session-Cn68UASv.js → session-B1RN0uoi.js} +42 -30
  31. package/dist/{session-Cn68UASv.js.map → session-B1RN0uoi.js.map} +1 -1
  32. package/dist/session.d.ts +1 -1
  33. package/dist/session.js +1 -1
  34. package/dist/skills.d.ts +2 -2
  35. package/dist/skills.js +1 -1
  36. package/dist/{stats-BT9l57RS.js → stats-DZIsGqzu.js} +15 -5
  37. package/dist/stats-DZIsGqzu.js.map +1 -0
  38. package/dist/{theme-BlXO6yHe.d.ts → theme-Caf4AvTO.d.ts} +147 -13
  39. package/dist/theme-Caf4AvTO.d.ts.map +1 -0
  40. package/dist/{theme-context-MungM3SY.js → theme-context-DQM2lx4U.js} +212 -72
  41. package/dist/theme-context-DQM2lx4U.js.map +1 -0
  42. package/dist/{tools-C8kDot0H.js → tools-BdQENveS.js} +409 -312
  43. package/dist/tools-BdQENveS.js.map +1 -0
  44. package/dist/tools.d.ts +2 -2
  45. package/dist/tools.js +1 -1
  46. package/dist/tui.d.ts +64 -7
  47. package/dist/tui.d.ts.map +1 -1
  48. package/dist/tui.js +481 -143
  49. package/dist/tui.js.map +1 -1
  50. package/dist/types.d.ts +3 -3
  51. package/dist/types.js +1 -1
  52. package/package.json +6 -1
  53. package/dist/index-bgh-k8Mv.d.ts.map +0 -1
  54. package/dist/interpolate-CukJwP2G.js.map +0 -1
  55. package/dist/mcp-8wClKY-3.js.map +0 -1
  56. package/dist/stats-BT9l57RS.js.map +0 -1
  57. package/dist/theme-BlXO6yHe.d.ts.map +0 -1
  58. package/dist/theme-context-MungM3SY.js.map +0 -1
  59. package/dist/tools-C8kDot0H.js.map +0 -1
package/dist/tui.js CHANGED
@@ -1,11 +1,211 @@
1
- import { d as createAgent } from "./tools-C8kDot0H.js";
2
- import { n as formatTokenUsage } from "./stats-BT9l57RS.js";
3
- import { n as loadSession, t as createSession } from "./session-Cn68UASv.js";
4
- import { $ as toolCallPreview, A as isOnSafelist, B as shortId, E as useSafeModeQueue, H as useConfig, I as runOAuthLogin, J as listSessionMeta, K as eventsFromTurns, L as supportsOAuth, O as addToSafelist, P as suggestSafelistEntry, Q as titleFromTurns, R as ageString, T as useSafeModeActions, U as resolveConfig, V as ConfigProvider, Z as stripSpawnTokensLine, c as finalizeStreamingMarkdownForOwner, d as DEFAULT_SETTINGS, et as toolResultText, f as SETTINGS_CHOICES, h as useSettings, i as useSurfaces, k as getSafelist, l as turnContextSize, m as SettingsProvider, n as useColors, o as useTheme, p as SETTINGS_TOGGLES, pt as getContextWindow, q as lastContextSizeFromTurns, r as useSelectStyle, s as finalizeStreamingMarkdown, st as setProviderCredential, t as ThemeProvider, tt as detectAuth, u as useStreamBuffer, v as resolveTheme, w as SafeModeProvider, z as fmtTokens } from "./theme-context-MungM3SY.js";
5
- import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
6
- import { jsx, jsxs } from "@opentui/react/jsx-runtime";
1
+ import { d as createAgent } from "./tools-BdQENveS.js";
2
+ import { n as formatTokenUsage } from "./stats-DZIsGqzu.js";
3
+ import { n as loadSession, t as createSession } from "./session-B1RN0uoi.js";
4
+ import { createTuiStore } from "./session/sqlite.js";
5
+ import { $ as toolResultText, A as isOnSafelist, B as shortId, E as useSafeModeQueue, G as eventsFromTurns, H as useConfig, I as runOAuthLogin, K as lastContextSizeFromTurns, L as supportsOAuth, O as addToSafelist, P as suggestSafelistEntry, Q as toolCallPreview, R as ageString, T as useSafeModeActions, U as resolveConfig, V as ConfigProvider, X as stripSpawnTokensLine, Z as titleFromTurns, c as finalizeStreamingMarkdownForOwner, d as DEFAULT_SETTINGS, et as detectAuth, f as SETTINGS_CHOICES, ft as getContextWindow, h as useSettings, i as useSurfaces, k as getSafelist, l as turnContextSize, m as SettingsProvider, n as useColors, o as useTheme, ot as setProviderCredential, p as SETTINGS_TOGGLES, q as listSessionMeta, r as useSelectStyle, s as finalizeStreamingMarkdown, t as ThemeProvider, u as useStreamBuffer, v as resolveTheme, w as SafeModeProvider, z as fmtTokens } from "./theme-context-DQM2lx4U.js";
6
+ import { createContext, createElement, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
7
7
  import { RGBA, SyntaxStyle, addDefaultParsers, createCliRenderer, defaultTextareaKeyBindings, getTreeSitterClient } from "@opentui/core";
8
8
  import { createRoot, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react";
9
+ import { jsx, jsxs } from "@opentui/react/jsx-runtime";
10
+ //#region src/tui/modal.tsx
11
+ const ModalContext = createContext(null);
12
+ function ModalRoot({ children }) {
13
+ const [active, setActive] = useState(null);
14
+ const api = useMemo(() => ({
15
+ open: (node) => setActive(node),
16
+ close: () => setActive(null),
17
+ get isOpen() {
18
+ return active !== null;
19
+ }
20
+ }), [active]);
21
+ return /* @__PURE__ */ jsxs(ModalContext.Provider, {
22
+ value: api,
23
+ children: [/* @__PURE__ */ jsx("box", {
24
+ style: {
25
+ flexDirection: "column",
26
+ flexGrow: 1
27
+ },
28
+ children
29
+ }), active && /* @__PURE__ */ jsx("box", {
30
+ style: {
31
+ position: "absolute",
32
+ top: 0,
33
+ left: 0,
34
+ right: 0,
35
+ bottom: 0,
36
+ alignItems: "center",
37
+ justifyContent: "center",
38
+ zIndex: 100
39
+ },
40
+ children: active
41
+ })]
42
+ });
43
+ }
44
+ function useModal() {
45
+ const ctx = useContext(ModalContext);
46
+ if (!ctx) throw new Error("useModal must be used inside <ModalRoot>");
47
+ return ctx;
48
+ }
49
+ /**
50
+ * Focus computed against the modal layer.
51
+ *
52
+ * Pass a component's preferred focus state and this returns `false` whenever a
53
+ * modal is open — so focused inputs (textarea, selects) release their focus and
54
+ * stop intercepting keys behind the overlay. Pair with `focusable={false}` on
55
+ * "passive" focusables (scrollbox) so the renderer doesn't cycle focus into
56
+ * them when the primary input blurs.
57
+ */
58
+ function useModalAwareFocus(preferred = true) {
59
+ const { isOpen } = useModal();
60
+ return preferred && !isOpen;
61
+ }
62
+ /**
63
+ * Responsive modal — picks a width based on the live terminal size.
64
+ *
65
+ * - On a wide terminal, the modal grows to `maxWidth` so descriptions sit on
66
+ * one line and don't wrap.
67
+ * - On a narrow terminal, the modal shrinks down to `minWidth`, keeping a
68
+ * small horizontal margin from the screen edges. Text inside wraps naturally.
69
+ *
70
+ * Uses `useTerminalDimensions()` so it reflows on `SIGWINCH` without remount.
71
+ */
72
+ function Modal({ title, onClose, children, maxWidth = 92, minWidth = 44, horizontalMargin = 4 }) {
73
+ const ctx = useContext(ModalContext);
74
+ const dismiss = onClose ?? ctx?.close;
75
+ const COLOR = useColors();
76
+ const SURFACE = useSurfaces();
77
+ useKeyboard((key) => {
78
+ if (key.name === "escape") dismiss?.();
79
+ });
80
+ const { width: termWidth } = useTerminalDimensions();
81
+ const width = Math.max(minWidth, Math.min(maxWidth, termWidth - horizontalMargin * 2));
82
+ return /* @__PURE__ */ jsx("box", {
83
+ title: title ? ` ${title} ` : void 0,
84
+ style: {
85
+ border: true,
86
+ borderColor: COLOR.borderActive,
87
+ backgroundColor: SURFACE.modal,
88
+ paddingTop: 1,
89
+ paddingBottom: 1,
90
+ paddingLeft: 2,
91
+ paddingRight: 2,
92
+ width,
93
+ flexDirection: "column",
94
+ gap: 1
95
+ },
96
+ children
97
+ });
98
+ }
99
+ //#endregion
100
+ //#region src/tui/agent-picker.tsx
101
+ /** Cap the scroll window — a long custom registry shouldn't push the modal off-screen. */
102
+ const VISIBLE_ROW_CAP$1 = 10;
103
+ /**
104
+ * Modal that lists the registered {@link AgentProfile}s and lets the user
105
+ * pick one. Rows show: `● selected · label description`.
106
+ *
107
+ * The accent column is intentionally compact (single-char marker) — the
108
+ * profile's `accent` color is read from the active theme so Build and Plan
109
+ * stand apart at a glance without taking horizontal space.
110
+ *
111
+ * Used by `App` (Ctrl+A binding) and exported for hosts that want to drive
112
+ * agent switching from elsewhere in their own composition.
113
+ */
114
+ function AgentPickerModal({ agents, currentAgentId, onPick }) {
115
+ const COLOR = useColors();
116
+ const SELECT_THEME = useSelectStyle();
117
+ const profiles = useMemo(() => Object.values(agents), [agents]);
118
+ const initialIndex = useMemo(() => profiles.findIndex((p) => p.id === currentAgentId), [profiles, currentAgentId]);
119
+ const options = useMemo(() => profiles.map((p) => ({
120
+ name: `${p.id === currentAgentId ? "● " : " "}${p.label}`,
121
+ description: p.description,
122
+ value: p.id
123
+ })), [profiles, currentAgentId]);
124
+ if (profiles.length === 0) return /* @__PURE__ */ jsx(EmptyState$2, {});
125
+ const visibleRows = Math.min(options.length, VISIBLE_ROW_CAP$1);
126
+ const currentMissing = initialIndex < 0;
127
+ const safeIndex = currentMissing ? 0 : initialIndex;
128
+ return /* @__PURE__ */ jsxs(Modal, {
129
+ title: "select agent",
130
+ children: [
131
+ currentMissing && /* @__PURE__ */ jsx("text", {
132
+ fg: COLOR.warn,
133
+ children: `Current agent "${currentAgentId}" is not in this registry — pick one below.`
134
+ }),
135
+ /* @__PURE__ */ jsx("select", {
136
+ ...SELECT_THEME,
137
+ focused: true,
138
+ options,
139
+ wrapSelection: true,
140
+ selectedIndex: safeIndex,
141
+ showScrollIndicator: options.length > visibleRows,
142
+ style: { height: visibleRows },
143
+ onSelect: (_idx, option) => {
144
+ if (option) onPick(option.value);
145
+ }
146
+ }),
147
+ /* @__PURE__ */ jsxs("text", {
148
+ fg: COLOR.mute,
149
+ children: [
150
+ /* @__PURE__ */ jsx("span", {
151
+ fg: COLOR.warn,
152
+ children: "↑↓"
153
+ }),
154
+ " navigate · ",
155
+ /* @__PURE__ */ jsx("span", {
156
+ fg: COLOR.warn,
157
+ children: "↵"
158
+ }),
159
+ " select · ",
160
+ /* @__PURE__ */ jsx("span", {
161
+ fg: COLOR.warn,
162
+ children: "esc"
163
+ }),
164
+ " close"
165
+ ]
166
+ })
167
+ ]
168
+ });
169
+ }
170
+ function EmptyState$2() {
171
+ const COLOR = useColors();
172
+ return /* @__PURE__ */ jsxs(Modal, {
173
+ title: "select agent",
174
+ children: [/* @__PURE__ */ jsx("text", {
175
+ fg: COLOR.dim,
176
+ children: "No agents registered."
177
+ }), /* @__PURE__ */ jsxs("text", {
178
+ fg: COLOR.mute,
179
+ children: [
180
+ "Pass an",
181
+ /* @__PURE__ */ jsx("span", {
182
+ fg: COLOR.model,
183
+ children: " agents "
184
+ }),
185
+ "registry to",
186
+ /* @__PURE__ */ jsx("span", {
187
+ fg: COLOR.model,
188
+ children: " runTui({ agents }) "
189
+ }),
190
+ "to populate this list."
191
+ ]
192
+ })]
193
+ });
194
+ }
195
+ /**
196
+ * Resolve a profile's `accent` token to a concrete theme color via the
197
+ * caller's color palette. Exposed for the Footer badge so all surfaces
198
+ * stay in sync with the picker's row tinting.
199
+ */
200
+ function accentColor(accent, COLOR) {
201
+ switch (accent) {
202
+ case "brand": return COLOR.brand;
203
+ case "warn": return COLOR.warn;
204
+ case "model": return COLOR.model;
205
+ default: return COLOR.accent;
206
+ }
207
+ }
208
+ //#endregion
9
209
  //#region src/tui/theme.ts
10
210
  /**
11
211
  * Convert the renderer-agnostic `Theme.syntax` map (hex strings + plain
@@ -29,14 +229,25 @@ function buildMdStyle(theme) {
29
229
  }
30
230
  return SyntaxStyle.fromStyles(styles);
31
231
  }
232
+ const MdStyleContext = createContext(null);
233
+ function MdStyleProvider({ children }) {
234
+ const theme = useTheme();
235
+ const style = useMemo(() => buildMdStyle(theme), [theme]);
236
+ return createElement(MdStyleContext.Provider, { value: style }, children);
237
+ }
32
238
  /**
33
- * Active markdown / syntax-highlighting style, memoized per theme. Reading
34
- * this in a component subscribes it to theme changes a `Settings.theme`
35
- * flip immediately re-renders the affected `<markdown>` instances.
239
+ * Active markdown / syntax-highlighting style. Returns a single shared
240
+ * `SyntaxStyle` instance for the active theme built once at provider
241
+ * mount, re-built on theme switch. A `Settings.theme` flip re-paints every
242
+ * `<markdown>` that reads this hook.
243
+ *
244
+ * Throws if used outside `<MdStyleProvider>` so a missing wiring shows up
245
+ * loudly in development rather than silently rendering plain text.
36
246
  */
37
247
  function useMdStyle() {
38
- const theme = useTheme();
39
- return useMemo(() => buildMdStyle(theme), [theme]);
248
+ const style = useContext(MdStyleContext);
249
+ if (!style) throw new Error("useMdStyle must be used inside <MdStyleProvider>");
250
+ return style;
40
251
  }
41
252
  //#endregion
42
253
  //#region src/tui/components.tsx
@@ -69,9 +280,27 @@ const EventLine = memo(({ event, previous, depthOffset = 0 }) => /* @__PURE__ */
69
280
  function onInputSubmit(handler) {
70
281
  return handler;
71
282
  }
72
- function Footer({ hints, picked, context }) {
73
- const COLOR = useColors();
74
- return /* @__PURE__ */ jsxs("box", {
283
+ /**
284
+ * Footer status bar. Renders as a single row when the terminal is wide enough,
285
+ * otherwise stacks into two rows (agent + hints + provider on top, context
286
+ * bottom-right) and falls back to one segment per row at terminal widths
287
+ * where even that overflows. The width tiering is driven by plain-text
288
+ * length estimates of each segment — close enough since the segments are
289
+ * ASCII-heavy and worst case we under-estimate the breakpoint by a few cells.
290
+ *
291
+ * Agent badge is **always rendered first** so the active profile (Build /
292
+ * Plan / host-custom) is the anchor on the left edge of every layout tier.
293
+ */
294
+ function Footer({ hints, picked, agent, context }) {
295
+ const { width } = useTerminalDimensions();
296
+ const inner = Math.max(0, width - 2);
297
+ const hW = hintsLength(hints);
298
+ const aW = agent ? agentBadgeLength(agent) : 0;
299
+ const pW = picked ? providerBadgeLength(picked) : 0;
300
+ const cW = context ? contextIndicatorLength(context) : 0;
301
+ const oneRowFits = aW + hW + pW + (cW > 0 ? cW + 1 : 0) <= inner;
302
+ const leftRowFits = aW + hW + pW <= inner;
303
+ if (oneRowFits) return /* @__PURE__ */ jsxs("box", {
75
304
  style: {
76
305
  flexDirection: "row",
77
306
  height: 1,
@@ -79,36 +308,154 @@ function Footer({ hints, picked, context }) {
79
308
  paddingRight: 1
80
309
  },
81
310
  children: [
82
- /* @__PURE__ */ jsx("text", {
83
- fg: COLOR.dim,
84
- children: hints.map((h, i) => /* @__PURE__ */ jsxs("span", { children: [
85
- i > 0 && /* @__PURE__ */ jsx("span", {
86
- fg: COLOR.mute,
87
- children: " · "
88
- }),
89
- /* @__PURE__ */ jsx("span", {
90
- fg: COLOR.warn,
91
- children: h.key
92
- }),
93
- /* @__PURE__ */ jsx("span", {
94
- fg: COLOR.dim,
95
- children: ` ${h.label}`
96
- })
97
- ] }, i))
311
+ agent && /* @__PURE__ */ jsx(AgentBadge, {
312
+ agent,
313
+ position: "leading"
98
314
  }),
315
+ /* @__PURE__ */ jsx(HintsText, { hints }),
99
316
  picked && /* @__PURE__ */ jsx(ProviderBadge, { picked }),
100
317
  /* @__PURE__ */ jsx("box", { style: { flexGrow: 1 } }),
101
318
  context && /* @__PURE__ */ jsx(ContextIndicator, { context })
102
319
  ]
103
320
  });
321
+ if (leftRowFits) return /* @__PURE__ */ jsxs("box", {
322
+ style: {
323
+ flexDirection: "column",
324
+ paddingLeft: 1,
325
+ paddingRight: 1
326
+ },
327
+ children: [/* @__PURE__ */ jsxs("box", {
328
+ style: {
329
+ flexDirection: "row",
330
+ height: 1
331
+ },
332
+ children: [
333
+ agent && /* @__PURE__ */ jsx(AgentBadge, {
334
+ agent,
335
+ position: "leading"
336
+ }),
337
+ /* @__PURE__ */ jsx(HintsText, { hints }),
338
+ picked && /* @__PURE__ */ jsx(ProviderBadge, { picked })
339
+ ]
340
+ }), context && /* @__PURE__ */ jsxs("box", {
341
+ style: {
342
+ flexDirection: "row",
343
+ height: 1
344
+ },
345
+ children: [/* @__PURE__ */ jsx("box", { style: { flexGrow: 1 } }), /* @__PURE__ */ jsx(ContextIndicator, { context })]
346
+ })]
347
+ });
348
+ return /* @__PURE__ */ jsxs("box", {
349
+ style: {
350
+ flexDirection: "column",
351
+ paddingLeft: 1,
352
+ paddingRight: 1
353
+ },
354
+ children: [
355
+ agent && /* @__PURE__ */ jsx("box", {
356
+ style: {
357
+ flexDirection: "row",
358
+ height: 1
359
+ },
360
+ children: /* @__PURE__ */ jsx(AgentBadge, {
361
+ agent,
362
+ position: "standalone"
363
+ })
364
+ }),
365
+ /* @__PURE__ */ jsx("box", {
366
+ style: {
367
+ flexDirection: "row",
368
+ height: 1
369
+ },
370
+ children: /* @__PURE__ */ jsx(HintsText, { hints })
371
+ }),
372
+ picked && /* @__PURE__ */ jsx("box", {
373
+ style: {
374
+ flexDirection: "row",
375
+ height: 1
376
+ },
377
+ children: /* @__PURE__ */ jsx(ProviderBadge, {
378
+ picked,
379
+ standalone: true
380
+ })
381
+ }),
382
+ context && /* @__PURE__ */ jsx("box", {
383
+ style: {
384
+ flexDirection: "row",
385
+ height: 1
386
+ },
387
+ children: /* @__PURE__ */ jsx(ContextIndicator, { context })
388
+ })
389
+ ]
390
+ });
391
+ }
392
+ function HintsText({ hints }) {
393
+ const COLOR = useColors();
394
+ return /* @__PURE__ */ jsx("text", {
395
+ fg: COLOR.dim,
396
+ children: hints.map((h, i) => /* @__PURE__ */ jsxs("span", { children: [
397
+ i > 0 && /* @__PURE__ */ jsx("span", {
398
+ fg: COLOR.mute,
399
+ children: " · "
400
+ }),
401
+ /* @__PURE__ */ jsx("span", {
402
+ fg: COLOR.warn,
403
+ children: h.key
404
+ }),
405
+ /* @__PURE__ */ jsx("span", {
406
+ fg: COLOR.dim,
407
+ children: ` ${h.label}`
408
+ })
409
+ ] }, i))
410
+ });
411
+ }
412
+ /**
413
+ * Colored badge for the active {@link AgentProfile}. The badge anchors the
414
+ * left edge of the bottom bar so the current mode (Build / Plan / host
415
+ * custom) is the first thing the eye lands on. Accent color comes from
416
+ * the profile's `accent` token, resolved against the active theme via
417
+ * `accentColor()` so picker rows and footer tinting stay in sync.
418
+ *
419
+ * Two render positions:
420
+ *
421
+ * - `leading` (default) — in-row at the very start. Renders just the
422
+ * label and a trailing ` · ` separator, e.g. `Build · `. Tight,
423
+ * prominent, and visually distinct from the surrounding hint text.
424
+ * - `standalone` — one-row-per-segment fallback for narrow terminals.
425
+ * Renders `agent <label>` with no separators so the row reads
426
+ * correctly on its own.
427
+ */
428
+ function AgentBadge({ agent, position = "leading" }) {
429
+ const COLOR = useColors();
430
+ const fg = accentColor(agent.accent, COLOR);
431
+ if (position === "standalone") return /* @__PURE__ */ jsxs("text", {
432
+ fg: COLOR.dim,
433
+ children: [/* @__PURE__ */ jsx("span", {
434
+ fg: COLOR.mute,
435
+ children: "agent "
436
+ }), /* @__PURE__ */ jsx("span", {
437
+ fg,
438
+ children: agent.label
439
+ })]
440
+ });
441
+ return /* @__PURE__ */ jsxs("text", {
442
+ fg: COLOR.dim,
443
+ children: [/* @__PURE__ */ jsx("span", {
444
+ fg,
445
+ children: agent.label
446
+ }), /* @__PURE__ */ jsx("span", {
447
+ fg: COLOR.mute,
448
+ children: " · "
449
+ })]
450
+ });
104
451
  }
105
- function ProviderBadge({ picked }) {
452
+ function ProviderBadge({ picked, standalone = false }) {
106
453
  const COLOR = useColors();
107
454
  const source = picked.provider.methods[0].source;
108
455
  return /* @__PURE__ */ jsxs("text", {
109
456
  fg: COLOR.dim,
110
457
  children: [
111
- /* @__PURE__ */ jsx("span", {
458
+ !standalone && /* @__PURE__ */ jsx("span", {
112
459
  fg: COLOR.mute,
113
460
  children: " · "
114
461
  }),
@@ -162,6 +509,22 @@ function ContextIndicator({ context }) {
162
509
  ]
163
510
  });
164
511
  }
512
+ function hintsLength(hints) {
513
+ if (hints.length === 0) return 0;
514
+ return hints.reduce((sum, h, i) => sum + h.key.length + 1 + h.label.length + (i > 0 ? 3 : 0), 0);
515
+ }
516
+ function providerBadgeLength(picked) {
517
+ const source = picked.provider.methods[0].source;
518
+ return 3 + picked.provider.label.length + 3 + picked.model.length + 3 + source.length;
519
+ }
520
+ function agentBadgeLength(agent) {
521
+ return agent.label.length + 3;
522
+ }
523
+ function contextIndicatorLength(context) {
524
+ const ratio = context.max > 0 ? context.used / context.max : 0;
525
+ const pct = Math.round(ratio * 100);
526
+ return 4 + fmtTokens(context.used).length + 3 + fmtTokens(context.max).length + 2 + String(pct).length + 2;
527
+ }
165
528
  const SPINNER_FRAMES = [
166
529
  "⠋",
167
530
  "⠙",
@@ -575,96 +938,6 @@ function ToolResultBlock({ text, indent }) {
575
938
  });
576
939
  }
577
940
  //#endregion
578
- //#region src/tui/modal.tsx
579
- const ModalContext = createContext(null);
580
- function ModalRoot({ children }) {
581
- const [active, setActive] = useState(null);
582
- const api = useMemo(() => ({
583
- open: (node) => setActive(node),
584
- close: () => setActive(null),
585
- get isOpen() {
586
- return active !== null;
587
- }
588
- }), [active]);
589
- return /* @__PURE__ */ jsxs(ModalContext.Provider, {
590
- value: api,
591
- children: [/* @__PURE__ */ jsx("box", {
592
- style: {
593
- flexDirection: "column",
594
- flexGrow: 1
595
- },
596
- children
597
- }), active && /* @__PURE__ */ jsx("box", {
598
- style: {
599
- position: "absolute",
600
- top: 0,
601
- left: 0,
602
- right: 0,
603
- bottom: 0,
604
- alignItems: "center",
605
- justifyContent: "center",
606
- zIndex: 100
607
- },
608
- children: active
609
- })]
610
- });
611
- }
612
- function useModal() {
613
- const ctx = useContext(ModalContext);
614
- if (!ctx) throw new Error("useModal must be used inside <ModalRoot>");
615
- return ctx;
616
- }
617
- /**
618
- * Focus computed against the modal layer.
619
- *
620
- * Pass a component's preferred focus state and this returns `false` whenever a
621
- * modal is open — so focused inputs (textarea, selects) release their focus and
622
- * stop intercepting keys behind the overlay. Pair with `focusable={false}` on
623
- * "passive" focusables (scrollbox) so the renderer doesn't cycle focus into
624
- * them when the primary input blurs.
625
- */
626
- function useModalAwareFocus(preferred = true) {
627
- const { isOpen } = useModal();
628
- return preferred && !isOpen;
629
- }
630
- /**
631
- * Responsive modal — picks a width based on the live terminal size.
632
- *
633
- * - On a wide terminal, the modal grows to `maxWidth` so descriptions sit on
634
- * one line and don't wrap.
635
- * - On a narrow terminal, the modal shrinks down to `minWidth`, keeping a
636
- * small horizontal margin from the screen edges. Text inside wraps naturally.
637
- *
638
- * Uses `useTerminalDimensions()` so it reflows on `SIGWINCH` without remount.
639
- */
640
- function Modal({ title, onClose, children, maxWidth = 92, minWidth = 44, horizontalMargin = 4 }) {
641
- const ctx = useContext(ModalContext);
642
- const dismiss = onClose ?? ctx?.close;
643
- const COLOR = useColors();
644
- const SURFACE = useSurfaces();
645
- useKeyboard((key) => {
646
- if (key.name === "escape") dismiss?.();
647
- });
648
- const { width: termWidth } = useTerminalDimensions();
649
- const width = Math.max(minWidth, Math.min(maxWidth, termWidth - horizontalMargin * 2));
650
- return /* @__PURE__ */ jsx("box", {
651
- title: title ? ` ${title} ` : void 0,
652
- style: {
653
- border: true,
654
- borderColor: COLOR.borderActive,
655
- backgroundColor: SURFACE.modal,
656
- paddingTop: 1,
657
- paddingBottom: 1,
658
- paddingLeft: 2,
659
- paddingRight: 2,
660
- width,
661
- flexDirection: "column",
662
- gap: 1
663
- },
664
- children
665
- });
666
- }
667
- //#endregion
668
941
  //#region src/tui/model-picker.tsx
669
942
  /** Cap the visible scroll window so a 30-model list doesn't push the modal off-screen. */
670
943
  const VISIBLE_ROW_CAP = 12;
@@ -1722,7 +1995,7 @@ function ThemedShell() {
1722
1995
  const { settings } = useSettings();
1723
1996
  return /* @__PURE__ */ jsx(ThemeProvider, {
1724
1997
  theme: useMemo(() => resolveTheme(settings.theme), [settings.theme]),
1725
- children: /* @__PURE__ */ jsx(SafeModeProvider, { children: /* @__PURE__ */ jsx(ModalRoot, { children: /* @__PURE__ */ jsx(AppShell, {}) }) })
1998
+ children: /* @__PURE__ */ jsx(MdStyleProvider, { children: /* @__PURE__ */ jsx(SafeModeProvider, { children: /* @__PURE__ */ jsx(ModalRoot, { children: /* @__PURE__ */ jsx(AppShell, {}) }) }) })
1726
1999
  });
1727
2000
  }
1728
2001
  function AppShell() {
@@ -1732,9 +2005,11 @@ function AppShell() {
1732
2005
  const { settings } = useSettings();
1733
2006
  const queue = useSafeModeQueue();
1734
2007
  const { requestApproval, resolveHead, denyAll } = useSafeModeActions();
1735
- const { providers: providerRegistry, preset, store, stateStore, modelsFor, resumeProvider, initialPicked, initialState } = config;
2008
+ const { providers: providerRegistry, agents: agentRegistry, initialAgentId, store, stateStore, modelsFor, resumeProvider, initialPicked, initialState } = config;
1736
2009
  const lastResumedSessionId = initialState.lastSessionId;
1737
2010
  const dataDir = config.paths.dir;
2011
+ const [pickedAgent, setPickedAgent] = useState(() => agentRegistry[initialAgentId] ?? Object.values(agentRegistry)[0]);
2012
+ const pickedAgentRef = useRef(pickedAgent);
1738
2013
  const safeModeEnabledRef = useRef(settings.safeMode);
1739
2014
  useEffect(() => {
1740
2015
  safeModeEnabledRef.current = settings.safeMode;
@@ -1804,8 +2079,9 @@ function AppShell() {
1804
2079
  const buildAgent = useCallback((session, key) => {
1805
2080
  const descriptor = providerRegistry[key];
1806
2081
  if (!descriptor) throw new Error(`No provider registered for key "${key}"`);
2082
+ const profile = pickedAgentRef.current;
1807
2083
  const agent = createAgent({
1808
- ...preset,
2084
+ ...profile.preset,
1809
2085
  provider: descriptor.factory(),
1810
2086
  session
1811
2087
  });
@@ -1911,7 +2187,6 @@ function AppShell() {
1911
2187
  return agent;
1912
2188
  }, [
1913
2189
  providerRegistry,
1914
- preset,
1915
2190
  stream,
1916
2191
  gateDecision
1917
2192
  ]);
@@ -1921,11 +2196,21 @@ function AppShell() {
1921
2196
  return list;
1922
2197
  }, [store]);
1923
2198
  const teardown = useCallback(async () => {
2199
+ try {
2200
+ denyAll();
2201
+ } catch (err) {
2202
+ debugLog("teardown: denyAll failed", err);
2203
+ }
2204
+ try {
2205
+ agentRef.current?.abort();
2206
+ } catch (err) {
2207
+ debugLog("teardown: agent.abort failed", err);
2208
+ }
1924
2209
  stream.reset();
1925
2210
  await agentRef.current?.destroy().catch((err) => debugLog("agent.destroy failed", err));
1926
2211
  agentRef.current = null;
1927
2212
  sessionRef.current = null;
1928
- }, [stream]);
2213
+ }, [stream, denyAll]);
1929
2214
  const activateSession = useCallback(async (id, key) => {
1930
2215
  await teardown();
1931
2216
  const session = (id ? await loadSession(store, id) : null) ?? await createSession({
@@ -1935,7 +2220,7 @@ function AppShell() {
1935
2220
  sessionRef.current = session;
1936
2221
  agentRef.current = buildAgent(session, key);
1937
2222
  setEvents(eventsFromTurns(session.turns, session.runs));
1938
- setLastInputTokens(lastContextSizeFromTurns(session.turns));
2223
+ setLastInputTokens(lastContextSizeFromTurns(session.turns, session.runs));
1939
2224
  setCurrentSession({
1940
2225
  id: session.id,
1941
2226
  title: titleFromTurns(session.turns) ?? "untitled",
@@ -2029,11 +2314,39 @@ function AppShell() {
2029
2314
  });
2030
2315
  modal.close();
2031
2316
  }, [modal, stateStore]);
2317
+ const onPickAgent = useCallback(async (id) => {
2318
+ const profile = agentRegistry[id];
2319
+ if (!profile) return;
2320
+ pickedAgentRef.current = profile;
2321
+ setPickedAgent(profile);
2322
+ stateStore.save({
2323
+ ...stateStore.load(),
2324
+ lastAgent: id
2325
+ });
2326
+ modal.close();
2327
+ if (picked && currentSession && !busy) await activateSession(currentSession.id, picked.provider.key);
2328
+ }, [
2329
+ agentRegistry,
2330
+ picked,
2331
+ currentSession,
2332
+ busy,
2333
+ activateSession,
2334
+ stateStore,
2335
+ modal
2336
+ ]);
2337
+ const onCycleAgent = useCallback(async () => {
2338
+ const ids = Object.keys(agentRegistry);
2339
+ if (ids.length <= 1) return;
2340
+ const nextId = ids[(ids.indexOf(pickedAgentRef.current.id) + 1) % ids.length];
2341
+ await onPickAgent(nextId);
2342
+ }, [agentRegistry, onPickAgent]);
2343
+ const eventsLengthRef = useRef(0);
2344
+ eventsLengthRef.current = events.length;
2032
2345
  const onSubmitPrompt = useCallback(async (prompt) => {
2033
2346
  const agent = agentRef.current;
2034
2347
  const session = sessionRef.current;
2035
2348
  if (!agent || !session || !picked || !prompt.trim()) return;
2036
- if (events.length > 0) stream.appendImmediate({
2349
+ if (eventsLengthRef.current > 0) stream.appendImmediate({
2037
2350
  kind: "separator",
2038
2351
  text: ""
2039
2352
  });
@@ -2063,16 +2376,18 @@ function AppShell() {
2063
2376
  stream.flushAndUpdate(finalizeStreamingMarkdown);
2064
2377
  setBusy(false);
2065
2378
  }
2066
- }, [
2067
- picked,
2068
- events.length,
2069
- stream
2070
- ]);
2379
+ }, [picked, stream]);
2380
+ const pendingApproval = queue[0] ?? null;
2071
2381
  const onReauth = useCallback(() => {
2382
+ if (busy || pendingApproval) return;
2072
2383
  modal.close();
2073
2384
  setScreen("auth");
2074
- }, [modal]);
2075
- const pendingApproval = queue[0] ?? null;
2385
+ }, [
2386
+ modal,
2387
+ busy,
2388
+ pendingApproval
2389
+ ]);
2390
+ const hasMultipleAgents = useMemo(() => Object.keys(agentRegistry).length > 1, [agentRegistry]);
2076
2391
  useKeyboard((key) => {
2077
2392
  if (modal.isOpen) return;
2078
2393
  if (key.ctrl && key.name === "," && screen !== "auth") {
@@ -2087,6 +2402,18 @@ function AppShell() {
2087
2402
  }));
2088
2403
  return;
2089
2404
  }
2405
+ if (key.ctrl && key.name === "a" && screen === "chat" && hasMultipleAgents && !busy) {
2406
+ modal.open(/* @__PURE__ */ jsx(AgentPickerModal, {
2407
+ agents: agentRegistry,
2408
+ currentAgentId: pickedAgent.id,
2409
+ onPick: onPickAgent
2410
+ }));
2411
+ return;
2412
+ }
2413
+ if (key.shift && key.name === "tab" && screen === "chat" && hasMultipleAgents && !busy) {
2414
+ onCycleAgent();
2415
+ return;
2416
+ }
2090
2417
  if (key.name !== "escape") return;
2091
2418
  if (busy || pendingApproval) return onAbort();
2092
2419
  if (screen === "chat") return onOpenSessions();
@@ -2101,11 +2428,12 @@ function AppShell() {
2101
2428
  }
2102
2429
  renderer.destroy();
2103
2430
  });
2104
- const hints = useMemo(() => buildHints(screen, busy, !!pendingApproval, currentSession), [
2431
+ const hints = useMemo(() => buildHints(screen, busy, !!pendingApproval, currentSession, hasMultipleAgents), [
2105
2432
  screen,
2106
2433
  busy,
2107
2434
  pendingApproval,
2108
- currentSession
2435
+ currentSession,
2436
+ hasMultipleAgents
2109
2437
  ]);
2110
2438
  const contextUsage = useMemo(() => {
2111
2439
  if (screen !== "chat" || !picked) return null;
@@ -2158,11 +2486,12 @@ function AppShell() {
2158
2486
  }), /* @__PURE__ */ jsx(Footer, {
2159
2487
  hints,
2160
2488
  picked,
2489
+ agent: screen === "chat" && hasMultipleAgents ? pickedAgent : null,
2161
2490
  context: contextUsage
2162
2491
  })]
2163
2492
  });
2164
2493
  }
2165
- function buildHints(screen, busy, pending, currentSession) {
2494
+ function buildHints(screen, busy, pending, currentSession, hasMultipleAgents) {
2166
2495
  if (pending) return [
2167
2496
  {
2168
2497
  key: "↑↓",
@@ -2218,6 +2547,10 @@ function buildHints(screen, busy, pending, currentSession) {
2218
2547
  key: "↵",
2219
2548
  label: "send"
2220
2549
  },
2550
+ ...hasMultipleAgents ? [{
2551
+ key: "shift+tab",
2552
+ label: "agent"
2553
+ }] : [],
2221
2554
  {
2222
2555
  key: "ctrl+m",
2223
2556
  label: "model"
@@ -2363,7 +2696,7 @@ let runTuiInvoked = false;
2363
2696
  * to `runTui({ storageDir, prefix })`.
2364
2697
  *
2365
2698
  * ```ts
2366
- * import { BUILTIN_PROVIDERS } from 'zidane/chat'
2699
+ * import { BUILTIN_AGENTS, BUILTIN_PROVIDERS } from 'zidane/chat'
2367
2700
  * import { runTui } from 'zidane/tui'
2368
2701
  * import { createRemoteStore } from 'zidane/session' // for the `store` option
2369
2702
  *
@@ -2372,6 +2705,7 @@ let runTuiInvoked = false;
2372
2705
  * await runTui({ storageDir: '/data', prefix: 'myapp' })
2373
2706
  * await runTui({ providers: { ...BUILTIN_PROVIDERS, mine: myDescriptor } })
2374
2707
  * await runTui({ store: createRemoteStore({ url: '…' }) })
2708
+ * await runTui({ agents: { ...BUILTIN_AGENTS, debug: myDebugProfile } })
2375
2709
  * ```
2376
2710
  */
2377
2711
  async function runTui(options = {}) {
@@ -2381,19 +2715,23 @@ async function runTui(options = {}) {
2381
2715
  const cause = err instanceof Error ? err.message : String(err);
2382
2716
  process.stderr.write(`[zidane/tui] tree-sitter setup failed: ${cause}\n`);
2383
2717
  });
2384
- const config = resolveConfig(options);
2718
+ const config = resolveConfig({
2719
+ ...options,
2720
+ store: options.store ?? ((paths) => createTuiStore(paths.db))
2721
+ });
2385
2722
  let done = () => {};
2386
2723
  const exited = new Promise((resolve) => {
2387
2724
  done = resolve;
2388
2725
  });
2389
2726
  createRoot(await createCliRenderer({
2390
2727
  exitOnCtrlC: true,
2391
- onDestroy: () => done()
2728
+ onDestroy: () => done(),
2729
+ debounceDelay: 0
2392
2730
  })).render(/* @__PURE__ */ jsx(App, { config }));
2393
2731
  await exited;
2394
2732
  process.exit(0);
2395
2733
  }
2396
2734
  //#endregion
2397
- export { App, AuthScreen, ChatScreen, Footer, Modal, ModalRoot, ModelPickerModal, SessionsScreen, SettingsModal, Spinner, Transcript, buildMdStyle, isVisible, marginTopFor, onInputSubmit, runTui, useMdStyle, useModal, useModalAwareFocus };
2735
+ export { AgentPickerModal, App, AuthScreen, ChatScreen, Footer, Modal, ModalRoot, ModelPickerModal, SessionsScreen, SettingsModal, Spinner, Transcript, accentColor, buildMdStyle, isVisible, marginTopFor, onInputSubmit, runTui, useMdStyle, useModal, useModalAwareFocus };
2398
2736
 
2399
2737
  //# sourceMappingURL=tui.js.map