@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
|
@@ -48,15 +48,6 @@ export interface MessageEntryProps {
|
|
|
48
48
|
* non-human messages.
|
|
49
49
|
*/
|
|
50
50
|
readonly interactionMode?: InteractionMode;
|
|
51
|
-
/**
|
|
52
|
-
* Renders a `MESSAGE_HUMAN` entry as a compact "Build from plan" chip
|
|
53
|
-
* instead of a prompt bubble. Stamped by the thread builder from
|
|
54
|
-
* `execution_config.build_from_plan`: the turn's message is a short label
|
|
55
|
-
* (the implement instruction is runner-injected), so a full-width bubble
|
|
56
|
-
* would dignify machine plumbing as user prose. Ignored for non-human
|
|
57
|
-
* messages.
|
|
58
|
-
*/
|
|
59
|
-
readonly isBuildFromPlan?: boolean;
|
|
60
51
|
}
|
|
61
52
|
|
|
62
53
|
/**
|
|
@@ -89,7 +80,6 @@ export const MessageEntry = memo(function MessageEntry({
|
|
|
89
80
|
onEdit,
|
|
90
81
|
isPlanDocument,
|
|
91
82
|
interactionMode,
|
|
92
|
-
isBuildFromPlan,
|
|
93
83
|
}: MessageEntryProps) {
|
|
94
84
|
useRenderTracer("MessageEntry", {
|
|
95
85
|
messageType: message.type,
|
|
@@ -99,11 +89,6 @@ export const MessageEntry = memo(function MessageEntry({
|
|
|
99
89
|
|
|
100
90
|
switch (message.type) {
|
|
101
91
|
case MessageType.MESSAGE_HUMAN:
|
|
102
|
-
if (isBuildFromPlan) {
|
|
103
|
-
return (
|
|
104
|
-
<BuildFromPlanChip content={message.content} className={className} />
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
92
|
return (
|
|
108
93
|
<HumanMessage
|
|
109
94
|
content={message.content}
|
|
@@ -196,53 +181,6 @@ function HumanMessage({
|
|
|
196
181
|
* label is not user prose to rephrase; refining the plan happens in the plan
|
|
197
182
|
* document, and the build is re-triggered from its card.
|
|
198
183
|
*/
|
|
199
|
-
function BuildFromPlanChip({
|
|
200
|
-
content,
|
|
201
|
-
className,
|
|
202
|
-
}: {
|
|
203
|
-
content: string;
|
|
204
|
-
className?: string;
|
|
205
|
-
}) {
|
|
206
|
-
return (
|
|
207
|
-
<div
|
|
208
|
-
role="article"
|
|
209
|
-
aria-label="Build from plan"
|
|
210
|
-
className={cn("flex justify-end", className)}
|
|
211
|
-
>
|
|
212
|
-
<span
|
|
213
|
-
className={cn(
|
|
214
|
-
"inline-flex items-center gap-1.5 rounded-full border border-border",
|
|
215
|
-
"bg-muted-subtle px-3 py-1.5 text-xs font-medium text-foreground",
|
|
216
|
-
)}
|
|
217
|
-
>
|
|
218
|
-
<BuildFromPlanIcon />
|
|
219
|
-
{content}
|
|
220
|
-
</span>
|
|
221
|
-
</div>
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/** The plan-card glyph (document lines + implement arrow) at chip scale. */
|
|
226
|
-
function BuildFromPlanIcon() {
|
|
227
|
-
return (
|
|
228
|
-
<svg
|
|
229
|
-
width="13"
|
|
230
|
-
height="13"
|
|
231
|
-
viewBox="0 0 16 16"
|
|
232
|
-
fill="none"
|
|
233
|
-
stroke="currentColor"
|
|
234
|
-
strokeWidth="1.5"
|
|
235
|
-
strokeLinecap="round"
|
|
236
|
-
strokeLinejoin="round"
|
|
237
|
-
className="shrink-0 text-muted-foreground"
|
|
238
|
-
aria-hidden="true"
|
|
239
|
-
>
|
|
240
|
-
<path d="M3 4h10M3 8h7M3 12h8" />
|
|
241
|
-
<path d="M12.5 10.5l1.5 1.5-1.5 1.5" />
|
|
242
|
-
</svg>
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
184
|
function EditIcon() {
|
|
247
185
|
return (
|
|
248
186
|
<svg
|
|
@@ -289,7 +289,7 @@ export function threadContentColumnClass(
|
|
|
289
289
|
* part of the public API.
|
|
290
290
|
*/
|
|
291
291
|
export type ThreadItem =
|
|
292
|
-
| { readonly kind: "message"; readonly message: AgentMessage; readonly key: string; readonly isPending?: boolean; readonly isFailed?: boolean; readonly isEditable?: boolean; readonly isPlanDocument?: boolean; readonly interactionMode?: InteractionMode
|
|
292
|
+
| { readonly kind: "message"; readonly message: AgentMessage; readonly key: string; readonly isPending?: boolean; readonly isFailed?: boolean; readonly isEditable?: boolean; readonly isPlanDocument?: boolean; readonly interactionMode?: InteractionMode }
|
|
293
293
|
| { readonly kind: "tool-group"; readonly toolCalls: readonly ToolCall[]; readonly subAgentExecutions: readonly SubAgentExecution[]; readonly key: string }
|
|
294
294
|
| { readonly kind: "sub-agent"; readonly subAgentExecution: SubAgentExecution; readonly key: string }
|
|
295
295
|
| { readonly kind: "phase-badge"; readonly phase: ExecutionPhase; readonly key: string }
|
|
@@ -317,9 +317,9 @@ export type ThreadItem =
|
|
|
317
317
|
readonly planTitle?: string;
|
|
318
318
|
/**
|
|
319
319
|
* True for the most recent completed Plan execution in the thread — the
|
|
320
|
-
* only plan whose card carries the primary "Build
|
|
321
|
-
*
|
|
322
|
-
*
|
|
320
|
+
* only plan whose card carries the primary "Build" action. Superseded
|
|
321
|
+
* plans keep their review actions (open/download) but never a build
|
|
322
|
+
* CTA, so a stale plan can't be implemented by accident.
|
|
323
323
|
*/
|
|
324
324
|
readonly isLatestPlan: boolean;
|
|
325
325
|
}
|
|
@@ -537,9 +537,9 @@ export function buildThreadItems(
|
|
|
537
537
|
? allExecutions.length - 1
|
|
538
538
|
: -1;
|
|
539
539
|
|
|
540
|
-
// The most recent completed Plan execution owns the primary "Build
|
|
541
|
-
//
|
|
542
|
-
//
|
|
540
|
+
// The most recent completed Plan execution owns the primary "Build" action;
|
|
541
|
+
// every earlier plan renders as a review-only record. Resolved up front so
|
|
542
|
+
// each segment can stamp its own card with the right authority.
|
|
543
543
|
let latestPlanExecutionId: string | null = null;
|
|
544
544
|
for (let i = allExecutions.length - 1; i >= 0; i--) {
|
|
545
545
|
if (isCompletedPlanExecution(allExecutions[i])) {
|
|
@@ -648,7 +648,13 @@ export function buildThreadItems(
|
|
|
648
648
|
}
|
|
649
649
|
|
|
650
650
|
const specMessage = exec.spec?.message;
|
|
651
|
-
|
|
651
|
+
// A Build-from-plan turn synthesizes NO prompt item: its message is a
|
|
652
|
+
// machine-written label ("Build from plan"), not user prose — the real
|
|
653
|
+
// instruction is runner-injected from the same flag. The plan card
|
|
654
|
+
// directly above the turn is the visible cause; rendering the label as a
|
|
655
|
+
// user bubble would attribute words to the user they never typed.
|
|
656
|
+
const isBuildTurn = exec.spec?.executionConfig?.buildFromPlan === true;
|
|
657
|
+
if (specMessage && specMessage !== "execute" && !isBuildTurn) {
|
|
652
658
|
const syntheticHumanMsg = create(AgentMessageSchema);
|
|
653
659
|
syntheticHumanMsg.type = MessageType.MESSAGE_HUMAN;
|
|
654
660
|
syntheticHumanMsg.content = specMessage;
|
|
@@ -671,10 +677,6 @@ export function buildThreadItems(
|
|
|
671
677
|
// The turn's mode marks the prompt bubble (a "Plan" pill on Plan
|
|
672
678
|
// turns) so the transcript reads unambiguously after mode switches.
|
|
673
679
|
interactionMode: exec.spec?.executionConfig?.interactionMode,
|
|
674
|
-
// A Build-from-plan turn's prompt renders as a compact chip, not the
|
|
675
|
-
// message text — the text is a short label; the real instruction is
|
|
676
|
-
// runner-injected from the same flag.
|
|
677
|
-
isBuildFromPlan: exec.spec?.executionConfig?.buildFromPlan || undefined,
|
|
678
680
|
});
|
|
679
681
|
}
|
|
680
682
|
|
|
@@ -896,11 +898,19 @@ export function buildThreadItems(
|
|
|
896
898
|
const reason = lastExec?.status?.error;
|
|
897
899
|
if (reason) {
|
|
898
900
|
const specMessage = lastExec?.spec?.message;
|
|
901
|
+
// A failed build turn offers no inline Retry: resending its label as an
|
|
902
|
+
// ordinary message would drop the buildFromPlan flag (no runner
|
|
903
|
+
// directive, no plan attachment). The plan card above the error is the
|
|
904
|
+
// fully-wired retry — its Build button re-runs the whole pipeline.
|
|
905
|
+
const isBuildTurn =
|
|
906
|
+
lastExec?.spec?.executionConfig?.buildFromPlan === true;
|
|
899
907
|
items.push({
|
|
900
908
|
kind: "execution-error",
|
|
901
909
|
error: reason,
|
|
902
910
|
retryMessage:
|
|
903
|
-
specMessage && specMessage !== "execute"
|
|
911
|
+
specMessage && specMessage !== "execute" && !isBuildTurn
|
|
912
|
+
? specMessage
|
|
913
|
+
: undefined,
|
|
904
914
|
key: `execution-error-${lastExec?.metadata?.id ?? lastPhase}`,
|
|
905
915
|
});
|
|
906
916
|
}
|
|
@@ -936,8 +946,13 @@ export function buildThreadItems(
|
|
|
936
946
|
// not thread items at all: they live in the composer-docked FileReviewDock.
|
|
937
947
|
|
|
938
948
|
if (pendingUserMessage) {
|
|
949
|
+
// A FAILED send never created an execution, so an existing execution with
|
|
950
|
+
// identical text (e.g. two build attempts of the same plan — both
|
|
951
|
+
// "Build from plan") must never swallow the failed bubble. Suppression
|
|
952
|
+
// only applies to the succeeded-send window where the stream has
|
|
953
|
+
// delivered the same turn but the pending state hasn't cleared yet.
|
|
939
954
|
const alreadySynthesized =
|
|
940
|
-
lastExec?.spec?.message === pendingUserMessage;
|
|
955
|
+
!pendingMessageFailed && lastExec?.spec?.message === pendingUserMessage;
|
|
941
956
|
if (!alreadySynthesized) {
|
|
942
957
|
const syntheticPending = create(AgentMessageSchema);
|
|
943
958
|
syntheticPending.type = MessageType.MESSAGE_HUMAN;
|
|
@@ -1333,7 +1348,6 @@ export function ThreadItemRenderer({
|
|
|
1333
1348
|
className={item.isPending ? "opacity-70" : undefined}
|
|
1334
1349
|
isPlanDocument={item.isPlanDocument}
|
|
1335
1350
|
interactionMode={item.interactionMode}
|
|
1336
|
-
isBuildFromPlan={item.isBuildFromPlan}
|
|
1337
1351
|
onEdit={
|
|
1338
1352
|
item.isEditable && onEditMessage
|
|
1339
1353
|
? () => onEditMessage(item.message.content)
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { memo, useState } from "react";
|
|
3
|
+
import { memo, useState, type ReactNode } from "react";
|
|
4
4
|
import { cn } from "@stigmer/theme";
|
|
5
5
|
import type { ExecutionArtifact } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/artifact_pb";
|
|
6
6
|
import { ArtifactPreviewModal } from "./ArtifactPreviewModal.js";
|
|
7
7
|
import { formatArtifactSize } from "./artifact-utils.js";
|
|
8
|
+
import { useArtifactCopy } from "./useArtifactCopy.js";
|
|
8
9
|
import { useArtifactDownload } from "./useArtifactDownload.js";
|
|
9
10
|
import { useBuildFromPlanHotkey } from "./use-build-from-plan-hotkey.js";
|
|
10
11
|
|
|
@@ -21,20 +22,20 @@ export interface PlanArtifactCardProps {
|
|
|
21
22
|
*/
|
|
22
23
|
readonly title?: string;
|
|
23
24
|
/**
|
|
24
|
-
* Organization slug. Required for the
|
|
25
|
+
* Organization slug. Required for the open-in-modal fallback (the shared
|
|
25
26
|
* {@link ArtifactPreviewModal} uses it for its detection/apply pipeline and
|
|
26
|
-
* to fetch the content). When omitted,
|
|
27
|
-
* "Build
|
|
27
|
+
* to fetch the content). When omitted, that fallback is hidden — the
|
|
28
|
+
* prominent "Build" action and the download icon remain.
|
|
28
29
|
*/
|
|
29
30
|
readonly org?: string;
|
|
30
|
-
/** Called when the user clicks "Build
|
|
31
|
+
/** Called when the user clicks "Build". Hidden when omitted. */
|
|
31
32
|
readonly onImplement?: () => void;
|
|
32
33
|
/**
|
|
33
34
|
* Opens the plan in the session panel's plan document tab — the
|
|
34
35
|
* side-by-side review/refine surface. When provided it REPLACES the
|
|
35
|
-
* modal-based
|
|
36
|
-
*
|
|
37
|
-
* modal
|
|
36
|
+
* modal-based open fallback (one review affordance, not two). When
|
|
37
|
+
* omitted (hosts without a session panel) the same "Open plan" icon
|
|
38
|
+
* opens the preview modal instead.
|
|
38
39
|
*/
|
|
39
40
|
readonly onOpenPlan?: () => void;
|
|
40
41
|
/** Disables the primary CTA (e.g., while an execution is active). */
|
|
@@ -52,9 +53,15 @@ export interface PlanArtifactCardProps {
|
|
|
52
53
|
* the panel carries the document.
|
|
53
54
|
*
|
|
54
55
|
* Anatomy: a document-style identity block (plan icon, the plan's title, a
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
56
|
+
* `<name> · size` meta line) with the actions trailing — open, copy, and
|
|
57
|
+
* download as icon-only secondaries (labelled via `aria-label`/`title`) and
|
|
58
|
+
* one prominent, themeable primary, **Build** — so the next step is
|
|
59
|
+
* unmistakable.
|
|
60
|
+
*
|
|
61
|
+
* Action-label rule (shared with `PlanStreamingCard`, which keeps its
|
|
62
|
+
* labelled "Open plan"): a lone secondary keeps its text label; a row of
|
|
63
|
+
* secondaries competing with a primary collapses to icons, so the primary
|
|
64
|
+
* carries the card's one visible verb.
|
|
58
65
|
*
|
|
59
66
|
* Only the LATEST plan's card carries `onImplement`; a superseded plan's card
|
|
60
67
|
* is review-only (its `onOpenPlan` opens the document read-only), so a stale
|
|
@@ -76,6 +83,7 @@ export const PlanArtifactCard = memo(function PlanArtifactCard({
|
|
|
76
83
|
}: PlanArtifactCardProps) {
|
|
77
84
|
const [previewOpen, setPreviewOpen] = useState(false);
|
|
78
85
|
const handleKeyDown = useBuildFromPlanHotkey(onImplement, disabled);
|
|
86
|
+
const { copy, isCopying, copied } = useArtifactCopy(executionId);
|
|
79
87
|
const { download, isDownloading } = useArtifactDownload(executionId);
|
|
80
88
|
|
|
81
89
|
return (
|
|
@@ -103,53 +111,41 @@ export const PlanArtifactCard = memo(function PlanArtifactCard({
|
|
|
103
111
|
</div>
|
|
104
112
|
</div>
|
|
105
113
|
|
|
106
|
-
<div className="ml-auto flex flex-wrap items-center gap-x-
|
|
114
|
+
<div className="ml-auto flex flex-wrap items-center gap-x-1.5 gap-y-1.5">
|
|
107
115
|
{onOpenPlan ? (
|
|
108
|
-
<
|
|
109
|
-
|
|
116
|
+
<IconAction
|
|
117
|
+
label="Open plan"
|
|
110
118
|
onClick={onOpenPlan}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
FOCUS_RING,
|
|
114
|
-
)}
|
|
115
|
-
>
|
|
116
|
-
<ExpandIcon />
|
|
117
|
-
Open plan
|
|
118
|
-
</button>
|
|
119
|
+
icon={<EyeIcon />}
|
|
120
|
+
/>
|
|
119
121
|
) : (
|
|
120
122
|
org && (
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
+
<IconAction
|
|
124
|
+
label="Open plan"
|
|
123
125
|
onClick={() => setPreviewOpen(true)}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
FOCUS_RING,
|
|
127
|
-
)}
|
|
128
|
-
>
|
|
129
|
-
<ExpandIcon />
|
|
130
|
-
Open full
|
|
131
|
-
</button>
|
|
126
|
+
icon={<EyeIcon />}
|
|
127
|
+
/>
|
|
132
128
|
)
|
|
133
129
|
)}
|
|
134
|
-
<
|
|
135
|
-
|
|
130
|
+
<IconAction
|
|
131
|
+
label={copied ? "Copied" : isCopying ? "Copying…" : "Copy plan"}
|
|
132
|
+
onClick={() => copy(artifact.storageKey)}
|
|
133
|
+
disabled={isCopying}
|
|
134
|
+
icon={copied ? <CheckIcon /> : <CopyIcon />}
|
|
135
|
+
/>
|
|
136
|
+
<IconAction
|
|
137
|
+
label={isDownloading ? "Preparing…" : `Download ${artifact.name}`}
|
|
136
138
|
onClick={() => download(artifact.storageKey, artifact.name)}
|
|
137
139
|
disabled={isDownloading}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
FOCUS_RING,
|
|
141
|
-
)}
|
|
142
|
-
>
|
|
143
|
-
<DownloadIcon />
|
|
144
|
-
{isDownloading ? "Preparing…" : "Download"}
|
|
145
|
-
</button>
|
|
140
|
+
icon={<DownloadIcon />}
|
|
141
|
+
/>
|
|
146
142
|
{onImplement && (
|
|
147
143
|
<button
|
|
148
144
|
type="button"
|
|
149
145
|
disabled={disabled}
|
|
150
146
|
onClick={onImplement}
|
|
151
147
|
className={cn(
|
|
152
|
-
"inline-flex items-center gap-1.5 rounded-md px-3 py-1.5",
|
|
148
|
+
"ml-1.5 inline-flex items-center gap-1.5 rounded-md px-3 py-1.5",
|
|
153
149
|
"text-xs font-medium transition-colors",
|
|
154
150
|
"bg-primary text-primary-foreground hover:bg-primary-hover",
|
|
155
151
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
@@ -157,13 +153,18 @@ export const PlanArtifactCard = memo(function PlanArtifactCard({
|
|
|
157
153
|
)}
|
|
158
154
|
>
|
|
159
155
|
<ImplementIcon />
|
|
160
|
-
{buildPending ? "Starting build…" : "Build
|
|
156
|
+
{buildPending ? "Starting build…" : "Build"}
|
|
161
157
|
</button>
|
|
162
158
|
)}
|
|
163
159
|
</div>
|
|
164
160
|
|
|
165
|
-
|
|
166
|
-
|
|
161
|
+
<div role="status" aria-live="polite" aria-atomic="true" className="sr-only">
|
|
162
|
+
{copied && "Plan copied to clipboard"}
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
{/* The modal open fallback reuses the shared artifact preview popup —
|
|
166
|
+
the single place that fetches/renders plan content (with Copy +
|
|
167
|
+
Source/Rendered). */}
|
|
167
168
|
{org && previewOpen && (
|
|
168
169
|
<ArtifactPreviewModal
|
|
169
170
|
artifact={artifact}
|
|
@@ -182,6 +183,41 @@ export const PlanArtifactCard = memo(function PlanArtifactCard({
|
|
|
182
183
|
const FOCUS_RING =
|
|
183
184
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:rounded-sm";
|
|
184
185
|
|
|
186
|
+
/**
|
|
187
|
+
* An icon-only secondary action. The accessible name doubles as the hover
|
|
188
|
+
* tooltip (native `title` — the codebase's tooltip pattern), and the padded
|
|
189
|
+
* hit area keeps the target comfortable despite the compact glyph.
|
|
190
|
+
*/
|
|
191
|
+
function IconAction({
|
|
192
|
+
label,
|
|
193
|
+
onClick,
|
|
194
|
+
icon,
|
|
195
|
+
disabled,
|
|
196
|
+
}: {
|
|
197
|
+
readonly label: string;
|
|
198
|
+
readonly onClick: () => void;
|
|
199
|
+
readonly icon: ReactNode;
|
|
200
|
+
readonly disabled?: boolean;
|
|
201
|
+
}) {
|
|
202
|
+
return (
|
|
203
|
+
<button
|
|
204
|
+
type="button"
|
|
205
|
+
aria-label={label}
|
|
206
|
+
title={label}
|
|
207
|
+
onClick={onClick}
|
|
208
|
+
disabled={disabled}
|
|
209
|
+
className={cn(
|
|
210
|
+
"inline-flex items-center rounded-md p-1.5 text-muted-foreground",
|
|
211
|
+
"transition-colors hover:bg-muted hover:text-foreground",
|
|
212
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
213
|
+
FOCUS_RING,
|
|
214
|
+
)}
|
|
215
|
+
>
|
|
216
|
+
{icon}
|
|
217
|
+
</button>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
185
221
|
function PlanIcon() {
|
|
186
222
|
return (
|
|
187
223
|
<svg
|
|
@@ -223,8 +259,8 @@ function ImplementIcon() {
|
|
|
223
259
|
function DownloadIcon() {
|
|
224
260
|
return (
|
|
225
261
|
<svg
|
|
226
|
-
width="
|
|
227
|
-
height="
|
|
262
|
+
width="13"
|
|
263
|
+
height="13"
|
|
228
264
|
viewBox="0 0 12 12"
|
|
229
265
|
fill="none"
|
|
230
266
|
stroke="currentColor"
|
|
@@ -241,21 +277,63 @@ function DownloadIcon() {
|
|
|
241
277
|
);
|
|
242
278
|
}
|
|
243
279
|
|
|
244
|
-
function
|
|
280
|
+
function CopyIcon() {
|
|
281
|
+
return (
|
|
282
|
+
<svg
|
|
283
|
+
width="13"
|
|
284
|
+
height="13"
|
|
285
|
+
viewBox="0 0 12 12"
|
|
286
|
+
fill="none"
|
|
287
|
+
stroke="currentColor"
|
|
288
|
+
strokeWidth="1.2"
|
|
289
|
+
strokeLinecap="round"
|
|
290
|
+
strokeLinejoin="round"
|
|
291
|
+
className="shrink-0"
|
|
292
|
+
aria-hidden="true"
|
|
293
|
+
>
|
|
294
|
+
<rect x="4" y="4" width="6.5" height="6.5" rx="1" />
|
|
295
|
+
<path d="M8 4V2.5C8 1.95 7.55 1.5 7 1.5H2.5C1.95 1.5 1.5 1.95 1.5 2.5V7C1.5 7.55 1.95 8 2.5 8H4" />
|
|
296
|
+
</svg>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** A check glyph — transient confirmation that the copy succeeded. */
|
|
301
|
+
function CheckIcon() {
|
|
245
302
|
return (
|
|
246
303
|
<svg
|
|
247
|
-
width="
|
|
248
|
-
height="
|
|
304
|
+
width="13"
|
|
305
|
+
height="13"
|
|
249
306
|
viewBox="0 0 12 12"
|
|
250
307
|
fill="none"
|
|
251
308
|
stroke="currentColor"
|
|
252
309
|
strokeWidth="1.5"
|
|
253
310
|
strokeLinecap="round"
|
|
254
311
|
strokeLinejoin="round"
|
|
312
|
+
className="shrink-0 text-success"
|
|
313
|
+
aria-hidden="true"
|
|
314
|
+
>
|
|
315
|
+
<path d="M2 6.5L4.5 9L10 3" />
|
|
316
|
+
</svg>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** An eye glyph — "view/open the plan to read it". */
|
|
321
|
+
function EyeIcon() {
|
|
322
|
+
return (
|
|
323
|
+
<svg
|
|
324
|
+
width="13"
|
|
325
|
+
height="13"
|
|
326
|
+
viewBox="0 0 16 16"
|
|
327
|
+
fill="none"
|
|
328
|
+
stroke="currentColor"
|
|
329
|
+
strokeWidth="1.5"
|
|
330
|
+
strokeLinecap="round"
|
|
331
|
+
strokeLinejoin="round"
|
|
255
332
|
className="shrink-0"
|
|
256
333
|
aria-hidden="true"
|
|
257
334
|
>
|
|
258
|
-
<path d="
|
|
335
|
+
<path d="M1.5 8S4 3.5 8 3.5s6.5 4.5 6.5 4.5-2.5 4.5-6.5 4.5S1.5 8 1.5 8Z" />
|
|
336
|
+
<circle cx="8" cy="8" r="1.75" />
|
|
259
337
|
</svg>
|
|
260
338
|
);
|
|
261
339
|
}
|
|
@@ -6,7 +6,7 @@ import { useBuildFromPlanHotkey } from "./use-build-from-plan-hotkey.js";
|
|
|
6
6
|
|
|
7
7
|
/** Props for {@link PlanCompletionCard}. */
|
|
8
8
|
export interface PlanCompletionCardProps {
|
|
9
|
-
/** Called when the user clicks the "Build
|
|
9
|
+
/** Called when the user clicks the "Build" button. */
|
|
10
10
|
readonly onImplement?: () => void;
|
|
11
11
|
/** Disables the CTA button (e.g., while an execution is active). */
|
|
12
12
|
readonly disabled?: boolean;
|
|
@@ -20,9 +20,9 @@ export interface PlanCompletionCardProps {
|
|
|
20
20
|
* a plan whose upload failed). When a plan artifact exists, the richer
|
|
21
21
|
* `PlanArtifactCard` is shown instead.
|
|
22
22
|
*
|
|
23
|
-
* Mirrors `PlanArtifactCard`'s hierarchy — one prominent, themeable "Build
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* Mirrors `PlanArtifactCard`'s hierarchy — one prominent, themeable "Build"
|
|
24
|
+
* primary action with the same card-scoped `Cmd/Ctrl+Enter` accelerator — so
|
|
25
|
+
* the two cards feel like the same affordance. The consumer wires the action
|
|
26
26
|
* to switch the interaction mode to Agent and submit the implement turn.
|
|
27
27
|
*
|
|
28
28
|
* Renders nothing when `onImplement` is not provided, so the card is fully
|
|
@@ -67,7 +67,7 @@ export const PlanCompletionCard = memo(function PlanCompletionCard({
|
|
|
67
67
|
)}
|
|
68
68
|
>
|
|
69
69
|
<ImplementIcon />
|
|
70
|
-
Build
|
|
70
|
+
Build
|
|
71
71
|
</button>
|
|
72
72
|
</div>
|
|
73
73
|
);
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import { memo } from "react";
|
|
4
4
|
import { cn } from "@stigmer/theme";
|
|
5
|
-
import { PLAN_ARTIFACT_NAME } from "../library/detect-plan-artifact.js";
|
|
6
5
|
import { formatArtifactSize } from "./artifact-utils.js";
|
|
7
6
|
|
|
8
7
|
/** Props for {@link PlanStreamingCard}. */
|
|
@@ -34,8 +33,13 @@ export interface PlanStreamingCardProps {
|
|
|
34
33
|
* The live counterpart of {@link PlanArtifactCard} — a streaming Plan turn's
|
|
35
34
|
* compact stand-in in the thread while the plan document renders live in the
|
|
36
35
|
* session panel's plan tab. Same anatomy as the completed card (icon, title,
|
|
37
|
-
* `
|
|
38
|
-
* the same card settling, not a new element appearing.
|
|
36
|
+
* a `Writing… · size` meta, trailing actions) so the completion handoff reads
|
|
37
|
+
* as the same card settling, not a new element appearing.
|
|
38
|
+
*
|
|
39
|
+
* The meta line deliberately omits a filename: the artifact's name is derived
|
|
40
|
+
* from the plan's title, which is still streaming in, so a name shown here
|
|
41
|
+
* would change under the user when the plan settles. The completed card
|
|
42
|
+
* introduces the final `<slug>_<id>.plan.md` name once it exists.
|
|
39
43
|
*
|
|
40
44
|
* Deliberately action-light: "Open plan" is the only affordance. Download and
|
|
41
45
|
* Build require the published artifact, which does not exist until the turn
|
|
@@ -72,8 +76,6 @@ export const PlanStreamingCard = memo(function PlanStreamingCard({
|
|
|
72
76
|
{title ?? "Writing plan…"}
|
|
73
77
|
</div>
|
|
74
78
|
<div className="text-[0.65rem] text-muted-foreground-faint">
|
|
75
|
-
{PLAN_ARTIFACT_NAME}
|
|
76
|
-
<span aria-hidden="true"> · </span>
|
|
77
79
|
<span role="status">Writing…</span>
|
|
78
80
|
<span aria-hidden="true"> · </span>
|
|
79
81
|
<span className="tabular-nums">{formatArtifactSize(sizeBytes)}</span>
|
|
@@ -89,7 +91,7 @@ export const PlanStreamingCard = memo(function PlanStreamingCard({
|
|
|
89
91
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:rounded-sm",
|
|
90
92
|
)}
|
|
91
93
|
>
|
|
92
|
-
<
|
|
94
|
+
<EyeIcon />
|
|
93
95
|
Open plan
|
|
94
96
|
</button>
|
|
95
97
|
)}
|
|
@@ -117,12 +119,13 @@ function PlanIcon({ className }: { readonly className?: string }) {
|
|
|
117
119
|
);
|
|
118
120
|
}
|
|
119
121
|
|
|
120
|
-
|
|
122
|
+
/** An eye glyph — "view/open the plan to read it" (matches PlanArtifactCard). */
|
|
123
|
+
function EyeIcon() {
|
|
121
124
|
return (
|
|
122
125
|
<svg
|
|
123
|
-
width="
|
|
124
|
-
height="
|
|
125
|
-
viewBox="0 0
|
|
126
|
+
width="11"
|
|
127
|
+
height="11"
|
|
128
|
+
viewBox="0 0 16 16"
|
|
126
129
|
fill="none"
|
|
127
130
|
stroke="currentColor"
|
|
128
131
|
strokeWidth="1.5"
|
|
@@ -131,7 +134,8 @@ function ExpandIcon() {
|
|
|
131
134
|
className="shrink-0"
|
|
132
135
|
aria-hidden="true"
|
|
133
136
|
>
|
|
134
|
-
<path d="
|
|
137
|
+
<path d="M1.5 8S4 3.5 8 3.5s6.5 4.5 6.5 4.5-2.5 4.5-6.5 4.5S1.5 8 1.5 8Z" />
|
|
138
|
+
<circle cx="8" cy="8" r="1.75" />
|
|
135
139
|
</svg>
|
|
136
140
|
);
|
|
137
141
|
}
|
|
@@ -26,8 +26,8 @@ function withStigmer(children: ReactNode) {
|
|
|
26
26
|
|
|
27
27
|
afterEach(cleanup);
|
|
28
28
|
|
|
29
|
-
describe("ArtifactPreviewContent — Build
|
|
30
|
-
it("renders a 'Build
|
|
29
|
+
describe("ArtifactPreviewContent — Build action", () => {
|
|
30
|
+
it("renders a 'Build' button only when onImplement is provided", () => {
|
|
31
31
|
const { rerender } = render(
|
|
32
32
|
withStigmer(
|
|
33
33
|
<ArtifactPreviewContent
|
|
@@ -39,7 +39,7 @@ describe("ArtifactPreviewContent — Build from plan action", () => {
|
|
|
39
39
|
/>,
|
|
40
40
|
),
|
|
41
41
|
);
|
|
42
|
-
expect(screen.queryByText("Build
|
|
42
|
+
expect(screen.queryByText("Build")).toBeNull();
|
|
43
43
|
|
|
44
44
|
rerender(
|
|
45
45
|
withStigmer(
|
|
@@ -53,10 +53,10 @@ describe("ArtifactPreviewContent — Build from plan action", () => {
|
|
|
53
53
|
/>,
|
|
54
54
|
),
|
|
55
55
|
);
|
|
56
|
-
expect(screen.getByText("Build
|
|
56
|
+
expect(screen.getByText("Build")).toBeTruthy();
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
it("calls onImplement then closes the modal when 'Build
|
|
59
|
+
it("calls onImplement then closes the modal when 'Build' is clicked", () => {
|
|
60
60
|
const calls: string[] = [];
|
|
61
61
|
const onImplement = vi.fn(() => calls.push("implement"));
|
|
62
62
|
const onClose = vi.fn(() => calls.push("close"));
|
|
@@ -74,7 +74,7 @@ describe("ArtifactPreviewContent — Build from plan action", () => {
|
|
|
74
74
|
),
|
|
75
75
|
);
|
|
76
76
|
|
|
77
|
-
fireEvent.click(screen.getByText("Build
|
|
77
|
+
fireEvent.click(screen.getByText("Build"));
|
|
78
78
|
|
|
79
79
|
expect(onImplement).toHaveBeenCalledTimes(1);
|
|
80
80
|
expect(onClose).toHaveBeenCalledTimes(1);
|