@hediet/linkrpc-cli 0.0.3-next.20260922.2 → 0.0.3-next.20260924.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.
@@ -1,12 +1,292 @@
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-Du5wmOdB.js";
2
- import { a as observableValue, c as Disposable, i as ObservablePromise, o as autorun, r as ObservableLazyPromise, s as derived } from "./cli-FDKN1ydZ.js";
3
- import { n as useObservable, t as useScroll } from "./scroll--AVfTs5W.js";
1
+ import { D as formatPrincipalSource, L as validateValueAgainstSchema, S as fetchSchema, T as walkHubDetailed, b as setupSigning, k as connect, r as withStaticHubReflection, x as fetchDefaults } from "./runComplete-F95L0neY.js";
2
+ import { a as formatCommandLegend, c as safeTerminalText, d as interfaceTarget, f as matchesTarget, i as dispatchHostKey, l as resolveViewTarget, n as views, o as hostCommands, p as targetId, r as ViewFrame, s as matchesKey, u as formatCondition } from "./cli-CY5WVWEQ.js";
3
+ import { a as derived, i as autorun, n as ObservablePromise, o as Disposable, r as observableValue, t as ObservableLazyPromise } from "./dist-BOcSU4-O.js";
4
+ import { n as useObservable, t as useScroll } from "./scroll-CY9Rcshb.js";
4
5
  import { ChannelConnector, RpcError, SigningSender } from "@hediet/linkrpc";
5
6
  import { isHubEndpoint, openHubChannel } from "@hediet/linkrpc/node";
7
+ import { stdin } from "node:process";
8
+ import { emitKeypressEvents } from "node:readline";
6
9
  import React from "react";
