@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function queueBindingWrite<T>(sid: string, fn: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Serialize session-metadata binding writes.
|
|
2
|
+
*
|
|
3
|
+
* Two writers read-modify-write `metadata.openshare` on the same session:
|
|
4
|
+
* the engine (`writeMetadata` in core.ts — preserves foreign keys, sets
|
|
5
|
+
* the servers map) and the TUI (`patchSessionBinding` in share-flow.ts —
|
|
6
|
+
* opt-outs, collision resolution). Their PATCHes interleave at the host:
|
|
7
|
+
* writer A reads at T0, writer B reads at T1, A PATCHes at T2, B PATCHes
|
|
8
|
+
* at T3 — A's change is silently lost (live-caught class: a TUI opt-out
|
|
9
|
+
* erased by the engine's binding refresh).
|
|
10
|
+
*
|
|
11
|
+
* Both route their whole read-modify-write through this per-session FIFO.
|
|
12
|
+
* The queue never breaks on errors; entries drop when the process exits.
|
|
13
|
+
*/
|
|
14
|
+
const queues = new Map();
|
|
15
|
+
export function queueBindingWrite(sid, fn) {
|
|
16
|
+
const prev = queues.get(sid) ?? Promise.resolve();
|
|
17
|
+
// run regardless of the previous slot's outcome — one failed write must
|
|
18
|
+
// not wedge the session's binding updates forever
|
|
19
|
+
const run = prev.then(fn, fn);
|
|
20
|
+
queues.set(sid, run.catch(() => { }));
|
|
21
|
+
return run;
|
|
22
|
+
}
|
package/dist/commands.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// OpenShare command registration. ONE user-facing /share command shadows
|
|
2
2
|
// the deprecated built-in (opncd.ai) by registering the same command name
|
|
3
|
-
// on a plugin keymap layer
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// on a plugin keymap layer. Running it on a shared session opens the SAME
|
|
4
|
+
// session menu as /openshare → This session (copy / stop / TTL / read
|
|
5
|
+
// auth — one surface, no duplicated manager); on an unshared session it
|
|
6
|
+
// asks consent and creates the share. /openshare opens settings.
|
|
6
7
|
//
|
|
7
8
|
// LAYER SHAPE (live-caught 2026-10-19): the v1 host keymap gates layers
|
|
8
9
|
// that declare `mode` on the mode stack (resting mode is "base"), so a
|
|
@@ -12,7 +13,7 @@
|
|
|
12
13
|
// is exactly what the proven family plugins use: {commands, bindings},
|
|
13
14
|
// commands with only defined fields, category "OpenShare" (own group,
|
|
14
15
|
// deliberately NOT joined into the shared family palette category).
|
|
15
|
-
import {
|
|
16
|
+
import { clearGoneBindingEntries, currentConfig, hostOf, isConfigured, patchSessionBinding, readBindings, resolveRoot, shareWithConsent, verifyShare, } from "./share-flow.js";
|
|
16
17
|
import { onboardingDialog, settingsDialog } from "./settings.js";
|
|
17
18
|
import { dbgLog } from "./sidebar.js";
|
|
18
19
|
export const PALETTE_CATEGORY = "OpenShare";
|
|
@@ -31,30 +32,26 @@ function finish(host, result) {
|
|
|
31
32
|
duration: result.ok ? 8000 : 15000,
|
|
32
33
|
});
|
|
33
34
|
}
|
|
34
|
-
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
async function
|
|
40
|
-
const sid = sessionID();
|
|
41
|
-
if (!sid)
|
|
42
|
-
return;
|
|
43
|
-
const root = await resolveRoot(host.client, sid);
|
|
35
|
+
/** Shared pre-menu pass: collision check + liveness sweep. Bindings whose
|
|
36
|
+
* page died server-side (purge/TTL/delete) are auto-cleared and surfaced
|
|
37
|
+
* in a gone modal offering a re-share. Returns null when a dialog flow
|
|
38
|
+
* took over (collision / re-share) and already finished; otherwise the
|
|
39
|
+
* surviving bindings and whether any gone modal was shown. */
|
|
40
|
+
async function sweepBindings(host, sid, root) {
|
|
44
41
|
let all = await readBindings(host.client, root);
|
|
42
|
+
let goneShown = false;
|
|
45
43
|
// collision first: the engine marked a server's credentials as rejected
|
|
46
44
|
const collided = all.find((b) => b.collided);
|
|
47
|
-
if (collided)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// modal says what happened and offers to re-share. Network errors are
|
|
52
|
-
// "unknown" and never treated as gone.
|
|
45
|
+
if (collided) {
|
|
46
|
+
await showCollisionDialog(host, root, collided.server, collided.id, collided.collided ?? "");
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
53
49
|
const live = all.filter((b) => b.url && b.enabled !== false && b.id);
|
|
54
50
|
if (live.length) {
|
|
55
51
|
const verdicts = await Promise.all(live.map((b) => verifyShare(b.server, b.id)));
|
|
56
52
|
const gone = live.filter((b, i) => verdicts[i] === "gone");
|
|
57
53
|
if (gone.length) {
|
|
54
|
+
goneShown = true;
|
|
58
55
|
await clearGoneBindingEntries(host, root, gone.map((b) => b.server));
|
|
59
56
|
all = all.filter((b) => !gone.some((g) => g.server === b.server));
|
|
60
57
|
const list = gone.map((b) => `• ${hostOf(b.url || b.server)} — ${b.url}`).join("\n");
|
|
@@ -67,242 +64,15 @@ async function showShareManager(host, sessionID) {
|
|
|
67
64
|
],
|
|
68
65
|
});
|
|
69
66
|
if (choice === "again") {
|
|
70
|
-
finish(host, await
|
|
71
|
-
return;
|
|
67
|
+
finish(host, await shareWithConsent(host, sid));
|
|
68
|
+
return null;
|
|
72
69
|
}
|
|
73
70
|
if (all.length === 0)
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
if (all.length === 0)
|
|
78
|
-
return;
|
|
79
|
-
const otherServers = serverEntries().filter((s) => !all.some((b) => b.server === s.url));
|
|
80
|
-
if (all.length === 1 && otherServers.length === 0) {
|
|
81
|
-
// the classic single-server menu
|
|
82
|
-
const b = all[0];
|
|
83
|
-
void host.ui
|
|
84
|
-
.showMenu({
|
|
85
|
-
title: "Session is shared",
|
|
86
|
-
hint: b.url,
|
|
87
|
-
options: [
|
|
88
|
-
{
|
|
89
|
-
title: "Copy share link",
|
|
90
|
-
value: "copy",
|
|
91
|
-
description: "copy the share URL to the clipboard",
|
|
92
|
-
help: `Copies the share URL to the clipboard (navigator + OSC52 fallback).\n\nURL: ${b.url}\nServer: ${b.server}\nShare id: ${b.id}\n\nAnyone with the link can read the page (plus the login, if read auth is set).`,
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
title: "Stop sharing",
|
|
96
|
-
value: "stop",
|
|
97
|
-
description: "Delete the share page — the link stops working for everyone",
|
|
98
|
-
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 — sharing again creates a NEW page with a new URL.`,
|
|
99
|
-
},
|
|
100
|
-
{ title: "Cancel", value: "cancel" },
|
|
101
|
-
],
|
|
102
|
-
})
|
|
103
|
-
.then(async (choice) => {
|
|
104
|
-
if (choice === "copy") {
|
|
105
|
-
const ok = await host.clipboard.write(b.url);
|
|
106
|
-
finish(host, ok ? { ok: true, message: `Share URL copied to clipboard: ${b.url}`, url: b.url } : { ok: false, message: `Could not write the clipboard — link: ${b.url}`, url: b.url });
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
if (choice === "stop") {
|
|
110
|
-
const how = await host.ui.showMenu({
|
|
111
|
-
title: "Stop sharing",
|
|
112
|
-
hint: b.url,
|
|
113
|
-
options: [
|
|
114
|
-
{
|
|
115
|
-
title: "Delete the page",
|
|
116
|
-
value: "delete",
|
|
117
|
-
description: "the link stops working for everyone",
|
|
118
|
-
danger: true,
|
|
119
|
-
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.`,
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
title: "Leave as-is (snapshot)",
|
|
123
|
-
value: "leave",
|
|
124
|
-
description: "stop updating; the page stays until its TTL",
|
|
125
|
-
help: `Stops mirroring WITHOUT deleting anything — viewers keep reading the snapshot until the share's TTL expires. Useful for handing someone a frozen copy while you keep working.`,
|
|
126
|
-
},
|
|
127
|
-
{ title: "Cancel", value: "cancel" },
|
|
128
|
-
],
|
|
129
|
-
});
|
|
130
|
-
if (!how || how === "cancel")
|
|
131
|
-
return;
|
|
132
|
-
if (how === "leave") {
|
|
133
|
-
await patchSessionBinding(host.client, root, (servers) => {
|
|
134
|
-
delete servers[b.server];
|
|
135
|
-
servers[b.server] = { optedOut: true };
|
|
136
|
-
});
|
|
137
|
-
finish(host, { ok: true, message: `${hostOf(b.url || b.server)} left as-is — a snapshot until its TTL.` });
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
const yes = await host.ui.showConfirm({
|
|
141
|
-
title: "Delete the share page",
|
|
142
|
-
message: `Delete the page on ${hostOf(b.url || b.server)}?\nThe link stops working for everyone.`,
|
|
143
|
-
confirmLabel: "Delete",
|
|
144
|
-
danger: true,
|
|
145
|
-
});
|
|
146
|
-
if (!yes) {
|
|
147
|
-
host.ui.dialog.clear();
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
finish(host, await runUnshareFlow(host, sessionID()));
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
// fan-out menu
|
|
156
|
-
const options = all.map((b) => ({
|
|
157
|
-
title: `Copy link — ${hostOf(b.url || b.server)}`,
|
|
158
|
-
value: `copy:${b.server}`,
|
|
159
|
-
description: b.url,
|
|
160
|
-
help: `Copies this server's share URL to the clipboard.\n\nURL: ${b.url}\nServer: ${b.server}\nShare id: ${b.id}\n\nEach server carries an independent page of the same session.`,
|
|
161
|
-
}));
|
|
162
|
-
options.push({
|
|
163
|
-
title: "Manage per server…",
|
|
164
|
-
value: "manage",
|
|
165
|
-
description: `${all.length} servers — stop one, leave a snapshot`,
|
|
166
|
-
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.\n\nBound servers:\n${all.map((b) => `• ${b.server} — ${b.url}`).join("\n")}`,
|
|
167
|
-
});
|
|
168
|
-
if (otherServers.length) {
|
|
169
|
-
options.push({
|
|
170
|
-
title: "Share to another server…",
|
|
171
|
-
value: "more",
|
|
172
|
-
description: otherServers.map((s) => hostOf(s.url)).join(", "),
|
|
173
|
-
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")}`,
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
options.push({
|
|
177
|
-
title: "Stop sharing (all)",
|
|
178
|
-
value: "stop-all",
|
|
179
|
-
description: "delete every server's page, or leave snapshots",
|
|
180
|
-
danger: true,
|
|
181
|
-
help: `Deletes the page on all ${all.length} servers (or leaves them as read-only snapshots — your choice on the next screen) and clears the share flag. Sharing again creates fresh pages.`,
|
|
182
|
-
});
|
|
183
|
-
options.push({ title: "Cancel", value: "cancel" });
|
|
184
|
-
const choice = await host.ui.showMenu({ title: `Session is shared · ${all.length} servers`, options });
|
|
185
|
-
if (!choice || choice === "cancel")
|
|
186
|
-
return;
|
|
187
|
-
if (choice.startsWith("copy:")) {
|
|
188
|
-
const b = all.find((x) => `copy:${x.server}` === choice);
|
|
189
|
-
if (b) {
|
|
190
|
-
const ok = await host.clipboard.write(b.url);
|
|
191
|
-
finish(host, ok ? { ok: true, message: `Share URL copied: ${b.url}`, url: b.url } : { ok: false, message: `Clipboard failed — ${b.url}`, url: b.url });
|
|
192
|
-
}
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
if (choice === "stop-all") {
|
|
196
|
-
const how = await host.ui.showMenu({
|
|
197
|
-
title: `Stop sharing · ${all.length} servers`,
|
|
198
|
-
options: [
|
|
199
|
-
{ 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.` },
|
|
200
|
-
{ title: "Leave all as-is (snapshots)", value: "leave", description: "stop updating; pages stay until their TTLs", help: `Stops mirroring everywhere without deleting anything — every page stays readable as a frozen snapshot until its TTL expires.` },
|
|
201
|
-
{ title: "Cancel", value: "cancel" },
|
|
202
|
-
],
|
|
203
|
-
});
|
|
204
|
-
if (!how || how === "cancel")
|
|
205
|
-
return;
|
|
206
|
-
if (how === "leave") {
|
|
207
|
-
await patchSessionBinding(host.client, root, (servers) => {
|
|
208
|
-
for (const b of all) {
|
|
209
|
-
delete servers[b.server];
|
|
210
|
-
servers[b.server] = { optedOut: true };
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
finish(host, { ok: true, message: `${all.length} server(s) left as-is — snapshots until their TTLs.` });
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
const yes = await host.ui.showConfirm({
|
|
217
|
-
title: "Stop sharing",
|
|
218
|
-
message: `Delete the share pages on all ${all.length} servers? The links stop working for everyone.`,
|
|
219
|
-
confirmLabel: "Stop all",
|
|
220
|
-
danger: true,
|
|
221
|
-
});
|
|
222
|
-
if (!yes)
|
|
223
|
-
return;
|
|
224
|
-
finish(host, await runUnshareFlow(host, sessionID()));
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
if (choice === "more") {
|
|
228
|
-
const pick = await host.ui.showMenu({
|
|
229
|
-
title: "Share to another server",
|
|
230
|
-
options: [...otherServers.map((s) => ({ title: hostOf(s.url), value: s.url, description: s.entry.label })), { title: "Cancel", value: "__cancel" }],
|
|
231
|
-
});
|
|
232
|
-
if (!pick || pick === "__cancel")
|
|
233
|
-
return;
|
|
234
|
-
const { saveServerEntry } = await import("./share-flow.js");
|
|
235
|
-
for (const s of serverEntries())
|
|
236
|
-
if (s.url !== pick)
|
|
237
|
-
saveServerEntry(s.url, { active: undefined });
|
|
238
|
-
saveServerEntry(pick, { active: true });
|
|
239
|
-
finish(host, { ok: true, message: `${hostOf(pick)} activated — this session mirrors there within seconds (new sessions share to it too).` });
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
if (choice === "manage") {
|
|
243
|
-
const pick = await host.ui.showMenu({
|
|
244
|
-
title: "Manage server",
|
|
245
|
-
options: [...all.map((b) => ({ title: hostOf(b.url || b.server), value: b.server, description: b.url })), { title: "Cancel", value: "__cancel" }],
|
|
246
|
-
});
|
|
247
|
-
if (!pick || pick === "__cancel")
|
|
248
|
-
return;
|
|
249
|
-
const b = all.find((x) => x.server === pick);
|
|
250
|
-
if (!b)
|
|
251
|
-
return;
|
|
252
|
-
const action = await host.ui.showMenu({
|
|
253
|
-
title: `${hostOf(b.url || b.server)}`,
|
|
254
|
-
hint: b.url,
|
|
255
|
-
options: [
|
|
256
|
-
{
|
|
257
|
-
title: "Copy link",
|
|
258
|
-
value: "copy",
|
|
259
|
-
description: "copy the share URL",
|
|
260
|
-
help: `Copies this server's share URL to the clipboard.\n\nURL: ${b.url}\nShare id: ${b.id}`,
|
|
261
|
-
},
|
|
262
|
-
{
|
|
263
|
-
title: "Stop sharing here",
|
|
264
|
-
value: "stop",
|
|
265
|
-
description: "delete THIS server's page; other servers unaffected",
|
|
266
|
-
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.`,
|
|
267
|
-
},
|
|
268
|
-
{
|
|
269
|
-
title: "Leave as-is (snapshot)",
|
|
270
|
-
value: "leave",
|
|
271
|
-
description: "stop updating it; the page stays until its TTL",
|
|
272
|
-
help: `Stops mirroring to ${hostOf(b.url || b.server)} WITHOUT deleting anything — viewers keep reading the snapshot until the share's TTL expires. Useful for handing someone a frozen copy while you keep working.`,
|
|
273
|
-
},
|
|
274
|
-
{ title: "Cancel", value: "cancel" },
|
|
275
|
-
],
|
|
276
|
-
});
|
|
277
|
-
if (!action || action === "cancel")
|
|
278
|
-
return;
|
|
279
|
-
if (action === "copy") {
|
|
280
|
-
const ok = await host.clipboard.write(b.url);
|
|
281
|
-
finish(host, ok ? { ok: true, message: `Share URL copied: ${b.url}`, url: b.url } : { ok: false, message: `Clipboard failed — ${b.url}`, url: b.url });
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
|
-
if (action === "stop") {
|
|
285
|
-
const deleted = await workerDeleteShare(b.server || b.url.slice(0, b.url.indexOf("/s/")), b.id, b.secret);
|
|
286
|
-
await patchSessionBinding(host.client, root, (servers) => {
|
|
287
|
-
delete servers[b.server];
|
|
288
|
-
servers[b.server] = { optedOut: true };
|
|
289
|
-
});
|
|
290
|
-
finish(host, { ok: true, message: deleted ? `Stopped sharing on ${hostOf(b.url || b.server)} — page deleted.` : `Stopped sharing on ${hostOf(b.url || b.server)} (page deletion follows via the sweeper).` });
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
if (action === "leave") {
|
|
294
|
-
await patchSessionBinding(host.client, root, (servers) => {
|
|
295
|
-
delete servers[b.server];
|
|
296
|
-
servers[b.server] = { optedOut: true };
|
|
297
|
-
});
|
|
298
|
-
finish(host, { ok: true, message: `${hostOf(b.url || b.server)} left as-is — a snapshot until its TTL.` });
|
|
299
|
-
return;
|
|
71
|
+
return { all, goneShown };
|
|
300
72
|
}
|
|
301
73
|
}
|
|
74
|
+
return { all, goneShown };
|
|
302
75
|
}
|
|
303
|
-
/** Credential collision: the engine refused to auto-recreate. The user
|
|
304
|
-
* chooses: copy debug info (for the hoster/issue), share on a new share,
|
|
305
|
-
* or don't share. */
|
|
306
76
|
async function showCollisionDialog(host, root, server, shareId, error) {
|
|
307
77
|
const debugInfo = JSON.stringify({ server, shareId, error, sessionID: root, plugin: "openshare", time: new Date().toISOString() }, null, 2);
|
|
308
78
|
const choice = await host.ui.showMenu({
|
|
@@ -375,44 +145,30 @@ export function buildShareCommand(host, sessionID) {
|
|
|
375
145
|
return;
|
|
376
146
|
}
|
|
377
147
|
const root = await resolveRoot(host.client, sid);
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
148
|
+
// collision + gone sweep, then the gate. null = a dialog flow took
|
|
149
|
+
// over (collision / gone re-share) and already finished.
|
|
150
|
+
const swept = await sweepBindings(host, sid, root);
|
|
151
|
+
if (swept === null)
|
|
152
|
+
return;
|
|
153
|
+
const { all, goneShown } = swept;
|
|
381
154
|
// live ⇔ url && enabled !== false (all-mode bindings carry
|
|
382
|
-
// enabled:true
|
|
383
|
-
//
|
|
384
|
-
if (
|
|
385
|
-
|
|
386
|
-
|
|
155
|
+
// enabled:true; legacy all-mode bindings without the field are live
|
|
156
|
+
// too — only an explicit false is dormant)
|
|
157
|
+
if (all.some((b) => b.url && b.enabled !== false)) {
|
|
158
|
+
// THE session menu — identical to /openshare → This session by
|
|
159
|
+
// construction (one surface, no duplicated manager). No
|
|
160
|
+
// dialog.clear() after: settings owns its chain end-to-end (the
|
|
161
|
+
// old fire-and-forget menu + immediate clear wiped it instantly —
|
|
162
|
+
// live-caught as "/share did nothing" on a shared session).
|
|
163
|
+
await settingsDialog(host, sid);
|
|
387
164
|
return;
|
|
388
165
|
}
|
|
389
|
-
//
|
|
390
|
-
|
|
391
|
-
if (
|
|
392
|
-
const yes = await host.ui.showConfirm({
|
|
393
|
-
title: "Share Session",
|
|
394
|
-
message: "Publish this session (and its subagent sessions) to your openshare worker?\nThe link lets anyone with it read the conversation.",
|
|
395
|
-
confirmLabel: "Share",
|
|
396
|
-
});
|
|
397
|
-
if (!yes)
|
|
398
|
-
return;
|
|
399
|
-
try {
|
|
400
|
-
host.kv.set(CONSENT_KV_KEY, true);
|
|
401
|
-
dbgLog("consent kv.set ok");
|
|
402
|
-
}
|
|
403
|
-
catch (e) {
|
|
404
|
-
dbgLog(`consent kv.set THREW: ${e?.message ?? e}`);
|
|
405
|
-
}
|
|
406
|
-
try {
|
|
407
|
-
finish(host, await confirmShare(host, root));
|
|
408
|
-
}
|
|
409
|
-
catch (e) {
|
|
410
|
-
dbgLog(`confirmShare rejected: ${e?.stack ?? e}`);
|
|
411
|
-
finish(host, { ok: false, message: "Share failed (see debug log)." });
|
|
412
|
-
}
|
|
166
|
+
// everything was just cleared by the gone modal and the user chose
|
|
167
|
+
// Close — leaving it unshared is the answer, do not offer a share
|
|
168
|
+
if (goneShown && all.length === 0)
|
|
413
169
|
return;
|
|
414
|
-
|
|
415
|
-
finish(host,
|
|
170
|
+
// not shared (or dormant): explicit consent, every time
|
|
171
|
+
finish(host, await shareWithConsent(host, sid));
|
|
416
172
|
},
|
|
417
173
|
};
|
|
418
174
|
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
export declare const CONTRACT_VERSION = 2;
|
|
2
|
+
/** Message tombstone item. sessionID MUST ride along: viewers key
|
|
3
|
+
* messages as message/<sessionID>/<id> — an unstamped del misses the
|
|
4
|
+
* key and undone turns resurrect on the live page (live-caught
|
|
5
|
+
* 2026-10-21, refresh-correct because the worker keys differently). */
|
|
6
|
+
export declare function messageTombstone(sessionID: string, messageID: string): {
|
|
7
|
+
readonly type: "message";
|
|
8
|
+
readonly del: true;
|
|
9
|
+
readonly sessionID: string;
|
|
10
|
+
readonly data: {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export type V2ToolState = {
|
|
15
|
+
status: "streaming";
|
|
16
|
+
input: string;
|
|
17
|
+
} | {
|
|
18
|
+
status: "running";
|
|
19
|
+
input: Record<string, unknown>;
|
|
20
|
+
metadata: Record<string, unknown>;
|
|
21
|
+
} | {
|
|
22
|
+
status: "completed";
|
|
23
|
+
input: Record<string, unknown>;
|
|
24
|
+
content: Array<{
|
|
25
|
+
type: "text";
|
|
26
|
+
text: string;
|
|
27
|
+
}>;
|
|
28
|
+
metadata?: Record<string, unknown>;
|
|
29
|
+
} | {
|
|
30
|
+
status: "error";
|
|
31
|
+
input: Record<string, unknown>;
|
|
32
|
+
error: {
|
|
33
|
+
type: string;
|
|
34
|
+
message: string;
|
|
35
|
+
};
|
|
36
|
+
content?: Array<{
|
|
37
|
+
type: "text";
|
|
38
|
+
text: string;
|
|
39
|
+
}>;
|
|
40
|
+
metadata?: Record<string, unknown>;
|
|
41
|
+
};
|
|
42
|
+
export type V2Content = {
|
|
43
|
+
type: "text";
|
|
44
|
+
text: string;
|
|
45
|
+
state?: string;
|
|
46
|
+
} | {
|
|
47
|
+
type: "reasoning";
|
|
48
|
+
text: string;
|
|
49
|
+
state?: string;
|
|
50
|
+
time?: {
|
|
51
|
+
created?: number;
|
|
52
|
+
completed?: number;
|
|
53
|
+
};
|
|
54
|
+
} | {
|
|
55
|
+
type: "tool";
|
|
56
|
+
id: string;
|
|
57
|
+
name: string;
|
|
58
|
+
executed?: boolean;
|
|
59
|
+
state: V2ToolState;
|
|
60
|
+
time: {
|
|
61
|
+
created?: number;
|
|
62
|
+
ran?: number;
|
|
63
|
+
completed?: number;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
export type V2FileAttachment = {
|
|
67
|
+
data: string;
|
|
68
|
+
mime: string;
|
|
69
|
+
source: {
|
|
70
|
+
type: "inline";
|
|
71
|
+
} | {
|
|
72
|
+
type: "uri";
|
|
73
|
+
uri: string;
|
|
74
|
+
};
|
|
75
|
+
name?: string;
|
|
76
|
+
};
|
|
77
|
+
export interface V2UserMessage {
|
|
78
|
+
id: string;
|
|
79
|
+
type: "user";
|
|
80
|
+
time: {
|
|
81
|
+
created: number;
|
|
82
|
+
};
|
|
83
|
+
text: string;
|
|
84
|
+
files?: V2FileAttachment[];
|
|
85
|
+
agents?: Array<{
|
|
86
|
+
name: string;
|
|
87
|
+
}>;
|
|
88
|
+
metadata?: Record<string, unknown>;
|
|
89
|
+
}
|
|
90
|
+
export interface V2AssistantMessage {
|
|
91
|
+
id: string;
|
|
92
|
+
type: "assistant";
|
|
93
|
+
agent: string;
|
|
94
|
+
model: {
|
|
95
|
+
id: string;
|
|
96
|
+
providerID: string;
|
|
97
|
+
variant?: string;
|
|
98
|
+
};
|
|
99
|
+
time: {
|
|
100
|
+
created: number;
|
|
101
|
+
streamed?: number;
|
|
102
|
+
completed?: number;
|
|
103
|
+
};
|
|
104
|
+
content: V2Content[];
|
|
105
|
+
cost?: number;
|
|
106
|
+
tokens?: Record<string, unknown>;
|
|
107
|
+
finish?: string;
|
|
108
|
+
error?: {
|
|
109
|
+
type: string;
|
|
110
|
+
message: string;
|
|
111
|
+
status?: number;
|
|
112
|
+
};
|
|
113
|
+
retry?: {
|
|
114
|
+
attempt: number;
|
|
115
|
+
at: number;
|
|
116
|
+
error?: {
|
|
117
|
+
type: string;
|
|
118
|
+
message: string;
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
export interface V2CompactionMessage {
|
|
123
|
+
id: string;
|
|
124
|
+
type: "compaction";
|
|
125
|
+
status: "completed";
|
|
126
|
+
reason: "auto" | "manual";
|
|
127
|
+
summary: string;
|
|
128
|
+
recent: string;
|
|
129
|
+
time: {
|
|
130
|
+
created: number;
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export type V2Message = V2UserMessage | V2AssistantMessage | V2CompactionMessage | Record<string, unknown>;
|
|
134
|
+
/** True when the object is already a v2 session message (v2 hosts pass
|
|
135
|
+
* messages through verbatim; v1 infos carry `role`). */
|
|
136
|
+
export declare function isV2NativeMessage(x: unknown): boolean;
|
|
137
|
+
/** v1 message + its parts → v2 message. Returns:
|
|
138
|
+
* - { message } — emit as a message item
|
|
139
|
+
* - { refill, summary } — v1 compaction summary assistant: do not emit;
|
|
140
|
+
* rewrite the parent compaction message instead
|
|
141
|
+
* - undefined — nothing to emit (should not happen in practice) */
|
|
142
|
+
export declare function normalizeV1Message(info: Record<string, unknown>, parts: Array<Record<string, unknown>>): {
|
|
143
|
+
message: V2Message;
|
|
144
|
+
} | {
|
|
145
|
+
refill: string;
|
|
146
|
+
summary: string;
|
|
147
|
+
} | undefined;
|
|
148
|
+
/** v1 session diff (FileDiff.Info[]) → v2 FileDiffInfo[]: the v2 shape
|
|
149
|
+
* REQUIRES numeric additions/deletions (renderers read them unguarded) —
|
|
150
|
+
* compute from the patch when the host didn't provide counts. */
|
|
151
|
+
export declare function normalizeV1Diff(diff: unknown): unknown;
|
|
152
|
+
/** v1 hosts paginate messages with an opaque cursor: base64url of
|
|
153
|
+
* {id, time} where time = the page tail's time_created (message-v2.ts
|
|
154
|
+
* cursor codec). A bare message id is rejected with 400 — that silently
|
|
155
|
+
* truncated every v1 backfill to one page (live-caught). */
|
|
156
|
+
export declare function cursorFor(id: string, time: number): string;
|