@pi-archimedes/core 0.6.0 → 0.8.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 -22
- package/src/settings-io.ts +42 -0
- package/src/startup/capture.ts +20 -6
- package/src/startup/index.ts +6 -4
- package/src/startup/sections.ts +34 -17
- package/src/text.ts +17 -2
- package/src/thinking/patch.ts +9 -4
- package/src/message/index.ts +0 -147
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pi-archimedes/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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
|
@@ -3,14 +3,17 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
|
3
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
|
+
|
|
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[] {
|
|
@@ -55,7 +58,6 @@ export function getCoreSettingsItems(config: CoreConfig): SettingItem[] {
|
|
|
55
58
|
|
|
56
59
|
// Module-level state for session lifecycle (shared between session_start and session_shutdown)
|
|
57
60
|
let coreRef: ListingRef | undefined;
|
|
58
|
-
let coreResponseTimes: number[] | undefined;
|
|
59
61
|
let coreCtx: ExtensionContext | undefined;
|
|
60
62
|
|
|
61
63
|
export function registerCore(pi: ExtensionAPI): void {
|
|
@@ -70,11 +72,26 @@ export function registerCore(pi: ExtensionAPI): void {
|
|
|
70
72
|
const listingRef = g["listingRef"] as ListingRef | undefined;
|
|
71
73
|
if (listingRef) { listingRef.settled = true; }
|
|
72
74
|
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
}
|
|
78
95
|
|
|
79
96
|
// Clear editor component override
|
|
80
97
|
if (coreCtx) { coreCtx.ui.setEditorComponent(undefined); }
|
|
@@ -110,10 +127,6 @@ export function registerCore(pi: ExtensionAPI): void {
|
|
|
110
127
|
};
|
|
111
128
|
ctx.ui.setHeader(headerFactory);
|
|
112
129
|
|
|
113
|
-
// Shared response times array (used by both patchUserMessage and message_end)
|
|
114
|
-
coreResponseTimes = [];
|
|
115
|
-
const responseTimes = coreResponseTimes;
|
|
116
|
-
|
|
117
130
|
// Set editor component
|
|
118
131
|
ctx.ui.setEditorComponent((tui: TUI, editorTheme: EditorTheme, keybindings: KeybindingsManager) => {
|
|
119
132
|
const theme = ctx.ui.theme;
|
|
@@ -127,9 +140,6 @@ export function registerCore(pi: ExtensionAPI): void {
|
|
|
127
140
|
// Patch thinking renderer
|
|
128
141
|
patchThinkingRenderer(() => ctx.ui.theme);
|
|
129
142
|
|
|
130
|
-
// Patch user message response time
|
|
131
|
-
patchUserMessage(() => ctx.ui.theme, responseTimes);
|
|
132
|
-
|
|
133
143
|
// Load config for thinking transformation
|
|
134
144
|
const config = loadCoreConfig();
|
|
135
145
|
|
|
@@ -139,13 +149,6 @@ export function registerCore(pi: ExtensionAPI): void {
|
|
|
139
149
|
if (config.codeUnindent) {
|
|
140
150
|
transformThinkingContent(event.message as any);
|
|
141
151
|
}
|
|
142
|
-
|
|
143
|
-
// Track response time from the raw message
|
|
144
|
-
const rawMsg = event.message as any;
|
|
145
|
-
if (rawMsg.duration) {
|
|
146
|
-
const idx = rawMsg.instanceIndex ?? responseTimes.length;
|
|
147
|
-
responseTimes[idx] = rawMsg.duration;
|
|
148
|
-
}
|
|
149
152
|
});
|
|
150
153
|
});
|
|
151
154
|
}
|
|
@@ -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,9 +3,7 @@ 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
|
-
import { resetInstanceCount } from "../message/index.js";
|
|
9
7
|
import { Text, Spacer, Container, TUI, truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
10
8
|
|
|
11
9
|
// Symbol keys (survive hot-reload)
|
|
@@ -14,6 +12,7 @@ const ANIM_INTERVAL = Symbol.for("splashscreen:animInterval");
|
|
|
14
12
|
const DEBOUNCE_TIMER = Symbol.for("splashscreen:debounceTimer");
|
|
15
13
|
const PATCHED_CLEAR = Symbol.for("splashscreen:clearPatched");
|
|
16
14
|
const PATCHED_LISTING = Symbol.for("splashscreen:listingPatched");
|
|
15
|
+
const ORIG_ADD_CHILD = Symbol.for("splashscreen:origAddChild");
|
|
17
16
|
|
|
18
17
|
// Animation constants
|
|
19
18
|
const MAX_RENDER_WIDTH = 9999;
|
|
@@ -166,6 +165,9 @@ export function patchStartupListing(
|
|
|
166
165
|
): void {
|
|
167
166
|
const chat = findChatContainer(tui);
|
|
168
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.");
|
|
169
171
|
return;
|
|
170
172
|
}
|
|
171
173
|
const cc = chat as any;
|
|
@@ -216,12 +218,11 @@ export function patchStartupListing(
|
|
|
216
218
|
}
|
|
217
219
|
});
|
|
218
220
|
|
|
219
|
-
// Patch clear() to
|
|
221
|
+
// Patch clear() to intercept container rebuild
|
|
220
222
|
if (!cc[PATCHED_CLEAR]) {
|
|
221
223
|
cc[PATCHED_CLEAR] = true;
|
|
222
224
|
const origClear = chat.clear.bind(chat);
|
|
223
225
|
chat.clear = () => {
|
|
224
|
-
resetInstanceCount();
|
|
225
226
|
return origClear();
|
|
226
227
|
};
|
|
227
228
|
}
|
|
@@ -234,6 +235,7 @@ export function patchStartupListing(
|
|
|
234
235
|
cc[PATCHED_LISTING] = true;
|
|
235
236
|
|
|
236
237
|
const origAddChild = chat.addChild.bind(chat);
|
|
238
|
+
cc[ORIG_ADD_CHILD] = origAddChild; // Store for cleanup on shutdown
|
|
237
239
|
chat.clear();
|
|
238
240
|
|
|
239
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
|
|
package/src/message/index.ts
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import type { Theme, UserMessageComponent } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { VERSION } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { RESET, resolvePalette, setThemeBg } from "../chrome.js";
|
|
4
|
-
|
|
5
|
-
type UserMsgCtor = typeof UserMessageComponent & { [PATCHED]?: boolean; [PATCH_VERSION]?: string };
|
|
6
|
-
|
|
7
|
-
// ── Constants ──────────────────────────────────────────────
|
|
8
|
-
|
|
9
|
-
const PATCHED = Symbol.for("splashscreen:userMsgPatched");
|
|
10
|
-
const PATCH_VERSION = Symbol.for("splashscreen:userMsgPatchVersion");
|
|
11
|
-
// Match OSC133 B (zone end) or C (zone final). v0.67 moved these from line
|
|
12
|
-
// tail to line head — we strip from wherever they sit and re-emit at the end.
|
|
13
|
-
const OSC133_RE = /\x1b\]133;[BC]\x07/g;
|
|
14
|
-
const MSG_PADDING_X = 3;
|
|
15
|
-
const TIME_COL = 9;
|
|
16
|
-
|
|
17
|
-
// ── Instance tracking ──────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
const instanceIndex = new WeakMap<object, number>();
|
|
20
|
-
let instanceCount = 0;
|
|
21
|
-
|
|
22
|
-
export function resetInstanceCount(): void {
|
|
23
|
-
instanceCount = 0;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function formatTime(ms: number): string {
|
|
27
|
-
if (ms < 1000) return `${ms}ms`;
|
|
28
|
-
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
29
|
-
const m = Math.floor(ms / 60000);
|
|
30
|
-
const s = Math.round((ms % 60000) / 1000);
|
|
31
|
-
return `${m}m ${s}s`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// ── Patch ──────────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
export function patchUserMessage(
|
|
37
|
-
getTheme: () => Theme,
|
|
38
|
-
responseTimes: number[],
|
|
39
|
-
): void {
|
|
40
|
-
try {
|
|
41
|
-
let lastBg = "";
|
|
42
|
-
const theme = getTheme();
|
|
43
|
-
const p = resolvePalette(theme);
|
|
44
|
-
lastBg = p.panelBg;
|
|
45
|
-
setThemeBg(theme, "userMessageBg", lastBg);
|
|
46
|
-
|
|
47
|
-
import("@earendil-works/pi-coding-agent").then(
|
|
48
|
-
({ UserMessageComponent }: { UserMessageComponent: UserMsgCtor }) => {
|
|
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
|
-
}
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
typeof UserMessageComponent.prototype.addChild !== "function" ||
|
|
59
|
-
typeof UserMessageComponent.prototype.render !== "function"
|
|
60
|
-
) {
|
|
61
|
-
console.warn("[splashscreen] UserMessageComponent shape changed — skipping patch");
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
UserMessageComponent[PATCHED] = true;
|
|
66
|
-
UserMessageComponent[PATCH_VERSION] = currentVersion;
|
|
67
|
-
|
|
68
|
-
const origAddChild = UserMessageComponent.prototype.addChild;
|
|
69
|
-
UserMessageComponent.prototype.addChild = function (child: any) {
|
|
70
|
-
if (child.paddingX !== undefined && !child._hephaestusPatched) {
|
|
71
|
-
child.paddingX = MSG_PADDING_X;
|
|
72
|
-
child._hephaestusPatched = true;
|
|
73
|
-
}
|
|
74
|
-
if (!instanceIndex.has(this)) {
|
|
75
|
-
instanceIndex.set(this, instanceCount++);
|
|
76
|
-
}
|
|
77
|
-
return origAddChild.call(this, child);
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const origRender = UserMessageComponent.prototype.render;
|
|
81
|
-
UserMessageComponent.prototype.render = function (
|
|
82
|
-
width: number,
|
|
83
|
-
): string[] {
|
|
84
|
-
try {
|
|
85
|
-
const currentTheme = getTheme();
|
|
86
|
-
const p = resolvePalette(currentTheme);
|
|
87
|
-
const bg = p.panelBg;
|
|
88
|
-
if (bg !== lastBg) { setThemeBg(currentTheme, "userMessageBg", bg); lastBg = bg; }
|
|
89
|
-
|
|
90
|
-
const idx = instanceIndex.get(this);
|
|
91
|
-
const elapsed = idx !== undefined ? (responseTimes[idx] ?? 0) : 0;
|
|
92
|
-
const hasTime = idx !== undefined;
|
|
93
|
-
|
|
94
|
-
const contentWidth = width - TIME_COL;
|
|
95
|
-
const lines: string[] = origRender.call(this, contentWidth);
|
|
96
|
-
if (lines.length < 3) return lines;
|
|
97
|
-
|
|
98
|
-
const timeStr = elapsed > 0 ? formatTime(elapsed) : "";
|
|
99
|
-
const timeRight = 2;
|
|
100
|
-
const timeLabel = timeStr.length > 0 ? p.time(timeStr) : "";
|
|
101
|
-
// Column that shows "42ms" right-aligned with bg — spaces for visual
|
|
102
|
-
// alignment remain, but they're wrapped in panelBg so the copy buffer
|
|
103
|
-
// only contains the label text.
|
|
104
|
-
const timeContent =
|
|
105
|
-
p.panelBg +
|
|
106
|
-
" ".repeat(Math.max(0, TIME_COL - timeStr.length - timeRight)) +
|
|
107
|
-
timeLabel +
|
|
108
|
-
" ".repeat(timeRight);
|
|
109
|
-
// Use \x1b[K (erase-to-end-of-line) instead of literal spaces for the
|
|
110
|
-
// gap between content and time column. Visually fills with panelBg but
|
|
111
|
-
// leaves nothing in the copy buffer.
|
|
112
|
-
const gapFill = p.panelBg + "\x1b[K";
|
|
113
|
-
// Empty column: just bg fill to the end — no spaces in copy buffer
|
|
114
|
-
const emptyTimeCol = p.panelBg + "\x1b[K";
|
|
115
|
-
|
|
116
|
-
const firstContent = 0;
|
|
117
|
-
|
|
118
|
-
for (let i = 0; i < lines.length; i++) {
|
|
119
|
-
let line = lines[i]!;
|
|
120
|
-
|
|
121
|
-
// Extract any OSC133 B/C markers (shell zone end/final). v0.67 placed
|
|
122
|
-
// these at the head of the last line; older versions at the tail.
|
|
123
|
-
// Strip them from the content line; re-emit after the time column.
|
|
124
|
-
const oscMatches = line.match(OSC133_RE);
|
|
125
|
-
const oscSuffix = oscMatches ? oscMatches.join("") : "";
|
|
126
|
-
if (oscSuffix) line = line.replace(OSC133_RE, "");
|
|
127
|
-
|
|
128
|
-
// Strip trailing spaces from the content portion (PI pads to contentWidth
|
|
129
|
-
// with plain spaces) and replace with \x1b[K for visual fill without spaces
|
|
130
|
-
// in the copy buffer.
|
|
131
|
-
const strippedLine = line.replace(/((?:\x1b\[[\d;]*m)*)\s+$/, "$1");
|
|
132
|
-
const col = i === firstContent && hasTime ? timeContent : emptyTimeCol;
|
|
133
|
-
lines[i] = strippedLine + gapFill + col + oscSuffix;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return lines;
|
|
137
|
-
} catch {
|
|
138
|
-
// During /resume, getTheme() may throw — fall back to default render
|
|
139
|
-
return origRender.call(this, width);
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
},
|
|
143
|
-
);
|
|
144
|
-
} catch {
|
|
145
|
-
/* skip if theme not ready */
|
|
146
|
-
}
|
|
147
|
-
}
|