zidane 5.4.2 → 5.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +45 -1
  2. package/dist/{agent-DxBoKDba.d.ts → agent-CvImMxMQ.d.ts} +256 -5
  3. package/dist/agent-CvImMxMQ.d.ts.map +1 -0
  4. package/dist/chat.d.ts +137 -16
  5. package/dist/chat.d.ts.map +1 -1
  6. package/dist/chat.js +3 -2
  7. package/dist/contexts/docker.d.ts +1 -1
  8. package/dist/contexts-DhmMlT2W.js +472 -0
  9. package/dist/contexts-DhmMlT2W.js.map +1 -0
  10. package/dist/contexts.d.ts +3 -3
  11. package/dist/contexts.js +1 -1
  12. package/dist/{errors-Byb0F8B9.js → errors-CDwtPIMX.js} +4 -2
  13. package/dist/{errors-Byb0F8B9.js.map → errors-CDwtPIMX.js.map} +1 -1
  14. package/dist/{index-BOtXdQkW.d.ts → index-B0uc2C5x.d.ts} +9 -3
  15. package/dist/index-B0uc2C5x.d.ts.map +1 -0
  16. package/dist/{index-BiO_5Hm4.d.ts → index-CbS75MD3.d.ts} +2 -2
  17. package/dist/index-CbS75MD3.d.ts.map +1 -0
  18. package/dist/{index-B2VOOijU.d.ts → index-CtXksgqb.d.ts} +73 -4
  19. package/dist/index-CtXksgqb.d.ts.map +1 -0
  20. package/dist/index.d.ts +6 -6
  21. package/dist/index.js +11 -11
  22. package/dist/{interpolate-ERgZUxgg.js → interpolate-BaaKaKzN.js} +156 -19
  23. package/dist/interpolate-BaaKaKzN.js.map +1 -0
  24. package/dist/{login-CJbeAadS.js → login-iTy-0wYz.js} +3 -3
  25. package/dist/{login-CJbeAadS.js.map → login-iTy-0wYz.js.map} +1 -1
  26. package/dist/{mcp-DhmmJfxK.js → mcp-CNUbvbsy.js} +2 -2
  27. package/dist/{mcp-DhmmJfxK.js.map → mcp-CNUbvbsy.js.map} +1 -1
  28. package/dist/mcp.d.ts +1 -1
  29. package/dist/mcp.js +1 -1
  30. package/dist/{messages-D0xT979U.js → messages-fTR19Ga6.js} +2 -2
  31. package/dist/{messages-D0xT979U.js.map → messages-fTR19Ga6.js.map} +1 -1
  32. package/dist/{presets-MCcvxiNT.js → presets-h6UWhghO.js} +3 -2
  33. package/dist/presets-h6UWhghO.js.map +1 -0
  34. package/dist/presets.d.ts +2 -2
  35. package/dist/presets.js +1 -1
  36. package/dist/{providers-x3LZByR5.js → providers-G0VBZK9j.js} +4 -4
  37. package/dist/{providers-x3LZByR5.js.map → providers-G0VBZK9j.js.map} +1 -1
  38. package/dist/providers.d.ts +1 -1
  39. package/dist/providers.js +2 -2
  40. package/dist/session/sqlite.d.ts +1 -1
  41. package/dist/session/sqlite.d.ts.map +1 -1
  42. package/dist/session/sqlite.js +2 -1
  43. package/dist/session/sqlite.js.map +1 -1
  44. package/dist/{session-BHZwxmfr.js → session-CbkiJDlH.js} +3 -2
  45. package/dist/session-CbkiJDlH.js.map +1 -0
  46. package/dist/session.d.ts +1 -1
  47. package/dist/session.js +2 -2
  48. package/dist/skills.d.ts +2 -2
  49. package/dist/skills.js +1 -1
  50. package/dist/{tools-BNfyY14s.js → tools-D_icxa-V.js} +813 -284
  51. package/dist/tools-D_icxa-V.js.map +1 -0
  52. package/dist/tools.d.ts +3 -3
  53. package/dist/tools.js +2 -2
  54. package/dist/{transcript-anchors-DonKvoh4.d.ts → transcript-anchors-3FFw2xuk.d.ts} +98 -15
  55. package/dist/transcript-anchors-3FFw2xuk.d.ts.map +1 -0
  56. package/dist/tui.d.ts +29 -5
  57. package/dist/tui.d.ts.map +1 -1
  58. package/dist/tui.js +879 -70
  59. package/dist/tui.js.map +1 -1
  60. package/dist/{turn-operations-TKvy0q29.js → turn-operations-CtgBlBHn.js} +412 -125
  61. package/dist/turn-operations-CtgBlBHn.js.map +1 -0
  62. package/dist/types-IcokUOyC.js.map +1 -1
  63. package/dist/types-KukEp-mi.d.ts +253 -0
  64. package/dist/types-KukEp-mi.d.ts.map +1 -0
  65. package/dist/types.d.ts +4 -4
  66. package/dist/types.js +1 -1
  67. package/docs/ARCHITECTURE.md +37 -3
  68. package/docs/CHAT.md +4 -2
  69. package/docs/RUN_IN_BACKGROUND.md +612 -0
  70. package/docs/SKILL.md +83 -14
  71. package/docs/TUI.md +40 -2
  72. package/package.json +4 -4
  73. package/dist/agent-DxBoKDba.d.ts.map +0 -1
  74. package/dist/contexts-BwiHIr2w.js +0 -129
  75. package/dist/contexts-BwiHIr2w.js.map +0 -1
  76. package/dist/index-B2VOOijU.d.ts.map +0 -1
  77. package/dist/index-BOtXdQkW.d.ts.map +0 -1
  78. package/dist/index-BiO_5Hm4.d.ts.map +0 -1
  79. package/dist/interpolate-ERgZUxgg.js.map +0 -1
  80. package/dist/presets-MCcvxiNT.js.map +0 -1
  81. package/dist/session-BHZwxmfr.js.map +0 -1
  82. package/dist/tools-BNfyY14s.js.map +0 -1
  83. package/dist/transcript-anchors-DonKvoh4.d.ts.map +0 -1
  84. package/dist/turn-operations-TKvy0q29.js.map +0 -1
  85. package/dist/types-Ce78ds4h.d.ts +0 -88
  86. package/dist/types-Ce78ds4h.d.ts.map +0 -1
package/dist/tui.js CHANGED
@@ -1,15 +1,15 @@
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 { A as resolvePersistDir, B as formatTaskStatus, H as previewLine, I as ageString, L as compactPath, O as cleanupPersistedSession, R as fmtTokens, U as shortId, V as formatTaskSummary, j as resolveTasksDir, p as createAgent, z as formatDuration } from "./tools-D_icxa-V.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-iTy-0wYz.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-CbkiJDlH.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, $n as tryOpenBrowser, A as getSafelist, At as resolveChipColor, B as supportsOAuth, Bt as useDiscoveryOptional, Cn as mergeApprovalAndBodyOutcomes, Cr as piIdOf, Ct as SETTINGS_CHOICES, D as useSafeModeQueue, Dn as stripEditOutcomesAnnotation, Dt as useSettings, E as useSafeModeActions, En as rewriteMultiEditHeader, Et as clampFps, F as suggestSafelistEntry, Fn as matchesBinding, Fr as TODO_STATUS_GLYPHS, Gr as useActiveTodos, H as filterModelCatalog, Hn as uniqueSkillNamesFromReferences, Ht as useConfig, Jt as isEditErrorResult, K as discoverProjectMcps, Kt as deriveSessionTitle, L as splitPromptSegments, Lt as createDiscoverySlot, Nn as ensureKeybindingsFile, On as summarizeOutcomes, Q as McpAuthProvider, Qn as buildLinearRamp, Qt as listSessionMeta, R as formatPathForCwd, Rt as DiscoveryProvider, St as DEFAULT_SETTINGS, T as SafeModeProvider, Tn as resolveApprovalForPayload, Tt as SettingsProvider, U as indexOfEntry, Ut as resolveConfig, V as buildModelCatalog, Vn as createSkillsCompletionProvider, Vt as ConfigProvider, W as buildMcpServers, Wn as createFilesCompletionProvider, Wt as EDIT_TOOL_NAMES, Xn as useCompletion, Xt as isVisible, Y as createFileMcpCredentialStore, Yt as isTurnHighlighted, Zn as blendHsl, Zt as lastContextSizeFromTurns, _ as turnContextSize, _n as previewEditPayload, _r as getContextWindow, _t as truncateTrailing, a as computeTurnAnchors, at as InteractionsProvider, b as defaultSkillScanPaths, bt as listProjectFiles, c as formatToolCall, cn as turnSelectionOwnership, ct as createInteractionTools, d as useSelectStyle, dn as buildContextualDiff, dt as pendingInteractionsFromTurns, en as marginTopFor, et as useMcpAuthState, f as useSurfaces, fn as buildUnifiedDiff, g as finalizeStreamingMarkdownForOwner, gn as filetypeFromPath, gt as hintsLength, h as finalizeStreamingMarkdown, hn as extractEditPayload, ht as clipHintsToWidth, i as turnAsText, in as sumRunCosts, j as isOnSafelist, jt as resolveTheme, k as addToSafelist, kn as findGitRoot, kr as accentColor, l as ThemeProvider, ln as updateToolEventOutcomes, lr as setProviderCredential, m as useTheme, mt as useInteractionsQueue, n as deleteTurnSafely, ni as buildBuildSystem, nn as selectableTurnIds, nr as shouldAutoCompact, o as TOOL_DISPLAY, on as toolCallPreview, pt as useInteractionsActions, qt as eventsFromTurns, r as truncateTurnsAt, ri as buildPlanSystem, rn as stripSpawnTokensLine, rr as detectAuth, rt as splitMarkdownCodeBlocks, s as displayNameFor, sn as toolResultText, st as buildResumedToolResultsTurn, tr as bootTick, tt as getMcpAuthStatus, u as useColors, ut as makeRequestInteraction, v as useStreamBuffer, w as writeSessionExport, wn as parseEditOutcomesFromResult, wt as SETTINGS_TOGGLES, x as discoverProjectSkills, xn as buildEditOutcomesAnnotation, xt as useEnabledToggleSet, y as buildSkillsConfig, yn as summarizeEditPayload, yr as modelSupportsReasoning, yt as generateSessionTitle, z as runOAuthLogin, zt as useDiscovery } from "./turn-operations-CtgBlBHn.js";
9
+ import { homedir } from "node:os";
9
10
  import { spawn } from "node:child_process";