7
- import { Box, Text, render, useApp, useInput, useStdout } from "ink";
8
- import TextInput from "ink-text-input";
10
+ import { Box, Text, measureElement, render, useApp, useInput, useStdout } from "ink";
9
11
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
+ import TextInput from "ink-text-input";
13
+ //#region src/ui/SchemaSession.tsx
14
+ /** Built-in read-only view; uses the same viewport and command contract as contributions. */
15
+ var SchemaSession = class {
16
+ schema;
17
+ element = /* @__PURE__ */ jsx(SchemaPanel, { session: this });
18
+ viewport = observableValue(this, {
19
+ rows: 24,
20
+ columns: 80,
21
+ top: 0,
22
+ left: 0,
23
+ disposed: false
24
+ });
25
+ constructor(schema) {
26
+ this.schema = schema;
27
+ }
28
+ lines = derived(this, (reader) => {
29
+ const value = this.schema.read(reader);
30
+ return (value.kind === "loaded" ? JSON.stringify(value.schema, null, 2) : value.kind === "error" ? String(value.error) : value.kind === "none" ? "Select an interface to inspect its schema." : "Loading schema…").split("\n").map(safeTerminalText);
31
+ });
32
+ commands = derived(this, (reader) => {
33
+ const viewport = this.viewport.read(reader);
34
+ if (viewport.disposed) return [];
35
+ const lines = this.lines.read(reader);
36
+ const page = Math.max(1, viewport.rows - 2);
37
+ const maxTop = Math.max(0, lines.length - page);
38
+ const maxLeft = Math.max(0, lines.reduce((width, line) => Math.max(width, [...line].length), 0) - viewport.columns);
39
+ const top = Math.min(viewport.top, maxTop), left = Math.min(viewport.left, maxLeft);
40
+ const move = (vertical, horizontal = 0) => this.viewport.set({
41
+ ...this.viewport.get(),
42
+ top: Math.max(0, Math.min(maxTop, top + vertical)),
43
+ left: Math.max(0, Math.min(maxLeft, left + horizontal))
44
+ }, void 0);
45
+ const command = (id, title, keys, execute, enabled) => ({
46
+ id: `schema.${id}`,
47
+ title,
48
+ keybindings: keys.map((key) => ({ key })),
49
+ execute,
50
+ enabled,
51
+ context: "Schema"
52
+ });
53
+ return [
54
+ command("up", "Scroll up", ["up", "k"], () => move(-1), top > 0),
55
+ command("down", "Scroll down", ["down", "j"], () => move(1), top < maxTop),
56
+ command("pageUp", "Page up", ["pageup"], () => move(-page), top > 0),
57
+ command("pageDown", "Page down", ["pagedown"], () => move(page), top < maxTop),
58
+ command("first", "First line", ["home", "g"], () => move(-maxTop), top > 0),
59
+ command("last", "Last line", ["end", "G"], () => move(maxTop), top < maxTop),
60
+ command("left", "Scroll left", ["left", "h"], () => move(0, -20), left > 0),
61
+ command("right", "Scroll right", ["right", "l"], () => move(0, 20), left < maxLeft)
62
+ ];
63
+ });
64
+ setViewport(rows, columns = this.viewport.get().columns) {
65
+ const state = this.viewport.get();
66
+ if (rows !== state.rows || columns !== state.columns) this.viewport.set({
67
+ ...state,
68
+ rows,
69
+ columns
70
+ }, void 0);
71
+ }
72
+ captureState() {
73
+ const { top, left } = this.viewport.get();
74
+ return {
75
+ top,
76
+ left
77
+ };
78
+ }
79
+ restoreState(value) {
80
+ const state = value;
81
+ if (state) this.viewport.set({
82
+ ...this.viewport.get(),
83
+ top: state.top ?? 0,
84
+ left: state.left ?? 0
85
+ }, void 0);
86
+ }
87
+ dispose() {
88
+ this.viewport.set({
89
+ ...this.viewport.get(),
90
+ disposed: true
91
+ }, void 0);
92
+ }
93
+ };
94
+ var SchemaPanel = class extends React.Component {
95
+ state = { revision: 0 };
96
+ subscription;
97
+ componentDidMount() {
98
+ this.subscription = autorun((reader) => {
99
+ this.props.session.viewport.read(reader);
100
+ this.props.session.lines.read(reader);
101
+ this.setState((previous) => ({ revision: previous.revision + 1 }));
102
+ });
103
+ }
104
+ componentWillUnmount() {
105
+ this.subscription?.dispose();
106
+ }
107
+ render() {
108
+ const { session } = this.props;
109
+ const { rows, columns, top } = session.viewport.get();
110
+ const lines = session.lines.get();
111
+ const left = Math.min(session.viewport.get().left, Math.max(0, lines.reduce((width, line) => Math.max(width, [...line].length), 0) - columns));
112
+ const capacity = Math.max(1, rows - 2);
113
+ const start = Math.min(top, Math.max(0, lines.length - capacity));
114
+ const schema = session.schema.get();
115
+ return /* @__PURE__ */ jsxs(Box, {
116
+ flexDirection: "column",
117
+ width: columns,
118
+ height: rows,
119
+ flexShrink: 0,
120
+ overflow: "hidden",
121
+ children: [
122
+ rows > 1 && /* @__PURE__ */ jsx(Box, {
123
+ height: 1,
124
+ flexShrink: 0,
125
+ overflow: "hidden",
126
+ children: /* @__PURE__ */ jsx(Text, {
127
+ bold: true,
128
+ color: "cyan",
129
+ wrap: "truncate-end",
130
+ children: schema.kind === "loaded" ? `SCHEMA ${safeTerminalText(schema.schema.id)}` : "SCHEMA"
131
+ })
132
+ }),
133
+ Array.from({ length: capacity }, (_, offset) => /* @__PURE__ */ jsx(Box, {
134
+ height: 1,
135
+ flexShrink: 0,
136
+ overflow: "hidden",
137
+ children: /* @__PURE__ */ jsx(Text, {
138
+ wrap: "truncate-end",
139
+ children: [...lines[start + offset] ?? ""].slice(left).join("") || " "
140
+ })
141
+ }, offset)),
142
+ rows > 2 && /* @__PURE__ */ jsx(Box, {
143
+ height: 1,
144
+ flexShrink: 0,
145
+ overflow: "hidden",
146
+ children: /* @__PURE__ */ jsx(Text, {
147
+ dimColor: true,
148
+ wrap: "truncate-end",
149
+ children: `Lines ${start + 1}–${Math.min(lines.length, start + capacity)}/${lines.length} · column ${left + 1}`
150
+ })
151
+ })
152
+ ]
153
+ });
154
+ }
155
+ };
156
+ //#endregion
157
+ //#region src/ui/ViewController.ts
158
+ var ViewController = class extends Disposable {
159
+ channel;
160
+ selection;
161
+ schema;
162
+ listings;
163
+ tab = observableValue(this, "methods");
164
+ kind = observableValue(this, "interface");
165
+ session = observableValue(this, void 0);
166
+ error = observableValue(this, void 0);
167
+ connectionGeneration = observableValue(this, 0);
168
+ connected = observableValue(this, true);
169
+ generation = 0;
170
+ savedState;
171
+ sessionKey;
172
+ pendingCleanup = /* @__PURE__ */ new Set();
173
+ constructor(channel, selection, schema, listings) {
174
+ super();
175
+ this.channel = channel;
176
+ this.selection = selection;
177
+ this.schema = schema;
178
+ this.listings = listings;
179
+ this._register(autorun((reader) => {
180
+ const tab = this.tab.read(reader);
181
+ const target = this.target.read(reader);
182
+ this.connectionGeneration.read(reader);
183
+ const connected = this.connected.read(reader);
184
+ const generation = ++this.generation;
185
+ const previous = this.session.get();
186
+ if (previous && this.sessionKey) this.savedState = {
187
+ key: this.sessionKey,
188
+ state: previous.captureState?.()
189
+ };
190
+ this.stopSession(previous);
191
+ this.session.set(void 0, void 0);
192
+ this.error.set(void 0, void 0);
193
+ if (tab === "schema") {
194
+ const key = `schema:${target?.id ?? ""}`;
195
+ const session = new SchemaSession(this.schema);
196
+ if (this.savedState?.key === key) session.restoreState(this.savedState.state);
197
+ this.sessionKey = key;
198
+ this.session.set(session, void 0);
199
+ return;
200
+ }
201
+ const view = views.find((value) => value.id === tab);
202
+ if (!view || !target || !matchesTarget(target, view) || !connected) return;
203
+ const key = `${tab}:${target.id}`;
204
+ resolveViewTarget(channel, view, target.id).then((context) => {
205
+ if (generation !== this.generation) return;
206
+ const session = view.createSession(context);
207
+ if (this.savedState?.key === key) session.restoreState?.(this.savedState.state);
208
+ this.sessionKey = key;
209
+ this.session.set(session, void 0);
210
+ }).catch((error) => {
211
+ if (generation === this.generation) this.error.set(String(error), void 0);
212
+ });
213
+ }));
214
+ }
215
+ target = derived(this, (reader) => {
216
+ const selection = this.selection.read(reader);
217
+ if (!selection) return void 0;
218
+ const state = this.schema.read(reader);
219
+ const all = this.listings();
220
+ const listing = all.find((value) => value.serviceId === selection.serviceId && value.interfaceId === selection.interfaceId && !!value.isDefault === !!selection.isDefault);
221
+ const selected = {
222
+ ...selection,
223
+ discoveredFrom: listing?.discoveredFrom ?? selection.serviceId,
224
+ hash: listing?.hash,
225
+ tags: state.kind === "loaded" ? state.schema.tags : listing?.tags
226
+ };
227
+ if (this.kind.read(reader) === "interface") return interfaceTarget(selected);
228
+ const interfaces = [];
229
+ for (const value of [selected, ...all.filter((value) => value.serviceId === selected.serviceId && value.discoveredFrom === selected.discoveredFrom)]) if (!interfaces.some((item) => item.interfaceId === value.interfaceId)) interfaces.push(value);
230
+ return {
231
+ id: targetId("service", selected),
232
+ kind: "service",
233
+ serviceId: selected.serviceId,
234
+ discoveredFrom: selected.discoveredFrom,
235
+ interfaces
236
+ };
237
+ });
238
+ tabs = derived(this, (reader) => {
239
+ const target = this.target.read(reader);
240
+ return [
241
+ {
242
+ id: "methods",
243
+ title: "Methods",
244
+ condition: "interface:any | service:any"
245
+ },
246
+ {
247
+ id: "schema",
248
+ title: "Schema",
249
+ condition: "interface:any | service:any"
250
+ },
251
+ ...views.filter((view) => target && matchesTarget(target, view)).map((view) => ({
252
+ id: view.id,
253
+ title: view.title,
254
+ condition: view.conditions.map(formatCondition).join(" | ")
255
+ }))
256
+ ];
257
+ });
258
+ cycleTab() {
259
+ const tabs = this.tabs.get();
260
+ const index = tabs.findIndex((tab) => tab.id === this.tab.get());
261
+ this.tab.set(tabs[(index + 1) % tabs.length].id, void 0);
262
+ }
263
+ toggleKind() {
264
+ this.kind.set(this.kind.get() === "interface" ? "service" : "interface", void 0);
265
+ }
266
+ disconnect() {
267
+ this.connected.set(false, void 0);
268
+ }
269
+ reconnect() {
270
+ if (!this.connected.get()) this.connected.set(true, void 0);
271
+ else this.connectionGeneration.set(this.connectionGeneration.get() + 1, void 0);
272
+ }
273
+ stopSession(session) {
274
+ session?.dispose();
275
+ const cleanup = session?.disposeAsync?.();
276
+ if (!cleanup) return;
277
+ this.pendingCleanup.add(cleanup);
278
+ cleanup.then(() => this.pendingCleanup.delete(cleanup), () => this.pendingCleanup.delete(cleanup));
279
+ }
280
+ async waitForIdle() {
281
+ await Promise.allSettled([...this.pendingCleanup]);
282
+ }
283
+ dispose() {
284
+ ++this.generation;
285
+ this.stopSession(this.session.get());
286
+ super.dispose();
287
+ }
288
+ };
289
+ //#endregion
10
290
  //#region src/ui/UiModel.ts
