pdf-server: add save_as interact action (#580)
* pdf-server: add save_as interact action
Adds a model-callable save_as action to the interact tool that writes the
viewer's current annotated state (annotations + form values) to a new file.
Reuses the get_pages request/reply pattern: model's interact call blocks
while the viewer builds bytes via getAnnotatedPdfBytes() and posts them
back through a new submit_save_data app-only tool.
Validation runs synchronously before bothering the viewer:
- path must be absolute or file://
- isWritablePath gate (same as save_pdf)
- existence check; fails with a descriptive error unless overwrite: true
The viewer can report build errors back via submit_save_data({error}),
so the model gets a real message instead of the 45s timeout.
* pdf-server: warn model off app-only tools in descriptions
Hosts that don't filter visibility:["app"] tools from the model's tool
list will still surface these. The save_pdf one in particular is a
near-miss for the new save_as interact action — point the model at the
right one.
* pdf-server: save_as without path overwrites original
When path is omitted, save_as falls back to the displayed PDF's local
path (tracked per-viewUUID by display_pdf). Same writability gate as
the viewer's save button: isWritablePath + OS-level W_OK probe, so it
refuses on read-only mounts before the viewer roundtrip.
Requires explicit overwrite: true since the original always exists —
the model can't accidentally clobber it.
Remote PDFs have no source path stored; the error tells the model to
pass an explicit path.
This commit is contained in:
parent
38700569e7
commit
4b3f6252a4
2 changed files with 557 additions and 19 deletions
|
|
@ -65,4 +65,5 @@ export type PdfCommand =
|
|||
getText: boolean;
|
||||
getScreenshots: boolean;
|
||||
}
|
||||
| { type: "save_as"; requestId: string }
|
||||
| { type: "file_changed"; mtimeMs: number };
|
||||
|
|
|
|||
|
|
@ -4298,6 +4298,30 @@ async function processCommands(commands: PdfCommand[]): Promise<void> {
|
|||
.catch(() => {});
|
||||
}
|
||||
break;
|
||||
case "save_as":
|
||||
// Same await-before-next-poll discipline as get_pages — submit must
|
||||
// be SENT before we re-poll, or it queues behind the 30s long-poll.
|
||||
try {
|
||||
const pdfBytes = await getAnnotatedPdfBytes();
|
||||
const base64 = uint8ArrayToBase64(pdfBytes);
|
||||
await app.callServerTool({
|
||||
name: "submit_save_data",
|
||||
arguments: { requestId: cmd.requestId, data: base64 },
|
||||
});
|
||||
log.info(`save_as: submitted ${pdfBytes.length} bytes`);
|
||||
} catch (err) {
|
||||
log.error("save_as: failed to build bytes — submitting error:", err);
|
||||
await app
|
||||
.callServerTool({
|
||||
name: "submit_save_data",
|
||||
arguments: {
|
||||
requestId: cmd.requestId,
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
},
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
break;
|
||||
case "file_changed": {
|
||||
// Skip our own save_pdf echo: either save is still in flight, or the
|
||||
// event's mtime matches what save_pdf just returned.
|
||||
|
|
@ -4337,10 +4361,15 @@ async function processCommands(commands: PdfCommand[]): Promise<void> {
|
|||
}
|
||||
|
||||
// Persist after processing batch — but only if anything mutated.
|
||||
// get_pages / file_changed are read-only; writing localStorage and
|
||||
// recomputing the diff for them is wasted work.
|
||||
// get_pages / save_as / file_changed are read-only; writing localStorage
|
||||
// and recomputing the diff for them is wasted work.
|
||||
if (
|
||||
commands.some((c) => c.type !== "get_pages" && c.type !== "file_changed")
|
||||
commands.some(
|
||||
(c) =>
|
||||
c.type !== "get_pages" &&
|
||||
c.type !== "save_as" &&
|
||||
c.type !== "file_changed",
|
||||
)
|
||||
) {
|
||||
persistAnnotations();
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue