@quandev104/pi-style 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +1 -1
  3. package/dist/extensions/pi-style.js +1256 -491
  4. package/dist/extensions/pi-style.js.map +1 -1
  5. package/extension-src/pi-style/app/index.ts +3 -2
  6. package/extension-src/pi-style/app/runtime.ts +99 -86
  7. package/extension-src/pi-style/app/snapshot.ts +41 -2
  8. package/extension-src/pi-style/domain/status-renderer.ts +40 -8
  9. package/extension-src/pi-style/domain/status.ts +15 -5
  10. package/extension-src/pi-style/domain/theme.ts +32 -1
  11. package/extension-src/pi-style/features/editor/index.ts +97 -66
  12. package/extension-src/pi-style/features/messages/index.ts +469 -90
  13. package/extension-src/pi-style/features/status-line/index.ts +41 -11
  14. package/extension-src/pi-style/features/tools/bash-execution.ts +12 -1
  15. package/extension-src/pi-style/features/tools/boxed/bash.ts +195 -66
  16. package/extension-src/pi-style/features/tools/boxed/batch.ts +60 -10
  17. package/extension-src/pi-style/features/tools/boxed/edit.ts +45 -25
  18. package/extension-src/pi-style/features/tools/boxed/find.ts +9 -4
  19. package/extension-src/pi-style/features/tools/boxed/git.ts +46 -2
  20. package/extension-src/pi-style/features/tools/boxed/grep.ts +9 -2
  21. package/extension-src/pi-style/features/tools/boxed/ls.ts +9 -4
  22. package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +45 -22
  23. package/extension-src/pi-style/features/tools/boxed/read.ts +4 -2
  24. package/extension-src/pi-style/features/tools/boxed/session-config.ts +45 -14
  25. package/extension-src/pi-style/features/tools/boxed/shared.ts +51 -0
  26. package/extension-src/pi-style/features/tools/boxed/turn-summary.ts +12 -0
  27. package/extension-src/pi-style/pi/compatibility-probe.ts +1 -1
  28. package/extension-src/pi-style/pi/index.ts +28 -13
  29. package/extension-src/pi-style/pi/session-usage.ts +204 -21
  30. package/extension-src/pi-style/shared/ansi.ts +17 -5
  31. package/extension-src/pi-style/shared/box.ts +83 -6
  32. package/extension-src/pi-style/shared/split-diff.ts +8 -5
  33. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  // extension-src/pi-style/shared/ansi.ts
2
+ import { visibleWidth as tuiVisibleWidth } from "@earendil-works/pi-tui";
2
3
  function isFinal(byte) {
3
4
  return byte >= "@" && byte <= "~";
4
5
  }
@@ -82,7 +83,7 @@ function stripAnsi(value) {
82
83
  return output;
83
84
  }
84
85
  function visibleWidth(value) {
85
- return [...stripAnsi(value)].length;
86
+ return tuiVisibleWidth(value);
86
87
  }
