@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,145 @@
1
+ /**
2
+ * The renderer's front door (UI-blocks U2 Task 1): one pi-tui
3
+ * component per block, dispatched on `block.type`. Layout blocks come
4
+ * from `layout.ts`, the simple content blocks from `content.ts`; later
5
+ * tasks slot their blocks into the same dispatch.
6
+ *
7
+ * - `visibleAt` (spec §12.2) gates here so every block inherits it:
8
+ * `atLeast:regular` renders nothing in a compact pane. An unknown
9
+ * condition fails visible, matching §4.4's lean (a newer spec's
10
+ * blocks stay on screen).
11
+ * - A type this client can't render yet (spec §4.4) shows the block's
12
+ * `fallback` text plus "Not supported in this client" — never
13
+ * dropped silently.
14
+ */
15
+ import { hiddenAtCompact } from "./context.js";
16
+ import { Lines, renderColumns, renderGrid, renderSection, renderStack } from "./layout.js";
17
+ import { renderContent } from "./content.js";
18
+ import { renderChart } from "./chart.js";
19
+ import { renderMeter } from "./meter.js";
20
+ import { LogComponent } from "./log.js";
21
+ import { TreeComponent } from "./treeview.js";
22
+ import { ImageComponent } from "./image.js";
23
+ import { renderCollection } from "./collection/collection.js";
24
+ import { FormComponent } from "./form/form.js";
25
+ import { PageComponent } from "./page.js";
26
+ import { wrapPlain } from "./width.js";
27
+ // The renderer's public surface re-exports its seam types and the width
28
+ // classes so callers have ONE import site (`render.js`).
29
+ export { widthClassOf } from "./width.js";
30
+ /** The fallback notice (spec §4.4's exact words). */
31
+ export const NOT_SUPPORTED = "Not supported in this client";
32
+ /** A block this client can't render: its fallback text plus the notice. */
33
+ function unsupported(block, ctx) {
34
+ return new Lines((width) => {
35
+ const lines = [];
36
+ const fallback = typeof block.fallback === "string" ? block.fallback : "";
37
+ if (fallback !== "")
38
+ lines.push(...wrapPlain(fallback, width));
39
+ lines.push(ctx.theme.dim(NOT_SUPPORTED));
40
+ return lines;
41
+ });
42
+ }
43
+ /**
44
+ * `visibleAt` as a width-aware wrapper: the same component answers
45
+ * every pane width, so a resize re-evaluates the gate. Input handling
46
+ * (button picks, the confirm gate) forwards to the inner component.
47
+ */
48
+ class WidthGated {
49
+ #block;
50
+ #inner;
51
+ constructor(block, inner) {
52
+ this.#block = block;
53
+ this.#inner = inner;
54
+ }
55
+ render(width) {
56
+ if (hiddenAtCompact(this.#block, width))
57
+ return [];
58
+ return this.#inner.render(width);
59
+ }
60
+ handleInput(data) {
61
+ // Input forwards to the inner component. Width cannot gate it here
62
+ // (a component learns widths only while rendering); the host only
63
+ // routes keys to components it shows, which is where hiding lands.
64
+ this.#inner.handleInput?.(data);
65
+ }
66
+ invalidate() {
67
+ this.#inner.invalidate();
68
+ }
69
+ /** Dispose forwards too (a form's pending live debounce must not
70
+ * outlive its surface host). */
71
+ dispose() {
72
+ this.#inner.dispose?.();
73
+ }
74
+ /** Form introspection forwards (tests and hosts read the wrapped
75
+ * form's values/focus — the wrapper is transparent). */
76
+ get values() {
77
+ return this.#inner instanceof FormComponent ? this.#inner.values : undefined;
78
+ }
79
+ get focusIndex() {
80
+ return this.#inner instanceof FormComponent ? this.#inner.focusIndex : undefined;
81
+ }
82
+ get step() {
83
+ return this.#inner instanceof FormComponent ? this.#inner.step : undefined;
84
+ }
85
+ get submittedOnce() {
86
+ return this.#inner instanceof FormComponent ? this.#inner.submittedOnce : undefined;
87
+ }
88
+ /** Page introspection forwards (the wrapper is transparent for the
89
+ * page shell's overlay/detail state too). */
90
+ get hasOverlay() {
91
+ return this.#inner instanceof PageComponent ? this.#inner.hasOverlay : undefined;
92
+ }
93
+ get detailPushed() {
94
+ return this.#inner instanceof PageComponent ? this.#inner.detailPushed : undefined;
95
+ }
96
+ get sidebarOpen() {
97
+ return this.#inner instanceof PageComponent ? this.#inner.sidebarOpen : undefined;
98
+ }
99
+ get menuKind() {
100
+ return this.#inner instanceof PageComponent ? this.#inner.menuKind : undefined;
101
+ }
102
+ setSideBySide(sideBySide) {
103
+ if (this.#inner instanceof PageComponent)
104
+ this.#inner.setSideBySide(sideBySide);
105
+ }
106
+ }
107
+ /** The type dispatch; unknown types answer `undefined` from content. */
108
+ function dispatch(block, ctx, options) {
109
+ switch (block.type) {
110
+ case "stack":
111
+ return renderStack(block, ctx);
112
+ case "columns":
113
+ return renderColumns(block, ctx);
114
+ case "grid":
115
+ return renderGrid(block, ctx);
116
+ case "section":
117
+ return renderSection(block, ctx);
118
+ case "chart":
119
+ return renderChart(block, ctx);
120
+ case "meter":
121
+ return renderMeter(block, ctx);
122
+ case "log":
123
+ return new LogComponent(block, ctx);
124
+ case "tree":
125
+ return new TreeComponent(block, ctx);
126
+ case "image":
127
+ return new ImageComponent(block, ctx, options);
128
+ case "collection": {
129
+ const component = renderCollection(block, ctx);
130
+ return component ?? unsupported(block, ctx);
131
+ }
132
+ case "form":
133
+ return new FormComponent(block, ctx, {
134
+ interaction: options.interaction === true,
135
+ });
136
+ case "page":
137
+ return new PageComponent(block, ctx, options);
138
+ default:
139
+ return renderContent(block, ctx) ?? unsupported(block, ctx);
140
+ }
141
+ }
142
+ /** Render any block as a pi-tui component (the renderer's dispatch). */
143
+ export function renderBlock(block, ctx, options = {}) {
144
+ return new WidthGated(block, dispatch(block, ctx, options));
145
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * SurfaceView (UI-blocks U2 Task 5, the port of U1's `SurfaceView.tsx`):
3
+ * the component host for a `SurfaceState` — loading, not-found, an
4
+ * error line with `r` to retry, ready. Ready builds the render context
5
+ * (the confirmed-plus-overlay data, the `/view/` branch, server field
6
+ * errors, dispatch and access) and renders the tree's root block.
7
+ *
8
+ * `handleInput` forwards to the rendered root component while ready;
9
+ * while error is showing, `r` retries the load (spec §12.3's fallback
10
+ * posture: an error never strands the surface).
11
+ */
12
+ import type { Component } from "@earendil-works/pi-tui";
13
+ import type { TuiTheme } from "../theme.js";
14
+ import { type RenderOptions } from "./render.js";
15
+ import type { SurfaceState } from "./surface.js";
16
+ /** The state lines (U1's wording, terminal-cased). */
17
+ export declare const SURFACE_LOADING = "loading\u2026";
18
+ export declare const SURFACE_NOT_FOUND = "surface not found";
19
+ export declare const SURFACE_ERROR = "something went wrong \u2014 r to retry";
20
+ export declare class SurfaceView implements Component {
21
+ #private;
22
+ constructor(surface: SurfaceState, opts: {
23
+ theme: TuiTheme;
24
+ locale?: string;
25
+ unicode: boolean;
26
+ options?: RenderOptions;
27
+ });
28
+ render(width: number): string[];
29
+ handleInput(data: string): void;
30
+ invalidate(): void;
31
+ /** The wrapped component (test introspection: forms expose state). */
32
+ get inner(): Component | undefined;
33
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * SurfaceView (UI-blocks U2 Task 5, the port of U1's `SurfaceView.tsx`):
3
+ * the component host for a `SurfaceState` — loading, not-found, an
4
+ * error line with `r` to retry, ready. Ready builds the render context
5
+ * (the confirmed-plus-overlay data, the `/view/` branch, server field
6
+ * errors, dispatch and access) and renders the tree's root block.
7
+ *
8
+ * `handleInput` forwards to the rendered root component while ready;
9
+ * while error is showing, `r` retries the load (spec §12.3's fallback
10
+ * posture: an error never strands the surface).
11
+ */
12
+ import { renderBlock } from "./render.js";
13
+ import { truncate } from "./width.js";
14
+ /** The state lines (U1's wording, terminal-cased). */
15
+ export const SURFACE_LOADING = "loading…";
16
+ export const SURFACE_NOT_FOUND = "surface not found";
17
+ export const SURFACE_ERROR = "something went wrong — r to retry";
18
+ export class SurfaceView {
19
+ #surface;
20
+ #theme;
21
+ #locale;
22
+ #unicode;
23
+ #options;
24
+ #component;
25
+ constructor(surface, opts) {
26
+ this.#surface = surface;
27
+ this.#theme = opts.theme;
28
+ this.#locale = opts.locale ?? "en";
29
+ this.#unicode = opts.unicode;
30
+ this.#options = opts.options ?? {};
31
+ }
32
+ /** The render context (rebuilt per view change — data updates flow
33
+ * through SurfaceState's change events; the host re-renders). */
34
+ #ctx() {
35
+ const view = this.#surface.view;
36
+ return {
37
+ data: view.data,
38
+ theme: this.#theme,
39
+ locale: this.#locale,
40
+ access: view.access === "write" ? "write" : "read",
41
+ unicode: this.#unicode,
42
+ dispatch: (action, payload) => this.#surface.dispatch(action, payload),
43
+ view: { values: this.#surface.viewValues },
44
+ setView: (path, value) => this.#surface.setView(path, value),
45
+ serverErrors: this.#surface.serverErrors,
46
+ };
47
+ }
48
+ render(width) {
49
+ const view = this.#surface.view;
50
+ if (view.status === "loading") {
51
+ return [truncate(this.#theme.dim(SURFACE_LOADING), width)];
52
+ }
53
+ if (view.status === "notFound") {
54
+ return [truncate(this.#theme.dim(SURFACE_NOT_FOUND), width)];
55
+ }
56
+ if (view.status === "error") {
57
+ return [truncate(this.#theme.fg("bad")(SURFACE_ERROR), width)];
58
+ }
59
+ if (view.tree === undefined)
60
+ return [];
61
+ if (this.#component === undefined) {
62
+ this.#component = renderBlock(view.tree.root, this.#ctx(), this.#options);
63
+ }
64
+ return this.#component.render(width);
65
+ }
66
+ handleInput(data) {
67
+ const view = this.#surface.view;
68
+ if (view.status === "error") {
69
+ if (data === "r")
70
+ void this.#surface.retry();
71
+ return;
72
+ }
73
+ if (view.status !== "ready")
74
+ return;
75
+ // The root component was built against the CURRENT ctx; rebuild
76
+ // statelessly so input handlers close over the live dispatch.
77
+ this.#component?.handleInput?.(data);
78
+ }
79
+ invalidate() {
80
+ this.#component?.invalidate();
81
+ }
82
+ /** The wrapped component (test introspection: forms expose state). */
83
+ get inner() {
84
+ return this.#component;
85
+ }
86
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * SurfaceState (UI-blocks U2 Task 4, spec §6.3): the confirmed-data plus
3
+ * optimistic-overlay model every TUI surface renders through — the TUI
4
+ * port of U1's `useSurface.ts`, with EXACTLY the same overlay rules:
5
+ *
6
+ * - **Confirmed data** changes ONLY from server data or ops (load,
7
+ * fetch, accepted actions carrying ops).
8
+ * - **The overlay** is a list of pending moves `{actionId, payload,
9
+ * target, untilRev?}`, keyed by `actionId`; the rendered `view` is
10
+ * confirmed data with the overlay applied on top.
11
+ * - `{ok: true, rev, ops}` → apply the ops to confirmed, drop the entry.
12
+ * - `{ok: true, rev}` (no ops) → keep the entry with `untilRev = rev`
13
+ * until a fetch at ≥ that rev arrives, then drop it — the item stays
14
+ * where the user dropped it with no snap-back flicker.
15
+ * - `{ok: false}` or the 10 s timeout → drop the entry and surface a
16
+ * NOTICE. U1 toasts; the TUI's notice is a line the app flashes in
17
+ * the status bar — `SurfaceState` collects `notices` and the host
18
+ * drains them through `consumeNotices` (the status-bar wiring is the
19
+ * app's seam, Task 6's surface host).
20
+ * - A `rev` gap (ops land behind the announced rev) or a `DataOpError`
21
+ * applying ops → a full refetch.
22
+ * - `access: "none"` → the not-found state (§6.5); a `treeRev` change
23
+ * → a full reload.
24
+ *
25
+ * The overlay locates a move in the data by scanning the loaded tree
26
+ * for the collection whose `onMove` handler name matches the action —
27
+ * identical to U1 (a move against an unknown handler still goes to the
28
+ * transport, unoverlaid).
29
+ */
30
+ import type { Action, JsonObject, JsonValue, SurfaceTransport, Tree } from "@sigrun/shared/blocks";
31
+ /** An action has 10 s to answer before it counts as a reject (§6.3). */
32
+ export declare const ACT_TIMEOUT_MS = 10000;
33
+ /** The notice cap: pending notices drop oldest-first past 50 (a flood
34
+ * of failures must not grow the list without bound; the host drains
35
+ * them through `consumeNotices`). */
36
+ export declare const NOTICE_CAP = 50;
37
+ /** The snapshot a render reads (the brief's `ViewState`). */
38
+ export interface SurfaceView {
39
+ readonly status: "loading" | "ready" | "notFound" | "error";
40
+ readonly tree?: Tree;
41
+ /** Confirmed data with the overlay applied — what blocks render. */
42
+ readonly data: JsonObject;
43
+ readonly rev: number;
44
+ readonly treeRev: number;
45
+ readonly access: "none" | "read" | "write";
46
+ /** Pending notice lines (drain via {@link SurfaceState.consumeNotices}). */
47
+ readonly notices: readonly string[];
48
+ }
49
+ /** The surface state container (class shape — the TUI has no hook layer). */
50
+ export declare class SurfaceState {
51
+ #private;
52
+ constructor(transport: SurfaceTransport);
53
+ /** The current snapshot: confirmed data with the overlay applied. */
54
+ get view(): SurfaceView;
55
+ /** Subscribe to state changes (returns the unsubscribe). */
56
+ onChange(cb: () => void): () => void;
57
+ /** Drain the pending notices (the host flashes them in the status bar). */
58
+ consumeNotices(): readonly string[];
59
+ /** The channel-owned `/view/` branch (spec §6.2) — a BlockCtx's
60
+ * `view.values` reads this object. */
61
+ get viewValues(): JsonObject;
62
+ /** The server field errors from the latest rejected action. */
63
+ get serverErrors(): Readonly<Record<string, string>>;
64
+ /** Write a pointer under the `/view/` branch (the form's live
65
+ * `/view/`-bound flush lands here; the write never dispatches). */
66
+ setView(path: string, value: JsonValue): void;
67
+ /** Reload after an error (SurfaceView's `r` key). */
68
+ retry(): Promise<void>;
69
+ /** Load the tree and initial data; subscribe to change events. */
70
+ load(): Promise<void>;
71
+ /** Stop listening and cancel every pending action timer. */
72
+ dispose(): void;
73
+ /** Dispatch an action to the surface; a move joins the overlay first. */
74
+ dispatch(action: Action, payload: JsonValue): void;
75
+ }