@orderful/droid 0.15.0 → 0.16.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 (149) hide show
  1. package/.claude/CLAUDE.md +19 -1
  2. package/CHANGELOG.md +13 -0
  3. package/dist/bin/droid.js +8 -8
  4. package/dist/bin/droid.js.map +1 -1
  5. package/dist/commands/config.js +1 -1
  6. package/dist/commands/config.js.map +1 -1
  7. package/dist/commands/install.js +3 -3
  8. package/dist/commands/install.js.map +1 -1
  9. package/dist/commands/setup.d.ts +1 -1
  10. package/dist/commands/setup.d.ts.map +1 -1
  11. package/dist/commands/setup.js +3 -3
  12. package/dist/commands/setup.js.map +1 -1
  13. package/dist/commands/skills.js +4 -4
  14. package/dist/commands/skills.js.map +1 -1
  15. package/dist/commands/tui/components/Badge.d.ts +13 -0
  16. package/dist/commands/tui/components/Badge.d.ts.map +1 -0
  17. package/dist/commands/tui/components/Badge.js +29 -0
  18. package/dist/commands/tui/components/Badge.js.map +1 -0
  19. package/dist/commands/tui/components/Markdown.d.ts +5 -0
  20. package/dist/commands/tui/components/Markdown.d.ts.map +1 -0
  21. package/dist/commands/tui/components/Markdown.js +42 -0
  22. package/dist/commands/tui/components/Markdown.js.map +1 -0
  23. package/dist/commands/tui/components/SettingsDetails.d.ts +5 -0
  24. package/dist/commands/tui/components/SettingsDetails.d.ts.map +1 -0
  25. package/dist/commands/tui/components/SettingsDetails.js +11 -0
  26. package/dist/commands/tui/components/SettingsDetails.js.map +1 -0
  27. package/dist/commands/tui/components/TabBar.d.ts +10 -0
  28. package/dist/commands/tui/components/TabBar.d.ts.map +1 -0
  29. package/dist/commands/tui/components/TabBar.js +7 -0
  30. package/dist/commands/tui/components/TabBar.js.map +1 -0
  31. package/dist/commands/tui/components/ToolDetails.d.ts +8 -0
  32. package/dist/commands/tui/components/ToolDetails.d.ts.map +1 -0
  33. package/dist/commands/tui/components/ToolDetails.js +35 -0
  34. package/dist/commands/tui/components/ToolDetails.js.map +1 -0
  35. package/dist/commands/tui/components/ToolItem.d.ts +9 -0
  36. package/dist/commands/tui/components/ToolItem.d.ts.map +1 -0
  37. package/dist/commands/tui/components/ToolItem.js +11 -0
  38. package/dist/commands/tui/components/ToolItem.js.map +1 -0
  39. package/dist/commands/tui/constants.d.ts +16 -0
  40. package/dist/commands/tui/constants.d.ts.map +1 -0
  41. package/dist/commands/tui/constants.js +17 -0
  42. package/dist/commands/tui/constants.js.map +1 -0
  43. package/dist/commands/tui/hooks/useAppUpdate.d.ts +13 -0
  44. package/dist/commands/tui/hooks/useAppUpdate.d.ts.map +1 -0
  45. package/dist/commands/tui/hooks/useAppUpdate.js +52 -0
  46. package/dist/commands/tui/hooks/useAppUpdate.js.map +1 -0
  47. package/dist/commands/tui/hooks/useToolUpdates.d.ts +22 -0
  48. package/dist/commands/tui/hooks/useToolUpdates.d.ts.map +1 -0
  49. package/dist/commands/tui/hooks/useToolUpdates.js +77 -0
  50. package/dist/commands/tui/hooks/useToolUpdates.js.map +1 -0
  51. package/dist/commands/tui/types.d.ts +5 -0
  52. package/dist/commands/tui/types.d.ts.map +1 -0
  53. package/dist/commands/tui/types.js +2 -0
  54. package/dist/commands/tui/types.js.map +1 -0
  55. package/dist/commands/tui/views/ReadmeViewer.d.ts +7 -0
  56. package/dist/commands/tui/views/ReadmeViewer.d.ts.map +1 -0
  57. package/dist/commands/tui/views/ReadmeViewer.js +56 -0
  58. package/dist/commands/tui/views/ReadmeViewer.js.map +1 -0
  59. package/dist/commands/tui/views/SetupScreen.d.ts +8 -0
  60. package/dist/commands/tui/views/SetupScreen.d.ts.map +1 -0
  61. package/dist/commands/tui/views/SetupScreen.js +114 -0
  62. package/dist/commands/tui/views/SetupScreen.js.map +1 -0
  63. package/dist/commands/tui/views/SkillConfigScreen.d.ts +8 -0
  64. package/dist/commands/tui/views/SkillConfigScreen.d.ts.map +1 -0
  65. package/dist/commands/tui/views/SkillConfigScreen.js +148 -0
  66. package/dist/commands/tui/views/SkillConfigScreen.js.map +1 -0
  67. package/dist/commands/tui/views/ToolExplorer.d.ts +8 -0
  68. package/dist/commands/tui/views/ToolExplorer.d.ts.map +1 -0
  69. package/dist/commands/tui/views/ToolExplorer.js +86 -0
  70. package/dist/commands/tui/views/ToolExplorer.js.map +1 -0
  71. package/dist/commands/tui/views/ToolUpdatePrompt.d.ts +10 -0
  72. package/dist/commands/tui/views/ToolUpdatePrompt.d.ts.map +1 -0
  73. package/dist/commands/tui/views/ToolUpdatePrompt.js +38 -0
  74. package/dist/commands/tui/views/ToolUpdatePrompt.js.map +1 -0
  75. package/dist/commands/tui/views/WelcomeScreen.d.ts +11 -0
  76. package/dist/commands/tui/views/WelcomeScreen.d.ts.map +1 -0
  77. package/dist/commands/tui/views/WelcomeScreen.js +46 -0
  78. package/dist/commands/tui/views/WelcomeScreen.js.map +1 -0
  79. package/dist/commands/tui.d.ts.map +1 -1
  80. package/dist/commands/tui.js +54 -755
  81. package/dist/commands/tui.js.map +1 -1
  82. package/dist/commands/uninstall.js +2 -2
  83. package/dist/commands/uninstall.js.map +1 -1
  84. package/dist/commands/update.js +1 -1
  85. package/dist/commands/update.js.map +1 -1
  86. package/dist/index.d.ts +4 -4
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +4 -4
  89. package/dist/index.js.map +1 -1
  90. package/dist/lib/agents.d.ts +1 -1
  91. package/dist/lib/agents.d.ts.map +1 -1
  92. package/dist/lib/agents.js +4 -4
  93. package/dist/lib/agents.js.map +1 -1
  94. package/dist/lib/config.d.ts +1 -1
  95. package/dist/lib/config.d.ts.map +1 -1
  96. package/dist/lib/config.js +1 -1
  97. package/dist/lib/config.js.map +1 -1
  98. package/dist/lib/platforms.d.ts +1 -1
  99. package/dist/lib/platforms.d.ts.map +1 -1
  100. package/dist/lib/platforms.js +1 -1
  101. package/dist/lib/platforms.js.map +1 -1
  102. package/dist/lib/skill-config.d.ts +1 -1
  103. package/dist/lib/skill-config.d.ts.map +1 -1
  104. package/dist/lib/skill-config.js +2 -2
  105. package/dist/lib/skill-config.js.map +1 -1
  106. package/dist/lib/skills.d.ts +1 -1
  107. package/dist/lib/skills.d.ts.map +1 -1
  108. package/dist/lib/skills.js +5 -5
  109. package/dist/lib/skills.js.map +1 -1
  110. package/dist/lib/tools.d.ts +1 -1
  111. package/dist/lib/tools.d.ts.map +1 -1
  112. package/dist/lib/tools.js +3 -3
  113. package/dist/lib/tools.js.map +1 -1
  114. package/package.json +3 -3
  115. package/src/bin/droid.ts +8 -8
  116. package/src/commands/config.ts +1 -1
  117. package/src/commands/install.ts +3 -3
  118. package/src/commands/setup.test.ts +1 -1
  119. package/src/commands/setup.ts +3 -3
  120. package/src/commands/skills.ts +4 -4
  121. package/src/commands/tui/components/Badge.tsx +86 -0
  122. package/src/commands/tui/components/Markdown.tsx +48 -0
  123. package/src/commands/tui/components/SettingsDetails.tsx +70 -0
  124. package/src/commands/tui/components/TabBar.tsx +26 -0
  125. package/src/commands/tui/components/ToolDetails.tsx +117 -0
  126. package/src/commands/tui/components/ToolItem.tsx +39 -0
  127. package/src/commands/tui/constants.ts +17 -0
  128. package/src/commands/tui/hooks/useAppUpdate.ts +67 -0
  129. package/src/commands/tui/hooks/useToolUpdates.ts +110 -0
  130. package/src/commands/tui/types.ts +4 -0
  131. package/src/commands/tui/views/ReadmeViewer.tsx +93 -0
  132. package/src/commands/tui/views/SetupScreen.tsx +242 -0
  133. package/src/commands/tui/views/SkillConfigScreen.tsx +278 -0
  134. package/src/commands/tui/views/ToolExplorer.tsx +190 -0
  135. package/src/commands/tui/views/ToolUpdatePrompt.tsx +109 -0
  136. package/src/commands/tui/views/WelcomeScreen.tsx +149 -0
  137. package/src/commands/tui.tsx +65 -1587
  138. package/src/commands/uninstall.ts +2 -2
  139. package/src/commands/update.ts +1 -1
  140. package/src/index.ts +4 -4
  141. package/src/lib/agents.ts +4 -4
  142. package/src/lib/config.ts +1 -1
  143. package/src/lib/platforms.ts +1 -1
  144. package/src/lib/skill-config.ts +2 -2
  145. package/src/lib/skills.test.ts +2 -2
  146. package/src/lib/skills.ts +5 -5
  147. package/src/lib/tools.ts +3 -3
  148. package/src/lib/types.test.ts +1 -1
  149. package/src/lib/version.test.ts +1 -1
