claudeup 4.6.0 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/package.json +1 -1
  2. package/src/data/settings-catalog.js +2 -7
  3. package/src/data/settings-catalog.ts +2 -7
  4. package/src/opentui.d.ts +7 -2
  5. package/src/services/claude-settings.js +31 -4
  6. package/src/services/claude-settings.ts +31 -4
  7. package/src/services/settings-manager.js +84 -5
  8. package/src/services/settings-manager.ts +86 -5
  9. package/src/types/index.ts +1 -1
  10. package/src/ui/adapters/settingsAdapter.js +8 -8
  11. package/src/ui/adapters/settingsAdapter.ts +8 -8
  12. package/src/ui/components/TabBar.js +1 -23
  13. package/src/ui/components/TabBar.tsx +1 -26
  14. package/src/ui/components/modals/ConfirmModal.js +1 -1
  15. package/src/ui/components/modals/ConfirmModal.tsx +17 -16
  16. package/src/ui/components/modals/InputModal.js +2 -13
  17. package/src/ui/components/modals/InputModal.tsx +21 -24
  18. package/src/ui/components/modals/LoadingModal.js +1 -1
  19. package/src/ui/components/modals/LoadingModal.tsx +6 -6
  20. package/src/ui/components/modals/MessageModal.js +4 -4
  21. package/src/ui/components/modals/MessageModal.tsx +13 -13
  22. package/src/ui/components/modals/ModalContainer.js +25 -2
  23. package/src/ui/components/modals/ModalContainer.tsx +25 -2
  24. package/src/ui/components/modals/SelectModal.js +3 -4
  25. package/src/ui/components/modals/SelectModal.tsx +18 -15
  26. package/src/ui/renderers/settingsRenderers.js +1 -1
  27. package/src/ui/renderers/settingsRenderers.tsx +5 -3
  28. package/src/ui/screens/CliToolsScreen.js +2 -2
  29. package/src/ui/screens/CliToolsScreen.tsx +3 -1
  30. package/src/ui/screens/EnvVarsScreen.js +27 -10
  31. package/src/ui/screens/EnvVarsScreen.tsx +33 -16
  32. package/src/ui/screens/McpRegistryScreen.js +2 -2
  33. package/src/ui/screens/McpRegistryScreen.tsx +3 -1
  34. package/src/ui/screens/McpScreen.js +1 -1
  35. package/src/ui/screens/McpScreen.tsx +2 -1
  36. package/src/ui/screens/ModelSelectorScreen.js +2 -2
  37. package/src/ui/screens/ModelSelectorScreen.tsx +3 -2
  38. package/src/ui/screens/ProfilesScreen.js +1 -1
  39. package/src/ui/screens/ProfilesScreen.tsx +2 -1
  40. package/src/ui/screens/StatusLineScreen.js +1 -1
  41. package/src/ui/screens/StatusLineScreen.tsx +2 -1
  42. package/src/ui/state/DimensionsContext.js +2 -2
  43. package/src/ui/state/DimensionsContext.tsx +3 -3
  44. package/src/ui/components/ScrollableDetail.js +0 -23
  45. package/src/ui/components/ScrollableDetail.tsx +0 -55
@@ -1,5 +1,4 @@
1
- import React, { useState } from "react";
2
- import { useKeyboard } from "../../hooks/useKeyboard.js";
1
+ import React from "react";
3
2
 
4
3
  interface InputModalProps {
5
4
  /** Modal title */
@@ -19,43 +18,41 @@ export function InputModal({
19
18
  label,
20
19
  defaultValue = "",
21
20
  onSubmit,
22
- onCancel,
23
21
  }: InputModalProps) {
24
- const [value, setValue] = useState(defaultValue);
25
-
26
- useKeyboard((key) => {
27
- if (key.name === "enter") {
28
- onSubmit(value);
29
- } else if (key.name === "escape") {
30
- onCancel();
31
- }
32
- });
33
-
34
22
  return (
35
23
  <box
36
24
  flexDirection="column"
37
25
  border
38
26
  borderStyle="rounded"
39
- borderColor="cyan"
40
- backgroundColor="#1a1a2e"
41
- paddingLeft={2}
42
- paddingRight={2}
27
+ borderColor="#525252"
28
+ backgroundColor="#1C1C1E"
29
+ paddingLeft={3}
30
+ paddingRight={3}
43
31
  paddingTop={1}
44
32
  paddingBottom={1}
45
33
  width={60}
46
34
  >
47
- <text>
48
- <strong>{title}</strong>
49
- </text>
35
+ <box marginBottom={1}>
36
+ <text fg="#EDEDED">
37
+ <strong>{title}</strong>
38
+ </text>
39
+ </box>
50
40
 
51
- <box marginTop={1} marginBottom={1}>
52
- <text>{label}</text>
41
+ <box marginBottom={1}>
42
+ <text fg="#A1A1AA">{label}</text>
53
43
  </box>
54
44
 
55
- <input value={value} onChange={setValue} focused width={54} />
45
+ <box border borderStyle="rounded" borderColor="#3F3F46" paddingLeft={1} paddingRight={1} width={54}>
46
+ <input
47
+ value={defaultValue}
48
+ onSubmit={onSubmit as any}
49
+ focused
50
+ width={50}
51
+ />
52
+ </box>
56
53
 
57
54
  <box marginTop={1}>
58
- <text fg="#666666">Enter to confirm • Escape to cancel</text>
55
+ <text fg="#71717A">↵ to confirm • Esc to cancel</text>
59
56
  </box>
60
57
  </box>
61
58
  );
@@ -9,6 +9,6 @@ export function LoadingModal({ message }) {
9
9
  }, 80);
10
10
  return () => clearInterval(interval);
11
11
  }, []);
