@esso0428/pi-subagents 0.15.17 → 0.17.0
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 +17 -0
- package/README.md +10 -12
- package/dist/agent-history-list.d.ts.map +1 -1
- package/dist/agent-history-list.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -40
- package/dist/index.js.map +1 -1
- package/dist/output-file.js +3 -3
- package/dist/output-file.js.map +1 -1
- package/dist/settings.d.ts +1 -8
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +0 -5
- package/dist/settings.js.map +1 -1
- package/dist/ui/agent-widget.d.ts +66 -28
- package/dist/ui/agent-widget.d.ts.map +1 -1
- package/dist/ui/agent-widget.js +325 -134
- package/dist/ui/agent-widget.js.map +1 -1
- package/dist/ui/ccstyle/diff/ansi-utils.js +2 -2
- package/dist/ui/ccstyle/diff/ansi-utils.js.map +1 -1
- package/dist/ui/ccstyle/diff/diff-presentation.d.ts.map +1 -1
- package/dist/ui/ccstyle/diff/diff-presentation.js +0 -1
- package/dist/ui/ccstyle/diff/diff-presentation.js.map +1 -1
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts +1 -1
- package/dist/ui/ccstyle/diff/diff-renderer.d.ts.map +1 -1
- package/dist/ui/ccstyle/diff/diff-renderer.js +15 -10
- package/dist/ui/ccstyle/diff/diff-renderer.js.map +1 -1
- package/dist/ui/ccstyle/diff/line-width-safety.d.ts.map +1 -1
- package/dist/ui/ccstyle/diff/line-width-safety.js +1 -2
- package/dist/ui/ccstyle/diff/line-width-safety.js.map +1 -1
- package/dist/ui/ccstyle/diff/shiki-highlight.d.ts +19 -0
- package/dist/ui/ccstyle/diff/shiki-highlight.d.ts.map +1 -0
- package/dist/ui/ccstyle/diff/shiki-highlight.js +85 -0
- package/dist/ui/ccstyle/diff/shiki-highlight.js.map +1 -0
- package/dist/ui/ccstyle/tool-renderer.d.ts +1 -1
- package/dist/ui/ccstyle/tool-renderer.d.ts.map +1 -1
- package/dist/ui/ccstyle/tool-renderer.js +1 -1
- package/dist/ui/ccstyle/tool-renderer.js.map +1 -1
- package/dist/ui/ccstyle/tool-result.d.ts +9 -1
- package/dist/ui/ccstyle/tool-result.d.ts.map +1 -1
- package/dist/ui/ccstyle/tool-result.js +137 -5
- package/dist/ui/ccstyle/tool-result.js.map +1 -1
- package/dist/ui/conversation-blocks.d.ts.map +1 -1
- package/dist/ui/conversation-blocks.js +14 -13
- package/dist/ui/conversation-blocks.js.map +1 -1
- package/dist/ui/conversation-role.d.ts +1 -1
- package/dist/ui/conversation-role.d.ts.map +1 -1
- package/dist/ui/conversation-search.js +4 -4
- package/dist/ui/conversation-search.js.map +1 -1
- package/dist/ui/conversation-timeline.d.ts +17 -1
- package/dist/ui/conversation-timeline.d.ts.map +1 -1
- package/dist/ui/conversation-timeline.js +116 -43
- package/dist/ui/conversation-timeline.js.map +1 -1
- package/dist/ui/conversation-viewer.d.ts +5 -1
- package/dist/ui/conversation-viewer.d.ts.map +1 -1
- package/dist/ui/conversation-viewer.js +67 -26
- package/dist/ui/conversation-viewer.js.map +1 -1
- package/dist/write-execution.d.ts.map +1 -1
- package/dist/write-execution.js +1 -1
- package/dist/write-execution.js.map +1 -1
- package/package.json +6 -5
- package/src/agent-history-list.ts +1 -1
- package/src/index.ts +19 -73
- package/src/output-file.ts +3 -3
- package/src/settings.ts +1 -12
- package/src/ui/agent-widget.ts +264 -114
- package/src/ui/ccstyle/diff/ansi-utils.ts +2 -2
- package/src/ui/ccstyle/diff/diff-presentation.ts +0 -1
- package/src/ui/ccstyle/diff/diff-renderer.ts +22 -21
- package/src/ui/ccstyle/diff/line-width-safety.ts +1 -2
- package/src/ui/ccstyle/diff/shiki-highlight.ts +97 -0
- package/src/ui/ccstyle/tool-renderer.ts +3 -3
- package/src/ui/ccstyle/tool-result.ts +175 -7
- package/src/ui/conversation-role.ts +1 -1
- package/src/ui/conversation-search.ts +4 -4
- package/src/ui/conversation-timeline.ts +7 -7
- package/src/ui/conversation-viewer.ts +16 -7
- package/src/write-execution.ts +2 -2
- package/dist/ui/fleet-list.d.ts +0 -122
- package/dist/ui/fleet-list.d.ts.map +0 -1
- package/dist/ui/fleet-list.js +0 -375
- package/dist/ui/fleet-list.js.map +0 -1
- package/src/ui/deferred-ui-refresh.ts +0 -72
- package/src/ui/fleet-list.ts +0 -585
- package/test/agent-history-list.test.ts +0 -121
- package/test/agent-manager-history.test.ts +0 -88
- package/test/agent-menu-history-navigation.test.ts +0 -86
- package/test/output-file-history.test.ts +0 -50
- package/test/ui/agent-widget.test.ts +0 -172
- package/test/ui/conversation-blocks.test.ts +0 -103
- package/test/ui/conversation-diff.test.ts +0 -91
- package/test/ui/conversation-search.test.ts +0 -55
- package/test/ui/conversation-viewer.test.ts +0 -438
- package/test/ui/deferred-ui-refresh.test.ts +0 -55
- package/test/ui/fleet-list.test.ts +0 -389
- package/test/write-execution.test.ts +0 -83
- package/tsconfig.json +0 -16
|
@@ -1,39 +1,37 @@
|
|
|
1
|
+
import { getLanguageFromPath, highlightCode } from "@earendil-works/pi-coding-agent";
|
|
1
2
|
import {
|
|
3
|
+
type Component,
|
|
2
4
|
Text,
|
|
3
5
|
truncateToWidth,
|
|
4
6
|
visibleWidth,
|
|
5
7
|
wrapTextWithAnsi,
|
|
6
|
-
type Component,
|
|
7
8
|
} from "@earendil-works/pi-tui";
|
|
8
|
-
import { getLanguageFromPath, highlightCode } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import {
|
|
10
10
|
ANSI_SGR_PATTERN,
|
|
11
|
-
|
|
11
|
+
expandSgrReset,
|
|
12
12
|
filterSgrSequences,
|
|
13
|
-
toSgrParams,
|
|
14
13
|
readSgrColorSequence,
|
|
15
|
-
|
|
16
|
-
expandSgrReset,
|
|
14
|
+
toSgrParams,
|
|
17
15
|
} from "./ansi-utils.js";
|
|
18
|
-
import {
|
|
19
|
-
buildCollapsedDiffHintText,
|
|
20
|
-
clampRenderedLineToWidth,
|
|
21
|
-
clampRenderedLinesToWidth,
|
|
22
|
-
} from "./line-width-safety.js";
|
|
23
16
|
import {
|
|
24
17
|
buildDiffSummaryText,
|
|
18
|
+
type DiffPresentationMode,
|
|
25
19
|
normalizeDiffRenderWidth,
|
|
26
20
|
resolveDiffPresentationMode,
|
|
27
|
-
type DiffPresentationMode,
|
|
28
21
|
} from "./diff-presentation.js";
|
|
29
|
-
import {
|
|
30
|
-
|
|
22
|
+
import {
|
|
23
|
+
buildCollapsedDiffHintText,
|
|
24
|
+
clampRenderedLinesToWidth,
|
|
25
|
+
clampRenderedLineToWidth,
|
|
26
|
+
} from "./line-width-safety.js";
|
|
27
|
+
import { sanitizeAnsiForThemedOutput, sanitizeToolResultText, showMoreHintText } from "./render-utils.js";
|
|
31
28
|
import {
|
|
32
29
|
DEFAULT_TOOL_DISPLAY_CONFIG,
|
|
33
30
|
type DiffIndicatorMode,
|
|
34
|
-
type ToolDisplayConfig,
|
|
35
31
|
type PersistedDiff,
|
|
32
|
+
type ToolDisplayConfig,
|
|
36
33
|
} from "./types.js";
|
|
34
|
+
import { splitWriteContentLines } from "./write-display-utils.js";
|
|
37
35
|
|
|
38
36
|
const MAX_HL_CHARS = 200_000;
|
|
39
37
|
|
|
@@ -267,7 +265,7 @@ function wrapToWidth(text: string, width: number, wordWrap: boolean): string[] {
|
|
|
267
265
|
}
|
|
268
266
|
|
|
269
267
|
function resolveLanguageFromPath(rawPath: string | undefined): string | undefined {
|
|
270
|
-
if (!rawPath
|
|
268
|
+
if (!rawPath?.trim()) {
|
|
271
269
|
return undefined;
|
|
272
270
|
}
|
|
273
271
|
const normalizedPath = rawPath.replace(/^@/, "").trim();
|
|
@@ -281,7 +279,7 @@ function resolveLanguageFromPath(rawPath: string | undefined): string | undefine
|
|
|
281
279
|
}
|
|
282
280
|
}
|
|
283
281
|
|
|
284
|
-
function
|
|
282
|
+
function _resolveShikiTheme(theme: DiffTheme): string {
|
|
285
283
|
if (process.env.DIFF_THEME) return process.env.DIFF_THEME;
|
|
286
284
|
const background =
|
|
287
285
|
parseAnsiColorCode(readThemeAnsi(theme, "bg", "toolSuccessBg")) ??
|
|
@@ -828,7 +826,9 @@ function tokenizeInlineDiff(input: string): Array<{ value: string; start: number
|
|
|
828
826
|
const pattern = /(\s+|[A-Za-z0-9_]+|[^A-Za-z0-9_\s])/g;
|
|
829
827
|
let match: RegExpExecArray | null;
|
|
830
828
|
|
|
831
|
-
while (
|
|
829
|
+
while (true) {
|
|
830
|
+
match = pattern.exec(input);
|
|
831
|
+
if (match === null) break;
|
|
832
832
|
const value = match[0] ?? "";
|
|
833
833
|
if (!value) {
|
|
834
834
|
continue;
|
|
@@ -1139,13 +1139,15 @@ function mixRgb(base: RgbColor, tint: RgbColor, ratio: number): RgbColor {
|
|
|
1139
1139
|
}
|
|
1140
1140
|
|
|
1141
1141
|
function extractThemeBackgroundAnsi(text: string): string | undefined {
|
|
1142
|
-
if (!text
|
|
1142
|
+
if (!text?.includes("\x1b[")) {
|
|
1143
1143
|
return undefined;
|
|
1144
1144
|
}
|
|
1145
1145
|
|
|
1146
1146
|
ANSI_SGR_PATTERN.lastIndex = 0;
|
|
1147
1147
|
let match: RegExpExecArray | null;
|
|
1148
|
-
while (
|
|
1148
|
+
while (true) {
|
|
1149
|
+
match = ANSI_SGR_PATTERN.exec(text);
|
|
1150
|
+
if (match === null) break;
|
|
1149
1151
|
const parsed = toSgrParams(match[1] ?? "");
|
|
1150
1152
|
for (let index = 0; index < parsed.length; index += 1) {
|
|
1151
1153
|
const param = parsed[index] ?? 0;
|
|
@@ -1372,7 +1374,6 @@ function resolveIndicatorGlyph(
|
|
|
1372
1374
|
return " ";
|
|
1373
1375
|
}
|
|
1374
1376
|
return kind === "add" ? "+" : "-";
|
|
1375
|
-
case "none":
|
|
1376
1377
|
default:
|
|
1377
1378
|
return " ";
|
|
1378
1379
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { normalizeDiffRenderWidth } from "./diff-presentation.js";
|
|
2
|
-
import { pluralize } from "./render-utils.js";
|
|
3
|
-
import { showMoreHintText } from "./render-utils.js";
|
|
2
|
+
import { pluralize, showMoreHintText } from "./render-utils.js";
|
|
4
3
|
|
|
5
4
|
export interface WidthMeasurementOps {
|
|
6
5
|
measure(text: string): number;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/** Maximum source size eligible for optional asynchronous Shiki enhancement. */
|
|
2
|
+
export const MAX_HL_CHARS = 32_000;
|
|
3
|
+
const MAX_CACHE_ENTRIES = 48;
|
|
4
|
+
|
|
5
|
+
type CodeToAnsi = (code: string, lang: string, theme: string) => Promise<string>;
|
|
6
|
+
export type ShikiLoader = () => Promise<CodeToAnsi>;
|
|
7
|
+
type Subscriber = { fallback: readonly string[]; invalidate: () => void };
|
|
8
|
+
type Pending = { work: Promise<void>; subscribers: Set<Subscriber> };
|
|
9
|
+
|
|
10
|
+
let defaultLoad: Promise<CodeToAnsi> | undefined;
|
|
11
|
+
const loadDefault: ShikiLoader = () => {
|
|
12
|
+
if (!defaultLoad) {
|
|
13
|
+
defaultLoad = import("@shikijs/cli")
|
|
14
|
+
.then((module) => module.codeToANSI as CodeToAnsi)
|
|
15
|
+
.catch((error) => {
|
|
16
|
+
defaultLoad = undefined;
|
|
17
|
+
throw error;
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return defaultLoad;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function differs(lines: readonly string[], fallback: readonly string[]): boolean {
|
|
24
|
+
return lines.length !== fallback.length || lines.some((line, index) => line !== fallback[index]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Small, viewer-local async cache. The synchronous Pi highlighter remains the
|
|
29
|
+
* first paint; Shiki is only requested by an expanded card that has metadata.
|
|
30
|
+
*/
|
|
31
|
+
export class ShikiHighlightCache {
|
|
32
|
+
private readonly cache = new Map<string, string[]>();
|
|
33
|
+
private readonly pending = new Map<string, Pending>();
|
|
34
|
+
|
|
35
|
+
constructor(private readonly loader: ShikiLoader = loadDefault) {}
|
|
36
|
+
|
|
37
|
+
get(
|
|
38
|
+
code: string,
|
|
39
|
+
lang: string | undefined,
|
|
40
|
+
theme: string,
|
|
41
|
+
fallback: readonly string[],
|
|
42
|
+
invalidate?: () => void,
|
|
43
|
+
): string[] | undefined {
|
|
44
|
+
if (!lang || code.length > MAX_HL_CHARS) return undefined;
|
|
45
|
+
|
|
46
|
+
const key = `${theme}\0${lang}\0${code}`;
|
|
47
|
+
const cached = this.cache.get(key);
|
|
48
|
+
if (cached) {
|
|
49
|
+
// LRU: a frequently viewed card should not evict itself immediately.
|
|
50
|
+
this.cache.delete(key);
|
|
51
|
+
this.cache.set(key, cached);
|
|
52
|
+
return cached;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const existing = this.pending.get(key);
|
|
56
|
+
if (existing) {
|
|
57
|
+
if (invalidate && ![...existing.subscribers].some((item) => item.invalidate === invalidate)) {
|
|
58
|
+
existing.subscribers.add({ fallback, invalidate });
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const pending: Pending = { work: undefined as never, subscribers: new Set() };
|
|
64
|
+
if (invalidate) pending.subscribers.add({ fallback, invalidate });
|
|
65
|
+
pending.work = Promise.resolve()
|
|
66
|
+
.then(() => this.loader())
|
|
67
|
+
.then((highlight) => highlight(code, lang, theme))
|
|
68
|
+
.then((ansi) => {
|
|
69
|
+
const lines = ansi.replace(/\r/g, "").replace(/\n$/, "").split("\n");
|
|
70
|
+
this.cache.set(key, lines);
|
|
71
|
+
while (this.cache.size > MAX_CACHE_ENTRIES) {
|
|
72
|
+
const oldest = this.cache.keys().next().value;
|
|
73
|
+
if (oldest !== undefined) this.cache.delete(oldest);
|
|
74
|
+
}
|
|
75
|
+
for (const subscriber of pending.subscribers) {
|
|
76
|
+
if (!differs(lines, subscriber.fallback)) continue;
|
|
77
|
+
try {
|
|
78
|
+
subscriber.invalidate();
|
|
79
|
+
} catch {
|
|
80
|
+
// A stale card must not prevent another card from being repainted.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
})
|
|
84
|
+
.catch(() => {
|
|
85
|
+
// The synchronous Pi highlighter remains the durable fallback.
|
|
86
|
+
})
|
|
87
|
+
.finally(() => this.pending.delete(key));
|
|
88
|
+
this.pending.set(key, pending);
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
clear(): void {
|
|
93
|
+
this.cache.clear();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const shikiHighlightCache = new ShikiHighlightCache();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createViewerCcstyleResult, oneLine } from "./tool-result.js";
|
|
1
|
+
import { type Component, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
2
|
import { DEFAULT_TOOL_DISPLAY_CONFIG } from "./diff/types.js";
|
|
3
|
+
import { createViewerCcstyleResult, oneLine } from "./tool-result.js";
|
|
4
4
|
|
|
5
5
|
export interface ViewerToolRenderers {
|
|
6
6
|
name: string;
|
|
@@ -82,5 +82,5 @@ export function createViewerCcstyleTool(toolName: string, isHovered: () => boole
|
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
export { renderRichToolResult } from "./tool-result.js";
|
|
86
85
|
export type { PersistedDiff, ViewerDiffConfig } from "./diff/types.js";
|
|
86
|
+
export { renderRichToolResult } from "./tool-result.js";
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getLanguageFromPath, highlightCode } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type Component, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
3
|
import {
|
|
4
|
+
type DisplayConfigInput,
|
|
3
5
|
renderEditDiffResult,
|
|
4
6
|
renderWriteDiffResult,
|
|
5
|
-
type DisplayConfigInput,
|
|
6
7
|
} from "./diff/diff-renderer.js";
|
|
8
|
+
import { sanitizeAnsiForThemedOutput, sanitizeToolResultText, showMoreHintText } from "./diff/render-utils.js";
|
|
9
|
+
import { MAX_HL_CHARS, shikiHighlightCache } from "./diff/shiki-highlight.js";
|
|
7
10
|
import { DEFAULT_TOOL_DISPLAY_CONFIG, type PersistedDiff, type ViewerDiffConfig } from "./diff/types.js";
|
|
8
|
-
import { sanitizeToolResultText, showMoreHintText } from "./diff/render-utils.js";
|
|
9
11
|
|
|
10
12
|
export function toolViewportWidth(width: number): number {
|
|
11
13
|
return Math.max(1, Math.floor(Number.isFinite(width) ? width : 1));
|
|
@@ -96,6 +98,97 @@ export function formatToolInputArgs(args: unknown, maxChars = 8_000): string {
|
|
|
96
98
|
return rendered.length > maxChars ? `${rendered.slice(0, maxChars)}…` : rendered;
|
|
97
99
|
}
|
|
98
100
|
|
|
101
|
+
type FencedCodeRegion = {
|
|
102
|
+
bodyStart: number;
|
|
103
|
+
bodyEnd: number;
|
|
104
|
+
language?: string;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
type OutputHighlightPlan = {
|
|
108
|
+
lines: string[];
|
|
109
|
+
code: string;
|
|
110
|
+
codeLineIndexes: number[];
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
function parseFenceOpening(line: string): { marker: string; language?: string } | undefined {
|
|
114
|
+
const match = line.match(/^\s*(`{3,}|~{3,})(?:\s*([A-Za-z][\w+.-]*))?(?:\s+.*)?\s*$/);
|
|
115
|
+
if (!match) return undefined;
|
|
116
|
+
return { marker: match[1]!, language: match[2] };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isFenceClosing(line: string, marker: string): boolean {
|
|
120
|
+
const trimmed = line.trim();
|
|
121
|
+
return trimmed.length >= marker.length &&
|
|
122
|
+
trimmed[0] === marker[0] &&
|
|
123
|
+
[...trimmed].every((character) => character === marker[0]);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function findFencedCodeRegion(lines: readonly string[]): FencedCodeRegion | undefined {
|
|
127
|
+
for (let openingIndex = 0; openingIndex < lines.length; openingIndex++) {
|
|
128
|
+
const opening = parseFenceOpening(lines[openingIndex]!);
|
|
129
|
+
if (!opening) continue;
|
|
130
|
+
let bodyEnd = lines.length;
|
|
131
|
+
for (let index = openingIndex + 1; index < lines.length; index++) {
|
|
132
|
+
if (isFenceClosing(lines[index]!, opening.marker)) {
|
|
133
|
+
bodyEnd = index;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
bodyStart: openingIndex + 1,
|
|
139
|
+
bodyEnd,
|
|
140
|
+
...(opening.language ? { language: opening.language } : {}),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function pathFromToolArgs(args: unknown): string | undefined {
|
|
147
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) return undefined;
|
|
148
|
+
const record = args as Record<string, unknown>;
|
|
149
|
+
const path = record.file_path ?? record.path;
|
|
150
|
+
return typeof path === "string" && path.trim() ? path : undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Resolve only explicit tool/path or fenced metadata; no command/log heuristics. */
|
|
154
|
+
export function resolveToolOutputLanguage(args: unknown, output: string): string | undefined {
|
|
155
|
+
const path = pathFromToolArgs(args)?.replace(/^@/, "").trim();
|
|
156
|
+
if (path) {
|
|
157
|
+
try {
|
|
158
|
+
const language = getLanguageFromPath(path);
|
|
159
|
+
if (language) return language;
|
|
160
|
+
} catch {
|
|
161
|
+
// Fall through to an explicit fence when the path mapping is unavailable.
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const lines = output.replace(/\t/g, " ").replace(/\n+$/, "").split("\n");
|
|
165
|
+
return findFencedCodeRegion(lines)?.language;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function outputHighlightPlan(output: string, language: string | undefined): OutputHighlightPlan | undefined {
|
|
169
|
+
if (!language || !output.trim() || output.length > MAX_HL_CHARS) return undefined;
|
|
170
|
+
const lines = output.replace(/\t/g, " ").replace(/\n+$/, "").split("\n");
|
|
171
|
+
// A path language follows Pi's read renderer and applies to the complete
|
|
172
|
+
// output. A fence-only language is also highlighted as a normal code body;
|
|
173
|
+
// keeping one line-to-line mapping preserves fences and wrapping safely.
|
|
174
|
+
const codeLineIndexes = lines.map((_, index) => index);
|
|
175
|
+
const codeLines = codeLineIndexes.map((index) => sanitizeToolResultText(lines[index] ?? "").replace(/\n/g, ""));
|
|
176
|
+
const code = codeLines.join("\n");
|
|
177
|
+
if (!code || code.length > MAX_HL_CHARS) return undefined;
|
|
178
|
+
return { lines, code, codeLineIndexes };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function syncHighlight(code: string, language: string, fallback: readonly string[]): string[] {
|
|
182
|
+
try {
|
|
183
|
+
const highlighted = highlightCode(code, language).map(sanitizeAnsiForThemedOutput);
|
|
184
|
+
return highlighted.length === fallback.length
|
|
185
|
+
? highlighted
|
|
186
|
+
: fallback.map((line, index) => highlighted[index] ?? line);
|
|
187
|
+
} catch {
|
|
188
|
+
return fallback.map(sanitizeAnsiForThemedOutput);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
99
192
|
function hasExpandableDetail(text: string, args: unknown): boolean {
|
|
100
193
|
return countLines(text) > 1 || formatToolInputArgs(args).trim().length > 0;
|
|
101
194
|
}
|
|
@@ -109,6 +202,19 @@ export class ExpandedToolIoView implements Component {
|
|
|
109
202
|
private lineSections: Array<ToolIoSection | null> = [];
|
|
110
203
|
private hoveredSection: ToolIoSection | null = null;
|
|
111
204
|
private showMoreRows: Partial<Record<ToolIoSection, number>> = {};
|
|
205
|
+
private readonly outputLanguage?: string;
|
|
206
|
+
private readonly invalidateCard?: () => void;
|
|
207
|
+
private outputHighlightKey: string | undefined;
|
|
208
|
+
private outputHighlightState: {
|
|
209
|
+
plan: OutputHighlightPlan;
|
|
210
|
+
fallback: string[];
|
|
211
|
+
fallbackReady: boolean;
|
|
212
|
+
shiki?: string[];
|
|
213
|
+
} | undefined;
|
|
214
|
+
private readonly onHighlightReady = (): void => {
|
|
215
|
+
this.invalidate();
|
|
216
|
+
this.invalidateCard?.();
|
|
217
|
+
};
|
|
112
218
|
|
|
113
219
|
constructor(
|
|
114
220
|
private input: string,
|
|
@@ -117,11 +223,56 @@ export class ExpandedToolIoView implements Component {
|
|
|
117
223
|
private readonly maxOutputLines = DEFAULT_TOOL_DISPLAY_CONFIG.expandedPreviewMaxLines,
|
|
118
224
|
private readonly maxInputLines = DEFAULT_TOOL_DISPLAY_CONFIG.expandedPreviewMaxLines,
|
|
119
225
|
private readonly theme?: any,
|
|
120
|
-
|
|
226
|
+
outputLanguage?: string,
|
|
227
|
+
invalidateCard?: () => void,
|
|
228
|
+
) {
|
|
229
|
+
this.outputLanguage = outputLanguage;
|
|
230
|
+
this.invalidateCard = invalidateCard;
|
|
231
|
+
}
|
|
121
232
|
|
|
122
233
|
getInputBody(): string { return this.input; }
|
|
123
234
|
getOutputBody(): string { return this.output.trim() ? this.output : "Done"; }
|
|
124
235
|
|
|
236
|
+
private highlightedOutputLines(body: string): string[] {
|
|
237
|
+
const plainLines = body.split("\n");
|
|
238
|
+
const plan = outputHighlightPlan(body, this.outputLanguage);
|
|
239
|
+
if (!plan) return plainLines;
|
|
240
|
+
|
|
241
|
+
const key = `${this.outputLanguage ?? ""}\0${plan.code}\0${plan.lines.join("\n")}`;
|
|
242
|
+
if (this.outputHighlightKey !== key || !this.outputHighlightState) {
|
|
243
|
+
this.outputHighlightKey = key;
|
|
244
|
+
this.outputHighlightState = {
|
|
245
|
+
plan,
|
|
246
|
+
// A plain fallback lets a cache hit avoid running the synchronous
|
|
247
|
+
// highlighter again. On a miss it is replaced below before paint.
|
|
248
|
+
fallback: plan.code.split("\n"),
|
|
249
|
+
fallbackReady: false,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const state = this.outputHighlightState;
|
|
254
|
+
const shikiLines = state.shiki ?? shikiHighlightCache.get(
|
|
255
|
+
plan.code,
|
|
256
|
+
this.outputLanguage,
|
|
257
|
+
process.env.DIFF_THEME || "github-dark",
|
|
258
|
+
state.fallback,
|
|
259
|
+
this.onHighlightReady,
|
|
260
|
+
);
|
|
261
|
+
if (shikiLines) {
|
|
262
|
+
state.shiki = shikiLines.map(sanitizeAnsiForThemedOutput);
|
|
263
|
+
} else if (!state.fallbackReady) {
|
|
264
|
+
state.fallback = syncHighlight(plan.code, this.outputLanguage!, state.fallback);
|
|
265
|
+
state.fallbackReady = true;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const highlightedCode = state.shiki ?? state.fallback;
|
|
269
|
+
const lines = plan.lines.slice();
|
|
270
|
+
for (const [codeIndex, lineIndex] of plan.codeLineIndexes.entries()) {
|
|
271
|
+
lines[lineIndex] = highlightedCode[codeIndex] ?? lines[lineIndex] ?? "";
|
|
272
|
+
}
|
|
273
|
+
return lines;
|
|
274
|
+
}
|
|
275
|
+
|
|
125
276
|
setHoveredSection(section: ToolIoSection | null): void {
|
|
126
277
|
if (this.hoveredSection === section) return;
|
|
127
278
|
this.hoveredSection = section;
|
|
@@ -160,6 +311,7 @@ export class ExpandedToolIoView implements Component {
|
|
|
160
311
|
return;
|
|
161
312
|
}
|
|
162
313
|
const sources = raw.split("\n");
|
|
314
|
+
const highlighted = input ? undefined : this.highlightedOutputLines(raw);
|
|
163
315
|
const wrapped: string[] = [];
|
|
164
316
|
const styleInput = (line: string): string => {
|
|
165
317
|
const match = line.match(/^([A-Za-z_][\w.-]*)(:\s*)(.*)$/);
|
|
@@ -167,8 +319,14 @@ export class ExpandedToolIoView implements Component {
|
|
|
167
319
|
? color("dim", `${match[1]}${match[2]}`) + color("muted", match[3] ?? "")
|
|
168
320
|
: color("muted", line);
|
|
169
321
|
};
|
|
170
|
-
for (const source of sources) {
|
|
171
|
-
const
|
|
322
|
+
for (const [index, source] of sources.entries()) {
|
|
323
|
+
const outputLine = highlighted?.[index] ?? source;
|
|
324
|
+
// Shiki/core ANSI carries the actual token colors. Do not wrap it in
|
|
325
|
+
// theme.fg(), whose closing reset would erase nested token styles.
|
|
326
|
+
const hasAnsi = /\x1b\[[0-?]*[ -/]*[@-~]/.test(outputLine);
|
|
327
|
+
const styled = input
|
|
328
|
+
? styleInput(source)
|
|
329
|
+
: hasAnsi ? sanitizeAnsiForThemedOutput(outputLine) : color(bodyColor, outputLine);
|
|
172
330
|
const lines = wrapTextWithAnsi(styled, contentWidth);
|
|
173
331
|
wrapped.push(...(lines.length ? lines : [styled]));
|
|
174
332
|
}
|
|
@@ -284,7 +442,17 @@ function renderFallbackResult(result: any, options: any, theme: any, context: an
|
|
|
284
442
|
const maxLines = Number.isFinite(context?.diffConfig?.expandedPreviewMaxLines)
|
|
285
443
|
? Math.max(1, Math.floor(context.diffConfig.expandedPreviewMaxLines))
|
|
286
444
|
: DEFAULT_TOOL_DISPLAY_CONFIG.expandedPreviewMaxLines;
|
|
287
|
-
|
|
445
|
+
const language = resolveToolOutputLanguage(context?.args, text);
|
|
446
|
+
return new ExpandedToolIoView(
|
|
447
|
+
input,
|
|
448
|
+
text,
|
|
449
|
+
Boolean(error),
|
|
450
|
+
maxLines,
|
|
451
|
+
maxLines,
|
|
452
|
+
theme,
|
|
453
|
+
language,
|
|
454
|
+
context?.invalidate,
|
|
455
|
+
);
|
|
288
456
|
}
|
|
289
457
|
const count = text ? text.split("\n").length : 0;
|
|
290
458
|
const summary = error ? oneLine(text) || "Failed" : count ? `${count} ${count === 1 ? "line" : "lines"} returned` : "Done";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Component } from "@earendil-works/pi-tui";
|
|
2
|
-
import type { ConversationBlock } from "./conversation-blocks.js";
|
|
3
2
|
import type { Theme } from "./agent-widget.js";
|
|
3
|
+
import type { ConversationBlock } from "./conversation-blocks.js";
|
|
4
4
|
|
|
5
5
|
export type ConversationRoleTheme = Pick<Theme, "fg">;
|
|
6
6
|
|
|
@@ -105,10 +105,10 @@ export function highlightConversationLine(
|
|
|
105
105
|
|
|
106
106
|
while (i < text.length) {
|
|
107
107
|
if (text[i] === "\x1b") {
|
|
108
|
-
const
|
|
109
|
-
if (
|
|
110
|
-
result +=
|
|
111
|
-
i +=
|
|
108
|
+
const ansiEscape = text.slice(i).match(ANSI_ESCAPE_RE);
|
|
109
|
+
if (ansiEscape) {
|
|
110
|
+
result += ansiEscape[0];
|
|
111
|
+
i += ansiEscape[0].length;
|
|
112
112
|
continue;
|
|
113
113
|
}
|
|
114
114
|
}
|
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
import * as CodingAgent from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import {
|
|
4
|
+
type Component,
|
|
3
5
|
Markdown,
|
|
4
6
|
Text,
|
|
5
|
-
type Component,
|
|
6
7
|
type TUI,
|
|
7
8
|
type TuiMouseEvent,
|
|
8
9
|
truncateToWidth,
|
|
9
10
|
} from "@earendil-works/pi-tui";
|
|
10
|
-
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import type { AgentRecord } from "../types.js";
|
|
12
12
|
import type { Theme } from "./agent-widget.js";
|
|
13
|
+
import { showMoreHintText } from "./ccstyle/diff/render-utils.js";
|
|
14
|
+
import {
|
|
15
|
+
createViewerCcstyleTool,
|
|
16
|
+
type ViewerToolRenderers,
|
|
17
|
+
} from "./ccstyle/tool-renderer.js";
|
|
13
18
|
import {
|
|
14
19
|
type ConversationBlock,
|
|
15
20
|
type ConversationToolResultSnapshot,
|
|
16
21
|
} from "./conversation-blocks.js";
|
|
17
22
|
import { renderConversationRoleHeader } from "./conversation-role.js";
|
|
18
23
|
import { stripAnsi } from "./conversation-search.js";
|
|
19
|
-
import {
|
|
20
|
-
createViewerCcstyleTool,
|
|
21
|
-
type ViewerToolRenderers,
|
|
22
|
-
} from "./ccstyle/tool-renderer.js";
|
|
23
|
-
import { showMoreHintText } from "./ccstyle/diff/render-utils.js";
|
|
24
24
|
|
|
25
25
|
export interface TimelineRenderLine {
|
|
26
26
|
text: string;
|
|
@@ -5,20 +5,20 @@
|
|
|
5
5
|
* blocks are formatted data, while Markdown/ANSI lines are a bounded cache.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { ExtensionAPI, ExtensionContext
|
|
8
|
+
import type { AgentSession, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { copyToClipboard } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { type Component, Input, Key, matchesKey, type TUI, type TuiMouseEvent, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
11
11
|
import type { AgentRecord } from "../types.js";
|
|
12
12
|
import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
|
|
13
13
|
import type { Theme } from "./agent-widget.js";
|
|
14
14
|
import { type AgentActivity, buildInvocationTags, describeActivity, fgPreservingNestedStyles, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel } from "./agent-widget.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { renderConversationRoleHeader } from "./conversation-role.js";
|
|
15
|
+
import { createViewerCcstyleResult } from "./ccstyle/tool-result.js";
|
|
16
|
+
import { type ConversationBlock, formatConversationMessages } from "./conversation-blocks.js";
|
|
18
17
|
import { editLiveAssistantBlockInNvim, viewConversationBlockInNvim } from "./conversation-nvim.js";
|
|
19
|
-
import {
|
|
18
|
+
import { renderConversationRoleHeader } from "./conversation-role.js";
|
|
19
|
+
import { type ConversationMatch, findConversationMatches, highlightConversationLine, nearestConversationMatchAfter, nearestConversationMatchBefore, stripAnsi } from "./conversation-search.js";
|
|
20
|
+
import { ConversationTimeline, type ConversationTimelineChange, type TimelineRenderLine } from "./conversation-timeline.js";
|
|
20
21
|
import { createViewerKeys, type ViewerKeybindings, type ViewerKeys } from "./viewer-keys.js";
|
|
21
|
-
import { createViewerCcstyleResult } from "./ccstyle/tool-result.js";
|
|
22
22
|
|
|
23
23
|
/** Base lines consumed by the overlay: borders, header, separator and footer. */
|
|
24
24
|
const CHROME_LINES_BASE = 6;
|
|
@@ -569,7 +569,16 @@ export class ConversationViewer implements Component {
|
|
|
569
569
|
this.timeline.setShowTools(this.showTools);
|
|
570
570
|
this.timeline.render(width);
|
|
571
571
|
const timelineSnapshot = this.timeline.getSnapshot();
|
|
572
|
-
const lines: RenderLine[] = timelineSnapshot.lines.map((line) =>
|
|
572
|
+
const lines: RenderLine[] = timelineSnapshot.lines.map((line) => {
|
|
573
|
+
const rawText = typeof line?.text === "string" ? line.text : "";
|
|
574
|
+
const text = truncateToWidth(rawText, width, "", true);
|
|
575
|
+
return {
|
|
576
|
+
text,
|
|
577
|
+
plain: stripAnsi(text),
|
|
578
|
+
blockIndex: typeof line?.blockIndex === "number" ? line.blockIndex : -1,
|
|
579
|
+
...(line?.railable ? { railable: true } : {}),
|
|
580
|
+
};
|
|
581
|
+
});
|
|
573
582
|
this.cachedMessageStarts = [...timelineSnapshot.messageStarts];
|
|
574
583
|
this.cachedMessageBlocks = [...timelineSnapshot.messageBlocks];
|
|
575
584
|
if (this.hasFocusedBlock) {
|
package/src/write-execution.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { createWriteToolDefinition, generateDiffString, generateUnifiedPatch, type ExtensionContext, type ToolDefinition, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
2
1
|
import { lstat, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
2
|
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
|
+
import { createWriteToolDefinition, type ExtensionContext, generateDiffString, generateUnifiedPatch, type ToolDefinition, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
|
|
5
5
|
/** Maximum UTF-8 size of the durable display diff plus unified patch. */
|
|
6
6
|
export const MAX_DURABLE_DIFF_BYTES = 4 * 1024 * 1024;
|
|
@@ -58,7 +58,7 @@ function resolveWritePath(path: string, cwd: string): string {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
async function capturePreviousContent(absolutePath: string): Promise<WriteExecutionMeta> {
|
|
61
|
-
let info
|
|
61
|
+
let info: Awaited<ReturnType<typeof lstat>>;
|
|
62
62
|
try {
|
|
63
63
|
info = await lstat(absolutePath);
|
|
64
64
|
} catch (error: any) {
|