zidane 5.3.0 → 5.3.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 (68) hide show
  1. package/README.md +2 -0
  2. package/dist/{agent-CYpPKn5Z.d.ts → agent-BXRCCHeq.d.ts} +557 -5
  3. package/dist/agent-BXRCCHeq.d.ts.map +1 -0
  4. package/dist/chat.d.ts +310 -6
  5. package/dist/chat.d.ts.map +1 -1
  6. package/dist/chat.js +2 -2
  7. package/dist/{errors-COmsomd5.js → errors-Byb0F8B9.js} +44 -2
  8. package/dist/errors-Byb0F8B9.js.map +1 -0
  9. package/dist/{index-D-cTScN3.d.ts → index-BPk8-Slm.d.ts} +81 -10
  10. package/dist/index-BPk8-Slm.d.ts.map +1 -0
  11. package/dist/{index-Cc-q1hLT.d.ts → index-CT5_p-3P.d.ts} +2 -2
  12. package/dist/{index-Cc-q1hLT.d.ts.map → index-CT5_p-3P.d.ts.map} +1 -1
  13. package/dist/index.d.ts +4 -4
  14. package/dist/index.js +10 -10
  15. package/dist/{interpolate-BhmHKD6x.js → interpolate-ERgZUxgg.js} +2 -2
  16. package/dist/{interpolate-BhmHKD6x.js.map → interpolate-ERgZUxgg.js.map} +1 -1
  17. package/dist/{login-BXVt5wuA.js → login-DrBZ15G7.js} +3 -3
  18. package/dist/{login-BXVt5wuA.js.map → login-DrBZ15G7.js.map} +1 -1
  19. package/dist/{mcp-B1psg7jf.js → mcp-DhmmJfxK.js} +16 -3
  20. package/dist/mcp-DhmmJfxK.js.map +1 -0
  21. package/dist/mcp.d.ts +1 -1
  22. package/dist/mcp.js +1 -1
  23. package/dist/{messages-DsbMYNmt.js → messages-D0xT979U.js} +631 -68
  24. package/dist/messages-D0xT979U.js.map +1 -0
  25. package/dist/{presets-tvD28pCu.js → presets-0_IRJAYF.js} +29 -10
  26. package/dist/presets-0_IRJAYF.js.map +1 -0
  27. package/dist/presets.d.ts +2 -2
  28. package/dist/presets.js +1 -1
  29. package/dist/{providers-v1Rn2rqG.js → providers-x3LZByR5.js} +38 -6
  30. package/dist/providers-x3LZByR5.js.map +1 -0
  31. package/dist/providers.d.ts +2 -2
  32. package/dist/providers.js +3 -3
  33. package/dist/session/sqlite.d.ts +1 -1
  34. package/dist/session/sqlite.js +1 -1
  35. package/dist/{session-DOJgRXvF.js → session-BHZwxmfr.js} +2 -2
  36. package/dist/{session-DOJgRXvF.js.map → session-BHZwxmfr.js.map} +1 -1
  37. package/dist/session.d.ts +1 -1
  38. package/dist/session.js +2 -2
  39. package/dist/skills.d.ts +2 -2
  40. package/dist/skills.js +1 -1
  41. package/dist/{tools-CMVruxF0.js → tools-CCsL5SCO.js} +516 -140
  42. package/dist/tools-CCsL5SCO.js.map +1 -0
  43. package/dist/tools.d.ts +3 -3
  44. package/dist/tools.js +2 -2
  45. package/dist/{transcript-anchors-eyhlGeBI.d.ts → transcript-anchors-DSk8LlWt.d.ts} +28 -4
  46. package/dist/transcript-anchors-DSk8LlWt.d.ts.map +1 -0
  47. package/dist/tui.d.ts +29 -3
  48. package/dist/tui.d.ts.map +1 -1
  49. package/dist/tui.js +365 -80
  50. package/dist/tui.js.map +1 -1
  51. package/dist/{turn-operations-Y7e15gJf.js → turn-operations-CutZin8X.js} +678 -33
  52. package/dist/turn-operations-CutZin8X.js.map +1 -0
  53. package/dist/types-IcokUOyC.js.map +1 -1
  54. package/dist/types.d.ts +2 -2
  55. package/dist/types.js +1 -1
  56. package/docs/ARCHITECTURE.md +1 -1
  57. package/docs/SKILL.md +23 -1
  58. package/package.json +1 -1
  59. package/dist/agent-CYpPKn5Z.d.ts.map +0 -1
  60. package/dist/errors-COmsomd5.js.map +0 -1
  61. package/dist/index-D-cTScN3.d.ts.map +0 -1
  62. package/dist/mcp-B1psg7jf.js.map +0 -1
  63. package/dist/messages-DsbMYNmt.js.map +0 -1
  64. package/dist/presets-tvD28pCu.js.map +0 -1
  65. package/dist/providers-v1Rn2rqG.js.map +0 -1
  66. package/dist/tools-CMVruxF0.js.map +0 -1
  67. package/dist/transcript-anchors-eyhlGeBI.d.ts.map +0 -1
  68. package/dist/turn-operations-Y7e15gJf.js.map +0 -1
