claudeup 4.18.0 → 4.22.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 (191) 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__/alias-adopt.test.ts +364 -0
  7. package/src/__tests__/alias-parser.test.ts +92 -0
  8. package/src/__tests__/alias-shell-writer.test.ts +7 -0
  9. package/src/__tests__/alias-store.test.ts +77 -0
  10. package/src/__tests__/cli-router.test.ts +65 -0
  11. package/src/__tests__/conventions-integration.test.ts +745 -0
  12. package/src/__tests__/conventions-manager.test.ts +1172 -0
  13. package/src/__tests__/doctor-bins.test.ts +35 -0
  14. package/src/__tests__/doctor.test.ts +450 -0
  15. package/src/__tests__/dual-write-prevention.test.ts +14 -4
  16. package/src/__tests__/file-locking.test.ts +208 -0
  17. package/src/__tests__/gitignore-prerun.test.ts +1 -0
  18. package/src/__tests__/install-command.test.ts +81 -0
  19. package/src/__tests__/install-plan.test.ts +98 -0
  20. package/src/__tests__/manifest.test.ts +136 -0
  21. package/src/__tests__/marketplace-refresh.test.ts +267 -0
  22. package/src/__tests__/plugin-requires.test.ts +133 -0
  23. package/src/__tests__/plugin-setup.test.ts +118 -0
  24. package/src/__tests__/profile-command.test.ts +116 -0
  25. package/src/__tests__/profile-materializer.test.ts +82 -0
  26. package/src/__tests__/profile-sync.test.ts +136 -0
  27. package/src/__tests__/registry-version-resolution.test.ts +74 -0
  28. package/src/__tests__/resolve-executable.test.ts +42 -0
  29. package/src/__tests__/resolver.test.ts +218 -0
  30. package/src/__tests__/symlink-manager.test.ts +99 -0
  31. package/src/__tests__/toolchain.test.ts +56 -0
  32. package/src/cli/claude.ts +21 -0
  33. package/src/cli/doctor.ts +132 -0
  34. package/src/cli/install.ts +360 -0
  35. package/src/cli/profile.ts +166 -0
  36. package/src/cli/router.ts +107 -0
  37. package/src/cli/update.ts +85 -0
  38. package/src/data/alias-flags.ts +11 -1
  39. package/src/data/cli-tools.ts +7 -7
  40. package/src/data/gitignore-defaults.ts +4 -0
  41. package/src/data/marketplaces.ts +12 -0
  42. package/src/data/predefined-profiles.ts +3 -4
  43. package/src/main.tsx +11 -154
  44. package/src/prerunner/index.ts +62 -27
  45. package/src/services/alias-shell-writer.ts +382 -8
  46. package/src/services/alias-store.ts +60 -0
  47. package/src/services/claude-cli.ts +19 -22
  48. package/src/services/claude-settings.ts +99 -26
  49. package/src/services/conventions-manager.ts +865 -0
  50. package/src/services/doctor-bins.ts +49 -0
  51. package/src/services/doctor.ts +509 -0
  52. package/src/services/install-plan.ts +107 -0
  53. package/src/services/manifest.ts +166 -0
  54. package/src/services/marketplace-refresh.ts +173 -0
  55. package/src/services/plugin-manager.ts +7 -1
  56. package/src/services/plugin-requires.ts +215 -0
  57. package/src/services/plugin-setup.ts +61 -4
  58. package/src/services/plugin-version-check.ts +9 -4
  59. package/src/services/profile-materializer.ts +61 -0
  60. package/src/services/profile-sync.ts +150 -0
  61. package/src/services/resolver.ts +293 -0
  62. package/src/services/symlink-manager.ts +146 -0
  63. package/src/services/toolchain.ts +97 -0
  64. package/src/services/version-check.ts +10 -11
  65. package/src/types/index.ts +138 -31
  66. package/src/ui/App.tsx +16 -11
  67. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  68. package/src/ui/components/modals/ConfirmModal.tsx +1 -1
  69. package/src/ui/screens/AliasScreen.tsx +491 -359
  70. package/src/ui/screens/CliToolsScreen.tsx +11 -3
  71. package/src/ui/screens/PluginsScreen.tsx +3 -1
  72. package/src/ui/screens/index.ts +0 -1
  73. package/src/ui/state/reducer.ts +6 -102
  74. package/src/ui/state/types.ts +8 -35
  75. package/src/utils/command-utils.ts +23 -0
  76. package/src/utils/file-locking.ts +144 -0
  77. package/src/data/alias-flags.js +0 -196
  78. package/src/data/cli-tools.js +0 -123
  79. package/src/data/gitignore-defaults.js +0 -24
  80. package/src/data/gitignore-reasons.js +0 -97
  81. package/src/data/gitignore-templates.js +0 -21
  82. package/src/data/marketplaces.js +0 -138
  83. package/src/data/mcp-servers.js +0 -509
  84. package/src/data/predefined-profiles.js +0 -248
  85. package/src/data/settings-catalog.js +0 -625
  86. package/src/data/skill-repos.js +0 -160
  87. package/src/data/statuslines.js +0 -159
  88. package/src/data/statuslines.ts +0 -188
  89. package/src/index.js +0 -4
  90. package/src/index.ts +0 -5
  91. package/src/main.js +0 -170
  92. package/src/prerunner/index.js +0 -252
  93. package/src/services/alias-settings.js +0 -51
  94. package/src/services/alias-shell-writer.js +0 -764
  95. package/src/services/alias-store.js +0 -77
  96. package/src/services/claude-cli.js +0 -189
  97. package/src/services/claude-runner.js +0 -28
  98. package/src/services/claude-settings.js +0 -1223
  99. package/src/services/gitignore-detector.js +0 -155
  100. package/src/services/gitignore-fixer.js +0 -231
  101. package/src/services/gitignore-prerun.js +0 -46
  102. package/src/services/gitignore-resolver.js +0 -143
  103. package/src/services/gitignore-service.js +0 -117
  104. package/src/services/local-marketplace.js +0 -339
  105. package/src/services/marketplace-fetcher.js +0 -96
  106. package/src/services/marketplace-sync.js +0 -94
  107. package/src/services/mcp-registry.js +0 -87
  108. package/src/services/plugin-manager.js +0 -473
  109. package/src/services/plugin-mcp-config.js +0 -177
  110. package/src/services/plugin-setup.js +0 -444
  111. package/src/services/plugin-version-check.js +0 -262
  112. package/src/services/profiles.js +0 -161
  113. package/src/services/settings-manager.js +0 -243
  114. package/src/services/skills-manager.js +0 -393
  115. package/src/services/skillsmp-client.js +0 -87
  116. package/src/services/update-cache.js +0 -52
  117. package/src/services/version-check.js +0 -99
  118. package/src/types/gitignore.js +0 -6
  119. package/src/types/index.js +0 -1
  120. package/src/ui/App.js +0 -346
  121. package/src/ui/adapters/pluginsAdapter.js +0 -139
  122. package/src/ui/adapters/settingsAdapter.js +0 -111
  123. package/src/ui/adapters/skillsAdapter.js +0 -154
  124. package/src/ui/components/CategoryHeader.js +0 -9
  125. package/src/ui/components/EmptyFilterState.js +0 -4
  126. package/src/ui/components/ScopeIndicator.js +0 -30
  127. package/src/ui/components/ScrollableList.js +0 -36
  128. package/src/ui/components/SearchInput.js +0 -19
  129. package/src/ui/components/StyledText.js +0 -39
  130. package/src/ui/components/TabBar.js +0 -19
  131. package/src/ui/components/layout/FooterHints.js +0 -29
  132. package/src/ui/components/layout/Panel.js +0 -6
  133. package/src/ui/components/layout/ProgressBar.js +0 -14
  134. package/src/ui/components/layout/ScopeTabs.js +0 -6
  135. package/src/ui/components/layout/ScreenLayout.js +0 -16
  136. package/src/ui/components/layout/index.js +0 -5
  137. package/src/ui/components/modals/ConfirmModal.js +0 -14
  138. package/src/ui/components/modals/InputModal.js +0 -5
  139. package/src/ui/components/modals/LoadingModal.js +0 -17
  140. package/src/ui/components/modals/MessageModal.js +0 -16
  141. package/src/ui/components/modals/ModalContainer.js +0 -137
  142. package/src/ui/components/modals/SelectModal.js +0 -18
  143. package/src/ui/components/modals/VersionMismatchModal.js +0 -36
  144. package/src/ui/components/modals/index.js +0 -6
  145. package/src/ui/components/primitives/ActionHints.js +0 -13
  146. package/src/ui/components/primitives/DetailSection.js +0 -7
  147. package/src/ui/components/primitives/KeyValueLine.js +0 -8
  148. package/src/ui/components/primitives/ListCategoryRow.js +0 -8
  149. package/src/ui/components/primitives/MetaText.js +0 -8
  150. package/src/ui/components/primitives/ScopeDetail.js +0 -32
  151. package/src/ui/components/primitives/ScopeSquares.js +0 -11
  152. package/src/ui/components/primitives/SelectableRow.js +0 -5
  153. package/src/ui/components/primitives/index.js +0 -8
  154. package/src/ui/hooks/index.js +0 -4
  155. package/src/ui/hooks/useAsyncData.js +0 -77
  156. package/src/ui/hooks/useGitignoreModal.js +0 -74
  157. package/src/ui/hooks/useKeyboard.js +0 -13
  158. package/src/ui/hooks/useKeyboardHandler.js +0 -39
  159. package/src/ui/hooks/useMismatchModal.js +0 -77
  160. package/src/ui/registry.js +0 -1
  161. package/src/ui/renderers/cliToolRenderers.js +0 -54
  162. package/src/ui/renderers/gitignoreRenderers.js +0 -46
  163. package/src/ui/renderers/mcpRenderers.js +0 -26
  164. package/src/ui/renderers/pluginRenderers.js +0 -124
  165. package/src/ui/renderers/profileRenderers.js +0 -177
  166. package/src/ui/renderers/settingsRenderers.js +0 -73
  167. package/src/ui/renderers/skillRenderers.js +0 -138
  168. package/src/ui/screens/AliasScreen.js +0 -1008
  169. package/src/ui/screens/CliToolsScreen.js +0 -338
  170. package/src/ui/screens/EnvVarsScreen.js +0 -152
  171. package/src/ui/screens/GitignoreScreen.js +0 -328
  172. package/src/ui/screens/McpRegistryScreen.js +0 -238
  173. package/src/ui/screens/McpScreen.js +0 -176
  174. package/src/ui/screens/ModelSelectorScreen.js +0 -296
  175. package/src/ui/screens/ModelSelectorScreen.tsx +0 -444
  176. package/src/ui/screens/PluginsScreen.js +0 -766
  177. package/src/ui/screens/ProfilesScreen.js +0 -298
  178. package/src/ui/screens/SkillsScreen.js +0 -549
  179. package/src/ui/screens/StatusLineScreen.js +0 -206
  180. package/src/ui/screens/StatusLineScreen.tsx +0 -416
  181. package/src/ui/screens/index.js +0 -10
  182. package/src/ui/state/AnimationContext.js +0 -34
  183. package/src/ui/state/AppContext.js +0 -162
  184. package/src/ui/state/DimensionsContext.js +0 -71
  185. package/src/ui/state/reducer.js +0 -543
  186. package/src/ui/state/types.js +0 -1
  187. package/src/ui/theme.js +0 -47
  188. package/src/utils/clipboard.js +0 -56
  189. package/src/utils/command-utils.js +0 -19
  190. package/src/utils/fuzzy-search.js +0 -101
  191. package/src/utils/string-utils.js +0 -62
