@runloop/rl-cli 1.5.0 → 1.6.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.
@@ -13,6 +13,7 @@ import { NavigationTips } from "../../components/NavigationTips.js";
13
13
  import { createTextColumn, Table } from "../../components/Table.js";
14
14
  import { ActionsPopup } from "../../components/ActionsPopup.js";
15
15
  import { formatTimeAgo } from "../../components/ResourceListView.js";
16
+ import { SearchBar } from "../../components/SearchBar.js";
16
17
  import { output, outputError } from "../../utils/output.js";
17
18
  import { getBlueprintUrl } from "../../utils/url.js";
18
19
  import { colors } from "../../utils/theme.js";
@@ -21,6 +22,7 @@ import { DevboxCreatePage } from "../../components/DevboxCreatePage.js";
21
22
  import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
22
23
  import { useViewportHeight } from "../../hooks/useViewportHeight.js";
23
24
  import { useCursorPagination } from "../../hooks/useCursorPagination.js";
25
+ import { useListSearch } from "../../hooks/useListSearch.js";
24
26
  import { useNavigation } from "../../store/navigationStore.js";
25
27
  import { ConfirmationPrompt } from "../../components/ConfirmationPrompt.js";
26
28
  const DEFAULT_PAGE_SIZE = 10;
@@ -39,8 +41,13 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
39
41
  const [selectedIndex, setSelectedIndex] = React.useState(0);
40
42
  const [showPopup, setShowPopup] = React.useState(false);
41
43
  const { navigate } = useNavigation();
44
+ // Search state
45
+ const search = useListSearch({
46
+ onSearchSubmit: () => setSelectedIndex(0),
47
+ onSearchClear: () => setSelectedIndex(0),
48
+ });
42
49
  // Calculate overhead for viewport height
43
- const overhead = 13;
50
+ const overhead = 13 + search.getSearchOverhead();
44
51
  const { viewportHeight, terminalWidth } = useViewportHeight({
45
52
  overhead,
46
53
  minHeight: 5,
@@ -70,6 +77,9 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
70
77
  if (params.startingAt) {
71
78
  queryParams.starting_after = params.startingAt;
72
79
  }
80
+ if (search.submittedSearchQuery) {
81
+ queryParams.search = search.submittedSearchQuery;
82
+ }
73
83
  // Fetch ONE page only
74
84
  const page = (await client.blueprints.list(queryParams));
75
85
  // Extract data and create defensive copies
@@ -89,7 +99,7 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
89
99
  totalCount: page.total_count || pageBlueprints.length,
90
100
  };
91
101
  return result;
92
- }, []);
102
+ }, [search.submittedSearchQuery]);
93
103
  // Use the shared pagination hook
94
104
  const { items: blueprints, loading, navigating, error: listError, currentPage, hasMore, hasPrev, totalCount, nextPage, prevPage, } = useCursorPagination({
95
105
  fetchPage,
@@ -99,8 +109,9 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
99
109
  pollingEnabled: !showPopup &&
100
110
  !showCreateDevbox &&
101
111
  !executingOperation &&
102
- !showDeleteConfirm,
103
- deps: [PAGE_SIZE],
112
+ !showDeleteConfirm &&
113
+ !search.searchMode,
114
+ deps: [PAGE_SIZE, search.submittedSearchQuery],
104
115
  });
105
116
  // Memoize columns array
106
117
  const blueprintColumns = React.useMemo(() => [
@@ -272,6 +283,13 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
272
283
  : allOperations;
273
284
  // Handle input for all views
274
285
  useInput((input, key) => {
286
+ // Handle search mode input
287
+ if (search.searchMode) {
288
+ if (key.escape) {
289
+ search.cancelSearch();
290
+ }
291
+ return;
292
+ }
275
293
  // Handle operation input mode
276
294
  if (executingOperation && !operationResult && !operationError) {
277
295
  // Allow escape/q to cancel any operation, even during loading
@@ -431,7 +449,13 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
431
449
  };
432
450
  openBrowser();
433
451
  }
452
+ else if (input === "/") {
453
+ search.enterSearchMode();
454
+ }
434
455
  else if (key.escape) {
456
+ if (search.handleEscape()) {
457
+ return;
458
+ }
435
459
  if (onBack) {
436
460
  onBack();
437
461
  }
@@ -520,7 +544,7 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
520
544
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), _jsx(ErrorMessage, { message: "Failed to load blueprints", error: listError })] }));
521
545
  }
522
546
  // List view
