feat(phase-06): add reference file contract and submit payload helper

This commit is contained in:
肖应宇 2026-04-15 10:18:47 +08:00
parent b4fe531a0c
commit 99e0fe13fd
4 changed files with 108 additions and 19 deletions

View File

@ -468,6 +468,16 @@ export const PromptInputActionAddAttachments = ({
export type PromptInputMessage = {
text: string;
files: FileUIPart[];
references?: PromptInputReference[];
};
export type PromptInputReference = {
filename: string;
path?: string;
size?: number;
ref_kind: "mention";
ref_source: "artifact" | "upload";
stale?: boolean;
};
export type PromptInputProps = Omit<
@ -866,6 +876,8 @@ export type PromptInputTextareaProps = ComponentProps<
export const PromptInputTextarea = ({
onChange,
onKeyDown,
onPaste,
className,
placeholder = "What would you like to know?",
submitOnEnter = true,
@ -876,6 +888,10 @@ export const PromptInputTextarea = ({
const [isComposing, setIsComposing] = useState(false);
const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
onKeyDown?.(e);
if (e.defaultPrevented) {
return;
}
if (e.key === "Enter") {
if (isComposing || e.nativeEvent.isComposing) {
return;
@ -940,6 +956,10 @@ export const PromptInputTextarea = ({
};
const handlePaste: ClipboardEventHandler<HTMLTextAreaElement> = (event) => {
onPaste?.(event);
if (event.defaultPrevented) {
return;
}
const items = event.clipboardData?.items;
if (!items) {

View File

@ -332,6 +332,8 @@ export interface FileInMessage {
size: number; // bytes
path?: string; // virtual path, may not be set during upload
status?: "uploading" | "uploaded";
ref_kind?: "mention";
ref_source?: "artifact" | "upload";
}
/**

View File

@ -1,29 +1,50 @@
import assert from "node:assert/strict";
import test from "node:test";
const { resolveThreadQueryIntent } = await import(
new URL("./utils.ts", import.meta.url).href
const { buildFilesForSubmit } = await import(
new URL("./submit-files.ts", import.meta.url).href
);
void test("uses /chats/new route as the only new-session signal", () => {
const intent = resolveThreadQueryIntent({
pathThreadId: "new",
queryThreadId: "thread-from-query",
isNewRoute: true,
});
void test("buildFilesForSubmit keeps uploads and appends valid references", () => {
const result = buildFilesForSubmit(
[
{
filename: "uploaded.md",
size: 12,
virtual_path: "/mnt/user-data/uploads/uploaded.md",
},
],
[
{
filename: "artifact.md",
path: "/mnt/user-data/artifacts/artifact.md",
ref_kind: "mention",
ref_source: "artifact",
},
],
);
assert.equal(intent.isNewThread, true);
assert.equal(intent.showWelcomeStyle, true);
assert.equal(intent.threadId, "thread-from-query");
assert.equal(result.staleCount, 0);
assert.equal(result.files.length, 2);
assert.equal(result.files[0]?.filename, "uploaded.md");
assert.equal(result.files[1]?.ref_kind, "mention");
assert.equal(result.files[1]?.ref_source, "artifact");
});
void test("prefers path thread id over query thread id when not on /new", () => {
const intent = resolveThreadQueryIntent({
pathThreadId: "thread-from-path",
queryThreadId: "thread-from-query",
isNewRoute: false,
});
void test("buildFilesForSubmit drops stale references without blocking submit", () => {
const result = buildFilesForSubmit(
[],
[
{
filename: "stale.md",
path: "/stale.md",
ref_kind: "mention",
ref_source: "upload",
stale: true,
},
],
);
assert.equal(intent.isNewThread, false);
assert.equal(intent.threadId, "thread-from-path");
assert.equal(result.staleCount, 1);
assert.equal(result.files.length, 0);
});

View File

@ -0,0 +1,46 @@
import type { FileInMessage } from "../messages/utils";
import type { UploadedFileInfo } from "../uploads/api";
export type MentionReference = {
filename: string;
path?: string;
size?: number;
ref_kind: "mention";
ref_source: "artifact" | "upload";
stale?: boolean;
};
export function buildFilesForSubmit(
uploadedFileInfo: UploadedFileInfo[],
references: MentionReference[] = [],
) {
const uploadedFiles: FileInMessage[] = uploadedFileInfo.map((info) => ({
filename: info.filename,
size: info.size,
path: info.virtual_path,
status: "uploaded" as const,
}));
const referenceFiles: FileInMessage[] = [];
let staleCount = 0;
for (const reference of references) {
if (reference.stale) {
staleCount += 1;
continue;
}
referenceFiles.push({
filename: reference.filename,
size: reference.size ?? 0,
path: reference.path,
status: "uploaded" as const,
ref_kind: "mention",
ref_source: reference.ref_source,
});
}
return {
files: [...uploadedFiles, ...referenceFiles],
staleCount,
};
}