@neuralis/brain-core 0.2.0 → 0.2.1
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/app/README.md +1 -1
- package/app/filesystem/README.md +3 -3
- package/app/filesystem/api/README.md +3 -3
- package/app/filesystem/api/index.ts +127 -122
- package/app/filesystem/domain/types.ts +12 -0
- package/app/filesystem/hooks/README.md +5 -5
- package/app/filesystem/hooks/useDiscoverableSources.ts +4 -2
- package/app/filesystem/hooks/useFilesystemSSE.ts +31 -23
- package/app/filesystem/hooks/useGitChangesInventory.ts +4 -2
- package/app/filesystem/host/README.md +3 -2
- package/app/filesystem/host/installFilesystemHostComponents.tsx +22 -9
- package/app/filesystem/store/README.md +11 -7
- package/app/filesystem/store/filesystemStore.ts +434 -161
- package/app/filesystem/store/gitSlice.ts +6 -6
- package/app/filesystem/store/listSlice.ts +8 -6
- package/app/filesystem/store/searchSlice.ts +3 -3
- package/app/filesystem/ui/FileEditor.tsx +48 -35
- package/app/filesystem/ui/FileTree.tsx +10 -9
- package/app/filesystem/ui/GitCommitChangesView.tsx +5 -3
- package/app/filesystem/ui/GitCommitGraph.tsx +6 -5
- package/app/filesystem/ui/GitControlPanel.tsx +14 -13
- package/app/filesystem/ui/GitDiffView.tsx +2 -1
- package/app/filesystem/ui/GitFileDiff.tsx +7 -6
- package/app/filesystem/ui/PendingChangesPanel.tsx +16 -13
- package/app/filesystem/ui/PendingReviewDialog.tsx +8 -5
- package/app/filesystem/ui/ReembedDialog.tsx +4 -2
- package/app/filesystem/ui/SourcesPanel.tsx +26 -26
- package/app/filesystem/ui/VectorHealthPanel.tsx +6 -4
- package/dist/app/host.js +46 -46
- package/package.json +4 -4
- package/skills/create-package/references/manifest-and-discovery.md +1 -1
- package/skills/create-package/references/surfaces-and-design.md +7 -5
- package/skills/create-package/scripts/check-package.sh +47 -1
- package/app/filesystem/hooks/useContainerWidth.ts +0 -39
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
import { computeGitDiffTokens, renderShikiToken } from './gitDiffShikiTokenizer';
|
|
37
37
|
import { resolveShikiLang, isShikiReady, ensureLanguage, getHighlighter } from './codeEditor/shikiHighlighter';
|
|
38
38
|
import { detectCodeLanguage } from '../lib/detectLanguage';
|
|
39
|
-
import { useContainerWidth } from '
|
|
39
|
+
import { useContainerWidth } from '@neuralis/package-system/client';
|
|
40
40
|
import type { VisibleDiffQueue } from '../hooks/useVisibleDiffCards';
|
|
41
41
|
import { DiffModeToggle } from './DiffModeToggle';
|
|
42
42
|
|
|
@@ -156,6 +156,7 @@ export function GitFileDiff({
|
|
|
156
156
|
/** The lane's synchronous cache read: a re-mounted card paints its last answer on its first frame. */
|
|
157
157
|
peekContent?: VisibleDiffQueue['peek'];
|
|
158
158
|
}): React.ReactElement {
|
|
159
|
+
const client = useFilesystemStore((s) => s.client);
|
|
159
160
|
const [initial] = useState(() => peekContent?.<DiffAnswer>(`${diffFamily(source, repo, path, mode)}\u0000${revision}`, diffFamily(source, repo, path, mode)));
|
|
160
161
|
const [raw, setRaw] = useState<string | null>(() => initial === undefined ? null : initial && initial.ok !== false ? initial.diff : '');
|
|
161
162
|
const [loading, setLoading] = useState(false);
|
|
@@ -262,7 +263,7 @@ export function GitFileDiff({
|
|
|
262
263
|
path,
|
|
263
264
|
// ONE mode per request — the server 400s any combination.
|
|
264
265
|
...diffRequestParams(normalizedMode),
|
|
265
|
-
});
|
|
266
|
+
}, client);
|
|
266
267
|
// Keyed on everything the answer depends on; after a hunk apply the refetch
|
|
267
268
|
// replaces the cached answer instead of reading it.
|
|
268
269
|
const family = diffFamily(source, repo, path, normalizedMode);
|
|
@@ -364,7 +365,7 @@ export function GitFileDiff({
|
|
|
364
365
|
...(repo ? { repo } : {}),
|
|
365
366
|
path,
|
|
366
367
|
rev: expanderSrc.rev,
|
|
367
|
-
});
|
|
368
|
+
}, client);
|
|
368
369
|
const res = runContent ? await runContent(fetchSource, () => mySeq === seqRef.current) : await fetchSource();
|
|
369
370
|
if (!res || mySeq !== seqRef.current) return null;
|
|
370
371
|
return settle(res.ok ? { ok: true, content: res.content } : { ok: false });
|
|
@@ -376,7 +377,7 @@ export function GitFileDiff({
|
|
|
376
377
|
agentId,
|
|
377
378
|
...(projectId ? { projectId } : {}),
|
|
378
379
|
files: [`${source}://${repo ? `${repo}/` : ''}${path}`],
|
|
379
|
-
});
|
|
380
|
+
}, client);
|
|
380
381
|
const res = runContent ? await runContent(fetchSource, () => mySeq === seqRef.current) : await fetchSource();
|
|
381
382
|
if (!res || mySeq !== seqRef.current) return null;
|
|
382
383
|
const file = extractReadFilesPayload(res)[0];
|
|
@@ -467,9 +468,9 @@ export function GitFileDiff({
|
|
|
467
468
|
setHunkError(null);
|
|
468
469
|
try {
|
|
469
470
|
if (staged) {
|
|
470
|
-
await unstageHunk({ agentId, ...(projectId ? { projectId } : {}), source, ...(repo ? { repo } : {}), hunk: patch });
|
|
471
|
+
await unstageHunk({ agentId, ...(projectId ? { projectId } : {}), source, ...(repo ? { repo } : {}), hunk: patch }, client);
|
|
471
472
|
} else {
|
|
472
|
-
await stageHunk({ agentId, ...(projectId ? { projectId } : {}), source, ...(repo ? { repo } : {}), hunk: patch });
|
|
473
|
+
await stageHunk({ agentId, ...(projectId ? { projectId } : {}), source, ...(repo ? { repo } : {}), hunk: patch }, client);
|
|
473
474
|
}
|
|
474
475
|
// The index changed → the cached `rev=index` source is stale (the WT
|
|
475
476
|
// entry would survive, but a whole-map clear is cheap and race-free).
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
ChevronLeft, CheckCheck, X, XCircle, Wrench, Unlink, ShieldAlert,
|
|
24
24
|
} from 'lucide-react';
|
|
25
25
|
import { SkillLauncher } from '@neuralis/package-system/client';
|
|
26
|
-
import { useFilesystemStore } from '../store/filesystemStore';
|
|
26
|
+
import { useFilesystemStore, useFilesystemStoreApi } from '../store/filesystemStore';
|
|
27
27
|
import {
|
|
28
28
|
listPendingChanges, previewPendingChange, discardPendingChange, discardOrphanedPending,
|
|
29
29
|
type PendingChangeRecord, type PendingPreview,
|
|
@@ -115,6 +115,7 @@ export const PendingDiffCard = memo(function PendingDiffCard({
|
|
|
115
115
|
/** The dialog's post-409 refetch: `'wait'` while in flight, then its answer ONCE for the key it was taken for. */
|
|
116
116
|
takeRefetch: (id: string, key: string) => PendingPreview | 'wait' | undefined; extra?: React.ReactNode;
|
|
117
117
|
}) {
|
|
118
|
+
const client = useFilesystemStore((s) => s.client);
|
|
118
119
|
// Cards open inline by default — the diff is the review unit.
|
|
119
120
|
const [expanded, setExpanded] = useState(true);
|
|
120
121
|
// The last answer this record had (a remount or a new source revision) stays on screen
|
|
@@ -140,7 +141,7 @@ export const PendingDiffCard = memo(function PendingDiffCard({
|
|
|
140
141
|
if (adopted === 'wait') { setLoading(true); return; }
|
|
141
142
|
if (adopted) { setPreview(adopted); setLoadError(null); setAnswerKey(cacheKey); loadedKey.current = cacheKey; setLoading(false); return; }
|
|
142
143
|
setLoading(true);
|
|
143
|
-
void queue.run(() => previewPendingChange({ agentId, ...(projectId ? { projectId } : {}), id: rec.id }),
|
|
144
|
+
void queue.run(() => previewPendingChange({ agentId, ...(projectId ? { projectId } : {}), id: rec.id }, client),
|
|
144
145
|
() => generation === seq.current && active.current, { key: cacheKey, bytes: previewBytes }).then(next => {
|
|
145
146
|
if (!next || generation !== seq.current || !active.current) return;
|
|
146
147
|
onPreview(next);
|
|
@@ -212,8 +213,10 @@ const NO_FILTERS: ChangeFilters = { kind: 'all', source: '', repo: '', branch: '
|
|
|
212
213
|
/** One bulk pass (the sweep and the grouped dialog) covers at most this many records. */ const REVIEW_BATCH = 100;
|
|
213
214
|
|
|
214
215
|
export function PendingChangesPanel({ agentId, projectId }: Props) {
|
|
216
|
+
const client = useFilesystemStore((s) => s.client);
|
|
215
217
|
const host = useFilesystemHost();
|
|
216
218
|
const canWrite = hasFeature({ grantedFeatures: [...(host.grantedFeatures ?? [])] }, 'drive.write');
|
|
219
|
+
const storeApi = useFilesystemStoreApi();
|
|
217
220
|
const sources = useFilesystemStore(s => s.sources);
|
|
218
221
|
const liveRevisions = useFilesystemStore(s => s.sourceChangeRevision);
|
|
219
222
|
// A review batch (the sweep, a dialog group) HOLDS the revisions its own writes bump: one re-list
|
|
@@ -261,7 +264,7 @@ export function PendingChangesPanel({ agentId, projectId }: Props) {
|
|
|
261
264
|
refetches.current.set(rec.id, null);
|
|
262
265
|
try {
|
|
263
266
|
const listed = (await relisted.current.promise)?.find(r => r.id === rec.id) ?? rec;
|
|
264
|
-
const fresh = await previewPendingChange({ agentId, ...(projectId ? { projectId } : {}), id: rec.id });
|
|
267
|
+
const fresh = await previewPendingChange({ agentId, ...(projectId ? { projectId } : {}), id: rec.id }, client);
|
|
265
268
|
const revision = (latest.current.nonces[rec.id] ?? 0) + 1 + (latest.current.revisions[listed.sourceSlug] ?? 0);
|
|
266
269
|
refetches.current.set(rec.id, { key: previewKey(listed, revision), preview: fresh });
|
|
267
270
|
previews.current.set(rec.id, fresh);
|
|
@@ -289,7 +292,7 @@ export function PendingChangesPanel({ agentId, projectId }: Props) {
|
|
|
289
292
|
setLoading(true); setError(null);
|
|
290
293
|
try {
|
|
291
294
|
const list = await listPendingChanges({ agentId, ...(projectId ? { projectId } : {}),
|
|
292
|
-
...(convFilter ? { conversationId: convFilter } : {}), ...(withOrphans ? { include_orphans: true } : {}) });
|
|
295
|
+
...(convFilter ? { conversationId: convFilter } : {}), ...(withOrphans ? { include_orphans: true } : {}) }, client);
|
|
293
296
|
if (mine !== listSeq.current) return undefined;
|
|
294
297
|
const ids = new Set(list.changes.map(rec => rec.id));
|
|
295
298
|
setChanges(list.changes);
|
|
@@ -372,9 +375,9 @@ export function PendingChangesPanel({ agentId, projectId }: Props) {
|
|
|
372
375
|
*/
|
|
373
376
|
const dropResolvedDeleteRow = useCallback((rec: PendingChangeRecord) => {
|
|
374
377
|
if (rec.op !== 'delete' || !rec.uri) return;
|
|
375
|
-
const node =
|
|
378
|
+
const node = storeApi.getState().files[rec.uri];
|
|
376
379
|
if (!node || node.status === 'deleted') removeFilePath(rec.uri);
|
|
377
|
-
}, [removeFilePath]);
|
|
380
|
+
}, [removeFilePath, storeApi]);
|
|
378
381
|
|
|
379
382
|
const afterMutation = useCallback(async (done: ReviewResult[]) => {
|
|
380
383
|
for (const result of done) {
|
|
@@ -388,17 +391,17 @@ export function PendingChangesPanel({ agentId, projectId }: Props) {
|
|
|
388
391
|
setResults(done);
|
|
389
392
|
const after = await fetchPending(false);
|
|
390
393
|
// The STORE's value, not the last render's: the batch's own bumps may not have rendered yet.
|
|
391
|
-
listedRevisions.current =
|
|
394
|
+
listedRevisions.current = storeApi.getState().sourceChangeRevision; setHeld(null);
|
|
392
395
|
relisted.current.resolve(after); relisted.current = Promise.withResolvers();
|
|
393
396
|
void refreshPendingCount({ agentId, projectId });
|
|
394
|
-
}, [changes, dropResolvedDeleteRow, fetchPending, refreshPendingCount, agentId, projectId]);
|
|
397
|
+
}, [changes, dropResolvedDeleteRow, fetchPending, refreshPendingCount, agentId, projectId, storeApi]);
|
|
395
398
|
|
|
396
399
|
const resolveReview = useCallback(async (rec: PendingChangeRecord, action: ReviewAction) => {
|
|
397
400
|
if (!canWrite) return;
|
|
398
401
|
const preview = previews.current.get(rec.id);
|
|
399
402
|
if (!preview || preview.requiresAcknowledgement.length) { setReview({ kind: 'single', rec, action, ...(preview ? { preview } : {}) }); return; }
|
|
400
403
|
setBusyId(rec.id);
|
|
401
|
-
try { await afterMutation([await applyReview({ agentId, projectId }, rec, preview, action, false)]); }
|
|
404
|
+
try { await afterMutation([await applyReview({ agentId, projectId, client }, rec, preview, action, false)]); }
|
|
402
405
|
finally { setBusyId(null); }
|
|
403
406
|
}, [canWrite, agentId, projectId, afterMutation]);
|
|
404
407
|
|
|
@@ -406,7 +409,7 @@ export function PendingChangesPanel({ agentId, projectId }: Props) {
|
|
|
406
409
|
const discard = useCallback(async (rec: PendingChangeRecord) => {
|
|
407
410
|
setBusyId(rec.id);
|
|
408
411
|
try {
|
|
409
|
-
const res = await discardPendingChange({ agentId, ...(projectId ? { projectId } : {}), id: rec.id });
|
|
412
|
+
const res = await discardPendingChange({ agentId, ...(projectId ? { projectId } : {}), id: rec.id }, client);
|
|
410
413
|
if (!res.ok) setError(res.error || 'Discard failed'); else dropResolvedDeleteRow(rec);
|
|
411
414
|
await fetchPending(false);
|
|
412
415
|
void refreshPendingCount({ agentId, projectId });
|
|
@@ -419,7 +422,7 @@ export function PendingChangesPanel({ agentId, projectId }: Props) {
|
|
|
419
422
|
if (repairTimer.current) { clearTimeout(repairTimer.current); repairTimer.current = null; }
|
|
420
423
|
setRepairing(true); setConfirmRepair(false); setError(null);
|
|
421
424
|
try {
|
|
422
|
-
await discardOrphanedPending({ agentId, ...(projectId ? { projectId } : {}), ...(convFilter ? { conversationId: convFilter } : {}) });
|
|
425
|
+
await discardOrphanedPending({ agentId, ...(projectId ? { projectId } : {}), ...(convFilter ? { conversationId: convFilter } : {}) }, client);
|
|
423
426
|
const candidates = changes.filter((rec) => orphanIds.has(rec.id));
|
|
424
427
|
const after = await fetchPending(true);
|
|
425
428
|
if (after) {
|
|
@@ -446,12 +449,12 @@ export function PendingChangesPanel({ agentId, projectId }: Props) {
|
|
|
446
449
|
for (const rec of approvable) {
|
|
447
450
|
let preview = previews.current.get(rec.id);
|
|
448
451
|
if (!preview) {
|
|
449
|
-
try { preview = await previewPendingChange({ agentId, ...(projectId ? { projectId } : {}), id: rec.id }); }
|
|
452
|
+
try { preview = await previewPendingChange({ agentId, ...(projectId ? { projectId } : {}), id: rec.id }, client); }
|
|
450
453
|
catch (e) { done.push({ id: rec.id, uri: rec.uri, action: 'approve', outcome: 'error', detail: e instanceof Error ? e.message : 'Preview failed' }); continue; }
|
|
451
454
|
previews.current.set(rec.id, preview);
|
|
452
455
|
}
|
|
453
456
|
if (preview.requiresAcknowledgement.length) { done.push({ id: rec.id, uri: rec.uri, action: 'approve', outcome: 'needs-ack' }); continue; }
|
|
454
|
-
done.push(await applyReview({ agentId, projectId }, rec, preview, 'approve', false));
|
|
457
|
+
done.push(await applyReview({ agentId, projectId, client }, rec, preview, 'approve', false));
|
|
455
458
|
}
|
|
456
459
|
} finally {
|
|
457
460
|
setApprovingAll(false);
|
|
@@ -19,6 +19,8 @@ import { DiffView } from '../../cards/DiffView';
|
|
|
19
19
|
import { detectLanguage } from '../../cards/diffUtils';
|
|
20
20
|
import type { VisibleDiffQueue } from '../hooks/useVisibleDiffCards';
|
|
21
21
|
import { acceptsAbsence, classifyReviewOutcome, hunksOf, type ReviewOutcome } from './pendingChangesModel';
|
|
22
|
+
import { useFilesystemStore } from '../store/filesystemStore';
|
|
23
|
+
import type { PackageApi } from '@neuralis/package-system/client';
|
|
22
24
|
|
|
23
25
|
export type ReviewAction = 'approve' | 'dismiss' | 'revert';
|
|
24
26
|
export type ReviewRequest =
|
|
@@ -42,13 +44,13 @@ const OUTCOME: Record<ReviewOutcome, { label: string; tone: string }> = {
|
|
|
42
44
|
export type ReviewResult = { id: string; uri: string; action: ReviewAction; outcome: ReviewOutcome; detail?: string };
|
|
43
45
|
|
|
44
46
|
/** Run one review verb with the record's own fingerprint; never throws. */
|
|
45
|
-
export async function applyReview(params: { agentId: string; projectId: string | null }, rec: PendingChangeRecord,
|
|
47
|
+
export async function applyReview(params: { agentId: string; projectId: string | null; client: PackageApi }, rec: PendingChangeRecord,
|
|
46
48
|
preview: PendingPreview, action: ReviewAction, acknowledge: boolean): Promise<ReviewResult> {
|
|
47
49
|
const body = { agentId: params.agentId, ...(params.projectId ? { projectId: params.projectId } : {}), id: rec.id,
|
|
48
50
|
expectedFingerprint: preview.expectedFingerprint, ...(acknowledge ? { acknowledgeCurrentState: true as const } : {}) };
|
|
49
51
|
try {
|
|
50
|
-
const result = action === 'approve' ? await approveFileReview({ ...body, uri: rec.uri })
|
|
51
|
-
: action === 'revert' ? await revertPendingChange(body) : await dismissPendingChange(body);
|
|
52
|
+
const result = action === 'approve' ? await approveFileReview({ ...body, uri: rec.uri }, params.client)
|
|
53
|
+
: action === 'revert' ? await revertPendingChange(body, params.client) : await dismissPendingChange(body, params.client);
|
|
52
54
|
if (result && result.ok === false) {
|
|
53
55
|
const detail = 'error' in result && result.error ? String(result.error) : 'Review refused';
|
|
54
56
|
return { id: rec.id, uri: rec.uri, action, outcome: classifyReviewOutcome(detail), detail };
|
|
@@ -90,6 +92,7 @@ export function PendingReviewDialog({ request, agentId, projectId, run, onClose,
|
|
|
90
92
|
/** Records still waiting outside this batch; `onNext` (given this batch's ids) opens the next one. */
|
|
91
93
|
next?: number; onNext?: (batch: string[]) => void;
|
|
92
94
|
}): React.ReactElement | null {
|
|
95
|
+
const client = useFilesystemStore((s) => s.client);
|
|
93
96
|
const records = useMemo(() => request.kind === 'single' ? [request.rec] : request.records, [request]);
|
|
94
97
|
const [previews, setPreviews] = useState<Record<string, PendingPreview | Error>>(
|
|
95
98
|
() => request.kind === 'single' && request.preview ? { [request.rec.id]: request.preview } : {});
|
|
@@ -104,7 +107,7 @@ export function PendingReviewDialog({ request, agentId, projectId, run, onClose,
|
|
|
104
107
|
const seq = (loads.current.get(rec.id) ?? 0) + 1;
|
|
105
108
|
loads.current.set(rec.id, seq);
|
|
106
109
|
const current = () => mounted.current && loads.current.get(rec.id) === seq;
|
|
107
|
-
const load = () => previewPendingChange({ agentId, ...(projectId ? { projectId } : {}), id: rec.id });
|
|
110
|
+
const load = () => previewPendingChange({ agentId, ...(projectId ? { projectId } : {}), id: rec.id }, client);
|
|
108
111
|
void (again && refetch ? refetch(rec) : run(load, current))
|
|
109
112
|
.then(preview => { if (current() && preview) setPreviews(p => ({ ...p, [rec.id]: preview })); },
|
|
110
113
|
error => { if (current()) setPreviews(p => ({ ...p, [rec.id]: error instanceof Error ? error : new Error('Preview failed') })); });
|
|
@@ -140,7 +143,7 @@ export function PendingReviewDialog({ request, agentId, projectId, run, onClose,
|
|
|
140
143
|
for (const rec of targets) {
|
|
141
144
|
const preview = previews[rec.id];
|
|
142
145
|
if (!preview || preview instanceof Error || results[rec.id]?.outcome === 'ok') continue;
|
|
143
|
-
const result = await applyReview({ agentId, projectId }, rec, preview, action, preview.requiresAcknowledgement.length > 0);
|
|
146
|
+
const result = await applyReview({ agentId, projectId, client }, rec, preview, action, preview.requiresAcknowledgement.length > 0);
|
|
144
147
|
done.push(result);
|
|
145
148
|
setResults(r => ({ ...r, [rec.id]: result }));
|
|
146
149
|
// The state moved under the preview: fetch ONE fresh preview for this record
|
|
@@ -18,6 +18,7 @@ import { useEffect, useState } from 'react';
|
|
|
18
18
|
import { createPortal } from 'react-dom';
|
|
19
19
|
import { Loader2, RefreshCcw, X } from 'lucide-react';
|
|
20
20
|
import { estimateReembed, startReembed, type ReembedEstimate, type ReembedKind } from '../api';
|
|
21
|
+
import { useFilesystemStore } from '../store/filesystemStore';
|
|
21
22
|
|
|
22
23
|
export type ReembedTarget = {
|
|
23
24
|
source: string;
|
|
@@ -58,6 +59,7 @@ export function ReembedDialog({
|
|
|
58
59
|
/** Called after a job was queued — the caller follows it however it follows sync jobs. */
|
|
59
60
|
onStarted?: () => void;
|
|
60
61
|
}) {
|
|
62
|
+
const client = useFilesystemStore((s) => s.client);
|
|
61
63
|
const [estimate, setEstimate] = useState<ReembedEstimate | null>(null);
|
|
62
64
|
const [estimating, setEstimating] = useState(true);
|
|
63
65
|
const [starting, setStarting] = useState(false);
|
|
@@ -75,7 +77,7 @@ export function ReembedDialog({
|
|
|
75
77
|
useEffect(() => {
|
|
76
78
|
let live = true;
|
|
77
79
|
setEstimating(true);
|
|
78
|
-
estimateReembed(scope, kind)
|
|
80
|
+
estimateReembed(scope, kind, client)
|
|
79
81
|
.then((result) => {
|
|
80
82
|
if (live) setEstimate(result);
|
|
81
83
|
})
|
|
@@ -108,7 +110,7 @@ export function ReembedDialog({
|
|
|
108
110
|
setStarting(true);
|
|
109
111
|
setError(null);
|
|
110
112
|
try {
|
|
111
|
-
const started = await startReembed(scope, kind);
|
|
113
|
+
const started = await startReembed(scope, kind, client);
|
|
112
114
|
if (!started.job) {
|
|
113
115
|
// Nothing readable in scope — the route queued nothing. Say so.
|
|
114
116
|
setNothingToDo(true);
|
|
@@ -470,6 +470,7 @@ const textareaCls = cn(
|
|
|
470
470
|
// ---------------------------------------------------------------------------
|
|
471
471
|
|
|
472
472
|
export function SourcesPanel(props: Props) {
|
|
473
|
+
const client = useFilesystemStore((s) => s.client);
|
|
473
474
|
const { agentId, projectId, isDocker } = props;
|
|
474
475
|
const open = useFilesystemStore((s) => s.workspacePanels.sources);
|
|
475
476
|
const setWorkspacePanelOpen = useFilesystemStore((s) => s.setWorkspacePanelOpen);
|
|
@@ -577,7 +578,7 @@ export function SourcesPanel(props: Props) {
|
|
|
577
578
|
// schema-driven picker (replacing the old hardcoded KIND_META).
|
|
578
579
|
useEffect(() => {
|
|
579
580
|
let cancelled = false;
|
|
580
|
-
listSourceKinds()
|
|
581
|
+
listSourceKinds(client)
|
|
581
582
|
.then((res) => {
|
|
582
583
|
if (cancelled) return;
|
|
583
584
|
setKinds(res.kinds ?? []);
|
|
@@ -704,8 +705,8 @@ export function SourcesPanel(props: Props) {
|
|
|
704
705
|
agentId,
|
|
705
706
|
projectId,
|
|
706
707
|
scope: activeScope,
|
|
707
|
-
}),
|
|
708
|
-
listDiscoverableSources({ agentId, projectId })
|
|
708
|
+
}, client),
|
|
709
|
+
listDiscoverableSources({ agentId, projectId }, client)
|
|
709
710
|
.catch(() => ({ suggestions: [] as DiscoverableSourceSuggestion[] })),
|
|
710
711
|
]);
|
|
711
712
|
setConfigs(next);
|
|
@@ -746,7 +747,7 @@ export function SourcesPanel(props: Props) {
|
|
|
746
747
|
});
|
|
747
748
|
if (alreadyLoading) return;
|
|
748
749
|
try {
|
|
749
|
-
const info = await fetchSourceCounts(agentId, projectId ?? undefined, source);
|
|
750
|
+
const info = await fetchSourceCounts(agentId, projectId ?? undefined, source, client);
|
|
750
751
|
if (info) setCountsBySource((prev) => ({ ...prev, [source]: info }));
|
|
751
752
|
} catch {
|
|
752
753
|
// Count failure is non-fatal — the row still works, the stats row just stays blank.
|
|
@@ -793,7 +794,7 @@ export function SourcesPanel(props: Props) {
|
|
|
793
794
|
useEffect(() => {
|
|
794
795
|
if (!showAddForm || !isDocker || !canMount) return;
|
|
795
796
|
setDockerMountsLoading(true);
|
|
796
|
-
browseDirectories({ path: '/mounts', agentId, ...(projectId ? { projectId } : {}) })
|
|
797
|
+
browseDirectories({ path: '/mounts', agentId, ...(projectId ? { projectId } : {}) }, client)
|
|
797
798
|
.then((result) => {
|
|
798
799
|
// Filter out volumes already configured as sources
|
|
799
800
|
const configuredSources = new Set(configs.map((c) => c.source));
|
|
@@ -905,7 +906,7 @@ export function SourcesPanel(props: Props) {
|
|
|
905
906
|
agentId,
|
|
906
907
|
...(projectId ? { projectId } : {}),
|
|
907
908
|
config: { ...payload, ...(clearSync ? { clearSync: true } : {}) },
|
|
908
|
-
});
|
|
909
|
+
}, client);
|
|
909
910
|
setConfigs((s) => s.map((e) => (e.source === updated.source ? updated : e)));
|
|
910
911
|
setDrafts((s) => ({ ...s, [updated.source]: buildDraft(updated) }));
|
|
911
912
|
// Counted refresh ONLY when the save moved the sync FILTER set — that is
|
|
@@ -943,7 +944,7 @@ export function SourcesPanel(props: Props) {
|
|
|
943
944
|
agentId,
|
|
944
945
|
...(projectId ? { projectId } : {}),
|
|
945
946
|
config: { ...currentDraft, permissions: nextPerms },
|
|
946
|
-
});
|
|
947
|
+
}, client);
|
|
947
948
|
setConfigs((s) => s.map((e) => (e.source === updated.source ? updated : e)));
|
|
948
949
|
setDrafts((s) => ({ ...s, [updated.source]: buildDraft(updated) }));
|
|
949
950
|
// A permissions edit moves no number: the indexed count is a
|
|
@@ -977,7 +978,7 @@ export function SourcesPanel(props: Props) {
|
|
|
977
978
|
// is cleared on UNMOUNT ONLY (never in a dependency-change cleanup), so a
|
|
978
979
|
// live poll is never dropped while the panel is still mounted.
|
|
979
980
|
if (!mountedRef.current) return null;
|
|
980
|
-
const status = await getSyncStatus({ agentId, ...(projectId ? { projectId } : {}), source });
|
|
981
|
+
const status = await getSyncStatus({ agentId, ...(projectId ? { projectId } : {}), source }, client);
|
|
981
982
|
const job = status.job;
|
|
982
983
|
if (!job) return null;
|
|
983
984
|
if (job.status === 'pending' || job.status === 'running') continue;
|
|
@@ -1005,7 +1006,7 @@ export function SourcesPanel(props: Props) {
|
|
|
1005
1006
|
setSyncingSource(source);
|
|
1006
1007
|
setError(null);
|
|
1007
1008
|
try {
|
|
1008
|
-
await triggerSourceSync({ agentId, ...(projectId ? { projectId } : {}), source });
|
|
1009
|
+
await triggerSourceSync({ agentId, ...(projectId ? { projectId } : {}), source }, client);
|
|
1009
1010
|
await pollSyncJob(source);
|
|
1010
1011
|
await load();
|
|
1011
1012
|
await refresh({ agentId, projectId, withCounts: true });
|
|
@@ -1019,7 +1020,7 @@ export function SourcesPanel(props: Props) {
|
|
|
1019
1020
|
|
|
1020
1021
|
const handleCancelSync = async (source?: string) => {
|
|
1021
1022
|
try {
|
|
1022
|
-
await cancelSync({ agentId, ...(projectId ? { projectId } : {}), ...(source ? { source } : {}) });
|
|
1023
|
+
await cancelSync({ agentId, ...(projectId ? { projectId } : {}), ...(source ? { source } : {}) }, client);
|
|
1023
1024
|
// The polling loop in handleSync/handleReindex observes the cancelled
|
|
1024
1025
|
// status and clears the spinner; clear eagerly for instant feedback.
|
|
1025
1026
|
setSyncingSource(null);
|
|
@@ -1058,7 +1059,7 @@ export function SourcesPanel(props: Props) {
|
|
|
1058
1059
|
const handleResumeEmbedding = (source: string) =>
|
|
1059
1060
|
followReindex(
|
|
1060
1061
|
source,
|
|
1061
|
-
() => startReembed({ agentId, ...(projectId ? { projectId } : {}), source }, 'pending'),
|
|
1062
|
+
() => startReembed({ agentId, ...(projectId ? { projectId } : {}), source }, 'pending', client),
|
|
1062
1063
|
'Embedding failed',
|
|
1063
1064
|
);
|
|
1064
1065
|
|
|
@@ -1066,7 +1067,7 @@ export function SourcesPanel(props: Props) {
|
|
|
1066
1067
|
const handleReindex = (source: string) =>
|
|
1067
1068
|
followReindex(
|
|
1068
1069
|
source,
|
|
1069
|
-
() => reindexSource({ agentId, ...(projectId ? { projectId } : {}), source }),
|
|
1070
|
+
() => reindexSource({ agentId, ...(projectId ? { projectId } : {}), source }, client),
|
|
1070
1071
|
'Rescan failed',
|
|
1071
1072
|
);
|
|
1072
1073
|
|
|
@@ -1076,7 +1077,7 @@ export function SourcesPanel(props: Props) {
|
|
|
1076
1077
|
setPurgeBusy(true);
|
|
1077
1078
|
setError(null);
|
|
1078
1079
|
try {
|
|
1079
|
-
const counts = await purgeSourceIndex({ agentId, ...(projectId ? { projectId } : {}), source, selector, mode, obsoleteOnly: purgeObsoleteOnly });
|
|
1080
|
+
const counts = await purgeSourceIndex({ agentId, ...(projectId ? { projectId } : {}), source, selector, mode, obsoleteOnly: purgeObsoleteOnly }, client);
|
|
1080
1081
|
if (epoch !== purgeEpoch.current) return;
|
|
1081
1082
|
if (mode === 'estimate') setPurgeCounts(counts);
|
|
1082
1083
|
else { setPurgeFor(null); setPurgeCounts(null); clearCachedCounts(source); await load(); await refresh({ agentId, projectId, withCounts: true }); }
|
|
@@ -1088,7 +1089,7 @@ export function SourcesPanel(props: Props) {
|
|
|
1088
1089
|
setResettingSource(source);
|
|
1089
1090
|
setError(null);
|
|
1090
1091
|
try {
|
|
1091
|
-
await resetSync({ agentId, ...(projectId ? { projectId } : {}), source });
|
|
1092
|
+
await resetSync({ agentId, ...(projectId ? { projectId } : {}), source }, client);
|
|
1092
1093
|
await load();
|
|
1093
1094
|
} catch (err) {
|
|
1094
1095
|
setError(err instanceof Error ? err.message : 'Reset failed');
|
|
@@ -1107,7 +1108,7 @@ export function SourcesPanel(props: Props) {
|
|
|
1107
1108
|
setTogglingEnabledSource(source);
|
|
1108
1109
|
setError(null);
|
|
1109
1110
|
try {
|
|
1110
|
-
await setSourceEnabled({ agentId, ...(projectId ? { projectId } : {}), source, enabled: next });
|
|
1111
|
+
await setSourceEnabled({ agentId, ...(projectId ? { projectId } : {}), source, enabled: next }, client);
|
|
1111
1112
|
await load();
|
|
1112
1113
|
} catch (err) {
|
|
1113
1114
|
setError(err instanceof Error ? err.message : 'Toggle failed');
|
|
@@ -1125,7 +1126,7 @@ export function SourcesPanel(props: Props) {
|
|
|
1125
1126
|
...(projectId ? { projectId } : {}),
|
|
1126
1127
|
source,
|
|
1127
1128
|
...(opts.purgeVectors ? { purgeVectors: true } : {}),
|
|
1128
|
-
});
|
|
1129
|
+
}, client);
|
|
1129
1130
|
await load();
|
|
1130
1131
|
// The deleted row is gone; no surviving source's count moved, so the
|
|
1131
1132
|
// refresh runs count-free. `clearCachedCounts` still drops the row's own
|
|
@@ -1151,7 +1152,7 @@ export function SourcesPanel(props: Props) {
|
|
|
1151
1152
|
setBrowseLoading(true);
|
|
1152
1153
|
setBrowseError(null);
|
|
1153
1154
|
try {
|
|
1154
|
-
const result = await browseDirectories({ path, agentId, ...(projectId ? { projectId } : {}) });
|
|
1155
|
+
const result = await browseDirectories({ path, agentId, ...(projectId ? { projectId } : {}) }, client);
|
|
1155
1156
|
setBrowseEntries(result.entries);
|
|
1156
1157
|
setBrowsePath(path);
|
|
1157
1158
|
} catch {
|
|
@@ -1212,7 +1213,7 @@ export function SourcesPanel(props: Props) {
|
|
|
1212
1213
|
...(isOwnerEffective && addKind === 'local' && addDraft.recognizesPackages
|
|
1213
1214
|
? { recognizesPackages: true }
|
|
1214
1215
|
: {}),
|
|
1215
|
-
});
|
|
1216
|
+
}, client);
|
|
1216
1217
|
setShowAddForm(false);
|
|
1217
1218
|
{
|
|
1218
1219
|
const reset = buildAddDraft(selectedKind, sourceKeyIds);
|
|
@@ -1672,13 +1673,11 @@ export function SourcesPanel(props: Props) {
|
|
|
1672
1673
|
|
|
1673
1674
|
return (
|
|
1674
1675
|
<div key={fileKey(sourceRootUri(config.source))} className="rounded-sm">
|
|
1675
|
-
{/* Accordion header */}
|
|
1676
|
-
<
|
|
1677
|
-
type="button"
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
className="w-full flex items-center gap-2 py-2 text-left group hover:bg-white/[0.02] rounded-md px-1 transition"
|
|
1681
|
-
>
|
|
1676
|
+
{/* Accordion header: selection and enabled actions are siblings. */}
|
|
1677
|
+
<div className="w-full flex items-center gap-2 py-2 text-left group hover:bg-white/[0.02] rounded-md px-1 transition">
|
|
1678
|
+
<button type="button" onClick={() => toggleSource(config.source)} aria-expanded={isOpen}
|
|
1679
|
+
title={disconnected ? sourceLiveReasonLabel(config.live_reason) : undefined}
|
|
1680
|
+
className="min-w-0 flex-1 flex items-center gap-2">
|
|
1682
1681
|
{/* Git indicator (G2.1) — a GitBranch overlay on a git-repo
|
|
1683
1682
|
source's icon, ORTHOGONAL to the sync tone (visualTone still
|
|
1684
1683
|
owns the icon colour). */}
|
|
@@ -1713,6 +1712,7 @@ export function SourcesPanel(props: Props) {
|
|
|
1713
1712
|
{dirty && (
|
|
1714
1713
|
<span className="shrink-0 rounded px-1 py-0.5 text-[8px] font-medium text-amber-300/80 bg-amber-500/10">unsaved</span>
|
|
1715
1714
|
)}
|
|
1715
|
+
</button>
|
|
1716
1716
|
{/* W3K — enabled toggle (per scope, per source). Machine sources also offer Auto. */}
|
|
1717
1717
|
{(() => {
|
|
1718
1718
|
const enabled = config.enabled ?? true;
|
|
@@ -1763,7 +1763,7 @@ export function SourcesPanel(props: Props) {
|
|
|
1763
1763
|
{describeConn(config, meta)}
|
|
1764
1764
|
</span>
|
|
1765
1765
|
<ChevronDown className={cn('h-3 w-3 text-white/25 shrink-0 transition-transform', isOpen && 'rotate-180')} />
|
|
1766
|
-
</
|
|
1766
|
+
</div>
|
|
1767
1767
|
|
|
1768
1768
|
{/* Expanded body — when open it expands to full height (no clamp)
|
|
1769
1769
|
so every Path Rule / uri-policy is visible; the outer panel
|
|
@@ -7,6 +7,7 @@ import { hasFeature } from '@neuralis/package-system/access';
|
|
|
7
7
|
import { useFilesystemHost } from '../FilesystemHost';
|
|
8
8
|
import { cn } from '../lib/cn';
|
|
9
9
|
import { checkVectorHealth, scanVectorHealth, runHealthAction, getSourcesHealth } from '../api';
|
|
10
|
+
import { useFilesystemStore } from '../store/filesystemStore';
|
|
10
11
|
|
|
11
12
|
type Props = {
|
|
12
13
|
agentId: string;
|
|
@@ -73,6 +74,7 @@ const ACTION_COPY: Record<ActionKind, { label: string; explainer: string; toolti
|
|
|
73
74
|
};
|
|
74
75
|
|
|
75
76
|
export function VectorHealthPanel({ agentId, projectId }: Props) {
|
|
77
|
+
const client = useFilesystemStore((s) => s.client);
|
|
76
78
|
const host = useFilesystemHost();
|
|
77
79
|
// A mirror only — the server's `drive.write` escalation is the gate.
|
|
78
80
|
const canWrite = hasFeature({ grantedFeatures: [...(host.grantedFeatures ?? [])] }, 'drive.write');
|
|
@@ -108,8 +110,8 @@ export function VectorHealthPanel({ agentId, projectId }: Props) {
|
|
|
108
110
|
// table so it surfaces real "when was this source last synced"
|
|
109
111
|
// info instead of being a count-only view.
|
|
110
112
|
const [data, sourcesHealth] = await Promise.all([
|
|
111
|
-
checkVectorHealth({ agentId, ...(projectId ? { projectId } : {}) }),
|
|
112
|
-
getSourcesHealth({ agentId, ...(projectId ? { projectId } : {}) })
|
|
113
|
+
checkVectorHealth({ agentId, ...(projectId ? { projectId } : {}) }, client),
|
|
114
|
+
getSourcesHealth({ agentId, ...(projectId ? { projectId } : {}) }, client)
|
|
113
115
|
.catch(() => [] as Array<{ source: string; last_sync?: string; sync_status: string }>),
|
|
114
116
|
]);
|
|
115
117
|
const lastSyncBySource = new Map<string, { last_sync?: string; sync_status: string }>();
|
|
@@ -138,7 +140,7 @@ export function VectorHealthPanel({ agentId, projectId }: Props) {
|
|
|
138
140
|
setScanLoading(true);
|
|
139
141
|
setError(null);
|
|
140
142
|
try {
|
|
141
|
-
const result = await scanVectorHealth({ agentId, ...(projectId ? { projectId } : {}) });
|
|
143
|
+
const result = await scanVectorHealth({ agentId, ...(projectId ? { projectId } : {}) }, client);
|
|
142
144
|
setScan({ orphanChunkCount: result.orphanChunkCount ?? 0, duplicateGroups: result.duplicateGroups ?? 0 });
|
|
143
145
|
} catch (err) {
|
|
144
146
|
setError(err instanceof Error ? err.message : 'Scan failed');
|
|
@@ -168,7 +170,7 @@ export function VectorHealthPanel({ agentId, projectId }: Props) {
|
|
|
168
170
|
// we're actually deleting something (`dryRun=false`).
|
|
169
171
|
setPrevReport(!dryRun ? report : null);
|
|
170
172
|
try {
|
|
171
|
-
const result = await runHealthAction({ agentId, ...(projectId ? { projectId } : {}), action, dryRun, retentionHours });
|
|
173
|
+
const result = await runHealthAction({ agentId, ...(projectId ? { projectId } : {}), action, dryRun, retentionHours }, client);
|
|
172
174
|
setActionResult(result);
|
|
173
175
|
if (!dryRun) {
|
|
174
176
|
void fetchReport();
|