@zigai/pi-response-renderer 0.1.1
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 +11 -0
- package/package.json +34 -0
- package/src/index.ts +224 -0
package/README.md
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zigai/pi-response-renderer",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Pi package for personal response rendering tweaks.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-extension",
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi-ui",
|
|
9
|
+
"rendering"
|
|
10
|
+
],
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/zigai/pi-ui-tweaks.git",
|
|
14
|
+
"directory": "packages/pi-response-renderer"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"src",
|
|
18
|
+
"README.md",
|
|
19
|
+
"*.json"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
27
|
+
"@earendil-works/pi-tui": "*"
|
|
28
|
+
},
|
|
29
|
+
"pi": {
|
|
30
|
+
"extensions": [
|
|
31
|
+
"./src/index.ts"
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { Markdown, type Component } from "@earendil-works/pi-tui";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
const MARKDOWN_FENCES_PATCH_KEY = Symbol.for("zigai.pi-ui-tweaks.markdown-fences-patched");
|
|
6
|
+
const ESC = String.fromCharCode(0x1b);
|
|
7
|
+
const BEL = String.fromCharCode(0x07);
|
|
8
|
+
const ANSI_OSC_REGEX = new RegExp(`${ESC}\\][^${BEL}]*${BEL}`, "g");
|
|
9
|
+
const ANSI_CSI_REGEX = new RegExp(`${ESC}\\[[0-9;?]*[ -/]*[@-~]`, "g");
|
|
10
|
+
const ANSI_SGR_REGEX = new RegExp(`${ESC}\\[([0-9;]*)m`, "g");
|
|
11
|
+
|
|
12
|
+
const fencesHiddenInstances = new WeakSet<object>();
|
|
13
|
+
|
|
14
|
+
type PatchState = typeof globalThis & {
|
|
15
|
+
[MARKDOWN_FENCES_PATCH_KEY]?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type AssistantMessageComponentInstance = Component & {
|
|
19
|
+
contentContainer?: {
|
|
20
|
+
addChild(component: Component): void;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type AssistantMessageComponentPrototype = {
|
|
25
|
+
render(this: AssistantMessageComponentInstance, width: number): string[];
|
|
26
|
+
updateContent(this: AssistantMessageComponentInstance, message: unknown): void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
function stripAnsi(text: string): string {
|
|
30
|
+
return text.replace(ANSI_OSC_REGEX, "").replace(ANSI_CSI_REGEX, "");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function stripItalicAnsi(text: string): string {
|
|
34
|
+
return text.replace(ANSI_SGR_REGEX, (_match, params: string) => {
|
|
35
|
+
let codes: string[] = [];
|
|
36
|
+
if (params.length > 0) {
|
|
37
|
+
codes = params.split(";").filter((code) => code.length > 0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const filtered = codes.filter((code) => code !== "3" && code !== "23");
|
|
41
|
+
if (filtered.length === 0) {
|
|
42
|
+
return "";
|
|
43
|
+
}
|
|
44
|
+
return `\u001b[${filtered.join(";")}m`;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isFenceLine(line: string): boolean {
|
|
49
|
+
return /^```[a-zA-Z0-9]*$/.test(stripAnsi(line).trim());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isBlankRenderedLine(line: string): boolean {
|
|
53
|
+
return stripAnsi(line).trim().length === 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isIntroLine(line: string): boolean {
|
|
57
|
+
return stripAnsi(line).trimEnd().endsWith(":");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isIntroducedBlockLine(line: string): boolean {
|
|
61
|
+
const plainLine = stripAnsi(line);
|
|
62
|
+
const trimmedStart = plainLine.trimStart();
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
/^[-*+]\s+/.test(trimmedStart) ||
|
|
66
|
+
/^\d+[.)]\s+/.test(trimmedStart) ||
|
|
67
|
+
trimmedStart.startsWith("```") ||
|
|
68
|
+
trimmedStart.startsWith("|") ||
|
|
69
|
+
/^ {2,}\S/.test(plainLine)
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isMicroHeadingLine(line: string): boolean {
|
|
74
|
+
const text = stripAnsi(line).trim();
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
text.length > 0 &&
|
|
78
|
+
text.length <= 48 &&
|
|
79
|
+
!/[.!?;:]$/.test(text) &&
|
|
80
|
+
!/^[-*+]\s+/.test(text) &&
|
|
81
|
+
!/^\d+[.)]\s+/.test(text) &&
|
|
82
|
+
!text.startsWith("|") &&
|
|
83
|
+
!text.startsWith("```")
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isPlainParagraphLine(line: string): boolean {
|
|
88
|
+
return (
|
|
89
|
+
!isBlankRenderedLine(line) &&
|
|
90
|
+
!isMicroHeadingLine(line) &&
|
|
91
|
+
!isIntroLine(line) &&
|
|
92
|
+
!isIntroducedBlockLine(line)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function shouldCollapseBlankLine(lines: string[], index: number): boolean {
|
|
97
|
+
const previousLine = lines[index - 1];
|
|
98
|
+
const nextLine = lines[index + 1];
|
|
99
|
+
if (previousLine === undefined || nextLine === undefined) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (isIntroLine(previousLine) && isIntroducedBlockLine(nextLine)) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return isPlainParagraphLine(previousLine) && isPlainParagraphLine(nextLine);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function collapseAssistantBlankLines(lines: string[]): string[] {
|
|
111
|
+
return lines.filter((line, index) => {
|
|
112
|
+
if (!isBlankRenderedLine(line)) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return !shouldCollapseBlankLine(lines, index);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function shouldHideFences(instance: object): boolean {
|
|
121
|
+
return fencesHiddenInstances.has(instance);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function markFencesHidden(instance: object): void {
|
|
125
|
+
fencesHiddenInstances.add(instance);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getPatchState(): PatchState {
|
|
129
|
+
return globalThis as PatchState;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function resolvePiDistDir(): Promise<string> {
|
|
133
|
+
const codingAgentEntry = fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
|
|
134
|
+
return dirname(codingAgentEntry);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function patchMarkdownFences(): Promise<void> {
|
|
138
|
+
const state = getPatchState();
|
|
139
|
+
if (state[MARKDOWN_FENCES_PATCH_KEY] === true) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
state[MARKDOWN_FENCES_PATCH_KEY] = true;
|
|
143
|
+
|
|
144
|
+
const markdownPrototype = Markdown.prototype as {
|
|
145
|
+
render(width: number): string[];
|
|
146
|
+
};
|
|
147
|
+
const originalMarkdownRender = Reflect.get(markdownPrototype, "render") as (
|
|
148
|
+
this: Markdown,
|
|
149
|
+
width: number,
|
|
150
|
+
) => string[];
|
|
151
|
+
markdownPrototype.render = function patchedMarkdownRender(
|
|
152
|
+
this: Markdown,
|
|
153
|
+
width: number,
|
|
154
|
+
): string[] {
|
|
155
|
+
let lines = originalMarkdownRender.call(this, width);
|
|
156
|
+
if (shouldHideFences(this)) {
|
|
157
|
+
lines = lines.filter((line) => !isFenceLine(line));
|
|
158
|
+
}
|
|
159
|
+
return collapseAssistantBlankLines(lines);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const distDir = await resolvePiDistDir();
|
|
163
|
+
const assistantMessagePath = pathToFileURL(
|
|
164
|
+
join(distDir, "modes/interactive/components/assistant-message.js"),
|
|
165
|
+
).href;
|
|
166
|
+
const assistantModule = (await import(assistantMessagePath)) as {
|
|
167
|
+
AssistantMessageComponent?: { prototype?: AssistantMessageComponentPrototype };
|
|
168
|
+
};
|
|
169
|
+
const assistantPrototype = assistantModule.AssistantMessageComponent?.prototype;
|
|
170
|
+
if (assistantPrototype === undefined) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const originalRender = Reflect.get(assistantPrototype, "render") as (
|
|
175
|
+
this: AssistantMessageComponentInstance,
|
|
176
|
+
width: number,
|
|
177
|
+
) => string[];
|
|
178
|
+
assistantPrototype.render = function patchedAssistantRender(
|
|
179
|
+
this: AssistantMessageComponentInstance,
|
|
180
|
+
width: number,
|
|
181
|
+
): string[] {
|
|
182
|
+
return originalRender.call(this, width).map(stripItalicAnsi);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const originalUpdateContent = Reflect.get(assistantPrototype, "updateContent") as (
|
|
186
|
+
this: AssistantMessageComponentInstance,
|
|
187
|
+
message: unknown,
|
|
188
|
+
) => void;
|
|
189
|
+
assistantPrototype.updateContent = function patchedUpdateContent(
|
|
190
|
+
this: AssistantMessageComponentInstance,
|
|
191
|
+
message: unknown,
|
|
192
|
+
): void {
|
|
193
|
+
const contentContainer = this.contentContainer;
|
|
194
|
+
const originalAddChild = Reflect.get(contentContainer ?? {}, "addChild") as
|
|
195
|
+
| ((
|
|
196
|
+
this: NonNullable<AssistantMessageComponentInstance["contentContainer"]>,
|
|
197
|
+
component: Component,
|
|
198
|
+
) => void)
|
|
199
|
+
| undefined;
|
|
200
|
+
|
|
201
|
+
if (contentContainer !== undefined && originalAddChild !== undefined) {
|
|
202
|
+
contentContainer.addChild = function addChildWithFenceTracking(
|
|
203
|
+
component: Component,
|
|
204
|
+
): void {
|
|
205
|
+
if (component instanceof Markdown) {
|
|
206
|
+
markFencesHidden(component);
|
|
207
|
+
}
|
|
208
|
+
originalAddChild.call(this, component);
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
originalUpdateContent.call(this, message);
|
|
214
|
+
} finally {
|
|
215
|
+
if (contentContainer !== undefined && originalAddChild !== undefined) {
|
|
216
|
+
contentContainer.addChild = originalAddChild;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export default async function assistantRenderingExtension(): Promise<void> {
|
|
223
|
+
await patchMarkdownFences();
|
|
224
|
+
}
|