@stigmer/react 3.1.2 → 3.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/composer/SessionComposer.d.ts +6 -5
- package/composer/SessionComposer.d.ts.map +1 -1
- package/composer/SessionComposer.js +1 -1
- package/composer/SessionComposer.js.map +1 -1
- package/execution/ArtifactPreviewModal.d.ts +5 -5
- package/execution/ArtifactPreviewModal.js +4 -4
- package/execution/ArtifactPreviewModal.js.map +1 -1
- package/execution/FileChangeProgressBar.d.ts +47 -0
- package/execution/FileChangeProgressBar.d.ts.map +1 -0
- package/execution/FileChangeProgressBar.js +49 -0
- package/execution/FileChangeProgressBar.js.map +1 -0
- package/execution/FileChangesView.d.ts.map +1 -1
- package/execution/FileChangesView.js +19 -4
- package/execution/FileChangesView.js.map +1 -1
- package/execution/FileReviewAtoms.d.ts +27 -0
- package/execution/FileReviewAtoms.d.ts.map +1 -0
- package/execution/FileReviewAtoms.js +64 -0
- package/execution/FileReviewAtoms.js.map +1 -0
- package/execution/FileReviewCard.d.ts +2 -2
- package/execution/FileReviewCard.d.ts.map +1 -1
- package/execution/FileReviewCard.js +32 -62
- package/execution/FileReviewCard.js.map +1 -1
- package/execution/MessageEntry.d.ts +0 -9
- package/execution/MessageEntry.d.ts.map +1 -1
- package/execution/MessageEntry.js +1 -11
- package/execution/MessageEntry.js.map +1 -1
- package/execution/MessageThread.d.ts +3 -4
- package/execution/MessageThread.d.ts.map +1 -1
- package/execution/MessageThread.js +25 -11
- package/execution/MessageThread.js.map +1 -1
- package/execution/PlanArtifactCard.d.ts +16 -10
- package/execution/PlanArtifactCard.d.ts.map +1 -1
- package/execution/PlanArtifactCard.js +31 -7
- package/execution/PlanArtifactCard.js.map +1 -1
- package/execution/PlanCompletionCard.d.ts +4 -4
- package/execution/PlanCompletionCard.d.ts.map +1 -1
- package/execution/PlanCompletionCard.js +4 -4
- package/execution/PlanCompletionCard.js.map +1 -1
- package/execution/PlanStreamingCard.d.ts +7 -2
- package/execution/PlanStreamingCard.d.ts.map +1 -1
- package/execution/PlanStreamingCard.js +11 -6
- package/execution/PlanStreamingCard.js.map +1 -1
- package/execution/index.d.ts +4 -0
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +2 -0
- package/execution/index.js.map +1 -1
- package/execution/use-build-from-plan-hotkey.d.ts +1 -1
- package/execution/use-build-from-plan-hotkey.js +1 -1
- package/execution/useArtifactCopy.d.ts +51 -0
- package/execution/useArtifactCopy.d.ts.map +1 -0
- package/execution/useArtifactCopy.js +75 -0
- package/execution/useArtifactCopy.js.map +1 -0
- package/execution/useArtifactDownload.d.ts.map +1 -1
- package/execution/useArtifactDownload.js +17 -5
- package/execution/useArtifactDownload.js.map +1 -1
- package/execution/useCreateAgentExecution.d.ts +5 -3
- package/execution/useCreateAgentExecution.d.ts.map +1 -1
- 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/file-tree/FileTreeNode.d.ts.map +1 -1
- package/internal/file-tree/FileTreeNode.js +2 -2
- package/internal/file-tree/FileTreeNode.js.map +1 -1
- package/library/detect-plan-artifact.d.ts +27 -9
- package/library/detect-plan-artifact.d.ts.map +1 -1
- package/library/detect-plan-artifact.js +26 -6
- package/library/detect-plan-artifact.js.map +1 -1
- package/library/index.d.ts +1 -1
- package/library/index.d.ts.map +1 -1
- package/library/index.js +1 -1
- package/library/index.js.map +1 -1
- package/package.json +4 -4
- package/session/PlanEditor.js +1 -1
- package/session/PlanEditor.js.map +1 -1
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +32 -13
- package/session/SessionViewer.js.map +1 -1
- package/session/plan-document.d.ts +14 -2
- package/session/plan-document.d.ts.map +1 -1
- package/session/plan-document.js +14 -3
- package/session/plan-document.js.map +1 -1
- package/session/usePlanDraft.d.ts +1 -1
- package/session/useSessionConversation.d.ts +9 -2
- package/session/useSessionConversation.d.ts.map +1 -1
- package/session/useSessionConversation.js +19 -2
- package/session/useSessionConversation.js.map +1 -1
- package/src/composer/SessionComposer.tsx +7 -6
- package/src/execution/ArtifactPreviewModal.tsx +9 -9
- package/src/execution/FileChangeProgressBar.tsx +121 -0
- package/src/execution/FileChangesView.tsx +18 -4
- package/src/execution/FileReviewAtoms.tsx +89 -0
- package/src/execution/FileReviewCard.tsx +57 -90
- package/src/execution/MessageEntry.tsx +0 -62
- package/src/execution/MessageThread.tsx +29 -15
- package/src/execution/PlanArtifactCard.tsx +130 -52
- package/src/execution/PlanCompletionCard.tsx +5 -5
- package/src/execution/PlanStreamingCard.tsx +15 -11
- package/src/execution/__tests__/ArtifactPreviewModal.test.tsx +6 -6
- package/src/execution/__tests__/FileChangeProgressBar.test.tsx +125 -0
- package/src/execution/__tests__/FileReviewCard.test.tsx +66 -7
- package/src/execution/__tests__/MessageThread.test.tsx +1 -1
- package/src/execution/__tests__/PlanArtifactCard.test.tsx +52 -18
- package/src/execution/__tests__/PlanCompletionCard.test.tsx +2 -2
- package/src/execution/__tests__/PlanStreamingCard.test.tsx +11 -4
- package/src/execution/__tests__/buildThreadItems-plan-completion.test.ts +104 -8
- package/src/execution/__tests__/message-entry.test.tsx +6 -37
- package/src/execution/__tests__/useArtifactCopy.test.tsx +90 -0
- package/src/execution/__tests__/useArtifactDownload.test.tsx +2 -0
- package/src/execution/index.ts +6 -0
- package/src/execution/use-build-from-plan-hotkey.ts +1 -1
- package/src/execution/useArtifactCopy.ts +109 -0
- package/src/execution/useArtifactDownload.ts +17 -5
- package/src/execution/useCreateAgentExecution.ts +5 -3
- package/src/index.ts +6 -0
- package/src/internal/file-tree/FileTreeNode.tsx +13 -2
- package/src/library/__tests__/detect-plan-artifact.test.ts +21 -0
- package/src/library/detect-plan-artifact.ts +32 -10
- package/src/library/index.ts +2 -0
- package/src/session/PlanEditor.tsx +1 -1
- package/src/session/SessionViewer.tsx +50 -18
- package/src/session/__tests__/PlanEditor.test.tsx +4 -4
- package/src/session/__tests__/PlanStreamingDocument.test.tsx +1 -1
- package/src/session/__tests__/SessionViewer-fileReviewDock.test.tsx +57 -0
- package/src/session/__tests__/useSessionConversation.test.tsx +44 -0
- package/src/session/facets/__tests__/ArtifactsTab.test.tsx +7 -7
- package/src/session/plan-document.ts +14 -3
- package/src/session/usePlanDraft.ts +1 -1
- package/src/session/useSessionConversation.ts +32 -4
- package/src/workspace/EditorTabs.tsx +38 -9
- package/src/workspace/ExplorerTree.tsx +10 -4
- package/src/workspace/FileViewer.tsx +27 -6
- package/src/workspace/WorkspaceContentSearch.tsx +44 -20
- package/src/workspace/WorkspaceEditor.tsx +19 -16
- package/src/workspace/WorkspaceFileSearch.tsx +51 -28
- package/src/workspace/WorkspaceSurface.tsx +106 -48
- package/src/workspace/__tests__/EditorTabs.test.tsx +4 -2
- package/src/workspace/__tests__/ExplorerTree.test.tsx +52 -0
- package/src/workspace/__tests__/FileViewer.test.tsx +18 -0
- package/src/workspace/__tests__/WorkspaceContentSearch.test.tsx +38 -5
- package/src/workspace/__tests__/WorkspaceEditor-initialPanel.test.tsx +19 -0
- package/src/workspace/__tests__/WorkspaceFileSearch.test.tsx +27 -6
- package/src/workspace/__tests__/WorkspaceSurface.test.tsx +70 -0
- package/src/workspace/__tests__/a11y/harness.tsx +140 -0
- package/src/workspace/__tests__/a11y/mock-capabilities.ts +138 -0
- package/src/workspace/__tests__/a11y/workspace-editor.a11y.test.tsx +65 -0
- package/src/workspace/__tests__/a11y/workspace-explorer-search.a11y.test.tsx +112 -0
- package/src/workspace/__tests__/a11y/workspace-viewer-tabs.a11y.test.tsx +69 -0
- package/styles.css +1 -1
- package/workspace/EditorTabs.d.ts +21 -3
- package/workspace/EditorTabs.d.ts.map +1 -1
- package/workspace/EditorTabs.js +18 -6
- package/workspace/EditorTabs.js.map +1 -1
- package/workspace/ExplorerTree.js +8 -4
- package/workspace/ExplorerTree.js.map +1 -1
- package/workspace/FileViewer.d.ts.map +1 -1
- package/workspace/FileViewer.js +19 -8
- package/workspace/FileViewer.js.map +1 -1
- package/workspace/WorkspaceContentSearch.d.ts.map +1 -1
- package/workspace/WorkspaceContentSearch.js +23 -2
- package/workspace/WorkspaceContentSearch.js.map +1 -1
- package/workspace/WorkspaceEditor.d.ts.map +1 -1
- package/workspace/WorkspaceEditor.js +8 -10
- package/workspace/WorkspaceEditor.js.map +1 -1
- package/workspace/WorkspaceFileSearch.d.ts.map +1 -1
- package/workspace/WorkspaceFileSearch.js +23 -3
- package/workspace/WorkspaceFileSearch.js.map +1 -1
- package/workspace/WorkspaceSurface.d.ts.map +1 -1
- package/workspace/WorkspaceSurface.js +54 -18
- package/workspace/WorkspaceSurface.js.map +1 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { renderHook, act, waitFor } from "@testing-library/react";
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import type { Stigmer } from "@stigmer/sdk";
|
|
5
|
+
import { StigmerContext } from "../../context";
|
|
6
|
+
import { useArtifactCopy } from "../useArtifactCopy";
|
|
7
|
+
|
|
8
|
+
function wrapperFor(stigmer: Stigmer) {
|
|
9
|
+
return ({ children }: { children: ReactNode }) => (
|
|
10
|
+
<StigmerContext.Provider value={stigmer}>{children}</StigmerContext.Provider>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function makeStigmer(getArtifactContent = vi.fn()) {
|
|
15
|
+
return { agentExecution: { getArtifactContent } } as unknown as Stigmer;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const writeText = vi.fn().mockResolvedValue(undefined);
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
writeText.mockClear();
|
|
22
|
+
vi.stubGlobal("navigator", { clipboard: { writeText } });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("useArtifactCopy", () => {
|
|
26
|
+
it("fetches content at click time and writes decoded text to the clipboard", async () => {
|
|
27
|
+
const bytes = new TextEncoder().encode("# Plan\n\nsteps");
|
|
28
|
+
const fn = vi.fn().mockResolvedValue({ content: bytes });
|
|
29
|
+
const { result } = renderHook(() => useArtifactCopy("aex_1"), {
|
|
30
|
+
wrapper: wrapperFor(makeStigmer(fn)),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(result.current.copied).toBe(false);
|
|
34
|
+
await act(async () => {
|
|
35
|
+
await result.current.copy("artifacts/aex_1/plan.md");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(fn).toHaveBeenCalledTimes(1);
|
|
39
|
+
const req = fn.mock.calls[0][0];
|
|
40
|
+
expect(req.executionId).toBe("aex_1");
|
|
41
|
+
expect(req.storageKey).toBe("artifacts/aex_1/plan.md");
|
|
42
|
+
expect(writeText).toHaveBeenCalledWith("# Plan\n\nsteps");
|
|
43
|
+
expect(result.current.copied).toBe(true);
|
|
44
|
+
expect(result.current.error).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("resets the copied flag after the feedback window", async () => {
|
|
48
|
+
vi.useFakeTimers();
|
|
49
|
+
const bytes = new TextEncoder().encode("x");
|
|
50
|
+
const fn = vi.fn().mockResolvedValue({ content: bytes });
|
|
51
|
+
const { result } = renderHook(() => useArtifactCopy("aex_1"), {
|
|
52
|
+
wrapper: wrapperFor(makeStigmer(fn)),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await act(async () => {
|
|
56
|
+
await result.current.copy("artifacts/aex_1/plan.md");
|
|
57
|
+
});
|
|
58
|
+
expect(result.current.copied).toBe(true);
|
|
59
|
+
|
|
60
|
+
act(() => {
|
|
61
|
+
vi.advanceTimersByTime(2000);
|
|
62
|
+
});
|
|
63
|
+
expect(result.current.copied).toBe(false);
|
|
64
|
+
vi.useRealTimers();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("is a no-op when the execution id is unknown", async () => {
|
|
68
|
+
const fn = vi.fn();
|
|
69
|
+
const { result } = renderHook(() => useArtifactCopy(null), {
|
|
70
|
+
wrapper: wrapperFor(makeStigmer(fn)),
|
|
71
|
+
});
|
|
72
|
+
await act(async () => {
|
|
73
|
+
await result.current.copy("artifacts/x/plan.md");
|
|
74
|
+
});
|
|
75
|
+
expect(fn).not.toHaveBeenCalled();
|
|
76
|
+
expect(writeText).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("surfaces an error when the content RPC fails", async () => {
|
|
80
|
+
const fn = vi.fn().mockRejectedValue(new Error("boom"));
|
|
81
|
+
const { result } = renderHook(() => useArtifactCopy("aex_1"), {
|
|
82
|
+
wrapper: wrapperFor(makeStigmer(fn)),
|
|
83
|
+
});
|
|
84
|
+
await act(async () => {
|
|
85
|
+
await result.current.copy("artifacts/aex_1/plan.md");
|
|
86
|
+
});
|
|
87
|
+
await waitFor(() => expect(result.current.error?.message).toBe("boom"));
|
|
88
|
+
expect(result.current.copied).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -38,6 +38,8 @@ describe("useArtifactDownload", () => {
|
|
|
38
38
|
const req = fn.mock.calls[0][0];
|
|
39
39
|
expect(req.executionId).toBe("aex_1");
|
|
40
40
|
expect(req.storageKey).toBe("artifacts/aex_1/file.bin");
|
|
41
|
+
// Save-to-disk actions force an attachment disposition on the minted URL.
|
|
42
|
+
expect(req.asAttachment).toBe(true);
|
|
41
43
|
expect(result.current.isDownloading).toBe(false);
|
|
42
44
|
expect(result.current.error).toBeNull();
|
|
43
45
|
});
|
package/src/execution/index.ts
CHANGED
|
@@ -62,6 +62,9 @@ export type {
|
|
|
62
62
|
export { useArtifactDownload } from "./useArtifactDownload.js";
|
|
63
63
|
export type { UseArtifactDownloadReturn } from "./useArtifactDownload.js";
|
|
64
64
|
|
|
65
|
+
export { useArtifactCopy } from "./useArtifactCopy.js";
|
|
66
|
+
export type { UseArtifactCopyReturn } from "./useArtifactCopy.js";
|
|
67
|
+
|
|
65
68
|
export { useToolOutputContent } from "./useToolOutputContent.js";
|
|
66
69
|
export type {
|
|
67
70
|
UseToolOutputContentReturn,
|
|
@@ -230,6 +233,9 @@ export type { FileReviewCardProps } from "./FileReviewCard.js";
|
|
|
230
233
|
export { FileReviewDock } from "./FileReviewDock.js";
|
|
231
234
|
export type { FileReviewDockProps } from "./FileReviewDock.js";
|
|
232
235
|
|
|
236
|
+
export { FileChangeProgressBar } from "./FileChangeProgressBar.js";
|
|
237
|
+
export type { FileChangeProgressBarProps } from "./FileChangeProgressBar.js";
|
|
238
|
+
|
|
233
239
|
export { ApprovalContext, useApproval } from "./ApprovalContext.js";
|
|
234
240
|
export type { ApprovalContextValue, UseApprovalResult } from "./ApprovalContext.js";
|
|
235
241
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useCallback, type KeyboardEvent } from "react";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Card-scoped `Cmd/Ctrl+Enter` accelerator for
|
|
6
|
+
* Card-scoped `Cmd/Ctrl+Enter` accelerator for a plan card's "Build" action.
|
|
7
7
|
*
|
|
8
8
|
* Returns an `onKeyDown` handler for a plan card's root element. Because it is
|
|
9
9
|
* attached to the card (not `window`), it fires only when the keystroke
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { create } from "@bufbuild/protobuf";
|
|
5
|
+
import { GetArtifactContentRequestSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/io_pb";
|
|
6
|
+
import { useStigmer } from "../hooks.js";
|
|
7
|
+
import { toError } from "../internal/toError.js";
|
|
8
|
+
|
|
9
|
+
/** How long the transient `copied` flag stays true after a successful copy. */
|
|
10
|
+
const COPIED_FEEDBACK_MS = 2000;
|
|
11
|
+
|
|
12
|
+
/** Return value of {@link useArtifactCopy}. */
|
|
13
|
+
export interface UseArtifactCopyReturn {
|
|
14
|
+
/**
|
|
15
|
+
* Fetch the artifact's text at click time and write it to the clipboard.
|
|
16
|
+
* No-op when the execution id is unknown or a copy is already in flight.
|
|
17
|
+
* Resolves once the copy has been attempted.
|
|
18
|
+
*
|
|
19
|
+
* @param storageKey - Storage key of the artifact to copy.
|
|
20
|
+
*/
|
|
21
|
+
readonly copy: (storageKey: string) => Promise<void>;
|
|
22
|
+
|
|
23
|
+
/** `true` while the content is being fetched / written to the clipboard. */
|
|
24
|
+
readonly isCopying: boolean;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* `true` for a brief window after a successful copy (drives "Copied"
|
|
28
|
+
* affordance feedback), then resets automatically.
|
|
29
|
+
*/
|
|
30
|
+
readonly copied: boolean;
|
|
31
|
+
|
|
32
|
+
/** Error from the last failed copy attempt, or `null` when healthy. */
|
|
33
|
+
readonly error: Error | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Behavior hook that copies an execution artifact's text to the clipboard on
|
|
38
|
+
* demand.
|
|
39
|
+
*
|
|
40
|
+
* The imperative sibling of {@link useArtifactDownload}: it fetches content at
|
|
41
|
+
* click time via `getArtifactContent` (CORS-safe, through the Stigmer API)
|
|
42
|
+
* rather than requiring the caller to have loaded it. This suits content-free
|
|
43
|
+
* surfaces — a plan card that detects the artifact by convention and never
|
|
44
|
+
* fetches until the user acts — where a mount-time {@link useArtifactContent}
|
|
45
|
+
* would be wasteful.
|
|
46
|
+
*
|
|
47
|
+
* Returns an imperative `copy(storageKey)` action plus a transient `copied`
|
|
48
|
+
* flag for feedback, mirroring the modal's copy affordance so every surface
|
|
49
|
+
* shares one implementation.
|
|
50
|
+
*
|
|
51
|
+
* @param executionId - Execution that produced the artifacts, or `null` to
|
|
52
|
+
* disable (the action becomes a no-op).
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```tsx
|
|
56
|
+
* const { copy, isCopying, copied } = useArtifactCopy(executionId);
|
|
57
|
+
* <button onClick={() => copy(artifact.storageKey)} disabled={isCopying}>
|
|
58
|
+
* {copied ? "Copied" : "Copy"}
|
|
59
|
+
* </button>
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* @see useArtifactDownload — imperative save-to-disk counterpart
|
|
63
|
+
* @see useArtifactContent — declarative content reader (preview rendering)
|
|
64
|
+
*/
|
|
65
|
+
export function useArtifactCopy(
|
|
66
|
+
executionId: string | null,
|
|
67
|
+
): UseArtifactCopyReturn {
|
|
68
|
+
const stigmer = useStigmer();
|
|
69
|
+
const [isCopying, setIsCopying] = useState(false);
|
|
70
|
+
const [copied, setCopied] = useState(false);
|
|
71
|
+
const [error, setError] = useState<Error | null>(null);
|
|
72
|
+
|
|
73
|
+
// Clear the pending "copied" timer on unmount so a resolved copy never sets
|
|
74
|
+
// state on an unmounted component.
|
|
75
|
+
const copiedTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
return () => {
|
|
78
|
+
if (copiedTimer.current !== null) clearTimeout(copiedTimer.current);
|
|
79
|
+
};
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
const copy = useCallback(
|
|
83
|
+
async (storageKey: string) => {
|
|
84
|
+
if (!executionId || !storageKey || isCopying) return;
|
|
85
|
+
setIsCopying(true);
|
|
86
|
+
setError(null);
|
|
87
|
+
try {
|
|
88
|
+
const result = await stigmer.agentExecution.getArtifactContent(
|
|
89
|
+
create(GetArtifactContentRequestSchema, { executionId, storageKey }),
|
|
90
|
+
);
|
|
91
|
+
const text = new TextDecoder().decode(result.content);
|
|
92
|
+
await navigator.clipboard.writeText(text);
|
|
93
|
+
setCopied(true);
|
|
94
|
+
if (copiedTimer.current !== null) clearTimeout(copiedTimer.current);
|
|
95
|
+
copiedTimer.current = setTimeout(() => setCopied(false), COPIED_FEEDBACK_MS);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
setError(toError(err));
|
|
98
|
+
} finally {
|
|
99
|
+
setIsCopying(false);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
[executionId, stigmer, isCopying],
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
return useMemo(
|
|
106
|
+
() => ({ copy, isCopying, copied, error }),
|
|
107
|
+
[copy, isCopying, copied, error],
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -65,7 +65,16 @@ export function useArtifactDownload(
|
|
|
65
65
|
setError(null);
|
|
66
66
|
try {
|
|
67
67
|
const result = await stigmer.agentExecution.getArtifactDownloadUrl(
|
|
68
|
-
create(GetArtifactDownloadUrlRequestSchema, {
|
|
68
|
+
create(GetArtifactDownloadUrlRequestSchema, {
|
|
69
|
+
executionId,
|
|
70
|
+
storageKey,
|
|
71
|
+
// This is a save-to-disk action, so mint a URL that forces a
|
|
72
|
+
// browser download (Content-Disposition: attachment). Browsers
|
|
73
|
+
// ignore the anchor's `download` attribute cross-origin, so the
|
|
74
|
+
// disposition must ride on the URL itself. Contrast
|
|
75
|
+
// useArtifactDownloadUrl, which stays inline for `<img src>`.
|
|
76
|
+
asAttachment: true,
|
|
77
|
+
}),
|
|
69
78
|
);
|
|
70
79
|
if (result.downloadUrl) {
|
|
71
80
|
triggerBrowserDownload(result.downloadUrl, fileName);
|
|
@@ -86,10 +95,13 @@ export function useArtifactDownload(
|
|
|
86
95
|
}
|
|
87
96
|
|
|
88
97
|
/**
|
|
89
|
-
* Trigger a browser download for a URL via a transient anchor click.
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
98
|
+
* Trigger a browser download for a URL via a transient anchor click.
|
|
99
|
+
*
|
|
100
|
+
* The URL minted for this flow carries `Content-Disposition: attachment`
|
|
101
|
+
* (requested via `asAttachment`), so the browser saves the file even
|
|
102
|
+
* cross-origin — where it would ignore the anchor's `download` attribute. The
|
|
103
|
+
* `download` hint is still set as a same-origin nicety. Falls back to a new tab
|
|
104
|
+
* in non-DOM environments.
|
|
93
105
|
*/
|
|
94
106
|
function triggerBrowserDownload(url: string, fileName?: string): void {
|
|
95
107
|
if (typeof document === "undefined") {
|
|
@@ -55,11 +55,13 @@ export interface CreateAgentExecutionInput {
|
|
|
55
55
|
*/
|
|
56
56
|
readonly interactionMode?: "agent" | "plan";
|
|
57
57
|
/**
|
|
58
|
-
* Marks this execution as a
|
|
58
|
+
* Marks this execution as a Build-from-plan turn: the user approved a
|
|
59
59
|
* plan from a prior Plan-mode execution and asked the agent to implement
|
|
60
60
|
* it. The runner injects the implement-plan directive (pointing at the
|
|
61
|
-
* attached approved plan when present); the thread
|
|
62
|
-
*
|
|
61
|
+
* attached approved plan when present); the thread hides the turn's
|
|
62
|
+
* machine-written message entirely — the plan card above it is the
|
|
63
|
+
* visible cause. Surfaces without that treatment (the CLI, history)
|
|
64
|
+
* show the message text as-is.
|
|
63
65
|
*
|
|
64
66
|
* Maps to `ExecutionConfig.build_from_plan` in the proto.
|
|
65
67
|
*/
|
package/src/index.ts
CHANGED
|
@@ -283,6 +283,7 @@ export {
|
|
|
283
283
|
ApprovalCardBody,
|
|
284
284
|
FileReviewCard,
|
|
285
285
|
FileReviewDock,
|
|
286
|
+
FileChangeProgressBar,
|
|
286
287
|
ApprovalContext,
|
|
287
288
|
useApproval,
|
|
288
289
|
FileReviewContext,
|
|
@@ -307,6 +308,7 @@ export {
|
|
|
307
308
|
useArtifactContent,
|
|
308
309
|
useArtifactDownloadUrl,
|
|
309
310
|
useArtifactDownload,
|
|
311
|
+
useArtifactCopy,
|
|
310
312
|
useToolOutputContent,
|
|
311
313
|
useFileChangeContent,
|
|
312
314
|
execIdFromStorageKey,
|
|
@@ -375,6 +377,7 @@ export type {
|
|
|
375
377
|
ApprovalCardBodyProps,
|
|
376
378
|
FileReviewCardProps,
|
|
377
379
|
FileReviewDockProps,
|
|
380
|
+
FileChangeProgressBarProps,
|
|
378
381
|
ApprovalContextValue,
|
|
379
382
|
UseApprovalResult,
|
|
380
383
|
FileReviewContextValue,
|
|
@@ -397,6 +400,7 @@ export type {
|
|
|
397
400
|
UseArtifactDownloadUrlReturn,
|
|
398
401
|
UseArtifactDownloadUrlOptions,
|
|
399
402
|
UseArtifactDownloadReturn,
|
|
403
|
+
UseArtifactCopyReturn,
|
|
400
404
|
UseToolOutputContentReturn,
|
|
401
405
|
ToolOutputRefLike,
|
|
402
406
|
UseFileChangeContentReturn,
|
|
@@ -980,10 +984,12 @@ export {
|
|
|
980
984
|
detectSkillPackage,
|
|
981
985
|
useDetectSkillPackage,
|
|
982
986
|
isPlanArtifact,
|
|
987
|
+
isPlanArtifactName,
|
|
983
988
|
findPlanArtifact,
|
|
984
989
|
findLatestSessionPlan,
|
|
985
990
|
findStreamingPlan,
|
|
986
991
|
PLAN_ARTIFACT_NAME,
|
|
992
|
+
PLAN_ARTIFACT_SUFFIX,
|
|
987
993
|
parseResourceYaml,
|
|
988
994
|
serializeAgentYaml,
|
|
989
995
|
serializeMcpServerYaml,
|
|
@@ -102,7 +102,15 @@ export function FileTreeNode({
|
|
|
102
102
|
);
|
|
103
103
|
|
|
104
104
|
return (
|
|
105
|
-
<li
|
|
105
|
+
<li
|
|
106
|
+
role="treeitem"
|
|
107
|
+
// Depth conveys tree nesting to assistive tech (indentation is visual
|
|
108
|
+
// only). aria-level is 1-based; `depth` is 0-based. aria-expanded stays on
|
|
109
|
+
// the treeitem (the <li>) — its child group is nested here, so this is the
|
|
110
|
+
// element a tree-aware screen reader reads expandability from.
|
|
111
|
+
aria-level={depth + 1}
|
|
112
|
+
aria-expanded={isFolder ? expanded : undefined}
|
|
113
|
+
>
|
|
106
114
|
<button
|
|
107
115
|
type="button"
|
|
108
116
|
onClick={handleClick}
|
|
@@ -123,7 +131,10 @@ export function FileTreeNode({
|
|
|
123
131
|
aria-current={isSelected ? "true" : undefined}
|
|
124
132
|
>
|
|
125
133
|
{isFolder && (
|
|
126
|
-
<span
|
|
134
|
+
<span
|
|
135
|
+
aria-hidden="true"
|
|
136
|
+
className="text-[10px] text-muted-foreground-subtle"
|
|
137
|
+
>
|
|
127
138
|
{expanded ? "▼" : "▶"}
|
|
128
139
|
</span>
|
|
129
140
|
)}
|
|
@@ -9,6 +9,7 @@ import { ExecutionArtifactKind } from "@stigmer/protos/ai/stigmer/agentic/agente
|
|
|
9
9
|
import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
|
|
10
10
|
import {
|
|
11
11
|
isPlanArtifact,
|
|
12
|
+
isPlanArtifactName,
|
|
12
13
|
findPlanArtifact,
|
|
13
14
|
findLatestSessionPlan,
|
|
14
15
|
PLAN_ARTIFACT_NAME,
|
|
@@ -35,11 +36,31 @@ function executionWithId(id: string, ...artifacts: ReturnType<typeof artifact>[]
|
|
|
35
36
|
});
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
describe("isPlanArtifactName", () => {
|
|
40
|
+
it("accepts the legacy exact name and any *.plan.md", () => {
|
|
41
|
+
expect(isPlanArtifactName(PLAN_ARTIFACT_NAME)).toBe(true);
|
|
42
|
+
expect(isPlanArtifactName("feature_x.plan.md")).toBe(true);
|
|
43
|
+
// The runner's current shape: hyphenated slug + content-hash discriminator.
|
|
44
|
+
expect(isPlanArtifactName("fix-the-download-ux_a1b2c3d4.plan.md")).toBe(true);
|
|
45
|
+
expect(isPlanArtifactName("a1b2c3d4.plan.md")).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("rejects non-plan filenames", () => {
|
|
49
|
+
expect(isPlanArtifactName("report.md")).toBe(false);
|
|
50
|
+
expect(isPlanArtifactName("plan.txt")).toBe(false);
|
|
51
|
+
expect(isPlanArtifactName("myplan.md")).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
38
55
|
describe("isPlanArtifact", () => {
|
|
39
56
|
it("is true for a FILE artifact named plan.md", () => {
|
|
40
57
|
expect(isPlanArtifact(artifact({ name: PLAN_ARTIFACT_NAME }))).toBe(true);
|
|
41
58
|
});
|
|
42
59
|
|
|
60
|
+
it("is true for a FILE artifact named <slug>_<id>.plan.md", () => {
|
|
61
|
+
expect(isPlanArtifact(artifact({ name: "plan-card-ux-cleanup_a1b2c3d4.plan.md" }))).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
43
64
|
it("is false for a directory named plan.md", () => {
|
|
44
65
|
expect(
|
|
45
66
|
isPlanArtifact(artifact({ name: "plan.md", kind: ExecutionArtifactKind.DIRECTORY })),
|
|
@@ -3,26 +3,48 @@ import type { ExecutionArtifact } from "@stigmer/protos/ai/stigmer/agentic/agent
|
|
|
3
3
|
import { ExecutionArtifactKind } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* The session's current plan: the published
|
|
7
|
-
* recent execution that produced one, plus the execution it belongs
|
|
8
|
-
* (artifact content RPCs are execution-scoped).
|
|
6
|
+
* The session's current plan: the published plan artifact (a `*.plan.md` FILE)
|
|
7
|
+
* of the most recent execution that produced one, plus the execution it belongs
|
|
8
|
+
* to (artifact content RPCs are execution-scoped).
|
|
9
9
|
*/
|
|
10
10
|
export interface SessionPlan {
|
|
11
11
|
/** ID of the execution that published the plan. */
|
|
12
12
|
readonly executionId: string;
|
|
13
|
-
/** The published
|
|
13
|
+
/** The published plan artifact (`*.plan.md`). */
|
|
14
14
|
readonly artifact: ExecutionArtifact;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* Legacy plan filename. Detection-only: {@link isPlanArtifactName} still
|
|
19
|
+
* accepts this exact name so plans published before named artifacts existed
|
|
20
|
+
* keep working. The runner never freshly emits it (a titleless plan falls back
|
|
21
|
+
* to a bare `<id>.plan.md`). Kept in sync with the runner's
|
|
22
|
+
* `PLAN_ARTIFACT_NAME` (shared/plan-artifact.ts).
|
|
20
23
|
*/
|
|
21
24
|
export const PLAN_ARTIFACT_NAME = "plan.md";
|
|
22
25
|
|
|
23
26
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
27
|
+
* Suffix every named plan artifact carries (`<slug>_<id>.plan.md`, or a bare
|
|
28
|
+
* `<id>.plan.md` when untitled). The runner derives the slug from the plan's
|
|
29
|
+
* title and the `<id>` from its content hash; detection keys on this suffix.
|
|
30
|
+
* Kept in sync with the runner's `PLAN_ARTIFACT_SUFFIX` (shared/plan-artifact.ts)
|
|
31
|
+
* — the runner and browser SDK have disjoint module graphs, so the constant is
|
|
32
|
+
* duplicated by design, mirroring {@link PLAN_ARTIFACT_NAME}.
|
|
33
|
+
*/
|
|
34
|
+
export const PLAN_ARTIFACT_SUFFIX = ".plan.md";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns `true` when a filename is a plan's: the legacy exact `plan.md`, or
|
|
38
|
+
* any `*.plan.md`. Accepting the legacy name keeps plans published before named
|
|
39
|
+
* artifacts existed detectable.
|
|
40
|
+
*/
|
|
41
|
+
export function isPlanArtifactName(name: string): boolean {
|
|
42
|
+
return name === PLAN_ARTIFACT_NAME || name.endsWith(PLAN_ARTIFACT_SUFFIX);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Returns `true` when an artifact is the published plan: a FILE artifact whose
|
|
47
|
+
* name satisfies {@link isPlanArtifactName}.
|
|
26
48
|
*
|
|
27
49
|
* Detection is by convention — the same lightweight, content-free approach used
|
|
28
50
|
* for skill packages ({@link isSkillPackage}) — so the UI never needs an extra
|
|
@@ -32,7 +54,7 @@ export const PLAN_ARTIFACT_NAME = "plan.md";
|
|
|
32
54
|
export function isPlanArtifact(artifact: ExecutionArtifact): boolean {
|
|
33
55
|
return (
|
|
34
56
|
artifact.kind === ExecutionArtifactKind.FILE &&
|
|
35
|
-
artifact.name
|
|
57
|
+
isPlanArtifactName(artifact.name)
|
|
36
58
|
);
|
|
37
59
|
}
|
|
38
60
|
|
|
@@ -56,7 +78,7 @@ export function findPlanArtifact(
|
|
|
56
78
|
|
|
57
79
|
/**
|
|
58
80
|
* Finds the session's LATEST plan across all executions — the plan the panel's
|
|
59
|
-
* plan document tab edits and "Build
|
|
81
|
+
* plan document tab edits and the "Build" action implements. Executions are
|
|
60
82
|
* scanned newest-first (the array is chronological), mirroring the thread's
|
|
61
83
|
* latest-plan-owns-the-build-action rule so every surface agrees on which
|
|
62
84
|
* plan is current.
|
package/src/library/index.ts
CHANGED
|
@@ -26,9 +26,11 @@ export type { SkillPackageDetection } from "./detect-skill-package.js";
|
|
|
26
26
|
|
|
27
27
|
export {
|
|
28
28
|
isPlanArtifact,
|
|
29
|
+
isPlanArtifactName,
|
|
29
30
|
findPlanArtifact,
|
|
30
31
|
findLatestSessionPlan,
|
|
31
32
|
PLAN_ARTIFACT_NAME,
|
|
33
|
+
PLAN_ARTIFACT_SUFFIX,
|
|
32
34
|
} from "./detect-plan-artifact.js";
|
|
33
35
|
export type { SessionPlan } from "./detect-plan-artifact.js";
|
|
34
36
|
|
|
@@ -25,13 +25,17 @@ import { useWorkspaceEditors, isVirtualEntryId } from "../internal/store/index.j
|
|
|
25
25
|
import { ThreadSelectionContext } from "../execution/ThreadSelectionContext.js";
|
|
26
26
|
import { useSelectedThreadItem } from "../execution/useThreadSelection.js";
|
|
27
27
|
import { MessageThread } from "../execution/MessageThread.js";
|
|
28
|
+
import { FileChangeProgressBar } from "../execution/FileChangeProgressBar.js";
|
|
28
29
|
import { FileReviewDock } from "../execution/FileReviewDock.js";
|
|
30
|
+
import {
|
|
31
|
+
FilePathContext,
|
|
32
|
+
type FilePathContextValue,
|
|
33
|
+
} from "../execution/FilePathContext.js";
|
|
29
34
|
import { ThreadSkeleton } from "../execution/ThreadSkeleton.js";
|
|
30
35
|
import { SessionComposer } from "../composer/index.js";
|
|
31
36
|
import { SecretFlowErrorGuide, isSecretFlowError } from "../error/index.js";
|
|
32
37
|
import { useStigmer } from "../hooks.js";
|
|
33
38
|
import {
|
|
34
|
-
PLAN_ARTIFACT_NAME,
|
|
35
39
|
findLatestSessionPlan,
|
|
36
40
|
findPlanArtifact,
|
|
37
41
|
type SessionPlan,
|
|
@@ -57,18 +61,22 @@ import type { SessionAudience } from "./audience.js";
|
|
|
57
61
|
* Where the approved plan mounts in the implement execution's workspace —
|
|
58
62
|
* the harnesses' standard attachment inputs directory, where the runner's
|
|
59
63
|
* implement-plan directive points the agent (shared/implement-plan-prompt.ts).
|
|
60
|
-
* Attaching
|
|
61
|
-
* the user approved (including in-place
|
|
64
|
+
* Attaching under the plan's own filename (e.g. `feature-x_a1b2c3d4.plan.md`) is what
|
|
65
|
+
* makes the build implement the document the user approved (including in-place
|
|
66
|
+
* edits), not a paraphrase from memory; the runner detects it by the plan
|
|
67
|
+
* filename convention (`isPlanArtifactName`), so the exact name is free to vary.
|
|
62
68
|
*/
|
|
63
|
-
|
|
69
|
+
function approvedPlanMountPath(planFileName: string): string {
|
|
70
|
+
return `.stigmer/inputs/${planFileName}`;
|
|
71
|
+
}
|
|
64
72
|
|
|
65
73
|
/**
|
|
66
74
|
* The build turn's user message — a short human-readable label, NOT the
|
|
67
75
|
* implement instruction. The agent-facing instruction is runner-injected
|
|
68
|
-
* (keyed off `executionConfig.buildFromPlan`)
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
76
|
+
* (keyed off `executionConfig.buildFromPlan`). The thread hides the turn
|
|
77
|
+
* entirely from the same flag (the plan card above it is the visible cause);
|
|
78
|
+
* this label exists for surfaces without that treatment (the CLI, execution
|
|
79
|
+
* history, older clients), which show it as-is.
|
|
72
80
|
*/
|
|
73
81
|
const BUILD_FROM_PLAN_MESSAGE = "Build from plan";
|
|
74
82
|
|
|
@@ -377,7 +385,7 @@ export function SessionViewer({
|
|
|
377
385
|
setPlanAttachFailed(false);
|
|
378
386
|
|
|
379
387
|
// `buildFromPlan` (not message text) is what tells the runner to inject
|
|
380
|
-
// the implement directive and the thread to
|
|
388
|
+
// the implement directive and the thread to hide the turn's message —
|
|
381
389
|
// with or without the attachment (the runner picks the directive variant).
|
|
382
390
|
const submitImplementTurn = (attachments?: AttachmentInput[]) => {
|
|
383
391
|
composerRef.current?.submit(BUILD_FROM_PLAN_MESSAGE, {
|
|
@@ -398,22 +406,25 @@ export function SessionViewer({
|
|
|
398
406
|
// execution. The published artifact itself stays immutable; the approved
|
|
399
407
|
// copy is a new input on the build turn (edit-as-input provenance).
|
|
400
408
|
setIsBuildingFromPlan(true);
|
|
409
|
+
// The approved copy keeps the published plan's filename, so the mounted
|
|
410
|
+
// input, the download, and the plan card all agree on one name.
|
|
411
|
+
const planFileName = sessionPlan.artifact.name;
|
|
401
412
|
void (async () => {
|
|
402
413
|
try {
|
|
403
414
|
const approvedText =
|
|
404
415
|
planDraft.readDraft() ?? (await fetchPlanText(stigmer, sessionPlan));
|
|
405
416
|
const response = await stigmer.agentExecution.uploadAttachment(
|
|
406
417
|
create(UploadAttachmentRequestSchema, {
|
|
407
|
-
filename:
|
|
418
|
+
filename: planFileName,
|
|
408
419
|
content: new TextEncoder().encode(approvedText),
|
|
409
420
|
contentType: "text/markdown",
|
|
410
421
|
}),
|
|
411
422
|
);
|
|
412
423
|
submitImplementTurn([
|
|
413
424
|
{
|
|
414
|
-
filename:
|
|
425
|
+
filename: planFileName,
|
|
415
426
|
storageKey: response.storageKey,
|
|
416
|
-
mountPath:
|
|
427
|
+
mountPath: approvedPlanMountPath(planFileName),
|
|
417
428
|
contentType: "text/markdown",
|
|
418
429
|
},
|
|
419
430
|
]);
|
|
@@ -655,6 +666,20 @@ const ConversationColumn = memo(function ConversationColumn({
|
|
|
655
666
|
[conv.stop, composerRef],
|
|
656
667
|
);
|
|
657
668
|
|
|
669
|
+
// The composer-docked FileReviewDock sits OUTSIDE MessageThread, so the
|
|
670
|
+
// thread's own FilePathContext provider does not reach it. This provider
|
|
671
|
+
// gives the dock's file list the same path resolution and click routing
|
|
672
|
+
// (open in the panel's viewer; GitHub/copy fallback) as the transcript —
|
|
673
|
+
// one click behavior for a path everywhere in the session. Memoized so the
|
|
674
|
+
// dock's memoized cards are not invalidated by unrelated renders (DD-010).
|
|
675
|
+
const dockFilePathCtx = useMemo<FilePathContextValue>(
|
|
676
|
+
() => ({
|
|
677
|
+
workspaceEntries: conv.workspaceEntries ?? [],
|
|
678
|
+
onFilePathClick,
|
|
679
|
+
}),
|
|
680
|
+
[conv.workspaceEntries, onFilePathClick],
|
|
681
|
+
);
|
|
682
|
+
|
|
658
683
|
return (
|
|
659
684
|
<div className="flex h-full min-w-0 flex-col">
|
|
660
685
|
<MessageThread
|
|
@@ -703,12 +728,19 @@ const ConversationColumn = memo(function ConversationColumn({
|
|
|
703
728
|
decision the agent is blocked on can never scroll out of view. The
|
|
704
729
|
thread renders only observational rows (badges) and read-only
|
|
705
730
|
settled records; this is the one decision surface. */}
|
|
706
|
-
<
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
731
|
+
<FilePathContext.Provider value={dockFilePathCtx}>
|
|
732
|
+
{/* Mid-run live capture (DD-32): the "N files changed so far" strip for
|
|
733
|
+
a still-running turn. Mutually exclusive with the dock below —
|
|
734
|
+
progress shows while CAPTURING, the dock once AWAITING_REVIEW — so it
|
|
735
|
+
hands off cleanly when review opens. Non-interactive. */}
|
|
736
|
+
<FileChangeProgressBar progress={conv.fileChangeProgress} />
|
|
737
|
+
<FileReviewDock
|
|
738
|
+
changeSets={conv.fileChangeSets}
|
|
739
|
+
onSubmit={conv.submitFileDecision}
|
|
740
|
+
submittingDecisionKeys={conv.submittingFileDecisionKeys}
|
|
741
|
+
decisionErrors={conv.fileDecisionErrors}
|
|
742
|
+
/>
|
|
743
|
+
</FilePathContext.Provider>
|
|
712
744
|
<SessionComposer
|
|
713
745
|
ref={composerRef}
|
|
714
746
|
onSubmit={flow.handleSubmit}
|