@orderful/droid 0.2.0 → 0.4.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.
- package/.claude/CLAUDE.md +41 -0
- package/.github/workflows/changeset-check.yml +43 -0
- package/.github/workflows/release.yml +6 -3
- package/CHANGELOG.md +43 -0
- package/bun.lock +357 -14
- package/dist/agents/README.md +137 -0
- package/dist/bin/droid.js +12 -1
- package/dist/bin/droid.js.map +1 -1
- package/dist/commands/setup.d.ts +8 -0
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +67 -0
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/tui.d.ts +2 -0
- package/dist/commands/tui.d.ts.map +1 -0
- package/dist/commands/tui.js +737 -0
- package/dist/commands/tui.js.map +1 -0
- package/dist/lib/agents.d.ts +53 -0
- package/dist/lib/agents.d.ts.map +1 -0
- package/dist/lib/agents.js +149 -0
- package/dist/lib/agents.js.map +1 -0
- package/dist/lib/skills.d.ts +20 -0
- package/dist/lib/skills.d.ts.map +1 -1
- package/dist/lib/skills.js +102 -0
- package/dist/lib/skills.js.map +1 -1
- package/dist/lib/types.d.ts +5 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/version.d.ts +5 -0
- package/dist/lib/version.d.ts.map +1 -1
- package/dist/lib/version.js +19 -1
- package/dist/lib/version.js.map +1 -1
- package/dist/skills/README.md +85 -0
- package/dist/skills/comments/SKILL.md +8 -0
- package/dist/skills/comments/SKILL.yaml +32 -0
- package/dist/skills/comments/commands/README.md +58 -0
- package/package.json +15 -2
- package/src/agents/README.md +137 -0
- package/src/bin/droid.ts +12 -1
- package/src/commands/setup.ts +77 -0
- package/src/commands/tui.tsx +1535 -0
- package/src/lib/agents.ts +186 -0
- package/src/lib/skills.test.ts +75 -1
- package/src/lib/skills.ts +125 -0
- package/src/lib/types.ts +7 -0
- package/src/lib/version.test.ts +20 -1
- package/src/lib/version.ts +19 -1
- package/src/skills/README.md +85 -0
- package/src/skills/comments/SKILL.md +8 -0
- package/src/skills/comments/SKILL.yaml +32 -0
- package/src/skills/comments/commands/README.md +58 -0
- package/tsconfig.json +5 -3
|
@@ -0,0 +1,1535 @@
|
|
|
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, readdirSync, readFileSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import {
|
|
8
|
+
getBundledSkills,
|
|
9
|
+
getBundledSkillsDir,
|
|
10
|
+
isSkillInstalled,
|
|
11
|
+
getInstalledSkill,
|
|
12
|
+
installSkill,
|
|
13
|
+
uninstallSkill,
|
|
14
|
+
isCommandInstalled,
|
|
15
|
+
installCommand,
|
|
16
|
+
uninstallCommand,
|
|
17
|
+
} from '../lib/skills.js';
|
|
18
|
+
import { getBundledAgents, getBundledAgentsDir, isAgentInstalled, installAgent, uninstallAgent, type AgentManifest } from '../lib/agents.js';
|
|
19
|
+
import { configExists, loadConfig, saveConfig, loadSkillOverrides, saveSkillOverrides } from '../lib/config.js';
|
|
20
|
+
import { configureAIToolPermissions } from './setup.js';
|
|
21
|
+
import { AITool, BuiltInOutput, ConfigOptionType, type DroidConfig, type OutputPreference, type SkillManifest, type ConfigOption, type SkillOverrides } from '../lib/types.js';
|
|
22
|
+
import { getVersion } from '../lib/version.js';
|
|
23
|
+
|
|
24
|
+
type Tab = 'skills' | 'commands' | 'agents' | 'settings';
|
|
25
|
+
type View = 'welcome' | 'setup' | 'menu' | 'detail' | 'configure' | 'readme';
|
|
26
|
+
type SetupStep = 'ai_tool' | 'user_mention' | 'output_preference' | 'confirm';
|
|
27
|
+
|
|
28
|
+
const colors = {
|
|
29
|
+
primary: '#6366f1',
|
|
30
|
+
bgSelected: '#2d2d2d',
|
|
31
|
+
border: '#3a3a3a',
|
|
32
|
+
text: '#e8e8e8',
|
|
33
|
+
textMuted: '#999999',
|
|
34
|
+
textDim: '#6a6a6a',
|
|
35
|
+
success: '#4ade80',
|
|
36
|
+
error: '#f87171',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
interface Command {
|
|
40
|
+
name: string;
|
|
41
|
+
description: string;
|
|
42
|
+
skillName: string;
|
|
43
|
+
usage: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getCommandsFromSkills(): Command[] {
|
|
47
|
+
const skillsDir = getBundledSkillsDir();
|
|
48
|
+
const commands: Command[] = [];
|
|
49
|
+
|
|
50
|
+
if (!existsSync(skillsDir)) return commands;
|
|
51
|
+
|
|
52
|
+
const skillDirs = readdirSync(skillsDir, { withFileTypes: true })
|
|
53
|
+
.filter((d) => d.isDirectory())
|
|
54
|
+
.map((d) => d.name);
|
|
55
|
+
|
|
56
|
+
for (const skillName of skillDirs) {
|
|
57
|
+
const commandsDir = join(skillsDir, skillName, 'commands');
|
|
58
|
+
if (!existsSync(commandsDir)) continue;
|
|
59
|
+
|
|
60
|
+
const cmdFiles = readdirSync(commandsDir).filter((f) => f.endsWith('.md'));
|
|
61
|
+
for (const file of cmdFiles) {
|
|
62
|
+
const content = readFileSync(join(commandsDir, file), 'utf-8');
|
|
63
|
+
const lines = content.split('\n');
|
|
64
|
+
|
|
65
|
+
const headerMatch = lines[0]?.match(/^#\s+\/(\S+)\s*-?\s*(.*)/);
|
|
66
|
+
if (!headerMatch) continue;
|
|
67
|
+
|
|
68
|
+
const usage: string[] = [];
|
|
69
|
+
let inUsage = false;
|
|
70
|
+
for (const line of lines) {
|
|
71
|
+
if (line.startsWith('## Usage')) inUsage = true;
|
|
72
|
+
else if (line.startsWith('## ') && inUsage) break;
|
|
73
|
+
else if (inUsage && line.startsWith('/')) usage.push(line.trim());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
commands.push({
|
|
77
|
+
name: headerMatch[1],
|
|
78
|
+
description: headerMatch[2] || '',
|
|
79
|
+
skillName,
|
|
80
|
+
usage,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return commands;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function detectAITool(): AITool | null {
|
|
89
|
+
try {
|
|
90
|
+
execSync('claude --version', { stdio: 'ignore' });
|
|
91
|
+
return AITool.ClaudeCode;
|
|
92
|
+
} catch {
|
|
93
|
+
// Claude Code not found
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
execSync('opencode --version', { stdio: 'ignore' });
|
|
97
|
+
return AITool.OpenCode;
|
|
98
|
+
} catch {
|
|
99
|
+
// OpenCode not found
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function getOutputOptions(): Array<{ label: string; value: OutputPreference }> {
|
|
105
|
+
const options: Array<{ label: string; value: OutputPreference }> = [
|
|
106
|
+
{ label: 'Terminal (display in CLI)', value: BuiltInOutput.Terminal },
|
|
107
|
+
{ label: 'Editor ($EDITOR)', value: BuiltInOutput.Editor },
|
|
108
|
+
];
|
|
109
|
+
const skills = getBundledSkills();
|
|
110
|
+
for (const skill of skills) {
|
|
111
|
+
if (skill.provides_output) {
|
|
112
|
+
options.push({ label: `${skill.name} (${skill.description})`, value: skill.name });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return options;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function WelcomeScreen({ onContinue }: { onContinue: () => void }) {
|
|
119
|
+
useInput((input, key) => {
|
|
120
|
+
if (key.return || input === 'q') {
|
|
121
|
+
onContinue();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<Box flexDirection="column" alignItems="center" justifyContent="center" height={16}>
|
|
127
|
+
<Box
|
|
128
|
+
flexDirection="column"
|
|
129
|
+
alignItems="center"
|
|
130
|
+
borderStyle="single"
|
|
131
|
+
borderColor={colors.border}
|
|
132
|
+
paddingX={4}
|
|
133
|
+
paddingY={1}
|
|
134
|
+
>
|
|
135
|
+
<Box flexDirection="column">
|
|
136
|
+
<Text>
|
|
137
|
+
<Text color={colors.textDim}>╔═════╗ </Text>
|
|
138
|
+
<Text color={colors.text}>droid</Text>
|
|
139
|
+
<Text color={colors.textDim}> v{getVersion()}</Text>
|
|
140
|
+
</Text>
|
|
141
|
+
<Text>
|
|
142
|
+
<Text color={colors.textDim}>║ </Text>
|
|
143
|
+
<Text color={colors.primary}>●</Text>
|
|
144
|
+
<Text color={colors.textDim}> </Text>
|
|
145
|
+
<Text color={colors.primary}>●</Text>
|
|
146
|
+
<Text color={colors.textDim}> ║ </Text>
|
|
147
|
+
<Text color={colors.textMuted}>Teaching your droid new tricks</Text>
|
|
148
|
+
</Text>
|
|
149
|
+
<Text>
|
|
150
|
+
<Text color={colors.textDim}>╚═╦═╦═╝ </Text>
|
|
151
|
+
<Text color={colors.textDim}>github.com/Orderful/droid</Text>
|
|
152
|
+
</Text>
|
|
153
|
+
</Box>
|
|
154
|
+
|
|
155
|
+
<Box marginTop={2} marginBottom={1}>
|
|
156
|
+
<Text backgroundColor={colors.primary} color="#ffffff" bold>
|
|
157
|
+
{' '}I'm probably the droid you are looking for.{' '}
|
|
158
|
+
</Text>
|
|
159
|
+
</Box>
|
|
160
|
+
|
|
161
|
+
<Text color={colors.textDim}>press enter</Text>
|
|
162
|
+
</Box>
|
|
163
|
+
</Box>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
interface SetupScreenProps {
|
|
168
|
+
onComplete: () => void;
|
|
169
|
+
onSkip: () => void;
|
|
170
|
+
initialConfig?: DroidConfig;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function SetupScreen({ onComplete, onSkip, initialConfig }: SetupScreenProps) {
|
|
174
|
+
const [step, setStep] = useState<SetupStep>('ai_tool');
|
|
175
|
+
const [aiTool, setAITool] = useState<AITool>(
|
|
176
|
+
initialConfig?.ai_tool || detectAITool() || AITool.ClaudeCode
|
|
177
|
+
);
|
|
178
|
+
const [userMention, setUserMention] = useState(initialConfig?.user_mention || '@user');
|
|
179
|
+
const [outputPreference, setOutputPreference] = useState<OutputPreference>(
|
|
180
|
+
initialConfig?.output_preference || BuiltInOutput.Terminal
|
|
181
|
+
);
|
|
182
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
183
|
+
const [error, setError] = useState<string | null>(null);
|
|
184
|
+
|
|
185
|
+
const aiToolOptions = useMemo(() => [
|
|
186
|
+
{ label: 'Claude Code', value: AITool.ClaudeCode },
|
|
187
|
+
{ label: 'OpenCode', value: AITool.OpenCode },
|
|
188
|
+
], []);
|
|
189
|
+
const outputOptions = useMemo(() => getOutputOptions(), []);
|
|
190
|
+
|
|
191
|
+
const steps: SetupStep[] = ['ai_tool', 'user_mention', 'output_preference', 'confirm'];
|
|
192
|
+
const stepIndex = steps.indexOf(step);
|
|
193
|
+
const totalSteps = steps.length - 1; // Don't count confirm as a step
|
|
194
|
+
|
|
195
|
+
const handleUserMentionSubmit = () => {
|
|
196
|
+
if (!userMention.startsWith('@')) {
|
|
197
|
+
setError('Mention should start with @');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
setError(null);
|
|
201
|
+
setStep('output_preference');
|
|
202
|
+
setSelectedIndex(0);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Handle escape during text input (only intercept escape, nothing else)
|
|
206
|
+
useInput((input, key) => {
|
|
207
|
+
if (key.escape) {
|
|
208
|
+
setStep('ai_tool');
|
|
209
|
+
setSelectedIndex(0);
|
|
210
|
+
}
|
|
211
|
+
}, { isActive: step === 'user_mention' });
|
|
212
|
+
|
|
213
|
+
// Handle all input for non-text-input steps
|
|
214
|
+
useInput((input, key) => {
|
|
215
|
+
if (key.escape) {
|
|
216
|
+
if (step === 'ai_tool') {
|
|
217
|
+
onSkip();
|
|
218
|
+
} else if (step === 'output_preference') {
|
|
219
|
+
setStep('user_mention');
|
|
220
|
+
} else if (step === 'confirm') {
|
|
221
|
+
setStep('output_preference');
|
|
222
|
+
setSelectedIndex(0);
|
|
223
|
+
}
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (step === 'ai_tool') {
|
|
228
|
+
if (key.upArrow) setSelectedIndex((prev) => Math.max(0, prev - 1));
|
|
229
|
+
if (key.downArrow) setSelectedIndex((prev) => Math.min(aiToolOptions.length - 1, prev + 1));
|
|
230
|
+
if (key.return) {
|
|
231
|
+
setAITool(aiToolOptions[selectedIndex].value);
|
|
232
|
+
setStep('user_mention');
|
|
233
|
+
}
|
|
234
|
+
} else if (step === 'output_preference') {
|
|
235
|
+
if (key.upArrow) setSelectedIndex((prev) => Math.max(0, prev - 1));
|
|
236
|
+
if (key.downArrow) setSelectedIndex((prev) => Math.min(outputOptions.length - 1, prev + 1));
|
|
237
|
+
if (key.return) {
|
|
238
|
+
setOutputPreference(outputOptions[selectedIndex].value);
|
|
239
|
+
setStep('confirm');
|
|
240
|
+
}
|
|
241
|
+
} else if (step === 'confirm') {
|
|
242
|
+
if (key.return) {
|
|
243
|
+
const config: DroidConfig = {
|
|
244
|
+
...loadConfig(),
|
|
245
|
+
ai_tool: aiTool,
|
|
246
|
+
user_mention: userMention,
|
|
247
|
+
output_preference: outputPreference,
|
|
248
|
+
};
|
|
249
|
+
saveConfig(config);
|
|
250
|
+
configureAIToolPermissions(aiTool);
|
|
251
|
+
onComplete();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}, { isActive: step !== 'user_mention' });
|
|
255
|
+
|
|
256
|
+
const renderHeader = () => (
|
|
257
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
258
|
+
<Text>
|
|
259
|
+
<Text color={colors.textDim}>[</Text>
|
|
260
|
+
<Text color={colors.primary}>●</Text>
|
|
261
|
+
<Text color={colors.textDim}> </Text>
|
|
262
|
+
<Text color={colors.primary}>●</Text>
|
|
263
|
+
<Text color={colors.textDim}>] </Text>
|
|
264
|
+
<Text color={colors.text} bold>droid setup</Text>
|
|
265
|
+
<Text color={colors.textDim}> · Step {Math.min(stepIndex + 1, totalSteps)} of {totalSteps}</Text>
|
|
266
|
+
</Text>
|
|
267
|
+
</Box>
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
if (step === 'ai_tool') {
|
|
271
|
+
return (
|
|
272
|
+
<Box flexDirection="column" padding={1}>
|
|
273
|
+
{renderHeader()}
|
|
274
|
+
<Text color={colors.text}>Which AI tool are you using?</Text>
|
|
275
|
+
<Box flexDirection="column" marginTop={1}>
|
|
276
|
+
{aiToolOptions.map((option, index) => (
|
|
277
|
+
<Text key={option.value}>
|
|
278
|
+
<Text color={colors.textDim}>{index === selectedIndex ? '>' : ' '} </Text>
|
|
279
|
+
<Text color={index === selectedIndex ? colors.text : colors.textMuted}>{option.label}</Text>
|
|
280
|
+
{option.value === detectAITool() && <Text color={colors.success}> (detected)</Text>}
|
|
281
|
+
</Text>
|
|
282
|
+
))}
|
|
283
|
+
</Box>
|
|
284
|
+
<Box marginTop={1}>
|
|
285
|
+
<Text color={colors.textDim}>↑↓ select · enter next · esc skip</Text>
|
|
286
|
+
</Box>
|
|
287
|
+
</Box>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (step === 'user_mention') {
|
|
292
|
+
return (
|
|
293
|
+
<Box flexDirection="column" padding={1}>
|
|
294
|
+
{renderHeader()}
|
|
295
|
+
<Text color={colors.text}>What @mention should be used for you?</Text>
|
|
296
|
+
<Box marginTop={1}>
|
|
297
|
+
<Text color={colors.textDim}>{'> '}</Text>
|
|
298
|
+
<TextInput
|
|
299
|
+
value={userMention}
|
|
300
|
+
onChange={setUserMention}
|
|
301
|
+
onSubmit={handleUserMentionSubmit}
|
|
302
|
+
placeholder="@user"
|
|
303
|
+
/>
|
|
304
|
+
</Box>
|
|
305
|
+
{error && (
|
|
306
|
+
<Box marginTop={1}>
|
|
307
|
+
<Text color={colors.error}>{error}</Text>
|
|
308
|
+
</Box>
|
|
309
|
+
)}
|
|
310
|
+
<Box marginTop={1}>
|
|
311
|
+
<Text color={colors.textDim}>enter next · esc back</Text>
|
|
312
|
+
</Box>
|
|
313
|
+
</Box>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (step === 'output_preference') {
|
|
318
|
+
return (
|
|
319
|
+
<Box flexDirection="column" padding={1}>
|
|
320
|
+
{renderHeader()}
|
|
321
|
+
<Text color={colors.text}>Default output preference for skill results?</Text>
|
|
322
|
+
<Box flexDirection="column" marginTop={1}>
|
|
323
|
+
{outputOptions.map((option, index) => (
|
|
324
|
+
<Text key={option.value}>
|
|
325
|
+
<Text color={colors.textDim}>{index === selectedIndex ? '>' : ' '} </Text>
|
|
326
|
+
<Text color={index === selectedIndex ? colors.text : colors.textMuted}>{option.label}</Text>
|
|
327
|
+
</Text>
|
|
328
|
+
))}
|
|
329
|
+
</Box>
|
|
330
|
+
<Box marginTop={1}>
|
|
331
|
+
<Text color={colors.textDim}>↑↓ select · enter next · esc back</Text>
|
|
332
|
+
</Box>
|
|
333
|
+
</Box>
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Confirm step
|
|
338
|
+
return (
|
|
339
|
+
<Box flexDirection="column" padding={1}>
|
|
340
|
+
{renderHeader()}
|
|
341
|
+
<Text color={colors.text} bold>Review your settings</Text>
|
|
342
|
+
<Box flexDirection="column" marginTop={1}>
|
|
343
|
+
<Text>
|
|
344
|
+
<Text color={colors.textDim}>AI Tool: </Text>
|
|
345
|
+
<Text color={colors.text}>{aiTool === AITool.ClaudeCode ? 'Claude Code' : 'OpenCode'}</Text>
|
|
346
|
+
</Text>
|
|
347
|
+
<Text>
|
|
348
|
+
<Text color={colors.textDim}>Your @mention: </Text>
|
|
349
|
+
<Text color={colors.text}>{userMention}</Text>
|
|
350
|
+
</Text>
|
|
351
|
+
<Text>
|
|
352
|
+
<Text color={colors.textDim}>Output: </Text>
|
|
353
|
+
<Text color={colors.text}>{outputOptions.find((o) => o.value === outputPreference)?.label || outputPreference}</Text>
|
|
354
|
+
</Text>
|
|
355
|
+
</Box>
|
|
356
|
+
<Box marginTop={2}>
|
|
357
|
+
<Text backgroundColor={colors.primary} color="#ffffff" bold>
|
|
358
|
+
{' '}Save{' '}
|
|
359
|
+
</Text>
|
|
360
|
+
</Box>
|
|
361
|
+
<Box marginTop={1}>
|
|
362
|
+
<Text color={colors.textDim}>enter save · esc back</Text>
|
|
363
|
+
</Box>
|
|
364
|
+
</Box>
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function TabBar({ tabs, activeTab }: { tabs: { id: Tab; label: string }[]; activeTab: Tab }) {
|
|
369
|
+
return (
|
|
370
|
+
<Box flexDirection="row">
|
|
371
|
+
{tabs.map((tab) => (
|
|
372
|
+
<Text
|
|
373
|
+
key={tab.id}
|
|
374
|
+
backgroundColor={tab.id === activeTab ? colors.primary : undefined}
|
|
375
|
+
color={tab.id === activeTab ? '#ffffff' : colors.textMuted}
|
|
376
|
+
bold={tab.id === activeTab}
|
|
377
|
+
>
|
|
378
|
+
{' '}{tab.label}{' '}
|
|
379
|
+
</Text>
|
|
380
|
+
))}
|
|
381
|
+
</Box>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function SkillItem({
|
|
386
|
+
skill,
|
|
387
|
+
isSelected,
|
|
388
|
+
isActive,
|
|
389
|
+
}: {
|
|
390
|
+
skill: SkillManifest;
|
|
391
|
+
isSelected: boolean;
|
|
392
|
+
isActive: boolean;
|
|
393
|
+
}) {
|
|
394
|
+
const installed = isSkillInstalled(skill.name);
|
|
395
|
+
const installedInfo = getInstalledSkill(skill.name);
|
|
396
|
+
|
|
397
|
+
return (
|
|
398
|
+
<Box paddingX={1} backgroundColor={isActive ? colors.bgSelected : undefined}>
|
|
399
|
+
<Text>
|
|
400
|
+
<Text color={colors.textDim}>{isSelected ? '>' : ' '} </Text>
|
|
401
|
+
<Text color={isSelected || isActive ? colors.text : colors.textMuted}>{skill.name}</Text>
|
|
402
|
+
{installed && installedInfo && <Text color={colors.textDim}> v{installedInfo.version}</Text>}
|
|
403
|
+
{installed && <Text color={colors.success}> *</Text>}
|
|
404
|
+
</Text>
|
|
405
|
+
</Box>
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function CommandItem({
|
|
410
|
+
command,
|
|
411
|
+
isSelected,
|
|
412
|
+
}: {
|
|
413
|
+
command: Command;
|
|
414
|
+
isSelected: boolean;
|
|
415
|
+
}) {
|
|
416
|
+
const installed = isSkillInstalled(command.skillName);
|
|
417
|
+
|
|
418
|
+
return (
|
|
419
|
+
<Box paddingX={1}>
|
|
420
|
+
<Text>
|
|
421
|
+
<Text color={colors.textDim}>{isSelected ? '>' : ' '} </Text>
|
|
422
|
+
<Text color={isSelected ? colors.text : colors.textMuted}>/{command.name}</Text>
|
|
423
|
+
{installed && <Text color={colors.success}> *</Text>}
|
|
424
|
+
</Text>
|
|
425
|
+
</Box>
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function SkillDetails({
|
|
430
|
+
skill,
|
|
431
|
+
isFocused,
|
|
432
|
+
selectedAction,
|
|
433
|
+
}: {
|
|
434
|
+
skill: SkillManifest | null;
|
|
435
|
+
isFocused: boolean;
|
|
436
|
+
selectedAction: number;
|
|
437
|
+
}) {
|
|
438
|
+
if (!skill) {
|
|
439
|
+
return (
|
|
440
|
+
<Box paddingLeft={2} flexGrow={1}>
|
|
441
|
+
<Text color={colors.textDim}>Select a skill</Text>
|
|
442
|
+
</Box>
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const installed = isSkillInstalled(skill.name);
|
|
447
|
+
const skillCommands = getCommandsFromSkills().filter((c) => c.skillName === skill.name);
|
|
448
|
+
|
|
449
|
+
const actions = installed
|
|
450
|
+
? [
|
|
451
|
+
{ id: 'view', label: 'View', variant: 'default' },
|
|
452
|
+
{ id: 'configure', label: 'Configure', variant: 'primary' },
|
|
453
|
+
{ id: 'uninstall', label: 'Uninstall', variant: 'danger' },
|
|
454
|
+
]
|
|
455
|
+
: [
|
|
456
|
+
{ id: 'view', label: 'View', variant: 'default' },
|
|
457
|
+
{ id: 'install', label: 'Install', variant: 'primary' },
|
|
458
|
+
];
|
|
459
|
+
|
|
460
|
+
return (
|
|
461
|
+
<Box flexDirection="column" paddingLeft={2} flexGrow={1}>
|
|
462
|
+
<Text color={colors.text} bold>{skill.name}</Text>
|
|
463
|
+
|
|
464
|
+
<Box marginTop={1}>
|
|
465
|
+
<Text color={colors.textDim}>
|
|
466
|
+
{skill.version}
|
|
467
|
+
{skill.status && ` · ${skill.status}`}
|
|
468
|
+
{installed && <Text color={colors.success}> · installed</Text>}
|
|
469
|
+
</Text>
|
|
470
|
+
</Box>
|
|
471
|
+
|
|
472
|
+
<Box marginTop={1}>
|
|
473
|
+
<Text color={colors.textMuted}>{skill.description}</Text>
|
|
474
|
+
</Box>
|
|
475
|
+
|
|
476
|
+
{skillCommands.length > 0 && (
|
|
477
|
+
<Box marginTop={1}>
|
|
478
|
+
<Text color={colors.textDim}>
|
|
479
|
+
Commands: {skillCommands.map((c) => `/${c.name}`).join(', ')}
|
|
480
|
+
</Text>
|
|
481
|
+
</Box>
|
|
482
|
+
)}
|
|
483
|
+
|
|
484
|
+
{skill.examples && skill.examples.length > 0 && (
|
|
485
|
+
<Box flexDirection="column" marginTop={1}>
|
|
486
|
+
<Text color={colors.textDim}>
|
|
487
|
+
Examples{skill.examples.length > 2 ? ` (showing 2 of ${skill.examples.length})` : ''}:
|
|
488
|
+
</Text>
|
|
489
|
+
{skill.examples.slice(0, 2).map((example, i) => (
|
|
490
|
+
<Box key={i} flexDirection="column" marginTop={i > 0 ? 1 : 0}>
|
|
491
|
+
<Text color={colors.textMuted}> {example.title}</Text>
|
|
492
|
+
{example.code
|
|
493
|
+
.trim()
|
|
494
|
+
.split('\n')
|
|
495
|
+
.slice(0, 3)
|
|
496
|
+
.map((line, j) => (
|
|
497
|
+
<Text key={j} color={colors.textDim}>
|
|
498
|
+
{' '}{line}
|
|
499
|
+
</Text>
|
|
500
|
+
))}
|
|
501
|
+
</Box>
|
|
502
|
+
))}
|
|
503
|
+
</Box>
|
|
504
|
+
)}
|
|
505
|
+
|
|
506
|
+
{isFocused && (
|
|
507
|
+
<Box flexDirection="row" marginTop={1}>
|
|
508
|
+
{actions.map((action, index) => (
|
|
509
|
+
<Text
|
|
510
|
+
key={action.id}
|
|
511
|
+
backgroundColor={
|
|
512
|
+
selectedAction === index
|
|
513
|
+
? action.variant === 'danger'
|
|
514
|
+
? colors.error
|
|
515
|
+
: colors.primary
|
|
516
|
+
: colors.bgSelected
|
|
517
|
+
}
|
|
518
|
+
color={selectedAction === index ? '#ffffff' : colors.textMuted}
|
|
519
|
+
bold={selectedAction === index}
|
|
520
|
+
>
|
|
521
|
+
{' '}{action.label}{' '}
|
|
522
|
+
</Text>
|
|
523
|
+
))}
|
|
524
|
+
</Box>
|
|
525
|
+
)}
|
|
526
|
+
</Box>
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function CommandDetails({
|
|
531
|
+
command,
|
|
532
|
+
isFocused,
|
|
533
|
+
selectedAction,
|
|
534
|
+
}: {
|
|
535
|
+
command: Command | null;
|
|
536
|
+
isFocused: boolean;
|
|
537
|
+
selectedAction: number;
|
|
538
|
+
}) {
|
|
539
|
+
if (!command) {
|
|
540
|
+
return (
|
|
541
|
+
<Box paddingLeft={2} flexGrow={1}>
|
|
542
|
+
<Text color={colors.textDim}>Select a command</Text>
|
|
543
|
+
</Box>
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const skillInstalled = isSkillInstalled(command.skillName);
|
|
548
|
+
const installed = isCommandInstalled(command.name, command.skillName);
|
|
549
|
+
|
|
550
|
+
// If skill is installed, command comes with it - no standalone uninstall
|
|
551
|
+
const actions = skillInstalled
|
|
552
|
+
? [{ id: 'view', label: 'View', variant: 'default' }]
|
|
553
|
+
: installed
|
|
554
|
+
? [
|
|
555
|
+
{ id: 'view', label: 'View', variant: 'default' },
|
|
556
|
+
{ id: 'uninstall', label: 'Uninstall', variant: 'danger' },
|
|
557
|
+
]
|
|
558
|
+
: [
|
|
559
|
+
{ id: 'view', label: 'View', variant: 'default' },
|
|
560
|
+
{ id: 'install', label: 'Install', variant: 'primary' },
|
|
561
|
+
];
|
|
562
|
+
|
|
563
|
+
return (
|
|
564
|
+
<Box flexDirection="column" paddingLeft={2} flexGrow={1}>
|
|
565
|
+
<Text color={colors.text} bold>/{command.name}</Text>
|
|
566
|
+
|
|
567
|
+
<Box marginTop={1}>
|
|
568
|
+
<Text color={colors.textDim}>
|
|
569
|
+
from {command.skillName}
|
|
570
|
+
{skillInstalled && <Text color={colors.success}> · via skill</Text>}
|
|
571
|
+
{!skillInstalled && installed && <Text color={colors.success}> · installed</Text>}
|
|
572
|
+
</Text>
|
|
573
|
+
</Box>
|
|
574
|
+
|
|
575
|
+
<Box marginTop={1}>
|
|
576
|
+
<Text color={colors.textMuted}>{command.description}</Text>
|
|
577
|
+
</Box>
|
|
578
|
+
|
|
579
|
+
{command.usage.length > 0 && (
|
|
580
|
+
<Box flexDirection="column" marginTop={1}>
|
|
581
|
+
<Text color={colors.textDim}>Usage:</Text>
|
|
582
|
+
{command.usage.map((u, i) => (
|
|
583
|
+
<Text key={i} color={colors.textMuted}>
|
|
584
|
+
{' '}{u}
|
|
585
|
+
</Text>
|
|
586
|
+
))}
|
|
587
|
+
</Box>
|
|
588
|
+
)}
|
|
589
|
+
|
|
590
|
+
{isFocused && (
|
|
591
|
+
<Box flexDirection="row" marginTop={1}>
|
|
592
|
+
{actions.map((action, index) => (
|
|
593
|
+
<Text
|
|
594
|
+
key={action.id}
|
|
595
|
+
backgroundColor={
|
|
596
|
+
selectedAction === index
|
|
597
|
+
? action.variant === 'danger'
|
|
598
|
+
? colors.error
|
|
599
|
+
: colors.primary
|
|
600
|
+
: colors.bgSelected
|
|
601
|
+
}
|
|
602
|
+
color={selectedAction === index ? '#ffffff' : colors.textMuted}
|
|
603
|
+
bold={selectedAction === index}
|
|
604
|
+
>
|
|
605
|
+
{' '}{action.label}{' '}
|
|
606
|
+
</Text>
|
|
607
|
+
))}
|
|
608
|
+
</Box>
|
|
609
|
+
)}
|
|
610
|
+
</Box>
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function AgentItem({ agent, isSelected }: { agent: AgentManifest; isSelected: boolean }) {
|
|
615
|
+
const statusDisplay = agent.status === 'alpha' ? '[alpha]' : agent.status === 'beta' ? '[beta]' : '';
|
|
616
|
+
|
|
617
|
+
return (
|
|
618
|
+
<Box paddingX={1}>
|
|
619
|
+
<Text>
|
|
620
|
+
<Text color={colors.textDim}>{isSelected ? '>' : ' '} </Text>
|
|
621
|
+
<Text color={isSelected ? colors.text : colors.textMuted}>{agent.name}</Text>
|
|
622
|
+
<Text color={colors.textDim}> v{agent.version}</Text>
|
|
623
|
+
{statusDisplay && <Text color={colors.textDim}> {statusDisplay}</Text>}
|
|
624
|
+
</Text>
|
|
625
|
+
</Box>
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function AgentDetails({
|
|
630
|
+
agent,
|
|
631
|
+
isFocused,
|
|
632
|
+
selectedAction,
|
|
633
|
+
}: {
|
|
634
|
+
agent: AgentManifest | null;
|
|
635
|
+
isFocused: boolean;
|
|
636
|
+
selectedAction: number;
|
|
637
|
+
}) {
|
|
638
|
+
if (!agent) {
|
|
639
|
+
return (
|
|
640
|
+
<Box paddingLeft={2} flexGrow={1}>
|
|
641
|
+
<Text color={colors.textDim}>Select an agent</Text>
|
|
642
|
+
</Box>
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const installed = isAgentInstalled(agent.name);
|
|
647
|
+
const statusDisplay = agent.status === 'alpha' ? '[alpha]' : agent.status === 'beta' ? '[beta]' : '';
|
|
648
|
+
const modeDisplay = agent.mode === 'primary' ? 'primary' : agent.mode === 'all' ? 'primary/subagent' : 'subagent';
|
|
649
|
+
|
|
650
|
+
const actions = installed
|
|
651
|
+
? [
|
|
652
|
+
{ id: 'view', label: 'View', variant: 'default' },
|
|
653
|
+
{ id: 'uninstall', label: 'Uninstall', variant: 'danger' },
|
|
654
|
+
]
|
|
655
|
+
: [
|
|
656
|
+
{ id: 'view', label: 'View', variant: 'default' },
|
|
657
|
+
{ id: 'install', label: 'Install', variant: 'primary' },
|
|
658
|
+
];
|
|
659
|
+
|
|
660
|
+
return (
|
|
661
|
+
<Box flexDirection="column" paddingLeft={2} flexGrow={1}>
|
|
662
|
+
<Text color={colors.text} bold>{agent.name}</Text>
|
|
663
|
+
|
|
664
|
+
<Box marginTop={1}>
|
|
665
|
+
<Text color={colors.textDim}>
|
|
666
|
+
v{agent.version}
|
|
667
|
+
{statusDisplay && <Text> · {statusDisplay}</Text>}
|
|
668
|
+
{' · '}{modeDisplay}
|
|
669
|
+
{installed && <Text color={colors.success}> · installed</Text>}
|
|
670
|
+
</Text>
|
|
671
|
+
</Box>
|
|
672
|
+
|
|
673
|
+
<Box marginTop={1}>
|
|
674
|
+
<Text color={colors.textMuted}>{agent.description}</Text>
|
|
675
|
+
</Box>
|
|
676
|
+
|
|
677
|
+
{agent.tools && agent.tools.length > 0 && (
|
|
678
|
+
<Box marginTop={1}>
|
|
679
|
+
<Text color={colors.textDim}>
|
|
680
|
+
Tools: {agent.tools.join(', ')}
|
|
681
|
+
</Text>
|
|
682
|
+
</Box>
|
|
683
|
+
)}
|
|
684
|
+
|
|
685
|
+
{agent.triggers && agent.triggers.length > 0 && (
|
|
686
|
+
<Box flexDirection="column" marginTop={1}>
|
|
687
|
+
<Text color={colors.textDim}>Triggers:</Text>
|
|
688
|
+
{agent.triggers.slice(0, 3).map((trigger, i) => (
|
|
689
|
+
<Text key={i} color={colors.textMuted}>
|
|
690
|
+
{' '}"{trigger}"
|
|
691
|
+
</Text>
|
|
692
|
+
))}
|
|
693
|
+
</Box>
|
|
694
|
+
)}
|
|
695
|
+
|
|
696
|
+
{isFocused && (
|
|
697
|
+
<Box flexDirection="row" marginTop={1}>
|
|
698
|
+
{actions.map((action, index) => (
|
|
699
|
+
<Text
|
|
700
|
+
key={action.id}
|
|
701
|
+
backgroundColor={
|
|
702
|
+
selectedAction === index
|
|
703
|
+
? action.variant === 'danger'
|
|
704
|
+
? colors.error
|
|
705
|
+
: colors.primary
|
|
706
|
+
: colors.bgSelected
|
|
707
|
+
}
|
|
708
|
+
color={selectedAction === index ? '#ffffff' : colors.textMuted}
|
|
709
|
+
bold={selectedAction === index}
|
|
710
|
+
>
|
|
711
|
+
{' '}{action.label}{' '}
|
|
712
|
+
</Text>
|
|
713
|
+
))}
|
|
714
|
+
</Box>
|
|
715
|
+
)}
|
|
716
|
+
</Box>
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function MarkdownLine({ line, inCodeBlock }: { line: string; inCodeBlock: boolean }) {
|
|
721
|
+
// Code block content
|
|
722
|
+
if (inCodeBlock) {
|
|
723
|
+
return <Text color="#a5d6ff">{line || ' '}</Text>;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Code block delimiter
|
|
727
|
+
if (line.startsWith('```')) {
|
|
728
|
+
return <Text color={colors.textDim}>{line}</Text>;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// Headers
|
|
732
|
+
if (line.startsWith('# ')) {
|
|
733
|
+
return <Text color={colors.text} bold>{line.slice(2)}</Text>;
|
|
734
|
+
}
|
|
735
|
+
if (line.startsWith('## ')) {
|
|
736
|
+
return <Text color={colors.text} bold>{line.slice(3)}</Text>;
|
|
737
|
+
}
|
|
738
|
+
if (line.startsWith('### ')) {
|
|
739
|
+
return <Text color="#c9d1d9" bold>{line.slice(4)}</Text>;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// YAML frontmatter delimiter
|
|
743
|
+
if (line === '---') {
|
|
744
|
+
return <Text color={colors.textDim}>{line}</Text>;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// List items
|
|
748
|
+
if (line.match(/^[\s]*[-*]\s/)) {
|
|
749
|
+
return <Text color={colors.textMuted}>{line}</Text>;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Blockquotes
|
|
753
|
+
if (line.startsWith('>')) {
|
|
754
|
+
return <Text color="#8b949e" italic>{line}</Text>;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Table rows
|
|
758
|
+
if (line.includes('|')) {
|
|
759
|
+
return <Text color={colors.textMuted}>{line}</Text>;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Default
|
|
763
|
+
return <Text color={colors.textMuted}>{line || ' '}</Text>;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function ReadmeViewer({
|
|
767
|
+
title,
|
|
768
|
+
content,
|
|
769
|
+
onClose,
|
|
770
|
+
}: {
|
|
771
|
+
title: string;
|
|
772
|
+
content: string;
|
|
773
|
+
onClose: () => void;
|
|
774
|
+
}) {
|
|
775
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
776
|
+
const lines = useMemo(() => content.split('\n'), [content]);
|
|
777
|
+
const maxVisible = 20;
|
|
778
|
+
|
|
779
|
+
// Pre-compute code block state for each line
|
|
780
|
+
const lineStates = useMemo(() => {
|
|
781
|
+
const states: boolean[] = [];
|
|
782
|
+
let inCode = false;
|
|
783
|
+
for (const line of lines) {
|
|
784
|
+
if (line.startsWith('```')) {
|
|
785
|
+
states.push(false); // Delimiter itself is not "in" code block for styling
|
|
786
|
+
inCode = !inCode;
|
|
787
|
+
} else {
|
|
788
|
+
states.push(inCode);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return states;
|
|
792
|
+
}, [lines]);
|
|
793
|
+
|
|
794
|
+
// Max offset: when at end, we have top indicator + (maxVisible-1) content lines
|
|
795
|
+
// So max offset is lines.length - (maxVisible - 1) = lines.length - maxVisible + 1
|
|
796
|
+
const maxOffset = Math.max(0, lines.length - maxVisible + 1);
|
|
797
|
+
|
|
798
|
+
useInput((input, key) => {
|
|
799
|
+
if (key.escape) {
|
|
800
|
+
onClose();
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
if (key.upArrow) {
|
|
804
|
+
setScrollOffset((prev) => Math.max(0, prev - 1));
|
|
805
|
+
}
|
|
806
|
+
if (key.downArrow) {
|
|
807
|
+
setScrollOffset((prev) => Math.min(maxOffset, prev + 1));
|
|
808
|
+
}
|
|
809
|
+
if (key.pageDown || input === ' ') {
|
|
810
|
+
setScrollOffset((prev) => Math.min(maxOffset, prev + maxVisible));
|
|
811
|
+
}
|
|
812
|
+
if (key.pageUp) {
|
|
813
|
+
setScrollOffset((prev) => Math.max(0, prev - maxVisible));
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
// Adjust visible lines based on whether indicators are shown
|
|
818
|
+
const showTopIndicator = scrollOffset > 0;
|
|
819
|
+
// Reserve space for bottom indicator if not at end
|
|
820
|
+
const contentLines = maxVisible - (showTopIndicator ? 1 : 0);
|
|
821
|
+
const endIndex = Math.min(scrollOffset + contentLines, lines.length);
|
|
822
|
+
const showBottomIndicator = endIndex < lines.length;
|
|
823
|
+
const actualContentLines = contentLines - (showBottomIndicator ? 1 : 0);
|
|
824
|
+
const visibleLines = lines.slice(scrollOffset, scrollOffset + actualContentLines);
|
|
825
|
+
|
|
826
|
+
return (
|
|
827
|
+
<Box flexDirection="column" padding={1}>
|
|
828
|
+
<Box marginBottom={1}>
|
|
829
|
+
<Text color={colors.text} bold>{title}</Text>
|
|
830
|
+
<Text color={colors.textDim}> · {lines.length} lines</Text>
|
|
831
|
+
</Box>
|
|
832
|
+
|
|
833
|
+
<Box
|
|
834
|
+
flexDirection="column"
|
|
835
|
+
borderStyle="single"
|
|
836
|
+
borderColor={colors.border}
|
|
837
|
+
paddingX={1}
|
|
838
|
+
>
|
|
839
|
+
{showTopIndicator && (
|
|
840
|
+
<Text color={colors.textDim}>↑ {scrollOffset} more lines</Text>
|
|
841
|
+
)}
|
|
842
|
+
{visibleLines.map((line, i) => (
|
|
843
|
+
<MarkdownLine key={scrollOffset + i} line={line} inCodeBlock={lineStates[scrollOffset + i]} />
|
|
844
|
+
))}
|
|
845
|
+
{showBottomIndicator && (
|
|
846
|
+
<Text color={colors.textDim}>↓ {lines.length - scrollOffset - actualContentLines} more lines</Text>
|
|
847
|
+
)}
|
|
848
|
+
</Box>
|
|
849
|
+
|
|
850
|
+
<Box marginTop={1}>
|
|
851
|
+
<Text color={colors.textDim}>↑↓ scroll · space/pgdn page · esc back</Text>
|
|
852
|
+
</Box>
|
|
853
|
+
</Box>
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
function SettingsDetails({
|
|
858
|
+
onEditSettings,
|
|
859
|
+
isFocused,
|
|
860
|
+
}: {
|
|
861
|
+
onEditSettings: () => void;
|
|
862
|
+
isFocused: boolean;
|
|
863
|
+
}) {
|
|
864
|
+
const config = loadConfig();
|
|
865
|
+
const outputOptions = getOutputOptions();
|
|
866
|
+
|
|
867
|
+
return (
|
|
868
|
+
<Box flexDirection="column" paddingLeft={2} flexGrow={1}>
|
|
869
|
+
<Text color={colors.text} bold>Settings</Text>
|
|
870
|
+
|
|
871
|
+
<Box flexDirection="column" marginTop={1}>
|
|
872
|
+
<Text>
|
|
873
|
+
<Text color={colors.textDim}>AI Tool: </Text>
|
|
874
|
+
<Text color={colors.text}>
|
|
875
|
+
{config.ai_tool === AITool.ClaudeCode ? 'Claude Code' : 'OpenCode'}
|
|
876
|
+
</Text>
|
|
877
|
+
</Text>
|
|
878
|
+
<Text>
|
|
879
|
+
<Text color={colors.textDim}>Your @mention: </Text>
|
|
880
|
+
<Text color={colors.text}>{config.user_mention}</Text>
|
|
881
|
+
</Text>
|
|
882
|
+
<Text>
|
|
883
|
+
<Text color={colors.textDim}>Output: </Text>
|
|
884
|
+
<Text color={colors.text}>
|
|
885
|
+
{outputOptions.find((o) => o.value === config.output_preference)?.label || config.output_preference}
|
|
886
|
+
</Text>
|
|
887
|
+
</Text>
|
|
888
|
+
</Box>
|
|
889
|
+
|
|
890
|
+
<Box marginTop={1}>
|
|
891
|
+
<Text color={colors.textDim}>Config: ~/.droid/config.yaml</Text>
|
|
892
|
+
</Box>
|
|
893
|
+
|
|
894
|
+
{isFocused && (
|
|
895
|
+
<Box marginTop={2}>
|
|
896
|
+
<Text backgroundColor={colors.primary} color="#ffffff" bold>
|
|
897
|
+
{' '}Edit Settings{' '}
|
|
898
|
+
</Text>
|
|
899
|
+
</Box>
|
|
900
|
+
)}
|
|
901
|
+
|
|
902
|
+
{!isFocused && (
|
|
903
|
+
<Box marginTop={2}>
|
|
904
|
+
<Text color={colors.textDim}>press enter to edit</Text>
|
|
905
|
+
</Box>
|
|
906
|
+
)}
|
|
907
|
+
</Box>
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
interface SkillConfigScreenProps {
|
|
912
|
+
skill: SkillManifest;
|
|
913
|
+
onComplete: () => void;
|
|
914
|
+
onCancel: () => void;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function SkillConfigScreen({ skill, onComplete, onCancel }: SkillConfigScreenProps) {
|
|
918
|
+
const configSchema = skill.config_schema || {};
|
|
919
|
+
const configKeys = Object.keys(configSchema);
|
|
920
|
+
|
|
921
|
+
const initialOverrides = useMemo(() => loadSkillOverrides(skill.name), [skill.name]);
|
|
922
|
+
|
|
923
|
+
// Initialize values from saved overrides or defaults
|
|
924
|
+
const [values, setValues] = useState<SkillOverrides>(() => {
|
|
925
|
+
const initial: SkillOverrides = {};
|
|
926
|
+
for (const key of configKeys) {
|
|
927
|
+
const option = configSchema[key];
|
|
928
|
+
initial[key] = initialOverrides[key] ?? option.default ?? (option.type === ConfigOptionType.Boolean ? false : '');
|
|
929
|
+
}
|
|
930
|
+
return initial;
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
934
|
+
const [editingField, setEditingField] = useState<string | null>(null);
|
|
935
|
+
const [editValue, setEditValue] = useState('');
|
|
936
|
+
|
|
937
|
+
const handleSave = () => {
|
|
938
|
+
saveSkillOverrides(skill.name, values);
|
|
939
|
+
onComplete();
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
const handleSubmitEdit = () => {
|
|
943
|
+
if (editingField) {
|
|
944
|
+
setValues((prev) => ({ ...prev, [editingField]: editValue }));
|
|
945
|
+
setEditingField(null);
|
|
946
|
+
setEditValue('');
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
// Handle text input for string fields
|
|
951
|
+
useInput((input, key) => {
|
|
952
|
+
if (key.escape) {
|
|
953
|
+
setEditingField(null);
|
|
954
|
+
setEditValue('');
|
|
955
|
+
}
|
|
956
|
+
}, { isActive: editingField !== null });
|
|
957
|
+
|
|
958
|
+
// Handle navigation and actions when not editing
|
|
959
|
+
useInput((input, key) => {
|
|
960
|
+
if (key.escape) {
|
|
961
|
+
onCancel();
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
if (key.upArrow) {
|
|
966
|
+
setSelectedIndex((prev) => Math.max(0, prev - 1));
|
|
967
|
+
}
|
|
968
|
+
if (key.downArrow) {
|
|
969
|
+
// +1 for the Save button at the end
|
|
970
|
+
setSelectedIndex((prev) => Math.min(configKeys.length, prev + 1));
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
if (key.return) {
|
|
974
|
+
// Save button is at index === configKeys.length
|
|
975
|
+
if (selectedIndex === configKeys.length) {
|
|
976
|
+
handleSave();
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const key = configKeys[selectedIndex];
|
|
981
|
+
const option = configSchema[key];
|
|
982
|
+
|
|
983
|
+
if (option.type === ConfigOptionType.Boolean) {
|
|
984
|
+
// Toggle boolean
|
|
985
|
+
setValues((prev) => ({ ...prev, [key]: !prev[key] }));
|
|
986
|
+
} else if (option.type === ConfigOptionType.String) {
|
|
987
|
+
// Enter edit mode for string
|
|
988
|
+
setEditingField(key);
|
|
989
|
+
setEditValue(String(values[key] || ''));
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}, { isActive: editingField === null });
|
|
993
|
+
|
|
994
|
+
if (configKeys.length === 0) {
|
|
995
|
+
return (
|
|
996
|
+
<Box flexDirection="column" padding={1}>
|
|
997
|
+
<Box marginBottom={1}>
|
|
998
|
+
<Text>
|
|
999
|
+
<Text color={colors.textDim}>[</Text>
|
|
1000
|
+
<Text color={colors.primary}>●</Text>
|
|
1001
|
+
<Text color={colors.textDim}> </Text>
|
|
1002
|
+
<Text color={colors.primary}>●</Text>
|
|
1003
|
+
<Text color={colors.textDim}>] </Text>
|
|
1004
|
+
<Text color={colors.text} bold>configure {skill.name}</Text>
|
|
1005
|
+
</Text>
|
|
1006
|
+
</Box>
|
|
1007
|
+
<Text color={colors.textMuted}>This skill has no configuration options.</Text>
|
|
1008
|
+
<Box marginTop={1}>
|
|
1009
|
+
<Text color={colors.textDim}>esc to go back</Text>
|
|
1010
|
+
</Box>
|
|
1011
|
+
</Box>
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
return (
|
|
1016
|
+
<Box flexDirection="column" padding={1}>
|
|
1017
|
+
<Box marginBottom={1}>
|
|
1018
|
+
<Text>
|
|
1019
|
+
<Text color={colors.textDim}>[</Text>
|
|
1020
|
+
<Text color={colors.primary}>●</Text>
|
|
1021
|
+
<Text color={colors.textDim}> </Text>
|
|
1022
|
+
<Text color={colors.primary}>●</Text>
|
|
1023
|
+
<Text color={colors.textDim}>] </Text>
|
|
1024
|
+
<Text color={colors.text} bold>configure {skill.name}</Text>
|
|
1025
|
+
</Text>
|
|
1026
|
+
</Box>
|
|
1027
|
+
|
|
1028
|
+
<Box flexDirection="column">
|
|
1029
|
+
{configKeys.map((key, index) => {
|
|
1030
|
+
const option = configSchema[key];
|
|
1031
|
+
const isSelected = selectedIndex === index;
|
|
1032
|
+
const isEditing = editingField === key;
|
|
1033
|
+
|
|
1034
|
+
return (
|
|
1035
|
+
<Box key={key} flexDirection="column" marginBottom={1}>
|
|
1036
|
+
<Text>
|
|
1037
|
+
<Text color={colors.textDim}>{isSelected ? '>' : ' '} </Text>
|
|
1038
|
+
<Text color={isSelected ? colors.text : colors.textMuted}>{key}</Text>
|
|
1039
|
+
</Text>
|
|
1040
|
+
<Text color={colors.textDim}> {option.description}</Text>
|
|
1041
|
+
<Box>
|
|
1042
|
+
<Text color={colors.textDim}> </Text>
|
|
1043
|
+
{option.type === ConfigOptionType.Boolean ? (
|
|
1044
|
+
<Text color={colors.text}>
|
|
1045
|
+
[{values[key] ? 'x' : ' '}] {values[key] ? 'enabled' : 'disabled'}
|
|
1046
|
+
</Text>
|
|
1047
|
+
) : isEditing ? (
|
|
1048
|
+
<Box>
|
|
1049
|
+
<Text color={colors.textDim}>{'> '}</Text>
|
|
1050
|
+
<TextInput
|
|
1051
|
+
value={editValue}
|
|
1052
|
+
onChange={setEditValue}
|
|
1053
|
+
onSubmit={handleSubmitEdit}
|
|
1054
|
+
/>
|
|
1055
|
+
</Box>
|
|
1056
|
+
) : (
|
|
1057
|
+
<Text color={colors.text}>{String(values[key]) || '(not set)'}</Text>
|
|
1058
|
+
)}
|
|
1059
|
+
</Box>
|
|
1060
|
+
</Box>
|
|
1061
|
+
);
|
|
1062
|
+
})}
|
|
1063
|
+
|
|
1064
|
+
{/* Save button */}
|
|
1065
|
+
<Box marginTop={1}>
|
|
1066
|
+
<Text>
|
|
1067
|
+
<Text color={colors.textDim}>{selectedIndex === configKeys.length ? '>' : ' '} </Text>
|
|
1068
|
+
<Text
|
|
1069
|
+
backgroundColor={selectedIndex === configKeys.length ? colors.primary : undefined}
|
|
1070
|
+
color={selectedIndex === configKeys.length ? '#ffffff' : colors.textMuted}
|
|
1071
|
+
bold={selectedIndex === configKeys.length}
|
|
1072
|
+
>
|
|
1073
|
+
{' '}Save{' '}
|
|
1074
|
+
</Text>
|
|
1075
|
+
</Text>
|
|
1076
|
+
</Box>
|
|
1077
|
+
</Box>
|
|
1078
|
+
|
|
1079
|
+
<Box marginTop={1}>
|
|
1080
|
+
<Text color={colors.textDim}>
|
|
1081
|
+
{editingField ? 'enter save · esc cancel' : '↑↓ select · enter toggle/edit · esc back'}
|
|
1082
|
+
</Text>
|
|
1083
|
+
</Box>
|
|
1084
|
+
</Box>
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function App() {
|
|
1089
|
+
const { exit } = useApp();
|
|
1090
|
+
const tabs: { id: Tab; label: string }[] = [
|
|
1091
|
+
{ id: 'skills', label: 'Skills' },
|
|
1092
|
+
{ id: 'commands', label: 'Commands' },
|
|
1093
|
+
{ id: 'agents', label: 'Agents' },
|
|
1094
|
+
{ id: 'settings', label: 'Settings' },
|
|
1095
|
+
];
|
|
1096
|
+
|
|
1097
|
+
const [activeTab, setActiveTab] = useState<Tab>('skills');
|
|
1098
|
+
const [tabIndex, setTabIndex] = useState(0);
|
|
1099
|
+
const [view, setView] = useState<View>('welcome');
|
|
1100
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
1101
|
+
const [selectedAction, setSelectedAction] = useState(0);
|
|
1102
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
1103
|
+
const [message, setMessage] = useState<{ text: string; type: 'success' | 'error' } | null>(null);
|
|
1104
|
+
const [isEditingSettings, setIsEditingSettings] = useState(false);
|
|
1105
|
+
const [readmeContent, setReadmeContent] = useState<{ title: string; content: string } | null>(null);
|
|
1106
|
+
|
|
1107
|
+
const MAX_VISIBLE_ITEMS = 6;
|
|
1108
|
+
|
|
1109
|
+
const skills = getBundledSkills();
|
|
1110
|
+
const commands = getCommandsFromSkills();
|
|
1111
|
+
const agents = getBundledAgents();
|
|
1112
|
+
|
|
1113
|
+
useInput((input, key) => {
|
|
1114
|
+
if (message) setMessage(null);
|
|
1115
|
+
|
|
1116
|
+
if (view === 'welcome') return;
|
|
1117
|
+
|
|
1118
|
+
if (input === 'q') {
|
|
1119
|
+
exit();
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
if (view === 'menu') {
|
|
1124
|
+
if (key.leftArrow) {
|
|
1125
|
+
const newIndex = Math.max(0, tabIndex - 1);
|
|
1126
|
+
setTabIndex(newIndex);
|
|
1127
|
+
setActiveTab(tabs[newIndex].id);
|
|
1128
|
+
setSelectedIndex(0);
|
|
1129
|
+
setScrollOffset(0);
|
|
1130
|
+
}
|
|
1131
|
+
if (key.rightArrow) {
|
|
1132
|
+
const newIndex = Math.min(tabs.length - 1, tabIndex + 1);
|
|
1133
|
+
setTabIndex(newIndex);
|
|
1134
|
+
setActiveTab(tabs[newIndex].id);
|
|
1135
|
+
setSelectedIndex(0);
|
|
1136
|
+
setScrollOffset(0);
|
|
1137
|
+
}
|
|
1138
|
+
if (key.upArrow) {
|
|
1139
|
+
setSelectedIndex((prev) => {
|
|
1140
|
+
const newIndex = Math.max(0, prev - 1);
|
|
1141
|
+
// Scroll up if needed
|
|
1142
|
+
if (newIndex < scrollOffset) {
|
|
1143
|
+
setScrollOffset(newIndex);
|
|
1144
|
+
}
|
|
1145
|
+
return newIndex;
|
|
1146
|
+
});
|
|
1147
|
+
setSelectedAction(0);
|
|
1148
|
+
}
|
|
1149
|
+
if (key.downArrow) {
|
|
1150
|
+
const maxIndex =
|
|
1151
|
+
activeTab === 'skills' ? skills.length - 1 : activeTab === 'commands' ? commands.length - 1 : 0;
|
|
1152
|
+
setSelectedIndex((prev) => {
|
|
1153
|
+
const newIndex = Math.min(maxIndex, prev + 1);
|
|
1154
|
+
// Scroll down if needed
|
|
1155
|
+
if (newIndex >= scrollOffset + MAX_VISIBLE_ITEMS) {
|
|
1156
|
+
setScrollOffset(newIndex - MAX_VISIBLE_ITEMS + 1);
|
|
1157
|
+
}
|
|
1158
|
+
return newIndex;
|
|
1159
|
+
});
|
|
1160
|
+
setSelectedAction(0);
|
|
1161
|
+
}
|
|
1162
|
+
if (key.return) {
|
|
1163
|
+
if (activeTab === 'skills' && skills.length > 0) {
|
|
1164
|
+
setView('detail');
|
|
1165
|
+
} else if (activeTab === 'commands' && commands.length > 0) {
|
|
1166
|
+
setView('detail');
|
|
1167
|
+
} else if (activeTab === 'agents' && agents.length > 0) {
|
|
1168
|
+
setView('detail');
|
|
1169
|
+
} else if (activeTab === 'settings') {
|
|
1170
|
+
setIsEditingSettings(true);
|
|
1171
|
+
setView('setup');
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
} else if (view === 'detail') {
|
|
1175
|
+
if (key.escape || key.backspace) {
|
|
1176
|
+
setView('menu');
|
|
1177
|
+
setSelectedAction(0);
|
|
1178
|
+
}
|
|
1179
|
+
if (key.leftArrow) {
|
|
1180
|
+
setSelectedAction((prev) => Math.max(0, prev - 1));
|
|
1181
|
+
}
|
|
1182
|
+
if (key.rightArrow) {
|
|
1183
|
+
let maxActions = 0;
|
|
1184
|
+
if (activeTab === 'skills') {
|
|
1185
|
+
const skill = skills[selectedIndex];
|
|
1186
|
+
const installed = skill ? isSkillInstalled(skill.name) : false;
|
|
1187
|
+
maxActions = installed ? 2 : 1; // View, Configure, Uninstall or View, Install
|
|
1188
|
+
} else if (activeTab === 'agents') {
|
|
1189
|
+
maxActions = 1; // View, Install/Uninstall
|
|
1190
|
+
} else if (activeTab === 'commands') {
|
|
1191
|
+
const command = commands[selectedIndex];
|
|
1192
|
+
// If parent skill is installed, only View is available
|
|
1193
|
+
const skillInstalled = command ? isSkillInstalled(command.skillName) : false;
|
|
1194
|
+
maxActions = skillInstalled ? 0 : 1;
|
|
1195
|
+
}
|
|
1196
|
+
setSelectedAction((prev) => Math.min(maxActions, prev + 1));
|
|
1197
|
+
}
|
|
1198
|
+
if (key.return && activeTab === 'skills') {
|
|
1199
|
+
const skill = skills[selectedIndex];
|
|
1200
|
+
if (skill) {
|
|
1201
|
+
const installed = isSkillInstalled(skill.name);
|
|
1202
|
+
// Actions: installed = [View, Configure, Uninstall], not installed = [View, Install]
|
|
1203
|
+
if (selectedAction === 0) {
|
|
1204
|
+
// View
|
|
1205
|
+
const skillMdPath = join(getBundledSkillsDir(), skill.name, 'SKILL.md');
|
|
1206
|
+
if (existsSync(skillMdPath)) {
|
|
1207
|
+
const content = readFileSync(skillMdPath, 'utf-8');
|
|
1208
|
+
setReadmeContent({ title: `${skill.name}/SKILL.md`, content });
|
|
1209
|
+
setView('readme');
|
|
1210
|
+
}
|
|
1211
|
+
} else if (installed && selectedAction === 1) {
|
|
1212
|
+
// Configure
|
|
1213
|
+
setView('configure');
|
|
1214
|
+
} else if (installed && selectedAction === 2) {
|
|
1215
|
+
// Uninstall
|
|
1216
|
+
const result = uninstallSkill(skill.name);
|
|
1217
|
+
setMessage({
|
|
1218
|
+
text: result.success ? `✓ Uninstalled ${skill.name}` : `✗ ${result.message}`,
|
|
1219
|
+
type: result.success ? 'success' : 'error',
|
|
1220
|
+
});
|
|
1221
|
+
if (result.success) {
|
|
1222
|
+
setView('menu');
|
|
1223
|
+
setSelectedAction(0);
|
|
1224
|
+
}
|
|
1225
|
+
} else if (!installed && selectedAction === 1) {
|
|
1226
|
+
// Install
|
|
1227
|
+
const result = installSkill(skill.name);
|
|
1228
|
+
setMessage({
|
|
1229
|
+
text: result.success ? `✓ Installed ${skill.name}` : `✗ ${result.message}`,
|
|
1230
|
+
type: result.success ? 'success' : 'error',
|
|
1231
|
+
});
|
|
1232
|
+
if (result.success) {
|
|
1233
|
+
setView('menu');
|
|
1234
|
+
setSelectedAction(0);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
if (key.return && activeTab === 'agents') {
|
|
1240
|
+
const agent = agents[selectedIndex];
|
|
1241
|
+
if (agent) {
|
|
1242
|
+
const installed = isAgentInstalled(agent.name);
|
|
1243
|
+
if (selectedAction === 0) {
|
|
1244
|
+
// View
|
|
1245
|
+
const agentMdPath = join(getBundledAgentsDir(), agent.name, 'AGENT.md');
|
|
1246
|
+
if (existsSync(agentMdPath)) {
|
|
1247
|
+
const content = readFileSync(agentMdPath, 'utf-8');
|
|
1248
|
+
setReadmeContent({ title: `${agent.name}/AGENT.md`, content });
|
|
1249
|
+
setView('readme');
|
|
1250
|
+
}
|
|
1251
|
+
} else if (installed && selectedAction === 1) {
|
|
1252
|
+
// Uninstall
|
|
1253
|
+
const result = uninstallAgent(agent.name);
|
|
1254
|
+
setMessage({
|
|
1255
|
+
text: result.success ? `✓ Uninstalled ${agent.name}` : `✗ ${result.message}`,
|
|
1256
|
+
type: result.success ? 'success' : 'error',
|
|
1257
|
+
});
|
|
1258
|
+
if (result.success) {
|
|
1259
|
+
setView('menu');
|
|
1260
|
+
setSelectedAction(0);
|
|
1261
|
+
}
|
|
1262
|
+
} else if (!installed && selectedAction === 1) {
|
|
1263
|
+
// Install
|
|
1264
|
+
const result = installAgent(agent.name);
|
|
1265
|
+
setMessage({
|
|
1266
|
+
text: result.success ? `✓ ${result.message}` : `✗ ${result.message}`,
|
|
1267
|
+
type: result.success ? 'success' : 'error',
|
|
1268
|
+
});
|
|
1269
|
+
if (result.success) {
|
|
1270
|
+
setView('menu');
|
|
1271
|
+
setSelectedAction(0);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
if (key.return && activeTab === 'commands') {
|
|
1277
|
+
const command = commands[selectedIndex];
|
|
1278
|
+
if (command) {
|
|
1279
|
+
const installed = isCommandInstalled(command.name, command.skillName);
|
|
1280
|
+
// Command file: extract part after skill name (e.g., "comments check" → "check.md")
|
|
1281
|
+
const cmdPart = command.name.startsWith(command.skillName + ' ')
|
|
1282
|
+
? command.name.slice(command.skillName.length + 1)
|
|
1283
|
+
: command.name;
|
|
1284
|
+
const commandMdPath = join(getBundledSkillsDir(), command.skillName, 'commands', `${cmdPart}.md`);
|
|
1285
|
+
|
|
1286
|
+
if (selectedAction === 0) {
|
|
1287
|
+
// View
|
|
1288
|
+
if (existsSync(commandMdPath)) {
|
|
1289
|
+
const content = readFileSync(commandMdPath, 'utf-8');
|
|
1290
|
+
setReadmeContent({ title: `/${command.name}`, content });
|
|
1291
|
+
setView('readme');
|
|
1292
|
+
}
|
|
1293
|
+
} else if (installed && selectedAction === 1) {
|
|
1294
|
+
// Uninstall
|
|
1295
|
+
const result = uninstallCommand(command.name, command.skillName);
|
|
1296
|
+
setMessage({
|
|
1297
|
+
text: result.success ? `✓ ${result.message}` : `✗ ${result.message}`,
|
|
1298
|
+
type: result.success ? 'success' : 'error',
|
|
1299
|
+
});
|
|
1300
|
+
if (result.success) {
|
|
1301
|
+
setView('menu');
|
|
1302
|
+
setSelectedAction(0);
|
|
1303
|
+
}
|
|
1304
|
+
} else if (!installed && selectedAction === 1) {
|
|
1305
|
+
// Install
|
|
1306
|
+
const result = installCommand(command.name, command.skillName);
|
|
1307
|
+
setMessage({
|
|
1308
|
+
text: result.success ? `✓ ${result.message}` : `✗ ${result.message}`,
|
|
1309
|
+
type: result.success ? 'success' : 'error',
|
|
1310
|
+
});
|
|
1311
|
+
if (result.success) {
|
|
1312
|
+
setView('menu');
|
|
1313
|
+
setSelectedAction(0);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
const selectedSkill = activeTab === 'skills' ? skills[selectedIndex] ?? null : null;
|
|
1322
|
+
const selectedCommand = activeTab === 'commands' ? commands[selectedIndex] ?? null : null;
|
|
1323
|
+
const selectedAgent = activeTab === 'agents' ? agents[selectedIndex] ?? null : null;
|
|
1324
|
+
|
|
1325
|
+
if (view === 'welcome') {
|
|
1326
|
+
return (
|
|
1327
|
+
<WelcomeScreen
|
|
1328
|
+
onContinue={() => {
|
|
1329
|
+
// If no config exists, show setup first
|
|
1330
|
+
if (!configExists()) {
|
|
1331
|
+
setView('setup');
|
|
1332
|
+
} else {
|
|
1333
|
+
setView('menu');
|
|
1334
|
+
}
|
|
1335
|
+
}}
|
|
1336
|
+
/>
|
|
1337
|
+
);
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
if (view === 'setup') {
|
|
1341
|
+
return (
|
|
1342
|
+
<SetupScreen
|
|
1343
|
+
onComplete={() => {
|
|
1344
|
+
setIsEditingSettings(false);
|
|
1345
|
+
setView('menu');
|
|
1346
|
+
}}
|
|
1347
|
+
onSkip={() => {
|
|
1348
|
+
setIsEditingSettings(false);
|
|
1349
|
+
setView('menu');
|
|
1350
|
+
}}
|
|
1351
|
+
initialConfig={isEditingSettings ? loadConfig() : undefined}
|
|
1352
|
+
/>
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
if (view === 'readme' && readmeContent) {
|
|
1357
|
+
return (
|
|
1358
|
+
<ReadmeViewer
|
|
1359
|
+
title={readmeContent.title}
|
|
1360
|
+
content={readmeContent.content}
|
|
1361
|
+
onClose={() => {
|
|
1362
|
+
setReadmeContent(null);
|
|
1363
|
+
setView('detail');
|
|
1364
|
+
}}
|
|
1365
|
+
/>
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
if (view === 'configure' && selectedSkill) {
|
|
1370
|
+
return (
|
|
1371
|
+
<SkillConfigScreen
|
|
1372
|
+
skill={selectedSkill}
|
|
1373
|
+
onComplete={() => {
|
|
1374
|
+
setMessage({ text: `✓ Configuration saved for ${selectedSkill.name}`, type: 'success' });
|
|
1375
|
+
setView('detail');
|
|
1376
|
+
}}
|
|
1377
|
+
onCancel={() => {
|
|
1378
|
+
setView('detail');
|
|
1379
|
+
}}
|
|
1380
|
+
/>
|
|
1381
|
+
);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
return (
|
|
1385
|
+
<Box flexDirection="row" padding={1}>
|
|
1386
|
+
{/* Left column */}
|
|
1387
|
+
<Box
|
|
1388
|
+
flexDirection="column"
|
|
1389
|
+
width={44}
|
|
1390
|
+
borderStyle="single"
|
|
1391
|
+
borderColor={colors.border}
|
|
1392
|
+
>
|
|
1393
|
+
{/* Header */}
|
|
1394
|
+
<Box paddingX={1}>
|
|
1395
|
+
<Text>
|
|
1396
|
+
<Text color={colors.textDim}>[</Text>
|
|
1397
|
+
<Text color={colors.primary}>●</Text>
|
|
1398
|
+
<Text color={colors.textDim}> </Text>
|
|
1399
|
+
<Text color={colors.primary}>●</Text>
|
|
1400
|
+
<Text color={colors.textDim}>] </Text>
|
|
1401
|
+
<Text color={colors.textMuted}>droid</Text>
|
|
1402
|
+
</Text>
|
|
1403
|
+
</Box>
|
|
1404
|
+
|
|
1405
|
+
{/* Tabs */}
|
|
1406
|
+
<Box paddingX={1} marginTop={1}>
|
|
1407
|
+
<TabBar tabs={tabs} activeTab={activeTab} />
|
|
1408
|
+
</Box>
|
|
1409
|
+
|
|
1410
|
+
{/* List */}
|
|
1411
|
+
<Box flexDirection="column" marginTop={1}>
|
|
1412
|
+
{activeTab === 'skills' && (
|
|
1413
|
+
<>
|
|
1414
|
+
{scrollOffset > 0 && (
|
|
1415
|
+
<Box paddingX={1}>
|
|
1416
|
+
<Text color={colors.textDim}>↑ {scrollOffset} more</Text>
|
|
1417
|
+
</Box>
|
|
1418
|
+
)}
|
|
1419
|
+
{skills.slice(scrollOffset, scrollOffset + MAX_VISIBLE_ITEMS).map((skill, index) => (
|
|
1420
|
+
<SkillItem
|
|
1421
|
+
key={skill.name}
|
|
1422
|
+
skill={skill}
|
|
1423
|
+
isSelected={scrollOffset + index === selectedIndex}
|
|
1424
|
+
isActive={scrollOffset + index === selectedIndex && view === 'detail'}
|
|
1425
|
+
/>
|
|
1426
|
+
))}
|
|
1427
|
+
{scrollOffset + MAX_VISIBLE_ITEMS < skills.length && (
|
|
1428
|
+
<Box paddingX={1}>
|
|
1429
|
+
<Text color={colors.textDim}>↓ {skills.length - scrollOffset - MAX_VISIBLE_ITEMS} more</Text>
|
|
1430
|
+
</Box>
|
|
1431
|
+
)}
|
|
1432
|
+
{skills.length > MAX_VISIBLE_ITEMS && (
|
|
1433
|
+
<Box paddingX={1} marginTop={1}>
|
|
1434
|
+
<Text color={colors.textDim}>{skills.length} skills total</Text>
|
|
1435
|
+
</Box>
|
|
1436
|
+
)}
|
|
1437
|
+
</>
|
|
1438
|
+
)}
|
|
1439
|
+
|
|
1440
|
+
{activeTab === 'commands' && (
|
|
1441
|
+
<>
|
|
1442
|
+
{scrollOffset > 0 && (
|
|
1443
|
+
<Box paddingX={1}>
|
|
1444
|
+
<Text color={colors.textDim}>↑ {scrollOffset} more</Text>
|
|
1445
|
+
</Box>
|
|
1446
|
+
)}
|
|
1447
|
+
{commands.slice(scrollOffset, scrollOffset + MAX_VISIBLE_ITEMS).map((cmd, index) => (
|
|
1448
|
+
<CommandItem key={cmd.name} command={cmd} isSelected={scrollOffset + index === selectedIndex} />
|
|
1449
|
+
))}
|
|
1450
|
+
{scrollOffset + MAX_VISIBLE_ITEMS < commands.length && (
|
|
1451
|
+
<Box paddingX={1}>
|
|
1452
|
+
<Text color={colors.textDim}>↓ {commands.length - scrollOffset - MAX_VISIBLE_ITEMS} more</Text>
|
|
1453
|
+
</Box>
|
|
1454
|
+
)}
|
|
1455
|
+
</>
|
|
1456
|
+
)}
|
|
1457
|
+
|
|
1458
|
+
{activeTab === 'agents' && (
|
|
1459
|
+
<>
|
|
1460
|
+
{scrollOffset > 0 && (
|
|
1461
|
+
<Box paddingX={1}>
|
|
1462
|
+
<Text color={colors.textDim}>↑ {scrollOffset} more</Text>
|
|
1463
|
+
</Box>
|
|
1464
|
+
)}
|
|
1465
|
+
{agents.length === 0 ? (
|
|
1466
|
+
<Box paddingX={1}>
|
|
1467
|
+
<Text color={colors.textDim}>No agents available</Text>
|
|
1468
|
+
</Box>
|
|
1469
|
+
) : (
|
|
1470
|
+
agents.slice(scrollOffset, scrollOffset + MAX_VISIBLE_ITEMS).map((agent, index) => (
|
|
1471
|
+
<AgentItem key={agent.name} agent={agent} isSelected={scrollOffset + index === selectedIndex} />
|
|
1472
|
+
))
|
|
1473
|
+
)}
|
|
1474
|
+
{scrollOffset + MAX_VISIBLE_ITEMS < agents.length && (
|
|
1475
|
+
<Box paddingX={1}>
|
|
1476
|
+
<Text color={colors.textDim}>↓ {agents.length - scrollOffset - MAX_VISIBLE_ITEMS} more</Text>
|
|
1477
|
+
</Box>
|
|
1478
|
+
)}
|
|
1479
|
+
</>
|
|
1480
|
+
)}
|
|
1481
|
+
|
|
1482
|
+
{activeTab === 'settings' && (
|
|
1483
|
+
<Box paddingX={1}>
|
|
1484
|
+
<Text color={colors.textDim}>View and edit config</Text>
|
|
1485
|
+
</Box>
|
|
1486
|
+
)}
|
|
1487
|
+
</Box>
|
|
1488
|
+
|
|
1489
|
+
{/* Footer */}
|
|
1490
|
+
<Box paddingX={1} marginTop={1}>
|
|
1491
|
+
<Text color={colors.textDim}>
|
|
1492
|
+
{view === 'menu' ? '←→ ↑↓ enter q' : '←→ enter esc q'}
|
|
1493
|
+
</Text>
|
|
1494
|
+
</Box>
|
|
1495
|
+
</Box>
|
|
1496
|
+
|
|
1497
|
+
{/* Right column */}
|
|
1498
|
+
{activeTab === 'skills' && (
|
|
1499
|
+
<SkillDetails skill={selectedSkill} isFocused={view === 'detail'} selectedAction={selectedAction} />
|
|
1500
|
+
)}
|
|
1501
|
+
|
|
1502
|
+
{activeTab === 'commands' && (
|
|
1503
|
+
<CommandDetails command={selectedCommand} isFocused={view === 'detail'} selectedAction={selectedAction} />
|
|
1504
|
+
)}
|
|
1505
|
+
|
|
1506
|
+
{activeTab === 'settings' && (
|
|
1507
|
+
<SettingsDetails onEditSettings={() => setView('setup')} isFocused={false} />
|
|
1508
|
+
)}
|
|
1509
|
+
|
|
1510
|
+
{activeTab === 'agents' && (
|
|
1511
|
+
<AgentDetails agent={selectedAgent} isFocused={view === 'detail'} selectedAction={selectedAction} />
|
|
1512
|
+
)}
|
|
1513
|
+
|
|
1514
|
+
{/* Message */}
|
|
1515
|
+
{message && (
|
|
1516
|
+
<Box position="absolute" marginTop={12}>
|
|
1517
|
+
<Text color={message.type === 'success' ? colors.success : colors.error}>{message.text}</Text>
|
|
1518
|
+
</Box>
|
|
1519
|
+
)}
|
|
1520
|
+
</Box>
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
export async function tuiCommand(): Promise<void> {
|
|
1525
|
+
// Enter alternate screen (fullscreen mode)
|
|
1526
|
+
process.stdout.write('\x1b[?1049h');
|
|
1527
|
+
process.stdout.write('\x1b[H'); // Move cursor to top-left
|
|
1528
|
+
|
|
1529
|
+
const { unmount, waitUntilExit } = render(<App />);
|
|
1530
|
+
|
|
1531
|
+
await waitUntilExit();
|
|
1532
|
+
|
|
1533
|
+
// Leave alternate screen
|
|
1534
|
+
process.stdout.write('\x1b[?1049l');
|
|
1535
|
+
}
|