@peers-app/peers-ui 0.7.39 → 0.8.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.
@@ -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>
@@ -98,127 +98,6 @@ const coreCommands: Command[] = [
98
98
  closeCommandPalette();
99
99
  goToTabPath('threads');
100
100
  }
101
- },
102
- {
103
- id: 'go-settings',
104
- label: 'Open Settings',
105
- description: 'Open application settings',
106
- iconClassName: 'bi-gear-fill',
107
- category: 'Navigation',
108
- action: () => {
109
- closeCommandPalette();
110
- goToTabPath('settings');
111
- }
112
- },
113
- {
114
- id: 'go-assistants',
115
- label: 'Go to Assistants',
116
- description: 'View and manage assistants',
117
- iconClassName: 'bi-person-fill-gear',
118
- category: 'Navigation',
119
- action: () => {
120
- closeCommandPalette();
121
- goToTabPath('assistants');
122
- }
123
- },
124
- {
125
- id: 'go-workflows',
126
- label: 'Go to Workflows',
127
- description: 'View and manage workflows',
128
- iconClassName: 'bi-database-fill-gear',
129
- category: 'Navigation',
130
- action: () => {
131
- closeCommandPalette();
132
- goToTabPath('workflows');
133
- }
134
- },
135
- {
136
- id: 'go-tools',
137
- label: 'Go to Tools',
138
- description: 'View and manage tools',
139
- iconClassName: 'bi-tools',
140
- category: 'Navigation',
141
- action: () => {
142
- closeCommandPalette();
143
- goToTabPath('tools');
144
- }
145
- },
146
- {
147
- id: 'go-events',
148
- label: 'Go to Events',
149
- description: 'View and manage events',
150
- iconClassName: 'bi-lightning-charge-fill',
151
- category: 'Navigation',
152
- action: () => {
153
- closeCommandPalette();
154
- goToTabPath('events');
155
- }
156
- },
157
- {
158
- id: 'go-predicates',
159
- label: 'Go to Predicates',
160
- description: 'View and manage predicates',
161
- iconClassName: 'bi-node-plus-fill',
162
- category: 'Navigation',
163
- action: () => {
164
- closeCommandPalette();
165
- goToTabPath('predicates');
166
- }
167
- },
168
- {
169
- id: 'go-peer-types',
170
- label: 'Go to Peer Types',
171
- description: 'View and manage peer types',
172
- iconClassName: 'bi-code-square',
173
- category: 'Navigation',
174
- action: () => {
175
- closeCommandPalette();
176
- goToTabPath('peer-types');
177
- }
178
- },
179
- {
180
- id: 'go-packages',
181
- label: 'Go to Packages',
182
- description: 'View and manage packages',
183
- iconClassName: 'bi-box-fill',
184
- category: 'Navigation',
185
- action: () => {
186
- closeCommandPalette();
187
- goToTabPath('packages');
188
- }
189
- },
190
- {
191
- id: 'go-variables',
192
- label: 'Go to Variables',
193
- description: 'View and manage variables',
194
- iconClassName: 'bi-braces',
195
- category: 'Navigation',
196
- action: () => {
197
- closeCommandPalette();
198
- goToTabPath('variables');
199
- }
200
- },
201
- {
202
- id: 'go-knowledge-values',
203
- label: 'Go to Knowledge Values',
204
- description: 'View and manage knowledge values',
205
- iconClassName: 'bi-journal-bookmark-fill',
206
- category: 'Navigation',
207
- action: () => {
208
- closeCommandPalette();
209
- goToTabPath('knowledge-values');
210
- }
211
- },
212
- {
213
- id: 'go-knowledge-frames',
214
- label: 'Go to Knowledge Frames',
215
- description: 'View and manage knowledge frames',
216
- iconClassName: 'bi-window-dock',
217
- category: 'Navigation',
218
- action: () => {
219
- closeCommandPalette();
220
- goToTabPath('knowledge-frames');
221
- }
222
101
  }
223
102
  ];
224
103
 
@@ -143,7 +143,7 @@ export const valueTypeMentionConfig: IMentionConfig = {
143
143
  name: item.name
144
144
  }),
145
145
  onClick(data) {
146
- rpcClientCalls.setClientPath(`value-types/${data.id}`);
146
+ rpcClientCalls.setClientPath(`peer-types/${data.id}`);
147
147
  },
148
148
  };
149
149
 
