@quandev104/pi-style 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +1 -1
  3. package/dist/extensions/pi-style.js +632 -262
  4. package/dist/extensions/pi-style.js.map +1 -1
  5. package/extension-src/pi-style/app/runtime.ts +29 -4
  6. package/extension-src/pi-style/domain/status-renderer.ts +40 -8
  7. package/extension-src/pi-style/domain/status.ts +15 -5
  8. package/extension-src/pi-style/domain/theme.ts +32 -1
  9. package/extension-src/pi-style/features/editor/index.ts +17 -5
  10. package/extension-src/pi-style/features/messages/index.ts +302 -61
  11. package/extension-src/pi-style/features/status-line/index.ts +41 -11
  12. package/extension-src/pi-style/features/tools/boxed/bash.ts +101 -40
  13. package/extension-src/pi-style/features/tools/boxed/batch.ts +17 -1
  14. package/extension-src/pi-style/features/tools/boxed/edit.ts +20 -15
  15. package/extension-src/pi-style/features/tools/boxed/find.ts +9 -4
  16. package/extension-src/pi-style/features/tools/boxed/git.ts +46 -2
  17. package/extension-src/pi-style/features/tools/boxed/grep.ts +9 -2
  18. package/extension-src/pi-style/features/tools/boxed/ls.ts +9 -4
  19. package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +18 -11
  20. package/extension-src/pi-style/features/tools/boxed/read.ts +4 -2
  21. package/extension-src/pi-style/features/tools/boxed/shared.ts +27 -1
  22. package/extension-src/pi-style/features/tools/boxed/turn-summary.ts +12 -0
  23. package/extension-src/pi-style/pi/index.ts +2 -0
  24. package/extension-src/pi-style/shared/ansi.ts +17 -5
  25. package/extension-src/pi-style/shared/box.ts +70 -4
  26. package/extension-src/pi-style/shared/split-diff.ts +8 -5
  27. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  // extension-src/pi-style/shared/ansi.ts
2
+ import { visibleWidth as tuiVisibleWidth } from "@earendil-works/pi-tui";
2
3
  function isFinal(byte) {
3
4
  return byte >= "@" && byte <= "~";
4
5
  }
@@ -82,7 +83,7 @@ function stripAnsi(value) {
82
83
  return output;
83
84
  }
84
85
  function visibleWidth(value) {
85
- return [...stripAnsi(value)].length;
86
+ return tuiVisibleWidth(value);
86
87
  }