523
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), !showPopup && (_jsx(Table, { data: blueprints, keyExtractor: (blueprint) => blueprint.id, selectedIndex: selectedIndex, title: `blueprints[${totalCount}]`, columns: blueprintColumns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No blueprints found. Try: rli blueprint create"] }) })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] })] })), showPopup && selectedBlueprintItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedBlueprintItem, operations: allOperations.map((op) => ({
547
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Blueprints", active: true }] }), _jsx(SearchBar, { searchMode: search.searchMode, searchQuery: search.searchQuery, submittedSearchQuery: search.submittedSearchQuery, resultCount: totalCount, onSearchChange: search.setSearchQuery, onSearchSubmit: search.submitSearch, placeholder: "Search blueprints..." }), !showPopup && (_jsx(Table, { data: blueprints, keyExtractor: (blueprint) => blueprint.id, selectedIndex: selectedIndex, title: `blueprints[${totalCount}]`, columns: blueprintColumns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No blueprints found. Try: rli blueprint create"] }) })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] }), search.submittedSearchQuery && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.warning, children: ["Filtered: \"", search.submittedSearchQuery, "\""] })] }))] })), showPopup && selectedBlueprintItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedBlueprintItem, operations: allOperations.map((op) => ({
524
548
  key: op.key,
525
549
  label: op.label,
526
550
  color: op.color,
@@ -543,6 +567,7 @@ const ListBlueprintsUI = ({ onBack, onExit, }) => {
543
567
  { key: "Enter", label: "Details" },
544
568
  { key: "a", label: "Actions" },
545
569
  { key: "o", label: "Browser" },
570
+ { key: "/", label: "Search" },
546
571
  { key: "Esc", label: "Back" },
547
572
  ] })] }));
548
573
  };
@@ -1,7 +1,6 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import React from "react";
3
3
  import { Box, Text, useInput, useApp } from "ink";
4
- import TextInput from "ink-text-input";
5
4
  import figures from "figures";
6
5
  import { getClient } from "../../utils/client.js";
7
6
  import { SpinnerComponent } from "../../components/Spinner.js";
@@ -11,6 +10,7 @@ import { Breadcrumb } from "../../components/Breadcrumb.js";
11
10
  import { NavigationTips } from "../../components/NavigationTips.js";
12
11
  import { Table, createTextColumn } from "../../components/Table.js";
13
12
  import { formatTimeAgo } from "../../components/ResourceListView.js";
13
+ import { SearchBar } from "../../components/SearchBar.js";
14
14
  import { output, outputError } from "../../utils/output.js";
15
15
  import { DevboxDetailPage } from "../../components/DevboxDetailPage.js";
16
16
  import { DevboxCreatePage } from "../../components/DevboxCreatePage.js";
@@ -20,6 +20,7 @@ import { getDevboxUrl } from "../../utils/url.js";
20
20
  import { useViewportHeight } from "../../hooks/useViewportHeight.js";
21
21
  import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
22
22
  import { useCursorPagination } from "../../hooks/useCursorPagination.js";
23
+ import { useListSearch } from "../../hooks/useListSearch.js";
23
24
  import { colors } from "../../utils/theme.js";
24
25
  import { useDevboxStore } from "../../store/devboxStore.js";
25
26
  const DEFAULT_PAGE_SIZE = 10;
@@ -31,9 +32,11 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
31
32
  const [showActions, setShowActions] = React.useState(false);
32
33
  const [showPopup, setShowPopup] = React.useState(false);
33
34
  const [selectedOperation, setSelectedOperation] = React.useState(0);
34
- const [searchMode, setSearchMode] = React.useState(false);
35
- const [searchQuery, setSearchQuery] = React.useState("");
36
- const [submittedSearchQuery, setSubmittedSearchQuery] = React.useState("");
35
+ // Search state using shared hook
36
+ const search = useListSearch({
37
+ onSearchSubmit: () => setSelectedIndex(0),
38
+ onSearchClear: () => setSelectedIndex(0),
39
+ });
37
40
  // Get devbox store setter to sync data for detail screen
38
41
  const setDevboxesInStore = useDevboxStore((state) => state.setDevboxes);
39
42
  // Calculate overhead for viewport height:
@@ -44,7 +47,7 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
44
47
  // - Help bar (marginTop + content): 2 lines
45
48
  // - Safety buffer for edge cases: 1 line
46
49
  // Total: 13 lines base + 2 if searching
47
- const overhead = 13 + (searchMode || submittedSearchQuery ? 2 : 0);
50
+ const overhead = 13 + search.getSearchOverhead();
48
51
  const { viewportHeight, terminalWidth } = useViewportHeight({
49
52
  overhead,
50
53
  minHeight: 5,
@@ -64,8 +67,8 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
64
67
  if (status) {
65
68
  queryParams.status = status;
66
69
  }
67
- if (submittedSearchQuery) {
68
- queryParams.search = submittedSearchQuery;
70
+ if (search.submittedSearchQuery) {
71
+ queryParams.search = search.submittedSearchQuery;
69
72
  }
70
73
  // Fetch ONE page only
71
74
  const page = (await client.devboxes.list(queryParams));
@@ -81,15 +84,19 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
81
84
  totalCount: page.total_count || pageDevboxes.length,
82
85
  };
83
86
  return result;
84
- }, [status, submittedSearchQuery]);
87
+ }, [status, search.submittedSearchQuery]);
85
88
  // Use the shared pagination hook
