Skip to content

Commit 5acaa7f

Browse files
chore(code): unify step progress views into shared StepList component (#1852)
1 parent e4d856b commit 5acaa7f

6 files changed

Lines changed: 145 additions & 201 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import {
2+
CheckCircle,
3+
Circle,
4+
CircleNotch,
5+
XCircle,
6+
} from "@phosphor-icons/react";
7+
import { Box, Flex, Text } from "@radix-ui/themes";
8+
9+
export type StepStatus = "pending" | "in_progress" | "completed" | "failed";
10+
11+
export interface Step {
12+
key: string;
13+
label: string;
14+
status: StepStatus;
15+
detail?: string;
16+
}
17+
18+
interface StepIconProps {
19+
status: StepStatus;
20+
size?: number;
21+
}
22+
23+
export function StepIcon({ status, size = 14 }: StepIconProps) {
24+
switch (status) {
25+
case "in_progress":
26+
return <CircleNotch size={size} className="animate-spin text-blue-9" />;
27+
case "completed":
28+
return <CheckCircle size={size} weight="fill" className="text-green-9" />;
29+
case "failed":
30+
return <XCircle size={size} weight="fill" className="text-red-9" />;
31+
default:
32+
return <Circle size={size} className="text-gray-8" />;
33+
}
34+
}
35+
36+
interface StepRowProps {
37+
step: Step;
38+
size?: "1" | "2";
39+
}
40+
41+
function StepRow({ step, size = "2" }: StepRowProps) {
42+
return (
43+
<Flex direction="column" gap="0">
44+
<Flex align="center" gap="2">
45+
<StepIcon status={step.status} />
46+
<Text size={size} className="text-gray-12">
47+
{step.label}
48+
</Text>
49+
</Flex>
50+
{step.detail && (
51+
<Box pl="5">
52+
<Text size="1" className="text-gray-10">
53+
{step.detail}
54+
</Text>
55+
</Box>
56+
)}
57+
</Flex>
58+
);
59+
}
60+
61+
interface StepListProps {
62+
steps: Step[];
63+
/** Text size for step labels. Default "2". */
64+
size?: "1" | "2";
65+
/** Gap between step rows. Default "1". */
66+
gap?: "1" | "2" | "3";
67+
}
68+
69+
export function StepList({ steps, size = "2", gap = "1" }: StepListProps) {
70+
return (
71+
<Flex direction="column" gap={gap}>
72+
{steps.map((step) => (
73+
<StepRow key={step.key} step={step} size={size} />
74+
))}
75+
</Flex>
76+
);
77+
}

apps/code/src/renderer/features/git-interaction/components/CreatePrDialog.tsx

Lines changed: 35 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { StepList, type StepStatus } from "@components/ui/StepList";
12
import {
23
CommitAllToggle,
34
ErrorContainer,
@@ -11,111 +12,46 @@ import {
1112
formatFileCountLabel,
1213
} from "@features/git-interaction/utils/diffStats";
1314
import { buildCreatePrFlowErrorPrompt } from "@features/git-interaction/utils/errorPrompts";
14-
import {
15-
CheckCircle,
16-
Circle,
17-
GitPullRequest,
18-
XCircle,
19-
} from "@phosphor-icons/react";
15+
import { GitPullRequest } from "@phosphor-icons/react";
2016
import {
2117
Button,
2218
Checkbox,
2319
Dialog,
2420
Flex,
25-
Spinner,
2621
Text,
2722
TextArea,
2823
TextField,
2924
} from "@radix-ui/themes";
3025

3126
const ICON_SIZE = 14;
3227

28+
const STEP_ORDER: CreatePrStep[] = [
29+
"creating-branch",
30+
"committing",
31+
"pushing",
32+
"creating-pr",
33+
"complete",
34+
];
35+
36+
function resolveStepStatus(
37+
stepId: CreatePrStep,
38+
currentStep: CreatePrStep,
39+
failedStep: CreatePrStep | null | undefined,
40+
): StepStatus {
41+
const currentIndex = STEP_ORDER.indexOf(currentStep);
42+
const stepIndex = STEP_ORDER.indexOf(stepId);
43+
if (currentStep === "error" && stepId === failedStep) return "failed";
44+
if (currentStep === "complete" || stepIndex < currentIndex)
45+
return "completed";
46+
if (stepId === currentStep) return "in_progress";
47+
return "pending";
48+
}
49+
3350
interface StepDef {
3451
id: CreatePrStep;
3552
label: string;
3653
}
3754

38-
function StepIndicator({
39-
steps,
40-
currentStep,
41-
failedStep,
42-
}: {
43-
steps: StepDef[];
44-
currentStep: CreatePrStep;
45-
failedStep?: CreatePrStep | null;
46-
}) {
47-
const stepOrder: CreatePrStep[] = [
48-
"creating-branch",
49-
"committing",
50-
"pushing",
51-
"creating-pr",
52-
"complete",
53-
];
54-
55-
const currentIndex = stepOrder.indexOf(currentStep);
56-
const isError = currentStep === "error";
57-
58-
return (
59-
<Flex direction="column" gap="3">
60-
{steps.map((step) => {
61-
const stepIndex = stepOrder.indexOf(step.id);
62-
const isComplete =
63-
currentStep === "complete" || stepIndex < currentIndex;
64-
const isActive = step.id === currentStep;
65-
const isFailed = isError && step.id === failedStep;
66-
67-
let icon: React.ReactNode;
68-
if (isFailed) {
69-
icon = <XCircle size={16} weight="fill" color="var(--red-9)" />;
70-
} else if (isComplete) {
71-
icon = <CheckCircle size={16} weight="fill" color="var(--green-9)" />;
72-
} else if (isActive) {
73-
icon = <Spinner size="1" />;
74-
} else {
75-
icon = <Circle size={16} color="var(--gray-6)" />;
76-
}
77-
78-
return (
79-
<Flex
80-
key={step.id}
81-
align="center"
82-
gap="2"
83-
style={{
84-
transition: "opacity 150ms ease",
85-
opacity: isComplete ? 0.6 : 1,
86-
}}
87-
>
88-
<Flex
89-
style={{
90-
transition: "transform 200ms ease",
91-
transform: isActive ? "scale(1.15)" : "scale(1)",
92-
}}
93-
>
94-
{icon}
95-
</Flex>
96-
<Text
97-
size="2"
98-
weight={isActive ? "medium" : "regular"}
99-
style={{
100-
transition: "color 150ms ease",
101-
color: isFailed
102-
? "var(--red-11)"
103-
: isComplete
104-
? "var(--green-11)"
105-
: isActive
106-
? "var(--gray-12)"
107-
: "var(--gray-9)",
108-
}}
109-
>
110-
{step.label}
111-
</Text>
112-
</Flex>
113-
);
114-
})}
115-
</Flex>
116-
);
117-
}
118-
11955
export interface CreatePrDialogProps {
12056
open: boolean;
12157
onOpenChange: (open: boolean) => void;
@@ -318,10 +254,17 @@ export function CreatePrDialog({
318254

319255
{isExecuting && (
320256
<>
321-
<StepIndicator
322-
steps={steps}
323-
currentStep={step}
324-
failedStep={store.createPrFailedStep}
257+
<StepList
258+
steps={steps.map((s) => ({
259+
key: s.id,
260+
label: s.label,
261+
status: resolveStepStatus(
262+
s.id,
263+
step,
264+
store.createPrFailedStep,
265+
),
266+
}))}
267+
gap="3"
325268
/>
326269

327270
{step === "error" && store.createPrError && (
Lines changed: 13 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1+
import { StepIcon, StepList, type StepStatus } from "@components/ui/StepList";
12
import { CHAT_CONTENT_MAX_WIDTH } from "@features/sessions/constants";
23
import type { Plan } from "@features/sessions/types";
3-
import {
4-
CaretDown,
5-
CaretRight,
6-
CheckCircle,
7-
Circle,
8-
Spinner,
9-
XCircle,
10-
} from "@phosphor-icons/react";
4+
import { CaretDown, CaretRight } from "@phosphor-icons/react";
115
import { Box, Flex, Text } from "@radix-ui/themes";
126
import { useMemo, useState } from "react";
137

8+
function planEntriesToSteps(plan: Plan) {
9+
return plan.entries.map((entry) => ({
10+
key: entry.content,
11+
label: entry.content,
12+
status: entry.status as StepStatus,
13+
}));
14+
}
15+
1416
interface PlanStatusBarProps {
1517
plan: Plan | null;
1618
}
@@ -54,7 +56,7 @@ export function PlanStatusBar({ plan }: PlanStatusBarProps) {
5456
<Text size="1" color="gray">
5557
5658
</Text>
57-
<Spinner size={12} className="animate-spin text-blue-9" />
59+
<StepIcon status="in_progress" />
5860
<Text size="1" className="truncate text-gray-11">
5961
{stats.inProgress.content}
6062
</Text>
@@ -63,39 +65,11 @@ export function PlanStatusBar({ plan }: PlanStatusBarProps) {
6365
</Flex>
6466

6567
{isExpanded && plan && (
66-
<Box className="border-gray-4 border-t px-3 pb-2">
67-
<Flex direction="column" gap="1" className="pt-2">
68-
{plan.entries.map((entry) => (
69-
<Flex key={entry.content} align="center" gap="2">
70-
<StatusIcon status={entry.status} />
71-
<Text
72-
size="1"
73-
color={entry.status === "completed" ? "gray" : undefined}
74-
className={
75-
entry.status === "completed" ? "text-gray-9" : ""
76-
}
77-
>
78-
{entry.content}
79-
</Text>
80-
</Flex>
81-
))}
82-
</Flex>
68+
<Box className="border-gray-4 border-t px-3 pt-2 pb-2">
69+
<StepList steps={planEntriesToSteps(plan)} size="1" />
8370
</Box>
8471
)}
8572
</Box>
8673
</Box>
8774
);
8875
}
89-
90-
function StatusIcon({ status }: { status: string }) {
91-
switch (status) {
92-
case "completed":
93-
return <CheckCircle size={14} className="text-green-9" />;
94-
case "in_progress":
95-
return <Spinner size={14} className="animate-spin text-blue-9" />;
96-
case "failed":
97-
return <XCircle size={14} className="text-red-9" />;
98-
default:
99-
return <Circle size={14} className="text-gray-8" />;
100-
}
101-
}

apps/code/src/renderer/features/sessions/components/buildConversationItems.ts

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {
22
ContentBlock,
33
SessionNotification,
44
} from "@agentclientprotocol/sdk";
5+
import type { Step, StepStatus } from "@components/ui/StepList";
56
import type { QueuedMessage } from "@features/sessions/stores/sessionStore";
67
import type { SessionUpdate, ToolCall } from "@features/sessions/types";
78
import {
@@ -56,15 +57,6 @@ export type ConversationItem =
5657
| UserShellExecute
5758
| { type: "queued"; id: string; message: QueuedMessage };
5859

59-
export type ProgressStatus = "in_progress" | "completed" | "failed";
60-
61-
export interface ProgressStep {
62-
key: string;
63-
status: ProgressStatus;
64-
label: string;
65-
detail?: string;
66-
}
67-
6860
export interface LastTurnInfo {
6961
isComplete: boolean;
7062
durationMs: number;
@@ -79,11 +71,11 @@ export interface BuildResult {
7971

8072
interface ProgressCardState {
8173
/** Step key → full step entry. Key order reflects arrival order. */
82-
steps: Map<string, ProgressStep>;
74+
steps: Map<string, Step>;
8375
/** Reference to the pushed render item; mutated in place as events arrive. */
8476
renderItem: {
8577
sessionUpdate: "progress_group";
86-
steps: ProgressStep[];
78+
steps: Step[];
8779
isActive: boolean;
8880
};
8981
}
@@ -436,7 +428,7 @@ function ensureProgressCardForGroup(
436428

437429
const renderItem = {
438430
sessionUpdate: "progress_group" as const,
439-
steps: [] as ProgressStep[],
431+
steps: [] as Step[],
440432
isActive: true,
441433
};
442434
const card: ProgressCardState = {
@@ -449,7 +441,7 @@ function ensureProgressCardForGroup(
449441
}
450442

451443
function syncProgressCard(card: ProgressCardState) {
452-
const ordered: ProgressStep[] = Array.from(card.steps.values());
444+
const ordered: Step[] = Array.from(card.steps.values());
453445
card.renderItem.steps = ordered;
454446
card.renderItem.isActive = ordered.some((s) => s.status === "in_progress");
455447
}
@@ -466,7 +458,7 @@ function handleProgress(b: ItemBuilder, rawParams: unknown, ts: number) {
466458
| undefined;
467459
if (!params?.step || !params.label || !params.group) return;
468460

469-
const status = normalizeProgressStatus(params.status);
461+
const status = normalizeStepStatus(params.status);
470462
const card = ensureProgressCardForGroup(b, params.group, ts);
471463
if (!card) return;
472464
card.steps.set(params.step, {
@@ -478,7 +470,7 @@ function handleProgress(b: ItemBuilder, rawParams: unknown, ts: number) {
478470
syncProgressCard(card);
479471
}
480472

481-
function normalizeProgressStatus(raw: string | undefined): ProgressStatus {
473+
function normalizeStepStatus(raw: string | undefined): StepStatus {
482474
switch (raw) {
483475
case "in_progress":
484476
case "completed":

0 commit comments

Comments
 (0)