@turri-dev/cli 0.1.7-next.eb57c34 → 0.1.7-next.f0d55df

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.
Files changed (94) hide show
  1. package/README.md +1 -3
  2. package/assets/jellyfish/dark-00.png +0 -0
  3. package/assets/jellyfish/dark-01.png +0 -0
  4. package/assets/jellyfish/dark-02.png +0 -0
  5. package/assets/jellyfish/dark-03.png +0 -0
  6. package/assets/jellyfish/dark-04.png +0 -0
  7. package/assets/jellyfish/dark-05.png +0 -0
  8. package/assets/jellyfish/dark-06.png +0 -0
  9. package/assets/jellyfish/dark-07.png +0 -0
  10. package/assets/jellyfish/dark-08.png +0 -0
  11. package/assets/jellyfish/dark-09.png +0 -0
  12. package/assets/jellyfish/dark-10.png +0 -0
  13. package/assets/jellyfish/dark-11.png +0 -0
  14. package/assets/jellyfish/dark-12.png +0 -0
  15. package/assets/jellyfish/dark-13.png +0 -0
  16. package/assets/jellyfish/dark-14.png +0 -0
  17. package/assets/jellyfish/dark-15.png +0 -0
  18. package/assets/jellyfish/dark-16.png +0 -0
  19. package/assets/jellyfish/dark-17.png +0 -0
  20. package/assets/jellyfish/dark-18.png +0 -0
  21. package/assets/jellyfish/dark-19.png +0 -0
  22. package/assets/jellyfish/dark-20.png +0 -0
  23. package/assets/jellyfish/dark-21.png +0 -0
  24. package/assets/jellyfish/dark-22.png +0 -0
  25. package/assets/jellyfish/dark-23.png +0 -0
  26. package/assets/jellyfish/dark-24.png +0 -0
  27. package/assets/jellyfish/dark-25.png +0 -0
  28. package/assets/jellyfish/dark-26.png +0 -0
  29. package/assets/jellyfish/dark-27.png +0 -0
  30. package/assets/jellyfish/dark-28.png +0 -0
  31. package/assets/jellyfish/dark-29.png +0 -0
  32. package/assets/jellyfish/dark-30.png +0 -0
  33. package/assets/jellyfish/dark-31.png +0 -0
  34. package/assets/jellyfish/dark-32.png +0 -0
  35. package/assets/jellyfish/dark-33.png +0 -0
  36. package/assets/jellyfish/dark-34.png +0 -0
  37. package/assets/jellyfish/dark-35.png +0 -0
  38. package/assets/jellyfish/dark-36.png +0 -0
  39. package/assets/jellyfish/dark-37.png +0 -0
  40. package/assets/jellyfish/dark-38.png +0 -0
  41. package/assets/jellyfish/dark-39.png +0 -0
  42. package/assets/jellyfish/dark-40.png +0 -0
  43. package/assets/jellyfish/light-00.png +0 -0
  44. package/assets/jellyfish/light-01.png +0 -0
  45. package/assets/jellyfish/light-02.png +0 -0
  46. package/assets/jellyfish/light-03.png +0 -0
  47. package/assets/jellyfish/light-04.png +0 -0
  48. package/assets/jellyfish/light-05.png +0 -0
  49. package/assets/jellyfish/light-06.png +0 -0
  50. package/assets/jellyfish/light-07.png +0 -0
  51. package/assets/jellyfish/light-08.png +0 -0
  52. package/assets/jellyfish/light-09.png +0 -0
  53. package/assets/jellyfish/light-10.png +0 -0
  54. package/assets/jellyfish/light-11.png +0 -0
  55. package/assets/jellyfish/light-12.png +0 -0
  56. package/assets/jellyfish/light-13.png +0 -0
  57. package/assets/jellyfish/light-14.png +0 -0
  58. package/assets/jellyfish/light-15.png +0 -0
  59. package/assets/jellyfish/light-16.png +0 -0
  60. package/assets/jellyfish/light-17.png +0 -0
  61. package/assets/jellyfish/light-18.png +0 -0
  62. package/assets/jellyfish/light-19.png +0 -0
  63. package/assets/jellyfish/light-20.png +0 -0
  64. package/assets/jellyfish/light-21.png +0 -0
  65. package/assets/jellyfish/light-22.png +0 -0
  66. package/assets/jellyfish/light-23.png +0 -0
  67. package/assets/jellyfish/light-24.png +0 -0
  68. package/assets/jellyfish/light-25.png +0 -0
  69. package/assets/jellyfish/light-26.png +0 -0
  70. package/assets/jellyfish/light-27.png +0 -0
  71. package/assets/jellyfish/light-28.png +0 -0
  72. package/assets/jellyfish/light-29.png +0 -0
  73. package/assets/jellyfish/light-30.png +0 -0
  74. package/assets/jellyfish/light-31.png +0 -0
  75. package/assets/jellyfish/light-32.png +0 -0
  76. package/assets/jellyfish/light-33.png +0 -0
  77. package/assets/jellyfish/light-34.png +0 -0
  78. package/assets/jellyfish/light-35.png +0 -0
  79. package/assets/jellyfish/light-36.png +0 -0
  80. package/assets/jellyfish/light-37.png +0 -0
  81. package/assets/jellyfish/light-38.png +0 -0
  82. package/assets/jellyfish/light-39.png +0 -0
  83. package/assets/jellyfish/light-40.png +0 -0
  84. package/dist/extension.js +13 -8
  85. package/dist/jellyfish.d.ts +23 -13
  86. package/dist/jellyfish.js +230 -97
  87. package/dist/regenerate/loop.d.ts +2 -0
  88. package/dist/regenerate/loop.js +3 -2
  89. package/docs/images/turri-banner.gif +0 -0
  90. package/docs/theming.md +6 -4
  91. package/docs/using.md +1 -1
  92. package/package.json +1 -1
  93. package/docs/images/turri-banner.png +0 -0
  94. package/docs/images/turri-screenshot.png +0 -0
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- <p align="center"><img src="docs/images/turri-banner.png" alt="turri - the first coding agent with the power of regenerative software, beside a pixel jellyfish: npm install -g @turri-dev/cli"></p>
1
+ <p align="center"><img src="docs/images/turri-banner.gif" alt="turri, beside its mark - the first coding agent with the power of regenerative software: npm install -g @turri-dev/cli - and a pixel jellyfish regenerating, from a polyp on the sea floor"></p>
2
2
 
