@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stigmer/react",
3
- "version": "3.5.1",
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.1",
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.1",
64
- "@stigmer/sdk": "3.5.1",
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;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,kBAAkB,EAClB,SAAS,GACV,EAAE,qBAAqB,kDAkGvB"}
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
- const [selectedPath, setSelectedPath] = useState("SKILL.md");
37
- const treeNodes = useMemo(() => (files ? buildFileTree(files.filter((f) => !f.isDirectory)) : []), [files]);
38
- const content = useMemo(() => (selectedPath ? getFileContent(selectedPath) : null), [selectedPath, getFileContent]);
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: setSelectedPath, depth: 0 }, node.path))) }) }), _jsx("div", { className: "min-h-[200px] max-h-[400px] overflow-y-auto p-4", children: 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" })) })] })] }));
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":"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"}
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,CA0B3B"}
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("/") && data.length > 0) {
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,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3C,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
+ {"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,CAqDxB"}
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
- contentMapRef.current = new Map(result.contentMap);
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":"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"}
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 { render, screen, fireEvent, cleanup } from "@testing-library/react";
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
- const [selectedPath, setSelectedPath] = useState<string>("SKILL.md");
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 treeNodes = useMemo(
56
- () => (files ? buildFileTree(files.filter((f) => !f.isDirectory)) : []),
58
+ const fileEntries = useMemo(
59
+ () => (files ? files.filter((f) => !f.isDirectory) : []),
57
60
  [files],
58
61
  );
59
62
 
60
- const content = useMemo(
61
- () => (selectedPath ? getFileContent(selectedPath) : null),
62
- [selectedPath, getFileContent],
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={setSelectedPath}
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("/") && data.length > 0) {
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
- contentMapRef.current = new Map(result.contentMap);
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((path: string): string | null => {
96
- return contentMapRef.current.get(path) ?? null;
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();