pi-claude-code-ui 1.0.79 → 1.0.83

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.
@@ -34,6 +34,7 @@ import {
34
34
  getImageDimensions,
35
35
  imageFallback,
36
36
  Markdown,
37
+ ProcessTerminal,
37
38
  Spacer,
38
39
  Text,
39
40
  truncateToWidth,
@@ -44,6 +45,14 @@ import {
44
45
  import * as Diff from "diff";
45
46
  import type { BundledLanguage, BundledTheme } from "shiki";
46
47
 
48
+ import {
49
+ buildBashCommandPresentation,
50
+ buildBashPreview,
51
+ describeBashSource,
52
+ formatBashDuration,
53
+ getLastBashOutputLine,
54
+ } from "./bash-command";
55
+
47
56
  const RESET = "\x1b[0m";
48
57
  const TRANSPARENT_BG = "\x1b[49m";
49
58
  const TRANSPARENT_RESET = `${RESET}${TRANSPARENT_BG}`;
@@ -67,6 +76,8 @@ const CUSTOM_MESSAGE_PATCH_FLAG = Symbol.for("pi-claude-style-tools:patched-cust
67
76
  const USER_MESSAGE_PATCH_FLAG = Symbol.for("pi-claude-style-tools:patched-user-message-render");
68
77
  const UI_NOTIFY_PATCH_FLAG = Symbol.for("pi-claude-style-tools:patched-ui-notifications-v2");
69
78
  const WRAP_MARK = "\uE000";
79
+ const CLIP_MARK = "\uE001";
80
+ const TRAILING_MARK = " · ";
70
81
  const KITTY_IMAGE_PREFIX = "\x1b_G";
71
82
  const ITERM2_IMAGE_PREFIX = "\x1b]1337;File=";
72
83
 
@@ -84,6 +95,8 @@ interface SettingsFile {
84
95
  groupToolCalls?: boolean;
85
96
  bashOutputMode?: "opencode" | "summary" | "preview";
86
97
  bashCollapsedLines?: number;
98
+ /** Verbatim script lines shown while bash is running or after failure. Defaults to 8. */
99
+ bashCommandPreviewLines?: number;
87
100
  /** Show a small live output preview while tools are still running. Defaults to true. */
88
101
  liveToolPreview?: boolean;
89
102
  /** Number of live output lines to show while collapsed. Defaults to 5. */
@@ -112,6 +125,13 @@ interface SettingsFile {
112
125
  * "(thinking · ↓ 10 tokens · 2s)" trailer). Defaults to "muted".
113
126
  */
114
127
  spinnerStatusColor?: string;
128
+ /**
129
+ * Thinking display mode. `live` (default): only the currently-streaming
130
+ * thinking is expanded; finished thinking collapses to a one-line
131
+ * `Thought for Xs` row (Ctrl+O still expands it). `full`: thinking always
132
+ * renders expanded, like stock pi.
133
+ */
134
+ thinkingMode?: "live" | "full";
115
135
  /** Gray level 0–255 for ├ └ │ when branch color mode is `fixed`. */
116
136
  toolBranchRgbGray?: number;
117
137
  /** `fixed` (default): rgb gray 72, theme-independent. `theme`: dim → muted → borderMuted. */
@@ -478,6 +498,49 @@ function setToolGroupingEnabled(enabled: boolean): void {
478
498
  writeSettingsKey("groupToolCalls", enabled);
479
499
  }
480
500
 
501
+ type ThinkingMode = "live" | "full";
502
+
503
+ function getThinkingMode(): ThinkingMode {
504
+ return getMode(readSettings().thinkingMode, ["live", "full"] as const, "live");
505
+ }
506
+
507
+ function isAssistantThinkingComplete(comp: any, message: any): boolean {
508
+ if (!message || message.role !== "assistant") return false;
509
+ if (typeof message[THINKING_DURATION_KEY] === "number") return true;
510
+ if (message[THINKING_ACTIVE_KEY]) return false;
511
+ // Providers keep stopReason "pending" for the whole stream ("deferred" while
512
+ // a deferred call is unresolved); both are in-flight sentinels, never
513
+ // completion signals. Without this, live-thinking detection would depend on
514
+ // THINKING_ACTIVE_KEY being stamped before the UI renders the same event.
515
+ if (message.stopReason === "pending" || message.stopReason === "deferred") return false;
516
+ if (typeof message.stopReason === "string" && message.stopReason.length > 0) return true;
517
+ if (Array.isArray(message.content)) {
518
+ let sawThinking = false;
519
+ for (const block of message.content) {
520
+ if (block?.type === "thinking" && typeof block.thinking === "string" && block.thinking.trim()) {
521
+ sawThinking = true;
522
+ } else if (sawThinking && (
523
+ (block?.type === "text" && typeof block.text === "string" && block.text.trim()) ||
524
+ block?.type === "toolCall"
525
+ )) {
526
+ return true;
527
+ }
528
+ }
529
+ if (sawThinking) return false;
530
+ }
531
+ return true;
532
+ }
533
+
534
+ function isLiveThinkingMessage(comp: any, message: any): boolean {
535
+ if (!message || message.role !== "assistant") return false;
536
+ if (isAssistantThinkingComplete(comp, message)) return false;
537
+ if ((message as any)[THINKING_ACTIVE_KEY]) return true;
538
+ if (Array.isArray(message.content)) {
539
+ return message.content.some((b: any) => b?.type === "thinking" && typeof b?.thinking === "string" && b.thinking.trim());
540
+ }
541
+ return false;
542
+ }
543
+
481
544
  type ToolStatus = "pending" | "success" | "error";
482
545
 
483
546
  function getToolStatusForGroup(tool: any): ToolStatus {
@@ -597,9 +660,22 @@ function formatToolNameList(tools: any[]): string {
597
660
  counts.set(name, (counts.get(name) ?? 0) + 1);
598
661
  }
599
662
  return [...counts.entries()]
600
- .slice(0, 4)
601
- .map(([name, count]) => `${name}${count > 1 ? `×${count}` : ""}`)
602
- .join(", ") + (counts.size > 4 ? ", …" : "");
663
+ .map(([name, count]) => `${humanizeToolName(name)}${count > 1 ? `×${count}` : ""}`)
664
+ .join(", ");
665
+ }
666
+
667
+ function getRepeatedToolSubject(tools: any[], groupedName: string | undefined): string {
668
+ if (!groupedName || tools.length === 0) return "";
669
+ if (groupedName === "read") {
670
+ const paths = tools.map((tool) => String(tool?.args?.path ?? ""));
671
+ if (paths[0] && paths.every((path) => path === paths[0])) {
672
+ return shortPath(process.cwd(), paths[0]);
673
+ }
674
+ }
675
+ const summaries = tools.map(getToolArgSummary);
676
+ return summaries[0] && summaries.every((summary) => summary === summaries[0])
677
+ ? summaries[0]
678
+ : "";
603
679
  }
604
680
 
605
681
  function escapeRegex(text: string): string {
@@ -660,7 +736,7 @@ function getToolArgSummary(tool: any): string {
660
736
  if (parts.length > 0) value += ` (${parts.join(", ")})`;
661
737
  return value;
662
738
  }
663
- if (name === "bash") return summarizeText(args.command ?? "", 72);
739
+ if (name === "bash") return buildBashCommandPresentation(args.command ?? "").headline;
664
740
  if (name === "grep") return `"${summarizeText(args.pattern ?? "", 40)}"${args.path ? ` in ${args.path}` : ""}`;
665
741
  if (name === "find") return `"${summarizeText(args.pattern ?? "", 40)}"${args.path ? ` in ${args.path}` : ""}`;
666
742
  if (name === "ls") return shortPath(process.cwd(), args.path ?? ".");
@@ -671,16 +747,80 @@ function getToolCallLine(tool: any): string {
671
747
  const value = (tool as any)?.callRendererComponent?.value;
672
748
  if (typeof value === "string" && value.trim()) {
673
749
  const line = value.split("\n").find((line) => stripAnsi(line).trim()) ?? value;
674
- return line.replaceAll(WRAP_MARK, "");
750
+ return line.replaceAll(WRAP_MARK, "").replaceAll(CLIP_MARK, "");
675
751
  }
676
752
  const summary = getToolArgSummary(tool);
677
753
  const label = humanizeToolName(getToolName(tool));
678
754
  return `${label}${summary ? ` ${summary}` : ""}`;
679
755
  }
680
756
 
681
- function getCompactToolLine(tool: any, width: number, groupedLabel?: string): string {
682
- const content = removeGroupedToolPrefix(getToolCallLine(tool), groupedLabel);
683
- return clampLineWidth(content || getToolName(tool), width);
757
+ function alignTrailingMarkedLine(line: string, width: number): string {
758
+ const markerIndex = line.indexOf(TRAILING_MARK);
759
+ if (markerIndex === -1) return clampLineWidth(line, width);
760
+ const safeWidth = Math.max(1, width);
761
+ const left = line.slice(0, markerIndex);
762
+ const right = line.slice(markerIndex + TRAILING_MARK.length);
763
+ const rightWidth = visibleWidth(right);
764
+ if (rightWidth >= safeWidth) return truncateToWidth(right, safeWidth, "", false);
765
+ const leftBudget = Math.max(0, safeWidth - rightWidth - 2);
766
+ const clippedLeft = leftBudget > 0 ? truncateToWidth(left, leftBudget, "…", false) : "";
767
+ const gap = Math.max(1, safeWidth - visibleWidth(clippedLeft) - rightWidth);
768
+ return `${clippedLeft}${" ".repeat(gap)}${right}`;
769
+ }
770
+
771
+ function getCompactToolLine(tool: any, width: number, groupedLabel?: string, showTrailing = true): string {
772
+ let content = removeGroupedToolPrefix(getToolCallLine(tool), groupedLabel);
773
+ if (!showTrailing) content = content.split(TRAILING_MARK, 1)[0] ?? content;
774
+ return alignTrailingMarkedLine(content || getToolName(tool), width);
775
+ }
776
+
777
+ interface CollapsedToolEntry {
778
+ tools: any[];
779
+ name: string;
780
+ subject: string;
781
+ }
782
+
783
+ function collapseRepeatedToolEntries(tools: any[]): CollapsedToolEntry[] {
784
+ const entries: CollapsedToolEntry[] = [];
785
+ for (const tool of tools) {
786
+ const name = getToolName(tool);
787
+ const subject = getRepeatedToolSubject([tool], name);
788
+ const previous = entries[entries.length - 1];
789
+ if (subject && previous?.name === name && previous.subject === subject) {
790
+ previous.tools.push(tool);
791
+ } else {
792
+ entries.push({ tools: [tool], name, subject });
793
+ }
794
+ }
795
+ return entries;
796
+ }
797
+
798
+ function stripReadRangeFromToolLine(line: string): string {
799
+ return line.replace(
800
+ /\s+(?:\x1b\[[0-9;]*m)*\((?:offset|limit)=\d+(?:,\s*(?:offset|limit)=\d+)*\)(?=(?:\x1b\[[0-9;]*m)*$)/,
801
+ "",
802
+ );
803
+ }
804
+
805
+ function getCollapsedToolEntryLine(entry: CollapsedToolEntry, width: number, groupedLabel?: string): string {
806
+ if (entry.tools.length === 1) return getCompactToolLine(entry.tools[0], width, groupedLabel);
807
+ const counts = countToolStatuses(entry.tools);
808
+ const attentionCounts = counts.pending > 0 || counts.error > 0
809
+ ? ` • ${formatToolGroupCounts(entry.tools)}`
810
+ : "";
811
+ const suffix = ` ${FG_DIM}×${entry.tools.length}${TRANSPARENT_RESET}${attentionCounts}`;
812
+ const firstLine = getCompactToolLine(entry.tools[0], Math.max(1, width - visibleWidth(suffix)), groupedLabel, false);
813
+ const sharedLine = entry.name === "read" ? stripReadRangeFromToolLine(firstLine) : firstLine;
814
+ return clampLineWidth(`${sharedLine}${suffix}`, width);
815
+ }
816
+
817
+ function getCollapsedToolEntryLines(entry: CollapsedToolEntry, width: number, groupedLabel?: string): string[] {
818
+ const lines = [getCollapsedToolEntryLine(entry, width, groupedLabel)];
819
+ if (entry.name !== "bash") return lines;
820
+ const running = [...entry.tools].reverse().find((tool) => getToolStatusForGroup(tool) === "pending");
821
+ const latestOutput = running ? getLastBashOutputLine(getTextContent(running.result)) : undefined;
822
+ if (latestOutput) lines.push(`${FG_DIM}${latestOutput}${TRANSPARENT_RESET}`);
823
+ return lines;
684
824
  }
685
825
 
686
826
  function getExpandedToolGroupLines(tool: any, width: number, groupedLabel?: string): string[] {
@@ -859,26 +999,48 @@ class ToolGroupComponent extends Container {
859
999
  if (status.success) countParts.push(statusText("success", status.success));
860
1000
  if (status.error) countParts.push(statusText("error", status.error));
861
1001
  const countsText = countParts.join(`${TRANSPARENT_RESET} • `);
862
- const summary = ` ${light} ${summaryLabel} ${countsText}${names ? ` ${TRANSPARENT_RESET}• ${names}` : ""}${toolOutputDetailHint(undefined as any, this.expanded, true)}`;
863
- const lines = [" ".repeat(safeWidth), clampLineWidth(summary, safeWidth)];
864
- const childWidth = Math.max(1, safeWidth - 6);
865
1002
  const total = this.tools.length;
866
-
867
- for (let index = 0; index < total; index++) {
868
- const tool = this.tools[index];
869
- const rawLines = this.expanded
870
- ? getExpandedToolGroupLines(tool, childWidth, groupedName ? label : undefined)
871
- : [getCompactToolLine(tool, childWidth, groupedName ? label : undefined)];
872
- const branched = formatBranchedToolLines(
873
- rawLines,
874
- index,
875
- total,
1003
+ const lines: string[] = [];
1004
+ const subject = this.expanded ? "" : getRepeatedToolSubject(this.tools, groupedName);
1005
+ const collapseToSingleRow = !this.expanded && !!groupedName && !!subject;
1006
+ if (collapseToSingleRow) {
1007
+ const entry = { tools: this.tools, name: groupedName, subject };
1008
+ const entryLine = getCollapsedToolEntryLine(entry, Math.max(1, safeWidth - 3));
1009
+ lines.push(clampLineWidth(
1010
+ ` ${light} ${entryLine}${toolOutputDetailHint(undefined as any, false, true)}`,
876
1011
  safeWidth,
877
- getToolStatusForGroup(tool),
878
- { agentBreathe: isAgentFamilyToolName(getToolName(tool)) },
879
- );
880
- for (let i = 0; i < branched.length; i++) {
881
- lines.push(clampLineWidth(branched[i], safeWidth));
1012
+ ));
1013
+ } else {
1014
+ const summary = ` ${light} ${summaryLabel} ${countsText}${names ? ` ${TRANSPARENT_RESET}• ${names}` : ""}${toolOutputDetailHint(undefined as any, this.expanded, true)}`;
1015
+ lines.push(" ".repeat(safeWidth), clampLineWidth(summary, safeWidth));
1016
+ const childWidth = Math.max(1, safeWidth - 6);
1017
+ if (this.expanded) {
1018
+ for (let index = 0; index < total; index++) {
1019
+ const tool = this.tools[index];
1020
+ const branched = formatBranchedToolLines(
1021
+ getExpandedToolGroupLines(tool, childWidth, groupedName ? label : undefined),
1022
+ index,
1023
+ total,
1024
+ safeWidth,
1025
+ getToolStatusForGroup(tool),
1026
+ { agentBreathe: isAgentFamilyToolName(getToolName(tool)) },
1027
+ );
1028
+ for (const line of branched) lines.push(clampLineWidth(line, safeWidth));
1029
+ }
1030
+ } else {
1031
+ const entries = collapseRepeatedToolEntries(this.tools);
1032
+ for (let index = 0; index < entries.length; index++) {
1033
+ const entry = entries[index];
1034
+ const branched = formatBranchedToolLines(
1035
+ getCollapsedToolEntryLines(entry, childWidth, groupedName ? label : undefined),
1036
+ index,
1037
+ entries.length,
1038
+ safeWidth,
1039
+ getToolGroupOverallStatus(entry.tools),
1040
+ { agentBreathe: entry.tools.every((tool) => isAgentFamilyToolName(getToolName(tool))) },
1041
+ );
1042
+ for (const line of branched) lines.push(clampLineWidth(line, safeWidth));
1043
+ }
882
1044
  }
883
1045
  }
884
1046
 
@@ -904,23 +1066,36 @@ function isToolGroupComponent(value: unknown): value is ToolGroupComponent {
904
1066
  return value instanceof ToolGroupComponent;
905
1067
  }
906
1068
 
1069
+ function isSpacerComponent(value: unknown): value is InstanceType<typeof Spacer> {
1070
+ return value instanceof Spacer || (value as any)?.constructor?.name === "Spacer";
1071
+ }
1072
+
1073
+ function isTextComponent(value: unknown): value is InstanceType<typeof Text> {
1074
+ return value instanceof Text || (value as any)?.constructor?.name === "Text";
1075
+ }
1076
+
1077
+ function isMarkdownComponent(value: unknown): value is InstanceType<typeof Markdown> {
1078
+ return value instanceof Markdown || (value as any)?.constructor?.name === "Markdown";
1079
+ }
1080
+
907
1081
  function isIgnorableToolSeparator(value: unknown): boolean {
908
- if (value instanceof Spacer) return true;
909
- if (value instanceof AssistantMessageComponent) {
1082
+ if (isSpacerComponent(value)) return true;
1083
+ if (value instanceof AssistantMessageComponent || (value as any)?.constructor?.name === "AssistantMessageComponent") {
1084
+ // Empty assistant framing stays ignorable so it never splits tool groups.
1085
+ // A rendered thinking row ("Thought for Xs" / live thinking) is a visible
1086
+ // boundary: tool calls that follow it must start a new group instead of
1087
+ // silently joining the batch that ran before the thought.
910
1088
  const contentChildren = (value as any).contentContainer?.children;
911
- return Array.isArray(contentChildren) && contentChildren.length === 0;
1089
+ if (!Array.isArray(contentChildren) || contentChildren.length === 0) return true;
1090
+ return contentChildren.every((child: any) => isSpacerComponent(child));
912
1091
  }
913
1092
  return false;
914
1093
  }
915
1094
 
916
1095
  function findPreviousToolSibling(children: any[], startIndex: number): { child: any; index: number } | undefined {
917
- let skippedSeparators = 0;
918
1096
  for (let index = startIndex; index >= 0; index--) {
919
1097
  const child = children[index];
920
- if (isIgnorableToolSeparator(child) && skippedSeparators < 3) {
921
- skippedSeparators++;
922
- continue;
923
- }
1098
+ if (isIgnorableToolSeparator(child)) continue;
924
1099
  return { child, index };
925
1100
  }
926
1101
  return undefined;
@@ -945,6 +1120,58 @@ function ungroupActiveToolGroups(): void {
945
1120
  }
946
1121
  }
947
1122
 
1123
+ function isThinkingOnlyAssistantComponent(comp: unknown): comp is InstanceType<typeof AssistantMessageComponent> {
1124
+ if (!comp || ((comp as any).constructor?.name !== "AssistantMessageComponent" && !(comp instanceof AssistantMessageComponent))) {
1125
+ return false;
1126
+ }
1127
+ const msg = (comp as any).lastMessage;
1128
+ if (!msg || msg.role !== "assistant" || !Array.isArray(msg.content)) return false;
1129
+ const hasThinking = msg.content.some((c: any) => c?.type === "thinking" && typeof c?.thinking === "string" && c.thinking.trim());
1130
+ if (!hasThinking) return false;
1131
+ const hasText = msg.content.some((c: any) => c?.type === "text" && typeof c?.text === "string" && c.text.trim());
1132
+ if (hasText) return false;
1133
+ const hasToolCalls = msg.content.some((c: any) => c?.type === "toolCall");
1134
+ if (hasToolCalls) return false;
1135
+ if ((comp as any).isStreaming === true || msg[THINKING_ACTIVE_KEY]) return false;
1136
+ return true;
1137
+ }
1138
+
1139
+ function maybeMergeConsecutiveThinkingMessages(parent: any): void {
1140
+ const children = parent?.children;
1141
+ if (!Array.isArray(children) || children.length < 2) return;
1142
+
1143
+ for (let i = 0; i < children.length; i++) {
1144
+ const current = children[i];
1145
+ if (!isThinkingOnlyAssistantComponent(current)) continue;
1146
+
1147
+ let nextIdx = i + 1;
1148
+ while (nextIdx < children.length && isSpacerComponent(children[nextIdx])) {
1149
+ nextIdx++;
1150
+ }
1151
+ if (nextIdx >= children.length) break;
1152
+
1153
+ const nextComp = children[nextIdx];
1154
+ if (isThinkingOnlyAssistantComponent(nextComp)) {
1155
+ const curMsg = (current as any).lastMessage;
1156
+ const nextMsg = (nextComp as any).lastMessage;
1157
+
1158
+ const durA = getMessageThinkingDurationMs(curMsg);
1159
+ const durB = getMessageThinkingDurationMs(nextMsg);
1160
+ const mergedDuration = durA + durB;
1161
+
1162
+ const nextThinkingBlocks = nextMsg.content.filter((c: any) => c?.type === "thinking");
1163
+ curMsg.content.push(...nextThinkingBlocks);
1164
+ curMsg[THINKING_DURATION_KEY] = mergedDuration;
1165
+
1166
+ (current as any).updateContent(curMsg);
1167
+
1168
+ const removeCount = nextIdx - i;
1169
+ children.splice(i + 1, removeCount);
1170
+ i--;
1171
+ }
1172
+ }
1173
+ }
1174
+
948
1175
  function maybeGroupToolComponent(parent: any, component: any): void {
949
1176
  if (!toolGroupingEnabled() || !isGroupableTool(component) || isToolGroupComponent(parent)) return;
950
1177
  const children = parent?.children;
@@ -957,6 +1184,7 @@ function maybeGroupToolComponent(parent: any, component: any): void {
957
1184
  if (isToolGroupComponent(previous)) {
958
1185
  children.splice(index, 1);
959
1186
  previous.addTool(component);
1187
+ maybeMergeConsecutiveThinkingMessages(parent);
960
1188
  return;
961
1189
  }
962
1190
  if (isGroupableTool(previous)) {
@@ -967,6 +1195,7 @@ function maybeGroupToolComponent(parent: any, component: any): void {
967
1195
  (group as any)[COMPONENT_PARENT] = parent;
968
1196
  children[previousEntry.index] = group;
969
1197
  children.splice(index, 1);
1198
+ maybeMergeConsecutiveThinkingMessages(parent);
970
1199
  }
971
1200
  }
972
1201
 
@@ -980,6 +1209,7 @@ function patchContainerParentTracking(): void {
980
1209
  const result = originalAddChild.call(this, component);
981
1210
  if (component && typeof component === "object") component[COMPONENT_PARENT] = this;
982
1211
  maybeGroupToolComponent(this, component);
1212
+ maybeMergeConsecutiveThinkingMessages(this);
983
1213
  return result;
984
1214
  };
985
1215
  proto.removeChild = function patchedRemoveChild(component: any) {
@@ -1025,6 +1255,7 @@ function patchGlobalToolBorders(): void {
1025
1255
 
1026
1256
  const originalRender = proto.render;
1027
1257
  proto.render = function patchedContainerRender(width: number): string[] {
1258
+ maybeMergeConsecutiveThinkingMessages(this);
1028
1259
  if (isToolExecutionLike(this)) {
1029
1260
  const cached = (this as any)[TOOL_RENDER_CACHE];
1030
1261
  const branchKey = toolBranchRenderCacheKey();
@@ -1213,12 +1444,12 @@ const OSC133_ZONE_START = "\x1b]133;A\x07";
1213
1444
  const OSC133_ZONE_END = "\x1b]133;B\x07";
1214
1445
  const OSC133_ZONE_FINAL = "\x1b]133;C\x07";
1215
1446
  const WORKED_DURATION_KEY = "_piClaudeStyleWorkedDurationMs";
1216
- const THINKING_DURATION_KEY = "_piClaudeStyleThinkingDurationMs";
1217
- const THINKING_ACTIVE_KEY = "_piClaudeStyleThinkingActive";
1218
1447
  const WORKED_START_KEY = "_piClaudeStyleWorkedStartMs";
1219
1448
  const WORKED_SESSION_TOTAL_KEY = "_piClaudeStyleWorkedSessionTotalMs";
1220
1449
  const WORKED_TURNS_KEY = "_piClaudeStyleWorkedTurns";
1221
1450
  const WORKED_DURATION_MARKER = "Turn took";
1451
+ const THINKING_DURATION_KEY = "_piClaudeStyleThinkingDurationMs";
1452
+ const THINKING_ACTIVE_KEY = "_piClaudeStyleThinkingActive";
1222
1453
  const MIN_THINKING_SUMMARY_MS = 100;
1223
1454
 
1224
1455
  let lastThinkingBlockDurationMs: number | undefined;
@@ -1335,32 +1566,43 @@ class HiddenThinkingSummary {
1335
1566
  }
1336
1567
  }
1337
1568
 
1338
- function assistantMessageThinkingComplete(message: any): boolean {
1339
- // toolUse is an intermediate assistant chunk — thinking may still be in progress on the next chunk.
1340
- const reason = message?.stopReason;
1341
- if (reason === "toolUse") return false;
1342
- return typeof reason === "string" && reason.length > 0;
1569
+ function getMessageThinkingDurationMs(message: any): number {
1570
+ const stored = (message as any)?.[THINKING_DURATION_KEY];
1571
+ if (typeof stored === "number" && stored > 0) return stored;
1572
+ if (typeof lastThinkingBlockDurationMs === "number" && lastThinkingBlockDurationMs > 0) {
1573
+ return lastThinkingBlockDurationMs;
1574
+ }
1575
+ if (typeof (message as any)?.[WORKED_DURATION_KEY] === "number" && (message as any)[WORKED_DURATION_KEY] > 0) {
1576
+ return (message as any)[WORKED_DURATION_KEY];
1577
+ }
1578
+ let totalChars = 0;
1579
+ if (Array.isArray(message?.content)) {
1580
+ for (const block of message.content) {
1581
+ if (block?.type === "thinking" && typeof block.thinking === "string") {
1582
+ totalChars += block.thinking.length;
1583
+ }
1584
+ }
1585
+ }
1586
+ return Math.max(1000, Math.round((totalChars / 150) * 1000));
1343
1587
  }
1344
1588
 
1345
- function hiddenThinkingSummaryForMessage(message: any): string {
1346
- // Per-message flags win over globals so a late render pass cannot keep
1347
- // "Thinking…" after thinking_end already stored duration on this message.
1348
- if ((message as any)?.[THINKING_ACTIVE_KEY]) return thinkingActiveSummaryText();
1349
- const stored = (message as any)?.[THINKING_DURATION_KEY];
1350
- const durationMs = typeof stored === "number"
1351
- ? stored
1352
- : assistantMessageThinkingComplete(message) && typeof lastThinkingBlockDurationMs === "number"
1353
- ? lastThinkingBlockDurationMs
1354
- : undefined;
1355
- if (typeof durationMs === "number" && durationMs >= MIN_THINKING_SUMMARY_MS) {
1356
- return thoughtDurationSummaryText(durationMs);
1589
+ function assistantMessageThinkingComplete(this: any, message: any): boolean {
1590
+ return isAssistantThinkingComplete(this, message);
1591
+ }
1592
+
1593
+ function hiddenThinkingSummaryForMessage(message: any, comp?: any): string {
1594
+ if (message && !isAssistantThinkingComplete(comp, message) && isLiveThinkingMessage(comp, message)) {
1595
+ return thinkingActiveSummaryText();
1596
+ }
1597
+ const durationMs = getMessageThinkingDurationMs(message);
1598
+ if (message && typeof message === "object") {
1599
+ (message as any)[THINKING_DURATION_KEY] = durationMs;
1357
1600
  }
1358
- if (thinkingBlockInFlight) return thinkingActiveSummaryText();
1359
- return thinkingActiveSummaryText();
1601
+ return thoughtDurationSummaryText(durationMs);
1360
1602
  }
1361
1603
 
1362
1604
  function isHiddenThinkingPlaceholderText(child: unknown): child is InstanceType<typeof Text> {
1363
- if (!(child instanceof Text)) return false;
1605
+ if (!isTextComponent(child)) return false;
1364
1606
  const plain = stripAnsi(String((child as any).text ?? "")).trim();
1365
1607
  if (/^[✻∴]\s*Thinking/i.test(plain)) return true;
1366
1608
  if (/^[✻∴]\s*Thought for/i.test(plain)) return true;
@@ -1383,10 +1625,6 @@ function workedDurationText(ms: number, sessionTotalMs?: number, turns?: number)
1383
1625
  return `${text}${RESET}`;
1384
1626
  }
1385
1627
 
1386
- function inlineWorkedDurationText(ms: number, sessionTotalMs?: number, turns?: number): string {
1387
- return workedDurationText(ms, sessionTotalMs, turns);
1388
- }
1389
-
1390
1628
  function isWorkedDurationLine(line: string): boolean {
1391
1629
  return line.includes(WORKED_DURATION_MARKER) && /^✻ Turn took [^\r\n]+$/.test(stripAnsi(line).trim());
1392
1630
  }
@@ -1408,8 +1646,6 @@ function hasWorkedDurationLine(message: any): boolean {
1408
1646
  });
1409
1647
  }
1410
1648
 
1411
-
1412
-
1413
1649
  type MarkdownThemeLike = ConstructorParameters<typeof Markdown>[3];
1414
1650
 
1415
1651
  type ParagraphSegment =
@@ -1516,6 +1752,30 @@ function sanitizeToolResultForDisplay(result: any): any {
1516
1752
  return changed ? { ...result, content } : result;
1517
1753
  }
1518
1754
 
1755
+ // Last-resort display scrubber at the terminal writer choke point. Every
1756
+ // rendered frame — every component, overlay, preview, and search hit — exits
1757
+ // through ProcessTerminal.write, so stripping complete §N§ tokens there covers
1758
+ // any surface the targeted strips above can't reach, including mid-sentence
1759
+ // tag references replayed from old tool output on resume. Display only:
1760
+ // storage, LLM context, copy/paste sources, and ANSI sequences are untouched
1761
+ // (tags are plain characters; escape sequences never contain them).
1762
+ const MAGIC_CONTEXT_TAG_TOKEN = /§\d+§/g;
1763
+ const TERMINAL_SCRUB_PATCH_FLAG = Symbol.for("pi-claude-style-tools:terminal-write-tag-scrub");
1764
+
1765
+ function patchTerminalWriteTagScrubber(): void {
1766
+ const proto = (ProcessTerminal as any)?.prototype;
1767
+ if (!proto || proto[TERMINAL_SCRUB_PATCH_FLAG]) return;
1768
+ const originalWrite = proto.write;
1769
+ if (typeof originalWrite !== "function") return;
1770
+ proto.write = function patchedTerminalWrite(this: any, data: any, ...rest: any[]) {
1771
+ if (typeof data === "string" && data.includes("§")) {
1772
+ data = data.replace(MAGIC_CONTEXT_TAG_TOKEN, "");
1773
+ }
1774
+ return originalWrite.call(this, data, ...rest);
1775
+ };
1776
+ proto[TERMINAL_SCRUB_PATCH_FLAG] = true;
1777
+ }
1778
+
1519
1779
  function replaceInlineMath(text: string): string {
1520
1780
  if (!hasInlineMathMarkers(text)) return text;
1521
1781
  const withParens = text.replace(/\\\(([\s\S]*?)\\\)/g, (_match, body: string) => {
@@ -1780,14 +2040,33 @@ class DottedParagraph {
1780
2040
  function replaceHiddenThinkingPlaceholders(container: { children?: any[] }, message: any): void {
1781
2041
  if (!container?.children) return;
1782
2042
  const summary = hiddenThinkingSummaryForMessage(message);
2043
+ let firstReplaced = false;
1783
2044
  for (let i = 0; i < container.children.length; i++) {
1784
2045
  const child = container.children[i];
1785
- if (child instanceof HiddenThinkingSummary) {
1786
- child.setSummary(summary);
2046
+ const inner = (child as any)?.child ?? child;
2047
+ if (inner instanceof HiddenThinkingSummary || (inner as any)?.constructor?.name === "HiddenThinkingSummary") {
2048
+ if (!firstReplaced) {
2049
+ inner.setSummary(summary);
2050
+ firstReplaced = true;
2051
+ } else {
2052
+ container.children.splice(i, 1);
2053
+ i--;
2054
+ }
1787
2055
  continue;
1788
2056
  }
1789
- if (isHiddenThinkingPlaceholderText(child)) {
1790
- container.children[i] = new HiddenThinkingSummary(summary);
2057
+ if (isHiddenThinkingPlaceholderText(inner)) {
2058
+ if (!firstReplaced) {
2059
+ const summaryComp = new HiddenThinkingSummary(summary);
2060
+ if ((child as any)?.child !== undefined) {
2061
+ (child as any).child = summaryComp;
2062
+ } else {
2063
+ container.children[i] = summaryComp;
2064
+ }
2065
+ firstReplaced = true;
2066
+ } else {
2067
+ container.children.splice(i, 1);
2068
+ i--;
2069
+ }
1791
2070
  }
1792
2071
  }
1793
2072
  }
@@ -2073,7 +2352,7 @@ function visitMarkdownDescendants(root: unknown, visit: (md: InstanceType<typeof
2073
2352
  if (!root || typeof root !== "object") return;
2074
2353
  const node = root as { children?: unknown[] };
2075
2354
  for (const child of node.children ?? []) {
2076
- if (child instanceof Markdown) visit(child);
2355
+ if (isMarkdownComponent(child)) visit(child);
2077
2356
  else visitMarkdownDescendants(child, visit);
2078
2357
  }
2079
2358
  }
@@ -2107,7 +2386,7 @@ function patchUserMessageRender(): void {
2107
2386
  if (!Array.isArray(lines) || lines.length === 0) return lines;
2108
2387
  const rendered = [
2109
2388
  roundedUserBorder(borderWidth, true),
2110
- ...lines.map((line: string) => borderedUserMessageLine(line, borderWidth)),
2389
+ ...lines.slice(1, -1).map((line: string) => borderedUserMessageLine(line, borderWidth)),
2111
2390
  roundedUserBorder(borderWidth, false),
2112
2391
  ];
2113
2392
  const clamped = rendered.map((line) => clampLineWidth(line, borderWidth));
@@ -2146,7 +2425,7 @@ function patchAssistantMessages(): void {
2146
2425
  proto[ASSISTANT_RENDER_PATCH_FLAG] = true;
2147
2426
  }
2148
2427
  const originalUpdateContent = proto.updateContent;
2149
- proto.updateContent = function patchedUpdateContent(message: any) {
2428
+ proto.updateContent = function patchedUpdateContent(message: any, isStreaming?: boolean) {
2150
2429
  // Content changed (also reached via invalidate() → updateContent): drop the
2151
2430
  // cached rendered output so the next render rebuilds with the new children.
2152
2431
  clearMessageRenderCache(this);
@@ -2154,32 +2433,58 @@ function patchAssistantMessages(): void {
2154
2433
  (this as any)[WORKED_START_KEY] = Date.now();
2155
2434
  }
2156
2435
  if (!message || !Array.isArray(message.content)) {
2157
- return originalUpdateContent.call(this, message);
2436
+ return originalUpdateContent.call(this, message, isStreaming);
2158
2437
  }
2159
- if ((this as any).hideThinkingBlock && messageHasThinkingContent(message)) {
2438
+ // Thinking display:
2439
+ // When thinking blocks are expanded via Ctrl+T (`hideThinkingBlock === false`),
2440
+ // all thinking blocks (old and new) render in full markdown.
2441
+ // When thinking blocks are collapsed (`hideThinkingBlock === true`):
2442
+ // - "live" mode (default): the actively-streaming thinking block renders
2443
+ // expanded while streaming, and collapses to `Thought for Xs` once done.
2444
+ // - "full" mode: behaves like stock pi (stays collapsed).
2445
+ const liveMode = getThinkingMode() === "live";
2446
+ const thinkingCollapsed = !!(this as any).hideThinkingBlock;
2447
+ const showLiveThinking = liveMode && thinkingCollapsed && isLiveThinkingMessage(this, message);
2448
+ if (thinkingCollapsed && messageHasThinkingContent(message)) {
2160
2449
  // Pi wraps this in theme.italic/fg again — keep plain label for the placeholder pass.
2161
2450
  (this as any).hiddenThinkingLabel = "Thinking…";
2162
2451
  }
2163
- // Call original to build all children (text, thinking, spacers, errors)
2164
- originalUpdateContent.call(this, message);
2452
+ if (showLiveThinking) (this as any).hideThinkingBlock = false;
2453
+ try {
2454
+ // Call original to build all children (text, thinking, spacers, errors)
2455
+ originalUpdateContent.call(this, message, isStreaming);
2456
+ } finally {
2457
+ if (showLiveThinking) (this as any).hideThinkingBlock = true;
2458
+ }
2165
2459
  // Replace text-block Markdown children with DottedParagraph wrappers
2166
2460
  const container = (this as any).contentContainer;
2167
2461
  if (!container?.children) return;
2168
- if ((this as any).hideThinkingBlock && messageHasThinkingContent(message)) {
2462
+ if (thinkingCollapsed && !showLiveThinking && messageHasThinkingContent(message)) {
2169
2463
  replaceHiddenThinkingPlaceholders(container, message);
2170
2464
  }
2171
2465
  const mdTheme = (this as any).markdownTheme;
2172
2466
  for (let i = container.children.length - 1; i >= 0; i--) {
2173
2467
  const child = container.children[i];
2174
- if (child instanceof Markdown) {
2175
- const text = (child as any).text;
2468
+ const inner = (child as any)?.child ?? child;
2469
+ if (isMarkdownComponent(inner)) {
2470
+ const text = (inner as any).text;
2176
2471
  if (!text) continue;
2177
- const isThinking = !!(child as any).defaultTextStyle?.italic;
2472
+ const isThinking = !!(inner as any).defaultTextStyle?.italic;
2178
2473
  if (isThinking) {
2179
- const style = (child as any).defaultTextStyle;
2180
- container.children[i] = new ThinkingParagraph(text, mdTheme, style);
2474
+ const style = (inner as any).defaultTextStyle;
2475
+ const replacement = new ThinkingParagraph(text, mdTheme, style);
2476
+ if ((child as any)?.child !== undefined) {
2477
+ (child as any).child = replacement;
2478
+ } else {
2479
+ container.children[i] = replacement;
2480
+ }
2181
2481
  } else {
2182
- container.children[i] = new DottedParagraph(text, mdTheme);
2482
+ const replacement = new DottedParagraph(text, mdTheme);
2483
+ if ((child as any)?.child !== undefined) {
2484
+ (child as any).child = replacement;
2485
+ } else {
2486
+ container.children[i] = replacement;
2487
+ }
2183
2488
  }
2184
2489
  }
2185
2490
  }
@@ -2187,16 +2492,11 @@ function patchAssistantMessages(): void {
2187
2492
  const explicitSessionTotal = (message as any)[WORKED_SESSION_TOTAL_KEY];
2188
2493
  const explicitTurns = (message as any)[WORKED_TURNS_KEY];
2189
2494
  // The "Turn took" line must only appear once the stream has truly closed.
2190
- // `message.stopReason === "stop"` is NOT a safe "finished" signal here: the
2191
- // Anthropic provider initializes the live message's stopReason to "stop" at
2192
- // creation and only updates it to the real value when `message_delta` arrives
2193
- // near the end of the stream — so it is already "stop" while text is still
2194
- // streaming, which made the line appear mid-stream. `explicitDuration` is
2195
- // stamped onto the message by the `message_end` handler (which fires after
2196
- // `message_delta`, when stopReason is the real final value), so gating on it
2197
- // guarantees the line shows only after the run is actually done. The line is
2198
- // baked into the message text at message_end; this child is just a fallback
2199
- // for re-renders where that baked text isn't present.
2495
+ // `message.stopReason === "stop"` is not a safe "finished" signal here because
2496
+ // providers may initialize a live message with that value. The `message_end`
2497
+ // handler stamps `explicitDuration` after the final stream event. Render the
2498
+ // styled line as a TUI child so ANSI presentation never enters message content
2499
+ // or persisted session transcripts.
2200
2500
  const isFinalAssistantMessage = message.stopReason === "stop";
2201
2501
  const workedDuration = typeof explicitDuration === "number" ? explicitDuration : undefined;
2202
2502
  const workedSessionTotal = typeof explicitSessionTotal === "number"
@@ -2406,6 +2706,82 @@ function liveLineCountTrailing(ctx: any, theme: Theme): string {
2406
2706
  return ` ${theme.fg("muted", `(${lineCountLabel(count)})`)}`;
2407
2707
  }
2408
2708
 
2709
+ const BASH_STARTED_AT_KEY = "_bashStartedAtMs";
2710
+ const BASH_ENDED_AT_KEY = "_bashEndedAtMs";
2711
+
2712
+ type BashDurationEntry = { invalidate: () => void };
2713
+
2714
+ const BASH_DURATION_CONTEXTS = new Map<any, BashDurationEntry>();
2715
+ let bashDurationTimer: ReturnType<typeof setTimeout> | null = null;
2716
+
2717
+ function scheduleBashDurationTick(): void {
2718
+ if (bashDurationTimer || BASH_DURATION_CONTEXTS.size === 0) return;
2719
+ bashDurationTimer = setTimeout(() => {
2720
+ bashDurationTimer = null;
2721
+ for (const entry of BASH_DURATION_CONTEXTS.values()) {
2722
+ try { entry.invalidate(); } catch { /* noop */ }
2723
+ }
2724
+ scheduleBashDurationTick();
2725
+ }, 1_000);
2726
+ unrefTimer(bashDurationTimer);
2727
+ }
2728
+
2729
+ function registerBashDurationContext(ctx: any): void {
2730
+ const key = ctx?.state ?? ctx;
2731
+ if (!key) return;
2732
+ const invalidate = typeof ctx?.invalidate === "function" ? () => safeInvalidate(ctx) : () => {};
2733
+ BASH_DURATION_CONTEXTS.set(key, { invalidate });
2734
+ scheduleBashDurationTick();
2735
+ }
2736
+
2737
+ function clearBashDurationContext(ctx: any): void {
2738
+ const key = ctx?.state ?? ctx;
2739
+ if (key) BASH_DURATION_CONTEXTS.delete(key);
2740
+ if (bashDurationTimer && BASH_DURATION_CONTEXTS.size === 0) {
2741
+ clearTimeout(bashDurationTimer);
2742
+ bashDurationTimer = null;
2743
+ }
2744
+ }
2745
+
2746
+ function clearAllBashDurationContexts(): void {
2747
+ BASH_DURATION_CONTEXTS.clear();
2748
+ if (bashDurationTimer) {
2749
+ clearTimeout(bashDurationTimer);
2750
+ bashDurationTimer = null;
2751
+ }
2752
+ }
2753
+
2754
+ function syncBashDuration(ctx: any, isPartial = true): void {
2755
+ const state = ctx?.state;
2756
+ if (!state) return;
2757
+ if (state._toolStatus === "pending" && typeof state[BASH_STARTED_AT_KEY] !== "number") {
2758
+ state[BASH_STARTED_AT_KEY] = Date.now();
2759
+ delete state[BASH_ENDED_AT_KEY];
2760
+ }
2761
+ const startedAt = state[BASH_STARTED_AT_KEY];
2762
+ if (typeof startedAt !== "number") return;
2763
+ if (!isPartial || ctx?.isError) {
2764
+ if (typeof state[BASH_ENDED_AT_KEY] !== "number") state[BASH_ENDED_AT_KEY] = Date.now();
2765
+ clearBashDurationContext(ctx);
2766
+ return;
2767
+ }
2768
+ registerBashDurationContext(ctx);
2769
+ }
2770
+
2771
+ function bashHeaderTrailing(ctx: any, theme: Theme): string {
2772
+ const parts: string[] = [];
2773
+ if (ctx?.isPartial === true) {
2774
+ const count = ctx?.state?._liveLineCount;
2775
+ if (typeof count === "number" && Number.isFinite(count) && count > 0) parts.push(lineCountLabel(count));
2776
+ }
2777
+ const startedAt = ctx?.state?.[BASH_STARTED_AT_KEY];
2778
+ if (typeof startedAt === "number") {
2779
+ const endedAt = ctx?.state?.[BASH_ENDED_AT_KEY];
2780
+ parts.push(formatBashDuration((typeof endedAt === "number" ? endedAt : Date.now()) - startedAt));
2781
+ }
2782
+ return parts.length > 0 ? `${TRAILING_MARK}${theme.fg("muted", parts.join(" · "))}` : "";
2783
+ }
2784
+
2409
2785
  function setToolStatus(ctx: any, status: "pending" | "success" | "error" | "idle"): void {
2410
2786
  if (ctx?.state) ctx.state._toolStatus = status;
2411
2787
  }
@@ -2682,6 +3058,10 @@ function withBranch(content: string, theme: Theme, _isError = false, continued =
2682
3058
  return `${branchLead(first, continued, theme)}\n${rest.join("\n")}`;
2683
3059
  }
2684
3060
 
3061
+ function withClippedBranch(content: string, theme: Theme, continued = false): string {
3062
+ return withBranch(content, theme, false, continued).replaceAll(WRAP_MARK, CLIP_MARK);
3063
+ }
3064
+
2685
3065
  function withFinalBranchBlock(content: string, theme: Theme, isError = false): string {
2686
3066
  if (!content || !content.trim()) return "";
2687
3067
  const lines = content.split("\n");
@@ -2952,6 +3332,16 @@ function markedContinuationPrefix(prefix: string): string {
2952
3332
  }
2953
3333
 
2954
3334
  function wrapMarkedLine(line: string, width: number): string[] {
3335
+ const clipIndex = line.indexOf(CLIP_MARK);
3336
+ if (clipIndex !== -1) {
3337
+ const prefix = line.slice(0, clipIndex);
3338
+ const body = line.slice(clipIndex + CLIP_MARK.length);
3339
+ if (body.includes(TRAILING_MARK)) return [alignTrailingMarkedLine(`${prefix}${body}`, width)];
3340
+ const bodyWidth = Math.max(1, width - visibleWidth(prefix));
3341
+ if (visibleWidth(body) <= bodyWidth) return [`${prefix}${body}`];
3342
+ const hint = "…";
3343
+ return [`${prefix}${truncateToWidth(body, Math.max(0, bodyWidth - visibleWidth(hint)), "", false)}${hint}`];
3344
+ }
2955
3345
  const markerIndex = line.indexOf(WRAP_MARK);
2956
3346
  if (markerIndex === -1) return wrapTextWithAnsi(line, width);
2957
3347
  const prefix = line.slice(0, markerIndex);
@@ -2968,6 +3358,10 @@ class ToolText extends Text {
2968
3358
  private toolCachedValue?: string;
2969
3359
  private toolCachedWidth?: number;
2970
3360
  private toolCachedLines?: string[];
3361
+ private observedWidth?: number;
3362
+ private pendingObservedWidth?: number;
3363
+ private widthObserver?: (width: number) => void;
3364
+ private widthObserverScheduled = false;
2971
3365
 
2972
3366
  constructor(text = "") {
2973
3367
  super("", 0, 0);
@@ -2980,6 +3374,26 @@ class ToolText extends Text {
2980
3374
  this.invalidate();
2981
3375
  }
2982
3376
 
3377
+ setWidthObserver(observer?: (width: number) => void): void {
3378
+ this.widthObserver = observer;
3379
+ if (!observer) this.pendingObservedWidth = undefined;
3380
+ }
3381
+
3382
+ private observeWidth(width: number): void {
3383
+ if (this.observedWidth === width) return;
3384
+ this.observedWidth = width;
3385
+ if (!this.widthObserver) return;
3386
+ this.pendingObservedWidth = width;
3387
+ if (this.widthObserverScheduled) return;
3388
+ this.widthObserverScheduled = true;
3389
+ queueMicrotask(() => {
3390
+ this.widthObserverScheduled = false;
3391
+ const observed = this.pendingObservedWidth;
3392
+ this.pendingObservedWidth = undefined;
3393
+ if (observed !== undefined) this.widthObserver?.(observed);
3394
+ });
3395
+ }
3396
+
2983
3397
  invalidate(): void {
2984
3398
  this.toolCachedValue = undefined;
2985
3399
  this.toolCachedWidth = undefined;
@@ -2987,6 +3401,7 @@ class ToolText extends Text {
2987
3401
  }
2988
3402
 
2989
3403
  render(width: number): string[] {
3404
+ this.observeWidth(width);
2990
3405
  const branchKey = toolBranchRenderCacheKey();
2991
3406
  if (
2992
3407
  this.toolCachedLines
@@ -3003,7 +3418,9 @@ class ToolText extends Text {
3003
3418
  }
3004
3419
  const contentWidth = Math.max(1, width);
3005
3420
  const lines = this.value.replace(/\t/g, " ").split("\n");
3006
- const rendered = lines.flatMap((line) => wrapMarkedLine(line, contentWidth)).map((line) => padToWidth(line, width));
3421
+ const rendered = lines
3422
+ .flatMap((line) => wrapMarkedLine(line, contentWidth))
3423
+ .map((line) => padToWidth(line, width));
3007
3424
  this.toolCachedValue = this.value;
3008
3425
  this.toolCachedWidth = width;
3009
3426
  this.toolCachedLines = rendered;
@@ -3015,10 +3432,21 @@ class ToolText extends Text {
3015
3432
 
3016
3433
  function makeText(last: unknown, text: string): Text {
3017
3434
  const component = last instanceof ToolText ? last : new ToolText();
3435
+ component.setWidthObserver();
3018
3436
  component.setText(text);
3019
3437
  return component;
3020
3438
  }
3021
3439
 
3440
+ function makeResponsiveDiffText(ctx: any, last: unknown, text: string): Text {
3441
+ const component = makeText(last, text) as ToolText;
3442
+ component.setWidthObserver((width) => {
3443
+ if (ctx.state?._diffComponentWidth === width) return;
3444
+ if (ctx.state) ctx.state._diffComponentWidth = width;
3445
+ safeInvalidate(ctx);
3446
+ });
3447
+ return component;
3448
+ }
3449
+
3022
3450
  function previewLimit(): number {
3023
3451
  const value = readSettings().previewLines;
3024
3452
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 8;
@@ -3037,6 +3465,29 @@ function bashCollapsedLimit(): number {
3037
3465
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : 10;
3038
3466
  }
3039
3467
 
3468
+ function bashCommandPreviewLimit(): number {
3469
+ const value = readSettings().bashCommandPreviewLines;
3470
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : 8;
3471
+ }
3472
+
3473
+ function renderBashCommandBlock(
3474
+ command: string,
3475
+ expanded: boolean,
3476
+ theme: Theme,
3477
+ ): string {
3478
+ const presentation = buildBashCommandPresentation(command);
3479
+ const limit = bashCommandPreviewLimit();
3480
+ if (!expanded && (limit === 0 || presentation.sourceLineCount < 2)) return "";
3481
+ const sourceLimit = expandedPreviewLimit();
3482
+ const lines = expanded ? presentation.sourceLines.slice(0, sourceLimit) : buildBashPreview(presentation.sourceLines, limit);
3483
+ if (lines.length === 0) return "";
3484
+ if (expanded && presentation.sourceLines.length > sourceLimit) {
3485
+ lines.push(`... ${presentation.sourceLines.length - sourceLimit} more command lines`);
3486
+ }
3487
+ const body = lines.map((line) => theme.fg("accent", line || " ")).join("\n");
3488
+ return expanded ? withBranch(body, theme, false, true) : withClippedBranch(body, theme, true);
3489
+ }
3490
+
3040
3491
  function liveToolPreviewEnabled(): boolean {
3041
3492
  return readSettings().liveToolPreview !== false;
3042
3493
  }
@@ -3504,12 +3955,12 @@ function rebindUiChromeToTheme(ctx: any): void {
3504
3955
  const theme = ctx.ui?.theme;
3505
3956
  invalidateThemePaletteCache();
3506
3957
  clearHighlightCache();
3507
- autoDerivePending = true;
3958
+ applyDiffPalette();
3508
3959
  bustSpinnerSettingsCache();
3509
3960
  applyToolBackgroundMode(theme);
3510
3961
  applyThemePaletteIfNeeded(theme);
3511
3962
  syncDiffShikiTheme(theme);
3512
- if (themeAdaptiveEnabled() && theme?.getFgAnsi) {
3963
+ if (themeAdaptiveEnabled() && theme?.getFgAnsi && !hasExplicitBgConfig) {
3513
3964
  autoDeriveBgFromTheme(theme);
3514
3965
  autoDerivePending = false;
3515
3966
  }
@@ -3746,6 +4197,7 @@ function applyThemePaletteIfNeeded(theme: any): void {
3746
4197
 
3747
4198
  function applyDiffPalette(): void {
3748
4199
  const config = loadDiffConfig();
4200
+ hasExplicitBgConfig = false;
3749
4201
  const preset = config.diffTheme ? DIFF_PRESETS[config.diffTheme] : null;
3750
4202
  if (preset) hasExplicitBgConfig = true;
3751
4203
  const overrides = config.diffColors ?? {};
@@ -3866,8 +4318,15 @@ function termW(): number {
3866
4318
  return Math.max(40, Math.min(raw - 4, MAX_TERM_WIDTH));
3867
4319
  }
3868
4320
 
3869
- function branchDiffWidth(): number {
3870
- return Math.max(40, termW() - 8);
4321
+ function branchDiffWidth(componentWidth?: number, chromeWidth = 2): number {
4322
+ const width = typeof componentWidth === "number" && Number.isFinite(componentWidth)
4323
+ ? Math.floor(componentWidth)
4324
+ : termW();
4325
+ return Math.max(20, Math.min(width - chromeWidth, MAX_TERM_WIDTH));
4326
+ }
4327
+
4328
+ function contextDiffWidth(ctx: any, chromeWidth = 2): number {
4329
+ return branchDiffWidth(ctx.state?._diffComponentWidth, chromeWidth);
3871
4330
  }
3872
4331
 
3873
4332
  function adaptiveWrapRows(tw?: number): number {
@@ -4727,7 +5186,7 @@ function renderEditPreviewBody(
4727
5186
  summary: string,
4728
5187
  ): void {
4729
5188
  const dc = resolveDiffColors(theme);
4730
- const branchWidth = branchDiffWidth();
5189
+ const branchWidth = contextDiffWidth(ctx, 3);
4731
5190
  if (operations.length === 1) {
4732
5191
  const [diff] = diffs;
4733
5192
  const line = lines[0] ?? getFirstChangedNewLine(diff);
@@ -4825,16 +5284,30 @@ function trackThinkingBlockEvents(event: any, ctx?: any): void {
4825
5284
  }
4826
5285
  if (evt.type === "thinking_end") {
4827
5286
  thinkingBlockInFlight = false;
4828
- const duration = Date.now() - thinkingBlockStartMs;
5287
+ const duration = Math.max(0, Date.now() - thinkingBlockStartMs);
4829
5288
  if (message?.role === "assistant") delete (message as any)[THINKING_ACTIVE_KEY];
4830
- if (duration >= MIN_THINKING_SUMMARY_MS) {
4831
- lastThinkingBlockDurationMs = duration;
4832
- if (message?.role === "assistant") (message as any)[THINKING_DURATION_KEY] = duration;
4833
- } else {
4834
- lastThinkingBlockDurationMs = undefined;
4835
- if (message?.role === "assistant") delete (message as any)[THINKING_DURATION_KEY];
4836
- }
5289
+ lastThinkingBlockDurationMs = duration;
5290
+ if (message?.role === "assistant") (message as any)[THINKING_DURATION_KEY] = duration;
4837
5291
  refreshThinkingChrome();
5292
+ return;
5293
+ }
5294
+ // Fallback: some providers/models never emit thinking_end (a second
5295
+ // thinking_start can overwrite the first, or the turn can end with toolUse).
5296
+ // The live "Thinking..." row would otherwise stick forever while later calls
5297
+ // run normally. Any non-thinking stream event on the same assistant message
5298
+ // means thinking is no longer the live activity: freeze the elapsed time into
5299
+ // a "Thought for Xs" duration so the row always resolves.
5300
+ if ((message as any)?.[THINKING_ACTIVE_KEY] || thinkingBlockInFlight) {
5301
+ if (evt.type === "text_start" || evt.type === "text_delta" || evt.type === "toolcall_start" || evt.type === "toolcall_end") {
5302
+ thinkingBlockInFlight = false;
5303
+ const duration = thinkingBlockStartMs > 0 ? Math.max(0, Date.now() - thinkingBlockStartMs) : undefined;
5304
+ if (message?.role === "assistant") delete (message as any)[THINKING_ACTIVE_KEY];
5305
+ if (typeof duration === "number") {
5306
+ lastThinkingBlockDurationMs = duration;
5307
+ if (message?.role === "assistant") (message as any)[THINKING_DURATION_KEY] = duration;
5308
+ }
5309
+ refreshThinkingChrome();
5310
+ }
4838
5311
  }
4839
5312
  }
4840
5313
 
@@ -4860,7 +5333,6 @@ function registerThinkingLabels(pi: ExtensionAPI): void {
4860
5333
  }
4861
5334
  if (sessionStartMs === undefined) sessionStartMs = Date.now();
4862
5335
  currentAssistantMessageStartMs = undefined;
4863
- thinkingBlockInFlight = false;
4864
5336
  });
4865
5337
  pi.on("agent_start", async () => {
4866
5338
  if (currentAgentWorkStartMs === undefined) {
@@ -4877,6 +5349,10 @@ function registerThinkingLabels(pi: ExtensionAPI): void {
4877
5349
  if (message?.role === "assistant") {
4878
5350
  currentAssistantMessageStartMs = Date.now();
4879
5351
  (message as any)[WORKED_START_KEY] = currentAssistantMessageStartMs;
5352
+ // A new assistant message starts a fresh thinking lifecycle. Without
5353
+ // this, a missing thinking_end on the previous message leaves the
5354
+ // global in-flight flag set and the next message renders a stale
5355
+ // "Thinking..." row until its own thinking events arrive.
4880
5356
  thinkingBlockInFlight = false;
4881
5357
  delete (message as any)[THINKING_ACTIVE_KEY];
4882
5358
  }
@@ -4888,9 +5364,22 @@ function registerThinkingLabels(pi: ExtensionAPI): void {
4888
5364
  pi.on("message_end", async (event, ctx) => {
4889
5365
  const message = (event as any)?.message;
4890
5366
  if (message?.role === "assistant") {
4891
- if (typeof lastThinkingBlockDurationMs === "number") {
4892
- (message as any)[THINKING_DURATION_KEY] = lastThinkingBlockDurationMs;
5367
+ // Belt-and-suspenders: if thinking_end never fired (provider skipped
5368
+ // it, or the turn ended on toolUse/text), freeze any live "Thinking..."
5369
+ // into its "Thought for Xs" duration here so the row always resolves
5370
+ // at the end of the message instead of sticking into later calls.
5371
+ thinkingBlockInFlight = false;
5372
+ delete (message as any)[THINKING_ACTIVE_KEY];
5373
+ if (typeof (message as any)[THINKING_DURATION_KEY] !== "number") {
5374
+ const duration = thinkingBlockStartMs > 0 ? Math.max(0, Date.now() - thinkingBlockStartMs) : undefined;
5375
+ if (typeof duration === "number" && duration > 0) {
5376
+ lastThinkingBlockDurationMs = duration;
5377
+ (message as any)[THINKING_DURATION_KEY] = duration;
5378
+ } else if (typeof lastThinkingBlockDurationMs === "number" && lastThinkingBlockDurationMs > 0) {
5379
+ (message as any)[THINKING_DURATION_KEY] = lastThinkingBlockDurationMs;
5380
+ }
4893
5381
  }
5382
+ thinkingBlockStartMs = 0;
4894
5383
  const started = typeof currentAgentWorkStartMs === "number"
4895
5384
  ? currentAgentWorkStartMs
4896
5385
  : typeof (message as any)[WORKED_START_KEY] === "number"
@@ -4904,6 +5393,8 @@ function registerThinkingLabels(pi: ExtensionAPI): void {
4904
5393
  (message as any)[WORKED_DURATION_KEY] = durationMs;
4905
5394
  if (typeof sessionTotalMs === "number") (message as any)[WORKED_SESSION_TOTAL_KEY] = sessionTotalMs;
4906
5395
  if (typeof turns === "number") (message as any)[WORKED_TURNS_KEY] = turns;
5396
+ // Duration metadata drives the assistant component's TUI-only status line.
5397
+ // Message content stays presentation-neutral for persistence and consumers.
4907
5398
  }
4908
5399
  currentAssistantMessageStartMs = undefined;
4909
5400
  }
@@ -5543,7 +6034,7 @@ function renderApplyPatchCall(args: any, theme: Theme, ctx: any, sp: (path: stri
5543
6034
  }
5544
6035
  ctx.state._openAiPatchFiles = preview.changes.map((change) => change.displayPath);
5545
6036
 
5546
- const diffWidth = branchDiffWidth();
6037
+ const diffWidth = contextDiffWidth(ctx);
5547
6038
  const key = `apply-preview:${ctx.state._applyPatchMetaKey ?? hashText(patchText)}:${diffWidth}:${ctx.expanded ? 1 : 0}`;
5548
6039
  if (ctx.state._applyPatchPreviewKey !== key) {
5549
6040
  ctx.state._applyPatchPreviewKey = key;
@@ -5596,7 +6087,7 @@ function renderApplyPatchCall(args: any, theme: Theme, ctx: any, sp: (path: stri
5596
6087
  }
5597
6088
 
5598
6089
  const body = ctx.state._applyPatchPreviewDisplay as string | undefined;
5599
- return makeText(ctx.lastComponent, body ? `${hdr}\n${body}` : hdr);
6090
+ return makeResponsiveDiffText(ctx, ctx.lastComponent, body ? `${hdr}\n${body}` : hdr);
5600
6091
  }
5601
6092
 
5602
6093
  function renderApplyPatchResult(result: any, isPartial: boolean, theme: Theme, ctx: any): Text {
@@ -5945,6 +6436,7 @@ function renderOpenAiToolResult(name: string, result: any, expanded: boolean, is
5945
6436
  // ===========================================================================
5946
6437
 
5947
6438
  export default function (pi: ExtensionAPI) {
6439
+ patchTerminalWriteTagScrubber();
5948
6440
  patchToolExecutionBackgroundSync();
5949
6441
  patchToolRenderCacheInvalidation();
5950
6442
  patchReadImageExpansion();
@@ -5972,7 +6464,7 @@ export default function (pi: ExtensionAPI) {
5972
6464
  // /cc-tools command — control tool chrome, grouping, and detail level.
5973
6465
  const TOOL_MODES = ["outlines", "transparent", "default"] as const;
5974
6466
  const TOOL_BOOL_MODES = ["on", "off", "toggle", "status"] as const;
5975
- const TOOL_SUBCOMMANDS = [...TOOL_MODES, "group", "detail", "branch", "status"] as const;
6467
+ const TOOL_SUBCOMMANDS = [...TOOL_MODES, "group", "detail", "thinking", "branch", "status"] as const;
5976
6468
  const booleanMode = (raw: string | undefined, current: boolean): boolean | "status" | undefined => {
5977
6469
  const mode = raw || "toggle";
5978
6470
  if (mode === "on") return true;
@@ -5995,6 +6487,7 @@ export default function (pi: ExtensionAPI) {
5995
6487
  ctx.ui.notify([
5996
6488
  `Tool style: ${toolBackgroundMode}`,
5997
6489
  `Tool grouping: ${toolGroupingEnabled() ? "on" : "off"}`,
6490
+ `Thinking: ${getThinkingMode()}`,
5998
6491
  `Extra detail: ${extraToolOutputExpanded ? "on" : "off"} (${rawKeyHint("ctrl+shift+o", "toggle")})`,
5999
6492
  branchLine,
6000
6493
  ` /cc-tools branch <0-255> | theme | fixed | reset`,
@@ -6013,6 +6506,7 @@ export default function (pi: ExtensionAPI) {
6013
6506
  label: m,
6014
6507
  description:
6015
6508
  m === "group" ? "Toggle grouped adjacent/concurrent tool rows"
6509
+ : m === "thinking" ? "Thinking display: live (default) or full"
6016
6510
  : m === "detail" ? "Toggle Ctrl+Shift+O extra-detail mode"
6017
6511
  : m === "branch" ? "├ └ │ gray (0-255), theme, fixed, or reset"
6018
6512
  : m === "status" ? "Show tool UI settings"
@@ -6028,6 +6522,12 @@ export default function (pi: ExtensionAPI) {
6028
6522
  .filter((o) => o.startsWith(second))
6029
6523
  .map((o) => ({ value: `branch ${o}`, label: o, description: "Branch connector color" }));
6030
6524
  }
6525
+ if (first === "thinking") {
6526
+ const second = parts[1] ?? "";
6527
+ return ["live", "full", "status"]
6528
+ .filter((m) => m.startsWith(second))
6529
+ .map((m) => ({ value: `thinking ${m}`, label: m, description: `${m} thinking display` }));
6530
+ }
6031
6531
  if (first === "group" || first === "detail" || first === "extra") {
6032
6532
  const second = parts[1] ?? "";
6033
6533
  return TOOL_BOOL_MODES
@@ -6100,6 +6600,24 @@ export default function (pi: ExtensionAPI) {
6100
6600
  return;
6101
6601
  }
6102
6602
 
6603
+ if (sub === "thinking") {
6604
+ const arg = (parts[1] ?? "status").toLowerCase();
6605
+ if (arg === "status" || !arg) {
6606
+ if (ctx.hasUI) ctx.ui.notify(`Thinking: ${getThinkingMode()} (live = only streaming thinking expands; full = always expanded)`, "info");
6607
+ return;
6608
+ }
6609
+ if (arg !== "live" && arg !== "full") {
6610
+ if (ctx.hasUI) ctx.ui.notify("Usage: /cc-tools thinking live|full|status", "error");
6611
+ return;
6612
+ }
6613
+ writeSettingsKey("thinkingMode", arg);
6614
+ if (ctx.hasUI) {
6615
+ ctx.ui.setToolsExpanded(ctx.ui.getToolsExpanded());
6616
+ ctx.ui.notify(`Thinking → ${arg}${arg === "live" ? " (only the active thinking expands)" : " (thinking always expanded)"}`, "info");
6617
+ }
6618
+ return;
6619
+ }
6620
+
6103
6621
  if (sub === "detail" || sub === "extra") {
6104
6622
  const next = booleanMode(parts[1], extraToolOutputExpanded);
6105
6623
  if (next === undefined) {
@@ -6119,7 +6637,7 @@ export default function (pi: ExtensionAPI) {
6119
6637
  }
6120
6638
 
6121
6639
  if (!(TOOL_MODES as readonly string[]).includes(sub)) {
6122
- if (ctx.hasUI) ctx.ui.notify(`Unknown option "${sub}". Try /cc-tools status, /cc-tools branch 72, or /cc-tools group toggle.`, "error");
6640
+ if (ctx.hasUI) ctx.ui.notify(`Unknown option "${sub}". Try /cc-tools status, /cc-tools thinking live, or /cc-tools group toggle.`, "error");
6123
6641
  return;
6124
6642
  }
6125
6643
  toolBackgroundOverride = sub as typeof toolBackgroundMode;
@@ -6413,15 +6931,27 @@ export default function (pi: ExtensionAPI) {
6413
6931
  },
6414
6932
  renderCall(args, theme, ctx) {
6415
6933
  syncToolCallStatus(ctx);
6934
+ syncBashDuration(ctx);
6416
6935
  const rewrite = ensureRtkRewriteForContext(ctx, args);
6417
- const summary = stableCallSummary(ctx, "_callSummary", () => summarizeText(args.command, 72));
6936
+ const command = typeof args.command === "string" ? args.command : "";
6937
+ const presentation = buildBashCommandPresentation(command);
6938
+ const summary = stableCallSummary(ctx, "_bashHeadline", () => presentation.headline);
6418
6939
  const rtkBadge = rewrite ? theme.fg("muted", " (RTK)") : "";
6419
- return makeText(
6420
- ctx.lastComponent,
6421
- toolHeader("Bash", `${summary}${rtkBadge}`, theme, toolStatusDot(ctx, theme), liveLineCountTrailing(ctx, theme)),
6422
- );
6940
+ const status = ctx?.state?._toolStatus;
6941
+ const showCommand = ctx.argsComplete === true && (status === "pending" || status === "error" || ctx.expanded === true);
6942
+ const commandBlock = showCommand ? renderBashCommandBlock(command, ctx.expanded === true, theme) : "";
6943
+ const headerSummary = ctx.expanded === true && commandBlock ? describeBashSource(presentation) : summary;
6944
+ const header = toolHeader(
6945
+ "Bash",
6946
+ `${headerSummary}${rtkBadge}`,
6947
+ theme,
6948
+ toolStatusDot(ctx, theme),
6949
+ bashHeaderTrailing(ctx, theme),
6950
+ ).replace(WRAP_MARK, CLIP_MARK);
6951
+ return makeText(ctx.lastComponent, commandBlock ? `${header}\n${commandBlock}` : header);
6423
6952
  },
6424
6953
  renderResult(result, { expanded, isPartial }, theme, ctx) {
6954
+ syncBashDuration(ctx, isPartial);
6425
6955
  const details = result.details as BashToolDetails | undefined;
6426
6956
  const rewrite = ensureRtkRewriteForContext(ctx, ctx.args);
6427
6957
  const output = result.content[0]?.type === "text" ? result.content[0].text : "";
@@ -6666,7 +7196,7 @@ export default function (pi: ExtensionAPI) {
6666
7196
  if (d?._type === "diff") {
6667
7197
  const previewLines = ctx.expanded ? MAX_RENDER_LINES : diffCollapsedLimit();
6668
7198
  const hunks = d.diff?.lines?.filter((l: any) => l.type === "sep").length + (d.diff?.lines?.length ? 1 : 0);
6669
- const diffWidth = branchDiffWidth();
7199
+ const diffWidth = contextDiffWidth(ctx);
6670
7200
  const mode = shouldUseSplit(d.diff, diffWidth, previewLines) ? "split" : "unified";
6671
7201
  const richSummary = diffSummaryWithMeta(d.diff.added, d.diff.removed, hunks, mode);
6672
7202
  const key = `wd:${diffWidth}:${d.summary}:${d.diff?.lines?.length ?? 0}:${d.language ?? ""}:${ctx.expanded ? 1 : 0}`;
@@ -6686,7 +7216,7 @@ export default function (pi: ExtensionAPI) {
6686
7216
  safeInvalidate(ctx);
6687
7217
  });
6688
7218
  }
6689
- return makeText(ctx.lastComponent, ctx.state._wdt ?? withBranch(richSummary, theme));
7219
+ return makeResponsiveDiffText(ctx, ctx.lastComponent, ctx.state._wdt ?? withBranch(richSummary, theme));
6690
7220
  }
6691
7221
  if (d?._type === "noChange") return makeText(ctx.lastComponent, withBranch(theme.fg("muted", "✓ no changes"), theme));
6692
7222
  if (d?._type === "new") {
@@ -6696,7 +7226,7 @@ export default function (pi: ExtensionAPI) {
6696
7226
  const syntheticDiff = getCachedParsedDiff(ctx, `nf-diff:${d.filePath}:${contentHash}`, "", content);
6697
7227
  const richSummary = diffSummaryWithMeta(syntheticDiff.added, 0, 1, "new file");
6698
7228
  const previewLines = ctx.expanded ? MAX_RENDER_LINES : diffCollapsedLimit();
6699
- const diffWidth = branchDiffWidth();
7229
+ const diffWidth = contextDiffWidth(ctx);
6700
7230
  const pk = `nf:${d.filePath}:${contentHash}:${diffWidth}:${ctx.expanded ? 1 : 0}`;
6701
7231
  if (ctx.state._nfk !== pk) {
6702
7232
  ctx.state._nfk = pk;
@@ -6714,7 +7244,7 @@ export default function (pi: ExtensionAPI) {
6714
7244
  safeInvalidate(ctx);
6715
7245
  });
6716
7246
  }
6717
- return makeText(ctx.lastComponent, ctx.state._nft ?? withBranch(`${richSummary} ${theme.fg("muted", `(${lineTotal} lines)`)}`, theme));
7247
+ return makeResponsiveDiffText(ctx, ctx.lastComponent, ctx.state._nft ?? withBranch(`${richSummary} ${theme.fg("muted", `(${lineTotal} lines)`)}`, theme));
6718
7248
  }
6719
7249
  return makeText(ctx.lastComponent, withBranch(theme.fg("success", "Written"), theme));
6720
7250
  },
@@ -6769,7 +7299,7 @@ export default function (pi: ExtensionAPI) {
6769
7299
  syncToolCallStatus(ctx);
6770
7300
  const hdr = toolHeader("Edit", summary, theme, ` ${toolStatusDot(ctx, theme)}`, liveLineCountTrailing(ctx, theme));
6771
7301
  if (!(ctx.argsComplete && operations.length > 0)) return makeText(ctx.lastComponent, hdr);
6772
- const diffWidth = branchDiffWidth();
7302
+ const diffWidth = contextDiffWidth(ctx, 3);
6773
7303
  const key = `edit:${fp}:${hashText(operations.map((edit) => `${edit.oldText}\u0000${edit.newText}`).join("\u0001"))}:${diffWidth}:${ctx.expanded ? 1 : 0}`;
6774
7304
  const { diffs: fallbackDiffs, summary: editSummary } = getCachedEditOperationSummary(ctx, key, operations);
6775
7305
  if (ctx.state._pk !== key) {
@@ -6789,8 +7319,8 @@ export default function (pi: ExtensionAPI) {
6789
7319
  renderEditPreviewBody(ctx, key, theme, lg, operations, fallbackDiffs, fallbackDiffs.map((diff) => getFirstChangedNewLine(diff)), editSummary);
6790
7320
  });
6791
7321
  }
6792
- const body = liveBranchDisplay(ctx.state, theme) ?? (ctx.state._ptDisplay as string | undefined);
6793
- return makeText(ctx.lastComponent, body ? `${hdr}\n${body}` : hdr);
7322
+ const body = liveBranchDisplay(ctx.state, theme) ?? (ctx.state._ptDisplay as string | undefined);
7323
+ return makeResponsiveDiffText(ctx, ctx.lastComponent, body ? `${hdr}\n${body}` : hdr);
6794
7324
  },
6795
7325
  renderResult(result, { expanded, isPartial }, theme, ctx) {
6796
7326
  if (isPartial) {
@@ -6943,6 +7473,7 @@ export default function (pi: ExtensionAPI) {
6943
7473
  });
6944
7474
  pi.on("session_shutdown", async () => {
6945
7475
  _clearAllBlinkContexts();
7476
+ clearAllBashDurationContexts();
6946
7477
  clearRtkRewriteState();
6947
7478
  WRITE_EXISTED_BEFORE.clear();
6948
7479
  clearHighlightCache();