@mirrowel/opencode-openshare 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +175 -2
- package/ci/README.md +56 -0
- package/ci/openshare.config.json +5 -0
- package/ci/openshare.js +2265 -0
- package/dist/commands.d.ts +7 -0
- package/dist/commands.js +440 -0
- package/dist/config.d.ts +134 -0
- package/dist/config.js +517 -0
- package/dist/core.d.ts +117 -0
- package/dist/core.js +1944 -0
- package/dist/dialogs.d.ts +94 -0
- package/dist/dialogs.js +725 -0
- package/dist/server.d.ts +64 -0
- package/dist/server.js +30 -0
- package/dist/settings.d.ts +24 -0
- package/dist/settings.js +1226 -0
- package/dist/share-flow.d.ts +287 -0
- package/dist/share-flow.js +516 -0
- package/dist/sidebar.d.ts +19 -0
- package/dist/sidebar.js +144 -0
- package/dist/tui-host.d.ts +158 -0
- package/dist/tui-host.js +6 -0
- package/dist/tui.d.ts +17 -0
- package/dist/tui.js +4762 -0
- package/dist/ui-prefs.d.ts +16 -0
- package/dist/ui-prefs.js +27 -0
- package/dist/v2-server.d.ts +81 -0
- package/dist/v2-server.js +402 -0
- package/dist/v2-tui.d.ts +24 -0
- package/dist/v2-tui.js +313 -0
- package/dist/v2-types.d.ts +134 -0
- package/dist/v2-types.js +8 -0
- package/openshare.config.json +16 -0
- package/package.json +98 -4
- package/src/banner.txt +22 -0
- package/src/commands.ts +452 -0
- package/src/config.ts +518 -0
- package/src/core.ts +1997 -0
- package/src/dialogs.tsx +971 -0
- package/src/server.ts +47 -0
- package/src/settings.ts +1239 -0
- package/src/share-flow.ts +551 -0
- package/src/sidebar.tsx +187 -0
- package/src/tui-host.ts +134 -0
- package/src/tui.tsx +318 -0
- package/src/ui-prefs.ts +36 -0
- package/src/v2-server.ts +438 -0
- package/src/v2-tui.ts +315 -0
- package/src/v2-types.ts +102 -0
- package/tsconfig.json +18 -0
package/src/v2-server.ts
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
// Open(code)Share — OpenCode v2 server setup.
|
|
2
|
+
//
|
|
3
|
+
// The v2 host validates `default` as `{ id, setup }` and calls setup with
|
|
4
|
+
// the v2 plugin context (packages/plugin/src/promise). The v2 world is
|
|
5
|
+
// shaped differently from v1 in three places this adapter bridges:
|
|
6
|
+
//
|
|
7
|
+
// 1. Events: v2 publishes fine-grained domain events
|
|
8
|
+
// (session.metadata.updated, session.text.started, session.tool.*,
|
|
9
|
+
// session.step.*) — not v1's coarse session/message/part updates.
|
|
10
|
+
// Mapping strategy: structural events (created/deleted/metadata/
|
|
11
|
+
// status) pass into the engine directly; content events trigger a
|
|
12
|
+
// debounced re-sync that pulls the fresh message list through the
|
|
13
|
+
// client. The engine's keyed queue makes re-syncs idempotent, and
|
|
14
|
+
// correctness never depends on decoding every fine-grained shape.
|
|
15
|
+
//
|
|
16
|
+
// 2. Client: the v2 client speaks /api/session with {sessionID} inputs,
|
|
17
|
+
// unwrapped outputs and cursor pagination. The adapter below presents
|
|
18
|
+
// the v1-shaped ClientApi the engine already speaks, including the
|
|
19
|
+
// v2-message -> v1 message+parts conversion (the worker/viewer
|
|
20
|
+
// contract is the v1 Data[] format).
|
|
21
|
+
//
|
|
22
|
+
// 3. Share mode: v2 auto-discovers the package's ./tui entrypoint, so
|
|
23
|
+
// presence detection cannot scan tui.json. The adapter applies the
|
|
24
|
+
// v2-native rule instead: when the plugin's own package declares the
|
|
25
|
+
// tui entrypoint (oc-plugin), a human drives sharing -> manual; the
|
|
26
|
+
// single-file build (no package) stays all-mode. Explicit shareMode
|
|
27
|
+
// config still wins.
|
|
28
|
+
|
|
29
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
30
|
+
import { join } from "node:path"
|
|
31
|
+
import { METADATA_KEY, moduleDir, packageRoot } from "./config.js"
|
|
32
|
+
import { createEngine, type ClientApi, type Engine, type MessageInfo, type PartInfo } from "./core.js"
|
|
33
|
+
|
|
34
|
+
type V2Event = {
|
|
35
|
+
type: string
|
|
36
|
+
data?: Record<string, unknown>
|
|
37
|
+
[key: string]: unknown
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type V2Message = {
|
|
41
|
+
id: string
|
|
42
|
+
type: string
|
|
43
|
+
time?: { created?: number; completed?: number; streamed?: number }
|
|
44
|
+
text?: string
|
|
45
|
+
agent?: string
|
|
46
|
+
model?: { providerID: string; id?: string; modelID?: string }
|
|
47
|
+
content?: Array<Record<string, unknown>>
|
|
48
|
+
error?: { message?: string } | null
|
|
49
|
+
metadata?: Record<string, unknown>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type V2SessionClient = {
|
|
53
|
+
get(input: { sessionID: string }): Promise<unknown>
|
|
54
|
+
update(input: { sessionID: string; title?: string; metadata?: Record<string, unknown> }): Promise<unknown>
|
|
55
|
+
list(input?: { directory?: string; parentID?: string; cursor?: string; limit?: number }): Promise<unknown>
|
|
56
|
+
diff(input: { sessionID: string }): Promise<unknown>
|
|
57
|
+
remove?(input: { sessionID: string }): Promise<unknown>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type V2MessageClient = {
|
|
61
|
+
list(input: { sessionID: string; limit?: number; cursor?: string }): Promise<unknown>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
type V2Context = {
|
|
65
|
+
event?: {
|
|
66
|
+
subscribe(options?: { signal?: AbortSignal }): AsyncIterable<V2Event>
|
|
67
|
+
}
|
|
68
|
+
client?: {
|
|
69
|
+
session?: V2SessionClient
|
|
70
|
+
message?: V2MessageClient
|
|
71
|
+
}
|
|
72
|
+
options?: Record<string, unknown>
|
|
73
|
+
app?: { name?: string; version?: string; channel?: string }
|
|
74
|
+
signal?: AbortSignal
|
|
75
|
+
}
|
|
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
|
+
// --- v2 client -> engine ClientApi ---------------------------------------------
|
|
185
|
+
|
|
186
|
+
function adaptSessionClient(session: V2SessionClient | undefined, message: V2MessageClient | undefined, directory: string | undefined): ClientApi | undefined {
|
|
187
|
+
if (!session) return undefined
|
|
188
|
+
const loc = () => (directory ? { directory } : undefined)
|
|
189
|
+
|
|
190
|
+
const listAll = async () => {
|
|
191
|
+
const out: unknown[] = []
|
|
192
|
+
let cursor: string | undefined
|
|
193
|
+
for (let page = 0; page < 50; page++) {
|
|
194
|
+
const res = (await session.list({ ...loc(), cursor, limit: 100 })) as { data?: unknown[]; cursor?: { next?: string | null } } | undefined
|
|
195
|
+
const data = res?.data
|
|
196
|
+
if (Array.isArray(data)) out.push(...data)
|
|
197
|
+
const next = res?.cursor?.next ?? undefined
|
|
198
|
+
if (!next || !Array.isArray(data) || data.length === 0) break
|
|
199
|
+
cursor = next
|
|
200
|
+
}
|
|
201
|
+
return out
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const listMessages = async (sessionID: string) => {
|
|
205
|
+
if (!message) return []
|
|
206
|
+
const out: Array<{ info: MessageInfo; parts: PartInfo[] }> = []
|
|
207
|
+
let cursor: string | undefined
|
|
208
|
+
for (let page = 0; page < 50; page++) {
|
|
209
|
+
const res = (await message.list({ sessionID, limit: 100, cursor })) as
|
|
210
|
+
| { data?: V2Message[]; cursor?: { next?: string | null } }
|
|
211
|
+
| undefined
|
|
212
|
+
const data = res?.data
|
|
213
|
+
if (!Array.isArray(data)) break
|
|
214
|
+
for (const raw of data) {
|
|
215
|
+
const converted = convertV2Message(raw, sessionID)
|
|
216
|
+
if (converted) out.push(converted)
|
|
217
|
+
}
|
|
218
|
+
const next = res?.cursor?.next ?? undefined
|
|
219
|
+
if (!next || data.length === 0) break
|
|
220
|
+
cursor = next
|
|
221
|
+
}
|
|
222
|
+
return out
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
session: {
|
|
227
|
+
async get(input) {
|
|
228
|
+
const res = await session.get({ sessionID: input.path.id })
|
|
229
|
+
return { data: res }
|
|
230
|
+
},
|
|
231
|
+
async update(input) {
|
|
232
|
+
const body = input.body as { title?: string; metadata?: Record<string, unknown> }
|
|
233
|
+
const res = await session.update({ sessionID: input.path.id, title: body.title, metadata: body.metadata })
|
|
234
|
+
return { data: res }
|
|
235
|
+
},
|
|
236
|
+
async list() {
|
|
237
|
+
return { data: await listAll() }
|
|
238
|
+
},
|
|
239
|
+
async messages(input) {
|
|
240
|
+
return { data: await listMessages(input.path.id) }
|
|
241
|
+
},
|
|
242
|
+
async diff(input) {
|
|
243
|
+
const res = await session.diff({ sessionID: input.path.id })
|
|
244
|
+
return { data: res }
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// --- event classification --------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
const SESSION_ITEM_EVENTS = new Set([
|
|
253
|
+
"session.created",
|
|
254
|
+
"session.renamed",
|
|
255
|
+
"session.metadata.updated",
|
|
256
|
+
"session.agent.selected",
|
|
257
|
+
"session.model.selected",
|
|
258
|
+
"session.moved",
|
|
259
|
+
"session.permissions",
|
|
260
|
+
"session.compaction.started",
|
|
261
|
+
"session.compaction.ended",
|
|
262
|
+
])
|
|
263
|
+
|
|
264
|
+
const CONTENT_EVENTS_PREFIXES = [
|
|
265
|
+
"session.text.",
|
|
266
|
+
"session.reasoning.",
|
|
267
|
+
"session.tool.",
|
|
268
|
+
"session.message.",
|
|
269
|
+
"session.step.streamed",
|
|
270
|
+
]
|
|
271
|
+
|
|
272
|
+
const BUSY_EVENTS = new Set(["session.step.started", "session.execution.started"])
|
|
273
|
+
const IDLE_EVENTS = new Set(["session.step.ended", "session.execution.succeeded", "session.execution.interrupted"])
|
|
274
|
+
const ERROR_EVENTS = new Set(["session.step.failed", "session.tool.failed", "session.retry.scheduled"])
|
|
275
|
+
|
|
276
|
+
function eventSessionID(event: V2Event): string | undefined {
|
|
277
|
+
const data = event.data
|
|
278
|
+
if (data && typeof data === "object" && typeof (data as { sessionID?: unknown }).sessionID === "string") {
|
|
279
|
+
return (data as { sessionID: string }).sessionID
|
|
280
|
+
}
|
|
281
|
+
return undefined
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// --- the setup ---------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
function resolveV2ShareMode(options: Record<string, unknown> | undefined): "all" | "manual" | undefined {
|
|
287
|
+
const explicit = options?.["shareMode"] ?? process.env.OPENSHARE_SHARE_MODE
|
|
288
|
+
if (explicit === "all" || explicit === "manual") return explicit
|
|
289
|
+
// v2-native presence: the v2 host auto-loads ./tui for packages that
|
|
290
|
+
// declare it. The single-file build ships no package.json next to it.
|
|
291
|
+
try {
|
|
292
|
+
const root = packageRoot(moduleDir())
|
|
293
|
+
const pkgPath = join(root, "package.json")
|
|
294
|
+
if (existsSync(pkgPath)) {
|
|
295
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"))
|
|
296
|
+
const declared = pkg["oc-plugin"]
|
|
297
|
+
if (Array.isArray(declared) && declared.includes("tui")) return "manual"
|
|
298
|
+
}
|
|
299
|
+
} catch {}
|
|
300
|
+
return undefined
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function createV2ServerSetup() {
|
|
304
|
+
return async (context: V2Context): Promise<(() => void) | void> => {
|
|
305
|
+
if (!context?.event?.subscribe || !context.client?.session) {
|
|
306
|
+
// v1 directory loads call BOTH `server` and `setup` on the default
|
|
307
|
+
// export; the v1 input arrives here and trips this guard. That is
|
|
308
|
+
// expected on v1 — stay inert and quiet (debug file only).
|
|
309
|
+
if (process.env.OPENSHARE_DEBUG_FILE) {
|
|
310
|
+
try {
|
|
311
|
+
console.error(`[${METADATA_KEY}] v2 setup not applicable on this host — sharing handled by the server entry`)
|
|
312
|
+
} catch {}
|
|
313
|
+
}
|
|
314
|
+
return () => {}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const directory = (context as { directory?: string }).directory
|
|
318
|
+
const client = adaptSessionClient(context.client.session, context.client.message, directory)
|
|
319
|
+
const modeOverride = resolveV2ShareMode(context.options)
|
|
320
|
+
const engine: Engine = createEngine({
|
|
321
|
+
client,
|
|
322
|
+
pluginDir: moduleDir(),
|
|
323
|
+
directory,
|
|
324
|
+
options: modeOverride ? { ...context.options, shareMode: modeOverride } : context.options,
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
if (!engine.hooks.event) {
|
|
328
|
+
return () => {}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// debounced content re-sync per session: fetch info + messages through
|
|
332
|
+
// the client adapter and feed them into the engine as the v1 events it
|
|
333
|
+
// understands (session.updated / message.updated / message.part.updated).
|
|
334
|
+
// The engine's keyed queue makes repeated re-syncs idempotent.
|
|
335
|
+
const pendingRefresh = new Map<string, ReturnType<typeof setTimeout>>()
|
|
336
|
+
const refresh = (sessionID: string) => {
|
|
337
|
+
let timer = pendingRefresh.get(sessionID)
|
|
338
|
+
if (timer) clearTimeout(timer)
|
|
339
|
+
timer = setTimeout(() => {
|
|
340
|
+
pendingRefresh.delete(sessionID)
|
|
341
|
+
void (async () => {
|
|
342
|
+
try {
|
|
343
|
+
const info = await (client as NonNullable<typeof client>).session.get({ path: { id: sessionID } })
|
|
344
|
+
await engine.hooks.event({
|
|
345
|
+
event: { type: "session.updated", properties: { sessionID, info: (info as { data?: unknown }).data ?? info } },
|
|
346
|
+
})
|
|
347
|
+
const messages = await (client as NonNullable<typeof client>).session.messages?.({ path: { id: sessionID } })
|
|
348
|
+
const list = ((messages as { data?: unknown })?.data ?? []) as Array<{ info: MessageInfo; parts: PartInfo[] }>
|
|
349
|
+
for (const message of list) {
|
|
350
|
+
await engine.hooks.event({
|
|
351
|
+
event: { type: "message.updated", properties: { sessionID, info: message.info } },
|
|
352
|
+
})
|
|
353
|
+
for (const part of message.parts ?? []) {
|
|
354
|
+
await engine.hooks.event({
|
|
355
|
+
event: { type: "message.part.updated", properties: { sessionID, part } },
|
|
356
|
+
})
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
} catch {
|
|
360
|
+
/* refresh is best-effort; the next event retries */
|
|
361
|
+
}
|
|
362
|
+
})()
|
|
363
|
+
}, 400)
|
|
364
|
+
pendingRefresh.set(sessionID, timer)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const unsubscribe = (() => {
|
|
368
|
+
let stopped = false
|
|
369
|
+
const run = async () => {
|
|
370
|
+
try {
|
|
371
|
+
const stream = context.event!.subscribe!({ signal: context.signal })
|
|
372
|
+
for await (const event of stream) {
|
|
373
|
+
if (stopped) break
|
|
374
|
+
if (!event || typeof event.type !== "string") continue
|
|
375
|
+
const sid = eventSessionID(event)
|
|
376
|
+
if (!sid) continue
|
|
377
|
+
const type = event.type
|
|
378
|
+
|
|
379
|
+
if (type === "session.deleted") {
|
|
380
|
+
await engine.hooks.event({ event: { type, properties: { sessionID: sid } } })
|
|
381
|
+
continue
|
|
382
|
+
}
|
|
383
|
+
if (type === "message.removed" || type === "message.part.removed") {
|
|
384
|
+
// undo cleanup: forwarded verbatim — the engine emits the
|
|
385
|
+
// tombstones that purge the reverted items from the share
|
|
386
|
+
const data = (event.data ?? {}) as { messageID?: string; partID?: string }
|
|
387
|
+
await engine.hooks.event({
|
|
388
|
+
event: { type, properties: { sessionID: sid, messageID: data.messageID, partID: data.partID } },
|
|
389
|
+
})
|
|
390
|
+
continue
|
|
391
|
+
}
|
|
392
|
+
if (SESSION_ITEM_EVENTS.has(type)) {
|
|
393
|
+
refresh(sid)
|
|
394
|
+
continue
|
|
395
|
+
}
|
|
396
|
+
if (BUSY_EVENTS.has(type)) {
|
|
397
|
+
await engine.hooks.event({ event: { type: "session.status", properties: { sessionID: sid, status: { type: "busy" } } } })
|
|
398
|
+
refresh(sid)
|
|
399
|
+
continue
|
|
400
|
+
}
|
|
401
|
+
if (IDLE_EVENTS.has(type)) {
|
|
402
|
+
await engine.hooks.event({ event: { type: "session.status", properties: { sessionID: sid, status: { type: "idle" } } } })
|
|
403
|
+
refresh(sid)
|
|
404
|
+
continue
|
|
405
|
+
}
|
|
406
|
+
if (type === "session.error" || type === "session.execution.failed") {
|
|
407
|
+
// terminal failure: the share says error at the next idle
|
|
408
|
+
// (recovered retries never set it)
|
|
409
|
+
await engine.hooks.event({ event: { type: "session.error", properties: { sessionID: sid } } })
|
|
410
|
+
continue
|
|
411
|
+
}
|
|
412
|
+
if (ERROR_EVENTS.has(type)) {
|
|
413
|
+
await engine.hooks.event({ event: { type: "session.status", properties: { sessionID: sid, status: { type: "retry" } } } })
|
|
414
|
+
continue
|
|
415
|
+
}
|
|
416
|
+
if (CONTENT_EVENTS_PREFIXES.some((prefix) => type.startsWith(prefix))) {
|
|
417
|
+
refresh(sid)
|
|
418
|
+
continue
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
} catch {
|
|
422
|
+
/* stream ended or aborted */
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
void run()
|
|
426
|
+
return () => {
|
|
427
|
+
stopped = true
|
|
428
|
+
}
|
|
429
|
+
})()
|
|
430
|
+
|
|
431
|
+
return () => {
|
|
432
|
+
unsubscribe()
|
|
433
|
+
for (const timer of pendingRefresh.values()) clearTimeout(timer)
|
|
434
|
+
pendingRefresh.clear()
|
|
435
|
+
void engine.hooks.dispose()
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|