@gendive/chatllm 0.22.1 → 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;
@@ -2249,6 +2681,12 @@ var useChatUI = (options) => {
2249
2681
  onSendMessage,
2250
2682
  onSessionChange,
2251
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,
2252
2690
  onTitleChange,
2253
2691
  generateTitle: generateTitleCallback,
2254
2692
  // Memory options
@@ -2315,8 +2753,8 @@ var useChatUI = (options) => {
2315
2753
  const [input, setInput] = useState6("");
2316
2754
  const [loadingSessionIds, setLoadingSessionIds] = useState6(/* @__PURE__ */ new Set());
2317
2755
  const isLoading = currentSessionId !== null && loadingSessionIds.has(currentSessionId);
2318
- const addLoadingSession = useCallback6((id) => setLoadingSessionIds((prev) => new Set(prev).add(id)), []);
2319
- 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) => {
2320
2758
  const next = new Set(prev);
2321
2759
  next.delete(id);
2322
2760
  return next;
@@ -2343,41 +2781,45 @@ var useChatUI = (options) => {
2343
2781
  const [deepResearchProgress, setDeepResearchProgress] = useState6(
2344
2782
  null
2345
2783
  );
2346
- const sessionsRef = useRef5(sessions);
2784
+ const sessionsRef = useRef7(sessions);
2347
2785
  useEffect4(() => {
2348
2786
  sessionsRef.current = sessions;
2349
2787
  }, [sessions]);
2350
- const modelsRef = useRef5(models);
2788
+ const modelsRef = useRef7(models);
2351
2789
  useEffect4(() => {
2352
2790
  modelsRef.current = models;
2353
2791
  }, [models]);
2354
- const onSendMessageRef = useRef5(onSendMessage);
2355
- const onResponseHeadersRef = useRef5(options.onResponseHeaders);
2356
- const onSessionChangeRef = useRef5(onSessionChange);
2357
- const onErrorRef = useRef5(onError);
2358
- const onTitleChangeRef = useRef5(onTitleChange);
2359
- const generateTitleRef = useRef5(generateTitleCallback);
2360
- const onPersonalizationChangeRef = useRef5(options.onPersonalizationChange);
2361
- const onPersonalizationSaveRef = useRef5(options.onPersonalizationSave);
2362
- const onLoadSessionsRef = useRef5(onLoadSessions);
2363
- const onCreateSessionRef = useRef5(onCreateSession);
2364
- const onLoadSessionRef = useRef5(onLoadSession);
2365
- const onDeleteSessionCallbackRef = useRef5(onDeleteSessionCallback);
2366
- const onUpdateSessionTitleRef = useRef5(onUpdateSessionTitle);
2367
- const onSaveMessagesRef = useRef5(onSaveMessages);
2368
- const onToolCallRef = useRef5(onToolCall);
2369
- const onSkillCompleteRef = useRef5(onSkillComplete);
2370
- const onSessionContextChangeRef = useRef5(onSessionContextChange);
2371
- const onLoadModelsRef = useRef5(onLoadModels);
2372
- const onUploadImageRef = useRef5(onUploadImage);
2373
- const onImageErrorRef = useRef5(onImageError);
2374
- const fileUploaderRef = useRef5(fileUploader);
2375
- 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);
2376
2816
  useEffect4(() => {
2377
2817
  onSendMessageRef.current = onSendMessage;
2378
2818
  onResponseHeadersRef.current = options.onResponseHeaders;
2379
2819
  onSessionChangeRef.current = onSessionChange;
2380
2820
  onErrorRef.current = onError;
2821
+ onAbortRef.current = onAbort;
2822
+ onTokenUsageRef.current = onTokenUsage;
2381
2823
  onTitleChangeRef.current = onTitleChange;
2382
2824
  generateTitleRef.current = generateTitleCallback;
2383
2825
  onPersonalizationChangeRef.current = options.onPersonalizationChange;
@@ -2396,15 +2838,19 @@ var useChatUI = (options) => {
2396
2838
  fileUploaderRef.current = fileUploader;
2397
2839
  onLoadModelsRef.current = onLoadModels;
2398
2840
  });
2399
- const abortControllersRef = useRef5(/* @__PURE__ */ new Map());
2400
- const pendingInitialLoadRef = useRef5(null);
2401
- const skipNextPollParsingRef = useRef5(false);
2402
- const skipNextSkillParsingRef = useRef5(false);
2403
- const skipNextChecklistParsingRef = useRef5(false);
2404
- const activeChecklistRef = useRef5(null);
2405
- const pendingChecklistRef = useRef5(null);
2406
- const pendingAttachmentDataRef = useRef5(null);
2407
- 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);
2408
2854
  const resolveChecklistRefImage = (item, checklistMessageId, sessionId) => {
2409
2855
  const session = sessionsRef.current.find((s) => s.id === sessionId);
2410
2856
  if (!session) return null;
@@ -2471,7 +2917,7 @@ ${hints.join(" ")}` : "";
2471
2917
  return `${stepLabel}
2472
2918
  ${focusHint}${hintLine} checklist \uD0DC\uADF8\uB294 \uC0AC\uC6A9\uD558\uC9C0 \uB9C8\uC138\uC694.`;
2473
2919
  };
2474
- const memoryOptions = useMemo3(
2920
+ const memoryOptions = useMemo4(
2475
2921
  () => ({
2476
2922
  storageType: globalMemoryConfig?.storageType || "localStorage",
2477
2923
  storageKey: globalMemoryConfig?.localStorage?.key || `${storageKey}_memory`,
@@ -2485,11 +2931,11 @@ ${focusHint}${hintLine} checklist \uD0DC\uADF8\uB294 \uC0AC\uC6A9\uD558\uC9C0 \u
2485
2931
  const globalMemoryRaw = useGlobalMemory(memoryOptions);
2486
2932
  const globalMemory = useGlobalMemoryEnabled ? globalMemoryRaw : null;
2487
2933
  globalMemoryRef.current = globalMemory;
2488
- const stableToolCall = useCallback6(
2934
+ const stableToolCall = useCallback8(
2489
2935
  (name, params) => onToolCallRef.current(name, params),
2490
2936
  []
2491
2937
  );
2492
- const mergedSkills = useMemo3(() => {
2938
+ const mergedSkills = useMemo4(() => {
2493
2939
  if (!tools || !onToolCall) return skills || {};
2494
2940
  const toolSkills = convertToolsToSkills(tools, stableToolCall);
2495
2941
  return { ...skills || {}, ...toolSkills };
@@ -2527,7 +2973,7 @@ ${focusHint}${hintLine} checklist \uD0DC\uADF8\uB294 \uC0AC\uC6A9\uD558\uC9C0 \u
2527
2973
  });
2528
2974
  const [projectSettingsOpen, setProjectSettingsOpen] = useState6(false);
2529
2975
  const projectMemoryKey = enableProjects && projectHook.currentProjectId ? `${storageKey}_project_memory_${projectHook.currentProjectId}` : `${storageKey}_project_memory_none`;
2530
- const projectMemoryOptions = useMemo3(
2976
+ const projectMemoryOptions = useMemo4(
2531
2977
  () => ({
2532
2978
  storageType: globalMemoryConfig?.storageType || "localStorage",
2533
2979
  storageKey: projectMemoryKey,
@@ -2539,7 +2985,7 @@ ${focusHint}${hintLine} checklist \uD0DC\uADF8\uB294 \uC0AC\uC6A9\uD558\uC9C0 \u
2539
2985
  );
2540
2986
  const projectMemoryRaw = useGlobalMemory(projectMemoryOptions);
2541
2987
  const projectMemory = enableProjects ? projectMemoryRaw : null;
2542
- const unwrapResponseHeaders = useCallback6((result) => {
2988
+ const unwrapResponseHeaders = useCallback8((result) => {
2543
2989
  if (typeof result === "object" && result !== null && "response" in result && "headers" in result) {
2544
2990
  const wrapped = result;
2545
2991
  onResponseHeadersRef.current?.(wrapped.headers);
@@ -2547,7 +2993,7 @@ ${focusHint}${hintLine} checklist \uD0DC\uADF8\uB294 \uC0AC\uC6A9\uD558\uC9C0 \u
2547
2993
  }
2548
2994
  return result;
2549
2995
  }, []);
2550
- const emitFetchHeaders = useCallback6((response) => {
2996
+ const emitFetchHeaders = useCallback8((response) => {
2551
2997
  if (onResponseHeadersRef.current && response.headers) {
2552
2998
  const headerMap = {};
2553
2999
  response.headers.forEach((v, k) => {
@@ -2556,7 +3002,7 @@ ${focusHint}${hintLine} checklist \uD0DC\uADF8\uB294 \uC0AC\uC6A9\uD558\uC9C0 \u
2556
3002
  onResponseHeadersRef.current(headerMap);
2557
3003
  }
2558
3004
  }, []);
2559
- const callInternalLLM = useCallback6(async (prompt, model) => {
3005
+ const callInternalLLM = useCallback8(async (prompt, model) => {
2560
3006
  if (onSendMessageRef.current) {
2561
3007
  const modelConfig = modelsRef.current.find((m) => m.id === model);
2562
3008
  const provider = modelConfig?.provider || "ollama";
@@ -2594,7 +3040,7 @@ ${focusHint}${hintLine} checklist \uD0DC\uADF8\uB294 \uC0AC\uC6A9\uD558\uC9C0 \u
2594
3040
  const currentSession = sessions.find((s) => s.id === currentSessionId) || null;
2595
3041
  const messages = currentSession?.messages.filter((m) => !m.hidden) || [];
2596
3042
  const compressionState = currentSession?.compressionState || null;
2597
- const visibleSessions = useMemo3(() => {
3043
+ const visibleSessions = useMemo4(() => {
2598
3044
  if (!enableProjects || !projectHook.currentProjectId) return sessions;
2599
3045
  return sessions.filter((s) => s.projectId === projectHook.currentProjectId);
2600
3046
  }, [sessions, enableProjects, projectHook.currentProjectId]);
@@ -2689,7 +3135,7 @@ ${focusHint}${hintLine} checklist \uD0DC\uADF8\uB294 \uC0AC\uC6A9\uD558\uC9C0 \u
2689
3135
  useEffect4(() => {
2690
3136
  onSessionChangeRef.current?.(currentSession);
2691
3137
  }, [currentSession]);
2692
- const buildSystemPrompt = useCallback6((session) => {
3138
+ const buildSystemPrompt = useCallback8((session) => {
2693
3139
  const parts = [];
2694
3140
  const { userProfile, responseStyle, language } = personalization;
2695
3141
  const identityName = assistantName || "AI \uC5B4\uC2DC\uC2A4\uD134\uD2B8";
@@ -3048,7 +3494,7 @@ AI (\uD655\uC815):
3048
3494
  }
3049
3495
  return parts.length > 0 ? parts.join("\n") : "";
3050
3496
  }, [personalization, globalMemory, useGlobalMemoryEnabled, enablePoll, enableChecklist, buildSkillsPrompt, enableProjects, projectHook.currentProject, projectMemory, resolvedSkills, assistantName, onBuildSystemPrompt, compactSystemPrompt, selectedModel]);
3051
- const promoteToSessionContext = useCallback6((sessionId, skillName, content, metadata, label) => {
3497
+ const promoteToSessionContext = useCallback8((sessionId, skillName, content, metadata, label) => {
3052
3498
  const item = createSessionContextItem(skillName, content, metadata, label);
3053
3499
  setSessions(
3054
3500
  (prev) => prev.map((s) => {
@@ -3059,7 +3505,7 @@ AI (\uD655\uC815):
3059
3505
  })
3060
3506
  );
3061
3507
  }, []);
3062
- const compressContext = useCallback6(async (messagesToCompress, model) => {
3508
+ const compressContext = useCallback8(async (messagesToCompress, model) => {
3063
3509
  const conversationText = messagesToCompress.map((m) => `${m.role === "user" ? "\uC0AC\uC6A9\uC790" : "AI"}: ${m.content}`).join("\n\n");
3064
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.
3065
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.
@@ -3074,7 +3520,7 @@ ${conversationText}
3074
3520
  return "";
3075
3521
  }
3076
3522
  }, [callInternalLLM]);
3077
- const incrementalCompressContext = useCallback6(
3523
+ const incrementalCompressContext = useCallback8(
3078
3524
  async (existingSummary, newMessages, model) => {
3079
3525
  const newConversation = newMessages.map((m) => `${m.role === "user" ? "\uC0AC\uC6A9\uC790" : "AI"}: ${m.content}`).join("\n\n");
3080
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.
@@ -3101,10 +3547,10 @@ ${newConversation}
3101
3547
  },
3102
3548
  [callInternalLLM]
3103
3549
  );
3104
- const estimateTokens = useCallback6((messages2) => {
3550
+ const estimateTokens = useCallback8((messages2) => {
3105
3551
  return messages2.reduce((sum, m) => sum + Math.ceil(m.content.length / 4), 0);
3106
3552
  }, []);
3107
- const newSession = useCallback6(async () => {
3553
+ const newSession = useCallback8(async () => {
3108
3554
  const projectId = enableProjects ? projectHook.currentProjectId || DEFAULT_PROJECT_ID : void 0;
3109
3555
  if (useExternalStorage && onCreateSessionRef.current) {
3110
3556
  setIsSessionLoading(true);
@@ -3142,7 +3588,7 @@ ${newConversation}
3142
3588
  setSessions((prev) => [newSess, ...prev]);
3143
3589
  setCurrentSessionId(newSess.id);
3144
3590
  }, [selectedModel, useExternalStorage, enableProjects, projectHook.currentProjectId]);
3145
- const selectSession = useCallback6(async (id) => {
3591
+ const selectSession = useCallback8(async (id) => {
3146
3592
  if (useExternalStorage && onLoadSessionRef.current) {
3147
3593
  setIsSessionLoading(true);
3148
3594
  try {
@@ -3252,7 +3698,7 @@ ${newConversation}
3252
3698
  pendingInitialLoadRef.current = null;
3253
3699
  selectSession(id);
3254
3700
  }, [currentSessionId, selectSession, useExternalStorage]);
3255
- const deleteSession = useCallback6(async (id) => {
3701
+ const deleteSession = useCallback8(async (id) => {
3256
3702
  if (useExternalStorage && onDeleteSessionCallbackRef.current) {
3257
3703
  try {
3258
3704
  await onDeleteSessionCallbackRef.current(id);
@@ -3280,7 +3726,7 @@ ${newConversation}
3280
3726
  return filtered;
3281
3727
  });
3282
3728
  }, [currentSessionId, storageKey, useExternalStorage]);
3283
- const renameSession = useCallback6(async (id, newTitle) => {
3729
+ const renameSession = useCallback8(async (id, newTitle) => {
3284
3730
  if (!newTitle.trim()) return;
3285
3731
  if (useExternalStorage && onUpdateSessionTitleRef.current) {
3286
3732
  try {
@@ -3303,7 +3749,7 @@ ${newConversation}
3303
3749
  );
3304
3750
  onTitleChangeRef.current?.(id, newTitle.trim());
3305
3751
  }, [useExternalStorage]);
3306
- const setModel = useCallback6((model) => {
3752
+ const setModel = useCallback8((model) => {
3307
3753
  setSelectedModel(model);
3308
3754
  if (currentSessionId) {
3309
3755
  setSessions(
@@ -3311,10 +3757,10 @@ ${newConversation}
3311
3757
  );
3312
3758
  }
3313
3759
  }, [currentSessionId]);
3314
- const toggleSidebar = useCallback6(() => setSidebarOpen((prev) => !prev), []);
3315
- const openSettings = useCallback6(() => setSettingsOpen(true), []);
3316
- const closeSettings = useCallback6(() => setSettingsOpen(false), []);
3317
- 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) => {
3318
3764
  if (typeof navigator !== "undefined") {
3319
3765
  navigator.clipboard.writeText(content).then(() => {
3320
3766
  setCopiedMessageId(id);
@@ -3322,30 +3768,30 @@ ${newConversation}
3322
3768
  });
3323
3769
  }
3324
3770
  }, []);
3325
- const startEdit = useCallback6((message) => {
3771
+ const startEdit = useCallback8((message) => {
3326
3772
  if (message.role === "user") {
3327
3773
  setEditingMessageId(message.id);
3328
3774
  }
3329
3775
  }, []);
3330
- const cancelEdit = useCallback6(() => {
3776
+ const cancelEdit = useCallback8(() => {
3331
3777
  setEditingMessageId(null);
3332
3778
  }, []);
3333
- const stopGeneration = useCallback6(() => {
3779
+ const stopGeneration = useCallback8(() => {
3334
3780
  if (currentSessionId) {
3335
3781
  abortControllersRef.current.get(currentSessionId)?.abort();
3336
3782
  }
3337
3783
  }, [currentSessionId]);
3338
- const updatePersonalization = useCallback6((config) => {
3784
+ const updatePersonalization = useCallback8((config) => {
3339
3785
  setPersonalization((prev) => {
3340
3786
  const next = { ...prev, ...config };
3341
3787
  onPersonalizationChangeRef.current?.(next);
3342
3788
  return next;
3343
3789
  });
3344
3790
  }, []);
3345
- const savePersonalization = useCallback6(() => {
3791
+ const savePersonalization = useCallback8(() => {
3346
3792
  onPersonalizationSaveRef.current?.(personalization);
3347
3793
  }, [personalization]);
3348
- const addAttachments = useCallback6((files) => {
3794
+ const addAttachments = useCallback8((files) => {
3349
3795
  const newAttachments = files.map((file) => {
3350
3796
  const isImage = file.type.startsWith("image/");
3351
3797
  return {
@@ -3360,7 +3806,7 @@ ${newConversation}
3360
3806
  });
3361
3807
  setAttachments((prev) => [...prev, ...newAttachments]);
3362
3808
  }, []);
3363
- const removeAttachment = useCallback6((id) => {
3809
+ const removeAttachment = useCallback8((id) => {
3364
3810
  setAttachments((prev) => {
3365
3811
  const target = prev.find((a) => a.id === id);
3366
3812
  if (target?.previewUrl) {
@@ -3369,17 +3815,17 @@ ${newConversation}
3369
3815
  return prev.filter((a) => a.id !== id);
3370
3816
  });
3371
3817
  }, []);
3372
- const toggleDeepResearchMode = useCallback6(() => {
3818
+ const toggleDeepResearchMode = useCallback8(() => {
3373
3819
  setIsDeepResearchMode((prev) => !prev);
3374
3820
  }, []);
3375
- const trackBehavior = useCallback6(async (key, updater) => {
3821
+ const trackBehavior = useCallback8(async (key, updater) => {
3376
3822
  if (!globalMemory) return;
3377
3823
  const fullKey = `behavior.${key}`;
3378
3824
  const prev = globalMemory.get(fullKey) ?? {};
3379
3825
  const next = updater(prev);
3380
3826
  await globalMemory.set(fullKey, next, { category: "behavior" });
3381
3827
  }, [globalMemory]);
3382
- const trackMessageLength = useCallback6((length) => {
3828
+ const trackMessageLength = useCallback8((length) => {
3383
3829
  trackBehavior("messageLength", (prev) => {
3384
3830
  const samples = (prev.messageLengthSamples || []).slice(-19);
3385
3831
  samples.push(length);
@@ -3387,39 +3833,69 @@ ${newConversation}
3387
3833
  return { ...prev, messageLengthSamples: samples, avgMessageLength: avg };
3388
3834
  });
3389
3835
  }, [trackBehavior]);
3390
- const trackSkillUsage = useCallback6((skillName) => {
3836
+ const trackSkillUsage = useCallback8((skillName) => {
3391
3837
  trackBehavior("skillUsage", (prev) => ({
3392
3838
  ...prev,
3393
3839
  [skillName]: (prev[skillName] || 0) + 1
3394
3840
  }));
3395
3841
  }, [trackBehavior]);
3396
- const trackChecklistSkip = useCallback6(() => {
3842
+ const trackChecklistSkip = useCallback8(() => {
3397
3843
  trackBehavior("checklistSkipRate", (prev) => ({
3398
3844
  total: (prev.total || 0) + 1,
3399
3845
  skipped: (prev.skipped || 0) + 1
3400
3846
  }));
3401
3847
  }, [trackBehavior]);
3402
- const trackChecklistComplete = useCallback6(() => {
3848
+ const trackChecklistComplete = useCallback8(() => {
3403
3849
  trackBehavior("checklistSkipRate", (prev) => ({
3404
3850
  total: (prev.total || 0) + 1,
3405
3851
  skipped: prev.skipped || 0
3406
3852
  }));
3407
3853
  }, [trackBehavior]);
3408
- 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(() => {
3409
3875
  trackBehavior("regenerateRate", (prev) => ({
3410
3876
  ...prev,
3411
3877
  regenerated: (prev.regenerated || 0) + 1
3412
3878
  }));
3413
3879
  }, [trackBehavior]);
3414
- const trackMessageSent = useCallback6(() => {
3880
+ const trackMessageSent = useCallback8(() => {
3415
3881
  trackBehavior("regenerateRate", (prev) => ({
3416
3882
  total: (prev.total || 0) + 1,
3417
3883
  regenerated: prev.regenerated || 0
3418
3884
  }));
3419
3885
  }, [trackBehavior]);
3420
- const sendMessage = useCallback6(async (content, options2) => {
3886
+ const sendMessage = useCallback8(async (content, options2) => {
3421
3887
  const messageContent = content || input;
3422
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
+ }
3423
3899
  let sessionId = currentSessionId;
3424
3900
  if (!sessionId) {
3425
3901
  if (useExternalStorage && onCreateSessionRef.current) {
@@ -3705,10 +4181,11 @@ ${finalContent}`;
3705
4181
  return;
3706
4182
  }
3707
4183
  abortControllersRef.current.set(capturedSessionId, new AbortController());
4184
+ let accumulatedContent = "";
4185
+ let lastUsage = null;
3708
4186
  try {
3709
4187
  const shouldSkipSkillParsing = skipNextSkillParsingRef.current;
3710
4188
  skipNextSkillParsingRef.current = false;
3711
- let accumulatedContent = "";
3712
4189
  let checklistStepImageUrl = null;
3713
4190
  let messagesToSend = [...existingMessages, userMessage];
3714
4191
  const recompressionThreshold = DEFAULT_RECOMPRESSION_THRESHOLD;
@@ -3914,119 +4391,71 @@ ${attachmentContext}
3914
4391
  emitFetchHeaders(response);
3915
4392
  }
3916
4393
  if (response) {
3917
- if (!response.ok) throw new Error("API error");
3918
- const reader = response.body?.getReader();
3919
- if (!reader) throw new Error("No reader");
3920
- const decoder = new TextDecoder();
3921
- let buffer = "";
3922
4394
  let skillTagDetected = false;
3923
4395
  let checklistTagDetected = false;
3924
4396
  let toolCallAcc = null;
3925
- while (true) {
3926
- const { done, value } = await reader.read();
3927
- if (done) break;
3928
- buffer += decoder.decode(value, { stream: true });
3929
- const lines = buffer.split("\n");
3930
- buffer = lines.pop() || "";
3931
- for (const line of lines) {
3932
- if (!line.trim()) continue;
3933
- let data = line;
3934
- if (line.startsWith("data: ")) {
3935
- data = line.slice(6);
3936
- 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;
3937
4413
  }
3938
- try {
3939
- const parsed = JSON.parse(data);
3940
- const delta = parsed.choices?.[0]?.delta;
3941
- const finishReason = parsed.choices?.[0]?.finish_reason;
3942
- if (useNativeTools && delta?.tool_calls) {
3943
- toolCallAcc = accumulateToolCallDelta(toolCallAcc, delta);
3944
- }
3945
- if (useNativeTools && finishReason === "tool_calls" && toolCallAcc) {
3946
- accumulatedContent += toolCallToSkillUseXml(toolCallAcc);
3947
- skillTagDetected = true;
3948
- break;
3949
- }
3950
- const content2 = delta?.content || parsed.message?.content || parsed.content || parsed.text || "";
3951
- const thinking = parsed.message?.thinking || "";
3952
- if (content2 || thinking) {
3953
- if (content2) accumulatedContent += content2;
3954
- if (!shouldSkipSkillParsing && accumulatedContent.includes("</skill_use>")) {
3955
- const endIdx = accumulatedContent.indexOf("</skill_use>");
3956
- accumulatedContent = accumulatedContent.substring(0, endIdx + "</skill_use>".length);
3957
- skillTagDetected = true;
3958
- }
3959
- if (!skipNextChecklistParsingRef.current && accumulatedContent.includes("</checklist>")) {
3960
- const endIdx = accumulatedContent.indexOf("</checklist>");
3961
- accumulatedContent = accumulatedContent.substring(0, endIdx + "</checklist>".length);
3962
- checklistTagDetected = true;
3963
- }
3964
- const displayContent = skillTagDetected ? accumulatedContent : null;
3965
- setSessions(
3966
- (prev) => prev.map((s) => {
3967
- if (s.id === capturedSessionId) {
3968
- return {
3969
- ...s,
3970
- messages: s.messages.map((m) => {
3971
- if (m.id !== assistantMessageId) return m;
3972
- if (displayContent) {
3973
- return { ...m, content: displayContent };
3974
- }
3975
- let newContent = m.content;
3976
- if (thinking) {
3977
- if (!newContent.includes("<thinking>")) {
3978
- newContent = "<thinking>" + thinking;
3979
- } else if (!newContent.includes("</thinking>")) {
3980
- newContent += thinking;
3981
- }
3982
- }
3983
- if (content2) {
3984
- if (newContent.includes("<thinking>") && !newContent.includes("</thinking>")) {
3985
- newContent += "</thinking>\n\n";
3986
- }
3987
- newContent += content2;
3988
- }
3989
- return { ...m, content: newContent };
3990
- })
3991
- };
3992
- }
3993
- return s;
3994
- })
3995
- );
3996
- if (skillTagDetected || checklistTagDetected) break;
3997
- }
3998
- } 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;
3999
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";
4000
4452
  }
4001
- if (skillTagDetected || checklistTagDetected) break;
4002
- }
4453
+ });
4454
+ lastUsage = streamResult.usage;
4003
4455
  if (useNativeTools && toolCallAcc && !skillTagDetected) {
4004
4456
  accumulatedContent += toolCallToSkillUseXml(toolCallAcc);
4005
4457
  skillTagDetected = true;
4006
4458
  }
4007
- if (buffer.trim()) {
4008
- try {
4009
- const parsed = JSON.parse(buffer);
4010
- const content2 = parsed.message?.content || parsed.content || parsed.text;
4011
- if (content2) {
4012
- accumulatedContent += content2;
4013
- setSessions(
4014
- (prev) => prev.map((s) => {
4015
- if (s.id === capturedSessionId) {
4016
- return {
4017
- ...s,
4018
- messages: s.messages.map(
4019
- (m) => m.id === assistantMessageId ? { ...m, content: m.content + content2 } : m
4020
- )
4021
- };
4022
- }
4023
- return s;
4024
- })
4025
- );
4026
- }
4027
- } catch {
4028
- }
4029
- }
4030
4459
  }
