@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.
- package/LICENSE +13 -0
- package/README.md +80 -0
- package/dist/cli/colors.d.ts +48 -0
- package/dist/cli/colors.js +48 -0
- package/dist/cli/commands/ask.d.ts +7 -0
- package/dist/cli/commands/ask.js +97 -0
- package/dist/cli/commands/auth.d.ts +10 -0
- package/dist/cli/commands/auth.js +484 -0
- package/dist/cli/commands/daemon.d.ts +22 -0
- package/dist/cli/commands/daemon.js +244 -0
- package/dist/cli/commands/docs.d.ts +7 -0
- package/dist/cli/commands/docs.js +188 -0
- package/dist/cli/commands/extensions.d.ts +7 -0
- package/dist/cli/commands/extensions.js +204 -0
- package/dist/cli/commands/misc.d.ts +7 -0
- package/dist/cli/commands/misc.js +172 -0
- package/dist/cli/commands/pending.d.ts +7 -0
- package/dist/cli/commands/pending.js +63 -0
- package/dist/cli/commands/projects.d.ts +7 -0
- package/dist/cli/commands/projects.js +136 -0
- package/dist/cli/commands/search.d.ts +7 -0
- package/dist/cli/commands/search.js +102 -0
- package/dist/cli/commands/skills.d.ts +24 -0
- package/dist/cli/commands/skills.js +447 -0
- package/dist/cli/commands/sources.d.ts +7 -0
- package/dist/cli/commands/sources.js +121 -0
- package/dist/cli/commands/sync.d.ts +31 -0
- package/dist/cli/commands/sync.js +768 -0
- package/dist/cli/helpers.d.ts +30 -0
- package/dist/cli/helpers.js +119 -0
- package/dist/core/auth.d.ts +62 -0
- package/dist/core/auth.js +330 -0
- package/dist/core/config.d.ts +41 -0
- package/dist/core/config.js +96 -0
- package/dist/core/data-repo.d.ts +31 -0
- package/dist/core/data-repo.js +146 -0
- package/dist/core/embedder.d.ts +22 -0
- package/dist/core/embedder.js +104 -0
- package/dist/core/git.d.ts +37 -0
- package/dist/core/git.js +140 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.js +5 -0
- package/dist/core/insight-extractor.d.ts +26 -0
- package/dist/core/insight-extractor.js +114 -0
- package/dist/core/local-search.d.ts +43 -0
- package/dist/core/local-search.js +221 -0
- package/dist/core/themes.d.ts +15 -0
- package/dist/core/themes.js +77 -0
- package/dist/core/types.d.ts +177 -0
- package/dist/core/types.js +9 -0
- package/dist/core/user-settings.d.ts +15 -0
- package/dist/core/user-settings.js +42 -0
- package/dist/core/vector-store-lance.d.ts +98 -0
- package/dist/core/vector-store-lance.js +384 -0
- package/dist/core/vector-store-supabase.d.ts +89 -0
- package/dist/core/vector-store-supabase.js +295 -0
- package/dist/core/vector-store.d.ts +131 -0
- package/dist/core/vector-store.js +503 -0
- package/dist/daemon-runner.d.ts +8 -0
- package/dist/daemon-runner.js +246 -0
- package/dist/extensions/config.d.ts +22 -0
- package/dist/extensions/config.js +102 -0
- package/dist/extensions/proposals.d.ts +30 -0
- package/dist/extensions/proposals.js +178 -0
- package/dist/extensions/registry.d.ts +35 -0
- package/dist/extensions/registry.js +309 -0
- package/dist/extensions/sandbox.d.ts +16 -0
- package/dist/extensions/sandbox.js +17 -0
- package/dist/extensions/types.d.ts +114 -0
- package/dist/extensions/types.js +4 -0
- package/dist/extensions/worker.d.ts +1 -0
- package/dist/extensions/worker.js +49 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +105 -0
- package/dist/mcp/handlers/archive-project.d.ts +51 -0
- package/dist/mcp/handlers/archive-project.js +112 -0
- package/dist/mcp/handlers/get-quotes.d.ts +27 -0
- package/dist/mcp/handlers/get-quotes.js +61 -0
- package/dist/mcp/handlers/get-source.d.ts +9 -0
- package/dist/mcp/handlers/get-source.js +40 -0
- package/dist/mcp/handlers/ingest.d.ts +25 -0
- package/dist/mcp/handlers/ingest.js +305 -0
- package/dist/mcp/handlers/list-projects.d.ts +4 -0
- package/dist/mcp/handlers/list-projects.js +16 -0
- package/dist/mcp/handlers/list-sources.d.ts +11 -0
- package/dist/mcp/handlers/list-sources.js +20 -0
- package/dist/mcp/handlers/research-agent.d.ts +21 -0
- package/dist/mcp/handlers/research-agent.js +369 -0
- package/dist/mcp/handlers/research.d.ts +22 -0
- package/dist/mcp/handlers/research.js +225 -0
- package/dist/mcp/handlers/retain.d.ts +18 -0
- package/dist/mcp/handlers/retain.js +92 -0
- package/dist/mcp/handlers/search.d.ts +52 -0
- package/dist/mcp/handlers/search.js +145 -0
- package/dist/mcp/handlers/sync.d.ts +47 -0
- package/dist/mcp/handlers/sync.js +211 -0
- package/dist/mcp/server.d.ts +10 -0
- package/dist/mcp/server.js +268 -0
- package/dist/mcp/tools.d.ts +16 -0
- package/dist/mcp/tools.js +297 -0
- package/dist/sync/config.d.ts +26 -0
- package/dist/sync/config.js +140 -0
- package/dist/sync/discover.d.ts +51 -0
- package/dist/sync/discover.js +190 -0
- package/dist/sync/index.d.ts +11 -0
- package/dist/sync/index.js +11 -0
- package/dist/sync/process.d.ts +50 -0
- package/dist/sync/process.js +285 -0
- package/dist/sync/processors.d.ts +24 -0
- package/dist/sync/processors.js +351 -0
- package/dist/tui/browse-handlers-ask.d.ts +30 -0
- package/dist/tui/browse-handlers-ask.js +372 -0
- package/dist/tui/browse-handlers-autocomplete.d.ts +49 -0
- package/dist/tui/browse-handlers-autocomplete.js +270 -0
- package/dist/tui/browse-handlers-extensions.d.ts +18 -0
- package/dist/tui/browse-handlers-extensions.js +107 -0
- package/dist/tui/browse-handlers-pending.d.ts +22 -0
- package/dist/tui/browse-handlers-pending.js +100 -0
- package/dist/tui/browse-handlers-research.d.ts +32 -0
- package/dist/tui/browse-handlers-research.js +363 -0
- package/dist/tui/browse-handlers-tools.d.ts +42 -0
- package/dist/tui/browse-handlers-tools.js +289 -0
- package/dist/tui/browse-handlers.d.ts +239 -0
- package/dist/tui/browse-handlers.js +1944 -0
- package/dist/tui/browse-render-extensions.d.ts +14 -0
- package/dist/tui/browse-render-extensions.js +114 -0
- package/dist/tui/browse-render-tools.d.ts +18 -0
- package/dist/tui/browse-render-tools.js +259 -0
- package/dist/tui/browse-render.d.ts +51 -0
- package/dist/tui/browse-render.js +599 -0
- package/dist/tui/browse-types.d.ts +142 -0
- package/dist/tui/browse-types.js +70 -0
- package/dist/tui/browse-ui.d.ts +10 -0
- package/dist/tui/browse-ui.js +432 -0
- package/dist/tui/browse.d.ts +17 -0
- package/dist/tui/browse.js +625 -0
- package/dist/tui/markdown.d.ts +22 -0
- package/dist/tui/markdown.js +223 -0
- package/package.json +71 -0
- package/plugins/claude-code/.claude-plugin/plugin.json +10 -0
- package/plugins/claude-code/.mcp.json +6 -0
- package/plugins/claude-code/skills/lore/SKILL.md +63 -0
- package/plugins/codex/SKILL.md +36 -0
- package/plugins/codex/agents/openai.yaml +10 -0
- package/plugins/gemini/GEMINI.md +31 -0
- package/plugins/gemini/gemini-extension.json +11 -0
- package/skills/generic-agent.md +99 -0
- 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
|
+
}
|