@gendive/chatllm 0.16.1 → 0.17.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.
@@ -1133,7 +1133,7 @@ var useProject = (options) => {
1133
1133
  const currentProject = projects.find((p) => p.id === currentProjectId) || null;
1134
1134
  useEffect2(() => {
1135
1135
  if (!enabled || useExternalStorage || !initializedRef.current) return;
1136
- if (projects.length > 0) {
1136
+ if (projects.length > 0 && typeof window !== "undefined") {
1137
1137
  localStorage.setItem(storageKey, JSON.stringify(projects));
1138
1138
  }
1139
1139
  }, [enabled, projects, storageKey, useExternalStorage]);
@@ -1167,7 +1167,7 @@ var useProject = (options) => {
1167
1167
  }
1168
1168
  } else {
1169
1169
  try {
1170
- const saved = localStorage.getItem(storageKey);
1170
+ const saved = typeof window !== "undefined" ? localStorage.getItem(storageKey) : null;
1171
1171
  if (saved) {
1172
1172
  const parsed = JSON.parse(saved);
1173
1173
  if (!parsed.find((p) => p.id === DEFAULT_PROJECT_ID)) {
@@ -1469,9 +1469,12 @@ var convertToolsToSkills = (tools, onToolCall) => {
1469
1469
  for (const tool of tools) {
1470
1470
  skillMap[tool.name] = {
1471
1471
  description: tool.description,
1472
- trigger: "both",
1472
+ /** @Todo vibecode - tool.trigger 지원 (기본 'both', 'attachment' 가능) */
1473
+ trigger: tool.trigger || "both",
1473
1474
  label: tool.label || tool.name,
1474
1475
  icon: tool.icon,
1476
+ acceptedTypes: tool.acceptedTypes,
1477
+ autoConvertBase64: tool.autoConvertBase64,
1475
1478
  parameters: {
1476
1479
  type: "object",
1477
1480
  properties: Object.fromEntries(
@@ -1545,6 +1548,20 @@ var DEFAULT_KEEP_RECENT = 6;
1545
1548
  var DEFAULT_RECOMPRESSION_THRESHOLD = 10;
1546
1549
  var DEFAULT_TOKEN_LIMIT = 8e3;
1547
1550
  var generateId3 = (prefix) => `${prefix}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
1551
+ var fileToBase64 = (file) => new Promise((resolve, reject) => {
1552
+ const reader = new FileReader();
1553
+ reader.onload = () => resolve(reader.result.split(",")[1] || "");
1554
+ reader.onerror = reject;
1555
+ reader.readAsDataURL(file);
1556
+ });
1557
+ var convertAttachmentsToBase64 = async (attachments) => Promise.all(
1558
+ attachments.map(async (att) => ({
1559
+ name: att.name,
1560
+ mimeType: att.mimeType,
1561
+ base64: await fileToBase64(att.file),
1562
+ size: att.size
1563
+ }))
1564
+ );
1548
1565
  var generateTitle = (messages) => {
1549
1566
  const firstUserMessage = messages.find((m) => m.role === "user");
1550
1567
  if (!firstUserMessage) return "\uC0C8 \uB300\uD654";
@@ -1596,7 +1613,12 @@ var useChatUI = (options) => {
1596
1613
  onUpdateProject,
1597
1614
  onDeleteProject,
1598
1615
  onAddProjectFile,
1599
- onDeleteProjectFile
1616
+ onDeleteProjectFile,
1617
+ // Stream control
1618
+ continueAfterToolResult = true,
1619
+ onSkillComplete,
1620
+ // Dynamic model loading
1621
+ onLoadModels
1600
1622
  } = options;
1601
1623
  const enableAutoExtraction = enableAutoExtractionProp ?? !useExternalStorage;
1602
1624
  const [sessions, setSessions] = useState5([]);
@@ -1620,6 +1642,8 @@ var useChatUI = (options) => {
1620
1642
  const [isSessionLoading, setIsSessionLoading] = useState5(false);
1621
1643
  const [isDeepResearchMode, setIsDeepResearchMode] = useState5(false);
1622
1644
  const [attachments, setAttachments] = useState5([]);
1645
+ const [isModelsLoading, setIsModelsLoading] = useState5(false);
1646
+ const [loadedModels, setLoadedModels] = useState5(null);
1623
1647
  const [deepResearchProgress, setDeepResearchProgress] = useState5(
1624
1648
  null
1625
1649
  );
@@ -1627,6 +1651,40 @@ var useChatUI = (options) => {
1627
1651
  useEffect3(() => {
1628
1652
  sessionsRef.current = sessions;
1629
1653
  }, [sessions]);
1654
+ const onSendMessageRef = useRef4(onSendMessage);
1655
+ const onSessionChangeRef = useRef4(onSessionChange);
1656
+ const onErrorRef = useRef4(onError);
1657
+ const onTitleChangeRef = useRef4(onTitleChange);
1658
+ const generateTitleRef = useRef4(generateTitleCallback);
1659
+ const onPersonalizationChangeRef = useRef4(options.onPersonalizationChange);
1660
+ const onPersonalizationSaveRef = useRef4(options.onPersonalizationSave);
1661
+ const onLoadSessionsRef = useRef4(onLoadSessions);
1662
+ const onCreateSessionRef = useRef4(onCreateSession);
1663
+ const onLoadSessionRef = useRef4(onLoadSession);
1664
+ const onDeleteSessionCallbackRef = useRef4(onDeleteSessionCallback);
1665
+ const onUpdateSessionTitleRef = useRef4(onUpdateSessionTitle);
1666
+ const onSaveMessagesRef = useRef4(onSaveMessages);
1667
+ const onToolCallRef = useRef4(onToolCall);
1668
+ const onSkillCompleteRef = useRef4(onSkillComplete);
1669
+ const onLoadModelsRef = useRef4(onLoadModels);
1670
+ useEffect3(() => {
1671
+ onSendMessageRef.current = onSendMessage;
1672
+ onSessionChangeRef.current = onSessionChange;
1673
+ onErrorRef.current = onError;
1674
+ onTitleChangeRef.current = onTitleChange;
1675
+ generateTitleRef.current = generateTitleCallback;
1676
+ onPersonalizationChangeRef.current = options.onPersonalizationChange;
1677
+ onPersonalizationSaveRef.current = options.onPersonalizationSave;
1678
+ onLoadSessionsRef.current = onLoadSessions;
1679
+ onCreateSessionRef.current = onCreateSession;
1680
+ onLoadSessionRef.current = onLoadSession;
1681
+ onDeleteSessionCallbackRef.current = onDeleteSessionCallback;
1682
+ onUpdateSessionTitleRef.current = onUpdateSessionTitle;
1683
+ onSaveMessagesRef.current = onSaveMessages;
1684
+ onToolCallRef.current = onToolCall;
1685
+ onSkillCompleteRef.current = onSkillComplete;
1686
+ onLoadModelsRef.current = onLoadModels;
1687
+ });
1630
1688
  const abortControllerRef = useRef4(null);
1631
1689
  const skipNextPollParsingRef = useRef4(false);
1632
1690
  const skipNextSkillParsingRef = useRef4(false);
@@ -1642,11 +1700,15 @@ var useChatUI = (options) => {
1642
1700
  [globalMemoryConfig, storageKey]
1643
1701
  );
1644
1702
  const globalMemory = useGlobalMemoryEnabled ? useGlobalMemory(memoryOptions) : null;
1703
+ const stableToolCall = useCallback5(
1704
+ (name, params) => onToolCallRef.current(name, params),
1705
+ []
1706
+ );
1645
1707
  const mergedSkills = useMemo2(() => {
1646
1708
  if (!tools || !onToolCall) return skills || {};
1647
- const toolSkills = convertToolsToSkills(tools, onToolCall);
1709
+ const toolSkills = convertToolsToSkills(tools, stableToolCall);
1648
1710
  return { ...skills || {}, ...toolSkills };
1649
- }, [skills, tools, onToolCall]);
1711
+ }, [skills, tools, !!onToolCall, stableToolCall]);
1650
1712
  const {
1651
1713
  buildSkillsPrompt,
1652
1714
  handleSkillCall,
@@ -1700,9 +1762,9 @@ var useChatUI = (options) => {
1700
1762
  }, [sessions, enableProjects, projectHook.currentProjectId]);
1701
1763
  useEffect3(() => {
1702
1764
  if (typeof window === "undefined") return;
1703
- if (useExternalStorage && onLoadSessions) {
1765
+ if (useExternalStorage && onLoadSessionsRef.current) {
1704
1766
  setIsSessionsLoading(true);
1705
- onLoadSessions().then((sessionList) => {
1767
+ onLoadSessionsRef.current().then((sessionList) => {
1706
1768
  const sessionsWithoutMessages = sessionList.map((s) => ({
1707
1769
  id: s.id,
1708
1770
  title: s.title,
@@ -1718,15 +1780,17 @@ var useChatUI = (options) => {
1718
1780
  setCurrentSessionId(targetId);
1719
1781
  }
1720
1782
  }).catch((error) => {
1721
- onError?.(error instanceof Error ? error : new Error("Failed to load sessions"));
1783
+ onErrorRef.current?.(error instanceof Error ? error : new Error("Failed to load sessions"));
1722
1784
  }).finally(() => {
1723
1785
  setIsSessionsLoading(false);
1724
1786
  });
1725
- const savedPersonalization2 = localStorage.getItem(`${storageKey}_personalization`);
1726
- if (savedPersonalization2) {
1727
- try {
1728
- setPersonalization(JSON.parse(savedPersonalization2));
1729
- } catch {
1787
+ if (typeof window !== "undefined") {
1788
+ const savedPersonalization2 = localStorage.getItem(`${storageKey}_personalization`);
1789
+ if (savedPersonalization2) {
1790
+ try {
1791
+ setPersonalization(JSON.parse(savedPersonalization2));
1792
+ } catch {
1793
+ }
1730
1794
  }
1731
1795
  }
1732
1796
  return;
@@ -1734,6 +1798,7 @@ var useChatUI = (options) => {
1734
1798
  if (enableProjects) {
1735
1799
  migrateSessionsToProjects(storageKey);
1736
1800
  }
1801
+ if (typeof window === "undefined") return;
1737
1802
  const saved = localStorage.getItem(storageKey);
1738
1803
  if (saved) {
1739
1804
  try {
@@ -1755,7 +1820,22 @@ var useChatUI = (options) => {
1755
1820
  } catch {
1756
1821
  }
1757
1822
  }
1758
- }, [storageKey, useExternalStorage, onLoadSessions, initialModel, models, onError]);
1823
+ }, [storageKey, useExternalStorage, initialModel, models]);
1824
+ useEffect3(() => {
1825
+ if (!onLoadModelsRef.current) return;
1826
+ setIsModelsLoading(true);
1827
+ onLoadModelsRef.current().then((modelList) => {
1828
+ setLoadedModels(modelList);
1829
+ if (modelList.length > 0 && !modelList.find((m) => m.id === selectedModel)) {
1830
+ setSelectedModel(modelList[0].id);
1831
+ }
1832
+ }).catch((error) => {
1833
+ onErrorRef.current?.(error instanceof Error ? error : new Error("Failed to load models"));
1834
+ }).finally(() => {
1835
+ setIsModelsLoading(false);
1836
+ });
1837
+ }, []);
1838
+ const effectiveModels = loadedModels || models;
1759
1839
  useEffect3(() => {
1760
1840
  if (typeof window === "undefined") return;
1761
1841
  if (useExternalStorage) return;
@@ -1768,8 +1848,8 @@ var useChatUI = (options) => {
1768
1848
  localStorage.setItem(`${storageKey}_personalization`, JSON.stringify(personalization));
1769
1849
  }, [personalization, storageKey]);
1770
1850
  useEffect3(() => {
1771
- onSessionChange?.(currentSession);
1772
- }, [currentSession, onSessionChange]);
1851
+ onSessionChangeRef.current?.(currentSession);
1852
+ }, [currentSession]);
1773
1853
  const buildSystemPrompt = useCallback5(() => {
1774
1854
  const parts = [];
1775
1855
  const { userProfile, responseStyle, language } = personalization;
@@ -1975,10 +2055,10 @@ ${newConversation}
1975
2055
  }, []);
1976
2056
  const newSession = useCallback5(async () => {
1977
2057
  const projectId = enableProjects ? projectHook.currentProjectId || DEFAULT_PROJECT_ID : void 0;
1978
- if (useExternalStorage && onCreateSession) {
2058
+ if (useExternalStorage && onCreateSessionRef.current) {
1979
2059
  setIsSessionLoading(true);
1980
2060
  try {
1981
- const created = await onCreateSession();
2061
+ const created = await onCreateSessionRef.current();
1982
2062
  const now2 = Date.now();
1983
2063
  const newSess2 = {
1984
2064
  id: created.id,
@@ -1992,7 +2072,7 @@ ${newConversation}
1992
2072
  setSessions((prev) => [newSess2, ...prev]);
1993
2073
  setCurrentSessionId(newSess2.id);
1994
2074
  } catch (error) {
1995
- onError?.(error instanceof Error ? error : new Error("Failed to create session"));
2075
+ onErrorRef.current?.(error instanceof Error ? error : new Error("Failed to create session"));
1996
2076
  } finally {
1997
2077
  setIsSessionLoading(false);
1998
2078
  }
@@ -2010,12 +2090,12 @@ ${newConversation}
2010
2090
  };
2011
2091
  setSessions((prev) => [newSess, ...prev]);
2012
2092
  setCurrentSessionId(newSess.id);
2013
- }, [selectedModel, useExternalStorage, onCreateSession, onError, enableProjects, projectHook.currentProjectId]);
2093
+ }, [selectedModel, useExternalStorage, enableProjects, projectHook.currentProjectId]);
2014
2094
  const selectSession = useCallback5(async (id) => {
2015
- if (useExternalStorage && onLoadSession) {
2095
+ if (useExternalStorage && onLoadSessionRef.current) {
2016
2096
  setIsSessionLoading(true);
2017
2097
  try {
2018
- const sessionDetail = await onLoadSession(id);
2098
+ const sessionDetail = await onLoadSessionRef.current(id);
2019
2099
  let loadedMessages = sessionDetail.messages.map((m, idx) => ({
2020
2100
  id: m.id || generateId3("msg"),
2021
2101
  role: typeof m.role === "string" ? m.role.toLowerCase() : m.role,
@@ -2061,7 +2141,7 @@ ${newConversation}
2061
2141
  setSelectedModel(existingSession.model);
2062
2142
  }
2063
2143
  } catch (error) {
2064
- onError?.(error instanceof Error ? error : new Error("Failed to load session"));
2144
+ onErrorRef.current?.(error instanceof Error ? error : new Error("Failed to load session"));
2065
2145
  const cached = readSessionCache(storageKey, id);
2066
2146
  if (cached && cached.messages.length > 0) {
2067
2147
  console.warn("[useChatUI] onLoadSession failed, using localStorage cache");
@@ -2086,11 +2166,11 @@ ${newConversation}
2086
2166
  setCurrentSessionId(id);
2087
2167
  setSelectedModel(session.model);
2088
2168
  }
2089
- }, [sessions, useExternalStorage, onLoadSession, onError, storageKey, initialModel, models]);
2169
+ }, [sessions, useExternalStorage, storageKey, initialModel, models]);
2090
2170
  const deleteSession = useCallback5(async (id) => {
2091
- if (useExternalStorage && onDeleteSessionCallback) {
2171
+ if (useExternalStorage && onDeleteSessionCallbackRef.current) {
2092
2172
  try {
2093
- await onDeleteSessionCallback(id);
2173
+ await onDeleteSessionCallbackRef.current(id);
2094
2174
  removeSessionCache(storageKey, id);
2095
2175
  setSessions((prev) => {
2096
2176
  const filtered = prev.filter((s) => s.id !== id);
@@ -2100,7 +2180,7 @@ ${newConversation}
2100
2180
  return filtered;
2101
2181
  });
2102
2182
  } catch (error) {
2103
- onError?.(error instanceof Error ? error : new Error("Failed to delete session"));
2183
+ onErrorRef.current?.(error instanceof Error ? error : new Error("Failed to delete session"));
2104
2184
  }
2105
2185
  return;
2106
2186
  }
@@ -2114,20 +2194,20 @@ ${newConversation}
2114
2194
  }
2115
2195
  return filtered;
2116
2196
  });
2117
- }, [currentSessionId, storageKey, useExternalStorage, onDeleteSessionCallback, onError]);
2197
+ }, [currentSessionId, storageKey, useExternalStorage]);
2118
2198
  const renameSession = useCallback5(async (id, newTitle) => {
2119
2199
  if (!newTitle.trim()) return;
2120
- if (useExternalStorage && onUpdateSessionTitle) {
2200
+ if (useExternalStorage && onUpdateSessionTitleRef.current) {
2121
2201
  try {
2122
- await onUpdateSessionTitle(id, newTitle.trim());
2202
+ await onUpdateSessionTitleRef.current(id, newTitle.trim());
2123
2203
  setSessions(
2124
2204
  (prev) => prev.map(
2125
2205
  (s) => s.id === id ? { ...s, title: newTitle.trim(), updatedAt: Date.now() } : s
2126
2206
  )
2127
2207
  );
2128
- onTitleChange?.(id, newTitle.trim());
2208
+ onTitleChangeRef.current?.(id, newTitle.trim());
2129
2209
  } catch (error) {
2130
- onError?.(error instanceof Error ? error : new Error("Failed to update session title"));
2210
+ onErrorRef.current?.(error instanceof Error ? error : new Error("Failed to update session title"));
2131
2211
  }
2132
2212
  return;
2133
2213
  }
@@ -2136,8 +2216,8 @@ ${newConversation}
2136
2216
  (s) => s.id === id ? { ...s, title: newTitle.trim(), updatedAt: Date.now() } : s
2137
2217
  )
2138
2218
  );
2139
- onTitleChange?.(id, newTitle.trim());
2140
- }, [onTitleChange, useExternalStorage, onUpdateSessionTitle, onError]);
2219
+ onTitleChangeRef.current?.(id, newTitle.trim());
2220
+ }, [useExternalStorage]);
2141
2221
  const setModel = useCallback5((model) => {
2142
2222
  setSelectedModel(model);
2143
2223
  if (currentSessionId) {
@@ -2171,13 +2251,13 @@ ${newConversation}
2171
2251
  const updatePersonalization = useCallback5((config) => {
2172
2252
  setPersonalization((prev) => {
2173
2253
  const next = { ...prev, ...config };
2174
- options.onPersonalizationChange?.(next);
2254
+ onPersonalizationChangeRef.current?.(next);
2175
2255
  return next;
2176
2256
  });
2177
- }, [options.onPersonalizationChange]);
2257
+ }, []);
2178
2258
  const savePersonalization = useCallback5(() => {
2179
- options.onPersonalizationSave?.(personalization);
2180
- }, [options.onPersonalizationSave, personalization]);
2259
+ onPersonalizationSaveRef.current?.(personalization);
2260
+ }, [personalization]);
2181
2261
  const addAttachments = useCallback5((files) => {
2182
2262
  const newAttachments = files.map((file) => {
2183
2263
  const isImage = file.type.startsWith("image/");
@@ -2210,9 +2290,9 @@ ${newConversation}
2210
2290
  if (!messageContent.trim() || isLoading) return;
2211
2291
  let sessionId = currentSessionId;
2212
2292
  if (!sessionId) {
2213
- if (useExternalStorage && onCreateSession) {
2293
+ if (useExternalStorage && onCreateSessionRef.current) {
2214
2294
  try {
2215
- const created = await onCreateSession();
2295
+ const created = await onCreateSessionRef.current();
2216
2296
  const now = Date.now();
2217
2297
  const newSess = {
2218
2298
  id: created.id,
@@ -2226,7 +2306,7 @@ ${newConversation}
2226
2306
  sessionId = newSess.id;
2227
2307
  setCurrentSessionId(sessionId);
2228
2308
  } catch (error) {
2229
- onError?.(error instanceof Error ? error : new Error("Failed to create session"));
2309
+ onErrorRef.current?.(error instanceof Error ? error : new Error("Failed to create session"));
2230
2310
  setIsLoading(false);
2231
2311
  return;
2232
2312
  }
@@ -2279,13 +2359,66 @@ ${finalContent}`;
2279
2359
  ...isHidden && { hidden: true },
2280
2360
  ...userContentParts && { contentParts: userContentParts }
2281
2361
  };
2362
+ let attachmentResults = [];
2363
+ if (currentAttachments.length > 0) {
2364
+ const attachmentSkills = Object.entries(resolvedSkills).filter(
2365
+ ([, config]) => config.trigger === "attachment"
2366
+ );
2367
+ for (const [skillName, skillConfig] of attachmentSkills) {
2368
+ const matchedFiles = currentAttachments.filter((att) => {
2369
+ if (!skillConfig.acceptedTypes || skillConfig.acceptedTypes.length === 0) return true;
2370
+ return skillConfig.acceptedTypes.some((type) => {
2371
+ if (type.startsWith(".")) return att.name.toLowerCase().endsWith(type.toLowerCase());
2372
+ if (type.includes("*")) {
2373
+ const regex = new RegExp("^" + type.replace("*", ".*") + "$");
2374
+ return regex.test(att.mimeType);
2375
+ }
2376
+ return att.mimeType === type;
2377
+ });
2378
+ });
2379
+ if (matchedFiles.length === 0) continue;
2380
+ try {
2381
+ const filesToPass = skillConfig.autoConvertBase64 ? await convertAttachmentsToBase64(matchedFiles) : matchedFiles;
2382
+ const result = await skillConfig.execute({ files: filesToPass, userMessage: finalContent });
2383
+ attachmentResults.push({
2384
+ type: "tool_result",
2385
+ toolName: skillName,
2386
+ label: skillConfig.label,
2387
+ icon: skillConfig.icon,
2388
+ result: {
2389
+ type: "text",
2390
+ content: result.content,
2391
+ metadata: result.metadata,
2392
+ sources: result.sources
2393
+ }
2394
+ });
2395
+ } catch (error) {
2396
+ console.error(`[useChatUI] attachment skill ${skillName} failed:`, error);
2397
+ }
2398
+ }
2399
+ }
2400
+ let shouldContinueAfterAttachment = continueAfterToolResult;
2401
+ if (attachmentResults.length > 0) {
2402
+ for (const part of attachmentResults) {
2403
+ if (part.type === "tool_result" && onSkillCompleteRef.current) {
2404
+ const decision = onSkillCompleteRef.current(part.toolName, {
2405
+ content: part.result.content,
2406
+ metadata: part.result.metadata,
2407
+ sources: part.result.sources
2408
+ });
2409
+ shouldContinueAfterAttachment = decision === "continue";
2410
+ if (!shouldContinueAfterAttachment) break;
2411
+ }
2412
+ }
2413
+ }
2282
2414
  const assistantMessageId = generateId3("msg");
2283
2415
  const assistantMessage = {
2284
2416
  id: assistantMessageId,
2285
2417
  role: "assistant",
2286
2418
  content: "",
2287
2419
  model: selectedModel,
2288
- timestamp: Date.now()
2420
+ timestamp: Date.now(),
2421
+ ...attachmentResults.length > 0 && { contentParts: attachmentResults }
2289
2422
  };
2290
2423
  setInput("");
2291
2424
  setQuotedText(null);
@@ -2312,19 +2445,23 @@ ${finalContent}`;
2312
2445
  return s;
2313
2446
  })
2314
2447
  );
2315
- if (isFirstMessage && generateTitleCallback) {
2316
- Promise.resolve(generateTitleCallback(finalContent)).then((generatedTitle) => {
2448
+ if (isFirstMessage && generateTitleRef.current) {
2449
+ Promise.resolve(generateTitleRef.current(finalContent)).then((generatedTitle) => {
2317
2450
  if (generatedTitle && generatedTitle.trim()) {
2318
2451
  setSessions(
2319
2452
  (prev) => prev.map(
2320
2453
  (s) => s.id === capturedSessionId ? { ...s, title: generatedTitle.trim(), updatedAt: Date.now() } : s
2321
2454
  )
2322
2455
  );
2323
- onTitleChange?.(capturedSessionId, generatedTitle.trim());
2456
+ onTitleChangeRef.current?.(capturedSessionId, generatedTitle.trim());
2324
2457
  }
2325
2458
  }).catch(() => {
2326
2459
  });
2327
2460
  }
2461
+ if (attachmentResults.length > 0 && !shouldContinueAfterAttachment) {
2462
+ setIsLoading(false);
2463
+ return;
2464
+ }
2328
2465
  setIsLoading(true);
2329
2466
  abortControllerRef.current = new AbortController();
2330
2467
  try {
@@ -2384,6 +2521,20 @@ ${currentContextSummary}` },
2384
2521
  } else {
2385
2522
  chatMessages = messagesToSend.map((m) => ({ role: m.role, content: m.content }));
2386
2523
  }
2524
+ if (attachmentResults.length > 0 && shouldContinueAfterAttachment) {
2525
+ const attachmentContext = attachmentResults.filter((part) => part.type === "tool_result").map((part) => `[${part.label || part.toolName} \uACB0\uACFC]
2526
+ ${part.result.content}`).join("\n\n");
2527
+ if (attachmentContext) {
2528
+ chatMessages.push({
2529
+ role: "user",
2530
+ content: `\uD30C\uC77C \uBD84\uC11D \uACB0\uACFC:
2531
+
2532
+ ${attachmentContext}
2533
+
2534
+ \uC704 \uACB0\uACFC\uB97C \uCC38\uACE0\uD558\uC5EC \uB2F5\uBCC0\uD574\uC8FC\uC138\uC694.`
2535
+ });
2536
+ }
2537
+ }
2387
2538
  console.log("[ChatUI] Messages to send:", chatMessages.length, chatMessages.map((m) => ({ role: m.role, content: m.content.slice(0, 50) })));
2388
2539
  const baseSystemPrompt = buildSystemPrompt();
2389
2540
  const combinedSystemPrompt = [baseSystemPrompt, actionPrompt].filter(Boolean).join("\n\n");
@@ -2648,6 +2799,16 @@ ${currentContextSummary}` },
2648
2799
  abortControllerRef.current = null;
2649
2800
  return;
2650
2801
  }
2802
+ let shouldContinue = continueAfterToolResult;
2803
+ if (onSkillCompleteRef.current) {
2804
+ const decision = onSkillCompleteRef.current(toolName, result);
2805
+ shouldContinue = decision === "continue";
2806
+ }
2807
+ if (!shouldContinue) {
2808
+ setIsLoading(false);
2809
+ abortControllerRef.current = null;
2810
+ return;
2811
+ }
2651
2812
  skipNextSkillParsingRef.current = true;
2652
2813
  const feedbackPrompt = resultType === "error" ? `\uB3C4\uAD6C "${toolName}" \uC2E4\uD589 \uC911 \uC624\uB958 \uBC1C\uC0DD: ${result.content}
2653
2814
 
@@ -2688,6 +2849,16 @@ ${result.content}
2688
2849
  abortControllerRef.current = null;
2689
2850
  return;
2690
2851
  }
2852
+ let shouldContinueSkill = continueAfterToolResult;
2853
+ if (onSkillCompleteRef.current) {
2854
+ const decision = onSkillCompleteRef.current(detectedSkill.name, result);
2855
+ shouldContinueSkill = decision === "continue";
2856
+ }
2857
+ if (!shouldContinueSkill) {
2858
+ setIsLoading(false);
2859
+ abortControllerRef.current = null;
2860
+ return;
2861
+ }
2691
2862
  skipNextSkillParsingRef.current = true;
2692
2863
  const resultPrompt = `\uC2A4\uD0AC "${detectedSkill.name}" \uC2E4\uD589 \uACB0\uACFC:
2693
2864
 
@@ -2727,7 +2898,7 @@ ${result.content}
2727
2898
  );
2728
2899
  if (useExternalStorage && capturedSessionId) {
2729
2900
  const assistantContentForSave = accumulatedContent;
2730
- if (assistantContentForSave && onSaveMessages) {
2901
+ if (assistantContentForSave && onSaveMessagesRef.current) {
2731
2902
  const latestSession = sessionsRef.current.find((s) => s.id === capturedSessionId);
2732
2903
  const latestMessages = latestSession?.messages || [];
2733
2904
  const userMsg = latestMessages.find((m) => m.role === "user" && m.content === finalContent);
@@ -2736,7 +2907,7 @@ ${result.content}
2736
2907
  { role: "user", message: finalContent, ...userMsg?.contentParts && { contentParts: userMsg.contentParts } },
2737
2908
  { role: "assistant", message: assistantContentForSave, ...assistantMsg?.contentParts && { contentParts: assistantMsg.contentParts } }
2738
2909
  ];
2739
- onSaveMessages(capturedSessionId, messagesToSave).catch((saveError) => {
2910
+ onSaveMessagesRef.current(capturedSessionId, messagesToSave).catch((saveError) => {
2740
2911
  console.error("[useChatUI] Failed to save messages:", saveError);
2741
2912
  });
2742
2913
  }
@@ -2764,7 +2935,7 @@ ${result.content}
2764
2935
  return;
2765
2936
  }
2766
2937
  const err = error instanceof Error ? error : new Error("Unknown error");
2767
- onError?.(err);
2938
+ onErrorRef.current?.(err);
2768
2939
  setSessions(
2769
2940
  (prev) => prev.map((s) => {
2770
2941
  if (s.id === capturedSessionId) {
@@ -3037,11 +3208,11 @@ ${formattedParts.join("\n")}
3037
3208
  return;
3038
3209
  }
3039
3210
  console.error("[ChatUI] Regenerate error:", error);
3040
- onError?.(error instanceof Error ? error : new Error("Unknown error"));
3211
+ onErrorRef.current?.(error instanceof Error ? error : new Error("Unknown error"));
3041
3212
  } finally {
3042
3213
  setIsLoading(false);
3043
3214
  }
3044
- }, [currentSession, currentSessionId, isLoading, selectedModel, models, apiEndpoint, apiKey, buildSystemPrompt, onError]);
3215
+ }, [currentSession, currentSessionId, isLoading, selectedModel, models, apiEndpoint, apiKey, buildSystemPrompt]);
3045
3216
  const askOtherModel = useCallback5(async (messageId, targetModel) => {
3046
3217
  if (!currentSession || !currentSessionId || isLoading) return;
3047
3218
  const assistantIndex = currentSession.messages.findIndex((m) => m.id === messageId);
@@ -3193,7 +3364,7 @@ ${currentSession.contextSummary}` },
3193
3364
  return;
3194
3365
  }
3195
3366
  const err = error instanceof Error ? error : new Error("Unknown error");
3196
- onError?.(err);
3367
+ onErrorRef.current?.(err);
3197
3368
  } finally {
3198
3369
  setIsLoading(false);
3199
3370
  setLoadingAlternativeFor(null);
@@ -3262,7 +3433,8 @@ ${currentSession.contextSummary}` },
3262
3433
  getActiveAlternative,
3263
3434
  updatePersonalization,
3264
3435
  savePersonalization,
3265
- models,
3436
+ models: effectiveModels,
3437
+ isModelsLoading,
3266
3438
  // Memory
3267
3439
  globalMemory,
3268
3440
  compressionState,
@@ -3350,7 +3522,25 @@ ${currentSession.contextSummary}` },
3350
3522
  systemPrompt: `\uC0AC\uC6A9\uC790\uAC00 "${toolConfig.label || skillName}" \uB3C4\uAD6C\uB97C \uC0AC\uC6A9\uD558\uB824\uACE0 \uD569\uB2C8\uB2E4. \uC0AC\uC6A9\uC790\uC758 \uBA54\uC2DC\uC9C0\uB97C \uBC14\uD0D5\uC73C\uB85C \uBC18\uB4DC\uC2DC <skill_use name="${skillName}"> \uD0DC\uADF8\uB97C \uC0AC\uC6A9\uD558\uC5EC \uC774 \uB3C4\uAD6C\uB97C \uD638\uCD9C\uD558\uC138\uC694. \uB3C4\uAD6C\uB97C \uC0AC\uC6A9\uD558\uC9C0 \uC54A\uACE0 \uD14D\uC2A4\uD2B8\uB85C\uB9CC \uB2F5\uBCC0\uD558\uC9C0 \uB9C8\uC138\uC694.`
3351
3523
  });
3352
3524
  } else {
3353
- executeManualSkill(skillName, { query: input });
3525
+ const currentQuery = input;
3526
+ executeManualSkill(skillName, { query: currentQuery }).then((result) => {
3527
+ if (!result || !result.content) return;
3528
+ let shouldContinue = continueAfterToolResult;
3529
+ if (onSkillCompleteRef.current) {
3530
+ const decision = onSkillCompleteRef.current(skillName, result);
3531
+ shouldContinue = decision === "continue";
3532
+ }
3533
+ if (!shouldContinue) return;
3534
+ skipNextSkillParsingRef.current = true;
3535
+ const resultPrompt = `\uC2A4\uD0AC "${skillName}" \uC2E4\uD589 \uACB0\uACFC:
3536
+
3537
+ ${result.content}
3538
+
3539
+ \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.`;
3540
+ setTimeout(() => {
3541
+ sendMessage(resultPrompt, { hiddenUserMessage: true });
3542
+ }, 100);
3543
+ });
3354
3544
  }