3
3
  <h1 align="center">turri<br>the first coding agent with the power of<br>regenerative software<br>&nbsp;</h1>
4
4
 
@@ -101,8 +101,6 @@ With **turri** you work on the durable intent. You say what the software must do
101
101
 
102
102
  ## How it works
103
103
 
104
- <p align="center"><img src="docs/images/turri-screenshot.png" width="900" alt="turri in the terminal: on the left the conversation, where /regenerate plans a project's modules and a coding agent works through one of them; on the right the spec map of that module's ***plain, each functionality marked as regenerated or being worked on"></p>
105
-
106
104
  **turri** captures the durable intent in [\*\*\*plain](https://plainlang.org/), a language for saying what software should do. \*\*\*plain breaks the intent down into functionalities - small units, each a single thing the software does - and **turri** regenerates the code from them piece by piece.
107
105
 
108
106
  1. **Define.** You say what the software should do; **turri** writes it down as \*\*\*plain.
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/dist/extension.js CHANGED
@@ -30,7 +30,7 @@ import { checkPlainFile, checkPlainTargets, formatCheckResults } from "./plain/c
30
30
  import { whyFailed } from "./regenerate/diagnose-job.js";
31
31
  import { createWorkLoop, describeModules, isUpToDate, previewJobs } from "./regenerate/loop.js";
32
32
  import { brandNeon, brandPink, plainPaint } from "./regenerate/plain-palette.js";
33
- import { halfBlocks, JELLYFISH_FRAME_MS, JELLYFISH_TEXT_FRAMES } from "./jellyfish.js";
33
+ import { halfBlocks, JELLYFISH_FRAME_MS, JELLYFISH_SEQUENCE, JELLYFISH_TEXT_FRAMES } from "./jellyfish.js";
34
34
  import { createJobPanel, didNotPass, keyPressed, PANEL_KEY, spinner } from "./regenerate/job-panel.js";
35
35
  import { clickToFocus, dockedWidth, narrowChat, shade, shadeChat } from "./regenerate/panel-dock.js";
36
36
  import { overPictureRow, PICTURE_AT } from "./picture-row.js";
@@ -2353,8 +2353,8 @@ function registerBranding(pi, skills) {
2353
2353
  ...(data.pending > 0 ? [theme.fg("warning", `${data.pending} skill${data.pending === 1 ? "" : "s"} failed to load`)] : []),
2354
2354
  ...(data.sandbox !== undefined && data.sandbox.level !== "ok" ? [theme.fg(data.sandbox.level === "limited" ? "warning" : "error", data.sandbox.text)] : []),
2355
2355
  ];
2356
- // The jellyfish turri is named after, in ink for the theme's background: a picture where the
2357
- // terminal shows images, drawn in text where it does not.
2356
+ // The jellyfish turri is named after, regenerating, in ink for the theme's background: a picture
2357
+ // where the terminal shows images, drawn in text where it does not.
2358
2358
  return new Banner(lines, (rule) => theme.fg("borderMuted", rule), jellyfishArt(theme.appearance, (theme.getColorMode?.() ?? "truecolor") === "truecolor"));
2359
2359
  });
2360
2360
  pi.on("session_start", (_event, ctx) => {
@@ -2377,10 +2377,15 @@ function registerBranding(pi, skills) {
2377
2377
  }
2378
2378
  });
2379
2379
  }
2380
- /** How many rows the banner's jellyfish takes at most: as tall as the one drawn in text. */
2381
- const JELLYFISH_ROWS = 10;
2382
- /** Which frame of the jellyfish every banner shows, moved on by `animateJellyfish`. */
2380
+ /**
2381
+ * How many rows the banner's picture takes at most: its frame reaches down to the sea floor the polyp stands on, so it
2382
+ * is given more than the jellyfish drawn in text, ten.
2383
+ */
2384
+ const JELLYFISH_ROWS = 14;
2385
+ /** How far along its sequence the jellyfish every banner shows is, moved on by `animateJellyfish`. */
2383
2386
  let jellyfishFrame = 0;
2387
+ /** The frame the jellyfish is on: the one its sequence has at this point. */
2388
+ const shownFrame = () => JELLYFISH_SEQUENCE[jellyfishFrame % JELLYFISH_SEQUENCE.length];
2384
2389
  let jellyfishTimer;
