@peers-app/peers-ui 0.7.39 → 0.7.40

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.
@@ -39,10 +39,14 @@ const hooks_1 = require("../../hooks");
39
39
  const color_mode_dropdown_1 = require("../settings/color-mode-dropdown");
40
40
  const tabs_state_1 = require("../../tabs-layout/tabs-state");
41
41
  const mention_configs_1 = require("../../mention-configs");
42
+ const routes_loader_1 = require("../../ui-router/routes-loader");
43
+ const system_apps_1 = require("../../system-apps");
42
44
  function GlobalSearch() {
43
45
  const [_colorMode] = (0, hooks_1.useObservable)(color_mode_dropdown_1.colorMode);
46
+ const [packages] = (0, hooks_1.useObservable)(routes_loader_1.allPackages);
44
47
  const [searchQuery, setSearchQuery] = (0, react_1.useState)('');
45
48
  const [searchResults, setSearchResults] = (0, react_1.useState)([]);
49
+ const [appResults, setAppResults] = (0, react_1.useState)([]);
46
50
  const [isSearching, setIsSearching] = (0, react_1.useState)(false);
47
51
  const inputRef = (0, react_1.useRef)(null);
48
52
  const isDark = _colorMode === 'dark';
@@ -62,10 +66,39 @@ function GlobalSearch() {
62
66
  { config: mention_configs_1.valueTypeMentionConfig, category: 'Types', navigationPath: 'peer-types' },
63
67
  { config: mention_configs_1.userMentionConfig, category: 'Users', navigationPath: 'profile' },
64
68
  ];
69
+ // Get all apps (system and user)
70
+ const getAllApps = () => {
71
+ const allPackages_ = [...packages, system_apps_1.systemPackage];
72
+ return allPackages_
73
+ .filter(p => !p.disabled && p.appNavs && p.appNavs.length > 0)
74
+ .flatMap(pkg => pkg.appNavs.map(navItem => {
75
+ // Construct path - use direct path for system apps, package-nav for others
76
+ let path;
77
+ if (pkg.packageId === 'system-apps') {
78
+ path = navItem.navigationPath ?? navItem.name.replace(/\s/g, '-').toLowerCase();
79
+ }
80
+ else {
81
+ path = `package-nav/${pkg.packageId}/${(navItem.navigationPath ?? navItem.name).replace(/[^a-zA-Z0-9]/g, '-').toLowerCase()}`;
82
+ while (path.includes('//')) {
83
+ path = path.replace('//', '/');
84
+ }
85
+ }
86
+ return {
87
+ packageId: pkg.packageId,
88
+ packageName: pkg.name,
89
+ navItem,
90
+ path,
91
+ name: navItem.name,
92
+ displayName: navItem.displayName || navItem.name,
93
+ iconClassName: navItem.iconClassName || 'bi-box-seam'
94
+ };
95
+ }));
96
+ };
65
97
  // Debounced search effect
66
98
  (0, react_1.useEffect)(() => {
67
99
  if (!searchQuery.trim()) {
68
100
  setSearchResults([]);
101
+ setAppResults([]);
69
102
  return;
70
103
  }
71
104
  const timeoutId = setTimeout(async () => {
@@ -88,13 +121,20 @@ function GlobalSearch() {
88
121
  const searchResults = await Promise.all(searchPromises);
89
122
  const filteredResults = searchResults.filter(Boolean);
90
123
  setSearchResults(filteredResults);
124
+ // Search apps
125
+ const allApps = getAllApps();
126
+ const lowerQuery = searchQuery.toLowerCase();
127
+ const filteredApps = allApps.filter(app => app.name.toLowerCase().includes(lowerQuery) ||
128
+ app.displayName.toLowerCase().includes(lowerQuery) ||
129
+ app.packageName.toLowerCase().includes(lowerQuery));
130
+ setAppResults(filteredApps);
91
131
  }
92
132
  finally {
93
133
  setIsSearching(false);
94
134
  }
95
135
  }, 300); // 300ms debounce
96
136
  return () => clearTimeout(timeoutId);
97
- }, [searchQuery]);
137
+ }, [searchQuery, packages]);
98
138
  const handleItemClick = (result, item) => {
99
139
  // Try using the config's onClick first
100
140
  if (result.config.onClick) {
@@ -114,7 +154,10 @@ function GlobalSearch() {
114
154
  }
115
155
  }
116
156
  };
