@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 DragEvent, type FormEvent, type KeyboardEvent, type RefObject } 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[]
@@ -344,7 +58,6 @@ interface ChatPanelProps {
344
58
  onSelectImage: (event: ChangeEvent<HTMLInputElement>) => void
345
59
  onSelectDocument: (event: ChangeEvent<HTMLInputElement>) => void
346
60
  onPasteImage: (clipboardData: DataTransfer) => boolean
347
- onReadClipboardImage: () => Promise<boolean>
348
61
  onSetMessage: (message: string) => void
349
62
  onSendVoiceMessage: (message: string, audioDataUri?: string | null) => Promise<void>
350
63
  onRemoveImage: (idx: number) => void
@@ -355,580 +68,12 @@ interface ChatPanelProps {
355
68
  onSetAgentMode: (enabled: boolean) => void
356
69
  onSetProvider: (provider: string) => void
357
70
  onSelectWorkspace: () => void
358
- onApproval: (approved: boolean, autoApproveSession?: boolean) => void
71
+ onApproval: (approved: boolean, autoApproveSession?: boolean, answer?: string) => void
359
72
  settingsConfig: any
360
73
  onSetModel: (model: string) => void
74
+ onCancelMessage: () => void
361
75
  }
362
76
 
363
- function ChatCodeBlock({ code, language }: { code: string; language: string; key?: any }) {
364
- const [copied, setCopied] = useState(false)
365
-
366
- const handleCopy = async () => {
367
- try {
368
- await navigator.clipboard.writeText(code)
369
- setCopied(true)
370
- setTimeout(() => setCopied(false), 2000)
371
- } catch (err) {
372
- console.error('Failed to copy code: ', err)
373
- }
374
- }
375
-
376
- const handleDownload = () => {
377
- try {
378
- const blob = new Blob([code], { type: 'text/plain;charset=utf-8' })
379
- const url = URL.createObjectURL(blob)
380
- const link = document.createElement('a')
381
- link.href = url
382
- link.download = `code.${language === 'plaintext' ? 'txt' : language}`
383
- link.click()
384
- URL.revokeObjectURL(url)
385
- } catch (err) {
386
- console.error('Failed to download code: ', err)
387
- }
388
- }
389
-
390
- const displayLang = language.charAt(0).toUpperCase() + language.slice(1).toLowerCase()
391
-
392
- return (
393
- <div className="chat-code-block-container" style={{ whiteSpace: 'normal' }}>
394
- <div className="chat-code-block-header">
395
- <span className="chat-code-block-lang">{displayLang}</span>
396
- <div className="chat-code-block-actions">
397
- <button type="button" onClick={handleDownload} title="Download code" className="chat-code-action-btn">
398
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
399
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
400
- <polyline points="7 10 12 15 17 10" />
401
- <line x1="12" y1="15" x2="12" y2="3" />
402
- </svg>
403
- </button>
404
- <button type="button" onClick={handleCopy} title="Copy code" className="chat-code-action-btn">
405
- {copied ? (
406
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
407
- <polyline points="20 6 9 17 4 12" />
408
- </svg>
409
- ) : (
410
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
411
- <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
412
- <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
413
- </svg>
414
- )}
415
- </button>
416
- </div>
417
- </div>
418
- <pre className="chat-code-block-body">
419
- <code>{code}</code>
420
- </pre>
421
- </div>
422
- )
423
- }
424
-
425
- const isTableLine = (line: string): boolean => {
426
- const trimmed = line.trim()
427
- return trimmed.startsWith('|') && trimmed.endsWith('|') && trimmed.length > 1
428
- }
429
-
430
- function renderFormattedMessage(text: string) {
431
- const displayText = readableAssistantText(text)
432
- if (!displayText) return null
433
-
434
- const formatInline = (str: string) => {
435
- const codeParts = str.split(/`([\s\S]*?)`/g)
436
- return codeParts.map((codePart, codeIndex) => {
437
- if (codeIndex % 2 === 1) {
438
- return (
439
- <code key={`code-${codeIndex}`} className="chat-inline-code">
440
- {codePart}
441
- </code>
442
- )
443
- }
444
- const boldParts = codePart.split(/\*\*([\s\S]*?)\*\*/g)
445
- return boldParts.map((boldPart, boldIndex) => {
446
- if (boldIndex % 2 === 1) {
447
- return (
448
- <strong key={`bold-${boldIndex}`} className="chat-bold-highlight">
449
- {boldPart}
450
- </strong>
451
- )
452
- }
453
- return boldPart
454
- })
455
- })
456
- }
457
-
458
- const renderHtmlTable = (tableLines: string[], key: string) => {
459
- const rows: string[][] = []
460
- for (const line of tableLines) {
461
- const trimmed = line.trim()
462
- if (!trimmed) continue
463
- const content = trimmed.slice(1, -1)
464
- const contentEscaped = content.replace(/\\\|/g, '\u0000')
465
- const cols = contentEscaped.split('|').map(s => s.replace(/\u0000/g, '|').trim())
466
- rows.push(cols)
467
- }
468
-
469
- if (rows.length === 0) return null
470
-
471
- let hasSeparator = false
472
- let separatorIdx = -1
473
- for (let i = 0; i < rows.length; i++) {
474
- const row = rows[i]
475
- if (row.length > 0 && row.every(col => /^[-\s:]+$/.test(col))) {
476
- hasSeparator = true
477
- separatorIdx = i
478
- break
479
- }
480
- }
481
-
482
- let headerRow: string[] | null = null
483
- const dataRows: string[][] = []
484
-
485
- if (hasSeparator) {
486
- if (separatorIdx > 0) {
487
- headerRow = rows[separatorIdx - 1]
488
- for (let i = 0; i < rows.length; i++) {
489
- if (i !== separatorIdx && i !== separatorIdx - 1) {
490
- dataRows.push(rows[i])
491
- }
492
- }
493
- } else {
494
- for (let i = 0; i < rows.length; i++) {
495
- if (i !== separatorIdx) {
496
- dataRows.push(rows[i])
497
- }
498
- }
499
- }
500
- } else {
501
- headerRow = rows[0]
502
- for (let i = 1; i < rows.length; i++) {
503
- dataRows.push(rows[i])
504
- }
505
- }
506
-
507
- return (
508
- <div key={key} className="chat-table-container" style={{
509
- overflowX: 'auto',
510
- margin: '14px 0',
511
- width: '100%',
512
- borderRadius: '8px',
513
- border: '1px solid rgba(255, 255, 255, 0.12)',
514
- background: 'rgba(30, 41, 59, 0.35)',
515
- boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
516
- }}>
517
- <table className="chat-table" style={{
518
- width: '100%',
519
- borderCollapse: 'collapse',
520
- fontSize: '0.86rem',
521
- textAlign: 'left',
522
- lineHeight: '1.5',
523
- }}>
524
- {headerRow && (
525
- <thead>
526
- <tr style={{
527
- background: 'rgba(255, 255, 255, 0.04)',
528
- borderBottom: '2px solid rgba(255, 255, 255, 0.15)',
529
- }}>
530
- {headerRow.map((col, idx) => (
531
- <th key={`th-${idx}`} style={{
532
- padding: '12px 16px',
533
- fontWeight: 700,
534
- color: 'var(--accent, #38bdf8)',
535
- fontFamily: 'ui-sans-serif, system-ui, sans-serif',
536
- }}>
537
- {formatInline(col)}
538
- </th>
539
- ))}
540
- </tr>
541
- </thead>
542
- )}
543
- <tbody>
544
- {dataRows.map((row, rIdx) => (
545
- <tr key={`tr-${rIdx}`} style={{
546
- background: rIdx % 2 === 1 ? 'rgba(255, 255, 255, 0.015)' : 'transparent',
547
- borderBottom: rIdx < dataRows.length - 1 ? '1px solid rgba(255, 255, 255, 0.08)' : 'none',
548
- }}>
549
- {row.map((col, cIdx) => (
550
- <td key={`td-${cIdx}`} style={{
551
- padding: '12px 16px',
552
- color: 'var(--text-main, #e2e8f0)',
553
- }}>
554
- {formatInline(col)}
555
- </td>
556
- ))}
557
- </tr>
558
- ))}
559
- </tbody>
560
- </table>
561
- </div>
562
- )
563
- }
564
-
565
- const lines = displayText.split('\n')
566
- const items: any[] = []
567
-
568
- let inCodeBlock = false
569
- let codeBlockLang = ''
570
- let codeBlockLines: string[] = []
571
-
572
- let inTable = false
573
- let tableLines: string[] = []
574
-
575
- const flushTable = (index: number) => {
576
- if (tableLines.length > 0) {
577
- items.push(renderHtmlTable(tableLines, `table-${index}`))
578
- tableLines = []
579
- inTable = false
580
- }
581
- }
582
-
583
- for (let i = 0; i < lines.length; i++) {
584
- const line = lines[i]
585
-
586
- if (line.trim().startsWith('```')) {
587
- if (inTable) {
588
- flushTable(i)
589
- }
590
- if (inCodeBlock) {
591
- const codeText = codeBlockLines.join('\n')
592
- items.push(
593
- <ChatCodeBlock
594
- key={`code-block-${i}`}
595
- code={codeText}
596
- language={codeBlockLang}
597
- />
598
- )
599
- inCodeBlock = false
600
- codeBlockLines = []
601
- } else {
602
- inCodeBlock = true
603
- codeBlockLang = line.trim().slice(3).trim() || 'plaintext'
604
- }
605
- continue
606
- }
607
-
608
- if (inCodeBlock) {
609
- codeBlockLines.push(line)
610
- continue
611
- }
612
-
613
- if (isTableLine(line)) {
614
- inTable = true
615
- tableLines.push(line)
616
- } else {
617
- if (inTable) {
618
- flushTable(i)
619
- }
620
-
621
- const headerMatch = line.match(/^(#{1,6})\s+(.*)$/)
622
- if (headerMatch) {
623
- const level = headerMatch[1].length
624
- const content = headerMatch[2]
625
-
626
- const style = {
627
- fontWeight: 'bold',
628
- display: 'block',
629
- marginTop: level === 1 ? '16px' : level === 2 ? '14px' : '10px',
630
- marginBottom: '6px',
631
- fontSize: level === 1 ? '1.25em' : level === 2 ? '1.15em' : '1.05em',
632
- color: 'var(--text-main)',
633
- }
634
-
635
- items.push(
636
- <span key={`line-${i}`} style={style}>
637
- {formatInline(content)}
638
- </span>
639
- )
640
- } else {
641
- const listMatch = line.match(/^(\s*)([-*+])\s+(.*)$/)
642
- if (listMatch) {
643
- const indent = listMatch[1]
644
- const content = listMatch[3]
645
- items.push(
646
- <Fragment key={`line-${i}`}>
647
- {indent}• {formatInline(content)}
648
- {i < lines.length - 1 && '\n'}
649
- </Fragment>
650
- )
651
- } else {
652
- items.push(
653
- <Fragment key={`line-${i}`}>
654
- {formatInline(line)}
655
- {i < lines.length - 1 && '\n'}
656
- </Fragment>
657
- )
658
- }
659
- }
660
- }
661
- }
662
-
663
- if (inTable) {
664
- flushTable(lines.length)
665
- }
666
-
667
- if (inCodeBlock && codeBlockLines.length > 0) {
668
- const codeText = codeBlockLines.join('\n')
669
- items.push(
670
- <ChatCodeBlock
671
- key={`code-block-end`}
672
- code={codeText}
673
- language={codeBlockLang}
674
- />
675
- )
676
- }
677
-
678
- return items
679
- }
680
-
681
- const THINKING_LABELS = {
682
- live: 'Thinking…',
683
- completed: (count: number) => `Thinking process (${count} steps)`,
684
- step: (index: number) => `Step ${index}`,
685
- emptyHint: 'Model didn\'t send thinking steps this time',
686
- } as const
687
-
688
- function ThinkingBlock({
689
- thoughts,
690
- isLive = false,
691
- blockKey,
692
- expanded,
693
- onExpandedChange,
694
- showEmptyHint = false,
695
- }: {
696
- thoughts: string[]
697
- isLive?: boolean
698
- blockKey: string
699
- expanded?: boolean
700
- onExpandedChange?: (key: string, open: boolean) => void
701
- showEmptyHint?: boolean
702
- }) {
703
- const contentRef = useRef<HTMLDivElement>(null)
704
- const isControlled = expanded !== undefined && Boolean(onExpandedChange)
705
- const [localOpen, setLocalOpen] = useState(isLive)
706
- const isOpen = isControlled ? Boolean(expanded) : localOpen
707
-
708
- const setOpen = (open: boolean) => {
709
- if (isControlled && onExpandedChange) onExpandedChange(blockKey, open)
710
- else setLocalOpen(open)
711
- }
712
-
713
- useEffect(() => {
714
- if (isLive && !isControlled) setLocalOpen(true)
715
- }, [isLive, isControlled])
716
-
717
- useEffect(() => {
718
- if (!isLive || !isOpen || !contentRef.current) return
719
- contentRef.current.scrollTop = contentRef.current.scrollHeight
720
- }, [thoughts, isLive, isOpen])
721
-
722
- if (thoughts.length === 0 && !showEmptyHint) return null
723
-
724
- if (thoughts.length === 0) {
725
- return (
726
- <div className="thinking-block thinking-block-empty-state" data-live={isLive ? 'true' : 'false'}>
727
- <span className="thinking-block-empty">{THINKING_LABELS.emptyHint}</span>
728
- </div>
729
- )
730
- }
731
-
732
- return (
733
- <div className={`thinking-block${isLive ? ' is-live' : ''}`}>
734
- <button
735
- type="button"
736
- className="thinking-block-header"
737
- onClick={() => setOpen(!isOpen)}
738
- aria-expanded={isOpen}
739
- >
740
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}>
741
- <path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
742
- <line x1="12" y1="17" x2="12.01" y2="17" />
743
- <circle cx="12" cy="12" r="10" />
744
- </svg>
745
- <span className="thinking-block-label">
746
- {isLive ? THINKING_LABELS.live : THINKING_LABELS.completed(thoughts.length)}
747
- </span>
748
- {isLive && <span className="thinking-block-live-dot" aria-hidden="true" />}
749
- <span className={`thinking-block-chevron${isOpen ? ' is-open' : ''}`} aria-hidden="true">
750
- <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
751
- <polyline points="9 18 15 12 9 6" />
752
- </svg>
753
- </span>
754
- </button>
755
- {isOpen && (
756
- <div className="thinking-block-body">
757
- <div className="thinking-block-content" ref={contentRef}>
758
- {thoughts.map((thought, index) => (
759
- <div className="thinking-block-step" key={`${blockKey}-${index}`}>
760
- {thoughts.length > 1 && (
761
- <div className="thinking-block-step-label">{THINKING_LABELS.step(index + 1)}</div>
762
- )}
763
- <div className="thinking-block-step-body">{renderFormattedMessage(thought)}</div>
764
- </div>
765
- ))}
766
- </div>
767
- </div>
768
- )}
769
- </div>
770
- )
771
- }
772
-
773
- function readableAssistantText(text: string) {
774
- if (typeof text !== 'string') return ''
775
- const trimmed = text.trim()
776
- if (!trimmed.startsWith('{')) return text
777
- try {
778
- const value = JSON.parse(trimmed)
779
- if (value?.action === 'finish' && typeof value?.input?.summary === 'string' && value.input.summary.trim()) {
780
- return value.input.summary
781
- }
782
- if (typeof value?.finish?.summary === 'string' && value.finish.summary.trim()) {
783
- return value.finish.summary
784
- }
785
- } catch {
786
- return text
787
- }
788
- return text
789
- }
790
-
791
- function renderSpeakerIcon(isSpeaking: boolean) {
792
- if (isSpeaking) {
793
- return (
794
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'block' }}>
795
- <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
796
- <path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
797
- <path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
798
- </svg>
799
- )
800
- }
801
- return (
802
- <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" style={{ display: 'block' }}>
803
- <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
804
- </svg>
805
- )
806
- }
807
-
808
- function cleanSpeechText(text: string) {
809
- return readableAssistantText(text)
810
- .replace(/\*\*([\s\S]*?)\*\*/g, '$1')
811
- .replace(/[*_`#]/g, '')
812
- .trim()
813
- }
814
-
815
- function numericSetting(value: unknown, fallback: number) {
816
- const numeric = Number(value)
817
- return Number.isFinite(numeric) ? numeric : fallback
818
- }
819
-
820
- interface DiffHunk {
821
- oldText: string
822
- newText: string
823
- }
824
-
825
- interface FileChange {
826
- path: string
827
- created: boolean
828
- additions: number
829
- deletions: number
830
- hunks: DiffHunk[]
831
- }
832
-
833
- function parseFileChangesFromProgress(progress: AgentProgress[]): FileChange[] {
834
- const changes = new Map<string, FileChange>()
835
- let activeEdit: { action: string; path: string; created: boolean; additions: number; deletions: number; hunks: DiffHunk[] } | null = null
836
-
837
- for (const event of progress || []) {
838
- if (event.type === 'ToolStart') {
839
- if (event.data.action === 'apply_patch') {
840
- const patch = (event.data.input as any)?.patch
841
- if (patch && typeof patch.path === 'string') {
842
- let additions = 0
843
- let deletions = 0
844
- const hunksList: DiffHunk[] = []
845
- const hunks = patch.hunks
846
- if (Array.isArray(hunks)) {
847
- for (const hunk of hunks) {
848
- const oldText = hunk?.oldText || ''
849
- const newText = hunk?.newText || ''
850
- const oldLines = oldText ? oldText.split('\n').length : 0
851
- const newLines = newText ? newText.split('\n').length : 0
852
- deletions += oldLines
853
- additions += newLines
854
- hunksList.push({ oldText, newText })
855
- }
856
- }
857
- activeEdit = {
858
- action: 'apply_patch',
859
- path: patch.path,
860
- created: false,
861
- additions,
862
- deletions,
863
- hunks: hunksList
864
- }
865
- }
866
- } else if (event.data.action === 'write_file') {
867
- const path = (event.data.input as any)?.path
868
- const fileContent = (event.data.input as any)?.file_content || ''
869
- if (typeof path === 'string') {
870
- const additions = fileContent ? fileContent.split('\n').length : 0
871
- activeEdit = {
872
- action: 'write_file',
873
- path,
874
- created: true,
875
- additions,
876
- deletions: 0,
877
- hunks: [{ oldText: '', newText: fileContent }]
878
- }
879
- }
880
- } else {
881
- activeEdit = null
882
- }
883
- } else if (event.type === 'ToolEnd') {
884
- if (activeEdit && (event.data.action === 'apply_patch' || event.data.action === 'write_file')) {
885
- const isError = typeof event.data.result === 'string' && event.data.result.startsWith('Error:')
886
- if (!isError) {
887
- try {
888
- const applied = JSON.parse(event.data.result)
889
- const appliedPaths = Array.isArray(applied) ? applied.map(item => item?.path).filter(Boolean) : [activeEdit.path]
890
-
891
- for (const path of appliedPaths) {
892
- const existing = changes.get(path)
893
- if (existing) {
894
- existing.additions += activeEdit.additions
895
- existing.deletions += activeEdit.deletions
896
- existing.hunks.push(...activeEdit.hunks)
897
- } else {
898
- changes.set(path, {
899
- path,
900
- created: activeEdit.created,
901
- additions: activeEdit.additions,
902
- deletions: activeEdit.deletions,
903
- hunks: [...activeEdit.hunks]
904
- })
905
- }
906
- }
907
- } catch (e) {
908
- const path = activeEdit.path
909
- const existing = changes.get(path)
910
- if (existing) {
911
- existing.additions += activeEdit.additions
912
- existing.deletions += activeEdit.deletions
913
- existing.hunks.push(...activeEdit.hunks)
914
- } else {
915
- changes.set(path, {
916
- path,
917
- created: activeEdit.created,
918
- additions: activeEdit.additions,
919
- deletions: activeEdit.deletions,
920
- hunks: [...activeEdit.hunks]
921
- })
922
- }
923
- }
924
- }
925
- }
926
- activeEdit = null
927
- }
928
- }
929
-
930
- return Array.from(changes.values())
931
- }
932
77
 
933
78
  export default function ChatPanel({
934
79
  interactions,
@@ -955,7 +100,6 @@ export default function ChatPanel({
955
100
  onSelectImage,
956
101
  onSelectDocument,
957
102
  onPasteImage,
958
- onReadClipboardImage,
959
103
  onSetMessage,
960
104
  onSendVoiceMessage,
961
105
  onRemoveImage,
@@ -969,14 +113,45 @@ export default function ChatPanel({
969
113
  onApproval,
970
114
  settingsConfig,
971
115
  onSetModel,
116
+ onCancelMessage,
972
117
  }: ChatPanelProps) {
973
118
  const agentActivities = activitiesFrom(agentProgress)
974
119
  const activeFallbackNotice = fallbackNotice(streamedResponse)
120
+ const lastThinkingProgress = [...agentProgress].reverse().find(p => p.type === 'Thinking')
121
+ let activeAgentName: string | null = null
122
+ let activeModelName: string | null = null
123
+ if (lastThinkingProgress && lastThinkingProgress.type === 'Thinking') {
124
+ activeAgentName = (lastThinkingProgress.data as any).agent_name || null
125
+ activeModelName = (lastThinkingProgress.data as any).model_name || null
126
+ }
975
127
  const [openActivityIds, setOpenActivityIds] = useState<Record<string, boolean>>({})
976
128
  const [openReviewIds, setOpenReviewIds] = useState<Record<string, boolean>>({})
977
129
  const [openFileDiffs, setOpenFileDiffs] = useState<Record<string, boolean>>({})
978
130
  const [toolMenuOpen, setToolMenuOpen] = useState(false)
979
131
  const [elapsedSeconds, setElapsedSeconds] = useState(0)
132
+ const [dynamicOllamaModels, setDynamicOllamaModels] = useState<string[]>(OLLAMA_MODELS)
133
+
134
+ useEffect(() => {
135
+ const fetchOllamaModels = async () => {
136
+ if (status?.activeProvider !== 'ollama') return;
137
+ const host = settingsConfig?.ollamaHost || 'http://localhost:11434';
138
+ const cleanHost = host.endsWith('/') ? host.slice(0, -1) : host;
139
+ try {
140
+ const res = await fetch(`${cleanHost}/api/tags`);
141
+ if (res.ok) {
142
+ const data = await res.json();
143
+ if (data && Array.isArray(data.models)) {
144
+ setDynamicOllamaModels(data.models.map((m: any) => m.name));
145
+ return;
146
+ }
147
+ }
148
+ } catch (err) {
149
+ // fallback to default if fetch fails
150
+ }
151
+ setDynamicOllamaModels(OLLAMA_MODELS);
152
+ }
153
+ fetchOllamaModels();
154
+ }, [status?.activeProvider, settingsConfig?.ollamaHost])
980
155
 
981
156
  useEffect(() => {
982
157
  if (!sending) {
@@ -994,27 +169,37 @@ export default function ChatPanel({
994
169
 
995
170
  return () => clearInterval(timer)
996
171
  }, [sending])
997
- const [isRecording, setIsRecording] = useState(false)
998
- const [voiceMode, setVoiceMode] = useState(false)
999
- const [voiceTranscript, setVoiceTranscript] = useState('')
1000
- const [voiceAwaitingResponse, setVoiceAwaitingResponse] = useState(false)
1001
172
  const [speakingText, setSpeakingText] = useState<string | null>(null)
1002
173
  const toolMenuRef = useRef<HTMLDivElement | null>(null)
1003
- const recognitionRef = useRef<SpeechRecognition | null>(null)
1004
- const mediaRecorderRef = useRef<MediaRecorder | null>(null)
1005
- const mediaStreamRef = useRef<MediaStream | null>(null)
1006
- const audioContextRef = useRef<AudioContext | null>(null)
1007
- const vadTimerRef = useRef<number | null>(null)
1008
174
  const audioRef = useRef<HTMLAudioElement | null>(null)
1009
175
  const speechRunRef = useRef(0)
1010
176
  const historyReadyRef = useRef(false)
1011
177
  const submittedDuringSessionRef = useRef(false)
1012
178
  const lastAutoSpokenIdRef = useRef<number | string | null>(null)
1013
- const voiceModeRef = useRef(false)
1014
- const sendingRef = useRef(false)
1015
- const voiceAwaitingResponseRef = useRef(false)
1016
179
  const speakingRef = useRef<string | null>(null)
1017
- const restartTimerRef = useRef<number | null>(null)
180
+
181
+ const {
182
+ isRecording,
183
+ voiceMode,
184
+ setVoiceMode,
185
+ voiceTranscript,
186
+ setVoiceTranscript,
187
+ voiceAwaitingResponse,
188
+ voiceAwaitingResponseRef,
189
+ voiceModeRef,
190
+ startRecognition,
191
+ stopRecognition,
192
+ scheduleVoiceListen,
193
+ clearRestartTimer
194
+ } = useSpeechToText({
195
+ language: settingsConfig?.language,
196
+ message,
197
+ sending,
198
+ isSpeaking: Boolean(speakingText),
199
+ onSendVoiceMessage,
200
+ onSetMessage: (val) => onSetMessage(val)
201
+ })
202
+
1018
203
  const canSubmit = Boolean(message.trim() || imageAttachments.length > 0 || documentName)
1019
204
  const sendingImageMarkers = Array.from({ length: sendingImageCount }, (_, index) => `[Image #${index + 1}]`).join(' ')
1020
205
  const voiceStatus = speakingText ? 'speaking' : (sending || voiceAwaitingResponse) ? 'thinking' : isRecording ? 'listening' : voiceMode ? 'ready' : 'off'
@@ -1037,7 +222,7 @@ export default function ChatPanel({
1037
222
  case 'local_openai':
1038
223
  return LOCAL_MODELS
1039
224
  case 'ollama':
1040
- return OLLAMA_MODELS
225
+ return dynamicOllamaModels
1041
226
  default:
1042
227
  return []
1043
228
  }
@@ -1081,39 +266,7 @@ export default function ChatPanel({
1081
266
  const handleInputKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
1082
267
  submitOnEnter(event)
1083
268
  }
1084
- const clearRestartTimer = () => {
1085
- if (restartTimerRef.current === null) return
1086
- window.clearTimeout(restartTimerRef.current)
1087
- restartTimerRef.current = null
1088
- }
1089
- const clearVadTimer = () => {
1090
- if (vadTimerRef.current === null) return
1091
- window.clearInterval(vadTimerRef.current)
1092
- vadTimerRef.current = null
1093
- }
1094
- const stopRecognition = () => {
1095
- clearRestartTimer()
1096
- recognitionRef.current?.stop()
1097
- recognitionRef.current = null
1098
- clearVadTimer()
1099
- if (mediaRecorderRef.current?.state === 'recording') {
1100
- mediaRecorderRef.current.stop()
1101
- }
1102
- mediaRecorderRef.current = null
1103
- mediaStreamRef.current?.getTracks().forEach((track) => track.stop())
1104
- mediaStreamRef.current = null
1105
- audioContextRef.current?.close().catch(() => {})
1106
- audioContextRef.current = null
1107
- setIsRecording(false)
1108
- }
1109
- const scheduleVoiceListen = (delayMs = 350) => {
1110
- clearRestartTimer()
1111
- if (!voiceModeRef.current || sendingRef.current || voiceAwaitingResponseRef.current || speakingRef.current) return
1112
- restartTimerRef.current = window.setTimeout(() => {
1113
- restartTimerRef.current = null
1114
- startRecognition(true)
1115
- }, delayMs)
1116
- }
269
+
1117
270
  const cancelSpeech = () => {
1118
271
  speechRunRef.current += 1
1119
272
  audioRef.current?.pause()
@@ -1135,7 +288,11 @@ export default function ChatPanel({
1135
288
  utterance.volume = Math.max(0, Math.min(1, numericSetting(settingsConfig?.ttsVolume, 1)))
1136
289
  utterance.rate = Math.max(0.1, Math.min(10, numericSetting(settingsConfig?.ttsSpeed, 1)))
1137
290
  utterance.pitch = Math.max(0, Math.min(2, numericSetting(settingsConfig?.ttsPitch, 1)))
1138
- const voice = window.speechSynthesis.getVoices().find((item) => item.lang.startsWith(hasThai ? 'th' : 'en'))
291
+ const voice = window.speechSynthesis.getVoices().find((item) => {
292
+ const lang = item.lang.toLowerCase()
293
+ const target = hasThai ? 'th' : 'en'
294
+ return lang.startsWith(target) || lang.includes(target)
295
+ })
1139
296
  if (voice) utterance.voice = voice
1140
297
  const finishSpeech = () => {
1141
298
  setSpeakingText((current) => (current === displayText ? null : current))
@@ -1190,183 +347,16 @@ export default function ChatPanel({
1190
347
  }
1191
348
  speakNative(speechText, text)
1192
349
  }
1193
- const blobToDataUri = (blob: Blob) => new Promise<string>((resolve, reject) => {
1194
- const reader = new FileReader()
1195
- reader.onerror = () => reject(reader.error ?? new Error('Failed to read audio recording'))
1196
- reader.onload = () => resolve(String(reader.result))
1197
- reader.readAsDataURL(blob)
1198
- })
1199
- const preferredAudioMimeType = () => {
1200
- const candidates = ['audio/webm;codecs=opus', 'audio/webm', 'audio/ogg;codecs=opus', 'audio/ogg']
1201
- return candidates.find((mimeType) => MediaRecorder.isTypeSupported(mimeType)) ?? ''
1202
- }
1203
- const startAudioRecording = async (autoSend: boolean) => {
1204
- if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === 'undefined') {
1205
- alert('Sorry, this system cannot access the microphone or record audio in this WebView')
1206
- voiceModeRef.current = false
1207
- setVoiceMode(false)
1208
- return
1209
- }
1210
-
1211
- try {
1212
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
1213
- const mimeType = preferredAudioMimeType()
1214
- const recorder = new MediaRecorder(stream, mimeType ? { mimeType } : undefined)
1215
- const chunks: Blob[] = []
1216
- let heardVoice = false
1217
- let quietSince = 0
1218
- const startedAt = Date.now()
1219
- const audioContext = new AudioContext()
1220
- const source = audioContext.createMediaStreamSource(stream)
1221
- const analyser = audioContext.createAnalyser()
1222
- const samples = new Uint8Array(analyser.fftSize)
1223
- source.connect(analyser)
1224
-
1225
- mediaStreamRef.current = stream
1226
- mediaRecorderRef.current = recorder
1227
- audioContextRef.current = audioContext
1228
- setVoiceTranscript('Listening to microphone...')
1229
350
 
1230
- recorder.ondataavailable = (event) => {
1231
- if (event.data.size > 0) chunks.push(event.data)
1232
- }
1233
- recorder.onstop = async () => {
1234
- clearVadTimer()
1235
- mediaRecorderRef.current = null
1236
- mediaStreamRef.current?.getTracks().forEach((track) => track.stop())
1237
- mediaStreamRef.current = null
1238
- audioContextRef.current?.close().catch(() => {})
1239
- audioContextRef.current = null
1240
- setIsRecording(false)
1241
- if (!autoSend || !voiceModeRef.current || chunks.length === 0) return
1242
- if (!heardVoice) {
1243
- setVoiceTranscript('No speech detected')
1244
- scheduleVoiceListen(700)
1245
- return
1246
- }
1247
- const blob = new Blob(chunks, { type: recorder.mimeType || 'audio/webm' })
1248
- const audioDataUri = await blobToDataUri(blob)
1249
- setVoiceTranscript('Sent audio to AI')
1250
- voiceAwaitingResponseRef.current = true
1251
- setVoiceAwaitingResponse(true)
1252
- onSendVoiceMessage('', audioDataUri)
1253
- .catch((error) => console.error('Voice audio message failed', error))
1254
- .finally(() => {
1255
- voiceAwaitingResponseRef.current = false
1256
- setVoiceAwaitingResponse(false)
1257
- scheduleVoiceListen()
1258
- })
1259
- }
1260
-
1261
- recorder.start()
1262
- setIsRecording(true)
1263
- vadTimerRef.current = window.setInterval(() => {
1264
- analyser.getByteTimeDomainData(samples)
1265
- let peak = 0
1266
- for (const sample of samples) {
1267
- peak = Math.max(peak, Math.abs(sample - 128))
1268
- }
1269
- const now = Date.now()
1270
- if (peak > 12) {
1271
- heardVoice = true
1272
- quietSince = 0
1273
- setVoiceTranscript('Listening...')
1274
- } else if (heardVoice) {
1275
- quietSince = quietSince || now
1276
- }
1277
- const silenceElapsed = quietSince ? now - quietSince : 0
1278
- const totalElapsed = now - startedAt
1279
- if ((heardVoice && silenceElapsed > 1300) || totalElapsed > 12000) {
1280
- recorder.stop()
1281
- }
1282
- }, 120)
1283
- } catch (error) {
1284
- console.error('Failed to record microphone audio', error)
1285
- setIsRecording(false)
1286
- voiceModeRef.current = false
1287
- setVoiceMode(false)
1288
- alert('Failed to open microphone. Please check microphone permissions for this app')
1289
- }
1290
- }
1291
- const startRecognition = (autoSend: boolean) => {
1292
- if (recognitionRef.current || sendingRef.current || voiceAwaitingResponseRef.current || speakingRef.current) return
1293
-
1294
- const SpeechRecognitionApi = window.SpeechRecognition || window.webkitSpeechRecognition
1295
- if (!SpeechRecognitionApi) {
1296
- startAudioRecording(autoSend)
1297
- return
1298
- }
1299
-
1300
- let sentTranscript = false
1301
-
1302
- try {
1303
- const recognition = new SpeechRecognitionApi()
1304
- recognition.continuous = false
1305
- recognition.interimResults = true
1306
- recognition.lang = settingsConfig?.language === 'en' ? 'en-US' : 'th-TH'
1307
- recognition.onstart = () => setIsRecording(true)
1308
- recognition.onresult = (event: SpeechRecognitionEvent) => {
1309
- let interimText = ''
1310
- let finalText = ''
1311
- for (let index = event.resultIndex; index < event.results.length; index += 1) {
1312
- const transcript = event.results[index]?.[0]?.transcript ?? ''
1313
- if (event.results[index]?.isFinal) finalText += transcript
1314
- else interimText += transcript
1315
- }
1316
- const displayText = (finalText || interimText).trim()
1317
- if (displayText) setVoiceTranscript(displayText)
1318
- const resultText = finalText.trim()
1319
- if (!resultText) return
1320
- sentTranscript = true
1321
- recognition.stop()
1322
- setVoiceTranscript(resultText)
1323
- if (autoSend) {
1324
- voiceAwaitingResponseRef.current = true
1325
- setVoiceAwaitingResponse(true)
1326
- onSendVoiceMessage(resultText)
1327
- .catch((error) => console.error('Voice message failed', error))
1328
- .finally(() => {
1329
- voiceAwaitingResponseRef.current = false
1330
- setVoiceAwaitingResponse(false)
1331
- scheduleVoiceListen()
1332
- })
1333
- } else {
1334
- onSetMessage(message.trim() ? `${message.trimEnd()} ${resultText}` : resultText)
1335
- }
1336
- }
1337
- recognition.onerror = (event: Event) => {
1338
- console.error('Speech recognition error', event)
1339
- setIsRecording(false)
1340
- }
1341
- recognition.onend = () => {
1342
- recognitionRef.current = null
1343
- setIsRecording(false)
1344
- if (autoSend && voiceModeRef.current && !sentTranscript) scheduleVoiceListen()
1345
- }
1346
- recognitionRef.current = recognition
1347
- recognition.start()
1348
- } catch (error) {
1349
- console.error('Failed to start speech recognition', error)
1350
- recognitionRef.current = null
1351
- setIsRecording(false)
1352
- }
1353
- }
1354
351
  const toggleRecording = () => {
1355
352
  if (voiceMode) {
1356
- voiceModeRef.current = false
1357
- voiceAwaitingResponseRef.current = false
1358
353
  setVoiceMode(false)
1359
- setVoiceAwaitingResponse(false)
1360
- setVoiceTranscript('')
1361
354
  stopRecognition()
1362
355
  cancelSpeech()
1363
356
  return
1364
357
  }
1365
358
 
1366
- voiceModeRef.current = true
1367
359
  setVoiceMode(true)
1368
- setVoiceAwaitingResponse(false)
1369
- setVoiceTranscript('')
1370
360
  startRecognition(true)
1371
361
  }
1372
362
  const resizeInput = (element: HTMLTextAreaElement) => {
@@ -1375,31 +365,9 @@ export default function ChatPanel({
1375
365
  }
1376
366
  useEffect(() => {
1377
367
  return () => {
1378
- clearRestartTimer()
1379
368
  cancelSpeech()
1380
- stopRecognition()
1381
369
  }
1382
370
  }, [])
1383
- useEffect(() => {
1384
- voiceModeRef.current = voiceMode
1385
- if (!voiceMode) {
1386
- clearRestartTimer()
1387
- setVoiceTranscript('')
1388
- }
1389
- }, [voiceMode])
1390
- useEffect(() => {
1391
- sendingRef.current = sending
1392
- }, [sending])
1393
- useEffect(() => {
1394
- voiceAwaitingResponseRef.current = voiceAwaitingResponse
1395
- }, [voiceAwaitingResponse])
1396
- useEffect(() => {
1397
- speakingRef.current = speakingText
1398
- }, [speakingText])
1399
- useEffect(() => {
1400
- if (!voiceMode || sending || voiceAwaitingResponse || speakingText || isRecording) return
1401
- scheduleVoiceListen()
1402
- }, [voiceMode, sending, voiceAwaitingResponse, speakingText, isRecording])
1403
371
  useEffect(() => {
1404
372
  if (sending) submittedDuringSessionRef.current = true
1405
373
  }, [sending])
@@ -1437,21 +405,20 @@ export default function ChatPanel({
1437
405
  if (onPasteImage(event.clipboardData)) {
1438
406
  event.preventDefault()
1439
407
  event.stopPropagation()
1440
- } else {
1441
- window.setTimeout(() => void onReadClipboardImage(), 0)
1442
408
  }
1443
409
  }
1444
410
  window.addEventListener('paste', handleWindowPaste, true)
1445
411
  return () => window.removeEventListener('paste', handleWindowPaste, true)
1446
- }, [onPasteImage, onReadClipboardImage])
412
+ }, [onPasteImage])
1447
413
  useEffect(() => {
1448
- const handleWindowKeyDown = (event: globalThis.KeyboardEvent) => {
1449
- if (!(event.ctrlKey || event.metaKey) || event.key.toLowerCase() !== 'v') return
1450
- window.setTimeout(() => void onReadClipboardImage(), 0)
414
+ const handleEscapeKeyDown = (event: globalThis.KeyboardEvent) => {
415
+ if (event.key === 'Escape' && sending) {
416
+ onCancelMessage()
417
+ }
1451
418
  }
1452
- window.addEventListener('keydown', handleWindowKeyDown, true)
1453
- return () => window.removeEventListener('keydown', handleWindowKeyDown, true)
1454
- }, [onReadClipboardImage])
419
+ window.addEventListener('keydown', handleEscapeKeyDown, true)
420
+ return () => window.removeEventListener('keydown', handleEscapeKeyDown, true)
421
+ }, [sending, onCancelMessage])
1455
422
  useEffect(() => {
1456
423
  if (message) return
1457
424
  const input = document.getElementById('chat-input') as HTMLTextAreaElement | null
@@ -1473,7 +440,7 @@ export default function ChatPanel({
1473
440
  const appendWorkspaceReference = (reference: string) => {
1474
441
  const trimmed = reference.trim()
1475
442
  if (!trimmed) return
1476
- onSetMessage(message.trim() ? `${message.trimEnd()} ${trimmed}` : trimmed)
443
+ onSetMessage(message.trim() ? `${message.trimEnd()} ${trimmed} ` : `${trimmed} `)
1477
444
  }
1478
445
  const handleWorkspaceDrop = (event: DragEvent<HTMLElement>) => {
1479
446
  const reference =
@@ -1487,24 +454,72 @@ export default function ChatPanel({
1487
454
  const renderCompletedActivity = (interaction: any) => {
1488
455
  const interactionId = String(interaction.id)
1489
456
  const activityView = activitiesFrom(agentActivitySnapshots[interactionId] ?? [])
1490
- if (activityView.items.length === 0) return null
1491
457
  const isOpen = Boolean(openActivityIds[interactionId])
1492
458
  return (
1493
- <div className="agent-activity-history">
1494
- <button
1495
- type="button"
1496
- className="agent-activity-toggle"
1497
- aria-expanded={isOpen}
1498
- onClick={() => setOpenActivityIds((current) => ({ ...current, [interactionId]: !current[interactionId] }))}
1499
- >
1500
- <span>{activityView.summary}</span>
1501
- <span aria-hidden="true">{isOpen ? '^' : '>'}</span>
1502
- </button>
1503
- {isOpen && (
1504
- <div className="agent-activity-card agent-activity-card-history">
1505
- {renderAgentActivityTable(activityView)}
1506
- </div>
1507
- )}
459
+ <AgentActivityDrawer
460
+ activityView={activityView}
461
+ isOpen={isOpen}
462
+ onToggle={() => setOpenActivityIds((current) => ({ ...current, [interactionId]: !current[interactionId] }))}
463
+ isHistorical={true}
464
+ />
465
+ )
466
+ }
467
+
468
+ const renderWebSearchSources = (interaction: any) => {
469
+ const interactionId = String(interaction.id)
470
+ const progress = agentActivitySnapshots[interactionId] ?? interaction.agentActivity ?? []
471
+ const sources = parseWebSearchSources(progress)
472
+ if (sources.length === 0) return null
473
+ return (
474
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '6px', marginBottom: '8px' }}>
475
+ <span style={{ fontSize: '0.72rem', color: '#64748b', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.05em' }}>Sources</span>
476
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
477
+ {sources.map((src, i) => (
478
+ <a
479
+ key={i}
480
+ href={src.url}
481
+ target="_blank"
482
+ rel="noopener noreferrer"
483
+ title={src.snippet || src.title}
484
+ style={{
485
+ display: 'flex',
486
+ alignItems: 'center',
487
+ gap: '6px',
488
+ padding: '5px 10px',
489
+ background: 'rgba(255,255,255,0.04)',
490
+ border: '1px solid rgba(255,255,255,0.08)',
491
+ borderRadius: '8px',
492
+ textDecoration: 'none',
493
+ color: '#cbd5e1',
494
+ fontSize: '0.78rem',
495
+ maxWidth: '220px',
496
+ overflow: 'hidden',
497
+ cursor: 'pointer',
498
+ transition: 'background 0.15s, border-color 0.15s',
499
+ }}
500
+ onMouseEnter={e => {
501
+ (e.currentTarget as HTMLAnchorElement).style.background = 'rgba(255,255,255,0.08)'
502
+ ;(e.currentTarget as HTMLAnchorElement).style.borderColor = 'rgba(255,255,255,0.18)'
503
+ }}
504
+ onMouseLeave={e => {
505
+ (e.currentTarget as HTMLAnchorElement).style.background = 'rgba(255,255,255,0.04)'
506
+ ;(e.currentTarget as HTMLAnchorElement).style.borderColor = 'rgba(255,255,255,0.08)'
507
+ }}
508
+ >
509
+ <img
510
+ src={src.faviconUrl}
511
+ alt=""
512
+ width={14}
513
+ height={14}
514
+ style={{ borderRadius: '2px', flexShrink: 0 }}
515
+ onError={e => { (e.currentTarget as HTMLImageElement).style.display = 'none' }}
516
+ />
517
+ <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
518
+ {src.domain}
519
+ </span>
520
+ </a>
521
+ ))}
522
+ </div>
1508
523
  </div>
1509
524
  )
1510
525
  }
@@ -1701,6 +716,12 @@ export default function ChatPanel({
1701
716
 
1702
717
  return (
1703
718
  <section className={`conversation-panel ${isEmptyChat ? 'is-empty' : ''}`}>
719
+ <div className="chat-header">
720
+ <div className="chat-header-title">
721
+ <img src="./assets/icon.png" alt="Logo" className="chat-header-logo" />
722
+ <span>Mint Agent</span>
723
+ </div>
724
+ </div>
1704
725
  <div className="chat-container">
1705
726
  {interactions.map((interaction) => {
1706
727
  const isSystemEvent = interaction.provider === 'system' && interaction.model === 'provider_change';
@@ -1746,6 +767,7 @@ export default function ChatPanel({
1746
767
  )
1747
768
  })()}
1748
769
  <div className="message-bubble" style={{ whiteSpace: 'pre-wrap' }}>{renderFormattedMessage(interaction.aiText)}</div>
770
+ {renderWebSearchSources(interaction)}
1749
771
  <div className="message-time" style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
1750
772
  <button className="provider-badge">{interaction.provider} • {interaction.model}</button>
1751
773
  {fallbackNotice(interaction) && <span className="provider-fallback-notice">{fallbackNotice(interaction)}</span>}
@@ -1768,18 +790,13 @@ export default function ChatPanel({
1768
790
  {sending && (
1769
791
  <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginBottom: '16px' }}>
1770
792
  <div className="message user-message"><div className="bubble-wrapper"><div className="message-bubble">{sendingImageMarkers ? renderFormattedMessage(`${sendingMessage} ${sendingImageMarkers}`) : renderFormattedMessage(sendingMessage)}</div></div></div>
1771
- {agentMode && agentActivities.items.length > 0 && (
1772
- <div className="message ai-message agent-activity-message">
1773
- <div className="agent-activity-card">
1774
- <div className="agent-activity-header">
1775
- <span>{agentActivities.summary}</span>
1776
- <span className="agent-activity-status" data-state={pendingApproval ? 'approval' : 'active'}>
1777
- {pendingApproval ? 'Waiting for approval' : 'Working'}
1778
- </span>
1779
- </div>
1780
- {renderAgentActivityTable(agentActivities)}
1781
- </div>
1782
- </div>
793
+ {agentMode && (
794
+ <AgentActivityDrawer
795
+ activityView={agentActivities}
796
+ isOpen={openActivityIds['live'] ?? true}
797
+ onToggle={() => setOpenActivityIds((current) => ({ ...current, live: !(current['live'] ?? true) }))}
798
+ pendingApproval={!!pendingApproval}
799
+ />
1783
800
  )}
1784
801
  {renderActiveFileChanges()}
1785
802
  <div className="message ai-message thinking-message">
@@ -1816,7 +833,12 @@ export default function ChatPanel({
1816
833
  />
1817
834
  </path>
1818
835
  </svg>
1819
- <span>Thinking for {elapsedSeconds}s</span>
836
+ <span>
837
+ {activeAgentName && activeModelName
838
+ ? `${activeAgentName} (${activeModelName}) is thinking... (${elapsedSeconds}s)`
839
+ : `Thinking for ${elapsedSeconds}s (Esc to cancel)`
840
+ }
841
+ </span>
1820
842
  </div>
1821
843
  )}
1822
844
  </span>
@@ -1842,42 +864,17 @@ export default function ChatPanel({
1842
864
  </div>
1843
865
  )}
1844
866
 
1845
- {pendingApproval && (() => {
1846
- const details = renderApprovalDetails(pendingApproval.approval)
1847
- const writeFile = pendingApproval.approval?.WriteFile
1848
- const applyPatch = pendingApproval.approval?.ApplyPatch
1849
- const diffText = writeFile?.diff || applyPatch?.diff
1850
-
1851
- return (
1852
- <div className="message ai-message" style={{ width: '100%' }}>
1853
- <div className="bubble-wrapper" style={{ width: '100%' }}>
1854
- <div className="action-card approval-card" data-tier={details.isDangerous ? 'dangerous' : undefined} style={{ width: '100%' }}>
1855
- <div className="approval-card-content" style={{ width: '100%' }}>
1856
- <div className="approval-card-title">{details.title}</div>
1857
- <div className="approval-card-body" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.body}</div>
1858
- {diffText ? (
1859
- <div className="approval-card-diff-container" style={{ marginTop: '8px', width: '100%' }}>
1860
- <div style={{ fontSize: '0.8rem', fontWeight: 600, color: 'var(--text-soft, #94a3b8)', marginBottom: '4px' }}>Diff:</div>
1861
- {renderDiff(diffText)}
1862
- </div>
1863
- ) : (
1864
- details.reason && <div className="approval-card-reason" style={{ whiteSpace: 'pre-wrap', fontFamily: 'monospace' }}>{details.reason}</div>
1865
- )}
1866
- </div>
1867
- <div className="approval-card-actions">
1868
- <button type="button" className="approval-btn approval-btn-approve" onClick={() => onApproval(true)}>Approve</button>
1869
- <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>
1870
- <button type="button" className="approval-btn approval-btn-cancel" onClick={() => onApproval(false)}>Cancel</button>
1871
- </div>
1872
- </div>
1873
- </div>
1874
- </div>
1875
- )
1876
- })()}
867
+ {pendingApproval && (
868
+ <ApprovalCard
869
+ key={pendingApproval.id || JSON.stringify(pendingApproval)}
870
+ pendingApproval={pendingApproval}
871
+ onApproval={onApproval}
872
+ />
873
+ )}
1877
874
  <div ref={chatEnd} />
1878
875
  </div>
1879
876
 
1880
- <div className="input-area">
877
+ <div className={`input-area ${voiceMode ? 'voice-active' : ''}`}>
1881
878
  {isEmptyChat && <div className="empty-chat-prompt">Mint Agent is ready to work</div>}
1882
879
  <button type="button" className="workspace-select-btn" onClick={onSelectWorkspace}>
1883
880
  <span aria-hidden="true" style={{ display: 'inline-flex', alignItems: 'center' }}>
@@ -1907,8 +904,21 @@ export default function ChatPanel({
1907
904
  {voiceMode && (
1908
905
  <div className="voice-mode-bar" data-state={voiceStatus}>
1909
906
  <span className="voice-mode-dot" />
1910
- <span>{voiceStatusLabel}</span>
1911
- {voiceTranscript && <span className="voice-mode-transcript">{voiceTranscript}</span>}
907
+ {voiceTranscript ? (
908
+ voiceTranscript === 'Listening to microphone...' ||
909
+ voiceTranscript === 'Listening...' ||
910
+ voiceTranscript === 'No speech detected' ||
911
+ voiceTranscript === 'Sent audio to AI' ? (
912
+ <span>{voiceTranscript}</span>
913
+ ) : (
914
+ <span style={{ display: 'flex', alignItems: 'center', gap: '4px', minWidth: 0, overflow: 'hidden' }}>
915
+ <span>{voiceStatusLabel}:</span>
916
+ <span className="voice-mode-transcript">"{voiceTranscript}"</span>
917
+ </span>
918
+ )
919
+ ) : (
920
+ <span>{voiceStatusLabel}</span>
921
+ )}
1912
922
  </div>
1913
923
  )}
1914
924
 
@@ -2048,7 +1058,7 @@ export default function ChatPanel({
2048
1058
  return <option key={provider} value={provider}>{displayName}</option>
2049
1059
  })}
2050
1060
  </select>
2051
- {availableModels.length > 0 && (
1061
+ {(availableModels.length > 0 || activeModel) && (
2052
1062
  <select
2053
1063
  value={activeModel}
2054
1064
  onChange={(event) => onSetModel(event.target.value)}
@@ -2098,14 +1108,32 @@ export default function ChatPanel({
2098
1108
  </svg>
2099
1109
  )}
2100
1110
  </button>
2101
- <button id="send-btn" type="submit" disabled={sending || !canSubmit} style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
2102
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
2103
- <line x1="22" y1="2" x2="11" y2="13"></line>
2104
- <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
2105
- </svg>
2106
- </button>
1111
+ {sending ? (
1112
+ <button
1113
+ id="send-btn"
1114
+ className="stop-btn"
1115
+ type="button"
1116
+ onClick={onCancelMessage}
1117
+ title="Stop generating"
1118
+ style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}
1119
+ >
1120
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
1121
+ <rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect>
1122
+ </svg>
1123
+ </button>
1124
+ ) : (
1125
+ <button id="send-btn" type="submit" disabled={!canSubmit} style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>
1126
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
1127
+ <line x1="22" y1="2" x2="11" y2="13"></line>
1128
+ <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
1129
+ </svg>
1130
+ </button>
1131
+ )}
2107
1132
  </form>
2108
1133
  </div>
1134
+ <p className="input-disclaimer">
1135
+ Mint Agent is an AI gateway. Responses via third-party APIs. Verify critical info.
1136
+ </p>
2109
1137
  </section>
2110
1138
  )
2111
1139
  }