newpr 0.6.6 → 1.0.1
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/package.json +1 -1
- package/src/history/store.ts +25 -0
- package/src/stack/balance.ts +128 -0
- package/src/stack/coupling.test.ts +158 -0
- package/src/stack/coupling.ts +135 -0
- package/src/stack/delta.test.ts +223 -0
- package/src/stack/delta.ts +264 -0
- package/src/stack/execute.test.ts +176 -0
- package/src/stack/execute.ts +194 -0
- package/src/stack/feasibility.test.ts +185 -0
- package/src/stack/feasibility.ts +286 -0
- package/src/stack/integration.test.ts +266 -0
- package/src/stack/merge-groups.test.ts +97 -0
- package/src/stack/merge-groups.ts +87 -0
- package/src/stack/partition.test.ts +233 -0
- package/src/stack/partition.ts +273 -0
- package/src/stack/plan.test.ts +154 -0
- package/src/stack/plan.ts +139 -0
- package/src/stack/pr-title.ts +90 -0
- package/src/stack/publish.ts +96 -0
- package/src/stack/split.ts +173 -0
- package/src/stack/types.ts +202 -0
- package/src/stack/verify.test.ts +137 -0
- package/src/stack/verify.ts +201 -0
- package/src/web/client/components/FeasibilityAlert.tsx +64 -0
- package/src/web/client/components/ResultsScreen.tsx +10 -2
- package/src/web/client/components/StackGroupCard.tsx +171 -0
- package/src/web/client/components/StackWarnings.tsx +135 -0
- package/src/web/client/hooks/useStack.ts +301 -0
- package/src/web/client/panels/StackPanel.tsx +289 -0
- package/src/web/server/routes.ts +114 -0
- package/src/web/server/stack-manager.ts +580 -0
- package/src/web/server.ts +15 -0
- package/src/web/styles/built.css +1 -1
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect, useRef } from "react";
|
|
2
|
+
import type { FeasibilityResult, StackWarning, StackGroupStats } from "../../../stack/types.ts";
|
|
3
|
+
|
|
4
|
+
type StackPhase = "idle" | "partitioning" | "planning" | "executing" | "publishing" | "done" | "error";
|
|
5
|
+
|
|
6
|
+
interface GroupData {
|
|
7
|
+
name: string;
|
|
8
|
+
type: string;
|
|
9
|
+
description: string;
|
|
10
|
+
files: string[];
|
|
11
|
+
key_changes?: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface PartitionData {
|
|
15
|
+
ownership: Record<string, string>;
|
|
16
|
+
reattributed: Array<{ path: string; from_groups: string[]; to_group: string; reason: string }>;
|
|
17
|
+
warnings: string[];
|
|
18
|
+
structured_warnings: StackWarning[];
|
|
19
|
+
forced_merges: Array<{ path: string; from_group: string; to_group: string }>;
|
|
20
|
+
groups?: GroupData[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface StackContext {
|
|
24
|
+
repo_path: string;
|
|
25
|
+
base_sha: string;
|
|
26
|
+
head_sha: string;
|
|
27
|
+
base_branch: string;
|
|
28
|
+
head_branch: string;
|
|
29
|
+
pr_number: number;
|
|
30
|
+
owner: string;
|
|
31
|
+
repo: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface PlanData {
|
|
35
|
+
base_sha: string;
|
|
36
|
+
head_sha: string;
|
|
37
|
+
groups: Array<{
|
|
38
|
+
id: string;
|
|
39
|
+
name: string;
|
|
40
|
+
type: string;
|
|
41
|
+
description: string;
|
|
42
|
+
files: string[];
|
|
43
|
+
deps: string[];
|
|
44
|
+
order: number;
|
|
45
|
+
stats?: StackGroupStats;
|
|
46
|
+
pr_title?: string;
|
|
47
|
+
}>;
|
|
48
|
+
expected_trees: Record<string, string>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface ExecResultData {
|
|
52
|
+
run_id: string;
|
|
53
|
+
source_copy_branch: string;
|
|
54
|
+
group_commits: Array<{
|
|
55
|
+
group_id: string;
|
|
56
|
+
commit_sha: string;
|
|
57
|
+
tree_sha: string;
|
|
58
|
+
branch_name: string;
|
|
59
|
+
pr_title?: string;
|
|
60
|
+
}>;
|
|
61
|
+
final_tree_sha: string;
|
|
62
|
+
verified: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface VerifyResultData {
|
|
66
|
+
verified: boolean;
|
|
67
|
+
errors: string[];
|
|
68
|
+
warnings: string[];
|
|
69
|
+
structured_warnings: StackWarning[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface PublishResultData {
|
|
73
|
+
branches: Array<{ name: string; pushed: boolean }>;
|
|
74
|
+
prs: Array<{
|
|
75
|
+
group_id: string;
|
|
76
|
+
number: number;
|
|
77
|
+
url: string;
|
|
78
|
+
title: string;
|
|
79
|
+
base_branch: string;
|
|
80
|
+
head_branch: string;
|
|
81
|
+
}>;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface ServerStackState {
|
|
85
|
+
status: string;
|
|
86
|
+
phase: string | null;
|
|
87
|
+
error: string | null;
|
|
88
|
+
maxGroups: number | null;
|
|
89
|
+
context: StackContext | null;
|
|
90
|
+
partition: PartitionData | null;
|
|
91
|
+
feasibility: FeasibilityResult | null;
|
|
92
|
+
plan: PlanData | null;
|
|
93
|
+
execResult: ExecResultData | null;
|
|
94
|
+
verifyResult: VerifyResultData | null;
|
|
95
|
+
startedAt: number;
|
|
96
|
+
finishedAt: number | null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface StackState {
|
|
100
|
+
phase: StackPhase;
|
|
101
|
+
error: string | null;
|
|
102
|
+
maxGroups: number | null;
|
|
103
|
+
partition: PartitionData | null;
|
|
104
|
+
feasibility: FeasibilityResult | null;
|
|
105
|
+
context: StackContext | null;
|
|
106
|
+
plan: PlanData | null;
|
|
107
|
+
execResult: ExecResultData | null;
|
|
108
|
+
verifyResult: VerifyResultData | null;
|
|
109
|
+
publishResult: PublishResultData | null;
|
|
110
|
+
progressMessage: string | null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function serverPhaseToClient(status: string, phase: string | null): StackPhase {
|
|
114
|
+
if (status === "error" || status === "canceled") return "error";
|
|
115
|
+
if (status === "done") return "done";
|
|
116
|
+
if (phase === "partitioning") return "partitioning";
|
|
117
|
+
if (phase === "planning") return "planning";
|
|
118
|
+
if (phase === "executing") return "executing";
|
|
119
|
+
if (phase === "done") return "done";
|
|
120
|
+
return "idle";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function applyServerState(server: ServerStackState): Partial<StackState> {
|
|
124
|
+
return {
|
|
125
|
+
phase: serverPhaseToClient(server.status, server.phase),
|
|
126
|
+
error: server.error,
|
|
127
|
+
context: server.context,
|
|
128
|
+
partition: server.partition,
|
|
129
|
+
feasibility: server.feasibility,
|
|
130
|
+
plan: server.plan,
|
|
131
|
+
execResult: server.execResult,
|
|
132
|
+
verifyResult: server.verifyResult,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function useStack(sessionId: string | null | undefined) {
|
|
137
|
+
const [state, setState] = useState<StackState>({
|
|
138
|
+
phase: "idle",
|
|
139
|
+
error: null,
|
|
140
|
+
maxGroups: null,
|
|
141
|
+
partition: null,
|
|
142
|
+
feasibility: null,
|
|
143
|
+
context: null,
|
|
144
|
+
plan: null,
|
|
145
|
+
execResult: null,
|
|
146
|
+
verifyResult: null,
|
|
147
|
+
publishResult: null,
|
|
148
|
+
progressMessage: null,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const eventSourceRef = useRef<EventSource | null>(null);
|
|
152
|
+
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (!sessionId) return;
|
|
155
|
+
|
|
156
|
+
fetch(`/api/stack/${sessionId}`)
|
|
157
|
+
.then((res) => res.json())
|
|
158
|
+
.then((data: { state: ServerStackState | null }) => {
|
|
159
|
+
if (!data.state) return;
|
|
160
|
+
setState((s) => ({ ...s, ...applyServerState(data.state!) }));
|
|
161
|
+
})
|
|
162
|
+
.catch(() => {});
|
|
163
|
+
}, [sessionId]);
|
|
164
|
+
|
|
165
|
+
const setMaxGroups = useCallback((n: number | null) => {
|
|
166
|
+
setState((s) => ({ ...s, maxGroups: n }));
|
|
167
|
+
}, []);
|
|
168
|
+
|
|
169
|
+
const connectSSE = useCallback((id: string) => {
|
|
170
|
+
eventSourceRef.current?.close();
|
|
171
|
+
|
|
172
|
+
const es = new EventSource(`/api/stack/${id}/events`);
|
|
173
|
+
eventSourceRef.current = es;
|
|
174
|
+
|
|
175
|
+
es.addEventListener("progress", (e) => {
|
|
176
|
+
try {
|
|
177
|
+
const data = JSON.parse(e.data) as { phase: string; message: string; state: ServerStackState };
|
|
178
|
+
setState((s) => ({
|
|
179
|
+
...s,
|
|
180
|
+
...applyServerState(data.state),
|
|
181
|
+
progressMessage: data.message,
|
|
182
|
+
}));
|
|
183
|
+
} catch {}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
es.addEventListener("done", (e) => {
|
|
187
|
+
try {
|
|
188
|
+
const data = JSON.parse(e.data) as { state: ServerStackState };
|
|
189
|
+
setState((s) => ({
|
|
190
|
+
...s,
|
|
191
|
+
...applyServerState(data.state),
|
|
192
|
+
progressMessage: null,
|
|
193
|
+
}));
|
|
194
|
+
} catch {}
|
|
195
|
+
es.close();
|
|
196
|
+
eventSourceRef.current = null;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
es.addEventListener("stack_error", (e) => {
|
|
200
|
+
try {
|
|
201
|
+
const data = JSON.parse(e.data) as { message: string; state?: ServerStackState };
|
|
202
|
+
setState((s) => ({
|
|
203
|
+
...s,
|
|
204
|
+
phase: "error",
|
|
205
|
+
error: data.message,
|
|
206
|
+
...(data.state ? applyServerState(data.state) : {}),
|
|
207
|
+
progressMessage: null,
|
|
208
|
+
}));
|
|
209
|
+
} catch {}
|
|
210
|
+
es.close();
|
|
211
|
+
eventSourceRef.current = null;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
es.onerror = () => {
|
|
215
|
+
es.close();
|
|
216
|
+
eventSourceRef.current = null;
|
|
217
|
+
};
|
|
218
|
+
}, []);
|
|
219
|
+
|
|
220
|
+
const runFullPipeline = useCallback(async () => {
|
|
221
|
+
if (!sessionId) return;
|
|
222
|
+
|
|
223
|
+
setState((s) => ({ ...s, phase: "partitioning", error: null, progressMessage: "Starting..." }));
|
|
224
|
+
try {
|
|
225
|
+
const res = await fetch("/api/stack/start", {
|
|
226
|
+
method: "POST",
|
|
227
|
+
headers: { "Content-Type": "application/json" },
|
|
228
|
+
body: JSON.stringify({ sessionId, maxGroups: state.maxGroups }),
|
|
229
|
+
});
|
|
230
|
+
const data = await res.json();
|
|
231
|
+
if (!res.ok) throw new Error(data.error ?? "Failed to start stack pipeline");
|
|
232
|
+
|
|
233
|
+
connectSSE(sessionId);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
setState((s) => ({
|
|
236
|
+
...s,
|
|
237
|
+
phase: "error",
|
|
238
|
+
error: err instanceof Error ? err.message : String(err),
|
|
239
|
+
progressMessage: null,
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
}, [sessionId, state.maxGroups, connectSSE]);
|
|
243
|
+
|
|
244
|
+
const startPublish = useCallback(async () => {
|
|
245
|
+
if (!sessionId) return;
|
|
246
|
+
setState((s) => ({ ...s, phase: "publishing" }));
|
|
247
|
+
try {
|
|
248
|
+
const res = await fetch("/api/stack/publish", {
|
|
249
|
+
method: "POST",
|
|
250
|
+
headers: { "Content-Type": "application/json" },
|
|
251
|
+
body: JSON.stringify({ sessionId }),
|
|
252
|
+
});
|
|
253
|
+
const data = await res.json();
|
|
254
|
+
if (!res.ok) throw new Error(data.error ?? "Publishing failed");
|
|
255
|
+
|
|
256
|
+
setState((s) => ({
|
|
257
|
+
...s,
|
|
258
|
+
phase: "done",
|
|
259
|
+
publishResult: data.publish_result,
|
|
260
|
+
}));
|
|
261
|
+
} catch (err) {
|
|
262
|
+
setState((s) => ({
|
|
263
|
+
...s,
|
|
264
|
+
phase: "error",
|
|
265
|
+
error: err instanceof Error ? err.message : String(err),
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
}, [sessionId]);
|
|
269
|
+
|
|
270
|
+
const reset = useCallback(() => {
|
|
271
|
+
eventSourceRef.current?.close();
|
|
272
|
+
eventSourceRef.current = null;
|
|
273
|
+
setState((s) => ({
|
|
274
|
+
phase: "idle",
|
|
275
|
+
error: null,
|
|
276
|
+
maxGroups: s.maxGroups,
|
|
277
|
+
partition: null,
|
|
278
|
+
feasibility: null,
|
|
279
|
+
context: null,
|
|
280
|
+
plan: null,
|
|
281
|
+
execResult: null,
|
|
282
|
+
verifyResult: null,
|
|
283
|
+
publishResult: null,
|
|
284
|
+
progressMessage: null,
|
|
285
|
+
}));
|
|
286
|
+
}, []);
|
|
287
|
+
|
|
288
|
+
useEffect(() => {
|
|
289
|
+
return () => {
|
|
290
|
+
eventSourceRef.current?.close();
|
|
291
|
+
};
|
|
292
|
+
}, []);
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
...state,
|
|
296
|
+
setMaxGroups,
|
|
297
|
+
runFullPipeline,
|
|
298
|
+
startPublish,
|
|
299
|
+
reset,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { Loader2, Play, Upload, RotateCcw, CheckCircle2, AlertTriangle, Circle, GitPullRequestArrow, ArrowRight, Layers } from "lucide-react";
|
|
2
|
+
import { useStack } from "../hooks/useStack.ts";
|
|
3
|
+
import { FeasibilityAlert } from "../components/FeasibilityAlert.tsx";
|
|
4
|
+
import { StackGroupCard } from "../components/StackGroupCard.tsx";
|
|
5
|
+
import { StackWarnings } from "../components/StackWarnings.tsx";
|
|
6
|
+
|
|
7
|
+
type StackPhase = "idle" | "partitioning" | "planning" | "executing" | "publishing" | "done" | "error";
|
|
8
|
+
|
|
9
|
+
const PIPELINE_STEPS = [
|
|
10
|
+
{ phase: "partitioning" as const, label: "Partition", description: "Assigning files to groups" },
|
|
11
|
+
{ phase: "planning" as const, label: "Plan", description: "Building stack plan" },
|
|
12
|
+
{ phase: "executing" as const, label: "Execute", description: "Creating commits" },
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
function getStepState(stepPhase: string, currentPhase: StackPhase, isDone: boolean) {
|
|
16
|
+
const order = ["partitioning", "planning", "executing"];
|
|
17
|
+
const stepIdx = order.indexOf(stepPhase);
|
|
18
|
+
const currentIdx = order.indexOf(currentPhase);
|
|
19
|
+
|
|
20
|
+
if (isDone || (currentIdx > stepIdx)) return "done";
|
|
21
|
+
if (currentPhase === stepPhase) return "active";
|
|
22
|
+
return "pending";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function PipelineTimeline({ phase }: { phase: StackPhase }) {
|
|
26
|
+
const isDone = phase === "done" || phase === "publishing";
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="relative flex flex-col gap-0 py-1">
|
|
30
|
+
{PIPELINE_STEPS.map((step, i) => {
|
|
31
|
+
const state = getStepState(step.phase, phase, isDone);
|
|
32
|
+
const isLast = i === PIPELINE_STEPS.length - 1;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div key={step.phase} className="relative flex items-start gap-3">
|
|
36
|
+
{!isLast && (
|
|
37
|
+
<div className={`absolute left-[9px] top-[20px] w-px h-[calc(100%-8px)] ${
|
|
38
|
+
state === "done" ? "bg-foreground/20" : "bg-border"
|
|
39
|
+
}`} />
|
|
40
|
+
)}
|
|
41
|
+
|
|
42
|
+
<div className="relative z-10 mt-0.5 shrink-0">
|
|
43
|
+
{state === "done" ? (
|
|
44
|
+
<CheckCircle2 className="h-[18px] w-[18px] text-foreground/70" />
|
|
45
|
+
) : state === "active" ? (
|
|
46
|
+
<Loader2 className="h-[18px] w-[18px] text-foreground animate-spin" />
|
|
47
|
+
) : (
|
|
48
|
+
<Circle className="h-[18px] w-[18px] text-muted-foreground/20" />
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div className={`pb-4 min-w-0 ${isLast ? "pb-0" : ""}`}>
|
|
53
|
+
<span className={`text-[12px] font-medium ${
|
|
54
|
+
state === "done" ? "text-muted-foreground"
|
|
55
|
+
: state === "active" ? "text-foreground"
|
|
56
|
+
: "text-muted-foreground/40"
|
|
57
|
+
}`}>
|
|
58
|
+
{step.label}
|
|
59
|
+
</span>
|
|
60
|
+
{state === "active" && (
|
|
61
|
+
<p className="text-[11px] text-muted-foreground/50 mt-0.5">{step.description}</p>
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
})}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function StackPanel({ sessionId }: { sessionId?: string | null }) {
|
|
72
|
+
const stack = useStack(sessionId);
|
|
73
|
+
|
|
74
|
+
if (stack.phase === "idle") {
|
|
75
|
+
return (
|
|
76
|
+
<div className="pt-6 px-1">
|
|
77
|
+
<div className="flex items-center gap-2.5 mb-5">
|
|
78
|
+
<div className="h-8 w-8 rounded-lg bg-foreground/[0.04] flex items-center justify-center">
|
|
79
|
+
<Layers className="h-4 w-4 text-foreground/50" />
|
|
80
|
+
</div>
|
|
81
|
+
<div>
|
|
82
|
+
<h3 className="text-[13px] font-semibold text-foreground">PR Stacking</h3>
|
|
83
|
+
<p className="text-[11px] text-muted-foreground/50">Split into focused, reviewable PRs</p>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<p className="text-[11px] text-muted-foreground/40 leading-[1.6] mb-5">
|
|
88
|
+
Automatically split this PR into a stack of smaller draft PRs based on the analysis groups. Each group becomes its own PR with proper dependency ordering.
|
|
89
|
+
</p>
|
|
90
|
+
|
|
91
|
+
<div className="flex items-center gap-3 mb-5">
|
|
92
|
+
<span className="text-[11px] text-muted-foreground/50">Max PRs</span>
|
|
93
|
+
<div className="flex items-center">
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
onClick={() => stack.setMaxGroups(Math.max(1, (stack.maxGroups ?? 2) - 1))}
|
|
97
|
+
className="h-7 w-7 rounded-l-md border border-r-0 bg-transparent text-[11px] text-muted-foreground/50 hover:bg-accent/30 transition-colors flex items-center justify-center"
|
|
98
|
+
>
|
|
99
|
+
−
|
|
100
|
+
</button>
|
|
101
|
+
<input
|
|
102
|
+
type="number"
|
|
103
|
+
min={1}
|
|
104
|
+
placeholder="auto"
|
|
105
|
+
value={stack.maxGroups ?? ""}
|
|
106
|
+
onChange={(e) => stack.setMaxGroups(e.target.value ? Number(e.target.value) : null)}
|
|
107
|
+
className="h-7 w-12 border-y bg-transparent text-[11px] text-center tabular-nums placeholder:text-muted-foreground/25 focus:outline-none"
|
|
108
|
+
/>
|
|
109
|
+
<button
|
|
110
|
+
type="button"
|
|
111
|
+
onClick={() => stack.setMaxGroups((stack.maxGroups ?? 2) + 1)}
|
|
112
|
+
className="h-7 w-7 rounded-r-md border border-l-0 bg-transparent text-[11px] text-muted-foreground/50 hover:bg-accent/30 transition-colors flex items-center justify-center"
|
|
113
|
+
>
|
|
114
|
+
+
|
|
115
|
+
</button>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
onClick={stack.runFullPipeline}
|
|
122
|
+
className="w-full flex items-center justify-center gap-2 rounded-lg bg-foreground text-background px-4 py-2.5 text-[12px] font-medium hover:bg-foreground/90 transition-colors"
|
|
123
|
+
>
|
|
124
|
+
<Play className="h-3.5 w-3.5" />
|
|
125
|
+
Start Stacking
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const isRunning = ["partitioning", "planning", "executing", "publishing"].includes(stack.phase);
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<div className="pt-6 px-1 space-y-5">
|
|
135
|
+
<div className="flex items-center gap-2">
|
|
136
|
+
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
137
|
+
<Layers className="h-3.5 w-3.5 text-muted-foreground/40 shrink-0" />
|
|
138
|
+
<span className="text-[12px] font-semibold text-foreground">PR Stacking</span>
|
|
139
|
+
{stack.phase === "done" && !stack.publishResult && (
|
|
140
|
+
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-foreground/[0.06] text-muted-foreground/60">Ready</span>
|
|
141
|
+
)}
|
|
142
|
+
{stack.publishResult && (
|
|
143
|
+
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-green-500/10 text-green-600 dark:text-green-400">Published</span>
|
|
144
|
+
)}
|
|
145
|
+
</div>
|
|
146
|
+
{stack.phase === "done" && (
|
|
147
|
+
<button
|
|
148
|
+
type="button"
|
|
149
|
+
onClick={stack.reset}
|
|
150
|
+
className="flex items-center gap-1.5 text-[10px] text-muted-foreground/30 hover:text-muted-foreground transition-colors"
|
|
151
|
+
>
|
|
152
|
+
<RotateCcw className="h-3 w-3" />
|
|
153
|
+
</button>
|
|
154
|
+
)}
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{isRunning && (
|
|
158
|
+
<>
|
|
159
|
+
<PipelineTimeline phase={stack.phase} />
|
|
160
|
+
{stack.progressMessage && (
|
|
161
|
+
<p className="text-[10px] text-muted-foreground/30 px-1 -mt-2">{stack.progressMessage}</p>
|
|
162
|
+
)}
|
|
163
|
+
</>
|
|
164
|
+
)}
|
|
165
|
+
|
|
166
|
+
{stack.phase === "error" && (
|
|
167
|
+
<div className="rounded-lg bg-red-500/[0.04] px-3.5 py-3 space-y-3">
|
|
168
|
+
<div className="flex items-start gap-2.5">
|
|
169
|
+
<AlertTriangle className="h-3.5 w-3.5 text-red-500/70 shrink-0 mt-px" />
|
|
170
|
+
<span className="text-[11px] text-red-600/80 dark:text-red-400/80 break-all leading-relaxed">
|
|
171
|
+
{stack.error}
|
|
172
|
+
</span>
|
|
173
|
+
</div>
|
|
174
|
+
<button
|
|
175
|
+
type="button"
|
|
176
|
+
onClick={stack.reset}
|
|
177
|
+
className="flex items-center gap-1.5 text-[11px] text-muted-foreground/40 hover:text-foreground transition-colors"
|
|
178
|
+
>
|
|
179
|
+
<RotateCcw className="h-3 w-3" />
|
|
180
|
+
Try again
|
|
181
|
+
</button>
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
|
|
185
|
+
{stack.feasibility && (
|
|
186
|
+
<FeasibilityAlert result={stack.feasibility} />
|
|
187
|
+
)}
|
|
188
|
+
|
|
189
|
+
{stack.partition && stack.partition.structured_warnings.length > 0 && (
|
|
190
|
+
<StackWarnings warnings={stack.partition.structured_warnings} />
|
|
191
|
+
)}
|
|
192
|
+
|
|
193
|
+
{stack.plan && (
|
|
194
|
+
<div className="space-y-1">
|
|
195
|
+
<div className="flex items-center justify-between mb-2">
|
|
196
|
+
<span className="text-[11px] font-medium text-muted-foreground/40">
|
|
197
|
+
{stack.plan.groups.length} PRs
|
|
198
|
+
</span>
|
|
199
|
+
{stack.plan.groups.length > 0 && (
|
|
200
|
+
<span className="text-[10px] text-muted-foreground/25 tabular-nums">
|
|
201
|
+
{stack.plan.groups.reduce((sum, g) => sum + g.files.length, 0)} files total
|
|
202
|
+
</span>
|
|
203
|
+
)}
|
|
204
|
+
</div>
|
|
205
|
+
<div className="space-y-0">
|
|
206
|
+
{stack.plan.groups.map((group) => {
|
|
207
|
+
const commit = stack.execResult?.group_commits.find((gc) => gc.group_id === group.id);
|
|
208
|
+
const pr = stack.publishResult?.prs.find((p) => p.group_id === group.id);
|
|
209
|
+
return (
|
|
210
|
+
<StackGroupCard
|
|
211
|
+
key={group.id}
|
|
212
|
+
group={group}
|
|
213
|
+
commit={commit}
|
|
214
|
+
pr={pr}
|
|
215
|
+
/>
|
|
216
|
+
);
|
|
217
|
+
})}
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
)}
|
|
221
|
+
|
|
222
|
+
{stack.verifyResult && (
|
|
223
|
+
<div className="space-y-2">
|
|
224
|
+
<div className={`flex items-center gap-2.5 rounded-lg px-3.5 py-2.5 ${
|
|
225
|
+
stack.verifyResult.verified
|
|
226
|
+
? "bg-green-500/[0.04]"
|
|
227
|
+
: "bg-red-500/[0.04]"
|
|
228
|
+
}`}>
|
|
229
|
+
{stack.verifyResult.verified
|
|
230
|
+
? <CheckCircle2 className="h-3.5 w-3.5 text-green-600/70 dark:text-green-400/70 shrink-0" />
|
|
231
|
+
: <AlertTriangle className="h-3.5 w-3.5 text-red-500/70 shrink-0" />
|
|
232
|
+
}
|
|
233
|
+
<span className={`text-[11px] ${
|
|
234
|
+
stack.verifyResult.verified
|
|
235
|
+
? "text-green-700/70 dark:text-green-300/70"
|
|
236
|
+
: "text-red-600/80 dark:text-red-400/80"
|
|
237
|
+
}`}>
|
|
238
|
+
{stack.verifyResult.verified
|
|
239
|
+
? "Tree equivalence verified"
|
|
240
|
+
: `Verification failed: ${stack.verifyResult.errors.join(", ")}`
|
|
241
|
+
}
|
|
242
|
+
</span>
|
|
243
|
+
</div>
|
|
244
|
+
{stack.verifyResult.structured_warnings.length > 0 && (
|
|
245
|
+
<StackWarnings warnings={stack.verifyResult.structured_warnings} defaultCollapsed={stack.verifyResult.verified} />
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
248
|
+
)}
|
|
249
|
+
|
|
250
|
+
{stack.phase === "done" && stack.execResult && !stack.publishResult && (
|
|
251
|
+
<button
|
|
252
|
+
type="button"
|
|
253
|
+
onClick={stack.startPublish}
|
|
254
|
+
className="w-full flex items-center justify-center gap-2 rounded-lg bg-foreground text-background px-4 py-2.5 text-[12px] font-medium hover:bg-foreground/90 transition-colors"
|
|
255
|
+
>
|
|
256
|
+
<Upload className="h-3.5 w-3.5" />
|
|
257
|
+
Publish as Draft PRs
|
|
258
|
+
</button>
|
|
259
|
+
)}
|
|
260
|
+
|
|
261
|
+
{stack.publishResult && stack.publishResult.prs.length > 0 && (
|
|
262
|
+
<div className="space-y-1.5">
|
|
263
|
+
{stack.publishResult.prs.map((pr) => (
|
|
264
|
+
<a
|
|
265
|
+
key={pr.number}
|
|
266
|
+
href={pr.url}
|
|
267
|
+
target="_blank"
|
|
268
|
+
rel="noopener noreferrer"
|
|
269
|
+
className="group flex items-center gap-3 rounded-lg px-3.5 py-2.5 hover:bg-accent/30 transition-colors"
|
|
270
|
+
>
|
|
271
|
+
<GitPullRequestArrow className="h-3.5 w-3.5 text-green-600/60 dark:text-green-400/60 shrink-0" />
|
|
272
|
+
<div className="flex-1 min-w-0">
|
|
273
|
+
<div className="flex items-center gap-2">
|
|
274
|
+
<span className="text-[11px] font-medium truncate">{pr.title}</span>
|
|
275
|
+
<span className="text-[10px] text-muted-foreground/25 tabular-nums shrink-0">#{pr.number}</span>
|
|
276
|
+
</div>
|
|
277
|
+
<div className="flex items-center gap-1 mt-0.5">
|
|
278
|
+
<span className="text-[10px] font-mono text-muted-foreground/25">{pr.base_branch}</span>
|
|
279
|
+
<ArrowRight className="h-2.5 w-2.5 text-muted-foreground/20" />
|
|
280
|
+
<span className="text-[10px] font-mono text-muted-foreground/25">{pr.head_branch}</span>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
</a>
|
|
284
|
+
))}
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
</div>
|
|
288
|
+
);
|
|
289
|
+
}
|