@@ -1,1416 +1,32 @@
1
1
  import { render, Box, Text, useInput, useApp } from 'ink';
2
- import TextInput from 'ink-text-input';
3
- import { useState, useMemo } from 'react';
4
- import { execSync } from 'child_process';
5
- import { existsSync, readFileSync } from 'fs';
6
- import { join } from 'path';
2
+ import { useState } from 'react';
7
3
  import {
8
4
  getBundledSkills,
9
- isSkillInstalled,
10
5
  installSkill,
11
6
  uninstallSkill,
12
7
  updateSkill,
13
- getSkillUpdateStatus,
14
- } from '../lib/skills.js';
15
- import { getBundledTools, getBundledToolsDir, isToolInstalled, getToolUpdateStatus, getInstalledToolVersion, getToolsWithUpdates, type ToolUpdateInfo } from '../lib/tools.js';
16
- import { configExists, loadConfig, saveConfig, loadSkillOverrides, saveSkillOverrides, getAutoUpdateConfig, setAutoUpdateConfig } from '../lib/config.js';
17
- import { configurePlatformPermissions } from './setup.js';
18
- import { Platform, BuiltInOutput, ConfigOptionType, type DroidConfig, type OutputPreference, type SkillManifest, type ConfigOption, type SkillOverrides, type ToolManifest } from '../lib/types.js';
19
- import { getVersion, getUpdateInfo, runUpdate, type UpdateInfo } from '../lib/version.js';
20
- import { getRandomQuote } from '../lib/quotes.js';
8
+ } from '../lib/skills';
9
+ import { getBundledTools, isToolInstalled, getToolUpdateStatus } from '../lib/tools';
10
+ import { configExists, loadConfig } from '../lib/config';
11
+ import { Platform, type ConfigOption, type ToolManifest } from '../lib/types';
12
+ import { getVersion } from '../lib/version';
13
+ import { type Tab, type View } from './tui/types';
14
+ import { colors, MAX_VISIBLE_ITEMS } from './tui/constants';
15
+ import { TabBar } from './tui/components/TabBar';
16
+ import { ToolItem } from './tui/components/ToolItem';
17
+ import { ToolDetails } from './tui/components/ToolDetails';
18
+ import { SettingsDetails } from './tui/components/SettingsDetails';
19
+ import { WelcomeScreen } from './tui/views/WelcomeScreen';
20
+ import { ToolUpdatePrompt } from './tui/views/ToolUpdatePrompt';
21
+ import { SetupScreen } from './tui/views/SetupScreen';
22
+ import { ReadmeViewer } from './tui/views/ReadmeViewer';
23
+ import { ToolExplorer } from './tui/views/ToolExplorer';
24
+ import { SkillConfigScreen } from './tui/views/SkillConfigScreen';
25
+ import { useAppUpdate } from './tui/hooks/useAppUpdate';
26
+ import { useToolUpdates } from './tui/hooks/useToolUpdates';
21
27
 
