@pheem49/mint 1.8.0 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/.github/workflows/release.yml +1 -1
  2. package/Cargo.lock +34 -3
  3. package/Cargo.toml +2 -2
  4. package/README.md +20 -6
  5. package/Release_Note.md +129 -0
  6. package/assets/Mint_CLI.png +0 -0
  7. package/bin/mint +0 -0
  8. package/crates/mint-cli/Cargo.toml +1 -0
  9. package/crates/mint-cli/src/agent.rs +242 -54
  10. package/crates/mint-cli/src/main.rs +397 -74
  11. package/crates/mint-core/src/agent_loop.rs +1 -0
  12. package/crates/mint-core/src/api_server.rs +64 -2
  13. package/crates/mint-core/src/browser.rs +456 -0
  14. package/crates/mint-core/src/channels.rs +1 -0
  15. package/crates/mint-core/src/chat.rs +3 -0
  16. package/crates/mint-core/src/config.rs +130 -1
  17. package/crates/mint-core/src/lib.rs +19 -0
  18. package/crates/mint-core/src/orchestration.rs +317 -12
  19. package/crates/mint-core/src/skills.rs +113 -4
  20. package/crates/mint-core/src/tts.rs +3 -1
  21. package/crates/mint-core/src/workflows.rs +1 -22
  22. package/flatpak/com.pheem49.mint.desktop +9 -0
  23. package/flatpak/com.pheem49.mint.metainfo.xml +35 -0
  24. package/flatpak/com.pheem49.mint.yaml +52 -0
  25. package/package.json +1 -1
  26. package/shared-modules/.github/workflows/ci.yml +120 -0
  27. package/shared-modules/.github/workflows/update.yaml +28 -0
  28. package/shared-modules/CODEOWNERS +23 -0
  29. package/shared-modules/README.md +79 -0
  30. package/shared-modules/SDL/README.md +3 -0
  31. package/shared-modules/SDL/SDL-1.2.15.json +51 -0
  32. package/shared-modules/SDL/SDL_Pango-0.1.2-API-adds.patch +118 -0
  33. package/shared-modules/SDL/SDL_image-1.2.12.json +25 -0
  34. package/shared-modules/SDL/SDL_mixer-1.2.12.json +27 -0
  35. package/shared-modules/SDL/SDL_net-1.2.8.json +19 -0
  36. package/shared-modules/SDL/SDL_pango-0.1.2.json +26 -0
  37. package/shared-modules/SDL/SDL_ttf-2.0.11.json +28 -0
  38. package/shared-modules/SDL/libsdl-1.2.15-strict-prototypes.patch +316 -0
  39. package/shared-modules/SDL/sdl-check-for-SDL_VIDEO_X11_BACKINGSTORE.patch +24 -0
  40. package/shared-modules/SDL/sdl-libx11-build.patch +59 -0
  41. package/shared-modules/SDL/sdl-sysloadso-buffer-length.patch +27 -0
  42. package/shared-modules/SDL/sdl12-compat.json +22 -0
  43. package/shared-modules/SDL2/SDL2_image.json +27 -0
  44. package/shared-modules/SDL2/SDL2_ttf.json +29 -0
  45. package/shared-modules/cld2/CMakeLists.txt +157 -0
  46. package/shared-modules/cld2/cld2.json +24 -0
  47. package/shared-modules/clutter/clutter.json +72 -0
  48. package/shared-modules/dbus-glib/dbus-glib.json +29 -0
  49. package/shared-modules/glew/glew.json +36 -0
  50. package/shared-modules/glu/glu-9.json +12 -0
  51. package/shared-modules/gtk2/arc-gtk2-theme-Replace-Inkscape-dependency-with-rsvg-convert.patch +180 -0
  52. package/shared-modules/gtk2/gtk2-common-themes.json +360 -0
  53. package/shared-modules/gtk2/gtk2-fix-crash-in-show-uri.patch +30 -0
  54. package/shared-modules/gtk2/gtk2-gcc14.patch +281 -0
  55. package/shared-modules/gtk2/gtk2-gcc15.patch +250 -0
  56. package/shared-modules/gtk2/gtk2-murrine-c99.patch +34 -0
  57. package/shared-modules/gtk2/gtk2-use-adwaita-theme.patch +80 -0
  58. package/shared-modules/gtk2/gtk2.json +120 -0
  59. package/shared-modules/gtk2/ibus-gcc15.patch +13 -0
  60. package/shared-modules/gtk2/murrine-engine-fix-crash.patch +12 -0
  61. package/shared-modules/gtk2/readme.md +10 -0
  62. package/shared-modules/intltool/intltool-0.51.json +20 -0
  63. package/shared-modules/intltool/intltool-perl5.26-regex-fixes.patch +59 -0
  64. package/shared-modules/lame/0001-Build-xmm_quantize_sub.c-with-msse.patch +34 -0
  65. package/shared-modules/lame/0002-Include-GTK-1-autoconf-directives-in-build-system.patch +222 -0
  66. package/shared-modules/lame/0003-Patch-out-remaining-ansi2knr.patch +70 -0
  67. package/shared-modules/lame/0004-initscr-is-not-used-anywhere-in-lame-sourcetree.patch +41 -0
  68. package/shared-modules/lame/0005-Update-autoconf.patch +81 -0
  69. package/shared-modules/lame/lame-3.99.5.json +32 -0
  70. package/shared-modules/libappindicator/libappindicator-gtk2-12.10.json +140 -0
  71. package/shared-modules/libappindicator/libappindicator-gtk3-12.10.json +140 -0
  72. package/shared-modules/libappindicator/libappindicator-gtk3-introspection-12.10.json +140 -0
  73. package/shared-modules/libappindicator/patches/0001-libappindicator-ftbfs.patch +20 -0
  74. package/shared-modules/libappindicator/patches/0002-libappindicator-no-python.patch +888 -0
  75. package/shared-modules/libappindicator/patches/0003-libappindicator-fix-crash-from-incorrect-signal-emission.patch +74 -0
  76. package/shared-modules/libappindicator/patches/0004-libappindicator-fix-crash-iterating-icon-themes.patch +11 -0
  77. package/shared-modules/libayatana-appindicator/0001-Fix-HAVE_VALGRIND-AM_CONDITIONAL.patch +51 -0
  78. package/shared-modules/libayatana-appindicator/0001-Make-introspection-configurable.patch +44 -0
  79. package/shared-modules/libayatana-appindicator/libayatana-appindicator-gtk3.json +121 -0
  80. package/shared-modules/libcanberra/0001-gtk-Don-t-assume-all-GdkDisplays-are-GdkX11Displays-.patch +123 -0
  81. package/shared-modules/libcanberra/libcanberra.json +44 -0
  82. package/shared-modules/libmad/libmad.json +19 -0
  83. package/shared-modules/libsecret/libsecret.json +23 -0
  84. package/shared-modules/libsoup/libsoup-2.4.json +14 -0
  85. package/shared-modules/libusb/libusb.json +27 -0
  86. package/shared-modules/linux-audio/dssi.json +31 -0
  87. package/shared-modules/linux-audio/fftw3f-static.json +44 -0
  88. package/shared-modules/linux-audio/fftw3f.json +42 -0
  89. package/shared-modules/linux-audio/fluidsynth2-static.json +30 -0
  90. package/shared-modules/linux-audio/fluidsynth2.json +19 -0
  91. package/shared-modules/linux-audio/ladspa.json +26 -0
  92. package/shared-modules/linux-audio/lash.json +35 -0
  93. package/shared-modules/linux-audio/libinstpatch.json +16 -0
  94. package/shared-modules/linux-audio/liblo-static.json +22 -0
  95. package/shared-modules/linux-audio/liblo.json +17 -0
  96. package/shared-modules/linux-audio/lilv-static.json +94 -0
  97. package/shared-modules/linux-audio/lilv.json +82 -0
  98. package/shared-modules/linux-audio/lrdf.json +57 -0
  99. package/shared-modules/linux-audio/lv2.json +21 -0
  100. package/shared-modules/linux-audio/patches/lash/0001-configure-remove-aclocal-m4.patch +29 -0
  101. package/shared-modules/linux-audio/patches/lash/0002-Fix-build-with-recent-gcc.patch +73 -0
  102. package/shared-modules/linux-audio/patches/lash/0003-Fix-linking.patch +136 -0
  103. package/shared-modules/linux-audio/patches/raptor-libxml.patch +31 -0
  104. package/shared-modules/linux-audio/patches/stk-install.sh +54 -0
  105. package/shared-modules/linux-audio/stk.json +28 -0
  106. package/shared-modules/lua5.1/0004-Fix-stack-overflow-in-vararg-functions.patch +21 -0
  107. package/shared-modules/lua5.1/lua-5.1.5-so.patch +43 -0
  108. package/shared-modules/lua5.1/lua-5.1.5.json +42 -0
  109. package/shared-modules/lua5.3/lua-5.3.5.json +41 -0
  110. package/shared-modules/lua5.3/lua.pc.in +8 -0
  111. package/shared-modules/lua5.3/luaconf.patch +47 -0
  112. package/shared-modules/lua5.3/makefile-add-liblua.so-rule.patch +47 -0
  113. package/shared-modules/lua5.4/lua-5.4.json +41 -0
  114. package/shared-modules/lua5.4/lua.pc.in +8 -0
  115. package/shared-modules/lua5.4/luaconf.patch +34 -0
  116. package/shared-modules/lua5.4/makefile-add-liblua.so-rule.patch +42 -0
  117. package/shared-modules/luajit/luajit.json +30 -0
  118. package/shared-modules/lzo/lzo.json +27 -0
  119. package/shared-modules/mac/mac.json +18 -0
  120. package/shared-modules/physfs/physfs.json +26 -0
  121. package/shared-modules/pygame/audiofile-gcc6.patch +21 -0
  122. package/shared-modules/pygame/cython-fixes.patch +34 -0
  123. package/shared-modules/pygame/fluidsynth-no-rawmidi.patch +69 -0
  124. package/shared-modules/pygame/glib.patch +25 -0
  125. package/shared-modules/pygame/pygame1-python2.json +157 -0
  126. package/shared-modules/pygtk/pygtk-c99.patch +15 -0
  127. package/shared-modules/pygtk/pygtk.json +117 -0
  128. package/shared-modules/pygtk/pygtk.patch +39 -0
  129. package/shared-modules/python2.7/python-2.7.json +48 -0
  130. package/shared-modules/smpeg/smpeg-0.4.5.json +50 -0
  131. package/shared-modules/smpeg/smpeg-am-prog-as.patch +12 -0
  132. package/shared-modules/smpeg/smpeg-export-mpegaudio-class.patch +17 -0
  133. package/shared-modules/smpeg/smpeg-gcc6.patch +40 -0
  134. package/shared-modules/smpeg/smpeg-no-gtk.patch +37 -0
  135. package/shared-modules/squashfs-tools/squashfs-tools.json +25 -0
  136. package/shared-modules/update-checker.sh +50 -0
  137. package/shared-modules/vorbisgain/0001-temp_files.patch +75 -0
  138. package/shared-modules/vorbisgain/0002-errno.patch +33 -0
  139. package/shared-modules/vorbisgain/0003-manpage.patch +248 -0
  140. package/shared-modules/vorbisgain/0004-vorbisgain_mtime.patch +183 -0
  141. package/shared-modules/vorbisgain/0005-double_fclose.patch +21 -0
  142. package/shared-modules/vorbisgain/0006-manpage_hyphens.patch +56 -0
  143. package/shared-modules/vorbisgain/0007-recursively_spelling.patch +14 -0
  144. package/shared-modules/vorbisgain/0008-manpage_recursion_mistake.patch +23 -0
  145. package/shared-modules/vorbisgain/0009-hardening.patch +13 -0
  146. package/shared-modules/vorbisgain/0010-fclose.patch +13 -0
  147. package/shared-modules/vorbisgain/README.md +23 -0
  148. package/shared-modules/vorbisgain/vorbisgain_0.37-2.json +29 -0
  149. package/src/renderer/shared/agentProgress.ts +145 -0
  150. package/src/renderer/shared/calculator.ts +52 -0
  151. package/src/renderer/shared/components/AgentActivityDrawer.tsx +72 -0
  152. package/src/renderer/shared/components/AgentActivityTable.tsx +66 -0
  153. package/src/renderer/shared/components/ApprovalCard.tsx +93 -0
  154. package/src/renderer/shared/components/ChatCodeBlock.tsx +75 -0
  155. package/src/renderer/shared/components/ThinkingBlock.tsx +101 -0
  156. package/src/renderer/shared/constants/models.ts +91 -0
  157. package/src/renderer/shared/platform.ts +82 -0
  158. package/src/renderer/shared/types.ts +140 -0
  159. package/src/renderer/shared/utils/agentActivity.ts +187 -0
  160. package/src/renderer/shared/utils/approval.ts +1 -0
  161. package/src/renderer/shared/utils/approval.tsx +64 -0
  162. package/src/renderer/shared/utils/markdown.tsx +347 -0
  163. package/src/renderer/shared/utils/providers.ts +36 -0
  164. package/src/renderer/shared/utils/speech.ts +216 -0
  165. package/src/renderer/shared/utils/ui.ts +147 -0
  166. package/src/renderer/src/agentProgress.ts +11 -39
  167. package/src/renderer/src/calculator.ts +3 -46
  168. package/src/renderer/src/components/ChatPanel.tsx +230 -1202
  169. package/src/renderer/src/components/ImageStudioPanel.tsx +11 -1
  170. package/src/renderer/src/components/MintDashboard.tsx +63 -171
  171. package/src/renderer/src/components/PicturesLibrary.tsx +32 -8
  172. package/src/renderer/src/components/Settings/AgentsTab.tsx +362 -0
  173. package/src/renderer/src/components/Settings/AutomationTab.tsx +0 -22
  174. package/src/renderer/src/components/Settings/PluginsTab.tsx +221 -41
  175. package/src/renderer/src/components/SettingsWindow.tsx +54 -12
  176. package/src/renderer/src/components/WorkspacePanel.tsx +66 -9
  177. package/src/renderer/src/css/image-studio.css +6 -0
  178. package/src/renderer/src/css/styles.css +148 -11
  179. package/src/renderer/src/index.css +12 -1
  180. package/src/renderer/src/tauri.ts +169 -143
  181. package/src/renderer/src-web/agentProgress.ts +11 -39
  182. package/src/renderer/src-web/calculator.ts +3 -46
  183. package/src/renderer/src-web/components/ChatPanel.tsx +223 -1199
  184. package/src/renderer/src-web/components/ImageStudioPanel.tsx +11 -1
  185. package/src/renderer/src-web/components/MintDashboard.tsx +31 -172
  186. package/src/renderer/src-web/components/PicturesLibrary.tsx +23 -29
  187. package/src/renderer/src-web/components/Settings/AgentsTab.tsx +362 -0
  188. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +0 -22
  189. package/src/renderer/src-web/components/Settings/PluginsTab.tsx +221 -41
  190. package/src/renderer/src-web/components/SettingsWindow.tsx +54 -12
  191. package/src/renderer/src-web/css/image-studio.css +6 -0
  192. package/src/renderer/src-web/css/styles.css +184 -12
  193. package/src/renderer/src-web/index.css +12 -1
  194. package/src/renderer/src-web/tauri.ts +204 -104
  195. package/src-tauri/src/browser.rs +1 -141
  196. package/src-tauri/src/headless.rs +3 -2
  197. package/src-tauri/src/lib.rs +254 -59
  198. package/src-tauri/src/webhooks.rs +1 -0
  199. package/src-tauri/src/workflows.rs +57 -4
  200. package/src-tauri/tauri.conf.json +1 -1
