@mirrowel/opencode-openshare 0.2.0-dev.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";
@@ -338,6 +338,8 @@ function dbg(v, depth = 0) {
338
338
  }
339
339
  }
340
340
  function dbgLog(_line) {}
341
+ var GONE_TTL_MS = 60000;
342
+ var goneCache = new Map;
341
343
  function goneVerdict(sid, server, shareId) {
342
344
  const key = `${sid}|${server}`;
343
345
  const cached = goneCache.get(key);
@@ -422,14 +424,17 @@ function createSidebarSlot(api, opts) {
422
424
  }
423
425
  };
424
426
  }
425
- var GONE_TTL_MS = 60000, goneCache;
426
- var init_sidebar = __esm(() => {
427
- init_share_flow();
428
- goneCache = new Map;
429
- });
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
+ }
430
436
 
431
437
  // src/share-flow.ts
432
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
433
438
  function currentConfig(baseDir = moduleDir()) {
434
439
  return loadConfig(baseDir);
435
440
  }
@@ -543,17 +548,19 @@ function hostOf(url) {
543
548
  }
544
549
  }
545
550
  async function patchSessionBinding(client, sid, mutator) {
546
- const info = await getSession(client, sid);
547
- if (!info)
548
- return false;
549
- const metadata = { ...info.metadata ?? {} };
550
- const raw = info.metadata?.[METADATA_KEY];
551
- const binding = raw && typeof raw === "object" ? { ...raw } : { v: 2, servers: {} };
552
- const servers = binding.servers && typeof binding.servers === "object" ? { ...binding.servers } : {};
553
- mutator(servers, binding);
554
- binding.servers = servers;
555
- metadata[METADATA_KEY] = binding;
556
- 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
+ });
557
564
  }