87
88
  function resetAnsi(value) {
88
89
  return `${value}\x1B[0m`;
@@ -93,9 +94,10 @@ function fitAnsiWidth(value, width, ellipsis = "\u2026") {
93
94
  function truncateAnsi(value, width, ellipsis = "\u2026") {
94
95
  if (width <= 0) return "";
95
96
  if (visibleWidth(value) <= width) return resetAnsi(value);
97
+ const ellipsisWidth = visibleWidth(ellipsis);
96
98
  let output = "";
97
99
  let visible = 0;
98
- for (let i = 0; i < value.length && visible < width - visibleWidth(ellipsis); i++) {
100
+ for (let i = 0; i < value.length && visible < width - ellipsisWidth; i++) {
99
101
  if (value.charCodeAt(i) === 27) {
100
102
  const start = i;
101
103
  i++;
@@ -183,9 +185,9 @@ function rgbTo256(r, g, b) {
183
185
  function colorEscapes(mode, hex, kind) {
184
186
  if (!isHexColor(hex)) return void 0;
185
187
  const { r, g, b } = hexToRgb(hex);
186
- const cacheKey = `${mode}:${kind}:${hex}`;
188
+ const cacheKey2 = `${mode}:${kind}:${hex}`;
187
189
  const cache = kind === "fg" ? fgEscapeCache : bgEscapeCache;
188
- let cached = cache.get(cacheKey);
190
+ let cached = cache.get(cacheKey2);
189
191
  if (!cached) {
190
192
  if (mode === "256color") {
191
193
  const idx = rgbTo256(r, g, b);
@@ -199,7 +201,7 @@ function colorEscapes(mode, hex, kind) {
199
201
  suffix: kind === "fg" ? "\x1B[39m" : RESET_BACKGROUND
200
202
  };
201
203
  }
202
- cache.set(cacheKey, cached);
204
+ cache.set(cacheKey2, cached);
203
205
  }
204
206
  return cached;
205
207
  }
@@ -233,7 +235,7 @@ function getElapsedMs(result) {
233
235
  // extension-src/pi-style/shared/render-budget.ts
234
236
  import {
235
237
  truncateToWidth as tuiTruncateToWidth,
236
- visibleWidth as tuiVisibleWidth,
238
+ visibleWidth as tuiVisibleWidth2,
237
239
  wrapTextWithAnsi
238
240
  } from "@earendil-works/pi-tui";
239
241
  var MAX_RENDER_LINE_CHARS = 2e3;
@@ -411,8 +413,8 @@ function truncatePrintableAscii(text, width, ellipsis = TRUNCATE_ELLIPSIS) {
411
413
  const targetWidth = Math.max(0, width - ellipsisWidth);
412
414
  return { text: `${text.slice(0, targetWidth)}${ellipsis}`, visibleWidth: targetWidth + ellipsisWidth };
413
415
  }
414
- function truncateSgrAscii(text, width, ellipsis, visibleWidth5) {
415
- if (visibleWidth5 <= width) return { text, visibleWidth: visibleWidth5 };
416
+ function truncateSgrAscii(text, width, ellipsis, visibleWidth4) {
417
+ if (visibleWidth4 <= width) return { text, visibleWidth: visibleWidth4 };
416
418
  const ellipsisWidth = knownEllipsisWidth(ellipsis);
417
419
  if (ellipsisWidth === null) return null;
418
420
  if (ellipsisWidth >= width) return truncatePrintableAscii(ellipsis, width, "");
@@ -455,7 +457,7 @@ function safeVisibleWidth(text) {
455
457
  const fastWidth = knownVisibleWidth(text);
456
458
  if (fastWidth) return fastWidth.visibleWidth;
457
459
  if (text === TRUNCATE_ELLIPSIS) return 1;
458
- return tuiVisibleWidth(text);
460
+ return tuiVisibleWidth2(text);
459
461
  }
460
462
  function safeTruncateToWidth(text, maxWidth, ellipsis = "...", pad = false) {
461
463
  const width = Math.floor(maxWidth);
@@ -684,10 +686,10 @@ function setFullTheme(theme, force = false) {
684
686
  const themeName = resolveThemeName(theme);
685
687
  const sourcePath = resolveThemeSourcePath(theme);
686
688
  if (!themeName && !sourcePath) return;
687
- const cacheKey = sourcePath || themeName;
688
- if (!force && cacheKey === cachedThemeName && (cachedExtras !== null || cachedVars !== null || cachedColors !== null || cachedThemeExport !== null))
689
+ const cacheKey2 = sourcePath || themeName;
690
+ if (!force && cacheKey2 === cachedThemeName && (cachedExtras !== null || cachedVars !== null || cachedColors !== null || cachedThemeExport !== null))
689
691
  return;
690
- cachedThemeName = cacheKey;
692
+ cachedThemeName = cacheKey2;
691
693
  const result = readThemeDiscoveryFromPath(sourcePath) ?? (themeName ? discoverThemeExtras(themeName) : null);
692
694
  cachedExtras = result?.extras ?? null;
693
695
  cachedVars = result?.vars ?? null;
@@ -703,6 +705,15 @@ function getThemeExtra(_theme, key) {
703
705
  }
704
706
 
705
707
  // extension-src/pi-style/shared/box.ts
708
+ var THEME_CACHE_KEYS = /* @__PURE__ */ new WeakMap();
709
+ var nextThemeCacheKey = 1;
710
+ function themeCacheKey(theme) {
711
+ const cached = THEME_CACHE_KEYS.get(theme);
712
+ if (cached !== void 0) return cached;
713
+ const created = nextThemeCacheKey++;
714
+ THEME_CACHE_KEYS.set(theme, created);
715
+ return created;
716
+ }
706
717
  function shortenPath(path) {
707
718
  const home = homedir2();
708
719
  if (path.startsWith(home)) return `~${path.slice(home.length)}`;
@@ -734,12 +745,48 @@ function getTextOutput(result) {
734
745
  });
735
746
  return textBlocks.map((contentBlock) => String(contentBlock.text ?? "")).join("\n").replace(/\r/g, "");
736
747
  }
748
+ var WORD_CP_UNKNOWN = 255;
749
+ var WORD_CP_SURROGATE = 254;
750
+ var WORD_CP_CLASS = new Uint8Array(65536).fill(WORD_CP_UNKNOWN);
751
+ for (let code = 48; code <= 57; code++) WORD_CP_CLASS[code] = 1;
752
+ for (let code = 65; code <= 90; code++) WORD_CP_CLASS[code] = 1;
753
+ for (let code = 97; code <= 122; code++) WORD_CP_CLASS[code] = 1;
754
+ WORD_CP_CLASS[39] = 1;
755
+ WORD_CP_CLASS[45] = 1;
756
+ WORD_CP_CLASS[95] = 1;
757
+ WORD_CP_CLASS.fill(WORD_CP_SURROGATE, 55296, 57344);
758
+ var NON_ASCII_WORD_RE = /[\p{L}\p{N}]/u;
759
+ var ASTRAL_WORD_CP = /* @__PURE__ */ new Map();
760
+ function astralWordMembership(codePoint) {
761
+ const cached = ASTRAL_WORD_CP.get(codePoint);
762
+ if (cached !== void 0) return cached;
763
+ const membership = NON_ASCII_WORD_RE.test(String.fromCodePoint(codePoint)) ? 1 : 0;
764
+ ASTRAL_WORD_CP.set(codePoint, membership);
765
+ return membership;
766
+ }
737
767
  function countWords(text) {
768
+ const len = text.length;
738
769
  let count = 0;
739
- let inWord = false;
740
- for (const char of text) {
741
- const isWord = /[\p{L}\p{N}_'-]/u.test(char);
742
- if (isWord && !inWord) count++;
770
+ let inWord = 0;
771
+ for (let i = 0; i < len; i++) {
772
+ const code = text.charCodeAt(i);
773
+ const membership = WORD_CP_CLASS[code] ?? 0;
774
+ let isWord;
775
+ if (membership <= 1) {
776
+ isWord = membership;
777
+ } else if (membership === WORD_CP_SURROGATE) {
778
+ const next = i + 1 < len ? text.charCodeAt(i + 1) : 0;
779
+ if (code <= 56319 && next >= 56320 && next <= 57343) {
780
+ isWord = astralWordMembership(65536 + (code - 55296 << 10) + (next - 56320));
781
+ i++;
782
+ } else {
783
+ isWord = 0;
784
+ }
785
+ } else {
786
+ isWord = NON_ASCII_WORD_RE.test(String.fromCharCode(code)) ? 1 : 0;
787
+ WORD_CP_CLASS[code] = isWord;
788
+ }
789
+ count += isWord & (inWord ^ 1);
743
790
  inWord = isWord;
744
791
  }
745
792
  return count;
@@ -868,7 +915,7 @@ function formatBoxedRunningStatus(theme, elapsedMs) {
868
915
  function dimLine(text) {
869
916
  return `\x1B[2m${text}\x1B[22m`;
870
917
  }
871
- function boxFrameText(theme, text) {
918
+ function boxFrameText(_theme, text) {
872
919
  return dimLine(text);
873
920
  }
874
921
  function boxBorder(theme, left, right, width) {
@@ -1445,6 +1492,18 @@ function setToolsRenderConfig(config) {
1445
1492
  function getToolsRenderConfig() {
1446
1493
  return sessionToolsConfig;
1447
1494
  }
1495
+ function getToolsRenderCacheSignature() {
1496
+ return [
1497
+ sessionToolsConfig.maxCollapsedLines,
1498
+ sessionToolsConfig.maxExpandedLines,
1499
+ sessionToolsConfig.dimOutput ? 1 : 0,
1500
+ sessionToolsConfig.showElapsed ? 1 : 0,
1501
+ sessionToolsConfig.batchOpenGlyph,
1502
+ sessionToolsConfig.nerdFonts ? 1 : 0,
1503
+ sessionToolsConfig.collapseAfterTurn ? 1 : 0,
1504
+ sessionToolsConfig.collapseMutatingTools ? 1 : 0
1505
+ ].join("|");
1506
+ }
1448
1507
  var STARTED_AT_KEY = "__piStyleStartedAt";
1449
1508
  var ENDED_AT_KEY = "__piStyleEndedAt";
1450
1509
  var RESULT_SEEN_KEY = "__piStyleResultSeen";
@@ -1472,27 +1531,35 @@ function markResultSeen(state) {
1472
1531
  if (!state || typeof state !== "object") return;
1473
1532
  state[RESULT_SEEN_KEY] = true;
1474
1533
  }
1475
- var tickerStates = /* @__PURE__ */ new Set();
1534
+ var tickerEntries = /* @__PURE__ */ new Map();
1535
+ var sharedTickerHandle;
1536
+ function ensureSharedTicker() {
1537
+ if (sharedTickerHandle !== void 0 || tickerEntries.size === 0) return;
1538
+ sharedTickerHandle = setInterval(() => {
1539
+ for (const { invalidate } of tickerEntries.values()) invalidate();
1540
+ }, 1e3);
1541
+ }
1542
+ function stopSharedTickerIfIdle() {
1543
+ if (sharedTickerHandle === void 0 || tickerEntries.size > 0) return;
1544
+ clearInterval(sharedTickerHandle);
1545
+ sharedTickerHandle = void 0;
1546
+ }
1476
1547
  function startElapsedTicker(state, invalidate) {
1477
1548
  if (!state || typeof state !== "object") return;
1478
- if (state[TICKER_KEY] !== void 0) return;
1479
- state[TICKER_KEY] = setInterval(() => invalidate(), 1e3);
1480
- tickerStates.add(state);
1549
+ state[TICKER_KEY] = true;
1550
+ tickerEntries.set(state, { invalidate });
1551
+ ensureSharedTicker();
1481
1552
  }
1482
1553
  function stopElapsedTicker(state) {
1483
1554
  if (!state || typeof state !== "object") return;
1484
- const handle = state[TICKER_KEY];
1485
- if (handle !== void 0) clearInterval(handle);
1486
1555
  delete state[TICKER_KEY];
1487
- tickerStates.delete(state);
1556
+ tickerEntries.delete(state);
1557
+ stopSharedTickerIfIdle();
1488
1558
  }
1489
1559
  function stopAllElapsedTickers() {
1490
- for (const state of tickerStates) {
1491
- const handle = state[TICKER_KEY];
1492
- if (handle !== void 0) clearInterval(handle);
1493
- delete state[TICKER_KEY];
1494
- }
1495
- tickerStates.clear();
1560
+ for (const state of tickerEntries.keys()) delete state[TICKER_KEY];
1561
+ tickerEntries.clear();
1562
+ stopSharedTickerIfIdle();
1496
1563
  }
1497
1564
 
1498
1565
  // extension-src/pi-style/features/tools/boxed/batch.ts
@@ -1528,6 +1595,7 @@ function createBatch(meta, leaderId, detail, opts = {}) {
1528
1595
  leaderId,
1529
1596
  startedAt: performance.now(),
1530
1597
  closed: false,
1598
+ revision: 0,
1531
1599
  members: [
1532
1600
  {
1533
1601
  toolCallId: leaderId,
@@ -1543,14 +1611,20 @@ function createBatch(meta, leaderId, detail, opts = {}) {
1543
1611
  batchByCallId.set(leaderId, batch);
1544
1612
  return batch;
1545
1613
  }
1614
+ function bumpBatchRevision(batch) {
1615
+ batch.revision++;
1616
+ delete batch.renderCache;
1617
+ }
1546
1618
  function registerBatchCall(meta, detail, context, opts = {}) {
1547
1619
  const existing = batchByCallId.get(context.toolCallId);
1548
1620
  if (existing) {
1549
1621
  const member2 = existing.members.find((entry) => entry.toolCallId === context.toolCallId);
1550
1622
  if (member2) {
1623
+ const changed = member2.detail !== detail || member2.pattern !== opts.pattern || member2.pathLabel !== opts.pathLabel;
1551
1624
  member2.detail = detail;
1552
1625
  if (opts.pattern !== void 0) member2.pattern = opts.pattern;
1553
1626
  if (opts.pathLabel !== void 0) member2.pathLabel = opts.pathLabel;
1627
+ if (changed) bumpBatchRevision(existing);
1554
1628
  }
1555
1629
  return { batch: existing, isLeader: existing.leaderId === context.toolCallId };
1556
1630
  }
@@ -1569,6 +1643,7 @@ function registerBatchCall(meta, detail, context, opts = {}) {
1569
1643
  };
1570
1644
  current.members.push(member);
1571
1645
  batchByCallId.set(context.toolCallId, current);
1646
+ bumpBatchRevision(current);
1572
1647
  return { batch: current, isLeader: false };
1573
1648
  }
1574
1649
  function registerBatchResult(meta, data, context) {
@@ -1576,17 +1651,28 @@ function registerBatchResult(meta, data, context) {
1576
1651
  if (!batch || batch.meta.toolName !== meta.toolName) return { batch: void 0, isLeader: false };
1577
1652
  const member = batch.members.find((entry) => entry.toolCallId === context.toolCallId);
1578
1653
  if (member) {
1579
- member.status = data.isPartial ? "running" : "done";
1580
- member.isError = !data.isPartial && data.isError;
1654
+ const nextStatus = data.isPartial ? "running" : "done";
1655
+ const nextIsError = !data.isPartial && data.isError;
1656
+ const changed = member.status !== nextStatus || member.isError !== nextIsError || member.errorText !== (nextIsError ? data.errorText : void 0) || data.entries !== void 0 && member.outputEntries !== data.entries;
1657
+ member.status = nextStatus;
1658
+ member.isError = nextIsError;
1581
1659
  if (member.isError && data.errorText !== void 0) member.errorText = data.errorText;
1582
1660
  else delete member.errorText;
1583
1661
  if (data.entries !== void 0) member.outputEntries = data.entries;
1662
+ if (changed) bumpBatchRevision(batch);
1584
1663
  }
1585
1664
  if (batch.completedAt === void 0 && batch.members.every((entry) => entry.status === "done")) {
1586
1665
  batch.completedAt = performance.now();
1666
+ bumpBatchRevision(batch);
1587
1667
  }
1588
1668
  return { batch, isLeader: batch.leaderId === context.toolCallId };
1589
1669
  }
1670
+ function hasFinalBatchOutput(toolCallId) {
1671
+ const batch = batchByCallId.get(toolCallId);
1672
+ if (!batch) return false;
1673
+ const member = batch.members.find((entry) => entry.toolCallId === toolCallId);
1674
+ return member !== void 0 && member.outputEntries !== void 0 && member.status === "done" && !member.isError;
1675
+ }
1590
1676
  function batchStatus(batch) {
1591
1677
  let done = 0;
1592
1678
  let failed = 0;
@@ -1776,9 +1862,16 @@ function renderBatchPanelLines(theme, batch, status, width) {
1776
1862
  function renderBatchAwareCall(theme, batch) {
1777
1863
  return {
1778
1864
  invalidate() {
1865
+ delete batch.renderCache;
1779
1866
  },
1780
1867
  render(width) {
1781
- return renderBatchPanelLines(theme, batch, batchStatus(batch), width);
1868
+ const status = batchStatus(batch);
1869
+ if (!status.allDone) return renderBatchPanelLines(theme, batch, status, width);
1870
+ const cacheKey2 = [themeCacheKey(theme), getToolsRenderCacheSignature(), width, batch.revision].join("|");
1871
+ if (batch.renderCache?.key === cacheKey2) return batch.renderCache.lines;
1872
+ const lines = renderBatchPanelLines(theme, batch, status, width);
1873
+ batch.renderCache = { key: cacheKey2, lines };
1874
+ return lines;
1782
1875
  }
1783
1876
  };
1784
1877
  }
@@ -1939,6 +2032,9 @@ function invalidateTurnMembers(turn) {
1939
2032
  }
1940
2033
  }
1941
2034
  }
2035
+ function releaseTurnInvalidators(turn) {
2036
+ for (const member of turn.members) invalidateByCallId.delete(member.toolCallId);
2037
+ }
1942
2038
  function noteTurnMemberElapsed(toolCallId, elapsedMs) {
1943
2039
  if (elapsedMs === void 0) return;
1944
2040
  const entry = memberByCallId.get(toolCallId);
@@ -2830,7 +2926,9 @@ import { getLanguageFromPath } from "@earendil-works/pi-coding-agent";
2830
2926
  import { highlightCode } from "@earendil-works/pi-coding-agent";
2831
2927
  var ESC2 = "\x1B";
2832
2928
  var BG_ANSI_PATTERN = new RegExp(`${ESC2}\\[(?:4\\d|10\\d|48;5;\\d{1,3}|48;2;\\d{1,3};\\d{1,3};\\d{1,3}|49)m`, "g");
2929
+ var ANSI_SGR_PATTERN = new RegExp(`${ESC2}\\[([0-9;]*)m`, "g");
2833
2930
  var CONTROL_CHARS = "\0-\b\v\f-\x7F";
2931
+ var CONTROL_CHARS_PATTERN = new RegExp(`[${CONTROL_CHARS}]`, "g");
2834
2932
  var ADD_ROW_BACKGROUND_MIX_RATIO = 0.24;
2835
2933
  var REMOVE_ROW_BACKGROUND_MIX_RATIO = 0.12;
2836
2934
  var ADD_INLINE_EMPHASIS_MIX_RATIO = 0.44;
@@ -2922,7 +3020,7 @@ function resolveDiffPalette(theme) {
2922
3020
  }
2923
3021
  function keepBackgroundAcrossResets(text, rowBgAnsi) {
2924
3022
  if (!text) return text;
2925
- return text.replace(new RegExp(`${ESC2}\\[([0-9;]*)m`, "g"), (sequence, rawCodes) => {
3023
+ return text.replace(ANSI_SGR_PATTERN, (sequence, rawCodes) => {
2926
3024
  const split = String(rawCodes ?? "").split(";").filter(Boolean);
2927
3025
  const codes = split.length > 0 ? split : ["0"];
2928
3026
  const hasGlobalReset = codes.includes("0");
@@ -2964,7 +3062,7 @@ function applyBackgroundToVisibleRange(ansiText, start, end, backgroundAnsi, res
2964
3062
  return output;
2965
3063
  }
2966
3064
  function sanitizeSingleLineText(value) {
2967
- return value.replace(/\r/g, "").replace(/\n/g, "").replace(new RegExp(`[${CONTROL_CHARS}]`, "g"), "");
3065
+ return value.replace(/\r/g, "").replace(/\n/g, "").replace(CONTROL_CHARS_PATTERN, "");
2968
3066
  }
2969
3067
  function stripInlineBreaksPreserveAnsi(value) {
2970
3068
  return value.replace(/\r/g, "").replace(/\n/g, "");
@@ -3561,6 +3659,101 @@ var AdaptiveDiffComponent = class {
3561
3659
  }
3562
3660
  };
3563
3661
 
3662
+ // extension-src/pi-style/features/tools/boxed/shared.ts
3663
+ function pendingFlag(context) {
3664
+ return Boolean(context.isPartial);
3665
+ }
3666
+ function displayPath(rawPath, context) {
3667
+ const path = String(rawPath ?? "");
3668
+ if (!path) return "(unknown)";
3669
+ return shortenPath(resolveRelativePath(path, context.cwd));
3670
+ }
3671
+ function pathRangeDetail(rawPath, offset, limit, context) {
3672
+ const path = displayPath(rawPath, context);
3673
+ let range = "";
3674
+ if (offset !== void 0 || limit !== void 0) {
3675
+ const start = offset ?? 1;
3676
+ const end = limit !== void 0 ? Number(start) + Number(limit) - 1 : "";
3677
+ range = `:${start}${end ? `-${end}` : ""}`;
3678
+ }
3679
+ return path ? `${path}${range}` : "(unknown)";
3680
+ }
3681
+ function compactCall(theme, toolName, detailLine, options) {
3682
+ return renderCompactBoxedToolCall(theme, toolName, detailLine, {
3683
+ widthKey: boxedToolWidthKey(toolName, options.detailKey),
3684
+ state: options.context.state,
3685
+ isError: Boolean(options.context.isError),
3686
+ isPartial: Boolean(options.context.isPartial),
3687
+ isPending: pendingFlag(options.context),
3688
+ running: Boolean(options.context.executionStarted)
3689
+ });
3690
+ }
3691
+ function noteExecutionStart(context) {
3692
+ recordExecutionStarted(context.state, context.executionStarted);
3693
+ }
3694
+ function noteBoxedCallState(context) {
3695
+ if (!context.executionStarted) return;
3696
+ if (context.isPartial) startElapsedTicker(context.state, context.invalidate);
3697
+ else {
3698
+ recordExecutionEnded(context.state);
3699
+ stopElapsedTicker(context.state);
3700
+ }
3701
+ }
3702
+ function noteBoxedResultPhase(context, isPartial) {
3703
+ const firstResultPass = !isResultSeen(context.state);
3704
+ markResultSeen(context.state);
3705
+ if (isPartial) startElapsedTicker(context.state, context.invalidate);
3706
+ else {
3707
+ recordExecutionEnded(context.state);
3708
+ stopElapsedTicker(context.state);
3709
+ }
3710
+ return firstResultPass;
3711
+ }
3712
+ function stateElapsedMs(context) {
3713
+ return getStateElapsedMs(context.state);
3714
+ }
3715
+ function compactFooterWithState(theme, result, context, options = {}) {
3716
+ const elapsedMs = stateElapsedMs(context);
3717
+ return renderCompactBoxedFooter(theme, result, {
3718
+ state: context.state,
3719
+ isError: Boolean(options.isError ?? context.isError),
3720
+ isPartial: Boolean(options.isPartial ?? context.isPartial),
3721
+ ...elapsedMs === void 0 ? {} : { elapsedMs }
3722
+ });
3723
+ }
3724
+ function resultFooterLines(theme, result, context, extraParts = []) {
3725
+ return [formatBoxedFooter(theme, result, extraParts, stateElapsedMs(context))];
3726
+ }
3727
+ var CACHE_KEY_LONG_PART_THRESHOLD = 64;
3728
+ function fnv1aHex(text) {
3729
+ let hash = 2166136261;
3730
+ for (let i = 0; i < text.length; i++) {
3731
+ hash ^= text.charCodeAt(i);
3732
+ hash = Math.imul(hash, 16777619) >>> 0;
3733
+ }
3734
+ return hash.toString(16).padStart(8, "0");
3735
+ }
3736
+ function boundedCacheKeyPart(part) {
3737
+ if (typeof part !== "string" || part.length <= CACHE_KEY_LONG_PART_THRESHOLD) return part;
3738
+ return `${part.length}:${fnv1aHex(part)}`;
3739
+ }
3740
+ function getRenderCacheKey(prefix, theme, ...parts) {
3741
+ const pieces = [prefix, themeCacheKey(theme), getToolsRenderCacheSignature()];
3742
+ for (const part of parts) pieces.push(boundedCacheKeyPart(part));
3743
+ return pieces.join("|");
3744
+ }
3745
+ function memoizedStateComponent(state, slot, key, build) {
3746
+ if (!state || typeof state !== "object") return build();
3747
+ const cached = state[slot];
3748
+ if (cached && cached.key === key) return cached.component;
3749
+ const component = build();
3750
+ state[slot] = { key, component };
3751
+ return component;
3752
+ }
3753
+ function clearFooterState(context) {
3754
+ clearCompactBoxedFooter(context.state);
3755
+ }
3756
+
3564
3757
  // extension-src/pi-style/features/tools/boxed/git.ts
3565
3758
  var GIT_SHORT_STATUS_FLAGS = /* @__PURE__ */ new Set(["-s", "--short", "--porcelain"]);
3566
3759
  var GIT_DIFF_FORMAT_REJECT = /* @__PURE__ */ new Set([
@@ -4867,6 +5060,34 @@ function binaryBodyLine(theme, status) {
4867
5060
  }
4868
5061
  function renderGitDiffResult(theme, parsed, options, context) {
4869
5062
  const expanded = Boolean(options.expanded);
5063
+ let totalAdditions = 0;
5064
+ let totalRemovals = 0;
5065
+ const sigParts = [];
5066
+ for (const file of parsed.files) {
5067
+ totalAdditions += file.additions;
5068
+ totalRemovals += file.removals;
5069
+ sigParts.push(
5070
+ `${file.path}:${file.body.length}:${file.additions}:${file.removals}:${file.binary ? 1 : 0}:${file.status ?? ""}`
5071
+ );
5072
+ }
5073
+ const sig = sigParts.join(";").slice(0, 2048);
5074
+ return memoizedStateComponent(
5075
+ context.state,
5076
+ "__piStyleGitDiffResult",
5077
+ getRenderCacheKey(
5078
+ "git-diff-result",
5079
+ theme,
5080
+ String(parsed.show),
5081
+ String(expanded),
5082
+ parsed.files.length,
5083
+ totalAdditions,
5084
+ totalRemovals,
5085
+ sig
5086
+ ),
5087
+ () => buildGitDiffResultComponent(theme, parsed, expanded, context)
5088
+ );
5089
+ }
5090
+ function buildGitDiffResultComponent(theme, parsed, expanded, context) {
4870
5091
  const elapsedMs = getStateElapsedMs(context.state);
4871
5092
  const fileCount = parsed.files.length;
4872
5093
  const footerParts = [];
@@ -4938,75 +5159,6 @@ function renderGitDiffResult(theme, parsed, options, context) {
4938
5159
  };
4939
5160
  }
4940
5161
 
4941
- // extension-src/pi-style/features/tools/boxed/shared.ts
4942
- function pendingFlag(context) {
4943
- return Boolean(context.isPartial);
4944
- }
4945
- function displayPath(rawPath, context) {
4946
- const path = String(rawPath ?? "");
4947
- if (!path) return "(unknown)";
4948
- return shortenPath(resolveRelativePath(path, context.cwd));
4949
- }
4950
- function pathRangeDetail(rawPath, offset, limit, context) {
4951
- const path = displayPath(rawPath, context);
4952
- let range = "";
4953
- if (offset !== void 0 || limit !== void 0) {
4954
- const start = offset ?? 1;
4955
- const end = limit !== void 0 ? Number(start) + Number(limit) - 1 : "";
4956
- range = `:${start}${end ? `-${end}` : ""}`;
4957
- }
4958
- return path ? `${path}${range}` : "(unknown)";
4959
- }
4960
- function compactCall(theme, toolName, detailLine, options) {
4961
- return renderCompactBoxedToolCall(theme, toolName, detailLine, {
4962
- widthKey: boxedToolWidthKey(toolName, options.detailKey),
4963
- state: options.context.state,
4964
- isError: Boolean(options.context.isError),
4965
- isPartial: Boolean(options.context.isPartial),
4966
- isPending: pendingFlag(options.context),
4967
- running: Boolean(options.context.executionStarted)
4968
- });
4969
- }
4970
- function noteExecutionStart(context) {
4971
- recordExecutionStarted(context.state, context.executionStarted);
4972
- }
4973
- function noteBoxedCallState(context) {
4974
- if (!context.executionStarted) return;
4975
- if (context.isPartial) startElapsedTicker(context.state, context.invalidate);
4976
- else {
4977
- recordExecutionEnded(context.state);
4978
- stopElapsedTicker(context.state);
4979
- }
4980
- }
4981
- function noteBoxedResultPhase(context, isPartial) {
4982
- const firstResultPass = !isResultSeen(context.state);
4983
- markResultSeen(context.state);
4984
- if (isPartial) startElapsedTicker(context.state, context.invalidate);
4985
- else {
4986
- recordExecutionEnded(context.state);
4987
- stopElapsedTicker(context.state);
4988
- }
4989
- return firstResultPass;
4990
- }
4991
- function stateElapsedMs(context) {
4992
- return getStateElapsedMs(context.state);
4993
- }
4994
- function compactFooterWithState(theme, result, context, options = {}) {
4995
- const elapsedMs = stateElapsedMs(context);
4996
- return renderCompactBoxedFooter(theme, result, {
4997
- state: context.state,
4998
- isError: Boolean(options.isError ?? context.isError),
4999
- isPartial: Boolean(options.isPartial ?? context.isPartial),
5000
- ...elapsedMs === void 0 ? {} : { elapsedMs }
5001
- });
5002
- }
5003
- function resultFooterLines(theme, result, context, extraParts = []) {
5004
- return [formatBoxedFooter(theme, result, extraParts, stateElapsedMs(context))];
5005
- }
5006
- function clearFooterState(context) {
5007
- clearCompactBoxedFooter(context.state);
5008
- }
5009
-
5010
5162
  // extension-src/pi-style/features/tools/boxed/bash.ts
5011
5163
  var MAX_LINE_CHARS = 2e3;
5012
5164
  var ESC3 = "\x1B";
@@ -5107,6 +5259,25 @@ function countNewlines(text, from, to) {
5107
5259
  }
5108
5260
  return count;
5109
5261
  }
5262
+ function findBackwardLineStart(text, need, end = text.length) {
5263
+ let found = 0;
5264
+ for (let i = end - 1; i >= 0; i--) {
5265
+ if (text.charCodeAt(i) === 10 && ++found >= need) return i + 1;
5266
+ }
5267
+ return 0;
5268
+ }
5269
+ function isOutputWhitespaceCode(code) {
5270
+ if (code === 32 || code >= 9 && code <= 13) return true;
5271
+ if (code === 133 || code === 160 || code === 5760) return true;
5272
+ if (code >= 8192 && code <= 8202) return true;
5273
+ return code === 8232 || code === 8233 || code === 8239 || code === 8287 || code === 12288 || code === 65279;
5274
+ }
5275
+ function lastVisibleEnd(text) {
5276
+ for (let i = text.length - 1; i >= 0; i--) {
5277
+ if (!isOutputWhitespaceCode(text.charCodeAt(i))) return i + 1;
5278
+ }
5279
+ return 0;
5280
+ }
5110
5281
  function stripBashToolNoticeLines(text) {
5111
5282
  const filteredLines = text.replace(/\r/g, "").split("\n").filter((line) => !BASH_TOOL_NOTICE_PATTERN.test(line.trim()));
5112
5283
  return filteredLines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
@@ -5237,8 +5408,10 @@ function bashBodyComponent(preview, emptyLines) {
5237
5408
  };
5238
5409
  }
5239
5410
  function renderBashStreamingResult(theme, raw, options, context) {
5240
- const body = stripBashToolNoticeLines(stripAnsi(raw));
5241
- const hasOutput = body.trim().length > 0;
5411
+ const contentEnd = lastVisibleEnd(raw);
5412
+ const hasOutput = contentEnd > 0;
5413
+ const tailStart = hasOutput ? findBackwardLineStart(raw, getToolsRenderConfig().maxCollapsedLines + 10, contentEnd) : 0;
5414
+ const body = stripBashToolNoticeLines(stripAnsi(raw.slice(tailStart, contentEnd)));
5242
5415
  const elapsed = getStateElapsedMs(context.state);
5243
5416
  const emptyLines = [theme.fg("dim", "No output received yet")];
5244
5417
  if (!hasOutput && isInteractiveCommand(context?.args?.command) && (elapsed ?? 0) >= 1e3) {
@@ -5269,17 +5442,7 @@ function renderBashFinalResult(theme, raw, options, context) {
5269
5442
  const referenceLines = rawCommand.split("\n").map((line, index) => `${index === 0 ? "$ " : "> "}${line}`);
5270
5443
  if (!options.expanded) {
5271
5444
  const scanLines = getToolsRenderConfig().maxCollapsedLines + 10;
5272
- let nlCount = 0;
5273
- let tailStart = 0;
5274
- for (let i = statusStripped.length - 1; i >= 0; i--) {
5275
- if (statusStripped.charCodeAt(i) === 10) {
5276
- nlCount++;
5277
- if (nlCount >= scanLines) {
5278
- tailStart = i + 1;
5279
- break;
5280
- }
5281
- }
5282
- }
5445
+ const tailStart = findBackwardLineStart(statusStripped, scanLines);
5283
5446
  const tail = stripBashToolNoticeLines(stripAnsi(statusStripped.slice(tailStart)));
5284
5447
  const totalLinesBefore = tailStart > 0 ? countNewlines(statusStripped, 0, tailStart) : 0;
5285
5448
  const preview2 = createBashResultPreview(theme, tail, options, outputColor);
@@ -5322,11 +5485,11 @@ var EMPTY_BASH_RESULT = Object.freeze({
5322
5485
  }
5323
5486
  });
5324
5487
  function createBashResultPreview(theme, text, options, color) {
5325
- let cacheKey = "";
5488
+ let cacheKey2 = "";
5326
5489
  let cacheLines = null;
5327
5490
  return {
5328
5491
  invalidate() {
5329
- cacheKey = "";
5492
+ cacheKey2 = "";
5330
5493
  cacheLines = null;
5331
5494
  },
5332
5495
  render(width) {
@@ -5334,29 +5497,19 @@ function createBashResultPreview(theme, text, options, color) {
5334
5497
  const cfg = getToolsRenderConfig();
5335
5498
  const expanded = Boolean(options.expanded);
5336
5499
  const cacheId = `${bodyWidth}|${expanded ? 1 : 0}|${cfg.maxExpandedLines}|${cfg.dimOutput ? 1 : 0}`;
5337
- if (cacheLines && cacheKey === cacheId) return cacheLines;
5500
+ if (cacheLines && cacheKey2 === cacheId) return cacheLines;
5338
5501
  if (!expanded) {
5339
5502
  const needed = cfg.maxCollapsedLines;
5340
- let totalNewlines = 0;
5341
- let scanFrom = 0;
5342
- for (let i = text.length - 1; i >= 0; i--) {
5343
- if (text.charCodeAt(i) === 10) {
5344
- totalNewlines++;
5345
- if (totalNewlines === needed) {
5346
- scanFrom = i + 1;
5347
- break;
5348
- }
5349
- }
5350
- }
5503
+ const scanFrom = findBackwardLineStart(text, needed);
5351
5504
  if (text.length === 0) {
5352
- cacheKey = cacheId;
5505
+ cacheKey2 = cacheId;
5353
5506
  cacheLines = [];
5354
5507
  return cacheLines;
5355
5508
  }
5356
5509
  const tail = replaceTabs(text.slice(scanFrom)).replace(/\r/g, "");
5357
5510
  const shownLines = tail ? tail.split("\n").map((l) => clampLineLength(l)) : [];
5358
5511
  if (shownLines.length === 0) {
5359
- cacheKey = cacheId;
5512
+ cacheKey2 = cacheId;
5360
5513
  cacheLines = [];
5361
5514
  return cacheLines;
5362
5515
  }
@@ -5365,31 +5518,31 @@ function createBashResultPreview(theme, text, options, color) {
5365
5518
  if (color === "error") return formatToolOutputLine(theme, truncated, "error");
5366
5519
  return cfg.dimOutput ? formatToolOutputLine(theme, truncated) : formatToolOutputLine(theme, truncated, "text");
5367
5520
  });
5368
- cacheKey = cacheId;
5521
+ cacheKey2 = cacheId;
5369
5522
  cacheLines = truncatedShown;
5370
5523
  return cacheLines;
5371
5524
  }
5372
5525
  const normalized = replaceTabs(text);
5373
- const logicalLines = normalized.split("\n").map((l) => clampLineLength(l));
5374
- const hasOutput = !(logicalLines.length === 1 && logicalLines[0] === "");
5526
+ const rawLines = normalized.split("\n");
5527
+ const totalLines = rawLines.length;
5528
+ const hasOutput = !(totalLines === 1 && rawLines[0] === "");
5375
5529
  if (!hasOutput) {
5376
- cacheKey = cacheId;
5530
+ cacheKey2 = cacheId;
5377
5531
  cacheLines = [];
5378
5532
  return cacheLines;
5379
5533
  }
5380
- const truncatedLines = logicalLines.map((line) => safeTruncateToWidth(line, bodyWidth, "\u2026"));
5381
- const expandedLines = truncatedLines.length === 1 && truncatedLines[0] === "" ? [] : truncatedLines;
5382
5534
  const applyColor = (l) => color === "error" ? formatToolOutputLine(theme, l, "error") : cfg.dimOutput ? formatToolOutputLine(theme, l) : formatToolOutputLine(theme, l, "text");
5383
- if (cfg.maxExpandedLines > 0 && expandedLines.length > cfg.maxExpandedLines) {
5384
- const truncated = expandedLines.slice(-cfg.maxExpandedLines).map(applyColor);
5385
- const remaining = expandedLines.length - cfg.maxExpandedLines;
5535
+ const renderRawLine = (line) => safeTruncateToWidth(clampLineLength(line), bodyWidth, "\u2026");
5536
+ if (cfg.maxExpandedLines > 0 && totalLines > cfg.maxExpandedLines) {
5537
+ const truncated = rawLines.slice(-cfg.maxExpandedLines).map((line) => applyColor(renderRawLine(line)));
5538
+ const remaining = totalLines - cfg.maxExpandedLines;
5386
5539
  truncated.unshift(theme.fg("dim", `\u2026 ${remaining} earlier lines`));
5387
- cacheKey = cacheId;
5540
+ cacheKey2 = cacheId;
5388
5541
  cacheLines = truncated;
5389
5542
  return cacheLines;
5390
5543
  }
5391
- cacheKey = cacheId;
5392
- cacheLines = expandedLines.map(applyColor);
5544
+ cacheKey2 = cacheId;
5545
+ cacheLines = rawLines.map((line) => applyColor(renderRawLine(line)));
5393
5546
  return cacheLines;
5394
5547
  }
5395
5548
  };
@@ -5556,6 +5709,7 @@ function isGitDiffClass(cls) {
5556
5709
  function isGitActionClass(cls) {
5557
5710
  return !isBashTreeClass(cls) && cls.kind === "action";
5558
5711
  }
5712
+ var PARTIAL_REPARSE_THRESHOLD = 4096;
5559
5713
  function parseSemanticOutput(cls, output) {
5560
5714
  if (isBashTreeClass(cls)) return parseBashTreeOutput(cls, output);
5561
5715
  if (isGhClass(cls)) return parseGhOutput(cls, output);
@@ -5602,27 +5756,35 @@ function renderSemanticPanel(theme, toolCallId, context) {
5602
5756
  },
5603
5757
  render(width) {
5604
5758
  if (!state) return [];
5605
- if (state.fallback) {
5606
- return renderBoxedBashCall(
5607
- theme,
5608
- state.command.split("\n"),
5609
- context,
5610
- bashWidthKey(state.command, context?.args?.timeout)
5611
- ).render(width);
5612
- }
5613
- if (isBashTreeClass(state.cls)) {
5614
- const treeState = { cls: state.cls };
5615
- if (state.parsed !== void 0) treeState.parsed = state.parsed;
5616
- return renderBashTreeLines(theme, treeState, width);
5617
- }
5618
- if (isGhClass(state.cls)) {
5619
- const ghState = { cls: state.cls };
5620
- if (state.parsed !== void 0) ghState.parsed = state.parsed;
5621
- return renderGhCardLines(theme, ghState, width);
5622
- }
5623
- const gitState = { cls: state.cls };
5624
- if (state.parsed !== void 0) gitState.parsed = state.parsed;
5625
- return renderGitCardLines(theme, gitState, width);
5759
+ const renderFresh = () => {
5760
+ if (state.fallback) {
5761
+ return renderBoxedBashCall(
5762
+ theme,
5763
+ state.command.split("\n"),
5764
+ context,
5765
+ bashWidthKey(state.command, context?.args?.timeout)
5766
+ ).render(width);
5767
+ }
5768
+ if (isBashTreeClass(state.cls)) {
5769
+ const treeState = { cls: state.cls };
5770
+ if (state.parsed !== void 0) treeState.parsed = state.parsed;
5771
+ return renderBashTreeLines(theme, treeState, width);
5772
+ }
5773
+ if (isGhClass(state.cls)) {
5774
+ const ghState = { cls: state.cls };
5775
+ if (state.parsed !== void 0) ghState.parsed = state.parsed;
5776
+ return renderGhCardLines(theme, ghState, width);
5777
+ }
5778
+ const gitState = { cls: state.cls };
5779
+ if (state.parsed !== void 0) gitState.parsed = state.parsed;
5780
+ return renderGitCardLines(theme, gitState, width);
5781
+ };
5782
+ if (!state.finished) return renderFresh();
5783
+ const cacheKey2 = [themeCacheKey(theme), getToolsRenderCacheSignature(), width, state.revision].join("|");
5784
+ if (state.renderCache?.key === cacheKey2) return state.renderCache.lines;
5785
+ const lines = renderFresh();
5786
+ state.renderCache = { key: cacheKey2, lines };
5787
+ return lines;
5626
5788
  }
5627
5789
  };
5628
5790
  }
@@ -5631,7 +5793,27 @@ var bashTool = {
5631
5793
  noteExecutionStart(context);
5632
5794
  const cls = classifyBashSemantic(String(args?.command ?? ""));
5633
5795
  if (cls) {
5634
- semanticStates.set(context.toolCallId, { cls, command: String(args?.command ?? "") });
5796
+ const command = String(args?.command ?? "");
5797
+ const existing = semanticStates.get(context.toolCallId);
5798
+ if (existing) {
5799
+ if (existing.command !== command || existing.cls.kind !== cls.kind) {
5800
+ delete existing.parsed;
5801
+ delete existing.fallback;
5802
+ existing.finished = false;
5803
+ existing.revision++;
5804
+ delete existing.renderCache;
5805
+ delete existing.lastParsedLength;
5806
+ }
5807
+ existing.command = command;
5808
+ existing.cls = cls;
5809
+ } else {
5810
+ semanticStates.set(context.toolCallId, {
5811
+ cls,
5812
+ command,
5813
+ finished: false,
5814
+ revision: 0
5815
+ });
5816
+ }
5635
5817
  return renderSemanticPanel(theme, context.toolCallId, context);
5636
5818
  }
5637
5819
  noteBoxedCallState(context);
@@ -5649,15 +5831,24 @@ var bashTool = {
5649
5831
  }
5650
5832
  if (!options.isPartial || isBashTreeClass(cls)) {
5651
5833
  const output = stripBashToolNoticeLines(stripAnsi(getTextOutput(result)));
5652
- const parsed = parseSemanticOutput(cls, output);
5653
5834
  const state = semanticStates.get(context.toolCallId);
5835
+ const shouldParse = !options.isPartial || state === void 0 || state.lastParsedLength === void 0 || output.length - state.lastParsedLength >= PARTIAL_REPARSE_THRESHOLD;
5836
+ const parsed = shouldParse ? parseSemanticOutput(cls, output) : void 0;
5654
5837
  if (parsed) {
5655
- if (state) state.parsed = parsed;
5656
- else
5838
+ if (state) {
5839
+ state.parsed = parsed;
5840
+ state.finished = !options.isPartial;
5841
+ state.revision++;
5842
+ delete state.renderCache;
5843
+ if (options.isPartial) state.lastParsedLength = output.length;
5844
+ } else
5657
5845
  semanticStates.set(context.toolCallId, {
5658
5846
  cls,
5659
5847
  command: String(context?.args?.command ?? ""),
5660
- parsed
5848
+ parsed,
5849
+ finished: !options.isPartial,
5850
+ revision: 0,
5851
+ ...options.isPartial ? { lastParsedLength: output.length } : {}
5661
5852
  });
5662
5853
  if (isGitDiffClass(cls)) {
5663
5854
  return renderGitDiffResult(theme, parsed, options, context);
@@ -5667,7 +5858,18 @@ var bashTool = {
5667
5858
  }
5668
5859
  return EMPTY_BASH_TREE_RESULT;
5669
5860
  }
5670
- if (state) state.fallback = true;
5861
+ if (!shouldParse) {
5862
+ if (state?.parsed !== void 0) return EMPTY_BASH_TREE_RESULT;
5863
+ }
5864
+ if (shouldParse) {
5865
+ if (state) {
5866
+ state.fallback = true;
5867
+ state.finished = !options.isPartial;
5868
+ state.revision++;
5869
+ delete state.renderCache;
5870
+ if (options.isPartial) state.lastParsedLength = output.length;
5871
+ }
5872
+ }
5671
5873
  }
5672
5874
  } else if (options.isPartial) {
5673
5875
  startElapsedTicker(context.state, context.invalidate);
@@ -5680,7 +5882,20 @@ var bashTool = {
5680
5882
  if (firstResultPass) return EMPTY_BASH_RESULT;
5681
5883
  return renderBashStreamingResult(theme, raw, options, context);
5682
5884
  }
5683
- return renderBashFinalResult(theme, raw, options, context);
5885
+ return memoizedStateComponent(
5886
+ context.state,
5887
+ "__piStyleBashFinalResult",
5888
+ getRenderCacheKey(
5889
+ "bash-final-result",
5890
+ theme,
5891
+ Boolean(options.expanded),
5892
+ Boolean(context.isError),
5893
+ String(context?.args?.command ?? ""),
5894
+ raw,
5895
+ getStateElapsedMs(context.state) ?? ""
5896
+ ),
5897
+ () => renderBashFinalResult(theme, raw, options, context)
5898
+ );
5684
5899
  }
5685
5900
  };
5686
5901
 
@@ -5752,28 +5967,42 @@ var editTool = {
5752
5967
  const message = firstText(result.content);
5753
5968
  const argPath = String(context?.args?.path ?? context?.args?.file_path ?? "");
5754
5969
  const sourcePath = details?.path ?? (argPath || extractEditedPath(message));
5755
- const language = sourcePath ? getLanguageFromPath2(sourcePath) : void 0;
5756
- const rows = buildSplitRows(diff);
5757
5970
  const expanded = options.expanded;
5758
- const shouldHighlight = Boolean(language) && diff.length <= MAX_HIGHLIGHT_DIFF_CHARS && rows.length <= MAX_HIGHLIGHT_DIFF_ROWS;
5759
5971
  const stats = countDiffStats(diff);
5760
- const maxRows = expanded ? 160 : 36;
5761
- const diffView = new AdaptiveDiffComponent(theme, rows, maxRows, shouldHighlight ? language : void 0);
5762
- const expandHint2 = !expanded && diffView.hasCollapsed() ? "Ctrl+O more" : void 0;
5763
- return renderBoxedToolResult(
5764
- theme,
5765
- {
5766
- render(width) {
5767
- return diffView.render(width);
5768
- },
5769
- invalidate() {
5770
- diffView.invalidate();
5771
- }
5772
- },
5773
- {
5774
- dividerLabel: diffDividerLabel2(theme, stats),
5775
- ...expandHint2 ? { dividerRightLabel: expandHint2 } : {},
5776
- footerLines: [editDiffFooter(theme, result, context, stats)]
5972
+ return memoizedStateComponent(
5973
+ context.state,
5974
+ "__piStyleEditDiffResult",
5975
+ getRenderCacheKey(
5976
+ "edit-diff-result",
5977
+ theme,
5978
+ Boolean(expanded),
5979
+ diff,
5980
+ sourcePath ?? "",
5981
+ editDiffFooter(theme, result, context, stats)
5982
+ ),
5983
+ () => {
5984
+ const language = sourcePath ? getLanguageFromPath2(sourcePath) : void 0;
5985
+ const rows = buildSplitRows(diff);
5986
+ const shouldHighlight = Boolean(language) && diff.length <= MAX_HIGHLIGHT_DIFF_CHARS && rows.length <= MAX_HIGHLIGHT_DIFF_ROWS;
5987
+ const maxRows = expanded ? 160 : 36;
5988
+ const diffView = new AdaptiveDiffComponent(theme, rows, maxRows, shouldHighlight ? language : void 0);
5989
+ const expandHint2 = !expanded && diffView.hasCollapsed() ? "Ctrl+O more" : void 0;
5990
+ return renderBoxedToolResult(
5991
+ theme,
5992
+ {
5993
+ render(width) {
5994
+ return diffView.render(width);
5995
+ },
5996
+ invalidate() {
5997
+ diffView.invalidate();
5998
+ }
5999
+ },
6000
+ {
6001
+ dividerLabel: diffDividerLabel2(theme, stats),
6002
+ ...expandHint2 ? { dividerRightLabel: expandHint2 } : {},
6003
+ footerLines: [editDiffFooter(theme, result, context, stats)]
6004
+ }
6005
+ );
5777
6006
  }
5778
6007
  );
5779
6008
  }
@@ -5879,14 +6108,16 @@ var findTool = {
5879
6108
  return renderBatchAwareCall(theme, batch);
5880
6109
  },
5881
6110
  result(result, options, _theme, context) {
5882
- const output = stripAnsi(getTextOutput(result)).trimEnd();
5883
- const entries = context.isError ? void 0 : parseFindOutput(output);
6111
+ const isError = Boolean(context.isError);
6112
+ const settled = !options.isPartial && !isError && hasFinalBatchOutput(context.toolCallId);
6113
+ const output = settled ? "" : stripAnsi(getTextOutput(result)).trimEnd();
6114
+ const entries = settled || isError ? void 0 : parseFindOutput(output);
5884
6115
  registerBatchResult(
5885
6116
  FIND_META,
5886
6117
  {
5887
6118
  isPartial: Boolean(options.isPartial),
5888
- isError: Boolean(context.isError),
5889
- errorText: context.isError ? output || void 0 : void 0,
6119
+ isError,
6120
+ errorText: isError ? output || void 0 : void 0,
5890
6121
  ...entries !== void 0 ? { entries } : {}
5891
6122
  },
5892
6123
  context
@@ -5997,14 +6228,19 @@ var grepTool = {
5997
6228
  return renderGrepPanel(theme, context.toolCallId);
5998
6229
  },
5999
6230
  result(result, options, _theme, context) {
6000
- const output = stripAnsi(getTextOutput(result)).trimEnd();
6001
6231
  const isError = Boolean(context.isError);
6232
+ const isPartial = Boolean(options.isPartial);
6233
+ const state = grepPanels.get(context.toolCallId);
6234
+ if (!isPartial && !isError && state !== void 0 && state.matches !== void 0 && !state.isPartial) {
6235
+ return EMPTY_GREP_RESULT;
6236
+ }
6237
+ const output = stripAnsi(getTextOutput(result)).trimEnd();
6002
6238
  const matches = isError ? [] : parseGrepOutput(output);
6003
6239
  registerGrepResult(context.toolCallId, {
6004
6240
  matches,
6005
6241
  isError,
6006
6242
  errorText: isError ? output || void 0 : void 0,
6007
- isPartial: Boolean(options.isPartial)
6243
+ isPartial
6008
6244
  });
6009
6245
  return EMPTY_GREP_RESULT;
6010
6246
  }
@@ -6031,14 +6267,16 @@ var lsTool = {
6031
6267
  return renderBatchAwareCall(theme, batch);
6032
6268
  },
6033
6269
  result(result, options, _theme, context) {
6034
- const output = stripAnsi(getTextOutput(result)).trimEnd();
6035
- const entries = context.isError ? void 0 : parseLsOutput(output);
6270
+ const isError = Boolean(context.isError);
6271
+ const settled = !options.isPartial && !isError && hasFinalBatchOutput(context.toolCallId);
6272
+ const output = settled ? "" : stripAnsi(getTextOutput(result)).trimEnd();
6273
+ const entries = settled || isError ? void 0 : parseLsOutput(output);
6036
6274
  registerBatchResult(
6037
6275
  LIST_META,
6038
6276
  {
6039
6277
  isPartial: Boolean(options.isPartial),
6040
- isError: Boolean(context.isError),
6041
- errorText: context.isError ? output || void 0 : void 0,
6278
+ isError,
6279
+ errorText: isError ? output || void 0 : void 0,
6042
6280
  ...entries !== void 0 ? { entries } : {}
6043
6281
  },
6044
6282
  context
@@ -6162,29 +6400,44 @@ function renderQuickEditResult(_toolName, result, options, theme, context, confi
6162
6400
  footerLines: [quickEditFooter(theme, context)]
6163
6401
  });
6164
6402
  }
6165
- const rows = buildSplitRows(diff);
6166
6403
  const expanded = options.expanded;
6167
6404
  const argPath = String(context?.args?.path ?? "");
6168
- const language = argPath ? getLanguageFromPath3(argPath) : void 0;
6169
- const shouldHighlight = Boolean(language) && diff.length <= MAX_HIGHLIGHT_DIFF_CHARS2 && rows.length <= MAX_HIGHLIGHT_DIFF_ROWS2;
6170
6405
  const stats = countDiffStats(diff);
6171
- const maxRows = expanded ? 160 : 36;
6172
- const diffView = new AdaptiveDiffComponent(theme, rows, maxRows, shouldHighlight ? language : void 0);
6173
- const expandHint2 = !expanded && diffView.hasCollapsed() ? "Ctrl+O more" : void 0;
6174
- return renderBoxedToolResult(
6175
- theme,
6176
- {
6177
- render(width) {
6178
- return diffView.render(width);
6179
- },
6180
- invalidate() {
6181
- diffView.invalidate();
6182
- }
6183
- },
6184
- {
6185
- dividerLabel: quickEditDividerLabel(theme, stats),
6186
- ...expandHint2 ? { dividerRightLabel: expandHint2 } : {},
6187
- footerLines: [quickEditDiffFooter(theme, result, context, stats)]
6406
+ return memoizedStateComponent(
6407
+ context.state,
6408
+ "__piStyleQuickEditDiffResult",
6409
+ getRenderCacheKey(
6410
+ "quick-edit-diff-result",
6411
+ theme,
6412
+ config.toolLabel,
6413
+ Boolean(expanded),
6414
+ diff,
6415
+ argPath,
6416
+ quickEditDiffFooter(theme, result, context, stats)
6417
+ ),
6418
+ () => {
6419
+ const rows = buildSplitRows(diff);
6420
+ const language = argPath ? getLanguageFromPath3(argPath) : void 0;
6421
+ const shouldHighlight = Boolean(language) && diff.length <= MAX_HIGHLIGHT_DIFF_CHARS2 && rows.length <= MAX_HIGHLIGHT_DIFF_ROWS2;
6422
+ const maxRows = expanded ? 160 : 36;
6423
+ const diffView = new AdaptiveDiffComponent(theme, rows, maxRows, shouldHighlight ? language : void 0);
6424
+ const expandHint2 = !expanded && diffView.hasCollapsed() ? "Ctrl+O more" : void 0;
6425
+ return renderBoxedToolResult(
6426
+ theme,
6427
+ {
6428
+ render(width) {
6429
+ return diffView.render(width);
6430
+ },
6431
+ invalidate() {
6432
+ diffView.invalidate();
6433
+ }
6434
+ },
6435
+ {
6436
+ dividerLabel: quickEditDividerLabel(theme, stats),
6437
+ ...expandHint2 ? { dividerRightLabel: expandHint2 } : {},
6438
+ footerLines: [quickEditDiffFooter(theme, result, context, stats)]
6439
+ }
6440
+ );
6188
6441
  }
6189
6442
  );
6190
6443
  }
@@ -6231,13 +6484,13 @@ var readTool = {
6231
6484
  return renderBatchAwareCall(theme, batch);
6232
6485
  },
6233
6486
  result(result, options, _theme, context) {
6234
- const output = stripAnsi(getTextOutput(result)).trimEnd();
6487
+ const errorText = context.isError ? stripAnsi(getTextOutput(result)).trimEnd() || void 0 : void 0;
6235
6488
  registerBatchResult(
6236
6489
  READ_META,
6237
6490
  {
6238
6491
  isPartial: Boolean(options.isPartial),
6239
6492
  isError: Boolean(context.isError),
6240
- errorText: context.isError ? output || void 0 : void 0
6493
+ errorText
6241
6494
  },
6242
6495
  context
6243
6496
  );
@@ -7941,11 +8194,10 @@ function createBuiltinSegments() {
7941
8194
  content: snapshot.sessionStartedAt === void 0 ? "" : theme.apply("time", formatElapsed2(Date.now() - snapshot.sessionStartedAt)),
7942
8195
  compactContent: snapshot.sessionStartedAt === void 0 ? "" : theme.apply("time", formatElapsed2(Date.now() - snapshot.sessionStartedAt))
7943
8196
  })),
7944
- segment("time", 20, ({ theme }) => ({
7945
- visible: true,
7946
- content: theme.apply("time", (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })),
7947
- compactContent: theme.apply("time", (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }))
7948
- })),
8197
+ segment("time", 20, ({ theme }) => {
8198
+ const text = theme.apply("time", clockTime());
8199
+ return { visible: true, content: text, compactContent: text };
8200
+ }),
7949
8201
  segment("hostname", 20, ({ snapshot, theme }) => ({
7950
8202
  visible: Boolean(snapshot.hostname),
7951
8203
  content: theme.apply("muted", snapshot.hostname ?? "")
@@ -7963,6 +8215,15 @@ function createBuiltinSegments() {
7963
8215
  ];
7964
8216
  return new Map(segments.map((item) => [item.id, item]));
7965
8217
  }
8218
+ var clockCache;
8219
+ function clockTime() {
8220
+ const now = /* @__PURE__ */ new Date();
8221
+ const minuteKey = now.getHours() * 60 + now.getMinutes();
8222
+ if (clockCache?.minuteKey !== minuteKey) {
8223
+ clockCache = { minuteKey, value: now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) };
8224
+ }
8225
+ return clockCache.value;
8226
+ }
7966
8227
  var CONTEXT_BAR_WIDTH = 10;
7967
8228
  function contextBar(percent, width = CONTEXT_BAR_WIDTH) {
7968
8229
  const filled = Math.max(0, Math.min(width, Math.round(percent / 100 * width)));
@@ -8148,7 +8409,24 @@ function colorPrefixFor(active, config, noColor, token) {
8148
8409
  }
8149
8410
  return "";
8150
8411
  }
8412
+ function envKeyFor(env) {
8413
+ return `${env.PI_STYLE_NERD_FONTS ?? ""}\0${env.GHOSTTY_RESOURCES_DIR ? "1" : "0"}\0${env.TERM_PROGRAM ?? ""}`;
8414
+ }
8415
+ var glyphModeCache = /* @__PURE__ */ new WeakMap();
8151
8416
  function detectGlyphMode(config, env = {}) {
8417
+ let byEnv = glyphModeCache.get(config);
8418
+ if (!byEnv) {
8419
+ byEnv = /* @__PURE__ */ new Map();
8420
+ glyphModeCache.set(config, byEnv);
8421
+ }
8422
+ const envKey = envKeyFor(env);
8423
+ const cached = byEnv.get(envKey);
8424
+ if (cached !== void 0) return cached;
8425
+ const mode = computeGlyphMode(config, env);
8426
+ byEnv.set(envKey, mode);
8427
+ return mode;
8428
+ }
8429
+ function computeGlyphMode(config, env) {
8152
8430
  if (env.PI_STYLE_NERD_FONTS === "1") return "nerd";
8153
8431
  if (env.PI_STYLE_NERD_FONTS === "0") return "unicode";
8154
8432
  if (config.theme.nerdFonts === "on") return "nerd";
@@ -8163,7 +8441,14 @@ function detectGlyphMode(config, env = {}) {
8163
8441
  function resolveTheme(active, config, env = {}) {
8164
8442
  const noColor = Object.hasOwn(env, "NO_COLOR") && env.NO_COLOR !== "" && config.theme.colors.colorOverride !== "on";
8165
8443
  const mode = config.preset === "ascii" ? "ascii" : detectGlyphMode(config, env);
8166
- const color = (token) => colorPrefixFor(active, config, noColor, token);
8444
+ const prefixes = /* @__PURE__ */ new Map();
8445
+ const color = (token) => {
8446
+ const cached = prefixes.get(token);
8447
+ if (cached !== void 0) return cached;
8448
+ const prefix = colorPrefixFor(active, config, noColor, token);
8449
+ prefixes.set(token, prefix);
8450
+ return prefix;
8451
+ };
8167
8452
  return {
8168
8453
  mode,
8169
8454
  noColor,
@@ -8181,9 +8466,13 @@ function resolveTheme(active, config, env = {}) {
8181
8466
  var widthOf = visibleWidth3;
8182
8467
  function widthSafe(value, width) {
8183
8468
  if (width <= 0) return "";
8184
- const fitted = widthOf(value) > width ? truncateAnsi(value, width, "") : value;
8185
- const current = widthOf(fitted);
8186
- return current < width ? fitted + " ".repeat(width - current) : fitted;
8469
+ const measured = widthOf(value);
8470
+ if (measured > width) {
8471
+ const fitted = truncateAnsi(value, width, "");
8472
+ const current = widthOf(fitted);
8473
+ return current < width ? fitted + " ".repeat(width - current) : fitted;
8474
+ }
8475
+ return measured < width ? value + " ".repeat(width - measured) : value;
8187
8476
  }
8188
8477
  function isNativeBorderLine(line) {
8189
8478
  const stripped = stripAnsi(line);
@@ -8253,7 +8542,10 @@ var StyledEditor = class extends CustomEditor {
8253
8542
  fullTheme;
8254
8543
  onSnapshot;
8255
8544
  semantic;
8545
+ /** Set when config changes; invalidate() then rebuilds the (otherwise stable) semantic theme. */
8546
+ semanticDirty = false;
8256
8547
  disposed = false;
8548
+ renderPlanCache;
8257
8549
  constructor(tui, theme, keybindings, options) {
8258
8550
  super(tui, theme, keybindings);
8259
8551
  this.config = options.config;
@@ -8272,7 +8564,7 @@ var StyledEditor = class extends CustomEditor {
8272
8564
  configure(config) {
8273
8565
  if (this.disposed) return;
8274
8566
  this.config = config;
8275
- this.semantic = semanticTheme(this.piTheme, config);
8567
+ this.semanticDirty = true;
8276
8568
  this.invalidate();
8277
8569
  }
8278
8570
  handleInput(data) {
@@ -8282,62 +8574,27 @@ var StyledEditor = class extends CustomEditor {
8282
8574
  }
8283
8575
  invalidate() {
8284
8576
  super.invalidate();
8285
- this.semantic = semanticTheme(this.piTheme, this.config);
8577
+ if (this.semanticDirty) {
8578
+ this.semantic = semanticTheme(this.piTheme, this.config);
8579
+ this.semanticDirty = false;
8580
+ }
8581
+ this.renderPlanCache = void 0;
8286
8582
  this.tui.requestRender();
8287
8583
  }
8288
8584
  render(width) {
8289
8585
  if (width <= 0) return [];
8290
- const nativeLines = super.render(width);
8291
- const style = this.styleFor(width);
8292
- if (this.autocompleteState) {
8293
- const prompt2 = this.prompt();
8294
- const padding2 = this.paddingFor(width, style);
8295
- const promptWidth2 = widthOf(prompt2) + 1;
8296
- const prefix2 = `${" ".repeat(padding2)}${prompt2} `;
8297
- const continuation2 = " ".repeat(padding2 + promptWidth2);
8298
- const borderIndex = nativeLines.slice(1).findIndex((line) => isNativeBorderLine(line));
8299
- const split = borderIndex >= 0 ? borderIndex + 1 : nativeLines.length;
8300
- const body2 = nativeLines.slice(1, split);
8301
- const dropdown = nativeLines.slice(split);
8302
- const border = this.borderFor();
8303
- const kind2 = this.frameKind(style);
8304
- const renderWidth2 = width - (kind2 === "rounded" ? 2 : 0);
8305
- const sideColor = kind2 === "rounded" ? this.borderColorFor() : void 0;
8306
- const wrap = (line) => kind2 === "rounded" && sideColor ? `${sideColor("\u2502")}${line}${sideColor("\u2502")}` : line;
8307
- const bashHidden2 = this.bashHiddenCount();
8308
- const renderedBody2 = body2.map((line, index) => {
8309
- const source = index === 0 && bashHidden2 > 0 ? stripLeadingVisibleChars(line, bashHidden2) : line;
8310
- return wrap(widthSafe(`${index === 0 ? prefix2 : continuation2}${source}`, renderWidth2));
8311
- });
8312
- const dropdownLines = dropdown.map((line) => wrap(widthSafe(line, renderWidth2)));
8313
- if (kind2 === "rounded") {
8314
- return [
8315
- border(`\u256D${"\u2500".repeat(Math.max(0, width - 2))}\u256E`),
8316
- ...renderedBody2,
8317
- ...dropdownLines,
8318
- border(`\u2570${"\u2500".repeat(Math.max(0, width - 2))}\u256F`)
8319
- ];
8320
- }
8321
- return [border("\u2500".repeat(width)), ...renderedBody2, ...dropdownLines, border("\u2500".repeat(width))];
8322
- }
8323
- if (style === "native") return nativeLines.map((line) => widthSafe(line, width));
8324
- const prompt = this.prompt();
8325
- const promptWidth = widthOf(prompt) + 1;
8326
- const padding = this.paddingFor(width, style);
8327
- const kind = this.frameKind(style);
8328
- const sideReserve = kind === "rounded" ? 2 : 0;
8329
- const renderWidth = Math.max(1, width - sideReserve);
8330
- const innerWidth = Math.max(1, renderWidth - promptWidth - padding * 2);
8331
- const innerLines = super.render(innerWidth);
8586
+ const plan = this.renderPlan(width);
8587
+ const autocompleteState = this.autocompleteState;
8588
+ if (plan.style === "native") return super.render(width).map((line) => widthSafe(line, width));
8589
+ if (autocompleteState) return this.renderAutocompleteFrame(width, plan);
8590
+ const innerLines = super.render(plan.innerWidth);
8332
8591
  if (innerLines.length === 0) return [];
8333
8592
  const body = innerLines.slice(1, -1);
8334
- const prefix = `${" ".repeat(padding)}${prompt} `;
8335
- const continuation = " ".repeat(padding + promptWidth);
8336
8593
  const hint = this.config.editor.hint;
8337
8594
  const showHint = hint !== "" && this.getText() === "";
8338
8595
  const bashHidden = this.bashHiddenCount();
8339
8596
  const renderedBody = body.map((line, index) => {
8340
- const lead = index === 0 ? prefix : continuation;
8597
+ const lead = index === 0 ? plan.prefix : plan.continuation;
8341
8598
  const source = index === 0 && bashHidden > 0 ? stripLeadingVisibleChars(line, bashHidden) : line;
8342
8599
  let content = `${lead}${source}`;
8343
8600
  if (showHint && index === 0 && line) {
@@ -8346,10 +8603,10 @@ var StyledEditor = class extends CustomEditor {
8346
8603
  if (end < content.length) content = content.slice(0, end);
8347
8604
  content += this.semantic.apply("hint", hint);
8348
8605
  }
8349
- return widthSafe(content, renderWidth);
8606
+ return widthSafe(content, plan.renderWidth);
8350
8607
  });
8351
- const metadata = this.metadata(width, style);
8352
- const framed = this.frame(width, style, renderedBody, metadata);
8608
+ const metadata = this.metadata(width, plan.style);
8609
+ const framed = this.frame(width, plan.style, renderedBody, metadata);
8353
8610
  return framed.map((line) => widthSafe(line, width));
8354
8611
  }
8355
8612
  dispose() {
@@ -8357,6 +8614,59 @@ var StyledEditor = class extends CustomEditor {
8357
8614
  this.disposed = true;
8358
8615
  this.invalidate();
8359
8616
  }
8617
+ renderPlan(width) {
8618
+ const style = this.styleFor(width);
8619
+ const prompt = this.prompt();
8620
+ const kind = this.frameKind(style);
8621
+ const key = `${width}:${style}:${kind}:${prompt}`;
8622
+ if (this.renderPlanCache?.key === key) return this.renderPlanCache.plan;
8623
+ const promptWidth = widthOf(prompt) + 1;
8624
+ const padding = this.paddingFor(width, style);
8625
+ const sideReserve = kind === "rounded" ? 2 : 0;
8626
+ const renderWidth = Math.max(1, width - sideReserve);
8627
+ const innerWidth = Math.max(1, renderWidth - promptWidth - padding * 2);
8628
+ const prefix = `${" ".repeat(padding)}${prompt} `;
8629
+ const continuation = " ".repeat(padding + promptWidth);
8630
+ const plan = {
8631
+ style,
8632
+ kind,
8633
+ prompt,
8634
+ promptWidth,
8635
+ padding,
8636
+ sideReserve,
8637
+ renderWidth,
8638
+ innerWidth,
8639
+ prefix,
8640
+ continuation
8641
+ };
8642
+ this.renderPlanCache = { key, plan };
8643
+ return plan;
8644
+ }
8645
+ renderAutocompleteFrame(width, plan) {
8646
+ const nativeLines = super.render(width);
8647
+ const borderIndex = nativeLines.slice(1).findIndex((line) => isNativeBorderLine(line));
8648
+ const split = borderIndex >= 0 ? borderIndex + 1 : nativeLines.length;
8649
+ const body = nativeLines.slice(1, split);
8650
+ const dropdown = nativeLines.slice(split);
8651
+ const border = this.borderFor();
8652
+ const sideColor = plan.kind === "rounded" ? this.borderColorFor() : void 0;
8653
+ const wrap = (line) => plan.kind === "rounded" && sideColor ? `${sideColor("\u2502")}${line}${sideColor("\u2502")}` : line;
8654
+ const bashHidden = this.bashHiddenCount();
8655
+ const renderedBody = body.map((line, index) => {
8656
+ const source = index === 0 && bashHidden > 0 ? stripLeadingVisibleChars(line, bashHidden) : line;
8657
+ return wrap(widthSafe(`${index === 0 ? plan.prefix : plan.continuation}${source}`, plan.renderWidth));
8658
+ });
8659
+ const dropdownLines = dropdown.map((line) => wrap(widthSafe(line, plan.renderWidth)));
8660
+ if (plan.kind === "rounded") {
8661
+ return [
8662
+ border(`\u256D${"\u2500".repeat(Math.max(0, width - 2))}\u256E`),
8663
+ ...renderedBody,
8664
+ ...dropdownLines,
8665
+ border(`\u2570${"\u2500".repeat(Math.max(0, width - 2))}\u256F`)
8666
+ ];
8667
+ }
8668
+ return [border("\u2500".repeat(width)), ...renderedBody, ...dropdownLines, border("\u2500".repeat(width))];
8669
+ }
8360
8670
  prompt() {
8361
8671
  if (this.isBashMode()) {
8362
8672
  const glyph = this.semantic.glyph("bashPrompt");
@@ -8598,13 +8908,13 @@ function resolveAccentRgb(resolved) {
8598
8908
  function styledLogoLines(resolved) {
8599
8909
  const accent = resolveAccentRgb(resolved);
8600
8910
  if (!accent) return [...PI_LOGO_LINES];
8601
- const cacheKey = `${resolved.color("accent")}|${resolved.mode}`;
8602
- if (cacheKey === logoGradientCacheKey && logoGradientCacheLines) return logoGradientCacheLines;
8911
+ const cacheKey2 = `${resolved.color("accent")}|${resolved.mode}`;
8912
+ if (cacheKey2 === logoGradientCacheKey && logoGradientCacheLines) return logoGradientCacheLines;
8603
8913
  const palette = buildLogoPalette(accent);
8604
8914
  logoGradientCacheLines = PI_LOGO_LINES.map(
8605
8915
  (line, rowIndex) => renderLogoGradientLine(line, palette, rowIndex * LOGO_ROW_PHASE_STEP)
8606
8916
  );
8607
- logoGradientCacheKey = cacheKey;
8917
+ logoGradientCacheKey = cacheKey2;
8608
8918
  return logoGradientCacheLines;
8609
8919
  }
8610
8920
  function compactLogoHeader(resolved, details, width) {
@@ -9051,9 +9361,6 @@ function uniqueLayout(layout) {
9051
9361
  function renderGroup(items, separator, padding) {
9052
9362
  return items.map((item) => item.content).filter(Boolean).join(`${padding}${separator}${padding}`);
9053
9363
  }
9054
- function widthOf2(items, separator, padding) {
9055
- return visibleWidth(renderGroup(items, separator, padding));
9056
- }
9057
9364
  function renderStatus(layout, snapshot, width, options) {
9058
9365
  if (width <= 0) return { primary: "", lines: [], visibleSegments: [] };
9059
9366
  const separator = options.separator ?? "\u2502";
@@ -9068,7 +9375,15 @@ function renderStatus(layout, snapshot, width, options) {
9068
9375
  try {
9069
9376
  const result = segment2.render(context);
9070
9377
  if (!result.visible || !result.content) continue;
9071
- candidates.set(id, { id, segment: segment2, result, content: result.content, compact: false, moved: false });
9378
+ candidates.set(id, {
9379
+ id,
9380
+ segment: segment2,
9381
+ result,
9382
+ content: result.content,
9383
+ contentWidth: visibleWidth(result.content),
9384
+ compact: false,
9385
+ moved: false
9386
+ });
9072
9387
  } catch {
9073
9388
  }
9074
9389
  }
@@ -9078,13 +9393,28 @@ function renderStatus(layout, snapshot, width, options) {
9078
9393
  const secondary = normalized.secondary.map((id) => candidates.get(id)).filter((candidate) => candidate !== void 0);
9079
9394
  const visible = [];
9080
9395
  const overflow = [];
9396
+ const gapWidth = visibleWidth(`${padding}${separator}${padding}`);
9397
+ let groupWidth = 0;
9398
+ let groupCount = 0;
9399
+ const widthAfter = (baseWidth, count, candidate) => count === 0 ? candidate.contentWidth : baseWidth + gapWidth + candidate.contentWidth;
9081
9400
  for (const candidate of primary) {
9082
9401
  visible.push(candidate);
9083
- if (widthOf2(visible, separator, padding) <= width) continue;
9402
+ const pushedWidth = widthAfter(groupWidth, groupCount, candidate);
9403
+ if (pushedWidth <= width) {
9404
+ groupWidth = pushedWidth;
9405
+ groupCount++;
9406
+ continue;
9407
+ }
9084
9408
  if (candidate.result.compactContent && !candidate.compact) {
9085
9409
  candidate.content = candidate.result.compactContent;
9086
9410
  candidate.compact = true;
9087
- if (widthOf2(visible, separator, padding) <= width) continue;
9411
+ candidate.contentWidth = visibleWidth(candidate.content);
9412
+ const compactedWidth = widthAfter(groupWidth, groupCount, candidate);
9413
+ if (compactedWidth <= width) {
9414
+ groupWidth = compactedWidth;
9415
+ groupCount++;
9416
+ continue;
9417
+ }
9088
9418
  }
9089
9419
  visible.pop();
9090
9420
  if (candidate.segment.overflow !== "drop" && candidate.segment.overflow !== "primary") {
@@ -9110,9 +9440,14 @@ function renderStatus(layout, snapshot, width, options) {
9110
9440
  }
9111
9441
  if (visibleWidth(primaryText) > width) primaryText = truncateAnsi(primaryText, width);
9112
9442
  const secondaryVisible = [];
9443
+ let secondaryWidth = 0;
9444
+ let secondaryCount = 0;
9113
9445
  for (const candidate of [...secondary].sort((a, b) => b.segment.defaultPriority - a.segment.defaultPriority)) {
9446
+ const pushedWidth = widthAfter(secondaryWidth, secondaryCount, candidate);
9447
+ if (pushedWidth > width) continue;
9114
9448
  secondaryVisible.push(candidate);
9115
- if (widthOf2(secondaryVisible, separator, padding) > width) secondaryVisible.pop();
9449
+ secondaryWidth = pushedWidth;
9450
+ secondaryCount++;
9116
9451
  }
9117
9452
  const secondaryText = renderGroup(secondaryVisible, separator, padding);
9118
9453
  const lines = secondaryText ? [primaryText, secondaryText] : primaryText ? [primaryText] : [];
@@ -9165,6 +9500,28 @@ function separatorsFor(config, theme) {
9165
9500
  if (style === "none") return " ";
9166
9501
  return theme.apply("separator", style);
9167
9502
  }
9503
+ var themeAssetsCache = /* @__PURE__ */ new WeakMap();
9504
+ function themeAssetsFor(activeTheme, config) {
9505
+ let byConfig = themeAssetsCache.get(activeTheme);
9506
+ if (!byConfig) {
9507
+ byConfig = /* @__PURE__ */ new WeakMap();
9508
+ themeAssetsCache.set(activeTheme, byConfig);
9509
+ }
9510
+ let assets = byConfig.get(config);
9511
+ if (!assets) {
9512
+ const resolved = resolveTheme(
9513
+ activeTheme.colors || activeTheme.fg ? {
9514
+ ...activeTheme.colors ? { colors: activeTheme.colors } : {},
9515
+ // Call through the theme instance so `this` binds correctly inside Pi's fg().
9516
+ ...activeTheme.fg ? { fg: (color, text) => activeTheme.fg?.(color, text) ?? text } : {}
9517
+ } : void 0,
9518
+ config
9519
+ );
9520
+ assets = { resolved, separator: separatorsFor(config, resolved) };
9521
+ byConfig.set(config, assets);
9522
+ }
9523
+ return assets;
9524
+ }
9168
9525
  function installStatusLine(options) {
9169
9526
  const existing = installationMap(options.host).get(options.generation);
9170
9527
  if (existing) return existing;
@@ -9197,16 +9554,9 @@ function installStatusLine(options) {
9197
9554
  }
9198
9555
  const render = (activeTheme, width, secondary) => {
9199
9556
  if (width <= 0 || !config.enabled || !config.statusLine.enabled) return [];
9200
- const resolved = resolveTheme(
9201
- activeTheme.colors || activeTheme.fg ? {
9202
- ...activeTheme.colors ? { colors: activeTheme.colors } : {},
9203
- // Call through the theme instance so `this` binds correctly inside Pi's fg().
9204
- ...activeTheme.fg ? { fg: (color, text) => activeTheme.fg?.(color, text) ?? text } : {}
9205
- } : void 0,
9206
- config
9207
- );
9557
+ const { resolved, separator } = themeAssetsFor(activeTheme, config);
9208
9558
  const result = renderStatus(config.statusLine.layout, effectiveSnapshot(snapshot), width, {
9209
- separator: separatorsFor(config, resolved),
9559
+ separator,
9210
9560
  segments,
9211
9561
  theme: resolved,
9212
9562
  options: {
@@ -9287,12 +9637,16 @@ function installStatusLine(options) {
9287
9637
  };
9288
9638
  const factory = (secondary) => (tui, theme) => {
9289
9639
  const currentTheme = theme;
9640
+ let renderCache;
9290
9641
  const component = {
9291
9642
  render(width) {
9643
+ if (renderCache?.width === width) return renderCache.lines;
9292
9644
  const lines = render(currentTheme, width, secondary);
9645
+ renderCache = { width, lines };
9293
9646
  return lines;
9294
9647
  },
9295
9648
  invalidate() {
9649
+ renderCache = void 0;
9296
9650
  primaryComponent = secondary ? primaryComponent : component;
9297
9651
  secondaryComponent = secondary ? component : secondaryComponent;
9298
9652
  if (tui.requestRender) tui.requestRender();
@@ -9667,16 +10021,52 @@ var RenderScheduler = class {
9667
10021
 
9668
10022
  // extension-src/pi-style/app/snapshot.ts
9669
10023
  function createSnapshot(generation2, revision = 0, values = {}) {
9670
- return Object.freeze({ generation: generation2, revision, ...values });
10024
+ return Object.freeze({ ...values, generation: generation2, revision });
9671
10025
  }
9672
10026
  function replaceSnapshot(current, generation2, values) {
9673
10027
  if (current.generation !== generation2) return current;
9674
- return createSnapshot(generation2, current.revision + 1, values);
10028
+ return equalStatusSnapshot(current, values) ? current : createSnapshot(generation2, current.revision + 1, values);
10029
+ }
10030
+ function equalStatusSnapshot(current, next) {
10031
+ const currentEntries = Object.entries(current).filter(([key]) => key !== "generation" && key !== "revision");
10032
+ const nextEntries = Object.entries(next).filter(([key]) => key !== "generation" && key !== "revision");
10033
+ if (currentEntries.length !== nextEntries.length) return false;
10034
+ for (const [key, value] of nextEntries) {
10035
+ if (!hasOwn(current, key)) return false;
10036
+ if (!equalValue(current[key], value)) return false;
10037
+ }
10038
+ return true;
10039
+ }
10040
+ function equalValue(left, right) {
10041
+ if (Object.is(left, right)) return true;
10042
+ if (Array.isArray(left) && Array.isArray(right)) {
10043
+ if (left.length !== right.length) return false;
10044
+ for (let index = 0; index < left.length; index++) {
10045
+ if (!equalValue(left[index], right[index])) return false;
10046
+ }
10047
+ return true;
10048
+ }
10049
+ if (!isPlainRecord(left) || !isPlainRecord(right)) return false;
10050
+ const leftKeys = Object.keys(left);
10051
+ const rightKeys = Object.keys(right);
10052
+ if (leftKeys.length !== rightKeys.length) return false;
10053
+ for (const key of rightKeys) {
10054
+ if (!hasOwn(left, key)) return false;
10055
+ if (!equalValue(left[key], right[key])) return false;
10056
+ }
10057
+ return true;
10058
+ }
10059
+ function hasOwn(value, key) {
10060
+ return Object.hasOwn(value, key);
10061
+ }
10062
+ function isPlainRecord(value) {
10063
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9675
10064
  }
9676
10065
 
9677
10066
  // extension-src/pi-style/app/runtime.ts
9678
- function createPiStyleRuntime(host, generation2, requestRender = () => {
9679
- }) {
10067
+ var GIT_INVALIDATE_DEBOUNCE_MS = 250;
10068
+ function createPiStyleRuntime(host, generation2, requestRender = host.requestRender ?? (() => {
10069
+ })) {
9680
10070
  let disposed = false;
9681
10071
  const extensionStatuses = () => {
9682
10072
  if (!host.extensionStatusProvider) return void 0;
@@ -9687,13 +10077,23 @@ function createPiStyleRuntime(host, generation2, requestRender = () => {
9687
10077
  return [];
9688
10078
  }
9689
10079
  };
10080
+ const contextSnapshot = () => {
10081
+ const usage = host.getContextUsage?.();
10082
+ if (!usage) return void 0;
10083
+ return {
10084
+ ...usage.tokens !== null ? { currentTokens: usage.tokens } : {},
10085
+ windowTokens: usage.contextWindow,
10086
+ ...usage.percent !== null ? { percent: usage.percent } : {}
10087
+ };
10088
+ };
9690
10089
  let currentConfig = host.config;
9691
10090
  const disposables = new DisposableStore();
9692
10091
  const scheduler = new RenderScheduler({ requestRender }, generation2, () => !disposed);
9693
10092
  const git = new CachedGitProvider(host.gitRunner);
10093
+ let pendingGitRefresh;
9694
10094
  const contextProvider = new InMemoryContextProvider();
9695
10095
  const usageProvider = new InMemoryUsageProvider();
9696
- const usage = host.getContextUsage?.();
10096
+ const initialContext = contextSnapshot();
9697
10097
  const initialStatuses = extensionStatuses();
9698
10098
  const initialValues = {
9699
10099
  ...initialStatuses ? { extensionStatuses: initialStatuses } : {},
@@ -9702,13 +10102,7 @@ function createPiStyleRuntime(host, generation2, requestRender = () => {
9702
10102
  ...host.model?.reasoning !== void 0 ? { reasoning: host.model.reasoning } : {},
9703
10103
  ...host.thinkingLevel ? { thinkingLevel: normalizeThinkingLevel(host.thinkingLevel) } : {},
9704
10104
  ...host.cwd ? { cwd: host.cwd } : {},
9705
- ...usage ? {
9706
- context: {
9707
- ...usage.tokens !== null ? { currentTokens: usage.tokens } : {},
9708
- windowTokens: usage.contextWindow,
9709
- ...usage.percent !== null ? { percent: usage.percent } : {}
9710
- }
9711
- } : {}
10105
+ ...initialContext ? { context: initialContext } : {}
9712
10106
  };
9713
10107
  let currentSnapshot = createSnapshot(generation2, 0, initialValues);
9714
10108
  let statusLine;
@@ -9719,14 +10113,35 @@ function createPiStyleRuntime(host, generation2, requestRender = () => {
9719
10113
  editor: "disabled",
9720
10114
  startup: "disabled"
9721
10115
  };
9722
- const startupSnapshot = (config) => ({
10116
+ const snapshotValues = (snapshot) => {
10117
+ const { generation: _generation, revision: _revision, ...values } = snapshot;
10118
+ return values;
10119
+ };
10120
+ const withSnapshotPatch = (patch) => {
10121
+ const next = { ...snapshotValues(currentSnapshot) };
10122
+ for (const [key, value] of Object.entries(patch)) {
10123
+ if (value === void 0) delete next[key];
10124
+ else next[key] = value;
10125
+ }
10126
+ return next;
10127
+ };
10128
+ const createStartupSnapshot = (config, resources = host.resources) => ({
9723
10129
  ...currentSnapshot,
9724
10130
  reason: host.startupReason ?? "startup",
9725
10131
  ...host.provider ? { startupProvider: host.provider } : {},
9726
10132
  ...host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {},
9727
10133
  preset: config.preset,
9728
- ...host.resources ? { resources: host.resources } : {}
10134
+ ...resources ? { resources } : {}
9729
10135
  });
10136
+ const applySnapshot = (nextSnapshot) => {
10137
+ if (nextSnapshot === currentSnapshot) return false;
10138
+ currentSnapshot = nextSnapshot;
10139
+ statusLine?.update(currentSnapshot);
10140
+ editor?.update(currentSnapshot);
10141
+ startup?.update(createStartupSnapshot(currentConfig));
10142
+ return true;
10143
+ };
10144
+ const updateSnapshot = (values) => applySnapshot(replaceSnapshot(currentSnapshot, generation2, values));
9730
10145
  const installStatus = () => {
9731
10146
  if (!host.hasUI || host.mode !== "tui" || !host.ui || !currentConfig.enabled || !currentConfig.statusLine.enabled) {
9732
10147
  installationState = { ...installationState, status: "disabled" };
@@ -9789,7 +10204,7 @@ function createPiStyleRuntime(host, generation2, requestRender = () => {
9789
10204
  startup = installStartup({
9790
10205
  host: { ...host.ui, mode: host.mode, hasUI: host.hasUI },
9791
10206
  config: currentConfig,
9792
- snapshot: startupSnapshot(currentConfig),
10207
+ snapshot: createStartupSnapshot(currentConfig),
9793
10208
  generation: generation2,
9794
10209
  requestRender,
9795
10210
  timeoutMs: 3e3,
@@ -9838,27 +10253,10 @@ function createPiStyleRuntime(host, generation2, requestRender = () => {
9838
10253
  if (host.cwd && currentConfig.enabled && currentConfig.statusLine.enabled) {
9839
10254
  void git.get(host.cwd).then((value) => {
9840
10255
  if (disposed) return;
9841
- currentSnapshot = replaceSnapshot(currentSnapshot, generation2, { ...currentSnapshot, git: value });
9842
- statusLine?.update(currentSnapshot);
9843
- editor?.update(currentSnapshot);
9844
- startup?.update({
9845
- ...currentSnapshot,
9846
- reason: host.startupReason ?? "startup",
9847
- ...host.provider ? { startupProvider: host.provider } : {},
9848
- ...host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {},
9849
- preset: currentConfig.preset,
9850
- ...host.resources ? { resources: host.resources } : {}
9851
- });
9852
- requestRender();
9853
- });
9854
- }
9855
- if (usage) {
9856
- contextProvider.set("active", {
9857
- ...usage.tokens !== null ? { currentTokens: usage.tokens } : {},
9858
- windowTokens: usage.contextWindow,
9859
- ...usage.percent !== null ? { percent: usage.percent } : {}
10256
+ if (updateSnapshot(withSnapshotPatch({ git: value }))) requestRender();
9860
10257
  });
9861
10258
  }
10259
+ if (initialContext) contextProvider.set("active", initialContext);
9862
10260
  return {
9863
10261
  generation: generation2,
9864
10262
  providerIdentity: { git, context: contextProvider, usage: usageProvider },
@@ -9867,49 +10265,33 @@ function createPiStyleRuntime(host, generation2, requestRender = () => {
9867
10265
  },
9868
10266
  mode: host.mode,
9869
10267
  hasUI: host.hasUI,
9870
- snapshot: currentSnapshot,
10268
+ get snapshot() {
10269
+ return currentSnapshot;
10270
+ },
9871
10271
  disposables,
9872
10272
  scheduler,
9873
10273
  updateStartupResources(resources) {
9874
- if (disposed) return;
9875
- startup?.update({
9876
- ...currentSnapshot,
9877
- reason: host.startupReason ?? "startup",
9878
- ...host.provider ? { startupProvider: host.provider } : {},
9879
- ...host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {},
9880
- preset: currentConfig.preset,
9881
- resources
9882
- });
10274
+ if (disposed || !startup) return false;
10275
+ startup.update(createStartupSnapshot(currentConfig, resources));
9883
10276
  requestRender();
10277
+ return true;
9884
10278
  },
9885
10279
  dismissStartup() {
9886
10280
  startup?.dismiss();
9887
10281
  },
9888
- update(values) {
9889
- if (disposed) return;
9890
- const liveUsage = host.getContextUsage?.();
9891
- const context = liveUsage ? {
9892
- ...liveUsage.tokens !== null ? { currentTokens: liveUsage.tokens } : {},
9893
- windowTokens: liveUsage.contextWindow,
9894
- ...liveUsage.percent !== null ? { percent: liveUsage.percent } : {}
9895
- } : void 0;
9896
- const statuses = extensionStatuses();
9897
- currentSnapshot = replaceSnapshot(currentSnapshot, generation2, {
9898
- ...currentSnapshot,
9899
- ...values,
9900
- ...context ? { context } : {},
9901
- ...statuses ? { extensionStatuses: statuses } : {}
9902
- });
9903
- statusLine?.update(currentSnapshot);
9904
- editor?.update(currentSnapshot);
9905
- startup?.update({
9906
- ...currentSnapshot,
9907
- reason: host.startupReason ?? "startup",
9908
- ...host.provider ? { startupProvider: host.provider } : {},
9909
- ...host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {},
9910
- preset: currentConfig.preset,
9911
- ...host.resources ? { resources: host.resources } : {}
9912
- });
10282
+ update(values, options = {}) {
10283
+ if (disposed) return false;
10284
+ const patch = {
10285
+ ...values
10286
+ };
10287
+ if (options.refreshContextUsage) {
10288
+ const context = contextSnapshot();
10289
+ patch.context = context;
10290
+ if (context) contextProvider.set("active", context);
10291
+ else contextProvider.clear();
10292
+ }
10293
+ if (options.refreshExtensionStatuses) patch.extensionStatuses = extensionStatuses();
10294
+ return updateSnapshot(withSnapshotPatch(patch));
9913
10295
  },
9914
10296
  configure(nextConfig) {
9915
10297
  if (disposed) return;
@@ -9941,21 +10323,18 @@ function createPiStyleRuntime(host, generation2, requestRender = () => {
9941
10323
  },
9942
10324
  invalidateGit() {
9943
10325
  if (disposed || !host.cwd || !currentConfig.enabled || !currentConfig.statusLine.enabled) return;
9944
- git.invalidate(host.cwd);
9945
- void git.get(host.cwd).then((value) => {
10326
+ const cwd = host.cwd;
10327
+ git.invalidate(cwd);
10328
+ if (pendingGitRefresh !== void 0) return;
10329
+ pendingGitRefresh = setTimeout(() => {
10330
+ pendingGitRefresh = void 0;
9946
10331
  if (disposed) return;
9947
- currentSnapshot = replaceSnapshot(currentSnapshot, generation2, { ...currentSnapshot, git: value });
9948
- statusLine?.update(currentSnapshot);
9949
- editor?.update(currentSnapshot);
9950
- startup?.update({
9951
- ...currentSnapshot,
9952
- reason: host.startupReason ?? "startup",
9953
- ...host.provider ? { startupProvider: host.provider } : {},
9954
- ...host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {},
9955
- preset: currentConfig.preset
10332
+ void git.get(cwd).then((value) => {
10333
+ if (disposed) return;
10334
+ if (updateSnapshot(withSnapshotPatch({ git: value }))) requestRender();
9956
10335
  });
9957
- requestRender();
9958
- });
10336
+ }, GIT_INVALIDATE_DEBOUNCE_MS);
10337
+ pendingGitRefresh.unref?.();
9959
10338
  },
9960
10339
  get disposed() {
9961
10340
  return disposed;
@@ -9963,6 +10342,10 @@ function createPiStyleRuntime(host, generation2, requestRender = () => {
9963
10342
  dispose() {
9964
10343
  if (disposed) return;
9965
10344
  disposed = true;
10345
+ if (pendingGitRefresh !== void 0) {
10346
+ clearTimeout(pendingGitRefresh);
10347
+ pendingGitRefresh = void 0;
10348
+ }
9966
10349
  scheduler.cancel();
9967
10350
  disposeStatus();
9968
10351
  disposeEditor();
@@ -10158,10 +10541,10 @@ function createPiStyleApp(initialConfig, reloadPort, onConfigChange) {
10158
10541
  sessionShutdown() {
10159
10542
  runtime.stop();
10160
10543
  },
10161
- update(values, kind = "coalesced") {
10544
+ update(values, kind = "coalesced", options) {
10162
10545
  const active = runtime.current;
10163
10546
  if (!active) return;
10164
- active.update(values);
10547
+ if (!active.update(values, options)) return;
10165
10548
  active.scheduler.schedule(kind);
10166
10549
  },
10167
10550
  reload() {
@@ -10464,9 +10847,11 @@ function renderBashExecutionBox(instance, args) {
10464
10847
  try {
10465
10848
  if (host.piStyleStart === void 0) host.piStyleStart = Date.now();
10466
10849
  const renderedWidth = boxWidth(width);
10850
+ const cacheKey2 = `${themeCacheKey(theme)}|${width}|${host.status}|${host.exitCode ?? ""}|${host.command}`;
10851
+ if (host.status !== "running" && host.piStyleRenderCache?.key === cacheKey2) return host.piStyleRenderCache.lines;
10467
10852
  const inner = boxInnerWidth(renderedWidth);
10468
10853
  const wrapped = content.render(inner).map((line) => boxLine(theme, line.startsWith(" ") ? line.slice(1) : line, renderedWidth));
10469
- return [
10854
+ const lines = [
10470
10855
  "",
10471
10856
  boxLabeledBorder(theme, TOP_LEFT, TOP_RIGHT, bashBoxTitle(theme, host), void 0, renderedWidth),
10472
10857
  boxBlankLine(theme, renderedWidth),
@@ -10474,6 +10859,8 @@ function renderBashExecutionBox(instance, args) {
10474
10859
  boxBlankLine(theme, renderedWidth),
10475
10860
  boxLabeledBorder(theme, BOTTOM_LEFT, BOTTOM_RIGHT, bashBoxFooter(theme, host), void 0, renderedWidth)
10476
10861
  ];
10862
+ if (host.status !== "running") host.piStyleRenderCache = { key: cacheKey2, lines };
10863
+ return lines;
10477
10864
  } catch {
10478
10865
  return void 0;
10479
10866
  }
@@ -10504,17 +10891,29 @@ import {
10504
10891
  } from "@earendil-works/pi-coding-agent";
10505
10892
 
10506
10893
  // extension-src/pi-style/features/messages/index.ts
10507
- import { visibleWidth as visibleWidth4 } from "@earendil-works/pi-tui";
10508
10894
  var OSC133_ZONE_START = "\x1B]133;A\x07";
10509
10895
  var OSC133_ZONE_END = "\x1B]133;B\x07";
10510
10896
  var OSC133_ZONE_FINAL = "\x1B]133;C\x07";
10897
+ var BG_RESET = "\x1B[49m";
10898
+ var MAX_RENDER_CACHE_KEYS_PER_INSTANCE = 8;
10899
+ var MAX_LINE_ANALYSIS_ENTRIES = 4096;
10900
+ var renderCacheByInstance = /* @__PURE__ */ new WeakMap();
10901
+ var lineAnalysisCache = /* @__PURE__ */ new Map();
10902
+ var lineCacheEvictionCursor;
10903
+ var childrenScanByInstance = /* @__PURE__ */ new WeakMap();
10904
+ var messageDecorationTestState = {
10905
+ decoratePasses: 0,
10906
+ cacheHits: 0,
10907
+ cacheMisses: 0,
10908
+ lineCacheHits: 0,
10909
+ lineCacheMisses: 0
10910
+ };
10511
10911
  function extractOscEnvelope(line) {
10512
10912
  if (!line.startsWith(OSC133_ZONE_START)) return void 0;
10513
10913
  const bodyEnd = line.indexOf(OSC133_ZONE_END, OSC133_ZONE_START.length);
10514
10914
  if (bodyEnd < 0 || !line.endsWith(OSC133_ZONE_FINAL)) return void 0;
10515
10915
  return { start: OSC133_ZONE_START, body: line.slice(OSC133_ZONE_START.length, bodyEnd), end: line.slice(bodyEnd) };
10516
10916
  }
10517
- var BG_RESET = "\x1B[49m";
10518
10917
  function splitLeadingMarkers(line) {
10519
10918
  let index = 0;
10520
10919
  while (line.startsWith("\x1B]", index)) {
@@ -10524,7 +10923,7 @@ function splitLeadingMarkers(line) {
10524
10923
  if (end === -1) break;
10525
10924
  index = end + 1;
10526
10925
  }
10527
- return { head: line.slice(0, index), rest: line.slice(index) };
10926
+ return index === 0 ? { head: "", rest: line } : { head: line.slice(0, index), rest: line.slice(index) };
10528
10927
  }
10529
10928
  function leadingSgr(line) {
10530
10929
  if (!line.startsWith("\x1B[")) return "";
@@ -10538,96 +10937,304 @@ function leadingSgr(line) {
10538
10937
  }
10539
10938
  function isBackgroundSgr(sequence) {
10540
10939
  if (!sequence.startsWith("\x1B[") || !sequence.endsWith("m")) return false;
10541
- for (const code of sequence.slice(2, -1).split(";")) {
10542
- const value = Number(code);
10543
- if (value === 48 || value === 49) return true;
10544
- if (value >= 40 && value <= 47) return true;
10545
- if (value >= 100 && value <= 107) return true;
10940
+ let value = 0;
10941
+ let empty = true;
10942
+ let valid = true;
10943
+ for (let index = 2; index < sequence.length - 1; index++) {
10944
+ const code = sequence.charCodeAt(index);
10945
+ if (code === 59) {
10946
+ if (valid && matchesBackgroundCode(empty ? 0 : value)) return true;
10947
+ value = 0;
10948
+ empty = true;
10949
+ valid = true;
10950
+ continue;
10951
+ }
10952
+ if (code < 48 || code > 57) {
10953
+ valid = false;
10954
+ continue;
10955
+ }
10956
+ value = value * 10 + (code - 48);
10957
+ empty = false;
10958
+ }
10959
+ return valid && matchesBackgroundCode(empty ? 0 : value);
10960
+ }
10961
+ function matchesBackgroundCode(value) {
10962
+ return value === 48 || value === 49 || value >= 40 && value <= 47 || value >= 100 && value <= 107;
10963
+ }
10964
+ function certifiedAsciiWidth(value) {
10965
+ let width = 0;
10966
+ let index = 0;
10967
+ const length = value.length;
10968
+ while (index < length) {
10969
+ const code = value.charCodeAt(index);
10970
+ if (code === 27) {
10971
+ const next = index + 1 < length ? value.charCodeAt(index + 1) : -1;
10972
+ if (next === 91) {
10973
+ let scan = index + 2;
10974
+ while (scan < length) {
10975
+ const terminator = value.charCodeAt(scan);
10976
+ if (terminator === 109 || // m
10977
+ terminator === 71 || // G
10978
+ terminator === 75 || // K
10979
+ terminator === 72 || // H
10980
+ terminator === 74) {
10981
+ index = scan + 1;
10982
+ break;
10983
+ }
10984
+ scan++;
10985
+ }
10986
+ if (scan >= length) return void 0;
10987
+ continue;
10988
+ }
10989
+ if (next === 93 || next === 95) {
10990
+ let scan = index + 2;
10991
+ let end = -1;
10992
+ while (scan < length) {
10993
+ const terminator = value.charCodeAt(scan);
10994
+ if (terminator === 7) {
10995
+ end = scan + 1;
10996
+ break;
10997
+ }
10998
+ if (terminator === 27 && scan + 1 < length && value.charCodeAt(scan + 1) === 92) {
10999
+ end = scan + 2;
11000
+ break;
11001
+ }
11002
+ scan++;
11003
+ }
11004
+ if (end < 0) return void 0;
11005
+ index = end;
11006
+ continue;
11007
+ }
11008
+ return void 0;
11009
+ }
11010
+ if (code < 32 || code > 126) return void 0;
11011
+ width++;
11012
+ index++;
11013
+ }
11014
+ return width;
11015
+ }
11016
+ function certifiedVisibleWidth(value) {
11017
+ return certifiedAsciiWidth(value) ?? visibleWidth(value);
11018
+ }
11019
+ var prefixWidthMemo;
11020
+ function prefixWidthOf(prefix) {
11021
+ if (prefixWidthMemo?.prefix === prefix) return prefixWidthMemo.width;
11022
+ const width = certifiedVisibleWidth(prefix);
11023
+ prefixWidthMemo = { prefix, width };
11024
+ return width;
11025
+ }
11026
+ function contentText(line) {
11027
+ if (!line.includes("\x1B")) return line;
11028
+ let output = "";
11029
+ let sliceStart = 0;
11030
+ for (let index = 0; index < line.length; index++) {
11031
+ if (line.charCodeAt(index) !== 27) continue;
11032
+ output += line.slice(sliceStart, index);
11033
+ const next = line[index + 1];
11034
+ if (next === "]") {
11035
+ index += 2;
11036
+ while (index < line.length && line.charCodeAt(index) !== 7) index++;
11037
+ } else if (next === "[") {
11038
+ index += 2;
11039
+ while (index < line.length && (line.charCodeAt(index) < 64 || line.charCodeAt(index) > 126)) index++;
11040
+ }
11041
+ sliceStart = index + 1;
11042
+ }
11043
+ output += line.slice(sliceStart);
11044
+ if (output.includes("\x1B]133;")) {
11045
+ return output.replaceAll(OSC133_ZONE_START, "").replaceAll(OSC133_ZONE_END, "").replaceAll(OSC133_ZONE_FINAL, "");
11046
+ }
11047
+ return output;
11048
+ }
11049
+ function isWhitespaceCode(code) {
11050
+ if (code === 32 || code >= 9 && code <= 13) return true;
11051
+ if (code < 128) return false;
11052
+ return code === 160 || code === 5760 || code >= 8192 && code <= 8202 || code === 8232 || code === 8233 || code === 8239 || code === 8287 || code === 12288 || code === 65279;
11053
+ }
11054
+ function hasContent(line) {
11055
+ const length = line.length;
11056
+ let index = 0;
11057
+ while (index < length) {
11058
+ const code = line.charCodeAt(index);
11059
+ if (code === 27) {
11060
+ const next = index + 1 < length ? line.charCodeAt(index + 1) : -1;
11061
+ if (next === 93) {
11062
+ index += 2;
11063
+ while (index < length && line.charCodeAt(index) !== 7) index++;
11064
+ } else if (next === 91) {
11065
+ index += 2;
11066
+ while (index < length) {
11067
+ const inner = line.charCodeAt(index);
11068
+ if (inner >= 64 && inner <= 126) break;
11069
+ index++;
11070
+ }
11071
+ }
11072
+ index++;
11073
+ continue;
11074
+ }
11075
+ if (code >= 55296 && code <= 57343) return true;
11076
+ if (!isWhitespaceCode(code)) return true;
11077
+ index++;
10546
11078
  }
10547
11079
  return false;
10548
11080
  }
10549
- function rebuildAtWidth(line, width, lead) {
10550
- const { head, rest } = splitLeadingMarkers(line);
10551
- const bgAnsi = leadingSgr(rest);
10552
- if (bgAnsi && isBackgroundSgr(bgAnsi) && rest.endsWith(BG_RESET)) {
10553
- const body = rest.slice(bgAnsi.length, rest.length - BG_RESET.length);
10554
- const pad = " ".repeat(Math.max(0, width - visibleWidth4(lead) - visibleWidth4(body)));
10555
- return `${head}${bgAnsi}${lead}${body}${pad}${BG_RESET}`;
10556
- }
10557
- const padded = `${lead}${line}`;
10558
- return `${padded}${" ".repeat(Math.max(0, width - visibleWidth4(padded)))}`;
10559
- }
10560
- function decorateMessageLine(line, index, lastIndex, contentIndex, width, options) {
10561
- const { firstEnvelope, firstHasStart, multilineEnvelope, prefix } = options;
10562
- const prefixWidth = visibleWidth4(prefix);
10563
- const lead = index === contentIndex ? prefix : index > contentIndex ? " ".repeat(prefixWidth) : "";
11081
+ function getLineAnalysis(line) {
11082
+ const cached = lineAnalysisCache.get(line);
11083
+ if (cached) {
11084
+ messageDecorationTestState.lineCacheHits++;
11085
+ lineAnalysisCache.delete(line);
11086
+ lineAnalysisCache.set(line, cached);
11087
+ return cached;
11088
+ }
11089
+ messageDecorationTestState.lineCacheMisses++;
11090
+ const leadingMarkers = splitLeadingMarkers(line);
11091
+ const backgroundAnsi = leadingSgr(leadingMarkers.rest);
11092
+ const isBackgroundWrapped = backgroundAnsi !== "" && isBackgroundSgr(backgroundAnsi) && leadingMarkers.rest.endsWith(BG_RESET);
11093
+ const backgroundBody = isBackgroundWrapped ? leadingMarkers.rest.slice(backgroundAnsi.length, leadingMarkers.rest.length - BG_RESET.length) : void 0;
11094
+ const hasOscStart = line.startsWith(OSC133_ZONE_START);
11095
+ const analysis = {
11096
+ visibleWidth: certifiedVisibleWidth(line),
11097
+ hasContent: hasContent(line),
11098
+ oscEnvelope: hasOscStart ? extractOscEnvelope(line) : void 0,
11099
+ hasOscStart,
11100
+ leadingMarkers,
11101
+ isBackgroundWrapped,
11102
+ backgroundAnsi,
11103
+ backgroundBody,
11104
+ backgroundBodyWidth: backgroundBody === void 0 ? void 0 : certifiedVisibleWidth(backgroundBody)
11105
+ };
11106
+ lineAnalysisCache.set(line, analysis);
11107
+ if (lineAnalysisCache.size > MAX_LINE_ANALYSIS_ENTRIES) {
11108
+ let cursor = lineCacheEvictionCursor;
11109
+ if (cursor === void 0) cursor = lineAnalysisCache.keys();
11110
+ let oldest = cursor.next();
11111
+ if (oldest.done) {
11112
+ cursor = lineAnalysisCache.keys();
11113
+ oldest = cursor.next();
11114
+ }
11115
+ lineCacheEvictionCursor = cursor;
11116
+ if (!oldest.done && oldest.value !== void 0) lineAnalysisCache.delete(oldest.value);
11117
+ }
11118
+ return analysis;
11119
+ }
11120
+ function rebuildAtWidth(line, width, lead, leadWidth, analysis = getLineAnalysis(line)) {
11121
+ if (analysis.isBackgroundWrapped && analysis.backgroundBody !== void 0 && analysis.backgroundBodyWidth !== void 0) {
11122
+ const pad2 = " ".repeat(Math.max(0, width - leadWidth - analysis.backgroundBodyWidth));
11123
+ return `${analysis.leadingMarkers.head}${analysis.backgroundAnsi}${lead}${analysis.backgroundBody}${pad2}${BG_RESET}`;
11124
+ }
11125
+ const pad = " ".repeat(Math.max(0, width - leadWidth - analysis.visibleWidth));
11126
+ return `${lead}${line}${pad}`;
11127
+ }
11128
+ function decorateMessageLine(line, index, lastIndex, contentIndex, width, options, analysis = getLineAnalysis(line)) {
11129
+ const { firstEnvelope, firstHasStart, multilineEnvelope, prefix, prefixWidth, continuationLead } = options;
11130
+ const lead = index === contentIndex ? prefix : index > contentIndex ? continuationLead : "";
11131
+ const leadWidth = index < contentIndex ? 0 : prefixWidth;
10564
11132
  if (index === contentIndex && firstEnvelope)
10565
- return `${firstEnvelope.start}${rebuildAtWidth(firstEnvelope.body, width, prefix)}${firstEnvelope.end}`;
11133
+ return `${firstEnvelope.start}${rebuildAtWidth(firstEnvelope.body, width, prefix, prefixWidth)}${firstEnvelope.end}`;
10566
11134
  if (index === contentIndex && firstHasStart)
10567
- return `${OSC133_ZONE_START}${rebuildAtWidth(line.slice(OSC133_ZONE_START.length), width, prefix)}`;
11135
+ return `${OSC133_ZONE_START}${rebuildAtWidth(line.slice(OSC133_ZONE_START.length), width, prefix, prefixWidth)}`;
10568
11136
  if (index === lastIndex && multilineEnvelope && index !== contentIndex)
10569
11137
  return `${OSC133_ZONE_END}${OSC133_ZONE_FINAL}${rebuildAtWidth(
10570
11138
  line.slice((OSC133_ZONE_END + OSC133_ZONE_FINAL).length),
10571
11139
  width,
10572
- lead
11140
+ lead,
11141
+ leadWidth
10573
11142
  )}`;
10574
11143
  if (index === contentIndex && index === lastIndex && multilineEnvelope && line.startsWith(OSC133_ZONE_END + OSC133_ZONE_FINAL))
10575
11144
  return `${OSC133_ZONE_END}${OSC133_ZONE_FINAL}${rebuildAtWidth(
10576
11145
  line.slice((OSC133_ZONE_END + OSC133_ZONE_FINAL).length),
10577
11146
  width,
10578
- prefix
11147
+ prefix,
11148
+ prefixWidth
10579
11149
  )}`;
10580
- return rebuildAtWidth(line, width, lead);
11150
+ return rebuildAtWidth(line, width, lead, leadWidth, analysis);
10581
11151
  }
10582
- function contentText(line) {
10583
- let output = "";
10584
- for (let index = 0; index < line.length; index++) {
10585
- if (line.charCodeAt(index) !== 27) {
10586
- output += line[index];
10587
- continue;
10588
- }
10589
- const next = line[index + 1];
10590
- if (next === "]") {
10591
- index += 2;
10592
- while (index < line.length && line.charCodeAt(index) !== 7) index++;
10593
- continue;
10594
- }
10595
- if (next === "[") {
10596
- index += 2;
10597
- while (index < line.length && (line.charCodeAt(index) < 64 || line.charCodeAt(index) > 126)) index++;
10598
- }
11152
+ function sameLines(left, right) {
11153
+ if (left === right) return true;
11154
+ if (left.length !== right.length) return false;
11155
+ for (let index = 0; index < left.length; index++) {
11156
+ if (left[index] !== right[index]) return false;
10599
11157
  }
10600
- return output.replaceAll(OSC133_ZONE_START, "").replaceAll(OSC133_ZONE_END, "").replaceAll(OSC133_ZONE_FINAL, "");
11158
+ return true;
10601
11159
  }
10602
- function hasContent(line) {
10603
- return [...contentText(line)].some((character) => !/\s/u.test(character));
11160
+ function cacheKey(width, prefix) {
11161
+ return `${width}\0${prefix}`;
11162
+ }
11163
+ function getRenderCache(instance) {
11164
+ let cache = renderCacheByInstance.get(instance);
11165
+ if (!cache) {
11166
+ cache = /* @__PURE__ */ new Map();
11167
+ renderCacheByInstance.set(instance, cache);
11168
+ }
11169
+ return cache;
11170
+ }
11171
+ function storeRenderCache(instance, width, prefix, native, result) {
11172
+ const cache = getRenderCache(instance);
11173
+ const key = cacheKey(width, prefix);
11174
+ if (cache.has(key)) cache.delete(key);
11175
+ cache.set(key, { nativeRef: native, nativeLines: [...native], result: [...result] });
11176
+ while (cache.size > MAX_RENDER_CACHE_KEYS_PER_INSTANCE) {
11177
+ const oldestKey = cache.keys().next().value;
11178
+ if (oldestKey === void 0) break;
11179
+ cache.delete(oldestKey);
11180
+ }
10604
11181
  }
10605
11182
  function prefixNative(lines, width, prefix) {
10606
11183
  if (!Array.isArray(lines) || lines.length === 0 || !lines.every((line) => typeof line === "string")) return void 0;
11184
+ messageDecorationTestState.decoratePasses++;
10607
11185
  const nativeLines = lines;
10608
- const prefixWidth = visibleWidth4(prefix);
11186
+ const prefixWidth = prefixWidthOf(prefix);
10609
11187
  if (width <= prefixWidth) return void 0;
10610
11188
  const bodyWidth = width - prefixWidth;
10611
- const first = nativeLines[0] ?? "";
11189
+ const analyses = nativeLines.map((line) => getLineAnalysis(line));
10612
11190
  const last = nativeLines.at(-1) ?? "";
11191
+ const lastAnalysis = analyses[analyses.length - 1] ?? getLineAnalysis(last);
10613
11192
  const multilineEnvelope = nativeLines.length > 1 && last.startsWith(OSC133_ZONE_END + OSC133_ZONE_FINAL);
10614
- const firstContentIndex = nativeLines.findIndex((line, index) => {
10615
- if (index !== nativeLines.length - 1 || !multilineEnvelope) return hasContent(line);
10616
- return !nativeLines.slice(0, index).some((earlier) => hasContent(earlier)) && hasContent(line);
10617
- });
11193
+ let firstContentIndex = -1;
11194
+ for (let index = 0; index < nativeLines.length; index++) {
11195
+ const analysis = analyses[index] ?? getLineAnalysis(nativeLines[index] ?? "");
11196
+ if (index !== nativeLines.length - 1 || !multilineEnvelope) {
11197
+ if (analysis.hasContent) {
11198
+ firstContentIndex = index;
11199
+ break;
11200
+ }
11201
+ continue;
11202
+ }
11203
+ let earlierHasContent = false;
11204
+ for (let earlier = 0; earlier < index; earlier++) {
11205
+ if ((analyses[earlier] ?? getLineAnalysis(nativeLines[earlier] ?? "")).hasContent) {
11206
+ earlierHasContent = true;
11207
+ break;
11208
+ }
11209
+ }
11210
+ if (!earlierHasContent && lastAnalysis.hasContent) firstContentIndex = index;
11211
+ break;
11212
+ }
10618
11213
  if (firstContentIndex < 0) return nativeLines;
10619
- const firstEnvelope = firstContentIndex === 0 ? extractOscEnvelope(first) : void 0;
10620
- const firstHasStart = firstContentIndex === 0 && first.startsWith(OSC133_ZONE_START);
11214
+ const firstAnalysis = analyses[0] ?? getLineAnalysis(nativeLines[0] ?? "");
11215
+ const firstEnvelope = firstContentIndex === 0 ? firstAnalysis.oscEnvelope : void 0;
11216
+ const firstHasStart = firstContentIndex === 0 && firstAnalysis.hasOscStart;
11217
+ const continuationLead = " ".repeat(prefixWidth);
10621
11218
  const decorated = nativeLines.map(
10622
- (line, index) => decorateMessageLine(line, index, nativeLines.length - 1, firstContentIndex, width, {
10623
- firstEnvelope,
10624
- firstHasStart,
10625
- multilineEnvelope,
10626
- prefix
10627
- })
11219
+ (line, index) => decorateMessageLine(
11220
+ line,
11221
+ index,
11222
+ nativeLines.length - 1,
11223
+ firstContentIndex,
11224
+ width,
11225
+ {
11226
+ firstEnvelope,
11227
+ firstHasStart,
11228
+ multilineEnvelope,
11229
+ prefix,
11230
+ prefixWidth,
11231
+ continuationLead
11232
+ },
11233
+ analyses[index]
11234
+ )
10628
11235
  );
10629
- if (!decorated.every((line) => visibleWidth4(line) <= width)) return void 0;
10630
- if (!nativeLines.every((line) => visibleWidth4(line) <= bodyWidth)) return void 0;
11236
+ if (!decorated.every((line) => certifiedVisibleWidth(line) <= width)) return void 0;
11237
+ if (!analyses.every((analysis) => analysis.visibleWidth <= bodyWidth)) return void 0;
10631
11238
  return decorated;
10632
11239
  }
10633
11240
  function decorateMessageRender(original, instance, args, snapshot = {
@@ -10640,10 +11247,20 @@ function decorateMessageRender(original, instance, args, snapshot = {
10640
11247
  const width = typeof args[0] === "number" ? args[0] : 0;
10641
11248
  const prefix = snapshot.assistantPrefix;
10642
11249
  if (!snapshot.assistantEnabled) return Reflect.apply(original, instance, args);
10643
- if (width <= visibleWidth4(prefix)) return Reflect.apply(original, instance, args);
10644
- const reducedWidth = width - visibleWidth4(prefix);
11250
+ const prefixWidth = prefixWidthOf(prefix);
11251
+ if (width <= prefixWidth) return Reflect.apply(original, instance, args);
11252
+ const reducedWidth = width - prefixWidth;
10645
11253
  const native = Reflect.apply(original, instance, [reducedWidth, ...args.slice(1)]);
10646
- return prefixNative(native, width, prefix) ?? native;
11254
+ if (!Array.isArray(native) || !native.every((line) => typeof line === "string")) return native;
11255
+ const cached = getRenderCache(instance).get(cacheKey(width, prefix));
11256
+ if (cached && (cached.nativeRef === native || sameLines(cached.nativeLines, native))) {
11257
+ messageDecorationTestState.cacheHits++;
11258
+ return [...cached.result];
11259
+ }
11260
+ messageDecorationTestState.cacheMisses++;
11261
+ const decorated = prefixNative(native, width, prefix) ?? native;
11262
+ storeRenderCache(instance, width, prefix, native, decorated);
11263
+ return decorated;
10647
11264
  }
10648
11265
  function isSpacerChild(child) {
10649
11266
  return typeof child?.setLines === "function";
@@ -10662,8 +11279,16 @@ function hasToolCallItems(message) {
10662
11279
  function isBlankTextChild(child) {
10663
11280
  const candidate = child;
10664
11281
  if (typeof candidate?.setCustomBgFn !== "function" || typeof candidate.render !== "function") return false;
11282
+ if (typeof candidate.text === "string") return contentText(candidate.text).trim() === "";
10665
11283
  return contentText(candidate.render(0).join("\n")).trim() === "";
10666
11284
  }
11285
+ function childrenUnchangedSinceScan(instance, children) {
11286
+ const state = childrenScanByInstance.get(instance);
11287
+ return state !== void 0 && state.childrenRef === children && state.length === children.length;
11288
+ }
11289
+ function markChildrenScanned(instance, children) {
11290
+ childrenScanByInstance.set(instance, { childrenRef: children, length: children.length });
11291
+ }
10667
11292
  function decorateMessageUpdate(original, instance, args, snapshot = {
10668
11293
  assistantPrefix: "\u2502 ",
10669
11294
  assistantEnabled: true,
@@ -10673,24 +11298,22 @@ function decorateMessageUpdate(original, instance, args, snapshot = {
10673
11298
  if (typeof original !== "function") return void 0;
10674
11299
  const result = Reflect.apply(original, instance, args);
10675
11300
  const target = instance;
10676
- if (snapshot.collapseHiddenThinking && target.hideThinkingBlock === true && target.hiddenThinkingLabel === "") {
10677
- const children = target.contentContainer?.children;
10678
- if (children) {
11301
+ const children = target.contentContainer?.children;
11302
+ if (children && !childrenUnchangedSinceScan(instance, children)) {
11303
+ if (snapshot.collapseHiddenThinking && target.hideThinkingBlock === true && target.hiddenThinkingLabel === "") {
10679
11304
  for (let index = children.length - 1; index >= 0; index--) {
10680
11305
  if (!isBlankTextChild(children[index])) continue;
10681
11306
  children.splice(index, 1);
10682
11307
  if (isSpacerChild(children[index])) children.splice(index, 1);
10683
11308
  }
10684
11309
  }
10685
- }
10686
- if (snapshot.hideInterimText && hasToolCallItems(args[0])) {
10687
- const children = target.contentContainer?.children;
10688
- if (children) {
11310
+ if (snapshot.hideInterimText && hasToolCallItems(args[0])) {
10689
11311
  for (let index = children.length - 1; index >= 0; index--) {
10690
11312
  if (isInterimTextChild(children[index])) children.splice(index, 1);
10691
11313
  }
10692
11314
  if (children.every((child) => isSpacerChild(child))) children.length = 0;
10693
11315
  }
11316
+ markChildrenScanned(instance, children);
10694
11317
  }
10695
11318
  return result;
10696
11319
  }
@@ -11003,7 +11626,7 @@ var KNOWN_NATIVE_IDENTITIES = Object.freeze({
11003
11626
  });
11004
11627
  var TRUSTED_NATIVE_FINGERPRINTS = Object.freeze(
11005
11628
  Object.fromEntries(
11006
- Object.entries(KNOWN_NATIVE_IDENTITIES).map(([key, identities]) => [key, identities[0].fingerprint])
11629
+ Object.entries(KNOWN_NATIVE_IDENTITIES).map(([key, identities]) => [key, identities[0]?.fingerprint ?? ""])
11007
11630
  )
11008
11631
  );
11009
11632
  function fingerprint(value) {
@@ -11859,22 +12482,134 @@ function createPiStyleSessionCoordinator(pi, hooks = {}) {
11859
12482
  }
11860
12483
 
11861
12484
  // extension-src/pi-style/pi/session-usage.ts
12485
+ var usageCache = /* @__PURE__ */ new WeakMap();
11862
12486
  function usageFromSession(session) {
11863
- const totals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
11864
- let sawUsage = false;
11865
- for (const entry of session.getEntries()) {
11866
- if (entry.type === "message") {
11867
- if (entry.message.role !== "assistant" && entry.message.role !== "toolResult") continue;
11868
- const usage = "usage" in entry.message ? entry.message.usage : void 0;
11869
- if (!usage) continue;
11870
- addUsage(totals, usage);
11871
- sawUsage = true;
11872
- } else if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) {
11873
- addUsage(totals, entry.usage);
11874
- sawUsage = true;
11875
- }
11876
- }
11877
- if (!sawUsage) return void 0;
12487
+ const entries = session.getEntries();
12488
+ const state = usageCache.get(session) ?? createState();
12489
+ usageCache.set(session, state);
12490
+ if (entries.length === 0) {
12491
+ state.cacheHits++;
12492
+ state.processedLength = 0;
12493
+ state.lastEntryId = void 0;
12494
+ state.lastEntryRef = void 0;
12495
+ state.lastContribution = emptyContribution();
12496
+ state.totals = emptyTotals();
12497
+ state.cached = void 0;
12498
+ return void 0;
12499
+ }
12500
+ if (state.processedLength === 0) {
12501
+ rebuildFrom(entries, state);
12502
+ return state.cached;
12503
+ }
12504
+ if (entries.length < state.processedLength) {
12505
+ rebuildFrom(entries, state);
12506
+ return state.cached;
12507
+ }
12508
+ const currentLast = entries.at(-1);
12509
+ if (!currentLast) {
12510
+ state.cacheHits++;
12511
+ state.cached = void 0;
12512
+ return void 0;
12513
+ }
12514
+ if (entries.length === state.processedLength) {
12515
+ if (currentLast.id !== state.lastEntryId) {
12516
+ rebuildFrom(entries, state);
12517
+ return state.cached;
12518
+ }
12519
+ if (currentLast !== state.lastEntryRef) refreshTailEntry(currentLast, state);
12520
+ else state.cacheHits++;
12521
+ return state.cached;
12522
+ }
12523
+ const previousLast = entries[state.processedLength - 1];
12524
+ if (!previousLast || previousLast.id !== state.lastEntryId) {
12525
+ rebuildFrom(entries, state);
12526
+ return state.cached;
12527
+ }
12528
+ if (previousLast !== state.lastEntryRef) refreshTailEntry(previousLast, state);
12529
+ for (const entry of entries.slice(state.processedLength)) appendEntry(entry, state);
12530
+ return state.cached;
12531
+ }
12532
+ function resetUsageFromSessionCache(session) {
12533
+ if (session) {
12534
+ usageCache.delete(session);
12535
+ return;
12536
+ }
12537
+ usageCache = /* @__PURE__ */ new WeakMap();
12538
+ }
12539
+ function createState() {
12540
+ return {
12541
+ processedLength: 0,
12542
+ lastEntryId: void 0,
12543
+ lastEntryRef: void 0,
12544
+ lastContribution: emptyContribution(),
12545
+ totals: emptyTotals(),
12546
+ cached: void 0,
12547
+ scannedEntries: 0,
12548
+ cacheHits: 0,
12549
+ rebuilds: 0,
12550
+ tailRefreshes: 0
12551
+ };
12552
+ }
12553
+ function rebuildFrom(entries, state) {
12554
+ state.rebuilds++;
12555
+ state.processedLength = 0;
12556
+ state.lastEntryId = void 0;
12557
+ state.lastEntryRef = void 0;
12558
+ state.lastContribution = emptyContribution();
12559
+ state.totals = emptyTotals();
12560
+ state.cached = void 0;
12561
+ for (const entry of entries) appendEntry(entry, state);
12562
+ }
12563
+ function refreshTailEntry(entry, state) {
12564
+ state.tailRefreshes++;
12565
+ subtractContribution(state.totals, state.lastContribution);
12566
+ const contribution = usageContribution(entry);
12567
+ addContribution(state.totals, contribution);
12568
+ state.lastEntryId = entry.id;
12569
+ state.lastEntryRef = entry;
12570
+ state.lastContribution = contribution;
12571
+ state.cached = snapshotFromTotals(state.totals);
12572
+ state.scannedEntries++;
12573
+ }
12574
+ function appendEntry(entry, state) {
12575
+ const contribution = usageContribution(entry);
12576
+ addContribution(state.totals, contribution);
12577
+ state.processedLength++;
12578
+ state.lastEntryId = entry.id;
12579
+ state.lastEntryRef = entry;
12580
+ state.lastContribution = contribution;
12581
+ state.cached = snapshotFromTotals(state.totals);
12582
+ state.scannedEntries++;
12583
+ }
12584
+ function usageContribution(entry) {
12585
+ if (entry.type === "message") {
12586
+ if (entry.message.role !== "assistant" && entry.message.role !== "toolResult") return emptyContribution();
12587
+ const usage = "usage" in entry.message ? entry.message.usage : void 0;
12588
+ if (!usage) return emptyContribution();
12589
+ return {
12590
+ input: usage.input,
12591
+ output: usage.output,
12592
+ cacheRead: usage.cacheRead,
12593
+ cacheWrite: usage.cacheWrite,
12594
+ cost: usage.cost.total,
12595
+ sawUsage: true
12596
+ };
12597
+ }
12598
+ if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) {
12599
+ return {
12600
+ input: entry.usage.input,
12601
+ output: entry.usage.output,
12602
+ cacheRead: entry.usage.cacheRead,
12603
+ cacheWrite: entry.usage.cacheWrite,
12604
+ cost: entry.usage.cost.total,
12605
+ sawUsage: true
12606
+ };
12607
+ }
12608
+ return emptyContribution();
12609
+ }
12610
+ function snapshotFromTotals(totals) {
12611
+ if (totals.input === 0 && totals.output === 0 && totals.cacheRead === 0 && totals.cacheWrite === 0 && totals.cost === 0)
12612
+ return void 0;
11878
12613
  return {
11879
12614
  inputTokens: totals.input,
11880
12615
  outputTokens: totals.output,
@@ -11885,18 +12620,30 @@ function usageFromSession(session) {
11885
12620
  streaming: false
11886
12621
  };
11887
12622
  }
11888
- function addUsage(totals, usage) {
12623
+ function addContribution(totals, usage) {
11889
12624
  totals.input += usage.input;
11890
12625
  totals.output += usage.output;
11891
12626
  totals.cacheRead += usage.cacheRead;
11892
12627
  totals.cacheWrite += usage.cacheWrite;
11893
- totals.cost += usage.cost.total;
12628
+ totals.cost += usage.cost;
12629
+ }
12630
+ function subtractContribution(totals, usage) {
12631
+ totals.input -= usage.input;
12632
+ totals.output -= usage.output;
12633
+ totals.cacheRead -= usage.cacheRead;
12634
+ totals.cacheWrite -= usage.cacheWrite;
12635
+ totals.cost -= usage.cost;
12636
+ }
12637
+ function emptyTotals() {
12638
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
12639
+ }
12640
+ function emptyContribution() {
12641
+ return { ...emptyTotals(), sawUsage: false };
11894
12642
  }
11895
12643
 
11896
12644
  // extension-src/pi-style/pi/index.ts
11897
12645
  function usagePatch(ctx) {
11898
- const usage = usageFromSession(ctx.sessionManager);
11899
- return usage ? { usage } : {};
12646
+ return { usage: usageFromSession(ctx.sessionManager) };
11900
12647
  }
11901
12648
  function activateReadOnlyTools(pi) {
11902
12649
  const available = new Set(pi.getAllTools().map((tool) => tool.name));
@@ -11931,6 +12678,7 @@ function piStyleExtension(pi) {
11931
12678
  const coordinator = createPiStyleSessionCoordinator(pi, compatibilityTestHooks);
11932
12679
  registerPiStyleCommand(pi, coordinator.app);
11933
12680
  pi.on("session_start", async (event, ctx) => {
12681
+ resetUsageFromSessionCache(ctx.sessionManager);
11934
12682
  if (pi.getFlag("pi-style-readonly-tools") === true) {
11935
12683
  activateReadOnlyTools(pi);
11936
12684
  }
@@ -11964,40 +12712,57 @@ function piStyleExtension(pi) {
11964
12712
  )
11965
12713
  );
11966
12714
  pi.on("thinking_level_select", (event) => coordinator.app.update({ thinkingLevel: event.level }, "immediate"));
11967
- pi.on("session_info_changed", (event) => coordinator.app.update({ sessionName: event.name }, "coalesced"));
12715
+ pi.on(
12716
+ "session_info_changed",
12717
+ (event) => coordinator.app.update({ sessionName: event.name }, "coalesced", { refreshExtensionStatuses: true })
12718
+ );
11968
12719
  pi.on("message_start", () => {
11969
12720
  closeActiveBatch();
11970
12721
  });
11971
12722
  pi.on("message_update", () => coordinator.app.update({}, "coalesced"));
11972
- pi.on("message_end", (_event, ctx) => coordinator.app.update({ ...usagePatch(ctx) }, "coalesced"));
12723
+ pi.on(
12724
+ "message_end",
12725
+ (_event, ctx) => coordinator.app.update({ ...usagePatch(ctx) }, "coalesced", { refreshContextUsage: true })
12726
+ );
11973
12727
  pi.on("turn_end", (event, ctx) => {
11974
12728
  registerTurnFromMessage(event.message, event.toolResults);
11975
- coordinator.app.update({ ...usagePatch(ctx) }, "deferred");
12729
+ coordinator.app.update({ ...usagePatch(ctx) }, "deferred", { refreshContextUsage: true });
11976
12730
  });
11977
12731
  pi.on("agent_end", () => {
11978
12732
  const run = finishAgentRun();
11979
12733
  if (run) {
11980
12734
  invalidateTurnMembers(run);
12735
+ releaseTurnInvalidators(run);
11981
12736
  requestToolPresentationRender();
11982
12737
  }
11983
12738
  });
11984
- pi.on("agent_settled", (_event, ctx) => coordinator.app.update({ ...usagePatch(ctx) }, "coalesced"));
12739
+ pi.on(
12740
+ "agent_settled",
12741
+ (_event, ctx) => coordinator.app.update({ ...usagePatch(ctx) }, "coalesced", { refreshContextUsage: true })
12742
+ );
11985
12743
  pi.on("session_tree", (_event, ctx) => {
12744
+ resetUsageFromSessionCache(ctx.sessionManager);
11986
12745
  rebuildTurnRegistryFromEntries(ctx.sessionManager.getEntries());
11987
- coordinator.app.update({ ...usagePatch(ctx) }, "deferred");
12746
+ coordinator.app.update({ ...usagePatch(ctx) }, "deferred", { refreshContextUsage: true });
12747
+ });
12748
+ pi.on("session_compact", (_event, ctx) => {
12749
+ resetUsageFromSessionCache(ctx.sessionManager);
12750
+ coordinator.app.update({ ...usagePatch(ctx) }, "deferred", { refreshContextUsage: true });
11988
12751
  });
11989
- pi.on("session_compact", (_event, ctx) => coordinator.app.update({ ...usagePatch(ctx) }, "deferred"));
11990
12752
  pi.on("tool_result", (event, ctx) => {
11991
12753
  if (["write", "edit", "bash"].includes(event.toolName)) {
11992
12754
  coordinator.app.runtime.current?.invalidateGit();
11993
- coordinator.app.update({ ...usagePatch(ctx) }, "delayed-retry");
12755
+ coordinator.app.update({ ...usagePatch(ctx) }, "delayed-retry", { refreshContextUsage: true });
11994
12756
  }
11995
12757
  });
11996
12758
  pi.on("user_bash", (_event, ctx) => {
11997
12759
  coordinator.app.runtime.current?.invalidateGit();
11998
- coordinator.app.update({ ...usagePatch(ctx) }, "delayed-retry");
12760
+ coordinator.app.update({ ...usagePatch(ctx) }, "delayed-retry", { refreshContextUsage: true });
12761
+ });
12762
+ pi.on("session_shutdown", (_event, ctx) => {
12763
+ resetUsageFromSessionCache(ctx.sessionManager);
12764
+ coordinator.shutdown();
11999
12765
  });
12000
- pi.on("session_shutdown", () => coordinator.shutdown());
12001
12766
  }
12002
12767
  export {
12003
12768
  __setCompatibilityTestHooks,