86
89
  const { items: devboxes, loading, navigating, error, currentPage, hasMore, hasPrev, totalCount, nextPage, prevPage, } = useCursorPagination({
87
90
  fetchPage,
88
91
  pageSize: PAGE_SIZE,
89
92
  getItemId: (devbox) => devbox.id,
90
93
  pollInterval: 2000,
91
- pollingEnabled: !showDetails && !showCreate && !showActions && !showPopup && !searchMode,
92
- deps: [status, submittedSearchQuery, PAGE_SIZE],
94
+ pollingEnabled: !showDetails &&
95
+ !showCreate &&
96
+ !showActions &&
97
+ !showPopup &&
98
+ !search.searchMode,
99
+ deps: [status, search.submittedSearchQuery, PAGE_SIZE],
93
100
  });
94
101
  // Sync devboxes to store for detail screen
95
102
  React.useEffect(() => {
@@ -315,10 +322,9 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
315
322
  useInput((input, key) => {
316
323
  const pageDevboxes = devboxes.length;
317
324
  // Skip input handling when in search mode - let TextInput handle it
318
- if (searchMode) {
325
+ if (search.searchMode) {
319
326
  if (key.escape) {
320
- setSearchMode(false);
321
- setSearchQuery("");
327
+ search.cancelSearch();
322
328
  }
323
329
  return;
324
330
  }
@@ -416,24 +422,20 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
416
422
  openBrowser();
417
423
  }
418
424
  else if (input === "/") {
419
- setSearchMode(true);
425
+ search.enterSearchMode();
420
426
  }
421
427
  else if (key.escape) {
422
- if (submittedSearchQuery) {
423
- setSubmittedSearchQuery("");
424
- setSearchQuery("");
425
- setSelectedIndex(0);
428
+ if (search.handleEscape()) {
429
+ return;
430
+ }
431
+ if (onBack) {
432
+ onBack();
433
+ }
434
+ else if (onExit) {
435
+ onExit();
426
436
  }
427
437
  else {
428
- if (onBack) {
429
- onBack();
430
- }
431
- else if (onExit) {
432
- onExit();
433
- }
434
- else {
435
- inkExit();
436
- }
438
+ inkExit();
437
439
  }
438
440
  }
439
441
  });
@@ -472,13 +474,7 @@ const ListDevboxesUI = ({ status, onBack, onExit, onNavigateToDetail, }) => {
472
474
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes", active: true }] }), _jsx(ErrorMessage, { message: "Failed to list devboxes", error: error })] }));
473
475
  }
474
476
  // Main list view
475
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes", active: true }] }), searchMode && (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, children: [figures.pointerSmall, " Search: "] }), _jsx(TextInput, { value: searchQuery, onChange: setSearchQuery, placeholder: "Type to search...", onSubmit: () => {
476
- setSearchMode(false);
477
- setSubmittedSearchQuery(searchQuery);
478
- setSelectedIndex(0);
479
- } }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to search, Esc to cancel]"] })] })), !searchMode && submittedSearchQuery && (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, children: [figures.info, " Searching for: "] }), _jsx(Text, { color: colors.warning, bold: true, children: submittedSearchQuery.length > 50
480
- ? submittedSearchQuery.substring(0, 50) + "..."
481
- : submittedSearchQuery }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "(", totalCount, " results) [/ to edit, Esc to clear]"] })] })), !showPopup && (_jsx(Table, { data: devboxes, keyExtractor: (devbox) => devbox.id, selectedIndex: selectedIndex, title: "devboxes", columns: tableColumns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No devboxes found. Press [c] to create one."] }) })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] }), submittedSearchQuery && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.warning, children: ["Filtered: \"", submittedSearchQuery, "\""] })] }))] })), showPopup && selectedDevbox && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedDevbox, operations: operations, selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })), _jsx(NavigationTips, { showArrows: true, tips: [
477
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Devboxes", active: true }] }), _jsx(SearchBar, { searchMode: search.searchMode, searchQuery: search.searchQuery, submittedSearchQuery: search.submittedSearchQuery, resultCount: totalCount, onSearchChange: search.setSearchQuery, onSearchSubmit: search.submitSearch, placeholder: "Search devboxes..." }), !showPopup && (_jsx(Table, { data: devboxes, keyExtractor: (devbox) => devbox.id, selectedIndex: selectedIndex, title: "devboxes", columns: tableColumns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No devboxes found. Press [c] to create one."] }) })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] }), search.submittedSearchQuery && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.warning, children: ["Filtered: \"", search.submittedSearchQuery, "\""] })] }))] })), showPopup && selectedDevbox && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedDevbox, operations: operations, selectedOperation: selectedOperation, onClose: () => setShowPopup(false) }) })), _jsx(NavigationTips, { showArrows: true, tips: [
482
478
  {
483
479
  icon: `${figures.arrowLeft}${figures.arrowRight}`,
484
480
  label: "Page",
@@ -12,11 +12,13 @@ import { NavigationTips } from "../../components/NavigationTips.js";
12
12
  import { Table, createTextColumn } from "../../components/Table.js";
13
13
  import { ActionsPopup } from "../../components/ActionsPopup.js";
14
14
  import { formatTimeAgo } from "../../components/ResourceListView.js";
15
+ import { SearchBar } from "../../components/SearchBar.js";
15
16
  import { output, outputError } from "../../utils/output.js";
16
17
  import { colors } from "../../utils/theme.js";
17
18
  import { useViewportHeight } from "../../hooks/useViewportHeight.js";
18
19
  import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
19
20
  import { useCursorPagination } from "../../hooks/useCursorPagination.js";
21
+ import { useListSearch } from "../../hooks/useListSearch.js";
20
22
  import { useNavigation } from "../../store/navigationStore.js";
21
23
  import { NetworkPolicyCreatePage } from "../../components/NetworkPolicyCreatePage.js";
22
24
  import { ConfirmationPrompt } from "../../components/ConfirmationPrompt.js";
@@ -60,8 +62,13 @@ const ListNetworkPoliciesUI = ({ onBack, onExit, }) => {
60
62
  const [showEditPolicy, setShowEditPolicy] = React.useState(false);
61
63
  const [editingPolicy, setEditingPolicy] = React.useState(null);
62
64
  const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
65
+ // Search state
66
+ const search = useListSearch({
67
+ onSearchSubmit: () => setSelectedIndex(0),
68
+ onSearchClear: () => setSelectedIndex(0),
69
+ });
63
70
  // Calculate overhead for viewport height
64
- const overhead = 13;
71
+ const overhead = 13 + search.getSearchOverhead();
65
72
  const { viewportHeight, terminalWidth } = useViewportHeight({
66
73
  overhead,
67
74
  minHeight: 5,
@@ -90,6 +97,9 @@ const ListNetworkPoliciesUI = ({ onBack, onExit, }) => {
90
97
  if (params.startingAt) {
91
98
  queryParams.starting_after = params.startingAt;
92
99
  }
100
+ if (search.submittedSearchQuery) {
101
+ queryParams.search = search.submittedSearchQuery;
102
+ }
93
103
  // Fetch ONE page only
94
104
  const page = (await client.networkPolicies.list(queryParams));
95
105
  // Extract data and create defensive copies
@@ -115,7 +125,7 @@ const ListNetworkPoliciesUI = ({ onBack, onExit, }) => {
115
125
  totalCount: page.total_count || pagePolicies.length,
116
126
  };
117
127
  return result;
118
- }, []);
128
+ }, [search.submittedSearchQuery]);
119
129
  // Use the shared pagination hook
120
130
  const { items: policies, loading, navigating, error, currentPage, hasMore, hasPrev, totalCount, nextPage, prevPage, refresh, } = useCursorPagination({
121
131
  fetchPage,
@@ -126,8 +136,9 @@ const ListNetworkPoliciesUI = ({ onBack, onExit, }) => {
126
136
  !executingOperation &&
127
137
  !showCreatePolicy &&
128
138
  !showEditPolicy &&
129
- !showDeleteConfirm,
130
- deps: [PAGE_SIZE],
139
+ !showDeleteConfirm &&
140
+ !search.searchMode,
141
+ deps: [PAGE_SIZE, search.submittedSearchQuery],
131
142
  });
132
143
  // Operations for a specific network policy (shown in popup)
133
144
  const operations = React.useMemo(() => [
@@ -232,6 +243,13 @@ const ListNetworkPoliciesUI = ({ onBack, onExit, }) => {
232
243
  }
233
244
  };
234
245
  useInput((input, key) => {
246
+ // Handle search mode input
247
+ if (search.searchMode) {
248
+ if (key.escape) {
249
+ search.cancelSearch();
250
+ }
251
+ return;
252
+ }
235
253
  // Handle operation result display
236
254
  if (operationResult || operationError) {
237
255
  if (input === "q" || key.escape || key.return) {
@@ -363,7 +381,13 @@ const ListNetworkPoliciesUI = ({ onBack, onExit, }) => {
363
381
  setEditingPolicy(selectedPolicyItem);
364
382
  setShowEditPolicy(true);
365
383
  }
384
+ else if (input === "/") {
385
+ search.enterSearchMode();
386
+ }
366
387
  else if (key.escape) {
388
+ if (search.handleEscape()) {
389
+ return;
390
+ }
367
391
  if (onBack) {
368
392
  onBack();
369
393
  }
@@ -443,7 +467,7 @@ const ListNetworkPoliciesUI = ({ onBack, onExit, }) => {
443
467
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Network Policies", active: true }] }), _jsx(ErrorMessage, { message: "Failed to list network policies", error: error })] }));
444
468
  }
445
469
  // Main list view
446
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Network Policies", active: true }] }), !showPopup && (_jsx(Table, { data: policies, keyExtractor: (policy) => policy.id, selectedIndex: selectedIndex, title: `network_policies[${totalCount}]`, columns: columns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No network policies found. Press [c] to create one."] }) })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] })] })), showPopup && selectedPolicyItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedPolicyItem, operations: operations.map((op) => ({
470
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Network Policies", active: true }] }), _jsx(SearchBar, { searchMode: search.searchMode, searchQuery: search.searchQuery, submittedSearchQuery: search.submittedSearchQuery, resultCount: totalCount, onSearchChange: search.setSearchQuery, onSearchSubmit: search.submitSearch, placeholder: "Search network policies..." }), !showPopup && (_jsx(Table, { data: policies, keyExtractor: (policy) => policy.id, selectedIndex: selectedIndex, title: `network_policies[${totalCount}]`, columns: columns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No network policies found. Press [c] to create one."] }) })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] }), search.submittedSearchQuery && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.warning, children: ["Filtered: \"", search.submittedSearchQuery, "\""] })] }))] })), showPopup && selectedPolicyItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedPolicyItem, operations: operations.map((op) => ({
447
471
  key: op.key,
448
472
  label: op.label,
449
473
  color: op.color,
@@ -467,6 +491,7 @@ const ListNetworkPoliciesUI = ({ onBack, onExit, }) => {
467
491
  { key: "c", label: "Create" },
468
492
  { key: "e", label: "Edit" },
469
493
  { key: "a", label: "Actions" },
494
+ { key: "/", label: "Search" },
470
495
  { key: "Esc", label: "Back" },
471
496
  ] })] }));
