Skip to content

Commit d8d3296

Browse files
authored
Show thread status in command palette (#2107)
1 parent 54179c8 commit d8d3296

5 files changed

Lines changed: 281 additions & 120 deletions

File tree

apps/web/src/components/CommandPalette.logic.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ export interface CommandPaletteItem {
1717
readonly description?: string;
1818
readonly timestamp?: string;
1919
readonly icon: ReactNode;
20+
/** Optional content rendered inline before the title text. */
21+
readonly titleLeadingContent?: ReactNode;
22+
/** Optional content rendered inline after the title text (before the timestamp). */
23+
readonly titleTrailingContent?: ReactNode;
2024
readonly shortcutCommand?: KeybindingCommand;
2125
}
2226

@@ -102,20 +106,24 @@ export function buildProjectActionItems(input: {
102106
}));
103107
}
104108

105-
export function buildThreadActionItems(input: {
106-
threads: ReadonlyArray<
107-
Pick<
108-
SidebarThreadSummary,
109-
"archivedAt" | "branch" | "createdAt" | "environmentId" | "id" | "projectId" | "title"
110-
> & {
111-
updatedAt?: string | undefined;
112-
latestUserMessageAt?: string | null;
113-
}
114-
>;
109+
export type BuildThreadActionItemsThread = Pick<
110+
SidebarThreadSummary,
111+
"archivedAt" | "branch" | "createdAt" | "environmentId" | "id" | "projectId" | "title"
112+
> & {
113+
updatedAt?: string | undefined;
114+
latestUserMessageAt?: string | null;
115+
};
116+
117+
export function buildThreadActionItems<TThread extends BuildThreadActionItemsThread>(input: {
118+
threads: ReadonlyArray<TThread>;
115119
activeThreadId?: Thread["id"];
116120
projectTitleById: ReadonlyMap<Project["id"], string>;
117121
sortOrder: SidebarThreadSortOrder;
118122
icon: ReactNode;
123+
/** Optional content rendered inline before the title text per-thread. */
124+
renderLeadingContent?: (thread: TThread) => ReactNode;
125+
/** Optional content rendered inline after the title text per-thread. */
126+
renderTrailingContent?: (thread: TThread) => ReactNode;
119127
runThread: (thread: Pick<SidebarThreadSummary, "environmentId" | "id">) => Promise<void>;
120128
limit?: number;
121129
}): CommandPaletteActionItem[] {
@@ -140,6 +148,9 @@ export function buildThreadActionItems(input: {
140148
descriptionParts.push("Current thread");
141149
}
142150

151+
const leadingContent = input.renderLeadingContent?.(thread);
152+
const trailingContent = input.renderTrailingContent?.(thread);
153+
143154
return {
144155
kind: "action",
145156
value: `thread:${thread.id}`,
@@ -150,6 +161,8 @@ export function buildThreadActionItems(input: {
150161
thread.latestUserMessageAt ?? thread.updatedAt ?? thread.createdAt,
151162
),
152163
icon: input.icon,
164+
...(leadingContent ? { titleLeadingContent: leadingContent } : {}),
165+
...(trailingContent ? { titleTrailingContent: trailingContent } : {}),
153166
run: async () => {
154167
await input.runThread(thread);
155168
},

apps/web/src/components/CommandPalette.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ import {
8989
import { resolveEnvironmentOptionLabel } from "./BranchToolbar.logic";
9090
import { CommandPaletteResults } from "./CommandPaletteResults";
9191
import { ProjectFavicon } from "./ProjectFavicon";
92+
import { ThreadRowLeadingStatus, ThreadRowTrailingStatus } from "./ThreadStatusIndicators";
9293
import { useServerKeybindings } from "../rpc/serverState";
9394
import { resolveShortcutCommand } from "../keybindings";
9495
import {
@@ -504,6 +505,8 @@ function OpenCommandPaletteDialog() {
504505
projectTitleById,
505506
sortOrder: settings.sidebarThreadSortOrder,
506507
icon: <MessageSquareIcon className={ITEM_ICON_CLASS} />,
508+
renderLeadingContent: (thread) => <ThreadRowLeadingStatus thread={thread} />,
509+
renderTrailingContent: (thread) => <ThreadRowTrailingStatus thread={thread} />,
507510
runThread: async (thread) => {
508511
await navigate({
509512
to: "/$environmentId/$threadId",

apps/web/src/components/CommandPaletteResults.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,20 @@ function CommandPaletteResultRow(props: {
8686
{props.item.icon}
8787
{props.item.description ? (
8888
<span className="flex min-w-0 flex-1 flex-col">
89-
<span className="truncate text-sm text-foreground">{props.item.title}</span>
89+
<span className="flex min-w-0 items-center gap-1.5 text-sm text-foreground">
90+
{props.item.titleLeadingContent}
91+
<span className="truncate">{props.item.title}</span>
92+
{props.item.titleTrailingContent}
93+
</span>
9094
<span className="truncate text-muted-foreground/70 text-xs">
9195
{props.item.description}
9296
</span>
9397
</span>
9498
) : (
95-
<span className="flex min-w-0 items-center gap-1.5 truncate text-sm text-foreground">
99+
<span className="flex min-w-0 flex-1 items-center gap-1.5 text-sm text-foreground">
100+
{props.item.titleLeadingContent}
96101
<span className="truncate">{props.item.title}</span>
102+
{props.item.titleTrailingContent}
97103
</span>
98104
)}
99105
{props.item.timestamp ? (

apps/web/src/components/Sidebar.tsx

Lines changed: 6 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ import {
1111
TerminalIcon,
1212
TriangleAlertIcon,
1313
} from "lucide-react";
14+
import {
15+
prStatusIndicator,
16+
resolveThreadPr,
17+
terminalStatusFromRunningIds,
18+
ThreadStatusLabel,
19+
} from "./ThreadStatusIndicators";
1420
import { ProjectFavicon } from "./ProjectFavicon";
1521
import { autoAnimate } from "@formkit/auto-animate";
1622
import React, { useCallback, useEffect, memo, useMemo, useRef, useState } from "react";
@@ -38,7 +44,6 @@ import {
3844
type SidebarProjectGroupingMode,
3945
type ThreadEnvMode,
4046
ThreadId,
41-
type GitStatusResult,
4247
} from "@t3tools/contracts";
4348
import {
4449
parseScopedThreadKey,
@@ -264,113 +269,6 @@ function buildThreadJumpLabelMap(input: {
264269
return mapping.size > 0 ? mapping : EMPTY_THREAD_JUMP_LABELS;
265270
}
266271

267-
interface TerminalStatusIndicator {
268-
label: "Terminal process running";
269-
colorClass: string;
270-
pulse: boolean;
271-
}
272-
273-
interface PrStatusIndicator {
274-
label: "PR open" | "PR closed" | "PR merged";
275-
colorClass: string;
276-
tooltip: string;
277-
url: string;
278-
}
279-
280-
type ThreadPr = GitStatusResult["pr"];
281-
282-
function ThreadStatusLabel({
283-
status,
284-
compact = false,
285-
}: {
286-
status: ThreadStatusPill;
287-
compact?: boolean;
288-
}) {
289-
if (compact) {
290-
return (
291-
<span
292-
title={status.label}
293-
className={`inline-flex size-3.5 shrink-0 items-center justify-center ${status.colorClass}`}
294-
>
295-
<span
296-
className={`size-[9px] rounded-full ${status.dotClass} ${
297-
status.pulse ? "animate-pulse" : ""
298-
}`}
299-
/>
300-
<span className="sr-only">{status.label}</span>
301-
</span>
302-
);
303-
}
304-
305-
return (
306-
<span
307-
title={status.label}
308-
className={`inline-flex items-center gap-1 text-[10px] ${status.colorClass}`}
309-
>
310-
<span
311-
className={`h-1.5 w-1.5 rounded-full ${status.dotClass} ${
312-
status.pulse ? "animate-pulse" : ""
313-
}`}
314-
/>
315-
<span className="hidden md:inline">{status.label}</span>
316-
</span>
317-
);
318-
}
319-
320-
function terminalStatusFromRunningIds(
321-
runningTerminalIds: string[],
322-
): TerminalStatusIndicator | null {
323-
if (runningTerminalIds.length === 0) {
324-
return null;
325-
}
326-
return {
327-
label: "Terminal process running",
328-
colorClass: "text-teal-600 dark:text-teal-300/90",
329-
pulse: true,
330-
};
331-
}
332-
333-
function prStatusIndicator(pr: ThreadPr): PrStatusIndicator | null {
334-
if (!pr) return null;
335-
336-
if (pr.state === "open") {
337-
return {
338-
label: "PR open",
339-
colorClass: "text-emerald-600 dark:text-emerald-300/90",
340-
tooltip: `#${pr.number} PR open: ${pr.title}`,
341-
url: pr.url,
342-
};
343-
}
344-
if (pr.state === "closed") {
345-
return {
346-
label: "PR closed",
347-
colorClass: "text-zinc-500 dark:text-zinc-400/80",
348-
tooltip: `#${pr.number} PR closed: ${pr.title}`,
349-
url: pr.url,
350-
};
351-
}
352-
if (pr.state === "merged") {
353-
return {
354-
label: "PR merged",
355-
colorClass: "text-violet-600 dark:text-violet-300/90",
356-
tooltip: `#${pr.number} PR merged: ${pr.title}`,
357-
url: pr.url,
358-
};
359-
}
360-
return null;
361-
}
362-
363-
function resolveThreadPr(
364-
threadBranch: string | null,
365-
gitStatus: GitStatusResult | null,
366-
): ThreadPr | null {
367-
if (threadBranch === null || gitStatus === null || gitStatus.branch !== threadBranch) {
368-
return null;
369-
}
370-
371-
return gitStatus.pr ?? null;
372-
}
373-
374272
interface SidebarThreadRowProps {
375273
thread: SidebarThreadSummary;
376274
projectCwd: string | null;

0 commit comments

Comments
 (0)