claudeup 4.19.0 → 4.23.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 (182) hide show
  1. package/bin/claudeup.js +43 -19
  2. package/package.json +14 -19
  3. package/scripts/build-binaries.ts +77 -0
  4. package/scripts/check-optional-deps.ts +49 -0
  5. package/scripts/test-isolated.ts +43 -0
  6. package/src/__tests__/cli-router.test.ts +65 -0
  7. package/src/__tests__/conventions-integration.test.ts +745 -0
  8. package/src/__tests__/conventions-manager.test.ts +1172 -0
  9. package/src/__tests__/doctor-bins.test.ts +35 -0
  10. package/src/__tests__/doctor.test.ts +450 -0
  11. package/src/__tests__/dual-write-prevention.test.ts +14 -4
  12. package/src/__tests__/file-locking.test.ts +208 -0
  13. package/src/__tests__/gitignore-prerun.test.ts +1 -0
  14. package/src/__tests__/install-command.test.ts +81 -0
  15. package/src/__tests__/install-plan.test.ts +98 -0
  16. package/src/__tests__/manifest.test.ts +136 -0
  17. package/src/__tests__/marketplace-badge.test.ts +91 -0
  18. package/src/__tests__/marketplace-refresh.test.ts +267 -0
  19. package/src/__tests__/plugin-requires.test.ts +133 -0
  20. package/src/__tests__/plugin-setup.test.ts +7 -0
  21. package/src/__tests__/profile-command.test.ts +116 -0
  22. package/src/__tests__/profile-materializer.test.ts +82 -0
  23. package/src/__tests__/profile-sync.test.ts +136 -0
  24. package/src/__tests__/registry-version-resolution.test.ts +74 -0
  25. package/src/__tests__/resolve-executable.test.ts +42 -0
  26. package/src/__tests__/resolver.test.ts +218 -0
  27. package/src/__tests__/symlink-manager.test.ts +99 -0
  28. package/src/__tests__/toolchain.test.ts +56 -0
  29. package/src/cli/claude.ts +21 -0
  30. package/src/cli/doctor.ts +132 -0
  31. package/src/cli/install.ts +360 -0
  32. package/src/cli/profile.ts +166 -0
  33. package/src/cli/router.ts +107 -0
  34. package/src/cli/update.ts +85 -0
  35. package/src/data/cli-tools.ts +7 -7
  36. package/src/data/gitignore-defaults.ts +4 -0
  37. package/src/data/marketplaces.ts +12 -0
  38. package/src/data/predefined-profiles.ts +3 -4
  39. package/src/main.tsx +11 -154
  40. package/src/prerunner/index.ts +62 -27
  41. package/src/services/claude-cli.ts +19 -22
  42. package/src/services/claude-settings.ts +99 -26
  43. package/src/services/conventions-manager.ts +865 -0
  44. package/src/services/doctor-bins.ts +49 -0
  45. package/src/services/doctor.ts +509 -0
  46. package/src/services/install-plan.ts +107 -0
  47. package/src/services/manifest.ts +166 -0
  48. package/src/services/marketplace-refresh.ts +173 -0
  49. package/src/services/plugin-manager.ts +7 -1
  50. package/src/services/plugin-requires.ts +215 -0
  51. package/src/services/plugin-version-check.ts +9 -4
  52. package/src/services/profile-materializer.ts +61 -0
  53. package/src/services/profile-sync.ts +150 -0
  54. package/src/services/resolver.ts +293 -0
  55. package/src/services/symlink-manager.ts +146 -0
  56. package/src/services/toolchain.ts +97 -0
  57. package/src/services/version-check.ts +10 -11
  58. package/src/types/index.ts +138 -31
  59. package/src/ui/App.tsx +0 -4
  60. package/src/ui/adapters/pluginsAdapter.ts +13 -0
  61. package/src/ui/screens/CliToolsScreen.tsx +11 -3
  62. package/src/ui/screens/index.ts +0 -1
  63. package/src/ui/state/reducer.ts +0 -101
  64. package/src/ui/state/types.ts +0 -35
  65. package/src/utils/command-utils.ts +23 -0
  66. package/src/utils/file-locking.ts +144 -0
  67. package/src/data/alias-flags.js +0 -205
  68. package/src/data/cli-tools.js +0 -123
  69. package/src/data/gitignore-defaults.js +0 -24
  70. package/src/data/gitignore-reasons.js +0 -97
  71. package/src/data/gitignore-templates.js +0 -21
  72. package/src/data/marketplaces.js +0 -138
  73. package/src/data/mcp-servers.js +0 -509
  74. package/src/data/predefined-profiles.js +0 -248
  75. package/src/data/settings-catalog.js +0 -625
  76. package/src/data/skill-repos.js +0 -160
  77. package/src/data/statuslines.js +0 -159
  78. package/src/data/statuslines.ts +0 -188
  79. package/src/index.js +0 -4
  80. package/src/index.ts +0 -5
  81. package/src/main.js +0 -170
  82. package/src/prerunner/index.js +0 -252
  83. package/src/services/alias-settings.js +0 -51
  84. package/src/services/alias-shell-writer.js +0 -1018
  85. package/src/services/alias-store.js +0 -129
  86. package/src/services/claude-cli.js +0 -189
  87. package/src/services/claude-runner.js +0 -28
  88. package/src/services/claude-settings.js +0 -1223
  89. package/src/services/gitignore-detector.js +0 -155
  90. package/src/services/gitignore-fixer.js +0 -231
  91. package/src/services/gitignore-prerun.js +0 -46
  92. package/src/services/gitignore-resolver.js +0 -143
  93. package/src/services/gitignore-service.js +0 -117
  94. package/src/services/local-marketplace.js +0 -339
  95. package/src/services/marketplace-fetcher.js +0 -96
  96. package/src/services/marketplace-sync.js +0 -94
  97. package/src/services/mcp-registry.js +0 -87
  98. package/src/services/plugin-manager.js +0 -473
  99. package/src/services/plugin-mcp-config.js +0 -177
  100. package/src/services/plugin-setup.js +0 -499
  101. package/src/services/plugin-version-check.js +0 -262
  102. package/src/services/profiles.js +0 -161
  103. package/src/services/settings-manager.js +0 -243
  104. package/src/services/skills-manager.js +0 -393
  105. package/src/services/skillsmp-client.js +0 -87
  106. package/src/services/update-cache.js +0 -52
  107. package/src/services/version-check.js +0 -99
  108. package/src/types/gitignore.js +0 -6
  109. package/src/types/index.js +0 -1
  110. package/src/ui/App.js +0 -355
  111. package/src/ui/adapters/pluginsAdapter.js +0 -139
  112. package/src/ui/adapters/settingsAdapter.js +0 -111
  113. package/src/ui/adapters/skillsAdapter.js +0 -154
  114. package/src/ui/components/CategoryHeader.js +0 -9
  115. package/src/ui/components/EmptyFilterState.js +0 -4
  116. package/src/ui/components/FlagDetailEditor.js +0 -0
  117. package/src/ui/components/ScopeIndicator.js +0 -30
  118. package/src/ui/components/ScrollableList.js +0 -36
  119. package/src/ui/components/SearchInput.js +0 -19
  120. package/src/ui/components/StyledText.js +0 -39
  121. package/src/ui/components/TabBar.js +0 -19
  122. package/src/ui/components/layout/FooterHints.js +0 -29
  123. package/src/ui/components/layout/Panel.js +0 -6
  124. package/src/ui/components/layout/ProgressBar.js +0 -14
  125. package/src/ui/components/layout/ScopeTabs.js +0 -6
  126. package/src/ui/components/layout/ScreenLayout.js +0 -16
  127. package/src/ui/components/layout/index.js +0 -5
  128. package/src/ui/components/modals/ConfirmModal.js +0 -14
  129. package/src/ui/components/modals/InputModal.js +0 -5
  130. package/src/ui/components/modals/LoadingModal.js +0 -17
  131. package/src/ui/components/modals/MessageModal.js +0 -16
  132. package/src/ui/components/modals/ModalContainer.js +0 -137
  133. package/src/ui/components/modals/SelectModal.js +0 -18
  134. package/src/ui/components/modals/VersionMismatchModal.js +0 -36
  135. package/src/ui/components/modals/index.js +0 -6
  136. package/src/ui/components/primitives/ActionHints.js +0 -13
  137. package/src/ui/components/primitives/DetailSection.js +0 -7
  138. package/src/ui/components/primitives/KeyValueLine.js +0 -8
  139. package/src/ui/components/primitives/ListCategoryRow.js +0 -8
  140. package/src/ui/components/primitives/MetaText.js +0 -8
  141. package/src/ui/components/primitives/ScopeDetail.js +0 -32
  142. package/src/ui/components/primitives/ScopeSquares.js +0 -11
  143. package/src/ui/components/primitives/SelectableRow.js +0 -5
  144. package/src/ui/components/primitives/index.js +0 -8
  145. package/src/ui/hooks/index.js +0 -4
  146. package/src/ui/hooks/useAsyncData.js +0 -77
  147. package/src/ui/hooks/useGitignoreModal.js +0 -74
  148. package/src/ui/hooks/useKeyboard.js +0 -13
  149. package/src/ui/hooks/useKeyboardHandler.js +0 -39
  150. package/src/ui/hooks/useMismatchModal.js +0 -77
  151. package/src/ui/registry.js +0 -1
  152. package/src/ui/renderers/cliToolRenderers.js +0 -54
  153. package/src/ui/renderers/gitignoreRenderers.js +0 -46
  154. package/src/ui/renderers/mcpRenderers.js +0 -26
  155. package/src/ui/renderers/pluginRenderers.js +0 -124
  156. package/src/ui/renderers/profileRenderers.js +0 -177
  157. package/src/ui/renderers/settingsRenderers.js +0 -73
  158. package/src/ui/renderers/skillRenderers.js +0 -138
  159. package/src/ui/screens/AliasScreen.js +0 -1111
  160. package/src/ui/screens/CliToolsScreen.js +0 -338
  161. package/src/ui/screens/EnvVarsScreen.js +0 -152
  162. package/src/ui/screens/GitignoreScreen.js +0 -328
  163. package/src/ui/screens/McpRegistryScreen.js +0 -238
  164. package/src/ui/screens/McpScreen.js +0 -176
  165. package/src/ui/screens/ModelSelectorScreen.js +0 -296
  166. package/src/ui/screens/ModelSelectorScreen.tsx +0 -444
  167. package/src/ui/screens/PluginsScreen.js +0 -769
  168. package/src/ui/screens/ProfilesScreen.js +0 -298
  169. package/src/ui/screens/SkillsScreen.js +0 -549
  170. package/src/ui/screens/StatusLineScreen.js +0 -206
  171. package/src/ui/screens/StatusLineScreen.tsx +0 -416
  172. package/src/ui/screens/index.js +0 -10
  173. package/src/ui/state/AnimationContext.js +0 -34
  174. package/src/ui/state/AppContext.js +0 -162
  175. package/src/ui/state/DimensionsContext.js +0 -71
  176. package/src/ui/state/reducer.js +0 -547
  177. package/src/ui/state/types.js +0 -1
  178. package/src/ui/theme.js +0 -47
  179. package/src/utils/clipboard.js +0 -56
  180. package/src/utils/command-utils.js +0 -19
  181. package/src/utils/fuzzy-search.js +0 -101
  182. package/src/utils/string-utils.js +0 -62
