@pi-archimedes/core 0.7.0 → 0.9.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 +3 -2
- package/src/color.ts +131 -131
- package/src/config.ts +5 -24
- package/src/index.ts +25 -1
- package/src/settings-io.ts +42 -0
- package/src/startup/capture.ts +20 -6
- package/src/startup/index.ts +5 -1
- package/src/startup/sections.ts +34 -17
- package/src/text.ts +17 -2
- package/src/thinking/patch.ts +9 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-archimedes/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package"
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"./chrome": "./src/chrome.ts",
|
|
17
17
|
"./text": "./src/text.ts",
|
|
18
18
|
"./color": "./src/color.ts",
|
|
19
|
-
"./config": "./src/config.ts"
|
|
19
|
+
"./config": "./src/config.ts",
|
|
20
|
+
"./settings-io": "./src/settings-io.ts"
|
|
20
21
|
},
|
|
21
22
|
"peerDependencies": {
|
|
22
23
|
"@earendil-works/pi-coding-agent": ">=0.1.0",
|
package/src/color.ts
CHANGED
|
@@ -1,177 +1,177 @@
|
|
|
1
1
|
// HSL / RGB / ANSI color utilities. Pure: no external imports.
|
|
2
2
|
|
|
3
3
|
export interface RGB {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
r: number;
|
|
5
|
+
g: number;
|
|
6
|
+
b: number;
|
|
7
7
|
} // each 0..255 integer
|
|
8
8
|
|
|
9
9
|
export interface HSL {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
h: number;
|
|
11
|
+
s: number;
|
|
12
|
+
l: number;
|
|
13
13
|
} // h: 0..360, s/l: 0..1
|
|
14
14
|
|
|
15
15
|
// xterm standard 16-color palette (ANSI codes 0..15).
|
|
16
16
|
// Source: https://en.wikipedia.org/wiki/ANSI_escape_code#3-bit_and_4-bit
|
|
17
17
|
const XTERM_16: readonly RGB[] = [
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
18
|
+
{ r: 0x00, g: 0x00, b: 0x00 }, // 0 black
|
|
19
|
+
{ r: 0x80, g: 0x00, b: 0x00 }, // 1 maroon
|
|
20
|
+
{ r: 0x00, g: 0x80, b: 0x00 }, // 2 green
|
|
21
|
+
{ r: 0x80, g: 0x80, b: 0x00 }, // 3 olive
|
|
22
|
+
{ r: 0x00, g: 0x00, b: 0x80 }, // 4 navy
|
|
23
|
+
{ r: 0x80, g: 0x80, b: 0x80 }, // 5 purple
|
|
24
|
+
{ r: 0x00, g: 0x80, b: 0x80 }, // 6 teal
|
|
25
|
+
{ r: 0xc0, g: 0xc0, b: 0xc0 }, // 7 silver
|
|
26
|
+
{ r: 0x80, g: 0x80, b: 0x80 }, // 8 grey
|
|
27
|
+
{ r: 0xff, g: 0x00, b: 0x00 }, // 9 red
|
|
28
|
+
{ r: 0x00, g: 0xff, b: 0x00 }, // 10 lime
|
|
29
|
+
{ r: 0xff, g: 0xff, b: 0x00 }, // 11 yellow
|
|
30
|
+
{ r: 0x00, g: 0x00, b: 0xff }, // 12 blue
|
|
31
|
+
{ r: 0xff, g: 0x00, b: 0xff }, // 13 fuchsia
|
|
32
|
+
{ r: 0x00, g: 0xff, b: 0xff }, // 14 aqua
|
|
33
|
+
{ r: 0xff, g: 0xff, b: 0xff }, // 15 white
|
|
34
34
|
];
|
|
35
35
|
|
|
36
36
|
// 6x6x6 cube levels for ANSI 16..231.
|
|
37
37
|
const CUBE_LEVELS: readonly number[] = [0, 95, 135, 175, 215, 255];
|
|
38
38
|
|
|
39
39
|
const clamp = (v: number, lo: number, hi: number): number =>
|
|
40
|
-
|
|
40
|
+
v < lo ? lo : v > hi ? hi : v;
|
|
41
41
|
|
|
42
42
|
const toByte = (v: number): number => clamp(Math.round(v), 0, 255);
|
|
43
43
|
|
|
44
44
|
export function hexToRgb(hex: string): RGB {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
45
|
+
let s = hex.trim();
|
|
46
|
+
if (s.startsWith("#")) s = s.slice(1);
|
|
47
|
+
if (s.length === 3) {
|
|
48
|
+
s = s[0]! + s[0]! + s[1]! + s[1]! + s[2]! + s[2]!;
|
|
49
|
+
}
|
|
50
|
+
if (s.length !== 6 || !/^[0-9a-fA-F]{6}$/.test(s)) {
|
|
51
|
+
throw new Error(`Invalid hex color: ${hex}`);
|
|
52
|
+
}
|
|
53
|
+
const n = parseInt(s, 16);
|
|
54
|
+
return {
|
|
55
|
+
r: (n >> 16) & 0xff,
|
|
56
|
+
g: (n >> 8) & 0xff,
|
|
57
|
+
b: n & 0xff,
|
|
58
|
+
};
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export function rgbToHex(rgb: RGB): string {
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
const hex = (v: number) => toByte(v).toString(16).padStart(2, "0");
|
|
63
|
+
return `#${hex(rgb.r)}${hex(rgb.g)}${hex(rgb.b)}`;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
export function rgbToHsl(rgb: RGB): HSL {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
67
|
+
const r = rgb.r / 255;
|
|
68
|
+
const g = rgb.g / 255;
|
|
69
|
+
const b = rgb.b / 255;
|
|
70
|
+
const max = Math.max(r, g, b);
|
|
71
|
+
const min = Math.min(r, g, b);
|
|
72
|
+
const l = (max + min) / 2;
|
|
73
|
+
let h = 0;
|
|
74
|
+
let s = 0;
|
|
75
|
+
const d = max - min;
|
|
76
|
+
if (d !== 0) {
|
|
77
|
+
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
78
|
+
switch (max) {
|
|
79
|
+
case r:
|
|
80
|
+
h = ((g - b) / d + (g < b ? 6 : 0)) * 60;
|
|
81
|
+
break;
|
|
82
|
+
case g:
|
|
83
|
+
h = ((b - r) / d + 2) * 60;
|
|
84
|
+
break;
|
|
85
|
+
default:
|
|
86
|
+
h = ((r - g) / d + 4) * 60;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return { h, s, l };
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
export function hslToRgb(hsl: HSL): RGB {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
94
|
+
const { h, s, l } = hsl;
|
|
95
|
+
if (s === 0) {
|
|
96
|
+
const v = toByte(l * 255);
|
|
97
|
+
return { r: v, g: v, b: v };
|
|
98
|
+
}
|
|
99
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
100
|
+
const p = 2 * l - q;
|
|
101
|
+
const hk = (((h % 360) + 360) % 360) / 360;
|
|
102
|
+
const hue2rgb = (t: number): number => {
|
|
103
|
+
let tt = t;
|
|
104
|
+
if (tt < 0) tt += 1;
|
|
105
|
+
if (tt > 1) tt -= 1;
|
|
106
|
+
if (tt < 1 / 6) return p + (q - p) * 6 * tt;
|
|
107
|
+
if (tt < 1 / 2) return q;
|
|
108
|
+
if (tt < 2 / 3) return p + (q - p) * (2 / 3 - tt) * 6;
|
|
109
|
+
return p;
|
|
110
|
+
};
|
|
111
|
+
return {
|
|
112
|
+
r: toByte(hue2rgb(hk + 1 / 3) * 255),
|
|
113
|
+
g: toByte(hue2rgb(hk) * 255),
|
|
114
|
+
b: toByte(hue2rgb(hk - 1 / 3) * 255),
|
|
115
|
+
};
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
export function ansi256ToRgb(code: number): RGB {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
119
|
+
const c = Math.floor(code);
|
|
120
|
+
if (c < 0 || c > 255) {
|
|
121
|
+
throw new Error(`ANSI 256 code out of range: ${code}`);
|
|
122
|
+
}
|
|
123
|
+
if (c < 16) {
|
|
124
|
+
return { ...XTERM_16[c]! };
|
|
125
|
+
}
|
|
126
|
+
if (c < 232) {
|
|
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]!;
|
|
131
|
+
return { r, g, b };
|
|
132
|
+
}
|
|
133
|
+
const v = 8 + (c - 232) * 10;
|
|
134
|
+
return { r: v, g: v, b: v };
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
export function parseAnsiFgToRgb(ansi: string): RGB | null {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
138
|
+
if (!ansi) return null;
|
|
139
|
+
// Match a leading CSI SGR sequence: ESC [ params m
|
|
140
|
+
const tc = /^\x1b\[38;2;(\d{1,3});(\d{1,3});(\d{1,3})m/.exec(ansi);
|
|
141
|
+
if (tc) {
|
|
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),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
const palette = /^\x1b\[38;5;(\d{1,3})m/.exec(ansi);
|
|
149
|
+
if (palette) {
|
|
150
|
+
const code = parseInt(palette[1]!, 10);
|
|
151
|
+
if (code < 0 || code > 255) return null;
|
|
152
|
+
return ansi256ToRgb(code);
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
export function deriveDimColor(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
input: string | number,
|
|
159
|
+
anchorLightness: number,
|
|
160
|
+
saturationFactor?: number,
|
|
161
161
|
): string {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
162
|
+
const rgb = typeof input === "number" ? ansi256ToRgb(input) : hexToRgb(input);
|
|
163
|
+
const hsl = rgbToHsl(rgb);
|
|
164
|
+
const factor = saturationFactor ?? 0.5;
|
|
165
|
+
const target: HSL = {
|
|
166
|
+
h: hsl.h,
|
|
167
|
+
s: hsl.s * factor,
|
|
168
|
+
l: Math.min(hsl.l, anchorLightness),
|
|
169
|
+
};
|
|
170
|
+
return rgbToHex(hslToRgb(target));
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
export function rgbToTruecolorFg(rgb: RGB): string {
|
|
174
|
-
|
|
174
|
+
return `\x1b[38;2;${toByte(rgb.r)};${toByte(rgb.g)};${toByte(rgb.b)}m`;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
// ── Color helpers (from utils/ansi.ts) ───────────────────────────────────────
|
package/src/config.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { loadConfig, saveConfig } from "./settings-io.js";
|
|
4
2
|
|
|
5
3
|
export const ANIMATION_STYLES = [
|
|
6
4
|
"diagonal",
|
|
@@ -23,8 +21,6 @@ export interface CoreConfig {
|
|
|
23
21
|
animationStyle: AnimationStyle;
|
|
24
22
|
}
|
|
25
23
|
|
|
26
|
-
const SETTINGS_PATH = join(getAgentDir(), "settings.json");
|
|
27
|
-
|
|
28
24
|
export const DEFAULT_CORE_CONFIG: CoreConfig = {
|
|
29
25
|
mutedTheme: false,
|
|
30
26
|
codeUnindent: true,
|
|
@@ -33,27 +29,12 @@ export const DEFAULT_CORE_CONFIG: CoreConfig = {
|
|
|
33
29
|
animationStyle: "vertical-up",
|
|
34
30
|
};
|
|
35
31
|
|
|
32
|
+
const NAMESPACE = "archimedes.core";
|
|
33
|
+
|
|
36
34
|
export function loadCoreConfig(): CoreConfig {
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
const full = JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
|
|
40
|
-
return { ...DEFAULT_CORE_CONFIG, ...(full["archimedes.core"] ?? {}) };
|
|
41
|
-
} catch {
|
|
42
|
-
/* ignore corrupt file */
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return DEFAULT_CORE_CONFIG;
|
|
35
|
+
return loadConfig(NAMESPACE, DEFAULT_CORE_CONFIG);
|
|
46
36
|
}
|
|
47
37
|
|
|
48
38
|
export function saveCoreConfig(config: CoreConfig): void {
|
|
49
|
-
|
|
50
|
-
if (existsSync(SETTINGS_PATH)) {
|
|
51
|
-
try {
|
|
52
|
-
full = JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
|
|
53
|
-
} catch {
|
|
54
|
-
/* ignore corrupt file */
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
full["archimedes.core"] = config;
|
|
58
|
-
writeFileSync(SETTINGS_PATH, JSON.stringify(full, null, 2), "utf-8");
|
|
39
|
+
saveConfig(NAMESPACE, config);
|
|
59
40
|
}
|
package/src/index.ts
CHANGED
|
@@ -5,12 +5,15 @@ import { TUI, type EditorTheme, type Component, type SettingItem } from "@earend
|
|
|
5
5
|
import { HephaestusEditor } from "./editor/index.js";
|
|
6
6
|
|
|
7
7
|
import { renderHeader, patchStartupListing, type ListingRef } from "./startup/index.js";
|
|
8
|
-
import { patchConsoleLog } from "./startup/capture.js";
|
|
8
|
+
import { patchConsoleLog, unpatchConsoleLog } from "./startup/capture.js";
|
|
9
9
|
import { patchThinkingRenderer } from "./thinking/patch.js";
|
|
10
10
|
import { transformThinkingContent } from "./thinking/transform.js";
|
|
11
11
|
import { loadCoreConfig, saveCoreConfig, DEFAULT_CORE_CONFIG, ANIMATION_STYLES, type CoreConfig } from "./config.js";
|
|
12
12
|
import { initBus } from "./bus.js";
|
|
13
13
|
|
|
14
|
+
// Re-export for session lifecycle management
|
|
15
|
+
export { unpatchConsoleLog } from "./startup/capture.js";
|
|
16
|
+
|
|
14
17
|
// ── Settings items ────────────────────────────────────────────────────────
|
|
15
18
|
|
|
16
19
|
export function getCoreSettingsItems(config: CoreConfig): SettingItem[] {
|
|
@@ -69,6 +72,27 @@ export function registerCore(pi: ExtensionAPI): void {
|
|
|
69
72
|
const listingRef = g["listingRef"] as ListingRef | undefined;
|
|
70
73
|
if (listingRef) { listingRef.settled = true; }
|
|
71
74
|
|
|
75
|
+
// Restore patched chat.addChild to prevent closure accumulation across reloads
|
|
76
|
+
const ORIG_ADD_CHILD = Symbol.for("splashscreen:origAddChild");
|
|
77
|
+
const PATCHED_LISTING = Symbol.for("splashscreen:listingPatched");
|
|
78
|
+
if (coreCtx) {
|
|
79
|
+
try {
|
|
80
|
+
const tui = (coreCtx.ui as any).tui;
|
|
81
|
+
if (tui?.children) {
|
|
82
|
+
for (const child of tui.children) {
|
|
83
|
+
const cc = child as any;
|
|
84
|
+
if (cc[PATCHED_LISTING] && cc[ORIG_ADD_CHILD]) {
|
|
85
|
+
child.addChild = cc[ORIG_ADD_CHILD];
|
|
86
|
+
cc[PATCHED_LISTING] = false;
|
|
87
|
+
cc[ORIG_ADD_CHILD] = undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch {
|
|
92
|
+
/* TUI structure may have changed — ignore */
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
72
96
|
// Clear editor component override
|
|
73
97
|
if (coreCtx) { coreCtx.ui.setEditorComponent(undefined); }
|
|
74
98
|
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, renameSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
const SETTINGS_PATH = join(getAgentDir(), "settings.json");
|
|
6
|
+
|
|
7
|
+
/** Read the full settings.json, returning empty object if missing/corrupt. */
|
|
8
|
+
function readSettings(): Record<string, unknown> {
|
|
9
|
+
if (!existsSync(SETTINGS_PATH)) return {};
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
|
|
12
|
+
} catch {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Load a config section from settings.json, merged with defaults.
|
|
19
|
+
*/
|
|
20
|
+
export function loadConfig<T>(
|
|
21
|
+
namespace: string,
|
|
22
|
+
defaults: T,
|
|
23
|
+
): T {
|
|
24
|
+
const full = readSettings();
|
|
25
|
+
return { ...defaults, ...(full[namespace] ?? {}) } as T;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Save a config section to settings.json (atomic: write to .tmp then rename).
|
|
30
|
+
*/
|
|
31
|
+
export function saveConfig(namespace: string, config: object): void {
|
|
32
|
+
const full = readSettings();
|
|
33
|
+
full[namespace] = config;
|
|
34
|
+
const tmpPath = SETTINGS_PATH + ".tmp";
|
|
35
|
+
writeFileSync(tmpPath, JSON.stringify(full, null, 2), "utf-8");
|
|
36
|
+
try {
|
|
37
|
+
renameSync(tmpPath, SETTINGS_PATH);
|
|
38
|
+
} catch {
|
|
39
|
+
try { unlinkSync(tmpPath); } catch { /* ignore */ }
|
|
40
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(full, null, 2), "utf-8");
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/startup/capture.ts
CHANGED
|
@@ -4,10 +4,14 @@ const MODEL_SCOPE_RE = /Model scope:\s*(.+)/;
|
|
|
4
4
|
export const CAPTURED_MODELS = Symbol.for("splashscreen:capturedModels");
|
|
5
5
|
export const PATCHED_LOG = Symbol.for("splashscreen:logPatched");
|
|
6
6
|
|
|
7
|
+
// Store original console.log for restoration
|
|
8
|
+
let _origLog: typeof console.log | undefined;
|
|
9
|
+
|
|
7
10
|
export function patchConsoleLog(): void {
|
|
8
11
|
if (g[PATCHED_LOG]) return;
|
|
9
12
|
g[PATCHED_LOG] = true;
|
|
10
|
-
|
|
13
|
+
_origLog = console.log;
|
|
14
|
+
|
|
11
15
|
console.log = (...args: unknown[]) => {
|
|
12
16
|
try {
|
|
13
17
|
if (args.length === 1 && typeof args[0] === "string") {
|
|
@@ -20,14 +24,24 @@ export function patchConsoleLog(): void {
|
|
|
20
24
|
.map((s: string) => s.trim())
|
|
21
25
|
.filter(Boolean);
|
|
22
26
|
// Unpatch once captured — no need to keep intercepting
|
|
23
|
-
|
|
24
|
-
g[PATCHED_LOG] = false;
|
|
27
|
+
unpatchConsoleLog();
|
|
25
28
|
return;
|
|
26
29
|
}
|
|
27
30
|
}
|
|
28
|
-
} catch {
|
|
29
|
-
|
|
31
|
+
} catch (err) {
|
|
32
|
+
// Log the error but don't suppress the original call
|
|
33
|
+
console.error("[archimedes] Error in console.log patch:", err);
|
|
30
34
|
}
|
|
31
|
-
|
|
35
|
+
_origLog!.apply(console, args);
|
|
32
36
|
};
|
|
33
37
|
}
|
|
38
|
+
|
|
39
|
+
/** Restore original console.log — call on session_shutdown. */
|
|
40
|
+
export function unpatchConsoleLog(): void {
|
|
41
|
+
if (!g[PATCHED_LOG]) return;
|
|
42
|
+
g[PATCHED_LOG] = false;
|
|
43
|
+
if (_origLog) {
|
|
44
|
+
console.log = _origLog;
|
|
45
|
+
_origLog = undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/startup/index.ts
CHANGED
|
@@ -3,7 +3,6 @@ import { getShinedLogo, TRUECOLOR, LOGO_PAD, LOGO_SETTLE_FRAME } from "./logo.js
|
|
|
3
3
|
import { loadCoreConfig } from "../config.js";
|
|
4
4
|
import { detectSection, parseSectionText, parseModelScope, formatColumns, buildItemWrapper, type ParsedSection, SECTION_KEYS } from "./sections.js";
|
|
5
5
|
import { fetchLatestVersion, compareVersions } from "./version.js";
|
|
6
|
-
import { patchConsoleLog } from "./capture.js";
|
|
7
6
|
import { stripAnsi } from "../text.js";
|
|
8
7
|
import { Text, Spacer, Container, TUI, truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
9
8
|
|
|
@@ -13,6 +12,7 @@ const ANIM_INTERVAL = Symbol.for("splashscreen:animInterval");
|
|
|
13
12
|
const DEBOUNCE_TIMER = Symbol.for("splashscreen:debounceTimer");
|
|
14
13
|
const PATCHED_CLEAR = Symbol.for("splashscreen:clearPatched");
|
|
15
14
|
const PATCHED_LISTING = Symbol.for("splashscreen:listingPatched");
|
|
15
|
+
const ORIG_ADD_CHILD = Symbol.for("splashscreen:origAddChild");
|
|
16
16
|
|
|
17
17
|
// Animation constants
|
|
18
18
|
const MAX_RENDER_WIDTH = 9999;
|
|
@@ -165,6 +165,9 @@ export function patchStartupListing(
|
|
|
165
165
|
): void {
|
|
166
166
|
const chat = findChatContainer(tui);
|
|
167
167
|
if (!chat) {
|
|
168
|
+
// Graceful degradation: if we can't find the chat container,
|
|
169
|
+
// the startup listing won't render but the extension continues.
|
|
170
|
+
console.warn("[archimedes] Could not find chat container — startup listing disabled. This may indicate a pi TUI structure change.");
|
|
168
171
|
return;
|
|
169
172
|
}
|
|
170
173
|
const cc = chat as any;
|
|
@@ -232,6 +235,7 @@ export function patchStartupListing(
|
|
|
232
235
|
cc[PATCHED_LISTING] = true;
|
|
233
236
|
|
|
234
237
|
const origAddChild = chat.addChild.bind(chat);
|
|
238
|
+
cc[ORIG_ADD_CHILD] = origAddChild; // Store for cleanup on shutdown
|
|
235
239
|
chat.clear();
|
|
236
240
|
|
|
237
241
|
chat.addChild = (component: Component) => {
|
package/src/startup/sections.ts
CHANGED
|
@@ -40,26 +40,32 @@ export function parseSectionText(plain: string): ParsedSection | undefined {
|
|
|
40
40
|
const sectionName = detectSection(plain);
|
|
41
41
|
if (!sectionName) return undefined;
|
|
42
42
|
|
|
43
|
+
const names = extractItemsFromSection(plain, sectionName);
|
|
44
|
+
const items = deduplicateItems(names);
|
|
45
|
+
return { name: sectionName, items };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Extract item names from a section's plain text.
|
|
50
|
+
* Tracks source prefixes (npm:/git:) for Extensions/Skills sections.
|
|
51
|
+
*/
|
|
52
|
+
function extractItemsFromSection(plain: string, sectionName: SectionKey): string[] {
|
|
43
53
|
const names: string[] = [];
|
|
44
54
|
const lines = plain.split("\n");
|
|
45
55
|
let currentSource = "";
|
|
46
56
|
let sourceIndent = 0;
|
|
57
|
+
const showSource = sectionName === "Extensions" || sectionName === "Skills";
|
|
47
58
|
|
|
48
59
|
for (const line of lines) {
|
|
49
60
|
const trimmed = line.trim();
|
|
50
|
-
if (
|
|
51
|
-
if (trimmed.startsWith("[")) continue;
|
|
52
|
-
if (/^(user|project|path)$/.test(trimmed)) { currentSource = ""; sourceIndent = 0; continue; }
|
|
61
|
+
if (shouldSkipLine(trimmed, currentSource)) continue;
|
|
53
62
|
|
|
54
63
|
const indent = line.length - line.trimStart().length;
|
|
55
64
|
|
|
56
65
|
// Track package source headers (e.g. "git:github.com/...", "npm:@foo/bar")
|
|
57
|
-
// Extract name from the header itself + let children inherit the prefix
|
|
58
66
|
if (/^(git:|npm:)\S+\//.test(trimmed)) {
|
|
59
67
|
currentSource = trimmed.startsWith("git:") ? "git:" : "npm:";
|
|
60
68
|
sourceIndent = indent;
|
|
61
|
-
// Extract name from source header (e.g. "npm:@foo/pi-tavily-tools" → "npm:pi-tavily-tools")
|
|
62
|
-
const showSource = sectionName === "Extensions" || sectionName === "Skills";
|
|
63
69
|
const name = extractName(trimmed, sectionName);
|
|
64
70
|
if (name && showSource) names.push(name);
|
|
65
71
|
continue;
|
|
@@ -71,27 +77,38 @@ export function parseSectionText(plain: string): ParsedSection | undefined {
|
|
|
71
77
|
sourceIndent = 0;
|
|
72
78
|
}
|
|
73
79
|
|
|
74
|
-
if (/^(index\.(ts|js)|src|dist|out|build|lib|bin)$/i.test(trimmed)) continue;
|
|
75
|
-
// Skip resolved file paths only under source headers (e.g. "dist/index.js" under npm:)
|
|
76
|
-
if (currentSource) {
|
|
77
|
-
if (/\/(src|dist|out|build|lib|bin)\//.test(trimmed)) continue;
|
|
78
|
-
if (/\.(ts|js)$/.test(trimmed) && trimmed.includes("/") && !/SKILL\.(ts|js)$/i.test(trimmed)) continue;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
80
|
const name = extractName(trimmed, sectionName);
|
|
82
|
-
// Prompts/Context don't need source prefix — only Extensions/Skills
|
|
83
|
-
const showSource = sectionName === "Extensions" || sectionName === "Skills";
|
|
84
81
|
if (name) names.push(showSource && currentSource ? currentSource + name : name);
|
|
85
82
|
}
|
|
83
|
+
return names;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Check if a line should be skipped during section parsing. */
|
|
87
|
+
function shouldSkipLine(trimmed: string, currentSource: string): boolean {
|
|
88
|
+
if (!trimmed) return true;
|
|
89
|
+
if (trimmed.startsWith("[")) return true;
|
|
90
|
+
if (/^(user|project|path)$/.test(trimmed)) return true;
|
|
91
|
+
if (/^(index\.(ts|js)|src|dist|out|build|lib|bin)$/i.test(trimmed)) return true;
|
|
92
|
+
// Skip resolved file paths only under source headers
|
|
93
|
+
if (currentSource) {
|
|
94
|
+
if (/\/(src|dist|out|build|lib|bin)\//.test(trimmed)) return true;
|
|
95
|
+
if (/\.(ts|js)$/.test(trimmed) && trimmed.includes("/") && !/SKILL\.(ts|js)$/i.test(trimmed)) return true;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
86
99
|
|
|
87
|
-
|
|
100
|
+
/**
|
|
101
|
+
* Deduplicate items by bare name (without prefix).
|
|
102
|
+
* Prefers prefixed versions (npm:/git:) over bare names.
|
|
103
|
+
*/
|
|
104
|
+
function deduplicateItems(names: string[]): string[] {
|
|
88
105
|
const seen = new Map<string, string>();
|
|
89
106
|
for (const n of names) {
|
|
90
107
|
if (/^(index|dist|src|out|lib|bin)$/i.test(n)) continue;
|
|
91
108
|
const bare = n.replace(/^(npm:|git:)/, "");
|
|
92
109
|
if (!seen.has(bare) || n.includes(":")) seen.set(bare, n);
|
|
93
110
|
}
|
|
94
|
-
return
|
|
111
|
+
return [...seen.values()];
|
|
95
112
|
}
|
|
96
113
|
|
|
97
114
|
export function parseModelScope(plain: string): ParsedSection | undefined {
|
package/src/text.ts
CHANGED
|
@@ -3,9 +3,24 @@ import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
|
3
3
|
// Inline stripSgr — narrow SGR-only strip (no trim) for char-level checks
|
|
4
4
|
const stripSgr = (s: string) => s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
5
5
|
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Strip ALL ANSI escape sequences. Covers:
|
|
8
|
+
* - CSI: ESC [ ... letter (SGR, cursor movement, etc.)
|
|
9
|
+
* - OSC: ESC ] ... ST (operating system commands)
|
|
10
|
+
* - DCS: ESC P ... ST (device control strings)
|
|
11
|
+
* - APC: ESC _ ... ST (application program commands)
|
|
12
|
+
* - SOS: ESC ^ ... ST (start of string)
|
|
13
|
+
* - PM: ESC \x5c ... ST (privacy message)
|
|
14
|
+
* - Character set: ESC ( or ESC ) (DECSET/DECRST)
|
|
15
|
+
* Trims whitespace. Use for text extraction.
|
|
16
|
+
*/
|
|
7
17
|
export function stripAnsi(s: string): string {
|
|
8
|
-
return s
|
|
18
|
+
return s
|
|
19
|
+
.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, "") // CSI
|
|
20
|
+
.replace(/\x1b\].*?(?:\x07|\x1b\\)/g, "") // OSC
|
|
21
|
+
.replace(/\x1b[P^_\x5c].*?(?:\x07|\x1b\\)/g, "") // DCS, SOS, APC, PM
|
|
22
|
+
.replace(/\x1b[()]/g, "") // character set
|
|
23
|
+
.trim();
|
|
9
24
|
}
|
|
10
25
|
|
|
11
26
|
/** Clamp a line to maxW visible characters, preserving ANSI escapes. */
|
package/src/thinking/patch.ts
CHANGED
|
@@ -30,10 +30,15 @@ export function patchThinkingRenderer(getTheme: () => Theme): void {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
const src = proto.updateContent.toString();
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
const hasThinkingCheck = src.includes('content.type === "thinking"');
|
|
34
|
+
const hasMarkdownTheme = src.includes("this.markdownTheme");
|
|
35
|
+
if (!hasThinkingCheck || !hasMarkdownTheme) {
|
|
36
|
+
console.warn(
|
|
37
|
+
`[archimedes] Skipping thinking renderer patch — signature mismatch
|
|
38
|
+
hasThinkingCheck: ${hasThinkingCheck}, hasMarkdownTheme: ${hasMarkdownTheme}
|
|
39
|
+
This likely means pi's AssistantMessageComponent changed. The muted theme
|
|
40
|
+
for thinking blocks will not be applied.`,
|
|
41
|
+
);
|
|
37
42
|
return;
|
|
38
43
|
}
|
|
39
44
|
|