@orderful/droid 0.11.1 → 0.12.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.
@@ -12,15 +12,18 @@ import {
12
12
  updateSkill,
13
13
  getSkillUpdateStatus,
14
14
  } from '../lib/skills.js';
15
- import { getBundledTools, getBundledToolsDir, isToolInstalled, getToolUpdateStatus, getInstalledToolVersion } from '../lib/tools.js';
16
- import { configExists, loadConfig, saveConfig, loadSkillOverrides, saveSkillOverrides } from '../lib/config.js';
15
+ import { getBundledTools, getBundledToolsDir, isToolInstalled, getToolUpdateStatus, getInstalledToolVersion, getToolsWithUpdates, type ToolUpdateInfo } from '../lib/tools.js';
16
+ import { configExists, loadConfig, saveConfig, loadSkillOverrides, saveSkillOverrides, getAutoUpdateConfig, setAutoUpdateConfig } from '../lib/config.js';
17
17
  import { configurePlatformPermissions } from './setup.js';
18
18
  import { Platform, BuiltInOutput, ConfigOptionType, type DroidConfig, type OutputPreference, type SkillManifest, type ConfigOption, type SkillOverrides, type ToolManifest } from '../lib/types.js';
19
19
  import { getVersion, getUpdateInfo, runUpdate, type UpdateInfo } from '../lib/version.js';
20
20
  import { getRandomQuote } from '../lib/quotes.js';
21
21
 
22
22
  type Tab = 'tools' | 'settings';
23
- type View = 'welcome' | 'setup' | 'menu' | 'detail' | 'configure' | 'readme' | 'explorer';
23
+ type View = 'welcome' | 'tool-updates' | 'setup' | 'menu' | 'detail' | 'configure' | 'readme' | 'explorer';
24
+
25
+ // Module-level variable to store exit message (printed after leaving alternate screen)
26
+ let exitMessage: string | null = null;
24
27
  type SetupStep = 'platform' | 'user_mention' | 'confirm';
25
28
  type ComponentType = 'skill' | 'command' | 'agent';
26
29
 
