@neuralis/brain-core 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/app/README.md +1 -1
- package/app/filesystem/README.md +3 -3
- package/app/filesystem/api/README.md +4 -4
- package/app/filesystem/api/index.ts +144 -139
- 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 +12 -8
- package/app/filesystem/store/filesystemStore.ts +444 -163
- package/app/filesystem/store/gitSlice.ts +6 -6
- package/app/filesystem/store/listSlice.ts +9 -6
- package/app/filesystem/store/searchSlice.ts +3 -3
- package/app/filesystem/ui/FileEditor.tsx +90 -39
- 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 -14
- package/app/filesystem/ui/GitDiffView.tsx +14 -6
- package/app/filesystem/ui/GitFileDiff.tsx +22 -15
- package/app/filesystem/ui/PendingChangesPanel.tsx +108 -44
- package/app/filesystem/ui/PendingReviewDialog.tsx +8 -5
- package/app/filesystem/ui/README.md +9 -9
- package/app/filesystem/ui/ReembedDialog.tsx +4 -2
- package/app/filesystem/ui/SourcesPanel.tsx +26 -28
- package/app/filesystem/ui/Toolbar.tsx +5 -4
- package/app/filesystem/ui/VectorHealthPanel.tsx +48 -17
- package/dist/app/host.js +46 -46
- package/dist/src/config/env.d.ts +4 -0
- package/dist/src/config/env.d.ts.map +1 -1
- package/dist/src/config/env.js +13 -0
- package/dist/src/config/env.js.map +1 -1
- package/dist/src/connectors/LocalDiskConnector.d.ts +9 -1
- package/dist/src/connectors/LocalDiskConnector.d.ts.map +1 -1
- package/dist/src/connectors/LocalDiskConnector.js +51 -2
- package/dist/src/connectors/LocalDiskConnector.js.map +1 -1
- package/dist/src/domain/healthTypes.d.ts +1 -7
- package/dist/src/domain/healthTypes.d.ts.map +1 -1
- package/dist/src/domain/pendingChange.d.ts +31 -0
- package/dist/src/domain/pendingChange.d.ts.map +1 -1
- package/dist/src/domain/pendingChange.js.map +1 -1
- package/dist/src/routes/health.js +2 -3
- package/dist/src/routes/health.js.map +1 -1
- package/dist/src/routes/pending.d.ts +7 -7
- package/dist/src/routes/pending.d.ts.map +1 -1
- package/dist/src/routes/pending.js +21 -21
- package/dist/src/routes/pending.js.map +1 -1
- package/dist/src/services/FilesystemService.d.ts +13 -61
- package/dist/src/services/FilesystemService.d.ts.map +1 -1
- package/dist/src/services/FilesystemService.js +47 -189
- package/dist/src/services/FilesystemService.js.map +1 -1
- package/dist/src/services/FilesystemUiService.d.ts +33 -1
- package/dist/src/services/FilesystemUiService.d.ts.map +1 -1
- package/dist/src/services/FilesystemUiService.js +50 -1
- package/dist/src/services/FilesystemUiService.js.map +1 -1
- package/dist/src/store/PendingChangeStore.d.ts +3 -0
- package/dist/src/store/PendingChangeStore.d.ts.map +1 -1
- package/dist/src/store/PendingChangeStore.js +84 -25
- package/dist/src/store/PendingChangeStore.js.map +1 -1
- package/dist/src/usecases/orphanedPending.d.ts +2 -2
- package/dist/src/usecases/orphanedPending.d.ts.map +1 -1
- package/dist/src/usecases/orphanedPending.js +5 -3
- package/dist/src/usecases/orphanedPending.js.map +1 -1
- package/dist/src/usecases/pendingListing.d.ts +34 -0
- package/dist/src/usecases/pendingListing.d.ts.map +1 -0
- package/dist/src/usecases/pendingListing.js +244 -0
- package/dist/src/usecases/pendingListing.js.map +1 -0
- package/dist/src/usecases/vectorMaintenance.d.ts +6 -9
- package/dist/src/usecases/vectorMaintenance.d.ts.map +1 -1
- package/dist/src/usecases/vectorMaintenance.js +4 -13
- package/dist/src/usecases/vectorMaintenance.js.map +1 -1
- package/dist/src/vector/InMemoryVectorIndex.d.ts.map +1 -1
- package/dist/src/vector/InMemoryVectorIndex.js +30 -9
- package/dist/src/vector/InMemoryVectorIndex.js.map +1 -1
- package/package.json +27 -5
- package/rules/brain-rules.md +1 -1
- package/skills/create-package/SKILL.md +3 -3
- package/skills/create-package/references/config-and-credentials.md +1 -1
- package/skills/create-package/references/manifest-and-discovery.md +1 -1
- package/skills/create-package/references/runtime-and-build.md +1 -1
- package/skills/create-package/references/skills-and-context.md +25 -12
- package/skills/create-package/references/surfaces-and-design.md +7 -5
- package/skills/create-package/references/test-and-ship.md +9 -16
- package/skills/create-package/scripts/check-package.sh +47 -1
- package/skills/create-package/scripts/verify-loaded.sh +18 -27
- package/skills/git/SKILL.md +27 -29
- package/skills/manage-files/SKILL.md +37 -32
- package/skills/manage-files/scripts/upload.sh +7 -9
- package/skills/manage-files/scripts/write.sh +2 -6
- package/skills/manage-sources/SKILL.md +93 -95
- package/skills/manage-sources/scripts/reembed.sh +2 -6
- package/skills/manage-uri-policy/SKILL.md +30 -58
- package/app/filesystem/hooks/useContainerWidth.ts +0 -39
- package/skills/git/scripts/git.sh +0 -48
- package/skills/manage-files/scripts/approve.sh +0 -9
- package/skills/manage-files/scripts/list.sh +0 -9
- package/skills/manage-files/scripts/pending.sh +0 -23
- package/skills/manage-files/scripts/raw.sh +0 -11
- package/skills/manage-files/scripts/read.sh +0 -9
- package/skills/manage-files/scripts/search.sh +0 -15
- package/skills/manage-sources/scripts/browse.sh +0 -12
- package/skills/manage-sources/scripts/cancel.sh +0 -14
- package/skills/manage-sources/scripts/create.sh +0 -13
- package/skills/manage-sources/scripts/delete.sh +0 -17
- package/skills/manage-sources/scripts/discoverable.sh +0 -6
- package/skills/manage-sources/scripts/get.sh +0 -7
- package/skills/manage-sources/scripts/kind-schema.sh +0 -7
- package/skills/manage-sources/scripts/kinds.sh +0 -6
- package/skills/manage-sources/scripts/list.sh +0 -6
- package/skills/manage-sources/scripts/purge-index.sh +0 -18
- package/skills/manage-sources/scripts/reindex.sh +0 -13
- package/skills/manage-sources/scripts/reset.sh +0 -11
- package/skills/manage-sources/scripts/set-enabled.sh +0 -17
- package/skills/manage-sources/scripts/sync-status.sh +0 -14
- package/skills/manage-sources/scripts/sync.sh +0 -13
- package/skills/manage-sources/scripts/update.sh +0 -18
- package/skills/manage-uri-policy/scripts/manifests.sh +0 -20
- package/skills/manage-uri-policy/scripts/set-uri.sh +0 -66
- package/skills/manage-uri-policy/scripts/show-uri.sh +0 -23
|
@@ -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
|
|
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
|
|
54
|
-
* own them), `
|
|
55
|
-
* `
|
|
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.
|
|
@@ -77,6 +76,7 @@ export function contextResetPatch(): Record<string, unknown> {
|
|
|
77
76
|
pendingReviewPaths: new Set<string>(),
|
|
78
77
|
// The Changes badge count is the context's own server answer.
|
|
79
78
|
pendingCount: 0,
|
|
79
|
+
pendingCountState: 'stale',
|
|
80
80
|
// Nothing is pre-expanded — under lazy hydration an expanded entry is a
|
|
81
81
|
// FETCH, and every source branch (including `brain://`) starts collapsed.
|
|
82
82
|
expandedFolders: new Set<string>(),
|
|
@@ -230,6 +230,9 @@ export function createListSlice(
|
|
|
230
230
|
set: (fn: (s: any) => any) => void,
|
|
231
231
|
get: () => any,
|
|
232
232
|
): ListSliceActions {
|
|
233
|
+
// The root-listing dedup belongs to ONE store (one Files window): two windows
|
|
234
|
+
// listing the same agent must each get their own answer into their own store.
|
|
235
|
+
const listInFlight = new Map<string, Promise<void>>();
|
|
233
236
|
return {
|
|
234
237
|
async loadList({ agentId, projectId, userId, withCounts }) {
|
|
235
238
|
if (!agentId) return;
|
|
@@ -312,7 +315,7 @@ export function createListSlice(
|
|
|
312
315
|
sources_only: true,
|
|
313
316
|
recursive: false,
|
|
314
317
|
...(withCounts ? { include_counts: true } : {}),
|
|
315
|
-
});
|
|
318
|
+
}, get().client);
|
|
316
319
|
} catch {
|
|
317
320
|
set(() => ({ snapshotError: 'Filesystem service unavailable', sourcesLoading: false }));
|
|
318
321
|
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 '
|
|
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';
|
|
@@ -21,6 +21,10 @@ import { getRawFileUrl, approveFileReview, writeFile, writeRefusalMessage, getGi
|
|
|
21
21
|
import { InlineUploader } from './InlineUploader';
|
|
22
22
|
import { SourcesPanel } from './SourcesPanel';
|
|
23
23
|
import { PackagesPanel } from './PackagesPanel';
|
|
24
|
+
import { PendingReviewDialog, type ReviewRequest, type ReviewAction } from './PendingReviewDialog';
|
|
25
|
+
import { useVisibleDiffCards } from '../hooks/useVisibleDiffCards';
|
|
26
|
+
import { normalizePolicyUri } from '@neuralis/package-system/policies';
|
|
27
|
+
import { listPendingChanges } from '../api';
|
|
24
28
|
import { PendingChangesPanel } from './PendingChangesPanel';
|
|
25
29
|
import { getDeltaTone } from '../lib/treeVisualState';
|
|
26
30
|
import { fileKeyFromNode, folderUri, hasCompletedRead, sourceFromUri, extractFilename, sourceRootUri } from '../store/helpers';
|
|
@@ -45,6 +49,7 @@ function normalizeUserUriInput(raw: string, fallbackUri: string): string {
|
|
|
45
49
|
}
|
|
46
50
|
|
|
47
51
|
export function FileEditor() {
|
|
52
|
+
const client = useFilesystemStore((s) => s.client);
|
|
48
53
|
const openFiles = useFilesystemStore((s) => s.openFiles);
|
|
49
54
|
const activeFile = useFilesystemStore((s) => s.activeFile);
|
|
50
55
|
const files = useFilesystemStore((s) => s.files);
|
|
@@ -98,6 +103,7 @@ export function FileEditor() {
|
|
|
98
103
|
const [reverting, setReverting] = useState(false);
|
|
99
104
|
const [showDiff, setShowDiff] = useState(true);
|
|
100
105
|
const [metaExpanded, setMetaExpanded] = useState(false);
|
|
106
|
+
const refreshPendingCount = useFilesystemStore((s) => s.refreshPendingCount);
|
|
101
107
|
const filePrevByPath = useFilesystemStore((s) => s.filePrevByPath);
|
|
102
108
|
const activePrev = activeFile ? filePrevByPath[activeFile] : undefined;
|
|
103
109
|
const hasPendingChanges = !!activePrev && activeNode?.status === 'pending_review';
|
|
@@ -137,6 +143,31 @@ export function FileEditor() {
|
|
|
137
143
|
!showUploadTab && !showSourcesTab && !showPackagesTab && !showPendingTab && !showGitDiff && !showGitCommit;
|
|
138
144
|
const rawAgentId = activeNode?.sourceAgentId ?? agentId ?? undefined;
|
|
139
145
|
const targetAgentId = activeNode?.sourceAgentId ?? agentId ?? null;
|
|
146
|
+
const reviewQueue = useVisibleDiffCards(null);
|
|
147
|
+
const [pendingReview, setPendingReview] = useState<ReviewRequest | null>(null);
|
|
148
|
+
const [pendingReviewError, setPendingReviewError] = useState<string | null>(null);
|
|
149
|
+
const reviewEpoch = useRef(0);
|
|
150
|
+
const pendingReviewEpoch = useRef(0);
|
|
151
|
+
useEffect(() => {
|
|
152
|
+
reviewEpoch.current++; setPendingReview(null); setPendingReviewError(null);
|
|
153
|
+
return () => { reviewEpoch.current++; };
|
|
154
|
+
}, [client, targetAgentId, projectId, activeUri, activeFile]);
|
|
155
|
+
const openPendingReview = useCallback(async (action: ReviewAction) => {
|
|
156
|
+
if (!targetAgentId || !activeUri) return;
|
|
157
|
+
const epoch = reviewEpoch.current;
|
|
158
|
+
setPendingReviewError(null);
|
|
159
|
+
try {
|
|
160
|
+
const page = await listPendingChanges({ agentId: targetAgentId, ...(projectId ? { projectId } : {}), uri: activeUri }, client);
|
|
161
|
+
if (epoch !== reviewEpoch.current) return;
|
|
162
|
+
const records = page.changes.filter(rec => normalizePolicyUri(rec.uri) === normalizePolicyUri(activeUri));
|
|
163
|
+
if (page.partial || page.complete === false || page.restartRequired || records.length !== 1) {
|
|
164
|
+
setPendingReviewError('The pending change is unavailable or ambiguous. Refresh and try again.'); return;
|
|
165
|
+
}
|
|
166
|
+
pendingReviewEpoch.current = epoch;
|
|
167
|
+
setPendingReview({ kind: 'single', rec: records[0]!, action });
|
|
168
|
+
} catch (error) { if (epoch === reviewEpoch.current) setPendingReviewError(error instanceof Error ? error.message : 'Could not load the pending change.'); }
|
|
169
|
+
}, [client, targetAgentId, projectId, activeUri]);
|
|
170
|
+
|
|
140
171
|
const rawUrl = useMemo(() => {
|
|
141
172
|
if (!activeNode?.uri || !rawAgentId) return null;
|
|
142
173
|
return getRawFileUrl(rawAgentId, activeNode.uri, projectId ?? undefined);
|
|
@@ -169,7 +200,7 @@ export function FileEditor() {
|
|
|
169
200
|
}
|
|
170
201
|
void (async () => {
|
|
171
202
|
try {
|
|
172
|
-
const res = await getGitDiff({ agentId: targetAgentId, ...(projectId ? { projectId } : {}), source, path, staged: false });
|
|
203
|
+
const res = await getGitDiff({ agentId: targetAgentId, ...(projectId ? { projectId } : {}), source, path, staged: false }, client);
|
|
173
204
|
if (cancelled) return;
|
|
174
205
|
if (res && (res as { ok?: boolean }).ok === false) {
|
|
175
206
|
setGitLineStatus(undefined);
|
|
@@ -243,7 +274,7 @@ export function FileEditor() {
|
|
|
243
274
|
id: activeNode.id,
|
|
244
275
|
uri: activeNode.uri,
|
|
245
276
|
fields: { status: 'active' },
|
|
246
|
-
});
|
|
277
|
+
}, client);
|
|
247
278
|
if (res && res.ok === false) {
|
|
248
279
|
setEditError(writeRefusalMessage(res, 'Restore failed'));
|
|
249
280
|
return;
|
|
@@ -266,7 +297,7 @@ export function FileEditor() {
|
|
|
266
297
|
const handleApprove = useCallback(async () => {
|
|
267
298
|
if (!targetAgentId || !activeNode || approving) return;
|
|
268
299
|
if (activeNode.status === 'pending_review') {
|
|
269
|
-
|
|
300
|
+
await openPendingReview('approve'); return;
|
|
270
301
|
}
|
|
271
302
|
setApproving(true);
|
|
272
303
|
try {
|
|
@@ -275,7 +306,7 @@ export function FileEditor() {
|
|
|
275
306
|
...(projectId ? { projectId } : {}),
|
|
276
307
|
id: activeNode.id,
|
|
277
308
|
uri: activeNode.uri,
|
|
278
|
-
});
|
|
309
|
+
}, client);
|
|
279
310
|
if (res && res.ok) {
|
|
280
311
|
const nodeKey = activeNode.storeKey || activeFile || fileKeyFromNode(activeNode);
|
|
281
312
|
if (nodeKey) {
|
|
@@ -295,12 +326,12 @@ export function FileEditor() {
|
|
|
295
326
|
} finally {
|
|
296
327
|
setApproving(false);
|
|
297
328
|
}
|
|
298
|
-
}, [targetAgentId, activeNode, approving, projectId, openFile, resolvedAgent?.id, activeFile, patchFileNode, clearFilePrev]);
|
|
329
|
+
}, [openPendingReview, client, targetAgentId, activeNode, approving, projectId, openFile, resolvedAgent?.id, activeFile, patchFileNode, clearFilePrev]);
|
|
299
330
|
|
|
300
331
|
const handleRevert = useCallback(async () => {
|
|
301
332
|
if (!targetAgentId || !activeNode || reverting) return;
|
|
302
333
|
if (activeNode.status === 'pending_review') {
|
|
303
|
-
|
|
334
|
+
await openPendingReview('revert'); return;
|
|
304
335
|
}
|
|
305
336
|
setReverting(true);
|
|
306
337
|
try {
|
|
@@ -310,7 +341,7 @@ export function FileEditor() {
|
|
|
310
341
|
id: activeNode.id,
|
|
311
342
|
uri: activeNode.uri,
|
|
312
343
|
revert: true,
|
|
313
|
-
});
|
|
344
|
+
}, client);
|
|
314
345
|
if (res && res.ok === false) {
|
|
315
346
|
setEditError(writeRefusalMessage(res, 'Revert failed'));
|
|
316
347
|
return;
|
|
@@ -334,7 +365,7 @@ export function FileEditor() {
|
|
|
334
365
|
} finally {
|
|
335
366
|
setReverting(false);
|
|
336
367
|
}
|
|
337
|
-
}, [targetAgentId, activeNode, reverting, projectId, openFile, resolvedAgent?.id, activeFile, activePrev, patchFileNode, clearFilePrev]);
|
|
368
|
+
}, [openPendingReview, client, targetAgentId, activeNode, reverting, projectId, openFile, resolvedAgent?.id, activeFile, activePrev, patchFileNode, clearFilePrev]);
|
|
338
369
|
|
|
339
370
|
useEffect(() => {
|
|
340
371
|
if (!activeNode) {
|
|
@@ -419,6 +450,14 @@ export function FileEditor() {
|
|
|
419
450
|
|
|
420
451
|
return (
|
|
421
452
|
<div className="h-full w-full flex flex-col min-w-0 min-h-0">
|
|
453
|
+
{pendingReviewError ? <div role="alert" className="px-3 py-2 text-xs text-red-300">{pendingReviewError}</div> : null}
|
|
454
|
+
{pendingReview && targetAgentId ? <PendingReviewDialog request={pendingReview} agentId={targetAgentId} projectId={projectId}
|
|
455
|
+
run={reviewQueue.run} onClose={() => setPendingReview(null)} onDone={done => {
|
|
456
|
+
if (pendingReviewEpoch.current !== reviewEpoch.current || !done.some(result => result.outcome === 'ok')) return;
|
|
457
|
+
setPendingReview(null);
|
|
458
|
+
void refreshPendingCount({ agentId: targetAgentId, projectId });
|
|
459
|
+
void openFile({ agentId: targetAgentId, projectId: projectId ?? null, uri: activeUri, force: true });
|
|
460
|
+
}} /> : null}
|
|
422
461
|
{openFiles.length > 0 || workspacePanels.upload || (supportsSources && workspacePanels.sources) || workspacePanels.packages || workspacePanels.pending || gitDiffTarget || gitCommitTarget ? (
|
|
423
462
|
<div
|
|
424
463
|
className="flex shrink-0 items-stretch"
|
|
@@ -434,98 +473,107 @@ export function FileEditor() {
|
|
|
434
473
|
<div {...{ [TAB_SCROLLER_ATTR]: '' }} className="flex min-w-0 flex-1 overflow-x-auto overflow-y-hidden">
|
|
435
474
|
<EditorTabBar />
|
|
436
475
|
{workspacePanels.upload ? (
|
|
476
|
+
<div className="group flex shrink-0 items-center">
|
|
437
477
|
<button
|
|
438
478
|
type="button"
|
|
439
479
|
className={cn(
|
|
440
|
-
'
|
|
480
|
+
'flex items-center gap-1.5 px-3 py-2 text-xs min-w-[120px] max-w-[240px] rounded-lg',
|
|
441
481
|
showUploadTab ? 'bg-white/10 text-white' : 'text-white/50 hover:bg-white/5 hover:text-white/80',
|
|
442
482
|
)}
|
|
443
483
|
onClick={() => setActiveWorkspaceTab('upload')}
|
|
444
484
|
>
|
|
445
485
|
<Upload className="w-3.5 h-3.5 shrink-0" />
|
|
446
486
|
<span className="truncate flex-1">Upload</span>
|
|
447
|
-
|
|
448
|
-
|
|
487
|
+
</button>
|
|
488
|
+
<button type="button" aria-label="Close Upload"
|
|
489
|
+
className="opacity-0 group-hover:opacity-100 focus-visible:opacity-100 p-0.5 rounded hover:bg-white/10"
|
|
449
490
|
onClick={(e) => {
|
|
450
491
|
e.stopPropagation();
|
|
451
492
|
setWorkspacePanelOpen('upload', false);
|
|
452
493
|
}}
|
|
453
494
|
>
|
|
454
495
|
<X className="w-3 h-3" />
|
|
455
|
-
</
|
|
456
|
-
</
|
|
496
|
+
</button>
|
|
497
|
+
</div>
|
|
457
498
|
) : null}
|
|
458
499
|
{supportsSources && workspacePanels.sources ? (
|
|
500
|
+
<div className="group flex shrink-0 items-center">
|
|
459
501
|
<button
|
|
460
502
|
type="button"
|
|
461
503
|
className={cn(
|
|
462
|
-
'
|
|
504
|
+
'flex items-center gap-1.5 px-3 py-2 text-xs min-w-[120px] max-w-[240px] rounded-lg',
|
|
463
505
|
showSourcesTab ? 'bg-white/10 text-white' : 'text-white/50 hover:bg-white/5 hover:text-white/80',
|
|
464
506
|
)}
|
|
465
507
|
onClick={() => setActiveWorkspaceTab('sources')}
|
|
466
508
|
>
|
|
467
509
|
<SlidersHorizontal className="w-3.5 h-3.5 shrink-0" />
|
|
468
510
|
<span className="truncate flex-1">Sources</span>
|
|
469
|
-
|
|
470
|
-
|
|
511
|
+
</button>
|
|
512
|
+
<button type="button" aria-label="Close Sources"
|
|
513
|
+
className="opacity-0 group-hover:opacity-100 focus-visible:opacity-100 p-0.5 rounded hover:bg-white/10"
|
|
471
514
|
onClick={(e) => {
|
|
472
515
|
e.stopPropagation();
|
|
473
516
|
setWorkspacePanelOpen('sources', false);
|
|
474
517
|
}}
|
|
475
518
|
>
|
|
476
519
|
<X className="w-3 h-3" />
|
|
477
|
-
</
|
|
478
|
-
</
|
|
520
|
+
</button>
|
|
521
|
+
</div>
|
|
479
522
|
) : null}
|
|
480
523
|
{workspacePanels.packages ? (
|
|
524
|
+
<div className="group flex shrink-0 items-center">
|
|
481
525
|
<button
|
|
482
526
|
type="button"
|
|
483
527
|
className={cn(
|
|
484
|
-
'
|
|
528
|
+
'flex items-center gap-1.5 px-3 py-2 text-xs min-w-[120px] max-w-[240px] rounded-lg',
|
|
485
529
|
showPackagesTab ? 'bg-white/10 text-white' : 'text-white/50 hover:bg-white/5 hover:text-white/80',
|
|
486
530
|
)}
|
|
487
531
|
onClick={() => setActiveWorkspaceTab('packages')}
|
|
488
532
|
>
|
|
489
533
|
<Package className="w-3.5 h-3.5 shrink-0" />
|
|
490
534
|
<span className="truncate flex-1">Packages</span>
|
|
491
|
-
|
|
492
|
-
|
|
535
|
+
</button>
|
|
536
|
+
<button type="button" aria-label="Close Packages"
|
|
537
|
+
className="opacity-0 group-hover:opacity-100 focus-visible:opacity-100 p-0.5 rounded hover:bg-white/10"
|
|
493
538
|
onClick={(e) => {
|
|
494
539
|
e.stopPropagation();
|
|
495
540
|
setWorkspacePanelOpen('packages', false);
|
|
496
541
|
}}
|
|
497
542
|
>
|
|
498
543
|
<X className="w-3 h-3" />
|
|
499
|
-
</
|
|
500
|
-
</
|
|
544
|
+
</button>
|
|
545
|
+
</div>
|
|
501
546
|
) : null}
|
|
502
547
|
{workspacePanels.pending ? (
|
|
548
|
+
<div className="group flex shrink-0 items-center">
|
|
503
549
|
<button
|
|
504
550
|
type="button"
|
|
505
551
|
className={cn(
|
|
506
|
-
'
|
|
552
|
+
'flex items-center gap-1.5 px-3 py-2 text-xs min-w-[120px] max-w-[240px] rounded-lg',
|
|
507
553
|
showPendingTab ? 'bg-white/10 text-white' : 'text-white/50 hover:bg-white/5 hover:text-white/80',
|
|
508
554
|
)}
|
|
509
555
|
onClick={() => setActiveWorkspaceTab('pending')}
|
|
510
556
|
>
|
|
511
557
|
<FileDiff className="w-3.5 h-3.5 shrink-0" />
|
|
512
558
|
<span className="truncate flex-1">Changes</span>
|
|
513
|
-
|
|
514
|
-
|
|
559
|
+
</button>
|
|
560
|
+
<button type="button" aria-label="Close Changes"
|
|
561
|
+
className="opacity-0 group-hover:opacity-100 focus-visible:opacity-100 p-0.5 rounded hover:bg-white/10"
|
|
515
562
|
onClick={(e) => {
|
|
516
563
|
e.stopPropagation();
|
|
517
564
|
setWorkspacePanelOpen('pending', false);
|
|
518
565
|
}}
|
|
519
566
|
>
|
|
520
567
|
<X className="w-3 h-3" />
|
|
521
|
-
</
|
|
522
|
-
</
|
|
568
|
+
</button>
|
|
569
|
+
</div>
|
|
523
570
|
) : null}
|
|
524
571
|
{gitDiffTarget ? (
|
|
572
|
+
<div className="group flex shrink-0 items-center">
|
|
525
573
|
<button
|
|
526
574
|
type="button"
|
|
527
575
|
className={cn(
|
|
528
|
-
'
|
|
576
|
+
'flex items-center gap-1.5 px-3 py-2 text-xs min-w-[120px] max-w-[240px] rounded-lg',
|
|
529
577
|
showGitDiff ? 'bg-white/10 text-white' : 'text-white/50 hover:bg-white/5 hover:text-white/80',
|
|
530
578
|
)}
|
|
531
579
|
onClick={() => setActiveWorkspaceTab('git-diff')}
|
|
@@ -537,8 +585,9 @@ export function FileEditor() {
|
|
|
537
585
|
<span className="truncate flex-1">
|
|
538
586
|
{repoQualifiedLabel(gitDiffTarget.repo, extractFilename(gitDiffTarget.path) || gitDiffTarget.path)}
|
|
539
587
|
</span>
|
|
540
|
-
|
|
541
|
-
|
|
588
|
+
</button>
|
|
589
|
+
<button type="button" aria-label="Close Git diff"
|
|
590
|
+
className="opacity-0 group-hover:opacity-100 focus-visible:opacity-100 p-0.5 rounded hover:bg-white/10"
|
|
542
591
|
onClick={(e) => {
|
|
543
592
|
e.stopPropagation();
|
|
544
593
|
setGitDiffTarget(null);
|
|
@@ -546,14 +595,15 @@ export function FileEditor() {
|
|
|
546
595
|
}}
|
|
547
596
|
>
|
|
548
597
|
<X className="w-3 h-3" />
|
|
549
|
-
</
|
|
550
|
-
</
|
|
598
|
+
</button>
|
|
599
|
+
</div>
|
|
551
600
|
) : null}
|
|
552
601
|
{gitCommitTarget ? (
|
|
602
|
+
<div className="group flex shrink-0 items-center">
|
|
553
603
|
<button
|
|
554
604
|
type="button"
|
|
555
605
|
className={cn(
|
|
556
|
-
'
|
|
606
|
+
'flex items-center gap-1.5 px-3 py-2 text-xs min-w-[120px] max-w-[240px] rounded-lg',
|
|
557
607
|
showGitCommit ? 'bg-white/10 text-white' : 'text-white/50 hover:bg-white/5 hover:text-white/80',
|
|
558
608
|
)}
|
|
559
609
|
onClick={() => setActiveWorkspaceTab('git-commit')}
|
|
@@ -575,8 +625,9 @@ export function FileEditor() {
|
|
|
575
625
|
? repoQualifiedLabel(gitCommitTarget.repo, `${shortSha(gitCommitTarget.sha)} ${gitCommitTarget.subject}`)
|
|
576
626
|
: repoQualifiedLabel(gitCommitTarget.repo, 'Outgoing changes')}
|
|
577
627
|
</span>
|
|
578
|
-
|
|
579
|
-
|
|
628
|
+
</button>
|
|
629
|
+
<button type="button" aria-label="Close Git commit"
|
|
630
|
+
className="opacity-0 group-hover:opacity-100 focus-visible:opacity-100 p-0.5 rounded hover:bg-white/10"
|
|
580
631
|
onClick={(e) => {
|
|
581
632
|
e.stopPropagation();
|
|
582
633
|
// The 'file' fallback for an ACTIVE git-commit tab lives in
|
|
@@ -585,8 +636,8 @@ export function FileEditor() {
|
|
|
585
636
|
}}
|
|
586
637
|
>
|
|
587
638
|
<X className="w-3 h-3" />
|
|
588
|
-
</
|
|
589
|
-
</
|
|
639
|
+
</button>
|
|
640
|
+
</div>
|
|
590
641
|
) : null}
|
|
591
642
|
</div>
|
|
592
643
|
<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
|
|
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
|
|
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 =
|
|
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 =
|
|
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[].
|