@gendive/chatllm 0.22.0 → 0.23.1

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.
@@ -2,7 +2,7 @@
2
2
  import React17 from "react";
3
3
 
4
4
  // src/react/hooks/useChatUI.ts
5
- import { useState as useState6, useRef as useRef5, useCallback as useCallback6, useEffect as useEffect4, useMemo as useMemo3 } from "react";
5
+ import { useState as useState6, useRef as useRef7, useCallback as useCallback8, useEffect as useEffect4, useMemo as useMemo4 } from "react";
6
6
 
7
7
  // src/types.ts
8
8
  var DEFAULT_PERSONALIZATION = {
@@ -2059,6 +2059,462 @@ var parseContextRefs = (content) => {
2059
2059
  return { refs, cleanContent };
2060
2060
  };
2061
2061
 
2062
+ // src/react/utils/errors.ts
2063
+ var ChatError = class extends Error {
2064
+ code;
2065
+ retryable;
2066
+ statusCode;
2067
+ originalError;
2068
+ constructor(code, message, options) {
2069
+ super(message);
2070
+ this.name = "ChatError";
2071
+ this.code = code;
2072
+ this.statusCode = options?.statusCode;
2073
+ this.originalError = options?.originalError;
2074
+ this.retryable = options?.retryable ?? isRetryableCode(code);
2075
+ }
2076
+ };
2077
+ var classifyStatusCode = (status) => {
2078
+ if (status === 401 || status === 403) return "AUTH";
2079
+ if (status === 429) return "RATE_LIMIT";
2080
+ if (status >= 500) return "API";
2081
+ if (status >= 400) return "API";
2082
+ return "UNKNOWN";
2083
+ };
2084
+ var isRetryableCode = (code) => code === "NETWORK" || code === "RATE_LIMIT" || code === "API";
2085
+ var classifyFetchError = (error, response) => {
2086
+ if (error instanceof DOMException && error.name === "AbortError") {
2087
+ return new ChatError("ABORT", "\uC694\uCCAD\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.", {
2088
+ originalError: error,
2089
+ retryable: false
2090
+ });
2091
+ }
2092
+ if (error instanceof Error && error.name === "AbortError") {
2093
+ return new ChatError("ABORT", "\uC694\uCCAD\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.", {
2094
+ originalError: error,
2095
+ retryable: false
2096
+ });
2097
+ }
2098
+ if (error instanceof ChatError) return error;
2099
+ if (response && !response.ok) {
2100
+ const code = classifyStatusCode(response.status);
2101
+ const messages = {
2102
+ AUTH: "\uC778\uC99D\uC5D0 \uC2E4\uD328\uD588\uC2B5\uB2C8\uB2E4. API \uD0A4\uB97C \uD655\uC778\uD574\uC8FC\uC138\uC694.",
2103
+ RATE_LIMIT: "\uC694\uCCAD \uD55C\uB3C4\uB97C \uCD08\uACFC\uD588\uC2B5\uB2C8\uB2E4. \uC7A0\uC2DC \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694.",
2104
+ API: `\uC11C\uBC84 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4. (${response.status})`,
2105
+ NETWORK: "\uB124\uD2B8\uC6CC\uD06C \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.",
2106
+ TIMEOUT: "\uC694\uCCAD \uC2DC\uAC04\uC774 \uCD08\uACFC\uB418\uC5C8\uC2B5\uB2C8\uB2E4.",
2107
+ ABORT: "\uC694\uCCAD\uC774 \uCDE8\uC18C\uB418\uC5C8\uC2B5\uB2C8\uB2E4.",
2108
+ UNKNOWN: "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4."
2109
+ };
2110
+ return new ChatError(code, messages[code], {
2111
+ statusCode: response.status,
2112
+ originalError: error
2113
+ });
2114
+ }
2115
+ if (error instanceof TypeError) {
2116
+ return new ChatError("NETWORK", "\uB124\uD2B8\uC6CC\uD06C \uC5F0\uACB0\uC744 \uD655\uC778\uD574\uC8FC\uC138\uC694.", {
2117
+ originalError: error
2118
+ });
2119
+ }
2120
+ const message = error instanceof Error ? error.message : "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4.";
2121
+ return new ChatError("UNKNOWN", message, { originalError: error });
2122
+ };
2123
+ var createTimeoutError = (timeoutMs) => new ChatError("TIMEOUT", `\uC2A4\uD2B8\uB9AC\uBC0D \uC751\uB2F5\uC774 ${timeoutMs / 1e3}\uCD08 \uB3D9\uC548 \uC5C6\uC2B5\uB2C8\uB2E4.`, {
2124
+ retryable: false
2125
+ });
2126
+
2127
+ // src/react/hooks/useStreamingFetch.ts
2128
+ import { useCallback as useCallback6, useRef as useRef5 } from "react";
2129
+ var DEFAULT_CHUNK_TIMEOUT = 3e4;
2130
+ var parseSSELine = (line) => {
2131
+ if (!line.trim()) return null;
2132
+ let data = line;
2133
+ if (line.startsWith("data: ")) {
2134
+ data = line.slice(6);
2135
+ if (data === "[DONE]") return null;
2136
+ }
2137
+ try {
2138
+ const parsed = JSON.parse(data);
2139
+ let usage = null;
2140
+ if (parsed.usage) {
2141
+ usage = {
2142
+ promptTokens: parsed.usage.prompt_tokens ?? parsed.usage.promptTokens ?? 0,
2143
+ completionTokens: parsed.usage.completion_tokens ?? parsed.usage.completionTokens ?? 0,
2144
+ totalTokens: parsed.usage.total_tokens ?? parsed.usage.totalTokens ?? 0
2145
+ };
2146
+ } else if (parsed.prompt_eval_count != null || parsed.eval_count != null) {
2147
+ usage = {
2148
+ promptTokens: parsed.prompt_eval_count ?? 0,
2149
+ completionTokens: parsed.eval_count ?? 0,
2150
+ totalTokens: (parsed.prompt_eval_count ?? 0) + (parsed.eval_count ?? 0)
2151
+ };
2152
+ }
2153
+ const delta = parsed.choices?.[0]?.delta ?? null;
2154
+ const finishReason = parsed.choices?.[0]?.finish_reason ?? null;
2155
+ const content = delta?.content ?? parsed.message?.content ?? parsed.content ?? parsed.text ?? "";
2156
+ const thinking = parsed.message?.thinking ?? "";
2157
+ return { content, thinking, finishReason, delta, usage, raw: parsed };
2158
+ } catch {
2159
+ return null;
2160
+ }
2161
+ };
2162
+ var parseSSEResponse = async (response) => {
2163
+ const reader = response.body?.getReader();
2164
+ if (!reader) return "";
2165
+ const decoder = new TextDecoder();
2166
+ let buffer = "";
2167
+ let result = "";
2168
+ try {
2169
+ while (true) {
2170
+ const { done, value } = await reader.read();
2171
+ if (done) break;
2172
+ buffer += decoder.decode(value, { stream: true });
2173
+ const lines = buffer.split("\n");
2174
+ buffer = lines.pop() || "";
2175
+ for (const line of lines) {
2176
+ const chunk = parseSSELine(line);
2177
+ if (chunk?.content) result += chunk.content;
2178
+ }
2179
+ }
2180
+ if (buffer.trim()) {
2181
+ const chunk = parseSSELine(buffer);
2182
+ if (chunk?.content) result += chunk.content;
2183
+ }
2184
+ } finally {
2185
+ reader.releaseLock();
2186
+ }
2187
+ return result;
2188
+ };
2189
+ var useStreamingFetch = (options = {}) => {
2190
+ const { chunkTimeout = DEFAULT_CHUNK_TIMEOUT } = options;
2191
+ const abortControllersRef = useRef5(/* @__PURE__ */ new Map());
2192
+ const createAbortController = useCallback6((sessionId) => {
2193
+ const controller = new AbortController();
2194
+ abortControllersRef.current.set(sessionId, controller);
2195
+ return controller;
2196
+ }, []);
2197
+ const abort = useCallback6((sessionId) => {
2198
+ abortControllersRef.current.get(sessionId)?.abort();
2199
+ }, []);
2200
+ const cleanup = useCallback6((sessionId) => {
2201
+ abortControllersRef.current.delete(sessionId);
2202
+ }, []);
2203
+ const getSignal = useCallback6((sessionId) => {
2204
+ return abortControllersRef.current.get(sessionId)?.signal;
2205
+ }, []);
2206
+ const readWithTimeout = useCallback6(async (reader) => {
2207
+ let timerId;
2208
+ try {
2209
+ return await Promise.race([
2210
+ reader.read(),
2211
+ new Promise((_, reject) => {
2212
+ timerId = setTimeout(() => reject(createTimeoutError(chunkTimeout)), chunkTimeout);
2213
+ })
2214
+ ]);
2215
+ } finally {
2216
+ clearTimeout(timerId);
2217
+ }
2218
+ }, [chunkTimeout]);
2219
+ const streamResponse = useCallback6(async (response, onChunk, streamOptions) => {
2220
+ if (!response.ok) {
2221
+ throw classifyFetchError(new Error("API error"), response);
2222
+ }
2223
+ const reader = response.body?.getReader();
2224
+ if (!reader) throw new Error("No reader");
2225
+ const decoder = new TextDecoder();
2226
+ let buffer = "";
2227
+ let lastUsage = null;
2228
+ try {
2229
+ while (true) {
2230
+ const { done, value } = streamOptions?.noTimeout ? await reader.read() : await readWithTimeout(reader);
2231
+ if (done) break;
2232
+ buffer += decoder.decode(value, { stream: true });
2233
+ const lines = buffer.split("\n");
2234
+ buffer = lines.pop() || "";
2235
+ let shouldBreak = false;
2236
+ for (const line of lines) {
2237
+ const chunk = parseSSELine(line);
2238
+ if (!chunk) continue;
2239
+ if (chunk.usage) lastUsage = chunk.usage;
2240
+ const result = onChunk(chunk);
2241
+ if (result === "break") {
2242
+ shouldBreak = true;
2243
+ break;
2244
+ }
2245
+ }
2246
+ if (shouldBreak) break;
2247
+ }
2248
+ if (buffer.trim()) {
2249
+ const chunk = parseSSELine(buffer);
2250
+ if (chunk) {
2251
+ if (chunk.usage) lastUsage = chunk.usage;
2252
+ onChunk(chunk);
2253
+ }
2254
+ }
2255
+ } finally {
2256
+ reader.releaseLock();
2257
+ }
2258
+ return { usage: lastUsage };
2259
+ }, [readWithTimeout]);
2260
+ const fetchAndStream = useCallback6(async (fetchOptions, onChunk) => {
2261
+ const response = await fetch(fetchOptions.url, {
2262
+ method: "POST",
2263
+ headers: { "Content-Type": "application/json" },
2264
+ body: JSON.stringify(fetchOptions.body),
2265
+ signal: fetchOptions.signal
2266
+ });
2267
+ fetchOptions.onHeaders?.(response);
2268
+ return streamResponse(response, onChunk);
2269
+ }, [streamResponse]);
2270
+ return {
2271
+ abortControllers: abortControllersRef,
2272
+ createAbortController,
2273
+ abort,
2274
+ cleanup,
2275
+ getSignal,
2276
+ readWithTimeout,
2277
+ streamResponse,
2278
+ fetchAndStream,
2279
+ parseSSELine
2280
+ };
2281
+ };
2282
+
2283
+ // src/react/hooks/useChecklist.ts
2284
+ import { useRef as useRef6, useCallback as useCallback7, useMemo as useMemo3 } from "react";
2285
+ var CHECKLIST_STEP_DELAY_MS = 100;
2286
+ var toActiveItems = (items) => items.map((it) => ({
2287
+ id: it.id,
2288
+ title: it.title,
2289
+ skill: it.skill,
2290
+ fileIndex: it.fileIndex,
2291
+ fileType: it.fileType,
2292
+ refImage: it.refImage,
2293
+ refStep: it.refStep
2294
+ }));
2295
+ var useChecklist = ({
2296
+ sessionsRef,
2297
+ sessions,
2298
+ setSessions,
2299
+ sendMessage,
2300
+ abortControllers,
2301
+ removeLoadingSession,
2302
+ pendingAttachmentDataRef,
2303
+ trackChecklistSkip,
2304
+ resolveChecklistRefImage,
2305
+ buildChecklistStepPrompt
2306
+ }) => {
2307
+ const skipNextChecklistParsingRef = useRef6(false);
2308
+ const activeChecklistRef = useRef6(null);
2309
+ const pendingChecklistRef = useRef6(null);
2310
+ const sendMessageRef = useRef6(sendMessage);
2311
+ sendMessageRef.current = sendMessage;
2312
+ const findSessionAndMessage = useCallback7(
2313
+ (messageId) => {
2314
+ const session = sessionsRef.current?.find(
2315
+ (s) => s.messages.some((m) => m.id === messageId)
2316
+ );
2317
+ if (!session) return null;
2318
+ const message = session.messages.find((m) => m.id === messageId);
2319
+ if (!message?.checklistBlock) return null;
2320
+ return { session, message };
2321
+ },
2322
+ [sessionsRef]
2323
+ );
2324
+ const executeStep = useCallback7(
2325
+ (items, stepIndex, messageId, sessionId, isFirst) => {
2326
+ skipNextChecklistParsingRef.current = true;
2327
+ setTimeout(() => {
2328
+ const item = items[stepIndex];
2329
+ const refUrl = resolveChecklistRefImage(item, messageId, sessionId);
2330
+ if (refUrl) {
2331
+ pendingAttachmentDataRef.current = [
2332
+ { name: "ref_image", mimeType: "image/png", base64: "", size: 0, url: refUrl }
2333
+ ];
2334
+ }
2335
+ sendMessageRef.current(
2336
+ buildChecklistStepPrompt(stepIndex, items.length, item, isFirst, refUrl),
2337
+ { hiddenUserMessage: true, isChecklistExecution: true, checklistStep: { index: stepIndex, title: item.title } }
2338
+ );
2339
+ }, CHECKLIST_STEP_DELAY_MS);
2340
+ },
2341
+ [resolveChecklistRefImage, buildChecklistStepPrompt, pendingAttachmentDataRef]
2342
+ );
2343
+ const updateChecklistItems = useCallback7(
2344
+ (sessionId, messageId, updater) => {
2345
+ setSessions(
2346
+ (prev) => prev.map((s) => {
2347
+ if (s.id !== sessionId) return s;
2348
+ return {
2349
+ ...s,
2350
+ messages: s.messages.map((m) => {
2351
+ if (m.id !== messageId || !m.checklistBlock) return m;
2352
+ const result = updater(m.checklistBlock.items, m.checklistBlock);
2353
+ return {
2354
+ ...m,
2355
+ checklistBlock: {
2356
+ ...m.checklistBlock,
2357
+ items: result.items,
2358
+ ...result.currentStep !== void 0 ? { currentStep: result.currentStep } : {},
2359
+ ...result.completed !== void 0 ? { completed: result.completed } : {}
2360
+ }
2361
+ };
2362
+ })
2363
+ };
2364
+ })
2365
+ );
2366
+ },
2367
+ [setSessions]
2368
+ );
2369
+ const handleChecklistStart = useCallback7(
2370
+ (messageId) => {
2371
+ const found = findSessionAndMessage(messageId);
2372
+ if (!found) return;
2373
+ const { session, message } = found;
2374
+ pendingChecklistRef.current = null;
2375
+ activeChecklistRef.current = {
2376
+ messageId,
2377
+ sessionId: session.id,
2378
+ items: toActiveItems(message.checklistBlock.items),
2379
+ currentStep: 0,
2380
+ stepResults: []
2381
+ };
2382
+ updateChecklistItems(session.id, messageId, (items) => ({
2383
+ items: items.map((it, idx) => ({
2384
+ ...it,
2385
+ status: idx === 0 ? "in_progress" : it.status
2386
+ })),
2387
+ currentStep: 0
2388
+ }));
2389
+ executeStep(message.checklistBlock.items, 0, messageId, session.id, true);
2390
+ },
2391
+ [findSessionAndMessage, updateChecklistItems, executeStep]
2392
+ );
2393
+ const handleChecklistAbort = useCallback7(() => {
2394
+ if (!activeChecklistRef.current) return;
2395
+ const checklist = activeChecklistRef.current;
2396
+ const stepIdx = checklist.currentStep;
2397
+ abortControllers.current?.get(checklist.sessionId)?.abort();
2398
+ updateChecklistItems(checklist.sessionId, checklist.messageId, (items) => ({
2399
+ items: items.map((it, idx) => ({
2400
+ ...it,
2401
+ status: idx === stepIdx ? "error" : it.status
2402
+ }))
2403
+ }));
2404
+ activeChecklistRef.current = null;
2405
+ removeLoadingSession(checklist.sessionId);
2406
+ }, [abortControllers, updateChecklistItems, removeLoadingSession]);
2407
+ const handleChecklistRetry = useCallback7(
2408
+ (messageId, stepIndex) => {
2409
+ const found = findSessionAndMessage(messageId);
2410
+ if (!found) return;
2411
+ const { session, message } = found;
2412
+ activeChecklistRef.current = {
2413
+ messageId,
2414
+ sessionId: session.id,
2415
+ items: toActiveItems(message.checklistBlock.items),
2416
+ currentStep: stepIndex,
2417
+ stepResults: message.checklistBlock.items.slice(0, stepIndex).map((it) => it.result || "")
2418
+ };
2419
+ updateChecklistItems(session.id, messageId, (items) => ({
2420
+ items: items.map((it, idx) => ({
2421
+ ...it,
2422
+ status: idx === stepIndex ? "in_progress" : idx > stepIndex ? "pending" : it.status
2423
+ })),
2424
+ currentStep: stepIndex
2425
+ }));
2426
+ executeStep(message.checklistBlock.items, stepIndex, messageId, session.id, stepIndex === 0);
2427
+ },
2428
+ [findSessionAndMessage, updateChecklistItems, executeStep]
2429
+ );
2430
+ const handleChecklistSkip = useCallback7(
2431
+ (messageId, stepIndex) => {
2432
+ const found = findSessionAndMessage(messageId);
2433
+ if (!found) return;
2434
+ const { session, message } = found;
2435
+ trackChecklistSkip();
2436
+ updateChecklistItems(session.id, messageId, (items) => ({
2437
+ items: items.map((it, idx) => ({
2438
+ ...it,
2439
+ status: idx === stepIndex ? "done" : it.status,
2440
+ result: idx === stepIndex ? "(\uAC74\uB108\uB700)" : it.result
2441
+ }))
2442
+ }));
2443
+ const nextPending = message.checklistBlock.items.findIndex(
2444
+ (it, idx) => idx > stepIndex && (it.status === "pending" || it.status === "error")
2445
+ );
2446
+ if (nextPending >= 0) {
2447
+ activeChecklistRef.current = {
2448
+ messageId,
2449
+ sessionId: session.id,
2450
+ items: toActiveItems(message.checklistBlock.items),
2451
+ currentStep: nextPending,
2452
+ stepResults: message.checklistBlock.items.slice(0, nextPending).map((it) => it.result || "(\uAC74\uB108\uB700)")
2453
+ };
2454
+ updateChecklistItems(session.id, messageId, (items) => ({
2455
+ items: items.map((it, idx) => ({
2456
+ ...it,
2457
+ status: idx === nextPending ? "in_progress" : it.status
2458
+ })),
2459
+ currentStep: nextPending
2460
+ }));
2461
+ executeStep(message.checklistBlock.items, nextPending, messageId, session.id, false);
2462
+ } else {
2463
+ const allResults = message.checklistBlock.items.map((it, i) => {
2464
+ const result = i === stepIndex ? "(\uAC74\uB108\uB700)" : it.result || "(\uAC74\uB108\uB700)";
2465
+ return `### ${i + 1}. ${it.title}
2466
+ ${result}`;
2467
+ }).join("\n\n");
2468
+ updateChecklistItems(session.id, messageId, () => ({
2469
+ items: message.checklistBlock.items.map((it, idx) => ({
2470
+ ...it,
2471
+ status: idx === stepIndex ? "done" : it.status,
2472
+ result: idx === stepIndex ? "(\uAC74\uB108\uB700)" : it.result
2473
+ })),
2474
+ completed: true
2475
+ }));
2476
+ skipNextChecklistParsingRef.current = true;
2477
+ setTimeout(() => {
2478
+ sendMessageRef.current(
2479
+ `\uBAA8\uB4E0 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8 \uB2E8\uACC4\uAC00 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC544\uB798\uB294 \uAC01 \uB2E8\uACC4\uBCC4 \uACB0\uACFC\uC785\uB2C8\uB2E4:
2480
+
2481
+ ${allResults}
2482
+
2483
+ \uC704 \uACB0\uACFC\uB97C \uC885\uD569\uD558\uC5EC \uCD5C\uC885 \uACB0\uACFC\uBB3C\uC744 \uC644\uC131\uD574\uC8FC\uC138\uC694. checklist \uD0DC\uADF8\uB294 \uC0AC\uC6A9\uD558\uC9C0 \uB9C8\uC138\uC694.`,
2484
+ { hiddenUserMessage: true, isChecklistExecution: true }
2485
+ );
2486
+ }, CHECKLIST_STEP_DELAY_MS);
2487
+ }
2488
+ },
2489
+ [findSessionAndMessage, updateChecklistItems, executeStep, trackChecklistSkip]
2490
+ );
2491
+ const activeChecklistMessage = useMemo3(() => {
2492
+ const active = activeChecklistRef.current;
2493
+ if (!active) {
2494
+ for (const session2 of sessions) {
2495
+ const msg = session2.messages.find(
2496
+ (m) => m.checklistBlock && !m.checklistBlock.completed
2497
+ );
2498
+ if (msg) return msg;
2499
+ }
2500
+ return null;
2501
+ }
2502
+ const session = sessions.find((s) => s.id === active.sessionId);
2503
+ if (!session) return null;
2504
+ return session.messages.find((m) => m.id === active.messageId) || null;
2505
+ }, [sessions]);
2506
+ return {
2507
+ activeChecklistRef,
2508
+ pendingChecklistRef,
2509
+ skipNextChecklistParsingRef,
2510
+ handleChecklistStart,
2511
+ handleChecklistAbort,
2512
+ handleChecklistRetry,
2513
+ handleChecklistSkip,
2514
+ activeChecklistMessage
2515
+ };
2516
+ };
2517
+
2062
2518
  // src/react/utils/sessionCache.ts
2063
2519
  var buildCacheKey = (storageKey, sessionId) => `${storageKey}_cache_${sessionId}`;
2064
2520
  var writeSessionCache = (storageKey, session) => {
@@ -2091,38 +2547,14 @@ var removeSessionCache = (storageKey, sessionId) => {
2091
2547
  };
2092
2548
 
2093
2549
  // src/react/hooks/useChatUI.ts
2094
- var parseSSEResponse = async (response) => {
2095
- const reader = response.body?.getReader();
2096
- if (!reader) return "";
2097
- const decoder = new TextDecoder();
2098
- let buffer = "";
2099
- let result = "";
2100
- while (true) {
2101
- const { done, value } = await reader.read();
2102
- if (done) break;
2103
- buffer += decoder.decode(value, { stream: true });
2104
- const lines = buffer.split("\n");
2105
- buffer = lines.pop() || "";
2106
- for (const line of lines) {
2107
- if (line.startsWith("data: ")) {
2108
- const data = line.slice(6);
2109
- if (data === "[DONE]") continue;
2110
- try {
2111
- const parsed = JSON.parse(data);
2112
- const chunk = parsed.content ?? parsed.text ?? "";
2113
- if (chunk) result += chunk;
2114
- } catch {
2115
- }
2116
- }
2117
- }
2118
- }
2119
- return result;
2120
- };
2121
2550
  var DEFAULT_STORAGE_KEY2 = "chatllm_sessions";
2122
2551
  var DEFAULT_COMPRESSION_THRESHOLD = 20;
2123
2552
  var DEFAULT_KEEP_RECENT = 6;
2124
2553
  var DEFAULT_RECOMPRESSION_THRESHOLD = 10;
2125
2554
  var DEFAULT_TOKEN_LIMIT = 8e3;
2555
+ var DEFAULT_STREAM_CHUNK_TIMEOUT = 3e4;
2556
+ var DEFAULT_MAX_TOOL_CALL_DEPTH = 5;
2557
+ var DEFAULT_MAX_TOOL_RESULT_SIZE = 1e4;
2126
2558
  var DEFAULT_SESSION_CONTEXT_MAX_CHARS = 4e3;
2127
2559
  var DEFAULT_SESSION_CONTEXT_MAX_ITEMS = 10;
2128
2560
  var SESSION_CONTEXT_ITEM_MAX_CHARS = 500;
@@ -2184,15 +2616,22 @@ var convertAttachmentsToBase64 = async (attachments) => Promise.all(
2184
2616
  size: att.size
2185
2617
  }))
