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
28 changes: 28 additions & 0 deletions apps/web/src/components/ChatMarkdown.workspace-images.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,34 @@ function renderStreaming(markdown: string): string {
return renderToStaticMarkup(<ChatMarkdown cwd={undefined} text={markdown} isStreaming />);
}

function renderFilePreview(cwd: string, relativePath: string): string {
return renderToStaticMarkup(
<FileMarkdownPreview
cwd={cwd}
relativePath={relativePath}
text="![diagram](images/diagram.png)"
threadRef={threadRef}
/>,
);
}

function copiedMarkdownFrom(html: string): string {
const copy = /data-markdown-copy="([^"]*)"/.exec(html)?.[1]?.replaceAll("&quot;", '"');
expect(copy).toBeDefined();
return copy ?? "";
}

function firstInlineStyle(html: string): Record<string, string> {
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 = [];
Expand Down
25 changes: 17 additions & 8 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<string | null>(null);
const draftHeroDockRequested =
Expand Down
39 changes: 0 additions & 39 deletions apps/web/src/components/Sidebar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
isTrailingDoubleClick,
orderItemsByPreferredIds,
resolveProjectStatusIndicator,
resolveSidebarStageBadgeLabel,
resolveSidebarReasoningLabel,
resolveThreadRowClassName,
resolveSidebarThreadStatus,
Expand Down Expand Up @@ -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",
Expand Down
40 changes: 35 additions & 5 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1071,7 +1072,7 @@ function UserTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "message"
<div className="group flex flex-col items-start gap-1">
<div className="flex min-w-0 w-full items-start gap-2">
<div className="relative min-w-0 w-full rounded-xl border border-border/35 bg-message/45 px-2.5 py-2 text-message-foreground">
{regularImages.length > 0 && (
{(regularImages.length > 0 || userVideos.length > 0) && (
<div className="mb-2 grid max-w-[420px] grid-cols-2 gap-2">
{regularImages.map((image) => (
<div
Expand Down Expand Up @@ -1102,6 +1103,35 @@ function UserTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "message"
)}
</div>
))}
{userVideos.map((file) => {
const isOpening = ctx.openingVideoAttachmentId === file.id;
return (
<div
key={file.id}
className="overflow-hidden rounded-lg border border-border/80 bg-black"
>
<button
type="button"
disabled={file.downloadable === false}
className="flex min-h-[72px] w-full cursor-zoom-in flex-col items-center justify-center gap-1 px-2 py-2 text-white disabled:cursor-default disabled:opacity-50 aria-disabled:cursor-default aria-disabled:opacity-50"
aria-busy={isOpening || undefined}
aria-disabled={isOpening || undefined}
aria-label={`${isOpening ? "Loading" : "Play"} ${file.name}`}
onClick={() => {
if (isOpening) return;
ctx.onFileOpen(file);
}}
>
{isOpening ? (
<span className="text-[11px]">Loading…</span>
) : (
<PlayIcon className="size-8 fill-current" />
)}
<span className="max-w-full truncate text-[11px]">{file.name}</span>
</button>
</div>
);
})}
</div>
)}
{previewAnnotations.map((annotation, index) => (
Expand All @@ -1111,9 +1141,9 @@ function UserTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "message"
image={previewImages[index] ?? null}
/>
))}
{userFiles.length > 0 || unknownAttachments.length > 0 ? (
{otherUserFiles.length > 0 || unknownAttachments.length > 0 ? (
<div className="mb-2 flex flex-col gap-1">
{userFiles.map((file) => {
{otherUserFiles.map((file) => {
const content = (
<>
<FileIcon className="size-4 shrink-0 text-secondary-label" />
Expand Down Expand Up @@ -1141,7 +1171,7 @@ function UserTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "message"
key={file.id}
type="button"
aria-label={`Download ${file.name}`}
onClick={() => 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}
Expand Down
Loading