@quandev104/pi-style 0.2.0 → 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.
@@ -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) {
@@ -1445,6 +1454,18 @@ function setToolsRenderConfig(config) {
1445
1454
  function getToolsRenderConfig() {
1446
1455
  return sessionToolsConfig;
1447
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
+ }
1448
1469
  var STARTED_AT_KEY = "__piStyleStartedAt";
1449
1470
  var ENDED_AT_KEY = "__piStyleEndedAt";
1450
1471
  var RESULT_SEEN_KEY = "__piStyleResultSeen";
@@ -1472,27 +1493,35 @@ function markResultSeen(state) {
1472
1493
  if (!state || typeof state !== "object") return;
1473
1494
  state[RESULT_SEEN_KEY] = true;
1474
1495
  }
1475
- 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
+ }
1476
1509
  function startElapsedTicker(state, invalidate) {
1477
1510
  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);
1511
+ state[TICKER_KEY] = true;
1512
+ tickerEntries.set(state, { invalidate });
1513
+ ensureSharedTicker();
1481
1514
  }
1482
1515
  function stopElapsedTicker(state) {
1483
1516
  if (!state || typeof state !== "object") return;
1484
- const handle = state[TICKER_KEY];
1485
- if (handle !== void 0) clearInterval(handle);
1486
1517
  delete state[TICKER_KEY];
1487
- tickerStates.delete(state);
1518
+ tickerEntries.delete(state);
1519
+ stopSharedTickerIfIdle();
1488
1520
  }
1489
1521
  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();
1522
+ for (const state of tickerEntries.keys()) delete state[TICKER_KEY];
1523
+ tickerEntries.clear();
1524
+ stopSharedTickerIfIdle();
1496
1525
  }
1497
1526
 
1498
1527
  // extension-src/pi-style/features/tools/boxed/batch.ts
