@nexus-ai-fs/tui 0.9.18

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.
Files changed (193) hide show
  1. package/README.md +30 -0
  2. package/package.json +48 -0
  3. package/src/app.tsx +349 -0
  4. package/src/index.tsx +137 -0
  5. package/src/opentui-env.d.ts +61 -0
  6. package/src/panels/access/access-panel.tsx +597 -0
  7. package/src/panels/access/alert-list.tsx +77 -0
  8. package/src/panels/access/constraint-creator.tsx +128 -0
  9. package/src/panels/access/constraint-list.tsx +72 -0
  10. package/src/panels/access/credential-list.tsx +68 -0
  11. package/src/panels/access/delegation-chain-view.tsx +110 -0
  12. package/src/panels/access/delegation-completer.tsx +120 -0
  13. package/src/panels/access/delegation-creator.tsx +237 -0
  14. package/src/panels/access/delegation-list.tsx +74 -0
  15. package/src/panels/access/fraud-score-view.tsx +94 -0
  16. package/src/panels/access/manifest-creator.tsx +167 -0
  17. package/src/panels/access/manifest-list.tsx +105 -0
  18. package/src/panels/access/namespace-config-view.tsx +525 -0
  19. package/src/panels/access/permission-checker.tsx +231 -0
  20. package/src/panels/agents/agent-status-view.tsx +196 -0
  21. package/src/panels/agents/agents-panel.tsx +493 -0
  22. package/src/panels/agents/delegation-list.tsx +154 -0
  23. package/src/panels/agents/inbox-view.tsx +96 -0
  24. package/src/panels/agents/trajectories-tab.tsx +40 -0
  25. package/src/panels/api-console/api-console-panel.tsx +189 -0
  26. package/src/panels/api-console/codegen-viewer.tsx +36 -0
  27. package/src/panels/api-console/codegen.ts +112 -0
  28. package/src/panels/api-console/endpoint-list.tsx +57 -0
  29. package/src/panels/api-console/request-builder.tsx +69 -0
  30. package/src/panels/api-console/response-viewer.tsx +54 -0
  31. package/src/panels/connectors/available-tab.tsx +357 -0
  32. package/src/panels/connectors/connector-row.tsx +121 -0
  33. package/src/panels/connectors/connectors-panel.tsx +88 -0
  34. package/src/panels/connectors/error-parser.ts +116 -0
  35. package/src/panels/connectors/mounted-tab.tsx +179 -0
  36. package/src/panels/connectors/skills-tab.tsx +235 -0
  37. package/src/panels/connectors/template-generator.ts +211 -0
  38. package/src/panels/connectors/write-tab.tsx +514 -0
  39. package/src/panels/events/audit-tab.tsx +69 -0
  40. package/src/panels/events/audit-trail.tsx +75 -0
  41. package/src/panels/events/connector-detail.tsx +49 -0
  42. package/src/panels/events/connector-list.tsx +73 -0
  43. package/src/panels/events/connectors-tab.tsx +92 -0
  44. package/src/panels/events/event-replay.tsx +80 -0
  45. package/src/panels/events/events-panel.tsx +414 -0
  46. package/src/panels/events/events-tab.tsx +212 -0
  47. package/src/panels/events/lock-list.tsx +54 -0
  48. package/src/panels/events/locks-tab.tsx +103 -0
  49. package/src/panels/events/mcl-replay.tsx +77 -0
  50. package/src/panels/events/mcl-tab.tsx +83 -0
  51. package/src/panels/events/operations-tab-wrapper.tsx +62 -0
  52. package/src/panels/events/operations-tab.tsx +41 -0
  53. package/src/panels/events/replay-tab.tsx +76 -0
  54. package/src/panels/events/secrets-audit.tsx +64 -0
  55. package/src/panels/events/secrets-tab.tsx +75 -0
  56. package/src/panels/events/subscription-list.tsx +54 -0
  57. package/src/panels/events/subscriptions-tab.tsx +82 -0
  58. package/src/panels/files/file-aspects.tsx +93 -0
  59. package/src/panels/files/file-editor.tsx +160 -0
  60. package/src/panels/files/file-explorer-keybindings.ts +468 -0
  61. package/src/panels/files/file-explorer-panel.tsx +545 -0
  62. package/src/panels/files/file-lineage.tsx +163 -0
  63. package/src/panels/files/file-list-item.tsx +28 -0
  64. package/src/panels/files/file-metadata.tsx +62 -0
  65. package/src/panels/files/file-preview.tsx +108 -0
  66. package/src/panels/files/file-schema.tsx +89 -0
  67. package/src/panels/files/file-tree-node.tsx +44 -0
  68. package/src/panels/files/file-tree.tsx +169 -0
  69. package/src/panels/files/share-links-tab.tsx +33 -0
  70. package/src/panels/files/uploads-tab.tsx +45 -0
  71. package/src/panels/payments/approval-list.tsx +83 -0
  72. package/src/panels/payments/balance-card.tsx +43 -0
  73. package/src/panels/payments/budget-card.tsx +70 -0
  74. package/src/panels/payments/payments-panel.tsx +451 -0
  75. package/src/panels/payments/policy-list.tsx +64 -0
  76. package/src/panels/payments/reservation-list.tsx +78 -0
  77. package/src/panels/payments/transaction-list.tsx +103 -0
  78. package/src/panels/payments/transfer-form.tsx +109 -0
  79. package/src/panels/search/column-search.tsx +79 -0
  80. package/src/panels/search/knowledge-view.tsx +100 -0
  81. package/src/panels/search/memory-list.tsx +197 -0
  82. package/src/panels/search/playbook-list.tsx +77 -0
  83. package/src/panels/search/rlm-answer-view.tsx +105 -0
  84. package/src/panels/search/search-panel.tsx +405 -0
  85. package/src/panels/search/search-results.tsx +116 -0
  86. package/src/panels/stack/stack-panel.tsx +474 -0
  87. package/src/panels/versions/conflicts-tab.tsx +59 -0
  88. package/src/panels/versions/entry-detail.tsx +89 -0
  89. package/src/panels/versions/transaction-actions.tsx +34 -0
  90. package/src/panels/versions/transaction-list.tsx +90 -0
  91. package/src/panels/versions/versions-panel.tsx +276 -0
  92. package/src/panels/workflows/execution-list.tsx +102 -0
  93. package/src/panels/workflows/scheduler-view.tsx +135 -0
  94. package/src/panels/workflows/workflow-list.tsx +88 -0
  95. package/src/panels/workflows/workflows-panel.tsx +295 -0
  96. package/src/panels/zones/brick-detail.tsx +136 -0
  97. package/src/panels/zones/brick-list.tsx +56 -0
  98. package/src/panels/zones/cache-tab.tsx +118 -0
  99. package/src/panels/zones/drift-view.tsx +97 -0
  100. package/src/panels/zones/mcp-mounts-tab.tsx +38 -0
  101. package/src/panels/zones/memories-tab.tsx +37 -0
  102. package/src/panels/zones/reindex-status.tsx +84 -0
  103. package/src/panels/zones/workspaces-tab.tsx +37 -0
  104. package/src/panels/zones/zone-list.tsx +73 -0
  105. package/src/panels/zones/zones-panel.tsx +559 -0
  106. package/src/services/command-runner.ts +303 -0
  107. package/src/shared/accessibility-announcements.ts +44 -0
  108. package/src/shared/action-registry.ts +466 -0
  109. package/src/shared/brick-states.ts +91 -0
  110. package/src/shared/command-palette.ts +35 -0
  111. package/src/shared/components/announcement-bar.tsx +30 -0
  112. package/src/shared/components/app-confirm-dialog.tsx +29 -0
  113. package/src/shared/components/breadcrumb.tsx +21 -0
  114. package/src/shared/components/brick-gate.tsx +60 -0
  115. package/src/shared/components/command-output.tsx +95 -0
  116. package/src/shared/components/command-palette.tsx +97 -0
  117. package/src/shared/components/confirm-dialog.tsx +61 -0
  118. package/src/shared/components/diff-viewer.tsx +219 -0
  119. package/src/shared/components/empty-state.tsx +36 -0
  120. package/src/shared/components/error-bar.tsx +60 -0
  121. package/src/shared/components/error-boundary.tsx +53 -0
  122. package/src/shared/components/help-overlay.tsx +99 -0
  123. package/src/shared/components/identity-switcher.tsx +168 -0
  124. package/src/shared/components/loading-indicator.tsx +40 -0
  125. package/src/shared/components/pagination-bar.tsx +68 -0
  126. package/src/shared/components/pre-connection-screen.tsx +398 -0
  127. package/src/shared/components/scroll-indicator.tsx +46 -0
  128. package/src/shared/components/side-nav-utils.ts +68 -0
  129. package/src/shared/components/side-nav.tsx +287 -0
  130. package/src/shared/components/spinner.tsx +26 -0
  131. package/src/shared/components/status-bar.tsx +117 -0
  132. package/src/shared/components/styled-text.tsx +72 -0
  133. package/src/shared/components/sub-tab-bar-utils.ts +100 -0
  134. package/src/shared/components/sub-tab-bar.tsx +40 -0
  135. package/src/shared/components/tab-bar-utils.ts +36 -0
  136. package/src/shared/components/tab-bar.tsx +50 -0
  137. package/src/shared/components/text-input.tsx +73 -0
  138. package/src/shared/components/tooltip.tsx +53 -0
  139. package/src/shared/components/virtual-list.tsx +93 -0
  140. package/src/shared/components/welcome-screen.tsx +111 -0
  141. package/src/shared/hooks/use-api.ts +10 -0
  142. package/src/shared/hooks/use-brick-available.ts +42 -0
  143. package/src/shared/hooks/use-confirm.ts +66 -0
  144. package/src/shared/hooks/use-connection-state.ts +67 -0
  145. package/src/shared/hooks/use-copy.ts +31 -0
  146. package/src/shared/hooks/use-fresh-server.ts +62 -0
  147. package/src/shared/hooks/use-keyboard.ts +58 -0
  148. package/src/shared/hooks/use-list-navigation.ts +106 -0
  149. package/src/shared/hooks/use-swr.ts +117 -0
  150. package/src/shared/hooks/use-tab-fallback.ts +32 -0
  151. package/src/shared/hooks/use-text-input.ts +113 -0
  152. package/src/shared/hooks/use-visible-tabs.ts +61 -0
  153. package/src/shared/lib/circular-buffer.ts +82 -0
  154. package/src/shared/lib/clipboard.ts +14 -0
  155. package/src/shared/nav-items.ts +73 -0
  156. package/src/shared/navigation.ts +110 -0
  157. package/src/shared/status-breadcrumb.ts +74 -0
  158. package/src/shared/syntax-style.ts +3 -0
  159. package/src/shared/tab-visibility.ts +15 -0
  160. package/src/shared/text-style.ts +23 -0
  161. package/src/shared/theme.ts +179 -0
  162. package/src/shared/utils/format-size.ts +20 -0
  163. package/src/shared/utils/format-text.ts +10 -0
  164. package/src/shared/utils/format-time.ts +72 -0
  165. package/src/shared/utils/lru-cache.ts +75 -0
  166. package/src/stores/access-store-types.ts +154 -0
  167. package/src/stores/access-store.ts +674 -0
  168. package/src/stores/agents-store.ts +404 -0
  169. package/src/stores/announcement-store.ts +46 -0
  170. package/src/stores/api-console-store.ts +476 -0
  171. package/src/stores/connectors-store.ts +434 -0
  172. package/src/stores/create-api-action.ts +140 -0
  173. package/src/stores/delegation-store.ts +300 -0
  174. package/src/stores/error-store.ts +102 -0
  175. package/src/stores/events-store.ts +163 -0
  176. package/src/stores/files-store.ts +630 -0
  177. package/src/stores/first-run-store.ts +34 -0
  178. package/src/stores/global-store.ts +255 -0
  179. package/src/stores/infra-store.ts +461 -0
  180. package/src/stores/knowledge-store.ts +358 -0
  181. package/src/stores/lineage-store.ts +126 -0
  182. package/src/stores/mcp-store.ts +147 -0
  183. package/src/stores/payments-store.ts +545 -0
  184. package/src/stores/search-store-types.ts +155 -0
  185. package/src/stores/search-store.ts +656 -0
  186. package/src/stores/share-link-store.ts +151 -0
  187. package/src/stores/stack-store.ts +352 -0
  188. package/src/stores/ui-store.ts +161 -0
  189. package/src/stores/upload-store.ts +131 -0
  190. package/src/stores/versions-store.ts +355 -0
  191. package/src/stores/workflows-store.ts +402 -0
  192. package/src/stores/workspace-store.ts +185 -0
  193. package/src/stores/zones-store.ts +378 -0
