@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.
- package/CHANGELOG.md +10 -1
- package/dist/actions.d.ts +119 -0
- package/dist/actions.js +12 -0
- package/dist/api.d.ts +209 -0
- package/dist/api.js +282 -0
- package/dist/blocks/braille.d.ts +41 -0
- package/dist/blocks/braille.js +131 -0
- package/dist/blocks/chart.d.ts +20 -0
- package/dist/blocks/chart.js +397 -0
- package/dist/blocks/chat-transport.d.ts +44 -0
- package/dist/blocks/chat-transport.js +116 -0
- package/dist/blocks/collection/calendar.d.ts +28 -0
- package/dist/blocks/collection/calendar.js +232 -0
- package/dist/blocks/collection/card.d.ts +36 -0
- package/dist/blocks/collection/card.js +128 -0
- package/dist/blocks/collection/collection.d.ts +9 -0
- package/dist/blocks/collection/collection.js +26 -0
- package/dist/blocks/collection/grid.d.ts +18 -0
- package/dist/blocks/collection/grid.js +72 -0
- package/dist/blocks/collection/lanes.d.ts +32 -0
- package/dist/blocks/collection/lanes.js +320 -0
- package/dist/blocks/collection/list.d.ts +30 -0
- package/dist/blocks/collection/list.js +273 -0
- package/dist/blocks/collection/move.d.ts +67 -0
- package/dist/blocks/collection/move.js +96 -0
- package/dist/blocks/collection/shared.d.ts +87 -0
- package/dist/blocks/collection/shared.js +177 -0
- package/dist/blocks/collection/table.d.ts +26 -0
- package/dist/blocks/collection/table.js +221 -0
- package/dist/blocks/content.d.ts +48 -0
- package/dist/blocks/content.js +253 -0
- package/dist/blocks/context.d.ts +117 -0
- package/dist/blocks/context.js +65 -0
- package/dist/blocks/diff.d.ts +21 -0
- package/dist/blocks/diff.js +25 -0
- package/dist/blocks/ext-transport.d.ts +70 -0
- package/dist/blocks/ext-transport.js +165 -0
- package/dist/blocks/form/choice.d.ts +54 -0
- package/dist/blocks/form/choice.js +175 -0
- package/dist/blocks/form/fields.d.ts +56 -0
- package/dist/blocks/form/fields.js +136 -0
- package/dist/blocks/form/file.d.ts +61 -0
- package/dist/blocks/form/file.js +121 -0
- package/dist/blocks/form/form.d.ts +66 -0
- package/dist/blocks/form/form.js +764 -0
- package/dist/blocks/form/list.d.ts +37 -0
- package/dist/blocks/form/list.js +70 -0
- package/dist/blocks/form/values.d.ts +20 -0
- package/dist/blocks/form/values.js +106 -0
- package/dist/blocks/icons.d.ts +14 -0
- package/dist/blocks/icons.js +74 -0
- package/dist/blocks/image.d.ts +39 -0
- package/dist/blocks/image.js +82 -0
- package/dist/blocks/interaction-transport.d.ts +42 -0
- package/dist/blocks/interaction-transport.js +79 -0
- package/dist/blocks/layout.d.ts +31 -0
- package/dist/blocks/layout.js +155 -0
- package/dist/blocks/log.d.ts +24 -0
- package/dist/blocks/log.js +111 -0
- package/dist/blocks/meter.d.ts +16 -0
- package/dist/blocks/meter.js +50 -0
- package/dist/blocks/page.d.ts +54 -0
- package/dist/blocks/page.js +360 -0
- package/dist/blocks/render.d.ts +31 -0
- package/dist/blocks/render.js +163 -0
- package/dist/blocks/surface-view.d.ts +41 -0
- package/dist/blocks/surface-view.js +137 -0
- package/dist/blocks/surface.d.ts +80 -0
- package/dist/blocks/surface.js +506 -0
- package/dist/blocks/tones.d.ts +23 -0
- package/dist/blocks/tones.js +63 -0
- package/dist/blocks/treeview.d.ts +22 -0
- package/dist/blocks/treeview.js +121 -0
- package/dist/blocks/width.d.ts +32 -0
- package/dist/blocks/width.js +70 -0
- package/dist/clipboard.d.ts +20 -0
- package/dist/clipboard.js +19 -0
- package/dist/editor-launch.d.ts +34 -0
- package/dist/editor-launch.js +70 -0
- package/dist/keymaps/emacs.d.ts +18 -0
- package/dist/keymaps/emacs.js +172 -0
- package/dist/keymaps/standard.d.ts +17 -0
- package/dist/keymaps/standard.js +175 -0
- package/dist/keymaps/types.d.ts +43 -0
- package/dist/keymaps/types.js +28 -0
- package/dist/keymaps/vim.d.ts +48 -0
- package/dist/keymaps/vim.js +291 -0
- package/dist/launch.d.ts +159 -0
- package/dist/launch.js +478 -0
- package/dist/sse.d.ts +112 -0
- package/dist/sse.js +418 -0
- package/dist/theme.d.ts +51 -0
- package/dist/theme.js +119 -0
- package/dist/ui/ambient.d.ts +78 -0
- package/dist/ui/ambient.js +264 -0
- package/dist/ui/app.d.ts +319 -0
- package/dist/ui/app.js +1539 -0
- package/dist/ui/composer.d.ts +66 -0
- package/dist/ui/composer.js +133 -0
- package/dist/ui/conversation-pane.d.ts +143 -0
- package/dist/ui/conversation-pane.js +615 -0
- package/dist/ui/help.d.ts +15 -0
- package/dist/ui/help.js +80 -0
- package/dist/ui/interactions.d.ts +91 -0
- package/dist/ui/interactions.js +477 -0
- package/dist/ui/picker.d.ts +26 -0
- package/dist/ui/picker.js +130 -0
- package/dist/ui/settings.d.ts +68 -0
- package/dist/ui/settings.js +254 -0
- package/dist/ui/sidebar.d.ts +99 -0
- package/dist/ui/sidebar.js +269 -0
- package/dist/ui/status-bar.d.ts +77 -0
- package/dist/ui/status-bar.js +146 -0
- 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;
|