claudeup 4.19.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 (180) 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-refresh.test.ts +267 -0
  18. package/src/__tests__/plugin-requires.test.ts +133 -0
  19. package/src/__tests__/plugin-setup.test.ts +7 -0
  20. package/src/__tests__/profile-command.test.ts +116 -0
  21. package/src/__tests__/profile-materializer.test.ts +82 -0
  22. package/src/__tests__/profile-sync.test.ts +136 -0
  23. package/src/__tests__/registry-version-resolution.test.ts +74 -0
  24. package/src/__tests__/resolve-executable.test.ts +42 -0
  25. package/src/__tests__/resolver.test.ts +218 -0
  26. package/src/__tests__/symlink-manager.test.ts +99 -0
  27. package/src/__tests__/toolchain.test.ts +56 -0
  28. package/src/cli/claude.ts +21 -0
  29. package/src/cli/doctor.ts +132 -0
  30. package/src/cli/install.ts +360 -0
  31. package/src/cli/profile.ts +166 -0
  32. package/src/cli/router.ts +107 -0
  33. package/src/cli/update.ts +85 -0
  34. package/src/data/cli-tools.ts +7 -7
  35. package/src/data/gitignore-defaults.ts +4 -0
  36. package/src/data/marketplaces.ts +12 -0
  37. package/src/data/predefined-profiles.ts +3 -4
  38. package/src/main.tsx +11 -154
  39. package/src/prerunner/index.ts +62 -27
  40. package/src/services/claude-cli.ts +19 -22
  41. package/src/services/claude-settings.ts +99 -26
  42. package/src/services/conventions-manager.ts +865 -0
  43. package/src/services/doctor-bins.ts +49 -0
  44. package/src/services/doctor.ts +509 -0
  45. package/src/services/install-plan.ts +107 -0
  46. package/src/services/manifest.ts +166 -0
  47. package/src/services/marketplace-refresh.ts +173 -0
  48. package/src/services/plugin-manager.ts +7 -1
  49. package/src/services/plugin-requires.ts +215 -0
  50. package/src/services/plugin-version-check.ts +9 -4
  51. package/src/services/profile-materializer.ts +61 -0
  52. package/src/services/profile-sync.ts +150 -0
  53. package/src/services/resolver.ts +293 -0
  54. package/src/services/symlink-manager.ts +146 -0
  55. package/src/services/toolchain.ts +97 -0
  56. package/src/services/version-check.ts +10 -11
  57. package/src/types/index.ts +138 -31
  58. package/src/ui/App.tsx +0 -4
  59. package/src/ui/screens/CliToolsScreen.tsx +11 -3
  60. package/src/ui/screens/index.ts +0 -1
  61. package/src/ui/state/reducer.ts +0 -101
  62. package/src/ui/state/types.ts +0 -35
  63. package/src/utils/command-utils.ts +23 -0
  64. package/src/utils/file-locking.ts +144 -0
  65. package/src/data/alias-flags.js +0 -205
  66. package/src/data/cli-tools.js +0 -123
  67. package/src/data/gitignore-defaults.js +0 -24
  68. package/src/data/gitignore-reasons.js +0 -97
  69. package/src/data/gitignore-templates.js +0 -21
  70. package/src/data/marketplaces.js +0 -138
  71. package/src/data/mcp-servers.js +0 -509
  72. package/src/data/predefined-profiles.js +0 -248
  73. package/src/data/settings-catalog.js +0 -625
  74. package/src/data/skill-repos.js +0 -160
  75. package/src/data/statuslines.js +0 -159
  76. package/src/data/statuslines.ts +0 -188
  77. package/src/index.js +0 -4
  78. package/src/index.ts +0 -5
  79. package/src/main.js +0 -170
  80. package/src/prerunner/index.js +0 -252
  81. package/src/services/alias-settings.js +0 -51
  82. package/src/services/alias-shell-writer.js +0 -1018
  83. package/src/services/alias-store.js +0 -129
  84. package/src/services/claude-cli.js +0 -189
  85. package/src/services/claude-runner.js +0 -28
  86. package/src/services/claude-settings.js +0 -1223
  87. package/src/services/gitignore-detector.js +0 -155
  88. package/src/services/gitignore-fixer.js +0 -231
  89. package/src/services/gitignore-prerun.js +0 -46
  90. package/src/services/gitignore-resolver.js +0 -143
  91. package/src/services/gitignore-service.js +0 -117
  92. package/src/services/local-marketplace.js +0 -339
  93. package/src/services/marketplace-fetcher.js +0 -96
  94. package/src/services/marketplace-sync.js +0 -94
  95. package/src/services/mcp-registry.js +0 -87
  96. package/src/services/plugin-manager.js +0 -473
  97. package/src/services/plugin-mcp-config.js +0 -177
  98. package/src/services/plugin-setup.js +0 -499
  99. package/src/services/plugin-version-check.js +0 -262
  100. package/src/services/profiles.js +0 -161
  101. package/src/services/settings-manager.js +0 -243
  102. package/src/services/skills-manager.js +0 -393
  103. package/src/services/skillsmp-client.js +0 -87
  104. package/src/services/update-cache.js +0 -52
  105. package/src/services/version-check.js +0 -99
  106. package/src/types/gitignore.js +0 -6
  107. package/src/types/index.js +0 -1
  108. package/src/ui/App.js +0 -355
  109. package/src/ui/adapters/pluginsAdapter.js +0 -139
  110. package/src/ui/adapters/settingsAdapter.js +0 -111
  111. package/src/ui/adapters/skillsAdapter.js +0 -154
  112. package/src/ui/components/CategoryHeader.js +0 -9
  113. package/src/ui/components/EmptyFilterState.js +0 -4
  114. package/src/ui/components/FlagDetailEditor.js +0 -0
  115. package/src/ui/components/ScopeIndicator.js +0 -30
  116. package/src/ui/components/ScrollableList.js +0 -36
  117. package/src/ui/components/SearchInput.js +0 -19
  118. package/src/ui/components/StyledText.js +0 -39
  119. package/src/ui/components/TabBar.js +0 -19
  120. package/src/ui/components/layout/FooterHints.js +0 -29
  121. package/src/ui/components/layout/Panel.js +0 -6
  122. package/src/ui/components/layout/ProgressBar.js +0 -14
  123. package/src/ui/components/layout/ScopeTabs.js +0 -6
  124. package/src/ui/components/layout/ScreenLayout.js +0 -16
  125. package/src/ui/components/layout/index.js +0 -5
  126. package/src/ui/components/modals/ConfirmModal.js +0 -14
  127. package/src/ui/components/modals/InputModal.js +0 -5
  128. package/src/ui/components/modals/LoadingModal.js +0 -17
  129. package/src/ui/components/modals/MessageModal.js +0 -16
  130. package/src/ui/components/modals/ModalContainer.js +0 -137
  131. package/src/ui/components/modals/SelectModal.js +0 -18
  132. package/src/ui/components/modals/VersionMismatchModal.js +0 -36
  133. package/src/ui/components/modals/index.js +0 -6
  134. package/src/ui/components/primitives/ActionHints.js +0 -13
  135. package/src/ui/components/primitives/DetailSection.js +0 -7
  136. package/src/ui/components/primitives/KeyValueLine.js +0 -8
  137. package/src/ui/components/primitives/ListCategoryRow.js +0 -8
  138. package/src/ui/components/primitives/MetaText.js +0 -8
  139. package/src/ui/components/primitives/ScopeDetail.js +0 -32
  140. package/src/ui/components/primitives/ScopeSquares.js +0 -11
  141. package/src/ui/components/primitives/SelectableRow.js +0 -5
  142. package/src/ui/components/primitives/index.js +0 -8
  143. package/src/ui/hooks/index.js +0 -4
  144. package/src/ui/hooks/useAsyncData.js +0 -77
  145. package/src/ui/hooks/useGitignoreModal.js +0 -74
  146. package/src/ui/hooks/useKeyboard.js +0 -13
  147. package/src/ui/hooks/useKeyboardHandler.js +0 -39
  148. package/src/ui/hooks/useMismatchModal.js +0 -77
  149. package/src/ui/registry.js +0 -1
  150. package/src/ui/renderers/cliToolRenderers.js +0 -54
  151. package/src/ui/renderers/gitignoreRenderers.js +0 -46
  152. package/src/ui/renderers/mcpRenderers.js +0 -26
  153. package/src/ui/renderers/pluginRenderers.js +0 -124
  154. package/src/ui/renderers/profileRenderers.js +0 -177
  155. package/src/ui/renderers/settingsRenderers.js +0 -73
  156. package/src/ui/renderers/skillRenderers.js +0 -138
  157. package/src/ui/screens/AliasScreen.js +0 -1111
  158. package/src/ui/screens/CliToolsScreen.js +0 -338
  159. package/src/ui/screens/EnvVarsScreen.js +0 -152
  160. package/src/ui/screens/GitignoreScreen.js +0 -328
  161. package/src/ui/screens/McpRegistryScreen.js +0 -238
  162. package/src/ui/screens/McpScreen.js +0 -176
  163. package/src/ui/screens/ModelSelectorScreen.js +0 -296
  164. package/src/ui/screens/ModelSelectorScreen.tsx +0 -444
  165. package/src/ui/screens/PluginsScreen.js +0 -769
  166. package/src/ui/screens/ProfilesScreen.js +0 -298
  167. package/src/ui/screens/SkillsScreen.js +0 -549
  168. package/src/ui/screens/StatusLineScreen.js +0 -206
  169. package/src/ui/screens/StatusLineScreen.tsx +0 -416
  170. package/src/ui/screens/index.js +0 -10
  171. package/src/ui/state/AnimationContext.js +0 -34
  172. package/src/ui/state/AppContext.js +0 -162
  173. package/src/ui/state/DimensionsContext.js +0 -71
  174. package/src/ui/state/reducer.js +0 -547
  175. package/src/ui/state/types.js +0 -1
  176. package/src/ui/theme.js +0 -47
  177. package/src/utils/clipboard.js +0 -56
  178. package/src/utils/command-utils.js +0 -19
  179. package/src/utils/fuzzy-search.js +0 -101
  180. 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;