4031
4460
  const saveMessagesOnEarlyReturn = (overrideContentParts) => {
4032
4461
  if (!useExternalStorage || !capturedSessionId) return;
@@ -4387,9 +4816,10 @@ ${result.content}
4387
4816
  }
4388
4817
  skipNextSkillParsingRef.current = true;
4389
4818
  saveMessagesOnEarlyReturn();
4819
+ const truncatedResult = result.content.length > maxToolResultSize ? result.content.substring(0, maxToolResultSize) + "\n\n...(truncated)" : result.content;
4390
4820
  const resultPrompt = `\uC2A4\uD0AC "${detectedSkill.name}" \uC2E4\uD589 \uACB0\uACFC:
4391
4821
 
4392
- ${result.content}
4822
+ ${truncatedResult}
4393
4823
 
4394
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.`;
4395
4825
  setTimeout(() => {
@@ -4715,6 +5145,32 @@ ${stepSummary}
4715
5145
  };
4716
5146
  })
4717
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
+ }
4718
5174
  if (useExternalStorage && capturedSessionId) {
4719
5175
  const assistantContentForSave = accumulatedContent;
4720
5176
  if (assistantContentForSave && onSaveMessagesRef.current) {
@@ -4743,296 +5199,77 @@ ${stepSummary}
4743
5199
  if (sinceLastExtraction >= 4) {
4744
5200
  lastExtractionMsgCountRef.current = currentMsgCount;
4745
5201
  const recentForExtraction = [...existingMessages.slice(-6), userMessage].map(
4746
- (m) => ({ role: m.role, content: m.content })
4747
- );
4748
- infoExtraction.extractInfo(recentForExtraction).catch(() => {
4749
- });
4750
- }
4751
- }
4752
- if (observerConfig) {
4753
- const newMessages = [userMessage, { id: assistantMessageId, role: "assistant", content: accumulatedContent, timestamp: Date.now() }];
4754
- observer.processMessages(capturedSessionId, newMessages);
4755
- }
4756
- } catch (error) {
4757
- if (error instanceof Error && error.name === "AbortError") {
4758
- return;
4759
- }
4760
- const err = error instanceof Error ? error : new Error("Unknown error");
4761
- onErrorRef.current?.(err);
4762
- setSessions(
4763
- (prev) => prev.map((s) => {
4764
- if (s.id === capturedSessionId) {
4765
- return {
4766
- ...s,
4767
- messages: s.messages.map(
4768
- (m) => m.id === assistantMessageId ? { ...m, content: "\uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4. \uB2E4\uC2DC \uC2DC\uB3C4\uD574\uC8FC\uC138\uC694." } : m
4769
- )
4770
- };
4771
- }
4772
- return s;
4773
- })
4774
- );
4775
- } finally {
4776
- removeLoadingSession(capturedSessionId);
4777
- abortControllersRef.current.delete(capturedSessionId);
4778
- }
4779
- }, [
4780
- input,
4781
- loadingSessionIds,
4782
- currentSessionId,
4783
- sessions,
4784
- selectedModel,
4785
- quotedText,
4786
- selectedAction,
4787
- apiEndpoint,
4788
- apiKey,
4789
- models,
4790
- contextCompressionThreshold,
4791
- keepRecentMessages,
4792
- buildSystemPrompt,
4793
- compressContext,
4794
- useExternalStorage,
4795
- handleSkillCall,
4796
- resolvedSkills,
4797
- /** @Todo vibecode - attachments, continueAfterToolResult를 deps에 추가하여 stale closure 방지 */
4798
- attachments,
4799
- continueAfterToolResult
4800
- ]);
4801
- const handleChecklistStart = useCallback6(
4802
- (messageId) => {
4803
- const session = sessionsRef.current.find(
4804
- (s) => s.messages.some((m) => m.id === messageId)
4805
- );
4806
- if (!session) return;
4807
- const message = session.messages.find((m) => m.id === messageId);
4808
- if (!message?.checklistBlock) return;
4809
- pendingChecklistRef.current = null;
4810
- activeChecklistRef.current = {
4811
- messageId,
4812
- sessionId: session.id,
4813
- 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 })),
4814
- currentStep: 0,
4815
- stepResults: []
4816
- };
4817
- setSessions(
4818
- (prev) => prev.map((s) => {
4819
- if (s.id !== session.id) return s;
4820
- return {
4821
- ...s,
4822
- messages: s.messages.map((m) => {
4823
- if (m.id !== messageId || !m.checklistBlock) return m;
4824
- const updatedItems = m.checklistBlock.items.map((it, idx) => ({
4825
- ...it,
4826
- status: idx === 0 ? "in_progress" : it.status
4827
- }));
4828
- return {
4829
- ...m,
4830
- checklistBlock: { ...m.checklistBlock, items: updatedItems, currentStep: 0 }
4831
- };
4832
- })
4833
- };
4834
- })
4835
- );
4836
- skipNextChecklistParsingRef.current = true;
4837
- setTimeout(() => {
4838
- const items = message.checklistBlock.items;
4839
- const refUrl = resolveChecklistRefImage(items[0], messageId, session.id);
4840
- if (refUrl) pendingAttachmentDataRef.current = [{ name: "ref_image", mimeType: "image/png", base64: "", size: 0, url: refUrl }];
4841
- sendMessage(
4842
- buildChecklistStepPrompt(0, items.length, items[0], true, refUrl),
4843
- { hiddenUserMessage: true, isChecklistExecution: true, checklistStep: { index: 0, title: items[0].title } }
4844
- );
4845
- }, 100);
4846
- },
4847
- [sendMessage]
4848
- );
4849
- const handleChecklistAbort = useCallback6(() => {
4850
- if (!activeChecklistRef.current) return;
4851
- const checklist = activeChecklistRef.current;
4852
- const stepIdx = checklist.currentStep;
4853
- abortControllersRef.current.get(checklist.sessionId)?.abort();
4854
- setSessions(
4855
- (prev) => prev.map((s) => {
4856
- if (s.id !== checklist.sessionId) return s;
4857
- return {
4858
- ...s,
4859
- messages: s.messages.map((m) => {
4860
- if (m.id !== checklist.messageId || !m.checklistBlock) return m;
4861
- return {
4862
- ...m,
4863
- checklistBlock: {
4864
- ...m.checklistBlock,
4865
- items: m.checklistBlock.items.map((it, idx) => ({
4866
- ...it,
4867
- status: idx === stepIdx ? "error" : it.status
4868
- }))
4869
- }
4870
- };
4871
- })
4872
- };
4873
- })
4874
- );
4875
- activeChecklistRef.current = null;
4876
- removeLoadingSession(checklist.sessionId);
4877
- }, [removeLoadingSession]);
4878
- const handleChecklistRetry = useCallback6(
4879
- (messageId, stepIndex) => {
4880
- const session = sessionsRef.current.find(
4881
- (s) => s.messages.some((m) => m.id === messageId)
4882
- );
4883
- if (!session) return;
4884
- const message = session.messages.find((m) => m.id === messageId);
4885
- if (!message?.checklistBlock) return;
4886
- activeChecklistRef.current = {
4887
- messageId,
4888
- sessionId: session.id,
4889
- 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 })),
4890
- currentStep: stepIndex,
4891
- stepResults: message.checklistBlock.items.slice(0, stepIndex).map((it) => it.result || "")
4892
- };
4893
- setSessions(
4894
- (prev) => prev.map((s) => {
4895
- if (s.id !== session.id) return s;
4896
- return {
4897
- ...s,
4898
- messages: s.messages.map((m) => {
4899
- if (m.id !== messageId || !m.checklistBlock) return m;
4900
- return {
4901
- ...m,
4902
- checklistBlock: {
4903
- ...m.checklistBlock,
4904
- items: m.checklistBlock.items.map((it, idx) => ({
4905
- ...it,
4906
- status: idx === stepIndex ? "in_progress" : idx > stepIndex ? "pending" : it.status
4907
- })),
4908
- currentStep: stepIndex
4909
- }
4910
- };
4911
- })
4912
- };
4913
- })
4914
- );
4915
- skipNextChecklistParsingRef.current = true;
4916
- setTimeout(() => {
4917
- const items = message.checklistBlock.items;
4918
- const refUrl = resolveChecklistRefImage(items[stepIndex], messageId, session.id);
4919
- if (refUrl) pendingAttachmentDataRef.current = [{ name: "ref_image", mimeType: "image/png", base64: "", size: 0, url: refUrl }];
4920
- sendMessage(
4921
- buildChecklistStepPrompt(stepIndex, items.length, items[stepIndex], stepIndex === 0, refUrl),
4922
- { hiddenUserMessage: true, isChecklistExecution: true, checklistStep: { index: stepIndex, title: items[stepIndex].title } }
4923
- );
4924
- }, 100);
4925
- },
4926
- [sendMessage]
4927
- );
4928
- const handleChecklistSkip = useCallback6(
4929
- (messageId, stepIndex) => {
4930
- const session = sessionsRef.current.find(
4931
- (s) => s.messages.some((m) => m.id === messageId)
4932
- );
4933
- if (!session) return;
4934
- const message = session.messages.find((m) => m.id === messageId);
4935
- if (!message?.checklistBlock) return;
4936
- trackChecklistSkip();
4937
- setSessions(
4938
- (prev) => prev.map((s) => {
4939
- if (s.id !== session.id) return s;
4940
- return {
4941
- ...s,
4942
- messages: s.messages.map((m) => {
4943
- if (m.id !== messageId || !m.checklistBlock) return m;
4944
- return {
4945
- ...m,
4946
- checklistBlock: {
4947
- ...m.checklistBlock,
4948
- items: m.checklistBlock.items.map((it, idx) => ({
4949
- ...it,
4950
- status: idx === stepIndex ? "done" : it.status,
4951
- result: idx === stepIndex ? "(\uAC74\uB108\uB700)" : it.result
4952
- }))
4953
- }
4954
- };
4955
- })
4956
- };
4957
- })
4958
- );
4959
- const nextPending = message.checklistBlock.items.findIndex(
4960
- (it, idx) => idx > stepIndex && (it.status === "pending" || it.status === "error")
4961
- );
4962
- if (nextPending >= 0) {
4963
- activeChecklistRef.current = {
4964
- messageId,
4965
- sessionId: session.id,
4966
- 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 })),
4967
- currentStep: nextPending,
4968
- stepResults: message.checklistBlock.items.slice(0, nextPending).map((it) => it.result || "(\uAC74\uB108\uB700)")
4969
- };
4970
- setSessions(
4971
- (prev) => prev.map((s) => {
4972
- if (s.id !== session.id) return s;
4973
- return {
4974
- ...s,
4975
- messages: s.messages.map((m) => {
4976
- if (m.id !== messageId || !m.checklistBlock) return m;
4977
- return {
4978
- ...m,
4979
- checklistBlock: {
4980
- ...m.checklistBlock,
4981
- items: m.checklistBlock.items.map((it, idx) => ({
4982
- ...it,
4983
- status: idx === nextPending ? "in_progress" : it.status
4984
- })),
4985
- currentStep: nextPending
4986
- }
4987
- };
4988
- })
4989
- };
4990
- })
4991
- );
4992
- skipNextChecklistParsingRef.current = true;
4993
- setTimeout(() => {
4994
- const items = message.checklistBlock.items;
4995
- const refUrl = resolveChecklistRefImage(items[nextPending], messageId, session.id);
4996
- if (refUrl) pendingAttachmentDataRef.current = [{ name: "ref_image", mimeType: "image/png", base64: "", size: 0, url: refUrl }];
4997
- sendMessage(
4998
- buildChecklistStepPrompt(nextPending, items.length, items[nextPending], false, refUrl),
4999
- { hiddenUserMessage: true, isChecklistExecution: true, checklistStep: { index: nextPending, title: items[nextPending].title } }
5202
+ (m) => ({ role: m.role, content: m.content })
5000
5203
  );
5001
- }, 100);
5002
- } else {
5003
- const allResults = message.checklistBlock.items.map((it, i) => {
5004
- const result = i === stepIndex ? "(\uAC74\uB108\uB700)" : it.result || "(\uAC74\uB108\uB700)";
5005
- return `### ${i + 1}. ${it.title}
5006
- ${result}`;
5007
- }).join("\n\n");
5008
- setSessions(
5009
- (prev) => prev.map((s) => {
5010
- 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) {
5011
5235
  return {
5012
5236
  ...s,
5013
- messages: s.messages.map((m) => {
5014
- if (m.id !== messageId || !m.checklistBlock) return m;
5015
- return { ...m, checklistBlock: { ...m.checklistBlock, completed: true } };
5016
- })
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
+ )
5017
5240
  };
5018
- })
5019
- );
5020
- skipNextChecklistParsingRef.current = true;
5021
- setTimeout(() => {
5022
- sendMessage(
5023
- `\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:
5024
-
5025
- ${allResults}
5026
-
5027
- \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.`,
5028
- { hiddenUserMessage: true, isChecklistExecution: true }
5029
- );
5030
- }, 100);
5031
- }
5032
- },
5033
- [sendMessage]
5034
- );
5035
- 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(
5036
5273
  (messageId, responses) => {
5037
5274
  const currentSess = sessions.find((s) => s.id === currentSessionId);
5038
5275
  const targetMessage = currentSess?.messages.find((m) => m.id === messageId);
@@ -5102,7 +5339,7 @@ ${formattedParts.join("\n")}
5102
5339
  },
5103
5340
  [sessions, currentSessionId, selectedModel, sendMessage]
5104
5341
  );
