diff --git a/apps/web/src/components/LegacySidebar.tsx b/apps/web/src/components/LegacySidebar.tsx index 13dc5b7c1a7a..3c7da604123d 100644 --- a/apps/web/src/components/LegacySidebar.tsx +++ b/apps/web/src/components/LegacySidebar.tsx @@ -909,11 +909,7 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr > Waiting for {thread.actionResume.actionName} diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 1379919fee6f..fa92aac8f1c2 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -628,7 +628,6 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { environmentLabel: string | null; environmentKnown: boolean; showLocalEnvironmentIcon: boolean; - showWorktreeIndicators: boolean; configuredEnvironmentIconColor: EnvironmentIconColor | undefined; projectCwd: string | null; projectFaviconPath: string | null; @@ -1622,9 +1621,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { branch, so the row lost its most stable identifier. */} {thread.branch ? ( <> - {props.showWorktreeIndicators ? ( - - ) : null} + {thread.branch} ) : ( @@ -1826,7 +1823,6 @@ export default function Sidebar() { const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings); const environmentIconColors = useClientSettings((s) => s.environmentIconColors); const showLocalEnvironmentIcon = useClientSettings((s) => s.showLocalEnvironmentIcon); - const showWorktreeIndicators = useClientSettings((s) => s.showThreadWorktreeIndicators); const { settleThread, unsettleThread, @@ -3878,7 +3874,6 @@ export default function Sidebar() { environmentLabel={environmentLabelById.get(thread.environmentId) ?? null} environmentKnown={environmentLabelById.has(thread.environmentId)} showLocalEnvironmentIcon={showLocalEnvironmentIcon} - showWorktreeIndicators={showWorktreeIndicators} configuredEnvironmentIconColor={environmentIconColors[thread.environmentId]} projectCwd={ projectCwdByKey.get(`${thread.environmentId}:${thread.projectId}`) ?? null diff --git a/apps/web/src/components/ThreadStatusIndicators.test.tsx b/apps/web/src/components/ThreadStatusIndicators.test.tsx index 868bd2cd99c0..205cb650cdd9 100644 --- a/apps/web/src/components/ThreadStatusIndicators.test.tsx +++ b/apps/web/src/components/ThreadStatusIndicators.test.tsx @@ -2,7 +2,25 @@ import { ThreadId } from "@t3tools/contracts"; import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it } from "vite-plus/test"; -import { ThreadWorktreeIndicator } from "./ThreadStatusIndicators"; +import { ThreadStatusLabel, ThreadWorktreeIndicator } from "./ThreadStatusIndicators"; + +describe("ThreadStatusLabel", () => { + it("keeps the status dot the same size when labels are compacted", () => { + const status = { + colorClass: "text-sky-600", + dotClass: "bg-sky-500", + label: "Working", + pulse: false, + }; + + const expandedMarkup = renderToStaticMarkup(); + const compactMarkup = renderToStaticMarkup(); + + expect(expandedMarkup).toContain("size-1.5"); + expect(compactMarkup).toContain("size-1.5"); + expect(compactMarkup).not.toContain("size-[9px]"); + }); +}); describe("ThreadWorktreeIndicator", () => { it("renders the worktree folder and branch in an accessible label", () => { diff --git a/apps/web/src/components/ThreadStatusIndicators.tsx b/apps/web/src/components/ThreadStatusIndicators.tsx index a94fc5e6e6b0..178a983162dc 100644 --- a/apps/web/src/components/ThreadStatusIndicators.tsx +++ b/apps/web/src/components/ThreadStatusIndicators.tsx @@ -369,7 +369,7 @@ export function ThreadStatusLabel({ > @@ -391,7 +391,7 @@ export function ThreadStatusLabel({ > diff --git a/apps/web/src/components/chat/MessagesTimeline.test.tsx b/apps/web/src/components/chat/MessagesTimeline.test.tsx index d1cc9aa37702..beec7fbb4d75 100644 --- a/apps/web/src/components/chat/MessagesTimeline.test.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.test.tsx @@ -391,6 +391,7 @@ describe("MessagesTimeline", () => { expect(markup).toContain('data-testid="timeline-minimap"'); expect(markup).toContain("data-thread-annotation-marker"); + expect(markup).toContain('data-slot="popover-trigger"'); expect(markup).toContain("[@media(pointer:coarse)]:block"); expect(markup).toContain("[@media(pointer:coarse)]:opacity-100"); expect(markup).not.toContain("data-thread-annotation-overflow"); @@ -413,6 +414,7 @@ describe("MessagesTimeline", () => { expect(markup).toContain("data-thread-annotation-overflow"); expect(markup).toContain('aria-label="Annotation attached to an earlier message"'); + expect(markup).toContain('data-slot="popover-trigger"'); expect(markup).toContain('class="pointer-events-auto absolute left-3"'); expect(markup).not.toContain("data-thread-annotation-marker"); }); diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 056ca265f59a..920f32f6adcb 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -95,6 +95,7 @@ import { } from "./MessagesTimeline.logic"; import { TerminalContextInlineChip } from "./TerminalContextInlineChip"; import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip"; +import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; import { deriveDisplayedUserMessageState, type ParsedTerminalContextEntry, @@ -774,6 +775,94 @@ function timelineMinimapEventTargetsPreview(target: EventTarget): boolean { return target instanceof Element && target.closest("[data-minimap-preview]") !== null; } +function TimelineAnnotationPopover({ + annotation, + ariaLabel, + earlier, + markdownCwd, + open, + threadRef, + triggerClassName, + onActivate, + onAnnotationBodyChange, + onAnnotationEdit, + onAnnotationResolve, + onAnnotationReopen, + onOpenChange, +}: { + annotation: ThreadAnnotation; + ariaLabel: string; + earlier: boolean; + markdownCwd: string | undefined; + open: boolean; + threadRef: ScopedThreadRef; + triggerClassName: string; + onActivate?: () => void; + onAnnotationBodyChange: ((body: string) => Promise) | undefined; + onAnnotationEdit: () => void; + onAnnotationResolve: () => void; + onAnnotationReopen: () => void; + onOpenChange: (open: boolean) => void; +}) { + const annotationBodyPending = useThreadAnnotationBodyPending(threadRef); + + return ( + + + } + onClick={(event) => { + event.stopPropagation(); + onActivate?.(); + }} + onFocus={onActivate} + /> + +
+ {earlier ? ( +
Attached to an earlier message
+ ) : null} + +
+ +
+
+
+
+ ); +} + function TimelineMinimap({ annotation, hasPersistentGutter, @@ -801,9 +890,8 @@ function TimelineMinimap({ onAnnotationReopen: () => void; onSelect: (item: TimelineMinimapItem) => void; }) { - const annotationBodyPending = useThreadAnnotationBodyPending(threadRef); const [activeIndex, setActiveIndex] = useState(null); - const [overflowAnnotationOpen, setOverflowAnnotationOpen] = useState(false); + const [annotationPopoverOpen, setAnnotationPopoverOpen] = useState(false); const resolvedActiveIndex = activeIndex !== null && activeIndex < items.length ? activeIndex : null; @@ -812,8 +900,6 @@ function TimelineMinimap({ ? items.findIndex((item) => item.messageId === annotation.anchorMessageId) : -1; const annotationIsEarlier = annotation !== null && annotationItemIndex === -1; - const activeItemHasAnnotation = - annotation !== null && activeItem?.messageId === annotation.anchorMessageId; const activeTopPercent = resolvedActiveIndex === null ? 0 @@ -904,6 +990,9 @@ function TimelineMinimap({ }} onFocus={() => setActiveIndex((current) => current ?? 0)} onKeyDown={(event) => { + if (timelineMinimapEventTargetsPreview(event.target)) { + return; + } if (event.key === "ArrowDown") { event.preventDefault(); moveActiveIndex(1); @@ -970,23 +1059,27 @@ function TimelineMinimap({ }} style={{ top }} > - {annotation?.anchorMessageId === item.messageId ? ( -