@orderful/droid 0.11.1 → 0.13.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>}
@@ -521,6 +646,7 @@ function ToolDetails({
521
646
  const installed = isToolInstalled(tool.name);
522
647
  const installedVersion = getInstalledToolVersion(tool.name);
523
648
  const updateStatus = getToolUpdateStatus(tool.name);
649
+ const isSystemTool = (tool as ToolManifest & { system?: boolean }).system === true;
524
650
 
525
651
  const actions = installed
526
652
  ? [
@@ -529,10 +655,12 @@ function ToolDetails({
529
655
  ? [{ id: 'update', label: `Update (${updateStatus.bundledVersion})`, variant: 'primary' }]
530
656
  : []),
531
657
  { id: 'configure', label: 'Configure', variant: 'default' },
532
- { id: 'uninstall', label: 'Uninstall', variant: 'danger' },
658
+ // System tools can't be uninstalled
659
+ ...(!isSystemTool ? [{ id: 'uninstall', label: 'Uninstall', variant: 'danger' }] : []),
533
660
  ]
534
661
  : [
535
662
  { id: 'explore', label: 'Explore', variant: 'default' },
663
+ // System tools auto-install, but show Install for manual trigger if needed
536
664
  { id: 'install', label: 'Install', variant: 'primary' },
537
665
  ];
538
666
 
@@ -930,12 +1058,16 @@ function ToolExplorer({ tool, onViewSource, onClose }: ToolExplorerProps) {
930
1058
  function SettingsDetails({
931
1059
  onEditSettings,
932
1060
  isFocused,
1061
+ onToggleAutoUpdate,
1062
+ selectedSetting,
933
1063
  }: {
934
1064
  onEditSettings: () => void;
935
1065
  isFocused: boolean;
1066
+ onToggleAutoUpdate: () => void;
1067
+ selectedSetting: number;
936
1068
  }) {
937
1069
  const config = loadConfig();
938
- const outputOptions = getOutputOptions();
1070
+ const autoUpdateConfig = getAutoUpdateConfig();
939
1071
 
940
1072
  return (
941
1073
  <Box flexDirection="column" paddingLeft={2} flexGrow={1}>
@@ -954,21 +1086,53 @@ function SettingsDetails({
954
1086
  </Text>
955
1087
  </Box>
956
1088
 
957
- <Box marginTop={1}>
1089
+ <Box flexDirection="column" marginTop={2}>
1090
+ <Text color={colors.text} bold>Auto-Update</Text>
1091
+ <Box marginTop={1}>
1092
+ <Text>
1093
+ <Text color={colors.textDim}>{isFocused && selectedSetting === 0 ? '> ' : ' '}</Text>
1094
+ <Text color={isFocused && selectedSetting === 0 ? colors.text : colors.textMuted}>
1095
+ [{autoUpdateConfig.tools ? 'x' : ' '}] Auto-update tools
1096
+ </Text>
1097
+ </Text>
1098
+ </Box>
1099
+ <Text color={colors.textDim}> Update tools automatically when droid starts</Text>
1100
+ <Box marginTop={1}>
1101
+ <Text>
1102
+ <Text color={colors.textDim}>{isFocused && selectedSetting === 1 ? '> ' : ' '}</Text>
1103
+ <Text color={isFocused && selectedSetting === 1 ? colors.text : colors.textMuted}>
1104
+ [{autoUpdateConfig.app ? 'x' : ' '}] Auto-update app
1105
+ </Text>
1106
+ </Text>
1107
+ </Box>
1108
+ <Text color={colors.textDim}> Update droid automatically when a new version is available</Text>
1109
+ </Box>
1110
+
1111
+ <Box marginTop={2}>
958
1112
  <Text color={colors.textDim}>Config: ~/.droid/config.yaml</Text>
959
1113
  </Box>
960
1114
 
961
1115
  {isFocused && (
962
1116
  <Box marginTop={2}>
963
- <Text backgroundColor={colors.primary} color="#ffffff" bold>
964
- {' '}Edit Settings{' '}
1117
+ <Text
1118
+ backgroundColor={selectedSetting === 2 ? colors.primary : colors.bgSelected}
1119
+ color={selectedSetting === 2 ? '#ffffff' : colors.textMuted}
1120
+ bold={selectedSetting === 2}
1121
+ >
1122
+ {' '}Edit Profile{' '}
965
1123
  </Text>
966
1124
  </Box>
967
1125
  )}
968
1126
 
1127
+ {isFocused && (
1128
+ <Box marginTop={1}>
1129
+ <Text color={colors.textDim}>↑↓ select · enter toggle/edit · esc back</Text>
1130
+ </Box>
1131
+ )}
1132
+
969
1133
  {!isFocused && (
970
1134
  <Box marginTop={2}>
971
- <Text color={colors.textDim}>press enter to edit</Text>
1135
+ <Text color={colors.textDim}>press enter to configure</Text>
972
1136
  </Box>
973
1137
  )}
974
1138
  </Box>
@@ -1265,11 +1429,16 @@ function App() {
1265
1429
  const [message, setMessage] = useState<{ text: string; type: 'success' | 'error' } | null>(null);
1266
1430
  const [isEditingSettings, setIsEditingSettings] = useState(false);
1267
1431
  const [readmeContent, setReadmeContent] = useState<{ title: string; content: string } | null>(null);
1432
+ const [selectedSetting, setSelectedSetting] = useState(0);
1268
1433
  const [isUpdating, setIsUpdating] = useState(false);
1434
+ const [isUpdatingTools, setIsUpdatingTools] = useState(false);
1269
1435
  const [previousView, setPreviousView] = useState<View>('detail'); // Track where to return from readme
1436
+ const [toolUpdates, setToolUpdates] = useState<ToolUpdateInfo[]>([]);
1437
+ const [autoUpdatedTools, setAutoUpdatedTools] = useState<string[]>([]);
1270
1438
 
1271
1439
  // Check for updates once on mount
1272
1440
  const updateInfo = useMemo(() => getUpdateInfo(), []);
1441
+ const autoUpdateConfig = useMemo(() => getAutoUpdateConfig(), []);
1273
1442
 
1274
1443
  const handleUpdate = () => {
1275
1444
  setIsUpdating(true);
@@ -1277,8 +1446,21 @@ function App() {
1277
1446
  setTimeout(() => {
1278
1447
  const result = runUpdate();
1279
1448
  if (result.success) {
1280
- // Print success message before exiting
1281
- console.log('\n✅ Update complete! Run `droid` to start the new version.\n');
1449
+ // Store message to print after leaving alternate screen (with ANSI colors)
1450
+ const blue = '\x1b[38;2;99;102;241m'; // #6366f1
1451
+ const dim = '\x1b[38;2;106;106;106m';
1452
+ const reset = '\x1b[0m';
1453
+ exitMessage = `
1454
+ ${dim}────────────────────────────────────────────────────────${reset}
1455
+
1456
+ ${dim}╔═════╗${reset}
1457
+ ${dim}║${reset} ${blue}●${reset} ${blue}●${reset} ${dim}║${reset} ${blue}"It's quite possible this system${reset}
1458
+ ${dim}╚═╦═╦═╝${reset} ${blue}is now fully operational."${reset}
1459
+
1460
+ Run ${blue}droid${reset} to start the new version.
1461
+
1462
+ ${dim}────────────────────────────────────────────────────────${reset}
1463
+ `;
1282
1464
  exit();
1283
1465
  } else {
1284
1466
  setIsUpdating(false);
@@ -1293,6 +1475,142 @@ function App() {
1293
1475
  }, 100);
1294
1476
  };
1295
1477
 
1478
+ const handleAlwaysUpdate = () => {
1479
+ // Enable auto-update for app in config
1480
+ setAutoUpdateConfig({ app: true });
1481
+ // Then run the update
1482
+ handleUpdate();
1483
+ };
1484
+
1485
+ // Ensure system tools (marked with system: true in TOOL.yaml) are always installed and current
1486
+ const ensureSystemTools = () => {
1487
+ // Find all tools marked as system tools
1488
+ const systemTools = tools.filter(t => (t as ToolManifest & { system?: boolean }).system === true);
1489
+
1490
+ for (const systemTool of systemTools) {
1491
+ const installed = isToolInstalled(systemTool.name);
1492
+ const updateStatus = getToolUpdateStatus(systemTool.name);
1493
+
1494
+ // Install if not installed, or update if outdated (regardless of auto-update settings)
1495
+ if (!installed || updateStatus.hasUpdate) {
1496
+ const primarySkill = systemTool.includes.skills.find(s => s.required)?.name || systemTool.name;
1497
+ installSkill(primarySkill);
1498
+ }
1499
+ }
1500
+ };
1501
+
1502
+ // Check for tool updates and proceed to next view
1503
+ const checkToolUpdatesAndProceed = () => {
1504
+ // Always ensure system tools are current (bypasses auto-update settings)
1505
+ ensureSystemTools();
1506
+
1507
+ const updates = getToolsWithUpdates();
1508
+ setToolUpdates(updates);
1509
+
1510
+ // If auto_update.tools is true, auto-update silently
1511
+ if (autoUpdateConfig.tools && updates.length > 0) {
1512
+ handleUpdateAllTools(updates, true);
1513
+ return;
1514
+ }
1515
+
1516
+ // If there are updates and auto-update is off, show prompt
1517
+ if (updates.length > 0) {
1518
+ setView('tool-updates');
1519
+ return;
1520
+ }
1521
+
1522
+ // No updates, proceed to setup or menu
1523
+ if (!configExists()) {
1524
+ setView('setup');
1525
+ } else {
1526
+ setView('menu');
1527
+ }
1528
+ };
1529
+
1530
+ const handleUpdateAllTools = (updates: ToolUpdateInfo[] = toolUpdates, silent = false) => {
1531
+ if (!silent) {
1532
+ setIsUpdatingTools(true);
1533
+ }
1534
+
1535
+ setTimeout(() => {
1536
+ let successCount = 0;
1537
+ let failCount = 0;
1538
+ const updatedNames: string[] = [];
1539
+
1540
+ for (const tool of updates) {
1541
+ // Find the tool to get its primary skill
1542
+ const toolManifest = tools.find(t => t.name === tool.name);
1543
+ if (toolManifest) {
1544
+ const primarySkill = toolManifest.includes.skills.find(s => s.required)?.name || toolManifest.name;
1545
+ const result = updateSkill(primarySkill);
1546
+ if (result.success) {
1547
+ successCount++;
1548
+ updatedNames.push(tool.name);
1549
+ } else {
1550
+ failCount++;
1551
+ }
1552
+ }
1553
+ }
1554
+
1555
+ setIsUpdatingTools(false);
1556
+
1557
+ // Track which tools were auto-updated for visual indicator
1558
+ if (silent && updatedNames.length > 0) {
1559
+ setAutoUpdatedTools(updatedNames);
1560
+ }
1561
+
1562
+ if (successCount > 0) {
1563
+ setMessage({
1564
+ text: silent
1565
+ ? `↑ Auto-updated ${successCount} tool${successCount > 1 ? 's' : ''}`
1566
+ : `✓ Updated ${successCount} tool${successCount > 1 ? 's' : ''}${failCount > 0 ? `, ${failCount} failed` : ''}`,
1567
+ type: failCount > 0 ? 'error' : 'success',
1568
+ });
1569
+ }
1570
+
1571
+ // Proceed to next view
1572
+ if (!configExists()) {
1573
+ setView('setup');
1574
+ } else {
1575
+ setView('menu');
1576
+ }
1577
+ }, 100);
1578
+ };
1579
+
1580
+ const handleAlwaysUpdateTools = () => {
1581
+ // Enable auto-update in config
1582
+ setAutoUpdateConfig({ tools: true });
1583
+ // Then update all
1584
+ handleUpdateAllTools();
1585
+ };
1586
+
1587
+ const handleSkipToolUpdates = () => {
1588
+ if (!configExists()) {
1589
+ setView('setup');
1590
+ } else {
1591
+ setView('menu');
1592
+ }
1593
+ };
1594
+
1595
+ const handleToggleAutoUpdateTools = () => {
1596
+ const current = getAutoUpdateConfig();
1597
+ setAutoUpdateConfig({ tools: !current.tools });
1598
+ // Force re-render by setting message (auto-clears on next input)
1599
+ setMessage({
1600
+ text: `✓ Auto-update tools ${!current.tools ? 'enabled' : 'disabled'}`,
1601
+ type: 'success',
1602
+ });
1603
+ };
1604
+
1605
+ const handleToggleAutoUpdateApp = () => {
1606
+ const current = getAutoUpdateConfig();
1607
+ setAutoUpdateConfig({ app: !current.app });
1608
+ setMessage({
1609
+ text: `✓ Auto-update app ${!current.app ? 'enabled' : 'disabled'}`,
1610
+ type: 'success',
1611
+ });
1612
+ };
1613
+
1296
1614
  const MAX_VISIBLE_ITEMS = 6;
1297
1615
 
1298
1616
  const tools = getBundledTools();
@@ -1349,27 +1667,47 @@ function App() {
1349
1667
  if (activeTab === 'tools' && tools.length > 0) {
1350
1668
  setView('detail');
1351
1669
  } else if (activeTab === 'settings') {
1352
- setIsEditingSettings(true);
1353
- setView('setup');
1670
+ setView('detail');
1671
+ setSelectedSetting(0);
1354
1672
  }
1355
1673
  }
1356
1674
  } else if (view === 'detail') {
1357
1675
  if (key.escape || key.backspace) {
1358
1676
  setView('menu');
1359
1677
  setSelectedAction(0);
1678
+ setSelectedSetting(0);
1360
1679
  }
1361
- if (key.leftArrow) {
1680
+ if (activeTab === 'settings') {
1681
+ // Settings detail view navigation
1682
+ if (key.upArrow) {
1683
+ setSelectedSetting((prev) => Math.max(0, prev - 1));
1684
+ }
1685
+ if (key.downArrow) {
1686
+ setSelectedSetting((prev) => Math.min(2, prev + 1)); // 0: auto-update tools, 1: auto-update app, 2: edit profile
1687
+ }
1688
+ if (key.return) {
1689
+ if (selectedSetting === 0) {
1690
+ handleToggleAutoUpdateTools();
1691
+ } else if (selectedSetting === 1) {
1692
+ handleToggleAutoUpdateApp();
1693
+ } else if (selectedSetting === 2) {
1694
+ setIsEditingSettings(true);
1695
+ setView('setup');
1696
+ }
1697
+ }
1698
+ }
1699
+ if (key.leftArrow && activeTab === 'tools') {
1362
1700
  setSelectedAction((prev) => Math.max(0, prev - 1));
1363
1701
  }
1364
- if (key.rightArrow) {
1702
+ if (key.rightArrow && activeTab === 'tools') {
1365
1703
  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
- }
1704
+ const tool = tools[selectedIndex];
1705
+ const installed = tool ? isToolInstalled(tool.name) : false;
1706
+ const hasUpdate = tool ? getToolUpdateStatus(tool.name).hasUpdate : false;
1707
+ const isSystem = tool ? (tool as ToolManifest & { system?: boolean }).system === true : false;
1708
+ // Explore, [Update], Configure, [Uninstall] or Explore, Install
1709
+ // System tools don't have Uninstall, so one less action
1710
+ maxActions = installed ? (hasUpdate ? (isSystem ? 2 : 3) : (isSystem ? 1 : 2)) : 1;
1373
1711
  setSelectedAction((prev) => Math.min(maxActions, prev + 1));
1374
1712
  }
1375
1713
  if (key.return && activeTab === 'tools') {
@@ -1377,13 +1715,14 @@ function App() {
1377
1715
  if (tool) {
1378
1716
  const installed = isToolInstalled(tool.name);
1379
1717
  const toolUpdateStatus = getToolUpdateStatus(tool.name);
1718
+ const isSystemTool = (tool as ToolManifest & { system?: boolean }).system === true;
1380
1719
  // Build actions array to match ToolDetails
1381
1720
  const toolActions = installed
1382
1721
  ? [
1383
1722
  { id: 'explore' },
1384
1723
  ...(toolUpdateStatus.hasUpdate ? [{ id: 'update' }] : []),
1385
1724
  { id: 'configure' },
1386
- { id: 'uninstall' },
1725
+ ...(!isSystemTool ? [{ id: 'uninstall' }] : []),
1387
1726
  ]
1388
1727
  : [{ id: 'explore' }, { id: 'install' }];
1389
1728
 
@@ -1443,7 +1782,7 @@ function App() {
1443
1782
  }
1444
1783
  }
1445
1784
  }
1446
- }, { isActive: view !== 'welcome' && view !== 'setup' && view !== 'configure' && view !== 'explorer' });
1785
+ }, { isActive: view !== 'welcome' && view !== 'tool-updates' && view !== 'setup' && view !== 'configure' && view !== 'explorer' });
1447
1786
 
1448
1787
  const selectedTool = activeTab === 'tools' ? tools[selectedIndex] ?? null : null;
1449
1788
  // For configure view, we need the matching skill manifest
@@ -1457,15 +1796,21 @@ function App() {
1457
1796
  updateInfo={updateInfo}
1458
1797
  isUpdating={isUpdating}
1459
1798
  onUpdate={handleUpdate}
1799
+ onAlways={handleAlwaysUpdate}
1460
1800
  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
- }}
1801
+ onContinue={checkToolUpdatesAndProceed}
1802
+ />
1803
+ );
1804
+ }
1805
+
1806
+ if (view === 'tool-updates' && toolUpdates.length > 0) {
1807
+ return (
1808
+ <ToolUpdatePrompt
1809
+ toolUpdates={toolUpdates}
1810
+ onUpdateAll={() => handleUpdateAllTools()}
1811
+ onAlways={handleAlwaysUpdateTools}
1812
+ onSkip={handleSkipToolUpdates}
1813
+ isUpdating={isUpdatingTools}
1469
1814
  />
1470
1815
  );
1471
1816
  }
@@ -1577,6 +1922,7 @@ function App() {
1577
1922
  tool={tool}
1578
1923
  isSelected={scrollOffset + index === selectedIndex}
1579
1924
  isActive={scrollOffset + index === selectedIndex && view === 'detail'}
1925
+ wasAutoUpdated={autoUpdatedTools.includes(tool.name)}
1580
1926
  />
1581
1927
  ))}
1582
1928
  {scrollOffset + MAX_VISIBLE_ITEMS < tools.length && (
@@ -1620,7 +1966,15 @@ function App() {
1620
1966
  )}
1621
1967
 
1622
1968
  {activeTab === 'settings' && (
1623
- <SettingsDetails onEditSettings={() => setView('setup')} isFocused={false} />
1969
+ <SettingsDetails
1970
+ onEditSettings={() => {
1971
+ setIsEditingSettings(true);
1972
+ setView('setup');
1973
+ }}
1974
+ isFocused={view === 'detail'}
1975
+ onToggleAutoUpdate={handleToggleAutoUpdateTools}
1976
+ selectedSetting={selectedSetting}
1977
+ />
1624
1978
  )}
1625
1979
  </Box>
1626
1980
  );
@@ -1637,4 +1991,10 @@ export async function tuiCommand(): Promise<void> {
1637
1991
 
1638
1992
  // Leave alternate screen
1639
1993
  process.stdout.write('\x1b[?1049l');
1994
+
1995
+ // Print exit message if set (e.g., after successful update)
1996
+ if (exitMessage) {
1997
+ console.log(exitMessage);
1998
+ exitMessage = null;
1999
+ }
1640
2000
  }