117
- const totalResults = searchResults.reduce((sum, result) => sum + result.items.length, 0);
157
+ const handleAppClick = (app) => {
158
+ (0, tabs_state_1.goToTabPath)(app.path);
159
+ };
160
+ const totalResults = searchResults.reduce((sum, result) => sum + result.items.length, 0) + appResults.length;
118
161
  return (react_1.default.createElement("div", { className: "container-fluid h-100 p-4", style: { maxHeight: '100vh', overflowY: 'auto' } },
119
162
  react_1.default.createElement("div", { className: "mb-4" },
120
163
  react_1.default.createElement("h1", { className: "h3 mb-3 d-flex align-items-center" },
@@ -129,7 +172,7 @@ function GlobalSearch() {
129
172
  zIndex: 1,
130
173
  fontSize: '18px'
131
174
  } }),
132
- react_1.default.createElement("input", { ref: inputRef, type: "text", className: `form-control form-control-lg ${isDark ? 'bg-dark text-light border-secondary' : ''}`, placeholder: "Search across tools, assistants, workflows, events, and more...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), style: {
175
+ react_1.default.createElement("input", { ref: inputRef, type: "text", className: `form-control form-control-lg ${isDark ? 'bg-dark text-light border-secondary' : ''}`, placeholder: "Search across apps, tools, assistants, workflows, events, and more...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), style: {
133
176
  paddingLeft: '50px',
134
177
  fontSize: '18px',
135
178
  borderRadius: '12px',
@@ -142,44 +185,75 @@ function GlobalSearch() {
142
185
  transform: 'translateY(-50%)'
143
186
  } },
144
187
  react_1.default.createElement("div", { className: "spinner-border spinner-border-sm text-muted" })))),
145
- searchQuery && (react_1.default.createElement("div", { className: "mt-3 text-muted small" }, isSearching ? ('Searching...') : totalResults > 0 ? (`Found ${totalResults} result${totalResults !== 1 ? 's' : ''} across ${searchResults.length} categor${searchResults.length !== 1 ? 'ies' : 'y'}`) : searchQuery.trim() ? ('No results found') : null))),
146
- searchQuery && !isSearching && (react_1.default.createElement("div", null, searchResults.length === 0 ? (react_1.default.createElement("div", { className: "text-center py-5" },
188
+ searchQuery && (react_1.default.createElement("div", { className: "mt-3 text-muted small" }, isSearching ? ('Searching...') : totalResults > 0 ? (`Found ${totalResults} result${totalResults !== 1 ? 's' : ''}`) : searchQuery.trim() ? ('No results found') : null))),
189
+ searchQuery && !isSearching && (react_1.default.createElement("div", null, searchResults.length === 0 && appResults.length === 0 ? (react_1.default.createElement("div", { className: "text-center py-5" },
147
190
  react_1.default.createElement("i", { className: "bi-search mb-3 d-block text-muted", style: { fontSize: '48px' } }),
148
191
  react_1.default.createElement("h4", { className: "text-muted" }, "No results found"),
149
- react_1.default.createElement("p", { className: "text-muted" }, "Try different keywords or check your spelling"))) : (react_1.default.createElement("div", null, searchResults.map((result) => (react_1.default.createElement("div", { key: result.category, className: "mb-5" },
150
- react_1.default.createElement("div", { className: "d-flex align-items-center mb-3" },
151
- react_1.default.createElement("i", { className: `${result.config.iconClass} me-3`, style: { fontSize: '20px', color: '#6c757d' } }),
152
- react_1.default.createElement("h4", { className: "mb-0 me-3" }, result.category),
153
- react_1.default.createElement("span", { className: "badge bg-secondary" }, result.items.length)),
154
- react_1.default.createElement("div", { className: "row g-3" }, result.items.map((item) => (react_1.default.createElement("div", { key: item.id, className: "col-12 col-md-6 col-lg-4" },
155
- react_1.default.createElement("div", { className: `card h-100 ${isDark ? 'bg-dark border-secondary' : 'bg-light'}`, style: {
156
- cursor: 'pointer',
157
- transition: 'all 0.15s ease',
158
- borderRadius: '8px'
159
- }, onClick: () => handleItemClick(result, item), onMouseEnter: (e) => {
160
- e.currentTarget.style.transform = 'translateY(-2px)';
161
- e.currentTarget.style.boxShadow = isDark
162
- ? '0 4px 12px rgba(0,0,0,0.3)'
163
- : '0 4px 12px rgba(0,0,0,0.1)';
164
- }, onMouseLeave: (e) => {
165
- e.currentTarget.style.transform = 'translateY(0)';
166
- e.currentTarget.style.boxShadow = 'none';
167
- } },
168
- react_1.default.createElement("div", { className: "card-body p-3" },
169
- react_1.default.createElement("div", { className: "d-flex align-items-start" },
170
- react_1.default.createElement("i", { className: `${result.config.iconClass} me-3 mt-1`, style: {
171
- fontSize: '16px',
172
- color: isDark ? '#0d6efd' : '#0d6efd',
173
- minWidth: '16px'
174
- } }),
175
- react_1.default.createElement("div", { className: "flex-grow-1" },
176
- react_1.default.createElement("h6", { className: "card-title mb-1 fw-medium" }, item.name),
177
- react_1.default.createElement("small", { className: "text-muted text-uppercase", style: { fontSize: '11px', letterSpacing: '0.5px' } }, result.category.slice(0, -1))),
178
- react_1.default.createElement("i", { className: "bi-arrow-right text-muted", style: { fontSize: '12px' } }))))))))))))))),
192
+ react_1.default.createElement("p", { className: "text-muted" }, "Try different keywords or check your spelling"))) : (react_1.default.createElement("div", null,
193
+ appResults.length > 0 && (react_1.default.createElement("div", { className: "mb-5" },
194
+ react_1.default.createElement("div", { className: "d-flex align-items-center mb-3" },
195
+ react_1.default.createElement("i", { className: "bi-grid-3x3-gap me-3", style: { fontSize: '20px', color: '#6c757d' } }),
196
+ react_1.default.createElement("h4", { className: "mb-0 me-3" }, "Apps"),
197
+ react_1.default.createElement("span", { className: "badge bg-secondary" }, appResults.length)),
198
+ react_1.default.createElement("div", { className: "row g-3" }, appResults.map((app) => (react_1.default.createElement("div", { key: `${app.packageId}-${app.path}`, className: "col-12 col-md-6 col-lg-4" },
199
+ react_1.default.createElement("div", { className: `card h-100 ${isDark ? 'bg-dark border-secondary' : 'bg-light'}`, style: {
200
+ cursor: 'pointer',
201
+ transition: 'all 0.15s ease',
202
+ borderRadius: '8px'
203
+ }, onClick: () => handleAppClick(app), onMouseEnter: (e) => {
204
+ e.currentTarget.style.transform = 'translateY(-2px)';
205
+ e.currentTarget.style.boxShadow = isDark
206
+ ? '0 4px 12px rgba(0,0,0,0.3)'
207
+ : '0 4px 12px rgba(0,0,0,0.1)';
208
+ }, onMouseLeave: (e) => {
209
+ e.currentTarget.style.transform = 'translateY(0)';
210
+ e.currentTarget.style.boxShadow = 'none';
211
+ } },
212
+ react_1.default.createElement("div", { className: "card-body p-3" },
213
+ react_1.default.createElement("div", { className: "d-flex align-items-start" },
214
+ react_1.default.createElement("i", { className: `${app.iconClassName} me-3 mt-1`, style: {
215
+ fontSize: '16px',
216
+ color: isDark ? '#0d6efd' : '#0d6efd',
217
+ minWidth: '16px'
218
+ } }),
219
+ react_1.default.createElement("div", { className: "flex-grow-1" },
220
+ react_1.default.createElement("h6", { className: "card-title mb-1 fw-medium" }, app.displayName),
221
+ react_1.default.createElement("small", { className: "text-muted text-uppercase", style: { fontSize: '11px', letterSpacing: '0.5px' } }, app.packageId === 'system-apps' ? 'System App' : app.packageName)),
222
+ react_1.default.createElement("i", { className: "bi-arrow-right text-muted", style: { fontSize: '12px' } })))))))))),
223
+ searchResults.map((result) => (react_1.default.createElement("div", { key: result.category, className: "mb-5" },
224
+ react_1.default.createElement("div", { className: "d-flex align-items-center mb-3" },
225
+ react_1.default.createElement("i", { className: `${result.config.iconClass} me-3`, style: { fontSize: '20px', color: '#6c757d' } }),
226
+ react_1.default.createElement("h4", { className: "mb-0 me-3" }, result.category),
227
+ react_1.default.createElement("span", { className: "badge bg-secondary" }, result.items.length)),
228
+ react_1.default.createElement("div", { className: "row g-3" }, result.items.map((item) => (react_1.default.createElement("div", { key: item.id, className: "col-12 col-md-6 col-lg-4" },
229
+ react_1.default.createElement("div", { className: `card h-100 ${isDark ? 'bg-dark border-secondary' : 'bg-light'}`, style: {
230
+ cursor: 'pointer',
231
+ transition: 'all 0.15s ease',
232
+ borderRadius: '8px'
233
+ }, onClick: () => handleItemClick(result, item), onMouseEnter: (e) => {
234
+ e.currentTarget.style.transform = 'translateY(-2px)';
235
+ e.currentTarget.style.boxShadow = isDark
236
+ ? '0 4px 12px rgba(0,0,0,0.3)'
237
+ : '0 4px 12px rgba(0,0,0,0.1)';
238
+ }, onMouseLeave: (e) => {
239
+ e.currentTarget.style.transform = 'translateY(0)';
240
+ e.currentTarget.style.boxShadow = 'none';
241
+ } },
242
+ react_1.default.createElement("div", { className: "card-body p-3" },
243
+ react_1.default.createElement("div", { className: "d-flex align-items-start" },
244
+ react_1.default.createElement("i", { className: `${result.config.iconClass} me-3 mt-1`, style: {
245
+ fontSize: '16px',
246
+ color: isDark ? '#0d6efd' : '#0d6efd',
247
+ minWidth: '16px'
248
+ } }),
249
+ react_1.default.createElement("div", { className: "flex-grow-1" },
250
+ react_1.default.createElement("h6", { className: "card-title mb-1 fw-medium" }, item.name),
251
+ react_1.default.createElement("small", { className: "text-muted text-uppercase", style: { fontSize: '11px', letterSpacing: '0.5px' } }, result.category.slice(0, -1))),
252
+ react_1.default.createElement("i", { className: "bi-arrow-right text-muted", style: { fontSize: '12px' } }))))))))))))))),
179
253
  !searchQuery && (react_1.default.createElement("div", { className: "text-center py-5" },
180
254
  react_1.default.createElement("i", { className: "bi-search mb-3 d-block text-muted", style: { fontSize: '64px' } }),
181
255
  react_1.default.createElement("h3", { className: "text-muted mb-3" }, "Search across everything"),
182
- react_1.default.createElement("p", { className: "text-muted mb-4", style: { maxWidth: '400px', margin: '0 auto' } }, "Find tools, assistants, workflows, events, predicates, types, and users all in one place."),
256
+ react_1.default.createElement("p", { className: "text-muted mb-4", style: { maxWidth: '400px', margin: '0 auto' } }, "Find apps, tools, assistants, workflows, events, predicates, types, and users all in one place."),
183
257
  react_1.default.createElement("div", { className: "d-flex flex-wrap justify-content-center gap-2" }, searchConfigs.map(({ config, category }) => (react_1.default.createElement("span", { key: category, className: `badge ${isDark ? 'bg-secondary' : 'bg-light text-dark'} px-3 py-2 d-flex align-items-center`, style: { fontSize: '12px' } },
184
258
  react_1.default.createElement("i", { className: `${config.iconClass} me-2` }),
185
259
  category))))))));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peers-app/peers-ui",
