@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/src/dialogs.tsx CHANGED
@@ -576,6 +576,32 @@ export function MenuDialog<Value>(props: {
576
576
  )
577
577
  }
578
578
 
579
+ /** Guard against stacked plugin dialogs. `dialog.replace` is documented as
580
+ * an atomic swap, but on some host builds a second replace while a dialog is
581
+ * still pending leaves BOTH renderers mounted - the reader sees two menus
582
+ * with two highlighted rows and keys go to a stale layer ("/share does
583
+ * nothing"). Cancelling the pending dialog and clearing before the next
584
+ * replace keeps exactly one dialog alive. The normal single-dialog path is
585
+ * untouched, so the esc-to-parent flicker fix stays intact. */
586
+ let pendingDialog: (() => void) | undefined
587
+ function claimDialog(api: TuiPluginApi): () => void {
588
+ if (pendingDialog) {
589
+ try {
590
+ pendingDialog()
591
+ } catch {}
592
+ try {
593
+ api.ui.dialog.clear()
594
+ } catch {}
595
+ }
596
+ let released = false
597
+ const release = () => {
598
+ if (released) return
599
+ released = true
600
+ if (pendingDialog === release) pendingDialog = undefined
601
+ }
602
+ pendingDialog = release
603
+ return release
604
+ }
579
605
  /** showMenuOnce, family discipline with the flicker fix (2026-10-19):
580
606
  * done() does NOT clear the host stack — the next dialog's replace() is an
581
607
  * atomic swap, so clearing first paints one empty frame (the esc-to-parent
@@ -584,9 +610,11 @@ export function MenuDialog<Value>(props: {
584
610
  export function showMenuOnce<Value>(api: TuiPluginApi, props: { title: string; options: MenuOption<Value>[]; current?: Value; hint?: string }): Promise<Value | undefined> {
585
611
  return new Promise((resolve) => {
586
612
  let settled = false
613
+ const release = claimDialog(api)
587
614
  const done = (value: Value | undefined, _clear = false) => {
588
615
  if (settled) return
589
616
  settled = true
617
+ release()
590
618
  dbgLog(`menu(${props.title}) -> ${value === undefined ? "cancel" : String(value)}`)
591
619
  resolve(value)
592
620
  }
@@ -610,9 +638,11 @@ export type DialogAction = { title: string; value: string; key?: string; danger?
610
638
  export function showInfoDialog(api: TuiPluginApi, props: { title: string; message: string; actions?: DialogAction[] }): Promise<string | undefined> {
611
639
  return new Promise((resolve) => {
612
640
  let settled = false
641
+ const release = claimDialog(api)
613
642
  const done = (value: string | undefined) => {
614
643
  if (settled) return
615
644
  settled = true
645
+ release()
616
646
  resolve(value)
617
647
  }
618
648
  api.ui.dialog.replace(
@@ -703,8 +733,17 @@ function InfoDialog(props: { api: TuiPluginApi; title: string; message: string;
703
733
  <box flexDirection="row" gap={1}>
704
734
  <For each={actions()}>
705
735
  {(action) => (
706
- <box paddingLeft={2} paddingRight={2} backgroundColor={action.danger ? theme().error : theme().primary} onMouseUp={() => props.onDone(action.value)}>
707
- <text fg={theme().background}><b>{action.title}</b></text>
736
+ <box
737
+ paddingLeft={2}
738
+ paddingRight={2}
739
+ backgroundColor={action.key === "enter" ? (action.danger ? theme().error : theme().primary) : undefined}
740
+ border={action.key === "enter" ? undefined : true}
741
+ borderColor={action.danger ? theme().error : theme().textMuted}
742
+ onMouseUp={() => props.onDone(action.value)}
743
+ >
744
+ <text fg={action.key === "enter" ? theme().background : action.danger ? theme().error : theme().textMuted}>
745
+ <b>{action.title}</b>
746
+ </text>
708
747
  </box>
709
748
  )}
710
749
  </For>
@@ -799,9 +838,11 @@ export function PromptDialog(props: {
799
838
  export function showPromptOnce(api: TuiPluginApi, props: { title: string; placeholder?: string; value?: string }): Promise<string | undefined> {
800
839
  return new Promise((resolve) => {
801
840
  let settled = false
841
+ const release = claimDialog(api)
802
842
  const done = (value: string | undefined, _clear = false) => {
803
843
  if (settled) return
804
844
  settled = true
845
+ release()
805
846
  dbgLog(`prompt(${props.title}) -> ${value === undefined ? "cancel" : `len=${value.length}`}`)
806
847
  resolve(value)
807
848
  }
@@ -829,9 +870,11 @@ export type SizePickerResult = { width: DialogWidth; heightPercent: number }
829
870
  export function showSizePickerOnce(api: TuiPluginApi, current: SizePickerResult): Promise<SizePickerResult | undefined> {
830
871
  return new Promise((resolve) => {
831
872
  let settled = false
873
+ const release = claimDialog(api)
832
874
  const done = (value: SizePickerResult | undefined) => {
833
875
  if (settled) return
834
876
  settled = true
877
+ release()
835
878
  resolve(value)
836
879
  }
837
880
  api.ui.dialog.replace(
package/src/settings.ts CHANGED
@@ -30,6 +30,8 @@ import {
30
30
  saveServerEntry,
31
31
  serverEntries,
32
32
  setShareFlag,
33
+ shareWithConsent,
34
+ workerDeleteShare,
33
35
  type AdminShare,
34
36
  type AdminUser,
35
37
  } from "./share-flow.js"
@@ -917,26 +919,68 @@ export async function settingsDialog(host: ShareHostApi, startAtSession?: string
917
919
 
918
920
  const sessionRows = async (): Promise<ShareDialogSelectOption<string>[]> => {
919
921
  if (!startAtSession) return []
920
- const binding = await readBinding(host.client, await resolveRoot(host.client, startAtSession))
921
- const rows: ShareDialogSelectOption<string>[] = [
922
- {
923
- title: binding?.url ? "Copy share link" : "Share this session",
922
+ const root = await resolveRoot(host.client, startAtSession)
923
+ const bindings = await readBindings(host.client, root)
924
+ const live = bindings.filter((b) => b.url && b.enabled !== false)
925
+ const rows: ShareDialogSelectOption<string>[] = []
926
+ if (live.length === 0) {
927
+ rows.push({
928
+ title: "Share this session",
924
929
  value: "session:share",
925
- description: binding?.url ? "copy the share URL to the clipboard" : "Set the share flag; the server plugin creates the page",
926
- help: binding?.url
927
- ? `Copies the share URL to the clipboard.\n\nURL: ${binding.url}\nShare id: ${binding.id ?? "?"}`
928
- : "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.",
929
- },
930
- ]
931
- if (binding?.url) {
930
+ description: "set the share flag; the server plugin creates the page",
931
+ 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.",
932
+ })
933
+ } else if (live.length === 1) {
934
+ const b = live[0]
935
+ rows.push({
936
+ title: "Copy share link",
937
+ value: "session:copy",
938
+ description: "copy the share URL to the clipboard",
939
+ help: `Copies the share URL to the clipboard.\n\nURL: ${b.url}\nShare id: ${b.id ?? "?"}`,
940
+ })
941
+ } else {
942
+ for (const b of live) {
943
+ rows.push({
944
+ title: `Copy link — ${hostOf(b.url || b.server)}`,
945
+ value: `session:copy:${b.server}`,
946
+ description: b.url,
947
+ 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.`,
948
+ })
949
+ }
950
+ rows.push({
951
+ title: "Copy all links",
952
+ value: "session:copyall",
953
+ description: `${live.length} servers`,
954
+ help: `Copies every bound server's share URL.\n\n${live.map((b) => `• ${b.url}`).join("\n")}`,
955
+ })
956
+ }
957
+ if (live.length) {
932
958
  rows.push({
933
959
  title: "Stop sharing",
934
960
  value: "session:unshare",
935
- description: "delete the page(s), or leave a snapshot",
961
+ description: live.length > 1 ? `delete every page (${live.length} servers), or leave snapshots` : "delete the page, or leave a snapshot",
936
962
  danger: true,
937
- 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${[binding.url].join("\n")}\n\nSharing again later always creates fresh page(s) with new URLs.`,
963
+ 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.`,
938
964
  })
965
+ if (live.length > 1) {
966
+ rows.push({
967
+ title: "Stop on one server…",
968
+ value: "session:unshare:one",
969
+ description: "delete or snapshot a single server's page",
970
+ 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")}`,
971
+ })
972
+ const otherServers = serverEntries().filter((s) => !live.some((b) => b.server === s.url))
973
+ if (otherServers.length) {
974
+ rows.push({
975
+ title: "Share to another server…",
976
+ value: "session:more",
977
+ description: otherServers.map((s) => hostOf(s.url)).join(", "),
978
+ 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")}`,
979
+ })
980
+ }
981
+ }
939
982
  }
983
+ const binding = bindings[0]
940
984
  rows.push({
941
985
  title: "Session TTL override (days)",
942
986
  value: "session:ttl",
@@ -1054,16 +1098,90 @@ export async function settingsDialog(host: ShareHostApi, startAtSession?: string
1054
1098
 
1055
1099
  if (choice.startsWith("session:") && startAtSession) {
1056
1100
  const root = await resolveRoot(host.client, startAtSession)
1057
- const current = await readBinding(host.client, root)
1101
+ const all = await readBindings(host.client, root)
1102
+ const live = all.filter((b) => b.url && b.enabled !== false)
1103
+ const current = all[0]
1058
1104
  if (choice === "session:share") {
1059
1105
  if (!isConfigured(cfg)) {
1060
1106
  await showInfo(host.ui, { title: "Not configured", message: "Connect a server (Servers group) first, then share." })
1061
1107
  return
1062
1108
  }
1063
- const result = await confirmShare(host, root)
1109
+ const result = await shareWithConsent(host, startAtSession)
1064
1110
  host.ui.toast({ variant: result.ok ? "success" : "error", title: "OpenShare", message: result.message, duration: result.ok ? 8000 : 15000 })
1065
1111
  return
1066
1112
  }
1113
+ if (choice === "session:copy" && live[0]) {
1114
+ const okw = await host.clipboard.write(live[0].url!)
1115
+ host.ui.toast({ variant: okw ? "success" : "error", title: "OpenShare", message: okw ? `Share URL copied: ${live[0].url}` : `Clipboard failed — ${live[0].url}` })
1116
+ return
1117
+ }
1118
+ if (choice.startsWith("session:copy:") ) {
1119
+ const b = live.find((x) => `session:copy:${x.server}` === choice)
1120
+ if (b) {
1121
+ const okw = await host.clipboard.write(b.url!)
1122
+ host.ui.toast({ variant: okw ? "success" : "error", title: "OpenShare", message: okw ? `Share URL copied: ${b.url}` : `Clipboard failed — ${b.url}` })
1123
+ }
1124
+ return
1125
+ }
1126
+ if (choice === "session:copyall") {
1127
+ const joined = live.map((b) => b.url).filter(Boolean).join("\n")
1128
+ const okw = await host.clipboard.write(joined)
1129
+ host.ui.toast({ variant: okw ? "success" : "error", title: "OpenShare", message: okw ? `${live.length} share URL(s) copied` : "Clipboard failed" })
1130
+ return
1131
+ }
1132
+ if (choice === "session:unshare:one") {
1133
+ const pick = await showSelect(host.ui, {
1134
+ title: "Stop on which server",
1135
+ options: [...live.map((b) => ({ title: hostOf(b.url || b.server), value: b.server, description: b.url })), { title: "Cancel", value: "__cancel" }],
1136
+ })
1137
+ if (!pick || pick === "__cancel") return
1138
+ const target = live.find((b) => b.server === pick)
1139
+ if (!target) return
1140
+ const how = await showSelect(host.ui, {
1141
+ title: `Stop sharing — ${hostOf(target.url || target.server)}`,
1142
+ options: [
1143
+ { title: "Delete the page", value: "delete", description: "this link stops working for everyone", danger: true },
1144
+ { title: "Leave as-is (snapshot)", value: "leave", description: "stop updating; the page stays until its TTL" },
1145
+ { title: "Cancel", value: "__cancel" },
1146
+ ],
1147
+ })
1148
+ if (!how || how === "__cancel") return
1149
+ if (how === "leave") {
1150
+ await patchSessionBinding(host.client, root, (servers) => {
1151
+ delete servers[target.server]
1152
+ servers[target.server] = { optedOut: true }
1153
+ })
1154
+ host.ui.toast({ variant: "success", title: "OpenShare", message: `${hostOf(target.url || target.server)} left as-is — a snapshot until its TTL.` })
1155
+ return
1156
+ }
1157
+ const targetEntry = all.find((b) => b.server === target.server)
1158
+ if (!targetEntry?.secret) {
1159
+ host.ui.toast({ variant: "error", title: "OpenShare", message: "No secret for this page — cannot delete it server-side; binding cleared." })
1160
+ await patchSessionBinding(host.client, root, (servers) => {
1161
+ delete servers[target.server]
1162
+ })
1163
+ return
1164
+ }
1165
+ const gone = await workerDeleteShare(target.server, target.id!, targetEntry.secret)
1166
+ await patchSessionBinding(host.client, root, (servers) => {
1167
+ delete servers[target.server]
1168
+ })
1169
+ 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.` })
1170
+ return
1171
+ }
1172
+ if (choice === "session:more") {
1173
+ const otherServers = serverEntries().filter((s) => !live.some((b) => b.server === s.url))
1174
+ if (!otherServers.length) return
1175
+ const pick = await showSelect(host.ui, {
1176
+ title: "Share to another server",
1177
+ options: [...otherServers.map((s) => ({ title: hostOf(s.url), value: s.url, description: s.entry.label })), { title: "Cancel", value: "__cancel" }],
1178
+ })
1179
+ if (!pick || pick === "__cancel") return
1180
+ for (const s of serverEntries()) if (s.url !== pick) saveServerEntry(s.url, { active: undefined })
1181
+ saveServerEntry(pick, { active: true })
1182
+ host.ui.toast({ variant: "success", title: "OpenShare", message: `${hostOf(pick)} activated — this session mirrors there within seconds.` })
1183
+ return
1184
+ }
1067
1185
  if (choice === "session:unshare") {
1068
1186
  const how = await showSelect(host.ui, {
1069
1187
  title: "Stop sharing",
package/src/share-flow.ts CHANGED
@@ -18,6 +18,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs"
18
18
  import { loadConfig, moduleDir, configPaths, configHomePath, resolveServer, saveServerEntry, deleteServerEntry, METADATA_KEY, type ShareConfig, type ServerEntry } from "./config.js"
19
19
  import { dbg, dbgLog } from "./sidebar.js"
20
20
  import type { ClientApi, SessionInfo, SessionOpts } from "./core.js"
21
+ import { queueBindingWrite } from "./binding-queue.js"
21
22
  import type { ShareHostApi } from "./tui-host.js"
22
23
 
23
24
  export type SessionBinding = {
@@ -29,8 +30,6 @@ export type SessionBinding = {
29
30
  opts?: SessionOpts
30
31
  }
31
32
 
32
- export const CONSENT_KV_KEY = "openshare_consent"
33
-
34
33
  export function pluginConfigPaths(baseDir: string = moduleDir()): string[] {
35
34
  return configPaths(baseDir)
36
35
  }
@@ -174,18 +173,23 @@ export async function patchSessionBinding(
174
173
  sid: string,
175
174
  mutator: (servers: Record<string, unknown>, binding: Record<string, unknown>) => void,
176
175
  ): Promise<boolean> {
177
- const info = await getSession(client, sid)
178
- if (!info) return false
179
- const metadata: Record<string, unknown> = { ...(info.metadata ?? {}) }
180
- const raw = info.metadata?.[METADATA_KEY]
181
- const binding: Record<string, unknown> =
182
- raw && typeof raw === "object" ? { ...(raw as Record<string, unknown>) } : { v: 2, servers: {} }
183
- const servers: Record<string, unknown> =
184
- binding.servers && typeof binding.servers === "object" ? { ...(binding.servers as Record<string, unknown>) } : {}
185
- mutator(servers, binding)
186
- binding.servers = servers
187
- metadata[METADATA_KEY] = binding
188
- return patchMetadata(client, sid, metadata)
176
+ // the whole read-modify-write runs inside the shared per-session queue:
177
+ // the engine patches the same metadata (binding refresh) and interleaved
178
+ // RMWs silently lose one side's update (see binding-queue.ts)
179
+ return queueBindingWrite(sid, async () => {
180
+ const info = await getSession(client, sid)
181
+ if (!info) return false
182
+ const metadata: Record<string, unknown> = { ...(info.metadata ?? {}) }
183
+ const raw = info.metadata?.[METADATA_KEY]
184
+ const binding: Record<string, unknown> =
185
+ raw && typeof raw === "object" ? { ...(raw as Record<string, unknown>) } : { v: 2, servers: {} }
186
+ const servers: Record<string, unknown> =
187
+ binding.servers && typeof binding.servers === "object" ? { ...(binding.servers as Record<string, unknown>) } : {}
188
+ mutator(servers, binding)
189
+ binding.servers = servers
190
+ metadata[METADATA_KEY] = binding
191
+ return patchMetadata(client, sid, metadata)
192
+ })
189
193
  }
190
194
 
191
195
  /** The display/primary binding: the active server's entry when bound
@@ -482,7 +486,20 @@ export async function waitForBinding(
482
486
 
483
487
  export type FlowResult = { ok: boolean; message: string; url?: string; unconfigured?: boolean }
484
488
 
485
- export async function runShareFlow(host: ShareHostApi, sessionID: string | undefined): Promise<FlowResult> {
489
+ /** Consent gate: EVERY share creation asks explicitly (per owner
490
+ * directive — no remembered consent, no silent creates). */
491
+ export async function requestShareConsent(host: ShareHostApi): Promise<boolean> {
492
+ return host.ui.showConfirm({
493
+ title: "Share Session",
494
+ message: "Publish this session (and its subagent sessions) to your openshare worker?\nThe link lets anyone with it read the conversation.",
495
+ confirmLabel: "Share",
496
+ })
497
+ }
498
+
499
+ /** runShareFlow with the consent gate applied: share copies the link,
500
+ * unshared asks consent once, then creates. Every create path routes
501
+ * through here (/share, settings, gone-again). */
502
+ export async function shareWithConsent(host: ShareHostApi, sessionID: string | undefined): Promise<FlowResult> {
486
503
  if (!sessionID) return { ok: false, message: "No active session to share." }
487
504
  const cfg = currentConfig()
488
505
  if (!isConfigured(cfg)) {
@@ -495,10 +512,7 @@ export async function runShareFlow(host: ShareHostApi, sessionID: string | undef
495
512
  await host.clipboard.write(existing.url)
496
513
  return { ok: true, message: `Share URL copied to clipboard: ${existing.url}`, url: existing.url }
497
514
  }
498
- if (!host.kv.get(CONSENT_KV_KEY, false)) {
499
- // caller shows the consent dialog before invoking confirmShare()
500
- return { ok: false, message: "consent required" }
501
- }
515
+ if (!(await requestShareConsent(host))) return { ok: false, message: "Share cancelled." }
502
516
  return confirmShare(host, root)
503
517
  }
504
518
 
package/src/sidebar.tsx CHANGED
@@ -16,24 +16,11 @@
16
16
  // proven consumer returns box-rooted trees (opencode's own default
17
17
  // children, magic-context's sidebar, the host's slot self-test); text
18
18
  // nodes stay LEAVES here — string content only.
19
- //
20
- // TEMPORARY crash-hunt instrumentation (owner-approved 2026-10-19): every
21
- // render logs its inputs (with TYPES), the computed locals, and a dump of
22
- // the returned element tree to %TEMP%/openshare-tui-debug.log via
23
- // synchronous appends, so a crash mid-render still leaves evidence. Any
24
- // throw inside the renderer is logged with its stack, then rethrown.
25
- // Remove once the live crash is cornered.
26
19
 
27
20
  import type { JSX } from "@opentui/solid"
28
21
  import { bindingsOf, bindingOf, verifyShare } from "./share-flow.js"
29
22
  import type { TuiPluginApi, TuiSlotContext } from "@opencode-ai/plugin/tui"
30
23
  import type { Session } from "@opencode-ai/sdk/v2"
31
- import { appendFileSync } from "node:fs"
32
- import { tmpdir } from "node:os"
33
- import { join } from "node:path"
34
-
35
- const DBG_FILE = join(tmpdir(), "openshare-tui-debug.log")
36
-
37
24
  /** value -> `kind:preview` so the log shows TYPES (the crash was a type bug) */
38
25
  export function dbg(v: unknown, depth = 0): string {
39
26
  const t = typeof v
@@ -52,30 +39,9 @@ export function dbg(v: unknown, depth = 0): string {
52
39
  }
53
40
  }
54
41
 
55
- /** shallow structural dump of a rendered element tree (type names, children kinds) */
56
- function dbgEl(el: unknown, depth = 0): string {
57
- if (el === null || el === undefined || typeof el === "string" || typeof el === "number" || typeof el === "boolean") return dbg(el)
58
- if (Array.isArray(el)) return `[${el.map((x) => dbgEl(x, depth)).join(",")}]`
59
- if (typeof el === "function") return "fn-node"
60
- if (depth >= 4) return "…"
61
- try {
62
- const rec = el as Record<string, unknown>
63
- const t = dbg(rec.t ?? rec.type, 2)
64
- const kids = rec.children
65
- const props = Object.keys(rec).filter((k) => !["t", "type", "children"].includes(k))
66
- return `<${t} ${props.join(" ")}>${kids === undefined ? "" : dbgEl(kids, depth + 1)}</${t}>`
67
- } catch (e) {
68
- return `el?-${dbg(e)}`
69
- }
70
- }
71
-
72
- export function dbgLog(line: string): void {
73
- try {
74
- appendFileSync(DBG_FILE, `${new Date().toISOString()} ${line}\n`)
75
- } catch {}
76
- }
77
-
78
- let renderSeq = 0
42
+ /** no-op: the TUI debug log file was a crash-hunt aid and must not write
43
+ * to temp any more (owner request 2026-10-21) */
44
+ export function dbgLog(_line: string): void {}
79
45
 
80
46
  /** Share liveness cache for the chip: a binding can claim "shared" long
81
47
  * after the page died server-side (purge/TTL/delete with no event ever
@@ -102,13 +68,10 @@ type SidebarTitleProps = {
102
68
  }
103
69
 
104
70
  export function createSidebarSlot(api: TuiPluginApi, opts?: { onShareCommand?: () => void }) {
105
- dbgLog(`createSidebarSlot called; api.state=${!!api.state} api.theme=${!!api.theme} api.version=${dbg(api.app?.version)}`)
106
71
  return {
107
72
  slots: {
108
73
  sidebar_title: (ctx: TuiSlotContext, props: SidebarTitleProps): JSX.Element => {
109
- const seq = ++renderSeq
110
74
  try {
111
- dbgLog(`render#${seq} ctx=${dbg(ctx)} props=${dbg(props)}`)
112
75
  const sid = typeof props?.session_id === "string" ? props.session_id : ""
113
76
  const title = String(props?.title ?? "") || "Session"
114
77
  const session = sid ? api.state.session.get(sid) : undefined
@@ -122,9 +85,7 @@ export function createSidebarSlot(api: TuiPluginApi, opts?: { onShareCommand?: (
122
85
  const anyGone = verdicts.some((v) => v?.status === "gone")
123
86
  const multi = liveCount > 1 ? `·${liveCount} ` : ""
124
87
  const theme = ctx?.theme?.current ?? api.theme.current
125
- dbgLog(
126
- `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)}`,
127
- )
88
+
128
89
  // chip on its own line under the title (takes the blank line that
129
90
  // separated the title block from the context blocks); clicking it
130
91
  // copies the share link — proven pattern: onMouseDown on a box.
@@ -175,10 +136,8 @@ export function createSidebarSlot(api: TuiPluginApi, opts?: { onShareCommand?: (
175
136
  ) : null}
176
137
  </box>
177
138
  )
178
- dbgLog(`render#${seq} RETURNS ${dbgEl(el)}`)
179
139
  return el
180
140
  } catch (error) {
181
- dbgLog(`render#${seq} THREW ${(error as Error)?.stack ?? dbg(error)} — rethrowing`)
182
141
  throw error
183
142
  }
184
143
  },