3355
3545
  },
3356
3546
  // Project
@@ -3375,8 +3565,8 @@ ${currentSession.contextSummary}` },
3375
3565
  if (!enableProjects) return;
3376
3566
  const projectSessions = sessionsRef.current.filter((s) => s.projectId === projectId);
3377
3567
  for (const sess of projectSessions) {
3378
- if (useExternalStorage && onDeleteSessionCallback) {
3379
- await onDeleteSessionCallback(sess.id).catch(() => {
3568
+ if (useExternalStorage && onDeleteSessionCallbackRef.current) {
3569
+ await onDeleteSessionCallbackRef.current(sess.id).catch(() => {
3380
3570
  });
3381
3571
  }
3382
3572
  removeSessionCache(storageKey, sess.id);
@@ -3736,7 +3926,8 @@ var ChatSidebar = ({
3736
3926
  onNewProject,
3737
3927
  onProjectSettings,
3738
3928
  renderAfterHeader,
3739
- renderFooter
3929
+ renderFooter,
3930
+ isLoading = false
3740
3931
  }) => {
3741
3932
  const sidebarWidth = typeof widthProp === "number" ? `${widthProp}px` : widthProp || "288px";
3742
3933
  const [editingId, setEditingId] = useState7(null);
@@ -3780,6 +3971,7 @@ var ChatSidebar = ({
3780
3971
  className: `chatllm-sidebar chatllm-sidebar-transition ${theme ? `chatllm-root ${themeClass}` : ""}`,
3781
3972
  style: {
3782
3973
  width: isOpen ? sidebarWidth : "0",
3974
+ height: "100%",
3783
3975
  flexShrink: 0,
3784
3976
  backgroundColor: "var(--chatllm-sidebar-bg)",
3785
3977
  borderRight: isOpen ? "1px solid var(--chatllm-border)" : "none",
@@ -3884,7 +4076,22 @@ var ChatSidebar = ({
3884
4076
  display: "flex",
3885
4077
  flexDirection: "column"
3886
4078
  },
3887
- children: sessions.length === 0 ? (
4079
+ children: isLoading ? (
4080
+ /* Skeleton Loading State */
4081
+ /* @__PURE__ */ jsx3("div", { style: { display: "flex", flexDirection: "column", gap: "8px", padding: "0 4px" }, children: [1, 2, 3, 4].map((i) => /* @__PURE__ */ jsx3(
4082
+ "div",
4083
+ {
4084
+ className: "chatllm-skeleton-pulse",
4085
+ style: {
4086
+ height: "48px",
4087
+ borderRadius: "8px",
4088
+ backgroundColor: "var(--chatllm-bg-tertiary)",
4089
+ opacity: 1 - (i - 1) * 0.15
4090
+ }
4091
+ },
4092
+ i
4093
+ )) })
4094
+ ) : sessions.length === 0 ? (
3888
4095
  /* Empty State */
3889
4096
  /* @__PURE__ */ jsxs2(
3890
4097
  "div",
@@ -4827,6 +5034,16 @@ var ChatInput = ({
4827
5034
  }
4828
5035
  )
4829
5036
  ] }),
5037
+ /* @__PURE__ */ jsx5(
5038
+ "button",
5039
+ {
5040
+ onClick: () => fileInputRef.current?.click(),
5041
+ style: iconButtonStyle,
5042
+ title: "\uD30C\uC77C \uCCA8\uBD80",
5043
+ "aria-label": "\uD30C\uC77C \uCCA8\uBD80",
5044
+ children: /* @__PURE__ */ jsx5(IconSvg, { name: "attachment-line", size: 22, color: "var(--chatllm-text-muted)" })
5045
+ }
5046
+ ),
4830
5047
  /* @__PURE__ */ jsxs4("div", { ref: mainMenuRef, style: { position: "relative" }, children: [
4831
5048
  /* @__PURE__ */ jsx5(
4832
5049
  "button",
@@ -6812,6 +7029,7 @@ var PollCard = ({
6812
7029
  onSkip?.();
6813
7030
  }, [questions, onSubmit, onSkip]);
6814
7031
  useEffect7(() => {
7032
+ if (typeof window === "undefined") return;
6815
7033
  const handleKeyDown = (e) => {
6816
7034
  if (e.key === "Escape") handleSkip();
6817
7035
  };
@@ -7756,7 +7974,7 @@ var MessageBubble = ({
7756
7974
  const displaySources = isAssistant && relevantAlternatives && relevantAlternatives.length > 0 && relevantActiveIndex > 0 ? relevantAlternatives[relevantActiveIndex - 1]?.sources : message.sources;
7757
7975
  const handleMouseUp = () => {
7758
7976
  if (!onQuote) return;
7759
- const selection = window.getSelection();
7977
+ const selection = typeof window !== "undefined" ? window.getSelection() : null;
7760
7978
  const text = selection?.toString().trim();
7761
7979
  if (text && text.length > 0) {
7762
7980
  }
@@ -8371,7 +8589,7 @@ var MessageList = ({
8371
8589
  messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
8372
8590
  }, [messages]);
8373
8591
  const handleMouseUp = useCallback8(() => {
8374
- const selection = window.getSelection();
8592
+ const selection = typeof window !== "undefined" ? window.getSelection() : null;
8375
8593
  const text = selection?.toString().trim();
8376
8594
  if (text && text.length > 0) {
8377
8595
  const range = selection?.getRangeAt(0);
@@ -8387,7 +8605,7 @@ var MessageList = ({
8387
8605
  }
8388
8606
  } else {
8389
8607
  setTimeout(() => {
8390
- const currentSelection = window.getSelection()?.toString().trim();
8608
+ const currentSelection = typeof window !== "undefined" ? window.getSelection()?.toString().trim() : void 0;
8391
8609
  if (!currentSelection) {
8392
8610
  setSelectionPosition(null);
8393
8611
  }
@@ -8399,7 +8617,9 @@ var MessageList = ({
8399
8617
  onQuote(selectedText);
8400
8618
  setSelectionPosition(null);
8401
8619
  setSelectedText("");
8402
- window.getSelection()?.removeAllRanges();
8620
+ if (typeof window !== "undefined") {
8621
+ window.getSelection()?.removeAllRanges();
8622
+ }
8403
8623
  }
8404
8624
  };
8405
8625
  return /* @__PURE__ */ jsxs15(
@@ -9057,7 +9277,7 @@ var SettingsModal = ({
9057
9277
  "button",
9058
9278
  {
9059
9279
  onClick: () => {
9060
- if (window.confirm("\uBAA8\uB4E0 \uB300\uD654 \uAE30\uB85D\uC744 \uC0AD\uC81C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?")) {
9280
+ if (typeof window !== "undefined" && window.confirm("\uBAA8\uB4E0 \uB300\uD654 \uAE30\uB85D\uC744 \uC0AD\uC81C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?")) {
9061
9281
  onClearAllData();
9062
9282
  }
9063
9283
  },
@@ -9328,7 +9548,7 @@ var MemoryTabContent = ({ items, contextSummary, onDelete, onClearAll, title = "
9328
9548
  "button",
9329
9549
  {
9330
9550
  onClick: () => {
9331
- if (window.confirm("\uBAA8\uB4E0 AI \uBA54\uBAA8\uB9AC\uB97C \uC0AD\uC81C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?")) {
9551
+ if (typeof window !== "undefined" && window.confirm("\uBAA8\uB4E0 AI \uBA54\uBAA8\uB9AC\uB97C \uC0AD\uC81C\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?")) {
9332
9552
  onClearAll();
9333
9553
  }
9334
9554
  },
@@ -9575,7 +9795,7 @@ var ProjectSettingsModal = ({
9575
9795
  "button",
9576
9796
  {
9577
9797
  onClick: () => {
9578
- if (window.confirm(`"${project.title}" \uD504\uB85C\uC81D\uD2B8\uC640 \uC18C\uC18D \uB300\uD654\uAC00 \uBAA8\uB450 \uC0AD\uC81C\uB429\uB2C8\uB2E4. \uACC4\uC18D\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?`)) {
9798
+ if (typeof window !== "undefined" && window.confirm(`"${project.title}" \uD504\uB85C\uC81D\uD2B8\uC640 \uC18C\uC18D \uB300\uD654\uAC00 \uBAA8\uB450 \uC0AD\uC81C\uB429\uB2C8\uB2E4. \uACC4\uC18D\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C?`)) {
9579
9799
  onDeleteProject(project.id);
9580
9800
  onClose();
9581
9801
  }
@@ -10080,7 +10300,8 @@ var ChatUIView = ({
10080
10300
  projectSettingsOpen,
10081
10301
  openProjectSettings,
10082
10302
  closeProjectSettings,
10083
- projectMemory
10303
+ projectMemory,
10304
+ isSessionsLoading
10084
10305
  } = state;
10085
10306
  const greeting = currentPersonalization.userProfile.nickname ? `\uC548\uB155\uD558\uC138\uC694, ${currentPersonalization.userProfile.nickname}\uB2D8` : "\uC548\uB155\uD558\uC138\uC694";
10086
10307
  const handleTemplateClick = (template) => {
@@ -10154,6 +10375,7 @@ var ChatUIView = ({
10154
10375
  currentProjectId,
10155
10376
  onSelectProject: projects.length > 0 ? selectProject : void 0,
10156
10377
  onNewProject: projects.length > 0 ? () => {
10378
+ if (typeof window === "undefined") return;
10157
10379
  const title = window.prompt("\uD504\uB85C\uC81D\uD2B8 \uC774\uB984\uC744 \uC785\uB825\uD558\uC138\uC694");
10158
10380
  if (title?.trim()) {
10159
10381
  createProject({ title: title.trim() });
@@ -10164,7 +10386,8 @@ var ChatUIView = ({
10164
10386
  openProjectSettings();
10165
10387
  } : void 0,
10166
10388
  renderAfterHeader: sidebarRenderAfterHeader,
10167
- renderFooter: sidebarRenderFooter
10389
+ renderFooter: sidebarRenderFooter,
10390
+ isLoading: isSessionsLoading
10168
10391
  }
10169
10392
  ),
10170
10393
  /* @__PURE__ */ jsxs19(
@@ -10336,6 +10559,9 @@ var ChatUIWithHook = ({
10336
10559
  skills,
10337
10560
  tools,
10338
10561
  onToolCall,
10562
+ continueAfterToolResult,
10563
+ onSkillComplete,
10564
+ onLoadModels,
10339
10565
  // Project options
10340
10566
  enableProjects,
10341
10567
  onLoadProjects,
@@ -10375,6 +10601,9 @@ var ChatUIWithHook = ({
10375
10601
  skills,
10376
10602
  tools,
10377
10603
  onToolCall,
10604
+ continueAfterToolResult,
10605
+ onSkillComplete,
10606
+ onLoadModels,
10378
10607
  enableProjects,
10379
10608
  onLoadProjects,
10380
10609
  onCreateProject,