@orderful/droid 0.15.0 → 0.16.1

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 (125) hide show
  1. package/.claude/CLAUDE.md +18 -40
  2. package/AGENTS.md +75 -0
  3. package/CHANGELOG.md +25 -0
  4. package/dist/bin/droid.js +3064 -54
  5. package/dist/commands/setup.d.ts +1 -1
  6. package/dist/commands/setup.d.ts.map +1 -1
  7. package/dist/commands/tui/components/Badge.d.ts +13 -0
  8. package/dist/commands/tui/components/Badge.d.ts.map +1 -0
  9. package/dist/commands/tui/components/Markdown.d.ts +5 -0
  10. package/dist/commands/tui/components/Markdown.d.ts.map +1 -0
  11. package/dist/commands/tui/components/SettingsDetails.d.ts +5 -0
  12. package/dist/commands/tui/components/SettingsDetails.d.ts.map +1 -0
  13. package/dist/commands/tui/components/TabBar.d.ts +10 -0
  14. package/dist/commands/tui/components/TabBar.d.ts.map +1 -0
  15. package/dist/commands/tui/components/ToolDetails.d.ts +8 -0
  16. package/dist/commands/tui/components/ToolDetails.d.ts.map +1 -0
  17. package/dist/commands/tui/components/ToolItem.d.ts +9 -0
  18. package/dist/commands/tui/components/ToolItem.d.ts.map +1 -0
  19. package/dist/commands/tui/constants.d.ts +16 -0
  20. package/dist/commands/tui/constants.d.ts.map +1 -0
  21. package/dist/commands/tui/hooks/useAppUpdate.d.ts +13 -0
  22. package/dist/commands/tui/hooks/useAppUpdate.d.ts.map +1 -0
  23. package/dist/commands/tui/hooks/useToolUpdates.d.ts +22 -0
  24. package/dist/commands/tui/hooks/useToolUpdates.d.ts.map +1 -0
  25. package/dist/commands/tui/types.d.ts +5 -0
  26. package/dist/commands/tui/types.d.ts.map +1 -0
  27. package/dist/commands/tui/views/ReadmeViewer.d.ts +7 -0
  28. package/dist/commands/tui/views/ReadmeViewer.d.ts.map +1 -0
  29. package/dist/commands/tui/views/SetupScreen.d.ts +8 -0
  30. package/dist/commands/tui/views/SetupScreen.d.ts.map +1 -0
  31. package/dist/commands/tui/views/SkillConfigScreen.d.ts +8 -0
  32. package/dist/commands/tui/views/SkillConfigScreen.d.ts.map +1 -0
  33. package/dist/commands/tui/views/ToolExplorer.d.ts +8 -0
  34. package/dist/commands/tui/views/ToolExplorer.d.ts.map +1 -0
  35. package/dist/commands/tui/views/ToolUpdatePrompt.d.ts +10 -0
  36. package/dist/commands/tui/views/ToolUpdatePrompt.d.ts.map +1 -0
  37. package/dist/commands/tui/views/WelcomeScreen.d.ts +11 -0
  38. package/dist/commands/tui/views/WelcomeScreen.d.ts.map +1 -0
  39. package/dist/commands/tui.d.ts.map +1 -1
  40. package/dist/index.d.ts +4 -4
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +952 -6
  43. package/dist/lib/agents.d.ts +1 -1
  44. package/dist/lib/agents.d.ts.map +1 -1
  45. package/dist/lib/config.d.ts +1 -1
  46. package/dist/lib/config.d.ts.map +1 -1
  47. package/dist/lib/platforms.d.ts +1 -1
  48. package/dist/lib/platforms.d.ts.map +1 -1
  49. package/dist/lib/skill-config.d.ts +1 -1
  50. package/dist/lib/skill-config.d.ts.map +1 -1
  51. package/dist/lib/skills.d.ts +1 -1
  52. package/dist/lib/skills.d.ts.map +1 -1
  53. package/dist/lib/tools.d.ts +1 -1
  54. package/dist/lib/tools.d.ts.map +1 -1
  55. package/package.json +3 -3
  56. package/scripts/build.ts +78 -0
  57. package/src/bin/droid.ts +8 -8
  58. package/src/commands/config.ts +1 -1
  59. package/src/commands/install.ts +3 -3
  60. package/src/commands/setup.test.ts +1 -1
  61. package/src/commands/setup.ts +3 -3
  62. package/src/commands/skills.ts +4 -4
  63. package/src/commands/tui/components/Badge.tsx +86 -0
  64. package/src/commands/tui/components/Markdown.tsx +48 -0
  65. package/src/commands/tui/components/SettingsDetails.tsx +70 -0
  66. package/src/commands/tui/components/TabBar.tsx +26 -0
  67. package/src/commands/tui/components/ToolDetails.tsx +117 -0
  68. package/src/commands/tui/components/ToolItem.tsx +39 -0
  69. package/src/commands/tui/constants.ts +17 -0
  70. package/src/commands/tui/hooks/useAppUpdate.ts +67 -0
  71. package/src/commands/tui/hooks/useToolUpdates.ts +110 -0
  72. package/src/commands/tui/types.ts +4 -0
  73. package/src/commands/tui/views/ReadmeViewer.tsx +93 -0
  74. package/src/commands/tui/views/SetupScreen.tsx +242 -0
  75. package/src/commands/tui/views/SkillConfigScreen.tsx +278 -0
  76. package/src/commands/tui/views/ToolExplorer.tsx +190 -0
  77. package/src/commands/tui/views/ToolUpdatePrompt.tsx +109 -0
  78. package/src/commands/tui/views/WelcomeScreen.tsx +149 -0
  79. package/src/commands/tui.tsx +65 -1587
  80. package/src/commands/uninstall.ts +2 -2
  81. package/src/commands/update.ts +1 -1
  82. package/src/index.ts +4 -4
  83. package/src/lib/agents.ts +4 -4
  84. package/src/lib/config.ts +1 -1
  85. package/src/lib/platforms.ts +1 -1
  86. package/src/lib/skill-config.ts +2 -2
  87. package/src/lib/skills.test.ts +2 -2
  88. package/src/lib/skills.ts +5 -5
  89. package/src/lib/tools.ts +3 -3
  90. package/src/lib/types.test.ts +1 -1
  91. package/src/lib/version.test.ts +1 -1
  92. package/dist/bin/droid.js.map +0 -1
  93. package/dist/commands/config.js +0 -67
  94. package/dist/commands/config.js.map +0 -1
  95. package/dist/commands/install.js +0 -45
  96. package/dist/commands/install.js.map +0 -1
  97. package/dist/commands/setup.js +0 -269
  98. package/dist/commands/setup.js.map +0 -1
  99. package/dist/commands/skills.js +0 -144
  100. package/dist/commands/skills.js.map +0 -1
  101. package/dist/commands/tui.js +0 -1008
  102. package/dist/commands/tui.js.map +0 -1
  103. package/dist/commands/uninstall.js +0 -26
  104. package/dist/commands/uninstall.js.map +0 -1
  105. package/dist/commands/update.js +0 -45
  106. package/dist/commands/update.js.map +0 -1
  107. package/dist/index.js.map +0 -1
  108. package/dist/lib/agents.js +0 -248
  109. package/dist/lib/agents.js.map +0 -1
  110. package/dist/lib/config.js +0 -196
  111. package/dist/lib/config.js.map +0 -1
  112. package/dist/lib/platforms.js +0 -52
  113. package/dist/lib/platforms.js.map +0 -1
  114. package/dist/lib/quotes.js +0 -24
  115. package/dist/lib/quotes.js.map +0 -1
  116. package/dist/lib/skill-config.js +0 -80
  117. package/dist/lib/skill-config.js.map +0 -1
  118. package/dist/lib/skills.js +0 -582
  119. package/dist/lib/skills.js.map +0 -1
  120. package/dist/lib/tools.js +0 -145
  121. package/dist/lib/tools.js.map +0 -1
  122. package/dist/lib/types.js +0 -50
  123. package/dist/lib/types.js.map +0 -1
  124. package/dist/lib/version.js +0 -100
  125. package/dist/lib/version.js.map +0 -1
