@pheem49/mint 1.8.1 → 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 (184) hide show
  1. package/Cargo.lock +3 -3
  2. package/Cargo.toml +1 -1
  3. package/README.md +0 -17
  4. package/Release_Note.md +92 -58
  5. package/bin/mint +0 -0
  6. package/crates/mint-cli/src/agent.rs +100 -6
  7. package/crates/mint-cli/src/main.rs +166 -52
  8. package/crates/mint-core/src/agent_loop.rs +1 -0
  9. package/crates/mint-core/src/api_server.rs +55 -2
  10. package/crates/mint-core/src/browser.rs +55 -0
  11. package/crates/mint-core/src/channels.rs +1 -0
  12. package/crates/mint-core/src/chat.rs +3 -0
  13. package/crates/mint-core/src/config.rs +61 -0
  14. package/crates/mint-core/src/lib.rs +14 -0
  15. package/crates/mint-core/src/orchestration.rs +180 -11
  16. package/crates/mint-core/src/tts.rs +3 -1
  17. package/flatpak/com.pheem49.mint.desktop +9 -0
  18. package/flatpak/com.pheem49.mint.metainfo.xml +35 -0
  19. package/flatpak/com.pheem49.mint.yaml +52 -0
  20. package/package.json +1 -1
  21. package/shared-modules/.github/workflows/ci.yml +120 -0
  22. package/shared-modules/.github/workflows/update.yaml +28 -0
  23. package/shared-modules/CODEOWNERS +23 -0
  24. package/shared-modules/README.md +79 -0
  25. package/shared-modules/SDL/README.md +3 -0
  26. package/shared-modules/SDL/SDL-1.2.15.json +51 -0
  27. package/shared-modules/SDL/SDL_Pango-0.1.2-API-adds.patch +118 -0
  28. package/shared-modules/SDL/SDL_image-1.2.12.json +25 -0
  29. package/shared-modules/SDL/SDL_mixer-1.2.12.json +27 -0
  30. package/shared-modules/SDL/SDL_net-1.2.8.json +19 -0
  31. package/shared-modules/SDL/SDL_pango-0.1.2.json +26 -0
  32. package/shared-modules/SDL/SDL_ttf-2.0.11.json +28 -0
  33. package/shared-modules/SDL/libsdl-1.2.15-strict-prototypes.patch +316 -0
  34. package/shared-modules/SDL/sdl-check-for-SDL_VIDEO_X11_BACKINGSTORE.patch +24 -0
  35. package/shared-modules/SDL/sdl-libx11-build.patch +59 -0
  36. package/shared-modules/SDL/sdl-sysloadso-buffer-length.patch +27 -0
  37. package/shared-modules/SDL/sdl12-compat.json +22 -0
  38. package/shared-modules/SDL2/SDL2_image.json +27 -0
  39. package/shared-modules/SDL2/SDL2_ttf.json +29 -0
  40. package/shared-modules/cld2/CMakeLists.txt +157 -0
  41. package/shared-modules/cld2/cld2.json +24 -0
  42. package/shared-modules/clutter/clutter.json +72 -0
  43. package/shared-modules/dbus-glib/dbus-glib.json +29 -0
  44. package/shared-modules/glew/glew.json +36 -0
  45. package/shared-modules/glu/glu-9.json +12 -0
  46. package/shared-modules/gtk2/arc-gtk2-theme-Replace-Inkscape-dependency-with-rsvg-convert.patch +180 -0
  47. package/shared-modules/gtk2/gtk2-common-themes.json +360 -0
  48. package/shared-modules/gtk2/gtk2-fix-crash-in-show-uri.patch +30 -0
  49. package/shared-modules/gtk2/gtk2-gcc14.patch +281 -0
  50. package/shared-modules/gtk2/gtk2-gcc15.patch +250 -0
  51. package/shared-modules/gtk2/gtk2-murrine-c99.patch +34 -0
  52. package/shared-modules/gtk2/gtk2-use-adwaita-theme.patch +80 -0
  53. package/shared-modules/gtk2/gtk2.json +120 -0
  54. package/shared-modules/gtk2/ibus-gcc15.patch +13 -0
  55. package/shared-modules/gtk2/murrine-engine-fix-crash.patch +12 -0
  56. package/shared-modules/gtk2/readme.md +10 -0
  57. package/shared-modules/intltool/intltool-0.51.json +20 -0
  58. package/shared-modules/intltool/intltool-perl5.26-regex-fixes.patch +59 -0
  59. package/shared-modules/lame/0001-Build-xmm_quantize_sub.c-with-msse.patch +34 -0
  60. package/shared-modules/lame/0002-Include-GTK-1-autoconf-directives-in-build-system.patch +222 -0
  61. package/shared-modules/lame/0003-Patch-out-remaining-ansi2knr.patch +70 -0
  62. package/shared-modules/lame/0004-initscr-is-not-used-anywhere-in-lame-sourcetree.patch +41 -0
  63. package/shared-modules/lame/0005-Update-autoconf.patch +81 -0
  64. package/shared-modules/lame/lame-3.99.5.json +32 -0
  65. package/shared-modules/libappindicator/libappindicator-gtk2-12.10.json +140 -0
  66. package/shared-modules/libappindicator/libappindicator-gtk3-12.10.json +140 -0
  67. package/shared-modules/libappindicator/libappindicator-gtk3-introspection-12.10.json +140 -0
  68. package/shared-modules/libappindicator/patches/0001-libappindicator-ftbfs.patch +20 -0
  69. package/shared-modules/libappindicator/patches/0002-libappindicator-no-python.patch +888 -0
  70. package/shared-modules/libappindicator/patches/0003-libappindicator-fix-crash-from-incorrect-signal-emission.patch +74 -0
  71. package/shared-modules/libappindicator/patches/0004-libappindicator-fix-crash-iterating-icon-themes.patch +11 -0
  72. package/shared-modules/libayatana-appindicator/0001-Fix-HAVE_VALGRIND-AM_CONDITIONAL.patch +51 -0
  73. package/shared-modules/libayatana-appindicator/0001-Make-introspection-configurable.patch +44 -0
  74. package/shared-modules/libayatana-appindicator/libayatana-appindicator-gtk3.json +121 -0
  75. package/shared-modules/libcanberra/0001-gtk-Don-t-assume-all-GdkDisplays-are-GdkX11Displays-.patch +123 -0
  76. package/shared-modules/libcanberra/libcanberra.json +44 -0
  77. package/shared-modules/libmad/libmad.json +19 -0
  78. package/shared-modules/libsecret/libsecret.json +23 -0
  79. package/shared-modules/libsoup/libsoup-2.4.json +14 -0
  80. package/shared-modules/libusb/libusb.json +27 -0
  81. package/shared-modules/linux-audio/dssi.json +31 -0
  82. package/shared-modules/linux-audio/fftw3f-static.json +44 -0
  83. package/shared-modules/linux-audio/fftw3f.json +42 -0
  84. package/shared-modules/linux-audio/fluidsynth2-static.json +30 -0
  85. package/shared-modules/linux-audio/fluidsynth2.json +19 -0
  86. package/shared-modules/linux-audio/ladspa.json +26 -0
  87. package/shared-modules/linux-audio/lash.json +35 -0
  88. package/shared-modules/linux-audio/libinstpatch.json +16 -0
  89. package/shared-modules/linux-audio/liblo-static.json +22 -0
  90. package/shared-modules/linux-audio/liblo.json +17 -0
  91. package/shared-modules/linux-audio/lilv-static.json +94 -0
  92. package/shared-modules/linux-audio/lilv.json +82 -0
  93. package/shared-modules/linux-audio/lrdf.json +57 -0
  94. package/shared-modules/linux-audio/lv2.json +21 -0
  95. package/shared-modules/linux-audio/patches/lash/0001-configure-remove-aclocal-m4.patch +29 -0
  96. package/shared-modules/linux-audio/patches/lash/0002-Fix-build-with-recent-gcc.patch +73 -0
  97. package/shared-modules/linux-audio/patches/lash/0003-Fix-linking.patch +136 -0
  98. package/shared-modules/linux-audio/patches/raptor-libxml.patch +31 -0
  99. package/shared-modules/linux-audio/patches/stk-install.sh +54 -0
  100. package/shared-modules/linux-audio/stk.json +28 -0
  101. package/shared-modules/lua5.1/0004-Fix-stack-overflow-in-vararg-functions.patch +21 -0
  102. package/shared-modules/lua5.1/lua-5.1.5-so.patch +43 -0
  103. package/shared-modules/lua5.1/lua-5.1.5.json +42 -0
  104. package/shared-modules/lua5.3/lua-5.3.5.json +41 -0
  105. package/shared-modules/lua5.3/lua.pc.in +8 -0
  106. package/shared-modules/lua5.3/luaconf.patch +47 -0
  107. package/shared-modules/lua5.3/makefile-add-liblua.so-rule.patch +47 -0
  108. package/shared-modules/lua5.4/lua-5.4.json +41 -0
  109. package/shared-modules/lua5.4/lua.pc.in +8 -0
  110. package/shared-modules/lua5.4/luaconf.patch +34 -0
  111. package/shared-modules/lua5.4/makefile-add-liblua.so-rule.patch +42 -0
  112. package/shared-modules/luajit/luajit.json +30 -0
  113. package/shared-modules/lzo/lzo.json +27 -0
  114. package/shared-modules/mac/mac.json +18 -0
  115. package/shared-modules/physfs/physfs.json +26 -0
  116. package/shared-modules/pygame/audiofile-gcc6.patch +21 -0
  117. package/shared-modules/pygame/cython-fixes.patch +34 -0
  118. package/shared-modules/pygame/fluidsynth-no-rawmidi.patch +69 -0
  119. package/shared-modules/pygame/glib.patch +25 -0
  120. package/shared-modules/pygame/pygame1-python2.json +157 -0
  121. package/shared-modules/pygtk/pygtk-c99.patch +15 -0
  122. package/shared-modules/pygtk/pygtk.json +117 -0
  123. package/shared-modules/pygtk/pygtk.patch +39 -0
  124. package/shared-modules/python2.7/python-2.7.json +48 -0
  125. package/shared-modules/smpeg/smpeg-0.4.5.json +50 -0
  126. package/shared-modules/smpeg/smpeg-am-prog-as.patch +12 -0
  127. package/shared-modules/smpeg/smpeg-export-mpegaudio-class.patch +17 -0
  128. package/shared-modules/smpeg/smpeg-gcc6.patch +40 -0
  129. package/shared-modules/smpeg/smpeg-no-gtk.patch +37 -0
  130. package/shared-modules/squashfs-tools/squashfs-tools.json +25 -0
  131. package/shared-modules/update-checker.sh +50 -0
  132. package/shared-modules/vorbisgain/0001-temp_files.patch +75 -0
  133. package/shared-modules/vorbisgain/0002-errno.patch +33 -0
  134. package/shared-modules/vorbisgain/0003-manpage.patch +248 -0
  135. package/shared-modules/vorbisgain/0004-vorbisgain_mtime.patch +183 -0
  136. package/shared-modules/vorbisgain/0005-double_fclose.patch +21 -0
  137. package/shared-modules/vorbisgain/0006-manpage_hyphens.patch +56 -0
  138. package/shared-modules/vorbisgain/0007-recursively_spelling.patch +14 -0
  139. package/shared-modules/vorbisgain/0008-manpage_recursion_mistake.patch +23 -0
  140. package/shared-modules/vorbisgain/0009-hardening.patch +13 -0
  141. package/shared-modules/vorbisgain/0010-fclose.patch +13 -0
  142. package/shared-modules/vorbisgain/README.md +23 -0
  143. package/shared-modules/vorbisgain/vorbisgain_0.37-2.json +29 -0
  144. package/src/renderer/shared/agentProgress.ts +145 -0
  145. package/src/renderer/shared/calculator.ts +52 -0
  146. package/src/renderer/shared/components/AgentActivityDrawer.tsx +72 -0
  147. package/src/renderer/shared/components/AgentActivityTable.tsx +66 -0
  148. package/src/renderer/shared/components/ApprovalCard.tsx +93 -0
  149. package/src/renderer/shared/components/ChatCodeBlock.tsx +75 -0
  150. package/src/renderer/shared/components/ThinkingBlock.tsx +101 -0
  151. package/src/renderer/shared/constants/models.ts +91 -0
  152. package/src/renderer/shared/platform.ts +82 -0
  153. package/src/renderer/shared/types.ts +140 -0
  154. package/src/renderer/shared/utils/agentActivity.ts +187 -0
  155. package/src/renderer/shared/utils/approval.ts +1 -0
  156. package/src/renderer/shared/utils/approval.tsx +64 -0
  157. package/src/renderer/shared/utils/markdown.tsx +347 -0
  158. package/src/renderer/shared/utils/providers.ts +36 -0
  159. package/src/renderer/shared/utils/speech.ts +216 -0
  160. package/src/renderer/shared/utils/ui.ts +147 -0
  161. package/src/renderer/src/agentProgress.ts +11 -39
  162. package/src/renderer/src/calculator.ts +3 -46
  163. package/src/renderer/src/components/ChatPanel.tsx +194 -1303
  164. package/src/renderer/src/components/MintDashboard.tsx +23 -169
  165. package/src/renderer/src/components/PicturesLibrary.tsx +32 -8
  166. package/src/renderer/src/components/Settings/AgentsTab.tsx +362 -0
  167. package/src/renderer/src/components/Settings/AutomationTab.tsx +0 -22
  168. package/src/renderer/src/components/SettingsWindow.tsx +53 -3
  169. package/src/renderer/src/css/styles.css +84 -0
  170. package/src/renderer/src/tauri.ts +128 -143
  171. package/src/renderer/src-web/agentProgress.ts +11 -39
  172. package/src/renderer/src-web/calculator.ts +3 -46
  173. package/src/renderer/src-web/components/ChatPanel.tsx +194 -1301
  174. package/src/renderer/src-web/components/MintDashboard.tsx +23 -169
  175. package/src/renderer/src-web/components/PicturesLibrary.tsx +23 -29
  176. package/src/renderer/src-web/components/Settings/AgentsTab.tsx +362 -0
  177. package/src/renderer/src-web/components/Settings/AutomationTab.tsx +0 -22
  178. package/src/renderer/src-web/components/SettingsWindow.tsx +53 -3
  179. package/src/renderer/src-web/css/styles.css +84 -0
  180. package/src/renderer/src-web/tauri.ts +180 -102
  181. package/src-tauri/src/lib.rs +137 -33
  182. package/src-tauri/src/webhooks.rs +1 -0
  183. package/src-tauri/src/workflows.rs +1 -0
  184. 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,364 +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: string[] = []
