Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions apps/web/src/components/SidebarV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
GitBranchIcon,
EllipsisIcon,
MessageSquareIcon,
PencilIcon,
PinIcon,
PlusIcon,
SearchIcon,
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -825,6 +828,23 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
<TerminalIcon className={cn("size-3.5", terminalStatus.pulse && "animate-status-pulse")} />
</span>
) : null;
const draftIcon = hasDraft ? (
<span
role="img"
aria-label="Unsent draft"
data-testid={`sidebar-v2-draft-${thread.id}`}
className={cn(
// Matches the PR badge's settled treatment: muted at rest so the
// parked tail stays quiet, brightening with the row on hover. The
// pencil has no state colour of its own, so it lifts to the same
// foreground the title does.
"inline-flex shrink-0 items-center justify-center text-secondary-label",
!props.isActive && "transition-colors group-hover/v2-row:text-foreground",
)}
>
<PencilIcon className="size-3.5" />
</span>
) : null;

if (variant === "slim") {
return (
Expand Down Expand Up @@ -865,6 +885,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
/>
</span>
{title}
{draftIcon}
{terminalStatusIcon}
{isRegeneratingTitle ? (
<span role="status" className="sr-only">
Expand Down Expand Up @@ -1125,6 +1146,7 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
) : (
<span className="flex-1" />
)}
{draftIcon}
{terminalStatusIcon}
{prBadge}
{diff ? (
Expand Down
23 changes: 23 additions & 0 deletions apps/web/src/composerDraftStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading