Skip to content

Commit 7801ce7

Browse files
committed
feat: add dialog for windows deps
1 parent 023103f commit 7801ce7

File tree

15 files changed

+310
-69
lines changed

15 files changed

+310
-69
lines changed

src/main/presenter/configPresenter/acpInitHelper.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -193,16 +193,19 @@ class AcpInitHelper {
193193

194194
// Check external dependencies before initialization
195195
const missingDeps = await this.checkRequiredDependencies(agentId)
196-
if (missingDeps.length > 0 && webContents && !webContents.isDestroyed()) {
197-
console.log('[ACP Init] Missing dependencies detected, sending notification:', {
196+
if (missingDeps.length > 0) {
197+
console.log('[ACP Init] Missing dependencies detected, blocking initialization:', {
198198
agentId,
199199
missingCount: missingDeps.length
200200
})
201-
webContents.send('external-deps-required', {
202-
agentId,
203-
missingDeps
204-
})
205-
// Continue with initialization anyway - user can install dependencies manually
201+
if (webContents && !webContents.isDestroyed()) {
202+
webContents.send('external-deps-required', {
203+
agentId,
204+
missingDeps
205+
})
206+
}
207+
// Stop initialization - user must install dependencies first
208+
return null
206209
}
207210

208211
const initConfig = BUILTIN_INIT_COMMANDS[agentId]

src/main/presenter/configPresenter/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1057,14 +1057,19 @@ export class ConfigPresenter implements IConfigPresenter {
10571057
throw new Error(`No active profile found for agent: ${agentId}`)
10581058
}
10591059

1060-
await initializeBuiltinAgent(
1060+
const result = await initializeBuiltinAgent(
10611061
agentId as AcpBuiltinAgentId,
10621062
activeProfile,
10631063
useBuiltinRuntime,
10641064
npmRegistry,
10651065
uvRegistry,
10661066
webContents
10671067
)
1068+
// If initialization returns null, it means dependencies are missing
1069+
// The event has already been sent to frontend, just return without error
1070+
if (result === null) {
1071+
return
1072+
}
10681073
} else {
10691074
// Get custom agent
10701075
const customs = await this.getAcpCustomAgents()
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
<template>
2+
<Dialog :open="open" @update:open="emit('update:open', $event)">
3+
<DialogContent class="sm:max-w-2xl max-h-[85vh] flex flex-col">
4+
<DialogHeader>
5+
<DialogTitle>{{ t('settings.acp.dependency.title') }}</DialogTitle>
6+
<DialogDescription>
7+
{{ t('settings.acp.dependency.description') }}
8+
</DialogDescription>
9+
</DialogHeader>
10+
11+
<div class="flex-1 overflow-y-auto space-y-4 pr-2">
12+
<div
13+
v-for="(dep, index) in dependencies"
14+
:key="index"
15+
class="border rounded-lg p-4 space-y-3 bg-zinc-50 dark:bg-zinc-900"
16+
>
17+
<div>
18+
<h3 class="font-semibold text-lg text-foreground">{{ dep.name }}</h3>
19+
<p class="text-sm text-muted-foreground mt-1">{{ dep.description }}</p>
20+
</div>
21+
22+
<!-- Installation Commands -->
23+
<div
24+
v-if="dep.installCommands && hasInstallCommands(dep.installCommands)"
25+
class="space-y-2"
26+
>
27+
<Label class="text-sm font-medium">{{
28+
t('settings.acp.dependency.installCommands')
29+
}}</Label>
30+
<div class="space-y-2">
31+
<template v-for="(command, cmdType) in dep.installCommands" :key="cmdType">
32+
<div v-if="command" class="flex items-center gap-2">
33+
<div class="flex-1 flex items-center gap-2 bg-background border rounded-md p-2">
34+
<code class="flex-1 text-sm font-mono text-foreground break-all">{{
35+
command
36+
}}</code>
37+
<Button
38+
variant="ghost"
39+
size="icon"
40+
class="h-8 w-8 shrink-0"
41+
@click="copyToClipboard(command)"
42+
:title="t('settings.acp.dependency.copy')"
43+
>
44+
<Icon icon="lucide:copy" class="h-4 w-4" />
45+
</Button>
46+
</div>
47+
</div>
48+
</template>
49+
</div>
50+
</div>
51+
52+
<!-- Download URL -->
53+
<div v-if="dep.downloadUrl" class="space-y-2">
54+
<Label class="text-sm font-medium">{{
55+
t('settings.acp.dependency.downloadUrl')
56+
}}</Label>
57+
<div class="flex items-center gap-2 bg-background border rounded-md p-2">
58+
<a
59+
:href="dep.downloadUrl"
60+
target="_blank"
61+
rel="noopener noreferrer"
62+
class="flex-1 text-sm text-primary hover:underline break-all"
63+
>
64+
{{ dep.downloadUrl }}
65+
</a>
66+
<Button
67+
variant="ghost"
68+
size="icon"
69+
class="h-8 w-8 shrink-0"
70+
@click="copyToClipboard(dep.downloadUrl)"
71+
:title="t('settings.acp.dependency.copy')"
72+
>
73+
<Icon icon="lucide:copy" class="h-4 w-4" />
74+
</Button>
75+
</div>
76+
</div>
77+
</div>
78+
</div>
79+
80+
<DialogFooter class="mt-4">
81+
<Button @click="emit('update:open', false)">
82+
{{ t('common.close') }}
83+
</Button>
84+
</DialogFooter>
85+
</DialogContent>
86+
</Dialog>
87+
</template>
88+
89+
<script setup lang="ts">
90+
import { useI18n } from 'vue-i18n'
91+
import { useToast } from '@/components/use-toast'
92+
import {
93+
Dialog,
94+
DialogContent,
95+
DialogDescription,
96+
DialogFooter,
97+
DialogHeader,
98+
DialogTitle
99+
} from '@shadcn/components/ui/dialog'
100+
import { Button } from '@shadcn/components/ui/button'
101+
import { Label } from '@shadcn/components/ui/label'
102+
import { Icon } from '@iconify/vue'
103+
104+
interface ExternalDependency {
105+
name: string
106+
description: string
107+
platform?: string[]
108+
checkCommand?: string
109+
checkPaths?: string[]
110+
installCommands?: {
111+
winget?: string
112+
chocolatey?: string
113+
scoop?: string
114+
}
115+
downloadUrl?: string
116+
requiredFor?: string[]
117+
}
118+
119+
defineProps<{
120+
open: boolean
121+
dependencies: ExternalDependency[]
122+
}>()
123+
124+
const emit = defineEmits<{
125+
(e: 'update:open', value: boolean): void
126+
}>()
127+
128+
const { t } = useI18n()
129+
const { toast } = useToast()
130+
131+
const hasInstallCommands = (commands: ExternalDependency['installCommands']): boolean => {
132+
if (!commands) return false
133+
return Boolean(commands.winget || commands.chocolatey || commands.scoop)
134+
}
135+
136+
const copyToClipboard = async (text: string) => {
137+
try {
138+
if (window.api?.copyText) {
139+
window.api.copyText(text)
140+
toast({
141+
title: t('settings.acp.dependency.copied'),
142+
duration: 2000
143+
})
144+
} else if (navigator.clipboard) {
145+
await navigator.clipboard.writeText(text)
146+
toast({
147+
title: t('settings.acp.dependency.copied'),
148+
duration: 2000
149+
})
150+
} else {
151+
console.warn('[AcpDependencyDialog] Clipboard API not available')
152+
}
153+
} catch (error) {
154+
console.error('[AcpDependencyDialog] Failed to copy to clipboard:', error)
155+
toast({
156+
title: t('settings.acp.dependency.copyFailed'),
157+
variant: 'destructive'
158+
})
159+
}
160+
}
161+
</script>

src/renderer/settings/components/AcpSettings.vue

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,13 @@
260260
<AcpTerminalDialog
261261
:open="terminalDialogOpen"
262262
@update:open="(value) => (terminalDialogOpen = value)"
263+
@dependencies-required="handleDependenciesRequired"
264+
/>
265+
266+
<AcpDependencyDialog
267+
:open="dependencyDialogOpen"
268+
:dependencies="missingDependencies"
269+
@update:open="(value) => (dependencyDialogOpen = value)"
263270
/>
264271
</div>
265272
</template>
@@ -296,6 +303,7 @@ import {
296303
import AcpProfileDialog from './AcpProfileDialog.vue'
297304
import AcpProfileManagerDialog from './AcpProfileManagerDialog.vue'
298305
import AcpTerminalDialog from './AcpTerminalDialog.vue'
306+
import AcpDependencyDialog from './AcpDependencyDialog.vue'
299307

300308
const { t } = useI18n()
301309
const { toast } = useToast()
@@ -316,6 +324,23 @@ const builtinPending = reactive<Record<string, boolean>>({})
316324
const customPending = reactive<Record<string, boolean>>({})
317325
const initializing = reactive<Record<string, boolean>>({})
318326
const terminalDialogOpen = ref(false)
327+
const dependencyDialogOpen = ref(false)
328+
const missingDependencies = ref<
329+
Array<{
330+
name: string
331+
description: string
332+
platform?: string[]
333+
checkCommand?: string
334+
checkPaths?: string[]
335+
installCommands?: {
336+
winget?: string
337+
chocolatey?: string
338+
scoop?: string
339+
}
340+
downloadUrl?: string
341+
requiredFor?: string[]
342+
}>
343+
>([])
319344

320345
const profileDialogState = reactive({
321346
open: false,
@@ -679,6 +704,13 @@ const setInitializing = (agentId: string, isBuiltin: boolean, state: boolean) =>
679704
}
680705
}
681706

707+
const handleDependenciesRequired = (dependencies: typeof missingDependencies.value) => {
708+
console.log('[AcpSettings] Dependencies required:', dependencies)
709+
missingDependencies.value = dependencies
710+
dependencyDialogOpen.value = true
711+
terminalDialogOpen.value = false
712+
}
713+
682714
const handleInitializeAgent = async (agentId: string, isBuiltin: boolean) => {
683715
if (isInitializing(agentId, isBuiltin)) return
684716

src/renderer/settings/components/AcpTerminalDialog.vue

Lines changed: 11 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ const props = defineProps<{
7676
const emit = defineEmits<{
7777
(e: 'update:open', value: boolean): void
7878
(e: 'close'): void
79+
(e: 'dependencies-required', dependencies: ExternalDependency[]): void
7980
}>()
8081

8182
const { t } = useI18n()
@@ -310,58 +311,12 @@ const handleExternalDepsRequired = (
310311
return
311312
}
312313

313-
// Build dependency information message
314-
const depMessages = data.missingDeps.map((dep) => {
315-
let message = `${dep.name}: ${dep.description}\n`
314+
// Emit event to parent to show dependency dialog
315+
emit('dependencies-required', data.missingDeps)
316316

317-
// Add installation commands
318-
if (dep.installCommands) {
319-
const commands: string[] = []
320-
if (dep.installCommands.winget) {
321-
commands.push(`winget: ${dep.installCommands.winget}`)
322-
}
323-
if (dep.installCommands.chocolatey) {
324-
commands.push(`chocolatey: ${dep.installCommands.chocolatey}`)
325-
}
326-
if (dep.installCommands.scoop) {
327-
commands.push(`scoop: ${dep.installCommands.scoop}`)
328-
}
329-
if (commands.length > 0) {
330-
message += `\nInstall commands:\n${commands.join('\n')}`
331-
}
332-
}
333-
334-
// Add download URL
335-
if (dep.downloadUrl) {
336-
message += `\nDownload: ${dep.downloadUrl}`
337-
}
338-
339-
return message
340-
})
341-
342-
const fullMessage = `Missing external dependencies:\n\n${depMessages.join('\n\n')}`
343-
344-
// Show toast notification with dependency information
345-
toast({
346-
title: t('settings.acp.terminal.missingDependencies'),
347-
description: fullMessage,
348-
duration: 10000, // Show for 10 seconds
349-
variant: 'default'
350-
})
351-
352-
// Also write to terminal
353-
if (terminal) {
354-
terminal.writeln(`\r\n\x1b[33m⚠ Missing external dependencies detected:\x1b[0m`)
355-
data.missingDeps.forEach((dep) => {
356-
terminal?.writeln(`\r\n\x1b[33m${dep.name}\x1b[0m: ${dep.description}`)
357-
if (dep.installCommands?.winget) {
358-
terminal?.writeln(` Install: ${dep.installCommands.winget}`)
359-
}
360-
if (dep.downloadUrl) {
361-
terminal?.writeln(` Download: ${dep.downloadUrl}`)
362-
}
363-
})
364-
}
317+
// Close terminal dialog since initialization is blocked
318+
emit('update:open', false)
319+
emit('close')
365320
}
366321

367322
const setupIpcListeners = () => {
@@ -451,6 +406,11 @@ onBeforeUnmount(() => {
451406
width: 100%;
452407
}
453408

409+
/* Hide default DialogContent close button */
410+
:deep([data-slot='dialog-close']) {
411+
display: none !important;
412+
}
413+
454414
/* Xterm.js styles */
455415
:deep(.xterm) {
456416
height: 100% !important;

src/renderer/src/i18n/en-US/settings.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -875,13 +875,21 @@
875875
"processError": "Process error",
876876
"paste": "Paste",
877877
"pasteError": "Failed to paste from clipboard",
878-
"missingDependencies": "Missing External Dependencies",
879878
"status": {
880879
"idle": "Idle",
881880
"running": "Running",
882881
"completed": "Completed",
883882
"error": "Error"
884883
}
884+
},
885+
"dependency": {
886+
"title": "Missing External Dependencies",
887+
"description": "The following dependencies need to be installed before initialization.",
888+
"installCommands": "Installation Commands",
889+
"downloadUrl": "Download Link",
890+
"copy": "Copy",
891+
"copied": "Copied to clipboard",
892+
"copyFailed": "Failed to copy"
885893
}
886894
},
887895
"rateLimit": {

src/renderer/src/i18n/fa-IR/settings.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -905,9 +905,17 @@
905905
},
906906
"title": "ترمینال را راه اندازی کنید",
907907
"waiting": "در انتظار شروع اولیه سازی...",
908-
"missingDependencies": "عدم وابستگی خارجی",
909908
"paste": "چسباندن",
910909
"pasteError": "جای‌گذاری از کلیپ بورد انجام نشد"
910+
},
911+
"dependency": {
912+
"copied": "در کلیپ بورد کپی شد",
913+
"copy": "کپی کنید",
914+
"copyFailed": "کپی ناموفق بود",
915+
"description": "وابستگی های زیر باید قبل از مقداردهی اولیه نصب شوند.",
916+
"downloadUrl": "لینک دانلود",
917+
"installCommands": "دستور نصب",
918+
"title": "عدم وابستگی خارجی"
911919
}
912920
}
913921
}

0 commit comments

Comments
 (0)