5105
- const saveEdit = useCallback6(async (content) => {
5342
+ const saveEdit = useCallback8(async (content) => {
5106
5343
  if (!editingMessageId || !currentSession || !currentSessionId) return;
5107
5344
  const messageIndex = currentSession.messages.findIndex((m) => m.id === editingMessageId);
5108
5345
  if (messageIndex === -1) return;
@@ -5119,7 +5356,7 @@ ${formattedParts.join("\n")}
5119
5356
  setEditingMessageId(null);
5120
5357
  await sendMessage(content);
5121
5358
  }, [editingMessageId, currentSession, currentSessionId, sendMessage]);
5122
- const regenerate = useCallback6(async (messageId) => {
5359
+ const regenerate = useCallback8(async (messageId) => {
5123
5360
  console.log("[ChatUI] Regenerate called:", { messageId, currentSessionId, isLoading });
5124
5361
  if (!currentSession || !currentSessionId || isLoading) {
5125
5362
  console.log("[ChatUI] Regenerate early return - missing session or loading");
@@ -5195,100 +5432,39 @@ ${formattedParts.join("\n")}
5195
5432
  signal: abortControllersRef.current.get(capturedSessionId).signal
5196
5433
  });
5197
5434
  console.log("[ChatUI] Regenerate response status:", response.status);
5198
- if (!response.ok) throw new Error(`API error: ${response.status}`);
5199
- const reader = response.body?.getReader();
5200
- if (!reader) throw new Error("No reader");
5201
- const decoder = new TextDecoder();
5202
- let buffer = "";
5203
- while (true) {
5204
- const { done, value } = await reader.read();
5205
- if (done) break;
5206
- buffer += decoder.decode(value, { stream: true });
5207
- const lines = buffer.split("\n");
5208
- buffer = lines.pop() || "";
5209
- for (const line of lines) {
5210
- if (!line.trim()) continue;
5211
- let data = line;
5212
- if (line.startsWith("data: ")) {
5213
- data = line.slice(6);
5214
- if (data === "[DONE]") continue;
5215
- }
5216
- try {
5217
- const parsed = JSON.parse(data);
5218
- const content = parsed.message?.content || parsed.content || parsed.text || "";
5219
- const thinking = parsed.message?.thinking || "";
5220
- if (content || thinking) {
5221
- setSessions(
5222
- (prev) => prev.map((s) => {
5223
- if (s.id === capturedSessionId) {
5224
- return {
5225
- ...s,
5226
- messages: s.messages.map((m) => {
5227
- if (m.id !== assistantMessageId) return m;
5228
- let newContent = m.content;
5229
- if (thinking) {
5230
- if (!newContent.includes("<thinking>")) {
5231
- newContent = "<thinking>" + thinking;
5232
- } else if (!newContent.includes("</thinking>")) {
5233
- newContent += thinking;
5234
- }
5235
- }
5236
- if (content) {
5237
- if (newContent.includes("<thinking>") && !newContent.includes("</thinking>")) {
5238
- newContent += "</thinking>\n\n";
5239
- }
5240
- newContent += content;
5241
- }
5242
- return { ...m, content: newContent };
5243
- })
5244
- };
5245
- }
5246
- return s;
5247
- })
5248
- );
5249
- }
5250
- } catch {
5251
- }
5252
- }
5253
- }
5254
- if (buffer.trim()) {
5255
- try {
5256
- const parsed = JSON.parse(buffer);
5257
- const content = parsed.message?.content || parsed.content || parsed.text || "";
5258
- const thinking = parsed.message?.thinking || "";
5259
- if (content || thinking) {
5260
- setSessions(
5261
- (prev) => prev.map((s) => {
5262
- if (s.id === capturedSessionId) {
5263
- return {
5264
- ...s,
5265
- messages: s.messages.map((m) => {
5266
- if (m.id !== assistantMessageId) return m;
5267
- let newContent = m.content;
5268
- if (thinking) {
5269
- if (!newContent.includes("<thinking>")) {
5270
- newContent = "<thinking>" + thinking;
5271
- } else if (!newContent.includes("</thinking>")) {
5272
- newContent += thinking;
5273
- }
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;
5274
5451
  }
5275
- if (content) {
5276
- if (newContent.includes("<thinking>") && !newContent.includes("</thinking>")) {
5277
- newContent += "</thinking>\n\n";
5278
- }
5279
- newContent += content;
5452
+ }
5453
+ if (content) {
5454
+ if (newContent.includes("<thinking>") && !newContent.includes("</thinking>")) {
5455
+ newContent += "</thinking>\n\n";
5280
5456
  }
5281
- return { ...m, content: newContent };
5282
- })
5283
- };
5284
- }
5285
- return s;
5286
- })
5287
- );
5288
- }
5289
- } catch {
5457
+ newContent += content;
5458
+ }
5459
+ return { ...m, content: newContent };
5460
+ })
5461
+ };
5462
+ }
5463
+ return s;
5464
+ })
5465
+ );
5290
5466
  }
