@stigmer/react 3.1.3 → 3.1.4
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/index.d.ts +8 -2
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +4 -1
- 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/index.d.ts +3 -3
- package/index.d.ts.map +1 -1
- package/index.js +2 -2
- 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/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.map +1 -1
- package/session/SessionViewer.js +91 -18
- 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 +2 -1
- package/session/index.d.ts.map +1 -1
- package/session/index.js +4 -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/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/__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__/message-entry.test.tsx +50 -2
- package/src/execution/__tests__/useArtifactInspection.test.tsx +151 -0
- package/src/execution/__tests__/useCreateAgentExecution.test.tsx +35 -0
- package/src/execution/index.ts +14 -2
- package/src/execution/useArtifactContent.ts +7 -0
- package/src/execution/useArtifactInspection.ts +252 -0
- package/src/execution/useCreateAgentExecution.ts +12 -0
- package/src/index.ts +11 -2
- package/src/internal/MermaidDiagram.tsx +164 -0
- 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 +177 -33
- 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/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 +5 -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/styles.css +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
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { describe, it, expect, afterEach } from "vitest";
|
|
2
2
|
import { renderHook, act, cleanup } from "@testing-library/react";
|
|
3
|
-
import {
|
|
3
|
+
import { create } from "@bufbuild/protobuf";
|
|
4
|
+
import {
|
|
5
|
+
ExecutionArtifactKind,
|
|
6
|
+
ExecutionPhase,
|
|
7
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
8
|
+
import { ExecutionArtifactSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
|
|
4
9
|
import { useSessionPanel, type UseSessionPanelOptions } from "../useSessionPanel";
|
|
5
10
|
import { PLAN_DOCUMENT_ENTRY_ID, PLAN_DOCUMENT_PATH } from "../plan-document";
|
|
11
|
+
import { ARTIFACT_DOCUMENT_ENTRY_ID } from "../artifact-document";
|
|
12
|
+
import { artifactKey } from "../useSessionArtifacts";
|
|
6
13
|
import type { SelectedThreadItem } from "../../internal/store/selection-store";
|
|
7
14
|
|
|
8
15
|
// ---------------------------------------------------------------------------
|
|
@@ -255,6 +262,68 @@ describe("useSessionPanel — plan document tab", () => {
|
|
|
255
262
|
});
|
|
256
263
|
});
|
|
257
264
|
|
|
265
|
+
describe("useSessionPanel — artifact document tabs", () => {
|
|
266
|
+
function artifact(sandboxPath: string, name: string) {
|
|
267
|
+
return create(ExecutionArtifactSchema, {
|
|
268
|
+
name,
|
|
269
|
+
kind: ExecutionArtifactKind.FILE,
|
|
270
|
+
sandboxPath,
|
|
271
|
+
storageKey: `artifacts/aex_1/${name}`,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
it("openArtifact opens the panel with a PREVIEW tab keyed by artifactKey", () => {
|
|
276
|
+
const { result } = renderPanel();
|
|
277
|
+
const a = artifact(".stigmer/notes.md", "notes.md");
|
|
278
|
+
act(() => result.current.openArtifact(a));
|
|
279
|
+
|
|
280
|
+
expect(result.current.isOpen).toBe(true);
|
|
281
|
+
expect(result.current.editorsStore.getSnapshot().editors).toEqual([
|
|
282
|
+
{ entryId: ARTIFACT_DOCUMENT_ENTRY_ID, path: artifactKey(a), preview: true },
|
|
283
|
+
]);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("reuses the single preview slot as different artifacts are browsed", () => {
|
|
287
|
+
const { result } = renderPanel();
|
|
288
|
+
act(() => result.current.openArtifact(artifact(".stigmer/a.md", "a.md")));
|
|
289
|
+
act(() => result.current.openArtifact(artifact(".stigmer/b.md", "b.md")));
|
|
290
|
+
|
|
291
|
+
// VS Code preview semantics: single-click browsing reuses one tab.
|
|
292
|
+
expect(result.current.editorsStore.getSnapshot().editors).toEqual([
|
|
293
|
+
{ entryId: ARTIFACT_DOCUMENT_ENTRY_ID, path: ".stigmer/b.md", preview: true },
|
|
294
|
+
]);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("does not evict a pinned plan tab when an artifact opens", () => {
|
|
298
|
+
const { result } = renderPanel();
|
|
299
|
+
act(() => result.current.openPlanDocument());
|
|
300
|
+
act(() => result.current.openArtifact(artifact(".stigmer/a.md", "a.md")));
|
|
301
|
+
|
|
302
|
+
expect(result.current.editorsStore.getSnapshot().editors).toEqual([
|
|
303
|
+
{ entryId: PLAN_DOCUMENT_ENTRY_ID, path: PLAN_DOCUMENT_PATH, preview: false },
|
|
304
|
+
{ entryId: ARTIFACT_DOCUMENT_ENTRY_ID, path: ".stigmer/a.md", preview: true },
|
|
305
|
+
]);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("pinArtifact promotes an open artifact preview to a persistent tab", () => {
|
|
309
|
+
const { result } = renderPanel();
|
|
310
|
+
const a = artifact(".stigmer/notes.md", "notes.md");
|
|
311
|
+
// The single-click open leaves a preview tab; the double-click pins it.
|
|
312
|
+
act(() => result.current.openArtifact(a));
|
|
313
|
+
act(() => result.current.pinArtifact(a));
|
|
314
|
+
|
|
315
|
+
expect(result.current.editorsStore.getSnapshot().editors).toEqual([
|
|
316
|
+
{ entryId: ARTIFACT_DOCUMENT_ENTRY_ID, path: artifactKey(a), preview: false },
|
|
317
|
+
]);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("pinArtifact is a no-op when the artifact is not open (mirrors the store's generic pin)", () => {
|
|
321
|
+
const { result } = renderPanel();
|
|
322
|
+
act(() => result.current.pinArtifact(artifact(".stigmer/x.md", "x.md")));
|
|
323
|
+
expect(result.current.editorsStore.getSnapshot().editors).toEqual([]);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
|
|
258
327
|
describe("useSessionPanel — defaultView (home view)", () => {
|
|
259
328
|
it("seeds the view to the provided defaultView (launcher homes on Config)", () => {
|
|
260
329
|
const { result } = renderPanel({ defaultView: "configure" });
|
|
@@ -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,
|
|
@@ -127,6 +127,10 @@ export {
|
|
|
127
127
|
PLAN_DOCUMENT_PATH,
|
|
128
128
|
} from "./plan-document.js";
|
|
129
129
|
|
|
130
|
+
// Artifact document — the editor-area rendering of an execution artifact
|
|
131
|
+
// (mounted as an artifact-family virtual document) and the identity of its tabs.
|
|
132
|
+
export { ARTIFACT_DOCUMENT_ENTRY_ID } from "./artifact-document.js";
|
|
133
|
+
|
|
130
134
|
// Selection types (for platform builders composing their own Inspect facet)
|
|
131
135
|
export type { SelectedThreadItem } from "../internal/store/selection-store.js";
|
|
132
136
|
|
|
@@ -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,
|
|
@@ -117,6 +117,15 @@ export interface SendFollowUpOptions {
|
|
|
117
117
|
* @see {@link CreateAgentExecutionInput.workspaceFileRefs}
|
|
118
118
|
*/
|
|
119
119
|
readonly workspaceFileRefs?: string[];
|
|
120
|
+
/**
|
|
121
|
+
* ID of the execution this follow-up supersedes (edit-and-resubmit).
|
|
122
|
+
*
|
|
123
|
+
* The conversation read model hides the superseded execution so the
|
|
124
|
+
* edited message replaces the original in place.
|
|
125
|
+
*
|
|
126
|
+
* @see {@link CreateAgentExecutionInput.supersedesExecutionId}
|
|
127
|
+
*/
|
|
128
|
+
readonly supersedesExecutionId?: string;
|
|
120
129
|
}
|
|
121
130
|
|
|
122
131
|
/**
|
|
@@ -129,7 +138,14 @@ export interface SendFollowUpOptions {
|
|
|
129
138
|
export interface UseSessionConversationReturn {
|
|
130
139
|
/** The session object, or null while loading. */
|
|
131
140
|
readonly session: Session | null;
|
|
132
|
-
/**
|
|
141
|
+
/**
|
|
142
|
+
* Executions in terminal phases, in chronological order.
|
|
143
|
+
*
|
|
144
|
+
* Excludes turns replaced via edit-and-resubmit (an execution whose id
|
|
145
|
+
* appears as another execution's `supersedesExecutionId`) — the edited
|
|
146
|
+
* message stands in the original's place. Superseded records remain in
|
|
147
|
+
* execution history surfaces; only the conversation view hides them.
|
|
148
|
+
*/
|
|
133
149
|
readonly completedExecutions: readonly AgentExecution[];
|
|
134
150
|
/** Currently streaming execution (stream or fetch fallback), or null. */
|
|
135
151
|
readonly activeStreamExecution: AgentExecution | null;
|
|
@@ -491,12 +507,37 @@ export function useSessionConversation(
|
|
|
491
507
|
}
|
|
492
508
|
}, [activeExecutionId, stream.phase, refetch]);
|
|
493
509
|
|
|
510
|
+
// Executions replaced via edit-and-resubmit. The successor carries
|
|
511
|
+
// `spec.supersedes_execution_id`; hiding the superseded turn makes the
|
|
512
|
+
// edited message read as a single corrected exchange (in-place replace).
|
|
513
|
+
// The link must also be read off the live stream copy — right after a
|
|
514
|
+
// resubmit, the successor streams before the list refetch delivers it.
|
|
515
|
+
// Display-only: the raw `executions` list (and therefore active-id
|
|
516
|
+
// resolution above) is never filtered.
|
|
517
|
+
//
|
|
518
|
+
// Dep is the scalar link, NOT stream.execution: the stream object changes
|
|
519
|
+
// reference every frame, and rebuilding the Set per frame would hand
|
|
520
|
+
// MessageThread a fresh executions array mid-stream, defeating its
|
|
521
|
+
// memoization (DD-010).
|
|
522
|
+
const streamSupersededId =
|
|
523
|
+
stream.execution?.spec?.supersedesExecutionId || null;
|
|
524
|
+
const supersededIds = useMemo(() => {
|
|
525
|
+
const ids = new Set<string>();
|
|
526
|
+
for (const e of executions) {
|
|
527
|
+
const superseded = e.spec?.supersedesExecutionId;
|
|
528
|
+
if (superseded) ids.add(superseded);
|
|
529
|
+
}
|
|
530
|
+
if (streamSupersededId) ids.add(streamSupersededId);
|
|
531
|
+
return ids;
|
|
532
|
+
}, [executions, streamSupersededId]);
|
|
533
|
+
|
|
494
534
|
const completedExecutions = useMemo(() => {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
(
|
|
498
|
-
|
|
499
|
-
|
|
535
|
+
return executions.filter((e) => {
|
|
536
|
+
const id = e.metadata?.id ?? "";
|
|
537
|
+
if (activeExecutionId && id === activeExecutionId) return false;
|
|
538
|
+
return !supersededIds.has(id);
|
|
539
|
+
});
|
|
540
|
+
}, [executions, activeExecutionId, supersededIds]);
|
|
500
541
|
|
|
501
542
|
const fetchedActiveExecution = useMemo(() => {
|
|
502
543
|
if (!activeExecutionId) return null;
|
|
@@ -604,6 +645,7 @@ export function useSessionConversation(
|
|
|
604
645
|
buildFromPlan: options?.buildFromPlan,
|
|
605
646
|
autoApproveAll: options?.autoApproveAll,
|
|
606
647
|
workspaceFileRefs: options?.workspaceFileRefs,
|
|
648
|
+
supersedesExecutionId: options?.supersedesExecutionId,
|
|
607
649
|
});
|
|
608
650
|
setPendingExecutionId(result.executionId);
|
|
609
651
|
refetch();
|
|
@@ -410,6 +410,7 @@ export function useSessionPageFlow(
|
|
|
410
410
|
// not from the composer (the pre-arm toggle was removed).
|
|
411
411
|
autoApproveAll: autoApproveAll || undefined,
|
|
412
412
|
workspaceFileRefs: context?.workspaceFileRefs,
|
|
413
|
+
supersedesExecutionId: context?.supersedesExecutionId,
|
|
413
414
|
});
|
|
414
415
|
|
|
415
416
|
sessionVariables.clear();
|