claudeup 3.7.1 → 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.
- package/package.json +1 -1
- package/src/data/settings-catalog.js +612 -0
- package/src/data/settings-catalog.ts +689 -0
- package/src/prerunner/index.js +2 -1
- package/src/prerunner/index.ts +2 -0
- package/src/services/plugin-manager.js +2 -0
- package/src/services/plugin-manager.ts +3 -0
- package/src/services/profiles.js +161 -0
- package/src/services/profiles.ts +225 -0
- package/src/services/settings-manager.js +108 -0
- package/src/services/settings-manager.ts +140 -0
- package/src/types/index.ts +34 -0
- package/src/ui/App.js +17 -18
- package/src/ui/App.tsx +21 -23
- package/src/ui/components/TabBar.js +8 -8
- package/src/ui/components/TabBar.tsx +14 -19
- package/src/ui/components/layout/ScreenLayout.js +8 -14
- package/src/ui/components/layout/ScreenLayout.tsx +51 -58
- package/src/ui/components/modals/ModalContainer.js +43 -11
- package/src/ui/components/modals/ModalContainer.tsx +44 -12
- package/src/ui/components/modals/SelectModal.js +4 -18
- package/src/ui/components/modals/SelectModal.tsx +10 -21
- package/src/ui/screens/CliToolsScreen.js +2 -2
- package/src/ui/screens/CliToolsScreen.tsx +8 -8
- package/src/ui/screens/EnvVarsScreen.js +248 -116
- package/src/ui/screens/EnvVarsScreen.tsx +419 -184
- package/src/ui/screens/McpRegistryScreen.tsx +18 -6
- package/src/ui/screens/McpScreen.js +1 -1
- package/src/ui/screens/McpScreen.tsx +15 -5
- package/src/ui/screens/ModelSelectorScreen.js +3 -5
- package/src/ui/screens/ModelSelectorScreen.tsx +12 -16
- package/src/ui/screens/PluginsScreen.js +181 -65
- package/src/ui/screens/PluginsScreen.tsx +308 -91
- package/src/ui/screens/ProfilesScreen.js +255 -0
- package/src/ui/screens/ProfilesScreen.tsx +487 -0
- package/src/ui/screens/StatusLineScreen.js +2 -2
- package/src/ui/screens/StatusLineScreen.tsx +10 -12
- package/src/ui/screens/index.js +2 -2
- package/src/ui/screens/index.ts +2 -2
- package/src/ui/state/AppContext.js +2 -1
- package/src/ui/state/AppContext.tsx +2 -0
- package/src/ui/state/reducer.js +63 -19
- package/src/ui/state/reducer.ts +68 -19
- package/src/ui/state/types.ts +33 -14
- package/src/utils/clipboard.js +56 -0
- 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
|
|
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
|
-
//
|
|
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 (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
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
|
|
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
|
-
|
|
23
|
-
|
|
23
|
+
defaultIndex,
|
|
24
|
+
onSelect: _onSelect,
|
|
25
|
+
onCancel: _onCancel,
|
|
24
26
|
}: SelectModalProps) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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: "
|
|
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(
|
|
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="
|
|
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
|
-
<
|
|
437
|
-
<
|
|
435
|
+
<text>
|
|
436
|
+
<span fg="gray">Installed: </span>
|
|
437
|
+
<span fg="cyan">
|
|
438
438
|
{installedCount}/{toolStatuses.length}
|
|
439
|
-
</
|
|
439
|
+
</span>
|
|
440
440
|
{updateCount > 0 && (
|
|
441
441
|
<>
|
|
442
|
-
<
|
|
443
|
-
<
|
|
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 {
|
|
2
|
-
import { useEffect, useCallback,
|
|
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 {
|
|
9
|
-
|
|
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 {
|
|
88
|
+
const { settings } = state;
|
|
12
89
|
const modal = useModal();
|
|
13
90
|
const dimensions = useDimensions();
|
|
14
|
-
|
|
15
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
16
|
-
// Fetch data
|
|
91
|
+
// Fetch data from both scopes
|
|
17
92
|
const fetchData = useCallback(async () => {
|
|
18
|
-
|
|
93
|
+
dispatch({ type: "SETTINGS_DATA_LOADING" });
|
|
19
94
|
try {
|
|
20
|
-
const
|
|
21
|
-
|
|
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
|
-
|
|
99
|
+
dispatch({
|
|
100
|
+
type: "SETTINGS_DATA_ERROR",
|
|
101
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
102
|
+
});
|
|
29
103
|
}
|
|
30
|
-
|
|
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,
|
|
41
|
-
dispatch({ type: "
|
|
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,
|
|
45
|
-
dispatch({ type: "
|
|
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 === "
|
|
48
|
-
|
|
183
|
+
else if (event.name === "u") {
|
|
184
|
+
handleScopeChange("user");
|
|
49
185
|
}
|
|
50
|
-
else if (event.name === "
|
|
51
|
-
|
|
186
|
+
else if (event.name === "p") {
|
|
187
|
+
handleScopeChange("project");
|
|
52
188
|
}
|
|
53
|
-
else if (event.name === "
|
|
54
|
-
|
|
189
|
+
else if (event.name === "enter") {
|
|
190
|
+
// Enter defaults to project scope
|
|
191
|
+
handleScopeChange("project");
|
|
55
192
|
}
|
|
56
193
|
});
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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 (
|
|
133
|
-
return _jsx("text", { fg: "gray", children: "Loading
|
|
225
|
+
if (settings.values.status === "loading") {
|
|
226
|
+
return _jsx("text", { fg: "gray", children: "Loading settings..." });
|
|
134
227
|
}
|
|
135
|
-
if (
|
|
136
|
-
return
|
|
228
|
+
if (settings.values.status === "error") {
|
|
229
|
+
return _jsx("text", { fg: "red", children: "Failed to load settings" });
|
|
137
230
|
}
|
|
138
|
-
if (!
|
|
139
|
-
return _jsx("text", { fg: "gray", children: "Select a
|
|
231
|
+
if (!selectedItem) {
|
|
232
|
+
return _jsx("text", { fg: "gray", children: "Select a setting to see details" });
|
|
140
233
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
152
|
-
|
|
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
|
|
286
|
+
export default SettingsScreen;
|