gentle-pi 0.2.3 → 0.2.5
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/extensions/skill-registry.ts +73 -4
- package/extensions/startup-banner.ts +371 -46
- package/package.json +1 -1
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
renameSync,
|
|
8
|
+
statSync,
|
|
9
|
+
watch,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
} from "node:fs";
|
|
3
12
|
import { homedir } from "node:os";
|
|
4
13
|
import { basename, join, relative } from "node:path";
|
|
5
14
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
@@ -11,7 +20,10 @@ const EXCLUDE_NAMES = new Set(["_shared", "skill-registry"]);
|
|
|
11
20
|
const EXCLUDE_PREFIXES = ["sdd-"];
|
|
12
21
|
const ATL_IGNORE_ENTRY = ".atl/";
|
|
13
22
|
const WATCH_DEBOUNCE_MS = 500;
|
|
14
|
-
const REGISTRY_SCHEMA_VERSION =
|
|
23
|
+
const REGISTRY_SCHEMA_VERSION = 3;
|
|
24
|
+
const LEGACY_PROJECT_REGISTRY_REL_PATH = ".pi/extensions/skill-registry.ts";
|
|
25
|
+
const LEGACY_PROJECT_REGISTRY_DISABLED_REL_PATH =
|
|
26
|
+
".pi/extensions/skill-registry.ts.disabled";
|
|
15
27
|
|
|
16
28
|
interface SkillEntry {
|
|
17
29
|
name: string;
|
|
@@ -188,7 +200,7 @@ function renderRegistry(cwd: string, sources: string[], entries: SkillEntry[]):
|
|
|
188
200
|
const lines: string[] = [];
|
|
189
201
|
lines.push(`# Skill Registry — ${projectName}`);
|
|
190
202
|
lines.push("");
|
|
191
|
-
lines.push("<!-- Auto-generated by
|
|
203
|
+
lines.push("<!-- Auto-generated by gentle-pi extensions/skill-registry.ts. Run /skill-registry:refresh to regenerate. -->");
|
|
192
204
|
lines.push("");
|
|
193
205
|
lines.push(`Last updated: ${today}`);
|
|
194
206
|
lines.push("");
|
|
@@ -243,6 +255,47 @@ function ensureAtlIgnored(cwd: string): void {
|
|
|
243
255
|
writeFileSync(gitignorePath, `${existing}${prefix}${header}${ATL_IGNORE_ENTRY}\n`);
|
|
244
256
|
}
|
|
245
257
|
|
|
258
|
+
function isGeneratedLegacyProjectRegistry(source: string): boolean {
|
|
259
|
+
return (
|
|
260
|
+
source.includes("Auto-generated by .pi/extensions/skill-registry.ts") &&
|
|
261
|
+
source.includes("const REGISTRY_REL_PATH = \".atl/skill-registry.md\"") &&
|
|
262
|
+
source.includes("function projectSkillDirs(cwd: string): string[]") &&
|
|
263
|
+
source.includes("function regenerateRegistry(cwd: string, force: boolean)") &&
|
|
264
|
+
(!source.includes('join(cwd, "skills")') ||
|
|
265
|
+
source.includes("const dirs = [...userSkillDirs(), ...projectSkillDirs(cwd)]") ||
|
|
266
|
+
source.includes("if (rules.length === 0) return undefined"))
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function nextLegacyDisabledPath(cwd: string): string {
|
|
271
|
+
const base = join(cwd, LEGACY_PROJECT_REGISTRY_DISABLED_REL_PATH);
|
|
272
|
+
if (!existsSync(base)) return base;
|
|
273
|
+
for (let i = 1; i < 100; i++) {
|
|
274
|
+
const candidate = `${base}.${i}`;
|
|
275
|
+
if (!existsSync(candidate)) return candidate;
|
|
276
|
+
}
|
|
277
|
+
return `${base}.${Date.now()}`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function quarantineLegacyProjectRegistry(cwd: string): boolean {
|
|
281
|
+
const legacyPath = join(cwd, LEGACY_PROJECT_REGISTRY_REL_PATH);
|
|
282
|
+
if (!existsSync(legacyPath)) return false;
|
|
283
|
+
let source = "";
|
|
284
|
+
try {
|
|
285
|
+
source = readFileSync(legacyPath, "utf8");
|
|
286
|
+
} catch {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
if (!isGeneratedLegacyProjectRegistry(source)) return false;
|
|
290
|
+
const disabledPath = nextLegacyDisabledPath(cwd);
|
|
291
|
+
try {
|
|
292
|
+
renameSync(legacyPath, disabledPath);
|
|
293
|
+
return true;
|
|
294
|
+
} catch {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
246
299
|
function regenerateRegistry(cwd: string, force: boolean): RegenResult {
|
|
247
300
|
const existingDirs = uniqueExistingDirs([...projectSkillDirs(cwd), ...userSkillDirs()]);
|
|
248
301
|
const files = existingDirs.flatMap(findSkillFiles).sort();
|
|
@@ -308,13 +361,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
308
361
|
pi.on("session_start", async (_event, ctx) => {
|
|
309
362
|
try {
|
|
310
363
|
ensureAtlIgnored(ctx.cwd);
|
|
311
|
-
const
|
|
364
|
+
const quarantinedLegacy = quarantineLegacyProjectRegistry(ctx.cwd);
|
|
365
|
+
const result = regenerateRegistry(ctx.cwd, quarantinedLegacy);
|
|
312
366
|
if (result.regenerated && ctx.hasUI) {
|
|
313
367
|
ctx.ui.notify(`Skill registry refreshed (${result.skillCount} skills)`, "info");
|
|
314
368
|
}
|
|
369
|
+
if (quarantinedLegacy && ctx.hasUI) {
|
|
370
|
+
ctx.ui.notify(
|
|
371
|
+
"Disabled stale project-local skill registry extension; using package registry with project skills first.",
|
|
372
|
+
"warning",
|
|
373
|
+
);
|
|
374
|
+
}
|
|
315
375
|
startSkillRegistryWatcher(ctx.cwd, (message) => {
|
|
316
376
|
if (ctx.hasUI) ctx.ui.notify(message, "info");
|
|
317
377
|
});
|
|
378
|
+
if (quarantinedLegacy) {
|
|
379
|
+
setTimeout(() => {
|
|
380
|
+
try {
|
|
381
|
+
regenerateRegistry(ctx.cwd, true);
|
|
382
|
+
} catch {
|
|
383
|
+
// Best-effort same-session self-heal in case the stale extension already ran.
|
|
384
|
+
}
|
|
385
|
+
}, WATCH_DEBOUNCE_MS);
|
|
386
|
+
}
|
|
318
387
|
} catch (error) {
|
|
319
388
|
if (ctx.hasUI) {
|
|
320
389
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -63,6 +63,9 @@ function padLines(lines: string[]): { lines: string[]; width: number } {
|
|
|
63
63
|
|
|
64
64
|
type CellType =
|
|
65
65
|
| "banner"
|
|
66
|
+
| "logo-tip"
|
|
67
|
+
| "logo-fresh"
|
|
68
|
+
| "logo-ink"
|
|
66
69
|
| "rose"
|
|
67
70
|
| "label"
|
|
68
71
|
| "value"
|
|
@@ -70,6 +73,280 @@ type CellType =
|
|
|
70
73
|
| "accent"
|
|
71
74
|
| "none";
|
|
72
75
|
type LayoutCell = { char: string; type: CellType };
|
|
76
|
+
type LogoCellType = Extract<
|
|
77
|
+
CellType,
|
|
78
|
+
"banner" | "logo-tip" | "logo-fresh" | "logo-ink"
|
|
79
|
+
>;
|
|
80
|
+
|
|
81
|
+
const LOGO_CELL_TYPES: ReadonlySet<CellType> = new Set<CellType>([
|
|
82
|
+
"banner",
|
|
83
|
+
"logo-tip",
|
|
84
|
+
"logo-fresh",
|
|
85
|
+
"logo-ink",
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
function isLogoCellType(type: CellType): type is LogoCellType {
|
|
89
|
+
return LOGO_CELL_TYPES.has(type);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
type Span = { start: number; end: number };
|
|
93
|
+
|
|
94
|
+
function computeLogoBounds(lines: string[]): Span {
|
|
95
|
+
let start = Number.POSITIVE_INFINITY;
|
|
96
|
+
let end = Number.NEGATIVE_INFINITY;
|
|
97
|
+
for (const line of lines) {
|
|
98
|
+
for (let i = 0; i < line.length; i++) {
|
|
99
|
+
if (line[i] !== " ") {
|
|
100
|
+
if (i < start) start = i;
|
|
101
|
+
if (i > end) end = i;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) {
|
|
106
|
+
return { start: 0, end: 0 };
|
|
107
|
+
}
|
|
108
|
+
return { start, end };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildLetterSpans(bounds: Span, weights: number[]): Span[] {
|
|
112
|
+
const spanWidth = Math.max(1, bounds.end - bounds.start + 1);
|
|
113
|
+
const total = weights.reduce((a, b) => a + b, 0);
|
|
114
|
+
let cursor = bounds.start;
|
|
115
|
+
return weights.map((w, i) => {
|
|
116
|
+
const remaining = bounds.end - cursor + 1;
|
|
117
|
+
const raw = Math.max(1, Math.round((w / total) * spanWidth));
|
|
118
|
+
const width =
|
|
119
|
+
i === weights.length - 1
|
|
120
|
+
? remaining
|
|
121
|
+
: Math.min(raw, remaining - (weights.length - i - 1));
|
|
122
|
+
const s = cursor;
|
|
123
|
+
const e = s + width - 1;
|
|
124
|
+
cursor = e + 1;
|
|
125
|
+
return { start: s, end: e };
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const LOGO_BOUNDS = computeLogoBounds(TEXT_LOGO);
|
|
130
|
+
const LETTER_WEIGHTS = [14, 10, 11, 10, 9, 11, 6, 13, 12]; // G E N T L E - P I
|
|
131
|
+
const LETTER_SPANS = buildLetterSpans(LOGO_BOUNDS, LETTER_WEIGHTS);
|
|
132
|
+
|
|
133
|
+
function letterIndexAtX(x: number): number {
|
|
134
|
+
for (let i = 0; i < LETTER_SPANS.length; i++) {
|
|
135
|
+
const s = LETTER_SPANS[i];
|
|
136
|
+
if (x >= s.start && x <= s.end) return i;
|
|
137
|
+
}
|
|
138
|
+
if (x < LETTER_SPANS[0].start) return 0;
|
|
139
|
+
return LETTER_SPANS.length - 1;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
type Point = { x: number; y: number };
|
|
143
|
+
|
|
144
|
+
function pointKey(x: number, y: number): string {
|
|
145
|
+
return `${x}:${y}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function buildLetterStrokeMap(letterIdx: number): { orderMap: Map<string, number>; maxOrder: number } {
|
|
149
|
+
const span = LETTER_SPANS[letterIdx];
|
|
150
|
+
const points: Point[] = [];
|
|
151
|
+
const pointSet = new Set<string>();
|
|
152
|
+
|
|
153
|
+
for (let y = 0; y < TEXT_LOGO.length; y++) {
|
|
154
|
+
const line = TEXT_LOGO[y] ?? "";
|
|
155
|
+
for (let x = span.start; x <= Math.min(span.end, line.length - 1); x++) {
|
|
156
|
+
if (line[x] !== " ") {
|
|
157
|
+
points.push({ x, y });
|
|
158
|
+
pointSet.add(pointKey(x, y));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const neighbors8 = [
|
|
164
|
+
[-1, -1], [0, -1], [1, -1],
|
|
165
|
+
[-1, 0], [1, 0],
|
|
166
|
+
[-1, 1], [0, 1], [1, 1],
|
|
167
|
+
] as const;
|
|
168
|
+
|
|
169
|
+
const visited = new Set<string>();
|
|
170
|
+
const components: Point[][] = [];
|
|
171
|
+
|
|
172
|
+
for (const p of points) {
|
|
173
|
+
const k = pointKey(p.x, p.y);
|
|
174
|
+
if (visited.has(k)) continue;
|
|
175
|
+
|
|
176
|
+
const stack = [p];
|
|
177
|
+
const comp: Point[] = [];
|
|
178
|
+
visited.add(k);
|
|
179
|
+
|
|
180
|
+
while (stack.length > 0) {
|
|
181
|
+
const cur = stack.pop()!;
|
|
182
|
+
comp.push(cur);
|
|
183
|
+
for (const [dx, dy] of neighbors8) {
|
|
184
|
+
const nk = pointKey(cur.x + dx, cur.y + dy);
|
|
185
|
+
if (!visited.has(nk) && pointSet.has(nk)) {
|
|
186
|
+
visited.add(nk);
|
|
187
|
+
stack.push({ x: cur.x + dx, y: cur.y + dy });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
components.push(comp);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
components.sort((a, b) => {
|
|
196
|
+
const ax = Math.min(...a.map((p) => p.x));
|
|
197
|
+
const bx = Math.min(...b.map((p) => p.x));
|
|
198
|
+
if (ax !== bx) return ax - bx;
|
|
199
|
+
const ay = Math.min(...a.map((p) => p.y));
|
|
200
|
+
const by = Math.min(...b.map((p) => p.y));
|
|
201
|
+
return ay - by;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const orderMap = new Map<string, number>();
|
|
205
|
+
let order = 0;
|
|
206
|
+
|
|
207
|
+
for (const comp of components) {
|
|
208
|
+
const compSet = new Set(comp.map((p) => pointKey(p.x, p.y)));
|
|
209
|
+
const compMap = new Map(comp.map((p) => [pointKey(p.x, p.y), p]));
|
|
210
|
+
|
|
211
|
+
let current = comp.reduce((best, p) =>
|
|
212
|
+
p.x < best.x || (p.x === best.x && p.y < best.y) ? p : best,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
let dirX = 1;
|
|
216
|
+
let dirY = 0;
|
|
217
|
+
|
|
218
|
+
while (compSet.size > 0) {
|
|
219
|
+
const ck = pointKey(current.x, current.y);
|
|
220
|
+
if (compSet.has(ck)) {
|
|
221
|
+
compSet.delete(ck);
|
|
222
|
+
orderMap.set(ck, order++);
|
|
223
|
+
}
|
|
224
|
+
if (compSet.size === 0) break;
|
|
225
|
+
|
|
226
|
+
const candidates: Point[] = [];
|
|
227
|
+
for (let dy = -2; dy <= 2; dy++) {
|
|
228
|
+
for (let dx = -2; dx <= 2; dx++) {
|
|
229
|
+
if (dx === 0 && dy === 0) continue;
|
|
230
|
+
const nk = pointKey(current.x + dx, current.y + dy);
|
|
231
|
+
if (compSet.has(nk)) {
|
|
232
|
+
const point = compMap.get(nk);
|
|
233
|
+
if (point) candidates.push(point);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
let next: Point | null = null;
|
|
239
|
+
if (candidates.length > 0) {
|
|
240
|
+
candidates.sort((a, b) => {
|
|
241
|
+
const adx = a.x - current.x;
|
|
242
|
+
const ady = a.y - current.y;
|
|
243
|
+
const bdx = b.x - current.x;
|
|
244
|
+
const bdy = b.y - current.y;
|
|
245
|
+
|
|
246
|
+
const aDist = Math.hypot(adx, ady);
|
|
247
|
+
const bDist = Math.hypot(bdx, bdy);
|
|
248
|
+
const aTurn = Math.abs(adx * dirY - ady * dirX);
|
|
249
|
+
const bTurn = Math.abs(bdx * dirY - bdy * dirX);
|
|
250
|
+
|
|
251
|
+
const aScore = aDist * 3.8 + aTurn * 1.3 + Math.abs(ady) * 0.12;
|
|
252
|
+
const bScore = bDist * 3.8 + bTurn * 1.3 + Math.abs(bdy) * 0.12;
|
|
253
|
+
return aScore - bScore;
|
|
254
|
+
});
|
|
255
|
+
next = candidates[0];
|
|
256
|
+
} else {
|
|
257
|
+
let best: Point | null = null;
|
|
258
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
259
|
+
for (const k of compSet) {
|
|
260
|
+
const p = compMap.get(k);
|
|
261
|
+
if (!p) continue;
|
|
262
|
+
const dx = p.x - current.x;
|
|
263
|
+
const dy = p.y - current.y;
|
|
264
|
+
const score = Math.hypot(dx, dy) + Math.abs(dy) * 0.16;
|
|
265
|
+
if (score < bestScore) {
|
|
266
|
+
bestScore = score;
|
|
267
|
+
best = p;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
next = best;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!next) break;
|
|
274
|
+
dirX = next.x - current.x;
|
|
275
|
+
dirY = next.y - current.y;
|
|
276
|
+
current = next;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return { orderMap, maxOrder: Math.max(1, order - 1) };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const LETTER_STROKES = LETTER_SPANS.map((_, i) => buildLetterStrokeMap(i));
|
|
284
|
+
const WRITING_START_TICK = 6;
|
|
285
|
+
const LETTER_TICKS = LETTER_STROKES.map((s) =>
|
|
286
|
+
Math.max(5, Math.ceil(((s.maxOrder + 8) / 11) * 0.48)),
|
|
287
|
+
);
|
|
288
|
+
const LETTER_START_TICKS = LETTER_TICKS.map((_, i) =>
|
|
289
|
+
WRITING_START_TICK + LETTER_TICKS.slice(0, i).reduce((a, b) => a + b, 0),
|
|
290
|
+
);
|
|
291
|
+
const WRITING_END_TICK = WRITING_START_TICK + LETTER_TICKS.reduce((a, b) => a + b, 0);
|
|
292
|
+
|
|
293
|
+
function buildPenLogoLine(
|
|
294
|
+
line: string,
|
|
295
|
+
rowIdx: number,
|
|
296
|
+
_totalRows: number,
|
|
297
|
+
tick: number,
|
|
298
|
+
): LayoutCell[] {
|
|
299
|
+
const out: LayoutCell[] = [];
|
|
300
|
+
|
|
301
|
+
for (let x = 0; x < line.length; x++) {
|
|
302
|
+
const ch = line[x] ?? " ";
|
|
303
|
+
if (ch === " ") {
|
|
304
|
+
out.push({ char: " ", type: "none" });
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const letterIdx = letterIndexAtX(x);
|
|
309
|
+
const stroke = LETTER_STROKES[letterIdx];
|
|
310
|
+
const startTick = LETTER_START_TICKS[letterIdx];
|
|
311
|
+
const duration = LETTER_TICKS[letterIdx];
|
|
312
|
+
const progress = (tick - startTick) / Math.max(1, duration);
|
|
313
|
+
|
|
314
|
+
if (progress < 0) {
|
|
315
|
+
out.push({ char: " ", type: "none" });
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const head = progress * (stroke.maxOrder + 7);
|
|
320
|
+
const rawOrder = stroke.orderMap.get(pointKey(x, rowIdx));
|
|
321
|
+
if (rawOrder === undefined) {
|
|
322
|
+
out.push({ char: " ", type: "none" });
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
let order = rawOrder;
|
|
327
|
+
// v1: ajuste SOLO para la primera letra (G), con más curvatura caligráfica.
|
|
328
|
+
if (letterIdx === 0) {
|
|
329
|
+
const s = LETTER_SPANS[0];
|
|
330
|
+
const w = Math.max(1, s.end - s.start + 1);
|
|
331
|
+
const localX = x - s.start;
|
|
332
|
+
const curveBias =
|
|
333
|
+
Math.sin((localX / w) * Math.PI * 1.35 + rowIdx * 0.26) * 2.2 +
|
|
334
|
+
Math.cos((localX / w) * Math.PI * 0.72 - rowIdx * 0.20) * 1.3;
|
|
335
|
+
order = rawOrder + curveBias;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (head < order) {
|
|
339
|
+
out.push({ char: " ", type: "none" });
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const age = head - order;
|
|
344
|
+
if (age < 1.2) out.push({ char: ch, type: "logo-tip" });
|
|
345
|
+
else if (age < 4.9) out.push({ char: ch, type: "logo-fresh" });
|
|
346
|
+
else out.push({ char: ch, type: "logo-ink" });
|
|
347
|
+
}
|
|
348
|
+
return out;
|
|
349
|
+
}
|
|
73
350
|
|
|
74
351
|
class LayoutBuilder {
|
|
75
352
|
lines: LayoutCell[][] = [];
|
|
@@ -86,7 +363,7 @@ class LayoutBuilder {
|
|
|
86
363
|
center(width: number) {
|
|
87
364
|
const row = this.lines[this.lines.length - 1];
|
|
88
365
|
const pad = Math.max(0, Math.floor((width - row.length) / 2));
|
|
89
|
-
const prefix = Array.from({ length: pad }, () => ({
|
|
366
|
+
const prefix: LayoutCell[] = Array.from({ length: pad }, () => ({
|
|
90
367
|
char: " ",
|
|
91
368
|
type: "none" as const,
|
|
92
369
|
}));
|
|
@@ -106,29 +383,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
106
383
|
|
|
107
384
|
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
108
385
|
|
|
109
|
-
let closeIntro: (() => void) | null = null;
|
|
110
|
-
const closeIntroSafely = () => {
|
|
111
|
-
if (!closeIntro) return;
|
|
112
|
-
const fn = closeIntro;
|
|
113
|
-
closeIntro = null;
|
|
114
|
-
try {
|
|
115
|
-
fn();
|
|
116
|
-
} catch {}
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
void ctx.ui
|
|
120
|
-
.custom((_tui, _theme, _keybindings, done) => {
|
|
121
|
-
closeIntro = () => done(undefined);
|
|
122
|
-
return {
|
|
123
|
-
render: () => [""],
|
|
124
|
-
invalidate: () => {},
|
|
125
|
-
handleInput: () => {},
|
|
126
|
-
};
|
|
127
|
-
})
|
|
128
|
-
.catch(() => {
|
|
129
|
-
closeIntro = null;
|
|
130
|
-
});
|
|
131
|
-
|
|
132
386
|
const roseBase = padLines(normalizeAscii(ROSE_LARGE_RAW));
|
|
133
387
|
const logoBase = padLines(TEXT_LOGO);
|
|
134
388
|
|
|
@@ -196,12 +450,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
196
450
|
|
|
197
451
|
state.timer = setInterval(() => {
|
|
198
452
|
tick++;
|
|
199
|
-
if (tick >
|
|
453
|
+
if (tick > WRITING_END_TICK + 22) {
|
|
200
454
|
if (state.timer) {
|
|
201
455
|
clearInterval(state.timer);
|
|
202
456
|
state.timer = null;
|
|
203
457
|
}
|
|
204
|
-
closeIntroSafely();
|
|
205
458
|
return;
|
|
206
459
|
}
|
|
207
460
|
try {
|
|
@@ -212,15 +465,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
212
465
|
state.timer = null;
|
|
213
466
|
}
|
|
214
467
|
}
|
|
215
|
-
},
|
|
468
|
+
}, 25);
|
|
216
469
|
|
|
217
470
|
return {
|
|
218
471
|
render(width: number): string[] {
|
|
219
|
-
const flashStartTick =
|
|
220
|
-
const roseOpacity = Math.min(1, tick /
|
|
472
|
+
const flashStartTick = 10;
|
|
473
|
+
const roseOpacity = Math.min(1, tick / 10);
|
|
221
474
|
const flashPhase =
|
|
222
475
|
tick >= flashStartTick
|
|
223
|
-
? Math.max(0, 1 - (tick - flashStartTick) /
|
|
476
|
+
? Math.max(0, 1 - (tick - flashStartTick) / 12)
|
|
224
477
|
: 0;
|
|
225
478
|
const frame = Math.floor(tick / 2);
|
|
226
479
|
|
|
@@ -262,16 +515,35 @@ export default function (pi: ExtensionAPI) {
|
|
|
262
515
|
b.addRow();
|
|
263
516
|
b.add("rose", roseLine);
|
|
264
517
|
b.add("none", " ");
|
|
265
|
-
|
|
518
|
+
if (logoI >= 0 && logoI < logoBase.lines.length) {
|
|
519
|
+
b.lines[b.lines.length - 1].push(
|
|
520
|
+
...buildPenLogoLine(
|
|
521
|
+
logoLine,
|
|
522
|
+
logoI,
|
|
523
|
+
logoBase.lines.length,
|
|
524
|
+
tick,
|
|
525
|
+
),
|
|
526
|
+
);
|
|
527
|
+
} else {
|
|
528
|
+
b.add("none", " ".repeat(logoBase.width));
|
|
529
|
+
}
|
|
266
530
|
b.center(width);
|
|
267
531
|
}
|
|
268
532
|
} else {
|
|
269
533
|
const showBanner = width >= logoBase.width + 2;
|
|
270
534
|
const showRose = width >= roseBase.width + 2;
|
|
271
535
|
if (showBanner) {
|
|
272
|
-
for (
|
|
536
|
+
for (let logoI = 0; logoI < logoBase.lines.length; logoI++) {
|
|
537
|
+
const logoLine = logoBase.lines[logoI];
|
|
273
538
|
b.addRow();
|
|
274
|
-
b.
|
|
539
|
+
b.lines[b.lines.length - 1].push(
|
|
540
|
+
...buildPenLogoLine(
|
|
541
|
+
logoLine,
|
|
542
|
+
logoI,
|
|
543
|
+
logoBase.lines.length,
|
|
544
|
+
tick,
|
|
545
|
+
),
|
|
546
|
+
);
|
|
275
547
|
b.center(width);
|
|
276
548
|
}
|
|
277
549
|
if (showRose) {
|
|
@@ -376,9 +648,46 @@ export default function (pi: ExtensionAPI) {
|
|
|
376
648
|
const out: string[] = [];
|
|
377
649
|
const layout = b.lines;
|
|
378
650
|
|
|
651
|
+
const logoRows = layout
|
|
652
|
+
.map((row, idx) => ({
|
|
653
|
+
idx,
|
|
654
|
+
hasLogo: (row || []).some((c) => isLogoCellType(c.type)),
|
|
655
|
+
}))
|
|
656
|
+
.filter((r) => r.hasLogo)
|
|
657
|
+
.map((r) => r.idx);
|
|
658
|
+
const sparkleY =
|
|
659
|
+
logoRows.length > 0
|
|
660
|
+
? logoRows[Math.floor(logoRows.length / 2)]
|
|
661
|
+
: -1;
|
|
662
|
+
const logoLastX = Math.max(
|
|
663
|
+
-1,
|
|
664
|
+
...layout.map((row) => {
|
|
665
|
+
let last = -1;
|
|
666
|
+
for (let i = 0; i < (row || []).length; i++) {
|
|
667
|
+
const cell = row?.[i];
|
|
668
|
+
if (cell && isLogoCellType(cell.type) && cell.char !== " ") {
|
|
669
|
+
last = i;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return last;
|
|
673
|
+
}),
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
const glintStartTick = WRITING_END_TICK + 3;
|
|
677
|
+
const glintEndTick = WRITING_END_TICK + 12;
|
|
678
|
+
const glintActive = tick >= glintStartTick && tick <= glintEndTick;
|
|
679
|
+
const glintHead =
|
|
680
|
+
((tick - glintStartTick) /
|
|
681
|
+
Math.max(1, glintEndTick - glintStartTick)) *
|
|
682
|
+
(LOGO_BOUNDS.end - LOGO_BOUNDS.start + 1);
|
|
683
|
+
const sparkleActive =
|
|
684
|
+
tick >= WRITING_END_TICK + 13 && tick <= WRITING_END_TICK + 20;
|
|
685
|
+
|
|
379
686
|
for (let y = 0; y < layout.length; y++) {
|
|
380
687
|
const row = layout[y] || [];
|
|
381
|
-
const
|
|
688
|
+
const firstLogoX = row.findIndex(
|
|
689
|
+
(c) => isLogoCellType(c.type) && c.char !== " ",
|
|
690
|
+
);
|
|
382
691
|
let line = "";
|
|
383
692
|
|
|
384
693
|
for (let x = 0; x < row.length; x++) {
|
|
@@ -391,7 +700,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
391
700
|
if (cell.type === "rose") {
|
|
392
701
|
const pulse = 0.9 + Math.sin((x + y + frame) * 0.08) * 0.1;
|
|
393
702
|
const k = Math.max(0.01, roseOpacity * pulse);
|
|
394
|
-
const f =
|
|
703
|
+
const f = flashPhase ** 0.4;
|
|
395
704
|
|
|
396
705
|
const rBase = Math.floor(255 * k);
|
|
397
706
|
const gBase = Math.floor(118 * k);
|
|
@@ -408,20 +717,37 @@ export default function (pi: ExtensionAPI) {
|
|
|
408
717
|
continue;
|
|
409
718
|
}
|
|
410
719
|
|
|
411
|
-
if (cell.type
|
|
412
|
-
|
|
413
|
-
|
|
720
|
+
if (isLogoCellType(cell.type)) {
|
|
721
|
+
const localLogoX = firstLogoX >= 0 ? x - firstLogoX : x;
|
|
722
|
+
const glintOnCell =
|
|
723
|
+
glintActive &&
|
|
724
|
+
localLogoX >= glintHead - 2 &&
|
|
725
|
+
localLogoX <= glintHead + 1;
|
|
726
|
+
const sparkleOnCell =
|
|
727
|
+
sparkleActive &&
|
|
728
|
+
y === sparkleY &&
|
|
729
|
+
(x === logoLastX || x === logoLastX - 1);
|
|
730
|
+
|
|
731
|
+
if (sparkleOnCell) {
|
|
732
|
+
line += `\x1b[1m` + rgb(255, 255, 255, "✦") + `\x1b[22m`;
|
|
414
733
|
continue;
|
|
415
734
|
}
|
|
416
|
-
const localX = firstBannerX >= 0 ? x - firstBannerX : x;
|
|
417
|
-
const sweep = Math.floor((tick - 16) * 2.2);
|
|
418
|
-
const isFlashing =
|
|
419
|
-
tick >= 16 && localX >= sweep - 4 && localX <= sweep + 2;
|
|
420
735
|
|
|
421
|
-
if (
|
|
422
|
-
line += `\x1b[1m
|
|
736
|
+
if (glintOnCell) {
|
|
737
|
+
line += `\x1b[1m` + rgb(255, 245, 252, cell.char) + `\x1b[22m`;
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (cell.type === "logo-tip") {
|
|
742
|
+
line += `\x1b[1m` + rgb(255, 205, 238, cell.char) + `\x1b[22m`;
|
|
743
|
+
} else if (cell.type === "logo-fresh") {
|
|
744
|
+
line += cell.char === "▒"
|
|
745
|
+
? rgb(110, 36, 70, cell.char)
|
|
746
|
+
: rgb(255, 138, 206, cell.char);
|
|
423
747
|
} else {
|
|
424
|
-
line +=
|
|
748
|
+
line += cell.char === "▒"
|
|
749
|
+
? rgb(95, 30, 60, cell.char)
|
|
750
|
+
: rgb(255, 120, 198, cell.char);
|
|
425
751
|
}
|
|
426
752
|
continue;
|
|
427
753
|
}
|
|
@@ -454,7 +780,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
454
780
|
clearInterval(state.timer);
|
|
455
781
|
state.timer = null;
|
|
456
782
|
}
|
|
457
|
-
closeIntroSafely();
|
|
458
783
|
},
|
|
459
784
|
};
|
|
460
785
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Turn Pi into el Gentleman: a senior-architect development harness with SDD/OpenSpec, subagents, strict TDD evidence, review guardrails, and skill discovery.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|