@getlore/cli 0.2.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.
Files changed (148) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +80 -0
  3. package/dist/cli/colors.d.ts +48 -0
  4. package/dist/cli/colors.js +48 -0
  5. package/dist/cli/commands/ask.d.ts +7 -0
  6. package/dist/cli/commands/ask.js +97 -0
  7. package/dist/cli/commands/auth.d.ts +10 -0
  8. package/dist/cli/commands/auth.js +484 -0
  9. package/dist/cli/commands/daemon.d.ts +22 -0
  10. package/dist/cli/commands/daemon.js +244 -0
  11. package/dist/cli/commands/docs.d.ts +7 -0
  12. package/dist/cli/commands/docs.js +188 -0
  13. package/dist/cli/commands/extensions.d.ts +7 -0
  14. package/dist/cli/commands/extensions.js +204 -0
  15. package/dist/cli/commands/misc.d.ts +7 -0
  16. package/dist/cli/commands/misc.js +172 -0
  17. package/dist/cli/commands/pending.d.ts +7 -0
  18. package/dist/cli/commands/pending.js +63 -0
  19. package/dist/cli/commands/projects.d.ts +7 -0
  20. package/dist/cli/commands/projects.js +136 -0
  21. package/dist/cli/commands/search.d.ts +7 -0
  22. package/dist/cli/commands/search.js +102 -0
  23. package/dist/cli/commands/skills.d.ts +24 -0
  24. package/dist/cli/commands/skills.js +447 -0
  25. package/dist/cli/commands/sources.d.ts +7 -0
  26. package/dist/cli/commands/sources.js +121 -0
  27. package/dist/cli/commands/sync.d.ts +31 -0
  28. package/dist/cli/commands/sync.js +768 -0
  29. package/dist/cli/helpers.d.ts +30 -0
  30. package/dist/cli/helpers.js +119 -0
  31. package/dist/core/auth.d.ts +62 -0
  32. package/dist/core/auth.js +330 -0
  33. package/dist/core/config.d.ts +41 -0
  34. package/dist/core/config.js +96 -0
  35. package/dist/core/data-repo.d.ts +31 -0
  36. package/dist/core/data-repo.js +146 -0
  37. package/dist/core/embedder.d.ts +22 -0
  38. package/dist/core/embedder.js +104 -0
  39. package/dist/core/git.d.ts +37 -0
  40. package/dist/core/git.js +140 -0
  41. package/dist/core/index.d.ts +4 -0
  42. package/dist/core/index.js +5 -0
  43. package/dist/core/insight-extractor.d.ts +26 -0
  44. package/dist/core/insight-extractor.js +114 -0
  45. package/dist/core/local-search.d.ts +43 -0
  46. package/dist/core/local-search.js +221 -0
  47. package/dist/core/themes.d.ts +15 -0
  48. package/dist/core/themes.js +77 -0
  49. package/dist/core/types.d.ts +177 -0
  50. package/dist/core/types.js +9 -0
  51. package/dist/core/user-settings.d.ts +15 -0
  52. package/dist/core/user-settings.js +42 -0
  53. package/dist/core/vector-store-lance.d.ts +98 -0
  54. package/dist/core/vector-store-lance.js +384 -0
  55. package/dist/core/vector-store-supabase.d.ts +89 -0
  56. package/dist/core/vector-store-supabase.js +295 -0
  57. package/dist/core/vector-store.d.ts +131 -0
  58. package/dist/core/vector-store.js +503 -0
  59. package/dist/daemon-runner.d.ts +8 -0
  60. package/dist/daemon-runner.js +246 -0
  61. package/dist/extensions/config.d.ts +22 -0
  62. package/dist/extensions/config.js +102 -0
  63. package/dist/extensions/proposals.d.ts +30 -0
  64. package/dist/extensions/proposals.js +178 -0
  65. package/dist/extensions/registry.d.ts +35 -0
  66. package/dist/extensions/registry.js +309 -0
  67. package/dist/extensions/sandbox.d.ts +16 -0
  68. package/dist/extensions/sandbox.js +17 -0
  69. package/dist/extensions/types.d.ts +114 -0
  70. package/dist/extensions/types.js +4 -0
  71. package/dist/extensions/worker.d.ts +1 -0
  72. package/dist/extensions/worker.js +49 -0
  73. package/dist/index.d.ts +17 -0
  74. package/dist/index.js +105 -0
  75. package/dist/mcp/handlers/archive-project.d.ts +51 -0
  76. package/dist/mcp/handlers/archive-project.js +112 -0
  77. package/dist/mcp/handlers/get-quotes.d.ts +27 -0
  78. package/dist/mcp/handlers/get-quotes.js +61 -0
  79. package/dist/mcp/handlers/get-source.d.ts +9 -0
  80. package/dist/mcp/handlers/get-source.js +40 -0
  81. package/dist/mcp/handlers/ingest.d.ts +25 -0
  82. package/dist/mcp/handlers/ingest.js +305 -0
  83. package/dist/mcp/handlers/list-projects.d.ts +4 -0
  84. package/dist/mcp/handlers/list-projects.js +16 -0
  85. package/dist/mcp/handlers/list-sources.d.ts +11 -0
  86. package/dist/mcp/handlers/list-sources.js +20 -0
  87. package/dist/mcp/handlers/research-agent.d.ts +21 -0
  88. package/dist/mcp/handlers/research-agent.js +369 -0
  89. package/dist/mcp/handlers/research.d.ts +22 -0
  90. package/dist/mcp/handlers/research.js +225 -0
  91. package/dist/mcp/handlers/retain.d.ts +18 -0
  92. package/dist/mcp/handlers/retain.js +92 -0
  93. package/dist/mcp/handlers/search.d.ts +52 -0
  94. package/dist/mcp/handlers/search.js +145 -0
  95. package/dist/mcp/handlers/sync.d.ts +47 -0
  96. package/dist/mcp/handlers/sync.js +211 -0
  97. package/dist/mcp/server.d.ts +10 -0
  98. package/dist/mcp/server.js +268 -0
  99. package/dist/mcp/tools.d.ts +16 -0
  100. package/dist/mcp/tools.js +297 -0
  101. package/dist/sync/config.d.ts +26 -0
  102. package/dist/sync/config.js +140 -0
  103. package/dist/sync/discover.d.ts +51 -0
  104. package/dist/sync/discover.js +190 -0
  105. package/dist/sync/index.d.ts +11 -0
  106. package/dist/sync/index.js +11 -0
  107. package/dist/sync/process.d.ts +50 -0
  108. package/dist/sync/process.js +285 -0
  109. package/dist/sync/processors.d.ts +24 -0
  110. package/dist/sync/processors.js +351 -0
  111. package/dist/tui/browse-handlers-ask.d.ts +30 -0
  112. package/dist/tui/browse-handlers-ask.js +372 -0
  113. package/dist/tui/browse-handlers-autocomplete.d.ts +49 -0
  114. package/dist/tui/browse-handlers-autocomplete.js +270 -0
  115. package/dist/tui/browse-handlers-extensions.d.ts +18 -0
  116. package/dist/tui/browse-handlers-extensions.js +107 -0
  117. package/dist/tui/browse-handlers-pending.d.ts +22 -0
  118. package/dist/tui/browse-handlers-pending.js +100 -0
  119. package/dist/tui/browse-handlers-research.d.ts +32 -0
  120. package/dist/tui/browse-handlers-research.js +363 -0
  121. package/dist/tui/browse-handlers-tools.d.ts +42 -0
  122. package/dist/tui/browse-handlers-tools.js +289 -0
  123. package/dist/tui/browse-handlers.d.ts +239 -0
  124. package/dist/tui/browse-handlers.js +1944 -0
  125. package/dist/tui/browse-render-extensions.d.ts +14 -0
  126. package/dist/tui/browse-render-extensions.js +114 -0
  127. package/dist/tui/browse-render-tools.d.ts +18 -0
  128. package/dist/tui/browse-render-tools.js +259 -0
  129. package/dist/tui/browse-render.d.ts +51 -0
  130. package/dist/tui/browse-render.js +599 -0
  131. package/dist/tui/browse-types.d.ts +142 -0
  132. package/dist/tui/browse-types.js +70 -0
  133. package/dist/tui/browse-ui.d.ts +10 -0
  134. package/dist/tui/browse-ui.js +432 -0
  135. package/dist/tui/browse.d.ts +17 -0
  136. package/dist/tui/browse.js +625 -0
  137. package/dist/tui/markdown.d.ts +22 -0
  138. package/dist/tui/markdown.js +223 -0
  139. package/package.json +71 -0
  140. package/plugins/claude-code/.claude-plugin/plugin.json +10 -0
  141. package/plugins/claude-code/.mcp.json +6 -0
  142. package/plugins/claude-code/skills/lore/SKILL.md +63 -0
  143. package/plugins/codex/SKILL.md +36 -0
  144. package/plugins/codex/agents/openai.yaml +10 -0
  145. package/plugins/gemini/GEMINI.md +31 -0
  146. package/plugins/gemini/gemini-extension.json +11 -0
  147. package/skills/generic-agent.md +99 -0
  148. package/skills/openclaw.md +67 -0
