@fatdoge/wtree 0.1.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 (65) hide show
  1. package/README.en.md +113 -0
  2. package/README.md +136 -0
  3. package/api/app.ts +19 -0
  4. package/api/cli/wtree.ts +809 -0
  5. package/api/core/config.ts +26 -0
  6. package/api/core/exec.ts +55 -0
  7. package/api/core/git.ts +35 -0
  8. package/api/core/id.ts +8 -0
  9. package/api/core/open.ts +58 -0
  10. package/api/core/worktree.test.ts +33 -0
  11. package/api/core/worktree.ts +72 -0
  12. package/api/createApiApp.ts +33 -0
  13. package/api/index.ts +9 -0
  14. package/api/routes/worktrees.ts +255 -0
  15. package/api/server.ts +34 -0
  16. package/api/ui/startUiDev.ts +82 -0
  17. package/dist/assets/index-D9inyPb3.js +179 -0
  18. package/dist/assets/index-W34LSHWF.css +1 -0
  19. package/dist/favicon.svg +4 -0
  20. package/dist/index.html +354 -0
  21. package/dist-node/api/app.js +17 -0
  22. package/dist-node/api/cli/wtree.js +722 -0
  23. package/dist-node/api/cli/wtui.js +722 -0
  24. package/dist-node/api/core/config.js +21 -0
  25. package/dist-node/api/core/exec.js +24 -0
  26. package/dist-node/api/core/git.js +24 -0
  27. package/dist-node/api/core/id.js +6 -0
  28. package/dist-node/api/core/open.js +51 -0
  29. package/dist-node/api/core/worktree.js +58 -0
  30. package/dist-node/api/core/worktree.test.js +30 -0
  31. package/dist-node/api/createApiApp.js +26 -0
  32. package/dist-node/api/routes/worktrees.js +213 -0
  33. package/dist-node/api/server.js +29 -0
  34. package/dist-node/api/ui/startUiDev.js +65 -0
  35. package/dist-node/shared/wtui-types.js +1 -0
  36. package/index.html +24 -0
  37. package/package.json +89 -0
  38. package/postcss.config.js +10 -0
  39. package/shared/wtui-types.ts +36 -0
  40. package/src/App.tsx +28 -0
  41. package/src/assets/react.svg +1 -0
  42. package/src/components/Button.tsx +34 -0
  43. package/src/components/Empty.tsx +8 -0
  44. package/src/components/Input.tsx +16 -0
  45. package/src/components/Modal.tsx +33 -0
  46. package/src/components/ToastHost.tsx +42 -0
  47. package/src/hooks/useTheme.ts +29 -0
  48. package/src/i18n/index.ts +22 -0
  49. package/src/i18n/locales/en.json +145 -0
  50. package/src/i18n/locales/zh.json +145 -0
  51. package/src/index.css +24 -0
  52. package/src/lib/utils.ts +6 -0
  53. package/src/main.tsx +11 -0
  54. package/src/pages/CreateWorktree.tsx +181 -0
  55. package/src/pages/HelpPage.tsx +67 -0
  56. package/src/pages/Home.tsx +3 -0
  57. package/src/pages/SettingsPage.tsx +218 -0
  58. package/src/pages/Worktrees.tsx +354 -0
  59. package/src/stores/themeStore.ts +44 -0
  60. package/src/stores/toastStore.ts +29 -0
  61. package/src/stores/worktreeStore.ts +93 -0
  62. package/src/utils/api.ts +36 -0
  63. package/src/vite-env.d.ts +1 -0
  64. package/tailwind.config.js +13 -0
  65. package/vite.config.ts +46 -0
