zidane 5.4.2 → 5.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +15 -0
  2. package/dist/{agent-DxBoKDba.d.ts → agent-Yu8uhpy-.d.ts} +74 -3
  3. package/dist/{agent-DxBoKDba.d.ts.map → agent-Yu8uhpy-.d.ts.map} +1 -1
  4. package/dist/chat.d.ts +49 -6
  5. package/dist/chat.d.ts.map +1 -1
  6. package/dist/chat.js +2 -2
  7. package/dist/{errors-Byb0F8B9.js → errors-CDwtPIMX.js} +4 -2
  8. package/dist/{errors-Byb0F8B9.js.map → errors-CDwtPIMX.js.map} +1 -1
  9. package/dist/{index-BOtXdQkW.d.ts → index-DklfxeYy.d.ts} +8 -2
  10. package/dist/index-DklfxeYy.d.ts.map +1 -0
  11. package/dist/{index-B2VOOijU.d.ts → index-j9tY28ah.d.ts} +16 -3
  12. package/dist/index-j9tY28ah.d.ts.map +1 -0
  13. package/dist/index.d.ts +4 -4
  14. package/dist/index.js +10 -10
  15. package/dist/{interpolate-ERgZUxgg.js → interpolate-CmtjEyRJ.js} +155 -18
  16. package/dist/interpolate-CmtjEyRJ.js.map +1 -0
  17. package/dist/{login-CJbeAadS.js → login-DxyAERe1.js} +3 -3
  18. package/dist/{login-CJbeAadS.js.map → login-DxyAERe1.js.map} +1 -1
  19. package/dist/{mcp-DhmmJfxK.js → mcp-CNUbvbsy.js} +2 -2
  20. package/dist/{mcp-DhmmJfxK.js.map → mcp-CNUbvbsy.js.map} +1 -1
  21. package/dist/mcp.d.ts +1 -1
  22. package/dist/mcp.js +1 -1
  23. package/dist/{messages-D0xT979U.js → messages-fTR19Ga6.js} +2 -2
  24. package/dist/{messages-D0xT979U.js.map → messages-fTR19Ga6.js.map} +1 -1
  25. package/dist/{presets-MCcvxiNT.js → presets-D9IbaI40.js} +2 -2
  26. package/dist/{presets-MCcvxiNT.js.map → presets-D9IbaI40.js.map} +1 -1
  27. package/dist/presets.d.ts +2 -2
  28. package/dist/presets.js +1 -1
  29. package/dist/{providers-x3LZByR5.js → providers-CEzRFYtS.js} +3 -3
  30. package/dist/{providers-x3LZByR5.js.map → providers-CEzRFYtS.js.map} +1 -1
  31. package/dist/providers.d.ts +1 -1
  32. package/dist/providers.js +2 -2
  33. package/dist/session/sqlite.d.ts +1 -1
  34. package/dist/session/sqlite.js +1 -1
  35. package/dist/{session-BHZwxmfr.js → session-kwsNnOmt.js} +2 -2
  36. package/dist/{session-BHZwxmfr.js.map → session-kwsNnOmt.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-BNfyY14s.js → tools-BK2vG9UX.js} +149 -32
  42. package/dist/tools-BK2vG9UX.js.map +1 -0
  43. package/dist/tools.d.ts +2 -2
  44. package/dist/tools.js +1 -1
  45. package/dist/{transcript-anchors-DonKvoh4.d.ts → transcript-anchors-DnaBcJej.d.ts} +52 -8
  46. package/dist/transcript-anchors-DnaBcJej.d.ts.map +1 -0
  47. package/dist/tui.d.ts +4 -2
  48. package/dist/tui.d.ts.map +1 -1
  49. package/dist/tui.js +651 -42
  50. package/dist/tui.js.map +1 -1
  51. package/dist/{turn-operations-TKvy0q29.js → turn-operations-OzKEOXul.js} +240 -52
  52. package/dist/turn-operations-OzKEOXul.js.map +1 -0
  53. package/dist/types.d.ts +2 -2
  54. package/dist/types.js +1 -1
  55. package/docs/ARCHITECTURE.md +16 -3
  56. package/docs/CHAT.md +1 -1
  57. package/docs/SKILL.md +24 -14
  58. package/docs/TUI.md +24 -0
  59. package/package.json +3 -3
  60. package/dist/index-B2VOOijU.d.ts.map +0 -1
  61. package/dist/index-BOtXdQkW.d.ts.map +0 -1
  62. package/dist/interpolate-ERgZUxgg.js.map +0 -1
  63. package/dist/tools-BNfyY14s.js.map +0 -1
  64. package/dist/transcript-anchors-DonKvoh4.d.ts.map +0 -1
  65. package/dist/turn-operations-TKvy0q29.js.map +0 -1
package/dist/tui.js CHANGED
@@ -1,11 +1,11 @@
1
- import { D as resolvePersistDir, T as cleanupPersistedSession, p as createAgent } from "./tools-BNfyY14s.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-CJbeAadS.js";
1
+ import { E as cleanupPersistedSession, O as resolvePersistDir, p as createAgent } from "./tools-BK2vG9UX.js";
2
+ import { s as errorMessage } from "./errors-CDwtPIMX.js";
3
+ import { s as McpOAuthProvider, t as connectMcpServers } from "./mcp-CNUbvbsy.js";
4
+ import { C as summaryToTurn, a as selectFilesFromSession, r as buildPostCompactAttachments, s as compactConversation, t as loginMcpServer } from "./login-DxyAERe1.js";
5
5
  import { n as formatTokenUsage } from "./stats-DgOvY7wd.js";
6
- import { n as loadSession, t as createSession } from "./session-BHZwxmfr.js";
6
+ import { n as loadSession, t as createSession } from "./session-kwsNnOmt.js";
7
7
  import { createTuiStore } from "./session/sqlite.js";