@@ -0,0 +1,105 @@
1
+ /**
2
+ * RLM document Q&A answer view: progressive streaming display.
3
+ *
4
+ * Shows document context paths, iteration steps as they arrive via SSE,
5
+ * then the final answer. Status bar shows model, tokens, duration, iteration count.
6
+ */
7
+
8
+ import React from "react";
9
+ import type { RlmAnswer, RlmStep } from "../../stores/search-store.js";
10
+
11
+ interface RlmAnswerViewProps {
12
+ readonly answer: RlmAnswer | null;
13
+ readonly loading: boolean;
14
+ readonly contextPaths: readonly string[];
15
+ }
16
+
17
+ function formatStep(step: RlmStep): string {
18
+ const code = step.code_executed.length > 80
19
+ ? `${step.code_executed.slice(0, 77)}...`
20
+ : step.code_executed;
21
+ const output = step.output_summary.length > 80
22
+ ? `${step.output_summary.slice(0, 77)}...`
23
+ : step.output_summary;
24
+ return `[${step.step}] ${code}\n → ${output} (${step.tokens_used} tok, ${step.duration_seconds.toFixed(1)}s)`;
25
+ }
26
+
27
+ export function RlmAnswerView({ answer, loading, contextPaths }: RlmAnswerViewProps): React.ReactNode {
28
+ if (!answer && !loading) {
29
+ return (
30
+ <box height="100%" width="100%" flexDirection="column" justifyContent="center" alignItems="center">
31
+ <text>Press / to ask a question about your documents</text>
32
+ {contextPaths.length > 0 ? (
33
+ <text>{`Context: ${contextPaths.length} file(s) — a:clear`}</text>
34
+ ) : (
35
+ <text>{"Tip: go to Search tab, select results, press 'a' to add document context"}</text>
36
+ )}
37
+ </box>
38
+ );
39
+ }
40
+
41
+ if (!answer) {
42
+ return (
43
+ <box height="100%" width="100%" justifyContent="center" alignItems="center">
44
+ <text>Connecting to RLM...</text>
45
+ </box>
46
+ );
47
+ }
48
+
49
+ const statusLabel =
50
+ answer.status === "streaming" ? "Streaming..."
51
+ : answer.status === "completed" ? "Completed"
52
+ : answer.status === "budget_exceeded" ? "Budget Exceeded"
53
+ : "Error";
54
+
55
+ return (
56
+ <box height="100%" width="100%" flexDirection="column">
57
+ {/* Document context paths */}
58
+ {contextPaths.length > 0 && (
59
+ <box height={1} width="100%">
60
+ <text>{`Docs: ${contextPaths.join(", ")}`}</text>
61
+ </box>
62
+ )}
63
+
64
+ {/* Status bar */}
65
+ <box height={1} width="100%">
66
+ <text>
67
+ {`${statusLabel} ${answer.model ? `Model: ${answer.model} ` : ""}Iterations: ${answer.iterations} Tokens: ${answer.total_tokens}${answer.total_duration_seconds > 0 ? ` Time: ${answer.total_duration_seconds.toFixed(1)}s` : ""}`}
68
+ </text>
69
+ </box>
70
+
71
+ {/* Error / budget message */}
72
+ {answer.error_message && (
73
+ <box height={1} width="100%">
74
+ <text>
75
+ {answer.status === "budget_exceeded"
76
+ ? `Budget exceeded: ${answer.error_message}`
77
+ : `Error: ${answer.error_message}`}
78
+ </text>
79
+ </box>
80
+ )}
81
+
82
+ {/* Main content: answer or streaming steps */}
83
+ <scrollbox flexGrow={1} width="100%">
84
+ {answer.answer ? (
85
+ <text>{answer.answer}</text>
86
+ ) : answer.steps.length > 0 ? (
87
+ <box flexDirection="column">
88
+ {answer.steps.map((step) => (
89
+ <box key={step.step} height={2} width="100%">
90
+ <text>{formatStep(step)}</text>
91
+ </box>
92
+ ))}
93
+ {answer.status === "streaming" && (
94
+ <box height={1} width="100%">
95
+ <text>{"Thinking..."}</text>
96
+ </box>
97
+ )}
98
+ </box>
99
+ ) : (
100
+ <text>{answer.status === "streaming" ? "Starting inference..." : "(no answer)"}</text>
101
+ )}
102
+ </scrollbox>
103
+ </box>
104
+ );
105
+ }
@@ -0,0 +1,405 @@
1
+ /**
2
+ * Search & Knowledge panel: tabbed layout with search, knowledge graph,
3
+ * and memories views.
4
+ *
5
+ * Press / to enter search input mode, type query, Enter to submit, Escape to cancel.
6
+ */
7
+
8
+ import React, { useCallback } from "react";
9
+ import { useSearchStore } from "../../stores/search-store.js";
10
+ import { useGlobalStore } from "../../stores/global-store.js";
11
+ import type { SearchMode } from "../../stores/search-store.js";
12
+ import { useKeyboard } from "../../shared/hooks/use-keyboard.js";
13
+ import { listNavigationBindings } from "../../shared/hooks/use-list-navigation.js";
14
+ import { useTextInput } from "../../shared/hooks/use-text-input.js";
15
+ import { useConfirmStore } from "../../shared/hooks/use-confirm.js";
16
+ import { useApi } from "../../shared/hooks/use-api.js";
17
+ import { useUiStore } from "../../stores/ui-store.js";
18
+ import { useVisibleTabs } from "../../shared/hooks/use-visible-tabs.js";
19
+ import { SubTabBar } from "../../shared/components/sub-tab-bar.js";
20
+ import { subTabCycleBindings } from "../../shared/components/sub-tab-bar-utils.js";
21
+ import { useTabFallback } from "../../shared/hooks/use-tab-fallback.js";
22
+ import { SearchResults } from "./search-results.js";
23
+ import { KnowledgeView } from "./knowledge-view.js";
24
+ import { MemoryList } from "./memory-list.js";
25
+ import { PlaybookList } from "./playbook-list.js";
26
+ import { RlmAnswerView } from "./rlm-answer-view.js";
27
+ import { ColumnSearch } from "./column-search.js";
28
+ import { useKnowledgeStore } from "../../stores/knowledge-store.js";
29
+ import { Tooltip } from "../../shared/components/tooltip.js";
30
+ import { SEARCH_TABS } from "../../shared/navigation.js";
31
+
32
+ const MODE_LABELS: Readonly<Record<SearchMode, string>> = {
33
+ keyword: "KW",
34
+ semantic: "SEM",
35
+ hybrid: "HYB",
36
+ };
37
+
38
+ const HELP_TEXT: Readonly<Record<string, string>> = {
39
+ search: "j/k:navigate a:add to context /:search m:mode Enter:select Tab:tab r:refresh q:quit",
40
+ knowledge: "j/k:navigate Tab:switch tab /:search m:mode Enter:select d:delete r:refresh q:quit",
41
+ memories: "j/k:navigate Tab:tab /:search Enter:history v:diff n:create u:update d:delete Esc:close r:refresh q:quit",
42
+ playbooks: "j/k:navigate Tab:switch tab /:search m:mode Enter:select d:delete r:refresh q:quit",
43
+ ask: "/:ask a:clear context Tab:switch tab r:refresh q:quit",
44
+ columns: "/:search column Tab:switch tab r:refresh q:quit",
45
+ };
46
+
47
+ export default function SearchPanel(): React.ReactNode {
48
+ const client = useApi();
49
+ const confirm = useConfirmStore((s) => s.confirm);
50
+ const overlayActive = useUiStore((s) => s.overlayActive);
51
+ const visibleTabs = useVisibleTabs(SEARCH_TABS);
52
+ // Effective zone: explicit config > server-discovered zone (matches status-bar fallback)
53
+ const configZoneId = useGlobalStore((s) => s.config.zoneId);
54
+ const serverZoneId = useGlobalStore((s) => s.zoneId);
55
+ const effectiveZoneId = configZoneId ?? serverZoneId ?? undefined;
56
+
57
+ const searchQuery = useSearchStore((s) => s.searchQuery);
58
+ const searchResults = useSearchStore((s) => s.searchResults);
59
+ const expandedContent = useSearchStore((s) => s.expandedContent);
60
+ const expandedPath = useSearchStore((s) => s.expandedPath);
61
+ const searchTotal = useSearchStore((s) => s.searchTotal);
62
+ const selectedResultIndex = useSearchStore((s) => s.selectedResultIndex);
63
+ const searchLoading = useSearchStore((s) => s.searchLoading);
64
+ const selectedEntity = useSearchStore((s) => s.selectedEntity);
65
+ const neighbors = useSearchStore((s) => s.neighbors);
66
+ const knowledgeSearchResult = useSearchStore((s) => s.knowledgeSearchResult);
67
+ const knowledgeLoading = useSearchStore((s) => s.knowledgeLoading);
68
+ const memories = useSearchStore((s) => s.memories);
69
+ const selectedMemoryIndex = useSearchStore((s) => s.selectedMemoryIndex);
70
+ const memoriesLoading = useSearchStore((s) => s.memoriesLoading);
71
+ const memoryHistory = useSearchStore((s) => s.memoryHistory);
72
+ const memoryHistoryLoading = useSearchStore((s) => s.memoryHistoryLoading);
73
+ const memoryDiff = useSearchStore((s) => s.memoryDiff);
74
+ const memoryDiffLoading = useSearchStore((s) => s.memoryDiffLoading);
75
+ const playbooks = useSearchStore((s) => s.playbooks);
76
+ const selectedPlaybookIndex = useSearchStore((s) => s.selectedPlaybookIndex);
77
+ const playbooksLoading = useSearchStore((s) => s.playbooksLoading);
78
+ const rlmAnswer = useSearchStore((s) => s.rlmAnswer);
79
+ const rlmLoading = useSearchStore((s) => s.rlmLoading);
80
+ const rlmContextPaths = useSearchStore((s) => s.rlmContextPaths);
81
+ const activeTab = useSearchStore((s) => s.activeTab);
82
+ const error = useSearchStore((s) => s.error);
83
+
84
+ // Knowledge store (column search)
85
+ const columnSearchResults = useKnowledgeStore((s) => s.columnSearchResults);
86
+ const columnSearchLoading = useKnowledgeStore((s) => s.columnSearchLoading);
87
+ const searchByColumn = useKnowledgeStore((s) => s.searchByColumn);
88
+
89
+ const searchMode = useSearchStore((s) => s.searchMode);
90
+ const cycleSearchMode = useSearchStore((s) => s.cycleSearchMode);
91
+
92
+ const search = useSearchStore((s) => s.search);
93
+ const fetchEntity = useSearchStore((s) => s.fetchEntity);
94
+ const fetchNeighbors = useSearchStore((s) => s.fetchNeighbors);
95
+ const searchKnowledge = useSearchStore((s) => s.searchKnowledge);
96
+ const fetchMemories = useSearchStore((s) => s.fetchMemories);
97
+ const fetchPlaybooks = useSearchStore((s) => s.fetchPlaybooks);
98
+ const deletePlaybook = useSearchStore((s) => s.deletePlaybook);
99
+ const deleteMemory = useSearchStore((s) => s.deleteMemory);
100
+ const createMemory = useSearchStore((s) => s.createMemory);
101
+ const updateMemory = useSearchStore((s) => s.updateMemory);
102
+ const setSelectedPlaybookIndex = useSearchStore((s) => s.setSelectedPlaybookIndex);
103
+ const askRlm = useSearchStore((s) => s.askRlm);
104
+ const addRlmContextPath = useSearchStore((s) => s.addRlmContextPath);
105
+ const clearRlmContextPaths = useSearchStore((s) => s.clearRlmContextPaths);
106
+ const fetchMemoryHistory = useSearchStore((s) => s.fetchMemoryHistory);
107
+ const fetchMemoryDiff = useSearchStore((s) => s.fetchMemoryDiff);
108
+ const clearMemoryHistory = useSearchStore((s) => s.clearMemoryHistory);
109
+ const clearMemoryDiff = useSearchStore((s) => s.clearMemoryDiff);
110
+ const setActiveTab = useSearchStore((s) => s.setActiveTab);
111
+ const setSelectedResultIndex = useSearchStore((s) => s.setSelectedResultIndex);
112
+ const setSelectedMemoryIndex = useSearchStore((s) => s.setSelectedMemoryIndex);
113
+ const setSearchQuery = useSearchStore((s) => s.setSearchQuery);
114
+
115
+ useTabFallback(visibleTabs, activeTab, setActiveTab);
116
+
117
+ const submitSearch = useCallback(
118
+ (query: string) => {
119
+ if (!client || !query.trim()) return;
120
+
121
+ setSearchQuery(query.trim());
122
+ if (activeTab === "search") {
123
+ search(query.trim(), client);
124
+ } else if (activeTab === "knowledge") {
125
+ searchKnowledge(query.trim(), client);
126
+ } else if (activeTab === "memories") {
127
+ fetchMemories(query.trim(), client);
128
+ } else if (activeTab === "playbooks") {
129
+ fetchPlaybooks(query.trim(), client);
130
+ } else if (activeTab === "ask") {
131
+ askRlm(query.trim(), client, effectiveZoneId);
132
+ } else if (activeTab === "columns") {
133
+ void searchByColumn(query.trim(), client);
134
+ }
135
+ },
136
+ [client, activeTab, search, searchKnowledge, fetchMemories, fetchPlaybooks, askRlm, searchByColumn, setSearchQuery, effectiveZoneId],
137
+ );
138
+
139
+ // Refresh current view based on active tab
140
+ const refreshCurrentView = useCallback((): void => {
141
+ if (!client) return;
142
+
143
+ if (activeTab === "search" && searchQuery) {
144
+ search(searchQuery, client);
145
+ } else if (activeTab === "knowledge" && searchQuery) {
146
+ searchKnowledge(searchQuery, client);
147
+ } else if (activeTab === "memories") {
148
+ fetchMemories("", client);
149
+ } else if (activeTab === "playbooks") {
150
+ fetchPlaybooks(searchQuery || "", client);
151
+ } else if (activeTab === "ask" && searchQuery) {
152
+ askRlm(searchQuery, client, effectiveZoneId);
153
+ } else if (activeTab === "columns" && searchQuery) {
154
+ void searchByColumn(searchQuery, client);
155
+ }
156
+ }, [client, activeTab, searchQuery, search, searchKnowledge, fetchMemories, fetchPlaybooks, askRlm, searchByColumn, effectiveZoneId]);
157
+
158
+ // Text input for search bar
159
+ const textInput = useTextInput({
160
+ onSubmit: (val) => {
161
+ // Clear expanded content when submitting a new search
162
+ useSearchStore.setState({ expandedContent: null, expandedPath: null });
163
+ submitSearch(val);
164
+ },
165
+ });
166
+
167
+ // Shared list navigation (j/k/up/down/g/G) — switches per active tab
168
+ const listNav = listNavigationBindings({
169
+ getIndex: () => {
170
+ if (activeTab === "search") return selectedResultIndex;
171
+ if (activeTab === "memories") return selectedMemoryIndex;
172
+ if (activeTab === "playbooks") return selectedPlaybookIndex;
173
+ return 0;
174
+ },
175
+ setIndex: (i) => {
176
+ if (activeTab === "search") setSelectedResultIndex(i);
177
+ else if (activeTab === "memories") setSelectedMemoryIndex(i);
178
+ else if (activeTab === "playbooks") setSelectedPlaybookIndex(i);
179
+ },
180
+ getLength: () => {
181
+ if (activeTab === "search") return searchResults.length;
182
+ if (activeTab === "memories") return memories.length;
183
+ if (activeTab === "playbooks") return playbooks.length;
184
+ return 0;
185
+ },
186
+ });
187
+
188
+ useKeyboard(
189
+ overlayActive
190
+ ? {}
191
+ : textInput.active
192
+ ? textInput.inputBindings
193
+ : {
194
+ ...listNav,
195
+ ...subTabCycleBindings(visibleTabs, activeTab, setActiveTab),
196
+ r: () => refreshCurrentView(),
197
+ m: () => cycleSearchMode(),
198
+ "/": () => textInput.activate(searchQuery),
199
+ return: () => {
200
+ if (!client) return;
201
+
202
+ if (activeTab === "search") {
203
+ const result = searchResults[selectedResultIndex];
204
+ if (result) {
205
+ // Read the full file and show as expanded content
206
+ client.get<{ content: string }>(`/api/v2/files/read?path=${encodeURIComponent(result.path)}`)
207
+ .then((r) => {
208
+ useSearchStore.setState({ expandedContent: r.content, expandedPath: result.path });
209
+ })
210
+ .catch(() => {});
211
+ }
212
+ } else if (activeTab === "knowledge") {
213
+ if (selectedEntity) {
214
+ const entityId = String(
215
+ (selectedEntity as Record<string, unknown>).entity_id ?? "",
216
+ );
217
+ if (entityId) {
218
+ fetchNeighbors(entityId, client);
219
+ }
220
+ }
221
+ } else if (activeTab === "memories") {
222
+ const memory = memories[selectedMemoryIndex];
223
+ if (memory) {
224
+ const memId = String(
225
+ (memory as Record<string, unknown>).memory_id ?? "",
226
+ );
227
+ if (memId) {
228
+ if (memoryHistory?.memory_id === memId) {
229
+ clearMemoryHistory();
230
+ clearMemoryDiff();
231
+ } else {
232
+ clearMemoryDiff();
233
+ fetchMemoryHistory(memId, client);
234
+ }
235
+ }
236
+ }
237
+ }
238
+ },
239
+ v: () => {
240
+ if (!client || activeTab !== "memories") return;
241
+
242
+ const memory = memories[selectedMemoryIndex];
243
+ if (!memory) return;
244
+
245
+ const memId = String(
246
+ (memory as Record<string, unknown>).memory_id ?? "",
247
+ );
248
+ if (!memId) return;
249
+
250
+ if (memoryHistory && memoryHistory.memory_id === memId && memoryHistory.current_version >= 2) {
251
+ fetchMemoryDiff(
252
+ memId,
253
+ memoryHistory.current_version - 1,
254
+ memoryHistory.current_version,
255
+ client,
256
+ );
257
+ }
258
+ },
259
+ d: async () => {
260
+ if (!client) return;
261
+ if (activeTab === "playbooks") {
262
+ const playbook = playbooks[selectedPlaybookIndex];
263
+ if (playbook) {
264
+ const ok = await confirm("Delete playbook?", "This cannot be undone.");
265
+ if (!ok) return;
266
+ deletePlaybook(playbook.playbook_id, client);
267
+ }
268
+ } else if (activeTab === "memories") {
269
+ const memory = memories[selectedMemoryIndex];
270
+ if (memory) {
271
+ const memId = String((memory as Record<string, unknown>).memory_id ?? "");
272
+ if (memId) {
273
+ const ok = await confirm("Delete memory?", "This cannot be undone.");
274
+ if (!ok) return;
275
+ deleteMemory(memId, client);
276
+ }
277
+ }
278
+ }
279
+ },
280
+ n: () => {
281
+ if (activeTab === "memories" && client && searchQuery.trim()) {
282
+ createMemory(searchQuery.trim(), {}, client);
283
+ }
284
+ },
285
+ u: () => {
286
+ if (activeTab === "memories" && client && searchQuery.trim()) {
287
+ const memory = memories[selectedMemoryIndex];
288
+ if (memory) {
289
+ const memId = String((memory as Record<string, unknown>).memory_id ?? "");
290
+ if (memId) updateMemory(memId, searchQuery.trim(), client);
291
+ }
292
+ }
293
+ },
294
+ a: () => {
295
+ if (activeTab === "search") {
296
+ const result = searchResults[selectedResultIndex];
297
+ if (result) {
298
+ addRlmContextPath(result.path);
299
+ }
300
+ } else if (activeTab === "ask") {
301
+ clearRlmContextPaths();
302
+ }
303
+ },
304
+ escape: () => {
305
+ // Close expanded file content
306
+ if (expandedContent !== null) {
307
+ useSearchStore.setState({ expandedContent: null, expandedPath: null });
308
+ return;
309
+ }
310
+ if (activeTab === "memories" && (memoryHistory || memoryDiff)) {
311
+ clearMemoryHistory();
312
+ clearMemoryDiff();
313
+ }
314
+ },
315
+ },
316
+ overlayActive ? undefined : textInput.active ? textInput.onUnhandled : undefined,
317
+ );
318
+
319
+ return (
320
+ <box height="100%" width="100%" flexDirection="column">
321
+ <Tooltip tooltipKey="search-panel" message="Tip: Press ? for keybinding help" />
322
+ {/* Search input bar */}
323
+ <box height={1} width="100%">
324
+ <text>
325
+ {textInput.active
326
+ ? `Search: ${textInput.buffer}█`
327
+ : `Query: ${searchQuery || "(press / to search)"} [${MODE_LABELS[searchMode]}]`}
328
+ </text>
329
+ </box>
330
+
331
+ {/* Tab bar */}
332
+ <SubTabBar tabs={visibleTabs} activeTab={activeTab} />
333
+
334
+ {/* Error display */}
335
+ {error && (
336
+ <box height={1} width="100%">
337
+ <text>{`Error: ${error}`}</text>
338
+ </box>
339
+ )}
340
+
341
+ {/* Tab content */}
342
+ <box flexGrow={1} borderStyle="single">
343
+ {activeTab === "search" && expandedContent !== null && (
344
+ <box height="100%" width="100%" flexDirection="column">
345
+ <box height={1} width="100%">
346
+ <text bold>{`── ${expandedPath} ── (Escape to close)`}</text>
347
+ </box>
348
+ <scrollbox flexGrow={1} width="100%">
349
+ <text>{expandedContent}</text>
350
+ </scrollbox>
351
+ </box>
352
+ )}
353
+ {activeTab === "search" && expandedContent === null && (
354
+ <SearchResults
355
+ results={searchResults}
356
+ total={searchTotal}
357
+ selectedIndex={selectedResultIndex}
358
+ loading={searchLoading}
359
+ />
360
+ )}
361
+ {activeTab === "knowledge" && (
362
+ <KnowledgeView
363
+ entity={selectedEntity}
364
+ neighbors={neighbors}
365
+ knowledgeSearchResult={knowledgeSearchResult}
366
+ loading={knowledgeLoading}
367
+ />
368
+ )}
369
+ {activeTab === "memories" && (
370
+ <MemoryList
371
+ memories={memories}
372
+ selectedIndex={selectedMemoryIndex}
373
+ loading={memoriesLoading}
374
+ memoryHistory={memoryHistory}
375
+ memoryHistoryLoading={memoryHistoryLoading}
376
+ memoryDiff={memoryDiff}
377
+ memoryDiffLoading={memoryDiffLoading}
378
+ />
379
+ )}
380
+ {activeTab === "playbooks" && (
381
+ <PlaybookList
382
+ playbooks={playbooks}
383
+ selectedIndex={selectedPlaybookIndex}
384
+ loading={playbooksLoading}
385
+ />
386
+ )}
387
+ {activeTab === "ask" && (
388
+ <RlmAnswerView answer={rlmAnswer} loading={rlmLoading} contextPaths={rlmContextPaths} />
389
+ )}
390
+ {activeTab === "columns" && (
391
+ <ColumnSearch results={columnSearchResults} loading={columnSearchLoading} />
392
+ )}
393
+ </box>
394
+
395
+ {/* Help bar */}
396
+ <box height={1} width="100%">
397
+ <text>
398
+ {textInput.active
399
+ ? "Type query, Enter:submit, Escape:cancel, Backspace:delete"
400
+ : HELP_TEXT[activeTab] ?? "j/k:navigate Tab:switch tab r:refresh q:quit"}
401
+ </text>
402
+ </box>
403
+ </box>
404
+ );
405
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Search results list: path, chunk_text (truncated), score, line range.
3
+ */
4
+
5
+ import React, { useCallback } from "react";
6
+ import type { SearchResult } from "../../stores/search-store.js";
7
+ import { textStyle } from "../../shared/text-style.js";
8
+ import { statusColor } from "../../shared/theme.js";
9
+ import { EmptyState } from "../../shared/components/empty-state.js";
10
+ import { VirtualList } from "../../shared/components/virtual-list.js";
11
+ import { truncateText } from "../../shared/utils/format-text.js";
12
+
13
+ const VIEWPORT_HEIGHT = 20;
14
+
15
+ interface SearchResultsProps {
16
+ readonly results: readonly SearchResult[];
17
+ readonly total: number;
18
+ readonly selectedIndex: number;
19
+ readonly loading: boolean;
20
+ }
21
+
22
+ function formatScore(score: number): string {
23
+ return score.toFixed(2);
24
+ }
25
+
26
+ function scoreColor(score: number): string {
27
+ if (score > 0.7) return statusColor.healthy;
28
+ if (score >= 0.4) return statusColor.warning;
29
+ return statusColor.dim;
30
+ }
31
+
32
+ function formatLineRange(start: number | null, end: number | null): string {
33
+ if (start == null) return "—";
34
+ if (start === end || end == null) return `L${start}`;
35
+ return `L${start}-${end}`;
36
+ }
37
+
38
+ function formatScoreBreakdown(result: SearchResult): string {
39
+ const parts: string[] = [];
40
+ if (result.keyword_score != null) parts.push(`bm25:${result.keyword_score.toFixed(2)}`);
41
+ if (result.vector_score != null) parts.push(`vec:${result.vector_score.toFixed(2)}`);
42
+ if (result.reranker_score != null) parts.push(`rerank:${result.reranker_score.toFixed(2)}`);
43
+ return parts.length > 0 ? parts.join(" ") : "";
44
+ }
45
+
46
+ export function SearchResults({
47
+ results,
48
+ total,
49
+ selectedIndex,
50
+ loading,
51
+ }: SearchResultsProps): React.ReactNode {
52
+ if (loading) {
53
+ return (
54
+ <box height="100%" width="100%" justifyContent="center" alignItems="center">
55
+ <text>Searching...</text>
56
+ </box>
57
+ );
58
+ }
59
+
60
+ if (results.length === 0) {
61
+ return (
62
+ <EmptyState
63
+ message="No results."
64
+ hint="Try a different query or search mode (m to cycle: KW → SEM → HYB)."
65
+ />
66
+ );
67
+ }
68
+
69
+ const renderResult = useCallback(
70
+ (result: SearchResult, i: number) => {
71
+ const isSelected = i === selectedIndex;
72
+ const prefix = isSelected ? "> " : " ";
73
+ const score = formatScore(result.score).padEnd(5);
74
+ const lines = formatLineRange(result.line_start, result.line_end).padEnd(9);
75
+ const path = truncateText(result.path, 29).padEnd(29);
76
+ const breakdown = formatScoreBreakdown(result);
77
+ const chunk = truncateText(result.chunk_text.replace(/\n/g, " "), 30);
78
+
79
+ return (
80
+ <box key={`${result.path}:${result.chunk_index}`} height={1} width="100%">
81
+ <text>
82
+ <span>{prefix}</span>
83
+ <span style={textStyle({ fg: scoreColor(result.score) })}>{score}</span>
84
+ <span>{` ${lines} ${path} `}</span>
85
+ <span style={textStyle({ dim: true })}>{breakdown ? `[${breakdown}] ` : ""}</span>
86
+ <span>{chunk}</span>
87
+ </text>
88
+ </box>
89
+ );
90
+ },
91
+ [selectedIndex],
92
+ );
93
+
94
+ return (
95
+ <box height="100%" width="100%" flexDirection="column">
96
+ {/* Header */}
97
+ <box height={1} width="100%">
98
+ <text>{`Results: ${total} found`}</text>
99
+ </box>
100
+ <box height={1} width="100%">
101
+ <text>{" SCORE LINES PATH CHUNK"}</text>
102
+ </box>
103
+ <box height={1} width="100%">
104
+ <text>{" ----- --------- ----------------------------- --------------------------------"}</text>
105
+ </box>
106
+
107
+ {/* Result rows */}
108
+ <VirtualList
109
+ items={results}
110
+ renderItem={renderResult}
111
+ viewportHeight={VIEWPORT_HEIGHT}
112
+ selectedIndex={selectedIndex}
113
+ />
114
+ </box>
115
+ );
116
+ }