472
497
  };
@@ -14,11 +14,13 @@ import { NavigationTips } from "../../components/NavigationTips.js";
14
14
  import { Table, createTextColumn } from "../../components/Table.js";
15
15
  import { ActionsPopup } from "../../components/ActionsPopup.js";
16
16
  import { formatTimeAgo } from "../../components/ResourceListView.js";
17
+ import { SearchBar } from "../../components/SearchBar.js";
17
18
  import { output, outputError } from "../../utils/output.js";
18
19
  import { colors } from "../../utils/theme.js";
19
20
  import { useViewportHeight } from "../../hooks/useViewportHeight.js";
20
21
  import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
21
22
  import { useCursorPagination } from "../../hooks/useCursorPagination.js";
23
+ import { useListSearch } from "../../hooks/useListSearch.js";
22
24
  import { useNavigation } from "../../store/navigationStore.js";
23
25
  import { formatFileSize } from "../../services/objectService.js";
24
26
  import { ConfirmationPrompt } from "../../components/ConfirmationPrompt.js";
@@ -38,8 +40,13 @@ const ListObjectsUI = ({ onBack, onExit, }) => {
38
40
  const [showDownloadPrompt, setShowDownloadPrompt] = React.useState(false);
39
41
  const [downloadPath, setDownloadPath] = React.useState("");
40
42
  const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
43
+ // Search state
44
+ const search = useListSearch({
45
+ onSearchSubmit: () => setSelectedIndex(0),
46
+ onSearchClear: () => setSelectedIndex(0),
47
+ });
41
48
  // Calculate overhead for viewport height
42
- const overhead = 13;
49
+ const overhead = 13 + search.getSearchOverhead();
43
50
  const { viewportHeight, terminalWidth } = useViewportHeight({
44
51
  overhead,
45
52
  minHeight: 5,
@@ -95,6 +102,9 @@ const ListObjectsUI = ({ onBack, onExit, }) => {
95
102
  if (params.startingAt) {
96
103
  queryParams.starting_after = params.startingAt;
97
104
  }
105
+ if (search.submittedSearchQuery) {
106
+ queryParams.search = search.submittedSearchQuery;
107
+ }
98
108
  // Fetch ONE page only
99
109
  const result = await client.objects.list(queryParams);
100
110
  // Extract data and create defensive copies
@@ -120,7 +130,7 @@ const ListObjectsUI = ({ onBack, onExit, }) => {
120
130
  hasMore: pageResult.has_more || false,
121
131
  totalCount: pageResult.total_count || pageObjects.length,
122
132
  };
123
- }, []);
133
+ }, [search.submittedSearchQuery]);
124
134
  // Use the shared pagination hook