8
- import { $ as useMcpAuthDispatch, $n as blendHsl, $t as isVisible, A as getSafelist, An as summarizeOutcomes, At as clampFps, B as supportsOAuth, Cn as buildEditOutcomesAnnotation, Ct as shortId, D as useSafeModeQueue, Dn as resolveApprovalForPayload, Dt as SETTINGS_CHOICES, E as useSafeModeActions, En as parseEditOutcomesFromResult, Et as DEFAULT_SETTINGS, F as suggestSafelistEntry, Fn as ensureKeybindingsFile, Ft as resolveTheme, Gt as ConfigProvider, H as filterModelCatalog, Ht as DiscoveryProvider, K as discoverProjectMcps, Kn as createFilesCompletionProvider, Kt as useConfig, L as splitPromptSegments, Ln as matchesBinding, Lr as TODO_STATUS_GLYPHS, On as rewriteMultiEditHeader, Ot as SETTINGS_TOGGLES, Pt as resolveChipColor, Q as McpAuthProvider, Qn as useCompletion, Qt as isTurnHighlighted, R as formatPathForCwd, St as fmtTokens, T as SafeModeProvider, Tn as mergeApprovalAndBodyOutcomes, Tr as piIdOf, Tt as useEnabledToggleSet, U as indexOfEntry, Un as createSkillsCompletionProvider, Ut as useDiscovery, V as buildModelCatalog, Vt as createDiscoverySlot, W as buildMcpServers, Wn as uniqueSkillNamesFromReferences, Wt as useDiscoveryOptional, Xt as eventsFromTurns, Y as createFileMcpCredentialStore, Yt as deriveSessionTitle, Zt as isEditErrorResult, _ as turnContextSize, _t as truncateTrailing, a as computeTurnAnchors, ai as buildPlanSystem, an as selectableTurnIds, ar as detectAuth, at as InteractionsProvider, b as defaultSkillScanPaths, bn as previewEditPayload, bt as ageString, c as formatToolCall, ct as createInteractionTools, d as useSelectStyle, dn as turnSelectionOwnership, dr as setProviderCredential, dt as pendingInteractionsFromTurns, en as lastContextSizeFromTurns, er as buildLinearRamp, et as useMcpAuthState, f as useSurfaces, fn as updateToolEventOutcomes, g as finalizeStreamingMarkdownForOwner, gt as hintsLength, h as finalizeStreamingMarkdown, hn as buildUnifiedDiff, ht as clipHintsToWidth, i as turnAsText, ii as buildBuildSystem, ir as shouldAutoCompact, j as isOnSafelist, jn as findGitRoot, jr as accentColor, jt as useSettings, k as addToSafelist, kn as stripEditOutcomesAnnotation, kt as SettingsProvider, l as ThemeProvider, ln as toolCallPreview, m as useTheme, mt as useInteractionsQueue, n as deleteTurnSafely, o as TOOL_DISPLAY, on as stripSpawnTokensLine, pt as useInteractionsActions, qr as useActiveTodos, qt as resolveConfig, r as truncateTurnsAt, rn as marginTopFor, rr as bootTick, rt as splitMarkdownCodeBlocks, s as displayNameFor, sn as sumRunCosts, st as buildResumedToolResultsTurn, tn as listSessionMeta, tr as tryOpenBrowser, tt as getMcpAuthStatus, u as useColors, un as toolResultText, ut as makeRequestInteraction, v as useStreamBuffer, vn as extractEditPayload, w as writeSessionExport, wt as listProjectFiles, x as discoverProjectSkills, xr as modelSupportsReasoning, xt as compactPath, y as buildSkillsConfig, yn as filetypeFromPath, yr as getContextWindow, yt as generateSessionTitle, z as runOAuthLogin } from "./turn-operations-TKvy0q29.js";
8
+ import { $ as useMcpAuthDispatch, $t as isTurnHighlighted, A as getSafelist, An as rewriteMultiEditHeader, At as clampFps, B as supportsOAuth, Cn as summarizeEditPayload, Cr as modelSupportsReasoning, Ct as shortId, D as useSafeModeQueue, Dn as mergeApprovalAndBodyOutcomes, Dr as piIdOf, Dt as SETTINGS_CHOICES, E as useSafeModeActions, Et as DEFAULT_SETTINGS, F as suggestSafelistEntry, Ft as resolveTheme, Gn as createSkillsCompletionProvider, Gt as ConfigProvider, H as filterModelCatalog, Ht as DiscoveryProvider, Jn as createFilesCompletionProvider, Jt as EDIT_TOOL_NAMES, K as discoverProjectMcps, Kn as uniqueSkillNamesFromReferences, Kt as useConfig, L as splitPromptSegments, Ln as ensureKeybindingsFile, Mn as summarizeOutcomes, Nn as findGitRoot, Nr as accentColor, On as parseEditOutcomesFromResult, Ot as SETTINGS_TOGGLES, Pt as resolveChipColor, Q as McpAuthProvider, Qt as isEditErrorResult, R as formatPathForCwd, St as fmtTokens, T as SafeModeProvider, Tn as buildEditOutcomesAnnotation, Tt as useEnabledToggleSet, U as indexOfEntry, Ut as useDiscovery, V as buildModelCatalog, Vt as createDiscoverySlot, W as buildMcpServers, Wt as useDiscoveryOptional, Xt as deriveSessionTitle, Y as createFileMcpCredentialStore, Yr as useActiveTodos, Zt as eventsFromTurns, _ as turnContextSize, _t as truncateTrailing, a as computeTurnAnchors, ar as bootTick, at as InteractionsProvider, b as defaultSkillScanPaths, bn as filetypeFromPath, bt as ageString, c as formatToolCall, cn as sumRunCosts, ct as createInteractionTools, d as useSelectStyle, dn as toolResultText, dt as pendingInteractionsFromTurns, en as isVisible, er as useCompletion, et as useMcpAuthState, f as useSurfaces, fn as turnSelectionOwnership, g as finalizeStreamingMarkdownForOwner, gn as buildUnifiedDiff, gt as hintsLength, h as finalizeStreamingMarkdown, hn as buildContextualDiff, ht as clipHintsToWidth, i as turnAsText, in as marginTopFor, j as isOnSafelist, jn as stripEditOutcomesAnnotation, jt as useSettings, k as addToSafelist, kn as resolveApprovalForPayload, kt as SettingsProvider, l as ThemeProvider, m as useTheme, mt as useInteractionsQueue, n as deleteTurnSafely, nn as listSessionMeta, nr as buildLinearRamp, o as TOOL_DISPLAY, oi as buildBuildSystem, on as selectableTurnIds, or as shouldAutoCompact, pn as updateToolEventOutcomes, pr as setProviderCredential, pt as useInteractionsActions, qt as resolveConfig, r as truncateTurnsAt, rr as tryOpenBrowser, rt as splitMarkdownCodeBlocks, s as displayNameFor, si as buildPlanSystem, sn as stripSpawnTokensLine, sr as detectAuth, st as buildResumedToolResultsTurn, tn as lastContextSizeFromTurns, tr as blendHsl, tt as getMcpAuthStatus, u as useColors, un as toolCallPreview, ut as makeRequestInteraction, v as useStreamBuffer, w as writeSessionExport, wt as listProjectFiles, x as discoverProjectSkills, xn as previewEditPayload, xr as getContextWindow, xt as compactPath, y as buildSkillsConfig, yn as extractEditPayload, yt as generateSessionTitle, z as runOAuthLogin, zn as matchesBinding, zr as TODO_STATUS_GLYPHS } from "./turn-operations-OzKEOXul.js";
9
9
  import { spawn } from "node:child_process";
10
10
  import { Buffer } from "node:buffer";
11
11
  import * as fs from "node:fs";
@@ -223,6 +223,170 @@ function EmptyState$1() {
223
223
  });
224
224
  }
225
225
  //#endregion
