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,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;