@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
@@ -5,7 +5,7 @@ import { sourceFromUri } from './helpers';
5
5
  /**
6
6
  * gitSlice (G2.1 / spellbound-heron) — client state for the read-only "Git
7
7
  * Control" sidebar panel. Mirrors `searchSlice`'s shape: a plain slice combined
8
- * into the single `useFilesystemStore`, with async actions calling the `../api`
8
+ * into each window's Files store (`createFilesystemStore`), with async actions calling the `../api`
9
9
  * git fetchers.
10
10
  *
11
11
  * The FOCUSED repo is NOT a git-slice field — it is the PAIR
@@ -18,9 +18,6 @@ import { sourceFromUri } from './helpers';
18
18
  * branch repo-correct for free. Write actions pass both explicitly.
19
19
  */
20
20
 
21
- let gitStatusSeq = 0;
22
- let gitBranchSeq = 0;
23
-
24
21
  export interface GitSliceState {
25
22
  gitStatus: GitControlStatus | null;
26
23
  /**
@@ -144,6 +141,9 @@ export function createGitSlice(
144
141
  set: (fn: (s: any) => any) => void,
145
142
  get: () => any,
146
143
  ): GitSliceActions {
144
+ // The newest status / branch request of THIS store — an older answer is dropped.
145
+ let gitStatusSeq = 0;
146
+ let gitBranchSeq = 0;
147
147
  return {
148
148
  setGitViewMode: (mode) => set(() => ({ gitViewMode: mode })),
149
149
 
@@ -196,7 +196,7 @@ export function createGitSlice(
196
196
  ...(projectId ? { projectId } : {}),
197
197
  source: slug,
198
198
  ...(repoRel ? { repo: repoRel } : {}),
199
- });
199
+ }, get().client);
200
200
  if (mySeq !== gitStatusSeq) return;
201
201
  // `totals` rides the SAME response as the rows it sums (the server
202
202
  // computes it after its read filter and cap), so the header can never
@@ -232,7 +232,7 @@ export function createGitSlice(
232
232
  ...(projectId ? { projectId } : {}),
233
233
  source: slug,
234
234
  ...(repoRel ? { repo: repoRel } : {}),
235
- });
235
+ }, get().client);
236
236
  if (mySeq !== gitBranchSeq) return;
237
237
  set(() => ({ gitBranches: Array.isArray(res?.branches) ? res.branches : [] }));
238
238
  } catch (error) {
@@ -8,8 +8,6 @@ import {
8
8
  sourceFromUri,
9
9
  } from './helpers';
10
10
 
11
- const listInFlight = new Map<string, Promise<void>>();
12
-
13
11
  export interface ListSliceActions {
14
12
  /**
15
13
  * Load the SOURCE SUMMARIES — the registry rows the tree headers and the Sources
@@ -50,9 +48,10 @@ export interface ListSliceActions {
50
48
  * context's slice — THESE keys — there and restores it from there; a context
51
49
  * reset must never wipe it, that is the whole point of it),
52
50
  * or the named-exception list — so a new key has to be filed somewhere:
53
- * - preferences, deliberately kept: `activeAgentId`/`activeProjectId` (the callers
54
- * own them), `sidebarOpen`, `sidebarWidth`, `gitViewMode`, `searchMode`,
55
- * `previewEditorsEnabled`, `diffViewMode`;
51
+ * - preferences and window identity, deliberately kept: `activeAgentId`/
52
+ * `activeProjectId` (the callers own them), `windowId` + `client` (the window
53
+ * the store was created for), `sidebarOpen`, `sidebarWidth`, `gitViewMode`,
54
+ * `searchMode`, `previewEditorsEnabled`, `diffViewMode`;
56
55
  * - NOT reset, deliberately: `uploading` alone — it self-heals through the
57
56
  * upload's `finally`, and an upload in flight across a switch must still
58
57
  * release it.
@@ -230,6 +229,9 @@ export function createListSlice(
230
229
  set: (fn: (s: any) => any) => void,
231
230
  get: () => any,
232
231
  ): ListSliceActions {
232
+ // The root-listing dedup belongs to ONE store (one Files window): two windows
233
+ // listing the same agent must each get their own answer into their own store.
234
+ const listInFlight = new Map<string, Promise<void>>();
233
235
  return {
234
236
  async loadList({ agentId, projectId, userId, withCounts }) {
235
237
  if (!agentId) return;
@@ -312,7 +314,7 @@ export function createListSlice(
312
314
  sources_only: true,
313
315
  recursive: false,
314
316
  ...(withCounts ? { include_counts: true } : {}),
315
- });
317
+ }, get().client);
316
318
  } catch {
317
319
  set(() => ({ snapshotError: 'Filesystem service unavailable', sourcesLoading: false }));
318
320
  return;
@@ -1,8 +1,6 @@
1
1
  import { searchFiles } from '../api';
2
2
  import { sourceRootUri } from './helpers';
3
3
 
4
- let searchSeq = 0;
5
-
6
4
  export type SearchSliceHit = {
7
5
  id: string;
8
6
  uri: string;
@@ -95,6 +93,8 @@ export function createSearchSlice(
95
93
  set: (fn: (s: any) => any) => void,
96
94
  get: () => any,
97
95
  ): SearchSliceActions {
96
+ // The newest search of THIS store — an older answer never overwrites a newer one.
97
+ let searchSeq = 0;
98
98
  return {
99
99
  setSearchQuery: (query) => set(() => ({ searchQuery: query })),
100
100
  setSearchMode: (mode) => set(() => ({ searchMode: mode })),
@@ -167,7 +167,7 @@ export function createSearchSlice(
167
167
  ...(searchMode !== 'grep' && filters.createdAfter.trim() ? { created_after: filters.createdAfter.trim() } : {}),
168
168
  ...(searchMode !== 'grep' && filters.createdBefore.trim() ? { created_before: filters.createdBefore.trim() } : {}),
169
169
  limit: searchMode === 'semantic' ? 30 : 50,
170
- });
170
+ }, get().client);
171
171
  if (mySeq !== searchSeq) return;
172
172
  const rows = res.results || [];
173
173
  // Backend error rows (grep guidance, index-unavailable) carry `error`
@@ -6,7 +6,7 @@ import { RefreshCw, FileCode, Pencil, Trash2, Loader2, Upload, Check, X, GitComp
6
6
  import { cn } from '../lib/cn';
7
7
  import { useFilesystemHost } from '../FilesystemHost';
8
8
  import { buildAgentIndex } from '../lib/agentUtils';
9
- import { useContainerWidth } from '../hooks/useContainerWidth';
9
+ import { useContainerWidth } from '@neuralis/package-system/client';
10
10
  import { detectCodeLanguage } from '../lib/detectLanguage';
11
11
  import { parseGitLineStatus, isEmptyGitLineStatus, type GitLineStatusMap } from './codeEditor/gitLineStatus';
12
12
  import { EditorTabBar, EditorTabsMenu, TAB_SCROLLER_ATTR } from './EditorTabBar';
@@ -45,6 +45,7 @@ function normalizeUserUriInput(raw: string, fallbackUri: string): string {
45
45
  }
46
46
 
47
47
  export function FileEditor() {
48
+ const client = useFilesystemStore((s) => s.client);
48
49
  const openFiles = useFilesystemStore((s) => s.openFiles);
49
50
  const activeFile = useFilesystemStore((s) => s.activeFile);
50
51
  const files = useFilesystemStore((s) => s.files);
@@ -169,7 +170,7 @@ export function FileEditor() {
169
170
  }
170
171
  void (async () => {
171
172
  try {
172
- const res = await getGitDiff({ agentId: targetAgentId, ...(projectId ? { projectId } : {}), source, path, staged: false });
173
+ const res = await getGitDiff({ agentId: targetAgentId, ...(projectId ? { projectId } : {}), source, path, staged: false }, client);
173
174
  if (cancelled) return;
174
175
  if (res && (res as { ok?: boolean }).ok === false) {
175
176
  setGitLineStatus(undefined);
@@ -243,7 +244,7 @@ export function FileEditor() {
243
244
  id: activeNode.id,
244
245
  uri: activeNode.uri,
245
246
  fields: { status: 'active' },
246
- });
247
+ }, client);
247
248
  if (res && res.ok === false) {
248
249
  setEditError(writeRefusalMessage(res, 'Restore failed'));
249
250
  return;
@@ -275,7 +276,7 @@ export function FileEditor() {
275
276
  ...(projectId ? { projectId } : {}),
276
277
  id: activeNode.id,
277
278
  uri: activeNode.uri,
278
- });
279
+ }, client);
279
280
  if (res && res.ok) {
280
281
  const nodeKey = activeNode.storeKey || activeFile || fileKeyFromNode(activeNode);
281
282
  if (nodeKey) {
@@ -310,7 +311,7 @@ export function FileEditor() {
310
311
  id: activeNode.id,
311
312
  uri: activeNode.uri,
312
313
  revert: true,
313
- });
314
+ }, client);
314
315
  if (res && res.ok === false) {
315
316
  setEditError(writeRefusalMessage(res, 'Revert failed'));
316
317
  return;
@@ -434,98 +435,107 @@ export function FileEditor() {
434
435
  <div {...{ [TAB_SCROLLER_ATTR]: '' }} className="flex min-w-0 flex-1 overflow-x-auto overflow-y-hidden">
435
436
  <EditorTabBar />
436
437
  {workspacePanels.upload ? (
438
+ <div className="group flex shrink-0 items-center">
437
439
  <button
438
440
  type="button"
439
441
  className={cn(
440
- 'group flex items-center gap-1.5 px-3 py-2 text-xs min-w-[120px] max-w-[240px] rounded-lg',
442
+ 'flex items-center gap-1.5 px-3 py-2 text-xs min-w-[120px] max-w-[240px] rounded-lg',
441
443
  showUploadTab ? 'bg-white/10 text-white' : 'text-white/50 hover:bg-white/5 hover:text-white/80',
442
444
  )}
443
445
  onClick={() => setActiveWorkspaceTab('upload')}
444
446
  >
445
447
  <Upload className="w-3.5 h-3.5 shrink-0" />
446
448
  <span className="truncate flex-1">Upload</span>
447
- <span
448
- className="opacity-0 group-hover:opacity-100 p-0.5 rounded hover:bg-white/10"
449
+ </button>
450
+ <button type="button" aria-label="Close Upload"
451
+ className="opacity-0 group-hover:opacity-100 focus-visible:opacity-100 p-0.5 rounded hover:bg-white/10"
449
452
  onClick={(e) => {
450
453
  e.stopPropagation();
451
454
  setWorkspacePanelOpen('upload', false);
452
455
  }}
453
456
  >
454
457
  <X className="w-3 h-3" />
455
- </span>
456
- </button>
458
+ </button>
459
+ </div>
457
460
  ) : null}
458
461
  {supportsSources && workspacePanels.sources ? (
462
+ <div className="group flex shrink-0 items-center">
459
463
  <button
460
464
  type="button"
461
465
  className={cn(
462
- 'group flex items-center gap-1.5 px-3 py-2 text-xs min-w-[120px] max-w-[240px] rounded-lg',
466
+ 'flex items-center gap-1.5 px-3 py-2 text-xs min-w-[120px] max-w-[240px] rounded-lg',
463
467
  showSourcesTab ? 'bg-white/10 text-white' : 'text-white/50 hover:bg-white/5 hover:text-white/80',
464
468
  )}
465
469
  onClick={() => setActiveWorkspaceTab('sources')}
466
470
  >
467
471
  <SlidersHorizontal className="w-3.5 h-3.5 shrink-0" />
468
472
  <span className="truncate flex-1">Sources</span>
469
- <span
470
- className="opacity-0 group-hover:opacity-100 p-0.5 rounded hover:bg-white/10"
473
+ </button>
474
+ <button type="button" aria-label="Close Sources"
475
+ className="opacity-0 group-hover:opacity-100 focus-visible:opacity-100 p-0.5 rounded hover:bg-white/10"
471
476
  onClick={(e) => {
472
477
  e.stopPropagation();
473
478
  setWorkspacePanelOpen('sources', false);
474
479
  }}
475
480
  >
476
481
  <X className="w-3 h-3" />
477
- </span>
478
- </button>
482
+ </button>
483
+ </div>
479
484
  ) : null}
480
485
  {workspacePanels.packages ? (
486
+ <div className="group flex shrink-0 items-center">
481
487
  <button
482
488
  type="button"
483
489
  className={cn(
484
- 'group flex items-center gap-1.5 px-3 py-2 text-xs min-w-[120px] max-w-[240px] rounded-lg',
490
+ 'flex items-center gap-1.5 px-3 py-2 text-xs min-w-[120px] max-w-[240px] rounded-lg',
485
491
  showPackagesTab ? 'bg-white/10 text-white' : 'text-white/50 hover:bg-white/5 hover:text-white/80',
486
492
  )}
487
493
  onClick={() => setActiveWorkspaceTab('packages')}
488
494
  >
489
495
  <Package className="w-3.5 h-3.5 shrink-0" />
490
496
  <span className="truncate flex-1">Packages</span>
491
- <span
492
- className="opacity-0 group-hover:opacity-100 p-0.5 rounded hover:bg-white/10"
497
+ </button>
498
+ <button type="button" aria-label="Close Packages"
499
+ className="opacity-0 group-hover:opacity-100 focus-visible:opacity-100 p-0.5 rounded hover:bg-white/10"
493
500
  onClick={(e) => {
494
501
  e.stopPropagation();
495
502
  setWorkspacePanelOpen('packages', false);
496
503
  }}
497
504
  >
498
505
  <X className="w-3 h-3" />
499
- </span>
500
- </button>
506
+ </button>
507
+ </div>
501
508
  ) : null}
502
509
  {workspacePanels.pending ? (
510
+ <div className="group flex shrink-0 items-center">
503
511
  <button
504
512
  type="button"
505
513
  className={cn(
506
- 'group flex items-center gap-1.5 px-3 py-2 text-xs min-w-[120px] max-w-[240px] rounded-lg',
514
+ 'flex items-center gap-1.5 px-3 py-2 text-xs min-w-[120px] max-w-[240px] rounded-lg',
507
515
  showPendingTab ? 'bg-white/10 text-white' : 'text-white/50 hover:bg-white/5 hover:text-white/80',
508
516
  )}
509
517
  onClick={() => setActiveWorkspaceTab('pending')}
510
518
  >
511
519
  <FileDiff className="w-3.5 h-3.5 shrink-0" />
512
520
  <span className="truncate flex-1">Changes</span>
513
- <span
514
- className="opacity-0 group-hover:opacity-100 p-0.5 rounded hover:bg-white/10"
521
+ </button>
522
+ <button type="button" aria-label="Close Changes"
523
+ className="opacity-0 group-hover:opacity-100 focus-visible:opacity-100 p-0.5 rounded hover:bg-white/10"
515
524
  onClick={(e) => {
516
525
  e.stopPropagation();
517
526
  setWorkspacePanelOpen('pending', false);
518
527
  }}
519
528
  >
520
529
  <X className="w-3 h-3" />
521
- </span>
522
- </button>
530
+ </button>
531
+ </div>
523
532
  ) : null}
524
533
  {gitDiffTarget ? (
534
+ <div className="group flex shrink-0 items-center">
525
535
  <button
526
536
  type="button"
527
537
  className={cn(
528
- 'group flex items-center gap-1.5 px-3 py-2 text-xs min-w-[120px] max-w-[240px] rounded-lg',
538
+ 'flex items-center gap-1.5 px-3 py-2 text-xs min-w-[120px] max-w-[240px] rounded-lg',
529
539
  showGitDiff ? 'bg-white/10 text-white' : 'text-white/50 hover:bg-white/5 hover:text-white/80',
530
540
  )}
531
541
  onClick={() => setActiveWorkspaceTab('git-diff')}
@@ -537,8 +547,9 @@ export function FileEditor() {
537
547
  <span className="truncate flex-1">
538
548
  {repoQualifiedLabel(gitDiffTarget.repo, extractFilename(gitDiffTarget.path) || gitDiffTarget.path)}
539
549
  </span>
540
- <span
541
- className="opacity-0 group-hover:opacity-100 p-0.5 rounded hover:bg-white/10"
550
+ </button>
551
+ <button type="button" aria-label="Close Git diff"
552
+ className="opacity-0 group-hover:opacity-100 focus-visible:opacity-100 p-0.5 rounded hover:bg-white/10"
542
553
  onClick={(e) => {
543
554
  e.stopPropagation();
544
555
  setGitDiffTarget(null);
@@ -546,14 +557,15 @@ export function FileEditor() {
546
557
  }}
547
558
  >
548
559
  <X className="w-3 h-3" />
549
- </span>
550
- </button>
560
+ </button>
561
+ </div>
551
562
  ) : null}
552
563
  {gitCommitTarget ? (
564
+ <div className="group flex shrink-0 items-center">
553
565
  <button
554
566
  type="button"
555
567
  className={cn(
556
- 'group flex items-center gap-1.5 px-3 py-2 text-xs min-w-[120px] max-w-[240px] rounded-lg',
568
+ 'flex items-center gap-1.5 px-3 py-2 text-xs min-w-[120px] max-w-[240px] rounded-lg',
557
569
  showGitCommit ? 'bg-white/10 text-white' : 'text-white/50 hover:bg-white/5 hover:text-white/80',
558
570
  )}
559
571
  onClick={() => setActiveWorkspaceTab('git-commit')}
@@ -575,8 +587,9 @@ export function FileEditor() {
575
587
  ? repoQualifiedLabel(gitCommitTarget.repo, `${shortSha(gitCommitTarget.sha)} ${gitCommitTarget.subject}`)
576
588
  : repoQualifiedLabel(gitCommitTarget.repo, 'Outgoing changes')}
577
589
  </span>
578
- <span
579
- className="opacity-0 group-hover:opacity-100 p-0.5 rounded hover:bg-white/10"
590
+ </button>
591
+ <button type="button" aria-label="Close Git commit"
592
+ className="opacity-0 group-hover:opacity-100 focus-visible:opacity-100 p-0.5 rounded hover:bg-white/10"
580
593
  onClick={(e) => {
581
594
  e.stopPropagation();
582
595
  // The 'file' fallback for an ACTIVE git-commit tab lives in
@@ -585,8 +598,8 @@ export function FileEditor() {
585
598
  }}
586
599
  >
587
600
  <X className="w-3 h-3" />
588
- </span>
589
- </button>
601
+ </button>
602
+ </div>
590
603
  ) : null}
591
604
  </div>
592
605
  <EditorTabsMenu />
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useFilesystemStore, type SidebarView } from '../store/filesystemStore';
3
+ import { useFilesystemStore, useFilesystemStoreApi, type SidebarView } from '../store/filesystemStore';
4
4
  import React, { useMemo, useState, useCallback, useRef, useEffect } from 'react';
5
5
  import { SkillLauncher } from '@neuralis/package-system/client';
6
6
  import { meetsRequires } from '@neuralis/package-system/access';
@@ -197,6 +197,7 @@ export function flattenSource(
197
197
  export function FileTree(props: { agentId: string; sidebarView?: SidebarView }) {
198
198
  const { agentId, sidebarView = 'tree' } = props;
199
199
  const host = useFilesystemHost();
200
+ const storeApi = useFilesystemStoreApi();
200
201
 
201
202
  const files = useFilesystemStore((s) => s.files);
202
203
  const folderBadges = useFilesystemStore((s) => s.folderBadges);
@@ -527,12 +528,12 @@ export function FileTree(props: { agentId: string; sidebarView?: SidebarView })
527
528
  // Expand + load the parent so the inline input row is visible in place.
528
529
  if (!parentUri.endsWith('://') && !expanded.has(parentUri)) {
529
530
  toggleFolder(parentUri);
530
- void useFilesystemStore.getState().ensureFolderLoaded({ agentId, projectId, uri: parentUri });
531
+ void storeApi.getState().ensureFolderLoaded({ agentId, projectId, uri: parentUri });
531
532
  }
532
533
  setCreating({ parentUri, kind });
533
534
  }
534
535
  },
535
- [contextMenu, menuTargets, expanded, toggleFolder, agentId, projectId, clipboard, pasteNodes],
536
+ [contextMenu, menuTargets, expanded, toggleFolder, agentId, projectId, clipboard, pasteNodes, storeApi],
536
537
  );
537
538
 
538
539
  const handleCreateCommit = useCallback(
@@ -607,12 +608,12 @@ export function FileTree(props: { agentId: string; sidebarView?: SidebarView })
607
608
  setActiveFolderPath(params.key);
608
609
  toggleFolder(params.key);
609
610
  if (wasExpanded) return;
610
- await useFilesystemStore.getState().ensureFolderLoaded({
611
+ await storeApi.getState().ensureFolderLoaded({
611
612
  agentId,
612
613
  projectId,
613
614
  uri: params.key,
614
615
  });
615
- }, [agentId, expanded, projectId, setActiveFolderPath, toggleFolder]);
616
+ }, [agentId, expanded, projectId, setActiveFolderPath, toggleFolder, storeApi]);
616
617
 
617
618
  const handleOpenFile = useCallback((params: { agentId: string; projectId: string | null; uri: string; readAgentId?: string }) => {
618
619
  if (params.uri) {
@@ -644,7 +645,7 @@ export function FileTree(props: { agentId: string; sidebarView?: SidebarView })
644
645
  */
645
646
  const handleRefreshSource = useCallback(async (uriRoot: string) => {
646
647
  if (!agentId) return;
647
- const store = useFilesystemStore.getState();
648
+ const store = storeApi.getState();
648
649
  setSourceBranchExpanded((prev) => (prev[uriRoot] ? prev : { ...prev, [uriRoot]: true }));
649
650
  if (store.loadedFolders?.[uriRoot]) {
650
651
  const source = sourceFromUri(uriRoot);
@@ -652,7 +653,7 @@ export function FileTree(props: { agentId: string; sidebarView?: SidebarView })
652
653
  return;
653
654
  }
654
655
  await store.ensureFolderLoaded({ agentId, projectId, uri: uriRoot, force: true });
655
- }, [agentId, projectId]);
656
+ }, [agentId, projectId, storeApi]);
656
657
 
