claudeup 4.35.0 → 4.36.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 (63) hide show
  1. package/package.json +4 -4
  2. package/src/__tests__/git-worktree.test.ts +108 -0
  3. package/src/__tests__/scope-squares.test.tsx +165 -0
  4. package/src/__tests__/theme-adaptive-colors.test.ts +307 -0
  5. package/src/__tests__/uppercase-keybindings.test.ts +101 -0
  6. package/src/__tests__/worktree-registry-resolution.test.ts +107 -0
  7. package/src/main.tsx +21 -5
  8. package/src/opentui.d.ts +21 -12
  9. package/src/services/claude-settings.ts +37 -7
  10. package/src/services/git-worktree.ts +129 -0
  11. package/src/services/plugin-manager.ts +10 -1
  12. package/src/ui/App.tsx +19 -12
  13. package/src/ui/adapters/pluginsAdapter.ts +174 -168
  14. package/src/ui/adapters/settingsAdapter.ts +119 -116
  15. package/src/ui/adapters/skillsAdapter.ts +203 -196
  16. package/src/ui/components/CategoryHeader.tsx +9 -8
  17. package/src/ui/components/EmptyFilterState.tsx +10 -5
  18. package/src/ui/components/FlagDetailEditor.tsx +0 -0
  19. package/src/ui/components/ScopeIndicator.tsx +10 -6
  20. package/src/ui/components/ScrollableList.tsx +3 -2
  21. package/src/ui/components/SearchInput.tsx +2 -1
  22. package/src/ui/components/StyledText.tsx +5 -4
  23. package/src/ui/components/TabBar.tsx +4 -3
  24. package/src/ui/components/layout/FooterHints.tsx +37 -30
  25. package/src/ui/components/layout/Panel.tsx +6 -5
  26. package/src/ui/components/layout/ProgressBar.tsx +7 -6
  27. package/src/ui/components/layout/ScopeTabs.tsx +6 -5
  28. package/src/ui/components/layout/ScreenLayout.tsx +27 -24
  29. package/src/ui/components/layout/index.ts +3 -3
  30. package/src/ui/components/modals/ConfirmModal.tsx +12 -11
  31. package/src/ui/components/modals/InputModal.tsx +14 -6
  32. package/src/ui/components/modals/LoadingModal.tsx +6 -5
  33. package/src/ui/components/modals/MessageModal.tsx +9 -8
  34. package/src/ui/components/modals/SelectModal.tsx +11 -7
  35. package/src/ui/components/modals/VersionMismatchModal.tsx +14 -16
  36. package/src/ui/components/primitives/ActionHints.tsx +26 -26
  37. package/src/ui/components/primitives/DetailSection.tsx +13 -12
  38. package/src/ui/components/primitives/KeyValueLine.tsx +9 -8
  39. package/src/ui/components/primitives/ListCategoryRow.tsx +25 -27
  40. package/src/ui/components/primitives/MetaText.tsx +3 -3
  41. package/src/ui/components/primitives/ScopeDetail.tsx +48 -48
  42. package/src/ui/components/primitives/ScopeSquares.tsx +47 -22
  43. package/src/ui/components/primitives/SelectableRow.tsx +22 -16
  44. package/src/ui/hooks/useGitignoreModal.ts +78 -74
  45. package/src/ui/registry.ts +11 -11
  46. package/src/ui/renderers/cliToolRenderers.tsx +260 -203
  47. package/src/ui/renderers/gitignoreRenderers.tsx +43 -42
  48. package/src/ui/renderers/mcpRenderers.tsx +121 -117
  49. package/src/ui/renderers/pluginRenderers.tsx +528 -471
  50. package/src/ui/renderers/profileRenderers.tsx +346 -300
  51. package/src/ui/renderers/settingsRenderers.tsx +183 -176
  52. package/src/ui/renderers/skillRenderers.tsx +410 -326
  53. package/src/ui/screens/AliasScreen.tsx +1336 -1309
  54. package/src/ui/screens/CliToolsScreen.tsx +92 -40
  55. package/src/ui/screens/EnvVarsScreen.tsx +19 -13
  56. package/src/ui/screens/GitignoreScreen.tsx +510 -493
  57. package/src/ui/screens/McpRegistryScreen.tsx +28 -21
  58. package/src/ui/screens/McpScreen.tsx +12 -3
  59. package/src/ui/screens/PluginsScreen.tsx +17 -7
  60. package/src/ui/screens/ProfilesScreen.tsx +39 -23
  61. package/src/ui/screens/SkillsScreen.tsx +832 -688
  62. package/src/ui/theme-mode.ts +73 -0
  63. package/src/ui/theme.ts +147 -53