2385
2390
  /**
2386
2391
  * Moves the jellyfish on a frame every JELLYFISH_FRAME_MS while the banner is on the screen. An entry's
@@ -2481,7 +2486,7 @@ class Banner {
2481
2486
  const text = this.lines.flatMap((line) => wrapTextWithAnsi(line, Math.max(1, width - 1 - left)));
2482
2487
  const height = Math.max(text.length, placed?.rows ?? 0);
2483
2488
  const above = Math.floor((height - text.length) / 2);
2484
- const drawn = placed?.kind === "text" ? placed.frames[jellyfishFrame % placed.frames.length] : undefined;
2489
+ const drawn = placed?.kind === "text" ? placed.frames[shownFrame()] : undefined;
2485
2490
  const rows = Array.from({ length: height }, (_, row) => {
2486
2491
  const line = text[row - above];
2487
2492
  const jellyfish = drawn !== undefined ? (drawn[row] ?? " ".repeat(placed.columns)) : " ".repeat(placed?.columns ?? 0);
@@ -2502,7 +2507,7 @@ class Banner {
2502
2507
  const kitty = getCapabilities().images === "kitty";
2503
2508
  if (kitty && this.kittyImageId === undefined)
2504
2509
  this.kittyImageId = allocateImageId();
2505
- const result = renderImage(art.frames[jellyfishFrame % art.frames.length], art.dimensions, {
2510
+ const result = renderImage(art.frames[shownFrame()], art.dimensions, {
2506
2511
  maxWidthCells: room,
2507
2512
  maxHeightCells: rows,
2508
2513
  imageId: this.kittyImageId,
@@ -1,22 +1,32 @@
1
1
  /**
2
- * The jellyfish turri is named after, in pixels: a pink bell, an orange gut, neon tentacles coming apart into code at
3
- * their tips - the design system's colours, swimming a stroke a loop. Drawn at two sizes from one design: the banner's
4
- * pictures are rendered from the large one (scripts/jellyfish-frames.mjs writes assets/jellyfish/), and where a terminal
5
- * shows no images the small one is drawn here in half blocks, two pixels a cell - drawn at its size, not shrunk, so no
6
- * pixel of it is lost.
2
+ * The jellyfish turri is named after, in pixels, regenerating: a polyp on the sea floor grows into the jellyfish, it
3
+ * swims a while, then sinks back to the floor and the polyp grows again - the design system's hero and its lifecycle
4
+ * (its pixel kit's `sprite("hero")` and `cycleFrames()`, ported here pixel for pixel). The banner's pictures are
5
+ * rendered from these frames (scripts/jellyfish-frames.mjs writes assets/jellyfish/); where a terminal shows no
6
+ * images the same frames are drawn here in half blocks, at a third of the size.
7
7
  */
8
- /** What a pixel is: the bell, its glint and shine, the gut, the underside, a tentacle, an oral arm, a tip fading. */
9
- type Pixel = "p" | "h" | "w" | "o" | "e" | "N" | "n" | "l" | "g";
10
- /** The pixels' colours on a dark background and a light one - the tentacles darker on light, where neon does not show. */
8
+ /**
9
+ * What a pixel is: the bell's pink, its midtone and shadow, its rim, its highlight, the gut, and a tentacle - whole,
10
+ * half seen, or a polyp's crown, a little more than half.
11
+ */
12
+ type Pixel = "p" | "m" | "i" | "e" | "h" | "o" | "N" | "n" | "c";
13
+ /** The pixels' colours on a dark background and a light one - the tentacles blue whale on light, where neon does not show. */
11
14
  export declare const JELLYFISH_COLOURS: Record<"dark" | "light", Record<Pixel, string>>;
15
+ /** How much of a pixel shows, for a picture's alpha. */
16
+ export declare function jellyfishAlpha(pixel: string): number;
17
+ /** The jellyfish's frames, each as rows of pixels: what the pictures are rendered from. */
18
+ export declare const JELLYFISH_FRAMES: string[][];
12
19
  export declare const JELLYFISH_WIDTH: number;
13
20
  export declare const JELLYFISH_HEIGHT: number;
14
- /** The large jellyfish's frames, a stroke: what the pictures are rendered from. */
15
- export declare const JELLYFISH_FRAMES: string[][];
16
- /** The small jellyfish's frames, a stroke: what a terminal without images draws, in half blocks. */
17
- export declare const JELLYFISH_TEXT_FRAMES: string[][];
21
+ /**
22
+ * The order the frames play in, round and round: the polyp a moment, growing into the jellyfish, swimming four
23
+ * seconds - a stroke every five frames - then sinking, and the polyp growing back.
24
+ */
25
+ export declare const JELLYFISH_SEQUENCE: number[];
18
26
  /** How long each frame shows. */
19
- export declare const JELLYFISH_FRAME_MS = 250;
27
+ export declare const JELLYFISH_FRAME_MS = 150;
28
+ /** The frames as a terminal without images draws them, in half blocks: the same order, a third of the size. */
29
+ export declare const JELLYFISH_TEXT_FRAMES: string[][];
20
30
  /**
21
31
  * A frame drawn in half blocks for a terminal that shows no images: two pixels a cell, the upper one the text's colour
22
32
  * and the lower one the background's, in as many colours as the terminal has: a column a pixel, a row every two.
package/dist/jellyfish.js CHANGED
@@ -1,114 +1,247 @@
1
1
  import { sgr } from "./regenerate/plain-palette.js";
2
- /** The pixels' colours on a dark background and a light one - the tentacles darker on light, where neon does not show. */
2
+ /** How much of a pixel shows. */
3
+ const ALPHA = { p: 1, m: 1, i: 1, e: 1, h: 1, o: 1, N: 1, n: 0.5, c: 0.6 };
4
+ /** The pixels' colours on a dark background and a light one - the tentacles blue whale on light, where neon does not show. */
3
5
  export const JELLYFISH_COLOURS = {
4
- dark: { p: "#ff5c8d", h: "#ff7fa6", w: "#f4f7f9", o: "#ff7a3d", e: "#732553", N: "#8eebee", n: "#5ce1e6", l: "#85a3b2", g: "#3e5a6b" },
5
- light: { p: "#ff5c8d", h: "#ff7fa6", w: "#f4f7f9", o: "#ff7a3d", e: "#732553", N: "#3e5a6b", n: "#1e3442", l: "#85a3b2", g: "#85a3b2" },
6
+ dark: { p: "#ff5c8d", m: "#a9479a", i: "#5b57d9", e: "#732553", h: "#ff7fa6", o: "#ff7a3d", N: "#5ce1e6", n: "#5ce1e6", c: "#5ce1e6" },
7
+ light: { p: "#ff5c8d", m: "#a9479a", i: "#5b57d9", e: "#732553", h: "#ff7fa6", o: "#ff7a3d", N: "#1e3442", n: "#1e3442", c: "#1e3442" },
6
8
  };