22
- type Tab = 'tools' | 'settings';
23
- type View = 'welcome' | 'tool-updates' | 'setup' | 'menu' | 'detail' | 'configure' | 'readme' | 'explorer';
24
-
25
- // Module-level variable to store exit message (printed after leaving alternate screen)
26
- let exitMessage: string | null = null;
27
- type SetupStep = 'platform' | 'user_mention' | 'confirm';
28
- type ComponentType = 'skill' | 'command' | 'agent';
29
-
30
- const colors = {
31
- primary: '#6366f1',
32
- bgSelected: '#2d2d2d',
33
- border: '#3a3a3a',
34
- text: '#e8e8e8',
35
- textMuted: '#999999',
36
- textDim: '#6a6a6a',
37
- success: '#4ade80',
38
- error: '#f87171',
39
- // Component type badges
40
- skill: '#ec4899', // pink/magenta
41
- command: '#22d3ee', // cyan
42
- agent: '#fbbf24', // yellow/amber
43
- };
44
-
45
- function Badge({
46
- type,
47
- label,
48
- isSelected = false,
49
- dimmed = false,
50
- }: {
51
- type: ComponentType;
52
- label?: string;
53
- isSelected?: boolean;
54
- dimmed?: boolean;
55
- }) {
56
- const color = colors[type];
57
- const displayLabel = label || type.charAt(0).toUpperCase() + type.slice(1);
58
-
59
- if (dimmed) {
60
- return (
61
- <Text color={colors.textDim}>{displayLabel}</Text>
62
- );
63
- }
64
-
65
- return (
66
- <Text
67
- backgroundColor={isSelected ? color : undefined}
68
- color={isSelected ? '#000000' : color}
69
- bold={isSelected}
70
- >
71
- {isSelected ? ` ${displayLabel} ` : displayLabel}
72
- </Text>
73
- );
74
- }
75
-
76
- function ComponentBadges({ tool, compact = false }: { tool: ToolManifest; compact?: boolean }) {
77
- const hasSkills = tool.includes.skills.length > 0;
78
- const hasCommands = tool.includes.commands.length > 0;
79
- const hasAgents = tool.includes.agents.length > 0;
80
-
81
- if (compact) {
82
- // Show colored squares for list view (single char with spacing)
83
- const parts: string[] = [];
84
- if (hasSkills) parts.push('skill');
85
- if (hasCommands) parts.push('command');
86
- if (hasAgents) parts.push('agent');
87
-
88
- return (
89
- <Box flexDirection="row">
90
- {parts.map((type, i) => (
91
- <Text key={type}>
92
- <Text backgroundColor={colors[type as ComponentType]}> </Text>
93
- {i < parts.length - 1 && ' '}
94
- </Text>
95
- ))}
96
- </Box>
97
- );
98
- }
99
-
100
- return (
101
- <Box flexDirection="row">
102
- {hasSkills && (
103
- <Box marginRight={1}>
104
- <Text backgroundColor={colors.skill} color="#000000" bold>
105
- {` ${tool.includes.skills.length} skill${tool.includes.skills.length > 1 ? 's' : ''} `}
106
- </Text>
107
- </Box>
108
- )}
109
- {hasCommands && (
110
- <Box marginRight={1}>
111
- <Text backgroundColor={colors.command} color="#000000" bold>
112
- {` ${tool.includes.commands.length} cmd${tool.includes.commands.length > 1 ? 's' : ''} `}
113
- </Text>
114
- </Box>
115
- )}
116
- {hasAgents && (
117
- <Box marginRight={1}>
118
- <Text backgroundColor={colors.agent} color="#000000" bold>
119
- {` ${tool.includes.agents.length} agent${tool.includes.agents.length > 1 ? 's' : ''} `}
120
- </Text>
121
- </Box>
122
- )}
123
- </Box>
124
- );
125
- }
126
-
127
- function detectInstalledPlatforms(): Set<Platform> {
128
- const installed = new Set<Platform>();
129
- try {
130
- execSync('claude --version', { stdio: 'ignore' });
131
- installed.add(Platform.ClaudeCode);
132
- } catch {
133
- // Claude Code not found
134
- }
135
- try {
136
- execSync('opencode --version', { stdio: 'ignore' });
137
- installed.add(Platform.OpenCode);
138
- } catch {
139
- // OpenCode not found
140
- }
141
- return installed;
142
- }
143
-
144
- function getDefaultPlatform(): Platform {
145
- const installed = detectInstalledPlatforms();
146
- if (installed.has(Platform.ClaudeCode)) return Platform.ClaudeCode;
147
- if (installed.has(Platform.OpenCode)) return Platform.OpenCode;
148
- return Platform.ClaudeCode;
149
- }
150
-
151
- function getOutputOptions(): Array<{ label: string; value: OutputPreference }> {
152
- const options: Array<{ label: string; value: OutputPreference }> = [
153
- { label: 'Terminal (display in CLI)', value: BuiltInOutput.Terminal },
154
- { label: 'Editor ($EDITOR)', value: BuiltInOutput.Editor },
155
- ];
156
- const skills = getBundledSkills();
157
- for (const skill of skills) {
158
- if (skill.provides_output) {
159
- options.push({ label: `${skill.name} (${skill.description})`, value: skill.name });
160
- }
161
- }
162
- return options;
163
- }
164
-
165
- interface WelcomeScreenProps {
166
- onContinue: () => void;
167
- onUpdate: () => void;
168
- onAlways: () => void;
169
- onExit: () => void;
170
- updateInfo: UpdateInfo;
171
- isUpdating: boolean;
172
- }
173
-
174
- function WelcomeScreen({ onContinue, onUpdate, onAlways, onExit, updateInfo, isUpdating }: WelcomeScreenProps) {
175
- const [selectedButton, setSelectedButton] = useState(0);
176
- const welcomeQuote = useMemo(() => getRandomQuote(), []);
177
-
178
- useInput((input, key) => {
179
- if (isUpdating) return;
180
-
181
- if (updateInfo.hasUpdate) {
182
- if (key.leftArrow) {
183
- setSelectedButton((prev) => Math.max(0, prev - 1));
184
- }
185
- if (key.rightArrow) {
186
- setSelectedButton((prev) => Math.min(2, prev + 1));
187
- }
188
- if (key.return) {
189
- if (selectedButton === 0) {
190
- onUpdate();
191
- } else if (selectedButton === 1) {
192
- onAlways();
193
- } else {
194
- onContinue();
195
- }
196
- }
197
- if (input === 'q') {
198
- onExit();
199
- }
200
- } else {
201
- if (key.return) {
202
- onContinue();
203
- }
204
- if (input === 'q') {
205
- onExit();
206
- }
207
- }
208
- });
209
-
210
- const hasUpdate = updateInfo.hasUpdate && updateInfo.latestVersion;
211
-
212
- return (
213
- <Box flexDirection="column" alignItems="center" justifyContent="center" height={18}>
214
- <Box
215
- flexDirection="column"
216
- alignItems="center"
217
- borderStyle="single"
218
- borderColor={hasUpdate ? '#eab308' : colors.border}
219
- paddingX={4}
220
- paddingY={1}
221
- >
222
- <Box flexDirection="column">
223
- <Text>
224
- <Text color={colors.textDim}>╔═════╗ </Text>
225
- <Text color={colors.text}>droid</Text>
226
- <Text color={colors.textDim}> v{updateInfo.currentVersion}</Text>
227
- </Text>
228
- <Text>
229
- <Text color={colors.textDim}>║ </Text>
230
- <Text color={hasUpdate ? '#eab308' : colors.primary}>●</Text>
231
- <Text color={colors.textDim}> </Text>
232
- <Text color={hasUpdate ? '#eab308' : colors.primary}>●</Text>
233
- <Text color={colors.textDim}> ║ </Text>
234
- <Text color={colors.textMuted}>Droid, teaching your AI new tricks</Text>
235
- </Text>
236
- <Text>
237
- <Text color={colors.textDim}>╚═╦═╦═╝ </Text>
238
- <Text color={colors.textDim}>github.com/Orderful/droid</Text>
239
- </Text>
240
- </Box>
241
-
242
- {hasUpdate ? (
243
- <>
244
- <Box marginTop={2} marginBottom={1} flexDirection="column" alignItems="center">
245
- <Text color="#eab308" italic>
246
- "The odds of functioning optimally without this
247
- </Text>
248
- <Text color="#eab308" italic>
249
- update are approximately 3,720 to 1."
250
- </Text>
251
- </Box>
252
-
253
- <Box marginBottom={1}>
254
- <Text color={colors.textMuted}>
255
- v{updateInfo.currentVersion} → v{updateInfo.latestVersion}
256
- </Text>
257
- </Box>
258
-
259
- {isUpdating ? (
260
- <Text color="#eab308">Updating...</Text>
261
- ) : (
262
- <Box flexDirection="row">
263
- <Text
264
- backgroundColor={selectedButton === 0 ? '#eab308' : colors.bgSelected}
265
- color={selectedButton === 0 ? '#000000' : colors.textMuted}
266
- bold={selectedButton === 0}
267
- >
268
- {' '}Update{' '}
269
- </Text>
270
- <Text> </Text>
271
- <Text
272
- backgroundColor={selectedButton === 1 ? '#eab308' : colors.bgSelected}
273
- color={selectedButton === 1 ? '#000000' : colors.textMuted}
274
- bold={selectedButton === 1}
275
- >
276
- {' '}Always{' '}
277
- </Text>
278
- <Text> </Text>
279
- <Text
280
- backgroundColor={selectedButton === 2 ? colors.bgSelected : undefined}
281
- color={selectedButton === 2 ? colors.text : colors.textMuted}
282
- bold={selectedButton === 2}
283
- >
284
- {' '}Skip{' '}
285
- </Text>
286
- </Box>
287
- )}
288
-
289
- <Box marginTop={1}>
290
- <Text color={colors.textDim}>←→ select · enter · "Always" enables auto-update</Text>
291
- </Box>
292
- </>
293
- ) : (
294
- <>
295
- <Box marginTop={2} marginBottom={1}>
296
- <Text backgroundColor={colors.primary} color="#ffffff" bold>
297
- {` ${welcomeQuote} `}
298
- </Text>
299
- </Box>
300
-
301
- <Text color={colors.textDim}>press enter</Text>
302
- </>
303
- )}
304
- </Box>
305
- </Box>
306
- );
307
- }
308
-
309
- interface ToolUpdatePromptProps {
310
- toolUpdates: ToolUpdateInfo[];
311
- onUpdateAll: () => void;
312
- onAlways: () => void;
313
- onSkip: () => void;
314
- isUpdating: boolean;
315
- }
316
-
317
- function ToolUpdatePrompt({ toolUpdates, onUpdateAll, onAlways, onSkip, isUpdating }: ToolUpdatePromptProps) {
318
- const [selectedButton, setSelectedButton] = useState(0);
319
-
320
- useInput((input, key) => {
321
- if (isUpdating) return;
322
-
323
- if (key.leftArrow) {
324
- setSelectedButton((prev) => Math.max(0, prev - 1));
325
- }
326
- if (key.rightArrow) {
327
- setSelectedButton((prev) => Math.min(2, prev + 1));
328
- }
329
- if (key.return) {
330
- if (selectedButton === 0) {
331
- onUpdateAll();
332
- } else if (selectedButton === 1) {
333
- onAlways();
334
- } else {
335
- onSkip();
336
- }
337
- }
338
- if (input === 'q') {
339
- onSkip();
340
- }
341
- });
342
-
343
- const buttons = [
344
- { label: 'Update All', action: onUpdateAll },
345
- { label: 'Always', action: onAlways },
346
- { label: 'Skip', action: onSkip },
347
- ];
348
-
349
- return (
350
- <Box flexDirection="column" alignItems="center" justifyContent="center" height={18}>
351
- <Box
352
- flexDirection="column"
353
- alignItems="center"
354
- borderStyle="single"
355
- borderColor={colors.primary}
356
- paddingX={4}
357
- paddingY={1}
358
- >
359
- <Box flexDirection="column">
360
- <Text>
361
- <Text color={colors.textDim}>╔═════╗ </Text>
362
- <Text color={colors.text}>Tool Updates</Text>
363
- </Text>
364
- <Text>
365
- <Text color={colors.textDim}>║ </Text>
366
- <Text color={colors.primary}>●</Text>
367
- <Text color={colors.textDim}> </Text>
368
- <Text color={colors.primary}>●</Text>
369
- <Text color={colors.textDim}> ║ </Text>
370
- <Text color={colors.textMuted}>{toolUpdates.length} tool{toolUpdates.length > 1 ? 's have' : ' has'} updates available</Text>
371
- </Text>
372
- <Text>
373
- <Text color={colors.textDim}>╚═╦═╦═╝</Text>
374
- </Text>
375
- </Box>
376
-
377
- <Box marginTop={1} marginBottom={1} flexDirection="column">
378
- {toolUpdates.slice(0, 5).map((tool) => (
379
- <Text key={tool.name} color={colors.textMuted}>
380
- <Text color={colors.primary}>↑</Text> {tool.name}
381
- <Text color={colors.textDim}> {tool.installedVersion} → {tool.bundledVersion}</Text>
382
- </Text>
383
- ))}
384
- {toolUpdates.length > 5 && (
385
- <Text color={colors.textDim}>...and {toolUpdates.length - 5} more</Text>
386
- )}
387
- </Box>
388
-
389
- {isUpdating ? (
390
- <Text color={colors.primary}>Updating tools...</Text>
391
- ) : (
392
- <Box flexDirection="row">
393
- {buttons.map((button, index) => (
394
- <Text
395
- key={button.label}
396
- backgroundColor={selectedButton === index ? colors.primary : colors.bgSelected}
397
- color={selectedButton === index ? '#000000' : colors.textMuted}
398
- bold={selectedButton === index}
399
- >
400
- {' '}{button.label}{' '}
401
- </Text>
402
- ))}
403
- </Box>
404
- )}
405
-
406
- <Box marginTop={1}>
407
- <Text color={colors.textDim}>←→ select · enter · "Always" enables auto-update</Text>
408
- </Box>
409
- </Box>
410
- </Box>
411
- );
412
- }
413
-
414
- interface SetupScreenProps {
415
- onComplete: () => void;
416
- onSkip: () => void;
417
- initialConfig?: DroidConfig;
418
- }
419
-
420
- function SetupScreen({ onComplete, onSkip, initialConfig }: SetupScreenProps) {
421
- const [step, setStep] = useState<SetupStep>('platform');
422
- const [platform, setPlatform] = useState<Platform>(
423
- initialConfig?.platform || getDefaultPlatform()
424
- );
425
- const [userMention, setUserMention] = useState(initialConfig?.user_mention || '@user');
426
- const [selectedIndex, setSelectedIndex] = useState(0);
427
- const [error, setError] = useState<string | null>(null);
428
-
429
- // Cache detection results once on mount (avoids re-running execSync on every render)
430
- const installedPlatforms = useMemo(() => detectInstalledPlatforms(), []);
431
- const platformOptions = useMemo(() => [
432
- { label: 'Claude Code', value: Platform.ClaudeCode },
433
- { label: 'OpenCode', value: Platform.OpenCode },
434
- ], []);
435
-
436
- const steps: SetupStep[] = ['platform', 'user_mention', 'confirm'];
437
- const stepIndex = steps.indexOf(step);
438
- const totalSteps = steps.length - 1; // Don't count confirm as a step
439
-
440
- const handleUserMentionSubmit = () => {
441
- // Auto-add @ prefix if missing
442
- const mention = userMention.startsWith('@') ? userMention : `@${userMention}`;
443
- setUserMention(mention);
444
- setError(null);
445
- setStep('confirm');
446
- };
447
-
448
- // Handle escape during text input (only intercept escape, nothing else)
449
- useInput((input, key) => {
450
- if (key.escape) {
451
- setStep('platform');
452
- setSelectedIndex(0);
453
- }
454
- }, { isActive: step === 'user_mention' });
455
-
456
- // Handle all input for non-text-input steps
457
- useInput((input, key) => {
458
- if (key.escape) {
459
- if (step === 'platform') {
460
- onSkip();
461
- } else if (step === 'confirm') {
462
- setStep('user_mention');
463
- }
464
- return;
465
- }
466
-
467
- if (step === 'platform') {
468
- if (key.upArrow) setSelectedIndex((prev) => Math.max(0, prev - 1));
469
- if (key.downArrow) setSelectedIndex((prev) => Math.min(platformOptions.length - 1, prev + 1));
470
- if (key.return) {
471
- setPlatform(platformOptions[selectedIndex].value);
472
- setStep('user_mention');
473
- }
474
- } else if (step === 'confirm') {
475
- if (key.return) {
476
- const existingConfig = loadConfig();
477
- const config: DroidConfig = {
478
- ...existingConfig,
479
- platform: platform,
480
- user_mention: userMention,
481
- output_preference: BuiltInOutput.Terminal, // Default to terminal
482
- };
483
- saveConfig(config);
484
- configurePlatformPermissions(platform);
485
- onComplete();
486
- }
487
- }
488
- }, { isActive: step !== 'user_mention' });
489
-
490
- const renderHeader = () => (
491
- <Box flexDirection="column" marginBottom={1}>
492
- <Text>
493
- <Text color={colors.textDim}>[</Text>
494
- <Text color={colors.primary}>●</Text>
495
- <Text color={colors.textDim}> </Text>
496
- <Text color={colors.primary}>●</Text>
497
- <Text color={colors.textDim}>] </Text>
498
- <Text color={colors.text} bold>droid setup</Text>
499
- <Text color={colors.textDim}> · Step {Math.min(stepIndex + 1, totalSteps)} of {totalSteps}</Text>
500
- </Text>
501
- </Box>
502
- );
503
-
504
- if (step === 'platform') {
505
- return (
506
- <Box flexDirection="column" padding={1}>
507
- {renderHeader()}
508
- <Text color={colors.text}>Which platform are you using?</Text>
509
- <Box flexDirection="column" marginTop={1}>
510
- {platformOptions.map((option, index) => (
511
- <Text key={option.value}>
512
- <Text color={colors.textDim}>{index === selectedIndex ? '>' : ' '} </Text>
513
- <Text color={index === selectedIndex ? colors.text : colors.textMuted}>{option.label}</Text>
514
- {installedPlatforms.has(option.value) && <Text color={colors.success}> (detected)</Text>}
515
- </Text>
516
- ))}
517
- </Box>
518
- <Box marginTop={1}>
519
- <Text color={colors.textDim}>↑↓ select · enter next · esc skip</Text>
520
- </Box>
521
- </Box>
522
- );
523
- }
524
-
525
- if (step === 'user_mention') {
526
- return (
527
- <Box flexDirection="column" padding={1}>
528
- {renderHeader()}
529
- <Text color={colors.text}>What @mention should be used for you?</Text>
530
- <Box marginTop={1}>
531
- <Text color={colors.textDim}>{'> '}</Text>
532
- <TextInput
533
- value={userMention}
534
- onChange={setUserMention}
535
- onSubmit={handleUserMentionSubmit}
536
- placeholder="@user"
537
- />
538
- </Box>
539
- {error && (
540
- <Box marginTop={1}>
541
- <Text color={colors.error}>{error}</Text>
542
- </Box>
543
- )}
544
- <Box marginTop={1}>
545
- <Text color={colors.textDim}>enter next · esc back</Text>
546
- </Box>
547
- </Box>
548
- );
549
- }
550
-
551
- // Confirm step
552
- return (
553
- <Box flexDirection="column" padding={1}>
554
- {renderHeader()}
555
- <Text color={colors.text} bold>Review your settings</Text>
556
- <Box flexDirection="column" marginTop={1}>
557
- <Text>
558
- <Text color={colors.textDim}>Platform: </Text>
559
- <Text color={colors.text}>{platform === Platform.ClaudeCode ? 'Claude Code' : 'OpenCode'}</Text>
560
- </Text>
561
- <Text>
562
- <Text color={colors.textDim}>Your @mention: </Text>
563
- <Text color={colors.text}>{userMention}</Text>
564
- </Text>
565
- </Box>
566
- <Box marginTop={2}>
567
- <Text backgroundColor={colors.primary} color="#ffffff" bold>
568
- {' '}Save{' '}
569
- </Text>
570
- </Box>
571
- <Box marginTop={1}>
572
- <Text color={colors.textDim}>enter save · esc back</Text>
573
- </Box>
574
- </Box>
575
- );
576
- }
577
-
578
- function TabBar({ tabs, activeTab }: { tabs: { id: Tab; label: string }[]; activeTab: Tab }) {
579
- return (
580
- <Box flexDirection="row" flexWrap="wrap">
581
- {tabs.map((tab) => (
582
- <Text
583
- key={tab.id}
584
- backgroundColor={tab.id === activeTab ? colors.primary : undefined}
585
- color={tab.id === activeTab ? '#ffffff' : colors.textMuted}
586
- bold={tab.id === activeTab}
587
- wrap="truncate"
588
- >
589
- {' '}{tab.label}{' '}
590
- </Text>
591
- ))}
592
- </Box>
593
- );
594
- }
595
-
596
- function ToolItem({
597
- tool,
598
- isSelected,
599
- isActive,
600
- wasAutoUpdated,
601
- }: {
602
- tool: ToolManifest;
603
- isSelected: boolean;
604
- isActive: boolean;
605
- wasAutoUpdated?: boolean;
606
- }) {
607
- const installed = isToolInstalled(tool.name);
608
- const installedVersion = getInstalledToolVersion(tool.name);
609
- const updateStatus = getToolUpdateStatus(tool.name);
610
-
611
- return (
612
- <Box paddingX={1} backgroundColor={isActive ? colors.bgSelected : undefined}>
613
- <Text wrap="truncate">
614
- <Text color={colors.textDim}>{isSelected ? '>' : ' '} </Text>
615
- <Text color={isSelected || isActive ? colors.text : colors.textMuted}>{tool.name}</Text>
616
- {installed && installedVersion && <Text color={colors.textDim}> v{installedVersion}</Text>}
617
- {installed && <Text color={colors.success}> ✓</Text>}
618
- {wasAutoUpdated && <Text color={colors.success}> ↑</Text>}
619
- {updateStatus.hasUpdate && !wasAutoUpdated && <Text color={colors.primary}> ↑</Text>}
620
- <Text> </Text>
621
- {tool.includes.skills.length > 0 && <Text color={colors.skill}>● </Text>}
622
- {tool.includes.commands.length > 0 && <Text color={colors.command}>● </Text>}
623
- {tool.includes.agents.length > 0 && <Text color={colors.agent}>●</Text>}
624
- </Text>
625
- </Box>
626
- );
627
- }
628
-
629
- function ToolDetails({
630
- tool,
631
- isFocused,
632
- selectedAction,
633
- }: {
634
- tool: ToolManifest | null;
635
- isFocused: boolean;
636
- selectedAction: number;
637
- }) {
638
- if (!tool) {
639
- return (
640
- <Box paddingLeft={2} flexGrow={1}>
641
- <Text color={colors.textDim}>Select a tool</Text>
642
- </Box>
643
- );
644
- }
645
-
646
- const installed = isToolInstalled(tool.name);
647
- const installedVersion = getInstalledToolVersion(tool.name);
648
- const updateStatus = getToolUpdateStatus(tool.name);
649
- const isSystemTool = (tool as ToolManifest & { system?: boolean }).system === true;
650
-
651
- const actions = installed
652
- ? [
653
- { id: 'explore', label: 'Explore', variant: 'default' },
654
- ...(updateStatus.hasUpdate
655
- ? [{ id: 'update', label: `Update (${updateStatus.bundledVersion})`, variant: 'primary' }]
656
- : []),
657
- { id: 'configure', label: 'Configure', variant: 'default' },
658
- // System tools can't be uninstalled
659
- ...(!isSystemTool ? [{ id: 'uninstall', label: 'Uninstall', variant: 'danger' }] : []),
660
- ]
661
- : [
662
- { id: 'explore', label: 'Explore', variant: 'default' },
663
- // System tools auto-install, but show Install for manual trigger if needed
664
- { id: 'install', label: 'Install', variant: 'primary' },
665
- ];
666
-
667
- return (
668
- <Box flexDirection="column" paddingLeft={2} flexGrow={1}>
669
- <Text color={colors.text} bold>{tool.name}</Text>
670
-
671
- <Box marginTop={1}>
672
- <Text color={colors.textDim}>
673
- {tool.version}
674
- {tool.status && ` · ${tool.status}`}
675
- {installed && <Text color={colors.success}> · installed</Text>}
676
- {updateStatus.hasUpdate && (
677
- <Text color={colors.primary}> · update ({installedVersion} → {updateStatus.bundledVersion})</Text>
678
- )}
679
- </Text>
680
- </Box>
681
-
682
- <Box marginTop={1}>
683
- <Text color={colors.textMuted}>{tool.description}</Text>
684
- </Box>
685
-
686
- {/* Colored component badges */}
687
- <Box flexDirection="column" marginTop={1}>
688
- <Text color={colors.textDim}>Includes:</Text>
689
- <Box marginTop={1}>
690
- <ComponentBadges tool={tool} />
691
- </Box>
692
- {/* Show names below badges */}
693
- <Box flexDirection="column" marginTop={1} marginLeft={1}>
694
- {tool.includes.skills.length > 0 && (
695
- <Text>
696
- <Text color={colors.skill}>Skills: </Text>
697
- <Text color={colors.textMuted}>{tool.includes.skills.map(s => s.name).join(', ')}</Text>
698
- </Text>
699
- )}
700
- {tool.includes.commands.length > 0 && (
701
- <Text>
702
- <Text color={colors.command}>Commands: </Text>
703
- <Text color={colors.textMuted}>{tool.includes.commands.map(c => `/${c}`).join(', ')}</Text>
704
- </Text>
705
- )}
706
- {tool.includes.agents.length > 0 && (
707
- <Text>
708
- <Text color={colors.agent}>Agents: </Text>
709
- <Text color={colors.textMuted}>{tool.includes.agents.join(', ')}</Text>
710
- </Text>
711
- )}
712
- </Box>
713
- </Box>
714
-
715
- {isFocused && (
716
- <Box flexDirection="row" marginTop={1}>
717
- {actions.map((action, index) => (
718
- <Text
719
- key={action.id}
720
- backgroundColor={
721
- selectedAction === index
722
- ? action.variant === 'danger'
723
- ? colors.error
724
- : colors.primary
725
- : colors.bgSelected
726
- }
727
- color={selectedAction === index ? '#ffffff' : colors.textMuted}
728
- bold={selectedAction === index}
729
- >
730
- {' '}{action.label}{' '}
731
- </Text>
732
- ))}
733
- </Box>
734
- )}
735
- </Box>
736
- );
737
- }
738
-
739
- function MarkdownLine({ line, inCodeBlock }: { line: string; inCodeBlock: boolean }) {
740
- // Code block content
741
- if (inCodeBlock) {
742
- return <Text color="#a5d6ff">{line || ' '}</Text>;
743
- }
744
-
745
- // Code block delimiter
746
- if (line.startsWith('```')) {
747
- return <Text color={colors.textDim}>{line}</Text>;
748
- }
749
-
750
- // Headers
751
- if (line.startsWith('# ')) {
752
- return <Text color={colors.text} bold>{line.slice(2)}</Text>;
753
- }
754
- if (line.startsWith('## ')) {
755
- return <Text color={colors.text} bold>{line.slice(3)}</Text>;
756
- }
757
- if (line.startsWith('### ')) {
758
- return <Text color="#c9d1d9" bold>{line.slice(4)}</Text>;
759
- }
760
-
761
- // YAML frontmatter delimiter
762
- if (line === '---') {
763
- return <Text color={colors.textDim}>{line}</Text>;
764
- }
765
-
766
- // List items
767
- if (line.match(/^[\s]*[-*]\s/)) {
768
- return <Text color={colors.textMuted}>{line}</Text>;
769
- }
770
-
771
- // Blockquotes
772
- if (line.startsWith('>')) {
773
- return <Text color="#8b949e" italic>{line}</Text>;
774
- }
775
-
776
- // Table rows
777
- if (line.includes('|')) {
778
- return <Text color={colors.textMuted}>{line}</Text>;
779
- }
780
-
781
- // Default
782
- return <Text color={colors.textMuted}>{line || ' '}</Text>;
783
- }
784
-
785
- function ReadmeViewer({
786
- title,
787
- content,
788
- onClose,
789
- }: {
790
- title: string;
791
- content: string;
792
- onClose: () => void;
793
- }) {
794
- const [scrollOffset, setScrollOffset] = useState(0);
795
- const lines = useMemo(() => content.split('\n'), [content]);
796
- const maxVisible = 20;
797
-
798
- // Pre-compute code block state for each line
799
- const lineStates = useMemo(() => {
800
- const states: boolean[] = [];
801
- let inCode = false;
802
- for (const line of lines) {
803
- if (line.startsWith('```')) {
804
- states.push(false); // Delimiter itself is not "in" code block for styling
805
- inCode = !inCode;
806
- } else {
807
- states.push(inCode);
808
- }
809
- }
810
- return states;
811
- }, [lines]);
812
-
813
- // Max offset: when at end, we have top indicator + (maxVisible-1) content lines
814
- // So max offset is lines.length - (maxVisible - 1) = lines.length - maxVisible + 1
815
- const maxOffset = Math.max(0, lines.length - maxVisible + 1);
816
-
817
- useInput((input, key) => {
818
- if (key.escape) {
819
- onClose();
820
- return;
821
- }
822
- if (key.upArrow) {
823
- setScrollOffset((prev) => Math.max(0, prev - 1));
824
- }
825
- if (key.downArrow) {
826
- setScrollOffset((prev) => Math.min(maxOffset, prev + 1));
827
- }
828
- if (key.pageDown || input === ' ') {
829
- setScrollOffset((prev) => Math.min(maxOffset, prev + maxVisible));
830
- }
831
- if (key.pageUp) {
832
- setScrollOffset((prev) => Math.max(0, prev - maxVisible));
833
- }
834
- });
835
-
836
- // Adjust visible lines based on whether indicators are shown
837
- const showTopIndicator = scrollOffset > 0;
838
- // Reserve space for bottom indicator if not at end
839
- const contentLines = maxVisible - (showTopIndicator ? 1 : 0);
840
- const endIndex = Math.min(scrollOffset + contentLines, lines.length);
841
- const showBottomIndicator = endIndex < lines.length;
842
- const actualContentLines = contentLines - (showBottomIndicator ? 1 : 0);
843
- const visibleLines = lines.slice(scrollOffset, scrollOffset + actualContentLines);
844
-
845
- return (
846
- <Box flexDirection="column" padding={1}>
847
- <Box marginBottom={1}>
848
- <Text color={colors.text} bold>{title}</Text>
849
- <Text color={colors.textDim}> · {lines.length} lines</Text>
850
- </Box>
851
-
852
- <Box
853
- flexDirection="column"
854
- borderStyle="single"
855
- borderColor={colors.border}
856
- paddingX={1}
857
- >
858
- {showTopIndicator && (
859
- <Text color={colors.textDim}>↑ {scrollOffset} more lines</Text>
860
- )}
861
- {visibleLines.map((line, i) => (
862
- <MarkdownLine key={scrollOffset + i} line={line} inCodeBlock={lineStates[scrollOffset + i]} />
863
- ))}
864
- {showBottomIndicator && (
865
- <Text color={colors.textDim}>↓ {lines.length - scrollOffset - actualContentLines} more lines</Text>
866
- )}
867
- </Box>
868
-
869
- <Box marginTop={1}>
870
- <Text color={colors.textDim}>↑↓ scroll · space/pgdn page · esc back</Text>
871
- </Box>
872
- </Box>
873
- );
874
- }
875
-
876
- interface ExplorerItem {
877
- type: ComponentType;
878
- name: string;
879
- path: string;
880
- }
881
-
882
- interface ToolExplorerProps {
883
- tool: ToolManifest;
884
- onViewSource: (title: string, content: string) => void;
885
- onClose: () => void;
886
- }
887
-
888
- function ToolExplorer({ tool, onViewSource, onClose }: ToolExplorerProps) {
889
- const [selectedIndex, setSelectedIndex] = useState(0);
890
-
891
- // Build list of all explorable items
892
- const items: ExplorerItem[] = useMemo(() => {
893
- const result: ExplorerItem[] = [];
894
- const toolDir = getBundledToolsDir();
895
-
896
- // Add skills
897
- for (const skill of tool.includes.skills) {
898
- result.push({
899
- type: 'skill',
900
- name: skill.name,
901
- path: join(toolDir, tool.name, 'skills', skill.name, 'SKILL.md'),
902
- });
903
- }
904
-
905
- // Add commands
906
- for (const cmd of tool.includes.commands) {
907
- result.push({
908
- type: 'command',
909
- name: `/${cmd}`,
910
- path: join(toolDir, tool.name, 'commands', `${cmd}.md`),
911
- });
912
- }
913
-
914
- for (const agent of tool.includes.agents) {
915
- result.push({
916
- type: 'agent',
917
- name: agent,
918
- path: join(toolDir, tool.name, 'agents', `${agent}.md`),
919
- });
920
- }
921
-
922
- return result;
923
- }, [tool]);
924
-
925
- useInput((input, key) => {
926
- if (key.escape) {
927
- onClose();
928
- return;
929
- }
930
-
931
- if (key.leftArrow || key.upArrow) {
932
- setSelectedIndex((prev) => Math.max(0, prev - 1));
933
- }
934
- if (key.rightArrow || key.downArrow) {
935
- setSelectedIndex((prev) => Math.min(items.length - 1, prev + 1));
936
- }
937
-
938
- if (key.return && items.length > 0) {
939
- const item = items[selectedIndex];
940
- if (existsSync(item.path)) {
941
- const content = readFileSync(item.path, 'utf-8');
942
- onViewSource(`${tool.name} / ${item.name}`, content);
943
- } else {
944
- // Try YAML fallback for skills/agents
945
- const yamlPath = item.path.replace('.md', '.yaml');
946
- if (existsSync(yamlPath)) {
947
- const content = readFileSync(yamlPath, 'utf-8');
948
- onViewSource(`${tool.name} / ${item.name}`, content);
949
- }
950
- }
951
- }
952
- });
953
-
954
- // Group items by type for display
955
- const skillItems = items.filter(i => i.type === 'skill');
956
- const commandItems = items.filter(i => i.type === 'command');
957
- const agentItems = items.filter(i => i.type === 'agent');
958
-
959
- const getItemIndex = (item: ExplorerItem) => items.indexOf(item);
960
-
961
- return (
962
- <Box flexDirection="column" padding={1}>
963
- <Box marginBottom={1}>
964
- <Text>
965
- <Text color={colors.textDim}>[</Text>
966
- <Text color={colors.primary}>●</Text>
967
- <Text color={colors.textDim}> </Text>
968
- <Text color={colors.primary}>●</Text>
969
- <Text color={colors.textDim}>] </Text>
970
- <Text color={colors.text} bold>{tool.name}</Text>
971
- </Text>
972
- </Box>
973
-
974
- <Box marginBottom={1}>
975
- <Text color={colors.textMuted}>{tool.description}</Text>
976
- </Box>
977
-
978
- {/* Skills section */}
979
- {skillItems.length > 0 && (
980
- <Box flexDirection="column" marginBottom={1}>
981
- <Text color={colors.skill} bold>Skills</Text>
982
- <Box flexDirection="row" flexWrap="wrap" marginTop={1}>
983
- {skillItems.map((item) => {
984
- const idx = getItemIndex(item);
985
- const isSelected = selectedIndex === idx;
986
- return (
987
- <Box key={item.name} marginRight={1} marginBottom={1}>
988
- <Text
989
- backgroundColor={isSelected ? colors.skill : colors.bgSelected}
990
- color={isSelected ? '#000000' : colors.skill}
991
- bold={isSelected}
992
- >
993
- {` ${item.name} `}
994
- </Text>
995
- </Box>
996
- );
997
- })}
998
- </Box>
999
- </Box>
1000
- )}
1001
-
1002
- {/* Commands section */}
1003
- {commandItems.length > 0 && (
1004
- <Box flexDirection="column" marginBottom={1}>
1005
- <Text color={colors.command} bold>Commands</Text>
1006
- <Box flexDirection="row" flexWrap="wrap" marginTop={1}>
1007
- {commandItems.map((item) => {
1008
- const idx = getItemIndex(item);
1009
- const isSelected = selectedIndex === idx;
1010
- return (
1011
- <Box key={item.name} marginRight={1} marginBottom={1}>
1012
- <Text
1013
- backgroundColor={isSelected ? colors.command : colors.bgSelected}
1014
- color={isSelected ? '#000000' : colors.command}
1015
- bold={isSelected}
1016
- >
1017
- {` ${item.name} `}
1018
- </Text>
1019
- </Box>
1020
- );
1021
- })}
1022
- </Box>
1023
- </Box>
1024
- )}
1025
-
1026
- {/* Agents section */}
1027
- {agentItems.length > 0 && (
1028
- <Box flexDirection="column" marginBottom={1}>
1029
- <Text color={colors.agent} bold>Agents</Text>
1030
- <Box flexDirection="row" flexWrap="wrap" marginTop={1}>
1031
- {agentItems.map((item) => {
1032
- const idx = getItemIndex(item);
1033
- const isSelected = selectedIndex === idx;
1034
- return (
1035
- <Box key={item.name} marginRight={1} marginBottom={1}>
1036
- <Text
1037
- backgroundColor={isSelected ? colors.agent : colors.bgSelected}
1038
- color={isSelected ? '#000000' : colors.agent}
1039
- bold={isSelected}
1040
- >
1041
- {` ${item.name} `}
1042
- </Text>
1043
- </Box>
1044
- );
1045
- })}
1046
- </Box>
1047
- </Box>
1048
- )}
1049
-
1050
- <Box marginTop={1}>
1051
- <Text color={colors.textDim}>←→ navigate · enter view source · esc back</Text>
1052
- </Box>
1053
- </Box>
1054
- );
1055
- }
1056
-
1057
- function SettingsDetails({
1058
- onEditSettings,
1059
- isFocused,
1060
- onToggleAutoUpdate,
1061
- selectedSetting,
1062
- }: {
1063
- onEditSettings: () => void;
1064
- isFocused: boolean;
1065
- onToggleAutoUpdate: () => void;
1066
- selectedSetting: number;
1067
- }) {
1068
- const config = loadConfig();
1069
- const autoUpdateConfig = getAutoUpdateConfig();
1070
-
1071
- return (
1072
- <Box flexDirection="column" paddingLeft={2} flexGrow={1}>
1073
- <Text color={colors.text} bold>Settings</Text>
1074
-
1075
- <Box flexDirection="column" marginTop={1}>
1076
- <Text>
1077
- <Text color={colors.textDim}>Platform: </Text>
1078
- <Text color={colors.text}>
1079
- {config.platform === Platform.ClaudeCode ? 'Claude Code' : 'OpenCode'}
1080
- </Text>
1081
- </Text>
1082
- <Text>
1083
- <Text color={colors.textDim}>Your @mention: </Text>
1084
- <Text color={colors.text}>{config.user_mention}</Text>
1085
- </Text>
1086
- </Box>
1087
-
1088
- <Box flexDirection="column" marginTop={2}>
1089
- <Text color={colors.text} bold>Auto-Update</Text>
1090
- <Box marginTop={1}>
1091
- <Text>
1092
- <Text color={colors.textDim}>{isFocused && selectedSetting === 0 ? '> ' : ' '}</Text>
1093
- <Text color={isFocused && selectedSetting === 0 ? colors.text : colors.textMuted}>
1094
- [{autoUpdateConfig.tools ? 'x' : ' '}] Auto-update tools
1095
- </Text>
1096
- </Text>
1097
- </Box>
1098
- <Text color={colors.textDim}> Update tools automatically when droid starts</Text>
1099
- <Box marginTop={1}>
1100
- <Text>
1101
- <Text color={colors.textDim}>{isFocused && selectedSetting === 1 ? '> ' : ' '}</Text>
1102
- <Text color={isFocused && selectedSetting === 1 ? colors.text : colors.textMuted}>
1103
- [{autoUpdateConfig.app ? 'x' : ' '}] Auto-update app
1104
- </Text>
1105
- </Text>
1106
- </Box>
1107
- <Text color={colors.textDim}> Update droid automatically when a new version is available</Text>
1108
- </Box>
1109
-
1110
- <Box marginTop={2}>
1111
- <Text color={colors.textDim}>Config: ~/.droid/config.yaml</Text>
1112
- </Box>
1113
-
1114
- {isFocused && (
1115
- <Box marginTop={2}>
1116
- <Text
1117
- backgroundColor={selectedSetting === 2 ? colors.primary : colors.bgSelected}
1118
- color={selectedSetting === 2 ? '#ffffff' : colors.textMuted}
1119
- bold={selectedSetting === 2}
1120
- >
1121
- {' '}Edit Profile{' '}
1122
- </Text>
1123
- </Box>
1124
- )}
1125
-
1126
- {isFocused && (
1127
- <Box marginTop={1}>
1128
- <Text color={colors.textDim}>↑↓ select · enter toggle/edit · esc back</Text>
1129
- </Box>
1130
- )}
1131
-
1132
- {!isFocused && (
1133
- <Box marginTop={2}>
1134
- <Text color={colors.textDim}>press enter to configure</Text>
1135
- </Box>
1136
- )}
1137
- </Box>
1138
- );
1139
- }
1140
-
1141
- interface SkillConfigScreenProps {
1142
- skill: SkillManifest;
1143
- onComplete: () => void;
1144
- onCancel: () => void;
1145
- }
1146
-
1147
- const MAX_VISIBLE_CONFIG_ITEMS = 4; // Each config item takes ~3 lines
1148
-
1149
- function SkillConfigScreen({ skill, onComplete, onCancel }: SkillConfigScreenProps) {
1150
- const configSchema = skill.config_schema || {};
1151
- const configKeys = Object.keys(configSchema);
1152
-
1153
- const initialOverrides = useMemo(() => loadSkillOverrides(skill.name), [skill.name]);
1154
-
1155
- // Initialize values from saved overrides or defaults
1156
- const [values, setValues] = useState<SkillOverrides>(() => {
1157
- const initial: SkillOverrides = {};
1158
- for (const key of configKeys) {
1159
- const option = configSchema[key];
1160
- initial[key] = initialOverrides[key] ?? option.default ?? (option.type === ConfigOptionType.Boolean ? false : '');
1161
- }
1162
- return initial;
1163
- });
1164
-
1165
- const [selectedIndex, setSelectedIndex] = useState(0);
1166
- const [scrollOffset, setScrollOffset] = useState(0);
1167
- const [editingField, setEditingField] = useState<string | null>(null);
1168
- const [editValue, setEditValue] = useState('');
1169
- const [editingSelect, setEditingSelect] = useState<string | null>(null);
1170
- const [selectOptionIndex, setSelectOptionIndex] = useState(0);
1171
-
1172
- // Total items = config keys + Save button
1173
- const totalItems = configKeys.length + 1;
1174
-
1175
- const handleSave = () => {
1176
- saveSkillOverrides(skill.name, values);
1177
- onComplete();
1178
- };
1179
-
1180
- const handleSubmitEdit = () => {
1181
- if (editingField) {
1182
- setValues((prev) => ({ ...prev, [editingField]: editValue }));
1183
- setEditingField(null);
1184
- setEditValue('');
1185
- }
1186
- };
1187
-
1188
- // Handle text input for string fields
1189
- useInput((input, key) => {
1190
- if (key.escape) {
1191
- setEditingField(null);
1192
- setEditValue('');
1193
- }
1194
- }, { isActive: editingField !== null });
1195
-
1196
- // Handle select field editing
1197
- useInput((input, key) => {
1198
- if (!editingSelect) return;
1199
- const option = configSchema[editingSelect];
1200
- if (!option?.options) return;
1201
-
1202
- if (key.escape) {
1203
- setEditingSelect(null);
1204
- return;
1205
- }
1206
- if (key.leftArrow || key.upArrow) {
1207
- setSelectOptionIndex((prev) => Math.max(0, prev - 1));
1208
- }
1209
- if (key.rightArrow || key.downArrow) {
1210
- setSelectOptionIndex((prev) => Math.min(option.options!.length - 1, prev + 1));
1211
- }
1212
- if (key.return) {
1213
- setValues((prev) => ({ ...prev, [editingSelect]: option.options![selectOptionIndex] }));
1214
- setEditingSelect(null);
1215
- }
1216
- }, { isActive: editingSelect !== null });
1217
-
1218
- // Handle navigation and actions when not editing
1219
- useInput((input, key) => {
1220
- if (key.escape) {
1221
- onCancel();
1222
- return;
1223
- }
1224
-
1225
- if (key.upArrow) {
1226
- setSelectedIndex((prev) => {
1227
- const newIndex = Math.max(0, prev - 1);
1228
- // Scroll up if needed
1229
- if (newIndex < scrollOffset) {
1230
- setScrollOffset(newIndex);
1231
- }
1232
- return newIndex;
1233
- });
1234
- }
1235
- if (key.downArrow) {
1236
- // +1 for the Save button at the end
1237
- setSelectedIndex((prev) => {
1238
- const newIndex = Math.min(totalItems - 1, prev + 1);
1239
- // Scroll down if needed
1240
- if (newIndex >= scrollOffset + MAX_VISIBLE_CONFIG_ITEMS) {
1241
- setScrollOffset(newIndex - MAX_VISIBLE_CONFIG_ITEMS + 1);
1242
- }
1243
- return newIndex;
1244
- });
1245
- }
1246
-
1247
- if (key.return) {
1248
- // Save button is at index === configKeys.length
1249
- if (selectedIndex === configKeys.length) {
1250
- handleSave();
1251
- return;
1252
- }
1253
-
1254
- const key = configKeys[selectedIndex];
1255
- const option = configSchema[key];
1256
-
1257
- if (option.type === ConfigOptionType.Boolean) {
1258
- // Toggle boolean
1259
- setValues((prev) => ({ ...prev, [key]: !prev[key] }));
1260
- } else if (option.type === ConfigOptionType.Select && option.options) {
1261
- // Enter select edit mode
1262
- const currentValue = String(values[key] || option.options[0]);
1263
- const currentIndex = option.options.indexOf(currentValue);
1264
- setSelectOptionIndex(currentIndex >= 0 ? currentIndex : 0);
1265
- setEditingSelect(key);
1266
- } else if (option.type === ConfigOptionType.String) {
1267
- // Enter edit mode for string
1268
- setEditingField(key);
1269
- setEditValue(String(values[key] || ''));
1270
- }
1271
- }
1272
- }, { isActive: editingField === null && editingSelect === null });
1273
-
1274
- if (configKeys.length === 0) {
1275
- return (
1276
- <Box flexDirection="column" padding={1}>
1277
- <Box marginBottom={1}>
1278
- <Text>
1279
- <Text color={colors.textDim}>[</Text>
1280
- <Text color={colors.primary}>●</Text>
1281
- <Text color={colors.textDim}> </Text>
1282
- <Text color={colors.primary}>●</Text>
1283
- <Text color={colors.textDim}>] </Text>
1284
- <Text color={colors.text} bold>configure {skill.name}</Text>
1285
- </Text>
1286
- </Box>
1287
- <Text color={colors.textMuted}>This skill has no configuration options.</Text>
1288
- <Box marginTop={1}>
1289
- <Text color={colors.textDim}>esc to go back</Text>
1290
- </Box>
1291
- </Box>
1292
- );
1293
- }
1294
-
1295
- // Calculate visible range
1296
- const visibleEndIndex = Math.min(scrollOffset + MAX_VISIBLE_CONFIG_ITEMS, totalItems);
1297
- const visibleConfigKeys = configKeys.slice(scrollOffset, Math.min(scrollOffset + MAX_VISIBLE_CONFIG_ITEMS, configKeys.length));
1298
- const showSaveButton = visibleEndIndex > configKeys.length || scrollOffset + MAX_VISIBLE_CONFIG_ITEMS > configKeys.length;
1299
- const showTopIndicator = scrollOffset > 0;
1300
- const showBottomIndicator = scrollOffset + MAX_VISIBLE_CONFIG_ITEMS < totalItems;
1301
-
1302
- return (
1303
- <Box flexDirection="column" padding={1}>
1304
- <Box marginBottom={1}>
1305
- <Text>
1306
- <Text color={colors.textDim}>[</Text>
1307
- <Text color={colors.primary}>●</Text>
1308
- <Text color={colors.textDim}> </Text>
1309
- <Text color={colors.primary}>●</Text>
1310
- <Text color={colors.textDim}>] </Text>
1311
- <Text color={colors.text} bold>configure {skill.name}</Text>
1312
- </Text>
1313
- </Box>
1314
-
1315
- <Box flexDirection="column">
1316
- {/* Scroll up indicator */}
1317
- {showTopIndicator && (
1318
- <Box marginBottom={1}>
1319
- <Text color={colors.textDim}> ↑ {scrollOffset} more</Text>
1320
- </Box>
1321
- )}
1322
-
1323
- {visibleConfigKeys.map((key) => {
1324
- const actualIndex = configKeys.indexOf(key);
1325
- const option = configSchema[key];
1326
- const isSelected = selectedIndex === actualIndex;
1327
- const isEditing = editingField === key;
1328
-
1329
- return (
1330
- <Box key={key} flexDirection="column" marginBottom={1}>
1331
- <Text>
1332
- <Text color={colors.textDim}>{isSelected ? '>' : ' '} </Text>
1333
- <Text color={isSelected ? colors.text : colors.textMuted}>{key}</Text>
1334
- </Text>
1335
- <Text color={colors.textDim}> {option.description}</Text>
1336
- <Box>
1337
- <Text color={colors.textDim}> </Text>
1338
- {option.type === ConfigOptionType.Boolean ? (
1339
- <Text color={colors.text}>
1340
- [{values[key] ? 'x' : ' '}] {values[key] ? 'enabled' : 'disabled'}
1341
- </Text>
1342
- ) : option.type === ConfigOptionType.Select && option.options ? (
1343
- <Text color={colors.text}>
1344
- {option.options.map((opt, i) => {
1345
- const isCurrentValue = String(values[key]) === opt;
1346
- const isEditingThis = editingSelect === key;
1347
- const isHighlighted = isEditingThis && selectOptionIndex === i;
1348
- return (
1349
- <Text key={opt}>
1350
- {i > 0 && <Text color={colors.textDim}> · </Text>}
1351
- <Text
1352
- color={isHighlighted ? '#ffffff' : isCurrentValue ? colors.primary : colors.textMuted}
1353
- backgroundColor={isHighlighted ? colors.primary : undefined}
1354
- >
1355
- {isCurrentValue && !isEditingThis ? `[${opt}]` : isHighlighted ? ` ${opt} ` : opt}
1356
- </Text>
1357
- </Text>
1358
- );
1359
- })}
1360
- </Text>
1361
- ) : isEditing ? (
1362
- <Box>
1363
- <Text color={colors.textDim}>{'> '}</Text>
1364
- <TextInput
1365
- value={editValue}
1366
- onChange={setEditValue}
1367
- onSubmit={handleSubmitEdit}
1368
- />
1369
- </Box>
1370
- ) : (
1371
- <Text color={colors.text}>{String(values[key]) || '(not set)'}</Text>
1372
- )}
1373
- </Box>
1374
- </Box>
1375
- );
1376
- })}
1377
-
1378
- {/* Save button - show if in visible range */}
1379
- {showSaveButton && (
1380
- <Box marginTop={1}>
1381
- <Text>
1382
- <Text color={colors.textDim}>{selectedIndex === configKeys.length ? '>' : ' '} </Text>
1383
- <Text
1384
- backgroundColor={selectedIndex === configKeys.length ? colors.primary : undefined}
1385
- color={selectedIndex === configKeys.length ? '#ffffff' : colors.textMuted}
1386
- bold={selectedIndex === configKeys.length}
1387
- >
1388
- {' '}Save{' '}
1389
- </Text>
1390
- </Text>
1391
- </Box>
1392
- )}
1393
-
1394
- {/* Scroll down indicator */}
1395
- {showBottomIndicator && (
1396
- <Box marginTop={1}>
1397
- <Text color={colors.textDim}> ↓ {totalItems - scrollOffset - MAX_VISIBLE_CONFIG_ITEMS} more</Text>
1398
- </Box>
1399
- )}
1400
- </Box>
1401
-
1402
- <Box marginTop={1}>
1403
- <Text color={colors.textDim}>
1404
- {editingField
1405
- ? 'enter save · esc cancel'
1406
- : editingSelect
1407
- ? '←→ choose · enter select · esc cancel'
1408
- : '↑↓ select · enter toggle/edit · esc back'}
1409
- </Text>
1410
- </Box>
1411
- </Box>
1412
- );
1413
- }
28
+ // Module-level variable to store exit message (printed after leaving alternate screen)
29
+ let exitMessage: string | null = null;
1414
30
 