@@ -1528,6 +1557,7 @@ function createBatch(meta, leaderId, detail, opts = {}) {
1528
1557
  leaderId,
1529
1558
  startedAt: performance.now(),
1530
1559
  closed: false,
1560
+ revision: 0,
1531
1561
  members: [
1532
1562
  {
1533
1563
  toolCallId: leaderId,
@@ -1543,14 +1573,20 @@ function createBatch(meta, leaderId, detail, opts = {}) {
1543
1573
  batchByCallId.set(leaderId, batch);
1544
1574
  return batch;
1545
1575
  }
1576
+ function bumpBatchRevision(batch) {
1577
+ batch.revision++;
1578
+ delete batch.renderCache;
1579
+ }
1546
1580
  function registerBatchCall(meta, detail, context, opts = {}) {
1547
1581
  const existing = batchByCallId.get(context.toolCallId);
1548
1582
  if (existing) {
1549
1583
  const member2 = existing.members.find((entry) => entry.toolCallId === context.toolCallId);
1550
1584
  if (member2) {
1585
+ const changed = member2.detail !== detail || member2.pattern !== opts.pattern || member2.pathLabel !== opts.pathLabel;
1551
1586
  member2.detail = detail;
1552
1587
  if (opts.pattern !== void 0) member2.pattern = opts.pattern;
1553
1588
  if (opts.pathLabel !== void 0) member2.pathLabel = opts.pathLabel;
1589
+ if (changed) bumpBatchRevision(existing);
1554
1590
  }
1555
1591
  return { batch: existing, isLeader: existing.leaderId === context.toolCallId };
1556
1592
  }
@@ -1569,6 +1605,7 @@ function registerBatchCall(meta, detail, context, opts = {}) {
1569
1605
  };
1570
1606
  current.members.push(member);
1571
1607
  batchByCallId.set(context.toolCallId, current);
1608
+ bumpBatchRevision(current);
1572
1609
  return { batch: current, isLeader: false };
1573
1610
  }
1574
1611
  function registerBatchResult(meta, data, context) {
@@ -1576,14 +1613,19 @@ function registerBatchResult(meta, data, context) {
1576
1613
  if (!batch || batch.meta.toolName !== meta.toolName) return { batch: void 0, isLeader: false };
1577
1614
  const member = batch.members.find((entry) => entry.toolCallId === context.toolCallId);
1578
1615
  if (member) {
1579
- member.status = data.isPartial ? "running" : "done";
1580
- 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;
1581
1621
  if (member.isError && data.errorText !== void 0) member.errorText = data.errorText;
1582
1622
  else delete member.errorText;
1583
1623
  if (data.entries !== void 0) member.outputEntries = data.entries;
1624
+ if (changed) bumpBatchRevision(batch);
1584
1625
  }
1585
1626
  if (batch.completedAt === void 0 && batch.members.every((entry) => entry.status === "done")) {
1586
1627
  batch.completedAt = performance.now();
1628
+ bumpBatchRevision(batch);
1587
1629
  }
1588
1630
  return { batch, isLeader: batch.leaderId === context.toolCallId };
1589
1631
  }
@@ -1776,9 +1818,16 @@ function renderBatchPanelLines(theme, batch, status, width) {
1776
1818
  function renderBatchAwareCall(theme, batch) {
1777
1819
  return {
1778
1820
  invalidate() {
1821
+ delete batch.renderCache;
1779
1822
  },
1780
1823
  render(width) {
1781
- 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;
1782
1831
  }
1783
1832
  };
1784
1833
  }
@@ -5003,6 +5052,17 @@ function compactFooterWithState(theme, result, context, options = {}) {
5003
5052
  function resultFooterLines(theme, result, context, extraParts = []) {
5004
5053
  return [formatBoxedFooter(theme, result, extraParts, stateElapsedMs(context))];
5005
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
+ }
5006
5066
  function clearFooterState(context) {
5007
5067
  clearCompactBoxedFooter(context.state);
5008
5068
  }
@@ -5322,11 +5382,11 @@ var EMPTY_BASH_RESULT = Object.freeze({
5322
5382
  }
5323
5383
  });
5324
5384
  function createBashResultPreview(theme, text, options, color) {
5325
- let cacheKey = "";
5385
+ let cacheKey2 = "";
5326
5386
  let cacheLines = null;
5327
5387
  return {
5328
5388
  invalidate() {
5329
- cacheKey = "";
5389
+ cacheKey2 = "";
5330
5390
  cacheLines = null;
5331
5391
  },
5332
5392
  render(width) {
@@ -5334,7 +5394,7 @@ function createBashResultPreview(theme, text, options, color) {
5334
5394
  const cfg = getToolsRenderConfig();
5335
5395
  const expanded = Boolean(options.expanded);
5336
5396
  const cacheId = `${bodyWidth}|${expanded ? 1 : 0}|${cfg.maxExpandedLines}|${cfg.dimOutput ? 1 : 0}`;
5337
- if (cacheLines && cacheKey === cacheId) return cacheLines;
5397
+ if (cacheLines && cacheKey2 === cacheId) return cacheLines;
5338
5398
  if (!expanded) {
5339
5399
  const needed = cfg.maxCollapsedLines;
5340
5400
  let totalNewlines = 0;
@@ -5349,14 +5409,14 @@ function createBashResultPreview(theme, text, options, color) {
5349
5409
  }
5350
5410
  }
5351
5411
  if (text.length === 0) {
5352
- cacheKey = cacheId;
5412
+ cacheKey2 = cacheId;
5353
5413
  cacheLines = [];
5354
5414
  return cacheLines;
5355
5415
  }
5356
5416
  const tail = replaceTabs(text.slice(scanFrom)).replace(/\r/g, "");
5357
5417
  const shownLines = tail ? tail.split("\n").map((l) => clampLineLength(l)) : [];
5358
5418
  if (shownLines.length === 0) {
5359
- cacheKey = cacheId;
5419
+ cacheKey2 = cacheId;
5360
5420
  cacheLines = [];
5361
5421
  return cacheLines;
5362
5422
  }
@@ -5365,7 +5425,7 @@ function createBashResultPreview(theme, text, options, color) {
5365
5425
  if (color === "error") return formatToolOutputLine(theme, truncated, "error");
5366
5426
  return cfg.dimOutput ? formatToolOutputLine(theme, truncated) : formatToolOutputLine(theme, truncated, "text");
5367
5427
  });
5368
- cacheKey = cacheId;
5428
+ cacheKey2 = cacheId;
5369
5429
  cacheLines = truncatedShown;
5370
5430
  return cacheLines;
5371
5431
  }
@@ -5373,7 +5433,7 @@ function createBashResultPreview(theme, text, options, color) {
5373
5433
  const logicalLines = normalized.split("\n").map((l) => clampLineLength(l));
5374
5434
  const hasOutput = !(logicalLines.length === 1 && logicalLines[0] === "");
5375
5435
  if (!hasOutput) {
5376
- cacheKey = cacheId;
5436
+ cacheKey2 = cacheId;
5377
5437
  cacheLines = [];
5378
5438
  return cacheLines;
5379
5439
  }
@@ -5384,11 +5444,11 @@ function createBashResultPreview(theme, text, options, color) {
5384
5444
  const truncated = expandedLines.slice(-cfg.maxExpandedLines).map(applyColor);
5385
5445
  const remaining = expandedLines.length - cfg.maxExpandedLines;
5386
5446
  truncated.unshift(theme.fg("dim", `\u2026 ${remaining} earlier lines`));
5387
- cacheKey = cacheId;
5447
+ cacheKey2 = cacheId;
5388
5448
  cacheLines = truncated;
5389
5449
  return cacheLines;
5390
5450
  }
5391
- cacheKey = cacheId;
5451
+ cacheKey2 = cacheId;
5392
5452
  cacheLines = expandedLines.map(applyColor);
5393
5453
  return cacheLines;
5394
5454
  }
@@ -5602,27 +5662,35 @@ function renderSemanticPanel(theme, toolCallId, context) {
5602
5662
  },
5603
5663
  render(width) {
5604
5664
  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);
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;
5626
5694
  }
5627
5695
  };
5628
5696
  }
@@ -5631,7 +5699,26 @@ var bashTool = {
5631
5699
  noteExecutionStart(context);
5632
5700
  const cls = classifyBashSemantic(String(args?.command ?? ""));
5633
5701
  if (cls) {
5634
- 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
+ }
5635
5722
  return renderSemanticPanel(theme, context.toolCallId, context);
5636
5723
  }
5637
5724
  noteBoxedCallState(context);
@@ -5652,12 +5739,18 @@ var bashTool = {
5652
5739
  const parsed = parseSemanticOutput(cls, output);
5653
5740
  const state = semanticStates.get(context.toolCallId);
5654
5741
  if (parsed) {
5655
- if (state) state.parsed = parsed;
5656
- else
5742
+ if (state) {
5743
+ state.parsed = parsed;
5744
+ state.finished = !options.isPartial;
5745
+ state.revision++;
5746
+ delete state.renderCache;
5747
+ } else
5657
5748
  semanticStates.set(context.toolCallId, {
5658
5749
  cls,
5659
5750
  command: String(context?.args?.command ?? ""),
5660
- parsed
5751
+ parsed,
5752
+ finished: !options.isPartial,
5753
+ revision: 0
5661
5754
  });
5662
5755
  if (isGitDiffClass(cls)) {
5663
5756
  return renderGitDiffResult(theme, parsed, options, context);
@@ -5667,7 +5760,12 @@ var bashTool = {
5667
5760
  }
5668
5761
  return EMPTY_BASH_TREE_RESULT;
5669
5762
  }
5670
- 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
+ }
5671
5769
  }
5672
5770
  } else if (options.isPartial) {
5673
5771
  startElapsedTicker(context.state, context.invalidate);
@@ -5680,7 +5778,20 @@ var bashTool = {
5680
5778
  if (firstResultPass) return EMPTY_BASH_RESULT;
5681
5779
  return renderBashStreamingResult(theme, raw, options, context);
5682
5780
  }
5683
- 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
+ );
5684
5795
  }
5685
5796
  };
5686
5797
 
@@ -5760,21 +5871,33 @@ var editTool = {
5760
5871
  const maxRows = expanded ? 160 : 36;
5761
5872
  const diffView = new AdaptiveDiffComponent(theme, rows, maxRows, shouldHighlight ? language : void 0);
5762
5873
  const expandHint2 = !expanded && diffView.hasCollapsed() ? "Ctrl+O more" : void 0;
5763
- return renderBoxedToolResult(
5764
- theme,
5765
- {
5766
- render(width) {
5767
- 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
+ }
5768
5894
  },
5769
- invalidate() {
5770
- diffView.invalidate();
5895
+ {
5896
+ dividerLabel: diffDividerLabel2(theme, stats),
5897
+ ...expandHint2 ? { dividerRightLabel: expandHint2 } : {},
5898
+ footerLines: [editDiffFooter(theme, result, context, stats)]
5771
5899
  }
5772
- },
5773
- {
5774
- dividerLabel: diffDividerLabel2(theme, stats),
5775
- ...expandHint2 ? { dividerRightLabel: expandHint2 } : {},
5776
- footerLines: [editDiffFooter(theme, result, context, stats)]
5777
- }
5900
+ )
5778
5901
  );
5779
5902
  }
