@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-archimedes/core",
3
- "version": "1.8.3",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "keywords": [
6
6
  "pi-package"
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 chat.addChild to prevent closure accumulation across reloads
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
- if (coreCtx) {
78
+ const ORIG_ADD_CHILD = Symbol.for("splashscreen:origAddChild");
79
+ if (coreTui) {
79
80
  try {
80
- const tui = (coreCtx.ui as any).tui;
81
- if (tui?.children) {
82
- for (const child of tui.children) {
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
- child.addChild = cc[ORIG_ADD_CHILD];
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
- } catch {
92
- /* TUI structure may have changed ignore */
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[] {
@@ -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 chat = findChatContainer(tui);
168
- if (!chat) {
169
- // Graceful degradation: if we can't find the chat container,
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 cc = chat as any;
198
+ const rc = resourcesContainer as any;
175
199
 
176
200
  // Always update ref + restart animation (critical for /reload)
177
- cc[LISTING_REF] = ref;
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 (cc[ANIM_INTERVAL]) clearInterval(cc[ANIM_INTERVAL]);
188
- if (cc[DEBOUNCE_TIMER]) clearTimeout(cc[DEBOUNCE_TIMER]);
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 = cc[LISTING_REF];
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
- cc[ANIM_INTERVAL] = null;
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
- cc[ANIM_INTERVAL] = interval;
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 = cc[LISTING_REF];
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 (!cc[PATCHED_CLEAR]) {
224
- cc[PATCHED_CLEAR] = true;
225
- const origClear = chat.clear.bind(chat);
226
- chat.clear = () => {
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 cc[LISTING_REF] dynamically
232
- if (cc[PATCHED_LISTING]) {
233
- chat.clear();
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
- cc[PATCHED_LISTING] = true;
260
+ rc[PATCHED_LISTING] = true;
237
261
 
238
- const origAddChild = chat.addChild.bind(chat);
239
- cc[ORIG_ADD_CHILD] = origAddChild; // Store for cleanup on shutdown
240
- chat.clear();
262
+ const origAddChild = resourcesContainer.addChild.bind(resourcesContainer);
263
+ rc[ORIG_ADD_CHILD] = origAddChild; // Store for cleanup on shutdown
264
+ resourcesContainer.clear();
241
265
 
242
- chat.addChild = (component: Component) => {
266
+ resourcesContainer.addChild = (component: Component) => {
243
267
  try {
244
- const currentRef: ListingRef = cc[LISTING_REF];
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 (cc[DEBOUNCE_TIMER]) clearTimeout(cc[DEBOUNCE_TIMER]);
274
- cc[DEBOUNCE_TIMER] = setTimeout(() => {
275
- const ref: ListingRef = cc[LISTING_REF];
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
- cc[DEBOUNCE_TIMER] = null;
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 chat.addChild:", err);
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);
@@ -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(content.text.trim(), 1, 0, this.markdownTheme),
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(thinkingContent, 1, 0, muted ?? this.markdownTheme, {
137
- color: (text: string) => t.fg("thinkingText", text),
138
- italic: true,
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));