@pi-archimedes/core 0.3.1 → 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 +1 -1
- package/src/bus.ts +14 -7
- package/src/color.ts +10 -10
- package/src/index.ts +1 -1
- package/src/message/index.ts +12 -3
- package/src/startup/capture.ts +4 -1
- package/src/startup/index.ts +36 -10
- package/src/startup/logo.ts +1 -1
- package/src/startup/sections.ts +3 -3
- package/src/thinking/patch.ts +24 -1
package/package.json
CHANGED
package/src/bus.ts
CHANGED
|
@@ -17,7 +17,13 @@ interface CostUpdatePayload {
|
|
|
17
17
|
cost?: number;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
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
|
-
|
|
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 =
|
|
60
|
+
let bus = getGlobal<Bus>(BUS_KEY);
|
|
54
61
|
if (!bus) {
|
|
55
62
|
bus = createBus();
|
|
56
|
-
|
|
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 =
|
|
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
|
-
|
|
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]
|
|
144
|
-
g: clamp(parseInt(tc[2]
|
|
145
|
-
b: clamp(parseInt(tc[3]
|
|
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]
|
|
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]
|
|
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";
|
package/src/message/index.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/src/startup/capture.ts
CHANGED
|
@@ -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]
|
|
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
|
}
|
package/src/startup/index.ts
CHANGED
|
@@ -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 (
|
|
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) +
|
|
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
|
-
|
|
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
|
|
157
|
-
ref.cachedWidth
|
|
158
|
-
ref.maxHeaderHeight
|
|
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
|
|
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
|
|
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 (
|
|
296
|
+
} catch (err) {
|
|
297
|
+
console.error("[archimedes] Error in chat.addChild:", err);
|
|
273
298
|
}
|
|
274
|
-
} catch (
|
|
299
|
+
} catch (err) {
|
|
300
|
+
console.error("[archimedes] Error in startup listing addChild handler:", err);
|
|
275
301
|
}
|
|
276
302
|
};
|
|
277
303
|
}
|
package/src/startup/logo.ts
CHANGED
|
@@ -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);
|
package/src/startup/sections.ts
CHANGED
|
@@ -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]
|
|
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
|
}
|
package/src/thinking/patch.ts
CHANGED
|
@@ -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
|
}
|