@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/README.md +36 -37
- package/ci/README.md +9 -3
- package/ci/openshare.js +916 -199
- package/dist/binding-queue.d.ts +1 -0
- package/dist/binding-queue.js +22 -0
- package/dist/commands.js +41 -285
- package/dist/contract.d.ts +156 -0
- package/dist/contract.js +263 -0
- package/dist/core.js +888 -96
- package/dist/dialogs.js +39 -1
- package/dist/settings.js +141 -16
- package/dist/share-flow.d.ts +7 -2
- package/dist/share-flow.js +32 -17
- package/dist/sidebar.d.ts +3 -1
- package/dist/sidebar.js +3 -38
- package/dist/tui.js +286 -421
- package/dist/v2-server.d.ts +0 -27
- package/dist/v2-server.js +61 -121
- package/package.json +3 -1
- package/src/binding-queue.ts +26 -0
- package/src/commands.ts +41 -280
- package/src/contract.ts +320 -0
- package/src/core.ts +864 -99
- package/src/dialogs.tsx +45 -2
- package/src/settings.ts +133 -15
- package/src/share-flow.ts +33 -19
- package/src/sidebar.tsx +4 -45
- package/src/v2-server.ts +57 -110
package/dist/dialogs.js
CHANGED
|
@@ -435,6 +435,36 @@ export function MenuDialog(props) {
|
|
|
435
435
|
props.onDone(undefined);
|
|
436
436
|
} })] }));
|
|
437
437
|
}
|
|
438
|
+
/** Guard against stacked plugin dialogs. `dialog.replace` is documented as
|
|
439
|
+
* an atomic swap, but on some host builds a second replace while a dialog is
|
|
440
|
+
* still pending leaves BOTH renderers mounted - the reader sees two menus
|
|
441
|
+
* with two highlighted rows and keys go to a stale layer ("/share does
|
|
442
|
+
* nothing"). Cancelling the pending dialog and clearing before the next
|
|
443
|
+
* replace keeps exactly one dialog alive. The normal single-dialog path is
|
|
444
|
+
* untouched, so the esc-to-parent flicker fix stays intact. */
|
|
445
|
+
let pendingDialog;
|
|
446
|
+
function claimDialog(api) {
|
|
447
|
+
if (pendingDialog) {
|
|
448
|
+
try {
|
|
449
|
+
pendingDialog();
|
|
450
|
+
}
|
|
451
|
+
catch { }
|
|
452
|
+
try {
|
|
453
|
+
api.ui.dialog.clear();
|
|
454
|
+
}
|
|
455
|
+
catch { }
|
|
456
|
+
}
|
|
457
|
+
let released = false;
|
|
458
|
+
const release = () => {
|
|
459
|
+
if (released)
|
|
460
|
+
return;
|
|
461
|
+
released = true;
|
|
462
|
+
if (pendingDialog === release)
|
|
463
|
+
pendingDialog = undefined;
|
|
464
|
+
};
|
|
465
|
+
pendingDialog = release;
|
|
466
|
+
return release;
|
|
467
|
+
}
|
|
438
468
|
/** showMenuOnce, family discipline with the flicker fix (2026-10-19):
|
|
439
469
|
* done() does NOT clear the host stack — the next dialog's replace() is an
|
|
440
470
|
* atomic swap, so clearing first paints one empty frame (the esc-to-parent
|
|
@@ -443,10 +473,12 @@ export function MenuDialog(props) {
|
|
|
443
473
|
export function showMenuOnce(api, props) {
|
|
444
474
|
return new Promise((resolve) => {
|
|
445
475
|
let settled = false;
|
|
476
|
+
const release = claimDialog(api);
|
|
446
477
|
const done = (value, _clear = false) => {
|
|
447
478
|
if (settled)
|
|
448
479
|
return;
|
|
449
480
|
settled = true;
|
|
481
|
+
release();
|
|
450
482
|
dbgLog(`menu(${props.title}) -> ${value === undefined ? "cancel" : String(value)}`);
|
|
451
483
|
resolve(value);
|
|
452
484
|
};
|
|
@@ -457,10 +489,12 @@ export function showMenuOnce(api, props) {
|
|
|
457
489
|
export function showInfoDialog(api, props) {
|
|
458
490
|
return new Promise((resolve) => {
|
|
459
491
|
let settled = false;
|
|
492
|
+
const release = claimDialog(api);
|
|
460
493
|
const done = (value) => {
|
|
461
494
|
if (settled)
|
|
462
495
|
return;
|
|
463
496
|
settled = true;
|
|
497
|
+
release();
|
|
464
498
|
resolve(value);
|
|
465
499
|
};
|
|
466
500
|
api.ui.dialog.replace(() => _jsx(InfoDialog, { api: api, title: props.title, message: props.message, actions: props.actions, onDone: done }), () => done(undefined));
|
|
@@ -528,7 +562,7 @@ function InfoDialog(props) {
|
|
|
528
562
|
unregister();
|
|
529
563
|
popMode();
|
|
530
564
|
});
|
|
531
|
-
return (_jsxs(DialogFrame, { api: props.api, title: props.title, children: [_jsx("scrollbox", { maxHeight: bodyHeight(), ref: (element) => (scroll = element), children: _jsx("box", { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: _jsx(InfoLines, { api: props.api, lines: lines() }) }) }), _jsxs("box", { flexDirection: "row", justifyContent: "space-between", width: "100%", paddingTop: 1, children: [_jsx("box", { flexDirection: "row", gap: 1, children: _jsx(For, { each: actions().filter((action) => action.key && action.key !== "enter"), children: (action) => (_jsx("text", { fg: action.danger ? theme().error : theme().textMuted, children: `${action.key}: ${action.title}` })) }) }), _jsx("box", { flexDirection: "row", gap: 1, children: _jsx(For, { each: actions(), children: (action) => (_jsx("box", { paddingLeft: 2, paddingRight: 2, backgroundColor: action.danger ? theme().error : theme().primary, onMouseUp: () => props.onDone(action.value), children: _jsx("text", { fg: theme().background, children: _jsx("b", { children: action.title }) }) })) }) })] })] }));
|
|
565
|
+
return (_jsxs(DialogFrame, { api: props.api, title: props.title, children: [_jsx("scrollbox", { maxHeight: bodyHeight(), ref: (element) => (scroll = element), children: _jsx("box", { flexDirection: "column", paddingLeft: 1, paddingRight: 1, children: _jsx(InfoLines, { api: props.api, lines: lines() }) }) }), _jsxs("box", { flexDirection: "row", justifyContent: "space-between", width: "100%", paddingTop: 1, children: [_jsx("box", { flexDirection: "row", gap: 1, children: _jsx(For, { each: actions().filter((action) => action.key && action.key !== "enter"), children: (action) => (_jsx("text", { fg: action.danger ? theme().error : theme().textMuted, children: `${action.key}: ${action.title}` })) }) }), _jsx("box", { flexDirection: "row", gap: 1, children: _jsx(For, { each: actions(), children: (action) => (_jsx("box", { paddingLeft: 2, paddingRight: 2, backgroundColor: action.key === "enter" ? (action.danger ? theme().error : theme().primary) : undefined, border: action.key === "enter" ? undefined : true, borderColor: action.danger ? theme().error : theme().textMuted, onMouseUp: () => props.onDone(action.value), children: _jsx("text", { fg: action.key === "enter" ? theme().background : action.danger ? theme().error : theme().textMuted, children: _jsx("b", { children: action.title }) }) })) }) })] })] }));
|
|
532
566
|
}
|
|
533
567
|
// --- confirm / alert on the InfoDialog body -------------------------------------
|
|
534
568
|
export function showConfirmMenu(api, props) {
|
|
@@ -596,10 +630,12 @@ export function PromptDialog(props) {
|
|
|
596
630
|
export function showPromptOnce(api, props) {
|
|
597
631
|
return new Promise((resolve) => {
|
|
598
632
|
let settled = false;
|
|
633
|
+
const release = claimDialog(api);
|
|
599
634
|
const done = (value, _clear = false) => {
|
|
600
635
|
if (settled)
|
|
601
636
|
return;
|
|
602
637
|
settled = true;
|
|
638
|
+
release();
|
|
603
639
|
dbgLog(`prompt(${props.title}) -> ${value === undefined ? "cancel" : `len=${value.length}`}`);
|
|
604
640
|
resolve(value);
|
|
605
641
|
};
|
|
@@ -614,10 +650,12 @@ export function toMenuOptions(options) {
|
|
|
614
650
|
export function showSizePickerOnce(api, current) {
|
|
615
651
|
return new Promise((resolve) => {
|
|
616
652
|
let settled = false;
|
|
653
|
+
const release = claimDialog(api);
|
|
617
654
|
const done = (value) => {
|
|
618
655
|
if (settled)
|
|
619
656
|
return;
|
|
620
657
|
settled = true;
|
|
658
|
+
release();
|
|
621
659
|
resolve(value);
|
|
622
660
|
};
|
|
623
661
|
api.ui.dialog.replace(() => _jsx(SizePickerDialog, { api: api, current: current, onDone: done }), () => done(undefined));
|
package/dist/settings.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Sections: Connection, Sharing, Read auth, Advanced, This session.
|
|
7
7
|
import { configSourceMap, moduleDir, writeCandidates } from "./config.js";
|
|
8
|
-
import { adminClient, adminTokenFor,
|
|
8
|
+
import { adminClient, adminTokenFor, currentConfig, deleteServerEntry, disconnectAtServer, enrollAtServer, hostOf, isConfigured, normServerUrl, probeAuthMode, probeWorker, patchSessionBinding, readBinding, readBindings, resolveRoot, rotateAtServer, runUnshareFlow, saveConfig, saveServerEntry, serverEntries, setShareFlag, shareWithConsent, workerDeleteShare, } from "./share-flow.js";
|
|
9
9
|
import { DIALOG_WIDTH_COLUMNS, kvHeightPercentOf, kvWidthOf, nextDialogWidth, UI_HEIGHT_PERCENT_KV, UI_WIDTH_KV, clampHeightPercent } from "./ui-prefs.js";
|
|
10
10
|
/** description suffix: `[from <source>]` when a key is overridden by a
|
|
11
11
|
* file/env higher in the chain than the write target. */
|
|
@@ -903,26 +903,70 @@ export async function settingsDialog(host, startAtSession) {
|
|
|
903
903
|
const sessionRows = async () => {
|
|
904
904
|
if (!startAtSession)
|
|
905
905
|
return [];
|
|
906
|
-
const
|
|
907
|
-
const
|
|
908
|
-
|
|
909
|
-
|
|
906
|
+
const root = await resolveRoot(host.client, startAtSession);
|
|
907
|
+
const bindings = await readBindings(host.client, root);
|
|
908
|
+
const live = bindings.filter((b) => b.url && b.enabled !== false);
|
|
909
|
+
const rows = [];
|
|
910
|
+
if (live.length === 0) {
|
|
911
|
+
rows.push({
|
|
912
|
+
title: "Share this session",
|
|
910
913
|
value: "session:share",
|
|
911
|
-
description:
|
|
912
|
-
help:
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
914
|
+
description: "set the share flag; the server plugin creates the page",
|
|
915
|
+
help: "Publishes this session (and its subagent sessions) to the active server. The full history lands in the page — sharing mid-session is first-class, nothing is missed. You will be asked to confirm before anything is published.",
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
else if (live.length === 1) {
|
|
919
|
+
const b = live[0];
|
|
920
|
+
rows.push({
|
|
921
|
+
title: "Copy share link",
|
|
922
|
+
value: "session:copy",
|
|
923
|
+
description: "copy the share URL to the clipboard",
|
|
924
|
+
help: `Copies the share URL to the clipboard.\n\nURL: ${b.url}\nShare id: ${b.id ?? "?"}`,
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
else {
|
|
928
|
+
for (const b of live) {
|
|
929
|
+
rows.push({
|
|
930
|
+
title: `Copy link — ${hostOf(b.url || b.server)}`,
|
|
931
|
+
value: `session:copy:${b.server}`,
|
|
932
|
+
description: b.url,
|
|
933
|
+
help: `Copies this server's share URL to the clipboard.\n\nURL: ${b.url}\nShare id: ${b.id}\n\nEach server carries an independent page of the same session.`,
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
rows.push({
|
|
937
|
+
title: "Copy all links",
|
|
938
|
+
value: "session:copyall",
|
|
939
|
+
description: `${live.length} servers`,
|
|
940
|
+
help: `Copies every bound server's share URL.\n\n${live.map((b) => `• ${b.url}`).join("\n")}`,
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
if (live.length) {
|
|
918
944
|
rows.push({
|
|
919
945
|
title: "Stop sharing",
|
|
920
946
|
value: "session:unshare",
|
|
921
|
-
description:
|
|
947
|
+
description: live.length > 1 ? `delete every page (${live.length} servers), or leave snapshots` : "delete the page, or leave a snapshot",
|
|
922
948
|
danger: true,
|
|
923
|
-
help: `Choose per run: DELETE every page server-side (links stop working — final "gone" state), or LEAVE AS-IS (stop updating; pages stay readable until their TTL — a frozen snapshot).\n\nURLs:\n${
|
|
949
|
+
help: `Choose per run: DELETE every page server-side (links stop working — final "gone" state), or LEAVE AS-IS (stop updating; pages stay readable until their TTL — a frozen snapshot).\n\nURLs:\n${live.map((b) => b.url).join("\n")}\n\nSharing again later always creates fresh page(s) with new URLs.`,
|
|
924
950
|
});
|
|
951
|
+
if (live.length > 1) {
|
|
952
|
+
rows.push({
|
|
953
|
+
title: "Stop on one server…",
|
|
954
|
+
value: "session:unshare:one",
|
|
955
|
+
description: "delete or snapshot a single server's page",
|
|
956
|
+
help: `Per-server stop: pick the server, then delete its page or leave it as a read-only snapshot. Other servers are unaffected.\n\nBound servers:\n${live.map((b) => `• ${b.server} — ${b.url}`).join("\n")}`,
|
|
957
|
+
});
|
|
958
|
+
const otherServers = serverEntries().filter((s) => !live.some((b) => b.server === s.url));
|
|
959
|
+
if (otherServers.length) {
|
|
960
|
+
rows.push({
|
|
961
|
+
title: "Share to another server…",
|
|
962
|
+
value: "session:more",
|
|
963
|
+
description: otherServers.map((s) => hostOf(s.url)).join(", "),
|
|
964
|
+
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.\n\nAvailable:\n${otherServers.map((s) => `• ${s.url}`).join("\n")}`,
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
}
|
|
925
968
|
}
|
|
969
|
+
const binding = bindings[0];
|
|
926
970
|
rows.push({
|
|
927
971
|
title: "Session TTL override (days)",
|
|
928
972
|
value: "session:ttl",
|
|
@@ -1033,16 +1077,97 @@ export async function settingsDialog(host, startAtSession) {
|
|
|
1033
1077
|
}
|
|
1034
1078
|
if (choice.startsWith("session:") && startAtSession) {
|
|
1035
1079
|
const root = await resolveRoot(host.client, startAtSession);
|
|
1036
|
-
const
|
|
1080
|
+
const all = await readBindings(host.client, root);
|
|
1081
|
+
const live = all.filter((b) => b.url && b.enabled !== false);
|
|
1082
|
+
const current = all[0];
|
|
1037
1083
|
if (choice === "session:share") {
|
|
1038
1084
|
if (!isConfigured(cfg)) {
|
|
1039
1085
|
await showInfo(host.ui, { title: "Not configured", message: "Connect a server (Servers group) first, then share." });
|
|
1040
1086
|
return;
|
|
1041
1087
|
}
|
|
1042
|
-
const result = await
|
|
1088
|
+
const result = await shareWithConsent(host, startAtSession);
|
|
1043
1089
|
host.ui.toast({ variant: result.ok ? "success" : "error", title: "OpenShare", message: result.message, duration: result.ok ? 8000 : 15000 });
|
|
1044
1090
|
return;
|
|
1045
1091
|
}
|
|
1092
|
+
if (choice === "session:copy" && live[0]) {
|
|
1093
|
+
const okw = await host.clipboard.write(live[0].url);
|
|
1094
|
+
host.ui.toast({ variant: okw ? "success" : "error", title: "OpenShare", message: okw ? `Share URL copied: ${live[0].url}` : `Clipboard failed — ${live[0].url}` });
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (choice.startsWith("session:copy:")) {
|
|
1098
|
+
const b = live.find((x) => `session:copy:${x.server}` === choice);
|
|
1099
|
+
if (b) {
|
|
1100
|
+
const okw = await host.clipboard.write(b.url);
|
|
1101
|
+
host.ui.toast({ variant: okw ? "success" : "error", title: "OpenShare", message: okw ? `Share URL copied: ${b.url}` : `Clipboard failed — ${b.url}` });
|
|
1102
|
+
}
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
if (choice === "session:copyall") {
|
|
1106
|
+
const joined = live.map((b) => b.url).filter(Boolean).join("\n");
|
|
1107
|
+
const okw = await host.clipboard.write(joined);
|
|
1108
|
+
host.ui.toast({ variant: okw ? "success" : "error", title: "OpenShare", message: okw ? `${live.length} share URL(s) copied` : "Clipboard failed" });
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
if (choice === "session:unshare:one") {
|
|
1112
|
+
const pick = await showSelect(host.ui, {
|
|
1113
|
+
title: "Stop on which server",
|
|
1114
|
+
options: [...live.map((b) => ({ title: hostOf(b.url || b.server), value: b.server, description: b.url })), { title: "Cancel", value: "__cancel" }],
|
|
1115
|
+
});
|
|
1116
|
+
if (!pick || pick === "__cancel")
|
|
1117
|
+
return;
|
|
1118
|
+
const target = live.find((b) => b.server === pick);
|
|
1119
|
+
if (!target)
|
|
1120
|
+
return;
|
|
1121
|
+
const how = await showSelect(host.ui, {
|
|
1122
|
+
title: `Stop sharing — ${hostOf(target.url || target.server)}`,
|
|
1123
|
+
options: [
|
|
1124
|
+
{ title: "Delete the page", value: "delete", description: "this link stops working for everyone", danger: true },
|
|
1125
|
+
{ title: "Leave as-is (snapshot)", value: "leave", description: "stop updating; the page stays until its TTL" },
|
|
1126
|
+
{ title: "Cancel", value: "__cancel" },
|
|
1127
|
+
],
|
|
1128
|
+
});
|
|
1129
|
+
if (!how || how === "__cancel")
|
|
1130
|
+
return;
|
|
1131
|
+
if (how === "leave") {
|
|
1132
|
+
await patchSessionBinding(host.client, root, (servers) => {
|
|
1133
|
+
delete servers[target.server];
|
|
1134
|
+
servers[target.server] = { optedOut: true };
|
|
1135
|
+
});
|
|
1136
|
+
host.ui.toast({ variant: "success", title: "OpenShare", message: `${hostOf(target.url || target.server)} left as-is — a snapshot until its TTL.` });
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
const targetEntry = all.find((b) => b.server === target.server);
|
|
1140
|
+
if (!targetEntry?.secret) {
|
|
1141
|
+
host.ui.toast({ variant: "error", title: "OpenShare", message: "No secret for this page — cannot delete it server-side; binding cleared." });
|
|
1142
|
+
await patchSessionBinding(host.client, root, (servers) => {
|
|
1143
|
+
delete servers[target.server];
|
|
1144
|
+
});
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
const gone = await workerDeleteShare(target.server, target.id, targetEntry.secret);
|
|
1148
|
+
await patchSessionBinding(host.client, root, (servers) => {
|
|
1149
|
+
delete servers[target.server];
|
|
1150
|
+
});
|
|
1151
|
+
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) — binding cleared.` });
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
if (choice === "session:more") {
|
|
1155
|
+
const otherServers = serverEntries().filter((s) => !live.some((b) => b.server === s.url));
|
|
1156
|
+
if (!otherServers.length)
|
|
1157
|
+
return;
|
|
1158
|
+
const pick = await showSelect(host.ui, {
|
|
1159
|
+
title: "Share to another server",
|
|
1160
|
+
options: [...otherServers.map((s) => ({ title: hostOf(s.url), value: s.url, description: s.entry.label })), { title: "Cancel", value: "__cancel" }],
|
|
1161
|
+
});
|
|
1162
|
+
if (!pick || pick === "__cancel")
|
|
1163
|
+
return;
|
|
1164
|
+
for (const s of serverEntries())
|
|
1165
|
+
if (s.url !== pick)
|
|
1166
|
+
saveServerEntry(s.url, { active: undefined });
|
|
1167
|
+
saveServerEntry(pick, { active: true });
|
|
1168
|
+
host.ui.toast({ variant: "success", title: "OpenShare", message: `${hostOf(pick)} activated — this session mirrors there within seconds.` });
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1046
1171
|
if (choice === "session:unshare") {
|
|
1047
1172
|
const how = await showSelect(host.ui, {
|
|
1048
1173
|
title: "Stop sharing",
|
package/dist/share-flow.d.ts
CHANGED
|
@@ -9,7 +9,6 @@ export type SessionBinding = {
|
|
|
9
9
|
enabled?: boolean;
|
|
10
10
|
opts?: SessionOpts;
|
|
11
11
|
};
|
|
12
|
-
export declare const CONSENT_KV_KEY = "openshare_consent";
|
|
13
12
|
export declare function pluginConfigPaths(baseDir?: string): string[];
|
|
14
13
|
export declare function currentConfig(baseDir?: string): ShareConfig;
|
|
15
14
|
/** Writes the config file. Target rule (owner-directed): the global
|
|
@@ -281,7 +280,13 @@ export type FlowResult = {
|
|
|
281
280
|
url?: string;
|
|
282
281
|
unconfigured?: boolean;
|
|
283
282
|
};
|
|
284
|
-
|
|
283
|
+
/** Consent gate: EVERY share creation asks explicitly (per owner
|
|
284
|
+
* directive — no remembered consent, no silent creates). */
|
|
285
|
+
export declare function requestShareConsent(host: ShareHostApi): Promise<boolean>;
|
|
286
|
+
/** runShareFlow with the consent gate applied: share copies the link,
|
|
287
|
+
* unshared asks consent once, then creates. Every create path routes
|
|
288
|
+
* through here (/share, settings, gone-again). */
|
|
289
|
+
export declare function shareWithConsent(host: ShareHostApi, sessionID: string | undefined): Promise<FlowResult>;
|
|
285
290
|
/** Post-consent share: set the flag, wait for the URL, copy it. */
|
|
286
291
|
export declare function confirmShare(host: ShareHostApi, rootSessionID: string): Promise<FlowResult>;
|
|
287
292
|
export declare function runUnshareFlow(host: ShareHostApi, sessionID: string | undefined): Promise<FlowResult>;
|
package/dist/share-flow.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
17
17
|
import { loadConfig, moduleDir, configPaths, configHomePath, resolveServer, saveServerEntry, deleteServerEntry, METADATA_KEY } from "./config.js";
|
|
18
18
|
import { dbg, dbgLog } from "./sidebar.js";
|
|
19
|
-
|
|
19
|
+
import { queueBindingWrite } from "./binding-queue.js";
|
|
20
20
|
export function pluginConfigPaths(baseDir = moduleDir()) {
|
|
21
21
|
return configPaths(baseDir);
|
|
22
22
|
}
|
|
@@ -155,17 +155,22 @@ export function hostOf(url) {
|
|
|
155
155
|
* server actions: opt-out entries, collision resolution). Reads-modifies-
|
|
156
156
|
* writes the whole metadata to keep foreign keys intact. */
|
|
157
157
|
export async function patchSessionBinding(client, sid, mutator) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
158
|
+
// the whole read-modify-write runs inside the shared per-session queue:
|
|
159
|
+
// the engine patches the same metadata (binding refresh) and interleaved
|
|
160
|
+
// RMWs silently lose one side's update (see binding-queue.ts)
|
|
161
|
+
return queueBindingWrite(sid, async () => {
|
|
162
|
+
const info = await getSession(client, sid);
|
|
163
|
+
if (!info)
|
|
164
|
+
return false;
|
|
165
|
+
const metadata = { ...(info.metadata ?? {}) };
|
|
166
|
+
const raw = info.metadata?.[METADATA_KEY];
|
|
167
|
+
const binding = raw && typeof raw === "object" ? { ...raw } : { v: 2, servers: {} };
|
|
168
|
+
const servers = binding.servers && typeof binding.servers === "object" ? { ...binding.servers } : {};
|
|
169
|
+
mutator(servers, binding);
|
|
170
|
+
binding.servers = servers;
|
|
171
|
+
metadata[METADATA_KEY] = binding;
|
|
172
|
+
return patchMetadata(client, sid, metadata);
|
|
173
|
+
});
|
|
169
174
|
}
|
|
170
175
|
/** The display/primary binding: the active server's entry when bound
|
|
171
176
|
* there, else the first live entry (single-server configs are unchanged). */
|
|
@@ -446,7 +451,19 @@ export async function waitForBinding(client, sid, timeoutMs = 25000) {
|
|
|
446
451
|
}
|
|
447
452
|
return undefined;
|
|
448
453
|
}
|
|
449
|
-
|
|
454
|
+
/** Consent gate: EVERY share creation asks explicitly (per owner
|
|
455
|
+
* directive — no remembered consent, no silent creates). */
|
|
456
|
+
export async function requestShareConsent(host) {
|
|
457
|
+
return host.ui.showConfirm({
|
|
458
|
+
title: "Share Session",
|
|
459
|
+
message: "Publish this session (and its subagent sessions) to your openshare worker?\nThe link lets anyone with it read the conversation.",
|
|
460
|
+
confirmLabel: "Share",
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
/** runShareFlow with the consent gate applied: share copies the link,
|
|
464
|
+
* unshared asks consent once, then creates. Every create path routes
|
|
465
|
+
* through here (/share, settings, gone-again). */
|
|
466
|
+
export async function shareWithConsent(host, sessionID) {
|
|
450
467
|
if (!sessionID)
|
|
451
468
|
return { ok: false, message: "No active session to share." };
|
|
452
469
|
const cfg = currentConfig();
|
|
@@ -460,10 +477,8 @@ export async function runShareFlow(host, sessionID) {
|
|
|
460
477
|
await host.clipboard.write(existing.url);
|
|
461
478
|
return { ok: true, message: `Share URL copied to clipboard: ${existing.url}`, url: existing.url };
|
|
462
479
|
}
|
|
463
|
-
if (!
|
|
464
|
-
|
|
465
|
-
return { ok: false, message: "consent required" };
|
|
466
|
-
}
|
|
480
|
+
if (!(await requestShareConsent(host)))
|
|
481
|
+
return { ok: false, message: "Share cancelled." };
|
|
467
482
|
return confirmShare(host, root);
|
|
468
483
|
}
|
|
469
484
|
/** Post-consent share: set the flag, wait for the URL, copy it. */
|
package/dist/sidebar.d.ts
CHANGED
|
@@ -3,7 +3,9 @@ import type { JSX } from "@opentui/solid";
|
|
|
3
3
|
import type { TuiPluginApi, TuiSlotContext } from "@opencode-ai/plugin/tui";
|
|
4
4
|
/** value -> `kind:preview` so the log shows TYPES (the crash was a type bug) */
|
|
5
5
|
export declare function dbg(v: unknown, depth?: number): string;
|
|
6
|
-
|
|
6
|
+
/** no-op: the TUI debug log file was a crash-hunt aid and must not write
|
|
7
|
+
* to temp any more (owner request 2026-10-21) */
|
|
8
|
+
export declare function dbgLog(_line: string): void;
|
|
7
9
|
type SidebarTitleProps = {
|
|
8
10
|
session_id: string;
|
|
9
11
|
title: string;
|
package/dist/sidebar.js
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/solid/jsx-runtime";
|
|
2
2
|
import { bindingsOf, bindingOf, verifyShare } from "./share-flow.js";
|
|
3
|
-
import { appendFileSync } from "node:fs";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
const DBG_FILE = join(tmpdir(), "openshare-tui-debug.log");
|
|
7
3
|
/** value -> `kind:preview` so the log shows TYPES (the crash was a type bug) */
|
|
8
4
|
export function dbg(v, depth = 0) {
|
|
9
5
|
const t = typeof v;
|
|
@@ -29,34 +25,9 @@ export function dbg(v, depth = 0) {
|
|
|
29
25
|
return "obj:?}";
|
|
30
26
|
}
|
|
31
27
|
}
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return dbg(el);
|
|
36
|
-
if (Array.isArray(el))
|
|
37
|
-
return `[${el.map((x) => dbgEl(x, depth)).join(",")}]`;
|
|
38
|
-
if (typeof el === "function")
|
|
39
|
-
return "fn-node";
|
|
40
|
-
if (depth >= 4)
|
|
41
|
-
return "…";
|
|
42
|
-
try {
|
|
43
|
-
const rec = el;
|
|
44
|
-
const t = dbg(rec.t ?? rec.type, 2);
|
|
45
|
-
const kids = rec.children;
|
|
46
|
-
const props = Object.keys(rec).filter((k) => !["t", "type", "children"].includes(k));
|
|
47
|
-
return `<${t} ${props.join(" ")}>${kids === undefined ? "" : dbgEl(kids, depth + 1)}</${t}>`;
|
|
48
|
-
}
|
|
49
|
-
catch (e) {
|
|
50
|
-
return `el?-${dbg(e)}`;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
export function dbgLog(line) {
|
|
54
|
-
try {
|
|
55
|
-
appendFileSync(DBG_FILE, `${new Date().toISOString()} ${line}\n`);
|
|
56
|
-
}
|
|
57
|
-
catch { }
|
|
58
|
-
}
|
|
59
|
-
let renderSeq = 0;
|
|
28
|
+
/** no-op: the TUI debug log file was a crash-hunt aid and must not write
|
|
29
|
+
* to temp any more (owner request 2026-10-21) */
|
|
30
|
+
export function dbgLog(_line) { }
|
|
60
31
|
const GONE_TTL_MS = 60_000;
|
|
61
32
|
const goneCache = new Map();
|
|
62
33
|
function goneVerdict(sid, server, shareId) {
|
|
@@ -70,13 +41,10 @@ function goneVerdict(sid, server, shareId) {
|
|
|
70
41
|
return cached;
|
|
71
42
|
}
|
|
72
43
|
export function createSidebarSlot(api, opts) {
|
|
73
|
-
dbgLog(`createSidebarSlot called; api.state=${!!api.state} api.theme=${!!api.theme} api.version=${dbg(api.app?.version)}`);
|
|
74
44
|
return {
|
|
75
45
|
slots: {
|
|
76
46
|
sidebar_title: (ctx, props) => {
|
|
77
|
-
const seq = ++renderSeq;
|
|
78
47
|
try {
|
|
79
|
-
dbgLog(`render#${seq} ctx=${dbg(ctx)} props=${dbg(props)}`);
|
|
80
48
|
const sid = typeof props?.session_id === "string" ? props.session_id : "";
|
|
81
49
|
const title = String(props?.title ?? "") || "Session";
|
|
82
50
|
const session = sid ? api.state.session.get(sid) : undefined;
|
|
@@ -90,7 +58,6 @@ export function createSidebarSlot(api, opts) {
|
|
|
90
58
|
const anyGone = verdicts.some((v) => v?.status === "gone");
|
|
91
59
|
const multi = liveCount > 1 ? `·${liveCount} ` : "";
|
|
92
60
|
const theme = ctx?.theme?.current ?? api.theme.current;
|
|
93
|
-
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)}`);
|
|
94
61
|
// chip on its own line under the title (takes the blank line that
|
|
95
62
|
// separated the title block from the context blocks); clicking it
|
|
96
63
|
// copies the share link — proven pattern: onMouseDown on a box.
|
|
@@ -131,11 +98,9 @@ export function createSidebarSlot(api, opts) {
|
|
|
131
98
|
: status === "live"
|
|
132
99
|
? `◉ re:share ${multi}— click to copy link`
|
|
133
100
|
: "◉ re:share (dormant)" }) })) : null] }));
|
|
134
|
-
dbgLog(`render#${seq} RETURNS ${dbgEl(el)}`);
|
|
135
101
|
return el;
|
|
136
102
|
}
|
|
137
103
|
catch (error) {
|
|
138
|
-
dbgLog(`render#${seq} THREW ${error?.stack ?? dbg(error)} — rethrowing`);
|
|
139
104
|
throw error;
|
|
140
105
|
}
|
|
141
106
|
},
|