pi-claude-code-ui 1.0.83 → 1.0.85
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 +10 -0
- package/README.md +3 -3
- package/extensions/index.ts +514 -119
- package/extensions/settings-path.ts +4 -0
- package/extensions/spinner.ts +63 -18
- package/package.json +4 -2
package/extensions/index.ts
CHANGED
|
@@ -41,6 +41,24 @@ import {
|
|
|
41
41
|
visibleWidth,
|
|
42
42
|
wrapTextWithAnsi,
|
|
43
43
|
} from "@earendil-works/pi-tui";
|
|
44
|
+
import * as PiTui from "@earendil-works/pi-tui";
|
|
45
|
+
|
|
46
|
+
// Pi 0.85+ fullscreen mouse types. The 0.79 peer typings do not export them yet.
|
|
47
|
+
interface TuiMouseEvent {
|
|
48
|
+
type: string;
|
|
49
|
+
button: string;
|
|
50
|
+
x: number;
|
|
51
|
+
y: number;
|
|
52
|
+
width: number;
|
|
53
|
+
height: number;
|
|
54
|
+
}
|
|
55
|
+
interface TuiMouseEventResult {
|
|
56
|
+
handled?: boolean;
|
|
57
|
+
capture?: boolean;
|
|
58
|
+
focus?: boolean;
|
|
59
|
+
render?: boolean;
|
|
60
|
+
}
|
|
61
|
+
type MouseLayoutEntry = { component?: any; height: number; top: number };
|
|
44
62
|
|
|
45
63
|
import * as Diff from "diff";
|
|
46
64
|
import type { BundledLanguage, BundledTheme } from "shiki";
|
|
@@ -52,6 +70,7 @@ import {
|
|
|
52
70
|
formatBashDuration,
|
|
53
71
|
getLastBashOutputLine,
|
|
54
72
|
} from "./bash-command";
|
|
73
|
+
import { getUserSettingsPath } from "./settings-path";
|
|
55
74
|
|
|
56
75
|
const RESET = "\x1b[0m";
|
|
57
76
|
const TRANSPARENT_BG = "\x1b[49m";
|
|
@@ -71,6 +90,8 @@ const TOOL_RENDER_CACHE = Symbol.for("pi-claude-style-tools:tool-render-cache");
|
|
|
71
90
|
const COMPONENT_PARENT = Symbol.for("pi-claude-style-tools:component-parent");
|
|
72
91
|
const PARENT_TRACKING_PATCH_FLAG = Symbol.for("pi-claude-style-tools:patched-parent-tracking");
|
|
73
92
|
const TOOL_CACHE_PATCH_FLAG = Symbol.for("pi-claude-style-tools:patched-tool-cache-invalidation");
|
|
93
|
+
const TOOL_MOUSE_PATCH_FLAG = Symbol.for("pi-claude-style-tools:patched-tool-fullscreen-mouse");
|
|
94
|
+
const FULLSCREEN_HOVER_PATCH_FLAG = Symbol.for("pi-claude-style-tools:patched-fullscreen-hover");
|
|
74
95
|
const TOOL_IMAGE_EXPAND_PATCH_FLAG = Symbol.for("pi-claude-style-tools:patched-read-image-expansion");
|
|
75
96
|
const CUSTOM_MESSAGE_PATCH_FLAG = Symbol.for("pi-claude-style-tools:patched-custom-message-render");
|
|
76
97
|
const USER_MESSAGE_PATCH_FLAG = Symbol.for("pi-claude-style-tools:patched-user-message-render");
|
|
@@ -147,7 +168,7 @@ function readSettings(): SettingsFile {
|
|
|
147
168
|
return _settingsCache.value;
|
|
148
169
|
}
|
|
149
170
|
const cwdPath = `${process.cwd()}/.pi/settings.json`;
|
|
150
|
-
const homePath =
|
|
171
|
+
const homePath = getUserSettingsPath();
|
|
151
172
|
const merged: SettingsFile = {};
|
|
152
173
|
for (const path of [cwdPath, homePath]) {
|
|
153
174
|
try {
|
|
@@ -175,9 +196,9 @@ function bustSpinnerSettingsCache(): void {
|
|
|
175
196
|
function writeSettingsKey(key: string, value: unknown): void {
|
|
176
197
|
_settingsCache = null; // invalidate cache on write
|
|
177
198
|
const home = process.env.HOME ?? "";
|
|
178
|
-
if (!home) return;
|
|
179
|
-
const
|
|
180
|
-
const
|
|
199
|
+
if (!home && !process.env.PI_CODING_AGENT_DIR) return;
|
|
200
|
+
const path = getUserSettingsPath();
|
|
201
|
+
const dir = dirname(path);
|
|
181
202
|
let settings: Record<string, unknown> = {};
|
|
182
203
|
try {
|
|
183
204
|
if (existsSync(path)) settings = JSON.parse(readFileSync(path, "utf8")) ?? {};
|
|
@@ -506,8 +527,8 @@ function getThinkingMode(): ThinkingMode {
|
|
|
506
527
|
|
|
507
528
|
function isAssistantThinkingComplete(comp: any, message: any): boolean {
|
|
508
529
|
if (!message || message.role !== "assistant") return false;
|
|
509
|
-
if (
|
|
510
|
-
if (message
|
|
530
|
+
if (getThinkingDurationMetadata(message) !== undefined) return true;
|
|
531
|
+
if (hasThinkingActiveMetadata(message)) return false;
|
|
511
532
|
// Providers keep stopReason "pending" for the whole stream ("deferred" while
|
|
512
533
|
// a deferred call is unresolved); both are in-flight sentinels, never
|
|
513
534
|
// completion signals. Without this, live-thinking detection would depend on
|
|
@@ -534,7 +555,7 @@ function isAssistantThinkingComplete(comp: any, message: any): boolean {
|
|
|
534
555
|
function isLiveThinkingMessage(comp: any, message: any): boolean {
|
|
535
556
|
if (!message || message.role !== "assistant") return false;
|
|
536
557
|
if (isAssistantThinkingComplete(comp, message)) return false;
|
|
537
|
-
if ((message
|
|
558
|
+
if (hasThinkingActiveMetadata(message)) return true;
|
|
538
559
|
if (Array.isArray(message.content)) {
|
|
539
560
|
return message.content.some((b: any) => b?.type === "thinking" && typeof b?.thinking === "string" && b.thinking.trim());
|
|
540
561
|
}
|
|
@@ -880,7 +901,8 @@ function isGroupableTool(value: unknown): value is InstanceType<typeof ToolExecu
|
|
|
880
901
|
|
|
881
902
|
class ToolGroupComponent extends Container {
|
|
882
903
|
private tools: any[] = [];
|
|
883
|
-
|
|
904
|
+
expanded = false;
|
|
905
|
+
private mouseLayout?: { width: number; children: MouseLayoutEntry[] };
|
|
884
906
|
// Memoize full group output. Grouped history is the long-chat bottleneck:
|
|
885
907
|
// each warm frame used to re-render every child tool, re-branch lines, and
|
|
886
908
|
// re-clamp every row even when nothing changed.
|
|
@@ -933,6 +955,8 @@ class ToolGroupComponent extends Container {
|
|
|
933
955
|
addTool(tool: any): void {
|
|
934
956
|
ACTIVE_TOOL_GROUPS.add(this);
|
|
935
957
|
this.tools.push(tool);
|
|
958
|
+
setToolHover(tool, false);
|
|
959
|
+
if (hoveredClickTarget?.owner === tool) hoveredClickTarget = undefined;
|
|
936
960
|
tool[COMPONENT_PARENT] = this;
|
|
937
961
|
// Don't cascade invalidate into every child — only drop our own cache.
|
|
938
962
|
// Child tools already rebuild via their own updateDisplay path.
|
|
@@ -948,7 +972,7 @@ class ToolGroupComponent extends Container {
|
|
|
948
972
|
}
|
|
949
973
|
|
|
950
974
|
setExpanded(expanded: boolean): void {
|
|
951
|
-
if (this.expanded === expanded) return;
|
|
975
|
+
if (this.expanded === expanded && this.tools.every((tool) => tool.expanded === expanded)) return;
|
|
952
976
|
this.expanded = expanded;
|
|
953
977
|
for (const tool of this.tools) tool.setExpanded?.(expanded);
|
|
954
978
|
this.clearRenderCache();
|
|
@@ -962,8 +986,27 @@ class ToolGroupComponent extends Container {
|
|
|
962
986
|
this.clearRenderCache();
|
|
963
987
|
}
|
|
964
988
|
|
|
989
|
+
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
990
|
+
const target = mouseLayoutTarget(this.mouseLayout, event);
|
|
991
|
+
if (event.type === "move") {
|
|
992
|
+
const row = target?.top ?? -1;
|
|
993
|
+
claimClickHover(this, () => { if (setGroupHover(this, -1)) requestComponentRender(this); });
|
|
994
|
+
return setGroupHover(this, row) ? { handled: true, render: true } : undefined;
|
|
995
|
+
}
|
|
996
|
+
if (isPrimaryClick(event) && target) {
|
|
997
|
+
const targets = target.component
|
|
998
|
+
? Array.isArray(target.component) ? target.component : [target.component]
|
|
999
|
+
: this.tools;
|
|
1000
|
+
return toggleGroupedTools(this, targets);
|
|
1001
|
+
}
|
|
1002
|
+
return undefined;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
965
1005
|
render(width: number): string[] {
|
|
966
|
-
if (this.tools.length === 0)
|
|
1006
|
+
if (this.tools.length === 0) {
|
|
1007
|
+
rememberMouseLayout(this, width, []);
|
|
1008
|
+
return [];
|
|
1009
|
+
}
|
|
967
1010
|
const safeWidth = Number.isFinite(width) ? Math.max(1, Math.floor(width)) : 1;
|
|
968
1011
|
// Fast path: settled groups with a valid memo skip ALL child walks.
|
|
969
1012
|
// Child mutations mark dirty via clearToolRenderCache → invalidate().
|
|
@@ -975,7 +1018,7 @@ class ToolGroupComponent extends Container {
|
|
|
975
1018
|
&& this.cachedMode === toolBackgroundMode
|
|
976
1019
|
&& this.cachedExpanded === this.expanded
|
|
977
1020
|
) {
|
|
978
|
-
return this.cachedLines;
|
|
1021
|
+
return applyGroupHover(this.cachedLines, this.mouseLayout, (this as any)[GROUP_HOVER_KEY]);
|
|
979
1022
|
}
|
|
980
1023
|
|
|
981
1024
|
const status = this.statusSnapshot();
|
|
@@ -988,6 +1031,12 @@ class ToolGroupComponent extends Container {
|
|
|
988
1031
|
const label = getToolGroupLabel(this.tools);
|
|
989
1032
|
const names = groupedName ? "" : formatToolNameList(this.tools);
|
|
990
1033
|
const overall: ToolStatus = status.error > 0 ? "error" : status.pending > 0 ? "pending" : "success";
|
|
1034
|
+
const mouseChildren: MouseLayoutEntry[] = [];
|
|
1035
|
+
let mouseY = 0;
|
|
1036
|
+
const recordLines = (rowLines: string[], target?: any) => {
|
|
1037
|
+
mouseChildren.push({ component: target, height: rowLines.length, top: mouseY });
|
|
1038
|
+
mouseY += rowLines.length;
|
|
1039
|
+
};
|
|
991
1040
|
// Group header breathes only when every pending member is Agent-family;
|
|
992
1041
|
// mixed groups keep the ordinary on/off light.
|
|
993
1042
|
const pendingTools = this.tools.filter((tool) => getToolStatusForGroup(tool) === "pending");
|
|
@@ -1006,13 +1055,17 @@ class ToolGroupComponent extends Container {
|
|
|
1006
1055
|
if (collapseToSingleRow) {
|
|
1007
1056
|
const entry = { tools: this.tools, name: groupedName, subject };
|
|
1008
1057
|
const entryLine = getCollapsedToolEntryLine(entry, Math.max(1, safeWidth - 3));
|
|
1009
|
-
|
|
1058
|
+
const row = [clampLineWidth(
|
|
1010
1059
|
` ${light} ${entryLine}${toolOutputDetailHint(undefined as any, false, true)}`,
|
|
1011
1060
|
safeWidth,
|
|
1012
|
-
)
|
|
1061
|
+
)];
|
|
1062
|
+
recordLines(row, undefined);
|
|
1063
|
+
lines.push(...row);
|
|
1013
1064
|
} else {
|
|
1014
1065
|
const summary = ` ${light} ${summaryLabel} ${countsText}${names ? ` ${TRANSPARENT_RESET}• ${names}` : ""}${toolOutputDetailHint(undefined as any, this.expanded, true)}`;
|
|
1015
|
-
|
|
1066
|
+
const header = [" ".repeat(safeWidth), clampLineWidth(summary, safeWidth)];
|
|
1067
|
+
recordLines(header, undefined);
|
|
1068
|
+
lines.push(...header);
|
|
1016
1069
|
const childWidth = Math.max(1, safeWidth - 6);
|
|
1017
1070
|
if (this.expanded) {
|
|
1018
1071
|
for (let index = 0; index < total; index++) {
|
|
@@ -1025,7 +1078,9 @@ class ToolGroupComponent extends Container {
|
|
|
1025
1078
|
getToolStatusForGroup(tool),
|
|
1026
1079
|
{ agentBreathe: isAgentFamilyToolName(getToolName(tool)) },
|
|
1027
1080
|
);
|
|
1028
|
-
|
|
1081
|
+
const row = branched.map((line) => clampLineWidth(line, safeWidth));
|
|
1082
|
+
recordLines(row, tool);
|
|
1083
|
+
lines.push(...row);
|
|
1029
1084
|
}
|
|
1030
1085
|
} else {
|
|
1031
1086
|
const entries = collapseRepeatedToolEntries(this.tools);
|
|
@@ -1039,10 +1094,13 @@ class ToolGroupComponent extends Container {
|
|
|
1039
1094
|
getToolGroupOverallStatus(entry.tools),
|
|
1040
1095
|
{ agentBreathe: entry.tools.every((tool) => isAgentFamilyToolName(getToolName(tool))) },
|
|
1041
1096
|
);
|
|
1042
|
-
|
|
1097
|
+
const row = branched.map((line) => clampLineWidth(line, safeWidth));
|
|
1098
|
+
recordLines(row, entry.tools.length === 1 ? entry.tools[0] : entry.tools);
|
|
1099
|
+
lines.push(...row);
|
|
1043
1100
|
}
|
|
1044
1101
|
}
|
|
1045
1102
|
}
|
|
1103
|
+
rememberMouseLayout(this, safeWidth, mouseChildren);
|
|
1046
1104
|
|
|
1047
1105
|
// Final clamp already applied per-line above; avoid a second full pass.
|
|
1048
1106
|
if (canCache) {
|
|
@@ -1058,10 +1116,20 @@ class ToolGroupComponent extends Container {
|
|
|
1058
1116
|
} else {
|
|
1059
1117
|
this.clearRenderCache();
|
|
1060
1118
|
}
|
|
1061
|
-
return lines;
|
|
1119
|
+
return applyGroupHover(lines, this.mouseLayout, (this as any)[GROUP_HOVER_KEY]);
|
|
1062
1120
|
}
|
|
1063
1121
|
}
|
|
1064
1122
|
|
|
1123
|
+
function applyGroupHover(lines: string[], layout: { children: MouseLayoutEntry[] } | undefined, hoverStart: unknown): string[] {
|
|
1124
|
+
if (typeof hoverStart !== "number" || hoverStart < 0 || !layout) return lines;
|
|
1125
|
+
const entry = layout.children.find((candidate) => candidate.top === hoverStart);
|
|
1126
|
+
if (!entry) return lines;
|
|
1127
|
+
const rendered = lines.slice();
|
|
1128
|
+
const line = entry.top + (entry.component ? 0 : Math.max(0, entry.height - 1));
|
|
1129
|
+
if (line < rendered.length) rendered[line] = brightenRenderedLine(rendered[line]);
|
|
1130
|
+
return rendered;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1065
1133
|
function isToolGroupComponent(value: unknown): value is ToolGroupComponent {
|
|
1066
1134
|
return value instanceof ToolGroupComponent;
|
|
1067
1135
|
}
|
|
@@ -1132,7 +1200,7 @@ function isThinkingOnlyAssistantComponent(comp: unknown): comp is InstanceType<t
|
|
|
1132
1200
|
if (hasText) return false;
|
|
1133
1201
|
const hasToolCalls = msg.content.some((c: any) => c?.type === "toolCall");
|
|
1134
1202
|
if (hasToolCalls) return false;
|
|
1135
|
-
if ((comp as any).isStreaming === true || msg
|
|
1203
|
+
if ((comp as any).isStreaming === true || hasThinkingActiveMetadata(msg)) return false;
|
|
1136
1204
|
return true;
|
|
1137
1205
|
}
|
|
1138
1206
|
|
|
@@ -1160,10 +1228,13 @@ function maybeMergeConsecutiveThinkingMessages(parent: any): void {
|
|
|
1160
1228
|
const mergedDuration = durA + durB;
|
|
1161
1229
|
|
|
1162
1230
|
const nextThinkingBlocks = nextMsg.content.filter((c: any) => c?.type === "thinking");
|
|
1163
|
-
|
|
1164
|
-
|
|
1231
|
+
const mergedMessage = {
|
|
1232
|
+
...curMsg,
|
|
1233
|
+
content: [...curMsg.content, ...nextThinkingBlocks],
|
|
1234
|
+
[THINKING_DURATION_KEY]: mergedDuration,
|
|
1235
|
+
};
|
|
1165
1236
|
|
|
1166
|
-
(current as any).updateContent(
|
|
1237
|
+
(current as any).updateContent(mergedMessage);
|
|
1167
1238
|
|
|
1168
1239
|
const removeCount = nextIdx - i;
|
|
1169
1240
|
children.splice(i + 1, removeCount);
|
|
@@ -1189,7 +1260,7 @@ function maybeGroupToolComponent(parent: any, component: any): void {
|
|
|
1189
1260
|
}
|
|
1190
1261
|
if (isGroupableTool(previous)) {
|
|
1191
1262
|
const group = new ToolGroupComponent();
|
|
1192
|
-
group.
|
|
1263
|
+
group.expanded = (previous as any).expanded === true || (component as any).expanded === true;
|
|
1193
1264
|
group.addTool(previous);
|
|
1194
1265
|
group.addTool(component);
|
|
1195
1266
|
(group as any)[COMPONENT_PARENT] = parent;
|
|
@@ -1265,17 +1336,21 @@ function patchGlobalToolBorders(): void {
|
|
|
1265
1336
|
&& cached?.branchKey === branchKey
|
|
1266
1337
|
&& cached?.branchEpoch === _toolBranchVisualEpoch
|
|
1267
1338
|
) {
|
|
1339
|
+
// A cache hit skips Container.render, which is also where fullscreen
|
|
1340
|
+
// mouse targets are recorded. Restore the layout from that render.
|
|
1341
|
+
if (cached.mouseLayout) (this as { mouseLayout?: unknown }).mouseLayout = cached.mouseLayout;
|
|
1268
1342
|
return cached.lines;
|
|
1269
1343
|
}
|
|
1270
1344
|
}
|
|
1271
1345
|
|
|
1272
1346
|
const rendered = originalRender.call(this, width);
|
|
1347
|
+
const mouseLayout = (this as { mouseLayout?: unknown }).mouseLayout;
|
|
1273
1348
|
if (!Array.isArray(rendered) || rendered.length === 0) return rendered;
|
|
1274
1349
|
const todoOverlay = formatTodoOverlayLines(rendered, width);
|
|
1275
1350
|
if (!isToolExecutionLike(this)) return todoOverlay;
|
|
1276
1351
|
const branchCache = { branchKey: toolBranchRenderCacheKey(), branchEpoch: _toolBranchVisualEpoch };
|
|
1277
1352
|
if (toolBackgroundMode === "default") {
|
|
1278
|
-
(this as any)[TOOL_RENDER_CACHE] = { width, mode: toolBackgroundMode, lines: rendered, ...branchCache };
|
|
1353
|
+
(this as any)[TOOL_RENDER_CACHE] = { width, mode: toolBackgroundMode, lines: rendered, mouseLayout, ...branchCache };
|
|
1279
1354
|
return rendered;
|
|
1280
1355
|
}
|
|
1281
1356
|
|
|
@@ -1287,7 +1362,7 @@ function patchGlobalToolBorders(): void {
|
|
|
1287
1362
|
|
|
1288
1363
|
const { textLines, imageLines } = splitRenderedImageBlock(rendered.slice(start, end + 1));
|
|
1289
1364
|
if (imageLines.length > 0) {
|
|
1290
|
-
(this as any)[TOOL_RENDER_CACHE] = { width, mode: toolBackgroundMode, lines: rendered, ...branchCache };
|
|
1365
|
+
(this as any)[TOOL_RENDER_CACHE] = { width, mode: toolBackgroundMode, lines: rendered, mouseLayout, ...branchCache };
|
|
1291
1366
|
return rendered;
|
|
1292
1367
|
}
|
|
1293
1368
|
// Agent-family tools stay column-aligned with every other tool row — no extra
|
|
@@ -1307,7 +1382,7 @@ function patchGlobalToolBorders(): void {
|
|
|
1307
1382
|
result = [spacerLine, ...core, ...imageLines];
|
|
1308
1383
|
}
|
|
1309
1384
|
|
|
1310
|
-
(this as any)[TOOL_RENDER_CACHE] = { width, mode: toolBackgroundMode, lines: result, ...branchCache };
|
|
1385
|
+
(this as any)[TOOL_RENDER_CACHE] = { width, mode: toolBackgroundMode, lines: result, mouseLayout, ...branchCache };
|
|
1311
1386
|
return result;
|
|
1312
1387
|
};
|
|
1313
1388
|
|
|
@@ -1379,6 +1454,96 @@ function clearToolRenderCache(value: unknown): void {
|
|
|
1379
1454
|
if (isToolGroupComponent(parent)) parent.invalidate();
|
|
1380
1455
|
}
|
|
1381
1456
|
|
|
1457
|
+
/** Fullscreen hit-testing reads the layout recorded by the last render. */
|
|
1458
|
+
function rememberMouseLayout(component: object, width: number, children: MouseLayoutEntry[]): void {
|
|
1459
|
+
(component as { mouseLayout?: { width: number; children: MouseLayoutEntry[] } }).mouseLayout = { width, children };
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
function isPrimaryClick(event: TuiMouseEvent): boolean {
|
|
1463
|
+
return event.type === "click" && event.button === "left";
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
function toggleExpandedComponent(component: any): boolean {
|
|
1467
|
+
if (!component || typeof component.setExpanded !== "function") return false;
|
|
1468
|
+
component.setExpanded(component.expanded !== true);
|
|
1469
|
+
return true;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
function requestComponentRender(component: any): void {
|
|
1473
|
+
const ui = component?.ui;
|
|
1474
|
+
if (typeof ui?.requestRender === "function") ui.requestRender();
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
const GROUP_HOVER_KEY = Symbol.for("pi-claude-style-tools:group-hover");
|
|
1478
|
+
|
|
1479
|
+
function setGroupHover(group: any, rowStart: number): boolean {
|
|
1480
|
+
if (group[GROUP_HOVER_KEY] === rowStart) return false;
|
|
1481
|
+
group[GROUP_HOVER_KEY] = rowStart;
|
|
1482
|
+
return true;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
function mouseLayoutTarget(
|
|
1486
|
+
layout: { width: number; children: MouseLayoutEntry[] } | undefined,
|
|
1487
|
+
event: TuiMouseEvent,
|
|
1488
|
+
): MouseLayoutEntry | undefined {
|
|
1489
|
+
if (!layout || layout.width !== event.width || event.y < 0 || event.y >= event.height) return undefined;
|
|
1490
|
+
return layout.children.find((entry) => event.y >= entry.top && event.y < entry.top + entry.height);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function toggleGroupedTools(group: any, tools: any[]): { handled: true } | undefined {
|
|
1494
|
+
const targets = tools.filter((tool) => typeof tool?.setExpanded === "function");
|
|
1495
|
+
if (targets.length === 0) return undefined;
|
|
1496
|
+
const expanded = targets.some((tool) => tool.expanded !== true);
|
|
1497
|
+
for (const tool of targets) tool.setExpanded(expanded);
|
|
1498
|
+
if (Array.isArray(group?.tools)) group.expanded = group.tools.some((tool: any) => tool?.expanded === true);
|
|
1499
|
+
group.invalidate?.();
|
|
1500
|
+
const ui = targets.find((tool) => typeof tool?.ui?.requestRender === "function")?.ui;
|
|
1501
|
+
ui?.requestRender?.();
|
|
1502
|
+
return { handled: true };
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
let hoveredClickTarget: { owner: object; clear: () => void } | undefined;
|
|
1506
|
+
let hoverClaimedThisMove = false;
|
|
1507
|
+
|
|
1508
|
+
function claimClickHover(owner: object, clear: () => void): void {
|
|
1509
|
+
hoverClaimedThisMove = true;
|
|
1510
|
+
if (hoveredClickTarget?.owner === owner) return;
|
|
1511
|
+
const previous = hoveredClickTarget;
|
|
1512
|
+
hoveredClickTarget = { owner, clear };
|
|
1513
|
+
previous?.clear();
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
function patchFullscreenHoverExit(): void {
|
|
1517
|
+
const proto = (PiTui as any).TuiAltScreen?.prototype;
|
|
1518
|
+
if (!proto || proto[FULLSCREEN_HOVER_PATCH_FLAG] || typeof proto.handleMouseEvent !== "function") return;
|
|
1519
|
+
const originalHandleMouse = proto.handleMouseEvent;
|
|
1520
|
+
proto.handleMouseEvent = function patchedFullscreenMouseEvent(raw: { button: number }) {
|
|
1521
|
+
const isMove = (raw.button & 35) === 35;
|
|
1522
|
+
if (isMove) hoverClaimedThisMove = false;
|
|
1523
|
+
const result = originalHandleMouse.call(this, raw);
|
|
1524
|
+
if (isMove && !hoverClaimedThisMove && hoveredClickTarget) {
|
|
1525
|
+
const previous = hoveredClickTarget;
|
|
1526
|
+
hoveredClickTarget = undefined;
|
|
1527
|
+
previous.clear();
|
|
1528
|
+
this.requestRender();
|
|
1529
|
+
}
|
|
1530
|
+
return result;
|
|
1531
|
+
};
|
|
1532
|
+
if (typeof proto.handleViewportInput === "function") {
|
|
1533
|
+
const originalInput = proto.handleViewportInput;
|
|
1534
|
+
proto.handleViewportInput = function patchedFullscreenInput(data: string) {
|
|
1535
|
+
if (data === "\x1b[O" && hoveredClickTarget) {
|
|
1536
|
+
const previous = hoveredClickTarget;
|
|
1537
|
+
hoveredClickTarget = undefined;
|
|
1538
|
+
previous.clear();
|
|
1539
|
+
this.requestRender();
|
|
1540
|
+
}
|
|
1541
|
+
return originalInput.call(this, data);
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
proto[FULLSCREEN_HOVER_PATCH_FLAG] = true;
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1382
1547
|
function unrefTimer(timer: ReturnType<typeof setTimeout> | null | undefined): void {
|
|
1383
1548
|
(timer as any)?.unref?.();
|
|
1384
1549
|
}
|
|
@@ -1393,6 +1558,7 @@ function safeInvalidate(ctx: any): void {
|
|
|
1393
1558
|
|
|
1394
1559
|
const ASSISTANT_PATCH_FLAG = Symbol.for("pi-claude-style-tools:patched-assistant-message");
|
|
1395
1560
|
const ASSISTANT_RENDER_PATCH_FLAG = Symbol.for("pi-claude-style-tools:patched-assistant-message-render");
|
|
1561
|
+
const ASSISTANT_MOUSE_PATCH_FLAG = Symbol.for("pi-claude-style-tools:patched-assistant-fullscreen-mouse");
|
|
1396
1562
|
const TOOL_EXECUTION_PATCH_FLAG = Symbol.for("pi-claude-style-tools:patched-tool-execution");
|
|
1397
1563
|
|
|
1398
1564
|
// Rendered-output cache for assistant/user/custom message components.
|
|
@@ -1450,9 +1616,48 @@ const WORKED_TURNS_KEY = "_piClaudeStyleWorkedTurns";
|
|
|
1450
1616
|
const WORKED_DURATION_MARKER = "Turn took";
|
|
1451
1617
|
const THINKING_DURATION_KEY = "_piClaudeStyleThinkingDurationMs";
|
|
1452
1618
|
const THINKING_ACTIVE_KEY = "_piClaudeStyleThinkingActive";
|
|
1453
|
-
const
|
|
1619
|
+
const MAX_REASONABLE_THINKING_DURATION_MS = 24 * 60 * 60 * 1000;
|
|
1620
|
+
|
|
1621
|
+
// Presentation metadata belongs to the renderer, not the canonical session
|
|
1622
|
+
// messages. Weak collections keep live state available to the TUI without
|
|
1623
|
+
// changing the objects Pi persists or projects for compaction replay.
|
|
1624
|
+
const thinkingDurationByMessage = new WeakMap<object, number>();
|
|
1625
|
+
const thinkingActiveMessages = new WeakSet<object>();
|
|
1626
|
+
const workedStartByMessage = new WeakMap<object, number>();
|
|
1627
|
+
const workedDurationByMessage = new WeakMap<object, { duration: number; sessionTotal?: number; turns?: number }>();
|
|
1628
|
+
|
|
1629
|
+
function getThinkingDurationMetadata(message: any): number | undefined {
|
|
1630
|
+
if (!message || typeof message !== "object") return undefined;
|
|
1631
|
+
const stored = thinkingDurationByMessage.get(message) ?? message[THINKING_DURATION_KEY];
|
|
1632
|
+
return typeof stored === "number"
|
|
1633
|
+
&& Number.isFinite(stored)
|
|
1634
|
+
&& stored > 0
|
|
1635
|
+
&& stored <= MAX_REASONABLE_THINKING_DURATION_MS
|
|
1636
|
+
? stored
|
|
1637
|
+
: undefined;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
function hasThinkingActiveMetadata(message: any): boolean {
|
|
1641
|
+
return !!message
|
|
1642
|
+
&& typeof message === "object"
|
|
1643
|
+
&& (thinkingActiveMessages.has(message) || message[THINKING_ACTIVE_KEY] === true);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
function workedMetadataForMessage(message: any): { duration: number; sessionTotal?: number; turns?: number } | undefined {
|
|
1647
|
+
if (!message || typeof message !== "object") return undefined;
|
|
1648
|
+
const metadata = workedDurationByMessage.get(message);
|
|
1649
|
+
if (metadata) return metadata;
|
|
1650
|
+
const duration = message[WORKED_DURATION_KEY];
|
|
1651
|
+
if (typeof duration !== "number" || !Number.isFinite(duration) || duration < 0) return undefined;
|
|
1652
|
+
const sessionTotal = message[WORKED_SESSION_TOTAL_KEY];
|
|
1653
|
+
const turns = message[WORKED_TURNS_KEY];
|
|
1654
|
+
return {
|
|
1655
|
+
duration,
|
|
1656
|
+
sessionTotal: typeof sessionTotal === "number" ? sessionTotal : undefined,
|
|
1657
|
+
turns: typeof turns === "number" ? turns : undefined,
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1454
1660
|
|
|
1455
|
-
let lastThinkingBlockDurationMs: number | undefined;
|
|
1456
1661
|
let thinkingBlockStartMs = 0;
|
|
1457
1662
|
/** True from thinking_start until thinking_end on the current assistant stream. */
|
|
1458
1663
|
let thinkingBlockInFlight = false;
|
|
@@ -1518,9 +1723,25 @@ function pluralizeTurns(n: number): string {
|
|
|
1518
1723
|
return `${n} turn${n === 1 ? "" : "s"}`;
|
|
1519
1724
|
}
|
|
1520
1725
|
|
|
1521
|
-
function thinkingSummaryStyledText(body: string): string {
|
|
1726
|
+
function thinkingSummaryStyledText(body: string, hovered = false): string {
|
|
1522
1727
|
// Preserve the visible thinking text column while omitting ∴ when collapsed.
|
|
1523
|
-
|
|
1728
|
+
const color = hovered ? brightenAnsi(WORKED_LINE_FG) : WORKED_LINE_FG;
|
|
1729
|
+
return ` ${color}${body}${RESET}`;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
/** Brighten an existing color without changing its hue. */
|
|
1733
|
+
function brightenAnsi(baseAnsi: string): string {
|
|
1734
|
+
const rgb = parseAnsiRgb(baseAnsi);
|
|
1735
|
+
if (!rgb) return "\x1b[97m";
|
|
1736
|
+
const maximum = Math.max(rgb.r, rgb.g, rgb.b);
|
|
1737
|
+
if (maximum === 0) return baseAnsi;
|
|
1738
|
+
const scale = Math.min(1.12, 255 / maximum);
|
|
1739
|
+
const lift = (channel: number) => Math.round(channel * scale);
|
|
1740
|
+
return `\x1b[38;2;${lift(rgb.r)};${lift(rgb.g)};${lift(rgb.b)}m`;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
function brightenRenderedLine(line: string): string {
|
|
1744
|
+
return line.replace(/\x1b\[38;2;(\d+);(\d+);(\d+)m/g, (_match, r, g, b) => brightenAnsi(`\x1b[38;2;${r};${g};${b}m`));
|
|
1524
1745
|
}
|
|
1525
1746
|
|
|
1526
1747
|
function thinkingActiveSummaryText(): string {
|
|
@@ -1533,48 +1754,55 @@ function thoughtDurationSummaryText(ms: number): string {
|
|
|
1533
1754
|
|
|
1534
1755
|
/** Single-line hidden thinking row — no Text paddingX or thinking symbol. */
|
|
1535
1756
|
class HiddenThinkingSummary {
|
|
1536
|
-
private
|
|
1757
|
+
private body: string;
|
|
1758
|
+
private hovered = false;
|
|
1537
1759
|
private cachedWidth?: number;
|
|
1760
|
+
private cachedHovered?: boolean;
|
|
1538
1761
|
private cachedLines?: string[];
|
|
1539
1762
|
|
|
1540
|
-
constructor(
|
|
1541
|
-
this.
|
|
1763
|
+
constructor(body: string) {
|
|
1764
|
+
this.body = stripAnsi(body).trim();
|
|
1542
1765
|
}
|
|
1543
1766
|
|
|
1544
1767
|
setSummary(summaryText: string): void {
|
|
1545
|
-
|
|
1768
|
+
const body = stripAnsi(summaryText).trim();
|
|
1769
|
+
if (this.body === body) return;
|
|
1770
|
+
this.body = body;
|
|
1771
|
+
this.invalidate();
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
setHovered(hovered: boolean): void {
|
|
1775
|
+
if (this.hovered === hovered) return;
|
|
1776
|
+
this.hovered = hovered;
|
|
1546
1777
|
this.invalidate();
|
|
1547
1778
|
}
|
|
1548
1779
|
|
|
1549
1780
|
invalidate(): void {
|
|
1550
1781
|
this.cachedWidth = undefined;
|
|
1782
|
+
this.cachedHovered = undefined;
|
|
1551
1783
|
this.cachedLines = undefined;
|
|
1552
1784
|
}
|
|
1553
1785
|
|
|
1554
1786
|
render(width: number): string[] {
|
|
1555
|
-
if (this.cachedLines && this.cachedWidth === width) return this.cachedLines;
|
|
1787
|
+
if (this.cachedLines && this.cachedWidth === width && this.cachedHovered === this.hovered) return this.cachedLines;
|
|
1556
1788
|
const safeWidth = Number.isFinite(width) ? Math.max(0, Math.floor(width)) : 0;
|
|
1557
1789
|
if (safeWidth <= 0) {
|
|
1558
1790
|
this.cachedWidth = width;
|
|
1791
|
+
this.cachedHovered = this.hovered;
|
|
1559
1792
|
this.cachedLines = [""];
|
|
1560
1793
|
return this.cachedLines;
|
|
1561
1794
|
}
|
|
1562
|
-
const line = padRenderedLineToWidth(this.
|
|
1795
|
+
const line = padRenderedLineToWidth(thinkingSummaryStyledText(this.body, this.hovered), safeWidth);
|
|
1563
1796
|
this.cachedWidth = width;
|
|
1797
|
+
this.cachedHovered = this.hovered;
|
|
1564
1798
|
this.cachedLines = [line];
|
|
1565
1799
|
return this.cachedLines;
|
|
1566
1800
|
}
|
|
1567
1801
|
}
|
|
1568
1802
|
|
|
1569
1803
|
function getMessageThinkingDurationMs(message: any): number {
|
|
1570
|
-
const stored = (message
|
|
1571
|
-
if (
|
|
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
|
-
}
|
|
1804
|
+
const stored = getThinkingDurationMetadata(message);
|
|
1805
|
+
if (stored !== undefined) return stored;
|
|
1578
1806
|
let totalChars = 0;
|
|
1579
1807
|
if (Array.isArray(message?.content)) {
|
|
1580
1808
|
for (const block of message.content) {
|
|
@@ -1595,9 +1823,6 @@ function hiddenThinkingSummaryForMessage(message: any, comp?: any): string {
|
|
|
1595
1823
|
return thinkingActiveSummaryText();
|
|
1596
1824
|
}
|
|
1597
1825
|
const durationMs = getMessageThinkingDurationMs(message);
|
|
1598
|
-
if (message && typeof message === "object") {
|
|
1599
|
-
(message as any)[THINKING_DURATION_KEY] = durationMs;
|
|
1600
|
-
}
|
|
1601
1826
|
return thoughtDurationSummaryText(durationMs);
|
|
1602
1827
|
}
|
|
1603
1828
|
|
|
@@ -1778,13 +2003,20 @@ function patchTerminalWriteTagScrubber(): void {
|
|
|
1778
2003
|
|
|
1779
2004
|
function replaceInlineMath(text: string): string {
|
|
1780
2005
|
if (!hasInlineMathMarkers(text)) return text;
|
|
1781
|
-
const
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
2006
|
+
const transform = (segment: string): string => {
|
|
2007
|
+
const withParens = segment.replace(/\\\(([\s\S]*?)\\\)/g, (_match, body: string) => {
|
|
2008
|
+
return codeSpan(formatMathForDisplay(body, false));
|
|
2009
|
+
});
|
|
2010
|
+
return withParens.replace(/(^|[^\\])\$([^\n$]{1,200})\$/g, (match, prefix: string, body: string) => {
|
|
2011
|
+
if (!looksLikeInlineMath(body)) return match;
|
|
2012
|
+
return `${prefix}${codeSpan(formatMathForDisplay(body, false))}`;
|
|
2013
|
+
});
|
|
2014
|
+
};
|
|
2015
|
+
// Markdown code spans and fenced blocks are literal source. Apply math
|
|
2016
|
+
// formatting only to prose segments so shell variables such as `$base` and
|
|
2017
|
+
// `$scope` remain byte-for-byte unchanged in displayed/copied commands.
|
|
2018
|
+
const parts = text.split(/(`{3,}[\s\S]*?`{3,}|`[^`\n]*`)/g);
|
|
2019
|
+
return parts.map((part, index) => index % 2 === 1 ? part : transform(part)).join("");
|
|
1788
2020
|
}
|
|
1789
2021
|
|
|
1790
2022
|
interface MathBlock {
|
|
@@ -1873,16 +2105,30 @@ function shouldFormatStandaloneMath(text: string): boolean {
|
|
|
1873
2105
|
return !/[.!?]\s/.test(plain) && plain.length <= 240;
|
|
1874
2106
|
}
|
|
1875
2107
|
|
|
1876
|
-
|
|
2108
|
+
// Options Pi hands to the Markdown components it builds, including the markdown
|
|
2109
|
+
// transformers (Pi's own Mermaid renderer, extension transformers, ...). Paragraph
|
|
2110
|
+
// wrappers rebuild those components, so they have to forward the options.
|
|
2111
|
+
type MarkdownOptions = ConstructorParameters<typeof Markdown>[5]
|
|
2112
|
+
|
|
2113
|
+
function appendMarkdownSegment(
|
|
2114
|
+
segments: ParagraphSegment[],
|
|
2115
|
+
text: string,
|
|
2116
|
+
theme: MarkdownThemeLike,
|
|
2117
|
+
options?: MarkdownOptions,
|
|
2118
|
+
): void {
|
|
1877
2119
|
if (!text.trim()) return;
|
|
1878
2120
|
const normalized = shouldFormatStandaloneMath(text) ? formatMathForDisplay(text, false) : replaceInlineMath(text);
|
|
1879
|
-
segments.push({ kind: "markdown", md: new Markdown(normalized, 0, 0, theme) });
|
|
2121
|
+
segments.push({ kind: "markdown", md: new Markdown(normalized, 0, 0, theme, undefined, options) });
|
|
1880
2122
|
}
|
|
1881
2123
|
|
|
1882
|
-
function buildParagraphSegments(
|
|
2124
|
+
function buildParagraphSegments(
|
|
2125
|
+
text: string,
|
|
2126
|
+
theme: MarkdownThemeLike,
|
|
2127
|
+
options?: MarkdownOptions,
|
|
2128
|
+
): ParagraphSegment[] {
|
|
1883
2129
|
const segments: ParagraphSegment[] = [];
|
|
1884
2130
|
if (!hasDisplayMathMarkers(text)) {
|
|
1885
|
-
appendMarkdownSegment(segments, text, theme);
|
|
2131
|
+
appendMarkdownSegment(segments, text, theme, options);
|
|
1886
2132
|
return segments;
|
|
1887
2133
|
}
|
|
1888
2134
|
const scanLoose = shouldScanLooseBracketMath(text);
|
|
@@ -1890,12 +2136,12 @@ function buildParagraphSegments(text: string, theme: MarkdownThemeLike): Paragra
|
|
|
1890
2136
|
while (cursor < text.length) {
|
|
1891
2137
|
const next = findNextDisplayMathBlock(text, cursor, scanLoose);
|
|
1892
2138
|
if (!next) break;
|
|
1893
|
-
appendMarkdownSegment(segments, text.slice(cursor, next.index), theme);
|
|
2139
|
+
appendMarkdownSegment(segments, text.slice(cursor, next.index), theme, options);
|
|
1894
2140
|
const raw = text.slice(next.contentStart, next.contentEnd).trim();
|
|
1895
2141
|
if (raw) segments.push({ kind: "math", raw });
|
|
1896
2142
|
cursor = next.endIndex;
|
|
1897
2143
|
}
|
|
1898
|
-
appendMarkdownSegment(segments, text.slice(cursor), theme);
|
|
2144
|
+
appendMarkdownSegment(segments, text.slice(cursor), theme, options);
|
|
1899
2145
|
return segments;
|
|
1900
2146
|
}
|
|
1901
2147
|
|
|
@@ -1982,9 +2228,9 @@ class DottedParagraph {
|
|
|
1982
2228
|
private cachedWidth?: number;
|
|
1983
2229
|
private cachedLines?: string[];
|
|
1984
2230
|
|
|
1985
|
-
constructor(text: string, markdownTheme: MarkdownThemeLike) {
|
|
2231
|
+
constructor(text: string, markdownTheme: MarkdownThemeLike, options?: MarkdownOptions) {
|
|
1986
2232
|
this.markdownTheme = copySafeMarkdownTheme(markdownTheme);
|
|
1987
|
-
this.segments = buildParagraphSegments(stripTransientMagicContextTags(text), this.markdownTheme);
|
|
2233
|
+
this.segments = buildParagraphSegments(stripTransientMagicContextTags(text), this.markdownTheme, options);
|
|
1988
2234
|
}
|
|
1989
2235
|
|
|
1990
2236
|
invalidate(): void {
|
|
@@ -2073,16 +2319,29 @@ function replaceHiddenThinkingPlaceholders(container: { children?: any[] }, mess
|
|
|
2073
2319
|
|
|
2074
2320
|
class ThinkingParagraph {
|
|
2075
2321
|
private text: string;
|
|
2322
|
+
private hovered = false;
|
|
2076
2323
|
private cachedWidth?: number;
|
|
2077
2324
|
private cachedLines?: string[];
|
|
2325
|
+
private cachedHovered = false;
|
|
2078
2326
|
private chromeEpoch = -1;
|
|
2327
|
+
private options?: MarkdownOptions;
|
|
2328
|
+
|
|
2329
|
+
setHovered(hovered: boolean): void {
|
|
2330
|
+
if (this.hovered === hovered) return;
|
|
2331
|
+
this.hovered = hovered;
|
|
2332
|
+
this.invalidate();
|
|
2333
|
+
}
|
|
2079
2334
|
|
|
2080
2335
|
constructor(
|
|
2081
2336
|
text: string,
|
|
2082
2337
|
_markdownTheme: ConstructorParameters<typeof Markdown>[3],
|
|
2083
2338
|
_defaultTextStyle?: ConstructorParameters<typeof Markdown>[4],
|
|
2339
|
+
options?: MarkdownOptions,
|
|
2084
2340
|
) {
|
|
2085
|
-
|
|
2341
|
+
// Prefix only the renderer's private copy. The assistant message itself is
|
|
2342
|
+
// canonical session data and must remain untouched for compaction replay.
|
|
2343
|
+
this.text = prefixThinkingLine(stripTransientMagicContextTags(text), undefined);
|
|
2344
|
+
this.options = options;
|
|
2086
2345
|
}
|
|
2087
2346
|
|
|
2088
2347
|
private thinkingMarkdown(): InstanceType<typeof Markdown> {
|
|
@@ -2110,7 +2369,7 @@ class ThinkingParagraph {
|
|
|
2110
2369
|
italic: false,
|
|
2111
2370
|
color: (s: string) => `${DIM_FG}${s}`,
|
|
2112
2371
|
};
|
|
2113
|
-
return new Markdown(this.text, 0, 0, plainTheme, plainStyle);
|
|
2372
|
+
return new Markdown(this.text, 0, 0, plainTheme, plainStyle, this.options);
|
|
2114
2373
|
}
|
|
2115
2374
|
|
|
2116
2375
|
invalidate(): void {
|
|
@@ -2123,6 +2382,7 @@ class ThinkingParagraph {
|
|
|
2123
2382
|
if (
|
|
2124
2383
|
this.cachedLines
|
|
2125
2384
|
&& this.cachedWidth === width
|
|
2385
|
+
&& this.cachedHovered === this.hovered
|
|
2126
2386
|
&& this.chromeEpoch === _toolBranchVisualEpoch
|
|
2127
2387
|
) {
|
|
2128
2388
|
return this.cachedLines;
|
|
@@ -2144,8 +2404,9 @@ class ThinkingParagraph {
|
|
|
2144
2404
|
return this.cachedLines;
|
|
2145
2405
|
}
|
|
2146
2406
|
const lines = sanitizeRenderedTextBlockLines(md.render(safeWidth - PREFIX_W), safeWidth - PREFIX_W);
|
|
2407
|
+
const displayLines = this.hovered ? lines.map((line) => brightenRenderedLine(line)) : lines;
|
|
2147
2408
|
let symbolPlaced = false;
|
|
2148
|
-
const rendered =
|
|
2409
|
+
const rendered = displayLines.map((line: string) => {
|
|
2149
2410
|
if (!symbolPlaced && stripAnsi(line).trim()) {
|
|
2150
2411
|
symbolPlaced = true;
|
|
2151
2412
|
return ` ${prefix} ${line}`;
|
|
@@ -2153,6 +2414,7 @@ class ThinkingParagraph {
|
|
|
2153
2414
|
return ` ${line}`;
|
|
2154
2415
|
}).map((line) => clampLineWidth(line, safeWidth));
|
|
2155
2416
|
this.cachedWidth = width;
|
|
2417
|
+
this.cachedHovered = this.hovered;
|
|
2156
2418
|
this.cachedLines = rendered;
|
|
2157
2419
|
this.chromeEpoch = _toolBranchVisualEpoch;
|
|
2158
2420
|
return rendered;
|
|
@@ -2444,8 +2706,10 @@ function patchAssistantMessages(): void {
|
|
|
2444
2706
|
// - "full" mode: behaves like stock pi (stays collapsed).
|
|
2445
2707
|
const liveMode = getThinkingMode() === "live";
|
|
2446
2708
|
const thinkingCollapsed = !!(this as any).hideThinkingBlock;
|
|
2447
|
-
const
|
|
2448
|
-
|
|
2709
|
+
const override = (this as any).thinkingVisibilityOverrides?.get?.(0);
|
|
2710
|
+
const showLiveThinking = liveMode && thinkingCollapsed && override !== true && isLiveThinkingMessage(this, message);
|
|
2711
|
+
const hideThinking = override ?? (thinkingCollapsed && !showLiveThinking);
|
|
2712
|
+
if (hideThinking && messageHasThinkingContent(message)) {
|
|
2449
2713
|
// Pi wraps this in theme.italic/fg again — keep plain label for the placeholder pass.
|
|
2450
2714
|
(this as any).hiddenThinkingLabel = "Thinking…";
|
|
2451
2715
|
}
|
|
@@ -2459,7 +2723,7 @@ function patchAssistantMessages(): void {
|
|
|
2459
2723
|
// Replace text-block Markdown children with DottedParagraph wrappers
|
|
2460
2724
|
const container = (this as any).contentContainer;
|
|
2461
2725
|
if (!container?.children) return;
|
|
2462
|
-
if (
|
|
2726
|
+
if (hideThinking && messageHasThinkingContent(message)) {
|
|
2463
2727
|
replaceHiddenThinkingPlaceholders(container, message);
|
|
2464
2728
|
}
|
|
2465
2729
|
const mdTheme = (this as any).markdownTheme;
|
|
@@ -2469,17 +2733,18 @@ function patchAssistantMessages(): void {
|
|
|
2469
2733
|
if (isMarkdownComponent(inner)) {
|
|
2470
2734
|
const text = (inner as any).text;
|
|
2471
2735
|
if (!text) continue;
|
|
2736
|
+
const options = (inner as any).options as MarkdownOptions;
|
|
2472
2737
|
const isThinking = !!(inner as any).defaultTextStyle?.italic;
|
|
2473
2738
|
if (isThinking) {
|
|
2474
2739
|
const style = (inner as any).defaultTextStyle;
|
|
2475
|
-
const replacement = new ThinkingParagraph(text, mdTheme, style);
|
|
2740
|
+
const replacement = new ThinkingParagraph(text, mdTheme, style, options);
|
|
2476
2741
|
if ((child as any)?.child !== undefined) {
|
|
2477
2742
|
(child as any).child = replacement;
|
|
2478
2743
|
} else {
|
|
2479
2744
|
container.children[i] = replacement;
|
|
2480
2745
|
}
|
|
2481
2746
|
} else {
|
|
2482
|
-
const replacement = new DottedParagraph(text, mdTheme);
|
|
2747
|
+
const replacement = new DottedParagraph(text, mdTheme, options);
|
|
2483
2748
|
if ((child as any)?.child !== undefined) {
|
|
2484
2749
|
(child as any).child = replacement;
|
|
2485
2750
|
} else {
|
|
@@ -2488,9 +2753,10 @@ function patchAssistantMessages(): void {
|
|
|
2488
2753
|
}
|
|
2489
2754
|
}
|
|
2490
2755
|
}
|
|
2491
|
-
const
|
|
2492
|
-
const
|
|
2493
|
-
const
|
|
2756
|
+
const workedMetadata = workedMetadataForMessage(message);
|
|
2757
|
+
const explicitDuration = workedMetadata?.duration;
|
|
2758
|
+
const explicitSessionTotal = workedMetadata?.sessionTotal;
|
|
2759
|
+
const explicitTurns = workedMetadata?.turns;
|
|
2494
2760
|
// The "Turn took" line must only appear once the stream has truly closed.
|
|
2495
2761
|
// `message.stopReason === "stop"` is not a safe "finished" signal here because
|
|
2496
2762
|
// providers may initialize a live message with that value. The `message_end`
|
|
@@ -2513,8 +2779,120 @@ function patchAssistantMessages(): void {
|
|
|
2513
2779
|
proto[ASSISTANT_PATCH_FLAG] = true;
|
|
2514
2780
|
}
|
|
2515
2781
|
|
|
2782
|
+
const THINKING_HOVER_KEY = Symbol.for("pi-claude-style-tools:thinking-hover");
|
|
2783
|
+
|
|
2784
|
+
function thinkingVisualChild(child: any): any {
|
|
2785
|
+
const inner = child?.child ?? child;
|
|
2786
|
+
if (
|
|
2787
|
+
inner instanceof HiddenThinkingSummary
|
|
2788
|
+
|| inner instanceof ThinkingParagraph
|
|
2789
|
+
|| inner?.constructor?.name === "HiddenThinkingSummary"
|
|
2790
|
+
|| inner?.constructor?.name === "ThinkingParagraph"
|
|
2791
|
+
) return inner;
|
|
2792
|
+
return undefined;
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
function thinkingLayout(component: any, width: number): Array<{ component: any; height: number }> {
|
|
2796
|
+
const container = component?.contentContainer;
|
|
2797
|
+
const children = Array.isArray(container?.children) ? container.children : [];
|
|
2798
|
+
if (container?.mouseLayout?.width === width) return container.mouseLayout.children;
|
|
2799
|
+
return children.map((child: any) => ({ component: child, height: child.render(width).length }));
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
function thinkingRowAt(component: any, event: TuiMouseEvent): any {
|
|
2803
|
+
let childY = 0;
|
|
2804
|
+
for (const entry of thinkingLayout(component, event.width)) {
|
|
2805
|
+
if (event.y >= childY && event.y < childY + entry.height) return thinkingVisualChild(entry.component);
|
|
2806
|
+
childY += entry.height;
|
|
2807
|
+
}
|
|
2808
|
+
return undefined;
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
function setThinkingHover(component: any, hovered: any): boolean {
|
|
2812
|
+
const previous = component[THINKING_HOVER_KEY];
|
|
2813
|
+
if (previous === hovered) return false;
|
|
2814
|
+
previous?.setHovered?.(false);
|
|
2815
|
+
if (hovered) hovered.setHovered(true);
|
|
2816
|
+
component[THINKING_HOVER_KEY] = hovered;
|
|
2817
|
+
clearMessageRenderCache(component);
|
|
2818
|
+
return true;
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
function patchAssistantFullscreenMouse(): void {
|
|
2822
|
+
const proto = AssistantMessageComponent.prototype as any;
|
|
2823
|
+
if (proto[ASSISTANT_MOUSE_PATCH_FLAG]) return;
|
|
2824
|
+
const originalHandleMouse = proto.handleMouse;
|
|
2825
|
+
proto.handleMouse = function patchedAssistantHandleMouse(event: TuiMouseEvent) {
|
|
2826
|
+
const hovered = thinkingRowAt(this, event);
|
|
2827
|
+
if (event.type === "move" && hovered) {
|
|
2828
|
+
claimClickHover(this, () => { if (setThinkingHover(this, undefined)) requestComponentRender(this); });
|
|
2829
|
+
}
|
|
2830
|
+
const hoverChanged = event.type === "move" ? setThinkingHover(this, hovered) : false;
|
|
2831
|
+
if (event.type === "move") return hoverChanged ? { handled: true, render: true } : undefined;
|
|
2832
|
+
// Claim the row before Pi's MouseRegion. Its handler restores the italic
|
|
2833
|
+
// "Thinking…" label while a block is streaming.
|
|
2834
|
+
if (isPrimaryClick(event) && hovered && this.lastMessage) {
|
|
2835
|
+
const overrides = this.thinkingVisibilityOverrides instanceof Map
|
|
2836
|
+
? this.thinkingVisibilityOverrides
|
|
2837
|
+
: new Map();
|
|
2838
|
+
this.thinkingVisibilityOverrides = overrides;
|
|
2839
|
+
overrides.set(0, hovered instanceof ThinkingParagraph);
|
|
2840
|
+
this.updateContent(this.lastMessage);
|
|
2841
|
+
return { handled: true };
|
|
2842
|
+
}
|
|
2843
|
+
return typeof originalHandleMouse === "function" ? originalHandleMouse.call(this, event) : undefined;
|
|
2844
|
+
};
|
|
2845
|
+
proto[ASSISTANT_MOUSE_PATCH_FLAG] = true;
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2516
2848
|
const TOOL_BG_PATCH_FLAG = Symbol.for("pi-claude-style-tools:patched-tool-bg-sync");
|
|
2517
2849
|
|
|
2850
|
+
const TOOL_HOVER_KEY = Symbol.for("pi-claude-style-tools:tool-hover");
|
|
2851
|
+
|
|
2852
|
+
function setToolHover(component: any, hovered: boolean): boolean {
|
|
2853
|
+
if (component[TOOL_HOVER_KEY] === hovered) return false;
|
|
2854
|
+
component[TOOL_HOVER_KEY] = hovered;
|
|
2855
|
+
clearToolRenderCache(component);
|
|
2856
|
+
return true;
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
function patchToolFullscreenMouse(): void {
|
|
2860
|
+
const proto = ToolExecutionComponent.prototype as any;
|
|
2861
|
+
if (proto[TOOL_MOUSE_PATCH_FLAG]) return;
|
|
2862
|
+
const originalHandleMouse = proto.handleMouse;
|
|
2863
|
+
if (typeof originalHandleMouse !== "function") return;
|
|
2864
|
+
const originalRender = proto.render;
|
|
2865
|
+
proto.render = function patchedToolHoverRender(width: number) {
|
|
2866
|
+
const lines = originalRender.call(this, width);
|
|
2867
|
+
if (this[TOOL_HOVER_KEY] !== true || !Array.isArray(lines)) return lines;
|
|
2868
|
+
const titleIndex = lines.findIndex((line: string) => stripAnsi(line).trim().length > 0 && !/^\s*[╭╰─│]+\s*$/.test(stripAnsi(line)));
|
|
2869
|
+
if (titleIndex < 0) return lines;
|
|
2870
|
+
const rendered = lines.slice();
|
|
2871
|
+
rendered[titleIndex] = brightenRenderedLine(rendered[titleIndex]);
|
|
2872
|
+
return rendered;
|
|
2873
|
+
};
|
|
2874
|
+
proto.handleMouse = function patchedToolHandleMouse(event: TuiMouseEvent) {
|
|
2875
|
+
if (event.type === "move") {
|
|
2876
|
+
claimClickHover(this, () => { if (setToolHover(this, false)) requestComponentRender(this); });
|
|
2877
|
+
return setToolHover(this, true) ? { handled: true, render: true } : undefined;
|
|
2878
|
+
}
|
|
2879
|
+
const result = originalHandleMouse.call(this, event);
|
|
2880
|
+
if (result) return result;
|
|
2881
|
+
// Self-rendered rows forward into their own content. Default-shell rows
|
|
2882
|
+
// reach here only when no nested region claimed the click, including the
|
|
2883
|
+
// blank outline padding our container patch paints around the content.
|
|
2884
|
+
if (!isPrimaryClick(event) || !this.result) return undefined;
|
|
2885
|
+
const group = this[COMPONENT_PARENT];
|
|
2886
|
+
if (group?.constructor?.name === "ToolGroupComponent") {
|
|
2887
|
+
return toggleGroupedTools(group, [this]);
|
|
2888
|
+
}
|
|
2889
|
+
if (!toggleExpandedComponent(this)) return undefined;
|
|
2890
|
+
requestComponentRender(this);
|
|
2891
|
+
return { handled: true };
|
|
2892
|
+
};
|
|
2893
|
+
proto[TOOL_MOUSE_PATCH_FLAG] = true;
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2518
2896
|
function patchToolExecutionBackgroundSync(): void {
|
|
2519
2897
|
const proto = ToolExecutionComponent.prototype as any;
|
|
2520
2898
|
if (proto[TOOL_BG_PATCH_FLAG]) return;
|
|
@@ -3465,6 +3843,10 @@ function bashCollapsedLimit(): number {
|
|
|
3465
3843
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : 10;
|
|
3466
3844
|
}
|
|
3467
3845
|
|
|
3846
|
+
function bashOutputMode(): "opencode" | "summary" | "preview" {
|
|
3847
|
+
return getMode(readSettings().bashOutputMode, ["opencode", "summary", "preview"] as const, "opencode");
|
|
3848
|
+
}
|
|
3849
|
+
|
|
3468
3850
|
function bashCommandPreviewLimit(): number {
|
|
3469
3851
|
const value = readSettings().bashCommandPreviewLines;
|
|
3470
3852
|
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : 8;
|
|
@@ -5274,20 +5656,23 @@ function trackThinkingBlockEvents(event: any, ctx?: any): void {
|
|
|
5274
5656
|
if (evt.type === "thinking_start") {
|
|
5275
5657
|
thinkingBlockInFlight = true;
|
|
5276
5658
|
thinkingBlockStartMs = Date.now();
|
|
5277
|
-
lastThinkingBlockDurationMs = undefined;
|
|
5278
5659
|
if (message?.role === "assistant") {
|
|
5279
|
-
(message
|
|
5280
|
-
delete
|
|
5660
|
+
thinkingActiveMessages.add(message);
|
|
5661
|
+
thinkingDurationByMessage.delete(message);
|
|
5281
5662
|
}
|
|
5282
5663
|
refreshThinkingChrome();
|
|
5283
5664
|
return;
|
|
5284
5665
|
}
|
|
5285
5666
|
if (evt.type === "thinking_end") {
|
|
5286
5667
|
thinkingBlockInFlight = false;
|
|
5287
|
-
const duration =
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
if (message?.role === "assistant") (message
|
|
5668
|
+
const duration = thinkingBlockStartMs > 0
|
|
5669
|
+
? Math.max(0, Date.now() - thinkingBlockStartMs)
|
|
5670
|
+
: undefined;
|
|
5671
|
+
if (message?.role === "assistant") thinkingActiveMessages.delete(message);
|
|
5672
|
+
if (typeof duration === "number" && duration <= MAX_REASONABLE_THINKING_DURATION_MS && message?.role === "assistant") {
|
|
5673
|
+
thinkingDurationByMessage.set(message, duration);
|
|
5674
|
+
}
|
|
5675
|
+
thinkingBlockStartMs = 0;
|
|
5291
5676
|
refreshThinkingChrome();
|
|
5292
5677
|
return;
|
|
5293
5678
|
}
|
|
@@ -5297,15 +5682,15 @@ function trackThinkingBlockEvents(event: any, ctx?: any): void {
|
|
|
5297
5682
|
// run normally. Any non-thinking stream event on the same assistant message
|
|
5298
5683
|
// means thinking is no longer the live activity: freeze the elapsed time into
|
|
5299
5684
|
// a "Thought for Xs" duration so the row always resolves.
|
|
5300
|
-
if ((message
|
|
5685
|
+
if (hasThinkingActiveMetadata(message) || thinkingBlockInFlight) {
|
|
5301
5686
|
if (evt.type === "text_start" || evt.type === "text_delta" || evt.type === "toolcall_start" || evt.type === "toolcall_end") {
|
|
5302
5687
|
thinkingBlockInFlight = false;
|
|
5303
5688
|
const duration = thinkingBlockStartMs > 0 ? Math.max(0, Date.now() - thinkingBlockStartMs) : undefined;
|
|
5304
|
-
if (message?.role === "assistant") delete
|
|
5305
|
-
if (typeof duration === "number") {
|
|
5306
|
-
|
|
5307
|
-
if (message?.role === "assistant") (message as any)[THINKING_DURATION_KEY] = duration;
|
|
5689
|
+
if (message?.role === "assistant") thinkingActiveMessages.delete(message);
|
|
5690
|
+
if (typeof duration === "number" && duration <= MAX_REASONABLE_THINKING_DURATION_MS && message?.role === "assistant") {
|
|
5691
|
+
thinkingDurationByMessage.set(message, duration);
|
|
5308
5692
|
}
|
|
5693
|
+
thinkingBlockStartMs = 0;
|
|
5309
5694
|
refreshThinkingChrome();
|
|
5310
5695
|
}
|
|
5311
5696
|
}
|
|
@@ -5318,11 +5703,9 @@ function registerThinkingLabels(pi: ExtensionAPI): void {
|
|
|
5318
5703
|
if (theme) applyThemePaletteIfNeeded(theme);
|
|
5319
5704
|
const message = event?.message;
|
|
5320
5705
|
if (!message || message.role !== "assistant" || !Array.isArray(message.content)) return;
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
}
|
|
5325
|
-
}
|
|
5706
|
+
// Thinking labels are applied by ThinkingParagraph while rendering. Never
|
|
5707
|
+
// rewrite the assistant message object: context/compaction consumers need
|
|
5708
|
+
// its canonical thinking content unchanged.
|
|
5326
5709
|
};
|
|
5327
5710
|
pi.on("before_agent_start", async () => {
|
|
5328
5711
|
// Start once per top-level request. Steering/follow-up messages can be
|
|
@@ -5348,13 +5731,14 @@ function registerThinkingLabels(pi: ExtensionAPI): void {
|
|
|
5348
5731
|
}
|
|
5349
5732
|
if (message?.role === "assistant") {
|
|
5350
5733
|
currentAssistantMessageStartMs = Date.now();
|
|
5351
|
-
(message
|
|
5734
|
+
workedStartByMessage.set(message, currentAssistantMessageStartMs);
|
|
5352
5735
|
// A new assistant message starts a fresh thinking lifecycle. Without
|
|
5353
5736
|
// this, a missing thinking_end on the previous message leaves the
|
|
5354
5737
|
// global in-flight flag set and the next message renders a stale
|
|
5355
5738
|
// "Thinking..." row until its own thinking events arrive.
|
|
5356
5739
|
thinkingBlockInFlight = false;
|
|
5357
|
-
|
|
5740
|
+
thinkingBlockStartMs = 0;
|
|
5741
|
+
thinkingActiveMessages.delete(message);
|
|
5358
5742
|
}
|
|
5359
5743
|
});
|
|
5360
5744
|
pi.on("message_update", async (event, ctx) => {
|
|
@@ -5369,30 +5753,22 @@ function registerThinkingLabels(pi: ExtensionAPI): void {
|
|
|
5369
5753
|
// into its "Thought for Xs" duration here so the row always resolves
|
|
5370
5754
|
// at the end of the message instead of sticking into later calls.
|
|
5371
5755
|
thinkingBlockInFlight = false;
|
|
5372
|
-
delete
|
|
5373
|
-
if (
|
|
5374
|
-
const duration =
|
|
5375
|
-
if (
|
|
5376
|
-
lastThinkingBlockDurationMs = duration;
|
|
5377
|
-
(message as any)[THINKING_DURATION_KEY] = duration;
|
|
5378
|
-
} else if (typeof lastThinkingBlockDurationMs === "number" && lastThinkingBlockDurationMs > 0) {
|
|
5379
|
-
(message as any)[THINKING_DURATION_KEY] = lastThinkingBlockDurationMs;
|
|
5380
|
-
}
|
|
5756
|
+
thinkingActiveMessages.delete(message);
|
|
5757
|
+
if (getThinkingDurationMetadata(message) === undefined && thinkingBlockStartMs > 0) {
|
|
5758
|
+
const duration = Math.max(0, Date.now() - thinkingBlockStartMs);
|
|
5759
|
+
if (duration <= MAX_REASONABLE_THINKING_DURATION_MS) thinkingDurationByMessage.set(message, duration);
|
|
5381
5760
|
}
|
|
5382
5761
|
thinkingBlockStartMs = 0;
|
|
5383
5762
|
const started = typeof currentAgentWorkStartMs === "number"
|
|
5384
5763
|
? currentAgentWorkStartMs
|
|
5385
|
-
:
|
|
5386
|
-
? (message as any)[WORKED_START_KEY]
|
|
5387
|
-
: currentAssistantMessageStartMs;
|
|
5764
|
+
: workedStartByMessage.get(message)
|
|
5765
|
+
?? (typeof (message as any)[WORKED_START_KEY] === "number" ? (message as any)[WORKED_START_KEY] : currentAssistantMessageStartMs);
|
|
5388
5766
|
const isFinalAssistantMessage = message.stopReason === "stop";
|
|
5389
5767
|
if (started !== undefined && isFinalAssistantMessage) {
|
|
5390
5768
|
const durationMs = Date.now() - started;
|
|
5391
5769
|
const sessionTotalMs = typeof sessionStartMs === "number" ? Date.now() - sessionStartMs : undefined;
|
|
5392
5770
|
const turns = userTurnCount > 0 ? userTurnCount : undefined;
|
|
5393
|
-
(message
|
|
5394
|
-
if (typeof sessionTotalMs === "number") (message as any)[WORKED_SESSION_TOTAL_KEY] = sessionTotalMs;
|
|
5395
|
-
if (typeof turns === "number") (message as any)[WORKED_TURNS_KEY] = turns;
|
|
5771
|
+
workedDurationByMessage.set(message, { duration: durationMs, sessionTotal: sessionTotalMs, turns });
|
|
5396
5772
|
// Duration metadata drives the assistant component's TUI-only status line.
|
|
5397
5773
|
// Message content stays presentation-neutral for persistence and consumers.
|
|
5398
5774
|
}
|
|
@@ -5440,17 +5816,10 @@ function registerThinkingLabels(pi: ExtensionAPI): void {
|
|
|
5440
5816
|
sessionStartMs = sessionStartMs === undefined ? earliest : Math.min(sessionStartMs, earliest);
|
|
5441
5817
|
}
|
|
5442
5818
|
if (userCount > userTurnCount) userTurnCount = userCount;
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
block.thinking = stripThinkingPresentationArtifacts(block.thinking);
|
|
5448
|
-
}
|
|
5449
|
-
if (block && block.type === "text" && typeof block.text === "string") {
|
|
5450
|
-
block.text = stripWorkedDurationLine(block.text);
|
|
5451
|
-
}
|
|
5452
|
-
}
|
|
5453
|
-
}
|
|
5819
|
+
// Do not normalize or strip message content here. The context event is also
|
|
5820
|
+
// consumed by compaction/replay integrations that compare these objects
|
|
5821
|
+
// against the persisted session projection. Presentation cleanup belongs in
|
|
5822
|
+
// the renderer and must not mutate canonical history.
|
|
5454
5823
|
});
|
|
5455
5824
|
}
|
|
5456
5825
|
|
|
@@ -6440,11 +6809,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
6440
6809
|
patchToolExecutionBackgroundSync();
|
|
6441
6810
|
patchToolRenderCacheInvalidation();
|
|
6442
6811
|
patchReadImageExpansion();
|
|
6812
|
+
patchFullscreenHoverExit();
|
|
6813
|
+
patchToolFullscreenMouse();
|
|
6443
6814
|
patchContainerParentTracking();
|
|
6444
6815
|
patchGlobalToolBorders();
|
|
6445
6816
|
patchCustomMessageRender();
|
|
6446
6817
|
patchUserMessageRender();
|
|
6447
6818
|
patchAssistantMessages();
|
|
6819
|
+
patchAssistantFullscreenMouse();
|
|
6448
6820
|
patchToolExecutionRenderers();
|
|
6449
6821
|
applyDiffPalette();
|
|
6450
6822
|
registerThinkingLabels(pi);
|
|
@@ -6870,6 +7242,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
6870
7242
|
name: "read",
|
|
6871
7243
|
label: "read",
|
|
6872
7244
|
description: readTool.description,
|
|
7245
|
+
promptSnippet: (readTool as any).promptSnippet,
|
|
7246
|
+
promptGuidelines: (readTool as any).promptGuidelines,
|
|
6873
7247
|
parameters: readTool.parameters,
|
|
6874
7248
|
async execute(toolCallId, params, signal, onUpdate) {
|
|
6875
7249
|
return readTool.execute(toolCallId, params, signal, onUpdate);
|
|
@@ -6925,6 +7299,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
6925
7299
|
name: "bash",
|
|
6926
7300
|
label: "bash",
|
|
6927
7301
|
description: bashTool.description,
|
|
7302
|
+
promptSnippet: (bashTool as any).promptSnippet,
|
|
7303
|
+
promptGuidelines: (bashTool as any).promptGuidelines,
|
|
6928
7304
|
parameters: bashTool.parameters,
|
|
6929
7305
|
async execute(toolCallId, params, signal, onUpdate) {
|
|
6930
7306
|
return bashTool.execute(toolCallId, params, signal, onUpdate);
|
|
@@ -6982,9 +7358,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
6982
7358
|
let text = exitCode === null || exitCode === 0 ? theme.fg("success", "Done") : theme.fg("error", `Exit ${exitCode}`);
|
|
6983
7359
|
text += theme.fg("muted", ` (${nonEmpty.total} lines)`);
|
|
6984
7360
|
if (details?.truncation?.truncated) text += theme.fg("warning", " [truncated]");
|
|
7361
|
+
const mode = bashOutputMode();
|
|
7362
|
+
if (mode === "summary") return makeText(ctx.lastComponent, withBranch(text, theme));
|
|
6985
7363
|
const persistentPreview = shouldPreserveBashPreview(ctx) ? buildPersistentBashPreview(nonEmpty.lines, theme) : "";
|
|
6986
|
-
if (!expanded &&
|
|
6987
|
-
|
|
7364
|
+
if (!expanded && mode === "preview") {
|
|
7365
|
+
const previewLines = nonEmpty.lines.slice(0, bashCollapsedLimit());
|
|
7366
|
+
const preview = previewLines.length > 0
|
|
7367
|
+
? buildPreviewText(previewLines, false, theme, bashCollapsedLimit(), nonEmpty.total, (line) => theme.fg("dim", line || " "))
|
|
7368
|
+
: "";
|
|
7369
|
+
const suffix = toolOutputDetailHint(theme, expanded);
|
|
7370
|
+
return makeText(ctx.lastComponent, withBranch(`${text}${suffix}${preview ? `\n${preview}` : ""}`, theme));
|
|
7371
|
+
}
|
|
7372
|
+
if (!expanded && mode === "opencode" && nonEmpty.total > 0) return makeText(ctx.lastComponent, withBranch(`${text}${toolOutputDetailHint(theme, expanded)}`, theme));
|
|
6988
7373
|
if (!expanded) return makeText(ctx.lastComponent, withBranch(text, theme));
|
|
6989
7374
|
const collapsed = bashCollapsedLimit();
|
|
6990
7375
|
if (rewrite) text += `\n${formatRtkRewriteDetails(rewrite, theme)}`;
|
|
@@ -6998,6 +7383,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
6998
7383
|
name: "grep",
|
|
6999
7384
|
label: "grep",
|
|
7000
7385
|
description: grepTool.description,
|
|
7386
|
+
promptSnippet: (grepTool as any).promptSnippet,
|
|
7387
|
+
promptGuidelines: (grepTool as any).promptGuidelines,
|
|
7001
7388
|
parameters: grepTool.parameters,
|
|
7002
7389
|
async execute(toolCallId, params, signal, onUpdate) {
|
|
7003
7390
|
return grepTool.execute(toolCallId, params, signal, onUpdate);
|
|
@@ -7038,6 +7425,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7038
7425
|
name: "find",
|
|
7039
7426
|
label: "find",
|
|
7040
7427
|
description: findTool.description,
|
|
7428
|
+
promptSnippet: (findTool as any).promptSnippet,
|
|
7429
|
+
promptGuidelines: (findTool as any).promptGuidelines,
|
|
7041
7430
|
parameters: findTool.parameters,
|
|
7042
7431
|
async execute(toolCallId, params, signal, onUpdate) {
|
|
7043
7432
|
return findTool.execute(toolCallId, params, signal, onUpdate);
|
|
@@ -7089,6 +7478,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7089
7478
|
name: "ls",
|
|
7090
7479
|
label: "ls",
|
|
7091
7480
|
description: lsTool.description,
|
|
7481
|
+
promptSnippet: (lsTool as any).promptSnippet,
|
|
7482
|
+
promptGuidelines: (lsTool as any).promptGuidelines,
|
|
7092
7483
|
parameters: lsTool.parameters,
|
|
7093
7484
|
async execute(toolCallId, params, signal, onUpdate) {
|
|
7094
7485
|
return lsTool.execute(toolCallId, params, signal, onUpdate);
|
|
@@ -7140,6 +7531,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7140
7531
|
name: "write",
|
|
7141
7532
|
label: "write",
|
|
7142
7533
|
description: writeTool.description,
|
|
7534
|
+
promptSnippet: (writeTool as any).promptSnippet,
|
|
7535
|
+
promptGuidelines: (writeTool as any).promptGuidelines,
|
|
7143
7536
|
parameters: writeTool.parameters,
|
|
7144
7537
|
async execute(toolCallId, params, signal, onUpdate, _ctx) {
|
|
7145
7538
|
const fp = params.path ?? (params as any).file_path ?? "";
|
|
@@ -7255,6 +7648,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7255
7648
|
name: "edit",
|
|
7256
7649
|
label: "edit",
|
|
7257
7650
|
description: editTool.description,
|
|
7651
|
+
promptSnippet: (editTool as any).promptSnippet,
|
|
7652
|
+
promptGuidelines: (editTool as any).promptGuidelines,
|
|
7258
7653
|
parameters: editTool.parameters,
|
|
7259
7654
|
async execute(toolCallId, params, signal, onUpdate, _ctx) {
|
|
7260
7655
|
const fp = params.path ?? (params as any).file_path ?? "";
|