claudeup 3.7.2 → 3.8.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 (44) hide show
  1. package/package.json +1 -1
  2. package/src/data/settings-catalog.js +612 -0
  3. package/src/data/settings-catalog.ts +689 -0
  4. package/src/services/plugin-manager.js +2 -0
  5. package/src/services/plugin-manager.ts +3 -0
  6. package/src/services/profiles.js +161 -0
  7. package/src/services/profiles.ts +225 -0
  8. package/src/services/settings-manager.js +108 -0
  9. package/src/services/settings-manager.ts +140 -0
  10. package/src/types/index.ts +34 -0
  11. package/src/ui/App.js +17 -18
  12. package/src/ui/App.tsx +21 -23
  13. package/src/ui/components/TabBar.js +8 -8
  14. package/src/ui/components/TabBar.tsx +14 -19
  15. package/src/ui/components/layout/ScreenLayout.js +8 -14
  16. package/src/ui/components/layout/ScreenLayout.tsx +51 -58
  17. package/src/ui/components/modals/ModalContainer.js +43 -11
  18. package/src/ui/components/modals/ModalContainer.tsx +44 -12
  19. package/src/ui/components/modals/SelectModal.js +4 -18
  20. package/src/ui/components/modals/SelectModal.tsx +10 -21
  21. package/src/ui/screens/CliToolsScreen.js +2 -2
  22. package/src/ui/screens/CliToolsScreen.tsx +8 -8
  23. package/src/ui/screens/EnvVarsScreen.js +248 -116
  24. package/src/ui/screens/EnvVarsScreen.tsx +419 -184
  25. package/src/ui/screens/McpRegistryScreen.tsx +18 -6
  26. package/src/ui/screens/McpScreen.js +1 -1
  27. package/src/ui/screens/McpScreen.tsx +15 -5
  28. package/src/ui/screens/ModelSelectorScreen.js +3 -5
  29. package/src/ui/screens/ModelSelectorScreen.tsx +12 -16
  30. package/src/ui/screens/PluginsScreen.js +154 -66
  31. package/src/ui/screens/PluginsScreen.tsx +280 -97
  32. package/src/ui/screens/ProfilesScreen.js +255 -0
  33. package/src/ui/screens/ProfilesScreen.tsx +487 -0
  34. package/src/ui/screens/StatusLineScreen.js +2 -2
  35. package/src/ui/screens/StatusLineScreen.tsx +10 -12
  36. package/src/ui/screens/index.js +2 -2
  37. package/src/ui/screens/index.ts +2 -2
  38. package/src/ui/state/AppContext.js +2 -1
  39. package/src/ui/state/AppContext.tsx +2 -0
  40. package/src/ui/state/reducer.js +63 -19
  41. package/src/ui/state/reducer.ts +68 -19
  42. package/src/ui/state/types.ts +33 -14
  43. package/src/utils/clipboard.js +56 -0
  44. package/src/utils/clipboard.ts +58 -0
@@ -1,4 +1,4 @@
1
- import React from "react";
1
+ import React, { useState } from "react";
2
2
  import { useApp } from "../../state/AppContext.js";
3
3
  import { useKeyboard } from "../../hooks/useKeyboard.js";
4
4
  import { ConfirmModal } from "./ConfirmModal.js";
@@ -9,23 +9,54 @@ import { LoadingModal } from "./LoadingModal.js";
9
9
 
10
10
  /**
11
11
  * Container that renders the active modal as an overlay
12
- * Handles global Escape key to close modals
12
+ * Handles ALL keyboard events when a modal is open to avoid
13
+ * conflicts with multiple useKeyboard hooks in child components
13
14
  */
