diff --git a/apps/web/src/components/SidebarV2.tsx b/apps/web/src/components/SidebarV2.tsx index 89419d428f6..2839a10bafd 100644 --- a/apps/web/src/components/SidebarV2.tsx +++ b/apps/web/src/components/SidebarV2.tsx @@ -29,6 +29,7 @@ import { GitBranchIcon, EllipsisIcon, MessageSquareIcon, + PencilIcon, PinIcon, PlusIcon, SearchIcon, @@ -83,6 +84,7 @@ import { type SidebarProjectGroupMember, type SidebarProjectSnapshot, } from "../sidebarProjectGrouping"; +import { useComposerThreadHasDraftContent } from "../composerDraftStore"; import { legacyProjectCwdPreferenceKey, useUiStateStore } from "../uiStateStore"; import { useThreadSelectionStore } from "../threadSelectionStore"; import { useThreadActions } from "../hooks/useThreadActions"; @@ -469,6 +471,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { }); const terminalStatus = terminalStatusFromRunningIds(runningTerminalIds); const terminalProcessCount = runningTerminalIds.length; + const hasDraft = useComposerThreadHasDraftContent(threadRef); const gitCwd = thread.worktreePath ?? props.projectCwd; const gitStatus = useEnvironmentQuery( @@ -825,6 +828,23 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { ) : null; + const draftIcon = hasDraft ? ( + + + + ) : null; if (variant === "slim") { return ( @@ -865,6 +885,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { /> {title} + {draftIcon} {terminalStatusIcon} {isRegeneratingTitle ? ( @@ -1125,6 +1146,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: { ) : ( )} + {draftIcon} {terminalStatusIcon} {prBadge} {diff ? ( diff --git a/apps/web/src/composerDraftStore.ts b/apps/web/src/composerDraftStore.ts index 95dde6187c8..77d43152780 100644 --- a/apps/web/src/composerDraftStore.ts +++ b/apps/web/src/composerDraftStore.ts @@ -3468,6 +3468,29 @@ export function clearComposerDraftsEnvironment(environmentId: EnvironmentId): vo composerDebouncedStorage.flush(); } +/** + * Whether the thread has unsent composer content worth surfacing in thread + * lists. Content means prompt text or attachments — a draft entry that only + * carries settings (model selection, runtime mode) doesn't count. + */ +export function useComposerThreadHasDraftContent(threadRef: ComposerThreadTarget): boolean { + return useComposerDraftStore((state) => { + const draft = getComposerDraftState(state, threadRef); + if (!draft) { + return false; + } + return ( + draft.prompt.trim().length > 0 || + draft.images.length > 0 || + draft.persistedAttachments.length > 0 || + draft.terminalContexts.length > 0 || + draft.elementContexts.length > 0 || + draft.previewAnnotations.length > 0 || + draft.reviewComments.length > 0 + ); + }); +} + export function useComposerThreadDraft(threadRef: ComposerThreadTarget): ComposerThreadDraftState { return useComposerDraftStore((state) => { return getComposerDraftState(state, threadRef) ?? EMPTY_THREAD_DRAFT;