feat(phase-06): add reference file contract and submit payload helper
This commit is contained in:
parent
b4fe531a0c
commit
99e0fe13fd
|
|
@ -468,6 +468,16 @@ export const PromptInputActionAddAttachments = ({
|
||||||
export type PromptInputMessage = {
|
export type PromptInputMessage = {
|
||||||
text: string;
|
text: string;
|
||||||
files: FileUIPart[];
|
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<
|
export type PromptInputProps = Omit<
|
||||||
|
|
@ -866,6 +876,8 @@ export type PromptInputTextareaProps = ComponentProps<
|
||||||
|
|
||||||
export const PromptInputTextarea = ({
|
export const PromptInputTextarea = ({
|
||||||
onChange,
|
onChange,
|
||||||
|
onKeyDown,
|
||||||
|
onPaste,
|
||||||
className,
|
className,
|
||||||
placeholder = "What would you like to know?",
|
placeholder = "What would you like to know?",
|
||||||
submitOnEnter = true,
|
submitOnEnter = true,
|
||||||
|
|
@ -876,6 +888,10 @@ export const PromptInputTextarea = ({
|
||||||
const [isComposing, setIsComposing] = useState(false);
|
const [isComposing, setIsComposing] = useState(false);
|
||||||
|
|
||||||
const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
|
const handleKeyDown: KeyboardEventHandler<HTMLTextAreaElement> = (e) => {
|
||||||
|
onKeyDown?.(e);
|
||||||
|
if (e.defaultPrevented) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
if (isComposing || e.nativeEvent.isComposing) {
|
if (isComposing || e.nativeEvent.isComposing) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -940,6 +956,10 @@ export const PromptInputTextarea = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePaste: ClipboardEventHandler<HTMLTextAreaElement> = (event) => {
|
const handlePaste: ClipboardEventHandler<HTMLTextAreaElement> = (event) => {
|
||||||
|
onPaste?.(event);
|
||||||
|
if (event.defaultPrevented) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const items = event.clipboardData?.items;
|
const items = event.clipboardData?.items;
|
||||||
|
|
||||||
if (!items) {
|
if (!items) {
|
||||||
|
|
|
||||||
|
|
@ -332,6 +332,8 @@ export interface FileInMessage {
|
||||||
size: number; // bytes
|
size: number; // bytes
|
||||||
path?: string; // virtual path, may not be set during upload
|
path?: string; // virtual path, may not be set during upload
|
||||||
status?: "uploading" | "uploaded";
|
status?: "uploading" | "uploaded";
|
||||||
|
ref_kind?: "mention";
|
||||||
|
ref_source?: "artifact" | "upload";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,50 @@
|
||||||
import assert from "node:assert/strict";
|
import assert from "node:assert/strict";
|
||||||
import test from "node:test";
|
import test from "node:test";
|
||||||
|
|
||||||
const { resolveThreadQueryIntent } = await import(
|
const { buildFilesForSubmit } = await import(
|
||||||
new URL("./utils.ts", import.meta.url).href
|
new URL("./submit-files.ts", import.meta.url).href
|
||||||
);
|
);
|
||||||
|
|
||||||
void test("uses /chats/new route as the only new-session signal", () => {
|
void test("buildFilesForSubmit keeps uploads and appends valid references", () => {
|
||||||
const intent = resolveThreadQueryIntent({
|
const result = buildFilesForSubmit(
|
||||||
pathThreadId: "new",
|
[
|
||||||
queryThreadId: "thread-from-query",
|
{
|
||||||
isNewRoute: true,
|
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(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");
|
||||||
});
|
});
|
||||||
|
|
||||||
assert.equal(intent.isNewThread, true);
|
void test("buildFilesForSubmit drops stale references without blocking submit", () => {
|
||||||
assert.equal(intent.showWelcomeStyle, true);
|
const result = buildFilesForSubmit(
|
||||||
assert.equal(intent.threadId, "thread-from-query");
|
[],
|
||||||
});
|
[
|
||||||
|
{
|
||||||
|
filename: "stale.md",
|
||||||
|
path: "/stale.md",
|
||||||
|
ref_kind: "mention",
|
||||||
|
ref_source: "upload",
|
||||||
|
stale: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
void test("prefers path thread id over query thread id when not on /new", () => {
|
assert.equal(result.staleCount, 1);
|
||||||
const intent = resolveThreadQueryIntent({
|
assert.equal(result.files.length, 0);
|
||||||
pathThreadId: "thread-from-path",
|
|
||||||
queryThreadId: "thread-from-query",
|
|
||||||
isNewRoute: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.equal(intent.isNewThread, false);
|
|
||||||
assert.equal(intent.threadId, "thread-from-path");
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue