@sigrun/tui 0.4.1 → 0.5.0

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 (114) hide show
  1. package/CHANGELOG.md +10 -1
  2. package/dist/actions.d.ts +119 -0
  3. package/dist/actions.js +12 -0
  4. package/dist/api.d.ts +209 -0
  5. package/dist/api.js +282 -0
  6. package/dist/blocks/braille.d.ts +41 -0
  7. package/dist/blocks/braille.js +131 -0
  8. package/dist/blocks/chart.d.ts +20 -0
  9. package/dist/blocks/chart.js +397 -0
  10. package/dist/blocks/chat-transport.d.ts +44 -0
  11. package/dist/blocks/chat-transport.js +116 -0
  12. package/dist/blocks/collection/calendar.d.ts +28 -0
  13. package/dist/blocks/collection/calendar.js +232 -0
  14. package/dist/blocks/collection/card.d.ts +36 -0
  15. package/dist/blocks/collection/card.js +128 -0
  16. package/dist/blocks/collection/collection.d.ts +9 -0
  17. package/dist/blocks/collection/collection.js +26 -0
  18. package/dist/blocks/collection/grid.d.ts +18 -0
  19. package/dist/blocks/collection/grid.js +72 -0
  20. package/dist/blocks/collection/lanes.d.ts +32 -0
  21. package/dist/blocks/collection/lanes.js +320 -0
  22. package/dist/blocks/collection/list.d.ts +30 -0
  23. package/dist/blocks/collection/list.js +273 -0
  24. package/dist/blocks/collection/move.d.ts +67 -0
  25. package/dist/blocks/collection/move.js +96 -0
  26. package/dist/blocks/collection/shared.d.ts +87 -0
  27. package/dist/blocks/collection/shared.js +177 -0
  28. package/dist/blocks/collection/table.d.ts +26 -0
  29. package/dist/blocks/collection/table.js +221 -0
  30. package/dist/blocks/content.d.ts +48 -0
  31. package/dist/blocks/content.js +253 -0
  32. package/dist/blocks/context.d.ts +117 -0
  33. package/dist/blocks/context.js +65 -0
  34. package/dist/blocks/diff.d.ts +21 -0
  35. package/dist/blocks/diff.js +25 -0
  36. package/dist/blocks/ext-transport.d.ts +70 -0
  37. package/dist/blocks/ext-transport.js +165 -0
  38. package/dist/blocks/form/choice.d.ts +54 -0
  39. package/dist/blocks/form/choice.js +175 -0
  40. package/dist/blocks/form/fields.d.ts +56 -0
  41. package/dist/blocks/form/fields.js +136 -0
  42. package/dist/blocks/form/file.d.ts +61 -0
  43. package/dist/blocks/form/file.js +121 -0
  44. package/dist/blocks/form/form.d.ts +66 -0
  45. package/dist/blocks/form/form.js +764 -0
  46. package/dist/blocks/form/list.d.ts +37 -0
  47. package/dist/blocks/form/list.js +70 -0
  48. package/dist/blocks/form/values.d.ts +20 -0
  49. package/dist/blocks/form/values.js +106 -0
  50. package/dist/blocks/icons.d.ts +14 -0
  51. package/dist/blocks/icons.js +74 -0
  52. package/dist/blocks/image.d.ts +39 -0
  53. package/dist/blocks/image.js +82 -0
  54. package/dist/blocks/interaction-transport.d.ts +42 -0
  55. package/dist/blocks/interaction-transport.js +79 -0
  56. package/dist/blocks/layout.d.ts +31 -0
  57. package/dist/blocks/layout.js +155 -0
  58. package/dist/blocks/log.d.ts +24 -0
  59. package/dist/blocks/log.js +111 -0
  60. package/dist/blocks/meter.d.ts +16 -0
  61. package/dist/blocks/meter.js +50 -0
  62. package/dist/blocks/page.d.ts +54 -0
  63. package/dist/blocks/page.js +360 -0
  64. package/dist/blocks/render.d.ts +31 -0
  65. package/dist/blocks/render.js +163 -0
  66. package/dist/blocks/surface-view.d.ts +41 -0
  67. package/dist/blocks/surface-view.js +137 -0
  68. package/dist/blocks/surface.d.ts +80 -0
  69. package/dist/blocks/surface.js +506 -0
  70. package/dist/blocks/tones.d.ts +23 -0
  71. package/dist/blocks/tones.js +63 -0
  72. package/dist/blocks/treeview.d.ts +22 -0
  73. package/dist/blocks/treeview.js +121 -0
  74. package/dist/blocks/width.d.ts +32 -0
  75. package/dist/blocks/width.js +70 -0
  76. package/dist/clipboard.d.ts +20 -0
  77. package/dist/clipboard.js +19 -0
  78. package/dist/editor-launch.d.ts +34 -0
  79. package/dist/editor-launch.js +70 -0
  80. package/dist/keymaps/emacs.d.ts +18 -0
  81. package/dist/keymaps/emacs.js +172 -0
  82. package/dist/keymaps/standard.d.ts +17 -0
  83. package/dist/keymaps/standard.js +175 -0
  84. package/dist/keymaps/types.d.ts +43 -0
  85. package/dist/keymaps/types.js +28 -0
  86. package/dist/keymaps/vim.d.ts +48 -0
  87. package/dist/keymaps/vim.js +291 -0
  88. package/dist/launch.d.ts +159 -0
  89. package/dist/launch.js +478 -0
  90. package/dist/sse.d.ts +112 -0
  91. package/dist/sse.js +418 -0
  92. package/dist/theme.d.ts +51 -0
  93. package/dist/theme.js +119 -0
  94. package/dist/ui/ambient.d.ts +78 -0
  95. package/dist/ui/ambient.js +264 -0
  96. package/dist/ui/app.d.ts +319 -0
  97. package/dist/ui/app.js +1539 -0
  98. package/dist/ui/composer.d.ts +66 -0
  99. package/dist/ui/composer.js +133 -0
  100. package/dist/ui/conversation-pane.d.ts +143 -0
  101. package/dist/ui/conversation-pane.js +615 -0
  102. package/dist/ui/help.d.ts +15 -0
  103. package/dist/ui/help.js +80 -0
  104. package/dist/ui/interactions.d.ts +91 -0
  105. package/dist/ui/interactions.js +477 -0
  106. package/dist/ui/picker.d.ts +26 -0
  107. package/dist/ui/picker.js +130 -0
  108. package/dist/ui/settings.d.ts +68 -0
  109. package/dist/ui/settings.js +254 -0
  110. package/dist/ui/sidebar.d.ts +99 -0
  111. package/dist/ui/sidebar.js +269 -0
  112. package/dist/ui/status-bar.d.ts +77 -0
  113. package/dist/ui/status-bar.js +146 -0
  114. package/package.json +4 -3
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The list field (UI-blocks U2 Task 5, spec §5.6): a repeatable group —
3
+ * `item` describes the field of ONE entry; the value is an array of
4
+ * entry values. Add (`a` on the field), remove (`d`), and reorder (`u`
5
+ * up / `j` down) honour `min`/`max`. Entries edit inline through the
6
+ * item field's text editor (choice/other items stay text-shaped in v1 —
7
+ * noted in the module header of the form).
8
+ */
9
+ import type { JsonValue, ListField } from "@sigrun/shared/blocks";
10
+ /** The entries of a list value (never undefined). */
11
+ export declare function listEntries(value: JsonValue): readonly JsonValue[];
12
+ /** Can the list add an entry (`max`)? */
13
+ export declare function listCanAdd(field: ListField, value: JsonValue): boolean;
14
+ /** Can the list remove an entry (`min`)? */
15
+ export declare function listCanRemove(field: ListField, value: JsonValue): boolean;
16
+ /** Add an entry (the item field's empty value). */
17
+ export declare function listAdd(value: JsonValue): JsonValue;
18
+ /** Remove the entry at `index`. */
19
+ export declare function listRemove(value: JsonValue, index: number): JsonValue;
20
+ /** Move the entry at `index` by `delta` (reorder; clamped). */
21
+ export declare function listMove(value: JsonValue, index: number, delta: number): JsonValue;
22
+ /** Replace the entry at `index`. */
23
+ export declare function listSet(value: JsonValue, index: number, entry: JsonValue): JsonValue;
24
+ /** The per-field list state: the entry cursor. */
25
+ export declare class ListFieldState {
26
+ cursor: number;
27
+ }
28
+ /** Render the entries (one editor line each) plus the hint row. */
29
+ export declare function listLines(opts: {
30
+ field: ListField;
31
+ width: number;
32
+ value: JsonValue;
33
+ state: ListFieldState;
34
+ focused: boolean;
35
+ /** The rendered editor line of an entry (the form seeds caret state). */
36
+ entryLine(index: number, entry: JsonValue, focused: boolean): string;
37
+ }): string[];
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The list field (UI-blocks U2 Task 5, spec §5.6): a repeatable group —
3
+ * `item` describes the field of ONE entry; the value is an array of
4
+ * entry values. Add (`a` on the field), remove (`d`), and reorder (`u`
5
+ * up / `j` down) honour `min`/`max`. Entries edit inline through the
6
+ * item field's text editor (choice/other items stay text-shaped in v1 —
7
+ * noted in the module header of the form).
8
+ */
9
+ import { truncate } from "../width.js";
10
+ /** The entries of a list value (never undefined). */
11
+ export function listEntries(value) {
12
+ return Array.isArray(value) ? value : [];
13
+ }
14
+ /** Can the list add an entry (`max`)? */
15
+ export function listCanAdd(field, value) {
16
+ return field.max === undefined || listEntries(value).length < field.max;
17
+ }
18
+ /** Can the list remove an entry (`min`)? */
19
+ export function listCanRemove(field, value) {
20
+ return listEntries(value).length > (field.min ?? 0);
21
+ }
22
+ /** Add an entry (the item field's empty value). */
23
+ export function listAdd(value) {
24
+ return [...listEntries(value), ""];
25
+ }
26
+ /** Remove the entry at `index`. */
27
+ export function listRemove(value, index) {
28
+ return listEntries(value).filter((_, at) => at !== index);
29
+ }
30
+ /** Move the entry at `index` by `delta` (reorder; clamped). */
31
+ export function listMove(value, index, delta) {
32
+ const entries = [...listEntries(value)];
33
+ const target = index + delta;
34
+ if (target < 0 || target >= entries.length)
35
+ return entries;
36
+ const [entry] = entries.splice(index, 1);
37
+ entries.splice(target, 0, entry);
38
+ return entries;
39
+ }
40
+ /** Replace the entry at `index`. */
41
+ export function listSet(value, index, entry) {
42
+ return listEntries(value).map((current, at) => (at === index ? entry : current));
43
+ }
44
+ /** The per-field list state: the entry cursor. */
45
+ export class ListFieldState {
46
+ cursor = 0;
47
+ }
48
+ /** Render the entries (one editor line each) plus the hint row. */
49
+ export function listLines(opts) {
50
+ const { field, width, value, state, focused } = opts;
51
+ const entries = listEntries(value);
52
+ const lines = [];
53
+ entries.forEach((entry, index) => {
54
+ const cursor = focused && index === state.cursor ? "> " : " ";
55
+ lines.push(truncate(`${cursor}${opts.entryLine(index, entry, focused && index === state.cursor)}`, width));
56
+ });
57
+ if (focused) {
58
+ const hints = [];
59
+ if (listCanAdd(field, value))
60
+ hints.push("a add");
61
+ if (entries.length > 0 && listCanRemove(field, value))
62
+ hints.push("d remove");
63
+ if (field.reorder === true && entries.length > 1)
64
+ hints.push("u/j move");
65
+ if (hints.length > 0) {
66
+ lines.push(truncate(` ${hints.join(" · ")}`, width));
67
+ }
68
+ }
69
+ return lines;
70
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Form values plumbing (UI-blocks U2 Task 5, spec §5.6 — the port of
3
+ * U1's `form/values.ts`): the form reads its initial values from the
4
+ * bound object and tracks edits locally; these helpers flatten the
5
+ * field tree (sections nest fields), read a field's value out of the
6
+ * bound object, and validate a value against the shared field props
7
+ * (`required`, `min`/`max`, `minLength`/`maxLength`, `pattern`).
8
+ */
9
+ import type { Field, FormBlock, JsonValue } from "@sigrun/shared/blocks";
10
+ /** Whether a form child is a field (sections hold fields/blocks). */
11
+ export declare function isFieldNode(node: unknown): node is Field;
12
+ /** All fields of the form, depth-first through sections. */
13
+ export declare function collectFields(block: FormBlock): readonly Field[];
14
+ /** The initial value of a field from the bound object (missing →
15
+ * a type-appropriate empty; a secret is write-only — the bound slot
16
+ * carries the STATUS, never a value to echo back, §5.6/§15). */
17
+ export declare function initialValue(field: Field, bound: Record<string, JsonValue>): JsonValue;
18
+ /** Client validation (spec §5.6): the error message or null.
19
+ * `label` names the field in the message. */
20
+ export declare function validateField(field: Field, value: JsonValue, label: string): string | null;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Form values plumbing (UI-blocks U2 Task 5, spec §5.6 — the port of
3
+ * U1's `form/values.ts`): the form reads its initial values from the
4
+ * bound object and tracks edits locally; these helpers flatten the
5
+ * field tree (sections nest fields), read a field's value out of the
6
+ * bound object, and validate a value against the shared field props
7
+ * (`required`, `min`/`max`, `minLength`/`maxLength`, `pattern`).
8
+ */
9
+ /** Whether a form child is a field (sections hold fields/blocks). */
10
+ export function isFieldNode(node) {
11
+ return (typeof node === "object" &&
12
+ node !== null &&
13
+ typeof node.type === "string" &&
14
+ typeof node.name === "string");
15
+ }
16
+ /** All fields of the form, depth-first through sections. */
17
+ export function collectFields(block) {
18
+ const out = [];
19
+ const walk = (children) => {
20
+ for (const child of children) {
21
+ if (isFieldNode(child))
22
+ out.push(child);
23
+ else if (typeof child === "object" &&
24
+ child !== null &&
25
+ child.type === "section") {
26
+ walk(child.children);
27
+ }
28
+ }
29
+ };
30
+ walk(block.children);
31
+ return out;
32
+ }
33
+ /** The initial value of a field from the bound object (missing →
34
+ * a type-appropriate empty; a secret is write-only — the bound slot
35
+ * carries the STATUS, never a value to echo back, §5.6/§15). */
36
+ export function initialValue(field, bound) {
37
+ if (field.type === "secret")
38
+ return "";
39
+ const value = bound[field.name];
40
+ if (value !== undefined)
41
+ return value;
42
+ switch (field.type) {
43
+ case "toggle":
44
+ // §13.4 fix: a default-ON toggle initializes ON when the bound
45
+ // value is absent (absent = the default).
46
+ return field.default === true;
47
+ case "choice":
48
+ return field.multi === true ? [] : "";
49
+ case "list":
50
+ return [];
51
+ case "daterange":
52
+ return {};
53
+ default:
54
+ return "";
55
+ }
56
+ }
57
+ /** Client validation (spec §5.6): the error message or null.
58
+ * `label` names the field in the message. */
59
+ export function validateField(field, value, label) {
60
+ const empty = value === undefined ||
61
+ value === null ||
62
+ value === "" ||
63
+ (Array.isArray(value) && value.length === 0) ||
64
+ (typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0);
65
+ if (field.required === true && empty)
66
+ return `${label} is required`;
67
+ if (empty)
68
+ return null;
69
+ if (typeof value === "number") {
70
+ if (field.min !== undefined && value < field.min) {
71
+ return `${label} must be at least ${field.min}`;
72
+ }
73
+ if (field.max !== undefined && value > field.max) {
74
+ return `${label} must be at most ${field.max}`;
75
+ }
76
+ }
77
+ if (typeof value === "string") {
78
+ if (field.minLength !== undefined && value.length < field.minLength) {
79
+ return `${label} must be at least ${field.minLength} characters`;
80
+ }
81
+ if (field.maxLength !== undefined && value.length > field.maxLength) {
82
+ return `${label} must be at most ${field.maxLength} characters`;
83
+ }
84
+ if (field.pattern !== undefined && value !== "") {
85
+ let regex = null;
86
+ try {
87
+ regex = new RegExp(`^(?:${field.pattern})$`);
88
+ }
89
+ catch {
90
+ regex = null;
91
+ }
92
+ if (regex !== null && !regex.test(value)) {
93
+ return `${label} does not match ${field.pattern}`;
94
+ }
95
+ }
96
+ }
97
+ if (Array.isArray(value)) {
98
+ if (field.min !== undefined && value.length < field.min) {
99
+ return `${label} needs at least ${field.min} entries`;
100
+ }
101
+ if (field.max !== undefined && value.length > field.max) {
102
+ return `${label} allows at most ${field.max} entries`;
103
+ }
104
+ }
105
+ return null;
106
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Named icons in the terminal (UI-blocks spec §10.2): each of the 49
3
+ * closed-set names maps to a Unicode glyph with an ASCII fallback. The
4
+ * ASCII form is used when the terminal can't be assumed to render the
5
+ * glyph (the plain/no-Unicode path, and tests that assert structure).
6
+ *
7
+ * Glyphs are chosen for terminal legibility over pictorial fidelity
8
+ * (a 1-cell glyph beats a 2-cell emoji that shifts the grid): mostly
9
+ * Miscellaneous Symbols and Dingbats, all narrow in standard terminals.
10
+ */
11
+ import type { IconName } from "@sigrun/shared/blocks";
12
+ /** The icon glyph for `name` (the ASCII fallback when the terminal
13
+ * lacks Unicode support — callers pass `!ctx.unicode`). */
14
+ export declare function iconGlyph(name: IconName, ascii: boolean): string;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Named icons in the terminal (UI-blocks spec §10.2): each of the 49
3
+ * closed-set names maps to a Unicode glyph with an ASCII fallback. The
4
+ * ASCII form is used when the terminal can't be assumed to render the
5
+ * glyph (the plain/no-Unicode path, and tests that assert structure).
6
+ *
7
+ * Glyphs are chosen for terminal legibility over pictorial fidelity
8
+ * (a 1-cell glyph beats a 2-cell emoji that shifts the grid): mostly
9
+ * Miscellaneous Symbols and Dingbats, all narrow in standard terminals.
10
+ */
11
+ /** The Unicode glyph and its ASCII fallback, per icon name. */
12
+ const GLYPHS = {
13
+ // navigation and actions
14
+ add: ["+", "+"],
15
+ edit: ["✎", "e"],
16
+ delete: ["✖", "x"],
17
+ refresh: ["↻", "r"],
18
+ search: ["⌕", "/"],
19
+ filter: ["▽", "f"],
20
+ share: ["⇱", "s"],
21
+ copy: ["⧉", "c"],
22
+ link: ["⚓", "l"],
23
+ external: ["↗", "^"],
24
+ more: ["⋯", "."],
25
+ check: ["✓", "v"],
26
+ close: ["✕", "x"],
27
+ drag: ["⠿", "#"],
28
+ // objects
29
+ file: ["▤", "f"],
30
+ folder: ["▸", ">"],
31
+ calendar: ["▦", "#"],
32
+ clock: ["◷", "o"],
33
+ user: ["◔", "u"],
34
+ users: ["◍", "U"],
35
+ tag: ["◇", "t"],
36
+ list: ["☰", "="],
37
+ board: ["▥", "|"],
38
+ chart: ["▄", "c"],
39
+ image: ["▣", "i"],
40
+ book: ["❑", "b"],
41
+ cart: ["▤", "C"],
42
+ home: ["⌂", "h"],
43
+ bell: ["⚑", "B"],
44
+ lock: ["▪", "L"],
45
+ key: ["◂", "K"],
46
+ // status
47
+ info: ["ℹ", "i"],
48
+ warn: ["▲", "!"],
49
+ error: ["✖", "!!"],
50
+ success: ["✓", "v"],
51
+ pending: ["◌", "-"],
52
+ running: ["▶", "R"],
53
+ // weather
54
+ sun: ["☀", "*"],
55
+ moon: ["☾", ")"],
56
+ cloud: ["☁", "o"],
57
+ "cloud-sun": ["⛅", "o*"],
58
+ "cloud-rain": ["☔", "or"],
59
+ "cloud-snow": ["❄", "os"],
60
+ "cloud-lightning": ["⚡", "ol"],
61
+ fog: ["≋", "~"],
62
+ wind: ["≈", "w"],
63
+ droplets: ["❍", "d"],
64
+ thermometer: ["▴", "T"],
65
+ umbrella: ["☂", "u"],
66
+ };
67
+ /** The icon glyph for `name` (the ASCII fallback when the terminal
68
+ * lacks Unicode support — callers pass `!ctx.unicode`). */
69
+ export function iconGlyph(name, ascii) {
70
+ const pair = GLYPHS[name];
71
+ if (pair === undefined)
72
+ return ascii ? "?" : name;
73
+ return ascii ? pair[1] : pair[0];
74
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The `image` block (UI-blocks spec §11.3): pi-tui's `Image` when
3
+ * `getCapabilities().images` is kitty/iterm2 and the bytes arrived;
4
+ * `[image: <alt>]` otherwise (no protocol, a failed or refused fetch,
5
+ * a missing alt). Every non-PNG source is requested with `as=png`
6
+ * (the server-side conversion, ruling 17); TMUX handling is pi-tui's
7
+ * own (`terminal-image.js` turns images off under TMUX unless
8
+ * `PI_IMAGE_PROTOCOL` overrides) — this block adds no detection.
9
+ *
10
+ * The byte-fetch is an injectable seam ({@link FetchImageBytes}): the
11
+ * surface transport that resolves `asset:`/URL sources lands in
12
+ * Task 4; tests fake it. Images size to the block's width in cells,
13
+ * capped at 20 rows.
14
+ */
15
+ import type { Component } from "@earendil-works/pi-tui";
16
+ import type { ImageBlock } from "@sigrun/shared/blocks";
17
+ import { type BlockCtx } from "./context.js";
18
+ /**
19
+ * Fetch the bytes for one image source. `asPng` asks the transport
20
+ * for the server-side PNG conversion (ruling 17); rejection or a
21
+ * throw renders the alt fallback.
22
+ */
23
+ export type FetchImageBytes = (src: string, opts: {
24
+ asPng: boolean;
25
+ }) => Promise<Uint8Array>;
26
+ /** The extra options {@link renderImageBlock} takes (the seams). */
27
+ export interface ImageOptions {
28
+ /** The byte transport; absent renders the alt (nothing to fetch with). */
29
+ fetchImage?: FetchImageBytes;
30
+ }
31
+ /** The fallback line for `alt` (`[image]` when the alt is missing). */
32
+ export declare function imageFallbackLine(alt: string): string;
33
+ /** `image`: fetch-then-render, with the alt as the standing fallback. */
34
+ export declare class ImageComponent implements Component {
35
+ #private;
36
+ constructor(block: ImageBlock, ctx: BlockCtx, options?: ImageOptions);
37
+ render(width: number): string[];
38
+ invalidate(): void;
39
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The `image` block (UI-blocks spec §11.3): pi-tui's `Image` when
3
+ * `getCapabilities().images` is kitty/iterm2 and the bytes arrived;
4
+ * `[image: <alt>]` otherwise (no protocol, a failed or refused fetch,
5
+ * a missing alt). Every non-PNG source is requested with `as=png`
6
+ * (the server-side conversion, ruling 17); TMUX handling is pi-tui's
7
+ * own (`terminal-image.js` turns images off under TMUX unless
8
+ * `PI_IMAGE_PROTOCOL` overrides) — this block adds no detection.
9
+ *
10
+ * The byte-fetch is an injectable seam ({@link FetchImageBytes}): the
11
+ * surface transport that resolves `asset:`/URL sources lands in
12
+ * Task 4; tests fake it. Images size to the block's width in cells,
13
+ * capped at 20 rows.
14
+ */
15
+ import { getCapabilities, Image } from "@earendil-works/pi-tui";
16
+ import { bindText } from "./context.js";
17
+ import { truncate } from "./width.js";
18
+ /** The row cap (spec §11.3). */
19
+ const MAX_IMAGE_ROWS = 20;
20
+ /** Does the source name a PNG already (extension or data URL)? */
21
+ function isPng(src) {
22
+ if (src.startsWith("data:"))
23
+ return src.startsWith("data:image/png");
24
+ return /\.png($|[?#])/i.test(src);
25
+ }
26
+ /** The fallback line for `alt` (`[image]` when the alt is missing). */
27
+ export function imageFallbackLine(alt) {
28
+ return alt === "" ? "[image]" : `[image: ${alt}]`;
29
+ }
30
+ /** `image`: fetch-then-render, with the alt as the standing fallback. */
31
+ export class ImageComponent {
32
+ #block;
33
+ #ctx;
34
+ #options;
35
+ /** The fetched component once the bytes are in (one fetch ever). */
36
+ #image;
37
+ /** "idle" not started; "pending" renders [] while the fetch runs. */
38
+ #state = "idle";
39
+ constructor(block, ctx, options = {}) {
40
+ this.#block = block;
41
+ this.#ctx = ctx;
42
+ this.#options = options;
43
+ }
44
+ render(width) {
45
+ const alt = bindText(this.#block.alt, this.#ctx);
46
+ const protocol = getCapabilities().images;
47
+ const src = bindText(this.#block.src, this.#ctx);
48
+ const fetch = this.#options.fetchImage;
49
+ if (protocol !== "kitty" && protocol !== "iterm2") {
50
+ return [truncate(imageFallbackLine(alt), width)];
51
+ }
52
+ if (fetch === undefined || src === "") {
53
+ return [truncate(imageFallbackLine(alt), width)];
54
+ }
55
+ if (this.#state === "idle") {
56
+ this.#state = "pending";
57
+ void fetch(src, { asPng: !isPng(src) })
58
+ .then((bytes) => {
59
+ let binary = "";
60
+ for (const byte of bytes)
61
+ binary += String.fromCharCode(byte);
62
+ this.#image = new Image(btoa(binary), "image/png", { fallbackColor: this.#ctx.theme.dim }, { maxWidthCells: width, maxHeightCells: MAX_IMAGE_ROWS });
63
+ })
64
+ .catch(() => {
65
+ this.#state = "failed";
66
+ });
67
+ }
68
+ if (this.#state === "failed") {
69
+ return [truncate(imageFallbackLine(alt), width)];
70
+ }
71
+ if (this.#image === undefined) {
72
+ // Bytes pending: render nothing until the fetch resolves
73
+ // (the host re-renders; a transient empty beats a flash of
74
+ // the fallback line).
75
+ return [];
76
+ }
77
+ return this.#image.render(width);
78
+ }
79
+ invalidate() {
80
+ this.#image?.invalidate();
81
+ }
82
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The TUI's interaction SurfaceTransport (UI-blocks U6 Task 3), the port
3
+ * of the webui's `interactionTransport.ts`: an interaction record IS the
4
+ * surface — `load` returns the record's block tree (placement
5
+ * `interaction`: a root `form` whose submit is the answer) with the
6
+ * form's bind-path document as the initial data, and `act` on the submit
7
+ * POSTs the payload as the answer `{values}` to the ONE generic answer
8
+ * route (ruling 20).
9
+ *
10
+ * No polling: the record never changes while rendered here (a settle
11
+ * lands via the prompter's list refetch, which drops the record from the
12
+ * pending set), so `onChanged` is inert and `fetchData` echoes the load
13
+ * data. `upload`/`image` reject: the interaction block set carries no
14
+ * data-backed or upload blocks (§5.7's field set).
15
+ */
16
+ import type { JsonObject, SurfaceTransport, Tree } from "@sigrun/shared/blocks";
17
+ import type { SigrunClient } from "../api.js";
18
+ export interface InteractionTransportOptions {
19
+ readonly client: SigrunClient;
20
+ readonly conversationId: number;
21
+ readonly callId: string;
22
+ /** The record's block-tree body (placement `interaction`). */
23
+ readonly tree: Tree;
24
+ /** The form's bind document (`/values` → `{values}`); a settled
25
+ * record passes its stored answer so the read-only form renders the
26
+ * values it was answered with. */
27
+ readonly data: JsonObject;
28
+ /** Settled records render read-only (spec §6.5). */
29
+ readonly access: "read" | "write";
30
+ /** The root form's submit action name (`onSubmit ?? "submit"`) — the
31
+ * ONLY action that answers the record; every other action (a
32
+ * callout button, a live-mode edit dispatch) is ignored. */
33
+ readonly submitAction: string;
34
+ /** An accepted answer's follow-through (the prompter's refetch). */
35
+ readonly onAnswered?: () => void;
36
+ /** A 404 (not_pending — the record settled under the user's fingers)
37
+ * or a transport failure's follow-through; the original error rides
38
+ * along so the host's error describer keeps its status formatting. */
39
+ readonly onSettledElsewhere?: (message: string, cause: unknown) => void;
40
+ }
41
+ /** The record-as-surface transport. */
42
+ export declare function createInteractionTransport(options: InteractionTransportOptions): SurfaceTransport;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The TUI's interaction SurfaceTransport (UI-blocks U6 Task 3), the port
3
+ * of the webui's `interactionTransport.ts`: an interaction record IS the
4
+ * surface — `load` returns the record's block tree (placement
5
+ * `interaction`: a root `form` whose submit is the answer) with the
6
+ * form's bind-path document as the initial data, and `act` on the submit
7
+ * POSTs the payload as the answer `{values}` to the ONE generic answer
8
+ * route (ruling 20).
9
+ *
10
+ * No polling: the record never changes while rendered here (a settle
11
+ * lands via the prompter's list refetch, which drops the record from the
12
+ * pending set), so `onChanged` is inert and `fetchData` echoes the load
13
+ * data. `upload`/`image` reject: the interaction block set carries no
14
+ * data-backed or upload blocks (§5.7's field set).
15
+ */
16
+ /** The record-as-surface transport. */
17
+ export function createInteractionTransport(options) {
18
+ let answered = false;
19
+ return {
20
+ load() {
21
+ return Promise.resolve({
22
+ tree: options.tree,
23
+ data: options.data,
24
+ rev: 0,
25
+ treeRev: 0,
26
+ access: options.access,
27
+ });
28
+ },
29
+ fetchData() {
30
+ return Promise.resolve({ rev: 0, treeRev: 0, data: options.data });
31
+ },
32
+ async act(body) {
33
+ // Only the root form's submit is the answer (Review fix): a
34
+ // callout button or a live-mode edit dispatch must never POST
35
+ // `{values}` — ignore it (an ok result keeps the form happy).
36
+ // The gate checks the action KIND too (fix round 2): a callout
37
+ // itemAction or a live-mode edit NAMED "submit" still isn't
38
+ // one.
39
+ if (body.type !== "submit" || body.action !== options.submitAction) {
40
+ return { ok: true, rev: 0 };
41
+ }
42
+ // The form's submit payload is `{values}` (spec §5.6) — the
43
+ // answer shape verbatim. A double-submit is a channel bug; the
44
+ // route would 404 — guard anyway.
45
+ if (answered) {
46
+ return { ok: false, error: { message: "already answered" } };
47
+ }
48
+ const payload = body.payload;
49
+ const values = typeof payload === "object" &&
50
+ payload !== null &&
51
+ !Array.isArray(payload) &&
52
+ typeof payload.values === "object" &&
53
+ payload.values !== null &&
54
+ !Array.isArray(payload.values)
55
+ ? payload.values
56
+ : {};
57
+ try {
58
+ await options.client.answerInteraction(options.conversationId, options.callId, { values });
59
+ }
60
+ catch (cause) {
61
+ const message = cause instanceof Error ? cause.message : String(cause);
62
+ options.onSettledElsewhere?.(message, cause);
63
+ return { ok: false, error: { message } };
64
+ }
65
+ answered = true;
66
+ options.onAnswered?.();
67
+ return { ok: true, rev: 1 };
68
+ },
69
+ upload(_body) {
70
+ return Promise.reject(new Error("interaction surfaces do not accept uploads"));
71
+ },
72
+ image() {
73
+ return Promise.reject(new Error("interaction surfaces have no image route"));
74
+ },
75
+ onChanged(_listener) {
76
+ return () => { };
77
+ },
78
+ };
79
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The layout blocks (UI-blocks spec §5.2): `stack`, `columns`, `grid`,
3
+ * `section` as pi-tui components.
4
+ *
5
+ * Columns/grid honour the compact fallbacks (spec §12.3): `columns`
6
+ * stack when the minimums don't fit; `grid` runs
7
+ * `max(1, floor(width / minItemWidth))` columns so a compact pane gets
8
+ * one. Every line every block emits is clamped to the pane
9
+ * ({@link truncate} — Review Focus 2).
10
+ */
11
+ import type { Component } from "@earendil-works/pi-tui";
12
+ import type { ColumnsBlock, GridBlock, SectionBlock, StackBlock } from "@sigrun/shared/blocks";
13
+ import { type BlockCtx } from "./context.js";
14
+ /**
15
+ * A component from a width-aware line builder. Each produced line is
16
+ * clamped to the render width (the invariant every block must keep).
17
+ */
18
+ export declare class Lines implements Component {
19
+ #private;
20
+ constructor(build: (width: number) => string[]);
21
+ render(width: number): string[];
22
+ invalidate(): void;
23
+ }
24
+ /** Render `children` vertically with `gap` blank lines between. */
25
+ export declare function renderStack(block: StackBlock, ctx: BlockCtx): Component;
26
+ /** Render children side by side, stacking when the minimums don't fit. */
27
+ export declare function renderColumns(block: ColumnsBlock, ctx: BlockCtx): Component;
28
+ /** Render children in as many columns as `minItemWidth` allows. */
29
+ export declare function renderGrid(block: GridBlock, ctx: BlockCtx): Component;
30
+ /** Render a titled, optionally toned group of children. */
31
+ export declare function renderSection(block: SectionBlock, ctx: BlockCtx): Component;