5780
5903
  };
@@ -6171,21 +6294,34 @@ function renderQuickEditResult(_toolName, result, options, theme, context, confi
6171
6294
  const maxRows = expanded ? 160 : 36;
6172
6295
  const diffView = new AdaptiveDiffComponent(theme, rows, maxRows, shouldHighlight ? language : void 0);
6173
6296
  const expandHint2 = !expanded && diffView.hasCollapsed() ? "Ctrl+O more" : void 0;
6174
- return renderBoxedToolResult(
6175
- theme,
6176
- {
6177
- render(width) {
6178
- 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
+ }
6179
6318
  },
6180
- invalidate() {
6181
- diffView.invalidate();
6319
+ {
6320
+ dividerLabel: quickEditDividerLabel(theme, stats),
6321
+ ...expandHint2 ? { dividerRightLabel: expandHint2 } : {},
6322
+ footerLines: [quickEditDiffFooter(theme, result, context, stats)]
6182
6323
  }
6183
- },
6184
- {
6185
- dividerLabel: quickEditDividerLabel(theme, stats),
6186
- ...expandHint2 ? { dividerRightLabel: expandHint2 } : {},
6187
- footerLines: [quickEditDiffFooter(theme, result, context, stats)]
6188
- }
6324
+ )
6189
6325
  );