@@ -1,4 +1,4 @@
1
- import { ConsoleLogs, DataFilter, IConsoleLog } from "@peers-app/peers-sdk";
1
+ import { ConsoleLogs, DataFilter, IConsoleLog, ISubscriptionResult, newid, sleep } from "@peers-app/peers-sdk";
2
2
  import { min, sortBy, uniqBy } from 'lodash';
3
3
  import React, { Fragment, useEffect, useMemo, useState } from 'react';
4
4
  import InfiniteScroll from 'react-infinite-scroll-component';
@@ -29,14 +29,15 @@ const DEFAULT_COLUMNS: Column[] = [
29
29
  export const ConsoleLogsList = () => {
30
30
  const logs = useObservableState<IConsoleLog[]>([]);
31
31
  const [allLogsLoaded, setAllLogsLoaded] = useState(false);
32
+ const loadMoreId = useObservableState<string>(newid(), true);
32
33
  const [levelFilter, setLevelFilter] = useState<string>('all');
33
34
  const [processFilter, setProcessFilter] = useState<string>('all');
34
- const [searchText, setSearchText] = useState('');
35
+ const searchText = useObservableState<string>('');
35
36
  const [columns, setColumns] = useState<Column[]>(DEFAULT_COLUMNS);
36
37
  const [totalLogCount, setTotalLogCount] = useState<number>(0);
37
38
  const [_colorMode] = useObservable(colorMode);
38
39
  const logsEndRef = React.useRef<HTMLDivElement>(null);
39
- const containerRef = React.useRef<HTMLDivElement>(null);
40
+ const containerRef = React.useRef<HTMLDivElement>(null);
40
41
 
41
42
  const batchSize = 50;
42
43
 
@@ -82,6 +83,9 @@ export const ConsoleLogsList = () => {
82
83
  if (processFilter !== 'all') {
83
84
  filter.process = processFilter;
84
85
  }
86
+ if (searchText()) {
87
+ filter.message = { $matchWords: searchText() }
88
+ }
85
89
  return filter;
86
90
  };
87
91
 
@@ -90,15 +94,6 @@ export const ConsoleLogsList = () => {
90
94
  const table = await ConsoleLogs();
91
95
  const filter = buildFilter();
92
96
  let count = await table.count(filter);
93
-
94
- // If search text is applied, we need to count manually since it's client-side filtering
95
- if (searchText) {
96
- const allLogs = await table.list(filter);
97
- count = allLogs.filter(log =>
98
- log.message.toLowerCase().includes(searchText.toLowerCase())
99
- ).length;
100
- }
101
-
102
97
  setTotalLogCount(count);
103
98
  }
104
99
 
@@ -106,63 +101,57 @@ export const ConsoleLogsList = () => {
106
101
  async function fetchLogs(lastLog?: IConsoleLog): Promise<IConsoleLog[]> {
107
102
  const table = await ConsoleLogs();
108
103
  const filter: any = buildFilter();
109
-
110
104
  if (lastLog) {
111
105
  filter.logId = { $lt: lastLog.logId };
112
106
  }
113
-
114
- const cursor = table.cursor(filter, {
115
- sortBy: ['-timestamp', '-logId'],
116
- });
117
-
118
- const fetchedLogs: IConsoleLog[] = [];
119
- for await (const log of cursor) {
120
- // Apply text search filter (if search is implemented in cursor, this can be removed)
121
- if (searchText && !log.message.toLowerCase().includes(searchText.toLowerCase())) {
122
- continue;
123
- }
124
- fetchedLogs.push(log);
125
- if (fetchedLogs.length >= batchSize) {
126
- break;
127
- }
128
- }
129
- return fetchedLogs;
107
+ const results = await table.list(filter, { pageSize: batchSize, sortBy: ['-timestamp', '-logId'] });
108
+ return results;
130
109
  }
131
110
 
132
111
  // Load older logs (prepend to list)
133
- function prependLogs() {
112
+ async function loadMoreLogs(startLoadId?: string) {
113
+ if (startLoadId && startLoadId !== loadMoreId()) {
114
+ return;
115
+ }
116
+ startLoadId ??= loadMoreId();
134
117
  const oldestLog = logs()[0];
135
- fetchLogs(oldestLog).then(fetchedLogs => {
136
- if (fetchedLogs.length === 0) {
137
- setAllLogsLoaded(true);
138
- } else {
139
- let _logs = sortBy([...logs(), ...fetchedLogs], 'timestamp');
140
- _logs = uniqBy(_logs, l => l.logId);
141
- logs(_logs);
142
- }
143
- });
144
- return false;
118
+ const fetchedLogs = await fetchLogs(oldestLog);
119
+ if (loadMoreId() !== startLoadId) {
120
+ loadMoreLogs(loadMoreId());
121
+ return;
122
+ }
123
+ if (fetchedLogs.length === 0) {
124
+ setAllLogsLoaded(true);
125
+ }
126
+ let _logs = sortBy([...logs(), ...fetchedLogs], 'timestamp');
127
+ _logs = uniqBy(_logs, l => l.logId);
128
+ logs(_logs);
145
129
  }
146
130
 
147
131
  // Initial load and ensure screen is filled
148
132
  const minHeightOfLog = 30;
149
133
  useEffect(() => {
150
134
  if (!allLogsLoaded && (!logs.length || logs.length * minHeightOfLog < windowHeight())) {
151
- prependLogs();
135
+ loadMoreLogs();
152
136
  }
153
- }, [logs, levelFilter, processFilter, searchText]);
137
+ }, [logs, levelFilter, processFilter, searchText()]);
154
138
 
155
139
  // Reset when filters change
156
140
  useEffect(() => {
141
+ loadMoreId(newid());
157
142
  logs([]);
158
- setAllLogsLoaded(false);
159
143
  updateLogCount();
160
- }, [levelFilter, processFilter, searchText]);
144
+ if (allLogsLoaded) {
145
+ setAllLogsLoaded(false);
146
+ loadMoreLogs()
147
+ }
148
+ }, [levelFilter, processFilter, searchText()]);
161
149
 
