@stigmer/react 3.5.0 → 3.5.2

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.0",
3
+ "version": "3.5.2",
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.0",
40
+ "@stigmer/theme": "3.5.2",
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.0",
64
- "@stigmer/sdk": "3.5.0",
63
+ "@stigmer/protos": "3.5.2",
64
+ "@stigmer/sdk": "3.5.2",
65
65
  "@tanstack/react-table": "^8.20.0",
66
66
  "@xyflow/react": "^12.0.0",
67
67
  "elkjs": "^0.9.0",
@@ -62,5 +62,19 @@ export interface UseSessionExecutionsReturn {
62
62
  * const { executions } = useSessionExecutions(sessionId ?? null);
63
63
  * ```
64
64
  */
65
+ /**
66
+ * Returns the executions in chronological (oldest-first) order — the order
67
+ * the conversation thread renders top-to-bottom.
68
+ *
69
+ * Defense-in-depth against an unordered list response: the executions ARE the
70
+ * transcript, so a scrambled order drops the newest turns out of view (they no
71
+ * longer sort to the bottom). The server orders this list, but the thread must
72
+ * never depend on that alone. Resource ids are time-sortable ULIDs
73
+ * (`aex_01k…`), so an ascending id sort is creation order without parsing
74
+ * timestamps; entries missing an id sort last but keep a stable relative order.
75
+ *
76
+ * @internal Exported for testing — not part of the public API.
77
+ */
78
+ export declare function sortChronologically(executions: readonly AgentExecution[]): AgentExecution[];
65
79
  export declare function useSessionExecutions(sessionId: string | null, options?: UseSessionExecutionsOptions): UseSessionExecutionsReturn;
66
80
  //# sourceMappingURL=useSessionExecutions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useSessionExecutions.d.ts","sourceRoot":"","sources":["../../src/session/useSessionExecutions.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6DAA6D,CAAC;AAKlG,gDAAgD;AAChD,MAAM,WAAW,2BAA2B;IAC1C;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC1C;;;;OAIG;IACH,QAAQ,CAAC,oBAAoB,CAAC,EAAE,OAAO,CAAC;CACzC;AAED,oDAAoD;AACpD,MAAM,WAAW,0BAA0B;IACzC,uEAAuE;IACvE,QAAQ,CAAC,UAAU,EAAE,SAAS,cAAc,EAAE,CAAC;IAC/C,mDAAmD;IACnD,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,sDAAsD;IACtD,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,kEAAkE;IAClE,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC7B,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,OAAO,CAAC,EAAE,2BAA2B,GACpC,0BAA0B,CAyB5B"}
1
+ {"version":3,"file":"useSessionExecutions.d.ts","sourceRoot":"","sources":["../../src/session/useSessionExecutions.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6DAA6D,CAAC;AAKlG,gDAAgD;AAChD,MAAM,WAAW,2BAA2B;IAC1C;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC1C;;;;OAIG;IACH,QAAQ,CAAC,oBAAoB,CAAC,EAAE,OAAO,CAAC;CACzC;AAED,oDAAoD;AACpD,MAAM,WAAW,0BAA0B;IACzC,uEAAuE;IACvE,QAAQ,CAAC,UAAU,EAAE,SAAS,cAAc,EAAE,CAAC;IAC/C,mDAAmD;IACnD,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,sDAAsD;IACtD,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B,kEAAkE;IAClE,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC7B,2EAA2E;IAC3E,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,SAAS,cAAc,EAAE,GACpC,cAAc,EAAE,CASlB;AAED,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,OAAO,CAAC,EAAE,2BAA2B,GACpC,0BAA0B,CAyB5B"}
@@ -37,6 +37,32 @@ import { useFetch } from "../internal/useFetch.js";
37
37
  * const { executions } = useSessionExecutions(sessionId ?? null);
38
38
  * ```
39
39
  */
40
+ /**
41
+ * Returns the executions in chronological (oldest-first) order — the order
42
+ * the conversation thread renders top-to-bottom.
43
+ *
44
+ * Defense-in-depth against an unordered list response: the executions ARE the
45
+ * transcript, so a scrambled order drops the newest turns out of view (they no
46
+ * longer sort to the bottom). The server orders this list, but the thread must
47
+ * never depend on that alone. Resource ids are time-sortable ULIDs
48
+ * (`aex_01k…`), so an ascending id sort is creation order without parsing
49
+ * timestamps; entries missing an id sort last but keep a stable relative order.
50
+ *
51
+ * @internal Exported for testing — not part of the public API.
52
+ */
53
+ export function sortChronologically(executions) {
54
+ return [...executions].sort((a, b) => {
55
+ const aId = a.metadata?.id ?? "";
56
+ const bId = b.metadata?.id ?? "";
57
+ if (aId === bId)
58
+ return 0;
59
+ if (!aId)
60
+ return 1;
61
+ if (!bId)
62
+ return -1;
63
+ return aId < bId ? -1 : 1;
64
+ });
65
+ }
40
66
  export function useSessionExecutions(sessionId, options) {
41
67
  const stigmer = useStigmer();
42
68
  const { data: executions, isLoading, isRefetching, error, refetch } = useFetch(sessionId
@@ -45,7 +71,7 @@ export function useSessionExecutions(sessionId, options) {
45
71
  sessionId,
46
72
  pageSize: 100,
47
73
  }))
48
- .then((result) => result.entries)
74
+ .then((result) => sortChronologically(result.entries))
49
75
  : null, [sessionId, stigmer], [], {
50
76
  cacheKey: sessionId ? `session-executions:${sessionId}` : undefined,
51
77
  refetchInterval: options?.refetchInterval,
@@ -1 +1 @@
1
- {"version":3,"file":"useSessionExecutions.js","sourceRoot":"","sources":["../../src/session/useSessionExecutions.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,OAAO,EAAE,yCAAyC,EAAE,MAAM,4DAA4D,CAAC;AACvH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAiCnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,UAAU,oBAAoB,CAClC,SAAwB,EACxB,OAAqC;IAErC,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAE7B,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,QAAQ,CAC5E,SAAS;QACP,CAAC,CAAC,GAAG,EAAE,CACH,OAAO,CAAC,cAAc;aACnB,aAAa,CACZ,MAAM,CAAC,yCAAyC,EAAE;YAChD,SAAS;YACT,QAAQ,EAAE,GAAG;SACd,CAAC,CACH;aACA,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC;QACvC,CAAC,CAAC,IAAI,EACR,CAAC,SAAS,EAAE,OAAO,CAAC,EACpB,EAAsB,EACtB;QACE,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC,sBAAsB,SAAS,EAAE,CAAC,CAAC,CAAC,SAAS;QACnE,eAAe,EAAE,OAAO,EAAE,eAAe;QACzC,oBAAoB,EAAE,OAAO,EAAE,oBAAoB;KACpD,CACF,CAAC;IAEF,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;AACjE,CAAC"}
1
+ {"version":3,"file":"useSessionExecutions.js","sourceRoot":"","sources":["../../src/session/useSessionExecutions.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,OAAO,EAAE,yCAAyC,EAAE,MAAM,4DAA4D,CAAC;AACvH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAiCnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,mBAAmB,CACjC,UAAqC;IAErC,OAAO,CAAC,GAAG,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACnC,MAAM,GAAG,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC;QACjC,MAAM,GAAG,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC;QACjC,IAAI,GAAG,KAAK,GAAG;YAAE,OAAO,CAAC,CAAC;QAC1B,IAAI,CAAC,GAAG;YAAE,OAAO,CAAC,CAAC;QACnB,IAAI,CAAC,GAAG;YAAE,OAAO,CAAC,CAAC,CAAC;QACpB,OAAO,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAC5B,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,SAAwB,EACxB,OAAqC;IAErC,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAE7B,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,QAAQ,CAC5E,SAAS;QACP,CAAC,CAAC,GAAG,EAAE,CACH,OAAO,CAAC,cAAc;aACnB,aAAa,CACZ,MAAM,CAAC,yCAAyC,EAAE;YAChD,SAAS;YACT,QAAQ,EAAE,GAAG;SACd,CAAC,CACH;aACA,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,mBAAmB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC5D,CAAC,CAAC,IAAI,EACR,CAAC,SAAS,EAAE,OAAO,CAAC,EACpB,EAAsB,EACtB;QACE,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC,sBAAsB,SAAS,EAAE,CAAC,CAAC,CAAC,SAAS;QACnE,eAAe,EAAE,OAAO,EAAE,eAAe;QACzC,oBAAoB,EAAE,OAAO,EAAE,oBAAoB;KACpD,CACF,CAAC;IAEF,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;AACjE,CAAC"}
@@ -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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"}
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { create } from "@bufbuild/protobuf";
3
+ import {
4
+ AgentExecutionSchema,
5
+ type AgentExecution,
6
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
7
+ import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
8
+ import { sortChronologically } from "../useSessionExecutions";
9
+
10
+ function exec(id: string): AgentExecution {
11
+ const e = create(AgentExecutionSchema);
12
+ const metadata = create(ApiResourceMetadataSchema);
13
+ metadata.id = id;
14
+ e.metadata = metadata;
15
+ return e;
16
+ }
17
+
18
+ function ids(list: readonly AgentExecution[]): string[] {
19
+ return list.map((e) => e.metadata?.id ?? "");
20
+ }
21
+
22
+ describe("sortChronologically", () => {
23
+ it("orders executions oldest-first by their ULID id", () => {
24
+ // Real ids from the pilot session, returned by the server in scrambled
25
+ // heap order — the afternoon turns (…vk…) interleaved with the evening
26
+ // turns (…w3…/…w4…). Chronological order is ascending ULID.
27
+ const scrambled = [
28
+ exec("aex_01kyw3zeq8cre6cc8gznf31y13"),
29
+ exec("aex_01kyvk1hxyrhch1q23p5j177pb"),
30
+ exec("aex_01kyw416w9mg6ezzpkc6q9y71x"),
31
+ exec("aex_01kyvk2sh0vjc69c7z3q4c9r1m"),
32
+ exec("aex_01kyw45qkf88x28pf9bxmrr3bf"),
33
+ exec("aex_01kyw3y0rfx9vd7dhnawgqgnb0"),
34
+ exec("aex_01kyvk3v34vh9phedx2ezajxtr"),
35
+ ];
36
+
37
+ expect(ids(sortChronologically(scrambled))).toEqual([
38
+ "aex_01kyvk1hxyrhch1q23p5j177pb",
39
+ "aex_01kyvk2sh0vjc69c7z3q4c9r1m",
40
+ "aex_01kyvk3v34vh9phedx2ezajxtr",
41
+ "aex_01kyw3y0rfx9vd7dhnawgqgnb0",
42
+ "aex_01kyw3zeq8cre6cc8gznf31y13",
43
+ "aex_01kyw416w9mg6ezzpkc6q9y71x",
44
+ "aex_01kyw45qkf88x28pf9bxmrr3bf",
45
+ ]);
46
+ });
47
+
48
+ it("does not mutate the input array", () => {
49
+ const input = [exec("aex_02"), exec("aex_01")];
50
+ const before = ids(input);
51
+ sortChronologically(input);
52
+ expect(ids(input)).toEqual(before);
53
+ });
54
+
55
+ it("sorts entries missing an id last", () => {
56
+ const list = [exec("aex_02"), exec(""), exec("aex_01")];
57
+ expect(ids(sortChronologically(list))).toEqual(["aex_01", "aex_02", ""]);
58
+ });
59
+
60
+ it("returns an empty array unchanged", () => {
61
+ expect(sortChronologically([])).toEqual([]);
62
+ });
63
+ });
@@ -71,6 +71,32 @@ export interface UseSessionExecutionsReturn {
71
71
  * const { executions } = useSessionExecutions(sessionId ?? null);
72
72
  * ```
73
73
  */
74
+ /**
75
+ * Returns the executions in chronological (oldest-first) order — the order
76
+ * the conversation thread renders top-to-bottom.
77
+ *
78
+ * Defense-in-depth against an unordered list response: the executions ARE the
79
+ * transcript, so a scrambled order drops the newest turns out of view (they no
80
+ * longer sort to the bottom). The server orders this list, but the thread must
81
+ * never depend on that alone. Resource ids are time-sortable ULIDs
82
+ * (`aex_01k…`), so an ascending id sort is creation order without parsing
83
+ * timestamps; entries missing an id sort last but keep a stable relative order.
84
+ *
85
+ * @internal Exported for testing — not part of the public API.
86
+ */
87
+ export function sortChronologically(
88
+ executions: readonly AgentExecution[],
89
+ ): AgentExecution[] {
90
+ return [...executions].sort((a, b) => {
91
+ const aId = a.metadata?.id ?? "";
92
+ const bId = b.metadata?.id ?? "";
93
+ if (aId === bId) return 0;
94
+ if (!aId) return 1;
95
+ if (!bId) return -1;
96
+ return aId < bId ? -1 : 1;
97
+ });
98
+ }
99
+
74
100
  export function useSessionExecutions(
75
101
  sessionId: string | null,
76
102
  options?: UseSessionExecutionsOptions,
@@ -87,7 +113,7 @@ export function useSessionExecutions(
87
113
  pageSize: 100,
88
114
  }),
89
115
  )
90
- .then((result) => result.entries)
116
+ .then((result) => sortChronologically(result.entries))
91
117
  : null,
92
118
  [sessionId, stigmer],
93
119
  [] as AgentExecution[],
@@ -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();