6190
6326
  }
6191
6327
  function quickEditFooter(theme, context) {
@@ -8254,6 +8390,7 @@ var StyledEditor = class extends CustomEditor {
8254
8390
  onSnapshot;
8255
8391
  semantic;
8256
8392
  disposed = false;
8393
+ renderPlanCache;
8257
8394
  constructor(tui, theme, keybindings, options) {
8258
8395
  super(tui, theme, keybindings);
8259
8396
  this.config = options.config;
@@ -8283,61 +8420,23 @@ var StyledEditor = class extends CustomEditor {
8283
8420
  invalidate() {
8284
8421
  super.invalidate();
8285
8422
  this.semantic = semanticTheme(this.piTheme, this.config);
8423
+ this.renderPlanCache = void 0;
8286
8424
  this.tui.requestRender();
8287
8425
  }
8288
8426
  render(width) {
8289
8427
  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);
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);
8332
8433
  if (innerLines.length === 0) return [];
8333
8434
  const body = innerLines.slice(1, -1);
8334
- const prefix = `${" ".repeat(padding)}${prompt} `;
8335
- const continuation = " ".repeat(padding + promptWidth);
8336
8435
  const hint = this.config.editor.hint;
8337
8436
  const showHint = hint !== "" && this.getText() === "";
8338
8437
  const bashHidden = this.bashHiddenCount();
8339
8438
  const renderedBody = body.map((line, index) => {
8340
- const lead = index === 0 ? prefix : continuation;
8439
+ const lead = index === 0 ? plan.prefix : plan.continuation;
8341
8440
  const source = index === 0 && bashHidden > 0 ? stripLeadingVisibleChars(line, bashHidden) : line;
8342
8441
  let content = `${lead}${source}`;
8343
8442
  if (showHint && index === 0 && line) {
@@ -8346,10 +8445,10 @@ var StyledEditor = class extends CustomEditor {
8346
8445
  if (end < content.length) content = content.slice(0, end);
8347
8446
  content += this.semantic.apply("hint", hint);
8348
8447
  }
8349
- return widthSafe(content, renderWidth);
8448
+ return widthSafe(content, plan.renderWidth);
8350
8449
  });
8351
- const metadata = this.metadata(width, style);
8352
- 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);
8353
8452
  return framed.map((line) => widthSafe(line, width));
8354
8453
  }
8355
8454
  dispose() {
@@ -8357,6 +8456,59 @@ var StyledEditor = class extends CustomEditor {
8357
8456
  this.disposed = true;
8358
8457
  this.invalidate();
8359
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
+ }
8360
8512
  prompt() {
8361
8513
  if (this.isBashMode()) {
8362
8514
  const glyph = this.semantic.glyph("bashPrompt");
@@ -8598,13 +8750,13 @@ function resolveAccentRgb(resolved) {
8598
8750
  function styledLogoLines(resolved) {
8599
8751
  const accent = resolveAccentRgb(resolved);
8600
8752
  if (!accent) return [...PI_LOGO_LINES];
8601
- const cacheKey = `${resolved.color("accent")}|${resolved.mode}`;
8602
- if (cacheKey === logoGradientCacheKey && logoGradientCacheLines) return logoGradientCacheLines;
8753
+ const cacheKey2 = `${resolved.color("accent")}|${resolved.mode}`;
8754
+ if (cacheKey2 === logoGradientCacheKey && logoGradientCacheLines) return logoGradientCacheLines;
8603
8755
  const palette = buildLogoPalette(accent);
8604
8756
  logoGradientCacheLines = PI_LOGO_LINES.map(
8605
8757
  (line, rowIndex) => renderLogoGradientLine(line, palette, rowIndex * LOGO_ROW_PHASE_STEP)
8606
8758
  );
8607
- logoGradientCacheKey = cacheKey;
8759
+ logoGradientCacheKey = cacheKey2;
8608
8760
  return logoGradientCacheLines;
8609
8761
  }
8610
8762
  function compactLogoHeader(resolved, details, width) {
@@ -9667,16 +9819,51 @@ var RenderScheduler = class {
9667
9819
 
9668
9820
  // extension-src/pi-style/app/snapshot.ts
9669
9821
  function createSnapshot(generation2, revision = 0, values = {}) {
9670
- return Object.freeze({ generation: generation2, revision, ...values });
9822
+ return Object.freeze({ ...values, generation: generation2, revision });
9671
9823
  }
9672
9824
  function replaceSnapshot(current, generation2, values) {
9673
9825
  if (current.generation !== generation2) return current;
9674
- 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);
9675
9862
  }
9676
9863
 
9677
9864
  // extension-src/pi-style/app/runtime.ts
9678
- function createPiStyleRuntime(host, generation2, requestRender = () => {
9679
- }) {
9865
+ function createPiStyleRuntime(host, generation2, requestRender = host.requestRender ?? (() => {
9866
+ })) {
9680
9867
  let disposed = false;
9681
9868
  const extensionStatuses = () => {
9682
9869
  if (!host.extensionStatusProvider) return void 0;
@@ -9687,13 +9874,22 @@ function createPiStyleRuntime(host, generation2, requestRender = () => {
9687
9874
  return [];
9688
9875
  }
9689
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
+ };
9690
9886
  let currentConfig = host.config;
9691
9887
  const disposables = new DisposableStore();
9692
9888
  const scheduler = new RenderScheduler({ requestRender }, generation2, () => !disposed);
9693
9889
  const git = new CachedGitProvider(host.gitRunner);
9694
9890
  const contextProvider = new InMemoryContextProvider();
9695
9891
  const usageProvider = new InMemoryUsageProvider();
9696
- const usage = host.getContextUsage?.();
9892
+ const initialContext = contextSnapshot();
9697
9893
  const initialStatuses = extensionStatuses();
9698
9894
  const initialValues = {
9699
9895
  ...initialStatuses ? { extensionStatuses: initialStatuses } : {},
@@ -9702,13 +9898,7 @@ function createPiStyleRuntime(host, generation2, requestRender = () => {
9702
9898
  ...host.model?.reasoning !== void 0 ? { reasoning: host.model.reasoning } : {},
9703
9899
  ...host.thinkingLevel ? { thinkingLevel: normalizeThinkingLevel(host.thinkingLevel) } : {},
9704
9900
  ...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
- } : {}
9901
+ ...initialContext ? { context: initialContext } : {}
9712
9902
  };
9713
9903
  let currentSnapshot = createSnapshot(generation2, 0, initialValues);
9714
9904
  let statusLine;
@@ -9719,14 +9909,35 @@ function createPiStyleRuntime(host, generation2, requestRender = () => {
9719
9909
  editor: "disabled",
9720
9910
  startup: "disabled"
9721
9911
  };
9722
- 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) => ({
9723
9925
  ...currentSnapshot,
9724
9926
  reason: host.startupReason ?? "startup",
9725
9927
  ...host.provider ? { startupProvider: host.provider } : {},
9726
9928
  ...host.cwd ? { project: host.cwd.split(/[\\/]/).filter(Boolean).at(-1) } : {},
9727
9929
  preset: config.preset,
9728
- ...host.resources ? { resources: host.resources } : {}
9930
+ ...resources ? { resources } : {}
9729
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));
9730
9941
  const installStatus = () => {
9731
9942
  if (!host.hasUI || host.mode !== "tui" || !host.ui || !currentConfig.enabled || !currentConfig.statusLine.enabled) {
9732
9943
  installationState = { ...installationState, status: "disabled" };
@@ -9789,7 +10000,7 @@ function createPiStyleRuntime(host, generation2, requestRender = () => {
9789
10000
  startup = installStartup({
9790
10001
  host: { ...host.ui, mode: host.mode, hasUI: host.hasUI },
9791
10002
  config: currentConfig,
9792
- snapshot: startupSnapshot(currentConfig),
10003
+ snapshot: createStartupSnapshot(currentConfig),
9793
10004
  generation: generation2,
9794
10005
  requestRender,
9795
10006
  timeoutMs: 3e3,
@@ -9838,27 +10049,10 @@ function createPiStyleRuntime(host, generation2, requestRender = () => {
9838
10049
  if (host.cwd && currentConfig.enabled && currentConfig.statusLine.enabled) {
9839
10050
  void git.get(host.cwd).then((value) => {
9840
10051
  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 } : {}
10052
+ if (updateSnapshot(withSnapshotPatch({ git: value }))) requestRender();
9860
10053
  });
9861
10054
  }
10055
+ if (initialContext) contextProvider.set("active", initialContext);
9862
10056
  return {
9863
10057
  generation: generation2,
9864
10058
  providerIdentity: { git, context: contextProvider, usage: usageProvider },
@@ -9867,49 +10061,33 @@ function createPiStyleRuntime(host, generation2, requestRender = () => {
9867
10061
  },
9868
10062
  mode: host.mode,
9869
10063
  hasUI: host.hasUI,
9870
- snapshot: currentSnapshot,
10064
+ get snapshot() {
10065
+ return currentSnapshot;
10066
+ },
9871
10067
  disposables,
9872
10068
  scheduler,
9873
10069
  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
- });
10070
+ if (disposed || !startup) return false;
10071
+ startup.update(createStartupSnapshot(currentConfig, resources));
9883
10072
  requestRender();
10073
+ return true;
9884
10074
  },
9885
10075
  dismissStartup() {
9886
10076
  startup?.dismiss();
9887
10077
  },
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
- });
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));
9913
10091
  },
9914
10092
  configure(nextConfig) {
9915
10093
  if (disposed) return;
@@ -9944,17 +10122,7 @@ function createPiStyleRuntime(host, generation2, requestRender = () => {
9944
10122
  git.invalidate(host.cwd);
9945
10123
  void git.get(host.cwd).then((value) => {
9946
10124
  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
9956
- });
9957
- requestRender();
10125
+ if (updateSnapshot(withSnapshotPatch({ git: value }))) requestRender();
9958
10126
  });
9959
10127
  },
9960
10128
  get disposed() {
@@ -10158,10 +10326,10 @@ function createPiStyleApp(initialConfig, reloadPort, onConfigChange) {
10158
10326
  sessionShutdown() {
10159
10327
  runtime.stop();
10160
10328
  },
10161
- update(values, kind = "coalesced") {
10329
+ update(values, kind = "coalesced", options) {
10162
10330
  const active = runtime.current;
10163
10331
  if (!active) return;
10164
- active.update(values);
10332
+ if (!active.update(values, options)) return;
10165
10333
  active.scheduler.schedule(kind);
10166
10334
  },
10167
10335
  reload() {
@@ -10464,9 +10632,11 @@ function renderBashExecutionBox(instance, args) {
10464
10632
  try {
10465
10633
  if (host.piStyleStart === void 0) host.piStyleStart = Date.now();
10466
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;
10467
10637
  const inner = boxInnerWidth(renderedWidth);
10468
10638
  const wrapped = content.render(inner).map((line) => boxLine(theme, line.startsWith(" ") ? line.slice(1) : line, renderedWidth));
10469
- return [
10639
+ const lines = [
10470
10640
  "",
10471
10641
  boxLabeledBorder(theme, TOP_LEFT, TOP_RIGHT, bashBoxTitle(theme, host), void 0, renderedWidth),
10472
10642
  boxBlankLine(theme, renderedWidth),
@@ -10474,6 +10644,8 @@ function renderBashExecutionBox(instance, args) {
10474
10644
  boxBlankLine(theme, renderedWidth),
10475
10645
  boxLabeledBorder(theme, BOTTOM_LEFT, BOTTOM_RIGHT, bashBoxFooter(theme, host), void 0, renderedWidth)
10476
10646
  ];
10647
+ if (host.status !== "running") host.piStyleRenderCache = { key: cacheKey2, lines };
10648
+ return lines;
10477
10649
  } catch {
10478
10650
  return void 0;
10479
10651
  }
@@ -10504,17 +10676,27 @@ import {
10504
10676
  } from "@earendil-works/pi-coding-agent";
10505
10677
 
10506
10678
  // extension-src/pi-style/features/messages/index.ts
10507
- import { visibleWidth as visibleWidth4 } from "@earendil-works/pi-tui";
10508
10679
  var OSC133_ZONE_START = "\x1B]133;A\x07";
10509
10680
  var OSC133_ZONE_END = "\x1B]133;B\x07";
10510
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
+ };
10511
10694
  function extractOscEnvelope(line) {
10512
10695
  if (!line.startsWith(OSC133_ZONE_START)) return void 0;
10513
10696
  const bodyEnd = line.indexOf(OSC133_ZONE_END, OSC133_ZONE_START.length);
10514
10697
  if (bodyEnd < 0 || !line.endsWith(OSC133_ZONE_FINAL)) return void 0;
10515
10698
  return { start: OSC133_ZONE_START, body: line.slice(OSC133_ZONE_START.length, bodyEnd), end: line.slice(bodyEnd) };
10516
10699
  }
10517
- var BG_RESET = "\x1B[49m";
10518
10700
  function splitLeadingMarkers(line) {
10519
10701
  let index = 0;
10520
10702
  while (line.startsWith("\x1B]", index)) {
@@ -10546,39 +10728,6 @@ function isBackgroundSgr(sequence) {
10546
10728
  }
10547
10729
  return false;
10548
10730
  }
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) : "";
10564
- if (index === contentIndex && firstEnvelope)
10565
- return `${firstEnvelope.start}${rebuildAtWidth(firstEnvelope.body, width, prefix)}${firstEnvelope.end}`;
10566
- if (index === contentIndex && firstHasStart)
10567
- return `${OSC133_ZONE_START}${rebuildAtWidth(line.slice(OSC133_ZONE_START.length), width, prefix)}`;
10568
- if (index === lastIndex && multilineEnvelope && index !== contentIndex)
10569
- return `${OSC133_ZONE_END}${OSC133_ZONE_FINAL}${rebuildAtWidth(
10570
- line.slice((OSC133_ZONE_END + OSC133_ZONE_FINAL).length),
10571
- width,
10572
- lead
10573
- )}`;
10574
- if (index === contentIndex && index === lastIndex && multilineEnvelope && line.startsWith(OSC133_ZONE_END + OSC133_ZONE_FINAL))
10575
- return `${OSC133_ZONE_END}${OSC133_ZONE_FINAL}${rebuildAtWidth(
10576
- line.slice((OSC133_ZONE_END + OSC133_ZONE_FINAL).length),
10577
- width,
10578
- prefix
10579
- )}`;
10580
- return rebuildAtWidth(line, width, lead);
10581
- }
10582
10731
  function contentText(line) {
10583
10732
  let output = "";
10584
10733
  for (let index = 0; index < line.length; index++) {
@@ -10602,32 +10751,127 @@ function contentText(line) {
10602
10751
  function hasContent(line) {
10603
10752
  return [...contentText(line)].some((character) => !/\s/u.test(character));
10604
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
+ }
10605
10845
  function prefixNative(lines, width, prefix) {
10606
10846
  if (!Array.isArray(lines) || lines.length === 0 || !lines.every((line) => typeof line === "string")) return void 0;
10847
+ messageDecorationTestState.decoratePasses++;
10607
10848
  const nativeLines = lines;
10608
- const prefixWidth = visibleWidth4(prefix);
10849
+ const prefixWidth = visibleWidth(prefix);
10609
10850
  if (width <= prefixWidth) return void 0;
10610
10851
  const bodyWidth = width - prefixWidth;
10611
10852
  const first = nativeLines[0] ?? "";
10612
10853
  const last = nativeLines.at(-1) ?? "";
10854
+ const lastAnalysis = getLineAnalysis(last);
10613
10855
  const multilineEnvelope = nativeLines.length > 1 && last.startsWith(OSC133_ZONE_END + OSC133_ZONE_FINAL);
10614
10856
  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);
10857
+ if (index !== nativeLines.length - 1 || !multilineEnvelope) return getLineAnalysis(line).hasContent;
10858
+ return !nativeLines.slice(0, index).some((earlier) => getLineAnalysis(earlier).hasContent) && lastAnalysis.hasContent;
10617
10859
  });
10618
10860
  if (firstContentIndex < 0) return nativeLines;
10619
- const firstEnvelope = firstContentIndex === 0 ? extractOscEnvelope(first) : void 0;
10620
- 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;
10621
10864
  const decorated = nativeLines.map(
10622
10865
  (line, index) => decorateMessageLine(line, index, nativeLines.length - 1, firstContentIndex, width, {
10623
10866
  firstEnvelope,
10624
10867
  firstHasStart,
10625
10868
  multilineEnvelope,
10626
- prefix
10869
+ prefix,
10870
+ prefixWidth
10627
10871
  })
10628
10872
  );
10629
- if (!decorated.every((line) => visibleWidth4(line) <= width)) return void 0;
10630
- 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;
10631
10875
  return decorated;
10632
10876
  }
10633
10877
  function decorateMessageRender(original, instance, args, snapshot = {
@@ -10640,10 +10884,20 @@ function decorateMessageRender(original, instance, args, snapshot = {
10640
10884
  const width = typeof args[0] === "number" ? args[0] : 0;
10641
10885
  const prefix = snapshot.assistantPrefix;
10642
10886
  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);
10887
+ const prefixWidth = visibleWidth(prefix);
10888
+ if (width <= prefixWidth) return Reflect.apply(original, instance, args);
10889
+ const reducedWidth = width - prefixWidth;
10645
10890
  const native = Reflect.apply(original, instance, [reducedWidth, ...args.slice(1)]);
10646
- 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;
10647
10901
  }
10648
10902
  function isSpacerChild(child) {
10649
10903
  return typeof child?.setLines === "function";
@@ -11003,7 +11257,7 @@ var KNOWN_NATIVE_IDENTITIES = Object.freeze({
11003
11257
  });
11004
11258
  var TRUSTED_NATIVE_FINGERPRINTS = Object.freeze(
11005
11259
  Object.fromEntries(
11006
- 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 ?? ""])
11007
11261
  )
11008
11262
  );
11009
11263
  function fingerprint(value) {
@@ -11859,22 +12113,134 @@ function createPiStyleSessionCoordinator(pi, hooks = {}) {
11859
12113
  }
11860
12114
 
11861
12115
  // extension-src/pi-style/pi/session-usage.ts
12116
+ var usageCache = /* @__PURE__ */ new WeakMap();
11862
12117
  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;
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;
11878
12244
  return {
11879
12245
  inputTokens: totals.input,
11880
12246
  outputTokens: totals.output,
@@ -11885,18 +12251,30 @@ function usageFromSession(session) {
11885
12251
  streaming: false
11886
12252
  };
11887
12253
  }
11888
- function addUsage(totals, usage) {
12254
+ function addContribution(totals, usage) {
11889
12255
  totals.input += usage.input;
11890
12256
  totals.output += usage.output;
11891
12257
  totals.cacheRead += usage.cacheRead;
11892
12258
  totals.cacheWrite += usage.cacheWrite;
11893
- 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 };
11894
12273
  }
11895
12274
 
11896
12275
  // extension-src/pi-style/pi/index.ts
11897
12276
  function usagePatch(ctx) {
11898
- const usage = usageFromSession(ctx.sessionManager);
11899
- return usage ? { usage } : {};
12277
+ return { usage: usageFromSession(ctx.sessionManager) };
11900
12278
  }
11901
12279
  function activateReadOnlyTools(pi) {
11902
12280
  const available = new Set(pi.getAllTools().map((tool) => tool.name));
@@ -11931,6 +12309,7 @@ function piStyleExtension(pi) {
11931
12309
  const coordinator = createPiStyleSessionCoordinator(pi, compatibilityTestHooks);
11932
12310
  registerPiStyleCommand(pi, coordinator.app);
11933
12311
  pi.on("session_start", async (event, ctx) => {
12312
+ resetUsageFromSessionCache(ctx.sessionManager);
11934
12313
  if (pi.getFlag("pi-style-readonly-tools") === true) {
11935
12314
  activateReadOnlyTools(pi);
11936
12315
  }
@@ -11964,15 +12343,21 @@ function piStyleExtension(pi) {
11964
12343
  )
11965
12344
  );
11966
12345
  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"));
12346
+ pi.on(
12347
+ "session_info_changed",
12348
+ (event) => coordinator.app.update({ sessionName: event.name }, "coalesced", { refreshExtensionStatuses: true })
12349
+ );
11968
12350
  pi.on("message_start", () => {
11969
12351
  closeActiveBatch();
11970
12352
  });
11971
12353
  pi.on("message_update", () => coordinator.app.update({}, "coalesced"));
11972
- 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
+ );
11973
12358
  pi.on("turn_end", (event, ctx) => {
11974
12359
  registerTurnFromMessage(event.message, event.toolResults);
11975
- coordinator.app.update({ ...usagePatch(ctx) }, "deferred");
12360
+ coordinator.app.update({ ...usagePatch(ctx) }, "deferred", { refreshContextUsage: true });
11976
12361
  });
11977
12362
  pi.on("agent_end", () => {
11978
12363
  const run = finishAgentRun();
@@ -11981,23 +12366,33 @@ function piStyleExtension(pi) {
11981
12366
  requestToolPresentationRender();
11982
12367
  }
11983
12368
  });
11984
- 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
+ );
11985
12373
  pi.on("session_tree", (_event, ctx) => {
12374
+ resetUsageFromSessionCache(ctx.sessionManager);
11986
12375
  rebuildTurnRegistryFromEntries(ctx.sessionManager.getEntries());
11987
- 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 });
11988
12381
  });
11989
- pi.on("session_compact", (_event, ctx) => coordinator.app.update({ ...usagePatch(ctx) }, "deferred"));
11990
12382
  pi.on("tool_result", (event, ctx) => {
11991
12383
  if (["write", "edit", "bash"].includes(event.toolName)) {
11992
12384
  coordinator.app.runtime.current?.invalidateGit();
11993
- coordinator.app.update({ ...usagePatch(ctx) }, "delayed-retry");
12385
+ coordinator.app.update({ ...usagePatch(ctx) }, "delayed-retry", { refreshContextUsage: true });
11994
12386
  }
11995
12387
  });
11996
12388
  pi.on("user_bash", (_event, ctx) => {
11997
12389
  coordinator.app.runtime.current?.invalidateGit();
11998
- 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();
11999
12395
  });
12000
- pi.on("session_shutdown", () => coordinator.shutdown());
12001
12396
  }
12002
12397
  export {
12003
12398
  __setCompatibilityTestHooks,