125
135
  const { items: objects, loading, navigating, error, currentPage, hasMore, hasPrev, totalCount, nextPage, prevPage, refresh, } = useCursorPagination({
126
136
  fetchPage,
@@ -130,8 +140,9 @@ const ListObjectsUI = ({ onBack, onExit, }) => {
130
140
  pollingEnabled: !showPopup &&
131
141
  !executingOperation &&
132
142
  !showDownloadPrompt &&
133
- !showDeleteConfirm,
134
- deps: [PAGE_SIZE],
143
+ !showDeleteConfirm &&
144
+ !search.searchMode,
145
+ deps: [PAGE_SIZE, search.submittedSearchQuery],
135
146
  });
136
147
  // Operations for objects
137
148
  const operations = React.useMemo(() => [
@@ -274,6 +285,13 @@ const ListObjectsUI = ({ onBack, onExit, }) => {
274
285
  }
275
286
  };
276
287
  useInput((input, key) => {
288
+ // Handle search mode input
289
+ if (search.searchMode) {
290
+ if (key.escape) {
291
+ search.cancelSearch();
292
+ }
293
+ return;
294
+ }
277
295
  // Handle operation result display
278
296
  if (operationResult || operationError) {
279
297
  if (input === "q" || key.escape || key.return) {
@@ -397,7 +415,13 @@ const ListObjectsUI = ({ onBack, onExit, }) => {
397
415
  setShowPopup(true);
398
416
  setSelectedOperation(0);
399
417
  }
418
+ else if (input === "/") {
419
+ search.enterSearchMode();
420
+ }
400
421
  else if (key.escape) {
422
+ if (search.handleEscape()) {
423
+ return;
424
+ }
401
425
  if (onBack) {
402
426
  onBack();
403
427
  }
@@ -470,7 +494,7 @@ const ListObjectsUI = ({ onBack, onExit, }) => {
470
494
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Storage Objects", active: true }] }), _jsx(ErrorMessage, { message: "Failed to list storage objects", error: error })] }));
471
495
  }
472
496
  // Main list view
473
- return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Storage Objects", active: true }] }), !showPopup && (_jsx(Table, { data: objects, keyExtractor: (obj) => obj.id, selectedIndex: selectedIndex, title: `storage_objects[${totalCount}]`, columns: columns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No storage objects found. Try: rli object upload", " ", "<file>"] }) })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] })] })), showPopup && selectedObjectItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedObjectItem, operations: operations.map((op) => ({
497
+ return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [{ label: "Storage Objects", active: true }] }), _jsx(SearchBar, { searchMode: search.searchMode, searchQuery: search.searchQuery, submittedSearchQuery: search.submittedSearchQuery, resultCount: totalCount, onSearchChange: search.setSearchQuery, onSearchSubmit: search.submitSearch, placeholder: "Search storage objects..." }), !showPopup && (_jsx(Table, { data: objects, keyExtractor: (obj) => obj.id, selectedIndex: selectedIndex, title: `storage_objects[${totalCount}]`, columns: columns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No storage objects found. Try: rli object upload", " ", "<file>"] }) })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] }), search.submittedSearchQuery && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.warning, children: ["Filtered: \"", search.submittedSearchQuery, "\""] })] }))] })), showPopup && selectedObjectItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedObjectItem, operations: operations.map((op) => ({
474
498
  key: op.key,
475
499
  label: op.label,
476
500
  color: op.color,
@@ -490,6 +514,7 @@ const ListObjectsUI = ({ onBack, onExit, }) => {
490
514
  },
491
515
  { key: "Enter", label: "Details" },
492
516
  { key: "a", label: "Actions" },
517
+ { key: "/", label: "Search" },
493
518
  { key: "Esc", label: "Back" },
494
519
  ] })] }));
