@stigmer/react 3.1.3 → 3.1.5
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/composer/SessionComposer.d.ts +31 -0
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +12 -4
- package/composer/SessionComposer.js.map +1 -1
- package/execution/ArtifactApplyButton.d.ts +25 -0
- package/execution/ArtifactApplyButton.d.ts.map +1 -0
- package/execution/ArtifactApplyButton.js +21 -0
- package/execution/ArtifactApplyButton.js.map +1 -0
- package/execution/ArtifactContentBody.d.ts +36 -0
- package/execution/ArtifactContentBody.d.ts.map +1 -0
- package/execution/ArtifactContentBody.js +62 -0
- package/execution/ArtifactContentBody.js.map +1 -0
- package/execution/ArtifactDocument.d.ts +48 -0
- package/execution/ArtifactDocument.d.ts.map +1 -0
- package/execution/ArtifactDocument.js +89 -0
- package/execution/ArtifactDocument.js.map +1 -0
- package/execution/ArtifactPreviewModal.d.ts +4 -4
- package/execution/ArtifactPreviewModal.d.ts.map +1 -1
- package/execution/ArtifactPreviewModal.js +15 -142
- package/execution/ArtifactPreviewModal.js.map +1 -1
- package/execution/ArtifactRow.d.ts +49 -0
- package/execution/ArtifactRow.d.ts.map +1 -0
- package/execution/ArtifactRow.js +56 -0
- package/execution/ArtifactRow.js.map +1 -0
- package/execution/ArtifactsWidget.d.ts +6 -9
- package/execution/ArtifactsWidget.d.ts.map +1 -1
- package/execution/ArtifactsWidget.js +15 -19
- package/execution/ArtifactsWidget.js.map +1 -1
- package/execution/FileReviewContext.d.ts +15 -1
- package/execution/FileReviewContext.d.ts.map +1 -1
- package/execution/FileReviewContext.js +20 -1
- package/execution/FileReviewContext.js.map +1 -1
- package/execution/MessageThread.d.ts +65 -2
- package/execution/MessageThread.d.ts.map +1 -1
- package/execution/MessageThread.js +42 -23
- package/execution/MessageThread.js.map +1 -1
- package/execution/TodoCard.d.ts +8 -0
- package/execution/TodoCard.d.ts.map +1 -1
- package/execution/TodoCard.js +6 -4
- package/execution/TodoCard.js.map +1 -1
- package/execution/TodoList.d.ts +20 -1
- package/execution/TodoList.d.ts.map +1 -1
- package/execution/TodoList.js +9 -6
- package/execution/TodoList.js.map +1 -1
- package/execution/ToolCallDetail.d.ts +3 -2
- package/execution/ToolCallDetail.d.ts.map +1 -1
- package/execution/ToolCallDetail.js +49 -16
- package/execution/ToolCallDetail.js.map +1 -1
- package/execution/file-review-status.d.ts +15 -0
- package/execution/file-review-status.d.ts.map +1 -1
- package/execution/file-review-status.js +22 -0
- package/execution/file-review-status.js.map +1 -1
- package/execution/index.d.ts +13 -7
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +7 -4
- package/execution/index.js.map +1 -1
- package/execution/useArtifactContent.d.ts +6 -1
- package/execution/useArtifactContent.d.ts.map +1 -1
- package/execution/useArtifactContent.js +7 -2
- package/execution/useArtifactContent.js.map +1 -1
- package/execution/useArtifactInspection.d.ts +85 -0
- package/execution/useArtifactInspection.d.ts.map +1 -0
- package/execution/useArtifactInspection.js +143 -0
- package/execution/useArtifactInspection.js.map +1 -0
- package/execution/useCreateAgentExecution.d.ts +11 -0
- package/execution/useCreateAgentExecution.d.ts.map +1 -1
- package/execution/useCreateAgentExecution.js +1 -0
- package/execution/useCreateAgentExecution.js.map +1 -1
- package/github/useGitHubFileReader.d.ts +4 -2
- package/github/useGitHubFileReader.d.ts.map +1 -1
- package/github/useGitHubFileReader.js +16 -3
- package/github/useGitHubFileReader.js.map +1 -1
- package/github/useGitHubTreeLister.d.ts +2 -1
- package/github/useGitHubTreeLister.d.ts.map +1 -1
- package/github/useGitHubTreeLister.js +7 -3
- package/github/useGitHubTreeLister.js.map +1 -1
- package/index.d.ts +4 -4
- package/index.d.ts.map +1 -1
- package/index.js +3 -3
- package/index.js.map +1 -1
- package/internal/MermaidDiagram.d.ts +47 -0
- package/internal/MermaidDiagram.d.ts.map +1 -0
- package/internal/MermaidDiagram.js +118 -0
- package/internal/MermaidDiagram.js.map +1 -0
- package/internal/VirtualizedThread.d.ts +3 -2
- package/internal/VirtualizedThread.d.ts.map +1 -1
- package/internal/VirtualizedThread.js +3 -2
- package/internal/VirtualizedThread.js.map +1 -1
- package/internal/markdown-components.d.ts.map +1 -1
- package/internal/markdown-components.js +34 -0
- package/internal/markdown-components.js.map +1 -1
- package/internal/mermaid-loader.d.ts +26 -0
- package/internal/mermaid-loader.d.ts.map +1 -0
- package/internal/mermaid-loader.js +34 -0
- package/internal/mermaid-loader.js.map +1 -0
- package/internal/mermaid-theme-css.d.ts +41 -0
- package/internal/mermaid-theme-css.d.ts.map +1 -0
- package/internal/mermaid-theme-css.js +179 -0
- package/internal/mermaid-theme-css.js.map +1 -0
- package/internal/slug.d.ts +9 -5
- package/internal/slug.d.ts.map +1 -1
- package/internal/slug.js +10 -6
- package/internal/slug.js.map +1 -1
- package/package.json +5 -4
- package/session/SessionViewer.d.ts +11 -1
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +101 -21
- package/session/SessionViewer.js.map +1 -1
- package/session/artifact-document.d.ts +13 -0
- package/session/artifact-document.d.ts.map +1 -0
- package/session/artifact-document.js +22 -0
- package/session/artifact-document.js.map +1 -0
- package/session/facets/ArtifactsTab.d.ts +30 -4
- package/session/facets/ArtifactsTab.d.ts.map +1 -1
- package/session/facets/ArtifactsTab.js +32 -10
- package/session/facets/ArtifactsTab.js.map +1 -1
- package/session/index.d.ts +3 -1
- package/session/index.d.ts.map +1 -1
- package/session/index.js +5 -1
- package/session/index.js.map +1 -1
- package/session/useSessionArtifacts.d.ts +17 -0
- package/session/useSessionArtifacts.d.ts.map +1 -1
- package/session/useSessionArtifacts.js +16 -5
- package/session/useSessionArtifacts.js.map +1 -1
- package/session/useSessionConversation.d.ts +17 -1
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +32 -4
- package/session/useSessionConversation.js.map +1 -1
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +1 -0
- package/session/useSessionPageFlow.js.map +1 -1
- package/session/useSessionPanel.d.ts +20 -0
- package/session/useSessionPanel.d.ts.map +1 -1
- package/session/useSessionPanel.js +13 -0
- package/session/useSessionPanel.js.map +1 -1
- package/session/useSessionRailViews.d.ts +13 -1
- package/session/useSessionRailViews.d.ts.map +1 -1
- package/session/useSessionRailViews.js +5 -3
- package/session/useSessionRailViews.js.map +1 -1
- package/session/useWorkspaceReadRefs.d.ts +45 -0
- package/session/useWorkspaceReadRefs.d.ts.map +1 -0
- package/session/useWorkspaceReadRefs.js +104 -0
- package/session/useWorkspaceReadRefs.js.map +1 -0
- package/src/composer/SessionComposer.tsx +69 -1
- package/src/composer/__tests__/SessionComposer-editing.test.tsx +128 -0
- package/src/execution/ArtifactApplyButton.tsx +79 -0
- package/src/execution/ArtifactContentBody.tsx +255 -0
- package/src/execution/ArtifactDocument.tsx +344 -0
- package/src/execution/ArtifactPreviewModal.tsx +39 -405
- package/src/execution/ArtifactRow.tsx +165 -0
- package/src/execution/ArtifactsWidget.tsx +24 -33
- package/src/execution/FileReviewContext.ts +27 -1
- package/src/execution/MessageThread.tsx +110 -22
- package/src/execution/TodoCard.tsx +20 -4
- package/src/execution/TodoList.tsx +26 -4
- package/src/execution/ToolCallDetail.tsx +69 -16
- package/src/execution/__tests__/ArtifactContentBody.test.tsx +131 -0
- package/src/execution/__tests__/ArtifactDocument.test.tsx +100 -0
- package/src/execution/__tests__/ArtifactRow.test.tsx +139 -0
- package/src/execution/__tests__/ArtifactsWidget.test.tsx +78 -0
- package/src/execution/__tests__/ToolCallDetail.test.tsx +188 -2
- package/src/execution/__tests__/file-review-status.test.ts +47 -0
- package/src/execution/__tests__/message-entry.test.tsx +50 -2
- package/src/execution/__tests__/message-thread-slots.test.tsx +463 -0
- package/src/execution/__tests__/useArtifactInspection.test.tsx +151 -0
- package/src/execution/__tests__/useCreateAgentExecution.test.tsx +35 -0
- package/src/execution/file-review-status.ts +24 -0
- package/src/execution/index.ts +27 -5
- package/src/execution/useArtifactContent.ts +7 -0
- package/src/execution/useArtifactInspection.ts +252 -0
- package/src/execution/useCreateAgentExecution.ts +12 -0
- package/src/github/__tests__/useGitHubFileReader.test.ts +35 -2
- package/src/github/__tests__/useGitHubTreeLister.test.ts +14 -0
- package/src/github/useGitHubFileReader.ts +19 -6
- package/src/github/useGitHubTreeLister.ts +7 -3
- package/src/index.ts +15 -2
- package/src/internal/MermaidDiagram.tsx +164 -0
- package/src/internal/VirtualizedThread.tsx +5 -1
- package/src/internal/__tests__/a11y/mermaid-theme.a11y.test.tsx +122 -0
- package/src/internal/__tests__/markdown-components.test.tsx +93 -1
- package/src/internal/__tests__/mermaid-diagram.test.tsx +282 -0
- package/src/internal/markdown-components.tsx +36 -1
- package/src/internal/mermaid-loader.ts +44 -0
- package/src/internal/mermaid-theme-css.ts +178 -0
- package/src/internal/slug.ts +10 -6
- package/src/session/SessionViewer.tsx +204 -35
- package/src/session/__tests__/SessionViewer-artifactTabs.test.tsx +210 -0
- package/src/session/__tests__/SessionViewer-editResubmit.test.tsx +233 -0
- package/src/session/__tests__/useSessionArtifacts.test.ts +57 -0
- package/src/session/__tests__/useSessionConversation.test.tsx +172 -1
- package/src/session/__tests__/useSessionPanel.test.ts +70 -1
- package/src/session/__tests__/useWorkspaceReadRefs.test.ts +245 -0
- package/src/session/artifact-document.ts +23 -0
- package/src/session/facets/ArtifactsTab.tsx +66 -18
- package/src/session/facets/__tests__/ArtifactsTab.test.tsx +111 -47
- package/src/session/index.ts +7 -1
- package/src/session/useSessionArtifacts.ts +16 -5
- package/src/session/useSessionConversation.ts +48 -6
- package/src/session/useSessionPageFlow.ts +1 -0
- package/src/session/useSessionPanel.ts +41 -0
- package/src/session/useSessionRailViews.tsx +21 -1
- package/src/session/useWorkspaceReadRefs.ts +118 -0
- package/src/workspace/FileViewer.tsx +144 -38
- package/src/workspace/WorkspaceFileReader.ts +21 -0
- package/src/workspace/__tests__/FileViewer.test.tsx +138 -3
- package/src/workspace/__tests__/useWorkspaceFileContent.test.tsx +20 -0
- package/src/workspace/__tests__/workspaceListingCache.test.ts +114 -0
- package/src/workspace/index.ts +4 -1
- package/src/workspace/useWorkspaceEntries.ts +11 -0
- package/src/workspace/useWorkspaceFileContent.ts +4 -1
- package/src/workspace/useWorkspaceFileSearch.ts +8 -3
- package/src/workspace/useWorkspaceFiles.ts +3 -2
- package/src/workspace/workspaceListingCache.ts +21 -7
- package/styles.css +1 -1
- package/workspace/FileViewer.d.ts +1 -1
- package/workspace/FileViewer.d.ts.map +1 -1
- package/workspace/FileViewer.js +55 -5
- package/workspace/FileViewer.js.map +1 -1
- package/workspace/WorkspaceFileReader.d.ts +17 -0
- package/workspace/WorkspaceFileReader.d.ts.map +1 -1
- package/workspace/WorkspaceFileReader.js +18 -0
- package/workspace/WorkspaceFileReader.js.map +1 -1
- package/workspace/index.d.ts +1 -1
- package/workspace/index.d.ts.map +1 -1
- package/workspace/index.js +1 -1
- package/workspace/index.js.map +1 -1
- package/workspace/useWorkspaceEntries.d.ts +11 -0
- package/workspace/useWorkspaceEntries.d.ts.map +1 -1
- package/workspace/useWorkspaceEntries.js.map +1 -1
- package/workspace/useWorkspaceFileContent.d.ts.map +1 -1
- package/workspace/useWorkspaceFileContent.js +4 -1
- package/workspace/useWorkspaceFileContent.js.map +1 -1
- package/workspace/useWorkspaceFileSearch.d.ts.map +1 -1
- package/workspace/useWorkspaceFileSearch.js +8 -3
- package/workspace/useWorkspaceFileSearch.js.map +1 -1
- package/workspace/useWorkspaceFiles.d.ts +2 -1
- package/workspace/useWorkspaceFiles.d.ts.map +1 -1
- package/workspace/useWorkspaceFiles.js +3 -2
- package/workspace/useWorkspaceFiles.js.map +1 -1
- package/workspace/workspaceListingCache.d.ts +1 -1
- package/workspace/workspaceListingCache.d.ts.map +1 -1
- package/workspace/workspaceListingCache.js +20 -7
- package/workspace/workspaceListingCache.js.map +1 -1
- package/execution/ArtifactCard.d.ts +0 -81
- package/execution/ArtifactCard.d.ts.map +0 -1
- package/execution/ArtifactCard.js +0 -130
- package/execution/ArtifactCard.js.map +0 -1
- package/src/execution/ArtifactCard.tsx +0 -320
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { renderHook } from "@testing-library/react";
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import {
|
|
5
|
+
AgentExecutionSchema,
|
|
6
|
+
AgentExecutionStatusSchema,
|
|
7
|
+
type AgentExecution,
|
|
8
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
9
|
+
import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
|
|
10
|
+
import {
|
|
11
|
+
WorkspaceWriteBackSchema,
|
|
12
|
+
WorkspaceWriteBackPhase,
|
|
13
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/writeback_pb";
|
|
14
|
+
import type { WorkspaceEntry } from "../../workspace/useWorkspaceEntries";
|
|
15
|
+
import { useWorkspaceReadRefs } from "../useWorkspaceReadRefs";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Fixtures
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
function gitEntry(overrides?: Partial<WorkspaceEntry>): WorkspaceEntry {
|
|
22
|
+
return {
|
|
23
|
+
id: "ws-1",
|
|
24
|
+
name: "acme/api",
|
|
25
|
+
type: "git",
|
|
26
|
+
gitUrl: "https://github.com/acme/api",
|
|
27
|
+
gitBranch: "main",
|
|
28
|
+
...overrides,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function localEntry(): WorkspaceEntry {
|
|
33
|
+
return {
|
|
34
|
+
id: "ws-local",
|
|
35
|
+
name: "dev/project",
|
|
36
|
+
type: "local",
|
|
37
|
+
localPath: "/Users/dev/project",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function execWithWriteBacks(
|
|
42
|
+
id: string,
|
|
43
|
+
writeBacks: ReadonlyArray<{
|
|
44
|
+
entryName: string;
|
|
45
|
+
commitSha: string;
|
|
46
|
+
phase?: WorkspaceWriteBackPhase;
|
|
47
|
+
}>,
|
|
48
|
+
): AgentExecution {
|
|
49
|
+
const exec = create(AgentExecutionSchema);
|
|
50
|
+
exec.metadata = create(ApiResourceMetadataSchema, { id });
|
|
51
|
+
exec.status = create(AgentExecutionStatusSchema);
|
|
52
|
+
exec.status.workspaceWriteBacks = writeBacks.map((wb) =>
|
|
53
|
+
create(WorkspaceWriteBackSchema, {
|
|
54
|
+
workspaceEntryName: wb.entryName,
|
|
55
|
+
commitSha: wb.commitSha,
|
|
56
|
+
phase: wb.phase ?? WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_PUSHED,
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
return exec;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// useWorkspaceReadRefs
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
describe("useWorkspaceReadRefs", () => {
|
|
67
|
+
it("returns the same entries reference when there are no write-backs", () => {
|
|
68
|
+
const entries = [gitEntry()];
|
|
69
|
+
const { result } = renderHook(() => useWorkspaceReadRefs([], entries));
|
|
70
|
+
expect(result.current).toBe(entries);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("decorates a single git entry from a write-back with a matching name", () => {
|
|
74
|
+
const entries = [gitEntry({ name: "acme/api" })];
|
|
75
|
+
const executions = [
|
|
76
|
+
execWithWriteBacks("e1", [{ entryName: "acme/api", commitSha: "sha-1" }]),
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
const { result } = renderHook(() =>
|
|
80
|
+
useWorkspaceReadRefs(executions, entries),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(result.current[0].readRef).toBe("sha-1");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("applies an empty-name write-back to the lone git entry (single-entry convention)", () => {
|
|
87
|
+
// Single-entry sessions provision with entryName "" — the runner's
|
|
88
|
+
// WriteBackCoordinator writes back under that empty name.
|
|
89
|
+
const entries = [gitEntry()];
|
|
90
|
+
const executions = [
|
|
91
|
+
execWithWriteBacks("e1", [{ entryName: "", commitSha: "sha-solo" }]),
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
const { result } = renderHook(() =>
|
|
95
|
+
useWorkspaceReadRefs(executions, entries),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(result.current[0].readRef).toBe("sha-solo");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("claims a lone mismatched name for a lone git entry, but never for multiple entries", () => {
|
|
102
|
+
const solo = renderHook(() =>
|
|
103
|
+
useWorkspaceReadRefs(
|
|
104
|
+
[execWithWriteBacks("e1", [{ entryName: "other-name", commitSha: "sha-x" }])],
|
|
105
|
+
[gitEntry()],
|
|
106
|
+
),
|
|
107
|
+
);
|
|
108
|
+
expect(solo.result.current[0].readRef).toBe("sha-x");
|
|
109
|
+
|
|
110
|
+
// With two git entries an unmatched name is ambiguous — decorate nothing.
|
|
111
|
+
const multi = renderHook(() =>
|
|
112
|
+
useWorkspaceReadRefs(
|
|
113
|
+
[execWithWriteBacks("e1", [{ entryName: "other-name", commitSha: "sha-x" }])],
|
|
114
|
+
[
|
|
115
|
+
gitEntry({ id: "ws-1", name: "acme/api" }),
|
|
116
|
+
gitEntry({ id: "ws-2", name: "acme/web" }),
|
|
117
|
+
],
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
expect(multi.result.current[0].readRef).toBeUndefined();
|
|
121
|
+
expect(multi.result.current[1].readRef).toBeUndefined();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("the latest non-empty SHA wins across executions in order", () => {
|
|
125
|
+
const entries = [gitEntry({ name: "acme/api" })];
|
|
126
|
+
const executions = [
|
|
127
|
+
execWithWriteBacks("e1", [{ entryName: "acme/api", commitSha: "sha-old" }]),
|
|
128
|
+
execWithWriteBacks("e2", [{ entryName: "acme/api", commitSha: "sha-new" }]),
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const { result } = renderHook(() =>
|
|
132
|
+
useWorkspaceReadRefs(executions, entries),
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
expect(result.current[0].readRef).toBe("sha-new");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("a FAILED write-back with no SHA never regresses an earlier successful ref", () => {
|
|
139
|
+
const entries = [gitEntry({ name: "acme/api" })];
|
|
140
|
+
const executions = [
|
|
141
|
+
execWithWriteBacks("e1", [{ entryName: "acme/api", commitSha: "sha-good" }]),
|
|
142
|
+
execWithWriteBacks("e2", [
|
|
143
|
+
{
|
|
144
|
+
entryName: "acme/api",
|
|
145
|
+
commitSha: "",
|
|
146
|
+
phase: WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_FAILED,
|
|
147
|
+
},
|
|
148
|
+
]),
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
const { result } = renderHook(() =>
|
|
152
|
+
useWorkspaceReadRefs(executions, entries),
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
expect(result.current[0].readRef).toBe("sha-good");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("returns entries unchanged when only FAILED no-SHA write-backs exist", () => {
|
|
159
|
+
const entries = [gitEntry()];
|
|
160
|
+
const executions = [
|
|
161
|
+
execWithWriteBacks("e1", [
|
|
162
|
+
{
|
|
163
|
+
entryName: "",
|
|
164
|
+
commitSha: "",
|
|
165
|
+
phase: WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_FAILED,
|
|
166
|
+
},
|
|
167
|
+
]),
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
const { result } = renderHook(() =>
|
|
171
|
+
useWorkspaceReadRefs(executions, entries),
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
expect(result.current).toBe(entries);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("matches strictly by name in multi-entry sessions", () => {
|
|
178
|
+
const entries = [
|
|
179
|
+
gitEntry({ id: "ws-1", name: "frontend" }),
|
|
180
|
+
gitEntry({ id: "ws-2", name: "backend" }),
|
|
181
|
+
];
|
|
182
|
+
const executions = [
|
|
183
|
+
execWithWriteBacks("e1", [
|
|
184
|
+
{ entryName: "frontend", commitSha: "sha-fe" },
|
|
185
|
+
{ entryName: "backend", commitSha: "sha-be" },
|
|
186
|
+
]),
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
const { result } = renderHook(() =>
|
|
190
|
+
useWorkspaceReadRefs(executions, entries),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
expect(result.current[0].readRef).toBe("sha-fe");
|
|
194
|
+
expect(result.current[1].readRef).toBe("sha-be");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("never decorates local entries", () => {
|
|
198
|
+
const entries = [localEntry()];
|
|
199
|
+
const executions = [
|
|
200
|
+
execWithWriteBacks("e1", [{ entryName: "", commitSha: "sha-1" }]),
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
const { result } = renderHook(() =>
|
|
204
|
+
useWorkspaceReadRefs(executions, entries),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
expect(result.current[0].readRef).toBeUndefined();
|
|
208
|
+
expect(result.current).toBe(entries);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("returns a stable array across streaming frames that change no SHA (DD-010)", () => {
|
|
212
|
+
const entries = [gitEntry({ name: "acme/api" })];
|
|
213
|
+
const { result, rerender } = renderHook(
|
|
214
|
+
(props: { executions: readonly AgentExecution[] }) =>
|
|
215
|
+
useWorkspaceReadRefs(props.executions, entries),
|
|
216
|
+
{
|
|
217
|
+
initialProps: {
|
|
218
|
+
executions: [
|
|
219
|
+
execWithWriteBacks("e1", [{ entryName: "acme/api", commitSha: "sha-1" }]),
|
|
220
|
+
] as readonly AgentExecution[],
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
const first = result.current;
|
|
226
|
+
expect(first[0].readRef).toBe("sha-1");
|
|
227
|
+
|
|
228
|
+
// A new executions array (fresh identity, e.g. a streaming frame) with the
|
|
229
|
+
// same SHA must return the identical decorated array.
|
|
230
|
+
rerender({
|
|
231
|
+
executions: [
|
|
232
|
+
execWithWriteBacks("e1", [{ entryName: "acme/api", commitSha: "sha-1" }]),
|
|
233
|
+
],
|
|
234
|
+
});
|
|
235
|
+
expect(result.current).toBe(first);
|
|
236
|
+
|
|
237
|
+
// Advancing the SHA produces a new decoration.
|
|
238
|
+
rerender({
|
|
239
|
+
executions: [
|
|
240
|
+
execWithWriteBacks("e1", [{ entryName: "acme/api", commitSha: "sha-2" }]),
|
|
241
|
+
],
|
|
242
|
+
});
|
|
243
|
+
expect(result.current[0].readRef).toBe("sha-2");
|
|
244
|
+
});
|
|
245
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Identity of the session's artifact document tabs in the workspace surface's
|
|
2
|
+
// editor group.
|
|
3
|
+
//
|
|
4
|
+
// Kept in its own module (not in ArtifactDocument.tsx) so behavior-only
|
|
5
|
+
// consumers — useSessionPanel opens an artifact tab, SessionViewer routes to it
|
|
6
|
+
// — depend on the identity without pulling the component (and its content
|
|
7
|
+
// renderer) into their module graph (DD-003 headless-first). Mirrors
|
|
8
|
+
// plan-document.ts.
|
|
9
|
+
|
|
10
|
+
import { virtualEntryId } from "../internal/store/index.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Virtual entry id of the artifact document FAMILY. Unlike the plan (one tab
|
|
14
|
+
* per session), artifacts are many: every opened artifact shares this one
|
|
15
|
+
* entry id and is distinguished by its {@link artifactKey}-derived `path`, so
|
|
16
|
+
* `editorKey(ARTIFACT_DOCUMENT_ENTRY_ID, path)` yields one tab per artifact —
|
|
17
|
+
* the VS Code "each file is a tab" model. The NUL-namespaced virtual id can
|
|
18
|
+
* never alias a real workspace entry, and its distinct `"artifact"` kind can
|
|
19
|
+
* never collide with the plan tab's `"plan"` kind.
|
|
20
|
+
*
|
|
21
|
+
* @see artifactKey — the `path` component (an artifact's stable identity)
|
|
22
|
+
*/
|
|
23
|
+
export const ARTIFACT_DOCUMENT_ENTRY_ID = virtualEntryId("artifact");
|
|
@@ -4,10 +4,11 @@ import { useCallback, useState } from "react";
|
|
|
4
4
|
import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
|
|
5
5
|
import {
|
|
6
6
|
useSessionArtifacts,
|
|
7
|
+
artifactKey,
|
|
7
8
|
type SessionArtifactEntry,
|
|
8
9
|
} from "../useSessionArtifacts.js";
|
|
9
|
-
import { ArtifactCard } from "../../execution/ArtifactCard.js";
|
|
10
10
|
import { ArtifactPreviewModal } from "../../execution/ArtifactPreviewModal.js";
|
|
11
|
+
import { ArtifactRow } from "../../execution/ArtifactRow.js";
|
|
11
12
|
import { isPlanArtifact } from "../../library/detect-plan-artifact.js";
|
|
12
13
|
import type { ApplyResourceResult } from "../../library/useApplyResource.js";
|
|
13
14
|
|
|
@@ -29,14 +30,39 @@ export interface ArtifactsTabProps {
|
|
|
29
30
|
* every artifact.
|
|
30
31
|
*/
|
|
31
32
|
readonly onOpenPlan?: (executionId: string) => void;
|
|
33
|
+
/**
|
|
34
|
+
* Open an artifact as an editor-pane document (the session panel's
|
|
35
|
+
* artifact-family virtual-document tab — the VS Code "each file is a tab"
|
|
36
|
+
* model). When provided, clicking a non-plan artifact routes here instead of
|
|
37
|
+
* the preview modal. When omitted (panel-less hosts) the modal remains the
|
|
38
|
+
* open mechanism. Plan artifacts always defer to {@link onOpenPlan} first.
|
|
39
|
+
*/
|
|
40
|
+
readonly onOpenArtifact?: (entry: SessionArtifactEntry) => void;
|
|
41
|
+
/**
|
|
42
|
+
* Pin a non-plan artifact's document tab — the double-click half of the
|
|
43
|
+
* open/activate split (mirrors the file tree's `onOpenFile`/`onActivateFile`).
|
|
44
|
+
* When provided, each row binds a double-click that promotes the preview the
|
|
45
|
+
* single click opened. Omit in panel-less/modal hosts: with no handler the
|
|
46
|
+
* row binds no double-click, so a double-click is just two harmless single
|
|
47
|
+
* clicks. Plans always route to {@link onOpenPlan} (their tab is already
|
|
48
|
+
* pinned), so double-click == single-click for a plan.
|
|
49
|
+
*/
|
|
50
|
+
readonly onActivateArtifact?: (entry: SessionArtifactEntry) => void;
|
|
32
51
|
}
|
|
33
52
|
|
|
34
53
|
/**
|
|
35
54
|
* Artifacts facet for the session panel (a `useSessionRailViews` rail view).
|
|
36
55
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
56
|
+
* A VS Code-style dense file list of the session's outputs: one compact
|
|
57
|
+
* {@link ArtifactRow} per artifact (shared file-type icon + name + size, with a
|
|
58
|
+
* hover/focus Download). Clicking a row opens the artifact — a `plan.md` in the
|
|
59
|
+
* plan document tab ({@link onOpenPlan}), any other artifact in an editor-pane
|
|
60
|
+
* document ({@link onOpenArtifact}), or, for panel-less hosts that inject
|
|
61
|
+
* neither, the {@link ArtifactPreviewModal} popup fallback.
|
|
62
|
+
*
|
|
63
|
+
* Deliberately metadata-only: rows do NOT fetch content for resource detection
|
|
64
|
+
* (that would be one request per artifact on open) — detection and the
|
|
65
|
+
* Apply/Push action live in the opened document/modal instead.
|
|
40
66
|
*/
|
|
41
67
|
export function ArtifactsTab({
|
|
42
68
|
executions,
|
|
@@ -44,20 +70,41 @@ export function ArtifactsTab({
|
|
|
44
70
|
onApplied,
|
|
45
71
|
onImplementPlan,
|
|
46
72
|
onOpenPlan,
|
|
73
|
+
onOpenArtifact,
|
|
74
|
+
onActivateArtifact,
|
|
47
75
|
}: ArtifactsTabProps) {
|
|
48
76
|
const { artifacts, hasArtifacts } = useSessionArtifacts(executions);
|
|
49
77
|
const [previewEntry, setPreviewEntry] = useState<SessionArtifactEntry | null>(null);
|
|
50
78
|
|
|
51
|
-
const
|
|
79
|
+
const handleOpen = useCallback(
|
|
52
80
|
(entry: SessionArtifactEntry) => {
|
|
53
|
-
//
|
|
81
|
+
// A plan is a first-class document — it opens in the dedicated plan tab,
|
|
82
|
+
// never as a generic artifact or a popup.
|
|
54
83
|
if (onOpenPlan && isPlanArtifact(entry.artifact)) {
|
|
55
84
|
onOpenPlan(entry.executionId);
|
|
56
85
|
return;
|
|
57
86
|
}
|
|
87
|
+
// Editor-pane document when the host injected it; otherwise the modal.
|
|
88
|
+
if (onOpenArtifact) {
|
|
89
|
+
onOpenArtifact(entry);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
58
92
|
setPreviewEntry(entry);
|
|
59
93
|
},
|
|
60
|
-
[onOpenPlan],
|
|
94
|
+
[onOpenPlan, onOpenArtifact],
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const handleActivate = useCallback(
|
|
98
|
+
(entry: SessionArtifactEntry) => {
|
|
99
|
+
// A plan's tab is always pinned, so double-click matches single-click:
|
|
100
|
+
// route to the plan tab, never pin a generic artifact.
|
|
101
|
+
if (onOpenPlan && isPlanArtifact(entry.artifact)) {
|
|
102
|
+
onOpenPlan(entry.executionId);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
onActivateArtifact?.(entry);
|
|
106
|
+
},
|
|
107
|
+
[onOpenPlan, onActivateArtifact],
|
|
61
108
|
);
|
|
62
109
|
|
|
63
110
|
const handleClosePreview = useCallback(() => {
|
|
@@ -76,19 +123,20 @@ export function ArtifactsTab({
|
|
|
76
123
|
|
|
77
124
|
return (
|
|
78
125
|
<>
|
|
79
|
-
<
|
|
126
|
+
<ul role="list" className="flex flex-col">
|
|
80
127
|
{artifacts.map((entry) => (
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
128
|
+
<ArtifactRow
|
|
129
|
+
key={artifactKey(entry.artifact)}
|
|
130
|
+
artifact={entry.artifact}
|
|
131
|
+
executionId={entry.executionId}
|
|
132
|
+
hasNameCollision={entry.hasNameCollision}
|
|
133
|
+
onOpen={() => handleOpen(entry)}
|
|
134
|
+
onActivate={
|
|
135
|
+
onActivateArtifact ? () => handleActivate(entry) : undefined
|
|
136
|
+
}
|
|
137
|
+
/>
|
|
90
138
|
))}
|
|
91
|
-
</
|
|
139
|
+
</ul>
|
|
92
140
|
|
|
93
141
|
{previewEntry && (
|
|
94
142
|
<ArtifactPreviewModal
|
|
@@ -9,7 +9,8 @@ import {
|
|
|
9
9
|
ExecutionPhase,
|
|
10
10
|
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
11
11
|
import { StigmerContext } from "../../../context";
|
|
12
|
-
import { ArtifactsTab } from "../ArtifactsTab";
|
|
12
|
+
import { ArtifactsTab, type ArtifactsTabProps } from "../ArtifactsTab";
|
|
13
|
+
import type { SessionArtifactEntry } from "../../useSessionArtifacts";
|
|
13
14
|
|
|
14
15
|
function artifact(name: string) {
|
|
15
16
|
return create(ExecutionArtifactSchema, {
|
|
@@ -38,86 +39,149 @@ function createStigmerMock(): Stigmer {
|
|
|
38
39
|
} as unknown as Stigmer;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
function renderTab(
|
|
42
|
-
onImplementPlan?: () => void,
|
|
43
|
-
onOpenPlan?: (executionId: string) => void,
|
|
44
|
-
) {
|
|
42
|
+
function renderTab(props: Partial<ArtifactsTabProps> = {}) {
|
|
45
43
|
return render(
|
|
46
44
|
<StigmerContext.Provider value={createStigmerMock()}>
|
|
47
|
-
<ArtifactsTab
|
|
48
|
-
executions={[execution]}
|
|
49
|
-
org="acme"
|
|
50
|
-
onImplementPlan={onImplementPlan}
|
|
51
|
-
onOpenPlan={onOpenPlan}
|
|
52
|
-
/>
|
|
45
|
+
<ArtifactsTab executions={[execution]} org="acme" {...props} />
|
|
53
46
|
</StigmerContext.Provider>,
|
|
54
47
|
);
|
|
55
48
|
}
|
|
56
49
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
fireEvent.click(
|
|
50
|
+
/** Click the open target of a row by its artifact name. */
|
|
51
|
+
function openRow(name: string) {
|
|
52
|
+
fireEvent.click(screen.getByText(name));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Double-click the open target of a row by its artifact name (the pin gesture). */
|
|
56
|
+
function activateRow(name: string) {
|
|
57
|
+
fireEvent.doubleClick(screen.getByText(name));
|
|
60
58
|
}
|
|
61
59
|
|
|
62
60
|
afterEach(cleanup);
|
|
63
61
|
|
|
64
|
-
describe("ArtifactsTab —
|
|
65
|
-
it("
|
|
66
|
-
renderTab(vi.fn());
|
|
62
|
+
describe("ArtifactsTab — dense row list", () => {
|
|
63
|
+
it("renders one row per artifact with a Download control (no old 'Preview' button)", () => {
|
|
64
|
+
renderTab({ onOpenArtifact: vi.fn() });
|
|
67
65
|
|
|
68
|
-
|
|
66
|
+
expect(screen.getAllByRole("listitem")).toHaveLength(2);
|
|
67
|
+
expect(screen.getByText("plan.md")).toBeTruthy();
|
|
68
|
+
expect(screen.getByText("notes.md")).toBeTruthy();
|
|
69
|
+
expect(screen.queryByText("Preview")).toBeNull();
|
|
70
|
+
expect(screen.getByLabelText("Download plan.md")).toBeTruthy();
|
|
71
|
+
expect(screen.getByLabelText("Download notes.md")).toBeTruthy();
|
|
72
|
+
});
|
|
69
73
|
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
it("renders the empty state with no artifacts", () => {
|
|
75
|
+
render(
|
|
76
|
+
<StigmerContext.Provider value={createStigmerMock()}>
|
|
77
|
+
<ArtifactsTab executions={[]} org="acme" />
|
|
78
|
+
</StigmerContext.Provider>,
|
|
79
|
+
);
|
|
80
|
+
expect(screen.getByText(/No artifacts yet/)).toBeTruthy();
|
|
72
81
|
});
|
|
82
|
+
});
|
|
73
83
|
|
|
74
|
-
|
|
75
|
-
|
|
84
|
+
describe("ArtifactsTab — open routing", () => {
|
|
85
|
+
it("routes a non-plan artifact to onOpenArtifact (no modal) when provided", () => {
|
|
86
|
+
const onOpenArtifact = vi.fn<(entry: SessionArtifactEntry) => void>();
|
|
87
|
+
renderTab({ onOpenArtifact, onOpenPlan: vi.fn() });
|
|
76
88
|
|
|
77
|
-
|
|
89
|
+
openRow("notes.md");
|
|
78
90
|
|
|
79
|
-
|
|
80
|
-
expect(
|
|
91
|
+
expect(onOpenArtifact).toHaveBeenCalledTimes(1);
|
|
92
|
+
expect(onOpenArtifact.mock.calls[0][0].artifact.name).toBe("notes.md");
|
|
93
|
+
expect(document.querySelector("dialog")).toBeNull();
|
|
81
94
|
});
|
|
82
95
|
|
|
83
|
-
it("
|
|
84
|
-
const
|
|
85
|
-
|
|
96
|
+
it("routes plan.md to onOpenPlan (never onOpenArtifact) when both are provided", () => {
|
|
97
|
+
const onOpenArtifact = vi.fn();
|
|
98
|
+
const onOpenPlan = vi.fn();
|
|
99
|
+
renderTab({ onOpenArtifact, onOpenPlan });
|
|
86
100
|
|
|
87
|
-
|
|
88
|
-
const dialog = document.querySelector("dialog")!;
|
|
89
|
-
fireEvent.click(within(dialog).getByText("Build"));
|
|
101
|
+
openRow("plan.md");
|
|
90
102
|
|
|
91
|
-
expect(
|
|
103
|
+
expect(onOpenPlan).toHaveBeenCalledWith("aex_1");
|
|
104
|
+
expect(onOpenArtifact).not.toHaveBeenCalled();
|
|
105
|
+
expect(document.querySelector("dialog")).toBeNull();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("falls back to the modal for a non-plan artifact when onOpenArtifact is omitted", () => {
|
|
109
|
+
renderTab({ onOpenPlan: vi.fn() });
|
|
110
|
+
|
|
111
|
+
openRow("notes.md");
|
|
112
|
+
|
|
113
|
+
expect(document.querySelector("dialog")).toBeTruthy();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("falls back to the modal for plan.md when neither onOpenPlan nor onOpenArtifact is provided", () => {
|
|
117
|
+
renderTab();
|
|
118
|
+
|
|
119
|
+
openRow("plan.md");
|
|
120
|
+
|
|
121
|
+
expect(document.querySelector("dialog")).toBeTruthy();
|
|
92
122
|
});
|
|
93
123
|
});
|
|
94
124
|
|
|
95
|
-
describe("ArtifactsTab —
|
|
96
|
-
it("routes a plan
|
|
125
|
+
describe("ArtifactsTab — pin (double-click) routing", () => {
|
|
126
|
+
it("routes a non-plan double-click to onActivateArtifact (pin)", () => {
|
|
127
|
+
const onActivateArtifact = vi.fn<(entry: SessionArtifactEntry) => void>();
|
|
128
|
+
renderTab({ onOpenArtifact: vi.fn(), onActivateArtifact, onOpenPlan: vi.fn() });
|
|
129
|
+
|
|
130
|
+
activateRow("notes.md");
|
|
131
|
+
|
|
132
|
+
expect(onActivateArtifact).toHaveBeenCalledTimes(1);
|
|
133
|
+
expect(onActivateArtifact.mock.calls[0][0].artifact.name).toBe("notes.md");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("routes a plan.md double-click to onOpenPlan (its tab is already pinned), never onActivateArtifact", () => {
|
|
137
|
+
const onActivateArtifact = vi.fn();
|
|
97
138
|
const onOpenPlan = vi.fn();
|
|
98
|
-
renderTab(vi.fn(), onOpenPlan);
|
|
139
|
+
renderTab({ onOpenArtifact: vi.fn(), onActivateArtifact, onOpenPlan });
|
|
99
140
|
|
|
100
|
-
|
|
141
|
+
activateRow("plan.md");
|
|
101
142
|
|
|
102
143
|
expect(onOpenPlan).toHaveBeenCalledWith("aex_1");
|
|
103
|
-
expect(
|
|
144
|
+
expect(onActivateArtifact).not.toHaveBeenCalled();
|
|
104
145
|
});
|
|
105
146
|
|
|
106
|
-
it("
|
|
107
|
-
const
|
|
108
|
-
renderTab(
|
|
147
|
+
it("binds no double-click when onActivateArtifact is omitted (panel-less host) — a double-click is harmless", () => {
|
|
148
|
+
const onOpenArtifact = vi.fn();
|
|
149
|
+
renderTab({ onOpenArtifact });
|
|
109
150
|
|
|
110
|
-
|
|
151
|
+
// With no pin handler the row binds no onDoubleClick, so a dblclick fires
|
|
152
|
+
// nothing (in a real browser the leading single-clicks still preview).
|
|
153
|
+
expect(() => activateRow("notes.md")).not.toThrow();
|
|
154
|
+
expect(onOpenArtifact).not.toHaveBeenCalled();
|
|
155
|
+
});
|
|
156
|
+
});
|
|
111
157
|
|
|
112
|
-
|
|
113
|
-
|
|
158
|
+
describe("ArtifactsTab — modal fallback plan 'Build' wiring", () => {
|
|
159
|
+
it("shows 'Build' in the modal preview of a plan.md artifact (panel-less host)", () => {
|
|
160
|
+
renderTab({ onImplementPlan: vi.fn() });
|
|
161
|
+
|
|
162
|
+
openRow("plan.md");
|
|
163
|
+
|
|
164
|
+
const dialog = document.querySelector("dialog")!;
|
|
165
|
+
expect(within(dialog).getByText("Build")).toBeTruthy();
|
|
114
166
|
});
|
|
115
167
|
|
|
116
|
-
it("
|
|
117
|
-
renderTab(vi.fn());
|
|
168
|
+
it("does not show 'Build' in the modal preview of a non-plan artifact", () => {
|
|
169
|
+
renderTab({ onImplementPlan: vi.fn() });
|
|
118
170
|
|
|
119
|
-
|
|
171
|
+
openRow("notes.md");
|
|
120
172
|
|
|
121
|
-
|
|
173
|
+
const dialog = document.querySelector("dialog")!;
|
|
174
|
+
expect(within(dialog).queryByText("Build")).toBeNull();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("invokes onImplementPlan when 'Build' is clicked for a plan", () => {
|
|
178
|
+
const onImplementPlan = vi.fn();
|
|
179
|
+
renderTab({ onImplementPlan });
|
|
180
|
+
|
|
181
|
+
openRow("plan.md");
|
|
182
|
+
const dialog = document.querySelector("dialog")!;
|
|
183
|
+
fireEvent.click(within(dialog).getByText("Build"));
|
|
184
|
+
|
|
185
|
+
expect(onImplementPlan).toHaveBeenCalledTimes(1);
|
|
122
186
|
});
|
|
123
187
|
});
|
package/src/session/index.ts
CHANGED
|
@@ -33,7 +33,7 @@ export type {
|
|
|
33
33
|
UseSessionConversationReturn,
|
|
34
34
|
} from "./useSessionConversation.js";
|
|
35
35
|
|
|
36
|
-
export { useSessionArtifacts } from "./useSessionArtifacts.js";
|
|
36
|
+
export { useSessionArtifacts, artifactKey } from "./useSessionArtifacts.js";
|
|
37
37
|
export type {
|
|
38
38
|
SessionArtifactEntry,
|
|
39
39
|
UseSessionArtifactsReturn,
|
|
@@ -45,6 +45,8 @@ export type {
|
|
|
45
45
|
UseSessionWriteBacksReturn,
|
|
46
46
|
} from "./useSessionWriteBacks.js";
|
|
47
47
|
|
|
48
|
+
export { useWorkspaceReadRefs } from "./useWorkspaceReadRefs.js";
|
|
49
|
+
|
|
48
50
|
export { useSessionFileChanges } from "./useSessionFileChanges.js";
|
|
49
51
|
export type { UseSessionFileChangesReturn } from "./useSessionFileChanges.js";
|
|
50
52
|
|
|
@@ -127,6 +129,10 @@ export {
|
|
|
127
129
|
PLAN_DOCUMENT_PATH,
|
|
128
130
|
} from "./plan-document.js";
|
|
129
131
|
|
|
132
|
+
// Artifact document — the editor-area rendering of an execution artifact
|
|
133
|
+
// (mounted as an artifact-family virtual document) and the identity of its tabs.
|
|
134
|
+
export { ARTIFACT_DOCUMENT_ENTRY_ID } from "./artifact-document.js";
|
|
135
|
+
|
|
130
136
|
// Selection types (for platform builders composing their own Inspect facet)
|
|
131
137
|
export type { SelectedThreadItem } from "../internal/store/selection-store.js";
|
|
132
138
|
|
|
@@ -41,11 +41,22 @@ export interface UseSessionArtifactsReturn {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
45
|
-
* available (
|
|
46
|
-
* falling back to `name` for older artifacts that predate the field.
|
|
44
|
+
* The stable identity of an artifact within a session. Uses `sandbox_path`
|
|
45
|
+
* when available (the artifact's filesystem identity within the session
|
|
46
|
+
* sandbox), falling back to `name` for older artifacts that predate the field.
|
|
47
|
+
*
|
|
48
|
+
* This is the SINGLE source of truth for artifact identity across three sites
|
|
49
|
+
* that must agree or a tab could fail to resolve its artifact:
|
|
50
|
+
* 1. deduplication here (latest execution wins per key),
|
|
51
|
+
* 2. the virtual-document tab `path` when an artifact opens in the editor area
|
|
52
|
+
* (see `SessionViewer` / `useSessionPanel.openArtifact`), and
|
|
53
|
+
* 3. the `artifactKey → entry` lookup that renders the open tab's document.
|
|
54
|
+
*
|
|
55
|
+
* Its basename doubles as the editor tab's label (paths render as their last
|
|
56
|
+
* `/` segment in `EditorTabs`), so `sandbox_path` gives a natural filename
|
|
57
|
+
* label with cross-directory disambiguation for free.
|
|
47
58
|
*/
|
|
48
|
-
function
|
|
59
|
+
export function artifactKey(artifact: ExecutionArtifact): string {
|
|
49
60
|
return artifact.sandboxPath || artifact.name;
|
|
50
61
|
}
|
|
51
62
|
|
|
@@ -102,7 +113,7 @@ export function useSessionArtifacts(
|
|
|
102
113
|
const terminal = isTerminalPhase(phase);
|
|
103
114
|
|
|
104
115
|
for (const artifact of execution.status?.artifacts ?? []) {
|
|
105
|
-
const key =
|
|
116
|
+
const key = artifactKey(artifact);
|
|
106
117
|
entryMap.set(key, {
|
|
107
118
|
artifact,
|
|
108
119
|
executionId,
|