package/dist/tui.js CHANGED
@@ -1,11 +1,11 @@
1
- import { S as cleanupPersistedSession, p as createAgent, w as resolvePersistDir } from "./tools-CMVruxF0.js";
2
- import { o as errorMessage } from "./errors-COmsomd5.js";
3
- import { s as McpOAuthProvider, t as connectMcpServers } from "./mcp-B1psg7jf.js";
4
- import { C as summaryToTurn, a as selectFilesFromSession, r as buildPostCompactAttachments, s as compactConversation, t as loginMcpServer } from "./login-BXVt5wuA.js";
1
+ import { D as resolvePersistDir, T as cleanupPersistedSession, p as createAgent } from "./tools-CCsL5SCO.js";
2
+ import { s as errorMessage } from "./errors-Byb0F8B9.js";
3
+ import { s as McpOAuthProvider, t as connectMcpServers } from "./mcp-DhmmJfxK.js";
4
+ import { C as summaryToTurn, a as selectFilesFromSession, r as buildPostCompactAttachments, s as compactConversation, t as loginMcpServer } from "./login-DrBZ15G7.js";
5
5
  import { n as formatTokenUsage } from "./stats-DgOvY7wd.js";
6
- import { n as loadSession, t as createSession } from "./session-DOJgRXvF.js";
6
+ import { n as loadSession, t as createSession } from "./session-BHZwxmfr.js";
7
7
  import { createTuiStore } from "./session/sqlite.js";
8
- import { $ as useMcpAuthDispatch, $n as bootTick, $t as isVisible, A as getSafelist, At as clampFps, B as supportsOAuth, Bn as uniqueSkillNamesFromReferences, Br as buildBuildSystem, Ct as shortId, D as useSafeModeQueue, Dn as findGitRoot, Dr as accentColor, Dt as SETTINGS_CHOICES, E as useSafeModeActions, En as summarizeOutcomes, Et as DEFAULT_SETTINGS, F as suggestSafelistEntry, Ft as resolveTheme, Gt as ConfigProvider, H as filterModelCatalog, Hn as createFilesCompletionProvider, Ht as DiscoveryProvider, Jn as useCompletion, K as discoverProjectMcps, Kt as useConfig, L as splitPromptSegments, Nn as matchesBinding, Ot as SETTINGS_TOGGLES, Pt as resolveChipColor, Q as McpAuthProvider, Qt as isTurnHighlighted, R as formatPathForCwd, Sn as buildEditOutcomesAnnotation, St as fmtTokens, T as SafeModeProvider, Tn as resolveApprovalForPayload, Tt as useEnabledToggleSet, U as indexOfEntry, Ut as useDiscovery, V as buildModelCatalog, Vr as buildPlanSystem, Vt as createDiscoverySlot, W as buildMcpServers, Wt as useDiscoveryOptional, Xn as buildLinearRamp, Xt as eventsFromTurns, Y as createFileMcpCredentialStore, Yn as blendHsl, Yt as deriveSessionTitle, Zn as tryOpenBrowser, Zt as isEditErrorResult, _ as turnContextSize, _n as extractEditPayload, _r as modelSupportsReasoning, _t as truncateTrailing, a as computeTurnAnchors, an as selectableTurnIds, at as InteractionsProvider, b as defaultSkillScanPaths, bt as ageString, c as formatToolCall, ct as createInteractionTools, d as useSelectStyle, dn as turnSelectionOwnership, dt as pendingInteractionsFromTurns, en as lastContextSizeFromTurns, er as shouldAutoCompact, et as useMcpAuthState, f as useSurfaces, g as finalizeStreamingMarkdownForOwner, gt as hintsLength, h as finalizeStreamingMarkdown, hr as getContextWindow, ht as clipHintsToWidth, i as turnAsText, j as isOnSafelist, jn as ensureKeybindingsFile, jt as useSettings, k as addToSafelist, kt as SettingsProvider, l as ThemeProvider, ln as toolCallPreview, m as useTheme, mn as buildUnifiedDiff, mt as useInteractionsQueue, n as deleteTurnSafely, o as TOOL_DISPLAY, on as stripSpawnTokensLine, pt as useInteractionsActions, qt as resolveConfig, r as truncateTurnsAt, rn as marginTopFor, rt as splitMarkdownCodeBlocks, s as displayNameFor, sn as sumRunCosts, sr as setProviderCredential, st as buildResumedToolResultsTurn, tn as listSessionMeta, tr as detectAuth, tt as getMcpAuthStatus, u as useColors, un as toolResultText, ut as makeRequestInteraction, v as useStreamBuffer, vn as filetypeFromPath, w as writeSessionExport, wt as listProjectFiles, x as discoverProjectSkills, xr as piIdOf, xt as compactPath, y as buildSkillsConfig, yn as previewEditPayload, yt as generateSessionTitle, z as runOAuthLogin, zn as createSkillsCompletionProvider } from "./turn-operations-Y7e15gJf.js";
8
+ import { $ as useMcpAuthDispatch, $n as bootTick, $t as isVisible, A as getSafelist, At as clampFps, B as supportsOAuth, Bn as uniqueSkillNamesFromReferences, Ct as shortId, D as useSafeModeQueue, Dn as findGitRoot, Dr as accentColor, Dt as SETTINGS_CHOICES, E as useSafeModeActions, En as summarizeOutcomes, Et as DEFAULT_SETTINGS, F as suggestSafelistEntry, Ft as resolveTheme, Gt as ConfigProvider, H as filterModelCatalog, Hn as createFilesCompletionProvider, Ht as DiscoveryProvider, Jn as useCompletion, K as discoverProjectMcps, Kt as useConfig, L as splitPromptSegments, Nn as matchesBinding, Nr as TODO_STATUS_GLYPHS, Ot as SETTINGS_TOGGLES, Pt as resolveChipColor, Q as McpAuthProvider, Qt as isTurnHighlighted, R as formatPathForCwd, Sn as buildEditOutcomesAnnotation, St as fmtTokens, T as SafeModeProvider, Tn as resolveApprovalForPayload, Tt as useEnabledToggleSet, U as indexOfEntry, Ur as useActiveTodos, Ut as useDiscovery, V as buildModelCatalog, Vt as createDiscoverySlot, W as buildMcpServers, Wt as useDiscoveryOptional, Xn as buildLinearRamp, Xt as eventsFromTurns, Y as createFileMcpCredentialStore, Yn as blendHsl, Yt as deriveSessionTitle, Zn as tryOpenBrowser, Zt as isEditErrorResult, _ as turnContextSize, _n as extractEditPayload, _r as modelSupportsReasoning, _t as truncateTrailing, a as computeTurnAnchors, an as selectableTurnIds, at as InteractionsProvider, b as defaultSkillScanPaths, bt as ageString, c as formatToolCall, ct as createInteractionTools, d as useSelectStyle, dn as turnSelectionOwnership, dt as pendingInteractionsFromTurns, ei as buildBuildSystem, en as lastContextSizeFromTurns, er as shouldAutoCompact, et as useMcpAuthState, f as useSurfaces, g as finalizeStreamingMarkdownForOwner, gt as hintsLength, h as finalizeStreamingMarkdown, hr as getContextWindow, ht as clipHintsToWidth, i as turnAsText, j as isOnSafelist, jn as ensureKeybindingsFile, jt as useSettings, k as addToSafelist, kt as SettingsProvider, l as ThemeProvider, ln as toolCallPreview, m as useTheme, mn as buildUnifiedDiff, mt as useInteractionsQueue, n as deleteTurnSafely, o as TOOL_DISPLAY, on as stripSpawnTokensLine, pt as useInteractionsActions, qt as resolveConfig, r as truncateTurnsAt, rn as marginTopFor, rt as splitMarkdownCodeBlocks, s as displayNameFor, sn as sumRunCosts, sr as setProviderCredential, st as buildResumedToolResultsTurn, ti as buildPlanSystem, tn as listSessionMeta, tr as detectAuth, tt as getMcpAuthStatus, u as useColors, un as toolResultText, ut as makeRequestInteraction, v as useStreamBuffer, vn as filetypeFromPath, w as writeSessionExport, wt as listProjectFiles, x as discoverProjectSkills, xr as piIdOf, xt as compactPath, y as buildSkillsConfig, yn as previewEditPayload, yt as generateSessionTitle, z as runOAuthLogin, zn as createSkillsCompletionProvider } from "./turn-operations-CutZin8X.js";
9
9
  import { spawn } from "node:child_process";