12
- return (_jsxs("box", { flexDirection: "row", border: true, borderStyle: "rounded", borderColor: "cyan", backgroundColor: "#1a1a2e", paddingLeft: 2, paddingRight: 2, paddingTop: 1, paddingBottom: 1, children: [_jsx("text", { fg: "cyan", children: SPINNER_FRAMES[frame] }), _jsxs("text", { children: [" ", message] })] }));
12
+ return (_jsxs("box", { flexDirection: "row", border: true, borderStyle: "rounded", borderColor: "#525252", backgroundColor: "#1C1C1E", paddingLeft: 3, paddingRight: 3, paddingTop: 1, paddingBottom: 1, children: [_jsx("text", { fg: "#A1A1AA", children: SPINNER_FRAMES[frame] }), _jsxs("text", { fg: "#EDEDED", children: [" ", message] })] }));
13
13
  }
14
14
  export default LoadingModal;
@@ -23,15 +23,15 @@ export function LoadingModal({ message }: LoadingModalProps) {
23
23
  flexDirection="row"
24
24
  border
25
25
  borderStyle="rounded"
26
- borderColor="cyan"
27
- backgroundColor="#1a1a2e"
28
- paddingLeft={2}
29
- paddingRight={2}
26
+ borderColor="#525252"
27
+ backgroundColor="#1C1C1E"
28
+ paddingLeft={3}
29
+ paddingRight={3}
30
30
  paddingTop={1}
31
31
  paddingBottom={1}
32
32
  >
33
- <text fg="cyan">{SPINNER_FRAMES[frame]}</text>
34
- <text> {message}</text>
33
+ <text fg="#A1A1AA">{SPINNER_FRAMES[frame]}</text>
34
+ <text fg="#EDEDED"> {message}</text>
35
35
  </box>
36
36
  );
37
37
  }
@@ -1,9 +1,9 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
2
  import { useKeyboard } from "../../hooks/useKeyboard.js";
3
3
  const variantConfig = {
4
- info: { icon: "ℹ", color: "cyan" },
5
- success: { icon: "✓", color: "green" },
6
- error: { icon: "✗", color: "red" },
4
+ info: { icon: "ℹ", color: "#60A5FA" },
5
+ success: { icon: "✓", color: "#4ADE80" },
6
+ error: { icon: "✗", color: "#F87171" },
7
7
  };
8
8
  export function MessageModal({ title, message, variant, onDismiss, }) {
9
9
  const config = variantConfig[variant];
@@ -11,6 +11,6 @@ export function MessageModal({ title, message, variant, onDismiss, }) {
11
11
  // Any key dismisses
12
12
  onDismiss();
13
13
  });
14
- return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: config.color, backgroundColor: "#1a1a2e", paddingLeft: 2, paddingRight: 2, paddingTop: 1, paddingBottom: 1, width: 60, children: [_jsxs("box", { children: [_jsxs("text", { fg: config.color, children: [config.icon, " "] }), _jsx("text", { children: _jsx("strong", { children: title }) })] }), _jsx("box", { marginTop: 1, marginBottom: 1, children: _jsx("text", { children: message }) }), _jsx("box", { children: _jsx("text", { fg: "#666666", children: "Press any key to continue" }) })] }));
14
+ return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: "#525252", backgroundColor: "#1C1C1E", paddingLeft: 3, paddingRight: 3, paddingTop: 1, paddingBottom: 1, width: 60, children: [_jsxs("box", { marginBottom: 1, children: [_jsxs("text", { fg: config.color, children: [config.icon, " "] }), _jsx("text", { fg: "#EDEDED", children: _jsx("strong", { children: title }) })] }), _jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#A1A1AA", children: message }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "#71717A", children: "Press any key to continue" }) })] }));
15
15
  }
