@stigmer/react 3.1.7 → 3.1.8
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/agent/AgentDetailView.d.ts +9 -1
- package/agent/AgentDetailView.d.ts.map +1 -1
- package/agent/AgentDetailView.js +15 -4
- package/agent/AgentDetailView.js.map +1 -1
- package/environment/EnvironmentListPanel.d.ts.map +1 -1
- package/environment/EnvironmentListPanel.js +11 -3
- package/environment/EnvironmentListPanel.js.map +1 -1
- package/environment/EnvironmentVariableEditor.d.ts.map +1 -1
- package/environment/EnvironmentVariableEditor.js +9 -3
- package/environment/EnvironmentVariableEditor.js.map +1 -1
- package/environment/index.d.ts +1 -0
- package/environment/index.d.ts.map +1 -1
- package/environment/index.js +1 -0
- package/environment/index.js.map +1 -1
- package/environment/shareRestriction.d.ts +13 -0
- package/environment/shareRestriction.d.ts.map +1 -0
- package/environment/shareRestriction.js +15 -0
- package/environment/shareRestriction.js.map +1 -0
- package/execution/ToolCallItem.d.ts.map +1 -1
- package/execution/ToolCallItem.js +12 -0
- package/execution/ToolCallItem.js.map +1 -1
- package/execution/ToolRunGroup.js +4 -0
- package/execution/ToolRunGroup.js.map +1 -1
- package/execution/WriteBackCard.d.ts +14 -5
- package/execution/WriteBackCard.d.ts.map +1 -1
- package/execution/WriteBackCard.js +103 -32
- package/execution/WriteBackCard.js.map +1 -1
- package/execution/WriteBacksWidget.d.ts +1 -1
- package/execution/WriteBacksWidget.js +2 -2
- package/execution/WriteBacksWidget.js.map +1 -1
- package/execution/index.d.ts +2 -0
- package/execution/index.d.ts.map +1 -1
- package/execution/index.js +1 -0
- package/execution/index.js.map +1 -1
- package/execution/write-back-utils.d.ts +57 -0
- package/execution/write-back-utils.d.ts.map +1 -0
- package/execution/write-back-utils.js +81 -0
- package/execution/write-back-utils.js.map +1 -0
- package/index.d.ts +6 -2
- package/index.d.ts.map +1 -1
- package/index.js +6 -1
- package/index.js.map +1 -1
- package/internal/dev/dom-counter.d.ts.map +1 -1
- package/internal/dev/dom-counter.js +2 -2
- package/internal/dev/dom-counter.js.map +1 -1
- package/internal/dev/enabled.d.ts +7 -0
- package/internal/dev/enabled.d.ts.map +1 -0
- package/internal/dev/enabled.js +38 -0
- package/internal/dev/enabled.js.map +1 -0
- package/internal/dev/profiler-wrapper.d.ts +3 -3
- package/internal/dev/profiler-wrapper.d.ts.map +1 -1
- package/internal/dev/profiler-wrapper.js +5 -5
- package/internal/dev/profiler-wrapper.js.map +1 -1
- package/internal/dev/use-key-stability.d.ts.map +1 -1
- package/internal/dev/use-key-stability.js +2 -2
- package/internal/dev/use-key-stability.js.map +1 -1
- package/internal/dev/use-render-tracer.d.ts +2 -2
- package/internal/dev/use-render-tracer.d.ts.map +1 -1
- package/internal/dev/use-render-tracer.js +4 -4
- package/internal/dev/use-render-tracer.js.map +1 -1
- package/internal/dev/use-stream-rate.d.ts +2 -1
- package/internal/dev/use-stream-rate.d.ts.map +1 -1
- package/internal/dev/use-stream-rate.js +4 -3
- package/internal/dev/use-stream-rate.js.map +1 -1
- package/library/ResourceVisibilityControl.d.ts +3 -0
- package/library/ResourceVisibilityControl.d.ts.map +1 -1
- package/library/ResourceVisibilityControl.js +22 -8
- package/library/ResourceVisibilityControl.js.map +1 -1
- package/library/useUpdateVisibility.d.ts +5 -3
- package/library/useUpdateVisibility.d.ts.map +1 -1
- package/library/useUpdateVisibility.js +7 -2
- package/library/useUpdateVisibility.js.map +1 -1
- package/library/visibilityLevels.d.ts +17 -0
- package/library/visibilityLevels.d.ts.map +1 -1
- package/library/visibilityLevels.js +32 -0
- package/library/visibilityLevels.js.map +1 -1
- package/package.json +4 -4
- package/session/NewSessionViewer.d.ts +11 -1
- package/session/NewSessionViewer.d.ts.map +1 -1
- package/session/NewSessionViewer.js +28 -8
- package/session/NewSessionViewer.js.map +1 -1
- package/session/SessionViewer.d.ts +9 -2
- package/session/SessionViewer.d.ts.map +1 -1
- package/session/SessionViewer.js +26 -12
- package/session/SessionViewer.js.map +1 -1
- package/session/audience.d.ts +12 -4
- package/session/audience.d.ts.map +1 -1
- package/session/facets/ChangesTab.d.ts.map +1 -1
- package/session/facets/ChangesTab.js +3 -1
- package/session/facets/ChangesTab.js.map +1 -1
- package/session/useNewSessionFlow.d.ts +12 -0
- package/session/useNewSessionFlow.d.ts.map +1 -1
- package/session/useNewSessionFlow.js +13 -1
- package/session/useNewSessionFlow.js.map +1 -1
- package/session/useSessionPageFlow.d.ts +13 -0
- package/session/useSessionPageFlow.d.ts.map +1 -1
- package/session/useSessionPageFlow.js +7 -2
- package/session/useSessionPageFlow.js.map +1 -1
- package/sharing/ShareAgentDialog.d.ts +55 -0
- package/sharing/ShareAgentDialog.d.ts.map +1 -0
- package/sharing/ShareAgentDialog.js +393 -0
- package/sharing/ShareAgentDialog.js.map +1 -0
- package/sharing/SharedAgentChat.d.ts +84 -0
- package/sharing/SharedAgentChat.d.ts.map +1 -0
- package/sharing/SharedAgentChat.js +80 -0
- package/sharing/SharedAgentChat.js.map +1 -0
- package/sharing/index.d.ts +16 -0
- package/sharing/index.d.ts.map +1 -0
- package/sharing/index.js +11 -0
- package/sharing/index.js.map +1 -0
- package/sharing/useRotateShareLink.d.ts +42 -0
- package/sharing/useRotateShareLink.d.ts.map +1 -0
- package/sharing/useRotateShareLink.js +55 -0
- package/sharing/useRotateShareLink.js.map +1 -0
- package/sharing/useShareAgent.d.ts +68 -0
- package/sharing/useShareAgent.d.ts.map +1 -0
- package/sharing/useShareAgent.js +41 -0
- package/sharing/useShareAgent.js.map +1 -0
- package/sharing/useShareToolReadiness.d.ts +48 -0
- package/sharing/useShareToolReadiness.d.ts.map +1 -0
- package/sharing/useShareToolReadiness.js +93 -0
- package/sharing/useShareToolReadiness.js.map +1 -0
- package/sharing/useSharedAgentProfile.d.ts +84 -0
- package/sharing/useSharedAgentProfile.d.ts.map +1 -0
- package/sharing/useSharedAgentProfile.js +72 -0
- package/sharing/useSharedAgentProfile.js.map +1 -0
- package/sharing/useUpdateAgentSharing.d.ts +86 -0
- package/sharing/useUpdateAgentSharing.d.ts.map +1 -0
- package/sharing/useUpdateAgentSharing.js +81 -0
- package/sharing/useUpdateAgentSharing.js.map +1 -0
- package/src/agent/AgentDetailView.tsx +28 -3
- package/src/environment/EnvironmentListPanel.tsx +47 -18
- package/src/environment/EnvironmentVariableEditor.tsx +27 -17
- package/src/environment/__tests__/shareRestriction.test.ts +35 -0
- package/src/environment/index.ts +5 -0
- package/src/environment/shareRestriction.ts +22 -0
- package/src/execution/ToolCallItem.tsx +19 -1
- package/src/execution/ToolRunGroup.tsx +4 -0
- package/src/execution/WriteBackCard.tsx +285 -88
- package/src/execution/WriteBacksWidget.tsx +5 -5
- package/src/execution/__tests__/ToolCallItem.test.tsx +51 -0
- package/src/execution/__tests__/ToolRunGroup.test.tsx +22 -0
- package/src/execution/__tests__/WriteBackCard.test.tsx +64 -10
- package/src/execution/__tests__/write-back-utils.test.ts +135 -0
- package/src/execution/index.ts +7 -0
- package/src/execution/write-back-utils.ts +107 -0
- package/src/index.ts +34 -0
- package/src/internal/dev/__tests__/use-key-stability.test.ts +4 -0
- package/src/internal/dev/__tests__/use-render-tracer.test.ts +4 -0
- package/src/internal/dev/dom-counter.ts +2 -3
- package/src/internal/dev/enabled.ts +39 -0
- package/src/internal/dev/profiler-wrapper.tsx +5 -6
- package/src/internal/dev/use-key-stability.ts +2 -3
- package/src/internal/dev/use-render-tracer.ts +4 -5
- package/src/internal/dev/use-stream-rate.ts +4 -4
- package/src/library/ResourceVisibilityControl.tsx +23 -7
- package/src/library/__tests__/visibilityLevels.test.ts +33 -0
- package/src/library/useUpdateVisibility.ts +9 -3
- package/src/library/visibilityLevels.ts +36 -0
- package/src/session/NewSessionViewer.tsx +67 -31
- package/src/session/SessionViewer.tsx +67 -36
- package/src/session/__tests__/audienceWiring.test.tsx +99 -0
- package/src/session/__tests__/useNewSessionFlow.test.tsx +75 -1
- package/src/session/__tests__/useSessionPageFlow.guest.test.tsx +112 -0
- package/src/session/audience.ts +12 -4
- package/src/session/facets/ChangesTab.tsx +6 -4
- package/src/session/useNewSessionFlow.ts +26 -1
- package/src/session/useSessionPageFlow.ts +24 -2
- package/src/sharing/ShareAgentDialog.tsx +1255 -0
- package/src/sharing/SharedAgentChat.tsx +283 -0
- package/src/sharing/__tests__/ShareAgentDialog.test.tsx +745 -0
- package/src/sharing/__tests__/SharedAgentChat.test.tsx +222 -0
- package/src/sharing/__tests__/useRotateShareLink.test.tsx +96 -0
- package/src/sharing/__tests__/useShareAgent.test.tsx +127 -0
- package/src/sharing/__tests__/useShareToolReadiness.test.tsx +182 -0
- package/src/sharing/__tests__/useSharedAgentProfile.test.tsx +201 -0
- package/src/sharing/__tests__/useUpdateAgentSharing.test.tsx +200 -0
- package/src/sharing/index.ts +30 -0
- package/src/sharing/useRotateShareLink.ts +76 -0
- package/src/sharing/useShareAgent.tsx +106 -0
- package/src/sharing/useShareToolReadiness.ts +129 -0
- package/src/sharing/useSharedAgentProfile.ts +131 -0
- package/src/sharing/useUpdateAgentSharing.ts +151 -0
- package/src/switch/Switch.tsx +92 -0
- package/src/switch/__tests__/Switch.test.tsx +88 -0
- package/src/switch/index.ts +2 -0
- package/styles.css +1 -1
- package/switch/Switch.d.ts +47 -0
- package/switch/Switch.d.ts.map +1 -0
- package/switch/Switch.js +36 -0
- package/switch/Switch.js.map +1 -0
- package/switch/index.d.ts +3 -0
- package/switch/index.d.ts.map +1 -0
- package/switch/index.js +2 -0
- package/switch/index.js.map +1 -0
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
4
|
import type { WorkspaceWriteBack } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/writeback_pb";
|
|
4
5
|
import { WorkspaceWriteBackPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/writeback_pb";
|
|
5
6
|
import { cn } from "@stigmer/theme";
|
|
7
|
+
import { DiffSummary } from "../version-history/DiffSummary.js";
|
|
8
|
+
import {
|
|
9
|
+
parseDiffStatSummary,
|
|
10
|
+
trailingDiffStatLine,
|
|
11
|
+
writeBackDisplayName,
|
|
12
|
+
} from "./write-back-utils.js";
|
|
6
13
|
|
|
7
14
|
/** Props for {@link WriteBackCard}. */
|
|
8
15
|
export interface WriteBackCardProps {
|
|
@@ -13,11 +20,15 @@ export interface WriteBackCardProps {
|
|
|
13
20
|
}
|
|
14
21
|
|
|
15
22
|
/**
|
|
16
|
-
* Renders a single workspace write-back outcome as a
|
|
23
|
+
* Renders a single workspace write-back outcome as a dense, VS Code
|
|
24
|
+
* Source-Control-style row group: a header (workspace name + quiet phase
|
|
25
|
+
* caption), a pull-request row that opens the PR on the remote, a branch row
|
|
26
|
+
* with a hover-revealed copy control, and an aggregate change-stat line.
|
|
17
27
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
28
|
+
* Row geometry, hover, and focus treatment match {@link ArtifactRow}, so the
|
|
29
|
+
* session panel's Changes and Artifacts facets read as one surface. The PR
|
|
30
|
+
* link and the copy control are SIBLING interactive elements, never nested
|
|
31
|
+
* (axe `nested-interactive`, WCAG 4.1.2).
|
|
21
32
|
*
|
|
22
33
|
* Error rendering follows the record's phase honestly (DD-006):
|
|
23
34
|
* a FAILED record's error is destructive (the work did not reach the
|
|
@@ -25,8 +36,12 @@ export interface WriteBackCardProps {
|
|
|
25
36
|
* degraded notice — the branch is live and its info stays usable; only
|
|
26
37
|
* the PR step failed.
|
|
27
38
|
*
|
|
39
|
+
* The stat line is parsed from the record's `git diff --stat` summary and
|
|
40
|
+
* rendered through the shared {@link DiffSummary} component; unparseable
|
|
41
|
+
* text degrades to the raw trailing line rather than showing wrong numbers.
|
|
42
|
+
*
|
|
28
43
|
* Themed via standard semantic tokens — no hardcoded colors, no
|
|
29
|
-
* Console dependencies. Embedders can theme this
|
|
44
|
+
* Console dependencies. Embedders can theme this component through
|
|
30
45
|
* their Tailwind/CSS configuration.
|
|
31
46
|
*
|
|
32
47
|
* @example
|
|
@@ -42,53 +57,49 @@ export interface WriteBackCardProps {
|
|
|
42
57
|
* ```
|
|
43
58
|
*
|
|
44
59
|
* @see useWorkspaceWriteBacks — extracts write-back data from an execution
|
|
60
|
+
* @see writeBackDisplayName — the header's entry-name fallback chain
|
|
45
61
|
*/
|
|
46
62
|
export function WriteBackCard({ writeBack, className }: WriteBackCardProps) {
|
|
47
63
|
const isFailed =
|
|
48
64
|
writeBack.phase === WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_FAILED;
|
|
49
65
|
const hasPR = !!writeBack.pullRequestUrl;
|
|
66
|
+
const displayName = writeBackDisplayName(writeBack);
|
|
50
67
|
|
|
51
68
|
return (
|
|
52
69
|
<div
|
|
53
70
|
role="article"
|
|
54
|
-
aria-label={`Write-back: ${
|
|
55
|
-
className={cn(
|
|
56
|
-
"rounded-md border p-3",
|
|
57
|
-
isFailed ? "border-destructive/40" : "border-border",
|
|
58
|
-
className,
|
|
59
|
-
)}
|
|
71
|
+
aria-label={`Write-back: ${displayName}`}
|
|
72
|
+
className={cn("flex flex-col", className)}
|
|
60
73
|
>
|
|
61
|
-
{/* Header:
|
|
62
|
-
<div className="flex items-
|
|
63
|
-
<span
|
|
64
|
-
|
|
74
|
+
{/* Header: workspace name + quiet phase caption */}
|
|
75
|
+
<div className="flex items-center gap-2 px-2 py-1">
|
|
76
|
+
<span
|
|
77
|
+
className="min-w-0 flex-1 truncate text-xs font-medium text-foreground"
|
|
78
|
+
title={displayName}
|
|
79
|
+
>
|
|
80
|
+
{displayName}
|
|
65
81
|
</span>
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
</span>
|
|
71
|
-
<PhaseBadge phase={writeBack.phase} />
|
|
72
|
-
</div>
|
|
73
|
-
{writeBack.branchName && (
|
|
74
|
-
<div className="mt-0.5 truncate font-mono text-xs text-muted-foreground">
|
|
75
|
-
{writeBack.branchName}
|
|
76
|
-
{writeBack.baseBranch && (
|
|
77
|
-
<span className="text-muted-foreground-subtle">
|
|
78
|
-
{" \u2190 "}
|
|
79
|
-
{writeBack.baseBranch}
|
|
80
|
-
</span>
|
|
81
|
-
)}
|
|
82
|
-
</div>
|
|
83
|
-
)}
|
|
84
|
-
</div>
|
|
82
|
+
<PhaseCaption
|
|
83
|
+
phase={writeBack.phase}
|
|
84
|
+
pullRequestNumber={writeBack.pullRequestNumber}
|
|
85
|
+
/>
|
|
85
86
|
</div>
|
|
86
87
|
|
|
87
|
-
{/*
|
|
88
|
-
{
|
|
89
|
-
<
|
|
90
|
-
{writeBack.
|
|
91
|
-
|
|
88
|
+
{/* Pull-request row — the primary action when the PR exists */}
|
|
89
|
+
{hasPR && (
|
|
90
|
+
<PullRequestRow
|
|
91
|
+
url={writeBack.pullRequestUrl}
|
|
92
|
+
number={writeBack.pullRequestNumber}
|
|
93
|
+
/>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
{/* Branch row — primary when no PR was created; the branch is still
|
|
97
|
+
the user's handle on the pushed work */}
|
|
98
|
+
{writeBack.branchName && (
|
|
99
|
+
<BranchRow
|
|
100
|
+
branchName={writeBack.branchName}
|
|
101
|
+
baseBranch={writeBack.baseBranch}
|
|
102
|
+
/>
|
|
92
103
|
)}
|
|
93
104
|
|
|
94
105
|
{/* Error message: destructive when the write-back itself failed,
|
|
@@ -96,7 +107,7 @@ export function WriteBackCard({ writeBack, className }: WriteBackCardProps) {
|
|
|
96
107
|
{writeBack.error && (
|
|
97
108
|
<div
|
|
98
109
|
className={cn(
|
|
99
|
-
"
|
|
110
|
+
"mx-2 my-1 rounded px-2 py-1 text-xs",
|
|
100
111
|
isFailed
|
|
101
112
|
? "bg-destructive-subtle text-destructive"
|
|
102
113
|
: "bg-status-degraded-subtle text-status-degraded",
|
|
@@ -106,78 +117,202 @@ export function WriteBackCard({ writeBack, className }: WriteBackCardProps) {
|
|
|
106
117
|
</div>
|
|
107
118
|
)}
|
|
108
119
|
|
|
109
|
-
{/*
|
|
110
|
-
{
|
|
111
|
-
<div className="mt-2">
|
|
112
|
-
<a
|
|
113
|
-
href={writeBack.pullRequestUrl}
|
|
114
|
-
target="_blank"
|
|
115
|
-
rel="noopener noreferrer"
|
|
116
|
-
className={cn(
|
|
117
|
-
"inline-flex items-center gap-1 text-xs font-medium text-primary transition-colors hover:text-primary-muted",
|
|
118
|
-
FOCUS_RING_CLASSES,
|
|
119
|
-
)}
|
|
120
|
-
>
|
|
121
|
-
<ExternalLinkIcon />
|
|
122
|
-
View PR #{writeBack.pullRequestNumber}
|
|
123
|
-
</a>
|
|
124
|
-
</div>
|
|
125
|
-
)}
|
|
120
|
+
{/* Aggregate change stats */}
|
|
121
|
+
{writeBack.diffSummary && <StatLine diffSummary={writeBack.diffSummary} />}
|
|
126
122
|
</div>
|
|
127
123
|
);
|
|
128
124
|
}
|
|
129
125
|
|
|
130
126
|
// ---------------------------------------------------------------------------
|
|
131
|
-
// Phase badge
|
|
127
|
+
// Phase caption — quiet header text, replacing the retired pill badge
|
|
132
128
|
// ---------------------------------------------------------------------------
|
|
133
129
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (!config) return null;
|
|
130
|
+
function PhaseCaption({
|
|
131
|
+
phase,
|
|
132
|
+
pullRequestNumber,
|
|
133
|
+
}: {
|
|
134
|
+
phase: WorkspaceWriteBackPhase;
|
|
135
|
+
pullRequestNumber: number;
|
|
136
|
+
}) {
|
|
137
|
+
let label: string;
|
|
138
|
+
switch (phase) {
|
|
139
|
+
case WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_COMMITTED:
|
|
140
|
+
label = "Committed";
|
|
141
|
+
break;
|
|
142
|
+
case WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_PUSHED:
|
|
143
|
+
label = "Pushed";
|
|
144
|
+
break;
|
|
145
|
+
case WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_PR_CREATED:
|
|
146
|
+
label = pullRequestNumber > 0 ? `PR #${pullRequestNumber}` : "PR created";
|
|
147
|
+
break;
|
|
148
|
+
case WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_FAILED:
|
|
149
|
+
label = "Failed";
|
|
150
|
+
break;
|
|
151
|
+
default:
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
159
154
|
|
|
160
155
|
return (
|
|
161
156
|
<span
|
|
162
157
|
className={cn(
|
|
163
|
-
"
|
|
164
|
-
|
|
158
|
+
"shrink-0 text-[10px] font-medium tabular-nums",
|
|
159
|
+
phase === WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_FAILED
|
|
160
|
+
? "text-destructive"
|
|
161
|
+
: "text-muted-foreground",
|
|
165
162
|
)}
|
|
166
163
|
>
|
|
167
|
-
{
|
|
164
|
+
{label}
|
|
168
165
|
</span>
|
|
169
166
|
);
|
|
170
167
|
}
|
|
171
168
|
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
// Pull-request row
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
function PullRequestRow({ url, number }: { url: string; number: number }) {
|
|
174
|
+
return (
|
|
175
|
+
<a
|
|
176
|
+
href={url}
|
|
177
|
+
target="_blank"
|
|
178
|
+
rel="noopener noreferrer"
|
|
179
|
+
className={cn(
|
|
180
|
+
"group flex items-center gap-2 px-2 py-1 text-xs text-muted-foreground transition-colors",
|
|
181
|
+
"hover:bg-muted hover:text-foreground",
|
|
182
|
+
FOCUS_RING_CLASSES,
|
|
183
|
+
)}
|
|
184
|
+
>
|
|
185
|
+
<span className="shrink-0">
|
|
186
|
+
<PullRequestIcon />
|
|
187
|
+
</span>
|
|
188
|
+
<span className="min-w-0 flex-1 truncate text-foreground">
|
|
189
|
+
{number > 0 ? `Pull Request #${number}` : "Pull Request"}
|
|
190
|
+
</span>
|
|
191
|
+
<span
|
|
192
|
+
className="shrink-0 opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100"
|
|
193
|
+
aria-hidden="true"
|
|
194
|
+
>
|
|
195
|
+
<ExternalLinkIcon />
|
|
196
|
+
</span>
|
|
197
|
+
</a>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// Branch row — info + sibling hover-revealed copy control
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
/** How long the copy control shows its "copied" confirmation. */
|
|
206
|
+
const COPIED_FEEDBACK_MS = 2000;
|
|
207
|
+
|
|
208
|
+
function BranchRow({
|
|
209
|
+
branchName,
|
|
210
|
+
baseBranch,
|
|
211
|
+
}: {
|
|
212
|
+
branchName: string;
|
|
213
|
+
baseBranch: string;
|
|
214
|
+
}) {
|
|
215
|
+
const [copied, setCopied] = useState(false);
|
|
216
|
+
|
|
217
|
+
// Clear the pending feedback timer on unmount so a resolved copy never
|
|
218
|
+
// sets state on an unmounted component (same guard as useArtifactCopy).
|
|
219
|
+
const copiedTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
return () => {
|
|
222
|
+
if (copiedTimer.current !== null) clearTimeout(copiedTimer.current);
|
|
223
|
+
};
|
|
224
|
+
}, []);
|
|
225
|
+
|
|
226
|
+
const handleCopy = useCallback(() => {
|
|
227
|
+
navigator.clipboard.writeText(branchName).then(() => {
|
|
228
|
+
setCopied(true);
|
|
229
|
+
if (copiedTimer.current !== null) clearTimeout(copiedTimer.current);
|
|
230
|
+
copiedTimer.current = setTimeout(
|
|
231
|
+
() => setCopied(false),
|
|
232
|
+
COPIED_FEEDBACK_MS,
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
}, [branchName]);
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<div className="group flex items-stretch">
|
|
239
|
+
<div
|
|
240
|
+
className="flex min-w-0 flex-1 items-center gap-2 px-2 py-1 text-xs"
|
|
241
|
+
title={baseBranch ? `${branchName} \u2190 ${baseBranch}` : branchName}
|
|
242
|
+
>
|
|
243
|
+
<span className="shrink-0 text-muted-foreground">
|
|
244
|
+
<GitBranchIcon />
|
|
245
|
+
</span>
|
|
246
|
+
<span className="min-w-0 truncate font-mono text-foreground">
|
|
247
|
+
{branchName}
|
|
248
|
+
</span>
|
|
249
|
+
{baseBranch && (
|
|
250
|
+
<span className="shrink-0 font-mono text-muted-foreground-faint">
|
|
251
|
+
{"\u2190 "}
|
|
252
|
+
{baseBranch}
|
|
253
|
+
</span>
|
|
254
|
+
)}
|
|
255
|
+
</div>
|
|
256
|
+
<button
|
|
257
|
+
type="button"
|
|
258
|
+
onClick={handleCopy}
|
|
259
|
+
aria-label={copied ? "Branch name copied" : `Copy branch name ${branchName}`}
|
|
260
|
+
title={copied ? "Copied" : "Copy branch name"}
|
|
261
|
+
className={cn(
|
|
262
|
+
"flex shrink-0 items-center px-2 text-muted-foreground transition-opacity",
|
|
263
|
+
copied
|
|
264
|
+
? "opacity-100"
|
|
265
|
+
: "opacity-0 group-hover:opacity-100 focus-visible:opacity-100",
|
|
266
|
+
"hover:text-foreground",
|
|
267
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring",
|
|
268
|
+
)}
|
|
269
|
+
>
|
|
270
|
+
{copied ? <CheckIcon /> : <CopyIcon />}
|
|
271
|
+
</button>
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
// Stat line — parsed --stat summary through the shared DiffSummary
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
function StatLine({ diffSummary }: { diffSummary: string }) {
|
|
281
|
+
const stats = parseDiffStatSummary(diffSummary);
|
|
282
|
+
|
|
283
|
+
if (stats) {
|
|
284
|
+
return (
|
|
285
|
+
<DiffSummary
|
|
286
|
+
fileCount={stats.filesChanged}
|
|
287
|
+
additions={stats.insertions}
|
|
288
|
+
deletions={stats.deletions}
|
|
289
|
+
className="px-2 py-1"
|
|
290
|
+
/>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Unparseable text (format drift): the raw trailing line is still honest;
|
|
295
|
+
// structured-looking wrong numbers would not be.
|
|
296
|
+
const raw = trailingDiffStatLine(diffSummary);
|
|
297
|
+
if (!raw) return null;
|
|
298
|
+
|
|
299
|
+
return (
|
|
300
|
+
<div className="px-2 py-1 font-mono text-xs text-muted-foreground">
|
|
301
|
+
{raw}
|
|
302
|
+
</div>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
172
306
|
// ---------------------------------------------------------------------------
|
|
173
307
|
// Shared style constants
|
|
174
308
|
// ---------------------------------------------------------------------------
|
|
175
309
|
|
|
176
310
|
const FOCUS_RING_CLASSES =
|
|
177
|
-
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-
|
|
311
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring";
|
|
178
312
|
|
|
179
313
|
// ---------------------------------------------------------------------------
|
|
180
|
-
// Inline SVG icons
|
|
314
|
+
// Inline SVG icons — monochrome, `currentColor`-tinted (DD-005; SDK
|
|
315
|
+
// independence — no icon-library dependency)
|
|
181
316
|
// ---------------------------------------------------------------------------
|
|
182
317
|
|
|
183
318
|
function GitBranchIcon() {
|
|
@@ -203,6 +338,29 @@ function GitBranchIcon() {
|
|
|
203
338
|
);
|
|
204
339
|
}
|
|
205
340
|
|
|
341
|
+
/** Git pull-request glyph: base branch line joined to the merge commit. */
|
|
342
|
+
function PullRequestIcon() {
|
|
343
|
+
return (
|
|
344
|
+
<svg
|
|
345
|
+
width="14"
|
|
346
|
+
height="14"
|
|
347
|
+
viewBox="0 0 14 14"
|
|
348
|
+
fill="none"
|
|
349
|
+
stroke="currentColor"
|
|
350
|
+
strokeWidth="1.5"
|
|
351
|
+
strokeLinecap="round"
|
|
352
|
+
strokeLinejoin="round"
|
|
353
|
+
aria-hidden="true"
|
|
354
|
+
>
|
|
355
|
+
<circle cx="3.5" cy="3.5" r="1.5" />
|
|
356
|
+
<circle cx="3.5" cy="10.5" r="1.5" />
|
|
357
|
+
<circle cx="10.5" cy="10.5" r="1.5" />
|
|
358
|
+
<path d="M3.5 5V9" />
|
|
359
|
+
<path d="M7 3.5H8.5C9.6 3.5 10.5 4.4 10.5 5.5V9" />
|
|
360
|
+
</svg>
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
206
364
|
function ExternalLinkIcon() {
|
|
207
365
|
return (
|
|
208
366
|
<svg
|
|
@@ -223,3 +381,42 @@ function ExternalLinkIcon() {
|
|
|
223
381
|
</svg>
|
|
224
382
|
);
|
|
225
383
|
}
|
|
384
|
+
|
|
385
|
+
function CopyIcon() {
|
|
386
|
+
return (
|
|
387
|
+
<svg
|
|
388
|
+
width="12"
|
|
389
|
+
height="12"
|
|
390
|
+
viewBox="0 0 12 12"
|
|
391
|
+
fill="none"
|
|
392
|
+
stroke="currentColor"
|
|
393
|
+
strokeWidth="1.5"
|
|
394
|
+
strokeLinecap="round"
|
|
395
|
+
strokeLinejoin="round"
|
|
396
|
+
className="shrink-0"
|
|
397
|
+
aria-hidden="true"
|
|
398
|
+
>
|
|
399
|
+
<rect x="4" y="4" width="6.5" height="6.5" rx="1" />
|
|
400
|
+
<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" />
|
|
401
|
+
</svg>
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function CheckIcon() {
|
|
406
|
+
return (
|
|
407
|
+
<svg
|
|
408
|
+
width="12"
|
|
409
|
+
height="12"
|
|
410
|
+
viewBox="0 0 12 12"
|
|
411
|
+
fill="none"
|
|
412
|
+
stroke="currentColor"
|
|
413
|
+
strokeWidth="1.5"
|
|
414
|
+
strokeLinecap="round"
|
|
415
|
+
strokeLinejoin="round"
|
|
416
|
+
className="shrink-0"
|
|
417
|
+
aria-hidden="true"
|
|
418
|
+
>
|
|
419
|
+
<path d="M2.5 6.5L5 9L9.5 3.5" />
|
|
420
|
+
</svg>
|
|
421
|
+
);
|
|
422
|
+
}
|
|
@@ -49,7 +49,7 @@ export interface WriteBacksWidgetProps {
|
|
|
49
49
|
* />
|
|
50
50
|
* ```
|
|
51
51
|
*
|
|
52
|
-
* @see {@link WriteBackCard} —
|
|
52
|
+
* @see {@link WriteBackCard} — dense row group per write-back
|
|
53
53
|
* @see {@link useSessionWriteBacks} — headless session-level write-back aggregation hook
|
|
54
54
|
* @see {@link useWorkspaceWriteBacks} — headless single-execution write-back extraction hook
|
|
55
55
|
*/
|
|
@@ -71,13 +71,13 @@ export function WriteBacksWidget({
|
|
|
71
71
|
</span>
|
|
72
72
|
</div>
|
|
73
73
|
|
|
74
|
-
<
|
|
74
|
+
<ul role="list" className="flex flex-col gap-3">
|
|
75
75
|
{writeBacks.map((entry) => (
|
|
76
|
-
<
|
|
76
|
+
<li key={entry.writeBack.workspaceEntryName}>
|
|
77
77
|
<WriteBackCard writeBack={entry.writeBack} />
|
|
78
|
-
</
|
|
78
|
+
</li>
|
|
79
79
|
))}
|
|
80
|
-
</
|
|
80
|
+
</ul>
|
|
81
81
|
</section>
|
|
82
82
|
);
|
|
83
83
|
}
|
|
@@ -117,6 +117,57 @@ describe("ToolCallItem disclosure", () => {
|
|
|
117
117
|
expect(isExpanded(container)).toBe(false);
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
+
// --- Interrupted rows (issue #207): settled, neutral, never live -----------
|
|
121
|
+
|
|
122
|
+
it("renders an interrupted row settled: collapsed, no spinner, neutral slash icon", () => {
|
|
123
|
+
// A tool call the platform settled to INTERRUPTED when its execution
|
|
124
|
+
// terminalized mid-call. Before #207 a zombie RUNNING row spun forever and
|
|
125
|
+
// force-opened; the settled row must read as finished-by-interruption.
|
|
126
|
+
const { container } = render(
|
|
127
|
+
<ToolCallItem
|
|
128
|
+
toolCall={makeToolCall({
|
|
129
|
+
id: "tc-int",
|
|
130
|
+
name: "delete_file",
|
|
131
|
+
args: { path: "/x.ts" },
|
|
132
|
+
status: ToolCallStatus.TOOL_CALL_INTERRUPTED,
|
|
133
|
+
})}
|
|
134
|
+
/>,
|
|
135
|
+
);
|
|
136
|
+
expect(container.querySelector(".animate-spin")).toBeNull();
|
|
137
|
+
expect(hasHeaderChevron(container)).toBe(true);
|
|
138
|
+
expect(isExpanded(container)).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("collapses a force-opened running row the moment it settles to interrupted", () => {
|
|
142
|
+
// Mirrors the running→completed settle: the server-authored interruption
|
|
143
|
+
// arrives via a status update and the row must release its auto-open.
|
|
144
|
+
const { container, rerender } = render(
|
|
145
|
+
<ToolCallItem
|
|
146
|
+
toolCall={makeToolCall({
|
|
147
|
+
id: "tc-int",
|
|
148
|
+
name: "delete_file",
|
|
149
|
+
args: { path: "/x.ts" },
|
|
150
|
+
status: ToolCallStatus.TOOL_CALL_RUNNING,
|
|
151
|
+
})}
|
|
152
|
+
/>,
|
|
153
|
+
);
|
|
154
|
+
expect(isExpanded(container)).toBe(true);
|
|
155
|
+
expect(container.querySelector(".animate-spin")).not.toBeNull();
|
|
156
|
+
|
|
157
|
+
rerender(
|
|
158
|
+
<ToolCallItem
|
|
159
|
+
toolCall={makeToolCall({
|
|
160
|
+
id: "tc-int",
|
|
161
|
+
name: "delete_file",
|
|
162
|
+
args: { path: "/x.ts" },
|
|
163
|
+
status: ToolCallStatus.TOOL_CALL_INTERRUPTED,
|
|
164
|
+
})}
|
|
165
|
+
/>,
|
|
166
|
+
);
|
|
167
|
+
expect(container.querySelector(".animate-spin")).toBeNull();
|
|
168
|
+
expect(isExpanded(container)).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
|
|
120
171
|
// --- Preview categories: no chevron, always-visible body ------------------
|
|
121
172
|
|
|
122
173
|
it("renders a settled edit as an always-visible diff with no header chevron", () => {
|
|
@@ -74,6 +74,28 @@ describe("ToolRunGroup", () => {
|
|
|
74
74
|
expect(chipExpanded(container)).toBe(false);
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
+
it("treats an interrupted call as terminal: the chip settles instead of pinning to pending", () => {
|
|
78
|
+
// A platform-settled INTERRUPTED call (issue #207) inside a folded run.
|
|
79
|
+
// Before the fix an unknown status kept allTerminal=false forever, pinning
|
|
80
|
+
// the whole chip to a live-looking pending state with a spinner-adjacent
|
|
81
|
+
// dot; a settled run must render settled.
|
|
82
|
+
const { container } = render(
|
|
83
|
+
<ToolRunGroup
|
|
84
|
+
category="read"
|
|
85
|
+
toolCalls={[
|
|
86
|
+
makeRead("r1", "/a.ts", ToolCallStatus.TOOL_CALL_COMPLETED),
|
|
87
|
+
makeRead("r2", "/b.ts", ToolCallStatus.TOOL_CALL_INTERRUPTED),
|
|
88
|
+
]}
|
|
89
|
+
/>,
|
|
90
|
+
);
|
|
91
|
+
expect(container.querySelector(".animate-spin")).toBeNull();
|
|
92
|
+
expect(chipExpanded(container)).toBe(false);
|
|
93
|
+
// The aggregate resolves to the terminal branch (completed chip in
|
|
94
|
+
// text-success), not the non-terminal pending fallback (muted dot).
|
|
95
|
+
expect(container.querySelector(".text-success")).not.toBeNull();
|
|
96
|
+
expect(screen.getByText("Read 2 files")).toBeTruthy();
|
|
97
|
+
});
|
|
98
|
+
|
|
77
99
|
it("auto-opens when a folded call is awaiting approval, so the gate is reachable", () => {
|
|
78
100
|
const gated = makeRead("r2", "/b.ts", ToolCallStatus.TOOL_CALL_WAITING_APPROVAL);
|
|
79
101
|
const approval: PendingApproval = create(PendingApprovalSchema, {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* WriteBackCard — phase-honest rendering of a workspace write-back record
|
|
2
|
+
* WriteBackCard — phase-honest rendering of a workspace write-back record
|
|
3
|
+
* as a dense VS Code-style row group.
|
|
4
|
+
*
|
|
3
5
|
* The high-stakes matrix is the error treatment: FAILED errors are
|
|
4
6
|
* destructive, while a PUSHED record carrying a PR error renders it as a
|
|
5
7
|
* degraded notice with the branch info intact (the branch IS live).
|
|
6
8
|
*/
|
|
7
|
-
import { describe, it, expect, afterEach } from "vitest";
|
|
8
|
-
import { render, screen, cleanup } from "@testing-library/react";
|
|
9
|
+
import { describe, it, expect, vi, afterEach } from "vitest";
|
|
10
|
+
import { render, screen, cleanup, fireEvent, waitFor } from "@testing-library/react";
|
|
9
11
|
import { create } from "@bufbuild/protobuf";
|
|
10
12
|
import {
|
|
11
13
|
WorkspaceWriteBackSchema,
|
|
@@ -19,7 +21,7 @@ function makeWriteBack(overrides: Record<string, unknown> = {}) {
|
|
|
19
21
|
branchName: "stigmer/ses-01test",
|
|
20
22
|
baseBranch: "main",
|
|
21
23
|
commitSha: "abc123",
|
|
22
|
-
diffSummary: " 2 files changed",
|
|
24
|
+
diffSummary: " 2 files changed, 10 insertions(+), 3 deletions(-)",
|
|
23
25
|
phase: WorkspaceWriteBackPhase.WORKSPACE_WRITE_BACK_PR_CREATED,
|
|
24
26
|
pullRequestUrl: "https://github.com/acme/api/pull/12",
|
|
25
27
|
pullRequestNumber: 12,
|
|
@@ -30,13 +32,64 @@ function makeWriteBack(overrides: Record<string, unknown> = {}) {
|
|
|
30
32
|
afterEach(() => cleanup());
|
|
31
33
|
|
|
32
34
|
describe("WriteBackCard", () => {
|
|
33
|
-
it("renders
|
|
35
|
+
it("renders the header name, PR row, branch row, and parsed stats for a PR_CREATED record", () => {
|
|
34
36
|
render(<WriteBackCard writeBack={makeWriteBack()} />);
|
|
35
|
-
|
|
36
|
-
expect(screen.getByText(/
|
|
37
|
-
|
|
37
|
+
|
|
38
|
+
expect(screen.getByText("acme/api")).toBeTruthy();
|
|
39
|
+
// Quiet phase caption in the header.
|
|
40
|
+
expect(screen.getByText("PR #12")).toBeTruthy();
|
|
41
|
+
|
|
42
|
+
const link = screen.getByRole("link", { name: /pull request #12/i });
|
|
38
43
|
expect(link.getAttribute("href")).toBe("https://github.com/acme/api/pull/12");
|
|
39
|
-
expect(
|
|
44
|
+
expect(link.getAttribute("target")).toBe("_blank");
|
|
45
|
+
expect(link.getAttribute("rel")).toContain("noopener");
|
|
46
|
+
|
|
47
|
+
expect(screen.getByText("stigmer/ses-01test")).toBeTruthy();
|
|
48
|
+
expect(screen.getByText(/main/)).toBeTruthy();
|
|
49
|
+
|
|
50
|
+
// The --stat summary line renders structured, not as raw mono text.
|
|
51
|
+
expect(screen.getByText("2 files changed")).toBeTruthy();
|
|
52
|
+
expect(screen.getByText("+10 additions")).toBeTruthy();
|
|
53
|
+
expect(screen.getByText("-3 deletions")).toBeTruthy();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("falls back to the raw trailing --stat line when the summary is unparseable", () => {
|
|
57
|
+
render(
|
|
58
|
+
<WriteBackCard
|
|
59
|
+
writeBack={makeWriteBack({ diffSummary: " something unexpected" })}
|
|
60
|
+
/>,
|
|
61
|
+
);
|
|
62
|
+
expect(screen.getByText("something unexpected")).toBeTruthy();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Single-entry sessions can write back under an empty entry name (the
|
|
66
|
+
// runner's resolveEntry convention) — the header must never be blank.
|
|
67
|
+
it("derives the header from the PR URL when the entry name is empty", () => {
|
|
68
|
+
render(
|
|
69
|
+
<WriteBackCard writeBack={makeWriteBack({ workspaceEntryName: "" })} />,
|
|
70
|
+
);
|
|
71
|
+
expect(screen.getByText("acme/api")).toBeTruthy();
|
|
72
|
+
expect(
|
|
73
|
+
screen.getByRole("article", { name: "Write-back: acme/api" }),
|
|
74
|
+
).toBeTruthy();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("copies the branch name with transient feedback", async () => {
|
|
78
|
+
const writeText = vi.fn().mockResolvedValue(undefined);
|
|
79
|
+
Object.defineProperty(navigator, "clipboard", {
|
|
80
|
+
value: { writeText },
|
|
81
|
+
configurable: true,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
render(<WriteBackCard writeBack={makeWriteBack()} />);
|
|
85
|
+
fireEvent.click(
|
|
86
|
+
screen.getByRole("button", { name: /copy branch name/i }),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(writeText).toHaveBeenCalledWith("stigmer/ses-01test");
|
|
90
|
+
await waitFor(() =>
|
|
91
|
+
expect(screen.getByRole("button", { name: /copied/i })).toBeTruthy(),
|
|
92
|
+
);
|
|
40
93
|
});
|
|
41
94
|
|
|
42
95
|
it("renders a FAILED record's error destructively", () => {
|
|
@@ -52,7 +105,8 @@ describe("WriteBackCard", () => {
|
|
|
52
105
|
);
|
|
53
106
|
const error = screen.getByText(/git push failed/);
|
|
54
107
|
expect(error.className).toContain("text-destructive");
|
|
55
|
-
|
|
108
|
+
const caption = screen.getByText("Failed");
|
|
109
|
+
expect(caption.className).toContain("text-destructive");
|
|
56
110
|
});
|
|
57
111
|
|
|
58
112
|
it("renders a PUSHED record's PR error as a degraded notice, branch info intact", () => {
|