@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
@@ -0,0 +1,278 @@
1
+ import { Box, Text, useInput } from 'ink';
2
+ import TextInput from 'ink-text-input';
3
+ import { useState, useMemo } from 'react';
4
+ import { loadSkillOverrides, saveSkillOverrides } from '../../../lib/config';
5
+ import { ConfigOptionType, type SkillManifest, type SkillOverrides } from '../../../lib/types';
6
+ import { colors, MAX_VISIBLE_CONFIG_ITEMS } from '../constants';
7
+
8
+ export interface SkillConfigScreenProps {
9
+ skill: SkillManifest;
10
+ onComplete: () => void;
11
+ onCancel: () => void;
12
+ }
13
+
14
+ export function SkillConfigScreen({ skill, onComplete, onCancel }: SkillConfigScreenProps) {
15
+ const configSchema = skill.config_schema || {};
16
+ const configKeys = Object.keys(configSchema);
17
+
18
+ const initialOverrides = useMemo(() => loadSkillOverrides(skill.name), [skill.name]);
19
+
20
+ // Initialize values from saved overrides or defaults
21
+ const [values, setValues] = useState<SkillOverrides>(() => {
22
+ const initial: SkillOverrides = {};
23
+ for (const key of configKeys) {
24
+ const option = configSchema[key];
25
+ initial[key] = initialOverrides[key] ?? option.default ?? (option.type === ConfigOptionType.Boolean ? false : '');
26
+ }
27
+ return initial;
28
+ });
29
+
30
+ const [selectedIndex, setSelectedIndex] = useState(0);
31
+ const [scrollOffset, setScrollOffset] = useState(0);
32
+ const [editingField, setEditingField] = useState<string | null>(null);
33
+ const [editValue, setEditValue] = useState('');
34
+ const [editingSelect, setEditingSelect] = useState<string | null>(null);
35
+ const [selectOptionIndex, setSelectOptionIndex] = useState(0);
36
+
37
+ // Total items = config keys + Save button
38
+ const totalItems = configKeys.length + 1;
39
+
40
+ const handleSave = () => {
41
+ saveSkillOverrides(skill.name, values);
42
+ onComplete();
43
+ };
44
+
45
+ const handleSubmitEdit = () => {
46
+ if (editingField) {
47
+ setValues((prev) => ({ ...prev, [editingField]: editValue }));
48
+ setEditingField(null);
49
+ setEditValue('');
50
+ }
51
+ };
52
+
53
+ // Handle text input for string fields
54
+ useInput((input, key) => {
55
+ if (key.escape) {
56
+ setEditingField(null);
57
+ setEditValue('');
58
+ }
59
+ }, { isActive: editingField !== null });
60
+
61
+ // Handle select field editing
62
+ useInput((input, key) => {
63
+ if (!editingSelect) return;
64
+ const option = configSchema[editingSelect];
65
+ if (!option?.options) return;
66
+
67
+ if (key.escape) {
68
+ setEditingSelect(null);
69
+ return;
70
+ }
71
+ if (key.leftArrow || key.upArrow) {
72
+ setSelectOptionIndex((prev) => Math.max(0, prev - 1));
73
+ }
74
+ if (key.rightArrow || key.downArrow) {
75
+ setSelectOptionIndex((prev) => Math.min(option.options!.length - 1, prev + 1));
76
+ }
77
+ if (key.return) {
78
+ setValues((prev) => ({ ...prev, [editingSelect]: option.options![selectOptionIndex] }));
79
+ setEditingSelect(null);
80
+ }
81
+ }, { isActive: editingSelect !== null });
82
+
83
+ // Handle navigation and actions when not editing
84
+ useInput((input, key) => {
85
+ if (key.escape) {
86
+ onCancel();
87
+ return;
88
+ }
89
+
90
+ if (key.upArrow) {
91
+ setSelectedIndex((prev) => {
92
+ const newIndex = Math.max(0, prev - 1);
93
+ // Scroll up if needed
94
+ if (newIndex < scrollOffset) {
95
+ setScrollOffset(newIndex);
96
+ }
97
+ return newIndex;
98
+ });
99
+ }
100
+ if (key.downArrow) {
101
+ // +1 for the Save button at the end
102
+ setSelectedIndex((prev) => {
103
+ const newIndex = Math.min(totalItems - 1, prev + 1);
104
+ // Scroll down if needed
105
+ if (newIndex >= scrollOffset + MAX_VISIBLE_CONFIG_ITEMS) {
106
+ setScrollOffset(newIndex - MAX_VISIBLE_CONFIG_ITEMS + 1);
107
+ }
108
+ return newIndex;
109
+ });
110
+ }
111
+
112
+ if (key.return) {
113
+ // Save button is at index === configKeys.length
114
+ if (selectedIndex === configKeys.length) {
115
+ handleSave();
116
+ return;
117
+ }
118
+
119
+ const key = configKeys[selectedIndex];
120
+ const option = configSchema[key];
121
+
122
+ if (option.type === ConfigOptionType.Boolean) {
123
+ // Toggle boolean
124
+ setValues((prev) => ({ ...prev, [key]: !prev[key] }));
125
+ } else if (option.type === ConfigOptionType.Select && option.options) {
126
+ // Enter select edit mode
127
+ const currentValue = String(values[key] || option.options[0]);
128
+ const currentIndex = option.options.indexOf(currentValue);
129
+ setSelectOptionIndex(currentIndex >= 0 ? currentIndex : 0);
130
+ setEditingSelect(key);
131
+ } else if (option.type === ConfigOptionType.String) {
132
+ // Enter edit mode for string
133
+ setEditingField(key);
134
+ setEditValue(String(values[key] || ''));
135
+ }
136
+ }
137
+ }, { isActive: editingField === null && editingSelect === null });
138
+
139
+ if (configKeys.length === 0) {
140
+ return (
141
+ <Box flexDirection="column" padding={1}>
142
+ <Box marginBottom={1}>
143
+ <Text>
144
+ <Text color={colors.textDim}>[</Text>
145
+ <Text color={colors.primary}>●</Text>
146
+ <Text color={colors.textDim}> </Text>
147
+ <Text color={colors.primary}>●</Text>
148
+ <Text color={colors.textDim}>] </Text>
149
+ <Text color={colors.text} bold>configure {skill.name}</Text>
150
+ </Text>
151
+ </Box>
152
+ <Text color={colors.textMuted}>This skill has no configuration options.</Text>
153
+ <Box marginTop={1}>
154
+ <Text color={colors.textDim}>esc to go back</Text>
155
+ </Box>
156
+ </Box>
157
+ );
158
+ }
159
+
160
+ // Calculate visible range
161
+ const visibleEndIndex = Math.min(scrollOffset + MAX_VISIBLE_CONFIG_ITEMS, totalItems);
162
+ const visibleConfigKeys = configKeys.slice(scrollOffset, Math.min(scrollOffset + MAX_VISIBLE_CONFIG_ITEMS, configKeys.length));
163
+ const showSaveButton = visibleEndIndex > configKeys.length || scrollOffset + MAX_VISIBLE_CONFIG_ITEMS > configKeys.length;
164
+ const showTopIndicator = scrollOffset > 0;
165
+ const showBottomIndicator = scrollOffset + MAX_VISIBLE_CONFIG_ITEMS < totalItems;
166
+
167
+ return (
168
+ <Box flexDirection="column" padding={1}>
169
+ <Box marginBottom={1}>
170
+ <Text>
171
+ <Text color={colors.textDim}>[</Text>
172
+ <Text color={colors.primary}>●</Text>
173
+ <Text color={colors.textDim}> </Text>
174
+ <Text color={colors.primary}>●</Text>
175
+ <Text color={colors.textDim}>] </Text>
176
+ <Text color={colors.text} bold>configure {skill.name}</Text>
177
+ </Text>
178
+ </Box>
179
+
180
+ <Box flexDirection="column">
181
+ {/* Scroll up indicator */}
182
+ {showTopIndicator && (
183
+ <Box marginBottom={1}>
184
+ <Text color={colors.textDim}> ↑ {scrollOffset} more</Text>
185
+ </Box>
186
+ )}
187
+
188
+ {visibleConfigKeys.map((configKey) => {
189
+ const actualIndex = configKeys.indexOf(configKey);
190
+ const option = configSchema[configKey];
191
+ const isSelected = selectedIndex === actualIndex;
192
+ const isEditing = editingField === configKey;
193
+
194
+ return (
195
+ <Box key={configKey} flexDirection="column" marginBottom={1}>
196
+ <Text>
197
+ <Text color={colors.textDim}>{isSelected ? '>' : ' '} </Text>
198
+ <Text color={isSelected ? colors.text : colors.textMuted}>{configKey}</Text>
199
+ </Text>
200
+ <Text color={colors.textDim}> {option.description}</Text>
201
+ <Box>
202
+ <Text color={colors.textDim}> </Text>
203
+ {option.type === ConfigOptionType.Boolean ? (
204
+ <Text color={colors.text}>
205
+ [{values[configKey] ? 'x' : ' '}] {values[configKey] ? 'enabled' : 'disabled'}
206
+ </Text>
207
+ ) : option.type === ConfigOptionType.Select && option.options ? (
208
+ <Text color={colors.text}>
209
+ {option.options.map((opt, i) => {
210
+ const isCurrentValue = String(values[configKey]) === opt;
211
+ const isEditingThis = editingSelect === configKey;
212
+ const isHighlighted = isEditingThis && selectOptionIndex === i;
213
+ return (
214
+ <Text key={opt}>
215
+ {i > 0 && <Text color={colors.textDim}> · </Text>}
216
+ <Text
217
+ color={isHighlighted ? '#ffffff' : isCurrentValue ? colors.primary : colors.textMuted}
218
+ backgroundColor={isHighlighted ? colors.primary : undefined}
219
+ >
220
+ {isCurrentValue && !isEditingThis ? `[${opt}]` : isHighlighted ? ` ${opt} ` : opt}
221
+ </Text>
222
+ </Text>
223
+ );
224
+ })}
225
+ </Text>
226
+ ) : isEditing ? (
227
+ <Box>
228
+ <Text color={colors.textDim}>{'> '}</Text>
229
+ <TextInput
230
+ value={editValue}
231
+ onChange={setEditValue}
232
+ onSubmit={handleSubmitEdit}
233
+ />
234
+ </Box>
235
+ ) : (
236
+ <Text color={colors.text}>{String(values[configKey]) || '(not set)'}</Text>
237
+ )}
238
+ </Box>
239
+ </Box>
240
+ );
241
+ })}
242
+
243
+ {/* Save button - show if in visible range */}
244
+ {showSaveButton && (
245
+ <Box marginTop={1}>
246
+ <Text>
247
+ <Text color={colors.textDim}>{selectedIndex === configKeys.length ? '>' : ' '} </Text>
248
+ <Text
249
+ backgroundColor={selectedIndex === configKeys.length ? colors.primary : undefined}
250
+ color={selectedIndex === configKeys.length ? '#ffffff' : colors.textMuted}
251
+ bold={selectedIndex === configKeys.length}
252
+ >
253
+ {' '}Save{' '}
254
+ </Text>
255
+ </Text>
256
+ </Box>
257
+ )}
258
+
259
+ {/* Scroll down indicator */}
260
+ {showBottomIndicator && (
261
+ <Box marginTop={1}>
262
+ <Text color={colors.textDim}> ↓ {totalItems - scrollOffset - MAX_VISIBLE_CONFIG_ITEMS} more</Text>
263
+ </Box>
264
+ )}
265
+ </Box>
266
+
267
+ <Box marginTop={1}>
268
+ <Text color={colors.textDim}>
269
+ {editingField
270
+ ? 'enter save · esc cancel'
271
+ : editingSelect
272
+ ? '←→ choose · enter select · esc cancel'
273
+ : '↑↓ select · enter toggle/edit · esc back'}
274
+ </Text>
275
+ </Box>
276
+ </Box>
277
+ );
278
+ }
@@ -0,0 +1,190 @@
1
+ import { Box, Text, useInput } from 'ink';
2
+ import { useState, useMemo } from 'react';
3
+ import { existsSync, readFileSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { getBundledToolsDir } from '../../../lib/tools';
6
+ import type { ToolManifest } from '../../../lib/types';
7
+ import { colors } from '../constants';
8
+ import type { ComponentType } from '../types';
9
+
10
+ interface ExplorerItem {
11
+ type: ComponentType;
12
+ name: string;
13
+ path: string;
14
+ }
15
+
16
+ export interface ToolExplorerProps {
17
+ tool: ToolManifest;
18
+ onViewSource: (title: string, content: string) => void;
19
+ onClose: () => void;
20
+ }
21
+
22
+ export function ToolExplorer({ tool, onViewSource, onClose }: ToolExplorerProps) {
23
+ const [selectedIndex, setSelectedIndex] = useState(0);
24
+
25
+ // Build list of all explorable items
26
+ const items: ExplorerItem[] = useMemo(() => {
27
+ const result: ExplorerItem[] = [];
28
+ const toolDir = getBundledToolsDir();
29
+
30
+ // Add skills
31
+ for (const skill of tool.includes.skills) {
32
+ result.push({
33
+ type: 'skill',
34
+ name: skill.name,
35
+ path: join(toolDir, tool.name, 'skills', skill.name, 'SKILL.md'),
36
+ });
37
+ }
38
+
39
+ // Add commands
40
+ for (const cmd of tool.includes.commands) {
41
+ result.push({
42
+ type: 'command',
43
+ name: `/${cmd}`,
44
+ path: join(toolDir, tool.name, 'commands', `${cmd}.md`),
45
+ });
46
+ }
47
+
48
+ // Add agents
49
+ for (const agent of tool.includes.agents) {
50
+ result.push({
51
+ type: 'agent',
52
+ name: agent,
53
+ path: join(toolDir, tool.name, 'agents', agent, 'AGENT.md'),
54
+ });
55
+ }
56
+
57
+ return result;
58
+ }, [tool]);
59
+
60
+ useInput((input, key) => {
61
+ if (key.escape) {
62
+ onClose();
63
+ return;
64
+ }
65
+
66
+ if (key.leftArrow || key.upArrow) {
67
+ setSelectedIndex((prev) => Math.max(0, prev - 1));
68
+ }
69
+ if (key.rightArrow || key.downArrow) {
70
+ setSelectedIndex((prev) => Math.min(items.length - 1, prev + 1));
71
+ }
72
+
73
+ if (key.return && items.length > 0) {
74
+ const item = items[selectedIndex];
75
+ if (existsSync(item.path)) {
76
+ const content = readFileSync(item.path, 'utf-8');
77
+ onViewSource(`${tool.name} / ${item.name}`, content);
78
+ } else {
79
+ // Try YAML fallback for skills/agents
80
+ const yamlPath = item.path.replace('.md', '.yaml');
81
+ if (existsSync(yamlPath)) {
82
+ const content = readFileSync(yamlPath, 'utf-8');
83
+ onViewSource(`${tool.name} / ${item.name}`, content);
84
+ }
85
+ }
86
+ }
87
+ });
88
+
89
+ // Group items by type for display
90
+ const skillItems = items.filter(i => i.type === 'skill');
91
+ const commandItems = items.filter(i => i.type === 'command');
92
+ const agentItems = items.filter(i => i.type === 'agent');
93
+
94
+ const getItemIndex = (item: ExplorerItem) => items.indexOf(item);
95
+
96
+ return (
97
+ <Box flexDirection="column" padding={1}>
98
+ <Box marginBottom={1}>
99
+ <Text>
100
+ <Text color={colors.textDim}>[</Text>
101
+ <Text color={colors.primary}>●</Text>
102
+ <Text color={colors.textDim}> </Text>
103
+ <Text color={colors.primary}>●</Text>
104
+ <Text color={colors.textDim}>] </Text>
105
+ <Text color={colors.text} bold>{tool.name}</Text>
106
+ </Text>
107
+ </Box>
108
+
109
+ <Box marginBottom={1}>
110
+ <Text color={colors.textMuted}>{tool.description}</Text>
111
+ </Box>
112
+
113
+ {/* Skills section */}
114
+ {skillItems.length > 0 && (
115
+ <Box flexDirection="column" marginBottom={1}>
116
+ <Text color={colors.skill} bold>Skills</Text>
117
+ <Box flexDirection="row" flexWrap="wrap" marginTop={1}>
118
+ {skillItems.map((item) => {
119
+ const idx = getItemIndex(item);
120
+ const isSelected = selectedIndex === idx;
121
+ return (
122
+ <Box key={item.name} marginRight={1} marginBottom={1}>
123
+ <Text
124
+ backgroundColor={isSelected ? colors.skill : colors.bgSelected}
125
+ color={isSelected ? '#000000' : colors.skill}
126
+ bold={isSelected}
127
+ >
128
+ {` ${item.name} `}
129
+ </Text>
130
+ </Box>
131
+ );
132
+ })}
133
+ </Box>
134
+ </Box>
135
+ )}
136
+
137
+ {/* Commands section */}
138
+ {commandItems.length > 0 && (
139
+ <Box flexDirection="column" marginBottom={1}>
140
+ <Text color={colors.command} bold>Commands</Text>
141
+ <Box flexDirection="row" flexWrap="wrap" marginTop={1}>
142
+ {commandItems.map((item) => {
143
+ const idx = getItemIndex(item);
144
+ const isSelected = selectedIndex === idx;
145
+ return (
146
+ <Box key={item.name} marginRight={1} marginBottom={1}>
147
+ <Text
148
+ backgroundColor={isSelected ? colors.command : colors.bgSelected}
149
+ color={isSelected ? '#000000' : colors.command}
150
+ bold={isSelected}
151
+ >
152
+ {` ${item.name} `}
153
+ </Text>
154
+ </Box>
155
+ );
156
+ })}
157
+ </Box>
158
+ </Box>
159
+ )}
160
+
161
+ {/* Agents section */}
162
+ {agentItems.length > 0 && (
163
+ <Box flexDirection="column" marginBottom={1}>
164
+ <Text color={colors.agent} bold>Agents</Text>
165
+ <Box flexDirection="row" flexWrap="wrap" marginTop={1}>
166
+ {agentItems.map((item) => {
167
+ const idx = getItemIndex(item);
168
+ const isSelected = selectedIndex === idx;
169
+ return (
170
+ <Box key={item.name} marginRight={1} marginBottom={1}>
171
+ <Text
172
+ backgroundColor={isSelected ? colors.agent : colors.bgSelected}
173
+ color={isSelected ? '#000000' : colors.agent}
174
+ bold={isSelected}
175
+ >
176
+ {` ${item.name} `}
177
+ </Text>
178
+ </Box>
179
+ );
180
+ })}
181
+ </Box>
182
+ </Box>
183
+ )}
184
+
185
+ <Box marginTop={1}>
186
+ <Text color={colors.textDim}>←→ navigate · enter view source · esc back</Text>
187
+ </Box>
188
+ </Box>
189
+ );
190
+ }
@@ -0,0 +1,109 @@
1
+ import { Box, Text, useInput } from 'ink';
2
+ import { useState } from 'react';
3
+ import { colors } from '../constants';
4
+ import type { ToolUpdateInfo } from '../../../lib/tools';
5
+
6
+ export interface ToolUpdatePromptProps {
7
+ toolUpdates: ToolUpdateInfo[];
8
+ onUpdateAll: () => void;
9
+ onAlways: () => void;
10
+ onSkip: () => void;
11
+ isUpdating: boolean;
12
+ }
13
+
14
+ export function ToolUpdatePrompt({ toolUpdates, onUpdateAll, onAlways, onSkip, isUpdating }: ToolUpdatePromptProps) {
15
+ const [selectedButton, setSelectedButton] = useState(0);
16
+
17
+ useInput((input, key) => {
18
+ if (isUpdating) return;
19
+
20
+ if (key.leftArrow) {
21
+ setSelectedButton((prev) => Math.max(0, prev - 1));
22
+ }
23
+ if (key.rightArrow) {
24
+ setSelectedButton((prev) => Math.min(2, prev + 1));
25
+ }
26
+ if (key.return) {
27
+ if (selectedButton === 0) {
28
+ onUpdateAll();
29
+ } else if (selectedButton === 1) {
30
+ onAlways();
31
+ } else {
32
+ onSkip();
33
+ }
34
+ }
35
+ if (input === 'q') {
36
+ onSkip();
37
+ }
38
+ });
39
+
40
+ const buttons = [
41
+ { label: 'Update All', action: onUpdateAll },
42
+ { label: 'Always', action: onAlways },
43
+ { label: 'Skip', action: onSkip },
44
+ ];
45
+
46
+ return (
47
+ <Box flexDirection="column" alignItems="center" justifyContent="center" height={18}>
48
+ <Box
49
+ flexDirection="column"
50
+ alignItems="center"
51
+ borderStyle="single"
52
+ borderColor={colors.primary}
53
+ paddingX={4}
54
+ paddingY={1}
55
+ >
56
+ <Box flexDirection="column">
57
+ <Text>
58
+ <Text color={colors.textDim}>╔═════╗ </Text>
59
+ <Text color={colors.text}>Tool Updates</Text>
60
+ </Text>
61
+ <Text>
62
+ <Text color={colors.textDim}>║ </Text>
63
+ <Text color={colors.primary}>●</Text>
64
+ <Text color={colors.textDim}> </Text>
65
+ <Text color={colors.primary}>●</Text>
66
+ <Text color={colors.textDim}> ║ </Text>
67
+ <Text color={colors.textMuted}>{toolUpdates.length} tool{toolUpdates.length > 1 ? 's have' : ' has'} updates available</Text>
68
+ </Text>
69
+ <Text>
70
+ <Text color={colors.textDim}>╚═╦═╦═╝</Text>
71
+ </Text>
72
+ </Box>
73
+
74
+ <Box marginTop={1} marginBottom={1} flexDirection="column">
75
+ {toolUpdates.slice(0, 5).map((tool) => (
76
+ <Text key={tool.name} color={colors.textMuted}>
77
+ <Text color={colors.primary}>↑</Text> {tool.name}
78
+ <Text color={colors.textDim}> {tool.installedVersion} → {tool.bundledVersion}</Text>
79
+ </Text>
80
+ ))}
81
+ {toolUpdates.length > 5 && (
82
+ <Text color={colors.textDim}>...and {toolUpdates.length - 5} more</Text>
83
+ )}
84
+ </Box>
85
+
86
+ {isUpdating ? (
87
+ <Text color={colors.primary}>Updating tools...</Text>
88
+ ) : (
89
+ <Box flexDirection="row">
90
+ {buttons.map((button, index) => (
91
+ <Text
92
+ key={button.label}
93
+ backgroundColor={selectedButton === index ? colors.primary : colors.bgSelected}
94
+ color={selectedButton === index ? '#000000' : colors.textMuted}
95
+ bold={selectedButton === index}
96
+ >
97
+ {' '}{button.label}{' '}
98
+ </Text>
99
+ ))}
100
+ </Box>
101
+ )}
102
+
103
+ <Box marginTop={1}>
104
+ <Text color={colors.textDim}>←→ select · enter · "Always" enables auto-update</Text>
105
+ </Box>
106
+ </Box>
107
+ </Box>
108
+ );
109
+ }