@pinepaper.studio/mcp-server 1.6.17 → 1.6.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +92 -0
  2. package/dist/browser/puppeteer-controller.d.ts +61 -1
  3. package/dist/browser/puppeteer-controller.d.ts.map +1 -1
  4. package/dist/browser/puppeteer-controller.js +168 -6
  5. package/dist/browser/puppeteer-controller.js.map +1 -1
  6. package/dist/build-info.json +2 -2
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +75 -10
  10. package/dist/index.js.map +1 -1
  11. package/dist/ontology/ontology-validator.d.ts.map +1 -1
  12. package/dist/ontology/ontology-validator.js +9 -5
  13. package/dist/ontology/ontology-validator.js.map +1 -1
  14. package/dist/ontology/upstream-vocabulary.d.ts +2 -2
  15. package/dist/ontology/upstream-vocabulary.js +2 -2
  16. package/dist/tools/definitions.d.ts.map +1 -1
  17. package/dist/tools/definitions.js +138 -35
  18. package/dist/tools/definitions.js.map +1 -1
  19. package/dist/tools/engine-lists.d.ts +20 -0
  20. package/dist/tools/engine-lists.d.ts.map +1 -0
  21. package/dist/tools/engine-lists.js +86 -0
  22. package/dist/tools/engine-lists.js.map +1 -0
  23. package/dist/tools/engine-methods.d.ts.map +1 -1
  24. package/dist/tools/engine-methods.js +4 -0
  25. package/dist/tools/engine-methods.js.map +1 -1
  26. package/dist/tools/engine-surface.d.ts +1 -1
  27. package/dist/tools/engine-surface.d.ts.map +1 -1
  28. package/dist/tools/engine-surface.js +20 -4
  29. package/dist/tools/engine-surface.js.map +1 -1
  30. package/dist/tools/handlers/brand-intake.d.ts +95 -0
  31. package/dist/tools/handlers/brand-intake.d.ts.map +1 -0
  32. package/dist/tools/handlers/brand-intake.js +242 -0
  33. package/dist/tools/handlers/brand-intake.js.map +1 -0
  34. package/dist/tools/handlers.d.ts +23 -11
  35. package/dist/tools/handlers.d.ts.map +1 -1
  36. package/dist/tools/handlers.js +226 -10
  37. package/dist/tools/handlers.js.map +1 -1
  38. package/dist/tools/minimal-descriptions.js +4 -4
  39. package/dist/tools/minimal-descriptions.js.map +1 -1
  40. package/dist/tools/shape-params.d.ts +2 -2
  41. package/dist/tools/shape-params.js +2 -2
  42. package/dist/tools/stick-vocabulary.d.ts +1 -1
  43. package/dist/tools/stick-vocabulary.js +1 -1
  44. package/dist/types/code-generator.d.ts +40 -2
  45. package/dist/types/code-generator.d.ts.map +1 -1
  46. package/dist/types/code-generator.js +260 -26
  47. package/dist/types/code-generator.js.map +1 -1
  48. package/dist/types/schemas.d.ts +2170 -698
  49. package/dist/types/schemas.d.ts.map +1 -1
  50. package/dist/types/schemas.js +152 -36
  51. package/dist/types/schemas.js.map +1 -1
  52. package/dist/utils/mcp-log.d.ts +16 -0
  53. package/dist/utils/mcp-log.d.ts.map +1 -0
  54. package/dist/utils/mcp-log.js +10 -0
  55. package/dist/utils/mcp-log.js.map +1 -0
  56. package/dist/utils/progress.d.ts +28 -0
  57. package/dist/utils/progress.d.ts.map +1 -0
  58. package/dist/utils/progress.js +24 -0
  59. package/dist/utils/progress.js.map +1 -0
  60. package/dist/vendor/design/design-style.js +1 -1
  61. package/manifest.json +1 -1
  62. package/package.json +7 -4
@@ -5,7 +5,7 @@
5
5
  * Supports i18n for localized error and success messages.
6
6
  * Supports browser execution via Puppeteer.
7
7
  */