657
658
  // Hydrate what is VISIBLE and not yet fetched.
658
659
  //
@@ -664,7 +665,7 @@ export function FileTree(props: { agentId: string; sidebarView?: SidebarView })
664
665
  // rather than retry-looping. `ensureFolderLoaded` is itself idempotent.
665
666
  useEffect(() => {
666
667
  if (!agentId) return;
667
- const state = useFilesystemStore.getState();
668
+ const state = storeApi.getState();
668
669
  const wanted: string[] = [];
669
670
  for (const info of sourceInfos) {
670
671
  if (sourceBranchExpanded[info.uri_root]) wanted.push(info.uri_root);
@@ -676,7 +677,7 @@ export function FileTree(props: { agentId: string; sidebarView?: SidebarView })
676
677
  if (hydration && hydration !== 'idle') continue;
677
678
  void state.ensureFolderLoaded({ agentId, projectId, uri });
678
679
  }
679
- }, [agentId, projectId, sourceInfos, sourceBranchExpanded, expanded]);
680
+ }, [agentId, projectId, sourceInfos, sourceBranchExpanded, expanded, storeApi]);
680
681
 
681
682
  const handleMoveFileToFolder = useCallback(
682
683
  async ({ fromUri, id, toFolderUri }: { fromUri: string; id?: string; toFolderUri: string }) => {
@@ -16,6 +16,7 @@ import type { GitCommitFileChange, GitCommitTarget } from '../domain/types';
16
16
  import { useVisibleDiffCards, type VisibleDiffQueue } from '../hooks/useVisibleDiffCards';
17
17
  import { GitFileDiff } from './GitFileDiff';
18
18
  import { shortSha, type GitFileDiffMode } from './gitFileDiffMode';
19
+ import { useFilesystemStore } from '../store/filesystemStore';
19
20
 
20
21
  /**
21
22
  * GitCommitChangesView (G2.4/C2+C3 / cobalt-wren) — the multi-file stacked
@@ -182,6 +183,7 @@ export function GitCommitChangesView({
182
183
  agentId: string | null;
183
184
  projectId: string | null;
184
185
  }): React.ReactElement {
186
+ const client = useFilesystemStore((s) => s.client);
185
187
  const [scrollRoot, setScrollRoot] = useState<HTMLDivElement | null>(null);
186
188
  const queue = useVisibleDiffCards(scrollRoot);
187
189
  const [state, setState] = useState<ListState>({ phase: 'loading' });
@@ -211,7 +213,7 @@ export function GitCommitChangesView({
211
213
  source,
212
214
  ...(repo ? { repo } : {}),
213
215
  sha: target.sha,
214
- });
216
+ }, client);
215
217
  if (cancelled) return;
216
218
  if (res.ok === false) {
217
219
  setState({ phase: 'unavailable' });
@@ -228,7 +230,7 @@ export function GitCommitChangesView({
228
230
  ...(repo ? { repo } : {}),
229
231
  maxCount: 1,
230
232
  ref: target.sha,
231
- });
233
+ }, client);
232
234
  const head = Array.isArray(log?.commits) ? log.commits[0] : undefined;
233
235
  if (!cancelled && head) setMeta({ author: head.author, date: head.date });
234
236
  } catch {
@@ -240,7 +242,7 @@ export function GitCommitChangesView({
240
242
  ...(projectId ? { projectId } : {}),
241
243
  source,
242
244
  ...(repo ? { repo } : {}),
243
- });
245
+ }, client);
244
246
  if (cancelled) return;
245
247
  // 3b: a deep-fail is a typed `{ok:false,'unavailable'}` refusal now — treat
246
248
  // it like no-upstream (nothing to show), never crash on a missing files[].
@@ -431,6 +431,7 @@ export function GitCommitGraph({
431
431
  * E) — two-part `slug:// Name` header render; absent → slug-only fallback. */
432
432
  repoName?: string;
433
433
  }) {
434
+ const client = useFilesystemStore((s) => s.client);
434
435
  const [page, setPage] = useState<GraphPageState>(emptyGraphPageState);
435
436
  const [loading, setLoading] = useState(false);
436
437
  const [error, setError] = useState<string | null>(null);
@@ -508,7 +509,7 @@ export function GitCommitGraph({
508
509
  ...(repo ? { repo } : {}),
509
510
  maxCount,
510
511
  skip,
511
- });
512
+ }, client);
512
513
  if (mySeq !== seqRef.current) return;
