@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
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:
|
|
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
|
-
|
|
216
|
-
|
|
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
|
|
@@ -332,7 +229,20 @@ export function createV2ServerSetup() {
|
|
|
332
229
|
// the client adapter and feed them into the engine as the v1 events it
|
|
333
230
|
// understands (session.updated / message.updated / message.part.updated).
|
|
334
231
|
// The engine's keyed queue makes repeated re-syncs idempotent.
|
|
232
|
+
// NOTE: no content-hash dedupe here — a refresh that runs while the
|
|
233
|
+
// session is still dormant (pre-flag) would poison the cache without
|
|
234
|
+
// emitting anything, and the flag-time refresh would then SKIP the
|
|
235
|
+
// unchanged messages (live-caught in the v2 mock suite: the share's
|
|
236
|
+
// first assistant content arrived only as the next change). The dedupe
|
|
237
|
+
// belongs engine-side, where dormancy is known, if it's ever needed.
|
|
335
238
|
const pendingRefresh = new Map<string, ReturnType<typeof setTimeout>>()
|
|
239
|
+
// queued inbox inputs per session: v2 persists the prompt in the inbox
|
|
240
|
+
// (delivery "queue") while the current turn still runs — the message
|
|
241
|
+
// row can exist before the model ever sees it, and a mid-turn refresh
|
|
242
|
+
// would surface it early. Held here until session.inbox.delivered
|
|
243
|
+
// fires (the turn actually starts); "steer" inputs inject into the
|
|
244
|
+
// live run, so they pass straight through.
|
|
245
|
+
const queuedInbox = new Map<string, Set<string>>()
|
|
336
246
|
const refresh = (sessionID: string) => {
|
|
337
247
|
let timer = pendingRefresh.get(sessionID)
|
|
338
248
|
if (timer) clearTimeout(timer)
|
|
@@ -346,7 +256,9 @@ export function createV2ServerSetup() {
|
|
|
346
256
|
})
|
|
347
257
|
const messages = await (client as NonNullable<typeof client>).session.messages?.({ path: { id: sessionID } })
|
|
348
258
|
const list = ((messages as { data?: unknown })?.data ?? []) as Array<{ info: MessageInfo; parts: PartInfo[] }>
|
|
259
|
+
const held = queuedInbox.get(sessionID)
|
|
349
260
|
for (const message of list) {
|
|
261
|
+
if (held?.has(message.info.id)) continue // still queued — not sent to the model yet
|
|
350
262
|
await engine.hooks.event({
|
|
351
263
|
event: { type: "message.updated", properties: { sessionID, info: message.info } },
|
|
352
264
|
})
|
|
@@ -380,6 +292,41 @@ export function createV2ServerSetup() {
|
|
|
380
292
|
await engine.hooks.event({ event: { type, properties: { sessionID: sid } } })
|
|
381
293
|
continue
|
|
382
294
|
}
|
|
295
|
+
if (type.startsWith("session.inbox.")) {
|
|
296
|
+
// queued-prompt lifecycle: hold delivery="queue" inputs off
|
|
297
|
+
// the share until they are actually delivered to the model
|
|
298
|
+
// (cancelled ones never appear). inboxID shares the message
|
|
299
|
+
// id space (the inbox item promotes into that message row).
|
|
300
|
+
const data = (event.data ?? {}) as { inboxID?: string; item?: { delivery?: string } }
|
|
301
|
+
const inboxID = data.inboxID
|
|
302
|
+
if (inboxID) {
|
|
303
|
+
let set = queuedInbox.get(sid)
|
|
304
|
+
if (!set) {
|
|
305
|
+
set = new Set()
|
|
306
|
+
queuedInbox.set(sid, set)
|
|
307
|
+
}
|
|
308
|
+
if (type === "session.inbox.enqueued" && data.item?.delivery === "queue") {
|
|
309
|
+
set.add(inboxID)
|
|
310
|
+
} else if (type === "session.inbox.delivered") {
|
|
311
|
+
set.delete(inboxID)
|
|
312
|
+
refresh(sid) // the held prompt's turn is starting now
|
|
313
|
+
} else if (type === "session.inbox.cancelled") {
|
|
314
|
+
// cancelled-before-run: the prompt never reaches the
|
|
315
|
+
// model — keep it held forever so no later refresh
|
|
316
|
+
// surfaces the orphaned row
|
|
317
|
+
set.add(inboxID)
|
|
318
|
+
} else if (type === "session.inbox.delivery.changed") {
|
|
319
|
+
// steer inputs inject into the live run — the model
|
|
320
|
+
// sees them; only "queue" holds
|
|
321
|
+
if (data.item?.delivery === "queue") set.add(inboxID)
|
|
322
|
+
else {
|
|
323
|
+
set.delete(inboxID)
|
|
324
|
+
refresh(sid)
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
continue
|
|
329
|
+
}
|
|
383
330
|
if (type === "message.removed" || type === "message.part.removed") {
|
|
384
331
|
// undo cleanup: forwarded verbatim — the engine emits the
|
|
385
332
|
// tombstones that purge the reverted items from the share
|