495
520
  };
@@ -12,11 +12,13 @@ import { NavigationTips } from "../../components/NavigationTips.js";
12
12
  import { Table, createTextColumn } from "../../components/Table.js";
13
13
  import { ActionsPopup } from "../../components/ActionsPopup.js";
14
14
  import { formatTimeAgo } from "../../components/ResourceListView.js";
15
+ import { SearchBar } from "../../components/SearchBar.js";
15
16
  import { output, outputError } from "../../utils/output.js";
16
17
  import { colors } from "../../utils/theme.js";
17
18
  import { useViewportHeight } from "../../hooks/useViewportHeight.js";
18
19
  import { useExitOnCtrlC } from "../../hooks/useExitOnCtrlC.js";
19
20
  import { useCursorPagination } from "../../hooks/useCursorPagination.js";
21
+ import { useListSearch } from "../../hooks/useListSearch.js";
20
22
  import { DevboxCreatePage } from "../../components/DevboxCreatePage.js";
21
23
  import { useNavigation } from "../../store/navigationStore.js";
22
24
  import { ConfirmationPrompt } from "../../components/ConfirmationPrompt.js";
@@ -35,8 +37,13 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
35
37
  const [operationLoading, setOperationLoading] = React.useState(false);
36
38
  const [showCreateDevbox, setShowCreateDevbox] = React.useState(false);
37
39
  const [showDeleteConfirm, setShowDeleteConfirm] = React.useState(false);
40
+ // Search state
41
+ const search = useListSearch({
42
+ onSearchSubmit: () => setSelectedIndex(0),
43
+ onSearchClear: () => setSelectedIndex(0),
44
+ });
38
45
  // Calculate overhead for viewport height
39
- const overhead = 13;
46
+ const overhead = 13 + search.getSearchOverhead();
40
47
  const { viewportHeight, terminalWidth } = useViewportHeight({
41
48
  overhead,
42
49
  minHeight: 5,
@@ -67,6 +74,9 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
67
74
  if (devboxId) {
68
75
  queryParams.devbox_id = devboxId;
69
76
  }
77
+ if (search.submittedSearchQuery) {
78
+ queryParams.search = search.submittedSearchQuery;
79
+ }
70
80
  // Fetch ONE page only
71
81
  const page = (await client.devboxes.listDiskSnapshots(queryParams));
72
82
  // Extract data and create defensive copies
@@ -87,7 +97,7 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
87
97
  totalCount: page.total_count || pageSnapshots.length,
88
98
  };
89
99
  return result;
90
- }, [devboxId]);
100
+ }, [devboxId, search.submittedSearchQuery]);
91
101
  // Use the shared pagination hook
