@pi-archimedes/core 1.8.3 → 2.0.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/LICENSE +21 -0
- package/package.json +1 -1
- package/src/index.ts +41 -9
- package/src/startup/index.ts +55 -31
- package/src/thinking/patch.ts +53 -6
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Daniel Cherubini and contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -59,6 +59,7 @@ export function getCoreSettingsItems(config: CoreConfig): SettingItem[] {
|
|
|
59
59
|
// Module-level state for session lifecycle (shared between session_start and session_shutdown)
|
|
60
60
|
let coreRef: ListingRef | undefined;
|
|
61
61
|
let coreCtx: ExtensionContext | undefined;
|
|
62
|
+
let coreTui: TUI | undefined;
|
|
62
63
|
|
|
63
64
|
export function registerCore(pi: ExtensionAPI): void {
|
|
64
65
|
// Patch console.log for model scope capture
|
|
@@ -72,26 +73,56 @@ export function registerCore(pi: ExtensionAPI): void {
|
|
|
72
73
|
const listingRef = g["listingRef"] as ListingRef | undefined;
|
|
73
74
|
if (listingRef) { listingRef.settled = true; }
|
|
74
75
|
|
|
75
|
-
// Restore patched
|
|
76
|
-
const ORIG_ADD_CHILD = Symbol.for("splashscreen:origAddChild");
|
|
76
|
+
// Restore patched addChild to prevent closure accumulation across reloads
|
|
77
77
|
const PATCHED_LISTING = Symbol.for("splashscreen:listingPatched");
|
|
78
|
-
|
|
78
|
+
const ORIG_ADD_CHILD = Symbol.for("splashscreen:origAddChild");
|
|
79
|
+
if (coreTui) {
|
|
79
80
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
// Direct TUI children (old Pi: chatContainer was a direct child)
|
|
82
|
+
for (const child of coreTui.children) {
|
|
83
|
+
const cc = child as any;
|
|
84
|
+
if (cc[PATCHED_LISTING] && cc[ORIG_ADD_CHILD]) {
|
|
85
|
+
cc.addChild = cc[ORIG_ADD_CHILD];
|
|
86
|
+
cc[PATCHED_LISTING] = false;
|
|
87
|
+
cc[ORIG_ADD_CHILD] = undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
// Nested children (Pi 0.84+: loadedResourcesContainer is inside documentContainer)
|
|
91
|
+
for (const topChild of coreTui.children) {
|
|
92
|
+
const tc = topChild as any;
|
|
93
|
+
if (!tc.children) continue;
|
|
94
|
+
for (const child of tc.children) {
|
|
83
95
|
const cc = child as any;
|
|
84
96
|
if (cc[PATCHED_LISTING] && cc[ORIG_ADD_CHILD]) {
|
|
85
|
-
|
|
97
|
+
cc.addChild = cc[ORIG_ADD_CHILD];
|
|
86
98
|
cc[PATCHED_LISTING] = false;
|
|
87
99
|
cc[ORIG_ADD_CHILD] = undefined;
|
|
88
100
|
}
|
|
89
101
|
}
|
|
90
102
|
}
|
|
91
|
-
|
|
92
|
-
|
|
103
|
+
|
|
104
|
+
// Clear animation interval and debounce timer for prompt cleanup
|
|
105
|
+
const ANIM_INTERVAL = Symbol.for("splashscreen:animInterval");
|
|
106
|
+
const DEBOUNCE_TIMER = Symbol.for("splashscreen:debounceTimer");
|
|
107
|
+
for (const child of coreTui.children) {
|
|
108
|
+
const cc = child as any;
|
|
109
|
+
if (cc[ANIM_INTERVAL]) clearInterval(cc[ANIM_INTERVAL]);
|
|
110
|
+
if (cc[DEBOUNCE_TIMER]) clearTimeout(cc[DEBOUNCE_TIMER]);
|
|
111
|
+
}
|
|
112
|
+
// Nested children (Pi 0.84+: loadedResourcesContainer is inside documentContainer)
|
|
113
|
+
for (const topChild of coreTui.children) {
|
|
114
|
+
const tc = topChild as any;
|
|
115
|
+
if (!tc.children) continue;
|
|
116
|
+
for (const child of tc.children) {
|
|
117
|
+
const cc = child as any;
|
|
118
|
+
if (cc[ANIM_INTERVAL]) clearInterval(cc[ANIM_INTERVAL]);
|
|
119
|
+
if (cc[DEBOUNCE_TIMER]) clearTimeout(cc[DEBOUNCE_TIMER]);
|
|
120
|
+
}
|
|
93
121
|
}
|
|
122
|
+
} catch {
|
|
123
|
+
/* TUI structure may have changed — ignore */
|
|
94
124
|
}
|
|
125
|
+
}
|
|
95
126
|
|
|
96
127
|
// Clear editor component override
|
|
97
128
|
if (coreCtx) { coreCtx.ui.setEditorComponent(undefined); }
|
|
@@ -116,6 +147,7 @@ export function registerCore(pi: ExtensionAPI): void {
|
|
|
116
147
|
};
|
|
117
148
|
const ref = coreRef;
|
|
118
149
|
const headerFactory = (tui: TUI, theme: Theme): Component & { dispose?(): void } => {
|
|
150
|
+
coreTui = tui; // Capture for shutdown cleanup
|
|
119
151
|
const comp: Component & { dispose?(): void } = {
|
|
120
152
|
invalidate(): void { /* no-op */ },
|
|
121
153
|
render(width: number): string[] {
|
package/src/startup/index.ts
CHANGED
|
@@ -159,22 +159,46 @@ function findChatContainer(tui: TUI): Container | undefined {
|
|
|
159
159
|
return undefined;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
// Pi 0.84+ moved resource sections from chatContainer to loadedResourcesContainer
|
|
163
|
+
// (child of documentContainer). Find it by locating documentContainer (the first TUI
|
|
164
|
+
// child that contains Container sub-children), then taking its second Container child.
|
|
165
|
+
//
|
|
166
|
+
// Pi 0.84+ TUI layout (regular mode, 7 top-level children):
|
|
167
|
+
// documentContainer → [headerContainer(0), loadedResourcesContainer(1), chatContainer(2)]
|
|
168
|
+
function findLoadedResourcesContainer(tui: TUI): Container | undefined {
|
|
169
|
+
// Only run this heuristic on Pi >= 0.84.0 where loadedResourcesContainer exists.
|
|
170
|
+
// On older Pi versions, return undefined so the caller falls back to findChatContainer().
|
|
171
|
+
if (compareVersions(VERSION, "0.84.0") < 0) return undefined;
|
|
172
|
+
|
|
173
|
+
// Find documentContainer: first tui child that has Container sub-children
|
|
174
|
+
const docContainer = tui.children.find(
|
|
175
|
+
(c) => c instanceof Container && c.children.some((child) => child instanceof Container)
|
|
176
|
+
) as Container | undefined;
|
|
177
|
+
if (!docContainer) return undefined;
|
|
178
|
+
|
|
179
|
+
const containerChildren = docContainer.children.filter(
|
|
180
|
+
(c): c is Container => c instanceof Container
|
|
181
|
+
);
|
|
182
|
+
// Pi 0.84+: [headerContainer(0), loadedResourcesContainer(1), chatContainer(2)]
|
|
183
|
+
// The index [1] assumption is load-bearing — tied to Pi 0.84+ initialization order.
|
|
184
|
+
if (containerChildren.length < 2) return undefined;
|
|
185
|
+
return containerChildren[1]; // loadedResourcesContainer
|
|
186
|
+
}
|
|
187
|
+
|
|
162
188
|
export function patchStartupListing(
|
|
163
189
|
tui: TUI,
|
|
164
190
|
_theme: Theme,
|
|
165
191
|
ref: ListingRef,
|
|
166
192
|
): void {
|
|
167
|
-
const
|
|
168
|
-
if (!
|
|
169
|
-
|
|
170
|
-
// the startup listing won't render but the extension continues.
|
|
171
|
-
console.warn("[archimedes] Could not find chat container — startup listing disabled. This may indicate a pi TUI structure change.");
|
|
193
|
+
const resourcesContainer = findLoadedResourcesContainer(tui) ?? findChatContainer(tui);
|
|
194
|
+
if (!resourcesContainer) {
|
|
195
|
+
console.warn("[archimedes] Could not find resources container — startup listing disabled. This may indicate a pi TUI structure change.");
|
|
172
196
|
return;
|
|
173
197
|
}
|
|
174
|
-
const
|
|
198
|
+
const rc = resourcesContainer as any;
|
|
175
199
|
|
|
176
200
|
// Always update ref + restart animation (critical for /reload)
|
|
177
|
-
|
|
201
|
+
rc[LISTING_REF] = ref;
|
|
178
202
|
ref.frame = 0;
|
|
179
203
|
ref.revealed = false;
|
|
180
204
|
ref.revealedAt = 0;
|
|
@@ -184,12 +208,12 @@ export function patchStartupListing(
|
|
|
184
208
|
delete ref.cachedWidth;
|
|
185
209
|
delete ref.maxHeaderHeight;
|
|
186
210
|
|
|
187
|
-
if (
|
|
188
|
-
if (
|
|
211
|
+
if (rc[ANIM_INTERVAL]) clearInterval(rc[ANIM_INTERVAL]);
|
|
212
|
+
if (rc[DEBOUNCE_TIMER]) clearTimeout(rc[DEBOUNCE_TIMER]);
|
|
189
213
|
|
|
190
214
|
const interval = setInterval(() => {
|
|
191
215
|
try {
|
|
192
|
-
const current: ListingRef =
|
|
216
|
+
const current: ListingRef = rc[LISTING_REF];
|
|
193
217
|
if (!current) {
|
|
194
218
|
clearInterval(interval);
|
|
195
219
|
return;
|
|
@@ -197,7 +221,7 @@ export function patchStartupListing(
|
|
|
197
221
|
current.frame++;
|
|
198
222
|
if (current.settled && current.frame >= LOGO_SETTLE_FRAME) {
|
|
199
223
|
clearInterval(interval);
|
|
200
|
-
|
|
224
|
+
rc[ANIM_INTERVAL] = null;
|
|
201
225
|
return;
|
|
202
226
|
}
|
|
203
227
|
tui.requestRender();
|
|
@@ -206,12 +230,12 @@ export function patchStartupListing(
|
|
|
206
230
|
}
|
|
207
231
|
}, 16);
|
|
208
232
|
|
|
209
|
-
|
|
233
|
+
rc[ANIM_INTERVAL] = interval;
|
|
210
234
|
|
|
211
235
|
// Fetch latest version from npm
|
|
212
236
|
fetchLatestVersion().then(v => {
|
|
213
237
|
if (v) {
|
|
214
|
-
const current: ListingRef =
|
|
238
|
+
const current: ListingRef = rc[LISTING_REF];
|
|
215
239
|
current.latestVersion = v;
|
|
216
240
|
// Invalidate cache so version updates on next render
|
|
217
241
|
delete current.cachedLines;
|
|
@@ -220,28 +244,28 @@ export function patchStartupListing(
|
|
|
220
244
|
});
|
|
221
245
|
|
|
222
246
|
// Patch clear() to intercept container rebuild
|
|
223
|
-
if (!
|
|
224
|
-
|
|
225
|
-
const origClear =
|
|
226
|
-
|
|
247
|
+
if (!rc[PATCHED_CLEAR]) {
|
|
248
|
+
rc[PATCHED_CLEAR] = true;
|
|
249
|
+
const origClear = resourcesContainer.clear.bind(resourcesContainer);
|
|
250
|
+
resourcesContainer.clear = () => {
|
|
227
251
|
return origClear();
|
|
228
252
|
};
|
|
229
253
|
}
|
|
230
254
|
|
|
231
|
-
// Only patch addChild once — the closure reads
|
|
232
|
-
if (
|
|
233
|
-
|
|
255
|
+
// Only patch addChild once — the closure reads rc[LISTING_REF] dynamically
|
|
256
|
+
if (rc[PATCHED_LISTING]) {
|
|
257
|
+
resourcesContainer.clear();
|
|
234
258
|
return;
|
|
235
259
|
}
|
|
236
|
-
|
|
260
|
+
rc[PATCHED_LISTING] = true;
|
|
237
261
|
|
|
238
|
-
const origAddChild =
|
|
239
|
-
|
|
240
|
-
|
|
262
|
+
const origAddChild = resourcesContainer.addChild.bind(resourcesContainer);
|
|
263
|
+
rc[ORIG_ADD_CHILD] = origAddChild; // Store for cleanup on shutdown
|
|
264
|
+
resourcesContainer.clear();
|
|
241
265
|
|
|
242
|
-
|
|
266
|
+
resourcesContainer.addChild = (component: Component) => {
|
|
243
267
|
try {
|
|
244
|
-
const currentRef: ListingRef =
|
|
268
|
+
const currentRef: ListingRef = rc[LISTING_REF];
|
|
245
269
|
|
|
246
270
|
if (component instanceof Text) {
|
|
247
271
|
// pi ≥0.67.6 wraps startup sections in ExpandableText; collapsed body
|
|
@@ -270,14 +294,14 @@ export function patchStartupListing(
|
|
|
270
294
|
tui.requestRender();
|
|
271
295
|
} else {
|
|
272
296
|
// Batch initial sections — reset debounce on each arrival
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
const ref: ListingRef =
|
|
297
|
+
if (rc[DEBOUNCE_TIMER]) clearTimeout(rc[DEBOUNCE_TIMER]);
|
|
298
|
+
rc[DEBOUNCE_TIMER] = setTimeout(() => {
|
|
299
|
+
const ref: ListingRef = rc[LISTING_REF];
|
|
276
300
|
ref.revealed = true;
|
|
277
301
|
ref.revealedAt = ref.frame;
|
|
278
302
|
ref.scaffoldAt = ref.frame;
|
|
279
303
|
tui.requestRender();
|
|
280
|
-
|
|
304
|
+
rc[DEBOUNCE_TIMER] = null;
|
|
281
305
|
}, REVEAL_DEBOUNCE_MS);
|
|
282
306
|
}
|
|
283
307
|
|
|
@@ -297,7 +321,7 @@ export function patchStartupListing(
|
|
|
297
321
|
try {
|
|
298
322
|
origAddChild(component);
|
|
299
323
|
} catch (err) {
|
|
300
|
-
console.error("[archimedes] Error in
|
|
324
|
+
console.error("[archimedes] Error in resourcesContainer.addChild:", err);
|
|
301
325
|
}
|
|
302
326
|
} catch (err) {
|
|
303
327
|
console.error("[archimedes] Error in startup listing addChild handler:", err);
|
package/src/thinking/patch.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { AssistantMessageComponent, VERSION } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { Markdown, type MarkdownTheme, Spacer, Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { Markdown, type MarkdownOptions, type MarkdownTheme, Spacer, Text } from "@earendil-works/pi-tui";
|
|
4
4
|
import { buildMutedMarkdownTheme } from "./theme.js";
|
|
5
5
|
|
|
6
6
|
// The label we prepend to visible thinking content.
|
|
@@ -99,12 +99,52 @@ export function patchThinkingRenderer(getTheme: () => Theme): void {
|
|
|
99
99
|
return mutedTheme;
|
|
100
100
|
};
|
|
101
101
|
|
|
102
|
+
// Pi's native updateContent passes a `transform` (createMarkdownTransform)
|
|
103
|
+
// to Markdown so the markdown-transformer pipeline runs — that is what
|
|
104
|
+
// renders Mermaid blocks to ASCII and applies any extension transformers.
|
|
105
|
+
// We must preserve it here, otherwise those transformers are silently
|
|
106
|
+
// dropped when this patch replaces updateContent.
|
|
107
|
+
//
|
|
108
|
+
// NOTE: the `transform` option was added to @earendil-works/pi-tui in
|
|
109
|
+
// 0.84.1. The repo's lockfile pins 0.78.0 whose types lack the field, so we
|
|
110
|
+
// extend the options type locally; at runtime the field is ignored by very
|
|
111
|
+
// old pi-tui and honored by 0.84.1+ (which is where Mermaid rendering and
|
|
112
|
+
// the bug both exist).
|
|
113
|
+
type MarkdownOptionsWithTransform = MarkdownOptions & {
|
|
114
|
+
transform?: (markdown: string, availableWidth: number) => string;
|
|
115
|
+
};
|
|
116
|
+
const transformFor =
|
|
117
|
+
(messageType: "assistant" | "assistant-thinking") =>
|
|
118
|
+
(markdown: string, availableWidth: number): string => {
|
|
119
|
+
let out = markdown;
|
|
120
|
+
for (const transformer of (this as any).markdownTransformers ?? []) {
|
|
121
|
+
try {
|
|
122
|
+
const transformed = transformer(out, {
|
|
123
|
+
messageType,
|
|
124
|
+
isStreaming: this.isStreaming,
|
|
125
|
+
availableWidth,
|
|
126
|
+
});
|
|
127
|
+
if (typeof transformed === "string") out = transformed;
|
|
128
|
+
} catch {
|
|
129
|
+
// Keep the current markdown and continue with the next transformer.
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
};
|
|
134
|
+
|
|
102
135
|
// Render content in order.
|
|
103
136
|
for (let i = 0; i < message.content.length; i++) {
|
|
104
137
|
const content = message.content[i];
|
|
105
138
|
if (content.type === "text" && content.text.trim()) {
|
|
106
139
|
this.contentContainer.addChild(
|
|
107
|
-
new Markdown(
|
|
140
|
+
new Markdown(
|
|
141
|
+
content.text.trim(),
|
|
142
|
+
1,
|
|
143
|
+
0,
|
|
144
|
+
this.markdownTheme,
|
|
145
|
+
undefined,
|
|
146
|
+
{ transform: transformFor("assistant") } as MarkdownOptionsWithTransform,
|
|
147
|
+
),
|
|
108
148
|
);
|
|
109
149
|
} else if (content.type === "thinking" && content.thinking.trim()) {
|
|
110
150
|
const hasVisibleContentAfter = message.content
|
|
@@ -133,10 +173,17 @@ export function patchThinkingRenderer(getTheme: () => Theme): void {
|
|
|
133
173
|
if (!t) continue;
|
|
134
174
|
const muted = ensureMuted();
|
|
135
175
|
this.contentContainer.addChild(
|
|
136
|
-
new Markdown(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
176
|
+
new Markdown(
|
|
177
|
+
thinkingContent,
|
|
178
|
+
1,
|
|
179
|
+
0,
|
|
180
|
+
muted ?? this.markdownTheme,
|
|
181
|
+
{
|
|
182
|
+
color: (text: string) => t.fg("thinkingText", text),
|
|
183
|
+
italic: true,
|
|
184
|
+
},
|
|
185
|
+
{ transform: transformFor("assistant-thinking") } as MarkdownOptionsWithTransform,
|
|
186
|
+
),
|
|
140
187
|
);
|
|
141
188
|
if (hasVisibleContentAfter) {
|
|
142
189
|
this.contentContainer.addChild(new Spacer(1));
|