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.
- 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 +654 -123
- 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));
|
|
1343
1587
|
}
|
|
1344
1588
|
|
|
1345
|
-
function
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
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
|
-
|
|
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 =
|
|
@@ -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
|
-
|
|
1786
|
-
|
|
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(
|
|
1790
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2164
|
-
|
|
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 (
|
|
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
|
-
|
|
2175
|
-
|
|
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 = !!(
|
|
2472
|
+
const isThinking = !!(inner as any).defaultTextStyle?.italic;
|
|
2178
2473
|
if (isThinking) {
|
|
2179
|
-
const style = (
|
|
2180
|
-
|
|
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
|
-
|
|
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
|
|
2191
|
-
//
|
|
2192
|
-
//
|
|
2193
|
-
//
|
|
2194
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
4831
|
-
|
|
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
|
|
4892
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
6793
|
-
return
|
|
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();
|