@stigmer/react 3.5.1 → 3.5.3
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/package.json +4 -4
- package/skill/SkillFileBrowser.d.ts +2 -1
- package/skill/SkillFileBrowser.d.ts.map +1 -1
- package/skill/SkillFileBrowser.js +19 -5
- package/skill/SkillFileBrowser.js.map +1 -1
- package/skill/internal/fetchAndUnpackArtifact.d.ts.map +1 -1
- package/skill/internal/fetchAndUnpackArtifact.js +4 -1
- package/skill/internal/fetchAndUnpackArtifact.js.map +1 -1
- package/skill/useSkillArtifact.d.ts.map +1 -1
- package/skill/useSkillArtifact.js +9 -5
- package/skill/useSkillArtifact.js.map +1 -1
- package/src/execution/__tests__/PlanArtifactCard.test.tsx +23 -2
- package/src/skill/SkillFileBrowser.tsx +23 -11
- package/src/skill/__tests__/SkillFileBrowser.test.tsx +100 -0
- package/src/skill/internal/fetchAndUnpackArtifact.ts +4 -1
- package/src/skill/useSkillArtifact.ts +14 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stigmer/react",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.3",
|
|
4
4
|
"description": "React provider and client hook for the Stigmer platform SDK",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@bufbuild/protovalidate": "^1.1.1",
|
|
39
39
|
"@dagrejs/dagre": "^1.1.4",
|
|
40
|
-
"@stigmer/theme": "3.5.
|
|
40
|
+
"@stigmer/theme": "3.5.3",
|
|
41
41
|
"diff": "^8.0.3",
|
|
42
42
|
"fflate": "^0.8.2",
|
|
43
43
|
"hast-util-to-jsx-runtime": "^2.3.6",
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
"@codemirror/state": "^6.0.0",
|
|
61
61
|
"@codemirror/view": "^6.0.0",
|
|
62
62
|
"@lezer/highlight": "^1.0.0",
|
|
63
|
-
"@stigmer/protos": "3.5.
|
|
64
|
-
"@stigmer/sdk": "3.5.
|
|
63
|
+
"@stigmer/protos": "3.5.3",
|
|
64
|
+
"@stigmer/sdk": "3.5.3",
|
|
65
65
|
"@tanstack/react-table": "^8.20.0",
|
|
66
66
|
"@xyflow/react": "^12.0.0",
|
|
67
67
|
"elkjs": "^0.9.0",
|
|
@@ -12,7 +12,8 @@ export interface SkillFileBrowserProps {
|
|
|
12
12
|
* - Left panel: file tree with folder grouping
|
|
13
13
|
* - Right panel: content viewer (rendered Markdown for .md, raw code otherwise)
|
|
14
14
|
*
|
|
15
|
-
* SKILL.md is selected by default on load.
|
|
15
|
+
* SKILL.md is selected by default on load; packages without a root SKILL.md
|
|
16
|
+
* fall back to the first file in tree order.
|
|
16
17
|
* Responsive: stacked layout on mobile viewports.
|
|
17
18
|
*
|
|
18
19
|
* Zero Console dependencies — safe for platform builder embedding.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SkillFileBrowser.d.ts","sourceRoot":"","sources":["../../src/skill/SkillFileBrowser.tsx"],"names":[],"mappings":"AAaA,0CAA0C;AAC1C,MAAM,WAAW,qBAAqB;IACpC,mEAAmE;IACnE,QAAQ,CAAC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,qDAAqD;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAMD
|
|
1
|
+
{"version":3,"file":"SkillFileBrowser.d.ts","sourceRoot":"","sources":["../../src/skill/SkillFileBrowser.tsx"],"names":[],"mappings":"AAaA,0CAA0C;AAC1C,MAAM,WAAW,qBAAqB;IACpC,mEAAmE;IACnE,QAAQ,CAAC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,qDAAqD;IACrD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAMD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,kBAAkB,EAClB,SAAS,GACV,EAAE,qBAAqB,kDA6GvB"}
|
|
@@ -16,7 +16,8 @@ import { useSkillArtifact } from "./useSkillArtifact.js";
|
|
|
16
16
|
* - Left panel: file tree with folder grouping
|
|
17
17
|
* - Right panel: content viewer (rendered Markdown for .md, raw code otherwise)
|
|
18
18
|
*
|
|
19
|
-
* SKILL.md is selected by default on load.
|
|
19
|
+
* SKILL.md is selected by default on load; packages without a root SKILL.md
|
|
20
|
+
* fall back to the first file in tree order.
|
|
20
21
|
* Responsive: stacked layout on mobile viewports.
|
|
21
22
|
*
|
|
22
23
|
* Zero Console dependencies — safe for platform builder embedding.
|
|
@@ -33,9 +34,22 @@ import { useSkillArtifact } from "./useSkillArtifact.js";
|
|
|
33
34
|
*/
|
|
34
35
|
export function SkillFileBrowser({ artifactStorageKey, className, }) {
|
|
35
36
|
const { files, isLoading, error, getFileContent } = useSkillArtifact(artifactStorageKey);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
const
|
|
37
|
+
// Only the user's explicit click is state; the initial selection is derived
|
|
38
|
+
// from the loaded file list below, so it exists the moment files do.
|
|
39
|
+
const [userSelectedPath, setUserSelectedPath] = useState(null);
|
|
40
|
+
const fileEntries = useMemo(() => (files ? files.filter((f) => !f.isDirectory) : []), [files]);
|
|
41
|
+
const treeNodes = useMemo(() => buildFileTree(fileEntries), [fileEntries]);
|
|
42
|
+
// Default to the manifest; fall back to the first file in rendered tree
|
|
43
|
+
// order (buildFileTree sorts by localeCompare, and depth-first tree order
|
|
44
|
+
// follows that sort) so the auto-highlighted row is the topmost file.
|
|
45
|
+
const defaultPath = useMemo(() => {
|
|
46
|
+
if (fileEntries.some((f) => f.path === "SKILL.md"))
|
|
47
|
+
return "SKILL.md";
|
|
48
|
+
const sorted = [...fileEntries].sort((a, b) => a.path.localeCompare(b.path));
|
|
49
|
+
return sorted[0]?.path ?? null;
|
|
50
|
+
}, [fileEntries]);
|
|
51
|
+
const selectedPath = userSelectedPath ?? defaultPath;
|
|
52
|
+
const content = selectedPath !== null ? getFileContent(selectedPath) : null;
|
|
39
53
|
if (!artifactStorageKey)
|
|
40
54
|
return null;
|
|
41
55
|
if (isLoading) {
|
|
@@ -46,7 +60,7 @@ export function SkillFileBrowser({ artifactStorageKey, className, }) {
|
|
|
46
60
|
}
|
|
47
61
|
if (!files || files.length === 0)
|
|
48
62
|
return null;
|
|
49
|
-
return (_jsxs("div", { className: cn("rounded-lg border border-border overflow-hidden", className), children: [_jsx("div", { className: "border-b border-border px-4 py-2", children: _jsx("h3", { className: "text-xs font-medium uppercase tracking-wider text-muted-foreground", children: "Package Files" }) }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-[220px_1fr]", children: [_jsx("nav", { className: "border-b border-border md:border-b-0 md:border-r overflow-y-auto", "aria-label": "Skill package file tree", children: _jsx("ul", { className: "py-1", role: "tree", children: treeNodes.map((node) => (_jsx(FileTreeNode, { node: node, selectedPath: selectedPath, onSelect:
|
|
63
|
+
return (_jsxs("div", { className: cn("rounded-lg border border-border overflow-hidden", className), children: [_jsx("div", { className: "border-b border-border px-4 py-2", children: _jsx("h3", { className: "text-xs font-medium uppercase tracking-wider text-muted-foreground", children: "Package Files" }) }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-[220px_1fr]", children: [_jsx("nav", { className: "border-b border-border md:border-b-0 md:border-r overflow-y-auto", "aria-label": "Skill package file tree", children: _jsx("ul", { className: "py-1", role: "tree", children: treeNodes.map((node) => (_jsx(FileTreeNode, { node: node, selectedPath: selectedPath ?? "", onSelect: setUserSelectedPath, depth: 0 }, node.path))) }) }), _jsx("div", { className: "min-h-[200px] max-h-[400px] overflow-y-auto p-4", children: selectedPath !== null && content !== null ? (_jsx(FileContentViewer, { path: selectedPath, content: content })) : (_jsx("p", { className: "text-sm text-muted-foreground-subtle italic", children: "Select a file to view its contents" })) })] })] }));
|
|
50
64
|
}
|
|
51
65
|
// ---------------------------------------------------------------------------
|
|
52
66
|
// Content viewer
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SkillFileBrowser.js","sourceRoot":"","sources":["../../src/skill/SkillFileBrowser.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAC1C,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAC;AACpC,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,oCAAoC,CAAC;AAC3G,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AAC7E,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAczD,8EAA8E;AAC9E,iBAAiB;AACjB,8EAA8E;AAE9E
|
|
1
|
+
{"version":3,"file":"SkillFileBrowser.js","sourceRoot":"","sources":["../../src/skill/SkillFileBrowser.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetchAndUnpackArtifact.d.ts","sourceRoot":"","sources":["../../../src/skill/internal/fetchAndUnpackArtifact.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,8EAA8E;AAC9E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,cAAc,EAAE,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClD;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,OAAO,EAChB,kBAAkB,EAAE,MAAM,GACzB,OAAO,CAAC,gBAAgB,CAAC,
|
|
1
|
+
{"version":3,"file":"fetchAndUnpackArtifact.d.ts","sourceRoot":"","sources":["../../../src/skill/internal/fetchAndUnpackArtifact.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,8EAA8E;AAC9E,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,cAAc,EAAE,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClD;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,OAAO,EAChB,kBAAkB,EAAE,MAAM,GACzB,OAAO,CAAC,gBAAgB,CAAC,CA6B3B"}
|
|
@@ -18,9 +18,12 @@ export async function fetchAndUnpackArtifact(stigmer, artifactStorageKey) {
|
|
|
18
18
|
size: data.length,
|
|
19
19
|
isDirectory: data.length === 0 && path.endsWith("/"),
|
|
20
20
|
}));
|
|
21
|
+
// Every non-directory entry gets a map entry — including zero-byte files,
|
|
22
|
+
// which map to "". Skipping them would make an empty file unreadable in the
|
|
23
|
+
// browser and read as *deleted* (rather than emptied) in version diffs.
|
|
21
24
|
const contentMap = new Map();
|
|
22
25
|
for (const [path, data] of entries) {
|
|
23
|
-
if (!path.endsWith("/")
|
|
26
|
+
if (!path.endsWith("/")) {
|
|
24
27
|
try {
|
|
25
28
|
contentMap.set(path, strFromU8(data));
|
|
26
29
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetchAndUnpackArtifact.js","sourceRoot":"","sources":["../../../src/skill/internal/fetchAndUnpackArtifact.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,wBAAwB,EAAE,MAAM,mDAAmD,CAAC;AAU7F;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,OAAgB,EAChB,kBAA0B;IAE1B,MAAM,OAAO,GAAG,MAAM,CAAC,wBAAwB,EAAE,EAAE,kBAAkB,EAAE,CAAC,CAAC;IACzE,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IAE1D,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;IACxD,MAAM,QAAQ,GAAG,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAE9C,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACzC,MAAM,KAAK,GAAqB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;QAC7D,IAAI;QACJ,IAAI,EAAE,IAAI,CAAC,MAAM;QACjB,WAAW,EAAE,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;KACrD,CAAC,CAAC,CAAC;IAEJ,MAAM,UAAU,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC7C,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,OAAO,EAAE,CAAC;QACnC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,
|
|
1
|
+
{"version":3,"file":"fetchAndUnpackArtifact.js","sourceRoot":"","sources":["../../../src/skill/internal/fetchAndUnpackArtifact.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,wBAAwB,EAAE,MAAM,mDAAmD,CAAC;AAU7F;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,OAAgB,EAChB,kBAA0B;IAE1B,MAAM,OAAO,GAAG,MAAM,CAAC,wBAAwB,EAAE,EAAE,kBAAkB,EAAE,CAAC,CAAC;IACzE,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IAE1D,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;IACxD,MAAM,QAAQ,GAAG,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAE9C,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACzC,MAAM,KAAK,GAAqB,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;QAC7D,IAAI;QACJ,IAAI,EAAE,IAAI,CAAC,MAAM;QACjB,WAAW,EAAE,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;KACrD,CAAC,CAAC,CAAC;IAEJ,0EAA0E;IAC1E,4EAA4E;IAC5E,wEAAwE;IACxE,MAAM,UAAU,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC7C,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,OAAO,EAAE,CAAC;QACnC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YACxB,IAAI,CAAC;gBACH,UAAU,CAAC,GAAG,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;YACxC,CAAC;YAAC,MAAM,CAAC;gBACP,UAAU,CAAC,GAAG,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC;YAC3C,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;AAC/B,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSkillArtifact.d.ts","sourceRoot":"","sources":["../../src/skill/useSkillArtifact.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAO1D,gDAAgD;AAChD,MAAM,WAAW,sBAAsB;IACrC,2EAA2E;IAC3E,QAAQ,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,IAAI,CAAC;IACxC,+DAA+D;IAC/D,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC7B,0EAA0E;IAC1E,QAAQ,CAAC,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACzD,6CAA6C;IAC7C,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;CAC9B;AAMD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAC9B,kBAAkB,EAAE,MAAM,GAAG,IAAI,GAChC,sBAAsB,
|
|
1
|
+
{"version":3,"file":"useSkillArtifact.d.ts","sourceRoot":"","sources":["../../src/skill/useSkillArtifact.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAO1D,gDAAgD;AAChD,MAAM,WAAW,sBAAsB;IACrC,2EAA2E;IAC3E,QAAQ,CAAC,KAAK,EAAE,cAAc,EAAE,GAAG,IAAI,CAAC;IACxC,+DAA+D;IAC/D,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC7B,0EAA0E;IAC1E,QAAQ,CAAC,cAAc,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACzD,6CAA6C;IAC7C,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;CAC9B;AAMD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,CAC9B,kBAAkB,EAAE,MAAM,GAAG,IAAI,GAChC,sBAAsB,CA8DxB"}
|
|
@@ -30,13 +30,18 @@ import { fetchAndUnpackArtifact } from "./internal/fetchAndUnpackArtifact.js";
|
|
|
30
30
|
export function useSkillArtifact(artifactStorageKey) {
|
|
31
31
|
const stigmer = useStigmer();
|
|
32
32
|
const [files, setFiles] = useState(null);
|
|
33
|
+
// State, not a ref: `getFileContent` derives its identity from this map, so
|
|
34
|
+
// consumers' memos invalidate exactly when the artifact's contents arrive.
|
|
35
|
+
// A ref here once made the lookup silently non-reactive — a viewer memoizing
|
|
36
|
+
// `getFileContent(path)` stayed stuck on the pre-download `null` forever.
|
|
37
|
+
const [contentMap, setContentMap] = useState(new Map());
|
|
33
38
|
const [isLoading, setIsLoading] = useState(false);
|
|
34
39
|
const [error, setError] = useState(null);
|
|
35
|
-
const contentMapRef = useRef(new Map());
|
|
36
40
|
const fetchIdRef = useRef(0);
|
|
37
41
|
const doFetch = useCallback(async () => {
|
|
38
42
|
if (!artifactStorageKey) {
|
|
39
43
|
setFiles(null);
|
|
44
|
+
setContentMap(new Map());
|
|
40
45
|
setError(null);
|
|
41
46
|
return;
|
|
42
47
|
}
|
|
@@ -47,7 +52,7 @@ export function useSkillArtifact(artifactStorageKey) {
|
|
|
47
52
|
const result = await fetchAndUnpackArtifact(stigmer, artifactStorageKey);
|
|
48
53
|
if (fetchIdRef.current !== fetchId)
|
|
49
54
|
return;
|
|
50
|
-
|
|
55
|
+
setContentMap(result.contentMap);
|
|
51
56
|
setFiles(result.files);
|
|
52
57
|
}
|
|
53
58
|
catch (err) {
|
|
@@ -55,6 +60,7 @@ export function useSkillArtifact(artifactStorageKey) {
|
|
|
55
60
|
return;
|
|
56
61
|
setError(toError(err));
|
|
57
62
|
setFiles(null);
|
|
63
|
+
setContentMap(new Map());
|
|
58
64
|
}
|
|
59
65
|
finally {
|
|
60
66
|
if (fetchIdRef.current === fetchId) {
|
|
@@ -65,9 +71,7 @@ export function useSkillArtifact(artifactStorageKey) {
|
|
|
65
71
|
useEffect(() => {
|
|
66
72
|
doFetch();
|
|
67
73
|
}, [doFetch]);
|
|
68
|
-
const getFileContent = useCallback((path) =>
|
|
69
|
-
return contentMapRef.current.get(path) ?? null;
|
|
70
|
-
}, []);
|
|
74
|
+
const getFileContent = useCallback((path) => contentMap.get(path) ?? null, [contentMap]);
|
|
71
75
|
const refetch = useCallback(() => {
|
|
72
76
|
doFetch();
|
|
73
77
|
}, [doFetch]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useSkillArtifact.js","sourceRoot":"","sources":["../../src/skill/useSkillArtifact.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAoB9E,8EAA8E;AAC9E,sBAAsB;AACtB,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,gBAAgB,CAC9B,kBAAiC;IAEjC,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAA0B,IAAI,CAAC,CAAC;IAClE,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"useSkillArtifact.js","sourceRoot":"","sources":["../../src/skill/useSkillArtifact.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
-
import {
|
|
1
|
+
import { describe, it, expect, vi, afterEach, beforeEach } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
render,
|
|
4
|
+
screen,
|
|
5
|
+
fireEvent,
|
|
6
|
+
cleanup,
|
|
7
|
+
waitFor,
|
|
8
|
+
} from "@testing-library/react";
|
|
3
9
|
import type { ReactNode } from "react";
|
|
4
10
|
import { create } from "@bufbuild/protobuf";
|
|
5
11
|
import type { Stigmer } from "@stigmer/sdk";
|
|
@@ -47,6 +53,17 @@ function renderCard(ui: ReactNode) {
|
|
|
47
53
|
|
|
48
54
|
afterEach(cleanup);
|
|
49
55
|
|
|
56
|
+
// The download flow ends in a transient-anchor click with target="_blank".
|
|
57
|
+
// Left unstubbed, happy-dom treats that click as a real popup navigation and
|
|
58
|
+
// fetches the fixture URL over the actual network — after this test has
|
|
59
|
+
// already returned, so the DNS failure gets attributed to whichever test the
|
|
60
|
+
// worker is running by then (the flake in issue #334).
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
vi.spyOn(HTMLAnchorElement.prototype, "click")
|
|
63
|
+
.mockImplementation(() => {})
|
|
64
|
+
.mockClear();
|
|
65
|
+
});
|
|
66
|
+
|
|
50
67
|
describe("PlanArtifactCard — compact document card", () => {
|
|
51
68
|
it("is an accessible region with the plan's title, filename, and size", () => {
|
|
52
69
|
renderCard(
|
|
@@ -178,6 +195,7 @@ describe("PlanArtifactCard — compact document card", () => {
|
|
|
178
195
|
|
|
179
196
|
it("downloads the artifact on demand via a freshly minted URL", async () => {
|
|
180
197
|
const { stigmer, getArtifactDownloadUrl } = createStigmerMock();
|
|
198
|
+
const anchorClick = vi.spyOn(HTMLAnchorElement.prototype, "click");
|
|
181
199
|
render(
|
|
182
200
|
withStigmer(
|
|
183
201
|
<PlanArtifactCard executionId="aex_1" artifact={planArtifact} org="acme" />,
|
|
@@ -191,6 +209,9 @@ describe("PlanArtifactCard — compact document card", () => {
|
|
|
191
209
|
const req = getArtifactDownloadUrl.mock.calls[0][0];
|
|
192
210
|
expect(req.executionId).toBe("aex_1");
|
|
193
211
|
expect(req.storageKey).toBe("artifacts/aex_1/plan.md");
|
|
212
|
+
// Wait for the mint-URL → anchor-click continuation INSIDE the test body;
|
|
213
|
+
// returning while it is still in flight is what let it escape.
|
|
214
|
+
await waitFor(() => expect(anchorClick).toHaveBeenCalledTimes(1));
|
|
194
215
|
});
|
|
195
216
|
});
|
|
196
217
|
|
|
@@ -30,7 +30,8 @@ export interface SkillFileBrowserProps {
|
|
|
30
30
|
* - Left panel: file tree with folder grouping
|
|
31
31
|
* - Right panel: content viewer (rendered Markdown for .md, raw code otherwise)
|
|
32
32
|
*
|
|
33
|
-
* SKILL.md is selected by default on load.
|
|
33
|
+
* SKILL.md is selected by default on load; packages without a root SKILL.md
|
|
34
|
+
* fall back to the first file in tree order.
|
|
34
35
|
* Responsive: stacked layout on mobile viewports.
|
|
35
36
|
*
|
|
36
37
|
* Zero Console dependencies — safe for platform builder embedding.
|
|
@@ -50,17 +51,28 @@ export function SkillFileBrowser({
|
|
|
50
51
|
className,
|
|
51
52
|
}: SkillFileBrowserProps) {
|
|
52
53
|
const { files, isLoading, error, getFileContent } = useSkillArtifact(artifactStorageKey);
|
|
53
|
-
|
|
54
|
+
// Only the user's explicit click is state; the initial selection is derived
|
|
55
|
+
// from the loaded file list below, so it exists the moment files do.
|
|
56
|
+
const [userSelectedPath, setUserSelectedPath] = useState<string | null>(null);
|
|
54
57
|
|
|
55
|
-
const
|
|
56
|
-
() => (files ?
|
|
58
|
+
const fileEntries = useMemo(
|
|
59
|
+
() => (files ? files.filter((f) => !f.isDirectory) : []),
|
|
57
60
|
[files],
|
|
58
61
|
);
|
|
59
62
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
const treeNodes = useMemo(() => buildFileTree(fileEntries), [fileEntries]);
|
|
64
|
+
|
|
65
|
+
// Default to the manifest; fall back to the first file in rendered tree
|
|
66
|
+
// order (buildFileTree sorts by localeCompare, and depth-first tree order
|
|
67
|
+
// follows that sort) so the auto-highlighted row is the topmost file.
|
|
68
|
+
const defaultPath = useMemo(() => {
|
|
69
|
+
if (fileEntries.some((f) => f.path === "SKILL.md")) return "SKILL.md";
|
|
70
|
+
const sorted = [...fileEntries].sort((a, b) => a.path.localeCompare(b.path));
|
|
71
|
+
return sorted[0]?.path ?? null;
|
|
72
|
+
}, [fileEntries]);
|
|
73
|
+
|
|
74
|
+
const selectedPath = userSelectedPath ?? defaultPath;
|
|
75
|
+
const content = selectedPath !== null ? getFileContent(selectedPath) : null;
|
|
64
76
|
|
|
65
77
|
if (!artifactStorageKey) return null;
|
|
66
78
|
|
|
@@ -125,8 +137,8 @@ export function SkillFileBrowser({
|
|
|
125
137
|
<FileTreeNode
|
|
126
138
|
key={node.path}
|
|
127
139
|
node={node}
|
|
128
|
-
selectedPath={selectedPath}
|
|
129
|
-
onSelect={
|
|
140
|
+
selectedPath={selectedPath ?? ""}
|
|
141
|
+
onSelect={setUserSelectedPath}
|
|
130
142
|
depth={0}
|
|
131
143
|
/>
|
|
132
144
|
))}
|
|
@@ -135,7 +147,7 @@ export function SkillFileBrowser({
|
|
|
135
147
|
|
|
136
148
|
{/* Content viewer */}
|
|
137
149
|
<div className="min-h-[200px] max-h-[400px] overflow-y-auto p-4">
|
|
138
|
-
{content !== null ? (
|
|
150
|
+
{selectedPath !== null && content !== null ? (
|
|
139
151
|
<FileContentViewer path={selectedPath} content={content} />
|
|
140
152
|
) : (
|
|
141
153
|
<p className="text-sm text-muted-foreground-subtle italic">
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { zipSync, strToU8 } from "fflate";
|
|
5
|
+
import type { Stigmer } from "@stigmer/sdk";
|
|
6
|
+
import { StigmerContext } from "../../context";
|
|
7
|
+
import { SkillFileBrowser } from "../SkillFileBrowser";
|
|
8
|
+
|
|
9
|
+
function wrapperFor(stigmer: Stigmer) {
|
|
10
|
+
return ({ children }: { children: ReactNode }) => (
|
|
11
|
+
<StigmerContext.Provider value={stigmer}>{children}</StigmerContext.Provider>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Builds a real ZIP (via fflate, the same library the unpacker uses) and a
|
|
17
|
+
* Stigmer client whose `skill.getArtifact` serves it — so tests exercise the
|
|
18
|
+
* genuine fetch → unzip → browse path instead of stubbing the unpacker.
|
|
19
|
+
*/
|
|
20
|
+
function makeStigmerServing(files: Record<string, Uint8Array>) {
|
|
21
|
+
const getArtifact = vi.fn().mockResolvedValue({ artifact: zipSync(files) });
|
|
22
|
+
return { skill: { getArtifact } } as unknown as Stigmer;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function renderBrowser(stigmer: Stigmer) {
|
|
26
|
+
return render(<SkillFileBrowser artifactStorageKey="skills/test/pkg.zip" />, {
|
|
27
|
+
wrapper: wrapperFor(stigmer),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const PLACEHOLDER = "Select a file to view its contents";
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("SkillFileBrowser", () => {
|
|
38
|
+
it("renders SKILL.md content without any click for a single-file package", async () => {
|
|
39
|
+
// Regression: the content lookup used to be memoized against a frozen
|
|
40
|
+
// getter, so a one-file package stayed stuck on the placeholder forever.
|
|
41
|
+
const stigmer = makeStigmerServing({
|
|
42
|
+
"SKILL.md": strToU8("# Docs Skill\n\nAnswers questions about Stigmer."),
|
|
43
|
+
});
|
|
44
|
+
renderBrowser(stigmer);
|
|
45
|
+
|
|
46
|
+
expect(
|
|
47
|
+
await screen.findByRole("heading", { name: "Docs Skill" }),
|
|
48
|
+
).toBeTruthy();
|
|
49
|
+
expect(screen.queryByText(PLACEHOLDER)).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("prefers SKILL.md as the default in a multi-file package and switches on click", async () => {
|
|
53
|
+
const stigmer = makeStigmerServing({
|
|
54
|
+
"SKILL.md": strToU8("# Manifest"),
|
|
55
|
+
"scripts/validate.py": strToU8('print("validated")'),
|
|
56
|
+
});
|
|
57
|
+
renderBrowser(stigmer);
|
|
58
|
+
|
|
59
|
+
expect(
|
|
60
|
+
await screen.findByRole("heading", { name: "Manifest" }),
|
|
61
|
+
).toBeTruthy();
|
|
62
|
+
|
|
63
|
+
fireEvent.click(screen.getByRole("button", { name: "validate.py" }));
|
|
64
|
+
expect(screen.getByText('print("validated")')).toBeTruthy();
|
|
65
|
+
expect(
|
|
66
|
+
screen.queryByRole("heading", { name: "Manifest" }),
|
|
67
|
+
).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("falls back to the first file in tree order when there is no root SKILL.md", async () => {
|
|
71
|
+
// buildFileTree sorts by localeCompare, so "alpha/inner.md" renders above
|
|
72
|
+
// "beta.md" — the default selection must match what the user sees on top.
|
|
73
|
+
const stigmer = makeStigmerServing({
|
|
74
|
+
"beta.md": strToU8("# Beta"),
|
|
75
|
+
"alpha/inner.md": strToU8("# Inner Alpha"),
|
|
76
|
+
});
|
|
77
|
+
renderBrowser(stigmer);
|
|
78
|
+
|
|
79
|
+
expect(
|
|
80
|
+
await screen.findByRole("heading", { name: "Inner Alpha" }),
|
|
81
|
+
).toBeTruthy();
|
|
82
|
+
expect(screen.queryByText(PLACEHOLDER)).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("shows an empty file as (empty) content, not the placeholder", async () => {
|
|
86
|
+
const stigmer = makeStigmerServing({
|
|
87
|
+
"SKILL.md": strToU8("# Manifest"),
|
|
88
|
+
"notes.txt": new Uint8Array(0),
|
|
89
|
+
});
|
|
90
|
+
renderBrowser(stigmer);
|
|
91
|
+
|
|
92
|
+
await screen.findByRole("heading", { name: "Manifest" });
|
|
93
|
+
fireEvent.click(screen.getByRole("button", { name: "notes.txt" }));
|
|
94
|
+
|
|
95
|
+
// The viewer switched away from the manifest and, despite the file's
|
|
96
|
+
// empty content, did not regress to the "select a file" placeholder.
|
|
97
|
+
expect(screen.queryByRole("heading", { name: "Manifest" })).toBeNull();
|
|
98
|
+
expect(screen.queryByText(PLACEHOLDER)).toBeNull();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -33,9 +33,12 @@ export async function fetchAndUnpackArtifact(
|
|
|
33
33
|
isDirectory: data.length === 0 && path.endsWith("/"),
|
|
34
34
|
}));
|
|
35
35
|
|
|
36
|
+
// Every non-directory entry gets a map entry — including zero-byte files,
|
|
37
|
+
// which map to "". Skipping them would make an empty file unreadable in the
|
|
38
|
+
// browser and read as *deleted* (rather than emptied) in version diffs.
|
|
36
39
|
const contentMap = new Map<string, string>();
|
|
37
40
|
for (const [path, data] of entries) {
|
|
38
|
-
if (!path.endsWith("/")
|
|
41
|
+
if (!path.endsWith("/")) {
|
|
39
42
|
try {
|
|
40
43
|
contentMap.set(path, strFromU8(data));
|
|
41
44
|
} catch {
|
|
@@ -54,14 +54,21 @@ export function useSkillArtifact(
|
|
|
54
54
|
): UseSkillArtifactReturn {
|
|
55
55
|
const stigmer = useStigmer();
|
|
56
56
|
const [files, setFiles] = useState<SkillFileEntry[] | null>(null);
|
|
57
|
+
// State, not a ref: `getFileContent` derives its identity from this map, so
|
|
58
|
+
// consumers' memos invalidate exactly when the artifact's contents arrive.
|
|
59
|
+
// A ref here once made the lookup silently non-reactive — a viewer memoizing
|
|
60
|
+
// `getFileContent(path)` stayed stuck on the pre-download `null` forever.
|
|
61
|
+
const [contentMap, setContentMap] = useState<ReadonlyMap<string, string>>(
|
|
62
|
+
new Map(),
|
|
63
|
+
);
|
|
57
64
|
const [isLoading, setIsLoading] = useState(false);
|
|
58
65
|
const [error, setError] = useState<Error | null>(null);
|
|
59
|
-
const contentMapRef = useRef<Map<string, string>>(new Map());
|
|
60
66
|
const fetchIdRef = useRef(0);
|
|
61
67
|
|
|
62
68
|
const doFetch = useCallback(async () => {
|
|
63
69
|
if (!artifactStorageKey) {
|
|
64
70
|
setFiles(null);
|
|
71
|
+
setContentMap(new Map());
|
|
65
72
|
setError(null);
|
|
66
73
|
return;
|
|
67
74
|
}
|
|
@@ -75,12 +82,13 @@ export function useSkillArtifact(
|
|
|
75
82
|
|
|
76
83
|
if (fetchIdRef.current !== fetchId) return;
|
|
77
84
|
|
|
78
|
-
|
|
85
|
+
setContentMap(result.contentMap);
|
|
79
86
|
setFiles(result.files);
|
|
80
87
|
} catch (err) {
|
|
81
88
|
if (fetchIdRef.current !== fetchId) return;
|
|
82
89
|
setError(toError(err));
|
|
83
90
|
setFiles(null);
|
|
91
|
+
setContentMap(new Map());
|
|
84
92
|
} finally {
|
|
85
93
|
if (fetchIdRef.current === fetchId) {
|
|
86
94
|
setIsLoading(false);
|
|
@@ -92,9 +100,10 @@ export function useSkillArtifact(
|
|
|
92
100
|
doFetch();
|
|
93
101
|
}, [doFetch]);
|
|
94
102
|
|
|
95
|
-
const getFileContent = useCallback(
|
|
96
|
-
|
|
97
|
-
|
|
103
|
+
const getFileContent = useCallback(
|
|
104
|
+
(path: string): string | null => contentMap.get(path) ?? null,
|
|
105
|
+
[contentMap],
|
|
106
|
+
);
|
|
98
107
|
|
|
99
108
|
const refetch = useCallback(() => {
|
|
100
109
|
doFetch();
|