11
291
  /**
12
292
  * View model for the TUI. Owns the live channel and an in-memory schema cache
@@ -15,6 +295,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
15
295
  */
16
296
  var UiModel = class extends Disposable {
17
297
  _channel;
298
+ views;
18
299
  /** Initial directory fetch — kicked off in the constructor. */
19
300
  servicesPromise;
20
301
  /** Non-fatal discovery failures for services that could not be inspected. */
@@ -63,6 +344,7 @@ var UiModel = class extends Disposable {
63
344
  return result.services;
64
345
  });
65
346
  this.servicesPromise.promise.catch(() => {});
347
+ this.views = this._register(new ViewController(_channel, this.selection, this.currentSchemaState, () => this.servicesPromise.promiseResult.get()?.data ?? []));
66
348
  this._register(autorun((reader) => {
67
349
  const sel = this.selection.read(reader);
68
350
  if (!sel) return;
@@ -617,24 +899,171 @@ function pad(text, width) {
617
899
  return text.slice(0, width - 1) + "…";
618
900
  }
619
901
  //#endregion
902
+ //#region src/ui/ViewArea.tsx
903
+ /** Class binding keeps contributed views hook-free. Models own their sessions. */
904
+ var ViewArea = class extends React.Component {
905
+ state = { revision: 0 };
906
+ subscription;
907
+ componentDidMount() {
908
+ this.subscription = autorun((reader) => {
909
+ const model = this.props.model;
910
+ model.views.tab.read(reader);
911
+ model.views.kind.read(reader);
912
+ model.views.tabs.read(reader);
913
+ model.views.session.read(reader);
914
+ model.views.error.read(reader);
915
+ model.focusedColumn.read(reader);
916
+ model.currentSchemaState.read(reader);
917
+ this.setState((previous) => ({ revision: previous.revision + 1 }));
918
+ });
919
+ }
920
+ componentWillUnmount() {
921
+ this.subscription?.dispose();
922
+ }
923
+ render() {
924
+ const model = this.props.model;
925
+ const views = model.views;
926
+ const selected = views.tab.get();
927
+ const tabs = views.tabs.get();
928
+ const current = tabs.find((tab) => tab.id === selected);
929
+ const chromeRows = this.props.chromeRows ?? 2;
930
+ const contentRows = Math.max(1, this.props.height - chromeRows);
931
+ return /* @__PURE__ */ jsxs(Box, {
932
+ flexDirection: "column",
933
+ flexGrow: 1,
934
+ flexBasis: 0,
935
+ minWidth: 0,
936
+ height: this.props.height,
937
+ overflow: "hidden",
938
+ children: [
939
+ chromeRows > 0 && /* @__PURE__ */ jsx(Box, {
940
+ height: 1,
941
+ flexShrink: 0,
942
+ children: /* @__PURE__ */ jsxs(Text, {
943
+ wrap: "truncate-end",
944
+ children: [
945
+ views.kind.get(),
946
+ ": ",
947
+ tabs.map((tab) => tab.id === selected ? `[${tab.title}]` : tab.title).join(" ")
948
+ ]
949
+ })
950
+ }),
951
+ chromeRows > 1 && /* @__PURE__ */ jsx(Box, {
952
+ height: 1,
953
+ flexShrink: 0,
954
+ children: /* @__PURE__ */ jsx(Text, {
955
+ dimColor: true,
956
+ wrap: "truncate-end",
957
+ children: safeTerminalText(current?.condition ?? "Unavailable for this target")
958
+ })
959
+ }),
960
+ selected === "methods" ? /* @__PURE__ */ jsx(Box, {
961
+ height: contentRows,
962
+ flexShrink: 0,
963
+ overflow: "hidden",
964
+ children: this.props.children
965
+ }) : views.error.get() ? /* @__PURE__ */ jsx(Text, {
966
+ color: "red",
967
+ children: safeTerminalText(views.error.get())
968
+ }) : views.session.get() ? /* @__PURE__ */ jsx(ViewFrame, {
969
+ session: views.session.get(),
970
+ rows: contentRows,
971
+ maxColumns: this.props.columns,
972
+ tabbed: true,
973
+ active: model.focusedColumn.get() > 0
974
+ }) : /* @__PURE__ */ jsx(Text, { children: current ? "Loading view…" : "View unavailable; press tab" })
975
+ ]
976
+ });
977
+ }
978
+ };
979
+ var ViewFooter = class extends React.Component {
980
+ state = { revision: 0 };
981
+ subscription;
982
+ componentDidMount() {
983
+ this.subscription = autorun((reader) => {
984
+ this.props.model.views.tab.read(reader);
985
+ this.props.model.views.session.read(reader);
986
+ this.props.model.focusedColumn.read(reader);
987
+ this.props.model.formEditing.read(reader);
988
+ this.setState((previous) => ({ revision: previous.revision + 1 }));
989
+ });
990
+ }
991
+ componentWillUnmount() {
992
+ this.subscription?.dispose();
993
+ }
994
+ render() {
995
+ const model = this.props.model;
996
+ if (model.views.session.get()) return /* @__PURE__ */ jsx(Text, {
997
+ dimColor: true,
998
+ children: model.focusedColumn.get() === 0 ? "Enter/→: focus view" : "View commands shown above"
999
+ });
1000
+ const noop = () => {};
1001
+ const commands = hostCommands({
1002
+ quit: noop,
1003
+ nextTab: noop,
1004
+ toggleScope: noop,
1005
+ back: model.views.tab.get() !== "methods" ? noop : void 0
1006
+ }, model.formEditing.get());
1007
+ return /* @__PURE__ */ jsx(Text, {
1008
+ dimColor: true,
1009
+ wrap: "truncate-end",
1010
+ children: (model.views.tab.get() === "methods" ? "←/→: switch column · ↑/↓: move · Enter: edit/submit · Esc: cancel edit · " : "") + formatCommandLegend(commands)
1011
+ });
1012
+ }
1013
+ };
1014
+ //#endregion
1015
+ //#region src/views/TerminalKeyInput.tsx
1016
+ /** Readline preserves named keys such as Home/End that Ink's Key type omits. */
1017
+ var TerminalKeyInput = class extends React.Component {
1018
+ onKey = (input, key) => this.props.onKey({
1019
+ ...key,
1020
+ input
1021
+ });
1022
+ componentDidMount() {
1023
+ emitKeypressEvents(stdin);
1024
+ stdin.on("keypress", this.onKey);
1025
+ }
1026
+ componentWillUnmount() {
1027
+ stdin.off("keypress", this.onKey);
1028
+ }
1029
+ render() {
1030
+ return null;
1031
+ }
1032
+ };
1033
+ //#endregion
620
1034
  //#region src/ui/App.tsx
621
- const IDENTITY_HEADER_H = 1;
622
- const FOOTER_H = 1;
623
- const RESULT_H = 8;
624
1035
  /** Column chrome that is not list rows: top border + bottom border + title. */
625
1036
  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;
1037
+ function uiLayout(columns, rows, focusedColumn, warningCount = 0) {
1038
+ const width = Math.max(1, Math.floor(columns));
1039
+ const height = Math.max(1, Math.floor(rows));
1040
+ const compact = width < 60;
1041
+ const headerRows = height > 2 ? 1 + Math.min(warningCount, Math.max(0, height - 10)) : 0;
1042
+ const footerRows = height > 1 ? 1 : 0;
1043
+ const bodyRows = Math.max(1, height - headerRows - footerRows);
1044
+ const sidebarWidth = compact ? focusedColumn === 0 ? width : 0 : Math.min(28, Math.max(18, Math.floor(width * .22)));
1045
+ const mainWidth = width - sidebarWidth;
1046
+ const viewHeaderRows = Math.min(bodyRows - 1, height < 20 ? 1 : 2);
1047
+ const contentRows = bodyRows - viewHeaderRows;
1048
+ const resultRows = Math.min(Math.max(0, contentRows - 1), Math.max(3, Math.min(6, Math.floor(contentRows / 3))));
1049
+ const methodRows = contentRows - resultRows;
1050
+ const splitMethods = mainWidth >= 65;
1051
+ return {
1052
+ width,
1053
+ height,
1054
+ compact,
1055
+ headerRows,
1056
+ footerRows,
1057
+ bodyRows,
1058
+ sidebarWidth,
1059
+ mainWidth,
1060
+ viewHeaderRows,
1061
+ contentRows,
1062
+ resultRows,
1063
+ methodRows,
1064
+ splitMethods,
1065
+ methodWidth: splitMethods ? Math.min(30, Math.max(22, Math.floor(mainWidth * .35))) : mainWidth
1066
+ };
638
1067
  }
639
1068
  /**
640
1069
  * Miller-column TUI: three columns left → right (services, methods, form)
@@ -643,67 +1072,162 @@ function useTerminalRows() {
643
1072
  * 0 / 1 immediately previews into the column to its right.
644
1073
  */
645
1074
  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
1075
  const { exit } = useApp();
651
- useInput((input, key) => {
652
- if (editing) return;
653
- if (input === "q") {
654
- exit();
1076
+ const { stdout } = useStdout();
1077
+ useInput(() => {});
1078
+ return /* @__PURE__ */ jsx(AppLayout, {
1079
+ model,
1080
+ exit,
1081
+ stdout
1082
+ });
1083
+ };
1084
+ var AppLayout = class extends React.Component {
1085
+ state = { revision: 0 };
1086
+ measuredColumns = observableValue(this, void 0);
1087
+ root = React.createRef();
1088
+ mounted = false;
1089
+ subscription;
1090
+ invalidate = () => this.setState((previous) => ({ revision: previous.revision + 1 }));
1091
+ componentDidMount() {
1092
+ this.mounted = true;
1093
+ this.props.stdout.prependListener("resize", this.invalidate);
1094
+ this.subscription = autorun((reader) => {
1095
+ const { model } = this.props;
1096
+ model.focusedColumn.read(reader);
1097
+ model.formEditing.read(reader);
1098
+ model.discoveryWarnings.read(reader);
1099
+ model.views.tab.read(reader);
1100
+ this.measuredColumns.read(reader);
1101
+ this.invalidate();
1102
+ });
1103
+ this.measure();
1104
+ }
1105
+ componentDidUpdate() {
1106
+ this.measure();
1107
+ }
1108
+ measure() {
1109
+ queueMicrotask(() => {
1110
+ if (!this.mounted || !this.root.current) return;
1111
+ const width = Math.max(1, Math.floor(measureElement(this.root.current).width));
1112
+ if (width !== this.measuredColumns.get()) this.measuredColumns.set(width, void 0);
1113
+ });
1114
+ }
1115
+ componentWillUnmount() {
1116
+ this.mounted = false;
1117
+ this.subscription?.dispose();
1118
+ this.props.stdout.off("resize", this.invalidate);
1119
+ }
1120
+ onKey = (event) => {
1121
+ const { model, exit } = this.props;
1122
+ const focusedColumn = model.focusedColumn.get();
1123
+ if (model.formEditing.get()) {
1124
+ if (matchesKey({
1125
+ key: "c",
1126
+ ctrl: true
1127
+ }, event)) exit();
655
1128
  return;
656
1129
  }
657
- if (key.leftArrow && focusedColumn > 0) {
1130
+ const inView = model.views.tab.get() !== "methods" && focusedColumn > 0;
1131
+ if (dispatchHostKey(inView ? model.views.session.get() : void 0, event, {
1132
+ quit: exit,
1133
+ nextTab: () => model.views.cycleTab(),
1134
+ toggleScope: () => model.views.toggleKind(),
1135
+ back: inView ? () => model.focusColumn(0) : void 0
1136
+ })) return;
1137
+ if (inView) return;
1138
+ if (event.name === "left" && focusedColumn > 0) {
658
1139
  model.focusColumn(focusedColumn - 1);
659
1140
  return;
660
1141
  }
661
- if (key.rightArrow && focusedColumn < 2) {
1142
+ if (event.name === "right" && focusedColumn < 2) {
662
1143
  model.focusColumn(focusedColumn + 1);
663
1144
  return;
664
1145
  }
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, {
1146
+ };
1147
+ render() {
1148
+ const { model, stdout } = this.props;
1149
+ const focusedColumn = model.focusedColumn.get();
1150
+ const warnings = model.discoveryWarnings.get();
1151
+ const layout = uiLayout(Math.min(this.measuredColumns.get() ?? 1, stdout.columns ?? 80), stdout.rows ?? 30, focusedColumn, warnings.length);
1152
+ return /* @__PURE__ */ jsxs(Box, {
1153
+ ref: this.root,
1154
+ flexDirection: "column",
1155
+ width: "100%",
1156
+ minWidth: 0,
1157
+ height: layout.height,
1158
+ overflow: "hidden",
1159
+ children: [
1160
+ /* @__PURE__ */ jsx(TerminalKeyInput, { onKey: this.onKey }),
1161
+ layout.headerRows > 0 && /* @__PURE__ */ jsx(Box, {
1162
+ height: layout.headerRows,
1163
+ flexShrink: 0,
1164
+ overflow: "hidden",
1165
+ children: /* @__PURE__ */ jsx(Header, {
693
1166
  model,
694
- focused: focusedColumn === 2,
695
- height: bodyHeight,
696
- listHeight
1167
+ warnings: warnings.slice(0, layout.headerRows - 1)
697
1168
  })
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
- });
1169
+ }),
1170
+ /* @__PURE__ */ jsxs(Box, {
1171
+ width: layout.width,
1172
+ height: layout.bodyRows,
1173
+ flexShrink: 0,
1174
+ overflow: "hidden",
1175
+ children: [layout.sidebarWidth > 0 && /* @__PURE__ */ jsx(ServicesColumn, {
1176
+ model,
1177
+ focused: focusedColumn === 0,
1178
+ width: layout.sidebarWidth,
1179
+ height: layout.bodyRows,
1180
+ listHeight: Math.max(1, layout.bodyRows - COLUMN_CHROME_H)
1181
+ }), layout.mainWidth > 0 && /* @__PURE__ */ jsx(Box, {
1182
+ width: layout.mainWidth,
1183
+ height: layout.bodyRows,
1184
+ flexShrink: 0,
1185
+ overflow: "hidden",
1186
+ children: /* @__PURE__ */ jsx(ViewArea, {
1187
+ model,
1188
+ height: layout.bodyRows,
1189
+ columns: layout.mainWidth,
1190
+ chromeRows: layout.viewHeaderRows,
1191
+ children: /* @__PURE__ */ jsxs(Box, {
1192
+ flexDirection: "column",
1193
+ height: layout.contentRows,
1194
+ width: layout.mainWidth,
1195
+ flexShrink: 0,
1196
+ overflow: "hidden",
1197
+ children: [/* @__PURE__ */ jsxs(Box, {
1198
+ height: layout.methodRows,
1199
+ width: layout.mainWidth,
1200
+ flexShrink: 0,
1201
+ overflow: "hidden",
1202
+ children: [(layout.splitMethods || focusedColumn !== 2) && /* @__PURE__ */ jsx(MethodsColumn, {
1203
+ model,
1204
+ focused: focusedColumn === 1,
1205
+ width: layout.methodWidth,
1206
+ height: layout.methodRows,
1207
+ listHeight: Math.max(1, layout.methodRows - COLUMN_CHROME_H)
1208
+ }), (layout.splitMethods || focusedColumn === 2) && /* @__PURE__ */ jsx(FormColumn, {
1209
+ model,
1210
+ focused: focusedColumn === 2,
1211
+ height: layout.methodRows,
1212
+ listHeight: Math.max(1, layout.methodRows - COLUMN_CHROME_H)
1213
+ })]
1214
+ }), layout.resultRows > 0 && /* @__PURE__ */ jsx(ResultPane, {
1215
+ model,
1216
+ height: layout.resultRows
1217
+ })]
1218
+ })
1219
+ })
1220
+ })]
1221
+ }),
1222
+ layout.footerRows > 0 && /* @__PURE__ */ jsx(Box, {
1223
+ height: layout.footerRows,
1224
+ flexShrink: 0,
1225
+ overflow: "hidden",
1226
+ children: /* @__PURE__ */ jsx(ViewFooter, { model })
1227
+ })
1228
+ ]
1229
+ });
1230
+ }
707
1231
  };
708
1232
  /** "▲ N more" / "▼ N more" indicator row, shown only when items are hidden. */
709
1233
  const ScrollIndicator = ({ direction, count }) => {
@@ -717,17 +1241,24 @@ const Header = ({ model, warnings }) => {
717
1241
  const identity = useObservable(model.identity);
718
1242
  return /* @__PURE__ */ jsxs(Box, {
719
1243
  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, {
1244
+ children: [/* @__PURE__ */ jsxs(Box, {
1245
+ height: 1,
1246
+ flexShrink: 0,
1247
+ children: [/* @__PURE__ */ jsx(Text, {
1248
+ dimColor: true,
1249
+ children: "identity: "
1250
+ }), /* @__PURE__ */ jsx(Text, {
1251
+ wrap: "truncate-end",
1252
+ children: identity ?? "(resolving…)"
1253
+ })]
1254
+ }), warnings.map((warning) => /* @__PURE__ */ jsxs(Text, {
724
1255
  color: "yellow",
725
1256
  wrap: "truncate-end",
726
1257
  children: ["! ", warning]
727
1258
  }, warning))]
728
1259
  });
729
1260
  };
730
- const ServicesColumn = ({ model, focused, height, listHeight }) => {
1261
+ const ServicesColumn = ({ model, focused, width, height, listHeight }) => {
731
1262
  const result = useObservable(model.servicesPromise.promiseResult);
732
1263
  const selection = useObservable(model.selection);
733
1264
  useInput((_input, key) => {
@@ -741,7 +1272,7 @@ const ServicesColumn = ({ model, focused, height, listHeight }) => {
741
1272
  return /* @__PURE__ */ jsx(Column, {
742
1273
  title: "Services",
743
1274
  focused,
744
- width: "25%",
1275
+ width,
745
1276
  height,
746
1277
  children: !result ? /* @__PURE__ */ jsx(Text, {
747
1278
  dimColor: true,
@@ -775,7 +1306,7 @@ const ServicesColumn = ({ model, focused, height, listHeight }) => {
775
1306
  ] })
776
1307
  });
777
1308
  };
778
- const MethodsColumn = ({ model, focused, height, listHeight }) => {
1309
+ const MethodsColumn = ({ model, focused, width, height, listHeight }) => {
779
1310
  const selection = useObservable(model.selection);
780
1311
  const schemaState = useObservable(model.currentSchemaState);
781
1312
  const methods = useObservable(model.currentMethods);
@@ -789,7 +1320,7 @@ const MethodsColumn = ({ model, focused, height, listHeight }) => {
789
1320
  return /* @__PURE__ */ jsx(Column, {
790
1321
  title: "Methods",
791
1322
  focused,
792
- width: "25%",
1323
+ width,
793
1324
  height,
794
1325
  children: !selection ? /* @__PURE__ */ jsx(Text, {
795
1326
  dimColor: true,
@@ -938,7 +1469,7 @@ const MethodForm = ({ model, method, focused, listHeight }) => {
938
1469
  children: [
939
1470
  /* @__PURE__ */ jsx(Text, {
940
1471
  bold: true,
941
- children: (method.result === void 0 ? "notify " : "request ") + selection?.methodName
1472
+ children: (method.result === void 0 ? "notify " : "request ") + method.name
942
1473
  }),
943
1474
  method.summary ? /* @__PURE__ */ jsx(Text, {
944
1475
  dimColor: true,
@@ -993,6 +1524,7 @@ const Column = ({ title, focused, children, width, flexGrow, height }) => {
993
1524
  width,
994
1525
  flexGrow,
995
1526
  height,
1527
+ minWidth: 0,
996
1528
  overflow: "hidden",
997
1529
  borderStyle: "single",
998
1530
  borderColor: focused ? "cyan" : void 0,
@@ -1003,13 +1535,14 @@ const Column = ({ title, focused, children, width, flexGrow, height }) => {
1003
1535
  }), children]
1004
1536
  });
1005
1537
  };
1006
- const ResultPane = ({ model }) => {
1538
+ const ResultPane = ({ model, height }) => {
1007
1539
  const promise = useObservable(model.lastCall);
1008
1540
  const result = useObservable(promise ? promise.promiseResult : NO_RESULT);
1009
1541
  const chunks = useObservable(model.streamChunks);
1010
1542
  return /* @__PURE__ */ jsxs(Box, {
1011
1543
  flexDirection: "column",
1012
- height: RESULT_H,
1544
+ height,
1545
+ flexShrink: 0,
1013
1546
  overflow: "hidden",
1014
1547
  borderStyle: "single",
1015
1548
  paddingX: 1,
@@ -1056,20 +1589,21 @@ function formatError(e) {
1056
1589
  //#endregion
1057
1590
  //#region src/ui/runUi.tsx
1058
1591
  async function runUi(opts) {
1059
- if (isHubEndpoint(opts.endpoint)) await _runUiReconnecting(opts.endpoint, opts.principalSpec);
1060
- else await _runUiOnce(opts.endpoint, opts.principalSpec);
1592
+ if (isHubEndpoint(opts.endpoint) && opts.profile !== "rpc") await _runUiReconnecting(opts.endpoint, opts.principalSpec, opts.schema);
1593
+ else await _runUiOnce(opts.endpoint, opts.principalSpec, opts.schema, opts.profile);
1061
1594
  }
1062
1595
  /** Non-Hub endpoints: a single connection, no redial. */
1063
- async function _runUiOnce(endpoint, principalSpec) {
1596
+ async function _runUiOnce(endpoint, principalSpec, schema, profile) {
1064
1597
  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);
1598
+ const identity = _isRawEndpoint(endpoint) || profile === "rpc" ? "unsigned (raw endpoint)" : _formatIdentity(await setupSigning(conn.channel, conn.signing, principalSpec, { negotiateHubCaps: false }));
1599
+ const model = new UiModel(schema ? withStaticHubReflection(conn.channel, schema) : conn.channel);
1067
1600
  model.identity.set(identity, void 0);
1068
- const instance = render(/* @__PURE__ */ jsx(App, { model }));
1601
+ const instance = render(/* @__PURE__ */ jsx(App, { model }), { exitOnCtrlC: false });
1069
1602
  try {
1070
1603
  await instance.waitUntilExit();
1071
1604
  } finally {
1072
1605
  model.dispose();
1606
+ await model.views.waitForIdle();
1073
1607
  conn.close();
1074
1608
  }
1075
1609
  }
@@ -1082,7 +1616,7 @@ function _isRawEndpoint(endpoint) {
1082
1616
  * open a fresh hub channel, install signing, and point the swappable sender at
1083
1617
  * the new signed channel. The TUI is rendered once, after the first connect.
1084
1618
  */
1085
- async function _runUiReconnecting(endpoint, principalSpec) {
1619
+ async function _runUiReconnecting(endpoint, principalSpec, schema) {
1086
1620
  const swappable = new SwappableSender();
1087
1621
  let model;
1088
1622
  const hubEndpoint = endpoint.kind === "ws" ? endpoint.url : endpoint.path;
@@ -1094,16 +1628,29 @@ async function _runUiReconnecting(endpoint, principalSpec) {
1094
1628
  const signed = SigningSender.wrapChannel(channel, signing).sender;
1095
1629
  const session = await setupSigning(signed, signing, principalSpec, { negotiateHubCaps: true });
1096
1630
  swappable.setTarget(signed);
1631
+ channel.onClose(() => {
1632
+ swappable.setTarget(void 0);
1633
+ model?.views.disconnect();
1634
+ model?.identity.set("disconnected; reconnecting…", void 0);
1635
+ });
1097
1636
  if (!model) {
1098
- model = new UiModel(swappable);
1637
+ model = new UiModel(schema ? withStaticHubReflection(swappable, schema) : swappable);
1099
1638
  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);
1639
+ render(/* @__PURE__ */ jsx(App, { model }), { exitOnCtrlC: false }).waitUntilExit().then(async () => {
1640
+ model?.dispose();
1641
+ await model?.views.waitForIdle();
1642
+ handle.stop();
1643
+ }, () => handle.stop());
1644
+ } else {
1645
+ model.identity.set(_formatIdentity(session), void 0);
1646
+ model.views.reconnect();
1647
+ }
1102
1648
  });
1103
1649
  try {
1104
1650
  await handle.done;
1105
1651
  } finally {
1106
1652
  model?.dispose();
1653
+ await model?.views.waitForIdle();
1107
1654
  }
1108
1655
  }
1109
1656
  /** One-line identity label for the TUI header (source + truncated nodeId). */
@@ -1140,4 +1687,4 @@ var SwappableSender = class {
1140
1687
  //#endregion
1141
1688
  export { runUi };
1142
1689
 
1143
- //# sourceMappingURL=runUi-CuJr8_tY.js.map
1690
+ //# sourceMappingURL=runUi-Dp2P3aNN.js.map