@@ -0,0 +1,599 @@
1
+ /**
2
+ * Rendering functions for the Lore Document Browser TUI
3
+ *
4
+ * Functions for formatting and rendering content to the UI.
5
+ */
6
+ import { existsSync, readFileSync } from 'fs';
7
+ import path from 'path';
8
+ import os from 'os';
9
+ import { emojiReplacements } from './browse-types.js';
10
+ // Daemon status file path
11
+ const CONFIG_DIR = path.join(os.homedir(), '.config', 'lore');
12
+ const STATUS_FILE = path.join(CONFIG_DIR, 'daemon.status.json');
13
+ const AUTH_FILE = path.join(CONFIG_DIR, 'auth.json');
14
+ /**
15
+ * Check if daemon is running and get its status
16
+ */
17
+ function getDaemonStatus() {
18
+ if (!existsSync(STATUS_FILE)) {
19
+ return { running: false };
20
+ }
21
+ try {
22
+ const status = JSON.parse(readFileSync(STATUS_FILE, 'utf-8'));
23
+ // Check if the process is still running
24
+ try {
25
+ process.kill(status.pid, 0);
26
+ // Process exists
27
+ return {
28
+ running: true,
29
+ lastSync: status.last_sync,
30
+ };
31
+ }
32
+ catch {
33
+ // Process not running
34
+ return { running: false };
35
+ }
36
+ }
37
+ catch {
38
+ return { running: false };
39
+ }
40
+ }
41
+ /**
42
+ * Get the currently signed-in user's email (if any)
43
+ */
44
+ function getAuthUser() {
45
+ if (process.env.SUPABASE_SERVICE_KEY) {
46
+ return '[service key]';
47
+ }
48
+ if (!existsSync(AUTH_FILE)) {
49
+ return null;
50
+ }
51
+ try {
52
+ const auth = JSON.parse(readFileSync(AUTH_FILE, 'utf-8'));
53
+ return auth?.user?.email || null;
54
+ }
55
+ catch {
56
+ return null;
57
+ }
58
+ }
59
+ /**
60
+ * Format relative time for daemon status
61
+ */
62
+ function formatSyncTime(isoTime) {
63
+ const ms = Date.now() - new Date(isoTime).getTime();
64
+ const seconds = Math.floor(ms / 1000);
65
+ if (seconds < 60)
66
+ return 'just now';
67
+ const minutes = Math.floor(seconds / 60);
68
+ if (minutes < 60)
69
+ return `${minutes}m ago`;
70
+ const hours = Math.floor(minutes / 60);
71
+ if (hours < 24)
72
+ return `${hours}h ago`;
73
+ return `${Math.floor(hours / 24)}d ago`;
74
+ }
75
+ /**
76
+ * Format a date for display
77
+ */
78
+ export function formatDate(dateStr) {
79
+ const date = new Date(dateStr);
80
+ const now = new Date();
81
+ const diffDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
82
+ if (diffDays === 0)
83
+ return 'Today';
84
+ if (diffDays === 1)
85
+ return 'Yesterday';
86
+ if (diffDays < 7)
87
+ return `${diffDays}d ago`;
88
+ if (diffDays < 30)
89
+ return `${Math.floor(diffDays / 7)}w ago`;
90
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
91
+ }
92
+ /**
93
+ * Truncate a string to a maximum length
94
+ */
95
+ export function truncate(str, len) {
96
+ if (str.length <= len)
97
+ return str;
98
+ return str.slice(0, len - 1) + '…';
99
+ }
100
+ /**
101
+ * Escape text for blessed tags - must escape curly braces and handle special chars
102
+ */
103
+ export function escapeForBlessed(text) {
104
+ let result = text;
105
+ // Replace known emojis with ASCII equivalents
106
+ for (const [emoji, replacement] of Object.entries(emojiReplacements)) {
107
+ result = result.split(emoji).join(replacement);
108
+ }
109
+ return result
110
+ .replace(/\{/g, '\\{')
111
+ .replace(/\}/g, '\\}')
112
+ .replace(/\t/g, ' ') // Replace tabs with spaces
113
+ // Remove any remaining emojis (fallback)
114
+ .replace(/[\u{1F300}-\u{1F9FF}]/gu, '') // Misc symbols, emoticons, etc.
115
+ .replace(/[\u{2600}-\u{26FF}]/gu, '') // Misc symbols
116
+ .replace(/[\u{2700}-\u{27BF}]/gu, '') // Dingbats
117
+ .replace(/[\u{FE00}-\u{FE0F}]/gu, '') // Variation selectors
118
+ .replace(/[\u{1F000}-\u{1F02F}]/gu, '') // Mahjong, dominos
119
+ .replace(/[\u{1F0A0}-\u{1F0FF}]/gu, ''); // Playing cards
120
+ }
121
+ /**
122
+ * Format value as JSON for preview display
123
+ */
124
+ export function formatJsonForPreview(value) {
125
+ try {
126
+ if (typeof value === 'string') {
127
+ return value;
128
+ }
129
+ return JSON.stringify(value ?? null, null, 2) || '';
130
+ }
131
+ catch (error) {
132
+ return `JSON error: ${error instanceof Error ? error.message : String(error)}`;
133
+ }
134
+ }
135
+ /**
136
+ * Simple markdown to blessed tags converter (no ANSI codes)
137
+ */
138
+ export function markdownToBlessed(text) {
139
+ const lines = text.split('\n');
140
+ const result = [];
141
+ for (const line of lines) {
142
+ let processed = line;
143
+ // Escape first to protect content
144
+ processed = escapeForBlessed(processed);
145
+ // Headers (must check longer patterns first)
146
+ if (processed.startsWith('### ')) {
147
+ result.push(`{bold}{cyan-fg}${processed.slice(4)}{/cyan-fg}{/bold}`);
148
+ continue;
149
+ }
150
+ if (processed.startsWith('## ')) {
151
+ result.push('');
152
+ result.push(`{bold}{blue-fg}${processed.slice(3)}{/blue-fg}{/bold}`);
153
+ continue;
154
+ }
155
+ if (processed.startsWith('# ')) {
156
+ result.push('');
157
+ result.push(`{bold}{cyan-fg}${processed.slice(2)}{/cyan-fg}{/bold}`);
158
+ continue;
159
+ }
160
+ // Blockquotes
161
+ if (processed.startsWith('> ')) {
162
+ result.push(`{blue-fg}│{/blue-fg} {italic}${processed.slice(2)}{/italic}`);
163
+ continue;
164
+ }
165
+ // List items
166
+ if (processed.match(/^\s*[-*]\s/)) {
167
+ processed = processed.replace(/^(\s*)[-*]\s/, '$1{yellow-fg}•{/yellow-fg} ');
168
+ }
169
+ // Bold **text**
170
+ processed = processed.replace(/\*\*([^*]+)\*\*/g, '{bold}$1{/bold}');
171
+ // Italic *text*
172
+ processed = processed.replace(/\*([^*]+)\*/g, '{italic}$1{/italic}');
173
+ // Inline code `text`
174
+ processed = processed.replace(/`([^`]+)`/g, '{magenta-fg}$1{/magenta-fg}');
175
+ result.push(processed);
176
+ }
177
+ return result.join('\n');
178
+ }
179
+ /**
180
+ * Update the status bar
181
+ */
182
+ export function updateStatus(ui, state, project, sourceType) {
183
+ const count = state.filtered.length;
184
+ const label = 'document';
185
+ // Display user-friendly names for special project values
186
+ let projectDisplay = project;
187
+ if (project === '__unassigned__') {
188
+ projectDisplay = 'Unassigned';
189
+ }
190
+ else if (project === '__all__') {
191
+ projectDisplay = undefined; // Don't show when viewing all
192
+ }
193
+ const projectInfo = projectDisplay ? ` · ${projectDisplay}` : '';
194
+ const typeInfo = sourceType ? ` · ${sourceType}` : '';
195
+ const contentTypeInfo = state.currentContentType ? ` · type:${state.currentContentType}` : '';
196
+ const searchInfo = state.searchQuery ? ` · ${state.searchMode}: "${state.searchQuery}"` : '';
197
+ // Count unique projects in grouped view
198
+ let groupInfo = '';
199
+ if (state.groupByProject && state.listItems.length > 0) {
200
+ const projectCount = state.listItems.filter(i => i.type === 'header').length;
201
+ groupInfo = ` in ${projectCount} project${projectCount !== 1 ? 's' : ''}`;
202
+ }
203
+ // Check daemon status
204
+ const daemon = getDaemonStatus();
205
+ let daemonInfo = '';
206
+ if (daemon.running) {
207
+ const syncTime = daemon.lastSync ? formatSyncTime(daemon.lastSync) : 'starting';
208
+ daemonInfo = ` · sync: ${syncTime}`;
209
+ }
210
+ else {
211
+ daemonInfo = ' · [daemon off]';
212
+ }
213
+ // Show signed-in user
214
+ const authUser = getAuthUser();
215
+ const userInfo = authUser ? ` · ${authUser}` : ' · [not signed in]';
216
+ ui.statusBar.setContent(` ${count} ${label}${count !== 1 ? 's' : ''}${groupInfo}${projectInfo}${contentTypeInfo}${typeInfo}${searchInfo}${daemonInfo}${userInfo}`);
217
+ }
218
+ /**
219
+ * Build flattened list items from grouped sources
220
+ */
221
+ export function buildListItems(state) {
222
+ if (!state.groupByProject) {
223
+ // Flat view - just wrap documents
224
+ return state.filtered.map(source => ({
225
+ type: 'document',
226
+ source,
227
+ projectName: source.projects[0] || '__unassigned__',
228
+ }));
229
+ }
230
+ // Group documents by project
231
+ const byProject = new Map();
232
+ for (const source of state.filtered) {
233
+ const projectName = source.projects[0] || '__unassigned__';
234
+ if (!byProject.has(projectName)) {
235
+ byProject.set(projectName, []);
236
+ }
237
+ byProject.get(projectName).push(source);
238
+ }
239
+ // Sort projects alphabetically, but put __unassigned__ at the end
240
+ const projectNames = Array.from(byProject.keys()).sort((a, b) => {
241
+ if (a === '__unassigned__')
242
+ return 1;
243
+ if (b === '__unassigned__')
244
+ return -1;
245
+ return a.localeCompare(b);
246
+ });
247
+ // Build flattened list
248
+ const items = [];
249
+ for (const projectName of projectNames) {
250
+ const docs = byProject.get(projectName);
251
+ const expanded = state.expandedProjects.has(projectName);
252
+ const displayName = projectName === '__unassigned__' ? 'Unassigned' : projectName;
253
+ // Add header
254
+ items.push({
255
+ type: 'header',
256
+ projectName,
257
+ displayName,
258
+ documentCount: docs.length,
259
+ expanded,
260
+ });
261
+ // Add documents if expanded
262
+ if (expanded) {
263
+ for (const source of docs) {
264
+ items.push({
265
+ type: 'document',
266
+ source,
267
+ projectName,
268
+ });
269
+ }
270
+ }
271
+ }
272
+ return items;
273
+ }
274
+ /**
275
+ * Get the currently selected source (if any)
276
+ */
277
+ export function getSelectedSource(state) {
278
+ if (!state.groupByProject) {
279
+ return state.filtered[state.selectedIndex] || null;
280
+ }
281
+ const item = state.listItems[state.selectedIndex];
282
+ if (item && item.type === 'document') {
283
+ return item.source;
284
+ }
285
+ return null;
286
+ }
287
+ /**
288
+ * Render the document list (supports both flat and grouped views)
289
+ */
290
+ export function renderList(ui, state) {
291
+ const width = ui.listContent.width - 2;
292
+ const height = ui.listContent.height - 1;
293
+ const lines = [];
294
+ if (state.filtered.length === 0) {
295
+ lines.push('');
296
+ lines.push('{blue-fg} No documents found{/blue-fg}');
297
+ lines.push('');
298
+ if (state.searchQuery) {
299
+ lines.push('{blue-fg} Try a different search{/blue-fg}');
300
+ lines.push('{blue-fg} Press Esc to clear filter{/blue-fg}');
301
+ }
302
+ else {
303
+ lines.push('{blue-fg} Run "lore sync" to import documents{/blue-fg}');
304
+ }
305
+ ui.listContent.setContent(lines.join('\n'));
306
+ return;
307
+ }
308
+ // Rebuild list items if in grouped mode
309
+ if (state.groupByProject) {
310
+ state.listItems = buildListItems(state);
311
+ }
312
+ // Use grouped view if enabled
313
+ if (state.groupByProject && state.listItems.length > 0) {
314
+ renderGroupedList(ui, state, width, height, lines);
315
+ }
316
+ else {
317
+ renderFlatList(ui, state, width, height, lines);
318
+ }
319
+ ui.listContent.setContent(lines.join('\n'));
320
+ }
321
+ /**
322
+ * Render flat list (original behavior)
323
+ */
324
+ function renderFlatList(ui, state, width, height, lines) {
325
+ const linesPerItem = 3;
326
+ const itemsVisible = Math.floor(height / linesPerItem);
327
+ let visibleStart = 0;
328
+ if (state.selectedIndex >= itemsVisible) {
329
+ visibleStart = state.selectedIndex - itemsVisible + 1;
330
+ }
331
+ const visibleEnd = Math.min(state.filtered.length, visibleStart + itemsVisible);
332
+ for (let i = visibleStart; i < visibleEnd; i++) {
333
+ const source = state.filtered[i];
334
+ const isSelected = i === state.selectedIndex;
335
+ renderDocumentItem(source, isSelected, width, lines, true);
336
+ }
337
+ }
338
+ /**
339
+ * Render grouped list with collapsible project folders
340
+ */
341
+ function renderGroupedList(ui, state, width, height, lines) {
342
+ // Calculate lines per item (headers take 2, docs take 3)
343
+ const avgLinesPerItem = 2.5;
344
+ const itemsVisible = Math.floor(height / avgLinesPerItem);
345
+ let visibleStart = 0;
346
+ if (state.selectedIndex >= itemsVisible) {
347
+ visibleStart = state.selectedIndex - itemsVisible + 1;
348
+ }
349
+ const visibleEnd = Math.min(state.listItems.length, visibleStart + itemsVisible);
350
+ for (let i = visibleStart; i < visibleEnd; i++) {
351
+ const item = state.listItems[i];
352
+ const isSelected = i === state.selectedIndex;
353
+ if (item.type === 'header') {
354
+ renderProjectHeader(item, isSelected, width, lines);
355
+ }
356
+ else {
357
+ renderDocumentItem(item.source, isSelected, width, lines, false);
358
+ }
359
+ }
360
+ }
361
+ /**
362
+ * Render a project header row
363
+ */
364
+ function renderProjectHeader(item, isSelected, width, lines) {
365
+ const icon = item.expanded ? '▼' : '▶';
366
+ const countStr = `(${item.documentCount})`;
367
+ const name = truncate(item.displayName, width - 10);
368
+ if (isSelected) {
369
+ lines.push(`{inverse}{yellow-fg} ${icon} ${name} {cyan-fg}${countStr}{/cyan-fg} {/yellow-fg}{/inverse}`);
370
+ }
371
+ else {
372
+ lines.push(`{yellow-fg} ${icon} ${name}{/yellow-fg} {cyan-fg}${countStr}{/cyan-fg}`);
373
+ }
374
+ lines.push('');
375
+ }
376
+ /**
377
+ * Render a document item row
378
+ */
379
+ function renderDocumentItem(source, isSelected, width, lines, showProject) {
380
+ const date = formatDate(source.created_at);
381
+ const contentType = source.content_type || 'document';
382
+ const project = source.projects[0] || '';
383
+ // Format content type as a tag
384
+ const typeTag = `[${contentType}]`;
385
+ // Build metadata string (don't show project in grouped view)
386
+ const meta = showProject
387
+ ? `${date} {yellow-fg}${typeTag}{/yellow-fg}${project ? ` ${project}` : ''}`
388
+ : `${date} {yellow-fg}${typeTag}{/yellow-fg}`;
389
+ const indent = showProject ? '' : ' '; // Indent docs under headers
390
+ const title = truncate(source.title, width - 4 - indent.length);
391
+ const metaTrunc = truncate(meta, width - 6 - indent.length);
392
+ const accent = isSelected ? '{cyan-fg}▌{/cyan-fg}' : ' ';
393
+ lines.push(`${accent}${indent} {bold}${title}{/bold}`);
394
+ lines.push(`${accent}${indent} {cyan-fg}${metaTrunc}{/cyan-fg}`);
395
+ // Show relevance score if from semantic search
396
+ if (source.score !== undefined) {
397
+ const pct = Math.round(source.score * 100);
398
+ const filled = Math.round(pct / 10);
399
+ const bar = '●'.repeat(filled) + '○'.repeat(10 - filled);
400
+ lines.push(`${accent}${indent} {cyan-fg}${bar} ${pct}%{/cyan-fg}`);
401
+ }
402
+ lines.push('');
403
+ }
404
+ /**
405
+ * Render the preview pane
406
+ */
407
+ export function renderPreview(ui, state) {
408
+ if (state.filtered.length === 0) {
409
+ ui.previewContent.setContent('{blue-fg}No documents{/blue-fg}');
410
+ return;
411
+ }
412
+ const lines = [];
413
+ const previewWidth = ui.previewContent.width - 2;
414
+ // Handle grouped view
415
+ if (state.groupByProject && state.listItems.length > 0) {
416
+ const item = state.listItems[state.selectedIndex];
417
+ if (!item) {
418
+ ui.previewContent.setContent('{blue-fg}No selection{/blue-fg}');
419
+ return;
420
+ }
421
+ if (item.type === 'header') {
422
+ // Show project info
423
+ lines.push(`{bold}{yellow-fg}${item.displayName}{/yellow-fg}{/bold}`);
424
+ lines.push('');
425
+ lines.push(`{cyan-fg}${item.documentCount} document${item.documentCount !== 1 ? 's' : ''}{/cyan-fg}`);
426
+ lines.push('');
427
+ lines.push('{cyan-fg}─────────────────────────────────{/cyan-fg}');
428
+ lines.push('');
429
+ if (item.expanded) {
430
+ lines.push('{blue-fg}Press Space to collapse{/blue-fg}');
431
+ }
432
+ else {
433
+ lines.push('{blue-fg}Press Space to expand{/blue-fg}');
434
+ }
435
+ ui.previewContent.setContent(lines.join('\n'));
436
+ return;
437
+ }
438
+ // It's a document
439
+ renderDocumentPreview(item.source, previewWidth, lines);
440
+ ui.previewContent.setContent(lines.join('\n'));
441
+ return;
442
+ }
443
+ // Flat view
444
+ const source = state.filtered[state.selectedIndex];
445
+ if (!source)
446
+ return;
447
+ renderDocumentPreview(source, previewWidth, lines);
448
+ ui.previewContent.setContent(lines.join('\n'));
449
+ }
450
+ /**
451
+ * Render document preview content
452
+ */
453
+ function renderDocumentPreview(source, previewWidth, lines) {
454
+ // Title
455
+ lines.push(`{bold}${truncate(source.title, previewWidth)}{/bold}`);
456
+ lines.push('');
457
+ // Metadata
458
+ const date = formatDate(source.created_at);
459
+ const type = source.content_type || source.source_type;
460
+ const project = source.projects[0] || '';
461
+ lines.push(`{cyan-fg}${date} · ${type}${project ? ` · ${project}` : ''}{/cyan-fg}`);
462
+ // Show similarity score if from search
463
+ if (source.score !== undefined) {
464
+ const pct = Math.round(source.score * 100);
465
+ const filled = Math.round(pct / 10);
466
+ const bar = '●'.repeat(filled) + '○'.repeat(10 - filled);
467
+ lines.push(`{cyan-fg}${bar} ${pct}% match{/cyan-fg}`);
468
+ }
469
+ lines.push('');
470
+ lines.push('{cyan-fg}─────────────────────────────────{/cyan-fg}');
471
+ lines.push('');
472
+ // Summary with word wrap
473
+ const words = source.summary.split(' ');
474
+ let currentLine = '';
475
+ for (const word of words) {
476
+ if (currentLine.length + word.length + 1 > previewWidth) {
477
+ lines.push(currentLine);
478
+ currentLine = word;
479
+ }
480
+ else {
481
+ currentLine = currentLine ? `${currentLine} ${word}` : word;
482
+ }
483
+ }
484
+ if (currentLine)
485
+ lines.push(currentLine);
486
+ lines.push('');
487
+ lines.push('{cyan-fg}Press Enter to view full document{/cyan-fg}');
488
+ }
489
+ function highlightMatchesInLine(rawLine, pattern, isCurrentMatch) {
490
+ try {
491
+ const regex = new RegExp(`(${pattern})`, 'gi');
492
+ const highlightTag = isCurrentMatch
493
+ ? '{yellow-bg}{black-fg}'
494
+ : '{cyan-bg}{black-fg}';
495
+ const closeTag = isCurrentMatch
496
+ ? '{/black-fg}{/yellow-bg}'
497
+ : '{/black-fg}{/cyan-bg}';
498
+ // Escape the line for blessed first
499
+ let escaped = escapeForBlessed(rawLine);
500
+ // Then apply highlights to the escaped content
501
+ // We need to match on the original escaped text
502
+ escaped = escaped.replace(regex, `${highlightTag}$1${closeTag}`);
503
+ return escaped;
504
+ }
505
+ catch {
506
+ return escapeForBlessed(rawLine);
507
+ }
508
+ }
509
+ /**
510
+ * Build scrollbar content as a vertical string
511
+ */
512
+ function buildScrollbarContent(visibleHeight, totalLines, scrollOffset) {
513
+ // If content fits in view, no scrollbar needed
514
+ if (totalLines <= visibleHeight) {
515
+ return '';
516
+ }
517
+ // Calculate thumb size (minimum 1 line)
518
+ const thumbSize = Math.max(1, Math.round((visibleHeight / totalLines) * visibleHeight));
519
+ // Calculate thumb position
520
+ const maxScroll = totalLines - visibleHeight;
521
+ const scrollRatio = maxScroll > 0 ? scrollOffset / maxScroll : 0;
522
+ const thumbStart = Math.round(scrollRatio * (visibleHeight - thumbSize));
523
+ const thumbEnd = thumbStart + thumbSize;
524
+ // Build scrollbar as array of lines
525
+ const lines = [];
526
+ for (let i = 0; i < visibleHeight; i++) {
527
+ if (i >= thumbStart && i < thumbEnd) {
528
+ lines.push('{blue-fg}█{/blue-fg}'); // Thumb
529
+ }
530
+ else {
531
+ lines.push('{blue-fg}│{/blue-fg}'); // Track
532
+ }
533
+ }
534
+ return lines.join('\n');
535
+ }
536
+ /**
537
+ * Render the full view pane
538
+ */
539
+ export function renderFullView(ui, state) {
540
+ // Update title header with document info
541
+ const source = getSelectedSource(state);
542
+ if (source) {
543
+ const date = formatDate(source.created_at);
544
+ const type = source.content_type || source.source_type;
545
+ const project = source.projects[0] || '';
546
+ const titleWidth = ui.fullViewTitle.width - 2;
547
+ const titleLines = [];
548
+ titleLines.push(`{bold}${truncate(source.title, titleWidth)}{/bold}`);
549
+ titleLines.push(`{cyan-fg}${date} · ${type}${project ? ` · ${project}` : ''}{/cyan-fg}`);
550
+ titleLines.push('{blue-fg}' + '─'.repeat(Math.min(50, titleWidth)) + '{/blue-fg}');
551
+ ui.fullViewTitle.setContent(titleLines.join('\n'));
552
+ }
553
+ const height = ui.fullViewContent.height - 1;
554
+ // Get visible line range
555
+ const startLine = state.scrollOffset;
556
+ const endLine = Math.min(startLine + height, state.fullContentLines.length);
557
+ const totalLines = state.fullContentLines.length;
558
+ const visible = [];
559
+ for (let lineIndex = 0; lineIndex < endLine - startLine; lineIndex++) {
560
+ const lineNum = startLine + lineIndex;
561
+ const isMatchLine = state.docSearchMatches.includes(lineNum);
562
+ const isCurrentMatch = state.docSearchMatches[state.docSearchCurrentIdx] === lineNum;
563
+ let lineContent;
564
+ if (state.docSearchPattern && isMatchLine) {
565
+ // Get raw line and highlight matches within it
566
+ const rawLine = state.fullContentLinesRaw[lineNum] || '';
567
+ lineContent = highlightMatchesInLine(rawLine, state.docSearchPattern, isCurrentMatch);
568
+ }
569
+ else {
570
+ // No search or non-matching line - render normally
571
+ lineContent = state.fullContentLines[lineNum];
572
+ }
573
+ visible.push(lineContent);
574
+ }
575
+ ui.fullViewContent.setContent(visible.join('\n'));
576
+ // Update scrollbar (separate element on right edge)
577
+ const scrollbarContent = buildScrollbarContent(height, totalLines, state.scrollOffset);
578
+ ui.fullViewScrollbar.setContent(scrollbarContent);
579
+ // Show/hide scrollbar based on whether content is scrollable
580
+ if (totalLines > height) {
581
+ ui.fullViewScrollbar.show();
582
+ }
583
+ else {
584
+ ui.fullViewScrollbar.hide();
585
+ }
586
+ // Update footer for full view mode with scroll position
587
+ const currentLine = state.scrollOffset + 1;
588
+ const lastVisibleLine = Math.min(state.scrollOffset + height, totalLines);
589
+ const positionInfo = totalLines > height ? `{cyan-fg}${currentLine}-${lastVisibleLine}/${totalLines}{/cyan-fg} ` : '';
590
+ let footerText = ` ${positionInfo}j/k: scroll /: search e: editor Esc: back`;
591
+ if (state.docSearchPattern && state.docSearchMatches.length > 0) {
592
+ footerText = ` ${positionInfo}[${state.docSearchCurrentIdx + 1}/${state.docSearchMatches.length}] n/N: next/prev /: new search Esc: clear`;
593
+ }
594
+ else if (state.docSearchPattern && state.docSearchMatches.length === 0) {
595
+ footerText = ` ${positionInfo}No matches for "${state.docSearchPattern}" /: new search Esc: clear`;
596
+ }
597
+ ui.footer.setContent(footerText);
598
+ ui.screen.render();
599
+ }