pi-claude-code-ui 1.0.79 → 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 +13 -0
- package/README.md +10 -4
- package/config/config.example.json +2 -0
- package/extensions/bash-command.ts +84 -0
- package/extensions/index.ts +631 -117
- 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
|
-
: assistantMessageThinkingComplete(message) && typeof lastThinkingBlockDurationMs === "number"
|
|
1353
|
-
? lastThinkingBlockDurationMs
|
|
1354
|
-
: undefined;
|
|
1355
|
-
if (typeof durationMs === "number" && durationMs >= MIN_THINKING_SUMMARY_MS) {
|
|
1356
|
-
return thoughtDurationSummaryText(durationMs);
|
|
1589
|
+
function assistantMessageThinkingComplete(this: any, message: any): boolean {
|
|
1590
|
+
return isAssistantThinkingComplete(this, message);
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
function hiddenThinkingSummaryForMessage(message: any, comp?: any): string {
|
|
1594
|
+
if (message && !isAssistantThinkingComplete(comp, message) && isLiveThinkingMessage(comp, message)) {
|
|
1595
|
+
return thinkingActiveSummaryText();
|
|
1357
1596
|
}
|
|
1358
|
-
|
|
1359
|
-
|
|
1597
|
+
const durationMs = getMessageThinkingDurationMs(message);
|
|
1598
|
+
if (message && typeof message === "object") {
|
|
1599
|
+
(message as any)[THINKING_DURATION_KEY] = durationMs;
|
|
1600
|
+
}
|
|
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,27 @@ class DottedParagraph {
|
|
|
1780
2040
|
function replaceHiddenThinkingPlaceholders(container: { children?: any[] }, message: any): void {
|
|
1781
2041
|
if (!container?.children) return;
|
|
1782
2042
|
const summary = hiddenThinkingSummaryForMessage(message);
|
|
2043
|
+
let firstReplaced = false;
|
|
1783
2044
|
for (let i = 0; i < container.children.length; i++) {
|
|
1784
2045
|
const child = container.children[i];
|
|
1785
|
-
if (child instanceof HiddenThinkingSummary) {
|
|
1786
|
-
|
|
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
|
+
}
|
|
1787
2054
|
continue;
|
|
1788
2055
|
}
|
|
1789
2056
|
if (isHiddenThinkingPlaceholderText(child)) {
|
|
1790
|
-
|
|
2057
|
+
if (!firstReplaced) {
|
|
2058
|
+
container.children[i] = new HiddenThinkingSummary(summary);
|
|
2059
|
+
firstReplaced = true;
|
|
2060
|
+
} else {
|
|
2061
|
+
container.children.splice(i, 1);
|
|
2062
|
+
i--;
|
|
2063
|
+
}
|
|
1791
2064
|
}
|
|
1792
2065
|
}
|
|
1793
2066
|
}
|
|
@@ -2073,7 +2346,7 @@ function visitMarkdownDescendants(root: unknown, visit: (md: InstanceType<typeof
|
|
|
2073
2346
|
if (!root || typeof root !== "object") return;
|
|
2074
2347
|
const node = root as { children?: unknown[] };
|
|
2075
2348
|
for (const child of node.children ?? []) {
|
|
2076
|
-
if (child
|
|
2349
|
+
if (isMarkdownComponent(child)) visit(child);
|
|
2077
2350
|
else visitMarkdownDescendants(child, visit);
|
|
2078
2351
|
}
|
|
2079
2352
|
}
|
|
@@ -2107,7 +2380,7 @@ function patchUserMessageRender(): void {
|
|
|
2107
2380
|
if (!Array.isArray(lines) || lines.length === 0) return lines;
|
|
2108
2381
|
const rendered = [
|
|
2109
2382
|
roundedUserBorder(borderWidth, true),
|
|
2110
|
-
...lines.map((line: string) => borderedUserMessageLine(line, borderWidth)),
|
|
2383
|
+
...lines.slice(1, -1).map((line: string) => borderedUserMessageLine(line, borderWidth)),
|
|
2111
2384
|
roundedUserBorder(borderWidth, false),
|
|
2112
2385
|
];
|
|
2113
2386
|
const clamped = rendered.map((line) => clampLineWidth(line, borderWidth));
|
|
@@ -2146,7 +2419,7 @@ function patchAssistantMessages(): void {
|
|
|
2146
2419
|
proto[ASSISTANT_RENDER_PATCH_FLAG] = true;
|
|
2147
2420
|
}
|
|
2148
2421
|
const originalUpdateContent = proto.updateContent;
|
|
2149
|
-
proto.updateContent = function patchedUpdateContent(message: any) {
|
|
2422
|
+
proto.updateContent = function patchedUpdateContent(message: any, isStreaming?: boolean) {
|
|
2150
2423
|
// Content changed (also reached via invalidate() → updateContent): drop the
|
|
2151
2424
|
// cached rendered output so the next render rebuilds with the new children.
|
|
2152
2425
|
clearMessageRenderCache(this);
|
|
@@ -2154,24 +2427,39 @@ function patchAssistantMessages(): void {
|
|
|
2154
2427
|
(this as any)[WORKED_START_KEY] = Date.now();
|
|
2155
2428
|
}
|
|
2156
2429
|
if (!message || !Array.isArray(message.content)) {
|
|
2157
|
-
return originalUpdateContent.call(this, message);
|
|
2430
|
+
return originalUpdateContent.call(this, message, isStreaming);
|
|
2158
2431
|
}
|
|
2159
|
-
|
|
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)) {
|
|
2160
2443
|
// Pi wraps this in theme.italic/fg again — keep plain label for the placeholder pass.
|
|
2161
2444
|
(this as any).hiddenThinkingLabel = "Thinking…";
|
|
2162
2445
|
}
|
|
2163
|
-
|
|
2164
|
-
|
|
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
|
+
}
|
|
2165
2453
|
// Replace text-block Markdown children with DottedParagraph wrappers
|
|
2166
2454
|
const container = (this as any).contentContainer;
|
|
2167
2455
|
if (!container?.children) return;
|
|
2168
|
-
if (
|
|
2456
|
+
if (thinkingCollapsed && !showLiveThinking && messageHasThinkingContent(message)) {
|
|
2169
2457
|
replaceHiddenThinkingPlaceholders(container, message);
|
|
2170
2458
|
}
|
|
2171
2459
|
const mdTheme = (this as any).markdownTheme;
|
|
2172
2460
|
for (let i = container.children.length - 1; i >= 0; i--) {
|
|
2173
2461
|
const child = container.children[i];
|
|
2174
|
-
if (child
|
|
2462
|
+
if (isMarkdownComponent(child)) {
|
|
2175
2463
|
const text = (child as any).text;
|
|
2176
2464
|
if (!text) continue;
|
|
2177
2465
|
const isThinking = !!(child as any).defaultTextStyle?.italic;
|
|
@@ -2187,16 +2475,11 @@ function patchAssistantMessages(): void {
|
|
|
2187
2475
|
const explicitSessionTotal = (message as any)[WORKED_SESSION_TOTAL_KEY];
|
|
2188
2476
|
const explicitTurns = (message as any)[WORKED_TURNS_KEY];
|
|
2189
2477
|
// 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.
|
|
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.
|
|
2200
2483
|
const isFinalAssistantMessage = message.stopReason === "stop";
|
|
2201
2484
|
const workedDuration = typeof explicitDuration === "number" ? explicitDuration : undefined;
|
|
2202
2485
|
const workedSessionTotal = typeof explicitSessionTotal === "number"
|
|
@@ -2406,6 +2689,82 @@ function liveLineCountTrailing(ctx: any, theme: Theme): string {
|
|
|
2406
2689
|
return ` ${theme.fg("muted", `(${lineCountLabel(count)})`)}`;
|
|
2407
2690
|
}
|
|
2408
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
|
+
|
|
2409
2768
|
function setToolStatus(ctx: any, status: "pending" | "success" | "error" | "idle"): void {
|
|
2410
2769
|
if (ctx?.state) ctx.state._toolStatus = status;
|
|
2411
2770
|
}
|
|
@@ -2682,6 +3041,10 @@ function withBranch(content: string, theme: Theme, _isError = false, continued =
|
|
|
2682
3041
|
return `${branchLead(first, continued, theme)}\n${rest.join("\n")}`;
|
|
2683
3042
|
}
|
|
2684
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
|
+
|
|
2685
3048
|
function withFinalBranchBlock(content: string, theme: Theme, isError = false): string {
|
|
2686
3049
|
if (!content || !content.trim()) return "";
|
|
2687
3050
|
const lines = content.split("\n");
|
|
@@ -2952,6 +3315,16 @@ function markedContinuationPrefix(prefix: string): string {
|
|
|
2952
3315
|
}
|
|
2953
3316
|
|
|
2954
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
|
+
}
|
|
2955
3328
|
const markerIndex = line.indexOf(WRAP_MARK);
|
|
2956
3329
|
if (markerIndex === -1) return wrapTextWithAnsi(line, width);
|
|
2957
3330
|
const prefix = line.slice(0, markerIndex);
|
|
@@ -2968,6 +3341,10 @@ class ToolText extends Text {
|
|
|
2968
3341
|
private toolCachedValue?: string;
|
|
2969
3342
|
private toolCachedWidth?: number;
|
|
2970
3343
|
private toolCachedLines?: string[];
|
|
3344
|
+
private observedWidth?: number;
|
|
3345
|
+
private pendingObservedWidth?: number;
|
|
3346
|
+
private widthObserver?: (width: number) => void;
|
|
3347
|
+
private widthObserverScheduled = false;
|
|
2971
3348
|
|
|
2972
3349
|
constructor(text = "") {
|
|
2973
3350
|
super("", 0, 0);
|
|
@@ -2980,6 +3357,26 @@ class ToolText extends Text {
|
|
|
2980
3357
|
this.invalidate();
|
|
2981
3358
|
}
|
|
2982
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
|
+
|
|
2983
3380
|
invalidate(): void {
|
|
2984
3381
|
this.toolCachedValue = undefined;
|
|
2985
3382
|
this.toolCachedWidth = undefined;
|
|
@@ -2987,6 +3384,7 @@ class ToolText extends Text {
|
|
|
2987
3384
|
}
|
|
2988
3385
|
|
|
2989
3386
|
render(width: number): string[] {
|
|
3387
|
+
this.observeWidth(width);
|
|
2990
3388
|
const branchKey = toolBranchRenderCacheKey();
|
|
2991
3389
|
if (
|
|
2992
3390
|
this.toolCachedLines
|
|
@@ -3003,7 +3401,9 @@ class ToolText extends Text {
|
|
|
3003
3401
|
}
|
|
3004
3402
|
const contentWidth = Math.max(1, width);
|
|
3005
3403
|
const lines = this.value.replace(/\t/g, " ").split("\n");
|
|
3006
|
-
const rendered = lines
|
|
3404
|
+
const rendered = lines
|
|
3405
|
+
.flatMap((line) => wrapMarkedLine(line, contentWidth))
|
|
3406
|
+
.map((line) => padToWidth(line, width));
|
|
3007
3407
|
this.toolCachedValue = this.value;
|
|
3008
3408
|
this.toolCachedWidth = width;
|
|
3009
3409
|
this.toolCachedLines = rendered;
|
|
@@ -3015,10 +3415,21 @@ class ToolText extends Text {
|
|
|
3015
3415
|
|
|
3016
3416
|
function makeText(last: unknown, text: string): Text {
|
|
3017
3417
|
const component = last instanceof ToolText ? last : new ToolText();
|
|
3418
|
+
component.setWidthObserver();
|
|
3018
3419
|
component.setText(text);
|
|
3019
3420
|
return component;
|
|
3020
3421
|
}
|
|
3021
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
|
+
|
|
3022
3433
|
function previewLimit(): number {
|
|
3023
3434
|
const value = readSettings().previewLines;
|
|
3024
3435
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.floor(value) : 8;
|
|
@@ -3037,6 +3448,29 @@ function bashCollapsedLimit(): number {
|
|
|
3037
3448
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : 10;
|
|
3038
3449
|
}
|
|
3039
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
|
+
|
|
3040
3474
|
function liveToolPreviewEnabled(): boolean {
|
|
3041
3475
|
return readSettings().liveToolPreview !== false;
|
|
3042
3476
|
}
|
|
@@ -3504,12 +3938,12 @@ function rebindUiChromeToTheme(ctx: any): void {
|
|
|
3504
3938
|
const theme = ctx.ui?.theme;
|
|
3505
3939
|
invalidateThemePaletteCache();
|
|
3506
3940
|
clearHighlightCache();
|
|
3507
|
-
|
|
3941
|
+
applyDiffPalette();
|
|
3508
3942
|
bustSpinnerSettingsCache();
|
|
3509
3943
|
applyToolBackgroundMode(theme);
|
|
3510
3944
|
applyThemePaletteIfNeeded(theme);
|
|
3511
3945
|
syncDiffShikiTheme(theme);
|
|
3512
|
-
if (themeAdaptiveEnabled() && theme?.getFgAnsi) {
|
|
3946
|
+
if (themeAdaptiveEnabled() && theme?.getFgAnsi && !hasExplicitBgConfig) {
|
|
3513
3947
|
autoDeriveBgFromTheme(theme);
|
|
3514
3948
|
autoDerivePending = false;
|
|
3515
3949
|
}
|
|
@@ -3746,6 +4180,7 @@ function applyThemePaletteIfNeeded(theme: any): void {
|
|
|
3746
4180
|
|
|
3747
4181
|
function applyDiffPalette(): void {
|
|
3748
4182
|
const config = loadDiffConfig();
|
|
4183
|
+
hasExplicitBgConfig = false;
|
|
3749
4184
|
const preset = config.diffTheme ? DIFF_PRESETS[config.diffTheme] : null;
|
|
3750
4185
|
if (preset) hasExplicitBgConfig = true;
|
|
3751
4186
|
const overrides = config.diffColors ?? {};
|
|
@@ -3866,8 +4301,15 @@ function termW(): number {
|
|
|
3866
4301
|
return Math.max(40, Math.min(raw - 4, MAX_TERM_WIDTH));
|
|
3867
4302
|
}
|
|
3868
4303
|
|
|
3869
|
-
function branchDiffWidth(): number {
|
|
3870
|
-
|
|
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);
|
|
3871
4313
|
}
|
|
3872
4314
|
|
|
3873
4315
|
function adaptiveWrapRows(tw?: number): number {
|
|
@@ -4727,7 +5169,7 @@ function renderEditPreviewBody(
|
|
|
4727
5169
|
summary: string,
|
|
4728
5170
|
): void {
|
|
4729
5171
|
const dc = resolveDiffColors(theme);
|
|
4730
|
-
const branchWidth =
|
|
5172
|
+
const branchWidth = contextDiffWidth(ctx, 3);
|
|
4731
5173
|
if (operations.length === 1) {
|
|
4732
5174
|
const [diff] = diffs;
|
|
4733
5175
|
const line = lines[0] ?? getFirstChangedNewLine(diff);
|
|
@@ -4825,16 +5267,30 @@ function trackThinkingBlockEvents(event: any, ctx?: any): void {
|
|
|
4825
5267
|
}
|
|
4826
5268
|
if (evt.type === "thinking_end") {
|
|
4827
5269
|
thinkingBlockInFlight = false;
|
|
4828
|
-
const duration = Date.now() - thinkingBlockStartMs;
|
|
5270
|
+
const duration = Math.max(0, Date.now() - thinkingBlockStartMs);
|
|
4829
5271
|
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
|
-
}
|
|
5272
|
+
lastThinkingBlockDurationMs = duration;
|
|
5273
|
+
if (message?.role === "assistant") (message as any)[THINKING_DURATION_KEY] = duration;
|
|
4837
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
|
+
}
|
|
4838
5294
|
}
|
|
4839
5295
|
}
|
|
4840
5296
|
|
|
@@ -4860,7 +5316,6 @@ function registerThinkingLabels(pi: ExtensionAPI): void {
|
|
|
4860
5316
|
}
|
|
4861
5317
|
if (sessionStartMs === undefined) sessionStartMs = Date.now();
|
|
4862
5318
|
currentAssistantMessageStartMs = undefined;
|
|
4863
|
-
thinkingBlockInFlight = false;
|
|
4864
5319
|
});
|
|
4865
5320
|
pi.on("agent_start", async () => {
|
|
4866
5321
|
if (currentAgentWorkStartMs === undefined) {
|
|
@@ -4877,6 +5332,10 @@ function registerThinkingLabels(pi: ExtensionAPI): void {
|
|
|
4877
5332
|
if (message?.role === "assistant") {
|
|
4878
5333
|
currentAssistantMessageStartMs = Date.now();
|
|
4879
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.
|
|
4880
5339
|
thinkingBlockInFlight = false;
|
|
4881
5340
|
delete (message as any)[THINKING_ACTIVE_KEY];
|
|
4882
5341
|
}
|
|
@@ -4888,9 +5347,22 @@ function registerThinkingLabels(pi: ExtensionAPI): void {
|
|
|
4888
5347
|
pi.on("message_end", async (event, ctx) => {
|
|
4889
5348
|
const message = (event as any)?.message;
|
|
4890
5349
|
if (message?.role === "assistant") {
|
|
4891
|
-
if
|
|
4892
|
-
|
|
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
|
+
}
|
|
4893
5364
|
}
|
|
5365
|
+
thinkingBlockStartMs = 0;
|
|
4894
5366
|
const started = typeof currentAgentWorkStartMs === "number"
|
|
4895
5367
|
? currentAgentWorkStartMs
|
|
4896
5368
|
: typeof (message as any)[WORKED_START_KEY] === "number"
|
|
@@ -4904,6 +5376,8 @@ function registerThinkingLabels(pi: ExtensionAPI): void {
|
|
|
4904
5376
|
(message as any)[WORKED_DURATION_KEY] = durationMs;
|
|
4905
5377
|
if (typeof sessionTotalMs === "number") (message as any)[WORKED_SESSION_TOTAL_KEY] = sessionTotalMs;
|
|
4906
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.
|
|
4907
5381
|
}
|
|
4908
5382
|
currentAssistantMessageStartMs = undefined;
|
|
4909
5383
|
}
|
|
@@ -5543,7 +6017,7 @@ function renderApplyPatchCall(args: any, theme: Theme, ctx: any, sp: (path: stri
|
|
|
5543
6017
|
}
|
|
5544
6018
|
ctx.state._openAiPatchFiles = preview.changes.map((change) => change.displayPath);
|
|
5545
6019
|
|
|
5546
|
-
const diffWidth =
|
|
6020
|
+
const diffWidth = contextDiffWidth(ctx);
|
|
5547
6021
|
const key = `apply-preview:${ctx.state._applyPatchMetaKey ?? hashText(patchText)}:${diffWidth}:${ctx.expanded ? 1 : 0}`;
|
|
5548
6022
|
if (ctx.state._applyPatchPreviewKey !== key) {
|
|
5549
6023
|
ctx.state._applyPatchPreviewKey = key;
|
|
@@ -5596,7 +6070,7 @@ function renderApplyPatchCall(args: any, theme: Theme, ctx: any, sp: (path: stri
|
|
|
5596
6070
|
}
|
|
5597
6071
|
|
|
5598
6072
|
const body = ctx.state._applyPatchPreviewDisplay as string | undefined;
|
|
5599
|
-
return
|
|
6073
|
+
return makeResponsiveDiffText(ctx, ctx.lastComponent, body ? `${hdr}\n${body}` : hdr);
|
|
5600
6074
|
}
|
|
5601
6075
|
|
|
5602
6076
|
function renderApplyPatchResult(result: any, isPartial: boolean, theme: Theme, ctx: any): Text {
|
|
@@ -5945,6 +6419,7 @@ function renderOpenAiToolResult(name: string, result: any, expanded: boolean, is
|
|
|
5945
6419
|
// ===========================================================================
|
|
5946
6420
|
|
|
5947
6421
|
export default function (pi: ExtensionAPI) {
|
|
6422
|
+
patchTerminalWriteTagScrubber();
|
|
5948
6423
|
patchToolExecutionBackgroundSync();
|
|
5949
6424
|
patchToolRenderCacheInvalidation();
|
|
5950
6425
|
patchReadImageExpansion();
|
|
@@ -5972,7 +6447,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
5972
6447
|
// /cc-tools command — control tool chrome, grouping, and detail level.
|
|
5973
6448
|
const TOOL_MODES = ["outlines", "transparent", "default"] as const;
|
|
5974
6449
|
const TOOL_BOOL_MODES = ["on", "off", "toggle", "status"] as const;
|
|
5975
|
-
const TOOL_SUBCOMMANDS = [...TOOL_MODES, "group", "detail", "branch", "status"] as const;
|
|
6450
|
+
const TOOL_SUBCOMMANDS = [...TOOL_MODES, "group", "detail", "thinking", "branch", "status"] as const;
|
|
5976
6451
|
const booleanMode = (raw: string | undefined, current: boolean): boolean | "status" | undefined => {
|
|
5977
6452
|
const mode = raw || "toggle";
|
|
5978
6453
|
if (mode === "on") return true;
|
|
@@ -5995,6 +6470,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
5995
6470
|
ctx.ui.notify([
|
|
5996
6471
|
`Tool style: ${toolBackgroundMode}`,
|
|
5997
6472
|
`Tool grouping: ${toolGroupingEnabled() ? "on" : "off"}`,
|
|
6473
|
+
`Thinking: ${getThinkingMode()}`,
|
|
5998
6474
|
`Extra detail: ${extraToolOutputExpanded ? "on" : "off"} (${rawKeyHint("ctrl+shift+o", "toggle")})`,
|
|
5999
6475
|
branchLine,
|
|
6000
6476
|
` /cc-tools branch <0-255> | theme | fixed | reset`,
|
|
@@ -6013,6 +6489,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6013
6489
|
label: m,
|
|
6014
6490
|
description:
|
|
6015
6491
|
m === "group" ? "Toggle grouped adjacent/concurrent tool rows"
|
|
6492
|
+
: m === "thinking" ? "Thinking display: live (default) or full"
|
|
6016
6493
|
: m === "detail" ? "Toggle Ctrl+Shift+O extra-detail mode"
|
|
6017
6494
|
: m === "branch" ? "├ └ │ gray (0-255), theme, fixed, or reset"
|
|
6018
6495
|
: m === "status" ? "Show tool UI settings"
|
|
@@ -6028,6 +6505,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
6028
6505
|
.filter((o) => o.startsWith(second))
|
|
6029
6506
|
.map((o) => ({ value: `branch ${o}`, label: o, description: "Branch connector color" }));
|
|
6030
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
|
+
}
|
|
6031
6514
|
if (first === "group" || first === "detail" || first === "extra") {
|
|
6032
6515
|
const second = parts[1] ?? "";
|
|
6033
6516
|
return TOOL_BOOL_MODES
|
|
@@ -6100,6 +6583,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
6100
6583
|
return;
|
|
6101
6584
|
}
|
|
6102
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
|
+
|
|
6103
6604
|
if (sub === "detail" || sub === "extra") {
|
|
6104
6605
|
const next = booleanMode(parts[1], extraToolOutputExpanded);
|
|
6105
6606
|
if (next === undefined) {
|
|
@@ -6119,7 +6620,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6119
6620
|
}
|
|
6120
6621
|
|
|
6121
6622
|
if (!(TOOL_MODES as readonly string[]).includes(sub)) {
|
|
6122
|
-
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");
|
|
6123
6624
|
return;
|
|
6124
6625
|
}
|
|
6125
6626
|
toolBackgroundOverride = sub as typeof toolBackgroundMode;
|
|
@@ -6413,15 +6914,27 @@ export default function (pi: ExtensionAPI) {
|
|
|
6413
6914
|
},
|
|
6414
6915
|
renderCall(args, theme, ctx) {
|
|
6415
6916
|
syncToolCallStatus(ctx);
|
|
6917
|
+
syncBashDuration(ctx);
|
|
6416
6918
|
const rewrite = ensureRtkRewriteForContext(ctx, args);
|
|
6417
|
-
const
|
|
6919
|
+
const command = typeof args.command === "string" ? args.command : "";
|
|
6920
|
+
const presentation = buildBashCommandPresentation(command);
|
|
6921
|
+
const summary = stableCallSummary(ctx, "_bashHeadline", () => presentation.headline);
|
|
6418
6922
|
const rtkBadge = rewrite ? theme.fg("muted", " (RTK)") : "";
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
);
|
|
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);
|
|
6423
6935
|
},
|
|
6424
6936
|
renderResult(result, { expanded, isPartial }, theme, ctx) {
|
|
6937
|
+
syncBashDuration(ctx, isPartial);
|
|
6425
6938
|
const details = result.details as BashToolDetails | undefined;
|
|
6426
6939
|
const rewrite = ensureRtkRewriteForContext(ctx, ctx.args);
|
|
6427
6940
|
const output = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
@@ -6666,7 +7179,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6666
7179
|
if (d?._type === "diff") {
|
|
6667
7180
|
const previewLines = ctx.expanded ? MAX_RENDER_LINES : diffCollapsedLimit();
|
|
6668
7181
|
const hunks = d.diff?.lines?.filter((l: any) => l.type === "sep").length + (d.diff?.lines?.length ? 1 : 0);
|
|
6669
|
-
const diffWidth =
|
|
7182
|
+
const diffWidth = contextDiffWidth(ctx);
|
|
6670
7183
|
const mode = shouldUseSplit(d.diff, diffWidth, previewLines) ? "split" : "unified";
|
|
6671
7184
|
const richSummary = diffSummaryWithMeta(d.diff.added, d.diff.removed, hunks, mode);
|
|
6672
7185
|
const key = `wd:${diffWidth}:${d.summary}:${d.diff?.lines?.length ?? 0}:${d.language ?? ""}:${ctx.expanded ? 1 : 0}`;
|
|
@@ -6686,7 +7199,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6686
7199
|
safeInvalidate(ctx);
|
|
6687
7200
|
});
|
|
6688
7201
|
}
|
|
6689
|
-
return
|
|
7202
|
+
return makeResponsiveDiffText(ctx, ctx.lastComponent, ctx.state._wdt ?? withBranch(richSummary, theme));
|
|
6690
7203
|
}
|
|
6691
7204
|
if (d?._type === "noChange") return makeText(ctx.lastComponent, withBranch(theme.fg("muted", "✓ no changes"), theme));
|
|
6692
7205
|
if (d?._type === "new") {
|
|
@@ -6696,7 +7209,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6696
7209
|
const syntheticDiff = getCachedParsedDiff(ctx, `nf-diff:${d.filePath}:${contentHash}`, "", content);
|
|
6697
7210
|
const richSummary = diffSummaryWithMeta(syntheticDiff.added, 0, 1, "new file");
|
|
6698
7211
|
const previewLines = ctx.expanded ? MAX_RENDER_LINES : diffCollapsedLimit();
|
|
6699
|
-
const diffWidth =
|
|
7212
|
+
const diffWidth = contextDiffWidth(ctx);
|
|
6700
7213
|
const pk = `nf:${d.filePath}:${contentHash}:${diffWidth}:${ctx.expanded ? 1 : 0}`;
|
|
6701
7214
|
if (ctx.state._nfk !== pk) {
|
|
6702
7215
|
ctx.state._nfk = pk;
|
|
@@ -6714,7 +7227,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6714
7227
|
safeInvalidate(ctx);
|
|
6715
7228
|
});
|
|
6716
7229
|
}
|
|
6717
|
-
return
|
|
7230
|
+
return makeResponsiveDiffText(ctx, ctx.lastComponent, ctx.state._nft ?? withBranch(`${richSummary} ${theme.fg("muted", `(${lineTotal} lines)`)}`, theme));
|
|
6718
7231
|
}
|
|
6719
7232
|
return makeText(ctx.lastComponent, withBranch(theme.fg("success", "Written"), theme));
|
|
6720
7233
|
},
|
|
@@ -6769,7 +7282,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6769
7282
|
syncToolCallStatus(ctx);
|
|
6770
7283
|
const hdr = toolHeader("Edit", summary, theme, ` ${toolStatusDot(ctx, theme)}`, liveLineCountTrailing(ctx, theme));
|
|
6771
7284
|
if (!(ctx.argsComplete && operations.length > 0)) return makeText(ctx.lastComponent, hdr);
|
|
6772
|
-
const diffWidth =
|
|
7285
|
+
const diffWidth = contextDiffWidth(ctx, 3);
|
|
6773
7286
|
const key = `edit:${fp}:${hashText(operations.map((edit) => `${edit.oldText}\u0000${edit.newText}`).join("\u0001"))}:${diffWidth}:${ctx.expanded ? 1 : 0}`;
|
|
6774
7287
|
const { diffs: fallbackDiffs, summary: editSummary } = getCachedEditOperationSummary(ctx, key, operations);
|
|
6775
7288
|
if (ctx.state._pk !== key) {
|
|
@@ -6789,8 +7302,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
6789
7302
|
renderEditPreviewBody(ctx, key, theme, lg, operations, fallbackDiffs, fallbackDiffs.map((diff) => getFirstChangedNewLine(diff)), editSummary);
|
|
6790
7303
|
});
|
|
6791
7304
|
}
|
|
6792
|
-
|
|
6793
|
-
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);
|
|
6794
7307
|
},
|
|
6795
7308
|
renderResult(result, { expanded, isPartial }, theme, ctx) {
|
|
6796
7309
|
if (isPartial) {
|
|
@@ -6943,6 +7456,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
6943
7456
|
});
|
|
6944
7457
|
pi.on("session_shutdown", async () => {
|
|
6945
7458
|
_clearAllBlinkContexts();
|
|
7459
|
+
clearAllBashDurationContexts();
|
|
6946
7460
|
clearRtkRewriteState();
|
|
6947
7461
|
WRITE_EXISTED_BEFORE.clear();
|
|
6948
7462
|
clearHighlightCache();
|