10
- import { Buffer } from "node:buffer";
11
11
  import * as fs from "node:fs";
12
- import { homedir } from "node:os";
12
+ import { Buffer } from "node:buffer";
13
13
  import { createContext, createElement, memo, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
14
14
  import { BoxRenderable, CodeRenderable, RGBA, SyntaxStyle, TextRenderable, addDefaultParsers, createCliRenderer, decodePasteBytes, defaultTextareaKeyBindings, getTreeSitterClient, stripAnsiSequences } from "@opentui/core";
15
15
  import { createRoot, useKeyboard, useRenderer, useSelectionHandler, useTerminalDimensions } from "@opentui/react";
@@ -223,6 +223,182 @@ 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) {
254
+ const result = onCancel(row, "user-clicked-cancel");
255
+ if (result instanceof Promise) result.catch(() => {});
256
+ }
257
+ onClose();
258
+ return;
259
+ }
260
+ if (key.name === "a") {
261
+ onCancelAll();
262
+ onClose();
263
+ }
264
+ });
265
+ const elapsedColWidth = 8;
266
+ const callIdColWidth = 14;
267
+ const childColWidth = 10;
268
+ return /* @__PURE__ */ jsxs(Modal, {
269
+ title: "cancel tool call",
270
+ bottomTitle: empty ? "no calls in flight" : `${rows.length} in flight`,
271
+ maxWidth: Math.min(96, Math.max(64, termWidth - 8)),
272
+ minWidth: 56,
273
+ onClose,
274
+ children: [empty ? /* @__PURE__ */ jsxs("text", {
275
+ fg: COLOR.dim,
276
+ children: [/* @__PURE__ */ jsx("span", {
277
+ fg: COLOR.mute,
278
+ children: "no tool calls are currently in flight — "
279
+ }), /* @__PURE__ */ jsx("span", {
280
+ fg: COLOR.dim,
281
+ children: "nothing to cancel."
282
+ })]
283
+ }) : /* @__PURE__ */ jsx("box", {
284
+ style: {
285
+ flexDirection: "column",
286
+ flexShrink: 0
287
+ },
288
+ children: rows.map((row, i) => /* @__PURE__ */ jsx(CancelToolRow, {
289
+ row,
290
+ isFocused: i === safeIndex,
291
+ highlightBg: SURFACE.selection,
292
+ elapsedColWidth,
293
+ callIdColWidth,
294
+ childColWidth
295
+ }, row.callId))
296
+ }), /* @__PURE__ */ jsxs("text", {
297
+ fg: COLOR.dim,
298
+ children: [
299
+ /* @__PURE__ */ jsx("span", {
300
+ fg: COLOR.warn,
301
+ children: "↑↓"
302
+ }),
303
+ " navigate · ",
304
+ /* @__PURE__ */ jsx("span", {
305
+ fg: COLOR.warn,
306
+ children: "↵"
307
+ }),
308
+ " cancel selected · ",
309
+ /* @__PURE__ */ jsx("span", {
310
+ fg: COLOR.warn,
311
+ children: "a"
312
+ }),
313
+ " cancel all · ",
314
+ /* @__PURE__ */ jsx("span", {
315
+ fg: COLOR.warn,
316
+ children: "esc"
317
+ }),
318
+ " close"
319
+ ]
320
+ })]
321
+ });
322
+ }
323
+ function CancelToolRow({ row, isFocused, highlightBg, elapsedColWidth, callIdColWidth, childColWidth }) {
324
+ const COLOR = useColors();
325
+ const elapsed = formatElapsed(Date.now() - row.startedAt).padStart(elapsedColWidth, " ");
326
+ const idLabel = truncate(row.callId, callIdColWidth).padEnd(callIdColWidth, " ");
327
+ const childLabel = (row.childId ? `· ${row.childId}` : "").padEnd(childColWidth, " ");
328
+ const kindGlyph = row.kind === "task" ? "⌁" : "·";
329
+ return /* @__PURE__ */ jsx("box", {
330
+ style: {
331
+ height: 1,
332
+ paddingLeft: 1,
333
+ paddingRight: 1,
334
+ flexShrink: 0,
335
+ backgroundColor: isFocused ? highlightBg : void 0
336
+ },
337
+ children: /* @__PURE__ */ jsxs("text", {
338
+ wrapMode: "none",
339
+ children: [
340
+ /* @__PURE__ */ jsx("span", {
341
+ fg: isFocused ? COLOR.brand : COLOR.mute,
342
+ children: isFocused ? "›" : " "
343
+ }),
344
+ /* @__PURE__ */ jsx("span", {
345
+ fg: COLOR.mute,
346
+ children: " "
347
+ }),
348
+ /* @__PURE__ */ jsx("span", {
349
+ fg: isFocused ? COLOR.warn : COLOR.mute,
350
+ children: kindGlyph
351
+ }),
352
+ /* @__PURE__ */ jsx("span", {
353
+ fg: COLOR.mute,
354
+ children: " "
355
+ }),
356
+ /* @__PURE__ */ jsx("span", {
357
+ fg: isFocused ? COLOR.brand : COLOR.dim,
358
+ children: row.tool
359
+ }),
360
+ /* @__PURE__ */ jsx("span", {
361
+ fg: COLOR.mute,
362
+ children: " "
363
+ }),
364
+ /* @__PURE__ */ jsx("span", {
365
+ fg: COLOR.mute,
366
+ children: idLabel
367
+ }),
368
+ /* @__PURE__ */ jsx("span", {
369
+ fg: COLOR.mute,
370
+ children: " "
371
+ }),
372
+ /* @__PURE__ */ jsx("span", {
373
+ fg: COLOR.mute,
374
+ children: childLabel
375
+ }),
376
+ /* @__PURE__ */ jsx("span", {
377
+ fg: COLOR.warn,
378
+ children: elapsed
379
+ })
380
+ ]
381
+ })
382
+ });
383
+ }
384
+ /**
385
+ * Format a sub-minute duration as `0.3s` / `7.4s`; minute-plus as `2m12s`.
386
+ * Tight + monospace-friendly so the column stays right-aligned.
387
+ */
388
+ function formatElapsed(ms) {
389
+ if (ms < 0) return "0.0s";
390
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
391
+ const totalSeconds = Math.floor(ms / 1e3);
392
+ const minutes = Math.floor(totalSeconds / 60);
393
+ const seconds = totalSeconds % 60;
394
+ return `${minutes}m${String(seconds).padStart(2, "0")}s`;
395
+ }
396
+ function truncate(s, max) {
397
+ if (s.length <= max) return s;
398
+ if (max <= 1) return s.slice(0, max);
399
+ return `${s.slice(0, max - 1)}…`;
400
+ }
401
+ //#endregion
226
402
  //#region src/tui/clipboard.ts
227
403
  /**
228
404
  * Two-pronged clipboard write.
@@ -407,8 +583,15 @@ function CrushThrobber({ label, size = 15, from, to, labelColor }) {
407
583
  * embedded Tree-sitter language tokens (`keyword`, `string`, `function`,
408
584
  * …) — OpenTUI's `<markdown>` re-uses the same `SyntaxStyle` for the
409
585
  * fenced-code renderable, so one table drives both surfaces.
586
+ *
587
+ * `overrides`, when set, replaces individual entries by token name —
588
+ * each override is merged INTO the resolved entry (shallow). Used by
589
+ * the "on-selection" variant to swap `markup.raw.bg` for the row's
590
+ * selection surface so inline code chips blend into the highlight
591
+ * rather than reading as "punched out" rectangles. Top-level keys
592
+ * absent from the base theme become brand-new entries.
410
593
  */
