claudeup 3.9.0 → 3.11.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.js CHANGED
@@ -92,24 +92,24 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
92
92
  if (input === "1")
93
93
  navigateToScreen("plugins");
94
94
  else if (input === "2")
95
- navigateToScreen("mcp");
95
+ navigateToScreen("skills");
96
96
  else if (input === "3")
97
- navigateToScreen("settings");
97
+ navigateToScreen("mcp");
98
98
  else if (input === "4")
99
- navigateToScreen("cli-tools");
99
+ navigateToScreen("settings");
100
100
  else if (input === "5")
101
101
  navigateToScreen("profiles");
102
102
  else if (input === "6")
103
- navigateToScreen("skills");
103
+ navigateToScreen("cli-tools");
104
104
  // Tab navigation cycling
105
105
  if (key.tab) {
106
106
  const screens = [
107
107
  "plugins",
108
+ "skills",
108
109
  "mcp",
109
110
  "settings",
110
- "cli-tools",
111
111
  "profiles",
112
- "skills",
112
+ "cli-tools",
113
113
  ];
114
114
  const currentIndex = screens.indexOf(state.currentRoute.screen);
115
115
  if (currentIndex !== -1) {
@@ -144,9 +144,9 @@ function GlobalKeyHandler({ onDebugToggle, onExit, }) {
144
144
  ? This help
145
145
 
146
146
  Quick Navigation
147
- 1 Plugins 4 CLI Tools
148
- 2 MCP Servers 5 Profiles
149
- 3 Settings 6 Skills
147
+ 1 Plugins 4 Settings
148
+ 2 Skills 5 Profiles
149
+ 3 MCP Servers 6 CLI Tools
150
150
 
151
151
  Plugin Actions
152
152
  u Update d Uninstall
package/src/ui/App.tsx CHANGED
@@ -124,21 +124,21 @@ function GlobalKeyHandler({
124
124
 
125
125
  if (isTopLevel) {
126
126
  if (input === "1") navigateToScreen("plugins");
127
- else if (input === "2") navigateToScreen("mcp");
128
- else if (input === "3") navigateToScreen("settings");
129
- 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");
130
130
  else if (input === "5") navigateToScreen("profiles");
131
- else if (input === "6") navigateToScreen("skills");
131
+ else if (input === "6") navigateToScreen("cli-tools");
132
132
 
133
133
  // Tab navigation cycling
134
134
  if (key.tab) {
135
135
  const screens: Screen[] = [
136
136
  "plugins",
137
+ "skills",
137
138
  "mcp",
138
139
  "settings",
139
- "cli-tools",
140
140
  "profiles",
141
- "skills",
141
+ "cli-tools",
142
142
  ];
143
143
  const currentIndex = screens.indexOf(
144
144
  state.currentRoute.screen as Screen,
@@ -177,9 +177,9 @@ function GlobalKeyHandler({
177
177
  ? This help
178
178
 
179
179
  Quick Navigation
180
- 1 Plugins 4 CLI Tools
181
- 2 MCP Servers 5 Profiles
182
- 3 Settings 6 Skills
180
+ 1 Plugins 4 Settings
181
+ 2 Skills 5 Profiles
182
+ 3 MCP Servers 6 CLI Tools
183
183
 
184
184
  Plugin Actions
185
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,11 +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: "Skills", screen: "skills" },
9
+ { key: "6", label: "CLI", screen: "cli-tools" },
10
10
  ];
11
11
  export function TabBar({ currentScreen, onTabChange }) {
12
12
  // Handle number key shortcuts (1-5)
@@ -10,11 +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: "Skills", screen: "skills" },
17
+ { key: "6", label: "CLI", screen: "cli-tools" },
18
18
  ];
19
19
 
20
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 });
@@ -264,18 +251,11 @@ export function PluginsScreen() {
264
251
  handleUpdate();
265
252
  else if (event.name === "a")
266
253
  handleUpdateAll();
267
- else if (event.name === "d")
268
- handleUninstall();
269
254
  else if (event.name === "s")
270
255
  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)) {
256
+ // "/" to enter search mode
257
+ else if (event.name === "/") {
273
258
  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
259
  }
280
260
  });
281
261
  // Handle actions
@@ -639,7 +619,7 @@ export function PluginsScreen() {
639
619
  const plugin = item.plugin;
640
620
  const latestVersion = plugin.version || "0.0.0";
641
621
  const scopeLabel = scope === "user" ? "User" : scope === "project" ? "Project" : "Local";
642
- // Check if installed in this scope
622
+ // Check if installed in this specific scope
643
623
  const scopeData = scope === "user"
644
624
  ? plugin.userScope
645
625
  : scope === "project"
@@ -647,12 +627,16 @@ export function PluginsScreen() {
647
627
  : plugin.localScope;
648
628
  const isInstalledInScope = scopeData?.enabled;
649
629
  const installedVersion = scopeData?.version;
630
+ // Also check if installed in ANY scope (for the toggle behavior)
631
+ const isInstalledAnywhere = plugin.userScope?.enabled || plugin.projectScope?.enabled || plugin.localScope?.enabled;
650
632
  // Check if this scope has an update available
651
633
  const hasUpdateInScope = isInstalledInScope &&
652
634
  installedVersion &&
653
635
  latestVersion !== "0.0.0" &&
654
636
  installedVersion !== latestVersion;
655
- // Determine action: update if available, otherwise toggle install/uninstall
637
+ // Determine action: if installed in this scope → uninstall
638
+ // If installed anywhere else but not this scope → uninstall from detected scope
639
+ // Otherwise → install
656
640
  let action;
657
641
  if (isInstalledInScope && hasUpdateInScope) {
658
642
  action = "update";
@@ -660,9 +644,13 @@ export function PluginsScreen() {
660
644
  else if (isInstalledInScope) {
661
645
  action = "uninstall";
662
646
  }
663
- else {
647
+ else if (!isInstalledAnywhere) {
664
648
  action = "install";
665
649
  }
650
+ else {
651
+ // Installed in a different scope — uninstall from the scope it's actually in
652
+ action = "uninstall";
653
+ }
666
654
  const actionLabel = action === "update"
667
655
  ? `Updating ${scopeLabel}`
668
656
  : action === "install"
@@ -819,18 +807,15 @@ export function PluginsScreen() {
819
807
  const plugin = item.plugin;
820
808
  let statusIcon = "○";
821
809
  let statusColor = "gray";
810
+ const isAnyScope = plugin.userScope?.enabled || plugin.projectScope?.enabled || plugin.localScope?.enabled;
822
811
  if (plugin.isOrphaned) {
823
812
  statusIcon = "x";
824
813
  statusColor = "red";
825
814
  }
826
- else if (plugin.enabled) {
815
+ else if (isAnyScope) {
827
816
  statusIcon = "●";
828
817
  statusColor = "green";
829
818
  }
830
- else if (plugin.installedVersion) {
831
- statusIcon = "●";
832
- statusColor = "yellow";
833
- }
834
819
  // Build version string
835
820
  let versionStr = "";
836
821
  if (plugin.isOrphaned) {
@@ -899,7 +884,7 @@ export function PluginsScreen() {
899
884
  }
900
885
  if (selectedItem.type === "plugin" && selectedItem.plugin) {
901
886
  const plugin = selectedItem.plugin;
902
- const isInstalled = plugin.enabled || plugin.installedVersion;
887
+ const isInstalled = plugin.userScope?.enabled || plugin.projectScope?.enabled || plugin.localScope?.enabled;
903
888
  // Orphaned/deprecated plugin
904
889
  if (plugin.isOrphaned) {
905
890
  return (_jsxs("box", { flexDirection: "column", children: [_jsx("box", { justifyContent: "center", children: _jsx("text", { bg: "yellow", fg: "black", children: _jsxs("strong", { children: [" ", plugin.name, " \u2014 DEPRECATED "] }) }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "yellow", children: "This plugin is no longer in the marketplace." }) }), _jsx("box", { marginTop: 1, children: _jsx("text", { fg: "gray", children: "It was removed from the marketplace but still referenced in your settings. Press d to uninstall and clean up." }) }), isInstalled && (_jsx("box", { flexDirection: "column", marginTop: 2, children: _jsxs("box", { children: [_jsx("text", { bg: "red", fg: "white", children: " d " }), _jsx("text", { children: " Uninstall (recommended)" })] }) }))] }));
@@ -920,14 +905,14 @@ export function PluginsScreen() {
920
905
  // Show version only if valid (not null, not 0.0.0)
921
906
  const showVersion = plugin.version && plugin.version !== "0.0.0";
922
907
  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 &&
924
- 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" })] })] }))] }));
908
+ 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 &&
909
+ 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 && (_jsx("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] })] })) }))] }));
925
910
  }
926
911
  return null;
927
912
  };
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";
913
+ const footerHints = isSearchActive
914
+ ? "type to filter │ Enter:done │ Esc:clear"
915
+ : "u/p/l:toggle │ U:update │ a:all │ s:profile │ /:search";
931
916
  // Calculate status for subtitle
932
917
  const scopeLabel = pluginsState.scope === "global" ? "Global" : "Project";
933
918
  const plugins = pluginsState.plugins.status === "success" ? pluginsState.plugins.data : [];
@@ -940,6 +925,6 @@ export function PluginsScreen() {
940
925
  isActive: isSearchActive,
941
926
  query: pluginsState.searchQuery,
942
927
  placeholder: searchPlaceholder,
943
- }, footerHints: footerHints, listPanel: _jsx(ScrollableList, { items: selectableItems, selectedIndex: pluginsState.selectedIndex, renderItem: renderListItem, maxHeight: dimensions.listPanelHeight }), detailPanel: renderDetail() }));
928
+ }, 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
929
  }
945
930
  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 === "/") {
@@ -332,16 +317,10 @@ export function PluginsScreen() {
332
317
  else if (event.name === "l") handleScopeToggle("local");
333
318
  else if (event.name === "U") handleUpdate();
334
319
  else if (event.name === "a") handleUpdateAll();
335
- else if (event.name === "d") handleUninstall();
336
320
  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)) {
321
+ // "/" to enter search mode
322
+ else if (event.name === "/") {
339
323
  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
324
  }
346
325
  });
347
326
 
@@ -812,7 +791,7 @@ export function PluginsScreen() {
812
791
  const scopeLabel =
813
792
  scope === "user" ? "User" : scope === "project" ? "Project" : "Local";
814
793
 
815
- // Check if installed in this scope
794
+ // Check if installed in this specific scope
816
795
  const scopeData =
817
796
  scope === "user"
818
797
  ? plugin.userScope
@@ -822,6 +801,9 @@ export function PluginsScreen() {
822
801
  const isInstalledInScope = scopeData?.enabled;
823
802
  const installedVersion = scopeData?.version;
824
803
 
804
+ // Also check if installed in ANY scope (for the toggle behavior)
805
+ const isInstalledAnywhere = plugin.userScope?.enabled || plugin.projectScope?.enabled || plugin.localScope?.enabled;
806
+
825
807
  // Check if this scope has an update available
826
808
  const hasUpdateInScope =
827
809
  isInstalledInScope &&
@@ -829,14 +811,19 @@ export function PluginsScreen() {
829
811
  latestVersion !== "0.0.0" &&
830
812
  installedVersion !== latestVersion;
831
813
 
832
- // Determine action: update if available, otherwise toggle install/uninstall
814
+ // Determine action: if installed in this scope → uninstall
815
+ // If installed anywhere else but not this scope → uninstall from detected scope
816
+ // Otherwise → install
833
817
  let action: "update" | "install" | "uninstall";
834
818
  if (isInstalledInScope && hasUpdateInScope) {
835
819
  action = "update";
836
820
  } else if (isInstalledInScope) {
837
821
  action = "uninstall";
838
- } else {
822
+ } else if (!isInstalledAnywhere) {
839
823
  action = "install";
824
+ } else {
825
+ // Installed in a different scope — uninstall from the scope it's actually in
826
+ action = "uninstall";
840
827
  }
841
828
 
842
829
  const actionLabel =
@@ -1066,15 +1053,13 @@ export function PluginsScreen() {
1066
1053
  let statusIcon = "○";
1067
1054
  let statusColor = "gray";
1068
1055
 
1056
+ const isAnyScope = plugin.userScope?.enabled || plugin.projectScope?.enabled || plugin.localScope?.enabled;
1069
1057
  if (plugin.isOrphaned) {
1070
1058
  statusIcon = "x";
1071
1059
  statusColor = "red";
1072
- } else if (plugin.enabled) {
1060
+ } else if (isAnyScope) {
1073
1061
  statusIcon = "●";
1074
1062
  statusColor = "green";
1075
- } else if (plugin.installedVersion) {
1076
- statusIcon = "●";
1077
- statusColor = "yellow";
1078
1063
  }
1079
1064
 
1080
1065
  // Build version string
@@ -1198,7 +1183,7 @@ export function PluginsScreen() {
1198
1183
 
1199
1184
  if (selectedItem.type === "plugin" && selectedItem.plugin) {
1200
1185
  const plugin = selectedItem.plugin;
1201
- const isInstalled = plugin.enabled || plugin.installedVersion;
1186
+ const isInstalled = plugin.userScope?.enabled || plugin.projectScope?.enabled || plugin.localScope?.enabled;
1202
1187
 
1203
1188
  // Orphaned/deprecated plugin
1204
1189
  if (plugin.isOrphaned) {
@@ -1260,9 +1245,7 @@ export function PluginsScreen() {
1260
1245
  {/* Status line */}
1261
1246
  <box marginTop={1}>
1262
1247
  {isInstalled ? (
1263
- <text fg={plugin.enabled ? "green" : "yellow"}>
1264
- {plugin.enabled ? "● Enabled" : "● Disabled"}
1265
- </text>
1248
+ <text fg="green">● Installed</text>
1266
1249
  ) : (
1267
1250
  <text fg="gray">○ Not installed</text>
1268
1251
  )}
@@ -1367,13 +1350,6 @@ export function PluginsScreen() {
1367
1350
  <text> Update to v{plugin.version}</text>
1368
1351
  </box>
1369
1352
  )}
1370
- <box>
1371
- <text bg="red" fg="white">
1372
- {" "}
1373
- d{" "}
1374
- </text>
1375
- <text> Uninstall</text>
1376
- </box>
1377
1353
  </box>
1378
1354
  )}
1379
1355
  </box>
@@ -1383,10 +1359,9 @@ export function PluginsScreen() {
1383
1359
  return null;
1384
1360
  };
1385
1361
 
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";
1362
+ const footerHints = isSearchActive
1363
+ ? "type to filter │ Enter:done │ Esc:clear"
1364
+ : "u/p/l:toggleU:updatea:alls:profile /:search";
1390
1365
 
1391
1366
  // Calculate status for subtitle
1392
1367
  const scopeLabel = pluginsState.scope === "global" ? "Global" : "Project";
@@ -1411,12 +1386,17 @@ export function PluginsScreen() {
1411
1386
  }}
1412
1387
  footerHints={footerHints}
1413
1388
  listPanel={
1414
- <ScrollableList
1415
- items={selectableItems}
1416
- selectedIndex={pluginsState.selectedIndex}
1417
- renderItem={renderListItem}
1418
- maxHeight={dimensions.listPanelHeight}
1419
- />
1389
+ <box flexDirection="column">
1390
+ <ScrollableList
1391
+ items={selectableItems}
1392
+ selectedIndex={pluginsState.selectedIndex}
1393
+ renderItem={renderListItem}
1394
+ maxHeight={dimensions.listPanelHeight}
1395
+ />
1396
+ {pluginsState.searchQuery && selectableItems.length === 0 && (
1397
+ <EmptyFilterState query={pluginsState.searchQuery} entityName="plugins" />
1398
+ )}
1399
+ </box>
1420
1400
  }
1421
1401
  detailPanel={renderDetail()}
1422
1402
  />