1- import { Check , ChevronDown , Download , Loader2 , Plus , RefreshCw , Trash2 , X } from "lucide-react" ;
1+ import { Check , ChevronDown , Plus , Trash2 , X } from "lucide-react" ;
22import type React from "react" ;
33import { useCallback , useEffect , useRef , useState } from "react" ;
44import { AGENT_PLUGINS , getAgentPlugin } from "./agents/registry" ;
5- import { useAgentInstall , useClickOutside , useEscapeKey , useNodeRuntime } from "./hooks" ;
5+ import { useClickOutside , useEscapeKey , useNodeRuntime } from "./hooks" ;
66
77interface SettingsModalProps {
88 isOpen : boolean ;
@@ -35,6 +35,9 @@ const SettingsModal: React.FC<SettingsModalProps> = ({
3535 const [ newEnvVal , setNewEnvVal ] = useState ( "" ) ;
3636 const [ activeTab , setActiveTab ] = useState < "agents" | "general" > ( "agents" ) ;
3737 const [ isAgentDropdownOpen , setIsAgentDropdownOpen ] = useState ( false ) ;
38+ const [ pluginInstallStatuses , setPluginInstallStatuses ] = useState <
39+ Record < string , string >
40+ > ( { } ) ;
3841
3942 const selectedPlugin = getAgentPlugin ( selectedPluginId ) ;
4043
@@ -53,13 +56,32 @@ const SettingsModal: React.FC<SettingsModalProps> = ({
5356 restart,
5457 } = useNodeRuntime ( isOpen ) ;
5558
56- const {
57- installStatus,
58- installedVersion,
59- install : handleInstall ,
60- update : handleUpdate ,
61- uninstall : handleUninstall ,
62- } = useAgentInstall ( selectedPlugin , isOpen , onAgentCommandChange ) ;
59+ // Check install status for all plugins
60+ useEffect ( ( ) => {
61+ const checkAllPlugins = async ( ) => {
62+ const statuses : Record < string , string > = { } ;
63+ for ( const plugin of AGENT_PLUGINS ) {
64+ try {
65+ if ( plugin . checkCommand ) {
66+ const res = await window . electron . invoke (
67+ "agent:check-command" ,
68+ plugin . checkCommand ,
69+ ) ;
70+ statuses [ plugin . id ] = res . installed ? "installed" : "not-installed" ;
71+ } else {
72+ statuses [ plugin . id ] = "installed" ;
73+ }
74+ } catch {
75+ statuses [ plugin . id ] = "not-installed" ;
76+ }
77+ }
78+ setPluginInstallStatuses ( statuses ) ;
79+ } ;
80+
81+ if ( isOpen ) {
82+ checkAllPlugins ( ) ;
83+ }
84+ } , [ isOpen ] ) ;
6385
6486 // Close handlers
6587 useEscapeKey ( onClose , isOpen ) ;
@@ -75,7 +97,8 @@ const SettingsModal: React.FC<SettingsModalProps> = ({
7597 useEffect ( ( ) => {
7698 if ( ! isOpen ) return ;
7799 const found = AGENT_PLUGINS . find ( ( plugin ) => {
78- const heuristic = plugin . checkCommand || plugin . defaultCommand . split ( " " ) [ 0 ] ;
100+ const heuristic =
101+ plugin . checkCommand || plugin . defaultCommand . split ( " " ) [ 0 ] ;
79102 return agentCommand . includes ( heuristic ) ;
80103 } ) ;
81104 setSelectedPluginId ( found ?. id ?? "custom" ) ;
@@ -107,18 +130,6 @@ const SettingsModal: React.FC<SettingsModalProps> = ({
107130 [ onAgentCommandChange ] ,
108131 ) ;
109132
110- const handleAuthTerminal = useCallback ( async ( ) => {
111- try {
112- const cmd = selectedPlugin ?. checkCommand || agentCommand . split ( " " ) [ 0 ] || agentCommand . trim ( ) ;
113- const res = await window . electron . invoke ( "agent:auth-terminal" , cmd , currentWorkspace ) ;
114- if ( ! res ?. success ) {
115- console . error ( `Failed to launch terminal: ${ res ?. error || "unknown error" } ` ) ;
116- }
117- } catch ( e : any ) {
118- console . error ( `Failed to launch terminal: ${ e . message } ` ) ;
119- }
120- } , [ selectedPlugin , agentCommand , currentWorkspace ] ) ;
121-
122133 if ( ! isOpen ) return null ;
123134
124135 return (
@@ -156,19 +167,24 @@ const SettingsModal: React.FC<SettingsModalProps> = ({
156167 < div className = "settings-content" >
157168 < div className = "settings-content-header" >
158169 < h2 className = "settings-title" >
159- { activeTab === "agents" ? "Agents Configuration" : "General Settings" }
170+ { activeTab === "agents"
171+ ? "Agents Configuration"
172+ : "General Settings" }
160173 </ h2 >
161174 < button type = "button" onClick = { onClose } className = "modal-close-btn" >
162175 < X size = { 20 } />
163176 </ button >
164177 </ div >
165178
166179 { activeTab === "general" && (
167- < div style = { { display : "flex" , flexDirection : "column" , gap : "20px" } } >
180+ < div
181+ style = { { display : "flex" , flexDirection : "column" , gap : "20px" } }
182+ >
168183 < div className = "modal-section" >
169184 < label className = "modal-label" > Node Runtime</ label >
170185 < span className = "modal-input-hint" >
171- Choose the runtime used to launch JS agents. Changes apply after restart.
186+ Choose the runtime used to launch JS agents. Changes apply
187+ after restart.
172188 </ span >
173189 < div className = "preset-buttons" style = { { maxWidth : "420px" } } >
174190 < button
@@ -189,7 +205,10 @@ const SettingsModal: React.FC<SettingsModalProps> = ({
189205
190206 { nodeRuntime === "custom" && (
191207 < >
192- < label className = "modal-label" style = { { marginTop : "16px" } } >
208+ < label
209+ className = "modal-label"
210+ style = { { marginTop : "16px" } }
211+ >
193212 Custom Node.js Path
194213 </ label >
195214 < div
@@ -207,7 +226,11 @@ const SettingsModal: React.FC<SettingsModalProps> = ({
207226 value = { customNodePath }
208227 onChange = { ( e ) => setCustomNodePath ( e . target . value ) }
209228 />
210- < button type = "button" className = "btn-secondary" onClick = { browseNodePath } >
229+ < button
230+ type = "button"
231+ className = "btn-secondary"
232+ onClick = { browseNodePath }
233+ >
211234 Browse...
212235 </ button >
213236 </ div >
@@ -218,7 +241,9 @@ const SettingsModal: React.FC<SettingsModalProps> = ({
218241 ) }
219242
220243 { runtimeError && (
221- < div style = { { marginTop : "12px" , color : "var(--error)" } } > { runtimeError } </ div >
244+ < div style = { { marginTop : "12px" , color : "var(--error)" } } >
245+ { runtimeError }
246+ </ div >
222247 ) }
223248 { runtimeSaved && ! runtimeError && (
224249 < div
@@ -251,7 +276,10 @@ const SettingsModal: React.FC<SettingsModalProps> = ({
251276 type = "button"
252277 className = "btn-primary"
253278 onClick = { applyRuntime }
254- disabled = { runtimeSaving || ( nodeRuntime === "custom" && ! customNodePath . trim ( ) ) }
279+ disabled = {
280+ runtimeSaving ||
281+ ( nodeRuntime === "custom" && ! customNodePath . trim ( ) )
282+ }
255283 >
256284 { runtimeSaving ? "Saving..." : "Apply" }
257285 </ button >
@@ -275,8 +303,9 @@ const SettingsModal: React.FC<SettingsModalProps> = ({
275303 opacity : 0.9 ,
276304 } }
277305 >
278- No Node.js runtime found. Some agents may fail to start. If using a custom
279- path, verify it; otherwise ensure the bundled runtime is available.
306+ No Node.js runtime found. Some agents may fail to start.
307+ If using a custom path, verify it; otherwise ensure the
308+ bundled runtime is available.
280309 </ div >
281310 </ div >
282311 </ div >
@@ -291,7 +320,9 @@ const SettingsModal: React.FC<SettingsModalProps> = ({
291320 className = "custom-select-trigger"
292321 onClick = { ( ) => setIsAgentDropdownOpen ( ! isAgentDropdownOpen ) }
293322 >
294- < span > { selectedPlugin ? selectedPlugin . name : "Custom Agent" } </ span >
323+ < span >
324+ { selectedPlugin ? selectedPlugin . name : "Custom Agent" }
325+ </ span >
295326 < ChevronDown
296327 size = { 16 }
297328 className = { `select-arrow ${ isAgentDropdownOpen ? "open" : "" } ` }
@@ -321,79 +352,47 @@ const SettingsModal: React.FC<SettingsModalProps> = ({
321352 setIsAgentDropdownOpen ( false ) ;
322353 } }
323354 >
324- { plugin . name }
325- { selectedPluginId === plugin . id && < Check size = { 14 } /> }
355+ < div
356+ style = { {
357+ display : "flex" ,
358+ alignItems : "center" ,
359+ gap : "8px" ,
360+ } }
361+ >
362+ { plugin . name }
363+ { pluginInstallStatuses [ plugin . id ] ===
364+ "installed" && (
365+ < span
366+ style = { {
367+ fontSize : "0.75rem" ,
368+ color : "var(--text-secondary)" ,
369+ } }
370+ >
371+ (已安装)
372+ </ span >
373+ ) }
374+ { pluginInstallStatuses [ plugin . id ] ===
375+ "not-installed" && (
376+ < span
377+ style = { {
378+ fontSize : "0.75rem" ,
379+ color : "var(--error)" ,
380+ } }
381+ >
382+ (未安装)
383+ </ span >
384+ ) }
385+ </ div >
386+ { selectedPluginId === plugin . id && (
387+ < Check size = { 14 } />
388+ ) }
326389 </ button >
327390 ) ) }
328391 </ div >
329392 ) }
330393 </ div >
331394 </ div >
332395
333- { /* Plugin Status & Install */ }
334- { selectedPlugin && selectedPlugin . packageSpec && (
335- < div className = "status-box" style = { { marginTop : 0 , marginBottom : "20px" } } >
336- < div className = "status-info" >
337- < span className = "status-label" > Status:</ span >
338- { installStatus === "checking" && (
339- < span className = "status-text checking" > Checking...</ span >
340- ) }
341- { installStatus === "installed" && (
342- < span className = "status-text installed" >
343- < Check size = { 16 } /> Installed
344- { installedVersion && (
345- < span style = { { opacity : 0.7 , fontSize : "0.85rem" } } >
346- v{ installedVersion }
347- </ span >
348- ) }
349- </ span >
350- ) }
351- { installStatus === "not-installed" && (
352- < span className = "status-text not-installed" > Not Installed</ span >
353- ) }
354- { installStatus === "installing" && (
355- < span className = "status-text processing" >
356- < Loader2 size = { 16 } className = "animate-spin" /> Installing...
357- </ span >
358- ) }
359- { installStatus === "updating" && (
360- < span className = "status-text processing" >
361- < Loader2 size = { 16 } className = "animate-spin" /> Updating...
362- </ span >
363- ) }
364- { installStatus === "uninstalling" && (
365- < span className = "status-text processing" >
366- < Loader2 size = { 16 } className = "animate-spin" /> Uninstalling...
367- </ span >
368- ) }
369- </ div >
370-
371- { installStatus === "not-installed" && (
372- < button type = "button" onClick = { handleInstall } className = "btn-primary btn-small" >
373- < Download size = { 14 } /> Install
374- </ button >
375- ) }
376- { installStatus === "installed" && (
377- < div style = { { display : "flex" , gap : "8px" } } >
378- < button
379- type = "button"
380- onClick = { handleUpdate }
381- className = "btn-primary btn-small"
382- >
383- < RefreshCw size = { 14 } /> Update
384- </ button >
385- < button
386- type = "button"
387- onClick = { handleUninstall }
388- className = "btn-danger-outline"
389- >
390- < Trash2 size = { 14 } /> Uninstall
391- </ button >
392- </ div >
393- ) }
394- </ div >
395- ) }
396-
397396 { /* Command Input */ }
398397 < div className = "modal-section" >
399398 < label htmlFor = "agent-command" className = "modal-label" >
@@ -420,7 +419,12 @@ const SettingsModal: React.FC<SettingsModalProps> = ({
420419 { Object . entries ( agentEnv ) . map ( ( [ key , val ] ) => (
421420 < div key = { key } className = "env-row" >
422421 < input readOnly value = { key } className = "env-input key" />
423- < input readOnly value = { val } type = "password" className = "env-input val" />
422+ < input
423+ readOnly
424+ value = { val }
425+ type = "password"
426+ className = "env-input val"
427+ />
424428 < button
425429 type = "button"
426430 onClick = { ( ) => removeEnvVar ( key ) }
@@ -456,23 +460,13 @@ const SettingsModal: React.FC<SettingsModalProps> = ({
456460 </ div >
457461
458462 < div className = "modal-footer" style = { { marginTop : "auto" } } >
459- { selectedPlugin && selectedPlugin . packageSpec && installStatus === "installed" ? (
460- < div style = { { marginRight : "auto" } } >
461- < button
462- type = "button"
463- onClick = { handleAuthTerminal }
464- title = "Open system terminal to login manually (type '/auth')"
465- className = "btn-secondary"
466- >
467- < span style = { { fontSize : "1.1em" } } > 🔑</ span > Authenticate in Terminal
468- </ button >
469- </ div >
470- ) : null }
471463 < button
472464 type = "button"
473465 onClick = { onConnectToggle }
474466 className = "btn-primary"
475- style = { isConnected ? { backgroundColor : "#ef4444" } : undefined }
467+ style = {
468+ isConnected ? { backgroundColor : "#ef4444" } : undefined
469+ }
476470 >
477471 { isConnected ? "Disconnect" : "Connect & Save" }
478472 </ button >
0 commit comments