@hediet/linkrpc-cli 0.0.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.
@@ -0,0 +1,1143 @@
1
+ import { D as formatPrincipalSource, L as validateValueAgainstSchema, S as fetchSchema, T as walkHubDetailed, b as setupSigning, k as connect, x as fetchDefaults } from "./runComplete-BkQwzPbF.js";
2
+ import { a as observableValue, c as Disposable, i as ObservablePromise, o as autorun, r as ObservableLazyPromise, s as derived } from "./cli-D_-vlAa2.js";
3
+ import { n as useObservable, t as useScroll } from "./scroll-BTzAYh4N.js";
4
+ import { ChannelConnector, RpcError, SigningSender } from "@hediet/linkrpc";
5
+ import { isHubEndpoint, openHubChannel } from "@hediet/linkrpc/node";
6
+ import React from "react";
7
+ import { Box, Text, render, useApp, useInput, useStdout } from "ink";
8
+ import TextInput from "ink-text-input";
9
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
+ //#region src/ui/UiModel.ts
11
+ /**
12
+ * View model for the TUI. Owns the live channel and an in-memory schema cache
13
+ * keyed by interface id. Loading state for every async lookup is expressed as
14
+ * an `ObservablePromise` from `@vscode/observables` — no ad-hoc loading flags.
15
+ */
16
+ var UiModel = class extends Disposable {
17
+ _channel;
18
+ /** Initial directory fetch — kicked off in the constructor. */
19
+ servicesPromise;
20
+ /** Non-fatal discovery failures for services that could not be inspected. */
21
+ discoveryWarnings = observableValue("UiModel.discoveryWarnings", []);
22
+ selection = observableValue("UiModel.selection", void 0);
23
+ formValues = observableValue("UiModel.formValues", {});
24
+ /** Index of the currently-focused form field (clamped at render time). */
25
+ formCursor = observableValue("UiModel.formCursor", 0);
26
+ /** True when the focused field is in input mode (text being typed). */
27
+ formEditing = observableValue("UiModel.formEditing", false);
28
+ /** Which Miller column currently owns keyboard focus. */
29
+ focusedColumn = observableValue("UiModel.focusedColumn", 0);
30
+ rawJsonMode = observableValue("UiModel.rawJsonMode", false);
31
+ rawJsonText = observableValue("UiModel.rawJsonText", "{}");
32
+ history = observableValue("UiModel.history", []);
33
+ /**
34
+ * Wraps the in-flight call (if any). `undefined` until the user first
35
+ * submits, then re-assigned on each submit. The view reads
36
+ * `promiseResult` to render loading / result / error.
37
+ */
38
+ lastCall = observableValue("UiModel.lastCall", void 0);
39
+ /**
40
+ * Server→client stream messages received during the in-flight call.
41
+ * Reset to `[]` on each `submit()`, then appended to as `$stream::send`
42
+ * notifications arrive. The view renders these live below the result.
43
+ */
44
+ streamChunks = observableValue("UiModel.streamChunks", []);
45
+ /**
46
+ * One-line, human-readable description of the identity signing outbound
47
+ * calls (e.g. `managed (node abcd012345…)`). Rendered in the header.
48
+ * `undefined` until `runUi` resolves the principal.
49
+ */
50
+ identity = observableValue("UiModel.identity", void 0);
51
+ /**
52
+ * Per-interface schema cache. `ObservableLazyPromise` defers the actual
53
+ * fetch until something asks for it; the autorun below makes "something
54
+ * asks" happen whenever a method on that interface is selected.
55
+ */
56
+ _schemas = /* @__PURE__ */ new Map();
57
+ constructor(_channel) {
58
+ super();
59
+ this._channel = _channel;
60
+ this.servicesPromise = ObservablePromise.fromFn(async () => {
61
+ const result = await loadUiServices(_channel);
62
+ this.discoveryWarnings.set(result.warnings, void 0);
63
+ return result.services;
64
+ });
65
+ this.servicesPromise.promise.catch(() => {});
66
+ this._register(autorun((reader) => {
67
+ const sel = this.selection.read(reader);
68
+ if (!sel) return;
69
+ this._getOrCreateSchema(sel.serviceId, sel.interfaceId, sel.isDefault === true).getPromise().catch(() => {});
70
+ }));
71
+ this._register(autorun((reader) => {
72
+ if (this.focusedColumn.read(reader) !== 1) return;
73
+ const sel = this.selection.read(reader);
74
+ if (!sel || sel.methodName !== void 0) return;
75
+ const methods = this.currentMethods.read(reader);
76
+ if (methods.length === 0) return;
77
+ this.selection.set({
78
+ ...sel,
79
+ methodName: methods[0].name
80
+ }, void 0);
81
+ }));
82
+ }
83
+ currentSchemaState = derived(this, (reader) => {
84
+ const sel = this.selection.read(reader);
85
+ if (!sel) return { kind: "none" };
86
+ const result = this._getOrCreateSchema(sel.serviceId, sel.interfaceId, sel.isDefault === true).cachedPromiseResult.read(reader);
87
+ if (!result) return { kind: "loading" };
88
+ if (result.error) return {
89
+ kind: "error",
90
+ error: result.error
91
+ };
92
+ const schema = result.data;
93
+ const methodSchema = sel.methodName !== void 0 ? schema.methods[sel.methodName] : void 0;
94
+ return {
95
+ kind: "loaded",
96
+ schema,
97
+ method: methodSchema && sel.methodName !== void 0 ? {
98
+ ...methodSchema,
99
+ name: sel.methodName
100
+ } : void 0
101
+ };
102
+ });
103
+ /** Flat list of top-level fields for the currently-selected method. */
104
+ currentFields = derived(this, (reader) => {
105
+ const state = this.currentSchemaState.read(reader);
106
+ if (state.kind !== "loaded" || !state.method) return [];
107
+ const paramsSchema = state.method.params;
108
+ return collectTopLevelFields(paramsSchema, state.schema.components?.schemas ?? {});
109
+ });
110
+ /** All methods on the currently-selected interface (column 2). */
111
+ currentMethods = derived(this, (reader) => {
112
+ const state = this.currentSchemaState.read(reader);
113
+ return state.kind === "loaded" ? Object.entries(state.schema.methods).map(([name, method]) => ({
114
+ name,
115
+ ...method
116
+ })) : [];
117
+ });
118
+ /**
119
+ * Whether the currently-selected method declares stream payloads in
120
+ * either direction. Drives the "streaming" affordance in the view.
121
+ */
122
+ currentMethodStreams = derived(this, (reader) => {
123
+ const state = this.currentSchemaState.read(reader);
124
+ if (state.kind !== "loaded" || !state.method) return {
125
+ server: false,
126
+ client: false
127
+ };
128
+ return {
129
+ server: state.method.serverStream !== void 0,
130
+ client: state.method.clientStream !== void 0
131
+ };
132
+ });
133
+ /** Per-field validation errors against the live method schema. */
134
+ formErrors = derived(this, (reader) => {
135
+ const state = this.currentSchemaState.read(reader);
136
+ const errors = /* @__PURE__ */ new Map();
137
+ if (state.kind !== "loaded" || !state.method) return errors;
138
+ const paramsSchema = state.method.params;
139
+ const components = state.schema.components?.schemas ?? {};
140
+ const resolvedParamsSchema = resolveSchema(paramsSchema, components);
141
+ if (!isObjectSchema(resolvedParamsSchema)) return errors;
142
+ const values = this.formValues.read(reader);
143
+ const required = new Set(resolvedParamsSchema.required ?? []);
144
+ for (const [name, sub] of Object.entries(resolvedParamsSchema.properties)) {
145
+ const value = values[name];
146
+ if (value === void 0) {
147
+ if (required.has(name)) errors.set(name, "required");
148
+ continue;
149
+ }
150
+ const reason = validateValueAgainstSchema(value, sub, components);
151
+ if (reason !== void 0) errors.set(name, reason);
152
+ }
153
+ return errors;
154
+ });
155
+ canSubmit = derived(this, (reader) => {
156
+ const sel = this.selection.read(reader);
157
+ if (!sel || sel.methodName === void 0) return false;
158
+ const state = this.currentSchemaState.read(reader);
159
+ if (state.kind !== "loaded" || !state.method) return false;
160
+ return this.formErrors.read(reader).size === 0;
161
+ });
162
+ select(key) {
163
+ this.selection.set(key, void 0);
164
+ this.formValues.set({}, void 0);
165
+ this.rawJsonText.set("{}", void 0);
166
+ this.formCursor.set(0, void 0);
167
+ this.formEditing.set(false, void 0);
168
+ }
169
+ /** Pick a concrete method on the currently-selected service/interface. */
170
+ selectMethod(methodName) {
171
+ const sel = this.selection.get();
172
+ if (!sel) return;
173
+ this.select({
174
+ ...sel,
175
+ methodName
176
+ });
177
+ }
178
+ focusColumn(column) {
179
+ this.focusedColumn.set(column, void 0);
180
+ }
181
+ /**
182
+ * Move the column-0 cursor by `delta`. The cursor is implicit: it's the
183
+ * row whose `(serviceId, interfaceId)` matches the current selection.
184
+ * Stepping off the ends clamps; methodName is cleared so the methods
185
+ * column re-previews the new service.
186
+ */
187
+ moveServiceCursor(delta) {
188
+ const services = this.servicesPromise.promiseResult.get()?.data ?? [];
189
+ if (services.length === 0) return;
190
+ const sel = this.selection.get();
191
+ const currentIdx = sel ? services.findIndex((s) => s.serviceId === sel.serviceId && s.interfaceId === sel.interfaceId && s.isDefault === true === (sel.isDefault === true)) : -1;
192
+ const next = services[Math.max(0, Math.min(services.length - 1, currentIdx + delta))];
193
+ this.select({
194
+ serviceId: next.serviceId,
195
+ interfaceId: next.interfaceId,
196
+ methodName: void 0,
197
+ ...next.isDefault === true ? { isDefault: true } : {}
198
+ });
199
+ }
200
+ /**
201
+ * Move the column-1 cursor by `delta`. Triggers a full `select()` so the
202
+ * form column starts fresh for the newly-previewed method (Miller-style
203
+ * "each cursor move is a new preview").
204
+ */
205
+ moveMethodCursor(delta) {
206
+ const methods = this.currentMethods.get();
207
+ if (methods.length === 0) return;
208
+ const sel = this.selection.get();
209
+ if (!sel) return;
210
+ const currentIdx = sel.methodName !== void 0 ? methods.findIndex((m) => m.name === sel.methodName) : -1;
211
+ const nextIdx = Math.max(0, Math.min(methods.length - 1, currentIdx + delta));
212
+ this.selectMethod(methods[nextIdx].name);
213
+ }
214
+ setField(name, value) {
215
+ const current = this.formValues.get();
216
+ if (value === void 0) {
217
+ const { [name]: _drop, ...rest } = current;
218
+ this.formValues.set(rest, void 0);
219
+ return;
220
+ }
221
+ this.formValues.set({
222
+ ...current,
223
+ [name]: value
224
+ }, void 0);
225
+ }
226
+ /**
227
+ * Returns the exact `{ method, params }` `submit` would send right
228
+ * now, or `undefined` when no method is fully selected. Lets a host
229
+ * (e.g. the explorer's access-request prompt) preview the concrete
230
+ * call before issuing a one-shot capability bound to it.
231
+ */
232
+ peekPendingCall() {
233
+ const sel = this.selection.get();
234
+ if (!sel || sel.methodName === void 0) return void 0;
235
+ const params = this._collectParams();
236
+ return {
237
+ method: sel.isDefault === true ? sel.methodName : sel.serviceId ? `${sel.serviceId}::${sel.interfaceId}::${sel.methodName}` : `${sel.interfaceId}::${sel.methodName}`,
238
+ params
239
+ };
240
+ }
241
+ submit() {
242
+ const sel = this.selection.get();
243
+ if (!sel || sel.methodName === void 0) return;
244
+ const params = this._collectParams();
245
+ const wireMethod = sel.isDefault === true ? sel.methodName : sel.serviceId ? `${sel.serviceId}::${sel.interfaceId}::${sel.methodName}` : `${sel.interfaceId}::${sel.methodName}`;
246
+ const start = performance.now();
247
+ this.streamChunks.set([], void 0);
248
+ const promise = ObservablePromise.fromFn(async () => {
249
+ return {
250
+ result: await this._channel.sendRequestWithStream(wireMethod, params, { onStreamMessage: (payload) => {
251
+ this.streamChunks.set([...this.streamChunks.get(), payload], void 0);
252
+ } }).result,
253
+ latencyMs: performance.now() - start
254
+ };
255
+ });
256
+ this.lastCall.set(promise, void 0);
257
+ promise.promise.then((outcome) => {
258
+ this.history.set([...this.history.get(), {
259
+ method: wireMethod,
260
+ params,
261
+ result: outcome.result,
262
+ error: void 0,
263
+ latencyMs: outcome.latencyMs,
264
+ timestamp: Date.now()
265
+ }], void 0);
266
+ }, (error) => {
267
+ const latencyMs = performance.now() - start;
268
+ this.history.set([...this.history.get(), {
269
+ method: wireMethod,
270
+ params,
271
+ result: void 0,
272
+ error,
273
+ latencyMs,
274
+ timestamp: Date.now()
275
+ }], void 0);
276
+ });
277
+ }
278
+ _collectParams() {
279
+ if (this.rawJsonMode.get()) {
280
+ const text = this.rawJsonText.get().trim();
281
+ if (text.length === 0) return void 0;
282
+ try {
283
+ return JSON.parse(text);
284
+ } catch {
285
+ return text;
286
+ }
287
+ }
288
+ return this.formValues.get();
289
+ }
290
+ _getOrCreateSchema(serviceId, interfaceId, isDefault) {
291
+ const key = `${isDefault ? "default" : serviceId}::${interfaceId}`;
292
+ let p = this._schemas.get(key);
293
+ if (!p) {
294
+ const match = (this.servicesPromise.promiseResult.get()?.data ?? []).find((s) => s.serviceId === serviceId && s.interfaceId === interfaceId && s.isDefault === true === isDefault);
295
+ const reporter = isDefault ? "" : match?.discoveredFrom ?? serviceId;
296
+ const target = reporter === "" ? void 0 : reporter;
297
+ p = new ObservableLazyPromise(() => fetchSchema(this._channel, interfaceId, match?.hash, target));
298
+ this._schemas.set(key, p);
299
+ }
300
+ return p;
301
+ }
302
+ };
303
+ async function loadUiServices(channel) {
304
+ const [walkResult, defaultsResult] = await Promise.all([walkHubDetailed(channel), fetchDefaults(channel).then((defaults) => ({
305
+ ok: true,
306
+ defaults
307
+ }), (error) => ({
308
+ ok: false,
309
+ error
310
+ }))]);
311
+ const warnings = walkResult.inaccessible.map(({ serviceId, reason }) => `Service "${serviceId}" does not offer a usable hubrpc.directory::list: ${reason}`);
312
+ if (!defaultsResult.ok) {
313
+ warnings.push(`The root service does not offer hubrpc.defaults::get: ${getErrorMessage(defaultsResult.error)}`);
314
+ return {
315
+ services: walkResult.listings,
316
+ warnings
317
+ };
318
+ }
319
+ const { defaults } = defaultsResult;
320
+ if (defaults.interfaceId === void 0) return {
321
+ services: walkResult.listings,
322
+ warnings
323
+ };
324
+ return {
325
+ services: [{
326
+ serviceId: "",
327
+ interfaceId: defaults.interfaceId,
328
+ ...defaults.hash === void 0 ? {} : { hash: defaults.hash },
329
+ discoveredFrom: "",
330
+ isDefault: true
331
+ }, ...walkResult.listings],
332
+ warnings
333
+ };
334
+ }
335
+ function getErrorMessage(error) {
336
+ return error instanceof Error ? error.message : String(error);
337
+ }
338
+ function collectTopLevelFields(schema, components) {
339
+ const resolved = resolveSchema(schema, components);
340
+ if (!isObjectSchema(resolved)) return [];
341
+ const required = new Set(resolved.required ?? []);
342
+ return Object.entries(resolved.properties).map(([name, sub]) => ({
343
+ name,
344
+ required: required.has(name),
345
+ schema: resolveSchema(sub, components) ?? sub
346
+ }));
347
+ }
348
+ function resolveSchema(schema, components, seen = /* @__PURE__ */ new Set()) {
349
+ if (schema === void 0 || typeof schema !== "object" || !("$ref" in schema)) return schema;
350
+ if (!schema.$ref.startsWith("#/components/schemas/") || seen.has(schema.$ref)) return schema;
351
+ const target = components[schema.$ref.slice(21)];
352
+ if (target === void 0) return schema;
353
+ return resolveSchema(target, components, /* @__PURE__ */ new Set([...seen, schema.$ref]));
354
+ }
355
+ function isObjectSchema(s) {
356
+ return !!s && typeof s === "object" && "type" in s && s.type === "object" && "properties" in s;
357
+ }
358
+ //#endregion
359
+ //#region src/ui/schemaInspect.ts
360
+ function classifyField(schema) {
361
+ if (typeof schema === "boolean") return { kind: "json" };
362
+ if ("enum" in schema) return {
363
+ kind: "enum",
364
+ enumValues: schema.enum
365
+ };
366
+ if ("const" in schema) return {
367
+ kind: "enum",
368
+ enumValues: [schema.const]
369
+ };
370
+ if ("anyOf" in schema) {
371
+ const values = [];
372
+ for (const branch of schema.anyOf) if (typeof branch === "object" && "const" in branch) values.push(branch.const);
373
+ else if (typeof branch === "object" && "enum" in branch) values.push(...branch.enum);
374
+ else return { kind: "json" };
375
+ return {
376
+ kind: "enum",
377
+ enumValues: values
378
+ };
379
+ }
380
+ if ("type" in schema && typeof schema.type === "string") switch (schema.type) {
381
+ case "string": return { kind: "string" };
382
+ case "number": return { kind: "number" };
383
+ case "integer": return { kind: "integer" };
384
+ case "boolean": return { kind: "boolean" };
385
+ default: return { kind: "json" };
386
+ }
387
+ return { kind: "json" };
388
+ }
389
+ /** A sensible default value for a freshly-focused field, by kind. */
390
+ function defaultValueFor(c) {
391
+ switch (c.kind) {
392
+ case "string": return "";
393
+ case "number":
394
+ case "integer": return 0;
395
+ case "boolean": return false;
396
+ case "enum": return c.enumValues?.[0] ?? null;
397
+ case "json": return null;
398
+ }
399
+ }
400
+ /**
401
+ * Move to the next/previous value in an enum field. Wraps at the ends. The
402
+ * cycle is what powers `←`/`→` on enum fields without needing edit mode.
403
+ */
404
+ function cycleEnum(values, current, delta) {
405
+ if (values.length === 0) return current;
406
+ const idx = values.findIndex((v) => deepEqual(v, current));
407
+ return values[(idx < 0 ? 0 : idx + delta + values.length) % values.length];
408
+ }
409
+ function deepEqual(a, b) {
410
+ if (a === b) return true;
411
+ if (typeof a !== typeof b) return false;
412
+ if (a === null || b === null) return false;
413
+ if (typeof a !== "object") return false;
414
+ return JSON.stringify(a) === JSON.stringify(b);
415
+ }
416
+ /** Parse a user-typed string into a value compatible with the given kind. */
417
+ function parseTypedValue(raw, kind) {
418
+ switch (kind) {
419
+ case "string": return {
420
+ ok: true,
421
+ value: raw
422
+ };
423
+ case "number": {
424
+ if (raw.trim() === "") return {
425
+ ok: true,
426
+ value: void 0
427
+ };
428
+ const n = Number(raw);
429
+ if (!Number.isFinite(n)) return {
430
+ ok: false,
431
+ error: `not a number: ${raw}`
432
+ };
433
+ return {
434
+ ok: true,
435
+ value: n
436
+ };
437
+ }
438
+ case "integer": {
439
+ if (raw.trim() === "") return {
440
+ ok: true,
441
+ value: void 0
442
+ };
443
+ const n = Number(raw);
444
+ if (!Number.isInteger(n)) return {
445
+ ok: false,
446
+ error: `not an integer: ${raw}`
447
+ };
448
+ return {
449
+ ok: true,
450
+ value: n
451
+ };
452
+ }
453
+ case "json":
454
+ if (raw.trim() === "") return {
455
+ ok: true,
456
+ value: void 0
457
+ };
458
+ try {
459
+ return {
460
+ ok: true,
461
+ value: JSON.parse(raw)
462
+ };
463
+ } catch (e) {
464
+ return {
465
+ ok: false,
466
+ error: e.message
467
+ };
468
+ }
469
+ case "boolean":
470
+ case "enum": return {
471
+ ok: false,
472
+ error: "not editable via text input"
473
+ };
474
+ }
475
+ }
476
+ /** A short human description of the schema, shown in dim text after the value. */
477
+ function describeSchema(s) {
478
+ if (typeof s === "boolean") return s ? "any" : "never";
479
+ if ("type" in s && typeof s.type === "string") return s.type;
480
+ if ("enum" in s) return `enum(${s.enum.map((v) => JSON.stringify(v)).join(" | ")})`;
481
+ if ("const" in s) return `const ${JSON.stringify(s.const)}`;
482
+ if ("anyOf" in s) return s.anyOf.map(describeSchema).join(" | ");
483
+ if ("$ref" in s) return s.$ref;
484
+ return "?";
485
+ }
486
+ //#endregion
487
+ //#region src/ui/FieldRow.tsx
488
+ const NAME_W = 12;
489
+ const VALUE_W = 24;
490
+ /**
491
+ * One row of the method form. To keep ink/yoga happy across resizes, the
492
+ * non-editing path renders the whole row as a single `<Text>` with nested
493
+ * colored spans, instead of multiple Boxes that try to share the column's
494
+ * width. Boxes-around-Text in row direction tend to collapse to the
495
+ * narrowest measurement on the first paint — single-Text rows lay out
496
+ * predictably.
497
+ *
498
+ * The editing path swaps the value column for a `TextInput`; the surrounding
499
+ * row stays a Box because TextInput is a component, not a string.
500
+ */
501
+ const FieldRow = ({ model, field, value, focused, editing, error }) => {
502
+ const cls = classifyField(field.schema);
503
+ const cursor = focused ? "› " : " ";
504
+ const namePadded = pad(field.name, NAME_W);
505
+ const optionalMark = field.required ? "" : "?";
506
+ const typeLabel = describeSchema(field.schema);
507
+ return /* @__PURE__ */ jsxs(Box, {
508
+ flexDirection: "column",
509
+ flexShrink: 0,
510
+ children: [editing ? /* @__PURE__ */ jsxs(Box, {
511
+ flexDirection: "row",
512
+ flexShrink: 0,
513
+ children: [
514
+ /* @__PURE__ */ jsx(Text, {
515
+ color: focused ? "cyan" : void 0,
516
+ children: cursor
517
+ }),
518
+ /* @__PURE__ */ jsx(Text, {
519
+ bold: true,
520
+ children: namePadded
521
+ }),
522
+ /* @__PURE__ */ jsx(Text, {
523
+ dimColor: true,
524
+ children: optionalMark === "" ? "" : optionalMark + " "
525
+ }),
526
+ /* @__PURE__ */ jsx(Box, {
527
+ flexGrow: 1,
528
+ flexShrink: 1,
529
+ children: /* @__PURE__ */ jsx(TextEditor, {
530
+ model,
531
+ field,
532
+ value,
533
+ cls
534
+ })
535
+ }),
536
+ /* @__PURE__ */ jsxs(Text, {
537
+ dimColor: true,
538
+ children: [" ", typeLabel]
539
+ })
540
+ ]
541
+ }) : /* @__PURE__ */ jsxs(Text, { children: [
542
+ /* @__PURE__ */ jsx(Text, {
543
+ color: focused ? "cyan" : void 0,
544
+ children: cursor
545
+ }),
546
+ /* @__PURE__ */ jsx(Text, {
547
+ bold: true,
548
+ children: namePadded
549
+ }),
550
+ /* @__PURE__ */ jsx(Text, {
551
+ dimColor: true,
552
+ children: optionalMark + " "
553
+ }),
554
+ /* @__PURE__ */ jsx(Text, {
555
+ color: "yellow",
556
+ children: pad(formatValue(value, cls), VALUE_W)
557
+ }),
558
+ /* @__PURE__ */ jsxs(Text, {
559
+ dimColor: true,
560
+ children: [" ", typeLabel]
561
+ })
562
+ ] }), error ? /* @__PURE__ */ jsx(Box, {
563
+ marginLeft: 14,
564
+ flexShrink: 0,
565
+ children: /* @__PURE__ */ jsxs(Text, {
566
+ color: "red",
567
+ children: ["! ", error]
568
+ })
569
+ }) : null]
570
+ });
571
+ };
572
+ const TextEditor = ({ model, field, value, cls }) => {
573
+ const initial = textForEditing(value, cls.kind);
574
+ const [draft, setDraft] = React.useState(initial);
575
+ React.useEffect(() => {
576
+ setDraft(initial);
577
+ }, [initial]);
578
+ return /* @__PURE__ */ jsx(TextInput, {
579
+ value: draft,
580
+ onChange: setDraft,
581
+ onSubmit: (submitted) => {
582
+ const parsed = parseTypedValue(submitted, cls.kind);
583
+ if (parsed.ok) {
584
+ model.setField(field.name, parsed.value);
585
+ model.formEditing.set(false, void 0);
586
+ }
587
+ },
588
+ focus: true
589
+ });
590
+ };
591
+ function textForEditing(value, kind) {
592
+ if (value === void 0) return "";
593
+ if (kind === "string") return typeof value === "string" ? value : JSON.stringify(value);
594
+ if (kind === "json") return JSON.stringify(value);
595
+ return String(value);
596
+ }
597
+ function formatValue(value, cls) {
598
+ if (value === void 0) return "(unset)";
599
+ switch (cls.kind) {
600
+ case "boolean": return value ? "[x]" : "[ ]";
601
+ case "enum": return `< ${stringifyEnum(value)} >`;
602
+ case "string": return typeof value === "string" ? value : JSON.stringify(value);
603
+ case "json": {
604
+ const text = JSON.stringify(value);
605
+ return text.length > 40 ? text.slice(0, 37) + "..." : text;
606
+ }
607
+ default: return JSON.stringify(value);
608
+ }
609
+ }
610
+ function stringifyEnum(value) {
611
+ return typeof value === "string" ? value : JSON.stringify(value);
612
+ }
613
+ /** Pad to `width` with spaces, or truncate with `…` if too long. */
614
+ function pad(text, width) {
615
+ if (text.length === width) return text;
616
+ if (text.length < width) return text + " ".repeat(width - text.length);
617
+ return text.slice(0, width - 1) + "…";
618
+ }
619
+ //#endregion
620
+ //#region src/ui/App.tsx
621
+ const IDENTITY_HEADER_H = 1;
622
+ const FOOTER_H = 1;
623
+ const RESULT_H = 8;
624
+ /** Column chrome that is not list rows: top border + bottom border + title. */
625
+ const COLUMN_CHROME_H = 3;
626
+ /** Re-render on terminal resize so the height math tracks the live row count. */
627
+ function useTerminalRows() {
628
+ const { stdout } = useStdout();
629
+ const [rows, setRows] = React.useState(stdout.rows ?? 30);
630
+ React.useEffect(() => {
631
+ const onResize = () => setRows(stdout.rows ?? 30);
632
+ stdout.on("resize", onResize);
633
+ return () => {
634
+ stdout.off("resize", onResize);
635
+ };
636
+ }, [stdout]);
637
+ return rows;
638
+ }
639
+ /**
640
+ * Miller-column TUI: three columns left → right (services, methods, form)
641
+ * plus a result pane at the bottom. ←/→ moves focus between columns, ↑/↓
642
+ * moves the cursor inside the focused column. Each cursor move in columns
643
+ * 0 / 1 immediately previews into the column to its right.
644
+ */
645
+ const App = ({ model }) => {
646
+ const focusedColumn = useObservable(model.focusedColumn);
647
+ const editing = useObservable(model.formEditing);
648
+ const discoveryWarnings = useObservable(model.discoveryWarnings);
649
+ const termRows = useTerminalRows();
650
+ const { exit } = useApp();
651
+ useInput((input, key) => {
652
+ if (editing) return;
653
+ if (input === "q") {
654
+ exit();
655
+ return;
656
+ }
657
+ if (key.leftArrow && focusedColumn > 0) {
658
+ model.focusColumn(focusedColumn - 1);
659
+ return;
660
+ }
661
+ if (key.rightArrow && focusedColumn < 2) {
662
+ model.focusColumn(focusedColumn + 1);
663
+ return;
664
+ }
665
+ });
666
+ const headerHeight = IDENTITY_HEADER_H + discoveryWarnings.length;
667
+ const bodyHeight = Math.max(4, termRows - headerHeight - RESULT_H - FOOTER_H);
668
+ const listHeight = Math.max(1, bodyHeight - COLUMN_CHROME_H);
669
+ return /* @__PURE__ */ jsxs(Box, {
670
+ flexDirection: "column",
671
+ height: termRows,
672
+ children: [
673
+ /* @__PURE__ */ jsx(Header, {
674
+ model,
675
+ warnings: discoveryWarnings
676
+ }),
677
+ /* @__PURE__ */ jsxs(Box, {
678
+ height: bodyHeight,
679
+ children: [
680
+ /* @__PURE__ */ jsx(ServicesColumn, {
681
+ model,
682
+ focused: focusedColumn === 0,
683
+ height: bodyHeight,
684
+ listHeight
685
+ }),
686
+ /* @__PURE__ */ jsx(MethodsColumn, {
687
+ model,
688
+ focused: focusedColumn === 1,
689
+ height: bodyHeight,
690
+ listHeight
691
+ }),
692
+ /* @__PURE__ */ jsx(FormColumn, {
693
+ model,
694
+ focused: focusedColumn === 2,
695
+ height: bodyHeight,
696
+ listHeight
697
+ })
698
+ ]
699
+ }),
700
+ /* @__PURE__ */ jsx(ResultPane, { model }),
701
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(Text, {
702
+ dimColor: true,
703
+ children: "←/→: switch column | ↑/↓: move | enter: edit / submit | esc: cancel edit | q: quit"
704
+ }) })
705
+ ]
706
+ });
707
+ };
708
+ /** "▲ N more" / "▼ N more" indicator row, shown only when items are hidden. */
709
+ const ScrollIndicator = ({ direction, count }) => {
710
+ if (count <= 0) return null;
711
+ return /* @__PURE__ */ jsx(Text, {
712
+ dimColor: true,
713
+ children: ` ${direction === "up" ? "▲" : "▼"} ${count} more`
714
+ });
715
+ };
716
+ const Header = ({ model, warnings }) => {
717
+ const identity = useObservable(model.identity);
718
+ return /* @__PURE__ */ jsxs(Box, {
719
+ flexDirection: "column",
720
+ children: [/* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
721
+ dimColor: true,
722
+ children: "identity: "
723
+ }), /* @__PURE__ */ jsx(Text, { children: identity ?? "(resolving…)" })] }), warnings.map((warning) => /* @__PURE__ */ jsxs(Text, {
724
+ color: "yellow",
725
+ wrap: "truncate-end",
726
+ children: ["! ", warning]
727
+ }, warning))]
728
+ });
729
+ };
730
+ const ServicesColumn = ({ model, focused, height, listHeight }) => {
731
+ const result = useObservable(model.servicesPromise.promiseResult);
732
+ const selection = useObservable(model.selection);
733
+ useInput((_input, key) => {
734
+ if (key.upArrow) model.moveServiceCursor(-1);
735
+ else if (key.downArrow) model.moveServiceCursor(1);
736
+ else if (key.return) model.focusColumn(1);
737
+ }, { isActive: focused });
738
+ const services = !result || result.error ? [] : result.data ?? [];
739
+ const cursorIdx = selection ? services.findIndex((s) => s.serviceId === selection.serviceId && s.interfaceId === selection.interfaceId && s.isDefault === true === (selection.isDefault === true)) : -1;
740
+ const win = useScroll(services.length, Math.max(0, cursorIdx), listHeight);
741
+ return /* @__PURE__ */ jsx(Column, {
742
+ title: "Services",
743
+ focused,
744
+ width: "25%",
745
+ height,
746
+ children: !result ? /* @__PURE__ */ jsx(Text, {
747
+ dimColor: true,
748
+ children: "Loading…"
749
+ }) : result.error ? /* @__PURE__ */ jsxs(Text, {
750
+ color: "red",
751
+ children: ["Error: ", String(result.error)]
752
+ }) : services.length === 0 ? /* @__PURE__ */ jsx(Text, {
753
+ dimColor: true,
754
+ children: "(no services)"
755
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [
756
+ /* @__PURE__ */ jsx(ScrollIndicator, {
757
+ direction: "up",
758
+ count: win.above
759
+ }),
760
+ services.slice(win.start, win.end).map((s) => {
761
+ const isCursor = !!selection && selection.serviceId === s.serviceId && selection.interfaceId === s.interfaceId && selection.isDefault === true === (s.isDefault === true);
762
+ return /* @__PURE__ */ jsxs(Text, {
763
+ color: isCursor ? focused ? "cyan" : "white" : void 0,
764
+ wrap: "truncate-end",
765
+ children: [(isCursor ? "› " : " ") + (s.isDefault === true ? "default " : s.serviceId ? `${s.serviceId} ` : ""), /* @__PURE__ */ jsx(Text, {
766
+ dimColor: true,
767
+ children: s.interfaceId
768
+ })]
769
+ }, `${s.isDefault === true ? "default" : s.serviceId}/${s.interfaceId}`);
770
+ }),
771
+ /* @__PURE__ */ jsx(ScrollIndicator, {
772
+ direction: "down",
773
+ count: win.below
774
+ })
775
+ ] })
776
+ });
777
+ };
778
+ const MethodsColumn = ({ model, focused, height, listHeight }) => {
779
+ const selection = useObservable(model.selection);
780
+ const schemaState = useObservable(model.currentSchemaState);
781
+ const methods = useObservable(model.currentMethods);
782
+ useInput((_input, key) => {
783
+ if (key.upArrow) model.moveMethodCursor(-1);
784
+ else if (key.downArrow) model.moveMethodCursor(1);
785
+ else if (key.return) model.focusColumn(2);
786
+ }, { isActive: focused });
787
+ const cursorIdx = selection?.methodName !== void 0 ? methods.findIndex((m) => m.name === selection.methodName) : -1;
788
+ const win = useScroll(methods.length, Math.max(0, cursorIdx), listHeight);
789
+ return /* @__PURE__ */ jsx(Column, {
790
+ title: "Methods",
791
+ focused,
792
+ width: "25%",
793
+ height,
794
+ children: !selection ? /* @__PURE__ */ jsx(Text, {
795
+ dimColor: true,
796
+ children: "(select a service)"
797
+ }) : schemaState.kind === "loading" ? /* @__PURE__ */ jsx(Text, {
798
+ dimColor: true,
799
+ children: "Loading…"
800
+ }) : schemaState.kind === "error" ? /* @__PURE__ */ jsxs(Text, {
801
+ color: "red",
802
+ children: ["Error: ", String(schemaState.error)]
803
+ }) : methods.length === 0 ? /* @__PURE__ */ jsx(Text, {
804
+ dimColor: true,
805
+ children: "(no methods)"
806
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [
807
+ /* @__PURE__ */ jsx(ScrollIndicator, {
808
+ direction: "up",
809
+ count: win.above
810
+ }),
811
+ methods.slice(win.start, win.end).map((m) => {
812
+ const isCursor = selection.methodName === m.name;
813
+ return /* @__PURE__ */ jsxs(Text, {
814
+ color: isCursor ? focused ? "cyan" : "white" : void 0,
815
+ wrap: "truncate-end",
816
+ children: [
817
+ isCursor ? "› " : " ",
818
+ /* @__PURE__ */ jsx(Text, {
819
+ dimColor: true,
820
+ children: m.result === void 0 ? "notify " : "req "
821
+ }),
822
+ m.name,
823
+ m.serverStream !== void 0 ? /* @__PURE__ */ jsx(Text, {
824
+ color: "magenta",
825
+ children: " ↓stream"
826
+ }) : null
827
+ ]
828
+ }, m.name);
829
+ }),
830
+ /* @__PURE__ */ jsx(ScrollIndicator, {
831
+ direction: "down",
832
+ count: win.below
833
+ })
834
+ ] })
835
+ });
836
+ };
837
+ const FormColumn = ({ model, focused, height, listHeight }) => {
838
+ const selection = useObservable(model.selection);
839
+ const schemaState = useObservable(model.currentSchemaState);
840
+ return /* @__PURE__ */ jsx(Column, {
841
+ title: "Form",
842
+ focused,
843
+ flexGrow: 1,
844
+ height,
845
+ children: !selection ? /* @__PURE__ */ jsx(Text, {
846
+ dimColor: true,
847
+ children: "(select a service first)"
848
+ }) : selection.methodName === void 0 ? /* @__PURE__ */ jsx(Text, {
849
+ dimColor: true,
850
+ children: "(select a method first)"
851
+ }) : schemaState.kind === "loading" ? /* @__PURE__ */ jsx(Text, {
852
+ dimColor: true,
853
+ children: "Loading…"
854
+ }) : schemaState.kind === "loaded" && schemaState.method ? /* @__PURE__ */ jsx(MethodForm, {
855
+ model,
856
+ method: schemaState.method,
857
+ focused,
858
+ listHeight
859
+ }) : schemaState.kind === "error" ? /* @__PURE__ */ jsxs(Text, {
860
+ color: "red",
861
+ children: ["Error: ", String(schemaState.error)]
862
+ }) : /* @__PURE__ */ jsxs(Text, {
863
+ color: "red",
864
+ children: [
865
+ "Method \"",
866
+ selection.methodName,
867
+ "\" not in schema"
868
+ ]
869
+ })
870
+ });
871
+ };
872
+ const MethodForm = ({ model, method, focused, listHeight }) => {
873
+ const fields = useObservable(model.currentFields);
874
+ const formValues = useObservable(model.formValues);
875
+ const errors = useObservable(model.formErrors);
876
+ const canSubmit = useObservable(model.canSubmit);
877
+ const cursor = useObservable(model.formCursor);
878
+ const editing = useObservable(model.formEditing);
879
+ const submitRowIdx = fields.length;
880
+ const safeCursor = Math.max(0, Math.min(submitRowIdx, cursor));
881
+ const focusedField = safeCursor < submitRowIdx ? fields[safeCursor] : void 0;
882
+ const headerLines = 1 + (method.summary ? 1 : 0) + 1;
883
+ const fieldsHeight = Math.max(1, listHeight - headerLines - 2);
884
+ const win = useScroll(fields.length, Math.min(safeCursor, Math.max(0, fields.length - 1)), fieldsHeight);
885
+ useInput((input, key) => {
886
+ if (key.upArrow) {
887
+ model.formCursor.set(Math.max(0, safeCursor - 1), void 0);
888
+ return;
889
+ }
890
+ if (key.downArrow) {
891
+ model.formCursor.set(Math.min(submitRowIdx, safeCursor + 1), void 0);
892
+ return;
893
+ }
894
+ if (focusedField) {
895
+ const cls = classifyField(focusedField.schema);
896
+ const current = formValues[focusedField.name];
897
+ if (cls.kind === "boolean" && input === " ") {
898
+ model.setField(focusedField.name, !current);
899
+ return;
900
+ }
901
+ if (cls.kind === "enum") {
902
+ if (key.leftArrow || key.rightArrow) {
903
+ const dir = key.rightArrow ? 1 : -1;
904
+ const seed = current === void 0 ? defaultValueFor(cls) : current;
905
+ model.setField(focusedField.name, cycleEnum(cls.enumValues ?? [], seed, dir));
906
+ return;
907
+ }
908
+ }
909
+ if (key.return) {
910
+ if (cls.kind === "boolean") {
911
+ model.setField(focusedField.name, !current);
912
+ return;
913
+ }
914
+ if (cls.kind === "enum") {
915
+ const seed = current === void 0 ? defaultValueFor(cls) : current;
916
+ model.setField(focusedField.name, cycleEnum(cls.enumValues ?? [], seed, 1));
917
+ return;
918
+ }
919
+ if (current === void 0) model.setField(focusedField.name, defaultValueFor(cls));
920
+ model.formEditing.set(true, void 0);
921
+ return;
922
+ }
923
+ if (input === "x" && focusedField.required === false && current !== void 0) {
924
+ model.setField(focusedField.name, void 0);
925
+ return;
926
+ }
927
+ } else if (safeCursor === submitRowIdx && key.return) {
928
+ if (canSubmit) model.submit();
929
+ return;
930
+ }
931
+ }, { isActive: focused && !editing });
932
+ useInput((_input, key) => {
933
+ if (key.escape) model.formEditing.set(false, void 0);
934
+ }, { isActive: focused && editing });
935
+ return /* @__PURE__ */ jsxs(Box, {
936
+ flexDirection: "column",
937
+ flexShrink: 0,
938
+ children: [
939
+ /* @__PURE__ */ jsx(Text, {
940
+ bold: true,
941
+ children: (method.result === void 0 ? "notify " : "request ") + selection?.methodName
942
+ }),
943
+ method.summary ? /* @__PURE__ */ jsx(Text, {
944
+ dimColor: true,
945
+ children: method.summary
946
+ }) : null,
947
+ /* @__PURE__ */ jsx(Box, {
948
+ marginTop: 1,
949
+ flexDirection: "column",
950
+ flexShrink: 0,
951
+ children: fields.length === 0 ? /* @__PURE__ */ jsx(Text, {
952
+ dimColor: true,
953
+ children: "(no parameters)"
954
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [
955
+ /* @__PURE__ */ jsx(ScrollIndicator, {
956
+ direction: "up",
957
+ count: win.above
958
+ }),
959
+ fields.slice(win.start, win.end).map((f, i) => {
960
+ const idx = win.start + i;
961
+ return /* @__PURE__ */ jsx(FieldRow, {
962
+ model,
963
+ field: f,
964
+ value: formValues[f.name],
965
+ focused: focused && safeCursor === idx,
966
+ editing: editing && safeCursor === idx,
967
+ error: errors.get(f.name)
968
+ }, f.name);
969
+ }),
970
+ /* @__PURE__ */ jsx(ScrollIndicator, {
971
+ direction: "down",
972
+ count: win.below
973
+ })
974
+ ] })
975
+ }),
976
+ /* @__PURE__ */ jsx(Box, {
977
+ marginTop: 1,
978
+ flexShrink: 0,
979
+ children: /* @__PURE__ */ jsx(Text, {
980
+ color: canSubmit ? "green" : "gray",
981
+ children: submitLabel(focused && safeCursor === submitRowIdx, canSubmit)
982
+ })
983
+ })
984
+ ]
985
+ });
986
+ };
987
+ function submitLabel(focused, canSubmit) {
988
+ return `${focused ? "› " : " "}[Submit${canSubmit ? "" : " — fix errors first"}]`;
989
+ }
990
+ const Column = ({ title, focused, children, width, flexGrow, height }) => {
991
+ return /* @__PURE__ */ jsxs(Box, {
992
+ flexDirection: "column",
993
+ width,
994
+ flexGrow,
995
+ height,
996
+ overflow: "hidden",
997
+ borderStyle: "single",
998
+ borderColor: focused ? "cyan" : void 0,
999
+ paddingX: 1,
1000
+ children: [/* @__PURE__ */ jsx(Text, {
1001
+ bold: true,
1002
+ children: title
1003
+ }), children]
1004
+ });
1005
+ };
1006
+ const ResultPane = ({ model }) => {
1007
+ const promise = useObservable(model.lastCall);
1008
+ const result = useObservable(promise ? promise.promiseResult : NO_RESULT);
1009
+ const chunks = useObservable(model.streamChunks);
1010
+ return /* @__PURE__ */ jsxs(Box, {
1011
+ flexDirection: "column",
1012
+ height: RESULT_H,
1013
+ overflow: "hidden",
1014
+ borderStyle: "single",
1015
+ paddingX: 1,
1016
+ children: [
1017
+ /* @__PURE__ */ jsx(Text, {
1018
+ bold: true,
1019
+ children: "Result"
1020
+ }),
1021
+ chunks.length > 0 ? /* @__PURE__ */ jsx(Box, {
1022
+ flexDirection: "column",
1023
+ children: chunks.map((c, i) => /* @__PURE__ */ jsx(Text, {
1024
+ dimColor: true,
1025
+ children: "│ " + formatChunk(c)
1026
+ }, i))
1027
+ }) : null,
1028
+ !promise ? /* @__PURE__ */ jsx(Text, {
1029
+ dimColor: true,
1030
+ children: "(no calls yet)"
1031
+ }) : !result ? /* @__PURE__ */ jsx(Text, {
1032
+ dimColor: true,
1033
+ children: chunks.length > 0 ? "Streaming…" : "Calling…"
1034
+ }) : result.error ? /* @__PURE__ */ jsxs(Text, {
1035
+ color: "red",
1036
+ children: ["Error: ", formatError(result.error)]
1037
+ }) : /* @__PURE__ */ jsxs(Text, { children: [
1038
+ JSON.stringify(result.data?.result),
1039
+ " ",
1040
+ /* @__PURE__ */ jsxs(Text, {
1041
+ dimColor: true,
1042
+ children: [result.data?.latencyMs.toFixed(1), "ms"]
1043
+ })
1044
+ ] })
1045
+ ]
1046
+ });
1047
+ };
1048
+ const NO_RESULT = observableValue("App.NO_RESULT", void 0);
1049
+ function formatChunk(c) {
1050
+ return typeof c === "string" ? c : JSON.stringify(c);
1051
+ }
1052
+ function formatError(e) {
1053
+ if (e instanceof Error) return e.message;
1054
+ return String(e);
1055
+ }
1056
+ //#endregion
1057
+ //#region src/ui/runUi.tsx
1058
+ async function runUi(opts) {
1059
+ if (isHubEndpoint(opts.endpoint)) await _runUiReconnecting(opts.endpoint, opts.principalSpec);
1060
+ else await _runUiOnce(opts.endpoint, opts.principalSpec);
1061
+ }
1062
+ /** Non-Hub endpoints: a single connection, no redial. */
1063
+ async function _runUiOnce(endpoint, principalSpec) {
1064
+ const conn = await connect(endpoint);
1065
+ const identity = _isRawEndpoint(endpoint) ? "unsigned (raw endpoint)" : _formatIdentity(await setupSigning(conn.channel, conn.signing, principalSpec, { negotiateHubCaps: false }));
1066
+ const model = new UiModel(conn.channel);
1067
+ model.identity.set(identity, void 0);
1068
+ const instance = render(/* @__PURE__ */ jsx(App, { model }));
1069
+ try {
1070
+ await instance.waitUntilExit();
1071
+ } finally {
1072
+ model.dispose();
1073
+ conn.close();
1074
+ }
1075
+ }
1076
+ function _isRawEndpoint(endpoint) {
1077
+ return endpoint.kind === "ws-no-init" || endpoint.kind === "socket" && endpoint.brokerMode === "raw";
1078
+ }
1079
+ /**
1080
+ * Hub / env connection: keep the UI alive across socket drops. A stable
1081
+ * {@link SwappableSender} backs the {@link UiModel}; on every (re)connect we
1082
+ * open a fresh hub channel, install signing, and point the swappable sender at
1083
+ * the new signed channel. The TUI is rendered once, after the first connect.
1084
+ */
1085
+ async function _runUiReconnecting(endpoint, principalSpec) {
1086
+ const swappable = new SwappableSender();
1087
+ let model;
1088
+ const hubEndpoint = endpoint.kind === "ws" ? endpoint.url : endpoint.path;
1089
+ const handle = ChannelConnector.expBackoff(() => openHubChannel({
1090
+ endpoint: hubEndpoint,
1091
+ token: endpoint.token ?? ""
1092
+ })).keepConnected(async ({ channel }) => {
1093
+ const signing = {};
1094
+ const signed = SigningSender.wrapChannel(channel, signing).sender;
1095
+ const session = await setupSigning(signed, signing, principalSpec, { negotiateHubCaps: true });
1096
+ swappable.setTarget(signed);
1097
+ if (!model) {
1098
+ model = new UiModel(swappable);
1099
+ model.identity.set(_formatIdentity(session), void 0);
1100
+ render(/* @__PURE__ */ jsx(App, { model })).waitUntilExit().finally(() => handle.stop());
1101
+ } else model.identity.set(_formatIdentity(session), void 0);
1102
+ });
1103
+ try {
1104
+ await handle.done;
1105
+ } finally {
1106
+ model?.dispose();
1107
+ }
1108
+ }
1109
+ /** One-line identity label for the TUI header (source + truncated nodeId). */
1110
+ function _formatIdentity(session) {
1111
+ return formatPrincipalSource(session.principalSource, session.principal.identity.publicSigningIdentity.principal);
1112
+ }
1113
+ /**
1114
+ * An {@link IRequestSender} whose delegate can be swapped at runtime. Lets the
1115
+ * {@link UiModel} hold one stable channel reference while the underlying signed
1116
+ * channel is replaced on each reconnect. Calls issued while disconnected reject.
1117
+ */
1118
+ var SwappableSender = class {
1119
+ _target;
1120
+ setTarget(target) {
1121
+ this._target = target;
1122
+ }
1123
+ _require() {
1124
+ if (!this._target) throw new RpcError("not connected", -32e3);
1125
+ return this._target;
1126
+ }
1127
+ sendRequest(method, params, opts) {
1128
+ return this._require().sendRequest(method, params, opts);
1129
+ }
1130
+ sendNotification(method, params, opts) {
1131
+ return this._require().sendNotification(method, params, opts);
1132
+ }
1133
+ sendRequestWithStream(method, params, opts) {
1134
+ return this._require().sendRequestWithStream(method, params, opts);
1135
+ }
1136
+ close() {
1137
+ this._target?.close();
1138
+ }
1139
+ };
1140
+ //#endregion
1141
+ export { runUi };
1142
+
1143
+ //# sourceMappingURL=runUi-D-8LMU4F.js.map