5291
- }
5467
+ }, { noTimeout: true });
5292
5468
  } catch (error) {
5293
5469
  if (error instanceof Error && error.name === "AbortError") {
5294
5470
  return;
@@ -5299,7 +5475,7 @@ ${formattedParts.join("\n")}
5299
5475
  removeLoadingSession(capturedSessionId);
5300
5476
  }
5301
5477
  }, [currentSession, currentSessionId, loadingSessionIds, selectedModel, models, apiEndpoint, apiKey, buildSystemPrompt]);
5302
- const askOtherModel = useCallback6(async (messageId, targetModel) => {
5478
+ const askOtherModel = useCallback8(async (messageId, targetModel) => {
5303
5479
  if (!currentSession || !currentSessionId || isLoading) return;
5304
5480
  const assistantIndex = currentSession.messages.findIndex((m) => m.id === messageId);
5305
5481
  if (assistantIndex === -1) return;
@@ -5354,30 +5530,7 @@ ${currentSession.contextSummary}` },
5354
5530
  responseContent = result.content;
5355
5531
  responseSources = result.sources;
5356
5532
  } else {
5357
- const reader = result.getReader();
5358
- const decoder = new TextDecoder();
5359
- let buffer = "";
5360
- while (true) {
5361
- const { done, value } = await reader.read();
5362
- if (done) break;
5363
- buffer += decoder.decode(value, { stream: true });
5364
- const lines = buffer.split("\n");
5365
- buffer = lines.pop() || "";
5366
- for (const line of lines) {
5367
- if (line.startsWith("data: ")) {
5368
- const data = line.slice(6);
5369
- if (data === "[DONE]") continue;
5370
- try {
5371
- const parsed = JSON.parse(data);
5372
- {
5373
- const chunk = parsed.content ?? parsed.text ?? "";
5374
- if (chunk) responseContent += chunk;
5375
- }
5376
- } catch {
5377
- }
5378
- }
5379
- }
5380
- }
5533
+ responseContent = await parseSSEResponse(new Response(result));
5381
5534
  }
5382
5535
  } else {
5383
5536
  const isOllama = provider === "ollama" || apiEndpoint.includes("ollama") || apiEndpoint.includes("11434");
@@ -5395,32 +5548,9 @@ ${currentSession.contextSummary}` },
5395
5548
  signal: abortControllersRef.current.get(currentSessionId).signal
5396
5549
  });
5397
5550
  emitFetchHeaders(response);
5398
- if (!response.ok) throw new Error("API error");
5399
- const reader = response.body?.getReader();
5400
- if (!reader) throw new Error("No reader");
5401
- const decoder = new TextDecoder();
5402
- let buffer = "";
5403
- while (true) {
5404
- const { done, value } = await reader.read();
5405
- if (done) break;
5406
- buffer += decoder.decode(value, { stream: true });
5407
- const lines = buffer.split("\n");
5408
- buffer = lines.pop() || "";
5409
- for (const line of lines) {
5410
- if (line.startsWith("data: ")) {
5411
- const data = line.slice(6);
5412
- if (data === "[DONE]") continue;
5413
- try {
5414
- const parsed = JSON.parse(data);
5415
- {
5416
- const chunk = parsed.content ?? parsed.text ?? "";
5417
- if (chunk) responseContent += chunk;
5418
- }
5419
- } catch {
5420
- }
5421
- }
5422
- }
5423
- }
5551
+ await streamResponse(response, (chunk) => {
5552
+ if (chunk.content) responseContent += chunk.content;
5553
+ }, { noTimeout: true });
5424
5554
  }