87
88
  function resetAnsi(value) {
88
89
  return `${value}\x1B[0m`;
@@ -93,9 +94,10 @@ function fitAnsiWidth(value, width, ellipsis = "\u2026") {
93
94
  function truncateAnsi(value, width, ellipsis = "\u2026") {
94
95
  if (width <= 0) return "";
95
96
  if (visibleWidth(value) <= width) return resetAnsi(value);
97
+ const ellipsisWidth = visibleWidth(ellipsis);
96
98
  let output = "";
97
99
  let visible = 0;
98
- for (let i = 0; i < value.length && visible < width - visibleWidth(ellipsis); i++) {
100
+ for (let i = 0; i < value.length && visible < width - ellipsisWidth; i++) {
99
101
  if (value.charCodeAt(i) === 27) {
100
102
  const start = i;
101
103
  i++;
@@ -233,7 +235,7 @@ function getElapsedMs(result) {
233
235
  // extension-src/pi-style/shared/render-budget.ts
234
236
  import {
235
237
  truncateToWidth as tuiTruncateToWidth,
236
- visibleWidth as tuiVisibleWidth,
238
+ visibleWidth as tuiVisibleWidth2,
237
239
  wrapTextWithAnsi
238
240
  } from "@earendil-works/pi-tui";
239
241
  var MAX_RENDER_LINE_CHARS = 2e3;
@@ -455,7 +457,7 @@ function safeVisibleWidth(text) {
455
457
  const fastWidth = knownVisibleWidth(text);
456
458
  if (fastWidth) return fastWidth.visibleWidth;
457
459
  if (text === TRUNCATE_ELLIPSIS) return 1;
458
- return tuiVisibleWidth(text);
460
+ return tuiVisibleWidth2(text);
459
461
  }
460
462
  function safeTruncateToWidth(text, maxWidth, ellipsis = "...", pad = false) {
461
463
  const width = Math.floor(maxWidth);
@@ -743,12 +745,48 @@ function getTextOutput(result) {
743
745
  });
744
746
  return textBlocks.map((contentBlock) => String(contentBlock.text ?? "")).join("\n").replace(/\r/g, "");
745
747
  }
748
+ var WORD_CP_UNKNOWN = 255;
749
+ var WORD_CP_SURROGATE = 254;
750
+ var WORD_CP_CLASS = new Uint8Array(65536).fill(WORD_CP_UNKNOWN);
751
+ for (let code = 48; code <= 57; code++) WORD_CP_CLASS[code] = 1;
752
+ for (let code = 65; code <= 90; code++) WORD_CP_CLASS[code] = 1;
753
+ for (let code = 97; code <= 122; code++) WORD_CP_CLASS[code] = 1;
754
+ WORD_CP_CLASS[39] = 1;
755
+ WORD_CP_CLASS[45] = 1;
756
+ WORD_CP_CLASS[95] = 1;
757
+ WORD_CP_CLASS.fill(WORD_CP_SURROGATE, 55296, 57344);
758
+ var NON_ASCII_WORD_RE = /[\p{L}\p{N}]/u;
759
+ var ASTRAL_WORD_CP = /* @__PURE__ */ new Map();
760
+ function astralWordMembership(codePoint) {
761
+ const cached = ASTRAL_WORD_CP.get(codePoint);
762
+ if (cached !== void 0) return cached;
763
+ const membership = NON_ASCII_WORD_RE.test(String.fromCodePoint(codePoint)) ? 1 : 0;
764
+ ASTRAL_WORD_CP.set(codePoint, membership);
765
+ return membership;
766
+ }
746
767
  function countWords(text) {
768
+ const len = text.length;
747
769
  let count = 0;
748
- let inWord = false;
749
- for (const char of text) {
750
- const isWord = /[\p{L}\p{N}_'-]/u.test(char);
751
- if (isWord && !inWord) count++;
770
+ let inWord = 0;
771
+ for (let i = 0; i < len; i++) {
772
+ const code = text.charCodeAt(i);
773
+ const membership = WORD_CP_CLASS[code] ?? 0;
774
+ let isWord;
775
+ if (membership <= 1) {
776
+ isWord = membership;
777
+ } else if (membership === WORD_CP_SURROGATE) {
778
+ const next = i + 1 < len ? text.charCodeAt(i + 1) : 0;
779
+ if (code <= 56319 && next >= 56320 && next <= 57343) {
780
+ isWord = astralWordMembership(65536 + (code - 55296 << 10) + (next - 56320));
781
+ i++;
782
+ } else {
783
+ isWord = 0;
784
+ }
785
+ } else {
786
+ isWord = NON_ASCII_WORD_RE.test(String.fromCharCode(code)) ? 1 : 0;
787
+ WORD_CP_CLASS[code] = isWord;
788
+ }
789
+ count += isWord & (inWord ^ 1);
752
790
  inWord = isWord;
753
791
  }
754
792
  return count;
@@ -1615,7 +1653,7 @@ function registerBatchResult(meta, data, context) {
1615
1653
  if (member) {
1616
1654
  const nextStatus = data.isPartial ? "running" : "done";
1617
1655
  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;
1656
+ const changed = member.status !== nextStatus || member.isError !== nextIsError || member.errorText !== (nextIsError ? data.errorText : void 0) || data.entries !== void 0 && member.outputEntries !== data.entries;
1619
1657
  member.status = nextStatus;
1620
1658
  member.isError = nextIsError;
1621
1659
  if (member.isError && data.errorText !== void 0) member.errorText = data.errorText;
@@ -1629,6 +1667,12 @@ function registerBatchResult(meta, data, context) {
1629
1667
  }
1630
1668
  return { batch, isLeader: batch.leaderId === context.toolCallId };
1631
1669
  }
1670
+ function hasFinalBatchOutput(toolCallId) {
1671
+ const batch = batchByCallId.get(toolCallId);
1672
+ if (!batch) return false;
1673
+ const member = batch.members.find((entry) => entry.toolCallId === toolCallId);
1674
+ return member !== void 0 && member.outputEntries !== void 0 && member.status === "done" && !member.isError;
1675
+ }
1632
1676
  function batchStatus(batch) {
1633
1677
  let done = 0;
1634
1678
  let failed = 0;
@@ -1988,6 +2032,9 @@ function invalidateTurnMembers(turn) {
1988
2032
  }
1989
2033
  }
1990
2034
  }
2035
+ function releaseTurnInvalidators(turn) {
2036
+ for (const member of turn.members) invalidateByCallId.delete(member.toolCallId);
2037
+ }
1991
2038
  function noteTurnMemberElapsed(toolCallId, elapsedMs) {
1992
2039
  if (elapsedMs === void 0) return;
1993
2040
  const entry = memberByCallId.get(toolCallId);
@@ -2879,7 +2926,9 @@ import { getLanguageFromPath } from "@earendil-works/pi-coding-agent";
2879
2926
  import { highlightCode } from "@earendil-works/pi-coding-agent";
2880
2927
  var ESC2 = "\x1B";
2881
2928
  var BG_ANSI_PATTERN = new RegExp(`${ESC2}\\[(?:4\\d|10\\d|48;5;\\d{1,3}|48;2;\\d{1,3};\\d{1,3};\\d{1,3}|49)m`, "g");
2929
+ var ANSI_SGR_PATTERN = new RegExp(`${ESC2}\\[([0-9;]*)m`, "g");
2882
2930
  var CONTROL_CHARS = "\0-\b\v\f-\x7F";
2931
+ var CONTROL_CHARS_PATTERN = new RegExp(`[${CONTROL_CHARS}]`, "g");
2883
2932
  var ADD_ROW_BACKGROUND_MIX_RATIO = 0.24;
2884
2933
  var REMOVE_ROW_BACKGROUND_MIX_RATIO = 0.12;
2885
2934
  var ADD_INLINE_EMPHASIS_MIX_RATIO = 0.44;
@@ -2971,7 +3020,7 @@ function resolveDiffPalette(theme) {
2971
3020
  }
2972
3021
  function keepBackgroundAcrossResets(text, rowBgAnsi) {
2973
3022
  if (!text) return text;
2974
- return text.replace(new RegExp(`${ESC2}\\[([0-9;]*)m`, "g"), (sequence, rawCodes) => {
3023
+ return text.replace(ANSI_SGR_PATTERN, (sequence, rawCodes) => {
2975
3024
  const split = String(rawCodes ?? "").split(";").filter(Boolean);
2976
3025
  const codes = split.length > 0 ? split : ["0"];
2977
3026
  const hasGlobalReset = codes.includes("0");
@@ -3013,7 +3062,7 @@ function applyBackgroundToVisibleRange(ansiText, start, end, backgroundAnsi, res
3013
3062
  return output;
3014
3063
  }
3015
3064
  function sanitizeSingleLineText(value) {
3016
- return value.replace(/\r/g, "").replace(/\n/g, "").replace(new RegExp(`[${CONTROL_CHARS}]`, "g"), "");
3065
+ return value.replace(/\r/g, "").replace(/\n/g, "").replace(CONTROL_CHARS_PATTERN, "");
3017
3066
  }
3018
3067
  function stripInlineBreaksPreserveAnsi(value) {
3019
3068
  return value.replace(/\r/g, "").replace(/\n/g, "");
@@ -3610,6 +3659,101 @@ var AdaptiveDiffComponent = class {
3610
3659
  }
3611
3660
  };
3612
3661
 
3662
+ // extension-src/pi-style/features/tools/boxed/shared.ts
3663
+ function pendingFlag(context) {
3664
+ return Boolean(context.isPartial);
3665
+ }
3666
+ function displayPath(rawPath, context) {
3667
+ const path = String(rawPath ?? "");
3668
+ if (!path) return "(unknown)";
3669
+ return shortenPath(resolveRelativePath(path, context.cwd));
3670
+ }
3671
+ function pathRangeDetail(rawPath, offset, limit, context) {
3672
+ const path = displayPath(rawPath, context);
3673
+ let range = "";
3674
+ if (offset !== void 0 || limit !== void 0) {
3675
+ const start = offset ?? 1;
3676
+ const end = limit !== void 0 ? Number(start) + Number(limit) - 1 : "";
3677
+ range = `:${start}${end ? `-${end}` : ""}`;
3678
+ }
3679
+ return path ? `${path}${range}` : "(unknown)";
3680
+ }
3681
+ function compactCall(theme, toolName, detailLine, options) {
3682
+ return renderCompactBoxedToolCall(theme, toolName, detailLine, {
3683
+ widthKey: boxedToolWidthKey(toolName, options.detailKey),
3684
+ state: options.context.state,
3685
+ isError: Boolean(options.context.isError),
3686
+ isPartial: Boolean(options.context.isPartial),
3687
+ isPending: pendingFlag(options.context),
3688
+ running: Boolean(options.context.executionStarted)
3689
+ });
3690
+ }
3691
+ function noteExecutionStart(context) {
3692
+ recordExecutionStarted(context.state, context.executionStarted);
3693
+ }
3694
+ function noteBoxedCallState(context) {
3695
+ if (!context.executionStarted) return;
3696
+ if (context.isPartial) startElapsedTicker(context.state, context.invalidate);
3697
+ else {
3698
+ recordExecutionEnded(context.state);
3699
+ stopElapsedTicker(context.state);
3700
+ }
3701
+ }
3702
+ function noteBoxedResultPhase(context, isPartial) {
3703
+ const firstResultPass = !isResultSeen(context.state);
3704
+ markResultSeen(context.state);
3705
+ if (isPartial) startElapsedTicker(context.state, context.invalidate);
3706
+ else {
3707
+ recordExecutionEnded(context.state);
3708
+ stopElapsedTicker(context.state);
3709
+ }
3710
+ return firstResultPass;
3711
+ }
3712
+ function stateElapsedMs(context) {
3713
+ return getStateElapsedMs(context.state);
3714
+ }
3715
+ function compactFooterWithState(theme, result, context, options = {}) {
3716
+ const elapsedMs = stateElapsedMs(context);
3717
+ return renderCompactBoxedFooter(theme, result, {
3718
+ state: context.state,
3719
+ isError: Boolean(options.isError ?? context.isError),
3720
+ isPartial: Boolean(options.isPartial ?? context.isPartial),
3721
+ ...elapsedMs === void 0 ? {} : { elapsedMs }
3722
+ });
3723
+ }
3724
+ function resultFooterLines(theme, result, context, extraParts = []) {
3725
+ return [formatBoxedFooter(theme, result, extraParts, stateElapsedMs(context))];
3726
+ }
3727
+ var CACHE_KEY_LONG_PART_THRESHOLD = 64;
3728
+ function fnv1aHex(text) {
3729
+ let hash = 2166136261;
3730
+ for (let i = 0; i < text.length; i++) {
3731
+ hash ^= text.charCodeAt(i);
3732
+ hash = Math.imul(hash, 16777619) >>> 0;
3733
+ }
3734
+ return hash.toString(16).padStart(8, "0");
3735
+ }
3736
+ function boundedCacheKeyPart(part) {
3737
+ if (typeof part !== "string" || part.length <= CACHE_KEY_LONG_PART_THRESHOLD) return part;
3738
+ return `${part.length}:${fnv1aHex(part)}`;
3739
+ }
3740
+ function getRenderCacheKey(prefix, theme, ...parts) {
3741
+ const pieces = [prefix, themeCacheKey(theme), getToolsRenderCacheSignature()];
3742
+ for (const part of parts) pieces.push(boundedCacheKeyPart(part));
3743
+ return pieces.join("|");
3744
+ }
3745
+ function memoizedStateComponent(state, slot, key, build) {
3746
+ if (!state || typeof state !== "object") return build();
3747
+ const cached = state[slot];
3748
+ if (cached && cached.key === key) return cached.component;
3749
+ const component = build();
3750
+ state[slot] = { key, component };
3751
+ return component;
3752
+ }
3753
+ function clearFooterState(context) {
3754
+ clearCompactBoxedFooter(context.state);
3755
+ }
3756
+
3613
3757
  // extension-src/pi-style/features/tools/boxed/git.ts
3614
3758
  var GIT_SHORT_STATUS_FLAGS = /* @__PURE__ */ new Set(["-s", "--short", "--porcelain"]);
3615
3759
  var GIT_DIFF_FORMAT_REJECT = /* @__PURE__ */ new Set([
@@ -4916,6 +5060,34 @@ function binaryBodyLine(theme, status) {
4916
5060
  }
4917
5061
  function renderGitDiffResult(theme, parsed, options, context) {
4918
5062
  const expanded = Boolean(options.expanded);
5063
+ let totalAdditions = 0;
5064
+ let totalRemovals = 0;
5065
+ const sigParts = [];
5066
+ for (const file of parsed.files) {
5067
+ totalAdditions += file.additions;
5068
+ totalRemovals += file.removals;
5069
+ sigParts.push(
5070
+ `${file.path}:${file.body.length}:${file.additions}:${file.removals}:${file.binary ? 1 : 0}:${file.status ?? ""}`
5071
+ );
5072
+ }
5073
+ const sig = sigParts.join(";").slice(0, 2048);
5074
+ return memoizedStateComponent(
5075
+ context.state,
5076
+ "__piStyleGitDiffResult",
5077
+ getRenderCacheKey(
5078
+ "git-diff-result",
5079
+ theme,
5080
+ String(parsed.show),
5081
+ String(expanded),
5082
+ parsed.files.length,
5083
+ totalAdditions,
5084
+ totalRemovals,
5085
+ sig
5086
+ ),
5087
+ () => buildGitDiffResultComponent(theme, parsed, expanded, context)
5088
+ );
5089
+ }
5090
+ function buildGitDiffResultComponent(theme, parsed, expanded, context) {
4919
5091
  const elapsedMs = getStateElapsedMs(context.state);
4920
5092
  const fileCount = parsed.files.length;
4921
5093
  const footerParts = [];
@@ -4987,86 +5159,6 @@ function renderGitDiffResult(theme, parsed, options, context) {
4987
5159
  };
4988
5160
  }
4989
5161
 
4990
- // extension-src/pi-style/features/tools/boxed/shared.ts
4991
- function pendingFlag(context) {
4992
- return Boolean(context.isPartial);
4993
- }
4994
- function displayPath(rawPath, context) {
4995
- const path = String(rawPath ?? "");
4996
- if (!path) return "(unknown)";
4997
- return shortenPath(resolveRelativePath(path, context.cwd));
4998
- }
4999
- function pathRangeDetail(rawPath, offset, limit, context) {
5000
- const path = displayPath(rawPath, context);
5001
- let range = "";
5002
- if (offset !== void 0 || limit !== void 0) {
5003
- const start = offset ?? 1;
5004
- const end = limit !== void 0 ? Number(start) + Number(limit) - 1 : "";
5005
- range = `:${start}${end ? `-${end}` : ""}`;
5006
- }
5007
- return path ? `${path}${range}` : "(unknown)";
5008
- }
5009
- function compactCall(theme, toolName, detailLine, options) {
5010
- return renderCompactBoxedToolCall(theme, toolName, detailLine, {
5011
- widthKey: boxedToolWidthKey(toolName, options.detailKey),
5012
- state: options.context.state,
5013
- isError: Boolean(options.context.isError),
5014
- isPartial: Boolean(options.context.isPartial),
5015
- isPending: pendingFlag(options.context),
5016
- running: Boolean(options.context.executionStarted)
5017
- });
5018
- }
5019
- function noteExecutionStart(context) {
5020
- recordExecutionStarted(context.state, context.executionStarted);
5021
- }
5022
- function noteBoxedCallState(context) {
5023
- if (!context.executionStarted) return;
5024
- if (context.isPartial) startElapsedTicker(context.state, context.invalidate);
5025
- else {
5026
- recordExecutionEnded(context.state);
5027
- stopElapsedTicker(context.state);
5028
- }
5029
- }
5030
- function noteBoxedResultPhase(context, isPartial) {
5031
- const firstResultPass = !isResultSeen(context.state);
5032
- markResultSeen(context.state);
5033
- if (isPartial) startElapsedTicker(context.state, context.invalidate);
5034
- else {
5035
- recordExecutionEnded(context.state);
5036
- stopElapsedTicker(context.state);
5037
- }
5038
- return firstResultPass;
5039
- }
5040
- function stateElapsedMs(context) {
5041
- return getStateElapsedMs(context.state);
5042
- }
5043
- function compactFooterWithState(theme, result, context, options = {}) {
5044
- const elapsedMs = stateElapsedMs(context);
5045
- return renderCompactBoxedFooter(theme, result, {
5046
- state: context.state,
5047
- isError: Boolean(options.isError ?? context.isError),
5048
- isPartial: Boolean(options.isPartial ?? context.isPartial),
5049
- ...elapsedMs === void 0 ? {} : { elapsedMs }
5050
- });
5051
- }
5052
- function resultFooterLines(theme, result, context, extraParts = []) {
5053
- return [formatBoxedFooter(theme, result, extraParts, stateElapsedMs(context))];
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
- }
5066
- function clearFooterState(context) {
5067
- clearCompactBoxedFooter(context.state);
5068
- }
5069
-
5070
5162
  // extension-src/pi-style/features/tools/boxed/bash.ts
5071
5163
  var MAX_LINE_CHARS = 2e3;
5072
5164
  var ESC3 = "\x1B";
@@ -5167,6 +5259,25 @@ function countNewlines(text, from, to) {
5167
5259
  }
5168
5260
  return count;
5169
5261
  }
5262
+ function findBackwardLineStart(text, need, end = text.length) {
5263
+ let found = 0;
5264
+ for (let i = end - 1; i >= 0; i--) {
5265
+ if (text.charCodeAt(i) === 10 && ++found >= need) return i + 1;
5266
+ }
5267
+ return 0;
5268
+ }
5269
+ function isOutputWhitespaceCode(code) {
5270
+ if (code === 32 || code >= 9 && code <= 13) return true;
5271
+ if (code === 133 || code === 160 || code === 5760) return true;
5272
+ if (code >= 8192 && code <= 8202) return true;
5273
+ return code === 8232 || code === 8233 || code === 8239 || code === 8287 || code === 12288 || code === 65279;
5274
+ }
5275
+ function lastVisibleEnd(text) {
5276
+ for (let i = text.length - 1; i >= 0; i--) {
5277
+ if (!isOutputWhitespaceCode(text.charCodeAt(i))) return i + 1;
5278
+ }
5279
+ return 0;
5280
+ }
5170
5281
  function stripBashToolNoticeLines(text) {
5171
5282
  const filteredLines = text.replace(/\r/g, "").split("\n").filter((line) => !BASH_TOOL_NOTICE_PATTERN.test(line.trim()));
5172
5283
  return filteredLines.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
@@ -5297,8 +5408,10 @@ function bashBodyComponent(preview, emptyLines) {
5297
5408
  };
5298
5409
  }
5299
5410
  function renderBashStreamingResult(theme, raw, options, context) {
5300
- const body = stripBashToolNoticeLines(stripAnsi(raw));
5301
- const hasOutput = body.trim().length > 0;
5411
+ const contentEnd = lastVisibleEnd(raw);
5412
+ const hasOutput = contentEnd > 0;
5413
+ const tailStart = hasOutput ? findBackwardLineStart(raw, getToolsRenderConfig().maxCollapsedLines + 10, contentEnd) : 0;
5414
+ const body = stripBashToolNoticeLines(stripAnsi(raw.slice(tailStart, contentEnd)));
5302
5415
  const elapsed = getStateElapsedMs(context.state);
5303
5416
  const emptyLines = [theme.fg("dim", "No output received yet")];
5304
5417
  if (!hasOutput && isInteractiveCommand(context?.args?.command) && (elapsed ?? 0) >= 1e3) {
@@ -5329,17 +5442,7 @@ function renderBashFinalResult(theme, raw, options, context) {
5329
5442
  const referenceLines = rawCommand.split("\n").map((line, index) => `${index === 0 ? "$ " : "> "}${line}`);
5330
5443
  if (!options.expanded) {
5331
5444
  const scanLines = getToolsRenderConfig().maxCollapsedLines + 10;
5332
- let nlCount = 0;
5333
- let tailStart = 0;
5334
- for (let i = statusStripped.length - 1; i >= 0; i--) {
5335
- if (statusStripped.charCodeAt(i) === 10) {
5336
- nlCount++;
5337
- if (nlCount >= scanLines) {
5338
- tailStart = i + 1;
5339
- break;
5340
- }
5341
- }
5342
- }
5445
+ const tailStart = findBackwardLineStart(statusStripped, scanLines);
5343
5446
  const tail = stripBashToolNoticeLines(stripAnsi(statusStripped.slice(tailStart)));
5344
5447
  const totalLinesBefore = tailStart > 0 ? countNewlines(statusStripped, 0, tailStart) : 0;
5345
5448
  const preview2 = createBashResultPreview(theme, tail, options, outputColor);
@@ -5397,17 +5500,7 @@ function createBashResultPreview(theme, text, options, color) {
5397
5500
  if (cacheLines && cacheKey2 === cacheId) return cacheLines;
5398
5501
  if (!expanded) {
5399
5502
  const needed = cfg.maxCollapsedLines;
5400
- let totalNewlines = 0;
5401
- let scanFrom = 0;
5402
- for (let i = text.length - 1; i >= 0; i--) {
5403
- if (text.charCodeAt(i) === 10) {
5404
- totalNewlines++;
5405
- if (totalNewlines === needed) {
5406
- scanFrom = i + 1;
5407
- break;
5408
- }
5409
- }
5410
- }
5503
+ const scanFrom = findBackwardLineStart(text, needed);
5411
5504
  if (text.length === 0) {
5412
5505
  cacheKey2 = cacheId;
5413
5506
  cacheLines = [];
@@ -5430,26 +5523,26 @@ function createBashResultPreview(theme, text, options, color) {
5430
5523
  return cacheLines;
5431
5524
  }
5432
5525
  const normalized = replaceTabs(text);
5433
- const logicalLines = normalized.split("\n").map((l) => clampLineLength(l));
5434
- const hasOutput = !(logicalLines.length === 1 && logicalLines[0] === "");
5526
+ const rawLines = normalized.split("\n");
5527
+ const totalLines = rawLines.length;
5528
+ const hasOutput = !(totalLines === 1 && rawLines[0] === "");
5435
5529
  if (!hasOutput) {
5436
5530
  cacheKey2 = cacheId;
5437
5531
  cacheLines = [];
5438
5532
  return cacheLines;
5439
5533
  }
5440
- const truncatedLines = logicalLines.map((line) => safeTruncateToWidth(line, bodyWidth, "\u2026"));
5441
- const expandedLines = truncatedLines.length === 1 && truncatedLines[0] === "" ? [] : truncatedLines;
5442
5534
  const applyColor = (l) => color === "error" ? formatToolOutputLine(theme, l, "error") : cfg.dimOutput ? formatToolOutputLine(theme, l) : formatToolOutputLine(theme, l, "text");
5443
- if (cfg.maxExpandedLines > 0 && expandedLines.length > cfg.maxExpandedLines) {
5444
- const truncated = expandedLines.slice(-cfg.maxExpandedLines).map(applyColor);
5445
- const remaining = expandedLines.length - cfg.maxExpandedLines;
5535
+ const renderRawLine = (line) => safeTruncateToWidth(clampLineLength(line), bodyWidth, "\u2026");
5536
+ if (cfg.maxExpandedLines > 0 && totalLines > cfg.maxExpandedLines) {
5537
+ const truncated = rawLines.slice(-cfg.maxExpandedLines).map((line) => applyColor(renderRawLine(line)));
5538
+ const remaining = totalLines - cfg.maxExpandedLines;
5446
5539
  truncated.unshift(theme.fg("dim", `\u2026 ${remaining} earlier lines`));
5447
5540
  cacheKey2 = cacheId;
5448
5541
  cacheLines = truncated;
5449
5542
  return cacheLines;
5450
5543
  }
5451
5544
  cacheKey2 = cacheId;
5452
- cacheLines = expandedLines.map(applyColor);
5545
+ cacheLines = rawLines.map((line) => applyColor(renderRawLine(line)));
5453
5546
  return cacheLines;
5454
5547
  }
5455
5548
  };
@@ -5616,6 +5709,7 @@ function isGitDiffClass(cls) {
5616
5709
  function isGitActionClass(cls) {
5617
5710
  return !isBashTreeClass(cls) && cls.kind === "action";
5618
5711
  }
5712
+ var PARTIAL_REPARSE_THRESHOLD = 4096;
5619
5713
  function parseSemanticOutput(cls, output) {
5620
5714
  if (isBashTreeClass(cls)) return parseBashTreeOutput(cls, output);
5621
5715
  if (isGhClass(cls)) return parseGhOutput(cls, output);
@@ -5708,6 +5802,7 @@ var bashTool = {
5708
5802
  existing.finished = false;
5709
5803
  existing.revision++;
5710
5804
  delete existing.renderCache;
5805
+ delete existing.lastParsedLength;
5711
5806
  }
5712
5807
  existing.command = command;
5713
5808
  existing.cls = cls;
@@ -5736,21 +5831,24 @@ var bashTool = {
5736
5831
  }
5737
5832
  if (!options.isPartial || isBashTreeClass(cls)) {
5738
5833
  const output = stripBashToolNoticeLines(stripAnsi(getTextOutput(result)));
5739
- const parsed = parseSemanticOutput(cls, output);
5740
5834
  const state = semanticStates.get(context.toolCallId);
5835
+ const shouldParse = !options.isPartial || state === void 0 || state.lastParsedLength === void 0 || output.length - state.lastParsedLength >= PARTIAL_REPARSE_THRESHOLD;
5836
+ const parsed = shouldParse ? parseSemanticOutput(cls, output) : void 0;
5741
5837
  if (parsed) {
5742
5838
  if (state) {
5743
5839
  state.parsed = parsed;
5744
5840
  state.finished = !options.isPartial;
5745
5841
  state.revision++;
5746
5842
  delete state.renderCache;
5843
+ if (options.isPartial) state.lastParsedLength = output.length;
5747
5844
  } else
5748
5845
  semanticStates.set(context.toolCallId, {
5749
5846
  cls,
5750
5847
  command: String(context?.args?.command ?? ""),
5751
5848
  parsed,
5752
5849
  finished: !options.isPartial,
5753
- revision: 0
5850
+ revision: 0,
5851
+ ...options.isPartial ? { lastParsedLength: output.length } : {}
5754
5852
  });
5755
5853
  if (isGitDiffClass(cls)) {
5756
5854
  return renderGitDiffResult(theme, parsed, options, context);
@@ -5760,11 +5858,17 @@ var bashTool = {
5760
5858
  }
5761
5859
  return EMPTY_BASH_TREE_RESULT;
5762
5860
  }
5763
- if (state) {
5764
- state.fallback = true;
5765
- state.finished = !options.isPartial;
5766
- state.revision++;
5767
- delete state.renderCache;
5861
+ if (!shouldParse) {
5862
+ if (state?.parsed !== void 0) return EMPTY_BASH_TREE_RESULT;
5863
+ }
5864
+ if (shouldParse) {
5865
+ if (state) {
5866
+ state.fallback = true;
5867
+ state.finished = !options.isPartial;
5868
+ state.revision++;
5869
+ delete state.renderCache;
5870
+ if (options.isPartial) state.lastParsedLength = output.length;
5871
+ }
5768
5872
  }
5769
5873
  }
5770
5874
  } else if (options.isPartial) {
@@ -5863,14 +5967,8 @@ var editTool = {
5863
5967
  const message = firstText(result.content);
5864
5968
  const argPath = String(context?.args?.path ?? context?.args?.file_path ?? "");
5865
5969
  const sourcePath = details?.path ?? (argPath || extractEditedPath(message));
5866
- const language = sourcePath ? getLanguageFromPath2(sourcePath) : void 0;
5867
- const rows = buildSplitRows(diff);
5868
5970
  const expanded = options.expanded;
5869
- const shouldHighlight = Boolean(language) && diff.length <= MAX_HIGHLIGHT_DIFF_CHARS && rows.length <= MAX_HIGHLIGHT_DIFF_ROWS;
5870
5971
  const stats = countDiffStats(diff);
5871
- const maxRows = expanded ? 160 : 36;
5872
- const diffView = new AdaptiveDiffComponent(theme, rows, maxRows, shouldHighlight ? language : void 0);
5873
- const expandHint2 = !expanded && diffView.hasCollapsed() ? "Ctrl+O more" : void 0;
5874
5972
  return memoizedStateComponent(
5875
5973
  context.state,
5876
5974
  "__piStyleEditDiffResult",
@@ -5882,22 +5980,30 @@ var editTool = {
5882
5980
  sourcePath ?? "",
5883
5981
  editDiffFooter(theme, result, context, stats)
5884
5982
  ),
5885
- () => renderBoxedToolResult(
5886
- theme,
5887
- {
5888
- render(width) {
5889
- return diffView.render(width);
5983
+ () => {
5984
+ const language = sourcePath ? getLanguageFromPath2(sourcePath) : void 0;
5985
+ const rows = buildSplitRows(diff);
5986
+ const shouldHighlight = Boolean(language) && diff.length <= MAX_HIGHLIGHT_DIFF_CHARS && rows.length <= MAX_HIGHLIGHT_DIFF_ROWS;
5987
+ const maxRows = expanded ? 160 : 36;
5988
+ const diffView = new AdaptiveDiffComponent(theme, rows, maxRows, shouldHighlight ? language : void 0);
5989
+ const expandHint2 = !expanded && diffView.hasCollapsed() ? "Ctrl+O more" : void 0;
5990
+ return renderBoxedToolResult(
5991
+ theme,
5992
+ {
5993
+ render(width) {
5994
+ return diffView.render(width);
5995
+ },
5996
+ invalidate() {
5997
+ diffView.invalidate();
5998
+ }
5890
5999
  },
5891
- invalidate() {
5892
- diffView.invalidate();
6000
+ {
6001
+ dividerLabel: diffDividerLabel2(theme, stats),
6002
+ ...expandHint2 ? { dividerRightLabel: expandHint2 } : {},
6003
+ footerLines: [editDiffFooter(theme, result, context, stats)]
5893
6004
  }
5894
- },
5895
- {
5896
- dividerLabel: diffDividerLabel2(theme, stats),
5897
- ...expandHint2 ? { dividerRightLabel: expandHint2 } : {},
5898
- footerLines: [editDiffFooter(theme, result, context, stats)]
5899
- }
5900
- )
6005
+ );
6006
+ }
5901
6007
  );
5902
6008
  }
5903
6009
  };
@@ -6002,14 +6108,16 @@ var findTool = {
6002
6108
  return renderBatchAwareCall(theme, batch);
6003
6109
  },
6004
6110
  result(result, options, _theme, context) {
6005
- const output = stripAnsi(getTextOutput(result)).trimEnd();
6006
- const entries = context.isError ? void 0 : parseFindOutput(output);
6111
+ const isError = Boolean(context.isError);
6112
+ const settled = !options.isPartial && !isError && hasFinalBatchOutput(context.toolCallId);
6113
+ const output = settled ? "" : stripAnsi(getTextOutput(result)).trimEnd();
6114
+ const entries = settled || isError ? void 0 : parseFindOutput(output);
6007
6115
  registerBatchResult(
6008
6116
  FIND_META,
6009
6117
  {
6010
6118
  isPartial: Boolean(options.isPartial),
6011
- isError: Boolean(context.isError),
6012
- errorText: context.isError ? output || void 0 : void 0,
6119
+ isError,
6120
+ errorText: isError ? output || void 0 : void 0,
6013
6121
  ...entries !== void 0 ? { entries } : {}
6014
6122
  },
6015
6123
  context
@@ -6120,14 +6228,19 @@ var grepTool = {
6120
6228
  return renderGrepPanel(theme, context.toolCallId);
6121
6229
  },
6122
6230
  result(result, options, _theme, context) {
6123
- const output = stripAnsi(getTextOutput(result)).trimEnd();
6124
6231
  const isError = Boolean(context.isError);
6232
+ const isPartial = Boolean(options.isPartial);
6233
+ const state = grepPanels.get(context.toolCallId);
6234
+ if (!isPartial && !isError && state !== void 0 && state.matches !== void 0 && !state.isPartial) {
6235
+ return EMPTY_GREP_RESULT;
6236
+ }
6237
+ const output = stripAnsi(getTextOutput(result)).trimEnd();
6125
6238
  const matches = isError ? [] : parseGrepOutput(output);
6126
6239
  registerGrepResult(context.toolCallId, {
6127
6240
  matches,
6128
6241
  isError,
6129
6242
  errorText: isError ? output || void 0 : void 0,
6130
- isPartial: Boolean(options.isPartial)
6243
+ isPartial
6131
6244
  });
6132
6245
  return EMPTY_GREP_RESULT;
6133
6246
  }
@@ -6154,14 +6267,16 @@ var lsTool = {
6154
6267
  return renderBatchAwareCall(theme, batch);
6155
6268
  },
6156
6269
  result(result, options, _theme, context) {
6157
- const output = stripAnsi(getTextOutput(result)).trimEnd();
6158
- const entries = context.isError ? void 0 : parseLsOutput(output);
6270
+ const isError = Boolean(context.isError);
6271
+ const settled = !options.isPartial && !isError && hasFinalBatchOutput(context.toolCallId);
6272
+ const output = settled ? "" : stripAnsi(getTextOutput(result)).trimEnd();
6273
+ const entries = settled || isError ? void 0 : parseLsOutput(output);
6159
6274
  registerBatchResult(
6160
6275
  LIST_META,
6161
6276
  {
6162
6277
  isPartial: Boolean(options.isPartial),
6163
- isError: Boolean(context.isError),
6164
- errorText: context.isError ? output || void 0 : void 0,
6278
+ isError,
6279
+ errorText: isError ? output || void 0 : void 0,
6165
6280
  ...entries !== void 0 ? { entries } : {}
6166
6281
  },
6167
6282
  context
@@ -6285,15 +6400,9 @@ function renderQuickEditResult(_toolName, result, options, theme, context, confi
6285
6400
  footerLines: [quickEditFooter(theme, context)]
6286
6401
  });
6287
6402
  }
6288
- const rows = buildSplitRows(diff);
6289
6403
  const expanded = options.expanded;
6290
6404
  const argPath = String(context?.args?.path ?? "");
6291
- const language = argPath ? getLanguageFromPath3(argPath) : void 0;
6292
- const shouldHighlight = Boolean(language) && diff.length <= MAX_HIGHLIGHT_DIFF_CHARS2 && rows.length <= MAX_HIGHLIGHT_DIFF_ROWS2;
6293
6405
  const stats = countDiffStats(diff);
6294
- const maxRows = expanded ? 160 : 36;
6295
- const diffView = new AdaptiveDiffComponent(theme, rows, maxRows, shouldHighlight ? language : void 0);
6296
- const expandHint2 = !expanded && diffView.hasCollapsed() ? "Ctrl+O more" : void 0;
6297
6406
  return memoizedStateComponent(
6298
6407
  context.state,
6299
6408
  "__piStyleQuickEditDiffResult",
@@ -6306,22 +6415,30 @@ function renderQuickEditResult(_toolName, result, options, theme, context, confi
6306
6415
  argPath,
6307
6416
  quickEditDiffFooter(theme, result, context, stats)
6308
6417
  ),
6309
- () => renderBoxedToolResult(
6310
- theme,
6311
- {
6312
- render(width) {
6313
- return diffView.render(width);
6418
+ () => {
6419
+ const rows = buildSplitRows(diff);
6420
+ const language = argPath ? getLanguageFromPath3(argPath) : void 0;
6421
+ const shouldHighlight = Boolean(language) && diff.length <= MAX_HIGHLIGHT_DIFF_CHARS2 && rows.length <= MAX_HIGHLIGHT_DIFF_ROWS2;
6422
+ const maxRows = expanded ? 160 : 36;
6423
+ const diffView = new AdaptiveDiffComponent(theme, rows, maxRows, shouldHighlight ? language : void 0);
6424
+ const expandHint2 = !expanded && diffView.hasCollapsed() ? "Ctrl+O more" : void 0;
6425
+ return renderBoxedToolResult(
6426
+ theme,
6427
+ {
6428
+ render(width) {
6429
+ return diffView.render(width);
6430
+ },
6431
+ invalidate() {
6432
+ diffView.invalidate();
6433
+ }
6314
6434
  },
6315
- invalidate() {
6316
- diffView.invalidate();
6435
+ {
6436
+ dividerLabel: quickEditDividerLabel(theme, stats),
6437
+ ...expandHint2 ? { dividerRightLabel: expandHint2 } : {},
6438
+ footerLines: [quickEditDiffFooter(theme, result, context, stats)]
6317
6439
  }
6318
- },
6319
- {
6320
- dividerLabel: quickEditDividerLabel(theme, stats),
6321
- ...expandHint2 ? { dividerRightLabel: expandHint2 } : {},
6322
- footerLines: [quickEditDiffFooter(theme, result, context, stats)]
6323
- }
6324
- )
6440
+ );
6441
+ }
6325
6442
  );
6326
6443
  }
6327
6444
  function quickEditFooter(theme, context) {
@@ -6367,13 +6484,13 @@ var readTool = {
6367
6484
  return renderBatchAwareCall(theme, batch);
6368
6485
  },
6369
6486
  result(result, options, _theme, context) {
6370
- const output = stripAnsi(getTextOutput(result)).trimEnd();
6487
+ const errorText = context.isError ? stripAnsi(getTextOutput(result)).trimEnd() || void 0 : void 0;
6371
6488
  registerBatchResult(
6372
6489
  READ_META,
6373
6490
  {
6374
6491
  isPartial: Boolean(options.isPartial),
6375
6492
  isError: Boolean(context.isError),
6376
- errorText: context.isError ? output || void 0 : void 0
6493
+ errorText
6377
6494
  },
6378
6495
  context
6379
6496
  );
@@ -8077,11 +8194,10 @@ function createBuiltinSegments() {
8077
8194
  content: snapshot.sessionStartedAt === void 0 ? "" : theme.apply("time", formatElapsed2(Date.now() - snapshot.sessionStartedAt)),
8078
8195
  compactContent: snapshot.sessionStartedAt === void 0 ? "" : theme.apply("time", formatElapsed2(Date.now() - snapshot.sessionStartedAt))
8079
8196
  })),
8080
- segment("time", 20, ({ theme }) => ({
8081
- visible: true,
8082
- content: theme.apply("time", (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })),
8083
- compactContent: theme.apply("time", (/* @__PURE__ */ new Date()).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }))
8084
- })),
8197
+ segment("time", 20, ({ theme }) => {
8198
+ const text = theme.apply("time", clockTime());
8199
+ return { visible: true, content: text, compactContent: text };
8200
+ }),
8085
8201
  segment("hostname", 20, ({ snapshot, theme }) => ({
8086
8202
  visible: Boolean(snapshot.hostname),
8087
8203
  content: theme.apply("muted", snapshot.hostname ?? "")
@@ -8099,6 +8215,15 @@ function createBuiltinSegments() {
8099
8215
  ];
8100
8216
  return new Map(segments.map((item) => [item.id, item]));
8101
8217
  }
8218
+ var clockCache;
8219
+ function clockTime() {
8220
+ const now = /* @__PURE__ */ new Date();
8221
+ const minuteKey = now.getHours() * 60 + now.getMinutes();
8222
+ if (clockCache?.minuteKey !== minuteKey) {
8223
+ clockCache = { minuteKey, value: now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) };
8224
+ }
8225
+ return clockCache.value;
8226
+ }
8102
8227
  var CONTEXT_BAR_WIDTH = 10;
8103
8228
  function contextBar(percent, width = CONTEXT_BAR_WIDTH) {
8104
8229
  const filled = Math.max(0, Math.min(width, Math.round(percent / 100 * width)));
@@ -8284,7 +8409,24 @@ function colorPrefixFor(active, config, noColor, token) {
8284
8409
  }
8285
8410
  return "";
8286
8411
  }
8412
+ function envKeyFor(env) {
8413
+ return `${env.PI_STYLE_NERD_FONTS ?? ""}\0${env.GHOSTTY_RESOURCES_DIR ? "1" : "0"}\0${env.TERM_PROGRAM ?? ""}`;
8414
+ }
8415
+ var glyphModeCache = /* @__PURE__ */ new WeakMap();
8287
8416
  function detectGlyphMode(config, env = {}) {
8417
+ let byEnv = glyphModeCache.get(config);
8418
+ if (!byEnv) {
8419
+ byEnv = /* @__PURE__ */ new Map();
8420
+ glyphModeCache.set(config, byEnv);
8421
+ }
8422
+ const envKey = envKeyFor(env);
8423
+ const cached = byEnv.get(envKey);
8424
+ if (cached !== void 0) return cached;
8425
+ const mode = computeGlyphMode(config, env);
8426
+ byEnv.set(envKey, mode);
8427
+ return mode;
8428
+ }
8429
+ function computeGlyphMode(config, env) {
8288
8430
  if (env.PI_STYLE_NERD_FONTS === "1") return "nerd";
8289
8431
  if (env.PI_STYLE_NERD_FONTS === "0") return "unicode";
8290
8432
  if (config.theme.nerdFonts === "on") return "nerd";
@@ -8299,7 +8441,14 @@ function detectGlyphMode(config, env = {}) {
8299
8441
  function resolveTheme(active, config, env = {}) {
8300
8442
  const noColor = Object.hasOwn(env, "NO_COLOR") && env.NO_COLOR !== "" && config.theme.colors.colorOverride !== "on";
8301
8443
  const mode = config.preset === "ascii" ? "ascii" : detectGlyphMode(config, env);
8302
- const color = (token) => colorPrefixFor(active, config, noColor, token);
8444
+ const prefixes = /* @__PURE__ */ new Map();
8445
+ const color = (token) => {
8446
+ const cached = prefixes.get(token);
8447
+ if (cached !== void 0) return cached;
8448
+ const prefix = colorPrefixFor(active, config, noColor, token);
8449
+ prefixes.set(token, prefix);
8450
+ return prefix;
8451
+ };
8303
8452
  return {
8304
8453
  mode,
8305
8454
  noColor,
@@ -8317,9 +8466,13 @@ function resolveTheme(active, config, env = {}) {
8317
8466
  var widthOf = visibleWidth3;
8318
8467
  function widthSafe(value, width) {
8319
8468
  if (width <= 0) return "";
8320
- const fitted = widthOf(value) > width ? truncateAnsi(value, width, "") : value;
8321
- const current = widthOf(fitted);
8322
- return current < width ? fitted + " ".repeat(width - current) : fitted;
8469
+ const measured = widthOf(value);
8470
+ if (measured > width) {
8471
+ const fitted = truncateAnsi(value, width, "");
8472
+ const current = widthOf(fitted);
8473
+ return current < width ? fitted + " ".repeat(width - current) : fitted;
8474
+ }
8475
+ return measured < width ? value + " ".repeat(width - measured) : value;
8323
8476
  }
8324
8477
  function isNativeBorderLine(line) {
8325
8478
  const stripped = stripAnsi(line);
@@ -8389,6 +8542,8 @@ var StyledEditor = class extends CustomEditor {
8389
8542
  fullTheme;
8390
8543
  onSnapshot;
8391
8544
  semantic;
8545
+ /** Set when config changes; invalidate() then rebuilds the (otherwise stable) semantic theme. */
8546
+ semanticDirty = false;
8392
8547
  disposed = false;
8393
8548
  renderPlanCache;
8394
8549
  constructor(tui, theme, keybindings, options) {
@@ -8409,7 +8564,7 @@ var StyledEditor = class extends CustomEditor {
8409
8564
  configure(config) {
8410
8565
  if (this.disposed) return;
8411
8566
  this.config = config;
8412
- this.semantic = semanticTheme(this.piTheme, config);
8567
+ this.semanticDirty = true;
8413
8568
  this.invalidate();
8414
8569
  }
8415
8570
  handleInput(data) {
@@ -8419,7 +8574,10 @@ var StyledEditor = class extends CustomEditor {
8419
8574
  }
8420
8575
  invalidate() {
8421
8576
  super.invalidate();
8422
- this.semantic = semanticTheme(this.piTheme, this.config);
8577
+ if (this.semanticDirty) {
8578
+ this.semantic = semanticTheme(this.piTheme, this.config);
8579
+ this.semanticDirty = false;
8580
+ }
8423
8581
  this.renderPlanCache = void 0;
8424
8582
  this.tui.requestRender();
8425
8583
  }
@@ -9203,9 +9361,6 @@ function uniqueLayout(layout) {
9203
9361
  function renderGroup(items, separator, padding) {
9204
9362
  return items.map((item) => item.content).filter(Boolean).join(`${padding}${separator}${padding}`);
9205
9363
  }
9206
- function widthOf2(items, separator, padding) {
9207
- return visibleWidth(renderGroup(items, separator, padding));
9208
- }
9209
9364
  function renderStatus(layout, snapshot, width, options) {
9210
9365
  if (width <= 0) return { primary: "", lines: [], visibleSegments: [] };
9211
9366
  const separator = options.separator ?? "\u2502";
@@ -9220,7 +9375,15 @@ function renderStatus(layout, snapshot, width, options) {
9220
9375
  try {
9221
9376
  const result = segment2.render(context);
9222
9377
  if (!result.visible || !result.content) continue;
9223
- candidates.set(id, { id, segment: segment2, result, content: result.content, compact: false, moved: false });
9378
+ candidates.set(id, {
9379
+ id,
9380
+ segment: segment2,
9381
+ result,
9382
+ content: result.content,
9383
+ contentWidth: visibleWidth(result.content),
9384
+ compact: false,
9385
+ moved: false
9386
+ });
9224
9387
  } catch {
9225
9388
  }
9226
9389
  }
@@ -9230,13 +9393,28 @@ function renderStatus(layout, snapshot, width, options) {
9230
9393
  const secondary = normalized.secondary.map((id) => candidates.get(id)).filter((candidate) => candidate !== void 0);
9231
9394
  const visible = [];
9232
9395
  const overflow = [];
9396
+ const gapWidth = visibleWidth(`${padding}${separator}${padding}`);
9397
+ let groupWidth = 0;
9398
+ let groupCount = 0;
9399
+ const widthAfter = (baseWidth, count, candidate) => count === 0 ? candidate.contentWidth : baseWidth + gapWidth + candidate.contentWidth;
9233
9400
  for (const candidate of primary) {
9234
9401
  visible.push(candidate);
9235
- if (widthOf2(visible, separator, padding) <= width) continue;
9402
+ const pushedWidth = widthAfter(groupWidth, groupCount, candidate);
9403
+ if (pushedWidth <= width) {
9404
+ groupWidth = pushedWidth;
9405
+ groupCount++;
9406
+ continue;
9407
+ }
9236
9408
  if (candidate.result.compactContent && !candidate.compact) {
9237
9409
  candidate.content = candidate.result.compactContent;
9238
9410
  candidate.compact = true;
9239
- if (widthOf2(visible, separator, padding) <= width) continue;
9411
+ candidate.contentWidth = visibleWidth(candidate.content);
9412
+ const compactedWidth = widthAfter(groupWidth, groupCount, candidate);
9413
+ if (compactedWidth <= width) {
9414
+ groupWidth = compactedWidth;
9415
+ groupCount++;
9416
+ continue;
9417
+ }
9240
9418
  }
9241
9419
  visible.pop();
9242
9420
  if (candidate.segment.overflow !== "drop" && candidate.segment.overflow !== "primary") {
@@ -9262,9 +9440,14 @@ function renderStatus(layout, snapshot, width, options) {
9262
9440
  }
9263
9441
  if (visibleWidth(primaryText) > width) primaryText = truncateAnsi(primaryText, width);
9264
9442
  const secondaryVisible = [];
9443
+ let secondaryWidth = 0;
9444
+ let secondaryCount = 0;
9265
9445
  for (const candidate of [...secondary].sort((a, b) => b.segment.defaultPriority - a.segment.defaultPriority)) {
9446
+ const pushedWidth = widthAfter(secondaryWidth, secondaryCount, candidate);
9447
+ if (pushedWidth > width) continue;
9266
9448
  secondaryVisible.push(candidate);
9267
- if (widthOf2(secondaryVisible, separator, padding) > width) secondaryVisible.pop();
9449
+ secondaryWidth = pushedWidth;
9450
+ secondaryCount++;
9268
9451
  }
9269
9452
  const secondaryText = renderGroup(secondaryVisible, separator, padding);
9270
9453
  const lines = secondaryText ? [primaryText, secondaryText] : primaryText ? [primaryText] : [];
@@ -9317,6 +9500,28 @@ function separatorsFor(config, theme) {
9317
9500
  if (style === "none") return " ";
9318
9501
  return theme.apply("separator", style);
9319
9502
  }
9503
+ var themeAssetsCache = /* @__PURE__ */ new WeakMap();
9504
+ function themeAssetsFor(activeTheme, config) {
9505
+ let byConfig = themeAssetsCache.get(activeTheme);
9506
+ if (!byConfig) {
9507
+ byConfig = /* @__PURE__ */ new WeakMap();
9508
+ themeAssetsCache.set(activeTheme, byConfig);
9509
+ }
9510
+ let assets = byConfig.get(config);
9511
+ if (!assets) {
9512
+ const resolved = resolveTheme(
9513
+ activeTheme.colors || activeTheme.fg ? {
9514
+ ...activeTheme.colors ? { colors: activeTheme.colors } : {},
9515
+ // Call through the theme instance so `this` binds correctly inside Pi's fg().
9516
+ ...activeTheme.fg ? { fg: (color, text) => activeTheme.fg?.(color, text) ?? text } : {}
9517
+ } : void 0,
9518
+ config
9519
+ );
9520
+ assets = { resolved, separator: separatorsFor(config, resolved) };
9521
+ byConfig.set(config, assets);
9522
+ }
9523
+ return assets;
9524
+ }
9320
9525
  function installStatusLine(options) {
9321
9526
  const existing = installationMap(options.host).get(options.generation);
9322
9527
  if (existing) return existing;
@@ -9349,16 +9554,9 @@ function installStatusLine(options) {
9349
9554
  }
9350
9555
  const render = (activeTheme, width, secondary) => {
9351
9556
  if (width <= 0 || !config.enabled || !config.statusLine.enabled) return [];
9352
- const resolved = resolveTheme(
9353
- activeTheme.colors || activeTheme.fg ? {
9354
- ...activeTheme.colors ? { colors: activeTheme.colors } : {},
9355
- // Call through the theme instance so `this` binds correctly inside Pi's fg().
9356
- ...activeTheme.fg ? { fg: (color, text) => activeTheme.fg?.(color, text) ?? text } : {}
9357
- } : void 0,
9358
- config
9359
- );
9557
+ const { resolved, separator } = themeAssetsFor(activeTheme, config);
9360
9558
  const result = renderStatus(config.statusLine.layout, effectiveSnapshot(snapshot), width, {
9361
- separator: separatorsFor(config, resolved),
9559
+ separator,
9362
9560
  segments,
9363
9561
  theme: resolved,
9364
9562
  options: {
@@ -9439,12 +9637,16 @@ function installStatusLine(options) {
9439
9637
  };
9440
9638
  const factory = (secondary) => (tui, theme) => {
9441
9639
  const currentTheme = theme;
9640
+ let renderCache;
9442
9641
  const component = {
9443
9642
  render(width) {
9643
+ if (renderCache?.width === width) return renderCache.lines;
9444
9644
  const lines = render(currentTheme, width, secondary);
9645
+ renderCache = { width, lines };
9445
9646
  return lines;
9446
9647
  },
9447
9648
  invalidate() {
9649
+ renderCache = void 0;
9448
9650
  primaryComponent = secondary ? primaryComponent : component;
9449
9651
  secondaryComponent = secondary ? component : secondaryComponent;
9450
9652
  if (tui.requestRender) tui.requestRender();
@@ -9862,6 +10064,7 @@ function isPlainRecord(value) {
9862
10064
  }
9863
10065
 
9864
10066
  // extension-src/pi-style/app/runtime.ts
10067
+ var GIT_INVALIDATE_DEBOUNCE_MS = 250;
9865
10068
  function createPiStyleRuntime(host, generation2, requestRender = host.requestRender ?? (() => {
9866
10069
  })) {
9867
10070
  let disposed = false;
@@ -9887,6 +10090,7 @@ function createPiStyleRuntime(host, generation2, requestRender = host.requestRen
9887
10090
  const disposables = new DisposableStore();
9888
10091
  const scheduler = new RenderScheduler({ requestRender }, generation2, () => !disposed);
9889
10092
  const git = new CachedGitProvider(host.gitRunner);
10093
+ let pendingGitRefresh;
9890
10094
  const contextProvider = new InMemoryContextProvider();
9891
10095
  const usageProvider = new InMemoryUsageProvider();
9892
10096
  const initialContext = contextSnapshot();
@@ -10119,11 +10323,18 @@ function createPiStyleRuntime(host, generation2, requestRender = host.requestRen
10119
10323
  },
10120
10324
  invalidateGit() {
10121
10325
  if (disposed || !host.cwd || !currentConfig.enabled || !currentConfig.statusLine.enabled) return;
10122
- git.invalidate(host.cwd);
10123
- void git.get(host.cwd).then((value) => {
10326
+ const cwd = host.cwd;
10327
+ git.invalidate(cwd);
10328
+ if (pendingGitRefresh !== void 0) return;
10329
+ pendingGitRefresh = setTimeout(() => {
10330
+ pendingGitRefresh = void 0;
10124
10331
  if (disposed) return;
10125
- if (updateSnapshot(withSnapshotPatch({ git: value }))) requestRender();
10126
- });
10332
+ void git.get(cwd).then((value) => {
10333
+ if (disposed) return;
10334
+ if (updateSnapshot(withSnapshotPatch({ git: value }))) requestRender();
10335
+ });
10336
+ }, GIT_INVALIDATE_DEBOUNCE_MS);
10337
+ pendingGitRefresh.unref?.();
10127
10338
  },
10128
10339
  get disposed() {
10129
10340
  return disposed;
@@ -10131,6 +10342,10 @@ function createPiStyleRuntime(host, generation2, requestRender = host.requestRen
10131
10342
  dispose() {
10132
10343
  if (disposed) return;
10133
10344
  disposed = true;
10345
+ if (pendingGitRefresh !== void 0) {
10346
+ clearTimeout(pendingGitRefresh);
10347
+ pendingGitRefresh = void 0;
10348
+ }
10134
10349
  scheduler.cancel();
10135
10350
  disposeStatus();
10136
10351
  disposeEditor();
@@ -10684,6 +10899,8 @@ var MAX_RENDER_CACHE_KEYS_PER_INSTANCE = 8;
10684
10899
  var MAX_LINE_ANALYSIS_ENTRIES = 4096;
10685
10900
  var renderCacheByInstance = /* @__PURE__ */ new WeakMap();
10686
10901
  var lineAnalysisCache = /* @__PURE__ */ new Map();
10902
+ var lineCacheEvictionCursor;
10903
+ var childrenScanByInstance = /* @__PURE__ */ new WeakMap();
10687
10904
  var messageDecorationTestState = {
10688
10905
  decoratePasses: 0,
10689
10906
  cacheHits: 0,
@@ -10706,7 +10923,7 @@ function splitLeadingMarkers(line) {
10706
10923
  if (end === -1) break;
10707
10924
  index = end + 1;
10708
10925
  }
10709
- return { head: line.slice(0, index), rest: line.slice(index) };
10926
+ return index === 0 ? { head: "", rest: line } : { head: line.slice(0, index), rest: line.slice(index) };
10710
10927
  }
10711
10928
  function leadingSgr(line) {
10712
10929
  if (!line.startsWith("\x1B[")) return "";
@@ -10720,41 +10937,153 @@ function leadingSgr(line) {
10720
10937
  }
10721
10938
  function isBackgroundSgr(sequence) {
10722
10939
  if (!sequence.startsWith("\x1B[") || !sequence.endsWith("m")) return false;
10723
- for (const code of sequence.slice(2, -1).split(";")) {
10724
- const value = Number(code);
10725
- if (value === 48 || value === 49) return true;
10726
- if (value >= 40 && value <= 47) return true;
10727
- if (value >= 100 && value <= 107) return true;
10940
+ let value = 0;
10941
+ let empty = true;
10942
+ let valid = true;
10943
+ for (let index = 2; index < sequence.length - 1; index++) {
10944
+ const code = sequence.charCodeAt(index);
10945
+ if (code === 59) {
10946
+ if (valid && matchesBackgroundCode(empty ? 0 : value)) return true;
10947
+ value = 0;
10948
+ empty = true;
10949
+ valid = true;
10950
+ continue;
10951
+ }
10952
+ if (code < 48 || code > 57) {
10953
+ valid = false;
10954
+ continue;
10955
+ }
10956
+ value = value * 10 + (code - 48);
10957
+ empty = false;
10728
10958
  }
10729
- return false;
10959
+ return valid && matchesBackgroundCode(empty ? 0 : value);
10960
+ }
10961
+ function matchesBackgroundCode(value) {
10962
+ return value === 48 || value === 49 || value >= 40 && value <= 47 || value >= 100 && value <= 107;
10963
+ }
10964
+ function certifiedAsciiWidth(value) {
10965
+ let width = 0;
10966
+ let index = 0;
10967
+ const length = value.length;
10968
+ while (index < length) {
10969
+ const code = value.charCodeAt(index);
10970
+ if (code === 27) {
10971
+ const next = index + 1 < length ? value.charCodeAt(index + 1) : -1;
10972
+ if (next === 91) {
10973
+ let scan = index + 2;
10974
+ while (scan < length) {
10975
+ const terminator = value.charCodeAt(scan);
10976
+ if (terminator === 109 || // m
10977
+ terminator === 71 || // G
10978
+ terminator === 75 || // K
10979
+ terminator === 72 || // H
10980
+ terminator === 74) {
10981
+ index = scan + 1;
10982
+ break;
10983
+ }
10984
+ scan++;
10985
+ }
10986
+ if (scan >= length) return void 0;
10987
+ continue;
10988
+ }
10989
+ if (next === 93 || next === 95) {
10990
+ let scan = index + 2;
10991
+ let end = -1;
10992
+ while (scan < length) {
10993
+ const terminator = value.charCodeAt(scan);
10994
+ if (terminator === 7) {
10995
+ end = scan + 1;
10996
+ break;
10997
+ }
10998
+ if (terminator === 27 && scan + 1 < length && value.charCodeAt(scan + 1) === 92) {
10999
+ end = scan + 2;
11000
+ break;
11001
+ }
11002
+ scan++;
11003
+ }
11004
+ if (end < 0) return void 0;
11005
+ index = end;
11006
+ continue;
11007
+ }
11008
+ return void 0;
11009
+ }
11010
+ if (code < 32 || code > 126) return void 0;
11011
+ width++;
11012
+ index++;
11013
+ }
11014
+ return width;
11015
+ }
11016
+ function certifiedVisibleWidth(value) {
11017
+ return certifiedAsciiWidth(value) ?? visibleWidth(value);
11018
+ }
11019
+ var prefixWidthMemo;
11020
+ function prefixWidthOf(prefix) {
11021
+ if (prefixWidthMemo?.prefix === prefix) return prefixWidthMemo.width;
11022
+ const width = certifiedVisibleWidth(prefix);
11023
+ prefixWidthMemo = { prefix, width };
11024
+ return width;
10730
11025
  }
10731
11026
  function contentText(line) {
11027
+ if (!line.includes("\x1B")) return line;
10732
11028
  let output = "";
11029
+ let sliceStart = 0;
10733
11030
  for (let index = 0; index < line.length; index++) {
10734
- if (line.charCodeAt(index) !== 27) {
10735
- output += line[index];
10736
- continue;
10737
- }
11031
+ if (line.charCodeAt(index) !== 27) continue;
11032
+ output += line.slice(sliceStart, index);
10738
11033
  const next = line[index + 1];
10739
11034
  if (next === "]") {
10740
11035
  index += 2;
10741
11036
  while (index < line.length && line.charCodeAt(index) !== 7) index++;
10742
- continue;
10743
- }
10744
- if (next === "[") {
11037
+ } else if (next === "[") {
10745
11038
  index += 2;
10746
11039
  while (index < line.length && (line.charCodeAt(index) < 64 || line.charCodeAt(index) > 126)) index++;
10747
11040
  }
11041
+ sliceStart = index + 1;
10748
11042
  }
10749
- return output.replaceAll(OSC133_ZONE_START, "").replaceAll(OSC133_ZONE_END, "").replaceAll(OSC133_ZONE_FINAL, "");
11043
+ output += line.slice(sliceStart);
11044
+ if (output.includes("\x1B]133;")) {
11045
+ return output.replaceAll(OSC133_ZONE_START, "").replaceAll(OSC133_ZONE_END, "").replaceAll(OSC133_ZONE_FINAL, "");
11046
+ }
11047
+ return output;
11048
+ }
11049
+ function isWhitespaceCode(code) {
11050
+ if (code === 32 || code >= 9 && code <= 13) return true;
11051
+ if (code < 128) return false;
11052
+ return code === 160 || code === 5760 || code >= 8192 && code <= 8202 || code === 8232 || code === 8233 || code === 8239 || code === 8287 || code === 12288 || code === 65279;
10750
11053
  }
10751
11054
  function hasContent(line) {
10752
- return [...contentText(line)].some((character) => !/\s/u.test(character));
11055
+ const length = line.length;
11056
+ let index = 0;
11057
+ while (index < length) {
11058
+ const code = line.charCodeAt(index);
11059
+ if (code === 27) {
11060
+ const next = index + 1 < length ? line.charCodeAt(index + 1) : -1;
11061
+ if (next === 93) {
11062
+ index += 2;
11063
+ while (index < length && line.charCodeAt(index) !== 7) index++;
11064
+ } else if (next === 91) {
11065
+ index += 2;
11066
+ while (index < length) {
11067
+ const inner = line.charCodeAt(index);
11068
+ if (inner >= 64 && inner <= 126) break;
11069
+ index++;
11070
+ }
11071
+ }
11072
+ index++;
11073
+ continue;
11074
+ }
11075
+ if (code >= 55296 && code <= 57343) return true;
11076
+ if (!isWhitespaceCode(code)) return true;
11077
+ index++;
11078
+ }
11079
+ return false;
10753
11080
  }
10754
11081
  function getLineAnalysis(line) {
10755
11082
  const cached = lineAnalysisCache.get(line);
10756
11083
  if (cached) {
10757
11084
  messageDecorationTestState.lineCacheHits++;
11085
+ lineAnalysisCache.delete(line);
11086
+ lineAnalysisCache.set(line, cached);
10758
11087
  return cached;
10759
11088
  }
10760
11089
  messageDecorationTestState.lineCacheMisses++;
@@ -10762,21 +11091,29 @@ function getLineAnalysis(line) {
10762
11091
  const backgroundAnsi = leadingSgr(leadingMarkers.rest);
10763
11092
  const isBackgroundWrapped = backgroundAnsi !== "" && isBackgroundSgr(backgroundAnsi) && leadingMarkers.rest.endsWith(BG_RESET);
10764
11093
  const backgroundBody = isBackgroundWrapped ? leadingMarkers.rest.slice(backgroundAnsi.length, leadingMarkers.rest.length - BG_RESET.length) : void 0;
11094
+ const hasOscStart = line.startsWith(OSC133_ZONE_START);
10765
11095
  const analysis = {
10766
- visibleWidth: visibleWidth(line),
11096
+ visibleWidth: certifiedVisibleWidth(line),
10767
11097
  hasContent: hasContent(line),
10768
- oscEnvelope: extractOscEnvelope(line),
10769
- hasOscStart: line.startsWith(OSC133_ZONE_START),
11098
+ oscEnvelope: hasOscStart ? extractOscEnvelope(line) : void 0,
11099
+ hasOscStart,
10770
11100
  leadingMarkers,
10771
11101
  isBackgroundWrapped,
10772
11102
  backgroundAnsi,
10773
11103
  backgroundBody,
10774
- backgroundBodyWidth: backgroundBody === void 0 ? void 0 : visibleWidth(backgroundBody)
11104
+ backgroundBodyWidth: backgroundBody === void 0 ? void 0 : certifiedVisibleWidth(backgroundBody)
10775
11105
  };
10776
11106
  lineAnalysisCache.set(line, analysis);
10777
11107
  if (lineAnalysisCache.size > MAX_LINE_ANALYSIS_ENTRIES) {
10778
- const oldestKey = lineAnalysisCache.keys().next().value;
10779
- if (oldestKey !== void 0) lineAnalysisCache.delete(oldestKey);
11108
+ let cursor = lineCacheEvictionCursor;
11109
+ if (cursor === void 0) cursor = lineAnalysisCache.keys();
11110
+ let oldest = cursor.next();
11111
+ if (oldest.done) {
11112
+ cursor = lineAnalysisCache.keys();
11113
+ oldest = cursor.next();
11114
+ }
11115
+ lineCacheEvictionCursor = cursor;
11116
+ if (!oldest.done && oldest.value !== void 0) lineAnalysisCache.delete(oldest.value);
10780
11117
  }
10781
11118
  return analysis;
10782
11119
  }
@@ -10789,8 +11126,8 @@ function rebuildAtWidth(line, width, lead, leadWidth, analysis = getLineAnalysis
10789
11126
  return `${lead}${line}${pad}`;
10790
11127
  }
10791
11128
  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) : "";
11129
+ const { firstEnvelope, firstHasStart, multilineEnvelope, prefix, prefixWidth, continuationLead } = options;
11130
+ const lead = index === contentIndex ? prefix : index > contentIndex ? continuationLead : "";
10794
11131
  const leadWidth = index < contentIndex ? 0 : prefixWidth;
10795
11132
  if (index === contentIndex && firstEnvelope)
10796
11133
  return `${firstEnvelope.start}${rebuildAtWidth(firstEnvelope.body, width, prefix, prefixWidth)}${firstEnvelope.end}`;
@@ -10846,32 +11183,58 @@ function prefixNative(lines, width, prefix) {
10846
11183
  if (!Array.isArray(lines) || lines.length === 0 || !lines.every((line) => typeof line === "string")) return void 0;
10847
11184
  messageDecorationTestState.decoratePasses++;
10848
11185
  const nativeLines = lines;
10849
- const prefixWidth = visibleWidth(prefix);
11186
+ const prefixWidth = prefixWidthOf(prefix);
10850
11187
  if (width <= prefixWidth) return void 0;
10851
11188
  const bodyWidth = width - prefixWidth;
10852
- const first = nativeLines[0] ?? "";
11189
+ const analyses = nativeLines.map((line) => getLineAnalysis(line));
10853
11190
  const last = nativeLines.at(-1) ?? "";
10854
- const lastAnalysis = getLineAnalysis(last);
11191
+ const lastAnalysis = analyses[analyses.length - 1] ?? getLineAnalysis(last);
10855
11192
  const multilineEnvelope = nativeLines.length > 1 && last.startsWith(OSC133_ZONE_END + OSC133_ZONE_FINAL);
10856
- const firstContentIndex = nativeLines.findIndex((line, index) => {
10857
- if (index !== nativeLines.length - 1 || !multilineEnvelope) return getLineAnalysis(line).hasContent;
10858
- return !nativeLines.slice(0, index).some((earlier) => getLineAnalysis(earlier).hasContent) && lastAnalysis.hasContent;
10859
- });
11193
+ let firstContentIndex = -1;
11194
+ for (let index = 0; index < nativeLines.length; index++) {
11195
+ const analysis = analyses[index] ?? getLineAnalysis(nativeLines[index] ?? "");
11196
+ if (index !== nativeLines.length - 1 || !multilineEnvelope) {
11197
+ if (analysis.hasContent) {
11198
+ firstContentIndex = index;
11199
+ break;
11200
+ }
11201
+ continue;
11202
+ }
11203
+ let earlierHasContent = false;
11204
+ for (let earlier = 0; earlier < index; earlier++) {
11205
+ if ((analyses[earlier] ?? getLineAnalysis(nativeLines[earlier] ?? "")).hasContent) {
11206
+ earlierHasContent = true;
11207
+ break;
11208
+ }
11209
+ }
11210
+ if (!earlierHasContent && lastAnalysis.hasContent) firstContentIndex = index;
11211
+ break;
11212
+ }
10860
11213
  if (firstContentIndex < 0) return nativeLines;
10861
- const firstAnalysis = getLineAnalysis(first);
11214
+ const firstAnalysis = analyses[0] ?? getLineAnalysis(nativeLines[0] ?? "");
10862
11215
  const firstEnvelope = firstContentIndex === 0 ? firstAnalysis.oscEnvelope : void 0;
10863
11216
  const firstHasStart = firstContentIndex === 0 && firstAnalysis.hasOscStart;
11217
+ const continuationLead = " ".repeat(prefixWidth);
10864
11218
  const decorated = nativeLines.map(
10865
- (line, index) => decorateMessageLine(line, index, nativeLines.length - 1, firstContentIndex, width, {
10866
- firstEnvelope,
10867
- firstHasStart,
10868
- multilineEnvelope,
10869
- prefix,
10870
- prefixWidth
10871
- })
11219
+ (line, index) => decorateMessageLine(
11220
+ line,
11221
+ index,
11222
+ nativeLines.length - 1,
11223
+ firstContentIndex,
11224
+ width,
11225
+ {
11226
+ firstEnvelope,
11227
+ firstHasStart,
11228
+ multilineEnvelope,
11229
+ prefix,
11230
+ prefixWidth,
11231
+ continuationLead
11232
+ },
11233
+ analyses[index]
11234
+ )
10872
11235
  );
10873
- if (!decorated.every((line) => visibleWidth(line) <= width)) return void 0;
10874
- if (!nativeLines.every((line) => getLineAnalysis(line).visibleWidth <= bodyWidth)) return void 0;
11236
+ if (!decorated.every((line) => certifiedVisibleWidth(line) <= width)) return void 0;
11237
+ if (!analyses.every((analysis) => analysis.visibleWidth <= bodyWidth)) return void 0;
10875
11238
  return decorated;
10876
11239
  }
10877
11240
  function decorateMessageRender(original, instance, args, snapshot = {
@@ -10884,7 +11247,7 @@ function decorateMessageRender(original, instance, args, snapshot = {
10884
11247
  const width = typeof args[0] === "number" ? args[0] : 0;
10885
11248
  const prefix = snapshot.assistantPrefix;
10886
11249
  if (!snapshot.assistantEnabled) return Reflect.apply(original, instance, args);
10887
- const prefixWidth = visibleWidth(prefix);
11250
+ const prefixWidth = prefixWidthOf(prefix);
10888
11251
  if (width <= prefixWidth) return Reflect.apply(original, instance, args);
10889
11252
  const reducedWidth = width - prefixWidth;
10890
11253
  const native = Reflect.apply(original, instance, [reducedWidth, ...args.slice(1)]);
@@ -10916,8 +11279,16 @@ function hasToolCallItems(message) {
10916
11279
  function isBlankTextChild(child) {
10917
11280
  const candidate = child;
10918
11281
  if (typeof candidate?.setCustomBgFn !== "function" || typeof candidate.render !== "function") return false;
11282
+ if (typeof candidate.text === "string") return contentText(candidate.text).trim() === "";
10919
11283
  return contentText(candidate.render(0).join("\n")).trim() === "";
10920
11284
  }
11285
+ function childrenUnchangedSinceScan(instance, children) {
11286
+ const state = childrenScanByInstance.get(instance);
11287
+ return state !== void 0 && state.childrenRef === children && state.length === children.length;
11288
+ }
11289
+ function markChildrenScanned(instance, children) {
11290
+ childrenScanByInstance.set(instance, { childrenRef: children, length: children.length });
11291
+ }
10921
11292
  function decorateMessageUpdate(original, instance, args, snapshot = {
10922
11293
  assistantPrefix: "\u2502 ",
10923
11294
  assistantEnabled: true,
@@ -10927,24 +11298,22 @@ function decorateMessageUpdate(original, instance, args, snapshot = {
10927
11298
  if (typeof original !== "function") return void 0;
10928
11299
  const result = Reflect.apply(original, instance, args);
10929
11300
  const target = instance;
10930
- if (snapshot.collapseHiddenThinking && target.hideThinkingBlock === true && target.hiddenThinkingLabel === "") {
10931
- const children = target.contentContainer?.children;
10932
- if (children) {
11301
+ const children = target.contentContainer?.children;
11302
+ if (children && !childrenUnchangedSinceScan(instance, children)) {
11303
+ if (snapshot.collapseHiddenThinking && target.hideThinkingBlock === true && target.hiddenThinkingLabel === "") {
10933
11304
  for (let index = children.length - 1; index >= 0; index--) {
10934
11305
  if (!isBlankTextChild(children[index])) continue;
10935
11306
  children.splice(index, 1);
10936
11307
  if (isSpacerChild(children[index])) children.splice(index, 1);
10937
11308
  }
10938
11309
  }
10939
- }
10940
- if (snapshot.hideInterimText && hasToolCallItems(args[0])) {
10941
- const children = target.contentContainer?.children;
10942
- if (children) {
11310
+ if (snapshot.hideInterimText && hasToolCallItems(args[0])) {
10943
11311
  for (let index = children.length - 1; index >= 0; index--) {
10944
11312
  if (isInterimTextChild(children[index])) children.splice(index, 1);
10945
11313
  }
10946
11314
  if (children.every((child) => isSpacerChild(child))) children.length = 0;
10947
11315
  }
11316
+ markChildrenScanned(instance, children);
10948
11317
  }
10949
11318
  return result;
10950
11319
  }
@@ -12363,6 +12732,7 @@ function piStyleExtension(pi) {
12363
12732
  const run = finishAgentRun();
12364
12733
  if (run) {
12365
12734
  invalidateTurnMembers(run);
12735
+ releaseTurnInvalidators(run);
12366
12736
  requestToolPresentationRender();
12367
12737
  }
12368
12738
  });