@sigrun/tui 0.0.0-stage → 0.1.1

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 (110) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +9 -2
  4. package/dist/actions.d.ts +119 -0
  5. package/dist/actions.js +12 -0
  6. package/dist/api.d.ts +152 -0
  7. package/dist/api.js +189 -0
  8. package/dist/blocks/braille.d.ts +41 -0
  9. package/dist/blocks/braille.js +131 -0
  10. package/dist/blocks/chart.d.ts +20 -0
  11. package/dist/blocks/chart.js +397 -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 +96 -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 +53 -0
  45. package/dist/blocks/form/form.js +703 -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 +104 -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/layout.d.ts +31 -0
  55. package/dist/blocks/layout.js +155 -0
  56. package/dist/blocks/log.d.ts +24 -0
  57. package/dist/blocks/log.js +111 -0
  58. package/dist/blocks/meter.d.ts +16 -0
  59. package/dist/blocks/meter.js +50 -0
  60. package/dist/blocks/page.d.ts +54 -0
  61. package/dist/blocks/page.js +355 -0
  62. package/dist/blocks/render.d.ts +31 -0
  63. package/dist/blocks/render.js +145 -0
  64. package/dist/blocks/surface-view.d.ts +33 -0
  65. package/dist/blocks/surface-view.js +86 -0
  66. package/dist/blocks/surface.d.ts +75 -0
  67. package/dist/blocks/surface.js +468 -0
  68. package/dist/blocks/tones.d.ts +23 -0
  69. package/dist/blocks/tones.js +63 -0
  70. package/dist/blocks/treeview.d.ts +22 -0
  71. package/dist/blocks/treeview.js +121 -0
  72. package/dist/blocks/width.d.ts +32 -0
  73. package/dist/blocks/width.js +70 -0
  74. package/dist/clipboard.d.ts +20 -0
  75. package/dist/clipboard.js +19 -0
  76. package/dist/editor-launch.d.ts +34 -0
  77. package/dist/editor-launch.js +70 -0
  78. package/dist/keymaps/emacs.d.ts +18 -0
  79. package/dist/keymaps/emacs.js +172 -0
  80. package/dist/keymaps/standard.d.ts +17 -0
  81. package/dist/keymaps/standard.js +175 -0
  82. package/dist/keymaps/types.d.ts +43 -0
  83. package/dist/keymaps/types.js +28 -0
  84. package/dist/keymaps/vim.d.ts +48 -0
  85. package/dist/keymaps/vim.js +291 -0
  86. package/dist/launch.d.ts +149 -0
  87. package/dist/launch.js +446 -0
  88. package/dist/sse.d.ts +109 -0
  89. package/dist/sse.js +401 -0
  90. package/dist/theme.d.ts +51 -0
  91. package/dist/theme.js +119 -0
  92. package/dist/ui/ambient.d.ts +81 -0
  93. package/dist/ui/ambient.js +301 -0
  94. package/dist/ui/app.d.ts +245 -0
  95. package/dist/ui/app.js +1325 -0
  96. package/dist/ui/composer.d.ts +66 -0
  97. package/dist/ui/composer.js +125 -0
  98. package/dist/ui/conversation-pane.d.ts +123 -0
  99. package/dist/ui/conversation-pane.js +463 -0
  100. package/dist/ui/help.d.ts +15 -0
  101. package/dist/ui/help.js +80 -0
  102. package/dist/ui/interactions.d.ts +90 -0
  103. package/dist/ui/interactions.js +330 -0
  104. package/dist/ui/picker.d.ts +26 -0
  105. package/dist/ui/picker.js +130 -0
  106. package/dist/ui/sidebar.d.ts +99 -0
  107. package/dist/ui/sidebar.js +269 -0
  108. package/dist/ui/status-bar.d.ts +77 -0
  109. package/dist/ui/status-bar.js +146 -0
  110. package/package.json +49 -5
