claudeup 3.8.0 → 3.10.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.
package/src/ui/App.tsx CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  CliToolsScreen,
21
21
  ModelSelectorScreen,
22
22
  ProfilesScreen,
23
+ SkillsScreen,
23
24
  } from "./screens/index.js";
24
25
  import type { Screen } from "./state/types.js";
25
26
  import { repairAllMarketplaces } from "../services/local-marketplace.js";
@@ -57,6 +58,8 @@ function Router() {
57
58
  return <ModelSelectorScreen />;
58
59
  case "profiles":
59
60
  return <ProfilesScreen />;
61
+ case "skills":
62
+ return <SkillsScreen />;
60
63
  default:
61
64
  return <PluginsScreen />;
62
65
  }
@@ -116,23 +119,26 @@ function GlobalKeyHandler({
116
119
  "cli-tools",
117
120
  "model-selector",
118
121
  "profiles",
122
+ "skills",
119
123
  ].includes(state.currentRoute.screen);
120
124
 
121
125
  if (isTopLevel) {
122
126
  if (input === "1") navigateToScreen("plugins");
123
- else if (input === "2") navigateToScreen("mcp");
124
- else if (input === "3") navigateToScreen("settings");
125
- else if (input === "4") navigateToScreen("cli-tools");
127
+ else if (input === "2") navigateToScreen("skills");
128
+ else if (input === "3") navigateToScreen("mcp");
129
+ else if (input === "4") navigateToScreen("settings");
126
130
  else if (input === "5") navigateToScreen("profiles");
131
+ else if (input === "6") navigateToScreen("cli-tools");
127
132
 
128
133
  // Tab navigation cycling
129
134
  if (key.tab) {
130
135
  const screens: Screen[] = [
131
136
  "plugins",
137
+ "skills",
132
138
  "mcp",
133
139
  "settings",
134
- "cli-tools",
135
140
  "profiles",
141
+ "cli-tools",
136
142
  ];
137
143
  const currentIndex = screens.indexOf(
138
144
  state.currentRoute.screen as Screen,
@@ -171,9 +177,9 @@ function GlobalKeyHandler({
171
177
  ? This help
172
178
 
173
179
  Quick Navigation
174
- 1 Plugins 4 CLI Tools
175
- 2 MCP Servers 5 Profiles
176
- 3 Settings
180
+ 1 Plugins 4 Settings
181
+ 2 Skills 5 Profiles
182
+ 3 MCP Servers 6 CLI Tools
177
183
 
178
184
  Plugin Actions
179
185
  u Update d Uninstall
@@ -0,0 +1,4 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
+ export function EmptyFilterState({ query, entityName = "items", }) {
3
+ return (_jsxs("box", { flexDirection: "column", marginTop: 2, paddingLeft: 2, paddingRight: 2, children: [_jsxs("text", { fg: "yellow", children: ["No ", entityName, " found for \"", query, "\""] }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: "Try a different search term, or if you think" }) }), _jsx("text", { fg: "gray", children: "this is a bug, report it at:" }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "#5c9aff", children: "github.com/MadAppGang/magus/issues" }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: "Press Esc to clear the filter." }) })] }));
4
+ }
@@ -0,0 +1,27 @@
1
+ import React from "react";
2
+
3
+ interface EmptyFilterStateProps {
4
+ query: string;
5
+ entityName?: string; // "plugins", "skills", "MCP servers"
6
+ }
7
+
8
+ export function EmptyFilterState({
9
+ query,
10
+ entityName = "items",
11
+ }: EmptyFilterStateProps) {
12
+ return (
13
+ <box flexDirection="column" marginTop={2} paddingLeft={2} paddingRight={2}>
14
+ <text fg="yellow">No {entityName} found for "{query}"</text>
15
+ <box marginTop={1}>
16
+ <text fg="gray">Try a different search term, or if you think</text>
17
+ </box>
18
+ <text fg="gray">this is a bug, report it at:</text>
19
+ <box marginTop={1}>
20
+ <text fg="#5c9aff">github.com/MadAppGang/magus/issues</text>
21
+ </box>
22
+ <box marginTop={1}>
23
+ <text fg="gray">Press Esc to clear the filter.</text>
24
+ </box>
25
+ </box>
26
+ );
27
+ }
@@ -2,10 +2,11 @@ import { jsxs as _jsxs, jsx as _jsx } from "@opentui/react/jsx-runtime";
2
2
  import { useKeyboardHandler } from "../hooks/useKeyboardHandler";
