@mirrowel/opencode-openshare 0.1.1 → 0.2.0-dev.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tui.js CHANGED
@@ -1,20 +1,37 @@
1
1
  // @bun
2
- var __esm = (fn, res, err) => () => {
3
- if (fn)
4
- try {
5
- res = fn(fn = 0);
6
- } catch (e) {
7
- err = [e];
8
- }
9
- if (err)
10
- throw err[0];
11
- return res;
12
- };
2
+ // src/tui.tsx
3
+ import { effect as _$effect3 } from "@opentui/solid";
4
+ import { createTextNode as _$createTextNode2 } from "@opentui/solid";
5
+ import { insertNode as _$insertNode3 } from "@opentui/solid";
6
+ import { insert as _$insert3 } from "@opentui/solid";
7
+ import { setProp as _$setProp3 } from "@opentui/solid";
8
+ import { createElement as _$createElement3 } from "@opentui/solid";
9
+
10
+ // src/share-flow.ts
11
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
13
12
 
14
13
  // src/config.ts
15
14
  import { existsSync, readFileSync, writeFileSync } from "fs";
16
15
  import { basename, dirname, join, relative } from "path";
17
16
  import { fileURLToPath } from "url";
17
+ var METADATA_KEY = "openshare";
18
+ var URL_FILE_NONE = "none";
19
+ var DEFAULTS = {
20
+ workerUrl: "",
21
+ createToken: "",
22
+ debounceMs: 1e4,
23
+ completedDelayMs: 5000,
24
+ modelLabel: "$Provider/$Model",
25
+ ttlDays: 90,
26
+ openTasksInNewTab: true,
27
+ authLogin: "",
28
+ authPassword: "",
29
+ spoolDir: "",
30
+ urlFile: "",
31
+ announceStderr: true
32
+ };
33
+ var CONFIG_NAME = "openshare.config.json";
34
+ var LOCAL_NAME = "openshare.local.json";
18
35
  function moduleDir() {
19
36
  return dirname(fileURLToPath(import.meta.pathname ?? import.meta.url));
20
37
  }
@@ -290,23 +307,6 @@ function globalConfigDir(env) {
290
307
  const home = process.env.USERPROFILE ?? process.env.HOME ?? "";
291
308
  return join(home, ".config", "opencode");
292
309
  }
293
- var METADATA_KEY = "openshare", URL_FILE_NONE = "none", DEFAULTS, CONFIG_NAME = "openshare.config.json", LOCAL_NAME = "openshare.local.json";
294
- var init_config = __esm(() => {
295
- DEFAULTS = {
296
- workerUrl: "",
297
- createToken: "",
298
- debounceMs: 1e4,
299
- completedDelayMs: 5000,
300
- modelLabel: "$Provider/$Model",
301
- ttlDays: 90,
302
- openTasksInNewTab: true,
303
- authLogin: "",
304
- authPassword: "",
305
- spoolDir: "",
306
- urlFile: "",
307
- announceStderr: true
308
- };
309
- });
310
310
 
311
311
  // src/sidebar.tsx
312
312
  import { effect as _$effect } from "@opentui/solid";
@@ -314,9 +314,6 @@ import { insertNode as _$insertNode } from "@opentui/solid";
314
314
  import { insert as _$insert } from "@opentui/solid";
315
315
  import { setProp as _$setProp } from "@opentui/solid";
316
316
  import { createElement as _$createElement } from "@opentui/solid";
317
- import { appendFileSync } from "fs";
318
- import { tmpdir } from "os";
319
- import { join as join2 } from "path";
320
317
  function dbg(v, depth = 0) {
321
318
  const t = typeof v;
322
319
  if (v === null)
@@ -340,31 +337,9 @@ function dbg(v, depth = 0) {
340
337
  return "obj:?}";
341
338
  }
342
339
  }
343
- function dbgEl(el, depth = 0) {
344
- if (el === null || el === undefined || typeof el === "string" || typeof el === "number" || typeof el === "boolean")
345
- return dbg(el);
346
- if (Array.isArray(el))
347
- return `[${el.map((x) => dbgEl(x, depth)).join(",")}]`;
348
- if (typeof el === "function")
349
- return "fn-node";
350
- if (depth >= 4)
351
- return "\u2026";
352
- try {
353
- const rec = el;
354
- const t = dbg(rec.t ?? rec.type, 2);
355
- const kids = rec.children;
356
- const props = Object.keys(rec).filter((k) => !["t", "type", "children"].includes(k));
357
- return `<${t} ${props.join(" ")}>${kids === undefined ? "" : dbgEl(kids, depth + 1)}</${t}>`;
358
- } catch (e) {
359
- return `el?-${dbg(e)}`;
360
- }
361
- }
362
- function dbgLog(line) {
363
- try {
364
- appendFileSync(DBG_FILE, `${new Date().toISOString()} ${line}
365
- `);
366
- } catch {}
367
- }
340
+ function dbgLog(_line) {}
341
+ var GONE_TTL_MS = 60000;
342
+ var goneCache = new Map;
368
343
  function goneVerdict(sid, server, shareId) {
369
344
  const key = `${sid}|${server}`;
370
345
  const cached = goneCache.get(key);
@@ -379,13 +354,10 @@ function goneVerdict(sid, server, shareId) {
379
354
  return cached;
380
355
  }
381
356
  function createSidebarSlot(api, opts) {
382
- dbgLog(`createSidebarSlot called; api.state=${!!api.state} api.theme=${!!api.theme} api.version=${dbg(api.app?.version)}`);
383
357
  return {
384
358
  slots: {
385
359
  sidebar_title: (ctx, props) => {
386
- const seq = ++renderSeq;
387
360
  try {
388
- dbgLog(`render#${seq} ctx=${dbg(ctx)} props=${dbg(props)}`);
389
361
  const sid = typeof props?.session_id === "string" ? props.session_id : "";
390
362
  const title = String(props?.title ?? "") || "Session";
391
363
  const session = sid ? api.state.session.get(sid) : undefined;
@@ -398,7 +370,6 @@ function createSidebarSlot(api, opts) {
398
370
  const anyGone = verdicts.some((v) => v?.status === "gone");
399
371
  const multi = liveCount > 1 ? `\xB7${liveCount} ` : "";
400
372
  const theme = ctx?.theme?.current ?? api.theme.current;
401
- 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)}`);
402
373
  const copyLink = () => {
403
374
  if (anyGone) {
404
375
  opts?.onShareCommand?.();
@@ -445,25 +416,25 @@ function createSidebarSlot(api, opts) {
445
416
  _$effect((_$p) => _$setProp(_el$2, "fg", theme.text, _$p));
446
417
  return _el$;
447
418
  })();
448
- dbgLog(`render#${seq} RETURNS ${dbgEl(el)}`);
449
419
  return el;
450
420
  } catch (error) {
451
- dbgLog(`render#${seq} THREW ${error?.stack ?? dbg(error)} \u2014 rethrowing`);
452
421
  throw error;
453
422
  }
454
423
  }
455
424
  }
456
425
  };
457
426
  }