16
16
  export default MessageModal;
@@ -13,9 +13,9 @@ interface MessageModalProps {
13
13
  }
14
14
 
15
15
  const variantConfig = {
16
- info: { icon: "ℹ", color: "cyan" },
17
- success: { icon: "✓", color: "green" },
18
- error: { icon: "✗", color: "red" },
16
+ info: { icon: "ℹ", color: "#60A5FA" },
17
+ success: { icon: "✓", color: "#4ADE80" },
18
+ error: { icon: "✗", color: "#F87171" },
19
19
  } as const;
20
20
 
21
21
  export function MessageModal({
@@ -36,27 +36,27 @@ export function MessageModal({
36
36
  flexDirection="column"
37
37
  border
38
38
  borderStyle="rounded"
39
- borderColor={config.color}
40
- backgroundColor="#1a1a2e"
41
- paddingLeft={2}
42
- paddingRight={2}
39
+ borderColor="#525252"
40
+ backgroundColor="#1C1C1E"
41
+ paddingLeft={3}
42
+ paddingRight={3}
43
43
  paddingTop={1}
44
44
  paddingBottom={1}
45
45
  width={60}
46
46
  >
47
- <box>
47
+ <box marginBottom={1}>
48
48
  <text fg={config.color}>{config.icon} </text>
49
- <text>
49
+ <text fg="#EDEDED">
50
50
  <strong>{title}</strong>
51
51
  </text>
52
52
  </box>
53
53
 
54
- <box marginTop={1} marginBottom={1}>
55
- <text>{message}</text>
54
+ <box marginBottom={1}>
55
+ <text fg="#A1A1AA">{message}</text>
56
56
  </box>
57
57
 
58
- <box>
59
- <text fg="#666666">Press any key to continue</text>
58
+ <box marginTop={1}>
59
+ <text fg="#71717A">Press any key to continue</text>
60
60
  </box>
61
61
  </box>
62
62
  );
@@ -11,6 +11,9 @@ import { LoadingModal } from "./LoadingModal.js";
11
11
  * Container that renders the active modal as an overlay
12
12
  * Handles ALL keyboard events when a modal is open to avoid
13
13
  * conflicts with multiple useKeyboard hooks in child components
14
+ *
15
+ * Input modal: Enter/Escape handled by OpenTUI <input> onSubmit + ModalContainer escape
16
+ * Select modal: keyboard fully handled here (index tracking + Enter/arrows)
14
17
  */
15
18
  export function ModalContainer() {
16
19
  const { state } = useApp();
@@ -25,14 +28,14 @@ export function ModalContainer() {
25
28
  setSelectIndex(modal.defaultIndex ?? 0);
26
29
  }
27
30
  }
28
- // Handle ALL keyboard events for modals
31
+ // Handle keyboard events for modals
29
32
  useKeyboard((key) => {
30
33
  if (!modal)
31
34
  return;
32
35
  if (modal.type === "loading")
33
36
  return;
34
37
  // Escape — close any modal
35
- if (key.name === "escape" || key.name === "q") {
38
+ if (key.name === "escape") {
36
39
  if (modal.type === "confirm")
37
40
  modal.onCancel();
38
41
  else if (modal.type === "input")
@@ -43,6 +46,20 @@ export function ModalContainer() {
43
46
  modal.onDismiss();
44
47
  return;
45
48
  }
49
+ // 'q' to close — but NOT for input modals (need to type 'q')
50
+ if (key.name === "q" && modal.type !== "input") {
51
+ if (modal.type === "confirm")
52
+ modal.onCancel();
53
+ else if (modal.type === "select")
54
+ modal.onCancel();
55
+ else if (modal.type === "message")
56
+ modal.onDismiss();
57
+ return;
58
+ }
59
+ // Input modal — let OpenTUI <input> handle Enter via onSubmit
60
+ if (modal.type === "input") {
61
+ return;
62
+ }
46
63
  // Select modal — handle navigation and selection
47
64
  if (modal.type === "select") {
48
65
  if (key.name === "return" || key.name === "enter") {
@@ -62,6 +79,12 @@ export function ModalContainer() {
62
79
  modal.onDismiss();
63
80
  }
64
81
  }
82
+ // Confirm modal — Enter to confirm
83
+ if (modal.type === "confirm") {
84
+ if (key.name === "return" || key.name === "enter") {
85
+ modal.onConfirm();
86
+ }
87
+ }
65
88
  });
66
89
  if (!modal) {
67
90
  return null;
@@ -11,6 +11,9 @@ import { LoadingModal } from "./LoadingModal.js";
11
11
  * Container that renders the active modal as an overlay
12
12
  * Handles ALL keyboard events when a modal is open to avoid
13
13
  * conflicts with multiple useKeyboard hooks in child components
14
+ *
15
+ * Input modal: Enter/Escape handled by OpenTUI <input> onSubmit + ModalContainer escape
16
+ * Select modal: keyboard fully handled here (index tracking + Enter/arrows)
14
17
  */
15
18
  export function ModalContainer() {
16
19
  const { state } = useApp();
@@ -28,13 +31,13 @@ export function ModalContainer() {
28
31
  }
29
32
  }
30
33
 
31
- // Handle ALL keyboard events for modals
34
+ // Handle keyboard events for modals
32
35
  useKeyboard((key) => {
33
36
  if (!modal) return;
34
37
  if (modal.type === "loading") return;
35
38
 
36
39
  // Escape — close any modal
37
- if (key.name === "escape" || key.name === "q") {
40
+ if (key.name === "escape") {
38
41
  if (modal.type === "confirm") modal.onCancel();
39
42
  else if (modal.type === "input") modal.onCancel();
40
43
  else if (modal.type === "select") modal.onCancel();
@@ -42,6 +45,19 @@ export function ModalContainer() {
42
45
  return;
43
46
  }
44
47
 
48
+ // 'q' to close — but NOT for input modals (need to type 'q')
49
+ if (key.name === "q" && modal.type !== "input") {
50
+ if (modal.type === "confirm") modal.onCancel();
51
+ else if (modal.type === "select") modal.onCancel();
52
+ else if (modal.type === "message") modal.onDismiss();
53
+ return;
54
+ }
55
+
56
+ // Input modal — let OpenTUI <input> handle Enter via onSubmit
57
+ if (modal.type === "input") {
58
+ return;
59
+ }
60
+
45
61
  // Select modal — handle navigation and selection
46
62
  if (modal.type === "select") {
47
63
  if (key.name === "return" || key.name === "enter") {
@@ -60,6 +76,13 @@ export function ModalContainer() {
60
76
  modal.onDismiss();
61
77
  }
62
78
  }
79
+
80
+ // Confirm modal — Enter to confirm
81
+ if (modal.type === "confirm") {
82
+ if (key.name === "return" || key.name === "enter") {
83
+ modal.onConfirm();
84
+ }
85
+ }
63
86
  });
64
87
 
65
88
  if (!modal) {
@@ -3,10 +3,9 @@ export function SelectModal({ title, message, options, defaultIndex, onSelect: _
3
3
  // Keyboard handling is done by ModalContainer
4
4
  // defaultIndex is the live selectedIndex from ModalContainer state
5
5
  const selectedIndex = defaultIndex ?? 0;
6
- return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: "cyan", backgroundColor: "#1a1a2e", paddingLeft: 2, paddingRight: 2, paddingTop: 1, paddingBottom: 1, width: 50, children: [_jsx("text", { children: _jsx("strong", { children: title }) }), _jsx("box", { marginTop: 1, marginBottom: 1, children: _jsx("text", { children: message }) }), _jsx("box", { flexDirection: "column", children: options.map((option, idx) => {
6
+ return (_jsxs("box", { flexDirection: "column", border: true, borderStyle: "rounded", borderColor: "#525252", backgroundColor: "#1C1C1E", paddingLeft: 3, paddingRight: 3, paddingTop: 1, paddingBottom: 1, width: 50, children: [_jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#EDEDED", children: _jsx("strong", { children: title }) }) }), _jsx("box", { marginBottom: 1, children: _jsx("text", { fg: "#A1A1AA", children: message }) }), _jsx("box", { flexDirection: "column", paddingLeft: 1, children: options.map((option, idx) => {
7
7
  const isSelected = idx === selectedIndex;
8
- const label = isSelected ? `> ${option.label}` : ` ${option.label}`;
9
- return (_jsxs("text", { fg: isSelected ? "cyan" : "#666666", children: [isSelected && _jsx("strong", { children: label }), !isSelected && label] }, option.value));
10
- }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "#666666", children: "\u2191\u2193 Select \u2022 Enter \u2022 Esc" }) })] }));
8
+ return (_jsxs("text", { fg: isSelected ? "#F4F4F5" : "#A1A1AA", children: [_jsx("span", { fg: isSelected ? "#F4F4F5" : "#71717A", children: isSelected ? "❯ " : " " }), isSelected ? _jsx("strong", { children: option.label }) : option.label] }, option.value));
9
+ }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "#71717A", children: "\u2191\u2193 Select \u2022 \u21B5 Confirm \u2022 Esc Cancel" }) })] }));
11
10
  }
12
11
  export default SelectModal;
@@ -33,37 +33,40 @@ export function SelectModal({
33
33
  flexDirection="column"
34
34
  border
35
35
  borderStyle="rounded"
36
- borderColor="cyan"
37
- backgroundColor="#1a1a2e"
38
- paddingLeft={2}
39
- paddingRight={2}
36
+ borderColor="#525252"
37
+ backgroundColor="#1C1C1E"
38
+ paddingLeft={3}
39
+ paddingRight={3}
40
40
  paddingTop={1}
41
41
  paddingBottom={1}
42
42
  width={50}
43
43
  >
44
- <text>
45
- <strong>{title}</strong>
46
- </text>
44
+ <box marginBottom={1}>
45
+ <text fg="#EDEDED">
46
+ <strong>{title}</strong>
47
+ </text>
48
+ </box>
47
49
 
48
- <box marginTop={1} marginBottom={1}>
49
- <text>{message}</text>
50
+ <box marginBottom={1}>
51
+ <text fg="#A1A1AA">{message}</text>
50
52
  </box>
51
53
 
52
- <box flexDirection="column">
54
+ <box flexDirection="column" paddingLeft={1}>
53
55
  {options.map((option, idx) => {
54
56
  const isSelected = idx === selectedIndex;
55
- const label = isSelected ? `> ${option.label}` : ` ${option.label}`;
56
57
  return (
57
- <text key={option.value} fg={isSelected ? "cyan" : "#666666"}>
58
- {isSelected && <strong>{label}</strong>}
59
- {!isSelected && label}
58
+ <text key={option.value} fg={isSelected ? "#F4F4F5" : "#A1A1AA"}>
59
+ <span fg={isSelected ? "#F4F4F5" : "#71717A"}>
60
+ {isSelected ? "❯ " : " "}
61
+ </span>
62
+ {isSelected ? <strong>{option.label}</strong> : option.label}
60
63
  </text>
61
64
  );
62
65
  })}
63
66
  </box>
64
67
 
65
68
  <box marginTop={1}>
66
- <text fg="#666666">↑↓ Select • Enter • Esc</text>
69
+ <text fg="#71717A">↑↓ Select • Confirm • Esc Cancel</text>
67
70
  </box>
68
71
  </box>
69
72
  );
@@ -8,7 +8,7 @@ const categoryRenderer = {
8
8
  const star = item.category === "recommended" ? "★ " : "";
9
9
  const label = `${star}${CATEGORY_LABELS[item.category]}`;
10
10
  const bg = isSelected ? theme.selection.bg : CATEGORY_BG[item.category];
11
- return (_jsx("text", { bg: bg, fg: "white", children: _jsxs("strong", { children: [" ", label, " "] }) }));
11
+ return (_jsx("box", { width: "100%", children: _jsx("text", { bg: bg, fg: "white", children: _jsxs("strong", { children: [" ", label, " "] }) }) }));
12
12
  },
13
13
  renderDetail: ({ item }) => {
14
14
  const cat = item.category;
@@ -24,9 +24,11 @@ const categoryRenderer: ItemRenderer<SettingsCategoryItem> = {
24
24
  const bg = isSelected ? theme.selection.bg : CATEGORY_BG[item.category];
25
25
 
26
26
  return (
27
- <text bg={bg} fg="white">
28
- <strong> {label} </strong>
29
- </text>
27
+ <box width="100%">
28
+ <text bg={bg} fg="white">
29
+ <strong> {label} </strong>
30
+ </text>
31
+ </box>
30
32
  );
31
33
  },
32
34
 
@@ -6,9 +6,9 @@ import { useApp, useModal } from "../state/AppContext.js";
6
6
  import { useDimensions } from "../state/DimensionsContext.js";
7
7
  import { useKeyboard } from "../hooks/useKeyboard.js";
8
8
  import { ScreenLayout } from "../components/layout/index.js";
9
- import { ScrollableList } from "../components/ScrollableList.js";
10
9
  import { cliTools } from "../../data/cli-tools.js";
11
10
  import { renderCliToolRow, renderCliToolDetail, } from "../renderers/cliToolRenderers.js";
11
+ import { ScrollableList } from "../components/ScrollableList.js";
12
12
  const execAsync = promisify(exec);
13
13
  // ─── Version helpers ───────────────────────────────────────────────────────────
14
14
  // Session-level cache
@@ -328,6 +328,6 @@ export function CliToolsScreen() {
328
328
  const installedCount = toolStatuses.filter((s) => s.installed).length;
329
329
  const updateCount = toolStatuses.filter((s) => s.hasUpdate).length;
330
330
  const statusContent = (_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Installed: " }), _jsxs("span", { fg: "cyan", children: [installedCount, "/", toolStatuses.length] }), updateCount > 0 && (_jsxs(_Fragment, { children: [_jsx("span", { fg: "gray", children: " \u2502 Updates: " }), _jsx("span", { fg: "yellow", children: updateCount })] }))] }));
331
- return (_jsx(ScreenLayout, { title: "claudeup CLI Tools", currentScreen: "cli-tools", statusLine: statusContent, footerHints: "Enter:install \u2502 a:update all \u2502 c:fix conflict \u2502 r:refresh", listPanel: _jsx(ScrollableList, { items: toolStatuses, selectedIndex: cliToolsState.selectedIndex, renderItem: renderCliToolRow, maxHeight: dimensions.listPanelHeight }), detailPanel: renderCliToolDetail(selectedStatus) }));
331
+ return (_jsx(ScreenLayout, { title: "claudeup CLI Tools", currentScreen: "cli-tools", statusLine: statusContent, footerHints: "Enter:install \u2502 a:update all \u2502 c:fix conflict \u2502 r:refresh", listPanel: _jsx(ScrollableList, { items: toolStatuses, selectedIndex: cliToolsState.selectedIndex, renderItem: renderCliToolRow, maxHeight: dimensions.listPanelHeight, getKey: (status) => status.tool.name }), detailPanel: renderCliToolDetail(selectedStatus) }));
332
332
  }
333
333
  export default CliToolsScreen;
@@ -5,7 +5,7 @@ import { useApp, useModal } from "../state/AppContext.js";
5
5
  import { useDimensions } from "../state/DimensionsContext.js";
6
6
  import { useKeyboard } from "../hooks/useKeyboard.js";
7
7
  import { ScreenLayout } from "../components/layout/index.js";
8
- import { ScrollableList } from "../components/ScrollableList.js";
8
+
9
9
  import { cliTools } from "../../data/cli-tools.js";
10
10
  import {
11
11
  renderCliToolRow,
@@ -13,6 +13,7 @@ import {
13
13
  type CliToolStatus,
14
14
  type InstallMethod,
15
15
  } from "../renderers/cliToolRenderers.js";
16
+ import { ScrollableList } from "../components/ScrollableList.js";
16
17
 
17
18
  const execAsync = promisify(exec);
18
19
 
@@ -423,6 +424,7 @@ export function CliToolsScreen() {
423
424
  selectedIndex={cliToolsState.selectedIndex}
424
425
  renderItem={renderCliToolRow}
425
426
  maxHeight={dimensions.listPanelHeight}
427
+ getKey={(status) => status.tool.name}
426
428
  />
427
429
  }
428
430
  detailPanel={renderCliToolDetail(selectedStatus)}
@@ -4,11 +4,11 @@ import { useApp, useModal } from "../state/AppContext.js";
4
4
  import { useDimensions } from "../state/DimensionsContext.js";
5
5
  import { useKeyboard } from "../hooks/useKeyboard.js";
6
6
  import { ScreenLayout } from "../components/layout/index.js";
7
- import { ScrollableList } from "../components/ScrollableList.js";
8
7
  import { SETTINGS_CATALOG, } from "../../data/settings-catalog.js";
9
- import { readAllSettingsBothScopes, writeSettingValue, } from "../../services/settings-manager.js";
8
+ import { readAllSettingsBothScopes, writeSettingValue, discoverOutputStyles, } from "../../services/settings-manager.js";
10
9
  import { buildSettingsBrowserItems, } from "../adapters/settingsAdapter.js";
11
10
  import { renderSettingRow, renderSettingDetail } from "../renderers/settingsRenderers.js";
11
+ import { ScrollableList } from "../components/ScrollableList.js";
12
12
  export function SettingsScreen() {
13
13
  const { state, dispatch } = useApp();
14
14
  const { settings } = state;
@@ -17,6 +17,11 @@ export function SettingsScreen() {
17
17
  const fetchData = useCallback(async () => {
18
18
  dispatch({ type: "SETTINGS_DATA_LOADING" });
19
19
  try {
20
+ // Populate dynamic output style options from installed plugins
21
+ const outputStyleSetting = SETTINGS_CATALOG.find((s) => s.id === "output-style");
22
+ if (outputStyleSetting && outputStyleSetting.type === "select") {
23
+ outputStyleSetting.options = await discoverOutputStyles(state.projectPath);
24
+ }
20
25
  const values = await readAllSettingsBothScopes(SETTINGS_CATALOG, state.projectPath);
21
26
  dispatch({ type: "SETTINGS_DATA_SUCCESS", values });
22
27
  }
@@ -76,15 +81,27 @@ export function SettingsScreen() {
76
81
  }
77
82
  }
78
83
  else {
79
- const newValue = await modal.input(`${setting.name} ${scope}`, setting.description, currentValue || "");
80
- if (newValue === null)
81
- return;
82
- try {
83
- await writeSettingValue(setting, newValue || undefined, scope, state.projectPath);
84
- await fetchData();
84
+ // String type: if already set, clear it; if unset, show input modal
85
+ if (currentValue !== undefined && currentValue !== "") {
86
+ try {
87
+ await writeSettingValue(setting, undefined, scope, state.projectPath);
88
+ await fetchData();
89
+ }
90
+ catch (error) {
91
+ await modal.message("Error", `Failed to update: ${error}`, "error");
92
+ }
85
93
  }
86
- catch (error) {
87
- await modal.message("Error", `Failed to update: ${error}`, "error");
94
+ else {
95
+ const newValue = await modal.input(`${setting.name} ${scope}`, setting.description, currentValue || setting.defaultValue || "");
96
+ if (newValue === null)
97
+ return;
98
+ try {
99
+ await writeSettingValue(setting, newValue || undefined, scope, state.projectPath);
100
+ await fetchData();
101
+ }
102
+ catch (error) {
103
+ await modal.message("Error", `Failed to update: ${error}`, "error");
104
+ }
88
105
  }
89
106
  }
90
107
  };
@@ -3,19 +3,20 @@ import { useApp, useModal } from "../state/AppContext.js";
3
3
  import { useDimensions } from "../state/DimensionsContext.js";
4
4
  import { useKeyboard } from "../hooks/useKeyboard.js";
5
5
  import { ScreenLayout } from "../components/layout/index.js";
6
- import { ScrollableList } from "../components/ScrollableList.js";
7
6
  import {
8
7
  SETTINGS_CATALOG,
9
8
  } from "../../data/settings-catalog.js";
10
9
  import {
11
10
  readAllSettingsBothScopes,
12
11
  writeSettingValue,
12
+ discoverOutputStyles,
13
13
  } from "../../services/settings-manager.js";
14
14
  import {
15
15
  buildSettingsBrowserItems,
16
16
  type SettingsBrowserItem,
17
17
  } from "../adapters/settingsAdapter.js";
18
18
  import { renderSettingRow, renderSettingDetail } from "../renderers/settingsRenderers.js";
19
+ import { ScrollableList } from "../components/ScrollableList.js";
19
20
 
20
21
  export function SettingsScreen() {
21
22
  const { state, dispatch } = useApp();
@@ -26,6 +27,12 @@ export function SettingsScreen() {
26
27
  const fetchData = useCallback(async () => {
27
28
  dispatch({ type: "SETTINGS_DATA_LOADING" });
28
29
  try {
30
+ // Populate dynamic output style options from installed plugins
31
+ const outputStyleSetting = SETTINGS_CATALOG.find((s) => s.id === "output-style");
32
+ if (outputStyleSetting && outputStyleSetting.type === "select") {
33
+ outputStyleSetting.options = await discoverOutputStyles(state.projectPath);
34
+ }
35
+
29
36
  const values = await readAllSettingsBothScopes(
30
37
  SETTINGS_CATALOG,
31
38
  state.projectPath,
@@ -95,22 +102,32 @@ export function SettingsScreen() {
95
102
  await modal.message("Error", `Failed to update: ${error}`, "error");
96
103
  }
97
104
  } else {
98
- const newValue = await modal.input(
99
- `${setting.name} ${scope}`,
100
- setting.description,
101
- currentValue || "",
102
- );
103
- if (newValue === null) return;
104
- try {
105
- await writeSettingValue(
106
- setting,
107
- newValue || undefined,
108
- scope,
109
- state.projectPath,
105
+ // String type: if already set, clear it; if unset, show input modal
106
+ if (currentValue !== undefined && currentValue !== "") {
107
+ try {
108
+ await writeSettingValue(setting, undefined, scope, state.projectPath);
109
+ await fetchData();
110
+ } catch (error) {
111
+ await modal.message("Error", `Failed to update: ${error}`, "error");
112
+ }
113
+ } else {
114
+ const newValue = await modal.input(
115
+ `${setting.name} — ${scope}`,
116
+ setting.description,
117
+ currentValue || setting.defaultValue || "",
110
118
  );
111
- await fetchData();
112
- } catch (error) {
113
- await modal.message("Error", `Failed to update: ${error}`, "error");
119
+ if (newValue === null) return;
120
+ try {
121
+ await writeSettingValue(
122
+ setting,
123
+ newValue || undefined,
124
+ scope,
125
+ state.projectPath,
126
+ );
127
+ await fetchData();
128
+ } catch (error) {
129
+ await modal.message("Error", `Failed to update: ${error}`, "error");
130
+ }
114
131
  }
115
132
  }
116
133
  };
@@ -4,9 +4,9 @@ import { useApp, useModal, useNavigation } from "../state/AppContext.js";
4
4
  import { useDimensions } from "../state/DimensionsContext.js";
5
5
  import { useKeyboard } from "../hooks/useKeyboard.js";
6
6
  import { ScreenLayout } from "../components/layout/index.js";
7
- import { ScrollableList } from "../components/ScrollableList.js";
8
7
  import { searchMcpServers, formatDate } from "../../services/mcp-registry.js";
9
8
  import { addMcpServer, setAllowMcp } from "../../services/claude-settings.js";
9
+ import { ScrollableList } from "../components/ScrollableList.js";
10
10
  /**
11
11
  * Deduplicate servers by name, keeping only the latest version.
12
12
  * Uses version string comparison, falling back to published_at date.
@@ -222,6 +222,6 @@ export function McpRegistryScreen() {
222
222
  isActive: isSearchActive,
223
223
  query: searchQuery,
224
224
  placeholder: searchPlaceholder,
225
- }, footerHints: footerHints, listPanel: isLoading ? (_jsx("text", { fg: "gray", children: "Loading..." })) : error ? (_jsxs("text", { fg: "red", children: ["Error: ", error] })) : servers.length === 0 ? (_jsx("text", { fg: "gray", children: "No servers found" })) : (_jsx(ScrollableList, { items: servers, selectedIndex: mcpRegistry.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight })), detailPanel: renderDetail() }));
225
+ }, footerHints: footerHints, listPanel: isLoading ? (_jsx("text", { fg: "gray", children: "Loading..." })) : error ? (_jsxs("text", { fg: "red", children: ["Error: ", error] })) : servers.length === 0 ? (_jsx("text", { fg: "gray", children: "No servers found" })) : (_jsx(ScrollableList, { items: servers, selectedIndex: mcpRegistry.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight, getKey: (server) => server.name })), detailPanel: renderDetail() }));
226
226
  }
227
227
  export default McpRegistryScreen;
@@ -3,10 +3,11 @@ import { useApp, useModal, useNavigation } from "../state/AppContext.js";
3
3
  import { useDimensions } from "../state/DimensionsContext.js";
4
4
  import { useKeyboard } from "../hooks/useKeyboard.js";
5
5
  import { ScreenLayout } from "../components/layout/index.js";
6
- import { ScrollableList } from "../components/ScrollableList.js";
6
+
7
7
  import { searchMcpServers, formatDate } from "../../services/mcp-registry.js";
8
8
  import { addMcpServer, setAllowMcp } from "../../services/claude-settings.js";
9
9
  import type { McpRegistryServer, McpServerConfig } from "../../types/index.js";
10
+ import { ScrollableList } from "../components/ScrollableList.js";
10
11
 
11
12
  /**
12
13
  * Deduplicate servers by name, keeping only the latest version.
@@ -332,6 +333,7 @@ export function McpRegistryScreen() {
332
333
  selectedIndex={mcpRegistry.selectedIndex}
333
334
  renderItem={renderListItem}
334
335
  maxHeight={dimensions.listPanelHeight}
336
+ getKey={(server) => server.name}
335
337
  />
336
338
  )
337
339
  }
@@ -4,10 +4,10 @@ import { useApp, useModal, useNavigation } from "../state/AppContext.js";
4
4
  import { useDimensions } from "../state/DimensionsContext.js";
5
5
  import { useKeyboard } from "../hooks/useKeyboard.js";
6
6
  import { ScreenLayout } from "../components/layout/index.js";
7
- import { ScrollableList } from "../components/ScrollableList.js";
8
7
  import { getMcpServersByCategory, getCategoryDisplayName, categoryOrder, } from "../../data/mcp-servers.js";
9
8
  import { addMcpServer, removeMcpServer, getInstalledMcpServers, getEnabledMcpServers, } from "../../services/claude-settings.js";
10
9
  import { renderMcpRow, renderMcpDetail, } from "../renderers/mcpRenderers.js";
10
+ import { ScrollableList } from "../components/ScrollableList.js";
11
11
  export function McpScreen() {
12
12
  const { state, dispatch } = useApp();
13
13
  const { mcp } = state;