72
-
73
- interface ApprovalDetails {
74
- title: string
75
- body: string
76
- reason?: string
77
- isDangerous: boolean
78
- }
79
-
80
- function badge(provider: string, model: string) {
81
- return [provider, model].filter(Boolean).join(' / ')
82
- }
83
-
84
- function providerLabel(provider: string) {
85
- switch (provider) {
86
- case 'gemini':
87
- return 'Gemini'
88
- case 'openai':
89
- return 'OpenAI'
90
- case 'openrouter':
91
- return 'OpenRouter'
92
- case 'deepseek':
93
- return 'DeepSeek'
94
- case 'anthropic':
95
- return 'Claude'
96
- case 'huggingface':
97
- return 'Hugging Face'
98
- case 'local_openai':
99
- return 'Local OpenAI'
100
- case 'ollama':
101
- return 'Ollama'
102
- default:
103
- return provider || 'Primary provider'
104
- }
105
- }
106
-
107
- function fallbackNotice(response: Pick<ChatResponse, 'provider' | 'fallbackProvider'> | null | undefined) {
108
- if (!response?.fallbackProvider) return ''
109
- return `${providerLabel(response.fallbackProvider)} unavailable, fell back to ${providerLabel(response.provider)}.`
110
- }
111
-
112
- interface AgentActivity {
113
- label: string
114
- target: string
115
- kind: 'file' | 'folder' | 'search' | 'terminal' | 'tool'
116
- state: 'active' | 'done' | 'error'
117
- action?: string
118
- }
119
-
120
- interface AgentActivityView {
121
- summary: string
122
- items: AgentActivity[]
123
- }
124
-
125
- function activityDetail(input: Record<string, unknown>, key: string) {
126
- const value = input[key]
127
- return typeof value === 'string' && value.trim() ? value : ''
128
- }
129
-
130
- function formatActivityTarget(value: string) {
131
- const compact = value.replace(/^\/home\/([^/]+)/, '~')
132
- return compact || 'workspace'
133
- }
134
-
135
- function activityKind(action: string, target: string): AgentActivity['kind'] {
136
- if (['search_code', 'semantic_search', 'knowledge_search', 'web_search', 'memory_recall'].includes(action)) return 'search'
137
- if (['run_shell', 'verify'].includes(action)) return 'terminal'
138
- if (['list_files', 'detect_project'].includes(action)) return 'folder'
139
- if (['read_file', 'symbols', 'read_diagnostics', 'git_diff', 'apply_patch', 'write_file', 'note_write', 'view_image'].includes(action)) return 'file'
140
- return target.includes('/') && !/\.[^/]+$/.test(target) ? 'folder' : 'tool'
141
- }
142
-
143
- function describeTool(action: string, input: Record<string, unknown>): AgentActivity {
144
- const path = activityDetail(input, 'path')
145
- const query = activityDetail(input, 'query')
146
- const command = activityDetail(input, 'command')
147
- const name = activityDetail(input, 'name')
148
- const tool = activityDetail(input, 'tool')
149
- const rawTarget = path || query || command || name || tool || action.replaceAll('_', ' ')
150
-
151
- // Append line range for read_file when startLine / endLine are available
152
- let target = rawTarget
153
- if (action === 'read_file' && path) {
154
- const startLine = typeof input.startLine === 'number' ? input.startLine : undefined
155
- const endLine = typeof input.endLine === 'number' ? input.endLine : undefined
156
- if (startLine !== undefined && endLine !== undefined) {
157
- target = `${rawTarget} #L${startLine}-${endLine}`
158
- } else if (startLine !== undefined) {
159
- target = `${rawTarget} #L${startLine}`
160
- }
161
- }
162
-
163
- const labels: Record<string, string> = {
164
- apply_patch: 'Applying patch',
165
- ask_user: 'Asking user',
166
- create_plan: 'Creating plan',
167
- detect_project: 'Detecting project',
168
- git_branch: 'Reading branch',
169
- git_diff: 'Reading diff',
170
- git_log: 'Reading log',
171
- git_status: 'Reading git status',
172
- knowledge_search: 'Searching knowledge',
173
- list_files: 'Listing files',
174
- list_tests: 'Listing tests',
175
- mcp_tool: 'Calling MCP tool',
176
- memory_recall: 'Recalling memory',
177
- note_write: 'Writing note',
178
- read_diagnostics: 'Reading diagnostics',
179
- read_file: 'Reading file',
180
- request_user_approval: 'Requesting approval',
181
- run_plugin: 'Running plugin',
182
- run_shell: 'Running command',
183
- search_code: 'Searching code',
184
- semantic_index: 'Indexing code',
185
- semantic_search: 'Searching code',
186
- symbols: 'Inspecting symbols',
187
- update_plan: 'Updating plan',
188
- verify: 'Verifying',
189
- view_image: 'Viewing image',
190
- web_search: 'Searching web',
191
- write_file: 'Writing file',
192
- }
193
- return {
194
- label: labels[action] ?? 'Using tool',
195
- target: formatActivityTarget(target),
196
- kind: activityKind(action, target),
197
- state: 'active',
198
- action,
199
- }
200
- }
201
-
202
- function activitySummary(items: AgentActivity[]) {
203
- const files = new Set<string>()
204
- const folders = new Set<string>()
205
- for (const item of items) {
206
- if (item.kind === 'file') files.add(item.target)
207
- if (item.kind === 'folder') folders.add(item.target)
208
- }
209
- const parts = [
210
- files.size ? `${files.size} ${files.size === 1 ? 'file' : 'files'}` : '',
211
- folders.size ? `${folders.size} ${folders.size === 1 ? 'folder' : 'folders'}` : '',
212
- ].filter(Boolean)
213
- return parts.length ? `Exploring ${parts.join(', ')}` : 'Working through task'
214
- }
215
-
216
- function activitiesFrom(progress: AgentProgress[]): AgentActivityView {
217
- const activities: AgentActivity[] = []
218
- for (const event of progress) {
219
- if (event.type === 'ToolStart') {
220
- activities.push(describeTool(event.data.action, event.data.input))
221
- } else if (event.type === 'ToolEnd') {
222
- for (let index = activities.length - 1; index >= 0; index -= 1) {
223
- if (activities[index].state !== 'active') continue
224
- activities[index].state = event.data.result.startsWith('Error:') ? 'error' : 'done'
225
- if (activities[index].state === 'error') {
226
- activities[index].label = 'Failed'
227
- }
228
- if (activities[index].action === 'ask_user' && event.data.result.startsWith('User answered:')) {
229
- const answer = event.data.result.replace('User answered:', '').trim()
230
- activities[index].target = `(Answered: "${answer}") ${activities[index].target}`
231
- }
232
- break
233
- }
234
- }
235
- }
236
- const items = activities.slice(-12)
237
- return { summary: activitySummary(activities), items }
238
- }
239
-
240
- function AgentActivityTable({ activityView }: { activityView: AgentActivityView }) {
241
- const [expandedIndices, setExpandedIndices] = useState<Record<number, boolean>>({})
242
-
243
- const toggleExpand = (index: number) => {
244
- setExpandedIndices(prev => ({
245
- ...prev,
246
- [index]: !prev[index]
247
- }))
248
- }
249
-
250
- return (
251
- <div className="agent-activity-list">
252
- <div className="agent-activity-table-head" aria-hidden="true">
253
- <span>Tool</span>
254
- <span />
255
- <span>Target</span>
256
- <span />
257
- </div>
258
- {activityView.items.map((activity, index) => {
259
- const isExpanded = !!expandedIndices[index]
260
- return (
261
- <div
262
- className="agent-activity-item"
263
- data-kind={activity.kind}
264
- data-state={activity.state}
265
- key={`${index}-${activity.label}-${activity.target}`}
266
- style={{ cursor: 'pointer' }}
267
- onClick={() => toggleExpand(index)}
268
- >
269
- <span className="agent-activity-label">{activity.label}</span>
270
- <span className="agent-activity-icon" aria-hidden="true" />
271
- <span
272
- className="agent-activity-text"
273
- style={isExpanded ? { whiteSpace: 'normal', wordBreak: 'break-all', overflow: 'visible', textOverflow: 'clip' } : undefined}
274
- >
275
- {activity.target}
276
- </span>
277
- <span
278
- className="agent-activity-chevron"
279
- style={{
280
- transform: isExpanded ? 'rotate(90deg)' : 'none',
281
- transition: 'transform 0.2s ease',
282
- display: 'inline-block'
283
- }}
284
- aria-hidden="true"
285
- >
286
- &gt;
287
- </span>
288
- </div>
289
- )
290
- })}
291
- </div>
292
- )
293
- }
294
-
295
- function renderApprovalDetails(approval: any): ApprovalDetails {
296
- if (!approval) return { title: 'Action Pending Approval', body: 'No action details available.', isDangerous: false }
297
- 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 }
298
- 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 }
299
- if (approval.RunShell) return { title: 'Run Shell Command', body: approval.RunShell.command, reason: 'Executing shell commands can modify your system.', isDangerous: true }
300
- if (approval.NoteWrite) return { title: 'Write Note', body: `Path: ${approval.NoteWrite.path}`, reason: 'Creating or updating workspace notes.', isDangerous: false }
301
- if (approval.RunPlugin) return { title: `Run Plugin: ${approval.RunPlugin.name}`, body: approval.RunPlugin.instruction, reason: 'Executing a native plugin action.', isDangerous: false }
302
- if (approval.McpTool) {
303
- const { server, tool, arguments: args } = approval.McpTool
304
- return { title: `Run MCP Tool: ${server}/${tool}`, body: typeof args === 'string' ? args : JSON.stringify(args, null, 2), reason: 'Running external MCP tool.', isDangerous: false }
305
- }
306
- if (approval.UserApproval) return { title: approval.UserApproval.title, body: approval.UserApproval.prompt, reason: 'The agent requested explicit approval.', isDangerous: false }
307
- if (approval.AskUser) return { title: 'Question From Agent', body: approval.AskUser.question, reason: 'Type your answer below and submit to respond to the agent.', isDangerous: false }
308
- return { title: 'Unknown Action', body: JSON.stringify(approval, null, 2), reason: 'Requires approval to proceed.', isDangerous: false }
309
- }
310
-
311
- function renderDiff(diffText: string) {
312
- if (!diffText) return null
313
- const lines = diffText.split('\n')
314
- return (
315
- <div style={{
316
- background: '#0b0f19',
317
- borderRadius: '6px',
318
- padding: '8px',
319
- border: '1px solid rgba(255, 255, 255, 0.08)',
320
- overflowX: 'auto',
321
- maxHeight: '400px',
322
- fontFamily: 'monospace',
323
- fontSize: '0.74rem',
324
- lineHeight: '1.4',
325
- }}>
326
- {lines.map((line, idx) => {
327
- let style: any = {
328
- whiteSpace: 'pre-wrap',
329
- padding: '2px 6px',
330
- }
331
-
332
- if (line.startsWith('@@') || line.startsWith('---') || line.startsWith('+++')) {
333
- style = {
334
- ...style,
335
- color: '#64748b',
336
- fontWeight: 'bold',
337
- }
338
- } else if (line.startsWith('+')) {
339
- style = {
340
- ...style,
341
- background: 'rgba(16, 185, 129, 0.12)',
342
- borderLeft: '3px solid #10b981',
343
- color: '#a7f3d0'
344
- }
345
- } else if (line.startsWith('-')) {
346
- style = {
347
- ...style,
348
- background: 'rgba(239, 68, 68, 0.12)',
349
- borderLeft: '3px solid #ef4444',
350
- color: '#fca5a5'
351
- }
352
- } else {
353
- style = {
354
- ...style,
355
- color: '#e2e8f0',
356
- }
357
- }
358
-
359
- return (
360
- <div key={idx} style={style}>
361
- {line}
362
- </div>
363
- )
364
- })}
365
- </div>
366
- )
367
- }
368
35
 
