@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,1944 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event handlers and operations for the Lore Document Browser TUI
|
|
3
|
+
*
|
|
4
|
+
* Contains navigation, search, editor integration, and mode switching.
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
import { writeFileSync, unlinkSync } from 'fs';
|
|
8
|
+
import { tmpdir } from 'os';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { formatDate, markdownToBlessed, renderFullView, renderList, renderPreview, updateStatus, buildListItems, getSelectedSource, } from './browse-render.js';
|
|
11
|
+
import { getSourceById, searchSources, getProjectStats, getAllSources, deleteSource, updateSourceProjects, updateSourceTitle, updateSourceContentType } from '../core/vector-store.js';
|
|
12
|
+
import { generateEmbedding } from '../core/embedder.js';
|
|
13
|
+
import { searchLocalFiles } from '../core/local-search.js';
|
|
14
|
+
import { gitCommitAndPush, deleteFileAndCommit } from '../core/git.js';
|
|
15
|
+
/**
|
|
16
|
+
* Helper to re-render ask/research pane when returning from pickers
|
|
17
|
+
* Exported so browse.ts can use it for autocomplete direct selection
|
|
18
|
+
*/
|
|
19
|
+
export function renderReturnToAskOrResearch(state, ui, mode) {
|
|
20
|
+
const filters = [];
|
|
21
|
+
if (state.currentProject)
|
|
22
|
+
filters.push(`project: ${state.currentProject}`);
|
|
23
|
+
if (state.currentContentType)
|
|
24
|
+
filters.push(`type: ${state.currentContentType}`);
|
|
25
|
+
const filterInfo = filters.length > 0
|
|
26
|
+
? `{yellow-fg}Scope: ${filters.join(', ')}{/yellow-fg}`
|
|
27
|
+
: '{blue-fg}No filters{/blue-fg}';
|
|
28
|
+
const footerNote = filters.length > 0
|
|
29
|
+
? `{yellow-fg}${filters.join(', ')}{/yellow-fg}`
|
|
30
|
+
: '{blue-fg}all sources{/blue-fg}';
|
|
31
|
+
if (mode === 'ask') {
|
|
32
|
+
const lines = [
|
|
33
|
+
`${filterInfo} {blue-fg}│{/blue-fg} {white-fg}/help{/white-fg} for commands {blue-fg}│{/blue-fg} {white-fg}/new{/white-fg} to start fresh`,
|
|
34
|
+
'',
|
|
35
|
+
];
|
|
36
|
+
if (state.askHistory.length > 0) {
|
|
37
|
+
for (const msg of state.askHistory) {
|
|
38
|
+
if (msg.role === 'user') {
|
|
39
|
+
lines.push(`{cyan-fg}You:{/cyan-fg} ${msg.content}`);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
const escaped = msg.content.replace(/\{/g, '\\{').replace(/\}/g, '\\}');
|
|
43
|
+
lines.push(`{green-fg}Assistant:{/green-fg}`);
|
|
44
|
+
lines.push(escaped);
|
|
45
|
+
}
|
|
46
|
+
lines.push('');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
lines.push('{blue-fg}Ask a question about your knowledge base...{/blue-fg}');
|
|
51
|
+
}
|
|
52
|
+
ui.askPane.setContent(lines.join('\n'));
|
|
53
|
+
const historyNote = state.askHistory.length > 0 ? `${state.askHistory.length / 2} Q&A │ ` : '';
|
|
54
|
+
ui.footer.setContent(` ${historyNote}Enter: Send │ Esc: Back │ Scope: ${footerNote}`);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const lines = [
|
|
58
|
+
`${filterInfo} {blue-fg}│{/blue-fg} {white-fg}/help{/white-fg} for commands {blue-fg}│{/blue-fg} {white-fg}/new{/white-fg} to start fresh`,
|
|
59
|
+
'',
|
|
60
|
+
];
|
|
61
|
+
if (state.researchHistory.length > 0) {
|
|
62
|
+
for (const item of state.researchHistory) {
|
|
63
|
+
lines.push(`{cyan-fg}Research:{/cyan-fg} ${item.query}`);
|
|
64
|
+
lines.push('');
|
|
65
|
+
const escaped = item.summary.replace(/\{/g, '\\{').replace(/\}/g, '\\}');
|
|
66
|
+
lines.push(escaped);
|
|
67
|
+
lines.push('');
|
|
68
|
+
lines.push('{blue-fg}───────────────────────────────────────{/blue-fg}');
|
|
69
|
+
lines.push('');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
lines.push('{blue-fg}Enter a research task to begin comprehensive analysis...{/blue-fg}');
|
|
74
|
+
lines.push('');
|
|
75
|
+
lines.push('{blue-fg}The research agent will iteratively explore sources,{/blue-fg}');
|
|
76
|
+
lines.push('{blue-fg}cross-reference findings, and synthesize results.{/blue-fg}');
|
|
77
|
+
}
|
|
78
|
+
ui.askPane.setContent(lines.join('\n'));
|
|
79
|
+
const historyNote = state.researchHistory.length > 0 ? `${state.researchHistory.length} tasks │ ` : '';
|
|
80
|
+
ui.footer.setContent(` ${historyNote}Enter: Research │ Esc: Back │ Scope: ${footerNote}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Load full content for the selected document
|
|
85
|
+
*/
|
|
86
|
+
export async function loadFullContent(state, ui, dbPath, sourcesDir) {
|
|
87
|
+
if (state.filtered.length === 0)
|
|
88
|
+
return;
|
|
89
|
+
// Get selected source (handles both grouped and flat view)
|
|
90
|
+
const source = getSelectedSource(state);
|
|
91
|
+
if (!source)
|
|
92
|
+
return;
|
|
93
|
+
// Try to load from disk first
|
|
94
|
+
const contentPath = path.join(sourcesDir, source.id, 'content.md');
|
|
95
|
+
try {
|
|
96
|
+
const { readFile } = await import('fs/promises');
|
|
97
|
+
state.fullContent = await readFile(contentPath, 'utf-8');
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Fall back to database source details
|
|
101
|
+
const details = await getSourceById(dbPath, source.id);
|
|
102
|
+
if (details) {
|
|
103
|
+
state.fullContent = [
|
|
104
|
+
`# ${details.title}`,
|
|
105
|
+
'',
|
|
106
|
+
`**Type:** ${details.source_type} · ${details.content_type}`,
|
|
107
|
+
`**Date:** ${formatDate(details.created_at)}`,
|
|
108
|
+
`**Projects:** ${details.projects.join(', ') || '(none)'}`,
|
|
109
|
+
'',
|
|
110
|
+
'## Summary',
|
|
111
|
+
details.summary,
|
|
112
|
+
'',
|
|
113
|
+
].join('\n');
|
|
114
|
+
if (details.themes && details.themes.length > 0) {
|
|
115
|
+
state.fullContent += '## Themes\n';
|
|
116
|
+
for (const theme of details.themes) {
|
|
117
|
+
state.fullContent += `- **${theme.name}**`;
|
|
118
|
+
if (theme.summary)
|
|
119
|
+
state.fullContent += `: ${theme.summary}`;
|
|
120
|
+
state.fullContent += '\n';
|
|
121
|
+
}
|
|
122
|
+
state.fullContent += '\n';
|
|
123
|
+
}
|
|
124
|
+
if (details.quotes && details.quotes.length > 0) {
|
|
125
|
+
state.fullContent += '## Key Quotes\n';
|
|
126
|
+
for (const quote of details.quotes.slice(0, 10)) {
|
|
127
|
+
const speaker = quote.speaker === 'user' ? '[You]' : `[${quote.speaker_name || 'Participant'}]`;
|
|
128
|
+
state.fullContent += `> ${speaker} "${quote.text}"\n\n`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
state.fullContent = `Could not load content for ${source.title}`;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Store raw lines for searching
|
|
137
|
+
state.fullContentLinesRaw = state.fullContent.split('\n');
|
|
138
|
+
// Convert markdown to blessed tags (no ANSI codes)
|
|
139
|
+
const rendered = markdownToBlessed(state.fullContent);
|
|
140
|
+
state.fullContentLines = rendered.split('\n');
|
|
141
|
+
// Reset search state
|
|
142
|
+
state.docSearchPattern = '';
|
|
143
|
+
state.docSearchMatches = [];
|
|
144
|
+
state.docSearchCurrentIdx = 0;
|
|
145
|
+
state.scrollOffset = 0;
|
|
146
|
+
renderFullView(ui, state);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Enter full view mode
|
|
150
|
+
*/
|
|
151
|
+
export async function enterFullView(state, ui, dbPath, sourcesDir) {
|
|
152
|
+
state.mode = 'fullview';
|
|
153
|
+
ui.listPane.hide();
|
|
154
|
+
ui.previewPane.hide();
|
|
155
|
+
ui.fullViewPane.show();
|
|
156
|
+
// Show loading state
|
|
157
|
+
ui.fullViewContent.setContent('{blue-fg}Loading...{/blue-fg}');
|
|
158
|
+
ui.screen.render();
|
|
159
|
+
// Load content and render
|
|
160
|
+
await loadFullContent(state, ui, dbPath, sourcesDir);
|
|
161
|
+
ui.footer.setContent(' j/k Scroll │ / Search │ y Copy │ e Edit │ Esc Back');
|
|
162
|
+
ui.screen.render();
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Exit full view mode
|
|
166
|
+
*/
|
|
167
|
+
export function exitFullView(state, ui) {
|
|
168
|
+
state.mode = 'list';
|
|
169
|
+
ui.fullViewPane.hide();
|
|
170
|
+
ui.listPane.show();
|
|
171
|
+
ui.previewPane.show();
|
|
172
|
+
ui.footer.setContent(' j/k Nav │ / Search │ a Ask │ R Research │ p Proj │ c Type │ m Move │ i Edit │ Esc Quit │ ? Help');
|
|
173
|
+
ui.screen.render();
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Enter semantic/hybrid search mode
|
|
177
|
+
*/
|
|
178
|
+
export function enterSearch(state, ui) {
|
|
179
|
+
state.mode = 'search';
|
|
180
|
+
state.searchMode = 'hybrid';
|
|
181
|
+
ui.searchInput.show();
|
|
182
|
+
ui.searchInput.setValue('/');
|
|
183
|
+
ui.searchInput.focus();
|
|
184
|
+
ui.screen.render();
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Enter regex search mode
|
|
188
|
+
*/
|
|
189
|
+
export function enterRegexSearch(state, ui) {
|
|
190
|
+
state.mode = 'regex-search';
|
|
191
|
+
state.searchMode = 'regex';
|
|
192
|
+
ui.regexInput.show();
|
|
193
|
+
ui.regexInput.setValue(':');
|
|
194
|
+
ui.regexInput.focus();
|
|
195
|
+
ui.screen.render();
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Exit search mode
|
|
199
|
+
*/
|
|
200
|
+
export function exitSearch(state, ui) {
|
|
201
|
+
state.mode = 'list';
|
|
202
|
+
ui.searchInput.hide();
|
|
203
|
+
ui.regexInput.hide();
|
|
204
|
+
ui.docSearchInput.hide();
|
|
205
|
+
ui.listContent.focus();
|
|
206
|
+
ui.screen.render();
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Enter document search mode (within full view)
|
|
210
|
+
*/
|
|
211
|
+
export function enterDocSearch(state, ui) {
|
|
212
|
+
state.mode = 'doc-search';
|
|
213
|
+
ui.docSearchInput.show();
|
|
214
|
+
ui.docSearchInput.setValue('/');
|
|
215
|
+
ui.docSearchInput.focus();
|
|
216
|
+
ui.screen.render();
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Exit document search mode
|
|
220
|
+
*/
|
|
221
|
+
export function exitDocSearch(state, ui, clearPattern = false) {
|
|
222
|
+
state.mode = 'fullview';
|
|
223
|
+
ui.docSearchInput.hide();
|
|
224
|
+
if (clearPattern) {
|
|
225
|
+
state.docSearchPattern = '';
|
|
226
|
+
state.docSearchMatches = [];
|
|
227
|
+
state.docSearchCurrentIdx = 0;
|
|
228
|
+
}
|
|
229
|
+
renderFullView(ui, state);
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Apply document search pattern
|
|
233
|
+
*/
|
|
234
|
+
export function applyDocSearch(state, ui, pattern) {
|
|
235
|
+
state.docSearchPattern = pattern;
|
|
236
|
+
state.docSearchMatches = [];
|
|
237
|
+
state.docSearchCurrentIdx = 0;
|
|
238
|
+
if (!pattern) {
|
|
239
|
+
renderFullView(ui, state);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
const regex = new RegExp(pattern, 'gi');
|
|
244
|
+
// Find all lines that match
|
|
245
|
+
for (let i = 0; i < state.fullContentLinesRaw.length; i++) {
|
|
246
|
+
if (regex.test(state.fullContentLinesRaw[i])) {
|
|
247
|
+
state.docSearchMatches.push(i);
|
|
248
|
+
}
|
|
249
|
+
regex.lastIndex = 0; // Reset for next test
|
|
250
|
+
}
|
|
251
|
+
// Jump to first match if found
|
|
252
|
+
if (state.docSearchMatches.length > 0) {
|
|
253
|
+
scrollToMatch(state, ui, 0);
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
renderFullView(ui, state);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
// Invalid regex
|
|
261
|
+
state.docSearchMatches = [];
|
|
262
|
+
renderFullView(ui, state);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Scroll to a specific match
|
|
267
|
+
*/
|
|
268
|
+
export function scrollToMatch(state, ui, matchIdx) {
|
|
269
|
+
if (state.docSearchMatches.length === 0)
|
|
270
|
+
return;
|
|
271
|
+
state.docSearchCurrentIdx = matchIdx;
|
|
272
|
+
const matchLine = state.docSearchMatches[matchIdx];
|
|
273
|
+
const height = ui.fullViewContent.height - 1;
|
|
274
|
+
// Center the match on screen
|
|
275
|
+
state.scrollOffset = Math.max(0, matchLine - Math.floor(height / 2));
|
|
276
|
+
state.scrollOffset = Math.min(state.scrollOffset, Math.max(0, state.fullContentLines.length - height));
|
|
277
|
+
renderFullView(ui, state);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Go to next match
|
|
281
|
+
*/
|
|
282
|
+
export function nextMatch(state, ui) {
|
|
283
|
+
if (state.docSearchMatches.length === 0)
|
|
284
|
+
return;
|
|
285
|
+
const nextIdx = (state.docSearchCurrentIdx + 1) % state.docSearchMatches.length;
|
|
286
|
+
scrollToMatch(state, ui, nextIdx);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Go to previous match
|
|
290
|
+
*/
|
|
291
|
+
export function prevMatch(state, ui) {
|
|
292
|
+
if (state.docSearchMatches.length === 0)
|
|
293
|
+
return;
|
|
294
|
+
const prevIdx = (state.docSearchCurrentIdx - 1 + state.docSearchMatches.length) % state.docSearchMatches.length;
|
|
295
|
+
scrollToMatch(state, ui, prevIdx);
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Apply search filter
|
|
299
|
+
*/
|
|
300
|
+
export async function applyFilter(state, ui, query, filterMode, dbPath, dataDir, project, sourceType) {
|
|
301
|
+
state.searchQuery = query;
|
|
302
|
+
state.searchMode = filterMode;
|
|
303
|
+
if (!query) {
|
|
304
|
+
state.filtered = [...state.sources];
|
|
305
|
+
state.searchMode = 'hybrid'; // Reset mode when clearing
|
|
306
|
+
}
|
|
307
|
+
else if (filterMode === 'regex') {
|
|
308
|
+
// Use local regex search
|
|
309
|
+
ui.statusBar.setContent(` Regex search "${query}"...`);
|
|
310
|
+
ui.screen.render();
|
|
311
|
+
try {
|
|
312
|
+
const results = await searchLocalFiles(dataDir, query, {
|
|
313
|
+
maxTotalResults: 50,
|
|
314
|
+
maxMatchesPerFile: 5,
|
|
315
|
+
});
|
|
316
|
+
// Convert to SourceItem format, respecting project filter
|
|
317
|
+
const sourceIds = results.map(r => r.source_id);
|
|
318
|
+
state.filtered = state.sources
|
|
319
|
+
.filter(s => {
|
|
320
|
+
// Must match regex result
|
|
321
|
+
if (!sourceIds.includes(s.id))
|
|
322
|
+
return false;
|
|
323
|
+
// Must match project filter if set
|
|
324
|
+
if (project && !s.projects.includes(project))
|
|
325
|
+
return false;
|
|
326
|
+
return true;
|
|
327
|
+
})
|
|
328
|
+
.map(s => {
|
|
329
|
+
const matchResult = results.find(r => r.source_id === s.id);
|
|
330
|
+
return {
|
|
331
|
+
...s,
|
|
332
|
+
score: matchResult ? matchResult.matches.length / 10 : 0,
|
|
333
|
+
};
|
|
334
|
+
})
|
|
335
|
+
.sort((a, b) => (b.score || 0) - (a.score || 0));
|
|
336
|
+
if (state.filtered.length === 0) {
|
|
337
|
+
// No matches found
|
|
338
|
+
const projectNote = project ? ` in project "${project}"` : '';
|
|
339
|
+
ui.statusBar.setContent(` No regex matches for "${query}"${projectNote}`);
|
|
340
|
+
ui.screen.render();
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
ui.statusBar.setContent(` Regex error: ${error}`);
|
|
345
|
+
ui.screen.render();
|
|
346
|
+
state.filtered = [];
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
// Use hybrid/semantic search
|
|
351
|
+
ui.statusBar.setContent(` Searching "${query}"...`);
|
|
352
|
+
ui.screen.render();
|
|
353
|
+
try {
|
|
354
|
+
// Use hybrid search
|
|
355
|
+
const queryVector = await generateEmbedding(query);
|
|
356
|
+
const results = await searchSources(dbPath, queryVector, {
|
|
357
|
+
limit: 50,
|
|
358
|
+
project,
|
|
359
|
+
source_type: sourceType,
|
|
360
|
+
mode: filterMode,
|
|
361
|
+
queryText: query,
|
|
362
|
+
});
|
|
363
|
+
// Convert search results to SourceItem format, sorted by score
|
|
364
|
+
state.filtered = results
|
|
365
|
+
.sort((a, b) => b.score - a.score) // Highest score first
|
|
366
|
+
.map(r => ({
|
|
367
|
+
id: r.id,
|
|
368
|
+
title: r.title,
|
|
369
|
+
source_type: r.source_type,
|
|
370
|
+
content_type: r.content_type,
|
|
371
|
+
projects: r.projects,
|
|
372
|
+
created_at: r.created_at,
|
|
373
|
+
summary: r.summary,
|
|
374
|
+
score: r.score,
|
|
375
|
+
}));
|
|
376
|
+
}
|
|
377
|
+
catch (error) {
|
|
378
|
+
// Fall back to text filter on error
|
|
379
|
+
ui.statusBar.setContent(` Search error, using text filter...`);
|
|
380
|
+
ui.screen.render();
|
|
381
|
+
const lower = query.toLowerCase();
|
|
382
|
+
state.filtered = state.sources.filter(s => s.title.toLowerCase().includes(lower) ||
|
|
383
|
+
s.summary.toLowerCase().includes(lower) ||
|
|
384
|
+
s.projects.some(p => p.toLowerCase().includes(lower)) ||
|
|
385
|
+
s.content_type.toLowerCase().includes(lower));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// Apply content type filter if set
|
|
389
|
+
if (state.currentContentType) {
|
|
390
|
+
state.filtered = state.filtered.filter(s => s.content_type === state.currentContentType);
|
|
391
|
+
}
|
|
392
|
+
// Rebuild list items if in grouped mode
|
|
393
|
+
if (state.groupByProject) {
|
|
394
|
+
state.listItems = buildListItems(state);
|
|
395
|
+
}
|
|
396
|
+
state.selectedIndex = 0;
|
|
397
|
+
updateStatus(ui, state, project, sourceType);
|
|
398
|
+
renderList(ui, state);
|
|
399
|
+
renderPreview(ui, state);
|
|
400
|
+
ui.screen.render();
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Show help overlay
|
|
404
|
+
*/
|
|
405
|
+
export function showHelp(state, ui) {
|
|
406
|
+
state.mode = 'help';
|
|
407
|
+
ui.helpPane.show();
|
|
408
|
+
ui.screen.render();
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Hide help overlay
|
|
412
|
+
*/
|
|
413
|
+
export function hideHelp(state, ui) {
|
|
414
|
+
state.mode = 'list';
|
|
415
|
+
ui.helpPane.hide();
|
|
416
|
+
ui.screen.render();
|
|
417
|
+
}
|
|
418
|
+
// ============================================================================
|
|
419
|
+
// Tools View
|
|
420
|
+
// ============================================================================
|
|
421
|
+
export async function openInEditor(state, ui, sourcesDir) {
|
|
422
|
+
if (state.filtered.length === 0)
|
|
423
|
+
return;
|
|
424
|
+
// Get selected source (handles both grouped and flat view)
|
|
425
|
+
const source = getSelectedSource(state);
|
|
426
|
+
if (!source)
|
|
427
|
+
return;
|
|
428
|
+
const editorEnv = process.env.EDITOR || 'vi';
|
|
429
|
+
// Parse editor command (might include args like "code -w")
|
|
430
|
+
const editorParts = editorEnv.split(/\s+/);
|
|
431
|
+
const editor = editorParts[0];
|
|
432
|
+
const editorArgs = editorParts.slice(1);
|
|
433
|
+
// Get content
|
|
434
|
+
let content = state.fullContent;
|
|
435
|
+
if (!content) {
|
|
436
|
+
const contentPath = path.join(sourcesDir, source.id, 'content.md');
|
|
437
|
+
try {
|
|
438
|
+
const { readFile } = await import('fs/promises');
|
|
439
|
+
content = await readFile(contentPath, 'utf-8');
|
|
440
|
+
}
|
|
441
|
+
catch {
|
|
442
|
+
content = source.summary;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// Write to temp file
|
|
446
|
+
const tmpPath = path.join(tmpdir(), `lore-${source.id}.md`);
|
|
447
|
+
writeFileSync(tmpPath, content);
|
|
448
|
+
// Open editor in background (detached so TUI keeps running)
|
|
449
|
+
const child = spawn(editor, [...editorArgs, tmpPath], {
|
|
450
|
+
detached: true,
|
|
451
|
+
stdio: 'ignore',
|
|
452
|
+
});
|
|
453
|
+
child.unref(); // Don't wait for editor to exit
|
|
454
|
+
// Show confirmation
|
|
455
|
+
ui.statusBar.setContent(` Opened in ${editor}`);
|
|
456
|
+
ui.screen.render();
|
|
457
|
+
// Clean up temp file after a delay (give editor time to read it)
|
|
458
|
+
setTimeout(() => {
|
|
459
|
+
try {
|
|
460
|
+
unlinkSync(tmpPath);
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
// Ignore - file might still be in use
|
|
464
|
+
}
|
|
465
|
+
}, 5000);
|
|
466
|
+
}
|
|
467
|
+
// Navigation functions
|
|
468
|
+
export function moveDown(state, ui) {
|
|
469
|
+
if (state.mode === 'fullview') {
|
|
470
|
+
const maxScroll = Math.max(0, state.fullContentLines.length - (ui.fullViewContent.height - 1));
|
|
471
|
+
state.scrollOffset = Math.min(state.scrollOffset + 1, maxScroll);
|
|
472
|
+
renderFullView(ui, state);
|
|
473
|
+
}
|
|
474
|
+
else if (state.mode === 'list') {
|
|
475
|
+
const maxIndex = state.groupByProject
|
|
476
|
+
? state.listItems.length - 1
|
|
477
|
+
: state.filtered.length - 1;
|
|
478
|
+
if (state.selectedIndex < maxIndex) {
|
|
479
|
+
state.selectedIndex++;
|
|
480
|
+
renderList(ui, state);
|
|
481
|
+
renderPreview(ui, state);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
ui.screen.render();
|
|
485
|
+
}
|
|
486
|
+
export function moveUp(state, ui) {
|
|
487
|
+
if (state.mode === 'fullview') {
|
|
488
|
+
state.scrollOffset = Math.max(0, state.scrollOffset - 1);
|
|
489
|
+
renderFullView(ui, state);
|
|
490
|
+
}
|
|
491
|
+
else if (state.mode === 'list') {
|
|
492
|
+
if (state.selectedIndex > 0) {
|
|
493
|
+
state.selectedIndex--;
|
|
494
|
+
renderList(ui, state);
|
|
495
|
+
renderPreview(ui, state);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
ui.screen.render();
|
|
499
|
+
}
|
|
500
|
+
export function pageDown(state, ui) {
|
|
501
|
+
const pageSize = state.mode === 'fullview'
|
|
502
|
+
? ui.fullViewContent.height - 2
|
|
503
|
+
: ui.listContent.height / 3;
|
|
504
|
+
if (state.mode === 'fullview') {
|
|
505
|
+
const maxScroll = Math.max(0, state.fullContentLines.length - (ui.fullViewContent.height - 1));
|
|
506
|
+
state.scrollOffset = Math.min(state.scrollOffset + pageSize, maxScroll);
|
|
507
|
+
renderFullView(ui, state);
|
|
508
|
+
}
|
|
509
|
+
else if (state.mode === 'list') {
|
|
510
|
+
const maxIndex = state.groupByProject
|
|
511
|
+
? state.listItems.length - 1
|
|
512
|
+
: state.filtered.length - 1;
|
|
513
|
+
state.selectedIndex = Math.min(state.selectedIndex + Math.floor(pageSize), maxIndex);
|
|
514
|
+
renderList(ui, state);
|
|
515
|
+
renderPreview(ui, state);
|
|
516
|
+
}
|
|
517
|
+
ui.screen.render();
|
|
518
|
+
}
|
|
519
|
+
export function pageUp(state, ui) {
|
|
520
|
+
const pageSize = state.mode === 'fullview'
|
|
521
|
+
? ui.fullViewContent.height - 2
|
|
522
|
+
: ui.listContent.height / 3;
|
|
523
|
+
if (state.mode === 'fullview') {
|
|
524
|
+
state.scrollOffset = Math.max(0, state.scrollOffset - pageSize);
|
|
525
|
+
renderFullView(ui, state);
|
|
526
|
+
}
|
|
527
|
+
else if (state.mode === 'list') {
|
|
528
|
+
state.selectedIndex = Math.max(state.selectedIndex - Math.floor(pageSize), 0);
|
|
529
|
+
renderList(ui, state);
|
|
530
|
+
renderPreview(ui, state);
|
|
531
|
+
}
|
|
532
|
+
ui.screen.render();
|
|
533
|
+
}
|
|
534
|
+
export function jumpToEnd(state, ui) {
|
|
535
|
+
if (state.mode === 'fullview') {
|
|
536
|
+
state.scrollOffset = Math.max(0, state.fullContentLines.length - (ui.fullViewContent.height - 1));
|
|
537
|
+
renderFullView(ui, state);
|
|
538
|
+
}
|
|
539
|
+
else if (state.mode === 'list') {
|
|
540
|
+
const maxIndex = state.groupByProject
|
|
541
|
+
? state.listItems.length - 1
|
|
542
|
+
: state.filtered.length - 1;
|
|
543
|
+
state.selectedIndex = maxIndex;
|
|
544
|
+
renderList(ui, state);
|
|
545
|
+
renderPreview(ui, state);
|
|
546
|
+
}
|
|
547
|
+
ui.screen.render();
|
|
548
|
+
}
|
|
549
|
+
export function jumpToStart(state, ui) {
|
|
550
|
+
if (state.mode === 'fullview') {
|
|
551
|
+
state.scrollOffset = 0;
|
|
552
|
+
renderFullView(ui, state);
|
|
553
|
+
}
|
|
554
|
+
else if (state.mode === 'list') {
|
|
555
|
+
state.selectedIndex = 0;
|
|
556
|
+
renderList(ui, state);
|
|
557
|
+
renderPreview(ui, state);
|
|
558
|
+
}
|
|
559
|
+
ui.screen.render();
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Trigger a manual sync
|
|
563
|
+
*/
|
|
564
|
+
export async function triggerSync(state, ui, dbPath, dataDir, project, sourceType) {
|
|
565
|
+
ui.statusBar.setContent(' {yellow-fg}Syncing...{/yellow-fg}');
|
|
566
|
+
ui.screen.render();
|
|
567
|
+
try {
|
|
568
|
+
const { handleSync } = await import('../mcp/handlers/sync.js');
|
|
569
|
+
const { getAllSources } = await import('../core/vector-store.js');
|
|
570
|
+
const result = await handleSync(dbPath, dataDir, {
|
|
571
|
+
git_pull: true,
|
|
572
|
+
git_push: true,
|
|
573
|
+
}, { hookContext: { mode: 'cli' } });
|
|
574
|
+
const processed = result.processing?.processed || 0;
|
|
575
|
+
// Reload sources if anything was processed
|
|
576
|
+
if (processed > 0) {
|
|
577
|
+
state.sources = await getAllSources(dbPath, {
|
|
578
|
+
project,
|
|
579
|
+
source_type: sourceType,
|
|
580
|
+
limit: 100,
|
|
581
|
+
});
|
|
582
|
+
state.filtered = [...state.sources];
|
|
583
|
+
state.selectedIndex = 0;
|
|
584
|
+
renderList(ui, state);
|
|
585
|
+
renderPreview(ui, state);
|
|
586
|
+
}
|
|
587
|
+
updateStatus(ui, state, project, sourceType);
|
|
588
|
+
ui.statusBar.setContent(` {green-fg}Synced: ${processed} new file(s){/green-fg}`);
|
|
589
|
+
ui.screen.render();
|
|
590
|
+
// Restore normal status after a delay
|
|
591
|
+
setTimeout(() => {
|
|
592
|
+
updateStatus(ui, state, project, sourceType);
|
|
593
|
+
ui.screen.render();
|
|
594
|
+
}, 2000);
|
|
595
|
+
}
|
|
596
|
+
catch (error) {
|
|
597
|
+
ui.statusBar.setContent(` {red-fg}Sync failed: ${error}{/red-fg}`);
|
|
598
|
+
ui.screen.render();
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
// ============================================================================
|
|
602
|
+
// Project Picker
|
|
603
|
+
// ============================================================================
|
|
604
|
+
/**
|
|
605
|
+
* Load projects and show the project picker
|
|
606
|
+
*/
|
|
607
|
+
export async function showProjectPicker(state, ui, dbPath) {
|
|
608
|
+
ui.statusBar.setContent(' Loading projects...');
|
|
609
|
+
ui.screen.render();
|
|
610
|
+
try {
|
|
611
|
+
const stats = await getProjectStats(dbPath);
|
|
612
|
+
// Build projects list with special entries
|
|
613
|
+
const projects = [];
|
|
614
|
+
// Add "All Projects" option
|
|
615
|
+
projects.push({
|
|
616
|
+
name: '__all__',
|
|
617
|
+
count: state.sources.length,
|
|
618
|
+
latestActivity: new Date().toISOString(),
|
|
619
|
+
});
|
|
620
|
+
// Add "Unassigned" option (docs with empty projects array)
|
|
621
|
+
const unassignedCount = state.sources.filter(s => s.projects.length === 0).length;
|
|
622
|
+
if (unassignedCount > 0) {
|
|
623
|
+
projects.push({
|
|
624
|
+
name: '__unassigned__',
|
|
625
|
+
count: unassignedCount,
|
|
626
|
+
latestActivity: new Date().toISOString(),
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
// Add actual projects
|
|
630
|
+
for (const stat of stats) {
|
|
631
|
+
projects.push({
|
|
632
|
+
name: stat.project,
|
|
633
|
+
count: stat.source_count,
|
|
634
|
+
latestActivity: stat.latest_activity,
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
state.projects = projects;
|
|
638
|
+
state.projectPickerIndex = 0;
|
|
639
|
+
// Find current project in list
|
|
640
|
+
if (state.currentProject) {
|
|
641
|
+
const idx = projects.findIndex(p => p.name === state.currentProject);
|
|
642
|
+
if (idx >= 0)
|
|
643
|
+
state.projectPickerIndex = idx;
|
|
644
|
+
}
|
|
645
|
+
state.mode = 'project-picker';
|
|
646
|
+
renderProjectPicker(state, ui);
|
|
647
|
+
ui.projectPicker.show();
|
|
648
|
+
ui.screen.render();
|
|
649
|
+
}
|
|
650
|
+
catch (error) {
|
|
651
|
+
ui.statusBar.setContent(` {red-fg}Failed to load projects: ${error}{/red-fg}`);
|
|
652
|
+
ui.screen.render();
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Render the project picker content
|
|
657
|
+
*/
|
|
658
|
+
export function renderProjectPicker(state, ui) {
|
|
659
|
+
const lines = [];
|
|
660
|
+
lines.push('{bold}{yellow-fg}Select Project{/yellow-fg}{/bold}');
|
|
661
|
+
lines.push('');
|
|
662
|
+
for (let i = 0; i < state.projects.length; i++) {
|
|
663
|
+
const p = state.projects[i];
|
|
664
|
+
const isSelected = i === state.projectPickerIndex;
|
|
665
|
+
const prefix = isSelected ? '{inverse} > ' : ' ';
|
|
666
|
+
const suffix = isSelected ? ' {/inverse}' : '';
|
|
667
|
+
let displayName;
|
|
668
|
+
let extra = '';
|
|
669
|
+
if (p.name === '__all__') {
|
|
670
|
+
displayName = '{cyan-fg}[All Projects]{/cyan-fg}';
|
|
671
|
+
extra = ` (${p.count})`;
|
|
672
|
+
}
|
|
673
|
+
else if (p.name === '__unassigned__') {
|
|
674
|
+
displayName = '{magenta-fg}[Unassigned]{/magenta-fg}';
|
|
675
|
+
extra = ` (${p.count})`;
|
|
676
|
+
}
|
|
677
|
+
else {
|
|
678
|
+
displayName = p.name;
|
|
679
|
+
const ago = formatRelativeTime(p.latestActivity);
|
|
680
|
+
extra = ` (${p.count}, ${ago})`;
|
|
681
|
+
}
|
|
682
|
+
lines.push(`${prefix}${displayName}${extra}${suffix}`);
|
|
683
|
+
}
|
|
684
|
+
lines.push('');
|
|
685
|
+
lines.push('{blue-fg}j/k: navigate Enter: select Esc: cancel{/blue-fg}');
|
|
686
|
+
ui.projectPickerContent.setContent(lines.join('\n'));
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Format relative time for project display
|
|
690
|
+
*/
|
|
691
|
+
function formatRelativeTime(isoTime) {
|
|
692
|
+
const date = new Date(isoTime);
|
|
693
|
+
const now = new Date();
|
|
694
|
+
const diffMs = now.getTime() - date.getTime();
|
|
695
|
+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
696
|
+
if (diffDays === 0)
|
|
697
|
+
return 'today';
|
|
698
|
+
if (diffDays === 1)
|
|
699
|
+
return '1d ago';
|
|
700
|
+
if (diffDays < 7)
|
|
701
|
+
return `${diffDays}d ago`;
|
|
702
|
+
if (diffDays < 30)
|
|
703
|
+
return `${Math.floor(diffDays / 7)}w ago`;
|
|
704
|
+
if (diffDays < 365)
|
|
705
|
+
return `${Math.floor(diffDays / 30)}mo ago`;
|
|
706
|
+
return `${Math.floor(diffDays / 365)}y ago`;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Navigate project picker down
|
|
710
|
+
*/
|
|
711
|
+
export function projectPickerDown(state, ui) {
|
|
712
|
+
if (state.projectPickerIndex < state.projects.length - 1) {
|
|
713
|
+
state.projectPickerIndex++;
|
|
714
|
+
renderProjectPicker(state, ui);
|
|
715
|
+
ui.screen.render();
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Navigate project picker up
|
|
720
|
+
*/
|
|
721
|
+
export function projectPickerUp(state, ui) {
|
|
722
|
+
if (state.projectPickerIndex > 0) {
|
|
723
|
+
state.projectPickerIndex--;
|
|
724
|
+
renderProjectPicker(state, ui);
|
|
725
|
+
ui.screen.render();
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Select current project and filter documents
|
|
730
|
+
*/
|
|
731
|
+
export async function selectProject(state, ui, dbPath, dataDir, sourceType) {
|
|
732
|
+
const selected = state.projects[state.projectPickerIndex];
|
|
733
|
+
// Hide picker
|
|
734
|
+
ui.projectPicker.hide();
|
|
735
|
+
let newProject;
|
|
736
|
+
if (selected.name === '__all__') {
|
|
737
|
+
newProject = undefined;
|
|
738
|
+
}
|
|
739
|
+
else if (selected.name === '__unassigned__') {
|
|
740
|
+
newProject = '__unassigned__';
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
newProject = selected.name;
|
|
744
|
+
}
|
|
745
|
+
state.currentProject = newProject;
|
|
746
|
+
// Check if we should return to ask/research mode
|
|
747
|
+
if (state.pickerReturnMode === 'ask') {
|
|
748
|
+
state.mode = 'ask';
|
|
749
|
+
state.pickerReturnMode = undefined;
|
|
750
|
+
ui.listPane.hide();
|
|
751
|
+
ui.previewPane.hide();
|
|
752
|
+
ui.askInput.show();
|
|
753
|
+
ui.askPane.show();
|
|
754
|
+
ui.askPane.setLabel(' Ask Lore ');
|
|
755
|
+
renderReturnToAskOrResearch(state, ui, 'ask');
|
|
756
|
+
ui.askInput.focus();
|
|
757
|
+
ui.askInput.readInput();
|
|
758
|
+
ui.screen.render();
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
else if (state.pickerReturnMode === 'research') {
|
|
762
|
+
state.mode = 'research';
|
|
763
|
+
state.pickerReturnMode = undefined;
|
|
764
|
+
ui.listPane.hide();
|
|
765
|
+
ui.previewPane.hide();
|
|
766
|
+
ui.askInput.show();
|
|
767
|
+
ui.askPane.show();
|
|
768
|
+
ui.askPane.setLabel(' Research Agent ');
|
|
769
|
+
renderReturnToAskOrResearch(state, ui, 'research');
|
|
770
|
+
ui.askInput.focus();
|
|
771
|
+
ui.askInput.readInput();
|
|
772
|
+
ui.screen.render();
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
state.mode = 'list';
|
|
776
|
+
// Reload sources with new project filter
|
|
777
|
+
ui.statusBar.setContent(' Filtering...');
|
|
778
|
+
ui.screen.render();
|
|
779
|
+
try {
|
|
780
|
+
if (newProject === '__unassigned__') {
|
|
781
|
+
// Special case: filter for docs with no project
|
|
782
|
+
const allSources = await getAllSources(dbPath, {
|
|
783
|
+
source_type: sourceType,
|
|
784
|
+
limit: 100,
|
|
785
|
+
});
|
|
786
|
+
state.sources = allSources.filter(s => s.projects.length === 0);
|
|
787
|
+
}
|
|
788
|
+
else {
|
|
789
|
+
state.sources = await getAllSources(dbPath, {
|
|
790
|
+
project: newProject,
|
|
791
|
+
source_type: sourceType,
|
|
792
|
+
limit: 100,
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
state.filtered = [...state.sources];
|
|
796
|
+
state.selectedIndex = 0;
|
|
797
|
+
state.searchQuery = '';
|
|
798
|
+
updateStatus(ui, state, newProject, sourceType);
|
|
799
|
+
renderList(ui, state);
|
|
800
|
+
renderPreview(ui, state);
|
|
801
|
+
ui.screen.render();
|
|
802
|
+
}
|
|
803
|
+
catch (error) {
|
|
804
|
+
ui.statusBar.setContent(` {red-fg}Filter failed: ${error}{/red-fg}`);
|
|
805
|
+
ui.screen.render();
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
/**
|
|
809
|
+
* Cancel project picker
|
|
810
|
+
*/
|
|
811
|
+
export function cancelProjectPicker(state, ui) {
|
|
812
|
+
ui.projectPicker.hide();
|
|
813
|
+
// Check if we should return to ask/research mode
|
|
814
|
+
if (state.pickerReturnMode === 'ask') {
|
|
815
|
+
state.mode = 'ask';
|
|
816
|
+
state.pickerReturnMode = undefined;
|
|
817
|
+
ui.listPane.hide();
|
|
818
|
+
ui.previewPane.hide();
|
|
819
|
+
ui.askInput.show();
|
|
820
|
+
ui.askPane.show();
|
|
821
|
+
ui.askPane.setLabel(' Ask Lore ');
|
|
822
|
+
renderReturnToAskOrResearch(state, ui, 'ask');
|
|
823
|
+
ui.askInput.focus();
|
|
824
|
+
ui.askInput.readInput();
|
|
825
|
+
}
|
|
826
|
+
else if (state.pickerReturnMode === 'research') {
|
|
827
|
+
state.mode = 'research';
|
|
828
|
+
state.pickerReturnMode = undefined;
|
|
829
|
+
ui.listPane.hide();
|
|
830
|
+
ui.previewPane.hide();
|
|
831
|
+
ui.askInput.show();
|
|
832
|
+
ui.askPane.show();
|
|
833
|
+
ui.askPane.setLabel(' Research Agent ');
|
|
834
|
+
renderReturnToAskOrResearch(state, ui, 'research');
|
|
835
|
+
ui.askInput.focus();
|
|
836
|
+
ui.askInput.readInput();
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
state.mode = 'list';
|
|
840
|
+
}
|
|
841
|
+
ui.screen.render();
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Clear project filter (show all)
|
|
845
|
+
*/
|
|
846
|
+
export async function clearProjectFilter(state, ui, dbPath, dataDir, sourceType) {
|
|
847
|
+
state.currentProject = undefined;
|
|
848
|
+
ui.statusBar.setContent(' Loading all documents...');
|
|
849
|
+
ui.screen.render();
|
|
850
|
+
try {
|
|
851
|
+
state.sources = await getAllSources(dbPath, {
|
|
852
|
+
source_type: sourceType,
|
|
853
|
+
limit: 100,
|
|
854
|
+
});
|
|
855
|
+
state.filtered = [...state.sources];
|
|
856
|
+
state.selectedIndex = 0;
|
|
857
|
+
state.searchQuery = '';
|
|
858
|
+
updateStatus(ui, state, undefined, sourceType);
|
|
859
|
+
renderList(ui, state);
|
|
860
|
+
renderPreview(ui, state);
|
|
861
|
+
ui.screen.render();
|
|
862
|
+
}
|
|
863
|
+
catch (error) {
|
|
864
|
+
ui.statusBar.setContent(` {red-fg}Failed: ${error}{/red-fg}`);
|
|
865
|
+
ui.screen.render();
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
// ============================================================================
|
|
869
|
+
// Delete Document
|
|
870
|
+
// ============================================================================
|
|
871
|
+
/**
|
|
872
|
+
* Show delete confirmation dialog (for document or project)
|
|
873
|
+
*/
|
|
874
|
+
export function showDeleteConfirm(state, ui) {
|
|
875
|
+
if (state.filtered.length === 0)
|
|
876
|
+
return;
|
|
877
|
+
// Check if we're on a project header (project deletion)
|
|
878
|
+
if (state.groupByProject && state.listItems.length > 0) {
|
|
879
|
+
const item = state.listItems[state.selectedIndex];
|
|
880
|
+
if (item?.type === 'header') {
|
|
881
|
+
showProjectDeleteConfirm(state, ui, item);
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
// Get selected source (handles both grouped and flat view)
|
|
886
|
+
const source = getSelectedSource(state);
|
|
887
|
+
if (!source)
|
|
888
|
+
return;
|
|
889
|
+
const title = source.title.length > 40
|
|
890
|
+
? source.title.slice(0, 37) + '...'
|
|
891
|
+
: source.title;
|
|
892
|
+
state.mode = 'delete-confirm';
|
|
893
|
+
const lines = [
|
|
894
|
+
'',
|
|
895
|
+
'{bold}{red-fg}Delete Document?{/red-fg}{/bold}',
|
|
896
|
+
'',
|
|
897
|
+
` {bold}${title}{/bold}`,
|
|
898
|
+
'',
|
|
899
|
+
'{yellow-fg}This will delete from Supabase and local files.{/yellow-fg}',
|
|
900
|
+
'',
|
|
901
|
+
'{blue-fg} y: confirm delete n/Esc: cancel{/blue-fg}',
|
|
902
|
+
];
|
|
903
|
+
ui.deleteConfirm.setContent(lines.join('\n'));
|
|
904
|
+
ui.deleteConfirm.show();
|
|
905
|
+
ui.screen.render();
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Show delete confirmation for an entire project
|
|
909
|
+
*/
|
|
910
|
+
function showProjectDeleteConfirm(state, ui, header) {
|
|
911
|
+
state.mode = 'delete-confirm';
|
|
912
|
+
const lines = [
|
|
913
|
+
'',
|
|
914
|
+
'{bold}{red-fg}Delete Entire Project?{/red-fg}{/bold}',
|
|
915
|
+
'',
|
|
916
|
+
` {bold}{yellow-fg}${header.displayName}{/yellow-fg}{/bold}`,
|
|
917
|
+
'',
|
|
918
|
+
`{yellow-fg}This will delete ${header.documentCount} document${header.documentCount !== 1 ? 's' : ''}{/yellow-fg}`,
|
|
919
|
+
'{yellow-fg}from Supabase and local files.{/yellow-fg}',
|
|
920
|
+
'',
|
|
921
|
+
'{blue-fg} y: confirm delete n/Esc: cancel{/blue-fg}',
|
|
922
|
+
];
|
|
923
|
+
ui.deleteConfirm.setContent(lines.join('\n'));
|
|
924
|
+
ui.deleteConfirm.show();
|
|
925
|
+
ui.screen.render();
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Cancel delete operation
|
|
929
|
+
*/
|
|
930
|
+
export function cancelDelete(state, ui) {
|
|
931
|
+
state.mode = 'list';
|
|
932
|
+
ui.deleteConfirm.hide();
|
|
933
|
+
ui.screen.render();
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Confirm and execute delete operation (document or project)
|
|
937
|
+
*/
|
|
938
|
+
export async function confirmDelete(state, ui, dbPath, dataDir, project, sourceType) {
|
|
939
|
+
if (state.filtered.length === 0) {
|
|
940
|
+
cancelDelete(state, ui);
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
// Check if we're deleting a project
|
|
944
|
+
if (state.groupByProject && state.listItems.length > 0) {
|
|
945
|
+
const item = state.listItems[state.selectedIndex];
|
|
946
|
+
if (item?.type === 'header') {
|
|
947
|
+
await confirmProjectDelete(state, ui, dbPath, dataDir, item, project, sourceType);
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
// Get selected source (handles both grouped and flat view)
|
|
952
|
+
const source = getSelectedSource(state);
|
|
953
|
+
if (!source) {
|
|
954
|
+
cancelDelete(state, ui);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
// Hide dialog and show progress
|
|
958
|
+
ui.deleteConfirm.hide();
|
|
959
|
+
state.mode = 'list';
|
|
960
|
+
ui.statusBar.setContent(` {yellow-fg}Deleting "${source.title}"...{/yellow-fg}`);
|
|
961
|
+
ui.screen.render();
|
|
962
|
+
try {
|
|
963
|
+
// 1. Delete from Supabase (this also handles chunks cascade)
|
|
964
|
+
const { sourcePath: originalPath } = await deleteSource(dbPath, source.id);
|
|
965
|
+
// 2. Delete local files in data directory
|
|
966
|
+
const { rm } = await import('fs/promises');
|
|
967
|
+
const loreSourcePath = path.join(dataDir, 'sources', source.id);
|
|
968
|
+
try {
|
|
969
|
+
await rm(loreSourcePath, { recursive: true });
|
|
970
|
+
}
|
|
971
|
+
catch {
|
|
972
|
+
// File may not exist on disk - that's ok
|
|
973
|
+
}
|
|
974
|
+
// 3. Delete original source file from sync directory (and commit to its repo)
|
|
975
|
+
if (originalPath) {
|
|
976
|
+
await deleteFileAndCommit(originalPath, `Delete: ${source.title.slice(0, 50)}`);
|
|
977
|
+
}
|
|
978
|
+
// 4. Git commit and push the lore-data changes
|
|
979
|
+
await gitCommitAndPush(dataDir, `Delete source: ${source.title.slice(0, 50)}`);
|
|
980
|
+
// 4. Refresh the source list
|
|
981
|
+
state.sources = await getAllSources(dbPath, {
|
|
982
|
+
project: state.currentProject,
|
|
983
|
+
source_type: sourceType,
|
|
984
|
+
limit: 100,
|
|
985
|
+
});
|
|
986
|
+
state.filtered = [...state.sources];
|
|
987
|
+
// Rebuild list items if in grouped mode
|
|
988
|
+
if (state.groupByProject) {
|
|
989
|
+
state.listItems = buildListItems(state);
|
|
990
|
+
// Adjust selection if needed
|
|
991
|
+
if (state.selectedIndex >= state.listItems.length) {
|
|
992
|
+
state.selectedIndex = Math.max(0, state.listItems.length - 1);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
else {
|
|
996
|
+
// Adjust selection if needed
|
|
997
|
+
if (state.selectedIndex >= state.filtered.length) {
|
|
998
|
+
state.selectedIndex = Math.max(0, state.filtered.length - 1);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
// Update UI
|
|
1002
|
+
updateStatus(ui, state, state.currentProject, sourceType);
|
|
1003
|
+
renderList(ui, state);
|
|
1004
|
+
renderPreview(ui, state);
|
|
1005
|
+
ui.statusBar.setContent(` {green-fg}Deleted successfully{/green-fg}`);
|
|
1006
|
+
ui.screen.render();
|
|
1007
|
+
// Restore normal status after delay
|
|
1008
|
+
setTimeout(() => {
|
|
1009
|
+
updateStatus(ui, state, state.currentProject, sourceType);
|
|
1010
|
+
ui.screen.render();
|
|
1011
|
+
}, 2000);
|
|
1012
|
+
}
|
|
1013
|
+
catch (error) {
|
|
1014
|
+
ui.statusBar.setContent(` {red-fg}Delete failed: ${error}{/red-fg}`);
|
|
1015
|
+
ui.screen.render();
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Confirm and execute project deletion (all documents in a project)
|
|
1020
|
+
*/
|
|
1021
|
+
async function confirmProjectDelete(state, ui, dbPath, dataDir, header, project, sourceType) {
|
|
1022
|
+
// Hide dialog and show progress
|
|
1023
|
+
ui.deleteConfirm.hide();
|
|
1024
|
+
state.mode = 'list';
|
|
1025
|
+
ui.statusBar.setContent(` {yellow-fg}Deleting ${header.documentCount} documents from "${header.displayName}"...{/yellow-fg}`);
|
|
1026
|
+
ui.screen.render();
|
|
1027
|
+
try {
|
|
1028
|
+
// Get all documents in this project
|
|
1029
|
+
const docsToDelete = state.filtered.filter(s => {
|
|
1030
|
+
if (header.projectName === '__unassigned__') {
|
|
1031
|
+
return s.projects.length === 0;
|
|
1032
|
+
}
|
|
1033
|
+
return s.projects.includes(header.projectName);
|
|
1034
|
+
});
|
|
1035
|
+
let deleted = 0;
|
|
1036
|
+
const errors = [];
|
|
1037
|
+
const { rm } = await import('fs/promises');
|
|
1038
|
+
for (const source of docsToDelete) {
|
|
1039
|
+
try {
|
|
1040
|
+
// Delete from Supabase
|
|
1041
|
+
const { sourcePath: originalPath } = await deleteSource(dbPath, source.id);
|
|
1042
|
+
// Delete local files
|
|
1043
|
+
const loreSourcePath = path.join(dataDir, 'sources', source.id);
|
|
1044
|
+
try {
|
|
1045
|
+
await rm(loreSourcePath, { recursive: true });
|
|
1046
|
+
}
|
|
1047
|
+
catch {
|
|
1048
|
+
// File may not exist on disk
|
|
1049
|
+
}
|
|
1050
|
+
// Delete original source file from sync directory (and commit to its repo)
|
|
1051
|
+
if (originalPath) {
|
|
1052
|
+
await deleteFileAndCommit(originalPath, `Delete: ${source.title.slice(0, 50)}`);
|
|
1053
|
+
}
|
|
1054
|
+
deleted++;
|
|
1055
|
+
ui.statusBar.setContent(` {yellow-fg}Deleting... ${deleted}/${docsToDelete.length}{/yellow-fg}`);
|
|
1056
|
+
ui.screen.render();
|
|
1057
|
+
}
|
|
1058
|
+
catch (err) {
|
|
1059
|
+
errors.push(`${source.title}: ${err}`);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
// Git commit and push the deletions
|
|
1063
|
+
await gitCommitAndPush(dataDir, `Delete project: ${header.displayName} (${deleted} documents)`);
|
|
1064
|
+
// Remove from expanded set
|
|
1065
|
+
state.expandedProjects.delete(header.projectName);
|
|
1066
|
+
// Clear project filter - after deleting a project, show all remaining
|
|
1067
|
+
state.currentProject = undefined;
|
|
1068
|
+
// Refresh the source list (no project filter - show all remaining)
|
|
1069
|
+
state.sources = await getAllSources(dbPath, {
|
|
1070
|
+
source_type: sourceType,
|
|
1071
|
+
limit: 100,
|
|
1072
|
+
});
|
|
1073
|
+
state.filtered = [...state.sources];
|
|
1074
|
+
// Rebuild list items
|
|
1075
|
+
if (state.groupByProject) {
|
|
1076
|
+
state.listItems = buildListItems(state);
|
|
1077
|
+
if (state.selectedIndex >= state.listItems.length) {
|
|
1078
|
+
state.selectedIndex = Math.max(0, state.listItems.length - 1);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
else {
|
|
1082
|
+
if (state.selectedIndex >= state.filtered.length) {
|
|
1083
|
+
state.selectedIndex = Math.max(0, state.filtered.length - 1);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
// Update UI
|
|
1087
|
+
updateStatus(ui, state, state.currentProject, sourceType);
|
|
1088
|
+
renderList(ui, state);
|
|
1089
|
+
renderPreview(ui, state);
|
|
1090
|
+
if (errors.length > 0) {
|
|
1091
|
+
ui.statusBar.setContent(` {yellow-fg}Deleted ${deleted} documents, ${errors.length} failed{/yellow-fg}`);
|
|
1092
|
+
}
|
|
1093
|
+
else {
|
|
1094
|
+
ui.statusBar.setContent(` {green-fg}Deleted ${deleted} documents{/green-fg}`);
|
|
1095
|
+
}
|
|
1096
|
+
ui.screen.render();
|
|
1097
|
+
// Restore normal status after delay
|
|
1098
|
+
setTimeout(() => {
|
|
1099
|
+
updateStatus(ui, state, state.currentProject, sourceType);
|
|
1100
|
+
ui.screen.render();
|
|
1101
|
+
}, 2000);
|
|
1102
|
+
}
|
|
1103
|
+
catch (error) {
|
|
1104
|
+
ui.statusBar.setContent(` {red-fg}Delete failed: ${error}{/red-fg}`);
|
|
1105
|
+
ui.screen.render();
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
// ============================================================================
|
|
1109
|
+
// Clipboard Copy
|
|
1110
|
+
// ============================================================================
|
|
1111
|
+
/**
|
|
1112
|
+
* Copy text to system clipboard
|
|
1113
|
+
*/
|
|
1114
|
+
function copyToClipboard(text) {
|
|
1115
|
+
return new Promise((resolve, reject) => {
|
|
1116
|
+
const platform = process.platform;
|
|
1117
|
+
let cmd;
|
|
1118
|
+
let args;
|
|
1119
|
+
if (platform === 'darwin') {
|
|
1120
|
+
cmd = 'pbcopy';
|
|
1121
|
+
args = [];
|
|
1122
|
+
}
|
|
1123
|
+
else if (platform === 'linux') {
|
|
1124
|
+
// Try xclip first, fall back to xsel
|
|
1125
|
+
cmd = 'xclip';
|
|
1126
|
+
args = ['-selection', 'clipboard'];
|
|
1127
|
+
}
|
|
1128
|
+
else if (platform === 'win32') {
|
|
1129
|
+
cmd = 'clip';
|
|
1130
|
+
args = [];
|
|
1131
|
+
}
|
|
1132
|
+
else {
|
|
1133
|
+
reject(new Error(`Unsupported platform: ${platform}`));
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
const proc = spawn(cmd, args, { stdio: ['pipe', 'ignore', 'ignore'] });
|
|
1137
|
+
proc.stdin?.write(text);
|
|
1138
|
+
proc.stdin?.end();
|
|
1139
|
+
proc.on('close', (code) => {
|
|
1140
|
+
if (code === 0) {
|
|
1141
|
+
resolve();
|
|
1142
|
+
}
|
|
1143
|
+
else {
|
|
1144
|
+
reject(new Error(`Clipboard command failed with code ${code}`));
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
proc.on('error', (err) => {
|
|
1148
|
+
reject(err);
|
|
1149
|
+
});
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Copy current view content to clipboard
|
|
1154
|
+
*/
|
|
1155
|
+
export async function copyCurrentContent(state, ui) {
|
|
1156
|
+
let content = null;
|
|
1157
|
+
let description = '';
|
|
1158
|
+
if (state.mode === 'fullview') {
|
|
1159
|
+
content = state.fullContent;
|
|
1160
|
+
description = 'Document';
|
|
1161
|
+
}
|
|
1162
|
+
else if (state.mode === 'ask' && state.askResponse) {
|
|
1163
|
+
content = state.askResponse;
|
|
1164
|
+
description = 'Response';
|
|
1165
|
+
}
|
|
1166
|
+
else if (state.mode === 'research' && state.researchResponse) {
|
|
1167
|
+
content = state.researchResponse;
|
|
1168
|
+
description = 'Research result';
|
|
1169
|
+
}
|
|
1170
|
+
if (!content) {
|
|
1171
|
+
ui.statusBar.setContent(' {yellow-fg}Nothing to copy{/yellow-fg}');
|
|
1172
|
+
ui.screen.render();
|
|
1173
|
+
setTimeout(() => {
|
|
1174
|
+
updateStatus(ui, state, state.currentProject);
|
|
1175
|
+
ui.screen.render();
|
|
1176
|
+
}, 1500);
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
try {
|
|
1180
|
+
await copyToClipboard(content);
|
|
1181
|
+
ui.statusBar.setContent(` {green-fg}${description} copied to clipboard{/green-fg}`);
|
|
1182
|
+
ui.screen.render();
|
|
1183
|
+
setTimeout(() => {
|
|
1184
|
+
updateStatus(ui, state, state.currentProject);
|
|
1185
|
+
ui.screen.render();
|
|
1186
|
+
}, 1500);
|
|
1187
|
+
}
|
|
1188
|
+
catch (error) {
|
|
1189
|
+
ui.statusBar.setContent(` {red-fg}Copy failed: ${error}{/red-fg}`);
|
|
1190
|
+
ui.screen.render();
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
// ============================================================================
|
|
1194
|
+
// Move Document to Project
|
|
1195
|
+
// ============================================================================
|
|
1196
|
+
/**
|
|
1197
|
+
* Show move picker to relocate a document to a different project
|
|
1198
|
+
*/
|
|
1199
|
+
export async function showMovePicker(state, ui, dbPath) {
|
|
1200
|
+
// Get selected source
|
|
1201
|
+
const source = getSelectedSource(state);
|
|
1202
|
+
if (!source) {
|
|
1203
|
+
ui.statusBar.setContent(' {yellow-fg}No document selected{/yellow-fg}');
|
|
1204
|
+
ui.screen.render();
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
state.moveTargetSource = source;
|
|
1208
|
+
ui.statusBar.setContent(' Loading projects...');
|
|
1209
|
+
ui.screen.render();
|
|
1210
|
+
try {
|
|
1211
|
+
const stats = await getProjectStats(dbPath);
|
|
1212
|
+
// Build projects list (no "All Projects" option - doesn't make sense for move)
|
|
1213
|
+
const projects = [];
|
|
1214
|
+
// Add "New Project..." option at top
|
|
1215
|
+
projects.push({
|
|
1216
|
+
name: '__new__',
|
|
1217
|
+
count: 0,
|
|
1218
|
+
latestActivity: new Date().toISOString(),
|
|
1219
|
+
});
|
|
1220
|
+
// Add actual projects
|
|
1221
|
+
for (const stat of stats) {
|
|
1222
|
+
projects.push({
|
|
1223
|
+
name: stat.project,
|
|
1224
|
+
count: stat.source_count,
|
|
1225
|
+
latestActivity: stat.latest_activity,
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
state.movePickerProjects = projects;
|
|
1229
|
+
state.movePickerIndex = 0;
|
|
1230
|
+
// Find current project in list (skip if doc has no project)
|
|
1231
|
+
const currentProj = source.projects[0];
|
|
1232
|
+
if (currentProj) {
|
|
1233
|
+
const idx = projects.findIndex(p => p.name === currentProj);
|
|
1234
|
+
if (idx >= 0)
|
|
1235
|
+
state.movePickerIndex = idx;
|
|
1236
|
+
}
|
|
1237
|
+
state.mode = 'move-picker';
|
|
1238
|
+
renderMovePicker(state, ui);
|
|
1239
|
+
ui.projectPicker.show();
|
|
1240
|
+
ui.screen.render();
|
|
1241
|
+
}
|
|
1242
|
+
catch (error) {
|
|
1243
|
+
ui.statusBar.setContent(` {red-fg}Failed to load projects: ${error}{/red-fg}`);
|
|
1244
|
+
ui.screen.render();
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
/**
|
|
1248
|
+
* Render the move picker UI
|
|
1249
|
+
*/
|
|
1250
|
+
export function renderMovePicker(state, ui) {
|
|
1251
|
+
const source = state.moveTargetSource;
|
|
1252
|
+
const currentProject = source?.projects[0] || '(none)';
|
|
1253
|
+
const lines = [];
|
|
1254
|
+
lines.push('{bold}{yellow-fg}Move Document to Project{/yellow-fg}{/bold}');
|
|
1255
|
+
lines.push(`{gray-fg}Current: ${currentProject}{/gray-fg}`);
|
|
1256
|
+
lines.push('');
|
|
1257
|
+
for (let i = 0; i < state.movePickerProjects.length; i++) {
|
|
1258
|
+
const p = state.movePickerProjects[i];
|
|
1259
|
+
const isSelected = i === state.movePickerIndex;
|
|
1260
|
+
const isCurrent = p.name === source?.projects[0];
|
|
1261
|
+
const prefix = isSelected ? '{inverse} > ' : ' ';
|
|
1262
|
+
const suffix = isSelected ? ' {/inverse}' : '';
|
|
1263
|
+
let displayName;
|
|
1264
|
+
let extra = '';
|
|
1265
|
+
if (p.name === '__new__') {
|
|
1266
|
+
displayName = '{cyan-fg}[New Project...]{/cyan-fg}';
|
|
1267
|
+
}
|
|
1268
|
+
else {
|
|
1269
|
+
displayName = p.name;
|
|
1270
|
+
const ago = formatRelativeTime(p.latestActivity);
|
|
1271
|
+
extra = ` (${p.count}, ${ago})`;
|
|
1272
|
+
if (isCurrent) {
|
|
1273
|
+
extra += ' {magenta-fg}(current){/magenta-fg}';
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
lines.push(`${prefix}${displayName}${extra}${suffix}`);
|
|
1277
|
+
}
|
|
1278
|
+
lines.push('');
|
|
1279
|
+
lines.push('{blue-fg}j/k: navigate Enter: move Esc: cancel{/blue-fg}');
|
|
1280
|
+
ui.projectPickerContent.setContent(lines.join('\n'));
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Navigate down in move picker
|
|
1284
|
+
*/
|
|
1285
|
+
export function movePickerDown(state, ui) {
|
|
1286
|
+
if (state.movePickerIndex < state.movePickerProjects.length - 1) {
|
|
1287
|
+
state.movePickerIndex++;
|
|
1288
|
+
renderMovePicker(state, ui);
|
|
1289
|
+
ui.screen.render();
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Navigate up in move picker
|
|
1294
|
+
*/
|
|
1295
|
+
export function movePickerUp(state, ui) {
|
|
1296
|
+
if (state.movePickerIndex > 0) {
|
|
1297
|
+
state.movePickerIndex--;
|
|
1298
|
+
renderMovePicker(state, ui);
|
|
1299
|
+
ui.screen.render();
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Confirm move to selected project
|
|
1304
|
+
*/
|
|
1305
|
+
export async function confirmMove(state, ui, dbPath, dataDir, sourceType) {
|
|
1306
|
+
const source = state.moveTargetSource;
|
|
1307
|
+
if (!source) {
|
|
1308
|
+
cancelMovePicker(state, ui);
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
const selected = state.movePickerProjects[state.movePickerIndex];
|
|
1312
|
+
// Handle "New Project..." option
|
|
1313
|
+
if (selected.name === '__new__') {
|
|
1314
|
+
// Show input for new project name
|
|
1315
|
+
ui.projectPicker.hide();
|
|
1316
|
+
state.mode = 'list';
|
|
1317
|
+
// For now, show a message that they need to type a project name
|
|
1318
|
+
// In the future, we could add a text input modal
|
|
1319
|
+
ui.statusBar.setContent(' {yellow-fg}New project creation coming soon. Use edit (i) to set project name.{/yellow-fg}');
|
|
1320
|
+
ui.screen.render();
|
|
1321
|
+
setTimeout(() => {
|
|
1322
|
+
updateStatus(ui, state, state.currentProject, sourceType);
|
|
1323
|
+
ui.screen.render();
|
|
1324
|
+
}, 2000);
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
// Don't move if same project
|
|
1328
|
+
if (source.projects.includes(selected.name)) {
|
|
1329
|
+
ui.projectPicker.hide();
|
|
1330
|
+
state.mode = 'list';
|
|
1331
|
+
ui.statusBar.setContent(' {yellow-fg}Document already in this project{/yellow-fg}');
|
|
1332
|
+
ui.screen.render();
|
|
1333
|
+
setTimeout(() => {
|
|
1334
|
+
updateStatus(ui, state, state.currentProject, sourceType);
|
|
1335
|
+
ui.screen.render();
|
|
1336
|
+
}, 1500);
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
ui.projectPicker.hide();
|
|
1340
|
+
ui.statusBar.setContent(` Moving to "${selected.name}"...`);
|
|
1341
|
+
ui.screen.render();
|
|
1342
|
+
try {
|
|
1343
|
+
// Update the source's projects array (replace, not add)
|
|
1344
|
+
const success = await updateSourceProjects(dbPath, source.id, [selected.name]);
|
|
1345
|
+
if (!success) {
|
|
1346
|
+
throw new Error('Failed to update source');
|
|
1347
|
+
}
|
|
1348
|
+
// Update the local source object
|
|
1349
|
+
source.projects = [selected.name];
|
|
1350
|
+
// Rebuild list if in grouped mode
|
|
1351
|
+
if (state.groupByProject) {
|
|
1352
|
+
state.listItems = buildListItems(state);
|
|
1353
|
+
}
|
|
1354
|
+
state.mode = 'list';
|
|
1355
|
+
renderList(ui, state);
|
|
1356
|
+
renderPreview(ui, state);
|
|
1357
|
+
ui.statusBar.setContent(` {green-fg}Moved to "${selected.name}"{/green-fg}`);
|
|
1358
|
+
ui.screen.render();
|
|
1359
|
+
setTimeout(() => {
|
|
1360
|
+
updateStatus(ui, state, state.currentProject, sourceType);
|
|
1361
|
+
ui.screen.render();
|
|
1362
|
+
}, 1500);
|
|
1363
|
+
}
|
|
1364
|
+
catch (error) {
|
|
1365
|
+
state.mode = 'list';
|
|
1366
|
+
ui.statusBar.setContent(` {red-fg}Move failed: ${error}{/red-fg}`);
|
|
1367
|
+
ui.screen.render();
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
/**
|
|
1371
|
+
* Cancel move picker
|
|
1372
|
+
*/
|
|
1373
|
+
export function cancelMovePicker(state, ui) {
|
|
1374
|
+
ui.projectPicker.hide();
|
|
1375
|
+
state.mode = 'list';
|
|
1376
|
+
state.moveTargetSource = undefined;
|
|
1377
|
+
ui.screen.render();
|
|
1378
|
+
}
|
|
1379
|
+
// ============================================================================
|
|
1380
|
+
// Edit Document Info
|
|
1381
|
+
// ============================================================================
|
|
1382
|
+
/**
|
|
1383
|
+
* Enter edit info mode for the selected document
|
|
1384
|
+
*/
|
|
1385
|
+
export function enterEditInfo(state, ui) {
|
|
1386
|
+
const source = getSelectedSource(state);
|
|
1387
|
+
if (!source) {
|
|
1388
|
+
ui.statusBar.setContent(' {yellow-fg}No document selected{/yellow-fg}');
|
|
1389
|
+
ui.screen.render();
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
state.editSource = source;
|
|
1393
|
+
state.editTitle = source.title;
|
|
1394
|
+
state.editProjects = [...source.projects];
|
|
1395
|
+
state.editFieldIndex = 0;
|
|
1396
|
+
state.mode = 'edit-info';
|
|
1397
|
+
// Hide list/preview, show edit pane
|
|
1398
|
+
ui.listPane.hide();
|
|
1399
|
+
ui.previewPane.hide();
|
|
1400
|
+
// Show the input with current title - update label on input box
|
|
1401
|
+
ui.askInput.setLabel(' Edit Title ');
|
|
1402
|
+
ui.askInput.setValue(source.title);
|
|
1403
|
+
ui.askInput.show();
|
|
1404
|
+
// Show info pane with instructions
|
|
1405
|
+
ui.askPane.setLabel(' Document Info ');
|
|
1406
|
+
ui.askPane.setContent('{cyan-fg}Edit the title above and press Enter to save{/cyan-fg}\n\n{gray-fg}Press Esc to cancel{/gray-fg}');
|
|
1407
|
+
ui.askPane.show();
|
|
1408
|
+
ui.footer.setContent(' Enter: Save │ Esc: Cancel');
|
|
1409
|
+
ui.askInput.focus();
|
|
1410
|
+
ui.askInput.readInput();
|
|
1411
|
+
ui.screen.render();
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Save edit info changes
|
|
1415
|
+
*/
|
|
1416
|
+
export async function saveEditInfo(state, ui, dbPath, sourceType) {
|
|
1417
|
+
const source = state.editSource;
|
|
1418
|
+
if (!source) {
|
|
1419
|
+
exitEditInfo(state, ui);
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
ui.askPane.setContent('{yellow-fg}Saving...{/yellow-fg}');
|
|
1423
|
+
ui.screen.render();
|
|
1424
|
+
try {
|
|
1425
|
+
let updated = false;
|
|
1426
|
+
// Update title if changed
|
|
1427
|
+
if (state.editTitle !== source.title && state.editTitle.trim()) {
|
|
1428
|
+
const success = await updateSourceTitle(dbPath, source.id, state.editTitle.trim());
|
|
1429
|
+
if (success) {
|
|
1430
|
+
source.title = state.editTitle.trim();
|
|
1431
|
+
updated = true;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
// Update projects if changed
|
|
1435
|
+
const newProjects = state.editProjects.filter(p => p.trim());
|
|
1436
|
+
const projectsChanged = JSON.stringify(newProjects) !== JSON.stringify(source.projects);
|
|
1437
|
+
if (projectsChanged) {
|
|
1438
|
+
const success = await updateSourceProjects(dbPath, source.id, newProjects);
|
|
1439
|
+
if (success) {
|
|
1440
|
+
source.projects = newProjects;
|
|
1441
|
+
updated = true;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
exitEditInfo(state, ui);
|
|
1445
|
+
// Rebuild list if in grouped mode
|
|
1446
|
+
if (state.groupByProject) {
|
|
1447
|
+
state.listItems = buildListItems(state);
|
|
1448
|
+
}
|
|
1449
|
+
renderList(ui, state);
|
|
1450
|
+
renderPreview(ui, state);
|
|
1451
|
+
if (updated) {
|
|
1452
|
+
ui.statusBar.setContent(' {green-fg}Document updated{/green-fg}');
|
|
1453
|
+
}
|
|
1454
|
+
else {
|
|
1455
|
+
ui.statusBar.setContent(' {gray-fg}No changes{/gray-fg}');
|
|
1456
|
+
}
|
|
1457
|
+
ui.screen.render();
|
|
1458
|
+
setTimeout(() => {
|
|
1459
|
+
updateStatus(ui, state, state.currentProject, sourceType);
|
|
1460
|
+
ui.screen.render();
|
|
1461
|
+
}, 1500);
|
|
1462
|
+
}
|
|
1463
|
+
catch (error) {
|
|
1464
|
+
exitEditInfo(state, ui);
|
|
1465
|
+
ui.statusBar.setContent(` {red-fg}Save failed: ${error}{/red-fg}`);
|
|
1466
|
+
ui.screen.render();
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* Exit edit info mode without saving
|
|
1471
|
+
*/
|
|
1472
|
+
export function exitEditInfo(state, ui) {
|
|
1473
|
+
state.mode = 'list';
|
|
1474
|
+
state.editSource = undefined;
|
|
1475
|
+
state.editTitle = '';
|
|
1476
|
+
state.editProjects = [];
|
|
1477
|
+
state.editFieldIndex = 0;
|
|
1478
|
+
// Hide and reset ask components
|
|
1479
|
+
ui.askInput.hide();
|
|
1480
|
+
ui.askInput.setValue('');
|
|
1481
|
+
ui.askInput.setLabel(' Ask Lore ');
|
|
1482
|
+
ui.askPane.hide();
|
|
1483
|
+
ui.askPane.setLabel(' Response ');
|
|
1484
|
+
ui.listPane.show();
|
|
1485
|
+
ui.previewPane.show();
|
|
1486
|
+
ui.footer.setContent(' j/k Nav │ / Search │ a Ask │ R Research │ p Proj │ c Type │ m Move │ i Edit │ Esc Quit │ ? Help');
|
|
1487
|
+
ui.listContent.focus();
|
|
1488
|
+
ui.screen.render();
|
|
1489
|
+
}
|
|
1490
|
+
// ============================================================================
|
|
1491
|
+
// Edit Content Type (Type Picker)
|
|
1492
|
+
// ============================================================================
|
|
1493
|
+
// Valid content types
|
|
1494
|
+
const CONTENT_TYPES = [
|
|
1495
|
+
'interview',
|
|
1496
|
+
'meeting',
|
|
1497
|
+
'conversation',
|
|
1498
|
+
'document',
|
|
1499
|
+
'note',
|
|
1500
|
+
'analysis',
|
|
1501
|
+
];
|
|
1502
|
+
/**
|
|
1503
|
+
* Show type picker to change document content type
|
|
1504
|
+
*/
|
|
1505
|
+
export function showTypePicker(state, ui) {
|
|
1506
|
+
const source = getSelectedSource(state);
|
|
1507
|
+
if (!source) {
|
|
1508
|
+
ui.statusBar.setContent(' {yellow-fg}No document selected{/yellow-fg}');
|
|
1509
|
+
ui.screen.render();
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
state.typePickerSource = source;
|
|
1513
|
+
state.typePickerIndex = 0;
|
|
1514
|
+
// Find current type in list
|
|
1515
|
+
const currentIdx = CONTENT_TYPES.indexOf(source.content_type);
|
|
1516
|
+
if (currentIdx >= 0) {
|
|
1517
|
+
state.typePickerIndex = currentIdx;
|
|
1518
|
+
}
|
|
1519
|
+
state.mode = 'type-picker';
|
|
1520
|
+
renderTypePicker(state, ui);
|
|
1521
|
+
ui.projectPicker.show();
|
|
1522
|
+
ui.screen.render();
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* Render the type picker UI
|
|
1526
|
+
*/
|
|
1527
|
+
export function renderTypePicker(state, ui) {
|
|
1528
|
+
const source = state.typePickerSource;
|
|
1529
|
+
const currentType = source?.content_type || '(unknown)';
|
|
1530
|
+
const lines = [];
|
|
1531
|
+
lines.push('{bold}{yellow-fg}Change Content Type{/yellow-fg}{/bold}');
|
|
1532
|
+
lines.push(`{gray-fg}Current: ${currentType}{/gray-fg}`);
|
|
1533
|
+
lines.push('');
|
|
1534
|
+
for (let i = 0; i < CONTENT_TYPES.length; i++) {
|
|
1535
|
+
const type = CONTENT_TYPES[i];
|
|
1536
|
+
const isSelected = i === state.typePickerIndex;
|
|
1537
|
+
const isCurrent = type === source?.content_type;
|
|
1538
|
+
const prefix = isSelected ? '{inverse} > ' : ' ';
|
|
1539
|
+
const suffix = isSelected ? ' {/inverse}' : '';
|
|
1540
|
+
let displayName = type;
|
|
1541
|
+
let extra = '';
|
|
1542
|
+
if (isCurrent) {
|
|
1543
|
+
extra = ' {magenta-fg}(current){/magenta-fg}';
|
|
1544
|
+
}
|
|
1545
|
+
lines.push(`${prefix}${displayName}${extra}${suffix}`);
|
|
1546
|
+
}
|
|
1547
|
+
lines.push('');
|
|
1548
|
+
lines.push('{blue-fg}j/k: navigate Enter: select Esc: cancel{/blue-fg}');
|
|
1549
|
+
ui.projectPickerContent.setContent(lines.join('\n'));
|
|
1550
|
+
}
|
|
1551
|
+
/**
|
|
1552
|
+
* Navigate down in type picker
|
|
1553
|
+
*/
|
|
1554
|
+
export function typePickerDown(state, ui) {
|
|
1555
|
+
if (state.typePickerIndex < CONTENT_TYPES.length - 1) {
|
|
1556
|
+
state.typePickerIndex++;
|
|
1557
|
+
renderTypePicker(state, ui);
|
|
1558
|
+
ui.screen.render();
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* Navigate up in type picker
|
|
1563
|
+
*/
|
|
1564
|
+
export function typePickerUp(state, ui) {
|
|
1565
|
+
if (state.typePickerIndex > 0) {
|
|
1566
|
+
state.typePickerIndex--;
|
|
1567
|
+
renderTypePicker(state, ui);
|
|
1568
|
+
ui.screen.render();
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
/**
|
|
1572
|
+
* Confirm type selection
|
|
1573
|
+
*/
|
|
1574
|
+
export async function confirmTypeChange(state, ui, dbPath, sourceType) {
|
|
1575
|
+
const source = state.typePickerSource;
|
|
1576
|
+
if (!source) {
|
|
1577
|
+
cancelTypePicker(state, ui);
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
const selectedType = CONTENT_TYPES[state.typePickerIndex];
|
|
1581
|
+
// Don't update if same type
|
|
1582
|
+
if (selectedType === source.content_type) {
|
|
1583
|
+
ui.projectPicker.hide();
|
|
1584
|
+
state.mode = 'list';
|
|
1585
|
+
ui.statusBar.setContent(' {yellow-fg}No change{/yellow-fg}');
|
|
1586
|
+
ui.screen.render();
|
|
1587
|
+
setTimeout(() => {
|
|
1588
|
+
updateStatus(ui, state, state.currentProject, sourceType);
|
|
1589
|
+
ui.screen.render();
|
|
1590
|
+
}, 1500);
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
ui.projectPicker.hide();
|
|
1594
|
+
ui.statusBar.setContent(` Updating type to "${selectedType}"...`);
|
|
1595
|
+
ui.screen.render();
|
|
1596
|
+
try {
|
|
1597
|
+
const success = await updateSourceContentType(dbPath, source.id, selectedType);
|
|
1598
|
+
if (!success) {
|
|
1599
|
+
throw new Error('Failed to update content type');
|
|
1600
|
+
}
|
|
1601
|
+
// Update the local source object
|
|
1602
|
+
source.content_type = selectedType;
|
|
1603
|
+
state.mode = 'list';
|
|
1604
|
+
renderList(ui, state);
|
|
1605
|
+
renderPreview(ui, state);
|
|
1606
|
+
ui.statusBar.setContent(` {green-fg}Type changed to "${selectedType}"{/green-fg}`);
|
|
1607
|
+
ui.screen.render();
|
|
1608
|
+
setTimeout(() => {
|
|
1609
|
+
updateStatus(ui, state, state.currentProject, sourceType);
|
|
1610
|
+
ui.screen.render();
|
|
1611
|
+
}, 1500);
|
|
1612
|
+
}
|
|
1613
|
+
catch (error) {
|
|
1614
|
+
state.mode = 'list';
|
|
1615
|
+
ui.statusBar.setContent(` {red-fg}Update failed: ${error}{/red-fg}`);
|
|
1616
|
+
ui.screen.render();
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
/**
|
|
1620
|
+
* Cancel type picker
|
|
1621
|
+
*/
|
|
1622
|
+
export function cancelTypePicker(state, ui) {
|
|
1623
|
+
ui.projectPicker.hide();
|
|
1624
|
+
state.mode = 'list';
|
|
1625
|
+
state.typePickerSource = undefined;
|
|
1626
|
+
ui.screen.render();
|
|
1627
|
+
}
|
|
1628
|
+
// ============================================================================
|
|
1629
|
+
// Content Type Filter (Filter list by content type)
|
|
1630
|
+
// ============================================================================
|
|
1631
|
+
// Content types for filtering (includes "All" option)
|
|
1632
|
+
const FILTER_CONTENT_TYPES = [
|
|
1633
|
+
'__all__',
|
|
1634
|
+
'interview',
|
|
1635
|
+
'meeting',
|
|
1636
|
+
'conversation',
|
|
1637
|
+
'document',
|
|
1638
|
+
'note',
|
|
1639
|
+
'analysis',
|
|
1640
|
+
];
|
|
1641
|
+
/**
|
|
1642
|
+
* Show content type filter picker
|
|
1643
|
+
*/
|
|
1644
|
+
export function showContentTypeFilter(state, ui) {
|
|
1645
|
+
state.contentTypeFilterIndex = 0;
|
|
1646
|
+
// Find current filter in list
|
|
1647
|
+
if (state.currentContentType) {
|
|
1648
|
+
const idx = FILTER_CONTENT_TYPES.indexOf(state.currentContentType);
|
|
1649
|
+
if (idx >= 0) {
|
|
1650
|
+
state.contentTypeFilterIndex = idx;
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
state.mode = 'content-type-filter';
|
|
1654
|
+
renderContentTypeFilter(state, ui);
|
|
1655
|
+
ui.projectPicker.show();
|
|
1656
|
+
ui.screen.render();
|
|
1657
|
+
}
|
|
1658
|
+
/**
|
|
1659
|
+
* Render the content type filter UI
|
|
1660
|
+
*/
|
|
1661
|
+
export function renderContentTypeFilter(state, ui) {
|
|
1662
|
+
const currentFilter = state.currentContentType || 'All';
|
|
1663
|
+
const lines = [];
|
|
1664
|
+
lines.push('{bold}{yellow-fg}Filter by Content Type{/yellow-fg}{/bold}');
|
|
1665
|
+
lines.push(`{gray-fg}Current: ${currentFilter}{/gray-fg}`);
|
|
1666
|
+
lines.push('');
|
|
1667
|
+
for (let i = 0; i < FILTER_CONTENT_TYPES.length; i++) {
|
|
1668
|
+
const type = FILTER_CONTENT_TYPES[i];
|
|
1669
|
+
const isSelected = i === state.contentTypeFilterIndex;
|
|
1670
|
+
const isCurrent = type === state.currentContentType || (type === '__all__' && !state.currentContentType);
|
|
1671
|
+
const prefix = isSelected ? '{inverse} > ' : ' ';
|
|
1672
|
+
const suffix = isSelected ? ' {/inverse}' : '';
|
|
1673
|
+
let displayName = type === '__all__' ? '{cyan-fg}[All Types]{/cyan-fg}' : type;
|
|
1674
|
+
let extra = '';
|
|
1675
|
+
if (isCurrent) {
|
|
1676
|
+
extra = ' {magenta-fg}(current){/magenta-fg}';
|
|
1677
|
+
}
|
|
1678
|
+
lines.push(`${prefix}${displayName}${extra}${suffix}`);
|
|
1679
|
+
}
|
|
1680
|
+
lines.push('');
|
|
1681
|
+
lines.push('{blue-fg}j/k: navigate Enter: select Esc: cancel{/blue-fg}');
|
|
1682
|
+
ui.projectPickerContent.setContent(lines.join('\n'));
|
|
1683
|
+
}
|
|
1684
|
+
/**
|
|
1685
|
+
* Navigate down in content type filter
|
|
1686
|
+
*/
|
|
1687
|
+
export function contentTypeFilterDown(state, ui) {
|
|
1688
|
+
if (state.contentTypeFilterIndex < FILTER_CONTENT_TYPES.length - 1) {
|
|
1689
|
+
state.contentTypeFilterIndex++;
|
|
1690
|
+
renderContentTypeFilter(state, ui);
|
|
1691
|
+
ui.screen.render();
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
/**
|
|
1695
|
+
* Navigate up in content type filter
|
|
1696
|
+
*/
|
|
1697
|
+
export function contentTypeFilterUp(state, ui) {
|
|
1698
|
+
if (state.contentTypeFilterIndex > 0) {
|
|
1699
|
+
state.contentTypeFilterIndex--;
|
|
1700
|
+
renderContentTypeFilter(state, ui);
|
|
1701
|
+
ui.screen.render();
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Apply content type filter
|
|
1706
|
+
*/
|
|
1707
|
+
export async function applyContentTypeFilter(state, ui, dbPath, dataDir, sourceType) {
|
|
1708
|
+
const selectedType = FILTER_CONTENT_TYPES[state.contentTypeFilterIndex];
|
|
1709
|
+
ui.projectPicker.hide();
|
|
1710
|
+
const newFilter = selectedType === '__all__' ? undefined : selectedType;
|
|
1711
|
+
state.currentContentType = newFilter;
|
|
1712
|
+
// Check if we should return to ask/research mode
|
|
1713
|
+
if (state.pickerReturnMode === 'ask') {
|
|
1714
|
+
state.mode = 'ask';
|
|
1715
|
+
state.pickerReturnMode = undefined;
|
|
1716
|
+
ui.listPane.hide();
|
|
1717
|
+
ui.previewPane.hide();
|
|
1718
|
+
ui.askInput.show();
|
|
1719
|
+
ui.askPane.show();
|
|
1720
|
+
ui.askPane.setLabel(' Ask Lore ');
|
|
1721
|
+
renderReturnToAskOrResearch(state, ui, 'ask');
|
|
1722
|
+
ui.askInput.focus();
|
|
1723
|
+
ui.askInput.readInput();
|
|
1724
|
+
ui.screen.render();
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
else if (state.pickerReturnMode === 'research') {
|
|
1728
|
+
state.mode = 'research';
|
|
1729
|
+
state.pickerReturnMode = undefined;
|
|
1730
|
+
ui.listPane.hide();
|
|
1731
|
+
ui.previewPane.hide();
|
|
1732
|
+
ui.askInput.show();
|
|
1733
|
+
ui.askPane.show();
|
|
1734
|
+
ui.askPane.setLabel(' Research Agent ');
|
|
1735
|
+
renderReturnToAskOrResearch(state, ui, 'research');
|
|
1736
|
+
ui.askInput.focus();
|
|
1737
|
+
ui.askInput.readInput();
|
|
1738
|
+
ui.screen.render();
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
state.mode = 'list';
|
|
1742
|
+
ui.statusBar.setContent(' Filtering...');
|
|
1743
|
+
ui.screen.render();
|
|
1744
|
+
try {
|
|
1745
|
+
// Reload sources with content type filter
|
|
1746
|
+
// Note: getAllSources doesn't support content_type filter directly,
|
|
1747
|
+
// so we filter client-side for now
|
|
1748
|
+
state.sources = await getAllSources(dbPath, {
|
|
1749
|
+
project: state.currentProject,
|
|
1750
|
+
source_type: sourceType,
|
|
1751
|
+
limit: 100,
|
|
1752
|
+
});
|
|
1753
|
+
// Apply content type filter client-side
|
|
1754
|
+
if (newFilter) {
|
|
1755
|
+
state.filtered = state.sources.filter(s => s.content_type === newFilter);
|
|
1756
|
+
}
|
|
1757
|
+
else {
|
|
1758
|
+
state.filtered = [...state.sources];
|
|
1759
|
+
}
|
|
1760
|
+
state.selectedIndex = 0;
|
|
1761
|
+
// Rebuild list items if in grouped mode
|
|
1762
|
+
if (state.groupByProject) {
|
|
1763
|
+
state.listItems = buildListItems(state);
|
|
1764
|
+
}
|
|
1765
|
+
updateStatus(ui, state, state.currentProject, sourceType);
|
|
1766
|
+
renderList(ui, state);
|
|
1767
|
+
renderPreview(ui, state);
|
|
1768
|
+
ui.screen.render();
|
|
1769
|
+
}
|
|
1770
|
+
catch (error) {
|
|
1771
|
+
ui.statusBar.setContent(` {red-fg}Filter failed: ${error}{/red-fg}`);
|
|
1772
|
+
ui.screen.render();
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
/**
|
|
1776
|
+
* Cancel content type filter picker
|
|
1777
|
+
*/
|
|
1778
|
+
export function cancelContentTypeFilter(state, ui) {
|
|
1779
|
+
ui.projectPicker.hide();
|
|
1780
|
+
// Check if we should return to ask/research mode
|
|
1781
|
+
if (state.pickerReturnMode === 'ask') {
|
|
1782
|
+
state.mode = 'ask';
|
|
1783
|
+
state.pickerReturnMode = undefined;
|
|
1784
|
+
ui.listPane.hide();
|
|
1785
|
+
ui.previewPane.hide();
|
|
1786
|
+
ui.askInput.show();
|
|
1787
|
+
ui.askPane.show();
|
|
1788
|
+
ui.askPane.setLabel(' Ask Lore ');
|
|
1789
|
+
renderReturnToAskOrResearch(state, ui, 'ask');
|
|
1790
|
+
ui.askInput.focus();
|
|
1791
|
+
ui.askInput.readInput();
|
|
1792
|
+
}
|
|
1793
|
+
else if (state.pickerReturnMode === 'research') {
|
|
1794
|
+
state.mode = 'research';
|
|
1795
|
+
state.pickerReturnMode = undefined;
|
|
1796
|
+
ui.listPane.hide();
|
|
1797
|
+
ui.previewPane.hide();
|
|
1798
|
+
ui.askInput.show();
|
|
1799
|
+
ui.askPane.show();
|
|
1800
|
+
ui.askPane.setLabel(' Research Agent ');
|
|
1801
|
+
renderReturnToAskOrResearch(state, ui, 'research');
|
|
1802
|
+
ui.askInput.focus();
|
|
1803
|
+
ui.askInput.readInput();
|
|
1804
|
+
}
|
|
1805
|
+
else {
|
|
1806
|
+
state.mode = 'list';
|
|
1807
|
+
}
|
|
1808
|
+
ui.screen.render();
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Clear content type filter
|
|
1812
|
+
*/
|
|
1813
|
+
export async function clearContentTypeFilter(state, ui, dbPath, dataDir, sourceType) {
|
|
1814
|
+
state.currentContentType = undefined;
|
|
1815
|
+
ui.statusBar.setContent(' Clearing filter...');
|
|
1816
|
+
ui.screen.render();
|
|
1817
|
+
try {
|
|
1818
|
+
state.sources = await getAllSources(dbPath, {
|
|
1819
|
+
project: state.currentProject,
|
|
1820
|
+
source_type: sourceType,
|
|
1821
|
+
limit: 100,
|
|
1822
|
+
});
|
|
1823
|
+
state.filtered = [...state.sources];
|
|
1824
|
+
state.selectedIndex = 0;
|
|
1825
|
+
if (state.groupByProject) {
|
|
1826
|
+
state.listItems = buildListItems(state);
|
|
1827
|
+
}
|
|
1828
|
+
updateStatus(ui, state, state.currentProject, sourceType);
|
|
1829
|
+
renderList(ui, state);
|
|
1830
|
+
renderPreview(ui, state);
|
|
1831
|
+
ui.screen.render();
|
|
1832
|
+
}
|
|
1833
|
+
catch (error) {
|
|
1834
|
+
ui.statusBar.setContent(` {red-fg}Clear failed: ${error}{/red-fg}`);
|
|
1835
|
+
ui.screen.render();
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
// ============================================================================
|
|
1839
|
+
// Project Folders (Collapsible Groups)
|
|
1840
|
+
// ============================================================================
|
|
1841
|
+
/**
|
|
1842
|
+
* Toggle expand/collapse for the currently selected project header
|
|
1843
|
+
*/
|
|
1844
|
+
export function toggleProjectExpand(state, ui) {
|
|
1845
|
+
if (!state.groupByProject || state.listItems.length === 0) {
|
|
1846
|
+
return false;
|
|
1847
|
+
}
|
|
1848
|
+
const item = state.listItems[state.selectedIndex];
|
|
1849
|
+
if (!item || item.type !== 'header') {
|
|
1850
|
+
return false;
|
|
1851
|
+
}
|
|
1852
|
+
// Toggle expansion
|
|
1853
|
+
if (state.expandedProjects.has(item.projectName)) {
|
|
1854
|
+
state.expandedProjects.delete(item.projectName);
|
|
1855
|
+
}
|
|
1856
|
+
else {
|
|
1857
|
+
state.expandedProjects.add(item.projectName);
|
|
1858
|
+
}
|
|
1859
|
+
// Rebuild list items and re-render
|
|
1860
|
+
state.listItems = buildListItems(state);
|
|
1861
|
+
renderList(ui, state);
|
|
1862
|
+
renderPreview(ui, state);
|
|
1863
|
+
ui.screen.render();
|
|
1864
|
+
return true;
|
|
1865
|
+
}
|
|
1866
|
+
/**
|
|
1867
|
+
* Expand the currently selected project (or the project containing the selected doc)
|
|
1868
|
+
*/
|
|
1869
|
+
export function expandCurrentProject(state, ui) {
|
|
1870
|
+
if (!state.groupByProject || state.listItems.length === 0)
|
|
1871
|
+
return;
|
|
1872
|
+
const item = state.listItems[state.selectedIndex];
|
|
1873
|
+
if (!item)
|
|
1874
|
+
return;
|
|
1875
|
+
const projectName = item.type === 'header' ? item.projectName : item.projectName;
|
|
1876
|
+
if (!state.expandedProjects.has(projectName)) {
|
|
1877
|
+
state.expandedProjects.add(projectName);
|
|
1878
|
+
state.listItems = buildListItems(state);
|
|
1879
|
+
renderList(ui, state);
|
|
1880
|
+
renderPreview(ui, state);
|
|
1881
|
+
ui.screen.render();
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
/**
|
|
1885
|
+
* Collapse the currently selected project (or the project containing the selected doc)
|
|
1886
|
+
*/
|
|
1887
|
+
export function collapseCurrentProject(state, ui) {
|
|
1888
|
+
if (!state.groupByProject || state.listItems.length === 0)
|
|
1889
|
+
return;
|
|
1890
|
+
const item = state.listItems[state.selectedIndex];
|
|
1891
|
+
if (!item)
|
|
1892
|
+
return;
|
|
1893
|
+
const projectName = item.type === 'header' ? item.projectName : item.projectName;
|
|
1894
|
+
if (state.expandedProjects.has(projectName)) {
|
|
1895
|
+
state.expandedProjects.delete(projectName);
|
|
1896
|
+
// Find the header for this project and move selection to it
|
|
1897
|
+
state.listItems = buildListItems(state);
|
|
1898
|
+
const headerIdx = state.listItems.findIndex(i => i.type === 'header' && i.projectName === projectName);
|
|
1899
|
+
if (headerIdx >= 0) {
|
|
1900
|
+
state.selectedIndex = headerIdx;
|
|
1901
|
+
}
|
|
1902
|
+
renderList(ui, state);
|
|
1903
|
+
renderPreview(ui, state);
|
|
1904
|
+
ui.screen.render();
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
/**
|
|
1908
|
+
* Toggle grouped view mode
|
|
1909
|
+
*/
|
|
1910
|
+
export function toggleGroupedView(state, ui) {
|
|
1911
|
+
state.groupByProject = !state.groupByProject;
|
|
1912
|
+
if (state.groupByProject) {
|
|
1913
|
+
// Switching to grouped view - rebuild list items
|
|
1914
|
+
state.listItems = buildListItems(state);
|
|
1915
|
+
state.selectedIndex = 0;
|
|
1916
|
+
}
|
|
1917
|
+
else {
|
|
1918
|
+
// Switching to flat view
|
|
1919
|
+
state.listItems = [];
|
|
1920
|
+
state.selectedIndex = 0;
|
|
1921
|
+
}
|
|
1922
|
+
renderList(ui, state);
|
|
1923
|
+
renderPreview(ui, state);
|
|
1924
|
+
ui.screen.render();
|
|
1925
|
+
}
|
|
1926
|
+
/**
|
|
1927
|
+
* Check if selection is on a document (for Enter key handling)
|
|
1928
|
+
*/
|
|
1929
|
+
export function isDocumentSelected(state) {
|
|
1930
|
+
if (!state.groupByProject) {
|
|
1931
|
+
return state.filtered.length > 0;
|
|
1932
|
+
}
|
|
1933
|
+
const item = state.listItems[state.selectedIndex];
|
|
1934
|
+
return item?.type === 'document';
|
|
1935
|
+
}
|
|
1936
|
+
/**
|
|
1937
|
+
* Get the selected source for full view (handles both modes)
|
|
1938
|
+
*/
|
|
1939
|
+
export function getSelectedSourceForFullView(state) {
|
|
1940
|
+
return getSelectedSource(state);
|
|
1941
|
+
}
|
|
1942
|
+
// ============================================================================
|
|
1943
|
+
// Pending Proposals
|
|
1944
|
+
// ============================================================================
|