5425
5555
  const alternative = {
5426
5556
  id: generateId5("alt"),
@@ -5480,13 +5610,13 @@ ${currentSession.contextSummary}` },
5480
5610
  onSendMessage,
5481
5611
  onError
5482
5612
  ]);
5483
- const setActiveAlternative = useCallback6((assistantMessageId, index) => {
5613
+ const setActiveAlternative = useCallback8((assistantMessageId, index) => {
5484
5614
  setActiveAlternatives((prev) => ({
5485
5615
  ...prev,
5486
5616
  [assistantMessageId]: index
5487
5617
  }));
5488
5618
  }, []);
5489
- const getActiveAlternative = useCallback6((assistantMessageId) => {
5619
+ const getActiveAlternative = useCallback8((assistantMessageId) => {
5490
5620
  return activeAlternatives[assistantMessageId] ?? 0;
5491
5621
  }, [activeAlternatives]);
5492
5622
  return {
@@ -5544,6 +5674,10 @@ ${currentSession.contextSummary}` },
5544
5674
  }));
5545
5675
  await infoExtraction.extractInfo(recentMessages);
5546
5676
  },
5677
+ getTokenStats: useCallback8((sessionId) => {
5678
+ const session = sessions.find((s) => s.id === sessionId);
5679
+ return session?.tokenStats ?? null;
5680
+ }, [sessions]),
5547
5681
  // External Storage Loading States
5548
5682
  /**
5549
5683
  * @description 세션 목록 로딩 상태
@@ -5704,7 +5838,7 @@ var ImageErrorContext = createContext(null);
5704
5838
  var useImageError = () => useContext(ImageErrorContext);
5705
5839
 
5706
5840
  // src/react/components/ChatSidebar.tsx
5707
- 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";
5708
5842
 
5709
5843
  // src/react/components/Icon.tsx
5710
5844
  import { jsx } from "react/jsx-runtime";
@@ -5804,7 +5938,7 @@ var IconSvg = ({
5804
5938
  };
5805
5939
 
5806
5940
  // src/react/components/MarkdownRenderer.tsx
5807
- import React2, { useMemo as useMemo4 } from "react";
5941
+ import React2, { useMemo as useMemo5 } from "react";
5808
5942
 
5809
5943
  // src/react/components/LinkChip.tsx
5810
5944
  import { useState as useState7 } from "react";
@@ -6861,11 +6995,11 @@ var MarkdownRenderer = ({
6861
6995
  sources,
6862
6996
  inline = false
6863
6997
  }) => {
6864
- const inlineRendered = useMemo4(() => {
6998
+ const inlineRendered = useMemo5(() => {
6865
6999
  if (!inline) return null;
6866
7000
  return parseInlineElements(content, "inline-md", sources);
6867
7001
  }, [inline, content, sources]);
6868
- const rendered = useMemo4(() => {
7002
+ const rendered = useMemo5(() => {
6869
7003
  if (inline) return null;
6870
7004
  const elements = [];
6871
7005
  let processedContent = content;
@@ -7262,7 +7396,7 @@ var MarkdownRenderer = ({
7262
7396
  };
7263
7397
 
7264
7398
  // src/react/components/ProjectSelector.tsx
7265
- 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";
7266
7400
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
7267
7401
  var ProjectSelector = ({
7268
7402
  projects,
@@ -7272,7 +7406,7 @@ var ProjectSelector = ({
7272
7406
  onProjectSettings
7273
7407
  }) => {
7274
7408
  const [isOpen, setIsOpen] = useState8(false);
7275
- const dropdownRef = useRef6(null);
7409
+ const dropdownRef = useRef8(null);
7276
7410
  const currentProject = projects.find((p) => p.id === currentProjectId);
7277
7411
  useEffect5(() => {
7278
7412
  const handleClickOutside = (e) => {
@@ -7510,7 +7644,7 @@ var ChatSidebar = ({
7510
7644
  const sidebarWidth = typeof widthProp === "number" ? `${widthProp}px` : widthProp || "288px";
7511
7645
  const [editingId, setEditingId] = useState9(null);
7512
7646
  const [editingTitle, setEditingTitle] = useState9("");
7513
- const inputRef = useRef7(null);
7647
+ const inputRef = useRef9(null);
7514
7648
  useEffect6(() => {
7515
7649
  if (editingId && inputRef.current) {
7516
7650
  inputRef.current.focus();
@@ -8152,7 +8286,7 @@ var ChatHeader = ({
8152
8286
  };
8153
8287
 
8154
8288
  // src/react/components/ChatInput.tsx
8155
- 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";
8156
8290
  import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
8157
8291
  var ChatInput = ({
8158
8292
  value,
@@ -8181,12 +8315,12 @@ var ChatInput = ({
8181
8315
  inline = false
8182
8316
  }) => {
8183
8317
  const [mainMenuOpen, setMainMenuOpen] = React6.useState(false);
8184
- const textareaRef = useRef8(null);
8185
- const fileInputRef = useRef8(null);
8318
+ const textareaRef = useRef10(null);
8319
+ const fileInputRef = useRef10(null);
8186
8320
  const [actionMenuOpen, setActionMenuOpen] = useState11(false);
8187
8321
  const [isDragOver, setIsDragOver] = useState11(false);
8188
- const mainMenuRef = useRef8(null);
8189
- const actionMenuRef = useRef8(null);
8322
+ const mainMenuRef = useRef10(null);
8323
+ const actionMenuRef = useRef10(null);
8190
8324
  useEffect7(() => {
8191
8325
  if (textareaRef.current) {
8192
8326
  textareaRef.current.style.height = "auto";
@@ -8915,7 +9049,7 @@ var iconButtonStyle = {
8915
9049
  };
8916
9050
 
8917
9051
  // src/react/components/MessageList.tsx
8918
- 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";
8919
9053
 
8920
9054
  // src/react/components/MessageBubble.tsx
8921
9055
  import { useState as useState17 } from "react";
@@ -9204,7 +9338,7 @@ var DeepResearchProgressUI = ({ progress }) => {
9204
9338
  };
9205
9339
 
9206
9340
  // src/react/components/PollCard.tsx
9207
- 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";
9208
9342
  import { Fragment as Fragment5, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
9209
9343
  var renderInlineMarkdown = (text) => {
9210
9344
  const parts = [];
@@ -9248,7 +9382,7 @@ var PollCard = ({
9248
9382
  const [otherTexts, setOtherTexts] = useState12({});
9249
9383
  const [otherSelected, setOtherSelected] = useState12({});
9250
9384
  const currentQuestion = questions[activeTab];
9251
- const handleOptionToggle = useCallback7(
9385
+ const handleOptionToggle = useCallback9(
9252
9386
  (questionId, optionId, multiSelect) => {
9253
9387
  setSelections((prev) => {
9254
9388
  const current = prev[questionId] || /* @__PURE__ */ new Set();