92
102
  const { items: snapshots, loading, navigating, error, currentPage, hasMore, hasPrev, totalCount, nextPage, prevPage, refresh, } = useCursorPagination({
93
103
  fetchPage,
@@ -97,8 +107,9 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
97
107
  pollingEnabled: !showPopup &&
98
108
  !executingOperation &&
99
109
  !showCreateDevbox &&
100
- !showDeleteConfirm,
101
- deps: [devboxId, PAGE_SIZE],
110
+ !showDeleteConfirm &&
111
+ !search.searchMode,
112
+ deps: [devboxId, PAGE_SIZE, search.submittedSearchQuery],
102
113
  });
103
114
  // Operations for snapshots
104
115
  const operations = React.useMemo(() => [
@@ -180,6 +191,13 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
180
191
  }
181
192
  };
182
193
  useInput((input, key) => {
194
+ // Handle search mode input
195
+ if (search.searchMode) {
196
+ if (key.escape) {
197
+ search.cancelSearch();
198
+ }
199
+ return;
200
+ }
183
201
  // Handle operation result display
184
202
  if (operationResult || operationError) {
185
203
  if (input === "q" || key.escape || key.return) {
@@ -289,7 +307,13 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
289
307
  setShowPopup(true);
290
308
  setSelectedOperation(0);
291
309
  }
310
+ else if (input === "/") {
311
+ search.enterSearchMode();
312
+ }
292
313
  else if (key.escape) {
314
+ if (search.handleEscape()) {
315
+ return;
316
+ }
293
317
  if (onBack) {
294
318
  onBack();
295
319
  }
@@ -374,7 +398,7 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
374
398
  return (_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: [
375
399
  { label: "Snapshots", active: !devboxId },
376
400
  ...(devboxId ? [{ label: `Devbox: ${devboxId}`, active: true }] : []),
377
- ] }), !showPopup && (_jsx(Table, { data: snapshots, keyExtractor: (snapshot) => snapshot.id, selectedIndex: selectedIndex, title: `snapshots[${totalCount}]`, columns: columns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No snapshots found. Try: rli snapshot create", " ", "<devbox-id>"] }) })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] })] })), showPopup && selectedSnapshotItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedSnapshotItem, operations: operations.map((op) => ({
401
+ ] }), _jsx(SearchBar, { searchMode: search.searchMode, searchQuery: search.searchQuery, submittedSearchQuery: search.submittedSearchQuery, resultCount: totalCount, onSearchChange: search.setSearchQuery, onSearchSubmit: search.submitSearch, placeholder: "Search snapshots..." }), !showPopup && (_jsx(Table, { data: snapshots, keyExtractor: (snapshot) => snapshot.id, selectedIndex: selectedIndex, title: `snapshots[${totalCount}]`, columns: columns, emptyState: _jsxs(Text, { color: colors.textDim, children: [figures.info, " No snapshots found. Try: rli snapshot create", " ", "<devbox-id>"] }) })), !showPopup && (_jsxs(Box, { marginTop: 1, paddingX: 1, children: [_jsxs(Text, { color: colors.primary, bold: true, children: [figures.hamburger, " ", totalCount] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "total"] }), totalPages > 1 && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), navigating ? (_jsxs(Text, { color: colors.warning, children: [figures.pointer, " Loading page ", currentPage + 1, "..."] })) : (_jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Page ", currentPage + 1, " of ", totalPages] }))] })), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: ["Showing ", startIndex + 1, "-", endIndex, " of ", totalCount] }), search.submittedSearchQuery && (_jsxs(_Fragment, { children: [_jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "\u2022", " "] }), _jsxs(Text, { color: colors.warning, children: ["Filtered: \"", search.submittedSearchQuery, "\""] })] }))] })), showPopup && selectedSnapshotItem && (_jsx(Box, { marginTop: 2, justifyContent: "center", children: _jsx(ActionsPopup, { devbox: selectedSnapshotItem, operations: operations.map((op) => ({
378
402
  key: op.key,
379
403
  label: op.label,
380
404
  color: op.color,
@@ -394,6 +418,7 @@ const ListSnapshotsUI = ({ devboxId, onBack, onExit, }) => {
394
418
  },
395
419
  { key: "Enter", label: "Details" },
396
420
  { key: "a", label: "Actions" },
421
+ { key: "/", label: "Search" },
397
422
  { key: "Esc", label: "Back" },
398
423
  ] })] }));
399
424
  };
