@valyrianjs/terminal 0.2.1 → 0.2.3

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 (89) hide show
  1. package/dist/ansi.d.ts.map +1 -1
  2. package/dist/ansi.js +177 -17
  3. package/dist/ansi.js.map +1 -1
  4. package/dist/events.d.ts.map +1 -1
  5. package/dist/events.js +4 -0
  6. package/dist/events.js.map +1 -1
  7. package/dist/frame-style.d.ts +7 -0
  8. package/dist/frame-style.d.ts.map +1 -0
  9. package/dist/frame-style.js +27 -0
  10. package/dist/frame-style.js.map +1 -0
  11. package/dist/layout.d.ts +5 -1
  12. package/dist/layout.d.ts.map +1 -1
  13. package/dist/layout.js +53 -23
  14. package/dist/layout.js.map +1 -1
  15. package/dist/mouse.d.ts.map +1 -1
  16. package/dist/mouse.js +8 -1
  17. package/dist/mouse.js.map +1 -1
  18. package/dist/render-internal.d.ts +10 -0
  19. package/dist/render-internal.d.ts.map +1 -0
  20. package/dist/render-internal.js +1295 -0
  21. package/dist/render-internal.js.map +1 -0
  22. package/dist/render.d.ts.map +1 -1
  23. package/dist/render.js +13 -1205
  24. package/dist/render.js.map +1 -1
  25. package/dist/session.d.ts.map +1 -1
  26. package/dist/session.js +78 -4
  27. package/dist/session.js.map +1 -1
  28. package/dist/text.d.ts +7 -0
  29. package/dist/text.d.ts.map +1 -1
  30. package/dist/text.js +125 -0
  31. package/dist/text.js.map +1 -1
  32. package/dist/theme.d.ts.map +1 -1
  33. package/dist/theme.js +18 -2
  34. package/dist/theme.js.map +1 -1
  35. package/dist/types.d.ts +3 -2
  36. package/dist/types.d.ts.map +1 -1
  37. package/docs/api-reference.md +6 -3
  38. package/docs/cookbook.md +1 -1
  39. package/docs/interaction-model.md +5 -5
  40. package/docs/primitive-gallery.md +4 -4
  41. package/examples/basic.tsx +22 -0
  42. package/examples/cli.tsx +55 -0
  43. package/examples/demo.tsx +98 -0
  44. package/examples/docs/background-fill.tsx +107 -0
  45. package/examples/docs/component-composition.tsx +140 -0
  46. package/examples/docs/cursor.tsx +121 -0
  47. package/examples/docs/employees-list.tsx +138 -0
  48. package/examples/docs/hello.tsx +98 -0
  49. package/examples/docs/interactive-note.tsx +111 -0
  50. package/examples/docs/module-api-dashboard.tsx +307 -0
  51. package/examples/docs/module-flux-store.tsx +181 -0
  52. package/examples/docs/module-form-workflow.tsx +339 -0
  53. package/examples/docs/module-forms.tsx +218 -0
  54. package/examples/docs/module-money.tsx +175 -0
  55. package/examples/docs/module-native-store.tsx +188 -0
  56. package/examples/docs/module-pulses.tsx +142 -0
  57. package/examples/docs/module-query.tsx +209 -0
  58. package/examples/docs/module-request.tsx +194 -0
  59. package/examples/docs/module-state-workbench.tsx +283 -0
  60. package/examples/docs/module-tasks.tsx +223 -0
  61. package/examples/docs/module-translate.tsx +194 -0
  62. package/examples/docs/module-utils.tsx +168 -0
  63. package/examples/docs/module-valyrian-core.tsx +159 -0
  64. package/examples/docs/pizza-builder.tsx +463 -0
  65. package/examples/docs/primitive-activity-console.tsx +113 -0
  66. package/examples/docs/primitive-command-panel.tsx +186 -0
  67. package/examples/docs/primitive-data-explorer.tsx +155 -0
  68. package/examples/docs/primitive-input-workbench.tsx +128 -0
  69. package/examples/docs/primitive-layout-shell.tsx +115 -0
  70. package/examples/docs/responsive-split.tsx +186 -0
  71. package/examples/docs/style-system.tsx +209 -0
  72. package/examples/docs/theme-colors.tsx +225 -0
  73. package/examples/docs/virtualized-list-workbench.tsx +232 -0
  74. package/examples/opencode-dogfood-app.tsx +215 -0
  75. package/examples/opencode-dogfood-lifecycle.tsx +194 -0
  76. package/examples/opencode-dogfood.tsx +11 -0
  77. package/llms-full.txt +16 -13
  78. package/package.json +3 -2
  79. package/src/ansi.ts +207 -17
  80. package/src/events.ts +2 -0
  81. package/src/frame-style.ts +36 -0
  82. package/src/layout.ts +57 -24
  83. package/src/mouse.ts +10 -1
  84. package/src/render-internal.ts +1441 -0
  85. package/src/render.ts +14 -1324
  86. package/src/session.ts +99 -12
  87. package/src/text.ts +160 -0
  88. package/src/theme.ts +22 -2
  89. package/src/types.ts +3 -2
