@pinepaper.studio/mcp-server 1.6.35 → 1.6.37

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 (67) hide show
  1. package/README.de.md +1 -1
  2. package/README.es.md +1 -1
  3. package/README.fr.md +1 -1
  4. package/README.hi.md +1 -1
  5. package/README.ja.md +1 -1
  6. package/README.ko.md +1 -1
  7. package/README.md +139 -7
  8. package/README.pt-BR.md +1 -1
  9. package/README.zh-CN.md +1 -1
  10. package/dist/agent/types.d.ts +12 -12
  11. package/dist/browser/puppeteer-controller.d.ts.map +1 -1
  12. package/dist/browser/puppeteer-controller.js +7 -1
  13. package/dist/browser/puppeteer-controller.js.map +1 -1
  14. package/dist/build-info.json +2 -2
  15. package/dist/ontology/upstream-vocabulary.d.ts +2 -2
  16. package/dist/ontology/upstream-vocabulary.js +2 -2
  17. package/dist/tools/definitions.d.ts.map +1 -1
  18. package/dist/tools/definitions.js +141 -91
  19. package/dist/tools/definitions.js.map +1 -1
  20. package/dist/tools/effect-class.d.ts.map +1 -1
  21. package/dist/tools/effect-class.js +4 -0
  22. package/dist/tools/effect-class.js.map +1 -1
  23. package/dist/tools/engine-lists.d.ts +30 -3
  24. package/dist/tools/engine-lists.d.ts.map +1 -1
  25. package/dist/tools/engine-lists.js +346 -4
  26. package/dist/tools/engine-lists.js.map +1 -1
  27. package/dist/tools/engine-methods.d.ts.map +1 -1
  28. package/dist/tools/engine-methods.js +4 -0
  29. package/dist/tools/engine-methods.js.map +1 -1
  30. package/dist/tools/engine-surface.d.ts +1 -1
  31. package/dist/tools/engine-surface.d.ts.map +1 -1
  32. package/dist/tools/engine-surface.js +29 -7
  33. package/dist/tools/engine-surface.js.map +1 -1
  34. package/dist/tools/export-file-receipt.d.ts +33 -0
  35. package/dist/tools/export-file-receipt.d.ts.map +1 -1
  36. package/dist/tools/export-file-receipt.js +49 -0
  37. package/dist/tools/export-file-receipt.js.map +1 -1
  38. package/dist/tools/frozen-output.d.ts +33 -0
  39. package/dist/tools/frozen-output.d.ts.map +1 -0
  40. package/dist/tools/frozen-output.js +73 -0
  41. package/dist/tools/frozen-output.js.map +1 -0
  42. package/dist/tools/handlers.d.ts +15 -0
  43. package/dist/tools/handlers.d.ts.map +1 -1
  44. package/dist/tools/handlers.js +141 -15
  45. package/dist/tools/handlers.js.map +1 -1
  46. package/dist/tools/minimal-descriptions.d.ts.map +1 -1
  47. package/dist/tools/minimal-descriptions.js +1 -0
  48. package/dist/tools/minimal-descriptions.js.map +1 -1
  49. package/dist/tools/shape-params.d.ts +2 -2
  50. package/dist/tools/shape-params.js +2 -2
  51. package/dist/tools/stick-vocabulary.d.ts +1 -1
  52. package/dist/tools/stick-vocabulary.js +1 -1
  53. package/dist/tools/toolkits.d.ts.map +1 -1
  54. package/dist/tools/toolkits.js +2 -1
  55. package/dist/tools/toolkits.js.map +1 -1
  56. package/dist/types/code-generator.d.ts +18 -2
  57. package/dist/types/code-generator.d.ts.map +1 -1
  58. package/dist/types/code-generator.js +462 -198
  59. package/dist/types/code-generator.js.map +1 -1
  60. package/dist/types/schemas.d.ts +112 -270
  61. package/dist/types/schemas.d.ts.map +1 -1
  62. package/dist/types/schemas.js +46 -145
  63. package/dist/types/schemas.js.map +1 -1
  64. package/dist/utils/asset-paths.js +1 -1
  65. package/dist/utils/asset-paths.js.map +1 -1
  66. package/manifest.json +5 -1
  67. package/package.json +1 -1