226
+ //#region src/tui/cancel-tool-modal.tsx
227
+ /** @jsxImportSource @opentui/react */
228
+ function CancelToolModal({ inFlight, onCancel, onCancelAll, onClose }) {
229
+ const COLOR = useColors();
230
+ const SURFACE = useSurfaces();
231
+ const { width: termWidth } = useTerminalDimensions();
232
+ const [selectedIdx, setSelectedIdx] = useState(0);
233
+ const rows = inFlight;
234
+ const empty = rows.length === 0;
235
+ useEffect(() => {
236
+ if (!empty) return;
237
+ const t = setTimeout(onClose, 500);
238
+ return () => clearTimeout(t);
239
+ }, [empty, onClose]);
240
+ const safeIndex = empty ? 0 : Math.min(selectedIdx, rows.length - 1);
241
+ useKeyboard((key) => {
242
+ if (empty) return;
243
+ if (key.name === "up") {
244
+ setSelectedIdx((i) => ((i - 1) % rows.length + rows.length) % rows.length);
245
+ return;
246
+ }
247
+ if (key.name === "down") {
248
+ setSelectedIdx((i) => (i + 1) % rows.length);
249
+ return;
250
+ }
251
+ if (key.name === "return") {
252
+ const row = rows[safeIndex];
253
+ if (row) onCancel(row.callId, "user-clicked-cancel");
254
+ onClose();
255
+ return;
256
+ }
257
+ if (key.name === "a") {
258
+ onCancelAll();
259
+ onClose();
260
+ }
261
+ });
262
+ const elapsedColWidth = 8;
263
+ const callIdColWidth = 14;
264
+ const childColWidth = 10;
265
+ return /* @__PURE__ */ jsxs(Modal, {
266
+ title: "cancel tool call",
267
+ bottomTitle: empty ? "no calls in flight" : `${rows.length} in flight`,
268
+ maxWidth: Math.min(96, Math.max(64, termWidth - 8)),
269
+ minWidth: 56,
270
+ onClose,
271
+ children: [empty ? /* @__PURE__ */ jsxs("text", {
272
+ fg: COLOR.dim,
273
+ children: [/* @__PURE__ */ jsx("span", {
274
+ fg: COLOR.mute,
275
+ children: "no tool calls are currently in flight — "
276
+ }), /* @__PURE__ */ jsx("span", {
277
+ fg: COLOR.dim,
278
+ children: "nothing to cancel."
279
+ })]
280
+ }) : /* @__PURE__ */ jsx("box", {
281
+ style: {
282
+ flexDirection: "column",
283
+ flexShrink: 0
284
+ },
285
+ children: rows.map((row, i) => /* @__PURE__ */ jsx(CancelToolRow, {
286
+ row,
287
+ isFocused: i === safeIndex,
288
+ highlightBg: SURFACE.selection,
289
+ elapsedColWidth,
290
+ callIdColWidth,
291
+ childColWidth
292
+ }, row.callId))
293
+ }), /* @__PURE__ */ jsxs("text", {
294
+ fg: COLOR.dim,
295
+ children: [
296
+ /* @__PURE__ */ jsx("span", {
297
+ fg: COLOR.warn,
298
+ children: "↑↓"
299
+ }),
300
+ " navigate · ",
301
+ /* @__PURE__ */ jsx("span", {
302
+ fg: COLOR.warn,
303
+ children: "↵"
304
+ }),
305
+ " cancel selected · ",
306
+ /* @__PURE__ */ jsx("span", {
307
+ fg: COLOR.warn,
308
+ children: "a"
309
+ }),
310
+ " cancel all · ",
311
+ /* @__PURE__ */ jsx("span", {
312
+ fg: COLOR.warn,
313
+ children: "esc"
314
+ }),
315
+ " close"
316
+ ]
317
+ })]
318
+ });
319
+ }
320
+ function CancelToolRow({ row, isFocused, highlightBg, elapsedColWidth, callIdColWidth, childColWidth }) {
321
+ const COLOR = useColors();
322
+ const elapsed = formatElapsed(Date.now() - row.startedAt).padStart(elapsedColWidth, " ");
323
+ const idLabel = truncate(row.callId, callIdColWidth).padEnd(callIdColWidth, " ");
324
+ const childLabel = (row.childId ? `· ${row.childId}` : "").padEnd(childColWidth, " ");
325
+ return /* @__PURE__ */ jsx("box", {
326
+ style: {
327
+ height: 1,
328
+ paddingLeft: 1,
329
+ paddingRight: 1,
330
+ flexShrink: 0,
331
+ backgroundColor: isFocused ? highlightBg : void 0
332
+ },
333
+ children: /* @__PURE__ */ jsxs("text", {
334
+ wrapMode: "none",
335
+ children: [
336
+ /* @__PURE__ */ jsx("span", {
337
+ fg: isFocused ? COLOR.brand : COLOR.mute,
338
+ children: isFocused ? "›" : " "
339
+ }),
340
+ /* @__PURE__ */ jsx("span", {
341
+ fg: COLOR.mute,
342
+ children: " "
343
+ }),
344
+ /* @__PURE__ */ jsx("span", {
345
+ fg: isFocused ? COLOR.brand : COLOR.dim,
346
+ children: row.tool
347
+ }),
348
+ /* @__PURE__ */ jsx("span", {
349
+ fg: COLOR.mute,
350
+ children: " "
351
+ }),
352
+ /* @__PURE__ */ jsx("span", {
353
+ fg: COLOR.mute,
354
+ children: idLabel
355
+ }),
356
+ /* @__PURE__ */ jsx("span", {
357
+ fg: COLOR.mute,
358
+ children: " "
359
+ }),
360
+ /* @__PURE__ */ jsx("span", {
361
+ fg: COLOR.mute,
362
+ children: childLabel
363
+ }),
364
+ /* @__PURE__ */ jsx("span", {
365
+ fg: COLOR.warn,
366
+ children: elapsed
367
+ })
368
+ ]
369
+ })
370
+ });
371
+ }
372
+ /**
373
+ * Format a sub-minute duration as `0.3s` / `7.4s`; minute-plus as `2m12s`.
374
+ * Tight + monospace-friendly so the column stays right-aligned.
375
+ */
376
+ function formatElapsed(ms) {
377
+ if (ms < 0) return "0.0s";
378
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
379
+ const totalSeconds = Math.floor(ms / 1e3);
380
+ const minutes = Math.floor(totalSeconds / 60);
381
+ const seconds = totalSeconds % 60;
382
+ return `${minutes}m${String(seconds).padStart(2, "0")}s`;
383
+ }
384
+ function truncate(s, max) {
385
+ if (s.length <= max) return s;
386
+ if (max <= 1) return s.slice(0, max);
387
+ return `${s.slice(0, max - 1)}…`;
388
+ }
389
+ //#endregion
226
390
  //#region src/tui/clipboard.ts
227
391
  /**
228
392
  * Two-pronged clipboard write.
@@ -1721,11 +1885,15 @@ function EditDiffBlock({ payload, dim }) {
1721
1885
  const COLOR = useColors();
1722
1886
  const SURFACE = useSurfaces();
1723
1887
  const mdStyle = useMdStyle();
1888
+ const { settings } = useSettings();
1724
1889
  const filetype = useMemo(() => filetypeFromPath(payload.path), [payload.path]);
1725
1890
  const replaceAllCount = payload.hunks.filter((h) => h.replaceAll).length;
1726
1891
  const hunkBadge = payload.tool === "multi_edit" && payload.hunks.length > 1 ? `${payload.hunks.length} hunks` : null;
1727
- const summary = summarizeOutcomes(payload.outcomes);
1728
- const perHunkMode = !!payload.outcomes && summary.denied + summary.skipped + summary.failed > 0;
1892
+ const outcomeSummary = summarizeOutcomes(payload.outcomes);
1893
+ const hasMixedOutcomes = !!payload.outcomes && outcomeSummary.denied + outcomeSummary.skipped + outcomeSummary.failed > 0;
1894
+ const editSummary = useMemo(() => summarizeEditPayload(payload), [payload]);
1895
+ const perHunkMode = hasMixedOutcomes;
1896
+ const compact = settings.editDiffDisplay === "compact";
1729
1897
  return /* @__PURE__ */ jsxs("box", {
1730
1898
  style: {
1731
1899
  flexDirection: "column",
@@ -1751,6 +1919,24 @@ function EditDiffBlock({ payload, dim }) {
1751
1919
  fg: dim ? COLOR.dim : COLOR.warn,
1752
1920
  children: payload.path
1753
1921
  }),
1922
+ (editSummary.totalAdded > 0 || editSummary.totalRemoved > 0) && /* @__PURE__ */ jsxs(Fragment, { children: [
1923
+ /* @__PURE__ */ jsx("span", {
1924
+ fg: COLOR.mute,
1925
+ children: " · "
1926
+ }),
1927
+ editSummary.totalAdded > 0 && /* @__PURE__ */ jsx("span", {
1928
+ fg: dim ? COLOR.dim : SURFACE.diff.addFg,
1929
+ children: `+${editSummary.totalAdded}`
1930
+ }),
1931
+ editSummary.totalAdded > 0 && editSummary.totalRemoved > 0 && /* @__PURE__ */ jsx("span", {
1932
+ fg: COLOR.mute,
1933
+ children: " "
1934
+ }),
1935
+ editSummary.totalRemoved > 0 && /* @__PURE__ */ jsx("span", {
1936
+ fg: dim ? COLOR.dim : SURFACE.diff.removeFg,
1937
+ children: `−${editSummary.totalRemoved}`
1938
+ })
1939
+ ] }),
1754
1940
  hunkBadge && /* @__PURE__ */ jsx("span", {
1755
1941
  fg: COLOR.mute,
1756
1942
  children: ` · ${hunkBadge}`
@@ -1765,33 +1951,36 @@ function EditDiffBlock({ payload, dim }) {
1765
1951
  children: " · "
1766
1952
  }),
1767
1953
  /* @__PURE__ */ jsx("span", {
1768
- fg: summary.applied > 0 ? COLOR.accent : COLOR.mute,
1769
- children: `${summary.applied} applied`
1954
+ fg: outcomeSummary.applied > 0 ? COLOR.accent : COLOR.mute,
1955
+ children: `${outcomeSummary.applied} applied`
1770
1956
  }),
1771
- summary.denied > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1957
+ outcomeSummary.denied > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1772
1958
  fg: COLOR.mute,
1773
1959
  children: " · "
1774
1960
  }), /* @__PURE__ */ jsx("span", {
1775
1961
  fg: COLOR.error,
1776
- children: `${summary.denied} denied`
1962
+ children: `${outcomeSummary.denied} denied`
1777
1963
  })] }),
