1- import { memo , useState , useCallback , useRef , useEffect } from "react" ;
1+ import { memo , useState , useCallback , useRef , useEffect , useMemo } from "react" ;
22import { type TimestampFormat } from "../appSettings" ;
3- import { Badge } from "./ui/badge" ;
43import { Button } from "./ui/button" ;
54import { ScrollArea } from "./ui/scroll-area" ;
65import ChatMarkdown from "./ChatMarkdown" ;
@@ -15,7 +14,6 @@ import {
1514import { cn } from "~/lib/utils" ;
1615import type { ActivePlanState } from "../session-logic" ;
1716import type { LatestProposedPlanState } from "../session-logic" ;
18- import { formatTimestamp } from "../timestampFormat" ;
1917import {
2018 proposedPlanTitle ,
2119 buildProposedPlanMarkdownFilename ,
@@ -38,21 +36,21 @@ const PLAN_SIDEBAR_MAX_WIDTH = 800;
3836function 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+
6975function 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 }
0 commit comments