@valyrianjs/terminal 0.2.1 → 0.2.2
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/dist/ansi.d.ts.map +1 -1
- package/dist/ansi.js +12 -14
- package/dist/ansi.js.map +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +4 -0
- package/dist/events.js.map +1 -1
- package/dist/frame-style.d.ts +7 -0
- package/dist/frame-style.d.ts.map +1 -0
- package/dist/frame-style.js +27 -0
- package/dist/frame-style.js.map +1 -0
- package/dist/layout.d.ts +5 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +53 -23
- package/dist/layout.js.map +1 -1
- package/dist/mouse.d.ts.map +1 -1
- package/dist/mouse.js +8 -1
- package/dist/mouse.js.map +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +87 -48
- package/dist/render.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +2 -0
- package/dist/session.js.map +1 -1
- package/dist/text.d.ts +7 -0
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +114 -0
- package/dist/text.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/api-reference.md +6 -3
- package/docs/cookbook.md +1 -1
- package/docs/interaction-model.md +5 -5
- package/docs/primitive-gallery.md +4 -4
- package/examples/basic.tsx +22 -0
- package/examples/cli.tsx +55 -0
- package/examples/demo.tsx +98 -0
- package/examples/docs/background-fill.tsx +107 -0
- package/examples/docs/component-composition.tsx +140 -0
- package/examples/docs/cursor.tsx +121 -0
- package/examples/docs/employees-list.tsx +138 -0
- package/examples/docs/hello.tsx +98 -0
- package/examples/docs/interactive-note.tsx +111 -0
- package/examples/docs/module-api-dashboard.tsx +307 -0
- package/examples/docs/module-flux-store.tsx +181 -0
- package/examples/docs/module-form-workflow.tsx +339 -0
- package/examples/docs/module-forms.tsx +218 -0
- package/examples/docs/module-money.tsx +175 -0
- package/examples/docs/module-native-store.tsx +188 -0
- package/examples/docs/module-pulses.tsx +142 -0
- package/examples/docs/module-query.tsx +209 -0
- package/examples/docs/module-request.tsx +194 -0
- package/examples/docs/module-state-workbench.tsx +283 -0
- package/examples/docs/module-tasks.tsx +223 -0
- package/examples/docs/module-translate.tsx +194 -0
- package/examples/docs/module-utils.tsx +168 -0
- package/examples/docs/module-valyrian-core.tsx +159 -0
- package/examples/docs/pizza-builder.tsx +463 -0
- package/examples/docs/primitive-activity-console.tsx +113 -0
- package/examples/docs/primitive-command-panel.tsx +186 -0
- package/examples/docs/primitive-data-explorer.tsx +155 -0
- package/examples/docs/primitive-input-workbench.tsx +128 -0
- package/examples/docs/primitive-layout-shell.tsx +115 -0
- package/examples/docs/responsive-split.tsx +186 -0
- package/examples/docs/style-system.tsx +209 -0
- package/examples/docs/theme-colors.tsx +225 -0
- package/examples/docs/virtualized-list-workbench.tsx +232 -0
- package/examples/opencode-dogfood-app.tsx +215 -0
- package/examples/opencode-dogfood-lifecycle.tsx +194 -0
- package/examples/opencode-dogfood.tsx +11 -0
- package/llms-full.txt +16 -13
- package/package.json +3 -2
- package/src/ansi.ts +12 -14
- package/src/events.ts +2 -0
- package/src/frame-style.ts +36 -0
- package/src/layout.ts +57 -24
- package/src/mouse.ts +10 -1
- package/src/render.ts +92 -48
- package/src/session.ts +2 -0
- package/src/text.ts +148 -0
- package/src/types.ts +3 -0
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { Task } from "valyrian.js/tasks";
|
|
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 TaskDemoState {
|
|
15
|
+
status: string;
|
|
16
|
+
result: string;
|
|
17
|
+
error: string;
|
|
18
|
+
currentJob: string;
|
|
19
|
+
seenStates: string[];
|
|
20
|
+
running: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const PANEL_STYLE = { color: "#f8fafc", background: "#111827", padding: { left: 1, right: 1 } };
|
|
24
|
+
const FOOTER_STYLE = { color: "#dbeafe", background: "#1e293b" };
|
|
25
|
+
|
|
26
|
+
function shouldRunSnapshot() {
|
|
27
|
+
return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function waitForControlledAsync(signal: AbortSignal) {
|
|
31
|
+
return new Promise<void>((resolve) => {
|
|
32
|
+
const timer = setTimeout(resolve, 20);
|
|
33
|
+
signal.addEventListener(
|
|
34
|
+
"abort",
|
|
35
|
+
() => {
|
|
36
|
+
clearTimeout(timer);
|
|
37
|
+
resolve();
|
|
38
|
+
},
|
|
39
|
+
{ once: true }
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function syncTaskState(state: TaskDemoState, task: Task<string, string>) {
|
|
45
|
+
const snapshot = task.state;
|
|
46
|
+
state.status = snapshot.status;
|
|
47
|
+
state.result = snapshot.result ? `Result: ${snapshot.result}` : "Result: none";
|
|
48
|
+
state.error = snapshot.error instanceof Error ? `Error: ${snapshot.error.message}` : "Error: none";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function visibleTaskStates(states: string[]) {
|
|
52
|
+
const compact: string[] = [];
|
|
53
|
+
for (const status of states) {
|
|
54
|
+
if ((status === "cancelled" || status === "error") && compact[compact.length - 1] === "running") {
|
|
55
|
+
compact.pop();
|
|
56
|
+
}
|
|
57
|
+
if (compact[compact.length - 1] !== status) {
|
|
58
|
+
compact.push(status);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return compact.join(" -> ");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function App({ state }: { state: TaskDemoState }) {
|
|
65
|
+
return (
|
|
66
|
+
<Screen title="Background Maintenance Runner">
|
|
67
|
+
<Text style={{ color: "#ffffff", background: "#020617" }}>Background Maintenance Runner</Text>
|
|
68
|
+
<Pane style={PANEL_STYLE}>
|
|
69
|
+
<Text>Maintenance Runner: runs local cleanup work through a Valyrian Task.</Text>
|
|
70
|
+
<Text>{`Task status: ${state.status}`}</Text>
|
|
71
|
+
<Text>{`Maintenance job: ${state.currentJob}`}</Text>
|
|
72
|
+
<Text>{state.result}</Text>
|
|
73
|
+
<Text>{state.error}</Text>
|
|
74
|
+
<Text>{`Seen states: ${visibleTaskStates(state.seenStates)}`}</Text>
|
|
75
|
+
</Pane>
|
|
76
|
+
<Text style={FOOTER_STYLE}>R: run cleanup C: cancel scan X: controlled fail Z: reset Ctrl+C: quit</Text>
|
|
77
|
+
</Screen>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function createModuleTasksDemo(options: TerminalMountOptions = {}): ModuleDemo {
|
|
82
|
+
const state: TaskDemoState = {
|
|
83
|
+
status: "idle",
|
|
84
|
+
result: "Result: none",
|
|
85
|
+
error: "Error: none",
|
|
86
|
+
currentJob: "none",
|
|
87
|
+
seenStates: ["idle"],
|
|
88
|
+
running: true
|
|
89
|
+
};
|
|
90
|
+
const task = new Task<string, string>(async (label, { signal }) => {
|
|
91
|
+
if (label === "fail") {
|
|
92
|
+
throw new Error("controlled task failure");
|
|
93
|
+
}
|
|
94
|
+
if (label === "scan") {
|
|
95
|
+
await waitForControlledAsync(signal);
|
|
96
|
+
if (signal.aborted) return "Cancelled stale scan";
|
|
97
|
+
}
|
|
98
|
+
return `Completed ${label}`;
|
|
99
|
+
}, { strategy: "restartable" });
|
|
100
|
+
const rememberState = () => {
|
|
101
|
+
const status = task.state.status;
|
|
102
|
+
if (state.seenStates[state.seenStates.length - 1] !== status) {
|
|
103
|
+
state.seenStates.push(status);
|
|
104
|
+
}
|
|
105
|
+
syncTaskState(state, task);
|
|
106
|
+
};
|
|
107
|
+
const offTaskState = task.on("state", () => {
|
|
108
|
+
rememberState();
|
|
109
|
+
session?.update();
|
|
110
|
+
});
|
|
111
|
+
let session: TerminalSession;
|
|
112
|
+
|
|
113
|
+
function quit() {
|
|
114
|
+
state.running = false;
|
|
115
|
+
offTaskState();
|
|
116
|
+
task.reset();
|
|
117
|
+
session.destroy();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function runTask(label: string, job: string) {
|
|
121
|
+
state.currentJob = job;
|
|
122
|
+
try {
|
|
123
|
+
rememberState();
|
|
124
|
+
await task.run(label);
|
|
125
|
+
} catch {
|
|
126
|
+
// The task state snapshot carries the controlled error message.
|
|
127
|
+
}
|
|
128
|
+
syncTaskState(state, task);
|
|
129
|
+
session.update();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
session = mountTerminal(<App state={state} />, {
|
|
133
|
+
...options,
|
|
134
|
+
cols: options.cols ?? 92,
|
|
135
|
+
rows: options.rows ?? 16,
|
|
136
|
+
keymap: {
|
|
137
|
+
...options.keymap,
|
|
138
|
+
bindings: [
|
|
139
|
+
...(options.keymap?.bindings || []),
|
|
140
|
+
{ key: "r", command: { id: "task.run" }, scope: "global" },
|
|
141
|
+
{ key: "R", command: { id: "task.run" }, scope: "global" },
|
|
142
|
+
{ key: "c", command: { id: "task.cancel" }, scope: "global" },
|
|
143
|
+
{ key: "C", command: { id: "task.cancel" }, scope: "global" },
|
|
144
|
+
{ key: "x", command: { id: "task.error" }, scope: "global" },
|
|
145
|
+
{ key: "X", command: { id: "task.error" }, scope: "global" },
|
|
146
|
+
{ key: "z", command: { id: "task.reset" }, scope: "global" },
|
|
147
|
+
{ key: "Z", command: { id: "task.reset" }, scope: "global" },
|
|
148
|
+
{ key: "CTRL_C", command: { id: "quit" }, scope: "global" }
|
|
149
|
+
],
|
|
150
|
+
onCommand(command, context) {
|
|
151
|
+
if (command.id === "task.run") {
|
|
152
|
+
void runTask("cleanup", "nightly cleanup");
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
if (command.id === "task.cancel") {
|
|
156
|
+
state.currentJob = "stale scan";
|
|
157
|
+
void task.run("scan");
|
|
158
|
+
rememberState();
|
|
159
|
+
task.cancel();
|
|
160
|
+
rememberState();
|
|
161
|
+
session.update();
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
if (command.id === "task.error") {
|
|
165
|
+
void runTask("fail", "permission audit");
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
if (command.id === "task.reset") {
|
|
169
|
+
state.currentJob = "none";
|
|
170
|
+
task.reset();
|
|
171
|
+
rememberState();
|
|
172
|
+
session.update();
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
if (command.id === "quit") {
|
|
176
|
+
quit();
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
return options.keymap?.onCommand?.(command, context);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
session,
|
|
186
|
+
dispatchKey(key: string) {
|
|
187
|
+
return session.dispatchKey(key);
|
|
188
|
+
},
|
|
189
|
+
output() {
|
|
190
|
+
return session.output();
|
|
191
|
+
},
|
|
192
|
+
ansiOutput() {
|
|
193
|
+
return session.ansiOutput();
|
|
194
|
+
},
|
|
195
|
+
isRunning() {
|
|
196
|
+
return state.running;
|
|
197
|
+
},
|
|
198
|
+
destroy() {
|
|
199
|
+
state.running = false;
|
|
200
|
+
offTaskState();
|
|
201
|
+
task.reset();
|
|
202
|
+
session.destroy();
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (import.meta.main) {
|
|
208
|
+
if (shouldRunSnapshot()) {
|
|
209
|
+
const demo = createModuleTasksDemo({ runtime: "headless", cols: 92, rows: 16 });
|
|
210
|
+
demo.dispatchKey("R");
|
|
211
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
212
|
+
demo.dispatchKey("C");
|
|
213
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
214
|
+
demo.dispatchKey("X");
|
|
215
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
216
|
+
demo.dispatchKey("Z");
|
|
217
|
+
process.stdout.write(demo.output());
|
|
218
|
+
process.stdout.write("\n");
|
|
219
|
+
demo.destroy();
|
|
220
|
+
} else {
|
|
221
|
+
createModuleTasksDemo();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { getLang, setLang, setLog, setStoreStrategy, setTranslations, t } from "valyrian.js/translate";
|
|
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
|
+
type Ticket = { id: string; customer: string; status: "open" | "waiting" | "closed" };
|
|
15
|
+
type SupportState = { selected: number; language: "en" | "es"; filter: "all" | "open" };
|
|
16
|
+
|
|
17
|
+
const TICKETS: Ticket[] = [
|
|
18
|
+
{ id: "T-104", customer: "Northwind", status: "open" },
|
|
19
|
+
{ id: "T-118", customer: "Contoso", status: "waiting" },
|
|
20
|
+
{ id: "T-130", customer: "Tailspin", status: "closed" }
|
|
21
|
+
];
|
|
22
|
+
const PANEL_STYLE = { color: "#f8fafc", background: "#111827", padding: { left: 1, right: 1 } };
|
|
23
|
+
const FOOTER_STYLE = { color: "#dbeafe", background: "#1e293b" };
|
|
24
|
+
let storedLanguage = "en";
|
|
25
|
+
|
|
26
|
+
function configureTranslations() {
|
|
27
|
+
setLog(false);
|
|
28
|
+
setStoreStrategy({
|
|
29
|
+
get: () => storedLanguage,
|
|
30
|
+
set: (language) => {
|
|
31
|
+
storedLanguage = language;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
setTranslations(
|
|
35
|
+
{
|
|
36
|
+
title: "Bilingual Support Console",
|
|
37
|
+
purpose: "Support Console",
|
|
38
|
+
language: "Language",
|
|
39
|
+
filter: "Filter",
|
|
40
|
+
selected: "Selected ticket",
|
|
41
|
+
all: "all tickets",
|
|
42
|
+
openOnly: "open only",
|
|
43
|
+
status: { open: "open", waiting: "waiting", closed: "closed" },
|
|
44
|
+
help: "L language J/K select ticket F filter status Ctrl+C: quit"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
es: {
|
|
48
|
+
title: "Bilingual Support Console",
|
|
49
|
+
purpose: "Consola de soporte",
|
|
50
|
+
language: "Idioma",
|
|
51
|
+
filter: "Filtro",
|
|
52
|
+
selected: "Ticket elegido",
|
|
53
|
+
all: "todos los tickets",
|
|
54
|
+
openOnly: "solo abiertos",
|
|
55
|
+
status: { open: "abierto", waiting: "en espera", closed: "cerrado" },
|
|
56
|
+
help: "L idioma J/K elegir ticket F filtrar estado Ctrl+C: quit"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function shouldRunSnapshot() {
|
|
63
|
+
return process.argv.includes("--snapshot") || process.env.VALYRIAN_TERMINAL_EXAMPLE_SNAPSHOT === "1" || !process.stdin.isTTY;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function visibleTickets(filter: SupportState["filter"]) {
|
|
67
|
+
return filter === "open" ? TICKETS.filter((ticket) => ticket.status === "open") : TICKETS;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function App({ state }: { state: SupportState }) {
|
|
71
|
+
setLang(state.language);
|
|
72
|
+
const tickets = visibleTickets(state.filter);
|
|
73
|
+
const selected = tickets[state.selected] || tickets[0];
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<Screen title="Bilingual Support Console">
|
|
77
|
+
<Text style={{ color: "#ffffff", background: "#020617" }}>{t("title")}</Text>
|
|
78
|
+
<Pane style={PANEL_STYLE}>
|
|
79
|
+
<Text>{t("purpose")}</Text>
|
|
80
|
+
<Text>{`${t("language")}: ${state.language === "en" ? "English" : "Español"}`}</Text>
|
|
81
|
+
<Text>{`${t("filter")}: ${state.filter === "open" ? t("openOnly") : t("all")}`}</Text>
|
|
82
|
+
{tickets.map((ticket, index) => (
|
|
83
|
+
<Text>{`${index === state.selected ? ">" : " "} ${ticket.id} ${ticket.customer} — ${t(`status.${ticket.status}`)}`}</Text>
|
|
84
|
+
))}
|
|
85
|
+
<Text>{`${t("selected")}: ${selected.id} ${selected.customer}`}</Text>
|
|
86
|
+
</Pane>
|
|
87
|
+
<Text style={FOOTER_STYLE}>{t("help")}</Text>
|
|
88
|
+
</Screen>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function createModuleTranslateDemo(options: TerminalMountOptions = {}): ModuleDemo {
|
|
93
|
+
const previousLanguage = getLang();
|
|
94
|
+
configureTranslations();
|
|
95
|
+
const state: SupportState = { selected: 0, language: "en", filter: "all" };
|
|
96
|
+
setLang(state.language);
|
|
97
|
+
let running = true;
|
|
98
|
+
let session: TerminalSession;
|
|
99
|
+
|
|
100
|
+
function quit() {
|
|
101
|
+
running = false;
|
|
102
|
+
setLang(previousLanguage);
|
|
103
|
+
session.destroy();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function clampSelection() {
|
|
107
|
+
const count = visibleTickets(state.filter).length;
|
|
108
|
+
state.selected = Math.min(state.selected, Math.max(0, count - 1));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function move(delta: number) {
|
|
112
|
+
const count = visibleTickets(state.filter).length;
|
|
113
|
+
state.selected = (state.selected + delta + count) % count;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
session = mountTerminal(<App state={state} />, {
|
|
117
|
+
...options,
|
|
118
|
+
cols: options.cols ?? 92,
|
|
119
|
+
rows: options.rows ?? 18,
|
|
120
|
+
keymap: {
|
|
121
|
+
...options.keymap,
|
|
122
|
+
bindings: [
|
|
123
|
+
...(options.keymap?.bindings || []),
|
|
124
|
+
{ key: "l", command: { id: "support.language" }, scope: "global" },
|
|
125
|
+
{ key: "L", command: { id: "support.language" }, scope: "global" },
|
|
126
|
+
{ key: "j", command: { id: "support.next" }, scope: "global" },
|
|
127
|
+
{ key: "J", command: { id: "support.next" }, scope: "global" },
|
|
128
|
+
{ key: "k", command: { id: "support.previous" }, scope: "global" },
|
|
129
|
+
{ key: "K", command: { id: "support.previous" }, scope: "global" },
|
|
130
|
+
{ key: "f", command: { id: "support.filter" }, scope: "global" },
|
|
131
|
+
{ key: "F", command: { id: "support.filter" }, scope: "global" },
|
|
132
|
+
{ key: "CTRL_C", command: { id: "quit" }, scope: "global" }
|
|
133
|
+
],
|
|
134
|
+
onCommand(command, context) {
|
|
135
|
+
if (command.id === "support.language") {
|
|
136
|
+
state.language = state.language === "en" ? "es" : "en";
|
|
137
|
+
setLang(state.language);
|
|
138
|
+
session.update();
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
if (command.id === "support.next") {
|
|
142
|
+
move(1);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
if (command.id === "support.previous") {
|
|
146
|
+
move(-1);
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
if (command.id === "support.filter") {
|
|
150
|
+
state.filter = state.filter === "all" ? "open" : "all";
|
|
151
|
+
clampSelection();
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
if (command.id === "quit") {
|
|
155
|
+
quit();
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
return options.keymap?.onCommand?.(command, context);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
session,
|
|
165
|
+
dispatchKey(key: string) {
|
|
166
|
+
return session.dispatchKey(key);
|
|
167
|
+
},
|
|
168
|
+
output() {
|
|
169
|
+
return session.output();
|
|
170
|
+
},
|
|
171
|
+
ansiOutput() {
|
|
172
|
+
return session.ansiOutput();
|
|
173
|
+
},
|
|
174
|
+
isRunning() {
|
|
175
|
+
return running;
|
|
176
|
+
},
|
|
177
|
+
destroy() {
|
|
178
|
+
running = false;
|
|
179
|
+
setLang(previousLanguage);
|
|
180
|
+
session.destroy();
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (import.meta.main) {
|
|
186
|
+
if (shouldRunSnapshot()) {
|
|
187
|
+
const demo = createModuleTranslateDemo({ runtime: "headless", cols: 92, rows: 18 });
|
|
188
|
+
process.stdout.write(demo.output());
|
|
189
|
+
process.stdout.write("\n");
|
|
190
|
+
demo.destroy();
|
|
191
|
+
} else {
|
|
192
|
+
createModuleTranslateDemo();
|
|
193
|
+
}
|
|
194
|
+
}
|