@@ -162,12 +165,13 @@ function getOutputOptions(): Array<{ label: string; value: OutputPreference }> {
162
165
  interface WelcomeScreenProps {
163
166
  onContinue: () => void;
164
167
  onUpdate: () => void;
168
+ onAlways: () => void;
165
169
  onExit: () => void;
166
170
  updateInfo: UpdateInfo;
167
171
  isUpdating: boolean;
168
172
  }
169
173
 
170
- function WelcomeScreen({ onContinue, onUpdate, onExit, updateInfo, isUpdating }: WelcomeScreenProps) {
174
+ function WelcomeScreen({ onContinue, onUpdate, onAlways, onExit, updateInfo, isUpdating }: WelcomeScreenProps) {
171
175
  const [selectedButton, setSelectedButton] = useState(0);
172
176
  const welcomeQuote = useMemo(() => getRandomQuote(), []);
173
177
 
@@ -175,12 +179,17 @@ function WelcomeScreen({ onContinue, onUpdate, onExit, updateInfo, isUpdating }:
175
179
  if (isUpdating) return;
176
180
 
177
181
  if (updateInfo.hasUpdate) {
178
- if (key.leftArrow || key.rightArrow) {
179
- setSelectedButton((prev) => (prev === 0 ? 1 : 0));
182
+ if (key.leftArrow) {
183
+ setSelectedButton((prev) => Math.max(0, prev - 1));
184
+ }
185
+ if (key.rightArrow) {
186
+ setSelectedButton((prev) => Math.min(2, prev + 1));
180
187
  }
181
188
  if (key.return) {
182
189
  if (selectedButton === 0) {
183
190
  onUpdate();
191
+ } else if (selectedButton === 1) {
192
+ onAlways();
184
193
  } else {
185
194
  onContinue();
186
195
  }
@@ -260,17 +269,25 @@ function WelcomeScreen({ onContinue, onUpdate, onExit, updateInfo, isUpdating }:
260
269
  </Text>
261
270
  <Text> </Text>
262
271
  <Text
263
- backgroundColor={selectedButton === 1 ? colors.bgSelected : undefined}
264
- color={selectedButton === 1 ? colors.text : colors.textMuted}
272
+ backgroundColor={selectedButton === 1 ? '#eab308' : colors.bgSelected}
273
+ color={selectedButton === 1 ? '#000000' : colors.textMuted}
265
274
  bold={selectedButton === 1}
266
275
  >
267
- {' '}Skip for now{' '}
276
+ {' '}Always{' '}
277
+ </Text>
278
+ <Text> </Text>
279
+ <Text
280
+ backgroundColor={selectedButton === 2 ? colors.bgSelected : undefined}
281
+ color={selectedButton === 2 ? colors.text : colors.textMuted}
282
+ bold={selectedButton === 2}
283
+ >
284
+ {' '}Skip{' '}
268
285
  </Text>
269
286
  </Box>
270
287
  )}
271
288
 
272
289
  <Box marginTop={1}>
273
- <Text color={colors.textDim}>←→ select · enter</Text>
290
+ <Text color={colors.textDim}>←→ select · enter · "Always" enables auto-update</Text>
274
291
  </Box>
275
292
  </>
276
293
  ) : (
@@ -289,6 +306,111 @@ function WelcomeScreen({ onContinue, onUpdate, onExit, updateInfo, isUpdating }:
289
306
  );
290
307
  }
291
308
 
309
+ interface ToolUpdatePromptProps {
310
+ toolUpdates: ToolUpdateInfo[];
311
+ onUpdateAll: () => void;
312
+ onAlways: () => void;
313
+ onSkip: () => void;
314
+ isUpdating: boolean;
315
+ }
316
+
317
+ function ToolUpdatePrompt({ toolUpdates, onUpdateAll, onAlways, onSkip, isUpdating }: ToolUpdatePromptProps) {
318
+ const [selectedButton, setSelectedButton] = useState(0);
319
+
320
+ useInput((input, key) => {
321
+ if (isUpdating) return;
322
+
323
+ if (key.leftArrow) {
324
+ setSelectedButton((prev) => Math.max(0, prev - 1));
325
+ }
326
+ if (key.rightArrow) {
327
+ setSelectedButton((prev) => Math.min(2, prev + 1));
328
+ }
329
+ if (key.return) {
330
+ if (selectedButton === 0) {
331
+ onUpdateAll();
332
+ } else if (selectedButton === 1) {
333
+ onAlways();
334
+ } else {
335
+ onSkip();
336
+ }
337
+ }
338
+ if (input === 'q') {
339
+ onSkip();
340
+ }
341
+ });
342
+
343
+ const buttons = [
344
+ { label: 'Update All', action: onUpdateAll },
345
+ { label: 'Always', action: onAlways },
346
+ { label: 'Skip', action: onSkip },
347
+ ];
348
+
349
+ return (
350
+ <Box flexDirection="column" alignItems="center" justifyContent="center" height={18}>
351
+ <Box
352
+ flexDirection="column"
353
+ alignItems="center"
354
+ borderStyle="single"
355
+ borderColor={colors.primary}
356
+ paddingX={4}
357
+ paddingY={1}
358
+ >
359
+ <Box flexDirection="column">
360
+ <Text>
361
+ <Text color={colors.textDim}>╔═════╗ </Text>
362
+ <Text color={colors.text}>Tool Updates</Text>
363
+ </Text>
364
+ <Text>
365
+ <Text color={colors.textDim}>║ </Text>
366
+ <Text color={colors.primary}>●</Text>
367
+ <Text color={colors.textDim}> </Text>
368
+ <Text color={colors.primary}>●</Text>
369
+ <Text color={colors.textDim}> ║ </Text>
370
+ <Text color={colors.textMuted}>{toolUpdates.length} tool{toolUpdates.length > 1 ? 's have' : ' has'} updates available</Text>
371
+ </Text>
372
+ <Text>
373
+ <Text color={colors.textDim}>╚═╦═╦═╝</Text>
374
+ </Text>
375
+ </Box>
376
+
377
+ <Box marginTop={1} marginBottom={1} flexDirection="column">
378
+ {toolUpdates.slice(0, 5).map((tool) => (
379
+ <Text key={tool.name} color={colors.textMuted}>
380
+ <Text color={colors.primary}>↑</Text> {tool.name}
381
+ <Text color={colors.textDim}> {tool.installedVersion} → {tool.bundledVersion}</Text>
382
+ </Text>
383
+ ))}
384
+ {toolUpdates.length > 5 && (
385
+ <Text color={colors.textDim}>...and {toolUpdates.length - 5} more</Text>
386
+ )}
387
+ </Box>
388
+
389
+ {isUpdating ? (
390
+ <Text color={colors.primary}>Updating tools...</Text>
391
+ ) : (
392
+ <Box flexDirection="row">
393
+ {buttons.map((button, index) => (
394
+ <Text
395
+ key={button.label}
396
+ backgroundColor={selectedButton === index ? colors.primary : colors.bgSelected}
397
+ color={selectedButton === index ? '#000000' : colors.textMuted}
398
+ bold={selectedButton === index}
399
+ >
400
+ {' '}{button.label}{' '}
401
+ </Text>
402
+ ))}
403
+ </Box>
404
+ )}
405
+
406
+ <Box marginTop={1}>
407
+ <Text color={colors.textDim}>←→ select · enter · "Always" enables auto-update</Text>
408
+ </Box>
409
+ </Box>
410
+ </Box>
411
+ );
412
+ }
413
+
292
414
  interface SetupScreenProps {
293
415
  onComplete: () => void;
294
416
  onSkip: () => void;
@@ -475,10 +597,12 @@ function ToolItem({
475
597
  tool,
476
598
  isSelected,
477
599
  isActive,
600
+ wasAutoUpdated,
478
601
  }: {
479
602
  tool: ToolManifest;
480
603
  isSelected: boolean;
481
604
  isActive: boolean;
605
+ wasAutoUpdated?: boolean;
482
606
  }) {
483
607
  const installed = isToolInstalled(tool.name);
484
608
  const installedVersion = getInstalledToolVersion(tool.name);
@@ -491,7 +615,8 @@ function ToolItem({
491
615
  <Text color={isSelected || isActive ? colors.text : colors.textMuted}>{tool.name}</Text>
492
616
  {installed && installedVersion && <Text color={colors.textDim}> v{installedVersion}</Text>}
493
617
  {installed && <Text color={colors.success}> ✓</Text>}
494
- {updateStatus.hasUpdate && <Text color={colors.primary}> ↑</Text>}
618
+ {wasAutoUpdated && <Text color={colors.success}> ↑</Text>}
619
+ {updateStatus.hasUpdate && !wasAutoUpdated && <Text color={colors.primary}> ↑</Text>}
495
620
  <Text> </Text>
496
621
  {tool.includes.skills.length > 0 && <Text color={colors.skill}>● </Text>}
497
622
  {tool.includes.commands.length > 0 && <Text color={colors.command}>● </Text>}
@@ -930,12 +1055,16 @@ function ToolExplorer({ tool, onViewSource, onClose }: ToolExplorerProps) {
930
1055
  function SettingsDetails({
931
1056
  onEditSettings,
932
1057
  isFocused,
1058
+ onToggleAutoUpdate,
1059
+ selectedSetting,
933
1060
  }: {
934
1061
  onEditSettings: () => void;
935
1062
  isFocused: boolean;
1063
+ onToggleAutoUpdate: () => void;
1064
+ selectedSetting: number;
936
1065
  }) {
937
1066
  const config = loadConfig();
938
- const outputOptions = getOutputOptions();
1067
+ const autoUpdateConfig = getAutoUpdateConfig();
939
1068
 
940
1069
  return (
941
1070
  <Box flexDirection="column" paddingLeft={2} flexGrow={1}>
@@ -954,21 +1083,53 @@ function SettingsDetails({
954
1083
  </Text>
955
1084
  </Box>
956
1085
 
957
- <Box marginTop={1}>
1086
+ <Box flexDirection="column" marginTop={2}>
1087
+ <Text color={colors.text} bold>Auto-Update</Text>
1088
+ <Box marginTop={1}>
1089
+ <Text>
1090
+ <Text color={colors.textDim}>{isFocused && selectedSetting === 0 ? '> ' : ' '}</Text>
1091
+ <Text color={isFocused && selectedSetting === 0 ? colors.text : colors.textMuted}>
1092
+ [{autoUpdateConfig.tools ? 'x' : ' '}] Auto-update tools
1093
+ </Text>
1094
+ </Text>
1095
+ </Box>
1096
+ <Text color={colors.textDim}> Update tools automatically when droid starts</Text>
1097
+ <Box marginTop={1}>
1098
+ <Text>
1099
+ <Text color={colors.textDim}>{isFocused && selectedSetting === 1 ? '> ' : ' '}</Text>
1100
+ <Text color={isFocused && selectedSetting === 1 ? colors.text : colors.textMuted}>
1101
+ [{autoUpdateConfig.app ? 'x' : ' '}] Auto-update app
1102
+ </Text>
1103
+ </Text>
1104
+ </Box>
1105
+ <Text color={colors.textDim}> Update droid automatically when a new version is available</Text>
1106
+ </Box>
1107
+
1108
+ <Box marginTop={2}>
958
1109
  <Text color={colors.textDim}>Config: ~/.droid/config.yaml</Text>
959
1110
  </Box>
960
1111
 
961
1112
  {isFocused && (
962
1113
  <Box marginTop={2}>
963
- <Text backgroundColor={colors.primary} color="#ffffff" bold>
964
- {' '}Edit Settings{' '}
1114
+ <Text
1115
+ backgroundColor={selectedSetting === 2 ? colors.primary : colors.bgSelected}
1116
+ color={selectedSetting === 2 ? '#ffffff' : colors.textMuted}
1117
+ bold={selectedSetting === 2}
1118
+ >
1119
+ {' '}Edit Profile{' '}
965
1120
  </Text>
966
1121
  </Box>
967
1122
  )}
968
1123
 
1124
+ {isFocused && (
1125
+ <Box marginTop={1}>
1126
+ <Text color={colors.textDim}>↑↓ select · enter toggle/edit · esc back</Text>
1127
+ </Box>
1128
+ )}
1129
+
969
1130
  {!isFocused && (
970
1131
  <Box marginTop={2}>
971
- <Text color={colors.textDim}>press enter to edit</Text>
1132
+ <Text color={colors.textDim}>press enter to configure</Text>
972
1133
  </Box>
973
1134
  )}
974
1135
  </Box>
@@ -1265,11 +1426,16 @@ function App() {
1265
1426
  const [message, setMessage] = useState<{ text: string; type: 'success' | 'error' } | null>(null);
1266
1427
  const [isEditingSettings, setIsEditingSettings] = useState(false);
1267
1428
  const [readmeContent, setReadmeContent] = useState<{ title: string; content: string } | null>(null);
1429
+ const [selectedSetting, setSelectedSetting] = useState(0);
1268
1430
  const [isUpdating, setIsUpdating] = useState(false);
1431
+ const [isUpdatingTools, setIsUpdatingTools] = useState(false);
1269
1432
  const [previousView, setPreviousView] = useState<View>('detail'); // Track where to return from readme
1433
+ const [toolUpdates, setToolUpdates] = useState<ToolUpdateInfo[]>([]);
1434
+ const [autoUpdatedTools, setAutoUpdatedTools] = useState<string[]>([]);
1270
1435
 
1271
1436
  // Check for updates once on mount
1272
1437
  const updateInfo = useMemo(() => getUpdateInfo(), []);
1438
+ const autoUpdateConfig = useMemo(() => getAutoUpdateConfig(), []);
1273
1439
 
1274
1440
  const handleUpdate = () => {
1275
1441
  setIsUpdating(true);
@@ -1277,8 +1443,21 @@ function App() {
1277
1443
  setTimeout(() => {
1278
1444
  const result = runUpdate();
1279
1445
  if (result.success) {
1280
- // Print success message before exiting
1281
- console.log('\n✅ Update complete! Run `droid` to start the new version.\n');
1446
+ // Store message to print after leaving alternate screen (with ANSI colors)
1447
+ const blue = '\x1b[38;2;99;102;241m'; // #6366f1
1448
+ const dim = '\x1b[38;2;106;106;106m';
1449
+ const reset = '\x1b[0m';
1450
+ exitMessage = `
1451
+ ${dim}────────────────────────────────────────────────────────${reset}
1452
+
1453
+ ${dim}╔═════╗${reset}
1454
+ ${dim}║${reset} ${blue}●${reset} ${blue}●${reset} ${dim}║${reset} ${blue}"It's quite possible this system${reset}
1455
+ ${dim}╚═╦═╦═╝${reset} ${blue}is now fully operational."${reset}
1456
+
1457
+ Run ${blue}droid${reset} to start the new version.
1458
+
1459
+ ${dim}────────────────────────────────────────────────────────${reset}
1460
+ `;
1282
1461
  exit();
1283
1462
  } else {
1284
1463
  setIsUpdating(false);
@@ -1293,6 +1472,122 @@ function App() {
1293
1472
  }, 100);
1294
1473
  };
1295
1474
 
1475
+ const handleAlwaysUpdate = () => {
1476
+ // Enable auto-update for app in config
1477
+ setAutoUpdateConfig({ app: true });
1478
+ // Then run the update
1479
+ handleUpdate();
1480
+ };
1481
+
1482
+ // Check for tool updates and proceed to next view
1483
+ const checkToolUpdatesAndProceed = () => {
1484
+ const updates = getToolsWithUpdates();
1485
+ setToolUpdates(updates);
1486
+
1487
+ // If auto_update.tools is true, auto-update silently
1488
+ if (autoUpdateConfig.tools && updates.length > 0) {
1489
+ handleUpdateAllTools(updates, true);
1490
+ return;
1491
+ }
1492
+
1493
+ // If there are updates and auto-update is off, show prompt
1494
+ if (updates.length > 0) {
1495
+ setView('tool-updates');
1496
+ return;
1497
+ }
1498
+
1499
+ // No updates, proceed to setup or menu
1500
+ if (!configExists()) {
1501
+ setView('setup');
1502
+ } else {
1503
+ setView('menu');
1504
+ }
1505
+ };
1506
+
1507
+ const handleUpdateAllTools = (updates: ToolUpdateInfo[] = toolUpdates, silent = false) => {
1508
+ if (!silent) {
1509
+ setIsUpdatingTools(true);
1510
+ }
1511
+
1512
+ setTimeout(() => {
1513
+ let successCount = 0;
1514
+ let failCount = 0;
1515
+ const updatedNames: string[] = [];
1516
+
1517
+ for (const tool of updates) {
1518
+ // Find the tool to get its primary skill
1519
+ const toolManifest = tools.find(t => t.name === tool.name);
1520
+ if (toolManifest) {
1521
+ const primarySkill = toolManifest.includes.skills.find(s => s.required)?.name || toolManifest.name;
1522
+ const result = updateSkill(primarySkill);
1523
+ if (result.success) {
1524
+ successCount++;
1525
+ updatedNames.push(tool.name);
1526
+ } else {
1527
+ failCount++;
1528
+ }
1529
+ }
1530
+ }
1531
+
1532
+ setIsUpdatingTools(false);
1533
+
1534
+ // Track which tools were auto-updated for visual indicator
1535
+ if (silent && updatedNames.length > 0) {
1536
+ setAutoUpdatedTools(updatedNames);
1537
+ }
1538
+
1539
+ if (successCount > 0) {
1540
+ setMessage({
1541
+ text: silent
1542
+ ? `↑ Auto-updated ${successCount} tool${successCount > 1 ? 's' : ''}`
1543
+ : `✓ Updated ${successCount} tool${successCount > 1 ? 's' : ''}${failCount > 0 ? `, ${failCount} failed` : ''}`,
1544
+ type: failCount > 0 ? 'error' : 'success',
1545
+ });
1546
+ }
1547
+
1548
+ // Proceed to next view
1549
+ if (!configExists()) {
1550
+ setView('setup');
1551
+ } else {
1552
+ setView('menu');
1553
+ }
1554
+ }, 100);
1555
+ };
1556
+
1557
+ const handleAlwaysUpdateTools = () => {
1558
+ // Enable auto-update in config
1559
+ setAutoUpdateConfig({ tools: true });
1560
+ // Then update all
1561
+ handleUpdateAllTools();
1562
+ };
1563
+
1564
+ const handleSkipToolUpdates = () => {
1565
+ if (!configExists()) {
1566
+ setView('setup');
1567
+ } else {
1568
+ setView('menu');
1569
+ }
1570
+ };
1571
+
1572
+ const handleToggleAutoUpdateTools = () => {
1573
+ const current = getAutoUpdateConfig();
1574
+ setAutoUpdateConfig({ tools: !current.tools });
1575
+ // Force re-render by setting message (auto-clears on next input)
1576
+ setMessage({
1577
+ text: `✓ Auto-update tools ${!current.tools ? 'enabled' : 'disabled'}`,
1578
+ type: 'success',
1579
+ });
1580
+ };
1581
+
1582
+ const handleToggleAutoUpdateApp = () => {
1583
+ const current = getAutoUpdateConfig();
1584
+ setAutoUpdateConfig({ app: !current.app });
1585
+ setMessage({
1586
+ text: `✓ Auto-update app ${!current.app ? 'enabled' : 'disabled'}`,
1587
+ type: 'success',
1588
+ });
1589
+ };
1590
+
1296
1591
  const MAX_VISIBLE_ITEMS = 6;
1297
1592
 
1298
1593
  const tools = getBundledTools();
@@ -1349,27 +1644,45 @@ function App() {
1349
1644
  if (activeTab === 'tools' && tools.length > 0) {
1350
1645
  setView('detail');
1351
1646
  } else if (activeTab === 'settings') {
1352
- setIsEditingSettings(true);
1353
- setView('setup');
1647
+ setView('detail');
1648
+ setSelectedSetting(0);
1354
1649
  }
1355
1650
  }
1356
1651
  } else if (view === 'detail') {
1357
1652
  if (key.escape || key.backspace) {
1358
1653
  setView('menu');
1359
1654
  setSelectedAction(0);
1655
+ setSelectedSetting(0);
1360
1656
  }
1361
- if (key.leftArrow) {
1657
+ if (activeTab === 'settings') {
1658
+ // Settings detail view navigation
1659
+ if (key.upArrow) {
1660
+ setSelectedSetting((prev) => Math.max(0, prev - 1));
1661
+ }
1662
+ if (key.downArrow) {
1663
+ setSelectedSetting((prev) => Math.min(2, prev + 1)); // 0: auto-update tools, 1: auto-update app, 2: edit profile
1664
+ }
1665
+ if (key.return) {
1666
+ if (selectedSetting === 0) {
1667
+ handleToggleAutoUpdateTools();
1668
+ } else if (selectedSetting === 1) {
1669
+ handleToggleAutoUpdateApp();
1670
+ } else if (selectedSetting === 2) {
1671
+ setIsEditingSettings(true);
1672
+ setView('setup');
1673
+ }
1674
+ }
1675
+ }
1676
+ if (key.leftArrow && activeTab === 'tools') {
1362
1677
  setSelectedAction((prev) => Math.max(0, prev - 1));
1363
1678
  }
1364
- if (key.rightArrow) {
1679
+ if (key.rightArrow && activeTab === 'tools') {
1365
1680
  let maxActions = 0;
1366
- if (activeTab === 'tools') {
1367
- const tool = tools[selectedIndex];
1368
- const installed = tool ? isToolInstalled(tool.name) : false;
1369
- const hasUpdate = tool ? getToolUpdateStatus(tool.name).hasUpdate : false;
1370
- // Explore, [Update], Configure, Uninstall or Explore, Install
1371
- maxActions = installed ? (hasUpdate ? 3 : 2) : 1;
1372
- }
1681
+ const tool = tools[selectedIndex];
1682
+ const installed = tool ? isToolInstalled(tool.name) : false;
1683
+ const hasUpdate = tool ? getToolUpdateStatus(tool.name).hasUpdate : false;
1684
+ // Explore, [Update], Configure, Uninstall or Explore, Install
1685
+ maxActions = installed ? (hasUpdate ? 3 : 2) : 1;
1373
1686
  setSelectedAction((prev) => Math.min(maxActions, prev + 1));
1374
1687
  }
1375
1688
  if (key.return && activeTab === 'tools') {
@@ -1443,7 +1756,7 @@ function App() {
1443
1756
  }
1444
1757
  }
1445
1758
  }
1446
- }, { isActive: view !== 'welcome' && view !== 'setup' && view !== 'configure' && view !== 'explorer' });
1759
+ }, { isActive: view !== 'welcome' && view !== 'tool-updates' && view !== 'setup' && view !== 'configure' && view !== 'explorer' });
1447
1760
 
1448
1761
  const selectedTool = activeTab === 'tools' ? tools[selectedIndex] ?? null : null;
1449
1762
  // For configure view, we need the matching skill manifest
@@ -1457,15 +1770,21 @@ function App() {
1457
1770
  updateInfo={updateInfo}
1458
1771
  isUpdating={isUpdating}
1459
1772
  onUpdate={handleUpdate}
1773
+ onAlways={handleAlwaysUpdate}
1460
1774
  onExit={exit}
1461
- onContinue={() => {
1462
- // If no config exists, show setup first
1463
- if (!configExists()) {
1464
- setView('setup');
1465
- } else {
1466
- setView('menu');
1467
- }
1468
- }}
1775
+ onContinue={checkToolUpdatesAndProceed}
1776
+ />
1777
+ );
1778
+ }
1779
+
1780
+ if (view === 'tool-updates' && toolUpdates.length > 0) {
1781
+ return (
1782
+ <ToolUpdatePrompt
1783
+ toolUpdates={toolUpdates}
1784
+ onUpdateAll={() => handleUpdateAllTools()}
1785
+ onAlways={handleAlwaysUpdateTools}
1786
+ onSkip={handleSkipToolUpdates}
1787
+ isUpdating={isUpdatingTools}
1469
1788
  />
1470
1789
  );
1471
1790
  }
@@ -1577,6 +1896,7 @@ function App() {
1577
1896
  tool={tool}
1578
1897
  isSelected={scrollOffset + index === selectedIndex}
1579
1898
  isActive={scrollOffset + index === selectedIndex && view === 'detail'}
1899
+ wasAutoUpdated={autoUpdatedTools.includes(tool.name)}
1580
1900
  />
1581
1901
  ))}
1582
1902
  {scrollOffset + MAX_VISIBLE_ITEMS < tools.length && (
@@ -1620,7 +1940,15 @@ function App() {
1620
1940
  )}
1621
1941
 
1622
1942
  {activeTab === 'settings' && (
1623
- <SettingsDetails onEditSettings={() => setView('setup')} isFocused={false} />
1943
+ <SettingsDetails
1944
+ onEditSettings={() => {
1945
+ setIsEditingSettings(true);
1946
+ setView('setup');
1947
+ }}
1948
+ isFocused={view === 'detail'}
1949
+ onToggleAutoUpdate={handleToggleAutoUpdateTools}
1950
+ selectedSetting={selectedSetting}
1951
+ />
1624
1952
  )}
1625
1953
  </Box>
1626
1954
  );
@@ -1637,4 +1965,10 @@ export async function tuiCommand(): Promise<void> {
1637
1965
 
1638
1966
  // Leave alternate screen
1639
1967
  process.stdout.write('\x1b[?1049l');
1968
+
1969
+ // Print exit message if set (e.g., after successful update)
1970
+ if (exitMessage) {
1971
+ console.log(exitMessage);
1972
+ exitMessage = null;
1973
+ }
1640
1974
  }
package/src/lib/config.ts CHANGED
@@ -2,11 +2,16 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
2
2
  import { homedir } from 'os';
3
3
  import { join } from 'path';
4
4
  import YAML from 'yaml';
5
- import { Platform, BuiltInOutput, type DroidConfig, type LegacyDroidConfig, type SkillOverrides } from './types.js';
5
+ import { Platform, BuiltInOutput, type DroidConfig, type LegacyDroidConfig, type SkillOverrides, type AutoUpdateConfig } from './types.js';
6
6
 
7
7
  const CONFIG_DIR = join(homedir(), '.droid');
8
8
  const CONFIG_FILE = join(CONFIG_DIR, 'config.yaml');
9
9
 
10
+ const DEFAULT_AUTO_UPDATE: AutoUpdateConfig = {
11
+ app: false, // Opt-in: user must enable
12
+ tools: true, // Opt-out: enabled by default (tools only update when app updates)
13
+ };
14
+
10
15
  const DEFAULT_CONFIG: DroidConfig = {
11
16
  platform: Platform.ClaudeCode,
12
17
  user_mention: '@user',
@@ -194,3 +199,27 @@ export function saveSkillOverrides(skillName: string, overrides: SkillOverrides)
194
199
  const content = YAML.stringify(overrides, { indent: 2 });
195
200
  writeFileSync(overridesPath, content, 'utf-8');
196
201
  }
202
+
203
+ /**
204
+ * Get auto-update config with defaults applied
205
+ */
206
+ export function getAutoUpdateConfig(): AutoUpdateConfig {
207
+ const config = loadConfig();
208
+ return {
209
+ ...DEFAULT_AUTO_UPDATE,
210
+ ...config.auto_update,
211
+ };
212
+ }
213
+
214
+ /**
215
+ * Update auto-update config
216
+ */
217
+ export function setAutoUpdateConfig(updates: Partial<AutoUpdateConfig>): void {
218
+ const config = loadConfig();
219
+ config.auto_update = {
220
+ ...DEFAULT_AUTO_UPDATE,
221
+ ...config.auto_update,
222
+ ...updates,
223
+ };
224
+ saveConfig(config);
225
+ }