Skip to content

Commit 37675c6

Browse files
authored
Redesign plan sidebar and follow-up banner (#69)
- Surface plan title and execution progress in the sidebar - Simplify step presentation and auto-expand plans without active steps - Add clearer composer guidance for implementing or refining the plan
1 parent cda6aca commit 37675c6

2 files changed

Lines changed: 88 additions & 74 deletions

File tree

apps/web/src/components/PlanSidebar.tsx

Lines changed: 85 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { memo, useState, useCallback, useRef, useEffect } from "react";
1+
import { memo, useState, useCallback, useRef, useEffect, useMemo } from "react";
22
import { type TimestampFormat } from "../appSettings";
3-
import { Badge } from "./ui/badge";
43
import { Button } from "./ui/button";
54
import { ScrollArea } from "./ui/scroll-area";
65
import ChatMarkdown from "./ChatMarkdown";
@@ -15,7 +14,6 @@ import {
1514
import { cn } from "~/lib/utils";
1615
import type { ActivePlanState } from "../session-logic";
1716
import type { LatestProposedPlanState } from "../session-logic";
18-
import { formatTimestamp } from "../timestampFormat";
1917
import {
2018
proposedPlanTitle,
2119
buildProposedPlanMarkdownFilename,
@@ -38,21 +36,21 @@ const PLAN_SIDEBAR_MAX_WIDTH = 800;
3836
function stepStatusIcon(status: string): React.ReactNode {
3937
if (status === "completed") {
4038
return (
41-
<span className="flex size-5 shrink-0 items-center justify-center rounded-full bg-emerald-500/15 text-emerald-500">
39+
<span className="flex size-4 shrink-0 items-center justify-center text-emerald-500">
4240
<CheckIcon className="size-3" />
4341
</span>
4442
);
4543
}
4644
if (status === "inProgress") {
4745
return (
48-
<span className="flex size-5 shrink-0 items-center justify-center rounded-full bg-blue-500/15 text-blue-400">
46+
<span className="flex size-4 shrink-0 items-center justify-center text-blue-400">
4947
<LoaderIcon className="size-3 animate-spin" />
5048
</span>
5149
);
5250
}
5351
return (
54-
<span className="flex size-5 shrink-0 items-center justify-center rounded-full border border-border/60 bg-muted/30">
55-
<span className="size-1.5 rounded-full bg-muted-foreground/30" />
52+
<span className="flex size-4 shrink-0 items-center justify-center">
53+
<span className="size-1.5 rounded-full bg-muted-foreground/25" />
5654
</span>
5755
);
5856
}
@@ -66,6 +64,14 @@ interface PlanSidebarProps {
6664
onClose: () => void;
6765
}
6866

67+
function usePlanProgress(steps: ActivePlanState["steps"] | undefined) {
68+
return useMemo(() => {
69+
if (!steps || steps.length === 0) return null;
70+
const completed = steps.filter((s) => s.status === "completed").length;
71+
return { completed, total: steps.length };
72+
}, [steps]);
73+
}
74+
6975
function clampWidth(width: number): number {
7076
return Math.max(PLAN_SIDEBAR_MIN_WIDTH, Math.min(width, PLAN_SIDEBAR_MAX_WIDTH));
7177
}
@@ -166,18 +172,26 @@ const PlanSidebar = memo(function PlanSidebar({
166172
activeProposedPlan,
167173
markdownCwd,
168174
workspaceRoot,
169-
timestampFormat,
170175
onClose,
171176
}: PlanSidebarProps) {
172-
const [proposedPlanExpanded, setProposedPlanExpanded] = useState(false);
177+
const hasActiveSteps = (activePlan?.steps.length ?? 0) > 0;
178+
const [proposedPlanExpanded, setProposedPlanExpanded] = useState(!hasActiveSteps);
173179
const [isSavingToWorkspace, setIsSavingToWorkspace] = useState(false);
174180
const { copyToClipboard, isCopied } = useCopyToClipboard();
175181
const { width, railProps } = useResizablePlanSidebar();
182+
const progress = usePlanProgress(activePlan?.steps);
176183

177184
const planMarkdown = activeProposedPlan?.planMarkdown ?? null;
178185
const displayedPlanMarkdown = planMarkdown ? stripDisplayedPlanMarkdown(planMarkdown) : null;
179186
const planTitle = planMarkdown ? proposedPlanTitle(planMarkdown) : null;
180187

188+
// Auto-expand the full plan when there are no active execution steps
189+
useEffect(() => {
190+
if (!hasActiveSteps && planMarkdown) {
191+
setProposedPlanExpanded(true);
192+
}
193+
}, [hasActiveSteps, planMarkdown]);
194+
181195
const handleCopyPlan = useCallback(() => {
182196
if (!planMarkdown) return;
183197
copyToClipboard(planMarkdown);
@@ -234,59 +248,66 @@ const PlanSidebar = memo(function PlanSidebar({
234248
{...railProps}
235249
/>
236250
{/* Header */}
237-
<div className="flex h-12 shrink-0 items-center justify-between border-b border-border/60 px-3">
238-
<div className="flex items-center gap-2">
239-
<Badge
240-
variant="secondary"
241-
className="rounded-md bg-blue-500/10 px-1.5 py-0 text-[10px] font-semibold tracking-wide text-blue-400 uppercase"
242-
>
243-
Plan
244-
</Badge>
245-
{activePlan ? (
246-
<span className="text-[11px] text-muted-foreground/60">
247-
{formatTimestamp(activePlan.createdAt, timestampFormat)}
248-
</span>
249-
) : null}
250-
</div>
251-
<div className="flex items-center gap-1">
252-
{planMarkdown ? (
253-
<Menu>
254-
<MenuTrigger
255-
render={
256-
<Button
257-
size="icon-xs"
258-
variant="ghost"
259-
className="text-muted-foreground/50 hover:text-foreground/70"
260-
aria-label="Plan actions"
261-
/>
262-
}
263-
>
264-
<EllipsisIcon className="size-3.5" />
265-
</MenuTrigger>
266-
<MenuPopup align="end">
267-
<MenuItem onClick={handleCopyPlan}>
268-
{isCopied ? "Copied!" : "Copy to clipboard"}
269-
</MenuItem>
270-
<MenuItem onClick={handleDownload}>Download as markdown</MenuItem>
271-
<MenuItem
272-
onClick={handleSaveToWorkspace}
273-
disabled={!workspaceRoot || isSavingToWorkspace}
251+
<div className="flex shrink-0 flex-col border-b border-border/60 px-3">
252+
<div className="flex h-12 items-center justify-between">
253+
<p className="min-w-0 flex-1 truncate text-sm font-medium text-foreground/90">
254+
{planTitle ?? "Plan"}
255+
</p>
256+
<div className="flex shrink-0 items-center gap-1">
257+
{planMarkdown ? (
258+
<Menu>
259+
<MenuTrigger
260+
render={
261+
<Button
262+
size="icon-xs"
263+
variant="ghost"
264+
className="text-muted-foreground/50 hover:text-foreground/70"
265+
aria-label="Plan actions"
266+
/>
267+
}
274268
>
275-
Save to workspace
276-
</MenuItem>
277-
</MenuPopup>
278-
</Menu>
279-
) : null}
280-
<Button
281-
size="icon-xs"
282-
variant="ghost"
283-
onClick={onClose}
284-
aria-label="Close plan sidebar"
285-
className="text-muted-foreground/50 hover:text-foreground/70"
286-
>
287-
<PanelRightCloseIcon className="size-3.5" />
288-
</Button>
269+
<EllipsisIcon className="size-3.5" />
270+
</MenuTrigger>
271+
<MenuPopup align="end">
272+
<MenuItem onClick={handleCopyPlan}>
273+
{isCopied ? "Copied!" : "Copy to clipboard"}
274+
</MenuItem>
275+
<MenuItem onClick={handleDownload}>Download as markdown</MenuItem>
276+
<MenuItem
277+
onClick={handleSaveToWorkspace}
278+
disabled={!workspaceRoot || isSavingToWorkspace}
279+
>
280+
Save to workspace
281+
</MenuItem>
282+
</MenuPopup>
283+
</Menu>
284+
) : null}
285+
<Button
286+
size="icon-xs"
287+
variant="ghost"
288+
onClick={onClose}
289+
aria-label="Close plan sidebar"
290+
className="text-muted-foreground/50 hover:text-foreground/70"
291+
>
292+
<PanelRightCloseIcon className="size-3.5" />
293+
</Button>
294+
</div>
289295
</div>
296+
{progress ? (
297+
<div className="flex items-center gap-2.5 pb-2.5">
298+
<div className="h-1 min-w-0 flex-1 overflow-hidden rounded-full bg-muted/50">
299+
<div
300+
className="h-full rounded-full bg-emerald-500/70 transition-all duration-500 ease-out"
301+
style={{
302+
width: `${Math.round((progress.completed / progress.total) * 100)}%`,
303+
}}
304+
/>
305+
</div>
306+
<span className="shrink-0 text-[11px] tabular-nums text-muted-foreground/50">
307+
{progress.completed}/{progress.total}
308+
</span>
309+
</div>
310+
) : null}
290311
</div>
291312

292313
{/* Content */}
@@ -301,28 +322,21 @@ const PlanSidebar = memo(function PlanSidebar({
301322

302323
{/* Plan Steps */}
303324
{activePlan && activePlan.steps.length > 0 ? (
304-
<div className="space-y-1">
305-
<p className="mb-2 text-[10px] font-semibold tracking-widest text-muted-foreground/40 uppercase">
306-
Steps
307-
</p>
325+
<div className="space-y-0.5">
308326
{activePlan.steps.map((step) => (
309327
<div
310328
key={`${step.status}:${step.step}`}
311-
className={cn(
312-
"flex items-start gap-2.5 rounded-lg px-2.5 py-2 transition-colors duration-200",
313-
step.status === "inProgress" && "bg-blue-500/5",
314-
step.status === "completed" && "bg-emerald-500/5",
315-
)}
329+
className="flex items-start gap-2 px-1 py-1.5"
316330
>
317331
<div className="mt-0.5">{stepStatusIcon(step.status)}</div>
318332
<p
319333
className={cn(
320334
"text-[13px] leading-snug",
321335
step.status === "completed"
322-
? "text-muted-foreground/50 line-through decoration-muted-foreground/20"
336+
? "text-muted-foreground/40 line-through decoration-muted-foreground/20"
323337
: step.status === "inProgress"
324338
? "text-foreground/90"
325-
: "text-muted-foreground/70",
339+
: "text-muted-foreground/60",
326340
)}
327341
>
328342
{step.step}

apps/web/src/components/chat/ComposerPlanFollowUpBanner.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ export const ComposerPlanFollowUpBanner = memo(function ComposerPlanFollowUpBann
1313
<span className="min-w-0 flex-1 truncate text-sm font-medium">{planTitle}</span>
1414
) : null}
1515
</div>
16-
{/* <div className="mt-2 text-xs text-muted-foreground">
17-
Review the plan
18-
</div> */}
16+
<p className="mt-1.5 text-xs text-muted-foreground/60">
17+
Press Enter to implement &middot; or type to refine
18+
</p>
1919
</div>
2020
);
2121
});

0 commit comments

Comments
 (0)