@mirrowel/opencode-openshare 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +175 -2
  3. package/ci/README.md +56 -0
  4. package/ci/openshare.config.json +5 -0
  5. package/ci/openshare.js +2265 -0
  6. package/dist/commands.d.ts +7 -0
  7. package/dist/commands.js +440 -0
  8. package/dist/config.d.ts +134 -0
  9. package/dist/config.js +517 -0
  10. package/dist/core.d.ts +117 -0
  11. package/dist/core.js +1944 -0
  12. package/dist/dialogs.d.ts +94 -0
  13. package/dist/dialogs.js +725 -0
  14. package/dist/server.d.ts +64 -0
  15. package/dist/server.js +30 -0
  16. package/dist/settings.d.ts +24 -0
  17. package/dist/settings.js +1226 -0
  18. package/dist/share-flow.d.ts +287 -0
  19. package/dist/share-flow.js +516 -0
  20. package/dist/sidebar.d.ts +19 -0
  21. package/dist/sidebar.js +144 -0
  22. package/dist/tui-host.d.ts +158 -0
  23. package/dist/tui-host.js +6 -0
  24. package/dist/tui.d.ts +17 -0
  25. package/dist/tui.js +4762 -0
  26. package/dist/ui-prefs.d.ts +16 -0
  27. package/dist/ui-prefs.js +27 -0
  28. package/dist/v2-server.d.ts +81 -0
  29. package/dist/v2-server.js +402 -0
  30. package/dist/v2-tui.d.ts +24 -0
  31. package/dist/v2-tui.js +313 -0
  32. package/dist/v2-types.d.ts +134 -0
  33. package/dist/v2-types.js +8 -0
  34. package/openshare.config.json +16 -0
  35. package/package.json +98 -4
  36. package/src/banner.txt +22 -0
  37. package/src/commands.ts +452 -0
  38. package/src/config.ts +518 -0
  39. package/src/core.ts +1997 -0
  40. package/src/dialogs.tsx +971 -0
  41. package/src/server.ts +47 -0
  42. package/src/settings.ts +1239 -0
  43. package/src/share-flow.ts +551 -0
  44. package/src/sidebar.tsx +187 -0
  45. package/src/tui-host.ts +134 -0
  46. package/src/tui.tsx +318 -0
  47. package/src/ui-prefs.ts +36 -0
  48. package/src/v2-server.ts +438 -0
  49. package/src/v2-tui.ts +315 -0
  50. package/src/v2-types.ts +102 -0
  51. package/tsconfig.json +18 -0