411
- function buildMdStyle(theme) {
594
+ function buildMdStyle(theme, overrides) {
412
595
  const styles = {};
413
596
  for (const [token, style] of Object.entries(theme.syntax)) {
414
597
  const out = {};
@@ -420,13 +603,26 @@ function buildMdStyle(theme) {
420
603
  if (style.dim) out.dim = true;
421
604
  styles[token] = out;
422
605
  }
606
+ if (overrides) for (const [token, patch] of Object.entries(overrides)) styles[token] = {
607
+ ...styles[token] ?? {},
608
+ ...patch
609
+ };
423
610
  return SyntaxStyle.fromStyles(styles);
424
611
  }
425
612
  const MdStyleContext = createContext(null);
426
613
  function MdStyleProvider({ children }) {
427
614
  const theme = useTheme();
428
- const style = useMemo(() => buildMdStyle(theme), [theme]);
429
- return createElement(MdStyleContext.Provider, { value: style }, children);
615
+ const bundle = useMemo(() => {
616
+ const selectionBg = RGBA.fromHex(theme.surfaces.selection);
617
+ return {
618
+ regular: buildMdStyle(theme),
619
+ selected: buildMdStyle(theme, {
620
+ "markup.raw": { bg: selectionBg },
621
+ "markup.raw.block": { bg: selectionBg }
622
+ })
623
+ };
624
+ }, [theme]);
625
+ return createElement(MdStyleContext.Provider, { value: bundle }, children);
430
626
  }
431
627
  /**
432
628
  * Active markdown / syntax-highlighting style. Returns a single shared
@@ -434,13 +630,18 @@ function MdStyleProvider({ children }) {
434
630
  * mount, re-built on theme switch. A `Settings.theme` flip re-paints every
435
631
  * `<markdown>` that reads this hook.
436
632
  *
633
+ * Pass `{ selected: true }` to get the on-selection variant where inline
634
+ * code chips' background matches the row's selection surface (so the
635
+ * chips blend into the highlight rather than reading as punched-out
636
+ * rectangles).
637
+ *
437
638
  * Throws if used outside `<MdStyleProvider>` so a missing wiring shows up
438
639
  * loudly in development rather than silently rendering plain text.
439
640
  */
440
- function useMdStyle() {
441
- const style = useContext(MdStyleContext);
442
- if (!style) throw new Error("useMdStyle must be used inside <MdStyleProvider>");
443
- return style;
641
+ function useMdStyle(opts = {}) {
642
+ const bundle = useContext(MdStyleContext);
643
+ if (!bundle) throw new Error("useMdStyle must be used inside <MdStyleProvider>");
644
+ return opts.selected ? bundle.selected : bundle.regular;
444
645
  }
445
646
  const CHIP_TOKEN_PREFIX = "completion.reference";
446
647
  /** Per-kind token name in the chip `SyntaxStyle` — e.g. `completion.reference.skills`. */
@@ -554,7 +755,7 @@ function useChipHighlights(textareaRef, references, chipStyle) {
554
755
  * same-turn events read as one continuous highlighted block instead of a
555
756
  * striped list.
556
757
  */
557
- const EventLine = memo(({ event, previous, depthOffset = 0, selected = false, anchorId }) => {
758
+ const EventLine = memo(({ event, previous, depthOffset = 0, selected = false, anchorId, hideChildLabel = false }) => {
558
759
  const SURFACE = useSurfaces();
559
760
  const gap = marginTopFor(event, previous);
560
761
  return /* @__PURE__ */ jsx("box", {
@@ -569,7 +770,9 @@ const EventLine = memo(({ event, previous, depthOffset = 0, selected = false, an
569
770
  },
570
771
  children: /* @__PURE__ */ jsx(EventLineImpl, {
571
772
  event,
572
- depthOffset
773
+ depthOffset,
774
+ hideChildLabel,
775
+ selected
573
776
  })
574
777
  });
575
778
  });
@@ -1065,11 +1268,12 @@ function SubagentBlock({ events, previous, selectedTurnId = null, anchorIds }) {
1065
1268
  }, [events]);
1066
1269
  const title = childIds.length === 0 ? " subagent " : childIds.length === 1 ? ` ${childIds[0]} ` : ` subagents · ${childIds.join(", ")} `;
1067
1270
  const marginTop = previous ? 1 : 0;
1271
+ const hideChildLabel = childIds.length === 1;
1068
1272
  return /* @__PURE__ */ jsx("box", {
1069
1273
  title,
1070
1274
  style: {
1071
1275
  border: true,
1072
- borderColor: COLOR.mute,
1276
+ borderColor: COLOR.brand,
1073
1277
  paddingLeft: 1,
1074
1278
  paddingRight: 1,
1075
1279
  paddingTop: 0,
@@ -1084,7 +1288,8 @@ function SubagentBlock({ events, previous, selectedTurnId = null, anchorIds }) {
1084
1288
  previous: events[i - 1],
1085
1289
  depthOffset: 1,
1086
1290
  selected: selectedTurnId !== null && evt.turnId === selectedTurnId,
1087
- anchorId: anchorIds?.[i]
1291
+ anchorId: anchorIds?.[i],
1292
+ hideChildLabel
1088
1293
  }, i))
1089
1294
  });
1090
1295
  }
@@ -1126,7 +1331,7 @@ function rowStyle(paddingLeft) {
1126
1331
  alignSelf: "stretch"
1127
1332
  };
1128
1333
  }
1129
- function EventLineImpl({ event, depthOffset = 0 }) {
1334
+ function EventLineImpl({ event, depthOffset = 0, hideChildLabel = false, selected = false }) {
1130
1335
  const COLOR = useColors();
1131
1336
  const { settings } = useSettings();
1132
1337
  const safeText = event.text === "" ? " " : event.text;
@@ -1189,7 +1394,8 @@ function EventLineImpl({ event, depthOffset = 0 }) {
1189
1394
  style: row,
1190
1395
  children: /* @__PURE__ */ jsx(MarkdownBlock, {
1191
1396
  text: event.text,
1192
- dim: child
1397
+ dim: child,
1398
+ selected
1193
1399
  })
1194
1400
  });
1195
1401
  case "spawn-start": return /* @__PURE__ */ jsx("box", {
@@ -1201,10 +1407,13 @@ function EventLineImpl({ event, depthOffset = 0 }) {
1201
1407
  fg: COLOR.accent,
1202
1408
  children: "🌱 "
1203
1409
  }),
1204
- /* @__PURE__ */ jsx("span", {
1205
- fg: COLOR.dim,
1206
- children: `[${event.childId ?? "child"}] `
1207
- }),
1410
+ !hideChildLabel && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1411
+ fg: COLOR.brand,
1412
+ children: event.childId ?? "child"
1413
+ }), /* @__PURE__ */ jsx("span", {
1414
+ fg: COLOR.mute,
1415
+ children: " · "
1416
+ })] }),
1208
1417
  /* @__PURE__ */ jsx("span", {
1209
1418
  fg: COLOR.dim,
1210
1419
  children: safeText
@@ -1221,10 +1430,13 @@ function EventLineImpl({ event, depthOffset = 0 }) {
1221
1430
  fg: COLOR.accent,
1222
1431
  children: "🌳 "
1223
1432
  }),
1224
- /* @__PURE__ */ jsx("span", {
1225
- fg: COLOR.dim,
1226
- children: `[${event.childId ?? "child"}] `
1227
- }),
1433
+ !hideChildLabel && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1434
+ fg: COLOR.brand,
1435
+ children: event.childId ?? "child"
1436
+ }), /* @__PURE__ */ jsx("span", {
1437
+ fg: COLOR.mute,
1438
+ children: " · "
1439
+ })] }),
1228
1440
  /* @__PURE__ */ jsx("span", {
1229
1441
  fg: COLOR.mute,
1230
1442
  children: safeText
@@ -1236,10 +1448,80 @@ function EventLineImpl({ event, depthOffset = 0 }) {
1236
1448
  event,
1237
1449
  indent: row.paddingLeft
1238
1450
  });
1451
+ case "task-notification": return /* @__PURE__ */ jsx(TaskNotificationBlock, {
1452
+ event,
1453
+ indent: row.paddingLeft
1454
+ });
1239
1455
  default: return /* @__PURE__ */ jsx("text", { children: safeText });
1240
1456
  }
1241
1457
  }
1242
1458
  /**
1459
+ * One-line completion banner for a background task that exited or was
1460
+ * killed. Visual contract:
1461
+ *
1462
+ * - Glyph color reflects exit cleanliness:
1463
+ * `'exited'` exit code 0 → success (subtle accent — quiet).
1464
+ * `'exited'` non-zero → warn (model / user should notice).
1465
+ * `'killed'` → warn (we issued SIGTERM).
1466
+ * - Task id reads in the agent's brand accent so the banner stands
1467
+ * apart from surrounding markdown without screaming.
1468
+ * - Status label takes the same accent as the glyph — color-coupling
1469
+ * reinforces the "this is what happened" mental model at a glance.
1470
+ * - Output path is `compactPath()`-formatted (`~/.zidane/...` when
1471
+ * under `$HOME`) so the column doesn't blow past terminal width
1472
+ * just to show the user's home prefix they already know about.
1473
+ *
1474
+ * Vertical spacing comes from `marginTopFor` — banners get a `tool`-like
1475
+ * gap before (and the next non-task event provides the gap after), but
1476
+ * consecutive banners stack tightly so a burst of completions doesn't
1477
+ * scatter the transcript.
1478
+ */
1479
+ function TaskNotificationBlock({ event, indent }) {
1480
+ const COLOR = useColors();
1481
+ const task = event.task;
1482
+ const statusAccent = (task ? task.status === "killed" || task.exitCode !== 0 : false) ? COLOR.warn : COLOR.brand;
1483
+ const statusLabel = task ? formatTaskStatus(task) : "done";
1484
+ const displayPath = task?.outputPath ? compactPath(task.outputPath) : "";
1485
+ return /* @__PURE__ */ jsx("box", {
1486
+ style: { paddingLeft: indent },
1487
+ children: /* @__PURE__ */ jsxs("text", {
1488
+ wrapMode: "none",
1489
+ children: [
1490
+ /* @__PURE__ */ jsx("span", {
1491
+ fg: statusAccent,
1492
+ children: "⌁ "
1493
+ }),
1494
+ /* @__PURE__ */ jsx("span", {
1495
+ fg: COLOR.brand,
1496
+ children: task?.taskId ?? "?"
1497
+ }),
1498
+ /* @__PURE__ */ jsx("span", {
1499
+ fg: COLOR.mute,
1500
+ children: " · "
1501
+ }),
1502
+ /* @__PURE__ */ jsx("span", {
1503
+ fg: statusAccent,
1504
+ children: statusLabel
1505
+ }),
1506
+ task && task.durationMs > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1507
+ fg: COLOR.mute,
1508
+ children: " · "
1509
+ }), /* @__PURE__ */ jsx("span", {
1510
+ fg: COLOR.dim,
1511
+ children: formatDuration(task.durationMs)
1512
+ })] }),
1513
+ displayPath && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1514
+ fg: COLOR.mute,
1515
+ children: " · "
1516
+ }), /* @__PURE__ */ jsx("span", {
1517
+ fg: COLOR.dim,
1518
+ children: displayPath
1519
+ })] })
1520
+ ]
1521
+ })
1522
+ });
1523
+ }
1524
+ /**
1243
1525
  * Boundary card for `compact-summary` events. Two visual rows:
1244
1526
  *
1245
1527
  * 1. Meta line — emoji + replaced-turn count + model + token usage,
@@ -1657,10 +1939,10 @@ function parseBlockIndex(id) {
1657
1939
  const parsed = Number.parseInt(match[1] ?? "", 10);
1658
1940
  return Number.isFinite(parsed) ? parsed : 0;
1659
1941
  }
1660
- const MarkdownBlock = memo(({ text, dim }) => {
1942
+ const MarkdownBlock = memo(({ text, dim, selected = false }) => {
1661
1943
  const COLOR = useColors();
1662
1944
  const SURFACE = useSurfaces();
1663
- const mdStyle = useMdStyle();
1945
+ const mdStyle = useMdStyle({ selected });
1664
1946
  const renderer = useRenderer();
1665
1947
  const content = text.replace(/^\n+|\n+$/g, "");
1666
1948
  const bag = useRef({
@@ -1682,7 +1964,7 @@ const MarkdownBlock = memo(({ text, dim }) => {
1682
1964
  streaming: true,
1683
1965
  internalBlockMode: "coalesced",
1684
1966
  fg: dim ? COLOR.dim : void 0,
1685
- bg: SURFACE.background,
1967
+ bg: selected ? SURFACE.selection : SURFACE.background,
1686
1968
  renderNode
1687
1969
  });
1688
1970
  });
@@ -1721,11 +2003,15 @@ function EditDiffBlock({ payload, dim }) {
1721
2003
  const COLOR = useColors();
1722
2004
  const SURFACE = useSurfaces();
1723
2005
  const mdStyle = useMdStyle();
2006
+ const { settings } = useSettings();
1724
2007
  const filetype = useMemo(() => filetypeFromPath(payload.path), [payload.path]);
1725
2008
  const replaceAllCount = payload.hunks.filter((h) => h.replaceAll).length;
1726
2009
  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;
2010
+ const outcomeSummary = summarizeOutcomes(payload.outcomes);
2011
+ const hasMixedOutcomes = !!payload.outcomes && outcomeSummary.denied + outcomeSummary.skipped + outcomeSummary.failed > 0;
2012
+ const editSummary = useMemo(() => summarizeEditPayload(payload), [payload]);
2013
+ const perHunkMode = hasMixedOutcomes;
2014
+ const compact = settings.editDiffDisplay === "compact";
1729
2015
  return /* @__PURE__ */ jsxs("box", {
1730
2016
  style: {
1731
2017
  flexDirection: "column",
@@ -1751,6 +2037,24 @@ function EditDiffBlock({ payload, dim }) {
1751
2037
  fg: dim ? COLOR.dim : COLOR.warn,
1752
2038
  children: payload.path
1753
2039
  }),
2040
+ (editSummary.totalAdded > 0 || editSummary.totalRemoved > 0) && /* @__PURE__ */ jsxs(Fragment, { children: [
2041
+ /* @__PURE__ */ jsx("span", {
2042
+ fg: COLOR.mute,
2043
+ children: " · "
2044
+ }),
2045
+ editSummary.totalAdded > 0 && /* @__PURE__ */ jsx("span", {
2046
+ fg: dim ? COLOR.dim : SURFACE.diff.addFg,
2047
+ children: `+${editSummary.totalAdded}`
2048
+ }),
2049
+ editSummary.totalAdded > 0 && editSummary.totalRemoved > 0 && /* @__PURE__ */ jsx("span", {
2050
+ fg: COLOR.mute,
2051
+ children: " "
2052
+ }),
2053
+ editSummary.totalRemoved > 0 && /* @__PURE__ */ jsx("span", {
2054
+ fg: dim ? COLOR.dim : SURFACE.diff.removeFg,
2055
+ children: `−${editSummary.totalRemoved}`
2056
+ })
2057
+ ] }),
1754
2058
  hunkBadge && /* @__PURE__ */ jsx("span", {
1755
2059
  fg: COLOR.mute,
1756
2060
  children: ` · ${hunkBadge}`
@@ -1765,33 +2069,36 @@ function EditDiffBlock({ payload, dim }) {
1765
2069
  children: " · "
1766
2070
  }),
1767
2071
  /* @__PURE__ */ jsx("span", {
1768
- fg: summary.applied > 0 ? COLOR.accent : COLOR.mute,
1769
- children: `${summary.applied} applied`
2072
+ fg: outcomeSummary.applied > 0 ? COLOR.accent : COLOR.mute,
2073
+ children: `${outcomeSummary.applied} applied`
1770
2074
  }),
1771
- summary.denied > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2075
+ outcomeSummary.denied > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1772
2076
  fg: COLOR.mute,
1773
2077
  children: " · "
1774
2078
  }), /* @__PURE__ */ jsx("span", {
1775
2079
  fg: COLOR.error,
1776
- children: `${summary.denied} denied`
2080
+ children: `${outcomeSummary.denied} denied`
1777
2081
  })] }),
