pi-claude-code-ui 1.0.92 → 1.0.93
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 +16 -0
- package/README.md +7 -3
- package/extensions/bash-file-edit.ts +301 -0
- package/extensions/index.ts +551 -118
- package/extensions/spinner.ts +79 -2
- package/package.json +2 -1
package/extensions/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, statSync, writeFileSync, mkdirSync } from "node:fs";
|
|
2
2
|
import { readFile as readFileAsync } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, extname, relative, resolve } from "node:path";
|
|
4
4
|
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
} from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import {
|
|
14
14
|
AssistantMessageComponent,
|
|
15
|
+
CustomEditor,
|
|
15
16
|
CustomMessageComponent,
|
|
16
17
|
ToolExecutionComponent,
|
|
17
18
|
UserMessageComponent,
|
|
@@ -68,8 +69,8 @@ import {
|
|
|
68
69
|
buildBashPreview,
|
|
69
70
|
describeBashSource,
|
|
70
71
|
formatBashDuration,
|
|
71
|
-
getLastBashOutputLine,
|
|
72
72
|
} from "./bash-command";
|
|
73
|
+
import { detectBashFileEdit, type BashFileEdit } from "./bash-file-edit";
|
|
73
74
|
import { getUserSettingsPath } from "./settings-path";
|
|
74
75
|
import {
|
|
75
76
|
blendForContrast,
|
|
@@ -105,6 +106,7 @@ const ANSI_PRESENT_RE = /\x1b\[[0-9;]*m/;
|
|
|
105
106
|
const PATCH_FLAG = Symbol.for("pi-claude-style-tools:patched-container-render");
|
|
106
107
|
const TOOL_RENDER_CACHE = Symbol.for("pi-claude-style-tools:tool-render-cache");
|
|
107
108
|
const COMPONENT_PARENT = Symbol.for("pi-claude-style-tools:component-parent");
|
|
109
|
+
const BASH_FILE_EDIT_PATCH_FLAG = Symbol.for("pi-claude-style-tools:bash-file-edit-grouping");
|
|
108
110
|
const PARENT_TRACKING_PATCH_FLAG = Symbol.for("pi-claude-style-tools:patched-parent-tracking");
|
|
109
111
|
const TOOL_UPDATE_DISPLAY_ACTIVE = Symbol("pi-claude-style-tools:update-display-active");
|
|
110
112
|
const TOOL_UPDATE_DISPLAY_RERUN = Symbol("pi-claude-style-tools:update-display-rerun");
|
|
@@ -704,15 +706,49 @@ function groupStatusLight(status: ToolStatus, options?: { agentBreathe?: boolean
|
|
|
704
706
|
return paintStatusDot(color);
|
|
705
707
|
}
|
|
706
708
|
|
|
707
|
-
|
|
709
|
+
// Plain-sentence summaries for grouped tool rows ("Read 2 files, ran 1 shell command").
|
|
710
|
+
// Verbs: [done, running, one, many].
|
|
711
|
+
const GROUP_VERBS: Record<string, [string, string, string, string]> = {
|
|
712
|
+
read: ["read", "reading", "file", "files"],
|
|
713
|
+
bash: ["ran", "running", "shell command", "shell commands"],
|
|
714
|
+
grep: ["searched", "searching", "pattern", "patterns"],
|
|
715
|
+
find: ["searched", "searching", "pattern", "patterns"],
|
|
716
|
+
ls: ["listed", "listing", "directory", "directories"],
|
|
717
|
+
Agent: ["launched", "launching", "agent", "agents"],
|
|
718
|
+
};
|
|
719
|
+
// grep and find both count as searches.
|
|
720
|
+
const GROUP_VERB_KEY: Record<string, string> = { find: "grep" };
|
|
721
|
+
|
|
722
|
+
/** "Read 2 files, ran 1 shell command, called Ctx Reduce" (or the running form). */
|
|
723
|
+
function groupSentence(tools: any[], running: boolean): string {
|
|
708
724
|
const counts = new Map<string, number>();
|
|
725
|
+
const called = new Map<string, number>();
|
|
709
726
|
for (const tool of tools) {
|
|
710
727
|
const name = getToolName(tool);
|
|
711
|
-
|
|
728
|
+
if (GROUP_VERBS[name]) {
|
|
729
|
+
const key = GROUP_VERB_KEY[name] ?? name;
|
|
730
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
731
|
+
} else {
|
|
732
|
+
if (!counts.has("call")) counts.set("call", 0);
|
|
733
|
+
const label = toolDisplayName(name);
|
|
734
|
+
called.set(label, (called.get(label) ?? 0) + 1);
|
|
735
|
+
}
|
|
712
736
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
737
|
+
const parts = [...counts].map(([key, count]) => {
|
|
738
|
+
if (key === "call") {
|
|
739
|
+
const names = [...called].map(([label, n]) => (n > 1 ? `${label} ×${n}` : label)).join(", ");
|
|
740
|
+
return `${running ? "calling" : "called"} ${names}`;
|
|
741
|
+
}
|
|
742
|
+
const [done, active, one, many] = GROUP_VERBS[key];
|
|
743
|
+
return `${running ? active : done} ${count} ${count === 1 ? one : many}`;
|
|
744
|
+
});
|
|
745
|
+
const text = parts.join(", ");
|
|
746
|
+
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function formatElapsedSeconds(ms: number): string {
|
|
750
|
+
const secs = Math.max(0, Math.round(ms / 1000));
|
|
751
|
+
return secs < 60 ? `${secs}s` : `${Math.floor(secs / 60)}m ${secs % 60}s`;
|
|
716
752
|
}
|
|
717
753
|
|
|
718
754
|
function getRepeatedToolSubject(tools: any[], groupedName: string | undefined): string {
|
|
@@ -792,7 +828,17 @@ function getToolArgSummary(tool: any): string {
|
|
|
792
828
|
if (name === "find") return `"${summarizeText(args.pattern ?? "", 40)}"${args.path ? ` in ${args.path}` : ""}`;
|
|
793
829
|
if (name === "ls") return shortPath(process.cwd(), args.path ?? ".");
|
|
794
830
|
if (parseNamespacedMcpToolName(name)) return summarizeDirectMcpArgs(args);
|
|
795
|
-
return
|
|
831
|
+
return summarizeUnknownToolArgs(args);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Summary for a tool without its own case: its most telling string argument, or the
|
|
836
|
+
* first one it has. Empty when there is none; never the tool's name, which the row
|
|
837
|
+
* already shows as its label ("Ctx Reduce Ctx Reduce").
|
|
838
|
+
*/
|
|
839
|
+
function summarizeUnknownToolArgs(args: any): string {
|
|
840
|
+
const known = getStringArg(args, "path", "file_path", "url", "query", "name", "subject", "tool", "description", "prompt");
|
|
841
|
+
return known ? summarizeText(known, 72) : summarizeDirectMcpArgs(args);
|
|
796
842
|
}
|
|
797
843
|
|
|
798
844
|
function getToolCallLine(tool: any): string {
|
|
@@ -832,21 +878,6 @@ interface CollapsedToolEntry {
|
|
|
832
878
|
subject: string;
|
|
833
879
|
}
|
|
834
880
|
|
|
835
|
-
function collapseRepeatedToolEntries(tools: any[]): CollapsedToolEntry[] {
|
|
836
|
-
const entries: CollapsedToolEntry[] = [];
|
|
837
|
-
for (const tool of tools) {
|
|
838
|
-
const name = getToolName(tool);
|
|
839
|
-
const subject = getRepeatedToolSubject([tool], name);
|
|
840
|
-
const previous = entries[entries.length - 1];
|
|
841
|
-
if (subject && previous?.name === name && previous.subject === subject) {
|
|
842
|
-
previous.tools.push(tool);
|
|
843
|
-
} else {
|
|
844
|
-
entries.push({ tools: [tool], name, subject });
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
return entries;
|
|
848
|
-
}
|
|
849
|
-
|
|
850
881
|
function stripReadRangeFromToolLine(line: string): string {
|
|
851
882
|
return line.replace(
|
|
852
883
|
/\s+(?:\x1b\[[0-9;]*m)*\((?:offset|limit)=\d+(?:,\s*(?:offset|limit)=\d+)*\)(?=(?:\x1b\[[0-9;]*m)*$)/,
|
|
@@ -866,15 +897,6 @@ function getCollapsedToolEntryLine(entry: CollapsedToolEntry, width: number, gro
|
|
|
866
897
|
return clampLineWidth(`${sharedLine}${suffix}`, width);
|
|
867
898
|
}
|
|
868
899
|
|
|
869
|
-
function getCollapsedToolEntryLines(entry: CollapsedToolEntry, width: number, groupedLabel?: string): string[] {
|
|
870
|
-
const lines = [getCollapsedToolEntryLine(entry, width, groupedLabel)];
|
|
871
|
-
if (entry.name !== "bash") return lines;
|
|
872
|
-
const running = [...entry.tools].reverse().find((tool) => getToolStatusForGroup(tool) === "pending");
|
|
873
|
-
const latestOutput = running ? getLastBashOutputLine(getTextContent(running.result)) : undefined;
|
|
874
|
-
if (latestOutput) lines.push(`${FG_DIM}${latestOutput}${TRANSPARENT_RESET}`);
|
|
875
|
-
return lines;
|
|
876
|
-
}
|
|
877
|
-
|
|
878
900
|
function getExpandedToolGroupLines(tool: any, width: number, groupedLabel?: string): string[] {
|
|
879
901
|
const lines = stripToolChrome(tool.render(Math.max(1, width)))
|
|
880
902
|
.map((line) => removeGroupedToolPrefix(line, groupedLabel))
|
|
@@ -927,7 +949,65 @@ const NON_GROUPABLE_TOOL_NAMES = new Set(["edit", "write", "apply_patch"]);
|
|
|
927
949
|
const ACTIVE_TOOL_GROUPS = new Set<any>();
|
|
928
950
|
|
|
929
951
|
function isGroupableTool(value: unknown): value is InstanceType<typeof ToolExecutionComponent> {
|
|
930
|
-
return value instanceof ToolExecutionComponent && !NON_GROUPABLE_TOOL_NAMES.has(getToolName(value));
|
|
952
|
+
return value instanceof ToolExecutionComponent && !NON_GROUPABLE_TOOL_NAMES.has(getToolName(value)) && !isBashFileEditTool(value);
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/** A bash row that only changes files (sed -i, heredoc, ...): drawn like edit/write, never grouped. */
|
|
956
|
+
function isBashFileEditTool(tool: any): boolean {
|
|
957
|
+
if (getToolName(tool) !== "bash") return false;
|
|
958
|
+
const command = tool?.args?.command;
|
|
959
|
+
return typeof command === "string" && !!detectBashFileEdit(command);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/**
|
|
963
|
+
* Live rows join a group as soon as they appear, before their arguments have streamed in.
|
|
964
|
+
* When a bash row turns out to be a file edit, take it back out: the calls before it stay
|
|
965
|
+
* grouped, the edit gets its own row, and the calls after it form a new group.
|
|
966
|
+
*/
|
|
967
|
+
function ejectToolFromGroup(tool: any): void {
|
|
968
|
+
const group = tool?.[COMPONENT_PARENT];
|
|
969
|
+
if (!isToolGroupComponent(group)) return;
|
|
970
|
+
const parent = (group as any)[COMPONENT_PARENT];
|
|
971
|
+
const children = parent?.children;
|
|
972
|
+
if (!Array.isArray(children)) return;
|
|
973
|
+
const groupIndex = children.indexOf(group);
|
|
974
|
+
if (groupIndex === -1) return;
|
|
975
|
+
const expanded = (group as any).expanded === true;
|
|
976
|
+
const tools = (group as any).releaseTools() as any[];
|
|
977
|
+
const at = tools.indexOf(tool);
|
|
978
|
+
if (at === -1) return;
|
|
979
|
+
const piece = (members: any[]): any[] => {
|
|
980
|
+
if (members.length === 0) return [];
|
|
981
|
+
if (members.length === 1) {
|
|
982
|
+
members[0][COMPONENT_PARENT] = parent;
|
|
983
|
+
return members;
|
|
984
|
+
}
|
|
985
|
+
const next = new ToolGroupComponent();
|
|
986
|
+
next.expanded = expanded;
|
|
987
|
+
for (const member of members) next.addTool(member);
|
|
988
|
+
(next as any)[COMPONENT_PARENT] = parent;
|
|
989
|
+
return [next];
|
|
990
|
+
};
|
|
991
|
+
tool[COMPONENT_PARENT] = parent;
|
|
992
|
+
children.splice(groupIndex, 1, ...piece(tools.slice(0, at)), tool, ...piece(tools.slice(at + 1)));
|
|
993
|
+
try {
|
|
994
|
+
tool.ui?.requestRender?.();
|
|
995
|
+
} catch { /* noop */ }
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function patchBashFileEditGrouping(): void {
|
|
999
|
+
const proto = ToolExecutionComponent.prototype as any;
|
|
1000
|
+
if (proto[BASH_FILE_EDIT_PATCH_FLAG]) return;
|
|
1001
|
+
for (const method of ["setArgsComplete", "markExecutionStarted"]) {
|
|
1002
|
+
const original = proto[method];
|
|
1003
|
+
if (typeof original !== "function") continue;
|
|
1004
|
+
proto[method] = function patchedBashFileEditGrouping(this: any, ...args: any[]) {
|
|
1005
|
+
const out = original.apply(this, args);
|
|
1006
|
+
if (isBashFileEditTool(this)) ejectToolFromGroup(this);
|
|
1007
|
+
return out;
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
proto[BASH_FILE_EDIT_PATCH_FLAG] = true;
|
|
931
1011
|
}
|
|
932
1012
|
|
|
933
1013
|
class ToolGroupComponent extends Container {
|
|
@@ -947,6 +1027,8 @@ class ToolGroupComponent extends Container {
|
|
|
947
1027
|
private cachedToolCount?: number;
|
|
948
1028
|
private cachedLines?: string[];
|
|
949
1029
|
private paletteFingerprint?: string;
|
|
1030
|
+
/** When the group's current run started, for the "· 3s" on a running header. */
|
|
1031
|
+
private runStartedAt?: number;
|
|
950
1032
|
|
|
951
1033
|
private clearRenderCache(): void {
|
|
952
1034
|
this.dirty = true;
|
|
@@ -1072,7 +1154,6 @@ class ToolGroupComponent extends Container {
|
|
|
1072
1154
|
|
|
1073
1155
|
const groupedName = getGroupedToolName(this.tools);
|
|
1074
1156
|
const label = getToolGroupLabel(this.tools);
|
|
1075
|
-
const names = groupedName ? "" : formatToolNameList(this.tools);
|
|
1076
1157
|
const overall: ToolStatus = status.error > 0 ? "error" : status.pending > 0 ? "pending" : "success";
|
|
1077
1158
|
const mouseChildren: MouseLayoutEntry[] = [];
|
|
1078
1159
|
let mouseY = 0;
|
|
@@ -1085,12 +1166,9 @@ class ToolGroupComponent extends Container {
|
|
|
1085
1166
|
const pendingTools = this.tools.filter((tool) => getToolStatusForGroup(tool) === "pending");
|
|
1086
1167
|
const headerBreathe = pendingTools.length > 0 && pendingTools.every((tool) => isAgentFamilyToolName(getToolName(tool)));
|
|
1087
1168
|
const light = groupStatusLight(overall, { agentBreathe: headerBreathe });
|
|
1088
|
-
const
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
if (status.success) countParts.push(statusText("success", status.success));
|
|
1092
|
-
if (status.error) countParts.push(statusText("error", status.error));
|
|
1093
|
-
const countsText = countParts.join(`${TRANSPARENT_RESET} • `);
|
|
1169
|
+
const running = status.pending > 0;
|
|
1170
|
+
if (running) this.runStartedAt ??= Date.now();
|
|
1171
|
+
else this.runStartedAt = undefined;
|
|
1094
1172
|
const total = this.tools.length;
|
|
1095
1173
|
const lines: string[] = [];
|
|
1096
1174
|
const subject = this.expanded ? "" : getRepeatedToolSubject(this.tools, groupedName);
|
|
@@ -1105,8 +1183,26 @@ class ToolGroupComponent extends Container {
|
|
|
1105
1183
|
recordLines(row, undefined);
|
|
1106
1184
|
lines.push(...row);
|
|
1107
1185
|
} else {
|
|
1108
|
-
|
|
1186
|
+
// One plain sentence instead of a list of every call: "Read 2 files, ran 1 shell
|
|
1187
|
+
// command". While running: "Reading 2 files, running 1 shell command… · 3s", with
|
|
1188
|
+
// the call in progress underneath. ctrl+o (or a click) shows every call.
|
|
1189
|
+
const sentence = groupSentence(this.tools, running);
|
|
1190
|
+
const elapsed = running && this.runStartedAt !== undefined
|
|
1191
|
+
? `${FG_DIM} · ${formatElapsedSeconds(Date.now() - this.runStartedAt)}${TRANSPARENT_RESET}`
|
|
1192
|
+
: "";
|
|
1193
|
+
const failed = status.error > 0 ? ` ${TOOL_STATUS_ERROR}· ${status.error} failed${TRANSPARENT_RESET}` : "";
|
|
1194
|
+
const summary = ` ${light} ${sentence}${running ? "…" : ""}${elapsed}${failed}${toolOutputDetailHint(undefined as any, this.expanded, true)}`;
|
|
1109
1195
|
const header = [" ".repeat(safeWidth), clampLineWidth(summary, safeWidth)];
|
|
1196
|
+
if (running && !this.expanded) {
|
|
1197
|
+
const active = pendingTools[pendingTools.length - 1];
|
|
1198
|
+
if (active) {
|
|
1199
|
+
// The call in progress, without the live "(N lines)" count.
|
|
1200
|
+
const activeLine = stripAnsi(getCompactToolLine(active, Math.max(1, safeWidth - 3), undefined, false))
|
|
1201
|
+
.trim()
|
|
1202
|
+
.replace(/\s+\(\d+ lines?\)$/, "");
|
|
1203
|
+
header.push(clampLineWidth(` ${currentToolBranchAnsi()}└${TRANSPARENT_RESET} ${FG_DIM}${activeLine}${TRANSPARENT_RESET}`, safeWidth));
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1110
1206
|
recordLines(header, undefined);
|
|
1111
1207
|
lines.push(...header);
|
|
1112
1208
|
const childWidth = Math.max(1, safeWidth - 6);
|
|
@@ -1125,22 +1221,6 @@ class ToolGroupComponent extends Container {
|
|
|
1125
1221
|
recordLines(row, tool);
|
|
1126
1222
|
lines.push(...row);
|
|
1127
1223
|
}
|
|
1128
|
-
} else {
|
|
1129
|
-
const entries = collapseRepeatedToolEntries(this.tools);
|
|
1130
|
-
for (let index = 0; index < entries.length; index++) {
|
|
1131
|
-
const entry = entries[index];
|
|
1132
|
-
const branched = formatBranchedToolLines(
|
|
1133
|
-
getCollapsedToolEntryLines(entry, childWidth, groupedName ? label : undefined),
|
|
1134
|
-
index,
|
|
1135
|
-
entries.length,
|
|
1136
|
-
safeWidth,
|
|
1137
|
-
getToolGroupOverallStatus(entry.tools),
|
|
1138
|
-
{ agentBreathe: entry.tools.every((tool) => isAgentFamilyToolName(getToolName(tool))) },
|
|
1139
|
-
);
|
|
1140
|
-
const row = branched.map((line) => clampLineWidth(line, safeWidth));
|
|
1141
|
-
recordLines(row, entry.tools.length === 1 ? entry.tools[0] : entry.tools);
|
|
1142
|
-
lines.push(...row);
|
|
1143
|
-
}
|
|
1144
1224
|
}
|
|
1145
1225
|
}
|
|
1146
1226
|
rememberMouseLayout(this, safeWidth, mouseChildren);
|
|
@@ -2512,6 +2592,157 @@ function trimRenderedBlankLines(lines: string[]): string[] {
|
|
|
2512
2592
|
return start <= end ? lines.slice(start, end + 1) : [];
|
|
2513
2593
|
}
|
|
2514
2594
|
|
|
2595
|
+
// ---------------------------------------------------------------------------
|
|
2596
|
+
// pi-subagents (@tintinweb/pi-subagents): Agent rows, completion notices and the live
|
|
2597
|
+
// "Agents" widget, drawn with the same dots, connectors and indentation as tool rows.
|
|
2598
|
+
// ---------------------------------------------------------------------------
|
|
2599
|
+
|
|
2600
|
+
function branchGlyph(glyph: "├" | "└" | "│"): string {
|
|
2601
|
+
return `${currentToolBranchAnsi()}${glyph}${TRANSPARENT_RESET}`;
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
function formatAgentTokens(tokens: unknown): string {
|
|
2605
|
+
if (typeof tokens === "string") return tokens.trim();
|
|
2606
|
+
if (typeof tokens !== "number" || !Number.isFinite(tokens) || tokens <= 0) return "";
|
|
2607
|
+
return tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k tokens` : `${tokens} tokens`;
|
|
2608
|
+
}
|
|
2609
|
+
|
|
2610
|
+
/** "3 turns · 4 tool uses · 28.5k tokens · 5s" from pi-subagents' details. */
|
|
2611
|
+
function agentStatsText(details: any, durationMs?: number): string {
|
|
2612
|
+
const parts: string[] = [];
|
|
2613
|
+
if (typeof details?.modelName === "string" && details.modelName) parts.push(details.modelName);
|
|
2614
|
+
const turns = Number(details?.turnCount);
|
|
2615
|
+
if (Number.isFinite(turns) && turns > 1) parts.push(`${turns} turns`);
|
|
2616
|
+
const toolUses = Number(details?.toolUses);
|
|
2617
|
+
if (Number.isFinite(toolUses) && toolUses > 0) parts.push(`${toolUses} tool use${toolUses === 1 ? "" : "s"}`);
|
|
2618
|
+
const tokens = formatAgentTokens(details?.tokens ?? details?.totalTokens);
|
|
2619
|
+
if (tokens) parts.push(tokens);
|
|
2620
|
+
const ms = Number(durationMs ?? details?.durationMs);
|
|
2621
|
+
if (Number.isFinite(ms) && ms > 0) parts.push(formatWorkedDuration(ms));
|
|
2622
|
+
return parts.join(" · ");
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
/** Result rows for pi-subagents' Agent tool, or undefined to use the generic rendering. */
|
|
2626
|
+
function renderAgentToolResult(result: any, expanded: boolean, isPartial: boolean, theme: Theme, ctx: any): Text | undefined {
|
|
2627
|
+
const details = result?.details;
|
|
2628
|
+
const status = typeof details?.status === "string" ? details.status : undefined;
|
|
2629
|
+
if (!status || ctx.isError) return undefined;
|
|
2630
|
+
const stats = agentStatsText(details);
|
|
2631
|
+
const statsSuffix = stats ? theme.fg("muted", ` · ${stats}`) : "";
|
|
2632
|
+
if (isPartial || status === "running" || status === "queued") {
|
|
2633
|
+
const activity = typeof details.activity === "string" && details.activity ? details.activity : "thinking…";
|
|
2634
|
+
return makeText(ctx.lastComponent, withBranch(`${theme.fg("dim", activity)}${statsSuffix}`, theme));
|
|
2635
|
+
}
|
|
2636
|
+
clearBlinkTimer(ctx);
|
|
2637
|
+
if (status === "background") {
|
|
2638
|
+
setToolStatus(ctx, "success");
|
|
2639
|
+
const id = typeof details.agentId === "string" ? theme.fg("muted", ` (${details.agentId})`) : "";
|
|
2640
|
+
return makeText(ctx.lastComponent, withBranch(`${theme.fg("dim", "Running in background")}${id}`, theme));
|
|
2641
|
+
}
|
|
2642
|
+
if (status === "completed" || status === "steered") {
|
|
2643
|
+
setToolStatus(ctx, "success");
|
|
2644
|
+
const label = status === "steered" ? theme.fg("warning", "Wrapped up (turn limit)") : theme.fg("success", "Done");
|
|
2645
|
+
const text = getTextContent(result).trim();
|
|
2646
|
+
if (!expanded || !text) {
|
|
2647
|
+
return makeText(ctx.lastComponent, withBranch(`${label}${statsSuffix}${text ? toolOutputDetailHint(theme, expanded) : ""}`, theme));
|
|
2648
|
+
}
|
|
2649
|
+
const lines = text.split("\n");
|
|
2650
|
+
const preview = buildPreviewText(lines, true, theme, previewLimit(), lines.length, (line) => theme.fg("dim", line || " "));
|
|
2651
|
+
return makeText(ctx.lastComponent, withBranch(`${label}${statsSuffix}\n${preview}`, theme));
|
|
2652
|
+
}
|
|
2653
|
+
if (status === "stopped") {
|
|
2654
|
+
setToolStatus(ctx, "success");
|
|
2655
|
+
return makeText(ctx.lastComponent, withBranch(`${theme.fg("muted", "Stopped")}${statsSuffix}`, theme));
|
|
2656
|
+
}
|
|
2657
|
+
setToolStatus(ctx, "error");
|
|
2658
|
+
const reason = status === "aborted" ? "Aborted (max turns exceeded)" : `Error: ${details.error ?? "unknown"}`;
|
|
2659
|
+
return makeText(ctx.lastComponent, withBranch(`${theme.fg("error", reason)}${statsSuffix}`, theme));
|
|
2660
|
+
}
|
|
2661
|
+
|
|
2662
|
+
/** A finished background agent's notice, drawn like a finished tool row. */
|
|
2663
|
+
function renderSubagentNotificationLines(details: any, expanded: boolean, width: number, theme: Theme): string[] {
|
|
2664
|
+
const agents = [details, ...(Array.isArray(details?.others) ? details.others : [])].filter((agent) => agent && typeof agent === "object");
|
|
2665
|
+
const blocks: string[][] = [];
|
|
2666
|
+
for (const agent of agents) {
|
|
2667
|
+
const status = String(agent.status ?? "completed");
|
|
2668
|
+
const failed = status === "error" || status === "stopped" || status === "aborted";
|
|
2669
|
+
const dot = paintStatusDot(failed ? TOOL_STATUS_ERROR : TOOL_STATUS_SUCCESS);
|
|
2670
|
+
const statusLabel = status === "steered" ? "completed (turn limit)" : status;
|
|
2671
|
+
const header = ` ${dot} ${theme.fg("toolTitle", theme.bold("Agent"))} ${theme.fg("accent", String(agent.description ?? "agent"))} ${theme.fg(failed ? "error" : "muted", statusLabel)}`;
|
|
2672
|
+
const body: string[] = [];
|
|
2673
|
+
const stats = agentStatsText(agent);
|
|
2674
|
+
if (stats) body.push(theme.fg("muted", stats));
|
|
2675
|
+
const preview = String(agent.resultPreview ?? "").trim();
|
|
2676
|
+
if (preview) {
|
|
2677
|
+
const previewLines = preview.split("\n");
|
|
2678
|
+
const shown = expanded ? previewLines.slice(0, 30) : [previewLines[0] ?? ""];
|
|
2679
|
+
for (const line of shown) body.push(theme.fg("dim", line || " "));
|
|
2680
|
+
}
|
|
2681
|
+
if (expanded && typeof agent.outputFile === "string" && agent.outputFile) {
|
|
2682
|
+
body.push(theme.fg("muted", `transcript: ${agent.outputFile}`));
|
|
2683
|
+
}
|
|
2684
|
+
const lines = [header];
|
|
2685
|
+
body.forEach((line, index) => lines.push(` ${branchGlyph(index === body.length - 1 ? "└" : "├")} ${line}`));
|
|
2686
|
+
blocks.push(lines.map((line) => clampLineWidth(line, width)));
|
|
2687
|
+
}
|
|
2688
|
+
return blocks.flatMap((block, index) => (index === 0 ? block : ["", ...block]));
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
const AGENT_WIDGET_PATCH_FLAG = Symbol.for("pi-claude-style-tools:agent-widget-restyle");
|
|
2692
|
+
const ANSI_PREFIX = "((?:\\x1b\\[[0-9;]*m)*)";
|
|
2693
|
+
const AGENT_WIDGET_HEADING_RE = new RegExp(`^${ANSI_PREFIX}([●○])${ANSI_PREFIX}\\s+${ANSI_PREFIX}Agents`);
|
|
2694
|
+
const AGENT_WIDGET_ITEM_RE = new RegExp(`^${ANSI_PREFIX}([├└])─${ANSI_PREFIX} (.*)$`);
|
|
2695
|
+
const AGENT_WIDGET_ACTIVITY_RE = new RegExp(`^${ANSI_PREFIX}(│| ) ${ANSI_PREFIX}(?:${ANSI_PREFIX})?\\s*⎿\\s+(.*)$`);
|
|
2696
|
+
const AGENT_WIDGET_SPINNER_RE = new RegExp(`^${ANSI_PREFIX}[\\u2800-\\u28ff]${ANSI_PREFIX}`);
|
|
2697
|
+
|
|
2698
|
+
/** pi-subagents' "Agents" widget lines, re-indented and re-marked to match tool rows. */
|
|
2699
|
+
function restyleAgentWidgetLines(lines: string[], width: number): string[] {
|
|
2700
|
+
if (!Array.isArray(lines)) return lines;
|
|
2701
|
+
return lines.map((line) => {
|
|
2702
|
+
if (typeof line !== "string") return line;
|
|
2703
|
+
const heading = AGENT_WIDGET_HEADING_RE.exec(line);
|
|
2704
|
+
if (heading) {
|
|
2705
|
+
const active = heading[2] === "●";
|
|
2706
|
+
const dot = active ? paintAgentBreatheDot(TOOL_STATUS_SUCCESS) : paintStatusDot(TOOL_STATUS_SUCCESS);
|
|
2707
|
+
return clampLineWidth(` ${dot} Agents`, width);
|
|
2708
|
+
}
|
|
2709
|
+
const item = AGENT_WIDGET_ITEM_RE.exec(line);
|
|
2710
|
+
if (item) {
|
|
2711
|
+
let rest = item[4];
|
|
2712
|
+
// The running spinner becomes the breathing dot running agent rows use.
|
|
2713
|
+
rest = rest.replace(AGENT_WIDGET_SPINNER_RE, () => paintAgentBreatheDot(TOOL_STATUS_SUCCESS));
|
|
2714
|
+
rest = normalizeLeadingCheckGlyph(rest);
|
|
2715
|
+
return clampLineWidth(` ${branchGlyph(item[2] as "├" | "└")} ${rest}`, width);
|
|
2716
|
+
}
|
|
2717
|
+
const activity = AGENT_WIDGET_ACTIVITY_RE.exec(line);
|
|
2718
|
+
if (activity) {
|
|
2719
|
+
const bar = activity[2] === "│" ? branchGlyph("│") : " ";
|
|
2720
|
+
return clampLineWidth(` ${bar} ${FG_DIM}${stripAnsi(activity[5])}${TRANSPARENT_RESET}`, width);
|
|
2721
|
+
}
|
|
2722
|
+
return line;
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
function patchAgentWidget(ui: any): void {
|
|
2727
|
+
if (!ui || ui[AGENT_WIDGET_PATCH_FLAG] || typeof ui.setWidget !== "function") return;
|
|
2728
|
+
const original = ui.setWidget;
|
|
2729
|
+
ui.setWidget = function patchedSetWidget(this: any, key: string, content: any, options?: any) {
|
|
2730
|
+
if (key === "agents" && typeof content === "function") {
|
|
2731
|
+
const factory = content;
|
|
2732
|
+
content = (tui: any, theme: any) => {
|
|
2733
|
+
const component = factory(tui, theme);
|
|
2734
|
+
if (component && typeof component.render === "function") {
|
|
2735
|
+
const render = component.render.bind(component);
|
|
2736
|
+
component.render = (width: number) => restyleAgentWidgetLines(render(width), width);
|
|
2737
|
+
}
|
|
2738
|
+
return component;
|
|
2739
|
+
};
|
|
2740
|
+
}
|
|
2741
|
+
return original.call(this, key, content, options);
|
|
2742
|
+
};
|
|
2743
|
+
ui[AGENT_WIDGET_PATCH_FLAG] = true;
|
|
2744
|
+
}
|
|
2745
|
+
|
|
2515
2746
|
function isSubagentNotificationMessage(message: unknown): boolean {
|
|
2516
2747
|
const candidate = message as Record<string, unknown> | undefined;
|
|
2517
2748
|
return candidate?.customType === "subagent-notification";
|
|
@@ -2617,8 +2848,11 @@ function patchCustomMessageRender(): void {
|
|
|
2617
2848
|
});
|
|
2618
2849
|
const lines = originalRender.call(this, width);
|
|
2619
2850
|
if (!Array.isArray(lines)) return lines;
|
|
2851
|
+
const details = (this?.message as any)?.details;
|
|
2620
2852
|
const result = isSubagentNotificationMessage(this?.message)
|
|
2621
|
-
?
|
|
2853
|
+
? details && typeof details === "object" && typeof details.description === "string"
|
|
2854
|
+
? frameToolLikeLines(renderSubagentNotificationLines(details, this._expanded === true, Math.max(1, width), getGlobalPiTheme() as Theme), width)
|
|
2855
|
+
: formatSubagentNotification(lines, width)
|
|
2622
2856
|
: lines.map(normalizeLeadingCheckGlyph);
|
|
2623
2857
|
return storeMessageRenderCache(this, width, result);
|
|
2624
2858
|
};
|
|
@@ -2660,19 +2894,6 @@ function stripBackgroundAnsi(text: string): string {
|
|
|
2660
2894
|
});
|
|
2661
2895
|
}
|
|
2662
2896
|
|
|
2663
|
-
function roundedUserBorder(width: number, top: boolean): string {
|
|
2664
|
-
if (width <= 1) return `${BORDER_COLOR}│${TRANSPARENT_RESET}`;
|
|
2665
|
-
const left = top ? "╭" : "╰";
|
|
2666
|
-
const right = top ? "╮" : "╯";
|
|
2667
|
-
if (!top || width < 10) {
|
|
2668
|
-
return `${BORDER_COLOR}${left}${"─".repeat(Math.max(0, width - 2))}${right}${TRANSPARENT_RESET}`;
|
|
2669
|
-
}
|
|
2670
|
-
const label = `${WORKED_LINE_FG} User ${TRANSPARENT_RESET}`;
|
|
2671
|
-
const prefix = "─";
|
|
2672
|
-
const suffixWidth = Math.max(0, width - 2 - visibleWidth(prefix) - visibleWidth(label));
|
|
2673
|
-
return `${BORDER_COLOR}${left}${prefix}${TRANSPARENT_RESET}${label}${BORDER_COLOR}${"─".repeat(suffixWidth)}${right}${TRANSPARENT_RESET}`;
|
|
2674
|
-
}
|
|
2675
|
-
|
|
2676
2897
|
function trimAnsiRight(text: string): string {
|
|
2677
2898
|
let trimmed = text;
|
|
2678
2899
|
while (true) {
|
|
@@ -2686,11 +2907,31 @@ function cleanUserMessageLine(line: string): string {
|
|
|
2686
2907
|
return `${TRANSPARENT_BG}${trimAnsiRight(stripBackgroundAnsi(stripOsc133Zones(line)))}${TRANSPARENT_BG}`;
|
|
2687
2908
|
}
|
|
2688
2909
|
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2910
|
+
// Past prompts: one band in the theme's userMessageBg, with ❯ where tool rows put their ●
|
|
2911
|
+
// (column 1) and the text where tool labels start (column 3).
|
|
2912
|
+
const USER_PROMPT_MARK = "❯";
|
|
2913
|
+
const USER_MESSAGE_PAD = 3;
|
|
2914
|
+
|
|
2915
|
+
/** Background escape for the prompt band, or "" when the theme has no distinct userMessageBg. */
|
|
2916
|
+
function userMessageBandAnsi(): string {
|
|
2917
|
+
const theme = getGlobalPiTheme();
|
|
2918
|
+
const band = themeTokenRgb(theme, "userMessageBg");
|
|
2919
|
+
const terminal = terminalDefaultColors(theme)?.background;
|
|
2920
|
+
if (!band || (terminal && rgbToHex(band) === rgbToHex(terminal))) return "";
|
|
2921
|
+
return rgbBgAnsi(theme, band);
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
function markUserPromptLine(line: string): string {
|
|
2925
|
+
const mark = `${chromeAnsi(getGlobalPiTheme(), "outline")}${USER_PROMPT_MARK}\x1b[39m`;
|
|
2926
|
+
return line.replace(/^((?:\x1b\[[0-9;]*m)*) {3}/, (_match, esc: string) => `${esc} ${mark} `);
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
/** Paint a full-width band behind a line, surviving resets inside it. */
|
|
2930
|
+
function withUserBand(line: string, width: number, band: string): string {
|
|
2931
|
+
const content = clampLineWidth(line, width);
|
|
2932
|
+
if (!band) return content;
|
|
2933
|
+
const pad = " ".repeat(Math.max(0, width - visibleWidth(content)));
|
|
2934
|
+
return `${band}${content.replace(/\x1b\[0?m|\x1b\[49m/g, (reset) => reset + band)}${pad}${TRANSPARENT_BG}`;
|
|
2694
2935
|
}
|
|
2695
2936
|
|
|
2696
2937
|
function visitMarkdownDescendants(root: unknown, visit: (md: InstanceType<typeof Markdown>) => void): void {
|
|
@@ -2707,9 +2948,19 @@ function patchUserMessageRender(): void {
|
|
|
2707
2948
|
if (proto[USER_MESSAGE_PATCH_FLAG]) return;
|
|
2708
2949
|
const originalRender = proto.render;
|
|
2709
2950
|
if (typeof originalRender !== "function") return;
|
|
2951
|
+
const originalRebuild = proto.rebuild;
|
|
2952
|
+
if (typeof originalRebuild === "function") {
|
|
2953
|
+
proto.rebuild = function patchedUserMessageRebuild(this: any, ...args: any[]) {
|
|
2954
|
+
// Room for the ❯ marker: text starts in column 3, like tool labels.
|
|
2955
|
+
this.outputPad = Math.max(this.outputPad ?? 1, USER_MESSAGE_PAD);
|
|
2956
|
+
clearMessageRenderCache(this);
|
|
2957
|
+
return originalRebuild.apply(this, args);
|
|
2958
|
+
};
|
|
2959
|
+
}
|
|
2710
2960
|
proto.render = function patchedUserMessageRender(width: number) {
|
|
2711
2961
|
const cached = messageRenderCacheHit(this, width);
|
|
2712
2962
|
if (cached) return cached;
|
|
2963
|
+
if ((this.outputPad ?? 1) < USER_MESSAGE_PAD) this.rebuild?.();
|
|
2713
2964
|
visitMarkdownDescendants(this, (child) => {
|
|
2714
2965
|
const markdownAny = child as any;
|
|
2715
2966
|
if (typeof markdownAny.text === "string") {
|
|
@@ -2720,26 +2971,58 @@ function patchUserMessageRender(): void {
|
|
|
2720
2971
|
}
|
|
2721
2972
|
}
|
|
2722
2973
|
makeMarkdownLinksCopySafe(child);
|
|
2974
|
+
// The band is painted below, from the theme's concrete color; pi's own
|
|
2975
|
+
// background would be the transparent override from applyToolBackgroundMode.
|
|
2723
2976
|
if (markdownAny.defaultTextStyle?.bgColor) {
|
|
2724
2977
|
markdownAny.defaultTextStyle.bgColor = undefined;
|
|
2725
2978
|
child.invalidate?.();
|
|
2726
2979
|
}
|
|
2727
2980
|
});
|
|
2728
|
-
const
|
|
2729
|
-
|
|
2730
|
-
|
|
2981
|
+
const safeWidth = Math.max(1, width);
|
|
2982
|
+
// Pi before 1.4 draws prompts in a Box with one column of padding and no outputPad:
|
|
2983
|
+
// render two columns narrower and shift right instead.
|
|
2984
|
+
const padded = typeof this.rebuild === "function" && "outputPad" in this;
|
|
2985
|
+
const lines = originalRender.call(this, padded ? safeWidth : Math.max(1, safeWidth - 2));
|
|
2731
2986
|
if (!Array.isArray(lines) || lines.length === 0) return lines;
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
const
|
|
2738
|
-
return storeMessageRenderCache(this, width, applyTerminalCopyZones(
|
|
2987
|
+
// Drop the top and bottom padding rows: one compact band.
|
|
2988
|
+
let body = (lines.length > 2 ? lines.slice(1, -1) : lines).map(cleanUserMessageLine);
|
|
2989
|
+
if (!padded) body = body.map((line) => line.replace(/^((?:\x1b\[[0-9;]*m)*)/, "$1 "));
|
|
2990
|
+
body[0] = markUserPromptLine(body[0]);
|
|
2991
|
+
const band = userMessageBandAnsi();
|
|
2992
|
+
const rendered = body.map((line) => withUserBand(line, safeWidth, band));
|
|
2993
|
+
return storeMessageRenderCache(this, width, applyTerminalCopyZones(rendered));
|
|
2739
2994
|
};
|
|
2740
2995
|
proto[USER_MESSAGE_PATCH_FLAG] = true;
|
|
2741
2996
|
}
|
|
2742
2997
|
|
|
2998
|
+
// The input line gets the same ❯ as past prompts. Wraps whatever editor is active
|
|
2999
|
+
// (pi's default, or one set by another extension) instead of replacing it.
|
|
3000
|
+
const PROMPT_EDITOR_FLAG = Symbol.for("pi-claude-style-tools:prompt-marker-editor");
|
|
3001
|
+
|
|
3002
|
+
function installPromptMarkerEditor(ctx: any): void {
|
|
3003
|
+
const ui = ctx?.ui;
|
|
3004
|
+
if (typeof ui?.setEditorComponent !== "function" || typeof ui?.getEditorComponent !== "function") return;
|
|
3005
|
+
const previous = ui.getEditorComponent();
|
|
3006
|
+
if (previous?.[PROMPT_EDITOR_FLAG]) return;
|
|
3007
|
+
const factory: any = (tui: any, theme: any, keybindings: any) => {
|
|
3008
|
+
const editor: any = previous?.(tui, theme, keybindings) ?? new CustomEditor(tui, theme, keybindings);
|
|
3009
|
+
if (typeof editor?.render !== "function") return editor;
|
|
3010
|
+
const render = editor.render.bind(editor);
|
|
3011
|
+
editor.render = (width: number) => {
|
|
3012
|
+
// Every render: pi re-applies the editorPaddingX setting after creation.
|
|
3013
|
+
if ((editor.paddingX ?? 0) < USER_MESSAGE_PAD) editor.paddingX = USER_MESSAGE_PAD;
|
|
3014
|
+
const lines: string[] = render(width);
|
|
3015
|
+
if (Array.isArray(lines) && lines.length > 1) lines[1] = markUserPromptLine(lines[1]);
|
|
3016
|
+
return lines;
|
|
3017
|
+
};
|
|
3018
|
+
return editor;
|
|
3019
|
+
};
|
|
3020
|
+
factory[PROMPT_EDITOR_FLAG] = true;
|
|
3021
|
+
try {
|
|
3022
|
+
ui.setEditorComponent(factory);
|
|
3023
|
+
} catch { /* not interactive */ }
|
|
3024
|
+
}
|
|
3025
|
+
|
|
2743
3026
|
function patchAssistantMessages(): void {
|
|
2744
3027
|
const proto = AssistantMessageComponent.prototype as any;
|
|
2745
3028
|
if (proto[ASSISTANT_PATCH_FLAG]) return;
|
|
@@ -3563,13 +3846,6 @@ function withFinalBranchBlock(content: string, theme: Theme, isError = false): s
|
|
|
3563
3846
|
return [branchLead(first, true, theme), ...middle, branchLead(last, false, theme)].join("\n");
|
|
3564
3847
|
}
|
|
3565
3848
|
|
|
3566
|
-
function indentBranchBlock(block: string): string {
|
|
3567
|
-
return block
|
|
3568
|
-
.split("\n")
|
|
3569
|
-
.map((line) => (line ? ` ${line}` : line))
|
|
3570
|
-
.join("\n");
|
|
3571
|
-
}
|
|
3572
|
-
|
|
3573
3849
|
// ---------------------------------------------------------------------------
|
|
3574
3850
|
// Blink timer for partial (running) states
|
|
3575
3851
|
// ---------------------------------------------------------------------------
|
|
@@ -4467,11 +4743,11 @@ function liveBranchDisplay(state: Record<string, unknown> | undefined, theme: Th
|
|
|
4467
4743
|
if (!state || typeof state !== "object") return undefined;
|
|
4468
4744
|
const body = state._ptBody;
|
|
4469
4745
|
if (typeof body === "string" && body.trim() && !body.includes("(rendering")) {
|
|
4470
|
-
return
|
|
4746
|
+
return withBranch(body, theme, false, true);
|
|
4471
4747
|
}
|
|
4472
4748
|
const display = state._ptDisplay;
|
|
4473
4749
|
if (typeof display === "string" && display.trim()) {
|
|
4474
|
-
return
|
|
4750
|
+
return withBranch(stripBranchMarkupBlock(display), theme, false, true);
|
|
4475
4751
|
}
|
|
4476
4752
|
return undefined;
|
|
4477
4753
|
}
|
|
@@ -4480,13 +4756,13 @@ function refreshToolBranchDisplaysInState(state: Record<string, unknown> | undef
|
|
|
4480
4756
|
if (!state || typeof state !== "object") return;
|
|
4481
4757
|
const body = state._ptBody;
|
|
4482
4758
|
if (typeof body === "string" && body.trim() && !body.includes("(rendering")) {
|
|
4483
|
-
state._ptDisplay =
|
|
4759
|
+
state._ptDisplay = withBranch(body, theme, false, true);
|
|
4484
4760
|
return;
|
|
4485
4761
|
}
|
|
4486
4762
|
const display = state._ptDisplay;
|
|
4487
4763
|
if (typeof display === "string" && display.trim()) {
|
|
4488
4764
|
const stripped = stripBranchMarkupBlock(display);
|
|
4489
|
-
state._ptDisplay =
|
|
4765
|
+
state._ptDisplay = withBranch(stripped, theme, false, true);
|
|
4490
4766
|
}
|
|
4491
4767
|
}
|
|
4492
4768
|
|
|
@@ -5223,6 +5499,7 @@ const EXT_LANG: Record<string, BundledLanguage> = {
|
|
|
5223
5499
|
cs: "csharp",
|
|
5224
5500
|
swift: "swift",
|
|
5225
5501
|
kt: "kotlin",
|
|
5502
|
+
kts: "kotlin",
|
|
5226
5503
|
html: "html",
|
|
5227
5504
|
css: "css",
|
|
5228
5505
|
scss: "scss",
|
|
@@ -5905,7 +6182,7 @@ function renderEditPreviewBody(
|
|
|
5905
6182
|
summary: string,
|
|
5906
6183
|
): void {
|
|
5907
6184
|
const dc = resolveDiffColors(theme);
|
|
5908
|
-
const branchWidth = contextDiffWidth(ctx
|
|
6185
|
+
const branchWidth = contextDiffWidth(ctx);
|
|
5909
6186
|
if (operations.length === 1) {
|
|
5910
6187
|
const [diff] = diffs;
|
|
5911
6188
|
const line = lines[0] ?? getFirstChangedNewLine(diff);
|
|
@@ -5913,13 +6190,13 @@ function renderEditPreviewBody(
|
|
|
5913
6190
|
.then((rendered) => {
|
|
5914
6191
|
if (ctx.state._pk !== key) return;
|
|
5915
6192
|
ctx.state._ptBody = `${summarizeDiff(diff.added, diff.removed)}${formatLineMeta(line, theme)}\n${rendered}`;
|
|
5916
|
-
ctx.state._ptDisplay =
|
|
6193
|
+
ctx.state._ptDisplay = withBranch(ctx.state._ptBody, theme, false, true);
|
|
5917
6194
|
safeInvalidate(ctx);
|
|
5918
6195
|
})
|
|
5919
6196
|
.catch(() => {
|
|
5920
6197
|
if (ctx.state._pk !== key) return;
|
|
5921
6198
|
ctx.state._ptBody = `${summarizeDiff(diff.added, diff.removed)}${formatLineMeta(line, theme)}`;
|
|
5922
|
-
ctx.state._ptDisplay =
|
|
6199
|
+
ctx.state._ptDisplay = withBranch(ctx.state._ptBody, theme, false, true);
|
|
5923
6200
|
safeInvalidate(ctx);
|
|
5924
6201
|
});
|
|
5925
6202
|
return;
|
|
@@ -5941,13 +6218,13 @@ function renderEditPreviewBody(
|
|
|
5941
6218
|
? `\n${theme.fg("muted", `… ${remainder} more edit blocks${toolOutputDetailHint(theme, ctx.expanded, true)}`)}`
|
|
5942
6219
|
: "";
|
|
5943
6220
|
ctx.state._ptBody = `${operations.length} edits ${summary}\n\n${sections.join("\n\n")}${suffix}`;
|
|
5944
|
-
ctx.state._ptDisplay =
|
|
6221
|
+
ctx.state._ptDisplay = withBranch(ctx.state._ptBody, theme, false, true);
|
|
5945
6222
|
safeInvalidate(ctx);
|
|
5946
6223
|
})
|
|
5947
6224
|
.catch(() => {
|
|
5948
6225
|
if (ctx.state._pk !== key) return;
|
|
5949
6226
|
ctx.state._ptBody = `${operations.length} edits ${summary}`;
|
|
5950
|
-
ctx.state._ptDisplay =
|
|
6227
|
+
ctx.state._ptDisplay = withBranch(ctx.state._ptBody, theme, false, true);
|
|
5951
6228
|
safeInvalidate(ctx);
|
|
5952
6229
|
});
|
|
5953
6230
|
}
|
|
@@ -6975,8 +7252,11 @@ function summarizeOpenAiToolCall(name: string, args: any, theme: Theme, sp: (pat
|
|
|
6975
7252
|
return theme.fg("muted", "enable read-only planning");
|
|
6976
7253
|
case "ExitPlanMode":
|
|
6977
7254
|
return theme.fg("muted", "present plan");
|
|
6978
|
-
case "Agent":
|
|
6979
|
-
|
|
7255
|
+
case "Agent": {
|
|
7256
|
+
const description = summarizeText(getStringArg(args, "description", "prompt") || "launch agent", 72);
|
|
7257
|
+
const type = getStringArg(args, "subagent_type");
|
|
7258
|
+
return type ? `${description} ${theme.fg("muted", `(${type})`)}` : description;
|
|
7259
|
+
}
|
|
6980
7260
|
case "get_subagent_result":
|
|
6981
7261
|
return getStringArg(args, "agent_id") || theme.fg("muted", "agent result");
|
|
6982
7262
|
case "steer_subagent":
|
|
@@ -6997,10 +7277,7 @@ function summarizeOpenAiToolCall(name: string, args: any, theme: Theme, sp: (pat
|
|
|
6997
7277
|
return taskIds.length === 1 ? taskIds[0] : `${taskIds[0]} ${theme.fg("muted", `(+${taskIds.length - 1} tasks)`)}`;
|
|
6998
7278
|
}
|
|
6999
7279
|
default:
|
|
7000
|
-
return
|
|
7001
|
-
getStringArg(args, "path", "file_path", "url", "query", "name", "subject", "tool", "description", "prompt") || humanizeToolName(name),
|
|
7002
|
-
72,
|
|
7003
|
-
);
|
|
7280
|
+
return summarizeUnknownToolArgs(args);
|
|
7004
7281
|
}
|
|
7005
7282
|
}
|
|
7006
7283
|
|
|
@@ -7136,6 +7413,10 @@ function renderReadImageResult(result: any, expanded: boolean, theme: Theme, ctx
|
|
|
7136
7413
|
}
|
|
7137
7414
|
|
|
7138
7415
|
function renderOpenAiToolResult(name: string, result: any, expanded: boolean, isPartial: boolean, theme: Theme, ctx: any): Text {
|
|
7416
|
+
if (name === "Agent") {
|
|
7417
|
+
const agentRow = renderAgentToolResult(result, expanded, isPartial, theme, ctx);
|
|
7418
|
+
if (agentRow) return agentRow;
|
|
7419
|
+
}
|
|
7139
7420
|
if (isPartial) {
|
|
7140
7421
|
return makeText(ctx.lastComponent, runningPreviewBlock(result, theme.fg("dim", `${humanizeToolName(name)}...`), expanded, theme, ctx));
|
|
7141
7422
|
}
|
|
@@ -7182,6 +7463,116 @@ function renderOpenAiToolResult(name: string, result: any, expanded: boolean, is
|
|
|
7182
7463
|
return makeText(ctx.lastComponent, withBranch(`${statusText}\n${preview}`, theme));
|
|
7183
7464
|
}
|
|
7184
7465
|
|
|
7466
|
+
// ===========================================================================
|
|
7467
|
+
// Bash commands that only change files (sed -i, heredocs, ...), drawn as edit/write rows
|
|
7468
|
+
// ===========================================================================
|
|
7469
|
+
|
|
7470
|
+
interface BashFileEditRecord {
|
|
7471
|
+
/** Path as written in the command. */
|
|
7472
|
+
path: string;
|
|
7473
|
+
created: boolean;
|
|
7474
|
+
diff: ParsedDiff;
|
|
7475
|
+
language?: BundledLanguage;
|
|
7476
|
+
}
|
|
7477
|
+
|
|
7478
|
+
const MAX_BASH_SNAPSHOT_BYTES = 1_000_000;
|
|
7479
|
+
|
|
7480
|
+
function resolveBashEditPath(cwd: string, edit: BashFileEdit, path: string): string {
|
|
7481
|
+
const home = process.env.HOME ?? "";
|
|
7482
|
+
const expand = (value: string) => (home && (value === "~" || value.startsWith("~/")) ? `${home}${value.slice(1)}` : value);
|
|
7483
|
+
return resolve(edit.cd ? resolve(cwd, expand(edit.cd)) : cwd, expand(path));
|
|
7484
|
+
}
|
|
7485
|
+
|
|
7486
|
+
/** File text; null when the file does not exist; undefined when it can't be compared (too big, binary, unreadable). */
|
|
7487
|
+
function readBashEditSnapshot(path: string): string | null | undefined {
|
|
7488
|
+
try {
|
|
7489
|
+
if (!existsSync(path)) return null;
|
|
7490
|
+
const stat = statSync(path);
|
|
7491
|
+
if (!stat.isFile() || stat.size > MAX_BASH_SNAPSHOT_BYTES) return undefined;
|
|
7492
|
+
const text = readFileSync(path, "utf8");
|
|
7493
|
+
return text.includes("\0") ? undefined : text;
|
|
7494
|
+
} catch {
|
|
7495
|
+
return undefined;
|
|
7496
|
+
}
|
|
7497
|
+
}
|
|
7498
|
+
|
|
7499
|
+
function snapshotBashFileEdit(cwd: string, edit: BashFileEdit): Map<string, string | null | undefined> {
|
|
7500
|
+
const snapshot = new Map<string, string | null | undefined>();
|
|
7501
|
+
for (const path of edit.paths) snapshot.set(path, readBashEditSnapshot(resolveBashEditPath(cwd, edit, path)));
|
|
7502
|
+
return snapshot;
|
|
7503
|
+
}
|
|
7504
|
+
|
|
7505
|
+
function collectBashFileEdits(cwd: string, edit: BashFileEdit, before: Map<string, string | null | undefined>): BashFileEditRecord[] {
|
|
7506
|
+
const records: BashFileEditRecord[] = [];
|
|
7507
|
+
for (const path of edit.paths) {
|
|
7508
|
+
const full = resolveBashEditPath(cwd, edit, path);
|
|
7509
|
+
const previous = before.get(path);
|
|
7510
|
+
const current = readBashEditSnapshot(full);
|
|
7511
|
+
if (previous === undefined || typeof current !== "string" || previous === current) continue;
|
|
7512
|
+
records.push({ path, created: previous === null, diff: parseDiff(previous ?? "", current), language: lang(full) });
|
|
7513
|
+
}
|
|
7514
|
+
return records;
|
|
7515
|
+
}
|
|
7516
|
+
|
|
7517
|
+
function bashFileEditHeader(edit: BashFileEdit, records: BashFileEditRecord[] | undefined, theme: Theme, cwd: string, sp: (path: string) => string, prefix: string, trailing: string): string {
|
|
7518
|
+
applyThemePaletteIfNeeded(theme);
|
|
7519
|
+
const created = edit.kind === "write" && !!records?.length && records.every((record) => record.created);
|
|
7520
|
+
const label = edit.kind === "edit" ? "Edit" : created ? "Create" : "Write";
|
|
7521
|
+
const first = sp(resolveBashEditPath(cwd, edit, edit.paths[0]));
|
|
7522
|
+
const more = edit.paths.length > 1 ? ` ${theme.fg("muted", `(+${edit.paths.length - 1} files)`)}` : "";
|
|
7523
|
+
return `${prefix}${theme.fg("toolTitle", theme.bold(label))} ${theme.fg("muted", `(${edit.via})`)} ${WRAP_MARK}${theme.fg("accent", first)}${more}${trailing}`;
|
|
7524
|
+
}
|
|
7525
|
+
|
|
7526
|
+
/** The diff block for one changed file, rendered asynchronously like write's; "" until known. */
|
|
7527
|
+
function bashFileEditDiffBlock(record: BashFileEditRecord, index: number, theme: Theme, ctx: any): string {
|
|
7528
|
+
const diff = record.diff;
|
|
7529
|
+
const previewLines = ctx.expanded ? MAX_RENDER_LINES : diffCollapsedLimit();
|
|
7530
|
+
const hunks = diff.lines.filter((line) => line.type === "sep").length + (diff.lines.length ? 1 : 0);
|
|
7531
|
+
const width = contextDiffWidth(ctx);
|
|
7532
|
+
const mode = record.created ? "new file" : shouldUseSplit(diff, width, previewLines) ? "split" : "unified";
|
|
7533
|
+
const summary = diffSummaryWithMeta(diff.added, diff.removed, hunks, mode);
|
|
7534
|
+
const key = `bfe:${index}:${record.path}:${width}:${diff.added}:${diff.removed}:${diff.lines.length}:${ctx.expanded ? 1 : 0}:${_toolBranchVisualEpoch}`;
|
|
7535
|
+
const keys = (ctx.state._bfeKeys ??= []) as string[];
|
|
7536
|
+
const blocks = (ctx.state._bfeBlocks ??= []) as string[];
|
|
7537
|
+
if (keys[index] !== key) {
|
|
7538
|
+
keys[index] = key;
|
|
7539
|
+
blocks[index] = `${summary}\n${theme.fg("muted", "rendering diff…")}`;
|
|
7540
|
+
const dc = resolveDiffColors(theme);
|
|
7541
|
+
const render = mode === "split" ? renderSplit(diff, record.language, previewLines, dc, width) : renderUnified(diff, record.language, previewLines, dc, width);
|
|
7542
|
+
render
|
|
7543
|
+
.then((rendered) => {
|
|
7544
|
+
if (keys[index] !== key) return;
|
|
7545
|
+
blocks[index] = `${summary}\n${rendered}`;
|
|
7546
|
+
safeInvalidate(ctx);
|
|
7547
|
+
})
|
|
7548
|
+
.catch(() => {
|
|
7549
|
+
if (keys[index] !== key) return;
|
|
7550
|
+
blocks[index] = summary;
|
|
7551
|
+
safeInvalidate(ctx);
|
|
7552
|
+
});
|
|
7553
|
+
}
|
|
7554
|
+
return blocks[index];
|
|
7555
|
+
}
|
|
7556
|
+
|
|
7557
|
+
/** Result body for a bash file edit, or undefined to fall back to the ordinary bash rendering. */
|
|
7558
|
+
function renderBashFileEditResult(edit: BashFileEdit, records: BashFileEditRecord[] | undefined, theme: Theme, ctx: any, sp: (path: string) => string): Text | undefined {
|
|
7559
|
+
if (records === undefined) {
|
|
7560
|
+
// Rows from before diffs were recorded: a heredoc or echo still says what it wrote.
|
|
7561
|
+
if (edit.content === undefined || edit.kind !== "write") return undefined;
|
|
7562
|
+
const diff = getCachedParsedDiff(ctx, `bfe-content:${hashText(edit.content)}`, "", edit.content);
|
|
7563
|
+
records = [{ path: edit.paths[0], created: true, diff, language: lang(edit.paths[0]) }];
|
|
7564
|
+
}
|
|
7565
|
+
if (records.length === 0) return makeText(ctx.lastComponent, withBranch(theme.fg("muted", "✓ no changes"), theme));
|
|
7566
|
+
if (!diffWidthReady(ctx)) {
|
|
7567
|
+
return makeResponsiveDiffText(ctx, ctx.lastComponent, withFinalBranchBlock(theme.fg("muted", "rendering diff…"), theme));
|
|
7568
|
+
}
|
|
7569
|
+
const blocks = records.map((record, index) => {
|
|
7570
|
+
const block = bashFileEditDiffBlock(record, index, theme, ctx);
|
|
7571
|
+
return records!.length > 1 ? `${theme.fg("accent", sp(resolveBashEditPath(ctx.cwd ?? process.cwd(), edit, record.path)))}\n${block}` : block;
|
|
7572
|
+
});
|
|
7573
|
+
return makeResponsiveDiffText(ctx, ctx.lastComponent, withFinalBranchBlock(blocks.join("\n"), theme));
|
|
7574
|
+
}
|
|
7575
|
+
|
|
7185
7576
|
// ===========================================================================
|
|
7186
7577
|
// Extension
|
|
7187
7578
|
// ===========================================================================
|
|
@@ -7200,6 +7591,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7200
7591
|
patchAssistantMessages();
|
|
7201
7592
|
patchAssistantFullscreenMouse();
|
|
7202
7593
|
patchToolExecutionRenderers();
|
|
7594
|
+
patchBashFileEditGrouping();
|
|
7203
7595
|
applyDiffPalette();
|
|
7204
7596
|
registerThinkingLabels(pi);
|
|
7205
7597
|
syncExtraToolDetailMode();
|
|
@@ -7591,6 +7983,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7591
7983
|
clearRtkRewriteState();
|
|
7592
7984
|
if (!ctx.hasUI) return;
|
|
7593
7985
|
patchUiNotifications(ctx.ui);
|
|
7986
|
+
patchAgentWidget(ctx.ui);
|
|
7987
|
+
installPromptMarkerEditor(ctx);
|
|
7594
7988
|
// terminalTheme (on by default): switch to, and stay on, the theme built from the
|
|
7595
7989
|
// terminal's palette. `pi --use-theme <name>` still wins for that run.
|
|
7596
7990
|
if (terminalThemeEnabled() && !(themeChosenOnCommandLine() && activeThemeName(ctx.ui) !== SYSTEM_THEME_NAME)) {
|
|
@@ -7612,6 +8006,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7612
8006
|
pi.on("turn_start", async (_event, ctx) => {
|
|
7613
8007
|
if (!ctx.hasUI) return;
|
|
7614
8008
|
patchUiNotifications(ctx.ui);
|
|
8009
|
+
patchAgentWidget(ctx.ui);
|
|
7615
8010
|
applyToolBackgroundMode(ctx.ui.theme);
|
|
7616
8011
|
applyThemePaletteIfNeeded(ctx.ui.theme);
|
|
7617
8012
|
});
|
|
@@ -7698,13 +8093,31 @@ export default function (pi: ExtensionAPI) {
|
|
|
7698
8093
|
promptGuidelines: (bashTool as any).promptGuidelines,
|
|
7699
8094
|
parameters: bashTool.parameters,
|
|
7700
8095
|
async execute(toolCallId, params, signal, onUpdate) {
|
|
7701
|
-
|
|
8096
|
+
// A command that only changes files: look at them before and after, so the row
|
|
8097
|
+
// can show a diff like an edit/write row. The command itself runs unchanged.
|
|
8098
|
+
const fileEdit = detectBashFileEdit(typeof params.command === "string" ? params.command : "");
|
|
8099
|
+
const before = fileEdit ? snapshotBashFileEdit(cwd, fileEdit) : undefined;
|
|
8100
|
+
const result = await bashTool.execute(toolCallId, params, signal, onUpdate);
|
|
8101
|
+
if (fileEdit && before) {
|
|
8102
|
+
const details = result.details && typeof result.details === "object" ? result.details : {};
|
|
8103
|
+
(result as any).details = { ...details, fileEdits: collectBashFileEdits(cwd, fileEdit, before) };
|
|
8104
|
+
}
|
|
8105
|
+
return result;
|
|
7702
8106
|
},
|
|
7703
8107
|
renderCall(args, theme, ctx) {
|
|
7704
8108
|
syncToolCallStatus(ctx);
|
|
7705
8109
|
syncBashDuration(ctx);
|
|
7706
8110
|
const rewrite = ensureRtkRewriteForContext(ctx, args);
|
|
7707
8111
|
const command = typeof args.command === "string" ? args.command : "";
|
|
8112
|
+
const fileEdit = ctx.argsComplete === true || ctx.executionStarted === true ? detectBashFileEdit(command) : undefined;
|
|
8113
|
+
if (fileEdit) {
|
|
8114
|
+
const status = ctx?.state?._toolStatus;
|
|
8115
|
+
const showCommand = status === "error" || ctx.expanded === true;
|
|
8116
|
+
const commandBlock = showCommand ? renderBashCommandBlock(command, ctx.expanded === true, theme) : "";
|
|
8117
|
+
const rowCwd = ctx.cwd ?? cwd;
|
|
8118
|
+
const header = bashFileEditHeader(fileEdit, ctx.state?._bashFileEdits, theme, rowCwd, (path) => shortPath(rowCwd, path), toolStatusDot(ctx, theme), bashHeaderTrailing(ctx, theme));
|
|
8119
|
+
return makeText(ctx.lastComponent, commandBlock ? `${header}\n${commandBlock}` : header);
|
|
8120
|
+
}
|
|
7708
8121
|
const presentation = buildBashCommandPresentation(command);
|
|
7709
8122
|
const summary = stableCallSummary(ctx, "_bashHeadline", () => presentation.headline);
|
|
7710
8123
|
const rtkBadge = rewrite ? theme.fg("muted", " (RTK)") : "";
|
|
@@ -7726,6 +8139,22 @@ export default function (pi: ExtensionAPI) {
|
|
|
7726
8139
|
const details = result.details as BashToolDetails | undefined;
|
|
7727
8140
|
const rewrite = ensureRtkRewriteForContext(ctx, ctx.args);
|
|
7728
8141
|
const output = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
8142
|
+
const fileEdit = !isPartial && !ctx.isError ? detectBashFileEdit(typeof ctx.args?.command === "string" ? ctx.args.command : "") : undefined;
|
|
8143
|
+
if (fileEdit && !/exit code: [1-9]/.test(output)) {
|
|
8144
|
+
const records = Array.isArray((details as any)?.fileEdits) ? (details as any).fileEdits as BashFileEditRecord[] : undefined;
|
|
8145
|
+
if (ctx.state && ctx.state._bashFileEdits !== records) {
|
|
8146
|
+
ctx.state._bashFileEdits = records;
|
|
8147
|
+
// The header says Create or Write from these; draw it again (not from inside this render).
|
|
8148
|
+
unrefTimer(setTimeout(() => safeInvalidate(ctx), 0));
|
|
8149
|
+
}
|
|
8150
|
+
const rowCwd = ctx.cwd ?? cwd;
|
|
8151
|
+
const body = renderBashFileEditResult(fileEdit, records, theme, ctx, (path) => shortPath(rowCwd, path));
|
|
8152
|
+
if (body) {
|
|
8153
|
+
clearBlinkTimer(ctx);
|
|
8154
|
+
setToolStatus(ctx, "success");
|
|
8155
|
+
return body;
|
|
8156
|
+
}
|
|
8157
|
+
}
|
|
7729
8158
|
if (isPartial) {
|
|
7730
8159
|
const preview = collectNonEmptyLines(output, expanded ? undefined : liveToolPreviewLimit());
|
|
7731
8160
|
const running = runningPreviewBlock(result, "", expanded, theme, ctx, {
|
|
@@ -8056,6 +8485,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
8056
8485
|
pi.registerTool({
|
|
8057
8486
|
name: "edit",
|
|
8058
8487
|
label: "edit",
|
|
8488
|
+
// Pi's built-in edit draws its own shell (no padding); ours uses the standard one, like
|
|
8489
|
+
// every other tool. Without this, older pi falls back to the built-in "self" shell
|
|
8490
|
+
// while pi 1.4+ uses "default", so the row shifted by a column between versions.
|
|
8491
|
+
renderShell: "default",
|
|
8059
8492
|
description: editTool.description,
|
|
8060
8493
|
promptSnippet: (editTool as any).promptSnippet,
|
|
8061
8494
|
promptGuidelines: (editTool as any).promptGuidelines,
|
|
@@ -8106,21 +8539,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
8106
8539
|
const revealSummary = shouldRevealCallArgs(ctx) || (!!fp && hasOwnArg(args, "edits"));
|
|
8107
8540
|
const summary = stableCallSummary(ctx, "_callSummary", () => shouldRevealCallArgs(ctx) && operations.length > 1 ? `${sp(fp)} ${theme.fg("muted", `(${operations.length} edits)`)}` : sp(fp), revealSummary);
|
|
8108
8541
|
syncToolCallStatus(ctx);
|
|
8109
|
-
const hdr = toolHeader("Edit", summary, theme,
|
|
8542
|
+
const hdr = toolHeader("Edit", summary, theme, toolStatusDot(ctx, theme), liveLineCountTrailing(ctx, theme));
|
|
8110
8543
|
// History replay never sets argsComplete; a finished edit is drawn from its result instead.
|
|
8111
8544
|
const finished = ctx.state._editResult as Record<string, unknown> | undefined;
|
|
8112
8545
|
if (!((ctx.argsComplete || finished) && operations.length > 0)) return makeText(ctx.lastComponent, hdr);
|
|
8113
8546
|
if (!diffWidthReady(ctx)) {
|
|
8114
|
-
const pending = liveBranchDisplay(ctx.state, theme) ??
|
|
8547
|
+
const pending = liveBranchDisplay(ctx.state, theme) ?? withBranch(theme.fg("muted", "(rendering…)"), theme, false, true);
|
|
8115
8548
|
return makeResponsiveDiffText(ctx, ctx.lastComponent, `${hdr}\n${pending}`);
|
|
8116
8549
|
}
|
|
8117
|
-
const diffWidth = contextDiffWidth(ctx
|
|
8550
|
+
const diffWidth = contextDiffWidth(ctx);
|
|
8118
8551
|
const key = `edit:${fp}:${hashText(operations.map((edit) => `${edit.oldText}\u0000${edit.newText}`).join("\u0001"))}:${diffWidth}:${ctx.expanded ? 1 : 0}:${_toolBranchVisualEpoch}`;
|
|
8119
8552
|
const { diffs: fallbackDiffs, summary: editSummary } = getCachedEditOperationSummary(ctx, key, operations);
|
|
8120
8553
|
if (ctx.state._pk !== key) {
|
|
8121
8554
|
ctx.state._pk = key;
|
|
8122
8555
|
ctx.state._ptBody = theme.fg("muted", "(rendering…)");
|
|
8123
|
-
ctx.state._ptDisplay =
|
|
8556
|
+
ctx.state._ptDisplay = withBranch(ctx.state._ptBody, theme, false, true);
|
|
8124
8557
|
const lg = lang(fp);
|
|
8125
8558
|
// Located once per row: expanding or resizing must not look in the file again, it no longer holds oldText.
|
|
8126
8559
|
ctx.state._editLocalized ??= !finished
|
|
@@ -8145,7 +8578,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
8145
8578
|
},
|
|
8146
8579
|
renderResult(result, { expanded, isPartial }, theme, ctx) {
|
|
8147
8580
|
if (isPartial) {
|
|
8148
|
-
return makeText(ctx.lastComponent,
|
|
8581
|
+
return makeText(ctx.lastComponent, runningPreviewBlock(result, theme.fg("dim", "Editing..."), expanded, theme, ctx));
|
|
8149
8582
|
}
|
|
8150
8583
|
clearBlinkTimer(ctx);
|
|
8151
8584
|
setToolStatus(ctx, ctx.isError ? "error" : "success");
|
|
@@ -8155,20 +8588,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
8155
8588
|
?.filter((c: any) => c.type === "text")
|
|
8156
8589
|
.map((c: any) => c.text || "")
|
|
8157
8590
|
.join("\n") ?? "Error";
|
|
8158
|
-
return makeText(ctx.lastComponent,
|
|
8591
|
+
return makeText(ctx.lastComponent, withBranch(theme.fg("error", e), theme));
|
|
8159
8592
|
}
|
|
8160
8593
|
if ((result as any).details?._type === "editInfo") {
|
|
8161
8594
|
const { editLine, hunks, added, removed } = (result as any).details;
|
|
8162
8595
|
const loc = formatLineMeta(editLine ?? 0, theme);
|
|
8163
8596
|
const summary = diffSummaryWithMeta(added ?? 0, removed ?? 0, hunks ?? 0, "");
|
|
8164
|
-
return makeText(ctx.lastComponent,
|
|
8597
|
+
return makeText(ctx.lastComponent, withBranch(`${summary}${loc}`, theme));
|
|
8165
8598
|
}
|
|
8166
8599
|
if ((result as any).details?._type === "multiEditInfo") {
|
|
8167
8600
|
const { editCount, diffLineCount, hunks, totalAdded, totalRemoved } = (result as any).details;
|
|
8168
8601
|
const summary = diffSummaryWithMeta(totalAdded ?? 0, totalRemoved ?? 0, hunks ?? 0, "");
|
|
8169
|
-
return makeText(ctx.lastComponent,
|
|
8602
|
+
return makeText(ctx.lastComponent, withBranch(`${editCount} edits ${summary}${typeof diffLineCount === "number" ? ` ${theme.fg("muted", `(${diffLineCount} diff lines)`)}` : ""}`, theme));
|
|
8170
8603
|
}
|
|
8171
|
-
return makeText(ctx.lastComponent,
|
|
8604
|
+
return makeText(ctx.lastComponent, withBranch(theme.fg("success", "Applied"), theme));
|
|
8172
8605
|
},
|
|
8173
8606
|
});
|
|
8174
8607
|
|