@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/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(
@@ -799,9 +829,11 @@ export function PromptDialog(props: {
799
829
  export function showPromptOnce(api: TuiPluginApi, props: { title: string; placeholder?: string; value?: string }): Promise<string | undefined> {
800
830
  return new Promise((resolve) => {
801
831
  let settled = false
832
+ const release = claimDialog(api)
802
833
  const done = (value: string | undefined, _clear = false) => {
803
834
  if (settled) return
804
835
  settled = true
836
+ release()
805
837
  dbgLog(`prompt(${props.title}) -> ${value === undefined ? "cancel" : `len=${value.length}`}`)
806
838
  resolve(value)
807
839
  }
@@ -829,9 +861,11 @@ export type SizePickerResult = { width: DialogWidth; heightPercent: number }
829
861
  export function showSizePickerOnce(api: TuiPluginApi, current: SizePickerResult): Promise<SizePickerResult | undefined> {
830
862
  return new Promise((resolve) => {
831
863
  let settled = false
864
+ const release = claimDialog(api)
832
865
  const done = (value: SizePickerResult | undefined) => {
833
866
  if (settled) return
834
867
  settled = true
868
+ release()
835
869
  resolve(value)
836
870
  }
837
871
  api.ui.dialog.replace(
package/src/sidebar.tsx CHANGED
@@ -28,12 +28,6 @@ import type { JSX } from "@opentui/solid"
28
28
  import { bindingsOf, bindingOf, verifyShare } from "./share-flow.js"
29
29
  import type { TuiPluginApi, TuiSlotContext } from "@opencode-ai/plugin/tui"
30
30
  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
31
  /** value -> `kind:preview` so the log shows TYPES (the crash was a type bug) */
38
32
  export function dbg(v: unknown, depth = 0): string {
39
33
  const t = typeof v
@@ -69,13 +63,9 @@ function dbgEl(el: unknown, depth = 0): string {
69
63
  }
70
64
  }
71
65
 
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
66
+ /** no-op: the TUI debug log file was a crash-hunt aid and must not write
67
+ * to temp any more (owner request 2026-10-21) */
68
+ export function dbgLog(_line: string): void {}
79
69
 
80
70
  /** Share liveness cache for the chip: a binding can claim "shared" long
81
71
  * after the page died server-side (purge/TTL/delete with no event ever
@@ -102,13 +92,10 @@ type SidebarTitleProps = {
102
92
  }
103
93
 
104
94
  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
95
  return {
107
96
  slots: {
108
97
  sidebar_title: (ctx: TuiSlotContext, props: SidebarTitleProps): JSX.Element => {
109
- const seq = ++renderSeq
110
98
  try {
111
- dbgLog(`render#${seq} ctx=${dbg(ctx)} props=${dbg(props)}`)
112
99
  const sid = typeof props?.session_id === "string" ? props.session_id : ""
113
100
  const title = String(props?.title ?? "") || "Session"
114
101
  const session = sid ? api.state.session.get(sid) : undefined
@@ -122,9 +109,7 @@ export function createSidebarSlot(api: TuiPluginApi, opts?: { onShareCommand?: (
122
109
  const anyGone = verdicts.some((v) => v?.status === "gone")
123
110
  const multi = liveCount > 1 ? `·${liveCount} ` : ""
124
111
  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
- )
112
+
128
113
  // chip on its own line under the title (takes the blank line that
129
114
  // separated the title block from the context blocks); clicking it
130
115
  // copies the share link — proven pattern: onMouseDown on a box.
@@ -175,10 +160,8 @@ export function createSidebarSlot(api: TuiPluginApi, opts?: { onShareCommand?: (
175
160
  ) : null}
176
161
  </box>
177
162
  )
178
- dbgLog(`render#${seq} RETURNS ${dbgEl(el)}`)
179
163
  return el
180
164
  } catch (error) {
181
- dbgLog(`render#${seq} THREW ${(error as Error)?.stack ?? dbg(error)} — rethrowing`)
182
165
  throw error
183
166
  }
184
167
  },
package/src/v2-server.ts CHANGED
@@ -74,114 +74,10 @@ type V2Context = {
74
74
  signal?: AbortSignal
75
75
  }
76
76
 
77
- // --- v2 message -> v1 items ---------------------------------------------------
78
-
79
- function toolContentText(content: unknown): string {
80
- if (typeof content === "string") return content
81
- if (Array.isArray(content)) {
82
- return content
83
- .map((item) => {
84
- if (typeof item === "string") return item
85
- if (item && typeof item === "object") {
86
- const record = item as Record<string, unknown>
87
- if (typeof record.text === "string") return record.text
88
- }
89
- return ""
90
- })
91
- .join("")
92
- }
93
- return ""
94
- }
95
-
96
- function convertToolState(state: unknown): Record<string, unknown> {
97
- if (!state || typeof state !== "object") return { status: "pending" }
98
- const record = state as Record<string, unknown>
99
- const status = String(record.status ?? "pending")
100
- const mapped: Record<string, unknown> = {}
101
- if (status === "streaming" || status === "running") mapped.status = "running"
102
- else if (status === "completed") mapped.status = "completed"
103
- else if (status === "failed" || status === "error") mapped.status = "error"
104
- else mapped.status = "pending"
105
- if (record.input !== undefined) mapped.input = record.input
106
- if (record.output !== undefined) mapped.output = toolContentText(record.output)
107
- if (record.error !== undefined) mapped.error = record.error
108
- if (record.time !== undefined) mapped.time = record.time
109
- return mapped
110
- }
111
-
112
- /** v2 flat message -> { info, parts } in the v1 shapes the viewer speaks. */
113
- export function convertV2Message(message: V2Message, sessionID: string): { info: MessageInfo; parts: PartInfo[] } | undefined {
114
- if (!message || typeof message.id !== "string") return undefined
115
- const base = {
116
- id: message.id,
117
- sessionID,
118
- }
119
- if (message.type === "user") {
120
- return {
121
- info: {
122
- ...base,
123
- role: "user",
124
- time: { created: message.time?.created ?? 0 },
125
- text: message.text ?? "",
126
- },
127
- parts: [],
128
- }
129
- }
130
- if (message.type === "assistant") {
131
- const parts: PartInfo[] = []
132
- const content = Array.isArray(message.content) ? message.content : []
133
- content.forEach((block, index) => {
134
- const record = block as Record<string, unknown>
135
- const kind = String(record.type ?? "")
136
- if (kind === "text") {
137
- parts.push({
138
- id: `${message.id}_t${index}`,
139
- messageID: message.id,
140
- sessionID,
141
- type: "text",
142
- text: String(record.text ?? ""),
143
- synthetic: false,
144
- time: { start: message.time?.created ?? 0, end: message.time?.completed ?? 0 },
145
- })
146
- } else if (kind === "reasoning") {
147
- parts.push({
148
- id: `${message.id}_r${index}`,
149
- messageID: message.id,
150
- sessionID,
151
- type: "reasoning",
152
- text: String(record.text ?? ""),
153
- synthetic: false,
154
- time: { start: message.time?.created ?? 0, end: message.time?.completed ?? 0 },
155
- })
156
- } else if (kind === "tool") {
157
- parts.push({
158
- id: String(record.id ?? `${message.id}_tool${index}`),
159
- messageID: message.id,
160
- sessionID,
161
- type: "tool",
162
- tool: String(record.name ?? "unknown"),
163
- state: convertToolState(record.state),
164
- synthetic: false,
165
- })
166
- }
167
- })
168
- return {
169
- info: {
170
- ...base,
171
- role: "assistant",
172
- agent: message.agent,
173
- model: message.model
174
- ? { providerID: message.model.providerID, modelID: message.model.modelID ?? message.model.id }
175
- : undefined,
176
- time: { created: message.time?.created ?? 0, completed: message.time?.completed ?? 0 },
177
- },
178
- parts,
179
- }
180
- }
181
- return undefined
182
- }
183
-
184
77
  // --- v2 client -> engine ClientApi ---------------------------------------------
78
+ // Contract v2: messages pass through VERBATIM as v2 session messages (the
79
+ // engine's ingestion detects v2-native shapes and enqueues them directly —
80
+ // no v1 conversion, no dropped compaction, no dropped user text).
185
81
 
186
82
  function adaptSessionClient(session: V2SessionClient | undefined, message: V2MessageClient | undefined, directory: string | undefined): ClientApi | undefined {
187
83
  if (!session) return undefined
@@ -203,7 +99,7 @@ function adaptSessionClient(session: V2SessionClient | undefined, message: V2Mes
203
99
 
204
100
  const listMessages = async (sessionID: string) => {
205
101
  if (!message) return []
206
- const out: Array<{ info: MessageInfo; parts: PartInfo[] }> = []
102
+ const out: Array<{ info: unknown; parts: unknown[] }> = []
207
103
  let cursor: string | undefined
208
104
  for (let page = 0; page < 50; page++) {
209
105
  const res = (await message.list({ sessionID, limit: 100, cursor })) as
@@ -212,8 +108,9 @@ function adaptSessionClient(session: V2SessionClient | undefined, message: V2Mes
212
108
  const data = res?.data
213
109
  if (!Array.isArray(data)) break
214
110
  for (const raw of data) {
215
- const converted = convertV2Message(raw, sessionID)
216
- if (converted) out.push(converted)
111
+ if (raw && typeof raw === "object" && typeof (raw as { id?: unknown }).id === "string") {
112
+ out.push({ info: raw, parts: [] })
113
+ }
217
114
  }
218
115
  const next = res?.cursor?.next ?? undefined
219
116
  if (!next || data.length === 0) break