1778
- summary.skipped > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2082
+ outcomeSummary.skipped > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1779
2083
  fg: COLOR.mute,
1780
2084
  children: " · "
1781
2085
  }), /* @__PURE__ */ jsx("span", {
1782
2086
  fg: COLOR.warn,
1783
- children: `${summary.skipped} skipped`
2087
+ children: `${outcomeSummary.skipped} skipped`
1784
2088
  })] }),
1785
- summary.failed > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2089
+ outcomeSummary.failed > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1786
2090
  fg: COLOR.mute,
1787
2091
  children: " · "
1788
2092
  }), /* @__PURE__ */ jsx("span", {
1789
2093
  fg: COLOR.error,
1790
- children: `${summary.failed} failed`
2094
+ children: `${outcomeSummary.failed} failed`
1791
2095
  })] })
1792
2096
  ] })
1793
2097
  ]
1794
- }), perHunkMode ? /* @__PURE__ */ jsx("box", {
2098
+ }), compact ? /* @__PURE__ */ jsx(CompactDiffSummary, {
2099
+ summary: editSummary,
2100
+ dim
2101
+ }) : perHunkMode ? /* @__PURE__ */ jsx("box", {
1795
2102
  style: {
1796
2103
  flexDirection: "column",
1797
2104
  flexShrink: 0
@@ -1809,10 +2116,10 @@ function EditDiffBlock({ payload, dim }) {
1809
2116
  dim
1810
2117
  }, i))
1811
2118
  }) : /* @__PURE__ */ jsx("diff", {
1812
- diff: buildUnifiedDiff(payload),
2119
+ diff: payload.priorContent !== void 0 ? buildContextualDiff(payload, payload.priorContent) : buildUnifiedDiff(payload),
1813
2120
  view: "unified",
1814
2121
  wrapMode: "word",
1815
- showLineNumbers: false,
2122
+ showLineNumbers: true,
1816
2123
  ...filetype ? { filetype } : {},
1817
2124
  syntaxStyle: mdStyle,
1818
2125
  addedBg: SURFACE.diff.addBg,
@@ -1828,6 +2135,91 @@ function EditDiffBlock({ payload, dim }) {
1828
2135
  });
1829
2136
  }
1830
2137
  /**
2138
+ * Compact-mode body — one line per hunk under the tool header. Format:
2139
+ *
2140
+ * ` L42 · +2 −1 · old → new`
2141
+ *
2142
+ * Where `L<n>` is the new-file line position (omitted when priorContent
2143
+ * is absent and the position is unknown), and the `old → new` preview
2144
+ * is the FIRST changed line on each side, ASCII-arrowed and truncated
2145
+ * by the renderer's own word-wrap. A pure addition shows `+ new` only;
2146
+ * a pure deletion shows `− old` only.
2147
+ */
2148
+ function CompactDiffSummary({ summary, dim }) {
2149
+ const COLOR = useColors();
2150
+ const SURFACE = useSurfaces();
2151
+ if (summary.hunks.length === 0) return null;
2152
+ return /* @__PURE__ */ jsx("box", {
2153
+ style: {
2154
+ flexDirection: "column",
2155
+ flexShrink: 0
2156
+ },
2157
+ children: summary.hunks.map((h, i) => {
2158
+ const oldPreview = h.firstOld?.trim();
2159
+ const newPreview = h.firstNew?.trim();
2160
+ return /* @__PURE__ */ jsxs("text", {
2161
+ fg: dim ? COLOR.dim : COLOR.mute,
2162
+ wrapMode: "word",
2163
+ children: [
2164
+ /* @__PURE__ */ jsx("span", {
2165
+ fg: COLOR.mute,
2166
+ children: " "
2167
+ }),
2168
+ h.line !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2169
+ fg: dim ? COLOR.dim : COLOR.mute,
2170
+ children: `L${h.line}`
2171
+ }), /* @__PURE__ */ jsx("span", {
2172
+ fg: COLOR.mute,
2173
+ children: " · "
2174
+ })] }),
2175
+ h.added > 0 && /* @__PURE__ */ jsx("span", {
2176
+ fg: dim ? COLOR.dim : SURFACE.diff.addFg,
2177
+ children: `+${h.added}`
2178
+ }),
2179
+ h.added > 0 && h.removed > 0 && /* @__PURE__ */ jsx("span", {
2180
+ fg: COLOR.mute,
2181
+ children: " "
2182
+ }),
2183
+ h.removed > 0 && /* @__PURE__ */ jsx("span", {
2184
+ fg: dim ? COLOR.dim : SURFACE.diff.removeFg,
2185
+ children: `−${h.removed}`
2186
+ }),
2187
+ (oldPreview || newPreview) && /* @__PURE__ */ jsx("span", {
2188
+ fg: COLOR.mute,
2189
+ children: " · "
2190
+ }),
2191
+ oldPreview && newPreview ? /* @__PURE__ */ jsxs(Fragment, { children: [
2192
+ /* @__PURE__ */ jsx("span", {
2193
+ fg: dim ? COLOR.dim : SURFACE.diff.removeFg,
2194
+ children: oldPreview
2195
+ }),
2196
+ /* @__PURE__ */ jsx("span", {
2197
+ fg: COLOR.mute,
2198
+ children: " → "
2199
+ }),
2200
+ /* @__PURE__ */ jsx("span", {
2201
+ fg: dim ? COLOR.dim : SURFACE.diff.addFg,
2202
+ children: newPreview
2203
+ })
2204
+ ] }) : oldPreview ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2205
+ fg: dim ? COLOR.dim : SURFACE.diff.removeFg,
2206
+ children: "− "
2207
+ }), /* @__PURE__ */ jsx("span", {
2208
+ fg: dim ? COLOR.dim : SURFACE.diff.removeFg,
2209
+ children: oldPreview
2210
+ })] }) : newPreview ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
2211
+ fg: dim ? COLOR.dim : SURFACE.diff.addFg,
2212
+ children: "+ "
2213
+ }), /* @__PURE__ */ jsx("span", {
2214
+ fg: dim ? COLOR.dim : SURFACE.diff.addFg,
2215
+ children: newPreview
2216
+ })] }) : null
2217
+ ]
2218
+ }, i);
2219
+ })
2220
+ });
2221
+ }
2222
+ /**
1831
2223
  * One hunk inside an `EditDiffBlock` rendered as its own mini-diff with
1832
2224
  * a status badge above it. Used only in the per-hunk view (multi_edit
1833
2225
  * with mixed outcomes) so denied / skipped / failed edits remain
@@ -1835,9 +2227,11 @@ function EditDiffBlock({ payload, dim }) {
1835
2227
  */
