Skip to content

Commit 55e6ad9

Browse files
author
Omri Hopson
committed
fixed search
1 parent facaa30 commit 55e6ad9

12 files changed

Lines changed: 552 additions & 119 deletions

File tree

dashboard/src/pages/Repositories/Repositories.tsx

Lines changed: 108 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ export default function Repositories() {
2424
const [addMode, setAddMode] = useState<'browse' | 'manual'>('browse');
2525
const [busyAction, setBusyAction] = useState<string | null>(null);
2626
const [workspaceRoot, setWorkspaceRoot] = useState('');
27+
const [autoIndex, setAutoIndex] = useState(true);
28+
const [autoIndexLocked, setAutoIndexLocked] = useState(false);
29+
const [togglingAutoIndex, setTogglingAutoIndex] = useState(false);
2730
const { toasts, push: toast, dismiss: dismissToast } = useToast();
2831

2932
useEffect(() => {
@@ -32,8 +35,20 @@ export default function Repositories() {
3235
.then((r) => r.json())
3336
.then((d) => setWorkspaceRoot(d.workspaceRoot || ''))
3437
.catch((err) => console.warn('Failed to load workspace info:', err));
38+
loadConfig();
3539
}, []);
3640

41+
const loadConfig = async () => {
42+
try {
43+
const response = await fetch('/api/config');
44+
const data = await response.json();
45+
setAutoIndex(data.config.autoIndex || false);
46+
setAutoIndexLocked(data.envLocked.autoIndex || false);
47+
} catch (error) {
48+
console.error('Failed to load config:', error);
49+
}
50+
};
51+
3752
const loadRepositories = async () => {
3853
try {
3954
const response = await fetch('/api/repositories');
@@ -108,6 +123,63 @@ export default function Repositories() {
108123
}
109124
};
110125

126+
const handleToggleAutoIndex = async () => {
127+
if (autoIndexLocked) {
128+
toast('info', 'Auto-index is locked by environment variable. Cannot change via UI.');
129+
return;
130+
}
131+
132+
setTogglingAutoIndex(true);
133+
try {
134+
const response = await fetch('/api/config', {
135+
method: 'PUT',
136+
headers: { 'Content-Type': 'application/json' },
137+
body: JSON.stringify({ autoIndex: !autoIndex }),
138+
});
139+
140+
if (response.ok) {
141+
const data = await response.json();
142+
if (data.success) {
143+
setAutoIndex(!autoIndex);
144+
toast('success', !autoIndex
145+
? 'Auto-indexing enabled. New repositories will index automatically.'
146+
: 'Auto-indexing disabled. Use manual indexing for new repositories.');
147+
} else {
148+
toast('error', 'Failed to update config.');
149+
}
150+
} else {
151+
toast('error', 'Failed to update auto-index setting.');
152+
}
153+
} catch (error) {
154+
toast('error', 'Network error — could not reach server.');
155+
} finally {
156+
setTogglingAutoIndex(false);
157+
}
158+
};
159+
160+
const handleQuickIndexWorkspace = async () => {
161+
setBusyAction('quick-index');
162+
try {
163+
const response = await fetch('/api/repositories', {
164+
method: 'POST',
165+
headers: { 'Content-Type': 'application/json' },
166+
body: JSON.stringify({ path: '/workspace', incremental: false }),
167+
});
168+
169+
if (response.ok) {
170+
toast('success', 'Workspace indexing started. This may take a few minutes for large projects.');
171+
setTimeout(() => loadRepositories(), 2000);
172+
} else {
173+
const error = await response.text();
174+
toast('error', `Failed to index workspace: ${error}`);
175+
}
176+
} catch (error) {
177+
toast('error', 'Network error — could not reach server.');
178+
} finally {
179+
setBusyAction(null);
180+
}
181+
};
182+
111183
const handleAddRepository = async () => {
112184
if (!newRepoPath.trim()) return;
113185

@@ -181,6 +253,22 @@ export default function Repositories() {
181253
</p>
182254
</div>
183255
<div className="flex gap-3">
256+
<button
257+
onClick={handleToggleAutoIndex}
258+
disabled={togglingAutoIndex || autoIndexLocked}
259+
className={`px-6 py-3 font-semibold text-[0.875rem] rounded-xl transition-all flex items-center gap-2 ${
260+
autoIndex
261+
? 'bg-green-500/10 text-green-600 hover:bg-green-500/20 border border-green-500/30'
262+
: 'bg-surface-container-highest text-on-surface hover:bg-surface-dim'
263+
} disabled:opacity-50 disabled:cursor-not-allowed`}
264+
title={autoIndexLocked ? 'Locked by environment variable' : 'Toggle auto-indexing'}
265+
>
266+
<span className={`material-symbols-outlined text-[18px] ${togglingAutoIndex ? 'animate-spin' : ''}`}>
267+
{autoIndex ? 'toggle_on' : 'toggle_off'}
268+
</span>
269+
Auto-Index: {autoIndex ? 'ON' : 'OFF'}
270+
{autoIndexLocked && <span className="material-symbols-outlined text-[14px]">lock</span>}
271+
</button>
184272
{repos.length > 0 && (
185273
<button
186274
onClick={() => {
@@ -222,15 +310,27 @@ export default function Repositories() {
222310
</span>
223311
<h2 className="text-2xl font-bold mb-2">No Repositories Indexed</h2>
224312
<p className="text-on-surface-variant mb-6">
225-
Get started by adding a repository to index for code intelligence.
313+
Get started by indexing your workspace or adding a specific repository.
226314
</p>
227-
<button
228-
onClick={() => setShowAddDialog(true)}
229-
className="px-6 py-3 primary-gradient text-white font-semibold text-[0.875rem] rounded-xl shadow-lg shadow-tertiary/10 active:scale-95 transition-all inline-flex items-center gap-2"
230-
>
231-
<span className="material-symbols-outlined text-[18px]">add</span>
232-
Add Repository
233-
</button>
315+
<div className="flex gap-3 justify-center">
316+
<button
317+
onClick={handleQuickIndexWorkspace}
318+
disabled={busyAction === 'quick-index'}
319+
className="px-6 py-3 bg-tertiary/10 text-tertiary font-semibold text-[0.875rem] rounded-xl hover:bg-tertiary/20 active:scale-95 transition-all inline-flex items-center gap-2 disabled:opacity-50"
320+
>
321+
<span className={`material-symbols-outlined text-[18px] ${busyAction === 'quick-index' ? 'animate-spin' : ''}`}>
322+
{busyAction === 'quick-index' ? 'refresh' : 'bolt'}
323+
</span>
324+
{busyAction === 'quick-index' ? 'Indexing...' : 'Quick Index Workspace'}
325+
</button>
326+
<button
327+
onClick={() => setShowAddDialog(true)}
328+
className="px-6 py-3 primary-gradient text-white font-semibold text-[0.875rem] rounded-xl shadow-lg shadow-tertiary/10 active:scale-95 transition-all inline-flex items-center gap-2"
329+
>
330+
<span className="material-symbols-outlined text-[18px]">add</span>
331+
Add Specific Path
332+
</button>
333+
</div>
234334
</div>
235335
)}
236336

src/api/routes/search.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ export interface SearchRouteOptions {
6868
vectorSearch?: any;
6969
hybridSearch?: any;
7070
configManager?: any;
71+
workspaceRoot?: string;
7172
}
7273

7374
export async function registerSearchRoutes(
@@ -106,7 +107,7 @@ export async function registerSearchRoutes(
106107
let semanticTime = 0;
107108
let fusionTime = 0;
108109

109-
let results: unknown[] = [];
110+
let results: any[] = [];
110111
let total = 0;
111112
let hasMore = false;
112113

@@ -130,10 +131,23 @@ export async function registerSearchRoutes(
130131
});
131132
}
132133

134+
// Get repository ID - use provided one or first available
135+
let repoId = input.repositoryId;
136+
if (!repoId) {
137+
const repos = options.storage.listRepositories();
138+
if (repos.length === 0) {
139+
return reply.status(400).send({
140+
error: 'No repositories indexed',
141+
message: 'Please index a repository first',
142+
});
143+
}
144+
repoId = repos[0]!.id;
145+
}
146+
133147
const semanticStart = Date.now();
134148
const response = await vectorSearch.search(
135149
input.query,
136-
input.repositoryId || 'default-repo',
150+
repoId,
137151
input.limit,
138152
input.offset
139153
);
@@ -151,10 +165,23 @@ export async function registerSearchRoutes(
151165
});
152166
}
153167

168+
// Get repository ID - use provided one or first available
169+
let repoId = input.repositoryId;
170+
if (!repoId) {
171+
const repos = options.storage.listRepositories();
172+
if (repos.length === 0) {
173+
return reply.status(400).send({
174+
error: 'No repositories indexed',
175+
message: 'Please index a repository first',
176+
});
177+
}
178+
repoId = repos[0]!.id;
179+
}
180+
154181
const hybridStart = Date.now();
155182
const response = await hybridSearch.search(
156183
input.query,
157-
input.repositoryId || 'default-repo',
184+
repoId,
158185
input.limit,
159186
input.offset
160187
);
@@ -165,6 +192,32 @@ export async function registerSearchRoutes(
165192
hasMore = response.hasMore;
166193
}
167194

195+
// Add code snippets if workspace root is available
196+
if (options.workspaceRoot && results.length > 0) {
197+
try {
198+
const { extractSnippetsBatch } = await import('../../search/snippet.js');
199+
const snippets = await extractSnippetsBatch(
200+
options.workspaceRoot,
201+
results.map(r => ({
202+
filePath: r.filePath,
203+
lineStart: r.lineStart,
204+
lineEnd: r.lineEnd,
205+
})),
206+
{ maxLines: 10, maxChars: 500 }
207+
);
208+
209+
// Attach snippets to results
210+
results = results.map(r => {
211+
const key = `${r.filePath}:${r.lineStart}:${r.lineEnd}`;
212+
const snippet = snippets.get(key);
213+
return snippet ? { ...r, snippet } : r;
214+
});
215+
} catch (error) {
216+
// Snippet extraction failed, continue without snippets
217+
console.warn('Failed to extract snippets:', error);
218+
}
219+
}
220+
168221
const totalTime = Date.now() - startTime;
169222

170223
return {

src/api/server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,7 @@ export async function createAPIServer(
193193
vectorSearch: options.vectorSearch,
194194
hybridSearch: options.hybridSearch,
195195
configManager: options.configManager,
196+
workspaceRoot: options.workspaceRoot,
196197
});
197198

198199
await registerGraphRoutes(fastify, {

src/core/config-manager.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ import type { HybridSearch } from '../search/hybrid.js';
3636
export interface ConfigManagerOptions {
3737
storage: StorageProvider;
3838
vectorStore?: LanceDBVectorStore;
39+
indexer?: any;
40+
workspaceRoot?: string;
3941
onEmbeddingProviderChange?: (provider: EmbeddingProvider | undefined) => Promise<void>;
4042
onEmbeddingQueueChange?: (queue: EmbeddingQueue | undefined) => Promise<void>;
4143
onVectorSearchChange?: (vectorSearch: VectorSearch | undefined, hybridSearch: HybridSearch | undefined) => Promise<void>;
@@ -119,13 +121,19 @@ export class ConfigManager extends EventEmitter {
119121

120122
const needsProviderReload = this.needsProviderReload(updates);
121123
const needsQueueUpdate = this.needsQueueUpdate(updates);
124+
const needsAutoIndex = this.needsAutoIndexTrigger(updates);
122125

123126
if (needsProviderReload) {
124127
await this.reloadEmbeddingProvider(newConfig, changes, warnings);
125128
} else if (needsQueueUpdate) {
126129
await this.updateQueueSettings(newConfig, changes);
127130
}
128131

132+
if (needsAutoIndex) {
133+
changes.push('Auto-indexing enabled - will trigger on next config reload or manually via API');
134+
warnings.push('Auto-indexing is now enabled. Use POST /api/repositories to trigger indexing.');
135+
}
136+
129137
this.emit('reloaded', { changes, warnings });
130138
return { success: true, changes, warnings };
131139
} catch (error) {
@@ -156,6 +164,10 @@ export class ConfigManager extends EventEmitter {
156164
return !!(updates.embeddingConcurrency || updates.embeddingBatchSize);
157165
}
158166

167+
private needsAutoIndexTrigger(updates: Record<string, unknown>): boolean {
168+
return updates.autoIndex === true;
169+
}
170+
159171
private async reloadEmbeddingProvider(
160172
newConfig: AppConfig,
161173
changes: string[],

src/core/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,12 @@ export const SearchResultSchema = z.object({
145145
snippet: z.string().optional(),
146146
language: z.string(),
147147
repositoryId: z.string(),
148+
// Enhanced metadata
149+
docstring: z.string().optional(),
150+
complexity: z.number().int().nonnegative().optional(),
151+
visibility: VisibilitySchema.optional(),
152+
isExported: z.boolean().optional(),
153+
parentSymbol: z.string().optional(),
148154
});
149155

150156
export type SearchResult = z.infer<typeof SearchResultSchema>;

0 commit comments

Comments
 (0)