@mirrowel/opencode-openshare 0.1.1 → 0.2.0-dev.1
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/ci/openshare.js +583 -160
- package/dist/contract.d.ts +156 -0
- package/dist/contract.js +263 -0
- package/dist/core.js +411 -55
- package/dist/dialogs.js +38 -0
- package/dist/sidebar.d.ts +3 -1
- package/dist/sidebar.js +3 -17
- package/dist/tui.js +31 -36
- package/dist/v2-server.d.ts +0 -27
- package/dist/v2-server.js +6 -121
- package/package.json +3 -1
- package/src/contract.ts +320 -0
- package/src/core.ts +403 -56
- package/src/dialogs.tsx +34 -0
- package/src/sidebar.tsx +4 -21
- package/src/v2-server.ts +7 -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));
|
|
@@ -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/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;
|
|
@@ -50,13 +46,9 @@ function dbgEl(el, depth = 0) {
|
|
|
50
46
|
return `el?-${dbg(e)}`;
|
|
51
47
|
}
|
|
52
48
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
catch { }
|
|
58
|
-
}
|
|
59
|
-
let renderSeq = 0;
|
|
49
|
+
/** no-op: the TUI debug log file was a crash-hunt aid and must not write
|
|
50
|
+
* to temp any more (owner request 2026-10-21) */
|
|
51
|
+
export function dbgLog(_line) { }
|
|
60
52
|
const GONE_TTL_MS = 60_000;
|
|
61
53
|
const goneCache = new Map();
|
|
62
54
|
function goneVerdict(sid, server, shareId) {
|
|
@@ -70,13 +62,10 @@ function goneVerdict(sid, server, shareId) {
|
|
|
70
62
|
return cached;
|
|
71
63
|
}
|
|
72
64
|
export function createSidebarSlot(api, opts) {
|
|
73
|
-
dbgLog(`createSidebarSlot called; api.state=${!!api.state} api.theme=${!!api.theme} api.version=${dbg(api.app?.version)}`);
|
|
74
65
|
return {
|
|
75
66
|
slots: {
|
|
76
67
|
sidebar_title: (ctx, props) => {
|
|
77
|
-
const seq = ++renderSeq;
|
|
78
68
|
try {
|
|
79
|
-
dbgLog(`render#${seq} ctx=${dbg(ctx)} props=${dbg(props)}`);
|
|
80
69
|
const sid = typeof props?.session_id === "string" ? props.session_id : "";
|
|
81
70
|
const title = String(props?.title ?? "") || "Session";
|
|
82
71
|
const session = sid ? api.state.session.get(sid) : undefined;
|
|
@@ -90,7 +79,6 @@ export function createSidebarSlot(api, opts) {
|
|
|
90
79
|
const anyGone = verdicts.some((v) => v?.status === "gone");
|
|
91
80
|
const multi = liveCount > 1 ? `·${liveCount} ` : "";
|
|
92
81
|
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
82
|
// chip on its own line under the title (takes the blank line that
|
|
95
83
|
// separated the title block from the context blocks); clicking it
|
|
96
84
|
// copies the share link — proven pattern: onMouseDown on a box.
|
|
@@ -131,11 +119,9 @@ export function createSidebarSlot(api, opts) {
|
|
|
131
119
|
: status === "live"
|
|
132
120
|
? `◉ re:share ${multi}— click to copy link`
|
|
133
121
|
: "◉ re:share (dormant)" }) })) : null] }));
|
|
134
|
-
dbgLog(`render#${seq} RETURNS ${dbgEl(el)}`);
|
|
135
122
|
return el;
|
|
136
123
|
}
|
|
137
124
|
catch (error) {
|
|
138
|
-
dbgLog(`render#${seq} THREW ${error?.stack ?? dbg(error)} — rethrowing`);
|
|
139
125
|
throw error;
|
|
140
126
|
}
|
|
141
127
|
},
|
package/dist/tui.js
CHANGED
|
@@ -314,9 +314,6 @@ import { insertNode as _$insertNode } from "@opentui/solid";
|
|
|
314
314
|
import { insert as _$insert } from "@opentui/solid";
|
|
315
315
|
import { setProp as _$setProp } from "@opentui/solid";
|
|
316
316
|
import { createElement as _$createElement } from "@opentui/solid";
|
|
317
|
-
import { appendFileSync } from "fs";
|
|
318
|
-
import { tmpdir } from "os";
|
|
319
|
-
import { join as join2 } from "path";
|
|
320
317
|
function dbg(v, depth = 0) {
|
|
321
318
|
const t = typeof v;
|
|
322
319
|
if (v === null)
|
|
@@ -340,31 +337,7 @@ function dbg(v, depth = 0) {
|
|
|
340
337
|
return "obj:?}";
|
|
341
338
|
}
|
|
342
339
|
}
|
|
343
|
-
function
|
|
344
|
-
if (el === null || el === undefined || typeof el === "string" || typeof el === "number" || typeof el === "boolean")
|
|
345
|
-
return dbg(el);
|
|
346
|
-
if (Array.isArray(el))
|
|
347
|
-
return `[${el.map((x) => dbgEl(x, depth)).join(",")}]`;
|
|
348
|
-
if (typeof el === "function")
|
|
349
|
-
return "fn-node";
|
|
350
|
-
if (depth >= 4)
|
|
351
|
-
return "\u2026";
|
|
352
|
-
try {
|
|
353
|
-
const rec = el;
|
|
354
|
-
const t = dbg(rec.t ?? rec.type, 2);
|
|
355
|
-
const kids = rec.children;
|
|
356
|
-
const props = Object.keys(rec).filter((k) => !["t", "type", "children"].includes(k));
|
|
357
|
-
return `<${t} ${props.join(" ")}>${kids === undefined ? "" : dbgEl(kids, depth + 1)}</${t}>`;
|
|
358
|
-
} catch (e) {
|
|
359
|
-
return `el?-${dbg(e)}`;
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
function dbgLog(line) {
|
|
363
|
-
try {
|
|
364
|
-
appendFileSync(DBG_FILE, `${new Date().toISOString()} ${line}
|
|
365
|
-
`);
|
|
366
|
-
} catch {}
|
|
367
|
-
}
|
|
340
|
+
function dbgLog(_line) {}
|
|
368
341
|
function goneVerdict(sid, server, shareId) {
|
|
369
342
|
const key = `${sid}|${server}`;
|
|
370
343
|
const cached = goneCache.get(key);
|
|
@@ -379,13 +352,10 @@ function goneVerdict(sid, server, shareId) {
|
|
|
379
352
|
return cached;
|
|
380
353
|
}
|
|
381
354
|
function createSidebarSlot(api, opts) {
|
|
382
|
-
dbgLog(`createSidebarSlot called; api.state=${!!api.state} api.theme=${!!api.theme} api.version=${dbg(api.app?.version)}`);
|
|
383
355
|
return {
|
|
384
356
|
slots: {
|
|
385
357
|
sidebar_title: (ctx, props) => {
|
|
386
|
-
const seq = ++renderSeq;
|
|
387
358
|
try {
|
|
388
|
-
dbgLog(`render#${seq} ctx=${dbg(ctx)} props=${dbg(props)}`);
|
|
389
359
|
const sid = typeof props?.session_id === "string" ? props.session_id : "";
|
|
390
360
|
const title = String(props?.title ?? "") || "Session";
|
|
391
361
|
const session = sid ? api.state.session.get(sid) : undefined;
|
|
@@ -398,7 +368,6 @@ function createSidebarSlot(api, opts) {
|
|
|
398
368
|
const anyGone = verdicts.some((v) => v?.status === "gone");
|
|
399
369
|
const multi = liveCount > 1 ? `\xB7${liveCount} ` : "";
|
|
400
370
|
const theme = ctx?.theme?.current ?? api.theme.current;
|
|
401
|
-
dbgLog(`render#${seq} sid=${dbg(sid)} title=${dbg(title)} session=${session ? "found" : "MISSING"} binding=${binding ? `id=${dbg(binding.id)} enabled=${dbg(binding.enabled)}` : "none"} status=${dbg(status)} theme=${dbg(theme)}`);
|
|
402
371
|
const copyLink = () => {
|
|
403
372
|
if (anyGone) {
|
|
404
373
|
opts?.onShareCommand?.();
|
|
@@ -445,20 +414,17 @@ function createSidebarSlot(api, opts) {
|
|
|
445
414
|
_$effect((_$p) => _$setProp(_el$2, "fg", theme.text, _$p));
|
|
446
415
|
return _el$;
|
|
447
416
|
})();
|
|
448
|
-
dbgLog(`render#${seq} RETURNS ${dbgEl(el)}`);
|
|
449
417
|
return el;
|
|
450
418
|
} catch (error) {
|
|
451
|
-
dbgLog(`render#${seq} THREW ${error?.stack ?? dbg(error)} \u2014 rethrowing`);
|
|
452
419
|
throw error;
|
|
453
420
|
}
|
|
454
421
|
}
|
|
455
422
|
}
|
|
456
423
|
};
|
|
457
424
|
}
|
|
458
|
-
var
|
|
425
|
+
var GONE_TTL_MS = 60000, goneCache;
|
|
459
426
|
var init_sidebar = __esm(() => {
|
|
460
427
|
init_share_flow();
|
|
461
|
-
DBG_FILE = join2(tmpdir(), "openshare-tui-debug.log");
|
|
462
428
|
goneCache = new Map;
|
|
463
429
|
});
|
|
464
430
|
|
|
@@ -3337,13 +3303,36 @@ function MenuDialog(props) {
|
|
|
3337
3303
|
}
|
|
3338
3304
|
});
|
|
3339
3305
|
}
|
|
3306
|
+
var pendingDialog;
|
|
3307
|
+
function claimDialog(api) {
|
|
3308
|
+
if (pendingDialog) {
|
|
3309
|
+
try {
|
|
3310
|
+
pendingDialog();
|
|
3311
|
+
} catch {}
|
|
3312
|
+
try {
|
|
3313
|
+
api.ui.dialog.clear();
|
|
3314
|
+
} catch {}
|
|
3315
|
+
}
|
|
3316
|
+
let released = false;
|
|
3317
|
+
const release = () => {
|
|
3318
|
+
if (released)
|
|
3319
|
+
return;
|
|
3320
|
+
released = true;
|
|
3321
|
+
if (pendingDialog === release)
|
|
3322
|
+
pendingDialog = undefined;
|
|
3323
|
+
};
|
|
3324
|
+
pendingDialog = release;
|
|
3325
|
+
return release;
|
|
3326
|
+
}
|
|
3340
3327
|
function showMenuOnce(api, props) {
|
|
3341
3328
|
return new Promise((resolve) => {
|
|
3342
3329
|
let settled = false;
|
|
3330
|
+
const release = claimDialog(api);
|
|
3343
3331
|
const done = (value, _clear = false) => {
|
|
3344
3332
|
if (settled)
|
|
3345
3333
|
return;
|
|
3346
3334
|
settled = true;
|
|
3335
|
+
release();
|
|
3347
3336
|
dbgLog(`menu(${props.title}) -> ${value === undefined ? "cancel" : String(value)}`);
|
|
3348
3337
|
resolve(value);
|
|
3349
3338
|
};
|
|
@@ -3369,10 +3358,12 @@ function showMenuOnce(api, props) {
|
|
|
3369
3358
|
function showInfoDialog(api, props) {
|
|
3370
3359
|
return new Promise((resolve) => {
|
|
3371
3360
|
let settled = false;
|
|
3361
|
+
const release = claimDialog(api);
|
|
3372
3362
|
const done = (value) => {
|
|
3373
3363
|
if (settled)
|
|
3374
3364
|
return;
|
|
3375
3365
|
settled = true;
|
|
3366
|
+
release();
|
|
3376
3367
|
resolve(value);
|
|
3377
3368
|
};
|
|
3378
3369
|
api.ui.dialog.replace(() => _$createComponent(InfoDialog, {
|
|
@@ -3767,10 +3758,12 @@ function PromptDialog(props) {
|
|
|
3767
3758
|
function showPromptOnce(api, props) {
|
|
3768
3759
|
return new Promise((resolve) => {
|
|
3769
3760
|
let settled = false;
|
|
3761
|
+
const release = claimDialog(api);
|
|
3770
3762
|
const done = (value, _clear = false) => {
|
|
3771
3763
|
if (settled)
|
|
3772
3764
|
return;
|
|
3773
3765
|
settled = true;
|
|
3766
|
+
release();
|
|
3774
3767
|
dbgLog(`prompt(${props.title}) -> ${value === undefined ? "cancel" : `len=${value.length}`}`);
|
|
3775
3768
|
resolve(value);
|
|
3776
3769
|
};
|
|
@@ -3805,10 +3798,12 @@ function toMenuOptions(options) {
|
|
|
3805
3798
|
function showSizePickerOnce(api, current) {
|
|
3806
3799
|
return new Promise((resolve) => {
|
|
3807
3800
|
let settled = false;
|
|
3801
|
+
const release = claimDialog(api);
|
|
3808
3802
|
const done = (value) => {
|
|
3809
3803
|
if (settled)
|
|
3810
3804
|
return;
|
|
3811
3805
|
settled = true;
|
|
3806
|
+
release();
|
|
3812
3807
|
resolve(value);
|
|
3813
3808
|
};
|
|
3814
3809
|
api.ui.dialog.replace(() => _$createComponent(SizePickerDialog, {
|
package/dist/v2-server.d.ts
CHANGED
|
@@ -1,30 +1,8 @@
|
|
|
1
|
-
import { type MessageInfo, type PartInfo } from "./core.js";
|
|
2
1
|
type V2Event = {
|
|
3
2
|
type: string;
|
|
4
3
|
data?: Record<string, unknown>;
|
|
5
4
|
[key: string]: unknown;
|
|
6
5
|
};
|
|
7
|
-
type V2Message = {
|
|
8
|
-
id: string;
|
|
9
|
-
type: string;
|
|
10
|
-
time?: {
|
|
11
|
-
created?: number;
|
|
12
|
-
completed?: number;
|
|
13
|
-
streamed?: number;
|
|
14
|
-
};
|
|
15
|
-
text?: string;
|
|
16
|
-
agent?: string;
|
|
17
|
-
model?: {
|
|
18
|
-
providerID: string;
|
|
19
|
-
id?: string;
|
|
20
|
-
modelID?: string;
|
|
21
|
-
};
|
|
22
|
-
content?: Array<Record<string, unknown>>;
|
|
23
|
-
error?: {
|
|
24
|
-
message?: string;
|
|
25
|
-
} | null;
|
|
26
|
-
metadata?: Record<string, unknown>;
|
|
27
|
-
};
|
|
28
6
|
type V2SessionClient = {
|
|
29
7
|
get(input: {
|
|
30
8
|
sessionID: string;
|
|
@@ -72,10 +50,5 @@ type V2Context = {
|
|
|
72
50
|
};
|
|
73
51
|
signal?: AbortSignal;
|
|
74
52
|
};
|
|
75
|
-
/** v2 flat message -> { info, parts } in the v1 shapes the viewer speaks. */
|
|
76
|
-
export declare function convertV2Message(message: V2Message, sessionID: string): {
|
|
77
|
-
info: MessageInfo;
|
|
78
|
-
parts: PartInfo[];
|
|
79
|
-
} | undefined;
|
|
80
53
|
export declare function createV2ServerSetup(): (context: V2Context) => Promise<(() => void) | void>;
|
|
81
54
|
export {};
|
package/dist/v2-server.js
CHANGED
|
@@ -29,125 +29,10 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
29
29
|
import { join } from "node:path";
|
|
30
30
|
import { METADATA_KEY, moduleDir, packageRoot } from "./config.js";
|
|
31
31
|
import { createEngine } from "./core.js";
|
|
32
|
-
// --- v2 message -> v1 items ---------------------------------------------------
|
|
33
|
-
function toolContentText(content) {
|
|
34
|
-
if (typeof content === "string")
|
|
35
|
-
return content;
|
|
36
|
-
if (Array.isArray(content)) {
|
|
37
|
-
return content
|
|
38
|
-
.map((item) => {
|
|
39
|
-
if (typeof item === "string")
|
|
40
|
-
return item;
|
|
41
|
-
if (item && typeof item === "object") {
|
|
42
|
-
const record = item;
|
|
43
|
-
if (typeof record.text === "string")
|
|
44
|
-
return record.text;
|
|
45
|
-
}
|
|
46
|
-
return "";
|
|
47
|
-
})
|
|
48
|
-
.join("");
|
|
49
|
-
}
|
|
50
|
-
return "";
|
|
51
|
-
}
|
|
52
|
-
function convertToolState(state) {
|
|
53
|
-
if (!state || typeof state !== "object")
|
|
54
|
-
return { status: "pending" };
|
|
55
|
-
const record = state;
|
|
56
|
-
const status = String(record.status ?? "pending");
|
|
57
|
-
const mapped = {};
|
|
58
|
-
if (status === "streaming" || status === "running")
|
|
59
|
-
mapped.status = "running";
|
|
60
|
-
else if (status === "completed")
|
|
61
|
-
mapped.status = "completed";
|
|
62
|
-
else if (status === "failed" || status === "error")
|
|
63
|
-
mapped.status = "error";
|
|
64
|
-
else
|
|
65
|
-
mapped.status = "pending";
|
|
66
|
-
if (record.input !== undefined)
|
|
67
|
-
mapped.input = record.input;
|
|
68
|
-
if (record.output !== undefined)
|
|
69
|
-
mapped.output = toolContentText(record.output);
|
|
70
|
-
if (record.error !== undefined)
|
|
71
|
-
mapped.error = record.error;
|
|
72
|
-
if (record.time !== undefined)
|
|
73
|
-
mapped.time = record.time;
|
|
74
|
-
return mapped;
|
|
75
|
-
}
|
|
76
|
-
/** v2 flat message -> { info, parts } in the v1 shapes the viewer speaks. */
|
|
77
|
-
export function convertV2Message(message, sessionID) {
|
|
78
|
-
if (!message || typeof message.id !== "string")
|
|
79
|
-
return undefined;
|
|
80
|
-
const base = {
|
|
81
|
-
id: message.id,
|
|
82
|
-
sessionID,
|
|
83
|
-
};
|
|
84
|
-
if (message.type === "user") {
|
|
85
|
-
return {
|
|
86
|
-
info: {
|
|
87
|
-
...base,
|
|
88
|
-
role: "user",
|
|
89
|
-
time: { created: message.time?.created ?? 0 },
|
|
90
|
-
text: message.text ?? "",
|
|
91
|
-
},
|
|
92
|
-
parts: [],
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
if (message.type === "assistant") {
|
|
96
|
-
const parts = [];
|
|
97
|
-
const content = Array.isArray(message.content) ? message.content : [];
|
|
98
|
-
content.forEach((block, index) => {
|
|
99
|
-
const record = block;
|
|
100
|
-
const kind = String(record.type ?? "");
|
|
101
|
-
if (kind === "text") {
|
|
102
|
-
parts.push({
|
|
103
|
-
id: `${message.id}_t${index}`,
|
|
104
|
-
messageID: message.id,
|
|
105
|
-
sessionID,
|
|
106
|
-
type: "text",
|
|
107
|
-
text: String(record.text ?? ""),
|
|
108
|
-
synthetic: false,
|
|
109
|
-
time: { start: message.time?.created ?? 0, end: message.time?.completed ?? 0 },
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
else if (kind === "reasoning") {
|
|
113
|
-
parts.push({
|
|
114
|
-
id: `${message.id}_r${index}`,
|
|
115
|
-
messageID: message.id,
|
|
116
|
-
sessionID,
|
|
117
|
-
type: "reasoning",
|
|
118
|
-
text: String(record.text ?? ""),
|
|
119
|
-
synthetic: false,
|
|
120
|
-
time: { start: message.time?.created ?? 0, end: message.time?.completed ?? 0 },
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
else if (kind === "tool") {
|
|
124
|
-
parts.push({
|
|
125
|
-
id: String(record.id ?? `${message.id}_tool${index}`),
|
|
126
|
-
messageID: message.id,
|
|
127
|
-
sessionID,
|
|
128
|
-
type: "tool",
|
|
129
|
-
tool: String(record.name ?? "unknown"),
|
|
130
|
-
state: convertToolState(record.state),
|
|
131
|
-
synthetic: false,
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
return {
|
|
136
|
-
info: {
|
|
137
|
-
...base,
|
|
138
|
-
role: "assistant",
|
|
139
|
-
agent: message.agent,
|
|
140
|
-
model: message.model
|
|
141
|
-
? { providerID: message.model.providerID, modelID: message.model.modelID ?? message.model.id }
|
|
142
|
-
: undefined,
|
|
143
|
-
time: { created: message.time?.created ?? 0, completed: message.time?.completed ?? 0 },
|
|
144
|
-
},
|
|
145
|
-
parts,
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
return undefined;
|
|
149
|
-
}
|
|
150
32
|
// --- v2 client -> engine ClientApi ---------------------------------------------
|
|
33
|
+
// Contract v2: messages pass through VERBATIM as v2 session messages (the
|
|
34
|
+
// engine's ingestion detects v2-native shapes and enqueues them directly —
|
|
35
|
+
// no v1 conversion, no dropped compaction, no dropped user text).
|
|
151
36
|
function adaptSessionClient(session, message, directory) {
|
|
152
37
|
if (!session)
|
|
153
38
|
return undefined;
|
|
@@ -178,9 +63,9 @@ function adaptSessionClient(session, message, directory) {
|
|
|
178
63
|
if (!Array.isArray(data))
|
|
179
64
|
break;
|
|
180
65
|
for (const raw of data) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
66
|
+
if (raw && typeof raw === "object" && typeof raw.id === "string") {
|
|
67
|
+
out.push({ info: raw, parts: [] });
|
|
68
|
+
}
|
|
184
69
|
}
|
|
185
70
|
const next = res?.cursor?.next ?? undefined;
|
|
186
71
|
if (!next || data.length === 0)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mirrowel/opencode-openshare",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-dev.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Open(code)Share — re:share: self-hosted session sharing for opencode. Server plugin (single file or package), optional TUI for interactive opt-in sharing.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -76,6 +76,8 @@
|
|
|
76
76
|
"test:v2": "node scripts/v2-mock-tests.mjs",
|
|
77
77
|
"test:auth": "node scripts/auth-tests.mjs",
|
|
78
78
|
"test:server-auth": "node scripts/auth-tests.mjs --backend bun",
|
|
79
|
+
"test:big": "node scripts/big-tests.mjs",
|
|
80
|
+
"test:server-big": "node scripts/big-tests.mjs --backend bun",
|
|
79
81
|
"test:server-enroll": "node scripts/enroll-tests.mjs --backend bun",
|
|
80
82
|
"test:enroll": "node scripts/enroll-tests.mjs",
|
|
81
83
|
"test:fanout": "node scripts/fanout-tests.mjs",
|