Archived
Rename attachment folder from .pi-web/paste to .pi-web/attachments
Attachments can be pasted, dropped, or uploaded, so 'paste' was too narrow. Rename the default folder and filename prefix accordingly and update the changeset reference.
This commit is contained in:
@@ -2,4 +2,4 @@
|
|||||||
"@jmfederico/pi-web": minor
|
"@jmfederico/pi-web": minor
|
||||||
---
|
---
|
||||||
|
|
||||||
Add image attachments to the chat composer. You can now paste (Ctrl/Cmd+V), drag-and-drop, or use the new Attach button to add PNG, JPEG, GIF, and WebP images to a message, with thumbnail previews and multi-image support. Attachments are delivered to the session using pi's native image format (images are auto-resized to pi's inline limits for full compatibility), and image content now renders inline in the transcript. A per-message delivery toggle also lets you instead save attachments into the workspace `.pi-web/paste` folder and reference them so the agent reads them with its own tools. The accepted HTTP upload size is now configurable via `PI_WEB_MAX_UPLOAD_BYTES` or the `maxUploadBytes` config value.
|
Add image attachments to the chat composer. You can now paste (Ctrl/Cmd+V), drag-and-drop, or use the new Attach button to add PNG, JPEG, GIF, and WebP images to a message, with thumbnail previews and multi-image support. Attachments are delivered to the session using pi's native image format (images are auto-resized to pi's inline limits for full compatibility), and image content now renders inline in the transcript. A per-message delivery toggle also lets you instead save attachments into the workspace `.pi-web/attachments` folder and reference them so the agent reads them with its own tools. The accepted HTTP upload size is now configurable via `PI_WEB_MAX_UPLOAD_BYTES` or the `maxUploadBytes` config value.
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ export class PromptEditor extends LitElement {
|
|||||||
<label class="attachment-delivery" title="How attachments are delivered to the agent">
|
<label class="attachment-delivery" title="How attachments are delivered to the agent">
|
||||||
<select .value=${this.attachmentDelivery} @change=${(event: Event) => { this.changeDelivery(event); }}>
|
<select .value=${this.attachmentDelivery} @change=${(event: Event) => { this.changeDelivery(event); }}>
|
||||||
<option value="inline">Attach to message</option>
|
<option value="inline">Attach to message</option>
|
||||||
<option value="folder">Save to .pi-web/paste</option>
|
<option value="folder">Save to .pi-web/attachments</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
` : null}
|
` : null}
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ describe("SessionController", () => {
|
|||||||
const attachments: PromptAttachment[] = [{ kind: "image", mimeType: "image/png", data: "QUJD", name: "shot.png" }];
|
const attachments: PromptAttachment[] = [{ kind: "image", mimeType: "image/png", data: "QUJD", name: "shot.png" }];
|
||||||
const api: typeof defaultApi = {
|
const api: typeof defaultApi = {
|
||||||
...defaultApi,
|
...defaultApi,
|
||||||
saveAttachments: (_session, sent) => { savedCalledWith = sent; return Promise.resolve([{ path: ".pi-web/paste/shot.png", mimeType: "image/png", size: 3 }]); },
|
saveAttachments: (_session, sent) => { savedCalledWith = sent; return Promise.resolve([{ path: ".pi-web/attachments/shot.png", mimeType: "image/png", size: 3 }]); },
|
||||||
prompt: (_session, text, _behavior, _machineId, sentAttachments) => { promptText = text; promptAttachments = sentAttachments; return Promise.resolve({ accepted: true }); },
|
prompt: (_session, text, _behavior, _machineId, sentAttachments) => { promptText = text; promptAttachments = sentAttachments; return Promise.resolve({ accepted: true }); },
|
||||||
};
|
};
|
||||||
const controller = new SessionController(
|
const controller = new SessionController(
|
||||||
@@ -283,7 +283,7 @@ describe("SessionController", () => {
|
|||||||
await controller.send("check this", undefined, attachments, "folder");
|
await controller.send("check this", undefined, attachments, "folder");
|
||||||
|
|
||||||
expect(savedCalledWith).toEqual(attachments);
|
expect(savedCalledWith).toEqual(attachments);
|
||||||
expect(promptText).toBe("check this\n\[email protected]/paste/shot.png");
|
expect(promptText).toBe("check this\n\[email protected]/attachments/shot.png");
|
||||||
expect(promptAttachments).toBeUndefined();
|
expect(promptAttachments).toBeUndefined();
|
||||||
expect(state.sendingPrompts).toEqual({});
|
expect(state.sendingPrompts).toEqual({});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,12 +30,12 @@ describe("saveAttachmentsToWorkspace", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(saved).toHaveLength(2);
|
expect(saved).toHaveLength(2);
|
||||||
expect(saved[0]?.path.startsWith(`${DEFAULT_ATTACHMENT_FOLDER}/paste-`)).toBe(true);
|
expect(saved[0]?.path.startsWith(`${DEFAULT_ATTACHMENT_FOLDER}/attachment-`)).toBe(true);
|
||||||
expect(saved[0]?.path.endsWith(".png")).toBe(true);
|
expect(saved[0]?.path.endsWith(".png")).toBe(true);
|
||||||
expect(saved[1]?.path.endsWith(".webp")).toBe(true);
|
expect(saved[1]?.path.endsWith(".webp")).toBe(true);
|
||||||
expect(saved[0]?.size).toBe(pngBytes.byteLength);
|
expect(saved[0]?.size).toBe(pngBytes.byteLength);
|
||||||
|
|
||||||
const folderEntries = await readdir(join(workspace, ".pi-web", "paste"));
|
const folderEntries = await readdir(join(workspace, ".pi-web", "attachments"));
|
||||||
expect(folderEntries).toHaveLength(2);
|
expect(folderEntries).toHaveLength(2);
|
||||||
|
|
||||||
const firstPath = saved[0]?.path ?? "";
|
const firstPath = saved[0]?.path ?? "";
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { resolveParentInsideWorkspace } from "../workspaces/pathSafety.js";
|
|||||||
* Default workspace-relative folder used when saving pasted/dropped
|
* Default workspace-relative folder used when saving pasted/dropped
|
||||||
* attachments for the agent to read with its own tools.
|
* attachments for the agent to read with its own tools.
|
||||||
*/
|
*/
|
||||||
export const DEFAULT_ATTACHMENT_FOLDER = ".pi-web/paste";
|
export const DEFAULT_ATTACHMENT_FOLDER = ".pi-web/attachments";
|
||||||
|
|
||||||
export interface InlineImage {
|
export interface InlineImage {
|
||||||
image: ImageContent;
|
image: ImageContent;
|
||||||
@@ -42,7 +42,7 @@ export async function attachmentsToInlineImages(attachments: PromptAttachment[])
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SaveAttachmentsOptions {
|
export interface SaveAttachmentsOptions {
|
||||||
/** Workspace-relative folder to write into. Defaults to `.pi-web/paste`. */
|
/** Workspace-relative folder to write into. Defaults to `.pi-web/attachments`. */
|
||||||
folder?: string;
|
folder?: string;
|
||||||
/** Clock injection for deterministic tests. */
|
/** Clock injection for deterministic tests. */
|
||||||
now?: () => Date;
|
now?: () => Date;
|
||||||
@@ -66,7 +66,7 @@ export async function saveAttachmentsToWorkspace(
|
|||||||
const saved: SavedPromptAttachment[] = [];
|
const saved: SavedPromptAttachment[] = [];
|
||||||
for (const [index, attachment] of attachments.entries()) {
|
for (const [index, attachment] of attachments.entries()) {
|
||||||
const bytes = Buffer.from(attachment.data, "base64");
|
const bytes = Buffer.from(attachment.data, "base64");
|
||||||
const filename = `paste-${stamp}-${String(index + 1)}.${extensionForImageMimeType(attachment.mimeType)}`;
|
const filename = `attachment-${stamp}-${String(index + 1)}.${extensionForImageMimeType(attachment.mimeType)}`;
|
||||||
const relativePath = `${folder}/${filename}`;
|
const relativePath = `${folder}/${filename}`;
|
||||||
await writeFile(join(folderTarget, filename), bytes);
|
await writeFile(join(folderTarget, filename), bytes);
|
||||||
saved.push({ path: relativePath, mimeType: attachment.mimeType, size: bytes.byteLength });
|
saved.push({ path: relativePath, mimeType: attachment.mimeType, size: bytes.byteLength });
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ class CapturingRouteSessionService extends PiSessionService {
|
|||||||
override saveAttachments(_lookup: string | PiSessionRef, attachments: unknown, folder?: string) {
|
override saveAttachments(_lookup: string | PiSessionRef, attachments: unknown, folder?: string) {
|
||||||
const list = Array.isArray(attachments) ? attachments : [];
|
const list = Array.isArray(attachments) ? attachments : [];
|
||||||
return Promise.resolve(list.map((attachment: { mimeType: string; data: string; name?: string }) => ({
|
return Promise.resolve(list.map((attachment: { mimeType: string; data: string; name?: string }) => ({
|
||||||
path: `${folder ?? ".pi-web/paste"}/${attachment.name ?? "file.png"}`,
|
path: `${folder ?? ".pi-web/attachments"}/${attachment.name ?? "file.png"}`,
|
||||||
mimeType: attachment.mimeType,
|
mimeType: attachment.mimeType,
|
||||||
size: Buffer.from(attachment.data, "base64").byteLength,
|
size: Buffer.from(attachment.data, "base64").byteLength,
|
||||||
})));
|
})));
|
||||||
|
|||||||
Reference in New Issue
Block a user