@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,1211 @@
1
+ const require_follow = require("./follow-Dug-Afeu.cjs");
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 require_follow.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) => !require_follow.isAbortError(reason)) ?? reasons[0] ?? require_follow.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
+ require_follow._classPrivateMethodInitSpec(this, _ChatImpl_brand);
240
+ require_follow._classPrivateFieldInitSpec(this, _config, void 0);
241
+ require_follow._classPrivateFieldInitSpec(this, _api, void 0);
242
+ require_follow._classPrivateFieldInitSpec(this, _local, void 0);
243
+ require_follow._classPrivateFieldInitSpec(this, _listeners, /* @__PURE__ */ new Set());
244
+ require_follow._classPrivateFieldInitSpec(this, _state, void 0);
245
+ require_follow._classPrivateFieldInitSpec(this, _turn, void 0);
246
+ require_follow._classPrivateFieldInitSpec(this, _page, 1);
247
+ require_follow._classPrivateFieldInitSpec(this, _conversationKind, void 0);
248
+ require_follow._classPrivateFieldInitSpec(this, _persistTimer, void 0);
249
+ require_follow._classPrivateFieldInitSpec(this, _selecting, Promise.resolve());
250
+ require_follow._classPrivateFieldInitSpec(this, _epoch, 0);
251
+ this.getState = () => require_follow._classPrivateFieldGet2(_state, this);
252
+ this.subscribe = (listener) => {
253
+ require_follow._classPrivateFieldGet2(_listeners, this).add(listener);
254
+ listener(require_follow._classPrivateFieldGet2(_state, this));
255
+ return () => {
256
+ require_follow._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 (require_follow._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 = require_follow._classPrivateFieldGet2(_state, this).conversationId === void 0;
272
+ const conversationId = require_follow._classPrivateFieldGet2(_state, this).conversationId ?? require_follow.randomId();
273
+ const turn = {
274
+ controller: new AbortController(),
275
+ conversationId,
276
+ userMessageId: `pending-${require_follow.randomId()}`,
277
+ isNew,
278
+ started: false,
279
+ withdrawn: false,
280
+ streamedText: false
281
+ };
282
+ require_follow._classPrivateFieldSet2(_turn, this, turn);
283
+ require_follow._classPrivateFieldSet2(_epoch, this, (_this$epoch = require_follow._classPrivateFieldGet2(_epoch, this), _this$epoch++, _this$epoch));
284
+ const timestamp = require_follow.now();
285
+ const touched = {
286
+ ...require_follow._classPrivateFieldGet2(_state, this).conversations.find((c) => c.id === conversationId) ?? {
287
+ id: conversationId,
288
+ title: require_follow.conversationTitle(content),
289
+ createdAt: timestamp,
290
+ updatedAt: timestamp
291
+ },
292
+ updatedAt: timestamp
293
+ };
294
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
295
+ conversationId,
296
+ messages: [...require_follow._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
+ ...require_follow._classPrivateFieldGet2(_config, this).inputs,
310
+ ...options.inputs,
311
+ user_message: content
312
+ };
313
+ if (attachmentsInput && attachments.length > 0) {
314
+ const uploaded = await uploadAttachments(require_follow._classPrivateFieldGet2(_api, this), attachments, require_follow.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 (require_follow._assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, { messages: require_follow._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 require_follow.abortError();
327
+ turn.started = true;
328
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, { conversations: [touched, ...require_follow._classPrivateFieldGet2(_state, this).conversations.filter((c) => c.id !== conversationId)] });
329
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _rememberConversation).call(this);
330
+ const context = {
331
+ memoryId: conversationId,
332
+ conversationId,
333
+ signal: turn.controller.signal
334
+ };
335
+ turn.jobId = require_follow._classPrivateFieldGet2(_config, this).run ? await require_follow._classPrivateFieldGet2(_config, this).run(args, context) : await require_follow._classPrivateFieldGet2(_api, this).runFlow(require_follow._classPrivateFieldGet2(_config, this).flowPath, args, context);
336
+ } catch (e) {
337
+ try {
338
+ if (e instanceof require_follow.TurnRunningError || !turn.started) {
339
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _withdrawTurn).call(this, turn);
340
+ throw e;
341
+ }
342
+ if (turn.controller.signal.aborted || require_follow.isAbortError(e)) return;
343
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _failTurn).call(this, turn, e);
344
+ return;
345
+ } finally {
346
+ if (require_follow._classPrivateFieldGet2(_turn, this) === turn) require_follow._classPrivateFieldSet2(_turn, this, void 0);
347
+ }
348
+ }
349
+ await require_follow._assertClassBrand(_ChatImpl_brand, this, _followTurn).call(this, turn, isNew);
350
+ };
351
+ this.resumeTurn = async ({ jobId, userSeq }) => {
352
+ var _this$epoch3;
353
+ const conversationId = require_follow._classPrivateFieldGet2(_state, this).conversationId;
354
+ if (!conversationId || require_follow._classPrivateFieldGet2(_turn, this)) return;
355
+ if (require_follow._classPrivateFieldGet2(_state, this).history !== "server") {
356
+ if (require_follow._classPrivateFieldGet2(_state, this).status === "submitted") require_follow._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
+ require_follow._classPrivateFieldSet2(_turn, this, turn);
370
+ require_follow._classPrivateFieldSet2(_epoch, this, (_this$epoch3 = require_follow._classPrivateFieldGet2(_epoch, this), _this$epoch3++, _this$epoch3));
371
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
372
+ status: "submitted",
373
+ error: void 0
374
+ });
375
+ try {
376
+ await require_follow._classPrivateFieldGet2(_selecting, this);
377
+ if (!require_follow._assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
378
+ const newest = require_follow._assertClassBrand(_ChatImpl_brand, this, _latestUserAfter).call(this, userSeq);
379
+ if (newest) {
380
+ if (!newest.jobId) {
381
+ if (require_follow._classPrivateFieldGet2(_turn, this) === turn) require_follow._classPrivateFieldSet2(_turn, this, void 0);
382
+ require_follow._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 = require_follow._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 require_follow._classPrivateFieldGet2(_api, this).listMessages(conversationId, {
399
+ afterSeq: userSeq - 1,
400
+ perPage: 1,
401
+ signal: turn.controller.signal
402
+ });
403
+ if (!require_follow._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
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, { messages });
410
+ } catch (e) {
411
+ if (!(turn.controller.signal.aborted || require_follow.isAbortError(e))) require_follow._assertClassBrand(_ChatImpl_brand, this, _failTurn).call(this, turn, e);
412
+ if (require_follow._classPrivateFieldGet2(_turn, this) === turn) require_follow._classPrivateFieldSet2(_turn, this, void 0);
413
+ return;
414
+ }
415
+ await require_follow._assertClassBrand(_ChatImpl_brand, this, _followTurn).call(this, turn, false);
416
+ };
417
+ this.stop = async () => {
418
+ const turn = require_follow._classPrivateFieldGet2(_turn, this);
419
+ if (!turn) return;
420
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _detachTurn).call(this);
421
+ if (!turn.started) {
422
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _withdrawTurn).call(this, turn);
423
+ return;
424
+ }
425
+ if (require_follow._classPrivateFieldGet2(_state, this).conversationId === turn.conversationId) {
426
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
427
+ messages: finalized(require_follow._classPrivateFieldGet2(_state, this).messages),
428
+ status: "idle"
429
+ });
430
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _persistLocal).call(this);
431
+ }
432
+ if (turn.jobId) await require_follow._classPrivateFieldGet2(_api, this).cancelJob(turn.jobId).catch(() => {});
433
+ if (require_follow._classPrivateFieldGet2(_state, this).history !== "server") return;
434
+ await require_follow.sleep(RECONCILE_DELAY_MS).catch(() => {});
435
+ if (require_follow._classPrivateFieldGet2(_turn, this) || require_follow._classPrivateFieldGet2(_state, this).conversationId !== turn.conversationId) return;
436
+ await require_follow._assertClassBrand(_ChatImpl_brand, this, _syncFromServer).call(this, turn.conversationId).catch(() => {});
437
+ };
438
+ this.newConversation = () => {
439
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _leaveConversation).call(this);
440
+ require_follow._classPrivateFieldSet2(_page, this, 1);
441
+ require_follow._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 === require_follow._classPrivateFieldGet2(_state, this).conversationId) return require_follow._classPrivateFieldGet2(_selecting, this);
452
+ const selecting = require_follow._assertClassBrand(_ChatImpl_brand, this, _select).call(this, conversationId);
453
+ require_follow._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 !== require_follow._classPrivateFieldGet2(_conversationKind, this);
459
+ if ("kind" in options) require_follow._classPrivateFieldSet2(_conversationKind, this, options.kind);
460
+ const kind = require_follow._classPrivateFieldGet2(_conversationKind, this);
461
+ let conversations;
462
+ if (require_follow._classPrivateFieldGet2(_state, this).history === "server") try {
463
+ conversations = (await require_follow._classPrivateFieldGet2(_api, this).listConversations(require_follow._classPrivateFieldGet2(_config, this).flowPath, {
464
+ page,
465
+ perPage: options.perPage ?? require_follow._classPrivateFieldGet2(_config, this).pageSize,
466
+ kind
467
+ })).map(fromConversation);
468
+ } catch (e) {
469
+ if (!require_follow._assertClassBrand(_ChatImpl_brand, this, _fallBackToLocal).call(this, e)) throw e;
470
+ conversations = require_follow._classPrivateFieldGet2(_local, this).listConversations();
471
+ }
472
+ else conversations = require_follow._classPrivateFieldGet2(_state, this).history === "local" ? require_follow._classPrivateFieldGet2(_local, this).listConversations() : [];
473
+ if (kind !== require_follow._classPrivateFieldGet2(_conversationKind, this)) return conversations;
474
+ const known = new Set(require_follow._classPrivateFieldGet2(_state, this).conversations.map((c) => c.id));
475
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, { conversations: page === 1 || kindChanged ? conversations : [...require_follow._classPrivateFieldGet2(_state, this).conversations, ...conversations.filter((c) => !known.has(c.id))] });
476
+ return conversations;
477
+ };
478
+ this.deleteConversation = async (conversationId) => {
479
+ if (require_follow._classPrivateFieldGet2(_state, this).conversationId === conversationId) {
480
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _detachTurn).call(this);
481
+ clearTimeout(require_follow._classPrivateFieldGet2(_persistTimer, this));
482
+ require_follow._classPrivateFieldSet2(_persistTimer, this, void 0);
483
+ this.newConversation();
484
+ }
485
+ if (require_follow._classPrivateFieldGet2(_state, this).history === "server") await require_follow._classPrivateFieldGet2(_api, this).deleteConversation(conversationId);
486
+ else if (require_follow._classPrivateFieldGet2(_state, this).history === "local") require_follow._classPrivateFieldGet2(_local, this).deleteConversation(conversationId);
487
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, { conversations: require_follow._classPrivateFieldGet2(_state, this).conversations.filter((c) => c.id !== conversationId) });
488
+ };
489
+ this.renameConversation = async (conversationId, title) => {
490
+ const trimmed = require_follow.truncateTitle(title.trim());
491
+ if (!trimmed) return;
492
+ if (require_follow._classPrivateFieldGet2(_state, this).history === "server") await require_follow._classPrivateFieldGet2(_api, this).renameConversation(conversationId, trimmed);
493
+ else if (require_follow._classPrivateFieldGet2(_state, this).history === "local") require_follow._classPrivateFieldGet2(_local, this).renameConversation(conversationId, trimmed);
494
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, { conversations: require_follow._classPrivateFieldGet2(_state, this).conversations.map((c) => c.id === conversationId ? {
495
+ ...c,
496
+ title: trimmed
497
+ } : c) });
498
+ };
499
+ this.loadOlderMessages = async () => {
500
+ const conversationId = require_follow._classPrivateFieldGet2(_state, this).conversationId;
501
+ if (!conversationId || require_follow._classPrivateFieldGet2(_state, this).history !== "server" || !require_follow._classPrivateFieldGet2(_state, this).hasMoreMessages || require_follow._classPrivateFieldGet2(_state, this).loadingMessages) return;
502
+ const page = require_follow._classPrivateFieldGet2(_page, this) + 1;
503
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, { loadingMessages: true });
504
+ try {
505
+ const rows = await require_follow._classPrivateFieldGet2(_api, this).listMessages(conversationId, {
506
+ page,
507
+ perPage: require_follow._classPrivateFieldGet2(_config, this).pageSize
508
+ });
509
+ if (require_follow._classPrivateFieldGet2(_state, this).conversationId !== conversationId) return;
510
+ const known = new Set(require_follow._classPrivateFieldGet2(_state, this).messages.map((m) => m.serverId ?? m.id));
511
+ const oldest = require_follow._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
+ require_follow._classPrivateFieldSet2(_page, this, page);
514
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
515
+ messages: [...older, ...require_follow._classPrivateFieldGet2(_state, this).messages],
516
+ hasMoreMessages: rows.length === require_follow._classPrivateFieldGet2(_config, this).pageSize
517
+ });
518
+ } finally {
519
+ if (require_follow._classPrivateFieldGet2(_state, this).conversationId === conversationId) require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, { loadingMessages: false });
520
+ }
521
+ };
522
+ this.refreshMessages = async () => {
523
+ const conversationId = require_follow._classPrivateFieldGet2(_state, this).conversationId;
524
+ if (!conversationId || require_follow._classPrivateFieldGet2(_turn, this) || require_follow._classPrivateFieldGet2(_state, this).history !== "server" || require_follow._classPrivateFieldGet2(_state, this).loadingMessages) return;
525
+ const newestBefore = latestSeq(require_follow._classPrivateFieldGet2(_state, this).messages);
526
+ const failure = lastFailureShown(require_follow._classPrivateFieldGet2(_state, this).messages);
527
+ const epoch = require_follow._classPrivateFieldGet2(_epoch, this);
528
+ await require_follow._assertClassBrand(_ChatImpl_brand, this, _syncFromServer).call(this, conversationId, () => require_follow._classPrivateFieldGet2(_epoch, this) === epoch);
529
+ if (require_follow._classPrivateFieldGet2(_epoch, this) !== epoch) return;
530
+ if (!failure || require_follow._classPrivateFieldGet2(_state, this).conversationId !== conversationId) return;
531
+ if (!require_follow._classPrivateFieldGet2(_state, this).messages.some((m) => m.id === failure.id)) return;
532
+ const jobs = failure.jobId ? await require_follow._assertClassBrand(_ChatImpl_brand, this, _turnJobIds).call(this, failure.jobId) : void 0;
533
+ if (require_follow._classPrivateFieldGet2(_state, this).conversationId !== conversationId) return;
534
+ const held = require_follow._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 = require_follow._classPrivateFieldGet2(_state, this).status === "error" && lastFailureShown(held)?.id === failure.id;
538
+ require_follow._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
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _leaveConversation).call(this);
548
+ };
549
+ require_follow._classPrivateFieldSet2(_config, this, resolveConfig(options));
550
+ require_follow._classPrivateFieldSet2(_api, this, new require_follow.WindmillChatApi({
551
+ baseUrl: require_follow._classPrivateFieldGet2(_config, this).baseUrl,
552
+ workspace: require_follow._classPrivateFieldGet2(_config, this).workspace,
553
+ token: require_follow._classPrivateFieldGet2(_config, this).token,
554
+ fetch: require_follow._classPrivateFieldGet2(_config, this).fetch,
555
+ pollDelayMs: require_follow._classPrivateFieldGet2(_config, this).pollDelayMs
556
+ }));
557
+ require_follow._classPrivateFieldSet2(_local, this, createLocalHistory(require_follow._classPrivateFieldGet2(_config, this).storage, `windmill-chat:${require_follow._classPrivateFieldGet2(_config, this).baseUrl}:${require_follow._classPrivateFieldGet2(_config, this).workspace}:${require_follow._classPrivateFieldGet2(_config, this).flowPath}` + (require_follow._classPrivateFieldGet2(_config, this).storageKey ? `:${require_follow._classPrivateFieldGet2(_config, this).storageKey}` : "")));
558
+ require_follow._classPrivateFieldSet2(_state, this, {
559
+ conversationId: void 0,
560
+ messages: [],
561
+ status: "idle",
562
+ error: void 0,
563
+ conversations: [],
564
+ history: require_follow._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 = require_follow._classPrivateFieldGet2(_state, this).history === "server" ? require_follow._assertClassBrand(_ChatImpl_brand, this, _startPolling).call(this, turn) : () => {};
574
+ let result;
575
+ try {
576
+ result = await require_follow._assertClassBrand(_ChatImpl_brand, this, _follow).call(this, turn, stopPolling);
577
+ } finally {
578
+ stopPolling();
579
+ }
580
+ nextTurn = await require_follow._assertClassBrand(_ChatImpl_brand, this, _finishTurn).call(this, turn, result, isNew);
581
+ } catch (e) {
582
+ if (!turn.started) {
583
+ if (require_follow._classPrivateFieldGet2(_turn, this) === turn) require_follow._classPrivateFieldSet2(_turn, this, void 0);
584
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _withdrawTurn).call(this, turn);
585
+ throw e;
586
+ }
587
+ if (turn.controller.signal.aborted || require_follow.isAbortError(e)) return;
588
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _failTurn).call(this, turn, e);
589
+ } finally {
590
+ if (require_follow._classPrivateFieldGet2(_turn, this) === turn) require_follow._classPrivateFieldSet2(_turn, this, void 0);
591
+ }
592
+ if (nextTurn) await this.resumeTurn(nextTurn);
593
+ }
594
+ async function _select(conversationId) {
595
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _leaveConversation).call(this);
596
+ require_follow._classPrivateFieldSet2(_page, this, 1);
597
+ require_follow._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 (require_follow._classPrivateFieldGet2(_state, this).history !== "server") {
606
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
607
+ messages: require_follow._classPrivateFieldGet2(_state, this).history === "local" ? require_follow._classPrivateFieldGet2(_local, this).getMessages(conversationId) : [],
608
+ loadingMessages: false
609
+ });
610
+ return;
611
+ }
612
+ try {
613
+ const rows = await require_follow._classPrivateFieldGet2(_api, this).listMessages(conversationId, { perPage: require_follow._classPrivateFieldGet2(_config, this).pageSize });
614
+ if (require_follow._classPrivateFieldGet2(_state, this).conversationId !== conversationId) return;
615
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
616
+ messages: rows.map(fromRow),
617
+ loadingMessages: false,
618
+ hasMoreMessages: rows.length === require_follow._classPrivateFieldGet2(_config, this).pageSize
619
+ });
620
+ } catch (e) {
621
+ if (require_follow._classPrivateFieldGet2(_state, this).conversationId !== conversationId) return;
622
+ if (require_follow._assertClassBrand(_ChatImpl_brand, this, _fallBackToLocal).call(this, e)) {
623
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
624
+ messages: require_follow._classPrivateFieldGet2(_local, this).getMessages(conversationId),
625
+ loadingMessages: false
626
+ });
627
+ return;
628
+ }
629
+ require_follow._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 require_follow.followJob(require_follow._classPrivateFieldGet2(_api, this), turn.jobId, { signal: turn.controller.signal })) {
639
+ if (event.type === "completed") {
640
+ if (event.streamLost) require_follow._assertClassBrand(_ChatImpl_brand, this, _dropCutRound).call(this, turn);
641
+ return event.result;
642
+ }
643
+ if (!started) {
644
+ started = true;
645
+ onStreamStart();
646
+ }
647
+ require_follow._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 (!require_follow._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) require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, { messages: require_follow._classPrivateFieldGet2(_state, this).messages.filter((m) => m.id !== cut) });
662
+ }
663
+ function _applyEvents(turn, events) {
664
+ if (events.length === 0 || !require_follow._assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
665
+ let messages = [...require_follow._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-${require_follow.randomId()}`,
685
+ role: "tool",
686
+ content: content ?? "",
687
+ reasoning,
688
+ success: success ?? true,
689
+ createdAt: require_follow.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-${require_follow.randomId()}`;
712
+ messages.push({
713
+ id: turn.assistantId,
714
+ role: "assistant",
715
+ content: text,
716
+ reasoning: reasoning || void 0,
717
+ success: true,
718
+ createdAt: require_follow.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
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
748
+ messages,
749
+ status: "streaming"
750
+ });
751
+ }
752
+ async function _finishTurn(turn, result, isNew) {
753
+ if (!require_follow._assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
754
+ if (require_follow._classPrivateFieldGet2(_state, this).history === "server") {
755
+ turn.jobIds = await require_follow._assertClassBrand(_ChatImpl_brand, this, _turnJobIds).call(this, turn.jobId, turn.controller.signal);
756
+ if (!require_follow._assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
757
+ const reconciled = await require_follow._assertClassBrand(_ChatImpl_brand, this, _reconcileTurn).call(this, turn);
758
+ if (!require_follow._assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
759
+ if (reconciled) {
760
+ const nextTurn = require_follow._assertClassBrand(_ChatImpl_brand, this, _nextTurnAfter).call(this, turn);
761
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, { status: nextTurn ? "submitted" : "idle" });
762
+ require_follow._classPrivateFieldGet2(_config, this).onFinish?.({
763
+ conversationId: turn.conversationId,
764
+ jobId: turn.jobId,
765
+ messages: require_follow._classPrivateFieldGet2(_state, this).messages
766
+ });
767
+ if (isNew) await this.loadConversations().catch(() => {});
768
+ if (!require_follow._assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
769
+ return nextTurn;
770
+ }
771
+ }
772
+ let messages = require_follow._classPrivateFieldGet2(_state, this).messages;
773
+ let failed = false;
774
+ if (require_follow.isErrorResult(result)) {
775
+ failed = await require_follow._classPrivateFieldGet2(_api, this).getCompletedResult(turn.jobId, turn.controller.signal).then((r) => r.success === false).catch(() => true);
776
+ if (!require_follow._assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
777
+ if (failed) messages = [...messages, assistantMessage(require_follow.errorResultMessage(result), false, turn.jobId)];
778
+ }
779
+ if (!failed && !turn.streamedText) {
780
+ const answer = require_follow.extractChatAnswer(result);
781
+ if (answer !== void 0) messages = [...messages, assistantMessage(answer, true, turn.jobId)];
782
+ }
783
+ const nextTurn = require_follow._classPrivateFieldGet2(_state, this).history === "server" ? require_follow._assertClassBrand(_ChatImpl_brand, this, _nextTurnAfter).call(this, turn) : void 0;
784
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
785
+ messages: finalized(messages),
786
+ status: nextTurn ? "submitted" : "idle"
787
+ });
788
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _persistLocal).call(this);
789
+ require_follow._classPrivateFieldGet2(_config, this).onFinish?.({
790
+ conversationId: turn.conversationId,
791
+ jobId: turn.jobId,
792
+ messages: require_follow._classPrivateFieldGet2(_state, this).messages
793
+ });
794
+ if (!require_follow._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 = () => require_follow._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 require_follow._assertClassBrand(_ChatImpl_brand, this, _rowsAfterLastSeq).call(this, turn.conversationId, turn.controller.signal);
814
+ } catch (e) {
815
+ if (require_follow.isAbortError(e)) throw e;
816
+ if (require_follow._assertClassBrand(_ChatImpl_brand, this, _fallBackToLocal).call(this, e)) return false;
817
+ if (e instanceof require_follow.WindmillApiError && (e.status === 401 || e.status === 403) || attempt === RECONCILE_ATTEMPTS) return answered();
818
+ await require_follow.sleep(RECONCILE_DELAY_MS, turn.controller.signal);
819
+ continue;
820
+ }
821
+ if (!require_follow._assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return true;
822
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _mergeRows).call(this, rows);
823
+ if (answered() && !require_follow._classPrivateFieldGet2(_state, this).messages.some((m) => m.pending && m.content)) break;
824
+ if (attempt < RECONCILE_ATTEMPTS) await require_follow.sleep(RECONCILE_DELAY_MS, turn.controller.signal);
825
+ }
826
+ if (!require_follow._assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return true;
827
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, { messages: finalized(require_follow._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 = require_follow._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 require_follow._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 (require_follow.isAbortError(e)) throw e;
885
+ if (e instanceof require_follow.WindmillApiError && e.status >= 400 && e.status < 500 || attempt === RECONCILE_ATTEMPTS) return void 0;
886
+ await require_follow.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 (require_follow._classPrivateFieldGet2(_state, this).conversationId === id) {
901
+ const messages = withoutTurn(require_follow._classPrivateFieldGet2(_state, this).messages);
902
+ if (require_follow._classPrivateFieldGet2(_turn, this) !== void 0 && require_follow._classPrivateFieldGet2(_turn, this) !== turn) require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, { messages });
903
+ else {
904
+ const unopened = turn.isNew && messages.length === 0;
905
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
906
+ messages,
907
+ status: "idle",
908
+ error: void 0,
909
+ ...unopened ? { conversationId: void 0 } : {}
910
+ });
911
+ }
912
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _persistLocal).call(this);
913
+ }
914
+ if (require_follow._classPrivateFieldGet2(_state, this).history === "local" && require_follow._classPrivateFieldGet2(_state, this).conversationId !== id) {
915
+ const stored = withoutTurn(require_follow._classPrivateFieldGet2(_local, this).getMessages(id));
916
+ if (stored.length > 0) require_follow._classPrivateFieldGet2(_local, this).saveMessages(id, stored);
917
+ else if (!require_follow._classPrivateFieldGet2(_state, this).conversations.some((c) => c.id === id)) require_follow._classPrivateFieldGet2(_local, this).deleteConversation(id);
918
+ }
919
+ }
920
+ function _failTurn(turn, e) {
921
+ if (!require_follow._assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) return;
922
+ const error = toError(e);
923
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
924
+ messages: [...finalized(require_follow._classPrivateFieldGet2(_state, this).messages), assistantMessage(error.message, false, turn.jobId)],
925
+ status: "error",
926
+ error
927
+ });
928
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _persistLocal).call(this);
929
+ require_follow._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 require_follow.sleep(POLL_INTERVAL_MS, signal);
942
+ } catch {
943
+ return;
944
+ }
945
+ if (stopped) return;
946
+ try {
947
+ const rows = await require_follow._assertClassBrand(_ChatImpl_brand, this, _rowsAfterLastSeq).call(this, turn.conversationId, signal);
948
+ if (!stopped && require_follow._assertClassBrand(_ChatImpl_brand, this, _turnActive).call(this, turn)) require_follow._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 = require_follow._assertClassBrand(_ChatImpl_brand, this, _lastSeq).call(this);
961
+ while (true) {
962
+ const page = await require_follow._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 require_follow._assertClassBrand(_ChatImpl_brand, this, _rowsAfterLastSeq).call(this, conversationId);
974
+ if (require_follow._classPrivateFieldGet2(_turn, this) || require_follow._classPrivateFieldGet2(_state, this).conversationId !== conversationId) return;
975
+ if (stillCurrent && !stillCurrent()) return;
976
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _mergeRows).call(this, rows);
977
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, { messages: finalized(require_follow._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 = [...require_follow._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
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, { messages });
1029
+ }
1030
+ function _lastSeq() {
1031
+ let last;
1032
+ for (const m of require_follow._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(require_follow._classPrivateFieldGet2(_persistTimer, this));
1038
+ require_follow._classPrivateFieldSet2(_persistTimer, this, void 0);
1039
+ const id = require_follow._classPrivateFieldGet2(_state, this).conversationId;
1040
+ if (require_follow._classPrivateFieldGet2(_state, this).history !== "local" || !id) return;
1041
+ require_follow._classPrivateFieldGet2(_local, this).saveMessages(id, require_follow._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 = require_follow._classPrivateFieldGet2(_state, this).conversationId;
1049
+ if (require_follow._classPrivateFieldGet2(_state, this).history !== "local" || !id) return;
1050
+ const conversation = require_follow._classPrivateFieldGet2(_state, this).conversations.find((c) => c.id === id);
1051
+ if (conversation) require_follow._classPrivateFieldGet2(_local, this).upsertConversation(conversation);
1052
+ }
1053
+ /** Whether an unreadable server history should silently become local history. */
1054
+ function _fallBackToLocal(e) {
1055
+ if (require_follow._classPrivateFieldGet2(_state, this).history !== "server" || require_follow._classPrivateFieldGet2(_config, this).historyExplicit) return false;
1056
+ if (e instanceof require_follow.WindmillApiError && (e.status === 401 || e.status === 403)) {
1057
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, { history: "local" });
1058
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _rememberConversation).call(this);
1059
+ return true;
1060
+ }
1061
+ return false;
1062
+ }
1063
+ function _turnActive(turn) {
1064
+ return require_follow._classPrivateFieldGet2(_turn, this) === turn && require_follow._classPrivateFieldGet2(_state, this).conversationId === turn.conversationId;
1065
+ }
1066
+ function _latestUserAfter(userSeq) {
1067
+ return require_follow._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 = require_follow._classPrivateFieldGet2(_state, this).messages.find((message) => message.id === turn.userMessageId)?.seq;
1071
+ if (userSeq === void 0) return void 0;
1072
+ const next = require_follow._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 = require_follow._classPrivateFieldGet2(_turn, this);
1081
+ if (!turn) return;
1082
+ require_follow._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
+ require_follow._classPrivateFieldSet2(_epoch, this, (_this$epoch5 = require_follow._classPrivateFieldGet2(_epoch, this), _this$epoch5++, _this$epoch5));
1093
+ if (require_follow._classPrivateFieldGet2(_turn, this)) {
1094
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _detachTurn).call(this);
1095
+ require_follow._assertClassBrand(_ChatImpl_brand, this, _set).call(this, {
1096
+ messages: finalized(require_follow._classPrivateFieldGet2(_state, this).messages),
1097
+ status: "idle"
1098
+ });
1099
+ }
1100
+ if (require_follow._classPrivateFieldGet2(_persistTimer, this)) require_follow._assertClassBrand(_ChatImpl_brand, this, _persistLocal).call(this);
1101
+ }
1102
+ function _set(patch) {
1103
+ require_follow._classPrivateFieldSet2(_state, this, {
1104
+ ...require_follow._classPrivateFieldGet2(_state, this),
1105
+ ...patch
1106
+ });
1107
+ for (const listener of require_follow._classPrivateFieldGet2(_listeners, this)) listener(require_follow._classPrivateFieldGet2(_state, this));
1108
+ if (require_follow._classPrivateFieldGet2(_state, this).history === "local" && require_follow._classPrivateFieldGet2(_state, this).conversationId) {
1109
+ clearTimeout(require_follow._classPrivateFieldGet2(_persistTimer, this));
1110
+ require_follow._classPrivateFieldSet2(_persistTimer, this, setTimeout(() => require_follow._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-${require_follow.randomId()}`,
1164
+ role: "assistant",
1165
+ content,
1166
+ success,
1167
+ createdAt: require_follow.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
+ Object.defineProperty(exports, "CHAT_UPLOADS_PREFIX", {
1183
+ enumerable: true,
1184
+ get: function() {
1185
+ return CHAT_UPLOADS_PREFIX;
1186
+ }
1187
+ });
1188
+ Object.defineProperty(exports, "createChat", {
1189
+ enumerable: true,
1190
+ get: function() {
1191
+ return createChat;
1192
+ }
1193
+ });
1194
+ Object.defineProperty(exports, "detectRawApp", {
1195
+ enumerable: true,
1196
+ get: function() {
1197
+ return detectRawApp;
1198
+ }
1199
+ });
1200
+ Object.defineProperty(exports, "storedAttachmentName", {
1201
+ enumerable: true,
1202
+ get: function() {
1203
+ return storedAttachmentName;
1204
+ }
1205
+ });
1206
+ Object.defineProperty(exports, "uploadAttachments", {
1207
+ enumerable: true,
1208
+ get: function() {
1209
+ return uploadAttachments;
1210
+ }
1211
+ });