@@ -8,6 +8,7 @@ import { searchMcpServers, formatDate } from "../../services/mcp-registry.js";
8
8
  import { addMcpServer, setAllowMcp } from "../../services/claude-settings.js";
9
9
  import type { McpRegistryServer, McpServerConfig } from "../../types/index.js";
10
10
  import { ScrollableList } from "../components/ScrollableList.js";
11
+ import { theme } from "../theme.js";
11
12
 
12
13
  /**
13
14
  * Deduplicate servers by name, keeping only the latest version.
@@ -182,7 +183,9 @@ export function McpRegistryScreen() {
182
183
  dispatch({ type: "MCPREGISTRY_SELECT", index: newIndex });
183
184
  } else if (event.name === "l") {
184
185
  navigateToScreen("mcp");
185
- } else if (event.name === "R") {
186
+ } else if (event.name === "r" && event.shift) {
187
+ // `name` is always the unshifted key, so `=== "R"` never matched and
188
+ // refresh silently did nothing.
186
189
  loadServers(searchQuery);
187
190
  } else if (event.name === "enter") {
188
191
  handleInstall();
@@ -219,15 +222,17 @@ export function McpRegistryScreen() {
219
222
 
220
223
  const renderDetail = () => {
221
224
  if (isLoading) {
222
- return <text fg="gray">Loading...</text>;
225
+ return <text fg={theme.colors.muted}>Loading...</text>;
223
226
  }
224
227
 
225
228
  if (error) {
226
- return <text fg="red">Error: {error}</text>;
229
+ return <text fg={theme.colors.danger}>Error: {error}</text>;
227
230
  }
228
231
 
229
232
  if (!selectedServer) {
230
- return <text fg="gray">Select a server to see details</text>;
233
+ return (
234
+ <text fg={theme.colors.muted}>Select a server to see details</text>
235
+ );
231
236
  }
232
237
 
233
238
  const dateDisplay = selectedServer.published_at
@@ -240,40 +245,42 @@ export function McpRegistryScreen() {
240
245
 
241
246
  return (
242
247
  <box flexDirection="column">
243
- <text fg="magenta">
248
+ <text fg={theme.colors.accent}>
244
249
  <strong>{selectedServer.name}</strong>
245
250
  </text>
246
251
  <box marginTop={1}>
247
- <text>{selectedServer.short_description}</text>
252
+ <text fg={theme.colors.text}>{selectedServer.short_description}</text>
248
253
  </box>
249
254
  <box marginTop={1}>
250
- <text>
255
+ <text fg={theme.colors.text}>
251
256
  <strong>Version: </strong>
252
257
  </text>
253
- <text fg="green">{versionDisplay}</text>
258
+ <text fg={theme.colors.success}>{versionDisplay}</text>
254
259
  </box>
255
260
  <box>
256
- <text>
261
+ <text fg={theme.colors.text}>
257
262
  <strong>Published: </strong>
258
263
  </text>
259
- <text fg="cyan">{dateDisplay}</text>
264
+ <text fg={theme.colors.info}>{dateDisplay}</text>
260
265
  </box>
261
266
  <box marginTop={1} flexDirection="column">
262
- <text>
267
+ <text fg={theme.colors.text}>
263
268
  <strong>URL:</strong>
264
269
  </text>
265
- <text fg="cyan">{selectedServer.url}</text>
270
+ <text fg={theme.colors.info}>{selectedServer.url}</text>
266
271
  </box>
267
272
  {selectedServer.source_code_url && (
268
273
  <box marginTop={1} flexDirection="column">
269
- <text>
274
+ <text fg={theme.colors.text}>
270
275
  <strong>Source:</strong>
271
276
  </text>
272
- <text fg="gray">{selectedServer.source_code_url}</text>
277
+ <text fg={theme.colors.muted}>
278
+ {selectedServer.source_code_url}
279
+ </text>
273
280
  </box>
274
281
  )}
275
282
  <box marginTop={1}>
276
- <text fg="green">Press Enter to install</text>
283
+ <text fg={theme.colors.success}>Press Enter to install</text>
277
284
  </box>
278
285
  </box>
279
286
  );
@@ -286,17 +293,17 @@ export function McpRegistryScreen() {
286
293
  ) => {
287
294
  const version = server.version ? ` v${server.version}` : "";
288
295
  return isSelected ? (
289
- <text bg="magenta" fg="white">
296
+ <text bg={theme.colors.accent} fg={theme.hints.fg}>
290
297
  {" "}
291
298
  {server.name}
292
299
  {version}{" "}
293
300
  </text>
294
301
  ) : (
295
- <text>
302
+ <text fg={theme.colors.text}>
296
303
  <span>
297
304
  <strong>{server.name}</strong>
298
305
  </span>
299
- <span fg="green">{version}</span>
306
+ <span fg={theme.colors.success}>{version}</span>
300
307
  </text>
301
308
  );
302
309
  };
@@ -333,11 +340,11 @@ export function McpRegistryScreen() {
333
340
  footerHints={footerHints}
334
341
  listPanel={
335
342
  isLoading ? (
336
- <text fg="gray">Loading...</text>
343
+ <text fg={theme.colors.muted}>Loading...</text>
337
344
  ) : error ? (
338
- <text fg="red">Error: {error}</text>
345
+ <text fg={theme.colors.danger}>Error: {error}</text>
339
346
  ) : servers.length === 0 ? (
340
- <text fg="gray">No servers found</text>
347
+ <text fg={theme.colors.muted}>No servers found</text>
341
348
  ) : (
342
349
  <ScrollableList
343
350
  items={servers}
@@ -130,7 +130,11 @@ export function McpScreen() {
130
130
  try {
131
131
  await removeMcpServer(server.name, state.projectPath);
132
132
  modal.hideModal();
133
- await modal.message("Removed", `${server.name} has been removed.`, "success");
133
+ await modal.message(
134
+ "Removed",
135
+ `${server.name} has been removed.`,
136
+ "success",
137
+ );
134
138
  fetchData();
135
139
  } catch (error) {
136
140
  modal.hideModal();
@@ -146,7 +150,8 @@ export function McpScreen() {
146
150
  let config: McpServerConfig;
147
151
 
148
152
  if (server.type === "http") {
149
- if (!server.url) throw new Error(`${server.name} is missing its HTTP URL`);
153
+ if (!server.url)
154
+ throw new Error(`${server.name} is missing its HTTP URL`);
150
155
  config = { type: "http", url: server.url };
151
156
  } else {
152
157
  if (!server.command) {
@@ -184,7 +189,11 @@ export function McpScreen() {
184
189
  if (value === null) return;
185
190
 
186
191
  if (field.required && !value) {
187
- await modal.message("Required Field", `${field.label} is required.`, "error");
192
+ await modal.message(
193
+ "Required Field",
194
+ `${field.label} is required.`,
195
+ "error",
196
+ );
188
197
  return;
189
198
  }
190
199
 
@@ -6,7 +6,10 @@ import { ScreenLayout } from "../components/layout/index.js";
6
6
  import { ScrollableList } from "../components/ScrollableList.js";
7
7
  import { EmptyFilterState } from "../components/EmptyFilterState.js";
8
8
  import { fuzzyFilter } from "../../utils/fuzzy-search.js";
9
- import { defaultMarketplaces, getAllMarketplaces } from "../../data/marketplaces.js";
9
+ import {
10
+ defaultMarketplaces,
11
+ getAllMarketplaces,
12
+ } from "../../data/marketplaces.js";
10
13
  import { clearContentDriftCache } from "../../services/content-drift.js";
11
14
  import {
12
15
  diffVersions,
@@ -63,6 +66,7 @@ import {
63
66
  renderPluginRow,
64
67
  renderPluginDetail,
65
68
  } from "../renderers/pluginRenderers.js";
69
+ import { theme } from "../theme.js";
66
70
 
67
71
  export function PluginsScreen() {
68
72
  const { state, dispatch } = useApp();
@@ -297,11 +301,15 @@ export function PluginsScreen() {
297
301
  if (event.name === "r") handleRefresh();
298
302
  else if (event.name === "n") handleShowAddMarketplaceInstructions();
299
303
  else if (event.name === "t") handleShowTeamConfigHelp();
304
+ // `name` is ALWAYS the unshifted key — Shift+U parses as
305
+ // {name:"u", shift:true, sequence:"U"}, so `name === "U"` can never be
306
+ // true. It sat below the plain "u" branch, so pressing Shift+U silently
307
+ // installed at user scope instead of updating.
308
+ else if (event.name === "u" && event.shift) handleUpdate();
300
309
  else if (event.name === "u") handleScopeToggle("user");
301
310
  else if (event.name === "p") handleScopeToggle("project");
302
311
  else if (event.name === "l") handleScopeToggle("local");
303
312
  else if (event.name === "d") handleRemoveDeprecated();
304
- else if (event.name === "U") handleUpdate();
305
313
  else if (event.name === "a") handleUpdateAll();
306
314
  else if (event.name === "m") handleCheckMismatches();
307
315
  else if (event.name === "s") handleSaveAsProfile();
@@ -993,7 +1001,9 @@ export function PluginsScreen() {
993
1001
  // Reinstall under the new namespace in every scope it occupied, so
994
1002
  // the migration does not quietly change where the plugin is enabled.
995
1003
  const newId = `${plugin.name}@${migrateTo}`;
996
- for (const scope of removedFrom.length > 0 ? removedFrom : ["project" as PluginScope]) {
1004
+ for (const scope of removedFrom.length > 0
1005
+ ? removedFrom
1006
+ : ["project" as PluginScope]) {
997
1007
  modal.loading(
998
1008
  `Installing ${newId} (${scope})…\nclaude plugin install ${newId} --scope ${scope}`,
999
1009
  );
@@ -1057,10 +1067,10 @@ export function PluginsScreen() {
1057
1067
  ) {
1058
1068
  return (
1059
1069
  <box flexDirection="column" paddingLeft={1} paddingRight={1}>
1060
- <text fg="#7e57c2">
1070
+ <text fg={theme.colors.accent}>
1061
1071
  <strong>claudeup Plugins</strong>
1062
1072
  </text>
1063
- <text fg="gray">Loading...</text>
1073
+ <text fg={theme.colors.muted}>Loading...</text>
1064
1074
  </box>
1065
1075
  );
1066
1076
  }
@@ -1071,10 +1081,10 @@ export function PluginsScreen() {
1071
1081
  ) {
1072
1082
  return (
1073
1083
  <box flexDirection="column" paddingLeft={1} paddingRight={1}>
1074
- <text fg="#7e57c2">
1084
+ <text fg={theme.colors.accent}>
1075
1085
  <strong>claudeup Plugins</strong>
1076
1086
  </text>
1077
- <text fg="red">Error loading data</text>
1087
+ <text fg={theme.colors.danger}>Error loading data</text>
1078
1088
  </box>
1079
1089
  );
1080
1090
  }
@@ -12,10 +12,7 @@ import {
12
12
  exportProfileToJson,
13
13
  importProfileFromJson,
14
14
  } from "../../services/profiles.js";
15
- import {
16
- readSettings,
17
- writeSettings,
18
- } from "../../services/claude-settings.js";
15
+ import { readSettings, writeSettings } from "../../services/claude-settings.js";
19
16
  import {
20
17
  writeClipboard,
21
18
  readClipboard,
@@ -32,6 +29,7 @@ import {
32
29
  type ProfileListItem,
33
30
  } from "../renderers/profileRenderers.js";
34
31
  import { ScrollableList } from "../components/ScrollableList.js";
32
+ import { theme } from "../theme.js";
35
33
 
36
34
  export function ProfilesScreen() {
37
35
  const { state, dispatch } = useApp();
@@ -111,9 +109,7 @@ export function ProfilesScreen() {
111
109
  const handleApplyPredefined = async (profile: PredefinedProfile) => {
112
110
  const allPlugins = [
113
111
  ...profile.magusPlugins.map((p) => `${p}@magus`),
114
- ...profile.anthropicPlugins.map(
115
- (p) => `${p}@claude-plugins-official`,
116
- ),
112
+ ...profile.anthropicPlugins.map((p) => `${p}@claude-plugins-official`),
117
113
  ];
118
114
  const settingsCount = Object.keys(profile.settings).length;
119
115
 
@@ -161,7 +157,11 @@ export function ProfilesScreen() {
161
157
  );
162
158
  } catch (error) {
163
159
  modal.hideModal();
164
- await modal.message("Error", `Failed to apply profile: ${error}`, "error");
160
+ await modal.message(
161
+ "Error",
162
+ `Failed to apply profile: ${error}`,
163
+ "error",
164
+ );
165
165
  }
166
166
  };
167
167
 
@@ -217,7 +217,11 @@ export function ProfilesScreen() {
217
217
  );
218
218
  } catch (error) {
219
219
  modal.hideModal();
220
- await modal.message("Error", `Failed to apply profile: ${error}`, "error");
220
+ await modal.message(
221
+ "Error",
222
+ `Failed to apply profile: ${error}`,
223
+ "error",
224
+ );
221
225
  }
222
226
  };
223
227
 
@@ -242,7 +246,11 @@ export function ProfilesScreen() {
242
246
  );
243
247
  modal.hideModal();
244
248
  await fetchData();
245
- await modal.message("Renamed", `Profile renamed to "${newName.trim()}".`, "success");
249
+ await modal.message(
250
+ "Renamed",
251
+ `Profile renamed to "${newName.trim()}".`,
252
+ "success",
253
+ );
246
254
  } catch (error) {
247
255
  modal.hideModal();
248
256
  await modal.message("Error", `Failed to rename: ${error}`, "error");
@@ -345,10 +353,18 @@ export function ProfilesScreen() {
345
353
 
346
354
  modal.loading("Importing...");
347
355
  try {
348
- const id = await importProfileFromJson(json, targetScope, state.projectPath);
356
+ const id = await importProfileFromJson(
357
+ json,
358
+ targetScope,
359
+ state.projectPath,
360
+ );
349
361
  modal.hideModal();
350
362
  await fetchData();
351
- await modal.message("Imported", `Profile imported successfully (id: ${id}).`, "success");
363
+ await modal.message(
364
+ "Imported",
365
+ `Profile imported successfully (id: ${id}).`,
366
+ "success",
367
+ );
352
368
  } catch (error) {
353
369
  modal.hideModal();
354
370
  await modal.message("Error", `Failed to import: ${error}`, "error");
@@ -362,14 +378,16 @@ export function ProfilesScreen() {
362
378
  const projCount = profileList.filter((p) => p.scope === "project").length;
363
379
 
364
380
  const statusContent = (
365
- <text>
366
- <span fg="blue">{PREDEFINED_PROFILES.length} presets</span>
367
- <span fg="gray"> + </span>
368
- <span fg="cyan">{userCount} user</span>
369
- <span fg="gray"> + </span>
370
- <span fg="green">{projCount} project</span>
371
- <span fg="gray"> = </span>
372
- <span fg="white">{PREDEFINED_PROFILES.length + profileCount} total</span>
381
+ <text fg={theme.colors.text}>
382
+ <span fg={theme.colors.link}>{PREDEFINED_PROFILES.length} presets</span>
383
+ <span fg={theme.colors.muted}> + </span>
384
+ <span fg={theme.colors.info}>{userCount} user</span>
385
+ <span fg={theme.colors.muted}> + </span>
386
+ <span fg={theme.colors.success}>{projCount} project</span>
387
+ <span fg={theme.colors.muted}> = </span>
388
+ <span fg={theme.colors.text}>
389
+ {PREDEFINED_PROFILES.length + profileCount} total
390
+ </span>
373
391
  </text>
374
392
  );
375
393
 
@@ -380,9 +398,7 @@ export function ProfilesScreen() {
380
398
  : profilesState.selectedIndex;
381
399
 
382
400
  const loadingStatus =
383
- profilesState.profiles.status === "loading"
384
- ? true
385
- : false;
401
+ profilesState.profiles.status === "loading" ? true : false;
386
402
  const errorMessage =
387
403
  profilesState.profiles.status === "error"
388
404
  ? profilesState.profiles.error.message