14
15
  export function ModalContainer() {
15
16
  const { state } = useApp();
16
17
  const { modal } = state;
17
18
 
18
- // Handle Escape key to close modal (except loading)
19
+ // Track select modal index here (lifted from SelectModal)
20
+ const [selectIndex, setSelectIndex] = useState(0);
21
+
22
+ // Reset select index when modal changes
23
+ const modalRef = React.useRef(modal);
24
+ if (modal !== modalRef.current) {
25
+ modalRef.current = modal;
26
+ if (modal?.type === "select") {
27
+ setSelectIndex(modal.defaultIndex ?? 0);
28
+ }
29
+ }
30
+
31
+ // Handle ALL keyboard events for modals
19
32
  useKeyboard((key) => {
20
- if (key.name === "escape" && modal && modal.type !== "loading") {
21
- // Loading modal cannot be dismissed with Escape
22
- if (modal.type === "confirm") {
23
- modal.onCancel();
24
- } else if (modal.type === "input") {
25
- modal.onCancel();
26
- } else if (modal.type === "select") {
27
- modal.onCancel();
28
- } else if (modal.type === "message") {
33
+ if (!modal) return;
34
+ if (modal.type === "loading") return;
35
+
36
+ // Escape — close any modal
37
+ if (key.name === "escape" || key.name === "q") {
38
+ if (modal.type === "confirm") modal.onCancel();
39
+ else if (modal.type === "input") modal.onCancel();
40
+ else if (modal.type === "select") modal.onCancel();
41
+ else if (modal.type === "message") modal.onDismiss();
42
+ return;
43
+ }
44
+
45
+ // Select modal — handle navigation and selection
46
+ if (modal.type === "select") {
47
+ if (key.name === "return" || key.name === "enter") {
48
+ modal.onSelect(modal.options[selectIndex].value);
49
+ } else if (key.name === "up" || key.name === "k") {
50
+ setSelectIndex((prev) => Math.max(0, prev - 1));
51
+ } else if (key.name === "down" || key.name === "j") {
52
+ setSelectIndex((prev) => Math.min(modal.options.length - 1, prev + 1));
53
+ }
54
+ return;
55
+ }
56
+
57
+ // Message modal — Enter to dismiss
58
+ if (modal.type === "message") {
59
+ if (key.name === "return" || key.name === "enter") {
29
60
  modal.onDismiss();
30
61
  }
31
62
  }
@@ -64,6 +95,7 @@ export function ModalContainer() {
64
95
  title={modal.title}
65
96
  message={modal.message}
66
97
  options={modal.options}
98
+ defaultIndex={selectIndex}
67
99
  onSelect={modal.onSelect}
68
100
  onCancel={modal.onCancel}
69
101
  />
@@ -1,22 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
2
- import { useState } from "react";
3
- import { useKeyboard } from "../../hooks/useKeyboard.js";
4
- export function SelectModal({ title, message, options, onSelect, onCancel, }) {
5
- const [selectedIndex, setSelectedIndex] = useState(0);
6
- useKeyboard((key) => {
7
- if (key.name === "enter") {
8
- onSelect(options[selectedIndex].value);
9
- }
10
- else if (key.name === "escape" || key.name === "q") {
11
- onCancel();
12
- }
13
- else if (key.name === "up" || key.name === "k") {
14
- setSelectedIndex((prev) => Math.max(0, prev - 1));
15
- }
16
- else if (key.name === "down" || key.name === "j") {
17
- setSelectedIndex((prev) => Math.min(options.length - 1, prev + 1));
18
- }
19
- });
2
+ export function SelectModal({ title, message, options, defaultIndex, onSelect: _onSelect, onCancel: _onCancel, }) {
3
+ // Keyboard handling is done by ModalContainer
4
+ // defaultIndex is the live selectedIndex from ModalContainer state
5
+ const selectedIndex = defaultIndex ?? 0;
20
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) => {
21
7
  const isSelected = idx === selectedIndex;
22
8
  const label = isSelected ? `> ${option.label}` : ` ${option.label}`;
@@ -1,5 +1,4 @@
1
- import React, { useState } from "react";
2
- import { useKeyboard } from "../../hooks/useKeyboard.js";
1
+ import React from "react";
3
2
  import type { SelectOption } from "../../state/types.js";
4
3
 
5
4
  interface SelectModalProps {
@@ -9,6 +8,8 @@ interface SelectModalProps {
9
8
  message: string;
10
9
  /** Select options */
11
10
  options: SelectOption[];
11
+ /** Currently selected index (controlled by ModalContainer) */
12
+ defaultIndex?: number;
12
13
  /** Callback when option selected */
13
14
  onSelect: (value: string) => void;
14
15
  /** Callback when cancelled */
@@ -19,22 +20,13 @@ export function SelectModal({
19
20
  title,
20
21
  message,
21
22
  options,
22
- onSelect,
23
- onCancel,
23
+ defaultIndex,
24
+ onSelect: _onSelect,
25
+ onCancel: _onCancel,
24
26
  }: SelectModalProps) {
25
- const [selectedIndex, setSelectedIndex] = useState(0);
26
-
27
- useKeyboard((key) => {
28
- if (key.name === "enter") {
29
- onSelect(options[selectedIndex].value);
30
- } else if (key.name === "escape" || key.name === "q") {
31
- onCancel();
32
- } else if (key.name === "up" || key.name === "k") {
33
- setSelectedIndex((prev) => Math.max(0, prev - 1));
34
- } else if (key.name === "down" || key.name === "j") {
35
- setSelectedIndex((prev) => Math.min(options.length - 1, prev + 1));
36
- }
37
- });
27
+ // Keyboard handling is done by ModalContainer
28
+ // defaultIndex is the live selectedIndex from ModalContainer state
29
+ const selectedIndex = defaultIndex ?? 0;
38
30
 
39
31
  return (
40
32
  <box
@@ -62,10 +54,7 @@ export function SelectModal({
62
54
  const isSelected = idx === selectedIndex;
63
55
  const label = isSelected ? `> ${option.label}` : ` ${option.label}`;
64
56
  return (
65
- <text
66
- key={option.value}
67
- fg={isSelected ? "cyan" : "#666666"}
68
- >
57
+ <text key={option.value} fg={isSelected ? "cyan" : "#666666"}>
69
58
  {isSelected && <strong>{label}</strong>}
70
59
  {!isSelected && label}
71
60
  </text>
@@ -244,7 +244,7 @@ export function CliToolsScreen() {
244
244
  return (_jsx("box", { flexDirection: "column", alignItems: "center", justifyContent: "center", flexGrow: 1, children: _jsx("text", { fg: "gray", children: "Select a tool to see details" }) }));
245
245
  }
246
246
  const { tool, installed, installedVersion, latestVersion, hasUpdate, checking, } = selectedStatus;
247
- return (_jsxs("box", { flexDirection: "column", children: [_jsxs("box", { marginBottom: 1, children: [_jsx("text", { fg: "cyan", children: _jsxs("strong", { children: ["\u2699 ", tool.displayName] }) }), hasUpdate && _jsx("text", { fg: "yellow", children: " \u2B06" })] }), _jsx("text", { fg: "gray", children: tool.description }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("box", { children: [_jsx("text", { fg: "gray", children: "Status " }), !installed ? (_jsx("text", { fg: "gray", children: "\u25CB Not installed" })) : checking ? (_jsx("text", { fg: "green", children: "\u25CF Checking..." })) : hasUpdate ? (_jsx("text", { fg: "yellow", children: "\u25CF Update available" })) : (_jsx("text", { fg: "green", children: "\u25CF Up to date" }))] }), installedVersion && (_jsxs("box", { children: [_jsx("text", { fg: "gray", children: "Installed " }), _jsxs("text", { fg: "green", children: ["v", installedVersion] })] })), latestVersion && (_jsxs("box", { children: [_jsx("text", { fg: "gray", children: "Latest " }), _jsxs("text", { fg: "white", children: ["v", latestVersion] })] })), _jsxs("box", { children: [_jsx("text", { fg: "gray", children: "Website " }), _jsx("text", { fg: "blue", children: tool.website })] })] }), _jsx("box", { marginTop: 2, children: !installed ? (_jsxs("box", { children: [_jsxs("text", { bg: "green", fg: "black", children: [" ", "Enter", " "] }), _jsx("text", { fg: "gray", children: " Install" })] })) : hasUpdate ? (_jsxs("box", { children: [_jsxs("text", { bg: "yellow", fg: "black", children: [" ", "Enter", " "] }), _jsxs("text", { fg: "gray", children: [" Update to v", latestVersion] })] })) : (_jsxs("box", { children: [_jsxs("text", { bg: "gray", fg: "white", children: [" ", "Enter", " "] }), _jsx("text", { fg: "gray", children: " Reinstall" })] })) })] }));
247
+ return (_jsxs("box", { flexDirection: "column", children: [_jsxs("box", { marginBottom: 1, children: [_jsx("text", { fg: "cyan", children: _jsxs("strong", { children: ["\u2699 ", tool.displayName] }) }), hasUpdate && _jsx("text", { fg: "yellow", children: " \u2B06" })] }), _jsx("text", { fg: "gray", children: tool.description }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("box", { children: [_jsx("text", { fg: "gray", children: "Status " }), !installed ? (_jsx("text", { fg: "gray", children: "\u25CB Not installed" })) : checking ? (_jsx("text", { fg: "green", children: "\u25CF Checking..." })) : hasUpdate ? (_jsx("text", { fg: "yellow", children: "\u25CF Update available" })) : (_jsx("text", { fg: "green", children: "\u25CF Up to date" }))] }), installedVersion && (_jsxs("box", { children: [_jsx("text", { fg: "gray", children: "Installed " }), _jsxs("text", { fg: "green", children: ["v", installedVersion] })] })), latestVersion && (_jsxs("box", { children: [_jsx("text", { fg: "gray", children: "Latest " }), _jsxs("text", { fg: "white", children: ["v", latestVersion] })] })), _jsxs("box", { children: [_jsx("text", { fg: "gray", children: "Website " }), _jsx("text", { fg: "#5c9aff", children: tool.website })] })] }), _jsx("box", { marginTop: 2, children: !installed ? (_jsxs("box", { children: [_jsxs("text", { bg: "green", fg: "black", children: [" ", "Enter", " "] }), _jsx("text", { fg: "gray", children: " Install" })] })) : hasUpdate ? (_jsxs("box", { children: [_jsxs("text", { bg: "yellow", fg: "black", children: [" ", "Enter", " "] }), _jsxs("text", { fg: "gray", children: [" Update to v", latestVersion] })] })) : (_jsxs("box", { children: [_jsxs("text", { bg: "gray", fg: "white", children: [" ", "Enter", " "] }), _jsx("text", { fg: "gray", children: " Reinstall" })] })) })] }));
248
248
  };
249
249
  const renderListItem = (status, _idx, isSelected) => {
250
250
  const { tool, installed, installedVersion, hasUpdate, checking } = status;
@@ -268,7 +268,7 @@ export function CliToolsScreen() {
268
268
  // Calculate stats for status line
269
269
  const installedCount = toolStatuses.filter((s) => s.installed).length;
270
270
  const updateCount = toolStatuses.filter((s) => s.hasUpdate).length;
271
- const statusContent = (_jsxs(_Fragment, { children: [_jsx("text", { fg: "gray", children: "Installed: " }), _jsxs("text", { fg: "cyan", children: [installedCount, "/", toolStatuses.length] }), updateCount > 0 && (_jsxs(_Fragment, { children: [_jsx("text", { fg: "gray", children: " \u2502 Updates: " }), _jsx("text", { fg: "yellow", children: updateCount })] }))] }));
271
+ 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 })] }))] }));
272
272
  return (_jsx(ScreenLayout, { title: "claudeup CLI Tools", currentScreen: "cli-tools", statusLine: statusContent, footerHints: "\u2191\u2193:nav \u2502 Enter:install \u2502 a:update all \u2502 r:refresh", listPanel: _jsx(ScrollableList, { items: toolStatuses, selectedIndex: cliToolsState.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight }), detailPanel: renderDetail() }));
273
273
  }
274
274
  export default CliToolsScreen;
@@ -354,7 +354,7 @@ export function CliToolsScreen() {
354
354
  )}
