diff --git a/.lane/memory/apps/app/src/attachments.ts/01M53FDJP7K240NMG7Q4E5B154-pdf-js-page-text-must-be-rea.md b/.lane/memory/apps/app/src/attachments.ts/01M53FDJP7K240NMG7Q4E5B154-pdf-js-page-text-must-be-rea.md new file mode 100644 index 00000000..986ec64e --- /dev/null +++ b/.lane/memory/apps/app/src/attachments.ts/01M53FDJP7K240NMG7Q4E5B154-pdf-js-page-text-must-be-rea.md @@ -0,0 +1,12 @@ +--- +id: 01M53FDJP7K240NMG7Q4E5B154 +anchor: function text +created: 2026-10-16T23:05:41Z +norm: '1' +sig: 11c5a30588ada982 +body_hash: b3cafd9a4f9acb15 +raw_hash: cce4e0f9162ae184 +lines: 31-40 +--- + +pdf.js page text must be read through the stream's reader; WebKit's ReadableStream isn't async-iterable, so getTextContent throws in the desktop webview diff --git a/.lane/memory/apps/app/src/attachments.ts/01M53FDJPMP928VPQA5JD1CVHC-detect-pdfs-by-mime-type-onl.md b/.lane/memory/apps/app/src/attachments.ts/01M53FDJPMP928VPQA5JD1CVHC-detect-pdfs-by-mime-type-onl.md new file mode 100644 index 00000000..0ff3cf44 --- /dev/null +++ b/.lane/memory/apps/app/src/attachments.ts/01M53FDJPMP928VPQA5JD1CVHC-detect-pdfs-by-mime-type-onl.md @@ -0,0 +1,12 @@ +--- +id: 01M53FDJPMP928VPQA5JD1CVHC +anchor: function read +created: 2026-10-16T23:05:41Z +norm: '1' +sig: e902509aeec5b6f5 +body_hash: 08302543447ad2e9 +raw_hash: c349d3cc21691287 +lines: 72-83 +--- + +detect PDFs by MIME type only; a withdrawn PDF returns to the composer as its extracted text typed text/plain under its .pdf name diff --git a/.lane/memory/package.json/01M4X5AF9TCS4B752Z16B7W75M-oxlint-applies-the-shared-gi.md b/.lane/memory/package.json/01M4X5AF9TCS4B752Z16B7W75M-oxlint-applies-the-shared-gi.md index 854d64f2..d1ceec49 100644 --- a/.lane/memory/package.json/01M4X5AF9TCS4B752Z16B7W75M-oxlint-applies-the-shared-gi.md +++ b/.lane/memory/package.json/01M4X5AF9TCS4B752Z16B7W75M-oxlint-applies-the-shared-gi.md @@ -4,9 +4,9 @@ anchor: '@file' created: 2026-10-14T12:13:47Z norm: '1' sig: 021fb596db81e6d0 -body_hash: d63795af6ea9f4f1 -raw_hash: 3c9928c1a31b3a70 -vouched: 2026-10-15T23:58:07Z +body_hash: 98e1d861f9b08883 +raw_hash: 3afc751c85317c2d +vouched: 2026-10-16T23:05:45Z lines: 1-92 --- diff --git a/.lane/memory/package.json/01M4ZHQR0KZMW3P78F7T8XMEXX-desktop-tools-must-stay-at-o.md b/.lane/memory/package.json/01M4ZHQR0KZMW3P78F7T8XMEXX-desktop-tools-must-stay-at-o.md index 2ddc4e5f..e7403d81 100644 --- a/.lane/memory/package.json/01M4ZHQR0KZMW3P78F7T8XMEXX-desktop-tools-must-stay-at-o.md +++ b/.lane/memory/package.json/01M4ZHQR0KZMW3P78F7T8XMEXX-desktop-tools-must-stay-at-o.md @@ -4,9 +4,9 @@ anchor: '@file' created: 2026-10-15T10:29:13Z norm: '1' sig: 021fb596db81e6d0 -body_hash: d63795af6ea9f4f1 -raw_hash: 3c9928c1a31b3a70 -vouched: 2026-10-15T23:58:07Z +body_hash: 98e1d861f9b08883 +raw_hash: 3afc751c85317c2d +vouched: 2026-10-16T23:05:45Z lines: 1-94 --- diff --git a/.lane/memory/packages/ui/src/components/timeline/layout.ts/01M53FDJQ4C0BGDGXNAY5GK09T-a-file-block-s-cache-key-inc.md b/.lane/memory/packages/ui/src/components/timeline/layout.ts/01M53FDJQ4C0BGDGXNAY5GK09T-a-file-block-s-cache-key-inc.md new file mode 100644 index 00000000..1d67066a --- /dev/null +++ b/.lane/memory/packages/ui/src/components/timeline/layout.ts/01M53FDJQ4C0BGDGXNAY5GK09T-a-file-block-s-cache-key-inc.md @@ -0,0 +1,12 @@ +--- +id: 01M53FDJQ4C0BGDGXNAY5GK09T +anchor: '@file' +created: 2026-10-16T23:05:41Z +norm: '1' +sig: ce515648b5bedef2 +body_hash: fb0394625d6bac4e +raw_hash: 1a90d920d15ad713 +lines: 1-912 +--- + +a file block's cache key includes its text, since the block opens that text and names can repeat across messages diff --git a/apps/app/package.json b/apps/app/package.json index a166836b..31bc9cae 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -19,6 +19,7 @@ "@xterm/addon-fit": "catalog:ui", "@xterm/xterm": "catalog:ui", "electrobun": "catalog:desktop", + "pdfjs-dist": "catalog:ui", "react": "catalog:ui", "react-dom": "catalog:ui" }, diff --git a/apps/app/src/attachments.ts b/apps/app/src/attachments.ts index e92d96e2..595051c6 100644 --- a/apps/app/src/attachments.ts +++ b/apps/app/src/attachments.ts @@ -1,3 +1,5 @@ +import type { PDFPageProxy } from "pdfjs-dist"; + import type { Image, TextFile } from "@ace/channel/protocol"; import type { Attachment } from "@ace/ui"; @@ -5,6 +7,8 @@ import type { Attachment } from "@ace/ui"; const BUDGET = 1_400_000; const EDGE = 2000; +type TextContent = Awaited>; + async function encode(url: string, edge: number, quality: number): Promise { const bitmap = await createImageBitmap(await (await fetch(url)).blob()); const scale = Math.min(1, edge / Math.max(bitmap.width, bitmap.height)); @@ -23,18 +27,63 @@ async function encode(url: string, edge: number, quality: number): Promise { + const reader: ReadableStreamDefaultReader = page.streamTextContent().getReader(); + let lines = ""; + for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) { + for (const item of chunk.value.items) { + if ("str" in item) lines += item.str + (item.hasEOL ? "\n" : ""); + } + } + return lines; +} + +/** + * Providers' document inputs aren't in pi's message format, so a PDF goes to models as its pages' + * text, like any text file. Scanned pages have no text and can't be read this way. + */ +async function pdf(file: File): Promise { + const [{ getDocument, GlobalWorkerOptions }, worker] = await Promise.all([ + import("pdfjs-dist"), + import("pdfjs-dist/build/pdf.worker.min.mjs?url"), + ]); + GlobalWorkerOptions.workerSrc = worker.default; + const task = getDocument({ data: new Uint8Array(await file.arrayBuffer()) }); + try { + const doc = await task.promise.catch(() => { + throw new Error(`${file.name} couldn't be read as a PDF`); + }); + const pages: string[] = []; + for (let number = 1; number <= doc.numPages; number++) { + pages.push(await text(await doc.getPage(number))); + } + const joined = pages.join("\n\n").trim(); + if (!joined) { + throw new Error(`${file.name} has no text to read; scanned PDFs can't be attached`); + } + return { name: file.name, text: joined }; + } finally { + await task.destroy(); + } +} + /** Models read files as text, so only valid UTF-8 without NUL bytes can be sent. */ async function read(file: File): Promise { + // A withdrawn PDF returns to the composer as its text, typed text/plain. + if (file.type === "application/pdf") return pdf(file); const decoder = new TextDecoder("utf-8", { fatal: true }); const text = await file.arrayBuffer().then((bytes) => decoder.decode(bytes)).catch(() => "\0"); if (text.includes("\0")) { - throw new Error(`${file.name} isn't a text file; only images and text files can be attached`); + throw new Error( + `${file.name} isn't a text file; only images, text files, and PDFs can be attached`, + ); } return { name: file.name, text }; } /** - * Read attached text files, then re-encode attached images as JPEG, shrinking until the message + * Read attached text files and PDFs, then re-encode attached images as JPEG, shrinking until the message * fits the channel's limit. Re-encoding also strips metadata such as location from photos. */ export async function attachments( diff --git a/apps/app/src/timeline.ts b/apps/app/src/timeline.ts index ad6c64fc..7211da3c 100644 --- a/apps/app/src/timeline.ts +++ b/apps/app/src/timeline.ts @@ -161,11 +161,12 @@ export function toEvents( type: "image" as const, image: `data:${image.mimeType};base64,${image.data}`, })), - // The block shows only the name; contents stay in the transcript, not a URL. + // Contents travel in the transcript, not at a URL; the block opens them. ...(item.files || []).map((file) => ({ type: "file" as const, name: file.name, file: "", + text: file.text, })), ], } as Event; diff --git a/bun.lock b/bun.lock index c651bb1a..9ba89d22 100644 --- a/bun.lock +++ b/bun.lock @@ -21,6 +21,7 @@ "@xterm/addon-fit": "catalog:ui", "@xterm/xterm": "catalog:ui", "electrobun": "catalog:desktop", + "pdfjs-dist": "catalog:ui", "react": "catalog:ui", "react-dom": "catalog:ui", }, @@ -207,6 +208,7 @@ "lucide-static": "1.48.0", "markdown-it": "14.1.1", "motion": "12.23.24", + "pdfjs-dist": "6.3.289", "prosemirror-commands": "1.7.1", "prosemirror-highlight": "0.15.1", "prosemirror-history": "1.5.0", @@ -557,6 +559,30 @@ "@mswjs/interceptors": ["@mswjs/interceptors@0.41.9", "", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w=="], + "@napi-rs/canvas": ["@napi-rs/canvas@1.0.11", "", { "optionalDependencies": { "@napi-rs/canvas-android-arm64": "1.0.11", "@napi-rs/canvas-darwin-arm64": "1.0.11", "@napi-rs/canvas-darwin-x64": "1.0.11", "@napi-rs/canvas-linux-arm-gnueabihf": "1.0.11", "@napi-rs/canvas-linux-arm64-gnu": "1.0.11", "@napi-rs/canvas-linux-arm64-musl": "1.0.11", "@napi-rs/canvas-linux-riscv64-gnu": "1.0.11", "@napi-rs/canvas-linux-x64-gnu": "1.0.11", "@napi-rs/canvas-linux-x64-musl": "1.0.11", "@napi-rs/canvas-win32-arm64-msvc": "1.0.11", "@napi-rs/canvas-win32-x64-msvc": "1.0.11" } }, "sha512-vt5tog1cr+QKadIddGe+3LrcX0EmjEd0izF717Uwf92mBmgYfq2XRmL2r4bZU40spB69exxhrBn9I91HYEr/7w=="], + + "@napi-rs/canvas-android-arm64": ["@napi-rs/canvas-android-arm64@1.0.11", "", { "os": "android", "cpu": "arm64" }, "sha512-dtJS4FQEfH9OwSYLtMrEnj7T8DzBbJc/97LI/OkiWOYpW/iW5t6R/gcxVrUXS8l45gCVpwMufgucRe/pe0ISqA=="], + + "@napi-rs/canvas-darwin-arm64": ["@napi-rs/canvas-darwin-arm64@1.0.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-J/JylWoJTG8OQqZaiqly4IODyAxNofUlZo0wtSBJtHk6AdymbdIkPynAM1JvQaZpPaG5h+msk9QuK8y4i1ooqw=="], + + "@napi-rs/canvas-darwin-x64": ["@napi-rs/canvas-darwin-x64@1.0.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-nlecCk3xy06eAGC7krZtzNDm9UNTHKuHQYK8ZYegdjzgSL8anGt/eLJZm86OWaNgZkwQwvDqfSmG8Nqe6MPZNQ=="], + + "@napi-rs/canvas-linux-arm-gnueabihf": ["@napi-rs/canvas-linux-arm-gnueabihf@1.0.11", "", { "os": "linux", "cpu": "arm" }, "sha512-DKImmG+sTPolE0kGPsYPBxTtOjb3INALp32O2lW2CfSr0bAzSEKKuu+GrQbtilClyoPOahUErS3OrznFDraoWA=="], + + "@napi-rs/canvas-linux-arm64-gnu": ["@napi-rs/canvas-linux-arm64-gnu@1.0.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-55sykACGENj5j1rhj+81AO8JbR9HYgGjRZ7/H+pNr4VE1DvINKYNMTcutk8ZmWxk276jAQvUKJ3+Kdj9GXU3lQ=="], + + "@napi-rs/canvas-linux-arm64-musl": ["@napi-rs/canvas-linux-arm64-musl@1.0.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-6Jz6jys17yXydO+WdJgu7JhoB4K7/w9sWHByzzTmLjQghUx3TDMWvLv+UcCdnWhg4oprET67EMymnrETdeGNAg=="], + + "@napi-rs/canvas-linux-riscv64-gnu": ["@napi-rs/canvas-linux-riscv64-gnu@1.0.11", "", { "os": "linux", "cpu": "none" }, "sha512-lf2XeEebWiUYVOVEjLXxF7dfXrBBSEOBn9AqBh39VYXrPt5cDqI+1t58IFSjn8npqkg5jIOs8TQbjKABbFW1gQ=="], + + "@napi-rs/canvas-linux-x64-gnu": ["@napi-rs/canvas-linux-x64-gnu@1.0.11", "", { "os": "linux", "cpu": "x64" }, "sha512-iDapw7nFSE5tsmQxcH2+KotJhyfVPmHd6vUOQN/Em+8WNbkZJwJ/rDowlJMFnMl0SYSDSfgKCNUGi6Sn5++Smg=="], + + "@napi-rs/canvas-linux-x64-musl": ["@napi-rs/canvas-linux-x64-musl@1.0.11", "", { "os": "linux", "cpu": "x64" }, "sha512-26q26AyITo3CGZX4HWFIMgBMA1lu3YN07gxzBm4tW6Hglq/OVyTP50kDrmWuKybCX6dh3m8Ly+c7YhI+sqCgCQ=="], + + "@napi-rs/canvas-win32-arm64-msvc": ["@napi-rs/canvas-win32-arm64-msvc@1.0.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-cUO96IdsfvIj2S+2PNcDSkTcLhjPj8pIOEWRV1Kj70MOP4wnE9zSk7N4C1EKHlFGRcgdR5AkbnBZj0dfHK9nbQ=="], + + "@napi-rs/canvas-win32-x64-msvc": ["@napi-rs/canvas-win32-x64-msvc@1.0.11", "", { "os": "win32", "cpu": "x64" }, "sha512-DFYp/ZQRdKunI/LPc0UkRXG1vlNjJKOJf8O8Ctb8+wm2Nt2ELg9Eamc31D2VoakkfGo8L+z0Iejz6WtFbQwyGw=="], + "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -1413,6 +1439,8 @@ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "pdfjs-dist": ["pdfjs-dist@6.3.289", "", { "optionalDependencies": { "@napi-rs/canvas": "^1.0.0" } }, "sha512-ZHjSVpDa3D6izMq8/04lvkhkATUmL9px6ChPaXc1k6nU2Mrhlg1/7F0bdUqCwUjw3NsPTfPZsMDUU6ZIcRaeQw=="], + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], diff --git a/docs/architecture.md b/docs/architecture.md index 24e1d671..7c18bd9f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -131,6 +131,15 @@ is one pi conversation in that Session. edit or deletion of an entry wins. Agent replies to the earlier text stay in the transcript and in later context, and an edit never reruns the agent. Watches that opt in receive `edited` live; every watch replays a message with its newest text and when it was edited. +- **Attachments live in the message.** Images and files travel on `say` and `ask` and are stored + in the human message's pi entry, so every model, channel placement, and teammate reads them + without a second store or a workspace. Files are text: `` blocks after + the message's own text. The app sends a PDF as its pages' text, extracted with pdf.js before + sending, because pi's messages carry no provider document input; scanned PDFs and other binary + files are refused. A message's images and file text share a 1.5 MB budget under the Durable + Object row cap, and the app opens a sent file's text from its timeline block. The budget is + bytes, not tokens: a file that alone exceeds the model's context fails the run, and later runs, + until its message is deleted. - **Tool progress is live only.** Watches that opt in receive `progress` events: the bounded tail of each running call's output from pi's live tool slots, at most every half second per watch, each replacing the last. Attachment rebuilds them from pi's snapshot, and the call's durable diff --git a/package.json b/package.json index b414cefa..1bce0659 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "lucide-static": "1.48.0", "markdown-it": "14.1.1", "motion": "12.23.24", + "pdfjs-dist": "6.3.289", "prosemirror-commands": "1.7.1", "prosemirror-highlight": "0.15.1", "prosemirror-history": "1.5.0", diff --git a/packages/channel/src/room.ts b/packages/channel/src/room.ts index 41c000f6..645fd0dd 100644 --- a/packages/channel/src/room.ts +++ b/packages/channel/src/room.ts @@ -169,7 +169,7 @@ export function section(room: Room): PromptSection { "you act only when someone invokes you; answer the person who did. Write replies as plain", "text without a name prefix: the channel shows who wrote each message.", 'Files people attach follow their message as blocks holding the', - "whole file; they are not on disk, so read them there.", + "whole file, or a PDF's extracted text; they are not on disk, so read them there.", "", "To wait for another channel's agent, say in this chat what you are waiting for and end your", "run; its message with invoke set wakes you. Never wait for another channel in a shell sleep or", diff --git a/packages/ui/src/components/blocks/file-view.tsx b/packages/ui/src/components/blocks/file-view.tsx index 31ac6e10..398e7207 100644 --- a/packages/ui/src/components/blocks/file-view.tsx +++ b/packages/ui/src/components/blocks/file-view.tsx @@ -1,17 +1,47 @@ +import { useState } from "react"; + +import { cn } from "../../lib/utils"; +import { Dialog, DialogContent, DialogTitle } from "../../ui/dialog"; +import { FileView as Contents } from "../file/file-view"; import { FILE_HEIGHT } from "./constants"; -function FileView({ name, size }: { name: string; size: string }) { - return ( -
+function FileView({ name, text }: { name: string; text?: string }) { + let [open, setOpen] = useState(false); + let lines = text ? text.split("\n").length - (text.endsWith("\n") ? 1 : 0) : 0; + let card = ( + <>
📄
{name} - {size} + + {text === undefined ? "" : `${lines.toLocaleString()} ${lines === 1 ? "line" : "lines"}`} +
-
+ + ); + let className = + "flex w-full min-w-0 items-center gap-3 overflow-hidden rounded-lg squircle border border-border px-3 text-left contain-strict"; + if (text === undefined) { + return
{card}
; + } + return ( + <> + + + + {name} + + + + ); } diff --git a/packages/ui/src/components/blocks/file.tsx b/packages/ui/src/components/blocks/file.tsx index 2dec338d..0cdc6416 100644 --- a/packages/ui/src/components/blocks/file.tsx +++ b/packages/ui/src/components/blocks/file.tsx @@ -3,14 +3,14 @@ import type { Block } from "../../lib/block"; import { FileView } from "./file-view"; import { FILE_HEIGHT } from "./constants"; -/** File attachment block with fixed height. */ -function file(name: string, size: string): Block { +/** File attachment block with fixed height; one carrying its contents opens them. */ +function file(name: string, text?: string): Block { return { measure(width) { return { height: FILE_HEIGHT, fit: width }; }, render() { - return ; + return ; }, }; } diff --git a/packages/ui/src/components/timeline/layout.ts b/packages/ui/src/components/timeline/layout.ts index fc990e7f..f8cbdbc1 100644 --- a/packages/ui/src/components/timeline/layout.ts +++ b/packages/ui/src/components/timeline/layout.ts @@ -153,7 +153,7 @@ function key( ...chunk.images.flatMap(img => [img.src, img.width, img.height, img.alt]), ); case "file": - return joinKey("file", mode, chunk.name); + return joinKey("file", mode, chunk.name, chunk.text); case "tool-group": return joinKey( "tools", @@ -263,7 +263,7 @@ function build( case "image-grid": return [gridBlock(chunk.images)]; case "file": - return [fileBlock(chunk.name, "")]; + return [fileBlock(chunk.name, chunk.text)]; case "tool-group": return tools(chunk.tools, expanded, expandedResults, settling); case "commit": diff --git a/packages/ui/src/lib/timeline.ts b/packages/ui/src/lib/timeline.ts index 3d15215a..d6b8fb3e 100644 --- a/packages/ui/src/lib/timeline.ts +++ b/packages/ui/src/lib/timeline.ts @@ -44,7 +44,7 @@ type Chunk = | { kind: "diff"; patch: string } | { kind: "image"; src: string; width: number; height: number; alt?: string } | { kind: "image-grid"; images: ImageNode[] } - | { kind: "file"; name: string; url: string } + | { kind: "file"; name: string; url: string; text?: string } | { kind: "tool-group"; tools: ToolNode[] } | { kind: "commit"; sha: string; message: string; authors: string[]; url?: string } | { kind: "pr"; pr: string; url: string; title: string; draft: boolean } @@ -268,6 +268,7 @@ function structure( kind: "file", name: c.name, url: typeof c.file === "string" ? c.file : c.file.url, + text: c.text, }); break; case "tool": diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts index 79a3b312..3fe75ad6 100644 --- a/packages/ui/src/types.ts +++ b/packages/ui/src/types.ts @@ -147,7 +147,13 @@ export namespace Event { export namespace Content { export type Text = { type: "text"; text: string; parts?: Part[] }; export type Image = { type: "image"; image: string | { url: string } }; - export type File = { type: "file"; name: string; file: string | { url: string } }; + export type File = { + type: "file"; + name: string; + file: string | { url: string }; + /** Contents carried with the message, which the block opens in a file view. */ + text?: string; + }; export type Tool = { type: "tool" } & import("./types").Tool; }