8
- import { codeGenerator, generateQueryMutationsCode } from '../types/code-generator.js';
8
+ import { codeGenerator, generateQueryMutationsCode, SHEET_RUBRIC } from '../types/code-generator.js';
9
9
  import { CreateItemInputSchema, ModifyItemInputSchema, DeleteItemInputSchema, AddRelationInputSchema, RemoveRelationInputSchema, QueryRelationsInputSchema, AnimateItemInputSchema, ImportMotionCaptureInputSchema, KeyframeAnimateInputSchema, ExecuteGeneratorInputSchema, ApplyEffectInputSchema, GetItemsInputSchema, PlayTimelineInputSchema, SetBackgroundColorInputSchema, SetCanvasSizeInputSchema, ImportSVGInputSchema, ImportMermaidInputSchema, AddFilterInputSchema, CreateGlossySphereInputSchema, CreateDiagonalStripesInputSchema, BatchCreateInputSchema, BatchModifyInputSchema, CreateGridInputSchema, GeometryInputSchema, EquationPathInputSchema, EventInputSchema, ConstructionSequenceInputSchema, ValidateSceneInputSchema, CaptureFramesInputSchema, InstantiateOntologyInputSchema, LintSceneInputSchema, MediaInputSchema, TextStyleInputSchema, TextEffectInputSchema, DesignMediumInputSchema, ShatterImageInputSchema, ImportLayeredCharacterInputSchema, CharacterInputSchema, GameInputSchema, World3DInputSchema, BrandKitInputSchema, ComponentInputSchema, ArtboardInputSchema, CommentInputSchema, ProvenanceInputSchema, SceneDiffInputSchema, AudioBeatsInputSchema, TemplateParamsInputSchema, RenderBatchInputSchema, AccessibilityCheckInputSchema, QueryMutationsInputSchema, ComposeInputSchema, CropImageInputSchema, PathOpInputSchema, MotionInputSchema, SoundInputSchema, InterchangeInputSchema, StickInputSchema, DesignSystemInputSchema, StoryInputSchema, ChromaKeyInputSchema, RiggingInputSchema, GroupInputSchema, CameraDirectorInputSchema, DetectObjectsInputSchema, ExtractObjectInputSchema, ArrangeInputSchema, CreateSceneInputSchema, GetPerformanceMetricsInputSchema, DiagnosticReportInputSchema, SearchAssetsInputSchema, ImportAssetInputSchema, P5DrawInputSchema,
10
10
  // Diagram schemas
11
11
  CreateDiagramShapeInputSchema, ConnectInputSchema, ConnectPortsInputSchema, AddPortsInputSchema, AutoLayoutInputSchema, GetDiagramShapesInputSchema, UpdateConnectorInputSchema, RemoveConnectorInputSchema, DiagramModeInputSchema,
@@ -85,13 +85,19 @@ export function getScreenshotMode() {
85
85
  // =============================================================================
86
86
  // EXPORT FILE SAVE HELPERS
87
87
  // =============================================================================
88
- function getExportDir() {
88
+ export function getExportDir() {
89
89
  return process.env.PINEPAPER_EXPORT_DIR || join(tmpdir(), 'pinepaper-exports');
90
90
  }
91
91
  // wav joins them: a minute of 48kHz 16-bit is ~5.8 MB of base64 and the ten
92
92
  // minutes the schema allows is ~77 MB. Deliverable across the bridge, useless
93
93
  // pasted into a response.
94
- export const ALWAYS_SAVE_FORMATS = new Set(['mp4', 'webm', 'gif', 'apng', 'pdf', 'wav', 'srt', 'vtt', 'scc', 'html5-ad', 'playable']);
94
+ // Stills join them too. The threshold below is in CHARACTERS of base64, and a
95
+ // 59 KB 1920x1080 PNG is ~79k of them: under the threshold, so it came back
96
+ // inline, and over the token limit of the client reading it (a Halloween
97
+ // look-frame session, 2026-09-29), while bigger PNGs saved to a file. Same
98
+ // format, two shapes, decided by file size. An image is never useful as
99
+ // base64 in a text result, so png/jpg/webp always save and name the path.
100
+ export const ALWAYS_SAVE_FORMATS = new Set(['mp4', 'webm', 'gif', 'apng', 'pdf', 'wav', 'srt', 'vtt', 'scc', 'html5-ad', 'playable', 'png', 'jpg', 'webp']);
95
101
  // 500_000 was chosen against the bridge's limits, not the CALLER's. A pilot
96
102
  // session hit a 263K-character end_job result — comfortably under this, so it
97
103
  // was returned inline, and over the tool-result limit of the client reading
@@ -583,6 +589,33 @@ function successResult(code, description) {
583
589
  function dataResult(value) {
584
590
  return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] };
585
591
  }
592
+ /**
593
+ * A call that only succeeded after the browser session was recovered says so,
594
+ * in the result: the controller has reported `recovered` / `canvasReset` for
595
+ * a while, but nothing read them, so an agent kept using item ids from a
596
+ * scene that was gone (M6).
597
+ */
598
+ function withSessionNote(out, r) {
599
+ if (!r.recovered)
600
+ return out;
601
+ const note = r.relaunched
602
+ ? `The browser session had been lost; the browser was relaunched and this call ran on an EMPTY canvas${r.canvasSizeRestored ? ` (size restored to ${r.canvasSizeRestored.width}x${r.canvasSizeRestored.height})` : ' at the default size'}. Item ids from before are gone: rebuild the scene.`
603
+ : r.canvasReset
604
+ ? 'The page had reloaded and the canvas came back EMPTY. Item ids from before are gone: rebuild the scene.'
605
+ : 'The page connection was re-established; the scene survived.';
606
+ const first = out.content[0];
607
+ if (first && first.type === 'text') {
608
+ try {
609
+ const obj = JSON.parse(first.text);
610
+ obj.session = { recovered: true, ...(r.relaunched ? { relaunched: true } : {}), ...(r.canvasReset !== undefined ? { canvasReset: r.canvasReset } : {}), note };
611
+ return { ...out, content: [{ type: 'text', text: JSON.stringify(obj, null, 2) }, ...out.content.slice(1)] };
612
+ }
613
+ catch {
614
+ return { ...out, content: [{ type: 'text', text: `${note}\n\n${first.text}` }, ...out.content.slice(1)] };
615
+ }
616
+ }
617
+ return out;
618
+ }
586
619
  function executedResult(code, result, screenshot, description, report) {
587
620
  const resultObj = { success: true, result };
588
621
  if (description)
@@ -740,6 +773,170 @@ function handleValidationError(error, i18n) {
740
773
  *
741
774
  * Default is 'on_request' for optimal performance per best practices.
742
775
  */
776
+ /** Page code: remember the id of every item on the canvas. */
777
+ const MARK_ITEMS_BEFORE_RUN = `(function () {
778
+ if (typeof paper === 'undefined' || !paper.project) return { ok: false };
779
+ const ids = new Set();
780
+ paper.project.getItems({}).forEach(function (i) { ids.add(i.id); });
781
+ window.__ppPreRunIds = ids;
782
+ return { ok: true };
783
+ })();`;
784
+ /** Page code: remove every item that was not there at the mark, outermost first. */
785
+ const REMOVE_ITEMS_CREATED_SINCE_MARK = `(function () {
786
+ const pre = window.__ppPreRunIds;
787
+ window.__ppPreRunIds = null;
788
+ if (!pre || typeof paper === 'undefined' || !paper.project) return { ok: false };
789
+ const fresh = paper.project.getItems({}).filter(function (i) { return !pre.has(i.id); });
790
+ const freshIds = new Set(fresh.map(function (i) { return i.id; }));
791
+ const tops = fresh.filter(function (i) { return !(i.parent && freshIds.has(i.parent.id)); });
792
+ let removed = 0;
793
+ tops.forEach(function (it) {
794
+ try { if (typeof app !== 'undefined' && typeof app.deleteItem === 'function' && app.deleteItem(it)) { removed++; return; } } catch (e) {}
795
+ try { if (it.parent) { it.remove(); removed++; } } catch (e) {}
796
+ });
797
+ const left = paper.project.getItems({}).filter(function (i) { return !pre.has(i.id); }).length;
798
+ try { if (paper.view) paper.view.update(); } catch (e) {}
799
+ return { ok: left === 0, removed: removed, left: left };
800
+ })();`;
801
+ /**
802
+ * Record which canvas items exist before agent-written code runs, so a failed
803
+ * run's leftovers can be told apart from the scene it started with. Returns
804
+ * false when the page could not be marked (the rollback is then skipped and
805
+ * the failure note says nothing about the scene it cannot vouch for).
806
+ */
807
+ async function markItemsBeforeRun(controller) {
808
+ try {
809
+ const r = await controller.executeCode(MARK_ITEMS_BEFORE_RUN, false, { bypassGovernor: true });
810
+ return !!(r.success && r.result?.ok);
811
+ }
812
+ catch {
813
+ return false;
814
+ }
815
+ }
816
+ /**
817
+ * Remove what agent-written code created before it failed, and say so.
818
+ *
819
+ * A run stopped by the loop deadline after drawing 42 strokes left them on
820
+ * the canvas: a half-painted scene. Undo cannot fix that. The governor's one
821
+ * history entry only covers mutations that announce themselves, and raw
822
+ * `new paper.Path(...)` does not (measured: report.mutated false, 30 paths
823
+ * left behind). So the rollback is by identity instead: every item that was
824
+ * not on the canvas before the call goes, through app.deleteItem where the
825
+ * engine knows the item (registry, trackers) and item.remove() otherwise.
826
+ * Edits to items that already existed are NOT reverted; the note says so.
827
+ */
828
+ export async function customCodeRollback(controller, error) {
829
+ let rolledBack = false;
830
+ let removed = 0;
831
+ let left = -1;
832
+ try {
833
+ const r = await controller.executeCode(REMOVE_ITEMS_CREATED_SINCE_MARK, false, { bypassGovernor: true });
834
+ const v = (r.result || {});
835
+ rolledBack = !!(r.success && v.ok);
836
+ removed = v.removed ?? 0;
837
+ left = typeof v.left === 'number' ? v.left : -1;
838
+ }
839
+ catch {
840
+ rolledBack = false;
841
+ }
842
+ const scene = rolledBack
843
+ ? (removed > 0
844
+ ? `The ${removed} item(s) this call created before it stopped were removed. Changes it made to items that already existed are not reverted.`
845
+ : 'This call created no items before it stopped. Changes it made to items that already existed are not reverted.')
846
+ : `Items this call created could not all be removed${left > 0 ? ` (${left} remain)` : ''}: check the canvas before continuing.`;
847
+ const how = /Synchronous execution exceeded|Loop budget exceeded/i.test(error || '')
848
+ ? ' This is execute_custom_code\'s per-call guard, not a limit on the scene: its loops may run about 4 s (and 2,000,000 iterations per loop) so the studio tab stays responsive. Split the work across several calls, each of which gets a fresh budget. For example, paint 10 strokes per call instead of 40 in one.'
849
+ : '';
850
+ return { rolledBack, note: scene + how };
851
+ }
852
+ /**
853
+ * pinepaper_brand_kit from_url: the product's real brand, read from its own
854
+ * site (see handlers/brand-intake.ts). Runs in the MCP server's browser, in an
855
+ * isolated context, so code mode cannot do it and says so.
856
+ */
857
+ async function brandFromUrl(url, shots, options) {
858
+ const effectiveExecMode = options.executionMode ?? getExecutionMode();
859
+ if (effectiveExecMode === 'code' || !options.executeInBrowser) {
860
+ return errorResult(ErrorCodes.EXECUTION_ERROR, 'brand_kit from_url reads the site in the MCP server\'s browser, so it needs browser mode; it cannot run as generated code.', {}, { toolName: 'pinepaper_brand_kit' });
861
+ }
862
+ const controller = options.browserController || getBrowserController();
863
+ if (!controller.connected) {
864
+ try {
865
+ await controller.connect();
866
+ }
867
+ catch (e) {
868
+ return errorResult(ErrorCodes.BROWSER_NOT_CONNECTED, `could not start the browser: ${e instanceof Error ? e.message : String(e)}`, {}, { toolName: 'pinepaper_brand_kit' });
869
+ }
870
+ }
871
+ const host = (() => { try {
872
+ return new URL(url).hostname.replace(/[^a-z0-9.-]/gi, '_');
873
+ }
874
+ catch {
875
+ return 'site';
876
+ } })();
877
+ const outDir = join(getExportDir(), `brand-${host}-${Date.now()}`);
878
+ try {
879
+ const { intakeBrand } = await import('./handlers/brand-intake.js');
880
+ const r = await controller
881
+ .withIsolatedPage((page) => intakeBrand(page, url, outDir, shots));
882
+ const text = [
883
+ `Brand read from ${url}. Files in ${outDir}:`,
884
+ ...r.screenshots.map((f) => `- screenshot: ${f}`),
885
+ r.logo ? `- logo: ${r.logo.file} (from ${r.logo.source})` : '- logo: none found in the header, icons or og:image',
886
+ '',
887
+ 'Review the proposed kit against the screenshots, then apply it with pinepaper_brand_kit { action: "apply", kit } (or "plan" first). Import the logo or a screenshot with pinepaper_import_image { url: <file> }.',
888
+ '',
889
+ JSON.stringify({ success: true, ...r }, null, 2),
890
+ ].join('\n');
891
+ return { content: [{ type: 'text', text }] };
892
+ }
893
+ catch (e) {
894
+ return errorResult(ErrorCodes.EXECUTION_ERROR, `could not read ${url}: ${e instanceof Error ? e.message : String(e)}`, {}, { toolName: 'pinepaper_brand_kit' });
895
+ }
896
+ }
897
+ /**
898
+ * pinepaper_capture_frames with `sheet`: one tiled image of the film, saved
899
+ * to a file (an image is never useful inline, see ALWAYS_SAVE_FORMATS), with
900
+ * the rubric the agent scores it against. Code mode returns the code as usual.
901
+ */
902
+ async function captureSheet(input, options) {
903
+ const code = codeGenerator.generateCaptureSheet(input);
904
+ const description = 'Contact sheet';
905
+ const effectiveExecMode = options.executionMode ?? getExecutionMode();
906
+ if (effectiveExecMode === 'code' || !options.executeInBrowser) {
907
+ return executeOrGenerate(code, description, options, 'pinepaper_capture_frames');
908
+ }
909
+ const controller = options.browserController || getBrowserController();
910
+ if (!controller.connected) {
911
+ try {
912
+ await controller.connect();
913
+ }
914
+ catch {
915
+ return executeOrGenerate(code, description, options, 'pinepaper_capture_frames');
916
+ }
917
+ }
918
+ // Frames wait for video seeks one at a time, so 48 of them can outlast the
919
+ // governor's 10 s default.
920
+ const run = await controller.executeCode(code, false, { governorTimeoutMs: 120_000 });
921
+ const value = (run.result ?? {});
922
+ if (!run.success || value.success === false || typeof value.dataUrl !== 'string') {
923
+ return errorResult(ErrorCodes.EXECUTION_ERROR, run.error || value.error || 'the contact sheet could not be made', { code, governorReport: run.report }, { toolName: 'pinepaper_capture_frames' });
924
+ }
925
+ const format = input.sheet?.format === 'png' ? 'png' : 'jpg';
926
+ const { filePath, fileSize } = await saveExportToFile(value.dataUrl, format, 'sheet');
927
+ const { dataUrl: _d, success: _s, ...facts } = value;
928
+ void _d;
929
+ void _s;
930
+ const result = { success: true, filePath, fileSize, format, ...facts, review: SHEET_RUBRIC };
931
+ return {
932
+ content: [{
933
+ type: 'text',
934
+ text: `Contact sheet saved: ${filePath} (${(fileSize / 1024).toFixed(1)} KB, ${String(facts.tiles)} tiles).\n`
935
+ + 'Open the image and score it against review.criteria (1-10 each), then check review.checklist.\n\n'
936
+ + JSON.stringify(result, null, 2),
937
+ }],
938
+ };
939
+ }
743
940
  export async function executeOrGenerate(code, description, options, toolName,
744
941
  /**
745
942
  * Values too large to live inside the generated code. Written to
@@ -880,6 +1077,10 @@ ${code}
880
1077
  if (shouldTakeScreenshot) {
881
1078
  tracker.startTimer(`${timerId}_screenshot`);
882
1079
  }
1080
+ // Agent-written code is rolled back when it fails part-way; a tool's own
1081
+ // emitter is not (its verdict already describes what it left behind).
1082
+ const rollbackOnFailure = toolName === 'pinepaper_execute_custom_code'
1083
+ && await markItemsBeforeRun(controller);
883
1084
  const result = await controller.executeCode(code, shouldTakeScreenshot, stage ? { stage } : {});
884
1085
  const browserDuration = tracker.endTimer(`${timerId}_browser_execution`);
885
1086
  tracker.recordMetric({
@@ -916,7 +1117,10 @@ ${code}
916
1117
  return errorResult(ErrorCodes.EXECUTION_ERROR, result.error || 'Failed to execute code in browser',
917
1118
  // errorCode surfaces the governor's structured code (PP_ITEM_BUDGET,
918
1119
  // PP_LOOP_BUDGET, PP_TIMEOUT) so the model can correct on the next turn.
919
- { code, errorCode: result.errorCode, governorReport: result.report }, {
1120
+ {
1121
+ code, errorCode: result.errorCode, governorReport: result.report,
1122
+ ...(rollbackOnFailure ? await customCodeRollback(controller, result.error) : {}),
1123
+ }, {
920
1124
  toolName,
921
1125
  canvasState: canvasState || undefined,
922
1126
  });
@@ -936,9 +1140,12 @@ ${code}
936
1140
  const inner = innerFailure(result.result);
937
1141
  if (inner) {
938
1142
  const canvasState = await captureCanvasState(controller);
939
- return errorResult(ErrorCodes.EXECUTION_ERROR, inner, { code, result: result.result, governorReport: result.report }, { toolName, canvasState: canvasState || undefined });
1143
+ return errorResult(ErrorCodes.EXECUTION_ERROR, inner, {
1144
+ code, result: result.result, governorReport: result.report,
1145
+ ...(rollbackOnFailure ? await customCodeRollback(controller, inner) : {}),
1146
+ }, { toolName, canvasState: canvasState || undefined });
940
1147
  }
941
- return executedResult(code, result.result, result.screenshot, description, result.report);
1148
+ return withSessionNote(executedResult(code, result.result, result.screenshot, description, result.report), result);
942
1149
  }
943
1150
  /**
944
1151
  * The emitted code's own verdict, when it reported one and it was a failure.
@@ -1502,8 +1709,10 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
1502
1709
  }
1503
1710
  case 'pinepaper_capture_frames': {
1504
1711
  const input = CaptureFramesInputSchema.parse(args);
1712
+ if (input.sheet)
1713
+ return captureSheet(input, options);
1505
1714
  const code = codeGenerator.generateCaptureFrames(input);
1506
- const description = `Deterministic capture of ${input.times.length} frame(s)`;
1715
+ const description = `Deterministic capture of ${(input.times ?? []).length} frame(s)`;
1507
1716
  return executeOrGenerate(code, description, options, 'pinepaper_capture_frames');
1508
1717
  }
1509
1718
  case 'pinepaper_instantiate_ontology': {
@@ -1563,6 +1772,8 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
1563
1772
  // ─── 1.6.4: agent surface for the Tier-2 engine features ───
1564
1773
  case 'pinepaper_brand_kit': {
1565
1774
  const input = BrandKitInputSchema.parse(args);
1775
+ if (input.action === 'from_url')
1776
+ return brandFromUrl(input.url, input.screenshots ?? 3, options);
1566
1777
  const code = codeGenerator.generateBrandKit(input);
1567
1778
  return executeOrGenerate(code, `Brand kit: ${input.action}`, options, 'pinepaper_brand_kit');
1568
1779
  }
@@ -2007,8 +2218,13 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
2007
2218
  return errorResult(ErrorCodes.BROWSER_NOT_CONNECTED, 'Not connected to PinePaper Studio. Call pinepaper_browser_connect first.');
2008
2219
  }
2009
2220
  try {
2010
- // Refresh the page
2011
- await controller.refreshPage();
2221
+ // Refresh the page (recovers a dead session first; M6)
2222
+ const refreshed = await controller.refreshPage();
2223
+ const sizeNote = refreshed.relaunched
2224
+ ? (refreshed.canvasSizeRestored
2225
+ ? `\n\nThe browser session had been lost, so the browser was relaunched. The canvas size was restored to ${refreshed.canvasSizeRestored.width}x${refreshed.canvasSizeRestored.height}.`
2226
+ : '\n\nThe browser session had been lost, so the browser was relaunched at the default canvas size: set the size again if you need another.')
2227
+ : '';
2012
2228
  // Wait a moment for the page to stabilize
2013
2229
  await new Promise(resolve => setTimeout(resolve, 2000));
2014
2230
  // Take a screenshot to show the result
@@ -2020,7 +2236,7 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
2020
2236
 
2021
2237
  The canvas is now completely empty. The welcome template will not appear (it only shows for first-time visitors).
2022
2238
 
2023
- You can now start creating new items on a clean canvas.`,
2239
+ You can now start creating new items on a clean canvas.${sizeNote}`,
2024
2240
  },
2025
2241
  ];
2026
2242
  if (screenshot) {