@gonrocca/zero-pi 0.1.31 → 0.1.32
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/zero-models.ts +105 -65
- package/package.json +1 -1
|
@@ -19,16 +19,25 @@ import { readFileSync, writeFileSync } from "node:fs";
|
|
|
19
19
|
import { homedir } from "node:os";
|
|
20
20
|
import { join } from "node:path";
|
|
21
21
|
|
|
22
|
-
// `@earendil-works/pi-tui`
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
22
|
+
// This module has NO `@earendil-works/pi-tui` import at all — not a value
|
|
23
|
+
// import, not an `import type`. The no-arg picker is a `ctx.ui.custom()`
|
|
24
|
+
// component, and `ctx.ui.custom`'s factory only needs an object that exposes
|
|
25
|
+
// `render(width): string[]` (plus optional `handleInput`/`invalidate`) — pi's
|
|
26
|
+
// own docs example returns exactly such a plain object literal. So the picker
|
|
27
|
+
// builds its framed layout by hand with Unicode box-drawing characters and
|
|
28
|
+
// satisfies the contract via the small local `Component` interface below. Not
|
|
29
|
+
// importing the ambient `@earendil-works/pi-tui` specifier at all means even a
|
|
30
|
+
// stray static reference can never crash `node --test` with
|
|
31
|
+
// `ERR_MODULE_NOT_FOUND` when `zero-models.test.ts` imports the deterministic
|
|
32
|
+
// helpers.
|
|
33
|
+
|
|
34
|
+
/** The shape `ctx.ui.custom()`'s factory must return — a renderable component.
|
|
35
|
+
* Declared locally so this module pulls in no ambient TUI specifier. */
|
|
36
|
+
interface Component {
|
|
37
|
+
render(width: number): string[];
|
|
38
|
+
handleInput?(data: string): void;
|
|
39
|
+
invalidate?(): void;
|
|
40
|
+
}
|
|
32
41
|
|
|
33
42
|
import { readAutotuneMode, type AutotuneMode } from "./autotune.ts";
|
|
34
43
|
import type { AutotunePending } from "./autotune-extension.ts";
|
|
@@ -206,18 +215,6 @@ export function groupByProvider(models: readonly PiModel[]): Map<string, string[
|
|
|
206
215
|
interface PiTui {
|
|
207
216
|
requestRender(): void;
|
|
208
217
|
}
|
|
209
|
-
/** A pi-TUI component that groups children vertically and renders a frame. */
|
|
210
|
-
interface PiBox extends Component {
|
|
211
|
-
addChild(child: Component): void;
|
|
212
|
-
}
|
|
213
|
-
/** The slice of `@earendil-works/pi-tui` the boxed picker constructs at
|
|
214
|
-
* runtime — `Box`/`Text`/`Spacer` constructors, captured from the lazy
|
|
215
|
-
* dynamic `import()` so this module never statically resolves the specifier. */
|
|
216
|
-
interface PiTuiModule {
|
|
217
|
-
Box: new (paddingX?: number, paddingY?: number) => PiBox;
|
|
218
|
-
Text: new (content: string, paddingX?: number, paddingY?: number) => Component;
|
|
219
|
-
Spacer: new (lines: number) => Component;
|
|
220
|
-
}
|
|
221
218
|
/** A pi theme — only the foreground-color helper the picker uses. */
|
|
222
219
|
interface PiTheme {
|
|
223
220
|
fg(color: string, text: string): string;
|
|
@@ -320,6 +317,60 @@ function clampLine(line: string, width: number): string {
|
|
|
320
317
|
return line.length > width ? line.slice(0, width) : line;
|
|
321
318
|
}
|
|
322
319
|
|
|
320
|
+
/** Unicode box-drawing characters for the picker's 4-sided frame. */
|
|
321
|
+
const BOX = {
|
|
322
|
+
topLeft: "┌",
|
|
323
|
+
topRight: "┐",
|
|
324
|
+
bottomLeft: "└",
|
|
325
|
+
bottomRight: "┘",
|
|
326
|
+
horizontal: "─",
|
|
327
|
+
vertical: "│",
|
|
328
|
+
} as const;
|
|
329
|
+
/** The theme color used for the picker's box frame. */
|
|
330
|
+
const FRAME_COLOR = "accent";
|
|
331
|
+
/** Below this `width` a real frame cannot be drawn — render unframed instead. */
|
|
332
|
+
const MIN_BOX_WIDTH = 10;
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* One content row of the boxed panel, given as *plain* text plus an optional
|
|
336
|
+
* theme color. {@link frameBox} measures the plain `text`, then sizes and
|
|
337
|
+
* colorizes it — so an ANSI escape is never fed to `clampLine`/`padEnd`.
|
|
338
|
+
*/
|
|
339
|
+
interface BoxRow {
|
|
340
|
+
text: string;
|
|
341
|
+
color?: string;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Wrap content rows in a true 4-sided Unicode box.
|
|
346
|
+
*
|
|
347
|
+
* `width` is the full outer box width. Each row's *plain* text is truncated
|
|
348
|
+
* and space-padded to the inner width (`width - 4`: two `│` columns plus one
|
|
349
|
+
* space of padding on each side) — measured *before* `theme.fg` runs, so an
|
|
350
|
+
* ANSI escape is never measured. The frame chars and the row content are
|
|
351
|
+
* themed separately. When `width` is too small to frame, the plain rows are
|
|
352
|
+
* returned unframed (defensive — never produce garbage).
|
|
353
|
+
*/
|
|
354
|
+
function frameBox(rows: readonly BoxRow[], width: number, theme: PiTheme): string[] {
|
|
355
|
+
if (width < MIN_BOX_WIDTH) {
|
|
356
|
+
return rows.map((row) => clampLine(row.text, Math.max(0, width)));
|
|
357
|
+
}
|
|
358
|
+
const inner = width - 4;
|
|
359
|
+
const frame = (s: string): string => theme.fg(FRAME_COLOR, s);
|
|
360
|
+
const horizontal = BOX.horizontal.repeat(width - 2);
|
|
361
|
+
const top = frame(`${BOX.topLeft}${horizontal}${BOX.topRight}`);
|
|
362
|
+
const bottom = frame(`${BOX.bottomLeft}${horizontal}${BOX.bottomRight}`);
|
|
363
|
+
const side = frame(BOX.vertical);
|
|
364
|
+
|
|
365
|
+
const body = rows.map((row) => {
|
|
366
|
+
// Size on plain text, then colorize — never measure an ANSI string.
|
|
367
|
+
const sized = clampLine(row.text, inner).padEnd(inner, " ");
|
|
368
|
+
const content = row.color ? theme.fg(row.color, sized) : sized;
|
|
369
|
+
return `${side} ${content} ${side}`;
|
|
370
|
+
});
|
|
371
|
+
return [top, ...body, bottom];
|
|
372
|
+
}
|
|
373
|
+
|
|
323
374
|
/**
|
|
324
375
|
* Build the inline pi-TUI component for the no-arg picker.
|
|
325
376
|
*
|
|
@@ -333,68 +384,62 @@ function clampLine(line: string, width: number): string {
|
|
|
333
384
|
* `Input`): pi-tui's `Input` is not shipped with type definitions in this pi
|
|
334
385
|
* build and an embedded-input + `Focusable` wiring is the design's named
|
|
335
386
|
* highest-risk path — the inline buffer is self-contained, needs no ambient
|
|
336
|
-
* class
|
|
337
|
-
*
|
|
387
|
+
* TUI class at all, and keeps the pure module's `submitText` contract
|
|
388
|
+
* untouched.
|
|
338
389
|
*
|
|
339
390
|
* The whole `handleInput` body is wrapped in a `try/catch` that closes the
|
|
340
391
|
* component cleanly via `done({ type: "quit" })` on any error (Req 9).
|
|
392
|
+
*
|
|
393
|
+
* `render(width)` builds the framed layout by hand with Unicode box-drawing
|
|
394
|
+
* characters ({@link frameBox}) — no pi-tui components are involved — so the
|
|
395
|
+
* picker draws as a real closed rectangle.
|
|
341
396
|
*/
|
|
342
397
|
function createPickerComponent(
|
|
343
398
|
initial: PickerState,
|
|
344
|
-
pitui: PiTuiModule,
|
|
345
399
|
theme: PiTheme,
|
|
346
400
|
tui: PiTui,
|
|
347
401
|
done: (result: EnterResult) => void,
|
|
348
402
|
): Component {
|
|
349
|
-
const { Box, Text, Spacer } = pitui;
|
|
350
403
|
// The single mutable state reference; reassigned from the pure functions.
|
|
351
404
|
let state = initial;
|
|
352
405
|
// Inline text buffer — non-null only while `state.textPrompt` is open.
|
|
353
406
|
let buffer: string | null = null;
|
|
354
407
|
|
|
355
|
-
/**
|
|
408
|
+
/**
|
|
409
|
+
* Render the boxed panel for the current state.
|
|
410
|
+
*
|
|
411
|
+
* Builds plain-text content rows with an optional theme color each, then
|
|
412
|
+
* hands them to {@link frameBox} which sizes (on plain text) and colorizes
|
|
413
|
+
* last. Defensive — it must never throw: rows carry plain text, theming is
|
|
414
|
+
* deferred to `frameBox`, and `frameBox` degrades gracefully on a tiny
|
|
415
|
+
* `width`.
|
|
416
|
+
*/
|
|
356
417
|
function render(width: number): string[] {
|
|
357
|
-
const
|
|
358
|
-
const inner = Math.max(1, width - 4); // account for the box's paddingX
|
|
418
|
+
const rows: BoxRow[] = [];
|
|
359
419
|
|
|
360
|
-
|
|
361
|
-
|
|
420
|
+
rows.push({ text: PICKER_TITLE });
|
|
421
|
+
rows.push({ text: "" });
|
|
362
422
|
|
|
363
423
|
if (state.textPrompt) {
|
|
364
424
|
// Inline text-entry mode: show the prompt label and the typed buffer.
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
);
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
new Text(theme.fg("accent", clampLine(typed, inner)), 0, 0),
|
|
371
|
-
);
|
|
372
|
-
box.addChild(new Spacer(1));
|
|
373
|
-
box.addChild(
|
|
374
|
-
new Text(
|
|
375
|
-
theme.fg("dim", clampLine("enter confirmar · esc volver", inner)),
|
|
376
|
-
0,
|
|
377
|
-
0,
|
|
378
|
-
),
|
|
379
|
-
);
|
|
380
|
-
return box.render(width);
|
|
425
|
+
rows.push({ text: state.textPrompt.label });
|
|
426
|
+
rows.push({ text: `> ${buffer ?? ""}`, color: "accent" });
|
|
427
|
+
rows.push({ text: "" });
|
|
428
|
+
rows.push({ text: "enter confirmar · esc volver", color: "dim" });
|
|
429
|
+
return frameBox(rows, width, theme);
|
|
381
430
|
}
|
|
382
431
|
|
|
383
432
|
state.entries.forEach((entry, index) => {
|
|
384
433
|
if (index === state.cursor) {
|
|
385
|
-
|
|
386
|
-
box.addChild(new Text(theme.fg("accent", row), 0, 0));
|
|
434
|
+
rows.push({ text: `> ${entry.label}`, color: "accent" });
|
|
387
435
|
} else {
|
|
388
|
-
|
|
389
|
-
box.addChild(new Text(theme.fg("dim", row), 0, 0));
|
|
436
|
+
rows.push({ text: ` ${entry.label}`, color: "dim" });
|
|
390
437
|
}
|
|
391
438
|
});
|
|
392
439
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
);
|
|
397
|
-
return box.render(width);
|
|
440
|
+
rows.push({ text: "" });
|
|
441
|
+
rows.push({ text: PICKER_HELP, color: "dim" });
|
|
442
|
+
return frameBox(rows, width, theme);
|
|
398
443
|
}
|
|
399
444
|
|
|
400
445
|
/** Apply an `EnterResult` — re-render on `state`, close on `save`/`quit`. */
|
|
@@ -560,16 +605,11 @@ export default function register(pi?: PiExtensionAPI): void {
|
|
|
560
605
|
fallbackModels: FALLBACK_MODELS,
|
|
561
606
|
});
|
|
562
607
|
|
|
563
|
-
//
|
|
564
|
-
//
|
|
565
|
-
//
|
|
566
|
-
// and never escapes (Req 9).
|
|
567
|
-
const pitui = (await import(
|
|
568
|
-
"@earendil-works/pi-tui"
|
|
569
|
-
)) as unknown as PiTuiModule;
|
|
570
|
-
|
|
608
|
+
// The picker is a self-contained `ctx.ui.custom()` component — it
|
|
609
|
+
// renders its own framed layout with box-drawing characters, so no
|
|
610
|
+
// pi-tui module is loaded here.
|
|
571
611
|
const result = await ctx.ui.custom<EnterResult>((tui, theme, _kb, done) =>
|
|
572
|
-
createPickerComponent(initialState,
|
|
612
|
+
createPickerComponent(initialState, theme, tui, done),
|
|
573
613
|
);
|
|
574
614
|
|
|
575
615
|
if (result.type !== "save") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gonrocca/zero-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.32",
|
|
4
4
|
"description": "zero-pi — an installable layer for pi (pi.dev): the zero spec-driven development workflow, per-phase model autotune, and skill auto-learning. Adds capability to pi without modifying pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|