pi-archimedes 0.2.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 +32 -0
- package/src/config.ts +87 -0
- package/src/index.ts +38 -0
- package/src/settings.ts +203 -0
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-archimedes",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package"
|
|
7
|
+
],
|
|
8
|
+
"description": "Visual polish and useful context for the Pi coding agent TUI",
|
|
9
|
+
"files": [
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"main": "./src/index.ts",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@pi-archimedes/core": "0.2.0",
|
|
15
|
+
"@pi-archimedes/diff": "0.2.0",
|
|
16
|
+
"@pi-archimedes/footer": "0.2.0",
|
|
17
|
+
"@pi-archimedes/image-paste": "0.2.0"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"@earendil-works/pi-coding-agent": ">=0.1.0",
|
|
21
|
+
"@earendil-works/pi-tui": ">=0.1.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"typescript": "^6.0.0"
|
|
25
|
+
},
|
|
26
|
+
"pi": {
|
|
27
|
+
"extensions": [
|
|
28
|
+
"./src/index.ts"
|
|
29
|
+
],
|
|
30
|
+
"image": "https://raw.githubusercontent.com/danielcherubini/pi-ui-hephaestus/main/docs/splash-screen.png"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
// ── Re-export core config ──────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
loadCoreConfig,
|
|
9
|
+
saveCoreConfig,
|
|
10
|
+
DEFAULT_CORE_CONFIG,
|
|
11
|
+
ANIMATION_STYLES,
|
|
12
|
+
type CoreConfig,
|
|
13
|
+
} from "@pi-archimedes/core/config";
|
|
14
|
+
export {
|
|
15
|
+
loadCoreConfig,
|
|
16
|
+
saveCoreConfig,
|
|
17
|
+
DEFAULT_CORE_CONFIG,
|
|
18
|
+
ANIMATION_STYLES,
|
|
19
|
+
type CoreConfig,
|
|
20
|
+
} from "@pi-archimedes/core/config";
|
|
21
|
+
|
|
22
|
+
// ── Re-export footer config ────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
loadFooterConfig,
|
|
26
|
+
saveFooterConfig,
|
|
27
|
+
DEFAULT_FOOTER_CONFIG,
|
|
28
|
+
type FooterConfig,
|
|
29
|
+
} from "@pi-archimedes/footer/config";
|
|
30
|
+
export {
|
|
31
|
+
loadFooterConfig,
|
|
32
|
+
saveFooterConfig,
|
|
33
|
+
DEFAULT_FOOTER_CONFIG,
|
|
34
|
+
type FooterConfig,
|
|
35
|
+
} from "@pi-archimedes/footer/config";
|
|
36
|
+
|
|
37
|
+
// ── Diff config ────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
import type { DiffConfig } from "@pi-archimedes/diff";
|
|
40
|
+
export type { DiffConfig } from "@pi-archimedes/diff";
|
|
41
|
+
|
|
42
|
+
export const DEFAULT_DIFF_CONFIG: DiffConfig = {
|
|
43
|
+
diffTheme: "github-dark",
|
|
44
|
+
diffSplitMinWidth: 150,
|
|
45
|
+
diffSplitMinCodeWidth: 60,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const SETTINGS_PATH = join(getAgentDir(), "settings.json");
|
|
49
|
+
|
|
50
|
+
export function loadDiffConfig(): DiffConfig {
|
|
51
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
52
|
+
try {
|
|
53
|
+
const full = JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
|
|
54
|
+
return { ...DEFAULT_DIFF_CONFIG, ...(full["archimedes.diff"] ?? {}) };
|
|
55
|
+
} catch {
|
|
56
|
+
/* ignore corrupt file */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return DEFAULT_DIFF_CONFIG;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function saveDiffConfig(config: DiffConfig): void {
|
|
63
|
+
let full: Record<string, unknown> = {};
|
|
64
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
65
|
+
try {
|
|
66
|
+
full = JSON.parse(readFileSync(SETTINGS_PATH, "utf-8"));
|
|
67
|
+
} catch {
|
|
68
|
+
/* ignore corrupt file */
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
full["archimedes.diff"] = config;
|
|
72
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(full, null, 2), "utf-8");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Composed config loader ─────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
export function loadAllConfig(): {
|
|
78
|
+
core: CoreConfig;
|
|
79
|
+
footer: FooterConfig;
|
|
80
|
+
diff: DiffConfig;
|
|
81
|
+
} {
|
|
82
|
+
return {
|
|
83
|
+
core: loadCoreConfig(),
|
|
84
|
+
footer: loadFooterConfig(),
|
|
85
|
+
diff: loadDiffConfig(),
|
|
86
|
+
};
|
|
87
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import { registerCore } from "@pi-archimedes/core";
|
|
4
|
+
import { registerFooter } from "@pi-archimedes/footer";
|
|
5
|
+
import { registerDiffTools } from "@pi-archimedes/diff";
|
|
6
|
+
import { registerImagePaste, initImagePasteSession, shutdownImagePaste } from "@pi-archimedes/image-paste";
|
|
7
|
+
import { loadDiffConfig } from "./config.js";
|
|
8
|
+
import { openSettings } from "./settings.js";
|
|
9
|
+
|
|
10
|
+
export default function (pi: ExtensionAPI): void {
|
|
11
|
+
// Register all component extensions
|
|
12
|
+
registerCore(pi);
|
|
13
|
+
registerFooter(pi);
|
|
14
|
+
|
|
15
|
+
// Register image paste (shortcuts, input handler, preview renderer)
|
|
16
|
+
registerImagePaste(pi);
|
|
17
|
+
|
|
18
|
+
// session_shutdown handler (top-level to prevent accumulation on /reload)
|
|
19
|
+
pi.on("session_shutdown", (_event, _ctx) => {
|
|
20
|
+
shutdownImagePaste();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
pi.on("session_start", (_event, ctx: ExtensionContext) => {
|
|
24
|
+
// Register diff tools (needs getTheme + readConfig callbacks)
|
|
25
|
+
registerDiffTools(pi, () => ctx.ui.theme, () => loadDiffConfig());
|
|
26
|
+
|
|
27
|
+
// Initialize image paste for this session
|
|
28
|
+
initImagePasteSession(ctx);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Register /archimedes command
|
|
32
|
+
pi.registerCommand("archimedes", {
|
|
33
|
+
description: "Open Archimedes settings",
|
|
34
|
+
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
35
|
+
openSettings(pi, ctx);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
package/src/settings.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { SettingsList, type SettingItem, TUI } from "@earendil-works/pi-tui";
|
|
5
|
+
|
|
6
|
+
import { getCoreSettingsItems } from "@pi-archimedes/core";
|
|
7
|
+
import { getFooterSettingsItems } from "@pi-archimedes/footer/config";
|
|
8
|
+
import { getDiffSettingsItems } from "@pi-archimedes/diff";
|
|
9
|
+
import {
|
|
10
|
+
loadAllConfig,
|
|
11
|
+
saveCoreConfig,
|
|
12
|
+
saveFooterConfig,
|
|
13
|
+
saveDiffConfig,
|
|
14
|
+
ANIMATION_STYLES,
|
|
15
|
+
type CoreConfig,
|
|
16
|
+
type FooterConfig,
|
|
17
|
+
type DiffConfig,
|
|
18
|
+
} from "./config.js";
|
|
19
|
+
|
|
20
|
+
// ── Factory: text submenu ───────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
function createTextSubmenu(opts: {
|
|
23
|
+
label: string;
|
|
24
|
+
cancelHint?: string;
|
|
25
|
+
confirmHint?: string;
|
|
26
|
+
}): SettingItem["submenu"] {
|
|
27
|
+
return (currentValue: string, done: (selectedValue?: string) => void) => {
|
|
28
|
+
const state = { value: currentValue };
|
|
29
|
+
return {
|
|
30
|
+
invalidate(): void { /* no-op */ },
|
|
31
|
+
render(): string[] {
|
|
32
|
+
const hints: string[] = [];
|
|
33
|
+
if (opts.cancelHint) hints.push(opts.cancelHint);
|
|
34
|
+
if (opts.confirmHint) hints.push(opts.confirmHint);
|
|
35
|
+
return [
|
|
36
|
+
opts.label,
|
|
37
|
+
"",
|
|
38
|
+
` ${state.value}`,
|
|
39
|
+
"",
|
|
40
|
+
hints.join(" | "),
|
|
41
|
+
];
|
|
42
|
+
},
|
|
43
|
+
handleInput(data: string): void {
|
|
44
|
+
if (data === "\x1b") { done(); return; }
|
|
45
|
+
if (data === "\r" || data === "\n") { done(state.value); return; }
|
|
46
|
+
if (data === "\x7f" || data === "\x08") { state.value = state.value.slice(0, -1); }
|
|
47
|
+
else if (data.length === 1) { state.value += data; }
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Factory: number submenu ─────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
function createNumberSubmenu(opts: {
|
|
56
|
+
label: string;
|
|
57
|
+
cancelHint?: string;
|
|
58
|
+
confirmHint?: string;
|
|
59
|
+
min?: number;
|
|
60
|
+
}): SettingItem["submenu"] {
|
|
61
|
+
return (currentValue: string, done: (selectedValue?: string) => void) => {
|
|
62
|
+
const state = { value: currentValue };
|
|
63
|
+
return {
|
|
64
|
+
invalidate(): void { /* no-op */ },
|
|
65
|
+
render(): string[] {
|
|
66
|
+
const hints: string[] = [];
|
|
67
|
+
if (opts.cancelHint) hints.push(opts.cancelHint);
|
|
68
|
+
if (opts.confirmHint) hints.push(opts.confirmHint);
|
|
69
|
+
return [
|
|
70
|
+
opts.label,
|
|
71
|
+
"",
|
|
72
|
+
` ${state.value}`,
|
|
73
|
+
"",
|
|
74
|
+
hints.join(" | "),
|
|
75
|
+
];
|
|
76
|
+
},
|
|
77
|
+
handleInput(data: string): void {
|
|
78
|
+
if (data === "\x1b") { done(); return; }
|
|
79
|
+
if (data === "\r" || data === "\n") {
|
|
80
|
+
const n = parseInt(state.value, 10);
|
|
81
|
+
if (Number.isFinite(n) && (!opts.min || n >= opts.min)) done(String(n));
|
|
82
|
+
else done();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (data === "\x7f" || data === "\x08") { state.value = state.value.slice(0, -1); }
|
|
86
|
+
else if (/^\d$/.test(data)) { state.value += data; }
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Settings UI ─────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
export function openSettings(pi: ExtensionAPI, ctx: ExtensionContext): void {
|
|
95
|
+
const allConfig = loadAllConfig();
|
|
96
|
+
|
|
97
|
+
const coreConfig: CoreConfig = { ...allConfig.core };
|
|
98
|
+
const footerConfig: FooterConfig = { ...allConfig.footer };
|
|
99
|
+
const diffConfig: DiffConfig = { ...allConfig.diff };
|
|
100
|
+
|
|
101
|
+
// Build composed items from sub-packages
|
|
102
|
+
const coreItems = getCoreSettingsItems(coreConfig);
|
|
103
|
+
const footerItems = getFooterSettingsItems();
|
|
104
|
+
const diffItems = getDiffSettingsItems();
|
|
105
|
+
|
|
106
|
+
// Add submenus for text/number fields
|
|
107
|
+
const addSubmenus = (items: SettingItem[]) => {
|
|
108
|
+
for (const item of items) {
|
|
109
|
+
if (item.id === "labelText") {
|
|
110
|
+
item.submenu = createTextSubmenu({
|
|
111
|
+
label: "Enter label text (ESC to cancel):",
|
|
112
|
+
cancelHint: "ESC: cancel",
|
|
113
|
+
confirmHint: "ENTER: confirm",
|
|
114
|
+
});
|
|
115
|
+
} else if (item.id === "labelColor") {
|
|
116
|
+
item.submenu = createTextSubmenu({
|
|
117
|
+
label: "Enter RGB color (ESC to cancel):",
|
|
118
|
+
cancelHint: "ESC: cancel",
|
|
119
|
+
confirmHint: "ENTER: confirm",
|
|
120
|
+
});
|
|
121
|
+
} else if (item.id === "diffTheme") {
|
|
122
|
+
item.submenu = createTextSubmenu({
|
|
123
|
+
label: "Enter Shiki theme (ESC to cancel):",
|
|
124
|
+
cancelHint: "ESC: cancel",
|
|
125
|
+
confirmHint: "ENTER: confirm",
|
|
126
|
+
});
|
|
127
|
+
} else if (item.id === "diffSplitMinWidth") {
|
|
128
|
+
item.submenu = createNumberSubmenu({
|
|
129
|
+
label: "Enter min width (ESC to cancel):",
|
|
130
|
+
cancelHint: "ESC: cancel",
|
|
131
|
+
confirmHint: "min 100",
|
|
132
|
+
min: 100,
|
|
133
|
+
});
|
|
134
|
+
} else if (item.id === "diffSplitMinCodeWidth") {
|
|
135
|
+
item.submenu = createNumberSubmenu({
|
|
136
|
+
label: "Enter min code width (ESC to cancel):",
|
|
137
|
+
cancelHint: "ESC: cancel",
|
|
138
|
+
confirmHint: "min 30",
|
|
139
|
+
min: 30,
|
|
140
|
+
});
|
|
141
|
+
} else if (item.id === "splitThreshold") {
|
|
142
|
+
item.submenu = createNumberSubmenu({
|
|
143
|
+
label: "Enter split threshold (ESC to cancel):",
|
|
144
|
+
cancelHint: "ESC: cancel",
|
|
145
|
+
confirmHint: "min 80",
|
|
146
|
+
min: 80,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
addSubmenus(coreItems);
|
|
153
|
+
addSubmenus(diffItems);
|
|
154
|
+
addSubmenus(footerItems);
|
|
155
|
+
|
|
156
|
+
const items: SettingItem[] = [
|
|
157
|
+
...coreItems,
|
|
158
|
+
...footerItems,
|
|
159
|
+
...diffItems,
|
|
160
|
+
{
|
|
161
|
+
id: "save",
|
|
162
|
+
label: "Save",
|
|
163
|
+
description: "Save changes and exit",
|
|
164
|
+
currentValue: "",
|
|
165
|
+
values: ["Save"],
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
ctx.ui.custom((tui: TUI, theme: Theme, _keybindings, done) => {
|
|
170
|
+
const settingsList = new SettingsList(items, 10, getSettingsListTheme(), (id: string, newValue: string) => {
|
|
171
|
+
switch (id) {
|
|
172
|
+
// ── Core settings ──
|
|
173
|
+
case "mutedTheme": coreConfig.mutedTheme = newValue === "On"; break;
|
|
174
|
+
case "codeUnindent": coreConfig.codeUnindent = newValue === "On"; break;
|
|
175
|
+
case "labelText": coreConfig.labelText = newValue; break;
|
|
176
|
+
case "labelColor": coreConfig.labelColor = newValue; break;
|
|
177
|
+
case "animationStyle": coreConfig.animationStyle = newValue as CoreConfig["animationStyle"]; break;
|
|
178
|
+
|
|
179
|
+
// ── Footer settings ──
|
|
180
|
+
case "splitThreshold": footerConfig.splitThreshold = parseInt(newValue, 10); break;
|
|
181
|
+
|
|
182
|
+
// ── Diff settings ──
|
|
183
|
+
case "diffTheme": diffConfig.diffTheme = newValue; break;
|
|
184
|
+
case "diffSplitMinWidth": diffConfig.diffSplitMinWidth = parseInt(newValue, 10); break;
|
|
185
|
+
case "diffSplitMinCodeWidth": diffConfig.diffSplitMinCodeWidth = parseInt(newValue, 10); break;
|
|
186
|
+
|
|
187
|
+
// ── Save ──
|
|
188
|
+
case "save": {
|
|
189
|
+
saveCoreConfig(coreConfig);
|
|
190
|
+
saveFooterConfig(footerConfig);
|
|
191
|
+
saveDiffConfig(diffConfig);
|
|
192
|
+
done(undefined);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}, () => {
|
|
197
|
+
// ESC cancels without saving
|
|
198
|
+
done(undefined);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return settingsList;
|
|
202
|
+
});
|
|
203
|
+
}
|