2186
2618
  );
2187
- var convertAttachmentsWithUploader = async (attachments, uploader) => Promise.all(
2188
- attachments.map(async (att) => ({
2189
- name: att.name,
2190
- mimeType: att.mimeType,
2191
- base64: "",
2192
- url: await uploader(att.file),
2193
- size: att.size,
2194
- source: "uploader"
2195
- }))
2619
+ var convertAttachmentsWithUploaderCached = async (attachments, uploader, cache) => Promise.all(
2620
+ attachments.map(async (att) => {
2621
+ let url = cache.get(att.id);
2622
+ if (!url) {
2623
+ url = await uploader(att.file);
2624
+ cache.set(att.id, url);
2625
+ }
2626
+ return {
2627
+ name: att.name,
2628
+ mimeType: att.mimeType,
2629
+ base64: "",
2630
+ url,
2631
+ size: att.size,
2632
+ source: "uploader"
2633
+ };
2634
+ })
2196
2635
  );
2197
2636
  var findPreviousResultImage = (messages) => {
2198
2637
  for (let i = messages.length - 1; i >= 0; i--) {
@@ -2242,6 +2681,12 @@ var useChatUI = (options) => {
2242
2681
  onSendMessage,
2243
2682
  onSessionChange,
2244
2683
  onError,
2684
+ streamChunkTimeout = DEFAULT_STREAM_CHUNK_TIMEOUT,
2685
+ retry: retryConfig,
2686
+ onAbort,
2687
+ onTokenUsage,
2688
+ maxToolCallDepth = DEFAULT_MAX_TOOL_CALL_DEPTH,
2689
+ maxToolResultSize = DEFAULT_MAX_TOOL_RESULT_SIZE,
2245
2690
  onTitleChange,
2246
2691
  generateTitle: generateTitleCallback,
2247
2692
  // Memory options
@@ -2308,8 +2753,8 @@ var useChatUI = (options) => {
2308
2753
  const [input, setInput] = useState6("");
2309
2754
  const [loadingSessionIds, setLoadingSessionIds] = useState6(/* @__PURE__ */ new Set());
2310
2755
  const isLoading = currentSessionId !== null && loadingSessionIds.has(currentSessionId);
2311
- const addLoadingSession = useCallback6((id) => setLoadingSessionIds((prev) => new Set(prev).add(id)), []);
2312
- const removeLoadingSession = useCallback6((id) => setLoadingSessionIds((prev) => {
2756
+ const addLoadingSession = useCallback8((id) => setLoadingSessionIds((prev) => new Set(prev).add(id)), []);
2757
+ const removeLoadingSession = useCallback8((id) => setLoadingSessionIds((prev) => {
2313
2758
  const next = new Set(prev);
2314
2759
  next.delete(id);
2315
2760
  return next;
@@ -2336,41 +2781,45 @@ var useChatUI = (options) => {
2336
2781
  const [deepResearchProgress, setDeepResearchProgress] = useState6(
2337
2782
  null
2338
2783
  );
2339
- const sessionsRef = useRef5(sessions);
2784
+ const sessionsRef = useRef7(sessions);
2340
2785
  useEffect4(() => {
2341
2786
  sessionsRef.current = sessions;
2342
2787
  }, [sessions]);
2343
- const modelsRef = useRef5(models);
2788
+ const modelsRef = useRef7(models);
2344
2789
  useEffect4(() => {
2345
2790
  modelsRef.current = models;
2346
2791
  }, [models]);
2347
- const onSendMessageRef = useRef5(onSendMessage);
2348
- const onResponseHeadersRef = useRef5(options.onResponseHeaders);
2349
- const onSessionChangeRef = useRef5(onSessionChange);
2350
- const onErrorRef = useRef5(onError);
2351
- const onTitleChangeRef = useRef5(onTitleChange);
2352
- const generateTitleRef = useRef5(generateTitleCallback);
2353
- const onPersonalizationChangeRef = useRef5(options.onPersonalizationChange);
2354
- const onPersonalizationSaveRef = useRef5(options.onPersonalizationSave);
2355
- const onLoadSessionsRef = useRef5(onLoadSessions);
2356
- const onCreateSessionRef = useRef5(onCreateSession);
2357
- const onLoadSessionRef = useRef5(onLoadSession);
2358
- const onDeleteSessionCallbackRef = useRef5(onDeleteSessionCallback);
2359
- const onUpdateSessionTitleRef = useRef5(onUpdateSessionTitle);
2360
- const onSaveMessagesRef = useRef5(onSaveMessages);
2361
- const onToolCallRef = useRef5(onToolCall);
2362
- const onSkillCompleteRef = useRef5(onSkillComplete);
2363
- const onSessionContextChangeRef = useRef5(onSessionContextChange);
2364
- const onLoadModelsRef = useRef5(onLoadModels);
2365
- const onUploadImageRef = useRef5(onUploadImage);
2366
- const onImageErrorRef = useRef5(onImageError);
2367
- const fileUploaderRef = useRef5(fileUploader);
2368
- const globalMemoryRef = useRef5(null);
2792
+ const onSendMessageRef = useRef7(onSendMessage);
2793
+ const onResponseHeadersRef = useRef7(options.onResponseHeaders);
2794
+ const onSessionChangeRef = useRef7(onSessionChange);
2795
+ const onErrorRef = useRef7(onError);
2796
+ const onAbortRef = useRef7(onAbort);
2797
+ const onTokenUsageRef = useRef7(onTokenUsage);
2798
+ const onTitleChangeRef = useRef7(onTitleChange);
2799
+ const generateTitleRef = useRef7(generateTitleCallback);
2800
+ const onPersonalizationChangeRef = useRef7(options.onPersonalizationChange);
2801
+ const onPersonalizationSaveRef = useRef7(options.onPersonalizationSave);
2802
+ const onLoadSessionsRef = useRef7(onLoadSessions);
2803
+ const onCreateSessionRef = useRef7(onCreateSession);
2804
+ const onLoadSessionRef = useRef7(onLoadSession);
2805
+ const onDeleteSessionCallbackRef = useRef7(onDeleteSessionCallback);
2806
+ const onUpdateSessionTitleRef = useRef7(onUpdateSessionTitle);
2807
+ const onSaveMessagesRef = useRef7(onSaveMessages);
2808
+ const onToolCallRef = useRef7(onToolCall);
2809
+ const onSkillCompleteRef = useRef7(onSkillComplete);
2810
+ const onSessionContextChangeRef = useRef7(onSessionContextChange);
2811
+ const onLoadModelsRef = useRef7(onLoadModels);
2812
+ const onUploadImageRef = useRef7(onUploadImage);
2813
+ const onImageErrorRef = useRef7(onImageError);
2814
+ const fileUploaderRef = useRef7(fileUploader);
2815
+ const globalMemoryRef = useRef7(null);
2369
2816
  useEffect4(() => {
2370
2817
  onSendMessageRef.current = onSendMessage;
2371
2818
  onResponseHeadersRef.current = options.onResponseHeaders;
2372
2819
  onSessionChangeRef.current = onSessionChange;
2373
2820
  onErrorRef.current = onError;
2821
+ onAbortRef.current = onAbort;
2822
+ onTokenUsageRef.current = onTokenUsage;
2374
2823
  onTitleChangeRef.current = onTitleChange;
2375
2824
  generateTitleRef.current = generateTitleCallback;
2376
2825
  onPersonalizationChangeRef.current = options.onPersonalizationChange;
@@ -2389,15 +2838,19 @@ var useChatUI = (options) => {
2389
2838
  fileUploaderRef.current = fileUploader;
2390
2839
  onLoadModelsRef.current = onLoadModels;
2391
2840
  });
2392
- const abortControllersRef = useRef5(/* @__PURE__ */ new Map());
2393
- const pendingInitialLoadRef = useRef5(null);
2394
- const skipNextPollParsingRef = useRef5(false);
2395
- const skipNextSkillParsingRef = useRef5(false);
2396
- const skipNextChecklistParsingRef = useRef5(false);
2397
- const activeChecklistRef = useRef5(null);
2398
- const pendingChecklistRef = useRef5(null);
2399
- const pendingAttachmentDataRef = useRef5(null);
2400
- const lastExtractionMsgCountRef = useRef5(0);
2841
+ const abortControllersRef = useRef7(/* @__PURE__ */ new Map());
2842
+ const { streamResponse } = useStreamingFetch({ chunkTimeout: streamChunkTimeout });
2843
+ const pendingInitialLoadRef = useRef7(null);
2844
+ const skipNextPollParsingRef = useRef7(false);
2845
+ const toolCallDepthRef = useRef7(0);
2846
+ const skipNextSkillParsingRef = useRef7(false);
2847
+ const sendMessageForChecklistRef = useRef7(
2848
+ async () => {
2849
+ console.warn("[ChatUI] sendMessageForChecklistRef called before sendMessage initialized");
2850
+ }
2851
+ );
2852
+ const pendingAttachmentDataRef = useRef7(null);
2853
+ const lastExtractionMsgCountRef = useRef7(0);
2401
2854
  const resolveChecklistRefImage = (item, checklistMessageId, sessionId) => {
2402
2855
  const session = sessionsRef.current.find((s) => s.id === sessionId);
2403
2856
  if (!session) return null;
@@ -2464,7 +2917,7 @@ ${hints.join(" ")}` : "";
2464
2917
  return `${stepLabel}
2465
2918
  ${focusHint}${hintLine} checklist \uD0DC\uADF8\uB294 \uC0AC\uC6A9\uD558\uC9C0 \uB9C8\uC138\uC694.`;
2466
2919
  };
2467
- const memoryOptions = useMemo3(
2920
+ const memoryOptions = useMemo4(
2468
2921
  () => ({
2469
2922
  storageType: globalMemoryConfig?.storageType || "localStorage",
2470
2923
  storageKey: globalMemoryConfig?.localStorage?.key || `${storageKey}_memory`,
@@ -2478,11 +2931,11 @@ ${focusHint}${hintLine} checklist \uD0DC\uADF8\uB294 \uC0AC\uC6A9\uD558\uC9C0 \u
2478
2931
  const globalMemoryRaw = useGlobalMemory(memoryOptions);
2479
2932
  const globalMemory = useGlobalMemoryEnabled ? globalMemoryRaw : null;
2480
2933
  globalMemoryRef.current = globalMemory;
2481
- const stableToolCall = useCallback6(
2934
+ const stableToolCall = useCallback8(
2482
2935
  (name, params) => onToolCallRef.current(name, params),
2483
2936
  []
2484
2937
  );
2485
- const mergedSkills = useMemo3(() => {
2938
+ const mergedSkills = useMemo4(() => {
2486
2939
  if (!tools || !onToolCall) return skills || {};
2487
2940
  const toolSkills = convertToolsToSkills(tools, stableToolCall);
2488
2941
  return { ...skills || {}, ...toolSkills };
@@ -2520,7 +2973,7 @@ ${focusHint}${hintLine} checklist \uD0DC\uADF8\uB294 \uC0AC\uC6A9\uD558\uC9C0 \u
2520
2973
  });
2521
2974
  const [projectSettingsOpen, setProjectSettingsOpen] = useState6(false);
2522
2975
  const projectMemoryKey = enableProjects && projectHook.currentProjectId ? `${storageKey}_project_memory_${projectHook.currentProjectId}` : `${storageKey}_project_memory_none`;
2523
- const projectMemoryOptions = useMemo3(
2976
+ const projectMemoryOptions = useMemo4(
2524
2977
  () => ({
2525
2978
  storageType: globalMemoryConfig?.storageType || "localStorage",
2526
2979
  storageKey: projectMemoryKey,
@@ -2532,7 +2985,7 @@ ${focusHint}${hintLine} checklist \uD0DC\uADF8\uB294 \uC0AC\uC6A9\uD558\uC9C0 \u
2532
2985
  );
2533
2986
  const projectMemoryRaw = useGlobalMemory(projectMemoryOptions);
2534
2987
  const projectMemory = enableProjects ? projectMemoryRaw : null;
2535
- const unwrapResponseHeaders = useCallback6((result) => {
2988
+ const unwrapResponseHeaders = useCallback8((result) => {
2536
2989
  if (typeof result === "object" && result !== null && "response" in result && "headers" in result) {
2537
2990
  const wrapped = result;
2538
2991
  onResponseHeadersRef.current?.(wrapped.headers);
@@ -2540,7 +2993,7 @@ ${focusHint}${hintLine} checklist \uD0DC\uADF8\uB294 \uC0AC\uC6A9\uD558\uC9C0 \u
2540
2993
  }
2541
2994
  return result;
2542
2995
  }, []);
2543
- const emitFetchHeaders = useCallback6((response) => {
2996
+ const emitFetchHeaders = useCallback8((response) => {
2544
2997
  if (onResponseHeadersRef.current && response.headers) {
2545
2998
  const headerMap = {};
2546
2999
  response.headers.forEach((v, k) => {
@@ -2549,7 +3002,7 @@ ${focusHint}${hintLine} checklist \uD0DC\uADF8\uB294 \uC0AC\uC6A9\uD558\uC9C0 \u
2549
3002
  onResponseHeadersRef.current(headerMap);
2550
3003
  }
2551
3004
  }, []);
2552
- const callInternalLLM = useCallback6(async (prompt, model) => {
3005
+ const callInternalLLM = useCallback8(async (prompt, model) => {
2553
3006
  if (onSendMessageRef.current) {
2554
3007
  const modelConfig = modelsRef.current.find((m) => m.id === model);
2555
3008
  const provider = modelConfig?.provider || "ollama";
@@ -2587,7 +3040,7 @@ ${focusHint}${hintLine} checklist \uD0DC\uADF8\uB294 \uC0AC\uC6A9\uD558\uC9C0 \u
2587
3040
  const currentSession = sessions.find((s) => s.id === currentSessionId) || null;
2588
3041
  const messages = currentSession?.messages.filter((m) => !m.hidden) || [];
2589
3042
  const compressionState = currentSession?.compressionState || null;
2590
- const visibleSessions = useMemo3(() => {
3043
+ const visibleSessions = useMemo4(() => {
2591
3044
  if (!enableProjects || !projectHook.currentProjectId) return sessions;
2592
3045
  return sessions.filter((s) => s.projectId === projectHook.currentProjectId);
2593
3046
  }, [sessions, enableProjects, projectHook.currentProjectId]);
@@ -2682,7 +3135,7 @@ ${focusHint}${hintLine} checklist \uD0DC\uADF8\uB294 \uC0AC\uC6A9\uD558\uC9C0 \u
2682
3135
  useEffect4(() => {
2683
3136
  onSessionChangeRef.current?.(currentSession);
2684
3137
  }, [currentSession]);
2685
- const buildSystemPrompt = useCallback6((session) => {
3138
+ const buildSystemPrompt = useCallback8((session) => {
2686
3139
  const parts = [];
2687
3140
  const { userProfile, responseStyle, language } = personalization;
2688
3141
  const identityName = assistantName || "AI \uC5B4\uC2DC\uC2A4\uD134\uD2B8";
@@ -3041,7 +3494,7 @@ AI (\uD655\uC815):
3041
3494
  }
3042
3495
  return parts.length > 0 ? parts.join("\n") : "";
3043
3496
  }, [personalization, globalMemory, useGlobalMemoryEnabled, enablePoll, enableChecklist, buildSkillsPrompt, enableProjects, projectHook.currentProject, projectMemory, resolvedSkills, assistantName, onBuildSystemPrompt, compactSystemPrompt, selectedModel]);
3044
- const promoteToSessionContext = useCallback6((sessionId, skillName, content, metadata, label) => {
3497
+ const promoteToSessionContext = useCallback8((sessionId, skillName, content, metadata, label) => {
3045
3498
  const item = createSessionContextItem(skillName, content, metadata, label);
3046
3499
  setSessions(
3047
3500
  (prev) => prev.map((s) => {
@@ -3052,7 +3505,7 @@ AI (\uD655\uC815):
3052
3505
  })
3053
3506
  );
3054
3507
  }, []);
3055
- const compressContext = useCallback6(async (messagesToCompress, model) => {
3508
+ const compressContext = useCallback8(async (messagesToCompress, model) => {
3056
3509
  const conversationText = messagesToCompress.map((m) => `${m.role === "user" ? "\uC0AC\uC6A9\uC790" : "AI"}: ${m.content}`).join("\n\n");
3057
3510
  const summaryPrompt = `\uB2E4\uC74C \uB300\uD654 \uB0B4\uC6A9\uC744 \uD575\uC2EC \uC815\uBCF4\uB9CC \uC720\uC9C0\uD558\uBA74\uC11C \uAC04\uACB0\uD558\uAC8C \uC694\uC57D\uD574\uC8FC\uC138\uC694.
3058
3511
  \uC911\uC694\uD55C \uACB0\uC815\uC0AC\uD56D, \uC0AC\uC6A9\uC790 \uC694\uAD6C\uC0AC\uD56D, \uB9E5\uB77D \uC815\uBCF4\uB97C \uBCF4\uC874\uD558\uC138\uC694.
@@ -3067,7 +3520,7 @@ ${conversationText}
3067
3520
  return "";
3068
3521
  }
3069
3522
  }, [callInternalLLM]);
3070
- const incrementalCompressContext = useCallback6(
3523
+ const incrementalCompressContext = useCallback8(
3071
3524
  async (existingSummary, newMessages, model) => {
3072
3525
  const newConversation = newMessages.map((m) => `${m.role === "user" ? "\uC0AC\uC6A9\uC790" : "AI"}: ${m.content}`).join("\n\n");
3073
3526
  const mergePrompt = `\uAE30\uC874 \uB300\uD654 \uC694\uC57D\uACFC \uC0C8\uB85C\uC6B4 \uB300\uD654 \uB0B4\uC6A9\uC744 \uD1B5\uD569\uD558\uC5EC \uD558\uB098\uC758 \uC694\uC57D\uC73C\uB85C \uB9CC\uB4E4\uC5B4\uC8FC\uC138\uC694.
@@ -3094,10 +3547,10 @@ ${newConversation}
3094
3547
  },
3095
3548
  [callInternalLLM]
3096
3549
  );
3097
- const estimateTokens = useCallback6((messages2) => {
3550
+ const estimateTokens = useCallback8((messages2) => {
3098
3551
  return messages2.reduce((sum, m) => sum + Math.ceil(m.content.length / 4), 0);
3099
3552
  }, []);
3100
- const newSession = useCallback6(async () => {
3553
+ const newSession = useCallback8(async () => {
3101
3554
  const projectId = enableProjects ? projectHook.currentProjectId || DEFAULT_PROJECT_ID : void 0;
3102
3555
  if (useExternalStorage && onCreateSessionRef.current) {
3103
3556
  setIsSessionLoading(true);
@@ -3135,7 +3588,7 @@ ${newConversation}
3135
3588
  setSessions((prev) => [newSess, ...prev]);
3136
3589
  setCurrentSessionId(newSess.id);
3137
3590
  }, [selectedModel, useExternalStorage, enableProjects, projectHook.currentProjectId]);
3138
- const selectSession = useCallback6(async (id) => {
3591
+ const selectSession = useCallback8(async (id) => {
3139
3592
  if (useExternalStorage && onLoadSessionRef.current) {
3140
3593
  setIsSessionLoading(true);
3141
3594
  try {
@@ -3245,7 +3698,7 @@ ${newConversation}
3245
3698
  pendingInitialLoadRef.current = null;
3246
3699
  selectSession(id);
3247
3700
  }, [currentSessionId, selectSession, useExternalStorage]);
3248
- const deleteSession = useCallback6(async (id) => {
3701
+ const deleteSession = useCallback8(async (id) => {
3249
3702
  if (useExternalStorage && onDeleteSessionCallbackRef.current) {
3250
3703
  try {
3251
3704
  await onDeleteSessionCallbackRef.current(id);
@@ -3273,7 +3726,7 @@ ${newConversation}
3273
3726
  return filtered;
3274
3727
  });
3275
3728
  }, [currentSessionId, storageKey, useExternalStorage]);
3276
- const renameSession = useCallback6(async (id, newTitle) => {
3729
+ const renameSession = useCallback8(async (id, newTitle) => {
3277
3730
  if (!newTitle.trim()) return;
3278
3731
  if (useExternalStorage && onUpdateSessionTitleRef.current) {
3279
3732
  try {
@@ -3296,7 +3749,7 @@ ${newConversation}
3296
3749
  );
3297
3750
  onTitleChangeRef.current?.(id, newTitle.trim());
3298
3751
  }, [useExternalStorage]);
3299
- const setModel = useCallback6((model) => {
3752
+ const setModel = useCallback8((model) => {
3300
3753
  setSelectedModel(model);
3301
3754
  if (currentSessionId) {
3302
3755
  setSessions(
@@ -3304,10 +3757,10 @@ ${newConversation}
3304
3757
  );
3305
3758
  }
3306
3759
  }, [currentSessionId]);
3307
- const toggleSidebar = useCallback6(() => setSidebarOpen((prev) => !prev), []);
3308
- const openSettings = useCallback6(() => setSettingsOpen(true), []);
3309
- const closeSettings = useCallback6(() => setSettingsOpen(false), []);
3310
- const copyMessage = useCallback6((content, id) => {
3760
+ const toggleSidebar = useCallback8(() => setSidebarOpen((prev) => !prev), []);
3761
+ const openSettings = useCallback8(() => setSettingsOpen(true), []);
3762
+ const closeSettings = useCallback8(() => setSettingsOpen(false), []);
3763
+ const copyMessage = useCallback8((content, id) => {
3311
3764
  if (typeof navigator !== "undefined") {
3312
3765
  navigator.clipboard.writeText(content).then(() => {
3313
3766
  setCopiedMessageId(id);
@@ -3315,30 +3768,30 @@ ${newConversation}
3315
3768
  });
3316
3769
  }
3317
3770
  }, []);
3318
- const startEdit = useCallback6((message) => {
3771
+ const startEdit = useCallback8((message) => {
3319
3772
  if (message.role === "user") {
3320
3773
  setEditingMessageId(message.id);
3321
3774
  }
3322
3775
  }, []);
3323
- const cancelEdit = useCallback6(() => {
3776
+ const cancelEdit = useCallback8(() => {
3324
3777
  setEditingMessageId(null);
3325
3778
  }, []);
3326
- const stopGeneration = useCallback6(() => {
3779
+ const stopGeneration = useCallback8(() => {
3327
3780
  if (currentSessionId) {
3328
3781
  abortControllersRef.current.get(currentSessionId)?.abort();
3329
3782
  }
3330
3783
  }, [currentSessionId]);
3331
- const updatePersonalization = useCallback6((config) => {
3784
+ const updatePersonalization = useCallback8((config) => {
3332
3785
  setPersonalization((prev) => {
3333
3786
  const next = { ...prev, ...config };
3334
3787
  onPersonalizationChangeRef.current?.(next);
3335
3788
  return next;
3336
3789
  });
3337
3790
  }, []);
3338
- const savePersonalization = useCallback6(() => {
3791
+ const savePersonalization = useCallback8(() => {
3339
3792
  onPersonalizationSaveRef.current?.(personalization);
3340
3793
  }, [personalization]);
3341
- const addAttachments = useCallback6((files) => {
3794
+ const addAttachments = useCallback8((files) => {
3342
3795
  const newAttachments = files.map((file) => {
3343
3796
  const isImage = file.type.startsWith("image/");
3344
3797
  return {
@@ -3353,7 +3806,7 @@ ${newConversation}
3353
3806
  });
3354
3807
  setAttachments((prev) => [...prev, ...newAttachments]);
3355
3808
  }, []);
3356
- const removeAttachment = useCallback6((id) => {
3809
+ const removeAttachment = useCallback8((id) => {
3357
3810
  setAttachments((prev) => {
3358
3811
  const target = prev.find((a) => a.id === id);
3359
3812
  if (target?.previewUrl) {
@@ -3362,17 +3815,17 @@ ${newConversation}
3362
3815
  return prev.filter((a) => a.id !== id);
3363
3816
  });
3364
3817
  }, []);
3365
- const toggleDeepResearchMode = useCallback6(() => {
3818
+ const toggleDeepResearchMode = useCallback8(() => {
3366
3819
  setIsDeepResearchMode((prev) => !prev);
3367
3820
  }, []);
3368
- const trackBehavior = useCallback6(async (key, updater) => {
3821
+ const trackBehavior = useCallback8(async (key, updater) => {
3369
3822
  if (!globalMemory) return;
3370
3823
  const fullKey = `behavior.${key}`;
3371
3824
  const prev = globalMemory.get(fullKey) ?? {};
3372
3825
  const next = updater(prev);
3373
3826
  await globalMemory.set(fullKey, next, { category: "behavior" });
3374
3827
  }, [globalMemory]);
3375
- const trackMessageLength = useCallback6((length) => {
3828
+ const trackMessageLength = useCallback8((length) => {
3376
3829
  trackBehavior("messageLength", (prev) => {
3377
3830
  const samples = (prev.messageLengthSamples || []).slice(-19);
3378
3831
  samples.push(length);
@@ -3380,39 +3833,69 @@ ${newConversation}
3380
3833
  return { ...prev, messageLengthSamples: samples, avgMessageLength: avg };
3381
3834
  });
3382
3835
  }, [trackBehavior]);
3383
- const trackSkillUsage = useCallback6((skillName) => {
3836
+ const trackSkillUsage = useCallback8((skillName) => {
3384
3837
  trackBehavior("skillUsage", (prev) => ({
3385
3838
  ...prev,
3386
3839
  [skillName]: (prev[skillName] || 0) + 1
3387
3840
  }));
3388
3841
  }, [trackBehavior]);
3389
- const trackChecklistSkip = useCallback6(() => {
3842
+ const trackChecklistSkip = useCallback8(() => {
3390
3843
  trackBehavior("checklistSkipRate", (prev) => ({
3391
3844
  total: (prev.total || 0) + 1,
3392
3845
  skipped: (prev.skipped || 0) + 1
3393
3846
  }));
3394
3847
  }, [trackBehavior]);
3395
- const trackChecklistComplete = useCallback6(() => {
3848
+ const trackChecklistComplete = useCallback8(() => {
3396
3849
  trackBehavior("checklistSkipRate", (prev) => ({
3397
3850
  total: (prev.total || 0) + 1,
3398
3851
  skipped: prev.skipped || 0
3399
3852
  }));
3400
3853
  }, [trackBehavior]);
3401
- const trackRegenerate = useCallback6(() => {
3854
+ const {
3855
+ activeChecklistRef,
3856
+ pendingChecklistRef,
3857
+ skipNextChecklistParsingRef,
3858
+ handleChecklistStart,
3859
+ handleChecklistAbort,
3860
+ handleChecklistRetry,
3861
+ handleChecklistSkip
3862
+ } = useChecklist({
3863
+ sessionsRef,
3864
+ sessions,
3865
+ setSessions,
3866
+ sendMessage: (...args) => sendMessageForChecklistRef.current(...args),
3867
+ abortControllers: abortControllersRef,
3868
+ removeLoadingSession,
3869
+ pendingAttachmentDataRef,
3870
+ trackChecklistSkip,
3871
+ resolveChecklistRefImage,
3872
+ buildChecklistStepPrompt
3873
+ });
3874
+ const trackRegenerate = useCallback8(() => {
3402
3875
  trackBehavior("regenerateRate", (prev) => ({
3403
3876
  ...prev,
3404
3877
  regenerated: (prev.regenerated || 0) + 1
3405
3878
  }));
3406
3879
  }, [trackBehavior]);
3407
- const trackMessageSent = useCallback6(() => {
3880
+ const trackMessageSent = useCallback8(() => {
3408
3881
  trackBehavior("regenerateRate", (prev) => ({
3409
3882
  total: (prev.total || 0) + 1,
3410
3883
  regenerated: prev.regenerated || 0
3411
3884
  }));
3412
3885
  }, [trackBehavior]);
3413
- const sendMessage = useCallback6(async (content, options2) => {
3886
+ const sendMessage = useCallback8(async (content, options2) => {
3414
3887
  const messageContent = content || input;
3415
3888
  if (!messageContent.trim() && attachments.length === 0 || isLoading) return;
3889
+ if (options2?.hiddenUserMessage) {
3890
+ toolCallDepthRef.current += 1;
3891
+ if (toolCallDepthRef.current > maxToolCallDepth) {
3892
+ toolCallDepthRef.current = 0;
3893
+ onErrorRef.current?.(new Error(`\uB3C4\uAD6C \uD638\uCD9C \uAE4A\uC774 \uC81C\uD55C \uCD08\uACFC (\uCD5C\uB300 ${maxToolCallDepth}\uD68C)`));
3894
+ return;
3895
+ }
3896
+ } else {
3897
+ toolCallDepthRef.current = 0;
3898
+ }
3416
3899
  let sessionId = currentSessionId;
3417
3900
  if (!sessionId) {
3418
3901
  if (useExternalStorage && onCreateSessionRef.current) {
@@ -3466,6 +3949,7 @@ ${finalContent}`;
3466
3949
  const actionPrompt = selectedAction?.systemPrompt;
3467
3950
  const isHidden = options2?.hiddenUserMessage ?? false;
3468
3951
  const currentAttachments = attachments;
3952
+ const uploadedUrlMap = /* @__PURE__ */ new Map();
3469
3953
  let userContentParts;
3470
3954
  if (currentAttachments.length > 0) {
3471
3955
  userContentParts = [];
@@ -3474,8 +3958,19 @@ ${finalContent}`;
3474
3958
  }
3475
3959
  for (const att of currentAttachments) {
3476
3960
  if (att.type === "image" && att.file) {
3477
- const dataUri = await fileToDataUri(att.file);
3478
- const imageUrl = onUploadImageRef.current ? await onUploadImageRef.current(dataUri, att.name) : dataUri;
3961
+ let imageUrl;
3962
+ try {
3963
+ if (fileUploaderRef.current) {
3964
+ imageUrl = await fileUploaderRef.current(att.file);
3965
+ } else {
3966
+ const dataUri = await fileToDataUri(att.file);
3967
+ imageUrl = onUploadImageRef.current ? await onUploadImageRef.current(dataUri, att.name) : dataUri;
3968
+ }
3969
+ } catch (err) {
3970
+ console.error("[chatllm] image upload failed, falling back to data URI:", err);
3971
+ imageUrl = await fileToDataUri(att.file);
3972
+ }
3973
+ uploadedUrlMap.set(att.id, imageUrl);
3479
3974
  userContentParts.push({ type: "image", url: imageUrl, alt: att.name, fileName: att.name });
3480
3975
  } else {
3481
3976
  userContentParts.push({ type: "file", name: att.name, url: att.previewUrl || "", mimeType: att.mimeType, size: att.size });
@@ -3574,7 +4069,7 @@ ${finalContent}`;
3574
4069
  })
3575
4070
  );
3576
4071
  try {
3577
- const filesToPass = fileUploaderRef.current ? await convertAttachmentsWithUploader(matchedFiles, fileUploaderRef.current) : skillConfig.autoConvertBase64 ? await convertAttachmentsToBase64(matchedFiles) : matchedFiles;
4072
+ const filesToPass = fileUploaderRef.current ? await convertAttachmentsWithUploaderCached(matchedFiles, fileUploaderRef.current, uploadedUrlMap) : skillConfig.autoConvertBase64 ? await convertAttachmentsToBase64(matchedFiles) : matchedFiles;
3578
4073
  const result = await skillConfig.execute({ files: filesToPass, userMessage: finalContent });
3579
4074
  const attachResultType = result.metadata?.type || result.metadata?.resultType || "text";
3580
4075
  const toolResultPart = {
@@ -3633,7 +4128,7 @@ ${finalContent}`;
3633
4128
  if (hasImageAttachments && hasUserText) {
3634
4129
  const imageAttachments = currentAttachments.filter((a) => a.type === "image");
3635
4130
  try {
3636
- pendingAttachmentDataRef.current = fileUploaderRef.current ? await convertAttachmentsWithUploader(imageAttachments, fileUploaderRef.current) : await convertAttachmentsToBase64(imageAttachments);
4131
+ pendingAttachmentDataRef.current = fileUploaderRef.current ? await convertAttachmentsWithUploaderCached(imageAttachments, fileUploaderRef.current, uploadedUrlMap) : await convertAttachmentsToBase64(imageAttachments);
3637
4132
  } catch (err) {
3638
4133
  console.error("[chatllm] pendingAttachment conversion failed:", err);
3639
4134
  pendingAttachmentDataRef.current = null;
@@ -3686,10 +4181,11 @@ ${finalContent}`;
3686
4181
  return;
3687
4182
  }
3688
4183
  abortControllersRef.current.set(capturedSessionId, new AbortController());
4184
+ let accumulatedContent = "";
4185
+ let lastUsage = null;
3689
4186
  try {
3690
4187
  const shouldSkipSkillParsing = skipNextSkillParsingRef.current;
3691
4188
  skipNextSkillParsingRef.current = false;
3692
- let accumulatedContent = "";
3693
4189
  let checklistStepImageUrl = null;
3694
4190
  let messagesToSend = [...existingMessages, userMessage];
3695
4191
  const recompressionThreshold = DEFAULT_RECOMPRESSION_THRESHOLD;
@@ -3895,119 +4391,71 @@ ${attachmentContext}
3895
4391
  emitFetchHeaders(response);
3896
4392
  }
3897
4393
  if (response) {
3898
- if (!response.ok) throw new Error("API error");
3899
- const reader = response.body?.getReader();
3900
- if (!reader) throw new Error("No reader");
3901
- const decoder = new TextDecoder();
3902
- let buffer = "";
3903
4394
  let skillTagDetected = false;
3904
4395
  let checklistTagDetected = false;
3905
4396
  let toolCallAcc = null;
3906
- while (true) {
3907
- const { done, value } = await reader.read();
3908
- if (done) break;
3909
- buffer += decoder.decode(value, { stream: true });
3910
- const lines = buffer.split("\n");
3911
- buffer = lines.pop() || "";
3912
- for (const line of lines) {
3913
- if (!line.trim()) continue;
3914
- let data = line;
3915
- if (line.startsWith("data: ")) {
3916
- data = line.slice(6);
3917
- if (data === "[DONE]") continue;
4397
+ const streamResult = await streamResponse(response, (chunk) => {
4398
+ if (useNativeTools && chunk.delta?.tool_calls) {
4399
+ toolCallAcc = accumulateToolCallDelta(toolCallAcc, chunk.delta);
4400
+ }
4401
+ if (useNativeTools && chunk.finishReason === "tool_calls" && toolCallAcc) {
4402
+ accumulatedContent += toolCallToSkillUseXml(toolCallAcc);
4403
+ skillTagDetected = true;
4404
+ return "break";
4405
+ }
4406
+ const { content: content2, thinking } = chunk;
4407
+ if (content2 || thinking) {
4408
+ if (content2) accumulatedContent += content2;
4409
+ if (!shouldSkipSkillParsing && accumulatedContent.includes("</skill_use>")) {
4410
+ const endIdx = accumulatedContent.indexOf("</skill_use>");
4411
+ accumulatedContent = accumulatedContent.substring(0, endIdx + "</skill_use>".length);
4412
+ skillTagDetected = true;
3918
4413
  }
3919
- try {
3920
- const parsed = JSON.parse(data);
3921
- const delta = parsed.choices?.[0]?.delta;
3922
- const finishReason = parsed.choices?.[0]?.finish_reason;
3923
- if (useNativeTools && delta?.tool_calls) {
3924
- toolCallAcc = accumulateToolCallDelta(toolCallAcc, delta);
3925
- }
3926
- if (useNativeTools && finishReason === "tool_calls" && toolCallAcc) {
3927
- accumulatedContent += toolCallToSkillUseXml(toolCallAcc);
3928
- skillTagDetected = true;
3929
- break;
3930
- }
3931
- const content2 = delta?.content || parsed.message?.content || parsed.content || parsed.text || "";
3932
- const thinking = parsed.message?.thinking || "";
3933
- if (content2 || thinking) {
3934
- if (content2) accumulatedContent += content2;
3935
- if (!shouldSkipSkillParsing && accumulatedContent.includes("</skill_use>")) {
3936
- const endIdx = accumulatedContent.indexOf("</skill_use>");
3937
- accumulatedContent = accumulatedContent.substring(0, endIdx + "</skill_use>".length);
3938
- skillTagDetected = true;
3939
- }
3940
- if (!skipNextChecklistParsingRef.current && accumulatedContent.includes("</checklist>")) {
3941
- const endIdx = accumulatedContent.indexOf("</checklist>");
3942
- accumulatedContent = accumulatedContent.substring(0, endIdx + "</checklist>".length);
3943
- checklistTagDetected = true;
3944
- }
3945
- const displayContent = skillTagDetected ? accumulatedContent : null;
3946
- setSessions(
3947
- (prev) => prev.map((s) => {
3948
- if (s.id === capturedSessionId) {
3949
- return {
3950
- ...s,
3951
- messages: s.messages.map((m) => {
3952
- if (m.id !== assistantMessageId) return m;
3953
- if (displayContent) {
3954
- return { ...m, content: displayContent };
3955
- }
3956
- let newContent = m.content;
3957
- if (thinking) {
3958
- if (!newContent.includes("<thinking>")) {
3959
- newContent = "<thinking>" + thinking;
3960
- } else if (!newContent.includes("</thinking>")) {
3961
- newContent += thinking;
3962
- }
3963
- }
3964
- if (content2) {
3965
- if (newContent.includes("<thinking>") && !newContent.includes("</thinking>")) {
3966
- newContent += "</thinking>\n\n";
3967
- }
3968
- newContent += content2;
3969
- }
3970
- return { ...m, content: newContent };
3971
- })
3972
- };
3973
- }
3974
- return s;
3975
- })
3976
- );
3977
- if (skillTagDetected || checklistTagDetected) break;
3978
- }
3979
- } catch {
4414
+ if (!skipNextChecklistParsingRef.current && accumulatedContent.includes("</checklist>")) {
4415
+ const endIdx = accumulatedContent.indexOf("</checklist>");
4416
+ accumulatedContent = accumulatedContent.substring(0, endIdx + "</checklist>".length);
4417
+ checklistTagDetected = true;
3980
4418
  }
4419
+ const displayContent = skillTagDetected ? accumulatedContent : null;
4420
+ setSessions(
4421
+ (prev) => prev.map((s) => {
4422
+ if (s.id === capturedSessionId) {
4423
+ return {
4424
+ ...s,
4425
+ messages: s.messages.map((m) => {
4426
+ if (m.id !== assistantMessageId) return m;
4427
+ if (displayContent) {
4428
+ return { ...m, content: displayContent };
4429
+ }
4430
+ let newContent = m.content;
4431
+ if (thinking) {
4432
+ if (!newContent.includes("<thinking>")) {
4433
+ newContent = "<thinking>" + thinking;
4434
+ } else if (!newContent.includes("</thinking>")) {
4435
+ newContent += thinking;
4436
+ }
4437
+ }
4438
+ if (content2) {
4439
+ if (newContent.includes("<thinking>") && !newContent.includes("</thinking>")) {
4440
+ newContent += "</thinking>\n\n";
4441
+ }
4442
+ newContent += content2;
4443
+ }
4444
+ return { ...m, content: newContent };
4445
+ })
4446
+ };
4447
+ }
4448
+ return s;
4449
+ })
4450
+ );
4451
+ if (skillTagDetected || checklistTagDetected) return "break";
3981
4452
  }
3982
- if (skillTagDetected || checklistTagDetected) break;
3983
- }
4453
+ });
4454
+ lastUsage = streamResult.usage;
3984
4455
  if (useNativeTools && toolCallAcc && !skillTagDetected) {
3985
4456
  accumulatedContent += toolCallToSkillUseXml(toolCallAcc);
3986
4457
  skillTagDetected = true;
3987
4458
  }
3988
- if (buffer.trim()) {
3989
- try {
3990
- const parsed = JSON.parse(buffer);
3991
- const content2 = parsed.message?.content || parsed.content || parsed.text;
3992
- if (content2) {
3993
- accumulatedContent += content2;
3994
- setSessions(
3995
- (prev) => prev.map((s) => {
3996
- if (s.id === capturedSessionId) {
3997
- return {
3998
- ...s,
3999
- messages: s.messages.map(
4000
- (m) => m.id === assistantMessageId ? { ...m, content: m.content + content2 } : m
4001
- )
4002
- };
4003
- }
4004
- return s;
4005
- })
4006
- );
4007
- }
4008
- } catch {
4009
- }
4010
- }
4011
4459
  }
4012
4460
  const saveMessagesOnEarlyReturn = (overrideContentParts) => {
4013
4461
  if (!useExternalStorage || !capturedSessionId) return;
@@ -4368,9 +4816,10 @@ ${result.content}
4368
4816
  }
4369
4817
  skipNextSkillParsingRef.current = true;
4370
4818
  saveMessagesOnEarlyReturn();
4819
+ const truncatedResult = result.content.length > maxToolResultSize ? result.content.substring(0, maxToolResultSize) + "\n\n...(truncated)" : result.content;
4371
4820
  const resultPrompt = `\uC2A4\uD0AC "${detectedSkill.name}" \uC2E4\uD589 \uACB0\uACFC:
4372
4821
 
4373
- ${result.content}
4822
+ ${truncatedResult}
4374
4823
 
4375
4824
  \uC704 \uACB0\uACFC\uB97C \uBC14\uD0D5\uC73C\uB85C \uC0AC\uC6A9\uC790\uC758 \uC6D0\uB798 \uC9C8\uBB38\uC5D0 \uB2F5\uBCC0\uD574\uC8FC\uC138\uC694. skill_use \uD0DC\uADF8\uB294 \uC0AC\uC6A9\uD558\uC9C0 \uB9C8\uC138\uC694.`;
4376
4825
  setTimeout(() => {
@@ -4696,6 +5145,32 @@ ${stepSummary}
4696
5145
  };
4697
5146
  })
4698
5147
  );
5148
+ if (lastUsage && capturedSessionId) {
5149
+ const usage = lastUsage;
5150
+ setSessions(
5151
+ (prev) => prev.map((s) => {
5152
+ if (s.id !== capturedSessionId) return s;
5153
+ const prevStats = s.tokenStats ?? {
5154
+ totalPromptTokens: 0,
5155
+ totalCompletionTokens: 0,
5156
+ totalTokens: 0,
5157
+ messageCount: 0,
5158
+ lastUpdated: 0
5159
+ };
5160
+ return {
5161
+ ...s,
5162
+ tokenStats: {
5163
+ totalPromptTokens: prevStats.totalPromptTokens + usage.promptTokens,
5164
+ totalCompletionTokens: prevStats.totalCompletionTokens + usage.completionTokens,
5165
+ totalTokens: prevStats.totalTokens + usage.totalTokens,
5166
+ messageCount: prevStats.messageCount + 1,
5167
+ lastUpdated: Date.now()
5168
+ }
5169
+ };
5170
+ })
5171
+ );
5172
+ onTokenUsageRef.current?.(capturedSessionId, usage);
5173
+ }
4699
5174
  if (useExternalStorage && capturedSessionId) {
4700
5175
  const assistantContentForSave = accumulatedContent;
4701
5176
  if (assistantContentForSave && onSaveMessagesRef.current) {
@@ -4724,296 +5199,77 @@ ${stepSummary}
4724
5199
  if (sinceLastExtraction >= 4) {
4725
5200
  lastExtractionMsgCountRef.current = currentMsgCount;
4726
5201
  const recentForExtraction = [...existingMessages.slice(-6), userMessage].map(
4727
- (m) => ({ role: m.role, content: m.content })
4728
- );
4729
- infoExtraction.extractInfo(recentForExtraction).catch(() => {
4730
- });
4731
- }
4732
- }
4733
- if (observerConfig) {
4734
- const newMessages = [userMessage, { id: assistantMessageId, role: "assistant", content: accumulatedContent, timestamp: Date.now() }];
4735
- observer.processMessages(capturedSessionId, newMessages);
4736
- }
4737
- } catch (error) {
4738
- if (error instanceof Error && error.name === "AbortError") {
4739
- return;
4740
- }
4741
- const err = error instanceof Error ? error : new Error("Unknown error");
4742
- onErrorRef.current?.(err);
4743
- setSessions(
4744
- (prev) => prev.map((s) => {
4745
- if (s.id === capturedSessionId) {
4746
- return {
4747
- ...s,
4748
- messages: s.messages.map(
4749
- (m) => m.id === assistantMessageId ? { ...m, content: "\uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694." } : m
4750
- )
4751
- };
4752
- }
4753
- return s;
4754
- })
4755
- );
4756
- } finally {
4757
- removeLoadingSession(capturedSessionId);
4758
- abortControllersRef.current.delete(capturedSessionId);
4759
- }
4760
- }, [
4761
- input,
4762
- loadingSessionIds,
4763
- currentSessionId,
4764
- sessions,
4765
- selectedModel,
4766
- quotedText,
4767
- selectedAction,
4768
- apiEndpoint,
4769
- apiKey,
4770
- models,
4771
- contextCompressionThreshold,
4772
- keepRecentMessages,
4773
- buildSystemPrompt,
4774
- compressContext,
4775
- useExternalStorage,
4776
- handleSkillCall,
4777
- resolvedSkills,
4778
- /** @Todo vibecode - attachments, continueAfterToolResult를 deps에 추가하여 stale closure 방지 */
4779
- attachments,
4780
- continueAfterToolResult
4781
- ]);
4782
- const handleChecklistStart = useCallback6(
4783
- (messageId) => {
4784
- const session = sessionsRef.current.find(
4785
- (s) => s.messages.some((m) => m.id === messageId)
4786
- );
4787
- if (!session) return;
4788
- const message = session.messages.find((m) => m.id === messageId);
4789
- if (!message?.checklistBlock) return;
4790
- pendingChecklistRef.current = null;
4791
- activeChecklistRef.current = {
4792
- messageId,
4793
- sessionId: session.id,
4794
- items: message.checklistBlock.items.map((it) => ({ id: it.id, title: it.title, skill: it.skill, fileIndex: it.fileIndex, fileType: it.fileType, refImage: it.refImage, refStep: it.refStep })),
4795
- currentStep: 0,
4796
- stepResults: []
4797
- };
4798
- setSessions(
4799
- (prev) => prev.map((s) => {
4800
- if (s.id !== session.id) return s;
4801
- return {
4802
- ...s,
4803
- messages: s.messages.map((m) => {
4804
- if (m.id !== messageId || !m.checklistBlock) return m;
4805
- const updatedItems = m.checklistBlock.items.map((it, idx) => ({
4806
- ...it,
4807
- status: idx === 0 ? "in_progress" : it.status
4808
- }));
4809
- return {
4810
- ...m,
4811
- checklistBlock: { ...m.checklistBlock, items: updatedItems, currentStep: 0 }
4812
- };
4813
- })
4814
- };
4815
- })
4816
- );
4817
- skipNextChecklistParsingRef.current = true;
4818
- setTimeout(() => {
4819
- const items = message.checklistBlock.items;
4820
- const refUrl = resolveChecklistRefImage(items[0], messageId, session.id);
4821
- if (refUrl) pendingAttachmentDataRef.current = [{ name: "ref_image", mimeType: "image/png", base64: "", size: 0, url: refUrl }];
4822
- sendMessage(
4823
- buildChecklistStepPrompt(0, items.length, items[0], true, refUrl),
4824
- { hiddenUserMessage: true, isChecklistExecution: true, checklistStep: { index: 0, title: items[0].title } }
4825
- );
4826
- }, 100);
4827
- },
4828
- [sendMessage]
4829
- );
4830
- const handleChecklistAbort = useCallback6(() => {
4831
- if (!activeChecklistRef.current) return;
4832
- const checklist = activeChecklistRef.current;
4833
- const stepIdx = checklist.currentStep;
4834
- abortControllersRef.current.get(checklist.sessionId)?.abort();
4835
- setSessions(
4836
- (prev) => prev.map((s) => {
4837
- if (s.id !== checklist.sessionId) return s;
4838
- return {
4839
- ...s,
4840
- messages: s.messages.map((m) => {
4841
- if (m.id !== checklist.messageId || !m.checklistBlock) return m;
4842
- return {
4843
- ...m,
4844
- checklistBlock: {
4845
- ...m.checklistBlock,
4846
- items: m.checklistBlock.items.map((it, idx) => ({
4847
- ...it,
4848
- status: idx === stepIdx ? "error" : it.status
4849
- }))
4850
- }
4851
- };
4852
- })
4853
- };
4854
- })
4855
- );
4856
- activeChecklistRef.current = null;
4857
- removeLoadingSession(checklist.sessionId);
4858
- }, [removeLoadingSession]);
4859
- const handleChecklistRetry = useCallback6(
4860
- (messageId, stepIndex) => {
4861
- const session = sessionsRef.current.find(
4862
- (s) => s.messages.some((m) => m.id === messageId)
4863
- );
4864
- if (!session) return;
4865
- const message = session.messages.find((m) => m.id === messageId);
4866
- if (!message?.checklistBlock) return;
4867
- activeChecklistRef.current = {
4868
- messageId,
4869
- sessionId: session.id,
4870
- items: message.checklistBlock.items.map((it) => ({ id: it.id, title: it.title, skill: it.skill, fileIndex: it.fileIndex, fileType: it.fileType, refImage: it.refImage, refStep: it.refStep })),
4871
- currentStep: stepIndex,
4872
- stepResults: message.checklistBlock.items.slice(0, stepIndex).map((it) => it.result || "")
4873
- };
4874
- setSessions(
4875
- (prev) => prev.map((s) => {
4876
- if (s.id !== session.id) return s;
4877
- return {
4878
- ...s,
4879
- messages: s.messages.map((m) => {
4880
- if (m.id !== messageId || !m.checklistBlock) return m;
4881
- return {
4882
- ...m,
4883
- checklistBlock: {
4884
- ...m.checklistBlock,
4885
- items: m.checklistBlock.items.map((it, idx) => ({
4886
- ...it,
4887
- status: idx === stepIndex ? "in_progress" : idx > stepIndex ? "pending" : it.status
4888
- })),
4889
- currentStep: stepIndex
4890
- }
4891
- };
4892
- })
4893
- };
4894
- })
4895
- );
4896
- skipNextChecklistParsingRef.current = true;
4897
- setTimeout(() => {
4898
- const items = message.checklistBlock.items;
4899
- const refUrl = resolveChecklistRefImage(items[stepIndex], messageId, session.id);
4900
- if (refUrl) pendingAttachmentDataRef.current = [{ name: "ref_image", mimeType: "image/png", base64: "", size: 0, url: refUrl }];
4901
- sendMessage(
4902
- buildChecklistStepPrompt(stepIndex, items.length, items[stepIndex], stepIndex === 0, refUrl),
4903
- { hiddenUserMessage: true, isChecklistExecution: true, checklistStep: { index: stepIndex, title: items[stepIndex].title } }
4904
- );
4905
- }, 100);
4906
- },
4907
- [sendMessage]
4908
- );
4909
- const handleChecklistSkip = useCallback6(
4910
- (messageId, stepIndex) => {
4911
- const session = sessionsRef.current.find(
4912
- (s) => s.messages.some((m) => m.id === messageId)
4913
- );
4914
- if (!session) return;
4915
- const message = session.messages.find((m) => m.id === messageId);
4916
- if (!message?.checklistBlock) return;
4917
- trackChecklistSkip();
4918
- setSessions(
4919
- (prev) => prev.map((s) => {
4920
- if (s.id !== session.id) return s;
4921
- return {
4922
- ...s,
4923
- messages: s.messages.map((m) => {
4924
- if (m.id !== messageId || !m.checklistBlock) return m;
4925
- return {
4926
- ...m,
4927
- checklistBlock: {
4928
- ...m.checklistBlock,
4929
- items: m.checklistBlock.items.map((it, idx) => ({
4930
- ...it,
4931
- status: idx === stepIndex ? "done" : it.status,
4932
- result: idx === stepIndex ? "(\uAC74\uB108\uB700)" : it.result
4933
- }))
4934
- }
4935
- };
4936
- })
4937
- };
4938
- })
4939
- );
4940
- const nextPending = message.checklistBlock.items.findIndex(
4941
- (it, idx) => idx > stepIndex && (it.status === "pending" || it.status === "error")
4942
- );
4943
- if (nextPending >= 0) {
4944
- activeChecklistRef.current = {
4945
- messageId,
4946
- sessionId: session.id,
4947
- items: message.checklistBlock.items.map((it) => ({ id: it.id, title: it.title, skill: it.skill, fileIndex: it.fileIndex, fileType: it.fileType, refImage: it.refImage, refStep: it.refStep })),
4948
- currentStep: nextPending,
4949
- stepResults: message.checklistBlock.items.slice(0, nextPending).map((it) => it.result || "(\uAC74\uB108\uB700)")
4950
- };
4951
- setSessions(
4952
- (prev) => prev.map((s) => {
4953
- if (s.id !== session.id) return s;
4954
- return {
4955
- ...s,
4956
- messages: s.messages.map((m) => {
4957
- if (m.id !== messageId || !m.checklistBlock) return m;
4958
- return {
4959
- ...m,
4960
- checklistBlock: {
4961
- ...m.checklistBlock,
4962
- items: m.checklistBlock.items.map((it, idx) => ({
4963
- ...it,
4964
- status: idx === nextPending ? "in_progress" : it.status
4965
- })),
4966
- currentStep: nextPending
4967
- }
4968
- };
4969
- })
4970
- };
4971
- })
4972
- );
4973
- skipNextChecklistParsingRef.current = true;
4974
- setTimeout(() => {
4975
- const items = message.checklistBlock.items;
4976
- const refUrl = resolveChecklistRefImage(items[nextPending], messageId, session.id);
4977
- if (refUrl) pendingAttachmentDataRef.current = [{ name: "ref_image", mimeType: "image/png", base64: "", size: 0, url: refUrl }];
4978
- sendMessage(
4979
- buildChecklistStepPrompt(nextPending, items.length, items[nextPending], false, refUrl),
4980
- { hiddenUserMessage: true, isChecklistExecution: true, checklistStep: { index: nextPending, title: items[nextPending].title } }
5202
+ (m) => ({ role: m.role, content: m.content })
4981
5203
  );
4982
- }, 100);
4983
- } else {
4984
- const allResults = message.checklistBlock.items.map((it, i) => {
4985
- const result = i === stepIndex ? "(\uAC74\uB108\uB700)" : it.result || "(\uAC74\uB108\uB700)";
4986
- return `### ${i + 1}. ${it.title}
4987
- ${result}`;
4988
- }).join("\n\n");
4989
- setSessions(
4990
- (prev) => prev.map((s) => {
4991
- if (s.id !== session.id) return s;
5204
+ infoExtraction.extractInfo(recentForExtraction).catch(() => {
5205
+ });
5206
+ }
5207
+ }
5208
+ if (observerConfig) {
5209
+ const newMessages = [userMessage, { id: assistantMessageId, role: "assistant", content: accumulatedContent, timestamp: Date.now() }];
5210
+ observer.processMessages(capturedSessionId, newMessages);
5211
+ }
5212
+ } catch (error) {
5213
+ const classified = classifyFetchError(error);
5214
+ if (classified.code === "ABORT") {
5215
+ if (accumulatedContent) {
5216
+ setSessions(
5217
+ (prev) => prev.map((s) => {
5218
+ if (s.id !== capturedSessionId) return s;
5219
+ return {
5220
+ ...s,
5221
+ messages: s.messages.map(
5222
+ (m) => m.id === assistantMessageId ? { ...m, content: accumulatedContent } : m
5223
+ )
5224
+ };
5225
+ })
5226
+ );
5227
+ onAbortRef.current?.(capturedSessionId, accumulatedContent);
5228
+ }
5229
+ return;
5230
+ }
5231
+ onErrorRef.current?.(classified);
5232
+ setSessions(
5233
+ (prev) => prev.map((s) => {
5234
+ if (s.id === capturedSessionId) {
4992
5235
  return {
4993
5236
  ...s,
4994
- messages: s.messages.map((m) => {
4995
- if (m.id !== messageId || !m.checklistBlock) return m;
4996
- return { ...m, checklistBlock: { ...m.checklistBlock, completed: true } };
4997
- })
5237
+ messages: s.messages.map(
5238
+ (m) => m.id === assistantMessageId ? { ...m, content: accumulatedContent || "\uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694." } : m
5239
+ )
4998
5240
  };
4999
- })
5000
- );
5001
- skipNextChecklistParsingRef.current = true;
5002
- setTimeout(() => {
5003
- sendMessage(
5004
- `\uBAA8\uB4E0 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8 \uB2E8\uACC4\uAC00 \uC644\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. \uC544\uB798\uB294 \uAC01 \uB2E8\uACC4\uBCC4 \uACB0\uACFC\uC785\uB2C8\uB2E4:
5005
-
5006
- ${allResults}
5007
-
5008
- \uC704 \uACB0\uACFC\uB97C \uC885\uD569\uD558\uC5EC \uCD5C\uC885 \uACB0\uACFC\uBB3C\uC744 \uC644\uC131\uD574\uC8FC\uC138\uC694. checklist \uD0DC\uADF8\uB294 \uC0AC\uC6A9\uD558\uC9C0 \uB9C8\uC138\uC694.`,
5009
- { hiddenUserMessage: true, isChecklistExecution: true }
5010
- );
5011
- }, 100);
5012
- }
5013
- },
5014
- [sendMessage]
5015
- );
5016
- const handlePollSubmit = useCallback6(
5241
+ }
5242
+ return s;
5243
+ })
5244
+ );
5245
+ } finally {
5246
+ removeLoadingSession(capturedSessionId);
5247
+ abortControllersRef.current.delete(capturedSessionId);
5248
+ }
5249
+ }, [
5250
+ input,
5251
+ loadingSessionIds,
5252
+ currentSessionId,
5253
+ sessions,
5254
+ selectedModel,
5255
+ quotedText,
5256
+ selectedAction,
5257
+ apiEndpoint,
5258
+ apiKey,
5259
+ models,
5260
+ contextCompressionThreshold,
5261
+ keepRecentMessages,
5262
+ buildSystemPrompt,
5263
+ compressContext,
5264
+ useExternalStorage,
5265
+ handleSkillCall,
5266
+ resolvedSkills,
5267
+ /** @Todo vibecode - attachments, continueAfterToolResult를 deps에 추가하여 stale closure 방지 */
5268
+ attachments,
5269
+ continueAfterToolResult
5270
+ ]);
5271
+ sendMessageForChecklistRef.current = sendMessage;
5272
+ const handlePollSubmit = useCallback8(
5017
5273
  (messageId, responses) => {
5018
5274
  const currentSess = sessions.find((s) => s.id === currentSessionId);
5019
5275
  const targetMessage = currentSess?.messages.find((m) => m.id === messageId);
@@ -5083,7 +5339,7 @@ ${formattedParts.join("\n")}
5083
5339
  },
5084
5340
  [sessions, currentSessionId, selectedModel, sendMessage]
5085
5341
  );
5086
- const saveEdit = useCallback6(async (content) => {
5342
+ const saveEdit = useCallback8(async (content) => {
5087
5343
  if (!editingMessageId || !currentSession || !currentSessionId) return;
5088
5344
  const messageIndex = currentSession.messages.findIndex((m) => m.id === editingMessageId);
5089
5345
  if (messageIndex === -1) return;
@@ -5100,7 +5356,7 @@ ${formattedParts.join("\n")}
5100
5356
  setEditingMessageId(null);
5101
5357
  await sendMessage(content);
5102
5358
  }, [editingMessageId, currentSession, currentSessionId, sendMessage]);
5103
- const regenerate = useCallback6(async (messageId) => {
5359
+ const regenerate = useCallback8(async (messageId) => {
5104
5360
  console.log("[ChatUI] Regenerate called:", { messageId, currentSessionId, isLoading });
5105
5361
  if (!currentSession || !currentSessionId || isLoading) {
5106
5362
  console.log("[ChatUI] Regenerate early return - missing session or loading");
@@ -5176,100 +5432,39 @@ ${formattedParts.join("\n")}
5176
5432
  signal: abortControllersRef.current.get(capturedSessionId).signal
5177
5433
  });
5178
5434
  console.log("[ChatUI] Regenerate response status:", response.status);
5179
- if (!response.ok) throw new Error(`API error: ${response.status}`);
5180
- const reader = response.body?.getReader();
5181
- if (!reader) throw new Error("No reader");
5182
- const decoder = new TextDecoder();
5183
- let buffer = "";
5184
- while (true) {
5185
- const { done, value } = await reader.read();
5186
- if (done) break;
5187
- buffer += decoder.decode(value, { stream: true });
5188
- const lines = buffer.split("\n");
5189
- buffer = lines.pop() || "";
5190
- for (const line of lines) {
5191
- if (!line.trim()) continue;
5192
- let data = line;
5193
- if (line.startsWith("data: ")) {
5194
- data = line.slice(6);
5195
- if (data === "[DONE]") continue;
5196
- }
5197
- try {
5198
- const parsed = JSON.parse(data);
5199
- const content = parsed.message?.content || parsed.content || parsed.text || "";
5200
- const thinking = parsed.message?.thinking || "";
5201
- if (content || thinking) {
5202
- setSessions(
5203
- (prev) => prev.map((s) => {
5204
- if (s.id === capturedSessionId) {
5205
- return {
5206
- ...s,
5207
- messages: s.messages.map((m) => {
5208
- if (m.id !== assistantMessageId) return m;
5209
- let newContent = m.content;
5210
- if (thinking) {
5211
- if (!newContent.includes("<thinking>")) {
5212
- newContent = "<thinking>" + thinking;
5213
- } else if (!newContent.includes("</thinking>")) {
5214
- newContent += thinking;
5215
- }
5216
- }
5217
- if (content) {
5218
- if (newContent.includes("<thinking>") && !newContent.includes("</thinking>")) {
5219
- newContent += "</thinking>\n\n";
5220
- }
5221
- newContent += content;
5222
- }
5223
- return { ...m, content: newContent };
5224
- })
5225
- };
5226
- }
5227
- return s;
5228
- })
5229
- );
5230
- }
5231
- } catch {
5232
- }
5233
- }
5234
- }
5235
- if (buffer.trim()) {
5236
- try {
5237
- const parsed = JSON.parse(buffer);
5238
- const content = parsed.message?.content || parsed.content || parsed.text || "";
5239
- const thinking = parsed.message?.thinking || "";
5240
- if (content || thinking) {
5241
- setSessions(
5242
- (prev) => prev.map((s) => {
5243
- if (s.id === capturedSessionId) {
5244
- return {
5245
- ...s,
5246
- messages: s.messages.map((m) => {
5247
- if (m.id !== assistantMessageId) return m;
5248
- let newContent = m.content;
5249
- if (thinking) {
5250
- if (!newContent.includes("<thinking>")) {
5251
- newContent = "<thinking>" + thinking;
5252
- } else if (!newContent.includes("</thinking>")) {
5253
- newContent += thinking;
5254
- }
5435
+ await streamResponse(response, (chunk) => {
5436
+ const { content, thinking } = chunk;
5437
+ if (content || thinking) {
5438
+ setSessions(
5439
+ (prev) => prev.map((s) => {
5440
+ if (s.id === capturedSessionId) {
5441
+ return {
5442
+ ...s,
5443
+ messages: s.messages.map((m) => {
5444
+ if (m.id !== assistantMessageId) return m;
5445
+ let newContent = m.content;
5446
+ if (thinking) {
5447
+ if (!newContent.includes("<thinking>")) {
5448
+ newContent = "<thinking>" + thinking;
5449
+ } else if (!newContent.includes("</thinking>")) {
5450
+ newContent += thinking;
5255
5451
  }
5256
- if (content) {
5257
- if (newContent.includes("<thinking>") && !newContent.includes("</thinking>")) {
5258
- newContent += "</thinking>\n\n";
5259
- }
5260
- newContent += content;
5452
+ }
5453
+ if (content) {
5454
+ if (newContent.includes("<thinking>") && !newContent.includes("</thinking>")) {
5455
+ newContent += "</thinking>\n\n";
5261
5456
  }
5262
- return { ...m, content: newContent };
5263
- })
5264
- };
5265
- }
5266
- return s;
5267
- })
5268
- );
5269
- }
5270
- } catch {
5457
+ newContent += content;
5458
+ }
5459
+ return { ...m, content: newContent };
5460
+ })
5461
+ };
5462
+ }
5463
+ return s;
5464
+ })
5465
+ );
5271
5466
  }
5272
- }
5467
+ }, { noTimeout: true });
5273
5468
  } catch (error) {
5274
5469
  if (error instanceof Error && error.name === "AbortError") {
5275
5470
  return;
@@ -5280,7 +5475,7 @@ ${formattedParts.join("\n")}
5280
5475
  removeLoadingSession(capturedSessionId);
5281
5476
  }
5282
5477
  }, [currentSession, currentSessionId, loadingSessionIds, selectedModel, models, apiEndpoint, apiKey, buildSystemPrompt]);
5283
- const askOtherModel = useCallback6(async (messageId, targetModel) => {
5478
+ const askOtherModel = useCallback8(async (messageId, targetModel) => {
5284
5479
  if (!currentSession || !currentSessionId || isLoading) return;
5285
5480
  const assistantIndex = currentSession.messages.findIndex((m) => m.id === messageId);
5286
5481
  if (assistantIndex === -1) return;
@@ -5335,30 +5530,7 @@ ${currentSession.contextSummary}` },
5335
5530
  responseContent = result.content;
5336
5531
  responseSources = result.sources;
5337
5532
  } else {
5338
- const reader = result.getReader();
5339
- const decoder = new TextDecoder();
5340
- let buffer = "";
5341
- while (true) {
5342
- const { done, value } = await reader.read();
5343
- if (done) break;
5344
- buffer += decoder.decode(value, { stream: true });
5345
- const lines = buffer.split("\n");
5346
- buffer = lines.pop() || "";
5347
- for (const line of lines) {
5348
- if (line.startsWith("data: ")) {
5349
- const data = line.slice(6);
5350
- if (data === "[DONE]") continue;
5351
- try {
5352
- const parsed = JSON.parse(data);
5353
- {
5354
- const chunk = parsed.content ?? parsed.text ?? "";
5355
- if (chunk) responseContent += chunk;
5356
- }
5357
- } catch {
5358
- }
5359
- }
5360
- }
5361
- }
5533
+ responseContent = await parseSSEResponse(new Response(result));
5362
5534
  }
5363
5535
  } else {
5364
5536
  const isOllama = provider === "ollama" || apiEndpoint.includes("ollama") || apiEndpoint.includes("11434");
@@ -5376,32 +5548,9 @@ ${currentSession.contextSummary}` },
5376
5548
  signal: abortControllersRef.current.get(currentSessionId).signal
5377
5549
  });
5378
5550
  emitFetchHeaders(response);
5379
- if (!response.ok) throw new Error("API error");
5380
- const reader = response.body?.getReader();
5381
- if (!reader) throw new Error("No reader");
5382
- const decoder = new TextDecoder();
5383
- let buffer = "";
5384
- while (true) {
5385
- const { done, value } = await reader.read();
5386
- if (done) break;
5387
- buffer += decoder.decode(value, { stream: true });
5388
- const lines = buffer.split("\n");
5389
- buffer = lines.pop() || "";
5390
- for (const line of lines) {
5391
- if (line.startsWith("data: ")) {
5392
- const data = line.slice(6);
5393
- if (data === "[DONE]") continue;
5394
- try {
5395
- const parsed = JSON.parse(data);
5396
- {
5397
- const chunk = parsed.content ?? parsed.text ?? "";
5398
- if (chunk) responseContent += chunk;
5399
- }
5400
- } catch {
5401
- }
5402
- }
5403
- }
5404
- }
5551
+ await streamResponse(response, (chunk) => {
5552
+ if (chunk.content) responseContent += chunk.content;
5553
+ }, { noTimeout: true });
5405
5554
  }
5406
5555
  const alternative = {
5407
5556
  id: generateId5("alt"),
@@ -5461,13 +5610,13 @@ ${currentSession.contextSummary}` },
5461
5610
  onSendMessage,
5462
5611
  onError
5463
5612
  ]);
5464
- const setActiveAlternative = useCallback6((assistantMessageId, index) => {
5613
+ const setActiveAlternative = useCallback8((assistantMessageId, index) => {
5465
5614
  setActiveAlternatives((prev) => ({
5466
5615
  ...prev,
5467
5616
  [assistantMessageId]: index
5468
5617
  }));
5469
5618
  }, []);
5470
- const getActiveAlternative = useCallback6((assistantMessageId) => {
5619
+ const getActiveAlternative = useCallback8((assistantMessageId) => {
5471
5620
  return activeAlternatives[assistantMessageId] ?? 0;
5472
5621
  }, [activeAlternatives]);
5473
5622
  return {
@@ -5525,6 +5674,10 @@ ${currentSession.contextSummary}` },
5525
5674
  }));
5526
5675
  await infoExtraction.extractInfo(recentMessages);
5527
5676
  },
5677
+ getTokenStats: useCallback8((sessionId) => {
5678
+ const session = sessions.find((s) => s.id === sessionId);
5679
+ return session?.tokenStats ?? null;
5680
+ }, [sessions]),
5528
5681
  // External Storage Loading States
5529
5682
  /**
5530
5683
  * @description 세션 목록 로딩 상태
@@ -5685,7 +5838,7 @@ var ImageErrorContext = createContext(null);
5685
5838
  var useImageError = () => useContext(ImageErrorContext);
5686
5839
 
5687
5840
  // src/react/components/ChatSidebar.tsx
5688
- import { useState as useState9, useRef as useRef7, useEffect as useEffect6 } from "react";
5841
+ import { useState as useState9, useRef as useRef9, useEffect as useEffect6 } from "react";
5689
5842
 
5690
5843
  // src/react/components/Icon.tsx
5691
5844
  import { jsx } from "react/jsx-runtime";
@@ -5785,7 +5938,7 @@ var IconSvg = ({
5785
5938
  };
5786
5939
 
5787
5940
  // src/react/components/MarkdownRenderer.tsx
5788
- import React2, { useMemo as useMemo4 } from "react";
5941
+ import React2, { useMemo as useMemo5 } from "react";
5789
5942
 
5790
5943
  // src/react/components/LinkChip.tsx
5791
5944
  import { useState as useState7 } from "react";
@@ -6842,11 +6995,11 @@ var MarkdownRenderer = ({
6842
6995
  sources,
6843
6996
  inline = false
6844
6997
  }) => {
6845
- const inlineRendered = useMemo4(() => {
6998
+ const inlineRendered = useMemo5(() => {
6846
6999
  if (!inline) return null;
6847
7000
  return parseInlineElements(content, "inline-md", sources);
6848
7001
  }, [inline, content, sources]);
6849
- const rendered = useMemo4(() => {
7002
+ const rendered = useMemo5(() => {
6850
7003
  if (inline) return null;
6851
7004
  const elements = [];
6852
7005
  let processedContent = content;
@@ -7243,7 +7396,7 @@ var MarkdownRenderer = ({
7243
7396
  };
7244
7397
 
7245
7398
  // src/react/components/ProjectSelector.tsx
7246
- import { useState as useState8, useRef as useRef6, useEffect as useEffect5 } from "react";
7399
+ import { useState as useState8, useRef as useRef8, useEffect as useEffect5 } from "react";
7247
7400
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
7248
7401
  var ProjectSelector = ({
7249
7402
  projects,
@@ -7253,7 +7406,7 @@ var ProjectSelector = ({
7253
7406
  onProjectSettings
7254
7407
  }) => {
7255
7408
  const [isOpen, setIsOpen] = useState8(false);
7256
- const dropdownRef = useRef6(null);
7409
+ const dropdownRef = useRef8(null);
7257
7410
  const currentProject = projects.find((p) => p.id === currentProjectId);
7258
7411
  useEffect5(() => {
7259
7412
  const handleClickOutside = (e) => {
@@ -7491,7 +7644,7 @@ var ChatSidebar = ({
7491
7644
  const sidebarWidth = typeof widthProp === "number" ? `${widthProp}px` : widthProp || "288px";
7492
7645
  const [editingId, setEditingId] = useState9(null);
7493
7646
  const [editingTitle, setEditingTitle] = useState9("");
7494
- const inputRef = useRef7(null);
7647
+ const inputRef = useRef9(null);
7495
7648
  useEffect6(() => {
7496
7649
  if (editingId && inputRef.current) {
7497
7650
  inputRef.current.focus();
@@ -8133,7 +8286,7 @@ var ChatHeader = ({
8133
8286
  };
8134
8287
 
8135
8288
  // src/react/components/ChatInput.tsx
8136
- import React6, { useRef as useRef8, useEffect as useEffect7, useState as useState11 } from "react";
8289
+ import React6, { useRef as useRef10, useEffect as useEffect7, useState as useState11 } from "react";
8137
8290
  import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
8138
8291
  var ChatInput = ({
8139
8292
  value,
@@ -8162,12 +8315,12 @@ var ChatInput = ({
8162
8315
  inline = false
8163
8316
  }) => {
8164
8317
  const [mainMenuOpen, setMainMenuOpen] = React6.useState(false);
8165
- const textareaRef = useRef8(null);
8166
- const fileInputRef = useRef8(null);
8318
+ const textareaRef = useRef10(null);
8319
+ const fileInputRef = useRef10(null);
8167
8320
  const [actionMenuOpen, setActionMenuOpen] = useState11(false);
8168
8321
  const [isDragOver, setIsDragOver] = useState11(false);
8169
- const mainMenuRef = useRef8(null);
8170
- const actionMenuRef = useRef8(null);
8322
+ const mainMenuRef = useRef10(null);
8323
+ const actionMenuRef = useRef10(null);
8171
8324
  useEffect7(() => {
8172
8325
  if (textareaRef.current) {
8173
8326
  textareaRef.current.style.height = "auto";
@@ -8896,7 +9049,7 @@ var iconButtonStyle = {
8896
9049
  };
8897
9050
 
8898
9051
  // src/react/components/MessageList.tsx
8899
- import React14, { useRef as useRef10, useEffect as useEffect10, useCallback as useCallback11, useState as useState18 } from "react";
9052
+ import React14, { useRef as useRef12, useEffect as useEffect10, useCallback as useCallback13, useState as useState18 } from "react";
8900
9053
 
8901
9054
  // src/react/components/MessageBubble.tsx
8902
9055
  import { useState as useState17 } from "react";
@@ -9185,7 +9338,7 @@ var DeepResearchProgressUI = ({ progress }) => {
9185
9338
  };
9186
9339
 
9187
9340
  // src/react/components/PollCard.tsx
9188
- import { useState as useState12, useCallback as useCallback7, useEffect as useEffect8 } from "react";
9341
+ import { useState as useState12, useCallback as useCallback9, useEffect as useEffect8 } from "react";
9189
9342
  import { Fragment as Fragment5, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
9190
9343
  var renderInlineMarkdown = (text) => {
9191
9344
  const parts = [];
@@ -9229,7 +9382,7 @@ var PollCard = ({
9229
9382
  const [otherTexts, setOtherTexts] = useState12({});
9230
9383
  const [otherSelected, setOtherSelected] = useState12({});
9231
9384
  const currentQuestion = questions[activeTab];
9232
- const handleOptionToggle = useCallback7(
9385
+ const handleOptionToggle = useCallback9(
9233
9386
  (questionId, optionId, multiSelect) => {
9234
9387
  setSelections((prev) => {
9235
9388
  const current = prev[questionId] || /* @__PURE__ */ new Set();
@@ -9251,7 +9404,7 @@ var PollCard = ({
9251
9404
  },
9252
9405
  []
9253
9406
  );
9254
- const handleOtherToggle = useCallback7((questionId, multiSelect) => {
9407
+ const handleOtherToggle = useCallback9((questionId, multiSelect) => {
9255
9408
  if (multiSelect) {
9256
9409
  setOtherSelected((prev) => ({ ...prev, [questionId]: !prev[questionId] }));
9257
9410
  } else {
@@ -9259,7 +9412,7 @@ var PollCard = ({
9259
9412
  setOtherSelected((prev) => ({ ...prev, [questionId]: true }));
9260
9413
  }
9261
9414
  }, []);
9262
- const handleSubmit = useCallback7(() => {
9415
+ const handleSubmit = useCallback9(() => {
9263
9416
  const responses = questions.map((q) => {
9264
9417
  const selected = selections[q.id] || /* @__PURE__ */ new Set();
9265
9418
  const other = otherSelected[q.id] && otherTexts[q.id]?.trim();
@@ -9272,7 +9425,7 @@ var PollCard = ({
9272
9425
  });
9273
9426
  onSubmit(responses);
9274
9427
  }, [questions, selections, otherSelected, otherTexts, onSubmit]);
9275
- const handleSkip = useCallback7(() => {
9428
+ const handleSkip = useCallback9(() => {
9276
9429
  const responses = questions.map((q) => ({
9277
9430
  questionId: q.id,
9278
9431
  selectedOptions: [],
@@ -9320,7 +9473,7 @@ var PollCard = ({
9320
9473
  }
9321
9474
  return () => timers.forEach(clearTimeout);
9322
9475
  }, [activeTab, currentQuestion.options.length]);
9323
- const handleNext = useCallback7(() => {
9476
+ const handleNext = useCallback9(() => {
9324
9477
  if (!isLastTab) {
9325
9478
  setActiveTab((prev) => prev + 1);
9326
9479
  } else {
@@ -9738,7 +9891,7 @@ var SkillProgressUI = ({
9738
9891
  };
9739
9892
 
9740
9893
  // src/react/components/ImageContentCard.tsx
9741
- import { useState as useState13, useCallback as useCallback8 } from "react";
9894
+ import { useState as useState13, useCallback as useCallback10 } from "react";
9742
9895
  import { Fragment as Fragment6, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
9743
9896
  var ImageContentCard = ({ part }) => {
9744
9897
  const [isExpanded, setIsExpanded] = useState13(false);
@@ -9746,7 +9899,7 @@ var ImageContentCard = ({ part }) => {
9746
9899
  const [hasError, setHasError] = useState13(false);
9747
9900
  const [imgSrc, setImgSrc] = useState13(part.url);
9748
9901
  const onImageError = useImageError();
9749
- const handleImageError = useCallback8(async () => {
9902
+ const handleImageError = useCallback10(async () => {
9750
9903
  if (onImageError) {
9751
9904
  const newUrl = await onImageError(imgSrc, part.fileName);
9752
9905
  if (newUrl) {
@@ -10123,7 +10276,7 @@ var ToolStatusCard = ({
10123
10276
  };
10124
10277
 
10125
10278
  // src/react/components/ArtifactCard.tsx
10126
- import { useRef as useRef9, useEffect as useEffect9, useState as useState15, useCallback as useCallback9 } from "react";
10279
+ import { useRef as useRef11, useEffect as useEffect9, useState as useState15, useCallback as useCallback11 } from "react";
10127
10280
  import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
10128
10281
  var getThemeStyles = () => {
10129
10282
  const root = document.documentElement;
@@ -10164,9 +10317,9 @@ var getAutoResizeScript = (id) => `
10164
10317
  });
10165
10318
  </script>`;
10166
10319
  var HtmlArtifact = ({ code }) => {
10167
- const iframeRef = useRef9(null);
10320
+ const iframeRef = useRef11(null);
10168
10321
  const [height, setHeight] = useState15(200);
10169
- const artifactId = useRef9(`artifact-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`);
10322
+ const artifactId = useRef11(`artifact-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`);
10170
10323
  useEffect9(() => {
10171
10324
  const id = artifactId.current;
10172
10325
  const handleMessage = (e) => {
@@ -10213,10 +10366,10 @@ var SvgArtifact = ({ code }) => {
10213
10366
  );
10214
10367
  };
10215
10368
  var MermaidArtifact = ({ code }) => {
10216
- const containerRef = useRef9(null);
10369
+ const containerRef = useRef11(null);
10217
10370
  const [svgHtml, setSvgHtml] = useState15(null);
10218
10371
  const [error, setError] = useState15(null);
10219
- const renderMermaid = useCallback9(async () => {
10372
+ const renderMermaid = useCallback11(async () => {
10220
10373
  try {
10221
10374
  let mermaid;
10222
10375
  try {
@@ -10475,7 +10628,7 @@ var ArtifactActions = ({ code, language, containerRef }) => {
10475
10628
  ] });
10476
10629
  };
10477
10630
  var ArtifactCard = ({ part, index = 0 }) => {
10478
- const contentRef = useRef9(null);
10631
+ const contentRef = useRef11(null);
10479
10632
  return /* @__PURE__ */ jsxs13(
10480
10633
  "div",
10481
10634
  {
@@ -10647,7 +10800,7 @@ var ContentPartRenderer = ({
10647
10800
  };
10648
10801
 
10649
10802
  // src/react/components/ChecklistCard.tsx
10650
- import { useState as useState16, useMemo as useMemo5 } from "react";
10803
+ import { useState as useState16, useMemo as useMemo6 } from "react";
10651
10804
  import { jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
10652
10805
  var ChecklistCard = ({
10653
10806
  items,
@@ -10658,7 +10811,7 @@ var ChecklistCard = ({
10658
10811
  onStart
10659
10812
  }) => {
10660
10813
  const [expandedItems, setExpandedItems] = useState16(/* @__PURE__ */ new Set());
10661
- const { doneCount, isRunning, hasError, isWaiting } = useMemo5(() => {
10814
+ const { doneCount, isRunning, hasError, isWaiting } = useMemo6(() => {
10662
10815
  const done = items.filter((it) => it.status === "done").length;
10663
10816
  const running = items.some((it) => it.status === "in_progress");
10664
10817
  const waiting = items.every((it) => it.status === "pending");
@@ -11908,12 +12061,12 @@ var MessageList = ({
11908
12061
  onExport,
11909
12062
  onToggleChecklistPanel
11910
12063
  }) => {
11911
- const messagesEndRef = useRef10(null);
11912
- const containerRef = useRef10(null);
12064
+ const messagesEndRef = useRef12(null);
12065
+ const containerRef = useRef12(null);
11913
12066
  const [selectedText, setSelectedText] = useState18("");
11914
12067
  const [selectionPosition, setSelectionPosition] = useState18(null);
11915
12068
  const [showScrollButton, setShowScrollButton] = useState18(false);
11916
- const userScrollLockRef = useRef10(false);
12069
+ const userScrollLockRef = useRef12(false);
11917
12070
  const SCROLL_THRESHOLD = 100;
11918
12071
  useEffect10(() => {
11919
12072
  const container = containerRef.current;
@@ -11952,7 +12105,7 @@ var MessageList = ({
11952
12105
  container.removeEventListener("touchmove", handleTouchMove);
11953
12106
  };
11954
12107
  }, []);
11955
- const handleScroll = useCallback11(() => {
12108
+ const handleScroll = useCallback13(() => {
11956
12109
  if (!containerRef.current) return;
11957
12110
  const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
11958
12111
  const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
@@ -11968,7 +12121,7 @@ var MessageList = ({
11968
12121
  behavior: "smooth"
11969
12122
  });
11970
12123
  }, [messages]);
11971
- const scrollToBottom = useCallback11(() => {
12124
+ const scrollToBottom = useCallback13(() => {
11972
12125
  userScrollLockRef.current = false;
11973
12126
  setShowScrollButton(false);
11974
12127
  containerRef.current?.scrollTo({
@@ -11976,7 +12129,7 @@ var MessageList = ({
11976
12129
  behavior: "smooth"
11977
12130
  });
11978
12131
  }, []);
11979
- const handleMouseUp = useCallback11(() => {
12132
+ const handleMouseUp = useCallback13(() => {
11980
12133
  const selection = typeof window !== "undefined" ? window.getSelection() : null;
11981
12134
  const text = selection?.toString().trim();
11982
12135
  if (text && text.length > 0) {
@@ -15247,13 +15400,13 @@ var ChatUI = (props) => {
15247
15400
  };
15248
15401
 
15249
15402
  // src/react/ChatFloatingWidget.tsx
15250
- import { useState as useState28, useRef as useRef17, useEffect as useEffect19, useMemo as useMemo7, useCallback as useCallback16 } from "react";
15403
+ import { useState as useState28, useRef as useRef19, useEffect as useEffect19, useMemo as useMemo8, useCallback as useCallback18 } from "react";
15251
15404
 
15252
15405
  // src/react/hooks/useFloatingWidget.ts
15253
- import { useState as useState22, useCallback as useCallback13, useEffect as useEffect14, useRef as useRef12 } from "react";
15406
+ import { useState as useState22, useCallback as useCallback15, useEffect as useEffect14, useRef as useRef14 } from "react";
15254
15407
 
15255
15408
  // src/react/hooks/useDragResize.ts
15256
- import { useState as useState21, useRef as useRef11, useCallback as useCallback12, useEffect as useEffect13, useMemo as useMemo6 } from "react";
15409
+ import { useState as useState21, useRef as useRef13, useCallback as useCallback14, useEffect as useEffect13, useMemo as useMemo7 } from "react";
15257
15410
  var DRAG_THRESHOLD = 5;
15258
15411
  var FAB_SIZE = 56;
15259
15412
  var EDGE_MARGIN = 24;
@@ -15348,7 +15501,7 @@ var useDragResize = (options) => {
15348
15501
  }));
15349
15502
  const vw = viewport.width;
15350
15503
  const vh = viewport.height;
15351
- const initializedRef = useRef11(false);
15504
+ const initializedRef = useRef13(false);
15352
15505
  useEffect13(() => {
15353
15506
  if (initializedRef.current || typeof window === "undefined") return;
15354
15507
  initializedRef.current = true;
@@ -15362,26 +15515,26 @@ var useDragResize = (options) => {
15362
15515
  setFabPos({ x: loaded.fabX, y: loaded.fabY });
15363
15516
  setPanelSize({ width: loaded.panelWidth, height: loaded.panelHeight });
15364
15517
  }, [initialPosition, storageKey, initialWidth, initialHeight]);
15365
- const dragStartRef = useRef11(null);
15366
- const isDraggingRef = useRef11(false);
15367
- const wasDragRef = useRef11(false);
15368
- const userDraggedRef = useRef11(false);
15369
- const dragDeltaRef = useRef11({ dx: 0, dy: 0 });
15370
- const fabElRef = useRef11(null);
15371
- const dragCleanupRef = useRef11(null);
15372
- const snapTimerRef = useRef11(null);
15373
- const prevDragRef = useRef11(null);
15374
- const shakeScoreRef = useRef11(0);
15375
- const prevVelocityRef = useRef11(null);
15376
- const isDizzyRef = useRef11(false);
15377
- const dizzyTimerRef = useRef11(null);
15378
- const lastVelocityRef = useRef11({ vx: 0, vy: 0, speed: 0 });
15379
- const dragRafRef = useRef11(null);
15380
- const animCleanupRef = useRef11(null);
15381
- const snapEnabledRef = useRef11(snapEnabled);
15382
- const storageKeyRef = useRef11(storageKey);
15383
- const panelSizeRef = useRef11(panelSize);
15384
- const fabPosRef = useRef11(fabPos);
15518
+ const dragStartRef = useRef13(null);
15519
+ const isDraggingRef = useRef13(false);
15520
+ const wasDragRef = useRef13(false);
15521
+ const userDraggedRef = useRef13(false);
15522
+ const dragDeltaRef = useRef13({ dx: 0, dy: 0 });
15523
+ const fabElRef = useRef13(null);
15524
+ const dragCleanupRef = useRef13(null);
15525
+ const snapTimerRef = useRef13(null);
15526
+ const prevDragRef = useRef13(null);
15527
+ const shakeScoreRef = useRef13(0);
15528
+ const prevVelocityRef = useRef13(null);
15529
+ const isDizzyRef = useRef13(false);
15530
+ const dizzyTimerRef = useRef13(null);
15531
+ const lastVelocityRef = useRef13({ vx: 0, vy: 0, speed: 0 });
15532
+ const dragRafRef = useRef13(null);
15533
+ const animCleanupRef = useRef13(null);
15534
+ const snapEnabledRef = useRef13(snapEnabled);
15535
+ const storageKeyRef = useRef13(storageKey);
15536
+ const panelSizeRef = useRef13(panelSize);
15537
+ const fabPosRef = useRef13(fabPos);
15385
15538
  useEffect13(() => {
15386
15539
  snapEnabledRef.current = snapEnabled;
15387
15540
  }, [snapEnabled]);
@@ -15394,7 +15547,7 @@ var useDragResize = (options) => {
15394
15547
  useEffect13(() => {
15395
15548
  fabPosRef.current = fabPos;
15396
15549
  }, [fabPos]);
15397
- const resizeStartRef = useRef11(null);
15550
+ const resizeStartRef = useRef13(null);
15398
15551
  useEffect13(() => {
15399
15552
  return () => {
15400
15553
  dragCleanupRef.current?.();
@@ -15403,7 +15556,7 @@ var useDragResize = (options) => {
15403
15556
  if (dragRafRef.current) cancelAnimationFrame(dragRafRef.current);
15404
15557
  };
15405
15558
  }, []);
15406
- const handleFabPointerDown = useCallback12((e) => {
15559
+ const handleFabPointerDown = useCallback14((e) => {
15407
15560
  if (disabled || isMobile) return;
15408
15561
  dragCleanupRef.current?.();
15409
15562
  const el = e.currentTarget;
@@ -15585,7 +15738,7 @@ var useDragResize = (options) => {
15585
15738
  }, [disabled, isMobile]);
15586
15739
  const fabCenterX = fabPos.x + FAB_SIZE / 2;
15587
15740
  const panelDirection = fabCenterX > vw / 2 ? "left" : "right";
15588
- const panelPositionStyle = useMemo6(() => {
15741
+ const panelPositionStyle = useMemo7(() => {
15589
15742
  if (isMobile || disabled) return {};
15590
15743
  const fabIsTop = fabPos.y < vh / 2;
15591
15744
  let panelBottom;
@@ -15617,7 +15770,7 @@ var useDragResize = (options) => {
15617
15770
  borderRadius: "var(--floating-panel-radius, 16px)"
15618
15771
  };
15619
15772
  }, [isMobile, disabled, fabPos.x, fabPos.y, panelSize.width, panelSize.height, vw, vh, panelDirection, minHeight]);
15620
- const handleResizePointerDown = useCallback12((edge, e) => {
15773
+ const handleResizePointerDown = useCallback14((edge, e) => {
15621
15774
  if (disabled || isMobile) return;
15622
15775
  e.preventDefault();
15623
15776
  e.stopPropagation();
@@ -15633,7 +15786,7 @@ var useDragResize = (options) => {
15633
15786
  };
15634
15787
  setIsResizing(true);
15635
15788
  }, [disabled, isMobile]);
15636
- const handleResizePointerMove = useCallback12((e) => {
15789
+ const handleResizePointerMove = useCallback14((e) => {
15637
15790
  if (!resizeStartRef.current) return;
15638
15791
  const { startX, startY, width, height, edge, fabY } = resizeStartRef.current;
15639
15792
  const dx = e.clientX - startX;
@@ -15657,7 +15810,7 @@ var useDragResize = (options) => {
15657
15810
  newHeight = Math.max(minHeight, Math.min(maxH, newHeight));
15658
15811
  setPanelSize({ width: newWidth, height: newHeight });
15659
15812
  }, [minWidth, maxWidth, minHeight]);
15660
- const handleResizePointerUp = useCallback12((e) => {
15813
+ const handleResizePointerUp = useCallback14((e) => {
15661
15814
  if (!resizeStartRef.current) return;
15662
15815
  e.currentTarget.releasePointerCapture(e.pointerId);
15663
15816
  resizeStartRef.current = null;
@@ -15730,7 +15883,7 @@ var useFloatingWidget = (options) => {
15730
15883
  } = options || {};
15731
15884
  const [isOpen, setIsOpen] = useState22(defaultOpen);
15732
15885
  const [activeTab, setActiveTab] = useState22(defaultTab);
15733
- const panelRef = useRef12(null);
15886
+ const panelRef = useRef14(null);
15734
15887
  const dragResize = useDragResize({
15735
15888
  initialPosition: position,
15736
15889
  initialWidth: width,
@@ -15742,9 +15895,9 @@ var useFloatingWidget = (options) => {
15742
15895
  maxWidth,
15743
15896
  minHeight
15744
15897
  });
15745
- const onOpenRef = useRef12(onOpen);
15746
- const onCloseRef = useRef12(onClose);
15747
- const onTabChangeRef = useRef12(onTabChange);
15898
+ const onOpenRef = useRef14(onOpen);
15899
+ const onCloseRef = useRef14(onClose);
15900
+ const onTabChangeRef = useRef14(onTabChange);
15748
15901
  useEffect14(() => {
15749
15902
  onOpenRef.current = onOpen;
15750
15903
  }, [onOpen]);
@@ -15754,15 +15907,15 @@ var useFloatingWidget = (options) => {
15754
15907
  useEffect14(() => {
15755
15908
  onTabChangeRef.current = onTabChange;
15756
15909
  }, [onTabChange]);
15757
- const open = useCallback13(() => {
15910
+ const open = useCallback15(() => {
15758
15911
  setIsOpen(true);
15759
15912
  onOpenRef.current?.();
15760
15913
  }, []);
15761
- const close = useCallback13(() => {
15914
+ const close = useCallback15(() => {
15762
15915
  setIsOpen(false);
15763
15916
  onCloseRef.current?.();
15764
15917
  }, []);
15765
- const toggle = useCallback13(() => {
15918
+ const toggle = useCallback15(() => {
15766
15919
  setIsOpen((prev) => {
15767
15920
  const next = !prev;
15768
15921
  if (next) onOpenRef.current?.();
@@ -15770,11 +15923,11 @@ var useFloatingWidget = (options) => {
15770
15923
  return next;
15771
15924
  });
15772
15925
  }, []);
15773
- const setTab = useCallback13((tabKey) => {
15926
+ const setTab = useCallback15((tabKey) => {
15774
15927
  setActiveTab(tabKey);
15775
15928
  onTabChangeRef.current?.(tabKey);
15776
15929
  }, []);
15777
- const handleFabInteraction = useCallback13(() => {
15930
+ const handleFabInteraction = useCallback15(() => {
15778
15931
  if (dragResize.isDragging) return;
15779
15932
  toggle();
15780
15933
  }, [dragResize.isDragging, toggle]);
@@ -15804,10 +15957,10 @@ var useFloatingWidget = (options) => {
15804
15957
  };
15805
15958
 
15806
15959
  // src/react/components/floating/FloatingFab.tsx
15807
- import { useRef as useRef14, useState as useState24, useEffect as useEffect16 } from "react";
15960
+ import { useRef as useRef16, useState as useState24, useEffect as useEffect16 } from "react";
15808
15961
 
15809
15962
  // src/react/components/floating/DevDiveCharacter.tsx
15810
- import { useState as useState23, useEffect as useEffect15, useRef as useRef13, useCallback as useCallback14 } from "react";
15963
+ import { useState as useState23, useEffect as useEffect15, useRef as useRef15, useCallback as useCallback16 } from "react";
15811
15964
  import { Fragment as Fragment10, jsx as jsx24, jsxs as jsxs23 } from "react/jsx-runtime";
15812
15965
  var THEMES = {
15813
15966
  dark: { body: "#2ecc71", stroke: "#27ae60", highlight: "#3ddc84", face: "#1a1a2e", eyeLight: "#fff" },
@@ -15838,11 +15991,11 @@ var DevDiveFabCharacter = ({
15838
15991
  const [isWinking, setIsWinking] = useState23(false);
15839
15992
  const [isCatMouth, setIsCatMouth] = useState23(false);
15840
15993
  const [isEntering, setIsEntering] = useState23(true);
15841
- const svgRef = useRef13(null);
15842
- const cleanupRef = useRef13(null);
15843
- const lastActivityRef = useRef13(Date.now());
15844
- const isSleepyRef = useRef13(false);
15845
- const markActivity = useCallback14(() => {
15994
+ const svgRef = useRef15(null);
15995
+ const cleanupRef = useRef15(null);
15996
+ const lastActivityRef = useRef15(Date.now());
15997
+ const isSleepyRef = useRef15(false);
15998
+ const markActivity = useCallback16(() => {
15846
15999
  lastActivityRef.current = Date.now();
15847
16000
  if (isSleepyRef.current) {
15848
16001
  isSleepyRef.current = false;
@@ -15962,7 +16115,7 @@ var DevDiveFabCharacter = ({
15962
16115
  }, 5e3);
15963
16116
  return () => clearInterval(check);
15964
16117
  }, [isOpen, isTalking, isDizzy]);
15965
- const prevCompleteRef = useRef13(false);
16118
+ const prevCompleteRef = useRef15(false);
15966
16119
  useEffect15(() => {
15967
16120
  const wasComplete = prevCompleteRef.current;
15968
16121
  prevCompleteRef.current = isComplete;
@@ -16163,7 +16316,7 @@ var FloatingFab = ({
16163
16316
  }) => {
16164
16317
  const isRight = position.includes("right");
16165
16318
  const isBottom = position.includes("bottom");
16166
- const fabRef = useRef14(null);
16319
+ const fabRef = useRef16(null);
16167
16320
  const isCharacterMode = !icon;
16168
16321
  const [bubbleOnRight, setBubbleOnRight] = useState24(!isRight);
16169
16322
  const posLeft = positionStyle?.left;
@@ -16178,11 +16331,11 @@ var FloatingFab = ({
16178
16331
  }, [isRight, posLeft, posTop]);
16179
16332
  const [bubbleText, setBubbleText] = useState24(null);
16180
16333
  const [bubbleExiting, setBubbleExiting] = useState24(false);
16181
- const bubbleTextRef = useRef14(bubbleText);
16334
+ const bubbleTextRef = useRef16(bubbleText);
16182
16335
  bubbleTextRef.current = bubbleText;
16183
16336
  const [displayText, setDisplayText] = useState24(null);
16184
16337
  const [isTyping, setIsTyping] = useState24(false);
16185
- const typingTimerRef = useRef14(null);
16338
+ const typingTimerRef = useRef16(null);
16186
16339
  useEffect16(() => {
16187
16340
  if (notification) {
16188
16341
  setBubbleText(notification);
@@ -16219,7 +16372,7 @@ var FloatingFab = ({
16219
16372
  }, 300);
16220
16373
  return () => clearTimeout(timer);
16221
16374
  }, [notification]);
16222
- const notifContentRef = useRef14(null);
16375
+ const notifContentRef = useRef16(null);
16223
16376
  const [needsMarquee, setNeedsMarquee] = useState24(false);
16224
16377
  useEffect16(() => {
16225
16378
  if (isTyping || !notification) {
@@ -16363,7 +16516,7 @@ var FloatingFab = ({
16363
16516
  };
16364
16517
 
16365
16518
  // src/react/components/floating/FloatingPanel.tsx
16366
- import { useState as useState25, useEffect as useEffect17, useRef as useRef15 } from "react";
16519
+ import { useState as useState25, useEffect as useEffect17, useRef as useRef17 } from "react";
16367
16520
  import { jsxs as jsxs25 } from "react/jsx-runtime";
16368
16521
  var FloatingPanel = ({
16369
16522
  isOpen,
@@ -16392,7 +16545,7 @@ var FloatingPanel = ({
16392
16545
  }, []);
16393
16546
  const [shouldRender, setShouldRender] = useState25(isOpen);
16394
16547
  const [isVisible, setIsVisible] = useState25(isOpen);
16395
- const rafRef = useRef15(0);
16548
+ const rafRef = useRef17(0);
16396
16549
  useEffect17(() => {
16397
16550
  if (isOpen) {
16398
16551
  setShouldRender(true);
@@ -16569,10 +16722,10 @@ var FloatingTabBar = ({
16569
16722
  };
16570
16723
 
16571
16724
  // src/react/components/floating/CompactChatView.tsx
16572
- import { useState as useState27, useCallback as useCallback15 } from "react";
16725
+ import { useState as useState27, useCallback as useCallback17 } from "react";
16573
16726
 
16574
16727
  // src/react/components/floating/CompactSessionMenu.tsx
16575
- import { useState as useState26, useRef as useRef16, useEffect as useEffect18 } from "react";
16728
+ import { useState as useState26, useRef as useRef18, useEffect as useEffect18 } from "react";
16576
16729
  import { jsx as jsx27, jsxs as jsxs27 } from "react/jsx-runtime";
16577
16730
  var CompactSessionMenu = ({
16578
16731
  sessions,
@@ -16584,9 +16737,9 @@ var CompactSessionMenu = ({
16584
16737
  onClose,
16585
16738
  isLoading = false
16586
16739
  }) => {
16587
- const menuRef = useRef16(null);
16588
- const inputRef = useRef16(null);
16589
- const onCloseRef = useRef16(onClose);
16740
+ const menuRef = useRef18(null);
16741
+ const inputRef = useRef18(null);
16742
+ const onCloseRef = useRef18(onClose);
16590
16743
  onCloseRef.current = onClose;
16591
16744
  const [editingId, setEditingId] = useState26(null);
16592
16745
  const [editingTitle, setEditingTitle] = useState26("");
@@ -16915,15 +17068,15 @@ var CompactChatView = ({
16915
17068
  setInput(choice.text);
16916
17069
  };
16917
17070
  const [showSessionMenu, setShowSessionMenu] = useState27(false);
16918
- const handleSessionSelect = useCallback15((id) => {
17071
+ const handleSessionSelect = useCallback17((id) => {
16919
17072
  selectSession(id);
16920
17073
  setShowSessionMenu(false);
16921
17074
  }, [selectSession]);
16922
- const handleNewSession = useCallback15(() => {
17075
+ const handleNewSession = useCallback17(() => {
16923
17076
  newSession();
16924
17077
  setShowSessionMenu(false);
16925
17078
  }, [newSession]);
16926
- const handleCloseMenu = useCallback15(() => {
17079
+ const handleCloseMenu = useCallback17(() => {
16927
17080
  setShowSessionMenu(false);
16928
17081
  }, []);
16929
17082
  const greeting = personalization?.userProfile?.nickname ? `${personalization.userProfile.nickname}\uB2D8, \uBB34\uC5C7\uC774\uB4E0 \uBB3C\uC5B4\uBCF4\uC138\uC694` : "\uBB34\uC5C7\uC774\uB4E0 \uBB3C\uC5B4\uBCF4\uC138\uC694";
@@ -17243,11 +17396,11 @@ var ChatFloatingWidget = ({
17243
17396
  maxWidth,
17244
17397
  minHeight
17245
17398
  });
17246
- const notifObj = useMemo7(() => {
17399
+ const notifObj = useMemo8(() => {
17247
17400
  if (!notification) return null;
17248
17401
  return typeof notification === "string" ? { text: notification } : notification;
17249
17402
  }, [notification]);
17250
- const handleFabClick = useCallback16(() => {
17403
+ const handleFabClick = useCallback18(() => {
17251
17404
  if (notifObj?.onClick) {
17252
17405
  notifObj.onClick();
17253
17406
  if (!isOpen) handleFabInteraction();
@@ -17261,7 +17414,7 @@ var ChatFloatingWidget = ({
17261
17414
  }
17262
17415
  handleFabInteraction();
17263
17416
  }, [notifObj, isOpen, handleFabInteraction, setTab, tabs]);
17264
- const allTabs = useMemo7(() => {
17417
+ const allTabs = useMemo8(() => {
17265
17418
  const chatTab = {
17266
17419
  key: "chat",
17267
17420
  label: "\uCC44\uD305",
@@ -17276,7 +17429,7 @@ var ChatFloatingWidget = ({
17276
17429
  return [chatTab, ...customTabs];
17277
17430
  }, [tabs]);
17278
17431
  const [unreadBadge, setUnreadBadge] = useState28(0);
17279
- const seenMessageIdsRef = useRef17(/* @__PURE__ */ new Set());
17432
+ const seenMessageIdsRef = useRef19(/* @__PURE__ */ new Set());
17280
17433
  useEffect19(() => {
17281
17434
  if (seenMessageIdsRef.current.size === 0 && chatState.messages.length > 0) {
17282
17435
  for (const m of chatState.messages) {
@@ -17309,11 +17462,11 @@ var ChatFloatingWidget = ({
17309
17462
  }
17310
17463
  }
17311
17464
  }, [isOpen, chatState.messages]);
17312
- const totalBadge = useMemo7(() => {
17465
+ const totalBadge = useMemo8(() => {
17313
17466
  return tabs.reduce((sum, t) => sum + (t.badge || 0), 0) + unreadBadge;
17314
17467
  }, [tabs, unreadBadge]);
17315
- const isTalking = useMemo7(() => !!notification || chatState.isLoading, [notification, chatState.isLoading]);
17316
- const prevLoadingRef = useRef17(chatState.isLoading);
17468
+ const isTalking = useMemo8(() => !!notification || chatState.isLoading, [notification, chatState.isLoading]);
17469
+ const prevLoadingRef = useRef19(chatState.isLoading);
17317
17470
  const [isComplete, setIsComplete] = useState28(false);
17318
17471
  useEffect19(() => {
17319
17472
  const wasLoading = prevLoadingRef.current;
@@ -17431,7 +17584,7 @@ var ChatFloatingWidget = ({
17431
17584
  };
17432
17585
 
17433
17586
  // src/react/hooks/useDeepResearch.ts
17434
- import { useState as useState29, useCallback as useCallback17, useRef as useRef18 } from "react";
17587
+ import { useState as useState29, useCallback as useCallback19, useRef as useRef20 } from "react";
17435
17588
  var REPORT_GENERATION_PROMPT2 = `\uB2F9\uC2E0\uC740 \uB9AC\uC11C\uCE58 \uBCF4\uACE0\uC11C \uC791\uC131 \uC804\uBB38\uAC00\uC785\uB2C8\uB2E4.
17436
17589
 
17437
17590
  <collected_sources>
@@ -17484,8 +17637,8 @@ var useDeepResearch = (options) => {
17484
17637
  const { onWebSearch, onExtractContent, apiEndpoint, apiKey, model, provider } = options;
17485
17638
  const [isResearching, setIsResearching] = useState29(false);
17486
17639
  const [progress, setProgress] = useState29(null);
17487
- const abortControllerRef = useRef18(null);
17488
- const callLLM2 = useCallback17(
17640
+ const abortControllerRef = useRef20(null);
17641
+ const callLLM2 = useCallback19(
17489
17642
  async (prompt, stream = false) => {
17490
17643
  const response = await fetch(apiEndpoint, {
17491
17644
  method: "POST",
@@ -17512,7 +17665,7 @@ var useDeepResearch = (options) => {
17512
17665
  },
17513
17666
  [apiEndpoint, apiKey, model, provider]
17514
17667
  );
17515
- const analyzeQuery2 = useCallback17(
17668
+ const analyzeQuery2 = useCallback19(
17516
17669
  async (query) => {
17517
17670
  const prompt = QUERY_ANALYSIS_PROMPT2.replace("{question}", query);
17518
17671
  const response = await callLLM2(prompt);
@@ -17531,7 +17684,7 @@ var useDeepResearch = (options) => {
17531
17684
  },
17532
17685
  [callLLM2]
17533
17686
  );
17534
- const runSubAgent2 = useCallback17(
17687
+ const runSubAgent2 = useCallback19(
17535
17688
  async (topic, queries, agentId, updateProgress) => {
17536
17689
  updateProgress({ status: "searching", searchCount: 0, resultsCount: 0 });
17537
17690
  const allResults = [];
@@ -17570,7 +17723,7 @@ var useDeepResearch = (options) => {
17570
17723
  },
17571
17724
  [onWebSearch, onExtractContent]
17572
17725
  );
17573
- const generateReport2 = useCallback17(
17726
+ const generateReport2 = useCallback19(
17574
17727
  async (query, results, onStreamContent) => {
17575
17728
  const allSources = [];
17576
17729
  const sourcesForPrompt = [];
@@ -17628,7 +17781,7 @@ var useDeepResearch = (options) => {
17628
17781
  },
17629
17782
  [callLLM2]
17630
17783
  );
17631
- const runDeepResearch = useCallback17(
17784
+ const runDeepResearch = useCallback19(
17632
17785
  async (query, onStreamContent) => {
17633
17786
  abortControllerRef.current = new AbortController();
17634
17787
  setIsResearching(true);
@@ -17731,7 +17884,7 @@ var useDeepResearch = (options) => {
17731
17884
  },
17732
17885
  [analyzeQuery2, runSubAgent2, generateReport2]
17733
17886
  );
17734
- const stopResearch = useCallback17(() => {
17887
+ const stopResearch = useCallback19(() => {
17735
17888
  abortControllerRef.current?.abort();
17736
17889
  setIsResearching(false);
17737
17890
  setProgress(null);
@@ -17744,6 +17897,122 @@ var useDeepResearch = (options) => {
17744
17897
  };
17745
17898
  };
17746
17899
 
17900
+ // src/react/hooks/useContentParsers.ts
17901
+ import { useCallback as useCallback20 } from "react";
17902
+ var useContentParsers = ({
17903
+ sessionsRef,
17904
+ setSessions
17905
+ }) => {
17906
+ const applyPollParsing = useCallback20(
17907
+ (sessionId, messageId, content, skipParsing) => {
17908
+ const { pollBlock, cleanContent } = parsePollFromContent(content);
17909
+ setSessions(
17910
+ (prev) => prev.map((s) => {
17911
+ if (s.id !== sessionId) return s;
17912
+ return {
17913
+ ...s,
17914
+ messages: s.messages.map((m) => {
17915
+ if (m.id !== messageId) return m;
17916
+ if (skipParsing) {
17917
+ return { ...m, content: cleanContent };
17918
+ }
17919
+ if (pollBlock) {
17920
+ return { ...m, content: cleanContent, pollBlock };
17921
+ }
17922
+ return m;
17923
+ })
17924
+ };
17925
+ })
17926
+ );
17927
+ return skipParsing ? null : pollBlock;
17928
+ },
17929
+ [setSessions]
17930
+ );
17931
+ const applyChecklistParsing = useCallback20(
17932
+ (sessionId, messageId, content, skipParsing) => {
17933
+ if (skipParsing) return null;
17934
+ const { checklistBlock, cleanContent } = parseChecklistFromContent(content);
17935
+ if (!checklistBlock) return null;
17936
+ setSessions(
17937
+ (prev) => prev.map((s) => {
17938
+ if (s.id !== sessionId) return s;
17939
+ return {
17940
+ ...s,
17941
+ messages: s.messages.map((m) => {
17942
+ if (m.id !== messageId) return m;
17943
+ return { ...m, content: cleanContent, checklistBlock };
17944
+ })
17945
+ };
17946
+ })
17947
+ );
17948
+ return checklistBlock;
17949
+ },
17950
+ [setSessions]
17951
+ );
17952
+ const applyArtifactParsing = useCallback20(
17953
+ (sessionId, messageId, content) => {
17954
+ if (!hasArtifactTag(content)) return false;
17955
+ let found = false;
17956
+ setSessions(
17957
+ (prev) => prev.map((s) => {
17958
+ if (s.id !== sessionId) return s;
17959
+ return {
17960
+ ...s,
17961
+ messages: s.messages.map((m) => {
17962
+ if (m.id !== messageId) return m;
17963
+ const { artifacts, cleanContent } = parseArtifactsFromContent(m.content);
17964
+ if (artifacts.length === 0) return m;
17965
+ found = true;
17966
+ const existingParts = m.contentParts || [];
17967
+ const textPart = cleanContent.trim() ? [{ type: "text", content: cleanContent }] : [];
17968
+ return {
17969
+ ...m,
17970
+ content: cleanContent,
17971
+ contentParts: [
17972
+ ...textPart,
17973
+ ...existingParts.filter(
17974
+ (p) => p.type !== "text" && p.type !== "artifact"
17975
+ ),
17976
+ ...artifacts
17977
+ ]
17978
+ };
17979
+ })
17980
+ };
17981
+ })
17982
+ );
17983
+ return found;
17984
+ },
17985
+ [setSessions]
17986
+ );
17987
+ const parseContextReferences = useCallback20(
17988
+ (content, sessionContext) => {
17989
+ const { refs, cleanContent } = parseContextRefs(content);
17990
+ if (refs.length === 0 || !sessionContext) {
17991
+ return { refs, cleanContent, refContents: null };
17992
+ }
17993
+ const assembled = refs.map((refId) => {
17994
+ const item = sessionContext.references.find(
17995
+ (r) => r.id === refId
17996
+ );
17997
+ return item ? `[${item.label || item.skillName}]
17998
+ ${item.summary}` : null;
17999
+ }).filter(Boolean).join("\n\n");
18000
+ return {
18001
+ refs,
18002
+ cleanContent,
18003
+ refContents: assembled || null
18004
+ };
18005
+ },
18006
+ []
18007
+ );
18008
+ return {
18009
+ applyPollParsing,
18010
+ applyChecklistParsing,
18011
+ applyArtifactParsing,
18012
+ parseContextReferences
18013
+ };
18014
+ };
18015
+
17747
18016
  // src/react/utils/conversationSearchAdapter.ts
17748
18017
  var formatSearchResults = (results) => {
17749
18018
  if (results.length === 0) {
@@ -18271,8 +18540,52 @@ var MemoryPanel = ({
18271
18540
  }
18272
18541
  );
18273
18542
  };
18543
+
18544
+ // src/react/utils/retry.ts
18545
+ var DEFAULT_RETRY_CONFIG = {
18546
+ maxRetries: 3,
18547
+ initialDelayMs: 1e3,
18548
+ maxDelayMs: 3e4,
18549
+ backoffMultiplier: 2
18550
+ };
18551
+ var calculateDelay = (attempt, config) => {
18552
+ const exponential = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt);
18553
+ const capped = Math.min(exponential, config.maxDelayMs);
18554
+ const jitter = 0.5 + Math.random() * 0.5;
18555
+ return Math.floor(capped * jitter);
18556
+ };
18557
+ var sleep = (ms, signal) => new Promise((resolve, reject) => {
18558
+ if (signal?.aborted) {
18559
+ reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
18560
+ return;
18561
+ }
18562
+ const timer = setTimeout(resolve, ms);
18563
+ signal?.addEventListener("abort", () => {
18564
+ clearTimeout(timer);
18565
+ reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
18566
+ }, { once: true });
18567
+ });
18568
+ var withRetry = async (fn, config, signal) => {
18569
+ const merged = { ...DEFAULT_RETRY_CONFIG, ...config };
18570
+ let lastError;
18571
+ for (let attempt = 0; attempt <= merged.maxRetries; attempt++) {
18572
+ try {
18573
+ return await fn();
18574
+ } catch (error) {
18575
+ lastError = error;
18576
+ if (error instanceof DOMException && error.name === "AbortError") throw error;
18577
+ if (error instanceof Error && error.name === "AbortError") throw error;
18578
+ if (signal?.aborted) throw error;
18579
+ if (attempt >= merged.maxRetries) throw error;
18580
+ if (error instanceof ChatError && !error.retryable) throw error;
18581
+ await sleep(calculateDelay(attempt, merged), signal);
18582
+ }
18583
+ }
18584
+ throw lastError;
18585
+ };
18274
18586
  export {
18275
18587
  ArtifactCard,
18588
+ ChatError,
18276
18589
  ChatFloatingWidget,
18277
18590
  ChatHeader,
18278
18591
  ChatInput,
@@ -18285,6 +18598,7 @@ export {
18285
18598
  ContentPartRenderer,
18286
18599
  DEFAULT_PROJECT_ID,
18287
18600
  DEFAULT_PROJECT_TITLE,
18601
+ DEFAULT_RETRY_CONFIG,
18288
18602
  DeepResearchProgressUI,
18289
18603
  DevDiveAvatar,
18290
18604
  DevDiveFabCharacter,
@@ -18308,20 +18622,28 @@ export {
18308
18622
  ResizeHandles,
18309
18623
  SettingsModal,
18310
18624
  SkillProgressUI,
18625
+ classifyFetchError,
18311
18626
  convertSkillsToOpenAITools,
18312
18627
  convertToolsToSkills,
18313
18628
  createAdvancedResearchSkill,
18314
18629
  createConversationSearchSkill,
18315
18630
  createDeepResearchSkill,
18631
+ createTimeoutError,
18316
18632
  migrateSessionsToProjects,
18633
+ parseSSELine,
18634
+ parseSSEResponse,
18317
18635
  useChatUI,
18636
+ useChecklist,
18637
+ useContentParsers,
18318
18638
  useDeepResearch,
18319
18639
  useDragResize,
18320
18640
  useFloatingWidget,
18321
18641
  useImageError,
18322
18642
  useObserver,
18323
18643
  useProject,
18324
- useSkills
18644
+ useSkills,
18645
+ useStreamingFetch,
18646
+ withRetry
18325
18647
  };
18326
18648
  /**
18327
18649
  * @description localStorage 기반 메모리 저장소 어댑터