3
3
  const TABS = [
4
4
  { key: "1", label: "Plugins", screen: "plugins" },
5
- { key: "2", label: "MCP", screen: "mcp" },
6
- { key: "3", label: "Settings", screen: "settings" },
7
- { key: "4", label: "CLI", screen: "cli-tools" },
5
+ { key: "2", label: "Skills", screen: "skills" },
6
+ { key: "3", label: "MCP", screen: "mcp" },
7
+ { key: "4", label: "Settings", screen: "settings" },
8
8
  { key: "5", label: "Profiles", screen: "profiles" },
9
+ { key: "6", label: "CLI", screen: "cli-tools" },
9
10
  ];
10
11
  export function TabBar({ currentScreen, onTabChange }) {
11
12
  // Handle number key shortcuts (1-5)
@@ -10,10 +10,11 @@ interface Tab {
10
10
 
11
11
  const TABS: Tab[] = [
12
12
  { key: "1", label: "Plugins", screen: "plugins" },
13
- { key: "2", label: "MCP", screen: "mcp" },
14
- { key: "3", label: "Settings", screen: "settings" },
15
- { key: "4", label: "CLI", screen: "cli-tools" },
13
+ { key: "2", label: "Skills", screen: "skills" },
14
+ { key: "3", label: "MCP", screen: "mcp" },
15
+ { key: "4", label: "Settings", screen: "settings" },
16
16
  { key: "5", label: "Profiles", screen: "profiles" },
17
+ { key: "6", label: "CLI", screen: "cli-tools" },
17
18
  ];
18
19
 
19
20
  interface TabBarProps {
@@ -6,6 +6,7 @@ import { useKeyboard } from "../hooks/useKeyboard.js";
6
6
  import { ScreenLayout } from "../components/layout/index.js";
7
7
  import { CategoryHeader } from "../components/CategoryHeader.js";
8
8
  import { ScrollableList } from "../components/ScrollableList.js";
9
+ import { EmptyFilterState } from "../components/EmptyFilterState.js";
9
10
  import { fuzzyFilter, highlightMatches } from "../../utils/fuzzy-search.js";
10
11
  import { getAllMarketplaces } from "../../data/marketplaces.js";
11
12
  import { getAvailablePlugins, refreshAllMarketplaces, clearMarketplaceCache, getLocalMarketplacesInfo, } from "../../services/plugin-manager.js";
@@ -175,31 +176,17 @@ export function PluginsScreen() {
175
176
  }
176
177
  return;
177
178
  }
178
- // Navigation — always works (even during search)
179
+ // Navigation — always works; exits search mode on navigate
179
180
  if (event.name === "up" || event.name === "k") {
180
- // 'k' navigates when query is empty, otherwise appends to search
181
- if (event.name === "k" && (hasQuery || isSearchActive)) {
182
- dispatch({
183
- type: "PLUGINS_SET_SEARCH",
184
- query: pluginsState.searchQuery + event.name,
185
- });
186
- dispatch({ type: "PLUGINS_SELECT", index: 0 });
187
- return;
188
- }
181
+ if (isSearchActive)
182
+ dispatch({ type: "SET_SEARCHING", isSearching: false });
189
183
  const newIndex = Math.max(0, pluginsState.selectedIndex - 1);
190
184
  dispatch({ type: "PLUGINS_SELECT", index: newIndex });
191
185
  return;
192
186
  }
193
187
  if (event.name === "down" || event.name === "j") {
194
- // 'j' navigates when query is empty, otherwise appends to search
195
- if (event.name === "j" && (hasQuery || isSearchActive)) {
196
- dispatch({
197
- type: "PLUGINS_SET_SEARCH",
198
- query: pluginsState.searchQuery + event.name,
199
- });
200
- dispatch({ type: "PLUGINS_SELECT", index: 0 });
201
- return;
202
- }
188
+ if (isSearchActive)
189
+ dispatch({ type: "SET_SEARCHING", isSearching: false });
203
190
  const newIndex = Math.min(selectableItems.length - 1, pluginsState.selectedIndex + 1);
204
191
  dispatch({ type: "PLUGINS_SELECT", index: newIndex });
205
192
  return;
@@ -229,9 +216,9 @@ export function PluginsScreen() {
229
216
  }
230
217
  return;
231
218
  }
232
- // When search query is non-empty, printable letters go to the query
233
- // (shortcuts are suspended while filtering, digits skip to let tab nav work)
234
- if (hasQuery || isSearchActive) {
219
+ // When actively typing in search, letters go to the query
220
+ // After Enter (isSearchActive=false, hasQuery=true), shortcuts resume
221
+ if (isSearchActive) {
235
222
  if (event.name.length === 1 && !event.ctrl && !event.meta && !/[0-9]/.test(event.name)) {
236
223
  dispatch({
237
224
  type: "PLUGINS_SET_SEARCH",
@@ -241,7 +228,7 @@ export function PluginsScreen() {
241
228
  }
242
229
  return;
243
230
  }
244
- // When search query is empty: action shortcuts work normally
231
+ // Action shortcuts work when not actively typing (even with filter visible)
245
232
  // Start explicit search mode with /
246
233
  if (event.name === "/") {
247
234
  dispatch({ type: "SET_SEARCHING", isSearching: true });
@@ -268,14 +255,9 @@ export function PluginsScreen() {
268
255
  handleUninstall();
269
256
  else if (event.name === "s")
270
257
  handleSaveAsProfile();
271
- // Any other printable letter: start inline search (skip digits — used for tab nav)
272
- else if (event.name.length === 1 && !event.ctrl && !event.meta && !/[0-9]/.test(event.name)) {
258
+ // "/" to enter search mode
259
+ else if (event.name === "/") {
273
260
  dispatch({ type: "SET_SEARCHING", isSearching: true });
274
- dispatch({
275
- type: "PLUGINS_SET_SEARCH",
276
- query: event.name,
277
- });
278
- dispatch({ type: "PLUGINS_SELECT", index: 0 });
279
261
  }
280
262
  });
281
263
  // Handle actions
@@ -920,14 +902,14 @@ export function PluginsScreen() {
920
902
  // Show version only if valid (not null, not 0.0.0)
921
903
  const showVersion = plugin.version && plugin.version !== "0.0.0";
922
904
  const showInstalledVersion = plugin.installedVersion && plugin.installedVersion !== "0.0.0";
923
- return (_jsxs("box", { flexDirection: "column", children: [_jsx("box", { justifyContent: "center", children: _jsx("text", { bg: "magenta", fg: "white", children: _jsxs("strong", { children: [" ", plugin.name, plugin.hasUpdate ? " ⬆" : "", " "] }) }) }), _jsx("box", { marginTop: 1, children: isInstalled ? (_jsx("text", { fg: plugin.enabled ? "green" : "yellow", children: plugin.enabled ? " Enabled" : "● Disabled" })) : (_jsx("text", { fg: "gray", children: "\u25CB Not installed" })) }), _jsx("box", { marginTop: 1, marginBottom: 1, children: _jsx("text", { fg: "white", children: plugin.description }) }), showVersion && (_jsxs("text", { children: [_jsx("span", { children: "Version " }), _jsxs("span", { fg: "#5c9aff", children: ["v", plugin.version] }), showInstalledVersion &&
905
+ return (_jsxs("box", { flexDirection: "column", children: [_jsx("box", { justifyContent: "center", children: _jsx("text", { bg: "magenta", fg: "white", children: _jsxs("strong", { children: [" ", plugin.name, plugin.hasUpdate ? " ⬆" : "", " "] }) }) }), _jsx("box", { marginTop: 1, children: isInstalled ? (_jsx("text", { fg: "green", children: "\u25CF Installed" })) : (_jsx("text", { fg: "gray", children: "\u25CB Not installed" })) }), _jsx("box", { marginTop: 1, marginBottom: 1, children: _jsx("text", { fg: "white", children: plugin.description }) }), showVersion && (_jsxs("text", { children: [_jsx("span", { children: "Version " }), _jsxs("span", { fg: "#5c9aff", children: ["v", plugin.version] }), showInstalledVersion &&
924
906
  plugin.installedVersion !== plugin.version && (_jsxs("span", { children: [" (v", plugin.installedVersion, " installed)"] }))] })), plugin.category && (_jsxs("text", { children: [_jsx("span", { children: "Category " }), _jsx("span", { fg: "magenta", children: plugin.category })] })), plugin.author && (_jsxs("text", { children: [_jsx("span", { children: "Author " }), _jsx("span", { children: plugin.author.name })] })), components.length > 0 && (_jsxs("text", { children: [_jsx("span", { children: "Contains " }), _jsx("span", { fg: "yellow", children: components.join(" · ") })] })), _jsxs("box", { flexDirection: "column", marginTop: 1, children: [_jsx("text", { children: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" }), _jsx("text", { children: _jsx("strong", { children: "Scopes:" }) }), _jsxs("box", { marginTop: 1, flexDirection: "column", children: [_jsxs("text", { children: [_jsxs("span", { bg: "cyan", fg: "black", children: [" ", "u", " "] }), _jsx("span", { fg: plugin.userScope?.enabled ? "cyan" : "gray", children: plugin.userScope?.enabled ? " ● " : " ○ " }), _jsx("span", { fg: "cyan", children: "User" }), _jsx("span", { children: " global" }), plugin.userScope?.version && (_jsxs("span", { fg: "cyan", children: [" v", plugin.userScope.version] }))] }), _jsxs("text", { children: [_jsxs("span", { bg: "green", fg: "black", children: [" ", "p", " "] }), _jsx("span", { fg: plugin.projectScope?.enabled ? "green" : "gray", children: plugin.projectScope?.enabled ? " ● " : " ○ " }), _jsx("span", { fg: "green", children: "Project" }), _jsx("span", { children: " team" }), plugin.projectScope?.version && (_jsxs("span", { fg: "green", children: [" v", plugin.projectScope.version] }))] }), _jsxs("text", { children: [_jsxs("span", { bg: "yellow", fg: "black", children: [" ", "l", " "] }), _jsx("span", { fg: plugin.localScope?.enabled ? "yellow" : "gray", children: plugin.localScope?.enabled ? " ● " : " ○ " }), _jsx("span", { fg: "yellow", children: "Local" }), _jsx("span", { children: " private" }), plugin.localScope?.version && (_jsxs("span", { fg: "yellow", children: [" v", plugin.localScope.version] }))] })] })] }), isInstalled && (_jsxs("box", { flexDirection: "column", marginTop: 1, children: [plugin.hasUpdate && (_jsxs("box", { children: [_jsxs("text", { bg: "magenta", fg: "white", children: [" ", "U", " "] }), _jsxs("text", { children: [" Update to v", plugin.version] })] })), _jsxs("box", { children: [_jsxs("text", { bg: "red", fg: "white", children: [" ", "d", " "] }), _jsx("text", { children: " Uninstall" })] })] }))] }));
925
907
  }
926
908
  return null;
927
909
  };
928
- const footerHints = isSearchActive || pluginsState.searchQuery
929
- ? "↑↓:nav │ Enter:select │ Esc:clear │ type to filter"
930
- : "u/p/l:scope │ U:update │ a:all │ d:remove │ s:profile │ type to search";
910
+ const footerHints = isSearchActive
911
+ ? "type to filter │ Enter:done │ Esc:clear"
912
+ : "u/p/l:scope │ U:update │ a:all │ d:remove │ s:profile │ /:search";
931
913
  // Calculate status for subtitle
932
914
  const scopeLabel = pluginsState.scope === "global" ? "Global" : "Project";
933
915
  const plugins = pluginsState.plugins.status === "success" ? pluginsState.plugins.data : [];
@@ -940,6 +922,6 @@ export function PluginsScreen() {
940
922
  isActive: isSearchActive,
941
923
  query: pluginsState.searchQuery,
942
924
  placeholder: searchPlaceholder,
943
- }, footerHints: footerHints, listPanel: _jsx(ScrollableList, { items: selectableItems, selectedIndex: pluginsState.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight }), detailPanel: renderDetail() }));
925
+ }, footerHints: footerHints, listPanel: _jsxs("box", { flexDirection: "column", children: [_jsx(ScrollableList, { items: selectableItems, selectedIndex: pluginsState.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight }), pluginsState.searchQuery && selectableItems.length === 0 && (_jsx(EmptyFilterState, { query: pluginsState.searchQuery, entityName: "plugins" }))] }), detailPanel: renderDetail() }));
944
926
  }
945
927
  export default PluginsScreen;
@@ -5,6 +5,7 @@ import { useKeyboard } from "../hooks/useKeyboard.js";
5
5
  import { ScreenLayout } from "../components/layout/index.js";
6
6
  import { CategoryHeader } from "../components/CategoryHeader.js";
7
7
  import { ScrollableList } from "../components/ScrollableList.js";
8
+ import { EmptyFilterState } from "../components/EmptyFilterState.js";
8
9
  import { fuzzyFilter, highlightMatches } from "../../utils/fuzzy-search.js";
9
10
  import { getAllMarketplaces } from "../../data/marketplaces.js";
10
11
  import {
@@ -240,31 +241,15 @@ export function PluginsScreen() {
240
241
  return;
241
242
  }
242
243
 
243
- // Navigation — always works (even during search)
244
+ // Navigation — always works; exits search mode on navigate
244
245
  if (event.name === "up" || event.name === "k") {
245
- // 'k' navigates when query is empty, otherwise appends to search
246
- if (event.name === "k" && (hasQuery || isSearchActive)) {
247
- dispatch({
248
- type: "PLUGINS_SET_SEARCH",
249
- query: pluginsState.searchQuery + event.name,
250
- });
251
- dispatch({ type: "PLUGINS_SELECT", index: 0 });
252
- return;
253
- }
246
+ if (isSearchActive) dispatch({ type: "SET_SEARCHING", isSearching: false });
254
247
  const newIndex = Math.max(0, pluginsState.selectedIndex - 1);
255
248
  dispatch({ type: "PLUGINS_SELECT", index: newIndex });
256
249
  return;
257
250
  }
258
251
  if (event.name === "down" || event.name === "j") {
259
- // 'j' navigates when query is empty, otherwise appends to search
260
- if (event.name === "j" && (hasQuery || isSearchActive)) {
261
- dispatch({
262
- type: "PLUGINS_SET_SEARCH",
263
- query: pluginsState.searchQuery + event.name,
264
- });
265
- dispatch({ type: "PLUGINS_SELECT", index: 0 });
266
- return;
267
- }
252
+ if (isSearchActive) dispatch({ type: "SET_SEARCHING", isSearching: false });
268
253
  const newIndex = Math.min(
269
254
  selectableItems.length - 1,
270
255
  pluginsState.selectedIndex + 1,
@@ -302,9 +287,9 @@ export function PluginsScreen() {
302
287
  return;
303
288
  }
304
289
 
305
- // When search query is non-empty, printable letters go to the query
306
- // (shortcuts are suspended while filtering, digits skip to let tab nav work)
307
- if (hasQuery || isSearchActive) {
290
+ // When actively typing in search, letters go to the query
291
+ // After Enter (isSearchActive=false, hasQuery=true), shortcuts resume
292
+ if (isSearchActive) {
308
293
  if (event.name.length === 1 && !event.ctrl && !event.meta && !/[0-9]/.test(event.name)) {
309
294
  dispatch({
310
295
  type: "PLUGINS_SET_SEARCH",
@@ -315,7 +300,7 @@ export function PluginsScreen() {
315
300
  return;
316
301
  }
317
302
 
318
- // When search query is empty: action shortcuts work normally
303
+ // Action shortcuts work when not actively typing (even with filter visible)
319
304
 
320
305
  // Start explicit search mode with /
321
306
  if (event.name === "/") {
@@ -334,14 +319,9 @@ export function PluginsScreen() {
334
319
  else if (event.name === "a") handleUpdateAll();
335
320
  else if (event.name === "d") handleUninstall();
336
321
  else if (event.name === "s") handleSaveAsProfile();
337
- // Any other printable letter: start inline search (skip digits — used for tab nav)
338
- else if (event.name.length === 1 && !event.ctrl && !event.meta && !/[0-9]/.test(event.name)) {
322
+ // "/" to enter search mode
323
+ else if (event.name === "/") {
339
324
  dispatch({ type: "SET_SEARCHING", isSearching: true });
340
- dispatch({
341
- type: "PLUGINS_SET_SEARCH",
342
- query: event.name,
343
- });
344
- dispatch({ type: "PLUGINS_SELECT", index: 0 });
345
325
  }
346
326
  });
347
327
 
@@ -1260,9 +1240,7 @@ export function PluginsScreen() {
1260
1240
  {/* Status line */}
1261
1241
  <box marginTop={1}>
1262
1242
  {isInstalled ? (
1263
- <text fg={plugin.enabled ? "green" : "yellow"}>
1264
- {plugin.enabled ? "● Enabled" : "● Disabled"}
1265
- </text>
1243
+ <text fg="green">● Installed</text>
1266
1244
  ) : (
1267
1245
  <text fg="gray">○ Not installed</text>
1268
1246
  )}
@@ -1383,10 +1361,9 @@ export function PluginsScreen() {
1383
1361
  return null;
1384
1362
  };
1385
1363
 
1386
- const footerHints =
1387
- isSearchActive || pluginsState.searchQuery
1388
- ? "↑↓:navEnter:selectEsc:cleartype to filter"
1389
- : "u/p/l:scope │ U:update │ a:all │ d:remove │ s:profile │ type to search";
1364
+ const footerHints = isSearchActive
1365
+ ? "type to filter │ Enter:done │ Esc:clear"
1366
+ : "u/p/l:scopeU:updatea:alld:remove s:profile │ /:search";
1390
1367
 
1391
1368
  // Calculate status for subtitle
1392
1369
  const scopeLabel = pluginsState.scope === "global" ? "Global" : "Project";
@@ -1411,12 +1388,17 @@ export function PluginsScreen() {
1411
1388
  }}
1412
1389
  footerHints={footerHints}
1413
1390
  listPanel={
1414
- <ScrollableList
1415
- items={selectableItems}
1416
- selectedIndex={pluginsState.selectedIndex}
1417
- renderItem={renderListItem}
1418
- maxHeight={dimensions.listPanelHeight}
1419
- />
1391
+ <box flexDirection="column">
1392
+ <ScrollableList
1393
+ items={selectableItems}
1394
+ selectedIndex={pluginsState.selectedIndex}
1395
+ renderItem={renderListItem}
1396
+ maxHeight={dimensions.listPanelHeight}
1397
+ />
1398
+ {pluginsState.searchQuery && selectableItems.length === 0 && (
1399
+ <EmptyFilterState query={pluginsState.searchQuery} entityName="plugins" />
1400
+ )}
1401
+ </box>
1420
1402
  }
1421
1403
  detailPanel={renderDetail()}
1422
1404
  />