diff --git a/apps/web/src/components/ChatMarkdown.workspace-images.test.tsx b/apps/web/src/components/ChatMarkdown.workspace-images.test.tsx index 60cc460453f4..6669258df408 100644 --- a/apps/web/src/components/ChatMarkdown.workspace-images.test.tsx +++ b/apps/web/src/components/ChatMarkdown.workspace-images.test.tsx @@ -63,6 +63,34 @@ function renderStreaming(markdown: string): string { return renderToStaticMarkup(); } +function renderFilePreview(cwd: string, relativePath: string): string { + return renderToStaticMarkup( + , + ); +} + +function copiedMarkdownFrom(html: string): string { + const copy = /data-markdown-copy="([^"]*)"/.exec(html)?.[1]?.replaceAll(""", '"'); + expect(copy).toBeDefined(); + return copy ?? ""; +} + +function firstInlineStyle(html: string): Record { + const style = /style="([^"]+)"/.exec(html)?.[1]; + expect(style).toBeDefined(); + return Object.fromEntries( + (style ?? "").split(";").map((declaration) => { + const separator = declaration.indexOf(":"); + return [declaration.slice(0, separator), declaration.slice(separator + 1)]; + }), + ); +} + describe("ChatMarkdown workspace images", () => { beforeEach(() => { testState.resources = []; diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 8b2e7b14899a..5f63bededc0f 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -2394,7 +2394,6 @@ function ChatViewContent(props: ChatViewProps) { () => deriveWorkLogEntries(timelineActivities), [timelineActivities], ); - const turnPlans = useMemo(() => deriveTurnPlans(timelineActivities), [timelineActivities]); // Native subagent fold: memoized by activity-list identity, shared by the // Agents surface, live strip, and workflow cards. v2Projection is null // until orchestration-v2 lands (source precedence lives in the derive). @@ -2464,6 +2463,21 @@ function ChatViewContent(props: ChatViewProps) { () => deriveActivePlanState(threadActivities, activeLatestTurn?.turnId ?? undefined), [activeLatestTurn?.turnId, threadActivities], ); + // Current step for the in-chat working row: only for the running turn's own + // plan (deriveActivePlanState falls back to older turns' plans, which must + // not label fresh work). Falls back to the first pending step so an + // all-pending freshly written plan labels the row, matching the chip and + // the server's planProgress. + const workingStepLabel = useMemo(() => { + if (!activePlan || activePlan.turnId !== (activeLatestTurn?.turnId ?? null)) { + return null; + } + return ( + activePlan.steps.find((step) => step.status === "inProgress")?.step ?? + activePlan.steps.find((step) => step.status === "pending")?.step ?? + null + ); + }, [activeLatestTurn?.turnId, activePlan]); const showPlanFollowUpPrompt = shouldShowPlanFollowUpPrompt({ pendingUserInputCount: pendingUserInputs.length, interactionMode, @@ -2852,13 +2866,8 @@ function ChatViewContent(props: ChatViewProps) { ]); const timelineEntries = useMemo( () => - deriveTimelineEntries( - timelineMessages, - timelineThread?.proposedPlans ?? [], - workLogEntries, - turnPlans, - ), - [timelineMessages, timelineThread?.proposedPlans, turnPlans, workLogEntries], + deriveTimelineEntries(timelineMessages, timelineThread?.proposedPlans ?? [], workLogEntries), + [timelineMessages, timelineThread?.proposedPlans, workLogEntries], ); const [dockedDraftHeroThreadKey, setDockedDraftHeroThreadKey] = useState(null); const draftHeroDockRequested = diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index 5a265139e8aa..7bb0078ba406 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -20,7 +20,6 @@ import { isTrailingDoubleClick, orderItemsByPreferredIds, resolveProjectStatusIndicator, - resolveSidebarStageBadgeLabel, resolveSidebarReasoningLabel, resolveThreadRowClassName, resolveSidebarThreadStatus, @@ -238,44 +237,6 @@ describe("buildMultiSelectThreadContextMenuItems", () => { }); }); -describe("resolveSidebarStageBadgeLabel", () => { - it("returns Nightly for nightly primary server versions", () => { - expect( - resolveSidebarStageBadgeLabel({ - primaryServerVersion: "0.0.28-nightly.20260616.12", - fallbackStageLabel: "Alpha", - }), - ).toBe("Nightly"); - }); - - it("returns the fallback label for stable primary server versions", () => { - expect( - resolveSidebarStageBadgeLabel({ - primaryServerVersion: "0.0.27", - fallbackStageLabel: "Alpha", - }), - ).toBe("Alpha"); - }); - - it("returns the fallback label when the primary server version is missing", () => { - expect( - resolveSidebarStageBadgeLabel({ - primaryServerVersion: null, - fallbackStageLabel: "Dev", - }), - ).toBe("Dev"); - }); - - it("returns the fallback label for malformed nightly prerelease versions", () => { - expect( - resolveSidebarStageBadgeLabel({ - primaryServerVersion: "0.0.28-nightly.20260616", - fallbackStageLabel: "Alpha", - }), - ).toBe("Alpha"); - }); -}); - describe("resolveSidebarReasoningLabel", () => { const model = { slug: "gpt-5.4", diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index f667a440bb27..af9bee5763e6 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -18,7 +18,8 @@ import { const EMPTY_AGENT_PANEL_MODEL = emptyAgentPanelModel(); const NOOP_OPEN_AGENTS = () => {}; const NOOP_RESPONSE_CLICK = () => {}; -const NOOP_DOWNLOAD_ATTACHMENT = (_attachment: ChatFileAttachment) => {}; +const NOOP_USE_ARTIFACT_TEMPLATE = () => {}; +const NOOP_OPEN_ATTACHMENT = (_attachment: ChatFileAttachment) => {}; import { resolveChatListAnchoredEndSpace } from "@t3tools/shared/chatList"; import { createContext, @@ -1071,7 +1072,7 @@ function UserTimelineRow({ row }: { row: Extract
- {regularImages.length > 0 && ( + {(regularImages.length > 0 || userVideos.length > 0) && (
{regularImages.map((image) => (
))} + {userVideos.map((file) => { + const isOpening = ctx.openingVideoAttachmentId === file.id; + return ( +
+ +
+ ); + })}
)} {previewAnnotations.map((annotation, index) => ( @@ -1111,9 +1141,9 @@ function UserTimelineRow({ row }: { row: Extract ))} - {userFiles.length > 0 || unknownAttachments.length > 0 ? ( + {otherUserFiles.length > 0 || unknownAttachments.length > 0 ? (
- {userFiles.map((file) => { + {otherUserFiles.map((file) => { const content = ( <> @@ -1141,7 +1171,7 @@ function UserTimelineRow({ row }: { row: Extract ctx.onFileDownload(file)} + onClick={() => ctx.onFileOpen(file)} className="flex min-w-0 cursor-pointer items-center gap-2 rounded-md py-1 text-left text-sm hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70" > {content}