@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
|
@@ -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
|
|
@@ -25,9 +25,9 @@ Re-exported from `app/filesystem/ui/index.ts`:
|
|
|
25
25
|
- **`DiffModeToggle`** is the ONE Auto/Unified/Split control bound to the store `diffViewMode`, shared by BOTH the review diff AND the git diff (Auto picks unified/split by container width via `useContainerWidth`).
|
|
26
26
|
- The editor also carries the VS Code-style **git left-gutter** (`codeEditor/editorGitGutter.ts` + the pure `codeEditor/gitLineStatus.ts` parser) for git-tracked files — a thin emerald(added)/blue(modified) bar + red delete caret, derived **fail-soft** from `getGitDiff({staged:false})` (Working↔Index).
|
|
27
27
|
- `DiffView` (the old hand-rolled Myers review diff) was **DELETED** — superseded by `ReviewDiffView`. NB `app/cards/DiffView.tsx` is a DIFFERENT file (the chat-card single-column diff) and stays.
|
|
28
|
-
- `Toolbar` — top toolbar with the active agent’s standalone configured icon (no visible name or identity badge), sidebar toggle, refresh, upload, and panel switches. The Changes switch carries the caller's visible pending COUNT as a static badge (`pendingCount`, the server `count_only` answer — `1000+`
|
|
28
|
+
- `Toolbar` — top toolbar with the active agent’s standalone configured icon (no visible name or identity badge), sidebar toggle, refresh, upload, and panel switches. The Changes switch carries the caller's visible pending COUNT as a static badge (`pendingCount`, the server `count_only` answer — `1000+` display cap; partial totals carry `+`, stale/unavailable totals `?`); the client `pendingReviewPaths` set only tones tree rows.
|
|
29
29
|
- `SourcesPanel`, `PackagesPanel`, `VectorHealthPanel`, `PanelScaffold` — right-hand panels. Sources offers advisory metrics exclusions only when enabling an attested own data source with no sync block; Save persists the editable draft. Media summaries include local structural PNG failures, and chunk-only cleanup refreshes Files separately from removed-file counts.
|
|
30
|
-
- `PendingChangesPanel` — Changes groups current Git status and pending review by source/repository, including non-Git sources. Normalized URI joining renders a file once while retaining separately labeled staged/unstaged comparisons for each overlapping repository and one pending review. All cards start expanded in ONE `useVirtualizer` list (`pendingChangesModel.ts` owns the filters, the flat row list, `rowEstimate`/`isMeasuredRow` and the Approve split; the compact row pins `h-8`, every card is measured; `overflow-anchor:none` plus an above-the-fold size adjustment keep the view still); the content lanes cache every loaded preview/diff, and source revisions plus context sequences fence obsolete replies. A **Type** filter (all · pending · Git) narrows the layers; the repository picker uses the Git panel's `resolveRepoLabel`. **Approve N** counts records with no acknowledgement on their own fields (`ackHintFor`, the server preview's own predicate) or in an already-loaded preview (`splitApprovable`'s `knownPreview`, no fetch of its own); the rest open `PendingReviewDialog` grouped by reason, each record sent with its own fingerprint, and every sweep reports a per-record result row (a refused revert included) — a record the sweep found needing an acknowledgement is reported AND handed to that grouped dialog (approve or dismiss with the acknowledgement). A batch (the sweep, a dialog group) HOLDS the source revisions its own writes bump (`holdRevisions`, released in `afterMutation`): N writes cost N verb requests, ONE re-list and at most one re-preview per card still listed — never a re-list plus every visible card's re-preview per write. A `changed` answer (`PENDING_STATE_CHANGED` / `PENDING_GENERATION_CHANGED`) is not final: the dialog fetches ONE fresh preview for that record (per-record sequence, liveness cleared on unmount only) and waits for a new click — never a replay. In the panel that ONE fetch is the panel's `refetch`: it waits for the re-list, the record's card holds meanwhile and adopts the answer under its final key (`takeRefetch`), so the card sends nothing of its own; a direct (dialog-less) refusal bumps only that card's preview nonce. A confirmed absence with a saved baseline is named in the preview body (`acceptsAbsence`). The orphan probe runs on mount, Refresh and Repair only — a source revision re-reads the plain list. Source inventory is separate from content, completed siblings are cached, truncation is shown, and Git errors do not hide other sources. Review reads a bounded pending preview/fingerprint with typed current/baseline/index states; ack-required state opens an explicit confirmation. Actual WRITE access gates card, compact, bulk and repair controls; historical cards remain read-only. Orphan ids arrive with the same pending listing, so Repair remains orphan-only and re-authorized server-side.
|
|
30
|
+
- `PendingChangesPanel` — Changes groups current Git status and pending review by source/repository, including non-Git sources. Normalized URI joining renders a file once while retaining separately labeled staged/unstaged comparisons for each overlapping repository and one pending review. All cards start expanded in ONE `useVirtualizer` list (`pendingChangesModel.ts` owns the filters, the flat row list, `rowEstimate`/`isMeasuredRow` and the Approve split; the compact row pins `h-8`, every card is measured; `overflow-anchor:none` plus an above-the-fold size adjustment keep the view still); the content lanes cache every loaded preview/diff, and source revisions plus context sequences fence obsolete replies. A **Type** filter (all · pending · Git) narrows the layers; the repository picker uses the Git panel's `resolveRepoLabel`. **Approve N** counts records with no acknowledgement on their own fields (`ackHintFor`, the server preview's own predicate) or in an already-loaded preview (`splitApprovable`'s `knownPreview`, no fetch of its own); the rest open `PendingReviewDialog` grouped by reason, each record sent with its own fingerprint, and every sweep reports a per-record result row (a refused revert included) — a record the sweep found needing an acknowledgement is reported AND handed to that grouped dialog (approve or dismiss with the acknowledgement). A batch (the sweep, a dialog group) HOLDS the source revisions its own writes bump (`holdRevisions`, released in `afterMutation`): N writes cost N verb requests, ONE re-list and at most one re-preview per card still listed — never a re-list plus every visible card's re-preview per write. A `changed` answer (`PENDING_STATE_CHANGED` / `PENDING_GENERATION_CHANGED`) is not final: the dialog fetches ONE fresh preview for that record (per-record sequence, liveness cleared on unmount only) and waits for a new click — never a replay. In the panel that ONE fetch is the panel's `refetch`: it waits for the re-list, the record's card holds meanwhile and adopts the answer under its final key (`takeRefetch`), so the card sends nothing of its own; a direct (dialog-less) refusal bumps only that card's preview nonce. A confirmed absence with a saved baseline is named in the preview body (`acceptsAbsence`). The orphan probe runs on mount, Refresh and Repair only — a source revision re-reads the plain list. Source inventory is separate from content, completed siblings are cached, truncation is shown, and Git errors do not hide other sources. Review reads a bounded pending preview/fingerprint with typed current/baseline/index states; ack-required state opens an explicit confirmation. Actual WRITE access gates card, compact, bulk and repair controls; historical cards remain read-only. Orphan ids arrive with the same pending listing, so Repair remains orphan-only and re-authorized server-side. Changes exposes Load more and incomplete inventory state. Repair processes batches sequentially with progress and Stop after the current batch; partial pages pause at their cursor for explicit Continue available/Retry. List and orphan epochs are independent; classification survives a plain relist only for matching record generations. FileEditor opens the shared review dialog for the exact visible URI. Returned generation conflicts appear in the existing Changes row status before preview succeeds. Git hunk rows retain geometry during same-context permission refresh while actions remain disabled until permission and diff settle.
|
|
31
31
|
|
|
32
32
|
- `InlineUploader` — the Upload tab (folder pick, new folder, drop zone). The two modals it once sat beside (`EditFileModal`, `UploadFileModal`) were deleted 2026-09-04: nothing had mounted either since the repository's baseline, so the settling-sandpiper M9 reconcile tails had been fixed on dead code while the live in-place Edit still called the upsert-only `refresh`. Those tails are store actions now (`saveFileInPlace` / `markFileDeleted`, `../store/README.md`) that `FileEditor` calls.
|
|
33
33
|
- `GitControlPanel`, `StackedSections` (+ pure `stackedSectionsLayout.ts`), `GitCommitGraph` (+ pure `gitGraphLayout.ts`/`gitGraphPaging.ts`/`gitGraphItems.ts` — the item model: expandable rows, the outgoing divider, ref-chip/dot classing, the branch-bar ahead/behind chips), `GitDiffView` + `GitFileDiff` + `GitCommitChangesView` (re-exported; + pure `gitDiffExpand.ts`/`gitDiffPatch.ts`/`gitFileDiffMode.ts`), `GitStatusBadge`, `DiffCountBadge` — the Git Control surface: the sidebar pane-stack panel, the in-panel virtualized commit graph with commit drill-in + outgoing markers, the side-by-side diff view (single + multi-file stacked tab), the per-file tree badge. The owning narrative (sections, paging/live-refresh, drill-in, outgoing, caps, write gating) is the package-root README "Git Control panel" section — this bullet only anchors the files.
|
|
@@ -39,7 +39,7 @@ Re-exported from `app/filesystem/ui/index.ts`:
|
|
|
39
39
|
- `PolicyEditor` — compact admin-style permissions editor for the source-config panel. Renders the persisted JSON tree (`default`, `byRole`, `byUser`, `byAgent`, `paths[]`) with tri-state R/W/X pill badges (allow / deny / inherit cycle), per-role/user/agent override rows (with `+ Add` input + trash), and path rules with clickable role chips + per-rule agent chips (`$self/agentId`, inline-add). `OVR` toggles the rule's `override` flag. Adding a rule **prepends** it (visible at the top of long policy sets; first-match-wins means the fresh rule also takes precedence). Two modes: `edit` (Save button visible when dirty) and `view` (read-only chat-config render). It is wired into `SourcesPanel`, laid out admin-style, and carries the By Role / By User / By Agent sections. Save threads through `handlePolicySave` → `updateSourceConfig` without disturbing the user's pending sync/credential edits.
|
|
40
40
|
- `SourcesPanel` "Discoverable" section — lives INSIDE the Vector Health expandable, alongside a `mount.mts` explainer card. Suggestions arrive from `GET /sources/discoverable` and use the shared `dimmedSourceTone('unattached')` util. The icon name (`AppWindow`, `GitBranch`, `FolderTree`, …) resolves through the shared `resolveLucideIcon` helper (same `ICON_MAP` as the workspace dock-left). Click pre-fills the add-source form with `{ key, path, kind, hostRoot? }` from the suggestion, plus the suggestion's optional `suggestedAccess` (read/write/exec) which overrides the connector-kind default, and — for `seed:'discoverable'` package declarations (e.g. the admin app zone, suggestion id `discover:decl:app`) — the **declared `description`** prefills the add form's description field. The **Full Host** suggestion (native/non-Docker only — `/` is the readable host root; in Docker the host must first be bound via `pnpm neuralis:mount add /`) ships `suggestedAccess: { read:true, write:false, exec:false }` so the whole-host root attaches **read-only by default** — the owner widens it (or narrows the uri-policy) in the form before saving. A host source is readable from the container shell, but host-native binaries still won't execute under the container runtime (a host PTY broker is a separate, future capability).
|
|
41
41
|
- `SourcesPanel` row actions say what they do: **Rescan changes** (the `reindex` route without `force` — changed files are embedded again, unchanged ones keep their vectors), **Re-embed…** (`ReembedDialog` — every file of the source with the active model, estimate first), **Embed pending…** (`ReembedDialog kind="pending"`; it and **Re-embed…** are disabled while the row's live `embedding_enabled` says off), **Forget sync state** (the `reset` route — the next sync compares every file again; deletes no file and no vector). A started re-embed is followed by the same job poll + count refresh as a rescan (`followReindex`); an embedding pass that ends `paused` (terminal, not failed) shows "Embedding paused: <reason>" with **Resume** (the same owed-only apply). The Modify-source body states `binarySkipLine` ("N binary files not indexed" — the last count a pass measured) and `embeddingLine` beside `mediaSkipLine`: off — the live `embedding_enabled`, never the last pass — reads neutral ("Semantic embedding off — text and field search only"); the last pass's other reasons read amber ("Semantic embedding pending: <reasons>").
|
|
42
|
-
- `SourcesPanel` row is **name-first** (the redesign for the always-narrow widget). The source name is the dominant element (`flex-1`, larger/bold); the kind icon uses the SAME resolved icon set as the file tree (`resolveIconWithFallback` over the connector-manifest `kind_icon`/`meta.icon`) and is **coloured by `getSourceVisualTone`** — so the icon tone IS the sync/live signal, matching the tree (the old `Healthy/Idle/Error` status text + `statusTone` are gone). The per-row **trust badge** (`first-party`/…) and the **scope badge** (`project`/`user`/`agent` — already a tab filter above) are removed; only `observed`, `orphan`, `unsaved`, and the enabled toggle remain inline. Rows are **grouped by `origin_package`** — the **declaring package id** the server resolves per source (the CONTRACT-BACKED `PackageSourceDeclaration` owner, returned on each `SourceRuntimeSummary` / `s.origin_package`), one group **per package**: each real `@neuralis/*` (or any other) package sorts **alphabetically** and user-added mounts collapse into a single **`Mounted (user-added)`** group that sorts **LAST** (`sourceOriginGroup` / `MOUNTED_GROUP`). The small left-indented group header **replaces** the per-row trust/`first-party` badge. The old client-side heuristic that inferred the owner from the connector kind's npm scope (`sourceOriginLabel`) is **gone** — attribution is now resolved server-side from the declarations (`matchDeclaredSource`, slug + resolved-root for rooted kinds; kind-only for rootless sandbox kinds), so a user mount named `app` that is NOT rooted at the platform app zone is NOT attributed to admin — the grouping is **unspoofable**.
|
|
42
|
+
- `SourcesPanel` row is **name-first** (the redesign for the always-narrow widget). The source name stays left-aligned and is the dominant element (`flex-1`, larger/bold); the kind icon uses the SAME resolved icon set as the file tree (`resolveIconWithFallback` over the connector-manifest `kind_icon`/`meta.icon`) and is **coloured by `getSourceVisualTone`** — so the icon tone IS the sync/live signal, matching the tree (the old `Healthy/Idle/Error` status text + `statusTone` are gone). The per-row **trust badge** (`first-party`/…) and the **scope badge** (`project`/`user`/`agent` — already a tab filter above) are removed; only `observed`, `orphan`, `unsaved`, and the enabled toggle remain inline. Rows are **grouped by `origin_package`** — the **declaring package id** the server resolves per source (the CONTRACT-BACKED `PackageSourceDeclaration` owner, returned on each `SourceRuntimeSummary` / `s.origin_package`), one group **per package**: each real `@neuralis/*` (or any other) package sorts **alphabetically** and user-added mounts collapse into a single **`Mounted (user-added)`** group that sorts **LAST** (`sourceOriginGroup` / `MOUNTED_GROUP`). The small left-indented group header **replaces** the per-row trust/`first-party` badge. The old client-side heuristic that inferred the owner from the connector kind's npm scope (`sourceOriginLabel`) is **gone** — attribution is now resolved server-side from the declarations (`matchDeclaredSource`, slug + resolved-root for rooted kinds; kind-only for rootless sandbox kinds), so a user mount named `app` that is NOT rooted at the platform app zone is NOT attributed to admin — the grouping is **unspoofable**.
|
|
43
43
|
- `SourcesPanel` source body: when a row is expanded it renders at **full height** (no `max-h` clamp) so every Path Rule / uri-policy is visible and the outer panel scrolls — the closed state keeps the collapse animation. The Sync sub-section shows a per-trigger explainer (Manual = only the Sync button; Auto = file-watch + reconcile; Scheduled = interval) and clarifies the "Always active" pin. Disabling sync (unchecking "Enable sync" + Save) sends `clearSync: true` — an omitted `sync` means "no change" server-side, so without the flag the disable would silently revert.
|
|
44
44
|
- `SourcesPanel` per-source counts are **lazy** — the exact `connector` / `indexed` / `unindexed` stats row is fetched ONLY when its row is opened (`toggleSource` → `fetchSourceCounts` → a list scoped to `<source>://` with `include_counts`, so `computeSourcesRegistry` walks that ONE source), cached in `countsBySource`, and shown behind a "Loading counts…" placeholder. Panel-open itself does a **count-FREE** `refresh()` (no `withCounts`) — that still populates `sourceSummaries` (`s.sources`) for source discovery (observed-only sources, project scope) + per-row connector liveness dim, but fires **zero** project-wide count walks. There is deliberately no eager `refresh({withCounts:true})` on open — that, together with the Vector Health walk below, is what used to make opening one panel slow everything. Counted refreshes are narrowed to the operations that actually MOVE a number: sync, reindex, and a config save that changed the sync FILTER set (`syncFilterChanged` — exclude/include/`maxScanFiles`/a cleared sync block, which `connector.count()` applies, so the on-disk total shifts at once). A permissions save, a delete and an add refresh count-FREE: the indexed count is a project-scoped `countByFilter` with no per-caller policy input, the on-disk count is physical truth, a deleted row is gone and a brand-new row's numbers are one expand away. Every counted path also evicts that source's cached counts (`clearCachedCounts`) so the fresh summary is what renders — the lazy cache used to shadow it indefinitely after an exclude edit. `pollSyncJob` (1.5 s × `MAX_POLLS` 2400 = a sixty-minute ceiling) checks a `mountedRef` each turn and returns the moment the panel unmounts; the ref is cleared on UNMOUNT ONLY, never in a dependency-change cleanup, so a live poll is never dropped while the panel is still there. And EVERY hook in `SourcesPanel` sits above its `if (!open) return null` guard: two `useCallback`s used to live below it, so the component rendered a different number of hooks open than closed — React refuses that on one instance, and only the parent's mount gate carrying the same `workspacePanels.sources` flag kept it from ever firing.
|
|
45
45
|
- `useFilesystemSSE` `sync:completed`: the handler is matched **before** the `if (!uri) return` guard — the event carries `source` (the slug), NOT a file `uri`. It runs the authoritative `reconcileSourceFolders` over that source's VISIBLE folders (a sync can have removed files), skips the pass entirely when every change count on the payload is zero (a missing count is unknown and still re-lists), and falls back to `refreshTree` only when no source is present. That pass rides the ONE per-source reconcile schedule shared with external-change events (a trailing quiet window with a max-wait), so a burst of changes becomes one pass. Owner: `app/filesystem/hooks/README.md`.
|
|
@@ -53,13 +53,13 @@ Re-exported from `app/filesystem/ui/index.ts`:
|
|
|
53
53
|
|
|
54
54
|
- `SourceDeleteConfirm` — inline rose-tinted popover under the row (no modal, matches the panel's "no overlay" pattern). Headline + one-line reminder that the host folder is not touched + `[ ] Purge vector index entries (~N point(s))` checkbox (default OFF — conservative) + Docker-bind-mount warning ribbon when `connection.config.hostRoot` is set, telling the user to run `neuralis mount remove <slug>` before restarting the container. On confirm, `SourcesPanel.handleDelete` calls `deleteSource({ purgeVectors })` and surfaces a transient `successMsg` ("Deleted "<source>" (vectors purged: N)").
|
|
55
55
|
- **`SkillLauncher` coverage** (`@neuralis/package-system/client`, additive "+" launchers) — every filesystem function the LLM can drive has a visible composer-prefill trace. Clicking a launcher PREFILLS the chat composer with the skill (+ scripts) like a `/` slash pick; it never sends/streams. **No launcher states a feature list**: visibility is DERIVED from the host's per-caller skill catalog, so it cannot drift from each skill's own `SKILL.md` `requiredFeatures` and an unresolved catalog renders nothing (the gate named per row below is the SKILL's, documented here, not repeated in the JSX). `packageId` is the REGISTRY id (`@neuralis/brain-core`), matched as an exact pair with the skill id. The launchers resolve the `WorkspaceHostPort` from the `WorkspaceHostPortProvider` mounted once in `host/installFilesystemHostComponents.tsx` — without it they render `null` silently. Current placements:
|
|
56
|
-
- `Toolbar` — `sync` (`drive.sync`, icon-`brain`, script-less) next to the Sources panel toggle, and `manage-files` (`drive.write`; `scripts: mkdir, copy, modify
|
|
57
|
-
- `SourcesPanel` header — `manage-sources` (`drive.read`, attached-left to the `+ Source` button
|
|
58
|
-
- `SourcesPanel` per-source **Permissions** header — `manage-uri-policy` (`drive.policy` — no grant below the `admin` tier, grantable to any custom role
|
|
56
|
+
- `Toolbar` — `sync` (`drive.sync`, icon-`brain`, script-less) next to the Sources panel toggle, and `manage-files` (`drive.write`; `scripts: mkdir, copy, modify`) as the general file-ops handoff. The tree's CONTEXT MENU deliberately carries none: it is a keyboard-navigable click-to-act list with one uniform item model, and a composer-prefill affordance would need a bespoke item kind there.
|
|
57
|
+
- `SourcesPanel` header — `manage-sources` (`drive.read`, attached-left to the `+ Source` button).
|
|
58
|
+
- `SourcesPanel` per-source **Permissions** header — `manage-uri-policy` (`drive.policy` — no grant below the `admin` tier, grantable to any custom role) hands the URI-policy / path-permission edit for that source to the agent, alongside the `PolicyEditor`.
|
|
59
59
|
- `FileTree` empty-state — `manage-files` (`drive.write`, label; `scripts: upload, modify`) to populate a source.
|
|
60
|
-
- `GitControlPanel` header — `git` (`drive.read
|
|
61
|
-
- `PendingChangesPanel` toolbar — `manage-files` (`drive.write
|
|
62
|
-
- `VectorHealthPanel` actions row — `manage-sources` (`drive.read
|
|
60
|
+
- `GitControlPanel` header — `git` (`drive.read`) next to the refresh button. The `git` skill drives the SAME `/git/*` routes this panel does — its own SKILL.md says so — and the panel shipped with no way to reach it until 2026-07-28.
|
|
61
|
+
- `PendingChangesPanel` toolbar — `manage-files` (`drive.write`) for bulk review; the per-row approve/revert buttons stay the primary path.
|
|
62
|
+
- `VectorHealthPanel` actions row — `manage-sources` (`drive.read`). The index is per-SOURCE, so the agent-side equivalent of scan/repair is a source reindex plus a sync-status readout.
|
|
63
63
|
- `PackagesPanel` header — `create-package` (label, script-less) hands the whole authoring flow (scaffold, manifest, tools, surfaces, connectors, build — including the WASM pipeline) to the agent; additive to the per-row `Build` button. This is the ONE launcher that narrows further, with `alsoRequires={["packages.author"]}`, even though the SKILL ships ungated (so it stays importable): it scaffolds into `packages://`, which needs write + install/build capability the default `member` role does not hold, so showing it to a caller whose first action would be denied is worse UX than hiding it. `alsoRequires` can only ever hide a launcher, never reveal one, so this cannot become a disclosure. Deliberate — do not "align" it with the skill frontmatter.
|
|
64
64
|
|
|
65
65
|
## How it plugs into the package-system kernel
|
|
@@ -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);
|
|
@@ -1260,7 +1261,6 @@ export function SourcesPanel(props: Props) {
|
|
|
1260
1261
|
attached="left"
|
|
1261
1262
|
skill="manage-sources"
|
|
1262
1263
|
packageId="@neuralis/brain-core"
|
|
1263
|
-
scripts={["scripts/sync.sh", "scripts/reindex.sh", "scripts/set-enabled.sh"]}
|
|
1264
1264
|
label="Manage sources with the agent"
|
|
1265
1265
|
/>
|
|
1266
1266
|
</div>
|
|
@@ -1672,13 +1672,11 @@ export function SourcesPanel(props: Props) {
|
|
|
1672
1672
|
|
|
1673
1673
|
return (
|
|
1674
1674
|
<div key={fileKey(sourceRootUri(config.source))} className="rounded-sm">
|
|
1675
|
-
{/* Accordion header */}
|
|
1676
|
-
<
|
|
1677
|
-
type="button"
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
className="w-full flex items-center gap-2 py-2 text-left group hover:bg-white/[0.02] rounded-md px-1 transition"
|
|
1681
|
-
>
|
|
1675
|
+
{/* Accordion header: selection and enabled actions are siblings. */}
|
|
1676
|
+
<div className="w-full flex items-center gap-2 py-2 text-left group hover:bg-white/[0.02] rounded-md px-1 transition">
|
|
1677
|
+
<button type="button" onClick={() => toggleSource(config.source)} aria-expanded={isOpen}
|
|
1678
|
+
title={disconnected ? sourceLiveReasonLabel(config.live_reason) : undefined}
|
|
1679
|
+
className="min-w-0 flex-1 flex items-center gap-2 text-left">
|
|
1682
1680
|
{/* Git indicator (G2.1) — a GitBranch overlay on a git-repo
|
|
1683
1681
|
source's icon, ORTHOGONAL to the sync tone (visualTone still
|
|
1684
1682
|
owns the icon colour). */}
|
|
@@ -1713,6 +1711,7 @@ export function SourcesPanel(props: Props) {
|
|
|
1713
1711
|
{dirty && (
|
|
1714
1712
|
<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
1713
|
)}
|
|
1714
|
+
</button>
|
|
1716
1715
|
{/* W3K — enabled toggle (per scope, per source). Machine sources also offer Auto. */}
|
|
1717
1716
|
{(() => {
|
|
1718
1717
|
const enabled = config.enabled ?? true;
|
|
@@ -1763,7 +1762,7 @@ export function SourcesPanel(props: Props) {
|
|
|
1763
1762
|
{describeConn(config, meta)}
|
|
1764
1763
|
</span>
|
|
1765
1764
|
<ChevronDown className={cn('h-3 w-3 text-white/25 shrink-0 transition-transform', isOpen && 'rotate-180')} />
|
|
1766
|
-
</
|
|
1765
|
+
</div>
|
|
1767
1766
|
|
|
1768
1767
|
{/* Expanded body — when open it expands to full height (no clamp)
|
|
1769
1768
|
so every Path Rule / uri-policy is visible; the outer panel
|
|
@@ -1902,7 +1901,6 @@ export function SourcesPanel(props: Props) {
|
|
|
1902
1901
|
size="sm"
|
|
1903
1902
|
skill="manage-uri-policy"
|
|
1904
1903
|
packageId="@neuralis/brain-core"
|
|
1905
|
-
scripts={["scripts/show-uri.sh", "scripts/set-uri.sh", "scripts/manifests.sh"]}
|
|
1906
1904
|
label={`Edit URI policy for "${config.source}" with the agent`}
|
|
1907
1905
|
icon="shield"
|
|
1908
1906
|
/>
|
|
@@ -20,6 +20,7 @@ export function Toolbar(props: { agentId: string; onOpenProjectFiles: () => void
|
|
|
20
20
|
const activeWorkspaceTab = useFilesystemStore((s) => s.activeWorkspaceTab);
|
|
21
21
|
// The server count of the caller's visible pending reviews (`refreshPendingCount`),
|
|
22
22
|
// never the client set of loaded folders' rows.
|
|
23
|
+
const pendingCountState = useFilesystemStore((s) => s.pendingCountState);
|
|
23
24
|
const pendingCount = useFilesystemStore((s) => s.pendingCount);
|
|
24
25
|
|
|
25
26
|
return (
|
|
@@ -112,7 +113,7 @@ export function Toolbar(props: { agentId: string; onOpenProjectFiles: () => void
|
|
|
112
113
|
size="sm"
|
|
113
114
|
skill="manage-files"
|
|
114
115
|
packageId="@neuralis/brain-core"
|
|
115
|
-
scripts={["scripts/mkdir.sh", "scripts/copy.sh", "scripts/modify.sh"
|
|
116
|
+
scripts={["scripts/mkdir.sh", "scripts/copy.sh", "scripts/modify.sh"]}
|
|
116
117
|
label="Create, copy, edit or search files with the agent"
|
|
117
118
|
icon="Folder"
|
|
118
119
|
/>
|
|
@@ -133,7 +134,7 @@ export function Toolbar(props: { agentId: string; onOpenProjectFiles: () => void
|
|
|
133
134
|
|
|
134
135
|
<button
|
|
135
136
|
className={`relative p-1.5 rounded-lg transition-colors ${workspacePanels.pending && activeWorkspaceTab === 'pending' ? 'bg-amber-500/15 text-amber-300' : 'text-white/50 hover:bg-white/5 hover:text-amber-200'}`}
|
|
136
|
-
title={pendingCount > 0 ? `Pending changes — ${pendingCount} to review` : 'Pending changes — review, approve & revert'}
|
|
137
|
+
title={pendingCountState === 'stale' ? `Pending changes — count unavailable${pendingCount > 0 ? ` (last count ${pendingCount})` : ''}` : pendingCountState === 'partial' ? `Pending changes — ${pendingCount}+ (incomplete) to review` : pendingCount > 0 ? `Pending changes — ${pendingCount} to review` : 'Pending changes — review, approve & revert'}
|
|
137
138
|
onClick={() => {
|
|
138
139
|
if (workspacePanels.pending && activeWorkspaceTab === 'pending') {
|
|
139
140
|
setWorkspacePanelOpen('pending', false);
|
|
@@ -143,9 +144,9 @@ export function Toolbar(props: { agentId: string; onOpenProjectFiles: () => void
|
|
|
143
144
|
}}
|
|
144
145
|
>
|
|
145
146
|
<FileDiff className="w-4 h-4" />
|
|
146
|
-
{pendingCount > 0 ? (
|
|
147
|
+
{pendingCount > 0 || pendingCountState !== 'exact' ? (
|
|
147
148
|
<span data-testid="pending-count-badge" className="absolute -top-1 -right-1 min-w-[14px] rounded-full px-1 text-center text-[9px] leading-[14px] font-semibold bg-amber-500/15 text-amber-300">
|
|
148
|
-
{pendingCount
|
|
149
|
+
{pendingCount > 999 ? '1000+' : pendingCount}{pendingCountState === 'partial' && pendingCount <= 999 ? '+' : pendingCountState === 'stale' ? '?' : ''}
|
|
149
150
|
</span>
|
|
150
151
|
) : null}
|
|
151
152
|
</button>
|
|
@@ -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');
|
|
@@ -97,9 +99,17 @@ export function VectorHealthPanel({ agentId, projectId }: Props) {
|
|
|
97
99
|
* Cleared automatically after 3s. Skipped when `dryRun === true` since
|
|
98
100
|
* preview-only doesn't need a guard. */
|
|
99
101
|
const [confirmingAction, setConfirmingAction] = useState<ActionKind | null>(null);
|
|
102
|
+
const epoch = useRef(0);
|
|
103
|
+
const reportSeq = useRef(0);
|
|
104
|
+
const scanSeq = useRef(0);
|
|
105
|
+
const stop = useRef(false);
|
|
106
|
+
const pendingCursor = useRef<string | undefined>(undefined);
|
|
107
|
+
const [pendingPaused, setPendingPaused] = useState(false);
|
|
108
|
+
useEffect(() => { pendingCursor.current = undefined; setPendingPaused(false); }, [dryRun, retentionHours]);
|
|
100
109
|
const confirmTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
101
110
|
|
|
102
111
|
const fetchReport = useCallback(async () => {
|
|
112
|
+
const mine = epoch.current; const request = ++reportSeq.current;
|
|
103
113
|
setLoading(true);
|
|
104
114
|
setError(null);
|
|
105
115
|
try {
|
|
@@ -108,8 +118,8 @@ export function VectorHealthPanel({ agentId, projectId }: Props) {
|
|
|
108
118
|
// table so it surfaces real "when was this source last synced"
|
|
109
119
|
// info instead of being a count-only view.
|
|
110
120
|
const [data, sourcesHealth] = await Promise.all([
|
|
111
|
-
checkVectorHealth({ agentId, ...(projectId ? { projectId } : {}) }),
|
|
112
|
-
getSourcesHealth({ agentId, ...(projectId ? { projectId } : {}) })
|
|
121
|
+
checkVectorHealth({ agentId, ...(projectId ? { projectId } : {}) }, client),
|
|
122
|
+
getSourcesHealth({ agentId, ...(projectId ? { projectId } : {}) }, client)
|
|
113
123
|
.catch(() => [] as Array<{ source: string; last_sync?: string; sync_status: string }>),
|
|
114
124
|
]);
|
|
115
125
|
const lastSyncBySource = new Map<string, { last_sync?: string; sync_status: string }>();
|
|
@@ -126,26 +136,29 @@ export function VectorHealthPanel({ agentId, projectId }: Props) {
|
|
|
126
136
|
return meta ? { ...row, ...meta } : row;
|
|
127
137
|
});
|
|
128
138
|
}
|
|
139
|
+
if (mine !== epoch.current || request !== reportSeq.current) return;
|
|
129
140
|
setReport(enriched);
|
|
130
141
|
} catch (err) {
|
|
131
|
-
setError(err instanceof Error ? err.message : 'Failed to fetch health report');
|
|
142
|
+
if (mine === epoch.current && request === reportSeq.current) setError(err instanceof Error ? err.message : 'Failed to fetch health report');
|
|
132
143
|
} finally {
|
|
133
|
-
setLoading(false);
|
|
144
|
+
if (mine === epoch.current && request === reportSeq.current) setLoading(false);
|
|
134
145
|
}
|
|
135
|
-
}, [agentId, projectId]);
|
|
146
|
+
}, [client, agentId, projectId]);
|
|
136
147
|
|
|
137
148
|
const runScan = useCallback(async () => {
|
|
149
|
+
const mine = epoch.current; const request = ++scanSeq.current;
|
|
138
150
|
setScanLoading(true);
|
|
139
151
|
setError(null);
|
|
140
152
|
try {
|
|
141
|
-
const result = await scanVectorHealth({ agentId, ...(projectId ? { projectId } : {}) });
|
|
153
|
+
const result = await scanVectorHealth({ agentId, ...(projectId ? { projectId } : {}) }, client);
|
|
154
|
+
if (mine !== epoch.current || request !== scanSeq.current) return;
|
|
142
155
|
setScan({ orphanChunkCount: result.orphanChunkCount ?? 0, duplicateGroups: result.duplicateGroups ?? 0 });
|
|
143
156
|
} catch (err) {
|
|
144
|
-
setError(err instanceof Error ? err.message : 'Scan failed');
|
|
157
|
+
if (mine === epoch.current && request === scanSeq.current) setError(err instanceof Error ? err.message : 'Scan failed');
|
|
145
158
|
} finally {
|
|
146
|
-
setScanLoading(false);
|
|
159
|
+
if (mine === epoch.current && request === scanSeq.current) setScanLoading(false);
|
|
147
160
|
}
|
|
148
|
-
}, [agentId, projectId]);
|
|
161
|
+
}, [client, agentId, projectId]);
|
|
149
162
|
|
|
150
163
|
const runAction = useCallback(async (action: ActionKind) => {
|
|
151
164
|
// Two-click confirm for destructive actions when not in dry-run mode.
|
|
@@ -160,6 +173,7 @@ export function VectorHealthPanel({ agentId, projectId }: Props) {
|
|
|
160
173
|
confirmTimerRef.current = null;
|
|
161
174
|
}
|
|
162
175
|
setConfirmingAction(null);
|
|
176
|
+
const mine = epoch.current; stop.current = false;
|
|
163
177
|
setActionLoading(true);
|
|
164
178
|
setActionResult(null);
|
|
165
179
|
setLastActionKind(action);
|
|
@@ -168,8 +182,22 @@ export function VectorHealthPanel({ agentId, projectId }: Props) {
|
|
|
168
182
|
// we're actually deleting something (`dryRun=false`).
|
|
169
183
|
setPrevReport(!dryRun ? report : null);
|
|
170
184
|
try {
|
|
171
|
-
|
|
172
|
-
|
|
185
|
+
let cursor = action === 'repair-pending' ? pendingCursor.current : undefined; setPendingPaused(false);
|
|
186
|
+
const totals: Record<string, number> = {};
|
|
187
|
+
do {
|
|
188
|
+
const result = await runHealthAction({ agentId, ...(projectId ? { projectId } : {}), action, dryRun, retentionHours, ...(cursor ? { cursor } : {}) }, client);
|
|
189
|
+
if (mine !== epoch.current) return;
|
|
190
|
+
for (const key of ['scannedVisible', 'orphaned', 'attempted', 'discarded', 'refused', 'failed', 'legacyUnknownDraft']) {
|
|
191
|
+
if (typeof result[key] === 'number') totals[key] = (totals[key] ?? 0) + result[key];
|
|
192
|
+
}
|
|
193
|
+
setActionResult({ ...result, ...totals });
|
|
194
|
+
cursor = action === 'repair-pending' && !result.restartRequired && typeof result.nextCursor === 'string' ? result.nextCursor : undefined;
|
|
195
|
+
if (action === 'repair-pending') pendingCursor.current = cursor;
|
|
196
|
+
if (result.partial === true) { setPendingPaused(true); setError('Repair paused; part of the inventory or orphan checks are unavailable.'); break; }
|
|
197
|
+
if (result.restartRequired) { pendingCursor.current = undefined; setPendingPaused(true); setError('Changes moved. Refresh before continuing repair.'); }
|
|
198
|
+
if (typeof result.failed === 'number' && result.failed > 0) { cursor = undefined; setPendingPaused(true); setError('Repair paused after a failed change. Refresh before continuing.'); }
|
|
199
|
+
} while (cursor && !stop.current);
|
|
200
|
+
if (cursor) setPendingPaused(true);
|
|
173
201
|
if (!dryRun) {
|
|
174
202
|
void fetchReport();
|
|
175
203
|
// A destructive action changes orphan/dup counts — refresh the scan
|
|
@@ -177,15 +205,17 @@ export function VectorHealthPanel({ agentId, projectId }: Props) {
|
|
|
177
205
|
if (scan) void runScan();
|
|
178
206
|
}
|
|
179
207
|
} catch (err) {
|
|
180
|
-
setError(err instanceof Error ? err.message : 'Action failed');
|
|
208
|
+
if (mine === epoch.current) { if (action === 'repair-pending') setPendingPaused(true); setError(err instanceof Error ? err.message : 'Action failed'); }
|
|
181
209
|
} finally {
|
|
182
|
-
setActionLoading(false);
|
|
210
|
+
if (mine === epoch.current) setActionLoading(false);
|
|
183
211
|
}
|
|
184
|
-
}, [agentId, projectId, dryRun, retentionHours, fetchReport, confirmingAction, report, scan, runScan]);
|
|
212
|
+
}, [client, agentId, projectId, dryRun, retentionHours, fetchReport, confirmingAction, report, scan, runScan]);
|
|
185
213
|
|
|
186
214
|
// Auto-load on mount
|
|
187
215
|
useEffect(() => {
|
|
216
|
+
epoch.current++; stop.current = true; pendingCursor.current = undefined; setPendingPaused(false); setActionLoading(false); setActionResult(null); setReport(null); setScan(null); setError(null);
|
|
188
217
|
void fetchReport();
|
|
218
|
+
return () => { epoch.current++; stop.current = true; };
|
|
189
219
|
}, [fetchReport]);
|
|
190
220
|
|
|
191
221
|
// Clean up confirm timer on unmount.
|
|
@@ -209,6 +239,8 @@ export function VectorHealthPanel({ agentId, projectId }: Props) {
|
|
|
209
239
|
|
|
210
240
|
return (
|
|
211
241
|
<div className="space-y-3 px-2 pb-2">
|
|
242
|
+
{actionLoading && lastActionKind === 'repair-pending' ? <button type="button" onClick={() => { stop.current = true; }} className="text-xs text-amber-200">Stop after this batch</button> : null}
|
|
243
|
+
{canWrite && pendingPaused && !actionLoading ? <button type="button" onClick={() => void runAction('repair-pending')} className="text-xs text-amber-200">{pendingCursor.current ? 'Continue available repair' : 'Retry repair'}</button> : null}
|
|
212
244
|
{/* Description */}
|
|
213
245
|
<div className="text-[10px] text-white/40 italic leading-relaxed">
|
|
214
246
|
Cleanup tool for the vector index. Removes leftover records:{' '}
|
|
@@ -368,7 +400,6 @@ export function VectorHealthPanel({ agentId, projectId }: Props) {
|
|
|
368
400
|
size="sm"
|
|
369
401
|
skill="manage-sources"
|
|
370
402
|
packageId="@neuralis/brain-core"
|
|
371
|
-
scripts={["scripts/sync-status.sh", "scripts/reindex.sh"]}
|
|
372
403
|
label="Inspect sync status or reindex a source with the agent"
|
|
373
404
|
icon="Database"
|
|
374
405
|
/>
|
|
@@ -376,13 +407,13 @@ export function VectorHealthPanel({ agentId, projectId }: Props) {
|
|
|
376
407
|
<div className="flex items-center gap-3">
|
|
377
408
|
<label className="inline-flex items-center gap-1 text-[10px] text-white/60 cursor-pointer"
|
|
378
409
|
title="Preview only — nothing will be deleted.">
|
|
379
|
-
<input type="checkbox" checked={dryRun} onChange={(e) => setDryRun(e.target.checked)} className="accent-emerald-400 h-3 w-3" />
|
|
410
|
+
<input type="checkbox" disabled={actionLoading} checked={dryRun} onChange={(e) => setDryRun(e.target.checked)} className="accent-emerald-400 h-3 w-3" />
|
|
380
411
|
Dry run
|
|
381
412
|
</label>
|
|
382
413
|
<label className="inline-flex items-center gap-1 text-[10px] text-white/60"
|
|
383
414
|
title="Records marked as deleted within this many hours are kept (GC ignores them).">
|
|
384
415
|
Retention:
|
|
385
|
-
<input type="number" min={1} value={retentionHours}
|
|
416
|
+
<input type="number" disabled={actionLoading} min={1} value={retentionHours}
|
|
386
417
|
onChange={(e) => setRetentionHours(Math.max(1, Number(e.target.value) || 24))}
|
|
387
418
|
className="w-12 h-5 rounded px-1 text-[10px] bg-white/[0.04] border border-white/[0.08] text-white/80 outline-none" />h
|
|
388
419
|
</label>
|