@@ -35,6 +35,7 @@ import {
3535 ZapIcon ,
3636} from "lucide-react" ;
3737import { Button } from "../ui/button" ;
38+ import { Badge } from "../ui/badge" ;
3839import { clamp } from "effect/Number" ;
3940import { estimateTimelineMessageHeight } from "../timelineHeight" ;
4041import { buildExpandedImagePreview , ExpandedImagePreview } from "./ExpandedImagePreview" ;
@@ -43,6 +44,7 @@ import { ChangedFilesTree } from "./ChangedFilesTree";
4344import { DiffStatLabel , hasNonZeroStat } from "./DiffStatLabel" ;
4445import { MessageCopyButton } from "./MessageCopyButton" ;
4546import { computeMessageDurationStart , normalizeCompactToolLabel } from "./MessagesTimeline.logic" ;
47+ import type { ChatShortcutGuide } from "~/lib/chatShortcutGuidance" ;
4648import { TerminalContextInlineChip } from "./TerminalContextInlineChip" ;
4749import {
4850 deriveDisplayedUserMessageState ,
@@ -82,6 +84,8 @@ interface MessagesTimelineProps {
8284 resolvedTheme : "light" | "dark" ;
8385 timestampFormat : TimestampFormat ;
8486 workspaceRoot : string | undefined ;
87+ shortcutGuides : ChatShortcutGuide [ ] ;
88+ onOpenSettings : ( ) => void ;
8589}
8690
8791export const MessagesTimeline = memo ( function MessagesTimeline ( {
@@ -106,6 +110,8 @@ export const MessagesTimeline = memo(function MessagesTimeline({
106110 resolvedTheme,
107111 timestampFormat,
108112 workspaceRoot,
113+ shortcutGuides,
114+ onOpenSettings,
109115} : MessagesTimelineProps ) {
110116 const timelineRootRef = useRef < HTMLDivElement | null > ( null ) ;
111117 const [ timelineWidthPx , setTimelineWidthPx ] = useState < number | null > ( null ) ;
@@ -600,11 +606,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({
600606
601607 if ( ! hasMessages && ! isWorking ) {
602608 return (
603- < div className = "flex h-full items-center justify-center" >
604- < p className = "text-sm text-muted-foreground/30" >
605- Send a message to start the conversation.
606- </ p >
607- </ div >
609+ < EmptyTimelineGuidance shortcutGuides = { shortcutGuides } onOpenSettings = { onOpenSettings } />
608610 ) ;
609611 }
610612
@@ -642,6 +644,87 @@ export const MessagesTimeline = memo(function MessagesTimeline({
642644 ) ;
643645} ) ;
644646
647+ function EmptyTimelineGuidance ( {
648+ shortcutGuides,
649+ onOpenSettings,
650+ } : {
651+ shortcutGuides : ChatShortcutGuide [ ] ;
652+ onOpenSettings : ( ) => void ;
653+ } ) {
654+ const [ guideIndex , setGuideIndex ] = useState ( 0 ) ;
655+ const guideCount = shortcutGuides . length ;
656+ const currentGuide = guideCount > 0 ? shortcutGuides [ guideIndex % guideCount ] : undefined ;
657+
658+ useEffect ( ( ) => {
659+ setGuideIndex ( 0 ) ;
660+ } , [ shortcutGuides ] ) ;
661+
662+ useEffect ( ( ) => {
663+ if ( shortcutGuides . length <= 1 ) return ;
664+
665+ const interval = window . setInterval ( ( ) => {
666+ setGuideIndex ( ( currentIndex ) => ( currentIndex + 1 ) % shortcutGuides . length ) ;
667+ } , 12_000 ) ;
668+
669+ return ( ) => {
670+ window . clearInterval ( interval ) ;
671+ } ;
672+ } , [ shortcutGuides . length ] ) ;
673+
674+ return (
675+ < div className = "flex h-full items-center justify-center px-4 py-10 sm:px-6" >
676+ < div className = "mx-auto flex w-full max-w-2xl flex-col items-center text-center" >
677+ < p className = "text-[11px] font-semibold uppercase tracking-[0.22em] text-muted-foreground/55" >
678+ Hotkey tip
679+ </ p >
680+ < div className = "mt-4 space-y-4" >
681+ < div className = "space-y-2" >
682+ < h3 className = "text-2xl font-medium tracking-tight text-foreground sm:text-3xl" >
683+ { currentGuide ?. title ?? "Start with a shortcut" }
684+ </ h3 >
685+ < p className = "mx-auto max-w-xl text-sm leading-6 text-muted-foreground sm:text-[15px]" >
686+ { currentGuide ?. description ??
687+ "A few useful bindings will appear here while the thread is empty." }
688+ </ p >
689+ </ div >
690+
691+ < div className = "flex flex-wrap justify-center gap-2" >
692+ { currentGuide ?. shortcutLabels . length ? (
693+ currentGuide . shortcutLabels . map ( ( label ) => (
694+ < Badge
695+ key = { `${ currentGuide . id } :${ label } ` }
696+ variant = "outline"
697+ size = "sm"
698+ className = "rounded-full border-border/70 bg-background/70 px-2.5 text-foreground"
699+ >
700+ { label }
701+ </ Badge >
702+ ) )
703+ ) : (
704+ < Badge
705+ variant = "outline"
706+ size = "sm"
707+ className = "rounded-full border-border/70 bg-background/70 px-2.5 text-foreground"
708+ >
709+ No shortcut assigned
710+ </ Badge >
711+ ) }
712+ </ div >
713+
714+ < div className = "space-y-3" >
715+ < p className = "text-xs leading-5 text-muted-foreground/70" >
716+ Edit shortcuts from Settings whenever you want to change the defaults.
717+ </ p >
718+ < Button type = "button" variant = "outline" size = "sm" onClick = { onOpenSettings } >
719+ Manage hotkeys
720+ </ Button >
721+ </ div >
722+ </ div >
723+ </ div >
724+ </ div >
725+ ) ;
726+ }
727+
645728type TimelineEntry = ReturnType < typeof deriveTimelineEntries > [ number ] ;
646729type TimelineMessage = Extract < TimelineEntry , { kind : "message" } > [ "message" ] ;
647730type TimelineProposedPlan = Extract < TimelineEntry , { kind : "proposed-plan" } > [ "proposedPlan" ] ;
0 commit comments