@geminilight/mindos 0.6.58 → 0.6.59

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.
Files changed (219) hide show
  1. package/_standalone/.mindos-build-version +1 -1
  2. package/_standalone/.next/BUILD_ID +1 -1
  3. package/_standalone/.next/app-path-routes-manifest.json +24 -24
  4. package/_standalone/.next/build-manifest.json +3 -3
  5. package/_standalone/.next/cache/.previewinfo +1 -1
  6. package/_standalone/.next/cache/.rscinfo +1 -1
  7. package/_standalone/.next/cache/config.json +3 -3
  8. package/_standalone/.next/prerender-manifest.json +3 -3
  9. package/_standalone/.next/react-loadable-manifest.json +1 -1
  10. package/_standalone/.next/server/app/.well-known/agent-card.json/route_client-reference-manifest.js +1 -1
  11. package/_standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  12. package/_standalone/.next/server/app/_global-error.html +2 -2
  13. package/_standalone/.next/server/app/_global-error.rsc +1 -1
  14. package/_standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  15. package/_standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  16. package/_standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  17. package/_standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  18. package/_standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  19. package/_standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  20. package/_standalone/.next/server/app/_not-found/page.js +1 -1
  21. package/_standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
  22. package/_standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  23. package/_standalone/.next/server/app/agents/[agentKey]/page.js +1 -1
  24. package/_standalone/.next/server/app/agents/[agentKey]/page.js.nft.json +1 -1
  25. package/_standalone/.next/server/app/agents/[agentKey]/page_client-reference-manifest.js +1 -1
  26. package/_standalone/.next/server/app/agents/page.js +2 -2
  27. package/_standalone/.next/server/app/agents/page.js.nft.json +1 -1
  28. package/_standalone/.next/server/app/agents/page_client-reference-manifest.js +1 -1
  29. package/_standalone/.next/server/app/api/a2a/agents/route_client-reference-manifest.js +1 -1
  30. package/_standalone/.next/server/app/api/a2a/delegations/route_client-reference-manifest.js +1 -1
  31. package/_standalone/.next/server/app/api/a2a/discover/route_client-reference-manifest.js +1 -1
  32. package/_standalone/.next/server/app/api/a2a/route_client-reference-manifest.js +1 -1
  33. package/_standalone/.next/server/app/api/acp/config/route_client-reference-manifest.js +1 -1
  34. package/_standalone/.next/server/app/api/acp/detect/route_client-reference-manifest.js +1 -1
  35. package/_standalone/.next/server/app/api/acp/install/route_client-reference-manifest.js +1 -1
  36. package/_standalone/.next/server/app/api/acp/registry/route_client-reference-manifest.js +1 -1
  37. package/_standalone/.next/server/app/api/acp/session/route_client-reference-manifest.js +1 -1
  38. package/_standalone/.next/server/app/api/agent-activity/route_client-reference-manifest.js +1 -1
  39. package/_standalone/.next/server/app/api/agents/custom/detect/route_client-reference-manifest.js +1 -1
  40. package/_standalone/.next/server/app/api/agents/custom/route_client-reference-manifest.js +1 -1
  41. package/_standalone/.next/server/app/api/ask/route.js +3 -3
  42. package/_standalone/.next/server/app/api/ask/route_client-reference-manifest.js +1 -1
  43. package/_standalone/.next/server/app/api/ask-sessions/route_client-reference-manifest.js +1 -1
  44. package/_standalone/.next/server/app/api/auth/route_client-reference-manifest.js +1 -1
  45. package/_standalone/.next/server/app/api/backlinks/route_client-reference-manifest.js +1 -1
  46. package/_standalone/.next/server/app/api/bootstrap/route_client-reference-manifest.js +1 -1
  47. package/_standalone/.next/server/app/api/changes/route_client-reference-manifest.js +1 -1
  48. package/_standalone/.next/server/app/api/export/route_client-reference-manifest.js +1 -1
  49. package/_standalone/.next/server/app/api/extract-pdf/route_client-reference-manifest.js +1 -1
  50. package/_standalone/.next/server/app/api/file/import/route_client-reference-manifest.js +1 -1
  51. package/_standalone/.next/server/app/api/file/raw/route_client-reference-manifest.js +1 -1
  52. package/_standalone/.next/server/app/api/file/route_client-reference-manifest.js +1 -1
  53. package/_standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  54. package/_standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  55. package/_standalone/.next/server/app/api/graph/route_client-reference-manifest.js +1 -1
  56. package/_standalone/.next/server/app/api/health/route_client-reference-manifest.js +1 -1
  57. package/_standalone/.next/server/app/api/inbox/route_client-reference-manifest.js +1 -1
  58. package/_standalone/.next/server/app/api/init/route_client-reference-manifest.js +1 -1
  59. package/_standalone/.next/server/app/api/mcp/agents/route_client-reference-manifest.js +1 -1
  60. package/_standalone/.next/server/app/api/mcp/install/route_client-reference-manifest.js +1 -1
  61. package/_standalone/.next/server/app/api/mcp/install-skill/route_client-reference-manifest.js +1 -1
  62. package/_standalone/.next/server/app/api/mcp/restart/route_client-reference-manifest.js +1 -1
  63. package/_standalone/.next/server/app/api/mcp/status/route_client-reference-manifest.js +1 -1
  64. package/_standalone/.next/server/app/api/mcp/uninstall/route_client-reference-manifest.js +1 -1
  65. package/_standalone/.next/server/app/api/monitoring/route_client-reference-manifest.js +1 -1
  66. package/_standalone/.next/server/app/api/recent-files/route_client-reference-manifest.js +1 -1
  67. package/_standalone/.next/server/app/api/restart/route_client-reference-manifest.js +1 -1
  68. package/_standalone/.next/server/app/api/search/route_client-reference-manifest.js +1 -1
  69. package/_standalone/.next/server/app/api/settings/list-models/route_client-reference-manifest.js +1 -1
  70. package/_standalone/.next/server/app/api/settings/reset-token/route_client-reference-manifest.js +1 -1
  71. package/_standalone/.next/server/app/api/settings/route_client-reference-manifest.js +1 -1
  72. package/_standalone/.next/server/app/api/settings/test-key/route_client-reference-manifest.js +1 -1
  73. package/_standalone/.next/server/app/api/setup/check-path/route_client-reference-manifest.js +1 -1
  74. package/_standalone/.next/server/app/api/setup/check-port/route_client-reference-manifest.js +1 -1
  75. package/_standalone/.next/server/app/api/setup/generate-token/route_client-reference-manifest.js +1 -1
  76. package/_standalone/.next/server/app/api/setup/ls/route_client-reference-manifest.js +1 -1
  77. package/_standalone/.next/server/app/api/setup/route_client-reference-manifest.js +1 -1
  78. package/_standalone/.next/server/app/api/skills/route_client-reference-manifest.js +1 -1
  79. package/_standalone/.next/server/app/api/sync/route_client-reference-manifest.js +1 -1
  80. package/_standalone/.next/server/app/api/tree-version/route_client-reference-manifest.js +1 -1
  81. package/_standalone/.next/server/app/api/uninstall/route_client-reference-manifest.js +1 -1
  82. package/_standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  83. package/_standalone/.next/server/app/api/update-check/route_client-reference-manifest.js +1 -1
  84. package/_standalone/.next/server/app/api/update-status/route_client-reference-manifest.js +1 -1
  85. package/_standalone/.next/server/app/api/workflows/route_client-reference-manifest.js +1 -1
  86. package/_standalone/.next/server/app/changes/page.js +2 -2
  87. package/_standalone/.next/server/app/changes/page.js.nft.json +1 -1
  88. package/_standalone/.next/server/app/changes/page_client-reference-manifest.js +1 -1
  89. package/_standalone/.next/server/app/echo/[segment]/page.js +3 -3
  90. package/_standalone/.next/server/app/echo/[segment]/page.js.nft.json +1 -1
  91. package/_standalone/.next/server/app/echo/[segment]/page_client-reference-manifest.js +1 -1
  92. package/_standalone/.next/server/app/echo/page.js +1 -1
  93. package/_standalone/.next/server/app/echo/page.js.nft.json +1 -1
  94. package/_standalone/.next/server/app/echo/page_client-reference-manifest.js +1 -1
  95. package/_standalone/.next/server/app/explore/page.js +2 -2
  96. package/_standalone/.next/server/app/explore/page.js.nft.json +1 -1
  97. package/_standalone/.next/server/app/explore/page_client-reference-manifest.js +1 -1
  98. package/_standalone/.next/server/app/help/page.js +2 -2
  99. package/_standalone/.next/server/app/help/page.js.nft.json +1 -1
  100. package/_standalone/.next/server/app/help/page_client-reference-manifest.js +1 -1
  101. package/_standalone/.next/server/app/inbox/history/page.js +1 -1
  102. package/_standalone/.next/server/app/inbox/history/page.js.nft.json +1 -1
  103. package/_standalone/.next/server/app/inbox/history/page_client-reference-manifest.js +1 -1
  104. package/_standalone/.next/server/app/login/page.js +2 -2
  105. package/_standalone/.next/server/app/login/page.js.nft.json +1 -1
  106. package/_standalone/.next/server/app/login/page_client-reference-manifest.js +1 -1
  107. package/_standalone/.next/server/app/page.js +2 -8
  108. package/_standalone/.next/server/app/page.js.nft.json +1 -1
  109. package/_standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  110. package/_standalone/.next/server/app/setup/page.js +2 -2
  111. package/_standalone/.next/server/app/setup/page.js.nft.json +1 -1
  112. package/_standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
  113. package/_standalone/.next/server/app/trash/page.js +3 -3
  114. package/_standalone/.next/server/app/trash/page.js.nft.json +1 -1
  115. package/_standalone/.next/server/app/trash/page_client-reference-manifest.js +1 -1
  116. package/_standalone/.next/server/app/view/[...path]/page.js +3 -3
  117. package/_standalone/.next/server/app/view/[...path]/page.js.nft.json +1 -1
  118. package/_standalone/.next/server/app/view/[...path]/page_client-reference-manifest.js +1 -1
  119. package/_standalone/.next/server/app/wiki/page.js +2 -2
  120. package/_standalone/.next/server/app/wiki/page.js.nft.json +1 -1
  121. package/_standalone/.next/server/app/wiki/page_client-reference-manifest.js +1 -1
  122. package/_standalone/.next/server/app-paths-manifest.json +24 -24
  123. package/_standalone/.next/server/chunks/3484.js +1 -1
  124. package/_standalone/.next/server/chunks/530.js +65 -66
  125. package/_standalone/.next/server/chunks/{6793.js → 8343.js} +2 -2
  126. package/_standalone/.next/server/chunks/9787.js +2 -0
  127. package/_standalone/.next/server/middleware-build-manifest.js +1 -1
  128. package/_standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  129. package/_standalone/.next/server/pages/500.html +2 -2
  130. package/_standalone/.next/server/server-reference-manifest.js +1 -1
  131. package/_standalone/.next/server/server-reference-manifest.json +1 -1
  132. package/_standalone/.next/static/chunks/{1814.a7c127b2c73d1f70.js → 1814.a79b84d37df75c43.js} +1 -1
  133. package/_standalone/.next/static/chunks/3427-2e61a5df1f5e55fb.js +1 -0
  134. package/_standalone/.next/static/chunks/{1053-fe009233cff06e72.js → 5581-dac72e9f16e5ea29.js} +3 -3
  135. package/_standalone/.next/static/chunks/6297-085daa21037d5f81.js +1 -0
  136. package/_standalone/.next/static/chunks/{7249-fa98ca10e9a10f39.js → 7249-6cf8f2b78718c59e.js} +1 -1
  137. package/_standalone/.next/static/chunks/8520-56ec9ff087c15204.js +22 -0
  138. package/_standalone/.next/static/chunks/9905-a19d379cb225246e.js +1 -0
  139. package/_standalone/.next/static/chunks/app/agents/[agentKey]/{page-7bdeab5af8e4f5f2.js → page-35ea6de1af2be3b5.js} +1 -1
  140. package/_standalone/.next/static/chunks/app/agents/page-b172ea3743adb047.js +1 -0
  141. package/_standalone/.next/static/chunks/app/changes/{page-5a72144d1080a699.js → page-6d2f49651c0061f7.js} +1 -1
  142. package/_standalone/.next/static/chunks/app/echo/[segment]/page-84b95256f6e38aae.js +11 -0
  143. package/_standalone/.next/static/chunks/app/explore/{page-d3d99308146c2240.js → page-d9f58000bc445360.js} +2 -2
  144. package/_standalone/.next/static/chunks/app/help/{page-222df603080b5fab.js → page-f8cb806371b3175f.js} +1 -1
  145. package/_standalone/.next/static/chunks/app/inbox/history/{page-07819cf95cb0805f.js → page-26e71fb6f716a4c4.js} +1 -1
  146. package/_standalone/.next/static/chunks/app/layout-b89b0d955f39a753.js +164 -0
  147. package/_standalone/.next/static/chunks/app/login/{page-0eeef685052869a6.js → page-18fb00d568cd1f0e.js} +1 -1
  148. package/_standalone/.next/static/chunks/app/page-a8e6f085f38388bf.js +1 -0
  149. package/_standalone/.next/static/chunks/app/setup/{page-99fcfc460fa29733.js → page-821714e7477be46c.js} +1 -1
  150. package/_standalone/.next/static/chunks/app/trash/{page-54cbd5c98d9de69b.js → page-f92b728b78ac0f7e.js} +1 -1
  151. package/_standalone/.next/static/chunks/app/view/[...path]/{not-found-fc04c2bd4f35bc6f.js → not-found-6e0c75ad26ce8572.js} +1 -1
  152. package/_standalone/.next/static/chunks/app/view/[...path]/page-f87f4901b5e1a88f.js +12 -0
  153. package/_standalone/.next/static/chunks/app/wiki/page-641edb1f3cff2f93.js +1 -0
  154. package/_standalone/.next/static/chunks/{webpack-2c19436659aa657b.js → webpack-72e8d9e9073fd1f9.js} +1 -1
  155. package/_standalone/.next/static/css/6c104b118d3bc9b7.css +1 -0
  156. package/_standalone/.next/trace +64 -64
  157. package/_standalone/app/globals.css +2 -1
  158. package/_standalone/components/AskFab.tsx +4 -4
  159. package/_standalone/components/AskModal.tsx +1 -1
  160. package/_standalone/components/GuideCard.tsx +101 -152
  161. package/_standalone/components/RightAskPanel.tsx +2 -2
  162. package/_standalone/components/ask/AskContent.tsx +90 -51
  163. package/_standalone/components/ask/AskHeader.tsx +218 -18
  164. package/_standalone/components/ask/MessageList.tsx +66 -47
  165. package/_standalone/components/ask/SessionHistory.tsx +86 -60
  166. package/_standalone/components/ask/SessionTabBar.tsx +29 -21
  167. package/_standalone/components/ask/ThinkingBlock.tsx +6 -5
  168. package/_standalone/components/ask/ToolCallBlock.tsx +10 -9
  169. package/_standalone/components/settings/SettingsContent.tsx +1 -1
  170. package/_standalone/data/skills/mindos/SKILL.md +67 -15
  171. package/_standalone/data/skills/mindos-zh/SKILL.md +67 -11
  172. package/_standalone/hooks/useAskSession.ts +23 -1
  173. package/_standalone/lib/stores/locale-store.ts +20 -6
  174. package/_standalone/tsconfig.tsbuildinfo +1 -1
  175. package/app/app/globals.css +2 -1
  176. package/app/app/layout.tsx +16 -4
  177. package/app/components/AskFab.tsx +4 -4
  178. package/app/components/AskModal.tsx +1 -1
  179. package/app/components/GuideCard.tsx +101 -152
  180. package/app/components/HomeContent.tsx +116 -575
  181. package/app/components/RightAskPanel.tsx +2 -2
  182. package/app/components/WikiHomeContent.tsx +151 -3
  183. package/app/components/ask/AskContent.tsx +90 -51
  184. package/app/components/ask/AskHeader.tsx +218 -18
  185. package/app/components/ask/MessageList.tsx +66 -47
  186. package/app/components/ask/SessionHistory.tsx +86 -60
  187. package/app/components/ask/SessionTabBar.tsx +29 -21
  188. package/app/components/ask/ThinkingBlock.tsx +6 -5
  189. package/app/components/ask/ToolCallBlock.tsx +10 -9
  190. package/app/components/settings/SettingsContent.tsx +1 -1
  191. package/app/data/skills/mindos/SKILL.md +67 -15
  192. package/app/data/skills/mindos-zh/SKILL.md +67 -11
  193. package/app/hooks/useAskSession.ts +23 -1
  194. package/app/lib/i18n/modules/ai-chat.ts +97 -10
  195. package/app/lib/i18n/modules/onboarding.ts +12 -12
  196. package/app/lib/stores/LocaleStoreInit.tsx +24 -1
  197. package/app/lib/stores/locale-store.ts +20 -6
  198. package/app/lib/types.ts +1 -0
  199. package/package.json +1 -1
  200. package/skills/mindos/SKILL.md +67 -15
  201. package/skills/mindos/references/knowledge-health.md +120 -0
  202. package/skills/mindos-max/SKILL.md +52 -5
  203. package/skills/mindos-max-zh/SKILL.md +55 -6
  204. package/skills/mindos-zh/SKILL.md +67 -11
  205. package/_standalone/.next/server/chunks/2364.js +0 -2
  206. package/_standalone/.next/server/chunks/357.js +0 -1
  207. package/_standalone/.next/static/chunks/178-105779afb62d36d9.js +0 -1
  208. package/_standalone/.next/static/chunks/2218-d54538000574ffef.js +0 -1
  209. package/_standalone/.next/static/chunks/2549-e63cf57fa927a41d.js +0 -1
  210. package/_standalone/.next/static/chunks/9274-296ab35f9f09e42e.js +0 -1
  211. package/_standalone/.next/static/chunks/app/agents/page-5d1446665ddb3801.js +0 -1
  212. package/_standalone/.next/static/chunks/app/echo/[segment]/page-b0103509ce34444b.js +0 -11
  213. package/_standalone/.next/static/chunks/app/layout-7e02ddf4144b01f1.js +0 -186
  214. package/_standalone/.next/static/chunks/app/page-6a6a12bd6d6812d0.js +0 -7
  215. package/_standalone/.next/static/chunks/app/view/[...path]/page-ca7bdcbf27f88a46.js +0 -12
  216. package/_standalone/.next/static/chunks/app/wiki/page-d492256a93f0b8bc.js +0 -1
  217. package/_standalone/.next/static/css/fd84c8316ead16eb.css +0 -1
  218. /package/_standalone/.next/static/{2ksXveDzEcnCMRIElDkLq → u8p6oIRTcr_ns-ElNZ9rl}/_buildManifest.js +0 -0
  219. /package/_standalone/.next/static/{2ksXveDzEcnCMRIElDkLq → u8p6oIRTcr_ns-ElNZ9rl}/_ssgManifest.js +0 -0