3
- "version": "0.7.39",
3
+ "version": "0.7.40",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/peers-app/peers-ui.git"
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "peerDependencies": {
29
29
  "bootstrap": "^5.3.3",
30
- "@peers-app/peers-sdk": "^0.7.39",
30
+ "@peers-app/peers-sdk": "^0.7.40",
31
31
  "react": "^18.0.0",
32
32
  "react-dom": "^18.0.0"
33
33
  },
@@ -57,7 +57,7 @@
57
57
  "jest": "^29.7.0",
58
58
  "jest-environment-jsdom": "^30.0.5",
59
59
  "path-browserify": "^1.0.1",
60
- "@peers-app/peers-sdk": "0.7.39",
60
+ "@peers-app/peers-sdk": "0.7.40",
61
61
  "react": "^18.0.0",
62
62
  "react-dom": "^18.0.0",
63
63
  "string-width": "^7.1.0",
@@ -9,11 +9,27 @@ import {
9
9
  isCommandPaletteOpen,
10
10
  searchCommands
11
11
  } from './command-palette';
12
+ import { allPackages } from '../ui-router/routes-loader';
13
+ import { systemPackage } from '../system-apps';
14
+ import { goToTabPath } from '../tabs-layout/tabs-state';
15
+ import { IAppNav, Messages, newid, getMe } from "@peers-app/peers-sdk";
16
+ import { openThreadInTab } from '../globals';
17
+
18
+ interface AppSearchItem {
19
+ packageId: string;
20
+ packageName: string;
21
+ navItem: IAppNav;
22
+ path: string;
23
+ name: string;
24
+ displayName: string;
25
+ iconClassName: string;
26
+ }
12
27
 