package/dist/tui.js ADDED
@@ -0,0 +1,4762 @@
1
+ // @bun
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
+
18
+ // src/config.ts
19
+ import { existsSync, readFileSync, writeFileSync } from "fs";
20
+ import { basename, dirname, join, relative } from "path";
21
+ import { fileURLToPath } from "url";
22
+ function moduleDir() {
23
+ return dirname(fileURLToPath(import.meta.pathname ?? import.meta.url));
24
+ }
25
+ function packageRoot(start) {
26
+ let current = start;
27
+ for (let guard = 0;guard < 16; guard++) {
28
+ if (existsSync(join(current, "package.json")))
29
+ return current;
30
+ const parent = dirname(current);
31
+ if (parent === current)
32
+ break;
33
+ current = parent;
34
+ }
35
+ return start;
36
+ }
37
+ function configPaths(pluginDir, env = process.env) {
38
+ const paths = [join(pluginDir, CONFIG_NAME)];
39
+ const root = packageRoot(pluginDir);
40
+ if (root !== pluginDir)
41
+ paths.push(join(root, CONFIG_NAME));
42
+ let current = pluginDir;
43
+ for (let i = 0;i < 5; i++) {
44
+ paths.push(join(current, LOCAL_NAME));
45
+ const parent = dirname(current);
46
+ if (parent === current)
47
+ break;
48
+ current = parent;
49
+ }
50
+ paths.push(join(globalConfigDir(env), CONFIG_NAME));
51
+ return paths;
52
+ }
53
+ function configHomePath(env = process.env) {
54
+ return join(globalConfigDir(env), CONFIG_NAME);
55
+ }
56
+ function writeCandidates(pluginDir, env = process.env) {
57
+ const candidates = [{ path: configHomePath(env), label: "global settings (~/.config/opencode)", always: true }];
58
+ const labels = [
59
+ [join(pluginDir, CONFIG_NAME), "plugin folder (adjacent template)"],
60
+ [join(packageRoot(pluginDir), CONFIG_NAME), "package root"]
61
+ ];
62
+ let current = pluginDir;
63
+ for (let i = 0;i < 5; i++) {
64
+ labels.push([join(current, LOCAL_NAME), `local override (${relative(process.cwd(), join(current, LOCAL_NAME)) || join(current, LOCAL_NAME)})`]);
65
+ const parent = dirname(current);
66
+ if (parent === current)
67
+ break;
68
+ current = parent;
69
+ }
70
+ for (const [path, label] of labels) {
71
+ if (path === candidates[0].path)
72
+ continue;
73
+ if (existsSync(path))
74
+ candidates.push({ path, label });
75
+ }
76
+ return candidates;
77
+ }
78
+ function configSourceMap(pluginDir, env = process.env) {
79
+ const emptyMeansUnset = new Set(["workerUrl", "createToken", "urlFile", "spoolDir"]);
80
+ const known = new Set([...Object.keys(DEFAULTS), "shareMode", "servers"]);
81
+ const sources = {};
82
+ const envKeys = {
83
+ workerUrl: env.OPENSHARE_WORKER_URL,
84
+ createToken: env.OPENSHARE_CREATE_TOKEN,
85
+ debounceMs: env.OPENSHARE_DEBOUNCE_MS,
86
+ completedDelayMs: env.OPENSHARE_COMPLETED_DELAY_MS,
87
+ modelLabel: env.OPENSHARE_MODEL_LABEL,
88
+ ttlDays: env.OPENSHARE_TTL_DAYS,
89
+ authLogin: env.OPENSHARE_AUTH_LOGIN,
90
+ authPassword: env.OPENSHARE_AUTH_PASSWORD,
91
+ urlFile: env.OPENSHARE_URL_FILE,
92
+ spoolDir: env.OPENSHARE_SPOOL_DIR,
93
+ announceStderr: env.OPENSHARE_ANNOUNCE_STDERR,
94
+ shareMode: env.OPENSHARE_SHARE_MODE
95
+ };
96
+ for (const [key, value] of Object.entries(envKeys)) {
97
+ if (value !== undefined && value !== "")
98
+ sources[key] = "environment";
99
+ }
100
+ for (const path of configPaths(pluginDir, env)) {
101
+ let raw;
102
+ try {
103
+ raw = JSON.parse(readFileSync(path, "utf8"));
104
+ if (!raw || typeof raw !== "object")
105
+ continue;
106
+ } catch {
107
+ continue;
108
+ }
109
+ const label = basename(path) === LOCAL_NAME ? `openshare.local.json (${dirname(path).split(/[\\/]/).pop()})` : `${basename(path)} (${dirname(path).split(/[\\/]/).pop()})`;
110
+ for (const [key, value] of Object.entries(raw)) {
111
+ if (key === "servers") {
112
+ if (sources.servers === undefined && normalizeServers(value))
113
+ sources.servers = label;
114
+ continue;
115
+ }
116
+ if (sources[key] !== undefined || !known.has(key))
117
+ continue;
118
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean")
119
+ continue;
120
+ if (typeof value === "string" && value === "" && emptyMeansUnset.has(key))
121
+ continue;
122
+ sources[key] = label;
123
+ }
124
+ }
125
+ return sources;
126
+ }
127
+ function num(value, fallback) {
128
+ const parsed = Number(value);
129
+ return Number.isFinite(parsed) ? parsed : fallback;
130
+ }
131
+ function str(value) {
132
+ return typeof value === "string" ? value : undefined;
133
+ }
134
+ function normalizeServers(raw) {
135
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
136
+ return;
137
+ const out = {};
138
+ for (const [url, value] of Object.entries(raw)) {
139
+ if (!url.startsWith("http") || !value || typeof value !== "object")
140
+ continue;
141
+ const e = value;
142
+ const entry = {};
143
+ if (typeof e.token === "string" && e.token)
144
+ entry.token = e.token;
145
+ if (typeof e.code === "string" && e.code)
146
+ entry.code = e.code;
147
+ if (typeof e.label === "string" && e.label)
148
+ entry.label = e.label;
149
+ if (typeof e.adminToken === "string" && e.adminToken)
150
+ entry.adminToken = e.adminToken;
151
+ if (e.active === true)
152
+ entry.active = true;
153
+ out[url] = entry;
154
+ }
155
+ return Object.keys(out).length ? out : undefined;
156
+ }
157
+ function mergeServerGroups(base, add) {
158
+ const merged = { ...base ?? {} };
159
+ for (const [url, entry] of Object.entries(add))
160
+ merged[url] = { ...merged[url] ?? {}, ...entry };
161
+ return merged;
162
+ }
163
+ function resolveServer(cfg) {
164
+ const servers = {};
165
+ if (cfg.workerUrl)
166
+ servers[cfg.workerUrl] = { token: cfg.createToken || "", adminToken: cfg.createToken || "", active: true };
167
+ if (cfg.servers)
168
+ for (const [url2, entry] of Object.entries(cfg.servers))
169
+ servers[url2] = entry;
170
+ const urls = Object.keys(servers);
171
+ if (!urls.length)
172
+ return;
173
+ const url = urls.find((u) => servers[u].active) ?? urls[0];
174
+ return { url, token: servers[url].token ?? "", entry: servers[url] };
175
+ }
176
+ function saveServerEntry(url, patch, target) {
177
+ const path = target ?? configHomePath();
178
+ if (!path || !url.startsWith("http"))
179
+ return;
180
+ try {
181
+ let previous = {};
182
+ try {
183
+ previous = JSON.parse(readFileSync(path, "utf8"));
184
+ if (!previous || typeof previous !== "object")
185
+ previous = {};
186
+ } catch {}
187
+ const servers = normalizeServers(previous.servers) ?? {};
188
+ servers[url] = { ...servers[url], ...patch };
189
+ previous.servers = servers;
190
+ writeFileSync(path, JSON.stringify(previous, null, 2) + `
191
+ `);
192
+ return path;
193
+ } catch {
194
+ return;
195
+ }
196
+ }
197
+ function deleteServerEntry(url, target) {
198
+ const path = target ?? configHomePath();
199
+ if (!path)
200
+ return;
201
+ try {
202
+ let previous = {};
203
+ try {
204
+ previous = JSON.parse(readFileSync(path, "utf8"));
205
+ if (!previous || typeof previous !== "object")
206
+ previous = {};
207
+ } catch {}
208
+ const servers = normalizeServers(previous.servers) ?? {};
209
+ delete servers[url];
210
+ previous.servers = servers;
211
+ writeFileSync(path, JSON.stringify(previous, null, 2) + `
212
+ `);
213
+ return path;
214
+ } catch {
215
+ return;
216
+ }
217
+ }
218
+ function loadConfig(pluginDir, options) {
219
+ const cfg = { ...DEFAULTS };
220
+ const emptyMeansUnset = new Set(["workerUrl", "createToken", "urlFile", "spoolDir"]);
221
+ const known = new Set([...Object.keys(DEFAULTS), "shareMode"]);
222
+ for (const path of [...configPaths(pluginDir)].reverse()) {
223
+ try {
224
+ const raw = JSON.parse(readFileSync(path, "utf8"));
225
+ if (!raw || typeof raw !== "object")
226
+ continue;
227
+ for (const [key, value] of Object.entries(raw)) {
228
+ if (key === "servers") {
229
+ const servers = normalizeServers(value);
230
+ if (servers)
231
+ cfg.servers = mergeServerGroups(cfg.servers, servers);
232
+ continue;
233
+ }
234
+ if (!known.has(key))
235
+ continue;
236
+ if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean")
237
+ continue;
238
+ if (typeof value === "string" && value === "" && emptyMeansUnset.has(key))
239
+ continue;
240
+ cfg[key] = value;
241
+ }
242
+ } catch {}
243
+ }
244
+ if (options && typeof options === "object") {
245
+ const serversOpt = normalizeServers(options.servers);
246
+ if (serversOpt)
247
+ cfg.servers = mergeServerGroups(cfg.servers, serversOpt);
248
+ for (const key of Object.keys(DEFAULTS)) {
249
+ const value = options[key];
250
+ if (value === undefined || value === null)
251
+ continue;
252
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "string") {
253
+ cfg[key] = value;
254
+ }
255
+ }
256
+ const mode = str(options.shareMode);
257
+ if (mode === "all" || mode === "manual")
258
+ cfg.shareMode = mode;
259
+ }
260
+ const env = process.env;
261
+ if (env.OPENSHARE_WORKER_URL)
262
+ cfg.workerUrl = env.OPENSHARE_WORKER_URL;
263
+ if (env.OPENSHARE_CREATE_TOKEN)
264
+ cfg.createToken = env.OPENSHARE_CREATE_TOKEN;
265
+ if (env.OPENSHARE_DEBOUNCE_MS)
266
+ cfg.debounceMs = num(env.OPENSHARE_DEBOUNCE_MS, cfg.debounceMs);
267
+ if (env.OPENSHARE_COMPLETED_DELAY_MS !== undefined)
268
+ cfg.completedDelayMs = num(env.OPENSHARE_COMPLETED_DELAY_MS, cfg.completedDelayMs);
269
+ if (env.OPENSHARE_MODEL_LABEL !== undefined)
270
+ cfg.modelLabel = env.OPENSHARE_MODEL_LABEL;
271
+ if (env.OPENSHARE_TTL_DAYS)
272
+ cfg.ttlDays = num(env.OPENSHARE_TTL_DAYS, cfg.ttlDays);
273
+ if (env.OPENSHARE_AUTH_LOGIN !== undefined)
274
+ cfg.authLogin = env.OPENSHARE_AUTH_LOGIN;
275
+ if (env.OPENSHARE_AUTH_PASSWORD !== undefined)
276
+ cfg.authPassword = env.OPENSHARE_AUTH_PASSWORD;
277
+ if (env.OPENSHARE_URL_FILE !== undefined)
278
+ cfg.urlFile = env.OPENSHARE_URL_FILE;
279
+ if (env.OPENSHARE_SPOOL_DIR)
280
+ cfg.spoolDir = env.OPENSHARE_SPOOL_DIR;
281
+ if (env.OPENSHARE_ANNOUNCE_STDERR !== undefined)
282
+ cfg.announceStderr = !/^(0|false|no|off)$/i.test(env.OPENSHARE_ANNOUNCE_STDERR.trim());
283
+ if (env.OPENSHARE_SHARE_MODE === "all" || env.OPENSHARE_SHARE_MODE === "manual")
284
+ cfg.shareMode = env.OPENSHARE_SHARE_MODE;
285
+ if (cfg.urlFile.trim().toLowerCase() === URL_FILE_NONE)
286
+ cfg.urlFile = URL_FILE_NONE;
287
+ return cfg;
288
+ }
289
+ function globalConfigDir(env) {
290
+ if (env["OPENCODE_CONFIG_DIR"])
291
+ return env["OPENCODE_CONFIG_DIR"];
292
+ if (env["XDG_CONFIG_HOME"])
293
+ return join(env["XDG_CONFIG_HOME"], "opencode");
294
+ const home = process.env.USERPROFILE ?? process.env.HOME ?? "";
295
+ return join(home, ".config", "opencode");
296
+ }
297
+ var METADATA_KEY = "openshare", URL_FILE_NONE = "none", DEFAULTS, CONFIG_NAME = "openshare.config.json", LOCAL_NAME = "openshare.local.json";
298
+ var init_config = __esm(() => {
299
+ DEFAULTS = {
300
+ workerUrl: "",
301
+ createToken: "",
302
+ debounceMs: 1e4,
303
+ completedDelayMs: 5000,
304
+ modelLabel: "$Provider/$Model",
305
+ ttlDays: 90,
306
+ openTasksInNewTab: true,
307
+ authLogin: "",
308
+ authPassword: "",
309
+ spoolDir: "",
310
+ urlFile: "",
311
+ announceStderr: true
312
+ };
313
+ });
314
+
315
+ // src/sidebar.tsx
316
+ import { effect as _$effect } from "@opentui/solid";
317
+ import { insertNode as _$insertNode } from "@opentui/solid";
318
+ import { insert as _$insert } from "@opentui/solid";
319
+ import { setProp as _$setProp } from "@opentui/solid";
320
+ import { createElement as _$createElement } from "@opentui/solid";
321
+ import { appendFileSync } from "fs";
322
+ import { tmpdir } from "os";
323
+ import { join as join2 } from "path";
324
+ function dbg(v, depth = 0) {
325
+ const t = typeof v;
326
+ if (v === null)
327
+ return "null";
328
+ if (t === "string")
329
+ return `str(${v.length}):${JSON.stringify(v.slice(0, 60))}`;
330
+ if (t === "number" || t === "boolean" || t === "bigint")
331
+ return `${t}:${String(v)}`;
332
+ if (t === "undefined")
333
+ return "undef";
334
+ if (t === "function")
335
+ return `fn:${v.name || "anon"}`;
336
+ if (Array.isArray(v))
337
+ return `arr[${v.length}]:[${v.slice(0, 6).map((x) => dbg(x, depth + 1)).join(",")}]`;
338
+ if (depth >= 3)
339
+ return "obj:\u2026";
340
+ try {
341
+ const keys = Object.keys(v);
342
+ return `obj{${keys.slice(0, 8).join(",")}}` + (depth < 2 ? `{${keys.slice(0, 8).map((k) => `${k}=${dbg(v[k], depth + 1)}`).join(",")}}` : "");
343
+ } catch {
344
+ return "obj:?}";
345
+ }
346
+ }
347
+ function dbgEl(el, depth = 0) {
348
+ if (el === null || el === undefined || typeof el === "string" || typeof el === "number" || typeof el === "boolean")
349
+ return dbg(el);
350
+ if (Array.isArray(el))
351
+ return `[${el.map((x) => dbgEl(x, depth)).join(",")}]`;
352
+ if (typeof el === "function")
353
+ return "fn-node";
354
+ if (depth >= 4)
355
+ return "\u2026";
356
+ try {
357
+ const rec = el;
358
+ const t = dbg(rec.t ?? rec.type, 2);
359
+ const kids = rec.children;
360
+ const props = Object.keys(rec).filter((k) => !["t", "type", "children"].includes(k));
361
+ return `<${t} ${props.join(" ")}>${kids === undefined ? "" : dbgEl(kids, depth + 1)}</${t}>`;
362
+ } catch (e) {
363
+ return `el?-${dbg(e)}`;
364
+ }
365
+ }
366
+ function dbgLog(line) {
367
+ try {
368
+ appendFileSync(DBG_FILE, `${new Date().toISOString()} ${line}
369
+ `);
370
+ } catch {}
371
+ }
372
+ function goneVerdict(sid, server, shareId) {
373
+ const key = `${sid}|${server}`;
374
+ const cached = goneCache.get(key);
375
+ if (cached && Date.now() - cached.at < GONE_TTL_MS)
376
+ return cached;
377
+ verifyShare(server, shareId).then((status) => {
378
+ goneCache.set(key, {
379
+ status,
380
+ at: Date.now()
381
+ });
382
+ });
383
+ return cached;
384
+ }
385
+ function createSidebarSlot(api, opts) {
386
+ dbgLog(`createSidebarSlot called; api.state=${!!api.state} api.theme=${!!api.theme} api.version=${dbg(api.app?.version)}`);
387
+ return {
388
+ slots: {
389
+ sidebar_title: (ctx, props) => {
390
+ const seq = ++renderSeq;
391
+ try {
392
+ dbgLog(`render#${seq} ctx=${dbg(ctx)} props=${dbg(props)}`);
393
+ const sid = typeof props?.session_id === "string" ? props.session_id : "";
394
+ const title = String(props?.title ?? "") || "Session";
395
+ const session = sid ? api.state.session.get(sid) : undefined;
396
+ const binding = bindingOf(session);
397
+ const allBindings = bindingsOf(session);
398
+ const liveCount = allBindings.filter((b) => b.url && b.enabled !== false).length;
399
+ const status = session?.time?.archived ? undefined : binding?.url ? binding.enabled === false ? "attached" : "live" : undefined;
400
+ const checked = allBindings.filter((b) => b.url && b.enabled !== false && b.id && b.server);
401
+ const verdicts = checked.map((b) => goneVerdict(sid, b.server, b.id));
402
+ const anyGone = verdicts.some((v) => v?.status === "gone");
403
+ const multi = liveCount > 1 ? `\xB7${liveCount} ` : "";
404
+ const theme = ctx?.theme?.current ?? api.theme.current;
405
+ dbgLog(`render#${seq} sid=${dbg(sid)} title=${dbg(title)} session=${session ? "found" : "MISSING"} binding=${binding ? `id=${dbg(binding.id)} enabled=${dbg(binding.enabled)}` : "none"} status=${dbg(status)} theme=${dbg(theme)}`);
406
+ const copyLink = () => {
407
+ if (anyGone) {
408
+ opts?.onShareCommand?.();
409
+ return;
410
+ }
411
+ const url = binding?.url;
412
+ if (!url)
413
+ return;
414
+ let copied = false;
415
+ try {
416
+ const clip = navigator.clipboard;
417
+ if (typeof clip?.writeText === "function") {
418
+ clip.writeText(url).then(() => {}, () => {});
419
+ copied = true;
420
+ }
421
+ } catch {}
422
+ try {
423
+ process.stdout.write(`\x1B]52;c;${Buffer.from(url, "utf8").toString("base64")}\x07`);
424
+ copied = true;
425
+ } catch {}
426
+ try {
427
+ api.ui.toast({
428
+ variant: copied ? "success" : "info",
429
+ title: "OpenShare",
430
+ message: copied ? `Share link copied: ${url}` : `Share link: ${url}`,
431
+ duration: 6000
432
+ });
433
+ } catch {}
434
+ };
435
+ const el = (() => {
436
+ var _el$ = _$createElement("box"), _el$2 = _$createElement("text");
437
+ _$insertNode(_el$, _el$2);
438
+ _$setProp(_el$, "flexDirection", "column");
439
+ _$setProp(_el$, "paddingRight", 1);
440
+ _$insert(_el$2, title);
441
+ _$insert(_el$, status ? (() => {
442
+ var _el$3 = _$createElement("box"), _el$4 = _$createElement("text");
443
+ _$insertNode(_el$3, _el$4);
444
+ _$setProp(_el$3, "onMouseDown", copyLink);
445
+ _$insert(_el$4, anyGone ? `\u2298 re:share \u2014 gone (removed/expired) \u2014 click to fix` : status === "live" ? `\u25C9 re:share ${multi}\u2014 click to copy link` : "\u25C9 re:share (dormant)");
446
+ _$effect((_$p) => _$setProp(_el$4, "fg", anyGone ? theme.error : binding?.enabled === false ? theme.warning : theme.success, _$p));
447
+ return _el$3;
448
+ })() : null, null);
449
+ _$effect((_$p) => _$setProp(_el$2, "fg", theme.text, _$p));
450
+ return _el$;
451
+ })();
452
+ dbgLog(`render#${seq} RETURNS ${dbgEl(el)}`);
453
+ return el;
454
+ } catch (error) {
455
+ dbgLog(`render#${seq} THREW ${error?.stack ?? dbg(error)} \u2014 rethrowing`);
456
+ throw error;
457
+ }
458
+ }
459
+ }
460
+ };
461
+ }
462
+ var DBG_FILE, renderSeq = 0, GONE_TTL_MS = 60000, goneCache;
463
+ var init_sidebar = __esm(() => {
464
+ init_share_flow();
465
+ DBG_FILE = join2(tmpdir(), "openshare-tui-debug.log");
466
+ goneCache = new Map;
467
+ });
468
+
469
+ // src/share-flow.ts
470
+ var exports_share_flow = {};
471
+ __export(exports_share_flow, {
472
+ workerDeleteShare: () => workerDeleteShare,
473
+ waitForBinding: () => waitForBinding,
474
+ verifyShare: () => verifyShare,
475
+ setShareFlag: () => setShareFlag,
476
+ serverEntries: () => serverEntries,
477
+ saveServerEntry: () => saveServerEntry,
478
+ saveConfig: () => saveConfig,
479
+ runUnshareFlow: () => runUnshareFlow,
480
+ runShareFlow: () => runShareFlow,
481
+ rotateAtServer: () => rotateAtServer,
482
+ resolveRoot: () => resolveRoot,
483
+ readBindings: () => readBindings,
484
+ readBinding: () => readBinding,
485
+ probeWorker: () => probeWorker,
486
+ probeAuthMode: () => probeAuthMode,
487
+ pluginConfigPaths: () => pluginConfigPaths,
488
+ patchSessionBinding: () => patchSessionBinding,
489
+ normServerUrl: () => normServerUrl,
490
+ isConfigured: () => isConfigured,
491
+ hostOf: () => hostOf,
492
+ enrollAtServer: () => enrollAtServer,
493
+ disconnectAtServer: () => disconnectAtServer,
494
+ deleteServerEntry: () => deleteServerEntry,
495
+ currentConfig: () => currentConfig,
496
+ confirmShare: () => confirmShare,
497
+ clearShareFlag: () => clearShareFlag,
498
+ clearGoneBindingEntries: () => clearGoneBindingEntries,
499
+ bindingsOf: () => bindingsOf,
500
+ bindingOf: () => bindingOf,
501
+ adminTokenFor: () => adminTokenFor,
502
+ adminClient: () => adminClient,
503
+ activeServer: () => activeServer,
504
+ CONSENT_KV_KEY: () => CONSENT_KV_KEY
505
+ });
506
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
507
+ function pluginConfigPaths(baseDir = moduleDir()) {
508
+ return configPaths(baseDir);
509
+ }
510
+ function currentConfig(baseDir = moduleDir()) {
511
+ return loadConfig(baseDir);
512
+ }
513
+ function saveConfig(cfg, target, baseDir = moduleDir()) {
514
+ const path = target ?? configHomePath();
515
+ if (!path)
516
+ return;
517
+ try {
518
+ const previous = existsSync2(path) ? (() => {
519
+ try {
520
+ return JSON.parse(readFileSync2(path, "utf8"));
521
+ } catch {
522
+ return {};
523
+ }
524
+ })() : {};
525
+ const next = { ...previous };
526
+ for (const key of [
527
+ "workerUrl",
528
+ "createToken",
529
+ "servers",
530
+ "debounceMs",
531
+ "completedDelayMs",
532
+ "modelLabel",
533
+ "ttlDays",
534
+ "openTasksInNewTab",
535
+ "authLogin",
536
+ "authPassword",
537
+ "spoolDir",
538
+ "urlFile",
539
+ "shareMode",
540
+ "announceStderr"
541
+ ]) {
542
+ next[key] = cfg[key];
543
+ }
544
+ writeFileSync2(path, JSON.stringify(next, null, 2) + `
545
+ `);
546
+ return { path };
547
+ } catch {
548
+ return;
549
+ }
550
+ }
551
+ function isConfigured(cfg) {
552
+ return Boolean(resolveServer(cfg)?.url);
553
+ }
554
+ function activeServer(cfg = currentConfig()) {
555
+ return resolveServer(cfg);
556
+ }
557
+ function serverEntries(cfg = currentConfig()) {
558
+ const merged = {};
559
+ if (cfg.workerUrl)
560
+ merged[cfg.workerUrl] = { token: cfg.createToken || "", active: true };
561
+ if (cfg.servers)
562
+ for (const [url, entry] of Object.entries(cfg.servers))
563
+ merged[url] = { ...merged[url], ...entry };
564
+ const urls = Object.keys(merged);
565
+ const activeUrl = urls.find((u) => merged[u].active) ?? urls[0];
566
+ return urls.map((url) => ({ url, entry: merged[url], active: url === activeUrl }));
567
+ }
568
+ function bindingsOf(info) {
569
+ const raw = info?.metadata?.[METADATA_KEY];
570
+ if (!raw || typeof raw !== "object")
571
+ return [];
572
+ const record = raw;
573
+ const opts = record.opts ?? undefined;
574
+ const enabled = record.enabled === true ? true : undefined;
575
+ const out = [];
576
+ if (record.servers && typeof record.servers === "object") {
577
+ for (const [server, entry] of Object.entries(record.servers)) {
578
+ if (!entry || typeof entry !== "object")
579
+ continue;
580
+ const e = entry;
581
+ if (typeof e.id !== "string" || typeof e.secret !== "string" || !e.id)
582
+ continue;
583
+ const collided = typeof e.collided === "string" ? e.collided : undefined;
584
+ out.push({
585
+ id: e.id,
586
+ secret: e.secret,
587
+ rev: typeof e.rev === "number" ? e.rev : 0,
588
+ url: typeof e.url === "string" ? e.url : "",
589
+ enabled,
590
+ opts,
591
+ server,
592
+ collided
593
+ });
594
+ }
595
+ return out;
596
+ }
597
+ if (typeof record.id === "string" && typeof record.secret === "string" && record.id) {
598
+ const collided = typeof record.collided === "string" ? record.collided : undefined;
599
+ out.push({
600
+ id: record.id,
601
+ secret: record.secret,
602
+ rev: typeof record.rev === "number" ? record.rev : 0,
603
+ url: typeof record.url === "string" ? record.url : "",
604
+ enabled,
605
+ opts,
606
+ server: "",
607
+ collided
608
+ });
609
+ }
610
+ return out;
611
+ }
612
+ async function readBindings(client, sid) {
613
+ return bindingsOf(await getSession(client, sid));
614
+ }
615
+ function hostOf(url) {
616
+ try {
617
+ return new URL(url).host;
618
+ } catch {
619
+ return url;
620
+ }
621
+ }
622
+ async function patchSessionBinding(client, sid, mutator) {
623
+ const info = await getSession(client, sid);
624
+ if (!info)
625
+ return false;
626
+ const metadata = { ...info.metadata ?? {} };
627
+ const raw = info.metadata?.[METADATA_KEY];
628
+ const binding = raw && typeof raw === "object" ? { ...raw } : { v: 2, servers: {} };
629
+ const servers = binding.servers && typeof binding.servers === "object" ? { ...binding.servers } : {};
630
+ mutator(servers, binding);
631
+ binding.servers = servers;
632
+ metadata[METADATA_KEY] = binding;
633
+ return patchMetadata(client, sid, metadata);
634
+ }
635
+ function bindingOf(info) {
636
+ const all = bindingsOf(info);
637
+ if (!all.length)
638
+ return;
639
+ const active = activeServer()?.url;
640
+ return all.find((b) => b.server === active) ?? all[0];
641
+ }
642
+ async function getSession(client, sid) {
643
+ if (!client?.session?.get)
644
+ return;
645
+ try {
646
+ const res = await client.session.get({ path: { id: sid } });
647
+ const info = res?.data ?? res;
648
+ return info && typeof info === "object" ? info : undefined;
649
+ } catch {
650
+ return;
651
+ }
652
+ }
653
+ async function patchMetadata(client, sid, metadata) {
654
+ if (!client?.session?.update) {
655
+ dbgLog(`patchMetadata(${sid}): no client.session.update`);
656
+ return false;
657
+ }
658
+ try {
659
+ const res = await client.session.update({ path: { id: sid }, body: { metadata } });
660
+ if (res && typeof res === "object" && "error" in res && res.error) {
661
+ const err = res.error;
662
+ dbgLog(`patchMetadata(${sid}) ERROR: ${err?.name}: ${err?.data?.message ?? ""} ref=${err?.data?.ref ?? ""}`);
663
+ return false;
664
+ }
665
+ dbgLog(`patchMetadata(${sid}): ok res=${dbg(res)}`);
666
+ return true;
667
+ } catch (e) {
668
+ dbgLog(`patchMetadata(${sid}) THREW: ${e?.stack ?? e}`);
669
+ return false;
670
+ }
671
+ }
672
+ async function resolveRoot(client, sid) {
673
+ let current = sid;
674
+ for (let hops = 0;hops < 10; hops++) {
675
+ const info = await getSession(client, current);
676
+ const parent = info?.parentID;
677
+ if (typeof parent !== "string" || !parent || parent === current)
678
+ return current;
679
+ current = parent;
680
+ }
681
+ return current;
682
+ }
683
+ async function readBinding(client, sid) {
684
+ return bindingOf(await getSession(client, sid));
685
+ }
686
+ async function setShareFlag(client, sid, opts) {
687
+ const info = await getSession(client, sid);
688
+ if (!info)
689
+ return false;
690
+ const metadata = { ...info.metadata ?? {} };
691
+ const existing = info.metadata?.[METADATA_KEY];
692
+ const value = existing && typeof existing === "object" ? { ...existing } : { v: 2, servers: {} };
693
+ value.enabled = true;
694
+ if (opts && Object.keys(opts).length)
695
+ value.opts = opts;
696
+ metadata[METADATA_KEY] = value;
697
+ return patchMetadata(client, sid, metadata);
698
+ }
699
+ async function clearShareFlag(client, sid) {
700
+ const info = await getSession(client, sid);
701
+ if (!info)
702
+ return false;
703
+ const metadata = { ...info.metadata ?? {} };
704
+ delete metadata[METADATA_KEY];
705
+ return patchMetadata(client, sid, metadata);
706
+ }
707
+ async function workerDeleteShare(workerUrl, id, secret) {
708
+ try {
709
+ const res = await fetch(`${workerUrl.replace(/\/$/, "")}/api/share/${id}`, {
710
+ method: "DELETE",
711
+ headers: { "content-type": "application/json" },
712
+ body: JSON.stringify({ secret }),
713
+ signal: AbortSignal.timeout(8000)
714
+ });
715
+ return res.ok;
716
+ } catch {
717
+ return false;
718
+ }
719
+ }
720
+ async function probeWorker(workerUrl) {
721
+ if (!workerUrl)
722
+ return { ok: false, detail: "workerUrl is empty" };
723
+ try {
724
+ const res = await fetch(`${workerUrl.replace(/\/$/, "")}/health`, { signal: AbortSignal.timeout(6000) });
725
+ if (!res.ok)
726
+ return { ok: false, detail: `HTTP ${res.status}` };
727
+ const body = await res.json().catch(() => ({}));
728
+ return { ok: true, detail: body.service ? `service: ${body.service}` : "reachable" };
729
+ } catch (error) {
730
+ return { ok: false, detail: error.message };
731
+ }
732
+ }
733
+ async function verifyShare(workerUrl, shareId) {
734
+ const base = normServerUrl(workerUrl);
735
+ if (!base || !shareId)
736
+ return "unknown";
737
+ try {
738
+ const res = await fetch(`${base}/api/share/${shareId}/data`, { signal: AbortSignal.timeout(4000) });
739
+ if (res.status === 404) {
740
+ const body = await res.json().catch(() => ({}));
741
+ return body.status === "gone" || Object.keys(body).length === 0 ? "gone" : "unknown";
742
+ }
743
+ return "live";
744
+ } catch {
745
+ return "unknown";
746
+ }
747
+ }
748
+ async function clearGoneBindingEntries(host, root, servers) {
749
+ if (!servers.length)
750
+ return false;
751
+ return patchSessionBinding(host.client, root, (map) => {
752
+ for (const server of servers)
753
+ delete map[server];
754
+ });
755
+ }
756
+ function normServerUrl(url) {
757
+ return url.trim().replace(/\/+$/, "");
758
+ }
759
+ async function probeAuthMode(workerUrl) {
760
+ const base = normServerUrl(workerUrl);
761
+ if (!base)
762
+ return { ok: false, detail: "no URL" };
763
+ try {
764
+ const res = await fetch(`${base}/health`, { signal: AbortSignal.timeout(6000) });
765
+ if (!res.ok)
766
+ return { ok: false, detail: `HTTP ${res.status}` };
767
+ const body = await res.json().catch(() => ({}));
768
+ const mode = body.auth?.mode === "public" ? "public" : body.auth?.mode === "private" ? "private" : undefined;
769
+ return { ok: true, mode, detail: mode ? `auth: ${mode}` : "reachable (pre-auth server?)" };
770
+ } catch (error) {
771
+ return { ok: false, detail: error.message };
772
+ }
773
+ }
774
+ async function enrollAtServer(workerUrl, code) {
775
+ const base = normServerUrl(workerUrl);
776
+ try {
777
+ const res = await fetch(`${base}/api/enroll`, {
778
+ method: "POST",
779
+ headers: { "content-type": "application/json" },
780
+ body: JSON.stringify({ code: code || undefined }),
781
+ signal: AbortSignal.timeout(1e4)
782
+ });
783
+ const body = await res.json().catch(() => ({}));
784
+ if (res.ok && body.ok && typeof body.token === "string") {
785
+ return { ok: true, token: body.token, userId: body.userId, label: body.label };
786
+ }
787
+ return { ok: false, error: body.error ?? `HTTP ${res.status}` };
788
+ } catch (error) {
789
+ return { ok: false, error: error.message };
790
+ }
791
+ }
792
+ async function rotateAtServer(workerUrl, token) {
793
+ const base = normServerUrl(workerUrl);
794
+ try {
795
+ const res = await fetch(`${base}/api/token/rotate`, {
796
+ method: "POST",
797
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
798
+ signal: AbortSignal.timeout(1e4)
799
+ });
800
+ const body = await res.json().catch(() => ({}));
801
+ if (res.ok && body.ok && typeof body.token === "string")
802
+ return { ok: true, token: body.token };
803
+ return { ok: false, error: body.error ?? `HTTP ${res.status}` };
804
+ } catch (error) {
805
+ return { ok: false, error: error.message };
806
+ }
807
+ }
808
+ async function disconnectAtServer(workerUrl, token) {
809
+ const base = normServerUrl(workerUrl);
810
+ try {
811
+ const res = await fetch(`${base}/api/token`, {
812
+ method: "DELETE",
813
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
814
+ signal: AbortSignal.timeout(1e4)
815
+ });
816
+ const body = await res.json().catch(() => ({}));
817
+ return { ok: res.ok && body.ok !== false, error: body.error };
818
+ } catch (error) {
819
+ return { ok: false, error: error.message };
820
+ }
821
+ }
822
+ function adminClient(workerUrl, token) {
823
+ const base = normServerUrl(workerUrl);
824
+ const call = async (method, path, body) => {
825
+ try {
826
+ const res = await fetch(`${base}${path}`, {
827
+ method,
828
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
829
+ body: body === undefined ? undefined : JSON.stringify(body),
830
+ signal: AbortSignal.timeout(12000)
831
+ });
832
+ const parsed = await res.json().catch(() => ({}));
833
+ if (!res.ok)
834
+ return { ok: false, status: res.status, error: parsed?.error ?? `HTTP ${res.status}` };
835
+ return { ok: parsed?.ok !== false, status: res.status, data: parsed, error: parsed?.error };
836
+ } catch (error) {
837
+ return { ok: false, status: 0, error: error.message };
838
+ }
839
+ };
840
+ return {
841
+ users: () => call("GET", "/api/admin/users"),
842
+ createUser: (label) => call("POST", "/api/admin/users", { label }),
843
+ freeze: (userId) => call("POST", `/api/admin/users/${userId}/freeze`),
844
+ unfreeze: (userId) => call("POST", `/api/admin/users/${userId}/unfreeze`),
845
+ deleteUser: (userId, mode) => call("DELETE", `/api/admin/users/${userId}`, { mode }),
846
+ tokens: () => call("GET", "/api/admin/tokens"),
847
+ mintToken: (userId, label, limits) => call("POST", "/api/admin/tokens", { userId, label, limits }),
848
+ revokeToken: (tokenId) => call("DELETE", `/api/admin/tokens/${tokenId}`),
849
+ codes: () => call("GET", "/api/admin/codes"),
850
+ createCode: (label, activations) => call("POST", "/api/admin/codes", { label, activations }),
851
+ disableCode: (codeId) => call("DELETE", `/api/admin/codes/${codeId}`),
852
+ shares: () => call("GET", "/api/admin/shares"),
853
+ deleteShare: (shareId) => call("DELETE", `/api/share/${shareId}`),
854
+ reassign: (shareId, userId) => call("POST", `/api/admin/shares/${shareId}/reassign`, { userId }),
855
+ purge: () => call("POST", "/api/admin/purge"),
856
+ health: () => call("GET", "/health")
857
+ };
858
+ }
859
+ function adminTokenFor(url, cfg = currentConfig()) {
860
+ const base = normServerUrl(url);
861
+ const entry = serverEntries(cfg).find((s) => s.url === base);
862
+ return entry?.entry.adminToken ?? (cfg.workerUrl && normServerUrl(cfg.workerUrl) === base ? cfg.createToken : "") ?? "";
863
+ }
864
+ async function waitForBinding(client, sid, timeoutMs = 25000) {
865
+ const start = Date.now();
866
+ while (Date.now() - start < timeoutMs) {
867
+ const binding = await readBinding(client, sid);
868
+ if (binding?.url)
869
+ return binding;
870
+ await sleep(600);
871
+ }
872
+ return;
873
+ }
874
+ async function runShareFlow(host, sessionID) {
875
+ if (!sessionID)
876
+ return { ok: false, message: "No active session to share." };
877
+ const cfg = currentConfig();
878
+ if (!isConfigured(cfg)) {
879
+ return { ok: false, message: "No share server connected \u2014 run /openshare to connect one.", unconfigured: true };
880
+ }
881
+ const root = await resolveRoot(host.client, sessionID);
882
+ const existing = await readBinding(host.client, root);
883
+ if (existing?.url && existing.enabled !== false) {
884
+ await host.clipboard.write(existing.url);
885
+ return { ok: true, message: `Share URL copied to clipboard: ${existing.url}`, url: existing.url };
886
+ }
887
+ if (!host.kv.get(CONSENT_KV_KEY, false)) {
888
+ return { ok: false, message: "consent required" };
889
+ }
890
+ return confirmShare(host, root);
891
+ }
892
+ async function confirmShare(host, rootSessionID) {
893
+ dbgLog(`confirmShare: setShareFlag(${rootSessionID})`);
894
+ const set = await setShareFlag(host.client, rootSessionID);
895
+ dbgLog(`confirmShare: setShareFlag -> ${set}`);
896
+ if (!set)
897
+ return { ok: false, message: "Could not write the share flag into session metadata." };
898
+ const binding = await waitForBinding(host.client, rootSessionID);
899
+ dbgLog(`confirmShare: waitForBinding -> ${binding?.url ? binding.url : "timeout"}`);
900
+ if (!binding?.url) {
901
+ return {
902
+ ok: false,
903
+ message: "Share did not come up (timeout waiting for the URL). Is the worker reachable? Check the server plugin log for [openshare] lines."
904
+ };
905
+ }
906
+ await host.clipboard.write(binding.url);
907
+ return { ok: true, message: `Share URL copied to clipboard: ${binding.url}`, url: binding.url };
908
+ }
909
+ async function runUnshareFlow(host, sessionID) {
910
+ if (!sessionID)
911
+ return { ok: false, message: "No active session." };
912
+ const root = await resolveRoot(host.client, sessionID);
913
+ const info = await getSession(host.client, root);
914
+ const all = bindingsOf(info);
915
+ if (!all.length)
916
+ return { ok: false, message: "This session is not shared." };
917
+ await clearShareFlag(host.client, root);
918
+ let deleted = 0;
919
+ for (const b of all) {
920
+ const bindingOrigin = b.url ? b.url.slice(0, b.url.indexOf("/s/")) : "";
921
+ const deleteBase = bindingOrigin || b.server;
922
+ if (deleteBase && b.id && b.secret) {
923
+ if (await workerDeleteShare(deleteBase, b.id, b.secret))
924
+ deleted++;
925
+ }
926
+ }
927
+ return {
928
+ ok: true,
929
+ message: deleted === all.length ? `Sharing stopped; ${deleted} share page${deleted === 1 ? "" : "s"} deleted.` : `Sharing stopped (${deleted}/${all.length} pages deleted; the rest will follow via the sweeper).`
930
+ };
931
+ }
932
+ var CONSENT_KV_KEY = "openshare_consent", sleep = (ms) => new Promise((r) => setTimeout(r, ms));
933
+ var init_share_flow = __esm(() => {
934
+ init_config();
935
+ init_sidebar();
936
+ });
937
+
938
+ // src/tui.tsx
939
+ import { effect as _$effect3 } from "@opentui/solid";
940
+ import { createTextNode as _$createTextNode2 } from "@opentui/solid";
941
+ import { insertNode as _$insertNode3 } from "@opentui/solid";
942
+ import { insert as _$insert3 } from "@opentui/solid";
943
+ import { setProp as _$setProp3 } from "@opentui/solid";
944
+ import { createElement as _$createElement3 } from "@opentui/solid";
945
+
946
+ // src/commands.ts
947
+ init_share_flow();
948
+
949
+ // src/settings.ts
950
+ init_config();
951
+ init_share_flow();
952
+
953
+ // src/ui-prefs.ts
954
+ var UI_WIDTH_KV = "openshare.ui-width";
955
+ var UI_HEIGHT_PERCENT_KV = "openshare.ui-height-percent";
956
+ var HEIGHT_PERCENT_MIN = 25;
957
+ var HEIGHT_PERCENT_MAX = 100;
958
+ var DIALOG_WIDTH_COLUMNS = { medium: 60, large: 88, xlarge: 116 };
959
+ function clampHeightPercent(value) {
960
+ return Math.max(HEIGHT_PERCENT_MIN, Math.min(HEIGHT_PERCENT_MAX, Math.round(value)));
961
+ }
962
+ function nextDialogWidth(width) {
963
+ return width === "medium" ? "large" : width === "large" ? "xlarge" : "medium";
964
+ }
965
+ function kvWidthOf(kv) {
966
+ const value = kv.get(UI_WIDTH_KV, "large");
967
+ return value === "medium" || value === "large" || value === "xlarge" ? value : "large";
968
+ }
969
+ function kvHeightPercentOf(kv) {
970
+ const value = kv.get(UI_HEIGHT_PERCENT_KV, 50);
971
+ return typeof value === "number" && Number.isFinite(value) ? clampHeightPercent(value) : 50;
972
+ }
973
+
974
+ // src/settings.ts
975
+ function withSource(display, source) {
976
+ return source ? `${display} [${source}]` : display;
977
+ }
978
+ function summarizeSources(sources) {
979
+ const labels = [...new Set(Object.values(sources))];
980
+ return labels.length ? labels.slice(0, 2).join(" + ") + (labels.length > 2 ? "\u2026" : "") : "all defaults";
981
+ }
982
+ function sourcesMessage(sources) {
983
+ const lines = Object.entries(sources).map(([key, source]) => `${key}: ${source}`);
984
+ return lines.length ? lines.join(`
985
+ `) : "All values are defaults \u2014 no config file or env var sets anything yet.";
986
+ }
987
+ function showSelect(ui, props) {
988
+ return ui.showMenu({ title: props.title, options: props.options, current: props.current, hint: props.placeholder });
989
+ }
990
+ function showPrompt(ui, props) {
991
+ return ui.showPrompt(props);
992
+ }
993
+ function showConfirm(ui, props) {
994
+ return ui.showConfirm(props);
995
+ }
996
+ function showInfo(ui, props) {
997
+ return ui.showAlert(props);
998
+ }
999
+ function mask(value) {
1000
+ if (!value)
1001
+ return "(not set)";
1002
+ if (value.length <= 6)
1003
+ return "****";
1004
+ return `${value.slice(0, 3)}${"*".repeat(Math.min(10, value.length - 5))}${value.slice(-2)}`;
1005
+ }
1006
+ var LABELS = {
1007
+ workerUrl: {
1008
+ label: "Worker URL",
1009
+ help: "Your openshare-worker deployment (https://\u2026). Empty disables the plugin.",
1010
+ kind: "text",
1011
+ section: "Connection",
1012
+ info: `The base URL of your share server \u2014 the Cloudflare Worker or the portable Bun server. Every share page is created, synced and deleted through it.
1013
+
1014
+ Legacy single-server key: the servers group below is the canonical multi-server form; this pair still works and synthesizes into it.`,
1015
+ def: "(empty \u2014 plugin disabled)"
1016
+ },
1017
+ createToken: {
1018
+ label: "Create token",
1019
+ help: "The worker's CREATE_TOKEN. Required to create shares.",
1020
+ kind: "password",
1021
+ section: "Connection",
1022
+ info: `The server's root CREATE_TOKEN \u2014 full admin rights (create, edit, delete ANY share, manage users). Kept for legacy setups and hoster machines.
1023
+
1024
+ Regular users should NOT hold the root token: connect via an access code instead (Connect to a server) and the plugin enrolls its own limited user token. The value is masked here but stored as-is in the config file.`,
1025
+ def: "(empty)"
1026
+ },
1027
+ shareMode: {
1028
+ label: "Share mode",
1029
+ help: '"all" shares every session; "manual" needs /share. Default: TUI presence.',
1030
+ kind: "text",
1031
+ options: ["auto", "all", "manual"],
1032
+ section: "Sharing",
1033
+ info: `all: every NEW session shares automatically (CI posture). Sessions that existed before the plugin started are never auto-shared \u2014 share those explicitly. manual: sessions stay private until you run /share.
1034
+
1035
+ Default (empty): TUI presence decides \u2014 installing the TUI part means manual, server-only means all.`,
1036
+ def: "auto (TUI presence)"
1037
+ },
1038
+ debounceMs: {
1039
+ label: "Sync interval (ms)",
1040
+ help: "Max time between syncs while streaming; anything batched rides the next one. Turn-end batches flush immediately. Default 10000; clamped into the server policy window.",
1041
+ kind: "number",
1042
+ section: "Sharing",
1043
+ info: `A rate limit anchored at the previous flush, not a restart-on-every-event debounce: an update arriving AFTER the interval flushes instantly; anything earlier rides the next tick. Turn-end batches (idle) always flush immediately regardless of this value.
1044
+
1045
+ The server clamps it into its allowed window (DEBOUNCE_MIN_MS..DEBOUNCE_MAX_MS, default 1000..60000).`,
1046
+ def: "10000"
1047
+ },
1048
+ completedDelayMs: {
1049
+ label: "Completed delay (ms)",
1050
+ help: "Idle this long before the share says completed (default 5000, 0 = immediate). Busy cancels; opencode exit flushes. Clamped by the server.",
1051
+ kind: "number",
1052
+ section: "Sharing",
1053
+ info: `opencode's idle signal is authoritative turn end (busy is held through tool runs and permission waits). This delay only smooths the gap before an automation's NEXT prompt \u2014 if a new turn starts inside the window the timer cancels and the share stays live.
1054
+
1055
+ A graceful opencode exit flushes the pending flip, so CI runs land their final status.`,
1056
+ def: "5000"
1057
+ },
1058
+ modelLabel: {
1059
+ label: "Model label template",
1060
+ help: "$Provider/$Model placeholders; literal text as-is; empty hides the model.",
1061
+ kind: "text",
1062
+ section: "Sharing",
1063
+ info: 'How the model name appears on the share page. "$Provider/$Model" renders e.g. "closedrouter/zai/glm-5.3". Any literal text is used as-is; empty string hides the model line entirely.',
1064
+ def: '"$Provider/$Model"'
1065
+ },
1066
+ ttlDays: {
1067
+ label: "TTL (days)",
1068
+ help: "Share lifetime; the worker caps it at its MAX_TTL_DAYS.",
1069
+ kind: "number",
1070
+ section: "Sharing",
1071
+ info: `How long pages live before the server's sweeper deletes them. Capped server-side: the deployment's MAX_TTL_DAYS and the token's maxTtlDays limit both apply.
1072
+
1073
+ A per-session override lives in the This-session section; it is sent at create/attach time.`,
1074
+ def: "worker cap (90 on the reference deployment)"
1075
+ },
1076
+ openTasksInNewTab: {
1077
+ label: "Open tasks in new tab",
1078
+ help: "Viewer: task rows open subagent pages in a new tab.",
1079
+ kind: "text",
1080
+ options: ["true", "false"],
1081
+ section: "Sharing",
1082
+ info: "Viewer behavior only: clicking a task (subagent) row opens its sub-page in a new browser tab instead of navigating. true/false.",
1083
+ def: "true"
1084
+ },
1085
+ authLogin: {
1086
+ label: "Read auth: login",
1087
+ help: "Optional read gate for share pages. Empty removes protection on the next attach.",
1088
+ kind: "text",
1089
+ section: "Read auth",
1090
+ info: `Optional username for a login gate on NEW shares. Viewers must enter login+password (PBKDF2-hashed server-side, 7-day cookie, lockout after failures).
1091
+
1092
+ Write access is a separate lock \u2014 a login never lets anyone edit. Removing both values removes the gate on the next attach.`,
1093
+ def: "(empty \u2014 no gate)"
1094
+ },
1095
+ authPassword: {
1096
+ label: "Read auth: password",
1097
+ help: "Optional read gate for share pages.",
1098
+ kind: "password",
1099
+ section: "Read auth",
1100
+ info: "The password half of the optional read gate. Stored in plain text in the config file \u2014 prefer per-session auth for anything sensitive. Only the hash ever reaches the server.",
1101
+ def: "(empty)"
1102
+ },
1103
+ spoolDir: {
1104
+ label: "Spool directory",
1105
+ help: "Temporary unacked-frame buffer (default: XDG data home).",
1106
+ kind: "text",
1107
+ section: "Advanced",
1108
+ info: "Where un-acked sync frames buffer on disk (one file per session per server). They replay automatically on reconnect. The binding in session metadata is the source of truth \u2014 the spool is an optimization, safe to clear while idle.",
1109
+ def: "XDG data home"
1110
+ },
1111
+ urlFile: {
1112
+ label: "URL file",
1113
+ help: 'File each share URL is appended to (all-mode). Empty = default openshare.urls.txt in the session directory; "none" = no URL file.',
1114
+ kind: "text",
1115
+ section: "Advanced",
1116
+ info: `All-mode CI surface: every created share URL is APPENDED to this file and announced on stderr. Empty uses openshare.urls.txt in the session's working directory; "none" disables the file; a path writes there.
1117
+
1118
+ Manual mode (TUI) never writes it \u2014 the link lives in the TUI.`,
1119
+ def: "openshare.urls.txt (session dir)"
1120
+ },
1121
+ announceStderr: {
1122
+ label: "Announce on stderr",
1123
+ help: '"true"/"false": the [openshare] live: and url file: log lines. false silences them; the share itself is unaffected.',
1124
+ kind: "text",
1125
+ options: ["true", "false"],
1126
+ section: "Advanced",
1127
+ info: "Whether the informational [openshare] lines reach stderr in all-mode (CI logs). Error-class lines (worker down, refused, timeouts) ALWAYS reach stderr regardless. In manual mode stderr is errors-only by design.",
1128
+ def: "true"
1129
+ }
1130
+ };
1131
+ function helpForKey(key, cfg, sources) {
1132
+ const meta = LABELS[key];
1133
+ if (!meta)
1134
+ return;
1135
+ const lines = [meta.info ?? meta.help];
1136
+ lines.push("");
1137
+ lines.push(`Current: ${displayValue(key, cfg)}`);
1138
+ if (sources[key])
1139
+ lines.push(`Source: ${sources[key]} (overrides anything written below it)`);
1140
+ if (meta.def)
1141
+ lines.push(`Default: ${meta.def}`);
1142
+ return lines.join(`
1143
+ `);
1144
+ }
1145
+ function displayValue(key, cfg) {
1146
+ if (key === "createToken" || key === "authPassword")
1147
+ return mask(String(cfg[key] ?? ""));
1148
+ if (key === "openTasksInNewTab")
1149
+ return String(cfg.openTasksInNewTab !== false);
1150
+ if (key === "announceStderr")
1151
+ return String(cfg.announceStderr !== false);
1152
+ const value = cfg[key];
1153
+ return value === undefined || value === "" ? "(not set)" : String(value);
1154
+ }
1155
+ async function editKey(host, cfg, key) {
1156
+ const meta = LABELS[key];
1157
+ if (!meta)
1158
+ return;
1159
+ if (meta.options) {
1160
+ const current = String(cfg[key] ?? "");
1161
+ const picked = await showSelect(host.ui, {
1162
+ title: meta.label,
1163
+ options: meta.options.map((option) => ({
1164
+ title: option,
1165
+ value: option,
1166
+ description: option === current ? "\u25CF current" : ""
1167
+ })),
1168
+ current
1169
+ });
1170
+ if (picked === undefined)
1171
+ return;
1172
+ const next2 = { ...cfg };
1173
+ if (key === "shareMode")
1174
+ next2[key] = picked === "auto" ? undefined : picked;
1175
+ else if (key === "openTasksInNewTab" || key === "announceStderr")
1176
+ next2[key] = picked === "true";
1177
+ else
1178
+ next2[key] = picked;
1179
+ return next2;
1180
+ }
1181
+ const raw = await showPrompt(host.ui, {
1182
+ title: `${meta.label} \u2014 ${displayValue(key, cfg)}`,
1183
+ placeholder: meta.kind === "number" ? "number" : meta.kind === "password" ? "paste token" : "value",
1184
+ value: meta.kind === "password" ? "" : String(cfg[key] ?? "")
1185
+ });
1186
+ if (raw === undefined)
1187
+ return;
1188
+ const next = { ...cfg };
1189
+ if (meta.kind === "number") {
1190
+ const parsed = Number(raw);
1191
+ if (!Number.isFinite(parsed) || parsed <= 0) {
1192
+ host.ui.toast({ variant: "error", title: "OpenShare", message: `${meta.label}: "${raw}" is not a positive number.` });
1193
+ return;
1194
+ }
1195
+ next[key] = Math.floor(parsed);
1196
+ } else if (key === "urlFile") {
1197
+ next[key] = raw.trim().toLowerCase() === "none" ? "none" : raw;
1198
+ } else {
1199
+ next[key] = raw;
1200
+ }
1201
+ return next;
1202
+ }
1203
+ async function connectServerFlow(host) {
1204
+ const raw = await showPrompt(host.ui, { title: "Share server URL", placeholder: "https://your-openshare-host" });
1205
+ if (raw === undefined || raw === "")
1206
+ return { ok: false, message: "Cancelled." };
1207
+ const url = normServerUrl(raw);
1208
+ if (!/^https?:\/\//.test(url))
1209
+ return { ok: false, message: "URL must start with http:// or https://" };
1210
+ host.ui.toast({ variant: "info", title: "OpenShare", message: `Probing ${url}\u2026` });
1211
+ const probe = await probeAuthMode(url);
1212
+ if (!probe.ok)
1213
+ return { ok: false, message: `${url}: ${probe.detail}` };
1214
+ let code = "";
1215
+ if (probe.mode === "private") {
1216
+ const typed = await showPrompt(host.ui, { title: `${url} needs an access code`, placeholder: "osc_\u2026 (from the hoster)" });
1217
+ if (typed === undefined)
1218
+ return { ok: false, message: "Cancelled \u2014 private servers need a code." };
1219
+ code = typed.trim();
1220
+ }
1221
+ const res = await enrollAtServer(url, code);
1222
+ if (!res.ok)
1223
+ return { ok: false, message: `Enrollment refused: ${res.error}` };
1224
+ for (const other of serverEntries()) {
1225
+ if (other.url !== url)
1226
+ saveServerEntry(other.url, { active: undefined });
1227
+ }
1228
+ saveServerEntry(url, { token: res.token, label: res.label, code: code || undefined, active: true });
1229
+ return { ok: true, message: `Connected to ${url} as "${res.label}" \u2014 sharing goes there now.`, url };
1230
+ }
1231
+ async function serverMenu(host, url) {
1232
+ while (true) {
1233
+ const entries = serverEntries();
1234
+ const mine = entries.find((s) => s.url === url);
1235
+ if (!mine)
1236
+ return;
1237
+ const holdsRoot = !!mine.entry.token && mine.entry.token === (mine.entry.adminToken ?? adminTokenFor(url));
1238
+ const cred = holdsRoot ? `ROOT token ${mask(mine.entry.token)} \u2014 admin, no user identity` : mine.entry.token ? `token ${mask(mine.entry.token)}` : mine.entry.code ? "no token (code saved, enrolls on demand)" : "no token (public server, enrolls on demand)";
1239
+ const options = [
1240
+ { title: url, value: "info", description: `${mine.entry.label ?? "(no label)"} \xB7 ${cred}${mine.active ? " \xB7 ACTIVE" : ""}`, disabled: true }
1241
+ ];
1242
+ if (!mine.active)
1243
+ options.push({ title: "Set active", value: "active", description: "new sessions share here" });
1244
+ if (holdsRoot) {
1245
+ options.push({
1246
+ title: "Get a user token",
1247
+ value: "adopt",
1248
+ description: "admin-mint YOUR user token; root stays saved",
1249
+ help: `You hold the server's ROOT token \u2014 full admin, but root is not a user identity: rotate/disconnect act on user tokens, and a root-holder "user" doesn't exist.
1250
+
1251
+ This creates your own user on ${url} (label prompt, prefilled), mints its token, and saves it as this server's user token. The root token stays in adminToken for the admin panel. You become admin AND user.`
1252
+ });
1253
+ }
1254
+ if (!mine.entry.token || holdsRoot)
1255
+ options.push({ title: "Enroll now", value: "enroll", description: holdsRoot ? "join as a user via code/public enrollment" : "get a user token immediately" });
1256
+ if (mine.entry.token && !holdsRoot)
1257
+ options.push({ title: "Rotate token", value: "rotate", description: "new token, old one revoked" });
1258
+ if (mine.entry.token && !holdsRoot)
1259
+ options.push({ title: "Disconnect", value: "disconnect", description: "revoke the token server-side and forget it", danger: true });
1260
+ options.push({
1261
+ title: "Admin panel",
1262
+ value: "admin",
1263
+ description: "root-token console: users, codes, tokens, shares",
1264
+ help: `Opens the admin console for THIS server: manage users (freeze/unfreeze/mint/delete), revoke tokens, create access codes, list/reassign/delete shares, purge.
1265
+
1266
+ Needs the server's root CREATE_TOKEN (saved admin token used first; prompted otherwise, and then saved). User tokens cannot administer.`
1267
+ });
1268
+ options.push({ title: "Test this server", value: "test", description: "probe /health" });
1269
+ options.push({ title: "Remove from config", value: "remove", description: "drop the saved membership (server-side state untouched)", danger: true });
1270
+ options.push({ title: "Back", value: "__back" });
1271
+ const choice = await showSelect(host.ui, { title: "Share server", options });
1272
+ if (choice === undefined || choice === "__back")
1273
+ return;
1274
+ if (choice === "active") {
1275
+ for (const other of entries) {
1276
+ if (other.url !== url)
1277
+ saveServerEntry(other.url, { active: undefined });
1278
+ }
1279
+ saveServerEntry(url, { active: true });
1280
+ host.ui.toast({ variant: "success", title: "OpenShare", message: `${url} is now the active server.` });
1281
+ return;
1282
+ }
1283
+ if (choice === "adopt") {
1284
+ const rootToken = mine.entry.adminToken ?? adminTokenFor(url);
1285
+ if (!rootToken) {
1286
+ await showInfo(host.ui, { title: "No admin token", message: "The root token is needed to mint your user token. Open the Admin panel once (it saves the token), then retry." });
1287
+ continue;
1288
+ }
1289
+ const prefill = `admin-${hostOf(url).replace(/[^a-z0-9-]/gi, "") || "user"}`;
1290
+ const label = (await showPrompt(host.ui, { title: "Your user label on this server", placeholder: "shown in the admin user list", value: prefill }))?.trim();
1291
+ if (!label)
1292
+ continue;
1293
+ const client = adminClient(url, rootToken);
1294
+ const made = await client.createUser(label);
1295
+ if (!made.ok || !made.data?.userId) {
1296
+ await showInfo(host.ui, { title: "User creation failed", message: `${url}: ${made.error ?? "unknown error"}` });
1297
+ continue;
1298
+ }
1299
+ const minted = await client.mintToken(made.data.userId, label);
1300
+ if (!minted.ok || !minted.data?.token) {
1301
+ await showInfo(host.ui, { title: "Token mint failed", message: `${url}: ${minted.error ?? "unknown error"} (user was created \u2014 retry mint from the admin panel)` });
1302
+ continue;
1303
+ }
1304
+ saveServerEntry(url, { token: minted.data.token, label, adminToken: rootToken });
1305
+ host.ui.toast({ variant: "success", title: "OpenShare", message: `User token for "${label}" saved \u2014 rotate and disconnect now work; root stays for the admin panel.` });
1306
+ continue;
1307
+ }
1308
+ if (choice === "enroll") {
1309
+ const probe = await probeAuthMode(url);
1310
+ if (!probe.ok) {
1311
+ await showInfo(host.ui, { title: "Unreachable", message: `${url}: ${probe.detail}` });
1312
+ continue;
1313
+ }
1314
+ let code = mine.entry.code ?? "";
1315
+ if (probe.mode === "private" && !code) {
1316
+ const typed = await showPrompt(host.ui, { title: `${url} needs an access code`, placeholder: "osc_\u2026 (from the hoster)" });
1317
+ if (typed === undefined)
1318
+ continue;
1319
+ code = typed.trim();
1320
+ if (code)
1321
+ saveServerEntry(url, { code });
1322
+ }
1323
+ const res = await enrollAtServer(url, code);
1324
+ if (res.ok) {
1325
+ saveServerEntry(url, { token: res.token, label: res.label });
1326
+ host.ui.toast({ variant: "success", title: "OpenShare", message: `Enrolled as "${res.label}".` });
1327
+ } else {
1328
+ await showInfo(host.ui, { title: "Enrollment refused", message: res.error ?? "unknown error" });
1329
+ }
1330
+ continue;
1331
+ }
1332
+ if (choice === "rotate") {
1333
+ const res = await rotateAtServer(url, mine.entry.token ?? "");
1334
+ if (res.ok && res.token) {
1335
+ saveServerEntry(url, { token: res.token });
1336
+ host.ui.toast({ variant: "success", title: "OpenShare", message: "Token rotated; the old one is revoked." });
1337
+ } else {
1338
+ await showInfo(host.ui, { title: "Rotation failed", message: `${res.error ?? "unknown"} \u2014 if the token was revoked server-side, use Enroll now instead.` });
1339
+ }
1340
+ continue;
1341
+ }
1342
+ if (choice === "disconnect") {
1343
+ const sure = await showConfirm(host.ui, { title: "Disconnect", message: `Revoke the token at ${url} and forget it?
1344
+ Your shares stay (read-only for you until TTL).`, confirmLabel: "Disconnect", danger: true });
1345
+ if (!sure)
1346
+ continue;
1347
+ const res = await disconnectAtServer(url, mine.entry.token ?? "");
1348
+ saveServerEntry(url, { token: undefined });
1349
+ host.ui.toast({ variant: res.ok ? "success" : "error", title: "OpenShare", message: res.ok ? "Disconnected." : `Server-side revoke failed (${res.error ?? "error"}) \u2014 token forgotten locally.` });
1350
+ continue;
1351
+ }
1352
+ if (choice === "test") {
1353
+ const result = await probeWorker(url);
1354
+ await showInfo(host.ui, { title: result.ok ? "Server reachable" : "Server unreachable", message: `${url} \u2014 ${result.detail}` });
1355
+ continue;
1356
+ }
1357
+ if (choice === "admin") {
1358
+ await adminConsole(host, url);
1359
+ continue;
1360
+ }
1361
+ if (choice === "remove") {
1362
+ const sure = await showConfirm(host.ui, { title: "Remove server", message: `Remove the saved membership for
1363
+ ${url}?`, confirmLabel: "Remove", danger: true });
1364
+ if (!sure)
1365
+ continue;
1366
+ deleteServerEntry(url);
1367
+ host.ui.toast({ variant: "success", title: "OpenShare", message: "Membership removed." });
1368
+ return;
1369
+ }
1370
+ }
1371
+ }
1372
+ async function adminConsole(host, pinnedUrl) {
1373
+ let url = pinnedUrl;
1374
+ if (!url) {
1375
+ const entries = serverEntries();
1376
+ const pickOptions = entries.map((s) => ({
1377
+ title: s.url,
1378
+ value: s.url,
1379
+ description: `${s.entry.label ? s.entry.label + " \xB7 " : ""}${s.entry.adminToken ? "admin token saved" : "no admin token saved"}`
1380
+ }));
1381
+ pickOptions.push({ title: "Enter a server URL\u2026", value: "__prompt" });
1382
+ const picked = await showSelect(host.ui, { title: "Admin console \u2014 server", options: pickOptions });
1383
+ if (picked === undefined)
1384
+ return;
1385
+ url = picked;
1386
+ if (picked === "__prompt") {
1387
+ const raw = await showPrompt(host.ui, { title: "Server URL", placeholder: "https://your-openshare-host" });
1388
+ if (raw === undefined || raw === "")
1389
+ return;
1390
+ url = normServerUrl(raw);
1391
+ }
1392
+ }
1393
+ let adminToken = adminTokenFor(url);
1394
+ let prompted = false;
1395
+ let client = adminClient(url, adminToken);
1396
+ while (true) {
1397
+ const probe = adminToken ? await client.users() : { ok: false, error: "no admin token" };
1398
+ if (probe.ok)
1399
+ break;
1400
+ const raw = await showPrompt(host.ui, {
1401
+ title: `Admin (root) token for ${url}`,
1402
+ placeholder: "the worker's CREATE_TOKEN",
1403
+ value: ""
1404
+ });
1405
+ if (raw === undefined)
1406
+ return;
1407
+ adminToken = raw.trim();
1408
+ prompted = true;
1409
+ client = adminClient(url, adminToken);
1410
+ const retry = await client.users();
1411
+ if (!retry.ok) {
1412
+ await showInfo(host.ui, { title: "Not an admin token", message: `${url}: ${retry.error ?? "rejected"} \u2014 the root CREATE_TOKEN is required (user tokens cannot administer).` });
1413
+ return;
1414
+ }
1415
+ break;
1416
+ }
1417
+ if (prompted && adminToken) {
1418
+ const saved = saveServerEntry(url, { adminToken });
1419
+ if (saved)
1420
+ host.ui.toast({ variant: "info", title: "OpenShare", message: `Admin token saved for ${url} (servers entry).` });
1421
+ }
1422
+ while (true) {
1423
+ const users = [];
1424
+ const tokens = [];
1425
+ const codes = [];
1426
+ const shares = [];
1427
+ await withBusy(host, `Loading ${hostOf(url)}\u2026`, async () => {
1428
+ const [usersR, tokensR, codesR, sharesR] = await Promise.all([client.users(), client.tokens(), client.codes(), client.shares()]);
1429
+ users.push(...usersR.data?.users ?? []);
1430
+ tokens.push(...tokensR.data?.tokens ?? []);
1431
+ codes.push(...codesR.data?.codes ?? []);
1432
+ shares.push(...sharesR.data?.shares ?? []);
1433
+ });
1434
+ const choice = await showSelect(host.ui, {
1435
+ title: `Admin \u2014 ${url}`,
1436
+ options: [
1437
+ { title: "Server info", value: "info", description: "health, policy, auth mode, your standing", help: `Full server state: service version, socket policy (formatted, not a JSON blob), what joining requires, and what THIS plugin instance holds (user token / root / none) \u2014 with follow-up actions.` },
1438
+ { title: `Users (${users.length})`, value: "users", description: `${users.filter((u) => u.frozen).length} frozen` },
1439
+ { title: `Tokens (${tokens.length})`, value: "tokens", description: `${tokens.filter((t) => !t.revoked).length} active` },
1440
+ { title: `Access codes (${codes.length})`, value: "codes", description: "invite codes with activation budgets" },
1441
+ { title: `Shares (${shares.length})`, value: "shares", description: "every share on this server" },
1442
+ { title: "Mint my user token", value: "adopt", description: "admin-mint YOUR user token and save it", help: `Creates your own user on ${url} and saves its token as this server's user credential \u2014 so rotate/disconnect and share ownership work for you. The root token stays untouched in adminToken.` },
1443
+ { title: "Purge ALL shares", value: "purge", description: "delete every share \u2014 maintenance", danger: true },
1444
+ { title: "Back", value: "__back" }
1445
+ ]
1446
+ });
1447
+ if (choice === undefined || choice === "__back")
1448
+ return;
1449
+ if (choice === "info") {
1450
+ await serverInfoScreen(host, url, client);
1451
+ continue;
1452
+ }
1453
+ if (choice === "users") {
1454
+ await adminUsersMenu(host, client, url, users);
1455
+ continue;
1456
+ }
1457
+ if (choice === "tokens") {
1458
+ await adminTokensMenu(host, client, tokens);
1459
+ continue;
1460
+ }
1461
+ if (choice === "codes") {
1462
+ await adminCodesMenu(host, client, codes);
1463
+ continue;
1464
+ }
1465
+ if (choice === "shares") {
1466
+ await adminSharesMenu(host, client, url, shares, users);
1467
+ continue;
1468
+ }
1469
+ if (choice === "adopt") {
1470
+ await adoptUserToken(host, url, adminToken);
1471
+ continue;
1472
+ }
1473
+ if (choice === "purge") {
1474
+ const sure = await showConfirm(host.ui, {
1475
+ title: "Purge every share",
1476
+ message: `Delete ALL ${shares.length} shares on
1477
+ ${url}?
1478
+ Viewers get a final "gone" frame. Users, tokens and codes are untouched.`,
1479
+ confirmLabel: "Purge",
1480
+ danger: true
1481
+ });
1482
+ if (!sure)
1483
+ continue;
1484
+ const res = await client.purge();
1485
+ host.ui.toast({ variant: res.ok ? "success" : "error", title: "OpenShare", message: res.ok ? `Removed ${res.data?.removed ?? "?"} shares.` : `Purge failed: ${res.error}` });
1486
+ continue;
1487
+ }
1488
+ }
1489
+ }
1490
+ async function withBusy(host, title, work) {
1491
+ const doneBusy = host.ui.showBusy?.(title);
1492
+ try {
1493
+ await (typeof work === "function" ? work() : work);
1494
+ } catch {}
1495
+ doneBusy?.();
1496
+ }
1497
+ async function serverInfoScreen(host, url, client) {
1498
+ const h = await client.health();
1499
+ if (!h.ok) {
1500
+ await showInfo(host.ui, { title: "Unreachable", message: `${url}: ${h.error ?? "unreachable"}` });
1501
+ return;
1502
+ }
1503
+ const policy = h.data?.policy ?? {};
1504
+ const policyLines = Object.entries(policy).map(([key, value]) => {
1505
+ if (typeof value === "number" && /ms$/i.test(key))
1506
+ return `${key}: ${value >= 60000 ? `${Math.round(value / 60000)}m` : `${Math.round(value / 1000)}s`} (${value}ms)`;
1507
+ return `${key}: ${String(value)}`;
1508
+ });
1509
+ const mode = h.data?.auth?.mode;
1510
+ const entry = serverEntries().find((s) => s.url === url)?.entry;
1511
+ const standing = entry?.token ? entry.token === (entry.adminToken ?? adminTokenFor(url)) ? `root token only \u2014 admin, NO user identity (see "Get a user token")` : `user token ${mask(entry.token)}${entry.adminToken ? " + root adminToken" : ""}` : entry?.code ? "access code saved \u2014 enrolls on demand" : "no credential held \u2014 public enrollment on demand";
1512
+ const message = [
1513
+ `${url}`,
1514
+ `service: ${h.data?.service ?? "?"}`,
1515
+ "",
1516
+ "Auth:",
1517
+ mode === "private" ? "private \u2014 joining needs an access code (osc_\u2026) issued by the hoster" : mode === "public" ? "public \u2014 anyone with the URL may enroll and share" : `unknown (${String(mode)})`,
1518
+ "You hold: " + standing,
1519
+ "",
1520
+ "Policy (server-owned; clients get clamped into it):",
1521
+ ...policyLines
1522
+ ].join(`
1523
+ `);
1524
+ await showInfo(host.ui, { title: "Server info", message });
1525
+ const followUp = await showSelect(host.ui, {
1526
+ title: "Server info \u2014 actions",
1527
+ options: [
1528
+ { title: "Create access code", value: "code", description: "invite: N activations, each issues a user token", help: `Mints an osc_ access code on ${url}. Each activation creates a new user + token; the budget limits how many. Shown once \u2014 copy it right away.` },
1529
+ { title: "Back", value: "__back" }
1530
+ ]
1531
+ });
1532
+ if (followUp === "code") {
1533
+ const raw = await showPrompt(host.ui, { title: "Activations budget", placeholder: "how many users this code may create", value: "1" });
1534
+ if (raw === undefined)
1535
+ return;
1536
+ const activations = Math.max(1, Math.floor(Number(raw) || 1));
1537
+ const res = await client.createCode(`info-${new Date().toISOString().slice(0, 10)}`, activations);
1538
+ if (res.ok && res.data?.code) {
1539
+ await host.clipboard.write(res.data.code).catch(() => false);
1540
+ await showInfo(host.ui, { title: "Access code created", message: `${res.data.code}
1541
+
1542
+ ${activations} activation(s). Copied to the clipboard \u2014 shown once.` });
1543
+ } else {
1544
+ await showInfo(host.ui, { title: "Creation failed", message: res.error ?? "unknown error" });
1545
+ }
1546
+ }
1547
+ }
1548
+ async function adoptUserToken(host, url, rootToken) {
1549
+ const prefill = `admin-${hostOf(url).replace(/[^a-z0-9-]/gi, "") || "user"}`;
1550
+ const label = (await showPrompt(host.ui, { title: "Your user label on this server", placeholder: "shown in the admin user list", value: prefill }))?.trim();
1551
+ if (!label)
1552
+ return;
1553
+ const client = adminClient(url, rootToken);
1554
+ const made = await client.createUser(label);
1555
+ if (!made.ok || !made.data?.userId) {
1556
+ await showInfo(host.ui, { title: "User creation failed", message: `${url}: ${made.error ?? "unknown error"}` });
1557
+ return;
1558
+ }
1559
+ const minted = await client.mintToken(made.data.userId, label);
1560
+ if (!minted.ok || !minted.data?.token) {
1561
+ await showInfo(host.ui, { title: "Token mint failed", message: `${url}: ${minted.error ?? "unknown error"} (user was created \u2014 retry mint from the admin panel)` });
1562
+ return;
1563
+ }
1564
+ saveServerEntry(url, { token: minted.data.token, label, adminToken: rootToken });
1565
+ host.ui.toast({ variant: "success", title: "OpenShare", message: `User token for "${label}" saved \u2014 root stays for the admin panel.` });
1566
+ }
1567
+ async function adminUsersMenu(host, client, url, users) {
1568
+ const options = users.map((u) => ({
1569
+ title: u.label,
1570
+ value: u.id,
1571
+ description: `${u.shares} shares \xB7 ${u.tokensActive}/${u.tokens} tokens${u.frozen ? " \xB7 FROZEN" : ""}`
1572
+ }));
1573
+ options.push({ title: "Create user", value: "__create", description: "empty user record; mint a token next" });
1574
+ options.push({ title: "Back", value: "__back" });
1575
+ if (users.length === 0)
1576
+ options.splice(0, 0, { title: "No users yet", value: "__empty", description: "create one below, or share an access code", disabled: true });
1577
+ while (true) {
1578
+ const picked = await showSelect(host.ui, { title: `Users \u2014 ${url}`, options });
1579
+ if (picked === undefined || picked === "__back")
1580
+ return;
1581
+ if (picked === "__create") {
1582
+ const label = await showPrompt(host.ui, { title: "New user label", placeholder: "name (optional)" });
1583
+ if (label === undefined)
1584
+ continue;
1585
+ const res = await client.createUser(label.trim());
1586
+ host.ui.toast({ variant: res.ok ? "success" : "error", title: "OpenShare", message: res.ok ? `User created: ${res.data?.label}` : `Failed: ${res.error}` });
1587
+ continue;
1588
+ }
1589
+ const user = users.find((u) => u.id === picked);
1590
+ if (!user)
1591
+ return;
1592
+ const action = await showSelect(host.ui, {
1593
+ title: `${user.label}${user.frozen ? " (FROZEN)" : ""}`,
1594
+ options: [
1595
+ { title: user.frozen ? "Unfreeze" : "Freeze", value: "freeze", description: user.frozen ? "restore edit rights on their shares" : "sever ALL edit paths on their shares (view unaffected)" },
1596
+ { title: "Mint token", value: "mint", description: "issue a new osk_ token (shown once)" },
1597
+ { title: "Delete user", value: "delete", description: "default freezes; delete mode also removes shares + tokens", danger: true },
1598
+ { title: "Back", value: "__back" }
1599
+ ]
1600
+ });
1601
+ if (action === undefined || action === "__back")
1602
+ continue;
1603
+ if (action === "freeze") {
1604
+ const res = user.frozen ? await client.unfreeze(user.id) : await client.freeze(user.id);
1605
+ host.ui.toast({ variant: res.ok ? "success" : "error", title: "OpenShare", message: res.ok ? user.frozen ? "Unfrozen." : `Frozen (${res.data?.shares ?? "?"} shares locked).` : `Failed: ${res.error}` });
1606
+ continue;
1607
+ }
1608
+ if (action === "mint") {
1609
+ const res = await client.mintToken(user.id);
1610
+ if (res.ok && res.data?.token) {
1611
+ await host.clipboard.write(res.data.token);
1612
+ await showInfo(host.ui, { title: "Token minted", message: `For ${user.label}:
1613
+ ${res.data.token}
1614
+
1615
+ (copied to clipboard \u2014 shown once)` });
1616
+ } else {
1617
+ await showInfo(host.ui, { title: "Mint failed", message: res.error ?? "unknown error" });
1618
+ }
1619
+ continue;
1620
+ }
1621
+ if (action === "delete") {
1622
+ const mode = await showSelect(host.ui, {
1623
+ title: `Delete ${user.label}?`,
1624
+ options: [
1625
+ { title: "Freeze instead (default)", value: "freeze", description: "reversible \u2014 shares stay, edits blocked" },
1626
+ { title: "Delete everything", value: "delete", description: "remove their shares + tokens + user", danger: true },
1627
+ { title: "Cancel", value: "__cancel" }
1628
+ ]
1629
+ });
1630
+ if (mode === undefined || mode === "__cancel")
1631
+ continue;
1632
+ const res = await client.deleteUser(user.id, mode === "delete" ? "delete" : "freeze");
1633
+ host.ui.toast({ variant: res.ok ? "success" : "error", title: "OpenShare", message: res.ok ? mode === "delete" ? `Deleted (${res.data?.shares ?? "?"} shares removed).` : "Frozen." : `Failed: ${res.error}` });
1634
+ continue;
1635
+ }
1636
+ }
1637
+ }
1638
+ async function adminTokensMenu(host, client, tokens) {
1639
+ while (true) {
1640
+ const fresh = (await client.tokens()).data?.tokens ?? [];
1641
+ const options = fresh.filter((t) => !t.revoked).map((t) => ({
1642
+ title: `${t.label || "(unlabeled)"} ${t.id}`,
1643
+ value: t.id,
1644
+ description: `last used ${t.lastUsedAt ? new Date(t.lastUsedAt).toISOString().slice(0, 16).replace("T", " ") : "never"}`
1645
+ }));
1646
+ if (options.length === 0)
1647
+ options.push({ title: "No active tokens", value: "__empty", description: "mint one from a user in Users", disabled: true });
1648
+ options.push({ title: "Back", value: "__back" });
1649
+ const picked = await showSelect(host.ui, { title: `Active tokens (${options.length - 1}) \u2014 revoke`, options });
1650
+ if (picked === undefined || picked === "__back")
1651
+ return;
1652
+ const sure = await showConfirm(host.ui, { title: "Revoke token", message: `Revoke ${picked}?
1653
+ The holder can no longer write (existing shares keep their secrets unless the user is frozen).`, confirmLabel: "Revoke", danger: true });
1654
+ if (!sure)
1655
+ continue;
1656
+ const res = await client.revokeToken(picked);
1657
+ host.ui.toast({ variant: res.ok ? "success" : "error", title: "OpenShare", message: res.ok ? "Token revoked." : `Failed: ${res.error}` });
1658
+ }
1659
+ }
1660
+ async function adminCodesMenu(host, client, codes) {
1661
+ while (true) {
1662
+ const fresh = (await client.codes()).data?.codes ?? [];
1663
+ const options = fresh.map((c) => ({
1664
+ title: `${c.label}${c.disabled ? " (disabled)" : ""}`,
1665
+ value: c.id,
1666
+ description: c.activationsLeft === -1 ? "unlimited activations" : `${c.activationsLeft} left \xB7 ${c.used} used`
1667
+ }));
1668
+ if (fresh.length === 0)
1669
+ options.push({ title: "No access codes yet", value: "__empty", description: "create one below to invite users", disabled: true });
1670
+ options.push({ title: "Create code", value: "__create", description: "invite: label + activation budget" });
1671
+ options.push({ title: "Back", value: "__back" });
1672
+ const picked = await showSelect(host.ui, { title: "Access codes", options });
1673
+ if (picked === undefined || picked === "__back")
1674
+ return;
1675
+ if (picked === "__create") {
1676
+ const label = await showPrompt(host.ui, { title: "Code label", placeholder: "e.g. friends, team" });
1677
+ if (label === undefined)
1678
+ continue;
1679
+ const raw = await showPrompt(host.ui, { title: "Activations", placeholder: "1 (personal invite) \xB7 number \xB7 -1 = unlimited" });
1680
+ if (raw === undefined)
1681
+ continue;
1682
+ const n = Number(raw.trim() === "" ? "1" : raw.trim());
1683
+ const activations = Number.isFinite(n) && n >= 1 ? Math.floor(n) : -1;
1684
+ const res2 = await client.createCode(label.trim() || "invite", activations);
1685
+ if (res2.ok && res2.data?.code) {
1686
+ await host.clipboard.write(res2.data.code);
1687
+ await showInfo(host.ui, { title: "Code created", message: `${label.trim() || "invite"}: ${res2.data.code}
1688
+ (${activations === -1 ? "unlimited" : activations} activation${activations === 1 ? "" : "s"})
1689
+
1690
+ (copied to clipboard \u2014 shown once)` });
1691
+ } else {
1692
+ await showInfo(host.ui, { title: "Create failed", message: res2.error ?? "unknown error" });
1693
+ }
1694
+ continue;
1695
+ }
1696
+ const code = fresh.find((c) => c.id === picked);
1697
+ if (!code || code.disabled)
1698
+ continue;
1699
+ const sure = await showConfirm(host.ui, { title: "Disable code", message: `Disable "${code.label}"?
1700
+ Holders can no longer enroll with it (existing tokens unaffected).`, confirmLabel: "Disable" });
1701
+ if (!sure)
1702
+ continue;
1703
+ const res = await client.disableCode(picked);
1704
+ host.ui.toast({ variant: res.ok ? "success" : "error", title: "OpenShare", message: res.ok ? "Code disabled." : `Failed: ${res.error}` });
1705
+ }
1706
+ }
1707
+ async function adminSharesMenu(host, client, url, shares, users) {
1708
+ const labelOf = (id) => users.find((u) => u.id === id)?.label ?? id ?? "(ownerless)";
1709
+ while (true) {
1710
+ const fresh = (await client.shares()).data?.shares ?? [];
1711
+ const options = fresh.map((s) => ({
1712
+ title: `${s.id.slice(0, 10)}\u2026`,
1713
+ value: s.id,
1714
+ description: `${labelOf(s.ownerId)} \xB7 ${new Date(s.createdAt).toISOString().slice(0, 16).replace("T", " ")}`
1715
+ }));
1716
+ if (fresh.length === 0)
1717
+ options.push({ title: "No shares on this server", value: "__empty", description: "nothing is being shared right now", disabled: true });
1718
+ options.push({ title: "Back", value: "__back" });
1719
+ const picked = await showSelect(host.ui, { title: `Shares \u2014 ${url}`, options });
1720
+ if (picked === undefined || picked === "__back" || picked === "__empty")
1721
+ return;
1722
+ const share = fresh.find((s) => s.id === picked);
1723
+ if (!share)
1724
+ continue;
1725
+ const action = await showSelect(host.ui, {
1726
+ title: `Share ${picked.slice(0, 12)}\u2026`,
1727
+ options: [
1728
+ { title: "Reassign owner", value: "reassign", description: `currently ${labelOf(share.ownerId)} \u2014 the rescue path` },
1729
+ { title: "Delete share", value: "delete", description: "viewers get a final gone frame", danger: true },
1730
+ { title: "Back", value: "__back" }
1731
+ ]
1732
+ });
1733
+ if (action === undefined || action === "__back")
1734
+ continue;
1735
+ if (action === "reassign") {
1736
+ const usersNow = (await client.users()).data?.users ?? [];
1737
+ const target = await showSelect(host.ui, {
1738
+ title: "Reassign to user",
1739
+ options: [...usersNow.map((u) => ({ title: u.label, value: u.id, description: u.frozen ? "FROZEN \u2014 unfreeze after" : `${u.shares} shares` })), { title: "Cancel", value: "__cancel" }]
1740
+ });
1741
+ if (target === undefined || target === "__cancel")
1742
+ continue;
1743
+ const res = await client.reassign(picked, target);
1744
+ host.ui.toast({ variant: res.ok ? "success" : "error", title: "OpenShare", message: res.ok ? "Reassigned (freeze cleared on this share)." : `Failed: ${res.error}` });
1745
+ continue;
1746
+ }
1747
+ if (action === "delete") {
1748
+ const sure = await showConfirm(host.ui, { title: "Delete share", message: `Delete ${picked} permanently?`, confirmLabel: "Delete", danger: true });
1749
+ if (!sure)
1750
+ continue;
1751
+ const res = await client.deleteShare(picked);
1752
+ host.ui.toast({ variant: res.ok ? "success" : "error", title: "OpenShare", message: res.ok ? "Share deleted." : `Failed: ${res.error}` });
1753
+ continue;
1754
+ }
1755
+ }
1756
+ }
1757
+ async function settingsDialog(host, startAtSession) {
1758
+ let cfg = currentConfig();
1759
+ let dirty = false;
1760
+ const candidates = writeCandidates(moduleDir());
1761
+ let writeTarget = candidates[0];
1762
+ const sources = configSourceMap(moduleDir());
1763
+ const GROUP_LABEL = {
1764
+ session: "This session",
1765
+ servers: "Servers",
1766
+ sharing: "Sharing",
1767
+ readauth: "Read auth",
1768
+ interface: "Interface",
1769
+ advanced: "Advanced",
1770
+ configfiles: "Config files"
1771
+ };
1772
+ const dirtyKeys = new Set;
1773
+ const keyRows = (section) => Object.entries(LABELS).filter(([, meta]) => meta.section === section).map(([key, meta]) => ({ title: meta.label, value: `key:${key}`, description: withSource(displayValue(key, cfg), sources[key]), help: helpForKey(key, cfg, sources), edited: dirtyKeys.has(key) }));
1774
+ const serverRows = () => {
1775
+ const servers = serverEntries(cfg);
1776
+ const rows = keyRows("Connection");
1777
+ rows.push({ title: "\u2014 saved servers \u2014", value: "__divider_servers", divider: true });
1778
+ for (const s of servers) {
1779
+ const cred = s.entry.token ? "token saved" : s.entry.code ? "code saved" : "enrolls on demand";
1780
+ rows.push({
1781
+ title: s.url,
1782
+ value: `server:${s.url}`,
1783
+ description: `${s.entry.label ? s.entry.label + " \xB7 " : ""}${cred}${s.active ? " \xB7 ACTIVE" : ""}`,
1784
+ help: [
1785
+ `## Server`,
1786
+ `Server: ${s.url}`,
1787
+ `Label: ${s.entry.label ?? "(none)"}`,
1788
+ `Credential: ${s.entry.token ? `user token ${mask(s.entry.token)}` : s.entry.code ? "access code saved \u2014 enrolls on demand" : "none \u2014 public server, enrolls on demand"}`,
1789
+ `Receives new sessions: ${s.active ? "YES (active)" : "no"}`,
1790
+ "",
1791
+ "## Notes",
1792
+ "Every ACTIVE server receives new sessions (fan-out); one active server is the normal setup. Tokens persist in the settings home; enroll once, then it just works. The per-server submenu (enter) adds the admin panel for this server."
1793
+ ].join(`
1794
+ `)
1795
+ });
1796
+ }
1797
+ rows.push({
1798
+ title: "Connect to a server",
1799
+ value: "server:add",
1800
+ description: "URL (+ access code) \u2014 enrolls and saves the token",
1801
+ help: "Onboarding flow: paste the server URL; public servers enroll immediately, private ones ask for an access code (osc_\u2026 from the hoster). The server issues a user token that persists in the settings home \u2014 the root CREATE_TOKEN is never needed for sharing."
1802
+ });
1803
+ rows.push({
1804
+ title: "Test connection",
1805
+ value: "action:test",
1806
+ description: "Probe the active server's /health",
1807
+ help: "Fetches /health on the active server and reports the service, auth mode, and socket policy. A quick way to verify URL and reachability before sharing."
1808
+ });
1809
+ return rows;
1810
+ };
1811
+ const interfaceRows = () => {
1812
+ const uiWidth = kvWidthOf(host.kv);
1813
+ const uiHeight = kvHeightPercentOf(host.kv);
1814
+ return [
1815
+ {
1816
+ title: "Menu size\u2026",
1817
+ value: "ui:size",
1818
+ description: `${uiWidth} (${DIALOG_WIDTH_COLUMNS[uiWidth]} cols) \xB7 ${uiHeight}% height`,
1819
+ help: `## Width
1820
+ medium = 60, large = 88, xlarge = 116 columns \u2014 opencode exposes fixed widths only. In the picker, w cycles the width and the dialog resizes live.
1821
+
1822
+ ## Height
1823
+ A percent of terminal rows (25-100), capped at 75% of the terminal minus chrome (the host's backdrop offset). Presets: compact 35, normal 50, tall 70, max 100. The picker's live preview shows the true capped height.
1824
+
1825
+ Current: ${uiWidth} (${DIALOG_WIDTH_COLUMNS[uiWidth]} columns) at ${uiHeight}%
1826
+ Stored per host (opencode's own preference store), not in the config file.`
1827
+ }
1828
+ ];
1829
+ };
1830
+ const configRows = () => [
1831
+ {
1832
+ title: "Write target",
1833
+ value: "action:target",
1834
+ description: writeTarget.label,
1835
+ disabled: candidates.length <= 1,
1836
+ help: `Which file "Save & exit" writes. The global settings home is always available; other files in the read chain only when they already exist (the TUI never creates deploy-time override files).
1837
+
1838
+ Writing: ${writeTarget.path}`
1839
+ },
1840
+ {
1841
+ title: "Where values come from",
1842
+ value: "action:sources",
1843
+ description: summarizeSources(sources),
1844
+ help: `Config merges per key, highest priority wins: OPENSHARE_* env > plugin options > adjacent openshare.config.json > openshare.local.json (repo walk-up) > settings home > defaults. An empty string means unset for url/token keys.
1845
+
1846
+ ${sourcesMessage(sources)}`
1847
+ }
1848
+ ];
1849
+ const sessionRows = async () => {
1850
+ if (!startAtSession)
1851
+ return [];
1852
+ const binding = await readBinding(host.client, await resolveRoot(host.client, startAtSession));
1853
+ const rows = [
1854
+ {
1855
+ title: binding?.url ? "Copy share link" : "Share this session",
1856
+ value: "session:share",
1857
+ description: binding?.url ? "copy the share URL to the clipboard" : "Set the share flag; the server plugin creates the page",
1858
+ help: binding?.url ? `Copies the share URL to the clipboard.
1859
+
1860
+ URL: ${binding.url}
1861
+ Share id: ${binding.id ?? "?"}` : "Publishes this session (and its subagent sessions) to the active server. The full history lands in the page \u2014 sharing mid-session is first-class, nothing is missed."
1862
+ }
1863
+ ];
1864
+ if (binding?.url) {
1865
+ rows.push({
1866
+ title: "Stop sharing",
1867
+ value: "session:unshare",
1868
+ description: "delete the page(s), or leave a snapshot",
1869
+ danger: true,
1870
+ help: `Choose per run: DELETE every page server-side (links stop working \u2014 final "gone" state), or LEAVE AS-IS (stop updating; pages stay readable until their TTL \u2014 a frozen snapshot).
1871
+
1872
+ URLs:
1873
+ ${[binding.url].join(`
1874
+ `)}
1875
+
1876
+ Sharing again later always creates fresh page(s) with new URLs.`
1877
+ });
1878
+ }
1879
+ rows.push({
1880
+ title: "Session TTL override (days)",
1881
+ value: "session:ttl",
1882
+ description: binding?.opts?.ttlDays !== undefined ? String(binding.opts.ttlDays) : "(worker default)",
1883
+ help: `Per-session share lifetime. Overrides the config ttlDays for THIS session's pages; still capped by the server's MAX_TTL_DAYS and the token's maxTtlDays. Applied at the next create/attach.
1884
+
1885
+ Current: ${binding?.opts?.ttlDays !== undefined ? `${binding.opts.ttlDays} days` : "(none \u2014 config default)"}`
1886
+ });
1887
+ rows.push({
1888
+ title: "Session read auth",
1889
+ value: "session:auth",
1890
+ description: binding?.opts?.authLogin ? `${binding.opts.authLogin} / ****` : "(config default)",
1891
+ help: `Login gate for THIS session's page only \u2014 viewers must log in (7-day cookie, lockout). Overrides the config read auth. Applied at the next create/attach; removing the login removes the gate.
1892
+
1893
+ Current: ${binding?.opts?.authLogin ? `${binding.opts.authLogin} / (password set)` : "(none \u2014 config default applies)"}`
1894
+ });
1895
+ return rows;
1896
+ };
1897
+ const rowsFor = async (id) => {
1898
+ switch (id) {
1899
+ case "session":
1900
+ return await sessionRows();
1901
+ case "servers":
1902
+ return serverRows();
1903
+ case "sharing":
1904
+ return keyRows("Sharing");
1905
+ case "readauth":
1906
+ return keyRows("Read auth");
1907
+ case "interface":
1908
+ return interfaceRows();
1909
+ case "advanced":
1910
+ return keyRows("Advanced");
1911
+ case "configfiles":
1912
+ return configRows();
1913
+ }
1914
+ };
1915
+ const dispatch = async (choice) => {
1916
+ if (choice === "action:target") {
1917
+ const picked = await showSelect(host.ui, {
1918
+ title: "Write target",
1919
+ placeholder: "Files that already exist; the global home is always available",
1920
+ options: candidates.map((candidate) => ({ title: candidate.label, value: candidate.path, description: candidate.path })),
1921
+ current: writeTarget.path
1922
+ });
1923
+ if (picked)
1924
+ writeTarget = candidates.find((candidate) => candidate.path === picked) ?? writeTarget;
1925
+ return;
1926
+ }
1927
+ if (choice === "action:sources") {
1928
+ await showInfo(host.ui, { title: "Where values come from", message: sourcesMessage(sources) });
1929
+ return;
1930
+ }
1931
+ if (choice === "ui:size") {
1932
+ if (host.ui.showSizePicker) {
1933
+ const result = await host.ui.showSizePicker({ width: kvWidthOf(host.kv), heightPercent: kvHeightPercentOf(host.kv) });
1934
+ if (result) {
1935
+ host.kv.set(UI_WIDTH_KV, result.width);
1936
+ host.kv.set(UI_HEIGHT_PERCENT_KV, clampHeightPercent(result.heightPercent));
1937
+ host.ui.toast({ variant: "info", title: "OpenShare", message: `Menu size: ${result.width} \xB7 ${result.heightPercent}% of terminal.` });
1938
+ }
1939
+ } else {
1940
+ const rawHeight = await showPrompt(host.ui, { title: "Menu height percent", placeholder: "25-100", value: String(kvHeightPercentOf(host.kv)) });
1941
+ if (rawHeight !== undefined) {
1942
+ const parsed = Number(rawHeight);
1943
+ if (!Number.isFinite(parsed)) {
1944
+ host.ui.toast({ variant: "error", title: "OpenShare", message: "Height must be a number from 25 to 100." });
1945
+ return;
1946
+ }
1947
+ host.kv.set(UI_HEIGHT_PERCENT_KV, clampHeightPercent(parsed));
1948
+ host.kv.set(UI_WIDTH_KV, nextDialogWidth(kvWidthOf(host.kv)));
1949
+ host.ui.toast({ variant: "info", title: "OpenShare", message: `Menu size: ${kvWidthOf(host.kv)} \xB7 ${kvHeightPercentOf(host.kv)}%` });
1950
+ }
1951
+ }
1952
+ return;
1953
+ }
1954
+ if (choice === "action:test") {
1955
+ const serverUrl = serverEntries(cfg).find((s) => s.active)?.url ?? cfg.workerUrl;
1956
+ host.ui.toast({ variant: "info", title: "OpenShare", message: "Testing server connection..." });
1957
+ const result = await probeWorker(serverUrl);
1958
+ await showInfo(host.ui, {
1959
+ title: result.ok ? "Server reachable" : "Server unreachable",
1960
+ message: `${serverUrl || "(no server set)"} \u2014 ${result.detail}${result.ok && !serverEntries(cfg).some((s) => s.entry.token) ? `
1961
+ Note: no token held \u2014 the plugin enrolls on first contact (public) or needs a code (private).` : ""}`
1962
+ });
1963
+ return;
1964
+ }
1965
+ if (choice === "server:add") {
1966
+ const res = await connectServerFlow(host);
1967
+ host.ui.toast({ variant: res.ok ? "success" : "error", title: "OpenShare", message: res.message, duration: 1e4 });
1968
+ if (res.ok)
1969
+ cfg = currentConfig();
1970
+ return;
1971
+ }
1972
+ if (choice.startsWith("server:")) {
1973
+ await serverMenu(host, choice.slice(7));
1974
+ cfg = currentConfig();
1975
+ return;
1976
+ }
1977
+ if (choice.startsWith("key:")) {
1978
+ const key = choice.slice(4);
1979
+ const next = await editKey(host, cfg, key);
1980
+ if (next) {
1981
+ cfg = next;
1982
+ dirty = true;
1983
+ dirtyKeys.add(key);
1984
+ }
1985
+ return;
1986
+ }
1987
+ if (choice.startsWith("session:") && startAtSession) {
1988
+ const root = await resolveRoot(host.client, startAtSession);
1989
+ const current = await readBinding(host.client, root);
1990
+ if (choice === "session:share") {
1991
+ if (!isConfigured(cfg)) {
1992
+ await showInfo(host.ui, { title: "Not configured", message: "Connect a server (Servers group) first, then share." });
1993
+ return;
1994
+ }
1995
+ const result = await confirmShare(host, root);
1996
+ host.ui.toast({ variant: result.ok ? "success" : "error", title: "OpenShare", message: result.message, duration: result.ok ? 8000 : 15000 });
1997
+ return;
1998
+ }
1999
+ if (choice === "session:unshare") {
2000
+ const how = await showSelect(host.ui, {
2001
+ title: "Stop sharing",
2002
+ options: [
2003
+ { title: "Delete every page", value: "delete", description: "all links stop working", danger: true, help: "Deletes every bound server's page and clears the share flag. Viewers see a final gone state." },
2004
+ { title: "Leave as-is (snapshot)", value: "leave", description: "stop updating; pages stay until their TTL", help: "Stops mirroring without deleting anything \u2014 pages stay readable as frozen snapshots until their TTL expires." },
2005
+ { title: "Cancel", value: "__cancel" }
2006
+ ]
2007
+ });
2008
+ if (how === undefined || how === "__cancel")
2009
+ return;
2010
+ if (how === "leave") {
2011
+ const all = await readBindings(host.client, root);
2012
+ const okPatch = await patchSessionBinding(host.client, root, (servers) => {
2013
+ for (const b of all) {
2014
+ if (!b.server)
2015
+ continue;
2016
+ delete servers[b.server];
2017
+ servers[b.server] = { optedOut: true };
2018
+ }
2019
+ });
2020
+ host.ui.toast({ variant: okPatch ? "success" : "error", title: "OpenShare", message: okPatch ? "Left as-is \u2014 snapshots until their TTLs." : "Could not update the binding." });
2021
+ return;
2022
+ }
2023
+ const result = await runUnshareFlow(host, startAtSession);
2024
+ host.ui.toast({ variant: result.ok ? "success" : "error", title: "OpenShare", message: result.message });
2025
+ return;
2026
+ }
2027
+ if (choice === "session:ttl") {
2028
+ const raw = await showPrompt(host.ui, { title: "Session TTL (days)", placeholder: "days", value: current?.opts?.ttlDays !== undefined ? String(current.opts.ttlDays) : "" });
2029
+ if (raw === undefined)
2030
+ return;
2031
+ const opts = { ...current?.opts ?? {} };
2032
+ if (raw === "")
2033
+ delete opts.ttlDays;
2034
+ else {
2035
+ const days = Number(raw);
2036
+ if (!Number.isFinite(days) || days <= 0) {
2037
+ host.ui.toast({ variant: "error", title: "OpenShare", message: "TTL must be a positive number of days." });
2038
+ return;
2039
+ }
2040
+ opts.ttlDays = Math.floor(days);
2041
+ }
2042
+ await setShareFlag(host.client, root, opts);
2043
+ host.ui.toast({ variant: "success", title: "OpenShare", message: opts.ttlDays !== undefined ? `Session TTL: ${opts.ttlDays}d (applies at the next attach/create)` : "Session TTL override cleared" });
2044
+ return;
2045
+ }
2046
+ if (choice === "session:auth") {
2047
+ const login = await showPrompt(host.ui, { title: "Session read auth: login", placeholder: "empty = use config default", value: current?.opts?.authLogin ?? "" });
2048
+ if (login === undefined)
2049
+ return;
2050
+ const password = await showPrompt(host.ui, { title: "Session read auth: password", placeholder: "empty = no password" });
2051
+ if (password === undefined)
2052
+ return;
2053
+ const opts = { ...current?.opts ?? {} };
2054
+ if (login === "") {
2055
+ delete opts.authLogin;
2056
+ delete opts.authPassword;
2057
+ } else {
2058
+ opts.authLogin = login;
2059
+ if (password !== "")
2060
+ opts.authPassword = password;
2061
+ else
2062
+ delete opts.authPassword;
2063
+ }
2064
+ await setShareFlag(host.client, root, opts);
2065
+ host.ui.toast({ variant: "success", title: "OpenShare", message: "Session read auth saved (applies at the next attach/create)." });
2066
+ return;
2067
+ }
2068
+ }
2069
+ };
2070
+ const groupMenu = async (id) => {
2071
+ while (true) {
2072
+ const rows = await rowsFor(id);
2073
+ if (!rows.length)
2074
+ return;
2075
+ const options = [...rows, { title: "Back", value: "__back" }];
2076
+ const choice = await showSelect(host.ui, { title: `OpenShare \u2014 ${GROUP_LABEL[id]}`, options });
2077
+ if (choice === undefined || choice === "__back")
2078
+ return;
2079
+ await dispatch(choice);
2080
+ }
2081
+ };
2082
+ const seconds = (ms, fallback) => `${Math.round((ms ?? fallback) / 100) / 10}s`;
2083
+ while (true) {
2084
+ const servers = serverEntries(cfg);
2085
+ const active = servers.find((s) => s.active);
2086
+ const uiWidth = kvWidthOf(host.kv);
2087
+ const groups = [];
2088
+ if (startAtSession) {
2089
+ const binding = await readBinding(host.client, await resolveRoot(host.client, startAtSession));
2090
+ const shared = binding?.url ? binding.enabled === false ? "attached (dormant)" : "live" : "not shared";
2091
+ groups.push({
2092
+ id: "session",
2093
+ summary: shared,
2094
+ help: `Per-session state and overrides for the session this dialog was opened on.
2095
+
2096
+ Status: ${shared}${binding?.url ? `
2097
+ URL: ${binding.url}` : ""}
2098
+
2099
+ Inside: share/stop sharing (delete or leave a snapshot), session TTL override, session read auth.`
2100
+ });
2101
+ }
2102
+ groups.push({
2103
+ id: "servers",
2104
+ summary: servers.length ? `${servers.length} saved${active ? ` \xB7 active: ${hostOf(active.url)}` : " \xB7 none active"}${cfg.workerUrl ? " \xB7 legacy pair set" : ""}` : cfg.workerUrl ? `${hostOf(cfg.workerUrl)} \xB7 legacy pair` : "no server yet",
2105
+ help: `Server connection, memberships, and admin \u2014 one place (owner: "we already do connection in servers, why was it separate").
2106
+
2107
+ Inside: the legacy workerUrl + createToken pair, saved servers (enroll, rotate, disconnect, admin panel, test, remove), Connect to a server, Test connection.
2108
+
2109
+ Every ACTIVE server receives new sessions (fan-out).`
2110
+ });
2111
+ groups.push({
2112
+ id: "sharing",
2113
+ summary: `sync ${seconds(cfg.debounceMs, 1e4)} \xB7 completed ${seconds(cfg.completedDelayMs, 5000)} \xB7 TTL ${cfg.ttlDays ?? "server cap"}`,
2114
+ help: `What and how sessions stream: share mode (all/manual), sync cadence, the completed-delay, the model label on pages, share TTL, and the viewer's task-tab behavior.
2115
+
2116
+ Cadence values are clamped by the server's policy window.`
2117
+ });
2118
+ groups.push({
2119
+ id: "readauth",
2120
+ summary: cfg.authLogin ? `${cfg.authLogin} / ****` : "none set",
2121
+ help: `Optional login gate for NEW share pages: viewers must enter login+password (PBKDF2 server-side, 7-day cookie, lockout after failures). Per-session overrides live in This session.
2122
+
2123
+ Write access is a separate lock \u2014 a login never lets anyone edit.`
2124
+ });
2125
+ groups.push({
2126
+ id: "interface",
2127
+ summary: `${uiWidth} (${DIALOG_WIDTH_COLUMNS[uiWidth]} cols) \xB7 ${kvHeightPercentOf(host.kv)}% height`,
2128
+ help: `OpenShare menu sizing: width (opencode's fixed dialog widths: 60/88/116 columns) and height (a percent of terminal rows, presets compact/normal/tall/max).
2129
+
2130
+ Stored in the host's preference store \u2014 per machine, not the config file.`
2131
+ });
2132
+ groups.push({
2133
+ id: "advanced",
2134
+ summary: "spool dir \xB7 url file \xB7 stderr",
2135
+ help: `Rarely-touched plumbing: the un-acked frame spool directory, the all-mode URL file (append log for CI), and the stderr announce lines.`
2136
+ });
2137
+ groups.push({
2138
+ id: "configfiles",
2139
+ summary: `write: ${writeTarget.label}`,
2140
+ help: `Which file Save & exit writes (global settings home by default; other existing files selectable) and the per-key source map of the current read chain (env > options > adjacent > local > home > defaults).`
2141
+ });
2142
+ const options = groups.map((group) => ({
2143
+ title: GROUP_LABEL[group.id],
2144
+ value: `group:${group.id}`,
2145
+ description: group.summary,
2146
+ help: group.help
2147
+ }));
2148
+ options.push({ title: dirty ? "Save & exit" : "Exit", value: "action:exit", description: dirty ? `Write ${writeTarget.path}` : "No changes" });
2149
+ const choice = await showSelect(host.ui, { title: "OpenShare settings", options });
2150
+ if (choice === undefined || choice === "action:exit") {
2151
+ if (dirty) {
2152
+ const saved = saveConfig(cfg, writeTarget.path);
2153
+ if (saved)
2154
+ host.ui.toast({ variant: "success", title: "OpenShare", message: `Saved ${saved.path}` });
2155
+ else
2156
+ host.ui.toast({ variant: "error", title: "OpenShare", message: `Could not write ${writeTarget.path}.` });
2157
+ }
2158
+ host.ui.dialog.clear();
2159
+ return;
2160
+ }
2161
+ if (choice.startsWith("group:")) {
2162
+ const id = choice.slice(6);
2163
+ if (GROUP_LABEL[id])
2164
+ await groupMenu(id);
2165
+ }
2166
+ }
2167
+ }
2168
+ async function onboardingDialog(host) {
2169
+ const ok = await showConfirm(host.ui, {
2170
+ title: "Welcome to Open(code)Share",
2171
+ message: `Sharing needs a share server.
2172
+ Connect now (URL + access code if the hoster gave you one), or open the full settings?`,
2173
+ confirmLabel: "Connect to a server"
2174
+ });
2175
+ if (ok) {
2176
+ const res = await connectServerFlow(host);
2177
+ host.ui.dialog.clear();
2178
+ host.ui.toast({ variant: res.ok ? "success" : "error", title: "OpenShare", message: res.message, duration: 1e4 });
2179
+ if (!res.ok)
2180
+ await settingsDialog(host);
2181
+ } else {
2182
+ await settingsDialog(host);
2183
+ }
2184
+ }
2185
+
2186
+ // src/commands.ts
2187
+ init_sidebar();
2188
+ var PALETTE_CATEGORY = "OpenShare";
2189
+ function finish(host, result) {
2190
+ try {
2191
+ host.ui.dialog.clear();
2192
+ } catch {}
2193
+ host.ui.toast({
2194
+ variant: result.ok ? "success" : result.unconfigured ? "warning" : "error",
2195
+ title: "OpenShare",
2196
+ message: result.message,
2197
+ duration: result.ok ? 8000 : 15000
2198
+ });
2199
+ }
2200
+ async function showShareManager(host, sessionID) {
2201
+ const sid = sessionID();
2202
+ if (!sid)
2203
+ return;
2204
+ const root = await resolveRoot(host.client, sid);
2205
+ let all = await readBindings(host.client, root);
2206
+ const collided = all.find((b) => b.collided);
2207
+ if (collided)
2208
+ return showCollisionDialog(host, root, collided.server, collided.id, collided.collided ?? "");
2209
+ const live = all.filter((b) => b.url && b.enabled !== false && b.id);
2210
+ if (live.length) {
2211
+ const verdicts = await Promise.all(live.map((b) => verifyShare(b.server, b.id)));
2212
+ const gone = live.filter((b, i) => verdicts[i] === "gone");
2213
+ if (gone.length) {
2214
+ await clearGoneBindingEntries(host, root, gone.map((b) => b.server));
2215
+ all = all.filter((b) => !gone.some((g) => g.server === b.server));
2216
+ const list = gone.map((b) => `\u2022 ${hostOf(b.url || b.server)} \u2014 ${b.url}`).join(`
2217
+ `);
2218
+ const choice2 = await host.ui.showMenu({
2219
+ title: "Share is gone",
2220
+ hint: "The page was removed, expired, or purged on the server \u2014 sharing disabled for it.",
2221
+ options: [
2222
+ { title: "Share again", value: "again", description: "create a fresh page with the full history", help: `The stale binding was already cleared. Sharing again creates a brand-new page on the ACTIVE server with this session's full history backfilled.
2223
+
2224
+ Gone:
2225
+ ${list}` },
2226
+ { title: "Close", value: "close", description: "leave the session unshared", help: `The stale binding entries were cleared automatically; the session is not shared anymore. Run /share any time to share it fresh.
2227
+
2228
+ Gone:
2229
+ ${list}` }
2230
+ ]
2231
+ });
2232
+ if (choice2 === "again") {
2233
+ finish(host, await runShareFlow(host, sid));
2234
+ return;
2235
+ }
2236
+ if (all.length === 0)
2237
+ return;
2238
+ }
2239
+ }
2240
+ if (all.length === 0)
2241
+ return;
2242
+ const otherServers = serverEntries().filter((s) => !all.some((b) => b.server === s.url));
2243
+ if (all.length === 1 && otherServers.length === 0) {
2244
+ const b = all[0];
2245
+ host.ui.showMenu({
2246
+ title: "Session is shared",
2247
+ hint: b.url,
2248
+ options: [
2249
+ {
2250
+ title: "Copy share link",
2251
+ value: "copy",
2252
+ description: "copy the share URL to the clipboard",
2253
+ help: `Copies the share URL to the clipboard (navigator + OSC52 fallback).
2254
+
2255
+ URL: ${b.url}
2256
+ Server: ${b.server}
2257
+ Share id: ${b.id}
2258
+
2259
+ Anyone with the link can read the page (plus the login, if read auth is set).`
2260
+ },
2261
+ {
2262
+ title: "Stop sharing",
2263
+ value: "stop",
2264
+ description: "Delete the share page \u2014 the link stops working for everyone",
2265
+ help: `Deletes the page on ${hostOf(b.url || b.server)} server-side and clears this session's share flag. Viewers with the link see a final "gone" state. This cannot be undone \u2014 sharing again creates a NEW page with a new URL.`
2266
+ },
2267
+ { title: "Cancel", value: "cancel" }
2268
+ ]
2269
+ }).then(async (choice2) => {
2270
+ if (choice2 === "copy") {
2271
+ const ok = await host.clipboard.write(b.url);
2272
+ finish(host, ok ? { ok: true, message: `Share URL copied to clipboard: ${b.url}`, url: b.url } : { ok: false, message: `Could not write the clipboard \u2014 link: ${b.url}`, url: b.url });
2273
+ return;
2274
+ }
2275
+ if (choice2 === "stop") {
2276
+ const how = await host.ui.showMenu({
2277
+ title: "Stop sharing",
2278
+ hint: b.url,
2279
+ options: [
2280
+ {
2281
+ title: "Delete the page",
2282
+ value: "delete",
2283
+ description: "the link stops working for everyone",
2284
+ danger: true,
2285
+ help: `Deletes the page on ${hostOf(b.url || b.server)} and clears this session's share flag. Viewers with the link see a final "gone" state. Sharing again later creates a fresh page with a new URL.`
2286
+ },
2287
+ {
2288
+ title: "Leave as-is (snapshot)",
2289
+ value: "leave",
2290
+ description: "stop updating; the page stays until its TTL",
2291
+ help: `Stops mirroring WITHOUT deleting anything \u2014 viewers keep reading the snapshot until the share's TTL expires. Useful for handing someone a frozen copy while you keep working.`
2292
+ },
2293
+ { title: "Cancel", value: "cancel" }
2294
+ ]
2295
+ });
2296
+ if (!how || how === "cancel")
2297
+ return;
2298
+ if (how === "leave") {
2299
+ await patchSessionBinding(host.client, root, (servers) => {
2300
+ delete servers[b.server];
2301
+ servers[b.server] = { optedOut: true };
2302
+ });
2303
+ finish(host, { ok: true, message: `${hostOf(b.url || b.server)} left as-is \u2014 a snapshot until its TTL.` });
2304
+ return;
2305
+ }
2306
+ const yes = await host.ui.showConfirm({
2307
+ title: "Delete the share page",
2308
+ message: `Delete the page on ${hostOf(b.url || b.server)}?
2309
+ The link stops working for everyone.`,
2310
+ confirmLabel: "Delete",
2311
+ danger: true
2312
+ });
2313
+ if (!yes) {
2314
+ host.ui.dialog.clear();
2315
+ return;
2316
+ }
2317
+ finish(host, await runUnshareFlow(host, sessionID()));
2318
+ }
2319
+ });
2320
+ return;
2321
+ }
2322
+ const options = all.map((b) => ({
2323
+ title: `Copy link \u2014 ${hostOf(b.url || b.server)}`,
2324
+ value: `copy:${b.server}`,
2325
+ description: b.url,
2326
+ help: `Copies this server's share URL to the clipboard.
2327
+
2328
+ URL: ${b.url}
2329
+ Server: ${b.server}
2330
+ Share id: ${b.id}
2331
+
2332
+ Each server carries an independent page of the same session.`
2333
+ }));
2334
+ options.push({
2335
+ title: "Manage per server\u2026",
2336
+ value: "manage",
2337
+ description: `${all.length} servers \u2014 stop one, leave a snapshot`,
2338
+ help: `Per-server actions: copy the link, stop sharing there (page deleted), or leave it as a read-only snapshot (updates stop, the page lives out its TTL). Other servers are unaffected.
2339
+
2340
+ Bound servers:
2341
+ ${all.map((b) => `\u2022 ${b.server} \u2014 ${b.url}`).join(`
2342
+ `)}`
2343
+ });
2344
+ if (otherServers.length) {
2345
+ options.push({
2346
+ title: "Share to another server\u2026",
2347
+ value: "more",
2348
+ description: otherServers.map((s) => hostOf(s.url)).join(", "),
2349
+ help: `Adds one of your other configured servers as a destination for THIS session: it becomes the active server and the full history is mirrored there within seconds.
2350
+
2351
+ Available:
2352
+ ${otherServers.map((s) => `\u2022 ${s.url}`).join(`
2353
+ `)}`
2354
+ });
2355
+ }
2356
+ options.push({
2357
+ title: "Stop sharing (all)",
2358
+ value: "stop-all",
2359
+ description: "delete every server's page, or leave snapshots",
2360
+ danger: true,
2361
+ help: `Deletes the page on all ${all.length} servers (or leaves them as read-only snapshots \u2014 your choice on the next screen) and clears the share flag. Sharing again creates fresh pages.`
2362
+ });
2363
+ options.push({ title: "Cancel", value: "cancel" });
2364
+ const choice = await host.ui.showMenu({ title: `Session is shared \xB7 ${all.length} servers`, options });
2365
+ if (!choice || choice === "cancel")
2366
+ return;
2367
+ if (choice.startsWith("copy:")) {
2368
+ const b = all.find((x) => `copy:${x.server}` === choice);
2369
+ if (b) {
2370
+ const ok = await host.clipboard.write(b.url);
2371
+ finish(host, ok ? { ok: true, message: `Share URL copied: ${b.url}`, url: b.url } : { ok: false, message: `Clipboard failed \u2014 ${b.url}`, url: b.url });
2372
+ }
2373
+ return;
2374
+ }
2375
+ if (choice === "stop-all") {
2376
+ const how = await host.ui.showMenu({
2377
+ title: `Stop sharing \xB7 ${all.length} servers`,
2378
+ options: [
2379
+ { title: "Delete every page", value: "delete", description: "all links stop working", danger: true, help: `Deletes the page on every bound server (${all.map((b) => hostOf(b.url || b.server)).join(", ")}) and clears the share flag.` },
2380
+ { title: "Leave all as-is (snapshots)", value: "leave", description: "stop updating; pages stay until their TTLs", help: `Stops mirroring everywhere without deleting anything \u2014 every page stays readable as a frozen snapshot until its TTL expires.` },
2381
+ { title: "Cancel", value: "cancel" }
2382
+ ]
2383
+ });
2384
+ if (!how || how === "cancel")
2385
+ return;
2386
+ if (how === "leave") {
2387
+ await patchSessionBinding(host.client, root, (servers) => {
2388
+ for (const b of all) {
2389
+ delete servers[b.server];
2390
+ servers[b.server] = { optedOut: true };
2391
+ }
2392
+ });
2393
+ finish(host, { ok: true, message: `${all.length} server(s) left as-is \u2014 snapshots until their TTLs.` });
2394
+ return;
2395
+ }
2396
+ const yes = await host.ui.showConfirm({
2397
+ title: "Stop sharing",
2398
+ message: `Delete the share pages on all ${all.length} servers? The links stop working for everyone.`,
2399
+ confirmLabel: "Stop all",
2400
+ danger: true
2401
+ });
2402
+ if (!yes)
2403
+ return;
2404
+ finish(host, await runUnshareFlow(host, sessionID()));
2405
+ return;
2406
+ }
2407
+ if (choice === "more") {
2408
+ const pick = await host.ui.showMenu({
2409
+ title: "Share to another server",
2410
+ options: [...otherServers.map((s) => ({ title: hostOf(s.url), value: s.url, description: s.entry.label })), { title: "Cancel", value: "__cancel" }]
2411
+ });
2412
+ if (!pick || pick === "__cancel")
2413
+ return;
2414
+ const { saveServerEntry: saveServerEntry2 } = await Promise.resolve().then(() => (init_share_flow(), exports_share_flow));
2415
+ for (const s of serverEntries())
2416
+ if (s.url !== pick)
2417
+ saveServerEntry2(s.url, { active: undefined });
2418
+ saveServerEntry2(pick, { active: true });
2419
+ finish(host, { ok: true, message: `${hostOf(pick)} activated \u2014 this session mirrors there within seconds (new sessions share to it too).` });
2420
+ return;
2421
+ }
2422
+ if (choice === "manage") {
2423
+ const pick = await host.ui.showMenu({
2424
+ title: "Manage server",
2425
+ options: [...all.map((b2) => ({ title: hostOf(b2.url || b2.server), value: b2.server, description: b2.url })), { title: "Cancel", value: "__cancel" }]
2426
+ });
2427
+ if (!pick || pick === "__cancel")
2428
+ return;
2429
+ const b = all.find((x) => x.server === pick);
2430
+ if (!b)
2431
+ return;
2432
+ const action = await host.ui.showMenu({
2433
+ title: `${hostOf(b.url || b.server)}`,
2434
+ hint: b.url,
2435
+ options: [
2436
+ {
2437
+ title: "Copy link",
2438
+ value: "copy",
2439
+ description: "copy the share URL",
2440
+ help: `Copies this server's share URL to the clipboard.
2441
+
2442
+ URL: ${b.url}
2443
+ Share id: ${b.id}`
2444
+ },
2445
+ {
2446
+ title: "Stop sharing here",
2447
+ value: "stop",
2448
+ description: "delete THIS server's page; other servers unaffected",
2449
+ help: `Deletes the page on ${hostOf(b.url || b.server)} and retires this server for the session (the engine stops mirroring here). Other servers keep streaming. Sharing here again later creates a new page.`
2450
+ },
2451
+ {
2452
+ title: "Leave as-is (snapshot)",
2453
+ value: "leave",
2454
+ description: "stop updating it; the page stays until its TTL",
2455
+ help: `Stops mirroring to ${hostOf(b.url || b.server)} WITHOUT deleting anything \u2014 viewers keep reading the snapshot until the share's TTL expires. Useful for handing someone a frozen copy while you keep working.`
2456
+ },
2457
+ { title: "Cancel", value: "cancel" }
2458
+ ]
2459
+ });
2460
+ if (!action || action === "cancel")
2461
+ return;
2462
+ if (action === "copy") {
2463
+ const ok = await host.clipboard.write(b.url);
2464
+ finish(host, ok ? { ok: true, message: `Share URL copied: ${b.url}`, url: b.url } : { ok: false, message: `Clipboard failed \u2014 ${b.url}`, url: b.url });
2465
+ return;
2466
+ }
2467
+ if (action === "stop") {
2468
+ const deleted = await workerDeleteShare(b.server || b.url.slice(0, b.url.indexOf("/s/")), b.id, b.secret);
2469
+ await patchSessionBinding(host.client, root, (servers) => {
2470
+ delete servers[b.server];
2471
+ servers[b.server] = { optedOut: true };
2472
+ });
2473
+ finish(host, { ok: true, message: deleted ? `Stopped sharing on ${hostOf(b.url || b.server)} \u2014 page deleted.` : `Stopped sharing on ${hostOf(b.url || b.server)} (page deletion follows via the sweeper).` });
2474
+ return;
2475
+ }
2476
+ if (action === "leave") {
2477
+ await patchSessionBinding(host.client, root, (servers) => {
2478
+ delete servers[b.server];
2479
+ servers[b.server] = { optedOut: true };
2480
+ });
2481
+ finish(host, { ok: true, message: `${hostOf(b.url || b.server)} left as-is \u2014 a snapshot until its TTL.` });
2482
+ return;
2483
+ }
2484
+ }
2485
+ }
2486
+ async function showCollisionDialog(host, root, server, shareId, error) {
2487
+ const debugInfo = JSON.stringify({ server, shareId, error, sessionID: root, plugin: "openshare", time: new Date().toISOString() }, null, 2);
2488
+ const choice = await host.ui.showMenu({
2489
+ title: `Share conflict \u2014 ${hostOf(server)}`,
2490
+ hint: `saved credentials for ${shareId.slice(0, 12)}\u2026 were rejected (${error})`,
2491
+ options: [
2492
+ {
2493
+ title: "Copy debug info",
2494
+ value: "debug",
2495
+ description: "server, share id, error \u2014 for the hoster or an issue report",
2496
+ help: `Copies a JSON blob with the server, share id, exact error, and session id \u2014 attach it when reporting to the hoster or filing an issue.
2497
+
2498
+ ${debugInfo}`
2499
+ },
2500
+ {
2501
+ title: "Share on a new share",
2502
+ value: "new",
2503
+ description: "abandon the old binding; create a fresh share with full history",
2504
+ help: `Drops the rejected binding and lets the engine create a brand-new share on ${hostOf(server)} with the session's full history. The old page (whoever owns it now) is left untouched.`
2505
+ },
2506
+ {
2507
+ title: "Don't share here",
2508
+ value: "no",
2509
+ description: "leave the old page; this server stops receiving this session",
2510
+ help: `Opts this session out on ${hostOf(server)}: the engine stops trying to attach. The rejected page stays as-is. Re-share here anytime via "Share to another server".`
2511
+ },
2512
+ { title: "Cancel", value: "cancel" }
2513
+ ]
2514
+ });
2515
+ if (!choice || choice === "cancel")
2516
+ return;
2517
+ if (choice === "debug") {
2518
+ const ok = await host.clipboard.write(debugInfo);
2519
+ finish(host, ok ? { ok: true, message: "Debug info copied to clipboard." } : { ok: false, message: `Clipboard failed:
2520
+ ${debugInfo}` });
2521
+ return;
2522
+ }
2523
+ if (choice === "new") {
2524
+ await patchSessionBinding(host.client, root, (servers) => {
2525
+ delete servers[server];
2526
+ });
2527
+ finish(host, { ok: true, message: `Re-sharing on ${hostOf(server)} \u2014 a new share appears within seconds.` });
2528
+ return;
2529
+ }
2530
+ if (choice === "no") {
2531
+ await patchSessionBinding(host.client, root, (servers) => {
2532
+ delete servers[server];
2533
+ servers[server] = { optedOut: true };
2534
+ });
2535
+ finish(host, { ok: true, message: `${hostOf(server)} opted out for this session.` });
2536
+ }
2537
+ }
2538
+ function buildShareCommand(host, sessionID) {
2539
+ return {
2540
+ name: "session.share",
2541
+ title: "Share session (re:share)",
2542
+ desc: "Share this session via openshare; on a shared session: copy the link or stop sharing",
2543
+ slashName: "share",
2544
+ slashAliases: ["reshare"],
2545
+ category: PALETTE_CATEGORY,
2546
+ group: PALETTE_CATEGORY,
2547
+ run: async () => {
2548
+ dbgLog(`/share run: entered (sid=${sessionID() ?? "none"})`);
2549
+ host.ui.dialog.clear();
2550
+ const cfg = currentConfig();
2551
+ if (!isConfigured(cfg)) {
2552
+ onboardingDialog(host);
2553
+ return;
2554
+ }
2555
+ const sid = sessionID();
2556
+ if (!sid) {
2557
+ finish(host, { ok: false, message: "No active session to share." });
2558
+ return;
2559
+ }
2560
+ const root = await resolveRoot(host.client, sid);
2561
+ const bindings = await readBindings(host.client, root);
2562
+ const binding = bindings[0];
2563
+ dbgLog(`/share gate: sid=${sid} root=${root} bindings=${bindings.length}`);
2564
+ if (bindings.some((b) => b.url && b.enabled !== false)) {
2565
+ await showShareManager(host, sessionID);
2566
+ host.ui.dialog.clear();
2567
+ return;
2568
+ }
2569
+ const result = await runShareFlow(host, sid);
2570
+ if (result.message === "consent required") {
2571
+ const yes = await host.ui.showConfirm({
2572
+ title: "Share Session",
2573
+ message: `Publish this session (and its subagent sessions) to your openshare worker?
2574
+ The link lets anyone with it read the conversation.`,
2575
+ confirmLabel: "Share"
2576
+ });
2577
+ if (!yes)
2578
+ return;
2579
+ try {
2580
+ host.kv.set(CONSENT_KV_KEY, true);
2581
+ dbgLog("consent kv.set ok");
2582
+ } catch (e) {
2583
+ dbgLog(`consent kv.set THREW: ${e?.message ?? e}`);
2584
+ }
2585
+ try {
2586
+ finish(host, await confirmShare(host, root));
2587
+ } catch (e) {
2588
+ dbgLog(`confirmShare rejected: ${e?.stack ?? e}`);
2589
+ finish(host, { ok: false, message: "Share failed (see debug log)." });
2590
+ }
2591
+ return;
2592
+ }
2593
+ finish(host, result);
2594
+ }
2595
+ };
2596
+ }
2597
+ function buildSettingsCommand(host, sessionID) {
2598
+ return {
2599
+ name: "openshare.settings",
2600
+ title: "OpenShare: Settings",
2601
+ desc: "Worker connection, sharing defaults, read auth, this session",
2602
+ slashName: "openshare",
2603
+ category: PALETTE_CATEGORY,
2604
+ group: PALETTE_CATEGORY,
2605
+ run: async () => {
2606
+ host.ui.dialog.clear();
2607
+ await settingsDialog(host, sessionID());
2608
+ }
2609
+ };
2610
+ }
2611
+ function registerCommands(host, sessionID) {
2612
+ return host.keymap.registerLayer({
2613
+ commands: [buildShareCommand(host, sessionID), buildSettingsCommand(host, sessionID)],
2614
+ bindings: []
2615
+ });
2616
+ }
2617
+
2618
+ // src/tui.tsx
2619
+ init_sidebar();
2620
+
2621
+ // src/dialogs.tsx
2622
+ init_sidebar();
2623
+ import { use as _$use } from "@opentui/solid";
2624
+ import { effect as _$effect2 } from "@opentui/solid";
2625
+ import { memo as _$memo } from "@opentui/solid";
2626
+ import { createTextNode as _$createTextNode } from "@opentui/solid";
2627
+ import { insertNode as _$insertNode2 } from "@opentui/solid";
2628
+ import { insert as _$insert2 } from "@opentui/solid";
2629
+ import { createComponent as _$createComponent } from "@opentui/solid";
2630
+ import { setProp as _$setProp2 } from "@opentui/solid";
2631
+ import { createElement as _$createElement2 } from "@opentui/solid";
2632
+ import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js";
2633
+ import { useTerminalDimensions } from "@opentui/solid";
2634
+ var blockKey = (ctx) => {
2635
+ ctx?.event?.preventDefault?.();
2636
+ ctx?.event?.stopPropagation?.();
2637
+ };
2638
+ var SHIELDED_KEYS = [..."abcdefghijklmnopqrstuvwxyz".split(""), ..."0123456789".split(""), "space", "tab", "backspace", "delete", "home", "end", "left", "right", "/", "?", ":", ";", "'", '"', ",", ".", "-", "=", "[", "]", "\\", "`"];
2639
+ function shieldBindings(command, except = []) {
2640
+ const allowed = new Set(except);
2641
+ return SHIELDED_KEYS.filter((key) => !allowed.has(key)).map((key) => ({
2642
+ key,
2643
+ cmd: command,
2644
+ desc: "Keep input inside OpenShare"
2645
+ }));
2646
+ }
2647
+ function collectEditors(root) {
2648
+ const children = root.getChildren?.() ?? [];
2649
+ return [...root.showCursor !== undefined ? [root] : [], ...children.flatMap((child) => collectEditors(child))];
2650
+ }
2651
+ function useHidePromptCursor(api) {
2652
+ const editors = collectEditors(api.renderer.root);
2653
+ const previous = editors.map((editor) => ({
2654
+ editor,
2655
+ showCursor: editor.showCursor
2656
+ }));
2657
+ for (const editor of editors) {
2658
+ editor.showCursor = false;
2659
+ }
2660
+ onCleanup(() => {
2661
+ for (const item of previous) {
2662
+ if (item.editor.isDestroyed)
2663
+ continue;
2664
+ item.editor.showCursor = item.showCursor;
2665
+ }
2666
+ });
2667
+ }
2668
+ var HEIGHT_PRESETS = [{
2669
+ label: "compact",
2670
+ value: 35,
2671
+ key: "1"
2672
+ }, {
2673
+ label: "normal",
2674
+ value: 50,
2675
+ key: "2"
2676
+ }, {
2677
+ label: "tall",
2678
+ value: 70,
2679
+ key: "3"
2680
+ }, {
2681
+ label: "max",
2682
+ value: 100,
2683
+ key: "4"
2684
+ }];
2685
+ function dialogWidthOf(api) {
2686
+ return kvWidthOf(api.kv);
2687
+ }
2688
+ function dialogHeightPercentOf(api) {
2689
+ return kvHeightPercentOf(api.kv);
2690
+ }
2691
+ function dialogMetrics(api, terminalHeight, chromeRows, minRows) {
2692
+ const percent = dialogHeightPercentOf(api);
2693
+ const backdropBudget = Math.max(minRows + chromeRows, Math.floor(terminalHeight * 0.75) - 1);
2694
+ const available = Math.max(minRows, backdropBudget - chromeRows);
2695
+ const requested = Math.max(minRows, Math.floor(terminalHeight * percent / 100) - chromeRows);
2696
+ return {
2697
+ availableRows: available,
2698
+ targetRows: Math.min(requested, available)
2699
+ };
2700
+ }
2701
+ function maxRows(api, terminalHeight, chromeRows, minRows) {
2702
+ return dialogMetrics(api, terminalHeight, chromeRows, minRows).targetRows;
2703
+ }
2704
+ function dialogContentWidth(api) {
2705
+ const width = dialogWidthOf(api);
2706
+ return (width === "xlarge" ? 116 : width === "large" ? 88 : 60) - 8;
2707
+ }
2708
+ function useDialogSize(api) {
2709
+ createEffect(() => {
2710
+ try {
2711
+ api.ui.dialog.setSize(dialogWidthOf(api));
2712
+ } catch {}
2713
+ });
2714
+ }
2715
+ function cappedHeight(count, max, min = 1) {
2716
+ if (count <= 0)
2717
+ return min;
2718
+ return Math.max(min, Math.min(count, max));
2719
+ }
2720
+ function preWrap(text, width) {
2721
+ const out = [];
2722
+ for (const raw of text.split(/\r?\n/)) {
2723
+ if (raw.length === 0) {
2724
+ out.push("");
2725
+ continue;
2726
+ }
2727
+ let line = "";
2728
+ for (const word of raw.split(" ")) {
2729
+ const candidate = line.length === 0 ? word : `${line} ${word}`;
2730
+ if (candidate.length <= width) {
2731
+ line = candidate;
2732
+ continue;
2733
+ }
2734
+ if (line.length > 0)
2735
+ out.push(line);
2736
+ line = "";
2737
+ let token = word;
2738
+ while (token.length > width) {
2739
+ out.push(token.slice(0, width));
2740
+ token = token.slice(width);
2741
+ }
2742
+ line = token;
2743
+ }
2744
+ if (line.length > 0)
2745
+ out.push(line);
2746
+ }
2747
+ return out;
2748
+ }
2749
+ function infoLineAttrs(line) {
2750
+ if (line.length === 0)
2751
+ return {
2752
+ fg: "muted",
2753
+ bold: false
2754
+ };
2755
+ const heading = !line.startsWith(" ") && (line.endsWith(":") || /^[A-Z][A-Za-z0-9 ._-]+$/.test(line));
2756
+ if (/error|failed|refus|invalid|gone|danger|purge|delete/i.test(line))
2757
+ return {
2758
+ fg: "warning",
2759
+ bold: false
2760
+ };
2761
+ if (/saved|enrolled|connected|active|live|success|token saved/i.test(line))
2762
+ return {
2763
+ fg: "positive",
2764
+ bold: false
2765
+ };
2766
+ if (heading)
2767
+ return {
2768
+ fg: "heading",
2769
+ bold: true
2770
+ };
2771
+ return {
2772
+ fg: "muted",
2773
+ bold: false
2774
+ };
2775
+ }
2776
+ function InfoLines(props) {
2777
+ const theme = () => props.api.theme.current;
2778
+ const colorOf = (kind) => kind === "warning" ? theme().error : kind === "positive" ? theme().success : kind === "heading" ? theme().accent : theme().text;
2779
+ return (() => {
2780
+ var _el$ = _$createElement2("box");
2781
+ _$setProp2(_el$, "flexDirection", "column");
2782
+ _$setProp2(_el$, "gap", 0);
2783
+ _$insert2(_el$, _$createComponent(For, {
2784
+ get each() {
2785
+ return props.lines;
2786
+ },
2787
+ children: (line) => {
2788
+ const attrs = infoLineAttrs(line);
2789
+ return line.length === 0 ? (() => {
2790
+ var _el$2 = _$createElement2("text");
2791
+ _$insertNode2(_el$2, _$createTextNode(` `));
2792
+ return _el$2;
2793
+ })() : (() => {
2794
+ var _el$4 = _$createElement2("text");
2795
+ _$setProp2(_el$4, "wrapMode", "none");
2796
+ _$insert2(_el$4, (() => {
2797
+ var _c$ = _$memo(() => !!attrs.bold);
2798
+ return () => _c$() ? (() => {
2799
+ var _el$5 = _$createElement2("b");
2800
+ _$insert2(_el$5, line);
2801
+ return _el$5;
2802
+ })() : line;
2803
+ })());
2804
+ _$effect2((_$p) => _$setProp2(_el$4, "fg", colorOf(attrs.fg), _$p));
2805
+ return _el$4;
2806
+ })();
2807
+ }
2808
+ }));
2809
+ return _el$;
2810
+ })();
2811
+ }
2812
+ function menuTitleWidth(width, options) {
2813
+ const longest = Math.max(0, ...options.map((option) => option.title.length)) + 2;
2814
+ if (width === "xlarge")
2815
+ return Math.min(Math.max(20, longest), 28);
2816
+ if (width === "large")
2817
+ return Math.min(Math.max(18, longest), 24);
2818
+ return Math.min(Math.max(16, longest), 20);
2819
+ }
2820
+ function ellipsize(text, width) {
2821
+ return text.length > width ? `${text.slice(0, Math.max(1, width - 1))}\u2026` : text;
2822
+ }
2823
+ function DialogFrame(props) {
2824
+ const theme = () => props.api.theme.current;
2825
+ return (() => {
2826
+ var _el$6 = _$createElement2("box"), _el$7 = _$createElement2("box"), _el$8 = _$createElement2("text"), _el$9 = _$createElement2("b"), _el$0 = _$createElement2("box");
2827
+ _$insertNode2(_el$6, _el$7);
2828
+ _$insertNode2(_el$6, _el$0);
2829
+ _$setProp2(_el$6, "flexDirection", "column");
2830
+ _$setProp2(_el$6, "width", "100%");
2831
+ _$setProp2(_el$6, "paddingLeft", 2);
2832
+ _$setProp2(_el$6, "paddingRight", 2);
2833
+ _$insertNode2(_el$7, _el$8);
2834
+ _$setProp2(_el$7, "width", "100%");
2835
+ _$setProp2(_el$7, "marginBottom", 1);
2836
+ _$insertNode2(_el$8, _el$9);
2837
+ _$insert2(_el$9, () => props.title);
2838
+ _$setProp2(_el$0, "width", "100%");
2839
+ _$setProp2(_el$0, "height", 1);
2840
+ _$setProp2(_el$0, "marginBottom", 1);
2841
+ _$insert2(_el$6, () => props.children, null);
2842
+ _$effect2((_p$) => {
2843
+ var _v$ = theme().accent, _v$2 = theme().accent;
2844
+ _v$ !== _p$.e && (_p$.e = _$setProp2(_el$8, "fg", _v$, _p$.e));
2845
+ _v$2 !== _p$.t && (_p$.t = _$setProp2(_el$0, "backgroundColor", _v$2, _p$.t));
2846
+ return _p$;
2847
+ }, {
2848
+ e: undefined,
2849
+ t: undefined
2850
+ });
2851
+ return _el$6;
2852
+ })();
2853
+ }
2854
+ function KeyLegend(props) {
2855
+ const theme = () => props.api.theme.current;
2856
+ return (() => {
2857
+ var _el$1 = _$createElement2("box");
2858
+ _$setProp2(_el$1, "flexDirection", "row");
2859
+ _$setProp2(_el$1, "gap", 1);
2860
+ _$setProp2(_el$1, "paddingTop", 1);
2861
+ _$setProp2(_el$1, "paddingLeft", 1);
2862
+ _$setProp2(_el$1, "alignItems", "center");
2863
+ _$insert2(_el$1, _$createComponent(For, {
2864
+ get each() {
2865
+ return props.keys;
2866
+ },
2867
+ children: (pair, index) => [(() => {
2868
+ var _el$10 = _$createElement2("box"), _el$11 = _$createElement2("text"), _el$12 = _$createElement2("b");
2869
+ _$insertNode2(_el$10, _el$11);
2870
+ _$setProp2(_el$10, "paddingLeft", 1);
2871
+ _$setProp2(_el$10, "paddingRight", 1);
2872
+ _$setProp2(_el$10, "onMouseUp", () => {
2873
+ if (pair[0] === "esc")
2874
+ props.onBack?.();
2875
+ });
2876
+ _$insertNode2(_el$11, _el$12);
2877
+ _$insert2(_el$12, () => pair[0]);
2878
+ _$effect2((_p$) => {
2879
+ var _v$3 = theme().backgroundPanel, _v$4 = theme().accent;
2880
+ _v$3 !== _p$.e && (_p$.e = _$setProp2(_el$10, "backgroundColor", _v$3, _p$.e));
2881
+ _v$4 !== _p$.t && (_p$.t = _$setProp2(_el$11, "fg", _v$4, _p$.t));
2882
+ return _p$;
2883
+ }, {
2884
+ e: undefined,
2885
+ t: undefined
2886
+ });
2887
+ return _el$10;
2888
+ })(), (() => {
2889
+ var _el$13 = _$createElement2("text");
2890
+ _$insert2(_el$13, () => pair[1]);
2891
+ _$effect2((_$p) => _$setProp2(_el$13, "fg", theme().textMuted, _$p));
2892
+ return _el$13;
2893
+ })(), _$createComponent(Show, {
2894
+ get when() {
2895
+ return index() < props.keys.length - 1;
2896
+ },
2897
+ get children() {
2898
+ var _el$14 = _$createElement2("text");
2899
+ _$insertNode2(_el$14, _$createTextNode(`\xB7`));
2900
+ _$effect2((_$p) => _$setProp2(_el$14, "fg", theme().textMuted, _$p));
2901
+ return _el$14;
2902
+ }
2903
+ })]
2904
+ }));
2905
+ return _el$1;
2906
+ })();
2907
+ }
2908
+ function MenuDialog(props) {
2909
+ const theme = () => props.api.theme.current;
2910
+ useDialogSize(props.api);
2911
+ useHidePromptCursor(props.api);
2912
+ const dimensions = useTerminalDimensions();
2913
+ const listHeight = createMemo(() => cappedHeight(props.options.length, maxRows(props.api, dimensions().height, 12, 4), 4));
2914
+ const width = dialogWidthOf(props.api);
2915
+ const titleWidth = createMemo(() => menuTitleWidth(width, props.options));
2916
+ const hasHelp = createMemo(() => props.options.some((option) => option.help));
2917
+ const hintLines = createMemo(() => props.hint ? preWrap(props.hint, dialogContentWidth(props.api)) : []);
2918
+ const [inspecting, setInspecting] = createSignal(false);
2919
+ let listScroll;
2920
+ let infoScroll;
2921
+ const popMode = props.api.mode.push("openshare.dialog");
2922
+ const [selected, setSelected] = createSignal((() => {
2923
+ const initial = Math.max(0, props.options.findIndex((option) => option.value === props.current));
2924
+ let index = initial;
2925
+ while (index < props.options.length && props.options[index]?.divider)
2926
+ index++;
2927
+ return index < props.options.length ? index : initial;
2928
+ })());
2929
+ const current = createMemo(() => props.options[selected()] ?? props.options[0]);
2930
+ const move = (delta) => setSelected((value) => {
2931
+ let next = Math.max(0, Math.min(props.options.length - 1, value + delta));
2932
+ while (next >= 0 && next < props.options.length && props.options[next]?.divider)
2933
+ next += delta >= 0 ? 1 : -1;
2934
+ if (next < 0 || next >= props.options.length || props.options[next]?.divider)
2935
+ return value;
2936
+ listScroll?.scrollTo(Math.max(0, next - 2));
2937
+ return next;
2938
+ });
2939
+ const helpSections = createMemo(() => {
2940
+ const raw = current()?.help ?? "";
2941
+ const sections = [{
2942
+ title: "",
2943
+ lines: []
2944
+ }];
2945
+ for (const line of raw.split(/\r?\n/)) {
2946
+ const match = /^##\s+(.*)$/.exec(line);
2947
+ if (match)
2948
+ sections.push({
2949
+ title: match[1],
2950
+ lines: []
2951
+ });
2952
+ else
2953
+ sections.at(-1)?.lines.push(line);
2954
+ }
2955
+ for (const section2 of sections) {
2956
+ while (section2.lines.length && section2.lines.at(-1) === "")
2957
+ section2.lines.pop();
2958
+ while (section2.lines.length && section2.lines[0] === "")
2959
+ section2.lines.shift();
2960
+ }
2961
+ const contentWidth = dialogContentWidth(props.api) - 2;
2962
+ return sections.filter((section2, index) => index === 0 ? section2.lines.length > 0 : true).map((section2) => ({
2963
+ title: section2.title,
2964
+ lines: preWrap(section2.lines.join(`
2965
+ `), contentWidth)
2966
+ }));
2967
+ });
2968
+ const infoHeight = createMemo(() => cappedHeight(helpSections().reduce((rows, section2) => rows + section2.lines.length + (section2.title ? 2 : 0), 0) + 1, maxRows(props.api, dimensions().height, 8, 4), 4));
2969
+ const [section, setSection] = createSignal(0);
2970
+ const jumpSection = (delta) => {
2971
+ const count = helpSections().length;
2972
+ if (count <= 1)
2973
+ return;
2974
+ const next = (section() + delta + count) % count;
2975
+ setSection(next);
2976
+ infoScroll?.scrollTo(0);
2977
+ };
2978
+ const vertical = (delta) => {
2979
+ if (inspecting())
2980
+ infoScroll?.scrollBy(delta);
2981
+ else
2982
+ move(delta);
2983
+ };
2984
+ const page = (delta) => {
2985
+ if (inspecting())
2986
+ infoScroll?.scrollBy(delta * Math.max(1, (infoScroll?.height ?? infoHeight()) - 1));
2987
+ };
2988
+ const choose = () => {
2989
+ const option = current();
2990
+ if (!option || option.disabled || option.divider)
2991
+ return;
2992
+ props.onDone(option.value);
2993
+ };
2994
+ const inspect = () => {
2995
+ const option = current();
2996
+ if (!option || !option.help)
2997
+ return;
2998
+ setInspecting(true);
2999
+ setSection(0);
3000
+ infoScroll?.scrollTo(0);
3001
+ };
3002
+ const commandPrefix = `openshare.menu.${Math.random().toString(36).slice(2)}`;
3003
+ const sectionJumpBindings = () => helpSections().length > 1 ? [{
3004
+ key: "p",
3005
+ cmd: `${commandPrefix}.prevSection`,
3006
+ desc: "Previous section"
3007
+ }, {
3008
+ key: "n",
3009
+ cmd: `${commandPrefix}.nextSection`,
3010
+ desc: "Next section"
3011
+ }, ...helpSections().slice(0, 9).map((_, index) => ({
3012
+ key: String(index + 1),
3013
+ cmd: `${commandPrefix}.jump${index + 1}`,
3014
+ desc: `Section ${index + 1}`
3015
+ }))] : [];
3016
+ const unregister = props.api.keymap.registerLayer({
3017
+ priority: 1e4,
3018
+ commands: [{
3019
+ name: `${commandPrefix}.up`,
3020
+ title: "Previous item",
3021
+ run: (ctx) => {
3022
+ blockKey(ctx);
3023
+ vertical(-1);
3024
+ }
3025
+ }, {
3026
+ name: `${commandPrefix}.down`,
3027
+ title: "Next item",
3028
+ run: (ctx) => {
3029
+ blockKey(ctx);
3030
+ vertical(1);
3031
+ }
3032
+ }, {
3033
+ name: `${commandPrefix}.pageUp`,
3034
+ title: "Page up",
3035
+ run: (ctx) => {
3036
+ blockKey(ctx);
3037
+ page(-1);
3038
+ }
3039
+ }, {
3040
+ name: `${commandPrefix}.pageDown`,
3041
+ title: "Page down",
3042
+ run: (ctx) => {
3043
+ blockKey(ctx);
3044
+ page(1);
3045
+ }
3046
+ }, {
3047
+ name: `${commandPrefix}.home`,
3048
+ title: "Top",
3049
+ run: (ctx) => {
3050
+ blockKey(ctx);
3051
+ if (inspecting())
3052
+ infoScroll?.scrollTo(0);
3053
+ }
3054
+ }, {
3055
+ name: `${commandPrefix}.end`,
3056
+ title: "Bottom",
3057
+ run: (ctx) => {
3058
+ blockKey(ctx);
3059
+ if (inspecting())
3060
+ infoScroll?.scrollTo(infoScroll.scrollHeight);
3061
+ }
3062
+ }, {
3063
+ name: `${commandPrefix}.select`,
3064
+ title: "Select item",
3065
+ run: (ctx) => {
3066
+ blockKey(ctx);
3067
+ if (inspecting())
3068
+ setInspecting(false);
3069
+ else
3070
+ choose();
3071
+ }
3072
+ }, {
3073
+ name: `${commandPrefix}.inspect`,
3074
+ title: "Option info",
3075
+ run: (ctx) => {
3076
+ blockKey(ctx);
3077
+ if (!inspecting())
3078
+ inspect();
3079
+ }
3080
+ }, {
3081
+ name: `${commandPrefix}.back`,
3082
+ title: "Back",
3083
+ run: (ctx) => {
3084
+ blockKey(ctx);
3085
+ if (inspecting())
3086
+ setInspecting(false);
3087
+ else
3088
+ props.onDone(undefined);
3089
+ }
3090
+ }, {
3091
+ name: `${commandPrefix}.prevSection`,
3092
+ title: "Previous section",
3093
+ run: (ctx) => {
3094
+ blockKey(ctx);
3095
+ jumpSection(-1);
3096
+ }
3097
+ }, {
3098
+ name: `${commandPrefix}.nextSection`,
3099
+ title: "Next section",
3100
+ run: (ctx) => {
3101
+ blockKey(ctx);
3102
+ jumpSection(1);
3103
+ }
3104
+ }, ...helpSections().slice(0, 9).map((_, index) => ({
3105
+ name: `${commandPrefix}.jump${index + 1}`,
3106
+ title: `Section ${index + 1}`,
3107
+ run: (ctx) => {
3108
+ blockKey(ctx);
3109
+ setSection(index);
3110
+ infoScroll?.scrollTo(0);
3111
+ }
3112
+ })), {
3113
+ name: `${commandPrefix}.shield`,
3114
+ title: "Block background input",
3115
+ run: blockKey
3116
+ }],
3117
+ bindings: [{
3118
+ key: "up",
3119
+ cmd: `${commandPrefix}.up`,
3120
+ desc: "Previous item"
3121
+ }, {
3122
+ key: "ctrl+p",
3123
+ cmd: `${commandPrefix}.up`,
3124
+ desc: "Previous item"
3125
+ }, {
3126
+ key: "down",
3127
+ cmd: `${commandPrefix}.down`,
3128
+ desc: "Next item"
3129
+ }, {
3130
+ key: "ctrl+n",
3131
+ cmd: `${commandPrefix}.down`,
3132
+ desc: "Next item"
3133
+ }, {
3134
+ key: "pageup",
3135
+ cmd: `${commandPrefix}.pageUp`,
3136
+ desc: "Page up"
3137
+ }, {
3138
+ key: "ctrl+b",
3139
+ cmd: `${commandPrefix}.pageUp`,
3140
+ desc: "Page up"
3141
+ }, {
3142
+ key: "pagedown",
3143
+ cmd: `${commandPrefix}.pageDown`,
3144
+ desc: "Page down"
3145
+ }, {
3146
+ key: "ctrl+f",
3147
+ cmd: `${commandPrefix}.pageDown`,
3148
+ desc: "Page down"
3149
+ }, {
3150
+ key: "home",
3151
+ cmd: `${commandPrefix}.home`,
3152
+ desc: "Top"
3153
+ }, {
3154
+ key: "end",
3155
+ cmd: `${commandPrefix}.end`,
3156
+ desc: "Bottom"
3157
+ }, {
3158
+ key: "enter",
3159
+ cmd: `${commandPrefix}.select`,
3160
+ desc: "Select item"
3161
+ }, {
3162
+ key: "i",
3163
+ cmd: `${commandPrefix}.inspect`,
3164
+ desc: "Option info"
3165
+ }, {
3166
+ key: "escape",
3167
+ cmd: `${commandPrefix}.back`,
3168
+ desc: "Back"
3169
+ }, ...sectionJumpBindings(), ...shieldBindings(`${commandPrefix}.shield`, ["i", ...sectionJumpBindings().map((binding) => binding.key)])]
3170
+ });
3171
+ onCleanup(() => {
3172
+ unregister();
3173
+ popMode();
3174
+ });
3175
+ return _$createComponent(DialogFrame, {
3176
+ get api() {
3177
+ return props.api;
3178
+ },
3179
+ get title() {
3180
+ return props.title;
3181
+ },
3182
+ get children() {
3183
+ return [_$createComponent(Show, {
3184
+ get when() {
3185
+ return hintLines().length > 0;
3186
+ },
3187
+ get children() {
3188
+ var _el$16 = _$createElement2("box");
3189
+ _$setProp2(_el$16, "flexDirection", "column");
3190
+ _$setProp2(_el$16, "marginBottom", 1);
3191
+ _$setProp2(_el$16, "paddingLeft", 1);
3192
+ _$setProp2(_el$16, "paddingRight", 1);
3193
+ _$insert2(_el$16, _$createComponent(InfoLines, {
3194
+ get api() {
3195
+ return props.api;
3196
+ },
3197
+ get lines() {
3198
+ return hintLines();
3199
+ }
3200
+ }));
3201
+ return _el$16;
3202
+ }
3203
+ }), _$createComponent(Show, {
3204
+ get when() {
3205
+ return _$memo(() => !!inspecting())() && current()?.help;
3206
+ },
3207
+ get fallback() {
3208
+ return (() => {
3209
+ var _el$25 = _$createElement2("scrollbox"), _el$26 = _$createElement2("box");
3210
+ _$insertNode2(_el$25, _el$26);
3211
+ _$use((element) => listScroll = element, _el$25);
3212
+ _$setProp2(_el$26, "flexDirection", "column");
3213
+ _$setProp2(_el$26, "gap", 0);
3214
+ _$insert2(_el$26, _$createComponent(For, {
3215
+ get each() {
3216
+ return props.options;
3217
+ },
3218
+ children: (option, index) => {
3219
+ const active = createMemo(() => selected() === index());
3220
+ const fg = createMemo(() => active() ? theme().background : option.divider ? theme().textMuted : option.disabled ? theme().textMuted : option.danger ? theme().error : option.edited ? theme().success : theme().text);
3221
+ const descFg = createMemo(() => active() ? theme().background : theme().textMuted);
3222
+ if (option.divider) {
3223
+ return (() => {
3224
+ var _el$27 = _$createElement2("box"), _el$28 = _$createElement2("text");
3225
+ _$insertNode2(_el$27, _el$28);
3226
+ _$setProp2(_el$27, "paddingLeft", 1);
3227
+ _$setProp2(_el$27, "paddingRight", 1);
3228
+ _$setProp2(_el$27, "marginTop", 1);
3229
+ _$setProp2(_el$27, "marginBottom", 1);
3230
+ _$insert2(_el$28, () => "\u2500".repeat(Math.max(8, dialogContentWidth(props.api) - 4)));
3231
+ _$effect2((_$p) => _$setProp2(_el$28, "fg", theme().textMuted, _$p));
3232
+ return _el$27;
3233
+ })();
3234
+ }
3235
+ return (() => {
3236
+ var _el$29 = _$createElement2("box"), _el$30 = _$createElement2("text"), _el$31 = _$createElement2("text"), _el$32 = _$createElement2("b"), _el$33 = _$createElement2("text");
3237
+ _$insertNode2(_el$29, _el$30);
3238
+ _$insertNode2(_el$29, _el$31);
3239
+ _$insertNode2(_el$29, _el$33);
3240
+ _$setProp2(_el$29, "flexDirection", "row");
3241
+ _$setProp2(_el$29, "width", "100%");
3242
+ _$setProp2(_el$29, "gap", 1);
3243
+ _$setProp2(_el$29, "paddingLeft", 1);
3244
+ _$setProp2(_el$29, "paddingRight", 1);
3245
+ _$setProp2(_el$29, "onMouseOver", () => {
3246
+ if (!option.divider)
3247
+ setSelected(index());
3248
+ });
3249
+ _$setProp2(_el$29, "onMouseUp", () => {
3250
+ if (!option.disabled && !option.divider)
3251
+ props.onDone(option.value);
3252
+ });
3253
+ _$setProp2(_el$30, "width", 2);
3254
+ _$setProp2(_el$30, "flexShrink", 0);
3255
+ _$insert2(_el$30, () => active() ? "\u276F" : " ");
3256
+ _$insertNode2(_el$31, _el$32);
3257
+ _$setProp2(_el$31, "flexShrink", 0);
3258
+ _$setProp2(_el$31, "wrapMode", "none");
3259
+ _$setProp2(_el$31, "overflow", "hidden");
3260
+ _$insert2(_el$32, () => ellipsize(option.title, titleWidth()));
3261
+ _$setProp2(_el$33, "flexGrow", 1);
3262
+ _$setProp2(_el$33, "wrapMode", "none");
3263
+ _$setProp2(_el$33, "overflow", "hidden");
3264
+ _$insert2(_el$33, () => option.help ? "\u24D8 " : "", null);
3265
+ _$insert2(_el$33, () => option.description ?? "", null);
3266
+ _$effect2((_p$) => {
3267
+ var _v$7 = active() ? theme().primary : theme().backgroundPanel, _v$8 = fg(), _v$9 = titleWidth(), _v$0 = fg(), _v$1 = descFg();
3268
+ _v$7 !== _p$.e && (_p$.e = _$setProp2(_el$29, "backgroundColor", _v$7, _p$.e));
3269
+ _v$8 !== _p$.t && (_p$.t = _$setProp2(_el$30, "fg", _v$8, _p$.t));
3270
+ _v$9 !== _p$.a && (_p$.a = _$setProp2(_el$31, "width", _v$9, _p$.a));
3271
+ _v$0 !== _p$.o && (_p$.o = _$setProp2(_el$31, "fg", _v$0, _p$.o));
3272
+ _v$1 !== _p$.i && (_p$.i = _$setProp2(_el$33, "fg", _v$1, _p$.i));
3273
+ return _p$;
3274
+ }, {
3275
+ e: undefined,
3276
+ t: undefined,
3277
+ a: undefined,
3278
+ o: undefined,
3279
+ i: undefined
3280
+ });
3281
+ return _el$29;
3282
+ })();
3283
+ }
3284
+ }));
3285
+ _$effect2((_$p) => _$setProp2(_el$25, "maxHeight", listHeight(), _$p));
3286
+ return _el$25;
3287
+ })();
3288
+ },
3289
+ get children() {
3290
+ var _el$17 = _$createElement2("box"), _el$18 = _$createElement2("box"), _el$19 = _$createElement2("text"), _el$20 = _$createElement2("b"), _el$21 = _$createElement2("scrollbox"), _el$22 = _$createElement2("box");
3291
+ _$insertNode2(_el$17, _el$18);
3292
+ _$insertNode2(_el$17, _el$21);
3293
+ _$setProp2(_el$17, "flexDirection", "column");
3294
+ _$insertNode2(_el$18, _el$19);
3295
+ _$setProp2(_el$18, "marginBottom", 1);
3296
+ _$setProp2(_el$18, "paddingLeft", 1);
3297
+ _$insertNode2(_el$19, _el$20);
3298
+ _$setProp2(_el$19, "wrapMode", "none");
3299
+ _$setProp2(_el$19, "overflow", "hidden");
3300
+ _$insert2(_el$20, () => current()?.title ?? "");
3301
+ _$insertNode2(_el$21, _el$22);
3302
+ _$use((element) => infoScroll = element, _el$21);
3303
+ _$setProp2(_el$22, "flexDirection", "column");
3304
+ _$setProp2(_el$22, "paddingLeft", 1);
3305
+ _$setProp2(_el$22, "paddingRight", 1);
3306
+ _$insert2(_el$22, _$createComponent(For, {
3307
+ get each() {
3308
+ return helpSections();
3309
+ },
3310
+ children: (sectionData, index) => [_$createComponent(Show, {
3311
+ get when() {
3312
+ return sectionData.title;
3313
+ },
3314
+ get children() {
3315
+ var _el$34 = _$createElement2("box"), _el$35 = _$createElement2("text"), _el$36 = _$createElement2("b");
3316
+ _$insertNode2(_el$34, _el$35);
3317
+ _$setProp2(_el$34, "marginBottom", 1);
3318
+ _$insertNode2(_el$35, _el$36);
3319
+ _$insert2(_el$36, () => `\u2500\u2500 ${sectionData.title} `, null);
3320
+ _$insert2(_el$36, () => "\u2500".repeat(Math.max(0, Math.min(40, 40 - sectionData.title.length))), null);
3321
+ _$effect2((_p$) => {
3322
+ var _v$10 = index() > 0 ? 1 : 0, _v$11 = theme().accent;
3323
+ _v$10 !== _p$.e && (_p$.e = _$setProp2(_el$34, "marginTop", _v$10, _p$.e));
3324
+ _v$11 !== _p$.t && (_p$.t = _$setProp2(_el$35, "fg", _v$11, _p$.t));
3325
+ return _p$;
3326
+ }, {
3327
+ e: undefined,
3328
+ t: undefined
3329
+ });
3330
+ return _el$34;
3331
+ }
3332
+ }), _$createComponent(InfoLines, {
3333
+ get api() {
3334
+ return props.api;
3335
+ },
3336
+ get lines() {
3337
+ return sectionData.lines;
3338
+ }
3339
+ })]
3340
+ }));
3341
+ _$insert2(_el$17, _$createComponent(Show, {
3342
+ get when() {
3343
+ return helpSections().length > 1;
3344
+ },
3345
+ get children() {
3346
+ var _el$23 = _$createElement2("box"), _el$24 = _$createElement2("text");
3347
+ _$insertNode2(_el$23, _el$24);
3348
+ _$setProp2(_el$23, "paddingLeft", 1);
3349
+ _$setProp2(_el$23, "marginTop", 1);
3350
+ _$insert2(_el$24, () => `section ${section() + 1}/${helpSections().length} \xB7 p/n move \xB7 1-${Math.min(9, helpSections().length)} jump`);
3351
+ _$effect2((_$p) => _$setProp2(_el$24, "fg", theme().textMuted, _$p));
3352
+ return _el$23;
3353
+ }
3354
+ }), null);
3355
+ _$effect2((_p$) => {
3356
+ var _v$5 = theme().text, _v$6 = infoHeight();
3357
+ _v$5 !== _p$.e && (_p$.e = _$setProp2(_el$19, "fg", _v$5, _p$.e));
3358
+ _v$6 !== _p$.t && (_p$.t = _$setProp2(_el$21, "maxHeight", _v$6, _p$.t));
3359
+ return _p$;
3360
+ }, {
3361
+ e: undefined,
3362
+ t: undefined
3363
+ });
3364
+ return _el$17;
3365
+ }
3366
+ }), _$createComponent(KeyLegend, {
3367
+ get api() {
3368
+ return props.api;
3369
+ },
3370
+ get keys() {
3371
+ return _$memo(() => !!inspecting())() ? helpSections().length > 1 ? [["\u2191\u2193", "scroll"], ["p/n", "section"], ["\u23CE", "back"], ["esc", "back"]] : [["\u2191\u2193", "scroll"], ["\u23CE", "back"], ["esc", "back"]] : hasHelp() ? [["\u2191\u2193", "move"], ["\u23CE", "select"], ["i", "info"], ["esc", "back"]] : [["\u2191\u2193", "move"], ["\u23CE", "select"], ["esc", "back"]];
3372
+ },
3373
+ onBack: () => {
3374
+ if (inspecting())
3375
+ setInspecting(false);
3376
+ else
3377
+ props.onDone(undefined);
3378
+ }
3379
+ })];
3380
+ }
3381
+ });
3382
+ }
3383
+ function showMenuOnce(api, props) {
3384
+ return new Promise((resolve) => {
3385
+ let settled = false;
3386
+ const done = (value, _clear = false) => {
3387
+ if (settled)
3388
+ return;
3389
+ settled = true;
3390
+ dbgLog(`menu(${props.title}) -> ${value === undefined ? "cancel" : String(value)}`);
3391
+ resolve(value);
3392
+ };
3393
+ dbgLog(`menu(${props.title}) OPEN (${props.options.length} options)`);
3394
+ api.ui.dialog.replace(() => _$createComponent(MenuDialog, {
3395
+ api,
3396
+ get title() {
3397
+ return props.title;
3398
+ },
3399
+ get options() {
3400
+ return props.options;
3401
+ },
3402
+ get current() {
3403
+ return props.current;
3404
+ },
3405
+ get hint() {
3406
+ return props.hint;
3407
+ },
3408
+ onDone: done
3409
+ }), () => done(undefined));
3410
+ });
3411
+ }
3412
+ function showInfoDialog(api, props) {
3413
+ return new Promise((resolve) => {
3414
+ let settled = false;
3415
+ const done = (value) => {
3416
+ if (settled)
3417
+ return;
3418
+ settled = true;
3419
+ resolve(value);
3420
+ };
3421
+ api.ui.dialog.replace(() => _$createComponent(InfoDialog, {
3422
+ api,
3423
+ get title() {
3424
+ return props.title;
3425
+ },
3426
+ get message() {
3427
+ return props.message;
3428
+ },
3429
+ get actions() {
3430
+ return props.actions;
3431
+ },
3432
+ onDone: done
3433
+ }), () => done(undefined));
3434
+ });
3435
+ }
3436
+ function InfoDialog(props) {
3437
+ const theme = () => props.api.theme.current;
3438
+ useDialogSize(props.api);
3439
+ useHidePromptCursor(props.api);
3440
+ const dimensions = useTerminalDimensions();
3441
+ const lines = createMemo(() => preWrap(props.message, dialogContentWidth(props.api)));
3442
+ const bodyHeight = createMemo(() => cappedHeight(lines().length + 1, maxRows(props.api, dimensions().height, 8, 4), 4));
3443
+ let scroll;
3444
+ const page = () => Math.max(1, (scroll?.height ?? bodyHeight()) - 1);
3445
+ const popMode = props.api.mode.push("openshare.dialog");
3446
+ const commandPrefix = `openshare.info.${Math.random().toString(36).slice(2)}`;
3447
+ const okValue = "__ok";
3448
+ const actions = createMemo(() => {
3449
+ const list = props.actions?.length ? props.actions : [{
3450
+ title: "ok",
3451
+ value: okValue,
3452
+ danger: false
3453
+ }];
3454
+ return list.map((action, index) => ({
3455
+ ...action,
3456
+ key: action.key ?? (index === 0 ? "enter" : undefined)
3457
+ }));
3458
+ });
3459
+ const unregister = props.api.keymap.registerLayer({
3460
+ priority: 1e4,
3461
+ commands: [{
3462
+ name: `${commandPrefix}.close`,
3463
+ title: "Close",
3464
+ run: (ctx) => {
3465
+ blockKey(ctx);
3466
+ props.onDone(actions().find((a) => a.key === "enter")?.value ?? okValue);
3467
+ }
3468
+ }, {
3469
+ name: `${commandPrefix}.back`,
3470
+ title: "Back",
3471
+ run: (ctx) => {
3472
+ blockKey(ctx);
3473
+ props.onDone(undefined);
3474
+ }
3475
+ }, {
3476
+ name: `${commandPrefix}.up`,
3477
+ title: "Scroll up",
3478
+ run: (ctx) => {
3479
+ blockKey(ctx);
3480
+ scroll?.scrollBy(-1);
3481
+ }
3482
+ }, {
3483
+ name: `${commandPrefix}.down`,
3484
+ title: "Scroll down",
3485
+ run: (ctx) => {
3486
+ blockKey(ctx);
3487
+ scroll?.scrollBy(1);
3488
+ }
3489
+ }, {
3490
+ name: `${commandPrefix}.pageUp`,
3491
+ title: "Page up",
3492
+ run: (ctx) => {
3493
+ blockKey(ctx);
3494
+ scroll?.scrollBy(-page());
3495
+ }
3496
+ }, {
3497
+ name: `${commandPrefix}.pageDown`,
3498
+ title: "Page down",
3499
+ run: (ctx) => {
3500
+ blockKey(ctx);
3501
+ scroll?.scrollBy(page());
3502
+ }
3503
+ }, {
3504
+ name: `${commandPrefix}.home`,
3505
+ title: "Scroll top",
3506
+ run: (ctx) => {
3507
+ blockKey(ctx);
3508
+ scroll?.scrollTo(0);
3509
+ }
3510
+ }, {
3511
+ name: `${commandPrefix}.end`,
3512
+ title: "Scroll bottom",
3513
+ run: (ctx) => {
3514
+ blockKey(ctx);
3515
+ scroll?.scrollTo(scroll.scrollHeight);
3516
+ }
3517
+ }, {
3518
+ name: `${commandPrefix}.shield`,
3519
+ title: "Block background input",
3520
+ run: blockKey
3521
+ }, ...actions().filter((action) => action.key && action.key !== "enter").map((action) => ({
3522
+ name: `${commandPrefix}.act_${action.value}`,
3523
+ title: action.title,
3524
+ run: (ctx) => {
3525
+ blockKey(ctx);
3526
+ props.onDone(action.value);
3527
+ }
3528
+ }))],
3529
+ bindings: [{
3530
+ key: "enter",
3531
+ cmd: `${commandPrefix}.close`,
3532
+ desc: "Close"
3533
+ }, {
3534
+ key: "escape",
3535
+ cmd: `${commandPrefix}.back`,
3536
+ desc: "Back"
3537
+ }, {
3538
+ key: "up",
3539
+ cmd: `${commandPrefix}.up`,
3540
+ desc: "Scroll up"
3541
+ }, {
3542
+ key: "ctrl+p",
3543
+ cmd: `${commandPrefix}.up`,
3544
+ desc: "Scroll up"
3545
+ }, {
3546
+ key: "down",
3547
+ cmd: `${commandPrefix}.down`,
3548
+ desc: "Scroll down"
3549
+ }, {
3550
+ key: "ctrl+n",
3551
+ cmd: `${commandPrefix}.down`,
3552
+ desc: "Scroll down"
3553
+ }, {
3554
+ key: "pageup",
3555
+ cmd: `${commandPrefix}.pageUp`,
3556
+ desc: "Page up"
3557
+ }, {
3558
+ key: "ctrl+b",
3559
+ cmd: `${commandPrefix}.pageUp`,
3560
+ desc: "Page up"
3561
+ }, {
3562
+ key: "pagedown",
3563
+ cmd: `${commandPrefix}.pageDown`,
3564
+ desc: "Page down"
3565
+ }, {
3566
+ key: "ctrl+f",
3567
+ cmd: `${commandPrefix}.pageDown`,
3568
+ desc: "Page down"
3569
+ }, {
3570
+ key: "home",
3571
+ cmd: `${commandPrefix}.home`,
3572
+ desc: "Scroll top"
3573
+ }, {
3574
+ key: "end",
3575
+ cmd: `${commandPrefix}.end`,
3576
+ desc: "Scroll bottom"
3577
+ }, ...actions().filter((action) => action.key && action.key !== "enter").map((action) => ({
3578
+ key: action.key,
3579
+ cmd: `${commandPrefix}.act_${action.value}`,
3580
+ desc: action.title
3581
+ })), ...shieldBindings(`${commandPrefix}.shield`, ["home", "end", ...actions().filter((a) => a.key && a.key !== "enter").map((a) => a.key)])]
3582
+ });
3583
+ onCleanup(() => {
3584
+ unregister();
3585
+ popMode();
3586
+ });
3587
+ return _$createComponent(DialogFrame, {
3588
+ get api() {
3589
+ return props.api;
3590
+ },
3591
+ get title() {
3592
+ return props.title;
3593
+ },
3594
+ get children() {
3595
+ return [(() => {
3596
+ var _el$37 = _$createElement2("scrollbox"), _el$38 = _$createElement2("box");
3597
+ _$insertNode2(_el$37, _el$38);
3598
+ _$use((element) => scroll = element, _el$37);
3599
+ _$setProp2(_el$38, "flexDirection", "column");
3600
+ _$setProp2(_el$38, "paddingLeft", 1);
3601
+ _$setProp2(_el$38, "paddingRight", 1);
3602
+ _$insert2(_el$38, _$createComponent(InfoLines, {
3603
+ get api() {
3604
+ return props.api;
3605
+ },
3606
+ get lines() {
3607
+ return lines();
3608
+ }
3609
+ }));
3610
+ _$effect2((_$p) => _$setProp2(_el$37, "maxHeight", bodyHeight(), _$p));
3611
+ return _el$37;
3612
+ })(), (() => {
3613
+ var _el$39 = _$createElement2("box"), _el$40 = _$createElement2("box"), _el$41 = _$createElement2("box");
3614
+ _$insertNode2(_el$39, _el$40);
3615
+ _$insertNode2(_el$39, _el$41);
3616
+ _$setProp2(_el$39, "flexDirection", "row");
3617
+ _$setProp2(_el$39, "justifyContent", "space-between");
3618
+ _$setProp2(_el$39, "width", "100%");
3619
+ _$setProp2(_el$39, "paddingTop", 1);
3620
+ _$setProp2(_el$40, "flexDirection", "row");
3621
+ _$setProp2(_el$40, "gap", 1);
3622
+ _$insert2(_el$40, _$createComponent(For, {
3623
+ get each() {
3624
+ return actions().filter((action) => action.key && action.key !== "enter");
3625
+ },
3626
+ children: (action) => (() => {
3627
+ var _el$42 = _$createElement2("text");
3628
+ _$insert2(_el$42, () => `${action.key}: ${action.title}`);
3629
+ _$effect2((_$p) => _$setProp2(_el$42, "fg", action.danger ? theme().error : theme().textMuted, _$p));
3630
+ return _el$42;
3631
+ })()
3632
+ }));
3633
+ _$setProp2(_el$41, "flexDirection", "row");
3634
+ _$setProp2(_el$41, "gap", 1);
3635
+ _$insert2(_el$41, _$createComponent(For, {
3636
+ get each() {
3637
+ return actions();
3638
+ },
3639
+ children: (action) => (() => {
3640
+ var _el$43 = _$createElement2("box"), _el$44 = _$createElement2("text"), _el$45 = _$createElement2("b");
3641
+ _$insertNode2(_el$43, _el$44);
3642
+ _$setProp2(_el$43, "paddingLeft", 2);
3643
+ _$setProp2(_el$43, "paddingRight", 2);
3644
+ _$setProp2(_el$43, "onMouseUp", () => props.onDone(action.value));
3645
+ _$insertNode2(_el$44, _el$45);
3646
+ _$insert2(_el$45, () => action.title);
3647
+ _$effect2((_p$) => {
3648
+ var _v$12 = action.danger ? theme().error : theme().primary, _v$13 = theme().background;
3649
+ _v$12 !== _p$.e && (_p$.e = _$setProp2(_el$43, "backgroundColor", _v$12, _p$.e));
3650
+ _v$13 !== _p$.t && (_p$.t = _$setProp2(_el$44, "fg", _v$13, _p$.t));
3651
+ return _p$;
3652
+ }, {
3653
+ e: undefined,
3654
+ t: undefined
3655
+ });
3656
+ return _el$43;
3657
+ })()
3658
+ }));
3659
+ return _el$39;
3660
+ })()];
3661
+ }
3662
+ });
3663
+ }
3664
+ function showConfirmMenu(api, props) {
3665
+ return showInfoDialog(api, {
3666
+ title: props.title,
3667
+ message: props.message,
3668
+ actions: [{
3669
+ title: props.confirmLabel ?? "Confirm",
3670
+ value: "yes",
3671
+ danger: props.danger
3672
+ }, {
3673
+ title: "Cancel",
3674
+ value: "no"
3675
+ }]
3676
+ }).then((value) => value === "yes");
3677
+ }
3678
+ function showAlertMenu(api, props) {
3679
+ return showInfoDialog(api, {
3680
+ title: props.title,
3681
+ message: props.message
3682
+ }).then(() => {
3683
+ return;
3684
+ });
3685
+ }
3686
+ var PROMPT_CHARS = [..."abcdefghijklmnopqrstuvwxyz", ..."ABCDEFGHIJKLMNOPQRSTUVWXYZ", ..."0123456789", ..."!@#$%^&*()-_=+[]{};:'\",.<>/?\\|`~", ..."://.-_?&=%~+#@"].join("").split("").filter((ch, index, all) => ch !== " " && all.indexOf(ch) === index).join("");
3687
+ function PromptDialog(props) {
3688
+ const theme = () => props.api.theme.current;
3689
+ useDialogSize(props.api);
3690
+ useHidePromptCursor(props.api);
3691
+ const [text, setText] = createSignal(props.value ?? "");
3692
+ const popMode = props.api.mode.push("openshare.dialog");
3693
+ const commandPrefix = `openshare.prompt.${Math.random().toString(36).slice(2)}`;
3694
+ const commands = [{
3695
+ name: `${commandPrefix}.ok`,
3696
+ title: "Confirm",
3697
+ run: (ctx) => {
3698
+ blockKey(ctx);
3699
+ props.onDone(text());
3700
+ }
3701
+ }, {
3702
+ name: `${commandPrefix}.back`,
3703
+ title: "Cancel",
3704
+ run: (ctx) => {
3705
+ blockKey(ctx);
3706
+ props.onDone(undefined);
3707
+ }
3708
+ }, {
3709
+ name: `${commandPrefix}.del`,
3710
+ title: "Backspace",
3711
+ run: (ctx) => {
3712
+ blockKey(ctx);
3713
+ setText((v) => v.slice(0, -1));
3714
+ }
3715
+ }, {
3716
+ name: `${commandPrefix}.shield`,
3717
+ title: "Block background input",
3718
+ run: blockKey
3719
+ }];
3720
+ const bindings = [{
3721
+ key: "enter",
3722
+ cmd: `${commandPrefix}.ok`,
3723
+ desc: "Confirm"
3724
+ }, {
3725
+ key: "escape",
3726
+ cmd: `${commandPrefix}.back`,
3727
+ desc: "Cancel"
3728
+ }, {
3729
+ key: "backspace",
3730
+ cmd: `${commandPrefix}.del`,
3731
+ desc: "Backspace"
3732
+ }, {
3733
+ key: "space",
3734
+ cmd: `${commandPrefix}.type_space`,
3735
+ desc: "Space"
3736
+ }];
3737
+ commands.push({
3738
+ name: `${commandPrefix}.type_space`,
3739
+ title: "Type space",
3740
+ run: (ctx) => {
3741
+ blockKey(ctx);
3742
+ setText((v) => v + " ");
3743
+ }
3744
+ });
3745
+ for (const ch of PROMPT_CHARS) {
3746
+ if (ch === " ")
3747
+ continue;
3748
+ const name = `type_${ch.codePointAt(0)}`;
3749
+ commands.push({
3750
+ name: `${commandPrefix}.${name}`,
3751
+ title: `Type ${ch}`,
3752
+ run: (ctx) => {
3753
+ blockKey(ctx);
3754
+ setText((v) => v + ch);
3755
+ }
3756
+ });
3757
+ bindings.push({
3758
+ key: ch,
3759
+ cmd: `${commandPrefix}.${name}`
3760
+ });
3761
+ }
3762
+ const unregister = props.api.keymap.registerLayer({
3763
+ priority: 1e4,
3764
+ commands,
3765
+ bindings
3766
+ });
3767
+ onCleanup(() => {
3768
+ unregister();
3769
+ popMode();
3770
+ });
3771
+ return _$createComponent(DialogFrame, {
3772
+ get api() {
3773
+ return props.api;
3774
+ },
3775
+ get title() {
3776
+ return props.title;
3777
+ },
3778
+ get children() {
3779
+ return [(() => {
3780
+ var _el$46 = _$createElement2("box"), _el$47 = _$createElement2("text"), _el$48 = _$createElement2("text");
3781
+ _$insertNode2(_el$46, _el$47);
3782
+ _$insertNode2(_el$46, _el$48);
3783
+ _$setProp2(_el$46, "paddingLeft", 1);
3784
+ _$setProp2(_el$46, "paddingRight", 1);
3785
+ _$setProp2(_el$46, "marginBottom", 1);
3786
+ _$insert2(_el$47, () => text() || props.placeholder || "");
3787
+ _$insertNode2(_el$48, _$createTextNode(`\u258C`));
3788
+ _$effect2((_p$) => {
3789
+ var _v$14 = theme().backgroundPanel, _v$15 = text() ? theme().text : theme().textMuted, _v$16 = theme().accent;
3790
+ _v$14 !== _p$.e && (_p$.e = _$setProp2(_el$46, "backgroundColor", _v$14, _p$.e));
3791
+ _v$15 !== _p$.t && (_p$.t = _$setProp2(_el$47, "fg", _v$15, _p$.t));
3792
+ _v$16 !== _p$.a && (_p$.a = _$setProp2(_el$48, "fg", _v$16, _p$.a));
3793
+ return _p$;
3794
+ }, {
3795
+ e: undefined,
3796
+ t: undefined,
3797
+ a: undefined
3798
+ });
3799
+ return _el$46;
3800
+ })(), _$createComponent(KeyLegend, {
3801
+ get api() {
3802
+ return props.api;
3803
+ },
3804
+ keys: [["a-z0-9", "type"], ["\u232B", "delete"], ["\u23CE", "confirm"], ["esc", "cancel"]],
3805
+ onBack: () => props.onDone(undefined)
3806
+ })];
3807
+ }
3808
+ });
3809
+ }
3810
+ function showPromptOnce(api, props) {
3811
+ return new Promise((resolve) => {
3812
+ let settled = false;
3813
+ const done = (value, _clear = false) => {
3814
+ if (settled)
3815
+ return;
3816
+ settled = true;
3817
+ dbgLog(`prompt(${props.title}) -> ${value === undefined ? "cancel" : `len=${value.length}`}`);
3818
+ resolve(value);
3819
+ };
3820
+ dbgLog(`prompt(${props.title}) OPEN`);
3821
+ api.ui.dialog.replace(() => _$createComponent(PromptDialog, {
3822
+ api,
3823
+ get title() {
3824
+ return props.title;
3825
+ },
3826
+ get placeholder() {
3827
+ return props.placeholder;
3828
+ },
3829
+ get value() {
3830
+ return props.value;
3831
+ },
3832
+ onDone: done
3833
+ }), () => done(undefined));
3834
+ });
3835
+ }
3836
+ function toMenuOptions(options) {
3837
+ return options.map((o) => ({
3838
+ title: o.title,
3839
+ description: o.description,
3840
+ value: o.value,
3841
+ disabled: o.disabled,
3842
+ help: o.help,
3843
+ danger: o.danger,
3844
+ edited: o.edited,
3845
+ divider: o.divider
3846
+ }));
3847
+ }
3848
+ function showSizePickerOnce(api, current) {
3849
+ return new Promise((resolve) => {
3850
+ let settled = false;
3851
+ const done = (value) => {
3852
+ if (settled)
3853
+ return;
3854
+ settled = true;
3855
+ resolve(value);
3856
+ };
3857
+ api.ui.dialog.replace(() => _$createComponent(SizePickerDialog, {
3858
+ api,
3859
+ current,
3860
+ onDone: done
3861
+ }), () => done(undefined));
3862
+ });
3863
+ }
3864
+ function SizePickerDialog(props) {
3865
+ const theme = () => props.api.theme.current;
3866
+ useDialogSize(props.api);
3867
+ useHidePromptCursor(props.api);
3868
+ const dimensions = useTerminalDimensions();
3869
+ const [height, setHeight] = createSignal(clampHeightPercent(props.current.heightPercent));
3870
+ const popMode = props.api.mode.push("openshare.dialog");
3871
+ const commandPrefix = `openshare.size.${Math.random().toString(36).slice(2)}`;
3872
+ const cycleWidth = () => {
3873
+ const next = kvWidthOf(props.api.kv) === "medium" ? "large" : kvWidthOf(props.api.kv) === "large" ? "xlarge" : "medium";
3874
+ props.api.kv.set(UI_WIDTH_KV, next);
3875
+ };
3876
+ const setPreset = (label) => {
3877
+ const preset = HEIGHT_PRESETS.find((p) => p.label === label);
3878
+ if (preset)
3879
+ setHeight(preset.value);
3880
+ };
3881
+ const move = (delta) => setHeight((current) => clampHeightPercent(current + delta));
3882
+ const save = () => {
3883
+ props.api.kv.set(UI_HEIGHT_PERCENT_KV, height());
3884
+ props.onDone({
3885
+ width: kvWidthOf(props.api.kv),
3886
+ heightPercent: height()
3887
+ });
3888
+ };
3889
+ const sliderWidth = () => kvWidthOf(props.api.kv) === "xlarge" ? 64 : kvWidthOf(props.api.kv) === "large" ? 48 : 34;
3890
+ const sliderCells = createMemo(() => {
3891
+ const width = sliderWidth();
3892
+ const selected = Math.round((height() - HEIGHT_PERCENT_MIN) / (HEIGHT_PERCENT_MAX - HEIGHT_PERCENT_MIN) * (width - 1));
3893
+ const presetPositions = new Map(HEIGHT_PRESETS.map((preset) => [Math.round((preset.value - HEIGHT_PERCENT_MIN) / (HEIGHT_PERCENT_MAX - HEIGHT_PERCENT_MIN) * (width - 1)), preset.label]));
3894
+ return Array.from({
3895
+ length: width
3896
+ }, (_, index) => {
3897
+ const isCurrent = index === selected;
3898
+ const preset = presetPositions.get(index);
3899
+ return {
3900
+ char: isCurrent ? "\u25CF" : preset ? "\u2502" : index < selected ? "\u2501" : "\u2500",
3901
+ color: isCurrent ? theme().primary : preset ? theme().accent : index < selected ? theme().success : theme().textMuted
3902
+ };
3903
+ });
3904
+ });
3905
+ const preview = createMemo(() => {
3906
+ const widthColumns = kvWidthOf(props.api.kv) === "xlarge" ? 116 : kvWidthOf(props.api.kv) === "large" ? 88 : 60;
3907
+ const previewWidth = Math.max(10, Math.min(sliderWidth() + 4, 72));
3908
+ const scale = previewWidth / widthColumns;
3909
+ const metrics = dialogMetrics(props.api, dimensions().height, 6, 4);
3910
+ const effective = metrics.targetRows;
3911
+ const requested = Math.floor(dimensions().height * Math.min(100, Math.max(25, height())) / 100);
3912
+ const rows = Math.max(4, Math.round(effective * scale));
3913
+ const fill = (text, width) => {
3914
+ const inner = width - 2;
3915
+ const slice = text.length > inner ? text.slice(0, inner - 1) + "\u2026" : text;
3916
+ return `\u2502${slice}${" ".repeat(Math.max(0, inner - slice.length))}\u2502`;
3917
+ };
3918
+ const lines = [];
3919
+ lines.push(`\u250C${"\u2500".repeat(previewWidth - 2)}\u2510`);
3920
+ lines.push(fill(`OpenShare (preview)${effective < requested ? " [capped]" : ""}`, previewWidth));
3921
+ for (let index = 0;index < rows - 3; index++)
3922
+ lines.push(fill("", previewWidth));
3923
+ lines.push(`\u2514${"\u2500".repeat(previewWidth - 2)}\u2518`);
3924
+ return lines;
3925
+ });
3926
+ const unregister = props.api.keymap.registerLayer({
3927
+ priority: 1e4,
3928
+ commands: [{
3929
+ name: `${commandPrefix}.width`,
3930
+ title: "Cycle width",
3931
+ run: (ctx) => {
3932
+ blockKey(ctx);
3933
+ cycleWidth();
3934
+ }
3935
+ }, {
3936
+ name: `${commandPrefix}.left`,
3937
+ title: "Lower height",
3938
+ run: (ctx) => {
3939
+ blockKey(ctx);
3940
+ move(-1);
3941
+ }
3942
+ }, {
3943
+ name: `${commandPrefix}.right`,
3944
+ title: "Raise height",
3945
+ run: (ctx) => {
3946
+ blockKey(ctx);
3947
+ move(1);
3948
+ }
3949
+ }, {
3950
+ name: `${commandPrefix}.down`,
3951
+ title: "Lower height faster",
3952
+ run: (ctx) => {
3953
+ blockKey(ctx);
3954
+ move(-5);
3955
+ }
3956
+ }, {
3957
+ name: `${commandPrefix}.up`,
3958
+ title: "Raise height faster",
3959
+ run: (ctx) => {
3960
+ blockKey(ctx);
3961
+ move(5);
3962
+ }
3963
+ }, ...HEIGHT_PRESETS.map((preset) => ({
3964
+ name: `${commandPrefix}.preset_${preset.label}`,
3965
+ title: `${preset.label} preset`,
3966
+ run: (ctx) => {
3967
+ blockKey(ctx);
3968
+ setPreset(preset.label);
3969
+ }
3970
+ })), {
3971
+ name: `${commandPrefix}.custom`,
3972
+ title: "Custom percent",
3973
+ run: (ctx) => {
3974
+ blockKey(ctx);
3975
+ props.onDone({
3976
+ width: kvWidthOf(props.api.kv),
3977
+ heightPercent: -1
3978
+ });
3979
+ }
3980
+ }, {
3981
+ name: `${commandPrefix}.save`,
3982
+ title: "Save",
3983
+ run: (ctx) => {
3984
+ blockKey(ctx);
3985
+ save();
3986
+ }
3987
+ }, {
3988
+ name: `${commandPrefix}.back`,
3989
+ title: "Back",
3990
+ run: (ctx) => {
3991
+ blockKey(ctx);
3992
+ props.onDone(undefined);
3993
+ }
3994
+ }, {
3995
+ name: `${commandPrefix}.shield`,
3996
+ title: "Block background input",
3997
+ run: blockKey
3998
+ }],
3999
+ bindings: [{
4000
+ key: "w",
4001
+ cmd: `${commandPrefix}.width`,
4002
+ desc: "Cycle width"
4003
+ }, {
4004
+ key: "left",
4005
+ cmd: `${commandPrefix}.left`,
4006
+ desc: "Lower height"
4007
+ }, {
4008
+ key: "right",
4009
+ cmd: `${commandPrefix}.right`,
4010
+ desc: "Raise height"
4011
+ }, {
4012
+ key: "down",
4013
+ cmd: `${commandPrefix}.down`,
4014
+ desc: "Lower height faster"
4015
+ }, {
4016
+ key: "up",
4017
+ cmd: `${commandPrefix}.up`,
4018
+ desc: "Raise height faster"
4019
+ }, ...HEIGHT_PRESETS.map((preset) => ({
4020
+ key: preset.key,
4021
+ cmd: `${commandPrefix}.preset_${preset.label}`,
4022
+ desc: `${preset.label} preset`
4023
+ })), {
4024
+ key: "c",
4025
+ cmd: `${commandPrefix}.custom`,
4026
+ desc: "Custom percent"
4027
+ }, {
4028
+ key: "enter",
4029
+ cmd: `${commandPrefix}.save`,
4030
+ desc: "Save"
4031
+ }, {
4032
+ key: "escape",
4033
+ cmd: `${commandPrefix}.back`,
4034
+ desc: "Back"
4035
+ }, ...shieldBindings(`${commandPrefix}.shield`, ["w", "c", ...HEIGHT_PRESETS.map((preset) => preset.key)])]
4036
+ });
4037
+ onCleanup(() => {
4038
+ unregister();
4039
+ popMode();
4040
+ });
4041
+ return _$createComponent(DialogFrame, {
4042
+ get api() {
4043
+ return props.api;
4044
+ },
4045
+ title: "Menu size",
4046
+ get children() {
4047
+ return [(() => {
4048
+ var _el$50 = _$createElement2("box"), _el$51 = _$createElement2("text"), _el$52 = _$createTextNode(`width (w): `), _el$53 = _$createTextNode(` (`), _el$54 = _$createTextNode(` cols)`), _el$55 = _$createElement2("text"), _el$56 = _$createElement2("b"), _el$57 = _$createTextNode(`%`);
4049
+ _$insertNode2(_el$50, _el$51);
4050
+ _$insertNode2(_el$50, _el$55);
4051
+ _$setProp2(_el$50, "flexDirection", "row");
4052
+ _$setProp2(_el$50, "justifyContent", "space-between");
4053
+ _$setProp2(_el$50, "width", "100%");
4054
+ _$setProp2(_el$50, "marginBottom", 1);
4055
+ _$setProp2(_el$50, "paddingLeft", 1);
4056
+ _$setProp2(_el$50, "paddingRight", 1);
4057
+ _$insertNode2(_el$51, _el$52);
4058
+ _$insertNode2(_el$51, _el$53);
4059
+ _$insertNode2(_el$51, _el$54);
4060
+ _$insert2(_el$51, () => kvWidthOf(props.api.kv), _el$53);
4061
+ _$insert2(_el$51, (() => {
4062
+ var _c$2 = _$memo(() => kvWidthOf(props.api.kv) === "xlarge");
4063
+ return () => _c$2() ? 116 : kvWidthOf(props.api.kv) === "large" ? 88 : 60;
4064
+ })(), _el$54);
4065
+ _$insertNode2(_el$55, _el$56);
4066
+ _$insertNode2(_el$56, _el$57);
4067
+ _$insert2(_el$56, height, _el$57);
4068
+ _$effect2((_p$) => {
4069
+ var _v$17 = theme().textMuted, _v$18 = theme().accent;
4070
+ _v$17 !== _p$.e && (_p$.e = _$setProp2(_el$51, "fg", _v$17, _p$.e));
4071
+ _v$18 !== _p$.t && (_p$.t = _$setProp2(_el$55, "fg", _v$18, _p$.t));
4072
+ return _p$;
4073
+ }, {
4074
+ e: undefined,
4075
+ t: undefined
4076
+ });
4077
+ return _el$50;
4078
+ })(), (() => {
4079
+ var _el$58 = _$createElement2("box"), _el$59 = _$createElement2("text"), _el$60 = _$createTextNode(`% `), _el$61 = _$createElement2("text"), _el$62 = _$createTextNode(` `), _el$63 = _$createTextNode(`%`);
4080
+ _$insertNode2(_el$58, _el$59);
4081
+ _$insertNode2(_el$58, _el$61);
4082
+ _$setProp2(_el$58, "flexDirection", "row");
4083
+ _$setProp2(_el$58, "width", "100%");
4084
+ _$setProp2(_el$58, "marginBottom", 1);
4085
+ _$setProp2(_el$58, "paddingLeft", 1);
4086
+ _$setProp2(_el$58, "paddingRight", 1);
4087
+ _$insertNode2(_el$59, _el$60);
4088
+ _$insert2(_el$59, HEIGHT_PERCENT_MIN, _el$60);
4089
+ _$insert2(_el$58, _$createComponent(For, {
4090
+ get each() {
4091
+ return sliderCells();
4092
+ },
4093
+ children: (cell) => (() => {
4094
+ var _el$66 = _$createElement2("text");
4095
+ _$insert2(_el$66, () => cell.char);
4096
+ _$effect2((_$p) => _$setProp2(_el$66, "fg", cell.color, _$p));
4097
+ return _el$66;
4098
+ })()
4099
+ }), _el$61);
4100
+ _$insertNode2(_el$61, _el$62);
4101
+ _$insertNode2(_el$61, _el$63);
4102
+ _$insert2(_el$61, HEIGHT_PERCENT_MAX, _el$63);
4103
+ _$effect2((_p$) => {
4104
+ var _v$19 = theme().textMuted, _v$20 = theme().textMuted;
4105
+ _v$19 !== _p$.e && (_p$.e = _$setProp2(_el$59, "fg", _v$19, _p$.e));
4106
+ _v$20 !== _p$.t && (_p$.t = _$setProp2(_el$61, "fg", _v$20, _p$.t));
4107
+ return _p$;
4108
+ }, {
4109
+ e: undefined,
4110
+ t: undefined
4111
+ });
4112
+ return _el$58;
4113
+ })(), (() => {
4114
+ var _el$64 = _$createElement2("box");
4115
+ _$setProp2(_el$64, "flexDirection", "column");
4116
+ _$setProp2(_el$64, "gap", 0);
4117
+ _$setProp2(_el$64, "marginBottom", 1);
4118
+ _$setProp2(_el$64, "paddingLeft", 1);
4119
+ _$setProp2(_el$64, "paddingRight", 1);
4120
+ _$insert2(_el$64, _$createComponent(For, {
4121
+ each: HEIGHT_PRESETS,
4122
+ children: (preset) => (() => {
4123
+ var _el$67 = _$createElement2("text"), _el$68 = _$createTextNode(` `), _el$69 = _$createTextNode(`: `), _el$70 = _$createTextNode(`%`);
4124
+ _$insertNode2(_el$67, _el$68);
4125
+ _$insertNode2(_el$67, _el$69);
4126
+ _$insertNode2(_el$67, _el$70);
4127
+ _$insert2(_el$67, () => preset.key, _el$68);
4128
+ _$insert2(_el$67, () => preset.label, _el$69);
4129
+ _$insert2(_el$67, () => preset.value, _el$70);
4130
+ _$insert2(_el$67, () => height() === preset.value ? " \u25CF current" : "", null);
4131
+ _$effect2((_$p) => _$setProp2(_el$67, "fg", height() === preset.value ? theme().accent : theme().textMuted, _$p));
4132
+ return _el$67;
4133
+ })()
4134
+ }));
4135
+ return _el$64;
4136
+ })(), (() => {
4137
+ var _el$65 = _$createElement2("box");
4138
+ _$setProp2(_el$65, "flexDirection", "column");
4139
+ _$setProp2(_el$65, "paddingLeft", 1);
4140
+ _$setProp2(_el$65, "paddingRight", 1);
4141
+ _$setProp2(_el$65, "marginBottom", 1);
4142
+ _$insert2(_el$65, _$createComponent(For, {
4143
+ get each() {
4144
+ return preview();
4145
+ },
4146
+ children: (line) => (() => {
4147
+ var _el$71 = _$createElement2("text");
4148
+ _$insert2(_el$71, line);
4149
+ _$effect2((_$p) => _$setProp2(_el$71, "fg", theme().textMuted, _$p));
4150
+ return _el$71;
4151
+ })()
4152
+ }));
4153
+ return _el$65;
4154
+ })(), _$createComponent(KeyLegend, {
4155
+ get api() {
4156
+ return props.api;
4157
+ },
4158
+ keys: [["w", "width"], ["\u2190\u2192", "\xB11%"], ["\u2191\u2193", "\xB15%"], ["c", "custom"], ["\u23CE", "save"], ["esc", "back"]],
4159
+ onBack: () => props.onDone(undefined)
4160
+ })];
4161
+ }
4162
+ });
4163
+ }
4164
+
4165
+ // src/v2-tui.ts
4166
+ import { createSignal as createSignal2 } from "solid-js";
4167
+ function pickThemeToken(source, ...path) {
4168
+ let current = source;
4169
+ for (const key of path) {
4170
+ if (!current || typeof current !== "object")
4171
+ return;
4172
+ current = current[key];
4173
+ }
4174
+ return current;
4175
+ }
4176
+ function buildV2Theme(context) {
4177
+ return {
4178
+ get current() {
4179
+ const theme = context.theme;
4180
+ const dark = context.themeMode === "dark";
4181
+ const text = pickThemeToken(theme, "text", "default");
4182
+ return {
4183
+ text,
4184
+ textMuted: pickThemeToken(theme, "text", "subdued") ?? text,
4185
+ background: pickThemeToken(theme, "background", "default"),
4186
+ backgroundPanel: pickThemeToken(theme, "background", "surface", "offset") ?? pickThemeToken(theme, "background", "default"),
4187
+ primary: pickThemeToken(theme, "text", "action", "primary", "selected") ?? text,
4188
+ secondary: pickThemeToken(theme, "hue", dark ? "interactive" : "neutral", dark ? 300 : 700) ?? text,
4189
+ accent: pickThemeToken(theme, "hue", "accent", dark ? 200 : 800) ?? text,
4190
+ success: pickThemeToken(theme, "text", "feedback", "success", "default") ?? text,
4191
+ warning: pickThemeToken(theme, "text", "feedback", "warning", "default") ?? text,
4192
+ error: pickThemeToken(theme, "text", "feedback", "error", "default") ?? text,
4193
+ info: pickThemeToken(theme, "text", "feedback", "info", "default") ?? text
4194
+ };
4195
+ }
4196
+ };
4197
+ }
4198
+ function convertKeymapLayer(layer) {
4199
+ const bindingsByName = new Map;
4200
+ for (const binding of layer.bindings ?? []) {
4201
+ if (!binding?.cmd)
4202
+ continue;
4203
+ const keys = bindingsByName.get(binding.cmd) ?? [];
4204
+ keys.push(binding.key);
4205
+ bindingsByName.set(binding.cmd, keys);
4206
+ }
4207
+ const commands = (layer.commands ?? []).map((command) => ({
4208
+ id: command.name,
4209
+ title: command.title ?? command.name,
4210
+ description: command.desc,
4211
+ group: command.group ?? command.category,
4212
+ palette: true,
4213
+ ...command.slashName ? { slash: { name: command.slashName, ...command.slashAliases?.length ? { aliases: command.slashAliases } : {} } } : {},
4214
+ ...command.suggested !== undefined ? { suggested: command.suggested } : {},
4215
+ ...command.enabled !== undefined ? { enabled: command.enabled } : {},
4216
+ ...bindingsByName.get(command.name)?.length ? { bind: bindingsByName.get(command.name).join(",") } : {},
4217
+ run: (_input, event) => command.run({
4218
+ event: event ? {
4219
+ preventDefault: () => event.preventDefault?.(),
4220
+ stopPropagation: () => event.stopPropagation?.()
4221
+ } : undefined
4222
+ })
4223
+ }));
4224
+ return { mode: layer.mode ?? "global", priority: layer.priority, commands };
4225
+ }
4226
+ function buildV2HostApi(context) {
4227
+ const disposeFns = [];
4228
+ const [kvStore, kvMutate] = context.storage.store("kv", { initial: {} });
4229
+ const kvMirror = {};
4230
+ for (const [key, value] of Object.entries(kvStore))
4231
+ kvMirror[key] = value;
4232
+ const kv = {
4233
+ get(key, fallback) {
4234
+ const value = key in kvMirror ? kvMirror[key] : kvStore[key];
4235
+ return value === undefined ? fallback : value;
4236
+ },
4237
+ set(key, value) {
4238
+ kvMirror[key] = value;
4239
+ kvMutate((draft) => {
4240
+ draft[key] = value;
4241
+ }).catch(() => {
4242
+ return;
4243
+ });
4244
+ }
4245
+ };
4246
+ const dialog = context.ui.dialog;
4247
+ let nestedOnClose;
4248
+ const settleNested = (dismissed) => {
4249
+ const onClose = nestedOnClose;
4250
+ nestedOnClose = undefined;
4251
+ if (dismissed)
4252
+ onClose?.();
4253
+ };
4254
+ return {
4255
+ hostVersion: 2,
4256
+ app: { version: context.app?.version },
4257
+ client: context.client,
4258
+ kv,
4259
+ lifecycle: {
4260
+ onDispose(fn) {
4261
+ disposeFns.push(fn);
4262
+ }
4263
+ },
4264
+ theme: buildV2Theme(context),
4265
+ ui: {
4266
+ toast: (input) => {
4267
+ try {
4268
+ context.ui.toast.show({ title: input.title, message: input.message, variant: input.variant, duration: input.duration });
4269
+ } catch {}
4270
+ },
4271
+ dialog: {
4272
+ replace(renderer, onClose) {
4273
+ nestedOnClose = undefined;
4274
+ let content;
4275
+ try {
4276
+ content = typeof renderer === "function" ? renderer() : renderer;
4277
+ } catch {
4278
+ content = undefined;
4279
+ }
4280
+ if (content === undefined || content === null) {
4281
+ nestedOnClose = onClose;
4282
+ return;
4283
+ }
4284
+ dialog.show(() => content, onClose);
4285
+ },
4286
+ clear() {
4287
+ nestedOnClose = undefined;
4288
+ dialog.clear();
4289
+ },
4290
+ setSize(size) {
4291
+ try {
4292
+ dialog.set({ size });
4293
+ } catch {}
4294
+ }
4295
+ },
4296
+ async showMenu(props) {
4297
+ const value = await dialog.select({
4298
+ title: props.title,
4299
+ placeholder: props.hint,
4300
+ current: props.current,
4301
+ options: props.options.map((option) => ({
4302
+ title: option.title,
4303
+ value: option.value,
4304
+ description: option.description,
4305
+ category: option.category,
4306
+ disabled: option.disabled
4307
+ }))
4308
+ });
4309
+ return value;
4310
+ },
4311
+ async showConfirm(props) {
4312
+ return await dialog.confirm({ title: props.title, message: props.message, label: { confirm: props.confirmLabel } }) === true;
4313
+ },
4314
+ async showAlert(props) {
4315
+ await dialog.alert({ title: props.title, message: props.message });
4316
+ },
4317
+ async showPrompt(props) {
4318
+ return await dialog.prompt({ title: props.title, placeholder: props.placeholder, value: props.value });
4319
+ },
4320
+ DialogSelect(props) {
4321
+ dialog.select({
4322
+ title: props.title,
4323
+ placeholder: props.placeholder,
4324
+ current: props.current,
4325
+ options: props.options.map((option) => ({
4326
+ title: option.title,
4327
+ value: option.value,
4328
+ description: option.description,
4329
+ category: option.category,
4330
+ disabled: option.disabled
4331
+ }))
4332
+ }).then((value) => {
4333
+ if (value === undefined) {
4334
+ settleNested(true);
4335
+ return;
4336
+ }
4337
+ settleNested(false);
4338
+ const option = props.options.find((item) => item.value === value);
4339
+ if (option)
4340
+ props.onSelect(option);
4341
+ }).catch(() => settleNested(true));
4342
+ },
4343
+ DialogPrompt(props) {
4344
+ dialog.prompt({ title: props.title, placeholder: props.placeholder, value: props.value }).then((value) => {
4345
+ settleNested(value === undefined);
4346
+ if (value === undefined)
4347
+ props.onCancel?.();
4348
+ else
4349
+ props.onConfirm(value);
4350
+ }).catch(() => {
4351
+ settleNested(true);
4352
+ props.onCancel?.();
4353
+ });
4354
+ },
4355
+ DialogConfirm(props) {
4356
+ dialog.confirm({ title: props.title, message: props.message, label: { confirm: props.confirmLabel } }).then((result) => {
4357
+ settleNested(result !== true);
4358
+ if (result === true)
4359
+ props.onConfirm();
4360
+ else
4361
+ props.onCancel?.();
4362
+ }).catch(() => {
4363
+ settleNested(true);
4364
+ props.onCancel?.();
4365
+ });
4366
+ },
4367
+ DialogAlert(props) {
4368
+ dialog.alert({ title: props.title, message: props.message }).then(() => {
4369
+ settleNested(false);
4370
+ props.onConfirm?.();
4371
+ }).catch(() => {
4372
+ settleNested(true);
4373
+ props.onConfirm?.();
4374
+ });
4375
+ }
4376
+ },
4377
+ keymap: {
4378
+ registerLayer(layer) {
4379
+ const [active, setActive] = createSignal2(true);
4380
+ try {
4381
+ context.keymap.layer(() => active() ? convertKeymapLayer(layer) : { mode: "global", enabled: false, commands: [], bindings: [] });
4382
+ } catch {
4383
+ return () => {
4384
+ return;
4385
+ };
4386
+ }
4387
+ return () => {
4388
+ setActive(false);
4389
+ };
4390
+ }
4391
+ },
4392
+ sessionInfo: () => {
4393
+ return;
4394
+ },
4395
+ currentSessionID: undefined,
4396
+ clipboard: {
4397
+ async write(text) {
4398
+ let ok = false;
4399
+ try {
4400
+ const clip = navigator.clipboard;
4401
+ if (typeof clip?.writeText === "function") {
4402
+ await clip.writeText(text).then(() => {
4403
+ ok = true;
4404
+ }, () => {});
4405
+ }
4406
+ } catch {}
4407
+ try {
4408
+ process.stdout.write(`\x1B]52;c;${Buffer.from(text, "utf8").toString("base64")}\x07`);
4409
+ ok = true;
4410
+ } catch {}
4411
+ return ok;
4412
+ }
4413
+ }
4414
+ };
4415
+ }
4416
+ function createV2TuiSetup() {
4417
+ return async (context) => {
4418
+ const host = buildV2HostApi(context);
4419
+ const sessionID = () => host.currentSessionID?.() ?? undefined;
4420
+ const unregister = registerCommands(host, sessionID);
4421
+ return () => {
4422
+ try {
4423
+ unregister();
4424
+ } catch {}
4425
+ };
4426
+ };
4427
+ }
4428
+
4429
+ // src/tui.tsx
4430
+ async function showSizePickerFlow(api, current) {
4431
+ let heightPercent = clampHeightPercent(current.heightPercent);
4432
+ while (true) {
4433
+ const choice = await showSizePickerOnce(api, {
4434
+ width: kvWidthOf(api.kv),
4435
+ heightPercent
4436
+ });
4437
+ if (!choice)
4438
+ return;
4439
+ if (choice.heightPercent >= 0)
4440
+ return {
4441
+ width: kvWidthOf(api.kv),
4442
+ heightPercent: clampHeightPercent(choice.heightPercent)
4443
+ };
4444
+ const input = await showPromptOnce(api, {
4445
+ title: "Menu height percent",
4446
+ placeholder: "25-100",
4447
+ value: String(heightPercent)
4448
+ });
4449
+ if (input === undefined)
4450
+ continue;
4451
+ const parsed = Number(input);
4452
+ if (!Number.isFinite(parsed)) {
4453
+ await showAlertMenu(api, {
4454
+ title: "Invalid height",
4455
+ message: "Enter a number from 25 to 100."
4456
+ });
4457
+ continue;
4458
+ }
4459
+ heightPercent = clampHeightPercent(parsed);
4460
+ }
4461
+ }
4462
+ function buildV1HostApi(api) {
4463
+ return {
4464
+ hostVersion: 1,
4465
+ app: {
4466
+ version: api.app?.version
4467
+ },
4468
+ client: (() => {
4469
+ const raw = api.client;
4470
+ const rawSession = raw?.session;
4471
+ if (!rawSession)
4472
+ return raw;
4473
+ const translateArgs = (args) => {
4474
+ if (!args || typeof args !== "object")
4475
+ return args;
4476
+ const a = args;
4477
+ if (a.path && "id" in a.path && !("sessionID" in a.path)) {
4478
+ const {
4479
+ id,
4480
+ ...rest
4481
+ } = a.path;
4482
+ return {
4483
+ ...a,
4484
+ path: {
4485
+ ...rest,
4486
+ sessionID: id
4487
+ }
4488
+ };
4489
+ }
4490
+ return args;
4491
+ };
4492
+ let argShape;
4493
+ const callV2 = async (method, args) => {
4494
+ const orig = rawSession[method];
4495
+ if (!orig)
4496
+ return;
4497
+ const a = args;
4498
+ const flat = {
4499
+ sessionID: a.path?.id ?? a.path?.sessionID,
4500
+ ...a.body ?? {}
4501
+ };
4502
+ const attempts = argShape === "flat" ? [["flat", flat]] : argShape === "path" ? [["path", translateArgs(args)]] : [["path", translateArgs(args)], ["flat", flat]];
4503
+ let last;
4504
+ for (const [shape, callArgs] of attempts) {
4505
+ try {
4506
+ last = await orig.call(rawSession, callArgs);
4507
+ } catch (e) {
4508
+ last = {
4509
+ error: e
4510
+ };
4511
+ }
4512
+ const failed = !last || typeof last === "object" && "error" in last && last.error;
4513
+ if (!failed) {
4514
+ if (!argShape) {
4515
+ argShape = shape;
4516
+ dbgLog(`client.${method} arg shape = ${shape}`);
4517
+ }
4518
+ return last;
4519
+ }
4520
+ }
4521
+ return last;
4522
+ };
4523
+ const adaptedGet = async (args) => {
4524
+ const res = await callV2("get", args);
4525
+ const info = (res && typeof res === "object" && "data" in res ? res.data : res) ?? undefined;
4526
+ if (info && typeof info === "object" && !info.metadata) {
4527
+ const stateInfo = api.state.session.get(args.path.id);
4528
+ if (stateInfo?.metadata) {
4529
+ dbgLog(`client.get metadata fallback: client keys=${Object.keys(info).slice(0, 12).join("|")} -> state metadata used`);
4530
+ if (res && typeof res === "object" && "data" in res)
4531
+ return {
4532
+ ...res,
4533
+ data: {
4534
+ ...info,
4535
+ metadata: stateInfo.metadata
4536
+ }
4537
+ };
4538
+ return {
4539
+ ...info,
4540
+ metadata: stateInfo.metadata
4541
+ };
4542
+ }
4543
+ }
4544
+ return res;
4545
+ };
4546
+ const adaptedSession = new Proxy(rawSession, {
4547
+ get(target, key) {
4548
+ if (key === "get")
4549
+ return adaptedGet;
4550
+ if (key === "update")
4551
+ return (args) => callV2("update", args);
4552
+ const value = Reflect.get(target, key, target);
4553
+ return typeof value === "function" ? value.bind(target) : value;
4554
+ }
4555
+ });
4556
+ return new Proxy(raw, {
4557
+ get(target, key) {
4558
+ if (key === "session")
4559
+ return adaptedSession;
4560
+ const value = Reflect.get(target, key, target);
4561
+ return typeof value === "function" ? value.bind(target) : value;
4562
+ }
4563
+ });
4564
+ })(),
4565
+ kv: api.kv,
4566
+ lifecycle: api.lifecycle,
4567
+ theme: api.theme,
4568
+ ui: {
4569
+ toast: (input) => api.ui.toast(input),
4570
+ dialog: new Proxy({}, {
4571
+ get: (_t, key) => {
4572
+ if (key === "clear")
4573
+ return () => {
4574
+ try {
4575
+ api.ui.dialog.clear();
4576
+ } catch {}
4577
+ };
4578
+ if (key === "replace")
4579
+ return (render, onClose) => {
4580
+ try {
4581
+ api.ui.dialog.replace(render, onClose);
4582
+ } catch (e) {
4583
+ dbgLog(`dialog.replace THREW: ${e?.message ?? e}`);
4584
+ }
4585
+ };
4586
+ const value = api.ui.dialog[key];
4587
+ return typeof value === "function" ? value.bind(api.ui.dialog) : value;
4588
+ }
4589
+ }),
4590
+ showMenu: (props) => showMenuOnce(api, {
4591
+ ...props,
4592
+ options: toMenuOptions(props.options)
4593
+ }),
4594
+ showConfirm: (props) => showConfirmMenu(api, props),
4595
+ showAlert: (props) => showAlertMenu(api, props),
4596
+ showPrompt: (props) => showPromptOnce(api, props),
4597
+ showSizePicker: (current) => showSizePickerFlow(api, current),
4598
+ showBusy: (title) => {
4599
+ const theme = () => api.theme.current;
4600
+ api.ui.dialog.replace(() => (() => {
4601
+ var _el$ = _$createElement3("box"), _el$2 = _$createElement3("text"), _el$3 = _$createElement3("b"), _el$4 = _$createElement3("text");
4602
+ _$insertNode3(_el$, _el$2);
4603
+ _$insertNode3(_el$, _el$4);
4604
+ _$setProp3(_el$, "flexDirection", "column");
4605
+ _$setProp3(_el$, "width", "100%");
4606
+ _$setProp3(_el$, "paddingLeft", 2);
4607
+ _$setProp3(_el$, "paddingRight", 2);
4608
+ _$setProp3(_el$, "paddingTop", 1);
4609
+ _$setProp3(_el$, "paddingBottom", 1);
4610
+ _$insertNode3(_el$2, _el$3);
4611
+ _$insert3(_el$3, title);
4612
+ _$insertNode3(_el$4, _$createTextNode2(`working\u2026 (esc does not cancel)`));
4613
+ _$effect3((_p$) => {
4614
+ var _v$ = theme().accent, _v$2 = theme().textMuted;
4615
+ _v$ !== _p$.e && (_p$.e = _$setProp3(_el$2, "fg", _v$, _p$.e));
4616
+ _v$2 !== _p$.t && (_p$.t = _$setProp3(_el$4, "fg", _v$2, _p$.t));
4617
+ return _p$;
4618
+ }, {
4619
+ e: undefined,
4620
+ t: undefined
4621
+ });
4622
+ return _el$;
4623
+ })());
4624
+ return () => {
4625
+ try {
4626
+ api.ui.dialog.clear();
4627
+ } catch {}
4628
+ };
4629
+ },
4630
+ DialogSelect: (props) => {
4631
+ showMenuOnce(api, {
4632
+ title: props.title,
4633
+ options: toMenuOptions(props.options),
4634
+ current: props.current,
4635
+ hint: props.placeholder
4636
+ }).then((value) => {
4637
+ if (value === undefined)
4638
+ props.onCancel?.();
4639
+ else {
4640
+ const option = props.options.find((o) => o.value === value);
4641
+ if (option)
4642
+ props.onSelect(option);
4643
+ }
4644
+ });
4645
+ return;
4646
+ },
4647
+ DialogPrompt: (props) => {
4648
+ showPromptOnce(api, {
4649
+ title: props.title,
4650
+ placeholder: props.placeholder,
4651
+ value: props.value
4652
+ }).then((value) => {
4653
+ if (value === undefined)
4654
+ props.onCancel?.();
4655
+ else
4656
+ props.onConfirm(value);
4657
+ });
4658
+ return;
4659
+ },
4660
+ DialogConfirm: (props) => {
4661
+ showConfirmMenu(api, {
4662
+ title: props.title,
4663
+ message: props.message,
4664
+ confirmLabel: props.confirmLabel
4665
+ }).then((yes) => yes ? props.onConfirm() : props.onCancel?.());
4666
+ return;
4667
+ },
4668
+ DialogAlert: (props) => {
4669
+ showAlertMenu(api, {
4670
+ title: props.title,
4671
+ message: props.message
4672
+ }).then(() => props.onConfirm?.());
4673
+ return;
4674
+ }
4675
+ },
4676
+ keymap: {
4677
+ registerLayer(layer) {
4678
+ const commands = layer.commands.map((command) => {
4679
+ const host = {
4680
+ namespace: "palette",
4681
+ name: command.name,
4682
+ title: command.title,
4683
+ run: (ctx) => command.run(ctx ?? {})
4684
+ };
4685
+ if (command.desc !== undefined)
4686
+ host.desc = command.desc;
4687
+ if (command.category !== undefined)
4688
+ host.category = command.category;
4689
+ if (command.slashName !== undefined)
4690
+ host.slashName = command.slashName;
4691
+ if (command.slashAliases?.length)
4692
+ host.slashAliases = command.slashAliases;
4693
+ if (command.suggested !== undefined)
4694
+ host.suggested = command.suggested;
4695
+ if (command.hidden !== undefined)
4696
+ host.hidden = command.hidden;
4697
+ if (command.enabled !== undefined)
4698
+ host.enabled = command.enabled;
4699
+ return host;
4700
+ });
4701
+ return api.keymap.registerLayer({
4702
+ commands,
4703
+ bindings: layer.bindings
4704
+ });
4705
+ }
4706
+ },
4707
+ sessionInfo: (sid) => api.state.session.get(sid),
4708
+ currentSessionID: () => {
4709
+ const route = api.route.current;
4710
+ return route.name === "session" ? route.params?.sessionID : undefined;
4711
+ },
4712
+ clipboard: {
4713
+ async write(text) {
4714
+ let ok = false;
4715
+ try {
4716
+ const clip = navigator.clipboard;
4717
+ if (typeof clip?.writeText === "function") {
4718
+ await clip.writeText(text).then(() => {
4719
+ ok = true;
4720
+ }, () => {});
4721
+ }
4722
+ } catch {}
4723
+ try {
4724
+ process.stdout.write(`\x1B]52;c;${Buffer.from(text, "utf8").toString("base64")}\x07`);
4725
+ ok = true;
4726
+ } catch {}
4727
+ return ok;
4728
+ }
4729
+ }
4730
+ };
4731
+ }
4732
+ var tui = async (api) => {
4733
+ dbgLog(`tui entry running; host v=${dbg(api.app?.version)} slots=${!!api.slots} keymap=${!!api.keymap} state=${!!api.state}`);
4734
+ const host = buildV1HostApi(api);
4735
+ const sessionID = () => host.currentSessionID?.() ?? undefined;
4736
+ const unregister = registerCommands(host, sessionID);
4737
+ try {
4738
+ const shareCommand = buildShareCommand(host, sessionID);
4739
+ const slot = createSidebarSlot(api, {
4740
+ onShareCommand: () => void shareCommand.run({})
4741
+ });
4742
+ dbgLog(`registering sidebar slot: ${Object.keys(slot?.slots ?? {}).join(", ") || "NO SLOTS"}`);
4743
+ const reg = api.slots.register(slot);
4744
+ dbgLog(`sidebar slot registered -> ${dbg(reg)}`);
4745
+ } catch (error) {
4746
+ dbgLog(`sidebar slot REGISTRATION THREW ${error?.stack ?? String(error)}`);
4747
+ }
4748
+ api.lifecycle.onDispose(() => {
4749
+ try {
4750
+ unregister();
4751
+ } catch {}
4752
+ });
4753
+ };
4754
+ var tui_default = {
4755
+ id: "openshare",
4756
+ tui,
4757
+ setup: createV2TuiSetup()
4758
+ };
4759
+ export {
4760
+ tui_default as default,
4761
+ buildV1HostApi
4762
+ };