Files
OpenHands/__tests__/api/workspace-upload-path.test.ts
T
e49f6d721e feat(chat): attachment UX, upload-as-file, and home/cloud submit fixes (#712)
* feat(chat): merge tools menu into plus button with file upload footer

Combine the chat tools dropdown with the + control and add an
Add Files and Images action with a paperclip icon at the bottom.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(chat): support clipboard image paste on home and conversation inputs

Read pasted screenshots from clipboard items, wire the home launcher
through the shared attachment upload flow, and send first messages with
attachments after creating a conversation.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(chat): per-pasted-image upload-as-file control on thumbnails

Replace the global checkbox with a circular upload button on clipboard
pasted images, track paste source in the store, and fix overlay stacking.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): polish pasted-image upload toggle styling and tooltips

Match the remove button size and corner inset, show a checkmark when
active with hover feedback, and swap the tooltip to "Do not upload as file".

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): add bottom padding below attachment thumbnails row

Match the chat input container's top inset so pasted images and files
are spaced evenly above and below the thumbnail strip.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): use surface grey for upload-as-file toggle and nudge position

Replace invalid primary tokens with oh-surface/oh-muted styling for both
states and raise the button slightly from the thumbnail corner.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(home): defer attachment sends until cloud start task is ready

Cloud conversation creation can return a provisional `task-{uuid}` URL
while the sandbox provisions. Sending messages or uploading files against
that id caused 422 UUID parsing errors on the home `/conversations` input.

- Queue attachments in memory keyed by start-task id when provisioning
- Flush uploads and the user message once `useTaskPolling` sees READY
- Always include typed text in the start request, even with attachments
- Add store and flush helper tests

Paste/drag on the home input were already wired; this fixes starting a
conversation with attachments (or text + attachments) on cloud backends.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Revert "fix(home): defer attachment sends until cloud start task is ready"

This reverts commit 9a443088ca5b14613c72b60459e26f1d8fb0f545.

* Reapply "fix(home): defer attachment sends until cloud start task is ready"

This reverts commit 7ba6a267cedaa67dbf08948ddcd00b9c5d6e269c.

* fix(chat): upload attachments into the conversation workspace

File uploads targeted read-only /workspace in Docker dev stacks and
cloud task flush used raw i18next before app init. Resolve the
conversation working_dir for upload paths and use the initialized
openhands i18n instance when flushing deferred attachments.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cloud): route home attachments through the runtime sandbox

Cloud file uploads and the first message were hitting the bundled
local agent-server and 404ing. Defer home attachments until the start
task is ready, upload via the provisioned runtime URL, and send events
through the cloud proxy.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): send home attachments in a single first message

Skip initial_message when starting with attachments and avoid enqueueing
optimistic duplicates after the attachment send already persisted the message.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cloud): show home submit as first message during task provisioning

Enqueue optimistic pending messages on cloud start-task routes so the chat
shows the user's message while the sandbox provisions, hide empty-state
suggestions during provisioning, and reassign pending bubbles to the real
conversation id when the task is ready.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): suppress spurious older-messages errors on new conversations

Skip auto-pagination when the initial history page is complete, on cloud
start-task routes, or while provisioning, and stop surfacing an error
banner when older events cannot be anchored.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): hide history skeleton when pending message is visible

Treat optimistic home-submit bubbles as loaded content so the feed
skeleton does not flash over the user's first message on new conversations.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(cloud): stop empty-state suggestions flashing after home submit

Keep pending bubbles linked across task-to-conversation redirect, reassign
them before paint on the real route, and tighten suggestion gating so the
Let's Start Building overlay does not flash during provisioning.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): show upload-as-file toggle on all attached images

Mark every attached image for the per-image upload control, not just
clipboard pastes, so file picker and drag-and-drop previews match pasted
screenshot behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): use file-plus icon for upload-as-file toggle

Replace the generic upload glyph with Lucide FilePlus so the per-image
control reads more clearly as "add this image as a workspace file."

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor: remove unrelated files

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-05-24 17:13:10 +07:00

54 lines
1.6 KiB
TypeScript

import { describe, expect, it, vi } from "vitest";
import {
buildWorkspaceUploadPath,
getSafeUploadFileName,
resolveConversationUploadWorkingDir,
toAbsoluteWorkspacePath,
} from "#/api/workspace-upload-path";
vi.mock("#/api/conversation-service/agent-server-conversation-service.api", () => ({
default: {
resolveConversationWorkingDir: vi.fn(
async (id: string) => `/workspace/project/${id.replace(/-/g, "")}`,
),
},
}));
describe("workspace-upload-path", () => {
it("normalizes relative working dirs to absolute paths", () => {
expect(toAbsoluteWorkspacePath("workspace/project")).toBe(
"/workspace/project",
);
expect(buildWorkspaceUploadPath("a.txt", "workspace/project")).toBe(
"/workspace/project/a.txt",
);
});
it("strips path segments from file names", () => {
expect(getSafeUploadFileName("../../evil.txt")).toBe("evil.txt");
expect(buildWorkspaceUploadPath("../../evil.txt", "/workspace/project")).toBe(
"/workspace/project/evil.txt",
);
});
it("prefers the active conversation workspace when ids match", async () => {
const dir = await resolveConversationUploadWorkingDir("conv-uuid", {
id: "conv-uuid",
workspace: { working_dir: "/workspace/project/custom" },
} as never);
expect(dir).toBe("/workspace/project/custom");
});
it("resolves per-conversation dirs for UUID ids", async () => {
const dir = await resolveConversationUploadWorkingDir(
"550e8400-e29b-41d4-a716-446655440000",
null,
);
expect(dir).toBe(
"/workspace/project/550e8400e29b41d4a716446655440000",
);
});
});