1778
- summary.skipped > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1964
+ outcomeSummary.skipped > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1779
1965
  fg: COLOR.mute,
1780
1966
  children: " · "
1781
1967
  }), /* @__PURE__ */ jsx("span", {
1782
1968
  fg: COLOR.warn,
1783
- children: `${summary.skipped} skipped`
1969
+ children: `${outcomeSummary.skipped} skipped`
1784
1970
  })] }),
1785
- summary.failed > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1971
+ outcomeSummary.failed > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1786
1972
  fg: COLOR.mute,
1787
1973
  children: " · "
1788
1974
  }), /* @__PURE__ */ jsx("span", {
1789
1975
  fg: COLOR.error,
1790
- children: `${summary.failed} failed`
1976
+ children: `${outcomeSummary.failed} failed`
1791
1977
  })] })
1792
1978
  ] })
1793
1979
  ]
1794
- }), perHunkMode ? /* @__PURE__ */ jsx("box", {
1980
+ }), compact ? /* @__PURE__ */ jsx(CompactDiffSummary, {
1981
+ summary: editSummary,
1982
+ dim
1983
+ }) : perHunkMode ? /* @__PURE__ */ jsx("box", {
1795
1984
  style: {
1796
1985
  flexDirection: "column",
1797
1986
  flexShrink: 0
@@ -1809,10 +1998,10 @@ function EditDiffBlock({ payload, dim }) {
1809
1998
  dim
1810
1999
  }, i))
1811
2000
  }) : /* @__PURE__ */ jsx("diff", {
1812
- diff: buildUnifiedDiff(payload),
2001
+ diff: payload.priorContent !== void 0 ? buildContextualDiff(payload, payload.priorContent) : buildUnifiedDiff(payload),
1813
2002
  view: "unified",
1814
2003
  wrapMode: "word",
1815
- showLineNumbers: false,
2004
+ showLineNumbers: true,
1816
2005
  ...filetype ? { filetype } : {},
1817
2006
  syntaxStyle: mdStyle,
1818
2007
  addedBg: SURFACE.diff.addBg,
@@ -1828,6 +2017,91 @@ function EditDiffBlock({ payload, dim }) {
1828
2017
  });
1829
2018
  }
1830
2019
  /**
2020
+ * Compact-mode body — one line per hunk under the tool header. Format:
2021
+ *
2022
+ * ` L42 · +2 −1 · old → new`
2023
+ *
2024
+ * Where `L<n>` is the new-file line position (omitted when priorContent
2025
+ * is absent and the position is unknown), and the `old → new` preview
2026
+ * is the FIRST changed line on each side, ASCII-arrowed and truncated
2027
+ * by the renderer's own word-wrap. A pure addition shows `+ new` only;
2028
+ * a pure deletion shows `− old` only.
2029
+ */
2030
+ function CompactDiffSummary({ summary, dim }) {
2031
+ const COLOR = useColors();
2032
+ const SURFACE = useSurfaces();
2033
+ if (summary.hunks.length === 0) return null;
2034
+ return /* @__PURE__ */ jsx("box", {
2035
+ style: {
2036
+ flexDirection: "column",
2037
+ flexShrink: 0
2038
+ },
2039
+ children: summary.hunks.map((h, i) => {
2040
+ const oldPreview = h.firstOld?.trim();
2041
+ const newPreview = h.firstNew?.trim();
2042
+ return /* @__PURE__ */ jsxs("text", {
2043
+ fg: dim ? COLOR.dim : COLOR.mute,
2044
+ wrapMode: "word",
2045
+ children: [
2046
+ /* @__PURE__ */ jsx("span", {
2047
+ fg: COLOR.mute,
2048
+ children: " "
2049
+ }),
2050
+ h.line !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2051
+ fg: dim ? COLOR.dim : COLOR.mute,
2052
+ children: `L${h.line}`
2053
+ }), /* @__PURE__ */ jsx("span", {
2054
+ fg: COLOR.mute,
2055
+ children: " · "
2056
+ })] }),
2057
+ h.added > 0 && /* @__PURE__ */ jsx("span", {
2058
+ fg: dim ? COLOR.dim : SURFACE.diff.addFg,
2059
+ children: `+${h.added}`
2060
+ }),
2061
+ h.added > 0 && h.removed > 0 && /* @__PURE__ */ jsx("span", {
2062
+ fg: COLOR.mute,
2063
+ children: " "
2064
+ }),
2065
+ h.removed > 0 && /* @__PURE__ */ jsx("span", {
2066
+ fg: dim ? COLOR.dim : SURFACE.diff.removeFg,
2067
+ children: `−${h.removed}`
2068
+ }),
2069
+ (oldPreview || newPreview) && /* @__PURE__ */ jsx("span", {
2070
+ fg: COLOR.mute,
2071
+ children: " · "
2072
+ }),
2073
+ oldPreview && newPreview ? /* @__PURE__ */ jsxs(Fragment, { children: [
2074
+ /* @__PURE__ */ jsx("span", {
2075
+ fg: dim ? COLOR.dim : SURFACE.diff.removeFg,
2076
+ children: oldPreview
2077
+ }),
2078
+ /* @__PURE__ */ jsx("span", {
2079
+ fg: COLOR.mute,
2080
+ children: " → "
2081
+ }),
2082
+ /* @__PURE__ */ jsx("span", {
2083
+ fg: dim ? COLOR.dim : SURFACE.diff.addFg,
2084
+ children: newPreview
2085
+ })
2086
+ ] }) : oldPreview ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2087
+ fg: dim ? COLOR.dim : SURFACE.diff.removeFg,
2088
+ children: "− "
2089
+ }), /* @__PURE__ */ jsx("span", {
2090
+ fg: dim ? COLOR.dim : SURFACE.diff.removeFg,
2091
+ children: oldPreview
2092
+ })] }) : newPreview ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2093
+ fg: dim ? COLOR.dim : SURFACE.diff.addFg,
2094
+ children: "+ "
2095
+ }), /* @__PURE__ */ jsx("span", {
2096
+ fg: dim ? COLOR.dim : SURFACE.diff.addFg,
2097
+ children: newPreview
2098
+ })] }) : null
2099
+ ]
2100
+ }, i);
2101
+ })
2102
+ });
2103
+ }
2104
+ /**
1831
2105
  * One hunk inside an `EditDiffBlock` rendered as its own mini-diff with
1832
2106
  * a status badge above it. Used only in the per-hunk view (multi_edit
1833
2107
  * with mixed outcomes) so denied / skipped / failed edits remain
@@ -1835,9 +2109,11 @@ function EditDiffBlock({ payload, dim }) {
1835
2109
  */
1836
2110
  const BADGE_WIDTH = 7;