10
10
  import { Buffer } from "node:buffer";
11
11
  import * as fs from "node:fs";
@@ -80,7 +80,7 @@ function useModalAwareFocus(preferred = true) {
80
80
  *
81
81
  * Uses `useTerminalDimensions()` so it reflows on `SIGWINCH` without remount.
82
82
  */
83
- function Modal({ title, bottomTitle, onClose, disableEscape = false, children, maxWidth = 92, minWidth = 44, maxHeight, horizontalMargin = 4, verticalMargin = 2 }) {
83
+ function Modal({ title, bottomTitle, rightTitle, onClose, disableEscape = false, children, maxWidth = 92, minWidth = 44, maxHeight, horizontalMargin = 4, verticalMargin = 2 }) {
84
84
  const ctx = useContext(ModalContext);
85
85
  const dismiss = onClose ?? ctx?.close;
86
86
  const COLOR = useColors();
@@ -91,7 +91,7 @@ function Modal({ title, bottomTitle, onClose, disableEscape = false, children, m
91
91
  const { width: termWidth, height: termHeight } = useTerminalDimensions();
92
92
  const width = Math.max(minWidth, Math.min(maxWidth, termWidth - horizontalMargin * 2));
93
93
  const height = maxHeight === void 0 ? void 0 : Math.min(maxHeight, Math.max(0, termHeight - verticalMargin * 2));
94
- return /* @__PURE__ */ jsx("box", {
94
+ const panel = /* @__PURE__ */ jsx("box", {
95
95
  title: title ? ` ${title} ` : void 0,
96
96
  bottomTitle: bottomTitle ? ` ${bottomTitle} ` : void 0,
97
97
  bottomTitleAlignment: "right",
@@ -110,6 +110,21 @@ function Modal({ title, bottomTitle, onClose, disableEscape = false, children, m
110
110
  },
111
111
  children
112
112
  });
113
+ if (rightTitle == null) return panel;
114
+ return /* @__PURE__ */ jsxs("box", {
115
+ style: {
116
+ width,
117
+ flexDirection: "column"
118
+ },
119
+ children: [panel, /* @__PURE__ */ jsx("box", {
120
+ style: {
121
+ position: "absolute",
122
+ top: 0,
123
+ right: 1
124
+ },
125
+ children: rightTitle
126
+ })]
127
+ });
113
128
  }
114
129
  //#endregion
115
130
  //#region src/tui/agent-picker.tsx
@@ -923,7 +938,7 @@ function StatusSpinner({ color }) {
923
938
  function Transcript({ events, settings, selectedTurnId = null, busy = false }) {
924
939
  const COLOR = useColors();
925
940
  const items = useMemo(() => partitionTranscript(events, settings), [events, settings]);
926
- const showThrobber = busy;
941
+ const showThrobber = busy && settings.showThrobber;
927
942
  const throbberLabel = events.length > 0 && events[events.length - 1].kind === "thinking" ? "Thinking" : void 0;
928
943
  const ownership = useMemo(() => turnSelectionOwnership(events), [events]);
929
944
  const scrollboxRef = useRef(null);
@@ -1145,13 +1160,16 @@ function EventLineImpl({ event, depthOffset = 0 }) {
1145
1160
  dim: child
1146
1161
  })
1147
1162
  });
1148
- return /* @__PURE__ */ jsx("box", {
1163
+ return /* @__PURE__ */ jsxs("box", {
1149
1164
  style: row,
1150
- children: /* @__PURE__ */ jsx(ToolCallBlock, {
1165
+ children: [/* @__PURE__ */ jsx(ToolCallBlock, {
1151
1166
  event,
1152
1167
  display: settings.toolCallDisplay === "full" ? "full" : "formatted",
1153
1168
  dim: child
1154
- })
1169
+ }), event.tool === "todowrite" && /* @__PURE__ */ jsx(TodoInProgressList, {
1170
+ input: event.input,
1171
+ dim: child
1172
+ })]
1155
1173
  });
1156
1174
  case "tool-result": return /* @__PURE__ */ jsx(ToolResultBlock, {
1157
1175
  text: event.text,
@@ -1644,6 +1662,7 @@ const MarkdownBlock = memo(({ text, dim }) => {
1644
1662
  const SURFACE = useSurfaces();
1645
1663
  const mdStyle = useMdStyle();
1646
1664
  const renderer = useRenderer();
1665
+ const content = text.replace(/^\n+|\n+$/g, "");
1647
1666
  const bag = useRef({
1648
1667
  ctx: renderer,
1649
1668
  colors: COLOR,
@@ -1658,7 +1677,7 @@ const MarkdownBlock = memo(({ text, dim }) => {
1658
1677
  }, [COLOR, SURFACE]);
1659
1678
  const renderNode = useMemo(() => makeMarkdownRenderNode(bag), []);
1660
1679
  return /* @__PURE__ */ jsx("markdown", {
1661
- content: text,
1680
+ content,
1662
1681
  syntaxStyle: mdStyle,
1663
1682
  streaming: true,
1664
1683
  internalBlockMode: "coalesced",
@@ -1966,6 +1985,43 @@ function ToolCallBlock({ event, display, dim }) {
1966
1985
  ]
1967
1986
  });
1968
1987
  }
1988
+ function TodoInProgressList({ input, dim }) {
1989
+ const COLOR = useColors();
1990
+ const items = useMemo(() => {
1991
+ const raw = input?.todos;
1992
+ if (!Array.isArray(raw)) return [];
1993
+ return raw.flatMap((t) => {
1994
+ if (t == null || typeof t !== "object") return [];
1995
+ const rec = t;
1996
+ if (rec.status !== "in_progress") return [];
1997
+ const id = typeof rec.id === "string" ? rec.id : "";
1998
+ const content = typeof rec.content === "string" ? rec.content : "";
1999
+ if (!id || !content) return [];
2000
+ return [{
2001
+ id,
2002
+ content
2003
+ }];
2004
+ });
2005
+ }, [input]);
2006
+ if (items.length === 0) return null;
2007
+ const glyph = TODO_STATUS_GLYPHS.in_progress;
2008
+ return /* @__PURE__ */ jsx("box", {
2009
+ style: {
2010
+ flexDirection: "column",
2011
+ marginLeft: 2
2012
+ },
2013
+ children: items.map((item) => /* @__PURE__ */ jsxs("text", {
2014
+ wrapMode: "none",
2015
+ children: [/* @__PURE__ */ jsx("span", {
2016
+ fg: COLOR.mute,
2017
+ children: `${glyph} `
2018
+ }), /* @__PURE__ */ jsx("span", {
2019
+ fg: dim ? COLOR.dim : COLOR.warn,
2020
+ children: item.content
2021
+ })]
2022
+ }, item.id))
2023
+ });
2024
+ }
1969
2025
  //#endregion
1970
2026
  //#region src/tui/discovery-shell.tsx
1971
2027
  /**
@@ -2740,26 +2796,6 @@ const FILE_EDIT_TOOLS = new Set([
2740
2796
  function isFileEditTool(tool) {
2741
2797
  return FILE_EDIT_TOOLS.has(tool);
2742
2798
  }
2743
- /** Page jump = viewport height minus 1 row of carry-over context. */
2744
- function diffPageStep(viewportHeight) {
2745
- return Math.max(1, viewportHeight - 1);
2746
- }
2747
- /** Clamp a candidate `scrollTop` into `[0, max]`, tolerating bad inputs. */
2748
- function clampScrollTop(next, max) {
2749
- if (!Number.isFinite(next)) return 0;
2750
- if (!Number.isFinite(max) || max < 0) return Math.max(0, next);
2751
- return Math.max(0, Math.min(max, next));
2752
- }
2753
- /**
2754
- * Apply a signed scroll delta to a scrollbox, clamping against its own
2755
- * scrollHeight/viewport. Mouse-wheel events still route through OpenTUI's
2756
- * own hit-tester regardless of focus; this only services keyboard scroll.
2757
- */
2758
- function applyDiffScroll(scrollbox, delta) {
2759
- if (!scrollbox) return;
2760
- const max = Math.max(0, scrollbox.scrollHeight - scrollbox.viewport.height);
2761
- scrollbox.scrollTop = clampScrollTop(scrollbox.scrollTop + delta, max);
2762
- }
2763
2799
  function useEditPayloadFromRequest(request) {
2764
2800
  const targetPath = String(request.input.path ?? "");
2765
2801
  const [priorContent, priorError] = useMemo(() => {
@@ -2891,7 +2927,6 @@ function SingleEditApprovalModal({ request, payload, priorContent, priorError, t
2891
2927
  const mdStyle = useMdStyle();
2892
2928
  const { width: termWidth } = useTerminalDimensions();
2893
2929
  const modal = useModal();
2894
- const diffScrollRef = useRef(null);
2895
2930
  const preview = useMemo(() => payload ? previewEditPayload(payload, priorContent, 6) : null, [payload, priorContent]);
2896
2931
  const diffText = preview?.diffText ?? "";
2897
2932
  const focusedResolved = preview?.resolution[0]?.resolved ?? true;
@@ -2908,20 +2943,6 @@ function SingleEditApprovalModal({ request, payload, priorContent, priorError, t
2908
2943
  modal.close();
2909
2944
  }, [onDecide, modal]);
2910
2945
  useKeyboard((key) => {
2911
- if (key.name === "pageup") {
2912
- const sb = diffScrollRef.current;
2913
- if (sb) applyDiffScroll(sb, -diffPageStep(sb.viewport.height));
2914
- return;
2915
- }
2916
- if (key.name === "pagedown") {
2917
- const sb = diffScrollRef.current;
2918
- if (sb) applyDiffScroll(sb, diffPageStep(sb.viewport.height));
2919
- return;
2920
- }
2921
- if ((key.name === "up" || key.name === "down") && key.shift) {
2922
- applyDiffScroll(diffScrollRef.current, key.name === "up" ? -1 : 1);
2923
- return;
2924
- }
2925
2946
  if (key.name === "left") {
2926
2947
  setSelected((i) => (i - 1 + BULK_ACTIONS.length) % BULK_ACTIONS.length);
2927
2948
  return;
@@ -2954,7 +2975,7 @@ function SingleEditApprovalModal({ request, payload, priorContent, priorError, t
2954
2975
  children: [/* @__PURE__ */ jsxs("box", {
2955
2976
  title: SINGLE_EDIT_TITLE,
2956
2977
  titleAlignment: "left",
2957
- bottomTitle: " ←→ navigate · ↵ confirm · esc deny · a/s/p/d shortcuts · PgUp/PgDn scroll ",
2978
+ bottomTitle: " ←→ navigate · ↵ confirm · esc deny · a/s/p/d shortcuts ",
2958
2979
  style: {
2959
2980
  flexGrow: 1,
2960
2981
  flexShrink: 1,
@@ -2991,7 +3012,6 @@ function SingleEditApprovalModal({ request, payload, priorContent, priorError, t
2991
3012
  marginBottom: 1
2992
3013
  },
2993
3014
  children: /* @__PURE__ */ jsx("scrollbox", {
2994
- ref: diffScrollRef,
2995
3015
  focusable: false,
2996
3016
  stickyScroll: false,
2997
3017
  style: {
@@ -3119,20 +3139,6 @@ function MultiEditApprovalModal({ request, payload, priorContent, priorError, ta
3119
3139
  decide("deny");
3120
3140
  return;
3121
3141
  }
3122
- if (key.name === "pageup") {
3123
- const sb = diffScrollRef.current;
3124
- if (sb) applyDiffScroll(sb, -diffPageStep(sb.viewport.height));
3125
- return;
3126
- }
3127
- if (key.name === "pagedown") {
3128
- const sb = diffScrollRef.current;
3129
- if (sb) applyDiffScroll(sb, diffPageStep(sb.viewport.height));
3130
- return;
3131
- }
3132
- if ((key.name === "up" || key.name === "down") && key.shift) {
3133
- applyDiffScroll(diffScrollRef.current, key.name === "up" ? -1 : 1);
3134
- return;
3135
- }
3136
3142
  if (key.name === "tab") {
3137
3143
  setZone((z) => z === "list" ? "actions" : "list");
3138
3144
  return;
@@ -3220,7 +3226,7 @@ function MultiEditApprovalModal({ request, payload, priorContent, priorError, ta
3220
3226
  children: [/* @__PURE__ */ jsxs("box", {
3221
3227
  title: MULTI_EDIT_TITLE,
3222
3228
  titleAlignment: "left",
3223
- bottomTitle: " ↑↓ select · space toggle · y/n all/none · tab focus · a/s/p/d shortcuts · ↵ confirm · esc deny · PgUp/PgDn scroll ",
3229
+ bottomTitle: " ↑↓ select · space toggle · y/n all/none · tab focus · a/s/p/d shortcuts · ↵ confirm · esc deny ",
3224
3230
  style: {
3225
3231
  flexGrow: 1,
3226
3232
  flexShrink: 1,
@@ -4210,6 +4216,72 @@ function OptionList({ items, initialCursor, onPick }) {
4210
4216
  });
4211
4217
  }
4212
4218
  //#endregion
4219
+ //#region src/tui/todo-indicator.tsx
4220
+ /**
4221
+ * Layout budget when truncating the content. The bar never wraps —
4222
+ * single-line by contract — so we subtract every column consumed by
4223
+ * surrounding chrome before measuring how much room is left for the
4224
+ * todo's `content` text.
4225
+ *
4226
+ * - 2 cols ⇒ `<ChatScreen>`'s `border: true` (1 cell each side).
4227
+ * - 2 cols ⇒ this indicator's own `paddingLeft: 1 + paddingRight: 1`
4228
+ * (mirrors the queue block's outer padding so the bar
4229
+ * and queue read as aligned siblings).
4230
+ * - 4 cols ⇒ the indicator's own visible chrome: glyph "◐" (1) +
4231
+ * two spaces (2) + 1 col of trailing breathing room.
4232
+ *
4233
+ * That's 8 cols total of non-content overhead. Below `MIN_VISIBLE_TAIL`
4234
+ * the bar bails — a one-or-two-char tail isn't worth painting.
4235
+ */
4236
+ const SCREEN_BORDER_COLS = 2;
4237
+ const INDICATOR_PADDING_COLS = 2;
4238
+ const CHROME_COLS = 4;
4239
+ const MIN_VISIBLE_TAIL = 8;
4240
+ function truncateForWidth(text, max) {
4241
+ if (max <= 0) return "";
4242
+ if (text.length <= max) return text;
4243
+ if (max <= 1) return "…";
4244
+ return `${text.slice(0, max - 1)}…`;
4245
+ }
4246
+ function TodoIndicator({ session }) {
4247
+ const { settings } = useSettings();
4248
+ const COLOR = useColors();
4249
+ const { width: termWidth } = useTerminalDimensions();
4250
+ const state = useActiveTodos(session);
4251
+ if (!settings.showTodoIndicator) return null;
4252
+ const item = state.inProgress;
4253
+ if (!item) return null;
4254
+ const usable = Math.max(0, termWidth - SCREEN_BORDER_COLS - INDICATOR_PADDING_COLS - CHROME_COLS);
4255
+ if (usable < MIN_VISIBLE_TAIL) return null;
4256
+ const display = truncateForWidth(item.content.replace(/\s+/g, " ").trim(), usable);
4257
+ return /* @__PURE__ */ jsx("box", {
4258
+ style: {
4259
+ marginTop: 1,
4260
+ flexShrink: 0,
4261
+ flexDirection: "row",
4262
+ paddingLeft: 1,
4263
+ paddingRight: 1
4264
+ },
4265
+ children: /* @__PURE__ */ jsxs("text", {
4266
+ wrapMode: "none",
4267
+ children: [
4268
+ /* @__PURE__ */ jsx("span", {
4269
+ fg: COLOR.warn,
4270
+ children: TODO_STATUS_GLYPHS.in_progress
4271
+ }),
4272
+ /* @__PURE__ */ jsx("span", {
4273
+ fg: COLOR.mute,
4274
+ children: " "
4275
+ }),
4276
+ /* @__PURE__ */ jsx("span", {
4277
+ fg: COLOR.dim,
4278
+ children: display
4279
+ })
4280
+ ]
4281
+ })
4282
+ });
4283
+ }
4284
+ //#endregion
4213
4285
  //#region src/tui/screens.tsx
4214
4286
  /**
4215
4287
  * Build a key-binding set for the prompt textarea / API-key input. Strips the
@@ -5028,7 +5100,7 @@ const CWD_DISPLAY = compactPath(process.cwd());
5028
5100
  * default-value reference at the prop destructure.
5029
5101
  */
5030
5102
  const EMPTY_QUEUED_MESSAGES = [];
5031
- function ChatScreen({ events, busy, compacting = false, queuedMessages = EMPTY_QUEUED_MESSAGES, queueSelectionIndex = null, queueShortcuts, onEnterQueueFromEmptyPrompt, settings, onSubmit, session, pending, onApproval, pendingInteraction, onInteraction, completionProviders, onPopupOpenChange, selectedTurnId, promptTriggerHints }) {
5103
+ function ChatScreen({ events, busy, compacting = false, queuedMessages = EMPTY_QUEUED_MESSAGES, queueSelectionIndex = null, queueShortcuts, onEnterQueueFromEmptyPrompt, settings, onSubmit, session, pending, onApproval, pendingInteraction, onInteraction, completionProviders, onPopupOpenChange, selectedTurnId, promptTriggerHints, liveSession = null }) {
5032
5104
  const COLOR = useColors();
5033
5105
  const titleText = session?.title ?? "untitled";
5034
5106
  const showSessionShortcut = !!session && !busy && !pending && !pendingInteraction;
@@ -5127,20 +5199,24 @@ function ChatScreen({ events, busy, compacting = false, queuedMessages = EMPTY_Q
5127
5199
  }) : pendingInteraction ? /* @__PURE__ */ jsx(InteractionBlock, {
5128
5200
  request: pendingInteraction,
5129
5201
  onResolve: onInteraction
5130
- }) : /* @__PURE__ */ jsxs(Fragment, { children: [queuedMessages.length > 0 && !completionPopupOpen && /* @__PURE__ */ jsx(QueuedMessagesBlock, {
5131
- messages: queuedMessages,
5132
- selectionIndex: queueSelectionIndex,
5133
- shortcuts: queueShortcuts
5134
- }), /* @__PURE__ */ jsx(PromptBlock, {
5135
- userPrompts,
5136
- onSubmit,
5137
- completionProviders,
5138
- onPopupOpenChange: handlePopupOpenChange,
5139
- selectMode: queueSelectionIndex != null ? "queue" : selectedTurnId != null ? "turn" : null,
5140
- triggerHints: promptTriggerHints,
5141
- busy,
5142
- onEnterQueueFromEmpty: onEnterQueueFromEmptyPrompt
5143
- })] }),
5202
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [
5203
+ queuedMessages.length > 0 && !completionPopupOpen && /* @__PURE__ */ jsx(QueuedMessagesBlock, {
5204
+ messages: queuedMessages,
5205
+ selectionIndex: queueSelectionIndex,
5206
+ shortcuts: queueShortcuts
5207
+ }),
5208
+ /* @__PURE__ */ jsx(TodoIndicator, { session: liveSession }),
5209
+ /* @__PURE__ */ jsx(PromptBlock, {
5210
+ userPrompts,
5211
+ onSubmit,
5212
+ completionProviders,
5213
+ onPopupOpenChange: handlePopupOpenChange,
5214
+ selectMode: queueSelectionIndex != null ? "queue" : selectedTurnId != null ? "turn" : null,
5215
+ triggerHints: promptTriggerHints,
5216
+ busy,
5217
+ onEnterQueueFromEmpty: onEnterQueueFromEmptyPrompt
5218
+ })
5219
+ ] }),
5144
5220
  /* @__PURE__ */ jsx(TitleOverlay, {
5145
5221
  title: titleText,
5146
5222
  meta: metaSegments
@@ -7326,6 +7402,207 @@ function displayPath$1(path, home) {
7326
7402
  return path;
7327
7403
  }
7328
7404
  //#endregion
7405
+ //#region src/tui/todos-modal.tsx
7406
+ /** Floor on the modal height — keeps the header + at least a few rows visible on tiny terminals. */
7407
+ const MIN_MODAL_HEIGHT = 12;
7408
+ /** Ceiling on the modal height — prevents a giant list from blanket-filling a tall window. */
7409
+ const MAX_MODAL_HEIGHT$1 = 36;
7410
+ /**
7411
+ * Per-status palette for a row — the glyph and the content text pick
7412
+ * different tones so a quick scan reads the status from the glyph color
7413
+ * while the row content stays in a calm reading tone:
7414
+ *
7415
+ * pending glyph `mute` · text `dim` — queued / not yet started
7416
+ * in_progress glyph `warn` · text `brand` — currently working (loudest)
7417
+ * completed glyph `accent` · text `dim` — done (success glyph, calm text)
7418
+ * cancelled glyph `error` · text `mute` — explicitly dropped
7419
+ *
7420
+ * All tokens come from the active theme — a runtime theme switch
7421
+ * repaints without touching this component.
7422
+ */
7423
+ function statusColors(status, COLOR) {
7424
+ switch (status) {
7425
+ case "in_progress": return {
7426
+ glyph: COLOR.warn,
7427
+ text: COLOR.brand
7428
+ };
7429
+ case "completed": return {
7430
+ glyph: COLOR.accent,
7431
+ text: COLOR.dim
7432
+ };
7433
+ case "cancelled": return {
7434
+ glyph: COLOR.error,
7435
+ text: COLOR.mute
7436
+ };
7437
+ default: return {
7438
+ glyph: COLOR.mute,
7439
+ text: COLOR.dim
7440
+ };
7441
+ }
7442
+ }
7443
+ function TodoRow({ item, rowId }) {
7444
+ const COLOR = useColors();
7445
+ const colors = statusColors(item.status, COLOR);
7446
+ return /* @__PURE__ */ jsx("box", {
7447
+ id: rowId,
7448
+ style: {
7449
+ flexShrink: 0,
7450
+ flexDirection: "row"
7451
+ },
7452
+ children: /* @__PURE__ */ jsxs("text", {
7453
+ wrapMode: "word",
7454
+ children: [
7455
+ /* @__PURE__ */ jsx("span", {
7456
+ fg: colors.glyph,
7457
+ children: TODO_STATUS_GLYPHS[item.status]
7458
+ }),
7459
+ /* @__PURE__ */ jsx("span", {
7460
+ fg: COLOR.mute,
7461
+ children: " "
7462
+ }),
7463
+ /* @__PURE__ */ jsx("span", {
7464
+ fg: colors.text,
7465
+ children: item.content
7466
+ })
7467
+ ]
7468
+ })
7469
+ });
7470
+ }
7471
+ function TodosModal({ session, agent }) {
7472
+ const COLOR = useColors();
7473
+ const { height: termHeight } = useTerminalDimensions();
7474
+ const [, setTick] = useState(0);
7475
+ useEffect(() => {
7476
+ if (!agent) return;
7477
+ return agent.hooks.hook("tool:after", (ctx) => {
7478
+ if (ctx.name === "todowrite") setTick((t) => t + 1);
7479
+ });
7480
+ }, [agent]);
7481
+ const state = useActiveTodos(session);
7482
+ const scrollRef = useRef(null);
7483
+ const inProgressId = state.inProgress?.id ?? null;
7484
+ useEffect(() => {
7485
+ if (!inProgressId) return;
7486
+ const sb = scrollRef.current;
7487
+ if (!sb) return;
7488
+ const handle = requestAnimationFrame(() => {
7489
+ sb.scrollChildIntoView(`todo-row-${inProgressId}`);
7490
+ });
7491
+ return () => cancelAnimationFrame(handle);
7492
+ }, [inProgressId]);
7493
+ const idealHeight = Math.floor((termHeight - 4) * .66);
7494
+ const maxHeight = Math.max(MIN_MODAL_HEIGHT, Math.min(MAX_MODAL_HEIGHT$1, idealHeight));
7495
+ const display = state.todos.length > 0 ? state.todos : state.archive;
7496
+ const total = display.length;
7497
+ return /* @__PURE__ */ jsx(Modal, {
7498
+ title: "todos",
7499
+ bottomTitle: total > 0 ? `${total} item${total === 1 ? "" : "s"} · esc close` : "esc close",
7500
+ rightTitle: display.length > 0 ? /* @__PURE__ */ jsx(CountsBadge, { items: display }) : null,
7501
+ maxWidth: 100,
7502
+ maxHeight,
7503
+ children: total === 0 ? /* @__PURE__ */ jsxs("text", { children: [
7504
+ /* @__PURE__ */ jsx("span", {
7505
+ fg: COLOR.mute,
7506
+ children: "(no todos yet — the agent hasn't called "
7507
+ }),
7508
+ /* @__PURE__ */ jsx("span", {
7509
+ fg: COLOR.warn,
7510
+ children: "todowrite"
7511
+ }),
7512
+ /* @__PURE__ */ jsx("span", {
7513
+ fg: COLOR.mute,
7514
+ children: ")"
7515
+ })
7516
+ ] }) : /* @__PURE__ */ jsx("box", {
7517
+ style: {
7518
+ flexDirection: "column",
7519
+ flexGrow: 1,
7520
+ flexShrink: 1,
7521
+ overflow: "hidden"
7522
+ },
7523
+ children: /* @__PURE__ */ jsx("scrollbox", {
7524
+ ref: scrollRef,
7525
+ focusable: false,
7526
+ stickyScroll: false,
7527
+ style: {
7528
+ flexGrow: 1,
7529
+ flexShrink: 1
7530
+ },
7531
+ children: display.map((item) => /* @__PURE__ */ jsx(TodoRow, {
7532
+ item,
7533
+ rowId: `todo-row-${item.id}`
7534
+ }, item.id))
7535
+ })
7536
+ })
7537
+ });
7538
+ }
7539
+ /**
7540
+ * Right-aligned top-border badge — "{in-progress} in progress ·
7541
+ * {completed} completed". Empty buckets are dropped so a homogeneous
7542
+ * list reads cleanly. Status order is fixed (in-progress first, then
7543
+ * completed) regardless of which buckets are populated — the badge's
7544
+ * shape stays predictable so the eye lands on the live count instantly.
7545
+ */
7546
+ function CountsBadge({ items }) {
7547
+ const COLOR = useColors();
7548
+ const counts = {
7549
+ pending: 0,
7550
+ in_progress: 0,
7551
+ completed: 0,
7552
+ cancelled: 0
7553
+ };
7554
+ for (const item of items) counts[item.status] += 1;
7555
+ const parts = [];
7556
+ if (counts.in_progress) parts.push({
7557
+ count: counts.in_progress,
7558
+ label: "in progress",
7559
+ color: COLOR.warn
7560
+ });
7561
+ if (counts.completed) parts.push({
7562
+ count: counts.completed,
7563
+ label: "completed",
7564
+ color: COLOR.accent
7565
+ });
7566
+ if (counts.pending) parts.push({
7567
+ count: counts.pending,
7568
+ label: "pending",
7569
+ color: COLOR.dim
7570
+ });
7571
+ if (counts.cancelled) parts.push({
7572
+ count: counts.cancelled,
7573
+ label: "cancelled",
7574
+ color: COLOR.error
7575
+ });
7576
+ if (parts.length === 0) return null;
7577
+ return /* @__PURE__ */ jsxs("text", {
7578
+ wrapMode: "none",
7579
+ children: [
7580
+ /* @__PURE__ */ jsx("span", {
7581
+ fg: COLOR.mute,
7582
+ children: " "
7583
+ }),
7584
+ parts.map((p, i) => /* @__PURE__ */ jsxs("span", { children: [
7585
+ i > 0 && /* @__PURE__ */ jsx("span", {
7586
+ fg: COLOR.mute,
7587
+ children: " · "
7588
+ }),
7589
+ /* @__PURE__ */ jsx("span", {
7590
+ fg: p.color,
7591
+ children: String(p.count)
7592
+ }),
7593
+ /* @__PURE__ */ jsx("span", {
7594
+ fg: COLOR.mute,
7595
+ children: ` ${p.label}`
7596
+ })
7597
+ ] }, p.label)),
7598
+ /* @__PURE__ */ jsx("span", {
7599
+ fg: COLOR.mute,
7600
+ children: " "
7601
+ })
7602
+ ]
7603
+ });
7604
+ }
7605
+ //#endregion
7329
7606
  //#region src/tui/turn-details-modal.tsx
7330
7607
  /** Max chars surfaced in the scrollable preview pane. Long enough that almost everything fits without truncation. */
7331
7608
  const PREVIEW_CHAR_MAX = 8e3;
@@ -9425,6 +9702,13 @@ function AppShell() {
9425
9702
  }));
9426
9703
  return;
9427
9704
  }
9705
+ if (matchesBinding(key, keybindings.openTodos) && screen === "chat" && currentSession) {
9706
+ modal.open(/* @__PURE__ */ jsx(TodosModal, {
9707
+ session: sessionRef.current,
9708
+ agent: agentRef.current
9709
+ }));
9710
+ return;
9711
+ }
9428
9712
  if (matchesBinding(key, keybindings.enterSelectTurnMode) && screen === "chat" && !busy && !pendingApproval && !pendingInteraction) {
9429
9713
  enterSelectMode();
9430
9714
  return;
@@ -9558,6 +9842,7 @@ function AppShell() {
9558
9842
  settings,
9559
9843
  onSubmit: onSubmitPrompt,
9560
9844
  session: currentSession,
9845
+ liveSession: sessionRef.current,
9561
9846
  pending: pendingApproval,
9562
9847
  onApproval: resolveHead,
9563
9848
  pendingInteraction,