@@ -9270,7 +9404,7 @@ var PollCard = ({
9270
9404
  },
9271
9405
  []
9272
9406
  );
9273
- const handleOtherToggle = useCallback7((questionId, multiSelect) => {
9407
+ const handleOtherToggle = useCallback9((questionId, multiSelect) => {
9274
9408
  if (multiSelect) {
9275
9409
  setOtherSelected((prev) => ({ ...prev, [questionId]: !prev[questionId] }));
9276
9410
  } else {
@@ -9278,7 +9412,7 @@ var PollCard = ({
9278
9412
  setOtherSelected((prev) => ({ ...prev, [questionId]: true }));
9279
9413
  }
9280
9414
  }, []);
9281
- const handleSubmit = useCallback7(() => {
9415
+ const handleSubmit = useCallback9(() => {
9282
9416
  const responses = questions.map((q) => {
9283
9417
  const selected = selections[q.id] || /* @__PURE__ */ new Set();
9284
9418
  const other = otherSelected[q.id] && otherTexts[q.id]?.trim();
@@ -9291,7 +9425,7 @@ var PollCard = ({
9291
9425
  });
9292
9426
  onSubmit(responses);
9293
9427
  }, [questions, selections, otherSelected, otherTexts, onSubmit]);
9294
- const handleSkip = useCallback7(() => {
9428
+ const handleSkip = useCallback9(() => {
9295
9429
  const responses = questions.map((q) => ({
9296
9430
  questionId: q.id,
9297
9431
  selectedOptions: [],
@@ -9339,7 +9473,7 @@ var PollCard = ({
9339
9473
  }
9340
9474
  return () => timers.forEach(clearTimeout);
9341
9475
  }, [activeTab, currentQuestion.options.length]);
9342
- const handleNext = useCallback7(() => {
9476
+ const handleNext = useCallback9(() => {
9343
9477
  if (!isLastTab) {
9344
9478
  setActiveTab((prev) => prev + 1);
9345
9479
  } else {
@@ -9757,7 +9891,7 @@ var SkillProgressUI = ({
9757
9891
  };
9758
9892
 
9759
9893
  // src/react/components/ImageContentCard.tsx
9760
- import { useState as useState13, useCallback as useCallback8 } from "react";
9894
+ import { useState as useState13, useCallback as useCallback10 } from "react";
9761
9895
  import { Fragment as Fragment6, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
9762
9896
  var ImageContentCard = ({ part }) => {
9763
9897
  const [isExpanded, setIsExpanded] = useState13(false);
@@ -9765,7 +9899,7 @@ var ImageContentCard = ({ part }) => {
9765
9899
  const [hasError, setHasError] = useState13(false);
9766
9900
  const [imgSrc, setImgSrc] = useState13(part.url);
9767
9901
  const onImageError = useImageError();
9768
- const handleImageError = useCallback8(async () => {
9902
+ const handleImageError = useCallback10(async () => {
9769
9903
  if (onImageError) {
9770
9904
  const newUrl = await onImageError(imgSrc, part.fileName);
9771
9905
  if (newUrl) {
@@ -10142,7 +10276,7 @@ var ToolStatusCard = ({
10142
10276
  };
10143
10277
 
10144
10278
  // src/react/components/ArtifactCard.tsx
10145
- 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";
10146
10280
  import { jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
10147
10281
  var getThemeStyles = () => {
10148
10282
  const root = document.documentElement;
@@ -10183,9 +10317,9 @@ var getAutoResizeScript = (id) => `
10183
10317
  });
10184
10318
  </script>`;
10185
10319
  var HtmlArtifact = ({ code }) => {
10186
- const iframeRef = useRef9(null);
10320
+ const iframeRef = useRef11(null);
10187
10321
  const [height, setHeight] = useState15(200);
10188
- 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)}`);
10189
10323
  useEffect9(() => {
10190
10324
  const id = artifactId.current;
10191
10325
  const handleMessage = (e) => {
@@ -10232,10 +10366,10 @@ var SvgArtifact = ({ code }) => {
10232
10366
  );
10233
10367
  };
10234
10368
  var MermaidArtifact = ({ code }) => {
10235
- const containerRef = useRef9(null);
10369
+ const containerRef = useRef11(null);
10236
10370
  const [svgHtml, setSvgHtml] = useState15(null);
10237
10371
  const [error, setError] = useState15(null);
10238
- const renderMermaid = useCallback9(async () => {
10372
+ const renderMermaid = useCallback11(async () => {
10239
10373
  try {
10240
10374
  let mermaid;
10241
10375
  try {
@@ -10494,7 +10628,7 @@ var ArtifactActions = ({ code, language, containerRef }) => {
10494
10628
  ] });
10495
10629
  };
10496
10630
  var ArtifactCard = ({ part, index = 0 }) => {
10497
- const contentRef = useRef9(null);
10631
+ const contentRef = useRef11(null);
10498
10632
  return /* @__PURE__ */ jsxs13(
10499
10633
  "div",
10500
10634
  {
@@ -10666,7 +10800,7 @@ var ContentPartRenderer = ({
10666
10800
  };
10667
10801
 
10668
10802
  // src/react/components/ChecklistCard.tsx
10669
- import { useState as useState16, useMemo as useMemo5 } from "react";
10803
+ import { useState as useState16, useMemo as useMemo6 } from "react";
10670
10804
  import { jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
10671
10805
  var ChecklistCard = ({
10672
10806
  items,
@@ -10677,7 +10811,7 @@ var ChecklistCard = ({
10677
10811
  onStart
10678
10812
  }) => {
10679
10813
  const [expandedItems, setExpandedItems] = useState16(/* @__PURE__ */ new Set());
10680
- const { doneCount, isRunning, hasError, isWaiting } = useMemo5(() => {
10814
+ const { doneCount, isRunning, hasError, isWaiting } = useMemo6(() => {
10681
10815
  const done = items.filter((it) => it.status === "done").length;
10682
10816
  const running = items.some((it) => it.status === "in_progress");
10683
10817
  const waiting = items.every((it) => it.status === "pending");
@@ -11927,12 +12061,12 @@ var MessageList = ({
11927
12061
  onExport,
11928
12062
  onToggleChecklistPanel
11929
12063
  }) => {
11930
- const messagesEndRef = useRef10(null);
11931
- const containerRef = useRef10(null);
12064
+ const messagesEndRef = useRef12(null);
12065
+ const containerRef = useRef12(null);
11932
12066
  const [selectedText, setSelectedText] = useState18("");
11933
12067
  const [selectionPosition, setSelectionPosition] = useState18(null);
11934
12068
  const [showScrollButton, setShowScrollButton] = useState18(false);
11935
- const userScrollLockRef = useRef10(false);
12069
+ const userScrollLockRef = useRef12(false);
11936
12070
  const SCROLL_THRESHOLD = 100;
11937
12071
  useEffect10(() => {
11938
12072
  const container = containerRef.current;
@@ -11971,7 +12105,7 @@ var MessageList = ({
11971
12105
  container.removeEventListener("touchmove", handleTouchMove);
11972
12106
  };
11973
12107
  }, []);
11974
- const handleScroll = useCallback11(() => {
12108
+ const handleScroll = useCallback13(() => {
11975
12109
  if (!containerRef.current) return;
11976
12110
  const { scrollTop, scrollHeight, clientHeight } = containerRef.current;
11977
12111
  const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
@@ -11987,7 +12121,7 @@ var MessageList = ({
11987
12121
  behavior: "smooth"
11988
12122
  });
11989
12123
  }, [messages]);
11990
- const scrollToBottom = useCallback11(() => {
12124
+ const scrollToBottom = useCallback13(() => {
11991
12125
  userScrollLockRef.current = false;
11992
12126
  setShowScrollButton(false);
11993
12127
  containerRef.current?.scrollTo({
@@ -11995,7 +12129,7 @@ var MessageList = ({
11995
12129
  behavior: "smooth"
11996
12130
  });
11997
12131
  }, []);
11998
- const handleMouseUp = useCallback11(() => {
12132
+ const handleMouseUp = useCallback13(() => {
11999
12133
  const selection = typeof window !== "undefined" ? window.getSelection() : null;
12000
12134
  const text = selection?.toString().trim();
12001
12135
  if (text && text.length > 0) {
@@ -15266,13 +15400,13 @@ var ChatUI = (props) => {
15266
15400
  };
15267
15401
 
15268
15402
  // src/react/ChatFloatingWidget.tsx
15269
- 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";
15270
15404
 
15271
15405
  // src/react/hooks/useFloatingWidget.ts
15272
- 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";
15273
15407
 
15274
15408
  // src/react/hooks/useDragResize.ts
15275
- 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";
15276
15410
  var DRAG_THRESHOLD = 5;
15277
15411
  var FAB_SIZE = 56;
15278
15412
  var EDGE_MARGIN = 24;
@@ -15367,7 +15501,7 @@ var useDragResize = (options) => {
15367
15501
  }));
15368
15502
  const vw = viewport.width;
15369
15503
  const vh = viewport.height;
15370
- const initializedRef = useRef11(false);
15504
+ const initializedRef = useRef13(false);
15371
15505
  useEffect13(() => {
15372
15506
  if (initializedRef.current || typeof window === "undefined") return;
15373
15507
  initializedRef.current = true;
@@ -15381,26 +15515,26 @@ var useDragResize = (options) => {
15381
15515
  setFabPos({ x: loaded.fabX, y: loaded.fabY });
15382
15516
  setPanelSize({ width: loaded.panelWidth, height: loaded.panelHeight });
15383
15517
  }, [initialPosition, storageKey, initialWidth, initialHeight]);
15384
- const dragStartRef = useRef11(null);
15385
- const isDraggingRef = useRef11(false);
15386
- const wasDragRef = useRef11(false);
15387
- const userDraggedRef = useRef11(false);
15388
- const dragDeltaRef = useRef11({ dx: 0, dy: 0 });
15389
- const fabElRef = useRef11(null);
15390
- const dragCleanupRef = useRef11(null);
15391
- const snapTimerRef = useRef11(null);
15392
- const prevDragRef = useRef11(null);
15393
- const shakeScoreRef = useRef11(0);
15394
- const prevVelocityRef = useRef11(null);
15395
- const isDizzyRef = useRef11(false);
15396
- const dizzyTimerRef = useRef11(null);
15397
- const lastVelocityRef = useRef11({ vx: 0, vy: 0, speed: 0 });
15398
- const dragRafRef = useRef11(null);
15399
- const animCleanupRef = useRef11(null);
15400
- const snapEnabledRef = useRef11(snapEnabled);
15401
- const storageKeyRef = useRef11(storageKey);
15402
- const panelSizeRef = useRef11(panelSize);
15403
- 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);
15404
15538
  useEffect13(() => {
15405
15539
  snapEnabledRef.current = snapEnabled;
15406
15540
  }, [snapEnabled]);
@@ -15413,7 +15547,7 @@ var useDragResize = (options) => {
15413
15547
  useEffect13(() => {
15414
15548
  fabPosRef.current = fabPos;
15415
15549
  }, [fabPos]);
15416
- const resizeStartRef = useRef11(null);
15550
+ const resizeStartRef = useRef13(null);
15417
15551
  useEffect13(() => {
15418
15552
  return () => {
15419
15553
  dragCleanupRef.current?.();
@@ -15422,7 +15556,7 @@ var useDragResize = (options) => {
15422
15556
  if (dragRafRef.current) cancelAnimationFrame(dragRafRef.current);
15423
15557
  };
15424
15558
  }, []);
15425
- const handleFabPointerDown = useCallback12((e) => {
15559
+ const handleFabPointerDown = useCallback14((e) => {
15426
15560
  if (disabled || isMobile) return;
15427
15561
  dragCleanupRef.current?.();
15428
15562
  const el = e.currentTarget;
@@ -15604,7 +15738,7 @@ var useDragResize = (options) => {
15604
15738
  }, [disabled, isMobile]);
15605
15739
  const fabCenterX = fabPos.x + FAB_SIZE / 2;
15606
15740
  const panelDirection = fabCenterX > vw / 2 ? "left" : "right";
15607
- const panelPositionStyle = useMemo6(() => {
15741
+ const panelPositionStyle = useMemo7(() => {
15608
15742
  if (isMobile || disabled) return {};
15609
15743
  const fabIsTop = fabPos.y < vh / 2;
15610
15744
  let panelBottom;
@@ -15636,7 +15770,7 @@ var useDragResize = (options) => {
15636
15770
  borderRadius: "var(--floating-panel-radius, 16px)"
15637
15771
  };
15638
15772
  }, [isMobile, disabled, fabPos.x, fabPos.y, panelSize.width, panelSize.height, vw, vh, panelDirection, minHeight]);
15639
- const handleResizePointerDown = useCallback12((edge, e) => {
15773
+ const handleResizePointerDown = useCallback14((edge, e) => {
15640
15774
  if (disabled || isMobile) return;
15641
15775
  e.preventDefault();
15642
15776
  e.stopPropagation();
@@ -15652,7 +15786,7 @@ var useDragResize = (options) => {
15652
15786
  };
15653
15787
  setIsResizing(true);
15654
15788
  }, [disabled, isMobile]);
15655
- const handleResizePointerMove = useCallback12((e) => {
15789
+ const handleResizePointerMove = useCallback14((e) => {
15656
15790
  if (!resizeStartRef.current) return;
15657
15791
  const { startX, startY, width, height, edge, fabY } = resizeStartRef.current;
15658
15792
  const dx = e.clientX - startX;
@@ -15676,7 +15810,7 @@ var useDragResize = (options) => {
15676
15810
  newHeight = Math.max(minHeight, Math.min(maxH, newHeight));
15677
15811
  setPanelSize({ width: newWidth, height: newHeight });
15678
15812
  }, [minWidth, maxWidth, minHeight]);
15679
- const handleResizePointerUp = useCallback12((e) => {
15813
+ const handleResizePointerUp = useCallback14((e) => {
15680
15814
  if (!resizeStartRef.current) return;
15681
15815
  e.currentTarget.releasePointerCapture(e.pointerId);
15682
15816
  resizeStartRef.current = null;
@@ -15749,7 +15883,7 @@ var useFloatingWidget = (options) => {
15749
15883
  } = options || {};
15750
15884
  const [isOpen, setIsOpen] = useState22(defaultOpen);
15751
15885
  const [activeTab, setActiveTab] = useState22(defaultTab);
15752
- const panelRef = useRef12(null);
15886
+ const panelRef = useRef14(null);
15753
15887
  const dragResize = useDragResize({
15754
15888
  initialPosition: position,
15755
15889
  initialWidth: width,
@@ -15761,9 +15895,9 @@ var useFloatingWidget = (options) => {
15761
15895
  maxWidth,
15762
15896
  minHeight
15763
15897
  });
15764
- const onOpenRef = useRef12(onOpen);
15765
- const onCloseRef = useRef12(onClose);
15766
- const onTabChangeRef = useRef12(onTabChange);
15898
+ const onOpenRef = useRef14(onOpen);
15899
+ const onCloseRef = useRef14(onClose);
15900
+ const onTabChangeRef = useRef14(onTabChange);
15767
15901
  useEffect14(() => {
15768
15902
  onOpenRef.current = onOpen;
15769
15903
  }, [onOpen]);
@@ -15773,15 +15907,15 @@ var useFloatingWidget = (options) => {
15773
15907
  useEffect14(() => {
15774
15908
  onTabChangeRef.current = onTabChange;
15775
15909
  }, [onTabChange]);
15776
- const open = useCallback13(() => {
15910
+ const open = useCallback15(() => {
15777
15911
  setIsOpen(true);
15778
15912
  onOpenRef.current?.();
15779
15913
  }, []);
15780
- const close = useCallback13(() => {
15914
+ const close = useCallback15(() => {
15781
15915
  setIsOpen(false);
15782
15916
  onCloseRef.current?.();
15783
15917
  }, []);
15784
- const toggle = useCallback13(() => {
15918
+ const toggle = useCallback15(() => {
15785
15919
  setIsOpen((prev) => {
15786
15920
  const next = !prev;
15787
15921
  if (next) onOpenRef.current?.();
@@ -15789,11 +15923,11 @@ var useFloatingWidget = (options) => {
15789
15923
  return next;
15790
15924
  });
15791
15925
  }, []);
15792
- const setTab = useCallback13((tabKey) => {
15926
+ const setTab = useCallback15((tabKey) => {
15793
15927
  setActiveTab(tabKey);
15794
15928
  onTabChangeRef.current?.(tabKey);
15795
15929
  }, []);
15796
- const handleFabInteraction = useCallback13(() => {
15930
+ const handleFabInteraction = useCallback15(() => {
15797
15931
  if (dragResize.isDragging) return;
15798
15932
  toggle();
15799
15933
  }, [dragResize.isDragging, toggle]);
@@ -15823,10 +15957,10 @@ var useFloatingWidget = (options) => {
15823
15957
  };
15824
15958
 
15825
15959
  // src/react/components/floating/FloatingFab.tsx
15826
- 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";
15827
15961
 
15828
15962
  // src/react/components/floating/DevDiveCharacter.tsx
15829
- 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";
15830
15964
  import { Fragment as Fragment10, jsx as jsx24, jsxs as jsxs23 } from "react/jsx-runtime";
15831
15965
  var THEMES = {
15832
15966
  dark: { body: "#2ecc71", stroke: "#27ae60", highlight: "#3ddc84", face: "#1a1a2e", eyeLight: "#fff" },
@@ -15857,11 +15991,11 @@ var DevDiveFabCharacter = ({
15857
15991
  const [isWinking, setIsWinking] = useState23(false);
15858
15992
  const [isCatMouth, setIsCatMouth] = useState23(false);
15859
15993
  const [isEntering, setIsEntering] = useState23(true);
15860
- const svgRef = useRef13(null);
15861
- const cleanupRef = useRef13(null);
15862
- const lastActivityRef = useRef13(Date.now());
15863
- const isSleepyRef = useRef13(false);
15864
- 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(() => {
15865
15999
  lastActivityRef.current = Date.now();
15866
16000
  if (isSleepyRef.current) {
15867
16001
  isSleepyRef.current = false;
@@ -15981,7 +16115,7 @@ var DevDiveFabCharacter = ({
15981
16115
  }, 5e3);
15982
16116
  return () => clearInterval(check);
15983
16117
  }, [isOpen, isTalking, isDizzy]);
15984
- const prevCompleteRef = useRef13(false);
16118
+ const prevCompleteRef = useRef15(false);
15985
16119
  useEffect15(() => {
15986
16120
  const wasComplete = prevCompleteRef.current;
15987
16121
  prevCompleteRef.current = isComplete;
@@ -16182,7 +16316,7 @@ var FloatingFab = ({
16182
16316
  }) => {
16183
16317
  const isRight = position.includes("right");
16184
16318
  const isBottom = position.includes("bottom");
16185
- const fabRef = useRef14(null);
16319
+ const fabRef = useRef16(null);
16186
16320
  const isCharacterMode = !icon;
16187
16321
  const [bubbleOnRight, setBubbleOnRight] = useState24(!isRight);
16188
16322
  const posLeft = positionStyle?.left;
@@ -16197,11 +16331,11 @@ var FloatingFab = ({
16197
16331
  }, [isRight, posLeft, posTop]);
16198
16332
  const [bubbleText, setBubbleText] = useState24(null);
16199
16333
  const [bubbleExiting, setBubbleExiting] = useState24(false);
16200
- const bubbleTextRef = useRef14(bubbleText);
16334
+ const bubbleTextRef = useRef16(bubbleText);
16201
16335
  bubbleTextRef.current = bubbleText;
16202
16336
  const [displayText, setDisplayText] = useState24(null);
16203
16337
  const [isTyping, setIsTyping] = useState24(false);
16204
- const typingTimerRef = useRef14(null);
16338
+ const typingTimerRef = useRef16(null);
16205
16339
  useEffect16(() => {
16206
16340
  if (notification) {
16207
16341
  setBubbleText(notification);
@@ -16238,7 +16372,7 @@ var FloatingFab = ({
16238
16372
  }, 300);
16239
16373
  return () => clearTimeout(timer);
16240
16374
  }, [notification]);
16241
- const notifContentRef = useRef14(null);
16375
+ const notifContentRef = useRef16(null);
16242
16376
  const [needsMarquee, setNeedsMarquee] = useState24(false);
16243
16377
  useEffect16(() => {
16244
16378
  if (isTyping || !notification) {
@@ -16382,7 +16516,7 @@ var FloatingFab = ({
16382
16516
  };
16383
16517
 
16384
16518
  // src/react/components/floating/FloatingPanel.tsx
16385
- 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";
16386
16520
  import { jsxs as jsxs25 } from "react/jsx-runtime";
16387
16521
  var FloatingPanel = ({
16388
16522
  isOpen,
@@ -16411,7 +16545,7 @@ var FloatingPanel = ({
16411
16545
  }, []);
16412
16546
  const [shouldRender, setShouldRender] = useState25(isOpen);
16413
16547
  const [isVisible, setIsVisible] = useState25(isOpen);
16414
- const rafRef = useRef15(0);
16548
+ const rafRef = useRef17(0);
16415
16549
  useEffect17(() => {
16416
16550
  if (isOpen) {
16417
16551
  setShouldRender(true);
@@ -16588,10 +16722,10 @@ var FloatingTabBar = ({
16588
16722
  };
16589
16723
 
16590
16724
  // src/react/components/floating/CompactChatView.tsx
16591
- import { useState as useState27, useCallback as useCallback15 } from "react";
16725
+ import { useState as useState27, useCallback as useCallback17 } from "react";
16592
16726
 
16593
16727
  // src/react/components/floating/CompactSessionMenu.tsx
16594
- 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";
16595
16729
  import { jsx as jsx27, jsxs as jsxs27 } from "react/jsx-runtime";
16596
16730
  var CompactSessionMenu = ({
16597
16731
  sessions,
@@ -16603,9 +16737,9 @@ var CompactSessionMenu = ({
16603
16737
  onClose,
16604
16738
  isLoading = false
16605
16739
  }) => {
16606
- const menuRef = useRef16(null);
16607
- const inputRef = useRef16(null);
16608
- const onCloseRef = useRef16(onClose);
16740
+ const menuRef = useRef18(null);
16741
+ const inputRef = useRef18(null);
16742
+ const onCloseRef = useRef18(onClose);
16609
16743
  onCloseRef.current = onClose;
16610
16744
  const [editingId, setEditingId] = useState26(null);
16611
16745
  const [editingTitle, setEditingTitle] = useState26("");
@@ -16934,15 +17068,15 @@ var CompactChatView = ({
16934
17068
  setInput(choice.text);
16935
17069
  };
16936
17070
  const [showSessionMenu, setShowSessionMenu] = useState27(false);
16937
- const handleSessionSelect = useCallback15((id) => {
17071
+ const handleSessionSelect = useCallback17((id) => {
16938
17072
  selectSession(id);
16939
17073
  setShowSessionMenu(false);
16940
17074
  }, [selectSession]);
16941
- const handleNewSession = useCallback15(() => {
17075
+ const handleNewSession = useCallback17(() => {
16942
17076
  newSession();
16943
17077
  setShowSessionMenu(false);
16944
17078
  }, [newSession]);
16945
- const handleCloseMenu = useCallback15(() => {
17079
+ const handleCloseMenu = useCallback17(() => {
16946
17080
  setShowSessionMenu(false);
16947
17081
  }, []);
16948
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";
@@ -17262,11 +17396,11 @@ var ChatFloatingWidget = ({
17262
17396
  maxWidth,
17263
17397
  minHeight
17264
17398
  });
17265
- const notifObj = useMemo7(() => {
17399
+ const notifObj = useMemo8(() => {
17266
17400
  if (!notification) return null;
17267
17401
  return typeof notification === "string" ? { text: notification } : notification;
17268
17402
  }, [notification]);
17269
- const handleFabClick = useCallback16(() => {
17403
+ const handleFabClick = useCallback18(() => {
17270
17404
  if (notifObj?.onClick) {
17271
17405
  notifObj.onClick();
17272
17406
  if (!isOpen) handleFabInteraction();
@@ -17280,7 +17414,7 @@ var ChatFloatingWidget = ({
17280
17414
  }
17281
17415
  handleFabInteraction();
17282
17416
  }, [notifObj, isOpen, handleFabInteraction, setTab, tabs]);
17283
- const allTabs = useMemo7(() => {
17417
+ const allTabs = useMemo8(() => {
17284
17418
  const chatTab = {
17285
17419
  key: "chat",
17286
17420
  label: "\uCC44\uD305",
@@ -17295,7 +17429,7 @@ var ChatFloatingWidget = ({
17295
17429
  return [chatTab, ...customTabs];
17296
17430
  }, [tabs]);
17297
17431
  const [unreadBadge, setUnreadBadge] = useState28(0);
17298
- const seenMessageIdsRef = useRef17(/* @__PURE__ */ new Set());
17432
+ const seenMessageIdsRef = useRef19(/* @__PURE__ */ new Set());
17299
17433
  useEffect19(() => {
17300
17434
  if (seenMessageIdsRef.current.size === 0 && chatState.messages.length > 0) {
17301
17435
  for (const m of chatState.messages) {
@@ -17328,11 +17462,11 @@ var ChatFloatingWidget = ({
17328
17462
  }
17329
17463
  }
17330
17464
  }, [isOpen, chatState.messages]);
17331
- const totalBadge = useMemo7(() => {
17465
+ const totalBadge = useMemo8(() => {
17332
17466
  return tabs.reduce((sum, t) => sum + (t.badge || 0), 0) + unreadBadge;
17333
17467
  }, [tabs, unreadBadge]);
17334
- const isTalking = useMemo7(() => !!notification || chatState.isLoading, [notification, chatState.isLoading]);
17335
- const prevLoadingRef = useRef17(chatState.isLoading);
17468
+ const isTalking = useMemo8(() => !!notification || chatState.isLoading, [notification, chatState.isLoading]);
17469
+ const prevLoadingRef = useRef19(chatState.isLoading);
17336
17470
  const [isComplete, setIsComplete] = useState28(false);
17337
17471
  useEffect19(() => {
17338
17472
  const wasLoading = prevLoadingRef.current;
@@ -17450,7 +17584,7 @@ var ChatFloatingWidget = ({
17450
17584
  };
17451
17585
 
17452
17586
  // src/react/hooks/useDeepResearch.ts
17453
- 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";
17454
17588
  var REPORT_GENERATION_PROMPT2 = `\uB2F9\uC2E0\uC740 \uB9AC\uC11C\uCE58 \uBCF4\uACE0\uC11C \uC791\uC131 \uC804\uBB38\uAC00\uC785\uB2C8\uB2E4.
17455
17589
 
17456
17590
  <collected_sources>
@@ -17503,8 +17637,8 @@ var useDeepResearch = (options) => {
17503
17637
  const { onWebSearch, onExtractContent, apiEndpoint, apiKey, model, provider } = options;
17504
17638
  const [isResearching, setIsResearching] = useState29(false);
17505
17639
  const [progress, setProgress] = useState29(null);
17506
- const abortControllerRef = useRef18(null);
17507
- const callLLM2 = useCallback17(
17640
+ const abortControllerRef = useRef20(null);
17641
+ const callLLM2 = useCallback19(
17508
17642
  async (prompt, stream = false) => {
17509
17643
  const response = await fetch(apiEndpoint, {
17510
17644
  method: "POST",
@@ -17531,7 +17665,7 @@ var useDeepResearch = (options) => {
17531
17665
  },
17532
17666
  [apiEndpoint, apiKey, model, provider]
17533
17667
  );
17534
- const analyzeQuery2 = useCallback17(
17668
+ const analyzeQuery2 = useCallback19(
17535
17669
  async (query) => {
17536
17670
  const prompt = QUERY_ANALYSIS_PROMPT2.replace("{question}", query);
17537
17671
  const response = await callLLM2(prompt);
@@ -17550,7 +17684,7 @@ var useDeepResearch = (options) => {
17550
17684
  },
17551
17685
  [callLLM2]
17552
17686
  );
17553
- const runSubAgent2 = useCallback17(
17687
+ const runSubAgent2 = useCallback19(
17554
17688
  async (topic, queries, agentId, updateProgress) => {
17555
17689
  updateProgress({ status: "searching", searchCount: 0, resultsCount: 0 });
17556
17690
  const allResults = [];
@@ -17589,7 +17723,7 @@ var useDeepResearch = (options) => {
17589
17723
  },
17590
17724
  [onWebSearch, onExtractContent]
17591
17725
  );
17592
- const generateReport2 = useCallback17(
17726
+ const generateReport2 = useCallback19(
17593
17727
  async (query, results, onStreamContent) => {
17594
17728
  const allSources = [];
17595
17729
  const sourcesForPrompt = [];
@@ -17647,7 +17781,7 @@ var useDeepResearch = (options) => {
17647
17781
  },
17648
17782
  [callLLM2]
17649
17783
  );
17650
- const runDeepResearch = useCallback17(
17784
+ const runDeepResearch = useCallback19(
17651
17785
  async (query, onStreamContent) => {
17652
17786
  abortControllerRef.current = new AbortController();
17653
17787
  setIsResearching(true);
@@ -17750,7 +17884,7 @@ var useDeepResearch = (options) => {
17750
17884
  },
17751
17885
  [analyzeQuery2, runSubAgent2, generateReport2]
17752
17886
  );
17753
- const stopResearch = useCallback17(() => {
17887
+ const stopResearch = useCallback19(() => {
17754
17888
  abortControllerRef.current?.abort();
17755
17889
  setIsResearching(false);
17756
17890
  setProgress(null);
@@ -17763,6 +17897,122 @@ var useDeepResearch = (options) => {
17763
17897
  };
17764
17898
  };
17765
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
+
17766
18016
  // src/react/utils/conversationSearchAdapter.ts
17767
18017
  var formatSearchResults = (results) => {
17768
18018
  if (results.length === 0) {
@@ -18290,8 +18540,52 @@ var MemoryPanel = ({
18290
18540
  }
18291
18541
  );
18292
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
+ };
18293
18586
  export {
18294
18587
  ArtifactCard,
18588
+ ChatError,
18295
18589
  ChatFloatingWidget,
18296
18590
  ChatHeader,
18297
18591
  ChatInput,
@@ -18304,6 +18598,7 @@ export {
18304
18598
  ContentPartRenderer,
18305
18599
  DEFAULT_PROJECT_ID,
18306
18600
  DEFAULT_PROJECT_TITLE,
18601
+ DEFAULT_RETRY_CONFIG,
18307
18602
  DeepResearchProgressUI,
18308
18603
  DevDiveAvatar,
18309
18604
  DevDiveFabCharacter,
@@ -18327,20 +18622,28 @@ export {
18327
18622
  ResizeHandles,
18328
18623
  SettingsModal,
18329
18624
  SkillProgressUI,
18625
+ classifyFetchError,
18330
18626
  convertSkillsToOpenAITools,
18331
18627
  convertToolsToSkills,
18332
18628
  createAdvancedResearchSkill,
18333
18629
  createConversationSearchSkill,
18334
18630
  createDeepResearchSkill,
18631
+ createTimeoutError,
18335
18632
  migrateSessionsToProjects,
18633
+ parseSSELine,
18634
+ parseSSEResponse,
18336
18635
  useChatUI,
18636
+ useChecklist,
18637
+ useContentParsers,
18337
18638
  useDeepResearch,
18338
18639
  useDragResize,
18339
18640
  useFloatingWidget,
18340
18641
  useImageError,
18341
18642
  useObserver,
18342
18643
  useProject,
18343
- useSkills
18644
+ useSkills,
18645
+ useStreamingFetch,
18646
+ withRetry
18344
18647
  };
18345
18648
  /**
18346
18649
  * @description localStorage 기반 메모리 저장소 어댑터