@@ -1,5 +1,30 @@
1
1
  import { useEffect, useRef, useState, Fragment, type ChangeEvent, type ClipboardEvent, type FormEvent, type KeyboardEvent, type RefObject, type DragEvent } from 'react'
2
- import { hasAgentToolActivity, thoughtsFrom } from '../agentProgress'
2
+ import { hasAgentToolActivity, thoughtsFrom, parseFileChangesFromProgress } from '../agentProgress'
3
+ import {
4
+ GEMINI_MODELS,
5
+ OPENAI_MODELS,
6
+ ANTHROPIC_MODELS,
7
+ OPENROUTER_MODELS,
8
+ DEEPSEEK_MODELS,
9
+ HF_MODELS,
10
+ LOCAL_MODELS,
11
+ OLLAMA_MODELS,
12
+ } from '../../shared/constants/models'
13
+ import { badge, providerLabel, fallbackNotice } from '../../shared/utils/providers'
14
+ import { activitiesFrom, parseWebSearchSources, type AgentActivity, type AgentActivityView } from '../../shared/utils/agentActivity'
15
+ import { AgentActivityTable } from '../../shared/components/AgentActivityTable'
16
+ import { ChatCodeBlock } from '../../shared/components/ChatCodeBlock'
17
+ import { renderApprovalDetails, renderDiff, type ApprovalDetails } from '../../shared/utils/approval'
18
+ import { ApprovalCard } from '../../shared/components/ApprovalCard'
19
+ import { renderFormattedMessage, readableAssistantText, cleanSpeechText, renderSpeakerIcon } from '../../shared/utils/markdown'
20
+ import { ThinkingBlock } from '../../shared/components/ThinkingBlock'
21
+ import { AgentActivityDrawer } from '../../shared/components/AgentActivityDrawer'
22
+ import type { DiffHunk, FileChange } from '../../shared/types'
23
+ import { numericSetting } from '../../shared/utils/ui'
24
+ import { useSpeechToText } from '../../shared/utils/speech'
25
+
26
+
27
+
3
28
  import {
4
29
  type AgentProgress,
5
30
  type ChatResponse,
@@ -7,317 +32,6 @@ import {
7
32
  getTtsUrls,
8
33
  } from '../tauri'
9
34
 
10
- const GEMINI_MODELS = [
11
- 'gemini-2.5-flash',
12
- 'gemini-2.5-pro',
13
- 'gemini-2.0-flash',
14
- 'gemini-1.5-flash',
15
- 'gemini-1.5-pro',
16
- 'gemini-3.1-flash-lite',
17
- 'gemini-3.1-flash-lite-preview'
18
- ]
19
-
20
- const OPENAI_MODELS = [
21
- 'gpt-4o',
22
- 'gpt-4o-mini',
23
- 'o1',
24
- 'o3-mini',
25
- 'o1-preview',
26
- 'o1-mini',
27
- 'gpt-4-turbo'
28
- ]
29
-
30
- const OPENROUTER_MODELS = [
31
- 'openai/gpt-4o-mini',
32
- 'openai/gpt-4o',
33
- 'anthropic/claude-3.5-sonnet',
34
- 'anthropic/claude-3.5-haiku',
35
- 'google/gemini-2.5-flash',
36
- 'meta-llama/llama-3.3-70b-instruct',
37
- 'mistralai/mistral-large'
38
- ]
39
-
40
- const DEEPSEEK_MODELS = [
41
- 'deepseek-v4-flash',
42
- 'deepseek-v4-pro',
43
- 'deepseek-chat',
44
- 'deepseek-reasoner'
45
- ]
46
-
47
- const ANTHROPIC_MODELS = [
48
- 'claude-3-7-sonnet-latest',
49
- 'claude-3-5-sonnet-latest',
50
- 'claude-3-5-haiku-latest',
51
- 'claude-3-opus-latest'
52
- ]
53
-
54
- const HF_MODELS = [
55
- 'meta-llama/Llama-3.3-70B-Instruct',
56
- 'meta-llama/Meta-Llama-3-8B-Instruct',
57
- 'meta-llama/Llama-3.2-3B-Instruct',
58
- 'Qwen/Qwen2.5-72B-Instruct',
59
- 'Qwen/Qwen2.5-Coder-32B-Instruct',
60
- 'mistralai/Mistral-7B-Instruct-v0.3',
61
- 'google/gemma-2-9b-it'
62
- ]
63
-
64
- const LOCAL_MODELS = [
65
- 'local-model',
66
- 'Qwen/Qwen2.5-7B-Instruct-GGUF',
67
- 'meta-llama/Llama-3.2-3B-Instruct-GGUF',
68
- 'lmstudio-community/gemma-2-9b-it-GGUF'
69
- ]
70
-
71
- const OLLAMA_MODELS = [
72
- 'llama3:latest',
73
- 'llama3.1:latest',
74
- 'llama3.2:latest',
75
- 'gemma2:latest',
76
- 'mistral:latest',
77
- 'phi3:latest',
78
- 'qwen2.5:latest'
79
- ]
80
-
81
- interface ApprovalDetails {
82
- title: string
83
- body: string
84
- reason?: string
85
- isDangerous: boolean
86
- }
87
-
88
- function badge(provider: string, model: string) {
89
- return [provider, model].filter(Boolean).join(' / ')
90
- }
91
-
92
- function providerLabel(provider: string) {
93
- switch (provider) {
94
- case 'gemini':
95
- return 'Gemini'
96
- case 'openai':
97
- return 'OpenAI'
98
- case 'openrouter':
99
- return 'OpenRouter'
100
- case 'deepseek':
101
- return 'DeepSeek'
102
- case 'anthropic':
103
- return 'Claude'
104
- case 'huggingface':
105
- return 'Hugging Face'
106
- case 'local_openai':
107
- return 'Local OpenAI'
108
- case 'ollama':
109
- return 'Ollama'
110
- default:
111
- return provider || 'Primary provider'
112
- }
113
- }
114
-
115
- function fallbackNotice(response: Pick<ChatResponse, 'provider' | 'fallbackProvider'> | null | undefined) {
116
- if (!response?.fallbackProvider) return ''
117
- return `${providerLabel(response.fallbackProvider)} unavailable, fell back to ${providerLabel(response.provider)}.`
118
- }
119
-
120
- interface AgentActivity {
121
- label: string
122
- target: string
123
- kind: 'file' | 'folder' | 'search' | 'terminal' | 'tool'
124
- state: 'active' | 'done' | 'error'
125
- }
126
-
127
- interface AgentActivityView {
128
- summary: string
129
- items: AgentActivity[]
130
- }
131
-
132
- function activityDetail(input: Record<string, unknown>, key: string) {
133
- const value = input[key]
134
- return typeof value === 'string' && value.trim() ? value : ''
135
- }
136
-
137
- function formatActivityTarget(value: string) {
138
- const compact = value.replace(/^\/home\/([^/]+)/, '~')
139
- return compact || 'workspace'
140
- }
141
-
142
- function activityKind(action: string, target: string): AgentActivity['kind'] {
143
- if (['search_code', 'semantic_search', 'knowledge_search', 'web_search', 'memory_recall'].includes(action)) return 'search'
144
- if (['run_shell', 'verify'].includes(action)) return 'terminal'
145
- if (['list_files', 'detect_project'].includes(action)) return 'folder'
146
- if (['read_file', 'symbols', 'read_diagnostics', 'git_diff', 'apply_patch', 'write_file', 'note_write', 'view_image'].includes(action)) return 'file'
147
- return target.includes('/') && !/\.[^/]+$/.test(target) ? 'folder' : 'tool'
148
- }
149
-
150
- function describeTool(action: string, input: Record<string, unknown>): AgentActivity {
151
- const path = activityDetail(input, 'path')
152
- const query = activityDetail(input, 'query')
153
- const command = activityDetail(input, 'command')
154
- const name = activityDetail(input, 'name')
155
- const tool = activityDetail(input, 'tool')
156
- const target = path || query || command || name || tool || action.replaceAll('_', ' ')
157
- const labels: Record<string, string> = {
158
- apply_patch: 'Applying patch',
159
- ask_user: 'Asking user',
160
- create_plan: 'Creating plan',
161
- detect_project: 'Detecting project',
162
- git_branch: 'Reading branch',
163
- git_diff: 'Reading diff',
164
- git_log: 'Reading log',
165
- git_status: 'Reading git status',
166
- knowledge_search: 'Searching knowledge',
167
- list_files: 'Listing files',
168
- list_tests: 'Listing tests',
169
- mcp_tool: 'Calling MCP tool',
170
- memory_recall: 'Recalling memory',
171
- note_write: 'Writing note',
172
- read_diagnostics: 'Reading diagnostics',
173
- read_file: 'Reading file',
174
- request_user_approval: 'Requesting approval',
175
- run_plugin: 'Running plugin',
176
- run_shell: 'Running command',
177
- search_code: 'Searching code',
178
- semantic_index: 'Indexing code',
179
- semantic_search: 'Searching code',
180
- symbols: 'Inspecting symbols',
181
- update_plan: 'Updating plan',
182
- verify: 'Verifying',
183
- view_image: 'Viewing image',
184
- web_search: 'Searching web',
185
- write_file: 'Writing file',
186
- }
187
- return {
188
- label: labels[action] ?? 'Using tool',
189
- target: formatActivityTarget(target),
190
- kind: activityKind(action, target),
191
- state: 'active',
192
- }
193
- }
194
-
195
- function activitySummary(items: AgentActivity[]) {
196
- const files = new Set<string>()
197
- const folders = new Set<string>()
198
- for (const item of items) {
199
- if (item.kind === 'file') files.add(item.target)
200
- if (item.kind === 'folder') folders.add(item.target)
201
- }
202
- const parts = [
203
- files.size ? `${files.size} ${files.size === 1 ? 'file' : 'files'}` : '',
204
- folders.size ? `${folders.size} ${folders.size === 1 ? 'folder' : 'folders'}` : '',
205
- ].filter(Boolean)
206
- return parts.length ? `Exploring ${parts.join(', ')}` : 'Working through task'
207
- }
208
-
209
- function activitiesFrom(progress: AgentProgress[]): AgentActivityView {
210
- const activities: AgentActivity[] = []
211
- for (const event of progress) {
212
- if (event.type === 'ToolStart') {
213
- activities.push(describeTool(event.data.action, event.data.input))
214
- } else if (event.type === 'ToolEnd') {
215
- for (let index = activities.length - 1; index >= 0; index -= 1) {
216
- if (activities[index].state !== 'active') continue
217
- activities[index].state = event.data.result.startsWith('Error:') ? 'error' : 'done'
218
- if (activities[index].state === 'error') activities[index].label = 'Failed'
219
- break
220
- }
221
- }
222
- }
223
- const items = activities.slice(-12)
224
- return { summary: activitySummary(activities), items }
225
- }
226
-
227
- function renderAgentActivityTable(activityView: AgentActivityView) {
228
- return (
229
- <div className="agent-activity-list">
230
- <div className="agent-activity-table-head" aria-hidden="true">
231
- <span>Tool</span>
232
- <span />
233
- <span>Target</span>
234
- <span />
235
- </div>
236
- {activityView.items.map((activity, index) => (
237
- <div className="agent-activity-item" data-kind={activity.kind} data-state={activity.state} key={`${index}-${activity.label}-${activity.target}`}>
238
- <span className="agent-activity-label">{activity.label}</span>
239
- <span className="agent-activity-icon" aria-hidden="true" />
240
- <span className="agent-activity-text">{activity.target}</span>
241
- <span className="agent-activity-chevron" aria-hidden="true">&gt;</span>
242
- </div>
243
- ))}
244
- </div>
245
- )
246
- }
247
-
248
- function renderApprovalDetails(approval: any): ApprovalDetails {
249
- if (!approval) return { title: 'Action Pending Approval', body: 'No action details available.', isDangerous: false }
250
- if (approval.WriteFile) return { title: 'Write File', body: `Path: ${approval.WriteFile.path}`, reason: approval.WriteFile.diff ? `Diff:\n${approval.WriteFile.diff}` : 'Writing new file content.', isDangerous: false }
251
- if (approval.ApplyPatch) return { title: 'Apply Patch', body: `Path: ${approval.ApplyPatch.path}`, reason: approval.ApplyPatch.diff ? `Diff:\n${approval.ApplyPatch.diff}` : 'Applying code patch.', isDangerous: false }
252
- if (approval.RunShell) return { title: 'Run Shell Command', body: approval.RunShell.command, reason: 'Executing shell commands can modify your system.', isDangerous: true }
253
- if (approval.NoteWrite) return { title: 'Write Note', body: `Path: ${approval.NoteWrite.path}`, reason: 'Creating or updating workspace notes.', isDangerous: false }
254
- if (approval.RunPlugin) return { title: `Run Plugin: ${approval.RunPlugin.name}`, body: approval.RunPlugin.instruction, reason: 'Executing a native plugin action.', isDangerous: false }
255
- if (approval.McpTool) {
256
- const { server, tool, arguments: args } = approval.McpTool
257
- return { title: `Run MCP Tool: ${server}/${tool}`, body: typeof args === 'string' ? args : JSON.stringify(args, null, 2), reason: 'Running external MCP tool.', isDangerous: false }
258
- }
259
- if (approval.UserApproval) return { title: approval.UserApproval.title, body: approval.UserApproval.prompt, reason: 'The agent requested explicit approval.', isDangerous: false }
260
- if (approval.AskUser) return { title: 'Question From Agent', body: approval.AskUser.question, reason: 'Approve to continue without a typed answer, or cancel to decline.', isDangerous: false }
261
- return { title: 'Unknown Action', body: JSON.stringify(approval, null, 2), reason: 'Requires approval to proceed.', isDangerous: false }
262
- }
263
-
264
- function renderDiff(diffText: string) {
265
- if (!diffText) return null
266
- const lines = diffText.split('\n')
267
- return (
268
- <div style={{
269
- background: '#0b0f19',
270
- borderRadius: '6px',
271
- padding: '8px',
272
- border: '1px solid rgba(255, 255, 255, 0.08)',
273
- overflowX: 'auto',
274
- maxHeight: '400px',
275
- fontFamily: 'monospace',
276
- fontSize: '0.74rem',
277
- lineHeight: '1.4',
278
- }}>
279
- {lines.map((line, idx) => {
280
- let style: any = {
281
- whiteSpace: 'pre-wrap',
282
- padding: '2px 6px',
283
- }
284
-
285
- if (line.startsWith('@@') || line.startsWith('---') || line.startsWith('+++')) {
286
- style = {
287
- ...style,
288
- color: '#64748b',
289
- fontWeight: 'bold',
290
- }
291
- } else if (line.startsWith('+')) {
292
- style = {
293
- ...style,
294
- background: 'rgba(16, 185, 129, 0.12)',
295
- borderLeft: '3px solid #10b981',
296
- color: '#a7f3d0'
297
- }
298
- } else if (line.startsWith('-')) {
299
- style = {
300
- ...style,
301
- background: 'rgba(239, 68, 68, 0.12)',
302
- borderLeft: '3px solid #ef4444',
303
- color: '#fca5a5'
304
- }
305
- } else {
306
- style = {
307
- ...style,
308
- color: '#e2e8f0',
309
- }
310
- }
311
-
312
- return (
313
- <div key={idx} style={style}>
314
- {line}
315
- </div>
316
- )
317
- })}
318
- </div>
319
- )
320
- }
321
35
 
322
36
  interface ChatPanelProps {
323
37
  interactions: any[]
@@ -343,7 +57,6 @@ interface ChatPanelProps {
343
57
  onSelectImage: (event: ChangeEvent<HTMLInputElement>) => void
344
58
  onSelectDocument: (event: ChangeEvent<HTMLInputElement>) => void
345
59
  onPasteImage: (clipboardData: DataTransfer) => boolean
346
- onReadClipboardImage: () => Promise<boolean>
347
60
  onSetMessage: (message: string) => void
348
61
  onSendVoiceMessage: (message: string, audioDataUri?: string | null) => Promise<void>
349
62
  onRemoveImage: (idx: number) => void
@@ -353,581 +66,13 @@ interface ChatPanelProps {
353
66
  onSetSmartContext: (enabled: boolean) => void
354
67
  onSetAgentMode: (enabled: boolean) => void
355
68
  onSetProvider: (provider: string) => void
356
- onApproval: (approved: boolean, autoApproveSession?: boolean) => void
69
+ onApproval: (approved: boolean, autoApproveSession?: boolean, answer?: string) => void
357
70
  onToggleMobileSidebar: () => void
358
71
  settingsConfig: any
359
72
  onSetModel: (model: string) => void
73
+ onCancelMessage: () => void
360
74
  }
361
75
 
362
- function ChatCodeBlock({ code, language }: { code: string; language: string; key?: any }) {
363
- const [copied, setCopied] = useState(false)
364
-
365
- const handleCopy = async () => {
366
- try {
367
- await navigator.clipboard.writeText(code)
368
- setCopied(true)
369
- setTimeout(() => setCopied(false), 2000)
370
- } catch (err) {
371
- console.error('Failed to copy code: ', err)
372
- }
373
- }
374
-
375
- const handleDownload = () => {
376
- try {
377
- const blob = new Blob([code], { type: 'text/plain;charset=utf-8' })
378
- const url = URL.createObjectURL(blob)
379
- const link = document.createElement('a')
380
- link.href = url
381
- link.download = `code.${language === 'plaintext' ? 'txt' : language}`
382
- link.click()
383
- URL.revokeObjectURL(url)
384
- } catch (err) {
385
- console.error('Failed to download code: ', err)
386
- }
387
- }
388
-
389
- const displayLang = language.charAt(0).toUpperCase() + language.slice(1).toLowerCase()
390
-
391
- return (
392
- <div className="chat-code-block-container" style={{ whiteSpace: 'normal' }}>
393
- <div className="chat-code-block-header">
394
- <span className="chat-code-block-lang">{displayLang}</span>
395
- <div className="chat-code-block-actions">
396
- <button type="button" onClick={handleDownload} title="Download code" className="chat-code-action-btn">
397
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
398
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
399
- <polyline points="7 10 12 15 17 10" />
400
- <line x1="12" y1="15" x2="12" y2="3" />
401
- </svg>
402
- </button>
403
- <button type="button" onClick={handleCopy} title="Copy code" className="chat-code-action-btn">
404
- {copied ? (
405
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
406
- <polyline points="20 6 9 17 4 12" />
407
- </svg>
408
- ) : (
409
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
410
- <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
411
- <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
412
- </svg>
413
- )}
414
- </button>
415
- </div>
416
- </div>
417
- <pre className="chat-code-block-body">
418
- <code>{code}</code>
419
- </pre>
420
- </div>
421
- )
422
- }
423
-
424
- const isTableLine = (line: string): boolean => {
425
- const trimmed = line.trim()
426
- return trimmed.startsWith('|') && trimmed.endsWith('|') && trimmed.length > 1
427
- }
428
-
429
- function renderFormattedMessage(text: string) {
430
- const displayText = readableAssistantText(text)
431
- if (!displayText) return null
432
-
433
- const formatInline = (str: string) => {
434
- const codeParts = str.split(/`([\s\S]*?)`/g)
435
- return codeParts.map((codePart, codeIndex) => {
436
- if (codeIndex % 2 === 1) {
437
- return (
438
- <code key={`code-${codeIndex}`} className="chat-inline-code">
439
- {codePart}
440
- </code>
441
- )
442
- }
443
- const boldParts = codePart.split(/\*\*([\s\S]*?)\*\*/g)
444
- return boldParts.map((boldPart, boldIndex) => {
445
- if (boldIndex % 2 === 1) {
446
- return (
447
- <strong key={`bold-${boldIndex}`} className="chat-bold-highlight">
448
- {boldPart}
449
- </strong>
450
- )
451
- }
452
- return boldPart
453
- })
454
- })
455
- }
456
-
457
- const renderHtmlTable = (tableLines: string[], key: string) => {
458
- const rows: string[][] = []
459
- for (const line of tableLines) {
460
- const trimmed = line.trim()
461
- if (!trimmed) continue
462
- const content = trimmed.slice(1, -1)
463
- const contentEscaped = content.replace(/\\\|/g, '\u0000')
464
- const cols = contentEscaped.split('|').map(s => s.replace(/\u0000/g, '|').trim())
465
- rows.push(cols)
466
- }
467
-
468
- if (rows.length === 0) return null
469
-
470
- let hasSeparator = false
471
- let separatorIdx = -1
472
- for (let i = 0; i < rows.length; i++) {
473
- const row = rows[i]
474
- if (row.length > 0 && row.every(col => /^[-\s:]+$/.test(col))) {
475
- hasSeparator = true
476
- separatorIdx = i
477
- break
478
- }
479
- }
480
-
481
- let headerRow: string[] | null = null
482
- const dataRows: string[][] = []
483
-
484
- if (hasSeparator) {
485
- if (separatorIdx > 0) {
486
- headerRow = rows[separatorIdx - 1]
487
- for (let i = 0; i < rows.length; i++) {
488
- if (i !== separatorIdx && i !== separatorIdx - 1) {
489
- dataRows.push(rows[i])
490
- }
491
- }
492
- } else {
493
- for (let i = 0; i < rows.length; i++) {
494
- if (i !== separatorIdx) {
495
- dataRows.push(rows[i])
496
- }
497
- }
498
- }
499
- } else {
500
- headerRow = rows[0]
501
- for (let i = 1; i < rows.length; i++) {
502
- dataRows.push(rows[i])
503
- }
504
- }
505
-
506
- return (
507
- <div key={key} className="chat-table-container" style={{
508
- overflowX: 'auto',
509
- margin: '14px 0',
510
- width: '100%',
511
- borderRadius: '8px',
512
- border: '1px solid rgba(255, 255, 255, 0.12)',
513
- background: 'rgba(30, 41, 59, 0.35)',
514
- boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
515
- }}>
516
- <table className="chat-table" style={{
517
- width: '100%',
518
- borderCollapse: 'collapse',
519
- fontSize: '0.86rem',
520
- textAlign: 'left',
521
- lineHeight: '1.5',
522
- }}>
523
- {headerRow && (
524
- <thead>
525
- <tr style={{
526
- background: 'rgba(255, 255, 255, 0.04)',
527
- borderBottom: '2px solid rgba(255, 255, 255, 0.15)',
528
- }}>
529
- {headerRow.map((col, idx) => (
530
- <th key={`th-${idx}`} style={{
531
- padding: '12px 16px',
532
- fontWeight: 700,
533
- color: 'var(--accent, #38bdf8)',
534
- fontFamily: 'ui-sans-serif, system-ui, sans-serif',
535
- }}>
536
- {formatInline(col)}
537
- </th>
538
- ))}
539
- </tr>
540
- </thead>
541
- )}
542
- <tbody>
543
- {dataRows.map((row, rIdx) => (
544
- <tr key={`tr-${rIdx}`} style={{
545
- background: rIdx % 2 === 1 ? 'rgba(255, 255, 255, 0.015)' : 'transparent',
546
- borderBottom: rIdx < dataRows.length - 1 ? '1px solid rgba(255, 255, 255, 0.08)' : 'none',
547
- }}>
548
- {row.map((col, cIdx) => (
549
- <td key={`td-${cIdx}`} style={{
550
- padding: '12px 16px',
551
- color: 'var(--text-main, #e2e8f0)',
552
- }}>
553
- {formatInline(col)}
554
- </td>
555
- ))}
556
- </tr>
557
- ))}
558
- </tbody>
559
- </table>
560
- </div>
561
- )
562
- }
563
-
564
- const lines = displayText.split('\n')
565
- const items: any[] = []
566
-
567
- let inCodeBlock = false
568
- let codeBlockLang = ''
569
- let codeBlockLines: string[] = []
570
-
571
- let inTable = false
572
- let tableLines: string[] = []
573
-
574
- const flushTable = (index: number) => {
575
- if (tableLines.length > 0) {
576
- items.push(renderHtmlTable(tableLines, `table-${index}`))
577
- tableLines = []
578
- inTable = false
579
- }
580
- }
581
-
582
- for (let i = 0; i < lines.length; i++) {
583
- const line = lines[i]
584
-
585
- if (line.trim().startsWith('```')) {
586
- if (inTable) {
587
- flushTable(i)
588
- }
589
- if (inCodeBlock) {
590
- const codeText = codeBlockLines.join('\n')
591
- items.push(
592
- <ChatCodeBlock
593
- key={`code-block-${i}`}
594
- code={codeText}
595
- language={codeBlockLang}
596
- />
597
- )
598
- inCodeBlock = false
599
- codeBlockLines = []
600
- } else {
601
- inCodeBlock = true
602
- codeBlockLang = line.trim().slice(3).trim() || 'plaintext'
603
- }
604
- continue
605
- }
606
-
607
- if (inCodeBlock) {
608
- codeBlockLines.push(line)
609
- continue
610
- }
611
-
612
- if (isTableLine(line)) {
613
- inTable = true
614
- tableLines.push(line)
615
- } else {
616
- if (inTable) {
617
- flushTable(i)
618
- }
619
-
620
- const headerMatch = line.match(/^(#{1,6})\s+(.*)$/)
621
- if (headerMatch) {
622
- const level = headerMatch[1].length
623
- const content = headerMatch[2]
624
-
625
- const style = {
626
- fontWeight: 'bold',
627
- display: 'block',
628
- marginTop: level === 1 ? '16px' : level === 2 ? '14px' : '10px',
629
- marginBottom: '6px',
630
- fontSize: level === 1 ? '1.25em' : level === 2 ? '1.15em' : '1.05em',
631
- color: 'var(--text-main)',
632
- }
633
-
634
- items.push(
635
- <span key={`line-${i}`} style={style}>
636
- {formatInline(content)}
637
- </span>
638
- )
639
- } else {
640
- const listMatch = line.match(/^(\s*)([-*+])\s+(.*)$/)
641
- if (listMatch) {
642
- const indent = listMatch[1]
643
- const content = listMatch[3]
644
- items.push(
645
- <Fragment key={`line-${i}`}>
646
- {indent}• {formatInline(content)}
647
- {i < lines.length - 1 && '\n'}
648
- </Fragment>
649
- )
650
- } else {
651
- items.push(
652
- <Fragment key={`line-${i}`}>
653
- {formatInline(line)}
654
- {i < lines.length - 1 && '\n'}
655
- </Fragment>
656
- )
657
- }
658
- }
659
- }
660
- }
661
-
662
- if (inTable) {
663
- flushTable(lines.length)
664
- }
665
-
666
- if (inCodeBlock && codeBlockLines.length > 0) {
667
- const codeText = codeBlockLines.join('\n')
668
- items.push(
669
- <ChatCodeBlock
670
- key={`code-block-end`}
671
- code={codeText}
672
- language={codeBlockLang}
673
- />
674
- )
675
- }
676
-
677
- return items
678
- }
679
-
680
- const THINKING_LABELS = {
681
- live: 'Thinking…',
682
- completed: (count: number) => `Thinking process (${count} steps)`,
683
- step: (index: number) => `Step ${index}`,
684
- emptyHint: 'Model didn\'t send thinking steps this time',
685
- } as const
686
-
687
- function ThinkingBlock({
688
- thoughts,
689
- isLive = false,
690
- blockKey,
691
- expanded,
692
- onExpandedChange,
693
- showEmptyHint = false,
694
- }: {
695
- thoughts: string[]
696
- isLive?: boolean
697
- blockKey: string
698
- expanded?: boolean
699
- onExpandedChange?: (key: string, open: boolean) => void
700
- showEmptyHint?: boolean
701
- }) {
702
- const contentRef = useRef<HTMLDivElement>(null)
703
- const isControlled = expanded !== undefined && Boolean(onExpandedChange)
704
- const [localOpen, setLocalOpen] = useState(isLive)
705
- const isOpen = isControlled ? Boolean(expanded) : localOpen
706
-
707
- const setOpen = (open: boolean) => {
708
- if (isControlled && onExpandedChange) onExpandedChange(blockKey, open)
709
- else setLocalOpen(open)
710
- }
711
-
712
- useEffect(() => {
713
- if (isLive && !isControlled) setLocalOpen(true)
714
- }, [isLive, isControlled])
715
-
716
- useEffect(() => {
717
- if (!isLive || !isOpen || !contentRef.current) return
718
- contentRef.current.scrollTop = contentRef.current.scrollHeight
719
- }, [thoughts, isLive, isOpen])
720
-
721
- if (thoughts.length === 0 && !showEmptyHint) return null
722
-
723
- if (thoughts.length === 0) {
724
- return (
725
- <div className="thinking-block thinking-block-empty-state" data-live={isLive ? 'true' : 'false'}>
726
- <span className="thinking-block-empty">{THINKING_LABELS.emptyHint}</span>
727
- </div>
728
- )
729
- }
730
-
731
- return (
732
- <div className={`thinking-block${isLive ? ' is-live' : ''}`}>
733
- <button
734
- type="button"
735
- className="thinking-block-header"
736
- onClick={() => setOpen(!isOpen)}
737
- aria-expanded={isOpen}
738
- >
739
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}>
740
- <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
741
- <line x1="12" y1="17" x2="12.01" y2="17" />
742
- <circle cx="12" cy="12" r="10" />
743
- </svg>
744
- <span className="thinking-block-label">
745
- {isLive ? THINKING_LABELS.live : THINKING_LABELS.completed(thoughts.length)}
746
- </span>
747
- {isLive && <span className="thinking-block-live-dot" aria-hidden="true" />}
748
- <span className={`thinking-block-chevron${isOpen ? ' is-open' : ''}`} aria-hidden="true">
749
- <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
750
- <polyline points="9 18 15 12 9 6" />
751
- </svg>
752
- </span>
753
- </button>
754
- {isOpen && (
755
- <div className="thinking-block-body">
756
- <div className="thinking-block-content" ref={contentRef}>
757
- {thoughts.map((thought, index) => (
758
- <div className="thinking-block-step" key={`${blockKey}-${index}`}>
759
- {thoughts.length > 1 && (
760
- <div className="thinking-block-step-label">{THINKING_LABELS.step(index + 1)}</div>
761
- )}
762
- <div className="thinking-block-step-body">{renderFormattedMessage(thought)}</div>
763
- </div>
764
- ))}
765
- </div>
766
- </div>
767
- )}
768
- </div>
769
- )
770
- }
771
-
772
- function readableAssistantText(text: string) {
773
- if (typeof text !== 'string') return ''
774
- const trimmed = text.trim()
775
- if (!trimmed.startsWith('{')) return text
776
- try {
777
- const value = JSON.parse(trimmed)
778
- if (value?.action === 'finish' && typeof value?.input?.summary === 'string' && value.input.summary.trim()) {
779
- return value.input.summary
780
- }
781
- if (typeof value?.finish?.summary === 'string' && value.finish.summary.trim()) {
782
- return value.finish.summary
783
- }
784
- } catch {
785
- return text
786
- }
787
- return text
788
- }
789
-
790
- function renderSpeakerIcon(isSpeaking: boolean) {
791
- if (isSpeaking) {
792
- return (
793
- <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'block' }}>
794
- <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
795
- <path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
796
- <path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
797
- </svg>
798
- )
799
- }
800
- return (
801
- <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'block' }}>
802
- <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
803
- </svg>
804
- )
805
- }
806
-
807
- function cleanSpeechText(text: string) {
808
- return readableAssistantText(text)
809
- .replace(/\*\*([\s\S]*?)\*\*/g, '$1')
810
- .replace(/[*_`#]/g, '')
811
- .trim()
812
- }
813
-
814
- function numericSetting(value: unknown, fallback: number) {
815
- const numeric = Number(value)
816
- return Number.isFinite(numeric) ? numeric : fallback
817
- }
818
-
819
- interface DiffHunk {
820
- oldText: string
821
- newText: string
822
- }
823
-
824
- interface FileChange {
825
- path: string
826
- created: boolean
827
- additions: number
828
- deletions: number
829
- hunks: DiffHunk[]
830
- }
831
-
832
- function parseFileChangesFromProgress(progress: AgentProgress[]): FileChange[] {
833
- const changes = new Map<string, FileChange>()
834
- let activeEdit: { action: string; path: string; created: boolean; additions: number; deletions: number; hunks: DiffHunk[] } | null = null
835
-
836
- for (const event of progress || []) {
837
- if (event.type === 'ToolStart') {
838
- if (event.data.action === 'apply_patch') {
839
- const patch = (event.data.input as any)?.patch
840
- if (patch && typeof patch.path === 'string') {
841
- let additions = 0
842
- let deletions = 0
843
- const hunksList: DiffHunk[] = []
844
- const hunks = patch.hunks
845
- if (Array.isArray(hunks)) {
846
- for (const hunk of hunks) {
847
- const oldText = hunk?.oldText || ''
848
- const newText = hunk?.newText || ''
849
- const oldLines = oldText ? oldText.split('\n').length : 0
850
- const newLines = newText ? newText.split('\n').length : 0
851
- deletions += oldLines
852
- additions += newLines
853
- hunksList.push({ oldText, newText })
854
- }
855
- }
856
- activeEdit = {
857
- action: 'apply_patch',
858
- path: patch.path,
859
- created: false,
860
- additions,
861
- deletions,
862
- hunks: hunksList
863
- }
864
- }
865
- } else if (event.data.action === 'write_file') {
866
- const path = (event.data.input as any)?.path
867
- const fileContent = (event.data.input as any)?.file_content || ''
868
- if (typeof path === 'string') {
869
- const additions = fileContent ? fileContent.split('\n').length : 0
870
- activeEdit = {
871
- action: 'write_file',
872
- path,
873
- created: true,
874
- additions,
875
- deletions: 0,
876
- hunks: [{ oldText: '', newText: fileContent }]
877
- }
878
- }
879
- } else {
880
- activeEdit = null
881
- }
882
- } else if (event.type === 'ToolEnd') {
883
- if (activeEdit && (event.data.action === 'apply_patch' || event.data.action === 'write_file')) {
884
- const isError = typeof event.data.result === 'string' && event.data.result.startsWith('Error:')
885
- if (!isError) {
886
- try {
887
- const applied = JSON.parse(event.data.result)
888
- const appliedPaths = Array.isArray(applied) ? applied.map(item => item?.path).filter(Boolean) : [activeEdit.path]
889
-
890
- for (const path of appliedPaths) {
891
- const existing = changes.get(path)
892
- if (existing) {
893
- existing.additions += activeEdit.additions
894
- existing.deletions += activeEdit.deletions
895
- existing.hunks.push(...activeEdit.hunks)
896
- } else {
897
- changes.set(path, {
898
- path,
899
- created: activeEdit.created,
900
- additions: activeEdit.additions,
901
- deletions: activeEdit.deletions,
902
- hunks: [...activeEdit.hunks]
903
- })
904
- }
905
- }
906
- } catch (e) {
907
- const path = activeEdit.path
908
- const existing = changes.get(path)
909
- if (existing) {
910
- existing.additions += activeEdit.additions
911
- existing.deletions += activeEdit.deletions
912
- existing.hunks.push(...activeEdit.hunks)
913
- } else {
914
- changes.set(path, {
915
- path,
916
- created: activeEdit.created,
917
- additions: activeEdit.additions,
918
- deletions: activeEdit.deletions,
919
- hunks: [...activeEdit.hunks]
920
- })
921
- }
922
- }
923
- }
924
- }
925
- activeEdit = null
926
- }
927
- }
928
-
929
- return Array.from(changes.values())
930
- }
931
76
 
932
77
  export default function ChatPanel({
933
78
  interactions,
@@ -953,7 +98,6 @@ export default function ChatPanel({
953
98
  onSelectImage,
954
99
  onSelectDocument,
955
100
  onPasteImage,
956
- onReadClipboardImage,
957
101
  onSetMessage,
958
102
  onSendVoiceMessage,
959
103
  onRemoveImage,
@@ -967,14 +111,45 @@ export default function ChatPanel({
967
111
  onToggleMobileSidebar,
968
112
  settingsConfig,
969
113
  onSetModel,
114
+ onCancelMessage,
970
115
  }: ChatPanelProps) {
971
116
  const agentActivities = activitiesFrom(agentProgress)
972
117
  const activeFallbackNotice = fallbackNotice(streamedResponse)
118
+ const lastThinkingProgress = [...agentProgress].reverse().find(p => p.type === 'Thinking')
119
+ let activeAgentName: string | null = null
120
+ let activeModelName: string | null = null
121
+ if (lastThinkingProgress && lastThinkingProgress.type === 'Thinking') {
122
+ activeAgentName = (lastThinkingProgress.data as any).agent_name || null
123
+ activeModelName = (lastThinkingProgress.data as any).model_name || null
124
+ }
973
125
  const [openActivityIds, setOpenActivityIds] = useState<Record<string, boolean>>({})
974
126
  const [openReviewIds, setOpenReviewIds] = useState<Record<string, boolean>>({})
975
127
  const [openFileDiffs, setOpenFileDiffs] = useState<Record<string, boolean>>({})
976
128
  const [toolMenuOpen, setToolMenuOpen] = useState(false)
977
129
  const toolMenuRef = useRef<HTMLDivElement | null>(null)
130
+ const [dynamicOllamaModels, setDynamicOllamaModels] = useState<string[]>(OLLAMA_MODELS)
131
+
132
+ useEffect(() => {
133
+ const fetchOllamaModels = async () => {
134
+ if (status?.activeProvider !== 'ollama') return;
135
+ const host = settingsConfig?.ollamaHost || 'http://localhost:11434';
136
+ const cleanHost = host.endsWith('/') ? host.slice(0, -1) : host;
137
+ try {
138
+ const res = await fetch(`${cleanHost}/api/tags`);
139
+ if (res.ok) {
140
+ const data = await res.json();
141
+ if (data && Array.isArray(data.models)) {
142
+ setDynamicOllamaModels(data.models.map((m: any) => m.name));
143
+ return;
144
+ }
145
+ }
146
+ } catch (err) {
147
+ // fallback to default if fetch fails
148
+ }
149
+ setDynamicOllamaModels(OLLAMA_MODELS);
150
+ }
151
+ fetchOllamaModels();
152
+ }, [status?.activeProvider, settingsConfig?.ollamaHost])
978
153
  const canSubmit = Boolean(message.trim() || imageAttachments.length > 0 || documentName)
979
154
  const sendingImageMarkers = Array.from({ length: sendingImageCount }, (_, index) => `[Image #${index + 1}]`).join(' ')
980
155
 
@@ -995,7 +170,7 @@ export default function ChatPanel({
995
170
  case 'local_openai':
996
171
  return LOCAL_MODELS
997
172
  case 'ollama':
998
- return OLLAMA_MODELS
173
+ return dynamicOllamaModels
999
174
  default:
1000
175
  return []
1001
176
  }
@@ -1048,60 +223,40 @@ export default function ChatPanel({
1048
223
  return () => clearInterval(timer)
1049
224
  }, [sending])
1050
225
 
1051
- const [isRecording, setIsRecording] = useState(false)
1052
- const [voiceMode, setVoiceMode] = useState(false)
1053
- const [voiceTranscript, setVoiceTranscript] = useState('')
1054
- const [voiceAwaitingResponse, setVoiceAwaitingResponse] = useState(false)
1055
226
  const [speakingText, setSpeakingText] = useState<string | null>(null)
1056
- const recognitionRef = useRef<SpeechRecognition | null>(null)
1057
- const mediaRecorderRef = useRef<MediaRecorder | null>(null)
1058
- const mediaStreamRef = useRef<MediaStream | null>(null)
1059
- const audioContextRef = useRef<AudioContext | null>(null)
1060
- const vadTimerRef = useRef<number | null>(null)
1061
227
  const audioRef = useRef<HTMLAudioElement | null>(null)
1062
228
  const speechRunRef = useRef(0)
1063
229
  const historyReadyRef = useRef(false)
1064
230
  const submittedDuringSessionRef = useRef(false)
1065
231
  const lastAutoSpokenIdRef = useRef<number | string | null>(null)
1066
- const voiceModeRef = useRef(false)
1067
- const sendingRef = useRef(false)
1068
- const voiceAwaitingResponseRef = useRef(false)
1069
232
  const speakingRef = useRef<string | null>(null)
1070
- const restartTimerRef = useRef<number | null>(null)
233
+
234
+ const {
235
+ isRecording,
236
+ voiceMode,
237
+ setVoiceMode,
238
+ voiceTranscript,
239
+ setVoiceTranscript,
240
+ voiceAwaitingResponse,
241
+ voiceAwaitingResponseRef,
242
+ voiceModeRef,
243
+ startRecognition,
244
+ stopRecognition,
245
+ scheduleVoiceListen,
246
+ clearRestartTimer
247
+ } = useSpeechToText({
248
+ language: settingsConfig?.language,
249
+ message,
250
+ sending,
251
+ isSpeaking: Boolean(speakingText),
252
+ onSendVoiceMessage,
253
+ onSetMessage: (val) => onSetMessage(val)
254
+ })
255
+
1071
256
  const voiceStatus = speakingText ? 'speaking' : (sending || voiceAwaitingResponse) ? 'thinking' : isRecording ? 'listening' : voiceMode ? 'ready' : 'off'
1072
257
  const voiceStatusLabel = voiceStatus === 'speaking' ? 'Speaking' : voiceStatus === 'thinking' ? 'Thinking' : voiceStatus === 'listening' ? 'Listening' : 'Ready'
1073
258
 
1074
- const clearRestartTimer = () => {
1075
- if (restartTimerRef.current === null) return
1076
- window.clearTimeout(restartTimerRef.current)
1077
- restartTimerRef.current = null
1078
- }
1079
- const clearVadTimer = () => {
1080
- if (vadTimerRef.current === null) return
1081
- window.clearInterval(vadTimerRef.current)
1082
- vadTimerRef.current = null
1083
- }
1084
- const stopRecognition = () => {
1085
- clearRestartTimer()
1086
- recognitionRef.current?.stop()
1087
- recognitionRef.current = null
1088
- clearVadTimer()
1089
- if (mediaRecorderRef.current?.state === 'recording') mediaRecorderRef.current.stop()
1090
- mediaRecorderRef.current = null
1091
- mediaStreamRef.current?.getTracks().forEach((track) => track.stop())
1092
- mediaStreamRef.current = null
1093
- audioContextRef.current?.close().catch(() => {})
1094
- audioContextRef.current = null
1095
- setIsRecording(false)
1096
- }
1097
- const scheduleVoiceListen = (delayMs = 350) => {
1098
- clearRestartTimer()
1099
- if (!voiceModeRef.current || sendingRef.current || voiceAwaitingResponseRef.current || speakingRef.current) return
1100
- restartTimerRef.current = window.setTimeout(() => {
1101
- restartTimerRef.current = null
1102
- startRecognition(true)
1103
- }, delayMs)
1104
- }
259
+
1105
260
  const cancelSpeech = () => {
1106
261
  speechRunRef.current += 1
1107
262
  audioRef.current?.pause()
@@ -1123,7 +278,11 @@ export default function ChatPanel({
1123
278
  utterance.volume = Math.max(0, Math.min(1, numericSetting(settingsConfig?.ttsVolume, 1)))
1124
279
  utterance.rate = Math.max(0.1, Math.min(10, numericSetting(settingsConfig?.ttsSpeed, 1)))
1125
280
  utterance.pitch = Math.max(0, Math.min(2, numericSetting(settingsConfig?.ttsPitch, 1)))
1126
- const voice = window.speechSynthesis.getVoices().find((item) => item.lang.startsWith(hasThai ? 'th' : 'en'))
281
+ const voice = window.speechSynthesis.getVoices().find((item) => {
282
+ const lang = item.lang.toLowerCase()
283
+ const target = hasThai ? 'th' : 'en'
284
+ return lang.startsWith(target) || lang.includes(target)
285
+ })
1127
286
  if (voice) utterance.voice = voice
1128
287
  const finishSpeech = () => {
1129
288
  setSpeakingText((current) => (current === displayText ? null : current))
@@ -1179,213 +338,24 @@ export default function ChatPanel({
1179
338
  }
1180
339
  speakNative(speechText, text)
1181
340
  }
1182
- const blobToDataUri = (blob: Blob) => new Promise<string>((resolve, reject) => {
1183
- const reader = new FileReader()
1184
- reader.onerror = () => reject(reader.error ?? new Error('Failed to read audio recording'))
1185
- reader.onload = () => resolve(String(reader.result))
1186
- reader.readAsDataURL(blob)
1187
- })
1188
- const preferredAudioMimeType = () => {
1189
- const candidates = ['audio/webm;codecs=opus', 'audio/webm', 'audio/ogg;codecs=opus', 'audio/ogg']
1190
- return candidates.find((mimeType) => MediaRecorder.isTypeSupported(mimeType)) ?? ''
1191
- }
1192
- const startAudioRecording = async (autoSend: boolean) => {
1193
- if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === 'undefined') {
1194
- alert("Sorry, this system cannot access the microphone or record audio in this browser")
1195
- voiceModeRef.current = false
1196
- setVoiceMode(false)
1197
- return
1198
- }
1199
-
1200
- try {
1201
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
1202
- const mimeType = preferredAudioMimeType()
1203
- const recorder = new MediaRecorder(stream, mimeType ? { mimeType } : undefined)
1204
- const chunks: Blob[] = []
1205
- let heardVoice = false
1206
- let quietSince = 0
1207
- const startedAt = Date.now()
1208
- const audioContext = new AudioContext()
1209
- const source = audioContext.createMediaStreamSource(stream)
1210
- const analyser = audioContext.createAnalyser()
1211
- const samples = new Uint8Array(analyser.fftSize)
1212
- source.connect(analyser)
1213
-
1214
- mediaStreamRef.current = stream
1215
- mediaRecorderRef.current = recorder
1216
- audioContextRef.current = audioContext
1217
- setVoiceTranscript('Listening to microphone...')
1218
-
1219
- recorder.ondataavailable = (event) => {
1220
- if (event.data.size > 0) chunks.push(event.data)
1221
- }
1222
- recorder.onstop = async () => {
1223
- clearVadTimer()
1224
- mediaRecorderRef.current = null
1225
- mediaStreamRef.current?.getTracks().forEach((track) => track.stop())
1226
- mediaStreamRef.current = null
1227
- audioContextRef.current?.close().catch(() => {})
1228
- audioContextRef.current = null
1229
- setIsRecording(false)
1230
- if (!autoSend || !voiceModeRef.current || chunks.length === 0) return
1231
- if (!heardVoice) {
1232
- setVoiceTranscript('No speech detected')
1233
- scheduleVoiceListen(700)
1234
- return
1235
- }
1236
- const blob = new Blob(chunks, { type: recorder.mimeType || 'audio/webm' })
1237
- const audioDataUri = await blobToDataUri(blob)
1238
- setVoiceTranscript('Sent audio to AI')
1239
- voiceAwaitingResponseRef.current = true
1240
- setVoiceAwaitingResponse(true)
1241
- onSendVoiceMessage('', audioDataUri)
1242
- .catch((error) => console.error('Voice audio message failed', error))
1243
- .finally(() => {
1244
- voiceAwaitingResponseRef.current = false
1245
- setVoiceAwaitingResponse(false)
1246
- scheduleVoiceListen()
1247
- })
1248
- }
1249
-
1250
- recorder.start()
1251
- setIsRecording(true)
1252
- vadTimerRef.current = window.setInterval(() => {
1253
- analyser.getByteTimeDomainData(samples)
1254
- let peak = 0
1255
- for (const sample of samples) {
1256
- peak = Math.max(peak, Math.abs(sample - 128))
1257
- }
1258
- const now = Date.now()
1259
- if (peak > 12) {
1260
- heardVoice = true
1261
- quietSince = 0
1262
- setVoiceTranscript('Listening...')
1263
- } else if (heardVoice) {
1264
- quietSince = quietSince || now
1265
- }
1266
- const silenceElapsed = quietSince ? now - quietSince : 0
1267
- const totalElapsed = now - startedAt
1268
- if ((heardVoice && silenceElapsed > 1300) || totalElapsed > 12000) {
1269
- recorder.stop()
1270
- }
1271
- }, 120)
1272
- } catch (error) {
1273
- console.error('Failed to record microphone audio', error)
1274
- setIsRecording(false)
1275
- voiceModeRef.current = false
1276
- setVoiceMode(false)
1277
- alert('Failed to open microphone. Please check microphone permissions in your browser')
1278
- }
1279
- }
1280
- const startRecognition = (autoSend: boolean) => {
1281
- if (recognitionRef.current || sendingRef.current || voiceAwaitingResponseRef.current || speakingRef.current) return
1282
-
1283
- const SpeechRecognitionApi = window.SpeechRecognition || window.webkitSpeechRecognition
1284
- if (!SpeechRecognitionApi) {
1285
- startAudioRecording(autoSend)
1286
- return
1287
- }
1288
-
1289
- let sentTranscript = false
1290
341
 
1291
- try {
1292
- const recognition = new SpeechRecognitionApi()
1293
- recognition.continuous = false
1294
- recognition.interimResults = true
1295
- recognition.lang = settingsConfig?.language === 'en' ? 'en-US' : 'th-TH'
1296
- recognition.onstart = () => setIsRecording(true)
1297
- recognition.onresult = (event: SpeechRecognitionEvent) => {
1298
- let interimText = ''
1299
- let finalText = ''
1300
- for (let index = event.resultIndex; index < event.results.length; index += 1) {
1301
- const transcript = event.results[index]?.[0]?.transcript ?? ''
1302
- if (event.results[index]?.isFinal) finalText += transcript
1303
- else interimText += transcript
1304
- }
1305
- const displayText = (finalText || interimText).trim()
1306
- if (displayText) setVoiceTranscript(displayText)
1307
- const resultText = finalText.trim()
1308
- if (!resultText) return
1309
- sentTranscript = true
1310
- recognition.stop()
1311
- setVoiceTranscript(resultText)
1312
- if (autoSend) {
1313
- voiceAwaitingResponseRef.current = true
1314
- setVoiceAwaitingResponse(true)
1315
- onSendVoiceMessage(resultText)
1316
- .catch((error) => console.error('Voice message failed', error))
1317
- .finally(() => {
1318
- voiceAwaitingResponseRef.current = false
1319
- setVoiceAwaitingResponse(false)
1320
- scheduleVoiceListen()
1321
- })
1322
- } else {
1323
- onSetMessage(message.trim() ? `${message.trimEnd()} ${resultText}` : resultText)
1324
- }
1325
- }
1326
- recognition.onerror = (event: Event) => {
1327
- console.error('Speech recognition error', event)
1328
- setIsRecording(false)
1329
- }
1330
- recognition.onend = () => {
1331
- recognitionRef.current = null
1332
- setIsRecording(false)
1333
- if (autoSend && voiceModeRef.current && !sentTranscript) scheduleVoiceListen()
1334
- }
1335
- recognitionRef.current = recognition
1336
- recognition.start()
1337
- } catch (error) {
1338
- console.error('Failed to start speech recognition', error)
1339
- recognitionRef.current = null
1340
- setIsRecording(false)
1341
- }
1342
- }
1343
342
  const toggleRecording = () => {
1344
343
  if (voiceMode) {
1345
- voiceModeRef.current = false
1346
- voiceAwaitingResponseRef.current = false
1347
344
  setVoiceMode(false)
1348
- setVoiceAwaitingResponse(false)
1349
- setVoiceTranscript('')
1350
345
  stopRecognition()
1351
346
  cancelSpeech()
1352
347
  return
1353
348
  }
1354
349
 
1355
- voiceModeRef.current = true
1356
350
  setVoiceMode(true)
1357
- setVoiceAwaitingResponse(false)
1358
- setVoiceTranscript('')
1359
351
  startRecognition(true)
1360
352
  }
1361
353
 
1362
354
  useEffect(() => {
1363
355
  return () => {
1364
- clearRestartTimer()
1365
356
  cancelSpeech()
1366
- stopRecognition()
1367
357
  }
1368
358
  }, [])
1369
- useEffect(() => {
1370
- voiceModeRef.current = voiceMode
1371
- if (!voiceMode) {
1372
- clearRestartTimer()
1373
- setVoiceTranscript('')
1374
- }
1375
- }, [voiceMode])
1376
- useEffect(() => {
1377
- sendingRef.current = sending
1378
- }, [sending])
1379
- useEffect(() => {
1380
- voiceAwaitingResponseRef.current = voiceAwaitingResponse
1381
- }, [voiceAwaitingResponse])
1382
- useEffect(() => {
1383
- speakingRef.current = speakingText
1384
- }, [speakingText])
1385
- useEffect(() => {
1386
- if (!voiceMode || sending || voiceAwaitingResponse || speakingText || isRecording) return
1387
- scheduleVoiceListen()
1388
- }, [voiceMode, sending, voiceAwaitingResponse, speakingText, isRecording])
1389
359
  useEffect(() => {
1390
360
  if (sending) submittedDuringSessionRef.current = true
1391
361
  }, [sending])
@@ -1491,21 +461,20 @@ export default function ChatPanel({
1491
461
  if (onPasteImage(event.clipboardData)) {
1492
462
  event.preventDefault()
1493
463
  event.stopPropagation()
1494
- } else {
1495
- window.setTimeout(() => void onReadClipboardImage(), 0)
1496
464
  }
1497
465
  }
1498
466
  window.addEventListener('paste', handleWindowPaste, true)
1499
467
  return () => window.removeEventListener('paste', handleWindowPaste, true)
1500
- }, [onPasteImage, onReadClipboardImage])
468
+ }, [onPasteImage])
1501
469
  useEffect(() => {
1502
- const handleWindowKeyDown = (event: globalThis.KeyboardEvent) => {
1503
- if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== 'v') return
1504
- window.setTimeout(() => void onReadClipboardImage(), 0)
470
+ const handleEscapeKeyDown = (event: globalThis.KeyboardEvent) => {
471
+ if (event.key === 'Escape' && sending) {
472
+ onCancelMessage()
473
+ }
1505
474
  }
1506
- window.addEventListener('keydown', handleWindowKeyDown, true)
1507
- return () => window.removeEventListener('keydown', handleWindowKeyDown, true)
1508
- }, [onReadClipboardImage])
475
+ window.addEventListener('keydown', handleEscapeKeyDown, true)
476
+ return () => window.removeEventListener('keydown', handleEscapeKeyDown, true)
477
+ }, [sending, onCancelMessage])
1509
478
  useEffect(() => {
1510
479
  if (message) return
1511
480
  const input = document.getElementById('chat-input') as HTMLTextAreaElement | null
@@ -1528,24 +497,72 @@ export default function ChatPanel({
1528
497
  const renderCompletedActivity = (interaction: any) => {
1529
498
  const interactionId = String(interaction.id)
1530
499
  const activityView = activitiesFrom(agentActivitySnapshots[interactionId] ?? [])
1531
- if (activityView.items.length === 0) return null
1532
500
  const isOpen = Boolean(openActivityIds[interactionId])
1533
501
  return (
1534
- <div className="agent-activity-history">
1535
- <button
1536
- type="button"
1537
- className="agent-activity-toggle"
1538
- aria-expanded={isOpen}
1539
- onClick={() => setOpenActivityIds((current) => ({ ...current, [interactionId]: !current[interactionId] }))}
1540
- >
1541
- <span>{activityView.summary}</span>
1542
- <span aria-hidden="true">{isOpen ? '^' : '>'}</span>
1543
- </button>
1544
- {isOpen && (
1545
- <div className="agent-activity-card agent-activity-card-history">
1546
- {renderAgentActivityTable(activityView)}
1547
- </div>
1548
- )}
502
+ <AgentActivityDrawer
503
+ activityView={activityView}
504
+ isOpen={isOpen}
505
+ onToggle={() => setOpenActivityIds((current) => ({ ...current, [interactionId]: !current[interactionId] }))}
506
+ isHistorical={true}
507
+ />
508
+ )
509
+ }
510
+
511
+ const renderWebSearchSources = (interaction: any) => {
512
+ const interactionId = String(interaction.id)
513
+ const progress = agentActivitySnapshots[interactionId] ?? interaction.agentActivity ?? []
514
+ const sources = parseWebSearchSources(progress)
515
+ if (sources.length === 0) return null
516
+ return (
517
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '6px', marginBottom: '8px' }}>
518
+ <span style={{ fontSize: '0.72rem', color: '#64748b', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.05em' }}>Sources</span>
519
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
520
+ {sources.map((src, i) => (
521
+ <a
522
+ key={i}
523
+ href={src.url}
524
+ target="_blank"
525
+ rel="noopener noreferrer"
526
+ title={src.snippet || src.title}
527
+ style={{
528
+ display: 'flex',
529
+ alignItems: 'center',
530
+ gap: '6px',
531
+ padding: '5px 10px',
532
+ background: 'rgba(255,255,255,0.04)',
533
+ border: '1px solid rgba(255,255,255,0.08)',
534
+ borderRadius: '8px',
535
+ textDecoration: 'none',
536
+ color: '#cbd5e1',
537
+ fontSize: '0.78rem',
538
+ maxWidth: '220px',
539
+ overflow: 'hidden',
540
+ cursor: 'pointer',
541
+ transition: 'background 0.15s, border-color 0.15s',
542
+ }}
543
+ onMouseEnter={e => {
544
+ (e.currentTarget as HTMLAnchorElement).style.background = 'rgba(255,255,255,0.08)'
545
+ ;(e.currentTarget as HTMLAnchorElement).style.borderColor = 'rgba(255,255,255,0.18)'
546
+ }}
547
+ onMouseLeave={e => {
548
+ (e.currentTarget as HTMLAnchorElement).style.background = 'rgba(255,255,255,0.04)'
549
+ ;(e.currentTarget as HTMLAnchorElement).style.borderColor = 'rgba(255,255,255,0.08)'
550
+ }}
551
+ >
552
+ <img
553
+ src={src.faviconUrl}
554
+ alt=""
555
+ width={14}
556
+ height={14}
557
+ style={{ borderRadius: '2px', flexShrink: 0 }}
558
+ onError={e => { (e.currentTarget as HTMLImageElement).style.display = 'none' }}
559
+ />
560
+ <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
561
+ {src.domain}
562
+ </span>
563
+ </a>
564
+ ))}
565
+ </div>
1549
566
  </div>
1550
567
  )
1551
568
  }
@@ -1842,6 +859,7 @@ export default function ChatPanel({
1842
859
  )
1843
860
  })()}
1844
861
  <div className="message-bubble" style={{ whiteSpace: 'pre-wrap' }}>{renderFormattedMessage(interaction.aiText)}</div>
862
+ {renderWebSearchSources(interaction)}
1845
863
  <div className="message-time" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
1846
864
  <button className="provider-badge">{interaction.provider} • {interaction.model}</button>
1847
865
  {fallbackNotice(interaction) && <span className="provider-fallback-notice">{fallbackNotice(interaction)}</span>}
@@ -1876,18 +894,13 @@ export default function ChatPanel({
1876
894
  {sending && (
1877
895
  <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' }}>
1878
896
  <div className="message user-message"><div className="bubble-wrapper"><div className="message-bubble">{sendingImageMarkers ? renderFormattedMessage(`${sendingMessage} ${sendingImageMarkers}`) : renderFormattedMessage(sendingMessage)}</div></div></div>
1879
- {agentMode && agentActivities.items.length > 0 && (
1880
- <div className="message ai-message agent-activity-message">
1881
- <div className="agent-activity-card">
1882
- <div className="agent-activity-header">
1883
- <span>{agentActivities.summary}</span>
1884
- <span className="agent-activity-status" data-state={pendingApproval ? 'approval' : 'active'}>
1885
- {pendingApproval ? 'Waiting for approval' : 'Working'}
1886
- </span>
1887
- </div>
1888
- {renderAgentActivityTable(agentActivities)}
1889
- </div>
1890
- </div>
897
+ {agentMode && (
898
+ <AgentActivityDrawer
899
+ activityView={agentActivities}
900
+ isOpen={openActivityIds['live'] ?? true}
901
+ onToggle={() => setOpenActivityIds((current) => ({ ...current, live: !(current['live'] ?? true) }))}
902
+ pendingApproval={!!pendingApproval}
903
+ />
1891
904
  )}
1892
905
  {renderActiveFileChanges()}
1893
906
  <div className="message ai-message thinking-message">
@@ -1924,7 +937,12 @@ export default function ChatPanel({
1924
937
  />
1925
938
  </path>
1926
939
  </svg>
1927
- <span>Thinking for {elapsedSeconds}s</span>
940
+ <span>
941
+ {activeAgentName && activeModelName
942
+ ? `${activeAgentName} (${activeModelName}) is thinking... (${elapsedSeconds}s)`
943
+ : `Thinking for ${elapsedSeconds}s (Esc to cancel)`
944
+ }
945
+ </span>
1928
946
  </div>
1929
947
  )}
1930
948
  </span>
@@ -1962,42 +980,17 @@ export default function ChatPanel({
1962
980
  </div>
1963
981
  )}
1964
982
 
1965
- {pendingApproval && (() => {
1966
- const details = renderApprovalDetails(pendingApproval.approval)
1967
- const writeFile = pendingApproval.approval?.WriteFile
1968
- const applyPatch = pendingApproval.approval?.ApplyPatch
1969
- const diffText = writeFile?.diff || applyPatch?.diff
1970
-
1971
- return (
1972
- <div className="message ai-message" style={{ width: '100%' }}>
1973
- <div className="bubble-wrapper" style={{ width: '100%' }}>
1974
- <div className="action-card approval-card" data-tier={details.isDangerous ? 'dangerous' : undefined} style={{ width: '100%' }}>
1975
- <div className="approval-card-content" style={{ width: '100%' }}>
1976
- <div className="approval-card-title">{details.title}</div>
1977
- <div className="approval-card-body" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.body}</div>
1978
- {diffText ? (
1979
- <div className="approval-card-diff-container" style={{ marginTop: '8px', width: '100%' }}>
1980
- <div style={{ fontSize: '0.8rem', fontWeight: 600, color: 'var(--text-soft, #94a3b8)', marginBottom: '4px' }}>Diff:</div>
1981
- {renderDiff(diffText)}
1982
- </div>
1983
- ) : (
1984
- details.reason && <div className="approval-card-reason" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.reason}</div>
1985
- )}
1986
- </div>
1987
- <div className="approval-card-actions">
1988
- <button type="button" className="approval-btn approval-btn-approve" onClick={() => onApproval(true)}>Approve</button>
1989
- <button type="button" className="approval-btn" style={{ backgroundColor: 'rgba(16, 185, 129, 0.22)', borderColor: 'rgba(16, 185, 129, 0.4)', color: '#a7f3d0' }} onClick={() => onApproval(true, true)}>Approve this session</button>
1990
- <button type="button" className="approval-btn approval-btn-cancel" onClick={() => onApproval(false)}>Cancel</button>
1991
- </div>
1992
- </div>
1993
- </div>
1994
- </div>
1995
- )
1996
- })()}
983
+ {pendingApproval && (
984
+ <ApprovalCard
985
+ key={pendingApproval.id || JSON.stringify(pendingApproval)}
986
+ pendingApproval={pendingApproval}
987
+ onApproval={onApproval}
988
+ />
989
+ )}
1997
990
  <div ref={chatEnd} />
1998
991
  </div>
1999
992
 
2000
- <div className="input-area">
993
+ <div className={`input-area ${voiceMode ? 'voice-active' : ''}`}>
2001
994
  {isEmptyChat && <div className="empty-chat-prompt">Mint Agent is ready to work</div>}
2002
995
  <div className="smart-context-bar">
2003
996
  <div className="smart-context-label" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
@@ -2011,8 +1004,21 @@ export default function ChatPanel({
2011
1004
  {voiceMode && (
2012
1005
  <div className="voice-mode-bar" data-state={voiceStatus}>
2013
1006
  <span className="voice-mode-dot" />
2014
- <span>{voiceStatusLabel}</span>
2015
- {voiceTranscript && <span className="voice-mode-transcript">{voiceTranscript}</span>}
1007
+ {voiceTranscript ? (
1008
+ voiceTranscript === 'Listening to microphone...' ||
1009
+ voiceTranscript === 'Listening...' ||
1010
+ voiceTranscript === 'No speech detected' ||
1011
+ voiceTranscript === 'Sent audio to AI' ? (
1012
+ <span>{voiceTranscript}</span>
1013
+ ) : (
1014
+ <span style={{ display: 'flex', alignItems: 'center', gap: '4px', minWidth: 0, overflow: 'hidden' }}>
1015
+ <span>{voiceStatusLabel}:</span>
1016
+ <span className="voice-mode-transcript">"{voiceTranscript}"</span>
1017
+ </span>
1018
+ )
1019
+ ) : (
1020
+ <span>{voiceStatusLabel}</span>
1021
+ )}
2016
1022
  </div>
2017
1023
  )}
2018
1024
 
@@ -2134,7 +1140,7 @@ export default function ChatPanel({
2134
1140
  return <option key={provider} value={provider}>{displayName}</option>
2135
1141
  })}
2136
1142
  </select>
2137
- {availableModels.length > 0 && (
1143
+ {(availableModels.length > 0 || activeModel) && (
2138
1144
  <select
2139
1145
  value={activeModel}
2140
1146
  onChange={(event) => onSetModel(event.target.value)}
@@ -2184,14 +1190,32 @@ export default function ChatPanel({
2184
1190
  </svg>
2185
1191
  )}
2186
1192
  </button>
2187
- <button id="send-btn" type="submit" disabled={sending || !canSubmit} style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
2188
- <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
2189
- <line x1="22" y1="2" x2="11" y2="13"></line>
2190
- <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
2191
- </svg>
2192
- </button>
1193
+ {sending ? (
1194
+ <button
1195
+ id="send-btn"
1196
+ className="stop-btn"
1197
+ type="button"
1198
+ onClick={onCancelMessage}
1199
+ title="Stop generating"
1200
+ style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}
1201
+ >
1202
+ <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
1203
+ <rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect>
1204
+ </svg>
1205
+ </button>
1206
+ ) : (
1207
+ <button id="send-btn" type="submit" disabled={!canSubmit} style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
1208
+ <svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
1209
+ <line x1="22" y1="2" x2="11" y2="13"></line>
1210
+ <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
1211
+ </svg>
1212
+ </button>
1213
+ )}
2193
1214
  </form>
2194
1215
  </div>
1216
+ <p className="input-disclaimer">
1217
+ Mint Agent is an AI gateway. Responses via third-party APIs. Verify critical info.
1218
+ </p>
2195
1219
  </section>
2196
1220
  )
2197
1221
  }