pi-claude-code-ui 1.0.78 → 1.0.82
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.
- package/CHANGELOG.md +19 -0
- package/README.md +10 -4
- package/config/config.example.json +2 -0
- package/extensions/bash-command.ts +84 -0
- package/extensions/index.ts +670 -122
- package/package.json +3 -1
package/extensions/index.ts
CHANGED
|
@@ -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
|
-
.
|
|
601
|
-
.
|
|
602
|
-
|
|
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
|
|
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
|
|
682
|
-
const
|
|
683
|
-
|
|
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
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
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
|
-
|
|
878
|
-
|
|
879
|
-
)
|
|
880
|
-
|
|
881
|
-
|
|
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
|
|
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
|
-
|
|
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)
|
|
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
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
if (
|
|
1342
|
-
|
|
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));
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
function assistantMessageThinkingComplete(this: any, message: any): boolean {
|
|
1590
|
+
return isAssistantThinkingComplete(this, message);
|
|
1343
1591
|
}
|
|
1344
1592
|
|
|
1345
|
-
function hiddenThinkingSummaryForMessage(message: any): string {
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
const
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
: assistantMessageThinkingComplete(message) && typeof lastThinkingBlockDurationMs === "number"
|
|
1353
|
-
? lastThinkingBlockDurationMs
|
|
1354
|
-
: undefined;
|
|
1355
|
-
if (typeof durationMs === "number" && durationMs >= MIN_THINKING_SUMMARY_MS) {
|
|
1356
|
-
return thoughtDurationSummaryText(durationMs);
|
|
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
|
-
|
|
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
|
|
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 =
|
|
@@ -1496,6 +1732,50 @@ function stripTransientMagicContextTags(text: string): string {
|
|
|
1496
1732
|
return text.replace(MAGIC_CONTEXT_TAG_LINE_PREFIX, "$1");
|
|
1497
1733
|
}
|
|
1498
1734
|
|
|
1735
|
+
// Tool results can carry transient Magic Context tags too (live-prefixed output
|
|
1736
|
+
// chunks). Renderers must see sanitized text WITHOUT mutating this.result — the
|
|
1737
|
+
// result object is the stored message used by context management. Clone blocks
|
|
1738
|
+
// only when a tag is actually present so the common path stays zero-cost.
|
|
1739
|
+
function sanitizeToolResultForDisplay(result: any): any {
|
|
1740
|
+
if (!result || !Array.isArray(result.content)) return result;
|
|
1741
|
+
let changed = false;
|
|
1742
|
+
const content = result.content.map((block: any) => {
|
|
1743
|
+
if (block && typeof block.text === "string") {
|
|
1744
|
+
const stripped = stripTransientMagicContextTags(block.text);
|
|
1745
|
+
if (stripped !== block.text) {
|
|
1746
|
+
changed = true;
|
|
1747
|
+
return { ...block, text: stripped };
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
return block;
|
|
1751
|
+
});
|
|
1752
|
+
return changed ? { ...result, content } : result;
|
|
1753
|
+
}
|
|
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
|
+
|
|
1499
1779
|
function replaceInlineMath(text: string): string {
|
|
1500
1780
|
if (!hasInlineMathMarkers(text)) return text;
|
|
1501
1781
|
const withParens = text.replace(/\\\(([\s\S]*?)\\\)/g, (_match, body: string) => {
|
|
@@ -1760,14 +2040,27 @@ class DottedParagraph {
|
|
|
1760
2040
|
function replaceHiddenThinkingPlaceholders(container: { children?: any[] }, message: any): void {
|
|
1761
2041
|
if (!container?.children) return;
|
|
1762
2042
|
const summary = hiddenThinkingSummaryForMessage(message);
|
|
2043
|
+
let firstReplaced = false;
|
|
1763
2044
|
for (let i = 0; i < container.children.length; i++) {
|
|
1764
2045
|
const child = container.children[i];
|
|
1765
|
-
if (child instanceof HiddenThinkingSummary) {
|
|
1766
|
-
|
|
2046
|
+
if (child instanceof HiddenThinkingSummary || (child as any)?.constructor?.name === "HiddenThinkingSummary") {
|
|
2047
|
+
if (!firstReplaced) {
|
|
2048
|
+
child.setSummary(summary);
|
|
2049
|
+
firstReplaced = true;
|
|
2050
|
+
} else {
|
|
2051
|
+
container.children.splice(i, 1);
|
|
2052
|
+
i--;
|
|
2053
|
+
}
|
|
1767
2054
|
continue;
|
|
1768
2055
|
}
|
|
1769
2056
|
if (isHiddenThinkingPlaceholderText(child)) {
|
|
1770
|
-
|
|
2057
|
+
if (!firstReplaced) {
|
|
2058
|
+
container.children[i] = new HiddenThinkingSummary(summary);
|
|
2059
|
+
firstReplaced = true;
|
|
2060
|
+
} else {
|
|
2061
|
+
container.children.splice(i, 1);
|
|
2062
|
+
i--;
|
|
2063
|
+
}
|
|
1771
2064
|
}
|
|
1772
2065
|
}
|
|
1773
2066
|
}
|
|
@@ -2053,7 +2346,7 @@ function visitMarkdownDescendants(root: unknown, visit: (md: InstanceType<typeof
|
|
|
2053
2346
|
if (!root || typeof root !== "object") return;
|
|
2054
2347
|
const node = root as { children?: unknown[] };
|
|
2055
2348
|
for (const child of node.children ?? []) {
|
|
2056
|
-
if (child
|
|
2349
|
+
if (isMarkdownComponent(child)) visit(child);
|
|
2057
2350
|
else visitMarkdownDescendants(child, visit);
|
|
2058
2351
|
}
|
|
2059
2352
|
}
|
|
@@ -2087,7 +2380,7 @@ function patchUserMessageRender(): void {
|
|
|
2087
2380
|
if (!Array.isArray(lines) || lines.length === 0) return lines;
|
|
2088
2381
|
const rendered = [
|
|
2089
2382
|
roundedUserBorder(borderWidth, true),
|
|
2090
|
-
...lines.map((line: string) => borderedUserMessageLine(line, borderWidth)),
|
|
2383
|
+
...lines.slice(1, -1).map((line: string) => borderedUserMessageLine(line, borderWidth)),
|
|
2091
2384
|
roundedUserBorder(borderWidth, false),
|
|
2092
2385
|
];
|
|
2093
2386
|
const clamped = rendered.map((line) => clampLineWidth(line, borderWidth));
|
|
@@ -2126,7 +2419,7 @@ function patchAssistantMessages(): void {
|
|
|
2126
2419
|
proto[ASSISTANT_RENDER_PATCH_FLAG] = true;
|
|
2127
2420
|
}
|
|
2128
2421
|
const originalUpdateContent = proto.updateContent;
|
|
2129
|
-
proto.updateContent = function patchedUpdateContent(message: any) {
|
|
2422
|
+
proto.updateContent = function patchedUpdateContent(message: any, isStreaming?: boolean) {
|
|
2130
2423
|
// Content changed (also reached via invalidate() → updateContent): drop the
|
|
2131
2424
|
// cached rendered output so the next render rebuilds with the new children.
|
|
2132
2425
|
clearMessageRenderCache(this);
|
|
@@ -2134,24 +2427,39 @@ function patchAssistantMessages(): void {
|
|
|
2134
2427
|
(this as any)[WORKED_START_KEY] = Date.now();
|
|
2135
2428
|
}
|
|
2136
2429
|
if (!message || !Array.isArray(message.content)) {
|
|
2137
|
-
return originalUpdateContent.call(this, message);
|
|
2430
|
+
return originalUpdateContent.call(this, message, isStreaming);
|
|
2138
2431
|
}
|
|
2139
|
-
|
|
2432
|
+
// Thinking display:
|
|
2433
|
+
// When thinking blocks are expanded via Ctrl+T (`hideThinkingBlock === false`),
|
|
2434
|
+
// all thinking blocks (old and new) render in full markdown.
|
|
2435
|
+
// When thinking blocks are collapsed (`hideThinkingBlock === true`):
|
|
2436
|
+
// - "live" mode (default): the actively-streaming thinking block renders
|
|
2437
|
+
// expanded while streaming, and collapses to `Thought for Xs` once done.
|
|
2438
|
+
// - "full" mode: behaves like stock pi (stays collapsed).
|
|
2439
|
+
const liveMode = getThinkingMode() === "live";
|
|
2440
|
+
const thinkingCollapsed = !!(this as any).hideThinkingBlock;
|
|
2441
|
+
const showLiveThinking = liveMode && thinkingCollapsed && isLiveThinkingMessage(this, message);
|
|
2442
|
+
if (thinkingCollapsed && messageHasThinkingContent(message)) {
|
|
2140
2443
|
// Pi wraps this in theme.italic/fg again — keep plain label for the placeholder pass.
|
|
2141
2444
|
(this as any).hiddenThinkingLabel = "Thinking…";
|
|
2142
2445
|
}
|
|
2143
|
-
|
|
2144
|
-
|
|
2446
|
+
if (showLiveThinking) (this as any).hideThinkingBlock = false;
|
|
2447
|
+
try {
|
|
2448
|
+
// Call original to build all children (text, thinking, spacers, errors)
|
|
2449
|
+
originalUpdateContent.call(this, message, isStreaming);
|
|
2450
|
+
} finally {
|
|
2451
|
+
if (showLiveThinking) (this as any).hideThinkingBlock = true;
|
|
2452
|
+
}
|
|
2145
2453
|
// Replace text-block Markdown children with DottedParagraph wrappers
|
|
2146
2454
|
const container = (this as any).contentContainer;
|
|
2147
2455
|
if (!container?.children) return;
|
|
2148
|
-
if (
|
|
2456
|
+
if (thinkingCollapsed && !showLiveThinking && messageHasThinkingContent(message)) {
|
|
2149
2457
|
replaceHiddenThinkingPlaceholders(container, message);
|
|
2150
2458
|
}
|
|
2151
2459
|
const mdTheme = (this as any).markdownTheme;
|
|
2152
2460
|
for (let i = container.children.length - 1; i >= 0; i--) {
|
|
2153
2461
|
const child = container.children[i];
|
|
2154
|
-
if (child
|
|
2462
|
+
if (isMarkdownComponent(child)) {
|
|
2155
2463
|
const text = (child as any).text;
|
|
2156
2464
|
if (!text) continue;
|
|
2157
2465
|
const isThinking = !!(child as any).defaultTextStyle?.italic;
|
|
@@ -2167,16 +2475,11 @@ function patchAssistantMessages(): void {
|
|
|
2167
2475
|
const explicitSessionTotal = (message as any)[WORKED_SESSION_TOTAL_KEY];
|
|
2168
2476
|
const explicitTurns = (message as any)[WORKED_TURNS_KEY];
|
|
2169
2477
|
// The "Turn took" line must only appear once the stream has truly closed.
|
|
2170
|
-
// `message.stopReason === "stop"` is
|
|
2171
|
-
//
|
|
2172
|
-
//
|
|
2173
|
-
//
|
|
2174
|
-
//
|
|
2175
|
-
// stamped onto the message by the `message_end` handler (which fires after
|
|
2176
|
-
// `message_delta`, when stopReason is the real final value), so gating on it
|
|
2177
|
-
// guarantees the line shows only after the run is actually done. The line is
|
|
2178
|
-
// baked into the message text at message_end; this child is just a fallback
|
|
2179
|
-
// for re-renders where that baked text isn't present.
|
|
2478
|
+
// `message.stopReason === "stop"` is not a safe "finished" signal here because
|
|
2479
|
+
// providers may initialize a live message with that value. The `message_end`
|
|
2480
|
+
// handler stamps `explicitDuration` after the final stream event. Render the
|
|
2481
|
+
// styled line as a TUI child so ANSI presentation never enters message content
|
|
2482
|
+
// or persisted session transcripts.
|
|
2180
2483
|
const isFinalAssistantMessage = message.stopReason === "stop";
|
|
2181
2484
|
const workedDuration = typeof explicitDuration === "number" ? explicitDuration : undefined;
|
|
2182
2485
|
const workedSessionTotal = typeof explicitSessionTotal === "number"
|
|
@@ -2326,17 +2629,31 @@ function patchToolExecutionRenderers(): void {
|
|
|
2326
2629
|
|
|
2327
2630
|
proto.getResultRenderer = function patchedGetResultRenderer() {
|
|
2328
2631
|
const toolName = typeof this?.toolName === "string" ? this.toolName : "";
|
|
2632
|
+
let renderer: any;
|
|
2329
2633
|
if (toolName === "apply_patch") {
|
|
2330
|
-
|
|
2634
|
+
renderer = (result: any, options: any, theme: Theme, ctx: any) =>
|
|
2331
2635
|
renderApplyPatchResult({ content: result.content, details: result.details }, options.isPartial, theme, ctx);
|
|
2332
|
-
}
|
|
2333
|
-
|
|
2334
|
-
return (result: any, options: any, theme: Theme, ctx: any) =>
|
|
2636
|
+
} else if (shouldUseGenericToolRenderer(toolName)) {
|
|
2637
|
+
renderer = (result: any, options: any, theme: Theme, ctx: any) =>
|
|
2335
2638
|
renderGenericToolResult(toolName, result, options, theme, ctx);
|
|
2639
|
+
} else {
|
|
2640
|
+
renderer = typeof originalGetResultRenderer === "function" ? originalGetResultRenderer.call(this) : undefined;
|
|
2336
2641
|
}
|
|
2337
|
-
|
|
2642
|
+
if (typeof renderer !== "function") return renderer;
|
|
2643
|
+
// Strip transient Magic Context tags from the text the renderer sees,
|
|
2644
|
+
// without touching the stored result message.
|
|
2645
|
+
return (result: any, options: any, theme: Theme, ctx: any) => renderer(sanitizeToolResultForDisplay(result), options, theme, ctx);
|
|
2338
2646
|
};
|
|
2339
2647
|
|
|
2648
|
+
// Fallback path for tools without a renderer definition formats raw text.
|
|
2649
|
+
const originalFormatToolExecution = proto.formatToolExecution;
|
|
2650
|
+
if (typeof originalFormatToolExecution === "function") {
|
|
2651
|
+
proto.formatToolExecution = function patchedFormatToolExecution(this: any, ...args: any[]) {
|
|
2652
|
+
const formatted = originalFormatToolExecution.apply(this, args);
|
|
2653
|
+
return typeof formatted === "string" ? stripTransientMagicContextTags(formatted) : formatted;
|
|
2654
|
+
};
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2340
2657
|
proto[TOOL_EXECUTION_PATCH_FLAG] = true;
|
|
2341
2658
|
}
|
|
2342
2659
|
|
|
@@ -2372,6 +2689,82 @@ function liveLineCountTrailing(ctx: any, theme: Theme): string {
|
|
|
2372
2689
|
return ` ${theme.fg("muted", `(${lineCountLabel(count)})`)}`;
|
|
2373
2690
|
}
|
|
2374
2691
|
|
|
2692
|
+
const BASH_STARTED_AT_KEY = "_bashStartedAtMs";
|
|
2693
|
+
const BASH_ENDED_AT_KEY = "_bashEndedAtMs";
|
|
2694
|
+
|
|
2695
|
+
type BashDurationEntry = { invalidate: () => void };
|
|
2696
|
+
|
|
2697
|
+
const BASH_DURATION_CONTEXTS = new Map<any, BashDurationEntry>();
|
|
2698
|
+
let bashDurationTimer: ReturnType<typeof setTimeout> | null = null;
|
|
2699
|
+
|
|
2700
|
+
function scheduleBashDurationTick(): void {
|
|
2701
|
+
if (bashDurationTimer || BASH_DURATION_CONTEXTS.size === 0) return;
|
|
2702
|
+
bashDurationTimer = setTimeout(() => {
|
|
2703
|
+
bashDurationTimer = null;
|
|
2704
|
+
for (const entry of BASH_DURATION_CONTEXTS.values()) {
|
|
2705
|
+
try { entry.invalidate(); } catch { /* noop */ }
|
|
2706
|
+
}
|
|
2707
|
+
scheduleBashDurationTick();
|
|
2708
|
+
}, 1_000);
|
|
2709
|
+
unrefTimer(bashDurationTimer);
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
function registerBashDurationContext(ctx: any): void {
|
|
2713
|
+
const key = ctx?.state ?? ctx;
|
|
2714
|
+
if (!key) return;
|
|
2715
|
+
const invalidate = typeof ctx?.invalidate === "function" ? () => safeInvalidate(ctx) : () => {};
|
|
2716
|
+
BASH_DURATION_CONTEXTS.set(key, { invalidate });
|
|
2717
|
+
scheduleBashDurationTick();
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
function clearBashDurationContext(ctx: any): void {
|
|
2721
|
+
const key = ctx?.state ?? ctx;
|
|
2722
|
+
if (key) BASH_DURATION_CONTEXTS.delete(key);
|
|
2723
|
+
if (bashDurationTimer && BASH_DURATION_CONTEXTS.size === 0) {
|
|
2724
|
+
clearTimeout(bashDurationTimer);
|
|
2725
|
+
bashDurationTimer = null;
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
function clearAllBashDurationContexts(): void {
|
|
2730
|
+
BASH_DURATION_CONTEXTS.clear();
|
|
2731
|
+
if (bashDurationTimer) {
|
|
2732
|
+
clearTimeout(bashDurationTimer);
|
|
2733
|
+
bashDurationTimer = null;
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
function syncBashDuration(ctx: any, isPartial = true): void {
|
|
2738
|
+
const state = ctx?.state;
|
|
2739
|
+
if (!state) return;
|
|
2740
|
+
if (state._toolStatus === "pending" && typeof state[BASH_STARTED_AT_KEY] !== "number") {
|
|
2741
|
+
state[BASH_STARTED_AT_KEY] = Date.now();
|
|
2742
|
+
delete state[BASH_ENDED_AT_KEY];
|
|
2743
|
+
}
|
|
2744
|
+
const startedAt = state[BASH_STARTED_AT_KEY];
|
|
2745
|
+
if (typeof startedAt !== "number") return;
|
|
2746
|
+
if (!isPartial || ctx?.isError) {
|
|
2747
|
+
if (typeof state[BASH_ENDED_AT_KEY] !== "number") state[BASH_ENDED_AT_KEY] = Date.now();
|
|
2748
|
+
clearBashDurationContext(ctx);
|
|
2749
|
+
return;
|
|
2750
|
+
}
|
|
2751
|
+
registerBashDurationContext(ctx);
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
function bashHeaderTrailing(ctx: any, theme: Theme): string {
|
|
2755
|
+
const parts: string[] = [];
|
|
2756
|
+
if (ctx?.isPartial === true) {
|
|
2757
|
+
const count = ctx?.state?._liveLineCount;
|
|
2758
|
+
if (typeof count === "number" && Number.isFinite(count) && count > 0) parts.push(lineCountLabel(count));
|
|
2759
|
+
}
|
|
2760
|
+
const startedAt = ctx?.state?.[BASH_STARTED_AT_KEY];
|
|
2761
|
+
if (typeof startedAt === "number") {
|
|
2762
|
+
const endedAt = ctx?.state?.[BASH_ENDED_AT_KEY];
|
|
2763
|
+
parts.push(formatBashDuration((typeof endedAt === "number" ? endedAt : Date.now()) - startedAt));
|
|
2764
|
+
}
|
|
2765
|
+
return parts.length > 0 ? `${TRAILING_MARK}${theme.fg("muted", parts.join(" · "))}` : "";
|
|
2766
|
+
}
|
|
2767
|
+
|
|
2375
2768
|
function setToolStatus(ctx: any, status: "pending" | "success" | "error" | "idle"): void {
|
|
2376
2769
|
if (ctx?.state) ctx.state._toolStatus = status;
|
|
2377
2770
|
}
|
|
@@ -2648,6 +3041,10 @@ function withBranch(content: string, theme: Theme, _isError = false, continued =
|
|
|
2648
3041
|
return `${branchLead(first, continued, theme)}\n${rest.join("\n")}`;
|
|
2649
3042
|
}
|
|
2650
3043
|
|
|
3044
|
+
function withClippedBranch(content: string, theme: Theme, continued = false): string {
|
|
3045
|
+
return withBranch(content, theme, false, continued).replaceAll(WRAP_MARK, CLIP_MARK);
|
|
3046
|
+
}
|
|
3047
|
+
|
|
2651
3048
|
function withFinalBranchBlock(content: string, theme: Theme, isError = false): string {
|
|
2652
3049
|
if (!content || !content.trim()) return "";
|
|
2653
3050
|
const lines = content.split("\n");
|
|
@@ -2918,6 +3315,16 @@ function markedContinuationPrefix(prefix: string): string {
|
|
|
2918
3315
|
}
|
|
2919
3316
|
|
|
2920
3317
|
function wrapMarkedLine(line: string, width: number): string[] {
|
|
3318
|
+
const clipIndex = line.indexOf(CLIP_MARK);
|
|
3319
|
+
if (clipIndex !== -1) {
|
|
3320
|
+
const prefix = line.slice(0, clipIndex);
|
|
3321
|
+
const body = line.slice(clipIndex + CLIP_MARK.length);
|
|
3322
|
+
if (body.includes(TRAILING_MARK)) return [alignTrailingMarkedLine(`${prefix}${body}`, width)];
|
|
3323
|
+
const bodyWidth = Math.max(1, width - visibleWidth(prefix));
|
|
3324
|
+
if (visibleWidth(body) <= bodyWidth) return [`${prefix}${body}`];
|
|
3325
|
+
const hint = "…";
|
|
3326
|
+
return [`${prefix}${truncateToWidth(body, Math.max(0, bodyWidth - visibleWidth(hint)), "", false)}${hint}`];
|
|
3327
|
+
}
|
|
2921
3328
|
const markerIndex = line.indexOf(WRAP_MARK);
|
|
2922
3329
|
if (markerIndex === -1) return wrapTextWithAnsi(line, width);
|
|
2923
3330
|
const prefix = line.slice(0, markerIndex);
|
|
@@ -2934,6 +3341,10 @@ class ToolText extends Text {
|
|
|
2934
3341
|
private toolCachedValue?: string;
|
|
2935
3342
|
private toolCachedWidth?: number;
|
|
2936
3343
|
private toolCachedLines?: string[];
|
|
3344
|
+
private observedWidth?: number;
|
|
3345
|
+
private pendingObservedWidth?: number;
|
|
3346
|
+
private widthObserver?: (width: number) => void;
|
|
3347
|
+
private widthObserverScheduled = false;
|
|
2937
3348
|
|
|
2938
3349
|
constructor(text = "") {
|
|
2939
3350
|
super("", 0, 0);
|
|
@@ -2946,6 +3357,26 @@ class ToolText extends Text {
|
|
|
2946
3357
|
this.invalidate();
|
|
2947
3358
|
}
|
|
2948
3359
|
|
|
3360
|
+
setWidthObserver(observer?: (width: number) => void): void {
|
|
3361
|
+
this.widthObserver = observer;
|
|
3362
|
+
if (!observer) this.pendingObservedWidth = undefined;
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
private observeWidth(width: number): void {
|
|
3366
|
+
if (this.observedWidth === width) return;
|
|
3367
|
+
this.observedWidth = width;
|
|
3368
|
+
if (!this.widthObserver) return;
|
|
3369
|
+
this.pendingObservedWidth = width;
|
|
3370
|
+
if (this.widthObserverScheduled) return;
|
|
3371
|
+
this.widthObserverScheduled = true;
|
|
3372
|
+
queueMicrotask(() => {
|
|
3373
|
+
this.widthObserverScheduled = false;
|
|
3374
|
+
const observed = this.pendingObservedWidth;
|
|
3375
|
+
this.pendingObservedWidth = undefined;
|
|
3376
|
+
if (observed !== undefined) this.widthObserver?.(observed);
|
|
3377
|
+
});
|
|
3378
|
+
}
|
|
3379
|
+
|
|
2949
3380
|
invalidate(): void {
|
|
2950
3381
|
this.toolCachedValue = undefined;
|
|
2951
3382
|
this.toolCachedWidth = undefined;
|
|
@@ -2953,6 +3384,7 @@ class ToolText extends Text {
|
|
|
2953
3384
|
}
|
|
2954
3385
|
|
|
2955
3386
|
render(width: number): string[] {
|
|
3387
|
+
this.observeWidth(width);
|
|
2956
3388
|
const branchKey = toolBranchRenderCacheKey();
|
|
2957
3389
|
if (
|
|
2958
3390
|
this.toolCachedLines
|
|
@@ -2969,7 +3401,9 @@ class ToolText extends Text {
|
|
|
2969
3401
|
}
|
|
2970
3402
|
const contentWidth = Math.max(1, width);
|
|
2971
3403
|
const lines = this.value.replace(/\t/g, " ").split("\n");
|
|
2972
|
-
const rendered = lines
|
|
3404
|
+
const rendered = lines
|
|
3405
|
+
.flatMap((line) => wrapMarkedLine(line, contentWidth))
|
|
3406
|
+
.map((line) => padToWidth(line, width));
|
|
2973
3407
|
this.toolCachedValue = this.value;
|
|
2974
3408
|
this.toolCachedWidth = width;
|
|
2975
3409
|
this.toolCachedLines = rendered;
|
|
@@ -2981,10 +3415,21 @@ class ToolText extends Text {
|
|
|
2981
3415
|
|
|
2982
3416
|
function makeText(last: unknown, text: string): Text {
|
|
2983
3417
|
const component = last instanceof ToolText ? last : new ToolText();
|
|
3418
|
+
component.setWidthObserver();
|
|
2984
3419
|
component.setText(text);
|
|
2985
3420
|
return component;
|
|
2986
3421
|
}
|
|
2987
3422
|
|
|
3423
|
+
function makeResponsiveDiffText(ctx: any, last: unknown, text: string): Text {
|
|
3424
|
+
const component = makeText(last, text) as ToolText;
|
|
3425
|
+
component.setWidthObserver((width) => {
|
|
3426
|
+
if (ctx.state?._diffComponentWidth === width) return;
|
|
3427
|
+
if (ctx.state) ctx.state._diffComponentWidth = width;
|
|
3428
|
+
safeInvalidate(ctx);
|
|
3429
|
+
});
|
|
3430
|
+
return component;
|
|
3431
|
+
}
|
|
3432
|
+
|
|
2988
3433
|
function previewLimit(): number {
|
|
2989
3434
|
const value = readSettings().previewLines;
|
|
2990
3435
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 8;
|
|
@@ -3003,6 +3448,29 @@ function bashCollapsedLimit(): number {
|
|
|
3003
3448
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : 10;
|
|
3004
3449
|
}
|
|
3005
3450
|
|
|
3451
|
+
function bashCommandPreviewLimit(): number {
|
|
3452
|
+
const value = readSettings().bashCommandPreviewLines;
|
|
3453
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : 8;
|
|
3454
|
+
}
|
|
3455
|
+
|
|
3456
|
+
function renderBashCommandBlock(
|
|
3457
|
+
command: string,
|
|
3458
|
+
expanded: boolean,
|
|
3459
|
+
theme: Theme,
|
|
3460
|
+
): string {
|
|
3461
|
+
const presentation = buildBashCommandPresentation(command);
|
|
3462
|
+
const limit = bashCommandPreviewLimit();
|
|
3463
|
+
if (!expanded && (limit === 0 || presentation.sourceLineCount < 2)) return "";
|
|
3464
|
+
const sourceLimit = expandedPreviewLimit();
|
|
3465
|
+
const lines = expanded ? presentation.sourceLines.slice(0, sourceLimit) : buildBashPreview(presentation.sourceLines, limit);
|
|
3466
|
+
if (lines.length === 0) return "";
|
|
3467
|
+
if (expanded && presentation.sourceLines.length > sourceLimit) {
|
|
3468
|
+
lines.push(`... ${presentation.sourceLines.length - sourceLimit} more command lines`);
|
|
3469
|
+
}
|
|
3470
|
+
const body = lines.map((line) => theme.fg("accent", line || " ")).join("\n");
|
|
3471
|
+
return expanded ? withBranch(body, theme, false, true) : withClippedBranch(body, theme, true);
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3006
3474
|
function liveToolPreviewEnabled(): boolean {
|
|
3007
3475
|
return readSettings().liveToolPreview !== false;
|
|
3008
3476
|
}
|
|
@@ -3470,12 +3938,12 @@ function rebindUiChromeToTheme(ctx: any): void {
|
|
|
3470
3938
|
const theme = ctx.ui?.theme;
|
|
3471
3939
|
invalidateThemePaletteCache();
|
|
3472
3940
|
clearHighlightCache();
|
|
3473
|
-
|
|
3941
|
+
applyDiffPalette();
|
|
3474
3942
|
bustSpinnerSettingsCache();
|
|
3475
3943
|
applyToolBackgroundMode(theme);
|
|
3476
3944
|
applyThemePaletteIfNeeded(theme);
|
|
3477
3945
|
syncDiffShikiTheme(theme);
|
|
3478
|
-
if (themeAdaptiveEnabled() && theme?.getFgAnsi) {
|
|
3946
|
+
if (themeAdaptiveEnabled() && theme?.getFgAnsi && !hasExplicitBgConfig) {
|
|
3479
3947
|
autoDeriveBgFromTheme(theme);
|
|
3480
3948
|
autoDerivePending = false;
|
|
3481
3949
|
}
|
|
@@ -3712,6 +4180,7 @@ function applyThemePaletteIfNeeded(theme: any): void {
|
|
|
3712
4180
|
|
|
3713
4181
|
function applyDiffPalette(): void {
|
|
3714
4182
|
const config = loadDiffConfig();
|
|
4183
|
+
hasExplicitBgConfig = false;
|
|
3715
4184
|
const preset = config.diffTheme ? DIFF_PRESETS[config.diffTheme] : null;
|
|
3716
4185
|
if (preset) hasExplicitBgConfig = true;
|
|
3717
4186
|
const overrides = config.diffColors ?? {};
|
|
@@ -3832,8 +4301,15 @@ function termW(): number {
|
|
|
3832
4301
|
return Math.max(40, Math.min(raw - 4, MAX_TERM_WIDTH));
|
|
3833
4302
|
}
|
|
3834
4303
|
|
|
3835
|
-
function branchDiffWidth(): number {
|
|
3836
|
-
|
|
4304
|
+
function branchDiffWidth(componentWidth?: number, chromeWidth = 2): number {
|
|
4305
|
+
const width = typeof componentWidth === "number" && Number.isFinite(componentWidth)
|
|
4306
|
+
? Math.floor(componentWidth)
|
|
4307
|
+
: termW();
|
|
4308
|
+
return Math.max(20, Math.min(width - chromeWidth, MAX_TERM_WIDTH));
|
|
4309
|
+
}
|
|
4310
|
+
|
|
4311
|
+
function contextDiffWidth(ctx: any, chromeWidth = 2): number {
|
|
4312
|
+
return branchDiffWidth(ctx.state?._diffComponentWidth, chromeWidth);
|
|
3837
4313
|
}
|
|
3838
4314
|
|
|
3839
4315
|
function adaptiveWrapRows(tw?: number): number {
|
|
@@ -4693,7 +5169,7 @@ function renderEditPreviewBody(
|
|
|
4693
5169
|
summary: string,
|
|
4694
5170
|
): void {
|
|
4695
5171
|
const dc = resolveDiffColors(theme);
|
|
4696
|
-
const branchWidth =
|
|
5172
|
+
const branchWidth = contextDiffWidth(ctx, 3);
|
|
4697
5173
|
if (operations.length === 1) {
|
|
4698
5174
|
const [diff] = diffs;
|
|
4699
5175
|
const line = lines[0] ?? getFirstChangedNewLine(diff);
|
|
@@ -4791,16 +5267,30 @@ function trackThinkingBlockEvents(event: any, ctx?: any): void {
|
|
|
4791
5267
|
}
|
|
4792
5268
|
if (evt.type === "thinking_end") {
|
|
4793
5269
|
thinkingBlockInFlight = false;
|
|
4794
|
-
const duration = Date.now() - thinkingBlockStartMs;
|
|
5270
|
+
const duration = Math.max(0, Date.now() - thinkingBlockStartMs);
|
|
4795
5271
|
if (message?.role === "assistant") delete (message as any)[THINKING_ACTIVE_KEY];
|
|
4796
|
-
|
|
4797
|
-
|
|
4798
|
-
if (message?.role === "assistant") (message as any)[THINKING_DURATION_KEY] = duration;
|
|
4799
|
-
} else {
|
|
4800
|
-
lastThinkingBlockDurationMs = undefined;
|
|
4801
|
-
if (message?.role === "assistant") delete (message as any)[THINKING_DURATION_KEY];
|
|
4802
|
-
}
|
|
5272
|
+
lastThinkingBlockDurationMs = duration;
|
|
5273
|
+
if (message?.role === "assistant") (message as any)[THINKING_DURATION_KEY] = duration;
|
|
4803
5274
|
refreshThinkingChrome();
|
|
5275
|
+
return;
|
|
5276
|
+
}
|
|
5277
|
+
// Fallback: some providers/models never emit thinking_end (a second
|
|
5278
|
+
// thinking_start can overwrite the first, or the turn can end with toolUse).
|
|
5279
|
+
// The live "Thinking..." row would otherwise stick forever while later calls
|
|
5280
|
+
// run normally. Any non-thinking stream event on the same assistant message
|
|
5281
|
+
// means thinking is no longer the live activity: freeze the elapsed time into
|
|
5282
|
+
// a "Thought for Xs" duration so the row always resolves.
|
|
5283
|
+
if ((message as any)?.[THINKING_ACTIVE_KEY] || thinkingBlockInFlight) {
|
|
5284
|
+
if (evt.type === "text_start" || evt.type === "text_delta" || evt.type === "toolcall_start" || evt.type === "toolcall_end") {
|
|
5285
|
+
thinkingBlockInFlight = false;
|
|
5286
|
+
const duration = thinkingBlockStartMs > 0 ? Math.max(0, Date.now() - thinkingBlockStartMs) : undefined;
|
|
5287
|
+
if (message?.role === "assistant") delete (message as any)[THINKING_ACTIVE_KEY];
|
|
5288
|
+
if (typeof duration === "number") {
|
|
5289
|
+
lastThinkingBlockDurationMs = duration;
|
|
5290
|
+
if (message?.role === "assistant") (message as any)[THINKING_DURATION_KEY] = duration;
|
|
5291
|
+
}
|
|
5292
|
+
refreshThinkingChrome();
|
|
5293
|
+
}
|
|
4804
5294
|
}
|
|
4805
5295
|
}
|
|
4806
5296
|
|
|
@@ -4826,7 +5316,6 @@ function registerThinkingLabels(pi: ExtensionAPI): void {
|
|
|
4826
5316
|
}
|
|
4827
5317
|
if (sessionStartMs === undefined) sessionStartMs = Date.now();
|
|
4828
5318
|
currentAssistantMessageStartMs = undefined;
|
|
4829
|
-
thinkingBlockInFlight = false;
|
|
4830
5319
|
});
|
|
4831
5320
|
pi.on("agent_start", async () => {
|
|
4832
5321
|
if (currentAgentWorkStartMs === undefined) {
|
|
@@ -4843,6 +5332,10 @@ function registerThinkingLabels(pi: ExtensionAPI): void {
|
|
|
4843
5332
|
if (message?.role === "assistant") {
|
|
4844
5333
|
currentAssistantMessageStartMs = Date.now();
|
|
4845
5334
|
(message as any)[WORKED_START_KEY] = currentAssistantMessageStartMs;
|
|
5335
|
+
// A new assistant message starts a fresh thinking lifecycle. Without
|
|
5336
|
+
// this, a missing thinking_end on the previous message leaves the
|
|
5337
|
+
// global in-flight flag set and the next message renders a stale
|
|
5338
|
+
// "Thinking..." row until its own thinking events arrive.
|
|
4846
5339
|
thinkingBlockInFlight = false;
|
|
4847
5340
|
delete (message as any)[THINKING_ACTIVE_KEY];
|
|
4848
5341
|
}
|
|
@@ -4854,9 +5347,22 @@ function registerThinkingLabels(pi: ExtensionAPI): void {
|
|
|
4854
5347
|
pi.on("message_end", async (event, ctx) => {
|
|
4855
5348
|
const message = (event as any)?.message;
|
|
4856
5349
|
if (message?.role === "assistant") {
|
|
4857
|
-
if
|
|
4858
|
-
|
|
5350
|
+
// Belt-and-suspenders: if thinking_end never fired (provider skipped
|
|
5351
|
+
// it, or the turn ended on toolUse/text), freeze any live "Thinking..."
|
|
5352
|
+
// into its "Thought for Xs" duration here so the row always resolves
|
|
5353
|
+
// at the end of the message instead of sticking into later calls.
|
|
5354
|
+
thinkingBlockInFlight = false;
|
|
5355
|
+
delete (message as any)[THINKING_ACTIVE_KEY];
|
|
5356
|
+
if (typeof (message as any)[THINKING_DURATION_KEY] !== "number") {
|
|
5357
|
+
const duration = thinkingBlockStartMs > 0 ? Math.max(0, Date.now() - thinkingBlockStartMs) : undefined;
|
|
5358
|
+
if (typeof duration === "number" && duration > 0) {
|
|
5359
|
+
lastThinkingBlockDurationMs = duration;
|
|
5360
|
+
(message as any)[THINKING_DURATION_KEY] = duration;
|
|
5361
|
+
} else if (typeof lastThinkingBlockDurationMs === "number" && lastThinkingBlockDurationMs > 0) {
|
|
5362
|
+
(message as any)[THINKING_DURATION_KEY] = lastThinkingBlockDurationMs;
|
|
5363
|
+
}
|
|
4859
5364
|
}
|
|
5365
|
+
thinkingBlockStartMs = 0;
|
|
4860
5366
|
const started = typeof currentAgentWorkStartMs === "number"
|
|
4861
5367
|
? currentAgentWorkStartMs
|
|
4862
5368
|
: typeof (message as any)[WORKED_START_KEY] === "number"
|
|
@@ -4870,6 +5376,8 @@ function registerThinkingLabels(pi: ExtensionAPI): void {
|
|
|
4870
5376
|
(message as any)[WORKED_DURATION_KEY] = durationMs;
|
|
4871
5377
|
if (typeof sessionTotalMs === "number") (message as any)[WORKED_SESSION_TOTAL_KEY] = sessionTotalMs;
|
|
4872
5378
|
if (typeof turns === "number") (message as any)[WORKED_TURNS_KEY] = turns;
|
|
5379
|
+
// Duration metadata drives the assistant component's TUI-only status line.
|
|
5380
|
+
// Message content stays presentation-neutral for persistence and consumers.
|
|
4873
5381
|
}
|
|
4874
5382
|
currentAssistantMessageStartMs = undefined;
|
|
4875
5383
|
}
|
|
@@ -5509,7 +6017,7 @@ function renderApplyPatchCall(args: any, theme: Theme, ctx: any, sp: (path: stri
|
|
|
5509
6017
|
}
|
|
5510
6018
|
ctx.state._openAiPatchFiles = preview.changes.map((change) => change.displayPath);
|
|
5511
6019
|
|
|
5512
|
-
const diffWidth =
|
|
6020
|
+
const diffWidth = contextDiffWidth(ctx);
|
|
5513
6021
|
const key = `apply-preview:${ctx.state._applyPatchMetaKey ?? hashText(patchText)}:${diffWidth}:${ctx.expanded ? 1 : 0}`;
|
|
5514
6022
|
if (ctx.state._applyPatchPreviewKey !== key) {
|
|
5515
6023
|
ctx.state._applyPatchPreviewKey = key;
|
|
@@ -5562,7 +6070,7 @@ function renderApplyPatchCall(args: any, theme: Theme, ctx: any, sp: (path: stri
|
|
|
5562
6070
|
}
|
|
5563
6071
|
|
|
5564
6072
|
const body = ctx.state._applyPatchPreviewDisplay as string | undefined;
|
|
5565
|
-
return
|
|
6073
|
+
return makeResponsiveDiffText(ctx, ctx.lastComponent, body ? `${hdr}\n${body}` : hdr);
|
|
5566
6074
|
}
|
|
5567
6075
|
|
|
5568
6076
|
function renderApplyPatchResult(result: any, isPartial: boolean, theme: Theme, ctx: any): Text {
|
|
@@ -5911,6 +6419,7 @@ function renderOpenAiToolResult(name: string, result: any, expanded: boolean, is
|
|
|
5911
6419
|
// ===========================================================================
|
|
5912
6420
|
|
|
5913
6421
|
export default function (pi: ExtensionAPI) {
|
|
6422
|
+
patchTerminalWriteTagScrubber();
|
|
5914
6423
|
patchToolExecutionBackgroundSync();
|
|
5915
6424
|
patchToolRenderCacheInvalidation();
|
|
5916
6425
|
patchReadImageExpansion();
|
|
@@ -5938,7 +6447,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
5938
6447
|
// /cc-tools command — control tool chrome, grouping, and detail level.
|
|
5939
6448
|
const TOOL_MODES = ["outlines", "transparent", "default"] as const;
|
|
5940
6449
|
const TOOL_BOOL_MODES = ["on", "off", "toggle", "status"] as const;
|
|
5941
|
-
const TOOL_SUBCOMMANDS = [...TOOL_MODES, "group", "detail", "branch", "status"] as const;
|
|
6450
|
+
const TOOL_SUBCOMMANDS = [...TOOL_MODES, "group", "detail", "thinking", "branch", "status"] as const;
|
|
5942
6451
|
const booleanMode = (raw: string | undefined, current: boolean): boolean | "status" | undefined => {
|
|
5943
6452
|
const mode = raw || "toggle";
|
|
5944
6453
|
if (mode === "on") return true;
|
|
@@ -5961,6 +6470,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
5961
6470
|
ctx.ui.notify([
|
|
5962
6471
|
`Tool style: ${toolBackgroundMode}`,
|
|
5963
6472
|
`Tool grouping: ${toolGroupingEnabled() ? "on" : "off"}`,
|
|
6473
|
+
`Thinking: ${getThinkingMode()}`,
|
|
5964
6474
|
`Extra detail: ${extraToolOutputExpanded ? "on" : "off"} (${rawKeyHint("ctrl+shift+o", "toggle")})`,
|
|
5965
6475
|
branchLine,
|
|
5966
6476
|
` /cc-tools branch <0-255> | theme | fixed | reset`,
|
|
@@ -5979,6 +6489,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
5979
6489
|
label: m,
|
|
5980
6490
|
description:
|
|
5981
6491
|
m === "group" ? "Toggle grouped adjacent/concurrent tool rows"
|
|
6492
|
+
: m === "thinking" ? "Thinking display: live (default) or full"
|
|
5982
6493
|
: m === "detail" ? "Toggle Ctrl+Shift+O extra-detail mode"
|
|
5983
6494
|
: m === "branch" ? "├ └ │ gray (0-255), theme, fixed, or reset"
|
|
5984
6495
|
: m === "status" ? "Show tool UI settings"
|
|
@@ -5994,6 +6505,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
5994
6505
|
.filter((o) => o.startsWith(second))
|
|
5995
6506
|
.map((o) => ({ value: `branch ${o}`, label: o, description: "Branch connector color" }));
|
|
5996
6507
|
}
|
|
6508
|
+
if (first === "thinking") {
|
|
6509
|
+
const second = parts[1] ?? "";
|
|
6510
|
+
return ["live", "full", "status"]
|
|
6511
|
+
.filter((m) => m.startsWith(second))
|
|
6512
|
+
.map((m) => ({ value: `thinking ${m}`, label: m, description: `${m} thinking display` }));
|
|
6513
|
+
}
|
|
5997
6514
|
if (first === "group" || first === "detail" || first === "extra") {
|
|
5998
6515
|
const second = parts[1] ?? "";
|
|
5999
6516
|
return TOOL_BOOL_MODES
|
|
@@ -6066,6 +6583,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
6066
6583
|
return;
|
|
6067
6584
|
}
|
|
6068
6585
|
|
|
6586
|
+
if (sub === "thinking") {
|
|
6587
|
+
const arg = (parts[1] ?? "status").toLowerCase();
|
|
6588
|
+
if (arg === "status" || !arg) {
|
|
6589
|
+
if (ctx.hasUI) ctx.ui.notify(`Thinking: ${getThinkingMode()} (live = only streaming thinking expands; full = always expanded)`, "info");
|
|
6590
|
+
return;
|
|
6591
|
+
}
|
|
6592
|
+
if (arg !== "live" && arg !== "full") {
|
|
6593
|
+
if (ctx.hasUI) ctx.ui.notify("Usage: /cc-tools thinking live|full|status", "error");
|
|
6594
|
+
return;
|
|
6595
|
+
}
|
|
6596
|
+
writeSettingsKey("thinkingMode", arg);
|
|
6597
|
+
if (ctx.hasUI) {
|
|
6598
|
+
ctx.ui.setToolsExpanded(ctx.ui.getToolsExpanded());
|
|
6599
|
+
ctx.ui.notify(`Thinking → ${arg}${arg === "live" ? " (only the active thinking expands)" : " (thinking always expanded)"}`, "info");
|
|
6600
|
+
}
|
|
6601
|
+
return;
|
|
6602
|
+
}
|
|
6603
|
+
|
|
6069
6604
|
if (sub === "detail" || sub === "extra") {
|
|
6070
6605
|
const next = booleanMode(parts[1], extraToolOutputExpanded);
|
|
6071
6606
|
if (next === undefined) {
|
|
@@ -6085,7 +6620,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6085
6620
|
}
|
|
6086
6621
|
|
|
6087
6622
|
if (!(TOOL_MODES as readonly string[]).includes(sub)) {
|
|
6088
|
-
if (ctx.hasUI) ctx.ui.notify(`Unknown option "${sub}". Try /cc-tools status, /cc-tools
|
|
6623
|
+
if (ctx.hasUI) ctx.ui.notify(`Unknown option "${sub}". Try /cc-tools status, /cc-tools thinking live, or /cc-tools group toggle.`, "error");
|
|
6089
6624
|
return;
|
|
6090
6625
|
}
|
|
6091
6626
|
toolBackgroundOverride = sub as typeof toolBackgroundMode;
|
|
@@ -6379,15 +6914,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
6379
6914
|
},
|
|
6380
6915
|
renderCall(args, theme, ctx) {
|
|
6381
6916
|
syncToolCallStatus(ctx);
|
|
6917
|
+
syncBashDuration(ctx);
|
|
6382
6918
|
const rewrite = ensureRtkRewriteForContext(ctx, args);
|
|
6383
|
-
const
|
|
6919
|
+
const command = typeof args.command === "string" ? args.command : "";
|
|
6920
|
+
const presentation = buildBashCommandPresentation(command);
|
|
6921
|
+
const summary = stableCallSummary(ctx, "_bashHeadline", () => presentation.headline);
|
|
6384
6922
|
const rtkBadge = rewrite ? theme.fg("muted", " (RTK)") : "";
|
|
6385
|
-
|
|
6386
|
-
|
|
6387
|
-
|
|
6388
|
-
);
|
|
6923
|
+
const status = ctx?.state?._toolStatus;
|
|
6924
|
+
const showCommand = ctx.argsComplete === true && (status === "pending" || status === "error" || ctx.expanded === true);
|
|
6925
|
+
const commandBlock = showCommand ? renderBashCommandBlock(command, ctx.expanded === true, theme) : "";
|
|
6926
|
+
const headerSummary = ctx.expanded === true && commandBlock ? describeBashSource(presentation) : summary;
|
|
6927
|
+
const header = toolHeader(
|
|
6928
|
+
"Bash",
|
|
6929
|
+
`${headerSummary}${rtkBadge}`,
|
|
6930
|
+
theme,
|
|
6931
|
+
toolStatusDot(ctx, theme),
|
|
6932
|
+
bashHeaderTrailing(ctx, theme),
|
|
6933
|
+
).replace(WRAP_MARK, CLIP_MARK);
|
|
6934
|
+
return makeText(ctx.lastComponent, commandBlock ? `${header}\n${commandBlock}` : header);
|
|
6389
6935
|
},
|
|
6390
6936
|
renderResult(result, { expanded, isPartial }, theme, ctx) {
|
|
6937
|
+
syncBashDuration(ctx, isPartial);
|
|
6391
6938
|
const details = result.details as BashToolDetails | undefined;
|
|
6392
6939
|
const rewrite = ensureRtkRewriteForContext(ctx, ctx.args);
|
|
6393
6940
|
const output = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
@@ -6632,7 +7179,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6632
7179
|
if (d?._type === "diff") {
|
|
6633
7180
|
const previewLines = ctx.expanded ? MAX_RENDER_LINES : diffCollapsedLimit();
|
|
6634
7181
|
const hunks = d.diff?.lines?.filter((l: any) => l.type === "sep").length + (d.diff?.lines?.length ? 1 : 0);
|
|
6635
|
-
const diffWidth =
|
|
7182
|
+
const diffWidth = contextDiffWidth(ctx);
|
|
6636
7183
|
const mode = shouldUseSplit(d.diff, diffWidth, previewLines) ? "split" : "unified";
|
|
6637
7184
|
const richSummary = diffSummaryWithMeta(d.diff.added, d.diff.removed, hunks, mode);
|
|
6638
7185
|
const key = `wd:${diffWidth}:${d.summary}:${d.diff?.lines?.length ?? 0}:${d.language ?? ""}:${ctx.expanded ? 1 : 0}`;
|
|
@@ -6652,7 +7199,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6652
7199
|
safeInvalidate(ctx);
|
|
6653
7200
|
});
|
|
6654
7201
|
}
|
|
6655
|
-
return
|
|
7202
|
+
return makeResponsiveDiffText(ctx, ctx.lastComponent, ctx.state._wdt ?? withBranch(richSummary, theme));
|
|
6656
7203
|
}
|
|
6657
7204
|
if (d?._type === "noChange") return makeText(ctx.lastComponent, withBranch(theme.fg("muted", "✓ no changes"), theme));
|
|
6658
7205
|
if (d?._type === "new") {
|
|
@@ -6662,7 +7209,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6662
7209
|
const syntheticDiff = getCachedParsedDiff(ctx, `nf-diff:${d.filePath}:${contentHash}`, "", content);
|
|
6663
7210
|
const richSummary = diffSummaryWithMeta(syntheticDiff.added, 0, 1, "new file");
|
|
6664
7211
|
const previewLines = ctx.expanded ? MAX_RENDER_LINES : diffCollapsedLimit();
|
|
6665
|
-
const diffWidth =
|
|
7212
|
+
const diffWidth = contextDiffWidth(ctx);
|
|
6666
7213
|
const pk = `nf:${d.filePath}:${contentHash}:${diffWidth}:${ctx.expanded ? 1 : 0}`;
|
|
6667
7214
|
if (ctx.state._nfk !== pk) {
|
|
6668
7215
|
ctx.state._nfk = pk;
|
|
@@ -6680,7 +7227,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6680
7227
|
safeInvalidate(ctx);
|
|
6681
7228
|
});
|
|
6682
7229
|
}
|
|
6683
|
-
return
|
|
7230
|
+
return makeResponsiveDiffText(ctx, ctx.lastComponent, ctx.state._nft ?? withBranch(`${richSummary} ${theme.fg("muted", `(${lineTotal} lines)`)}`, theme));
|
|
6684
7231
|
}
|
|
6685
7232
|
return makeText(ctx.lastComponent, withBranch(theme.fg("success", "Written"), theme));
|
|
6686
7233
|
},
|
|
@@ -6735,7 +7282,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6735
7282
|
syncToolCallStatus(ctx);
|
|
6736
7283
|
const hdr = toolHeader("Edit", summary, theme, ` ${toolStatusDot(ctx, theme)}`, liveLineCountTrailing(ctx, theme));
|
|
6737
7284
|
if (!(ctx.argsComplete && operations.length > 0)) return makeText(ctx.lastComponent, hdr);
|
|
6738
|
-
const diffWidth =
|
|
7285
|
+
const diffWidth = contextDiffWidth(ctx, 3);
|
|
6739
7286
|
const key = `edit:${fp}:${hashText(operations.map((edit) => `${edit.oldText}\u0000${edit.newText}`).join("\u0001"))}:${diffWidth}:${ctx.expanded ? 1 : 0}`;
|
|
6740
7287
|
const { diffs: fallbackDiffs, summary: editSummary } = getCachedEditOperationSummary(ctx, key, operations);
|
|
6741
7288
|
if (ctx.state._pk !== key) {
|
|
@@ -6755,8 +7302,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
6755
7302
|
renderEditPreviewBody(ctx, key, theme, lg, operations, fallbackDiffs, fallbackDiffs.map((diff) => getFirstChangedNewLine(diff)), editSummary);
|
|
6756
7303
|
});
|
|
6757
7304
|
}
|
|
6758
|
-
|
|
6759
|
-
return
|
|
7305
|
+
const body = liveBranchDisplay(ctx.state, theme) ?? (ctx.state._ptDisplay as string | undefined);
|
|
7306
|
+
return makeResponsiveDiffText(ctx, ctx.lastComponent, body ? `${hdr}\n${body}` : hdr);
|
|
6760
7307
|
},
|
|
6761
7308
|
renderResult(result, { expanded, isPartial }, theme, ctx) {
|
|
6762
7309
|
if (isPartial) {
|
|
@@ -6909,6 +7456,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6909
7456
|
});
|
|
6910
7457
|
pi.on("session_shutdown", async () => {
|
|
6911
7458
|
_clearAllBlinkContexts();
|
|
7459
|
+
clearAllBashDurationContexts();
|
|
6912
7460
|
clearRtkRewriteState();
|
|
6913
7461
|
WRITE_EXISTED_BEFORE.clear();
|
|
6914
7462
|
clearHighlightCache();
|