@love-moon/app-sdk 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,814 @@
1
+ // src/react/store/chat-store.tsx
2
+ import {
3
+ createContext,
4
+ useContext,
5
+ useMemo,
6
+ useReducer,
7
+ useRef,
8
+ useEffect
9
+ } from "react";
10
+ import { jsx } from "react/jsx-runtime";
11
+ function generateRequestId() {
12
+ const c = globalThis.crypto;
13
+ if (c?.randomUUID) return c.randomUUID();
14
+ if (c?.getRandomValues) {
15
+ const bytes = new Uint8Array(16);
16
+ c.getRandomValues(bytes);
17
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
18
+ }
19
+ return Math.random().toString(36).slice(2) + Date.now().toString(36);
20
+ }
21
+ var INITIAL_STATE = {
22
+ messages: [],
23
+ pendingByClientId: {},
24
+ runtime: null,
25
+ connectionState: "offline",
26
+ hasMoreBefore: false,
27
+ oldestMessageId: null,
28
+ loadingHistory: false,
29
+ error: null,
30
+ latestReplyId: null
31
+ };
32
+ function reducer(state, action) {
33
+ switch (action.type) {
34
+ case "HYDRATE_HISTORY":
35
+ return {
36
+ ...state,
37
+ messages: dedupSorted(action.messages),
38
+ hasMoreBefore: action.hasMoreBefore,
39
+ oldestMessageId: action.oldestMessageId
40
+ };
41
+ case "PREPEND_HISTORY":
42
+ return {
43
+ ...state,
44
+ messages: dedupSorted([...action.messages, ...state.messages]),
45
+ hasMoreBefore: action.hasMoreBefore,
46
+ oldestMessageId: action.oldestMessageId ?? state.oldestMessageId
47
+ };
48
+ case "LOADING_HISTORY":
49
+ return { ...state, loadingHistory: action.loading };
50
+ case "OPTIMISTIC_SEND": {
51
+ return {
52
+ ...state,
53
+ messages: dedupSorted([...state.messages, action.message]),
54
+ pendingByClientId: {
55
+ ...state.pendingByClientId,
56
+ [action.clientRequestId]: action.message
57
+ }
58
+ };
59
+ }
60
+ case "CONFIRM_SEND": {
61
+ const { [action.clientRequestId]: _pending, ...rest } = state.pendingByClientId;
62
+ const pendingId = `pending:${action.clientRequestId}`;
63
+ const filtered = state.messages.filter((m) => m.id !== pendingId);
64
+ return {
65
+ ...state,
66
+ pendingByClientId: rest,
67
+ messages: dedupSorted([...filtered, action.serverMessage])
68
+ };
69
+ }
70
+ case "ROLLBACK_SEND": {
71
+ const { [action.clientRequestId]: _pending, ...rest } = state.pendingByClientId;
72
+ return {
73
+ ...state,
74
+ pendingByClientId: rest,
75
+ messages: state.messages.filter(
76
+ (m) => m.id !== `pending:${action.clientRequestId}`
77
+ )
78
+ };
79
+ }
80
+ case "APPEND_MESSAGE": {
81
+ const next = dedupSorted([...state.messages, action.message]);
82
+ const meta = action.message.metadata;
83
+ const auditActor = meta && typeof meta === "object" ? meta.audit?.actor : void 0;
84
+ const isSynthetic = Boolean(meta && meta.synthetic === true);
85
+ const isAppEcho = auditActor === "app";
86
+ const isAssistant = action.message.role !== "user" && action.message.role !== "sdk" && action.message.role !== "system" && !isSynthetic && !isAppEcho;
87
+ return {
88
+ ...state,
89
+ messages: next,
90
+ latestReplyId: isAssistant ? action.message.id : state.latestReplyId
91
+ };
92
+ }
93
+ case "UPDATE_MESSAGE": {
94
+ const idx = state.messages.findIndex((m) => m.id === action.message.id);
95
+ if (idx === -1) return state;
96
+ const next = state.messages.slice();
97
+ next[idx] = action.message;
98
+ return { ...state, messages: next };
99
+ }
100
+ case "SET_RUNTIME": {
101
+ const replyTo = action.status.replyTo ?? state.latestReplyId;
102
+ return {
103
+ ...state,
104
+ runtime: action.status,
105
+ latestReplyId: replyTo ?? state.latestReplyId
106
+ };
107
+ }
108
+ case "SET_CONNECTION":
109
+ return { ...state, connectionState: action.state };
110
+ case "SET_ERROR":
111
+ return { ...state, error: action.error };
112
+ case "TASK_FAILED":
113
+ return { ...state, error: action.error };
114
+ case "TASK_FINISHED":
115
+ return { ...state, runtime: null };
116
+ default:
117
+ return state;
118
+ }
119
+ }
120
+ function dedupSorted(messages) {
121
+ const byId = /* @__PURE__ */ new Map();
122
+ for (const m of messages) byId.set(m.id, m);
123
+ return Array.from(byId.values()).sort((a, b) => {
124
+ const ta = Date.parse(a.createdAt || "");
125
+ const tb = Date.parse(b.createdAt || "");
126
+ if (Number.isFinite(ta) && Number.isFinite(tb) && ta !== tb) return ta - tb;
127
+ return a.id.localeCompare(b.id);
128
+ });
129
+ }
130
+ var ChatContext = createContext(null);
131
+ function useChat() {
132
+ const ctx = useContext(ChatContext);
133
+ if (!ctx) {
134
+ throw new Error(
135
+ "useChat() must be called inside <ChatProvider> (or <ChatView>, which wraps it)."
136
+ );
137
+ }
138
+ return ctx;
139
+ }
140
+ function ChatProvider(props) {
141
+ const { taskId, adapter, onError } = props;
142
+ const [state, dispatch] = useReducer(reducer, INITIAL_STATE);
143
+ const taskIdRef = useRef(taskId);
144
+ const adapterRef = useRef(adapter);
145
+ const onErrorRef = useRef(onError);
146
+ taskIdRef.current = taskId;
147
+ onErrorRef.current = onError;
148
+ useEffect(() => {
149
+ adapterRef.current = adapter;
150
+ }, [adapter]);
151
+ useEffect(() => {
152
+ const activeAdapter = adapterRef.current;
153
+ let cancelled = false;
154
+ const abort = new AbortController();
155
+ dispatch({ type: "LOADING_HISTORY", loading: true });
156
+ activeAdapter.fetchHistory(taskId, { signal: abort.signal }).then((page) => {
157
+ if (cancelled) return;
158
+ dispatch({
159
+ type: "HYDRATE_HISTORY",
160
+ messages: page.messages,
161
+ hasMoreBefore: page.hasMoreBefore,
162
+ oldestMessageId: page.oldestMessageId
163
+ });
164
+ }).catch((err) => {
165
+ if (cancelled) return;
166
+ onErrorRef.current?.(err);
167
+ dispatch({
168
+ type: "SET_ERROR",
169
+ error: extractError(err)
170
+ });
171
+ }).finally(() => {
172
+ if (!cancelled) dispatch({ type: "LOADING_HISTORY", loading: false });
173
+ });
174
+ const sub = activeAdapter.subscribe(taskId, (event) => {
175
+ if (cancelled) return;
176
+ switch (event.type) {
177
+ case "message_appended":
178
+ dispatch({ type: "APPEND_MESSAGE", message: event.message });
179
+ break;
180
+ case "message_updated":
181
+ dispatch({ type: "UPDATE_MESSAGE", message: event.message });
182
+ break;
183
+ case "runtime_status":
184
+ dispatch({ type: "SET_RUNTIME", status: event.status });
185
+ break;
186
+ case "task_finished":
187
+ dispatch({ type: "TASK_FINISHED" });
188
+ break;
189
+ case "task_failed": {
190
+ const taskErr = {
191
+ code: event.error.code,
192
+ message: event.error.message
193
+ };
194
+ if (event.error.details !== void 0) taskErr.details = event.error.details;
195
+ if (event.error.cause !== void 0) taskErr.cause = event.error.cause;
196
+ dispatch({ type: "TASK_FAILED", error: taskErr });
197
+ break;
198
+ }
199
+ case "connection_state":
200
+ dispatch({ type: "SET_CONNECTION", state: event.state });
201
+ break;
202
+ }
203
+ });
204
+ return () => {
205
+ cancelled = true;
206
+ abort.abort();
207
+ sub.unsubscribe();
208
+ };
209
+ }, [taskId]);
210
+ const value = useMemo(() => {
211
+ const send = async (content, opts) => {
212
+ const trimmed = content.trim();
213
+ if (!trimmed) return;
214
+ const clientRequestId = generateRequestId();
215
+ const optimistic = {
216
+ id: `pending:${clientRequestId}`,
217
+ taskId: taskIdRef.current,
218
+ // The widget represents a real human typing — always 'user'. This
219
+ // matters for routing on the backend: the daemon's message router
220
+ // only forwards `task_user_message` envelopes to the AI fire process
221
+ // (see conductor-sdk's message/router.ts). A message persisted with
222
+ // role='sdk' would silently get ignored by codex/claude, leaving
223
+ // the user wondering why their question never got a reply.
224
+ role: "user",
225
+ content: trimmed,
226
+ metadata: opts?.metadata ?? null,
227
+ attachments: [],
228
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
229
+ };
230
+ dispatch({ type: "OPTIMISTIC_SEND", message: optimistic, clientRequestId });
231
+ try {
232
+ const payload = {
233
+ content: trimmed,
234
+ clientRequestId,
235
+ role: "user",
236
+ ...opts?.metadata ? { metadata: opts.metadata } : {}
237
+ };
238
+ const server = await adapterRef.current.sendMessage(taskIdRef.current, payload);
239
+ dispatch({ type: "CONFIRM_SEND", clientRequestId, serverMessage: server });
240
+ } catch (err) {
241
+ dispatch({ type: "ROLLBACK_SEND", clientRequestId });
242
+ onErrorRef.current?.(err);
243
+ dispatch({ type: "SET_ERROR", error: extractError(err) });
244
+ }
245
+ };
246
+ const interrupt = async () => {
247
+ const targetReplyTo = state.runtime?.replyTo ?? state.latestReplyId;
248
+ if (!targetReplyTo) return;
249
+ try {
250
+ await adapterRef.current.interrupt(taskIdRef.current, { targetReplyTo });
251
+ } catch (err) {
252
+ onErrorRef.current?.(err);
253
+ dispatch({ type: "SET_ERROR", error: extractError(err) });
254
+ }
255
+ };
256
+ const loadEarlier = async () => {
257
+ if (!state.hasMoreBefore || state.loadingHistory) return;
258
+ dispatch({ type: "LOADING_HISTORY", loading: true });
259
+ try {
260
+ const page = await adapterRef.current.fetchHistory(taskIdRef.current, {
261
+ beforeId: state.oldestMessageId ?? void 0
262
+ });
263
+ dispatch({
264
+ type: "PREPEND_HISTORY",
265
+ messages: page.messages,
266
+ hasMoreBefore: page.hasMoreBefore,
267
+ oldestMessageId: page.oldestMessageId
268
+ });
269
+ } catch (err) {
270
+ onErrorRef.current?.(err);
271
+ dispatch({ type: "SET_ERROR", error: extractError(err) });
272
+ } finally {
273
+ dispatch({ type: "LOADING_HISTORY", loading: false });
274
+ }
275
+ };
276
+ return { state, taskId, adapter, send, interrupt, loadEarlier };
277
+ }, [state, taskId, adapter]);
278
+ return /* @__PURE__ */ jsx(ChatContext.Provider, { value, children: props.children });
279
+ }
280
+ function extractError(err) {
281
+ if (typeof err === "object" && err !== null) {
282
+ const obj = err;
283
+ const code = String(obj.code ?? "unknown_error");
284
+ const message = String(obj.message ?? "Unknown error");
285
+ const status = typeof obj.status === "number" ? obj.status : void 0;
286
+ const requestId = typeof obj.requestId === "string" ? obj.requestId : void 0;
287
+ const details = obj.details ?? void 0;
288
+ return {
289
+ code,
290
+ message,
291
+ ...status !== void 0 ? { status } : {},
292
+ ...requestId !== void 0 ? { requestId } : {},
293
+ ...details !== void 0 ? { details } : {}
294
+ };
295
+ }
296
+ return { code: "unknown_error", message: String(err) };
297
+ }
298
+
299
+ // src/react/components/MessageList.tsx
300
+ import { useEffect as useEffect2, useLayoutEffect, useRef as useRef2 } from "react";
301
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
302
+ function MessageList({ labels }) {
303
+ const { state, loadEarlier } = useChat();
304
+ const containerRef = useRef2(null);
305
+ const lastMessageCountRef = useRef2(state.messages.length);
306
+ useLayoutEffect(() => {
307
+ const el = containerRef.current;
308
+ if (!el) return;
309
+ const grew = state.messages.length > lastMessageCountRef.current;
310
+ lastMessageCountRef.current = state.messages.length;
311
+ if (!grew) return;
312
+ const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
313
+ if (distanceFromBottom < 120) {
314
+ el.scrollTop = el.scrollHeight;
315
+ }
316
+ }, [state.messages.length]);
317
+ useEffect2(() => {
318
+ const el = containerRef.current;
319
+ if (!el) return;
320
+ el.scrollTop = el.scrollHeight;
321
+ }, []);
322
+ return /* @__PURE__ */ jsxs("div", { ref: containerRef, className: "conductor-message-list", role: "log", "aria-live": "polite", children: [
323
+ state.hasMoreBefore && /* @__PURE__ */ jsx2("div", { className: "conductor-load-earlier", children: /* @__PURE__ */ jsx2(
324
+ "button",
325
+ {
326
+ type: "button",
327
+ onClick: () => {
328
+ void loadEarlier();
329
+ },
330
+ disabled: state.loadingHistory,
331
+ children: state.loadingHistory ? "\u2026" : labels.loadEarlier
332
+ }
333
+ ) }),
334
+ state.messages.length === 0 && !state.loadingHistory && /* @__PURE__ */ jsx2("div", { className: "conductor-empty" }),
335
+ state.messages.map((m) => {
336
+ const isUser = m.role === "user" || m.role === "sdk";
337
+ const isPending = m.id.startsWith("pending:");
338
+ return /* @__PURE__ */ jsx2(
339
+ "div",
340
+ {
341
+ className: "conductor-message " + (isUser ? "conductor-message--user" : "conductor-message--assistant") + (isPending ? " conductor-message--pending" : ""),
342
+ "data-role": m.role,
343
+ "data-message-id": m.id,
344
+ children: /* @__PURE__ */ jsx2("div", { className: "conductor-bubble", children: m.content })
345
+ },
346
+ m.id
347
+ );
348
+ })
349
+ ] });
350
+ }
351
+
352
+ // src/react/components/MessageInput.tsx
353
+ import { useCallback, useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState } from "react";
354
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
355
+ function MessageInput({ labels, disabled }) {
356
+ const { state, send, interrupt } = useChat();
357
+ const [value, setValue] = useState("");
358
+ const taRef = useRef3(null);
359
+ const isComposingRef = useRef3(false);
360
+ useLayoutEffect2(() => {
361
+ const el = taRef.current;
362
+ if (!el) return;
363
+ el.style.height = "auto";
364
+ el.style.height = `${Math.min(el.scrollHeight, 5 * 24 + 16)}px`;
365
+ }, [value]);
366
+ const handleSend = useCallback(() => {
367
+ if (!value.trim() || disabled) return;
368
+ void send(value);
369
+ setValue("");
370
+ }, [send, value, disabled]);
371
+ const handleKeyDown = useCallback(
372
+ (e) => {
373
+ const nativeComposing = e.nativeEvent.isComposing === true;
374
+ if (isComposingRef.current || nativeComposing) return;
375
+ if (e.key === "Enter" && !e.shiftKey) {
376
+ e.preventDefault();
377
+ handleSend();
378
+ }
379
+ },
380
+ [handleSend]
381
+ );
382
+ const handleCompositionStart = useCallback(() => {
383
+ isComposingRef.current = true;
384
+ }, []);
385
+ const handleCompositionEnd = useCallback(() => {
386
+ isComposingRef.current = false;
387
+ }, []);
388
+ const replyInProgress = state.runtime?.replyInProgress === true;
389
+ const canInterrupt = replyInProgress && Boolean(state.latestReplyId);
390
+ return /* @__PURE__ */ jsxs2("div", { className: "conductor-message-input", children: [
391
+ /* @__PURE__ */ jsx3(
392
+ "textarea",
393
+ {
394
+ ref: taRef,
395
+ value,
396
+ onChange: (e) => setValue(e.target.value),
397
+ onKeyDown: handleKeyDown,
398
+ onCompositionStart: handleCompositionStart,
399
+ onCompositionEnd: handleCompositionEnd,
400
+ placeholder: labels.inputPlaceholder,
401
+ rows: 1,
402
+ className: "conductor-message-input__textarea",
403
+ disabled
404
+ }
405
+ ),
406
+ /* @__PURE__ */ jsx3("div", { className: "conductor-message-input__actions", children: canInterrupt ? /* @__PURE__ */ jsx3(
407
+ "button",
408
+ {
409
+ type: "button",
410
+ className: "conductor-button conductor-button--interrupt",
411
+ onClick: () => void interrupt(),
412
+ children: labels.interrupt
413
+ }
414
+ ) : /* @__PURE__ */ jsx3(
415
+ "button",
416
+ {
417
+ type: "button",
418
+ className: "conductor-button conductor-button--send",
419
+ onClick: handleSend,
420
+ disabled: !value.trim() || disabled,
421
+ children: labels.send
422
+ }
423
+ ) })
424
+ ] });
425
+ }
426
+
427
+ // src/react/components/RuntimeStatusBar.tsx
428
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
429
+ function RuntimeStatusBar({ labels }) {
430
+ const { state } = useChat();
431
+ const runtime = state.runtime;
432
+ const isThinking = runtime?.replyInProgress === true || runtime?.state === "thinking";
433
+ const text = pickStatusText(runtime?.state, runtime?.statusLine, labels);
434
+ const showConnection = state.connectionState !== "connected" && state.connectionState !== "offline";
435
+ return /* @__PURE__ */ jsxs3("div", { className: "conductor-runtime-status", role: "status", "aria-live": "polite", children: [
436
+ /* @__PURE__ */ jsx4(
437
+ "span",
438
+ {
439
+ className: "conductor-runtime-indicator" + (isThinking ? " conductor-runtime-indicator--active" : ""),
440
+ "aria-hidden": "true"
441
+ }
442
+ ),
443
+ /* @__PURE__ */ jsx4("span", { className: "conductor-runtime-text", children: text }),
444
+ state.error && /* @__PURE__ */ jsx4("span", { className: "conductor-runtime-error", role: "alert", children: state.error.message }),
445
+ showConnection && /* @__PURE__ */ jsx4("span", { className: "conductor-runtime-connection", children: state.connectionState === "reconnecting" ? "\u2026" : "" })
446
+ ] });
447
+ }
448
+ function pickStatusText(state, statusLine, labels) {
449
+ if (statusLine) return statusLine;
450
+ switch (state) {
451
+ case "thinking":
452
+ return labels.statusThinking;
453
+ case "tool_call":
454
+ return labels.statusToolCall;
455
+ case "awaiting_user":
456
+ return labels.statusAwaitingUser;
457
+ case "done":
458
+ return labels.statusDone;
459
+ default:
460
+ return "";
461
+ }
462
+ }
463
+
464
+ // src/react/ChatView.tsx
465
+ import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
466
+ var DEFAULT_LABELS = {
467
+ statusThinking: "Thinking\u2026",
468
+ statusToolCall: "Calling tool\u2026",
469
+ statusAwaitingUser: "Waiting for input",
470
+ statusDone: "Done",
471
+ inputPlaceholder: "Type a message",
472
+ send: "Send",
473
+ interrupt: "Stop",
474
+ restart: "Restart",
475
+ loadEarlier: "Load earlier messages"
476
+ };
477
+ function ChatView(props) {
478
+ const labels = { ...DEFAULT_LABELS, ...props.labels ?? {} };
479
+ const themeStyle = props.theme ? toCssVariableStyle(props.theme) : void 0;
480
+ const className = ["conductor-chat-view", props.className].filter(Boolean).join(" ");
481
+ return /* @__PURE__ */ jsx5(
482
+ "div",
483
+ {
484
+ className,
485
+ "data-task-id": props.taskId,
486
+ "data-layout": props.layout ?? "auto",
487
+ style: themeStyle,
488
+ children: /* @__PURE__ */ jsxs4(ChatProvider, { taskId: props.taskId, adapter: props.adapter, onError: props.onError, children: [
489
+ /* @__PURE__ */ jsx5(RuntimeStatusBar, { labels }),
490
+ /* @__PURE__ */ jsx5(MessageList, { labels }),
491
+ /* @__PURE__ */ jsx5(MessageInput, { labels, disabled: props.readOnly })
492
+ ] })
493
+ }
494
+ );
495
+ }
496
+ function toCssVariableStyle(theme) {
497
+ const out = {};
498
+ for (const [key, value] of Object.entries(theme)) {
499
+ if (typeof value !== "string") continue;
500
+ const varName = WELL_KNOWN_THEME_VARS[key] ?? toCssVarName(key);
501
+ out[varName] = value;
502
+ }
503
+ return out;
504
+ }
505
+ var WELL_KNOWN_THEME_VARS = {
506
+ accent: "--accent",
507
+ background: "--conductor-paper",
508
+ bubbleUser: "--conductor-bubble-user",
509
+ bubbleAssistant: "--conductor-bubble-assistant"
510
+ };
511
+ function toCssVarName(camelCaseKey) {
512
+ if (camelCaseKey.startsWith("--")) return camelCaseKey;
513
+ return "--" + camelCaseKey.replace(/([A-Z])/g, (_, c) => `-${c.toLowerCase()}`);
514
+ }
515
+
516
+ // src/types/errors.ts
517
+ var ConductorAppError = class extends Error {
518
+ name = "ConductorAppError";
519
+ code;
520
+ status;
521
+ details;
522
+ requestId;
523
+ constructor(args) {
524
+ super(args.message, args.cause ? { cause: args.cause } : void 0);
525
+ this.code = args.code;
526
+ this.status = args.status;
527
+ this.details = args.details;
528
+ this.requestId = args.requestId;
529
+ }
530
+ };
531
+ function isConductorAppError(error) {
532
+ return typeof error === "object" && error !== null && error.name === "ConductorAppError";
533
+ }
534
+
535
+ // src/types/error-codes.ts
536
+ function extractErrorMarker(details, message) {
537
+ if (details && typeof details === "object" && "error" in details) {
538
+ const raw = details.error;
539
+ if (typeof raw === "string") return raw.toLowerCase();
540
+ }
541
+ if (message) return message.toLowerCase();
542
+ return "";
543
+ }
544
+ function mapHttpStatusToErrorCode(status, details, message) {
545
+ const marker = extractErrorMarker(details, message);
546
+ if (status === 401) return "unauthorized";
547
+ if (status === 403) return "forbidden";
548
+ if (status === 404) {
549
+ if (marker.includes("project")) return "project_not_found";
550
+ if (marker.includes("task")) return "task_not_found";
551
+ if (marker.includes("message")) return "message_not_found";
552
+ return "task_not_found";
553
+ }
554
+ if (status === 408) return "timeout";
555
+ if (status === 409) {
556
+ if (marker.includes("fire owner")) return "task_fire_owner_offline";
557
+ if (marker.includes("task_type")) return "task_type_not_messageable";
558
+ if (marker.includes("not_running") || marker.includes("not running"))
559
+ return "task_not_running";
560
+ if (marker.includes("daemon") || marker.includes("offline")) return "daemon_offline";
561
+ if (marker.includes("binding")) return "binding_validation_failed";
562
+ return "binding_validation_failed";
563
+ }
564
+ if (status === 422) return "invalid_input";
565
+ if (status === 429) return "rate_limited";
566
+ if (status >= 500) return "server_error";
567
+ if (status >= 400) return "invalid_input";
568
+ return "server_error";
569
+ }
570
+
571
+ // src/react/adapter/rest-adapter.ts
572
+ function createRestAdapter(options) {
573
+ if (!options.baseUrl) {
574
+ throw new ConductorAppError({
575
+ code: "invalid_input",
576
+ message: "createRestAdapter requires baseUrl"
577
+ });
578
+ }
579
+ const fetchImpl = options.fetch ?? globalThis.fetch;
580
+ const EventSourceCtor = options.eventSource ?? (typeof globalThis !== "undefined" ? globalThis.EventSource : void 0);
581
+ const credentials = options.credentials ?? "include";
582
+ const baseUrl = options.baseUrl.replace(/\/+$/, "");
583
+ const timeoutMs = options.timeoutMs ?? 3e4;
584
+ async function resolveAuth() {
585
+ const t = options.authToken;
586
+ if (!t) return null;
587
+ if (typeof t === "string") return t;
588
+ return await t();
589
+ }
590
+ async function buildHeaders(init = {}) {
591
+ const auth = await resolveAuth();
592
+ return {
593
+ Accept: "application/json",
594
+ ...init,
595
+ ...auth ? { Authorization: `Bearer ${auth}` } : {}
596
+ };
597
+ }
598
+ async function jsonFetch(method, path, init = {}) {
599
+ const url = buildUrl(baseUrl, path, init.query);
600
+ const headers = await buildHeaders(
601
+ init.body !== void 0 ? { "Content-Type": "application/json" } : {}
602
+ );
603
+ const composed = composeAbortSignals(init.signal, timeoutMs);
604
+ let res;
605
+ try {
606
+ res = await fetchImpl(url, {
607
+ method,
608
+ headers,
609
+ credentials,
610
+ body: init.body !== void 0 ? JSON.stringify(init.body) : void 0,
611
+ signal: composed.signal
612
+ });
613
+ } catch (cause) {
614
+ composed.dispose();
615
+ if (cause?.name === "AbortError") {
616
+ throw new ConductorAppError({
617
+ code: composed.timedOut ? "timeout" : "stream_aborted",
618
+ message: composed.timedOut ? `Request timed out after ${timeoutMs}ms` : "Request aborted",
619
+ cause
620
+ });
621
+ }
622
+ throw new ConductorAppError({
623
+ code: "network_error",
624
+ message: `Network error: ${cause?.message ?? String(cause)}`,
625
+ cause
626
+ });
627
+ }
628
+ composed.dispose();
629
+ if (!res.ok) {
630
+ const detail = await safeJson(res);
631
+ const message = detail && typeof detail === "object" && "error" in detail ? String(detail.error) : null;
632
+ throw new ConductorAppError({
633
+ code: mapHttpStatusToErrorCode(res.status, detail, message),
634
+ status: res.status,
635
+ message: message ?? `${method} ${path} failed with ${res.status}`,
636
+ details: detail
637
+ });
638
+ }
639
+ if (res.status === 204) return void 0;
640
+ const text = await res.text();
641
+ if (!text) return void 0;
642
+ return JSON.parse(text);
643
+ }
644
+ return {
645
+ async fetchHistory(taskId, opts) {
646
+ if (!taskId) {
647
+ throw new ConductorAppError({
648
+ code: "invalid_input",
649
+ message: "fetchHistory requires a taskId"
650
+ });
651
+ }
652
+ const body = await jsonFetch("GET", `/tasks/${encodeURIComponent(taskId)}/messages`, {
653
+ signal: opts?.signal,
654
+ query: {
655
+ pagination: "1",
656
+ ...opts?.beforeId ? { before_id: opts.beforeId } : {},
657
+ ...opts?.limit ? { limit: opts.limit } : {}
658
+ }
659
+ });
660
+ return {
661
+ messages: body?.messages ?? [],
662
+ hasMoreBefore: Boolean(body?.pagination?.has_more_before),
663
+ oldestMessageId: body?.pagination?.oldest_message_id ?? null
664
+ };
665
+ },
666
+ subscribe(taskId, handler) {
667
+ if (!taskId) {
668
+ throw new ConductorAppError({
669
+ code: "invalid_input",
670
+ message: "subscribe requires a taskId"
671
+ });
672
+ }
673
+ if (!EventSourceCtor) {
674
+ throw new ConductorAppError({
675
+ code: "subscribe_failed",
676
+ message: "No EventSource available. Either run in a browser or pass eventSource via createRestAdapter options."
677
+ });
678
+ }
679
+ const url = buildUrl(baseUrl, `/tasks/${encodeURIComponent(taskId)}/events`);
680
+ const es = new EventSourceCtor(url, { withCredentials: credentials === "include" });
681
+ let closed = false;
682
+ const emit = (state) => {
683
+ try {
684
+ handler({ type: "connection_state", state });
685
+ } catch {
686
+ }
687
+ };
688
+ const onOpen = () => emit("connected");
689
+ const onMessage = (e) => {
690
+ if (closed) return;
691
+ let parsed;
692
+ try {
693
+ parsed = JSON.parse(e.data);
694
+ } catch {
695
+ return;
696
+ }
697
+ if (parsed && typeof parsed === "object" && "type" in parsed) {
698
+ try {
699
+ handler(parsed);
700
+ } catch {
701
+ }
702
+ }
703
+ };
704
+ const onError = () => {
705
+ if (closed) return;
706
+ emit("reconnecting");
707
+ };
708
+ es.addEventListener("open", onOpen);
709
+ es.addEventListener("message", onMessage);
710
+ es.addEventListener("error", onError);
711
+ return {
712
+ unsubscribe() {
713
+ if (closed) return;
714
+ closed = true;
715
+ es.removeEventListener("open", onOpen);
716
+ es.removeEventListener("message", onMessage);
717
+ es.removeEventListener("error", onError);
718
+ es.close();
719
+ }
720
+ };
721
+ },
722
+ async sendMessage(taskId, input) {
723
+ if (!taskId) {
724
+ throw new ConductorAppError({
725
+ code: "invalid_input",
726
+ message: "sendMessage requires a taskId"
727
+ });
728
+ }
729
+ const body = await jsonFetch(
730
+ "POST",
731
+ `/tasks/${encodeURIComponent(taskId)}/messages`,
732
+ {
733
+ body: {
734
+ content: input.content,
735
+ ...input.role ? { role: input.role } : {},
736
+ ...input.clientRequestId ? { clientRequestId: input.clientRequestId } : {},
737
+ ...input.metadata ? { metadata: input.metadata } : {}
738
+ }
739
+ }
740
+ );
741
+ return body;
742
+ },
743
+ async interrupt(taskId, opts) {
744
+ if (!taskId) {
745
+ throw new ConductorAppError({
746
+ code: "invalid_input",
747
+ message: "interrupt requires a taskId"
748
+ });
749
+ }
750
+ await jsonFetch(
751
+ "POST",
752
+ `/tasks/${encodeURIComponent(taskId)}/interrupt`,
753
+ { body: { target_reply_to: opts.targetReplyTo } }
754
+ );
755
+ }
756
+ };
757
+ }
758
+ function buildUrl(base, path, query) {
759
+ const cleanPath = path.startsWith("/") ? path : `/${path}`;
760
+ let url = `${base}${cleanPath}`;
761
+ if (query) {
762
+ const params = new URLSearchParams();
763
+ for (const [k, v] of Object.entries(query)) {
764
+ if (v === void 0 || v === null) continue;
765
+ params.append(k, String(v));
766
+ }
767
+ const s = params.toString();
768
+ if (s) url += `?${s}`;
769
+ }
770
+ return url;
771
+ }
772
+ async function safeJson(res) {
773
+ try {
774
+ const text = await res.text();
775
+ if (!text) return null;
776
+ return JSON.parse(text);
777
+ } catch {
778
+ return null;
779
+ }
780
+ }
781
+ function composeAbortSignals(external, timeoutMs) {
782
+ const controller = new AbortController();
783
+ let timedOut = false;
784
+ const timer = timeoutMs > 0 ? setTimeout(() => {
785
+ timedOut = true;
786
+ controller.abort();
787
+ }, timeoutMs) : null;
788
+ const onExternal = () => controller.abort();
789
+ if (external) {
790
+ if (external.aborted) controller.abort();
791
+ else external.addEventListener("abort", onExternal);
792
+ }
793
+ return {
794
+ signal: controller.signal,
795
+ dispose: () => {
796
+ if (timer) clearTimeout(timer);
797
+ if (external) external.removeEventListener("abort", onExternal);
798
+ },
799
+ get timedOut() {
800
+ return timedOut;
801
+ }
802
+ };
803
+ }
804
+ export {
805
+ ChatProvider,
806
+ ChatView,
807
+ ConductorAppError,
808
+ MessageInput,
809
+ MessageList,
810
+ RuntimeStatusBar,
811
+ createRestAdapter,
812
+ isConductorAppError,
813
+ useChat
814
+ };