355
355
  <box>
356
356
  <text fg="gray">Website </text>
357
- <text fg="blue">{tool.website}</text>
357
+ <text fg="#5c9aff">{tool.website}</text>
358
358
  </box>
359
359
  </box>
360
360
 
@@ -432,18 +432,18 @@ export function CliToolsScreen() {
432
432
  const installedCount = toolStatuses.filter((s) => s.installed).length;
433
433
  const updateCount = toolStatuses.filter((s) => s.hasUpdate).length;
434
434
  const statusContent = (
435
- <>
436
- <text fg="gray">Installed: </text>
437
- <text fg="cyan">
435
+ <text>
436
+ <span fg="gray">Installed: </span>
437
+ <span fg="cyan">
438
438
  {installedCount}/{toolStatuses.length}
439
- </text>
439
+ </span>
440
440
  {updateCount > 0 && (
441
441
  <>
442
- <text fg="gray"> │ Updates: </text>
443
- <text fg="yellow">{updateCount}</text>
442
+ <span fg="gray"> │ Updates: </span>
443
+ <span fg="yellow">{updateCount}</span>
444
444
  </>
445
445
  )}
446
- </>
446
+ </text>
447
447
  );
448
448
 
449
449
  return (
@@ -1,154 +1,286 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/react/jsx-runtime";
2
- import { useEffect, useCallback, useState } from "react";
1
+ import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
+ import { useEffect, useCallback, useMemo } from "react";
3
3
  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
7
  import { ScrollableList } from "../components/ScrollableList.js";
8
- import { getMcpEnvVars, setMcpEnvVar, removeMcpEnvVar, } from "../../services/claude-settings.js";
9
- export function EnvVarsScreen() {
8
+ import { SETTINGS_CATALOG, } from "../../data/settings-catalog.js";
9
+ import { readAllSettingsBothScopes, writeSettingValue, } from "../../services/settings-manager.js";
10
+ const CATEGORY_LABELS = {
11
+ recommended: "Recommended",
12
+ agents: "Agents & Teams",
13
+ models: "Models & Thinking",
14
+ workflow: "Workflow",
15
+ terminal: "Terminal & UI",
16
+ performance: "Performance",
17
+ advanced: "Advanced",
18
+ };
19
+ const CATEGORY_ORDER = [
20
+ "recommended",
21
+ "agents",
22
+ "models",
23
+ "workflow",
24
+ "terminal",
25
+ "performance",
26
+ "advanced",
27
+ ];
28
+ /** Get the effective value (project overrides user) */
29
+ function getEffectiveValue(scoped) {
30
+ return scoped.project !== undefined ? scoped.project : scoped.user;
31
+ }
32
+ function buildListItems(values) {
33
+ const items = [];
34
+ for (const category of CATEGORY_ORDER) {
35
+ items.push({
36
+ id: `cat:${category}`,
37
+ type: "category",
38
+ label: CATEGORY_LABELS[category],
39
+ category,
40
+ isDefault: true,
41
+ });
42
+ const categorySettings = SETTINGS_CATALOG.filter((s) => s.category === category);
43
+ for (const setting of categorySettings) {
44
+ const scoped = values.get(setting.id) || {
45
+ user: undefined,
46
+ project: undefined,
47
+ };
48
+ const effective = getEffectiveValue(scoped);
49
+ items.push({
50
+ id: `setting:${setting.id}`,
51
+ type: "setting",
52
+ label: setting.name,
53
+ category,
54
+ setting,
55
+ scopedValues: scoped,
56
+ effectiveValue: effective,
57
+ isDefault: effective === undefined || effective === "",
58
+ });
59
+ }
60
+ }
61
+ return items;
62
+ }
63
+ function formatValue(setting, value) {
64
+ if (value === undefined || value === "") {
65
+ if (setting.defaultValue !== undefined) {
66
+ return setting.type === "boolean"
67
+ ? setting.defaultValue === "true"
68
+ ? "on"
69
+ : "off"
70
+ : setting.defaultValue || "default";
71
+ }
72
+ return "—";
73
+ }
74
+ if (setting.type === "boolean") {
75
+ return value === "true" || value === "1" ? "on" : "off";
76
+ }
77
+ if (setting.type === "select" && setting.options) {
78
+ const opt = setting.options.find((o) => o.value === value);
79
+ return opt ? opt.label : value;
80
+ }
81
+ if (value.length > 20) {
82
+ return value.slice(0, 20) + "...";
83
+ }
84
+ return value;
85
+ }
86
+ export function SettingsScreen() {
10
87
  const { state, dispatch } = useApp();
11
- const { envVars } = state;
88
+ const { settings } = state;
12
89
  const modal = useModal();
13
90
  const dimensions = useDimensions();
14
- const [envVarList, setEnvVarList] = useState([]);
15
- const [isLoading, setIsLoading] = useState(true);
16
- // Fetch data
91
+ // Fetch data from both scopes
17
92
  const fetchData = useCallback(async () => {
18
- setIsLoading(true);
93
+ dispatch({ type: "SETTINGS_DATA_LOADING" });
19
94
  try {
20
- const vars = await getMcpEnvVars(state.projectPath);
21
- const list = Object.entries(vars).map(([name, value]) => ({
22
- name,
23
- value,
24
- }));
25
- setEnvVarList(list);
95
+ const values = await readAllSettingsBothScopes(SETTINGS_CATALOG, state.projectPath);
96
+ dispatch({ type: "SETTINGS_DATA_SUCCESS", values });
26
97
  }
27
98
  catch (error) {
28
- setEnvVarList([]);
99
+ dispatch({
100
+ type: "SETTINGS_DATA_ERROR",
101
+ error: error instanceof Error ? error : new Error(String(error)),
102
+ });
29
103
  }
30
- setIsLoading(false);
31
- }, [state.projectPath]);
104
+ }, [dispatch, state.projectPath]);
32
105
  useEffect(() => {
33
106
  fetchData();
34
107
  }, [fetchData]);
108
+ // Build flat list items
109
+ const listItems = useMemo(() => {
110
+ if (settings.values.status !== "success")
111
+ return [];
112
+ return buildListItems(settings.values.data);
113
+ }, [settings.values]);
114
+ const selectableItems = useMemo(() => listItems.filter((item) => item.type === "category" || item.type === "setting"), [listItems]);
115
+ // Change a setting in a specific scope
116
+ const handleScopeChange = async (scope) => {
117
+ const item = selectableItems[settings.selectedIndex];
118
+ if (!item || item.type !== "setting" || !item.setting)
119
+ return;
120
+ const setting = item.setting;
121
+ const currentValue = scope === "user" ? item.scopedValues?.user : item.scopedValues?.project;
122
+ if (setting.type === "boolean") {
123
+ const currentBool = currentValue === "true" ||
124
+ currentValue === "1" ||
125
+ (currentValue === undefined && setting.defaultValue === "true");
126
+ const newValue = currentBool ? "false" : "true";
127
+ try {
128
+ await writeSettingValue(setting, newValue, scope, state.projectPath);
129
+ await fetchData();
130
+ }
131
+ catch (error) {
132
+ await modal.message("Error", `Failed to update: ${error}`, "error");
133
+ }
134
+ }
135
+ else if (setting.type === "select" && setting.options) {
136
+ const options = setting.options.map((o) => ({
137
+ label: o.label + (currentValue === o.value ? " (current)" : ""),
138
+ value: o.value,
139
+ }));
140
+ // Find current value index for pre-selection
141
+ const currentIndex = setting.options.findIndex((o) => o.value === currentValue);
142
+ // Add "clear" option to remove the setting
143
+ if (currentValue !== undefined) {
144
+ options.push({ label: "Clear (use default)", value: "__clear__" });
145
+ }
146
+ const selected = await modal.select(`${setting.name} — ${scope}`, setting.description, options, currentIndex >= 0 ? currentIndex : undefined);
147
+ if (selected === null)
148
+ return;
149
+ try {
150
+ const val = selected === "__clear__" ? undefined : selected || undefined;
151
+ await writeSettingValue(setting, val, scope, state.projectPath);
152
+ await fetchData();
153
+ }
154
+ catch (error) {
155
+ await modal.message("Error", `Failed to update: ${error}`, "error");
156
+ }
157
+ }
158
+ else {
159
+ const newValue = await modal.input(`${setting.name} — ${scope}`, setting.description, currentValue || "");
160
+ if (newValue === null)
161
+ return;
162
+ try {
163
+ await writeSettingValue(setting, newValue || undefined, scope, state.projectPath);
164
+ await fetchData();
165
+ }
166
+ catch (error) {
167
+ await modal.message("Error", `Failed to update: ${error}`, "error");
168
+ }
169
+ }
170
+ };
35
171
  // Keyboard handling
36
172
  useKeyboard((event) => {
37
173
  if (state.isSearching || state.modal)
38
174
  return;
39
175
  if (event.name === "up" || event.name === "k") {
40
- const newIndex = Math.max(0, envVars.selectedIndex - 1);
41
- dispatch({ type: "ENVVARS_SELECT", index: newIndex });
176
+ const newIndex = Math.max(0, settings.selectedIndex - 1);
177
+ dispatch({ type: "SETTINGS_SELECT", index: newIndex });
42
178
  }
43
179
  else if (event.name === "down" || event.name === "j") {
44
- const newIndex = Math.min(Math.max(0, envVarList.length - 1), envVars.selectedIndex + 1);
45
- dispatch({ type: "ENVVARS_SELECT", index: newIndex });
180
+ const newIndex = Math.min(Math.max(0, selectableItems.length - 1), settings.selectedIndex + 1);
181
+ dispatch({ type: "SETTINGS_SELECT", index: newIndex });
46
182
  }
47
- else if (event.name === "a") {
48
- handleAdd();
183
+ else if (event.name === "u") {
184
+ handleScopeChange("user");
49
185
  }
50
- else if (event.name === "e" || event.name === "enter") {
51
- handleEdit();
186
+ else if (event.name === "p") {
187
+ handleScopeChange("project");
52
188
  }
53
- else if (event.name === "d") {
54
- handleDelete();
189
+ else if (event.name === "enter") {
190
+ // Enter defaults to project scope
191
+ handleScopeChange("project");
55
192
  }
56
193
  });
57
- const handleAdd = async () => {
58
- const varName = await modal.input("Add Variable", "Variable name:");
59
- if (varName === null || !varName.trim())
60
- return;
61
- const cleanName = varName
62
- .trim()
63
- .toUpperCase()
64
- .replace(/[^A-Z0-9_]/g, "_");
65
- // Check if already exists
66
- const existing = envVarList.find((v) => v.name === cleanName);
67
- if (existing) {
68
- const overwrite = await modal.confirm(`${cleanName} exists`, "Overwrite existing value?");
69
- if (!overwrite)
70
- return;
71
- }
72
- const value = await modal.input(`Set ${cleanName}`, "Value:");
73
- if (value === null)
74
- return;
75
- modal.loading(`Adding ${cleanName}...`);
76
- try {
77
- await setMcpEnvVar(cleanName, value, state.projectPath);
78
- modal.hideModal();
79
- await modal.message("Added", `${cleanName} added.\nRestart Claude Code to apply.`, "success");
80
- fetchData();
81
- }
82
- catch (error) {
83
- modal.hideModal();
84
- await modal.message("Error", `Failed to add: ${error}`, "error");
85
- }
86
- };
87
- const handleEdit = async () => {
88
- if (envVarList.length === 0)
89
- return;
90
- const envVar = envVarList[envVars.selectedIndex];
91
- if (!envVar)
92
- return;
93
- const newValue = await modal.input(`Edit ${envVar.name}`, "New value:", envVar.value);
94
- if (newValue === null)
95
- return;
96
- modal.loading(`Updating ${envVar.name}...`);
97
- try {
98
- await setMcpEnvVar(envVar.name, newValue, state.projectPath);
99
- modal.hideModal();
100
- await modal.message("Updated", `${envVar.name} updated.\nRestart Claude Code to apply.`, "success");
101
- fetchData();
102
- }
103
- catch (error) {
104
- modal.hideModal();
105
- await modal.message("Error", `Failed to update: ${error}`, "error");
106
- }
107
- };
108
- const handleDelete = async () => {
109
- if (envVarList.length === 0)
110
- return;
111
- const envVar = envVarList[envVars.selectedIndex];
112
- if (!envVar)
113
- return;
114
- const confirmed = await modal.confirm(`Delete ${envVar.name}?`, "This will remove the variable from configuration.");
115
- if (confirmed) {
116
- modal.loading(`Deleting ${envVar.name}...`);
117
- try {
118
- await removeMcpEnvVar(envVar.name, state.projectPath);
119
- modal.hideModal();
120
- await modal.message("Deleted", `${envVar.name} removed.`, "success");
121
- fetchData();
194
+ const selectedItem = selectableItems[settings.selectedIndex];
195
+ const renderListItem = (item, _idx, isSelected) => {
196
+ if (item.type === "category") {
197
+ const cat = item.category;
198
+ const catBg = cat === "recommended" ? "#2e7d32"
199
+ : cat === "agents" ? "#00838f"
200
+ : cat === "models" ? "#4527a0"
201
+ : cat === "workflow" ? "#1565c0"
202
+ : cat === "terminal" ? "#4e342e"
203
+ : cat === "performance" ? "#6a1b9a"
204
+ : "#e65100";
205
+ const star = cat === "recommended" ? " " : "";
206
+ if (isSelected) {
207
+ return (_jsx("text", { bg: "magenta", fg: "white", children: _jsxs("strong", { children: [" ", star, CATEGORY_LABELS[cat], " "] }) }));
122
208
  }
123
- catch (error) {
124
- modal.hideModal();
125
- await modal.message("Error", `Failed to delete: ${error}`, "error");
209
+ return (_jsx("text", { bg: catBg, fg: "white", children: _jsxs("strong", { children: [" ", star, CATEGORY_LABELS[cat], " "] }) }));
210
+ }
211
+ if (item.type === "setting" && item.setting) {
212
+ const setting = item.setting;
213
+ const indicator = item.isDefault ? "○" : "●";
214
+ const indicatorColor = item.isDefault ? "gray" : "cyan";
215
+ const displayValue = formatValue(setting, item.effectiveValue);
216
+ const valueColor = item.isDefault ? "gray" : "green";
217
+ if (isSelected) {
218
+ return (_jsxs("text", { bg: "magenta", fg: "white", children: [" ", indicator, " ", setting.name.padEnd(28), displayValue, " "] }));
126
219
  }
220
+ return (_jsxs("text", { children: [_jsxs("span", { fg: indicatorColor, children: [" ", indicator, " "] }), _jsx("span", { children: setting.name.padEnd(28) }), _jsx("span", { fg: valueColor, children: displayValue })] }));
127
221
  }
222
+ return _jsx("text", { fg: "gray", children: item.label });
128
223
  };
129
- // Get selected item
130
- const selectedVar = envVarList[envVars.selectedIndex];
131
224
  const renderDetail = () => {
132
- if (isLoading) {
133
- return _jsx("text", { fg: "gray", children: "Loading environment variables..." });
225
+ if (settings.values.status === "loading") {
226
+ return _jsx("text", { fg: "gray", children: "Loading settings..." });
134
227
  }
135
- if (envVarList.length === 0) {
136
- return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "gray", children: "No environment variables configured." }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "green", children: "Press 'a' to add a new variable" }) })] }));
228
+ if (settings.values.status === "error") {
229
+ return _jsx("text", { fg: "red", children: "Failed to load settings" });
137
230
  }
138
- if (!selectedVar) {
139
- return _jsx("text", { fg: "gray", children: "Select a variable to see details" });
231
+ if (!selectedItem) {
232
+ return _jsx("text", { fg: "gray", children: "Select a setting to see details" });
140
233
  }
141
- return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "cyan", children: _jsx("strong", { children: selectedVar.name }) }), _jsxs("box", { marginTop: 1, children: [_jsx("text", { fg: "gray", children: "Value: " }), _jsx("text", { children: selectedVar.value.length > 50
142
- ? selectedVar.value.slice(0, 50) + "..."
143
- : selectedVar.value })] }), _jsxs("box", { marginTop: 2, flexDirection: "column", children: [_jsxs("box", { children: [_jsxs("text", { bg: "magenta", fg: "white", children: [" ", "Enter", " "] }), _jsx("text", { fg: "gray", children: " Edit value" })] }), _jsxs("box", { marginTop: 1, children: [_jsxs("text", { bg: "red", fg: "white", children: [" ", "d", " "] }), _jsx("text", { fg: "gray", children: " Delete variable" })] })] })] }));
144
- };
145
- const renderListItem = (envVar, _idx, isSelected) => {
146
- const masked = envVar.value.length > 20
147
- ? envVar.value.slice(0, 20) + "..."
148
- : envVar.value;
149
- return isSelected ? (_jsxs("text", { bg: "magenta", fg: "white", children: [" ", envVar.name, " = \"", masked, "\"", " "] })) : (_jsxs("text", { children: [_jsx("span", { fg: "cyan", children: envVar.name }), _jsxs("span", { fg: "gray", children: [" = \"", masked, "\""] })] }));
234
+ if (selectedItem.type === "category") {
235
+ const cat = selectedItem.category;
236
+ const catColor = cat === "recommended"
237
+ ? "green"
238
+ : cat === "agents" || cat === "models"
239
+ ? "cyan"
240
+ : cat === "workflow" || cat === "terminal"
241
+ ? "blue"
242
+ : cat === "performance"
243
+ ? "magentaBright"
244
+ : "yellow";
245
+ const descriptions = {
246
+ recommended: "Most impactful settings every user should know.",
247
+ agents: "Agent teams, task lists, and subagent configuration.",
248
+ models: "Model selection, extended thinking, and effort.",
249
+ workflow: "Git, plans, permissions, output style, and languages.",
250
+ terminal: "Shell, spinners, progress bars, voice, and UI behavior.",
251
+ performance: "Compaction, token limits, timeouts, and caching.",
252
+ advanced: "Telemetry, updates, debugging, and internal controls.",
253
+ };
254
+ return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: catColor, children: _jsx("strong", { children: CATEGORY_LABELS[cat] }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: descriptions[cat] }) })] }));
255
+ }
256
+ if (selectedItem.type === "setting" && selectedItem.setting) {
257
+ const setting = selectedItem.setting;
258
+ const scoped = selectedItem.scopedValues || {
259
+ user: undefined,
260
+ project: undefined,
261
+ };
262
+ const storageDesc = setting.storage.type === "env"
263
+ ? `env: ${setting.storage.key}`
264
+ : `settings.json: ${setting.storage.key}`;
265
+ const userValue = formatValue(setting, scoped.user);
266
+ const projectValue = formatValue(setting, scoped.project);
267
+ const userIsSet = scoped.user !== undefined && scoped.user !== "";
268
+ const projectIsSet = scoped.project !== undefined && scoped.project !== "";
269
+ const actionLabel = setting.type === "boolean"
270
+ ? "toggle"
271
+ : setting.type === "select"
272
+ ? "choose"
273
+ : "edit";
274
+ return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "cyan", children: _jsx("strong", { children: setting.name }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "white", children: setting.description }) }), _jsx("box", { marginTop: 1, children: _jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Stored " }), _jsx("span", { fg: "#5c9aff", children: storageDesc })] }) }), setting.defaultValue !== undefined && (_jsx("box", { children: _jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Default " }), _jsx("span", { children: setting.defaultValue })] }) })), _jsxs("box", { flexDirection: "column", marginTop: 1, children: [_jsx("text", { children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsx("text", { children: _jsx("strong", { children: "Scopes:" }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { children: [_jsxs("span", { bg: "cyan", fg: "black", children: [" ", "u", " "] }), _jsx("span", { fg: userIsSet ? "cyan" : "gray", children: userIsSet ? " ● " : " ○ " }), _jsx("span", { fg: "cyan", children: "User" }), _jsx("span", { children: " global" }), _jsxs("span", { fg: userIsSet ? "cyan" : "gray", children: [" ", userValue] })] }), _jsxs("text", { children: [_jsxs("span", { bg: "green", fg: "black", children: [" ", "p", " "] }), _jsx("span", { fg: projectIsSet ? "green" : "gray", children: projectIsSet ? " ● " : " ○ " }), _jsx("span", { fg: "green", children: "Project" }), _jsx("span", { children: " team" }), _jsxs("span", { fg: projectIsSet ? "green" : "gray", children: [" ", projectValue] })] })] })] }), _jsx("box", { marginTop: 1, children: _jsxs("text", { fg: "gray", children: ["Press u/p to ", actionLabel, " in scope"] }) })] }));
275
+ }
276
+ return null;
150
277
  };
