@quandev104/pi-style 0.1.7 → 0.2.1

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 (26) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/extensions/pi-style.js +763 -319
  3. package/dist/extensions/pi-style.js.map +1 -1
  4. package/extension-src/pi-style/app/command-service.ts +2 -0
  5. package/extension-src/pi-style/app/index.ts +3 -2
  6. package/extension-src/pi-style/app/runtime.ts +72 -84
  7. package/extension-src/pi-style/app/snapshot.ts +41 -2
  8. package/extension-src/pi-style/domain/config-normalization.ts +3 -0
  9. package/extension-src/pi-style/domain/config-types.ts +4 -0
  10. package/extension-src/pi-style/features/editor/index.ts +80 -61
  11. package/extension-src/pi-style/features/messages/index.ts +194 -56
  12. package/extension-src/pi-style/features/tools/bash-execution.ts +12 -1
  13. package/extension-src/pi-style/features/tools/boxed/bash.ts +99 -31
  14. package/extension-src/pi-style/features/tools/boxed/batch.ts +44 -10
  15. package/extension-src/pi-style/features/tools/boxed/edit.ts +30 -15
  16. package/extension-src/pi-style/features/tools/boxed/index.ts +7 -2
  17. package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +31 -15
  18. package/extension-src/pi-style/features/tools/boxed/session-config.ts +48 -14
  19. package/extension-src/pi-style/features/tools/boxed/shared.ts +25 -0
  20. package/extension-src/pi-style/features/tools/boxed/turn-summary.ts +56 -11
  21. package/extension-src/pi-style/features/tools/index.ts +19 -4
  22. package/extension-src/pi-style/pi/compatibility-probe.ts +1 -1
  23. package/extension-src/pi-style/pi/index.ts +26 -13
  24. package/extension-src/pi-style/pi/session-usage.ts +204 -21
  25. package/extension-src/pi-style/shared/box.ts +43 -8
  26. package/package.json +1 -1