@@ -5,6 +5,7 @@
5
5
  * Supports i18n for localized error and success messages.
6
6
  * Supports browser execution via Puppeteer.
7
7
  */
8
+ import { SCENE_MOTION_PROBE_JS, frozenOutputCheck } from './frozen-output.js';
8
9
  import { dropPrivateRelationParams, normalizeBatchOperationNames, renamedArgumentsNote, RENAMED_ARGUMENTS_META_KEY } from './batch-op-names.js';
9
10
  import { nestedIgnoredPaths } from './nested-ignored.js';
10
11
  import { CRAFT_FRAME_JS, generateDrawWeave, generateDrawKnots, generateDrawSashiko, generateDrawSubdivision, generateUiKit, uiKitCallProblems, } from './craft-tools.js';
@@ -12,7 +13,7 @@ import { exportPerScene } from './scene-cache-export.js';
12
13
  import { boundedStructured, reportedIgnoredPaths } from './reply-fields.js';
13
14
  import { codeGenerator, generateQueryMutationsCode, SHEET_RUBRIC } from '../types/code-generator.js';
14
15
  import { expandBatchMacros, BatchMacroError, batchPreflightView, withKeyColumns } from './batch-macros.js';
15
- 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, RelightInputSchema, ChoreographInputSchema, EmitterInputSchema, MorphSequenceInputSchema, OriginalCharacterInputSchema, LookInputSchema, RenderHookInputSchema, ShaderGraphInputSchema, 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,
16
+ 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, RelightInputSchema, ChoreographInputSchema, EmitterInputSchema, MorphSequenceInputSchema, OriginalCharacterInputSchema, LookInputSchema, RenderHookInputSchema, ShaderGraphInputSchema, ShatterImageInputSchema, ImportLayeredCharacterInputSchema, CharacterInputSchema, GameInputSchema, World3DInputSchema, BrandKitInputSchema, ComponentInputSchema, ArtboardInputSchema, CommentInputSchema, ProvenanceInputSchema, SceneDiffInputSchema, AudioBeatsInputSchema, TemplateParamsInputSchema, RenderBatchInputSchema, AccessibilityCheckInputSchema, QueryMutationsInputSchema, ComposeInputSchema, CropImageInputSchema, PathOpInputSchema, MotionInputSchema, SoundInputSchema, InterchangeInputSchema, StickInputSchema, InstancesInputSchema, DesignSystemInputSchema, StoryInputSchema, ChromaKeyInputSchema, RiggingInputSchema, GroupInputSchema, CameraDirectorInputSchema, DetectObjectsInputSchema, ExtractObjectInputSchema, ArrangeInputSchema, CreateSceneInputSchema, GetPerformanceMetricsInputSchema, DiagnosticReportInputSchema, SearchAssetsInputSchema, ImportAssetInputSchema, P5DrawInputSchema,
16
17
  // Diagram schemas
17
18
  CreateDiagramShapeInputSchema, ConnectInputSchema, ConnectPortsInputSchema, AddPortsInputSchema, AutoLayoutInputSchema, GetDiagramShapesInputSchema, UpdateConnectorInputSchema, RemoveConnectorInputSchema, DiagramModeInputSchema,
18
19
  // Agent flow mode schemas
@@ -38,7 +39,7 @@ import { ZodError, z } from 'zod';
38
39
  import { AsyncLocalStorage } from 'node:async_hooks';
39
40
  import { effectClass } from './effect-class.js';
40
41
  import { capabilityAllowed, capabilityRefusal, CAPABILITY_SWITCH } from '../utils/capabilities.js';
41
- import { exportFileReceiptOrReason, receiptErrorLine } from './export-file-receipt.js';
42
+ import { exportFileReceiptOrReason, receiptErrorLine, deliverAacIfOpus } from './export-file-receipt.js';
42
43
  import { ExportStepError, errorText, verifySavedFile, writeStep, bytesForSave } from './export-file-check.js';
