Skip to content

Commit 56d3a8d

Browse files
Jing-yilinclaude
andcommitted
Hand the agent a file for anything that is not a picture
A video, a note or a drawing the person placed was drawn and sent as a picture, as a board was. Now only pictures go as pictures; the rest keep their drawing for the tile and point the agent at the file behind them, `files/…` or `canvas.json#<id>`, in the project they were attached from. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent d1b1b6a commit 56d3a8d

6 files changed

Lines changed: 58 additions & 47 deletions

File tree

‎RELEASE-NOTES.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ The app, its skills and the toolkit carry one version.
1919

2020
Everything below is on `main` and reaches no install until a version is cut.
2121

22-
- **A mockup goes to the agent as its file.** Its **+** still shows a picture
23-
of it in the chat, but the agent is handed the board's `.html` path rather
24-
than that picture, since the file is what it reads and changes.
22+
- **Only pictures go to the agent as pictures.** The **+** on a board, a
23+
video or a note still shows a picture of it in the chat, but the agent is
24+
handed the file behind it rather than that picture, since the file is what
25+
it can read and change.
2526

2627
## v1.7.0
2728

‎canvas/server/agent.ts‎

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -311,13 +311,15 @@ export function createAgentServer(options: {
311311
)
312312
return send(400, "bad image data");
313313
if (
314-
i.page !== undefined &&
315-
(typeof i.page !== "object" ||
314+
i.reference !== undefined &&
315+
(typeof i.reference !== "object" ||
316316
!["project", "community"].every(
317-
(k) => i.page[k] === undefined || typeof i.page[k] === "string",
317+
(k) =>
318+
i.reference[k] === undefined ||
319+
typeof i.reference[k] === "string",
318320
))
319321
)
320-
return send(400, "bad image page");
322+
return send(400, "bad image reference");
321323
}
322324
// The body cap above is the panel's limit in base64; a client that is not the
323325
// panel meets the limit itself here, in the bytes the files come out as.
@@ -507,20 +509,24 @@ export function createAgentServer(options: {
507509
name: string;
508510
type: string;
509511
data: string;
510-
page?: { project?: string; community?: string };
512+
reference?: { project?: string; community?: string };
511513
}) => {
512-
// A mockup's picture is kept for the panel, and the agent is pointed at its
513-
// file, `<slug>/<file>.html` in the canvases of the project it was attached
514-
// from, which need not be the one it is sent from. A community project's is not
515-
// on this machine, so it keeps the name `sp fetch` finds it by.
516-
const [slug, file, ...rest] = i.name.split("/");
517-
const from = i.page?.project && projects().get(i.page.project);
514+
// Anything but a picture — a board, a video, a note — keeps its picture for the
515+
// panel, and the agent is pointed at its file, `<slug>/<path>` in the canvases of
516+
// the project it was attached from, which need not be the one it is sent from. A
517+
// community project's is not on this machine, so it keeps the name `sp fetch`
518+
// finds it by.
519+
const [slug, ...rest] = i.name.split("/");
520+
const ref = i.reference;
521+
const from = ref?.project && projects().get(ref.project);
518522
const local =
519-
i.page?.community === undefined &&
520-
(i.page?.project === undefined || from) &&
521-
rest.length === 0 &&
523+
ref?.community === undefined &&
524+
(ref?.project === undefined || from) &&
522525
SAFE_NAME.test(slug) &&
523-
SAFE_NAME.test(file ?? "");
526+
rest.length > 0 &&
527+
rest.every(
528+
(s) => s && !s.startsWith(".") && !s.includes("\\"),
529+
);
524530
return {
525531
...i,
526532
path: picture(
@@ -529,19 +535,19 @@ export function createAgentServer(options: {
529535
i.type,
530536
Buffer.from(i.data, "base64"),
531537
),
532-
page:
533-
i.page &&
538+
reference:
539+
ref &&
534540
(local
535541
? path.join(
536542
folderOf(
537543
from ? path.join(from, CANVASES) : examplesDir,
538544
examplesDir,
539545
slug,
540546
),
541-
file,
547+
...rest,
542548
)
543-
: i.page.community
544-
? `${i.name} of the community project ${i.page.community}`
549+
: ref.community
550+
? `${i.name} of the community project ${ref.community}`
545551
: i.name),
546552
};
547553
},

‎canvas/src/ChatPanel.tsx‎

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,9 @@ const OPEN_KEY = "sp-chat-open";
8787

8888
/**
8989
* What the canvas hands the chat panel when the button is pressed (canvasAttach.tsx): a picture
90-
* to attach to the message, or the reason none was. A board comes over as a picture too, for its
91-
* tile only: the agent is handed the board's file, which its name says, and the panel shows that
92-
* name under the tile. And the start
90+
* to attach to the message, or the reason none was. A board, a video or a note comes over as a
91+
* picture too, for its tile only: the agent is handed the file its name says, and the panel shows
92+
* that name under the tile. And the start
9393
* of a message, from the strip's "+" (CanvasStrip.tsx), because a canvas is only ever the
9494
* agent's work, and a folder with no boards in it is not one. And a whole message, sent as it is,
9595
* from the new-project dialog (AppShell.tsx), which starts the agent defining the product.
@@ -105,8 +105,9 @@ const OPEN_KEY = "sp-chat-open";
105105
export const CANVAS_ATTACH = "sp:canvas-attach";
106106

107107
export type CanvasAttachDetail =
108-
/** `page` for a mockup, whose drawing is only the tile's: the agent is handed its file. */
109-
| { kind: "board"; name: string; src: string; page?: true }
108+
/** `reference` for anything but a picture, whose drawing is only the tile's: the agent is
109+
* pointed at its file instead. */
110+
| { kind: "board"; name: string; src: string; reference?: true }
110111
| { kind: "image"; file: File }
111112
| { kind: "error"; message: string }
112113
| { kind: "draft"; text: string }
@@ -251,9 +252,9 @@ interface Attached {
251252
url: string;
252253
/** A board still being drawn, with no `url` yet, or one whose drawing failed. */
253254
state?: "pending" | "failed";
254-
/** A mockup, and the project it was attached from, which the panel may since have left: the
255-
* agent gets its file there rather than this picture of it (agents.ts). */
256-
page?: { project?: string; community?: string };
255+
/** Anything but a picture, and the project it was attached from, which the panel may since
256+
* have left: the agent gets its file there rather than this picture of it (agents.ts). */
257+
reference?: { project?: string; community?: string };
257258
}
258259

259260
/**
@@ -718,7 +719,7 @@ export function ChatPanel(props: {
718719
type: r.file.type,
719720
size: r.file.size,
720721
url: r.url,
721-
page: t.page,
722+
reference: t.reference,
722723
}
723724
: t;
724725
});
@@ -764,7 +765,7 @@ export function ChatPanel(props: {
764765
* drawing asked of the server, to land in that tile. Asked for again it keeps the tile it has —
765766
* one already there or on its way is only named again, and one that failed is drawn again.
766767
*/
767-
const addBoard = (name: string, src: string, page?: true) => {
768+
const addBoard = (name: string, src: string, reference?: true) => {
768769
let tile = tray.current.find((t) => t.name === name);
769770
if (!tile && tray.current.length >= MAX_IMAGES)
770771
return setSendError(
@@ -785,7 +786,7 @@ export function ChatPanel(props: {
785786
size: 0,
786787
url: "",
787788
state: "pending",
788-
page: page && { project, community },
789+
reference: reference && { project, community },
789790
};
790791
const next = tile;
791792
tray.current = [...tray.current.filter((t) => t.n !== next.n), next].sort(
@@ -827,7 +828,7 @@ export function ChatPanel(props: {
827828
// that is still being read.
828829
if (detail.kind === "board") {
829830
adds.current = adds.current.then(() =>
830-
addBoard(detail.name, detail.src, detail.page),
831+
addBoard(detail.name, detail.src, detail.reference),
831832
);
832833
return;
833834
}
@@ -945,12 +946,12 @@ export function ChatPanel(props: {
945946
agent,
946947
model,
947948
effort,
948-
images: attached.map(({ n, name, type, url, page }) => ({
949+
images: attached.map(({ n, name, type, url, reference }) => ({
949950
n,
950951
name,
951952
type,
952953
data: url.slice(url.indexOf(",") + 1),
953-
page,
954+
reference,
954955
})),
955956
}),
956957
});

‎canvas/src/agents.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ describe("AGENTS", () => {
9494
type: "image/png",
9595
data: "CCC",
9696
path: "/tmp/sp-chat-r/2.png",
97-
page: "/proj/canvases/shop/01-home.html",
97+
reference: "/proj/canvases/shop/01-home.html",
9898
};
9999
expect(
100100
JSON.parse(def("claude").stdin("tighten #2", "P", [board])).message

‎canvas/src/agents.ts‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,10 @@ export interface AgentImage {
8080
/** Where the server wrote it, for an agent that takes files rather than bytes; gone once
8181
* that agent has exited. */
8282
path: string;
83-
/** A mockup's HTML file, when the picture is only the panel's drawing of one: the agent is
84-
* pointed at the file and not handed the picture, since the file is what it reads and edits. */
85-
page?: string;
83+
/** The file behind it — a board's HTML, a video, a canvas.json record — when the picture is
84+
* only the panel's drawing of it: the agent is pointed at the file, which is what it can read
85+
* and change, and not handed the picture. */
86+
reference?: string;
8687
}
8788

8889
/** What the composer chose, handed to `args`. An empty string means the CLI decides. */
@@ -203,8 +204,8 @@ export const AGENTS: AgentDef[] = [
203204
// terminal, so the number arrives as something already read rather than a local convention.
204205
stdin: (message, _preamble, images) => {
205206
const blocks = images.flatMap((i) =>
206-
i.page
207-
? [{ type: "text", text: `[Image #${i.n}] ${i.page}` }]
207+
i.reference
208+
? [{ type: "text", text: `[Image #${i.n}] ${i.reference}` }]
208209
: [
209210
{ type: "text", text: `[Image #${i.n}] ${i.name}` },
210211
{
@@ -315,7 +316,7 @@ export const AGENTS: AgentDef[] = [
315316
stdin: (message, preamble, images) =>
316317
[
317318
preamble,
318-
images.map((i) => `[Image #${i.n}] ${i.page ?? i.path}`).join("\n"),
319+
images.map((i) => `[Image #${i.n}] ${i.reference ?? i.path}`).join("\n"),
319320
message,
320321
]
321322
.filter(Boolean)

‎canvas/src/canvasAttach.tsx‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,17 +54,19 @@ async function attach(editor: Editor, target: TLShape) {
5454
`&w=${Math.max(1, Math.round(w * scale))}&h=${Math.max(1, Math.round(h * scale))}`,
5555
window.location.href,
5656
).href;
57-
return dispatchAttach({ kind: "board", name, src, page: true });
57+
return dispatchAttach({ kind: "board", name, src, reference: true });
5858
}
59-
// One of the person's own: whatever it is, the agent gets a picture of it, named by where it
60-
// reads the thing itself (canvasContent.ts).
59+
// One of the person's own, drawn for the tile and named by where the agent reads the thing
60+
// itself (canvasContent.ts). A picture goes over as one; anything else — a video, a note, a
61+
// drawing — is a file the agent is pointed at, as a board is.
6162
const slug = personsShape(editor, target);
6263
if (slug) {
6364
const { blob } = await editor.toImage([target.id], { format: "png" });
6465
return dispatchAttach({
6566
kind: "board",
6667
name: personsShapeName(editor, target, slug),
6768
src: URL.createObjectURL(blob),
69+
reference: target.type === "image" ? undefined : true,
6870
});
6971
}
7072
const shape = target as TLImageShape;

0 commit comments

Comments
 (0)