1837
2111
  function HunkBlock({ index, hunk, outcome, tool, path, filetype, mdStyle, COLOR, SURFACE, dim }) {
2112
+ const { settings } = useSettings();
1838
2113
  const kind = outcome?.kind ?? "applied";
1839
2114
  const reason = outcome?.reason;
1840
2115
  const dimmed = dim || kind !== "applied";
2116
+ const compact = settings.editDiffDisplay === "compact";
1841
2117
  const diffText = useMemo(() => buildUnifiedDiff({
1842
2118
  tool,
1843
2119
  path,
@@ -1847,6 +2123,15 @@ function HunkBlock({ index, hunk, outcome, tool, path, filetype, mdStyle, COLOR,
1847
2123
  tool,
1848
2124
  path
1849
2125
  ]);
2126
+ const hunkStats = useMemo(() => summarizeEditPayload({
2127
+ tool,
2128
+ path,
2129
+ hunks: [hunk]
2130
+ }).hunks[0], [
2131
+ hunk,
2132
+ tool,
2133
+ path
2134
+ ]);
1850
2135
  const badge = kind === "applied" ? {
1851
2136
  label: "applied",
1852
2137
  fg: COLOR.accent
@@ -1881,6 +2166,24 @@ function HunkBlock({ index, hunk, outcome, tool, path, filetype, mdStyle, COLOR,
1881
2166
  fg: badge.fg,
1882
2167
  children: badge.label.padEnd(BADGE_WIDTH)
1883
2168
  }),
2169
+ hunkStats && (hunkStats.added > 0 || hunkStats.removed > 0) && /* @__PURE__ */ jsxs(Fragment, { children: [
2170
+ /* @__PURE__ */ jsx("span", {
2171
+ fg: COLOR.mute,
2172
+ children: " · "
2173
+ }),
2174
+ hunkStats.added > 0 && /* @__PURE__ */ jsx("span", {
2175
+ fg: dim ? COLOR.dim : SURFACE.diff.addFg,
2176
+ children: `+${hunkStats.added}`
2177
+ }),
2178
+ hunkStats.added > 0 && hunkStats.removed > 0 && /* @__PURE__ */ jsx("span", {
2179
+ fg: COLOR.mute,
2180
+ children: " "
2181
+ }),
2182
+ hunkStats.removed > 0 && /* @__PURE__ */ jsx("span", {
2183
+ fg: dim ? COLOR.dim : SURFACE.diff.removeFg,
2184
+ children: `−${hunkStats.removed}`
2185
+ })
2186
+ ] }),
1884
2187
  reason && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1885
2188
  fg: COLOR.mute,
1886
2189
  children: ": "
@@ -1889,11 +2192,11 @@ function HunkBlock({ index, hunk, outcome, tool, path, filetype, mdStyle, COLOR,
1889
2192
  children: reason
1890
2193
  })] })
1891
2194
  ]
1892
- }), /* @__PURE__ */ jsx("diff", {
2195
+ }), !compact && /* @__PURE__ */ jsx("diff", {
1893
2196
  diff: diffText,
1894
2197
  view: "unified",
1895
2198
  wrapMode: "word",
1896
- showLineNumbers: false,
2199
+ showLineNumbers: true,
1897
2200
  ...filetype ? { filetype } : {},
1898
2201
  syntaxStyle: mdStyle,
1899
2202
  addedBg: SURFACE.diff.addBg,
@@ -1913,7 +2216,7 @@ function ToolCallBlock({ event, display, dim }) {
1913
2216
  const COLOR = useColors();
1914
2217
  const mdStyle = useMdStyle();
1915
2218
  const name = event.tool ?? "";
1916
- const verb = displayNameFor(name);
2219
+ const verb = displayNameFor(name, event.input);
1917
2220
  const pretty = useMemo(() => {
1918
2221
  if (!event.input) return null;
1919
2222
  try {
@@ -7612,6 +7915,31 @@ const PREVIEW_CHAR_MAX = 8e3;
7612
7915
  * keeps a comfortable shape rather than stretching to the full height.
7613
7916
  */
7614
7917
  const MAX_MODAL_HEIGHT = 28;
7918
+ const EDIT_TEXTAREA_BINDINGS = [
7919
+ ...defaultTextareaKeyBindings.filter((b) => b.name !== "return" && !(b.name === "a" && b.ctrl && !b.shift && !b.meta)),
7920
+ {
7921
+ name: "a",
7922
+ ctrl: true,
7923
+ action: "select-all"
7924
+ },
7925
+ {
7926
+ name: "return",
7927
+ action: "submit"
7928
+ },
7929
+ {
7930
+ name: "return",
7931
+ shift: true,
7932
+ action: "newline"
7933
+ }
7934
+ ];
7935
+ /**
7936
+ * Extract the editable text from a turn — joins all `text` blocks.
7937
+ * Non-text blocks (tool_call, tool_result, thinking, etc.) are structural
7938
+ * and not included in the editable surface.
7939
+ */
7940
+ function editableText(turn) {
7941
+ return turn.content.filter((b) => b.type === "text").map((b) => b.text).join("\n\n");
7942
+ }
7615
7943
  function TurnDetailsModal({ turn, index, total, actions, keybindings }) {
7616
7944
  const COLOR = useColors();
7617
7945
  const modal = useModal();
@@ -7621,6 +7949,9 @@ function TurnDetailsModal({ turn, index, total, actions, keybindings }) {
7621
7949
  const bottomTitle = `${index - 1} before · ${total - index} after`;
7622
7950
  const [pending, setPending] = useState(null);
7623
7951
  const [copyStatus, setCopyStatus] = useState("idle");
7952
+ const [editing, setEditing] = useState(false);
7953
+ const textareaRef = useRef(null);
7954
+ const hasEditableText = turn.content.some((b) => b.type === "text");
7624
7955
  const commitFork = () => {
7625
7956
  modal.close();
7626
7957
  actions.onFork(turn.id);
@@ -7636,7 +7967,16 @@ function TurnDetailsModal({ turn, index, total, actions, keybindings }) {
7636
7967
  }
7637
7968
  setCopyStatus(writeToClipboard(fullText) ? "copied" : "failed");
7638
7969
  };
7970
+ const commitEdit = () => {
7971
+ const newText = textareaRef.current?.plainText ?? "";
7972
+ modal.close();
7973
+ actions.onEdit(turn.id, newText);
7974
+ };
7639
7975
  useKeyboard((key) => {
7976
+ if (editing) {
7977
+ if (key.name === "escape") setEditing(false);
7978
+ return;
7979
+ }
7640
7980
  if (key.name === "escape" && pending) {
7641
7981
  setPending(null);
7642
7982
  return;
@@ -7658,15 +7998,22 @@ function TurnDetailsModal({ turn, index, total, actions, keybindings }) {
7658
7998
  handleCopy();
7659
7999
  return;
7660
8000
  }
8001
+ if (matchesBinding(key, keybindings.turnEdit)) {
8002
+ if (!hasEditableText) return;
8003
+ setPending(null);
8004
+ setCopyStatus("idle");
8005
+ setEditing(true);
8006
+ return;
8007
+ }
7661
8008
  if (pending) setPending(null);
7662
8009
  });
7663
8010
  return /* @__PURE__ */ jsxs(Modal, {
7664
- title: `turn ${index} / ${total} · ${turn.role}`,
7665
- bottomTitle,
8011
+ title: editing ? `edit turn ${index} / ${total} · ${turn.role}` : `turn ${index} / ${total} · ${turn.role}`,
8012
+ bottomTitle: editing ? void 0 : bottomTitle,
7666
8013
  maxHeight: MAX_MODAL_HEIGHT,
7667
- disableEscape: pending !== null,
8014
+ disableEscape: editing || pending !== null,
7668
8015
  children: [
7669
- /* @__PURE__ */ jsxs("text", {
8016
+ !editing && /* @__PURE__ */ jsxs("text", {
7670
8017
  fg: COLOR.dim,
7671
8018
  children: [
7672
8019
  /* @__PURE__ */ jsx("span", {
@@ -7705,7 +8052,7 @@ function TurnDetailsModal({ turn, index, total, actions, keybindings }) {
7705
8052
  ] })
7706
8053
  ]
7707
8054
  }),
7708
- /* @__PURE__ */ jsxs("text", {
8055
+ !editing && /* @__PURE__ */ jsxs("text", {
7709
8056
  fg: COLOR.dim,
7710
8057
  children: [/* @__PURE__ */ jsx("span", {
7711
8058
  fg: COLOR.mute,
@@ -7715,7 +8062,31 @@ function TurnDetailsModal({ turn, index, total, actions, keybindings }) {
7715
8062
  children: summary
7716
8063
  })]
7717
8064
  }),
7718
- /* @__PURE__ */ jsx("box", {
8065
+ editing ? /* @__PURE__ */ jsx("box", {
8066
+ title: " edit ",
8067
+ style: {
8068
+ border: true,
8069
+ borderColor: COLOR.borderActive,
8070
+ paddingLeft: 1,
8071
+ paddingRight: 1,
8072
+ flexDirection: "column",
8073
+ flexGrow: 1,
8074
+ flexShrink: 1,
8075
+ minHeight: 5
8076
+ },
8077
+ children: /* @__PURE__ */ jsx("textarea", {
8078
+ ref: textareaRef,
8079
+ focused: true,
8080
+ keyBindings: EDIT_TEXTAREA_BINDINGS,
8081
+ initialValue: editableText(turn),
8082
+ placeholder: "enter text…",
8083
+ style: {
8084
+ flexGrow: 1,
8085
+ height: "100%"
8086
+ },
8087
+ onSubmit: commitEdit
8088
+ })
8089
+ }) : /* @__PURE__ */ jsx("box", {
7719
8090
  title: " preview ",
7720
8091
  style: {
7721
8092
  border: true,
@@ -7740,13 +8111,34 @@ function TurnDetailsModal({ turn, index, total, actions, keybindings }) {
7740
8111
  children: "— no text content —"
7741
8112
  })
7742
8113
  }),
7743
- /* @__PURE__ */ jsx(ActionRow, {
8114
+ editing ? /* @__PURE__ */ jsxs("text", {
8115
+ fg: COLOR.dim,
8116
+ children: [
8117
+ /* @__PURE__ */ jsx("span", {
8118
+ fg: COLOR.warn,
8119
+ children: "↵"
8120
+ }),
8121
+ " save · ",
8122
+ /* @__PURE__ */ jsx("span", {
8123
+ fg: COLOR.warn,
8124
+ children: "shift+↵"
8125
+ }),
8126
+ " newline · ",
8127
+ /* @__PURE__ */ jsx("span", {
8128
+ fg: COLOR.warn,
8129
+ children: "esc"
8130
+ }),
8131
+ " cancel"
8132
+ ]
8133
+ }) : /* @__PURE__ */ jsx(ActionRow, {
7744
8134
  pending,
7745
8135
  copyStatus,
7746
8136
  canCopy: fullText.length > 0,
8137
+ canEdit: hasEditableText,
7747
8138
  forkKey: keybindings.turnFork,
7748
8139
  deleteKey: keybindings.turnDelete,
7749
- copyKey: keybindings.turnCopy
8140
+ copyKey: keybindings.turnCopy,
8141
+ editKey: keybindings.turnEdit
7750
8142
  })
7751
8143
  ]
7752
8144
  });
@@ -7758,7 +8150,7 @@ function TurnDetailsModal({ turn, index, total, actions, keybindings }) {
7758
8150
  * by accident. The copy result rides the same row when present — same
7759
8151
  * geometry, no layout shift.
7760
8152
  */
7761
- function ActionRow({ pending, copyStatus, canCopy, forkKey, deleteKey, copyKey }) {
8153
+ function ActionRow({ pending, copyStatus, canCopy, canEdit, forkKey, deleteKey, copyKey, editKey }) {
7762
8154
  const COLOR = useColors();
7763
8155
  if (pending === "fork") return /* @__PURE__ */ jsxs("text", {
7764
8156
  fg: COLOR.dim,
@@ -7818,6 +8210,11 @@ function ActionRow({ pending, copyStatus, canCopy, forkKey, deleteKey, copyKey }
7818
8210
  children: deleteKey
7819
8211
  }),
7820
8212
  " delete · ",
8213
+ /* @__PURE__ */ jsx("span", {
8214
+ fg: canEdit ? COLOR.warn : COLOR.mute,
8215
+ children: editKey
8216
+ }),
8217
+ canEdit ? " edit · " : " (no text) · ",
7821
8218
  /* @__PURE__ */ jsx("span", {
7822
8219
  fg: COLOR.warn,
7823
8220
  children: "esc"
@@ -7858,6 +8255,11 @@ function ActionRow({ pending, copyStatus, canCopy, forkKey, deleteKey, copyKey }
7858
8255
  children: copyKey
7859
8256
  }),
7860
8257
  canCopy ? " copy · " : " (nothing to copy) · ",
8258
+ /* @__PURE__ */ jsx("span", {
8259
+ fg: canEdit ? COLOR.warn : COLOR.mute,
8260
+ children: editKey
8261
+ }),
8262
+ canEdit ? " edit · " : " (no text) · ",
7861
8263
  /* @__PURE__ */ jsx("span", {
7862
8264
  fg: COLOR.warn,
7863
8265
  children: "esc"
@@ -7919,6 +8321,42 @@ async function launchEditor(path) {
7919
8321
  }).unref();
7920
8322
  }
7921
8323
  /**
8324
+ * Session-metadata key under which the TUI persists the user's "pinned"
8325
+ * active skills — the set the user toggled on via `/skill-name` and
8326
+ * expects to stay active across run boundaries (and TUI restarts).
8327
+ *
8328
+ * Separate from the agent's `skillActivationState` for two reasons:
8329
+ * 1. The framework's run-end pass deactivates everything; the pinned
8330
+ * set survives that pass so the next prompt can re-activate.
8331
+ * 2. The agent's session-resume rehydrator only sees `skills_use`
8332
+ * `tool_call` blocks in history. Slash-command activations bypass
8333
+ * that path; this metadata key is the system-of-record for them.
8334
+ */
8335
+ const ACTIVE_SKILLS_META_KEY = "zidane.activeSkills";
8336
+ /**
8337
+ * Read the pinned-skills set out of session metadata. Tolerant by
8338
+ * design — older sessions, manually-edited metadata, or a future
8339
+ * type drift all degrade to "no pins", never throw. Returns a fresh
8340
+ * Set so callers can mutate-and-store without aliasing the input.
8341
+ */
8342
+ function readPinnedSkills(raw) {
8343
+ if (!Array.isArray(raw)) return /* @__PURE__ */ new Set();
8344
+ return new Set(raw.filter((v) => typeof v === "string" && v.length > 0));
8345
+ }
8346
+ /**
8347
+ * Mirror the pinned-skills set into session metadata. Sorted for
8348
+ * stable on-disk ordering (diffs / debugging). `session.setMeta`
8349
+ * already routes through `session:meta` hooks, so observers see a
8350
+ * single normalized payload regardless of insertion order.
8351
+ *
8352
+ * Imported lazily via the session ref — calling sites already
8353
+ * captured `session` in scope, so we accept it as an argument
8354
+ * rather than re-fetching from a ref.
8355
+ */
8356
+ function persistPinnedSkills(session, pins) {
8357
+ session.setMeta(ACTIVE_SKILLS_META_KEY, Array.from(pins).sort());
8358
+ }
8359
+ /**
7922
8360
  * Filter a `multi_edit` (or single `edit` / `write_file`) input's hunk
7923
8361
  * list down to the approved subset, in original order. Used by the
7924
8362
  * approval gate to rebind `ctx.input.edits` after a partial decision —
@@ -8239,6 +8677,60 @@ function AppShell() {
8239
8677
  const agentRef = useRef(null);
8240
8678
  const sessionRef = useRef(null);
8241
8679
  /**
8680
+ * Live registry of in-flight tool calls — populated by `tool:before`
8681
+ * (and `child:tool:before`), drained by `tool:after` / `tool:error` /
8682
+ * `tool:cancelled` (and their `child:*` siblings). Drives the
8683
+ * "cancel tool call" picker (see {@link CancelToolModal}).
8684
+ *
8685
+ * Mirrored in `inFlightToolsRef` for callbacks that read the latest
8686
+ * value synchronously without listing `inFlightTools` in their deps —
8687
+ * the picker open path needs both: a state snapshot for the modal's
8688
+ * rendered rows, and the live ref for any subsequent cancel actions
8689
+ * that fire after the snapshot was taken.
8690
+ *
8691
+ * Excludes `mcp:tool:before` / `mcp:tool:after` deliberately for
8692
+ * v1 — `agent.cancelTool(callId)` operates on the unified loop-side
8693
+ * callId registry, which MCP tools also live in (they're dispatched
8694
+ * through the same `executeSingleTool`), so the cancel works for
8695
+ * them too; we just don't surface them in the picker UI yet to
8696
+ * avoid drowning the list with high-cardinality MCP transient calls.
8697
+ */
8698
+ const [inFlightTools, setInFlightTools] = useState([]);
8699
+ const inFlightToolsRef = useRef([]);
8700
+ inFlightToolsRef.current = inFlightTools;
8701
+ /**
8702
+ * Names of currently-active skills, tracked via `skills:activate` /
8703
+ * `skills:deactivate` hooks. Drives the footer's "✦ N skill(s)"
8704
+ * chip — the user's only passive surface for noticing that a skill
8705
+ * (and its `allowed-tools` restrictions) is in effect. Cleared on
8706
+ * session teardown alongside the rest of the per-session live state.
8707
+ *
8708
+ * Stored as a Set rather than an array so dedup is structural (a
8709
+ * runaway `skills:activate` for the same name doesn't inflate the
8710
+ * count). React state is the snapshot we render against; a fresh
8711
+ * Set per update gives React identity-based change detection.
8712
+ */
8713
+ const [activeSkillNames, setActiveSkillNames] = useState(() => /* @__PURE__ */ new Set());
8714
+ /**
8715
+ * Mirror of {@link activeSkillNames} for synchronous reads in
8716
+ * `onSubmitPrompt`. The submit path runs outside React's render cycle
8717
+ * and needs to pre-activate every user-pinned skill before
8718
+ * `agent.run()` — listing the state in `useCallback`'s deps would
8719
+ * re-bind the handler on every activation change, which would
8720
+ * invalidate the textarea's submit binding on every `/skill` trigger.
8721
+ * The ref keeps the binding stable and the read fresh.
8722
+ */
8723
+ const activeSkillNamesRef = useRef(activeSkillNames);
8724
+ activeSkillNamesRef.current = activeSkillNames;
8725
+ const registerInFlightTool = useCallback((entry) => {
8726
+ setInFlightTools((prev) => {
8727
+ return [...prev.filter((e) => e.callId !== entry.callId), entry];
8728
+ });
8729
+ }, []);
8730
+ const unregisterInFlightTool = useCallback((callId) => {
8731
+ setInFlightTools((prev) => prev.filter((e) => e.callId !== callId));
8732
+ }, []);
8733
+ /**
8242
8734
  * In-flight auto-compaction promise. Held in a ref so the next
8243
8735
  * `onSubmitPrompt` invocation can `await` it before calling
8244
8736
  * `agent.run()` — this is what prevents a user-submitted prompt from
@@ -8491,9 +8983,14 @@ function AppShell() {
8491
8983
  agent.hooks.hook("stream:thinking", ({ delta, turnId }) => stream.queueStreamDelta("thinking", delta, { turnId }));
8492
8984
  agent.hooks.hook("stream:text", ({ delta, turnId }) => stream.queueStreamDelta("markdown", delta, { turnId }));
8493
8985
  agent.hooks.hook("tool:before", async ({ callId, name, input, turnId }) => {
8986
+ registerInFlightTool({
8987
+ callId,
8988
+ tool: name,
8989
+ startedAt: Date.now()
8990
+ });
8494
8991
  if (pendingAnnotationsRef.current.has(callId)) return;
8495
8992
  let priorContent;
8496
- if (name === "write_file" && agent.handle && typeof input.path === "string") try {
8993
+ if (EDIT_TOOL_NAMES.has(name) && agent.handle && typeof input.path === "string") try {
8497
8994
  priorContent = await agent.execution.readFile(agent.handle, input.path);
8498
8995
  } catch {}
8499
8996
  const edit = extractEditPayload(name, input, priorContent);
@@ -8508,6 +9005,7 @@ function AppShell() {
8508
9005
  });
8509
9006
  });
8510
9007
  agent.hooks.hook("tool:after", ({ callId, name, result, turnId }) => {
9008
+ unregisterInFlightTool(callId);
8511
9009
  const raw = toolResultText(result);
8512
9010
  const text = name === "spawn" ? stripSpawnTokensLine(raw) : raw;
8513
9011
  stream.appendImmediate({
@@ -8518,6 +9016,31 @@ function AppShell() {
8518
9016
  turnId
8519
9017
  });
8520
9018
  });
9019
+ agent.hooks.hook("tool:error", ({ callId }) => {
9020
+ unregisterInFlightTool(callId);
9021
+ });
9022
+ agent.hooks.hook("tool:cancelled", ({ callId }) => {
9023
+ unregisterInFlightTool(callId);
9024
+ });
9025
+ agent.hooks.hook("skills:activate", ({ skill }) => {
9026
+ setActiveSkillNames((prev) => {
9027
+ if (prev.has(skill.name)) return prev;
9028
+ const next = new Set(prev);
9029
+ next.add(skill.name);
9030
+ persistPinnedSkills(session, next);
9031
+ return next;
9032
+ });
9033
+ });
9034
+ agent.hooks.hook("skills:deactivate", ({ skill, reason }) => {
9035
+ if (reason === "run-end") return;
9036
+ setActiveSkillNames((prev) => {
9037
+ if (!prev.has(skill.name)) return prev;
9038
+ const next = new Set(prev);
9039
+ next.delete(skill.name);
9040
+ persistPinnedSkills(session, next);
9041
+ return next;
9042
+ });
9043
+ });
8521
9044
  agent.hooks.hook("mcp:tool:after", ({ callId, displayName, result, turnId }) => {
8522
9045
  stream.appendImmediate({
8523
9046
  kind: "tool-result",
@@ -8580,6 +9103,12 @@ function AppShell() {
8580
9103
  });
8581
9104
  });
8582
9105
  agent.hooks.hook("child:tool:before", ({ callId, name, input, childId, depth, turnId, priorContent }) => {
9106
+ registerInFlightTool({
9107
+ callId,
9108
+ tool: name,
9109
+ startedAt: Date.now(),
9110
+ childId
9111
+ });
8583
9112
  if (pendingAnnotationsRef.current.has(callId)) return;
8584
9113
  const edit = extractEditPayload(name, input, priorContent);
8585
9114
  stream.appendImmediate({
@@ -8595,6 +9124,7 @@ function AppShell() {
8595
9124
  });
8596
9125
  });
8597
9126
  agent.hooks.hook("child:tool:after", ({ callId, name, result, childId, depth, turnId }) => {
9127
+ unregisterInFlightTool(callId);
8598
9128
  stream.appendImmediate({
8599
9129
  kind: "tool-result",
8600
9130
  text: toolResultText(result),
@@ -8605,6 +9135,12 @@ function AppShell() {
8605
9135
  turnId
8606
9136
  });
8607
9137
  });
9138
+ agent.hooks.hook("child:tool:error", ({ callId }) => {
9139
+ unregisterInFlightTool(callId);
9140
+ });
9141
+ agent.hooks.hook("child:tool:cancelled", ({ callId }) => {
9142
+ unregisterInFlightTool(callId);
9143
+ });
8608
9144
  agent.hooks.hook("child:stream:end", ({ childId }) => {
8609
9145
  stream.flushAndUpdate((prev) => finalizeStreamingMarkdownForOwner(prev, childId));
8610
9146
  });
@@ -8621,7 +9157,9 @@ function AppShell() {
8621
9157
  config.prefix,
8622
9158
  interactions,
8623
9159
  dataDir,
8624
- mcpCredentialStore
9160
+ mcpCredentialStore,
9161
+ registerInFlightTool,
9162
+ unregisterInFlightTool
8625
9163
  ]);
8626
9164
  const refreshSessions = useCallback(async () => {
8627
9165
  const list = await listSessionMeta(store, settings.showAllProjects ? void 0 : { projectRoot: projectDir });
@@ -8662,6 +9200,8 @@ function AppShell() {
8662
9200
  runningRef.current = false;
8663
9201
  sessionSafelistRef.current.clear();
8664
9202
  pendingAnnotationsRef.current.clear();
9203
+ setInFlightTools([]);
9204
+ setActiveSkillNames(/* @__PURE__ */ new Set());
8665
9205
  }, [
8666
9206
  stream,
8667
9207
  denyAll,
@@ -8739,6 +9279,7 @@ function AppShell() {
8739
9279
  });
8740
9280
  sessionRef.current = session;
8741
9281
  agentRef.current = buildAgent(session, key);
9282
+ setActiveSkillNames(readPinnedSkills(session.metadata["zidane.activeSkills"]));
8742
9283
  setEvents(eventsFromTurns(session.turns, session.runs));
8743
9284
  const replayedTokens = lastContextSizeFromTurns(session.turns, session.runs);
8744
9285
  setLastInputTokens(replayedTokens);
@@ -9094,7 +9635,8 @@ function AppShell() {
9094
9635
  ...refSpans.length > 0 ? { refs: refSpans } : {}
9095
9636
  });
9096
9637
  if (autoCompactInFlightRef.current) await autoCompactInFlightRef.current.catch(() => {});
9097
- const skillNames = uniqueSkillNamesFromReferences(references);
9638
+ const newRefs = uniqueSkillNamesFromReferences(references);
9639
+ const skillNames = Array.from(new Set([...activeSkillNamesRef.current, ...newRefs]));
9098
9640
  for (const name of skillNames) try {
9099
9641
  await agent.activateSkill(name);
9100
9642
  } catch (err) {
@@ -9332,6 +9874,36 @@ function AppShell() {
9332
9874
  return nextTurns.some((t) => t.id === prev) ? prev : null;
9333
9875
  });
9334
9876
  }, []);
9877
+ const onEditTurn = useCallback(async (turnId, newText) => {
9878
+ const session = sessionRef.current;
9879
+ if (!session) return;
9880
+ const turn = session.turns.find((t) => t.id === turnId);
9881
+ if (!turn) return;
9882
+ if (!turn.content.some((b) => b.type === "text")) return;
9883
+ const nonTextBlocks = turn.content.filter((b) => b.type !== "text");
9884
+ const firstTextIdx = turn.content.findIndex((b) => b.type === "text");
9885
+ const updatedContent = [...nonTextBlocks];
9886
+ if (newText.trim()) updatedContent.splice(firstTextIdx >= 0 ? Math.min(firstTextIdx, updatedContent.length) : 0, 0, {
9887
+ type: "text",
9888
+ text: newText
9889
+ });
9890
+ turn.content = updatedContent;
9891
+ session.setTurns([...session.turns]);
9892
+ try {
9893
+ await session.save();
9894
+ } catch (err) {
9895
+ debugLog("edit: save failed", err);
9896
+ return;
9897
+ }
9898
+ setEvents(eventsFromTurns(session.turns, session.runs));
9899
+ const replayedTokens = lastContextSizeFromTurns(session.turns, session.runs);
9900
+ setLastInputTokens(replayedTokens);
9901
+ lastInputTokensRef.current = replayedTokens;
9902
+ setCurrentSession((prev) => prev ? {
9903
+ ...prev,
9904
+ updatedAt: Date.now()
9905
+ } : prev);
9906
+ }, []);
9335
9907
  /**
9336
9908
  * Identity of the session row the user has focused on the sessions
9337
9909
  * screen — single source of truth. `SessionsScreen` is rendered fully
@@ -9628,7 +10200,8 @@ function AppShell() {
9628
10200
  total: turnIds.length,
9629
10201
  actions: {
9630
10202
  onFork: onForkTurn,
9631
- onDelete: onDeleteTurn
10203
+ onDelete: onDeleteTurn,
10204
+ onEdit: onEditTurn
9632
10205
  },
9633
10206
  keybindings
9634
10207
  }));
@@ -9638,6 +10211,7 @@ function AppShell() {
9638
10211
  turnIds,
9639
10212
  onForkTurn,
9640
10213
  onDeleteTurn,
10214
+ onEditTurn,
9641
10215
  keybindings
9642
10216
  ]);
9643
10217
  useKeyboard((key) => {
@@ -9746,6 +10320,21 @@ function AppShell() {
9746
10320
  onCycleAgent();
9747
10321
  return;
9748
10322
  }
10323
+ if (matchesBinding(key, keybindings.cancelToolCall) && screen === "chat" && busy && !pendingApproval) {
10324
+ const snapshot = inFlightToolsRef.current;
10325
+ if (snapshot.length === 0) return;
10326
+ modal.open(/* @__PURE__ */ jsx(CancelToolModal, {
10327
+ inFlight: snapshot,
10328
+ onCancel: (callId, reason) => agentRef.current?.cancelTool(callId, reason) ?? false,
10329
+ onCancelAll: () => {
10330
+ const agent = agentRef.current;
10331
+ if (!agent) return;
10332
+ for (const entry of inFlightToolsRef.current) agent.cancelTool(entry.callId, "user-cancelled-all");
10333
+ },
10334
+ onClose: () => modal.close()
10335
+ }));
10336
+ return;
10337
+ }
9749
10338
  if (key.name !== "escape") return;
9750
10339
  if (busy || pendingApproval) return onAbort();
9751
10340
  if (popupOpenRef.current) return;
@@ -9777,7 +10366,10 @@ function AppShell() {
9777
10366
  effortKeyColor: COLOR.warn,
9778
10367
  agentLabel: pickedAgent.label,
9779
10368
  agentColor: accentColor(pickedAgent.accent, COLOR),
9780
- keybindings
10369
+ keybindings,
10370
+ inFlightToolCount: inFlightTools.length,
10371
+ activeSkillCount: activeSkillNames.size,
10372
+ skillsChipColor: COLOR.brand
9781
10373
  }), [
9782
10374
  screen,
9783
10375
  busy,
@@ -9790,7 +10382,9 @@ function AppShell() {
9790
10382
  pickedAgent,
9791
10383
  COLOR,
9792
10384
  modelHasReasoning,
9793
- keybindings
10385
+ keybindings,
10386
+ inFlightTools,
10387
+ activeSkillNames
9794
10388
  ]);
9795
10389
  const queuedMessagePreviews = useMemo(() => messageQueue.map((m) => ({
9796
10390
  text: m.prompt,
@@ -9909,7 +10503,7 @@ function effortForModel(descriptor, modelId, remembered) {
9909
10503
  * secondary `/n` chord with the current effort label, surfacing the
9910
10504
  * effort picker as a discoverable, in-place affordance.
9911
10505
  */
9912
- function buildHints({ screen, busy, pending, pendingInteractionLive, pendingInteractionResumed, currentSession, hasMultipleAgents, modelLabel, modelColor, effortLabel, effortColor, effortKeyColor, agentLabel, agentColor, keybindings }) {
10506
+ function buildHints({ screen, busy, pending, pendingInteractionLive, pendingInteractionResumed, currentSession, hasMultipleAgents, modelLabel, modelColor, effortLabel, effortColor, effortKeyColor, agentLabel, agentColor, keybindings, inFlightToolCount, activeSkillCount, skillsChipColor }) {
9913
10507
  if (pending) return [
9914
10508
  {
9915
10509
  key: "↑↓",
@@ -9952,10 +10546,18 @@ function buildHints({ screen, busy, pending, pendingInteractionLive, pendingInte
9952
10546
  label: "leave for later"
9953
10547
  }
9954
10548
  ];
9955
- if (busy) return [{
9956
- key: "esc",
9957
- label: "abort"
9958
- }];
10549
+ if (busy) {
10550
+ const baseBusyHints = [];
10551
+ if (inFlightToolCount > 0) baseBusyHints.push({
10552
+ key: keybindings.cancelToolCall,
10553
+ label: inFlightToolCount === 1 ? "cancel tool" : `cancel tool (${inFlightToolCount})`
10554
+ });
10555
+ baseBusyHints.push({
10556
+ key: "esc",
10557
+ label: "abort"
10558
+ });
10559
+ return baseBusyHints;
10560
+ }
9959
10561
  if (screen === "auth") return [
9960
10562
  {
9961
10563
  key: "↑↓",
@@ -10003,6 +10605,12 @@ function buildHints({ screen, busy, pending, pendingInteractionLive, pendingInte
10003
10605
  labelColor: effortColor
10004
10606
  } } : {}
10005
10607
  } : null;
10608
+ const skillsChip = activeSkillCount > 0 ? {
10609
+ key: "✦",
10610
+ keyColor: skillsChipColor,
10611
+ label: activeSkillCount === 1 ? "1 skill" : `${activeSkillCount} skills`,
10612
+ labelColor: skillsChipColor
10613
+ } : null;
10006
10614
  return [
10007
10615
  ...hasMultipleAgents ? [{
10008
10616
  key: keybindings.cycleAgent,
@@ -10010,6 +10618,7 @@ function buildHints({ screen, busy, pending, pendingInteractionLive, pendingInte
10010
10618
  labelColor: agentColor
10011
10619
  }] : [],
10012
10620
  ...modelHint ? [modelHint] : [],
10621
+ ...skillsChip ? [skillsChip] : [],
10013
10622
  ...currentSession ? [{
10014
10623
  key: keybindings.openSessionDetails,
10015
10624
  label: "session"