@nmzpy/pi-ember-stack 0.1.6 → 0.2.1
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 +12 -2
- package/plugins/devin-auth/extensions/index.ts +45 -7
- package/plugins/devin-auth/src/models.ts +42 -196
- package/plugins/index.ts +29 -6
- package/plugins/pi-compact-tools/index.ts +16 -191
- package/plugins/pi-compact-tools/renderer.ts +419 -0
- package/plugins/pi-custom-agents/index.ts +489 -343
- package/plugins/pi-custom-agents/subagent/agents/coder.md +1 -0
- package/plugins/pi-custom-agents/subagent/agents/scout.md +16 -37
- package/plugins/pi-custom-agents/subagent/extensions/index.ts +2 -2
- package/plugins/pi-custom-agents/subagent/extensions/runner.ts +31 -5
- package/plugins/pi-ember-fff/index.ts +717 -0
- package/plugins/pi-ember-fff/query.ts +87 -0
- package/plugins/pi-ember-fff/test/query.test.ts +66 -0
- package/plugins/pi-ember-fff/test/renderer.test.ts +376 -0
- package/plugins/pi-ember-tps/index.ts +122 -0
- package/plugins/pi-ember-ui/ember.json +96 -0
- package/plugins/pi-ember-ui/index.ts +618 -0
- package/plugins/pi-ember-ui/mode-colors.ts +168 -0
- package/tsconfig.json +2 -1
- package/plugins/pi-custom-agents/subagent/agents/architect.md +0 -56
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
|
3
|
+
"name": "ember",
|
|
4
|
+
"vars": {
|
|
5
|
+
"emberAccent": "#EB6E00",
|
|
6
|
+
"emberAccent90": "#EB6E00",
|
|
7
|
+
"emberAccent75": "#D9640F",
|
|
8
|
+
"emberAccent60": "#C25A17",
|
|
9
|
+
"emberAccent45": "#A8501F",
|
|
10
|
+
"emberAccent30": "#8F4627",
|
|
11
|
+
"emberAccent15": "#783C2F",
|
|
12
|
+
"emberGray": "#808080",
|
|
13
|
+
"emberDarkGray": "#505050",
|
|
14
|
+
"cyan": "#00d7ff",
|
|
15
|
+
"blue": "#5f87ff",
|
|
16
|
+
"green": "#b5bd68",
|
|
17
|
+
"red": "#cc6666",
|
|
18
|
+
"yellow": "#ffff00",
|
|
19
|
+
"text": "#d4d4d4",
|
|
20
|
+
"gray": "#808080",
|
|
21
|
+
"dimGray": "#666666",
|
|
22
|
+
"darkGray": "#505050",
|
|
23
|
+
"accent": "#EB6E00",
|
|
24
|
+
"selectedBg": "#3a3a4a",
|
|
25
|
+
"userMsgBg": "#343541",
|
|
26
|
+
"toolPendingBg": "#282832",
|
|
27
|
+
"toolSuccessBg": "#283228",
|
|
28
|
+
"toolErrorBg": "#3c2828",
|
|
29
|
+
"customMsgBg": "#2d2838"
|
|
30
|
+
},
|
|
31
|
+
"colors": {
|
|
32
|
+
"accent": "emberAccent",
|
|
33
|
+
"border": "emberAccent",
|
|
34
|
+
"borderAccent": "emberAccent",
|
|
35
|
+
"borderMuted": "emberDarkGray",
|
|
36
|
+
"success": "green",
|
|
37
|
+
"error": "red",
|
|
38
|
+
"warning": "yellow",
|
|
39
|
+
"muted": "gray",
|
|
40
|
+
"dim": "dimGray",
|
|
41
|
+
"text": "text",
|
|
42
|
+
"thinkingText": "gray",
|
|
43
|
+
|
|
44
|
+
"selectedBg": "selectedBg",
|
|
45
|
+
"userMessageBg": "userMsgBg",
|
|
46
|
+
"userMessageText": "text",
|
|
47
|
+
"customMessageBg": "customMsgBg",
|
|
48
|
+
"customMessageText": "text",
|
|
49
|
+
"customMessageLabel": "#9575cd",
|
|
50
|
+
"toolPendingBg": "toolPendingBg",
|
|
51
|
+
"toolSuccessBg": "toolSuccessBg",
|
|
52
|
+
"toolErrorBg": "toolErrorBg",
|
|
53
|
+
"toolTitle": "text",
|
|
54
|
+
"toolOutput": "gray",
|
|
55
|
+
|
|
56
|
+
"mdHeading": "#f0c674",
|
|
57
|
+
"mdLink": "#81a2be",
|
|
58
|
+
"mdLinkUrl": "dimGray",
|
|
59
|
+
"mdCode": "text",
|
|
60
|
+
"mdCodeBlock": "green",
|
|
61
|
+
"mdCodeBlockBorder": "gray",
|
|
62
|
+
"mdQuote": "gray",
|
|
63
|
+
"mdQuoteBorder": "gray",
|
|
64
|
+
"mdHr": "gray",
|
|
65
|
+
"mdListBullet": "emberAccent",
|
|
66
|
+
|
|
67
|
+
"toolDiffAdded": "green",
|
|
68
|
+
"toolDiffRemoved": "red",
|
|
69
|
+
"toolDiffContext": "gray",
|
|
70
|
+
|
|
71
|
+
"syntaxComment": "#6A9955",
|
|
72
|
+
"syntaxKeyword": "#569CD6",
|
|
73
|
+
"syntaxFunction": "#DCDCAA",
|
|
74
|
+
"syntaxVariable": "#9CDCFE",
|
|
75
|
+
"syntaxString": "#CE9178",
|
|
76
|
+
"syntaxNumber": "#B5CEA8",
|
|
77
|
+
"syntaxType": "#4EC9B0",
|
|
78
|
+
"syntaxOperator": "#D4D4D4",
|
|
79
|
+
"syntaxPunctuation": "#D4D4D4",
|
|
80
|
+
|
|
81
|
+
"thinkingOff": "emberDarkGray",
|
|
82
|
+
"thinkingMinimal": "emberAccent15",
|
|
83
|
+
"thinkingLow": "emberAccent30",
|
|
84
|
+
"thinkingMedium": "emberAccent45",
|
|
85
|
+
"thinkingHigh": "emberAccent60",
|
|
86
|
+
"thinkingXhigh": "emberAccent75",
|
|
87
|
+
"thinkingMax": "emberAccent90",
|
|
88
|
+
|
|
89
|
+
"bashMode": "green"
|
|
90
|
+
},
|
|
91
|
+
"export": {
|
|
92
|
+
"pageBg": "#18181e",
|
|
93
|
+
"cardBg": "#1e1e24",
|
|
94
|
+
"infoBg": "#3c3728"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import {
|
|
5
|
+
Theme,
|
|
6
|
+
type ExtensionAPI,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { Editor, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
9
|
+
import {
|
|
10
|
+
buildCodeBgHex,
|
|
11
|
+
buildThemeBgColors,
|
|
12
|
+
buildThemeFgColors,
|
|
13
|
+
getActiveModeColor,
|
|
14
|
+
} from "./mode-colors.ts";
|
|
15
|
+
|
|
16
|
+
const SOURCE_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const THEME_JSON = path.join(SOURCE_ROOT, "ember.json");
|
|
18
|
+
const THEME_NAME = "ember";
|
|
19
|
+
|
|
20
|
+
const THINKING_FRAME_INTERVAL_MS = 60;
|
|
21
|
+
const LOGO = [
|
|
22
|
+
" ██████ ██",
|
|
23
|
+
" ██ ██ ██",
|
|
24
|
+
" ██████ ██",
|
|
25
|
+
" ██ ██",
|
|
26
|
+
" ██ ██",
|
|
27
|
+
" ██ ██",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const SHADOW_OFFSET_X = 1;
|
|
31
|
+
const SHADOW_OFFSET_Y = 0;
|
|
32
|
+
const SHADOW_GLYPH = "\u2591";
|
|
33
|
+
const SHADOW_OPACITY = 0.4;
|
|
34
|
+
|
|
35
|
+
let thinkingActive = false;
|
|
36
|
+
let workingActive = false;
|
|
37
|
+
let thinkingFrame = 0;
|
|
38
|
+
let thinkingInterval: ReturnType<typeof setInterval> | undefined;
|
|
39
|
+
let logoAnimating = false;
|
|
40
|
+
let logoFrame = 0;
|
|
41
|
+
let logoTimer: ReturnType<typeof setInterval> | undefined;
|
|
42
|
+
let editorRenderPatched = false;
|
|
43
|
+
let requestRender: (() => void) | undefined;
|
|
44
|
+
let sessionCtx: any;
|
|
45
|
+
|
|
46
|
+
type EditorWithBorder = Editor & {
|
|
47
|
+
borderColor: (text: string) => string;
|
|
48
|
+
getText: () => string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function currentLabelText(): string {
|
|
52
|
+
return thinkingActive ? "Thinking" : "Working";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function stopThinkingAnimation(): void {
|
|
56
|
+
thinkingActive = false;
|
|
57
|
+
if (!workingActive) {
|
|
58
|
+
if (thinkingInterval) clearInterval(thinkingInterval);
|
|
59
|
+
thinkingInterval = undefined;
|
|
60
|
+
}
|
|
61
|
+
requestRender?.();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function startThinkingAnimation(): void {
|
|
65
|
+
thinkingActive = true;
|
|
66
|
+
if (!thinkingInterval) {
|
|
67
|
+
thinkingFrame = 0;
|
|
68
|
+
thinkingInterval = setInterval(() => {
|
|
69
|
+
thinkingFrame += 0.06;
|
|
70
|
+
if (thinkingFrame > 1) thinkingFrame -= 1;
|
|
71
|
+
requestRender?.();
|
|
72
|
+
}, THINKING_FRAME_INTERVAL_MS);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function startWorkingAnimation(): void {
|
|
77
|
+
workingActive = true;
|
|
78
|
+
if (!thinkingInterval) {
|
|
79
|
+
thinkingFrame = 0;
|
|
80
|
+
thinkingInterval = setInterval(() => {
|
|
81
|
+
thinkingFrame += 0.06;
|
|
82
|
+
if (thinkingFrame > 1) thinkingFrame -= 1;
|
|
83
|
+
requestRender?.();
|
|
84
|
+
}, THINKING_FRAME_INTERVAL_MS);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function stopWorkingAnimation(): void {
|
|
89
|
+
workingActive = false;
|
|
90
|
+
if (!thinkingActive && thinkingInterval) {
|
|
91
|
+
clearInterval(thinkingInterval);
|
|
92
|
+
thinkingInterval = undefined;
|
|
93
|
+
}
|
|
94
|
+
requestRender?.();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function wrapThemeWithCodeBg(base: Theme): Theme {
|
|
98
|
+
return new Proxy(base, {
|
|
99
|
+
get(target: Theme, prop: string | symbol, receiver: any) {
|
|
100
|
+
if (prop === "fg") {
|
|
101
|
+
return (color: string, text: string) => {
|
|
102
|
+
if (color === "mdCode") {
|
|
103
|
+
return (
|
|
104
|
+
liveCodeBgAnsi +
|
|
105
|
+
target.getFgAnsi("mdCode" as any) +
|
|
106
|
+
" " + text + " " +
|
|
107
|
+
"\x1b[39m\x1b[49m"
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
return target.fg(color as any, text);
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const val = Reflect.get(target, prop, receiver);
|
|
114
|
+
return typeof val === "function" ? val.bind(target) : val;
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function fgAnsi(hex: string): string {
|
|
120
|
+
const [r, g, b] = hexToRgbTriplet(hex);
|
|
121
|
+
return `\x1b[38;2;${r};${g};${b}m`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function bgAnsi(hex: string): string {
|
|
125
|
+
const [r, g, b] = hexToRgbTriplet(hex);
|
|
126
|
+
return `\x1b[48;2;${r};${g};${b}m`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const THEME_KEY = Symbol.for("@earendil-works/pi-coding-agent:theme");
|
|
130
|
+
const THEME_KEY_OLD = Symbol.for("@mariozechner/pi-coding-agent:theme");
|
|
131
|
+
|
|
132
|
+
let liveTheme: Theme | undefined;
|
|
133
|
+
let liveCodeBgAnsi = "";
|
|
134
|
+
|
|
135
|
+
function installProxiedTheme(fgColors: Record<string, string>, bgColors: Record<string, string>, codeBg: string): void {
|
|
136
|
+
const base = new Theme(
|
|
137
|
+
fgColors as any,
|
|
138
|
+
bgColors as any,
|
|
139
|
+
"truecolor",
|
|
140
|
+
{ name: "ember" },
|
|
141
|
+
);
|
|
142
|
+
liveTheme = base;
|
|
143
|
+
liveCodeBgAnsi = bgAnsi(codeBg);
|
|
144
|
+
const wrapped = wrapThemeWithCodeBg(base);
|
|
145
|
+
(globalThis as any)[THEME_KEY] = wrapped;
|
|
146
|
+
(globalThis as any)[THEME_KEY_OLD] = wrapped;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function applyDynamicTheme(): void {
|
|
150
|
+
const accent = getActiveModeColor();
|
|
151
|
+
const fgColors = buildThemeFgColors(accent);
|
|
152
|
+
const bgColors = buildThemeBgColors(accent);
|
|
153
|
+
const codeBg = buildCodeBgHex(accent);
|
|
154
|
+
|
|
155
|
+
if (liveTheme) {
|
|
156
|
+
liveCodeBgAnsi = bgAnsi(codeBg);
|
|
157
|
+
updateLiveThemeColors(fgColors, bgColors);
|
|
158
|
+
(tuiRef as any)?.invalidate();
|
|
159
|
+
requestRender?.();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
installProxiedTheme(fgColors, bgColors, codeBg);
|
|
163
|
+
requestRender?.();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function updateLiveThemeColors(fgColors: Record<string, string>, bgColors: Record<string, string>): void {
|
|
167
|
+
if (!liveTheme) return;
|
|
168
|
+
const fgMap = (liveTheme as any).fgColors as Map<string, string>;
|
|
169
|
+
const bgMap = (liveTheme as any).bgColors as Map<string, string>;
|
|
170
|
+
for (const [key, hex] of Object.entries(fgColors)) {
|
|
171
|
+
fgMap.set(key, fgAnsi(hex));
|
|
172
|
+
}
|
|
173
|
+
for (const [key, hex] of Object.entries(bgColors)) {
|
|
174
|
+
bgMap.set(key, bgAnsi(hex));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function renderGradientLabel(text: string, accent: string): string {
|
|
179
|
+
const chars = [...text];
|
|
180
|
+
const len = chars.length;
|
|
181
|
+
if (len === 0) return "";
|
|
182
|
+
const textHex = "#808080";
|
|
183
|
+
const result: string[] = [];
|
|
184
|
+
for (let i = 0; i < len; i++) {
|
|
185
|
+
const charPos = i / Math.max(1, len - 1);
|
|
186
|
+
const dist = charPos - thinkingFrame;
|
|
187
|
+
const wrapped = dist < -0.5 ? dist + 1 : dist > 0.5 ? dist - 1 : dist;
|
|
188
|
+
const intensity = Math.exp(-(wrapped * wrapped) * 8);
|
|
189
|
+
const hex = blendToHex(textHex, accent, intensity);
|
|
190
|
+
result.push(colorize(chars[i], hex));
|
|
191
|
+
}
|
|
192
|
+
return result.join("");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function installThinkingBorderOverride(): void {
|
|
196
|
+
if (editorRenderPatched) return;
|
|
197
|
+
editorRenderPatched = true;
|
|
198
|
+
const originalRender = Editor.prototype.render;
|
|
199
|
+
Editor.prototype.render = function renderThinkingBorder(this: EditorWithBorder, width: number): string[] {
|
|
200
|
+
const accent = getActiveModeColor();
|
|
201
|
+
const accentLight = lightenHex(accent, 0.5);
|
|
202
|
+
const accentBorder = (text: string): string => colorize(text, accentLight);
|
|
203
|
+
const originalBorderColor = this.borderColor;
|
|
204
|
+
this.borderColor = accentBorder;
|
|
205
|
+
const innerWidth = Math.max(1, width - 2);
|
|
206
|
+
const lines = originalRender.call(this, innerWidth);
|
|
207
|
+
this.borderColor = originalBorderColor;
|
|
208
|
+
|
|
209
|
+
const stripped = (s: string): string => s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
210
|
+
const isBorderLine = (s: string): boolean => {
|
|
211
|
+
const raw = stripped(s);
|
|
212
|
+
return raw.length > 0 && [...raw].every((ch) => ch === "\u2500" || ch === " ");
|
|
213
|
+
};
|
|
214
|
+
const borderIndices: number[] = [];
|
|
215
|
+
for (let i = 0; i < lines.length; i++) {
|
|
216
|
+
if (isBorderLine(lines[i])) borderIndices.push(i);
|
|
217
|
+
}
|
|
218
|
+
const topIdx = borderIndices[0] ?? 0;
|
|
219
|
+
const bottomBorderIdx = borderIndices.length > 1 ? borderIndices[borderIndices.length - 1] : -1;
|
|
220
|
+
|
|
221
|
+
for (let i = 1; i < lines.length; i++) {
|
|
222
|
+
if (i === bottomBorderIdx) continue;
|
|
223
|
+
lines[i] = ` ${lines[i]} `;
|
|
224
|
+
}
|
|
225
|
+
lines[topIdx] = accentBorder("\u2500".repeat(width));
|
|
226
|
+
if (bottomBorderIdx >= 0) {
|
|
227
|
+
const inputText = this.getText?.() ?? "";
|
|
228
|
+
if (inputText.trimStart().startsWith("/")) {
|
|
229
|
+
lines[bottomBorderIdx] = ` ${colorWithOpacity("\u2500".repeat(width - 2), accentLight, 0.1875)} `;
|
|
230
|
+
} else {
|
|
231
|
+
lines[bottomBorderIdx] = accentBorder("\u2500".repeat(width));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const lastLineIdx = lines.length - 1;
|
|
235
|
+
if (lastLineIdx > bottomBorderIdx && lastLineIdx > 0) {
|
|
236
|
+
lines[lastLineIdx] = accentBorder("\u2500".repeat(width));
|
|
237
|
+
}
|
|
238
|
+
if (lines.length === 0) return lines;
|
|
239
|
+
if (!thinkingActive && !workingActive) return lines;
|
|
240
|
+
|
|
241
|
+
const labelText = thinkingActive ? "Thinking" : "Working";
|
|
242
|
+
const label = ` ${renderGradientLabel(labelText, accent)} `;
|
|
243
|
+
const prefixLen = 2 + visibleWidth(label);
|
|
244
|
+
const remaining = Math.max(0, width - prefixLen);
|
|
245
|
+
lines[topIdx] = accentBorder("\u2500".repeat(2)) + label +
|
|
246
|
+
accentBorder("\u2500".repeat(remaining));
|
|
247
|
+
return lines;
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function colorWithOpacity(text: string, hex: string, opacity: number): string {
|
|
252
|
+
const source = hex.slice(1);
|
|
253
|
+
const base = [24, 24, 30];
|
|
254
|
+
const rgb = [
|
|
255
|
+
parseInt(source.slice(0, 2), 16),
|
|
256
|
+
parseInt(source.slice(2, 4), 16),
|
|
257
|
+
parseInt(source.slice(4, 6), 16),
|
|
258
|
+
].map((value, index) => Math.round(base[index] + (value - base[index]) * opacity));
|
|
259
|
+
return `\u001b[38;2;${rgb.join(";")}m${text}\u001b[39m`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function hexToRgbTriplet(hex: string): [number, number, number] {
|
|
263
|
+
return [
|
|
264
|
+
parseInt(hex.slice(1, 3), 16),
|
|
265
|
+
parseInt(hex.slice(3, 5), 16),
|
|
266
|
+
parseInt(hex.slice(5, 7), 16),
|
|
267
|
+
];
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function blendToHex(fgHex: string, bgHex: string, opacity: number): string {
|
|
271
|
+
const [fr, fg, fb] = hexToRgbTriplet(fgHex);
|
|
272
|
+
const [br, bg, bb] = hexToRgbTriplet(bgHex);
|
|
273
|
+
const r = Math.round(br + (fr - br) * opacity);
|
|
274
|
+
const g = Math.round(bg + (fg - bg) * opacity);
|
|
275
|
+
const b = Math.round(bb + (fb - bb) * opacity);
|
|
276
|
+
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function lightenHex(hex: string, amount: number): string {
|
|
280
|
+
const [r, g, b] = hexToRgbTriplet(hex);
|
|
281
|
+
const lr = Math.round(r + (255 - r) * amount);
|
|
282
|
+
const lg = Math.round(g + (255 - g) * amount);
|
|
283
|
+
const lb = Math.round(b + (255 - b) * amount);
|
|
284
|
+
return `#${lr.toString(16).padStart(2, "0")}${lg.toString(16).padStart(2, "0")}${lb.toString(16).padStart(2, "0")}`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function colorize(text: string, hex: string): string {
|
|
288
|
+
const [r, g, b] = hexToRgbTriplet(hex);
|
|
289
|
+
return `\x1b[38;2;${r};${g};${b}m${text}\x1b[39m`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function folderNameFromCwd(cwd: string): string {
|
|
293
|
+
return cwd.split(/[/\\]/).filter(Boolean).pop() ?? cwd;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function welcomeConfigPath(): string {
|
|
297
|
+
return path.join(
|
|
298
|
+
process.env.PI_HOME || path.join(process.env.HOME || process.env.USERPROFILE || "", ".pi", "agent"),
|
|
299
|
+
"welcome.json",
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function setWelcomeUpdates(enabled: boolean): void {
|
|
304
|
+
const file = welcomeConfigPath();
|
|
305
|
+
let config: Record<string, unknown> = {};
|
|
306
|
+
try {
|
|
307
|
+
config = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
308
|
+
} catch { /* first-run, no file yet */ }
|
|
309
|
+
config.updates = enabled;
|
|
310
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
311
|
+
fs.writeFileSync(file, `${JSON.stringify(config, null, "\t")}\n`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
type RadialPoint = { x: number; y: number; r: number; g: number; b: number; falloff: number };
|
|
315
|
+
|
|
316
|
+
function lerp(a: number, b: number, t: number): number {
|
|
317
|
+
return a + (b - a) * t;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function radialColorForCell(x: number, y: number, points: RadialPoint[]): [number, number, number] {
|
|
321
|
+
let totalWeight = 0;
|
|
322
|
+
let r = 0, g = 0, b = 0;
|
|
323
|
+
for (const p of points) {
|
|
324
|
+
const dx = x - p.x;
|
|
325
|
+
const dy = y - p.y;
|
|
326
|
+
const dist = Math.sqrt(dx * dx + dy * dy);
|
|
327
|
+
const weight = Math.exp(-(dist * dist) / (p.falloff * p.falloff));
|
|
328
|
+
r += p.r * weight;
|
|
329
|
+
g += p.g * weight;
|
|
330
|
+
b += p.b * weight;
|
|
331
|
+
totalWeight += weight;
|
|
332
|
+
}
|
|
333
|
+
return [
|
|
334
|
+
Math.round(r / totalWeight),
|
|
335
|
+
Math.round(g / totalWeight),
|
|
336
|
+
Math.round(b / totalWeight),
|
|
337
|
+
];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function startLogoAnimation(): void {
|
|
341
|
+
if (logoTimer) return;
|
|
342
|
+
logoAnimating = true;
|
|
343
|
+
logoFrame = 0;
|
|
344
|
+
logoTimer = setInterval(() => {
|
|
345
|
+
logoFrame += 0.04;
|
|
346
|
+
if (logoFrame > 1) logoFrame -= 1;
|
|
347
|
+
requestRender?.();
|
|
348
|
+
}, 60);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function stopLogoAnimation(): void {
|
|
352
|
+
logoAnimating = false;
|
|
353
|
+
if (logoTimer) {
|
|
354
|
+
clearInterval(logoTimer);
|
|
355
|
+
logoTimer = undefined;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function renderLogoWithGradient(accent: string): string[] {
|
|
360
|
+
const [ar, ag, ab] = hexToRgbTriplet(accent);
|
|
361
|
+
const accent70 = blendToHex(accent, "#18181e", 0.7);
|
|
362
|
+
const [s7r, s7g, s7b] = hexToRgbTriplet(accent70);
|
|
363
|
+
const accent40 = blendToHex(accent, "#18181e", 0.4);
|
|
364
|
+
const [s4r, s4g, s4b] = hexToRgbTriplet(accent40);
|
|
365
|
+
const points: RadialPoint[] = [
|
|
366
|
+
{ x: 2, y: 0, r: 255, g: 255, b: 255, falloff: 3 },
|
|
367
|
+
{ x: 10, y: 1, r: ar, g: ag, b: ab, falloff: 4 },
|
|
368
|
+
{ x: 5, y: 3, r: s7r, g: s7g, b: s7b, falloff: 3.5 },
|
|
369
|
+
{ x: 11, y: 5, r: s4r, g: s4g, b: s4b, falloff: 2.5 },
|
|
370
|
+
{ x: 0, y: 4, r: 200, g: 180, b: 140, falloff: 2 },
|
|
371
|
+
];
|
|
372
|
+
|
|
373
|
+
const logoRows = LOGO.length;
|
|
374
|
+
const logoCols = LOGO[0].length;
|
|
375
|
+
const gridCols = logoCols + SHADOW_OFFSET_X;
|
|
376
|
+
const gridRows = logoRows + Math.ceil(SHADOW_OFFSET_Y);
|
|
377
|
+
|
|
378
|
+
const grid: Array<Array<{ ch: string; rgb?: [number, number, number] }>> = [];
|
|
379
|
+
for (let row = 0; row < gridRows; row++) {
|
|
380
|
+
grid.push(new Array(gridCols).fill(null).map(() => ({ ch: " " })));
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
for (let row = 0; row < logoRows; row++) {
|
|
384
|
+
for (let col = 0; col < LOGO[row].length; col++) {
|
|
385
|
+
const ch = LOGO[row][col];
|
|
386
|
+
if (ch === "\u2588") {
|
|
387
|
+
let [r, g, b] = radialColorForCell(col, row, points);
|
|
388
|
+
if (logoAnimating) {
|
|
389
|
+
const charPos = col / Math.max(1, logoCols - 1);
|
|
390
|
+
const dist = charPos - logoFrame;
|
|
391
|
+
const wrapped = dist < -0.5 ? dist + 1 : dist > 0.5 ? dist - 1 : dist;
|
|
392
|
+
const intensity = Math.exp(-(wrapped * wrapped) * 6);
|
|
393
|
+
r = Math.round(r + (255 - r) * intensity);
|
|
394
|
+
g = Math.round(g + (255 - g) * intensity);
|
|
395
|
+
b = Math.round(b + (255 - b) * intensity);
|
|
396
|
+
}
|
|
397
|
+
grid[row][col] = { ch: "\u2588", rgb: [r, g, b] };
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const halfOpacity = SHADOW_OPACITY / 2;
|
|
403
|
+
const placeShadow = (sRow: number, sCol: number, opacity: number): void => {
|
|
404
|
+
if (sRow < 0 || sRow >= gridRows || sCol < 0 || sCol >= gridCols) return;
|
|
405
|
+
const existing = grid[sRow][sCol];
|
|
406
|
+
if (existing.ch !== " ") return;
|
|
407
|
+
const [gr, gg, gb] = radialColorForCell(sCol, sRow, points);
|
|
408
|
+
const bgR = 24, bgG = 24, bgB = 30;
|
|
409
|
+
const sr = Math.round(bgR + (gr - bgR) * opacity);
|
|
410
|
+
const sg = Math.round(bgG + (gg - bgG) * opacity);
|
|
411
|
+
const sb = Math.round(bgB + (gb - bgB) * opacity);
|
|
412
|
+
grid[sRow][sCol] = { ch: SHADOW_GLYPH, rgb: [sr, sg, sb] };
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
for (let row = 0; row < logoRows; row++) {
|
|
416
|
+
for (let col = 0; col < LOGO[row].length; col++) {
|
|
417
|
+
const ch = LOGO[row][col];
|
|
418
|
+
if (ch === "\u2588") {
|
|
419
|
+
const sCol = col + SHADOW_OFFSET_X;
|
|
420
|
+
const sRowBase = row + Math.floor(SHADOW_OFFSET_Y);
|
|
421
|
+
placeShadow(sRowBase, sCol, halfOpacity);
|
|
422
|
+
placeShadow(sRowBase + 1, sCol, halfOpacity);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return grid.map((rowCells) =>
|
|
428
|
+
rowCells.map((cell) => {
|
|
429
|
+
if (cell.rgb) {
|
|
430
|
+
const [r, g, b] = cell.rgb;
|
|
431
|
+
return `\x1b[38;2;${r};${g};${b}m${cell.ch}\x1b[39m`;
|
|
432
|
+
}
|
|
433
|
+
return cell.ch;
|
|
434
|
+
}).join("")
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
let tuiRef: any;
|
|
439
|
+
|
|
440
|
+
function installStartupHeader(ctx: any): void {
|
|
441
|
+
if (ctx.mode !== "tui") return;
|
|
442
|
+
|
|
443
|
+
ctx.ui.setHeader((tui: any, theme: any) => {
|
|
444
|
+
tuiRef = tui;
|
|
445
|
+
return {
|
|
446
|
+
render(width: number): string[] {
|
|
447
|
+
// Re-read every render so model/dir/mode changes are reflected.
|
|
448
|
+
const dir = folderNameFromCwd(ctx.sessionManager?.getCwd?.() ?? ctx.cwd ?? process.cwd());
|
|
449
|
+
const model = ctx.model;
|
|
450
|
+
const modelName = model?.name ?? model?.id ?? "no model";
|
|
451
|
+
|
|
452
|
+
const accent = getActiveModeColor();
|
|
453
|
+
const logoLines = renderLogoWithGradient(accent);
|
|
454
|
+
const logoWidth = visibleWidth(logoLines[0] ?? "");
|
|
455
|
+
const leftPad = Math.max(0, Math.floor((width - logoWidth) / 2));
|
|
456
|
+
const padStr = " ".repeat(leftPad);
|
|
457
|
+
|
|
458
|
+
const infoLine = `${theme.fg("text", modelName)} ${theme.fg("accent", "\u2022")} ${theme.fg("dim", dir)}`;
|
|
459
|
+
const infoPad = Math.max(0, Math.floor((width - visibleWidth(infoLine)) / 2));
|
|
460
|
+
const infoPadStr = " ".repeat(infoPad);
|
|
461
|
+
|
|
462
|
+
const lines = [
|
|
463
|
+
...logoLines.map((line) => padStr + line),
|
|
464
|
+
"",
|
|
465
|
+
infoPadStr + infoLine,
|
|
466
|
+
];
|
|
467
|
+
return lines.map((line) => visibleWidth(line) > width ? truncateToWidth(line, width) : line);
|
|
468
|
+
},
|
|
469
|
+
invalidate() {},
|
|
470
|
+
};
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function getAgentDir(): string {
|
|
475
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
476
|
+
return path.join(home, ".pi", "agent");
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function getThemesDir(): string {
|
|
480
|
+
return path.join(getAgentDir(), "themes");
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function ensureThemeInstalled(): void {
|
|
484
|
+
const themesDir = getThemesDir();
|
|
485
|
+
fs.mkdirSync(themesDir, { recursive: true });
|
|
486
|
+
const dest = path.join(themesDir, `${THEME_NAME}.json`);
|
|
487
|
+
if (!fs.existsSync(dest)) {
|
|
488
|
+
fs.copyFileSync(THEME_JSON, dest);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const srcContent = fs.readFileSync(THEME_JSON, "utf-8");
|
|
492
|
+
const destContent = fs.readFileSync(dest, "utf-8");
|
|
493
|
+
if (srcContent !== destContent) {
|
|
494
|
+
fs.copyFileSync(THEME_JSON, dest);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export default function piEmberUiPlugin(pi: ExtensionAPI): void {
|
|
499
|
+
ensureThemeInstalled();
|
|
500
|
+
installThinkingBorderOverride();
|
|
501
|
+
applyDynamicTheme();
|
|
502
|
+
|
|
503
|
+
pi.on("session_start", (_event, ctx) => {
|
|
504
|
+
sessionCtx = ctx;
|
|
505
|
+
liveTheme = undefined;
|
|
506
|
+
if (ctx.mode === "tui") {
|
|
507
|
+
requestRender = () => ctx.ui.setStatus("pi-ember-ui-thinking-tick", undefined);
|
|
508
|
+
ctx.ui.setWorkingVisible(false);
|
|
509
|
+
ctx.ui.setHiddenThinkingLabel("");
|
|
510
|
+
}
|
|
511
|
+
applyDynamicTheme();
|
|
512
|
+
if (ctx.mode === "tui") {
|
|
513
|
+
installStartupHeader(ctx);
|
|
514
|
+
if (tuiRef?.children) {
|
|
515
|
+
for (const child of tuiRef.children) {
|
|
516
|
+
if (child?.children?.length > 1 && typeof child.addChild === "function" && child !== tuiRef) {
|
|
517
|
+
const nonSpacers = child.children.filter((c: any) => c?.constructor?.name !== "Spacer");
|
|
518
|
+
child.children.length = 0;
|
|
519
|
+
child.children.push(...nonSpacers);
|
|
520
|
+
break;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
startLogoAnimation();
|
|
525
|
+
ctx.ui.onTerminalInput((data: string) => {
|
|
526
|
+
if (logoAnimating && data !== "\t" && !data.startsWith("/")) stopLogoAnimation();
|
|
527
|
+
return undefined;
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// Re-render header when model changes so the name updates live.
|
|
533
|
+
pi.on("model_select", (_event, ctx) => {
|
|
534
|
+
if (ctx.mode === "tui") requestRender?.();
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
// Re-render header/footer when the active agent mode changes. Emitted by
|
|
538
|
+
// pi-custom-agents (and any other extension) via the shared event bus.
|
|
539
|
+
pi.events.on("pi-ember-ui:mode-change", () => {
|
|
540
|
+
applyDynamicTheme();
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
pi.on("message_update", (event, ctx) => {
|
|
544
|
+
if (ctx.mode !== "tui") return;
|
|
545
|
+
const ev = event.assistantMessageEvent;
|
|
546
|
+
if (ev && (ev.type === "thinking_start" || ev.type === "thinking_delta")) {
|
|
547
|
+
if (!thinkingActive) startThinkingAnimation();
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
pi.on("message_end", () => {
|
|
552
|
+
stopThinkingAnimation();
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
pi.on("message_start", () => {
|
|
556
|
+
stopLogoAnimation();
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
560
|
+
if (ctx.mode !== "tui") return;
|
|
561
|
+
startWorkingAnimation();
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
pi.on("agent_end", () => {
|
|
565
|
+
stopThinkingAnimation();
|
|
566
|
+
stopWorkingAnimation();
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
pi.on("tool_execution_start", (_event, ctx) => {
|
|
570
|
+
if (ctx.mode !== "tui") return;
|
|
571
|
+
if (!thinkingActive) startWorkingAnimation();
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
pi.on("tool_execution_end", (_event, ctx) => {
|
|
575
|
+
if (ctx.mode !== "tui") return;
|
|
576
|
+
if (workingActive && !thinkingActive) requestRender?.();
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
pi.registerCommand("welcome", {
|
|
580
|
+
description: "Configure the startup welcome header",
|
|
581
|
+
handler: async (args: string, ctx: any) => {
|
|
582
|
+
const normalized = args.trim().toLowerCase();
|
|
583
|
+
if (normalized === "updates on") {
|
|
584
|
+
setWelcomeUpdates(true);
|
|
585
|
+
ctx.ui.notify("Welcome update notices enabled for future sessions", "info");
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (normalized === "updates off") {
|
|
589
|
+
setWelcomeUpdates(false);
|
|
590
|
+
ctx.ui.notify("Welcome update notices disabled for future sessions", "info");
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
ctx.ui.notify("Usage: /welcome updates on | /welcome updates off", "info");
|
|
594
|
+
},
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
pi.registerEntryRenderer("ember-turn-separator", (_entry, _opts, theme) => {
|
|
598
|
+
return {
|
|
599
|
+
render(width: number): string[] {
|
|
600
|
+
return [theme.fg("accent", "\u2500".repeat(Math.max(0, width)))];
|
|
601
|
+
},
|
|
602
|
+
invalidate() {},
|
|
603
|
+
} as any;
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
pi.on("turn_end", (_event, ctx) => {
|
|
607
|
+
if (ctx.mode !== "tui") return;
|
|
608
|
+
pi.appendEntry("ember-turn-separator");
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
612
|
+
sessionCtx = undefined;
|
|
613
|
+
stopLogoAnimation();
|
|
614
|
+
stopThinkingAnimation();
|
|
615
|
+
stopWorkingAnimation();
|
|
616
|
+
if (ctx.hasUI) ctx.ui.setHeader(undefined);
|
|
617
|
+
});
|
|
618
|
+
}
|