Skip to content

feat(slides,drive): add createFromJson, drive primitives, and theme system#348

Open
n0012 wants to merge 5 commits into
gemini-cli-extensions:mainfrom
n0012:feat/slides-create-from-json
Open

feat(slides,drive): add createFromJson, drive primitives, and theme system#348
n0012 wants to merge 5 commits into
gemini-cli-extensions:mainfrom
n0012:feat/slides-create-from-json

Conversation

@n0012

@n0012 n0012 commented Apr 25, 2026

Copy link
Copy Markdown

What this adds

Five new tools for building Google Slides presentations programmatically, plus three new Drive primitives for safe-by-default image staging.


slides.createFromJson — blueprint-to-slides in one call

Callers describe a deck as a JSON blueprint; the server translates it into a Slides API batchUpdate. No knowledge of raw API shape required.

Color aliases — named colors, never RGB:

Alias Value Use
text #202124 near-black body text
primary #101828 dark dark backgrounds
primary_text #FFFFFF white text on dark
blue #1A73E8 Google Blue accents, labels
red/yellow/green Google brand colors brand bar
surface #F1F3F4 light gray card backgrounds
text_muted #757575 gray secondary text

Theme system — 12 named themes (google, exec, pitch, technical, workshop, dark, demo, hcls, customer, simple, google-dark, google-minimal) drive font family, accent color, and layout guidance in the tool description.

Speaker notes — include "speaker_notes" in each slide object and they're written automatically. Tool warns when notes are missing and requests a second pass.

Layer ordering — elements render shapes → images → text, then by layer value. Background shapes reliably appear behind text without manual sequencing.

Blueprint format:

{ "slides": [
    { "speaker_notes": "...", "elements": [...] },
    { "speaker_notes": "...", "elements": [...] }
  ]
}

Element schema: type (text | shape | image), position ({x,y,w,h} in points on 720×405 canvas), layer (z-index), content, url, and a style object with: size, bold, color, bg_color, no_border, align, vertical_align.


Drive primitives — split for safe-by-default uploads

The Slides API's createImage endpoint requires a publicly accessible URL (per Google's docs) — OAuth tokens in URLs are not honored. To support image-heavy workflows without making the upload tool itself dangerous, the share lifecycle is split across three tools:

  • drive.uploadFile — uploads a local file to Drive. File is PRIVATE by default (no share granted). Returns id, name, webViewLink.
  • drive.addPublicAccess — explicit opt-in: grants anyone:reader on an existing file, returns the public imageUrl and the permission ID. Surfaces Workspace publishOutNotPermitted clearly so callers can fall back to another hosting path (GCS signed URLs, etc.).
  • drive.removePublicAccess — revokes every anyone:* permission on a file. Idempotent. File stays in Drive — only the public link is closed.

Typical use for embedding a local image in a slide:

1. id       = drive.uploadFile(localPath)            # private
2. imageUrl = drive.addPublicAccess(id)              # explicit opt-in
3. slides.createFromJson(..., url: imageUrl, ...)
4. drive.removePublicAccess(id)                       # close the window

Supporting tools

  • slides.create — create a blank presentation, returns {presentationId, url}
  • slides.batchUpdate — raw Slides API request array passthrough
  • slides.getText / getMetadata / getImages / getSlideThumbnail — read tools
  • slides.getSpeakerNotes / updateSpeakerNotes — read and write speaker notes per slide

Design notes

  • All new tools registered via server.registerTool and gated through feature-config.ts. slides.write and drive.write groups carry the new tools with correct scope requirements.
  • Color alias system replaces raw RGB so output stays consistent with the active theme.
  • Speaker notes written inline from blueprint (no second pass required unless omitted).
  • Drive upload split into three primitives so the safe default is private; sharing is always explicit and reversible.

