@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.
Files changed (34) hide show
  1. package/app/README.md +1 -1
  2. package/app/filesystem/README.md +3 -3
  3. package/app/filesystem/api/README.md +3 -3
  4. package/app/filesystem/api/index.ts +127 -122
  5. package/app/filesystem/domain/types.ts +12 -0
  6. package/app/filesystem/hooks/README.md +5 -5
  7. package/app/filesystem/hooks/useDiscoverableSources.ts +4 -2
  8. package/app/filesystem/hooks/useFilesystemSSE.ts +31 -23
  9. package/app/filesystem/hooks/useGitChangesInventory.ts +4 -2
  10. package/app/filesystem/host/README.md +3 -2
  11. package/app/filesystem/host/installFilesystemHostComponents.tsx +22 -9
  12. package/app/filesystem/store/README.md +11 -7
  13. package/app/filesystem/store/filesystemStore.ts +434 -161
  14. package/app/filesystem/store/gitSlice.ts +6 -6
  15. package/app/filesystem/store/listSlice.ts +8 -6
  16. package/app/filesystem/store/searchSlice.ts +3 -3
  17. package/app/filesystem/ui/FileEditor.tsx +48 -35
  18. package/app/filesystem/ui/FileTree.tsx +10 -9
  19. package/app/filesystem/ui/GitCommitChangesView.tsx +5 -3
  20. package/app/filesystem/ui/GitCommitGraph.tsx +6 -5
  21. package/app/filesystem/ui/GitControlPanel.tsx +14 -13
  22. package/app/filesystem/ui/GitDiffView.tsx +2 -1
  23. package/app/filesystem/ui/GitFileDiff.tsx +7 -6
  24. package/app/filesystem/ui/PendingChangesPanel.tsx +16 -13
  25. package/app/filesystem/ui/PendingReviewDialog.tsx +8 -5
  26. package/app/filesystem/ui/ReembedDialog.tsx +4 -2
  27. package/app/filesystem/ui/SourcesPanel.tsx +26 -26
  28. package/app/filesystem/ui/VectorHealthPanel.tsx +6 -4
  29. package/dist/app/host.js +46 -46
  30. package/package.json +4 -4
  31. package/skills/create-package/references/manifest-and-discovery.md +1 -1
  32. package/skills/create-package/references/surfaces-and-design.md +7 -5
  33. package/skills/create-package/scripts/check-package.sh +47 -1
  34. 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 '../hooks/useContainerWidth';
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 = useFilesystemStore.getState().files[rec.uri];
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 = useFilesystemStore.getState().sourceChangeRevision; setHeld(null);
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
- <button
1677
- type="button"
1678
- onClick={() => toggleSource(config.source)}
1679
- title={disconnected ? sourceLiveReasonLabel(config.live_reason) : undefined}
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
- </button>
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();