@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/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
- export declare function dbgLog(line: string): void;
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
- export function dbgLog(line) {
54
- try {
55
- appendFileSync(DBG_FILE, `${new Date().toISOString()} ${line}\n`);
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 dbgEl(el, depth = 0) {
344
- if (el === null || el === undefined || typeof el === "string" || typeof el === "number" || typeof el === "boolean")
345
- return dbg(el);
346
- if (Array.isArray(el))
347
- return `[${el.map((x) => dbgEl(x, depth)).join(",")}]`;
348
- if (typeof el === "function")
349
- return "fn-node";
350
- if (depth >= 4)
351
- return "\u2026";
352
- try {
353
- const rec = el;
354
- const t = dbg(rec.t ?? rec.type, 2);
355
- const kids = rec.children;
356
- const props = Object.keys(rec).filter((k) => !["t", "type", "children"].includes(k));
357
- return `<${t} ${props.join(" ")}>${kids === undefined ? "" : dbgEl(kids, depth + 1)}</${t}>`;
358
- } catch (e) {
359
- return `el?-${dbg(e)}`;
360
- }
361
- }
362
- function dbgLog(line) {
363
- try {
364
- appendFileSync(DBG_FILE, `${new Date().toISOString()} ${line}
365
- `);
366
- } catch {}
367
- }
340
+ function dbgLog(_line) {}
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 DBG_FILE, renderSeq = 0, GONE_TTL_MS = 60000, goneCache;
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, {
@@ -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
- const converted = convertV2Message(raw, sessionID);
182
- if (converted)
183
- out.push(converted);
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.1.1",
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",