@pi-archimedes/core 0.3.2 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-archimedes/core",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "keywords": [
6
6
  "pi-package"
package/src/bus.ts CHANGED
@@ -17,7 +17,13 @@ interface CostUpdatePayload {
17
17
  cost?: number;
18
18
  }
19
19
 
20
- const g = globalThis as unknown as Record<symbol, unknown>;
20
+ // Type-safe globalThis access — avoids `as unknown as` casts
21
+ function getGlobal<T>(key: symbol): T | undefined {
22
+ return (globalThis as Record<symbol, unknown>)[key] as T | undefined;
23
+ }
24
+ function setGlobal<T>(key: symbol, value: T): void {
25
+ (globalThis as Record<symbol, unknown>)[key] = value;
26
+ }
21
27
 
22
28
  function createBus(): Bus {
23
29
  const listeners = new Map<string, Array<(payload: unknown) => void>>();
@@ -29,8 +35,9 @@ function createBus(): Bus {
29
35
  for (const fn of subs) {
30
36
  try {
31
37
  fn(payload);
32
- } catch {
33
- /* ignore listener errors */
38
+ } catch (err) {
39
+ // Listener errors should not crash other listeners
40
+ console.error(`[archimedes:bus] Error in listener for "${event}":`, err);
34
41
  }
35
42
  }
36
43
  }
@@ -50,10 +57,10 @@ function createBus(): Bus {
50
57
  }
51
58
 
52
59
  export function getBus(): Bus {
53
- let bus = g[BUS_KEY] as Bus | undefined;
60
+ let bus = getGlobal<Bus>(BUS_KEY);
54
61
  if (!bus) {
55
62
  bus = createBus();
56
- g[BUS_KEY] = bus;
63
+ setGlobal(BUS_KEY, bus);
57
64
  }
58
65
  return bus;
59
66
  }
@@ -61,12 +68,12 @@ export function getBus(): Bus {
61
68
  export function initBus(): void {
62
69
  const bus = getBus();
63
70
  // Flush queued events (if any were emitted before init)
64
- const queue = g[QUEUE_KEY] as Array<{ event: string; payload: unknown }> | undefined;
71
+ const queue = getGlobal<Array<{ event: string; payload: unknown }>>(QUEUE_KEY);
65
72
  if (queue && queue.length > 0) {
66
73
  for (const { event, payload } of queue) {
67
74
  bus.emit(event, payload);
68
75
  }
69
- g[QUEUE_KEY] = [];
76
+ setGlobal(QUEUE_KEY, []);
70
77
  }
71
78
  }
72
79
 
package/src/color.ts CHANGED
@@ -45,7 +45,7 @@ export function hexToRgb(hex: string): RGB {
45
45
  let s = hex.trim();
46
46
  if (s.startsWith("#")) s = s.slice(1);
47
47
  if (s.length === 3) {
48
- s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2];
48
+ s = s[0]! + s[0]! + s[1]! + s[1]! + s[2]! + s[2]!;
49
49
  }
50
50
  if (s.length !== 6 || !/^[0-9a-fA-F]{6}$/.test(s)) {
51
51
  throw new Error(`Invalid hex color: ${hex}`);
@@ -121,13 +121,13 @@ export function ansi256ToRgb(code: number): RGB {
121
121
  throw new Error(`ANSI 256 code out of range: ${code}`);
122
122
  }
123
123
  if (c < 16) {
124
- return { ...XTERM_16[c] };
124
+ return { ...XTERM_16[c]! };
125
125
  }
126
126
  if (c < 232) {
127
127
  const idx = c - 16;
128
- const r = CUBE_LEVELS[Math.floor(idx / 36) % 6];
129
- const g = CUBE_LEVELS[Math.floor(idx / 6) % 6];
130
- const b = CUBE_LEVELS[idx % 6];
128
+ const r = CUBE_LEVELS[Math.floor(idx / 36) % 6]!;
129
+ const g = CUBE_LEVELS[Math.floor(idx / 6) % 6]!;
130
+ const b = CUBE_LEVELS[idx % 6]!;
131
131
  return { r, g, b };
132
132
  }
133
133
  const v = 8 + (c - 232) * 10;
@@ -140,14 +140,14 @@ export function parseAnsiFgToRgb(ansi: string): RGB | null {
140
140
  const tc = /^\x1b\[38;2;(\d{1,3});(\d{1,3});(\d{1,3})m/.exec(ansi);
141
141
  if (tc) {
142
142
  return {
143
- r: clamp(parseInt(tc[1], 10), 0, 255),
144
- g: clamp(parseInt(tc[2], 10), 0, 255),
145
- b: clamp(parseInt(tc[3], 10), 0, 255),
143
+ r: clamp(parseInt(tc[1]!, 10), 0, 255),
144
+ g: clamp(parseInt(tc[2]!, 10), 0, 255),
145
+ b: clamp(parseInt(tc[3]!, 10), 0, 255),
146
146
  };
147
147
  }
148
148
  const palette = /^\x1b\[38;5;(\d{1,3})m/.exec(ansi);
149
149
  if (palette) {
150
- const code = parseInt(palette[1], 10);
150
+ const code = parseInt(palette[1]!, 10);
151
151
  if (code < 0 || code > 255) return null;
152
152
  return ansi256ToRgb(code);
153
153
  }
@@ -187,7 +187,7 @@ export function rgb(r: number, g: number, b: number, text: string): string {
187
187
 
188
188
  export function extractRgb(themed: string): [number, number, number] {
189
189
  const m = themed.match(/\x1b\[38;2;(\d+);(\d+);(\d+)m/);
190
- return m ? [+m[1], +m[2], +m[3]] : [100, 100, 100];
190
+ return m ? [+m[1]!, +m[2]!, +m[3]!] : [100, 100, 100];
191
191
  }
192
192
 
193
193
  export function lerp(a: number, b: number, t: number): number {
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext, KeybindingsManager } from "@earendil-works/pi-coding-agent";
2
2
  import type { Theme } from "@earendil-works/pi-coding-agent";
3
- import { TUI, EditorTheme, Component, type SettingItem } from "@earendil-works/pi-tui";
3
+ import { TUI, type EditorTheme, type Component, type SettingItem } from "@earendil-works/pi-tui";
4
4
 
5
5
  import { HephaestusEditor } from "./editor/index.js";
6
6
  import { patchUserMessage, resetInstanceCount } from "./message/index.js";
@@ -1,11 +1,13 @@
1
1
  import type { Theme, UserMessageComponent } from "@earendil-works/pi-coding-agent";
2
+ import { VERSION } from "@earendil-works/pi-coding-agent";
2
3
  import { RESET, resolvePalette, setThemeBg } from "../chrome.js";
3
4
 
4
- type UserMsgCtor = typeof UserMessageComponent & { [PATCHED]?: boolean };
5
+ type UserMsgCtor = typeof UserMessageComponent & { [PATCHED]?: boolean; [PATCH_VERSION]?: string };
5
6
 
6
7
  // ── Constants ──────────────────────────────────────────────
7
8
 
8
9
  const PATCHED = Symbol.for("splashscreen:userMsgPatched");
10
+ const PATCH_VERSION = Symbol.for("splashscreen:userMsgPatchVersion");
9
11
  // Match OSC133 B (zone end) or C (zone final). v0.67 moved these from line
10
12
  // tail to line head — we strip from wherever they sit and re-emit at the end.
11
13
  const OSC133_RE = /\x1b\]133;[BC]\x07/g;
@@ -44,7 +46,13 @@ export function patchUserMessage(
44
46
 
45
47
  import("@earendil-works/pi-coding-agent").then(
46
48
  ({ UserMessageComponent }: { UserMessageComponent: UserMsgCtor }) => {
47
- if (UserMessageComponent[PATCHED]) return;
49
+ const currentVersion = VERSION ?? "unknown";
50
+ const patchVersion = UserMessageComponent[PATCH_VERSION];
51
+ // Skip if already patched for this version
52
+ if (UserMessageComponent[PATCHED] && patchVersion === currentVersion) return;
53
+ if (UserMessageComponent[PATCHED] && patchVersion && patchVersion !== currentVersion) {
54
+ console.warn(`[archimedes] Re-patching user message: pi version changed ${patchVersion} → ${currentVersion}`);
55
+ }
48
56
 
49
57
  if (
50
58
  typeof UserMessageComponent.prototype.addChild !== "function" ||
@@ -55,6 +63,7 @@ export function patchUserMessage(
55
63
  }
56
64
 
57
65
  UserMessageComponent[PATCHED] = true;
66
+ UserMessageComponent[PATCH_VERSION] = currentVersion;
58
67
 
59
68
  const origAddChild = UserMessageComponent.prototype.addChild;
60
69
  UserMessageComponent.prototype.addChild = function (child: any) {
@@ -79,7 +88,7 @@ export function patchUserMessage(
79
88
  if (bg !== lastBg) { setThemeBg(currentTheme, "userMessageBg", bg); lastBg = bg; }
80
89
 
81
90
  const idx = instanceIndex.get(this);
82
- const elapsed = idx !== undefined ? responseTimes[idx] : 0;
91
+ const elapsed = idx !== undefined ? (responseTimes[idx] ?? 0) : 0;
83
92
  const hasTime = idx !== undefined;
84
93
 
85
94
  const contentWidth = width - TIME_COL;
@@ -14,11 +14,14 @@ export function patchConsoleLog(): void {
14
14
  const plain = (args[0] as string).replace(/\x1b\[[0-9;]*[a-zA-Z]/g, "");
15
15
  const m = MODEL_SCOPE_RE.exec(plain);
16
16
  if (m) {
17
- const raw = m[1].replace(/\s*\(Ctrl\+\w[\w\s]*\)/gi, "");
17
+ const raw = m[1]!.replace(/\s*\(Ctrl\+\w[\w\s]*\)/gi, "");
18
18
  g[CAPTURED_MODELS] = raw
19
19
  .split(",")
20
20
  .map((s: string) => s.trim())
21
21
  .filter(Boolean);
22
+ // Unpatch once captured — no need to keep intercepting
23
+ console.log = origLog;
24
+ g[PATCHED_LOG] = false;
22
25
  return;
23
26
  }
24
27
  }
@@ -104,10 +104,11 @@ export function renderHeader(theme: Theme, ref: ListingRef, width: number, heigh
104
104
  contentLines.push(" ".repeat(LOGO_PAD) + " ".repeat(listingLeftPad) + listRow);
105
105
  }
106
106
 
107
- // Pad top and bottom to fill height (bias top by 2 for visual centering)
107
+ // Pad top and bottom to fill height (+5 top bias for visual centering)
108
+ const VERTICAL_CENTER_BIAS = 5; // Shift content slightly upward for visual balance
108
109
  const contentHeight = contentLines.length;
109
110
  const remaining = Math.max(0, height - contentHeight);
110
- const topPad = Math.floor(remaining / 2) + 5;
111
+ const topPad = Math.floor(remaining / 2) + VERTICAL_CENTER_BIAS;
111
112
  const bottomPad = remaining - topPad;
112
113
 
113
114
  const result: string[] = [];
@@ -123,15 +124,38 @@ export function renderHeader(theme: Theme, ref: ListingRef, width: number, heigh
123
124
  // Fragile: relies on TUI child ordering (header, chat, footer) which is an
124
125
  // internal layout detail of pi's InteractiveMode. If upstream changes the
125
126
  // child structure, this will need updating.
127
+ //
128
+ // Strategy (most specific → least specific):
129
+ // 1. Container with "Scrollable" in constructor name
130
+ // 2. Container with most children (chat usually has the most)
131
+ // 3. Middle child if exactly 3 children (header, chat, footer)
132
+ // 4. First Container found
126
133
  function findChatContainer(tui: TUI): Container | undefined {
134
+ // Strategy 1: Look for Scrollable container
127
135
  for (const child of tui.children) {
128
136
  if (child instanceof Container && child.constructor.name.includes("Scrollable")) {
129
137
  return child;
130
138
  }
131
139
  }
140
+
141
+ // Strategy 2: Find the Container with the most children (chat area)
142
+ const containers = tui.children.filter((c): c is Container => c instanceof Container);
143
+ if (containers.length > 1) {
144
+ const largest = containers.reduce((best, c) =>
145
+ c.children.length > best.children.length ? c : best,
146
+ );
147
+ if (largest.children.length > 0) return largest;
148
+ }
149
+
150
+ // Strategy 3: Middle child if exactly 3 children
132
151
  if (tui.children.length >= 3) {
133
- return tui.children[1] as Container;
152
+ const middle = tui.children[1];
153
+ if (middle instanceof Container) return middle;
134
154
  }
155
+
156
+ // Strategy 4: First Container
157
+ if (containers.length > 0) return containers[0];
158
+
135
159
  return undefined;
136
160
  }
137
161
 
@@ -153,9 +177,9 @@ export function patchStartupListing(
153
177
  ref.revealedAt = 0;
154
178
  ref.scaffoldAt = 0;
155
179
  ref.settled = false;
156
- ref.cachedLines = undefined;
157
- ref.cachedWidth = undefined;
158
- ref.maxHeaderHeight = undefined;
180
+ delete ref.cachedLines;
181
+ delete ref.cachedWidth;
182
+ delete ref.maxHeaderHeight;
159
183
 
160
184
  if (cc[ANIM_INTERVAL]) clearInterval(cc[ANIM_INTERVAL]);
161
185
  if (cc[DEBOUNCE_TIMER]) clearTimeout(cc[DEBOUNCE_TIMER]);
@@ -187,7 +211,7 @@ export function patchStartupListing(
187
211
  const current: ListingRef = cc[LISTING_REF];
188
212
  current.latestVersion = v;
189
213
  // Invalidate cache so version updates on next render
190
- current.cachedLines = undefined;
214
+ delete current.cachedLines;
191
215
  current.settled = false;
192
216
  }
193
217
  });
@@ -236,7 +260,7 @@ export function patchStartupListing(
236
260
 
237
261
  // Invalidate cache so late-arriving sections show up
238
262
  currentRef.settled = false;
239
- currentRef.cachedLines = undefined;
263
+ delete currentRef.cachedLines;
240
264
 
241
265
  if (currentRef.revealed) {
242
266
  // Already revealed — show new section immediately
@@ -269,9 +293,11 @@ export function patchStartupListing(
269
293
  if (component instanceof Spacer && !currentRef.revealed) return;
270
294
  try {
271
295
  origAddChild(component);
272
- } catch (e) {
296
+ } catch (err) {
297
+ console.error("[archimedes] Error in chat.addChild:", err);
273
298
  }
274
- } catch (e) {
299
+ } catch (err) {
300
+ console.error("[archimedes] Error in startup listing addChild handler:", err);
275
301
  }
276
302
  };
277
303
  }
@@ -67,7 +67,7 @@ export function getShinedLogo(frame: number, style: AnimationStyle = "wave"): st
67
67
  return LOGO.map((line, y) => {
68
68
  let result = "";
69
69
  for (let x = 0; x < line.length; x++) {
70
- const char = line[x];
70
+ const char = line[x]!;
71
71
  if (char === " ") { result += " "; continue; }
72
72
 
73
73
  const revealAt = computeRevealAt(x, y, style);
@@ -97,7 +97,7 @@ export function parseSectionText(plain: string): ParsedSection | undefined {
97
97
  export function parseModelScope(plain: string): ParsedSection | undefined {
98
98
  const m = plain.match(/Model scope:\s*(.+)/i);
99
99
  if (!m) return undefined;
100
- const raw = m[1].replace(/\s*\(Ctrl\+\w[\w\s]*\)/gi, "");
100
+ const raw = m[1]!.replace(/\s*\(Ctrl\+\w[\w\s]*\)/gi, "");
101
101
  const items = raw.split(",").map(s => s.trim()).filter(Boolean);
102
102
  return items.length > 0 ? { name: "Models", items } : undefined;
103
103
  }
@@ -186,7 +186,7 @@ export function formatColumns(sections: RenderSection[], theme: Theme, maxW: num
186
186
  const lines: string[] = [];
187
187
 
188
188
  for (let si = 0; si < sections.length; si++) {
189
- const sec = sections[si];
189
+ const sec = sections[si]!;
190
190
  if (sec.items.length === 0) continue;
191
191
 
192
192
  const availableW = maxW - headerW - 1;
@@ -207,7 +207,7 @@ export function formatColumns(sections: RenderSection[], theme: Theme, maxW: num
207
207
  const styleItem = (raw: string): string => {
208
208
  const prefixMatch = raw.match(/^(npm:|git:)/);
209
209
  if (prefixMatch) {
210
- const pfx = prefixMatch[1];
210
+ const pfx = prefixMatch[1]!;
211
211
  const name = raw.slice(pfx.length);
212
212
  return wrapLabel(pfx) + wrapItems(name);
213
213
  }
@@ -1,15 +1,21 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
- import { AssistantMessageComponent } from "@earendil-works/pi-coding-agent";
2
+ import { AssistantMessageComponent, VERSION } from "@earendil-works/pi-coding-agent";
3
3
  import { Markdown, 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.
7
7
  const THINKING_LABEL = "\x1b[1m\x1b[38;2;255;215;0mThinking...\x1b[39m\x1b[22m";
8
8
 
9
+ // Track which pi version we patched against to detect incompatibility
10
+ const PATCHED_KEY = Symbol.for("archimedes:thinkingPatched");
11
+ const PATCH_VERSION_KEY = Symbol.for("archimedes:thinkingPatchVersion");
12
+
9
13
  /**
10
14
  * Patches `AssistantMessageComponent.prototype.updateContent` so thinking
11
15
  * blocks render with a muted `MarkdownTheme`. Called on every session_start
12
16
  * to capture a fresh `getTheme` closure (required for /resume).
17
+ *
18
+ * Re-patches when pi version changes to catch breaking upstream changes.
13
19
  */
14
20
  export function patchThinkingRenderer(getTheme: () => Theme): void {
15
21
  if (!AssistantMessageComponent) return;
@@ -31,6 +37,19 @@ export function patchThinkingRenderer(getTheme: () => Theme): void {
31
37
  return;
32
38
  }
33
39
 
40
+ // Check if already patched against the current pi version
41
+ const currentVersion = VERSION ?? "unknown";
42
+ const patchVersion = (proto as any)[PATCH_VERSION_KEY] as string | undefined;
43
+ if ((proto as any)[PATCHED_KEY] && patchVersion === currentVersion) {
44
+ // Already patched for this version — just update the closure by re-patching
45
+ // (needed for /resume to get fresh getTheme)
46
+ } else {
47
+ // First patch or version changed — warn if version mismatch
48
+ if ((proto as any)[PATCHED_KEY] && patchVersion && patchVersion !== currentVersion) {
49
+ console.warn(`[archimedes] Re-patching thinking renderer: pi version changed ${patchVersion} → ${currentVersion}`);
50
+ }
51
+ }
52
+
34
53
  // Re-patch every time — /resume needs a fresh getTheme closure
35
54
  (proto as any).updateContent = function (this: any, message: any): void {
36
55
  this.lastMessage = message;
@@ -147,4 +166,8 @@ export function patchThinkingRenderer(getTheme: () => Theme): void {
147
166
  // Bottom padding so next message has breathing room
148
167
  this.contentContainer.addChild(new Spacer(1));
149
168
  };
169
+
170
+ // Mark as patched with version for incompatibility detection
171
+ (proto as any)[PATCHED_KEY] = true;
172
+ (proto as any)[PATCH_VERSION_KEY] = currentVersion;
150
173
  }