@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.
- package/CHANGELOG.md +135 -0
- package/README.md +173 -0
- package/dist/index.d.ts +299 -0
- package/dist/index.js +30 -0
- package/dist/react/index.d.ts +364 -0
- package/dist/react/index.js +814 -0
- package/dist/react/styles.css +264 -0
- package/dist/server/index.d.ts +376 -0
- package/dist/server/index.js +1387 -0
- package/examples/01_example/.env.example +17 -0
- package/examples/01_example/README.md +80 -0
- package/examples/01_example/chat-cli.mjs +125 -0
- package/examples/01_example/package-lock.json +52 -0
- package/examples/01_example/package.json +13 -0
- package/examples/02_bff/.env.example +16 -0
- package/examples/02_bff/README.md +63 -0
- package/examples/02_bff/app/api/conductor/[...path]/route.ts +277 -0
- package/examples/02_bff/app/api/conductor/bind/route.ts +45 -0
- package/examples/02_bff/app/layout.tsx +25 -0
- package/examples/02_bff/app/page.tsx +114 -0
- package/examples/02_bff/lib/conductor.ts +60 -0
- package/examples/02_bff/next.config.mjs +9 -0
- package/examples/02_bff/package-lock.json +1001 -0
- package/examples/02_bff/package.json +25 -0
- package/examples/02_bff/tsconfig.json +40 -0
- package/package.json +79 -0
|
@@ -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
|
+
};
|