@@ -1,176 +0,0 @@
1
- import { jsx as _jsx } from "@opentui/react/jsx-runtime";
2
- import { useEffect, useCallback, useMemo } from "react";
3
- import { useApp, useModal, useNavigation } 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 { getMcpServersByCategory, getCategoryDisplayName, categoryOrder, } from "../../data/mcp-servers.js";
8
- import { addMcpServer, removeMcpServer, getInstalledMcpServers, getEnabledMcpServers, } from "../../services/claude-settings.js";
9
- import { renderMcpRow, renderMcpDetail, } from "../renderers/mcpRenderers.js";
10
- import { ScrollableList } from "../components/ScrollableList.js";
11
- export function McpScreen() {
12
- const { state, dispatch } = useApp();
13
- const { mcp } = state;
14
- const modal = useModal();
15
- const { navigateToScreen } = useNavigation();
16
- const dimensions = useDimensions();
17
- const fetchData = useCallback(async () => {
18
- dispatch({ type: "MCP_DATA_LOADING" });
19
- try {
20
- const serversByCategory = getMcpServersByCategory();
21
- const installedServers = await getInstalledMcpServers(state.projectPath);
22
- const enabledServers = await getEnabledMcpServers(state.projectPath);
23
- const servers = [];
24
- for (const category of categoryOrder) {
25
- const categoryServers = serversByCategory[category];
26
- if (categoryServers) {
27
- servers.push(...categoryServers);
28
- }
29
- }
30
- const installedAsBooleans = {};
31
- for (const name of Object.keys(installedServers)) {
32
- installedAsBooleans[name] = true;
33
- }
34
- dispatch({
35
- type: "MCP_DATA_SUCCESS",
36
- servers,
37
- installedServers: { ...installedAsBooleans, ...enabledServers },
38
- });
39
- }
40
- catch (error) {
41
- dispatch({
42
- type: "MCP_DATA_ERROR",
43
- error: error instanceof Error ? error : new Error(String(error)),
44
- });
45
- }
46
- }, [dispatch, state.projectPath]);
47
- useEffect(() => {
48
- fetchData();
49
- }, [fetchData]);
50
- const allListItems = useMemo(() => {
51
- if (mcp.servers.status !== "success")
52
- return [];
53
- const serversByCategory = getMcpServersByCategory();
54
- const items = [];
55
- for (const category of categoryOrder) {
56
- const servers = serversByCategory[category];
57
- if (!servers || servers.length === 0)
58
- continue;
59
- items.push({ kind: "category", label: getCategoryDisplayName(category) });
60
- for (const server of servers) {
61
- const isInstalled = mcp.installedServers[server.name] !== undefined;
62
- items.push({ kind: "server", server, isInstalled });
63
- }
64
- }
65
- return items;
66
- }, [mcp.servers.status, mcp.installedServers]);
67
- useKeyboard((event) => {
68
- if (state.isSearching || state.modal)
69
- return;
70
- if (event.name === "/") {
71
- dispatch({ type: "SET_SEARCHING", isSearching: true });
72
- navigateToScreen("mcp-registry");
73
- return;
74
- }
75
- if (event.name === "up" || event.name === "k") {
76
- const newIndex = Math.max(0, mcp.selectedIndex - 1);
77
- dispatch({ type: "MCP_SELECT", index: newIndex });
78
- }
79
- else if (event.name === "down" || event.name === "j") {
80
- const newIndex = Math.min(allListItems.length - 1, mcp.selectedIndex + 1);
81
- dispatch({ type: "MCP_SELECT", index: newIndex });
82
- }
83
- else if (event.name === "r") {
84
- navigateToScreen("mcp-registry");
85
- }
86
- else if (event.name === "enter") {
87
- handleSelect();
88
- }
89
- });
90
- const handleSelect = async () => {
91
- const item = allListItems[mcp.selectedIndex];
92
- if (!item || item.kind === "category")
93
- return;
94
- const { server, isInstalled } = item;
95
- if (isInstalled) {
96
- const confirmed = await modal.confirm(`Remove ${server.name}?`, "This will remove the MCP server configuration.");
97
- if (confirmed) {
98
- modal.loading(`Removing ${server.name}...`);
99
- try {
100
- await removeMcpServer(server.name, state.projectPath);
101
- modal.hideModal();
102
- await modal.message("Removed", `${server.name} has been removed.`, "success");
103
- fetchData();
104
- }
105
- catch (error) {
106
- modal.hideModal();
107
- await modal.message("Error", `Failed to remove: ${error}`, "error");
108
- }
109
- }
110
- }
111
- else {
112
- await installMcpServer(server);
113
- }
114
- };
115
- const installMcpServer = async (server) => {
116
- let config;
117
- if (server.type === "http") {
118
- config = { type: "http", url: server.url };
119
- }
120
- else {
121
- config = {
122
- command: server.command,
123
- args: server.args ? [...server.args] : undefined,
124
- env: server.env ? { ...server.env } : undefined,
125
- };
126
- }
127
- if (server.requiresConfig && server.configFields) {
128
- for (const field of server.configFields) {
129
- const envVarName = field.envVar || field.name;
130
- const existingValue = process.env[envVarName];
131
- if (existingValue) {
132
- const useExisting = await modal.confirm(`Use ${envVarName}?`, `${envVarName} is set in your environment. Use the existing value?`);
133
- if (useExisting) {
134
- config.env = config.env || {};
135
- config.env[envVarName] = `\${${envVarName}}`;
136
- continue;
137
- }
138
- }
139
- const value = await modal.input(`Configure ${server.name}`, `${field.label}${field.required ? " (required)" : ""}:`, field.default);
140
- if (value === null)
141
- return;
142
- if (field.required && !value) {
143
- await modal.message("Required Field", `${field.label} is required.`, "error");
144
- return;
145
- }
146
- if (value) {
147
- config.env = config.env || {};
148
- config.env[envVarName] = value;
149
- }
150
- }
151
- }
152
- modal.loading(`Installing ${server.name}...`);
153
- try {
154
- await addMcpServer(server.name, config, state.projectPath);
155
- modal.hideModal();
156
- await modal.message("Installed", `${server.name} has been configured.\n\nRestart Claude Code to activate.`, "success");
157
- fetchData();
158
- }
159
- catch (error) {
160
- modal.hideModal();
161
- await modal.message("Error", `Failed to install: ${error}`, "error");
162
- }
163
- };
164
- const selectedItem = allListItems[mcp.selectedIndex];
165
- const isLoading = mcp.servers.status === "loading";
166
- const installedCount = Object.keys(mcp.installedServers).length;
167
- const enabledCount = Object.values(mcp.installedServers).filter((v) => v === true).length;
168
- const subtitle = `${enabledCount} enabled │ ${installedCount} configured │ / to search`;
169
- return (_jsx(ScreenLayout, { title: "claudeup MCP Servers", subtitle: subtitle, currentScreen: "mcp", footerHints: [
170
- { keys: ["↑", "↓"], label: "nav" },
171
- { keys: ["Enter"], label: "toggle" },
172
- { keys: ["/"], label: "search" },
173
- { keys: ["r"], label: "registry" },
174
- ], listPanel: _jsx(ScrollableList, { items: allListItems, selectedIndex: mcp.selectedIndex, renderItem: renderMcpRow, maxHeight: dimensions.listPanelHeight }), detailPanel: renderMcpDetail(selectedItem, isLoading) }));
175
- }
176
- export default McpScreen;
@@ -1,296 +0,0 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
- import { useMemo, useEffect } from "react";
3
- import { useApp } from "../state/AppContext.js";
4
- import { useDimensions } from "../state/DimensionsContext.js";
5
- import { fuzzyFilter, highlightMatches } from "../../utils/fuzzy-search.js";
6
- import { useKeyboard } from "../hooks/useKeyboard.js";
7
- import { ScrollableList } from "../components/ScrollableList.js";
8
- import { FooterHints } from "../components/layout/index.js";
9
- const RECENT_MODELS = [
10
- {
11
- id: "recent-1",
12
- type: "model",
13
- label: "MoonshotAI: Kimi K2 0905 (exacto)",
14
- provider: "OpenRouter",
15
- modelId: "moonshot/kimi-k2-0905",
16
- configured: true,
17
- },
18
- ];
19
- const ANTHROPIC_MODELS = [
20
- {
21
- id: "claude-opus-4",
22
- type: "model",
23
- label: "Claude Opus 4",
24
- provider: "Anthropic",
25
- modelId: "anthropic/claude-opus-4",
26
- configured: true,
27
- },
28
- {
29
- id: "claude-sonnet-4",
30
- type: "model",
31
- label: "Claude Sonnet 4",
32
- provider: "Anthropic",
33
- modelId: "anthropic/claude-sonnet-4",
34
- configured: true,
35
- },
36
- {
37
- id: "claude-opus-4.1",
38
- type: "model",
39
- label: "Claude Opus 4.1",
40
- provider: "Anthropic",
41
- modelId: "anthropic/claude-opus-4.1",
42
- configured: true,
43
- },
44
- {
45
- id: "claude-opus-4.5",
46
- type: "model",
47
- label: "Claude Opus 4.5",
48
- provider: "Anthropic",
49
- modelId: "anthropic/claude-opus-4.5",
50
- configured: true,
51
- },
52
- {
53
- id: "claude-3.5-haiku",
54
- type: "model",
55
- label: "Claude 3.5 Haiku",
56
- provider: "Anthropic",
57
- modelId: "anthropic/claude-3.5-haiku",
58
- configured: true,
59
- },
60
- {
61
- id: "claude-4.5-haiku",
62
- type: "model",
63
- label: "Claude 4.5 Haiku",
64
- provider: "Anthropic",
65
- modelId: "anthropic/claude-4.5-haiku",
66
- configured: true,
67
- },
68
- {
69
- id: "claude-3.7-sonnet",
70
- type: "model",
71
- label: "Claude 3.7 Sonnet",
72
- provider: "Anthropic",
73
- modelId: "anthropic/claude-3.7-sonnet",
74
- configured: true,
75
- },
76
- {
77
- id: "claude-sonnet-4.5",
78
- type: "model",
79
- label: "Claude Sonnet 4.5",
80
- provider: "Anthropic",
81
- modelId: "anthropic/claude-sonnet-4.5",
82
- configured: true,
83
- },
84
- {
85
- id: "claude-3.5-sonnet-new",
86
- type: "model",
87
- label: "Claude 3.5 Sonnet (New)",
88
- provider: "Anthropic",
89
- modelId: "anthropic/claude-3-5-sonnet-new",
90
- configured: true,
91
- },
92
- {
93
- id: "claude-3.5-sonnet-old",
94
- type: "model",
95
- label: "Claude 3.5 Sonnet (Old)",
96
- provider: "Anthropic",
97
- modelId: "anthropic/claude-3-5-sonnet-old",
98
- configured: true,
99
- },
100
- ];
101
- const OPENAI_MODELS = [
102
- {
103
- id: "gpt-5-codex",
104
- type: "model",
105
- label: "GPT-5 Codex",
106
- provider: "OpenAI",
107
- modelId: "openai/gpt-5-codex",
108
- configured: true,
109
- },
110
- {
111
- id: "gpt-5.1-codex",
112
- type: "model",
113
- label: "GPT-5.1 Codex",
114
- provider: "OpenAI",
115
- modelId: "openai/gpt-5.1-codex",
116
- configured: true,
117
- },
118
- {
119
- id: "gpt-5.1-codex-max",
120
- type: "model",
121
- label: "GPT-5.1 Codex Max",
122
- provider: "OpenAI",
123
- modelId: "openai/gpt-5.1-codex-max",
124
- configured: true,
125
- },
126
- {
127
- id: "gpt-5.1-codex-mini",
128
- type: "model",
129
- label: "GPT-5.1 Codex Mini",
130
- provider: "OpenAI",
131
- modelId: "openai/gpt-5.1-codex-mini",
132
- configured: true,
133
- },
134
- ];
135
- const XAI_MODELS = [
136
- {
137
- id: "grok-code-fast",
138
- type: "model",
139
- label: "Grok Code Fast",
140
- provider: "xAI",
141
- modelId: "xai/grok-code-fast",
142
- configured: false,
143
- }, // Configured check missing in reference image? No, line is very faint. Assuming not configured or just separator missing.
144
- ];
145
- export function ModelSelectorScreen() {
146
- const { state, dispatch } = useApp();
147
- const { modelSelector } = state;
148
- const dimensions = useDimensions();
149
- // Combine items into display list
150
- const allItems = useMemo(() => {
151
- return [
152
- {
153
- id: "header-recent",
154
- type: "header",
155
- label: "Recently used",
156
- provider: "OpenRouter",
157
- }, // Provider column used for right text
158
- ...RECENT_MODELS,
159
- {
160
- id: "header-anthropic",
161
- type: "header",
162
- label: "Anthropic",
163
- configured: true,
164
- },
165
- ...ANTHROPIC_MODELS,
166
- {
167
- id: "header-openai",
168
- type: "header",
169
- label: "OpenAI",
170
- configured: true,
171
- },
172
- ...OPENAI_MODELS,
173
- { id: "header-xai", type: "header", label: "xAI" },
174
- ...XAI_MODELS,
175
- ];
176
- }, []);
177
- // Filter items
178
- const filteredItems = useMemo(() => {
179
- if (!modelSelector.searchQuery)
180
- return allItems;
181
- // Filter only models
182
- const models = allItems.filter((i) => i.type === "model");
183
- const fuzzyResults = fuzzyFilter(models, modelSelector.searchQuery, (item) => item.label);
184
- // If searching, just show matched models without headers to keep it clean, or keep hierarchy?
185
- // "Switch Model" TUI usually flattens on search.
186
- return fuzzyResults.map((r) => ({ ...r.item, _matches: r.matches }));
187
- }, [allItems, modelSelector.searchQuery]);
188
- // Ensure selection is valid (not a header) on mount or search change
189
- useEffect(() => {
190
- // If current selection is a header or out of bounds, find first model
191
- const current = filteredItems[modelSelector.selectedIndex];
192
- if (!current || current.type === "header") {
193
- const firstModelIndex = filteredItems.findIndex((i) => i.type === "model");
194
- if (firstModelIndex !== -1 &&
195
- firstModelIndex !== modelSelector.selectedIndex) {
196
- dispatch({ type: "MODEL_SELECTOR_SELECT", index: firstModelIndex });
197
- }
198
- }
199
- }, [filteredItems, modelSelector.selectedIndex, dispatch]);
200
- // Handle keyboard
201
- useKeyboard((event) => {
202
- // Search input handling
203
- if (event.name === "backspace" || event.name === "delete") {
204
- dispatch({
205
- type: "MODEL_SELECTOR_SET_SEARCH",
206
- query: modelSelector.searchQuery.slice(0, -1),
207
- });
208
- return;
209
- }
210
- // Toggle Task Size
211
- if (event.name === "tab") {
212
- const newSize = modelSelector.taskSize === "large" ? "small" : "large";
213
- dispatch({ type: "MODEL_SELECTOR_SET_TASK_SIZE", size: newSize });
214
- return;
215
- }
216
- // Navigation
217
- if (event.name === "up" || (event.ctrl && event.name === "p")) {
218
- // Standard navigation
219
- let newIndex = modelSelector.selectedIndex - 1;
220
- // Skip headers while moving up
221
- while (newIndex >= 0 && filteredItems[newIndex]?.type === "header") {
222
- newIndex--;
223
- }
224
- if (newIndex >= 0) {
225
- dispatch({ type: "MODEL_SELECTOR_SELECT", index: newIndex });
226
- }
227
- return;
228
- }
229
- if (event.name === "down" || (event.ctrl && event.name === "n")) {
230
- let newIndex = modelSelector.selectedIndex + 1;
231
- // Skip headers while moving down
232
- while (newIndex < filteredItems.length &&
233
- filteredItems[newIndex]?.type === "header") {
234
- newIndex++;
235
- }
236
- if (newIndex < filteredItems.length) {
237
- dispatch({ type: "MODEL_SELECTOR_SELECT", index: newIndex });
238
- }
239
- return;
240
- }
241
- if (event.name === "enter" || event.name === "return") {
242
- // Just log selection for now or exit
243
- // dispatch({ type: 'NAVIGATE', route: { screen: 'plugins' } });
244
- return;
245
- }
246
- // Regular text input (single character keys)
247
- if (event.name.length === 1 && !event.ctrl && !event.shift) {
248
- dispatch({
249
- type: "MODEL_SELECTOR_SET_SEARCH",
250
- query: modelSelector.searchQuery + event.name,
251
- });
252
- return;
253
- }
254
- });
255
- // Render list item
256
- const renderItem = (item, _index, isSelected) => {
257
- if (item.type === "header") {
258
- // Header style: "Name ----------------------------- Status"
259
- const status = item.configured ? "✔ Configured" : "";
260
- return (_jsxs("box", { margin: 0, children: [_jsxs("text", { fg: "gray", children: [item.label, " "] }), _jsx("text", { fg: "#888888", children: "\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF\u23AF" }), status && _jsxs("text", { fg: "green", children: [" ", status] }), item.provider && _jsxs("text", { fg: "#5c9aff", children: [" ", item.provider] })] }));
261
- }
262
- // Model item
263
- // Matches highlighting
264
- const matches = item._matches;
265
- const labelSegment = matches ? highlightMatches(item.label, matches) : null;
266
- const Label = () => (_jsx("text", { fg: isSelected ? "white" : "white", children: labelSegment
267
- ? labelSegment.map((seg, i) => (_jsx("text", { fg: seg.highlighted
268
- ? isSelected
269
- ? "white"
270
- : "cyan"
271
- : isSelected
272
- ? "white"
273
- : "white", children: seg.highlighted ? (_jsx("u", { children: _jsx("strong", { children: seg.text }) })) : (seg.text) }, i)))
274
- : item.label }));
275
- if (isSelected) {
276
- return (_jsxs("box", { overflow: "hidden", children: [_jsx("text", { bg: "magenta", fg: "white", children: " " }), _jsx("text", { bg: "magenta", fg: "white", children: _jsx(Label, {}) }), _jsx("box", { flexGrow: 1, children: _jsx("text", { bg: "magenta", children: " " }) }), item.provider && (_jsxs("text", { bg: "magenta", fg: "white", children: [item.provider, " "] }))] }));
277
- }
278
- else {
279
- return (_jsxs("box", { overflow: "hidden", children: [_jsx(Label, {}), _jsx("box", { flexGrow: 1 }), item.provider && _jsx("text", { fg: "gray", children: item.provider })] }));
280
- }
281
- };
282
- const footerHints = [
283
- { keys: ["↑", "↓"], label: "choose" },
284
- { keys: ["tab"], label: "toggle type" },
285
- { keys: ["enter"], label: "choose" },
286
- { keys: ["esc"], label: "exit" },
287
- ];
288
- // Available height calculation
289
- // Header: 1 line (title) + 1 line (search) + 1 line (separator) = 3 lines?
290
- // Title line: Switch Model /// (o) Large
291
- // Cursor line: > c
292
- // Separator: handled by list? or explicit?
293
- const listHeight = Math.max(5, dimensions.contentHeight - 5);
294
- return (_jsxs("box", { flexDirection: "column", height: dimensions.contentHeight, children: [_jsx("box", { flexDirection: "row", border: true, borderStyle: "single", borderColor: "#7e57c2", paddingLeft: 1, paddingRight: 1, children: _jsxs("box", { flexDirection: "column", flexGrow: 1, children: [_jsx("box", { flexDirection: "row", justifyContent: "space-between", children: _jsxs("box", { children: [_jsx("text", { fg: "#7e57c2", children: "Switch Model " }), _jsxs("text", { fg: modelSelector.taskSize === "large" ? "white" : "gray", children: [modelSelector.taskSize === "large" ? "◎" : "○", " Large Task", " "] }), _jsxs("text", { fg: modelSelector.taskSize === "small" ? "white" : "gray", children: [modelSelector.taskSize === "small" ? "◎" : "○", " Small Task"] })] }) }), _jsxs("box", { flexDirection: "row", marginTop: 1, children: [_jsx("text", { fg: "green", children: "> " }), _jsx("text", { children: modelSelector.searchQuery }), _jsx("text", { bg: "gray", fg: "black", children: " " })] })] }) }), _jsx("box", { flexGrow: 1, paddingLeft: 1, paddingRight: 1, children: _jsx(ScrollableList, { items: filteredItems, selectedIndex: modelSelector.selectedIndex, renderItem: renderItem, maxHeight: listHeight, getKey: (item) => item.id }) }), _jsx("box", { height: 1, children: _jsx(FooterHints, { hints: footerHints }) })] }));
295
- }
296
- export default ModelSelectorScreen;