@@ -0,0 +1,209 @@
1
+ import { QueryClient } from "valyrian.js/query";
2
+ import { Pane, Screen, Text, mountTerminal } from "@valyrianjs/terminal";
3
+ import type { TerminalMountOptions, TerminalSession } from "@valyrianjs/terminal";
4
+
5
+ interface ModuleDemo {
6
+ session: TerminalSession;
7
+ dispatchKey(key: string): string;
8
+ output(): string;
9
+ ansiOutput(): string;
10
+ isRunning(): boolean;
11
+ destroy(): void;
12
+ }
13
+
14
+ interface InventoryItem {
15
+ sku: string;
16
+ name: string;
17
+ category: string;
18
+ stock: number;
19
+ }
20
+
21
+ interface InventoryResult {
22
+ readLabel: string;
23
+ items: InventoryItem[];
24
+ }
25
+
26
+ interface QueryState {
27
+ value: string;
28
+ status: string;
29
+ invalidated: boolean;
30
+ fetchCount: number;
31
+ selectedIndex: number;
32
+ items: InventoryItem[];
33
+ running: boolean;
34
+ }
35
+
36
+ const PANEL_STYLE = { color: "#f8fafc", background: "#111827", padding: { left: 1, right: 1 } };
37
+ const FOOTER_STYLE = { color: "#dbeafe", background: "#1e293b" };
38
+
39
+ const INVENTORY: InventoryItem[] = [
40
+ { sku: "OPS-100", name: "Battery pack", category: "Operations", stock: 12 },
41
+ { sku: "NET-210", name: "Switch kit", category: "Network", stock: 4 },
42
+ { sku: "SEC-404", name: "Seal tags", category: "Security", stock: 30 }
43
+ ];
44
+
45
+ function shouldRunSnapshot() {
46
+ return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
47
+ }
48
+
49
+ function selectedItem(state: QueryState) {
50
+ return state.items[state.selectedIndex] || null;
51
+ }
52
+
53
+ export function App({ state }: { state: QueryState }) {
54
+ const item = selectedItem(state);
55
+ return (
56
+ <Screen title="Cached Inventory Browser">
57
+ <Text style={{ color: "#ffffff", background: "#020617" }}>Cached Inventory Browser</Text>
58
+ <Pane style={PANEL_STYLE}>
59
+ <Text>Inventory Browser: fetches local stock data through a Valyrian query cache.</Text>
60
+ <Text>{`Query status: ${state.status}`}</Text>
61
+ <Text>{`Cached value: ${state.value}`}</Text>
62
+ <Text>{`Invalidated: ${state.invalidated ? "yes" : "no"}`}</Text>
63
+ <Text>{`Fetcher calls: ${state.fetchCount}`}</Text>
64
+ <Text>{`Selected item: ${item ? `${item.name} (${item.sku})` : "none"}`}</Text>
65
+ <Text>{`Category: ${item ? item.category : "none"}`}</Text>
66
+ <Text>{`Stock on hand: ${item ? item.stock : 0}`}</Text>
67
+ </Pane>
68
+ <Text style={FOOTER_STYLE}>F: fetch inventory I: invalidate cache J/K: select item Ctrl+C: quit</Text>
69
+ </Screen>
70
+ );
71
+ }
72
+
73
+ export function createModuleQueryDemo(options: TerminalMountOptions = {}): ModuleDemo {
74
+ const state: QueryState = {
75
+ value: "none",
76
+ status: "idle",
77
+ invalidated: false,
78
+ fetchCount: 0,
79
+ selectedIndex: 0,
80
+ items: [],
81
+ running: true
82
+ };
83
+ const client = new QueryClient({ staleTime: 30000, cacheTime: 1000 });
84
+ const query = client.query({
85
+ key: ["docs", "inventory"],
86
+ fetcher: (): InventoryResult => {
87
+ state.fetchCount += 1;
88
+ return {
89
+ readLabel: `terminal read #${state.fetchCount}`,
90
+ items: INVENTORY.map((item) => ({ ...item }))
91
+ };
92
+ }
93
+ });
94
+ let session: TerminalSession;
95
+
96
+ async function fetchInventory() {
97
+ state.status = "loading";
98
+ session.update();
99
+ const data = await query.fetch();
100
+ state.value = data?.readLabel || "none";
101
+ state.items = data?.items || [];
102
+ state.selectedIndex = Math.min(state.selectedIndex, Math.max(state.items.length - 1, 0));
103
+ state.status = query.state.status;
104
+ session.update();
105
+ }
106
+
107
+ function invalidateInventory() {
108
+ query.invalidate();
109
+ state.status = query.state.status;
110
+ state.invalidated = true;
111
+ session.update();
112
+ }
113
+
114
+ function moveSelection(delta: number) {
115
+ if (state.items.length === 0) {
116
+ return;
117
+ }
118
+ state.selectedIndex = (state.selectedIndex + delta + state.items.length) % state.items.length;
119
+ session.update();
120
+ }
121
+
122
+ function quit() {
123
+ state.running = false;
124
+ client.clear();
125
+ session.destroy();
126
+ }
127
+
128
+ session = mountTerminal(<App state={state} />, {
129
+ ...options,
130
+ cols: options.cols ?? 92,
131
+ rows: options.rows ?? 16,
132
+ keymap: {
133
+ ...options.keymap,
134
+ bindings: [
135
+ ...(options.keymap?.bindings || []),
136
+ { key: "f", command: { id: "query.fetch" }, scope: "global" },
137
+ { key: "F", command: { id: "query.fetch" }, scope: "global" },
138
+ { key: "i", command: { id: "query.invalidate" }, scope: "global" },
139
+ { key: "I", command: { id: "query.invalidate" }, scope: "global" },
140
+ { key: "j", command: { id: "inventory.next" }, scope: "global" },
141
+ { key: "J", command: { id: "inventory.next" }, scope: "global" },
142
+ { key: "k", command: { id: "inventory.previous" }, scope: "global" },
143
+ { key: "K", command: { id: "inventory.previous" }, scope: "global" },
144
+ { key: "CTRL_C", command: { id: "quit" }, scope: "global" }
145
+ ],
146
+ onCommand(command, context) {
147
+ if (command.id === "query.fetch") {
148
+ void fetchInventory();
149
+ return true;
150
+ }
151
+ if (command.id === "query.invalidate") {
152
+ invalidateInventory();
153
+ return true;
154
+ }
155
+ if (command.id === "inventory.next") {
156
+ moveSelection(1);
157
+ return true;
158
+ }
159
+ if (command.id === "inventory.previous") {
160
+ moveSelection(-1);
161
+ return true;
162
+ }
163
+ if (command.id === "quit") {
164
+ quit();
165
+ return true;
166
+ }
167
+ return options.keymap?.onCommand?.(command, context);
168
+ }
169
+ }
170
+ });
171
+
172
+ return {
173
+ session,
174
+ dispatchKey(key: string) {
175
+ return session.dispatchKey(key);
176
+ },
177
+ output() {
178
+ return session.output();
179
+ },
180
+ ansiOutput() {
181
+ return session.ansiOutput();
182
+ },
183
+ isRunning() {
184
+ return state.running;
185
+ },
186
+ destroy() {
187
+ state.running = false;
188
+ client.clear();
189
+ session.destroy();
190
+ }
191
+ };
192
+ }
193
+
194
+ if (import.meta.main) {
195
+ if (shouldRunSnapshot()) {
196
+ const demo = createModuleQueryDemo({ runtime: "headless", cols: 92, rows: 16 });
197
+ demo.dispatchKey("F");
198
+ await new Promise((resolve) => setTimeout(resolve, 0));
199
+ demo.dispatchKey("J");
200
+ demo.dispatchKey("I");
201
+ demo.dispatchKey("F");
202
+ await new Promise((resolve) => setTimeout(resolve, 0));
203
+ process.stdout.write(demo.output());
204
+ process.stdout.write("\n");
205
+ demo.destroy();
206
+ } else {
207
+ createModuleQueryDemo();
208
+ }
209
+ }
@@ -0,0 +1,194 @@
1
+ import { request } from "valyrian.js/request";
2
+ import { Pane, Screen, Text, mountTerminal } from "@valyrianjs/terminal";
3
+ import type { TerminalMountOptions, TerminalSession } from "@valyrianjs/terminal";
4
+
5
+ interface ModuleDemo {
6
+ session: TerminalSession;
7
+ dispatchKey(key: string): string;
8
+ output(): string;
9
+ ansiOutput(): string;
10
+ isRunning(): boolean;
11
+ destroy(): void;
12
+ }
13
+
14
+ interface RequestState {
15
+ status: "idle" | "loading" | "success" | "error";
16
+ message: string;
17
+ fetchedPath: string;
18
+ lastError: string;
19
+ failNext: boolean;
20
+ history: Array<"success" | "error">;
21
+ running: boolean;
22
+ }
23
+
24
+ const PANEL_STYLE = { color: "#f8fafc", background: "#111827", padding: { left: 1, right: 1 } };
25
+ const FOOTER_STYLE = { color: "#dbeafe", background: "#1e293b" };
26
+
27
+ function shouldRunSnapshot() {
28
+ return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
29
+ }
30
+
31
+ function createState(): RequestState {
32
+ return {
33
+ status: "idle",
34
+ message: "Ready to check the service status",
35
+ fetchedPath: "none",
36
+ lastError: "none",
37
+ failNext: false,
38
+ history: [],
39
+ running: true
40
+ };
41
+ }
42
+
43
+ function historySummary(history: RequestState["history"]) {
44
+ return history.length === 0 ? "none" : history.join(" -> ");
45
+ }
46
+
47
+ const api = request.new("http://terminal.local", { allowedMethods: ["get"] });
48
+
49
+ async function withFakeFetch<T>(shouldFail: boolean, run: () => Promise<T>) {
50
+ const originalFetch = globalThis.fetch;
51
+ globalThis.fetch = ((input: RequestInfo | URL) => {
52
+ const url = new URL(String(input));
53
+ if (shouldFail) {
54
+ return Promise.resolve(
55
+ new Response(JSON.stringify({ message: "controlled local failure" }), { status: 503 })
56
+ );
57
+ }
58
+ return Promise.resolve(
59
+ new Response(JSON.stringify({ status: "ok", path: `${url.pathname}${url.search}` }), { status: 200 })
60
+ );
61
+ }) as typeof fetch;
62
+
63
+ try {
64
+ return await run();
65
+ } finally {
66
+ globalThis.fetch = originalFetch;
67
+ }
68
+ }
69
+
70
+ export function App({ state }: { state: RequestState }) {
71
+ return (
72
+ <Screen title="Service Status Client">
73
+ <Text style={{ color: "#ffffff", background: "#020617" }}>Service Status Client</Text>
74
+ <Pane style={PANEL_STYLE}>
75
+ <Text>Status Client: checks a local service endpoint without using the network.</Text>
76
+ <Text>{`Request status: ${state.status}`}</Text>
77
+ <Text>{state.message}</Text>
78
+ <Text>{`Fetched: ${state.fetchedPath}`}</Text>
79
+ <Text>{`Last error: ${state.lastError}`}</Text>
80
+ <Text>{`Next request: ${state.failNext ? "controlled error" : "success"}`}</Text>
81
+ <Text>{`History: ${historySummary(state.history)}`}</Text>
82
+ </Pane>
83
+ <Text style={FOOTER_STYLE}>R: refresh status E: force next error Ctrl+C: quit</Text>
84
+ </Screen>
85
+ );
86
+ }
87
+
88
+ export function createModuleRequestDemo(options: TerminalMountOptions = {}): ModuleDemo {
89
+ const state = createState();
90
+ let session: TerminalSession;
91
+
92
+ async function loadStatus() {
93
+ if (state.status === "loading") {
94
+ state.message = "Refresh already in progress";
95
+ session.update();
96
+ return;
97
+ }
98
+
99
+ state.status = "loading";
100
+ state.message = "Loading local service status";
101
+ session.update();
102
+ try {
103
+ const shouldFail = state.failNext;
104
+ state.failNext = false;
105
+ const body = (await withFakeFetch(shouldFail, () => api.get("/status", { page: 1 }))) as { status: string; path: string };
106
+ state.status = "success";
107
+ state.history.push("success");
108
+ state.message = state.lastError === "none" ? `HTTP status: ${body.status}` : `Retry result: ${body.status}`;
109
+ state.fetchedPath = body.path;
110
+ } catch (error) {
111
+ state.status = "error";
112
+ state.history.push("error");
113
+ state.message = error instanceof Error ? error.message : "Request failed";
114
+ state.lastError = state.message;
115
+ state.fetchedPath = "/status?page=1";
116
+ }
117
+ session.update();
118
+ }
119
+
120
+ function quit() {
121
+ state.running = false;
122
+ session.destroy();
123
+ }
124
+
125
+ session = mountTerminal(<App state={state} />, {
126
+ ...options,
127
+ cols: options.cols ?? 92,
128
+ rows: options.rows ?? 16,
129
+ keymap: {
130
+ ...options.keymap,
131
+ bindings: [
132
+ ...(options.keymap?.bindings || []),
133
+ { key: "r", command: { id: "request.load" }, scope: "global" },
134
+ { key: "R", command: { id: "request.load" }, scope: "global" },
135
+ { key: "e", command: { id: "request.failNext" }, scope: "global" },
136
+ { key: "E", command: { id: "request.failNext" }, scope: "global" },
137
+ { key: "CTRL_C", command: { id: "quit" }, scope: "global" }
138
+ ],
139
+ onCommand(command, context) {
140
+ if (command.id === "request.load") {
141
+ void loadStatus();
142
+ return true;
143
+ }
144
+ if (command.id === "request.failNext") {
145
+ state.failNext = true;
146
+ state.message = "Next refresh will use the controlled error path";
147
+ session.update();
148
+ return true;
149
+ }
150
+ if (command.id === "quit") {
151
+ quit();
152
+ return true;
153
+ }
154
+ return options.keymap?.onCommand?.(command, context);
155
+ }
156
+ }
157
+ });
158
+
159
+ return {
160
+ session,
161
+ dispatchKey(key: string) {
162
+ return session.dispatchKey(key);
163
+ },
164
+ output() {
165
+ return session.output();
166
+ },
167
+ ansiOutput() {
168
+ return session.ansiOutput();
169
+ },
170
+ isRunning() {
171
+ return state.running;
172
+ },
173
+ destroy() {
174
+ state.running = false;
175
+ session.destroy();
176
+ }
177
+ };
178
+ }
179
+
180
+ if (import.meta.main) {
181
+ if (shouldRunSnapshot()) {
182
+ const demo = createModuleRequestDemo({ runtime: "headless", cols: 92, rows: 16 });
183
+ demo.dispatchKey("E");
184
+ demo.dispatchKey("R");
185
+ await new Promise((resolve) => setTimeout(resolve, 0));
186
+ demo.dispatchKey("R");
187
+ await new Promise((resolve) => setTimeout(resolve, 0));
188
+ process.stdout.write(demo.output());
189
+ process.stdout.write("\n");
190
+ demo.destroy();
191
+ } else {
192
+ createModuleRequestDemo();
193
+ }
194
+ }
@@ -0,0 +1,283 @@
1
+ import { v } from "valyrian.js";
2
+ import { createPulse, createPulseStore } from "valyrian.js/pulses";
3
+ import { FluxStore } from "valyrian.js/flux-store";
4
+ import { Pane, Screen, Split, Text, mountTerminal } from "@valyrianjs/terminal";
5
+ import type { TerminalMountOptions, TerminalSession } from "@valyrianjs/terminal";
6
+
7
+ type JobStatus = "queued" | "running" | "done" | "blocked";
8
+ type OperationAction = "dispatch" | "done" | "block" | "reset";
9
+
10
+ interface Job {
11
+ id: string;
12
+ label: string;
13
+ owner: string;
14
+ status: JobStatus;
15
+ }
16
+
17
+ interface OperationEvent {
18
+ action: OperationAction;
19
+ job: string;
20
+ note: string;
21
+ }
22
+
23
+ interface ModuleStateWorkbenchDemo {
24
+ session: TerminalSession;
25
+ dispatchKey(key: string): string;
26
+ output(): string;
27
+ ansiOutput(): string;
28
+ isRunning(): boolean;
29
+ destroy(): void;
30
+ }
31
+
32
+ const INITIAL_JOBS: Job[] = [
33
+ { id: "docs", label: "Docs refresh", owner: "Mina", status: "running" },
34
+ { id: "release", label: "Package release", owner: "Omar", status: "queued" },
35
+ { id: "support", label: "Support sweep", owner: "Iris", status: "blocked" }
36
+ ];
37
+
38
+ function createWorkbenchStores() {
39
+ const jobsStore = createPulseStore(
40
+ {
41
+ selectedIndex: 0,
42
+ jobs: INITIAL_JOBS.map((job) => ({ ...job }))
43
+ },
44
+ {
45
+ selectNext(state) {
46
+ state.selectedIndex = (state.selectedIndex + 1) % state.jobs.length;
47
+ },
48
+ selectPrevious(state) {
49
+ state.selectedIndex = (state.selectedIndex - 1 + state.jobs.length) % state.jobs.length;
50
+ },
51
+ updateSelectedStatus(state, status: JobStatus) {
52
+ state.jobs[state.selectedIndex].status = status;
53
+ },
54
+ reset(state) {
55
+ state.selectedIndex = 0;
56
+ state.jobs = INITIAL_JOBS.map((job) => ({ ...job }));
57
+ }
58
+ }
59
+ );
60
+
61
+ const [pulseRevision, setPulseRevision] = createPulse(1);
62
+
63
+ const operations = new FluxStore({
64
+ state: {
65
+ done: 0,
66
+ blocked: 1,
67
+ dispatched: 0,
68
+ timeline: ["Bootstrapped queue", "Loaded terminal session"] as string[],
69
+ lastAction: "dispatch" as OperationAction
70
+ },
71
+ mutations: {
72
+ SET_METRICS(state, jobs: Job[]) {
73
+ state.done = jobs.filter((job) => job.status === "done").length;
74
+ state.blocked = jobs.filter((job) => job.status === "blocked").length;
75
+ },
76
+ RECORD_EVENT(state, event: OperationEvent) {
77
+ state.lastAction = event.action;
78
+ state.dispatched += event.action === "dispatch" ? 1 : 0;
79
+ state.timeline.push(`${event.action}: ${event.job} — ${event.note}`);
80
+ state.timeline = state.timeline.slice(-5);
81
+ },
82
+ RESET(state) {
83
+ state.done = 0;
84
+ state.blocked = 1;
85
+ state.dispatched = 0;
86
+ state.lastAction = "reset";
87
+ state.timeline = ["reset: queue — restored baseline"];
88
+ }
89
+ },
90
+ actions: {
91
+ async dispatchSelected(store, job: Job) {
92
+ jobsStore.updateSelectedStatus("running");
93
+ store.commit("SET_METRICS", jobsStore.state.jobs);
94
+ store.commit("RECORD_EVENT", { action: "dispatch", job: job.label, note: "operator started work" });
95
+ },
96
+ async markDone(store, job: Job) {
97
+ jobsStore.updateSelectedStatus("done");
98
+ store.commit("SET_METRICS", jobsStore.state.jobs);
99
+ store.commit("RECORD_EVENT", { action: "done", job: job.label, note: "closed cleanly" });
100
+ },
101
+ async blockSelected(store, job: Job) {
102
+ jobsStore.updateSelectedStatus("blocked");
103
+ store.commit("SET_METRICS", jobsStore.state.jobs);
104
+ store.commit("RECORD_EVENT", { action: "block", job: job.label, note: "needs review" });
105
+ }
106
+ }
107
+ });
108
+
109
+ return { jobsStore, operations, pulseRevision, setPulseRevision };
110
+ }
111
+
112
+ type WorkbenchStores = ReturnType<typeof createWorkbenchStores>;
113
+
114
+ const SURFACE_STYLE = { color: "#f5f5f5", background: "#111827", padding: { left: 1, right: 1 } };
115
+ const DETAIL_STYLE = { color: "#f8fafc", background: "#172554", padding: { left: 1, right: 1 } };
116
+ const FOOTER_STYLE = { color: "#d1d5db", background: "#1f2937" };
117
+ const CURRENT_STYLE = { color: "#ffffff", background: "#14532d" };
118
+ const MUTED_STYLE = { color: "#9ca3af" };
119
+
120
+ function shouldRunSnapshot() {
121
+ return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
122
+ }
123
+
124
+ function selectedJob(stores: WorkbenchStores) {
125
+ return stores.jobsStore.state.jobs[stores.jobsStore.state.selectedIndex];
126
+ }
127
+
128
+ function resetStores(stores: WorkbenchStores) {
129
+ stores.jobsStore.reset();
130
+ stores.operations.commit("RESET");
131
+ stores.setPulseRevision(1);
132
+ }
133
+
134
+ function bumpRevision(stores: WorkbenchStores) {
135
+ stores.setPulseRevision((value) => value + 1);
136
+ }
137
+
138
+ export function App({ stores }: { stores: WorkbenchStores }) {
139
+ const selected = selectedJob(stores);
140
+ const manualVNode = v(Text, { style: MUTED_STYLE }, `Tracked jobs: ${stores.jobsStore.state.jobs.length}`);
141
+
142
+ return (
143
+ <Screen title="Operations Workbench">
144
+ <Text style={{ color: "#ffffff", background: "#0f172a" }}>Valyrian.js modules in terminal apps: state workbench</Text>
145
+ <Split direction="row" gap={1} sizes={["2fr", "1fr"]}>
146
+ <Pane style={SURFACE_STYLE}>
147
+ <Text>Operations Workbench</Text>
148
+ <Text>{`Pulse revision: ${stores.pulseRevision()} Done jobs: ${stores.operations.state.done} Blocked jobs: ${stores.operations.state.blocked}`}</Text>
149
+ <Text>{`Dispatches: ${stores.operations.state.dispatched} Last action: ${stores.operations.state.lastAction}`}</Text>
150
+ {stores.jobsStore.state.jobs.map((job, index) => (
151
+ <Text state={index === stores.jobsStore.state.selectedIndex ? "current" : undefined} style={index === stores.jobsStore.state.selectedIndex ? undefined : MUTED_STYLE} styles={{ current: CURRENT_STYLE }}>{`${index === stores.jobsStore.state.selectedIndex ? ">" : " "} ${job.label} — ${job.owner} — ${job.status}`}</Text>
152
+ ))}
153
+ </Pane>
154
+ <Pane style={DETAIL_STYLE}>
155
+ <Text>{`Selected: ${selected.label}`}</Text>
156
+ <Text>{`Owner: ${selected.owner}`}</Text>
157
+ <Text>{`Status: ${selected.status}`}</Text>
158
+ {manualVNode}
159
+ <Text>FluxStore dispatch coordinates operation actions.</Text>
160
+ <Text>{`Timeline: ${stores.operations.state.timeline.slice(-3).join(" | ")}`}</Text>
161
+ </Pane>
162
+ </Split>
163
+ <Text style={FOOTER_STYLE}>J/K: select Enter: dispatch D: done B: block R: reset Ctrl+C: quit</Text>
164
+ </Screen>
165
+ );
166
+ }
167
+
168
+ export function createModuleStateWorkbenchDemo(options: TerminalMountOptions = {}): ModuleStateWorkbenchDemo {
169
+ const stores = createWorkbenchStores();
170
+ resetStores(stores);
171
+ let running = true;
172
+ let session: TerminalSession;
173
+
174
+ function quit() {
175
+ running = false;
176
+ session.destroy();
177
+ }
178
+
179
+ function updateAfterOperation() {
180
+ bumpRevision(stores);
181
+ session.update();
182
+ }
183
+
184
+ function runSelected(action: "dispatchSelected" | "markDone" | "blockSelected") {
185
+ const job = selectedJob(stores);
186
+ void stores.operations.dispatch(action, job).then(updateAfterOperation);
187
+ updateAfterOperation();
188
+ }
189
+
190
+ session = mountTerminal(<App stores={stores} />, {
191
+ ...options,
192
+ cols: options.cols ?? 100,
193
+ rows: options.rows ?? 24,
194
+ keymap: {
195
+ ...options.keymap,
196
+ bindings: [
197
+ ...(options.keymap?.bindings || []),
198
+ { key: "j", command: { id: "jobs.next" }, scope: "global" },
199
+ { key: "J", command: { id: "jobs.next" }, scope: "global" },
200
+ { key: "k", command: { id: "jobs.previous" }, scope: "global" },
201
+ { key: "K", command: { id: "jobs.previous" }, scope: "global" },
202
+ { key: "ENTER", command: { id: "jobs.dispatch" }, scope: "global" },
203
+ { key: "d", command: { id: "jobs.done" }, scope: "global" },
204
+ { key: "D", command: { id: "jobs.done" }, scope: "global" },
205
+ { key: "b", command: { id: "jobs.block" }, scope: "global" },
206
+ { key: "B", command: { id: "jobs.block" }, scope: "global" },
207
+ { key: "r", command: { id: "jobs.reset" }, scope: "global" },
208
+ { key: "R", command: { id: "jobs.reset" }, scope: "global" },
209
+ { key: "CTRL_C", command: { id: "quit" }, scope: "global" }
210
+ ],
211
+ onCommand(command, context) {
212
+ if (command.id === "jobs.next") {
213
+ stores.jobsStore.selectNext();
214
+ session.update();
215
+ return true;
216
+ }
217
+ if (command.id === "jobs.previous") {
218
+ stores.jobsStore.selectPrevious();
219
+ session.update();
220
+ return true;
221
+ }
222
+ if (command.id === "jobs.dispatch") {
223
+ runSelected("dispatchSelected");
224
+ return true;
225
+ }
226
+ if (command.id === "jobs.done") {
227
+ runSelected("markDone");
228
+ return true;
229
+ }
230
+ if (command.id === "jobs.block") {
231
+ runSelected("blockSelected");
232
+ return true;
233
+ }
234
+ if (command.id === "jobs.reset") {
235
+ resetStores(stores);
236
+ session.update();
237
+ return true;
238
+ }
239
+ if (command.id === "quit") {
240
+ quit();
241
+ return true;
242
+ }
243
+ return options.keymap?.onCommand?.(command, context);
244
+ }
245
+ }
246
+ });
247
+
248
+ return {
249
+ session,
250
+ dispatchKey(key: string) {
251
+ return session.dispatchKey(key);
252
+ },
253
+ output() {
254
+ return session.output();
255
+ },
256
+ ansiOutput() {
257
+ return session.ansiOutput();
258
+ },
259
+ isRunning() {
260
+ return running;
261
+ },
262
+ destroy() {
263
+ running = false;
264
+ session.destroy();
265
+ }
266
+ };
267
+ }
268
+
269
+ if (import.meta.main) {
270
+ if (shouldRunSnapshot()) {
271
+ const demo = createModuleStateWorkbenchDemo({ runtime: "headless", cols: 100, rows: 24 });
272
+ demo.dispatchKey("J");
273
+ demo.dispatchKey("ENTER");
274
+ demo.dispatchKey("D");
275
+ demo.dispatchKey("K");
276
+ demo.dispatchKey("B");
277
+ process.stdout.write(demo.output());
278
+ process.stdout.write("\n");
279
+ demo.destroy();
280
+ } else {
281
+ createModuleStateWorkbenchDemo();
282
+ }
283
+ }