Open History to see the last commit
1import { RefreshCwIcon } from "lucide-react";
2import type { ReactNode } from "react";
3
4import { threadSyncLabel, type ThreadSyncPhase } from "../../threadSync";
5import { cn } from "~/lib/utils";
6import { NoticeText } from "./ComposerBannerStack";
7
8/** A line of thread state for the end of the timeline. */
9export interface ThreadNowItem {
10 readonly id: string;
11 readonly icon: ReactNode;
12 readonly title: ReactNode;
13 readonly description?: ReactNode;
14 readonly actions?: ReactNode;
15}
16
17/** Matches ThreadStatusLine, so every line at the end of the timeline reads as one family. */
18const lineClassName = cn(
19 "mx-auto flex w-full min-w-0 max-w-(--chat-content-max-width) items-center gap-1.5 px-1 pb-2 text-xs text-muted-foreground [&_svg]:size-3.5 [&_svg]:shrink-0",
20 // A details button must not make its line taller than the others.
21 "[&_[data-slot=popover-trigger]]:-my-1.5",
22 // Inline actions keep their text size but get a finger-sized hit area on touch.
23 "pointer-coarse:[&_[data-slot=inline-button]]:relative pointer-coarse:[&_[data-slot=inline-button]]:after:absolute pointer-coarse:[&_[data-slot=inline-button]]:after:-inset-x-1.5 pointer-coarse:[&_[data-slot=inline-button]]:after:-inset-y-3.5",
24 "transition-opacity duration-150 ease-out starting:opacity-0 motion-reduce:transition-none",
25);
26
27/** One line of thread state at the end of the timeline: icon, title, detail, then actions. */
28function ThreadNowLine({
29 item,
30 announce = false,
31}: {
32 readonly item: ThreadNowItem;
33 /** Announces the line when it appears; only for something that needs the user. */
34 readonly announce?: boolean;
35}) {
36 return (
37 <div
38 role={announce ? "alert" : undefined}
39 className={cn(lineClassName, "items-baseline")}
40 data-thread-now-line="true"
41 >
42 <span aria-hidden className="flex size-3.5 shrink-0 self-center items-center justify-center">
43 {item.icon}
44 </span>
45 {/* Title and detail read as one line that gives way before the actions, so Stop stays
46 reachable; when it is cut, its details button shows all of it. */}
47 <NoticeText
48 detailsLabel={
49 typeof item.title === "string" ? `Show details: ${item.title}` : "Show details"
50 }
51 className="min-w-0 truncate"
52 >
53 {item.title}
54 {item.description ? (
55 <>
56 {" "}
57 <span className="ms-0.5 text-muted-foreground/60 light:text-muted-foreground/80">
58 {item.description}
59 </span>
60 </>
61 ) : null}
62 </NoticeText>
63 {item.actions ? (
64 <span className="ms-1.5 flex shrink-0 items-baseline gap-2.5">{item.actions}</span>
65 ) : null}
66 </div>
67 );
68}
69
70function ThreadSyncLine({ phase }: { readonly phase: ThreadSyncPhase }) {
71 return (
72 <div className={lineClassName} role="status" data-composer-sync-status={phase}>
73 <RefreshCwIcon aria-hidden />
74 <span className="truncate">{threadSyncLabel(phase)}</span>
75 </div>
76 );
77}
78
79/**
80 * What the thread is doing now, at the end of the timeline: what needs you, background work,
81 * the sync status while messages load, then the goal.
82 */
83export function ThreadNow({
84 attention,
85 work,
86 syncPhase,
87 goal,
88}: {
89 /** Something that needs you elsewhere, such as a subagent's question. */
90 readonly attention: ThreadNowItem | null;
91 readonly work: ThreadNowItem | null;
92 readonly syncPhase: ThreadSyncPhase | null;
93 readonly goal: ThreadNowItem | null;
94}): ReactNode {
95 return (
96 <>
97 {attention ? <ThreadNowLine item={attention} announce /> : null}
98 {work ? <ThreadNowLine item={work} /> : null}
99 {syncPhase ? <ThreadSyncLine phase={syncPhase} /> : null}
100 {goal ? <ThreadNowLine item={goal} /> : null}
101 </>
102 );
103}