1836
2228
  const BADGE_WIDTH = 7;
1837
2229
  function HunkBlock({ index, hunk, outcome, tool, path, filetype, mdStyle, COLOR, SURFACE, dim }) {
2230
+ const { settings } = useSettings();
1838
2231
  const kind = outcome?.kind ?? "applied";
1839
2232
  const reason = outcome?.reason;
1840
2233
  const dimmed = dim || kind !== "applied";
2234
+ const compact = settings.editDiffDisplay === "compact";
1841
2235
  const diffText = useMemo(() => buildUnifiedDiff({
1842
2236
  tool,
1843
2237
  path,
@@ -1847,6 +2241,15 @@ function HunkBlock({ index, hunk, outcome, tool, path, filetype, mdStyle, COLOR,
1847
2241
  tool,
1848
2242
  path
1849
2243
  ]);
2244
+ const hunkStats = useMemo(() => summarizeEditPayload({
2245
+ tool,
2246
+ path,
2247
+ hunks: [hunk]
2248
+ }).hunks[0], [
2249
+ hunk,
2250
+ tool,
2251
+ path
2252
+ ]);
1850
2253
  const badge = kind === "applied" ? {
1851
2254
  label: "applied",
1852
2255
  fg: COLOR.accent
@@ -1881,6 +2284,24 @@ function HunkBlock({ index, hunk, outcome, tool, path, filetype, mdStyle, COLOR,
1881
2284
  fg: badge.fg,
1882
2285
  children: badge.label.padEnd(BADGE_WIDTH)
1883
2286
  }),
2287
+ hunkStats && (hunkStats.added > 0 || hunkStats.removed > 0) && /* @__PURE__ */ jsxs(Fragment, { children: [
2288
+ /* @__PURE__ */ jsx("span", {
2289
+ fg: COLOR.mute,
2290
+ children: " · "
2291
+ }),
2292
+ hunkStats.added > 0 && /* @__PURE__ */ jsx("span", {
2293
+ fg: dim ? COLOR.dim : SURFACE.diff.addFg,
2294
+ children: `+${hunkStats.added}`
2295
+ }),
2296
+ hunkStats.added > 0 && hunkStats.removed > 0 && /* @__PURE__ */ jsx("span", {
2297
+ fg: COLOR.mute,
2298
+ children: " "
2299
+ }),
2300
+ hunkStats.removed > 0 && /* @__PURE__ */ jsx("span", {
2301
+ fg: dim ? COLOR.dim : SURFACE.diff.removeFg,
2302
+ children: `−${hunkStats.removed}`
2303
+ })
2304
+ ] }),
1884
2305
  reason && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
1885
2306
  fg: COLOR.mute,
1886
2307
  children: ": "
@@ -1889,11 +2310,11 @@ function HunkBlock({ index, hunk, outcome, tool, path, filetype, mdStyle, COLOR,
1889
2310
  children: reason
1890
2311
  })] })
1891
2312
  ]
1892
- }), /* @__PURE__ */ jsx("diff", {
2313
+ }), !compact && /* @__PURE__ */ jsx("diff", {
1893
2314
  diff: diffText,
1894
2315
  view: "unified",
1895
2316
  wrapMode: "word",
1896
- showLineNumbers: false,
2317
+ showLineNumbers: true,
1897
2318
  ...filetype ? { filetype } : {},
1898
2319
  syntaxStyle: mdStyle,
1899
2320
  addedBg: SURFACE.diff.addBg,
@@ -1913,7 +2334,7 @@ function ToolCallBlock({ event, display, dim }) {
1913
2334
  const COLOR = useColors();
1914
2335
  const mdStyle = useMdStyle();
1915
2336
  const name = event.tool ?? "";
1916
- const verb = displayNameFor(name);
2337
+ const verb = displayNameFor(name, event.input);
1917
2338
  const pretty = useMemo(() => {
1918
2339
  if (!event.input) return null;
1919
2340
  try {
@@ -7612,6 +8033,31 @@ const PREVIEW_CHAR_MAX = 8e3;
7612
8033
  * keeps a comfortable shape rather than stretching to the full height.
7613
8034
  */
7614
8035
  const MAX_MODAL_HEIGHT = 28;
8036
+ const EDIT_TEXTAREA_BINDINGS = [
8037
+ ...defaultTextareaKeyBindings.filter((b) => b.name !== "return" && !(b.name === "a" && b.ctrl && !b.shift && !b.meta)),
8038
+ {
8039
+ name: "a",
8040
+ ctrl: true,
8041
+ action: "select-all"
8042
+ },
8043
+ {
8044
+ name: "return",
8045
+ action: "submit"
8046
+ },
8047
+ {
8048
+ name: "return",
8049
+ shift: true,
8050
+ action: "newline"
8051
+ }
8052
+ ];
8053
+ /**
8054
+ * Extract the editable text from a turn — joins all `text` blocks.
8055
+ * Non-text blocks (tool_call, tool_result, thinking, etc.) are structural
8056
+ * and not included in the editable surface.
8057
+ */
8058
+ function editableText(turn) {
8059
+ return turn.content.filter((b) => b.type === "text").map((b) => b.text).join("\n\n");
8060
+ }
7615
8061
  function TurnDetailsModal({ turn, index, total, actions, keybindings }) {
7616
8062
  const COLOR = useColors();
7617
8063
  const modal = useModal();
@@ -7621,6 +8067,9 @@ function TurnDetailsModal({ turn, index, total, actions, keybindings }) {
7621
8067
  const bottomTitle = `${index - 1} before · ${total - index} after`;
7622
8068
  const [pending, setPending] = useState(null);
7623
8069
  const [copyStatus, setCopyStatus] = useState("idle");
8070
+ const [editing, setEditing] = useState(false);
8071
+ const textareaRef = useRef(null);
8072
+ const hasEditableText = turn.content.some((b) => b.type === "text");
7624
8073
  const commitFork = () => {
7625
8074
  modal.close();
7626
8075
  actions.onFork(turn.id);
@@ -7636,7 +8085,16 @@ function TurnDetailsModal({ turn, index, total, actions, keybindings }) {
7636
8085
  }
7637
8086
  setCopyStatus(writeToClipboard(fullText) ? "copied" : "failed");
7638
8087
  };
8088
+ const commitEdit = () => {
8089
+ const newText = textareaRef.current?.plainText ?? "";
8090
+ modal.close();
8091
+ actions.onEdit(turn.id, newText);
8092
+ };
7639
8093
  useKeyboard((key) => {
8094
+ if (editing) {
8095
+ if (key.name === "escape") setEditing(false);
8096
+ return;
8097
+ }
7640
8098
  if (key.name === "escape" && pending) {
7641
8099
  setPending(null);
7642
8100
  return;
@@ -7658,15 +8116,22 @@ function TurnDetailsModal({ turn, index, total, actions, keybindings }) {
7658
8116
  handleCopy();
7659
8117
  return;
7660
8118
  }
8119
+ if (matchesBinding(key, keybindings.turnEdit)) {
8120
+ if (!hasEditableText) return;
8121
+ setPending(null);
8122
+ setCopyStatus("idle");
8123
+ setEditing(true);
8124
+ return;
8125
+ }
7661
8126
  if (pending) setPending(null);
7662
8127
  });
7663
8128
  return /* @__PURE__ */ jsxs(Modal, {
7664
- title: `turn ${index} / ${total} · ${turn.role}`,
7665
- bottomTitle,
8129
+ title: editing ? `edit turn ${index} / ${total} · ${turn.role}` : `turn ${index} / ${total} · ${turn.role}`,
8130
+ bottomTitle: editing ? void 0 : bottomTitle,
7666
8131
  maxHeight: MAX_MODAL_HEIGHT,
7667
- disableEscape: pending !== null,
8132
+ disableEscape: editing || pending !== null,
7668
8133
  children: [
7669
- /* @__PURE__ */ jsxs("text", {
8134
+ !editing && /* @__PURE__ */ jsxs("text", {
7670
8135
  fg: COLOR.dim,
7671
8136
  children: [
7672
8137
  /* @__PURE__ */ jsx("span", {
@@ -7705,7 +8170,7 @@ function TurnDetailsModal({ turn, index, total, actions, keybindings }) {
7705
8170
  ] })
7706
8171
  ]
7707
8172
  }),
7708
- /* @__PURE__ */ jsxs("text", {
8173
+ !editing && /* @__PURE__ */ jsxs("text", {
7709
8174
  fg: COLOR.dim,
7710
8175
  children: [/* @__PURE__ */ jsx("span", {
7711
8176
  fg: COLOR.mute,
@@ -7715,7 +8180,31 @@ function TurnDetailsModal({ turn, index, total, actions, keybindings }) {
7715
8180
  children: summary
7716
8181
  })]
7717
8182
  }),
7718
- /* @__PURE__ */ jsx("box", {
8183
+ editing ? /* @__PURE__ */ jsx("box", {
8184
+ title: " edit ",
8185
+ style: {
8186
+ border: true,
8187
+ borderColor: COLOR.borderActive,
8188
+ paddingLeft: 1,
8189
+ paddingRight: 1,
8190
+ flexDirection: "column",
8191
+ flexGrow: 1,
8192
+ flexShrink: 1,
8193
+ minHeight: 5
8194
+ },
8195
+ children: /* @__PURE__ */ jsx("textarea", {
8196
+ ref: textareaRef,
8197
+ focused: true,
8198
+ keyBindings: EDIT_TEXTAREA_BINDINGS,
8199
+ initialValue: editableText(turn),
8200
+ placeholder: "enter text…",
8201
+ style: {
8202
+ flexGrow: 1,
8203
+ height: "100%"
8204
+ },
8205
+ onSubmit: commitEdit
8206
+ })
8207
+ }) : /* @__PURE__ */ jsx("box", {
7719
8208
  title: " preview ",
7720
8209
  style: {
7721
8210
  border: true,
@@ -7740,13 +8229,34 @@ function TurnDetailsModal({ turn, index, total, actions, keybindings }) {
7740
8229
  children: "— no text content —"
7741
8230
  })
7742
8231
  }),
7743
- /* @__PURE__ */ jsx(ActionRow, {
8232
+ editing ? /* @__PURE__ */ jsxs("text", {
8233
+ fg: COLOR.dim,
8234
+ children: [
8235
+ /* @__PURE__ */ jsx("span", {
8236
+ fg: COLOR.warn,
8237
+ children: "↵"
8238
+ }),
8239
+ " save · ",
8240
+ /* @__PURE__ */ jsx("span", {
8241
+ fg: COLOR.warn,
8242
+ children: "shift+↵"
8243
+ }),
8244
+ " newline · ",
8245
+ /* @__PURE__ */ jsx("span", {
8246
+ fg: COLOR.warn,
8247
+ children: "esc"
8248
+ }),
8249
+ " cancel"
8250
+ ]
8251
+ }) : /* @__PURE__ */ jsx(ActionRow, {
7744
8252
  pending,
7745
8253
  copyStatus,
7746
8254
  canCopy: fullText.length > 0,
8255
+ canEdit: hasEditableText,
7747
8256
  forkKey: keybindings.turnFork,
7748
8257
  deleteKey: keybindings.turnDelete,
7749
- copyKey: keybindings.turnCopy
8258
+ copyKey: keybindings.turnCopy,
8259
+ editKey: keybindings.turnEdit
7750
8260
  })
7751
8261
  ]
7752
8262
  });
@@ -7758,7 +8268,7 @@ function TurnDetailsModal({ turn, index, total, actions, keybindings }) {
7758
8268
  * by accident. The copy result rides the same row when present — same
7759
8269
  * geometry, no layout shift.
7760
8270
  */
7761
- function ActionRow({ pending, copyStatus, canCopy, forkKey, deleteKey, copyKey }) {
8271
+ function ActionRow({ pending, copyStatus, canCopy, canEdit, forkKey, deleteKey, copyKey, editKey }) {
7762
8272
  const COLOR = useColors();
7763
8273
  if (pending === "fork") return /* @__PURE__ */ jsxs("text", {
7764
8274
  fg: COLOR.dim,
@@ -7818,6 +8328,11 @@ function ActionRow({ pending, copyStatus, canCopy, forkKey, deleteKey, copyKey }
7818
8328
  children: deleteKey
7819
8329
  }),
7820
8330
  " delete · ",
8331
+ /* @__PURE__ */ jsx("span", {
8332
+ fg: canEdit ? COLOR.warn : COLOR.mute,
8333
+ children: editKey
8334
+ }),
8335
+ canEdit ? " edit · " : " (no text) · ",
7821
8336
  /* @__PURE__ */ jsx("span", {
7822
8337
  fg: COLOR.warn,
7823
8338
  children: "esc"
@@ -7858,6 +8373,11 @@ function ActionRow({ pending, copyStatus, canCopy, forkKey, deleteKey, copyKey }
7858
8373
  children: copyKey
7859
8374
  }),
7860
8375
  canCopy ? " copy · " : " (nothing to copy) · ",
8376
+ /* @__PURE__ */ jsx("span", {
8377
+ fg: canEdit ? COLOR.warn : COLOR.mute,
8378
+ children: editKey
8379
+ }),
8380
+ canEdit ? " edit · " : " (no text) · ",
7861
8381
  /* @__PURE__ */ jsx("span", {
7862
8382
  fg: COLOR.warn,
7863
8383
  children: "esc"
@@ -7919,6 +8439,42 @@ async function launchEditor(path) {
7919
8439
  }).unref();
7920
8440
  }
7921
8441
  /**
8442
+ * Session-metadata key under which the TUI persists the user's "pinned"
8443
+ * active skills — the set the user toggled on via `/skill-name` and
8444
+ * expects to stay active across run boundaries (and TUI restarts).
8445
+ *
8446
+ * Separate from the agent's `skillActivationState` for two reasons:
8447
+ * 1. The framework's run-end pass deactivates everything; the pinned
8448
+ * set survives that pass so the next prompt can re-activate.
8449
+ * 2. The agent's session-resume rehydrator only sees `skills_use`
8450
+ * `tool_call` blocks in history. Slash-command activations bypass
8451
+ * that path; this metadata key is the system-of-record for them.
8452
+ */
8453
+ const ACTIVE_SKILLS_META_KEY = "zidane.activeSkills";
8454
+ /**
8455
+ * Read the pinned-skills set out of session metadata. Tolerant by
8456
+ * design — older sessions, manually-edited metadata, or a future
8457
+ * type drift all degrade to "no pins", never throw. Returns a fresh
8458
+ * Set so callers can mutate-and-store without aliasing the input.
8459
+ */
8460
+ function readPinnedSkills(raw) {
8461
+ if (!Array.isArray(raw)) return /* @__PURE__ */ new Set();
8462
+ return new Set(raw.filter((v) => typeof v === "string" && v.length > 0));
8463
+ }
8464
+ /**
8465
+ * Mirror the pinned-skills set into session metadata. Sorted for
8466
+ * stable on-disk ordering (diffs / debugging). `session.setMeta`
8467
+ * already routes through `session:meta` hooks, so observers see a
8468
+ * single normalized payload regardless of insertion order.
8469
+ *
8470
+ * Imported lazily via the session ref — calling sites already
8471
+ * captured `session` in scope, so we accept it as an argument
8472
+ * rather than re-fetching from a ref.
8473
+ */
8474
+ function persistPinnedSkills(session, pins) {
8475
+ session.setMeta(ACTIVE_SKILLS_META_KEY, Array.from(pins).sort());
8476
+ }
8477
+ /**
7922
8478
  * Filter a `multi_edit` (or single `edit` / `write_file`) input's hunk
7923
8479
  * list down to the approved subset, in original order. Used by the
7924
8480
  * approval gate to rebind `ctx.input.edits` after a partial decision —
@@ -8239,6 +8795,80 @@ function AppShell() {
8239
8795
  const agentRef = useRef(null);
8240
8796
  const sessionRef = useRef(null);
8241
8797
  /**
8798
+ * Live registry of in-flight tool calls — populated by `tool:before`
8799
+ * (and `child:tool:before`), drained by `tool:after` / `tool:error` /
8800
+ * `tool:cancelled` (and their `child:*` siblings). Drives the
8801
+ * "cancel tool call" picker (see {@link CancelToolModal}).
8802
+ *
8803
+ * Mirrored in `inFlightToolsRef` for callbacks that read the latest
8804
+ * value synchronously without listing `inFlightTools` in their deps —
8805
+ * the picker open path needs both: a state snapshot for the modal's
8806
+ * rendered rows, and the live ref for any subsequent cancel actions
8807
+ * that fire after the snapshot was taken.
8808
+ *
8809
+ * Excludes `mcp:tool:before` / `mcp:tool:after` deliberately for
8810
+ * v1 — `agent.cancelTool(callId)` operates on the unified loop-side
8811
+ * callId registry, which MCP tools also live in (they're dispatched
8812
+ * through the same `executeSingleTool`), so the cancel works for
8813
+ * them too; we just don't surface them in the picker UI yet to
8814
+ * avoid drowning the list with high-cardinality MCP transient calls.
8815
+ */
8816
+ const [inFlightTools, setInFlightTools] = useState([]);
8817
+ const inFlightToolsRef = useRef([]);
8818
+ inFlightToolsRef.current = inFlightTools;
8819
+ /**
8820
+ * Live registry of running background tasks — populated by
8821
+ * `background:start` (and `child:background:start`), drained by
8822
+ * `background:exit` (and the child sibling). Same shape as
8823
+ * {@link InFlightToolCall} so it merges cleanly into the cancel-tool
8824
+ * picker's snapshot; the `kind: 'task'` discriminator routes the
8825
+ * cancel callback to `agent.killBackgroundTask(taskId)` instead of
8826
+ * `agent.cancelTool(callId)`.
8827
+ *
8828
+ * Tracked separately from `inFlightTools` because the two have
8829
+ * different lifetimes: a tool call is in-flight only while
8830
+ * `tool:before` and `tool:after` straddle, but a background task
8831
+ * survives PAST its spawning tool call — the `shell` body returns
8832
+ * the handle and `tool:after` fires while the task keeps running.
8833
+ * Without this separate registry, `ctrl+k` would never see a task
8834
+ * because the tool entry has already drained.
8835
+ */
8836
+ const [backgroundTasks, setBackgroundTasks] = useState([]);
8837
+ const backgroundTasksRef = useRef([]);
8838
+ backgroundTasksRef.current = backgroundTasks;
8839
+ /**
8840
+ * Names of currently-active skills, tracked via `skills:activate` /
8841
+ * `skills:deactivate` hooks. Drives the footer's "✦ N skill(s)"
8842
+ * chip — the user's only passive surface for noticing that a skill
8843
+ * (and its `allowed-tools` restrictions) is in effect. Cleared on
8844
+ * session teardown alongside the rest of the per-session live state.
8845
+ *
8846
+ * Stored as a Set rather than an array so dedup is structural (a
8847
+ * runaway `skills:activate` for the same name doesn't inflate the
8848
+ * count). React state is the snapshot we render against; a fresh
8849
+ * Set per update gives React identity-based change detection.
8850
+ */
8851
+ const [activeSkillNames, setActiveSkillNames] = useState(() => /* @__PURE__ */ new Set());
8852
+ /**
8853
+ * Mirror of {@link activeSkillNames} for synchronous reads in
8854
+ * `onSubmitPrompt`. The submit path runs outside React's render cycle
8855
+ * and needs to pre-activate every user-pinned skill before
8856
+ * `agent.run()` — listing the state in `useCallback`'s deps would
8857
+ * re-bind the handler on every activation change, which would
8858
+ * invalidate the textarea's submit binding on every `/skill` trigger.
8859
+ * The ref keeps the binding stable and the read fresh.
8860
+ */
8861
+ const activeSkillNamesRef = useRef(activeSkillNames);
8862
+ activeSkillNamesRef.current = activeSkillNames;
8863
+ const registerInFlightTool = useCallback((entry) => {
8864
+ setInFlightTools((prev) => {
8865
+ return [...prev.filter((e) => e.callId !== entry.callId), entry];
8866
+ });
8867
+ }, []);
8868
+ const unregisterInFlightTool = useCallback((callId) => {
8869
+ setInFlightTools((prev) => prev.filter((e) => e.callId !== callId));
8870
+ }, []);
8871
+ /**
8242
8872
  * In-flight auto-compaction promise. Held in a ref so the next
8243
8873
  * `onSubmitPrompt` invocation can `await` it before calling
8244
8874
  * `agent.run()` — this is what prevents a user-submitted prompt from
@@ -8324,6 +8954,10 @@ function AppShell() {
8324
8954
  persistThreshold: 0,
8325
8955
  persistDir
8326
8956
  } : { persistDir };
8957
+ const tasksDir = resolveTasksDir({
8958
+ userDir: dataDir,
8959
+ sessionId: session.id
8960
+ });
8327
8961
  const agent = createAgent({
8328
8962
  ...profile.preset,
8329
8963
  ...builtInSystem ? { system: builtInSystem } : {},
@@ -8338,7 +8972,8 @@ function AppShell() {
8338
8972
  },
8339
8973
  behavior: {
8340
8974
  ...profile.preset.behavior ?? {},
8341
- ...persistBehavior
8975
+ ...persistBehavior,
8976
+ tasksDir
8342
8977
  },
8343
8978
  provider: descriptor.factory(),
8344
8979
  session,
@@ -8491,9 +9126,14 @@ function AppShell() {
8491
9126
  agent.hooks.hook("stream:thinking", ({ delta, turnId }) => stream.queueStreamDelta("thinking", delta, { turnId }));
8492
9127
  agent.hooks.hook("stream:text", ({ delta, turnId }) => stream.queueStreamDelta("markdown", delta, { turnId }));
8493
9128
  agent.hooks.hook("tool:before", async ({ callId, name, input, turnId }) => {
9129
+ registerInFlightTool({
9130
+ callId,
9131
+ tool: name,
9132
+ startedAt: Date.now()
9133
+ });
8494
9134
  if (pendingAnnotationsRef.current.has(callId)) return;
8495
9135
  let priorContent;
8496
- if (name === "write_file" && agent.handle && typeof input.path === "string") try {
9136
+ if (EDIT_TOOL_NAMES.has(name) && agent.handle && typeof input.path === "string") try {
8497
9137
  priorContent = await agent.execution.readFile(agent.handle, input.path);
8498
9138
  } catch {}
8499
9139
  const edit = extractEditPayload(name, input, priorContent);
@@ -8508,6 +9148,7 @@ function AppShell() {
8508
9148
  });
8509
9149
  });
8510
9150
  agent.hooks.hook("tool:after", ({ callId, name, result, turnId }) => {
9151
+ unregisterInFlightTool(callId);
8511
9152
  const raw = toolResultText(result);
8512
9153
  const text = name === "spawn" ? stripSpawnTokensLine(raw) : raw;
8513
9154
  stream.appendImmediate({
@@ -8518,6 +9159,69 @@ function AppShell() {
8518
9159
  turnId
8519
9160
  });
8520
9161
  });
9162
+ agent.hooks.hook("tool:error", ({ callId }) => {
9163
+ unregisterInFlightTool(callId);
9164
+ });
9165
+ agent.hooks.hook("tool:cancelled", ({ callId }) => {
9166
+ unregisterInFlightTool(callId);
9167
+ });
9168
+ const registerBackgroundTask = (ctx) => {
9169
+ setBackgroundTasks((prev) => [...prev, {
9170
+ kind: "task",
9171
+ callId: ctx.taskId,
9172
+ tool: `shell (background): ${previewLine(ctx.command, 60)}`,
9173
+ startedAt: ctx.startedAt,
9174
+ ...ctx.childId ? { childId: ctx.childId } : {}
9175
+ }]);
9176
+ };
9177
+ const dropBackgroundTaskAndEmitBanner = (ctx) => {
9178
+ setBackgroundTasks((prev) => prev.filter((t) => t.callId !== ctx.taskId));
9179
+ streamRef.current?.appendImmediate({
9180
+ kind: "task-notification",
9181
+ text: formatTaskSummary(ctx),
9182
+ task: {
9183
+ taskId: ctx.taskId,
9184
+ status: ctx.status,
9185
+ exitCode: ctx.exitCode,
9186
+ outputPath: ctx.outputPath,
9187
+ command: ctx.command,
9188
+ durationMs: ctx.durationMs
9189
+ },
9190
+ ...ctx.childId ? { childId: ctx.childId } : {},
9191
+ ...typeof ctx.depth === "number" ? { depth: ctx.depth } : {}
9192
+ });
9193
+ };
9194
+ agent.hooks.hook("background:start", (ctx) => registerBackgroundTask(ctx));
9195
+ agent.hooks.hook("background:exit", (ctx) => dropBackgroundTaskAndEmitBanner(ctx));
9196
+ agent.hooks.hook("background:reassign", (ctx) => {
9197
+ setBackgroundTasks((prev) => prev.map((entry) => entry.callId === ctx.taskId ? {
9198
+ kind: "task",
9199
+ callId: entry.callId,
9200
+ tool: entry.tool,
9201
+ startedAt: entry.startedAt
9202
+ } : entry));
9203
+ });
9204
+ agent.hooks.hook("child:background:start", (ctx) => registerBackgroundTask(ctx));
9205
+ agent.hooks.hook("child:background:exit", (ctx) => dropBackgroundTaskAndEmitBanner(ctx));
9206
+ agent.hooks.hook("skills:activate", ({ skill }) => {
9207
+ setActiveSkillNames((prev) => {
9208
+ if (prev.has(skill.name)) return prev;
9209
+ const next = new Set(prev);
9210
+ next.add(skill.name);
9211
+ persistPinnedSkills(session, next);
9212
+ return next;
9213
+ });
9214
+ });
9215
+ agent.hooks.hook("skills:deactivate", ({ skill, reason }) => {
9216
+ if (reason === "run-end") return;
9217
+ setActiveSkillNames((prev) => {
9218
+ if (!prev.has(skill.name)) return prev;
9219
+ const next = new Set(prev);
9220
+ next.delete(skill.name);
9221
+ persistPinnedSkills(session, next);
9222
+ return next;
9223
+ });
9224
+ });
8521
9225
  agent.hooks.hook("mcp:tool:after", ({ callId, displayName, result, turnId }) => {
8522
9226
  stream.appendImmediate({
8523
9227
  kind: "tool-result",
@@ -8540,7 +9244,7 @@ function AppShell() {
8540
9244
  stream.flushAndUpdate(finalizeStreamingMarkdown);
8541
9245
  });
8542
9246
  agent.hooks.hook("spawn:before", ({ id, task, depth }) => {
8543
- const taskPreview = task.length > 80 ? `${task.slice(0, 80)}…` : task;
9247
+ const taskPreview = previewLine(task, 80);
8544
9248
  stream.appendImmediate({
8545
9249
  kind: "spawn-start",
8546
9250
  text: taskPreview,
@@ -8580,6 +9284,12 @@ function AppShell() {
8580
9284
  });
8581
9285
  });
8582
9286
  agent.hooks.hook("child:tool:before", ({ callId, name, input, childId, depth, turnId, priorContent }) => {
9287
+ registerInFlightTool({
9288
+ callId,
9289
+ tool: name,
9290
+ startedAt: Date.now(),
9291
+ childId
9292
+ });
8583
9293
  if (pendingAnnotationsRef.current.has(callId)) return;
8584
9294
  const edit = extractEditPayload(name, input, priorContent);
8585
9295
  stream.appendImmediate({
@@ -8595,6 +9305,7 @@ function AppShell() {
8595
9305
  });
8596
9306
  });
8597
9307
  agent.hooks.hook("child:tool:after", ({ callId, name, result, childId, depth, turnId }) => {
9308
+ unregisterInFlightTool(callId);
8598
9309
  stream.appendImmediate({
8599
9310
  kind: "tool-result",
8600
9311
  text: toolResultText(result),
@@ -8605,6 +9316,12 @@ function AppShell() {
8605
9316
  turnId
8606
9317
  });
8607
9318
  });
9319
+ agent.hooks.hook("child:tool:error", ({ callId }) => {
9320
+ unregisterInFlightTool(callId);
9321
+ });
9322
+ agent.hooks.hook("child:tool:cancelled", ({ callId }) => {
9323
+ unregisterInFlightTool(callId);
9324
+ });
8608
9325
  agent.hooks.hook("child:stream:end", ({ childId }) => {
8609
9326
  stream.flushAndUpdate((prev) => finalizeStreamingMarkdownForOwner(prev, childId));
8610
9327
  });
@@ -8621,7 +9338,9 @@ function AppShell() {
8621
9338
  config.prefix,
8622
9339
  interactions,
8623
9340
  dataDir,
8624
- mcpCredentialStore
9341
+ mcpCredentialStore,
9342
+ registerInFlightTool,
9343
+ unregisterInFlightTool
8625
9344
  ]);
8626
9345
  const refreshSessions = useCallback(async () => {
8627
9346
  const list = await listSessionMeta(store, settings.showAllProjects ? void 0 : { projectRoot: projectDir });
@@ -8662,6 +9381,9 @@ function AppShell() {
8662
9381
  runningRef.current = false;
8663
9382
  sessionSafelistRef.current.clear();
8664
9383
  pendingAnnotationsRef.current.clear();
9384
+ setInFlightTools([]);
9385
+ setBackgroundTasks([]);
9386
+ setActiveSkillNames(/* @__PURE__ */ new Set());
8665
9387
  }, [
8666
9388
  stream,
8667
9389
  denyAll,
@@ -8739,6 +9461,7 @@ function AppShell() {
8739
9461
  });
8740
9462
  sessionRef.current = session;
8741
9463
  agentRef.current = buildAgent(session, key);
9464
+ setActiveSkillNames(readPinnedSkills(session.metadata["zidane.activeSkills"]));
8742
9465
  setEvents(eventsFromTurns(session.turns, session.runs));
8743
9466
  const replayedTokens = lastContextSizeFromTurns(session.turns, session.runs);
8744
9467
  setLastInputTokens(replayedTokens);
@@ -9094,7 +9817,8 @@ function AppShell() {
9094
9817
  ...refSpans.length > 0 ? { refs: refSpans } : {}
9095
9818
  });
9096
9819
  if (autoCompactInFlightRef.current) await autoCompactInFlightRef.current.catch(() => {});
9097
- const skillNames = uniqueSkillNamesFromReferences(references);
9820
+ const newRefs = uniqueSkillNamesFromReferences(references);
9821
+ const skillNames = Array.from(new Set([...activeSkillNamesRef.current, ...newRefs]));
9098
9822
  for (const name of skillNames) try {
9099
9823
  await agent.activateSkill(name);
9100
9824
  } catch (err) {
@@ -9332,6 +10056,36 @@ function AppShell() {
9332
10056
  return nextTurns.some((t) => t.id === prev) ? prev : null;
9333
10057
  });
9334
10058
  }, []);
10059
+ const onEditTurn = useCallback(async (turnId, newText) => {
10060
+ const session = sessionRef.current;
10061
+ if (!session) return;
10062
+ const turn = session.turns.find((t) => t.id === turnId);
10063
+ if (!turn) return;
10064
+ if (!turn.content.some((b) => b.type === "text")) return;
10065
+ const nonTextBlocks = turn.content.filter((b) => b.type !== "text");
10066
+ const firstTextIdx = turn.content.findIndex((b) => b.type === "text");
10067
+ const updatedContent = [...nonTextBlocks];
10068
+ if (newText.trim()) updatedContent.splice(firstTextIdx >= 0 ? Math.min(firstTextIdx, updatedContent.length) : 0, 0, {
10069
+ type: "text",
10070
+ text: newText
10071
+ });
10072
+ turn.content = updatedContent;
10073
+ session.setTurns([...session.turns]);
10074
+ try {
10075
+ await session.save();
10076
+ } catch (err) {
10077
+ debugLog("edit: save failed", err);
10078
+ return;
10079
+ }
10080
+ setEvents(eventsFromTurns(session.turns, session.runs));
10081
+ const replayedTokens = lastContextSizeFromTurns(session.turns, session.runs);
10082
+ setLastInputTokens(replayedTokens);
10083
+ lastInputTokensRef.current = replayedTokens;
10084
+ setCurrentSession((prev) => prev ? {
10085
+ ...prev,
10086
+ updatedAt: Date.now()
10087
+ } : prev);
10088
+ }, []);
9335
10089
  /**
9336
10090
  * Identity of the session row the user has focused on the sessions
9337
10091
  * screen — single source of truth. `SessionsScreen` is rendered fully
@@ -9357,6 +10111,10 @@ function AppShell() {
9357
10111
  userDir: dataDir,
9358
10112
  sessionId: id
9359
10113
  }));
10114
+ cleanupPersistedSession(resolveTasksDir({
10115
+ userDir: dataDir,
10116
+ sessionId: id
10117
+ }));
9360
10118
  const wasCurrent = id === currentSession?.id;
9361
10119
  if (wasCurrent) {
9362
10120
  await teardown();
@@ -9628,7 +10386,8 @@ function AppShell() {
9628
10386
  total: turnIds.length,
9629
10387
  actions: {
9630
10388
  onFork: onForkTurn,
9631
- onDelete: onDeleteTurn
10389
+ onDelete: onDeleteTurn,
10390
+ onEdit: onEditTurn
9632
10391
  },
9633
10392
  keybindings
9634
10393
  }));
@@ -9638,6 +10397,7 @@ function AppShell() {
9638
10397
  turnIds,
9639
10398
  onForkTurn,
9640
10399
  onDeleteTurn,
10400
+ onEditTurn,
9641
10401
  keybindings
9642
10402
  ]);
9643
10403
  useKeyboard((key) => {
@@ -9746,6 +10506,29 @@ function AppShell() {
9746
10506
  onCycleAgent();
9747
10507
  return;
9748
10508
  }
10509
+ if (matchesBinding(key, keybindings.cancelToolCall) && screen === "chat" && !pendingApproval) {
10510
+ const tools = inFlightToolsRef.current.filter((entry) => entry.childId === void 0);
10511
+ const tasks = backgroundTasksRef.current.filter((entry) => entry.childId === void 0);
10512
+ const snapshot = [...tools, ...tasks];
10513
+ if (snapshot.length === 0) return;
10514
+ modal.open(/* @__PURE__ */ jsx(CancelToolModal, {
10515
+ inFlight: snapshot,
10516
+ onCancel: (entry, reason) => {
10517
+ const agent = agentRef.current;
10518
+ if (!agent) return false;
10519
+ if (entry.kind === "task") return agent.killBackgroundTask(entry.callId);
10520
+ return agent.cancelTool(entry.callId, reason);
10521
+ },
10522
+ onCancelAll: () => {
10523
+ const agent = agentRef.current;
10524
+ if (!agent) return;
10525
+ for (const entry of snapshot) if (entry.kind === "task") agent.killBackgroundTask(entry.callId);
10526
+ else agent.cancelTool(entry.callId, "user-cancelled-all");
10527
+ },
10528
+ onClose: () => modal.close()
10529
+ }));
10530
+ return;
10531
+ }
9749
10532
  if (key.name !== "escape") return;
9750
10533
  if (busy || pendingApproval) return onAbort();
9751
10534
  if (popupOpenRef.current) return;
@@ -9777,7 +10560,10 @@ function AppShell() {
9777
10560
  effortKeyColor: COLOR.warn,
9778
10561
  agentLabel: pickedAgent.label,
9779
10562
  agentColor: accentColor(pickedAgent.accent, COLOR),
9780
- keybindings
10563
+ keybindings,
10564
+ inFlightToolCount: inFlightTools.reduce((n, entry) => entry.childId === void 0 ? n + 1 : n, 0) + backgroundTasks.reduce((n, entry) => entry.childId === void 0 ? n + 1 : n, 0),
10565
+ activeSkillCount: activeSkillNames.size,
10566
+ skillsChipColor: COLOR.brand
9781
10567
  }), [
9782
10568
  screen,
9783
10569
  busy,
@@ -9790,7 +10576,10 @@ function AppShell() {
9790
10576
  pickedAgent,
9791
10577
  COLOR,
9792
10578
  modelHasReasoning,
9793
- keybindings
10579
+ keybindings,
10580
+ inFlightTools,
10581
+ backgroundTasks,
10582
+ activeSkillNames
9794
10583
  ]);
9795
10584
  const queuedMessagePreviews = useMemo(() => messageQueue.map((m) => ({
9796
10585
  text: m.prompt,
@@ -9909,7 +10698,7 @@ function effortForModel(descriptor, modelId, remembered) {
9909
10698
  * secondary `/n` chord with the current effort label, surfacing the
9910
10699
  * effort picker as a discoverable, in-place affordance.
9911
10700
  */
9912
- function buildHints({ screen, busy, pending, pendingInteractionLive, pendingInteractionResumed, currentSession, hasMultipleAgents, modelLabel, modelColor, effortLabel, effortColor, effortKeyColor, agentLabel, agentColor, keybindings }) {
10701
+ function buildHints({ screen, busy, pending, pendingInteractionLive, pendingInteractionResumed, currentSession, hasMultipleAgents, modelLabel, modelColor, effortLabel, effortColor, effortKeyColor, agentLabel, agentColor, keybindings, inFlightToolCount, activeSkillCount, skillsChipColor }) {
9913
10702
  if (pending) return [
9914
10703
  {
9915
10704
  key: "↑↓",
@@ -9952,10 +10741,18 @@ function buildHints({ screen, busy, pending, pendingInteractionLive, pendingInte
9952
10741
  label: "leave for later"
9953
10742
  }
9954
10743
  ];
9955
- if (busy) return [{
9956
- key: "esc",
9957
- label: "abort"
9958
- }];
10744
+ if (busy) {
10745
+ const baseBusyHints = [];
10746
+ if (inFlightToolCount > 0) baseBusyHints.push({
10747
+ key: keybindings.cancelToolCall,
10748
+ label: inFlightToolCount === 1 ? "cancel" : `cancel (${inFlightToolCount})`
10749
+ });
10750
+ baseBusyHints.push({
10751
+ key: "esc",
10752
+ label: "abort"
10753
+ });
10754
+ return baseBusyHints;
10755
+ }
9959
10756
  if (screen === "auth") return [
9960
10757
  {
9961
10758
  key: "↑↓",
@@ -10003,6 +10800,16 @@ function buildHints({ screen, busy, pending, pendingInteractionLive, pendingInte
10003
10800
  labelColor: effortColor
10004
10801
  } } : {}
10005
10802
  } : null;
10803
+ const skillsChip = activeSkillCount > 0 ? {
10804
+ key: "✦",
10805
+ keyColor: skillsChipColor,
10806
+ label: activeSkillCount === 1 ? "1 skill" : `${activeSkillCount} skills`,
10807
+ labelColor: skillsChipColor
10808
+ } : null;
10809
+ const cancelTaskChip = inFlightToolCount > 0 ? {
10810
+ key: keybindings.cancelToolCall,
10811
+ label: inFlightToolCount === 1 ? "cancel task" : `cancel task (${inFlightToolCount})`
10812
+ } : null;
10006
10813
  return [
10007
10814
  ...hasMultipleAgents ? [{
10008
10815
  key: keybindings.cycleAgent,
@@ -10010,6 +10817,8 @@ function buildHints({ screen, busy, pending, pendingInteractionLive, pendingInte
10010
10817
  labelColor: agentColor
10011
10818
  }] : [],
10012
10819
  ...modelHint ? [modelHint] : [],
10820
+ ...skillsChip ? [skillsChip] : [],
10821
+ ...cancelTaskChip ? [cancelTaskChip] : [],
10013
10822
  ...currentSession ? [{
10014
10823
  key: keybindings.openSessionDetails,
10015
10824
  label: "session"