pi-ui-extend 1.0.35 → 1.0.37
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/README.md +21 -0
- package/dist/app/rendering/conversation-entry-renderer.js +25 -3
- package/dist/markdown-format.d.ts +8 -0
- package/dist/markdown-format.js +262 -6
- package/dist/theme.d.ts +2 -0
- package/dist/theme.js +4 -0
- package/external/pi-tools-suite/README.md +14 -1
- package/external/pi-tools-suite/docs/session-recovery.md +84 -0
- package/external/pi-tools-suite/src/index.ts +1 -0
- package/external/pi-tools-suite/src/session-recovery/index.ts +674 -0
- package/external/pi-tools-suite/src/tool-descriptions.ts +40 -0
- package/package.json +5 -2
- package/skills/skill-creator/scripts/__pycache__/__init__.cpython-314.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/aggregate_benchmark.cpython-314.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/generate_report.cpython-314.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/improve_description.cpython-314.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/package_skill.cpython-314.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_eval.cpython-314.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/run_loop.cpython-314.pyc +0 -0
- package/skills/skill-creator/scripts/__pycache__/utils.cpython-314.pyc +0 -0
package/README.md
CHANGED
|
@@ -401,6 +401,27 @@ Configurable areas include:
|
|
|
401
401
|
|
|
402
402
|
Use `/settings` to inspect the effective settings summary and `/reload` after changing resources. Some command-driven settings update the relevant config file directly.
|
|
403
403
|
|
|
404
|
+
### Ignoring legacy context files
|
|
405
|
+
|
|
406
|
+
If a legacy project already contains an `AGENTS.md` or `CLAUDE.md` that should not be loaded, the simplest project-local workaround for both Pi and Pix is an empty override file in the same directory:
|
|
407
|
+
|
|
408
|
+
```bash
|
|
409
|
+
touch AGENTS.override.md
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Pi and Pix load `AGENTS.override.md` instead of `AGENTS.md`/`CLAUDE.md` from that directory. To keep this local without changing the repository:
|
|
413
|
+
|
|
414
|
+
```bash
|
|
415
|
+
echo AGENTS.override.md >> .git/info/exclude
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
To disable discovery of all context files, including files in parent directories:
|
|
419
|
+
|
|
420
|
+
- start Pi with `pi --no-context-files` (or `pi -nc`);
|
|
421
|
+
- in Pix, run `/no-context-files on`. Pix saves `"ignoreContextFiles": true` to `<workspace>/.pi/pix.jsonc`.
|
|
422
|
+
|
|
423
|
+
Start a new session or restart Pix after changing this setting. Use `/no-context-files off` to enable context-file loading again.
|
|
424
|
+
|
|
404
425
|
## Updates
|
|
405
426
|
|
|
406
427
|
```bash
|
|
@@ -28,10 +28,15 @@ export function renderConversationEntry(entry, width, options) {
|
|
|
28
28
|
const lines = renderMarkdownTextLines(userEntry.text, userContentWidth, userContentLeft).map((line) => ({
|
|
29
29
|
...userLine(line.text, userEntry.id, line.syntaxHighlight, [
|
|
30
30
|
...(line.segments ?? []),
|
|
31
|
+
...diagramStyledSegments(line.diagramSegments, options.colors),
|
|
31
32
|
...(line.links?.map((link) => ({ ...link, foreground: options.colors.info, underline: true })) ?? []),
|
|
32
33
|
], line.links),
|
|
33
34
|
...(line.copyText === undefined ? {} : { copyText: line.copyText }),
|
|
34
35
|
...(line.continuesOnNextLine ? { continuesOnNextLine: true } : {}),
|
|
36
|
+
...(line.codeBlock ? {
|
|
37
|
+
colorOverride: options.colors.codeBlockForeground,
|
|
38
|
+
backgroundOverride: options.colors.codeBlockBackground,
|
|
39
|
+
} : {}),
|
|
35
40
|
}));
|
|
36
41
|
return attachImageClickTargets(lines, userEntry.id, userEntry.images, { foreground: options.colors.info, underline: true });
|
|
37
42
|
};
|
|
@@ -94,13 +99,16 @@ function renderAssistantLines(text, width, options) {
|
|
|
94
99
|
const existingSegments = line.segments?.map((segment) => ({ ...segment, start: segment.start + contentLeft, end: segment.end + contentLeft })) ?? [];
|
|
95
100
|
const links = line.links?.map((link) => ({ ...link, start: link.start + contentLeft, end: link.end + contentLeft })) ?? [];
|
|
96
101
|
const linkSegments = links.map((link) => ({ start: link.start, end: link.end, foreground: options.colors.info, underline: true }));
|
|
97
|
-
const
|
|
102
|
+
const diagramSegments = diagramStyledSegments(line.diagramSegments, options.colors, contentLeft);
|
|
103
|
+
const allSegments = headingSegment
|
|
104
|
+
? [headingSegment, ...existingSegments, ...diagramSegments, ...linkSegments]
|
|
105
|
+
: [...existingSegments, ...diagramSegments, ...linkSegments];
|
|
98
106
|
lines.push({
|
|
99
107
|
text: padHorizontalText(line.text, width),
|
|
100
108
|
...(line.copyText === undefined ? {} : { copyText: line.copyText }),
|
|
101
109
|
...(line.continuesOnNextLine ? { continuesOnNextLine: true } : {}),
|
|
102
|
-
colorOverride: options.colors.assistantForeground,
|
|
103
|
-
backgroundOverride: options.colors.assistantMessageBackground,
|
|
110
|
+
colorOverride: line.codeBlock ? options.colors.codeBlockForeground : options.colors.assistantForeground,
|
|
111
|
+
backgroundOverride: line.codeBlock ? options.colors.codeBlockBackground : options.colors.assistantMessageBackground,
|
|
104
112
|
...(allSegments.length > 0 ? { segments: allSegments } : {}),
|
|
105
113
|
...(links.length > 0 ? { links } : {}),
|
|
106
114
|
...(line.syntaxHighlight ? { syntaxHighlight: line.syntaxHighlight } : {}),
|
|
@@ -108,3 +116,17 @@ function renderAssistantLines(text, width, options) {
|
|
|
108
116
|
}
|
|
109
117
|
return lines;
|
|
110
118
|
}
|
|
119
|
+
function diagramStyledSegments(segments, colors, offset = 0) {
|
|
120
|
+
return (segments ?? []).map((segment) => {
|
|
121
|
+
const range = { start: segment.start + offset, end: segment.end + offset };
|
|
122
|
+
switch (segment.class) {
|
|
123
|
+
case "border":
|
|
124
|
+
case "edgeLabel":
|
|
125
|
+
return { ...range, foreground: colors.muted };
|
|
126
|
+
case "edge":
|
|
127
|
+
return { ...range, foreground: colors.accent };
|
|
128
|
+
case "title":
|
|
129
|
+
return { ...range, foreground: colors.accent, bold: true };
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Cls as MermaidSpanClass } from "grok-mermaid";
|
|
1
2
|
import { type SyntaxLineHighlight, type ToolBodySyntaxHighlights } from "./syntax-highlight.js";
|
|
2
3
|
export type RenderedMarkdownLine = {
|
|
3
4
|
text: string;
|
|
@@ -22,6 +23,7 @@ export type RenderedMarkdownTextLine = {
|
|
|
22
23
|
text: string;
|
|
23
24
|
copyText?: string;
|
|
24
25
|
continuesOnNextLine?: boolean;
|
|
26
|
+
codeBlock?: true;
|
|
25
27
|
segments?: readonly {
|
|
26
28
|
start: number;
|
|
27
29
|
end: number;
|
|
@@ -29,8 +31,14 @@ export type RenderedMarkdownTextLine = {
|
|
|
29
31
|
}[] | undefined;
|
|
30
32
|
links?: readonly RenderedMarkdownLink[] | undefined;
|
|
31
33
|
syntaxHighlight?: SyntaxLineHighlight | undefined;
|
|
34
|
+
diagramSegments?: readonly RenderedMarkdownDiagramSegment[] | undefined;
|
|
32
35
|
heading?: boolean;
|
|
33
36
|
};
|
|
37
|
+
export type RenderedMarkdownDiagramSegment = {
|
|
38
|
+
start: number;
|
|
39
|
+
end: number;
|
|
40
|
+
class: Exclude<MermaidSpanClass, "none" | "text">;
|
|
41
|
+
};
|
|
34
42
|
export type RenderMarkdownTextLinesOptions = {
|
|
35
43
|
preserveWrappedWordSeparator?: boolean;
|
|
36
44
|
};
|
package/dist/markdown-format.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import { render as renderMermaid } from "grok-mermaid";
|
|
1
2
|
import { displayGraphemes, expandTabs, stringDisplayWidth } from "./terminal-width.js";
|
|
2
3
|
import { syntaxHighlightLanguageForMarkdownFence, } from "./syntax-highlight.js";
|
|
3
4
|
const MIN_TRAILING_WORD_WIDTH_TO_REBALANCE = 5;
|
|
5
|
+
const MERMAID_CACHE_LIMIT = 100;
|
|
6
|
+
const MERMAID_MAX_PANELS = 4;
|
|
7
|
+
const MERMAID_MIN_PANEL_WIDTH = 40;
|
|
8
|
+
const MERMAID_SEQUENCE_MESSAGE_OPERATORS = ["-->>", "->>", "--x", "-x", "--)", "-)", "-->", "->"];
|
|
9
|
+
const mermaidRenderCache = new Map();
|
|
4
10
|
export function formatMarkdownTables(text, maxWidth) {
|
|
5
11
|
const lines = text.split("\n");
|
|
6
12
|
const formatted = [];
|
|
@@ -85,14 +91,40 @@ export function renderMarkdownLine(text, start = 0) {
|
|
|
85
91
|
export function renderMarkdownTextLines(text, width, start = 0, options = {}) {
|
|
86
92
|
const lines = [];
|
|
87
93
|
let fence;
|
|
94
|
+
let fenceHasBodyLine = false;
|
|
88
95
|
const formattedText = formatMarkdownTables(sanitizeMarkdownText(text), width);
|
|
89
96
|
if (formattedText.length === 0)
|
|
90
97
|
return [];
|
|
91
|
-
|
|
98
|
+
const rawLines = formattedText.split("\n");
|
|
99
|
+
for (let lineIndex = 0; lineIndex < rawLines.length;) {
|
|
100
|
+
const rawLine = rawLines[lineIndex] ?? "";
|
|
92
101
|
const nextFence = markdownFence(rawLine);
|
|
102
|
+
if (!fence && nextFence && isMermaidFence(nextFence)) {
|
|
103
|
+
const diagram = renderMermaidBlock(rawLines, lineIndex, nextFence, width);
|
|
104
|
+
if (diagram) {
|
|
105
|
+
lines.push(...diagram.lines);
|
|
106
|
+
lineIndex = diagram.nextLineIndex;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
93
110
|
const closesFence = Boolean(fence && nextFence && fence.marker === nextFence.marker && nextFence.length >= fence.length);
|
|
94
111
|
const opensFence = !fence && nextFence !== undefined;
|
|
95
|
-
|
|
112
|
+
if (opensFence && nextFence) {
|
|
113
|
+
fence = { ...nextFence, language: syntaxHighlightLanguageForMarkdownFence(nextFence.info) };
|
|
114
|
+
fenceHasBodyLine = false;
|
|
115
|
+
lineIndex += 1;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (closesFence) {
|
|
119
|
+
if (!fenceHasBodyLine)
|
|
120
|
+
lines.push({ text: "", codeBlock: true });
|
|
121
|
+
fence = undefined;
|
|
122
|
+
fenceHasBodyLine = false;
|
|
123
|
+
lineIndex += 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const syntaxHighlight = markdownLineSyntaxHighlight(fence, false, start);
|
|
127
|
+
const codeBlock = fence !== undefined;
|
|
96
128
|
const isHeadingLine = !fence && /^\s{0,3}#{1,6}\s/.test(rawLine);
|
|
97
129
|
const markdownLine = syntaxHighlight?.language === "markdown" || isHeadingLine ? renderMarkdownLine(rawLine) : undefined;
|
|
98
130
|
const logicalLine = markdownLine ?? { text: rawLine, segments: [], links: [] };
|
|
@@ -113,21 +145,245 @@ export function renderMarkdownTextLines(text, width, start = 0, options = {}) {
|
|
|
113
145
|
text: wrapped.text,
|
|
114
146
|
...(wrapped.copyText === undefined ? {} : { copyText: wrapped.copyText }),
|
|
115
147
|
...(wrapped.continuesOnNextLine ? { continuesOnNextLine: true } : {}),
|
|
148
|
+
...(codeBlock ? { codeBlock: true } : {}),
|
|
116
149
|
...(wrapped.segments.length > 0 ? { segments: wrapped.segments } : {}),
|
|
117
150
|
...(wrapped.links && wrapped.links.length > 0 ? { links: wrapped.links } : {}),
|
|
118
151
|
...(wrappedSyntaxHighlight ? { syntaxHighlight: wrappedSyntaxHighlight } : {}),
|
|
119
152
|
...(isHeadingLine ? { heading: true } : {}),
|
|
120
153
|
});
|
|
121
154
|
}
|
|
122
|
-
if (
|
|
123
|
-
|
|
155
|
+
if (codeBlock)
|
|
156
|
+
fenceHasBodyLine = true;
|
|
157
|
+
lineIndex += 1;
|
|
158
|
+
}
|
|
159
|
+
if (fence && !fenceHasBodyLine)
|
|
160
|
+
lines.push({ text: "", codeBlock: true });
|
|
161
|
+
return lines;
|
|
162
|
+
}
|
|
163
|
+
function renderMermaidBlock(rawLines, openingLineIndex, openingFence, width) {
|
|
164
|
+
let closingLineIndex;
|
|
165
|
+
for (let index = openingLineIndex + 1; index < rawLines.length; index += 1) {
|
|
166
|
+
const candidate = markdownFence(rawLines[index] ?? "");
|
|
167
|
+
if (candidate && candidate.marker === openingFence.marker && candidate.length >= openingFence.length) {
|
|
168
|
+
closingLineIndex = index;
|
|
169
|
+
break;
|
|
124
170
|
}
|
|
125
|
-
|
|
126
|
-
|
|
171
|
+
}
|
|
172
|
+
const sourceEnd = closingLineIndex ?? rawLines.length;
|
|
173
|
+
const source = rawLines.slice(openingLineIndex + 1, sourceEnd).join("\n");
|
|
174
|
+
let art = cachedMermaidRender(source);
|
|
175
|
+
if (!art)
|
|
176
|
+
return undefined;
|
|
177
|
+
const availableWidth = Math.max(1, width);
|
|
178
|
+
let participantLegend = [];
|
|
179
|
+
let messageLegend = [];
|
|
180
|
+
if (art.width > availableWidth) {
|
|
181
|
+
const compactSequence = compactMermaidSequence(source);
|
|
182
|
+
if (compactSequence) {
|
|
183
|
+
const compactArt = cachedMermaidRender(compactSequence.source);
|
|
184
|
+
if (compactArt && compactArt.width < art.width) {
|
|
185
|
+
art = compactArt;
|
|
186
|
+
participantLegend = compactSequence.participantLegend;
|
|
187
|
+
messageLegend = compactSequence.messageLegend;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const panelStarts = mermaidPanelStarts(art.width, availableWidth);
|
|
192
|
+
if (panelStarts.length > 1 && (availableWidth < MERMAID_MIN_PANEL_WIDTH || panelStarts.length > MERMAID_MAX_PANELS)) {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
lines: [
|
|
197
|
+
...mermaidArtLines(art, availableWidth, panelStarts),
|
|
198
|
+
...mermaidLegendLines("Participants", participantLegend, availableWidth),
|
|
199
|
+
...mermaidLegendLines("Messages", messageLegend, availableWidth),
|
|
200
|
+
],
|
|
201
|
+
nextLineIndex: closingLineIndex === undefined ? rawLines.length : closingLineIndex + 1,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function compactMermaidSequence(source) {
|
|
205
|
+
const lines = source.split("\n");
|
|
206
|
+
const header = lines.find((line) => line.trim().length > 0 && !line.trimStart().startsWith("%%"));
|
|
207
|
+
if (!header || !/^sequenceDiagram\s*$/iu.test(header.trim()))
|
|
208
|
+
return undefined;
|
|
209
|
+
const participantLegend = [];
|
|
210
|
+
const messageLegend = [];
|
|
211
|
+
const autonumber = lines.some((line) => /^\s*autonumber(?:\s|$)/iu.test(line));
|
|
212
|
+
let participantNumber = 0;
|
|
213
|
+
let messageNumber = 0;
|
|
214
|
+
const compactLines = lines.map((line) => {
|
|
215
|
+
const match = /^(\s*)(participant|actor)\s+(\S+?)(?:\s+as\s+(.+?))?\s*$/iu.exec(line);
|
|
216
|
+
if (match) {
|
|
217
|
+
const [, indent = "", declaration = "participant", id = "", rawLabel] = match;
|
|
218
|
+
participantNumber += 1;
|
|
219
|
+
const marker = `P${participantNumber}`;
|
|
220
|
+
const label = rawLabel ? normalizeMermaidLegendLabel(rawLabel) : id;
|
|
221
|
+
participantLegend.push(`${marker} (${id}) — ${label}`);
|
|
222
|
+
return `${indent}${declaration} ${id} as ${marker}`;
|
|
223
|
+
}
|
|
224
|
+
const message = mermaidSequenceMessage(line);
|
|
225
|
+
if (!message)
|
|
226
|
+
return line;
|
|
227
|
+
messageNumber += 1;
|
|
228
|
+
if (message.label.length === 0)
|
|
229
|
+
return line;
|
|
230
|
+
messageLegend.push(`${messageNumber}. ${normalizeMermaidLegendLabel(message.label)}`);
|
|
231
|
+
return autonumber ? message.prefix : `${message.prefix}: [${messageNumber}]`;
|
|
232
|
+
});
|
|
233
|
+
return participantLegend.length > 0 || messageLegend.length > 0
|
|
234
|
+
? { source: compactLines.join("\n"), participantLegend, messageLegend }
|
|
235
|
+
: undefined;
|
|
236
|
+
}
|
|
237
|
+
function mermaidSequenceMessage(line) {
|
|
238
|
+
for (const operator of MERMAID_SEQUENCE_MESSAGE_OPERATORS) {
|
|
239
|
+
const operatorIndex = line.indexOf(operator);
|
|
240
|
+
if (operatorIndex < 0)
|
|
241
|
+
continue;
|
|
242
|
+
const colonIndex = line.indexOf(":", operatorIndex + operator.length);
|
|
243
|
+
const targetEnd = colonIndex < 0 ? line.length : colonIndex;
|
|
244
|
+
const from = line.slice(0, operatorIndex).trim();
|
|
245
|
+
const to = line.slice(operatorIndex + operator.length, targetEnd).trim();
|
|
246
|
+
if (!from || !to || /\s/u.test(from) || /\s/u.test(to))
|
|
247
|
+
return undefined;
|
|
248
|
+
return {
|
|
249
|
+
prefix: line.slice(0, targetEnd).trimEnd(),
|
|
250
|
+
label: colonIndex < 0 ? "" : line.slice(colonIndex + 1).trim(),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return undefined;
|
|
254
|
+
}
|
|
255
|
+
function normalizeMermaidLegendLabel(label) {
|
|
256
|
+
const normalized = label
|
|
257
|
+
.replace(/<br\s*\/?\s*>/giu, " ")
|
|
258
|
+
.replace(/<\/?[A-Za-z][^>]*>/gu, "")
|
|
259
|
+
.replace(/^(?:"([\s\S]*)"|'([\s\S]*)')$/u, "$1$2")
|
|
260
|
+
.trim();
|
|
261
|
+
return normalized || label.trim();
|
|
262
|
+
}
|
|
263
|
+
function mermaidPanelStarts(artWidth, panelWidth) {
|
|
264
|
+
if (artWidth <= panelWidth)
|
|
265
|
+
return [0];
|
|
266
|
+
const overlap = Math.min(24, Math.max(8, Math.floor(panelWidth / 5)));
|
|
267
|
+
const stride = Math.max(1, panelWidth - overlap);
|
|
268
|
+
const panelCount = 1 + Math.ceil((artWidth - panelWidth) / stride);
|
|
269
|
+
return Array.from({ length: panelCount }, (_, index) => Math.min(index * stride, artWidth - panelWidth));
|
|
270
|
+
}
|
|
271
|
+
function mermaidArtLines(art, width, panelStarts) {
|
|
272
|
+
if (panelStarts.length === 1) {
|
|
273
|
+
return art.plain.map((text, rowIndex) => {
|
|
274
|
+
const diagramSegments = mermaidDiagramSegments(text, art.styled[rowIndex] ?? []);
|
|
275
|
+
return { text, ...(diagramSegments.length > 0 ? { diagramSegments } : {}) };
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
const lines = [];
|
|
279
|
+
for (const [panelIndex, startColumn] of panelStarts.entries()) {
|
|
280
|
+
const continuesLeft = startColumn > 0;
|
|
281
|
+
const continuesRight = startColumn + width < art.width;
|
|
282
|
+
const title = `Mermaid ${panelIndex + 1}/${panelStarts.length}${continuesLeft ? " ←" : ""}${continuesRight ? " →" : ""}`;
|
|
283
|
+
lines.push({
|
|
284
|
+
text: title,
|
|
285
|
+
diagramSegments: [{ start: 0, end: title.length, class: "title" }],
|
|
286
|
+
});
|
|
287
|
+
for (const [rowIndex, text] of art.plain.entries()) {
|
|
288
|
+
lines.push(sliceMermaidDiagramRow(text, art.styled[rowIndex] ?? [], startColumn, width));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return lines;
|
|
292
|
+
}
|
|
293
|
+
function mermaidLegendLines(title, entries, width) {
|
|
294
|
+
if (entries.length === 0)
|
|
295
|
+
return [];
|
|
296
|
+
const lines = [{
|
|
297
|
+
text: title,
|
|
298
|
+
diagramSegments: [{ start: 0, end: title.length, class: "title" }],
|
|
299
|
+
}];
|
|
300
|
+
for (const entry of entries) {
|
|
301
|
+
for (const wrapped of wrapRenderedMarkdownLine({ text: entry, segments: [], links: [] }, width, {})) {
|
|
302
|
+
lines.push({
|
|
303
|
+
text: wrapped.text,
|
|
304
|
+
...(wrapped.continuesOnNextLine ? { continuesOnNextLine: true } : {}),
|
|
305
|
+
});
|
|
127
306
|
}
|
|
128
307
|
}
|
|
129
308
|
return lines;
|
|
130
309
|
}
|
|
310
|
+
function sliceMermaidDiagramRow(text, spans, startColumn, width) {
|
|
311
|
+
const endColumn = startColumn + width;
|
|
312
|
+
const diagramSegments = [];
|
|
313
|
+
let result = "";
|
|
314
|
+
let column = 0;
|
|
315
|
+
let spanIndex = 0;
|
|
316
|
+
let spanEnd = spans[0]?.text.length ?? 0;
|
|
317
|
+
for (const grapheme of displayGraphemes(text)) {
|
|
318
|
+
const nextColumn = column + grapheme.width;
|
|
319
|
+
if (nextColumn <= startColumn) {
|
|
320
|
+
column = nextColumn;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (column >= endColumn)
|
|
324
|
+
break;
|
|
325
|
+
while (spanIndex < spans.length - 1 && grapheme.start >= spanEnd) {
|
|
326
|
+
spanIndex += 1;
|
|
327
|
+
spanEnd += spans[spanIndex]?.text.length ?? 0;
|
|
328
|
+
}
|
|
329
|
+
const fullyVisible = column >= startColumn && nextColumn <= endColumn;
|
|
330
|
+
const chunk = fullyVisible
|
|
331
|
+
? grapheme.text
|
|
332
|
+
: " ".repeat(Math.max(0, Math.min(nextColumn, endColumn) - Math.max(column, startColumn)));
|
|
333
|
+
const chunkStart = result.length;
|
|
334
|
+
result += chunk;
|
|
335
|
+
const spanClass = spans[spanIndex]?.cls;
|
|
336
|
+
if (fullyVisible && spanClass && spanClass !== "none" && spanClass !== "text") {
|
|
337
|
+
const previous = diagramSegments.at(-1);
|
|
338
|
+
if (previous && previous.class === spanClass && previous.end === chunkStart) {
|
|
339
|
+
previous.end = result.length;
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
diagramSegments.push({ start: chunkStart, end: result.length, class: spanClass });
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
column = nextColumn;
|
|
346
|
+
}
|
|
347
|
+
return { text: result, ...(diagramSegments.length > 0 ? { diagramSegments } : {}) };
|
|
348
|
+
}
|
|
349
|
+
function cachedMermaidRender(source) {
|
|
350
|
+
if (mermaidRenderCache.has(source)) {
|
|
351
|
+
const cached = mermaidRenderCache.get(source) ?? null;
|
|
352
|
+
mermaidRenderCache.delete(source);
|
|
353
|
+
mermaidRenderCache.set(source, cached);
|
|
354
|
+
return cached;
|
|
355
|
+
}
|
|
356
|
+
let art;
|
|
357
|
+
try {
|
|
358
|
+
art = renderMermaid(source.replace(/<br\s*\/?\s*>/giu, " "));
|
|
359
|
+
}
|
|
360
|
+
catch {
|
|
361
|
+
art = null;
|
|
362
|
+
}
|
|
363
|
+
mermaidRenderCache.set(source, art);
|
|
364
|
+
if (mermaidRenderCache.size > MERMAID_CACHE_LIMIT) {
|
|
365
|
+
const oldestKey = mermaidRenderCache.keys().next().value;
|
|
366
|
+
if (oldestKey !== undefined)
|
|
367
|
+
mermaidRenderCache.delete(oldestKey);
|
|
368
|
+
}
|
|
369
|
+
return art;
|
|
370
|
+
}
|
|
371
|
+
function mermaidDiagramSegments(text, spans) {
|
|
372
|
+
const segments = [];
|
|
373
|
+
let start = 0;
|
|
374
|
+
for (const span of spans) {
|
|
375
|
+
const end = start + span.text.length;
|
|
376
|
+
if (start < text.length && span.cls !== "none" && span.cls !== "text") {
|
|
377
|
+
segments.push({ start, end: Math.min(end, text.length), class: span.cls });
|
|
378
|
+
}
|
|
379
|
+
start = end;
|
|
380
|
+
}
|
|
381
|
+
return segments;
|
|
382
|
+
}
|
|
383
|
+
function isMermaidFence(fence) {
|
|
384
|
+
const token = fence.info.trim().split(/\s+/, 1)[0]?.toLowerCase().replace(/^[{.]+|[}.]+$/gu, "") ?? "";
|
|
385
|
+
return token === "mermaid";
|
|
386
|
+
}
|
|
131
387
|
export function markdownSyntaxHighlightsForText(text, startColumn = 0) {
|
|
132
388
|
const highlights = [];
|
|
133
389
|
let fence;
|
package/dist/theme.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export type Theme = {
|
|
|
18
18
|
assistantMessageBackground: string;
|
|
19
19
|
userMessageBackground: string;
|
|
20
20
|
thinkingMessageBackground: string;
|
|
21
|
+
codeBlockForeground: string;
|
|
22
|
+
codeBlockBackground: string;
|
|
21
23
|
inputCursorBackground: string;
|
|
22
24
|
popupForeground: string;
|
|
23
25
|
popupBackground: string;
|
package/dist/theme.js
CHANGED
|
@@ -19,6 +19,8 @@ export const THEMES = {
|
|
|
19
19
|
assistantMessageBackground: "",
|
|
20
20
|
userMessageBackground: "",
|
|
21
21
|
thinkingMessageBackground: "",
|
|
22
|
+
codeBlockForeground: "#e6edf3",
|
|
23
|
+
codeBlockBackground: "#2a2f36",
|
|
22
24
|
inputCursorBackground: "#7fb3c8",
|
|
23
25
|
popupForeground: "#e6edf3",
|
|
24
26
|
popupBackground: "#1e1e1e",
|
|
@@ -75,6 +77,8 @@ export const THEMES = {
|
|
|
75
77
|
assistantMessageBackground: "",
|
|
76
78
|
userMessageBackground: "",
|
|
77
79
|
thinkingMessageBackground: "",
|
|
80
|
+
codeBlockForeground: "#0f172a",
|
|
81
|
+
codeBlockBackground: "#f1f5f9",
|
|
78
82
|
inputCursorBackground: "#0284c7",
|
|
79
83
|
popupForeground: "#0f172a",
|
|
80
84
|
popupBackground: "#ffffff",
|
|
@@ -11,6 +11,7 @@ This package keeps shared Pi tools as ordinary source folders under `src/` and r
|
|
|
11
11
|
- `src/lsp` — shared LSP diagnostics hook/library that enriches mutating tool results with diagnostics and shuts down language servers on session shutdown
|
|
12
12
|
- `src/comment-checker` — AI-slop comment guard that listens to the `tool_result` event for `write` / `edit` / `apply_patch` mutations, extracts net-new code comment lines, classifies them (filler phrasing, restating code, decorative separators, generic paraphrasing, or — under aggressive strictness — any non-valuable comment), and appends a short nudge to the tool result so the agent removes unnecessary comments on its next turn; TODO/FIXME, license headers, docstrings, pragmas, linter directives, shebangs, and decorators are never flagged; language-agnostic across `//` / `/* */` / `#` / `--` / `<!-- -->` / triple-quote comment styles; per-session deduplication (at most one nudge per 30 s) prevents fix/remark loops; configured via the `commentChecker` section (`enabled`, `strictness`: `conservative` | `balanced` | `aggressive`, default `balanced`) or `PI_COMMENT_CHECKER_ENABLED` / `PI_COMMENT_CHECKER_STRICTNESS`
|
|
13
13
|
- `src/session-name` — `session_name` tool for reading or setting the current session title directly from tool calls, without relying on slash-command parsing
|
|
14
|
+
- `src/session-recovery` — branch- and compaction-aware `session_overview`, `session_read_section`, `session_search`, and `session_recovery_context` tools for bounded recovery from Pi's raw append-only session history
|
|
14
15
|
- `src/repo-discovery` — `/idx-init`, `/idx-update`, and indexed-only `repo_architecture` / `repo_structure` / `repo_ast` / `repo_search` / `repo_explain` / `repo_deps`; tools register only when the launch project has `.indexer-cli`
|
|
15
16
|
- `src/antigravity-auth` — `antigravity` custom provider with Google Antigravity OAuth login, startup account list, auth.json-only runtime account loading, `/antigravity-add-account` OAuth append into rotation, `/antigravity-account` status display, account rotation/failover, Antigravity plus Gemini CLI model registration, and streaming through the Cloud Code Assist unified gateway
|
|
16
17
|
- `src/opencode-import` — `/opencode-import` for bounded migration of supported OpenCode OpenAI/Codex, GitHub Copilot, Z.ai, and Antigravity credentials into Pi; existing entries are preserved unless `--force` is passed
|
|
@@ -24,7 +25,19 @@ This package keeps shared Pi tools as ordinary source folders under `src/` and r
|
|
|
24
25
|
|
|
25
26
|
`index.ts` is intentionally only a thin auto-discovery shim that re-exports `src/index.ts`. There is no `pi.extensions` manifest here, so local Pi auto-discovery loads the suite once via `~/.pi/agent/extensions/pi-tools-suite/index.ts` and does not double-register tools.
|
|
26
27
|
|
|
27
|
-
Registration order is preserved in `src/index.ts`: coding-discipline, ast-grep, async-subagents, lsp, comment-checker, session-name, repo-discovery command/tool gate, antigravity-auth provider, OpenCode import, todo, model-tools, usage, web-search, dcp, prompt-commands, skill-installer, credential-firewall, then codex-reasoning-fix. Tool metadata and active model-specific tool sets have two modes: standard and repo-aware. When `.indexer-cli` enables `repo_*`, those tools stay active ahead of overlapping lower-level aliases so the indexed discovery surface has priority.
|
|
28
|
+
Registration order is preserved in `src/index.ts`: coding-discipline, ast-grep, async-subagents, lsp, comment-checker, session-name, session-recovery, repo-discovery command/tool gate, antigravity-auth provider, OpenCode import, todo, model-tools, usage, web-search, dcp, prompt-commands, skill-installer, credential-firewall, then codex-reasoning-fix. Tool metadata and active model-specific tool sets have two modes: standard and repo-aware. When `.indexer-cli` enables `repo_*`, those tools stay active ahead of overlapping lower-level aliases so the indexed discovery surface has priority.
|
|
29
|
+
|
|
30
|
+
## Session recovery
|
|
31
|
+
|
|
32
|
+
When context compaction obscures the task, start with `session_overview`, inspect a
|
|
33
|
+
relevant ID with `session_read_section`, and use `session_search` once a concrete
|
|
34
|
+
phrase, path, symbol, tool, or error is known. `session_recovery_context` is the
|
|
35
|
+
compact convenience view for original/latest user instructions, file evidence,
|
|
36
|
+
recent errors, pending calls, and the last meaningful action. All four tools read
|
|
37
|
+
through Pi's active `SessionManager`; they do not accept arbitrary session paths.
|
|
38
|
+
They default to the active branch, while `scope: "all"` includes abandoned branches.
|
|
39
|
+
See [`docs/session-recovery.md`](docs/session-recovery.md) for the full contract and
|
|
40
|
+
limits.
|
|
28
41
|
|
|
29
42
|
## Disabling modules
|
|
30
43
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Session recovery tools
|
|
2
|
+
|
|
3
|
+
Status: implemented MVP contract (semantic search is intentionally deferred).
|
|
4
|
+
|
|
5
|
+
## Goal
|
|
6
|
+
|
|
7
|
+
Let an agent recover the task, recent instructions, file activity, and useful raw
|
|
8
|
+
session history after context compaction. Recovery reads the session already
|
|
9
|
+
owned by Pi's `SessionManager`; it never opens an arbitrary session path.
|
|
10
|
+
|
|
11
|
+
## Scope
|
|
12
|
+
|
|
13
|
+
The `session-recovery` module registers four read-only, headless tools:
|
|
14
|
+
|
|
15
|
+
- `session_overview` maps a session into stable, bounded sections.
|
|
16
|
+
- `session_read_section` renders one section by ID.
|
|
17
|
+
- `session_search` performs bounded lexical search over raw entries.
|
|
18
|
+
- `session_recovery_context` summarizes deterministic recovery signals.
|
|
19
|
+
|
|
20
|
+
Every tool defaults to the active root-to-leaf branch. `scope: "all"` includes
|
|
21
|
+
abandoned branches through `SessionManager.getEntries()`. Both modes use raw
|
|
22
|
+
append-only entries, not `buildContextEntries()`, so content hidden from the
|
|
23
|
+
active model context by compaction remains discoverable.
|
|
24
|
+
|
|
25
|
+
## Contracts
|
|
26
|
+
|
|
27
|
+
### Sections
|
|
28
|
+
|
|
29
|
+
A section starts at the first selected entry, a user message, a compaction, or a
|
|
30
|
+
branch summary. Its stable ID is derived from the start entry ID. The overview
|
|
31
|
+
reports bounded head and tail sections with entry ranges, counts, and compact
|
|
32
|
+
role/tool/error/file statistics. Labels are previews, not inferred decisions.
|
|
33
|
+
|
|
34
|
+
### Reading and search
|
|
35
|
+
|
|
36
|
+
`session_read_section` requires a section ID produced for the same scope. It
|
|
37
|
+
renders message roles and text, tool calls and arguments, tool results,
|
|
38
|
+
compaction summaries, and branch summaries with per-entry and total output
|
|
39
|
+
limits.
|
|
40
|
+
|
|
41
|
+
`session_search` is case-insensitive by default and searches message text, tool
|
|
42
|
+
arguments/results, custom-message content, and compaction or branch summaries.
|
|
43
|
+
It returns entry and section IDs plus bounded snippets. Regex and semantic
|
|
44
|
+
search are out of scope for the MVP.
|
|
45
|
+
|
|
46
|
+
### Recovery context
|
|
47
|
+
|
|
48
|
+
`session_recovery_context` reports only evidence that can be derived
|
|
49
|
+
deterministically: the original and latest user messages, recent tool errors,
|
|
50
|
+
unmatched tool calls, read and modified files, the last meaningful action, and
|
|
51
|
+
compaction count. It calls errors `recentErrors`; it does not claim that they
|
|
52
|
+
remain unresolved. It does not infer decisions. The currently executing
|
|
53
|
+
recovery tool call is excluded from unmatched-call reporting.
|
|
54
|
+
|
|
55
|
+
File activity comes from recognized tool calls and Pi-generated compaction or
|
|
56
|
+
branch-summary details. Read and modified paths remain separate, and unknown
|
|
57
|
+
tools are not guessed to be mutations.
|
|
58
|
+
|
|
59
|
+
## Limits and edge cases
|
|
60
|
+
|
|
61
|
+
- Results use small defaults and hard caps for result count, entry body size,
|
|
62
|
+
and total text size.
|
|
63
|
+
- Empty or in-memory sessions return a normal explanatory result.
|
|
64
|
+
- Unknown or partially shaped entries are ignored or rendered conservatively.
|
|
65
|
+
- Concurrent sibling tool results might not yet be visible when recovery runs.
|
|
66
|
+
- A section ID is stable while its start entry ID is stable, but scope changes
|
|
67
|
+
can change section membership.
|
|
68
|
+
- Parent-session metadata is reported when Pi exposes it; parent files are not
|
|
69
|
+
traversed.
|
|
70
|
+
|
|
71
|
+
## Verification
|
|
72
|
+
|
|
73
|
+
Deterministic tests cover active versus all branches, raw pre-compaction search,
|
|
74
|
+
stable section IDs, Unicode case-insensitive search, bounded output, empty
|
|
75
|
+
sessions, current-call exclusion, recent errors, file carry-forward details,
|
|
76
|
+
and conservative handling of unknown entries. Release verification runs the
|
|
77
|
+
suite typecheck/tests/smoke gate, host `npm run check`, and then syncs the suite
|
|
78
|
+
with `npm run sync:pi-tools-suite`.
|
|
79
|
+
|
|
80
|
+
## Evidence
|
|
81
|
+
|
|
82
|
+
Evidence is recorded by the implementation tests in
|
|
83
|
+
`test/session-recovery.test.ts` and the verification commands reported with the
|
|
84
|
+
change.
|
|
@@ -16,6 +16,7 @@ export const MODULES: Array<{ name: string; load: () => Promise<ExtensionModule>
|
|
|
16
16
|
{ name: "lsp", load: () => import("./lsp/index") },
|
|
17
17
|
{ name: "comment-checker", load: () => import("./comment-checker/index") },
|
|
18
18
|
{ name: "session-name", load: () => import("./session-name/index") },
|
|
19
|
+
{ name: "session-recovery", load: () => import("./session-recovery/index") },
|
|
19
20
|
{ name: "repo-discovery", load: () => import("./repo-discovery/index") },
|
|
20
21
|
{ name: "antigravity-auth", load: () => import("./antigravity-auth/index") },
|
|
21
22
|
{ name: "opencode-import", load: () => import("./opencode-import/index") },
|