@@ -1,206 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/react/jsx-runtime";
2
- import { useEffect, useCallback, useState } from "react";
3
- import { useApp, useModal } from "../state/AppContext.js";
4
- import { useDimensions } from "../state/DimensionsContext.js";
5
- import { useKeyboard } from "../hooks/useKeyboard.js";
6
- import { ScreenLayout } from "../components/layout/index.js";
7
- import { statusLineCategories } from "../../data/statuslines.js";
8
- import { ScrollableList } from "../components/ScrollableList.js";
9
- import { setStatusLine, getStatusLine, setGlobalStatusLine, getGlobalStatusLine, } from "../../services/claude-settings.js";
10
- export function StatusLineScreen() {
11
- const { state, dispatch } = useApp();
12
- const { statusline: statusLine } = state;
13
- const modal = useModal();
14
- const dimensions = useDimensions();
15
- const [projectStatusLine, setProjectStatusLineState] = useState();
16
- const [globalStatusLine, setGlobalStatusLineState] = useState();
17
- const [isLoading, setIsLoading] = useState(true);
18
- // Fetch data
19
- const fetchData = useCallback(async () => {
20
- setIsLoading(true);
21
- try {
22
- const [project, global] = await Promise.all([
23
- getStatusLine(state.projectPath),
24
- getGlobalStatusLine(),
25
- ]);
26
- setProjectStatusLineState(project);
27
- setGlobalStatusLineState(global);
28
- }
29
- catch (error) {
30
- // Silent error handling - show empty state
31
- }
32
- setIsLoading(false);
33
- }, [state.projectPath]);
34
- useEffect(() => {
35
- fetchData();
36
- }, [fetchData]);
37
- // Get current status line based on scope
38
- const getCurrentStatusLine = () => {
39
- return statusLine.scope === "project"
40
- ? projectStatusLine
41
- : globalStatusLine;
42
- };
43
- // Build list items with categories
44
- const buildListItems = () => {
45
- const currentForScope = getCurrentStatusLine();
46
- const items = [];
47
- for (const category of statusLineCategories) {
48
- items.push({
49
- label: category.name,
50
- isCategory: true,
51
- });
52
- for (const preset of category.presets) {
53
- const isActive = currentForScope === preset.template;
54
- const status = isActive ? "●" : "○";
55
- items.push({
56
- label: ` ${status} ${preset.name}`,
57
- preset,
58
- });
59
- }
60
- }
61
- // Add custom option at the end
62
- items.push({
63
- label: "+ Custom Status Line",
64
- isCustom: true,
65
- });
66
- return items;
67
- };
68
- const listItems = buildListItems();
69
- // Keyboard handling
70
- useKeyboard((event) => {
71
- if (state.isSearching || state.modal)
72
- return;
73
- if (event.name === "up" || event.name === "k") {
74
- const newIndex = Math.max(0, statusLine.selectedIndex - 1);
75
- dispatch({ type: "STATUSLINE_SELECT", index: newIndex });
76
- }
77
- else if (event.name === "down" || event.name === "j") {
78
- const newIndex = Math.min(listItems.length - 1, statusLine.selectedIndex + 1);
79
- dispatch({ type: "STATUSLINE_SELECT", index: newIndex });
80
- }
81
- else if (event.name === "p") {
82
- dispatch({ type: "STATUSLINE_SET_SCOPE", scope: "project" });
83
- }
84
- else if (event.name === "g") {
85
- dispatch({ type: "STATUSLINE_SET_SCOPE", scope: "global" });
86
- }
87
- else if (event.name === "r") {
88
- handleReset();
89
- }
90
- else if (event.name === "enter") {
91
- handleSelect();
92
- }
93
- });
94
- const saveStatusLine = async (template) => {
95
- if (statusLine.scope === "global") {
96
- await setGlobalStatusLine(template);
97
- }
98
- else {
99
- await setStatusLine(template, state.projectPath);
100
- }
101
- };
102
- const handleSelect = async () => {
103
- const item = listItems[statusLine.selectedIndex];
104
- if (!item || item.isCategory)
105
- return;
106
- const scopeLabel = statusLine.scope === "global" ? "Global" : "Project";
107
- if (item.isCustom) {
108
- const template = await modal.input(`Custom Status Line (${scopeLabel})`, "Enter template (use {model}, {cost}, etc.):", getCurrentStatusLine() || "");
109
- if (template !== null) {
110
- modal.loading(`Saving custom status line...`);
111
- try {
112
- await saveStatusLine(template);
113
- modal.hideModal();
114
- await modal.message("Saved", `Custom status line saved to ${scopeLabel}.\n\nRestart Claude Code to apply changes.`, "success");
115
- fetchData();
116
- }
117
- catch (error) {
118
- modal.hideModal();
119
- await modal.message("Error", `Failed to save: ${error}`, "error");
120
- }
121
- }
122
- }
123
- else if (item.preset) {
124
- modal.loading(`Applying ${item.preset.name}...`);
125
- try {
126
- await saveStatusLine(item.preset.template);
127
- modal.hideModal();
128
- await modal.message("Applied", `"${item.preset.name}" applied to ${scopeLabel}.\n\nRestart Claude Code to apply changes.`, "success");
129
- fetchData();
130
- }
131
- catch (error) {
132
- modal.hideModal();
133
- await modal.message("Error", `Failed to apply: ${error}`, "error");
134
- }
135
- }
136
- };
137
- const handleReset = async () => {
138
- const scopeLabel = statusLine.scope === "global" ? "Global" : "Project";
139
- const confirmed = await modal.confirm("Reset Status Line", `Clear the ${scopeLabel} status line configuration?`);
140
- if (confirmed) {
141
- modal.loading("Resetting...");
142
- try {
143
- await saveStatusLine("");
144
- modal.hideModal();
145
- await modal.message("Reset", `${scopeLabel} status line has been cleared.`, "success");
146
- fetchData();
147
- }
148
- catch (error) {
149
- modal.hideModal();
150
- await modal.message("Error", `Failed to reset: ${error}`, "error");
151
- }
152
- }
153
- };
154
- // Get selected item
155
- const selectedItem = listItems[statusLine.selectedIndex];
156
- // Build preview
157
- const renderPreview = () => {
158
- if (isLoading) {
159
- return _jsx("text", { fg: "gray", children: "Loading status line settings..." });
160
- }
161
- if (!selectedItem || selectedItem.isCategory) {
162
- return (_jsx("box", { flexDirection: "column", alignItems: "center", justifyContent: "center", flexGrow: 1, children: _jsx("text", { fg: "gray", children: "Select a theme to see preview" }) }));
163
- }
164
- if (selectedItem.isCustom) {
165
- return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "cyan", children: _jsx("strong", { children: "\u2728 Custom Status Line" }) }), _jsx("text", { fg: "gray", children: "Create your own unique status line!" }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsx("text", { fg: "yellow", children: _jsx("strong", { children: "Available variables:" }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { children: [_jsx("span", { fg: "green", children: _jsx("strong", { children: "{model}" }) }), " ", _jsx("span", { fg: "gray", children: "\u2192" }), " Model name"] }), _jsxs("text", { children: [_jsx("span", { fg: "green", children: _jsx("strong", { children: "{model_short}" }) }), " ", _jsx("span", { fg: "gray", children: "\u2192" }), " Short name"] }), _jsxs("text", { children: [_jsx("span", { fg: "yellow", children: _jsx("strong", { children: "{cost}" }) }), " ", _jsx("span", { fg: "gray", children: "\u2192" }), " Session cost"] }), _jsxs("text", { children: [_jsx("span", { fg: "#5c9aff", children: _jsx("strong", { children: "{cwd}" }) }), " ", _jsx("span", { fg: "gray", children: "\u2192" }), " Working directory"] }), _jsxs("text", { children: [_jsx("span", { fg: "magenta", children: _jsx("strong", { children: "{git_branch}" }) }), " ", _jsx("span", { fg: "gray", children: "\u2192" }), " Git branch"] }), _jsxs("text", { children: [_jsx("span", { fg: "cyan", children: _jsx("strong", { children: "{input_tokens}" }) }), " ", _jsx("span", { fg: "gray", children: "\u2192" }), " Input tokens"] }), _jsxs("text", { children: [_jsx("span", { fg: "cyan", children: _jsx("strong", { children: "{output_tokens}" }) }), " ", _jsx("span", { fg: "gray", children: "\u2192" }), " Output tokens"] }), _jsxs("text", { children: [_jsx("span", { fg: "red", children: _jsx("strong", { children: "{session_duration}" }) }), _jsx("span", { fg: "gray", children: "\u2192" }), " Duration"] })] })] })] }));
166
- }
167
- if (selectedItem.preset) {
168
- const example = selectedItem.preset.template
169
- .replace("{model}", "claude-sonnet-4")
170
- .replace("{model_short}", "sonnet")
171
- .replace("{cost}", "0.42")
172
- .replace("{cwd}", "~/myapp")
173
- .replace("{git_branch}", "main")
174
- .replace("{input_tokens}", "1.2k")
175
- .replace("{output_tokens}", "850")
176
- .replace("{session_duration}", "12m");
177
- return (_jsxs("box", { flexDirection: "column", children: [_jsx("text", { fg: "cyan", children: _jsxs("strong", { children: ["\u25C6 ", selectedItem.preset.name] }) }), _jsx("text", { fg: "gray", children: selectedItem.preset.description }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsx("text", { fg: "yellow", children: _jsx("strong", { children: "Preview:" }) }), _jsx("box", { marginTop: 1, paddingLeft: 1, paddingRight: 1, borderStyle: "rounded", borderColor: "green", children: _jsx("text", { fg: "white", children: example }) })] }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsx("text", { fg: "#666666", children: "Template:" }), _jsx("text", { fg: "gray", children: selectedItem.preset.template })] })] }));
178
- }
179
- return null;
180
- };
181
- const renderListItem = (item, _idx, isSelected) => {
182
- if (item.isCategory) {
183
- return (_jsx("text", { fg: "magenta", children: _jsxs("strong", { children: ["\u25B8 ", item.label] }) }));
184
- }
185
- if (item.isCustom) {
186
- return isSelected ? (_jsx("text", { bg: "cyan", fg: "black", children: _jsx("strong", { children: " \u2795 Custom Status Line " }) })) : (_jsx("text", { fg: "cyan", children: _jsxs("strong", { children: [" ", "\u2795 Custom Status Line"] }) }));
187
- }
188
- const currentForScope = getCurrentStatusLine();
189
- const isActive = item.preset && currentForScope === item.preset.template;
190
- return isSelected ? (_jsxs("text", { bg: "magenta", fg: "white", children: [" ", isActive ? "●" : "○", " ", item.preset?.name || "", " "] })) : (_jsxs("text", { fg: isActive ? "green" : "white", children: [" ", isActive ? "●" : "○", " ", item.preset?.name || ""] }));
191
- };
192
- // Build status line content
193
- const scopeLabel = statusLine.scope === "project" ? "Project" : "Global";
194
- const currentValue = getCurrentStatusLine();
195
- const statusContent = (_jsxs(_Fragment, { children: [_jsx("text", { fg: "gray", children: "Scope: " }), _jsx("text", { fg: "cyan", children: scopeLabel }), _jsx("text", { fg: "gray", children: " \u2502 Current: " }), _jsx("text", { fg: "green", children: currentValue
196
- ? currentValue.slice(0, 35) + (currentValue.length > 35 ? "..." : "")
197
- : "(not set)" })] }));
198
- return (_jsx(ScreenLayout, { title: "claudeup Status Line", currentScreen: "statusline", statusLine: statusContent, footerHints: [
199
- { keys: ["↑", "↓"], label: "nav" },
200
- { keys: ["Enter"], label: "apply" },
201
- { keys: ["p"], label: "project" },
202
- { keys: ["g"], label: "global" },
203
- { keys: ["r"], label: "reset" },
204
- ], listPanel: _jsx(ScrollableList, { items: listItems, selectedIndex: statusLine.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight }), detailPanel: renderPreview() }));
205
- }
206
- export default StatusLineScreen;
@@ -1,416 +0,0 @@
1
- import React, { useEffect, useCallback, useState } from "react";
2
- import { useApp, useModal } from "../state/AppContext.js";
3
- import { useDimensions } from "../state/DimensionsContext.js";
4
- import { useKeyboard } from "../hooks/useKeyboard.js";
5
- import { ScreenLayout } from "../components/layout/index.js";
6
-
7
- import { statusLineCategories } from "../../data/statuslines.js";
8
- import type { StatusLineConfig } from "../../types/index.js";
9
- import { ScrollableList } from "../components/ScrollableList.js";
10
- import {
11
- setStatusLine,
12
- getStatusLine,
13
- setGlobalStatusLine,
14
- getGlobalStatusLine,
15
- } from "../../services/claude-settings.js";
16
-
17
- interface ListItem {
18
- label: string;
19
- preset?: StatusLineConfig;
20
- isCustom?: boolean;
21
- isCategory?: boolean;
22
- }
23
-
24
- export function StatusLineScreen() {
25
- const { state, dispatch } = useApp();
26
- const { statusline: statusLine } = state;
27
- const modal = useModal();
28
- const dimensions = useDimensions();
29
-
30
- const [projectStatusLine, setProjectStatusLineState] = useState<
31
- string | undefined
32
- >();
33
- const [globalStatusLine, setGlobalStatusLineState] = useState<
34
- string | undefined
35
- >();
36
- const [isLoading, setIsLoading] = useState(true);
37
-
38
- // Fetch data
39
- const fetchData = useCallback(async () => {
40
- setIsLoading(true);
41
- try {
42
- const [project, global] = await Promise.all([
43
- getStatusLine(state.projectPath),
44
- getGlobalStatusLine(),
45
- ]);
46
- setProjectStatusLineState(project);
47
- setGlobalStatusLineState(global);
48
- } catch (error) {
49
- // Silent error handling - show empty state
50
- }
51
- setIsLoading(false);
52
- }, [state.projectPath]);
53
-
54
- useEffect(() => {
55
- fetchData();
56
- }, [fetchData]);
57
-
58
- // Get current status line based on scope
59
- const getCurrentStatusLine = (): string | undefined => {
60
- return statusLine.scope === "project"
61
- ? projectStatusLine
62
- : globalStatusLine;
63
- };
64
-
65
- // Build list items with categories
66
- const buildListItems = (): ListItem[] => {
67
- const currentForScope = getCurrentStatusLine();
68
- const items: ListItem[] = [];
69
-
70
- for (const category of statusLineCategories) {
71
- items.push({
72
- label: category.name,
73
- isCategory: true,
74
- });
75
-
76
- for (const preset of category.presets) {
77
- const isActive = currentForScope === preset.template;
78
- const status = isActive ? "●" : "○";
79
- items.push({
80
- label: ` ${status} ${preset.name}`,
81
- preset,
82
- });
83
- }
84
- }
85
-
86
- // Add custom option at the end
87
- items.push({
88
- label: "+ Custom Status Line",
89
- isCustom: true,
90
- });
91
-
92
- return items;
93
- };
94
-
95
- const listItems = buildListItems();
96
-
97
- // Keyboard handling
98
- useKeyboard((event) => {
99
- if (state.isSearching || state.modal) return;
100
-
101
- if (event.name === "up" || event.name === "k") {
102
- const newIndex = Math.max(0, statusLine.selectedIndex - 1);
103
- dispatch({ type: "STATUSLINE_SELECT", index: newIndex });
104
- } else if (event.name === "down" || event.name === "j") {
105
- const newIndex = Math.min(
106
- listItems.length - 1,
107
- statusLine.selectedIndex + 1,
108
- );
109
- dispatch({ type: "STATUSLINE_SELECT", index: newIndex });
110
- } else if (event.name === "p") {
111
- dispatch({ type: "STATUSLINE_SET_SCOPE", scope: "project" });
112
- } else if (event.name === "g") {
113
- dispatch({ type: "STATUSLINE_SET_SCOPE", scope: "global" });
114
- } else if (event.name === "r") {
115
- handleReset();
116
- } else if (event.name === "enter") {
117
- handleSelect();
118
- }
119
- });
120
-
121
- const saveStatusLine = async (template: string): Promise<void> => {
122
- if (statusLine.scope === "global") {
123
- await setGlobalStatusLine(template);
124
- } else {
125
- await setStatusLine(template, state.projectPath);
126
- }
127
- };
128
-
129
- const handleSelect = async () => {
130
- const item = listItems[statusLine.selectedIndex];
131
- if (!item || item.isCategory) return;
132
-
133
- const scopeLabel = statusLine.scope === "global" ? "Global" : "Project";
134
-
135
- if (item.isCustom) {
136
- const template = await modal.input(
137
- `Custom Status Line (${scopeLabel})`,
138
- "Enter template (use {model}, {cost}, etc.):",
139
- getCurrentStatusLine() || "",
140
- );
141
-
142
- if (template !== null) {
143
- modal.loading(`Saving custom status line...`);
144
- try {
145
- await saveStatusLine(template);
146
- modal.hideModal();
147
- await modal.message(
148
- "Saved",
149
- `Custom status line saved to ${scopeLabel}.\n\nRestart Claude Code to apply changes.`,
150
- "success",
151
- );
152
- fetchData();
153
- } catch (error) {
154
- modal.hideModal();
155
- await modal.message("Error", `Failed to save: ${error}`, "error");
156
- }
157
- }
158
- } else if (item.preset) {
159
- modal.loading(`Applying ${item.preset.name}...`);
160
- try {
161
- await saveStatusLine(item.preset.template);
162
- modal.hideModal();
163
- await modal.message(
164
- "Applied",
165
- `"${item.preset.name}" applied to ${scopeLabel}.\n\nRestart Claude Code to apply changes.`,
166
- "success",
167
- );
168
- fetchData();
169
- } catch (error) {
170
- modal.hideModal();
171
- await modal.message("Error", `Failed to apply: ${error}`, "error");
172
- }
173
- }
174
- };
175
-
176
- const handleReset = async () => {
177
- const scopeLabel = statusLine.scope === "global" ? "Global" : "Project";
178
- const confirmed = await modal.confirm(
179
- "Reset Status Line",
180
- `Clear the ${scopeLabel} status line configuration?`,
181
- );
182
-
183
- if (confirmed) {
184
- modal.loading("Resetting...");
185
- try {
186
- await saveStatusLine("");
187
- modal.hideModal();
188
- await modal.message(
189
- "Reset",
190
- `${scopeLabel} status line has been cleared.`,
191
- "success",
192
- );
193
- fetchData();
194
- } catch (error) {
195
- modal.hideModal();
196
- await modal.message("Error", `Failed to reset: ${error}`, "error");
197
- }
198
- }
199
- };
200
-
201
- // Get selected item
202
- const selectedItem = listItems[statusLine.selectedIndex];
203
-
204
- // Build preview
205
- const renderPreview = () => {
206
- if (isLoading) {
207
- return <text fg="gray">Loading status line settings...</text>;
208
- }
209
-
210
- if (!selectedItem || selectedItem.isCategory) {
211
- return (
212
- <box
213
- flexDirection="column"
214
- alignItems="center"
215
- justifyContent="center"
216
- flexGrow={1}
217
- >
218
- <text fg="gray">Select a theme to see preview</text>
219
- </box>
220
- );
221
- }
222
-
223
- if (selectedItem.isCustom) {
224
- return (
225
- <box flexDirection="column">
226
- <text fg="cyan">
227
- <strong>✨ Custom Status Line</strong>
228
- </text>
229
- <text fg="gray">Create your own unique status line!</text>
230
-
231
- <box marginTop={1} flexDirection="column">
232
- <text fg="yellow">
233
- <strong>Available variables:</strong>
234
- </text>
235
- <box marginTop={1} flexDirection="column">
236
- <text>
237
- <span fg="green">
238
- <strong>{"{model}"}</strong>
239
- </span>{" "}
240
- <span fg="gray">→</span> Model name
241
- </text>
242
- <text>
243
- <span fg="green">
244
- <strong>{"{model_short}"}</strong>
245
- </span>{" "}
246
- <span fg="gray">→</span> Short name
247
- </text>
248
- <text>
249
- <span fg="yellow">
250
- <strong>{"{cost}"}</strong>
251
- </span>{" "}
252
- <span fg="gray">→</span> Session cost
253
- </text>
254
- <text>
255
- <span fg="#5c9aff">
256
- <strong>{"{cwd}"}</strong>
257
- </span>{" "}
258
- <span fg="gray">→</span> Working directory
259
- </text>
260
- <text>
261
- <span fg="magenta">
262
- <strong>{"{git_branch}"}</strong>
263
- </span>{" "}
264
- <span fg="gray">→</span> Git branch
265
- </text>
266
- <text>
267
- <span fg="cyan">
268
- <strong>{"{input_tokens}"}</strong>
269
- </span>{" "}
270
- <span fg="gray">→</span> Input tokens
271
- </text>
272
- <text>
273
- <span fg="cyan">
274
- <strong>{"{output_tokens}"}</strong>
275
- </span>{" "}
276
- <span fg="gray">→</span> Output tokens
277
- </text>
278
- <text>
279
- <span fg="red">
280
- <strong>{"{session_duration}"}</strong>
281
- </span>
282
- <span fg="gray">→</span> Duration
283
- </text>
284
- </box>
285
- </box>
286
- </box>
287
- );
288
- }
289
-
290
- if (selectedItem.preset) {
291
- const example = selectedItem.preset.template
292
- .replace("{model}", "claude-sonnet-4")
293
- .replace("{model_short}", "sonnet")
294
- .replace("{cost}", "0.42")
295
- .replace("{cwd}", "~/myapp")
296
- .replace("{git_branch}", "main")
297
- .replace("{input_tokens}", "1.2k")
298
- .replace("{output_tokens}", "850")
299
- .replace("{session_duration}", "12m");
300
-
301
- return (
302
- <box flexDirection="column">
303
- <text fg="cyan">
304
- <strong>◆ {selectedItem.preset.name}</strong>
305
- </text>
306
- <text fg="gray">{selectedItem.preset.description}</text>
307
-
308
- <box marginTop={1} flexDirection="column">
309
- <text fg="yellow">
310
- <strong>Preview:</strong>
311
- </text>
312
- <box
313
- marginTop={1}
314
- paddingLeft={1}
315
- paddingRight={1}
316
- borderStyle="rounded"
317
- borderColor="green"
318
- >
319
- <text fg="white">{example}</text>
320
- </box>
321
- </box>
322
-
323
- <box marginTop={1} flexDirection="column">
324
- <text fg="#666666">Template:</text>
325
- <text fg="gray">{selectedItem.preset.template}</text>
326
- </box>
327
- </box>
328
- );
329
- }
330
-
331
- return null;
332
- };
333
-
334
- const renderListItem = (
335
- item: ListItem,
336
- _idx: number,
337
- isSelected: boolean,
338
- ) => {
339
- if (item.isCategory) {
340
- return (
341
- <text fg="magenta">
342
- <strong>▸ {item.label}</strong>
343
- </text>
344
- );
345
- }
346
-
347
- if (item.isCustom) {
348
- return isSelected ? (
349
- <text bg="cyan" fg="black">
350
- <strong> ➕ Custom Status Line </strong>
351
- </text>
352
- ) : (
353
- <text fg="cyan">
354
- <strong>{" "}➕ Custom Status Line</strong>
355
- </text>
356
- );
357
- }
358
-
359
- const currentForScope = getCurrentStatusLine();
360
- const isActive = item.preset && currentForScope === item.preset.template;
361
-
362
- return isSelected ? (
363
- <text bg="magenta" fg="white">
364
- {" "}
365
- {isActive ? "●" : "○"} {item.preset?.name || ""}{" "}
366
- </text>
367
- ) : (
368
- <text fg={isActive ? "green" : "white"}>
369
- {" "}
370
- {isActive ? "●" : "○"} {item.preset?.name || ""}
371
- </text>
372
- );
373
- };
374
-
375
- // Build status line content
376
- const scopeLabel = statusLine.scope === "project" ? "Project" : "Global";
377
- const currentValue = getCurrentStatusLine();
378
- const statusContent = (
379
- <>
380
- <text fg="gray">Scope: </text>
381
- <text fg="cyan">{scopeLabel}</text>
382
- <text fg="gray"> │ Current: </text>
383
- <text fg="green">
384
- {currentValue
385
- ? currentValue.slice(0, 35) + (currentValue.length > 35 ? "..." : "")
386
- : "(not set)"}
387
- </text>
388
- </>
389
- );
390
-
391
- return (
392
- <ScreenLayout
393
- title="claudeup Status Line"
394
- currentScreen={"statusline" as never}
395
- statusLine={statusContent}
396
- footerHints={[
397
- { keys: ["↑", "↓"], label: "nav" },
398
- { keys: ["Enter"], label: "apply" },
399
- { keys: ["p"], label: "project" },
400
- { keys: ["g"], label: "global" },
401
- { keys: ["r"], label: "reset" },
402
- ]}
403
- listPanel={
404
- <ScrollableList
405
- items={listItems}
406
- selectedIndex={statusLine.selectedIndex}
407
- renderItem={renderListItem}
408
- maxHeight={dimensions.listPanelHeight}
409
- />
410
- }
411
- detailPanel={renderPreview()}
412
- />
413
- );
414
- }
415
-
416
- export default StatusLineScreen;
@@ -1,10 +0,0 @@
1
- export { PluginsScreen } from "./PluginsScreen.js";
2
- export { McpScreen } from "./McpScreen.js";
3
- export { McpRegistryScreen } from "./McpRegistryScreen.js";
4
- export { SettingsScreen } from "./EnvVarsScreen.js";
5
- export { CliToolsScreen } from "./CliToolsScreen.js";
6
- export { ModelSelectorScreen } from "./ModelSelectorScreen.js";
7
- export { ProfilesScreen } from "./ProfilesScreen.js";
8
- export { SkillsScreen } from "./SkillsScreen.js";
9
- export { GitignoreScreen } from "./GitignoreScreen.js";
10
- export { AliasScreen } from "./AliasScreen.js";
@@ -1,34 +0,0 @@
1
- import { jsx as _jsx } from "@opentui/react/jsx-runtime";
2
- import { createContext, useContext, useState, } from "react";
3
- // ============================================================================
4
- // Context
5
- // ============================================================================
6
- const AnimationContext = createContext(null);
7
- export function AnimationProvider({ children }) {
8
- const [timelines, setTimelines] = useState(new Map());
9
- const registerTimeline = (id, timeline) => {
10
- setTimelines((prev) => {
11
- const next = new Map(prev);
12
- next.set(id, timeline);
13
- return next;
14
- });
15
- };
16
- const unregisterTimeline = (id) => {
17
- setTimelines((prev) => {
18
- const next = new Map(prev);
19
- next.delete(id);
20
- return next;
21
- });
22
- };
23
- return (_jsx(AnimationContext.Provider, { value: { timelines, registerTimeline, unregisterTimeline }, children: children }));
24
- }
25
- // ============================================================================
26
- // Hook: useAnimation
27
- // ============================================================================
28
- export function useAnimation() {
29
- const context = useContext(AnimationContext);
30
- if (!context) {
31
- throw new Error("useAnimation must be used within an AnimationProvider");
32
- }
33
- return context;
34
- }