7
- /**
8
- * How shut the bell is, frame by frame - a quarter second each: shut quickly, held a moment, opened slowly, at rest
9
- * until the next stroke.
10
- */
11
- const SHUT = [0.1, 0.55, 0.9, 1, 0.85, 0.7, 0.56, 0.44, 0.33, 0.23, 0.14, 0.06];
12
- const LARGE = {
13
- width: 24,
14
- height: 26,
15
- open: [6, 12, 16, 18, 20, 22, 22, 24, 24],
16
- shut: [6, 10, 14, 16, 18, 18, 18, 20, 20],
17
- glint: [[-3, 2], [-4, 2], [-5, 3], [-6, 4], [-6, 5]],
18
- shine: [-2, 2],
19
- gut: [".oo..oo.", "oooooooo", "oooooooo", ".oo..oo."],
20
- gutRow: 4,
21
- strands: [
22
- { at: -8, length: 10, sway: 1.1, ink: "N" },
23
- { at: -4.5, length: 13, sway: 1.3, ink: "N" },
24
- { at: -1, length: 14, sway: 0.8, ink: "n" },
25
- { at: 2, length: 13, sway: 0.8, ink: "n" },
26
- { at: 5.5, length: 13, sway: 1.3, ink: "N" },
27
- { at: 9, length: 10, sway: 1.1, ink: "N" },
28
- ],
29
- };
30
- const SMALL = {
31
- width: 18,
32
- height: 20,
33
- open: [4, 10, 12, 14, 16, 18, 18],
34
- shut: [4, 8, 10, 12, 14, 14, 14],
35
- glint: [[-2, 1], [-3, 2], [-4, 3]],
36
- shine: [-1, 1],
37
- gut: ["oo..oo", "oooooo", "oo..oo"],
38
- gutRow: 3,
39
- strands: [
40
- { at: -6, length: 8, sway: 0.9, ink: "N" },
41
- { at: -3, length: 10, sway: 1, ink: "N" },
42
- { at: -0.5, length: 11, sway: 0.7, ink: "n" },
43
- { at: 1.5, length: 10, sway: 0.7, ink: "n" },
44
- { at: 4, length: 10, sway: 1, ink: "N" },
45
- { at: 7, length: 8, sway: 0.9, ink: "N" },
46
- ],
9
+ /** What a pixel short of whole is laid over, to draw it in half blocks: abyss, or white. */
10
+ const BACKGROUND = { dark: "#0a0f1a", light: "#f4f7f9" };
11
+ /** How much of a pixel shows, for a picture's alpha. */
12
+ export function jellyfishAlpha(pixel) {
13
+ return ALPHA[pixel] ?? 0;
14
+ }
15
+ /** The lifecycle's grid, and the sea floor the polyp stands on. */
16
+ const WIDTH = 40;
17
+ const HEIGHT = 60;
18
+ const FLOOR = 59;
19
+ const put = (grid, x, y, pixel, bounded = true) => {
20
+ x = Math.round(x);
21
+ y = Math.round(y);
22
+ if (bounded && (x < 0 || x > WIDTH - 1 || y < 0 || y > FLOOR))
23
+ return;
24
+ grid.set(`${x},${y}`, pixel);
47
25
  };
48
- /** One frame of a design: its height in rows of its width in pixels, "." where there is none. */
49
- function frame(design, t) {
50
- const grid = Array.from({ length: design.height }, () => Array(design.width).fill("."));
51
- const centre = design.width / 2;
52
- const set = (y, x, pixel, over = true) => {
53
- if (y < 0 || y >= design.height || x < 0 || x >= design.width)
54
- return;
55
- if (over || grid[y][x] === ".")
56
- grid[y][x] = pixel;
57
- };
58
- const shut = SHUT[t % SHUT.length];
59
- // Shut, it has risen a pixel.
60
- const top = shut > 0.6 ? 0 : 1;
61
- const widths = design.open.map((open, row) => 2 * Math.round((open - (open - design.shut[row]) * shut) / 2));
62
- widths.forEach((width, row) => {
63
- for (let x = centre - width / 2; x < centre + width / 2; x++)
64
- set(top + row, x, "p");
65
- });
66
- const rimWidth = widths.at(-1);
67
- const rim = top + widths.length;
68
- // The underside along the bottom row, the rim scalloped under it.
69
- for (let x = centre - rimWidth / 2 + 2; x < centre + rimWidth / 2 - 2; x++)
70
- set(rim - 1, x, "e");
71
- for (let x = centre - rimWidth / 2; x < centre + rimWidth / 2; x += 2)
72
- set(rim, x + 1, "p");
73
- // A glint up on the left, curving with the dome, and a pixel of shine.
74
- for (const [dx, dy] of design.glint)
75
- set(top + dy, centre + dx, "h");
76
- set(top + design.shine[1], centre + design.shine[0], "w");
77
- const gutLeft = centre - Math.floor(design.gut[0].length / 2);
78
- design.gut.forEach((line, dy) => [...line].forEach((pixel, dx) => pixel === "o" && set(top + design.gutRow + dy, gutLeft + dx, "o")));
79
- // The tentacles: rooted under the bell, drawn in as it shuts, swaying - the further down, the later.
80
- const phase = (2 * Math.PI * t) / SHUT.length;
81
- design.strands.forEach((strand, i) => {
82
- const root = centre - 0.5 + strand.at * (rimWidth / design.open.at(-1));
83
- for (let y = 0; y < strand.length; y++) {
84
- const x = Math.round(root + strand.sway * Math.sin(phase - y * 0.55 + i * 0.8) * Math.min(1, y / 3));
85
- const left = strand.length - y;
86
- // The tips come apart, every other pixel, fading - into code.
87
- if (left <= 3 && y % 2 === 1)
26
+ /** The bell's colour down its height: pink, through its midtone, to its shadow - dithered where one meets the next. */
27
+ const band = (t, x, y) => t < 0.3 ? "p" : t < 0.42 ? ((x + y) % 2 ? "p" : "m") : t < 0.62 ? "m" : t < 0.74 ? ((x + y) % 2 ? "m" : "i") : "i";
28
+ /** An ellipse filled with the bell's colours, or its top half - a dome. */
29
+ function shape(grid, cx, cy, rx, ry, dome, bounded = true) {
30
+ for (let y = Math.floor(cy - ry); y <= Math.ceil(cy + ry); y++) {
31
+ if (dome && y > cy)
32
+ continue;
33
+ for (let x = Math.floor(cx - rx); x <= Math.ceil(cx + rx); x++) {
34
+ const dx = (x + 0.5 - cx) / rx;
35
+ const dy = (y + 0.5 - cy) / ry;
36
+ if (dx * dx + dy * dy > 1)
88
37
  continue;
89
- set(rim + 1 + y, x, left > 5 ? strand.ink : left > 2 ? "l" : "g", false);
90
- // The oral arms are frilled.
91
- if (strand.ink === "n" && left > 5 && (y + t) % 3 === 0)
92
- set(rim + 1 + y, x + (strand.at < 0 ? -1 : 1), "n", false);
38
+ const t = dome ? (y - (cy - ry)) / ry : (y - (cy - ry)) / (2 * ry);
39
+ put(grid, x, y, band(Math.max(0, Math.min(1, t)), x, y), bounded);
93
40
  }
94
- });
95
- return grid.map((row) => row.join(""));
41
+ }
96
42
  }
97
- export const JELLYFISH_WIDTH = LARGE.width;
98
- export const JELLYFISH_HEIGHT = LARGE.height;
99
- /** The large jellyfish's frames, a stroke: what the pictures are rendered from. */
100
- export const JELLYFISH_FRAMES = SHUT.map((_, t) => frame(LARGE, t));
101
- /** The small jellyfish's frames, a stroke: what a terminal without images draws, in half blocks. */
102
- export const JELLYFISH_TEXT_FRAMES = SHUT.map((_, t) => frame(SMALL, t));
43
+ /** The bell's rim: every fourth pixel from the middle left out, the rest eggplant. */
44
+ function rim(grid, y) {
45
+ for (let x = 0; x < WIDTH + 1; x++) {
46
+ const key = `${x},${y}`;
47
+ if (!grid.has(key))
48
+ continue;
49
+ if (Math.abs(x - 20) % 4 === 2)
50
+ grid.delete(key);
51
+ else
52
+ grid.set(key, "e");
53
+ }
54
+ }
55
+ /** The glint on the bell, up on the left. */
56
+ const GLINT = [[11, 6], [12, 5], [13, 5], [10, 7], [11, 7], [14, 4]];
57
+ /** Where each of the thirteen tentacles hangs, how long it is, and its place in the sway. */
58
+ const TENTACLES = [8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32].map((x, i) => ({ x, length: 16 + ((i * 7) % 12), i }));
59
+ /** The jellyfish at a scale `k`, its rim on row `cy`, its tentacles grown `grown` of the way. */
60
+ function medusa(grid, k, cy, grown) {
61
+ shape(grid, 20.5, cy, 14.5 * k, 16 * k, true);
62
+ if (k > 0.45)
63
+ rim(grid, Math.floor(cy));
64
+ if (k > 0.8) {
65
+ for (const [gx, gy] of GLINT) {
66
+ const x = Math.round(20 + (gx - 20) * k);
67
+ const y = Math.round(cy + (gy - 18) * k);
68
+ if (grid.has(`${x},${y}`))
69
+ put(grid, x, y, "h");
70
+ }
71
+ }
72
+ const gutY = cy - 7 * k;
73
+ const halfWidth = Math.max(1.5, 2.5 * k);
74
+ const halfHeight = Math.max(0.6, 1.5 * k);
75
+ for (let y = Math.round(gutY - halfHeight); y < gutY + halfHeight; y++)
76
+ for (let x = Math.round(20 - halfWidth); x < 20 + halfWidth; x++)
77
+ put(grid, x, y, "o");
78
+ if (k > 0.7) {
79
+ for (let x = 19; x <= 21; x++) {
80
+ put(grid, x, gutY - 2, "o");
81
+ put(grid, x, gutY + 2, "o");
82
+ }
83
+ }
84
+ for (const { x, length, i } of TENTACLES) {
85
+ const shown = Math.round(length * Math.pow(grown, 1.2));
86
+ const root = 20 + (x - 20) * k;
87
+ for (let j = 0; j < shown; j++)
88
+ put(grid, root + Math.round(Math.sin(j * 0.5 + i) * 1.6 * (j / shown)), cy + 1 + j, j % 2 ? "n" : "N");
89
+ }
90
+ }
91
+ /** The polyp on the floor, grown `u` of the way: a stalk, a head with its gut, a crown of tentacles. */
92
+ function polyp(grid, u) {
93
+ shape(grid, 20, FLOOR, 3, 1.4, true);
94
+ const height = Math.round(2 + 12 * u);
95
+ const radius = 1.4 + 2.2 * u;
96
+ for (let y = FLOOR - height; y <= FLOOR; y++) {
97
+ for (let x = 19; x <= 20; x++)
98
+ put(grid, x, y, band(0.45 + (0.55 * (y - (FLOOR - height))) / Math.max(1, height), x, y));
99
+ }
100
+ const head = FLOOR - height - radius + 1;
101
+ shape(grid, 20, head, radius, radius, false);
102
+ put(grid, 19, head, "o");
103
+ put(grid, 20, head, "o");
104
+ if (radius > 2.5) {
105
+ put(grid, 19, head - 1, "o");
106
+ put(grid, 20, head - 1, "o");
107
+ }
108
+ const crown = [[-4, -3], [3, -3], [-1, -5], [1, -5], [-5, -4], [4, -4], [-1, -6], [1, -6], [-6, -5], [5, -5]];
109
+ const scale = radius / 3.6;
110
+ for (let i = 0; i < Math.floor(u * crown.length); i++) {
111
+ const [dx, dy] = crown[i];
112
+ put(grid, 20 + dx * scale + (dx < 0 ? 0 : 0.5), head + dy * scale, i % 2 ? "c" : "N");
113
+ }
114
+ }
115
+ const ease = (t) => (t < 0 ? 0 : t > 1 ? 1 : t * t * (3 - 2 * t));
116
+ /** The lifecycle: twenty frames of the polyp growing into the jellyfish, nineteen of it sinking and the polyp growing back. */
117
+ function lifecycle() {
118
+ const frames = [];
119
+ for (let f = 0; f < 20; f++) {
120
+ const grid = new Map();
121
+ const g = f / 19;
122
+ const p = ease((g - 0.2) / 0.65);
123
+ if (1 - p > 0.06)
124
+ polyp(grid, 1 - p);
125
+ else if (p < 1)
126
+ shape(grid, 20, FLOOR, 2.2, 1, true);
127
+ const e = ease((g - 0.08) / 0.92);
128
+ if (e > 0)
129
+ medusa(grid, 0.18 + 0.82 * e, 40 - 22 * e, Math.pow(e, 1.5));
130
+ frames.push(grid);
131
+ }
132
+ // It sinks to the floor first, and the polyp grows up from there.
133
+ for (let f = 0; f <= 18; f++) {
134
+ const grid = new Map();
135
+ if (f <= 8) {
136
+ const s = f / 8;
137
+ medusa(grid, 1 - 0.72 * s, 18 + 40 * s, 1 - s);
138
+ }
139
+ else if (f <= 11) {
140
+ const w = (f - 9) / 2;
141
+ shape(grid, 20, FLOOR, 4.2 - 1.2 * w, 3.2 - 0.8 * w, true);
142
+ put(grid, 19, FLOOR - 1, "o");
143
+ put(grid, 20, FLOOR - 1, "o");
144
+ }
145
+ else
146
+ polyp(grid, (f - 11) / 7);
147
+ frames.push(grid);
148
+ }
149
+ return frames;
150
+ }
151
+ /**
152
+ * The jellyfish swimming, grown: its bell still, its tentacles in one of two strokes - the second drawn a pixel higher,
153
+ * as it bobs.
154
+ */
155
+ function hero(stroke) {
156
+ const grid = new Map();
157
+ shape(grid, 20.5, 18, 14.5, 16, true, false);
158
+ rim(grid, 18);
159
+ for (const [x, y] of GLINT)
160
+ if (grid.has(`${x},${y}`))
161
+ put(grid, x, y, "h", false);
162
+ for (let y = 10; y <= 12; y++)
163
+ for (let x = 18; x <= 22; x++)
164
+ put(grid, x, y, "o", false);
165
+ for (let x = 19; x <= 21; x++) {
166
+ put(grid, x, 9, "o", false);
167
+ put(grid, x, 13, "o", false);
168
+ }
169
+ for (const { x, length, i } of TENTACLES) {
170
+ for (let k = 0; k < length; k++) {
171
+ const sway = Math.round(Math.sin(k * 0.5 + i + (stroke === 1 ? 2.2 : 0)) * 1.6 * (k / length));
172
+ put(grid, x + sway, 19 + k, k % 2 ? "n" : "N", false);
173
+ }
174
+ }
175
+ if (stroke === 0)
176
+ return grid;
177
+ const raised = new Map();
178
+ for (const [key, pixel] of grid) {
179
+ const [x, y] = key.split(",").map(Number);
180
+ raised.set(`${x},${y - 1}`, pixel);
181
+ }
182
+ return raised;
183
+ }
184
+ /** Every frame on one grid, cut to what any of them draws on: rows of pixels, "." where there is none. */
185
+ function crop(grids) {
186
+ const cells = grids.flatMap((grid) => [...grid.keys()].map((key) => key.split(",").map(Number)));
187
+ const left = Math.min(...cells.map(([x]) => x));
188
+ const right = Math.max(...cells.map(([x]) => x));
189
+ const top = Math.min(...cells.map(([, y]) => y));
190
+ const bottom = Math.max(...cells.map(([, y]) => y));
191
+ return grids.map((grid) => Array.from({ length: bottom - top + 1 }, (_, y) => Array.from({ length: right - left + 1 }, (_, x) => grid.get(`${x + left},${y + top}`) ?? ".").join("")));
192
+ }
193
+ /** The lifecycle's frames, then the jellyfish's two strokes, swimming. */
194
+ const LIFECYCLE = lifecycle();
195
+ const SWIM = LIFECYCLE.length;
196
+ /** The jellyfish's frames, each as rows of pixels: what the pictures are rendered from. */
197
+ export const JELLYFISH_FRAMES = crop([...LIFECYCLE, hero(0), hero(1)]);
198
+ export const JELLYFISH_WIDTH = JELLYFISH_FRAMES[0][0].length;
199
+ export const JELLYFISH_HEIGHT = JELLYFISH_FRAMES[0].length;
200
+ /**
201
+ * The order the frames play in, round and round: the polyp a moment, growing into the jellyfish, swimming four
202
+ * seconds - a stroke every five frames - then sinking, and the polyp growing back.
203
+ */
204
+ export const JELLYFISH_SEQUENCE = [
205
+ ...[0, 0, 0],
206
+ ...Array.from({ length: 20 }, (_, f) => f),
207
+ ...Array.from({ length: 27 }, (_, f) => SWIM + (Math.floor(f / 5) % 2)),
208
+ ...Array.from({ length: 19 }, (_, f) => 20 + f),
209
+ ];
103
210
  /** How long each frame shows. */
104
- export const JELLYFISH_FRAME_MS = 250;
211
+ export const JELLYFISH_FRAME_MS = 150;
212
+ /** How much smaller the frames are drawn in half blocks: a cell is a block of SHRINK pixels square. */
213
+ const SHRINK = 3;
214
+ /**
215
+ * A frame a third of the size: each block drawn as the pixel at its middle. What most of a block is would run the
216
+ * tentacles, two pixels apart, together into one fringe; its middle keeps strands apart.
217
+ */
218
+ function shrink(pixels) {
219
+ const middle = Math.floor(SHRINK / 2);
220
+ const rows = [];
221
+ for (let y = middle; y < pixels.length; y += SHRINK) {
222
+ let row = "";
223
+ for (let x = middle; x < pixels[0].length; x += SHRINK)
224
+ row += pixels[y][x];
225
+ rows.push(row);
226
+ }
227
+ return rows;
228
+ }
229
+ /** The frames as a terminal without images draws them, in half blocks: the same order, a third of the size. */
230
+ export const JELLYFISH_TEXT_FRAMES = JELLYFISH_FRAMES.map(shrink);
231
+ /** A colour laid over another, `alpha` of it showing. */
232
+ function over(hex, under, alpha) {
233
+ if (alpha >= 1)
234
+ return hex;
235
+ const channel = (colour, at) => Number.parseInt(colour.slice(at, at + 2), 16);
236
+ return `#${[1, 3, 5].map((at) => Math.round(channel(hex, at) * alpha + channel(under, at) * (1 - alpha)).toString(16).padStart(2, "0")).join("")}`;
237
+ }
105
238
  /**
106
239
  * A frame drawn in half blocks for a terminal that shows no images: two pixels a cell, the upper one the text's colour
107
240
  * and the lower one the background's, in as many colours as the terminal has: a column a pixel, a row every two.
108
241
  */
109
242
  export function halfBlocks(pixels, appearance, truecolor) {
110
243
  const colours = JELLYFISH_COLOURS[appearance];
111
- const colour = (pixel) => (pixel === undefined || pixel === "." ? undefined : colours[pixel]);
244
+ const colour = (pixel) => pixel === undefined || pixel === "." ? undefined : over(colours[pixel], BACKGROUND[appearance], ALPHA[pixel]);
112
245
  const rows = [];
113
246
  for (let y = 0; y < pixels.length; y += 2) {
114
247
  let row = "";
@@ -281,6 +281,8 @@ export interface ModuleCompletion {
281
281
  completed: number;
282
282
  /** Of the completed ones, those its conformance tests are recorded as written for, as the spec has them now. */
283
283
  conformanceTested: number;
284
+ /** Those conformance-tested ones, by number: for a host that shows each functional spec's state. */
285
+ tested: string[];
284
286
  /** Its functional specs not completed, by number. */
285
287
  incomplete: string[];
286
288
  }
@@ -121,7 +121,7 @@ export function functionalityCompletion(plainFile) {
121
121
  if (readRecord(entry.buildDir) !== null)
122
122
  delivered = index;
123
123
  });
124
- const counts = chain.map((entry) => ({ module: entry.moduleName, specified: entry.frids.length, completed: 0, conformanceTested: 0, incomplete: [...entry.frids] }));
124
+ const counts = chain.map((entry) => ({ module: entry.moduleName, specified: entry.frids.length, completed: 0, conformanceTested: 0, tested: [], incomplete: [...entry.frids] }));
125
125
  if (delivered === -1)
126
126
  return { codebase: tipOf(chain).buildDir, modules: counts };
127
127
  const codebase = chain.slice(0, delivered + 1);
@@ -155,7 +155,8 @@ export function functionalityCompletion(plainFile) {
155
155
  const completed = entry.frids.filter((frid) => !touched.has(`${entry.moduleName}:${frid}`));
156
156
  const tested = (readRecord(conformanceDirFor(entry.moduleName, modulesRoot(projectDirOf(entry.plainFile))))?.[FUNCTIONAL_REQUIREMENTS] ?? []).filter((item) => item.frid !== undefined && item.markdown.trim() === specText(entry.moduleName, item.frid));
157
157
  count.completed = completed.length;
158
- count.conformanceTested = completed.filter((frid) => tested.some((item) => item.frid === frid)).length;
158
+ count.tested = completed.filter((frid) => tested.some((item) => item.frid === frid));
159
+ count.conformanceTested = count.tested.length;
159
160
  count.incomplete = entry.frids.filter((frid) => !completed.includes(frid));
160
161
  });
161
162
  return { codebase: tip.buildDir, modules: counts };
Binary file
package/docs/theming.md CHANGED
@@ -14,7 +14,7 @@ Bad input fails quietly - an unknown `--use-theme` name and a malformed theme fi
14
14
 
15
15
  ## The palette
16
16
 
17
- Both themes, the command line's colours, the spec diff's, the banner's jellyfish and the docs' diagram are drawn from turri's design system and nothing else: dark, quiet surfaces, one pink thing to press, neon for anything you can follow. These are all the colours turri has - none is mixed, tinted or added.
17
+ Both themes, the command line's colours, the spec diff's, the banner's jellyfish and the docs' diagram are drawn from turri's design system and nothing else: dark, quiet surfaces, one pink thing to press, neon for anything you can follow. These are all the colours turri has - none is mixed, tinted or added, but for the jellyfish's tentacles, every other pixel of which the design draws half seen.
18
18
 
19
19
  | name | value | in `turri-dark` |
20
20
  |---|---|---|
@@ -25,13 +25,15 @@ Both themes, the command line's colours, the spec diff's, the banner's jellyfish
25
25
  | lines | `#22344a` | rules, dividers, frames: `borderMuted`, `mdHr` |
26
26
  | path | `#3e5a6b` | the editor's border, what is dim: `dim`, line numbers |
27
27
  | grauzone | `#85a3b2` | body text, and what can be skimmed: `text`, `muted` |
28
- | white | `#f4f7f9` | headings, tool titles, what you typed, the jellyfish's shine |
28
+ | white | `#f4f7f9` | headings, tool titles, what you typed |
29
29
  | siesta tan | `#e9d8c8` | strings in code, \*\*\*plain's text in quotes or backticks |
30
- | eggplant | `#732553` | a removed line's fill in a spec diff |
30
+ | eggplant | `#732553` | a removed line's fill in a spec diff, the jellyfish's rim |
31
31
  | neon | `#5ce1e6` | links, focus, the selection, section labels, \*\*\*plain's concepts in a spec diff: `accent`, `mdLink`, regen's `◆` |
32
32
  | neon hover | `#8eebee` | inline code - siesta tan beside white text was too close to tell apart, and tiring to read - types in code, an added line's fill in a light theme's spec diff: `mdCode` |
33
33
  | pink | `#ff5c8d` | the wordmark, the jellyfish's bell, and what is done - its `✓`, the spec map's `◇` and `◆`: `success` |
34
- | pink hover | `#ff7fa6` | \*\*\*plain's section headers, a removed line's `-`, keywords in code |
34
+ | pink hover | `#ff7fa6` | the glint on the jellyfish's bell, \*\*\*plain's section headers, a removed line's `-`, keywords in code |
35
+ | orchid | `#a9479a` | the jellyfish's bell, between its pink and its indigo - nowhere else |
36
+ | indigo | `#5b57d9` | the jellyfish's bell, at its rim - nowhere else |
35
37
  | orange | `#ff7a3d` | the jellyfish's gut, the `$` of bash mode, and everything that asks for you - what failed (`✗`), what works with limits, what needs you (the spec map's `!`): `error`, `warning` |
36
38
 
37
39
  The design system keeps pink for the one thing to press and orange for the `$` prompt alone. A terminal has no buttons, but it needs status colours, so pink carries what is done and orange everything that asks for you - what failed, what works with limits, what waits on you - the mark (`✗`, `!`, `■`) telling them apart. The palette has no red.
package/docs/using.md CHANGED
@@ -7,7 +7,7 @@ cd ~/my-project
7
7
  turri
8
8
  ```
9
9
 
10
- That is pi, with turri's tools, skills and themes loaded - `turri` alone, or with pi's options (`turri -c`, `turri --harness-model <m>`). It opens with turri's own banner - the jellyfish it is named after, swimming until the conversation fills the screen: a pixel jellyfish - a pink bell, an orange gut, neon tentacles coming apart into code at their tips - swimming a stroke a loop, its bell shutting and rising, its tentacles following: a picture in a terminal that shows images (Kitty, Ghostty, WezTerm, Warp, iTerm2), and drawn smaller in half blocks in any other, tmux included (`src/jellyfish.ts` draws it, `scripts/jellyfish-frames.mjs` writes the pictures) - turri's version (and, run from a clone, the commit it is on, and whether the clone has changes) and where its settings are changed - and only when something is wrong, skills that failed to load or agents' commands less fenced than they should be - rather than pi's startup header and resource list; `turri --verbose` shows those too. pi's "Update Available" notice is left out - turri's pi is a pinned dependency `pi update` never touches - and the terminal's tab reads `🪼 turri - <folder>`, not pi's `π`. Talk to it: ask it to start a project, add a feature, render a module, fix a spec. It decides which skill or tool applies - but not *how* a skill runs: `cultivate` hands the work to the gated runner, which owns the steps, the gates and the judges (see [skills and gates](skills-and-gates.md)).
10
+ That is pi, with turri's tools, skills and themes loaded - `turri` alone, or with pi's options (`turri -c`, `turri --harness-model <m>`). It opens with turri's own banner - the jellyfish it is named after, regenerating until the conversation fills the screen: a pixel polyp on the sea floor grows into the jellyfish - a bell from pink down to indigo, an orange gut, neon tentacles - which swims a while, sinks back to the floor, and the polyp grows again, round and round: a picture in a terminal that shows images (Kitty, Ghostty, WezTerm, Warp, iTerm2), and drawn at a third of the size in half blocks in any other, tmux included (`src/jellyfish.ts` draws it, ported from the design system's pixel kit, `scripts/jellyfish-frames.mjs` writes the pictures) - turri's version (and, run from a clone, the commit it is on, and whether the clone has changes) and where its settings are changed - and only when something is wrong, skills that failed to load or agents' commands less fenced than they should be - rather than pi's startup header and resource list; `turri --verbose` shows those too. pi's "Update Available" notice is left out - turri's pi is a pinned dependency `pi update` never touches - and the terminal's tab reads `🪼 turri - <folder>`, not pi's `π`. Talk to it: ask it to start a project, add a feature, render a module, fix a spec. It decides which skill or tool applies - but not *how* a skill runs: `cultivate` hands the work to the gated runner, which owns the steps, the gates and the judges (see [skills and gates](skills-and-gates.md)).
11
11
 
12
12
  The skills that build from scratch:
13
13
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turri-dev/cli",
3
- "version": "0.1.7-next.eb57c34",
3
+ "version": "0.1.7-next.f0d55df",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
Binary file
Binary file