369
36
  interface ChatPanelProps {
370
37
  interactions: any[]
@@ -390,7 +57,6 @@ interface ChatPanelProps {
390
57
  onSelectImage: (event: ChangeEvent<HTMLInputElement>) => void
391
58
  onSelectDocument: (event: ChangeEvent<HTMLInputElement>) => void
392
59
  onPasteImage: (clipboardData: DataTransfer) => boolean
393
- onReadClipboardImage: () => Promise<boolean>
394
60
  onSetMessage: (message: string) => void
395
61
  onSendVoiceMessage: (message: string, audioDataUri?: string | null) => Promise<void>
396
62
  onRemoveImage: (idx: number) => void
@@ -404,587 +70,9 @@ interface ChatPanelProps {
404
70
  onToggleMobileSidebar: () => void
405
71
  settingsConfig: any
406
72
  onSetModel: (model: string) => void
73
+ onCancelMessage: () => void
407
74
  }
408
75
 
409
- function ChatCodeBlock({ code, language }: { code: string; language: string; key?: any }) {
410
- const [copied, setCopied] = useState(false)
411
-
412
- const handleCopy = async () => {
413
- try {
414
- await navigator.clipboard.writeText(code)
415
- setCopied(true)
416
- setTimeout(() => setCopied(false), 2000)
417
- } catch (err) {
418
- console.error('Failed to copy code: ', err)
419
- }
420
- }
421
-
422
- const handleDownload = () => {
423
- try {
424
- const blob = new Blob([code], { type: 'text/plain;charset=utf-8' })
425
- const url = URL.createObjectURL(blob)
426
- const link = document.createElement('a')
427
- link.href = url
428
- link.download = `code.${language === 'plaintext' ? 'txt' : language}`
429
- link.click()
430
- URL.revokeObjectURL(url)
431
- } catch (err) {
432
- console.error('Failed to download code: ', err)
433
- }
434
- }
435
-
436
- const displayLang = language.charAt(0).toUpperCase() + language.slice(1).toLowerCase()
437
-
438
- return (
439
- <div className="chat-code-block-container" style={{ whiteSpace: 'normal' }}>
440
- <div className="chat-code-block-header">
441
- <span className="chat-code-block-lang">{displayLang}</span>
442
- <div className="chat-code-block-actions">
443
- <button type="button" onClick={handleDownload} title="Download code" className="chat-code-action-btn">
444
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
445
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
446
- <polyline points="7 10 12 15 17 10" />
447
- <line x1="12" y1="15" x2="12" y2="3" />
448
- </svg>
449
- </button>
450
- <button type="button" onClick={handleCopy} title="Copy code" className="chat-code-action-btn">
451
- {copied ? (
452
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
453
- <polyline points="20 6 9 17 4 12" />
454
- </svg>
455
- ) : (
456
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
457
- <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
458
- <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
459
- </svg>
460
- )}
461
- </button>
462
- </div>
463
- </div>
464
- <pre className="chat-code-block-body">
465
- <code>{code}</code>
466
- </pre>
467
- </div>
468
- )
469
- }
470
-
471
- const isTableLine = (line: string): boolean => {
472
- const trimmed = line.trim()
473
- return trimmed.startsWith('|') && trimmed.endsWith('|') && trimmed.length > 1
474
- }
475
-
476
- function renderFormattedMessage(text: string) {
477
- const displayText = readableAssistantText(text)
478
- if (!displayText) return null
479
-
480
- const formatInline = (str: string) => {
481
- const codeParts = str.split(/`([\s\S]*?)`/g)
482
- return codeParts.map((codePart, codeIndex) => {
483
- if (codeIndex % 2 === 1) {
484
- return (
485
- <code key={`code-${codeIndex}`} className="chat-inline-code">
486
- {codePart}
487
- </code>
488
- )
489
- }
490
- const boldParts = codePart.split(/\*\*([\s\S]*?)\*\*/g)
491
- return boldParts.map((boldPart, boldIndex) => {
492
- if (boldIndex % 2 === 1) {
493
- return (
494
- <strong key={`bold-${boldIndex}`} className="chat-bold-highlight">
495
- {boldPart}
496
- </strong>
497
- )
498
- }
499
- const mentionParts = boldPart.split(/(@[\w\-\.\/]+)/g)
500
- return mentionParts.map((mentionPart, mentionIndex) => {
501
- if (mentionIndex % 2 === 1) {
502
- return (
503
- <span key={`mention-${mentionIndex}`} className="chat-mention">
504
- {mentionPart}
505
- </span>
506
- )
507
- }
508
- return mentionPart
509
- })
510
- })
511
- })
512
- }
513
-
514
- const renderHtmlTable = (tableLines: string[], key: string) => {
515
- const rows: string[][] = []
516
- for (const line of tableLines) {
517
- const trimmed = line.trim()
518
- if (!trimmed) continue
519
- const content = trimmed.slice(1, -1)
520
- const contentEscaped = content.replace(/\\\|/g, '\u0000')
521
- const cols = contentEscaped.split('|').map(s => s.replace(/\u0000/g, '|').trim())
522
- rows.push(cols)
523
- }
524
-
525
- if (rows.length === 0) return null
526
-
527
- let hasSeparator = false
528
- let separatorIdx = -1
529
- for (let i = 0; i < rows.length; i++) {
530
- const row = rows[i]
531
- if (row.length > 0 && row.every(col => /^[-\s:]+$/.test(col))) {
532
- hasSeparator = true
533
- separatorIdx = i
534
- break
535
- }
536
- }
537
-
538
- let headerRow: string[] | null = null
539
- const dataRows: string[][] = []
540
-
541
- if (hasSeparator) {
542
- if (separatorIdx > 0) {
543
- headerRow = rows[separatorIdx - 1]
544
- for (let i = 0; i < rows.length; i++) {
545
- if (i !== separatorIdx && i !== separatorIdx - 1) {
546
- dataRows.push(rows[i])
547
- }
548
- }
549
- } else {
550
- for (let i = 0; i < rows.length; i++) {
551
- if (i !== separatorIdx) {
552
- dataRows.push(rows[i])
553
- }
554
- }
555
- }
556
- } else {
557
- headerRow = rows[0]
558
- for (let i = 1; i < rows.length; i++) {
559
- dataRows.push(rows[i])
560
- }
561
- }
562
-
563
- return (
564
- <div key={key} className="chat-table-container" style={{
565
- overflowX: 'auto',
566
- margin: '14px 0',
567
- width: '100%',
568
- borderRadius: '8px',
569
- border: '1px solid rgba(255, 255, 255, 0.12)',
570
- background: 'rgba(30, 41, 59, 0.35)',
571
- boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
572
- }}>
573
- <table className="chat-table" style={{
574
- width: '100%',
575
- borderCollapse: 'collapse',
576
- fontSize: '0.86rem',
577
- textAlign: 'left',
578
- lineHeight: '1.5',
579
- }}>
580
- {headerRow && (
581
- <thead>
582
- <tr style={{
583
- background: 'rgba(255, 255, 255, 0.04)',
584
- borderBottom: '2px solid rgba(255, 255, 255, 0.15)',
585
- }}>
586
- {headerRow.map((col, idx) => (
587
- <th key={`th-${idx}`} style={{
588
- padding: '12px 16px',
589
- fontWeight: 700,
590
- color: 'var(--accent, #38bdf8)',
591
- fontFamily: 'ui-sans-serif, system-ui, sans-serif',
592
- }}>
593
- {formatInline(col)}
594
- </th>
595
- ))}
596
- </tr>
597
- </thead>
598
- )}
599
- <tbody>
600
- {dataRows.map((row, rIdx) => (
601
- <tr key={`tr-${rIdx}`} style={{
602
- background: rIdx % 2 === 1 ? 'rgba(255, 255, 255, 0.015)' : 'transparent',
603
- borderBottom: rIdx < dataRows.length - 1 ? '1px solid rgba(255, 255, 255, 0.08)' : 'none',
604
- }}>
605
- {row.map((col, cIdx) => (
606
- <td key={`td-${cIdx}`} style={{
607
- padding: '12px 16px',
608
- color: 'var(--text-main, #e2e8f0)',
609
- }}>
610
- {formatInline(col)}
611
- </td>
612
- ))}
613
- </tr>
614
- ))}
615
- </tbody>
616
- </table>
617
- </div>
618
- )
619
- }
620
-
621
- const lines = displayText.split('\n')
622
- const items: any[] = []
623
-
624
- let inCodeBlock = false
625
- let codeBlockLang = ''
626
- let codeBlockLines: string[] = []
627
-
628
- let inTable = false
629
- let tableLines: string[] = []
630
-
631
- const flushTable = (index: number) => {
632
- if (tableLines.length > 0) {
633
- items.push(renderHtmlTable(tableLines, `table-${index}`))
634
- tableLines = []
635
- inTable = false
636
- }
637
- }
638
-
639
- for (let i = 0; i < lines.length; i++) {
640
- const line = lines[i]
641
-
642
- if (line.trim().startsWith('```')) {
643
- if (inTable) {
644
- flushTable(i)
645
- }
646
- if (inCodeBlock) {
647
- const codeText = codeBlockLines.join('\n')
648
- items.push(
649
- <ChatCodeBlock
650
- key={`code-block-${i}`}
651
- code={codeText}
652
- language={codeBlockLang}
653
- />
654
- )
655
- inCodeBlock = false
656
- codeBlockLines = []
657
- } else {
658
- inCodeBlock = true
659
- codeBlockLang = line.trim().slice(3).trim() || 'plaintext'
660
- }
661
- continue
662
- }
663
-
664
- if (inCodeBlock) {
665
- codeBlockLines.push(line)
666
- continue
667
- }
668
-
669
- if (isTableLine(line)) {
670
- inTable = true
671
- tableLines.push(line)
672
- } else {
673
- if (inTable) {
674
- flushTable(i)
675
- }
676
-
677
- const headerMatch = line.match(/^(#{1,6})\s+(.*)$/)
678
- if (headerMatch) {
679
- const level = headerMatch[1].length
680
- const content = headerMatch[2]
681
-
682
- const style = {
683
- fontWeight: 'bold',
684
- display: 'block',
685
- marginTop: level === 1 ? '16px' : level === 2 ? '14px' : '10px',
686
- marginBottom: '6px',
687
- fontSize: level === 1 ? '1.25em' : level === 2 ? '1.15em' : '1.05em',
688
- color: 'var(--text-main)',
689
- }
690
-
691
- items.push(
692
- <span key={`line-${i}`} style={style}>
693
- {formatInline(content)}
694
- </span>
695
- )
696
- } else {
697
- const listMatch = line.match(/^(\s*)([-*+])\s+(.*)$/)
698
- if (listMatch) {
699
- const indent = listMatch[1]
700
- const content = listMatch[3]
701
- items.push(
702
- <Fragment key={`line-${i}`}>
703
- {indent}• {formatInline(content)}
704
- {i < lines.length - 1 && '\n'}
705
- </Fragment>
706
- )
707
- } else {
708
- items.push(
709
- <Fragment key={`line-${i}`}>
710
- {formatInline(line)}
711
- {i < lines.length - 1 && '\n'}
712
- </Fragment>
713
- )
714
- }
715
- }
716
- }
717
- }
718
-
719
- if (inTable) {
720
- flushTable(lines.length)
721
- }
722
-
723
- if (inCodeBlock && codeBlockLines.length > 0) {
724
- const codeText = codeBlockLines.join('\n')
725
- items.push(
726
- <ChatCodeBlock
727
- key={`code-block-end`}
728
- code={codeText}
729
- language={codeBlockLang}
730
- />
731
- )
732
- }
733
-
734
- return items
735
- }
736
-
737
- const THINKING_LABELS = {
738
- live: 'Thinking…',
739
- completed: (count: number) => `Thinking process (${count} steps)`,
740
- step: (index: number) => `Step ${index}`,
741
- emptyHint: 'Model didn\'t send thinking steps this time',
742
- } as const
743
-
744
- function ThinkingBlock({
745
- thoughts,
746
- isLive = false,
747
- blockKey,
748
- expanded,
749
- onExpandedChange,
750
- showEmptyHint = false,
751
- }: {
752
- thoughts: string[]
753
- isLive?: boolean
754
- blockKey: string
755
- expanded?: boolean
756
- onExpandedChange?: (key: string, open: boolean) => void
757
- showEmptyHint?: boolean
758
- }) {
759
- const contentRef = useRef<HTMLDivElement>(null)
760
- const isControlled = expanded !== undefined && Boolean(onExpandedChange)
761
- const [localOpen, setLocalOpen] = useState(isLive)
762
- const isOpen = isControlled ? Boolean(expanded) : localOpen
763
-
764
- const setOpen = (open: boolean) => {
765
- if (isControlled && onExpandedChange) onExpandedChange(blockKey, open)
766
- else setLocalOpen(open)
767
- }
768
-
769
- useEffect(() => {
770
- if (isLive && !isControlled) setLocalOpen(true)
771
- }, [isLive, isControlled])
772
-
773
- useEffect(() => {
774
- if (!isLive || !isOpen || !contentRef.current) return
775
- contentRef.current.scrollTop = contentRef.current.scrollHeight
776
- }, [thoughts, isLive, isOpen])
777
-
778
- if (thoughts.length === 0 && !showEmptyHint) return null
779
-
780
- if (thoughts.length === 0) {
781
- return (
782
- <div className="thinking-block thinking-block-empty-state" data-live={isLive ? 'true' : 'false'}>
783
- <span className="thinking-block-empty">{THINKING_LABELS.emptyHint}</span>
784
- </div>
785
- )
786
- }
787
-
788
- return (
789
- <div className={`thinking-block${isLive ? ' is-live' : ''}`}>
790
- <button
791
- type="button"
792
- className="thinking-block-header"
793
- onClick={() => setOpen(!isOpen)}
794
- aria-expanded={isOpen}
795
- >
796
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}>
797
- <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
798
- <line x1="12" y1="17" x2="12.01" y2="17" />
799
- <circle cx="12" cy="12" r="10" />
800
- </svg>
801
- <span className="thinking-block-label">
802
- {isLive ? THINKING_LABELS.live : THINKING_LABELS.completed(thoughts.length)}
803
- </span>
804
- {isLive && <span className="thinking-block-live-dot" aria-hidden="true" />}
805
- <span className={`thinking-block-chevron${isOpen ? ' is-open' : ''}`} aria-hidden="true">
806
- <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
807
- <polyline points="9 18 15 12 9 6" />
808
- </svg>
809
- </span>
810
- </button>
811
- {isOpen && (
812
- <div className="thinking-block-body">
813
- <div className="thinking-block-content" ref={contentRef}>
814
- {thoughts.map((thought, index) => (
815
- <div className="thinking-block-step" key={`${blockKey}-${index}`}>
816
- {thoughts.length > 1 && (
817
- <div className="thinking-block-step-label">{THINKING_LABELS.step(index + 1)}</div>
818
- )}
819
- <div className="thinking-block-step-body">{renderFormattedMessage(thought)}</div>
820
- </div>
821
- ))}
822
- </div>
823
- </div>
824
- )}
825
- </div>
826
- )
827
- }
828
-
829
- function readableAssistantText(text: string) {
830
- if (typeof text !== 'string') return ''
831
- const trimmed = text.trim()
832
- if (!trimmed.startsWith('{')) return text
833
- try {
834
- const value = JSON.parse(trimmed)
835
- if (value?.action === 'finish' && typeof value?.input?.summary === 'string' && value.input.summary.trim()) {
836
- return value.input.summary
837
- }
838
- if (typeof value?.finish?.summary === 'string' && value.finish.summary.trim()) {
839
- return value.finish.summary
840
- }
841
- } catch {
842
- return text
843
- }
844
- return text
845
- }
846
-
847
- function renderSpeakerIcon(isSpeaking: boolean) {
848
- if (isSpeaking) {
849
- return (
850
- <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' }}>
851
- <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
852
- <path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
853
- <path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
854
- </svg>
855
- )
856
- }
857
- return (
858
- <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' }}>
859
- <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
860
- </svg>
861
- )
862
- }
863
-
864
- function cleanSpeechText(text: string) {
865
- return readableAssistantText(text)
866
- .replace(/\*\*([\s\S]*?)\*\*/g, '$1')
867
- .replace(/[*_`#]/g, '')
868
- .trim()
869
- }
870
-
871
- function numericSetting(value: unknown, fallback: number) {
872
- const numeric = Number(value)
873
- return Number.isFinite(numeric) ? numeric : fallback
874
- }
875
-
876
- interface DiffHunk {
877
- oldText: string
878
- newText: string
879
- }
880
-
881
- interface FileChange {
882
- path: string
883
- created: boolean
884
- additions: number
885
- deletions: number
886
- hunks: DiffHunk[]
887
- }
888
-
889
- function parseFileChangesFromProgress(progress: AgentProgress[]): FileChange[] {
890
- const changes = new Map<string, FileChange>()
891
- let activeEdit: { action: string; path: string; created: boolean; additions: number; deletions: number; hunks: DiffHunk[] } | null = null
892
-
893
- for (const event of progress || []) {
894
- if (event.type === 'ToolStart') {
895
- if (event.data.action === 'apply_patch') {
896
- const patch = (event.data.input as any)?.patch
897
- if (patch && typeof patch.path === 'string') {
898
- let additions = 0
899
- let deletions = 0
900
- const hunksList: DiffHunk[] = []
901
- const hunks = patch.hunks
902
- if (Array.isArray(hunks)) {
903
- for (const hunk of hunks) {
904
- const oldText = hunk?.oldText || ''
905
- const newText = hunk?.newText || ''
906
- const oldLines = oldText ? oldText.split('\n').length : 0
907
- const newLines = newText ? newText.split('\n').length : 0
908
- deletions += oldLines
909
- additions += newLines
910
- hunksList.push({ oldText, newText })
911
- }
912
- }
913
- activeEdit = {
914
- action: 'apply_patch',
915
- path: patch.path,
916
- created: false,
917
- additions,
918
- deletions,
919
- hunks: hunksList
920
- }
921
- }
922
- } else if (event.data.action === 'write_file') {
923
- const path = (event.data.input as any)?.path
924
- const fileContent = (event.data.input as any)?.file_content || ''
925
- if (typeof path === 'string') {
926
- const additions = fileContent ? fileContent.split('\n').length : 0
927
- activeEdit = {
928
- action: 'write_file',
929
- path,
930
- created: true,
931
- additions,
932
- deletions: 0,
933
- hunks: [{ oldText: '', newText: fileContent }]
934
- }
935
- }
936
- } else {
937
- activeEdit = null
938
- }
939
- } else if (event.type === 'ToolEnd') {
940
- if (activeEdit && (event.data.action === 'apply_patch' || event.data.action === 'write_file')) {
941
- const isError = typeof event.data.result === 'string' && event.data.result.startsWith('Error:')
942
- if (!isError) {
943
- try {
944
- const applied = JSON.parse(event.data.result)
945
- const appliedPaths = Array.isArray(applied) ? applied.map(item => item?.path).filter(Boolean) : [activeEdit.path]
946
-
947
- for (const path of appliedPaths) {
948
- const existing = changes.get(path)
949
- if (existing) {
950
- existing.additions += activeEdit.additions
951
- existing.deletions += activeEdit.deletions
952
- existing.hunks.push(...activeEdit.hunks)
953
- } else {
954
- changes.set(path, {
955
- path,
956
- created: activeEdit.created,
957
- additions: activeEdit.additions,
958
- deletions: activeEdit.deletions,
959
- hunks: [...activeEdit.hunks]
960
- })
961
- }
962
- }
963
- } catch (e) {
964
- const path = activeEdit.path
965
- const existing = changes.get(path)
966
- if (existing) {
967
- existing.additions += activeEdit.additions
968
- existing.deletions += activeEdit.deletions
969
- existing.hunks.push(...activeEdit.hunks)
970
- } else {
971
- changes.set(path, {
972
- path,
973
- created: activeEdit.created,
974
- additions: activeEdit.additions,
975
- deletions: activeEdit.deletions,
976
- hunks: [...activeEdit.hunks]
977
- })
978
- }
979
- }
980
- }
981
- }
982
- activeEdit = null
983
- }
984
- }
985
-
986
- return Array.from(changes.values())
987
- }
988
76
 
989
77
  export default function ChatPanel({
990
78
  interactions,
@@ -1010,7 +98,6 @@ export default function ChatPanel({
1010
98
  onSelectImage,
1011
99
  onSelectDocument,
1012
100
  onPasteImage,
1013
- onReadClipboardImage,
1014
101
  onSetMessage,
1015
102
  onSendVoiceMessage,
1016
103
  onRemoveImage,
@@ -1024,20 +111,23 @@ export default function ChatPanel({
1024
111
  onToggleMobileSidebar,
1025
112
  settingsConfig,
1026
113
  onSetModel,
114
+ onCancelMessage,
1027
115
  }: ChatPanelProps) {
1028
116
  const agentActivities = activitiesFrom(agentProgress)
1029
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
+ }
1030
125
  const [openActivityIds, setOpenActivityIds] = useState<Record<string, boolean>>({})
1031
126
  const [openReviewIds, setOpenReviewIds] = useState<Record<string, boolean>>({})
1032
127
  const [openFileDiffs, setOpenFileDiffs] = useState<Record<string, boolean>>({})
1033
128
  const [toolMenuOpen, setToolMenuOpen] = useState(false)
1034
129
  const toolMenuRef = useRef<HTMLDivElement | null>(null)
1035
130
  const [dynamicOllamaModels, setDynamicOllamaModels] = useState<string[]>(OLLAMA_MODELS)
1036
- const [askAnswer, setAskAnswer] = useState('')
1037
-
1038
- useEffect(() => {
1039
- setAskAnswer('')
1040
- }, [pendingApproval])
1041
131
 
1042
132
  useEffect(() => {
1043
133
  const fetchOllamaModels = async () => {
@@ -1133,60 +223,40 @@ export default function ChatPanel({
1133
223
  return () => clearInterval(timer)
1134
224
  }, [sending])
1135
225
 
1136
- const [isRecording, setIsRecording] = useState(false)
1137
- const [voiceMode, setVoiceMode] = useState(false)
1138
- const [voiceTranscript, setVoiceTranscript] = useState('')
1139
- const [voiceAwaitingResponse, setVoiceAwaitingResponse] = useState(false)
1140
226
  const [speakingText, setSpeakingText] = useState<string | null>(null)
1141
- const recognitionRef = useRef<SpeechRecognition | null>(null)
1142
- const mediaRecorderRef = useRef<MediaRecorder | null>(null)
1143
- const mediaStreamRef = useRef<MediaStream | null>(null)
1144
- const audioContextRef = useRef<AudioContext | null>(null)
1145
- const vadTimerRef = useRef<number | null>(null)
1146
227
  const audioRef = useRef<HTMLAudioElement | null>(null)
1147
228
  const speechRunRef = useRef(0)
1148
229
  const historyReadyRef = useRef(false)
1149
230
  const submittedDuringSessionRef = useRef(false)
1150
231
  const lastAutoSpokenIdRef = useRef<number | string | null>(null)
1151
- const voiceModeRef = useRef(false)
1152
- const sendingRef = useRef(false)
1153
- const voiceAwaitingResponseRef = useRef(false)
1154
232
  const speakingRef = useRef<string | null>(null)
1155
- 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
+
1156
256
  const voiceStatus = speakingText ? 'speaking' : (sending || voiceAwaitingResponse) ? 'thinking' : isRecording ? 'listening' : voiceMode ? 'ready' : 'off'
1157
257
  const voiceStatusLabel = voiceStatus === 'speaking' ? 'Speaking' : voiceStatus === 'thinking' ? 'Thinking' : voiceStatus === 'listening' ? 'Listening' : 'Ready'
1158
258
 
1159
- const clearRestartTimer = () => {
1160
- if (restartTimerRef.current === null) return
1161
- window.clearTimeout(restartTimerRef.current)
1162
- restartTimerRef.current = null
1163
- }
1164
- const clearVadTimer = () => {
1165
- if (vadTimerRef.current === null) return
1166
- window.clearInterval(vadTimerRef.current)
1167
- vadTimerRef.current = null
1168
- }
1169
- const stopRecognition = () => {
1170
- clearRestartTimer()
1171
- recognitionRef.current?.stop()
1172
- recognitionRef.current = null
1173
- clearVadTimer()
1174
- if (mediaRecorderRef.current?.state === 'recording') mediaRecorderRef.current.stop()
1175
- mediaRecorderRef.current = null
1176
- mediaStreamRef.current?.getTracks().forEach((track) => track.stop())
1177
- mediaStreamRef.current = null
1178
- audioContextRef.current?.close().catch(() => {})
1179
- audioContextRef.current = null
1180
- setIsRecording(false)
1181
- }
1182
- const scheduleVoiceListen = (delayMs = 350) => {
1183
- clearRestartTimer()
1184
- if (!voiceModeRef.current || sendingRef.current || voiceAwaitingResponseRef.current || speakingRef.current) return
1185
- restartTimerRef.current = window.setTimeout(() => {
1186
- restartTimerRef.current = null
1187
- startRecognition(true)
1188
- }, delayMs)
1189
- }
259
+
1190
260
  const cancelSpeech = () => {
1191
261
  speechRunRef.current += 1
1192
262
  audioRef.current?.pause()
@@ -1208,7 +278,11 @@ export default function ChatPanel({
1208
278
  utterance.volume = Math.max(0, Math.min(1, numericSetting(settingsConfig?.ttsVolume, 1)))
1209
279
  utterance.rate = Math.max(0.1, Math.min(10, numericSetting(settingsConfig?.ttsSpeed, 1)))
1210
280
  utterance.pitch = Math.max(0, Math.min(2, numericSetting(settingsConfig?.ttsPitch, 1)))
1211
- 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
+ })
1212
286
  if (voice) utterance.voice = voice
1213
287
  const finishSpeech = () => {
1214
288
  setSpeakingText((current) => (current === displayText ? null : current))
@@ -1264,213 +338,24 @@ export default function ChatPanel({
1264
338
  }
1265
339
  speakNative(speechText, text)
1266
340
  }
1267
- const blobToDataUri = (blob: Blob) => new Promise<string>((resolve, reject) => {
1268
- const reader = new FileReader()
1269
- reader.onerror = () => reject(reader.error ?? new Error('Failed to read audio recording'))
1270
- reader.onload = () => resolve(String(reader.result))
1271
- reader.readAsDataURL(blob)
1272
- })
1273
- const preferredAudioMimeType = () => {
1274
- const candidates = ['audio/webm;codecs=opus', 'audio/webm', 'audio/ogg;codecs=opus', 'audio/ogg']
1275
- return candidates.find((mimeType) => MediaRecorder.isTypeSupported(mimeType)) ?? ''
1276
- }
1277
- const startAudioRecording = async (autoSend: boolean) => {
1278
- if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === 'undefined') {
1279
- alert("Sorry, this system cannot access the microphone or record audio in this browser")
1280
- voiceModeRef.current = false
1281
- setVoiceMode(false)
1282
- return
1283
- }
1284
-
1285
- try {
1286
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
1287
- const mimeType = preferredAudioMimeType()
1288
- const recorder = new MediaRecorder(stream, mimeType ? { mimeType } : undefined)
1289
- const chunks: Blob[] = []
1290
- let heardVoice = false
1291
- let quietSince = 0
1292
- const startedAt = Date.now()
1293
- const audioContext = new AudioContext()
1294
- const source = audioContext.createMediaStreamSource(stream)
1295
- const analyser = audioContext.createAnalyser()
1296
- const samples = new Uint8Array(analyser.fftSize)
1297
- source.connect(analyser)
1298
-
1299
- mediaStreamRef.current = stream
1300
- mediaRecorderRef.current = recorder
1301
- audioContextRef.current = audioContext
1302
- setVoiceTranscript('Listening to microphone...')
1303
-
1304
- recorder.ondataavailable = (event) => {
1305
- if (event.data.size > 0) chunks.push(event.data)
1306
- }
1307
- recorder.onstop = async () => {
1308
- clearVadTimer()
1309
- mediaRecorderRef.current = null
1310
- mediaStreamRef.current?.getTracks().forEach((track) => track.stop())
1311
- mediaStreamRef.current = null
1312
- audioContextRef.current?.close().catch(() => {})
1313
- audioContextRef.current = null
1314
- setIsRecording(false)
1315
- if (!autoSend || !voiceModeRef.current || chunks.length === 0) return
1316
- if (!heardVoice) {
1317
- setVoiceTranscript('No speech detected')
1318
- scheduleVoiceListen(700)
1319
- return
1320
- }
1321
- const blob = new Blob(chunks, { type: recorder.mimeType || 'audio/webm' })
1322
- const audioDataUri = await blobToDataUri(blob)
1323
- setVoiceTranscript('Sent audio to AI')
1324
- voiceAwaitingResponseRef.current = true
1325
- setVoiceAwaitingResponse(true)
1326
- onSendVoiceMessage('', audioDataUri)
1327
- .catch((error) => console.error('Voice audio message failed', error))
1328
- .finally(() => {
1329
- voiceAwaitingResponseRef.current = false
1330
- setVoiceAwaitingResponse(false)
1331
- scheduleVoiceListen()
1332
- })
1333
- }
1334
341
 
1335
- recorder.start()
1336
- setIsRecording(true)
1337
- vadTimerRef.current = window.setInterval(() => {
1338
- analyser.getByteTimeDomainData(samples)
1339
- let peak = 0
1340
- for (const sample of samples) {
1341
- peak = Math.max(peak, Math.abs(sample - 128))
1342
- }
1343
- const now = Date.now()
1344
- if (peak > 12) {
1345
- heardVoice = true
1346
- quietSince = 0
1347
- setVoiceTranscript('Listening...')
1348
- } else if (heardVoice) {
1349
- quietSince = quietSince || now
1350
- }
1351
- const silenceElapsed = quietSince ? now - quietSince : 0
1352
- const totalElapsed = now - startedAt
1353
- if ((heardVoice && silenceElapsed > 1300) || totalElapsed > 12000) {
1354
- recorder.stop()
1355
- }
1356
- }, 120)
1357
- } catch (error) {
1358
- console.error('Failed to record microphone audio', error)
1359
- setIsRecording(false)
1360
- voiceModeRef.current = false
1361
- setVoiceMode(false)
1362
- alert('Failed to open microphone. Please check microphone permissions in your browser')
1363
- }
1364
- }
1365
- const startRecognition = (autoSend: boolean) => {
1366
- if (recognitionRef.current || sendingRef.current || voiceAwaitingResponseRef.current || speakingRef.current) return
1367
-
1368
- const SpeechRecognitionApi = window.SpeechRecognition || window.webkitSpeechRecognition
1369
- if (!SpeechRecognitionApi) {
1370
- startAudioRecording(autoSend)
1371
- return
1372
- }
1373
-
1374
- let sentTranscript = false
1375
-
1376
- try {
1377
- const recognition = new SpeechRecognitionApi()
1378
- recognition.continuous = false
1379
- recognition.interimResults = true
1380
- recognition.lang = settingsConfig?.language === 'en' ? 'en-US' : 'th-TH'
1381
- recognition.onstart = () => setIsRecording(true)
1382
- recognition.onresult = (event: SpeechRecognitionEvent) => {
1383
- let interimText = ''
1384
- let finalText = ''
1385
- for (let index = event.resultIndex; index < event.results.length; index += 1) {
1386
- const transcript = event.results[index]?.[0]?.transcript ?? ''
1387
- if (event.results[index]?.isFinal) finalText += transcript
1388
- else interimText += transcript
1389
- }
1390
- const displayText = (finalText || interimText).trim()
1391
- if (displayText) setVoiceTranscript(displayText)
1392
- const resultText = finalText.trim()
1393
- if (!resultText) return
1394
- sentTranscript = true
1395
- recognition.stop()
1396
- setVoiceTranscript(resultText)
1397
- if (autoSend) {
1398
- voiceAwaitingResponseRef.current = true
1399
- setVoiceAwaitingResponse(true)
1400
- onSendVoiceMessage(resultText)
1401
- .catch((error) => console.error('Voice message failed', error))
1402
- .finally(() => {
1403
- voiceAwaitingResponseRef.current = false
1404
- setVoiceAwaitingResponse(false)
1405
- scheduleVoiceListen()
1406
- })
1407
- } else {
1408
- onSetMessage(message.trim() ? `${message.trimEnd()} ${resultText}` : resultText)
1409
- }
1410
- }
1411
- recognition.onerror = (event: Event) => {
1412
- console.error('Speech recognition error', event)
1413
- setIsRecording(false)
1414
- }
1415
- recognition.onend = () => {
1416
- recognitionRef.current = null
1417
- setIsRecording(false)
1418
- if (autoSend && voiceModeRef.current && !sentTranscript) scheduleVoiceListen()
1419
- }
1420
- recognitionRef.current = recognition
1421
- recognition.start()
1422
- } catch (error) {
1423
- console.error('Failed to start speech recognition', error)
1424
- recognitionRef.current = null
1425
- setIsRecording(false)
1426
- }
1427
- }
1428
342
  const toggleRecording = () => {
1429
343
  if (voiceMode) {
1430
- voiceModeRef.current = false
1431
- voiceAwaitingResponseRef.current = false
1432
344
  setVoiceMode(false)
1433
- setVoiceAwaitingResponse(false)
1434
- setVoiceTranscript('')
1435
345
  stopRecognition()
1436
346
  cancelSpeech()
1437
347
  return
1438
348
  }
1439
349
 
1440
- voiceModeRef.current = true
1441
350
  setVoiceMode(true)
1442
- setVoiceAwaitingResponse(false)
1443
- setVoiceTranscript('')
1444
351
  startRecognition(true)
1445
352
  }
1446
353
 
1447
354
  useEffect(() => {
1448
355
  return () => {
1449
- clearRestartTimer()
1450
356
  cancelSpeech()
1451
- stopRecognition()
1452
357
  }
1453
358
  }, [])
1454
- useEffect(() => {
1455
- voiceModeRef.current = voiceMode
1456
- if (!voiceMode) {
1457
- clearRestartTimer()
1458
- setVoiceTranscript('')
1459
- }
1460
- }, [voiceMode])
1461
- useEffect(() => {
1462
- sendingRef.current = sending
1463
- }, [sending])
1464
- useEffect(() => {
1465
- voiceAwaitingResponseRef.current = voiceAwaitingResponse
1466
- }, [voiceAwaitingResponse])
1467
- useEffect(() => {
1468
- speakingRef.current = speakingText
1469
- }, [speakingText])
1470
- useEffect(() => {
1471
- if (!voiceMode || sending || voiceAwaitingResponse || speakingText || isRecording) return
1472
- scheduleVoiceListen()
1473
- }, [voiceMode, sending, voiceAwaitingResponse, speakingText, isRecording])
1474
359
  useEffect(() => {
1475
360
  if (sending) submittedDuringSessionRef.current = true
1476
361
  }, [sending])
@@ -1576,21 +461,20 @@ export default function ChatPanel({
1576
461
  if (onPasteImage(event.clipboardData)) {
1577
462
  event.preventDefault()
1578
463
  event.stopPropagation()
1579
- } else {
1580
- window.setTimeout(() => void onReadClipboardImage(), 0)
1581
464
  }
1582
465
  }
1583
466
  window.addEventListener('paste', handleWindowPaste, true)
1584
467
  return () => window.removeEventListener('paste', handleWindowPaste, true)
1585
- }, [onPasteImage, onReadClipboardImage])
468
+ }, [onPasteImage])
1586
469
  useEffect(() => {
1587
- const handleWindowKeyDown = (event: globalThis.KeyboardEvent) => {
1588
- if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== 'v') return
1589
- window.setTimeout(() => void onReadClipboardImage(), 0)
470
+ const handleEscapeKeyDown = (event: globalThis.KeyboardEvent) => {
471
+ if (event.key === 'Escape' && sending) {
472
+ onCancelMessage()
473
+ }
1590
474
  }
1591
- window.addEventListener('keydown', handleWindowKeyDown, true)
1592
- return () => window.removeEventListener('keydown', handleWindowKeyDown, true)
1593
- }, [onReadClipboardImage])
475
+ window.addEventListener('keydown', handleEscapeKeyDown, true)
476
+ return () => window.removeEventListener('keydown', handleEscapeKeyDown, true)
477
+ }, [sending, onCancelMessage])
1594
478
  useEffect(() => {
1595
479
  if (message) return
1596
480
  const input = document.getElementById('chat-input') as HTMLTextAreaElement | null
@@ -1613,24 +497,72 @@ export default function ChatPanel({
1613
497
  const renderCompletedActivity = (interaction: any) => {
1614
498
  const interactionId = String(interaction.id)
1615
499
  const activityView = activitiesFrom(agentActivitySnapshots[interactionId] ?? [])
1616
- if (activityView.items.length === 0) return null
1617
500
  const isOpen = Boolean(openActivityIds[interactionId])
1618
501
  return (
1619
- <div className="agent-activity-history">
1620
- <button
1621
- type="button"
1622
- className="agent-activity-toggle"
1623
- aria-expanded={isOpen}
1624
- onClick={() => setOpenActivityIds((current) => ({ ...current, [interactionId]: !current[interactionId] }))}
1625
- >
1626
- <span>{activityView.summary}</span>
1627
- <span aria-hidden="true">{isOpen ? '^' : '>'}</span>
1628
- </button>
1629
- {isOpen && (
1630
- <div className="agent-activity-card agent-activity-card-history">
1631
- <AgentActivityTable activityView={activityView} />
1632
- </div>
1633
- )}
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>
1634
566
  </div>
1635
567
  )
1636
568
  }
@@ -1927,6 +859,7 @@ export default function ChatPanel({
1927
859
  )
1928
860
  })()}
1929
861
  <div className="message-bubble" style={{ whiteSpace: 'pre-wrap' }}>{renderFormattedMessage(interaction.aiText)}</div>
862
+ {renderWebSearchSources(interaction)}
1930
863
  <div className="message-time" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
1931
864
  <button className="provider-badge">{interaction.provider} • {interaction.model}</button>
1932
865
  {fallbackNotice(interaction) && <span className="provider-fallback-notice">{fallbackNotice(interaction)}</span>}
@@ -1961,18 +894,13 @@ export default function ChatPanel({
1961
894
  {sending && (
1962
895
  <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' }}>
1963
896
  <div className="message user-message"><div className="bubble-wrapper"><div className="message-bubble">{sendingImageMarkers ? renderFormattedMessage(`${sendingMessage} ${sendingImageMarkers}`) : renderFormattedMessage(sendingMessage)}</div></div></div>
1964
- {agentMode && agentActivities.items.length > 0 && (
1965
- <div className="message ai-message agent-activity-message">
1966
- <div className="agent-activity-card">
1967
- <div className="agent-activity-header">
1968
- <span>{agentActivities.summary}</span>
1969
- <span className="agent-activity-status" data-state={pendingApproval ? 'approval' : 'active'}>
1970
- {pendingApproval ? 'Waiting for approval' : 'Working'}
1971
- </span>
1972
- </div>
1973
- <AgentActivityTable activityView={agentActivities} />
1974
- </div>
1975
- </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
+ />
1976
904
  )}
1977
905
  {renderActiveFileChanges()}
1978
906
  <div className="message ai-message thinking-message">
@@ -2009,7 +937,12 @@ export default function ChatPanel({
2009
937
  />
2010
938
  </path>
2011
939
  </svg>
2012
- <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>
2013
946
  </div>
2014
947
  )}
2015
948
  </span>
@@ -2047,85 +980,17 @@ export default function ChatPanel({
2047
980
  </div>
2048
981
  )}
2049
982
 
2050
- {pendingApproval && (() => {
2051
- const details = renderApprovalDetails(pendingApproval.approval)
2052
- const writeFile = pendingApproval.approval?.WriteFile
2053
- const applyPatch = pendingApproval.approval?.ApplyPatch
2054
- const diffText = writeFile?.diff || applyPatch?.diff
2055
- const isAskUser = !!pendingApproval.approval?.AskUser
2056
-
2057
- return (
2058
- <div className="message ai-message" style={{ width: '100%' }}>
2059
- <div className="bubble-wrapper" style={{ width: '100%' }}>
2060
- <div className="action-card approval-card" data-tier={details.isDangerous ? 'dangerous' : undefined} style={{ width: '100%' }}>
2061
- <div className="approval-card-content" style={{ width: '100%' }}>
2062
- <div className="approval-card-title">{details.title}</div>
2063
- <div className="approval-card-body" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.body}</div>
2064
- {diffText ? (
2065
- <div className="approval-card-diff-container" style={{ marginTop: '8px', width: '100%' }}>
2066
- <div style={{ fontSize: '0.8rem', fontWeight: 600, color: 'var(--text-soft, #94a3b8)', marginBottom: '4px' }}>Diff:</div>
2067
- {renderDiff(diffText)}
2068
- </div>
2069
- ) : (
2070
- details.reason && <div className="approval-card-reason" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.reason}</div>
2071
- )}
2072
- {isAskUser && (
2073
- <div className="approval-card-input-container" style={{ marginTop: '12px', width: '100%' }}>
2074
- <textarea
2075
- style={{
2076
- width: '100%',
2077
- minHeight: '70px',
2078
- padding: '10px',
2079
- background: 'rgba(0, 0, 0, 0.2)',
2080
- border: '1px solid rgba(255, 255, 255, 0.1)',
2081
- borderRadius: '6px',
2082
- color: 'var(--text-main, #f8fafc)',
2083
- fontSize: '0.9rem',
2084
- fontFamily: 'inherit',
2085
- resize: 'vertical',
2086
- outline: 'none',
2087
- }}
2088
- placeholder="Type your answer here..."
2089
- value={askAnswer}
2090
- onChange={(e) => setAskAnswer(e.target.value)}
2091
- />
2092
- </div>
2093
- )}
2094
- </div>
2095
- {isAskUser ? (
2096
- <div className="approval-card-actions">
2097
- <button
2098
- type="button"
2099
- className="approval-btn approval-btn-approve"
2100
- disabled={!askAnswer.trim()}
2101
- onClick={() => onApproval(true, false, askAnswer)}
2102
- >
2103
- Submit Answer
2104
- </button>
2105
- <button
2106
- type="button"
2107
- className="approval-btn approval-btn-cancel"
2108
- onClick={() => onApproval(false)}
2109
- >
2110
- Decline
2111
- </button>
2112
- </div>
2113
- ) : (
2114
- <div className="approval-card-actions">
2115
- <button type="button" className="approval-btn approval-btn-approve" onClick={() => onApproval(true)}>Approve</button>
2116
- <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>
2117
- <button type="button" className="approval-btn approval-btn-cancel" onClick={() => onApproval(false)}>Cancel</button>
2118
- </div>
2119
- )}
2120
- </div>
2121
- </div>
2122
- </div>
2123
- )
2124
- })()}
983
+ {pendingApproval && (
984
+ <ApprovalCard
985
+ key={pendingApproval.id || JSON.stringify(pendingApproval)}
986
+ pendingApproval={pendingApproval}
987
+ onApproval={onApproval}
988
+ />
989
+ )}
2125
990
  <div ref={chatEnd} />
2126
991
  </div>
2127
992
 
2128
- <div className="input-area">
993
+ <div className={`input-area ${voiceMode ? 'voice-active' : ''}`}>
2129
994
  {isEmptyChat && <div className="empty-chat-prompt">Mint Agent is ready to work</div>}
2130
995
  <div className="smart-context-bar">
2131
996
  <div className="smart-context-label" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
@@ -2139,8 +1004,21 @@ export default function ChatPanel({
2139
1004
  {voiceMode && (
2140
1005
  <div className="voice-mode-bar" data-state={voiceStatus}>
2141
1006
  <span className="voice-mode-dot" />
2142
- <span>{voiceStatusLabel}</span>
2143
- {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
+ )}
2144
1022
  </div>
2145
1023
  )}
2146
1024
 
@@ -2312,12 +1190,27 @@ export default function ChatPanel({
2312
1190
  </svg>
2313
1191
  )}
2314
1192
  </button>
2315
- <button id="send-btn" type="submit" disabled={sending || !canSubmit} style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
2316
- <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">
2317
- <line x1="22" y1="2" x2="11" y2="13"></line>
2318
- <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
2319
- </svg>
2320
- </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
+ )}
2321
1214
  </form>
2322
1215
  </div>
2323
1216
  <p className="input-disclaimer">