558
565
  function bindingOf(info) {
559
566
  const all = bindingsOf(info);
@@ -784,6 +791,7 @@ function adminTokenFor(url, cfg = currentConfig()) {
784
791
  const entry = serverEntries(cfg).find((s) => s.url === base);
785
792
  return entry?.entry.adminToken ?? (cfg.workerUrl && normServerUrl(cfg.workerUrl) === base ? cfg.createToken : "") ?? "";
786
793
  }
794
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
787
795
  async function waitForBinding(client, sid, timeoutMs = 25000) {
788
796
  const start = Date.now();
789
797
  while (Date.now() - start < timeoutMs) {
@@ -794,7 +802,15 @@ async function waitForBinding(client, sid, timeoutMs = 25000) {
794
802
  }
795
803
  return;
796
804
  }
797
- 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) {
798
814
  if (!sessionID)
799
815
  return { ok: false, message: "No active session to share." };
800
816
  const cfg = currentConfig();
@@ -807,9 +823,8 @@ async function runShareFlow(host, sessionID) {
807
823
  await host.clipboard.write(existing.url);
808
824
  return { ok: true, message: `Share URL copied to clipboard: ${existing.url}`, url: existing.url };
809
825
  }
810
- if (!host.kv.get(CONSENT_KV_KEY, false)) {
811
- return { ok: false, message: "consent required" };
812
- }
826
+ if (!await requestShareConsent(host))
827
+ return { ok: false, message: "Share cancelled." };
813
828
  return confirmShare(host, root);
814
829
  }
815
830
  async function confirmShare(host, rootSessionID) {
@@ -852,26 +867,6 @@ async function runUnshareFlow(host, sessionID) {
852
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).`
853
868
  };
854
869
  }
855
- var CONSENT_KV_KEY = "openshare_consent", sleep = (ms) => new Promise((r) => setTimeout(r, ms));
856
- var init_share_flow = __esm(() => {
857
- init_config();
858
- init_sidebar();
859
- });
860
-
861
- // src/tui.tsx
862
- import { effect as _$effect3 } from "@opentui/solid";
863
- import { createTextNode as _$createTextNode2 } from "@opentui/solid";
864
- import { insertNode as _$insertNode3 } from "@opentui/solid";
865
- import { insert as _$insert3 } from "@opentui/solid";
866
- import { setProp as _$setProp3 } from "@opentui/solid";
867
- import { createElement as _$createElement3 } from "@opentui/solid";
868
-
869
- // src/commands.ts
870
- init_share_flow();
871
-
872
- // src/settings.ts
873
- init_config();
874
- init_share_flow();
875
870
 
876
871
  // src/ui-prefs.ts
877
872
  var UI_WIDTH_KV = "openshare.ui-width";
@@ -1772,33 +1767,93 @@ ${sourcesMessage(sources)}`
1772
1767
  const sessionRows = async () => {
1773
1768
  if (!startAtSession)
1774
1769
  return [];
1775
- const binding = await readBinding(host.client, await resolveRoot(host.client, startAtSession));
1776
- const rows = [
1777
- {
1778
- 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",
1779
1777
  value: "session:share",
1780
- description: binding?.url ? "copy the share URL to the clipboard" : "Set the share flag; the server plugin creates the page",
1781
- 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.
1782
1788
 
1783
- URL: ${binding.url}
1784
- 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."
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.
1799
+
1800
+ URL: ${b.url}
1801
+ Share id: ${b.id}
1802
+
1803
+ Each server carries an independent page of the same session.`
1804
+ });
1785
1805
  }
1786
- ];
1787
- 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) {
1788
1817
  rows.push({
1789
1818
  title: "Stop sharing",
1790
1819
  value: "session:unshare",
1791
- 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",
1792
1821
  danger: true,
1793
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).
1794
1823
 
1795
1824
  URLs:
1796
- ${[binding.url].join(`
1825
+ ${live.map((b) => b.url).join(`
1797
1826
  `)}
1798
1827
 
1799
1828
  Sharing again later always creates fresh page(s) with new URLs.`
1800
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
+ }
1801
1855
  }
1856
+ const binding = bindings[0];
1802
1857
  rows.push({
1803
1858
  title: "Session TTL override (days)",
1804
1859
  value: "session:ttl",
@@ -1909,16 +1964,98 @@ Note: no token held \u2014 the plugin enrolls on first contact (public) or needs
1909
1964
  }
1910
1965
  if (choice.startsWith("session:") && startAtSession) {
1911
1966
  const root = await resolveRoot(host.client, startAtSession);
1912
- 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];
1913
1970
  if (choice === "session:share") {
1914
1971
  if (!isConfigured(cfg)) {
1915
1972
  await showInfo(host.ui, { title: "Not configured", message: "Connect a server (Servers group) first, then share." });
1916
1973
  return;
1917
1974
  }
1918
- const result = await confirmShare(host, root);
1975
+ const result = await shareWithConsent(host, startAtSession);
1919
1976
  host.ui.toast({ variant: result.ok ? "success" : "error", title: "OpenShare", message: result.message, duration: result.ok ? 8000 : 15000 });
1920
1977
  return;
1921
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
+ }
1922
2059
  if (choice === "session:unshare") {
1923
2060
  const how = await showSelect(host.ui, {
1924
2061
  title: "Stop sharing",
@@ -2107,7 +2244,6 @@ Connect now (URL + access code if the hoster gave you one), or open the full set
2107
2244
  }
2108
2245
 
2109
2246
  // src/commands.ts
2110
- init_sidebar();
2111
2247
  var PALETTE_CATEGORY = "OpenShare";
2112
2248
  function finish(host, result) {
2113
2249
  try {
@@ -2120,20 +2256,20 @@ function finish(host, result) {
2120
2256
  duration: result.ok ? 8000 : 15000
2121
2257
  });
2122
2258
  }
2123
- async function showShareManager(host, sessionID) {
2124
- const sid = sessionID();
2125
- if (!sid)
2126
- return;
2127
- const root = await resolveRoot(host.client, sid);
2259
+ async function sweepBindings(host, sid, root) {
2128
2260
  let all = await readBindings(host.client, root);
2261
+ let goneShown = false;
2129
2262
  const collided = all.find((b) => b.collided);
2130
- if (collided)
2131
- 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
+ }
2132
2267
  const live = all.filter((b) => b.url && b.enabled !== false && b.id);
2133
2268
  if (live.length) {
2134
2269
  const verdicts = await Promise.all(live.map((b) => verifyShare(b.server, b.id)));
2135
2270
  const gone = live.filter((b, i) => verdicts[i] === "gone");
2136
2271
  if (gone.length) {
2272
+ goneShown = true;
2137
2273
  await clearGoneBindingEntries(host, root, gone.map((b) => b.server));
2138
2274
  all = all.filter((b) => !gone.some((g) => g.server === b.server));
2139
2275
  const list = gone.map((b) => `\u2022 ${hostOf(b.url || b.server)} \u2014 ${b.url}`).join(`
@@ -2153,258 +2289,14 @@ ${list}` }
2153
2289
  ]
2154
2290
  });
2155
2291
  if (choice === "again") {
2156
- finish(host, await runShareFlow(host, sid));
2157
- return;
2292
+ finish(host, await shareWithConsent(host, sid));
2293
+ return null;
2158
2294
  }
2159
2295
  if (all.length === 0)
2160
- return;
2161
- }
2162
- }
2163
- if (all.length === 0)
2164
- return;
2165
- const otherServers = serverEntries().filter((s) => !all.some((b) => b.server === s.url));
2166
- if (all.length === 1 && otherServers.length === 0) {
2167
- const b = all[0];
2168
- host.ui.showMenu({
2169
- title: "Session is shared",
2170
- hint: b.url,
2171
- options: [
2172
- {
2173
- title: "Copy share link",
2174
- value: "copy",
2175
- description: "copy the share URL to the clipboard",
2176
- help: `Copies the share URL to the clipboard (navigator + OSC52 fallback).
2177
-
2178
- URL: ${b.url}
2179
- Server: ${b.server}
2180
- Share id: ${b.id}
2181
-
2182
- Anyone with the link can read the page (plus the login, if read auth is set).`
2183
- },
2184
- {
2185
- title: "Stop sharing",
2186
- value: "stop",
2187
- description: "Delete the share page \u2014 the link stops working for everyone",
2188
- 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.`
2189
- },
2190
- { title: "Cancel", value: "cancel" }
2191
- ]
2192
- }).then(async (choice) => {
2193
- if (choice === "copy") {
2194
- const ok = await host.clipboard.write(b.url);
2195
- 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 });
2196
- return;
2197
- }
2198
- if (choice === "stop") {
2199
- const how = await host.ui.showMenu({
2200
- title: "Stop sharing",
2201
- hint: b.url,
2202
- options: [
2203
- {
2204
- title: "Delete the page",
2205
- value: "delete",
2206
- description: "the link stops working for everyone",
2207
- danger: true,
2208
- 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.`
2209
- },
2210
- {
2211
- title: "Leave as-is (snapshot)",
2212
- value: "leave",
2213
- description: "stop updating; the page stays until its TTL",
2214
- 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.`
2215
- },
2216
- { title: "Cancel", value: "cancel" }
2217
- ]
2218
- });
2219
- if (!how || how === "cancel")
2220
- return;
2221
- if (how === "leave") {
2222
- await patchSessionBinding(host.client, root, (servers) => {
2223
- delete servers[b.server];
2224
- servers[b.server] = { optedOut: true };
2225
- });
2226
- finish(host, { ok: true, message: `${hostOf(b.url || b.server)} left as-is \u2014 a snapshot until its TTL.` });
2227
- return;
2228
- }
2229
- const yes = await host.ui.showConfirm({
2230
- title: "Delete the share page",
2231
- message: `Delete the page on ${hostOf(b.url || b.server)}?
2232
- The link stops working for everyone.`,
2233
- confirmLabel: "Delete",
2234
- danger: true
2235
- });
2236
- if (!yes) {
2237
- host.ui.dialog.clear();
2238
- return;
2239
- }
2240
- finish(host, await runUnshareFlow(host, sessionID()));
2241
- }
2242
- });
2243
- return;
2244
- }
2245
- const options = all.map((b) => ({
2246
- title: `Copy link \u2014 ${hostOf(b.url || b.server)}`,
2247
- value: `copy:${b.server}`,
2248
- description: b.url,
2249
- help: `Copies this server's share URL to the clipboard.
2250
-
2251
- URL: ${b.url}
2252
- Server: ${b.server}
2253
- Share id: ${b.id}
2254
-
2255
- Each server carries an independent page of the same session.`
2256
- }));
2257
- options.push({
2258
- title: "Manage per server\u2026",
2259
- value: "manage",
2260
- description: `${all.length} servers \u2014 stop one, leave a snapshot`,
2261
- 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.
2262
-
2263
- Bound servers:
2264
- ${all.map((b) => `\u2022 ${b.server} \u2014 ${b.url}`).join(`
2265
- `)}`
2266
- });
2267
- if (otherServers.length) {
2268
- options.push({
2269
- title: "Share to another server\u2026",
2270
- value: "more",
2271
- description: otherServers.map((s) => hostOf(s.url)).join(", "),
2272
- 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.
2273
-
2274
- Available:
2275
- ${otherServers.map((s) => `\u2022 ${s.url}`).join(`
2276
- `)}`
2277
- });
2278
- }
2279
- options.push({
2280
- title: "Stop sharing (all)",
2281
- value: "stop-all",
2282
- description: "delete every server's page, or leave snapshots",
2283
- danger: true,
2284
- 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.`
2285
- });
2286
- options.push({ title: "Cancel", value: "cancel" });
2287
- const choice = await host.ui.showMenu({ title: `Session is shared \xB7 ${all.length} servers`, options });
2288
- if (!choice || choice === "cancel")
2289
- return;
2290
- if (choice.startsWith("copy:")) {
2291
- const b = all.find((x) => `copy:${x.server}` === choice);
2292
- if (b) {
2293
- const ok = await host.clipboard.write(b.url);
2294
- 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 });
2295
- }
2296
- return;
2297
- }
2298
- if (choice === "stop-all") {
2299
- const how = await host.ui.showMenu({
2300
- title: `Stop sharing \xB7 ${all.length} servers`,
2301
- options: [
2302
- { 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.` },
2303
- { 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.` },
2304
- { title: "Cancel", value: "cancel" }
2305
- ]
2306
- });
2307
- if (!how || how === "cancel")
2308
- return;
2309
- if (how === "leave") {
2310
- await patchSessionBinding(host.client, root, (servers) => {
2311
- for (const b of all) {
2312
- delete servers[b.server];
2313
- servers[b.server] = { optedOut: true };
2314
- }
2315
- });
2316
- finish(host, { ok: true, message: `${all.length} server(s) left as-is \u2014 snapshots until their TTLs.` });
2317
- return;
2318
- }
2319
- const yes = await host.ui.showConfirm({
2320
- title: "Stop sharing",
2321
- message: `Delete the share pages on all ${all.length} servers? The links stop working for everyone.`,
2322
- confirmLabel: "Stop all",
2323
- danger: true
2324
- });
2325
- if (!yes)
2326
- return;
2327
- finish(host, await runUnshareFlow(host, sessionID()));
2328
- return;
2329
- }
2330
- if (choice === "more") {
2331
- const pick = await host.ui.showMenu({
2332
- title: "Share to another server",
2333
- options: [...otherServers.map((s) => ({ title: hostOf(s.url), value: s.url, description: s.entry.label })), { title: "Cancel", value: "__cancel" }]
2334
- });
2335
- if (!pick || pick === "__cancel")
2336
- return;
2337
- await Promise.resolve().then(() => init_share_flow());
2338
- for (const s of serverEntries())
2339
- if (s.url !== pick)
2340
- saveServerEntry(s.url, { active: undefined });
2341
- saveServerEntry(pick, { active: true });
2342
- finish(host, { ok: true, message: `${hostOf(pick)} activated \u2014 this session mirrors there within seconds (new sessions share to it too).` });
2343
- return;
2344
- }
2345
- if (choice === "manage") {
2346
- const pick = await host.ui.showMenu({
2347
- title: "Manage server",
2348
- options: [...all.map((b) => ({ title: hostOf(b.url || b.server), value: b.server, description: b.url })), { title: "Cancel", value: "__cancel" }]
2349
- });
2350
- if (!pick || pick === "__cancel")
2351
- return;
2352
- const b = all.find((x) => x.server === pick);
2353
- if (!b)
2354
- return;
2355
- const action = await host.ui.showMenu({
2356
- title: `${hostOf(b.url || b.server)}`,
2357
- hint: b.url,
2358
- options: [
2359
- {
2360
- title: "Copy link",
2361
- value: "copy",
2362
- description: "copy the share URL",
2363
- help: `Copies this server's share URL to the clipboard.
2364
-
2365
- URL: ${b.url}
2366
- Share id: ${b.id}`
2367
- },
2368
- {
2369
- title: "Stop sharing here",
2370
- value: "stop",
2371
- description: "delete THIS server's page; other servers unaffected",
2372
- 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.`
2373
- },
2374
- {
2375
- title: "Leave as-is (snapshot)",
2376
- value: "leave",
2377
- description: "stop updating it; the page stays until its TTL",
2378
- 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.`
2379
- },
2380
- { title: "Cancel", value: "cancel" }
2381
- ]
2382
- });
2383
- if (!action || action === "cancel")
2384
- return;
2385
- if (action === "copy") {
2386
- const ok = await host.clipboard.write(b.url);
2387
- 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 });
2388
- return;
2389
- }
2390
- if (action === "stop") {
2391
- const deleted = await workerDeleteShare(b.server || b.url.slice(0, b.url.indexOf("/s/")), b.id, b.secret);
2392
- await patchSessionBinding(host.client, root, (servers) => {
2393
- delete servers[b.server];
2394
- servers[b.server] = { optedOut: true };
2395
- });
2396
- 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).` });
2397
- return;
2398
- }
2399
- if (action === "leave") {
2400
- await patchSessionBinding(host.client, root, (servers) => {
2401
- delete servers[b.server];
2402
- servers[b.server] = { optedOut: true };
2403
- });
2404
- finish(host, { ok: true, message: `${hostOf(b.url || b.server)} left as-is \u2014 a snapshot until its TTL.` });
2405
- return;
2296
+ return { all, goneShown };
2406
2297
  }