@@ -0,0 +1,181 @@
1
+ import { ArrowLeft, CheckCircle2, Loader2 } from 'lucide-react'
2
+ import { useEffect, useMemo, useState } from 'react'
3
+ import { Link, useNavigate } from 'react-router-dom'
4
+ import { useTranslation } from 'react-i18next'
5
+ import Button from '@/components/Button'
6
+ import Input from '@/components/Input'
7
+ import { useToastStore } from '@/stores/toastStore'
8
+ import { useWorktreeStore } from '@/stores/worktreeStore'
9
+ import type { CreateWorktreeRequest } from '../../shared/wtui-types'
10
+
11
+ type Mode = 'existing' | 'new'
12
+
13
+ export default function CreateWorktree() {
14
+ const { t } = useTranslation()
15
+ const navigate = useNavigate()
16
+ const create = useWorktreeStore((s) => s.create)
17
+ const branches = useWorktreeStore((s) => s.branches)
18
+ const fetchBranches = useWorktreeStore((s) => s.fetchBranches)
19
+ const toast = useToastStore((s) => s.push)
20
+
21
+ useEffect(() => {
22
+ fetchBranches()
23
+ }, [fetchBranches])
24
+
25
+ const [mode, setMode] = useState<Mode>('existing')
26
+ const [ref, setRef] = useState('')
27
+ const [newBranch, setNewBranch] = useState('')
28
+ const [p, setP] = useState('')
29
+ const [submitting, setSubmitting] = useState(false)
30
+ const [resultPath, setResultPath] = useState<string | null>(null)
31
+
32
+ const preview = useMemo(() => {
33
+ const target = p.trim() ? p.trim() : '<path>'
34
+ if (mode === 'new' && newBranch.trim()) {
35
+ return `git branch ${newBranch.trim()} ${ref.trim() || '<ref>'} && git worktree add ${target} ${newBranch.trim()}`
36
+ }
37
+ return `git worktree add ${target} ${ref.trim() || '<ref>'}`
38
+ }, [mode, newBranch, p, ref])
39
+
40
+ const canSubmit = ref.trim().length > 0 && p.trim().length > 0 && (!mode || mode === 'existing' || newBranch.trim().length > 0)
41
+
42
+ const onSubmit = async () => {
43
+ if (!canSubmit) return
44
+ setSubmitting(true)
45
+ setResultPath(null)
46
+ try {
47
+ const payload: CreateWorktreeRequest = {
48
+ ref: ref.trim(),
49
+ path: p.trim(),
50
+ ...(mode === 'new' ? { newBranch: newBranch.trim() } : {}),
51
+ }
52
+ const created = await create(payload)
53
+ if (!created) {
54
+ toast({ type: 'error', title: '创建失败' })
55
+ return
56
+ }
57
+ setResultPath(created.path)
58
+ toast({ type: 'success', title: '创建成功' })
59
+ } catch (e: unknown) {
60
+ const msg = e instanceof Error ? e.message : String(e)
61
+ toast({ type: 'error', title: '创建失败', detail: msg })
62
+ } finally {
63
+ setSubmitting(false)
64
+ }
65
+ }
66
+
67
+ return (
68
+ <div className="mx-auto w-full max-w-screen-xl px-4 py-6">
69
+ <div className="flex items-center justify-between gap-4">
70
+ <div className="flex items-center gap-2">
71
+ <Link to="/">
72
+ <Button variant="ghost" size="sm">
73
+ <ArrowLeft className="h-4 w-4" />
74
+ {t('create.back')}
75
+ </Button>
76
+ </Link>
77
+ <div className="text-lg font-semibold text-slate-900 dark:text-slate-100">{t('create.title')}</div>
78
+ </div>
79
+ <Button variant="secondary" size="sm" onClick={() => navigate('/help')}>{t('create.help')}</Button>
80
+ </div>
81
+
82
+ <div className="mt-5 grid grid-cols-12 gap-4">
83
+ <div className="col-span-12 lg:col-span-7">
84
+ <div className="rounded-xl border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-950 p-4">
85
+ <div className="flex gap-2">
86
+ <Button
87
+ size="sm"
88
+ variant={mode === 'existing' ? 'primary' : 'secondary'}
89
+ onClick={() => setMode('existing')}
90
+ >
91
+ {t('create.tabs.existing')}
92
+ </Button>
93
+ <Button size="sm" variant={mode === 'new' ? 'primary' : 'secondary'} onClick={() => setMode('new')}>
94
+ {t('create.tabs.new')}
95
+ </Button>
96
+ </div>
97
+
98
+ <div className="mt-4 grid grid-cols-12 gap-3">
99
+ <div className="col-span-12">
100
+ <div className="text-xs font-medium text-slate-600 dark:text-slate-300">{t('create.fields.ref')}</div>
101
+ <div className="mt-1">
102
+ <Input
103
+ value={ref}
104
+ onChange={(e) => setRef(e.target.value)}
105
+ placeholder={t('create.fields.refPlaceholder')}
106
+ list="branch-list"
107
+ />
108
+ <datalist id="branch-list">
109
+ {branches.map((b) => (
110
+ <option key={b} value={b} />
111
+ ))}
112
+ </datalist>
113
+ </div>
114
+ </div>
115
+
116
+ {mode === 'new' ? (
117
+ <div className="col-span-12">
118
+ <div className="text-xs font-medium text-slate-600 dark:text-slate-300">{t('create.fields.newBranch')}</div>
119
+ <div className="mt-1">
120
+ <Input value={newBranch} onChange={(e) => setNewBranch(e.target.value)} placeholder={t('create.fields.newBranchPlaceholder')} />
121
+ </div>
122
+ </div>
123
+ ) : null}
124
+
125
+ <div className="col-span-12">
126
+ <div className="text-xs font-medium text-slate-600 dark:text-slate-300">{t('create.fields.path')}</div>
127
+ <div className="mt-1">
128
+ <Input value={p} onChange={(e) => setP(e.target.value)} placeholder={t('create.fields.pathPlaceholder')} />
129
+ </div>
130
+ </div>
131
+ </div>
132
+
133
+ <div className="mt-4 flex flex-col sm:flex-row items-center justify-end gap-2">
134
+ <Button className="w-full sm:w-auto" variant="primary" size="sm" disabled={!canSubmit || submitting} onClick={onSubmit}>
135
+ {submitting ? <Loader2 className="h-4 w-4 animate-spin" /> : null}
136
+ {t('create.submit')}
137
+ </Button>
138
+ </div>
139
+ </div>
140
+ </div>
141
+
142
+ <div className="col-span-12 lg:col-span-5">
143
+ <div className="rounded-xl border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-950 p-4">
144
+ <div className="text-sm font-semibold text-slate-900 dark:text-slate-100">{t('create.preview.title')}</div>
145
+ <div className="mt-2 rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2 font-mono text-xs text-slate-900 dark:text-slate-100">
146
+ {preview}
147
+ </div>
148
+ <div className="mt-2 text-xs text-slate-500 dark:text-slate-400">{t('create.preview.desc')}</div>
149
+ </div>
150
+
151
+ <div className="mt-3 rounded-xl border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-950 p-4">
152
+ <div className="text-sm font-semibold text-slate-900 dark:text-slate-100">{t('create.result.title')}</div>
153
+ {submitting ? (
154
+ <div className="mt-2 flex items-center gap-2 text-xs text-slate-600 dark:text-slate-300">
155
+ <Loader2 className="h-4 w-4 animate-spin" />
156
+ {t('create.result.creating')}
157
+ </div>
158
+ ) : resultPath ? (
159
+ <div className="mt-2">
160
+ <div className="flex items-center gap-2 text-xs text-emerald-600 dark:text-emerald-400">
161
+ <CheckCircle2 className="h-4 w-4" />
162
+ {t('create.result.success')}
163
+ </div>
164
+ <div className="mt-2 rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2 font-mono text-xs text-slate-900 dark:text-slate-100">
165
+ {resultPath}
166
+ </div>
167
+ <div className="mt-3">
168
+ <Link to="/">
169
+ <Button variant="secondary" size="sm">{t('create.result.backToList')}</Button>
170
+ </Link>
171
+ </div>
172
+ </div>
173
+ ) : (
174
+ <div className="mt-2 text-xs text-slate-500 dark:text-slate-400">{t('create.result.empty')}</div>
175
+ )}
176
+ </div>
177
+ </div>
178
+ </div>
179
+ </div>
180
+ )
181
+ }
@@ -0,0 +1,67 @@
1
+ import { ArrowLeft } from 'lucide-react'
2
+ import { Link } from 'react-router-dom'
3
+ import { useTranslation } from 'react-i18next'
4
+ import Button from '@/components/Button'
5
+
6
+ export default function HelpPage() {
7
+ const { t } = useTranslation()
8
+ return (
9
+ <div className="mx-auto w-full max-w-screen-xl px-4 py-6">
10
+ <div className="flex items-center gap-2">
11
+ <Link to="/">
12
+ <Button variant="ghost" size="sm">
13
+ <ArrowLeft className="h-4 w-4" />
14
+ {t('create.back')}
15
+ </Button>
16
+ </Link>
17
+ <div className="text-lg font-semibold text-slate-900 dark:text-slate-100">{t('helpPage.title')}</div>
18
+ </div>
19
+
20
+ <div className="mt-5 grid grid-cols-12 gap-4">
21
+ <div className="col-span-12 lg:col-span-7">
22
+ <div className="rounded-xl border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-950 p-4">
23
+ <div className="text-sm font-semibold text-slate-900 dark:text-slate-100">{t('helpPage.commands')}</div>
24
+ <div className="mt-3 space-y-2 font-mono text-xs text-slate-700 dark:text-slate-200">
25
+ <div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree</div>
26
+ <div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree list</div>
27
+ <div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree create &lt;branch&gt;</div>
28
+ <div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree delete</div>
29
+ <div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree open &lt;path|branch&gt;</div>
30
+ <div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree lock &lt;path|branch&gt;</div>
31
+ <div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree unlock &lt;path|branch&gt;</div>
32
+ <div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree prune</div>
33
+ <div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree config</div>
34
+ <div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree config get &lt;key&gt;</div>
35
+ <div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree config set &lt;key&gt; &lt;value&gt;</div>
36
+ <div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree --ui</div>
37
+ <div className="rounded-md border border-slate-200 bg-slate-50 dark:border-slate-800 dark:bg-slate-900 px-3 py-2">wtree --ui --no-open --port 5173</div>
38
+ </div>
39
+ <div className="mt-3 text-xs text-slate-500 dark:text-slate-400">
40
+ {t('helpPage.commandsDesc')}
41
+ </div>
42
+ </div>
43
+ </div>
44
+
45
+ <div className="col-span-12 lg:col-span-5">
46
+ <div className="rounded-xl border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-950 p-4">
47
+ <div className="text-sm font-semibold text-slate-900 dark:text-slate-100">{t('helpPage.faq.title')}</div>
48
+ <div className="mt-2 space-y-2 text-xs text-slate-500 dark:text-slate-400">
49
+ <div>
50
+ <div className="text-slate-700 dark:text-slate-200">{t('helpPage.faq.q1')}</div>
51
+ <div className="mt-1">{t('helpPage.faq.a1')}</div>
52
+ </div>
53
+ <div>
54
+ <div className="text-slate-700 dark:text-slate-200">{t('helpPage.faq.q2')}</div>
55
+ <div className="mt-1">{t('helpPage.faq.a2')}</div>
56
+ </div>
57
+ <div>
58
+ <div className="text-slate-700 dark:text-slate-200">{t('helpPage.faq.q3')}</div>
59
+ <div className="mt-1">{t('helpPage.faq.a3')}</div>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ )
67
+ }
@@ -0,0 +1,3 @@
1
+ export default function Home() {
2
+ return <div></div>;
3
+ }
@@ -0,0 +1,218 @@
1
+ import { ArrowLeft, Save, RotateCcw, Eraser, Moon, Sun, Laptop, Languages } from 'lucide-react'
2
+ import { useEffect, useState } from 'react'
3
+ import { Link } from 'react-router-dom'
4
+ import { useTranslation } from 'react-i18next'
5
+ import Button from '@/components/Button'
6
+ import Input from '@/components/Input'
7
+ import { useToastStore } from '@/stores/toastStore'
8
+ import { useWorktreeStore } from '@/stores/worktreeStore'
9
+ import { useThemeStore } from '@/stores/themeStore'
10
+ import { apiGet, apiPut } from '@/utils/api'
11
+ import type { WtuiConfig } from '../../shared/wtui-types'
12
+
13
+ export default function SettingsPage() {
14
+ const { t, i18n } = useTranslation()
15
+ const toast = useToastStore((s) => s.push)
16
+ const prune = useWorktreeStore((s) => s.prune)
17
+ const { theme, setTheme } = useThemeStore()
18
+ const [cfg, setCfg] = useState<WtuiConfig>({})
19
+ const [loading, setLoading] = useState(false)
20
+ const [pruning, setPruning] = useState(false)
21
+
22
+ useEffect(() => {
23
+ setLoading(true)
24
+ apiGet<WtuiConfig>('/api/config')
25
+ .then((r) => {
26
+ if (r.ok) setCfg(r.data)
27
+ })
28
+ .finally(() => setLoading(false))
29
+ }, [])
30
+
31
+ const onSave = async () => {
32
+ setLoading(true)
33
+ try {
34
+ const r = await apiPut<WtuiConfig, WtuiConfig>('/api/config', cfg)
35
+ if (!r.ok) {
36
+ const msg = (r as { ok: false; error: { message: string } }).error.message
37
+ toast({ type: 'error', title: t('settings.toast.saveFailed'), detail: msg })
38
+ return
39
+ }
40
+ setCfg(r.data)
41
+ toast({ type: 'success', title: t('settings.toast.saveSuccess') })
42
+ } finally {
43
+ setLoading(false)
44
+ }
45
+ }
46
+
47
+ const onReset = async () => {
48
+ setCfg({})
49
+ setLoading(true)
50
+ try {
51
+ const r = await apiPut<WtuiConfig, WtuiConfig>('/api/config', {})
52
+ if (!r.ok) {
53
+ const msg = (r as { ok: false; error: { message: string } }).error.message
54
+ toast({ type: 'error', title: t('settings.toast.resetFailed', '重置失败'), detail: msg })
55
+ return
56
+ }
57
+ setCfg(r.data)
58
+ toast({ type: 'info', title: t('settings.toast.resetSuccess', '已重置') })
59
+ } finally {
60
+ setLoading(false)
61
+ }
62
+ }
63
+
64
+ const onPrune = async () => {
65
+ setPruning(true)
66
+ try {
67
+ const ok = await prune()
68
+ toast({ type: ok ? 'success' : 'error', title: ok ? t('settings.toast.pruneSuccess') : t('settings.toast.pruneFailed') })
69
+ } finally {
70
+ setPruning(false)
71
+ }
72
+ }
73
+
74
+ return (
75
+ <div className="mx-auto w-full max-w-screen-xl px-4 py-6">
76
+ <div className="flex items-center justify-between gap-4">
77
+ <div className="flex items-center gap-2">
78
+ <Link to="/">
79
+ <Button variant="ghost" size="sm">
80
+ <ArrowLeft className="h-4 w-4" />
81
+ {t('create.back')}
82
+ </Button>
83
+ </Link>
84
+ <div className="text-lg font-semibold text-slate-900 dark:text-slate-100">{t('settings.title')}</div>
85
+ </div>
86
+ <div className="flex flex-col sm:flex-row items-center sm:justify-end gap-2">
87
+ <Button className="w-full sm:w-auto" variant="secondary" size="sm" onClick={onReset} disabled={loading}>
88
+ <RotateCcw className="h-4 w-4" />
89
+ {t('settings.cli.reset')}
90
+ </Button>
91
+ <Button className="w-full sm:w-auto" variant="primary" size="sm" onClick={onSave} disabled={loading}>
92
+ <Save className="h-4 w-4" />
93
+ {t('settings.cli.save')}
94
+ </Button>
95
+ </div>
96
+ </div>
97
+
98
+ <div className="mt-5 grid grid-cols-12 gap-4">
99
+ <div className="col-span-12 lg:col-span-7">
100
+ <div className="rounded-xl border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-950">
101
+ <div className="text-sm font-semibold text-slate-900 dark:text-slate-100">{t('settings.preferences.title')}</div>
102
+ <div className="mt-4 grid grid-cols-12 gap-4">
103
+ <div className="col-span-12 lg:col-span-6">
104
+ <div className="text-xs font-medium text-slate-600 dark:text-slate-300">{t('settings.preferences.theme')}</div>
105
+ <div className="mt-2 flex gap-2">
106
+ <Button
107
+ size="sm"
108
+ variant={theme === 'light' ? 'primary' : 'secondary'}
109
+ onClick={() => setTheme('light')}
110
+ >
111
+ <Sun className="h-4 w-4" />
112
+ {t('settings.preferences.light')}
113
+ </Button>
114
+ <Button
115
+ size="sm"
116
+ variant={theme === 'dark' ? 'primary' : 'secondary'}
117
+ onClick={() => setTheme('dark')}
118
+ >
119
+ <Moon className="h-4 w-4" />
120
+ {t('settings.preferences.dark')}
121
+ </Button>
122
+ <Button
123
+ size="sm"
124
+ variant={theme === 'system' ? 'primary' : 'secondary'}
125
+ onClick={() => setTheme('system')}
126
+ >
127
+ <Laptop className="h-4 w-4" />
128
+ {t('settings.preferences.system')}
129
+ </Button>
130
+ </div>
131
+ </div>
132
+
133
+ <div className="col-span-12 lg:col-span-6">
134
+ <div className="text-xs font-medium text-slate-600 dark:text-slate-300">{t('settings.preferences.language')}</div>
135
+ <div className="mt-2 flex gap-2">
136
+ <Button
137
+ size="sm"
138
+ variant={i18n.language.startsWith('en') ? 'primary' : 'secondary'}
139
+ onClick={() => i18n.changeLanguage('en')}
140
+ >
141
+ <Languages className="h-4 w-4" />
142
+ {t('settings.preferences.en')}
143
+ </Button>
144
+ <Button
145
+ size="sm"
146
+ variant={i18n.language.startsWith('zh') ? 'primary' : 'secondary'}
147
+ onClick={() => i18n.changeLanguage('zh')}
148
+ >
149
+ <Languages className="h-4 w-4" />
150
+ {t('settings.preferences.zh')}
151
+ </Button>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ </div>
156
+
157
+ <div className="mt-4 rounded-xl border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-950">
158
+ <div className="text-sm font-semibold text-slate-900 dark:text-slate-100">{t('settings.cli.title')}</div>
159
+ <div className="mt-4 grid grid-cols-12 gap-3">
160
+ <div className="col-span-12">
161
+ <div className="text-xs font-medium text-slate-600 dark:text-slate-300">{t('settings.cli.baseDir')}</div>
162
+ <div className="mt-1">
163
+ <Input
164
+ value={cfg.baseDir || ''}
165
+ onChange={(e) => setCfg((c) => ({ ...c, baseDir: e.target.value }))}
166
+ placeholder={t('settings.cli.baseDirPlaceholder')}
167
+ />
168
+ </div>
169
+ </div>
170
+ <div className="col-span-12">
171
+ <div className="text-xs font-medium text-slate-600 dark:text-slate-300">{t('settings.cli.openCommand')}</div>
172
+ <div className="mt-1">
173
+ <Input
174
+ value={cfg.openCommand || ''}
175
+ onChange={(e) => setCfg((c) => ({ ...c, openCommand: e.target.value }))}
176
+ placeholder={t('settings.cli.openCommandPlaceholder')}
177
+ />
178
+ </div>
179
+ </div>
180
+ <div className="col-span-12">
181
+ <div className="text-xs font-medium text-slate-600 dark:text-slate-300">{t('settings.cli.editorCommand')}</div>
182
+ <div className="mt-1">
183
+ <Input
184
+ value={cfg.editorCommand || ''}
185
+ onChange={(e) => setCfg((c) => ({ ...c, editorCommand: e.target.value }))}
186
+ placeholder={t('settings.cli.editorCommandPlaceholder')}
187
+ />
188
+ </div>
189
+ </div>
190
+ </div>
191
+ </div>
192
+
193
+ <div className="mt-4 rounded-xl border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-950">
194
+ <div className="text-sm font-semibold text-slate-900 dark:text-slate-100">{t('settings.maintenance.title')}</div>
195
+ <div className="mt-3">
196
+ <Button className="w-full sm:w-auto" variant="secondary" size="sm" onClick={onPrune} disabled={pruning}>
197
+ <Eraser className="h-4 w-4" />
198
+ {t('settings.maintenance.prune')}
199
+ </Button>
200
+ <div className="mt-2 text-xs text-slate-500 dark:text-slate-400">
201
+ {t('settings.maintenance.pruneDesc')}
202
+ </div>
203
+ </div>
204
+ </div>
205
+ </div>
206
+
207
+ <div className="col-span-12 lg:col-span-5">
208
+ <div className="rounded-xl border border-slate-200 bg-white p-4 dark:border-slate-800 dark:bg-slate-950">
209
+ <div className="text-sm font-semibold text-slate-900 dark:text-slate-100">{t('settings.note.title')}</div>
210
+ <div className="mt-2 text-xs text-slate-500 dark:text-slate-400">
211
+ {t('settings.note.desc')}
212
+ </div>
213
+ </div>
214
+ </div>
215
+ </div>
216
+ </div>
217
+ )
218
+ }