@@ -0,0 +1,253 @@
1
+ /**
2
+ * The simple content blocks (UI-blocks spec §5.3): `markdown`,
3
+ * `fields`, `stat`, `callout`, `diff`, `details`.
4
+ *
5
+ * - `markdown` rides pi-tui's `Markdown` (the same renderer as chat)
6
+ * with the TuiTheme-mapped markdown theme.
7
+ * - `fields` aligns labels by display width.
8
+ * - `stat` is `label: value unit` plus an optional caption line (its
9
+ * `sparkline` becomes a real drawing in Task 2).
10
+ * - `callout` leads with the tone marker (colour + the NO_COLOR
11
+ * fallback), its body renders as markdown, and its actions render as
12
+ * `[1] label` buttons with the confirm flow and the OSC 52 copy.
13
+ * - `diff` reuses the interaction diff renderer (blocks/diff.ts).
14
+ * - `details` is one collapsed JSON line (the TUI's existing details
15
+ * primitive; a later task may add an expandable pretty view).
16
+ */
17
+ import { Markdown, hyperlink, getCapabilities, visibleWidth } from "@earendil-works/pi-tui";
18
+ import { formatValue } from "@sigrun/shared/blocks";
19
+ import { bindRaw, bindText, bindTone } from "./context.js";
20
+ import { truncate } from "./width.js";
21
+ import { iconGlyph } from "./icons.js";
22
+ import { toneFg, toneMarker } from "./tones.js";
23
+ import { blockDiff, diffLines } from "./diff.js";
24
+ import { Lines } from "./layout.js";
25
+ import { copyToTerminal } from "../clipboard.js";
26
+ /** The markdown theme rides the TuiTheme's token colours. */
27
+ function markdownTheme(theme) {
28
+ return {
29
+ heading: (text) => theme.bold(theme.fg("accent")(text)),
30
+ link: theme.fg("accent"),
31
+ linkUrl: theme.dim,
32
+ code: theme.fg("code"),
33
+ codeBlock: theme.fg("code"),
34
+ codeBlockBorder: theme.fg("border"),
35
+ quote: theme.italic,
36
+ quoteBorder: theme.fg("border"),
37
+ hr: theme.fg("border"),
38
+ listBullet: theme.fg("accent"),
39
+ bold: theme.bold,
40
+ italic: theme.italic,
41
+ strikethrough: (text) => text,
42
+ underline: (text) => text,
43
+ };
44
+ }
45
+ /** `markdown`: the same renderer as chat. */
46
+ export function renderMarkdown(block, ctx) {
47
+ return new Markdown(bindText(block.text, ctx), 0, 0, markdownTheme(ctx.theme));
48
+ }
49
+ /** Read `fields.rows` into label/value/tone/icon rows (each slot binds). */
50
+ function fieldRows(block, ctx) {
51
+ const raw = bindRaw(block.rows, ctx);
52
+ if (!Array.isArray(raw))
53
+ return [];
54
+ const rows = [];
55
+ for (const entry of raw) {
56
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry))
57
+ continue;
58
+ const record = entry;
59
+ const label = bindText(record.label, ctx);
60
+ const value = formatValue((bindRaw(record.value, ctx) ?? null), undefined, ctx.locale);
61
+ const tone = bindRaw(record.tone, ctx);
62
+ const icon = bindRaw(record.icon, ctx);
63
+ rows.push({
64
+ label,
65
+ value,
66
+ tone: tone === undefined ? undefined : bindTone(tone, ctx),
67
+ icon: icon === undefined ? undefined : icon,
68
+ });
69
+ }
70
+ return rows;
71
+ }
72
+ /** `fields`: `label: value` rows, labels aligned by display width. */
73
+ export function renderFields(block, ctx) {
74
+ return new Lines(() => {
75
+ const rows = fieldRows(block, ctx);
76
+ const labelWidth = Math.max(0, ...rows.map((row) => visibleWidth(row.label)));
77
+ return rows.map((row) => {
78
+ const padded = row.label + " ".repeat(Math.max(0, labelWidth - visibleWidth(row.label)));
79
+ const text = row.tone === undefined ? row.value : toneFg(ctx.theme, row.tone)(row.value);
80
+ const glyph = row.icon === undefined ? "" : `${iconGlyph(row.icon, !ctx.unicode)} `;
81
+ return `${ctx.theme.dim(padded)} ${glyph}${text}`;
82
+ });
83
+ });
84
+ }
85
+ /** `stat`: `label: value unit` plus an optional caption line. */
86
+ export function renderStat(block, ctx) {
87
+ return new Lines((width) => {
88
+ const value = formatValue((bindRaw(block.value, ctx) ?? null), block.format, ctx.locale);
89
+ const unit = bindText(block.unit, ctx);
90
+ const text = unit === "" ? value : `${value} ${unit}`;
91
+ const tone = bindRaw(block.tone, ctx);
92
+ const toneName = tone === undefined ? undefined : bindTone(tone, ctx);
93
+ const icon = bindRaw(block.icon, ctx);
94
+ const label = bindText(block.label, ctx);
95
+ const head = typeof icon === "string" ? `${iconGlyph(icon, !ctx.unicode)} ${label}` : label;
96
+ const painted = toneName === undefined ? text : toneFg(ctx.theme, toneName)(text);
97
+ const out = [truncate(`${ctx.theme.dim(`${head}:`)} ${painted}`, width)];
98
+ const caption = bindText(block.caption, ctx);
99
+ if (caption !== "")
100
+ out.push(ctx.theme.dim(caption));
101
+ return out;
102
+ });
103
+ }
104
+ /** `details`: one collapsed JSON line. */
105
+ export function renderDetails(block) {
106
+ return new Lines(() => [JSON.stringify(block.value)]);
107
+ }
108
+ /** `diff`: the interaction diff renderer (blocks/diff.ts). */
109
+ export function renderDiff(block) {
110
+ return new Lines(() => diffLines(blockDiff(block)));
111
+ }
112
+ /** The actions a block offers: read access keeps the view-only kinds
113
+ * (§6.5 — copy/open/navigate never edit or answer; twin of the webui's
114
+ * `isViewOnly`). */
115
+ export function visibleActions(actions, ctx) {
116
+ if (ctx.access === "read") {
117
+ return (actions ?? []).filter((button) => button.action.type === "copy" ||
118
+ button.action.type === "open" ||
119
+ button.action.type === "navigate");
120
+ }
121
+ return [...(actions ?? [])];
122
+ }
123
+ /** The URL schemes an `open` action may carry (§15: https/http/mailto). */
124
+ const OPEN_SCHEMES = new Set(["https", "http", "mailto"]);
125
+ /** Clean an `open` URL the way a browser would, then allowlist the scheme. */
126
+ function openUrl(url) {
127
+ const cleaned = url
128
+ .replace(/[\t\n\r]/g, "")
129
+ .replace(/[\u0000-\u0020]+$/, "")
130
+ .replace(/^[\u0000-\u0020]+/, "");
131
+ const scheme = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(cleaned)?.[1];
132
+ if (scheme === undefined)
133
+ return "";
134
+ return OPEN_SCHEMES.has(scheme.toLowerCase()) ? cleaned : "";
135
+ }
136
+ /** `open`: channel-local (§6.2 view-only) — print the URL as an OSC 8
137
+ * hyperlink when the terminal supports them, else as plain text. */
138
+ function runOpen(button, action, ctx) {
139
+ const url = openUrl(action.url);
140
+ if (url === "")
141
+ return; // disallowed scheme: nothing opens, nothing dispatches
142
+ const terminal = ctx.terminal ?? { write: (data) => process.stdout.write(data) };
143
+ const label = bindText(button.label, ctx);
144
+ const shown = getCapabilities().hyperlinks
145
+ ? hyperlink(label, url)
146
+ : `${label} — ${url}`;
147
+ terminal.write(`${shown}\n`);
148
+ }
149
+ /** Run a button's action (copy/open resolve locally; the rest dispatch). */
150
+ function runAction(button, ctx) {
151
+ const action = button.action;
152
+ if (action.type === "copy") {
153
+ const raw = bindRaw(action.text, ctx);
154
+ const text = typeof raw === "string" ? raw : "";
155
+ copyToTerminal(ctx.terminal ?? { write: (data) => process.stdout.write(data) }, text);
156
+ return;
157
+ }
158
+ if (action.type === "open") {
159
+ runOpen(button, action, ctx);
160
+ return;
161
+ }
162
+ ctx.dispatch(action, null);
163
+ }
164
+ /**
165
+ * `callout`: tone-marked title, wrapped markdown body, action buttons.
166
+ * `handleInput` picks a button by digit; a `confirm` action gates its
167
+ * dispatch behind a `y/N` prompt (anything but `y`/`Y` cancels and
168
+ * dispatches nothing).
169
+ */
170
+ export class CalloutComponent {
171
+ #block;
172
+ #ctx;
173
+ #confirm;
174
+ constructor(block, ctx) {
175
+ this.#block = block;
176
+ this.#ctx = ctx;
177
+ }
178
+ render(width) {
179
+ const theme = this.#ctx.theme;
180
+ const tone = bindTone(this.#block.tone, this.#ctx);
181
+ const title = bindText(this.#block.title, this.#ctx);
182
+ const lines = [
183
+ truncate(`${toneFg(theme, tone)(toneMarker(tone, !this.#ctx.unicode))} ${title}`, width),
184
+ ];
185
+ const body = bindText(this.#block.body, this.#ctx);
186
+ if (body !== "") {
187
+ // The body is markdown (§5.3); ride the same renderer as chat.
188
+ lines.push(...new Markdown(body, 0, 0, markdownTheme(theme)).render(width));
189
+ }
190
+ if (this.#confirm !== undefined) {
191
+ const { confirm, action } = this.#confirm;
192
+ const label = confirm.confirmLabel ??
193
+ (action.type === "restart" ? "Restart" : "Confirm");
194
+ lines.push("");
195
+ lines.push(truncate(`${toneFg(theme, confirm.tone ?? "warn")(toneMarker(confirm.tone ?? "warn", !this.#ctx.unicode))} ${confirm.title}`, width));
196
+ if (confirm.body !== undefined && confirm.body !== "") {
197
+ lines.push(...new Markdown(confirm.body, 0, 0, markdownTheme(theme)).render(width));
198
+ }
199
+ lines.push(truncate(`${label}? y/N`, width));
200
+ return lines;
201
+ }
202
+ const actions = visibleActions(this.#block.actions, this.#ctx);
203
+ if (actions.length > 0) {
204
+ lines.push("");
205
+ lines.push(truncate(actions
206
+ .map((button, index) => `[${index + 1}] ${bindText(button.label, this.#ctx)}`)
207
+ .join(" "), width));
208
+ }
209
+ return lines;
210
+ }
211
+ handleInput(data) {
212
+ if (this.#confirm !== undefined) {
213
+ const { action } = this.#confirm;
214
+ this.#confirm = undefined;
215
+ if (data === "y" || data === "Y")
216
+ this.#ctx.dispatch(action, null);
217
+ return;
218
+ }
219
+ const actions = visibleActions(this.#block.actions, this.#ctx);
220
+ const button = actions[Number(data) - 1];
221
+ if (button === undefined)
222
+ return;
223
+ const action = button.action;
224
+ if ((action.type === "submit" || action.type === "restart") &&
225
+ action.confirm !== undefined) {
226
+ this.#confirm = { confirm: action.confirm, action };
227
+ return;
228
+ }
229
+ runAction(button, this.#ctx);
230
+ }
231
+ invalidate() {
232
+ // Stateless render; the confirm gate is the only mutable state.
233
+ }
234
+ }
235
+ /** Render a Task-1 content block by type; the caller routes unknowns. */
236
+ export function renderContent(block, ctx) {
237
+ switch (block.type) {
238
+ case "markdown":
239
+ return renderMarkdown(block, ctx);
240
+ case "fields":
241
+ return renderFields(block, ctx);
242
+ case "stat":
243
+ return renderStat(block, ctx);
244
+ case "details":
245
+ return renderDetails(block);
246
+ case "diff":
247
+ return renderDiff(block);
248
+ case "callout":
249
+ return new CalloutComponent(block, ctx);
250
+ default:
251
+ return undefined;
252
+ }
253
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The block render context (UI-blocks U2 Task 1): everything a block
3
+ * component reads while rendering — the surface's data, the theme, the
4
+ * locale, the principal's access, the dispatch seam and the channel's
5
+ * view state — plus the binding/text resolution helpers every block
6
+ * renderer shares.
7
+ *
8
+ * `view` is the channel-owned view-state branch (spec §6.2): sort,
9
+ * filter, expansion, selection. Task 5 (forms) widened the seam with
10
+ * `setView` (the `/view/` writer a live form bound under `/view/` calls
11
+ * so sibling blocks reading `/view/...` bindings update — mirroring
12
+ * U1's `SurfaceActionsCtx.setView`) and `serverErrors` (a rejected
13
+ * action's `{error: {field}}` surfaces under that field, §5.6).
14
+ * `bindRaw` routes `/view/`-rooted pointers to `view.values` so EVERY
15
+ * block (not only the writing form) reads the branch.
16
+ */
17
+ import type { Action, Binding, JsonObject, JsonValue, Tone } from "@sigrun/shared/blocks";
18
+ import type { TuiTheme } from "../theme.js";
19
+ /** The channel-owned view state a render reads (spec §6.2). */
20
+ export interface ViewState {
21
+ /** Values written by view-only actions (live forms bound `/view/`). */
22
+ values: JsonObject;
23
+ }
24
+ /** The render context {@link renderBlock} threads to every component. */
25
+ export interface BlockCtx {
26
+ /** The surface's confirmed data model (bindings resolve against it). */
27
+ data: JsonObject;
28
+ /** The current item, inside a collection's card template (`./` pointers). */
29
+ item?: JsonValue;
30
+ theme: TuiTheme;
31
+ locale: string;
32
+ /**
33
+ * Time zone for `Intl` time formats (chart axes, log timestamps);
34
+ * absent means the runtime's local zone. Tests pass `UTC` for
35
+ * byte-stable output (`formatValue`'s fourth argument).
36
+ */
37
+ timeZone?: string;
38
+ access: "read" | "write";
39
+ /**
40
+ * Whether the terminal renders Unicode glyphs (spec §10.2): false
41
+ * selects the ASCII fallbacks — `icons.ts` glyphs and `tones.ts`
42
+ * markers — so an ASCII terminal never shows mojibake.
43
+ */
44
+ unicode: boolean;
45
+ /** Route an action to the surface (view-only ones never reach this). */
46
+ dispatch(action: Action, payload: JsonValue): void;
47
+ view: ViewState;
48
+ /**
49
+ * Write the channel-owned `/view/` branch (spec §6.2): `path` is a
50
+ * JSON pointer under the branch (no leading `/view/` — the writer
51
+ * roots at the branch, as U1's `setView` does). A live form bound
52
+ * under `/view/` calls this after its 300 ms debounce; the write
53
+ * never reaches the server.
54
+ */
55
+ setView?(path: string, value: JsonValue): void;
56
+ /**
57
+ * Server-side field errors, keyed by field name (spec §5.6: a
58
+ * rejected action's `{error: {field}}` surfaces under that field).
59
+ */
60
+ readonly serverErrors?: Readonly<Record<string, string>>;
61
+ /** The host's edit-in-$EDITOR seam (longtext with `syntax: code`;
62
+ * spec §5.6). The form calls it with the current text; it returns
63
+ * the new text (the TUI suspends while the editor owns the
64
+ * terminal, as the composer's vim flow does). */
65
+ openEditor?(text: string): {
66
+ ok: true;
67
+ text: string;
68
+ } | {
69
+ ok: false;
70
+ reason: string;
71
+ };
72
+ /** The surface's upload seam (§7.2): a file field hands the bytes
73
+ * here after dispatching `onUpload`. Optional — without it the
74
+ * bytes ride the dispatch payload as base64. */
75
+ uploadBytes?(field: string, name: string, bytes: Uint8Array): void;
76
+ /** The terminal copy writes to (OSC 52); defaults to process.stdout. */
77
+ terminal?: {
78
+ write(data: string): void;
79
+ };
80
+ }
81
+ /** Is `value` a binding (spec §4.2: `{ $: pointer }`)? */
82
+ export declare function isBinding(value: unknown): value is Binding;
83
+ /** Resolve a bindable of any shape to its raw JSON value (untyped).
84
+ * A `/view/`-rooted pointer resolves against the channel-owned view
85
+ * branch (spec §6.2 — the sibling-read half of the setView seam: a
86
+ * block bound `{$: "/view/..."}` sees what a view-bound live form
87
+ * wrote); everything else resolves against ctx.data. */
88
+ export declare function bindRaw(value: unknown, ctx: BlockCtx): JsonValue | undefined;
89
+ /** Resolve a bindable to text: empty string for missing or non-string values. */
90
+ export declare function bindText(value: unknown, ctx: BlockCtx): string;
91
+ /** Resolve a bindable tone: `neutral` when absent or not a tone name. */
92
+ export declare function bindTone(value: unknown, ctx: BlockCtx): Tone;
93
+ /** True when a block's `visibleAt` hides it at the rendered width class. */
94
+ export declare function hiddenAtCompact(block: {
95
+ readonly visibleAt?: string;
96
+ }, width: number): boolean;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The block render context (UI-blocks U2 Task 1): everything a block
3
+ * component reads while rendering — the surface's data, the theme, the
4
+ * locale, the principal's access, the dispatch seam and the channel's
5
+ * view state — plus the binding/text resolution helpers every block
6
+ * renderer shares.
7
+ *
8
+ * `view` is the channel-owned view-state branch (spec §6.2): sort,
9
+ * filter, expansion, selection. Task 5 (forms) widened the seam with
10
+ * `setView` (the `/view/` writer a live form bound under `/view/` calls
11
+ * so sibling blocks reading `/view/...` bindings update — mirroring
12
+ * U1's `SurfaceActionsCtx.setView`) and `serverErrors` (a rejected
13
+ * action's `{error: {field}}` surfaces under that field, §5.6).
14
+ * `bindRaw` routes `/view/`-rooted pointers to `view.values` so EVERY
15
+ * block (not only the writing form) reads the branch.
16
+ */
17
+ import { resolvePointer } from "@sigrun/shared/blocks";
18
+ /** Is `value` a binding (spec §4.2: `{ $: pointer }`)? */
19
+ export function isBinding(value) {
20
+ return (typeof value === "object" &&
21
+ value !== null &&
22
+ typeof value.$ === "string");
23
+ }
24
+ /** The tone names (the `bindTone` guard's set). */
25
+ const TONES = new Set([
26
+ "neutral",
27
+ "accent",
28
+ "good",
29
+ "warn",
30
+ "bad",
31
+ "info",
32
+ ]);
33
+ /** Resolve a bindable of any shape to its raw JSON value (untyped).
34
+ * A `/view/`-rooted pointer resolves against the channel-owned view
35
+ * branch (spec §6.2 — the sibling-read half of the setView seam: a
36
+ * block bound `{$: "/view/..."}` sees what a view-bound live form
37
+ * wrote); everything else resolves against ctx.data. */
38
+ export function bindRaw(value, ctx) {
39
+ if (!isBinding(value))
40
+ return value;
41
+ if (value.$.startsWith("/view/") || value.$ === "/view") {
42
+ const pointer = value.$.replace(/^\/view\/?/, "/");
43
+ const resolved = resolvePointer(ctx.view.values, pointer);
44
+ if (resolved !== undefined)
45
+ return resolved;
46
+ return undefined;
47
+ }
48
+ return resolvePointer(ctx.data, value.$, ctx.item);
49
+ }
50
+ /** Resolve a bindable to text: empty string for missing or non-string values. */
51
+ export function bindText(value, ctx) {
52
+ const resolved = bindRaw(value, ctx);
53
+ return typeof resolved === "string" ? resolved : "";
54
+ }
55
+ /** Resolve a bindable tone: `neutral` when absent or not a tone name. */
56
+ export function bindTone(value, ctx) {
57
+ const resolved = bindRaw(value, ctx);
58
+ return TONES.has(typeof resolved === "string" ? resolved : "")
59
+ ? resolved
60
+ : "neutral";
61
+ }
62
+ /** True when a block's `visibleAt` hides it at the rendered width class. */
63
+ export function hiddenAtCompact(block, width) {
64
+ return block.visibleAt === "atLeast:regular" && width < 80;
65
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The interaction diff renderer, extracted (UI-blocks U2 Task 1): the
3
+ * prompt strip's before/after lines — `[diff] path` then `- `/`+ `
4
+ * prefixed lines — reused VERBATIM by the `diff` block so chat diffs
5
+ * and interaction diffs render identically.
6
+ *
7
+ * The extraction is byte-identical to the lines
8
+ * `InteractionPrompter.render` produced inline (tui/src/ui/interactions.ts);
9
+ * that component now calls {@link diffLines}.
10
+ */
11
+ import type { DiffBlock } from "@sigrun/shared/blocks";
12
+ /** The shape the interaction seam's diff primitive carries. */
13
+ export interface InteractionDiff {
14
+ before: string;
15
+ after: string;
16
+ path?: string;
17
+ }
18
+ /** Render a diff as `[diff] path` plus `- `/`+ ` lines. */
19
+ export declare function diffLines(diff: InteractionDiff): string[];
20
+ /** The `diff` block's diff (same wire fields). */
21
+ export declare function blockDiff(block: DiffBlock): InteractionDiff;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The interaction diff renderer, extracted (UI-blocks U2 Task 1): the
3
+ * prompt strip's before/after lines — `[diff] path` then `- `/`+ `
4
+ * prefixed lines — reused VERBATIM by the `diff` block so chat diffs
5
+ * and interaction diffs render identically.
6
+ *
7
+ * The extraction is byte-identical to the lines
8
+ * `InteractionPrompter.render` produced inline (tui/src/ui/interactions.ts);
9
+ * that component now calls {@link diffLines}.
10
+ */
11
+ /** Render a diff as `[diff] path` plus `- `/`+ ` lines. */
12
+ export function diffLines(diff) {
13
+ const lines = [
14
+ diff.path === undefined ? "[diff]" : `[diff] ${diff.path}`,
15
+ ];
16
+ for (const line of diff.before.split("\n"))
17
+ lines.push(`- ${line}`);
18
+ for (const line of diff.after.split("\n"))
19
+ lines.push(`+ ${line}`);
20
+ return lines;
21
+ }
22
+ /** The `diff` block's diff (same wire fields). */
23
+ export function blockDiff(block) {
24
+ return { before: block.before, after: block.after, path: block.path };
25
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * The TUI's extension-surface transport (UI-blocks U3 Task 5): implements
3
+ * the shared `SurfaceTransport` contract over the U3 routes (spec §7.2)
4
+ * as the TUI's remote client. One transport = one surface instance
5
+ * (slug + id + params); the wire `k` (hashed instance key) keys the
6
+ * change-event routing in {@link ExtSurfaceHub}.
7
+ *
8
+ * Like the webui twin, every fetch carries `X-Sigrun-Stream` once the
9
+ * launcher hands the stream id in (the watcher entry's stream);
10
+ * `image()` rejects until U7 ships the route. The channel owns
11
+ * refreshMs cadence, the keepalive, and visibility gating (U2).
12
+ */
13
+ import type { JsonObject, SurfaceTransport } from "@sigrun/shared/blocks";
14
+ export interface ExtSurfaceTransportOptions {
15
+ /** The extension slug (the `/v1/ext/<slug>/` prefix segment). */
16
+ readonly slug: string;
17
+ /** The surface id within the extension. */
18
+ readonly id: string;
19
+ /** The instance params; absent → `{}` (the route's default). */
20
+ readonly params?: JsonObject;
21
+ /** The current `/v1/events` stream id (the watcher entry's stream). */
22
+ readonly streamId?: string;
23
+ /** The server's origin (the launcher that constructs the client owns it). */
24
+ readonly baseUrl: string;
25
+ readonly user?: string;
26
+ readonly token?: string;
27
+ readonly fetch?: typeof globalThis.fetch;
28
+ }
29
+ /**
30
+ * The TUI's transport handle (U3 Task 5 fix round): the `SurfaceTransport`
31
+ * plus `setStreamId` — a `/v1/events` reconnect hands the OPEN view the
32
+ * new stream id without re-creating the transport (the next fetch
33
+ * carries it, re-registering the watcher entry, spec §7.3).
34
+ */
35
+ export interface ExtSurfaceTransportHandle {
36
+ readonly transport: SurfaceTransport;
37
+ /** Update the watcher stream id (a reconnect's hello); subsequent
38
+ * fetches carry the new id. */
39
+ setStreamId(streamId: string | undefined): void;
40
+ }
41
+ /**
42
+ * The wire `k` for one surface instance (spec §7.3), Node flavor:
43
+ * `sha256(slug + "\0" + id + "\0" + key)` hex. The TUI's transport
44
+ * computes it synchronously (the WebCrypto twin in `@sigrun/shared/
45
+ * blocks` is async — DOM-only).
46
+ */
47
+ export declare function surfaceKeyHash(slug: string, id: string, key: string): string;
48
+ /** The instance key for one transport's params (the default rule). */
49
+ export declare function transportKey(options: ExtSurfaceTransportOptions): string;
50
+ /** Create the TUI's extension-surface transport. */
51
+ export declare function createExtSurfaceTransport(options: ExtSurfaceTransportOptions): ExtSurfaceTransportHandle;
52
+ /**
53
+ * The change-event router (the launcher's dispatch seam): `attach`
54
+ * creates a transport whose `onChanged` subscribes into the hub under
55
+ * the instance's (slug, id, k); `dispatch` fans a `surface.changed`
56
+ * event out to the matching subscriptions only.
57
+ */
58
+ export declare class ExtSurfaceHub {
59
+ #private;
60
+ /** Create a transport whose `onChanged` subscribes into this hub. */
61
+ attach(options: ExtSurfaceTransportOptions, keyHash: string): ExtSurfaceTransportHandle;
62
+ /** Fire every subscription matching the event's (slug, id, k). */
63
+ dispatch(event: {
64
+ readonly slug: string;
65
+ readonly id: string;
66
+ readonly k: string;
67
+ readonly rev: number;
68
+ readonly treeRev: number;
69
+ }): void;
70
+ }