From 8c929699d72620316146ca0cf0cd6d30a229158e Mon Sep 17 00:00:00 2001 From: David East Date: Fri, 25 Sep 2026 05:48:35 +0000 Subject: [PATCH] feat(sdk): expose Project.getCanvasLayout and full ScreenData metadata (#365, #138) --- packages/sdk/generated/domain-map.json | 20 +++ packages/sdk/generated/src/designsystem.ts | 2 +- packages/sdk/generated/src/index.ts | 2 +- packages/sdk/generated/src/project.ts | 19 ++- .../sdk/generated/src/responses.generated.ts | 2 +- packages/sdk/generated/src/screen.ts | 2 +- packages/sdk/generated/src/stitch.ts | 2 +- .../sdk/generated/src/tool-definitions.ts | 2 +- packages/sdk/generated/src/types.generated.ts | 11 +- packages/sdk/generated/stitch-sdk.lock | 12 +- packages/sdk/src/project-ext.ts | 11 ++ packages/sdk/src/spec/canvas.ts | 22 +++ .../test/fixtures/responses/get_project.json | 19 +++ .../issue-365-138-canvas-and-metadata.test.ts | 155 ++++++++++++++++++ .../sdk/test/unit/response-fixtures.test.ts | 6 + scripts/generate-sdk.ts | 9 + 16 files changed, 282 insertions(+), 14 deletions(-) create mode 100644 packages/sdk/src/spec/canvas.ts create mode 100644 packages/sdk/test/fixtures/responses/get_project.json create mode 100644 packages/sdk/test/unit/issue-365-138-canvas-and-metadata.test.ts diff --git a/packages/sdk/generated/domain-map.json b/packages/sdk/generated/domain-map.json index 8b280bb..21096d2 100644 --- a/packages/sdk/generated/domain-map.json +++ b/packages/sdk/generated/domain-map.json @@ -38,6 +38,11 @@ "method": "downloadAssets", "reason": "filesystem_io", "specPath": "src/spec/download.ts" + }, + { + "method": "getCanvasLayout", + "reason": "complex_orchestration", + "specPath": "src/spec/canvas.ts" } ], "reference": { @@ -89,6 +94,21 @@ } }, "bindings": [ + { + "tool": "get_project", + "class": "Project", + "method": "get", + "args": { + "name": { + "from": "computed", + "template": "projects/${this.projectId}" + } + }, + "returns": { + "class": "Project", + "projection": [] + } + }, { "tool": "list_projects", "class": "Stitch", diff --git a/packages/sdk/generated/src/designsystem.ts b/packages/sdk/generated/src/designsystem.ts index c53efb3..1772798 100644 --- a/packages/sdk/generated/src/designsystem.ts +++ b/packages/sdk/generated/src/designsystem.ts @@ -3,7 +3,7 @@ DO NOT EDIT — changes will be overwritten. Source: tools-manifest.json (sha256:88ec3dfa066c...) - domain-map.json (sha256:a6177cc7e2f4...) + domain-map.json (sha256:cd10fec78b13...) */ import { type StitchToolClientSpec } from "../../src/spec/client.js"; import { StitchError } from "../../src/spec/errors.js"; diff --git a/packages/sdk/generated/src/index.ts b/packages/sdk/generated/src/index.ts index 24cfa1c..1e8d2b6 100644 --- a/packages/sdk/generated/src/index.ts +++ b/packages/sdk/generated/src/index.ts @@ -3,7 +3,7 @@ DO NOT EDIT — changes will be overwritten. Source: tools-manifest.json (sha256:88ec3dfa066c...) - domain-map.json (sha256:a6177cc7e2f4...) + domain-map.json (sha256:cd10fec78b13...) */ export { Stitch } from "./stitch.js"; export { Project } from "./project.js"; diff --git a/packages/sdk/generated/src/project.ts b/packages/sdk/generated/src/project.ts index 04ef45a..ed23564 100644 --- a/packages/sdk/generated/src/project.ts +++ b/packages/sdk/generated/src/project.ts @@ -3,7 +3,7 @@ DO NOT EDIT — changes will be overwritten. Source: tools-manifest.json (sha256:88ec3dfa066c...) - domain-map.json (sha256:a6177cc7e2f4...) + domain-map.json (sha256:cd10fec78b13...) */ import { type StitchToolClientSpec } from "../../src/spec/client.js"; import { StitchError } from "../../src/spec/errors.js"; @@ -40,6 +40,7 @@ import { ProjectData, } from "./types.generated.js"; import { + GetProjectResponse, GenerateScreenFromTextResponse, ListScreensResponse, GetScreenResponse, @@ -112,6 +113,22 @@ export class Project { return this.projectId; } + /** + * Retrieves the details of a specific Stitch project using its project name. + * Tool: get_project + */ + async get(): Promise { + try { + const raw = await this.client.callTool( + "get_project", + { name: `projects/${this.projectId}` }, + ); + return this.client.entities.resolve(Project, ["projectId"], raw); + } catch (error) { + throw StitchError.fromUnknown(error); + } + } + /** * Generates a new screen within a project from a text prompt. * Tool: generate_screen_from_text diff --git a/packages/sdk/generated/src/responses.generated.ts b/packages/sdk/generated/src/responses.generated.ts index 40bb79b..0c88c93 100644 --- a/packages/sdk/generated/src/responses.generated.ts +++ b/packages/sdk/generated/src/responses.generated.ts @@ -33,7 +33,7 @@ import { DO NOT EDIT — changes will be overwritten. Source: tools-manifest.json (sha256:88ec3dfa066c...) - domain-map.json (sha256:a6177cc7e2f4...) + domain-map.json (sha256:cd10fec78b13...) */ /** Response message for create_project. */ diff --git a/packages/sdk/generated/src/screen.ts b/packages/sdk/generated/src/screen.ts index 738f4ad..c947723 100644 --- a/packages/sdk/generated/src/screen.ts +++ b/packages/sdk/generated/src/screen.ts @@ -3,7 +3,7 @@ DO NOT EDIT — changes will be overwritten. Source: tools-manifest.json (sha256:88ec3dfa066c...) - domain-map.json (sha256:a6177cc7e2f4...) + domain-map.json (sha256:cd10fec78b13...) */ import { type StitchToolClientSpec } from "../../src/spec/client.js"; import { StitchError } from "../../src/spec/errors.js"; diff --git a/packages/sdk/generated/src/stitch.ts b/packages/sdk/generated/src/stitch.ts index 65ddd51..c73573c 100644 --- a/packages/sdk/generated/src/stitch.ts +++ b/packages/sdk/generated/src/stitch.ts @@ -3,7 +3,7 @@ DO NOT EDIT — changes will be overwritten. Source: tools-manifest.json (sha256:88ec3dfa066c...) - domain-map.json (sha256:a6177cc7e2f4...) + domain-map.json (sha256:cd10fec78b13...) */ import { type StitchToolClientSpec } from "../../src/spec/client.js"; import { StitchError } from "../../src/spec/errors.js"; diff --git a/packages/sdk/generated/src/tool-definitions.ts b/packages/sdk/generated/src/tool-definitions.ts index 1bdbc83..30717bf 100644 --- a/packages/sdk/generated/src/tool-definitions.ts +++ b/packages/sdk/generated/src/tool-definitions.ts @@ -3,7 +3,7 @@ DO NOT EDIT — changes will be overwritten. Source: tools-manifest.json (sha256:88ec3dfa066c...) - domain-map.json (sha256:a6177cc7e2f4...) + domain-map.json (sha256:cd10fec78b13...) */ /** JSON Schema property descriptor for a tool parameter. */ export interface ToolPropertySchema { diff --git a/packages/sdk/generated/src/types.generated.ts b/packages/sdk/generated/src/types.generated.ts index 06a680a..5d2e926 100644 --- a/packages/sdk/generated/src/types.generated.ts +++ b/packages/sdk/generated/src/types.generated.ts @@ -3,7 +3,7 @@ DO NOT EDIT — changes will be overwritten. Source: tools-manifest.json (sha256:88ec3dfa066c...) - domain-map.json (sha256:a6177cc7e2f4...) + domain-map.json (sha256:cd10fec78b13...) */ /** A component style token in a design system. */ @@ -682,6 +682,15 @@ export interface ScreenData { title?: string; htmlCode?: File; screenshot?: File; + width?: string | number; + height?: string | number; + deviceType?: string; + screenType?: string; + generatedBy?: string; + prompt?: string; + theme?: DesignTheme; + designSystem?: DesignSystemInput; + screenMetadata?: ScreenMetadata; [key: string]: any; } diff --git a/packages/sdk/generated/stitch-sdk.lock b/packages/sdk/generated/stitch-sdk.lock index 685c2cb..0e90b63 100644 --- a/packages/sdk/generated/stitch-sdk.lock +++ b/packages/sdk/generated/stitch-sdk.lock @@ -1,10 +1,10 @@ { "schemaVersion": 1, "generated": { - "generatedAt": "2026-09-25T04:45:34.219Z", - "sourceHash": "sha256:aad79acad078a250f95d5e58767a9082c41be37d4f5565bf307c96f1cd654b38", + "generatedAt": "2026-09-25T05:45:19.883Z", + "sourceHash": "sha256:77d5e0d5f2a9dd242fda6962c86e75615a3211e0a8d6eea30a47e2b96bc3d24c", "manifestHash": "sha256:88ec3dfa066ce5060be236b0a5c0b5fb51671a7dd60d05556c028e6c4c8f0e1c", - "domainMapHash": "sha256:a6177cc7e2f44d2d536928d4e0ae7bd51ed2dd3393235787216b976498c3b1b2", + "domainMapHash": "sha256:cd10fec78b1365854d5453e47970c4c73896e32bb6b33255baeb5228851e5b89", "fileCount": 8, "repairedTools": [ "generate_screen_from_text", @@ -13,11 +13,11 @@ ] }, "domainMap": { - "generatedAt": "2026-09-23T18:36:09.460Z", - "sourceHash": "sha256:a6177cc7e2f44d2d536928d4e0ae7bd51ed2dd3393235787216b976498c3b1b2", + "generatedAt": "2026-09-25T05:40:41.689Z", + "sourceHash": "sha256:cd10fec78b1365854d5453e47970c4c73896e32bb6b33255baeb5228851e5b89", "manifestHash": "sha256:88ec3dfa066ce5060be236b0a5c0b5fb51671a7dd60d05556c028e6c4c8f0e1c", "classCount": 4, - "bindingCount": 15 + "bindingCount": 16 }, "manifest": { "capturedAt": "2026-09-23T18:42:56.588Z", diff --git a/packages/sdk/src/project-ext.ts b/packages/sdk/src/project-ext.ts index 05a4e29..5fd28e6 100644 --- a/packages/sdk/src/project-ext.ts +++ b/packages/sdk/src/project-ext.ts @@ -25,6 +25,7 @@ */ import { Project as GeneratedProject } from "../generated/src/project.js"; +import type { ScreenInstance } from "../generated/src/types.generated.js"; import { Screen } from "./screen-ext.js"; import { EntityManager } from "./entity-manager.js"; import { StitchError, StitchErrorCode } from "./spec/errors.js"; @@ -115,6 +116,16 @@ export class Project extends GeneratedProject { warnings: result.warnings ?? [], }; } + + /** + * Retrieves the project's canvas layout (`screenInstances`) containing + * placement coordinates (`x`, `y`, `width`, `height`), labels, and + * `sourceScreen` resource names (#365). + */ + async getCanvasLayout(): Promise { + const updated = await this.get(); + return updated.data?.screenInstances ?? []; + } } EntityManager.registerImplementation("Project", Project); diff --git a/packages/sdk/src/spec/canvas.ts b/packages/sdk/src/spec/canvas.ts new file mode 100644 index 0000000..78e609e --- /dev/null +++ b/packages/sdk/src/spec/canvas.ts @@ -0,0 +1,22 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { ScreenInstance } from "../../generated/src/types.generated.js"; + +/** + * Contract for reading the Stitch project canvas layout (`screenInstances`) (#365). + */ +export interface CanvasLayoutSpec { + getCanvasLayout(): Promise; +} diff --git a/packages/sdk/test/fixtures/responses/get_project.json b/packages/sdk/test/fixtures/responses/get_project.json new file mode 100644 index 0000000..ee8ce2a --- /dev/null +++ b/packages/sdk/test/fixtures/responses/get_project.json @@ -0,0 +1,19 @@ +{ + "name": "projects/p-fix", + "title": "Fixture Project", + "visibility": "PRIVATE", + "projectType": "PROJECT_DESIGN", + "deviceType": "DESKTOP", + "screenInstances": [ + { + "id": "inst-1", + "sourceScreen": "projects/p-fix/screens/s-1", + "x": 100, + "y": 200, + "width": 1280, + "height": 800, + "deviceType": "DESKTOP", + "label": "Home Screen" + } + ] +} diff --git a/packages/sdk/test/unit/issue-365-138-canvas-and-metadata.test.ts b/packages/sdk/test/unit/issue-365-138-canvas-and-metadata.test.ts new file mode 100644 index 0000000..7d92e98 --- /dev/null +++ b/packages/sdk/test/unit/issue-365-138-canvas-and-metadata.test.ts @@ -0,0 +1,155 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { describe, it, expect, vi } from "vitest"; +import { StitchToolClient, Stitch } from "../../src/index.js"; + +describe("Issues #365 & #138: Project.get() / getCanvasLayout() and Screen metadata preservation", () => { + it("fetches project metadata via project.get() and returns ScreenInstance[] via project.getCanvasLayout() (#365)", async () => { + const client = new StitchToolClient({ apiKey: "test-key" }); + client["isConnected"] = true; + + const screenInstances = [ + { + id: "uuid-1", + sourceScreen: "projects/p1/screens/s1", + label: "Login", + x: 100, + y: 200, + width: 390, + height: 844, + }, + { + id: "uuid-2", + sourceScreen: "projects/p1/screens/s2", + label: "Home", + x: 600, + y: 200, + width: 390, + height: 844, + }, + ]; + + client["client"].callTool = vi + .fn() + .mockImplementation(async ({ name, arguments: args }: any) => { + if (name === "get_project") { + expect(args).toEqual({ name: "projects/p1" }); + return { + isError: false, + content: [], + structuredContent: { + name: "projects/p1", + title: "Canvas App", + screenInstances, + }, + }; + } + throw new Error(`Unexpected tool call: ${name}`); + }); + + const sdk = new Stitch(client); + const project = sdk.project("p1"); + + const fetched = await project.get(); + expect(fetched).toBe(project); + expect(project.title).toBe("Canvas App"); + + const layout = await project.getCanvasLayout(); + expect(layout).toEqual(screenInstances); + }); + + it("preserves rich Screen metadata (theme, designSystem, screenMetadata, prompt) when get_screen returns a partial representation (#138)", async () => { + const client = new StitchToolClient({ apiKey: "test-key" }); + client["isConnected"] = true; + + client["client"].callTool = vi + .fn() + .mockImplementation(async ({ name }: any) => { + if (name === "generate_screen_from_text") { + return { + isError: false, + content: [], + structuredContent: { + outputComponents: [ + { + design: { + screens: [ + { + name: "projects/p1/screens/s1", + id: "s1", + title: "Dashboard", + prompt: "Modern analytics dashboard", + theme: { designMd: "# Dark Theme\nPrimary: #00FF88" }, + designSystem: { name: "assets/ds-1" }, + screenMetadata: { agentVersion: "v2" }, + }, + ], + }, + }, + ], + }, + }; + } + if (name === "get_screen") { + // Simulate backend #138: get_screen returns htmlCode/screenshot but omits theme/designSystem/prompt + return { + isError: false, + content: [], + structuredContent: { + name: "projects/p1/screens/s1", + id: "s1", + title: "Dashboard", + htmlCode: { + downloadUrl: "https://example.com/s1.html", + }, + }, + }; + } + if (name === "list_screens") { + return { + isError: false, + content: [], + structuredContent: { + screens: [ + { + name: "projects/p1/screens/s1", + id: "s1", + title: "Dashboard", + htmlCode: { + downloadUrl: "https://example.com/s1.html", + }, + }, + ], + }, + }; + } + throw new Error(`Unexpected tool call: ${name}`); + }); + + const sdk = new Stitch(client); + const project = sdk.project("p1"); + + const gen = await project.generate("Modern analytics dashboard"); + expect(await gen.getHtmlUrl()).toBe("https://example.com/s1.html"); + + const [reloaded] = await project.screens(); + expect(reloaded.data?.theme?.designMd).toBe( + "# Dark Theme\nPrimary: #00FF88", + ); + expect(reloaded.data?.designSystem).toEqual({ name: "assets/ds-1" }); + expect(reloaded.data?.screenMetadata).toEqual({ agentVersion: "v2" }); + expect(reloaded.data?.prompt).toBe("Modern analytics dashboard"); + }); +}); diff --git a/packages/sdk/test/unit/response-fixtures.test.ts b/packages/sdk/test/unit/response-fixtures.test.ts index 5526614..13171b4 100644 --- a/packages/sdk/test/unit/response-fixtures.test.ts +++ b/packages/sdk/test/unit/response-fixtures.test.ts @@ -93,6 +93,12 @@ const BINDING_CASES: Record Promise> = { const result = await new Stitch(client).createProject({ title: "x" }); expect(result.id).toBe("p-new"); }, + "Project.get": async () => { + arm("get_project"); + const result = await project().get(); + expect(result.id).toBe("p-fix"); + expect(result.title).toBe("Fixture Project"); + }, "Project.generate": async () => { arm("generate_screen_from_text"); const result = await project().generate("a page"); diff --git a/scripts/generate-sdk.ts b/scripts/generate-sdk.ts index a727675..17bb806 100644 --- a/scripts/generate-sdk.ts +++ b/scripts/generate-sdk.ts @@ -1026,6 +1026,15 @@ async function main() { ` title?: string;\n` + ` htmlCode?: File;\n` + ` screenshot?: File;\n` + + ` width?: string | number;\n` + + ` height?: string | number;\n` + + ` deviceType?: string;\n` + + ` screenType?: string;\n` + + ` generatedBy?: string;\n` + + ` prompt?: string;\n` + + ` theme?: DesignTheme;\n` + + ` designSystem?: DesignSystemInput;\n` + + ` screenMetadata?: ScreenMetadata;\n` + ` [key: string]: any;\n` + `}`, );