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
19 changes: 13 additions & 6 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ import {
ChevronLeftIcon,
ChevronRightIcon,
CircleAlertIcon,
ImagePlusIcon,
ListTodoIcon,
LockIcon,
LockOpenIcon,
Expand Down Expand Up @@ -389,11 +388,19 @@ export default function ChatView({ threadId }: ChatViewProps) {
const prompt = composerDraft.prompt;
const composerAttachments = composerDraft.attachments;
const composerImageAttachments = useMemo(
() => composerAttachments.filter((attachment) => attachment.type === "image"),
() =>
composerAttachments.filter(
(attachment): attachment is Extract<ComposerAttachment, { type: "image" }> =>
attachment.type === "image",
),
[composerAttachments],
);
const composerFileAttachments = useMemo(
() => composerAttachments.filter((attachment) => attachment.type === "file"),
() =>
composerAttachments.filter(
(attachment): attachment is Extract<ComposerAttachment, { type: "file" }> =>
attachment.type === "file",
),
[composerAttachments],
);
const composerTerminalContexts = composerDraft.terminalContexts;
Expand Down Expand Up @@ -4748,7 +4755,7 @@ export default function ChatView({ threadId }: ChatViewProps) {
: showPlanFollowUpPrompt && activeProposedPlan
? "Add feedback to refine the plan, or leave this blank to implement it"
: phase === "disconnected"
? "Ask for follow-up changes or attach images"
? "Ask for follow-up changes or attach files"
: "Ask anything, @tag files/folders, or use / to show available commands"
}
disabled={isConnecting || isComposerApprovalState || isRemoteActionBlocked}
Expand Down Expand Up @@ -4919,8 +4926,8 @@ export default function ChatView({ threadId }: ChatViewProps) {
type="button"
className="text-muted-foreground/70 hover:text-foreground/80"
onClick={openFilePicker}
title="Attach images"
aria-label="Attach images"
title="Attach files"
aria-label="Attach files"
>
<PaperclipIcon className="size-4" />
</Button>
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/chat/ClaudeTraitsPicker.browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ async function mountPicker(props?: {
>["draftsByThreadId"];
draftsByThreadId[threadId] = {
prompt: props?.prompt ?? "",
images: [],
nonPersistedImageIds: [],
attachments: [],
nonPersistedAttachmentIds: [],
persistedAttachments: [],
terminalContexts: [],
provider: "claudeAgent",
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/components/chat/CodexTraitsPicker.browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ async function mountPicker(props: {
>["draftsByThreadId"];
draftsByThreadId[threadId] = {
prompt: "",
images: [],
nonPersistedImageIds: [],
attachments: [],
nonPersistedAttachmentIds: [],
persistedAttachments: [],
terminalContexts: [],
provider: "codex",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ async function mountMenu(props?: {
>["draftsByThreadId"];
draftsByThreadId[threadId] = {
prompt: props?.prompt ?? "",
images: [],
nonPersistedImageIds: [],
attachments: [],
nonPersistedAttachmentIds: [],
persistedAttachments: [],
terminalContexts: [],
provider,
Expand Down
14 changes: 12 additions & 2 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -387,8 +387,18 @@ export const MessagesTimeline = memo(function MessagesTimeline({
row.message.role === "user" &&
(() => {
const userAttachments = row.message.attachments ?? [];
const userImages = userAttachments.filter((attachment) => attachment.type === "image");
const userFiles = userAttachments.filter((attachment) => attachment.type === "file");
const userImages = userAttachments.filter(
(
attachment,
): attachment is Extract<(typeof userAttachments)[number], { type: "image" }> =>
attachment.type === "image",
);
const userFiles = userAttachments.filter(
(
attachment,
): attachment is Extract<(typeof userAttachments)[number], { type: "file" }> =>
attachment.type === "file",
);
const displayedUserMessage = deriveDisplayedUserMessageState(row.message.text);
const terminalContexts = displayedUserMessage.contexts;
const canRevertAgentWork = revertTurnCountByUserMessageId.has(row.message.id);
Expand Down
23 changes: 12 additions & 11 deletions apps/web/src/composerDraftStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ function resetComposerDraftStore() {
});
}

describe("composerDraftStore addImages", () => {
describe("composerDraftStore addAttachments", () => {
const threadId = ThreadId.makeUnsafe("thread-dedupe");
let originalRevokeObjectUrl: typeof URL.revokeObjectURL;
let revokeSpy: ReturnType<typeof vi.fn<(url: string) => void>>;
Expand Down Expand Up @@ -106,10 +106,10 @@ describe("composerDraftStore addImages", () => {
lastModified: 12345,
});

useComposerDraftStore.getState().addImages(threadId, [first, duplicate]);
useComposerDraftStore.getState().addAttachments(threadId, [first, duplicate]);

const draft = useComposerDraftStore.getState().draftsByThreadId[threadId];
expect(draft?.images.map((image) => image.id)).toEqual(["img-1"]);
expect(draft?.attachments.map((image) => image.id)).toEqual(["img-1"]);
expect(revokeSpy).toHaveBeenCalledWith("blob:duplicate");
});

Expand All @@ -131,11 +131,11 @@ describe("composerDraftStore addImages", () => {
lastModified: 999,
});

useComposerDraftStore.getState().addImage(threadId, first);
useComposerDraftStore.getState().addImage(threadId, duplicateLater);
useComposerDraftStore.getState().addAttachment(threadId, first);
useComposerDraftStore.getState().addAttachment(threadId, duplicateLater);

const draft = useComposerDraftStore.getState().draftsByThreadId[threadId];
expect(draft?.images.map((image) => image.id)).toEqual(["img-a"]);
expect(draft?.attachments.map((image) => image.id)).toEqual(["img-a"]);
expect(revokeSpy).toHaveBeenCalledWith("blob:b");
});

Expand All @@ -149,10 +149,10 @@ describe("composerDraftStore addImages", () => {
previewUrl: "blob:shared",
});

useComposerDraftStore.getState().addImages(threadId, [first, duplicateSameUrl]);
useComposerDraftStore.getState().addAttachments(threadId, [first, duplicateSameUrl]);

const draft = useComposerDraftStore.getState().draftsByThreadId[threadId];
expect(draft?.images.map((image) => image.id)).toEqual(["img-shared"]);
expect(draft?.attachments.map((image) => image.id)).toEqual(["img-shared"]);
expect(revokeSpy).not.toHaveBeenCalledWith("blob:shared");
});
});
Expand All @@ -178,7 +178,7 @@ describe("composerDraftStore clearComposerContent", () => {
id: "img-optimistic",
previewUrl: "blob:optimistic",
});
useComposerDraftStore.getState().addImage(threadId, first);
useComposerDraftStore.getState().addAttachment(threadId, first);

useComposerDraftStore.getState().clearComposerContent(threadId);

Expand Down Expand Up @@ -209,7 +209,7 @@ describe("composerDraftStore syncPersistedAttachments", () => {
id: "img-persisted",
previewUrl: "blob:persisted",
});
useComposerDraftStore.getState().addImage(threadId, image);
useComposerDraftStore.getState().addAttachment(threadId, image);
setLocalStorageItem(
COMPOSER_DRAFT_STORAGE_KEY,
{
Expand All @@ -227,6 +227,7 @@ describe("composerDraftStore syncPersistedAttachments", () => {

useComposerDraftStore.getState().syncPersistedAttachments(threadId, [
{
type: "image",
id: image.id,
name: image.name,
mimeType: image.mimeType,
Expand All @@ -240,7 +241,7 @@ describe("composerDraftStore syncPersistedAttachments", () => {
useComposerDraftStore.getState().draftsByThreadId[threadId]?.persistedAttachments,
).toEqual([]);
expect(
useComposerDraftStore.getState().draftsByThreadId[threadId]?.nonPersistedImageIds,
useComposerDraftStore.getState().draftsByThreadId[threadId]?.nonPersistedAttachmentIds,
).toEqual([image.id]);
});
});
Expand Down
43 changes: 22 additions & 21 deletions apps/web/src/composerDraftStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -962,35 +962,36 @@ function hydratePersistedComposerAttachmentFile(
function hydrateAttachmentsFromPersisted(
attachments: ReadonlyArray<PersistedComposerAttachment>,
): ComposerAttachment[] {
return attachments.flatMap((attachment) => {
const hydrated: ComposerAttachment[] = [];
for (const attachment of attachments) {
const file = hydratePersistedComposerAttachmentFile(attachment);
if (!file) return [];

if (attachment.type === "image") {
return [
{
type: "image" as const,
id: attachment.id,
name: attachment.name,
mimeType: attachment.mimeType,
sizeBytes: attachment.sizeBytes,
previewUrl: attachment.dataUrl,
file,
} satisfies ComposerImageAttachment,
];
if (!file) {
continue;
}

return [
{
type: "file" as const,
if (attachment.type === "image") {
hydrated.push({
type: "image" as const,
id: attachment.id,
name: attachment.name,
mimeType: attachment.mimeType,
sizeBytes: attachment.sizeBytes,
previewUrl: attachment.dataUrl,
file,
} satisfies ComposerFileAttachment,
];
});
} satisfies ComposerImageAttachment);
continue;
}

hydrated.push({
type: "file" as const,
id: attachment.id,
name: attachment.name,
mimeType: attachment.mimeType,
sizeBytes: attachment.sizeBytes,
file,
} satisfies ComposerFileAttachment);
}
return hydrated;
}

function toHydratedThreadDraft(
Expand Down
Loading