@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.
- package/.github/workflows/release.yml +1 -1
- package/Cargo.lock +34 -3
- package/Cargo.toml +2 -2
- package/README.md +20 -6
- package/Release_Note.md +129 -0
- package/assets/Mint_CLI.png +0 -0
- package/bin/mint +0 -0
- package/crates/mint-cli/Cargo.toml +1 -0
- package/crates/mint-cli/src/agent.rs +242 -54
- package/crates/mint-cli/src/main.rs +397 -74
- package/crates/mint-core/src/agent_loop.rs +1 -0
- package/crates/mint-core/src/api_server.rs +64 -2
- package/crates/mint-core/src/browser.rs +456 -0
- package/crates/mint-core/src/channels.rs +1 -0
- package/crates/mint-core/src/chat.rs +3 -0
- package/crates/mint-core/src/config.rs +130 -1
- package/crates/mint-core/src/lib.rs +19 -0
- package/crates/mint-core/src/orchestration.rs +317 -12
- package/crates/mint-core/src/skills.rs +113 -4
- package/crates/mint-core/src/tts.rs +3 -1
- package/crates/mint-core/src/workflows.rs +1 -22
- package/flatpak/com.pheem49.mint.desktop +9 -0
- package/flatpak/com.pheem49.mint.metainfo.xml +35 -0
- package/flatpak/com.pheem49.mint.yaml +52 -0
- package/package.json +1 -1
- package/shared-modules/.github/workflows/ci.yml +120 -0
- package/shared-modules/.github/workflows/update.yaml +28 -0
- package/shared-modules/CODEOWNERS +23 -0
- package/shared-modules/README.md +79 -0
- package/shared-modules/SDL/README.md +3 -0
- package/shared-modules/SDL/SDL-1.2.15.json +51 -0
- package/shared-modules/SDL/SDL_Pango-0.1.2-API-adds.patch +118 -0
- package/shared-modules/SDL/SDL_image-1.2.12.json +25 -0
- package/shared-modules/SDL/SDL_mixer-1.2.12.json +27 -0
- package/shared-modules/SDL/SDL_net-1.2.8.json +19 -0
- package/shared-modules/SDL/SDL_pango-0.1.2.json +26 -0
- package/shared-modules/SDL/SDL_ttf-2.0.11.json +28 -0
- package/shared-modules/SDL/libsdl-1.2.15-strict-prototypes.patch +316 -0
- package/shared-modules/SDL/sdl-check-for-SDL_VIDEO_X11_BACKINGSTORE.patch +24 -0
- package/shared-modules/SDL/sdl-libx11-build.patch +59 -0
- package/shared-modules/SDL/sdl-sysloadso-buffer-length.patch +27 -0
- package/shared-modules/SDL/sdl12-compat.json +22 -0
- package/shared-modules/SDL2/SDL2_image.json +27 -0
- package/shared-modules/SDL2/SDL2_ttf.json +29 -0
- package/shared-modules/cld2/CMakeLists.txt +157 -0
- package/shared-modules/cld2/cld2.json +24 -0
- package/shared-modules/clutter/clutter.json +72 -0
- package/shared-modules/dbus-glib/dbus-glib.json +29 -0
- package/shared-modules/glew/glew.json +36 -0
- package/shared-modules/glu/glu-9.json +12 -0
- package/shared-modules/gtk2/arc-gtk2-theme-Replace-Inkscape-dependency-with-rsvg-convert.patch +180 -0
- package/shared-modules/gtk2/gtk2-common-themes.json +360 -0
- package/shared-modules/gtk2/gtk2-fix-crash-in-show-uri.patch +30 -0
- package/shared-modules/gtk2/gtk2-gcc14.patch +281 -0
- package/shared-modules/gtk2/gtk2-gcc15.patch +250 -0
- package/shared-modules/gtk2/gtk2-murrine-c99.patch +34 -0
- package/shared-modules/gtk2/gtk2-use-adwaita-theme.patch +80 -0
- package/shared-modules/gtk2/gtk2.json +120 -0
- package/shared-modules/gtk2/ibus-gcc15.patch +13 -0
- package/shared-modules/gtk2/murrine-engine-fix-crash.patch +12 -0
- package/shared-modules/gtk2/readme.md +10 -0
- package/shared-modules/intltool/intltool-0.51.json +20 -0
- package/shared-modules/intltool/intltool-perl5.26-regex-fixes.patch +59 -0
- package/shared-modules/lame/0001-Build-xmm_quantize_sub.c-with-msse.patch +34 -0
- package/shared-modules/lame/0002-Include-GTK-1-autoconf-directives-in-build-system.patch +222 -0
- package/shared-modules/lame/0003-Patch-out-remaining-ansi2knr.patch +70 -0
- package/shared-modules/lame/0004-initscr-is-not-used-anywhere-in-lame-sourcetree.patch +41 -0
- package/shared-modules/lame/0005-Update-autoconf.patch +81 -0
- package/shared-modules/lame/lame-3.99.5.json +32 -0
- package/shared-modules/libappindicator/libappindicator-gtk2-12.10.json +140 -0
- package/shared-modules/libappindicator/libappindicator-gtk3-12.10.json +140 -0
- package/shared-modules/libappindicator/libappindicator-gtk3-introspection-12.10.json +140 -0
- package/shared-modules/libappindicator/patches/0001-libappindicator-ftbfs.patch +20 -0
- package/shared-modules/libappindicator/patches/0002-libappindicator-no-python.patch +888 -0
- package/shared-modules/libappindicator/patches/0003-libappindicator-fix-crash-from-incorrect-signal-emission.patch +74 -0
- package/shared-modules/libappindicator/patches/0004-libappindicator-fix-crash-iterating-icon-themes.patch +11 -0
- package/shared-modules/libayatana-appindicator/0001-Fix-HAVE_VALGRIND-AM_CONDITIONAL.patch +51 -0
- package/shared-modules/libayatana-appindicator/0001-Make-introspection-configurable.patch +44 -0
- package/shared-modules/libayatana-appindicator/libayatana-appindicator-gtk3.json +121 -0
- package/shared-modules/libcanberra/0001-gtk-Don-t-assume-all-GdkDisplays-are-GdkX11Displays-.patch +123 -0
- package/shared-modules/libcanberra/libcanberra.json +44 -0
- package/shared-modules/libmad/libmad.json +19 -0
- package/shared-modules/libsecret/libsecret.json +23 -0
- package/shared-modules/libsoup/libsoup-2.4.json +14 -0
- package/shared-modules/libusb/libusb.json +27 -0
- package/shared-modules/linux-audio/dssi.json +31 -0
- package/shared-modules/linux-audio/fftw3f-static.json +44 -0
- package/shared-modules/linux-audio/fftw3f.json +42 -0
- package/shared-modules/linux-audio/fluidsynth2-static.json +30 -0
- package/shared-modules/linux-audio/fluidsynth2.json +19 -0
- package/shared-modules/linux-audio/ladspa.json +26 -0
- package/shared-modules/linux-audio/lash.json +35 -0
- package/shared-modules/linux-audio/libinstpatch.json +16 -0
- package/shared-modules/linux-audio/liblo-static.json +22 -0
- package/shared-modules/linux-audio/liblo.json +17 -0
- package/shared-modules/linux-audio/lilv-static.json +94 -0
- package/shared-modules/linux-audio/lilv.json +82 -0
- package/shared-modules/linux-audio/lrdf.json +57 -0
- package/shared-modules/linux-audio/lv2.json +21 -0
- package/shared-modules/linux-audio/patches/lash/0001-configure-remove-aclocal-m4.patch +29 -0
- package/shared-modules/linux-audio/patches/lash/0002-Fix-build-with-recent-gcc.patch +73 -0
- package/shared-modules/linux-audio/patches/lash/0003-Fix-linking.patch +136 -0
- package/shared-modules/linux-audio/patches/raptor-libxml.patch +31 -0
- package/shared-modules/linux-audio/patches/stk-install.sh +54 -0
- package/shared-modules/linux-audio/stk.json +28 -0
- package/shared-modules/lua5.1/0004-Fix-stack-overflow-in-vararg-functions.patch +21 -0
- package/shared-modules/lua5.1/lua-5.1.5-so.patch +43 -0
- package/shared-modules/lua5.1/lua-5.1.5.json +42 -0
- package/shared-modules/lua5.3/lua-5.3.5.json +41 -0
- package/shared-modules/lua5.3/lua.pc.in +8 -0
- package/shared-modules/lua5.3/luaconf.patch +47 -0
- package/shared-modules/lua5.3/makefile-add-liblua.so-rule.patch +47 -0
- package/shared-modules/lua5.4/lua-5.4.json +41 -0
- package/shared-modules/lua5.4/lua.pc.in +8 -0
- package/shared-modules/lua5.4/luaconf.patch +34 -0
- package/shared-modules/lua5.4/makefile-add-liblua.so-rule.patch +42 -0
- package/shared-modules/luajit/luajit.json +30 -0
- package/shared-modules/lzo/lzo.json +27 -0
- package/shared-modules/mac/mac.json +18 -0
- package/shared-modules/physfs/physfs.json +26 -0
- package/shared-modules/pygame/audiofile-gcc6.patch +21 -0
- package/shared-modules/pygame/cython-fixes.patch +34 -0
- package/shared-modules/pygame/fluidsynth-no-rawmidi.patch +69 -0
- package/shared-modules/pygame/glib.patch +25 -0
- package/shared-modules/pygame/pygame1-python2.json +157 -0
- package/shared-modules/pygtk/pygtk-c99.patch +15 -0
- package/shared-modules/pygtk/pygtk.json +117 -0
- package/shared-modules/pygtk/pygtk.patch +39 -0
- package/shared-modules/python2.7/python-2.7.json +48 -0
- package/shared-modules/smpeg/smpeg-0.4.5.json +50 -0
- package/shared-modules/smpeg/smpeg-am-prog-as.patch +12 -0
- package/shared-modules/smpeg/smpeg-export-mpegaudio-class.patch +17 -0
- package/shared-modules/smpeg/smpeg-gcc6.patch +40 -0
- package/shared-modules/smpeg/smpeg-no-gtk.patch +37 -0
- package/shared-modules/squashfs-tools/squashfs-tools.json +25 -0
- package/shared-modules/update-checker.sh +50 -0
- package/shared-modules/vorbisgain/0001-temp_files.patch +75 -0
- package/shared-modules/vorbisgain/0002-errno.patch +33 -0
- package/shared-modules/vorbisgain/0003-manpage.patch +248 -0
- package/shared-modules/vorbisgain/0004-vorbisgain_mtime.patch +183 -0
- package/shared-modules/vorbisgain/0005-double_fclose.patch +21 -0
- package/shared-modules/vorbisgain/0006-manpage_hyphens.patch +56 -0
- package/shared-modules/vorbisgain/0007-recursively_spelling.patch +14 -0
- package/shared-modules/vorbisgain/0008-manpage_recursion_mistake.patch +23 -0
- package/shared-modules/vorbisgain/0009-hardening.patch +13 -0
- package/shared-modules/vorbisgain/0010-fclose.patch +13 -0
- package/shared-modules/vorbisgain/README.md +23 -0
- package/shared-modules/vorbisgain/vorbisgain_0.37-2.json +29 -0
- package/src/renderer/shared/agentProgress.ts +145 -0
- package/src/renderer/shared/calculator.ts +52 -0
- package/src/renderer/shared/components/AgentActivityDrawer.tsx +72 -0
- package/src/renderer/shared/components/AgentActivityTable.tsx +66 -0
- package/src/renderer/shared/components/ApprovalCard.tsx +93 -0
- package/src/renderer/shared/components/ChatCodeBlock.tsx +75 -0
- package/src/renderer/shared/components/ThinkingBlock.tsx +101 -0
- package/src/renderer/shared/constants/models.ts +91 -0
- package/src/renderer/shared/platform.ts +82 -0
- package/src/renderer/shared/types.ts +140 -0
- package/src/renderer/shared/utils/agentActivity.ts +187 -0
- package/src/renderer/shared/utils/approval.ts +1 -0
- package/src/renderer/shared/utils/approval.tsx +64 -0
- package/src/renderer/shared/utils/markdown.tsx +347 -0
- package/src/renderer/shared/utils/providers.ts +36 -0
- package/src/renderer/shared/utils/speech.ts +216 -0
- package/src/renderer/shared/utils/ui.ts +147 -0
- package/src/renderer/src/agentProgress.ts +11 -39
- package/src/renderer/src/calculator.ts +3 -46
- package/src/renderer/src/components/ChatPanel.tsx +230 -1202
- package/src/renderer/src/components/ImageStudioPanel.tsx +11 -1
- package/src/renderer/src/components/MintDashboard.tsx +63 -171
- package/src/renderer/src/components/PicturesLibrary.tsx +32 -8
- package/src/renderer/src/components/Settings/AgentsTab.tsx +362 -0
- package/src/renderer/src/components/Settings/AutomationTab.tsx +0 -22
- package/src/renderer/src/components/Settings/PluginsTab.tsx +221 -41
- package/src/renderer/src/components/SettingsWindow.tsx +54 -12
- package/src/renderer/src/components/WorkspacePanel.tsx +66 -9
- package/src/renderer/src/css/image-studio.css +6 -0
- package/src/renderer/src/css/styles.css +148 -11
- package/src/renderer/src/index.css +12 -1
- package/src/renderer/src/tauri.ts +169 -143
- package/src/renderer/src-web/agentProgress.ts +11 -39
- package/src/renderer/src-web/calculator.ts +3 -46
- package/src/renderer/src-web/components/ChatPanel.tsx +223 -1199
- package/src/renderer/src-web/components/ImageStudioPanel.tsx +11 -1
- package/src/renderer/src-web/components/MintDashboard.tsx +31 -172
- package/src/renderer/src-web/components/PicturesLibrary.tsx +23 -29
- package/src/renderer/src-web/components/Settings/AgentsTab.tsx +362 -0
- package/src/renderer/src-web/components/Settings/AutomationTab.tsx +0 -22
- package/src/renderer/src-web/components/Settings/PluginsTab.tsx +221 -41
- package/src/renderer/src-web/components/SettingsWindow.tsx +54 -12
- package/src/renderer/src-web/css/image-studio.css +6 -0
- package/src/renderer/src-web/css/styles.css +184 -12
- package/src/renderer/src-web/index.css +12 -1
- package/src/renderer/src-web/tauri.ts +204 -104
- package/src-tauri/src/browser.rs +1 -141
- package/src-tauri/src/headless.rs +3 -2
- package/src-tauri/src/lib.rs +254 -59
- package/src-tauri/src/webhooks.rs +1 -0
- package/src-tauri/src/workflows.rs +57 -4
- 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">></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
|
-
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
412
|
+
}, [onPasteImage])
|
|
1447
413
|
useEffect(() => {
|
|
1448
|
-
const
|
|
1449
|
-
if (
|
|
1450
|
-
|
|
414
|
+
const handleEscapeKeyDown = (event: globalThis.KeyboardEvent) => {
|
|
415
|
+
if (event.key === 'Escape' && sending) {
|
|
416
|
+
onCancelMessage()
|
|
417
|
+
}
|
|
1451
418
|
}
|
|
1452
|
-
window.addEventListener('keydown',
|
|
1453
|
-
return () => window.removeEventListener('keydown',
|
|
1454
|
-
}, [
|
|
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
|
-
<
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
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 &&
|
|
1772
|
-
<
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
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=
|
|
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
|
-
|
|
1911
|
-
|
|
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
|
-
|
|
2102
|
-
<
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
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
|
}
|