Validation

  • TypeScript build passes (npm run build)
  • Full round-trip exercised end-to-end on both personal and corporate Google accounts: presentation creation, multi-slide blueprints with shapes/text/images, color aliases, speaker notes, image upload + share + revoke.
  • On corporate Workspace domains where publishOutNotPermitted blocks addPublicAccess, the error is clearly surfaced so callers can route images through an alternative public host.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces new Google Slides tools for creating presentations, performing batch updates, and generating slides from JSON blueprints. The review feedback highlights the need for consistency by using the registerTool wrapper to respect feature flags and suggests enhancing input schemas to support structured objects. Additionally, the feedback addresses a potential crash in the createFromJson service method and recommends adjusting the slide insertion logic to append new slides to the end of a presentation.

Comment thread workspace-server/src/index.ts Outdated
slidesService.getSlideThumbnail,
);

server.registerTool(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The slides.create tool is being registered using server.registerTool directly, which bypasses the registerTool wrapper defined on line 154. This wrapper is responsible for checking if the tool is enabled via feature flags (WORKSPACE_FEATURE_OVERRIDES). Using the wrapper ensures consistency and allows users to disable these tools if needed.

Suggested change
server.registerTool(
registerTool(

);

server.registerTool(
'slides.batchUpdate',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to slides.create, this tool should be registered using the registerTool wrapper to respect feature flags.

  registerTool(

});

server.registerTool(
'slides.createFromJson',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This tool should also use the registerTool wrapper to ensure it can be managed via feature flags.

  registerTool(

Comment on lines +482 to +486
requests: z
.string()
.describe(
'JSON string of an array of Slides API request objects (e.g., [{"createSlide":{}}, {"createShape":{...}}]). Will be parsed server-side.',
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The requests field is restricted to a string, but the SlidesService.batchUpdate implementation (line 329) and most MCP clients support passing structured arrays directly. Allowing both a JSON string and an array of objects provides a better experience for AI agents.

Suggested change
requests: z
.string()
.describe(
'JSON string of an array of Slides API request objects (e.g., [{"createSlide":{}}, {"createShape":{...}}]). Will be parsed server-side.',
),
requests: z
.union([z.string(), z.array(z.any())])
.describe(
'An array of Slides API request objects or a JSON string of that array (e.g., [{"createSlide":{}}, {"createShape":{...}}]).',
),

Comment on lines +602 to +606
slideJson: z
.string()
.describe(
'JSON string of the slide blueprint. Use {"slides":[{"elements":[...]},...]} for multiple slides or {"elements":[...]} for one slide. Will be parsed server-side.',
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The slideElementSchema defined on lines 493-591 is currently unused. It should be applied to the slideJson input schema to provide the AI agent with structured validation and clear documentation of the expected blueprint format. Additionally, allowing both objects and strings makes the tool more robust.

        slideJson: z
          .union([
            z.object({
              slides: z.array(z.object({ elements: z.array(slideElementSchema) })),
            }),
            z.object({
              elements: z.array(slideElementSchema),
            }),
            z.string(),
          ])
          .describe(
            'The slide blueprint. Use {"slides":[{"elements":[...]}]} for multiple slides or {"elements":[...]} for one slide. Can be a JSON string or object.',
          ),

Comment on lines +679 to +681
const slideDefs = (slideJson as any).slides
? (slideJson as any).slides
: [{ elements: (slideJson as any).elements || [] }];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the slides format is used but an individual slide object is missing the elements property (e.g., { "slides": [{}] }), slideDefs[i].elements will be undefined. This will cause a crash in buildSlideRequests when it attempts to spread or iterate over elements (line 421).

Suggested change
const slideDefs = (slideJson as any).slides
? (slideJson as any).slides
: [{ elements: (slideJson as any).elements || [] }];
const slideDefs = (slideJson as any).slides
? (slideJson as any).slides.map((s: any) => ({ ...s, elements: s.elements || [] }))
: [{ elements: (slideJson as any).elements || [] }];

requests.push({
createSlide: {
objectId: slideId,
insertionIndex: i + 1,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Hardcoding insertionIndex: i + 1 causes new slides to always be inserted at the beginning of the presentation (after the first slide). For an 'add slides' tool, the expected behavior is usually to append slides to the end. Omitting insertionIndex entirely will cause the Slides API to append the new slides to the end of the presentation.

Suggested change
insertionIndex: i + 1,
slideLayoutReference: { predefinedLayout: 'BLANK' },

@n0012

n0012 commented Apr 25, 2026

Copy link
Copy Markdown
Author

test with gemini extensions install https://github.com/n0012/workspace

new tool that can batch create slides is slides.createFromJson

@n0012

n0012 commented Apr 29, 2026

Copy link
Copy Markdown
Author

Bug fix bundled in this PR: docs.getText field mask error

While testing this PR I hit a systematic breakage in docs_getText (and writeText/replaceText) affecting all Google Docs — including single-tab docs with no suggestions. Root cause and fix:

Root cause: docs.documents.get with includeTabsContent: true and a wildcard field mask (tabs, tabs.documentTab.body, etc.) causes the API to validate the mask as including suggestion/comment sub-fields (suggestedInsertionIds, suggestedParagraphStyleChanges, etc.). The API then rejects with:

Field mask cannot retrieve comment-specific fields when include_comments is false.

A previous attempt added suggestionsViewMode: 'PREVIEW_WITHOUT_SUGGESTIONS' to suppress suggestion data, but this doesn't help — the field mask is validated before the view-mode filter is applied.

Fix (commit 02b3502): Replaced all three documents.get calls in DocsService with explicit field masks (DOCS_READ_FIELDS, DOCS_END_INDEX_FIELDS) that enumerate only the fields _readStructuralElement actually reads — no suggestion or comment sub-fields anywhere in the tree. Handles up to 3 levels of tab nesting (Google Docs maximum) and one level of table nesting.

@n0012 n0012 force-pushed the feat/slides-create-from-json branch from 02b3502 to abf1ecd Compare May 9, 2026 19:10
@n0012 n0012 changed the title feat(slides): add slides.createFromJson — agent-friendly blueprint-to-slides tool feat(slides,drive): add createFromJson, insertImageSlide, uploadFile, and theme system May 9, 2026
@n0012 n0012 force-pushed the feat/slides-create-from-json branch from abf1ecd to 7379d66 Compare May 9, 2026 19:17
@n0012

n0012 commented May 9, 2026

Copy link
Copy Markdown
Author

@allenhutchison — would appreciate a review when you get a chance! This adds slides.createFromJson, slides.insertImageSlide, and drive.uploadFile — tools we've been using in production with Claude Code + Gemini CLI for building AI-generated slide decks. CLA is green. Happy to address any feedback.

@n0012 n0012 force-pushed the feat/slides-create-from-json branch from 7379d66 to 119b16a Compare May 10, 2026 03:19
@n0012 n0012 changed the title feat(slides,drive): add createFromJson, insertImageSlide, uploadFile, and theme system feat(slides,drive): add createFromJson, drive primitives, and theme system May 19, 2026
Comment thread workspace-server/src/index.ts Outdated

// Speaker notes tools — approach adapted from PR #235
// https://github.com/gemini-cli-extensions/workspace/pull/235 by @stefanoamorelli
server.registerTool(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[blocking] These five new tools (this one plus slides.updateSpeakerNotes at 495, slides.create at 515, slides.batchUpdate at 527, and slides.createFromJson at 652) call server.registerTool(...) directly, but every other tool in this file goes through the wrapped registerTool(...) helper defined at lines 169–182. The wrapper is what honors enabledTools from feature-config — without it, slides.write will register regardless of defaultEnabled: false. Should be a s/server.registerTool/registerTool/ on all five.

}

// Delete the default blank slide ("p") that Google creates with new presentations
try {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[blocking] Two concerns on this delete-'p' block: (1) createFromJson takes an arbitrary presentationId, so if a caller appends to an existing deck and 'p' happens to be a real slide, we silently delete it; (2) the catch {} at 869 swallows everything — auth revocation, quota, 5xx — with no log.

Could we gate the delete on an explicit isNewPresentation flag (or fetch the slide list first and only delete if 'p' exists), and at minimum log unexpected catches? Silent catches are something we try to avoid repo-wide.

],
};
} catch (error) {
return this.handleError('drive.addPublicAccess', error);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[blocking] The PR description calls out publishOutNotPermitted as a feature surface, but here we fall through to the generic handleError that just stringifies error.message — the caller gets an opaque "insufficient permissions" string with no signal that it's org policy. Could we detect error.errors?.[0]?.reason === 'publishOutNotPermitted' (and cannotShareOutsideDomain) before the generic path and return a structured response with an actionable hint?

}

// Bold phrases
if (style.bold_phrases) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[blocking] content.indexOf("", n) returns n for every position, so an empty phrase runs content.length + 1 times and emits invalid updateTextStyle requests with startIndex === endIndex — Slides API rejects the whole batchUpdate. Same shape applies to the links loop further down. Either if (!phrase) continue; at the top of each loop, or add .min(1) to the schema.

accent4?: RGB;
}

const THEMES: Record<string, Theme> = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[blocking] The PR description advertises 12 themes (exec, pitch, technical, workshop, dark, demo, hcls, customer, simple, google-dark, google-minimal) but I only see google defined here. THEMES['exec'] returns undefined and createFromJson hardcodes THEMES['google'] so the others are unreachable anyway. Either ship the missing themes or pare the description and tool docs back to what actually exists. Also are we sure we want to ship a "Google" theme in a public extension?

};
}
const removed: string[] = [];
for (const p of anyonePerms) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[important] If permission #3 of 5 fails mid-loop (race, revoked auth), we throw and removed never gets returned — caller can't tell which perms were actually deleted, which breaks the "idempotent" contract on retry. Could each delete be in its own try/catch, collecting {removed, failed} in the response?

// Sanitize URLs that contain unresolved template placeholders (e.g. from LLM output)
let imageUrl = el.url ?? '';
if (imageUrl.includes('{') || imageUrl.includes('%7B')) {
imageUrl = 'https://img.icons8.com/m_rounded/512/4285F4/info.png';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[important] Substituting an icons8 icon when the URL contains { is a reasonable fallback, but please add a warnings: [{slideIndex, elementIndex, issue: "unresolved url placeholder, substituted fallback"}] entry on the response so the caller knows it happened. Right now a malformed blueprint silently fills the deck with info icons.

content: [
{
type: 'text' as const,
text: JSON.stringify({ error: errorMessage }),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[important] Error returns from the new SlidesService methods (this create plus batchUpdate and createFromJson) don't set isError: true on the MCP response — DriveService.handleError does this correctly at line 47. MCP clients use that flag to distinguish failure from success, so without it these look like successful responses that happen to contain an error string.

return `${prefix}_${Date.now()}_${objCounter.value}`;
};

// Sort: shapes first, then images, then text; within each group by layer

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Comment says "shapes first, then images, then text", but the sort key layerVal * 10 + typeVal puts layer first with type as the tiebreaker — the opposite. The behavior matches the tool description's "lower layers render first" promise, so the code is right; just the inline comment is misleading.

}
}

// Bold until (legacy)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] // Bold until (legacy) — calling it "legacy" is confusing since it's brand new in this PR. Drop the tag, or explain why it's deprecated on arrival?

@allenhutchison

Copy link
Copy Markdown
Contributor

Hi @n0012 👋 — just checking in on this one. This is a genuinely impressive contribution: createFromJson, the Drive staging primitives, and the 12-theme system are a lot of thoughtful work, and the blueprint-to-slides approach is really nice.

I left a round of review on May 26 — 15 inline comments, mostly focused things rather than anything fundamental. No rush at all, but I wanted to make sure it didn't slip by unnoticed since it landed just after a batch of approvals.

Are you planning to keep going with it? If so, I'm happy to help work through any of the comments or clarify anything that's unclear — just let me know. And if you'd like a second pass on anything once you've pushed updates, tag me and I'll re-review promptly. Thanks again for this! 🙌

@bdoyle0182

Copy link
Copy Markdown

@n0012 if planning to continue with this one, please refactor around the baseline slides tools that just got merged into the main branch!

@n0012

n0012 commented Jun 25, 2026

Copy link
Copy Markdown
Author

Thanks, had some time away but will review again and apply feedback per these comments. Please keep an eye out @allenhutchison and @bdoyle0182

…me system

Rebuilt on top of the baseline slides tools now in main. Layers the net-new
blueprint engine and Drive image-staging primitives onto the granular
slides.*/drive.* tools without duplicating them (create/getSpeakerNotes/
updateSpeakerNotes are reused from the baseline).

New tools (all registered through the feature-config-gated registerTool wrapper):
- slides.createFromJson — JSON blueprint → slides in one batchUpdate, with a
  theme/color-alias system, inline speaker notes, and layer ordering.
- slides.batchUpdate — raw Slides API passthrough (escape hatch); accepts a
  request array or a JSON string.
- drive.uploadFile / addPublicAccess / removePublicAccess — safe-by-default
  image staging: upload is private; sharing is explicit and reversible.

Addresses review feedback:
- Themes pared to a small honest set (default + dark); neutral naming; tool
  description trimmed to what actually ships.
- createFromJson only deletes the default "p" slide when isNewPresentation and
  after confirming it exists; no more silent catch; guards missing elements.
- buildSlideRequests skips empty bold_phrases/links (no zero-length ranges);
  placeholder image URLs are reported under result.warnings.
- color/bg_color/border_color accept a theme alias string OR an RGB object, so
  the alias system is reachable from the tool boundary.
- drive.addPublicAccess returns an lh3 image URL and a structured, actionable
  hint on publishOutNotPermitted/cannotShareOutsideDomain; removePublicAccess
  isolates each delete and reports {removed, failed}.
- All new returns set isError on failure.
- Unit tests for resolveColor, createFromJson (translation/guards/warnings/
  default-slide), batchUpdate, uploadFile, add/removePublicAccess.
@n0012 n0012 force-pushed the feat/slides-create-from-json branch from 431f439 to 2ae378c Compare June 26, 2026 04:59
n0012 added 4 commits June 25, 2026 23:08
High-level editor for an existing shape: clears it, inserts new text, and
applies each style attribute with an explicit fields mask so text never
inherits stray styling. Reuses the createFromJson style vocabulary (size,
bold/italic/underline/strikethrough, color aliases or RGB, font_family,
align, indent, bold_phrases, bold_until, links) and finds shapes nested in
groups. Empty bold_phrases/links are skipped (no zero-length ranges).
Complements createFromJson for editing existing decks.
An element missing a valid position {x,y,w,h} previously threw a cryptic
"Cannot read properties of undefined (reading 'h')" mid-batch, failing the
whole deck. Now such elements are skipped and reported under result.warnings
(consistent with the placeholder-image-URL handling), so messy LLM-generated
blueprints degrade gracefully instead of crashing. Adds a unit test.
A text element with empty/whitespace content produced an empty text box and
then an updateTextStyle call that the API rejects with "object has no text",
aborting the whole batch. Now such elements are skipped and reported under
result.warnings (matching the position/placeholder guards). Adds a unit test.
…eme + theme param)

The server stays deliberately unbranded: createFromJson resolves color aliases
against one neutral built-in palette. Removed the `dark` theme and the `theme`
selector param — branding (fonts, brand colors) belongs to the caller/skill,
which applies it via explicit font_family/colors (or RGB objects for one-offs).
Simplifies the API and addresses the "don't ship a Google theme in a public
extension" review note. Tool description + test updated.
@n0012

n0012 commented Jun 26, 2026

Copy link
Copy Markdown
Author

Thanks @allenhutchison and @bdoyle0182 for the thorough review, and @gemini-code-assist for the automated pass. I've had some time away but have now worked through every comment. Summary below, grouped by reviewer. The branch is rebased on the current main and sits at 5cb49dc.

@allenhutchison — blocking

  • registerTool wrapper (the 5 tools at index.ts:481/495/515/527/652) — ✅ All slides write tools now register through the wrapped registerTool(...) helper; there are no remaining server.registerTool(...) calls. slides.write now correctly honors defaultEnabled:false and WORKSPACE_FEATURE_OVERRIDES.
  • Silent delete of slide 'p' — ✅ The delete is gated behind an explicit isNewPresentation flag (default false), and even then only fires after fetching the slide list and confirming 'p' actually exists. The previously-silent catch now logs. Appending to an existing deck can never delete a real slide.
  • publishOutNotPermitted opaque error — ✅ DriveService.addPublicAccess now detects reason === 'publishOutNotPermitted' || 'cannotShareOutsideDomain' and returns a structured, actionable hint before falling through to the generic handler.
  • Empty-phrase indexOf bug — ✅ if (!phrase) continue; / if (!linkDef.text) continue; guard both the bold_phrases and links loops, in both createFromJson and setText.
  • 12 themes vs 1 / "ship a Google theme in a public extension?" — ✅ Resolved by removing branding from the server entirely. It now ships a single neutral palette (Arial), and the theme selector param is gone. Color aliases resolve against that neutral palette. Google branding (Google Sans + brand bar) lives in the consuming skill, not the extension — which I think directly answers the public-extension concern.
  • No tests — ✅ Added SlidesService.test.ts (+438) and DriveService.test.ts (+135) covering the targets you named: resolveColor alias paths, removePublicAccess (incl. the type === 'anyone' filter), uploadFile missing-file, and a createFromJson blueprint-translation case.

@allenhutchison — important

  • lh3 image URL — ✅ addPublicAccess now returns https://lh3.googleusercontent.com/d/<id> instead of the legacy uc?export=download form.
  • font_family default doc — ✅ Reworded to "defaults to the active theme font."
  • speaker_notes MUST/REQUIRED mismatch — ✅ Softened to "strongly recommended"; the code returns the action_required hint when notes are omitted, so schema and behavior now agree.
  • Color schema only accepted RGB — ✅ slidesColorSchema = z.union([z.string(), {red,green,blue}]), applied to color/bg_color/border_color, so the alias system is reachable from the tool boundary.
  • removePublicAccess partial-failure — ✅ Each delete is isolated in its own try/catch; the response returns { removed, failed } so retries stay idempotent.
  • Silent icon fallback on placeholder URLs — ✅ Now emits a warnings entry on the response (and documented in the tool description).
  • Missing isError: true — ✅ The new SlidesService error returns now set isError: true.

@allenhutchison — nits

  • ✅ Sort comment reworded (layer is primary; type is the tiebreaker within a layer).
  • ✅ Dropped the misleading "legacy" tag on bold_until.

@gemini-code-assist

  • registerTool wrapper on all flagged tools (covered above).
  • batchUpdate.requests accepts string | array.
  • slideJson accepts the structured slideElementSchema union (or a JSON string).
  • createFromJson defaults a missing elements to [] — no crash on { "slides": [{}] }.
  • createFromJson omits insertionIndex so slides append to the end.

@bdoyle0182

  • ✅ Rebased onto current main. The PR builds on the merged baseline slides tools (addSlide/addShape/addImage/addTable) rather than redefining them — the net-new surface here is createFromJson, setText, batchUpdate, and the drive.* staging primitives.

Would really appreciate another pass when you have a chance, @allenhutchison @bdoyle0182 — happy to keep iterating on anything that's still off.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants