@neuralis/brain-core 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/README.md +1 -1
- package/app/filesystem/README.md +3 -3
- package/app/filesystem/api/README.md +3 -3
- package/app/filesystem/api/index.ts +127 -122
- package/app/filesystem/domain/types.ts +12 -0
- package/app/filesystem/hooks/README.md +5 -5
- package/app/filesystem/hooks/useDiscoverableSources.ts +4 -2
- package/app/filesystem/hooks/useFilesystemSSE.ts +31 -23
- package/app/filesystem/hooks/useGitChangesInventory.ts +4 -2
- package/app/filesystem/host/README.md +3 -2
- package/app/filesystem/host/installFilesystemHostComponents.tsx +22 -9
- package/app/filesystem/store/README.md +11 -7
- package/app/filesystem/store/filesystemStore.ts +434 -161
- package/app/filesystem/store/gitSlice.ts +6 -6
- package/app/filesystem/store/listSlice.ts +8 -6
- package/app/filesystem/store/searchSlice.ts +3 -3
- package/app/filesystem/ui/FileEditor.tsx +48 -35
- package/app/filesystem/ui/FileTree.tsx +10 -9
- package/app/filesystem/ui/GitCommitChangesView.tsx +5 -3
- package/app/filesystem/ui/GitCommitGraph.tsx +6 -5
- package/app/filesystem/ui/GitControlPanel.tsx +14 -13
- package/app/filesystem/ui/GitDiffView.tsx +2 -1
- package/app/filesystem/ui/GitFileDiff.tsx +7 -6
- package/app/filesystem/ui/PendingChangesPanel.tsx +16 -13
- package/app/filesystem/ui/PendingReviewDialog.tsx +8 -5
- package/app/filesystem/ui/ReembedDialog.tsx +4 -2
- package/app/filesystem/ui/SourcesPanel.tsx +26 -26
- package/app/filesystem/ui/VectorHealthPanel.tsx +6 -4
- package/dist/app/host.js +46 -46
- package/package.json +4 -4
- package/skills/create-package/references/manifest-and-discovery.md +1 -1
- package/skills/create-package/references/surfaces-and-design.md +7 -5
- package/skills/create-package/scripts/check-package.sh +47 -1
- package/app/filesystem/hooks/useContainerWidth.ts +0 -39
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neuralis/brain-core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"license": "FSL-1.1-ALv2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"@codemirror/view": "^6.43.13",
|
|
64
64
|
"@iconify-json/vscode-icons": "^1.2.81",
|
|
65
65
|
"@iconify/react": "^6.0.2",
|
|
66
|
-
"@neuralis/package-system": "0.2.
|
|
66
|
+
"@neuralis/package-system": "0.2.1",
|
|
67
67
|
"@qdrant/js-client-rest": "1.19.0",
|
|
68
68
|
"@shikijs/engine-javascript": "^4.4.3",
|
|
69
69
|
"@shikijs/langs": "^4.4.3",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"peerDependencies": {
|
|
76
76
|
"@aws-sdk/client-s3": "^3.975.0",
|
|
77
77
|
"@aws-sdk/s3-request-presigner": "^3.975.0",
|
|
78
|
-
"@neuralis/agent-core": "0.2.
|
|
78
|
+
"@neuralis/agent-core": "0.2.1",
|
|
79
79
|
"@tanstack/react-virtual": "^3.13.0",
|
|
80
80
|
"lucide-react": "^1.47.0",
|
|
81
81
|
"react": "^19.0.0",
|
|
@@ -2491,7 +2491,7 @@
|
|
|
2491
2491
|
"contentClassName": "overflow-hidden"
|
|
2492
2492
|
},
|
|
2493
2493
|
"open": {
|
|
2494
|
-
"singleton":
|
|
2494
|
+
"singleton": false,
|
|
2495
2495
|
"defaultTitle": "Files"
|
|
2496
2496
|
}
|
|
2497
2497
|
},
|
|
@@ -56,7 +56,7 @@ dist tools workflows skills rules instructions agents docs team hooks.j
|
|
|
56
56
|
|
|
57
57
|
npm does no directory inference here, and a dev symlink hides the omission completely.
|
|
58
58
|
|
|
59
|
-
> **Symptom:** everything works from a source checkout, then after `npm pack` / install / container build the package loads with **zero tools and zero skills and no error at all** — discovery found no folders. `team` and `app` are most-forgotten: no `team` ships no characters, no `app` ships no UI.
|
|
59
|
+
> **Symptom:** everything works from a source checkout, then after `npm pack` / install / container build the package loads with **zero tools and zero skills and no error at all** — discovery found no folders. `team` and `app` are most-forgotten: no `team` ships no characters, no `app` ships no UI and no dock picture.
|
|
60
60
|
|
|
61
61
|
Diff `files` against the folders that exist before every publish. Directory drops and source packages are unaffected — no tarball step.
|
|
62
62
|
|
|
@@ -15,7 +15,7 @@ Every surface lives in `package.json` under `neuralis.app.surfaces[]` — a unio
|
|
|
15
15
|
| kind | required | useful optional |
|
|
16
16
|
|---|---|---|
|
|
17
17
|
| `widget` | `kind`, `id`, `type`, `title`, `component` | `icon`, `layout`, `chrome`, `open`, `defaultOpen`, `requires` |
|
|
18
|
-
| `dock` | `kind`, `id`, `label`, `action` | `icon`, `position`, `section`, `dock`, `tooltip`, `color` |
|
|
18
|
+
| `dock` | `kind`, `id`, `label`, `action` | `icon`, `image`, `position`, `section`, `dock`, `tooltip`, `color` |
|
|
19
19
|
| `card` | `kind`, `id`, `type` | `title`, `render`, `match`, `bridge` |
|
|
20
20
|
|
|
21
21
|
**Every icon comes from ONE platform library** — about 270 curated lucide names in categories, the `ICON_LIBRARY` export of `@neuralis/package-system/icons`. Write the PascalCase name (`ChartLine`) or its lucide-kebab form (`chart-line`); a few aliases (`layout`, `terminal`, `admin`) also resolve. The same rule covers a surface `icon`, a team member's `appearance.icon` and a workflow template's `icon`. Any other name renders the fallback glyph, warns at load, and is an ERROR in `check-package.sh`.
|
|
@@ -68,6 +68,8 @@ The smallest complete, **reachable** UI a project-dropped package can ship. Four
|
|
|
68
68
|
|
|
69
69
|
**Gate the widget, not the dock entry** — dock visibility inherits the target widget's `requires.features` automatically. Duplicating `requires` only creates drift: a user holding the widget's features but not your accidentally-different dock feature gets a widget they can never open.
|
|
70
70
|
|
|
71
|
+
**An app picture is optional: `"image": "app/shared/<name>.png"`.** A PNG, JPEG or WebP under `app/shared/`, at most 256 KiB, each path segment letters, digits, `.`, `_` or `-`. The dock, the app menu and the inbox show it in a fixed-size box to whoever can see the dock entry; keep the `icon` glyph — it renders until the picture arrives and whenever it cannot be shown. The platform judges the BYTES, never the extension: an SVG, a renamed document or an oversized file is not served and the glyph stays. An invalid path (absolute, remote, outside `app/shared/`, `..`, a non-raster extension) is a validation error that fails the whole package. `check-package.sh` checks the path, the file and its bytes.
|
|
72
|
+
|
|
71
73
|
## Task: pick a renderer
|
|
72
74
|
|
|
73
75
|
**Two renderers exist, and both render.** There is no third option and no placeholder tier: `inline-html` and `wasm-ui` were removed from the contract, and a manifest still declaring one is rejected outright — *renderer 'inline-html' was removed; ship the HTML asset and declare renderer 'iframe' with a relative url*. A leftover `fallback` key is inert and warned about. (WASM remains the **tool/route** sandbox runtime; there has never been a WASM UI engine, and one can no longer be declared.)
|
|
@@ -117,7 +119,7 @@ Five things to know before you use it:
|
|
|
117
119
|
- **Budgets: 64 distinct paths and 8 MiB per SCOPE**, and the lane is `no-store`, so every render refetches the whole set. The preflight errors past both numbers and warns past 1 MiB — but it counts only your SURFACE tree, while the runtime budget also charges every `app/shared/` file your entry pulls in, so a shared-heavy bundle can pass authoring and still be refused at request time. The per-file 2 MiB cap applies in both modes.
|
|
118
120
|
- **Still no remote origins.** The CSP names none, so a CDN script or a Google font is blocked before the request leaves. Ship the file.
|
|
119
121
|
|
|
120
|
-
`app/shared/` is a declared **package-public** namespace: any visible surface of the package may read it, so nothing feature-private belongs there.
|
|
122
|
+
`app/shared/` is a declared **package-public** namespace: any visible surface of the package may read it, and a declared dock picture is served from it, so nothing feature-private belongs there.
|
|
121
123
|
|
|
122
124
|
## The silent failures — symptom first
|
|
123
125
|
|
|
@@ -167,7 +169,7 @@ Asset serving is authenticated per request, scope-bound, traversal-safe, `Cache-
|
|
|
167
169
|
|
|
168
170
|
*Symptom of omitting `color-scheme` or an explicit height:* the frame collapses to the HTML default 150px, and the browser paints an opaque **white** backdrop behind a cross-origin frame whose colour scheme differs from the dark workspace. Declare `color-scheme: dark light` at **every** nesting level — outer element and embedded document both.
|
|
169
171
|
|
|
170
|
-
Host CSS custom properties (`--color-text`, `--w-bg-rgb`, `--icon-accent`, `--control-surface`, …) reach `direct`-rendered components only; there, never hardcode a hex —
|
|
172
|
+
Host CSS custom properties (`--color-text`, `--w-bg-rgb`, `--icon-accent`, `--control-surface`, …) reach `direct`-rendered components only; there, never hardcode a hex — four themes ship, independently selected for widgets, docks and background — and `--w-bg-rgb` is comma-separated channels: `rgba(var(--w-bg-rgb), 0.55)`. Every theme is dark polarity (white text), so design once for the dark design.
|
|
171
173
|
|
|
172
174
|
### Design floor
|
|
173
175
|
|
|
@@ -183,7 +185,7 @@ Host CSS custom properties (`--color-text`, `--w-bg-rgb`, `--icon-accent`, `--co
|
|
|
183
185
|
|
|
184
186
|
`chrome.mode` is `toolbar` (default), `frameless` or `floating`. All three render the **same** host control strip, differing only in placement and visibility. `toolbar`/`floating` wrap content with padding and automatic scroll; **`frameless` clips content with no padding and no scrollbar** — the widget root owns its scroll and padding and must reserve roughly 4rem at the top right. *Symptom of ignoring it:* your top-right controls sit under the hover-revealed host strip and become unclickable, and content is cut off with no way to scroll. Iframe widgets usually want `frameless`.
|
|
185
187
|
|
|
186
|
-
**The close (X) button minimises; it does not destroy.** The widget stays mounted off-screen and survives a refresh. *Symptom:* reopening shows it exactly as it was, "fresh start" initialisation never runs, and teardown in an unmount handler never fires. True destroy is the dock item's
|
|
188
|
+
**The close (X) button minimises; it does not destroy.** The widget stays mounted off-screen and survives a refresh. *Symptom:* reopening shows it exactly as it was, "fresh start" initialisation never runs, and teardown in an unmount handler never fires. True destroy is the dock item's clean (broom) control. **Exception for `renderer: "iframe"` widgets:** only the React subtree stays mounted — the frame itself is detached from the document while minimised (and on a stage-mode switch), so the browser discards its browsing context and the frame reloads from the same URL with everything inside it gone. *Symptom:* your in-frame bootstrap DOES re-run and any in-frame in-memory state is lost. Make an iframe widget's bootstrap re-runnable and persist state server-side or in the frame's own storage.
|
|
187
189
|
|
|
188
190
|
Transparency resolves per-instance override → manifest `chrome.transparent` → the user's Layout default → opaque; the cycle control is hidden when `chrome.userTransparency: false` or the package is untrusted. **The level is a user setting your surface cannot read, and it changes the panel's CSS containment**: the panel emits a `backdrop-filter` at `dim` and `opaque` (not at `transparent`), so at those two levels it is a containing block for `position: fixed` descendants. Design so the level never matters — portal every `fixed` overlay to `document.body` (see the Design floor above). The panel's stacking context does not vary: it is isolated at every level.
|
|
189
191
|
|
|
@@ -274,7 +276,7 @@ registerCardComponent('thing.result', ThingCard, {
|
|
|
274
276
|
|
|
275
277
|
`component.import` resolves through a client-side host registry the package populates from its runtime UI module: declare `app.module`, build `app/host.tsx` with `neuralis-build ui`, and the host imports the module and calls each `install…HostComponents({ registry, port })` export with a registry that accepts only your own package's registrations. Every `component.import` your manifest declares must be registered somewhere under `app/`, or the surface resolves to null and shows the placeholder — no error. A module built before a platform upgrade can be refused (its record names an older host API or no React major): rebuild it with `neuralis-build ui`.
|
|
276
278
|
|
|
277
|
-
The component receives exactly `{ agentId, widgetId, state }
|
|
279
|
+
The component receives exactly `{ agentId, widgetId, state }` — `agentId` is the window's BOUND agent (fixed for that window, never the workspace's active one) and `widgetId` names the window; everything else comes from the workspace host port captured at install time — project/user ids, agents, `openWidget`, `updateWidgetState`, `getGrantedFeatures`, `useVisibleSkills`, and `subscribeRealtime({ channel, projectId, agentId?, onEvent })` over `runtime | presence | workflow | filesystem | conversation | project` (plus the optional `isRealtimeConnected()` for that connection's current state — treat its absence as "may be disconnected"). A widget can be open in SEVERAL windows (unless its manifest sets `open.singleton: true`): keep one client store PER WINDOW, keyed on `widgetId` and the bound `agentId`, never one global store read through the active agent — two windows must never share a selection, a draft or a late response. `openWidget({ agentId, type, title, target? })` picks the window: absent or `'default'` is the most recently focused window of that type bound to that agent (opened only when none exists), `'new'` always opens another (a singleton answers as `'default'`), and `{ instanceId }` is exactly that window. Subscribe to the optional `onClientStateReset(handler)` at install time: the dock's clean (broom) control fires it at four levels — `instance` (clean ONE window, `instanceId`; the window stays open), `widget` (that window is closed; reset only on YOUR `widgetType`), `agent` and `project` — and your handler drops that scope's client state (persisted tabs included) without calling the host back or touching the server. A host without the method never resets you; assume nothing. HTTP goes through the generic package API client, which injects the project and agent headers — build it from the window's bound agent.
|
|
278
280
|
|
|
279
281
|
### Offer the skill next to the button that does the same thing
|
|
280
282
|
|
|
@@ -568,10 +568,56 @@ if (man) {
|
|
|
568
568
|
for (const [f, t] of toolCardTypes) {
|
|
569
569
|
if (!byType.has(t)) warn(f, `x-neuralis.ui.cardType "${t}" matches no card surface declared here — the result falls back to the generic default card. Deliberate when the type is a host/builtin card; otherwise add {"kind":"card","id":"…","type":"${t}"}`);
|
|
570
570
|
}
|
|
571
|
+
const imageDocks = docks.filter((d) => d.image !== undefined);
|
|
571
572
|
if (hasShared) {
|
|
572
573
|
warn(`${SHARED_DIR}/`, anyBundle
|
|
573
574
|
? `declared package-public: any visible surface of this package may read it, so no feature-private asset belongs there. It IS served — a bundle surface reaches it as '../shared/…' relative to the entry's own directory, so mind the depth (an entry at 'sub/index.html' needs '../../shared/…')`
|
|
574
|
-
: `declared package-public: any visible surface of this package may read it, so no feature-private asset belongs there. No surface here declares "assetMode":"bundle", so nothing serves it — a self-contained entry requests it with no session cookie. Declare bundle mode on the surface that needs it, or inline the asset`);
|
|
575
|
+
: `declared package-public: any visible surface of this package may read it, so no feature-private asset belongs there. No surface here declares "assetMode":"bundle", so ${imageDocks.length ? 'only the declared dock picture(s) are served from it' : 'nothing serves it'} — a self-contained entry requests it with no session cookie. Declare bundle mode on the surface that needs it, or inline the asset`);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// dock pictures — an optional raster `image` on a dock entry. The PATH rule is
|
|
579
|
+
// the platform's own (`resolveDockImagePath`), the BYTE rule its own raster
|
|
580
|
+
// sniff and 256 KiB floor (`@neuralis/package-system/web-common`); where those
|
|
581
|
+
// do not resolve from here, the check is SKIPPED and says so.
|
|
582
|
+
if (imageDocks.length) {
|
|
583
|
+
let kernel = null, raster = null;
|
|
584
|
+
for (const from of [ROOT, process.cwd(), SCRIPT_DIR]) {
|
|
585
|
+
try {
|
|
586
|
+
kernel = require(require.resolve('@neuralis/package-system', { paths: [from] }));
|
|
587
|
+
raster = require(require.resolve('@neuralis/package-system/web-common', { paths: [from] }));
|
|
588
|
+
break;
|
|
589
|
+
} catch { kernel = null; raster = null; }
|
|
590
|
+
}
|
|
591
|
+
if (!kernel || typeof kernel.resolveDockImagePath !== 'function' || !raster || typeof raster.sniffRasterImage !== 'function') {
|
|
592
|
+
F.push({ lvl: 'SKIP', f: MF, m: `${imageDocks.length} dock image(s) NOT checked — @neuralis/package-system does not resolve from here; run the check where the platform is installed` });
|
|
593
|
+
} else {
|
|
594
|
+
let sharedReal = null;
|
|
595
|
+
try { sharedReal = fs.realpathSync(at('app', 'shared')); } catch { /* no app/shared */ }
|
|
596
|
+
for (const d of imageDocks) {
|
|
597
|
+
const L = `dock "${d.id || '(no id)'}" image`;
|
|
598
|
+
const resolved = typeof d.image === 'string' ? kernel.resolveDockImagePath(d.image) : { ok: false, error: 'not a string' };
|
|
599
|
+
if (!resolved.ok) {
|
|
600
|
+
err(MF, `${L} is not a valid picture path (${resolved.error}) — declare a relative path under '${SHARED_DIR}/' naming a .png, .jpg, .jpeg or .webp file (letters, digits, '.', '_', '-' per segment). The package does NOT load with this value`);
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
let real = null;
|
|
604
|
+
try { real = fs.realpathSync(at(...resolved.path.split('/'))); } catch { /* missing */ }
|
|
605
|
+
if (!real) { err(resolved.path, `${L} is declared but not on disk — the dock shows only its glyph`); continue; }
|
|
606
|
+
if (!sharedReal || !real.startsWith(sharedReal + path.sep)) {
|
|
607
|
+
err(resolved.path, `${L} resolves outside '${SHARED_DIR}/' through a link — the platform refuses to serve it`);
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
const st = fs.statSync(real);
|
|
611
|
+
if (!st.isFile()) { err(resolved.path, `${L} is not a regular file`); continue; }
|
|
612
|
+
if (st.size > raster.APPEARANCE_IMAGE_MAX_BYTES) {
|
|
613
|
+
err(resolved.path, `${L} is ${st.size} bytes; a package picture is served only up to ${raster.APPEARANCE_IMAGE_MAX_BYTES} bytes (256 KiB) — downscale it, a dock tile is a few dozen pixels`);
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
if (!raster.sniffRasterImage(new Uint8Array(fs.readFileSync(real)))) {
|
|
617
|
+
err(resolved.path, `${L} is not a PNG, JPEG or WebP picture by its BYTES — the platform sniffs the content, never the extension, and serves nothing else (an SVG or a renamed document is refused)`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
575
621
|
}
|
|
576
622
|
|
|
577
623
|
// icons — a name outside the platform icon library renders the fallback
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { useEffect, useState, type RefObject } from 'react';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Reports whether a container is "wide" (≥ threshold), with hysteresis so a diff's
|
|
5
|
-
* auto unified↔split flip does not strobe while the user drags the widget across the
|
|
6
|
-
* boundary. Becomes wide at `wideThresholdPx`, flips back to narrow only below
|
|
7
|
-
* `wideThresholdPx − hysteresisPx`.
|
|
8
|
-
*
|
|
9
|
-
* It reads `clientWidth` ONLY — never `scrollWidth` — so the observer callback cannot
|
|
10
|
-
* force a layout flush of its own. (Its sibling `useElementFits` did compare the two
|
|
11
|
-
* and was deleted in 2026-08: it had no consumer anywhere in `packages/*`.)
|
|
12
|
-
*/
|
|
13
|
-
export function useContainerWidth<T extends HTMLElement>(
|
|
14
|
-
ref: RefObject<T | null>,
|
|
15
|
-
options?: { wideThresholdPx?: number; hysteresisPx?: number },
|
|
16
|
-
): boolean {
|
|
17
|
-
const wideThresholdPx = options?.wideThresholdPx ?? 900;
|
|
18
|
-
const hysteresisPx = options?.hysteresisPx ?? 60;
|
|
19
|
-
const [isWide, setIsWide] = useState(false);
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
const element = ref.current;
|
|
23
|
-
if (!element) {
|
|
24
|
-
setIsWide(false);
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
const update = () => {
|
|
28
|
-
const width = element.clientWidth;
|
|
29
|
-
setIsWide((prev) => (prev ? width >= wideThresholdPx - hysteresisPx : width >= wideThresholdPx));
|
|
30
|
-
};
|
|
31
|
-
update();
|
|
32
|
-
if (typeof ResizeObserver === 'undefined') return undefined;
|
|
33
|
-
const observer = new ResizeObserver(() => update());
|
|
34
|
-
observer.observe(element);
|
|
35
|
-
return () => observer.disconnect();
|
|
36
|
-
}, [ref, wideThresholdPx, hysteresisPx]);
|
|
37
|
-
|
|
38
|
-
return isWide;
|
|
39
|
-
}
|