2407
2298
  }
2299
+ return { all, goneShown };
2408
2300
  }
2409
2301
  async function showCollisionDialog(host, root, server, shareId, error) {
2410
2302
  const debugInfo = JSON.stringify({ server, shareId, error, sessionID: root, plugin: "openshare", time: new Date().toISOString() }, null, 2);
@@ -2481,39 +2373,17 @@ function buildShareCommand(host, sessionID) {
2481
2373
  return;
2482
2374
  }
2483
2375
  const root = await resolveRoot(host.client, sid);
2484
- const bindings = await readBindings(host.client, root);
2485
- const binding = bindings[0];
2486
- dbgLog(`/share gate: sid=${sid} root=${root} bindings=${bindings.length}`);
2487
- if (bindings.some((b) => b.url && b.enabled !== false)) {
2488
- await showShareManager(host, sessionID);
2489
- host.ui.dialog.clear();
2376
+ const swept = await sweepBindings(host, sid, root);
2377
+ if (swept === null)
2490
2378
  return;
2491
- }
2492
- const result = await runShareFlow(host, sid);
2493
- if (result.message === "consent required") {
2494
- const yes = await host.ui.showConfirm({
2495
- title: "Share Session",
2496
- message: `Publish this session (and its subagent sessions) to your openshare worker?
2497
- The link lets anyone with it read the conversation.`,
2498
- confirmLabel: "Share"
2499
- });
2500
- if (!yes)
2501
- return;
2502
- try {
2503
- host.kv.set(CONSENT_KV_KEY, true);
2504
- dbgLog("consent kv.set ok");
2505
- } catch (e) {
2506
- dbgLog(`consent kv.set THREW: ${e?.message ?? e}`);
2507
- }
2508
- try {
2509
- finish(host, await confirmShare(host, root));
2510
- } catch (e) {
2511
- dbgLog(`confirmShare rejected: ${e?.stack ?? e}`);
2512
- finish(host, { ok: false, message: "Share failed (see debug log)." });
2513
- }
2379
+ const { all, goneShown } = swept;
2380
+ if (all.some((b) => b.url && b.enabled !== false)) {
2381
+ await settingsDialog(host, sid);
2514
2382
  return;
2515
2383
  }
2516
- finish(host, result);
2384
+ if (goneShown && all.length === 0)
2385
+ return;
2386
+ finish(host, await shareWithConsent(host, sid));
2517
2387
  }
2518
2388
  };
