@windmill-labs/chat-sdk 0.0.0-stage → 1.831.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.
@@ -0,0 +1,1182 @@
1
+ import { C as _assertClassBrand, S as _classPrivateFieldSet2, T as _classPrivateMethodInitSpec, _ as WindmillApiError, a as errorResultMessage, c as isErrorResult, d as randomId, f as sleep, g as TurnRunningError, i as conversationTitle, l as now, n as abortError, o as extractChatAnswer, p as truncateTitle, s as isAbortError, t as followJob, v as WindmillChatApi, w as _classPrivateFieldInitSpec, x as _classPrivateFieldGet2 } from "./follow-DXfmfPfr.js";
2
+ //#region src/config.ts
3
+ /**
4
+ * The Windmill a raw app bundle runs in. A sandboxed app gets `window.process.env`
5
+ * from its wrapper once the viewer consented to the declared SDK scopes; an
6
+ * unsandboxed one runs on the Windmill origin with the viewer's session and only
7
+ * has `window.ctx`.
8
+ */
9
+ function detectRawApp() {
10
+ const g = globalThis;
11
+ const env = g.process?.env;
12
+ if (env?.WM_RAW_APP === "true" && env.WM_TOKEN && env.BASE_URL && env.WM_WORKSPACE) return {
13
+ baseUrl: env.BASE_URL,
14
+ workspace: env.WM_WORKSPACE,
15
+ token: env.WM_TOKEN
16
+ };
17
+ const workspace = g.ctx?.workspace;
18
+ const origin = g.location?.origin;
19
+ if (typeof workspace === "string" && workspace && origin && origin !== "null") return {
20
+ baseUrl: origin,
21
+ workspace
22
+ };
23
+ }
24
+ function resolveConfig(options) {
25
+ if (!options.flowPath) throw new Error("windmill-chat: flowPath is required");
26
+ const explicitToken = options.token !== void 0;
27
+ const detected = options.baseUrl && options.workspace && explicitToken ? void 0 : detectRawApp();
28
+ const baseUrl = options.baseUrl ?? detected?.baseUrl;
29
+ const workspace = options.workspace ?? detected?.workspace;
30
+ if (!baseUrl || !workspace) throw new Error("windmill-chat: pass baseUrl and workspace. They are only detected inside a raw app: an unsandboxed one on the Windmill origin, or a sandboxed one whose policy declares frontend SDK scopes.");
31
+ const token = options.token ?? (options.baseUrl === void 0 || options.baseUrl === detected?.baseUrl ? detected?.token : void 0);
32
+ return {
33
+ flowPath: options.flowPath,
34
+ baseUrl,
35
+ workspace,
36
+ token,
37
+ history: options.history ?? (explicitToken ? "local" : "server"),
38
+ historyExplicit: options.history !== void 0,
39
+ inputs: options.inputs ?? {},
40
+ fetch: options.fetch,
41
+ storage: options.storage,
42
+ storageKey: options.storageKey,
43
+ pageSize: options.pageSize ?? 50,
44
+ pollDelayMs: options.pollDelayMs,
45
+ run: options.run,
46
+ onFinish: options.onFinish,
47
+ onError: options.onError
48
+ };
49
+ }
50
+ //#endregion
51
+ //#region src/history.ts
52
+ const MAX_CONVERSATIONS = 100;
53
+ /**
54
+ * Conversation history in the browser, for a credential shared by every visitor
55
+ * (server history would show them each other's chats). One key per flow, and per
56
+ * `storageKey` when the caller sets one; every operation re-reads the store so
57
+ * several tabs stay consistent.
58
+ */
59
+ function createLocalHistory(storage, key) {
60
+ const store = storage ?? defaultStorage();
61
+ const read = () => {
62
+ try {
63
+ const raw = store.getItem(key);
64
+ if (raw) {
65
+ const parsed = JSON.parse(raw);
66
+ if (parsed && parsed.v === 1) return parsed;
67
+ }
68
+ } catch {}
69
+ return {
70
+ v: 1,
71
+ conversations: [],
72
+ messages: {}
73
+ };
74
+ };
75
+ const write = (snapshot) => {
76
+ try {
77
+ store.setItem(key, JSON.stringify(snapshot));
78
+ } catch {}
79
+ };
80
+ return {
81
+ listConversations: () => read().conversations,
82
+ getMessages: (id) => read().messages[id] ?? [],
83
+ upsertConversation(conversation) {
84
+ const s = read();
85
+ s.conversations = [conversation, ...s.conversations.filter((c) => c.id !== conversation.id)];
86
+ for (const dropped of s.conversations.splice(MAX_CONVERSATIONS)) delete s.messages[dropped.id];
87
+ write(s);
88
+ },
89
+ renameConversation(id, title) {
90
+ const s = read();
91
+ s.conversations = s.conversations.map((c) => c.id === id ? {
92
+ ...c,
93
+ title
94
+ } : c);
95
+ write(s);
96
+ },
97
+ saveMessages(id, messages) {
98
+ const s = read();
99
+ s.messages[id] = messages.map((m) => ({
100
+ ...m,
101
+ pending: false
102
+ }));
103
+ write(s);
104
+ },
105
+ deleteConversation(id) {
106
+ const s = read();
107
+ s.conversations = s.conversations.filter((c) => c.id !== id);
108
+ delete s.messages[id];
109
+ write(s);
110
+ }
111
+ };
112
+ }
113
+ function defaultStorage() {
114
+ try {
115
+ const ls = globalThis.localStorage;
116
+ if (ls) {
117
+ const probe = "__windmill_chat_probe__";
118
+ ls.setItem(probe, "1");
119
+ ls.removeItem(probe);
120
+ return ls;
121
+ }
122
+ } catch {}
123
+ const memory = /* @__PURE__ */ new Map();
124
+ return {
125
+ getItem: (k) => memory.get(k) ?? null,
126
+ setItem: (k, v) => void memory.set(k, v),
127
+ removeItem: (k) => void memory.delete(k)
128
+ };
129
+ }
130
+ //#endregion
131
+ //#region src/attachments.ts
132
+ /**
133
+ * Where a chat's uploads live in the workspace's object storage. Under `windmill_uploads/`
134
+ * because the default Enterprise storage permissions grant every user write and read there
135
+ * and deny any other top-level prefix: a key outside it is refused for non-admins, both on
136
+ * upload and when the agent's job reads the file back.
137
+ */
138
+ const CHAT_UPLOADS_PREFIX = "windmill_uploads/chat";
139
+ /**
140
+ * The extension each type must be stored under. The worker reads an attachment's media type
141
+ * from the key's extension only (`mime_guess` in `windmill-ai/src/image_handler.rs`, falling
142
+ * back to `image/png`), never from the stored content type, so the extension must be true.
143
+ */
144
+ const EXTENSION_BY_MEDIA_TYPE = {
145
+ "image/png": "png",
146
+ "image/jpeg": "jpg",
147
+ "application/pdf": "pdf"
148
+ };
149
+ /**
150
+ * The name an attachment is stored under: the picked name with the extension its media type
151
+ * needs, e.g. a `photo.webp` re-encoded to PNG becomes `photo.png`. Other types keep their name.
152
+ */
153
+ function storedAttachmentName(filename, mediaType) {
154
+ const extension = EXTENSION_BY_MEDIA_TYPE[mediaType];
155
+ if (!extension) return filename;
156
+ return `${filename.replace(/\.[^./]+$/, "") || filename}.${extension}`;
157
+ }
158
+ /** The bytes of an attachment as a Blob carrying its media type. */
159
+ function attachmentBlob(attachment) {
160
+ const data = typeof attachment.data === "string" ? dataUrlToBlob(attachment.data, "application/octet-stream") : attachment.data;
161
+ return attachment.mediaType && attachment.mediaType !== data.type ? new Blob([data], { type: attachment.mediaType }) : data;
162
+ }
163
+ function dataUrlToBlob(dataUrl, fallbackType) {
164
+ const comma = dataUrl.indexOf(",");
165
+ if (!dataUrl.startsWith("data:") || comma === -1) throw new Error("windmill-chat: an attachment given as a string must be a data: URL");
166
+ const header = dataUrl.slice(5, comma);
167
+ const isBase64 = header.endsWith(";base64");
168
+ const mediaType = (isBase64 ? header.slice(0, -7) : header) || fallbackType;
169
+ const payload = dataUrl.slice(comma + 1);
170
+ if (!isBase64) return new Blob([decodeURIComponent(payload)], { type: mediaType });
171
+ const binary = atob(payload);
172
+ const bytes = new Uint8Array(binary.length);
173
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
174
+ return new Blob([bytes], { type: mediaType });
175
+ }
176
+ /**
177
+ * Put each attachment in the workspace's object storage and hand back what the agent reads.
178
+ * The key's turn prefix and per-file index keep two files with the same name, in this turn or
179
+ * an earlier one, from overwriting each other; the name stays the last segment.
180
+ */
181
+ async function uploadAttachments(api, attachments, turnId, signal) {
182
+ const prefix = `${CHAT_UPLOADS_PREFIX}/${turnId}`;
183
+ if (signal?.aborted) throw abortError();
184
+ const batch = new AbortController();
185
+ const abortBatch = () => batch.abort();
186
+ signal?.addEventListener("abort", abortBatch, { once: true });
187
+ try {
188
+ const results = await Promise.allSettled(attachments.map(async (attachment, index) => {
189
+ try {
190
+ const blob = attachmentBlob(attachment);
191
+ const filename = storedAttachmentName(attachment.name || `attachment-${index + 1}`, blob.type);
192
+ const { file_key } = await api.uploadFile(`${prefix}/${index}/${filename}`, blob, {
193
+ contentType: blob.type,
194
+ signal: batch.signal
195
+ });
196
+ return {
197
+ s3: file_key,
198
+ filename
199
+ };
200
+ } catch (e) {
201
+ batch.abort();
202
+ throw e;
203
+ }
204
+ }));
205
+ const reasons = results.flatMap((r) => r.status === "rejected" ? [r.reason] : []);
206
+ if (reasons.length === 0 && !signal?.aborted) return results.flatMap((r) => r.status === "fulfilled" ? [r.value] : []);
207
+ throw reasons.find((reason) => !isAbortError(reason)) ?? reasons[0] ?? abortError();
208
+ } finally {
209
+ signal?.removeEventListener("abort", abortBatch);
210
+ }
211
+ }
212
+ //#endregion
213
+ //#region src/chat.ts
214
+ const POLL_INTERVAL_MS = 1e3;
215
+ /** Local history mirrors the state; a stream of deltas is coalesced into one write. */
216
+ const PERSIST_DEBOUNCE_MS = 250;
217
+ /** Messages persist from spawned tasks that can land just after the flow completes. */
218
+ const RECONCILE_ATTEMPTS = 3;
219
+ const RECONCILE_DELAY_MS = 400;
220
+ /** Rows per read of what a turn wrote; a fuller page is read on from its last row. */
221
+ const ROWS_PAGE = 100;
222
+ function createChat(options) {
223
+ return new ChatImpl(options);
224
+ }
225
+ var _config = /* @__PURE__ */ new WeakMap();
226
+ var _api = /* @__PURE__ */ new WeakMap();
227
+ var _local = /* @__PURE__ */ new WeakMap();
228
+ var _listeners = /* @__PURE__ */ new WeakMap();
229
+ var _state = /* @__PURE__ */ new WeakMap();
230
+ var _turn = /* @__PURE__ */ new WeakMap();
231
+ var _page = /* @__PURE__ */ new WeakMap();
232
+ var _conversationKind = /* @__PURE__ */ new WeakMap();
233
+ var _persistTimer = /* @__PURE__ */ new WeakMap();
234
+ var _selecting = /* @__PURE__ */ new WeakMap();
235
+ var _epoch = /* @__PURE__ */ new WeakMap();
236
+ var _ChatImpl_brand = /* @__PURE__ */ new WeakSet();
237
+ var ChatImpl = class {
238
+ constructor(options) {
239
+ _classPrivateMethodInitSpec(this, _ChatImpl_brand);
240
+ _classPrivateFieldInitSpec(this, _config, void 0);
241
+ _classPrivateFieldInitSpec(this, _api, void 0);
242
+ _classPrivateFieldInitSpec(this, _local, void 0);
243
+ _classPrivateFieldInitSpec(this, _listeners, /* @__PURE__ */ new Set());
244
+ _classPrivateFieldInitSpec(this, _state, void 0);
245
+ _classPrivateFieldInitSpec(this, _turn, void 0);
246
+ _classPrivateFieldInitSpec(this, _page, 1);
247
+ _classPrivateFieldInitSpec(this, _conversationKind, void 0);
248
+ _classPrivateFieldInitSpec(this, _persistTimer, void 0);
249
+ _classPrivateFieldInitSpec(this, _selecting, Promise.resolve());
250
+ _classPrivateFieldInitSpec(this, _epoch, 0);
251
+ this.getState = () => _classPrivateFieldGet2(_state, this);
252
+ this.subscribe = (listener) => {
253
+ _classPrivateFieldGet2(_listeners, this).add(listener);
254
+ listener(_classPrivateFieldGet2(_state, this));
255
+ return () => {
256
+ _classPrivateFieldGet2(_listeners, this).delete(listener);
257
+ };
258
+ };
259
+ this.sendMessage = async (text, options = {}) => {
260
+ var _this$epoch;
261
+ const content = text.trim();
262
+ if (!content) {
263
+ if (options.attachments?.length) throw new Error("windmill-chat: attachments need a message to go with them");
264
+ return;
265
+ }
266
+ if (_classPrivateFieldGet2(_turn, this)) throw new Error("windmill-chat: a message is already being answered; call stop() first");
267
+ const attachments = options.attachments ?? [];
268
+ const attachmentsInput = options.attachmentsInput;
269
+ if (attachments.length > 0 && !attachmentsInput) throw new Error("windmill-chat: attachments need `attachmentsInput`, the flow input that takes them");
270
+ if (attachmentsInput && !attachmentsInput.multiple && attachments.length > 1) throw new Error(`windmill-chat: \`${attachmentsInput.name}\` holds one file; got ${attachments.length}`);
271
+ const isNew = _classPrivateFieldGet2(_state, this).conversationId === void 0;
272
+ const conversationId = _classPrivateFieldGet2(_state, this).conversationId ?? randomId();
273
+ const turn = {
274
+ controller: new AbortController(),
275
+ conversationId,
276
+ userMessageId: `pending-${randomId()}`,
277
+ isNew,
278
+ started: false,
279
+ withdrawn: false,
280
+ streamedText: false
281
+ };
282
+ _classPrivateFieldSet2(_turn, this, turn);
283
+ _classPrivateFieldSet2(_epoch, this, (_this$epoch = _classPrivateFieldGet2(_epoch, this), _this$epoch++, _this$epoch));
284
+ const timestamp = now();
285
+ const touched = {
286
+ ..._classPrivateFieldGet2(_state, this).conversations.find((c) => c.id === conversationId) ?? {
287
+ id: conversationId,
288
+ title: conversationTitle(content),
289
+ createdAt: timestamp,
290
+ updatedAt: timestamp
291
+ },
292
+ updatedAt: timestamp
293
+ };
294
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
295
+ conversationId,
296
+ messages: [..._classPrivateFieldGet2(_state, this).messages, {
297
+ id: turn.userMessageId,
298
+ role: "user",
299
+ content,
300
+ success: true,
301
+ createdAt: timestamp,
302
+ pending: true
303
+ }],
304
+ status: "submitted",
305
+ error: void 0
306
+ });
307
+ try {
308
+ const args = {
309
+ ..._classPrivateFieldGet2(_config, this).inputs,
310
+ ...options.inputs,
311
+ user_message: content
312
+ };
313
+ if (attachmentsInput && attachments.length > 0) {
314
+ const uploaded = await uploadAttachments(_classPrivateFieldGet2(_api, this), attachments, randomId(), turn.controller.signal);
315
+ args[attachmentsInput.name] = attachmentsInput.multiple ? uploaded : uploaded[0];
316
+ const carried = uploaded.map((u) => ({
317
+ input: attachmentsInput.name,
318
+ s3: u.s3,
319
+ filename: u.filename
320
+ }));
321
+ if (_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) _assertClassBrand(_ChatImpl_brand, this, _set).call(this, { messages: _classPrivateFieldGet2(_state, this).messages.map((m) => m.id === turn.userMessageId ? {
322
+ ...m,
323
+ attachments: carried
324
+ } : m) });
325
+ }
326
+ if (turn.controller.signal.aborted) throw abortError();
327
+ turn.started = true;
328
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, { conversations: [touched, ..._classPrivateFieldGet2(_state, this).conversations.filter((c) => c.id !== conversationId)] });
329
+ _assertClassBrand(_ChatImpl_brand, this, _rememberConversation).call(this);
330
+ const context = {
331
+ memoryId: conversationId,
332
+ conversationId,
333
+ signal: turn.controller.signal
334
+ };
335
+ turn.jobId = _classPrivateFieldGet2(_config, this).run ? await _classPrivateFieldGet2(_config, this).run(args, context) : await _classPrivateFieldGet2(_api, this).runFlow(_classPrivateFieldGet2(_config, this).flowPath, args, context);
336
+ } catch (e) {
337
+ try {
338
+ if (e instanceof TurnRunningError || !turn.started) {
339
+ _assertClassBrand(_ChatImpl_brand, this, _withdrawTurn).call(this, turn);
340
+ throw e;
341
+ }
342
+ if (turn.controller.signal.aborted || isAbortError(e)) return;
343
+ _assertClassBrand(_ChatImpl_brand, this, _failTurn).call(this, turn, e);
344
+ return;
345
+ } finally {
346
+ if (_classPrivateFieldGet2(_turn, this) === turn) _classPrivateFieldSet2(_turn, this, void 0);
347
+ }
348
+ }
349
+ await _assertClassBrand(_ChatImpl_brand, this, _followTurn).call(this, turn, isNew);
350
+ };
351
+ this.resumeTurn = async ({ jobId, userSeq }) => {
352
+ var _this$epoch3;
353
+ const conversationId = _classPrivateFieldGet2(_state, this).conversationId;
354
+ if (!conversationId || _classPrivateFieldGet2(_turn, this)) return;
355
+ if (_classPrivateFieldGet2(_state, this).history !== "server") {
356
+ if (_classPrivateFieldGet2(_state, this).status === "submitted") _assertClassBrand(_ChatImpl_brand, this, _set).call(this, { status: "idle" });
357
+ return;
358
+ }
359
+ const turn = {
360
+ controller: new AbortController(),
361
+ conversationId,
362
+ userMessageId: "",
363
+ jobId,
364
+ isNew: false,
365
+ started: true,
366
+ withdrawn: false,
367
+ streamedText: false
368
+ };
369
+ _classPrivateFieldSet2(_turn, this, turn);
370
+ _classPrivateFieldSet2(_epoch, this, (_this$epoch3 = _classPrivateFieldGet2(_epoch, this), _this$epoch3++, _this$epoch3));
371
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
372
+ status: "submitted",
373
+ error: void 0
374
+ });
375
+ try {
376
+ await _classPrivateFieldGet2(_selecting, this);
377
+ if (!_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
378
+ const newest = _assertClassBrand(_ChatImpl_brand, this, _latestUserAfter).call(this, userSeq);
379
+ if (newest) {
380
+ if (!newest.jobId) {
381
+ if (_classPrivateFieldGet2(_turn, this) === turn) _classPrivateFieldSet2(_turn, this, void 0);
382
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, { status: "idle" });
383
+ return;
384
+ }
385
+ turn.jobId = newest.jobId;
386
+ userSeq = newest.seq;
387
+ }
388
+ const held = _classPrivateFieldGet2(_state, this).messages;
389
+ const at = held.findIndex((m) => m.seq === userSeq);
390
+ let user = at === -1 ? void 0 : held[at];
391
+ let messages = held.filter((m, i) => {
392
+ if (m.seq !== void 0) return m.seq < userSeq;
393
+ if (m.pending || m.jobId === turn.jobId) return false;
394
+ return m.jobId !== void 0 || at !== -1 && i < at;
395
+ });
396
+ if (user) messages = [...messages, user];
397
+ if (!user) {
398
+ const [row] = await _classPrivateFieldGet2(_api, this).listMessages(conversationId, {
399
+ afterSeq: userSeq - 1,
400
+ perPage: 1,
401
+ signal: turn.controller.signal
402
+ });
403
+ if (!_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
404
+ if (row?.created_seq !== userSeq || row.message_type !== "user") throw new Error("windmill-chat: the message that started the running turn is gone");
405
+ user = fromRow(row);
406
+ messages = [...messages, user];
407
+ }
408
+ turn.userMessageId = user.id;
409
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, { messages });
410
+ } catch (e) {
411
+ if (!(turn.controller.signal.aborted || isAbortError(e))) _assertClassBrand(_ChatImpl_brand, this, _failTurn).call(this, turn, e);
412
+ if (_classPrivateFieldGet2(_turn, this) === turn) _classPrivateFieldSet2(_turn, this, void 0);
413
+ return;
414
+ }
415
+ await _assertClassBrand(_ChatImpl_brand, this, _followTurn).call(this, turn, false);
416
+ };
417
+ this.stop = async () => {
418
+ const turn = _classPrivateFieldGet2(_turn, this);
419
+ if (!turn) return;
420
+ _assertClassBrand(_ChatImpl_brand, this, _detachTurn).call(this);
421
+ if (!turn.started) {
422
+ _assertClassBrand(_ChatImpl_brand, this, _withdrawTurn).call(this, turn);
423
+ return;
424
+ }
425
+ if (_classPrivateFieldGet2(_state, this).conversationId === turn.conversationId) {
426
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
427
+ messages: finalized(_classPrivateFieldGet2(_state, this).messages),
428
+ status: "idle"
429
+ });
430
+ _assertClassBrand(_ChatImpl_brand, this, _persistLocal).call(this);
431
+ }
432
+ if (turn.jobId) await _classPrivateFieldGet2(_api, this).cancelJob(turn.jobId).catch(() => {});
433
+ if (_classPrivateFieldGet2(_state, this).history !== "server") return;
434
+ await sleep(RECONCILE_DELAY_MS).catch(() => {});
435
+ if (_classPrivateFieldGet2(_turn, this) || _classPrivateFieldGet2(_state, this).conversationId !== turn.conversationId) return;
436
+ await _assertClassBrand(_ChatImpl_brand, this, _syncFromServer).call(this, turn.conversationId).catch(() => {});
437
+ };
438
+ this.newConversation = () => {
439
+ _assertClassBrand(_ChatImpl_brand, this, _leaveConversation).call(this);
440
+ _classPrivateFieldSet2(_page, this, 1);
441
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
442
+ conversationId: void 0,
443
+ messages: [],
444
+ status: "idle",
445
+ error: void 0,
446
+ loadingMessages: false,
447
+ hasMoreMessages: false
448
+ });
449
+ };
450
+ this.selectConversation = (conversationId) => {
451
+ if (conversationId === _classPrivateFieldGet2(_state, this).conversationId) return _classPrivateFieldGet2(_selecting, this);
452
+ const selecting = _assertClassBrand(_ChatImpl_brand, this, _select).call(this, conversationId);
453
+ _classPrivateFieldSet2(_selecting, this, selecting.catch(() => {}));
454
+ return selecting;
455
+ };
456
+ this.loadConversations = async (options = {}) => {
457
+ const page = options.page ?? 1;
458
+ const kindChanged = "kind" in options && options.kind !== _classPrivateFieldGet2(_conversationKind, this);
459
+ if ("kind" in options) _classPrivateFieldSet2(_conversationKind, this, options.kind);
460
+ const kind = _classPrivateFieldGet2(_conversationKind, this);
461
+ let conversations;
462
+ if (_classPrivateFieldGet2(_state, this).history === "server") try {
463
+ conversations = (await _classPrivateFieldGet2(_api, this).listConversations(_classPrivateFieldGet2(_config, this).flowPath, {
464
+ page,
465
+ perPage: options.perPage ?? _classPrivateFieldGet2(_config, this).pageSize,
466
+ kind
467
+ })).map(fromConversation);
468
+ } catch (e) {
469
+ if (!_assertClassBrand(_ChatImpl_brand, this, _fallBackToLocal).call(this, e)) throw e;
470
+ conversations = _classPrivateFieldGet2(_local, this).listConversations();
471
+ }
472
+ else conversations = _classPrivateFieldGet2(_state, this).history === "local" ? _classPrivateFieldGet2(_local, this).listConversations() : [];
473
+ if (kind !== _classPrivateFieldGet2(_conversationKind, this)) return conversations;
474
+ const known = new Set(_classPrivateFieldGet2(_state, this).conversations.map((c) => c.id));
475
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, { conversations: page === 1 || kindChanged ? conversations : [..._classPrivateFieldGet2(_state, this).conversations, ...conversations.filter((c) => !known.has(c.id))] });
476
+ return conversations;
477
+ };
478
+ this.deleteConversation = async (conversationId) => {
479
+ if (_classPrivateFieldGet2(_state, this).conversationId === conversationId) {
480
+ _assertClassBrand(_ChatImpl_brand, this, _detachTurn).call(this);
481
+ clearTimeout(_classPrivateFieldGet2(_persistTimer, this));
482
+ _classPrivateFieldSet2(_persistTimer, this, void 0);
483
+ this.newConversation();
484
+ }
485
+ if (_classPrivateFieldGet2(_state, this).history === "server") await _classPrivateFieldGet2(_api, this).deleteConversation(conversationId);
486
+ else if (_classPrivateFieldGet2(_state, this).history === "local") _classPrivateFieldGet2(_local, this).deleteConversation(conversationId);
487
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, { conversations: _classPrivateFieldGet2(_state, this).conversations.filter((c) => c.id !== conversationId) });
488
+ };
489
+ this.renameConversation = async (conversationId, title) => {
490
+ const trimmed = truncateTitle(title.trim());
491
+ if (!trimmed) return;
492
+ if (_classPrivateFieldGet2(_state, this).history === "server") await _classPrivateFieldGet2(_api, this).renameConversation(conversationId, trimmed);
493
+ else if (_classPrivateFieldGet2(_state, this).history === "local") _classPrivateFieldGet2(_local, this).renameConversation(conversationId, trimmed);
494
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, { conversations: _classPrivateFieldGet2(_state, this).conversations.map((c) => c.id === conversationId ? {
495
+ ...c,
496
+ title: trimmed
497
+ } : c) });
498
+ };
499
+ this.loadOlderMessages = async () => {
500
+ const conversationId = _classPrivateFieldGet2(_state, this).conversationId;
501
+ if (!conversationId || _classPrivateFieldGet2(_state, this).history !== "server" || !_classPrivateFieldGet2(_state, this).hasMoreMessages || _classPrivateFieldGet2(_state, this).loadingMessages) return;
502
+ const page = _classPrivateFieldGet2(_page, this) + 1;
503
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, { loadingMessages: true });
504
+ try {
505
+ const rows = await _classPrivateFieldGet2(_api, this).listMessages(conversationId, {
506
+ page,
507
+ perPage: _classPrivateFieldGet2(_config, this).pageSize
508
+ });
509
+ if (_classPrivateFieldGet2(_state, this).conversationId !== conversationId) return;
510
+ const known = new Set(_classPrivateFieldGet2(_state, this).messages.map((m) => m.serverId ?? m.id));
511
+ const oldest = _classPrivateFieldGet2(_state, this).messages.reduce((min, m) => m.seq !== void 0 && (min === void 0 || m.seq < min) ? m.seq : min, void 0);
512
+ const older = rows.map(fromRow).filter((m) => !known.has(m.id) && (oldest === void 0 || m.seq < oldest));
513
+ _classPrivateFieldSet2(_page, this, page);
514
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
515
+ messages: [...older, ..._classPrivateFieldGet2(_state, this).messages],
516
+ hasMoreMessages: rows.length === _classPrivateFieldGet2(_config, this).pageSize
517
+ });
518
+ } finally {
519
+ if (_classPrivateFieldGet2(_state, this).conversationId === conversationId) _assertClassBrand(_ChatImpl_brand, this, _set).call(this, { loadingMessages: false });
520
+ }
521
+ };
522
+ this.refreshMessages = async () => {
523
+ const conversationId = _classPrivateFieldGet2(_state, this).conversationId;
524
+ if (!conversationId || _classPrivateFieldGet2(_turn, this) || _classPrivateFieldGet2(_state, this).history !== "server" || _classPrivateFieldGet2(_state, this).loadingMessages) return;
525
+ const newestBefore = latestSeq(_classPrivateFieldGet2(_state, this).messages);
526
+ const failure = lastFailureShown(_classPrivateFieldGet2(_state, this).messages);
527
+ const epoch = _classPrivateFieldGet2(_epoch, this);
528
+ await _assertClassBrand(_ChatImpl_brand, this, _syncFromServer).call(this, conversationId, () => _classPrivateFieldGet2(_epoch, this) === epoch);
529
+ if (_classPrivateFieldGet2(_epoch, this) !== epoch) return;
530
+ if (!failure || _classPrivateFieldGet2(_state, this).conversationId !== conversationId) return;
531
+ if (!_classPrivateFieldGet2(_state, this).messages.some((m) => m.id === failure.id)) return;
532
+ const jobs = failure.jobId ? await _assertClassBrand(_ChatImpl_brand, this, _turnJobIds).call(this, failure.jobId) : void 0;
533
+ if (_classPrivateFieldGet2(_state, this).conversationId !== conversationId) return;
534
+ const held = _classPrivateFieldGet2(_state, this).messages;
535
+ if (!held.some((m) => m.id === failure.id)) return;
536
+ if (!held.some((m) => m.seq !== void 0 && m.seq > newestBefore && m.role === "assistant" && m.success && (jobs === void 0 || m.jobId === void 0 || jobs.has(m.jobId)))) return;
537
+ const settles = _classPrivateFieldGet2(_state, this).status === "error" && lastFailureShown(held)?.id === failure.id;
538
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
539
+ messages: held.filter((m) => m.id !== failure.id),
540
+ ...settles ? {
541
+ status: "idle",
542
+ error: void 0
543
+ } : {}
544
+ });
545
+ };
546
+ this.destroy = () => {
547
+ _assertClassBrand(_ChatImpl_brand, this, _leaveConversation).call(this);
548
+ };
549
+ _classPrivateFieldSet2(_config, this, resolveConfig(options));
550
+ _classPrivateFieldSet2(_api, this, new WindmillChatApi({
551
+ baseUrl: _classPrivateFieldGet2(_config, this).baseUrl,
552
+ workspace: _classPrivateFieldGet2(_config, this).workspace,
553
+ token: _classPrivateFieldGet2(_config, this).token,
554
+ fetch: _classPrivateFieldGet2(_config, this).fetch,
555
+ pollDelayMs: _classPrivateFieldGet2(_config, this).pollDelayMs
556
+ }));
557
+ _classPrivateFieldSet2(_local, this, createLocalHistory(_classPrivateFieldGet2(_config, this).storage, `windmill-chat:${_classPrivateFieldGet2(_config, this).baseUrl}:${_classPrivateFieldGet2(_config, this).workspace}:${_classPrivateFieldGet2(_config, this).flowPath}` + (_classPrivateFieldGet2(_config, this).storageKey ? `:${_classPrivateFieldGet2(_config, this).storageKey}` : "")));
558
+ _classPrivateFieldSet2(_state, this, {
559
+ conversationId: void 0,
560
+ messages: [],
561
+ status: "idle",
562
+ error: void 0,
563
+ conversations: [],
564
+ history: _classPrivateFieldGet2(_config, this).history,
565
+ loadingMessages: false,
566
+ hasMoreMessages: false
567
+ });
568
+ }
569
+ };
570
+ async function _followTurn(turn, isNew) {
571
+ let nextTurn;
572
+ try {
573
+ const stopPolling = _classPrivateFieldGet2(_state, this).history === "server" ? _assertClassBrand(_ChatImpl_brand, this, _startPolling).call(this, turn) : () => {};
574
+ let result;
575
+ try {
576
+ result = await _assertClassBrand(_ChatImpl_brand, this, _follow).call(this, turn, stopPolling);
577
+ } finally {
578
+ stopPolling();
579
+ }
580
+ nextTurn = await _assertClassBrand(_ChatImpl_brand, this, _finishTurn).call(this, turn, result, isNew);
581
+ } catch (e) {
582
+ if (!turn.started) {
583
+ if (_classPrivateFieldGet2(_turn, this) === turn) _classPrivateFieldSet2(_turn, this, void 0);
584
+ _assertClassBrand(_ChatImpl_brand, this, _withdrawTurn).call(this, turn);
585
+ throw e;
586
+ }
587
+ if (turn.controller.signal.aborted || isAbortError(e)) return;
588
+ _assertClassBrand(_ChatImpl_brand, this, _failTurn).call(this, turn, e);
589
+ } finally {
590
+ if (_classPrivateFieldGet2(_turn, this) === turn) _classPrivateFieldSet2(_turn, this, void 0);
591
+ }
592
+ if (nextTurn) await this.resumeTurn(nextTurn);
593
+ }
594
+ async function _select(conversationId) {
595
+ _assertClassBrand(_ChatImpl_brand, this, _leaveConversation).call(this);
596
+ _classPrivateFieldSet2(_page, this, 1);
597
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
598
+ conversationId,
599
+ messages: [],
600
+ status: "idle",
601
+ error: void 0,
602
+ loadingMessages: true,
603
+ hasMoreMessages: false
604
+ });
605
+ if (_classPrivateFieldGet2(_state, this).history !== "server") {
606
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
607
+ messages: _classPrivateFieldGet2(_state, this).history === "local" ? _classPrivateFieldGet2(_local, this).getMessages(conversationId) : [],
608
+ loadingMessages: false
609
+ });
610
+ return;
611
+ }
612
+ try {
613
+ const rows = await _classPrivateFieldGet2(_api, this).listMessages(conversationId, { perPage: _classPrivateFieldGet2(_config, this).pageSize });
614
+ if (_classPrivateFieldGet2(_state, this).conversationId !== conversationId) return;
615
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
616
+ messages: rows.map(fromRow),
617
+ loadingMessages: false,
618
+ hasMoreMessages: rows.length === _classPrivateFieldGet2(_config, this).pageSize
619
+ });
620
+ } catch (e) {
621
+ if (_classPrivateFieldGet2(_state, this).conversationId !== conversationId) return;
622
+ if (_assertClassBrand(_ChatImpl_brand, this, _fallBackToLocal).call(this, e)) {
623
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
624
+ messages: _classPrivateFieldGet2(_local, this).getMessages(conversationId),
625
+ loadingMessages: false
626
+ });
627
+ return;
628
+ }
629
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
630
+ loadingMessages: false,
631
+ status: "error",
632
+ error: toError(e)
633
+ });
634
+ }
635
+ }
636
+ async function _follow(turn, onStreamStart) {
637
+ let started = false;
638
+ for await (const event of followJob(_classPrivateFieldGet2(_api, this), turn.jobId, { signal: turn.controller.signal })) {
639
+ if (event.type === "completed") {
640
+ if (event.streamLost) _assertClassBrand(_ChatImpl_brand, this, _dropCutRound).call(this, turn);
641
+ return event.result;
642
+ }
643
+ if (!started) {
644
+ started = true;
645
+ onStreamStart();
646
+ }
647
+ _assertClassBrand(_ChatImpl_brand, this, _applyEvents).call(this, turn, event.events);
648
+ }
649
+ throw new Error("windmill-chat: the job stream ended before the flow completed");
650
+ }
651
+ /**
652
+ * The stream failed while a round's text was arriving, so that text stops wherever the
653
+ * connection did. It goes: the persisted rows or, without them, the flow result give
654
+ * the whole answer instead. Rounds a tool call closed were complete and stay.
655
+ */
656
+ function _dropCutRound(turn) {
657
+ if (!_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
658
+ const cut = turn.assistantId;
659
+ turn.assistantId = void 0;
660
+ turn.streamedText = false;
661
+ if (cut) _assertClassBrand(_ChatImpl_brand, this, _set).call(this, { messages: _classPrivateFieldGet2(_state, this).messages.filter((m) => m.id !== cut) });
662
+ }
663
+ function _applyEvents(turn, events) {
664
+ if (events.length === 0 || !_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
665
+ let messages = [..._classPrivateFieldGet2(_state, this).messages];
666
+ const upsertTool = (callId, name, patch) => {
667
+ const { content, success, ...toolPatch } = patch;
668
+ const i = messages.findIndex((m) => m.pending && m.role === "tool" && m.tool?.callId === callId);
669
+ if (i >= 0) {
670
+ const existing = messages[i];
671
+ messages[i] = {
672
+ ...existing,
673
+ content: content ?? existing.content,
674
+ success: success ?? existing.success,
675
+ tool: {
676
+ ...existing.tool,
677
+ ...toolPatch
678
+ }
679
+ };
680
+ } else {
681
+ const a = turn.assistantId ? messages.findIndex((m) => m.id === turn.assistantId) : -1;
682
+ const reasoning = a >= 0 && messages[a].content === "" ? messages.splice(a, 1)[0].reasoning : void 0;
683
+ messages.push({
684
+ id: `pending-${randomId()}`,
685
+ role: "tool",
686
+ content: content ?? "",
687
+ reasoning,
688
+ success: success ?? true,
689
+ createdAt: now(),
690
+ pending: true,
691
+ tool: {
692
+ callId,
693
+ name,
694
+ status: "running",
695
+ ...toolPatch
696
+ }
697
+ });
698
+ }
699
+ turn.assistantId = void 0;
700
+ };
701
+ const appendAssistant = (text, reasoning) => {
702
+ const i = turn.assistantId ? messages.findIndex((m) => m.id === turn.assistantId) : -1;
703
+ if (i >= 0) {
704
+ const m = messages[i];
705
+ messages[i] = {
706
+ ...m,
707
+ content: m.content + text,
708
+ reasoning: reasoning ? (m.reasoning ?? "") + reasoning : m.reasoning
709
+ };
710
+ } else {
711
+ turn.assistantId = `pending-${randomId()}`;
712
+ messages.push({
713
+ id: turn.assistantId,
714
+ role: "assistant",
715
+ content: text,
716
+ reasoning: reasoning || void 0,
717
+ success: true,
718
+ createdAt: now(),
719
+ pending: true
720
+ });
721
+ }
722
+ };
723
+ for (const event of events) switch (event.type) {
724
+ case "token_delta":
725
+ turn.streamedText = true;
726
+ appendAssistant(event.content, "");
727
+ break;
728
+ case "reasoning_token_delta":
729
+ appendAssistant("", event.content);
730
+ break;
731
+ case "tool_call":
732
+ upsertTool(event.call_id, event.function_name, { status: "running" });
733
+ break;
734
+ case "tool_call_arguments":
735
+ upsertTool(event.call_id, event.function_name, { arguments: event.arguments });
736
+ break;
737
+ case "tool_execution":
738
+ upsertTool(event.call_id, event.function_name, { status: "running" });
739
+ break;
740
+ case "tool_result": upsertTool(event.call_id, event.function_name, {
741
+ status: event.success ? "success" : "error",
742
+ result: event.result,
743
+ success: event.success,
744
+ content: event.success ? `Used ${event.function_name} tool` : `Error executing ${event.function_name}`
745
+ });
746
+ }
747
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
748
+ messages,
749
+ status: "streaming"
750
+ });
751
+ }
752
+ async function _finishTurn(turn, result, isNew) {
753
+ if (!_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
754
+ if (_classPrivateFieldGet2(_state, this).history === "server") {
755
+ turn.jobIds = await _assertClassBrand(_ChatImpl_brand, this, _turnJobIds).call(this, turn.jobId, turn.controller.signal);
756
+ if (!_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
757
+ const reconciled = await _assertClassBrand(_ChatImpl_brand, this, _reconcileTurn).call(this, turn);
758
+ if (!_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
759
+ if (reconciled) {
760
+ const nextTurn = _assertClassBrand(_ChatImpl_brand, this, _nextTurnAfter).call(this, turn);
761
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, { status: nextTurn ? "submitted" : "idle" });
762
+ _classPrivateFieldGet2(_config, this).onFinish?.({
763
+ conversationId: turn.conversationId,
764
+ jobId: turn.jobId,
765
+ messages: _classPrivateFieldGet2(_state, this).messages
766
+ });
767
+ if (isNew) await this.loadConversations().catch(() => {});
768
+ if (!_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
769
+ return nextTurn;
770
+ }
771
+ }
772
+ let messages = _classPrivateFieldGet2(_state, this).messages;
773
+ let failed = false;
774
+ if (isErrorResult(result)) {
775
+ failed = await _classPrivateFieldGet2(_api, this).getCompletedResult(turn.jobId, turn.controller.signal).then((r) => r.success === false).catch(() => true);
776
+ if (!_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
777
+ if (failed) messages = [...messages, assistantMessage(errorResultMessage(result), false, turn.jobId)];
778
+ }
779
+ if (!failed && !turn.streamedText) {
780
+ const answer = extractChatAnswer(result);
781
+ if (answer !== void 0) messages = [...messages, assistantMessage(answer, true, turn.jobId)];
782
+ }
783
+ const nextTurn = _classPrivateFieldGet2(_state, this).history === "server" ? _assertClassBrand(_ChatImpl_brand, this, _nextTurnAfter).call(this, turn) : void 0;
784
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
785
+ messages: finalized(messages),
786
+ status: nextTurn ? "submitted" : "idle"
787
+ });
788
+ _assertClassBrand(_ChatImpl_brand, this, _persistLocal).call(this);
789
+ _classPrivateFieldGet2(_config, this).onFinish?.({
790
+ conversationId: turn.conversationId,
791
+ jobId: turn.jobId,
792
+ messages: _classPrivateFieldGet2(_state, this).messages
793
+ });
794
+ if (!_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
795
+ return nextTurn;
796
+ }
797
+ /**
798
+ * Folds what the server persisted for the turn into the message list. The rows
799
+ * are written by the worker in their own transactions, each of which can trail
800
+ * the flow's completion, so a streamed message whose row hasn't landed stays and
801
+ * the list is re-read a few times before the rest is kept as streamed.
802
+ * Returns false when the server holds no answer for the turn: history fell back to
803
+ * local, the read was refused or kept failing, or no assistant row has landed. The
804
+ * caller then finishes the turn from the flow result, so an answer is never lost
805
+ * to history. Whether a row counts is read from the message list, not from what
806
+ * this read returned: the turn's polling may have merged the answer already.
807
+ */
808
+ async function _reconcileTurn(turn) {
809
+ const answered = () => _assertClassBrand(_ChatImpl_brand, this, _answered).call(this, turn);
810
+ for (let attempt = 1; attempt <= RECONCILE_ATTEMPTS; attempt++) {
811
+ let rows;
812
+ try {
813
+ rows = await _assertClassBrand(_ChatImpl_brand, this, _rowsAfterLastSeq).call(this, turn.conversationId, turn.controller.signal);
814
+ } catch (e) {
815
+ if (isAbortError(e)) throw e;
816
+ if (_assertClassBrand(_ChatImpl_brand, this, _fallBackToLocal).call(this, e)) return false;
817
+ if (e instanceof WindmillApiError && (e.status === 401 || e.status === 403) || attempt === RECONCILE_ATTEMPTS) return answered();
818
+ await sleep(RECONCILE_DELAY_MS, turn.controller.signal);
819
+ continue;
820
+ }
821
+ if (!_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return true;
822
+ _assertClassBrand(_ChatImpl_brand, this, _mergeRows).call(this, rows);
823
+ if (answered() && !_classPrivateFieldGet2(_state, this).messages.some((m) => m.pending && m.content)) break;
824
+ if (attempt < RECONCILE_ATTEMPTS) await sleep(RECONCILE_DELAY_MS, turn.controller.signal);
825
+ }
826
+ if (!_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return true;
827
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, { messages: finalized(_classPrivateFieldGet2(_state, this).messages) });
828
+ return answered();
829
+ }
830
+ /**
831
+ * The latest row the turn persisted after its user message is an assistant
832
+ * message. An agent issues each round's text row before that round's tool rows,
833
+ * and a tool row when the tool finishes, so an earlier round's text is followed
834
+ * by a tool row and only the answer closes the turn (the inserts are spawned, so
835
+ * a badly delayed one can invert that order at the cost of the reconcile
836
+ * retries). The content is not compared with the flow result: an image answer, a
837
+ * structured one and a forwarded agent result are all persisted in a shape the
838
+ * result does not reproduce. A row belongs to the turn when it was created after
839
+ * the user message and, when it carries a job id, the job is one of the turn's. The
840
+ * server refuses a turn while the previous run is queued, but an agent writes its
841
+ * answer row from a task the run does not wait for, so that row can still land after
842
+ * the next user message; its job says whose it is. A tool row without a job (an MCP
843
+ * call runs inside the agent step) belongs to the turn under way. Until the user
844
+ * message's own row has been read, its position in the list stands in for its seq.
845
+ */
846
+ function _answered(turn) {
847
+ const messages = _classPrivateFieldGet2(_state, this).messages;
848
+ const from = messages.findIndex((m) => m.id === turn.userMessageId);
849
+ const userSeq = messages[from]?.seq;
850
+ const ownJob = (m) => turn.jobIds === void 0 || (m.jobId === void 0 ? m.role === "tool" : turn.jobIds.has(m.jobId));
851
+ let latest;
852
+ messages.forEach((m, i) => {
853
+ if (m.seq === void 0 || m.role === "user" || !ownJob(m)) return;
854
+ if (userSeq !== void 0 ? m.seq <= userSeq : i <= from) return;
855
+ if (latest === void 0 || m.seq > latest.seq) latest = m;
856
+ });
857
+ return latest?.role === "assistant";
858
+ }
859
+ /**
860
+ * The flow job plus every step job it ran, the failure and preprocessor steps
861
+ * included (a failure handler's answer is persisted under its own job), and the
862
+ * jobs an agent step's tool calls ran as (a tool row is persisted under its own
863
+ * job too). A failed read is retried like the rows are; unknown when it keeps
864
+ * failing or the credential may not read jobs. Unknown accepts every row after the
865
+ * question: refusing them would leave a token without job access with no turn ever
866
+ * answered, each one finished a second time from its result.
867
+ */
868
+ async function _turnJobIds(jobId, signal) {
869
+ for (let attempt = 1;; attempt++) try {
870
+ const job = await _classPrivateFieldGet2(_api, this).getFlowJob(jobId, signal);
871
+ const ids = /* @__PURE__ */ new Set([jobId]);
872
+ const status = job.flow_status;
873
+ for (const m of [
874
+ ...status?.modules ?? [],
875
+ status?.failure_module,
876
+ status?.preprocessor_module
877
+ ]) {
878
+ if (m?.job) ids.add(m.job);
879
+ for (const j of m?.flow_jobs ?? []) ids.add(j);
880
+ for (const a of m?.agent_actions ?? []) if (a.job_id) ids.add(a.job_id);
881
+ }
882
+ return ids;
883
+ } catch (e) {
884
+ if (isAbortError(e)) throw e;
885
+ if (e instanceof WindmillApiError && e.status >= 400 && e.status < 500 || attempt === RECONCILE_ATTEMPTS) return void 0;
886
+ await sleep(RECONCILE_DELAY_MS, signal);
887
+ }
888
+ }
889
+ /**
890
+ * Take back the user message of a turn that never ran. A conversation it would have opened
891
+ * was never listed (see `sendMessage`), so only the message goes, and, while it is the turn
892
+ * on screen, the busy status. A switch away mid-upload has already written the message to
893
+ * local history, so it is removed there too.
894
+ */
895
+ function _withdrawTurn(turn) {
896
+ if (turn.withdrawn) return;
897
+ turn.withdrawn = true;
898
+ const id = turn.conversationId;
899
+ const withoutTurn = (messages) => messages.filter((m) => m.id !== turn.userMessageId);
900
+ if (_classPrivateFieldGet2(_state, this).conversationId === id) {
901
+ const messages = withoutTurn(_classPrivateFieldGet2(_state, this).messages);
902
+ if (_classPrivateFieldGet2(_turn, this) !== void 0 && _classPrivateFieldGet2(_turn, this) !== turn) _assertClassBrand(_ChatImpl_brand, this, _set).call(this, { messages });
903
+ else {
904
+ const unopened = turn.isNew && messages.length === 0;
905
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
906
+ messages,
907
+ status: "idle",
908
+ error: void 0,
909
+ ...unopened ? { conversationId: void 0 } : {}
910
+ });
911
+ }
912
+ _assertClassBrand(_ChatImpl_brand, this, _persistLocal).call(this);
913
+ }
914
+ if (_classPrivateFieldGet2(_state, this).history === "local" && _classPrivateFieldGet2(_state, this).conversationId !== id) {
915
+ const stored = withoutTurn(_classPrivateFieldGet2(_local, this).getMessages(id));
916
+ if (stored.length > 0) _classPrivateFieldGet2(_local, this).saveMessages(id, stored);
917
+ else if (!_classPrivateFieldGet2(_state, this).conversations.some((c) => c.id === id)) _classPrivateFieldGet2(_local, this).deleteConversation(id);
918
+ }
919
+ }
920
+ function _failTurn(turn, e) {
921
+ if (!_assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
922
+ const error = toError(e);
923
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
924
+ messages: [...finalized(_classPrivateFieldGet2(_state, this).messages), assistantMessage(error.message, false, turn.jobId)],
925
+ status: "error",
926
+ error
927
+ });
928
+ _assertClassBrand(_ChatImpl_brand, this, _persistLocal).call(this);
929
+ _classPrivateFieldGet2(_config, this).onError?.(error, {
930
+ conversationId: turn.conversationId,
931
+ jobId: turn.jobId
932
+ });
933
+ }
934
+ /** Before the answer streams, earlier steps may already have persisted messages. */
935
+ function _startPolling(turn) {
936
+ let stopped = false;
937
+ const { signal } = turn.controller;
938
+ const loop = async () => {
939
+ while (!stopped) {
940
+ try {
941
+ await sleep(POLL_INTERVAL_MS, signal);
942
+ } catch {
943
+ return;
944
+ }
945
+ if (stopped) return;
946
+ try {
947
+ const rows = await _assertClassBrand(_ChatImpl_brand, this, _rowsAfterLastSeq).call(this, turn.conversationId, signal);
948
+ if (!stopped && _assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) _assertClassBrand(_ChatImpl_brand, this, _mergeRows).call(this, rows);
949
+ } catch {}
950
+ }
951
+ };
952
+ loop();
953
+ return () => {
954
+ stopped = true;
955
+ };
956
+ }
957
+ /** Every row created after the newest one held, however many pages that takes. */
958
+ async function _rowsAfterLastSeq(conversationId, signal) {
959
+ const rows = [];
960
+ let afterSeq = _assertClassBrand(_ChatImpl_brand, this, _lastSeq).call(this);
961
+ while (true) {
962
+ const page = await _classPrivateFieldGet2(_api, this).listMessages(conversationId, {
963
+ afterSeq,
964
+ perPage: ROWS_PAGE,
965
+ signal
966
+ });
967
+ rows.push(...page);
968
+ if (page.length < ROWS_PAGE) return rows;
969
+ afterSeq = page[page.length - 1].created_seq;
970
+ }
971
+ }
972
+ async function _syncFromServer(conversationId, stillCurrent) {
973
+ const rows = await _assertClassBrand(_ChatImpl_brand, this, _rowsAfterLastSeq).call(this, conversationId);
974
+ if (_classPrivateFieldGet2(_turn, this) || _classPrivateFieldGet2(_state, this).conversationId !== conversationId) return;
975
+ if (stillCurrent && !stillCurrent()) return;
976
+ _assertClassBrand(_ChatImpl_brand, this, _mergeRows).call(this, rows);
977
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, { messages: finalized(_classPrivateFieldGet2(_state, this).messages) });
978
+ }
979
+ /**
980
+ * Folds persisted rows into the message list. A row standing for a message the
981
+ * client already shows (same role and text; for a tool, the same tool name, since
982
+ * the server words a failure differently) takes its place under the client's id
983
+ * and keeps what only the stream knew: reasoning, call id, arguments, result.
984
+ * Other rows append in server order. Nothing is dropped: a streamed message
985
+ * outlives a row that never lands.
986
+ */
987
+ function _mergeRows(rows) {
988
+ if (rows.length === 0) return;
989
+ const messages = [..._classPrivateFieldGet2(_state, this).messages];
990
+ const known = new Set(messages.map((m) => m.serverId ?? m.id));
991
+ for (const row of rows.map(fromRow)) {
992
+ if (known.has(row.id)) continue;
993
+ known.add(row.id);
994
+ let i = messages.findIndex((m) => m.seq === void 0 && m.role === row.role && (m.content === row.content || row.tool !== void 0 && m.tool?.name === row.tool.name));
995
+ if (i < 0 && row.role === "assistant") {
996
+ let j = messages.length - 1;
997
+ while (j >= 0 && messages[j].role !== "user") {
998
+ const m = messages[j];
999
+ if (m.seq === void 0 && m.role === "tool" && m.tool?.arguments === row.content) i = j;
1000
+ j--;
1001
+ }
1002
+ }
1003
+ if (i >= 0) {
1004
+ const m = messages[i];
1005
+ messages[i] = {
1006
+ ...row,
1007
+ id: m.id,
1008
+ reasoning: m.reasoning ?? row.reasoning,
1009
+ tool: row.role === "tool" && m.tool ? {
1010
+ ...m.tool,
1011
+ arguments: m.tool.arguments ?? row.tool?.arguments,
1012
+ result: m.tool.result ?? row.tool?.result,
1013
+ status: row.tool?.status ?? m.tool.status
1014
+ } : row.tool
1015
+ };
1016
+ } else {
1017
+ let at = messages.length;
1018
+ for (let j = messages.length - 1; row.role === "tool" && j >= 0; j--) {
1019
+ const seq = messages[j].seq;
1020
+ if (seq !== void 0 && seq < row.seq) {
1021
+ at = j + 1;
1022
+ break;
1023
+ }
1024
+ }
1025
+ messages.splice(at, 0, row);
1026
+ }
1027
+ }
1028
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, { messages });
1029
+ }
1030
+ function _lastSeq() {
1031
+ let last;
1032
+ for (const m of _classPrivateFieldGet2(_state, this).messages) if (m.seq !== void 0 && (last === void 0 || m.seq > last)) last = m.seq;
1033
+ return last;
1034
+ }
1035
+ /** Writes the current messages to local history; the conversation entry itself is `#rememberConversation`'s. */
1036
+ function _persistLocal() {
1037
+ clearTimeout(_classPrivateFieldGet2(_persistTimer, this));
1038
+ _classPrivateFieldSet2(_persistTimer, this, void 0);
1039
+ const id = _classPrivateFieldGet2(_state, this).conversationId;
1040
+ if (_classPrivateFieldGet2(_state, this).history !== "local" || !id) return;
1041
+ _classPrivateFieldGet2(_local, this).saveMessages(id, _classPrivateFieldGet2(_state, this).messages);
1042
+ }
1043
+ /**
1044
+ * Puts the current conversation at the head of local history. Only a turn moves
1045
+ * a conversation there: merely viewing one must not reorder the list.
1046
+ */
1047
+ function _rememberConversation() {
1048
+ const id = _classPrivateFieldGet2(_state, this).conversationId;
1049
+ if (_classPrivateFieldGet2(_state, this).history !== "local" || !id) return;
1050
+ const conversation = _classPrivateFieldGet2(_state, this).conversations.find((c) => c.id === id);
1051
+ if (conversation) _classPrivateFieldGet2(_local, this).upsertConversation(conversation);
1052
+ }
1053
+ /** Whether an unreadable server history should silently become local history. */
1054
+ function _fallBackToLocal(e) {
1055
+ if (_classPrivateFieldGet2(_state, this).history !== "server" || _classPrivateFieldGet2(_config, this).historyExplicit) return false;
1056
+ if (e instanceof WindmillApiError && (e.status === 401 || e.status === 403)) {
1057
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, { history: "local" });
1058
+ _assertClassBrand(_ChatImpl_brand, this, _rememberConversation).call(this);
1059
+ return true;
1060
+ }
1061
+ return false;
1062
+ }
1063
+ function _turnActive(turn) {
1064
+ return _classPrivateFieldGet2(_turn, this) === turn && _classPrivateFieldGet2(_state, this).conversationId === turn.conversationId;
1065
+ }
1066
+ function _latestUserAfter(userSeq) {
1067
+ return _classPrivateFieldGet2(_state, this).messages.reduce((found, message) => message.role === "user" && message.seq !== void 0 && message.seq > (found?.seq ?? userSeq) ? message : found, void 0);
1068
+ }
1069
+ function _nextTurnAfter(turn) {
1070
+ const userSeq = _classPrivateFieldGet2(_state, this).messages.find((message) => message.id === turn.userMessageId)?.seq;
1071
+ if (userSeq === void 0) return void 0;
1072
+ const next = _assertClassBrand(_ChatImpl_brand, this, _latestUserAfter).call(this, userSeq);
1073
+ return next?.jobId && next.seq !== void 0 ? {
1074
+ jobId: next.jobId,
1075
+ userSeq: next.seq
1076
+ } : void 0;
1077
+ }
1078
+ /** Stops following the current answer; the flow itself keeps running. */
1079
+ function _detachTurn() {
1080
+ const turn = _classPrivateFieldGet2(_turn, this);
1081
+ if (!turn) return;
1082
+ _classPrivateFieldSet2(_turn, this, void 0);
1083
+ turn.controller.abort();
1084
+ }
1085
+ /**
1086
+ * Leaves the current conversation (for another one, or because the page goes
1087
+ * away). A turn still in flight is detached and what it showed so far is kept,
1088
+ * written out now rather than on the debounce that may never fire.
1089
+ */
1090
+ function _leaveConversation() {
1091
+ var _this$epoch5;
1092
+ _classPrivateFieldSet2(_epoch, this, (_this$epoch5 = _classPrivateFieldGet2(_epoch, this), _this$epoch5++, _this$epoch5));
1093
+ if (_classPrivateFieldGet2(_turn, this)) {
1094
+ _assertClassBrand(_ChatImpl_brand, this, _detachTurn).call(this);
1095
+ _assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
1096
+ messages: finalized(_classPrivateFieldGet2(_state, this).messages),
1097
+ status: "idle"
1098
+ });
1099
+ }
1100
+ if (_classPrivateFieldGet2(_persistTimer, this)) _assertClassBrand(_ChatImpl_brand, this, _persistLocal).call(this);
1101
+ }
1102
+ function _set(patch) {
1103
+ _classPrivateFieldSet2(_state, this, {
1104
+ ..._classPrivateFieldGet2(_state, this),
1105
+ ...patch
1106
+ });
1107
+ for (const listener of _classPrivateFieldGet2(_listeners, this)) listener(_classPrivateFieldGet2(_state, this));
1108
+ if (_classPrivateFieldGet2(_state, this).history === "local" && _classPrivateFieldGet2(_state, this).conversationId) {
1109
+ clearTimeout(_classPrivateFieldGet2(_persistTimer, this));
1110
+ _classPrivateFieldSet2(_persistTimer, this, setTimeout(() => _assertClassBrand(_ChatImpl_brand, this, _persistLocal).call(this), PERSIST_DEBOUNCE_MS));
1111
+ }
1112
+ }
1113
+ /** The failure this chat is showing: the message `#failTurn` left, which is never a row. */
1114
+ function lastFailureShown(messages) {
1115
+ for (let i = messages.length - 1; i >= 0; i--) {
1116
+ const m = messages[i];
1117
+ if (m.seq === void 0 && m.success === false) return m;
1118
+ }
1119
+ }
1120
+ /** The newest row seq a list holds; 0 when it holds none. */
1121
+ function latestSeq(messages) {
1122
+ return messages.reduce((newest, m) => m.seq !== void 0 && m.seq > newest ? m.seq : newest, 0);
1123
+ }
1124
+ function fromRow(row) {
1125
+ const toolName = row.message_type === "tool" ? /^Used (.+) tool$/.exec(row.content)?.[1] ?? /^Error executing (.+)$/.exec(row.content)?.[1] : void 0;
1126
+ const success = row.success ?? true;
1127
+ return {
1128
+ id: row.id,
1129
+ serverId: row.id,
1130
+ role: row.message_type,
1131
+ content: row.content,
1132
+ success,
1133
+ createdAt: row.created_at,
1134
+ jobId: row.job_id ?? void 0,
1135
+ stepName: row.step_name ?? void 0,
1136
+ pending: false,
1137
+ seq: row.created_seq,
1138
+ reasoning: row.reasoning ?? void 0,
1139
+ attachments: row.attachments ?? void 0,
1140
+ tool: toolName ? {
1141
+ name: toolName,
1142
+ status: success ? "success" : "error",
1143
+ arguments: row.tool_arguments ?? void 0,
1144
+ result: row.tool_result ?? void 0
1145
+ } : void 0
1146
+ };
1147
+ }
1148
+ function fromConversation(row) {
1149
+ return {
1150
+ id: row.id,
1151
+ title: row.title ?? void 0,
1152
+ createdAt: row.created_at,
1153
+ updatedAt: row.updated_at,
1154
+ isTest: row.is_test,
1155
+ runningTurn: row.running_turn ? {
1156
+ jobId: row.running_turn.job_id,
1157
+ userSeq: row.running_turn.user_seq
1158
+ } : void 0
1159
+ };
1160
+ }
1161
+ function assistantMessage(content, success, jobId) {
1162
+ return {
1163
+ id: `local-${randomId()}`,
1164
+ role: "assistant",
1165
+ content,
1166
+ success,
1167
+ createdAt: now(),
1168
+ jobId,
1169
+ pending: false
1170
+ };
1171
+ }
1172
+ function finalized(messages) {
1173
+ return messages.some((m) => m.pending) ? messages.map((m) => m.pending ? {
1174
+ ...m,
1175
+ pending: false
1176
+ } : m) : messages;
1177
+ }
1178
+ function toError(e) {
1179
+ return e instanceof Error ? e : new Error(String(e));
1180
+ }
1181
+ //#endregion
1182
+ export { detectRawApp as a, uploadAttachments as i, CHAT_UPLOADS_PREFIX as n, storedAttachmentName as r, createChat as t };