@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 +4 -4
- package/session/useSessionExecutions.d.ts +14 -0
- package/session/useSessionExecutions.d.ts.map +1 -1
- package/session/useSessionExecutions.js +27 -1
- package/session/useSessionExecutions.js.map +1 -1
- 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/session/__tests__/useSessionExecutions.test.ts +63 -0
- package/src/session/useSessionExecutions.ts +27 -1
- 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.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.
|
|
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.
|
|
64
|
-
"@stigmer/sdk": "3.5.
|
|
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;
|
|
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
|
|
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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"}
|
|
@@ -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
|
-
|
|
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();
|