@@ -0,0 +1,24 @@
1
+ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import TextInput from "ink-text-input";
4
+ import figures from "figures";
5
+ import { colors } from "../utils/theme.js";
6
+ /**
7
+ * Reusable search bar component for list views.
8
+ * Displays either an input mode or the active search with result count.
9
+ */
10
+ export function SearchBar({ searchMode, searchQuery, submittedSearchQuery, resultCount, onSearchChange, onSearchSubmit, placeholder = "Type to search...", maxDisplayLength = 50, }) {
11
+ // Don't render if no search is active
12
+ if (!searchMode && !submittedSearchQuery) {
13
+ return null;
14
+ }
15
+ // Search input mode
16
+ if (searchMode) {
17
+ return (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, children: [figures.pointerSmall, " Search: "] }), _jsx(TextInput, { value: searchQuery, onChange: onSearchChange, placeholder: placeholder, onSubmit: onSearchSubmit }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "[Enter to search, Esc to cancel]"] })] }));
18
+ }
19
+ // Display active search with results
20
+ const displayQuery = submittedSearchQuery.length > maxDisplayLength
21
+ ? submittedSearchQuery.substring(0, maxDisplayLength) + "..."
22
+ : submittedSearchQuery;
23
+ return (_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { color: colors.primary, children: [figures.info, " Searching for: "] }), _jsx(Text, { color: colors.warning, bold: true, children: displayQuery }), _jsxs(Text, { color: colors.textDim, dimColor: true, children: [" ", "(", resultCount, " results) [/ to edit, Esc to clear]"] })] }));
24
+ }
@@ -0,0 +1,54 @@
1
+ import React from "react";
2
+ /**
3
+ * Shared hook for managing search state in list views.
4
+ * Provides consistent search behavior across all resource lists.
5
+ */
6
+ export function useListSearch(options = {}) {
7
+ const [searchMode, setSearchMode] = React.useState(false);
8
+ const [searchQuery, setSearchQuery] = React.useState("");
9
+ const [submittedSearchQuery, setSubmittedSearchQuery] = React.useState("");
10
+ const enterSearchMode = React.useCallback(() => {
11
+ setSearchMode(true);
12
+ }, []);
13
+ const cancelSearch = React.useCallback(() => {
14
+ setSearchMode(false);
15
+ setSearchQuery("");
16
+ }, []);
17
+ const submitSearch = React.useCallback(() => {
18
+ setSearchMode(false);
19
+ setSubmittedSearchQuery(searchQuery);
20
+ options.onSearchSubmit?.(searchQuery);
21
+ }, [searchQuery, options.onSearchSubmit]);
22
+ const clearSearch = React.useCallback(() => {
23
+ setSubmittedSearchQuery("");
24
+ setSearchQuery("");
25
+ options.onSearchClear?.();
26
+ }, [options.onSearchClear]);
27
+ const handleEscape = React.useCallback(() => {
28
+ if (searchMode) {
29
+ cancelSearch();
30
+ return true;
31
+ }
32
+ if (submittedSearchQuery) {
33
+ clearSearch();
34
+ return true;
35
+ }
36
+ return false;
37
+ }, [searchMode, submittedSearchQuery, cancelSearch, clearSearch]);
38
+ const getSearchOverhead = React.useCallback(() => {
39
+ // Search bar takes 2 lines when visible (1 line content + 1 marginBottom)
40
+ return searchMode || submittedSearchQuery ? 2 : 0;
41
+ }, [searchMode, submittedSearchQuery]);
42
+ return {
43
+ searchMode,
44
+ searchQuery,
45
+ submittedSearchQuery,
46
+ enterSearchMode,
47
+ cancelSearch,
48
+ setSearchQuery,
49
+ submitSearch,
50
+ clearSearch,
51
+ handleEscape,
52
+ getSearchOverhead,
53
+ };
54
+ }
@@ -117,7 +117,14 @@ export function output(data, options = {}) {
117
117
  export function outputError(message, error) {
118
118
  const errorMessage = error instanceof Error ? error.message : String(error || message);
119
119
  console.error(`Error: ${message}`);
120
- if (error && errorMessage !== message) {
120
+ // Only print the error message if it adds new information
121
+ // Skip if: same as message, message contains it, or it contains the message
122
+ const messageLower = message.toLowerCase();
123
+ const errorLower = errorMessage.toLowerCase();
124
+ const isRedundant = errorMessage === message ||
125
+ messageLower.includes(errorLower) ||
126
+ errorLower.includes(messageLower);
127
+ if (error && !isRedundant) {
121
128
  console.error(` ${errorMessage}`);
122
129
  }
123
130
  processUtils.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runloop/rl-cli",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Beautiful CLI for the Runloop platform",
5
5
  "type": "module",
6
6
  "bin": {
@@ -87,13 +87,13 @@
87
87
  "zustand": "^5.0.2"
88
88
  },
89
89
  "devDependencies": {
90
- "@anthropic-ai/mcpb": "^1.1.1",
90
+ "@anthropic-ai/mcpb": "^2.1.2",
91
91
  "@types/jest": "^29.5.0",
92
92
  "@types/node": "^22.7.9",
93
93
  "@types/react": "^19.2.2",
94
94
  "@typescript-eslint/eslint-plugin": "^8.46.0",
95
95
  "@typescript-eslint/parser": "^8.46.0",
96
- "esbuild": "^0.25.11",
96
+ "esbuild": "^0.27.2",
97
97
  "eslint": "^9.37.0",
98
98
  "eslint-plugin-react": "^7.37.5",
99
99
  "eslint-plugin-react-hooks": "^6.1.1",