@octo-cyber/ai 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/dist/ai.module.d.ts +3 -0
  2. package/dist/ai.module.d.ts.map +1 -0
  3. package/dist/ai.module.js +40 -0
  4. package/dist/ai.module.js.map +1 -0
  5. package/dist/cli/adapters/antigravity.adapter.d.ts +29 -0
  6. package/dist/cli/adapters/antigravity.adapter.d.ts.map +1 -0
  7. package/dist/cli/adapters/antigravity.adapter.js +170 -0
  8. package/dist/cli/adapters/antigravity.adapter.js.map +1 -0
  9. package/dist/cli/adapters/claude-cli.adapter.d.ts +23 -0
  10. package/dist/cli/adapters/claude-cli.adapter.d.ts.map +1 -0
  11. package/dist/cli/adapters/claude-cli.adapter.js +79 -0
  12. package/dist/cli/adapters/claude-cli.adapter.js.map +1 -0
  13. package/dist/cli/adapters/codex-cli.adapter.d.ts +22 -0
  14. package/dist/cli/adapters/codex-cli.adapter.d.ts.map +1 -0
  15. package/dist/cli/adapters/codex-cli.adapter.js +76 -0
  16. package/dist/cli/adapters/codex-cli.adapter.js.map +1 -0
  17. package/dist/cli/adapters/detect-cli.d.ts +11 -0
  18. package/dist/cli/adapters/detect-cli.d.ts.map +1 -0
  19. package/dist/cli/adapters/detect-cli.js +40 -0
  20. package/dist/cli/adapters/detect-cli.js.map +1 -0
  21. package/dist/cli/adapters/gemini-cli.adapter.d.ts +21 -0
  22. package/dist/cli/adapters/gemini-cli.adapter.d.ts.map +1 -0
  23. package/dist/cli/adapters/gemini-cli.adapter.js +72 -0
  24. package/dist/cli/adapters/gemini-cli.adapter.js.map +1 -0
  25. package/dist/cli/adapters/index.d.ts +4 -0
  26. package/dist/cli/adapters/index.d.ts.map +1 -0
  27. package/dist/cli/adapters/index.js +10 -0
  28. package/dist/cli/adapters/index.js.map +1 -0
  29. package/dist/cli/cli-executor.service.d.ts +23 -0
  30. package/dist/cli/cli-executor.service.d.ts.map +1 -0
  31. package/dist/cli/cli-executor.service.js +150 -0
  32. package/dist/cli/cli-executor.service.js.map +1 -0
  33. package/dist/cli/cli-registry.service.d.ts +38 -0
  34. package/dist/cli/cli-registry.service.d.ts.map +1 -0
  35. package/dist/cli/cli-registry.service.js +112 -0
  36. package/dist/cli/cli-registry.service.js.map +1 -0
  37. package/dist/cli/controllers/ai-cli.controller.d.ts +36 -0
  38. package/dist/cli/controllers/ai-cli.controller.d.ts.map +1 -0
  39. package/dist/cli/controllers/ai-cli.controller.js +179 -0
  40. package/dist/cli/controllers/ai-cli.controller.js.map +1 -0
  41. package/dist/cli/index.d.ts +8 -0
  42. package/dist/cli/index.d.ts.map +1 -0
  43. package/dist/cli/index.js +17 -0
  44. package/dist/cli/index.js.map +1 -0
  45. package/dist/cli/types.d.ts +131 -0
  46. package/dist/cli/types.d.ts.map +1 -0
  47. package/dist/cli/types.js +9 -0
  48. package/dist/cli/types.js.map +1 -0
  49. package/dist/cli/workspace.service.d.ts +34 -0
  50. package/dist/cli/workspace.service.d.ts.map +1 -0
  51. package/dist/cli/workspace.service.js +194 -0
  52. package/dist/cli/workspace.service.js.map +1 -0
  53. package/dist/controllers/agent.controller.d.ts +13 -0
  54. package/dist/controllers/agent.controller.d.ts.map +1 -0
  55. package/dist/controllers/agent.controller.js +117 -0
  56. package/dist/controllers/agent.controller.js.map +1 -0
  57. package/dist/controllers/capability.controller.d.ts +16 -0
  58. package/dist/controllers/capability.controller.d.ts.map +1 -0
  59. package/dist/controllers/capability.controller.js +108 -0
  60. package/dist/controllers/capability.controller.js.map +1 -0
  61. package/dist/controllers/client.controller.d.ts +10 -0
  62. package/dist/controllers/client.controller.d.ts.map +1 -0
  63. package/dist/controllers/client.controller.js +79 -0
  64. package/dist/controllers/client.controller.js.map +1 -0
  65. package/dist/controllers/execution.controller.d.ts +11 -0
  66. package/dist/controllers/execution.controller.d.ts.map +1 -0
  67. package/dist/controllers/execution.controller.js +50 -0
  68. package/dist/controllers/execution.controller.js.map +1 -0
  69. package/dist/controllers/index.d.ts +5 -0
  70. package/dist/controllers/index.d.ts.map +1 -0
  71. package/dist/controllers/index.js +12 -0
  72. package/dist/controllers/index.js.map +1 -0
  73. package/dist/entities/agent-binding.entity.d.ts +17 -0
  74. package/dist/entities/agent-binding.entity.d.ts.map +1 -0
  75. package/dist/entities/agent-binding.entity.js +64 -0
  76. package/dist/entities/agent-binding.entity.js.map +1 -0
  77. package/dist/entities/agent-definition.entity.d.ts +17 -0
  78. package/dist/entities/agent-definition.entity.d.ts.map +1 -0
  79. package/dist/entities/agent-definition.entity.js +70 -0
  80. package/dist/entities/agent-definition.entity.js.map +1 -0
  81. package/dist/entities/agent-execution.entity.d.ts +23 -0
  82. package/dist/entities/agent-execution.entity.d.ts.map +1 -0
  83. package/dist/entities/agent-execution.entity.js +93 -0
  84. package/dist/entities/agent-execution.entity.js.map +1 -0
  85. package/dist/entities/agent-instance.entity.d.ts +19 -0
  86. package/dist/entities/agent-instance.entity.d.ts.map +1 -0
  87. package/dist/entities/agent-instance.entity.js +76 -0
  88. package/dist/entities/agent-instance.entity.js.map +1 -0
  89. package/dist/entities/capability-definition.entity.d.ts +20 -0
  90. package/dist/entities/capability-definition.entity.d.ts.map +1 -0
  91. package/dist/entities/capability-definition.entity.js +77 -0
  92. package/dist/entities/capability-definition.entity.js.map +1 -0
  93. package/dist/entities/client-registration.entity.d.ts +19 -0
  94. package/dist/entities/client-registration.entity.d.ts.map +1 -0
  95. package/dist/entities/client-registration.entity.js +79 -0
  96. package/dist/entities/client-registration.entity.js.map +1 -0
  97. package/dist/entities/index.d.ts +7 -0
  98. package/dist/entities/index.d.ts.map +1 -0
  99. package/dist/entities/index.js +16 -0
  100. package/dist/entities/index.js.map +1 -0
  101. package/dist/index.d.ts +4 -0
  102. package/dist/index.d.ts.map +1 -0
  103. package/dist/index.js +16 -0
  104. package/dist/index.js.map +1 -0
  105. package/dist/services/agent.service.d.ts +23 -0
  106. package/dist/services/agent.service.d.ts.map +1 -0
  107. package/dist/services/agent.service.js +76 -0
  108. package/dist/services/agent.service.js.map +1 -0
  109. package/dist/services/capability-router.service.d.ts +45 -0
  110. package/dist/services/capability-router.service.d.ts.map +1 -0
  111. package/dist/services/capability-router.service.js +120 -0
  112. package/dist/services/capability-router.service.js.map +1 -0
  113. package/dist/services/client-registration.service.d.ts +27 -0
  114. package/dist/services/client-registration.service.d.ts.map +1 -0
  115. package/dist/services/client-registration.service.js +83 -0
  116. package/dist/services/client-registration.service.js.map +1 -0
  117. package/dist/services/index.d.ts +5 -0
  118. package/dist/services/index.d.ts.map +1 -0
  119. package/dist/services/index.js +12 -0
  120. package/dist/services/index.js.map +1 -0
  121. package/dist/services/sync-bridge.service.d.ts +41 -0
  122. package/dist/services/sync-bridge.service.d.ts.map +1 -0
  123. package/dist/services/sync-bridge.service.js +118 -0
  124. package/dist/services/sync-bridge.service.js.map +1 -0
  125. package/package.json +94 -0
  126. package/web/components/CliTestDialog.tsx +158 -0
  127. package/web/index.ts +19 -0
  128. package/web/manifest.ts +14 -0
  129. package/web/messages/en-US.json +81 -0
  130. package/web/messages/zh-CN.json +81 -0
  131. package/web/pages/AiCliToolsPage.tsx +223 -0
  132. package/web/pages/WorkspaceSection.tsx +260 -0
  133. package/web/pages/WorkspacesPage.tsx +397 -0
  134. package/web/services/ai-cli-service.ts +129 -0
@@ -0,0 +1,260 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect, useCallback } from 'react'
4
+ import { useTranslations } from 'next-intl'
5
+ import { toast } from 'sonner'
6
+ import {
7
+ RefreshCw,
8
+ Trash2,
9
+ Plus,
10
+ FolderOpen,
11
+ Clock,
12
+ HardDrive,
13
+ Loader2,
14
+ } from 'lucide-react'
15
+
16
+ import { ConfirmDialog } from '@octo-cyber/ui/components/shared/confirm-dialog'
17
+ import { Button } from '@octo-cyber/ui/components/ui/button'
18
+ import { Badge } from '@octo-cyber/ui/components/ui/badge'
19
+ import { Input } from '@octo-cyber/ui/components/ui/input'
20
+ import { Label } from '@octo-cyber/ui/components/ui/label'
21
+ import {
22
+ Card,
23
+ CardContent,
24
+ CardHeader,
25
+ CardTitle,
26
+ } from '@octo-cyber/ui/components/ui/card'
27
+ import {
28
+ Dialog,
29
+ DialogContent,
30
+ DialogHeader,
31
+ DialogTitle,
32
+ DialogDescription,
33
+ DialogFooter,
34
+ } from '@octo-cyber/ui/components/ui/dialog'
35
+ import { aiCliService } from '../services/ai-cli-service'
36
+ import type { WorkspaceInfo } from '../services/ai-cli-service'
37
+
38
+ function formatDate(dateStr: string): string {
39
+ try {
40
+ return new Date(dateStr).toLocaleString()
41
+ } catch {
42
+ return dateStr
43
+ }
44
+ }
45
+
46
+ export default function WorkspaceSection() {
47
+ const t = useTranslations('aiCli.workspaces')
48
+ const tc = useTranslations('common')
49
+
50
+ const [workspaces, setWorkspaces] = useState<WorkspaceInfo[]>([])
51
+ const [loading, setLoading] = useState(true)
52
+ const [createDialogOpen, setCreateDialogOpen] = useState(false)
53
+ const [newWorkspaceName, setNewWorkspaceName] = useState('')
54
+ const [isCreating, setIsCreating] = useState(false)
55
+ const [isCleaning, setIsCleaning] = useState(false)
56
+ const [deleteTarget, setDeleteTarget] = useState<WorkspaceInfo | null>(null)
57
+
58
+ const loadWorkspaces = useCallback(async () => {
59
+ setLoading(true)
60
+ try {
61
+ const data = await aiCliService.getWorkspaces()
62
+ setWorkspaces(data)
63
+ } catch {
64
+ toast.error(t('toast.loadFailed'))
65
+ } finally {
66
+ setLoading(false)
67
+ }
68
+ }, [t])
69
+
70
+ useEffect(() => {
71
+ loadWorkspaces()
72
+ }, [loadWorkspaces])
73
+
74
+ const handleCreatePersistent = async () => {
75
+ if (!newWorkspaceName.trim()) return
76
+ setIsCreating(true)
77
+ try {
78
+ await aiCliService.createWorkspace('persistent', newWorkspaceName.trim())
79
+ setCreateDialogOpen(false)
80
+ setNewWorkspaceName('')
81
+ await loadWorkspaces()
82
+ toast.success(t('toast.created'))
83
+ } catch {
84
+ toast.error(t('toast.createFailed'))
85
+ } finally {
86
+ setIsCreating(false)
87
+ }
88
+ }
89
+
90
+ const handleDelete = async () => {
91
+ if (!deleteTarget) return
92
+ try {
93
+ await aiCliService.deleteWorkspace(deleteTarget.id)
94
+ setDeleteTarget(null)
95
+ await loadWorkspaces()
96
+ toast.success(t('toast.deleted'))
97
+ } catch {
98
+ toast.error(t('toast.deleteFailed'))
99
+ }
100
+ }
101
+
102
+ const handleCleanup = async () => {
103
+ setIsCleaning(true)
104
+ try {
105
+ const result = await aiCliService.cleanupWorkspaces()
106
+ toast.success(t('toast.cleaned', { count: result.cleaned }))
107
+ await loadWorkspaces()
108
+ } catch {
109
+ toast.error(t('toast.cleanFailed'))
110
+ } finally {
111
+ setIsCleaning(false)
112
+ }
113
+ }
114
+
115
+ const tempCount = workspaces.filter((w) => w.type === 'temp').length
116
+ const persistentCount = workspaces.filter((w) => w.type === 'persistent').length
117
+
118
+ return (
119
+ <div className="space-y-4">
120
+ <div className="flex items-center justify-between">
121
+ <div>
122
+ <h3 className="text-lg font-semibold">{t('title')}</h3>
123
+ <p className="text-sm text-muted-foreground">
124
+ {t('summary', { temp: tempCount, persistent: persistentCount })}
125
+ </p>
126
+ </div>
127
+ <div className="flex gap-2">
128
+ <Button
129
+ variant="outline"
130
+ size="sm"
131
+ onClick={handleCleanup}
132
+ disabled={isCleaning}
133
+ >
134
+ {isCleaning ? (
135
+ <Loader2 className="mr-1 h-3 w-3 animate-spin" />
136
+ ) : (
137
+ <Clock className="mr-1 h-3 w-3" />
138
+ )}
139
+ {t('actions.cleanup')}
140
+ </Button>
141
+ <Button
142
+ variant="outline"
143
+ size="sm"
144
+ onClick={() => {
145
+ setNewWorkspaceName('')
146
+ setCreateDialogOpen(true)
147
+ }}
148
+ >
149
+ <Plus className="mr-1 h-3 w-3" />
150
+ {t('actions.create')}
151
+ </Button>
152
+ <Button variant="ghost" size="sm" onClick={loadWorkspaces} disabled={loading}>
153
+ <RefreshCw className={`h-3 w-3 ${loading ? 'animate-spin' : ''}`} />
154
+ </Button>
155
+ </div>
156
+ </div>
157
+
158
+ {loading ? (
159
+ <div className="flex items-center justify-center h-32">
160
+ <p className="text-muted-foreground">{tc('loading')}</p>
161
+ </div>
162
+ ) : workspaces.length === 0 ? (
163
+ <div className="flex items-center justify-center h-32">
164
+ <p className="text-muted-foreground">{t('empty')}</p>
165
+ </div>
166
+ ) : (
167
+ <div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
168
+ {workspaces.map((ws) => (
169
+ <Card key={ws.id}>
170
+ <CardHeader className="pb-2">
171
+ <div className="flex items-center justify-between">
172
+ <div className="flex items-center gap-2">
173
+ {ws.type === 'persistent' ? (
174
+ <HardDrive className="h-4 w-4 text-muted-foreground" />
175
+ ) : (
176
+ <FolderOpen className="h-4 w-4 text-muted-foreground" />
177
+ )}
178
+ <CardTitle className="text-sm">
179
+ {ws.name || ws.id.slice(0, 8)}
180
+ </CardTitle>
181
+ </div>
182
+ <Badge variant={ws.type === 'persistent' ? 'default' : 'secondary'}>
183
+ {ws.type === 'persistent' ? t('type.persistent') : t('type.temp')}
184
+ </Badge>
185
+ </div>
186
+ </CardHeader>
187
+ <CardContent className="space-y-2">
188
+ <p className="text-xs text-muted-foreground truncate" title={ws.path}>
189
+ {ws.path}
190
+ </p>
191
+ <div className="flex items-center justify-between text-xs text-muted-foreground">
192
+ <span>{formatDate(ws.createdAt)}</span>
193
+ {ws.expiresAt && (
194
+ <span className="text-orange-500">
195
+ {t('expiresAt', { time: formatDate(ws.expiresAt) })}
196
+ </span>
197
+ )}
198
+ </div>
199
+ <div className="flex justify-end pt-1">
200
+ <Button
201
+ variant="ghost"
202
+ size="sm"
203
+ className="text-destructive hover:text-destructive"
204
+ onClick={() => setDeleteTarget(ws)}
205
+ >
206
+ <Trash2 className="mr-1 h-3 w-3" />
207
+ {tc('delete')}
208
+ </Button>
209
+ </div>
210
+ </CardContent>
211
+ </Card>
212
+ ))}
213
+ </div>
214
+ )}
215
+
216
+ <Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
217
+ <DialogContent>
218
+ <DialogHeader>
219
+ <DialogTitle>{t('createDialog.title')}</DialogTitle>
220
+ <DialogDescription>{t('createDialog.description')}</DialogDescription>
221
+ </DialogHeader>
222
+ <div className="space-y-4">
223
+ <div className="space-y-2">
224
+ <Label>{t('createDialog.nameLabel')}</Label>
225
+ <Input
226
+ placeholder={t('createDialog.namePlaceholder')}
227
+ value={newWorkspaceName}
228
+ onChange={(e) => setNewWorkspaceName(e.target.value)}
229
+ />
230
+ </div>
231
+ </div>
232
+ <DialogFooter>
233
+ <Button variant="outline" onClick={() => setCreateDialogOpen(false)}>
234
+ {tc('cancel')}
235
+ </Button>
236
+ <Button
237
+ onClick={handleCreatePersistent}
238
+ disabled={isCreating || !newWorkspaceName.trim()}
239
+ >
240
+ {isCreating ? (
241
+ <Loader2 className="mr-1 h-3 w-3 animate-spin" />
242
+ ) : null}
243
+ {tc('create')}
244
+ </Button>
245
+ </DialogFooter>
246
+ </DialogContent>
247
+ </Dialog>
248
+
249
+ <ConfirmDialog
250
+ open={!!deleteTarget}
251
+ onOpenChange={(open) => { if (!open) setDeleteTarget(null) }}
252
+ title={tc('confirmDelete')}
253
+ description={t('deleteConfirm', { name: deleteTarget?.name || deleteTarget?.id || '' })}
254
+ onConfirm={handleDelete}
255
+ confirmText={tc('delete')}
256
+ variant="destructive"
257
+ />
258
+ </div>
259
+ )
260
+ }
@@ -0,0 +1,397 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect, useCallback } from 'react'
4
+ import { useTranslations } from 'next-intl'
5
+ import { toast } from 'sonner'
6
+ import {
7
+ RefreshCw,
8
+ Trash2,
9
+ Plus,
10
+ FolderOpen,
11
+ Github,
12
+ Loader2,
13
+ GitPullRequest,
14
+ } from 'lucide-react'
15
+
16
+ import { PageHeader } from '@octo-cyber/ui/components/shared/page-header'
17
+ import { ConfirmDialog } from '@octo-cyber/ui/components/shared/confirm-dialog'
18
+ import { Button } from '@octo-cyber/ui/components/ui/button'
19
+ import { Badge } from '@octo-cyber/ui/components/ui/badge'
20
+ import { Input } from '@octo-cyber/ui/components/ui/input'
21
+ import { Label } from '@octo-cyber/ui/components/ui/label'
22
+ import {
23
+ Card,
24
+ CardContent,
25
+ CardHeader,
26
+ CardTitle,
27
+ } from '@octo-cyber/ui/components/ui/card'
28
+ import {
29
+ Dialog,
30
+ DialogContent,
31
+ DialogHeader,
32
+ DialogTitle,
33
+ DialogDescription,
34
+ DialogFooter,
35
+ } from '@octo-cyber/ui/components/ui/dialog'
36
+ import {
37
+ Tabs,
38
+ TabsList,
39
+ TabsTrigger,
40
+ } from '@octo-cyber/ui/components/ui/tabs'
41
+ import { aiCliService } from '../services/ai-cli-service'
42
+ import type { WorkspaceInfo } from '../services/ai-cli-service'
43
+
44
+ function formatDate(dateStr: string): string {
45
+ try {
46
+ return new Date(dateStr).toLocaleString()
47
+ } catch {
48
+ return dateStr
49
+ }
50
+ }
51
+
52
+ function WorkspaceCard({
53
+ ws,
54
+ onDelete,
55
+ onPull,
56
+ isPulling,
57
+ }: {
58
+ ws: WorkspaceInfo
59
+ onDelete: (ws: WorkspaceInfo) => void
60
+ onPull: (id: string) => void
61
+ isPulling: boolean
62
+ }) {
63
+ const t = useTranslations('aiCli.workspaces')
64
+ const tc = useTranslations('common')
65
+
66
+ return (
67
+ <Card>
68
+ <CardHeader className="pb-2">
69
+ <div className="flex items-center justify-between">
70
+ <div className="flex items-center gap-2">
71
+ {ws.type === 'github' ? (
72
+ <Github className="h-4 w-4 text-muted-foreground" />
73
+ ) : (
74
+ <FolderOpen className="h-4 w-4 text-muted-foreground" />
75
+ )}
76
+ <CardTitle className="text-sm">
77
+ {ws.name || ws.id.slice(0, 8)}
78
+ </CardTitle>
79
+ </div>
80
+ <Badge variant={ws.type === 'github' ? 'default' : 'secondary'}>
81
+ {t(`type.${ws.type}`)}
82
+ </Badge>
83
+ </div>
84
+ </CardHeader>
85
+ <CardContent className="space-y-2">
86
+ <p className="text-xs text-muted-foreground truncate" title={ws.path}>
87
+ {ws.path}
88
+ </p>
89
+ {ws.type === 'github' && ws.repoUrl && (
90
+ <p className="text-xs text-muted-foreground truncate" title={ws.repoUrl}>
91
+ {ws.repoUrl}
92
+ </p>
93
+ )}
94
+ {ws.type === 'github' && ws.branch && (
95
+ <p className="text-xs text-muted-foreground">
96
+ {t('branch', { branch: ws.branch })}
97
+ </p>
98
+ )}
99
+ <div className="flex items-center justify-between text-xs text-muted-foreground">
100
+ <span>{formatDate(ws.createdAt)}</span>
101
+ </div>
102
+ <div className="flex justify-end gap-2 pt-1">
103
+ {ws.type === 'github' && (
104
+ <Button
105
+ variant="outline"
106
+ size="sm"
107
+ onClick={() => onPull(ws.id)}
108
+ disabled={isPulling}
109
+ >
110
+ {isPulling ? (
111
+ <Loader2 className="mr-1 h-3 w-3 animate-spin" />
112
+ ) : (
113
+ <GitPullRequest className="mr-1 h-3 w-3" />
114
+ )}
115
+ {isPulling ? t('pulling') : t('actions.pull')}
116
+ </Button>
117
+ )}
118
+ <Button
119
+ variant="ghost"
120
+ size="sm"
121
+ className="text-destructive hover:text-destructive"
122
+ onClick={() => onDelete(ws)}
123
+ >
124
+ <Trash2 className="mr-1 h-3 w-3" />
125
+ {tc('delete')}
126
+ </Button>
127
+ </div>
128
+ </CardContent>
129
+ </Card>
130
+ )
131
+ }
132
+
133
+ type WorkspaceType = 'local' | 'github'
134
+
135
+ export default function WorkspacesPage() {
136
+ const t = useTranslations('aiCli.workspaces')
137
+ const tc = useTranslations('common')
138
+
139
+ const [workspaces, setWorkspaces] = useState<WorkspaceInfo[]>([])
140
+ const [loading, setLoading] = useState(true)
141
+ const [createDialogOpen, setCreateDialogOpen] = useState(false)
142
+ const [isCreating, setIsCreating] = useState(false)
143
+ const [deleteTarget, setDeleteTarget] = useState<WorkspaceInfo | null>(null)
144
+ const [pullingId, setPullingId] = useState<string | null>(null)
145
+
146
+ // Create form state
147
+ const [createType, setCreateType] = useState<WorkspaceType>('local')
148
+ const [createName, setCreateName] = useState('')
149
+ const [createLocalPath, setCreateLocalPath] = useState('')
150
+ const [createRepoUrl, setCreateRepoUrl] = useState('')
151
+ const [createBranch, setCreateBranch] = useState('')
152
+
153
+ const loadWorkspaces = useCallback(async () => {
154
+ setLoading(true)
155
+ try {
156
+ const data = await aiCliService.getWorkspaces()
157
+ setWorkspaces(data)
158
+ } catch {
159
+ toast.error(t('toast.loadFailed'))
160
+ } finally {
161
+ setLoading(false)
162
+ }
163
+ }, [t])
164
+
165
+ useEffect(() => {
166
+ loadWorkspaces()
167
+ }, [loadWorkspaces])
168
+
169
+ const resetCreateForm = () => {
170
+ setCreateType('local')
171
+ setCreateName('')
172
+ setCreateLocalPath('')
173
+ setCreateRepoUrl('')
174
+ setCreateBranch('')
175
+ }
176
+
177
+ const handleCreate = async () => {
178
+ if (!createName.trim()) return
179
+ setIsCreating(true)
180
+ try {
181
+ if (createType === 'local') {
182
+ if (!createLocalPath.trim()) return
183
+ await aiCliService.createWorkspace({
184
+ type: 'local',
185
+ name: createName.trim(),
186
+ localPath: createLocalPath.trim(),
187
+ })
188
+ } else {
189
+ if (!createRepoUrl.trim() || !createBranch.trim()) return
190
+ await aiCliService.createWorkspace({
191
+ type: 'github',
192
+ name: createName.trim(),
193
+ repoUrl: createRepoUrl.trim(),
194
+ branch: createBranch.trim(),
195
+ })
196
+ }
197
+ setCreateDialogOpen(false)
198
+ resetCreateForm()
199
+ await loadWorkspaces()
200
+ toast.success(t('toast.created'))
201
+ } catch {
202
+ toast.error(t('toast.createFailed'))
203
+ } finally {
204
+ setIsCreating(false)
205
+ }
206
+ }
207
+
208
+ const handleDelete = async () => {
209
+ if (!deleteTarget) return
210
+ try {
211
+ await aiCliService.deleteWorkspace(deleteTarget.id)
212
+ setDeleteTarget(null)
213
+ await loadWorkspaces()
214
+ toast.success(t('toast.deleted'))
215
+ } catch {
216
+ toast.error(t('toast.deleteFailed'))
217
+ }
218
+ }
219
+
220
+ const handlePull = async (id: string) => {
221
+ setPullingId(id)
222
+ try {
223
+ await aiCliService.pullWorkspace(id)
224
+ toast.success(t('toast.pulled'))
225
+ } catch {
226
+ toast.error(t('toast.pullFailed'))
227
+ } finally {
228
+ setPullingId(null)
229
+ }
230
+ }
231
+
232
+ const isCreateValid =
233
+ createName.trim() !== '' &&
234
+ (createType === 'local'
235
+ ? createLocalPath.trim() !== ''
236
+ : createRepoUrl.trim() !== '' && createBranch.trim() !== '')
237
+
238
+ const localCount = workspaces.filter((w) => w.type === 'local').length
239
+ const githubCount = workspaces.filter((w) => w.type === 'github').length
240
+
241
+ const deleteDescription = deleteTarget
242
+ ? `${t('deleteConfirm', { name: deleteTarget.name || deleteTarget.id })} ${
243
+ deleteTarget.type === 'local'
244
+ ? t('deleteConfirmLocal')
245
+ : t('deleteConfirmGithub')
246
+ }`
247
+ : ''
248
+
249
+ return (
250
+ <div className="space-y-6">
251
+ <PageHeader title={t('title')} description={t('description')}>
252
+ <div className="flex gap-2">
253
+ <Button
254
+ variant="outline"
255
+ size="sm"
256
+ onClick={() => {
257
+ resetCreateForm()
258
+ setCreateDialogOpen(true)
259
+ }}
260
+ >
261
+ <Plus className="mr-1 h-4 w-4" />
262
+ {t('actions.create')}
263
+ </Button>
264
+ <Button variant="outline" onClick={loadWorkspaces} disabled={loading}>
265
+ {loading ? (
266
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
267
+ ) : (
268
+ <RefreshCw className="mr-2 h-4 w-4" />
269
+ )}
270
+ {tc('refresh')}
271
+ </Button>
272
+ </div>
273
+ </PageHeader>
274
+
275
+ {loading ? (
276
+ <div className="flex items-center justify-center h-64">
277
+ <p className="text-muted-foreground">{tc('loading')}</p>
278
+ </div>
279
+ ) : (
280
+ <>
281
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
282
+ <span>
283
+ {t('summary', { local: localCount, github: githubCount })}
284
+ </span>
285
+ </div>
286
+
287
+ {workspaces.length === 0 ? (
288
+ <div className="flex items-center justify-center h-32">
289
+ <p className="text-muted-foreground">{t('empty')}</p>
290
+ </div>
291
+ ) : (
292
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
293
+ {workspaces.map((ws) => (
294
+ <WorkspaceCard
295
+ key={ws.id}
296
+ ws={ws}
297
+ onDelete={setDeleteTarget}
298
+ onPull={handlePull}
299
+ isPulling={pullingId === ws.id}
300
+ />
301
+ ))}
302
+ </div>
303
+ )}
304
+ </>
305
+ )}
306
+
307
+ {/* Create Dialog */}
308
+ <Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
309
+ <DialogContent className="sm:max-w-md">
310
+ <DialogHeader>
311
+ <DialogTitle>{t('createDialog.title')}</DialogTitle>
312
+ <DialogDescription>{t('createDialog.description')}</DialogDescription>
313
+ </DialogHeader>
314
+ <div className="space-y-4">
315
+ <Tabs
316
+ value={createType}
317
+ onValueChange={(v) => setCreateType(v as WorkspaceType)}
318
+ >
319
+ <TabsList className="grid w-full grid-cols-2">
320
+ <TabsTrigger value="local">
321
+ <FolderOpen className="mr-1 h-3 w-3" />
322
+ {t('createDialog.typeLocal')}
323
+ </TabsTrigger>
324
+ <TabsTrigger value="github">
325
+ <Github className="mr-1 h-3 w-3" />
326
+ {t('createDialog.typeGithub')}
327
+ </TabsTrigger>
328
+ </TabsList>
329
+ </Tabs>
330
+
331
+ <div className="space-y-2">
332
+ <Label>{t('createDialog.nameLabel')}</Label>
333
+ <Input
334
+ placeholder={t('createDialog.namePlaceholder')}
335
+ value={createName}
336
+ onChange={(e) => setCreateName(e.target.value)}
337
+ />
338
+ </div>
339
+
340
+ {createType === 'local' ? (
341
+ <div className="space-y-2">
342
+ <Label>{t('createDialog.localPathLabel')}</Label>
343
+ <Input
344
+ placeholder={t('createDialog.localPathPlaceholder')}
345
+ value={createLocalPath}
346
+ onChange={(e) => setCreateLocalPath(e.target.value)}
347
+ />
348
+ </div>
349
+ ) : (
350
+ <>
351
+ <div className="space-y-2">
352
+ <Label>{t('createDialog.repoUrlLabel')}</Label>
353
+ <Input
354
+ placeholder={t('createDialog.repoUrlPlaceholder')}
355
+ value={createRepoUrl}
356
+ onChange={(e) => setCreateRepoUrl(e.target.value)}
357
+ />
358
+ </div>
359
+ <div className="space-y-2">
360
+ <Label>{t('createDialog.branchLabel')}</Label>
361
+ <Input
362
+ placeholder={t('createDialog.branchPlaceholder')}
363
+ value={createBranch}
364
+ onChange={(e) => setCreateBranch(e.target.value)}
365
+ />
366
+ </div>
367
+ </>
368
+ )}
369
+ </div>
370
+ <DialogFooter>
371
+ <Button variant="outline" onClick={() => setCreateDialogOpen(false)}>
372
+ {tc('cancel')}
373
+ </Button>
374
+ <Button
375
+ onClick={handleCreate}
376
+ disabled={isCreating || !isCreateValid}
377
+ >
378
+ {isCreating && <Loader2 className="mr-1 h-3 w-3 animate-spin" />}
379
+ {tc('create')}
380
+ </Button>
381
+ </DialogFooter>
382
+ </DialogContent>
383
+ </Dialog>
384
+
385
+ {/* Delete Confirm */}
386
+ <ConfirmDialog
387
+ open={!!deleteTarget}
388
+ onOpenChange={(open) => { if (!open) setDeleteTarget(null) }}
389
+ title={tc('confirmDelete')}
390
+ description={deleteDescription}
391
+ onConfirm={handleDelete}
392
+ confirmText={tc('delete')}
393
+ variant="destructive"
394
+ />
395
+ </div>
396
+ )
397
+ }