gentle-pi 0.2.4 → 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 +369 -25
- 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[][] = [];
|
|
@@ -106,10 +383,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
106
383
|
|
|
107
384
|
process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
|
|
108
385
|
|
|
109
|
-
const finishIntro = () => {
|
|
110
|
-
ctx.ui.setHeader(undefined);
|
|
111
|
-
};
|
|
112
|
-
|
|
113
386
|
const roseBase = padLines(normalizeAscii(ROSE_LARGE_RAW));
|
|
114
387
|
const logoBase = padLines(TEXT_LOGO);
|
|
115
388
|
|
|
@@ -177,12 +450,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
177
450
|
|
|
178
451
|
state.timer = setInterval(() => {
|
|
179
452
|
tick++;
|
|
180
|
-
if (tick >
|
|
453
|
+
if (tick > WRITING_END_TICK + 22) {
|
|
181
454
|
if (state.timer) {
|
|
182
455
|
clearInterval(state.timer);
|
|
183
456
|
state.timer = null;
|
|
184
457
|
}
|
|
185
|
-
finishIntro();
|
|
186
458
|
return;
|
|
187
459
|
}
|
|
188
460
|
try {
|
|
@@ -193,15 +465,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
193
465
|
state.timer = null;
|
|
194
466
|
}
|
|
195
467
|
}
|
|
196
|
-
},
|
|
468
|
+
}, 25);
|
|
197
469
|
|
|
198
470
|
return {
|
|
199
471
|
render(width: number): string[] {
|
|
200
|
-
const flashStartTick =
|
|
201
|
-
const roseOpacity = Math.min(1, tick /
|
|
472
|
+
const flashStartTick = 10;
|
|
473
|
+
const roseOpacity = Math.min(1, tick / 10);
|
|
202
474
|
const flashPhase =
|
|
203
475
|
tick >= flashStartTick
|
|
204
|
-
? Math.max(0, 1 - (tick - flashStartTick) /
|
|
476
|
+
? Math.max(0, 1 - (tick - flashStartTick) / 12)
|
|
205
477
|
: 0;
|
|
206
478
|
const frame = Math.floor(tick / 2);
|
|
207
479
|
|
|
@@ -243,16 +515,35 @@ export default function (pi: ExtensionAPI) {
|
|
|
243
515
|
b.addRow();
|
|
244
516
|
b.add("rose", roseLine);
|
|
245
517
|
b.add("none", " ");
|
|
246
|
-
|
|
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
|
+
}
|
|
247
530
|
b.center(width);
|
|
248
531
|
}
|
|
249
532
|
} else {
|
|
250
533
|
const showBanner = width >= logoBase.width + 2;
|
|
251
534
|
const showRose = width >= roseBase.width + 2;
|
|
252
535
|
if (showBanner) {
|
|
253
|
-
for (
|
|
536
|
+
for (let logoI = 0; logoI < logoBase.lines.length; logoI++) {
|
|
537
|
+
const logoLine = logoBase.lines[logoI];
|
|
254
538
|
b.addRow();
|
|
255
|
-
b.
|
|
539
|
+
b.lines[b.lines.length - 1].push(
|
|
540
|
+
...buildPenLogoLine(
|
|
541
|
+
logoLine,
|
|
542
|
+
logoI,
|
|
543
|
+
logoBase.lines.length,
|
|
544
|
+
tick,
|
|
545
|
+
),
|
|
546
|
+
);
|
|
256
547
|
b.center(width);
|
|
257
548
|
}
|
|
258
549
|
if (showRose) {
|
|
@@ -357,9 +648,46 @@ export default function (pi: ExtensionAPI) {
|
|
|
357
648
|
const out: string[] = [];
|
|
358
649
|
const layout = b.lines;
|
|
359
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
|
+
|
|
360
686
|
for (let y = 0; y < layout.length; y++) {
|
|
361
687
|
const row = layout[y] || [];
|
|
362
|
-
const
|
|
688
|
+
const firstLogoX = row.findIndex(
|
|
689
|
+
(c) => isLogoCellType(c.type) && c.char !== " ",
|
|
690
|
+
);
|
|
363
691
|
let line = "";
|
|
364
692
|
|
|
365
693
|
for (let x = 0; x < row.length; x++) {
|
|
@@ -389,20 +717,37 @@ export default function (pi: ExtensionAPI) {
|
|
|
389
717
|
continue;
|
|
390
718
|
}
|
|
391
719
|
|
|
392
|
-
if (cell.type
|
|
393
|
-
|
|
394
|
-
|
|
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`;
|
|
395
733
|
continue;
|
|
396
734
|
}
|
|
397
|
-
const localX = firstBannerX >= 0 ? x - firstBannerX : x;
|
|
398
|
-
const sweep = Math.floor((tick - 16) * 2.2);
|
|
399
|
-
const isFlashing =
|
|
400
|
-
tick >= 16 && localX >= sweep - 4 && localX <= sweep + 2;
|
|
401
735
|
|
|
402
|
-
if (
|
|
403
|
-
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);
|
|
404
747
|
} else {
|
|
405
|
-
line +=
|
|
748
|
+
line += cell.char === "▒"
|
|
749
|
+
? rgb(95, 30, 60, cell.char)
|
|
750
|
+
: rgb(255, 120, 198, cell.char);
|
|
406
751
|
}
|
|
407
752
|
continue;
|
|
408
753
|
}
|
|
@@ -435,7 +780,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
435
780
|
clearInterval(state.timer);
|
|
436
781
|
state.timer = null;
|
|
437
782
|
}
|
|
438
|
-
finishIntro();
|
|
439
783
|
},
|
|
440
784
|
};
|
|
441
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",
|