162
150
  // Subscribe to new logs
163
151
  useEffect(() => {
152
+ let sub: ISubscriptionResult | undefined = undefined;
164
153
  ConsoleLogs().then(table => {
165
- const sub = table.dataChanged.subscribe(evt => {
154
+ sub = table.dataChanged.subscribe(evt => {
166
155
  // Update count whenever data changes
167
156
  updateLogCount();
168
157
 
@@ -171,7 +160,11 @@ export const ConsoleLogsList = () => {
171
160
  // Check if log matches current filters
172
161
  if (levelFilter !== 'all' && log.level !== levelFilter) return;
173
162
  if (processFilter !== 'all' && log.process !== processFilter) return;
174
- if (searchText && !log.message.toLowerCase().includes(searchText.toLowerCase())) return;
163
+ if (searchText()) {
164
+ const logMessage = log.message.toLowerCase();
165
+ const filterOut = searchText().toLowerCase().split(' ').some(word => !logMessage.includes(word));
166
+ if (filterOut) return;
167
+ }
175
168
 
176
169
  // Don't add we're only showing a limited batch and this is older
177
170
  if (logs().length > batchSize && min(logs().map(l => l.timestamp))! > log.timestamp) return;
@@ -183,12 +176,11 @@ export const ConsoleLogsList = () => {
183
176
  scrollToBottom('smooth');
184
177
  }
185
178
  });
186
-
187
- return () => {
188
- sub.unsubscribe();
189
- };
190
179
  });
191
- }, [levelFilter, processFilter, searchText]);
180
+ return () => {
181
+ sub?.unsubscribe();
182
+ };
183
+ }, [levelFilter, processFilter]);
192
184
 
193
185
  function scrollToBottom(behavior: 'instant' | 'smooth', delay = 100) {
194
186
  setTimeout(() => {
@@ -202,7 +194,6 @@ export const ConsoleLogsList = () => {
202
194
  const table = await ConsoleLogs();
203
195
  await table.deleteOldLogs(Date.now());
204
196
  logs([]);
205
- setAllLogsLoaded(true);
206
197
  setTotalLogCount(0);
207
198
  } catch (err) {
208
199
  console.error('Failed to clear logs:', err);
@@ -219,8 +210,8 @@ export const ConsoleLogsList = () => {
219
210
  setLevelFilter={setLevelFilter}
220
211
  processFilter={processFilter}
221
212
  setProcessFilter={setProcessFilter}
222
- searchText={searchText}
223
- setSearchText={setSearchText}
213
+ searchText={searchText()}
214
+ setSearchText={searchText}
224
215
  />
225
216
 
226
217
  <div
@@ -254,7 +245,7 @@ export const ConsoleLogsList = () => {
254
245
  >
255
246
  <InfiniteScroll
256
247
  dataLength={_logs.length}
257
- next={prependLogs}
248
+ next={loadMoreLogs}
258
249
  style={{ display: 'flex', flexDirection: 'column-reverse', overflow: 'hidden' }}
259
250
  inverse={true}
260
251
  hasMore={!allLogsLoaded}