458
- var DBG_FILE, renderSeq = 0, GONE_TTL_MS = 60000, goneCache;
459
- var init_sidebar = __esm(() => {
460
- init_share_flow();
461
- DBG_FILE = join2(tmpdir(), "openshare-tui-debug.log");
462
- goneCache = new Map;
463
- });
427
+
428
+ // src/binding-queue.ts
429
+ var queues = new Map;
430
+ function queueBindingWrite(sid, fn) {
431
+ const prev = queues.get(sid) ?? Promise.resolve();
432
+ const run = prev.then(fn, fn);
433
+ queues.set(sid, run.catch(() => {}));
434
+ return run;
435
+ }
464
436
 
465
437
  // src/share-flow.ts
466
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
467
438
  function currentConfig(baseDir = moduleDir()) {
468
439
  return loadConfig(baseDir);
469
440
  }
@@ -577,17 +548,19 @@ function hostOf(url) {
577
548
  }
578
549
  }
579
550
  async function patchSessionBinding(client, sid, mutator) {
580
- const info = await getSession(client, sid);
581
- if (!info)
582
- return false;
583
- const metadata = { ...info.metadata ?? {} };
584
- const raw = info.metadata?.[METADATA_KEY];
585
- const binding = raw && typeof raw === "object" ? { ...raw } : { v: 2, servers: {} };
586
- const servers = binding.servers && typeof binding.servers === "object" ? { ...binding.servers } : {};
587
- mutator(servers, binding);
588
- binding.servers = servers;
589
- metadata[METADATA_KEY] = binding;
590
- return patchMetadata(client, sid, metadata);
551
+ return queueBindingWrite(sid, async () => {
552
+ const info = await getSession(client, sid);
553
+ if (!info)
554
+ return false;
555
+ const metadata = { ...info.metadata ?? {} };
556
+ const raw = info.metadata?.[METADATA_KEY];
557
+ const binding = raw && typeof raw === "object" ? { ...raw } : { v: 2, servers: {} };
558
+ const servers = binding.servers && typeof binding.servers === "object" ? { ...binding.servers } : {};
559
+ mutator(servers, binding);
560
+ binding.servers = servers;
561
+ metadata[METADATA_KEY] = binding;
562
+ return patchMetadata(client, sid, metadata);
563
+ });
591
564
  }