1415
31
  function App() {
1416
32
  const { exit } = useApp();
@@ -1428,97 +44,23 @@ function App() {
1428
44
  const [message, setMessage] = useState<{ text: string; type: 'success' | 'error' } | null>(null);
1429
45
  const [isEditingSettings, setIsEditingSettings] = useState(false);
1430
46
  const [readmeContent, setReadmeContent] = useState<{ title: string; content: string } | null>(null);
1431
- const [selectedSetting, setSelectedSetting] = useState(0);
1432
- const [isUpdating, setIsUpdating] = useState(false);
1433
- const [isUpdatingTools, setIsUpdatingTools] = useState(false);
1434
47
  const [previousView, setPreviousView] = useState<View>('detail'); // Track where to return from readme
1435
- const [toolUpdates, setToolUpdates] = useState<ToolUpdateInfo[]>([]);
1436
- const [autoUpdatedTools, setAutoUpdatedTools] = useState<string[]>([]);
1437
-
1438
- // Check for updates once on mount
1439
- const updateInfo = useMemo(() => getUpdateInfo(), []);
1440
- const autoUpdateConfig = useMemo(() => getAutoUpdateConfig(), []);
1441
48
 
1442
- const handleUpdate = () => {
1443
- setIsUpdating(true);
1444
- // Run update in next tick to allow UI to show "Updating..."
1445
- setTimeout(() => {
1446
- const result = runUpdate();
1447
- if (result.success) {
1448
- // Store message to print after leaving alternate screen (with ANSI colors)
1449
- const blue = '\x1b[38;2;99;102;241m'; // #6366f1
1450
- const dim = '\x1b[38;2;106;106;106m';
1451
- const reset = '\x1b[0m';
1452
- exitMessage = `
1453
- ${dim}────────────────────────────────────────────────────────${reset}
1454
-
1455
- ${dim}╔═════╗${reset}
1456
- ${dim}║${reset} ${blue}●${reset} ${blue}●${reset} ${dim}║${reset} ${blue}"It's quite possible this system${reset}
1457
- ${dim}╚═╦═╦═╝${reset} ${blue}is now fully operational."${reset}
1458
-
1459
- Run ${blue}droid${reset} to start the new version.
1460
-
1461
- ${dim}────────────────────────────────────────────────────────${reset}
1462
- `;
1463
- exit();
49
+ const { updateInfo, isUpdating, handleUpdate, handleAlwaysUpdate } = useAppUpdate({
50
+ onUpdateSuccess: (msg) => {
51
+ exitMessage = msg;
52
+ },
53
+ onUpdateFailure: (error) => {
54
+ setMessage({ text: error, type: 'error' });
55
+ if (!configExists()) {
56
+ setView('setup');
1464
57
  } else {
1465
- setIsUpdating(false);
1466
- setMessage({ text: result.message, type: 'error' });
1467
- // Continue to menu on failure
1468
- if (!configExists()) {
1469
- setView('setup');
1470
- } else {
1471
- setView('menu');
1472
- }
1473
- }
1474
- }, 100);
1475
- };
1476
-
1477
- const handleAlwaysUpdate = () => {
1478
- // Enable auto-update for app in config
1479
- setAutoUpdateConfig({ app: true });
1480
- // Then run the update
1481
- handleUpdate();
1482
- };
1483
-
1484
- // Ensure system tools (marked with system: true in TOOL.yaml) are always installed and current
1485
- const ensureSystemTools = () => {
1486
- // Find all tools marked as system tools
1487
- const systemTools = tools.filter(t => (t as ToolManifest & { system?: boolean }).system === true);
1488
-
1489
- for (const systemTool of systemTools) {
1490
- const installed = isToolInstalled(systemTool.name);
1491
- const updateStatus = getToolUpdateStatus(systemTool.name);
1492
-
1493
- // Install if not installed, or update if outdated (regardless of auto-update settings)
1494
- if (!installed || updateStatus.hasUpdate) {
1495
- const primarySkill = systemTool.includes.skills.find(s => s.required)?.name || systemTool.name;
1496
- installSkill(primarySkill);
58
+ setView('menu');
1497
59
  }
1498
- }
1499
- };
1500
-
1501
- // Check for tool updates and proceed to next view
1502
- const checkToolUpdatesAndProceed = () => {
1503
- // Always ensure system tools are current (bypasses auto-update settings)
1504
- ensureSystemTools();
1505
-
1506
- const updates = getToolsWithUpdates();
1507
- setToolUpdates(updates);
1508
-
1509
- // If auto_update.tools is true, auto-update silently
1510
- if (autoUpdateConfig.tools && updates.length > 0) {
1511
- handleUpdateAllTools(updates, true);
1512
- return;
1513
- }
1514
-
1515
- // If there are updates and auto-update is off, show prompt
1516
- if (updates.length > 0) {
1517
- setView('tool-updates');
1518
- return;
1519
- }
60
+ },
61
+ });
1520
62
 
1521
- // No updates, proceed to setup or menu
63
+ const proceedToNextView = () => {
1522
64
  if (!configExists()) {
1523
65
  setView('setup');
1524
66
  } else {
@@ -1526,38 +68,15 @@ ${dim}────────────────────────
1526
68
  }
1527
69
  };
1528
70
 
1529
- const handleUpdateAllTools = (updates: ToolUpdateInfo[] = toolUpdates, silent = false) => {
1530
- if (!silent) {
1531
- setIsUpdatingTools(true);
1532
- }
1533
-
1534
- setTimeout(() => {
1535
- let successCount = 0;
1536
- let failCount = 0;
1537
- const updatedNames: string[] = [];
1538
-
1539
- for (const tool of updates) {
1540
- // Find the tool to get its primary skill
1541
- const toolManifest = tools.find(t => t.name === tool.name);
1542
- if (toolManifest) {
1543
- const primarySkill = toolManifest.includes.skills.find(s => s.required)?.name || toolManifest.name;
1544
- const result = updateSkill(primarySkill);
1545
- if (result.success) {
1546
- successCount++;
1547
- updatedNames.push(tool.name);
1548
- } else {
1549
- failCount++;
1550
- }
1551
- }
1552
- }
1553
-
1554
- setIsUpdatingTools(false);
1555
-
1556
- // Track which tools were auto-updated for visual indicator
1557
- if (silent && updatedNames.length > 0) {
1558
- setAutoUpdatedTools(updatedNames);
1559
- }
1560
-
71
+ const {
72
+ toolUpdates,
73
+ isUpdatingTools,
74
+ autoUpdatedTools,
75
+ checkForUpdates,
76
+ updateAllTools,
77
+ enableAutoUpdateAndUpdate,
78
+ } = useToolUpdates({
79
+ onUpdateComplete: ({ successCount, failCount, silent }) => {
1561
80
  if (successCount > 0) {
1562
81
  setMessage({
1563
82
  text: silent
@@ -1566,52 +85,31 @@ ${dim}────────────────────────
1566
85
  type: failCount > 0 ? 'error' : 'success',
1567
86
  });
1568
87
  }
88
+ proceedToNextView();
89
+ },
90
+ });
1569
91
 
1570
- // Proceed to next view
1571
- if (!configExists()) {
1572
- setView('setup');
1573
- } else {
1574
- setView('menu');
1575
- }
1576
- }, 100);
1577
- };
92
+ // Check for tool updates and proceed to next view
93
+ const checkToolUpdatesAndProceed = () => {
94
+ const { updates, shouldAutoUpdate } = checkForUpdates();
1578
95
 
1579
- const handleAlwaysUpdateTools = () => {
1580
- // Enable auto-update in config
1581
- setAutoUpdateConfig({ tools: true });
1582
- // Then update all
1583
- handleUpdateAllTools();
1584
- };
96
+ if (shouldAutoUpdate) {
97
+ updateAllTools(updates, true);
98
+ return;
99
+ }
1585
100
 
1586
- const handleSkipToolUpdates = () => {
1587
- if (!configExists()) {
1588
- setView('setup');
1589
- } else {
1590
- setView('menu');
101
+ if (updates.length > 0) {
102
+ setView('tool-updates');
103
+ return;
1591
104
  }
1592
- };
1593
105
 
1594
- const handleToggleAutoUpdateTools = () => {
1595
- const current = getAutoUpdateConfig();
1596
- setAutoUpdateConfig({ tools: !current.tools });
1597
- // Force re-render by setting message (auto-clears on next input)
1598
- setMessage({
1599
- text: `✓ Auto-update tools ${!current.tools ? 'enabled' : 'disabled'}`,
1600
- type: 'success',
1601
- });
106
+ proceedToNextView();
1602
107
  };
1603
108
 
1604
- const handleToggleAutoUpdateApp = () => {
1605
- const current = getAutoUpdateConfig();
1606
- setAutoUpdateConfig({ app: !current.app });
1607
- setMessage({
1608
- text: `✓ Auto-update app ${!current.app ? 'enabled' : 'disabled'}`,
1609
- type: 'success',
1610
- });
109
+ const handleSkipToolUpdates = () => {
110
+ proceedToNextView();
1611
111
  };
1612
112
 
1613
- const MAX_VISIBLE_ITEMS = 6;
1614
-
1615
113
  const tools = getBundledTools();
1616
114
  // Keep skills for configure view (tools configure via their primary skill)
1617
115
  const skills = getBundledSkills();
@@ -1667,32 +165,18 @@ ${dim}────────────────────────
1667
165
  setView('detail');
1668
166
  } else if (activeTab === 'settings') {
1669
167
  setView('detail');
1670
- setSelectedSetting(0);
1671
168
  }
1672
169
  }
1673
170
  } else if (view === 'detail') {
1674
171
  if (key.escape || key.backspace) {
1675
172
  setView('menu');
1676
173
  setSelectedAction(0);
1677
- setSelectedSetting(0);
1678
174
  }
1679
175
  if (activeTab === 'settings') {
1680
- // Settings detail view navigation
1681
- if (key.upArrow) {
1682
- setSelectedSetting((prev) => Math.max(0, prev - 1));
1683
- }
1684
- if (key.downArrow) {
1685
- setSelectedSetting((prev) => Math.min(2, prev + 1)); // 0: auto-update tools, 1: auto-update app, 2: edit profile
1686
- }
176
+ // Settings detail view - just enter to edit
1687
177
  if (key.return) {
1688
- if (selectedSetting === 0) {
1689
- handleToggleAutoUpdateTools();
1690
- } else if (selectedSetting === 1) {
1691
- handleToggleAutoUpdateApp();
1692
- } else if (selectedSetting === 2) {
1693
- setIsEditingSettings(true);
1694
- setView('setup');
1695
- }
178
+ setIsEditingSettings(true);
179
+ setView('setup');
1696
180
  }
1697
181
  }
1698
182
  if (key.leftArrow && activeTab === 'tools') {
@@ -1806,8 +290,8 @@ ${dim}────────────────────────
1806
290
  return (
1807
291
  <ToolUpdatePrompt
1808
292
  toolUpdates={toolUpdates}
1809
- onUpdateAll={() => handleUpdateAllTools()}
1810
- onAlways={handleAlwaysUpdateTools}
293
+ onUpdateAll={() => updateAllTools()}
294
+ onAlways={enableAutoUpdateAndUpdate}
1811
295
  onSkip={handleSkipToolUpdates}
1812
296
  isUpdating={isUpdatingTools}
1813
297
  />
@@ -1966,13 +450,7 @@ ${dim}────────────────────────
1966
450
 
1967
451
  {activeTab === 'settings' && (
1968
452
  <SettingsDetails
1969
- onEditSettings={() => {
1970
- setIsEditingSettings(true);
1971
- setView('setup');
1972
- }}
1973
453
  isFocused={view === 'detail'}
1974
- onToggleAutoUpdate={handleToggleAutoUpdateTools}
1975
- selectedSetting={selectedSetting}
1976
454
  />
1977
455
  )}
1978
456
  </Box>
@@ -1984,7 +462,7 @@ export async function tuiCommand(): Promise<void> {
1984
462
  process.stdout.write('\x1b[?1049h');
1985
463
  process.stdout.write('\x1b[H'); // Move cursor to top-left
1986
464
 
1987
- const { unmount, waitUntilExit } = render(<App />);
465
+ const { waitUntilExit } = render(<App />);
1988
466
 
1989
467
  await waitUntilExit();
1990
468