@@ -1,6 +1,9 @@
1
- import { memo } from 'react';
2
- import { Sparkles, SquarePen, History, X, Maximize2, Minimize2, PanelRight, AppWindow } from 'lucide-react';
1
+ import { memo, useState, useRef, useEffect, useCallback } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import { Sparkles, SquarePen, History, X, Maximize2, Minimize2, PanelRight, AppWindow, ChevronDown, Check, Trash2, Pencil, Pin, PinOff } from 'lucide-react';
3
4
  import { useLocale } from '@/lib/stores/locale-store';
5
+ import type { ChatSession } from '@/lib/types';
6
+ import { sessionTitle } from '@/hooks/useAskSession';
4
7
 
5
8
  interface AskHeaderProps {
6
9
  isPanel: boolean;
@@ -13,49 +16,246 @@ interface AskHeaderProps {
13
16
  askMode?: 'panel' | 'popup';
14
17
  onModeSwitch?: () => void;
15
18
  onClose?: () => void;
19
+ hideTitle?: boolean;
20
+ /** Session switching — inline in header when >=2 sessions */
21
+ sessions?: ChatSession[];
22
+ activeSessionId?: string | null;
23
+ onLoadSession?: (id: string) => void;
24
+ onDeleteSession?: (id: string) => void;
25
+ onRenameSession?: (id: string, name: string) => void;
26
+ onTogglePinSession?: (id: string) => void;
16
27
  }
17
28
 
18
29
  export default memo(function AskHeader({
19
30
  isPanel, showHistory, onToggleHistory, onReset, isLoading,
20
- maximized, onMaximize, askMode, onModeSwitch, onClose,
31
+ maximized, onMaximize, askMode, onModeSwitch, onClose, hideTitle,
32
+ sessions, activeSessionId, onLoadSession, onDeleteSession, onRenameSession, onTogglePinSession,
21
33
  }: AskHeaderProps) {
22
34
  const { t } = useLocale();
23
- const iconSize = isPanel ? 13 : 14;
35
+ const iconSize = 14;
36
+ const hasMultipleSessions = sessions && sessions.length >= 1;
37
+ const activeSession = sessions?.find(s => s.id === activeSessionId);
38
+ const activeTitle = activeSession ? sessionTitle(activeSession) : null;
39
+
40
+ // Session switcher dropdown state
41
+ const [switcherOpen, setSwitcherOpen] = useState(false);
42
+ const switcherRef = useRef<HTMLButtonElement>(null);
43
+ const dropdownRef = useRef<HTMLDivElement>(null);
44
+
45
+ // Inline rename state
46
+ const [renamingId, setRenamingId] = useState<string | null>(null);
47
+ const [renameValue, setRenameValue] = useState('');
48
+ const renameInputRef = useRef<HTMLInputElement>(null);
49
+
50
+ // Close on outside click
51
+ useEffect(() => {
52
+ if (!switcherOpen) return;
53
+ const handler = (e: MouseEvent) => {
54
+ const target = e.target as Node;
55
+ if (
56
+ switcherRef.current && !switcherRef.current.contains(target) &&
57
+ dropdownRef.current && !dropdownRef.current.contains(target)
58
+ ) {
59
+ setSwitcherOpen(false);
60
+ setRenamingId(null);
61
+ }
62
+ };
63
+ document.addEventListener('mousedown', handler);
64
+ return () => document.removeEventListener('mousedown', handler);
65
+ }, [switcherOpen]);
66
+
67
+ useEffect(() => {
68
+ if (!switcherOpen) return;
69
+ const handler = (e: KeyboardEvent) => {
70
+ if (e.key === 'Escape') {
71
+ if (renamingId) { setRenamingId(null); } else { setSwitcherOpen(false); }
72
+ }
73
+ };
74
+ document.addEventListener('keydown', handler);
75
+ return () => document.removeEventListener('keydown', handler);
76
+ }, [switcherOpen, renamingId]);
77
+
78
+ // Focus rename input when it appears
79
+ useEffect(() => {
80
+ if (renamingId) setTimeout(() => renameInputRef.current?.focus(), 0);
81
+ }, [renamingId]);
82
+
83
+ const handleSelectSession = useCallback((id: string) => {
84
+ onLoadSession?.(id);
85
+ setSwitcherOpen(false);
86
+ }, [onLoadSession]);
87
+
88
+ const handleStartRename = useCallback((id: string, currentTitle: string) => {
89
+ setRenamingId(id);
90
+ setRenameValue(currentTitle === '(empty session)' ? '' : currentTitle);
91
+ }, []);
92
+
93
+ const handleCommitRename = useCallback(() => {
94
+ if (renamingId && onRenameSession && renameValue.trim()) {
95
+ onRenameSession(renamingId, renameValue.trim());
96
+ }
97
+ setRenamingId(null);
98
+ }, [renamingId, renameValue, onRenameSession]);
99
+
100
+ // Position dropdown below trigger
101
+ const [dropPos, setDropPos] = useState<{ top: number; left: number; width: number } | null>(null);
102
+ useEffect(() => {
103
+ if (!switcherOpen || !switcherRef.current) return;
104
+ const rect = switcherRef.current.getBoundingClientRect();
105
+ setDropPos({ top: rect.bottom + 4, left: rect.left, width: Math.max(rect.width, 240) });
106
+ }, [switcherOpen]);
107
+
108
+ const switcherDropdown = switcherOpen && dropPos && sessions ? createPortal(
109
+ <div
110
+ ref={dropdownRef}
111
+ className="fixed z-50 rounded-xl border border-border/50 bg-card shadow-lg py-1 animate-in fade-in-0 slide-in-from-top-1 duration-100"
112
+ style={{ top: dropPos.top, left: dropPos.left, minWidth: dropPos.width, maxWidth: 320 }}
113
+ role="listbox"
114
+ >
115
+ {sessions.map((s) => {
116
+ const isActive = s.id === activeSessionId;
117
+ const title = sessionTitle(s);
118
+ const displayTitle = title === '(empty session)' ? (t.hints?.newChat ?? 'New chat') : title;
119
+
120
+ if (renamingId === s.id) {
121
+ return (
122
+ <div key={s.id} className="flex items-center gap-1 px-2 py-1.5">
123
+ <input
124
+ ref={renameInputRef}
125
+ type="text"
126
+ value={renameValue}
127
+ onChange={(e) => setRenameValue(e.target.value)}
128
+ onKeyDown={(e) => {
129
+ if (e.key === 'Enter') handleCommitRename();
130
+ if (e.key === 'Escape') setRenamingId(null);
131
+ }}
132
+ onBlur={handleCommitRename}
133
+ className="flex-1 min-w-0 px-2 py-1 text-xs rounded-md border border-border bg-background text-foreground outline-none focus:border-[var(--amber)]/50"
134
+ placeholder="Session name..."
135
+ />
136
+ </div>
137
+ );
138
+ }
139
+
140
+ return (
141
+ <div key={s.id} className="group/item flex items-center">
142
+ <button
143
+ type="button"
144
+ role="option"
145
+ aria-selected={isActive}
146
+ onClick={() => handleSelectSession(s.id)}
147
+ className={`flex-1 min-w-0 flex items-center gap-2 px-3 py-2 text-xs text-left transition-colors ${
148
+ isActive ? 'text-foreground font-medium' : 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
149
+ }`}
150
+ >
151
+ {s.pinned && <Pin size={10} className="shrink-0 text-[var(--amber)]/60 -rotate-45" />}
152
+ {isActive && !s.pinned && <Check size={11} className="shrink-0 text-[var(--amber)]" />}
153
+ <span className="truncate">{displayTitle}</span>
154
+ </button>
155
+ <div className="shrink-0 flex items-center gap-0.5 mr-1 opacity-0 group-hover/item:opacity-100 transition-opacity">
156
+ {onTogglePinSession && (
157
+ <button
158
+ type="button"
159
+ onClick={(e) => { e.stopPropagation(); onTogglePinSession(s.id); }}
160
+ className={`p-1.5 rounded-md transition-colors ${s.pinned ? 'text-[var(--amber)] hover:text-muted-foreground hover:bg-muted/60' : 'text-muted-foreground/40 hover:text-[var(--amber)] hover:bg-[var(--amber)]/5'}`}
161
+ aria-label={s.pinned ? 'Unpin' : 'Pin'}
162
+ >
163
+ {s.pinned ? <PinOff size={10} /> : <Pin size={10} />}
164
+ </button>
165
+ )}
166
+ {onRenameSession && (
167
+ <button
168
+ type="button"
169
+ onClick={(e) => { e.stopPropagation(); handleStartRename(s.id, title); }}
170
+ className="p-1.5 rounded-md text-muted-foreground/40 hover:text-foreground hover:bg-muted/60"
171
+ aria-label={`Rename: ${displayTitle}`}
172
+ >
173
+ <Pencil size={10} />
174
+ </button>
175
+ )}
176
+ {sessions.length > 1 && onDeleteSession && (
177
+ <button
178
+ type="button"
179
+ onClick={(e) => { e.stopPropagation(); onDeleteSession(s.id); }}
180
+ className="p-1.5 rounded-md text-muted-foreground/40 hover:text-error hover:bg-error/5"
181
+ aria-label={`Delete: ${displayTitle}`}
182
+ >
183
+ <Trash2 size={10} />
184
+ </button>
185
+ )}
186
+ </div>
187
+ </div>
188
+ );
189
+ })}
190
+ </div>,
191
+ document.body,
192
+ ) : null;
24
193
 
25
194
  return (
26
- <div className="flex items-center justify-between px-4 py-3 border-b border-border shrink-0">
195
+ <div className="flex items-center justify-between px-4 py-2.5 shrink-0">
27
196
  {!isPanel && (
28
197
  <div className="absolute top-2 left-1/2 -translate-x-1/2 w-8 h-1 rounded-full bg-muted-foreground/20 md:hidden" />
29
198
  )}
30
- <div className="flex items-center gap-2 text-sm font-medium text-foreground">
31
- <Sparkles size={isPanel ? 14 : 15} className="text-[var(--amber)]" />
32
- <span className={isPanel ? 'text-xs uppercase tracking-wider text-muted-foreground' : ''}>
33
- {isPanel ? 'MindOS Agent' : t.ask.title}
34
- </span>
35
- </div>
199
+ {!hideTitle && (
200
+ <div className="flex items-center gap-2 min-w-0">
201
+ <div className="w-6 h-6 rounded-lg bg-[var(--amber)]/10 flex items-center justify-center shrink-0">
202
+ <Sparkles size={13} className="text-[var(--amber)]" />
203
+ </div>
204
+ {hasMultipleSessions && activeTitle ? (
205
+ <button
206
+ ref={switcherRef}
207
+ type="button"
208
+ onClick={() => {
209
+ if (sessions && sessions.length >= 2) {
210
+ setSwitcherOpen(v => !v);
211
+ } else {
212
+ onToggleHistory();
213
+ }
214
+ }}
215
+ className="flex items-center gap-1 min-w-0 text-sm font-medium text-[var(--amber)] hover:text-[var(--amber)]/80 transition-colors"
216
+ aria-expanded={switcherOpen}
217
+ aria-haspopup="listbox"
218
+ >
219
+ <span className="truncate max-w-[180px]">{activeTitle === '(empty session)' ? (t.hints?.newChat ?? 'New chat') : activeTitle}</span>
220
+ <ChevronDown size={12} className={`shrink-0 text-muted-foreground transition-transform duration-150 ${switcherOpen ? 'rotate-180' : ''}`} />
221
+ </button>
222
+ ) : activeTitle ? (
223
+ <span className="text-sm font-medium text-muted-foreground/60 truncate max-w-[180px]">
224
+ {activeTitle === '(empty session)' ? (t.hints?.newChat ?? 'New chat') : activeTitle}
225
+ </span>
226
+ ) : (
227
+ /* Placeholder while sessions load — avoids flash of "MindOS Agent" text */
228
+ <span className="text-sm font-medium text-muted-foreground/40">
229
+ {t.hints?.newChat ?? 'New chat'}
230
+ </span>
231
+ )}
232
+ </div>
233
+ )}
234
+ {hideTitle && <div />}
36
235
  <div className="flex items-center gap-1 shrink-0">
37
- <button type="button" onClick={(e) => { e.stopPropagation(); onToggleHistory(); }} aria-pressed={showHistory} className={`p-2 rounded transition-colors ${showHistory ? 'bg-muted text-foreground' : 'text-muted-foreground hover:text-foreground hover:bg-muted'}`} title={t.hints.sessionHistory}>
236
+ <button type="button" onClick={(e) => { e.stopPropagation(); onToggleHistory(); }} aria-pressed={showHistory} className={`p-2 rounded-lg transition-colors ${showHistory ? 'bg-[var(--amber)]/10 text-[var(--amber)]' : 'text-muted-foreground hover:text-foreground hover:bg-muted'}`} title={t.hints.sessionHistory}>
38
237
  <History size={iconSize} />
39
238
  </button>
40
- <button type="button" onClick={(e) => { e.stopPropagation(); onReset(); }} disabled={isLoading} className="p-2 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors disabled:opacity-40" title={t.hints.newSession}>
239
+ <button type="button" onClick={(e) => { e.stopPropagation(); onReset(); }} disabled={isLoading} className="p-2 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground transition-colors disabled:opacity-40" title={t.hints.newSession}>
41
240
  <SquarePen size={iconSize} />
42
241
  </button>
43
- {isPanel && onMaximize && (
44
- <button type="button" onClick={(e) => { e.stopPropagation(); onMaximize(); }} className="p-2 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={maximized ? t.hints.restorePanel : t.hints.maximizePanel}>
242
+ {onMaximize && (
243
+ <button type="button" onClick={(e) => { e.stopPropagation(); onMaximize(); }} className="p-2 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={maximized ? t.hints.restorePanel : t.hints.maximizePanel}>
45
244
  {maximized ? <Minimize2 size={iconSize} /> : <Maximize2 size={iconSize} />}
46
245
  </button>
47
246
  )}
48
247
  {onModeSwitch && (
49
- <button type="button" onClick={(e) => { e.stopPropagation(); onModeSwitch(); }} className="p-2 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={askMode === 'popup' ? t.hints.dockToSide : t.hints.openAsPopup}>
248
+ <button type="button" onClick={(e) => { e.stopPropagation(); onModeSwitch(); }} className="p-2 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={askMode === 'popup' ? t.hints.dockToSide : t.hints.openAsPopup}>
50
249
  {askMode === 'popup' ? <PanelRight size={iconSize} /> : <AppWindow size={iconSize} />}
51
250
  </button>
52
251
  )}
53
252
  {onClose && (
54
- <button type="button" onClick={(e) => { e.stopPropagation(); onClose(); }} className="p-2 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={t.hints.closePanel} aria-label="Close">
55
- <X size={isPanel ? iconSize : 15} />
253
+ <button type="button" onClick={(e) => { e.stopPropagation(); onClose(); }} className="p-2 rounded-lg hover:bg-muted text-muted-foreground hover:text-foreground transition-colors" title={t.hints.closePanel} aria-label="Close">
254
+ <X size={iconSize} />
56
255
  </button>
57
256
  )}
58
257
  </div>
258
+ {typeof document !== 'undefined' && switcherDropdown}
59
259
  </div>
60
260
  );
61
261
  });
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
 
3
3
  import { useRef, useEffect, memo, useState, useCallback } from 'react';
4
- import { Sparkles, Loader2, AlertCircle, Wrench, WifiOff, Zap, Copy, Check, ArrowDown } from 'lucide-react';
4
+ import { Sparkles, Loader2, AlertCircle, Wrench, WifiOff, Zap, Copy, Check, ArrowDown, FolderInput, Search, PenLine, Lightbulb } from 'lucide-react';
5
5
  import ReactMarkdown from 'react-markdown';
6
6
  import remarkGfm from 'remark-gfm';
7
7
  import type { Message, ImagePart } from '@/lib/types';
@@ -12,7 +12,7 @@ import ThinkingBlock from './ThinkingBlock';
12
12
 
13
13
  const SKILL_PREFIX_RE = /^Use the skill ([^:]+):\s*/;
14
14
 
15
- function CopyMessageButton({ text }: { text: string }) {
15
+ function CopyMessageButton({ text, label }: { text: string; label?: string }) {
16
16
  const [copied, setCopied] = useState(false);
17
17
  const handleCopy = useCallback(() => {
18
18
  copyToClipboard(text).then(ok => {
@@ -27,8 +27,8 @@ function CopyMessageButton({ text }: { text: string }) {
27
27
  <button
28
28
  type="button"
29
29
  onClick={handleCopy}
30
- className="absolute -bottom-1 right-1 p-1 rounded-md bg-card border border-border/60 shadow-sm text-muted-foreground hover:text-foreground opacity-0 group-hover:opacity-100 transition-opacity"
31
- title="Copy"
30
+ className="absolute -bottom-1 right-1 p-1 rounded-md bg-card border border-border/60 shadow-sm text-muted-foreground hover:text-foreground opacity-100 md:opacity-0 md:group-hover:opacity-100 transition-opacity"
31
+ title={label ?? 'Copy'}
32
32
  >
33
33
  {copied ? <Check size={11} className="text-success" /> : <Copy size={11} />}
34
34
  </button>
@@ -62,7 +62,7 @@ function UserMessageContent({ content, skillName, images }: { content: string; s
62
62
  )}
63
63
  {/* Skill capsule + text */}
64
64
  {resolved && (
65
- <span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-[11px] font-medium bg-[var(--amber)]/15 text-[var(--amber)] mr-1 align-middle">
65
+ <span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-[11px] font-medium bg-white/20 text-white/90 mr-1 align-middle">
66
66
  <Zap size={10} className="shrink-0" />
67
67
  {resolved}
68
68
  </span>
@@ -82,16 +82,16 @@ function AssistantMessage({ content, isStreaming }: { content: string; isStreami
82
82
  prose-h1:text-base prose-h2:text-[15px] prose-h3:text-sm
83
83
  prose-ul:my-1.5 prose-li:my-0.5
84
84
  prose-ol:my-1.5
85
- prose-code:text-[0.8em] prose-code:bg-background/60 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none
86
- prose-pre:bg-background/60 prose-pre:text-foreground prose-pre:text-xs
87
- prose-blockquote:border-l-[var(--amber)] prose-blockquote:text-muted-foreground
85
+ prose-code:text-[0.8em] prose-code:bg-muted/80 prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded-md prose-code:before:content-none prose-code:after:content-none prose-code:font-mono
86
+ prose-pre:bg-muted/60 prose-pre:text-foreground prose-pre:text-xs prose-pre:rounded-lg
87
+ prose-blockquote:border-l-[var(--amber)] prose-blockquote:text-muted-foreground prose-blockquote:not-italic
88
88
  prose-a:text-[var(--amber)] prose-a:no-underline hover:prose-a:underline
89
89
  prose-strong:text-foreground prose-strong:font-semibold
90
- prose-table:text-xs prose-th:py-1 prose-td:py-1
90
+ prose-table:text-xs prose-th:py-1.5 prose-td:py-1
91
91
  ">
92
92
  <ReactMarkdown remarkPlugins={[remarkGfm]}>{cleaned}</ReactMarkdown>
93
93
  {isStreaming && (
94
- <span className="inline-block w-1.5 h-3.5 bg-[var(--amber)] ml-0.5 align-middle animate-pulse rounded-sm" />
94
+ <span className="inline-block w-1.5 h-3.5 bg-[var(--amber)] ml-0.5 align-middle animate-pulse rounded-full" />
95
95
  )}
96
96
  </div>
97
97
  );
@@ -129,7 +129,7 @@ function AssistantMessageWithParts({ message, isStreaming }: { message: Message;
129
129
  return null;
130
130
  })}
131
131
  {showTrailingSpinner && (
132
- <div className="flex items-center gap-2 py-1 mt-1">
132
+ <div className="flex items-center gap-2 py-1.5 mt-1.5">
133
133
  <Loader2 size={12} className="animate-spin text-[var(--amber)]" />
134
134
  <span className="text-xs text-muted-foreground animate-pulse">Executing tool…</span>
135
135
  </div>
@@ -145,9 +145,9 @@ function StepCounter({ parts }: { parts: Message['parts'] }) {
145
145
  const lastToolCall = toolCalls[toolCalls.length - 1];
146
146
  const toolLabel = lastToolCall.type === 'tool-call' ? lastToolCall.toolName : '';
147
147
  return (
148
- <div className="flex items-center gap-1.5 mt-1.5 text-xs text-muted-foreground/70">
148
+ <div className="flex items-center gap-1.5 mt-2 pt-1.5 border-t border-border/15 text-xs text-muted-foreground/60">
149
149
  <Wrench size={10} />
150
- <span>Step {toolCalls.length}{toolLabel ? ` — ${toolLabel}` : ''}</span>
150
+ <span className="font-medium">Step {toolCalls.length}{toolLabel ? ` — ${toolLabel}` : ''}</span>
151
151
  </div>
152
152
  );
153
153
  }
@@ -158,13 +158,14 @@ interface MessageListProps {
158
158
  loadingPhase: 'connecting' | 'thinking' | 'streaming' | 'reconnecting';
159
159
  emptyPrompt: string;
160
160
  emptyHint?: string;
161
- suggestions: readonly string[];
161
+ suggestions: readonly { label: string; prompt: string }[];
162
162
  onSuggestionClick: (text: string) => void;
163
163
  labels: {
164
164
  connecting: string;
165
165
  thinking: string;
166
166
  generating: string;
167
167
  reconnecting?: string;
168
+ copyMessage?: string;
168
169
  };
169
170
  }
170
171
 
@@ -183,7 +184,9 @@ export default memo(function MessageList({
183
184
  const [showScrollDown, setShowScrollDown] = useState(false);
184
185
 
185
186
  const scrollToBottom = useCallback(() => {
186
- endRef.current?.scrollIntoView({ behavior: 'smooth' });
187
+ const container = scrollContainerRef.current;
188
+ if (!container) return;
189
+ container.scrollTo({ top: container.scrollHeight, behavior: 'smooth' });
187
190
  }, []);
188
191
 
189
192
  useEffect(() => {
@@ -202,56 +205,65 @@ export default memo(function MessageList({
202
205
  }, []);
203
206
 
204
207
  return (
205
- <div ref={scrollContainerRef} className="relative flex-1 overflow-y-auto overflow-x-hidden px-4 py-4 space-y-4 min-h-0">
208
+ <div ref={scrollContainerRef} role="log" aria-live="polite" className="relative flex-1 overflow-y-auto overflow-x-hidden px-4 py-5 space-y-5 min-h-0">
206
209
  {messages.length === 0 && (
207
- <div className="flex flex-col items-center justify-center flex-1 min-h-[200px] px-4">
208
- {/* Brand anchor */}
209
- <div className="w-10 h-10 rounded-xl bg-[var(--amber)]/10 flex items-center justify-center mb-4">
210
- <Sparkles size={20} className="text-[var(--amber)]" />
210
+ <div className="flex flex-col items-center justify-center flex-1 min-h-[260px] px-6 pt-10 pb-4">
211
+ {/* Brand anchor — refined presence */}
212
+ <div className="relative w-12 h-12 rounded-2xl bg-[var(--amber)]/10 flex items-center justify-center mb-6">
213
+ <div className="absolute inset-0 rounded-2xl bg-[var(--amber)]/5 scale-[1.4]" />
214
+ <Sparkles size={22} className="text-[var(--amber)] relative z-10" />
211
215
  </div>
212
- <p className="text-center text-sm font-medium text-foreground/80 mb-1">{emptyPrompt}</p>
216
+ <p className="text-center text-[15px] font-semibold text-foreground tracking-tight mb-2">{emptyPrompt}</p>
213
217
  {emptyHint && (
214
- <p className="text-center text-[11px] text-muted-foreground/50 mb-5">{emptyHint}</p>
218
+ <p className="text-center text-xs text-muted-foreground/80 mb-10 tracking-wide">{emptyHint}</p>
215
219
  )}
216
- {/* Suggestion chips — centered 2-column grid */}
217
- <div className="grid grid-cols-2 gap-2 max-w-sm w-full">
218
- {suggestions.map((s, i) => (
219
- <button
220
- key={i}
221
- type="button"
222
- onClick={() => onSuggestionClick(s)}
223
- className="text-left text-xs px-3 py-2.5 rounded-lg border border-border/60 bg-card text-muted-foreground hover:text-foreground hover:border-[var(--amber)]/30 hover:bg-[var(--amber)]/5 transition-colors leading-snug"
224
- >
225
- {s}
226
- </button>
227
- ))}
220
+ {/* Suggestion chips — refined single column */}
221
+ <div className="flex flex-col gap-2.5 max-w-[280px] w-full">
222
+ {suggestions.map((s, i) => {
223
+ const icons = [FolderInput, Search, PenLine, Lightbulb];
224
+ const SugIcon = icons[i % icons.length];
225
+ return (
226
+ <button
227
+ key={i}
228
+ type="button"
229
+ onClick={() => onSuggestionClick(s.prompt)}
230
+ className="group/sug flex items-center gap-3 text-left text-[13px] px-3.5 py-3 rounded-xl border border-border/40 bg-transparent text-muted-foreground hover:text-foreground hover:border-[var(--amber)]/30 hover:bg-[var(--amber)]/5 transition-all leading-snug"
231
+ aria-label={s.prompt}
232
+ >
233
+ <span className="shrink-0 w-8 h-8 rounded-lg bg-muted/60 flex items-center justify-center group-hover/sug:bg-[var(--amber)]/10 transition-colors">
234
+ <SugIcon size={15} className="text-muted-foreground/70 group-hover/sug:text-[var(--amber)] transition-colors" />
235
+ </span>
236
+ <span className="flex-1">{s.label}</span>
237
+ </button>
238
+ );
239
+ })}
228
240
  </div>
229
241
  </div>
230
242
  )}
231
243
  {messages.map((m, i) => (
232
- <div key={i} className={`flex gap-3 ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}>
244
+ <div key={i} className={`flex gap-3 animate-[fadeSlideUp_0.22s_ease_both] ${m.role === 'user' ? 'justify-end' : 'justify-start'}`}>
233
245
  {m.role === 'assistant' && (
234
246
  <div
235
- className="w-6 h-6 rounded-full flex items-center justify-center shrink-0 mt-0.5 bg-[var(--amber-dim)]"
247
+ className="w-7 h-7 rounded-lg flex items-center justify-center shrink-0 mt-0.5 bg-[var(--amber)]/8"
236
248
  >
237
- <Sparkles size={12} className="text-[var(--amber)]" />
249
+ <Sparkles size={13} className="text-[var(--amber)]" />
238
250
  </div>
239
251
  )}
240
252
  {m.role === 'user' ? (
241
253
  <div
242
- className="max-w-[85%] px-3 py-2 rounded-xl rounded-br-sm text-sm leading-relaxed whitespace-pre-wrap bg-[var(--amber)] text-[var(--amber-foreground)]"
254
+ className="max-w-[85%] px-3.5 py-2.5 rounded-2xl rounded-br-lg text-sm leading-relaxed whitespace-pre-wrap bg-[var(--amber)] text-[var(--amber-foreground)] shadow-sm shadow-[var(--amber)]/10"
243
255
  >
244
256
  <UserMessageContent content={m.content} skillName={m.skillName} images={m.images} />
245
257
  </div>
246
258
  ) : m.content.startsWith('__error__') ? (
247
- <div className="max-w-[85%] px-3 py-2.5 rounded-xl rounded-bl-sm border border-error/20 bg-error/8 text-sm">
248
- <div className="flex items-start gap-2 text-error">
249
- <AlertCircle size={14} className="shrink-0 mt-0.5" />
250
- <span className="leading-relaxed">{m.content.slice(9)}</span>
259
+ <div className="max-w-[85%] px-3.5 py-3 rounded-2xl rounded-bl-md border border-error/30 bg-error/10 text-sm shadow-sm">
260
+ <div className="flex items-start gap-2.5 text-error">
261
+ <AlertCircle size={15} className="shrink-0 mt-0.5" />
262
+ <span className="leading-relaxed font-medium">{m.content.slice(9)}</span>
251
263
  </div>
252
264
  </div>
253
265
  ) : (
254
- <div className="group relative max-w-[85%] px-3 py-2 rounded-xl rounded-bl-sm bg-muted text-foreground text-sm">
266
+ <div className="group relative max-w-[85%] px-3.5 py-2.5 rounded-2xl rounded-bl-lg bg-card border border-border/30 shadow-sm text-foreground text-sm">
255
267
  {(m.parts && m.parts.length > 0) || stripThinkingTags(m.content) ? (
256
268
  <>
257
269
  <AssistantMessageWithParts message={m} isStreaming={isLoading && i === messages.length - 1} />
@@ -259,17 +271,18 @@ export default memo(function MessageList({
259
271
  <StepCounter parts={m.parts} />
260
272
  )}
261
273
  {!(isLoading && i === messages.length - 1) && stripThinkingTags(m.content) && (
262
- <CopyMessageButton text={stripThinkingTags(m.content)} />
274
+ <CopyMessageButton text={stripThinkingTags(m.content)} label={labels.copyMessage} />
263
275
  )}
264
276
  </>
265
277
  ) : isLoading && i === messages.length - 1 ? (
266
- <div className="flex items-center gap-2 py-1">
278
+ <div className="flex items-center gap-2.5 py-1">
267
279
  {loadingPhase === 'reconnecting' ? (
268
280
  <WifiOff size={14} className="text-[var(--amber)] animate-pulse" />
269
281
  ) : (
270
282
  <Loader2 size={14} className="animate-spin text-[var(--amber)]" />
271
283
  )}
272
- <span className="text-xs text-muted-foreground animate-pulse">
284
+ <span className="text-xs text-muted-foreground">
285
+ <span className="inline-flex items-center gap-1">
273
286
  {loadingPhase === 'reconnecting'
274
287
  ? (labels.reconnecting ?? 'Reconnecting...')
275
288
  : loadingPhase === 'connecting'
@@ -277,6 +290,12 @@ export default memo(function MessageList({
277
290
  : loadingPhase === 'thinking'
278
291
  ? labels.thinking
279
292
  : labels.generating}
293
+ <span className="inline-flex gap-0.5">
294
+ <span className="w-1 h-1 rounded-full bg-[var(--amber)] animate-bounce [animation-delay:0ms]"></span>
295
+ <span className="w-1 h-1 rounded-full bg-[var(--amber)] animate-bounce [animation-delay:150ms]"></span>
296
+ <span className="w-1 h-1 rounded-full bg-[var(--amber)] animate-bounce [animation-delay:300ms]"></span>
297
+ </span>
298
+ </span>
280
299
  </span>
281
300
  </div>
282
301
  ) : null}
@@ -291,7 +310,7 @@ export default memo(function MessageList({
291
310
  <button
292
311
  type="button"
293
312
  onClick={scrollToBottom}
294
- className="sticky bottom-2 left-1/2 -translate-x-1/2 z-10 p-1.5 rounded-full border border-border/60 bg-card shadow-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
313
+ className="sticky bottom-2 left-1/2 -translate-x-1/2 z-10 p-2 rounded-full border border-border/60 bg-card shadow-md text-muted-foreground hover:text-foreground hover:bg-muted transition-all hover:shadow-lg"
295
314
  title="Scroll to bottom"
296
315
  >
297
316
  <ArrowDown size={14} />