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.
@@ -1,5 +1,14 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, watch, writeFileSync } from "node:fs";
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 = 2;
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 .pi/extensions/skill-registry.ts. Run /skill-registry:refresh to regenerate. -->");
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 result = regenerateRegistry(ctx.cwd, false);
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 > 90) {
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
- }, 50);
468
+ }, 25);
197
469
 
198
470
  return {
199
471
  render(width: number): string[] {
200
- const flashStartTick = 16;
201
- const roseOpacity = Math.min(1, tick / 16);
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) / 20)
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
- b.add("banner", logoLine);
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 (const logoLine of logoBase.lines) {
536
+ for (let logoI = 0; logoI < logoBase.lines.length; logoI++) {
537
+ const logoLine = logoBase.lines[logoI];
254
538
  b.addRow();
255
- b.add("banner", logoLine);
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 firstBannerX = row.findIndex((c) => c.type === "banner");
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 === "banner") {
393
- if (cell.char === "▒") {
394
- line += rgb(95, 30, 60, cell.char);
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 (isFlashing) {
403
- line += `\x1b[1m\x1b[38;2;255;255;255m${cell.char}\x1b[0m`;
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 += rgb(255, 120, 198, cell.char);
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.4",
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",