43
44
  import { EFFECT_BEFORE_JS, effectAfterJs, namedItemIds, idMayBeGone, NO_CHANGE_NOTE } from './effect-check.js';
44
45
  import { writeFile, mkdir, appendFile, unlink, readFile, stat } from 'node:fs/promises';
@@ -746,6 +747,21 @@ export function withEngineReport(result, report) {
746
747
  _meta: { ...(result._meta ?? {}), [RECEIPTS_META_KEY]: body },
747
748
  };
748
749
  }
750
+ /** B45 option D: the delivered file's audio, in the result and its words. */
751
+ function audioDeliveryFields(d) {
752
+ if (d.action === 'transcoded')
753
+ return { audioDelivered: { ...d.audio, preRemux: d.preRemux, note: 'file.sha256 is the delivered file. The studio file hash, bitExact and provenance describe the bytes before the audio was re-encoded; its frameHashes still describe the video, which was copied unchanged.' } };
754
+ if (d.action === 'kept-opus')
755
+ return { audioNote: d.note };
756
+ return {};
757
+ }
758
+ function audioDeliveryLine(d) {
759
+ if (d.action === 'transcoded')
760
+ return `Audio: re-encoded from Opus to AAC (${d.audio.bitrate}); the video is copied unchanged, and the file has a new sha256.\n`;
761
+ if (d.action === 'kept-opus')
762
+ return `Audio: ${d.note}\n`;
763
+ return '';
764
+ }
749
765
  function executedResult(code, result, screenshot, description, report) {
750
766
  const resultObj = { success: true, result };
751
767
  if (description)
@@ -851,6 +867,43 @@ function redactGeneratedCode(details) {
851
867
  * The studio's scene-lint warnings on an export (FxTool lintScene), one line each, so a centred title that runs
852
868
  * out of the frame ("IGNAL" for "SIGNAL") is read on the success path rather than left in the JSON. Empty when none.
853
869
  */
870
+ /** Put a frozen-output finding on the result: in `warnings`, in the `warning` line the reply prints, and as `frozenOutput`. */
871
+ export function noteFrozenOutput(exportResult, sceneMotion) {
872
+ if (!exportResult || typeof exportResult !== 'object')
873
+ return;
874
+ const f = frozenOutputCheck(exportResult, sceneMotion);
875
+ if (!f)
876
+ return;
877
+ const r = exportResult;
878
+ r.frozenOutput = { frozenAt: f.frozenAt, exportEnd: f.exportEnd, itemId: f.track.id, trackUntil: f.track.until >= 1e8 ? 'loops' : f.track.until };
879
+ if (Array.isArray(r.warnings))
880
+ r.warnings.push(f.warning);
881
+ else
882
+ r.warnings = [f.warning];
883
+ r.warning = typeof r.warning === 'string' && r.warning ? r.warning + ' ' + f.warning : f.warning;
884
+ }
885
+ /**
886
+ * A CLAIM THE NUMBERS NEXT TO IT CONTRADICT (tester, 1.6.36). The studio sets video.bitrateCeilingHeld: true
887
+ * whenever the file is within 10% OVER the target, and warns only past that, so a file 7% over its ceiling was
888
+ * reported as having held it. The flag is set from the file's own achievedBitrate and the target beside it.
889
+ */
890
+ export function correctBitrateClaim(exportResult) {
891
+ const r = exportResult;
892
+ const v = r && typeof r === 'object' ? r.video : null;
893
+ if (!v || typeof v !== 'object' || v.bitrateCeilingHeld !== true)
894
+ return;
895
+ const target = Number(v.bitrate), achieved = Number(v.achievedBitrate);
896
+ if (!(target > 0) || !(achieved > target))
897
+ return;
898
+ const over = Math.round((achieved / target - 1) * 1000) / 10;
899
+ v.bitrateCeilingHeld = false;
900
+ v.bitrateOverPercent = over;
901
+ const msg = `bitrate_exceeded: the ${(target / 1e6).toFixed(2)} Mbps target was not held: the file is ${(achieved / 1e6).toFixed(2)} Mbps (${over}% over). The browser encoder treats the target loosely on content it cannot compress; pass a lower bitrate, or bitrateMode 'constant', which held closer in measurement.`;
902
+ if (Array.isArray(r.warnings))
903
+ r.warnings.push(msg);
904
+ else
905
+ r.warnings = [msg];
906
+ }
854
907
  export function lintLines(exportResult) {
855
908
  const w = exportResult?.lintWarnings;
856
909
  if (!Array.isArray(w) || !w.length)
@@ -1262,6 +1315,34 @@ async function craftFrame(options) {
1262
1315
  return undefined;
1263
1316
  }
1264
1317
  }
1318
+ /**
1319
+ * B136: Chromium draws a raster's smoothing 'low', 'medium' and 'high' the same, so a call asking for 'medium' or
1320
+ * 'high' is applied and looks like 'low'. Said in the reply's JSON, since the docs alone left it silent. Any
1321
+ * `smoothing` set to one of those words, at any depth (import, modify, create, batch ops); numeric smoothing
1322
+ * (relations, chroma key) is another parameter and is left alone.
1323
+ */
1324
+ export function smoothingLevelField(args) {
1325
+ const seen = new Set();
1326
+ const walk = (v, depth) => {
1327
+ if (depth > 6 || !v || typeof v !== 'object')
1328
+ return;
1329
+ if (Array.isArray(v)) {
1330
+ v.forEach((x) => walk(x, depth + 1));
1331
+ return;
1332
+ }
1333
+ for (const [k, x] of Object.entries(v)) {
1334
+ if (k === 'smoothing' && (x === 'medium' || x === 'high'))
1335
+ seen.add(x);
1336
+ else
1337
+ walk(x, depth + 1);
1338
+ }
1339
+ };
1340
+ walk(args, 0);
1341
+ if (!seen.size)
1342
+ return {};
1343
+ const asked = [...seen].map((x) => `'${x}'`).join(' and ');
1344
+ return { smoothingNote: `smoothing ${asked} was applied, but Chromium draws 'low', 'medium' and 'high' the same, so it looks like 'low'. The visible choice is 'off' (nearest-neighbour, hard pixel edges) or not.` };
1345
+ }
1265
1346
  export async function executeOrGenerate(code, description, options, toolName,
1266
1347
  /**
1267
1348
  * Values too large to live inside the generated code. Written to
@@ -2010,6 +2091,7 @@ export async function handleToolCall(toolName, args, options = {}) {
2010
2091
  ...(dropped.length ? { droppedArguments: dropped } : {}),
2011
2092
  ...(ignoredPaths.length ? { ignoredArguments: ignoredPaths } : {}),
2012
2093
  ...await renderingBuildField(toolName, args, result, options),
2094
+ ...smoothingLevelField(args),
2013
2095
  });
2014
2096
  if (!folded) {
2015
2097
  const lines = [...(renamed.length ? [renamedArgumentsNote(renamed)] : []), ...(dropped.length ? [`Not sent to the studio, as its private state: ${dropped.join(', ')}.`] : [])];
@@ -2552,6 +2634,10 @@ async function handleToolCallInner(toolName, args, options, tracker, baseTimerId
2552
2634
  const input = MorphSequenceInputSchema.parse(args);
2553
2635
  return executeOrGenerate(codeGenerator.generateMorphSequence(input), `Morph sequence: ${input.states.length} states`, options, 'pinepaper_morph_sequence');
2554
2636
  }
2637
+ case 'pinepaper_instances': {
2638
+ const input = InstancesInputSchema.parse(args);
2639
+ return executeOrGenerate(codeGenerator.generateInstances(input), `Instances: ${input.action}`, options, 'pinepaper_instances');
2640
+ }
2555
2641
  case 'pinepaper_emitter': {
2556
2642
  const input = EmitterInputSchema.parse(args);
2557
2643
  return executeOrGenerate(codeGenerator.generateEmitter(input), `Emitter: ${input.action}`, options, 'pinepaper_emitter');
@@ -4222,6 +4308,11 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
4222
4308
  if (!exportSessionManager.hasActiveJob()) {
4223
4309
  exportSessionManager.startJob({ name: 'auto_session', screenshotPolicy: 'on_complete' });
4224
4310
  }
4311
+ // The scene's motion, read before a video export so the delivered frames can be checked against it
4312
+ // (frozen-output.ts). A probe that cannot run gives no verdict, never a false one.
4313
+ const sceneMotion = /^(mp4|webm|auto|)$/.test(String(input.format ?? ''))
4314
+ ? await controller.executeCode(SCENE_MOTION_PROBE_JS, false).then((r) => (r.success ? r.result : null)).catch(() => null)
4315
+ : null;
4225
4316
  const exportBrowserResult = await controller.executeCode(code, false, {
4226
4317
  governorTimeoutMs: exportGovernorTimeoutMs(),
4227
4318
  });
@@ -4230,6 +4321,8 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
4230
4321
  return errorResult(ErrorCodes.EXECUTION_ERROR, exportBrowserResult.error || 'Export failed', { code }, { toolName: 'pinepaper_agent_export', canvasState: canvasState || undefined });
4231
4322
  }
4232
4323
  const exportResult = exportBrowserResult.result;
4324
+ correctBitrateClaim(exportResult);
4325
+ noteFrozenOutput(exportResult, sceneMotion);
4233
4326
  // TWO VERDICTS, and only one of them was being read.
4234
4327
  //
4235
4328
  // `exportBrowserResult.success` says the CODE RAN. The export's own
@@ -4283,14 +4376,20 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
4283
4376
  format,
4284
4377
  chunkBytes: Number(exportResult.chunkBytes) || undefined,
4285
4378
  }, input.platform || 'auto');
4379
+ const delivery = await deliverAacIfOpus(filePath, format, exportResult, input.platform);
4380
+ if (delivery.action === 'refuse') {
4381
+ await (await import('node:fs/promises')).unlink(filePath).catch(() => undefined);
4382
+ return errorResult(ErrorCodes.EXECUTION_ERROR, delivery.message, { code: 'audio_codec_unavailable', format, platform: input.platform }, { toolName: 'pinepaper_agent_export' });
4383
+ }
4286
4384
  const receipt = await exportFileReceiptOrReason(filePath, exportResult);
4287
- const cleanResult = { ...exportResult, data: undefined, filePath, fileSize, chunks, file: receipt };
4385
+ const deliveredSize = 'bytes' in receipt ? receipt.bytes : fileSize;
4386
+ const cleanResult = { ...exportResult, data: undefined, filePath, fileSize: deliveredSize, chunks, file: receipt, ...audioDeliveryFields(delivery) };
4288
4387
  return withEngineReport({
4289
4388
  structuredContent: boundedStructured(cleanResult),
4290
4389
  content: [{
4291
4390
  type: 'text',
4292
- text: `Export saved to file:\n\nFile: ${filePath}\nFormat: ${format}\nSize: ${(fileSize / 1024).toFixed(1)} KB\nPlatform: ${input.platform}\nPaged out of the studio's export store in ${chunks} chunk${chunks === 1 ? '' : 's'}\n${receiptErrorLine(receipt)}${'receiptError' in receipt ? '' : probeLines(receipt)}${lintLines(exportResult)}\nResult: ${JSON.stringify(cleanResult, null, 2)}`,
4293
- }, exportFileLink(filePath, fileSize)],
4391
+ text: `Export saved to file:\n\nFile: ${filePath}\nFormat: ${format}\nSize: ${(deliveredSize / 1024).toFixed(1)} KB\nPlatform: ${input.platform}\nPaged out of the studio's export store in ${chunks} chunk${chunks === 1 ? '' : 's'}\n${typeof exportResult.warning === 'string' ? `WARNING: ${exportResult.warning}\n` : ''}${audioDeliveryLine(delivery)}${receiptErrorLine(receipt)}${'receiptError' in receipt ? '' : probeLines(receipt)}${lintLines(exportResult)}\nResult: ${JSON.stringify(cleanResult, null, 2)}`,
4392
+ }, exportFileLink(filePath, deliveredSize)],
4294
4393
  }, exportBrowserResult.report);
4295
4394
  }
4296
4395
  catch (streamError) {
@@ -4345,9 +4444,15 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
4345
4444
  data.length > SAVE_THRESHOLD_BYTES);
4346
4445
  if (shouldSaveToFile) {
4347
4446
  try {
4348
- const { filePath, fileSize } = await saveExportToFile(data, format, input.platform || 'auto');
4447
+ const { filePath, fileSize: savedSize } = await saveExportToFile(data, format, input.platform || 'auto');
4448
+ const delivery = await deliverAacIfOpus(filePath, format, exportResult, input.platform);
4449
+ if (delivery.action === 'refuse') {
4450
+ await (await import('node:fs/promises')).unlink(filePath).catch(() => undefined);
4451
+ return errorResult(ErrorCodes.EXECUTION_ERROR, delivery.message, { code: 'audio_codec_unavailable', format, platform: input.platform }, { toolName: 'pinepaper_agent_export' });
4452
+ }
4349
4453
  const receipt = await exportFileReceiptOrReason(filePath, exportResult);
4350
- const cleanResult = { ...exportResult, data: undefined, filePath, fileSize, file: receipt };
4454
+ const fileSize = 'bytes' in receipt ? receipt.bytes : savedSize;
4455
+ const cleanResult = { ...exportResult, data: undefined, filePath, fileSize, file: receipt, ...audioDeliveryFields(delivery) };
4351
4456
  // A mix that dropped sounds wrote a perfectly good file that is
4352
4457
  // missing tracks. It rides the SUCCESS path, which is exactly why
4353
4458
  // it has to be said out loud here rather than left in the JSON.
@@ -4357,7 +4462,7 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
4357
4462
  content: [{
4358
4463
  type: 'text',
4359
4464
  text: `Export saved to file:\n\nFile: ${filePath}\nFormat: ${format}\nSize: ${(fileSize / 1024).toFixed(1)} KB\nPlatform: ${input.platform}\n`
4360
- + (dropNote ? `DROPPED: ${dropNote}\n` : '')
4465
+ + (dropNote ? `DROPPED: ${dropNote}\n` : '') + audioDeliveryLine(delivery)
4361
4466
  // The studio's own warning on a saved file (a GIF over its maxBytes budget, GIFMAX34) was only
4362
4467
  // inside the Result JSON below; a reader of the words missed it.
4363
4468
  + (typeof exportResult.warning === 'string' ? `WARNING: ${exportResult.warning}\n` : '')
@@ -4425,11 +4530,28 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
4425
4530
  case 'list_styles':
4426
4531
  return dataResult({ styles: designSystems.listStyles() });
4427
4532
  case 'compose': {
4533
+ // NO SIZE GIVEN MEANS YOUR CANVAS, not the style's own (RISO-RESIZE, owner 2026-11-14). A style laid out
4534
+ // at its default size (1280x720 for most) and resized the canvas to it, so a 1080x1920 project became
4535
+ // 1280x720. Where the studio is reachable, an unsized compose uses the current canvas size.
4536
+ let width = input.width, height = input.height;
4537
+ let sizeFrom = width !== undefined || height !== undefined ? 'asked' : 'style';
4538
+ if (sizeFrom === 'style' && input.draw !== false && options.executeInBrowser) {
4539
+ const ctl = options.browserController || getBrowserController();
4540
+ if (ctl.connected) {
4541
+ const cs = await ctl.executeCode('(function () { const s = app.getCanvasSize ? app.getCanvasSize() : null; return s && s.width > 0 && s.height > 0 ? { width: s.width, height: s.height } : null; })()', false)
4542
+ .then((r) => (r.success ? r.result : null)).catch(() => null);
4543
+ if (cs) {
4544
+ width = cs.width;
4545
+ height = cs.height;
4546
+ sizeFrom = 'canvas';
4547
+ }
4548
+ }
4549
+ }
4428
4550
  let scene;
4429
4551
  try {
4430
4552
  scene = designSystems.compose(input.style, {
4431
4553
  title: input.title, subtitle: input.subtitle, body: input.body,
4432
- width: input.width, height: input.height, variant: input.variant,
4554
+ width, height, variant: input.variant,
4433
4555
  });
4434
4556
  }
4435
4557
  catch (composeError) {
@@ -4443,10 +4565,12 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
4443
4565
  // Some vendored styles lay out at their OWN fixed size and ignore
4444
4566
  // width/height (swiss_typographic is always 1280x720), so a caller
4445
4567
  // asking for 1080x1080 got a resized canvas (gate run 3). Say so.
4446
- const sizeIgnored = (input.width !== undefined && input.width !== scene.width)
4447
- || (input.height !== undefined && input.height !== scene.height);
4568
+ const sizeIgnored = (width !== undefined && width !== scene.width)
4569
+ || (height !== undefined && height !== scene.height);
4448
4570
  const sizeWarning = sizeIgnored
4449
- ? `"${scene.style}" lays out at its own size, ${scene.width}x${scene.height}; the requested ${input.width ?? '?'}x${input.height ?? '?'} was not used`
4571
+ ? (sizeFrom === 'canvas'
4572
+ ? `"${scene.style}" lays out at its own size, ${scene.width}x${scene.height}, not your canvas's ${width}x${height}, so the canvas is resized to it`
4573
+ : `"${scene.style}" lays out at its own size, ${scene.width}x${scene.height}; the requested ${input.width ?? '?'}x${input.height ?? '?'} was not used`)
4450
4574
  : undefined;
4451
4575
  // BAUHAUS-SUBTITLE: at a size the style was not laid out for, its
4452
4576
  // proportions can put text across a shape (bauhaus at 1920x1080
@@ -4454,18 +4578,20 @@ You can now start creating new items on a clean canvas.${sizeNote}`,
4454
4578
  // the result names each collision rather than drawing it as intended.
4455
4579
  const collisions = designSystems.scalingCollisions(input.style, {
4456
4580
  title: input.title, subtitle: input.subtitle, body: input.body,
4457
- width: input.width, height: input.height, variant: input.variant,
4581
+ width, height, variant: input.variant,
4458
4582
  }, scene);
4459
4583
  const layoutWarning = collisions.length
4460
4584
  ? `at ${scene.width}x${scene.height} the "${scene.style}" layout runs text onto a shape it does not touch at its own default size: `
4461
4585
  + collisions.map((c) => `${c.text} over ${c.shape}`).join(', ')
4462
- + '. Compose without width/height (its default size) for the layout as designed, or move the text afterwards with pinepaper_modify_item.'
4586
+ + '. For the layout as designed, compose at the style\'s own size (draw: false without width/height returns it), or move the text afterwards with pinepaper_modify_item.'
4463
4587
  : undefined;
4464
4588
  const collisionReport = collisions.length ? { layoutCollisions: collisions, layoutWarning } : {};
4465
4589
  if (input.draw === false) {
4466
4590
  return dataResult({ scene, ops, drawn: false, ...(sizeWarning ? { warning: sizeWarning } : {}), ...collisionReport });
4467
4591
  }
4468
- const code = codeGenerator.generateDesignCompose(scene, ops, scene.style, sizeWarning, collisionReport);
4592
+ // A style that cannot lay out at your canvas size is fitted into it rather than resizing it.
4593
+ const fitTo = sizeFrom === 'canvas' && sizeIgnored && width && height ? { width, height } : undefined;
4594
+ const code = codeGenerator.generateDesignCompose(scene, ops, scene.style, sizeWarning, collisionReport, fitTo);
4469
4595
  return executeOrGenerate(code, `Composed a ${scene.style} scene (${ops.length} items)`, options, 'pinepaper_design_system');
4470
4596
  }
4471
4597
  }