2519
2389
  }
@@ -2538,11 +2408,7 @@ function registerCommands(host, sessionID) {
2538
2408
  });
2539
2409
  }
2540
2410
 
2541
- // src/tui.tsx
2542
- init_sidebar();
2543
-
2544
2411
  // src/dialogs.tsx
2545
- init_sidebar();
2546
2412
  import { use as _$use } from "@opentui/solid";
2547
2413
  import { effect as _$effect2 } from "@opentui/solid";
2548
2414
  import { memo as _$memo } from "@opentui/solid";
@@ -3593,13 +3459,17 @@ function InfoDialog(props) {
3593
3459
  _$insertNode2(_el$44, _el$45);
3594
3460
  _$insert2(_el$45, () => action.title);
3595
3461
  _$effect2((_p$) => {
3596
- 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;
3597
3463
  _v$12 !== _p$.e && (_p$.e = _$setProp2(_el$43, "backgroundColor", _v$12, _p$.e));
3598
- _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));
3599
3467
  return _p$;
3600
3468
  }, {
3601
3469
  e: undefined,
3602
- t: undefined
3470
+ t: undefined,
3471
+ a: undefined,
3472
+ o: undefined
3603
3473
  });
3604
3474
  return _el$43;
3605
3475
  })()
@@ -3734,10 +3604,10 @@ function PromptDialog(props) {
3734
3604
  _$insert2(_el$47, () => text() || props.placeholder || "");
3735
3605
  _$insertNode2(_el$48, _$createTextNode(`\u258C`));
3736
3606
  _$effect2((_p$) => {
3737
- var _v$14 = theme().backgroundPanel, _v$15 = text() ? theme().text : theme().textMuted, _v$16 = theme().accent;
3738
- _v$14 !== _p$.e && (_p$.e = _$setProp2(_el$46, "backgroundColor", _v$14, _p$.e));
3739
- _v$15 !== _p$.t && (_p$.t = _$setProp2(_el$47, "fg", _v$15, _p$.t));
3740
- _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));
3741
3611
  return _p$;
3742
3612
  }, {
3743
3613
  e: undefined,
@@ -4018,9 +3888,9 @@ function SizePickerDialog(props) {
4018
3888
  _$insertNode2(_el$56, _el$57);
4019
3889
  _$insert2(_el$56, height, _el$57);
4020
3890
  _$effect2((_p$) => {
4021
- var _v$17 = theme().textMuted, _v$18 = theme().accent;
4022
- _v$17 !== _p$.e && (_p$.e = _$setProp2(_el$51, "fg", _v$17, _p$.e));
4023
- _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));
4024
3894
  return _p$;
4025
3895
  }, {
4026
3896
  e: undefined,
@@ -4053,9 +3923,9 @@ function SizePickerDialog(props) {
4053
3923
  _$insertNode2(_el$61, _el$63);
4054
3924
  _$insert2(_el$61, HEIGHT_PERCENT_MAX, _el$63);
4055
3925
  _$effect2((_p$) => {
4056
- var _v$19 = theme().textMuted, _v$20 = theme().textMuted;
4057
- _v$19 !== _p$.e && (_p$.e = _$setProp2(_el$59, "fg", _v$19, _p$.e));
4058
- _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));
4059
3929
  return _p$;
4060
3930
  }, {
4061
3931
  e: undefined,