592
565
  function bindingOf(info) {
593
566
  const all = bindingsOf(info);
@@ -818,6 +791,7 @@ function adminTokenFor(url, cfg = currentConfig()) {
818
791
  const entry = serverEntries(cfg).find((s) => s.url === base);
819
792
  return entry?.entry.adminToken ?? (cfg.workerUrl && normServerUrl(cfg.workerUrl) === base ? cfg.createToken : "") ?? "";
820
793
  }
794
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
821
795
  async function waitForBinding(client, sid, timeoutMs = 25000) {
822
796
  const start = Date.now();
823
797
  while (Date.now() - start < timeoutMs) {
@@ -828,7 +802,15 @@ async function waitForBinding(client, sid, timeoutMs = 25000) {
828
802
  }
829
803
  return;
830
804
  }
831
- async function runShareFlow(host, sessionID) {
805
+ async function requestShareConsent(host) {
806
+ return host.ui.showConfirm({
807
+ title: "Share Session",
808
+ message: `Publish this session (and its subagent sessions) to your openshare worker?
809
+ The link lets anyone with it read the conversation.`,
810
+ confirmLabel: "Share"
811
+ });
812
+ }
813
+ async function shareWithConsent(host, sessionID) {
832
814
  if (!sessionID)
833
815
  return { ok: false, message: "No active session to share." };
834
816
  const cfg = currentConfig();
@@ -841,9 +823,8 @@ async function runShareFlow(host, sessionID) {
841
823
  await host.clipboard.write(existing.url);
842
824
  return { ok: true, message: `Share URL copied to clipboard: ${existing.url}`, url: existing.url };
843
825
  }
844
- if (!host.kv.get(CONSENT_KV_KEY, false)) {
845
- return { ok: false, message: "consent required" };
846
- }
826
+ if (!await requestShareConsent(host))
827
+ return { ok: false, message: "Share cancelled." };
847
828
  return confirmShare(host, root);
848
829
  }
849
830
  async function confirmShare(host, rootSessionID) {
@@ -886,26 +867,6 @@ async function runUnshareFlow(host, sessionID) {
886
867
  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).`
887
868
  };
888
869
  }
889
- var CONSENT_KV_KEY = "openshare_consent", sleep = (ms) => new Promise((r) => setTimeout(r, ms));
890
- var init_share_flow = __esm(() => {
891
- init_config();
892
- init_sidebar();
893
- });
894
-
895
- // src/tui.tsx
896
- import { effect as _$effect3 } from "@opentui/solid";
897
- import { createTextNode as _$createTextNode2 } from "@opentui/solid";
898
- import { insertNode as _$insertNode3 } from "@opentui/solid";
899
- import { insert as _$insert3 } from "@opentui/solid";
900
- import { setProp as _$setProp3 } from "@opentui/solid";
901
- import { createElement as _$createElement3 } from "@opentui/solid";
902
-
903
- // src/commands.ts
904
- init_share_flow();
905
-
906
- // src/settings.ts
907
- init_config();
908
- init_share_flow();
909
870
 
910
871
  // src/ui-prefs.ts
911
872
  var UI_WIDTH_KV = "openshare.ui-width";
@@ -1806,33 +1767,93 @@ ${sourcesMessage(sources)}`
1806
1767
  const sessionRows = async () => {
1807
1768
  if (!startAtSession)
1808
1769
  return [];
1809
- const binding = await readBinding(host.client, await resolveRoot(host.client, startAtSession));
1810
- const rows = [
1811
- {
1812
- title: binding?.url ? "Copy share link" : "Share this session",
1770
+ const root = await resolveRoot(host.client, startAtSession);
1771
+ const bindings = await readBindings(host.client, root);
1772
+ const live = bindings.filter((b) => b.url && b.enabled !== false);
1773
+ const rows = [];
1774
+ if (live.length === 0) {
1775
+ rows.push({
1776
+ title: "Share this session",
1813
1777
  value: "session:share",
1814
- description: binding?.url ? "copy the share URL to the clipboard" : "Set the share flag; the server plugin creates the page",
1815
- help: binding?.url ? `Copies the share URL to the clipboard.
1778
+ description: "set the share flag; the server plugin creates the page",
1779
+ help: "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. You will be asked to confirm before anything is published."
1780
+ });
1781
+ } else if (live.length === 1) {
1782
+ const b = live[0];
1783
+ rows.push({
1784
+ title: "Copy share link",
1785
+ value: "session:copy",
1786
+ description: "copy the share URL to the clipboard",
1787
+ help: `Copies the share URL to the clipboard.
1788
+
1789
+ URL: ${b.url}
1790
+ Share id: ${b.id ?? "?"}`
1791
+ });
1792
+ } else {
1793
+ for (const b of live) {
1794
+ rows.push({
1795
+ title: `Copy link \u2014 ${hostOf(b.url || b.server)}`,
1796
+ value: `session:copy:${b.server}`,
1797
+ description: b.url,
1798
+ help: `Copies this server's share URL to the clipboard.
1816
1799
 
1817
- URL: ${binding.url}
1818
- 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."
1800
+ URL: ${b.url}
1801
+ Share id: ${b.id}
1802
+
1803
+ Each server carries an independent page of the same session.`
1804
+ });
1819
1805
  }
1820
- ];
1821
- if (binding?.url) {
1806
+ rows.push({
1807
+ title: "Copy all links",
1808
+ value: "session:copyall",
1809
+ description: `${live.length} servers`,
1810
+ help: `Copies every bound server's share URL.
1811
+
1812
+ ${live.map((b) => `\u2022 ${b.url}`).join(`
1813
+ `)}`
1814
+ });
1815
+ }
1816
+ if (live.length) {
1822
1817
  rows.push({
1823
1818
  title: "Stop sharing",
1824
1819
  value: "session:unshare",
1825
- description: "delete the page(s), or leave a snapshot",
1820
+ description: live.length > 1 ? `delete every page (${live.length} servers), or leave snapshots` : "delete the page, or leave a snapshot",
1826
1821
  danger: true,
1827
1822
  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).
1828
1823
 
1829
1824
  URLs:
1830
- ${[binding.url].join(`
1825
+ ${live.map((b) => b.url).join(`
1831
1826
  `)}
1832
1827
 
1833
1828
  Sharing again later always creates fresh page(s) with new URLs.`
1834
1829
  });
1830
+ if (live.length > 1) {
1831
+ rows.push({
1832
+ title: "Stop on one server\u2026",
1833
+ value: "session:unshare:one",
1834
+ description: "delete or snapshot a single server's page",
1835
+ help: `Per-server stop: pick the server, then delete its page or leave it as a read-only snapshot. Other servers are unaffected.
1836
+
1837
+ Bound servers:
1838
+ ${live.map((b) => `\u2022 ${b.server} \u2014 ${b.url}`).join(`
1839
+ `)}`
1840
+ });
1841
+ const otherServers = serverEntries().filter((s) => !live.some((b) => b.server === s.url));
1842
+ if (otherServers.length) {
1843
+ rows.push({
1844
+ title: "Share to another server\u2026",
1845
+ value: "session:more",
1846
+ description: otherServers.map((s) => hostOf(s.url)).join(", "),
1847
+ 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.
1848
+
1849
+ Available:
1850
+ ${otherServers.map((s) => `\u2022 ${s.url}`).join(`
1851
+ `)}`
1852
+ });
1853
+ }
1854
+ }
1835
1855
  }
1856
+ const binding = bindings[0];
1836
1857
  rows.push({
1837
1858
  title: "Session TTL override (days)",
1838
1859
  value: "session:ttl",
@@ -1943,16 +1964,98 @@ Note: no token held \u2014 the plugin enrolls on first contact (public) or needs
1943
1964
  }
1944
1965
  if (choice.startsWith("session:") && startAtSession) {
1945
1966
  const root = await resolveRoot(host.client, startAtSession);
1946
- const current = await readBinding(host.client, root);
1967
+ const all = await readBindings(host.client, root);
1968
+ const live = all.filter((b) => b.url && b.enabled !== false);
1969
+ const current = all[0];
1947
1970
  if (choice === "session:share") {
1948
1971
  if (!isConfigured(cfg)) {
1949
1972
  await showInfo(host.ui, { title: "Not configured", message: "Connect a server (Servers group) first, then share." });
1950
1973
  return;
1951
1974
  }
1952
- const result = await confirmShare(host, root);
1975
+ const result = await shareWithConsent(host, startAtSession);
1953
1976
  host.ui.toast({ variant: result.ok ? "success" : "error", title: "OpenShare", message: result.message, duration: result.ok ? 8000 : 15000 });
1954
1977
  return;
1955
1978
  }
1979
+ if (choice === "session:copy" && live[0]) {
1980
+ const okw = await host.clipboard.write(live[0].url);
1981
+ host.ui.toast({ variant: okw ? "success" : "error", title: "OpenShare", message: okw ? `Share URL copied: ${live[0].url}` : `Clipboard failed \u2014 ${live[0].url}` });
1982
+ return;
1983
+ }
1984
+ if (choice.startsWith("session:copy:")) {
1985
+ const b = live.find((x) => `session:copy:${x.server}` === choice);
1986
+ if (b) {
1987
+ const okw = await host.clipboard.write(b.url);
1988
+ host.ui.toast({ variant: okw ? "success" : "error", title: "OpenShare", message: okw ? `Share URL copied: ${b.url}` : `Clipboard failed \u2014 ${b.url}` });
1989
+ }
1990
+ return;
1991
+ }
1992
+ if (choice === "session:copyall") {
1993
+ const joined = live.map((b) => b.url).filter(Boolean).join(`
1994
+ `);
1995
+ const okw = await host.clipboard.write(joined);
1996
+ host.ui.toast({ variant: okw ? "success" : "error", title: "OpenShare", message: okw ? `${live.length} share URL(s) copied` : "Clipboard failed" });
1997
+ return;
1998
+ }
1999
+ if (choice === "session:unshare:one") {
2000
+ const pick = await showSelect(host.ui, {
2001
+ title: "Stop on which server",
2002
+ options: [...live.map((b) => ({ title: hostOf(b.url || b.server), value: b.server, description: b.url })), { title: "Cancel", value: "__cancel" }]
2003
+ });
2004
+ if (!pick || pick === "__cancel")
2005
+ return;
2006
+ const target = live.find((b) => b.server === pick);
2007
+ if (!target)
2008
+ return;
2009
+ const how = await showSelect(host.ui, {
2010
+ title: `Stop sharing \u2014 ${hostOf(target.url || target.server)}`,
2011
+ options: [
2012
+ { title: "Delete the page", value: "delete", description: "this link stops working for everyone", danger: true },
2013
+ { title: "Leave as-is (snapshot)", value: "leave", description: "stop updating; the page stays until its TTL" },
2014
+ { title: "Cancel", value: "__cancel" }
2015
+ ]
2016
+ });
2017
+ if (!how || how === "__cancel")
2018
+ return;
2019
+ if (how === "leave") {
2020
+ await patchSessionBinding(host.client, root, (servers) => {
2021
+ delete servers[target.server];
2022
+ servers[target.server] = { optedOut: true };
2023
+ });
2024
+ host.ui.toast({ variant: "success", title: "OpenShare", message: `${hostOf(target.url || target.server)} left as-is \u2014 a snapshot until its TTL.` });
2025
+ return;
2026
+ }
2027
+ const targetEntry = all.find((b) => b.server === target.server);
2028
+ if (!targetEntry?.secret) {
2029
+ host.ui.toast({ variant: "error", title: "OpenShare", message: "No secret for this page \u2014 cannot delete it server-side; binding cleared." });
2030
+ await patchSessionBinding(host.client, root, (servers) => {
2031
+ delete servers[target.server];
2032
+ });
2033
+ return;
2034
+ }
2035
+ const gone = await workerDeleteShare(target.server, target.id, targetEntry.secret);
2036
+ await patchSessionBinding(host.client, root, (servers) => {
2037
+ delete servers[target.server];
2038
+ });
2039
+ host.ui.toast({ variant: gone ? "success" : "warning", title: "OpenShare", message: gone ? `Page deleted on ${hostOf(target.url || target.server)}.` : `Could not delete the page (it may already be gone) \u2014 binding cleared.` });
2040
+ return;
2041
+ }
2042
+ if (choice === "session:more") {
2043
+ const otherServers = serverEntries().filter((s) => !live.some((b) => b.server === s.url));
2044
+ if (!otherServers.length)
2045
+ return;
2046
+ const pick = await showSelect(host.ui, {
2047
+ title: "Share to another server",
2048
+ options: [...otherServers.map((s) => ({ title: hostOf(s.url), value: s.url, description: s.entry.label })), { title: "Cancel", value: "__cancel" }]
2049
+ });
2050
+ if (!pick || pick === "__cancel")
2051
+ return;
2052
+ for (const s of serverEntries())
2053
+ if (s.url !== pick)
2054
+ saveServerEntry(s.url, { active: undefined });
2055
+ saveServerEntry(pick, { active: true });
2056
+ host.ui.toast({ variant: "success", title: "OpenShare", message: `${hostOf(pick)} activated \u2014 this session mirrors there within seconds.` });
2057
+ return;
2058
+ }
1956
2059
  if (choice === "session:unshare") {
1957
2060
  const how = await showSelect(host.ui, {
1958
2061
  title: "Stop sharing",
@@ -2141,7 +2244,6 @@ Connect now (URL + access code if the hoster gave you one), or open the full set
2141
2244
  }
2142
2245
 
2143
2246
  // src/commands.ts
2144
- init_sidebar();
2145
2247
  var PALETTE_CATEGORY = "OpenShare";
2146
2248
  function finish(host, result) {
2147
2249
  try {
@@ -2154,20 +2256,20 @@ function finish(host, result) {
2154
2256
  duration: result.ok ? 8000 : 15000
2155
2257
  });
2156
2258
  }
2157
- async function showShareManager(host, sessionID) {
2158
- const sid = sessionID();
2159
- if (!sid)
2160
- return;
2161
- const root = await resolveRoot(host.client, sid);
2259
+ async function sweepBindings(host, sid, root) {
2162
2260
  let all = await readBindings(host.client, root);
2261
+ let goneShown = false;
2163
2262
  const collided = all.find((b) => b.collided);
2164
- if (collided)
2165
- return showCollisionDialog(host, root, collided.server, collided.id, collided.collided ?? "");
2263
+ if (collided) {
2264
+ await showCollisionDialog(host, root, collided.server, collided.id, collided.collided ?? "");
2265
+ return null;
2266
+ }
2166
2267
  const live = all.filter((b) => b.url && b.enabled !== false && b.id);
2167
2268
  if (live.length) {
2168
2269
  const verdicts = await Promise.all(live.map((b) => verifyShare(b.server, b.id)));
2169
2270
  const gone = live.filter((b, i) => verdicts[i] === "gone");
2170
2271
  if (gone.length) {
2272
+ goneShown = true;
2171
2273
  await clearGoneBindingEntries(host, root, gone.map((b) => b.server));
2172
2274
  all = all.filter((b) => !gone.some((g) => g.server === b.server));
2173
2275
  const list = gone.map((b) => `\u2022 ${hostOf(b.url || b.server)} \u2014 ${b.url}`).join(`
@@ -2187,258 +2289,14 @@ ${list}` }
2187
2289
  ]
2188
2290
  });
2189
2291
  if (choice === "again") {
2190
- finish(host, await runShareFlow(host, sid));
2191
- return;
2292
+ finish(host, await shareWithConsent(host, sid));
2293
+ return null;
2192
2294
  }
2193
2295
  if (all.length === 0)
2194
- return;
2195
- }
2196
- }
2197
- if (all.length === 0)
2198
- return;
2199
- const otherServers = serverEntries().filter((s) => !all.some((b) => b.server === s.url));
2200
- if (all.length === 1 && otherServers.length === 0) {
2201
- const b = all[0];
2202
- host.ui.showMenu({
2203
- title: "Session is shared",
2204
- hint: b.url,
2205
- options: [
2206
- {
2207
- title: "Copy share link",
2208
- value: "copy",
2209
- description: "copy the share URL to the clipboard",
2210
- help: `Copies the share URL to the clipboard (navigator + OSC52 fallback).
2211
-
2212
- URL: ${b.url}
2213
- Server: ${b.server}
2214
- Share id: ${b.id}
2215
-
2216
- Anyone with the link can read the page (plus the login, if read auth is set).`
2217
- },
2218
- {
2219
- title: "Stop sharing",
2220
- value: "stop",
2221
- description: "Delete the share page \u2014 the link stops working for everyone",
2222
- 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.`
2223
- },
2224
- { title: "Cancel", value: "cancel" }
2225
- ]
2226
- }).then(async (choice) => {
2227
- if (choice === "copy") {
2228
- const ok = await host.clipboard.write(b.url);
2229
- 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 });
2230
- return;
2231
- }
2232
- if (choice === "stop") {
2233
- const how = await host.ui.showMenu({
2234
- title: "Stop sharing",
2235
- hint: b.url,
2236
- options: [
2237
- {
2238
- title: "Delete the page",
2239
- value: "delete",
2240
- description: "the link stops working for everyone",
2241
- danger: true,
2242
- 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.`
2243
- },
2244
- {
2245
- title: "Leave as-is (snapshot)",
2246
- value: "leave",
2247
- description: "stop updating; the page stays until its TTL",
2248
- 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.`
2249
- },
2250
- { title: "Cancel", value: "cancel" }
2251
- ]
2252
- });
2253
- if (!how || how === "cancel")
2254
- return;
2255
- if (how === "leave") {
2256
- await patchSessionBinding(host.client, root, (servers) => {
2257
- delete servers[b.server];
2258
- servers[b.server] = { optedOut: true };
2259
- });
2260
- finish(host, { ok: true, message: `${hostOf(b.url || b.server)} left as-is \u2014 a snapshot until its TTL.` });
2261
- return;
2262
- }
2263
- const yes = await host.ui.showConfirm({
2264
- title: "Delete the share page",
2265
- message: `Delete the page on ${hostOf(b.url || b.server)}?
2266
- The link stops working for everyone.`,
2267
- confirmLabel: "Delete",
2268
- danger: true
2269
- });
2270
- if (!yes) {
2271
- host.ui.dialog.clear();
2272
- return;
2273
- }
2274
- finish(host, await runUnshareFlow(host, sessionID()));
2275
- }
2276
- });
2277
- return;
2278
- }
2279
- const options = all.map((b) => ({
2280
- title: `Copy link \u2014 ${hostOf(b.url || b.server)}`,
2281
- value: `copy:${b.server}`,
2282
- description: b.url,
2283
- help: `Copies this server's share URL to the clipboard.
2284
-
2285
- URL: ${b.url}
2286
- Server: ${b.server}
2287
- Share id: ${b.id}
2288
-
2289
- Each server carries an independent page of the same session.`
2290
- }));
2291
- options.push({
2292
- title: "Manage per server\u2026",
2293
- value: "manage",
2294
- description: `${all.length} servers \u2014 stop one, leave a snapshot`,
2295
- 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.
2296
-
2297
- Bound servers:
2298
- ${all.map((b) => `\u2022 ${b.server} \u2014 ${b.url}`).join(`
2299
- `)}`
2300
- });
2301
- if (otherServers.length) {
2302
- options.push({
2303
- title: "Share to another server\u2026",
2304
- value: "more",
2305
- description: otherServers.map((s) => hostOf(s.url)).join(", "),
2306
- 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.
2307
-
2308
- Available:
2309
- ${otherServers.map((s) => `\u2022 ${s.url}`).join(`
2310
- `)}`
2311
- });
2312
- }
2313
- options.push({
2314
- title: "Stop sharing (all)",
2315
- value: "stop-all",
2316
- description: "delete every server's page, or leave snapshots",
2317
- danger: true,
2318
- 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.`
2319
- });
2320
- options.push({ title: "Cancel", value: "cancel" });
2321
- const choice = await host.ui.showMenu({ title: `Session is shared \xB7 ${all.length} servers`, options });
2322
- if (!choice || choice === "cancel")
2323
- return;
2324
- if (choice.startsWith("copy:")) {
2325
- const b = all.find((x) => `copy:${x.server}` === choice);
2326
- if (b) {
2327
- const ok = await host.clipboard.write(b.url);
2328
- 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 });
2329
- }
2330
- return;
2331
- }
2332
- if (choice === "stop-all") {
2333
- const how = await host.ui.showMenu({
2334
- title: `Stop sharing \xB7 ${all.length} servers`,
2335
- options: [
2336
- { 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.` },
2337
- { 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.` },
2338
- { title: "Cancel", value: "cancel" }
2339
- ]
2340
- });
2341
- if (!how || how === "cancel")
2342
- return;
2343
- if (how === "leave") {
2344
- await patchSessionBinding(host.client, root, (servers) => {
2345
- for (const b of all) {
2346
- delete servers[b.server];
2347
- servers[b.server] = { optedOut: true };
2348
- }
2349
- });
2350
- finish(host, { ok: true, message: `${all.length} server(s) left as-is \u2014 snapshots until their TTLs.` });
2351
- return;
2352
- }
2353
- const yes = await host.ui.showConfirm({
2354
- title: "Stop sharing",
2355
- message: `Delete the share pages on all ${all.length} servers? The links stop working for everyone.`,
2356
- confirmLabel: "Stop all",
2357
- danger: true
2358
- });
2359
- if (!yes)
2360
- return;
2361
- finish(host, await runUnshareFlow(host, sessionID()));
2362
- return;
2363
- }
2364
- if (choice === "more") {
2365
- const pick = await host.ui.showMenu({
2366
- title: "Share to another server",
2367
- options: [...otherServers.map((s) => ({ title: hostOf(s.url), value: s.url, description: s.entry.label })), { title: "Cancel", value: "__cancel" }]
2368
- });
2369
- if (!pick || pick === "__cancel")
2370
- return;
2371
- await Promise.resolve().then(() => init_share_flow());
2372
- for (const s of serverEntries())
2373
- if (s.url !== pick)
2374
- saveServerEntry(s.url, { active: undefined });
2375
- saveServerEntry(pick, { active: true });
2376
- finish(host, { ok: true, message: `${hostOf(pick)} activated \u2014 this session mirrors there within seconds (new sessions share to it too).` });
2377
- return;
2378
- }
2379
- if (choice === "manage") {
2380
- const pick = await host.ui.showMenu({
2381
- title: "Manage server",
2382
- options: [...all.map((b) => ({ title: hostOf(b.url || b.server), value: b.server, description: b.url })), { title: "Cancel", value: "__cancel" }]
2383
- });
2384
- if (!pick || pick === "__cancel")
2385
- return;
2386
- const b = all.find((x) => x.server === pick);
2387
- if (!b)
2388
- return;
2389
- const action = await host.ui.showMenu({
2390
- title: `${hostOf(b.url || b.server)}`,
2391
- hint: b.url,
2392
- options: [
2393
- {
2394
- title: "Copy link",
2395
- value: "copy",
2396
- description: "copy the share URL",
2397
- help: `Copies this server's share URL to the clipboard.
2398
-
2399
- URL: ${b.url}
2400
- Share id: ${b.id}`
2401
- },
2402
- {
2403
- title: "Stop sharing here",
2404
- value: "stop",
2405
- description: "delete THIS server's page; other servers unaffected",
2406
- 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.`
2407
- },
2408
- {
2409
- title: "Leave as-is (snapshot)",
2410
- value: "leave",
2411
- description: "stop updating it; the page stays until its TTL",
2412
- 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.`
2413
- },
2414
- { title: "Cancel", value: "cancel" }
2415
- ]
2416
- });
2417
- if (!action || action === "cancel")
2418
- return;
2419
- if (action === "copy") {
2420
- const ok = await host.clipboard.write(b.url);
2421
- 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 });
2422
- return;
2423
- }
2424
- if (action === "stop") {
2425
- const deleted = await workerDeleteShare(b.server || b.url.slice(0, b.url.indexOf("/s/")), b.id, b.secret);
2426
- await patchSessionBinding(host.client, root, (servers) => {
2427
- delete servers[b.server];
2428
- servers[b.server] = { optedOut: true };
2429
- });
2430
- 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).` });
2431
- return;
2432
- }
2433
- if (action === "leave") {
2434
- await patchSessionBinding(host.client, root, (servers) => {
2435
- delete servers[b.server];
2436
- servers[b.server] = { optedOut: true };
2437
- });
2438
- finish(host, { ok: true, message: `${hostOf(b.url || b.server)} left as-is \u2014 a snapshot until its TTL.` });
2439
- return;
2296
+ return { all, goneShown };
2440
2297
  }
2441
2298
  }
2299
+ return { all, goneShown };
2442
2300
  }
2443
2301
  async function showCollisionDialog(host, root, server, shareId, error) {
2444
2302
  const debugInfo = JSON.stringify({ server, shareId, error, sessionID: root, plugin: "openshare", time: new Date().toISOString() }, null, 2);
@@ -2515,39 +2373,17 @@ function buildShareCommand(host, sessionID) {
2515
2373
  return;
2516
2374
  }
2517
2375
  const root = await resolveRoot(host.client, sid);
2518
- const bindings = await readBindings(host.client, root);
2519
- const binding = bindings[0];
2520
- dbgLog(`/share gate: sid=${sid} root=${root} bindings=${bindings.length}`);
2521
- if (bindings.some((b) => b.url && b.enabled !== false)) {
2522
- await showShareManager(host, sessionID);
2523
- host.ui.dialog.clear();
2376
+ const swept = await sweepBindings(host, sid, root);
2377
+ if (swept === null)
2524
2378
  return;
2525
- }
2526
- const result = await runShareFlow(host, sid);
2527
- if (result.message === "consent required") {
2528
- const yes = await host.ui.showConfirm({
2529
- title: "Share Session",
2530
- message: `Publish this session (and its subagent sessions) to your openshare worker?
2531
- The link lets anyone with it read the conversation.`,
2532
- confirmLabel: "Share"
2533
- });
2534
- if (!yes)
2535
- return;
2536
- try {
2537
- host.kv.set(CONSENT_KV_KEY, true);
2538
- dbgLog("consent kv.set ok");
2539
- } catch (e) {
2540
- dbgLog(`consent kv.set THREW: ${e?.message ?? e}`);
2541
- }
2542
- try {
2543
- finish(host, await confirmShare(host, root));
2544
- } catch (e) {
2545
- dbgLog(`confirmShare rejected: ${e?.stack ?? e}`);
2546
- finish(host, { ok: false, message: "Share failed (see debug log)." });
2547
- }
2379
+ const { all, goneShown } = swept;
2380
+ if (all.some((b) => b.url && b.enabled !== false)) {
2381
+ await settingsDialog(host, sid);
2548
2382
  return;
2549
2383
  }
2550
- finish(host, result);
2384
+ if (goneShown && all.length === 0)
2385
+ return;
2386
+ finish(host, await shareWithConsent(host, sid));
2551
2387
  }
2552
2388
  };
2553
2389
  }
@@ -2572,11 +2408,7 @@ function registerCommands(host, sessionID) {
2572
2408
  });
2573
2409
  }
2574
2410
 
2575
- // src/tui.tsx
2576
- init_sidebar();
2577
-
2578
2411
  // src/dialogs.tsx
2579
- init_sidebar();
2580
2412
  import { use as _$use } from "@opentui/solid";
2581
2413
  import { effect as _$effect2 } from "@opentui/solid";
2582
2414
  import { memo as _$memo } from "@opentui/solid";
@@ -3337,13 +3169,36 @@ function MenuDialog(props) {
3337
3169
  }
3338
3170
  });
3339
3171
  }
3172
+ var pendingDialog;
3173
+ function claimDialog(api) {
3174
+ if (pendingDialog) {
3175
+ try {
3176
+ pendingDialog();
3177
+ } catch {}
3178
+ try {
3179
+ api.ui.dialog.clear();
3180
+ } catch {}
3181
+ }
3182
+ let released = false;
3183
+ const release = () => {
3184
+ if (released)
3185
+ return;
3186
+ released = true;
3187
+ if (pendingDialog === release)
3188
+ pendingDialog = undefined;
3189
+ };
3190
+ pendingDialog = release;
3191
+ return release;
3192
+ }
3340
3193
  function showMenuOnce(api, props) {
3341
3194
  return new Promise((resolve) => {
3342
3195
  let settled = false;
3196
+ const release = claimDialog(api);
3343
3197
  const done = (value, _clear = false) => {
3344
3198
  if (settled)
3345
3199
  return;
3346
3200
  settled = true;
3201
+ release();
3347
3202
  dbgLog(`menu(${props.title}) -> ${value === undefined ? "cancel" : String(value)}`);
3348
3203
  resolve(value);
3349
3204
  };
@@ -3369,10 +3224,12 @@ function showMenuOnce(api, props) {
3369
3224
  function showInfoDialog(api, props) {
3370
3225
  return new Promise((resolve) => {
3371
3226
  let settled = false;
3227
+ const release = claimDialog(api);
3372
3228
  const done = (value) => {
3373
3229
  if (settled)
3374
3230
  return;
3375
3231
  settled = true;
3232
+ release();
3376
3233
  resolve(value);
3377
3234
  };
3378
3235
  api.ui.dialog.replace(() => _$createComponent(InfoDialog, {
@@ -3602,13 +3459,17 @@ function InfoDialog(props) {
3602
3459
  _$insertNode2(_el$44, _el$45);
3603
3460
  _$insert2(_el$45, () => action.title);
3604
3461
  _$effect2((_p$) => {
3605
- var _v$12 = action.danger ? theme().error : theme().primary, _v$13 = theme().background;
3462
+ var _v$12 = action.key === "enter" ? action.danger ? theme().error : theme().primary : undefined, _v$13 = action.key === "enter" ? undefined : true, _v$14 = action.danger ? theme().error : theme().textMuted, _v$15 = action.key === "enter" ? theme().background : action.danger ? theme().error : theme().textMuted;
3606
3463
  _v$12 !== _p$.e && (_p$.e = _$setProp2(_el$43, "backgroundColor", _v$12, _p$.e));
3607
- _v$13 !== _p$.t && (_p$.t = _$setProp2(_el$44, "fg", _v$13, _p$.t));
3464
+ _v$13 !== _p$.t && (_p$.t = _$setProp2(_el$43, "border", _v$13, _p$.t));
3465
+ _v$14 !== _p$.a && (_p$.a = _$setProp2(_el$43, "borderColor", _v$14, _p$.a));
3466
+ _v$15 !== _p$.o && (_p$.o = _$setProp2(_el$44, "fg", _v$15, _p$.o));
3608
3467
  return _p$;
3609
3468
  }, {
3610
3469
  e: undefined,
3611
- t: undefined
3470
+ t: undefined,
3471
+ a: undefined,
3472
+ o: undefined
3612
3473
  });
3613
3474
  return _el$43;
3614
3475
  })()
@@ -3743,10 +3604,10 @@ function PromptDialog(props) {
3743
3604
  _$insert2(_el$47, () => text() || props.placeholder || "");
3744
3605
  _$insertNode2(_el$48, _$createTextNode(`\u258C`));
3745
3606
  _$effect2((_p$) => {
3746
- var _v$14 = theme().backgroundPanel, _v$15 = text() ? theme().text : theme().textMuted, _v$16 = theme().accent;
3747
- _v$14 !== _p$.e && (_p$.e = _$setProp2(_el$46, "backgroundColor", _v$14, _p$.e));
3748
- _v$15 !== _p$.t && (_p$.t = _$setProp2(_el$47, "fg", _v$15, _p$.t));
3749
- _v$16 !== _p$.a && (_p$.a = _$setProp2(_el$48, "fg", _v$16, _p$.a));
3607
+ var _v$16 = theme().backgroundPanel, _v$17 = text() ? theme().text : theme().textMuted, _v$18 = theme().accent;
3608
+ _v$16 !== _p$.e && (_p$.e = _$setProp2(_el$46, "backgroundColor", _v$16, _p$.e));
3609
+ _v$17 !== _p$.t && (_p$.t = _$setProp2(_el$47, "fg", _v$17, _p$.t));
3610
+ _v$18 !== _p$.a && (_p$.a = _$setProp2(_el$48, "fg", _v$18, _p$.a));
3750
3611
  return _p$;
3751
3612
  }, {
3752
3613
  e: undefined,
@@ -3767,10 +3628,12 @@ function PromptDialog(props) {
3767
3628
  function showPromptOnce(api, props) {
3768
3629
  return new Promise((resolve) => {
3769
3630
  let settled = false;
3631
+ const release = claimDialog(api);
3770
3632
  const done = (value, _clear = false) => {
3771
3633
  if (settled)
3772
3634
  return;
3773
3635
  settled = true;
3636
+ release();
3774
3637
  dbgLog(`prompt(${props.title}) -> ${value === undefined ? "cancel" : `len=${value.length}`}`);
3775
3638
  resolve(value);
3776
3639
  };
@@ -3805,10 +3668,12 @@ function toMenuOptions(options) {
3805
3668
  function showSizePickerOnce(api, current) {
3806
3669
  return new Promise((resolve) => {
3807
3670
  let settled = false;
3671
+ const release = claimDialog(api);
3808
3672
  const done = (value) => {
3809
3673
  if (settled)
3810
3674
  return;
3811
3675
  settled = true;
3676
+ release();
3812
3677
  resolve(value);
3813
3678
  };
3814
3679
  api.ui.dialog.replace(() => _$createComponent(SizePickerDialog, {
@@ -4023,9 +3888,9 @@ function SizePickerDialog(props) {
4023
3888
  _$insertNode2(_el$56, _el$57);
4024
3889
  _$insert2(_el$56, height, _el$57);
4025
3890
  _$effect2((_p$) => {
4026
- var _v$17 = theme().textMuted, _v$18 = theme().accent;
4027
- _v$17 !== _p$.e && (_p$.e = _$setProp2(_el$51, "fg", _v$17, _p$.e));
4028
- _v$18 !== _p$.t && (_p$.t = _$setProp2(_el$55, "fg", _v$18, _p$.t));
3891
+ var _v$19 = theme().textMuted, _v$20 = theme().accent;
3892
+ _v$19 !== _p$.e && (_p$.e = _$setProp2(_el$51, "fg", _v$19, _p$.e));
3893
+ _v$20 !== _p$.t && (_p$.t = _$setProp2(_el$55, "fg", _v$20, _p$.t));
4029
3894
  return _p$;
4030
3895
  }, {
4031
3896
  e: undefined,
@@ -4058,9 +3923,9 @@ function SizePickerDialog(props) {
4058
3923
  _$insertNode2(_el$61, _el$63);
4059
3924
  _$insert2(_el$61, HEIGHT_PERCENT_MAX, _el$63);
4060
3925
  _$effect2((_p$) => {
4061
- var _v$19 = theme().textMuted, _v$20 = theme().textMuted;
4062
- _v$19 !== _p$.e && (_p$.e = _$setProp2(_el$59, "fg", _v$19, _p$.e));
4063
- _v$20 !== _p$.t && (_p$.t = _$setProp2(_el$61, "fg", _v$20, _p$.t));
3926
+ var _v$21 = theme().textMuted, _v$22 = theme().textMuted;
3927
+ _v$21 !== _p$.e && (_p$.e = _$setProp2(_el$59, "fg", _v$21, _p$.e));
3928
+ _v$22 !== _p$.t && (_p$.t = _$setProp2(_el$61, "fg", _v$22, _p$.t));
4064
3929
  return _p$;
4065
3930
  }, {
4066
3931
  e: undefined,