@@ -183,9 +183,9 @@ function rgbTo256(r, g, b) {
183
183
  function colorEscapes(mode, hex, kind) {
184
184
  if (!isHexColor(hex)) return void 0;
185
185
  const { r, g, b } = hexToRgb(hex);
186
- const cacheKey = `${mode}:${kind}:${hex}`;
186
+ const cacheKey2 = `${mode}:${kind}:${hex}`;
187
187
  const cache = kind === "fg" ? fgEscapeCache : bgEscapeCache;
188
- let cached = cache.get(cacheKey);
188
+ let cached = cache.get(cacheKey2);
189
189
  if (!cached) {
190
190
  if (mode === "256color") {
191
191
  const idx = rgbTo256(r, g, b);
@@ -199,7 +199,7 @@ function colorEscapes(mode, hex, kind) {
199
199
  suffix: kind === "fg" ? "\x1B[39m" : RESET_BACKGROUND
200
200
  };
201
201
  }
202
- cache.set(cacheKey, cached);
202
+ cache.set(cacheKey2, cached);
203
203
  }
204
204
  return cached;
205
205
  }
@@ -411,8 +411,8 @@ function truncatePrintableAscii(text, width, ellipsis = TRUNCATE_ELLIPSIS) {
411
411
  const targetWidth = Math.max(0, width - ellipsisWidth);
412
412
  return { text: `${text.slice(0, targetWidth)}${ellipsis}`, visibleWidth: targetWidth + ellipsisWidth };
413
413
  }
414
- function truncateSgrAscii(text, width, ellipsis, visibleWidth5) {
415
- if (visibleWidth5 <= width) return { text, visibleWidth: visibleWidth5 };
414
+ function truncateSgrAscii(text, width, ellipsis, visibleWidth4) {
415
+ if (visibleWidth4 <= width) return { text, visibleWidth: visibleWidth4 };
416
416
  const ellipsisWidth = knownEllipsisWidth(ellipsis);
417
417
  if (ellipsisWidth === null) return null;
418
418
  if (ellipsisWidth >= width) return truncatePrintableAscii(ellipsis, width, "");
@@ -684,10 +684,10 @@ function setFullTheme(theme, force = false) {
684
684
  const themeName = resolveThemeName(theme);
685
685
  const sourcePath = resolveThemeSourcePath(theme);
686
686
  if (!themeName && !sourcePath) return;
687
- const cacheKey = sourcePath || themeName;
688
- if (!force && cacheKey === cachedThemeName && (cachedExtras !== null || cachedVars !== null || cachedColors !== null || cachedThemeExport !== null))
687
+ const cacheKey2 = sourcePath || themeName;
688
+ if (!force && cacheKey2 === cachedThemeName && (cachedExtras !== null || cachedVars !== null || cachedColors !== null || cachedThemeExport !== null))
689
689
  return;
690
- cachedThemeName = cacheKey;
690
+ cachedThemeName = cacheKey2;
691
691
  const result = readThemeDiscoveryFromPath(sourcePath) ?? (themeName ? discoverThemeExtras(themeName) : null);
692
692
  cachedExtras = result?.extras ?? null;
693
693
  cachedVars = result?.vars ?? null;
@@ -703,6 +703,15 @@ function getThemeExtra(_theme, key) {
703
703
  }
704
704
 
705
705
  // extension-src/pi-style/shared/box.ts
706
+ var THEME_CACHE_KEYS = /* @__PURE__ */ new WeakMap();
707
+ var nextThemeCacheKey = 1;
708
+ function themeCacheKey(theme) {
709
+ const cached = THEME_CACHE_KEYS.get(theme);
710
+ if (cached !== void 0) return cached;
711
+ const created = nextThemeCacheKey++;
712
+ THEME_CACHE_KEYS.set(theme, created);
713
+ return created;
714
+ }
706
715
  function shortenPath(path) {
707
716
  const home = homedir2();
708
717
  if (path.startsWith(home)) return `~${path.slice(home.length)}`;
@@ -868,7 +877,7 @@ function formatBoxedRunningStatus(theme, elapsedMs) {
868
877
  function dimLine(text) {
869
878
  return `\x1B[2m${text}\x1B[22m`;
870
879
  }
871
- function boxFrameText(theme, text) {
880
+ function boxFrameText(_theme, text) {
872
881
  return dimLine(text);
873
882
  }
874
883
  function boxBorder(theme, left, right, width) {
@@ -949,8 +958,16 @@ function renderBoxedOutputLines(theme, outputLines, width, rawLineBudget = DEFAU
949
958
  let nextInputIndex = 0;
950
959
  let truncated = false;
951
960
  for (; nextInputIndex < outputLines.length; nextInputIndex++) {
952
- const line = boxedTruncatedLine(theme, outputLines[nextInputIndex] ?? "", width);
953
- if (!pushBoundedLines(head, [line], headLimit)) {
961
+ const fragments = (outputLines[nextInputIndex] ?? "").split("\n");
962
+ let headExceeded = false;
963
+ for (const fragment of fragments) {
964
+ const line = boxedTruncatedLine(theme, fragment, width);
965
+ if (!pushBoundedLines(head, [line], headLimit)) {
966
+ headExceeded = true;
967
+ break;
968
+ }
969
+ }
970
+ if (headExceeded) {
954
971
  truncated = true;
955
972
  nextInputIndex++;
956
973
  break;
@@ -960,9 +977,12 @@ function renderBoxedOutputLines(theme, outputLines, width, rawLineBudget = DEFAU
960
977
  const tail = [];
961
978
  const tailStart = Math.max(nextInputIndex, outputLines.length - tailLimit);
962
979
  for (let i = tailStart; i < outputLines.length; i++) {
963
- const line = boxedTruncatedLine(theme, outputLines[i] ?? "", width);
964
- tail.push(line);
965
- if (tail.length > tailLimit) tail.splice(0, tail.length - tailLimit);
980
+ const fragments = (outputLines[i] ?? "").split("\n");
981
+ for (const fragment of fragments) {
982
+ const line = boxedTruncatedLine(theme, fragment, width);
983
+ tail.push(line);
984
+ if (tail.length > tailLimit) tail.splice(0, tail.length - tailLimit);
985
+ }
966
986
  }
967
987
  const skippedInputLines = Math.max(0, tailStart - nextInputIndex);
968
988
  const skippedText = skippedInputLines > 0 ? `\u2026 rendered output truncated; ${skippedInputLines} input lines skipped before tail` : "\u2026 rendered output truncated";
@@ -1083,6 +1103,7 @@ function renderBoxedToolResult(theme, body, options = {}) {
1083
1103
  const bodyLines = typeof body === "function" ? body(maxContentWidth) : body.render(maxContentWidth);
1084
1104
  const errorPrefix = options.isError ? [theme.fg("error", options.errorLabel ?? "\u2717 Error")] : [];
1085
1105
  const outputLines = bodyLines.length > 0 ? [...errorPrefix, ...bodyLines] : [theme.fg("muted", `\u2205 ${options.emptyText ?? "(no output)"}`)];
1106
+ const outputFragments = outputLines.flatMap((line) => line.split("\n"));
1086
1107
  const footerText = (options.footerLines ?? []).join(" \xB7 ");
1087
1108
  const dividerText = typeof options.dividerLabel === "function" ? options.dividerLabel(renderedWidth) : options.dividerLabel ?? "Response";
1088
1109
  const rendered = [
@@ -1097,7 +1118,12 @@ function renderBoxedToolResult(theme, body, options = {}) {
1097
1118
  )
1098
1119
  ],
1099
1120
  boxBlankLine(theme, renderedWidth),
1100
- ...renderBoxedOutputLines(theme, outputLines, renderedWidth, options.renderLineBudget ?? outputLines.length),
1121
+ ...renderBoxedOutputLines(
1122
+ theme,
1123
+ outputFragments,
1124
+ renderedWidth,
1125
+ options.renderLineBudget ?? outputFragments.length
1126
+ ),
1101
1127
  boxBlankLine(theme, renderedWidth),
1102
1128
  boxLabeledBorder(
1103
1129
  theme,
@@ -1419,7 +1445,8 @@ var sessionToolsConfig = {
1419
1445
  showElapsed: true,
1420
1446
  batchOpenGlyph: "\u25CF",
1421
1447
  nerdFonts: false,
1422
- collapseAfterTurn: true
1448
+ collapseAfterTurn: true,
1449
+ collapseMutatingTools: false
1423
1450
  };
1424
1451
  function setToolsRenderConfig(config) {
1425
1452
  sessionToolsConfig = { ...sessionToolsConfig, ...config };
@@ -1427,6 +1454,18 @@ function setToolsRenderConfig(config) {
1427
1454
  function getToolsRenderConfig() {
1428
1455
  return sessionToolsConfig;
1429
1456
  }
1457
+ function getToolsRenderCacheSignature() {
1458
+ return [
1459
+ sessionToolsConfig.maxCollapsedLines,
1460
+ sessionToolsConfig.maxExpandedLines,
1461
+ sessionToolsConfig.dimOutput ? 1 : 0,
1462
+ sessionToolsConfig.showElapsed ? 1 : 0,
1463
+ sessionToolsConfig.batchOpenGlyph,
1464
+ sessionToolsConfig.nerdFonts ? 1 : 0,
1465
+ sessionToolsConfig.collapseAfterTurn ? 1 : 0,
1466
+ sessionToolsConfig.collapseMutatingTools ? 1 : 0
1467
+ ].join("|");
1468
+ }
1430
1469
  var STARTED_AT_KEY = "__piStyleStartedAt";
1431
1470
  var ENDED_AT_KEY = "__piStyleEndedAt";
1432
1471
  var RESULT_SEEN_KEY = "__piStyleResultSeen";
@@ -1454,27 +1493,35 @@ function markResultSeen(state) {
1454
1493
  if (!state || typeof state !== "object") return;
1455
1494
  state[RESULT_SEEN_KEY] = true;
1456
1495
  }
1457
- var tickerStates = /* @__PURE__ */ new Set();
1496
+ var tickerEntries = /* @__PURE__ */ new Map();
1497
+ var sharedTickerHandle;
1498
+ function ensureSharedTicker() {
1499
+ if (sharedTickerHandle !== void 0 || tickerEntries.size === 0) return;
1500
+ sharedTickerHandle = setInterval(() => {
1501
+ for (const { invalidate } of tickerEntries.values()) invalidate();
1502
+ }, 1e3);
1503
+ }
1504
+ function stopSharedTickerIfIdle() {
1505
+ if (sharedTickerHandle === void 0 || tickerEntries.size > 0) return;
1506
+ clearInterval(sharedTickerHandle);
1507
+ sharedTickerHandle = void 0;
1508
+ }
1458
1509
  function startElapsedTicker(state, invalidate) {
1459
1510
  if (!state || typeof state !== "object") return;
1460
- if (state[TICKER_KEY] !== void 0) return;
1461
- state[TICKER_KEY] = setInterval(() => invalidate(), 1e3);
1462
- tickerStates.add(state);
1511
+ state[TICKER_KEY] = true;
1512
+ tickerEntries.set(state, { invalidate });
1513
+ ensureSharedTicker();
1463
1514
  }
1464
1515
  function stopElapsedTicker(state) {
1465
1516
  if (!state || typeof state !== "object") return;
1466
- const handle = state[TICKER_KEY];
1467
- if (handle !== void 0) clearInterval(handle);
1468
1517
  delete state[TICKER_KEY];
1469
- tickerStates.delete(state);
1518
+ tickerEntries.delete(state);
1519
+ stopSharedTickerIfIdle();
1470
1520
  }
1471
1521
  function stopAllElapsedTickers() {
1472
- for (const state of tickerStates) {
1473
- const handle = state[TICKER_KEY];
1474
- if (handle !== void 0) clearInterval(handle);
1475
- delete state[TICKER_KEY];
1476
- }
1477
- tickerStates.clear();
1522
+ for (const state of tickerEntries.keys()) delete state[TICKER_KEY];
1523
+ tickerEntries.clear();
1524
+ stopSharedTickerIfIdle();
1478
1525
  }
1479
1526
 
1480
1527
  // extension-src/pi-style/features/tools/boxed/batch.ts
@@ -1510,6 +1557,7 @@ function createBatch(meta, leaderId, detail, opts = {}) {
1510
1557
  leaderId,
1511
1558
  startedAt: performance.now(),
1512
1559
  closed: false,
1560
+ revision: 0,
1513
1561
  members: [
1514
1562
  {
1515
1563
  toolCallId: leaderId,
@@ -1525,14 +1573,20 @@ function createBatch(meta, leaderId, detail, opts = {}) {
1525
1573
  batchByCallId.set(leaderId, batch);
1526
1574
  return batch;
1527
1575
  }
1576
+ function bumpBatchRevision(batch) {
1577
+ batch.revision++;
1578
+ delete batch.renderCache;
1579
+ }
1528
1580
  function registerBatchCall(meta, detail, context, opts = {}) {
1529
1581
  const existing = batchByCallId.get(context.toolCallId);
1530
1582
  if (existing) {
1531
1583
  const member2 = existing.members.find((entry) => entry.toolCallId === context.toolCallId);
1532
1584
  if (member2) {
1585
+ const changed = member2.detail !== detail || member2.pattern !== opts.pattern || member2.pathLabel !== opts.pathLabel;
1533
1586
  member2.detail = detail;
1534
1587
  if (opts.pattern !== void 0) member2.pattern = opts.pattern;
1535
1588
  if (opts.pathLabel !== void 0) member2.pathLabel = opts.pathLabel;
1589
+ if (changed) bumpBatchRevision(existing);
1536
1590
  }
1537
1591
  return { batch: existing, isLeader: existing.leaderId === context.toolCallId };
1538
1592
  }
@@ -1551,6 +1605,7 @@ function registerBatchCall(meta, detail, context, opts = {}) {
1551
1605
  };
1552
1606
  current.members.push(member);
1553
1607
  batchByCallId.set(context.toolCallId, current);
1608
+ bumpBatchRevision(current);
1554
1609
  return { batch: current, isLeader: false };
1555
1610
  }
1556
1611
  function registerBatchResult(meta, data, context) {
@@ -1558,14 +1613,19 @@ function registerBatchResult(meta, data, context) {
1558
1613
  if (!batch || batch.meta.toolName !== meta.toolName) return { batch: void 0, isLeader: false };
1559
1614
  const member = batch.members.find((entry) => entry.toolCallId === context.toolCallId);
1560
1615
  if (member) {
1561
- member.status = data.isPartial ? "running" : "done";
1562
- member.isError = !data.isPartial && data.isError;
1616
+ const nextStatus = data.isPartial ? "running" : "done";
1617
+ const nextIsError = !data.isPartial && data.isError;
1618
+ const changed = member.status !== nextStatus || member.isError !== nextIsError || member.errorText !== (nextIsError ? data.errorText : void 0) || member.outputEntries !== data.entries;
1619
+ member.status = nextStatus;
1620
+ member.isError = nextIsError;
1563
1621
  if (member.isError && data.errorText !== void 0) member.errorText = data.errorText;
1564
1622
  else delete member.errorText;
1565
1623
  if (data.entries !== void 0) member.outputEntries = data.entries;
1624
+ if (changed) bumpBatchRevision(batch);
1566
1625
  }
1567
1626
  if (batch.completedAt === void 0 && batch.members.every((entry) => entry.status === "done")) {
1568
1627
  batch.completedAt = performance.now();
1628
+ bumpBatchRevision(batch);
1569
1629
  }
1570
1630
  return { batch, isLeader: batch.leaderId === context.toolCallId };
1571
1631
  }
@@ -1758,9 +1818,16 @@ function renderBatchPanelLines(theme, batch, status, width) {
1758
1818
  function renderBatchAwareCall(theme, batch) {
1759
1819
  return {
1760
1820
  invalidate() {
1821
+ delete batch.renderCache;
1761
1822
  },
1762
1823
  render(width) {
1763
- return renderBatchPanelLines(theme, batch, batchStatus(batch), width);
1824
+ const status = batchStatus(batch);
1825
+ if (!status.allDone) return renderBatchPanelLines(theme, batch, status, width);
1826
+ const cacheKey2 = [themeCacheKey(theme), getToolsRenderCacheSignature(), width, batch.revision].join("|");
1827
+ if (batch.renderCache?.key === cacheKey2) return batch.renderCache.lines;
1828
+ const lines = renderBatchPanelLines(theme, batch, status, width);
1829
+ batch.renderCache = { key: cacheKey2, lines };
1830
+ return lines;
1764
1831
  }
1765
1832
  };
1766
1833
  }
@@ -1775,6 +1842,13 @@ function emptyBatchResult() {
1775
1842
  }
1776
1843
 
1777
1844
  // extension-src/pi-style/features/tools/boxed/turn-summary.ts
1845
+ var MUTATING_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "quick_edit", "substitute_edit", "target_edit"]);
1846
+ function isMutatingTool(toolName) {
1847
+ return MUTATING_TOOLS.has(toolName);
1848
+ }
1849
+ function mutatingCollapses() {
1850
+ return getToolsRenderConfig().collapseMutatingTools;
1851
+ }
1778
1852
  var memberByCallId = /* @__PURE__ */ new Map();
1779
1853
  var invalidateByCallId = /* @__PURE__ */ new Map();
1780
1854
  function resetTurnRegistry() {
@@ -1805,7 +1879,7 @@ function registerTurn(calls, isErrorById, ended) {
1805
1879
  isError: isErrorById.get(toolCallId) === true
1806
1880
  };
1807
1881
  });
1808
- const leader = members.find((member) => !member.isError);
1882
+ const leader = members.find((member) => !member.isError && (!isMutatingTool(member.toolName) || mutatingCollapses()));
1809
1883
  const turn = {
1810
1884
  leaderId: leader?.toolCallId ?? "",
1811
1885
  ended: ended && complete,
@@ -1835,7 +1909,9 @@ function registerTurnFromMessage(message, toolResults) {
1835
1909
  isError: isErrorById.get(toolCallId) === true
1836
1910
  };
1837
1911
  });
1838
- const leader = newMembers.find((member) => !member.isError);
1912
+ const leader = newMembers.find(
1913
+ (member) => !member.isError && (!isMutatingTool(member.toolName) || mutatingCollapses())
1914
+ );
1839
1915
  if (!currentRun) {
1840
1916
  currentRun = {
1841
1917
  leaderId: leader?.toolCallId ?? "",
@@ -1935,11 +2011,13 @@ function turnSummaryParts(turn) {
1935
2011
  const order = [];
1936
2012
  let failedCount = 0;
1937
2013
  let elapsedMs;
2014
+ const collapseMutating = mutatingCollapses();
1938
2015
  for (const member of turn.members) {
1939
2016
  if (member.isError) {
1940
2017
  failedCount++;
1941
2018
  continue;
1942
2019
  }
2020
+ if (!collapseMutating && isMutatingTool(member.toolName)) continue;
1943
2021
  if (member.elapsedMs !== void 0) elapsedMs = (elapsedMs ?? 0) + member.elapsedMs;
1944
2022
  const existing = counts.get(member.toolName);
1945
2023
  if (existing === void 0) {
@@ -1949,8 +2027,8 @@ function turnSummaryParts(turn) {
1949
2027
  }
1950
2028
  const parts = order.map((toolName) => {
1951
2029
  const count = counts.get(toolName) ?? 0;
1952
- const style = TURN_SUMMARY_STYLE[toolName] ?? { verb: "ran", unit: `${toolName} call` };
1953
- return `${style.verb} ${count} ${pluralForm(style.unit, count)}`;
2030
+ const style = TURN_SUMMARY_STYLE[toolName];
2031
+ return style ? `${style.verb} ${count} ${pluralForm(style.unit, count)}` : `used ${count} ${toolName}`;
1954
2032
  });
1955
2033
  return { parts, failedCount, elapsedMs };
1956
2034
  }
@@ -1959,7 +2037,7 @@ function formatTurnSummaryLine(theme, turn) {
1959
2037
  const parts = summary.parts.join(", ");
1960
2038
  let line = `${theme.fg("dim", `\u2794 ${parts}`)}`;
1961
2039
  if (summary.failedCount > 0)
1962
- line += theme.fg("error", ` \xB7 ${summary.failedCount} ${pluralForm("failed", summary.failedCount)}`);
2040
+ line += theme.fg("error", ` \xB7 ${summary.failedCount} ${pluralForm("failure", summary.failedCount)}`);
1963
2041
  if (summary.elapsedMs !== void 0) line += theme.fg("dim", ` \xB7 ${(summary.elapsedMs / 1e3).toFixed(2)}s`);
1964
2042
  return line;
1965
2043
  }
@@ -4974,6 +5052,17 @@ function compactFooterWithState(theme, result, context, options = {}) {
4974
5052
  function resultFooterLines(theme, result, context, extraParts = []) {
4975
5053
  return [formatBoxedFooter(theme, result, extraParts, stateElapsedMs(context))];
4976
5054
  }
5055
+ function getRenderCacheKey(prefix, theme, ...parts) {
5056
+ return [prefix, themeCacheKey(theme), getToolsRenderCacheSignature(), ...parts].join("|");
5057
+ }
5058
+ function memoizedStateComponent(state, slot, key, build) {
5059
+ if (!state || typeof state !== "object") return build();
5060
+ const cached = state[slot];
5061
+ if (cached && cached.key === key) return cached.component;
5062
+ const component = build();
5063
+ state[slot] = { key, component };
5064
+ return component;
5065
+ }
4977
5066
  function clearFooterState(context) {
4978
5067
  clearCompactBoxedFooter(context.state);
4979
5068
  }
@@ -5293,11 +5382,11 @@ var EMPTY_BASH_RESULT = Object.freeze({
5293
5382
  }
5294
5383
  });
5295
5384
  function createBashResultPreview(theme, text, options, color) {
5296
- let cacheKey = "";
5385
+ let cacheKey2 = "";
5297
5386
  let cacheLines = null;
5298
5387
  return {
5299
5388
  invalidate() {
5300
- cacheKey = "";
5389
+ cacheKey2 = "";
5301
5390
  cacheLines = null;
5302
5391
  },
5303
5392
  render(width) {
@@ -5305,7 +5394,7 @@ function createBashResultPreview(theme, text, options, color) {
5305
5394
  const cfg = getToolsRenderConfig();
5306
5395
  const expanded = Boolean(options.expanded);
5307
5396
  const cacheId = `${bodyWidth}|${expanded ? 1 : 0}|${cfg.maxExpandedLines}|${cfg.dimOutput ? 1 : 0}`;
5308
- if (cacheLines && cacheKey === cacheId) return cacheLines;
5397
+ if (cacheLines && cacheKey2 === cacheId) return cacheLines;
5309
5398
  if (!expanded) {
5310
5399
  const needed = cfg.maxCollapsedLines;
5311
5400
  let totalNewlines = 0;
@@ -5320,14 +5409,14 @@ function createBashResultPreview(theme, text, options, color) {
5320
5409
  }
5321
5410
  }
5322
5411
  if (text.length === 0) {
5323
- cacheKey = cacheId;
5412
+ cacheKey2 = cacheId;
5324
5413
  cacheLines = [];
5325
5414
  return cacheLines;
5326
5415
  }
5327
5416
  const tail = replaceTabs(text.slice(scanFrom)).replace(/\r/g, "");
5328
5417
  const shownLines = tail ? tail.split("\n").map((l) => clampLineLength(l)) : [];
5329
5418
  if (shownLines.length === 0) {
5330
- cacheKey = cacheId;
5419
+ cacheKey2 = cacheId;
5331
5420
  cacheLines = [];
5332
5421
  return cacheLines;
5333
5422
  }
@@ -5336,7 +5425,7 @@ function createBashResultPreview(theme, text, options, color) {
5336
5425
  if (color === "error") return formatToolOutputLine(theme, truncated, "error");
5337
5426
  return cfg.dimOutput ? formatToolOutputLine(theme, truncated) : formatToolOutputLine(theme, truncated, "text");
5338
5427
  });
5339
- cacheKey = cacheId;
5428
+ cacheKey2 = cacheId;
5340
5429
  cacheLines = truncatedShown;
5341
5430
  return cacheLines;
5342
5431
  }
@@ -5344,7 +5433,7 @@ function createBashResultPreview(theme, text, options, color) {
5344
5433
  const logicalLines = normalized.split("\n").map((l) => clampLineLength(l));
5345
5434
  const hasOutput = !(logicalLines.length === 1 && logicalLines[0] === "");
5346
5435
  if (!hasOutput) {
5347
- cacheKey = cacheId;
5436
+ cacheKey2 = cacheId;
5348
5437
  cacheLines = [];
5349
5438
  return cacheLines;
5350
5439
  }
@@ -5355,11 +5444,11 @@ function createBashResultPreview(theme, text, options, color) {
5355
5444
  const truncated = expandedLines.slice(-cfg.maxExpandedLines).map(applyColor);
5356
5445
  const remaining = expandedLines.length - cfg.maxExpandedLines;
5357
5446
  truncated.unshift(theme.fg("dim", `\u2026 ${remaining} earlier lines`));
5358
- cacheKey = cacheId;
5447
+ cacheKey2 = cacheId;
5359
5448
  cacheLines = truncated;
5360
5449
  return cacheLines;
5361
5450
  }
5362
- cacheKey = cacheId;
5451
+ cacheKey2 = cacheId;
5363
5452
  cacheLines = expandedLines.map(applyColor);
5364
5453
  return cacheLines;
5365
5454
  }
@@ -5573,27 +5662,35 @@ function renderSemanticPanel(theme, toolCallId, context) {
5573
5662
  },
5574
5663
  render(width) {
5575
5664
  if (!state) return [];
5576
- if (state.fallback) {
5577
- return renderBoxedBashCall(
5578
- theme,
5579
- state.command.split("\n"),
5580
- context,
5581
- bashWidthKey(state.command, context?.args?.timeout)
5582
- ).render(width);
5583
- }
5584
- if (isBashTreeClass(state.cls)) {
5585
- const treeState = { cls: state.cls };
5586
- if (state.parsed !== void 0) treeState.parsed = state.parsed;
5587
- return renderBashTreeLines(theme, treeState, width);
5588
- }
5589
- if (isGhClass(state.cls)) {
5590
- const ghState = { cls: state.cls };
5591
- if (state.parsed !== void 0) ghState.parsed = state.parsed;
5592
- return renderGhCardLines(theme, ghState, width);
5593
- }
5594
- const gitState = { cls: state.cls };
5595
- if (state.parsed !== void 0) gitState.parsed = state.parsed;
5596
- return renderGitCardLines(theme, gitState, width);
5665
+ const renderFresh = () => {
5666
+ if (state.fallback) {
5667
+ return renderBoxedBashCall(
5668
+ theme,
5669
+ state.command.split("\n"),
5670
+ context,
5671
+ bashWidthKey(state.command, context?.args?.timeout)
5672
+ ).render(width);
5673
+ }
5674
+ if (isBashTreeClass(state.cls)) {
5675
+ const treeState = { cls: state.cls };
5676
+ if (state.parsed !== void 0) treeState.parsed = state.parsed;
5677
+ return renderBashTreeLines(theme, treeState, width);
5678
+ }
5679
+ if (isGhClass(state.cls)) {
5680
+ const ghState = { cls: state.cls };
5681
+ if (state.parsed !== void 0) ghState.parsed = state.parsed;
5682
+ return renderGhCardLines(theme, ghState, width);
5683
+ }
5684
+ const gitState = { cls: state.cls };
5685
+ if (state.parsed !== void 0) gitState.parsed = state.parsed;
5686
+ return renderGitCardLines(theme, gitState, width);
5687
+ };
5688
+ if (!state.finished) return renderFresh();
5689
+ const cacheKey2 = [themeCacheKey(theme), getToolsRenderCacheSignature(), width, state.revision].join("|");
5690
+ if (state.renderCache?.key === cacheKey2) return state.renderCache.lines;
5691
+ const lines = renderFresh();
5692
+ state.renderCache = { key: cacheKey2, lines };
5693
+ return lines;
5597
5694
  }
5598
5695
  };
5599
5696
  }
@@ -5602,7 +5699,26 @@ var bashTool = {
5602
5699
  noteExecutionStart(context);
5603
5700
  const cls = classifyBashSemantic(String(args?.command ?? ""));
5604
5701
  if (cls) {
5605
- semanticStates.set(context.toolCallId, { cls, command: String(args?.command ?? "") });
5702
+ const command = String(args?.command ?? "");
5703
+ const existing = semanticStates.get(context.toolCallId);
5704
+ if (existing) {
5705
+ if (existing.command !== command || existing.cls.kind !== cls.kind) {
5706
+ delete existing.parsed;
5707
+ delete existing.fallback;
5708
+ existing.finished = false;
5709
+ existing.revision++;
5710
+ delete existing.renderCache;
5711
+ }
5712
+ existing.command = command;
5713
+ existing.cls = cls;
5714
+ } else {
5715
+ semanticStates.set(context.toolCallId, {
5716
+ cls,
5717
+ command,
5718
+ finished: false,
5719
+ revision: 0
5720
+ });
5721
+ }
5606
5722
  return renderSemanticPanel(theme, context.toolCallId, context);
5607
5723
  }
5608
5724
  noteBoxedCallState(context);
@@ -5623,12 +5739,18 @@ var bashTool = {
5623
5739
  const parsed = parseSemanticOutput(cls, output);
5624
5740
  const state = semanticStates.get(context.toolCallId);
5625
5741
  if (parsed) {
5626
- if (state) state.parsed = parsed;
5627
- else
5742
+ if (state) {
5743
+ state.parsed = parsed;
5744
+ state.finished = !options.isPartial;
5745
+ state.revision++;
5746
+ delete state.renderCache;
5747
+ } else
5628
5748
  semanticStates.set(context.toolCallId, {
5629
5749
  cls,
5630
5750
  command: String(context?.args?.command ?? ""),
5631
- parsed
5751
+ parsed,
5752
+ finished: !options.isPartial,
5753
+ revision: 0
5632
5754
  });
5633
5755
  if (isGitDiffClass(cls)) {
5634
5756
  return renderGitDiffResult(theme, parsed, options, context);
@@ -5638,7 +5760,12 @@ var bashTool = {
5638
5760
  }
5639
5761
  return EMPTY_BASH_TREE_RESULT;
5640
5762
  }
5641
- if (state) state.fallback = true;
5763
+ if (state) {
5764
+ state.fallback = true;
5765
+ state.finished = !options.isPartial;
5766
+ state.revision++;
5767
+ delete state.renderCache;
5768
+ }
5642
5769
  }
5643
5770
  } else if (options.isPartial) {
5644
5771
  startElapsedTicker(context.state, context.invalidate);
@@ -5651,7 +5778,20 @@ var bashTool = {
5651
5778
  if (firstResultPass) return EMPTY_BASH_RESULT;
5652
5779
  return renderBashStreamingResult(theme, raw, options, context);
5653
5780
  }
5654
- return renderBashFinalResult(theme, raw, options, context);
5781
+ return memoizedStateComponent(
5782
+ context.state,
5783
+ "__piStyleBashFinalResult",
5784
+ getRenderCacheKey(
5785
+ "bash-final-result",
5786
+ theme,
5787
+ Boolean(options.expanded),
5788
+ Boolean(context.isError),
5789
+ String(context?.args?.command ?? ""),
5790
+ raw,
5791
+ getStateElapsedMs(context.state) ?? ""
5792
+ ),
5793
+ () => renderBashFinalResult(theme, raw, options, context)
5794
+ );
5655
5795
  }
5656
5796
  };
5657
5797
 
@@ -5731,21 +5871,33 @@ var editTool = {
5731
5871
  const maxRows = expanded ? 160 : 36;
5732
5872
  const diffView = new AdaptiveDiffComponent(theme, rows, maxRows, shouldHighlight ? language : void 0);
5733
5873
  const expandHint2 = !expanded && diffView.hasCollapsed() ? "Ctrl+O more" : void 0;
5734
- return renderBoxedToolResult(
5735
- theme,
5736
- {
5737
- render(width) {
5738
- return diffView.render(width);
5874
+ return memoizedStateComponent(
5875
+ context.state,
5876
+ "__piStyleEditDiffResult",
5877
+ getRenderCacheKey(
5878
+ "edit-diff-result",
5879
+ theme,
5880
+ Boolean(expanded),
5881
+ diff,
5882
+ sourcePath ?? "",
5883
+ editDiffFooter(theme, result, context, stats)
5884
+ ),
5885
+ () => renderBoxedToolResult(
5886
+ theme,
5887
+ {
5888
+ render(width) {
5889
+ return diffView.render(width);
5890
+ },
5891
+ invalidate() {
5892
+ diffView.invalidate();
5893
+ }
5739
5894
  },
5740
- invalidate() {
5741
- diffView.invalidate();
5895
+ {
5896
+ dividerLabel: diffDividerLabel2(theme, stats),
5897
+ ...expandHint2 ? { dividerRightLabel: expandHint2 } : {},
5898
+ footerLines: [editDiffFooter(theme, result, context, stats)]
5742
5899
  }
5743
- },
5744
- {
5745
- dividerLabel: diffDividerLabel2(theme, stats),
5746
- ...expandHint2 ? { dividerRightLabel: expandHint2 } : {},
5747
- footerLines: [editDiffFooter(theme, result, context, stats)]
5748
- }
5900
+ )
5749
5901
  );
5750
5902
  }
5751
5903
  };
@@ -6142,21 +6294,34 @@ function renderQuickEditResult(_toolName, result, options, theme, context, confi
6142
6294
  const maxRows = expanded ? 160 : 36;
6143
6295
  const diffView = new AdaptiveDiffComponent(theme, rows, maxRows, shouldHighlight ? language : void 0);
6144
6296
  const expandHint2 = !expanded && diffView.hasCollapsed() ? "Ctrl+O more" : void 0;
6145
- return renderBoxedToolResult(
6146
- theme,
6147
- {
6148
- render(width) {
6149
- return diffView.render(width);
6297
+ return memoizedStateComponent(
6298
+ context.state,
6299
+ "__piStyleQuickEditDiffResult",
6300
+ getRenderCacheKey(
6301
+ "quick-edit-diff-result",
6302
+ theme,
6303
+ config.toolLabel,
6304
+ Boolean(expanded),
6305
+ diff,
6306
+ argPath,
6307
+ quickEditDiffFooter(theme, result, context, stats)
6308
+ ),
6309
+ () => renderBoxedToolResult(
6310
+ theme,
6311
+ {
6312
+ render(width) {
6313
+ return diffView.render(width);
6314
+ },
6315
+ invalidate() {
6316
+ diffView.invalidate();
6317
+ }
6150
6318
  },
6151
- invalidate() {
6152
- diffView.invalidate();
6319
+ {
6320
+ dividerLabel: quickEditDividerLabel(theme, stats),
6321
+ ...expandHint2 ? { dividerRightLabel: expandHint2 } : {},
6322
+ footerLines: [quickEditDiffFooter(theme, result, context, stats)]
6153
6323
  }
6154
- },
6155
- {
6156
- dividerLabel: quickEditDividerLabel(theme, stats),
6157
- ...expandHint2 ? { dividerRightLabel: expandHint2 } : {},
6158
- footerLines: [quickEditDiffFooter(theme, result, context, stats)]
6159
- }
6324
+ )
6160
6325
  );
6161
6326
  }
6162
6327
  function quickEditFooter(theme, context) {
@@ -6314,9 +6479,11 @@ var REGISTRY = {
6314
6479
  target_edit: quickEditToolFor("target_edit")
6315
6480
  };
6316
6481
  function collapsedTurnFor(toolCallId, expanded) {
6317
- if (expanded || !getToolsRenderConfig().collapseAfterTurn) return void 0;
6482
+ const config = getToolsRenderConfig();
6483
+ if (expanded || !config.collapseAfterTurn) return void 0;
6318
6484
  const entry = getTurnEntry(toolCallId);
6319
6485
  if (!entry?.turn.ended || entry.member.isError) return void 0;
6486
+ if (isMutatingTool(entry.member.toolName) && !config.collapseMutatingTools) return void 0;
6320
6487
  return entry.turn;
6321
6488
  }
6322
6489
  function renderBoxedToolCall2(toolName, args, theme, context) {
@@ -6596,14 +6763,27 @@ function createToolDecorationOwner(snapshot = {}) {
6596
6763
  if (typeof renderer !== "function") {
6597
6764
  neutralizeToolContainerBackground(instance);
6598
6765
  if (subtype === "tool-call-renderer")
6599
- return (callArgs, theme, context) => renderBoxedToolCall2(toolName, callArgs, theme, context);
6600
- return (result, options, theme, context) => renderBoxedToolResult2(
6601
- toolName,
6602
- result,
6603
- options,
6604
- theme,
6605
- context
6606
- );
6766
+ return (callArgs, theme, context) => {
6767
+ const component = renderBoxedToolCall2(
6768
+ toolName,
6769
+ callArgs,
6770
+ theme,
6771
+ context
6772
+ );
6773
+ if (component === EMPTY_BATCH_COMPONENT) hideBatchMember(instance);
6774
+ return component;
6775
+ };
6776
+ return (result, options, theme, context) => {
6777
+ const component = renderBoxedToolResult2(
6778
+ toolName,
6779
+ result,
6780
+ options,
6781
+ theme,
6782
+ context
6783
+ );
6784
+ if (component === EMPTY_BATCH_COMPONENT) hideBatchMember(instance);
6785
+ return component;
6786
+ };
6607
6787
  }
6608
6788
  return function(...rendererArgs) {
6609
6789
  const valid = subtype === "tool-call-renderer" ? validCallArgs(rendererArgs) : validResultArgs(rendererArgs);
@@ -6824,7 +7004,8 @@ var DEFAULT_CONFIG = Object.freeze({
6824
7004
  maxExpandedLines: 50,
6825
7005
  dimOutput: false,
6826
7006
  showElapsed: true,
6827
- collapseAfterTurn: true
7007
+ collapseAfterTurn: true,
7008
+ collapseMutatingTools: false
6828
7009
  }),
6829
7010
  theme: Object.freeze({
6830
7011
  nerdFonts: "auto",
@@ -6940,7 +7121,8 @@ function normalizeConfig(input, defaults = DEFAULT_CONFIG) {
6940
7121
  maxExpandedLines: maxExpanded,
6941
7122
  dimOutput: bool(tools.dimOutput, defaults.tools.dimOutput),
6942
7123
  showElapsed: bool(tools.showElapsed, defaults.tools.showElapsed),
6943
- collapseAfterTurn: bool(tools.collapseAfterTurn, defaults.tools.collapseAfterTurn)
7124
+ collapseAfterTurn: bool(tools.collapseAfterTurn, defaults.tools.collapseAfterTurn),
7125
+ collapseMutatingTools: bool(tools.collapseMutatingTools, defaults.tools.collapseMutatingTools)
6944
7126
  }),
6945
7127
  theme: Object.freeze({
6946
7128
  nerdFonts: stringEnum(theme.nerdFonts, ["auto", "on", "off"], defaults.theme.nerdFonts),
@@ -6988,6 +7170,7 @@ var BOOL_PATHS = /* @__PURE__ */ new Set([
6988
7170
  "tools.showElapsed",
6989
7171
  "tools.dimOutput",
6990
7172
  "tools.collapseAfterTurn",
7173
+ "tools.collapseMutatingTools",
6991
7174
  "compatibility.allowSafePatches",
6992
7175
  "compatibility.allowCorePatches",
6993
7176
  "compatibility.preferExistingEditor",
@@ -7458,6 +7641,7 @@ var allowedPaths = /* @__PURE__ */ new Set([
7458
7641
  "tools.dimOutput",
7459
7642
  "tools.showElapsed",
7460
7643
  "tools.collapseAfterTurn",
7644
+ "tools.collapseMutatingTools",
7461
7645
  "theme.nerdFonts",
7462
7646
  "theme.terminalBackgroundSync",
7463
7647
  "theme.autoApply",
@@ -7486,6 +7670,7 @@ function validatePathValue(path, value) {
7486
7670
  "tools.showElapsed",
7487
7671
  "tools.dimOutput",
7488
7672
  "tools.collapseAfterTurn",
7673
+ "tools.collapseMutatingTools",
7489
7674
  "compatibility.allowSafePatches",
7490
7675
  "compatibility.allowCorePatches",
7491
7676
  "compatibility.preferExistingEditor",
@@ -8205,6 +8390,7 @@ var StyledEditor = class extends CustomEditor {
8205
8390
  onSnapshot;
8206
8391
  semantic;
8207
8392
  disposed = false;
8393
+ renderPlanCache;
8208
8394
  constructor(tui, theme, keybindings, options) {
8209
8395
  super(tui, theme, keybindings);
8210
8396
  this.config = options.config;
@@ -8234,61 +8420,23 @@ var StyledEditor = class extends CustomEditor {
8234
8420
  invalidate() {
8235
8421
  super.invalidate();
8236
8422
  this.semantic = semanticTheme(this.piTheme, this.config);
8423
+ this.renderPlanCache = void 0;
8237
8424
  this.tui.requestRender();
8238
8425
  }
8239
8426
  render(width) {
8240
8427
  if (width <= 0) return [];
8241
- const nativeLines = super.render(width);
8242
- const style = this.styleFor(width);
8243
- if (this.autocompleteState) {
8244
- const prompt2 = this.prompt();
8245
- const padding2 = this.paddingFor(width, style);
8246
- const promptWidth2 = widthOf(prompt2) + 1;
8247
- const prefix2 = `${" ".repeat(padding2)}${prompt2} `;
8248
- const continuation2 = " ".repeat(padding2 + promptWidth2);
8249
- const borderIndex = nativeLines.slice(1).findIndex((line) => isNativeBorderLine(line));
8250
- const split = borderIndex >= 0 ? borderIndex + 1 : nativeLines.length;
8251
- const body2 = nativeLines.slice(1, split);
8252
- const dropdown = nativeLines.slice(split);
8253
- const border = this.borderFor();
8254
- const kind2 = this.frameKind(style);
8255
- const renderWidth2 = width - (kind2 === "rounded" ? 2 : 0);
8256
- const sideColor = kind2 === "rounded" ? this.borderColorFor() : void 0;
8257
- const wrap = (line) => kind2 === "rounded" && sideColor ? `${sideColor("\u2502")}${line}${sideColor("\u2502")}` : line;
8258
- const bashHidden2 = this.bashHiddenCount();
8259
- const renderedBody2 = body2.map((line, index) => {
8260
- const source = index === 0 && bashHidden2 > 0 ? stripLeadingVisibleChars(line, bashHidden2) : line;
8261
- return wrap(widthSafe(`${index === 0 ? prefix2 : continuation2}${source}`, renderWidth2));
8262
- });
8263
- const dropdownLines = dropdown.map((line) => wrap(widthSafe(line, renderWidth2)));
8264
- if (kind2 === "rounded") {
8265
- return [
8266
- border(`\u256D${"\u2500".repeat(Math.max(0, width - 2))}\u256E`),
8267
- ...renderedBody2,
8268
- ...dropdownLines,
8269
- border(`\u2570${"\u2500".repeat(Math.max(0, width - 2))}\u256F`)
8270
- ];
8271
- }
8272
- return [border("\u2500".repeat(width)), ...renderedBody2, ...dropdownLines, border("\u2500".repeat(width))];
8273
- }
8274
- if (style === "native") return nativeLines.map((line) => widthSafe(line, width));
8275
- const prompt = this.prompt();
8276
- const promptWidth = widthOf(prompt) + 1;
8277
- const padding = this.paddingFor(width, style);
8278
- const kind = this.frameKind(style);
8279
- const sideReserve = kind === "rounded" ? 2 : 0;
8280
- const renderWidth = Math.max(1, width - sideReserve);
8281
- const innerWidth = Math.max(1, renderWidth - promptWidth - padding * 2);
8282
- const innerLines = super.render(innerWidth);
8428
+ const plan = this.renderPlan(width);
8429
+ const autocompleteState = this.autocompleteState;
8430
+ if (plan.style === "native") return super.render(width).map((line) => widthSafe(line, width));
8431
+ if (autocompleteState) return this.renderAutocompleteFrame(width, plan);
8432
+ const innerLines = super.render(plan.innerWidth);
8283
8433
  if (innerLines.length === 0) return [];
8284
8434
  const body = innerLines.slice(1, -1);
8285
- const prefix = `${" ".repeat(padding)}${prompt} `;
8286
- const continuation = " ".repeat(padding + promptWidth);
8287
8435
  const hint = this.config.editor.hint;
8288
8436
  const showHint = hint !== "" && this.getText() === "";
8289
8437
  const bashHidden = this.bashHiddenCount();
8290
8438
  const renderedBody = body.map((line, index) => {
8291
- const lead = index === 0 ? prefix : continuation;
8439
+ const lead = index === 0 ? plan.prefix : plan.continuation;
8292
8440
  const source = index === 0 && bashHidden > 0 ? stripLeadingVisibleChars(line, bashHidden) : line;
8293
8441
  let content = `${lead}${source}`;
8294
8442
  if (showHint && index === 0 && line) {
@@ -8297,10 +8445,10 @@ var StyledEditor = class extends CustomEditor {
8297
8445
  if (end < content.length) content = content.slice(0, end);
8298
8446
  content += this.semantic.apply("hint", hint);
8299
8447
  }
8300
- return widthSafe(content, renderWidth);
8448
+ return widthSafe(content, plan.renderWidth);
8301
8449
  });
8302
- const metadata = this.metadata(width, style);
8303
- const framed = this.frame(width, style, renderedBody, metadata);
8450
+ const metadata = this.metadata(width, plan.style);
8451
+ const framed = this.frame(width, plan.style, renderedBody, metadata);
8304
8452
  return framed.map((line) => widthSafe(line, width));
8305
8453
  }
8306
8454
  dispose() {
@@ -8308,6 +8456,59 @@ var StyledEditor = class extends CustomEditor {
8308
8456
  this.disposed = true;
8309
8457
  this.invalidate();
8310
8458
  }
8459
+ renderPlan(width) {
8460
+ const style = this.styleFor(width);
8461
+ const prompt = this.prompt();
8462
+ const kind = this.frameKind(style);
8463
+ const key = `${width}:${style}:${kind}:${prompt}`;
8464
+ if (this.renderPlanCache?.key === key) return this.renderPlanCache.plan;
8465
+ const promptWidth = widthOf(prompt) + 1;
8466
+ const padding = this.paddingFor(width, style);
8467
+ const sideReserve = kind === "rounded" ? 2 : 0;
8468
+ const renderWidth = Math.max(1, width - sideReserve);
8469
+ const innerWidth = Math.max(1, renderWidth - promptWidth - padding * 2);
8470
+ const prefix = `${" ".repeat(padding)}${prompt} `;
8471
+ const continuation = " ".repeat(padding + promptWidth);
8472
+ const plan = {
8473
+ style,
8474
+ kind,
8475
+ prompt,
8476
+ promptWidth,
8477
+ padding,
8478
+ sideReserve,
8479
+ renderWidth,
8480
+ innerWidth,
8481
+ prefix,
8482
+ continuation
8483
+ };
8484
+ this.renderPlanCache = { key, plan };
8485
+ return plan;
8486
+ }
8487
+ renderAutocompleteFrame(width, plan) {
8488
+ const nativeLines = super.render(width);
8489
+ const borderIndex = nativeLines.slice(1).findIndex((line) => isNativeBorderLine(line));
8490
+ const split = borderIndex >= 0 ? borderIndex + 1 : nativeLines.length;
8491
+ const body = nativeLines.slice(1, split);
8492
+ const dropdown = nativeLines.slice(split);
8493
+ const border = this.borderFor();
8494
+ const sideColor = plan.kind === "rounded" ? this.borderColorFor() : void 0;
8495
+ const wrap = (line) => plan.kind === "rounded" && sideColor ? `${sideColor("\u2502")}${line}${sideColor("\u2502")}` : line;
8496
+ const bashHidden = this.bashHiddenCount();
8497
+ const renderedBody = body.map((line, index) => {
8498
+ const source = index === 0 && bashHidden > 0 ? stripLeadingVisibleChars(line, bashHidden) : line;
8499
+ return wrap(widthSafe(`${index === 0 ? plan.prefix : plan.continuation}${source}`, plan.renderWidth));
8500
+ });
8501
+ const dropdownLines = dropdown.map((line) => wrap(widthSafe(line, plan.renderWidth)));
8502
+ if (plan.kind === "rounded") {
8503
+ return [
8504
+ border(`\u256D${"\u2500".repeat(Math.max(0, width - 2))}\u256E`),
8505
+ ...renderedBody,
8506
+ ...dropdownLines,
8507
+ border(`\u2570${"\u2500".repeat(Math.max(0, width - 2))}\u256F`)
8508
+ ];
8509
+ }
8510
+ return [border("\u2500".repeat(width)), ...renderedBody, ...dropdownLines, border("\u2500".repeat(width))];
8511
+ }
8311
8512
  prompt() {
8312
8513
  if (this.isBashMode()) {
8313
8514
  const glyph = this.semantic.glyph("bashPrompt");
@@ -8549,13 +8750,13 @@ function resolveAccentRgb(resolved) {
8549
8750
  function styledLogoLines(resolved) {
8550
8751
  const accent = resolveAccentRgb(resolved);
8551
8752
  if (!accent) return [...PI_LOGO_LINES];
8552
- const cacheKey = `${resolved.color("accent")}|${resolved.mode}`;
8553
- if (cacheKey === logoGradientCacheKey && logoGradientCacheLines) return logoGradientCacheLines;
8753
+ const cacheKey2 = `${resolved.color("accent")}|${resolved.mode}`;
8754
+ if (cacheKey2 === logoGradientCacheKey && logoGradientCacheLines) return logoGradientCacheLines;
8554
8755
  const palette = buildLogoPalette(accent);
8555
8756
  logoGradientCacheLines = PI_LOGO_LINES.map(
8556
8757
  (line, rowIndex) => renderLogoGradientLine(line, palette, rowIndex * LOGO_ROW_PHASE_STEP)
8557
8758
  );
8558
- logoGradientCacheKey = cacheKey;
8759
+ logoGradientCacheKey = cacheKey2;
8559
8760
  return logoGradientCacheLines;
8560
8761
  }
8561
8762
  function compactLogoHeader(resolved, details, width) {
@@ -9618,16 +9819,51 @@ var RenderScheduler = class {
9618
9819
 
9619
9820
  // extension-src/pi-style/app/snapshot.ts
9620
9821
  function createSnapshot(generation2, revision = 0, values = {}) {
9621
- return Object.freeze({ generation: generation2, revision, ...values });
9822
+ return Object.freeze({ ...values, generation: generation2, revision });
9622
9823
  }
9623
9824
  function replaceSnapshot(current, generation2, values) {
9624
9825
  if (current.generation !== generation2) return current;
9625
- return createSnapshot(generation2, current.revision + 1, values);
9826
+ return equalStatusSnapshot(current, values) ? current : createSnapshot(generation2, current.revision + 1, values);
9827
+ }
9828
+ function equalStatusSnapshot(current, next) {
9829
+ const currentEntries = Object.entries(current).filter(([key]) => key !== "generation" && key !== "revision");
9830
+ const nextEntries = Object.entries(next).filter(([key]) => key !== "generation" && key !== "revision");
9831
+ if (currentEntries.length !== nextEntries.length) return false;
9832
+ for (const [key, value] of nextEntries) {
9833
+ if (!hasOwn(current, key)) return false;
9834
+ if (!equalValue(current[key], value)) return false;
9835
+ }
9836
+ return true;
9837
+ }
9838
+ function equalValue(left, right) {
9839
+ if (Object.is(left, right)) return true;
9840
+ if (Array.isArray(left) && Array.isArray(right)) {
9841
+ if (left.length !== right.length) return false;
9842
+ for (let index = 0; index < left.length; index++) {
9843
+ if (!equalValue(left[index], right[index])) return false;
9844
+ }
9845
+ return true;
9846
+ }
9847
+ if (!isPlainRecord(left) || !isPlainRecord(right)) return false;
9848
+ const leftKeys = Object.keys(left);
9849
+ const rightKeys = Object.keys(right);
9850
+ if (leftKeys.length !== rightKeys.length) return false;
9851
+ for (const key of rightKeys) {
9852
+ if (!hasOwn(left, key)) return false;
9853
+ if (!equalValue(left[key], right[key])) return false;
9854
+ }
9855
+ return true;
9856
+ }
9857
+ function hasOwn(value, key) {
9858
+ return Object.hasOwn(value, key);
9859
+ }
9860
+ function isPlainRecord(value) {
9861
+ return typeof value === "object" && value !== null && !Array.isArray(value);
9626
9862
  }
9627
9863
 
9628
9864
  // extension-src/pi-style/app/runtime.ts
9629
- function createPiStyleRuntime(host, generation2, requestRender = () => {
9630
- }) {
9865
+ function createPiStyleRuntime(host, generation2, requestRender = host.requestRender ?? (() => {
9866
+ })) {
9631
9867
  let disposed = false;
9632
9868
  const extensionStatuses = () => {
9633
9869
  if (!host.extensionStatusProvider) return void 0;
@@ -9638,13 +9874,22 @@ function createPiStyleRuntime(host, generation2, requestRender = () => {
9638
9874
  return [];
9639
9875
  }
9640
9876
  };
9877
+ const contextSnapshot = () => {
9878
+ const usage = host.getContextUsage?.();
9879
+ if (!usage) return void 0;
9880
+ return {
9881
+ ...usage.tokens !== null ? { currentTokens: usage.tokens } : {},
9882
+ windowTokens: usage.contextWindow,
9883
+ ...usage.percent !== null ? { percent: usage.percent } : {}
9884
+ };
9885
+ };
9641
9886
  let currentConfig = host.config;
9642
9887
  const disposables = new DisposableStore();
9643
9888
  const scheduler = new RenderScheduler({ requestRender }, generation2, () => !disposed);
9644
9889
  const git = new CachedGitProvider(host.gitRunner);
9645
9890
  const contextProvider = new InMemoryContextProvider();
9646
9891
  const usageProvider = new InMemoryUsageProvider();
9647
- const usage = host.getContextUsage?.();
9892
+ const initialContext = contextSnapshot();
9648
9893
  const initialStatuses = extensionStatuses();
9649
9894
  const initialValues = {
9650
9895
  ...initialStatuses ? { extensionStatuses: initialStatuses } : {},
@@ -9653,13 +9898,7 @@ function createPiStyleRuntime(host, generation2, requestRender = () => {
9653
9898
  ...host.model?.reasoning !== void 0 ? { reasoning: host.model.reasoning } : {},
9654
9899
  ...host.thinkingLevel ? { thinkingLevel: normalizeThinkingLevel(host.thinkingLevel) } : {},
9655
9900
  ...host.cwd ? { cwd: host.cwd } : {},
9656
- ...usage ? {
9657
- context: {
9658
- ...usage.tokens !== null ? { currentTokens: usage.tokens } : {},
9659
- windowTokens: usage.contextWindow,
9660
- ...usage.percent !== null ? { percent: usage.percent } : {}
9661
- }
9662
- } : {}
9901
+ ...initialContext ? { context: initialContext } : {}
9663
9902
  };
9664
9903
  let currentSnapshot = createSnapshot(generation2, 0, initialValues);
9665
9904
  let statusLine;
@@ -9670,14 +9909,35 @@ function createPiStyleRuntime(host, generation2, requestRender = () => {
9670
9909
  editor: "disabled",
9671
9910
  startup: "disabled"
9672
9911
  };
9673
- const startupSnapshot = (config) => ({
9912
+ const snapshotValues = (snapshot) => {
9913
+ const { generation: _generation, revision: _revision, ...values } = snapshot;
9914
+ return values;
9915
+ };
9916
+ const withSnapshotPatch = (patch) => {
9917
+ const next = { ...snapshotValues(currentSnapshot) };
9918
+ for (const [key, value] of Object.entries(patch)) {
9919
+ if (value === void 0) delete next[key];
9920
+ else next[key] = value;
9921
+ }
9922
+ return next;
9923
+ };
9924
+ const createStartupSnapshot = (config, resources = host.resources) => ({
9674
9925
  ...currentSnapshot,
9675
9926
  reason: host.startupReason ?? "startup",
9676
9927
  ...host.provider ? { startupProvider: host.provider } : {},
9677
9928
  ...host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {},
9678
9929
  preset: config.preset,
9679
- ...host.resources ? { resources: host.resources } : {}
9930
+ ...resources ? { resources } : {}
9680
9931
  });
9932
+ const applySnapshot = (nextSnapshot) => {
9933
+ if (nextSnapshot === currentSnapshot) return false;
9934
+ currentSnapshot = nextSnapshot;
9935
+ statusLine?.update(currentSnapshot);
9936
+ editor?.update(currentSnapshot);
9937
+ startup?.update(createStartupSnapshot(currentConfig));
9938
+ return true;
9939
+ };
9940
+ const updateSnapshot = (values) => applySnapshot(replaceSnapshot(currentSnapshot, generation2, values));
9681
9941
  const installStatus = () => {
9682
9942
  if (!host.hasUI || host.mode !== "tui" || !host.ui || !currentConfig.enabled || !currentConfig.statusLine.enabled) {
9683
9943
  installationState = { ...installationState, status: "disabled" };
@@ -9740,7 +10000,7 @@ function createPiStyleRuntime(host, generation2, requestRender = () => {
9740
10000
  startup = installStartup({
9741
10001
  host: { ...host.ui, mode: host.mode, hasUI: host.hasUI },
9742
10002
  config: currentConfig,
9743
- snapshot: startupSnapshot(currentConfig),
10003
+ snapshot: createStartupSnapshot(currentConfig),
9744
10004
  generation: generation2,
9745
10005
  requestRender,
9746
10006
  timeoutMs: 3e3,
@@ -9789,27 +10049,10 @@ function createPiStyleRuntime(host, generation2, requestRender = () => {
9789
10049
  if (host.cwd && currentConfig.enabled && currentConfig.statusLine.enabled) {
9790
10050
  void git.get(host.cwd).then((value) => {
9791
10051
  if (disposed) return;
9792
- currentSnapshot = replaceSnapshot(currentSnapshot, generation2, { ...currentSnapshot, git: value });
9793
- statusLine?.update(currentSnapshot);
9794
- editor?.update(currentSnapshot);
9795
- startup?.update({
9796
- ...currentSnapshot,
9797
- reason: host.startupReason ?? "startup",
9798
- ...host.provider ? { startupProvider: host.provider } : {},
9799
- ...host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {},
9800
- preset: currentConfig.preset,
9801
- ...host.resources ? { resources: host.resources } : {}
9802
- });
9803
- requestRender();
9804
- });
9805
- }
9806
- if (usage) {
9807
- contextProvider.set("active", {
9808
- ...usage.tokens !== null ? { currentTokens: usage.tokens } : {},
9809
- windowTokens: usage.contextWindow,
9810
- ...usage.percent !== null ? { percent: usage.percent } : {}
10052
+ if (updateSnapshot(withSnapshotPatch({ git: value }))) requestRender();
9811
10053
  });
9812
10054
  }
10055
+ if (initialContext) contextProvider.set("active", initialContext);
9813
10056
  return {
9814
10057
  generation: generation2,
9815
10058
  providerIdentity: { git, context: contextProvider, usage: usageProvider },
@@ -9818,49 +10061,33 @@ function createPiStyleRuntime(host, generation2, requestRender = () => {
9818
10061
  },
9819
10062
  mode: host.mode,
9820
10063
  hasUI: host.hasUI,
9821
- snapshot: currentSnapshot,
10064
+ get snapshot() {
10065
+ return currentSnapshot;
10066
+ },
9822
10067
  disposables,
9823
10068
  scheduler,
9824
10069
  updateStartupResources(resources) {
9825
- if (disposed) return;
9826
- startup?.update({
9827
- ...currentSnapshot,
9828
- reason: host.startupReason ?? "startup",
9829
- ...host.provider ? { startupProvider: host.provider } : {},
9830
- ...host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {},
9831
- preset: currentConfig.preset,
9832
- resources
9833
- });
10070
+ if (disposed || !startup) return false;
10071
+ startup.update(createStartupSnapshot(currentConfig, resources));
9834
10072
  requestRender();
10073
+ return true;
9835
10074
  },
9836
10075
  dismissStartup() {
9837
10076
  startup?.dismiss();
9838
10077
  },
9839
- update(values) {
9840
- if (disposed) return;
9841
- const liveUsage = host.getContextUsage?.();
9842
- const context = liveUsage ? {
9843
- ...liveUsage.tokens !== null ? { currentTokens: liveUsage.tokens } : {},
9844
- windowTokens: liveUsage.contextWindow,
9845
- ...liveUsage.percent !== null ? { percent: liveUsage.percent } : {}
9846
- } : void 0;
9847
- const statuses = extensionStatuses();
9848
- currentSnapshot = replaceSnapshot(currentSnapshot, generation2, {
9849
- ...currentSnapshot,
9850
- ...values,
9851
- ...context ? { context } : {},
9852
- ...statuses ? { extensionStatuses: statuses } : {}
9853
- });
9854
- statusLine?.update(currentSnapshot);
9855
- editor?.update(currentSnapshot);
9856
- startup?.update({
9857
- ...currentSnapshot,
9858
- reason: host.startupReason ?? "startup",
9859
- ...host.provider ? { startupProvider: host.provider } : {},
9860
- ...host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {},
9861
- preset: currentConfig.preset,
9862
- ...host.resources ? { resources: host.resources } : {}
9863
- });
10078
+ update(values, options = {}) {
10079
+ if (disposed) return false;
10080
+ const patch = {
10081
+ ...values
10082
+ };
10083
+ if (options.refreshContextUsage) {
10084
+ const context = contextSnapshot();
10085
+ patch.context = context;
10086
+ if (context) contextProvider.set("active", context);
10087
+ else contextProvider.clear();
10088
+ }
10089
+ if (options.refreshExtensionStatuses) patch.extensionStatuses = extensionStatuses();
10090
+ return updateSnapshot(withSnapshotPatch(patch));
9864
10091
  },
9865
10092
  configure(nextConfig) {
9866
10093
  if (disposed) return;
@@ -9895,17 +10122,7 @@ function createPiStyleRuntime(host, generation2, requestRender = () => {
9895
10122
  git.invalidate(host.cwd);
9896
10123
  void git.get(host.cwd).then((value) => {
9897
10124
  if (disposed) return;
9898
- currentSnapshot = replaceSnapshot(currentSnapshot, generation2, { ...currentSnapshot, git: value });
9899
- statusLine?.update(currentSnapshot);
9900
- editor?.update(currentSnapshot);
9901
- startup?.update({
9902
- ...currentSnapshot,
9903
- reason: host.startupReason ?? "startup",
9904
- ...host.provider ? { startupProvider: host.provider } : {},
9905
- ...host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {},
9906
- preset: currentConfig.preset
9907
- });
9908
- requestRender();
10125
+ if (updateSnapshot(withSnapshotPatch({ git: value }))) requestRender();
9909
10126
  });
9910
10127
  },
9911
10128
  get disposed() {
@@ -10109,10 +10326,10 @@ function createPiStyleApp(initialConfig, reloadPort, onConfigChange) {
10109
10326
  sessionShutdown() {
10110
10327
  runtime.stop();
10111
10328
  },
10112
- update(values, kind = "coalesced") {
10329
+ update(values, kind = "coalesced", options) {
10113
10330
  const active = runtime.current;
10114
10331
  if (!active) return;
10115
- active.update(values);
10332
+ if (!active.update(values, options)) return;
10116
10333
  active.scheduler.schedule(kind);
10117
10334
  },
10118
10335
  reload() {
@@ -10415,9 +10632,11 @@ function renderBashExecutionBox(instance, args) {
10415
10632
  try {
10416
10633
  if (host.piStyleStart === void 0) host.piStyleStart = Date.now();
10417
10634
  const renderedWidth = boxWidth(width);
10635
+ const cacheKey2 = `${themeCacheKey(theme)}|${width}|${host.status}|${host.exitCode ?? ""}|${host.command}`;
10636
+ if (host.status !== "running" && host.piStyleRenderCache?.key === cacheKey2) return host.piStyleRenderCache.lines;
10418
10637
  const inner = boxInnerWidth(renderedWidth);
10419
10638
  const wrapped = content.render(inner).map((line) => boxLine(theme, line.startsWith(" ") ? line.slice(1) : line, renderedWidth));
10420
- return [
10639
+ const lines = [
10421
10640
  "",
10422
10641
  boxLabeledBorder(theme, TOP_LEFT, TOP_RIGHT, bashBoxTitle(theme, host), void 0, renderedWidth),
10423
10642
  boxBlankLine(theme, renderedWidth),
@@ -10425,6 +10644,8 @@ function renderBashExecutionBox(instance, args) {
10425
10644
  boxBlankLine(theme, renderedWidth),
10426
10645
  boxLabeledBorder(theme, BOTTOM_LEFT, BOTTOM_RIGHT, bashBoxFooter(theme, host), void 0, renderedWidth)
10427
10646
  ];
10647
+ if (host.status !== "running") host.piStyleRenderCache = { key: cacheKey2, lines };
10648
+ return lines;
10428
10649
  } catch {
10429
10650
  return void 0;
10430
10651
  }
@@ -10455,17 +10676,27 @@ import {
10455
10676
  } from "@earendil-works/pi-coding-agent";
10456
10677
 
10457
10678
  // extension-src/pi-style/features/messages/index.ts
10458
- import { visibleWidth as visibleWidth4 } from "@earendil-works/pi-tui";
10459
10679
  var OSC133_ZONE_START = "\x1B]133;A\x07";
10460
10680
  var OSC133_ZONE_END = "\x1B]133;B\x07";
10461
10681
  var OSC133_ZONE_FINAL = "\x1B]133;C\x07";
10682
+ var BG_RESET = "\x1B[49m";
10683
+ var MAX_RENDER_CACHE_KEYS_PER_INSTANCE = 8;
10684
+ var MAX_LINE_ANALYSIS_ENTRIES = 4096;
10685
+ var renderCacheByInstance = /* @__PURE__ */ new WeakMap();
10686
+ var lineAnalysisCache = /* @__PURE__ */ new Map();
10687
+ var messageDecorationTestState = {
10688
+ decoratePasses: 0,
10689
+ cacheHits: 0,
10690
+ cacheMisses: 0,
10691
+ lineCacheHits: 0,
10692
+ lineCacheMisses: 0
10693
+ };
10462
10694
  function extractOscEnvelope(line) {
10463
10695
  if (!line.startsWith(OSC133_ZONE_START)) return void 0;
10464
10696
  const bodyEnd = line.indexOf(OSC133_ZONE_END, OSC133_ZONE_START.length);
10465
10697
  if (bodyEnd < 0 || !line.endsWith(OSC133_ZONE_FINAL)) return void 0;
10466
10698
  return { start: OSC133_ZONE_START, body: line.slice(OSC133_ZONE_START.length, bodyEnd), end: line.slice(bodyEnd) };
10467
10699
  }
10468
- var BG_RESET = "\x1B[49m";
10469
10700
  function splitLeadingMarkers(line) {
10470
10701
  let index = 0;
10471
10702
  while (line.startsWith("\x1B]", index)) {
@@ -10497,39 +10728,6 @@ function isBackgroundSgr(sequence) {
10497
10728
  }
10498
10729
  return false;
10499
10730
  }
10500
- function rebuildAtWidth(line, width, lead) {
10501
- const { head, rest } = splitLeadingMarkers(line);
10502
- const bgAnsi = leadingSgr(rest);
10503
- if (bgAnsi && isBackgroundSgr(bgAnsi) && rest.endsWith(BG_RESET)) {
10504
- const body = rest.slice(bgAnsi.length, rest.length - BG_RESET.length);
10505
- const pad = " ".repeat(Math.max(0, width - visibleWidth4(lead) - visibleWidth4(body)));
10506
- return `${head}${bgAnsi}${lead}${body}${pad}${BG_RESET}`;
10507
- }
10508
- const padded = `${lead}${line}`;
10509
- return `${padded}${" ".repeat(Math.max(0, width - visibleWidth4(padded)))}`;
10510
- }
10511
- function decorateMessageLine(line, index, lastIndex, contentIndex, width, options) {
10512
- const { firstEnvelope, firstHasStart, multilineEnvelope, prefix } = options;
10513
- const prefixWidth = visibleWidth4(prefix);
10514
- const lead = index === contentIndex ? prefix : index > contentIndex ? " ".repeat(prefixWidth) : "";
10515
- if (index === contentIndex && firstEnvelope)
10516
- return `${firstEnvelope.start}${rebuildAtWidth(firstEnvelope.body, width, prefix)}${firstEnvelope.end}`;
10517
- if (index === contentIndex && firstHasStart)
10518
- return `${OSC133_ZONE_START}${rebuildAtWidth(line.slice(OSC133_ZONE_START.length), width, prefix)}`;
10519
- if (index === lastIndex && multilineEnvelope && index !== contentIndex)
10520
- return `${OSC133_ZONE_END}${OSC133_ZONE_FINAL}${rebuildAtWidth(
10521
- line.slice((OSC133_ZONE_END + OSC133_ZONE_FINAL).length),
10522
- width,
10523
- lead
10524
- )}`;
10525
- if (index === contentIndex && index === lastIndex && multilineEnvelope && line.startsWith(OSC133_ZONE_END + OSC133_ZONE_FINAL))
10526
- return `${OSC133_ZONE_END}${OSC133_ZONE_FINAL}${rebuildAtWidth(
10527
- line.slice((OSC133_ZONE_END + OSC133_ZONE_FINAL).length),
10528
- width,
10529
- prefix
10530
- )}`;
10531
- return rebuildAtWidth(line, width, lead);
10532
- }
10533
10731
  function contentText(line) {
10534
10732
  let output = "";
10535
10733
  for (let index = 0; index < line.length; index++) {
@@ -10553,32 +10751,127 @@ function contentText(line) {
10553
10751
  function hasContent(line) {
10554
10752
  return [...contentText(line)].some((character) => !/\s/u.test(character));
10555
10753
  }
10754
+ function getLineAnalysis(line) {
10755
+ const cached = lineAnalysisCache.get(line);
10756
+ if (cached) {
10757
+ messageDecorationTestState.lineCacheHits++;
10758
+ return cached;
10759
+ }
10760
+ messageDecorationTestState.lineCacheMisses++;
10761
+ const leadingMarkers = splitLeadingMarkers(line);
10762
+ const backgroundAnsi = leadingSgr(leadingMarkers.rest);
10763
+ const isBackgroundWrapped = backgroundAnsi !== "" && isBackgroundSgr(backgroundAnsi) && leadingMarkers.rest.endsWith(BG_RESET);
10764
+ const backgroundBody = isBackgroundWrapped ? leadingMarkers.rest.slice(backgroundAnsi.length, leadingMarkers.rest.length - BG_RESET.length) : void 0;
10765
+ const analysis = {
10766
+ visibleWidth: visibleWidth(line),
10767
+ hasContent: hasContent(line),
10768
+ oscEnvelope: extractOscEnvelope(line),
10769
+ hasOscStart: line.startsWith(OSC133_ZONE_START),
10770
+ leadingMarkers,
10771
+ isBackgroundWrapped,
10772
+ backgroundAnsi,
10773
+ backgroundBody,
10774
+ backgroundBodyWidth: backgroundBody === void 0 ? void 0 : visibleWidth(backgroundBody)
10775
+ };
10776
+ lineAnalysisCache.set(line, analysis);
10777
+ if (lineAnalysisCache.size > MAX_LINE_ANALYSIS_ENTRIES) {
10778
+ const oldestKey = lineAnalysisCache.keys().next().value;
10779
+ if (oldestKey !== void 0) lineAnalysisCache.delete(oldestKey);
10780
+ }
10781
+ return analysis;
10782
+ }
10783
+ function rebuildAtWidth(line, width, lead, leadWidth, analysis = getLineAnalysis(line)) {
10784
+ if (analysis.isBackgroundWrapped && analysis.backgroundBody !== void 0 && analysis.backgroundBodyWidth !== void 0) {
10785
+ const pad2 = " ".repeat(Math.max(0, width - leadWidth - analysis.backgroundBodyWidth));
10786
+ return `${analysis.leadingMarkers.head}${analysis.backgroundAnsi}${lead}${analysis.backgroundBody}${pad2}${BG_RESET}`;
10787
+ }
10788
+ const pad = " ".repeat(Math.max(0, width - leadWidth - analysis.visibleWidth));
10789
+ return `${lead}${line}${pad}`;
10790
+ }
10791
+ function decorateMessageLine(line, index, lastIndex, contentIndex, width, options, analysis = getLineAnalysis(line)) {
10792
+ const { firstEnvelope, firstHasStart, multilineEnvelope, prefix, prefixWidth } = options;
10793
+ const lead = index === contentIndex ? prefix : index > contentIndex ? " ".repeat(prefixWidth) : "";
10794
+ const leadWidth = index < contentIndex ? 0 : prefixWidth;
10795
+ if (index === contentIndex && firstEnvelope)
10796
+ return `${firstEnvelope.start}${rebuildAtWidth(firstEnvelope.body, width, prefix, prefixWidth)}${firstEnvelope.end}`;
10797
+ if (index === contentIndex && firstHasStart)
10798
+ return `${OSC133_ZONE_START}${rebuildAtWidth(line.slice(OSC133_ZONE_START.length), width, prefix, prefixWidth)}`;
10799
+ if (index === lastIndex && multilineEnvelope && index !== contentIndex)
10800
+ return `${OSC133_ZONE_END}${OSC133_ZONE_FINAL}${rebuildAtWidth(
10801
+ line.slice((OSC133_ZONE_END + OSC133_ZONE_FINAL).length),
10802
+ width,
10803
+ lead,
10804
+ leadWidth
10805
+ )}`;
10806
+ if (index === contentIndex && index === lastIndex && multilineEnvelope && line.startsWith(OSC133_ZONE_END + OSC133_ZONE_FINAL))
10807
+ return `${OSC133_ZONE_END}${OSC133_ZONE_FINAL}${rebuildAtWidth(
10808
+ line.slice((OSC133_ZONE_END + OSC133_ZONE_FINAL).length),
10809
+ width,
10810
+ prefix,
10811
+ prefixWidth
10812
+ )}`;
10813
+ return rebuildAtWidth(line, width, lead, leadWidth, analysis);
10814
+ }
10815
+ function sameLines(left, right) {
10816
+ if (left === right) return true;
10817
+ if (left.length !== right.length) return false;
10818
+ for (let index = 0; index < left.length; index++) {
10819
+ if (left[index] !== right[index]) return false;
10820
+ }
10821
+ return true;
10822
+ }
10823
+ function cacheKey(width, prefix) {
10824
+ return `${width}\0${prefix}`;
10825
+ }
10826
+ function getRenderCache(instance) {
10827
+ let cache = renderCacheByInstance.get(instance);
10828
+ if (!cache) {
10829
+ cache = /* @__PURE__ */ new Map();
10830
+ renderCacheByInstance.set(instance, cache);
10831
+ }
10832
+ return cache;
10833
+ }
10834
+ function storeRenderCache(instance, width, prefix, native, result) {
10835
+ const cache = getRenderCache(instance);
10836
+ const key = cacheKey(width, prefix);
10837
+ if (cache.has(key)) cache.delete(key);
10838
+ cache.set(key, { nativeRef: native, nativeLines: [...native], result: [...result] });
10839
+ while (cache.size > MAX_RENDER_CACHE_KEYS_PER_INSTANCE) {
10840
+ const oldestKey = cache.keys().next().value;
10841
+ if (oldestKey === void 0) break;
10842
+ cache.delete(oldestKey);
10843
+ }
10844
+ }
10556
10845
  function prefixNative(lines, width, prefix) {
10557
10846
  if (!Array.isArray(lines) || lines.length === 0 || !lines.every((line) => typeof line === "string")) return void 0;
10847
+ messageDecorationTestState.decoratePasses++;
10558
10848
  const nativeLines = lines;
10559
- const prefixWidth = visibleWidth4(prefix);
10849
+ const prefixWidth = visibleWidth(prefix);
10560
10850
  if (width <= prefixWidth) return void 0;
10561
10851
  const bodyWidth = width - prefixWidth;
10562
10852
  const first = nativeLines[0] ?? "";
10563
10853
  const last = nativeLines.at(-1) ?? "";
10854
+ const lastAnalysis = getLineAnalysis(last);
10564
10855
  const multilineEnvelope = nativeLines.length > 1 && last.startsWith(OSC133_ZONE_END + OSC133_ZONE_FINAL);
10565
10856
  const firstContentIndex = nativeLines.findIndex((line, index) => {
10566
- if (index !== nativeLines.length - 1 || !multilineEnvelope) return hasContent(line);
10567
- return !nativeLines.slice(0, index).some((earlier) => hasContent(earlier)) && hasContent(line);
10857
+ if (index !== nativeLines.length - 1 || !multilineEnvelope) return getLineAnalysis(line).hasContent;
10858
+ return !nativeLines.slice(0, index).some((earlier) => getLineAnalysis(earlier).hasContent) && lastAnalysis.hasContent;
10568
10859
  });
10569
10860
  if (firstContentIndex < 0) return nativeLines;
10570
- const firstEnvelope = firstContentIndex === 0 ? extractOscEnvelope(first) : void 0;
10571
- const firstHasStart = firstContentIndex === 0 && first.startsWith(OSC133_ZONE_START);
10861
+ const firstAnalysis = getLineAnalysis(first);
10862
+ const firstEnvelope = firstContentIndex === 0 ? firstAnalysis.oscEnvelope : void 0;
10863
+ const firstHasStart = firstContentIndex === 0 && firstAnalysis.hasOscStart;
10572
10864
  const decorated = nativeLines.map(
10573
10865
  (line, index) => decorateMessageLine(line, index, nativeLines.length - 1, firstContentIndex, width, {
10574
10866
  firstEnvelope,
10575
10867
  firstHasStart,
10576
10868
  multilineEnvelope,
10577
- prefix
10869
+ prefix,
10870
+ prefixWidth
10578
10871
  })
10579
10872
  );
10580
- if (!decorated.every((line) => visibleWidth4(line) <= width)) return void 0;
10581
- if (!nativeLines.every((line) => visibleWidth4(line) <= bodyWidth)) return void 0;
10873
+ if (!decorated.every((line) => visibleWidth(line) <= width)) return void 0;
10874
+ if (!nativeLines.every((line) => getLineAnalysis(line).visibleWidth <= bodyWidth)) return void 0;
10582
10875
  return decorated;
10583
10876
  }
10584
10877
  function decorateMessageRender(original, instance, args, snapshot = {
@@ -10591,10 +10884,20 @@ function decorateMessageRender(original, instance, args, snapshot = {
10591
10884
  const width = typeof args[0] === "number" ? args[0] : 0;
10592
10885
  const prefix = snapshot.assistantPrefix;
10593
10886
  if (!snapshot.assistantEnabled) return Reflect.apply(original, instance, args);
10594
- if (width <= visibleWidth4(prefix)) return Reflect.apply(original, instance, args);
10595
- const reducedWidth = width - visibleWidth4(prefix);
10887
+ const prefixWidth = visibleWidth(prefix);
10888
+ if (width <= prefixWidth) return Reflect.apply(original, instance, args);
10889
+ const reducedWidth = width - prefixWidth;
10596
10890
  const native = Reflect.apply(original, instance, [reducedWidth, ...args.slice(1)]);
10597
- return prefixNative(native, width, prefix) ?? native;
10891
+ if (!Array.isArray(native) || !native.every((line) => typeof line === "string")) return native;
10892
+ const cached = getRenderCache(instance).get(cacheKey(width, prefix));
10893
+ if (cached && (cached.nativeRef === native || sameLines(cached.nativeLines, native))) {
10894
+ messageDecorationTestState.cacheHits++;
10895
+ return [...cached.result];
10896
+ }
10897
+ messageDecorationTestState.cacheMisses++;
10898
+ const decorated = prefixNative(native, width, prefix) ?? native;
10899
+ storeRenderCache(instance, width, prefix, native, decorated);
10900
+ return decorated;
10598
10901
  }
10599
10902
  function isSpacerChild(child) {
10600
10903
  return typeof child?.setLines === "function";
@@ -10954,7 +11257,7 @@ var KNOWN_NATIVE_IDENTITIES = Object.freeze({
10954
11257
  });
10955
11258
  var TRUSTED_NATIVE_FINGERPRINTS = Object.freeze(
10956
11259
  Object.fromEntries(
10957
- Object.entries(KNOWN_NATIVE_IDENTITIES).map(([key, identities]) => [key, identities[0].fingerprint])
11260
+ Object.entries(KNOWN_NATIVE_IDENTITIES).map(([key, identities]) => [key, identities[0]?.fingerprint ?? ""])
10958
11261
  )
10959
11262
  );
10960
11263
  function fingerprint(value) {
@@ -11810,22 +12113,134 @@ function createPiStyleSessionCoordinator(pi, hooks = {}) {
11810
12113
  }
11811
12114
 
11812
12115
  // extension-src/pi-style/pi/session-usage.ts
12116
+ var usageCache = /* @__PURE__ */ new WeakMap();
11813
12117
  function usageFromSession(session) {
11814
- const totals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
11815
- let sawUsage = false;
11816
- for (const entry of session.getEntries()) {
11817
- if (entry.type === "message") {
11818
- if (entry.message.role !== "assistant" && entry.message.role !== "toolResult") continue;
11819
- const usage = "usage" in entry.message ? entry.message.usage : void 0;
11820
- if (!usage) continue;
11821
- addUsage(totals, usage);
11822
- sawUsage = true;
11823
- } else if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) {
11824
- addUsage(totals, entry.usage);
11825
- sawUsage = true;
11826
- }
11827
- }
11828
- if (!sawUsage) return void 0;
12118
+ const entries = session.getEntries();
12119
+ const state = usageCache.get(session) ?? createState();
12120
+ usageCache.set(session, state);
12121
+ if (entries.length === 0) {
12122
+ state.cacheHits++;
12123
+ state.processedLength = 0;
12124
+ state.lastEntryId = void 0;
12125
+ state.lastEntryRef = void 0;
12126
+ state.lastContribution = emptyContribution();
12127
+ state.totals = emptyTotals();
12128
+ state.cached = void 0;
12129
+ return void 0;
12130
+ }
12131
+ if (state.processedLength === 0) {
12132
+ rebuildFrom(entries, state);
12133
+ return state.cached;
12134
+ }
12135
+ if (entries.length < state.processedLength) {
12136
+ rebuildFrom(entries, state);
12137
+ return state.cached;
12138
+ }
12139
+ const currentLast = entries.at(-1);
12140
+ if (!currentLast) {
12141
+ state.cacheHits++;
12142
+ state.cached = void 0;
12143
+ return void 0;
12144
+ }
12145
+ if (entries.length === state.processedLength) {
12146
+ if (currentLast.id !== state.lastEntryId) {
12147
+ rebuildFrom(entries, state);
12148
+ return state.cached;
12149
+ }
12150
+ if (currentLast !== state.lastEntryRef) refreshTailEntry(currentLast, state);
12151
+ else state.cacheHits++;
12152
+ return state.cached;
12153
+ }
12154
+ const previousLast = entries[state.processedLength - 1];
12155
+ if (!previousLast || previousLast.id !== state.lastEntryId) {
12156
+ rebuildFrom(entries, state);
12157
+ return state.cached;
12158
+ }
12159
+ if (previousLast !== state.lastEntryRef) refreshTailEntry(previousLast, state);
12160
+ for (const entry of entries.slice(state.processedLength)) appendEntry(entry, state);
12161
+ return state.cached;
12162
+ }
12163
+ function resetUsageFromSessionCache(session) {
12164
+ if (session) {
12165
+ usageCache.delete(session);
12166
+ return;
12167
+ }
12168
+ usageCache = /* @__PURE__ */ new WeakMap();
12169
+ }
12170
+ function createState() {
12171
+ return {
12172
+ processedLength: 0,
12173
+ lastEntryId: void 0,
12174
+ lastEntryRef: void 0,
12175
+ lastContribution: emptyContribution(),
12176
+ totals: emptyTotals(),
12177
+ cached: void 0,
12178
+ scannedEntries: 0,
12179
+ cacheHits: 0,
12180
+ rebuilds: 0,
12181
+ tailRefreshes: 0
12182
+ };
12183
+ }
12184
+ function rebuildFrom(entries, state) {
12185
+ state.rebuilds++;
12186
+ state.processedLength = 0;
12187
+ state.lastEntryId = void 0;
12188
+ state.lastEntryRef = void 0;
12189
+ state.lastContribution = emptyContribution();
12190
+ state.totals = emptyTotals();
12191
+ state.cached = void 0;
12192
+ for (const entry of entries) appendEntry(entry, state);
12193
+ }
12194
+ function refreshTailEntry(entry, state) {
12195
+ state.tailRefreshes++;
12196
+ subtractContribution(state.totals, state.lastContribution);
12197
+ const contribution = usageContribution(entry);
12198
+ addContribution(state.totals, contribution);
12199
+ state.lastEntryId = entry.id;
12200
+ state.lastEntryRef = entry;
12201
+ state.lastContribution = contribution;
12202
+ state.cached = snapshotFromTotals(state.totals);
12203
+ state.scannedEntries++;
12204
+ }
12205
+ function appendEntry(entry, state) {
12206
+ const contribution = usageContribution(entry);
12207
+ addContribution(state.totals, contribution);
12208
+ state.processedLength++;
12209
+ state.lastEntryId = entry.id;
12210
+ state.lastEntryRef = entry;
12211
+ state.lastContribution = contribution;
12212
+ state.cached = snapshotFromTotals(state.totals);
12213
+ state.scannedEntries++;
12214
+ }
12215
+ function usageContribution(entry) {
12216
+ if (entry.type === "message") {
12217
+ if (entry.message.role !== "assistant" && entry.message.role !== "toolResult") return emptyContribution();
12218
+ const usage = "usage" in entry.message ? entry.message.usage : void 0;
12219
+ if (!usage) return emptyContribution();
12220
+ return {
12221
+ input: usage.input,
12222
+ output: usage.output,
12223
+ cacheRead: usage.cacheRead,
12224
+ cacheWrite: usage.cacheWrite,
12225
+ cost: usage.cost.total,
12226
+ sawUsage: true
12227
+ };
12228
+ }
12229
+ if ((entry.type === "branch_summary" || entry.type === "compaction") && entry.usage) {
12230
+ return {
12231
+ input: entry.usage.input,
12232
+ output: entry.usage.output,
12233
+ cacheRead: entry.usage.cacheRead,
12234
+ cacheWrite: entry.usage.cacheWrite,
12235
+ cost: entry.usage.cost.total,
12236
+ sawUsage: true
12237
+ };
12238
+ }
12239
+ return emptyContribution();
12240
+ }
12241
+ function snapshotFromTotals(totals) {
12242
+ if (totals.input === 0 && totals.output === 0 && totals.cacheRead === 0 && totals.cacheWrite === 0 && totals.cost === 0)
12243
+ return void 0;
11829
12244
  return {
11830
12245
  inputTokens: totals.input,
11831
12246
  outputTokens: totals.output,
@@ -11836,18 +12251,30 @@ function usageFromSession(session) {
11836
12251
  streaming: false
11837
12252
  };
11838
12253
  }
11839
- function addUsage(totals, usage) {
12254
+ function addContribution(totals, usage) {
11840
12255
  totals.input += usage.input;
11841
12256
  totals.output += usage.output;
11842
12257
  totals.cacheRead += usage.cacheRead;
11843
12258
  totals.cacheWrite += usage.cacheWrite;
11844
- totals.cost += usage.cost.total;
12259
+ totals.cost += usage.cost;
12260
+ }
12261
+ function subtractContribution(totals, usage) {
12262
+ totals.input -= usage.input;
12263
+ totals.output -= usage.output;
12264
+ totals.cacheRead -= usage.cacheRead;
12265
+ totals.cacheWrite -= usage.cacheWrite;
12266
+ totals.cost -= usage.cost;
12267
+ }
12268
+ function emptyTotals() {
12269
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
12270
+ }
12271
+ function emptyContribution() {
12272
+ return { ...emptyTotals(), sawUsage: false };
11845
12273
  }
11846
12274
 
11847
12275
  // extension-src/pi-style/pi/index.ts
11848
12276
  function usagePatch(ctx) {
11849
- const usage = usageFromSession(ctx.sessionManager);
11850
- return usage ? { usage } : {};
12277
+ return { usage: usageFromSession(ctx.sessionManager) };
11851
12278
  }
11852
12279
  function activateReadOnlyTools(pi) {
11853
12280
  const available = new Set(pi.getAllTools().map((tool) => tool.name));
@@ -11882,6 +12309,7 @@ function piStyleExtension(pi) {
11882
12309
  const coordinator = createPiStyleSessionCoordinator(pi, compatibilityTestHooks);
11883
12310
  registerPiStyleCommand(pi, coordinator.app);
11884
12311
  pi.on("session_start", async (event, ctx) => {
12312
+ resetUsageFromSessionCache(ctx.sessionManager);
11885
12313
  if (pi.getFlag("pi-style-readonly-tools") === true) {
11886
12314
  activateReadOnlyTools(pi);
11887
12315
  }
@@ -11915,15 +12343,21 @@ function piStyleExtension(pi) {
11915
12343
  )
11916
12344
  );
11917
12345
  pi.on("thinking_level_select", (event) => coordinator.app.update({ thinkingLevel: event.level }, "immediate"));
11918
- pi.on("session_info_changed", (event) => coordinator.app.update({ sessionName: event.name }, "coalesced"));
12346
+ pi.on(
12347
+ "session_info_changed",
12348
+ (event) => coordinator.app.update({ sessionName: event.name }, "coalesced", { refreshExtensionStatuses: true })
12349
+ );
11919
12350
  pi.on("message_start", () => {
11920
12351
  closeActiveBatch();
11921
12352
  });
11922
12353
  pi.on("message_update", () => coordinator.app.update({}, "coalesced"));
11923
- pi.on("message_end", (_event, ctx) => coordinator.app.update({ ...usagePatch(ctx) }, "coalesced"));
12354
+ pi.on(
12355
+ "message_end",
12356
+ (_event, ctx) => coordinator.app.update({ ...usagePatch(ctx) }, "coalesced", { refreshContextUsage: true })
12357
+ );
11924
12358
  pi.on("turn_end", (event, ctx) => {
11925
12359
  registerTurnFromMessage(event.message, event.toolResults);
11926
- coordinator.app.update({ ...usagePatch(ctx) }, "deferred");
12360
+ coordinator.app.update({ ...usagePatch(ctx) }, "deferred", { refreshContextUsage: true });
11927
12361
  });
11928
12362
  pi.on("agent_end", () => {
11929
12363
  const run = finishAgentRun();
@@ -11932,23 +12366,33 @@ function piStyleExtension(pi) {
11932
12366
  requestToolPresentationRender();
11933
12367
  }
11934
12368
  });
11935
- pi.on("agent_settled", (_event, ctx) => coordinator.app.update({ ...usagePatch(ctx) }, "coalesced"));
12369
+ pi.on(
12370
+ "agent_settled",
12371
+ (_event, ctx) => coordinator.app.update({ ...usagePatch(ctx) }, "coalesced", { refreshContextUsage: true })
12372
+ );
11936
12373
  pi.on("session_tree", (_event, ctx) => {
12374
+ resetUsageFromSessionCache(ctx.sessionManager);
11937
12375
  rebuildTurnRegistryFromEntries(ctx.sessionManager.getEntries());
11938
- coordinator.app.update({ ...usagePatch(ctx) }, "deferred");
12376
+ coordinator.app.update({ ...usagePatch(ctx) }, "deferred", { refreshContextUsage: true });
12377
+ });
12378
+ pi.on("session_compact", (_event, ctx) => {
12379
+ resetUsageFromSessionCache(ctx.sessionManager);
12380
+ coordinator.app.update({ ...usagePatch(ctx) }, "deferred", { refreshContextUsage: true });
11939
12381
  });
11940
- pi.on("session_compact", (_event, ctx) => coordinator.app.update({ ...usagePatch(ctx) }, "deferred"));
11941
12382
  pi.on("tool_result", (event, ctx) => {
11942
12383
  if (["write", "edit", "bash"].includes(event.toolName)) {
11943
12384
  coordinator.app.runtime.current?.invalidateGit();
11944
- coordinator.app.update({ ...usagePatch(ctx) }, "delayed-retry");
12385
+ coordinator.app.update({ ...usagePatch(ctx) }, "delayed-retry", { refreshContextUsage: true });
11945
12386
  }
11946
12387
  });
11947
12388
  pi.on("user_bash", (_event, ctx) => {
11948
12389
  coordinator.app.runtime.current?.invalidateGit();
11949
- coordinator.app.update({ ...usagePatch(ctx) }, "delayed-retry");
12390
+ coordinator.app.update({ ...usagePatch(ctx) }, "delayed-retry", { refreshContextUsage: true });
12391
+ });
12392
+ pi.on("session_shutdown", (_event, ctx) => {
12393
+ resetUsageFromSessionCache(ctx.sessionManager);
12394
+ coordinator.shutdown();
11950
12395
  });
11951
- pi.on("session_shutdown", () => coordinator.shutdown());
11952
12396
  }
11953
12397
  export {
11954
12398
  __setCompatibilityTestHooks,