13
28
  export function CommandPaletteOverlay() {
14
29
  const [isOpen] = useObservable(isCommandPaletteOpen);
15
30
  const [_persistedQuery] = useObservable(commandSearchQuery);
16
31
  const [_colorMode] = useObservable(colorMode);
32
+ const [packages] = useObservable(allPackages);
17
33
  const [selectedIndex, setSelectedIndex] = useState(0);
18
34
  const inputRef = useRef<HTMLInputElement>(null);
19
35
 
@@ -53,7 +69,78 @@ export function CommandPaletteOverlay() {
53
69
  const searchQuery = localQuery;
54
70
  const filteredCommands = searchCommands(searchQuery);
55
71
 
72
+ // Get all apps (system and user)
73
+ const getAllApps = (): AppSearchItem[] => {
74
+ const allPackages_ = [...packages, systemPackage];
75
+ return allPackages_
76
+ .filter(p => !p.disabled && p.appNavs && p.appNavs.length > 0)
77
+ .flatMap(pkg =>
78
+ pkg.appNavs!.map(navItem => {
79
+ // Construct path - use direct path for system apps, package-nav for others
80
+ let path: string;
81
+ if (pkg.packageId === 'system-apps') {
82
+ path = navItem.navigationPath ?? navItem.name.replace(/\s/g, '-').toLowerCase();
83
+ } else {
84
+ path = `package-nav/${pkg.packageId}/${(navItem.navigationPath ?? navItem.name).replace(/[^a-zA-Z0-9]/g, '-').toLowerCase()}`;
85
+ while (path.includes('//')) {
86
+ path = path.replace('//', '/');
87
+ }
88
+ }
89
+
90
+ return {
91
+ packageId: pkg.packageId,
92
+ packageName: pkg.name,
93
+ navItem,
94
+ path,
95
+ name: navItem.name,
96
+ displayName: navItem.displayName || navItem.name,
97
+ iconClassName: navItem.iconClassName || 'bi-box-seam'
98
+ };
99
+ })
100
+ );
101
+ };
102
+
103
+ // Search apps
104
+ const searchApps = (query: string): AppSearchItem[] => {
105
+ if (!query.trim()) return [];
106
+ const allApps = getAllApps();
107
+ const lowerQuery = query.toLowerCase();
108
+ return allApps.filter(app =>
109
+ app.name.toLowerCase().includes(lowerQuery) ||
110
+ app.displayName.toLowerCase().includes(lowerQuery) ||
111
+ app.packageName.toLowerCase().includes(lowerQuery)
112
+ );
113
+ };
114
+
115
+ const filteredApps = searchApps(searchQuery);
116
+
117
+ // Function to create a new thread from search query
118
+ const createNewThreadFromQuery = async (query: string) => {
119
+ try {
120
+ const currentUser = await getMe();
121
+ const threadMessage = await Messages().insert({
122
+ messageId: newid(),
123
+ userId: currentUser.userId,
124
+ channelId: currentUser.userId,
125
+ message: query.trim(),
126
+ createdAt: new Date(),
127
+ });
128
+ await openThreadInTab(threadMessage);
129
+ } catch (error) {
130
+ console.error('Failed to create new thread:', error);
131
+ }
132
+ };
133
+
56
134
  // Create a flattened list that matches the visual rendering order
135
+ // Apps first, then commands
136
+ const allItems: Array<{ type: 'app' | 'command'; app?: AppSearchItem; command?: Command; id: string }> = [];
137
+
138
+ // Add apps first
139
+ filteredApps.forEach((app) => {
140
+ allItems.push({ type: 'app', app, id: `app-${app.packageId}-${app.path}` });
141
+ });
142
+
143
+ // Then add commands
57
144
  const visualOrderCommands = Object.entries(
58
145
  filteredCommands.reduce((acc, cmd) => {
59
146
  const category = cmd.category || 'Other';
@@ -63,6 +150,10 @@ export function CommandPaletteOverlay() {
63
150
  }, {} as Record<string, Command[]>)
64
151
  ).flatMap(([, commands]) => commands);
65
152
 
153
+ visualOrderCommands.forEach((cmd) => {
154
+ allItems.push({ type: 'command', command: cmd, id: `command-${cmd.id}` });
155
+ });
156
+
66
157
  // Focus input when opened
67
158
  useEffect(() => {
68
159
  if (isOpen && inputRef.current) {
@@ -82,21 +173,40 @@ export function CommandPaletteOverlay() {
82
173
  const handleKeyDown = (e: KeyboardEvent) => {
83
174
  if (e.key === 'ArrowDown') {
84
175
  e.preventDefault();
85
- setSelectedIndex(prev => Math.min(prev + 1, visualOrderCommands.length - 1));
176
+ // Allow selection to go one past the end if there's a search query (for new thread option)
177
+ const maxIndex = searchQuery.trim() ? allItems.length : allItems.length - 1;
178
+ setSelectedIndex(prev => Math.min(prev + 1, maxIndex));
86
179
  } else if (e.key === 'ArrowUp') {
87
180
  e.preventDefault();
88
181
  setSelectedIndex(prev => Math.max(prev - 1, 0));
89
182
  } else if (e.key === 'Enter') {
90
183
  e.preventDefault();
91
- if (visualOrderCommands[selectedIndex]) {
92
- executeCommand(visualOrderCommands[selectedIndex].id);
184
+ // Check if the new thread option is selected (selectedIndex >= allItems.length)
185
+ if (selectedIndex >= allItems.length && searchQuery.trim()) {
186
+ closeCommandPalette();
187
+ createNewThreadFromQuery(searchQuery);
188
+ } else {
189
+ const selectedItem = allItems[selectedIndex];
190
+ if (selectedItem) {
191
+ if (selectedItem.type === 'app' && selectedItem.app) {
192
+ closeCommandPalette();
193
+ goToTabPath(selectedItem.app.path);
194
+ } else if (selectedItem.type === 'command' && selectedItem.command) {
195
+ executeCommand(selectedItem.command.id);
196
+ }
197
+ } else if (searchQuery.trim()) {
198
+ // Fallback: Create a new thread with the search query as the message
199
+ // This happens when no item is selected (selectedIndex is out of bounds or -1)
200
+ closeCommandPalette();
201
+ createNewThreadFromQuery(searchQuery);
202
+ }
93
203
  }
94
204
  }
95
205
  };
96
206
 
97
207
  document.addEventListener('keydown', handleKeyDown);
98
208
  return () => document.removeEventListener('keydown', handleKeyDown);
99
- }, [isOpen, visualOrderCommands, selectedIndex]);
209
+ }, [isOpen, allItems, selectedIndex]);
100
210
 
101
211
  if (!isOpen) return null;
102
212
 
@@ -151,7 +261,7 @@ export function CommandPaletteOverlay() {
151
261
  ref={inputRef}
152
262
  type="text"
153
263
  className={`form-control ${isDark ? 'bg-dark text-light border-secondary' : ''}`}
154
- placeholder="Type a command or search..."
264
+ placeholder="Search apps, commands, or navigate..."
155
265
  value={searchQuery}
156
266
  onChange={(e) => {
157
267
  const newQuery = e.target.value;
@@ -178,22 +288,85 @@ export function CommandPaletteOverlay() {
178
288
  </div>
179
289
  </div>
180
290
 
181
- {/* Commands List */}
291
+ {/* Commands and Apps List */}
182
292
  <div
183
293
  style={{
184
294
  maxHeight: '400px',
185
295
  overflowY: 'auto'
186
296
  }}
187
297
  >
188
- {visualOrderCommands.length === 0 ? (
298
+ {allItems.length === 0 && !searchQuery.trim() ? (
189
299
  <div className="p-4 text-center text-muted">
190
300
  <i className="bi-search mb-2 d-block" style={{ fontSize: '24px' }} />
191
- No commands found
301
+ No results found
192
302
  </div>
193
303
  ) : (
194
304
  <div className="py-2">
195
- {/* Group commands by category */}
196
- {Object.entries(
305
+ {/* Show "No results" message if there's a query but no results yet */}
306
+ {allItems.length === 0 && searchQuery.trim() && (
307
+ <div className="px-3 py-2 text-muted small text-center">
308
+ No matching results
309
+ </div>
310
+ )}
311
+ {/* Apps Section */}
312
+ {filteredApps.length > 0 && (
313
+ <div>
314
+ <div
315
+ className="px-3 py-1 small text-muted fw-bold text-uppercase"
316
+ style={{ fontSize: '11px', letterSpacing: '0.5px' }}
317
+ >
318
+ Apps
319
+ </div>
320
+ {filteredApps.map((app) => {
321
+ const appId = `app-${app.packageId}-${app.path}`;
322
+ const globalIndex = allItems.findIndex(item => item.id === appId);
323
+ const isSelected = globalIndex === selectedIndex;
324
+
325
+ return (
326
+ <div
327
+ key={`${app.packageId}-${app.path}`}
328
+ className={`px-3 py-2 d-flex align-items-center justify-content-between ${
329
+ isSelected
330
+ ? (isDark ? 'bg-primary bg-opacity-25' : 'bg-primary bg-opacity-10')
331
+ : ''
332
+ }`}
333
+ style={{
334
+ cursor: 'pointer',
335
+ transition: 'background-color 0.1s ease'
336
+ }}
337
+ onClick={() => {
338
+ closeCommandPalette();
339
+ goToTabPath(app.path);
340
+ }}
341
+ onMouseEnter={() => setSelectedIndex(globalIndex)}
342
+ >
343
+ <div className="d-flex align-items-center">
344
+ <i
345
+ className={`${app.iconClassName} me-3`}
346
+ style={{
347
+ fontSize: '16px',
348
+ color: isSelected ? (isDark ? '#ffffff' : '#0d6efd') : '#6c757d',
349
+ minWidth: '16px'
350
+ }}
351
+ />
352
+ <div>
353
+ <div className="fw-medium">{app.displayName}</div>
354
+ <div
355
+ className="small text-muted"
356
+ style={{ fontSize: '12px' }}
357
+ >
358
+ {app.packageId === 'system-apps' ? 'System App' : app.packageName}
359
+ </div>
360
+ </div>
361
+ </div>
362
+ </div>
363
+ );
364
+ })}
365
+ </div>
366
+ )}
367
+
368
+ {/* Commands Section - Group commands by category */}
369
+ {filteredCommands.length > 0 && Object.entries(
197
370
  filteredCommands.reduce((acc, cmd) => {
198
371
  const category = cmd.category || 'Other';
199
372
  if (!acc[category]) acc[category] = [];
@@ -212,7 +385,8 @@ export function CommandPaletteOverlay() {
212
385
 
213
386
  {/* Commands in Category */}
214
387
  {commands.map((command) => {
215
- const globalIndex = visualOrderCommands.indexOf(command);
388
+ const commandId = `command-${command.id}`;
389
+ const globalIndex = allItems.findIndex(item => item.id === commandId);
216
390
  const isSelected = globalIndex === selectedIndex;
217
391
 
218
392
  return (
@@ -274,6 +448,64 @@ export function CommandPaletteOverlay() {
274
448
  })}
275
449
  </div>
276
450
  ))}
451
+
452
+ {/* New Thread Indicator - Show when there's a search query */}
453
+ {searchQuery.trim() && (
454
+ <div>
455
+ <div
456
+ className="px-3 py-1 small text-muted fw-bold text-uppercase"
457
+ style={{ fontSize: '11px', letterSpacing: '0.5px' }}
458
+ >
459
+ Actions
460
+ </div>
461
+ <div
462
+ className={`px-3 py-2 d-flex align-items-center ${
463
+ (allItems.length === 0 || selectedIndex >= allItems.length) && searchQuery.trim()
464
+ ? (isDark ? 'bg-primary bg-opacity-25' : 'bg-primary bg-opacity-10')
465
+ : ''
466
+ }`}
467
+ style={{
468
+ cursor: 'pointer',
469
+ transition: 'background-color 0.1s ease',
470
+ borderTop: allItems.length > 0 ? `1px solid ${isDark ? '#495057' : '#dee2e6'}` : 'none',
471
+ marginTop: allItems.length > 0 ? '8px' : '0',
472
+ paddingTop: allItems.length > 0 ? '12px' : '8px'
473
+ }}
474
+ onClick={() => {
475
+ closeCommandPalette();
476
+ createNewThreadFromQuery(searchQuery);
477
+ }}
478
+ onMouseEnter={() => {
479
+ // Set selected index beyond the list to indicate this item is selected
480
+ setSelectedIndex(allItems.length);
481
+ }}
482
+ >
483
+ <div className="d-flex align-items-center">
484
+ <i
485
+ className="bi-chat-dots me-3"
486
+ style={{
487
+ fontSize: '16px',
488
+ color: (allItems.length === 0 || selectedIndex >= allItems.length) && searchQuery.trim()
489
+ ? (isDark ? '#ffffff' : '#0d6efd')
490
+ : '#6c757d',
491
+ minWidth: '16px'
492
+ }}
493
+ />
494
+ <div>
495
+ <div className="fw-medium">
496
+ Start new thread: <span className="text-muted">{searchQuery.trim().slice(0, 50)}{searchQuery.trim().length > 50 ? '...' : ''}</span>
497
+ </div>
498
+ <div
499
+ className="small text-muted"
500
+ style={{ fontSize: '12px' }}
501
+ >
502
+ Press Enter to create
503
+ </div>
504
+ </div>
505
+ </div>
506
+ </div>
507
+ </div>
508
+ )}
277
509
  </div>
278
510
  )}
279
511
  </div>
@@ -298,7 +530,8 @@ export function CommandPaletteOverlay() {
298
530
  </span>
299
531
  </div>
300
532
  <div>
301
- {visualOrderCommands.length} command{visualOrderCommands.length !== 1 ? 's' : ''}
533
+ {allItems.length} result{allItems.length !== 1 ? 's' : ''}
534
+ {searchQuery.trim() && allItems.length === 0 && ' • Start thread'}
302
535
  </div>
303
536
  </div>
304
537
  </div>