@@ -0,0 +1,39 @@
1
+ import { Box, Text } from 'ink';
2
+ import { isToolInstalled, getInstalledToolVersion, getToolUpdateStatus } from '../../../lib/tools';
3
+ import type { ToolManifest } from '../../../lib/types';
4
+ import { colors } from '../constants';
5
+
6
+ export interface ToolItemProps {
7
+ tool: ToolManifest;
8
+ isSelected: boolean;
9
+ isActive: boolean;
10
+ wasAutoUpdated?: boolean;
11
+ }
12
+
13
+ export function ToolItem({
14
+ tool,
15
+ isSelected,
16
+ isActive,
17
+ wasAutoUpdated,
18
+ }: ToolItemProps) {
19
+ const installed = isToolInstalled(tool.name);
20
+ const installedVersion = getInstalledToolVersion(tool.name);
21
+ const updateStatus = getToolUpdateStatus(tool.name);
22
+
23
+ return (
24
+ <Box paddingX={1} backgroundColor={isActive ? colors.bgSelected : undefined}>
25
+ <Text wrap="truncate">
26
+ <Text color={colors.textDim}>{isSelected ? '>' : ' '} </Text>
27
+ <Text color={isSelected || isActive ? colors.text : colors.textMuted}>{tool.name}</Text>
28
+ {installed && installedVersion && <Text color={colors.textDim}> v{installedVersion}</Text>}
29
+ {installed && <Text color={colors.success}> ✓</Text>}
30
+ {wasAutoUpdated && <Text color={colors.success}> ↑</Text>}
31
+ {updateStatus.hasUpdate && !wasAutoUpdated && <Text color={colors.primary}> ↑</Text>}
32
+ <Text> </Text>
33
+ {tool.includes.skills.length > 0 && <Text color={colors.skill}>● </Text>}
34
+ {tool.includes.commands.length > 0 && <Text color={colors.command}>● </Text>}
35
+ {tool.includes.agents.length > 0 && <Text color={colors.agent}>●</Text>}
36
+ </Text>
37
+ </Box>
38
+ );
39
+ }
@@ -0,0 +1,17 @@
1
+ export const colors = {
2
+ primary: '#6366f1',
3
+ bgSelected: '#2d2d2d',
4
+ border: '#3a3a3a',
5
+ text: '#e8e8e8',
6
+ textMuted: '#999999',
7
+ textDim: '#6a6a6a',
8
+ success: '#4ade80',
9
+ error: '#f87171',
10
+ // Component type badges
11
+ skill: '#ec4899', // pink/magenta
12
+ command: '#22d3ee', // cyan
13
+ agent: '#fbbf24', // yellow/amber
14
+ } as const;
15
+
16
+ export const MAX_VISIBLE_ITEMS = 6;
17
+ export const MAX_VISIBLE_CONFIG_ITEMS = 4; // Each config item takes ~3 lines
@@ -0,0 +1,67 @@
1
+ import { useState, useMemo } from 'react';
2
+ import { useApp } from 'ink';
3
+ import { getUpdateInfo, runUpdate, type UpdateInfo } from '../../../lib/version';
4
+ import { setAutoUpdateConfig } from '../../../lib/config';
5
+
6
+ export interface UseAppUpdateOptions {
7
+ onUpdateSuccess: (message: string) => void;
8
+ onUpdateFailure: (error: string) => void;
9
+ }
10
+
11
+ export interface UseAppUpdateResult {
12
+ updateInfo: UpdateInfo;
13
+ isUpdating: boolean;
14
+ handleUpdate: () => void;
15
+ handleAlwaysUpdate: () => void;
16
+ }
17
+
18
+ export function useAppUpdate({ onUpdateSuccess, onUpdateFailure }: UseAppUpdateOptions): UseAppUpdateResult {
19
+ const { exit } = useApp();
20
+ const [isUpdating, setIsUpdating] = useState(false);
21
+
22
+ const updateInfo = useMemo(() => getUpdateInfo(), []);
23
+
24
+ const handleUpdate = () => {
25
+ setIsUpdating(true);
26
+ // Run update in next tick to allow UI to show "Updating..."
27
+ setTimeout(() => {
28
+ const result = runUpdate();
29
+ if (result.success) {
30
+ // Build exit message with ANSI colors
31
+ const blue = '\x1b[38;2;99;102;241m'; // #6366f1
32
+ const dim = '\x1b[38;2;106;106;106m';
33
+ const reset = '\x1b[0m';
34
+ const message = `
35
+ ${dim}────────────────────────────────────────────────────────${reset}
36
+
37
+ ${dim}╔═════╗${reset}
38
+ ${dim}║${reset} ${blue}●${reset} ${blue}●${reset} ${dim}║${reset} ${blue}"It's quite possible this system${reset}
39
+ ${dim}╚═╦═╦═╝${reset} ${blue}is now fully operational."${reset}
40
+
41
+ Run ${blue}droid${reset} to start the new version.
42
+
43
+ ${dim}────────────────────────────────────────────────────────${reset}
44
+ `;
45
+ onUpdateSuccess(message);
46
+ exit();
47
+ } else {
48
+ setIsUpdating(false);
49
+ onUpdateFailure(result.message);
50
+ }
51
+ }, 100);
52
+ };
53
+
54
+ const handleAlwaysUpdate = () => {
55
+ // Enable auto-update for app in config
56
+ setAutoUpdateConfig({ app: true });
57
+ // Then run the update
58
+ handleUpdate();
59
+ };
60
+
61
+ return {
62
+ updateInfo,
63
+ isUpdating,
64
+ handleUpdate,
65
+ handleAlwaysUpdate,
66
+ };
67
+ }
@@ -0,0 +1,110 @@
1
+ import { useState, useCallback } from 'react';
2
+ import {
3
+ getBundledTools,
4
+ isToolInstalled,
5
+ getToolUpdateStatus,
6
+ getToolsWithUpdates,
7
+ type ToolUpdateInfo,
8
+ } from '../../../lib/tools';
9
+ import { installSkill, updateSkill } from '../../../lib/skills';
10
+ import { getAutoUpdateConfig, setAutoUpdateConfig } from '../../../lib/config';
11
+ import type { ToolManifest } from '../../../lib/types';
12
+
13
+ export interface UseToolUpdatesOptions {
14
+ onUpdateComplete: (result: { successCount: number; failCount: number; updatedNames: string[]; silent: boolean }) => void;
15
+ }
16
+
17
+ export interface UseToolUpdatesResult {
18
+ toolUpdates: ToolUpdateInfo[];
19
+ isUpdatingTools: boolean;
20
+ autoUpdatedTools: string[];
21
+ checkForUpdates: () => { updates: ToolUpdateInfo[]; shouldAutoUpdate: boolean };
22
+ updateAllTools: (updates?: ToolUpdateInfo[], silent?: boolean) => void;
23
+ enableAutoUpdateAndUpdate: () => void;
24
+ }
25
+
26
+ export function useToolUpdates({ onUpdateComplete }: UseToolUpdatesOptions): UseToolUpdatesResult {
27
+ const [toolUpdates, setToolUpdates] = useState<ToolUpdateInfo[]>([]);
28
+ const [isUpdatingTools, setIsUpdatingTools] = useState(false);
29
+ const [autoUpdatedTools, setAutoUpdatedTools] = useState<string[]>([]);
30
+
31
+ const tools = getBundledTools();
32
+
33
+ // Ensure system tools are installed/updated (bypasses auto-update settings)
34
+ const ensureSystemTools = useCallback(() => {
35
+ const systemTools = tools.filter(t => (t as ToolManifest & { system?: boolean }).system === true);
36
+
37
+ for (const systemTool of systemTools) {
38
+ const installed = isToolInstalled(systemTool.name);
39
+ const updateStatus = getToolUpdateStatus(systemTool.name);
40
+
41
+ if (!installed || updateStatus.hasUpdate) {
42
+ const primarySkill = systemTool.includes.skills.find(s => s.required)?.name || systemTool.name;
43
+ installSkill(primarySkill);
44
+ }
45
+ }
46
+ }, [tools]);
47
+
48
+ // Check for updates, returns info for caller to decide what to do
49
+ const checkForUpdates = useCallback(() => {
50
+ ensureSystemTools();
51
+ const updates = getToolsWithUpdates();
52
+ setToolUpdates(updates);
53
+
54
+ const autoUpdateConfig = getAutoUpdateConfig();
55
+ return {
56
+ updates,
57
+ shouldAutoUpdate: autoUpdateConfig.tools && updates.length > 0,
58
+ };
59
+ }, [ensureSystemTools]);
60
+
61
+ // Update all tools
62
+ const updateAllTools = useCallback((updates: ToolUpdateInfo[] = toolUpdates, silent = false) => {
63
+ if (!silent) {
64
+ setIsUpdatingTools(true);
65
+ }
66
+
67
+ setTimeout(() => {
68
+ let successCount = 0;
69
+ let failCount = 0;
70
+ const updatedNames: string[] = [];
71
+
72
+ for (const tool of updates) {
73
+ const toolManifest = tools.find(t => t.name === tool.name);
74
+ if (toolManifest) {
75
+ const primarySkill = toolManifest.includes.skills.find(s => s.required)?.name || toolManifest.name;
76
+ const result = updateSkill(primarySkill);
77
+ if (result.success) {
78
+ successCount++;
79
+ updatedNames.push(tool.name);
80
+ } else {
81
+ failCount++;
82
+ }
83
+ }
84
+ }
85
+
86
+ setIsUpdatingTools(false);
87
+
88
+ if (silent && updatedNames.length > 0) {
89
+ setAutoUpdatedTools(updatedNames);
90
+ }
91
+
92
+ onUpdateComplete({ successCount, failCount, updatedNames, silent });
93
+ }, 100);
94
+ }, [toolUpdates, tools, onUpdateComplete]);
95
+
96
+ // Enable auto-update and run updates
97
+ const enableAutoUpdateAndUpdate = useCallback(() => {
98
+ setAutoUpdateConfig({ tools: true });
99
+ updateAllTools();
100
+ }, [updateAllTools]);
101
+
102
+ return {
103
+ toolUpdates,
104
+ isUpdatingTools,
105
+ autoUpdatedTools,
106
+ checkForUpdates,
107
+ updateAllTools,
108
+ enableAutoUpdateAndUpdate,
109
+ };
110
+ }
@@ -0,0 +1,4 @@
1
+ export type Tab = 'tools' | 'settings';
2
+ export type View = 'welcome' | 'tool-updates' | 'setup' | 'menu' | 'detail' | 'configure' | 'readme' | 'explorer';
3
+ export type SetupStep = 'platform' | 'user_mention' | 'auto_update' | 'confirm';
4
+ export type ComponentType = 'skill' | 'command' | 'agent';
@@ -0,0 +1,93 @@
1
+ import { Box, Text, useInput } from 'ink';
2
+ import { useState, useMemo } from 'react';
3
+ import { colors } from '../constants';
4
+ import { MarkdownLine } from '../components/Markdown';
5
+
6
+ export interface ReadmeViewerProps {
7
+ title: string;
8
+ content: string;
9
+ onClose: () => void;
10
+ }
11
+
12
+ export function ReadmeViewer({ title, content, onClose }: ReadmeViewerProps) {
13
+ const [scrollOffset, setScrollOffset] = useState(0);
14
+ const lines = useMemo(() => content.split('\n'), [content]);
15
+ const maxVisible = 20;
16
+
17
+ // Pre-compute code block state for each line
18
+ const lineStates = useMemo(() => {
19
+ const states: boolean[] = [];
20
+ let inCode = false;
21
+ for (const line of lines) {
22
+ if (line.startsWith('```')) {
23
+ states.push(false); // Delimiter itself is not "in" code block for styling
24
+ inCode = !inCode;
25
+ } else {
26
+ states.push(inCode);
27
+ }
28
+ }
29
+ return states;
30
+ }, [lines]);
31
+
32
+ // Max offset: when at end, we have top indicator + (maxVisible-1) content lines
33
+ // So max offset is lines.length - (maxVisible - 1) = lines.length - maxVisible + 1
34
+ const maxOffset = Math.max(0, lines.length - maxVisible + 1);
35
+
36
+ useInput((input, key) => {
37
+ if (key.escape) {
38
+ onClose();
39
+ return;
40
+ }
41
+ if (key.upArrow) {
42
+ setScrollOffset((prev) => Math.max(0, prev - 1));
43
+ }
44
+ if (key.downArrow) {
45
+ setScrollOffset((prev) => Math.min(maxOffset, prev + 1));
46
+ }
47
+ if (key.pageDown || input === ' ') {
48
+ setScrollOffset((prev) => Math.min(maxOffset, prev + maxVisible));
49
+ }
50
+ if (key.pageUp) {
51
+ setScrollOffset((prev) => Math.max(0, prev - maxVisible));
52
+ }
53
+ });
54
+
55
+ // Adjust visible lines based on whether indicators are shown
56
+ const showTopIndicator = scrollOffset > 0;
57
+ // Reserve space for bottom indicator if not at end
58
+ const contentLines = maxVisible - (showTopIndicator ? 1 : 0);
59
+ const endIndex = Math.min(scrollOffset + contentLines, lines.length);
60
+ const showBottomIndicator = endIndex < lines.length;
61
+ const actualContentLines = contentLines - (showBottomIndicator ? 1 : 0);
62
+ const visibleLines = lines.slice(scrollOffset, scrollOffset + actualContentLines);
63
+
64
+ return (
65
+ <Box flexDirection="column" padding={1}>
66
+ <Box marginBottom={1}>
67
+ <Text color={colors.text} bold>{title}</Text>
68
+ <Text color={colors.textDim}> · {lines.length} lines</Text>
69
+ </Box>
70
+
71
+ <Box
72
+ flexDirection="column"
73
+ borderStyle="single"
74
+ borderColor={colors.border}
75
+ paddingX={1}
76
+ >
77
+ {showTopIndicator && (
78
+ <Text color={colors.textDim}>↑ {scrollOffset} more lines</Text>
79
+ )}
80
+ {visibleLines.map((line, i) => (
81
+ <MarkdownLine key={scrollOffset + i} line={line} inCodeBlock={lineStates[scrollOffset + i]} />
82
+ ))}
83
+ {showBottomIndicator && (
84
+ <Text color={colors.textDim}>↓ {lines.length - scrollOffset - actualContentLines} more lines</Text>
85
+ )}
86
+ </Box>
87
+
88
+ <Box marginTop={1}>
89
+ <Text color={colors.textDim}>↑↓ scroll · space/pgdn page · esc back</Text>
90
+ </Box>
91
+ </Box>
92
+ );
93
+ }
@@ -0,0 +1,242 @@
1
+ import { Box, Text, useInput } from 'ink';
2
+ import TextInput from 'ink-text-input';
3
+ import { useState } from 'react';
4
+ import { colors } from '../constants';
5
+ import type { SetupStep } from '../types';
6
+ import { Platform, BuiltInOutput, type DroidConfig } from '../../../lib/types';
7
+ import { loadConfig, saveConfig, getAutoUpdateConfig, setAutoUpdateConfig } from '../../../lib/config';
8
+ import { configurePlatformPermissions } from '../../setup';
9
+
10
+ export interface SetupScreenProps {
11
+ onComplete: () => void;
12
+ onSkip: () => void;
13
+ initialConfig?: DroidConfig;
14
+ }
15
+
16
+ const platformOptions = [
17
+ { label: 'Claude Code', value: Platform.ClaudeCode },
18
+ { label: 'OpenCode', value: Platform.OpenCode },
19
+ ];
20
+
21
+ export function SetupScreen({ onComplete, onSkip, initialConfig }: SetupScreenProps) {
22
+ const [step, setStep] = useState<SetupStep>('platform');
23
+ const [platform, setPlatform] = useState<Platform>(
24
+ initialConfig?.platform || Platform.ClaudeCode
25
+ );
26
+ const [userMention, setUserMention] = useState(initialConfig?.user_mention || '@user');
27
+ const [selectedIndex, setSelectedIndex] = useState(0);
28
+ const [error, setError] = useState<string | null>(null);
29
+
30
+ // Auto-update state
31
+ const existingAutoUpdate = getAutoUpdateConfig();
32
+ const [autoUpdateTools, setAutoUpdateTools] = useState(existingAutoUpdate.tools);
33
+ const [autoUpdateApp, setAutoUpdateApp] = useState(existingAutoUpdate.app);
34
+ const [autoUpdateSelectedIndex, setAutoUpdateSelectedIndex] = useState(0);
35
+
36
+ const steps: SetupStep[] = ['platform', 'user_mention', 'auto_update', 'confirm'];
37
+ const stepIndex = steps.indexOf(step);
38
+ const totalSteps = steps.length - 1; // Don't count confirm as a step
39
+
40
+ const handleUserMentionSubmit = () => {
41
+ // Auto-add @ prefix if missing
42
+ const mention = userMention.startsWith('@') ? userMention : `@${userMention}`;
43
+ setUserMention(mention);
44
+ setError(null);
45
+ setStep('auto_update');
46
+ setAutoUpdateSelectedIndex(0);
47
+ };
48
+
49
+ // Handle escape during text input (only intercept escape, nothing else)
50
+ useInput((input, key) => {
51
+ if (key.escape) {
52
+ setStep('platform');
53
+ setSelectedIndex(0);
54
+ }
55
+ }, { isActive: step === 'user_mention' });
56
+
57
+ // Handle all input for non-text-input steps
58
+ useInput((input, key) => {
59
+ if (key.escape) {
60
+ if (step === 'platform') {
61
+ onSkip();
62
+ } else if (step === 'auto_update') {
63
+ setStep('user_mention');
64
+ } else if (step === 'confirm') {
65
+ setStep('auto_update');
66
+ setAutoUpdateSelectedIndex(0);
67
+ }
68
+ return;
69
+ }
70
+
71
+ if (step === 'platform') {
72
+ if (key.upArrow) setSelectedIndex((prev) => Math.max(0, prev - 1));
73
+ if (key.downArrow) setSelectedIndex((prev) => Math.min(platformOptions.length - 1, prev + 1));
74
+ if (key.return) {
75
+ setPlatform(platformOptions[selectedIndex].value);
76
+ setStep('user_mention');
77
+ }
78
+ } else if (step === 'auto_update') {
79
+ // 0 = auto-update tools, 1 = auto-update app, 2 = next button
80
+ if (key.upArrow) setAutoUpdateSelectedIndex((prev) => Math.max(0, prev - 1));
81
+ if (key.downArrow) setAutoUpdateSelectedIndex((prev) => Math.min(2, prev + 1));
82
+ if (key.return) {
83
+ if (autoUpdateSelectedIndex === 0) {
84
+ setAutoUpdateTools(!autoUpdateTools);
85
+ } else if (autoUpdateSelectedIndex === 1) {
86
+ setAutoUpdateApp(!autoUpdateApp);
87
+ } else {
88
+ setStep('confirm');
89
+ }
90
+ }
91
+ } else if (step === 'confirm') {
92
+ if (key.return) {
93
+ const existingConfig = loadConfig();
94
+ const config: DroidConfig = {
95
+ ...existingConfig,
96
+ platform: platform,
97
+ user_mention: userMention,
98
+ output_preference: BuiltInOutput.Terminal, // Default to terminal
99
+ };
100
+ saveConfig(config);
101
+ setAutoUpdateConfig({ tools: autoUpdateTools, app: autoUpdateApp });
102
+ configurePlatformPermissions(platform);
103
+ onComplete();
104
+ }
105
+ }
106
+ }, { isActive: step !== 'user_mention' });
107
+
108
+ const renderHeader = () => (
109
+ <Box flexDirection="column" marginBottom={1}>
110
+ <Text>
111
+ <Text color={colors.textDim}>[</Text>
112
+ <Text color={colors.primary}>●</Text>
113
+ <Text color={colors.textDim}> </Text>
114
+ <Text color={colors.primary}>●</Text>
115
+ <Text color={colors.textDim}>] </Text>
116
+ <Text color={colors.text} bold>droid setup</Text>
117
+ <Text color={colors.textDim}> · Step {Math.min(stepIndex + 1, totalSteps)} of {totalSteps}</Text>
118
+ </Text>
119
+ </Box>
120
+ );
121
+
122
+ if (step === 'platform') {
123
+ return (
124
+ <Box flexDirection="column" padding={1}>
125
+ {renderHeader()}
126
+ <Text color={colors.text}>Which platform are you using?</Text>
127
+ <Box flexDirection="column" marginTop={1}>
128
+ {platformOptions.map((option, index) => (
129
+ <Text key={option.value}>
130
+ <Text color={colors.textDim}>{index === selectedIndex ? '>' : ' '} </Text>
131
+ <Text color={index === selectedIndex ? colors.text : colors.textMuted}>{option.label}</Text>
132
+ </Text>
133
+ ))}
134
+ </Box>
135
+ <Box marginTop={1}>
136
+ <Text color={colors.textDim}>↑↓ select · enter next · esc skip</Text>
137
+ </Box>
138
+ </Box>
139
+ );
140
+ }
141
+
142
+ if (step === 'user_mention') {
143
+ return (
144
+ <Box flexDirection="column" padding={1}>
145
+ {renderHeader()}
146
+ <Text color={colors.text}>What @mention should be used for you?</Text>
147
+ <Box marginTop={1}>
148
+ <Text color={colors.textDim}>{'> '}</Text>
149
+ <TextInput
150
+ value={userMention}
151
+ onChange={setUserMention}
152
+ onSubmit={handleUserMentionSubmit}
153
+ placeholder="@user"
154
+ />
155
+ </Box>
156
+ {error && (
157
+ <Box marginTop={1}>
158
+ <Text color={colors.error}>{error}</Text>
159
+ </Box>
160
+ )}
161
+ <Box marginTop={1}>
162
+ <Text color={colors.textDim}>enter next · esc back</Text>
163
+ </Box>
164
+ </Box>
165
+ );
166
+ }
167
+
168
+ if (step === 'auto_update') {
169
+ return (
170
+ <Box flexDirection="column" padding={1}>
171
+ {renderHeader()}
172
+ <Text color={colors.text}>Configure auto-update behaviour</Text>
173
+ <Box flexDirection="column" marginTop={1}>
174
+ <Box>
175
+ <Text>
176
+ <Text color={colors.textDim}>{autoUpdateSelectedIndex === 0 ? '>' : ' '} </Text>
177
+ <Text color={autoUpdateSelectedIndex === 0 ? colors.text : colors.textMuted}>
178
+ [{autoUpdateTools ? 'x' : ' '}] Auto-update tools
179
+ </Text>
180
+ </Text>
181
+ </Box>
182
+ <Text color={colors.textDim}> Update tools automatically when droid starts</Text>
183
+ <Box marginTop={1}>
184
+ <Text>
185
+ <Text color={colors.textDim}>{autoUpdateSelectedIndex === 1 ? '>' : ' '} </Text>
186
+ <Text color={autoUpdateSelectedIndex === 1 ? colors.text : colors.textMuted}>
187
+ [{autoUpdateApp ? 'x' : ' '}] Auto-update app
188
+ </Text>
189
+ </Text>
190
+ </Box>
191
+ <Text color={colors.textDim}> Update droid automatically when a new version is available</Text>
192
+ </Box>
193
+ <Box marginTop={2}>
194
+ <Text
195
+ backgroundColor={autoUpdateSelectedIndex === 2 ? colors.primary : colors.bgSelected}
196
+ color={autoUpdateSelectedIndex === 2 ? '#ffffff' : colors.textMuted}
197
+ bold={autoUpdateSelectedIndex === 2}
198
+ >
199
+ {' '}Next{' '}
200
+ </Text>
201
+ </Box>
202
+ <Box marginTop={1}>
203
+ <Text color={colors.textDim}>↑↓ select · enter toggle/next · esc back</Text>
204
+ </Box>
205
+ </Box>
206
+ );
207
+ }
208
+
209
+ // Confirm step
210
+ return (
211
+ <Box flexDirection="column" padding={1}>
212
+ {renderHeader()}
213
+ <Text color={colors.text} bold>Review your settings</Text>
214
+ <Box flexDirection="column" marginTop={1}>
215
+ <Text>
216
+ <Text color={colors.textDim}>Platform: </Text>
217
+ <Text color={colors.text}>{platform === Platform.ClaudeCode ? 'Claude Code' : 'OpenCode'}</Text>
218
+ </Text>
219
+ <Text>
220
+ <Text color={colors.textDim}>Your @mention: </Text>
221
+ <Text color={colors.text}>{userMention}</Text>
222
+ </Text>
223
+ <Text>
224
+ <Text color={colors.textDim}>Auto-update tools: </Text>
225
+ <Text color={colors.text}>{autoUpdateTools ? 'enabled' : 'disabled'}</Text>
226
+ </Text>
227
+ <Text>
228
+ <Text color={colors.textDim}>Auto-update app: </Text>
229
+ <Text color={colors.text}>{autoUpdateApp ? 'enabled' : 'disabled'}</Text>
230
+ </Text>
231
+ </Box>
232
+ <Box marginTop={2}>
233
+ <Text backgroundColor={colors.primary} color="#ffffff" bold>
234
+ {' '}Save{' '}
235
+ </Text>
236
+ </Box>
237
+ <Box marginTop={1}>
238
+ <Text color={colors.textDim}>enter save · esc back</Text>
239
+ </Box>
240
+ </Box>
241
+ );
242
+ }