513
514
  const fetched = {
514
515
  commits: Array.isArray(res?.commits) ? res.commits : [],
@@ -530,7 +531,7 @@ export function GitCommitGraph({
530
531
  ...(repo ? { repo } : {}),
531
532
  maxCount,
532
533
  skip,
533
- })
534
+ }, client)
534
535
  .then((stats) => {
535
536
  if (mySeq !== seqRef.current) return;
536
537
  setCommitStats((cur) => (mode === 'append' ? { ...cur, ...stats.stats } : { ...stats.stats }));
@@ -643,7 +644,7 @@ export function GitCommitGraph({
643
644
  source,
644
645
  ...(repo ? { repo } : {}),
645
646
  sha,
646
- });
647
+ }, client);
647
648
  if (epoch !== filesEpochRef.current) return;
648
649
  setCommitFiles((prev) =>
649
650
  new Map(prev).set(
@@ -702,7 +703,7 @@ export function GitCommitGraph({
702
703
  ...(projectId ? { projectId } : {}),
703
704
  source,
704
705
  ...(repo ? { repo } : {}),
705
- });
706
+ }, client);
706
707
  if (epoch !== outgoingEpochRef.current) return;
707
708
  // 3b: a deep-fail is a typed `{ok:false,'unavailable'}` refusal now — treat
