Skip to content

Commit 03557a0

Browse files
feat(web): show unsent drafts in the sidebar (#23)
* feat(web): show unsent drafts in the sidebar - Add a draft indicator to sidebar thread rows - Surface drafts containing text or attachments, excluding settings-only state * fix(web): brighten the sidebar draft pencil on row hover The pencil sat at a flat secondary-label colour, so on a settled row it read brighter than the row's own title and stayed put while the rest of the row lifted. Match the PR badge's settled treatment: muted at rest, transitioning with the row on hover. The pencil has no state colour of its own, so it lifts to the same foreground the title does, and the active row opts out like the PR badge already does. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 8df5d8f commit 03557a0

2 files changed

Lines changed: 45 additions & 0 deletions

File tree

apps/web/src/components/Sidebar.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
FolderPlusIcon,
4545
GitBranchIcon,
4646
MessageSquareIcon,
47+
PencilIcon,
4748
PinIcon,
4849
PlusIcon,
4950
SearchIcon,
@@ -93,6 +94,7 @@ import {
9394
buildSidebarProjectSnapshots,
9495
type SidebarProjectSnapshot,
9596
} from "../sidebarProjectGrouping";
97+
import { useComposerThreadHasDraftContent } from "../composerDraftStore";
9698
import { legacyProjectCwdPreferenceKey, useUiStateStore } from "../uiStateStore";
9799
import { useThreadSelectionStore } from "../threadSelectionStore";
98100
import { useThreadActions } from "../hooks/useThreadActions";
@@ -740,6 +742,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
740742
});
741743
const terminalStatus = terminalStatusFromRunningIds(runningTerminalIds);
742744
const terminalProcessCount = runningTerminalIds.length;
745+
const hasDraft = useComposerThreadHasDraftContent(threadRef);
743746

744747
const gitCwd = thread.worktreePath ?? props.projectCwd;
745748
const gitStatus = useEnvironmentQuery(
@@ -1113,6 +1116,23 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
11131116
<TerminalIcon className={cn("size-3.5", terminalStatus.pulse && "animate-status-pulse")} />
11141117
</span>
11151118
) : null;
1119+
const draftIcon = hasDraft ? (
1120+
<span
1121+
role="img"
1122+
aria-label="Unsent draft"
1123+
data-testid={`sidebar-v2-draft-${thread.id}`}
1124+
className={cn(
1125+
// Matches the PR badge's settled treatment: muted at rest so the
1126+
// parked tail stays quiet, brightening with the row on hover. The
1127+
// pencil has no state colour of its own, so it lifts to the same
1128+
// foreground the title does.
1129+
"inline-flex shrink-0 items-center justify-center text-secondary-label",
1130+
!props.isActive && "transition-colors group-hover/v2-row:text-foreground",
1131+
)}
1132+
>
1133+
<PencilIcon className="size-3.5" />
1134+
</span>
1135+
) : null;
11161136

11171137
if (variant === "slim") {
11181138
return (
@@ -1154,6 +1174,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
11541174
/>
11551175
</span>
11561176
{title}
1177+
{draftIcon}
11571178
{terminalStatusIcon}
11581179
{isRegeneratingTitle ? (
11591180
<span role="status" className="sr-only">
@@ -1436,6 +1457,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
14361457
) : (
14371458
<span className="flex-1" />
14381459
)}
1460+
{draftIcon}
14391461
{terminalStatusIcon}
14401462
{prBadge}
14411463
{diff ? (

apps/web/src/composerDraftStore.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3566,6 +3566,29 @@ export function clearComposerDraftsEnvironment(environmentId: EnvironmentId): vo
35663566
composerDebouncedStorage.flush();
35673567
}
35683568

3569+
/**
3570+
* Whether the thread has unsent composer content worth surfacing in thread
3571+
* lists. Content means prompt text or attachments — a draft entry that only
3572+
* carries settings (model selection, runtime mode) doesn't count.
3573+
*/
3574+
export function useComposerThreadHasDraftContent(threadRef: ComposerThreadTarget): boolean {
3575+
return useComposerDraftStore((state) => {
3576+
const draft = getComposerDraftState(state, threadRef);
3577+
if (!draft) {
3578+
return false;
3579+
}
3580+
return (
3581+
draft.prompt.trim().length > 0 ||
3582+
draft.images.length > 0 ||
3583+
draft.persistedAttachments.length > 0 ||
3584+
draft.terminalContexts.length > 0 ||
3585+
draft.elementContexts.length > 0 ||
3586+
draft.previewAnnotations.length > 0 ||
3587+
draft.reviewComments.length > 0
3588+
);
3589+
});
3590+
}
3591+
35693592
export function useComposerThreadDraft(threadRef: ComposerThreadTarget): ComposerThreadDraftState {
35703593
return useComposerDraftStore((state) => {
35713594
return getComposerDraftState(state, threadRef) ?? EMPTY_THREAD_DRAFT;

0 commit comments

Comments
 (0)