@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,158 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect } from 'react'
4
+ import { useTranslations } from 'next-intl'
5
+ import { toast } from 'sonner'
6
+ import { Play, Loader2 } from 'lucide-react'
7
+
8
+ import { Button } from '@octo-cyber/ui/components/ui/button'
9
+ import { Textarea } from '@octo-cyber/ui/components/ui/textarea'
10
+ import { Label } from '@octo-cyber/ui/components/ui/label'
11
+ import {
12
+ Dialog,
13
+ DialogContent,
14
+ DialogHeader,
15
+ DialogTitle,
16
+ DialogDescription,
17
+ DialogFooter,
18
+ } from '@octo-cyber/ui/components/ui/dialog'
19
+ import {
20
+ Select,
21
+ SelectContent,
22
+ SelectItem,
23
+ SelectTrigger,
24
+ SelectValue,
25
+ } from '@octo-cyber/ui/components/ui/select'
26
+ import { aiCliService } from '../services/ai-cli-service'
27
+ import type { CliToolInfo } from '../services/ai-cli-service'
28
+
29
+ interface CliTestDialogProps {
30
+ open: boolean
31
+ onOpenChange: (open: boolean) => void
32
+ tool: CliToolInfo | null
33
+ }
34
+
35
+ export default function CliTestDialog({
36
+ open,
37
+ onOpenChange,
38
+ tool,
39
+ }: CliTestDialogProps) {
40
+ const t = useTranslations('aiCli')
41
+ const tc = useTranslations('common')
42
+
43
+ const [selectedModel, setSelectedModel] = useState<string>('')
44
+ const [testPrompt, setTestPrompt] = useState('')
45
+ const [testResult, setTestResult] = useState('')
46
+ const [isExecuting, setIsExecuting] = useState(false)
47
+ const [models, setModels] = useState<string[]>([])
48
+
49
+ useEffect(() => {
50
+ if (!open || !tool) return
51
+ setTestPrompt('')
52
+ setTestResult('')
53
+ setSelectedModel(tool.defaultModel ?? '')
54
+ setModels(tool.models ?? [])
55
+
56
+ // Fetch fresh models list if tool is available
57
+ if (tool.available) {
58
+ aiCliService.getModels(tool.name).then(setModels).catch(() => {
59
+ // Fall back to models from tool info
60
+ })
61
+ }
62
+ }, [open, tool])
63
+
64
+ const handleExecute = async () => {
65
+ if (!testPrompt.trim() || !tool) return
66
+ setIsExecuting(true)
67
+ setTestResult('')
68
+ try {
69
+ const result = await aiCliService.execute({
70
+ tool: tool.name,
71
+ prompt: testPrompt,
72
+ model: selectedModel || undefined,
73
+ timeoutMs: 60000,
74
+ })
75
+ if (result.output) {
76
+ setTestResult(result.output)
77
+ } else if (result.error) {
78
+ setTestResult(`Error: ${result.error}`)
79
+ } else {
80
+ setTestResult(t('testDialog.noOutput'))
81
+ }
82
+ } catch {
83
+ toast.error(t('toast.executeFailed'))
84
+ } finally {
85
+ setIsExecuting(false)
86
+ }
87
+ }
88
+
89
+ return (
90
+ <Dialog open={open} onOpenChange={onOpenChange}>
91
+ <DialogContent className="sm:max-w-[600px]">
92
+ <DialogHeader>
93
+ <DialogTitle>
94
+ {t('testDialog.title', { tool: tool?.displayName ?? '' })}
95
+ </DialogTitle>
96
+ <DialogDescription>{t('testDialog.description')}</DialogDescription>
97
+ </DialogHeader>
98
+ <div className="space-y-4">
99
+ {models.length > 0 && (
100
+ <div className="space-y-2">
101
+ <Label>{t('testDialog.modelLabel')}</Label>
102
+ <Select value={selectedModel} onValueChange={setSelectedModel}>
103
+ <SelectTrigger>
104
+ <SelectValue placeholder={t('testDialog.modelPlaceholder')} />
105
+ </SelectTrigger>
106
+ <SelectContent>
107
+ {models.map((model) => (
108
+ <SelectItem key={model} value={model}>
109
+ {model}
110
+ {model === tool?.defaultModel && (
111
+ <span className="ml-2 text-muted-foreground text-xs">
112
+ ({t('testDialog.modelDefault')})
113
+ </span>
114
+ )}
115
+ </SelectItem>
116
+ ))}
117
+ </SelectContent>
118
+ </Select>
119
+ </div>
120
+ )}
121
+ <div className="space-y-2">
122
+ <Label>{t('testDialog.promptLabel')}</Label>
123
+ <Textarea
124
+ placeholder={t('testDialog.promptPlaceholder')}
125
+ value={testPrompt}
126
+ onChange={(e) => setTestPrompt(e.target.value)}
127
+ rows={3}
128
+ />
129
+ </div>
130
+ {testResult && (
131
+ <div className="space-y-2">
132
+ <Label>{t('testDialog.resultLabel')}</Label>
133
+ <pre className="rounded-md bg-muted p-3 text-sm overflow-auto max-h-64 whitespace-pre-wrap">
134
+ {testResult}
135
+ </pre>
136
+ </div>
137
+ )}
138
+ </div>
139
+ <DialogFooter>
140
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
141
+ {tc('close')}
142
+ </Button>
143
+ <Button
144
+ onClick={handleExecute}
145
+ disabled={isExecuting || !testPrompt.trim()}
146
+ >
147
+ {isExecuting ? (
148
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
149
+ ) : (
150
+ <Play className="mr-2 h-4 w-4" />
151
+ )}
152
+ {isExecuting ? t('testDialog.executing') : t('testDialog.execute')}
153
+ </Button>
154
+ </DialogFooter>
155
+ </DialogContent>
156
+ </Dialog>
157
+ )
158
+ }
package/web/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @octo-cyber/ai — Frontend entry point.
3
+ *
4
+ * Re-exports page components, services, and the frontend manifest.
5
+ */
6
+
7
+ // Manifest
8
+ export { aiFrontendManifest } from './manifest';
9
+
10
+ // i18n Messages
11
+ export { default as aiMessagesZhCN } from './messages/zh-CN.json';
12
+ export { default as aiMessagesEnUS } from './messages/en-US.json';
13
+
14
+ // Pages
15
+ export { default as AiCliToolsPage } from './pages/AiCliToolsPage';
16
+ export { default as WorkspacesPage } from './pages/WorkspacesPage';
17
+
18
+ // Services
19
+ export { aiCliService } from './services/ai-cli-service';
@@ -0,0 +1,14 @@
1
+ import type { IFrontendManifest } from '@octo-cyber/core';
2
+
3
+ export const aiFrontendManifest: IFrontendManifest = {
4
+ moduleId: 'ai',
5
+ icon: 'Terminal',
6
+ color: 'cyan',
7
+ position: 'main',
8
+ titleKey: 'aiCli.title',
9
+ descriptionKey: 'aiCli.description',
10
+ pages: [
11
+ { path: '/ai-tools', titleKey: 'aiCli.pages.tools', icon: 'Terminal' },
12
+ { path: '/ai-tools/workspaces', titleKey: 'aiCli.pages.workspaces', icon: 'Terminal' },
13
+ ],
14
+ };
@@ -0,0 +1,81 @@
1
+ {
2
+ "aiCli": {
3
+ "title": "AI CLI Tools",
4
+ "description": "Manage local AI tools (Claude, Gemini, Codex, Antigravity) status and testing",
5
+ "summary": "{available}/{total} tools available",
6
+ "noTools": "No CLI tools detected",
7
+ "status": {
8
+ "available": "Available",
9
+ "unavailable": "Unavailable"
10
+ },
11
+ "actions": {
12
+ "healthCheck": "Check",
13
+ "testRun": "Test"
14
+ },
15
+ "testDialog": {
16
+ "title": "Test {tool}",
17
+ "description": "Enter a prompt to test if the tool works correctly",
18
+ "modelLabel": "Model",
19
+ "modelPlaceholder": "Select model",
20
+ "modelDefault": "Default",
21
+ "promptLabel": "Prompt",
22
+ "promptPlaceholder": "Enter a test prompt, e.g.: Hello, please introduce yourself briefly",
23
+ "resultLabel": "Result",
24
+ "execute": "Execute",
25
+ "executing": "Executing...",
26
+ "noOutput": "No output"
27
+ },
28
+ "modelCount": "{count} models",
29
+ "toast": {
30
+ "loadFailed": "Failed to load tools",
31
+ "healthOk": "{name} check passed",
32
+ "healthUnavailable": "{name} is unavailable",
33
+ "healthFailed": "{name} check failed",
34
+ "executeFailed": "Execution failed"
35
+ },
36
+ "workspaces": {
37
+ "title": "Workspaces",
38
+ "description": "Manage local directory and GitHub repository workspaces",
39
+ "summary": "{local} local, {github} GitHub workspaces",
40
+ "empty": "No workspaces",
41
+ "type": {
42
+ "local": "Local",
43
+ "github": "GitHub"
44
+ },
45
+ "actions": {
46
+ "create": "Create",
47
+ "pull": "Pull"
48
+ },
49
+ "branch": "Branch: {branch}",
50
+ "pulling": "Pulling...",
51
+ "createDialog": {
52
+ "title": "Create Workspace",
53
+ "description": "Select workspace type and fill in the details",
54
+ "typeLocal": "Local Directory",
55
+ "typeLocalDesc": "Use an existing local directory as workspace",
56
+ "typeGithub": "GitHub Repository",
57
+ "typeGithubDesc": "Clone a GitHub repository as workspace",
58
+ "nameLabel": "Name",
59
+ "namePlaceholder": "e.g., my-project",
60
+ "localPathLabel": "Local Directory Path",
61
+ "localPathPlaceholder": "e.g., /Users/user/projects/my-project",
62
+ "repoUrlLabel": "Repository URL",
63
+ "repoUrlPlaceholder": "e.g., https://github.com/user/repo.git",
64
+ "branchLabel": "Branch",
65
+ "branchPlaceholder": "e.g., main"
66
+ },
67
+ "deleteConfirm": "Are you sure you want to delete workspace \"{name}\"?",
68
+ "deleteConfirmLocal": "Only the workspace record will be removed. The original directory will not be deleted.",
69
+ "deleteConfirmGithub": "The cloned repository directory will be deleted.",
70
+ "toast": {
71
+ "loadFailed": "Failed to load workspaces",
72
+ "created": "Workspace created",
73
+ "createFailed": "Failed to create workspace",
74
+ "deleted": "Workspace deleted",
75
+ "deleteFailed": "Failed to delete workspace",
76
+ "pulled": "Successfully pulled latest code",
77
+ "pullFailed": "Pull failed"
78
+ }
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,81 @@
1
+ {
2
+ "aiCli": {
3
+ "title": "AI CLI 工具",
4
+ "description": "管理本地 AI 工具(Claude、Gemini、Codex、Antigravity)的状态和测试",
5
+ "summary": "{available}/{total} 个工具可用",
6
+ "noTools": "未检测到任何 CLI 工具",
7
+ "status": {
8
+ "available": "可用",
9
+ "unavailable": "不可用"
10
+ },
11
+ "actions": {
12
+ "healthCheck": "检测",
13
+ "testRun": "测试"
14
+ },
15
+ "testDialog": {
16
+ "title": "测试 {tool}",
17
+ "description": "输入提示词,测试工具是否正常工作",
18
+ "modelLabel": "模型",
19
+ "modelPlaceholder": "选择模型",
20
+ "modelDefault": "默认",
21
+ "promptLabel": "提示词",
22
+ "promptPlaceholder": "输入测试提示词,例如:你好,请做个简单的自我介绍",
23
+ "resultLabel": "执行结果",
24
+ "execute": "执行",
25
+ "executing": "执行中...",
26
+ "noOutput": "无输出"
27
+ },
28
+ "modelCount": "{count} 个模型",
29
+ "toast": {
30
+ "loadFailed": "加载工具列表失败",
31
+ "healthOk": "{name} 检测通过",
32
+ "healthUnavailable": "{name} 不可用",
33
+ "healthFailed": "{name} 检测失败",
34
+ "executeFailed": "执行失败"
35
+ },
36
+ "workspaces": {
37
+ "title": "工作空间",
38
+ "description": "管理本地目录和 GitHub 仓库工作空间",
39
+ "summary": "{local} 个本地、{github} 个 GitHub 工作空间",
40
+ "empty": "暂无工作空间",
41
+ "type": {
42
+ "local": "本地",
43
+ "github": "GitHub"
44
+ },
45
+ "actions": {
46
+ "create": "新建",
47
+ "pull": "拉取"
48
+ },
49
+ "branch": "分支:{branch}",
50
+ "pulling": "拉取中...",
51
+ "createDialog": {
52
+ "title": "新建工作空间",
53
+ "description": "选择工作空间类型并填写相关信息",
54
+ "typeLocal": "本地目录",
55
+ "typeLocalDesc": "选择本地已有的目录作为工作空间",
56
+ "typeGithub": "GitHub 仓库",
57
+ "typeGithubDesc": "从 GitHub 仓库克隆代码作为工作空间",
58
+ "nameLabel": "名称",
59
+ "namePlaceholder": "例如:my-project",
60
+ "localPathLabel": "本地目录路径",
61
+ "localPathPlaceholder": "例如:/Users/user/projects/my-project",
62
+ "repoUrlLabel": "仓库地址",
63
+ "repoUrlPlaceholder": "例如:https://github.com/user/repo.git",
64
+ "branchLabel": "分支",
65
+ "branchPlaceholder": "例如:main"
66
+ },
67
+ "deleteConfirm": "确定要删除工作空间「{name}」吗?",
68
+ "deleteConfirmLocal": "仅移除工作空间记录,不会删除原目录。",
69
+ "deleteConfirmGithub": "将删除克隆的仓库目录。",
70
+ "toast": {
71
+ "loadFailed": "加载工作空间失败",
72
+ "created": "工作空间已创建",
73
+ "createFailed": "创建失败",
74
+ "deleted": "工作空间已删除",
75
+ "deleteFailed": "删除失败",
76
+ "pulled": "已拉取最新代码",
77
+ "pullFailed": "拉取失败"
78
+ }
79
+ }
80
+ }
81
+ }
@@ -0,0 +1,223 @@
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
+ Play,
9
+ CheckCircle2,
10
+ XCircle,
11
+ Loader2,
12
+ Terminal,
13
+ } from 'lucide-react'
14
+
15
+ import { PageHeader } from '@octo-cyber/ui/components/shared/page-header'
16
+ import { Button } from '@octo-cyber/ui/components/ui/button'
17
+ import { Badge } from '@octo-cyber/ui/components/ui/badge'
18
+ import {
19
+ Card,
20
+ CardContent,
21
+ CardDescription,
22
+ CardHeader,
23
+ CardTitle,
24
+ } from '@octo-cyber/ui/components/ui/card'
25
+ import { aiCliService } from '../services/ai-cli-service'
26
+ import type { CliToolInfo } from '../services/ai-cli-service'
27
+ import CliTestDialog from '../components/CliTestDialog'
28
+
29
+ function ToolStatusIcon({ isAvailable }: { isAvailable: boolean }) {
30
+ if (isAvailable) {
31
+ return <CheckCircle2 className="h-5 w-5 text-green-500" />
32
+ }
33
+ return <XCircle className="h-5 w-5 text-muted-foreground" />
34
+ }
35
+
36
+ function ToolCard({
37
+ tool,
38
+ onTest,
39
+ onHealthCheck,
40
+ isChecking,
41
+ }: {
42
+ tool: CliToolInfo
43
+ onTest: (tool: CliToolInfo) => void
44
+ onHealthCheck: (toolName: string) => void
45
+ isChecking: boolean
46
+ }) {
47
+ const t = useTranslations('aiCli')
48
+
49
+ return (
50
+ <Card>
51
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
52
+ <div className="flex items-center gap-3">
53
+ <Terminal className="h-5 w-5 text-muted-foreground" />
54
+ <div>
55
+ <CardTitle className="text-base">{tool.displayName}</CardTitle>
56
+ <CardDescription className="text-xs">{tool.name}</CardDescription>
57
+ </div>
58
+ </div>
59
+ <ToolStatusIcon isAvailable={tool.available} />
60
+ </CardHeader>
61
+ <CardContent>
62
+ <div className="space-y-3">
63
+ <div className="flex items-center gap-2 text-sm">
64
+ <Badge variant={tool.available ? 'default' : 'secondary'}>
65
+ {tool.available ? t('status.available') : t('status.unavailable')}
66
+ </Badge>
67
+ {tool.version && (
68
+ <span className="text-muted-foreground">v{tool.version}</span>
69
+ )}
70
+ {tool.models && tool.models.length > 0 && (
71
+ <span className="text-muted-foreground text-xs">
72
+ {t('modelCount', { count: tool.models.length })}
73
+ </span>
74
+ )}
75
+ </div>
76
+ {tool.path && (
77
+ <p className="text-xs text-muted-foreground truncate" title={tool.path}>
78
+ {tool.path}
79
+ </p>
80
+ )}
81
+ <div className="flex gap-2 pt-1">
82
+ <Button
83
+ variant="outline"
84
+ size="sm"
85
+ onClick={() => onHealthCheck(tool.name)}
86
+ disabled={isChecking}
87
+ >
88
+ {isChecking ? (
89
+ <Loader2 className="mr-1 h-3 w-3 animate-spin" />
90
+ ) : (
91
+ <RefreshCw className="mr-1 h-3 w-3" />
92
+ )}
93
+ {t('actions.healthCheck')}
94
+ </Button>
95
+ {tool.available && (
96
+ <Button
97
+ variant="outline"
98
+ size="sm"
99
+ onClick={() => onTest(tool)}
100
+ >
101
+ <Play className="mr-1 h-3 w-3" />
102
+ {t('actions.testRun')}
103
+ </Button>
104
+ )}
105
+ </div>
106
+ </div>
107
+ </CardContent>
108
+ </Card>
109
+ )
110
+ }
111
+
112
+ export default function AiCliToolsPage() {
113
+ const t = useTranslations('aiCli')
114
+ const tc = useTranslations('common')
115
+
116
+ const [tools, setTools] = useState<CliToolInfo[]>([])
117
+ const [loading, setLoading] = useState(true)
118
+ const [checkingTool, setCheckingTool] = useState<string | null>(null)
119
+ const [testDialogOpen, setTestDialogOpen] = useState(false)
120
+ const [testToolInfo, setTestToolInfo] = useState<CliToolInfo | null>(null)
121
+
122
+ const loadTools = useCallback(async () => {
123
+ setLoading(true)
124
+ try {
125
+ const data = await aiCliService.getTools()
126
+ setTools(data)
127
+ } catch {
128
+ toast.error(t('toast.loadFailed'))
129
+ } finally {
130
+ setLoading(false)
131
+ }
132
+ }, [t])
133
+
134
+ useEffect(() => {
135
+ loadTools()
136
+ }, [loadTools])
137
+
138
+ const handleHealthCheck = async (toolName: string) => {
139
+ setCheckingTool(toolName)
140
+ try {
141
+ const info = await aiCliService.healthCheck(toolName)
142
+ setTools((prev) =>
143
+ prev.map((tool) =>
144
+ tool.name === toolName
145
+ ? { ...tool, available: info.available, version: info.version, path: info.path }
146
+ : tool,
147
+ ),
148
+ )
149
+ toast.success(
150
+ info.available
151
+ ? t('toast.healthOk', { name: toolName })
152
+ : t('toast.healthUnavailable', { name: toolName }),
153
+ )
154
+ } catch {
155
+ toast.error(t('toast.healthFailed', { name: toolName }))
156
+ } finally {
157
+ setCheckingTool(null)
158
+ }
159
+ }
160
+
161
+ const openTestDialog = (tool: CliToolInfo) => {
162
+ setTestToolInfo(tool)
163
+ setTestDialogOpen(true)
164
+ }
165
+
166
+ const availableCount = tools.filter((t) => t.available).length
167
+
168
+ return (
169
+ <div className="space-y-6">
170
+ <PageHeader title={t('title')} description={t('description')}>
171
+ <Button variant="outline" onClick={loadTools} disabled={loading}>
172
+ {loading ? (
173
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
174
+ ) : (
175
+ <RefreshCw className="mr-2 h-4 w-4" />
176
+ )}
177
+ {tc('refresh')}
178
+ </Button>
179
+ </PageHeader>
180
+
181
+ {loading ? (
182
+ <div className="flex items-center justify-center h-64">
183
+ <p className="text-muted-foreground">{tc('loading')}</p>
184
+ </div>
185
+ ) : (
186
+ <>
187
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
188
+ <span>
189
+ {t('summary', {
190
+ available: availableCount,
191
+ total: tools.length,
192
+ })}
193
+ </span>
194
+ </div>
195
+
196
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
197
+ {tools.map((tool) => (
198
+ <ToolCard
199
+ key={tool.name}
200
+ tool={tool}
201
+ onTest={openTestDialog}
202
+ onHealthCheck={handleHealthCheck}
203
+ isChecking={checkingTool === tool.name}
204
+ />
205
+ ))}
206
+ </div>
207
+
208
+ {tools.length === 0 && (
209
+ <div className="flex items-center justify-center h-32">
210
+ <p className="text-muted-foreground">{t('noTools')}</p>
211
+ </div>
212
+ )}
213
+ </>
214
+ )}
215
+
216
+ <CliTestDialog
217
+ open={testDialogOpen}
218
+ onOpenChange={setTestDialogOpen}
219
+ tool={testToolInfo}
220
+ />
221
+ </div>
222
+ )
223
+ }