708
709
  // it like no-upstream (nothing to show), never crash on a missing files[].
@@ -812,7 +813,7 @@ export function GitCommitGraph({
812
813
  ...(projectId ? { projectId } : {}),
813
814
  source,
814
815
  ...(repo ? { repo } : {}),
815
- });
816
+ }, client);
816
817
  if (epoch !== outgoingEpochRef.current) return;
817
818
  // 3b: a deep-fail is a typed `{ok:false,'unavailable'}` refusal now — treat
818
819
  // it like no-upstream (nothing to show), never crash on a missing files[].
@@ -453,6 +453,7 @@ type ConfirmState = { title: string; message: string; confirmLabel: string; onCo
453
453
  type RescueState = { message: string; rescue: GitRescue };
454
454
 
455
455
  export function GitControlPanel({ agentId }: { agentId: string }) {
456
+ const client = useFilesystemStore((s) => s.client);
456
457
  const sources = useFilesystemStore((s) => s.sources);
457
458
  const projectId = useFilesystemStore((s) => s.activeProjectId);
458
459
  const activeSource = useFilesystemStore((s) => s.activeSource);
@@ -554,7 +555,7 @@ export function GitControlPanel({ agentId }: { agentId: string }) {
554
555
  if (cached && (cached instanceof Promise || !opts?.force)) return cached;
555
556
  const task = (async () => {
556
557
  try {
557
- const res = await getGitRepos({ agentId, ...(projectId ? { projectId } : {}), source: slug });
558
+ const res = await getGitRepos({ agentId, ...(projectId ? { projectId } : {}), source: slug }, client);
558
559
  const repos = Array.isArray(res?.repos) ? res.repos : [];
559
560
  repoListsRef.current.set(slug, repos);
560
561
  setRepoLists((prev) => new Map(prev).set(slug, repos));
@@ -799,7 +800,7 @@ export function GitControlPanel({ agentId }: { agentId: string }) {
799
800
  const handleStage = useCallback(
800
801
  (paths: string[]) => {
801
802
  if (paths.length === 0) return;
802
- void doWrite(() => stagePaths({ agentId, ...p, source: selectedSlug, paths }));
803
+ void doWrite(() => stagePaths({ agentId, ...p, source: selectedSlug, paths }, client));
803
804
  },
804
805
  [doWrite, agentId, p, selectedSlug],
805
806
  );
@@ -807,7 +808,7 @@ export function GitControlPanel({ agentId }: { agentId: string }) {
807
808
  const handleUnstage = useCallback(
808
809
  (paths: string[]) => {
809
810
  if (paths.length === 0) return;
810
- void doWrite(() => unstagePaths({ agentId, ...p, source: selectedSlug, paths }));
811
+ void doWrite(() => unstagePaths({ agentId, ...p, source: selectedSlug, paths }, client));
811
812
  },
812
813
  [doWrite, agentId, p, selectedSlug],
813
814
  );
@@ -815,7 +816,7 @@ export function GitControlPanel({ agentId }: { agentId: string }) {
815
816
  const runDiscard = useCallback(
816
817
  (paths: string[]) => {
817
818
  void (async () => {
818
- const res = await doWrite(() => discardPaths({ agentId, ...p, source: selectedSlug, paths }));
819
+ const res = await doWrite(() => discardPaths({ agentId, ...p, source: selectedSlug, paths }, client));
819
820
  if (res && res.rescue) {
820
821
  setRescue({
821
822
  message: `Discarded ${paths.length} file${paths.length === 1 ? '' : 's'} — recoverable`,
@@ -875,7 +876,7 @@ export function GitControlPanel({ agentId }: { agentId: string }) {
875
876
  const message = commitMessage.trim();
876
877
  if (!message) return;
877
878
  void (async () => {
878
- const res = await doWrite(() => commitGit({ agentId, ...p, source: selectedSlug, message, amend }));
879
+ const res = await doWrite(() => commitGit({ agentId, ...p, source: selectedSlug, message, amend }, client));
879
880
  if (res && res.ok) {
880
881
  setCommitMessage('');
881
882
  setAmend(false);
@@ -901,7 +902,7 @@ export function GitControlPanel({ agentId }: { agentId: string }) {
901
902
  (ref: string) => {
902
903
  setBranchPickerOpen(false);
903
904
  void (async () => {
904
- const res = await doWrite(() => checkoutGitBranch({ agentId, ...p, source: selectedSlug, ref }));
905
+ const res = await doWrite(() => checkoutGitBranch({ agentId, ...p, source: selectedSlug, ref }, client));
905
906
  if (res)
906
907
  void loadGitBranches({ agentId, projectId: projectId ?? null, source: selectedSlug, repo: activeRepoRel });
907
908
  })();
@@ -920,10 +921,10 @@ export function GitControlPanel({ agentId }: { agentId: string }) {
920
921
  void (async () => {
921
922
  // Create then switch to it (VS-Code "Create new branch" also checks out).
922
923
  const created = await doWrite(() =>
923
- createGitBranch({ agentId, ...p, source: selectedSlug, name, ...(from ? { startPoint: from } : {}) }),
924
+ createGitBranch({ agentId, ...p, source: selectedSlug, name, ...(from ? { startPoint: from } : {}) }, client),
924
925
  );
925
926
  if (created) {
926
- await doWrite(() => checkoutGitBranch({ agentId, ...p, source: selectedSlug, ref: name }));
927
+ await doWrite(() => checkoutGitBranch({ agentId, ...p, source: selectedSlug, ref: name }, client));
927
928
  void loadGitBranches({ agentId, projectId: projectId ?? null, source: selectedSlug, repo: activeRepoRel });
928
929
  }
929
930
  })();
@@ -932,7 +933,7 @@ export function GitControlPanel({ agentId }: { agentId: string }) {
932
933
  const runDeleteBranch = useCallback(
933
934
  (name: string) => {
934
935
  void (async () => {
935
- const res = await doWrite(() => deleteGitBranch({ agentId, ...p, source: selectedSlug, name }));
936
+ const res = await doWrite(() => deleteGitBranch({ agentId, ...p, source: selectedSlug, name }, client));
936
937
  if (res && res.rescue) {
937
938
  setRescue({ message: `Deleted branch "${name}" — recoverable`, rescue: res.rescue });
938
939
  }
@@ -966,7 +967,7 @@ export function GitControlPanel({ agentId }: { agentId: string }) {
966
967
  if (!rescue) return;
967
968
  const carrier = rescue.rescue;
968
969
  setRescue(null);
969
- void doWrite(() => restoreGit({ agentId, ...p, source: selectedSlug, rescue: carrier }));
970
+ void doWrite(() => restoreGit({ agentId, ...p, source: selectedSlug, rescue: carrier }, client));
970
971
  }, [rescue, doWrite, agentId, p, selectedSlug]);
971
972
 
972
973
  // --- Remote operations (G4 keyring-kestrel Slice 2) ------------------------
@@ -976,17 +977,17 @@ export function GitControlPanel({ agentId }: { agentId: string }) {
976
977
 
977
978
  const handleFetch = useCallback(() => {
978
979
  setRemotePickerOpen(false);
979
- void doWrite(() => fetchGit({ agentId, ...p, source: selectedSlug }));
980
+ void doWrite(() => fetchGit({ agentId, ...p, source: selectedSlug }, client));
980
981
  }, [doWrite, agentId, p, selectedSlug]);
981
982
 
982
983
  const handlePull = useCallback(() => {
983
984
  setRemotePickerOpen(false);
984
- void doWrite(() => pullGit({ agentId, ...p, source: selectedSlug }));
985
+ void doWrite(() => pullGit({ agentId, ...p, source: selectedSlug }, client));
985
986
  }, [doWrite, agentId, p, selectedSlug]);
986
987
 
987
988
  const handlePush = useCallback(() => {
988
989
  setRemotePickerOpen(false);
989
- void doWrite(() => pushGit({ agentId, ...p, source: selectedSlug }));
990
+ void doWrite(() => pushGit({ agentId, ...p, source: selectedSlug }, client));
990
991
  }, [doWrite, agentId, p, selectedSlug]);
991
992
 
992
993
  const unstagedChanges = gitStatus?.unstaged ?? [];
@@ -26,11 +26,12 @@ export function GitDiffView({
26
26
  agentId: string | null;
27
27
  projectId: string | null;
28
28
  }): React.ReactElement {
29
+ const client = useFilesystemStore((s) => s.client);
29
30
  const [canWrite, setCanWrite] = useState(false);
30
31
  const revision = useFilesystemStore(state => state.sourceChangeRevision[target.source] ?? 0);
31
32
  useEffect(() => {
32
33
  let alive = true; setCanWrite(false);
33
- if (agentId) void getGitRepos({ agentId, ...(projectId ? { projectId } : {}), source: target.source }).then(result => {
34
+ if (agentId) void getGitRepos({ agentId, ...(projectId ? { projectId } : {}), source: target.source }, client).then(result => {
34
35
  if (alive) setCanWrite(result.repos.find(repo => repo.rel === (target.repo ?? ''))?.canWrite === true);
35
36
  }).catch(() => { if (alive) setCanWrite(false); });
36
37
  return () => { alive = false; };