151
- const statusContent = (_jsxs(_Fragment, { children: [_jsx("text", { fg: "gray", children: "Variables: " }), _jsx("text", { fg: "cyan", children: envVarList.length }), _jsx("text", { fg: "gray", children: " \u2502 Location: " }), _jsx("text", { fg: "green", children: ".claude/settings.local.json" })] }));
152
- return (_jsx(ScreenLayout, { title: "claudeup Environment Variables", currentScreen: "env-vars", statusLine: statusContent, footerHints: "\u2191\u2193:nav \u2502 Enter/e:edit \u2502 a:add \u2502 d:delete", listPanel: envVarList.length === 0 ? (_jsx("text", { fg: "gray", children: "No environment variables configured" })) : (_jsx(ScrollableList, { items: envVarList, selectedIndex: envVars.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight })), detailPanel: renderDetail() }));
278
+ const totalSet = settings.values.status === "success"
279
+ ? Array.from(settings.values.data.values()).filter((v) => v.user !== undefined || v.project !== undefined).length
280
+ : 0;
281
+ const statusContent = (_jsxs("text", { children: [_jsx("span", { fg: "gray", children: "Settings: " }), _jsxs("span", { fg: "cyan", children: [totalSet, " configured"] }), _jsx("span", { fg: "gray", children: " \u2502 u:user p:project" })] }));
282
+ return (_jsx(ScreenLayout, { title: "claudeup Settings", currentScreen: "settings", statusLine: statusContent, footerHints: "\u2191\u2193:nav \u2502 u:user scope \u2502 p:project scope \u2502 Enter:project", listPanel: settings.values.status !== "success" ? (_jsx("text", { fg: "gray", children: settings.values.status === "loading"
283
+ ? "Loading..."
284
+ : "Error loading settings" })) : (_jsx(ScrollableList, { items: selectableItems, selectedIndex: settings.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight })), detailPanel: renderDetail() }));
153
285
  }
154
- export default EnvVarsScreen;
286
+ export default SettingsScreen;