Skip to content

Commit 0459a2a

Browse files
aaronbrezelclaude
andauthored
V0 Release (#43)
* docs: add sidebar feature parity design doc Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add sidebar feature parity implementation plan Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * build: copy Sidebar.html to dist/ alongside appsscript.json Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add Sidebar.html for persistent sidebar UI Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * build: update rollup footer stubs for showSidebar and runTool Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: update menu tests for sidebar UX (failing) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: replace menu with sidebar — add showSidebar and runTool Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: restore importDriveLinks, fix test mocks for runTool dispatch Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: add beforeEach clearAllMocks to runTool describe block Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: pass event explicitly in Sidebar run() to avoid window.event global Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: allow .html files through .claspignore so Sidebar.html is pushed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: replace menu with persistent sidebar (feature parity) (#12) (#13) * docs: add sidebar feature parity design doc * docs: add sidebar feature parity implementation plan * build: copy Sidebar.html to dist/ alongside appsscript.json * feat: add Sidebar.html for persistent sidebar UI * build: update rollup footer stubs for showSidebar and runTool * test: update menu tests for sidebar UX (failing) * feat: replace menu with sidebar — add showSidebar and runTool * fix: restore importDriveLinks, fix test mocks for runTool dispatch * test: add beforeEach clearAllMocks to runTool describe block * fix: pass event explicitly in Sidebar run() to avoid window.event global * fix: allow .html files through .claspignore so Sidebar.html is pushed --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * Dev default branch (#14) * feat: replace menu with persistent sidebar (feature parity) (#12) * docs: add sidebar feature parity design doc Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add sidebar feature parity implementation plan Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * build: copy Sidebar.html to dist/ alongside appsscript.json Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add Sidebar.html for persistent sidebar UI Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * build: update rollup footer stubs for showSidebar and runTool Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: update menu tests for sidebar UX (failing) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: replace menu with sidebar — add showSidebar and runTool Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: restore importDriveLinks, fix test mocks for runTool dispatch Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: add beforeEach clearAllMocks to runTool describe block Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: pass event explicitly in Sidebar run() to avoid window.event global Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: allow .html files through .claspignore so Sidebar.html is pushed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * setting app script ids and develop hooks --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add GitHub Actions clasp deployment design Covers WIF + --adc as primary approach and custom OAuth + .clasprc.json as a documented fallback if the experimental --adc flag proves unreliable. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add GitHub Actions clasp deployment implementation plan Step-by-step plan covering GCP WIF setup, GitHub Environments config, deploy.yml creation, actionlint validation, and end-to-end verification. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add deploy workflow with WIF authentication Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: use npx clasp to ensure local binary resolves in CI Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * adding visible test change * removing test change * updating worklow for debugging purposes * rmoving worklow for debugging purposes * fix: use access_token format to bridge WIF credentials into clasp Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: write clasprc in v3 format (tokens.default) not legacy v1 format * fix: clear GOOGLE_APPLICATION_CREDENTIALS in deploy step and add authorized_user type to clasprc Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: provide all required UserRefreshClient fields in clasprc to pass fromJSON validation * test change * docs: update deploy design doc with implementation history and current status WIF + service account approach is blocked by intentional Google limitation: Apps Script API does not support service account authentication. Documents all attempts made, root causes found, and promotes fallback (custom OAuth + stored .clasprc.json) as the recommended path forward. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove deploy.yml pending fallback auth implementation WIF + service account approach is blocked by Apps Script API's intentional rejection of service accounts. Last working version preserved at 407df59. Restore with: git show 407df59:.github/workflows/deploy.yml > .github/workflows/deploy.yml See docs/plans/2026-02-19-github-actions-clasp-deploy-design.md for full history. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: replace callGeminiAPI with GeminiRequest options object interface (#15) * docs: add Gemini API interface design doc Replaces AIContext discriminated union with a GeminiRequest options object. Separates Drive I/O from the HTTP adapter, exposes buildGeminiPayload as a pure testable function, and adds hooks for tools/function calling. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add Gemini API interface implementation plan 7-task TDD plan covering types, api.ts rewrite, fetchAndEncodeFile, getAIContext removal, and runBatchAI update. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: replace callGeminiAPI with GeminiRequest options object interface - Add GeminiRequest, GeminiInlineData, GeminiFunctionDeclaration, GeminiGenerationConfig interfaces to types.ts - Remove AIContext, TextContext, FileContext from types.ts - Split api.ts into buildGeminiPayload (pure) + callGeminiAPI(req) - Add fetchAndEncodeFile to drive.ts; remove Drive I/O from api.ts - Remove getAIContext from utils.ts; inline skip logic in runBatchAI - Update all tests accordingly Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: remove as-any cast in callGeminiAPI, use CONFIG.MODEL_NAME in test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: cover createSeededRandom no-seed branch, fix utils coverage threshold Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: upgrade GeminiRequest.inlineData to array for multi-file support (#17) * feat: upgrade GeminiRequest.inlineData to array for multi-file support Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: assert full inline_data object shape in multi-item test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add SSI Sheets custom function (text-only) (#18) * docs: add GEMINI custom function design doc Covers GeminiRequest.inlineData array upgrade, customFunctions.ts module, input normalization, tool registry, wiring, and test plan. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add GEMINI custom function implementation plan 5-task TDD plan: inlineData array upgrade, custom function with tests, Rollup wiring, coverage threshold, final verification. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add GEMINI custom function with tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: address quality review issues in GEMINI custom function - Validate extractId return value before fetchAndEncodeFile to surface actionable Drive URL errors rather than an opaque DriveApp exception - Replace unsafe (e as Error) cast with instanceof guard in catch block - Export TOOL_REGISTRY so tests can inject entries; add happy-path toolNames test verifying the declaration reaches the API payload Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: wire GEMINI custom function into the Apps Script bundle Re-export GEMINI from index.ts and add a global function stub in the Rollup footer so Apps Script can discover and invoke the custom function. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: add coverage thresholds for customFunctions.ts Observed: 96% statements, 90.9% branches, 100% functions. Thresholds set to 90/85/100 per design doc (≈5 points below observed), following the same margin used for api.ts, utils.ts, and drive.ts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: rename GEMINI custom function to SSI Google Sheets ships a built-in GEMINI function that shadows any user-defined function with the same name, preventing custom functions from appearing in autocomplete or executing correctly. Rename the function and all error string prefixes to SSI throughout: customFunctions.ts, index.ts, rollup.config.js, and the test file. Usage: =SSI(userTexts, inlineData?, systemPrompt?, toolNames?) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: add @customfunction JSDoc to SSI footer stub Google Sheets only registers a function as a custom function when the @customfunction tag appears in a JSDoc comment directly on the global function declaration. The TypeScript-level JSDoc is compiled away by Rollup, so the tag must be added explicitly to the footer stub. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: document @customfunction JSDoc requirement for footer stubs - CLAUDE.md: expand the two-step wiring rule into a three-step rule for custom functions, explaining that @customfunction must appear on the global stub in rollup.config.js (not just in the TypeScript source) - CLAUDE.md: correct the PropertiesService constraint — it IS available in custom functions after the add-on has been authorized - CLAUDE.md: update module dependency graph to include customFunctions.ts - rollup.config.js: add a prominent comment in the footer block warning that custom function stubs require a @customfunction JSDoc tag Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: remove inlineData parameter from SSI custom function Drive file access is not available from AuthMode.CUSTOM_FUNCTION context. Defer inlineData support to feature/ssi-inline-data. - SSI(): remove inlineData? parameter; signature is now (userTexts, systemPrompt?, toolNames?) - fetchAndEncodeFile: revert to DriveApp (REST API version was custom-function-only) - rollup.config.js: update SSI footer stub to 3-parameter signature - Tests: remove inlineData mocks and tests; revert drive.test.ts to DriveApp pattern - CLAUDE.md: remove AuthMode.CUSTOM_FUNCTION limitation notes (belong in inlineData PR) * test: cover null userTexts path in flattenArg Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * quick docs update * feat: add inlineData parameter to SSI custom function (deferred — Drive auth blocked) (#19) * fix: replace DriveApp with Drive REST API in fetchAndEncodeFile DriveApp is unavailable in Google Apps Script custom function execution contexts, causing "permissions not sufficient" errors when SSI() is called with an inlineData argument. UrlFetchApp and ScriptApp.getOAuthToken() work in all contexts and are the GAS-documented pattern for this case. fetchAndEncodeFile now makes two Drive REST API calls via UrlFetchApp: one for file metadata (mimeType + size) and one for file content. Error responses from each call surface a descriptive message. Update drive.test.ts to mock the Drive REST API calls instead of DriveApp, and add fallback-message tests to restore 100% branch coverage on drive.ts. Update customFunctions.test.ts to use mockDriveApiFile() helper and a getGeminiPayload() helper that locates the Gemini call by URL regardless of how many Drive calls precede it. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: document DriveApp restriction in custom function contexts Custom functions cannot use DriveApp or other OAuth-requiring services. Document the UrlFetchApp + ScriptApp.getOAuthToken() pattern as the correct alternative, with a pointer to fetchAndEncodeFile as an example. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: clarify Drive access limitation in custom function context Custom functions run in AuthMode.CUSTOM_FUNCTION, which scopes ScriptApp.getOAuthToken() to spreadsheets.currentonly only. Drive API calls with this token return 401 — not a permissions error, but an auth scope mismatch that cannot be resolved without a service account. - fetchAndEncodeFile: guard for null/insufficient token with a clear error pointing users to the Run AI menu tool - customFunctions.ts: revert the auth-error heuristic (no longer needed now that the error surfaces directly from fetchAndEncodeFile) - drive.ts / CLAUDE.md: accurately document the AuthMode.CUSTOM_FUNCTION constraint, why ScriptApp.getOAuthToken() doesn't help, and the service-account alternative with its UX trade-off - drive.test.ts: update null-token test assertion to match new message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: restore inlineData parameter in SSI rollup footer stub The cleanup commit on feature/ssi-custom-function removed inlineData from the footer stub, but feature/ssi-inline-data re-adds it to customFunctions.ts. The stub was out of sync, silently dropping the inlineData argument. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * Revert "feat: add inlineData parameter to SSI custom function (deferred — Drive auth blocked) (#19)" This reverts commit 4882a8ba49bd628a74e823295027a8fefa417c42. * missing docs * docs: add design for consolidating Gemini call path Introduces invokeGemini as the single production entry point for Gemini calls, moves flattenArg to utils.ts, and extracts TOOL_REGISTRY to its own tools.ts module. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: update consolidation plan to include runInference handler Adds inference.ts module with runInference — a unified handler that normalizes cell inputs, encodes Drive files, calls invokeGemini, and writes the result to an output cell. Simplifies runBatchAI loop. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: move flattenArg to utils.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: extract TOOL_REGISTRY to tools.ts * feat: add invokeGemini as single Gemini entry point Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: update api.test.ts header comment to reflect PropertiesService mock * docs: update inference handler to return string|null, no SpreadsheetApp dependency * feat: add runInference unified inference handler Implements runInference in src/server/inference.ts — a Sheets-free function that normalizes raw cell values (scalar or 2D range) into a Gemini request via invokeGemini, returning the response string, an "Error: ..." string on failure, or null to signal an empty-row skip. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: flattenArg returns [] for empty string scalar, remove redundant filter in runInference * refactor: SSI delegates to invokeGemini, imports from utils and tools Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: runBatchAI loop delegates to runInference Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: consolidate Gemini call path (#21) * docs: add design for consolidating Gemini call path Introduces invokeGemini as the single production entry point for Gemini calls, moves flattenArg to utils.ts, and extracts TOOL_REGISTRY to its own tools.ts module. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: update consolidation plan to include runInference handler Adds inference.ts module with runInference — a unified handler that normalizes cell inputs, encodes Drive files, calls invokeGemini, and writes the result to an output cell. Simplifies runBatchAI loop. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: move flattenArg to utils.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: extract TOOL_REGISTRY to tools.ts * feat: add invokeGemini as single Gemini entry point Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: update api.test.ts header comment to reflect PropertiesService mock * docs: update inference handler to return string|null, no SpreadsheetApp dependency * feat: add runInference unified inference handler Implements runInference in src/server/inference.ts — a Sheets-free function that normalizes raw cell values (scalar or 2D range) into a Gemini request via invokeGemini, returning the response string, an "Error: ..." string on failure, or null to signal an empty-row skip. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: flattenArg returns [] for empty string scalar, remove redundant filter in runInference * refactor: SSI delegates to invokeGemini, imports from utils and tools Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: runBatchAI loop delegates to runInference Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: dynamic column mapping for Run AI (#22) * docs: add dynamic column mapping design for runBatchAI Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add dynamic column mapping implementation plan Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: make runInference driveLinks and systemPrompt optional params Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: update stale JSDoc and test descriptions for optional runInference params Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add resolveColumns pure helper to utils Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: replace AIMode/ColumnMap with RunConfig, rewrite runBatchAI, add getSheetHeaders Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: guard empty sheet in runBatchAI, sync rollup footer stubs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: delete dialog.ts — replaced by sidebar config panel Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: replace AI modal with inline sidebar config panel Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove dead markup in sidebar, final verification Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: replace checkbox lists with pill tags, reset run button after success Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: port pill tag UX to system prompt and output column fields Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: sidebar refactor design — client TS/CSS split with RunConfig autopopulation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: sidebar refactor implementation plan Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add google.d.ts type stub and DOM lib for client code Add src/client/google.d.ts declaring the GoogleScriptRun interface and google.script.run global for sidebar client TypeScript. Add DOM to tsconfig lib to enable browser API types. Fix MimeType global collision in drive.ts by casting through unknown to GoogleAppsScript.Base.MimeType. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: use tsconfig.client.json for client types, fix withFailureHandler type - Revert DOM lib from shared tsconfig.json to avoid MimeType collision with GAS - Add tsconfig.client.json that extends tsconfig.json with DOM lib for src/client - Revert GASMimeType cast workaround in drive.ts; MimeType enum works directly now - Update typecheck script to run both tsc --noEmit and tsc -p tsconfig.client.json --noEmit - Add jest transform entry to use tsconfig.client.json for src/client source files - Fix withFailureHandler type: GAS always passes Error, not Error | string Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: extract Sidebar CSS to sidebar.css, drop Google Fonts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add sidebar.ts skeleton with exported stubs Adds src/client/sidebar.ts with empty exported stubs for all pure helper functions, using void expressions to satisfy noUnusedParameters. Also excludes src/client from the base tsconfig so DOM-dependent client code is only checked under tsconfig.client.json (which includes the DOM lib). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: correct import path in google.d.ts * feat: implement buildTagList with tests Add jest-environment-jsdom dependency and sidebar.test.ts with 4 TDD tests for buildTagList. Implement buildTagList to render .tag buttons, support pre-selection, toggle .selected on click, and clear container on re-render. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: pin jest-environment-jsdom to jest 29 major version Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: implement buildSingleTagList with tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: implement handleRowRangeChange with tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: implement applyPreset with tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: implement assembleRunConfig with tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: applyPreset reveals new-col-input for __new__ preset; add sidebar coverage threshold Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: restrict tsconfig types to prevent jsdom MimeType collision Add "types": ["google-apps-script", "jest"] to tsconfig.json so that @types/jsdom (pulled in by jest-environment-jsdom) no longer bleeds its DOM MimeType interface into server-side compilation, eliminating the need for the (MimeType as unknown as GoogleAppsScript.Base.MimeType) casts in drive.ts. Add tsconfig.test.client.json (DOM lib, no jsdom in types) and wire it into jest.config.cjs via a per-pattern transform for sidebar.test.ts, so the jsdom-environment test keeps its HTMLElement / document types without affecting server compilation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: declare global in google.d.ts and clear inherited exclude in client tsconfigs google.d.ts used a bare `declare const google` in a module file (file had a top-level import), scoping it locally rather than globally. Wrapping in `declare global {}` makes it a true ambient global visible to sidebar.ts. tsconfig.client.json and tsconfig.test.client.json both extended the base tsconfig which excludes src/client. Neither overrode `exclude`, so their `include` globs for src/client/**/*.ts were silently ignored. Adding `"exclude": []` clears the inherited exclusion and lets the files be compiled. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add showAIPanel, hideAIPanel, dispatchTool, runAI, init Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: convert Sidebar.html to build template with id-wired buttons Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add inlineSidebarHtml Rollup plugin and client build config Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove manual Sidebar.html copy from build scripts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove redundant tsconfig.test.client.json, consolidate to tsconfig.client.json Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: suppress sourcemap warning in client Rollup config Client JS is inlined into dist/Sidebar.html and the chunk is deleted, so sourcemaps serve no purpose. Disable them in the typescript plugin. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: sidebar TypeScript/CSS refactor with RunConfig autopopulation (#23) * docs: sidebar refactor design — client TS/CSS split with RunConfig autopopulation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: sidebar refactor implementation plan Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add google.d.ts type stub and DOM lib for client code Add src/client/google.d.ts declaring the GoogleScriptRun interface and google.script.run global for sidebar client TypeScript. Add DOM to tsconfig lib to enable browser API types. Fix MimeType global collision in drive.ts by casting through unknown to GoogleAppsScript.Base.MimeType. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: use tsconfig.client.json for client types, fix withFailureHandler type - Revert DOM lib from shared tsconfig.json to avoid MimeType collision with GAS - Add tsconfig.client.json that extends tsconfig.json with DOM lib for src/client - Revert GASMimeType cast workaround in drive.ts; MimeType enum works directly now - Update typecheck script to run both tsc --noEmit and tsc -p tsconfig.client.json --noEmit - Add jest transform entry to use tsconfig.client.json for src/client source files - Fix withFailureHandler type: GAS always passes Error, not Error | string Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: extract Sidebar CSS to sidebar.css, drop Google Fonts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add sidebar.ts skeleton with exported stubs Adds src/client/sidebar.ts with empty exported stubs for all pure helper functions, using void expressions to satisfy noUnusedParameters. Also excludes src/client from the base tsconfig so DOM-dependent client code is only checked under tsconfig.client.json (which includes the DOM lib). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: correct import path in google.d.ts * feat: implement buildTagList with tests Add jest-environment-jsdom dependency and sidebar.test.ts with 4 TDD tests for buildTagList. Implement buildTagList to render .tag buttons, support pre-selection, toggle .selected on click, and clear container on re-render. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: pin jest-environment-jsdom to jest 29 major version Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: implement buildSingleTagList with tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: implement handleRowRangeChange with tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: implement applyPreset with tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: implement assembleRunConfig with tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: applyPreset reveals new-col-input for __new__ preset; add sidebar coverage threshold Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: restrict tsconfig types to prevent jsdom MimeType collision Add "types": ["google-apps-script", "jest"] to tsconfig.json so that @types/jsdom (pulled in by jest-environment-jsdom) no longer bleeds its DOM MimeType interface into server-side compilation, eliminating the need for the (MimeType as unknown as GoogleAppsScript.Base.MimeType) casts in drive.ts. Add tsconfig.test.client.json (DOM lib, no jsdom in types) and wire it into jest.config.cjs via a per-pattern transform for sidebar.test.ts, so the jsdom-environment test keeps its HTMLElement / document types without affecting server compilation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: declare global in google.d.ts and clear inherited exclude in client tsconfigs google.d.ts used a bare `declare const google` in a module file (file had a top-level import), scoping it locally rather than globally. Wrapping in `declare global {}` makes it a true ambient global visible to sidebar.ts. tsconfig.client.json and tsconfig.test.client.json both extended the base tsconfig which excludes src/client. Neither overrode `exclude`, so their `include` globs for src/client/**/*.ts were silently ignored. Adding `"exclude": []` clears the inherited exclusion and lets the files be compiled. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add showAIPanel, hideAIPanel, dispatchTool, runAI, init Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: convert Sidebar.html to build template with id-wired buttons Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add inlineSidebarHtml Rollup plugin and client build config Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove manual Sidebar.html copy from build scripts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove redundant tsconfig.test.client.json, consolidate to tsconfig.client.json Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: suppress sourcemap warning in client Rollup config Client JS is inlined into dist/Sidebar.html and the chunk is deleted, so sourcemaps serve no purpose. Disable them in the typescript plugin. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: split sidebar into pure helpers and GAS-coupled entry point Move showAIPanel, hideAIPanel, dispatchTool, runAI, and init() from sidebar.ts into a new sidebar-entry.ts, mirroring the server-side pattern where index.ts (GAS-coupled) is excluded from coverage and pure modules (api.ts, utils.ts, etc.) hold high thresholds. sidebar-entry.ts is excluded from collectCoverageFrom; sidebar.ts now hits 100% statements/functions and 86% branches against its existing 95/81/95 thresholds with no istanbul-ignore annotations needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * adding npm fixes * docs: sidebar entry point testing design — fixture reuse + callback coverage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: sidebar entry point testing implementation plan Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: add shared sidebar DOM fixture module FULL_SIDEBAR_HTML reads from src/Sidebar.html at test time so fixtures stay structurally in sync with the template without manual drift. Adds testPathIgnorePatterns and helpers transform rule to jest.config.cjs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: sidebar tests use shared fixture module Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: scope Node types to fixture file, tighten tsconfig.client.json include Use /// <reference types="node" /> in sidebar-fixtures.ts instead of adding "node" to tsconfig.client.json types array, preventing a potential MimeType collision with google-apps-script types. Replace __tests__/**/*.ts glob with explicit paths so server-side test files are not type-checked under the DOM+GAS environment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: export sidebar-entry functions for testing * test: hideAIPanel and showAIPanel callback tests * test: dispatchTool loading state and runTool callback tests * test: runAI assembly, runBatchAI dispatch, and callback tests * test: sidebar-entry callback tests (hideAIPanel, showAIPanel, dispatchTool, runAI) Export showAIPanel, hideAIPanel, dispatchTool, runAI from sidebar-entry.ts for testing. Add __tests__/sidebar-entry.test.ts with 16 tests covering success/failure callbacks via mock capture pattern. Fix tsconfig.client.json rootDir to cover __tests__/ files without rootDir conflict. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: include sidebar-entry.ts in coverage with per-file thresholds Remove exclusion from collectCoverageFrom; add threshold block at observed actuals minus 5 points (82/52/52 stmts/branches/fns). Low branches and functions reflect init()'s untested addEventListener wiring — init() runs at module load before beforeEach sets up the DOM. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: update sidebar-entry.ts header — file is now partially covered Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: update CLAUDE.md with client build pipeline and sidebar test rig (#24) - Document two-config Rollup setup: Config 2 compiles sidebar-entry.ts and uses inlineSidebarHtml plugin to assemble dist/Sidebar.html - Add client module dependency graph (sidebar-entry.ts, sidebar.ts, google.d.ts, sidebar.css, Sidebar.html template) - Document tsconfig.client.json: rootDir ".", DOM lib, precise includes; typecheck now runs both tsconfigs - Document MimeType collision risk: never add "node" to tsconfig types; use triple-slash directive per-file instead - Document client-side google.script.run mock callback capture pattern - Document shared DOM fixture infrastructure: sidebar-fixtures.ts reads src/Sidebar.html at test time to prevent drift - Clarify coverage boundaries: index.ts excluded, sidebar-entry.ts included with lower thresholds (init() runs before DOM is available) - Reference new design docs for testing rationale Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: add component and panel test directories to jest and tsconfig Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add client navigation types (PanelId, NavigationContext, Panel) * feat: add Router with push/pop navigation stack and per-panel state serialization Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add services module wrapping google.script.run as Promises with header cache Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: update CLAUDE.md client architecture to reflect services.ts as GAS boundary * feat: add TagList component — multi-select, self-contained DOM, getValue() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add SingleTagList component — single-select, owns new-column input, handles custom savedState values Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add RowRange component — radio + range inputs, self-contained, getValue() Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: add missing RowRange tests and unique radio group name per instance Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add LockableField component — locked-by-default input/textarea with unlock toggle Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add ToolListPanel with navigate callbacks and tool dispatch Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: add missing ToolListPanel button-mapping tests, double-flush async assertions, use globalThis.alert Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: set btn.disabled in ToolListPanel loading state Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add ConfigureAIRunPanel with component-based form, savedState, and Promise-based GAS calls Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: assert button text restoration in ConfigureAIRunPanel success/failure tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: export SavedState from ConfigureAIRunPanel; use typed cast in test helper Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add RecipesListPanel and DocumentSummarizationPanel stubs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: migrate sidebar to panel-router architecture; cleanup old code - Simplify Sidebar.html to minimal <div id="app"> shell - Replace sidebar-entry.ts with thin Router init (all logic in panels/components) - Delete sidebar.ts, old sidebar tests, sidebar-fixtures.ts - Update jest.config.cjs: remove old transforms, add coverage thresholds for new files - Update tsconfig.client.json: remove deleted test file entries - Improve router/services tests to cover throw paths and runTool failure Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: client panel-router architecture with component-based sidebar (#26) * refactor: derive SavedState from RunConfig to eliminate field duplication Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * reordering menu buttons --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: recipe panels system with Document Summarization (#27) * docs: add recipe panels design document Covers RecipeParams/PrepRecipeParams/PrepRecipeResult type separation, generic RecipePanel with RecipePrepCook component, RECIPES registry pattern, server-side prepRecipe orchestrator, and full testing strategy. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add recipe panels implementation plan 12-task TDD plan covering shared types, sheet helpers, prepRecipe server function, services wrapper, LockableField onUnlock, RecipePrepCook component, RecipePanel, RecipesListPanel, and coverage thresholds. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add RecipeFieldConfig, RecipeParams, PrepRecipeParams, PrepRecipeResult types * feat: add 'recipe' to PanelId, add RecipeDefinition interface * feat: add findOrCreateColumn and writeColumn helpers to utils Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: update utils.ts header comment; guard writeColumn against empty array - Replace misleading "no dependency on Apps Script globals" header with accurate language about GAS objects received as arguments vs. singletons. - Clean up makeSheet helper in tests: replace identity map with headers.slice(). - Guard writeColumn against empty values array to prevent GAS runtime error when getRange is called with numRows=0; add corresponding test. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add prepRecipe server function and rollup global stub Implements the prepRecipe() server function that accepts PrepRecipeParams, writes Drive file URLs, system prompt, user prompts, and output column headers to the active sheet using findOrCreateColumn/writeColumn helpers, and returns a PrepRecipeResult with the populated row range and column names. Also adds the matching global stub in rollup.config.js so Apps Script can discover the function, and fixes a pre-existing PanelId type gap by adding "document-summarization" to the union. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: revert out-of-scope document-summarization PanelId addition Task 4 subagent incorrectly re-added "document-summarization" to PanelId to suppress typecheck errors in sidebar-entry.ts. Those errors are expected and will be resolved in Task 11 when sidebar-entry.ts is updated. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add prepRecipe service wrapper Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: use PrepRecipeParams type in google.d.ts stub Consistent with how runBatchAI uses RunConfig — unknown breaks type-level contract for direct callers of google.script.run.prepRecipe. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add onUnlock callback to LockableField * feat: add RecipePrepCook component with prep/cook state machine * feat: add RECIPES registry with Document Summarization entry * feat: add generic RecipePanel driven by RecipeParams * feat: replace RecipesListPanel stub with data-driven RECIPES implementation * fix: suppress double-alert on silent validation cancellation When buildPrepParams() returns null (drive folder URL empty), recipe.ts now rejects with null instead of new Error("cancelled"). RecipePrepCook checks err !== null before alerting, so only the validation alert from buildPrepParams fires. Test updated to assert alertMock called exactly once. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: register RecipePanel in router; remove DocumentSummarizationPanel stub Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: add coverage thresholds for recipe-prep-cook, recipe, recipes-list Also adds explicit return types to onPrep/onCook arrow functions in recipe.ts to satisfy the ESLint explicit-function-return-type rule. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: use single tsconfig in jest transform to avoid ts-jest ConfigSet caching bug ts-jest's TsJestTransformer._cachedConfigSets is a static array keyed by the global Jest config object reference, not by per-transform tsconfig options. On CI (1 worker), a server transformer running first would cache ConfigSet(tsconfig.json, no DOM), causing all subsequent client transforms to reuse the wrong tsconfig and fail with "Cannot find name 'document'". Consolidating to a single rule using tsconfig.client.json eliminates the conflict — server code compiles cleanly with DOM types in scope since it never references them. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: update CLAUDE.md to reflect PR #26/#27 changes (#28) - Client module graph: add types.ts, recipes.ts, expand panels and components lists with all new files from the panel-router and recipe-panels PRs - server/utils.ts: add findOrCreateColumn and writeColumn to description - TypeScript Configuration: add jest.config.cjs single-tsconfig note explaining the ts-jest ConfigSet caching bug workaround - Coverage section: fix stale sidebar-entry.ts description (old exported functions no longer exist; file is now fully excluded from collection) - Remove stale reference to sidebar-entry-testing-design.md Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: sidebar UI polish — cards, hierarchy, type cleanup, CLIP wrap (#29) * feat: sidebar UI polish — cards, hierarchy, type cleanup, CLIP wrap - Recipe section cards: each param group (Drive Folder, System Prompt, User Prompt, Output Column) rendered in a bordered card with an ALL-CAPS section title, giving clear visual separation - LockableField inline layout: single-line fields render label + input + lock button on one row; multiline keeps header-row + textarea layout - Recipe button hierarchy: description drops below name as smaller secondary text using flex-column .tool-btn-text wrapper - Recipe panel header now shows icon + name (e.g. "📄 Document Summarization") instead of the generic "Recipe" title - RecipeDefinition.params typed as RecipeParams (was unknown); panel receives full RecipeDefinition so name/icon flow naturally — no new interface needed - RecipesListPanel navigates with full RecipeDefinition, not just params - Configure AI Run panel renamed to "▶️ Run AI Inference" to match the tool-list button label - "✨ More recipes coming soon…" stub added to recipes list (dashed border, non-interactive) - findOrCreateColumn / writeColumn accept optional wrapStrategy param; all prepRecipe call sites pass SpreadsheetApp.WrapStrategy.CLIP so newly created columns don't blast users with wrapped text Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: invalidate header cache after prepRecipe succeeds When a user opened Run AI Inference on an empty sheet, getSheetHeaders cached the empty result. Subsequent navigations to the same panel after a recipe prep (which created new columns) returned the stale [] cache, showing "No columns found" despite headers now existing. Fix: call invalidateHeaderCache() inside prepRecipe's success handler so the cache is always fresh after a prep writes new columns to the sheet. Added a test that primes the cache with [], runs prepRecipe, then verifies the next getSheetHeaders hits GAS rather than returning []. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: cover wrapStrategy branches in findOrCreateColumn and writeColumn The new optional wrapStrategy param introduced two uncovered branches, dropping utils.ts below the 93% branch threshold. Added one test per function verifying that setWrapStrategy is called on the correct range when the param is provided. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: consistent button design language across recipe and run AI panels - Add .btn-outline (blue border, blue text, white fill) for Prep Recipe — signals "first step" without looking like a cancel or destructive action - Use .btn-run (filled blue) for Cook — primary culminating action that lights up after prep completes - Remove Cancel button from Run AI Inference panel; the back button in the header already covers this and the redundancy created confusion - Add .btn-outline:disabled state (muted border/text, no pointer) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Adding a arrow --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: remove header cache from services.ts (#30) The cache only saved one GAS round-trip per sidebar open but introduced correctness bugs — it was poisoned by the first empty lookup, and it had no invalidation path when the user switched sheets mid-session. Since GAS round-trips are cheap relative to the Prep/Cook workflow, the complexity isn't worth it. getSheetHeaders() now always fetches live. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: rewrite README for contributor onboarding, add google.d.ts step to CLAUDE.md (#31) Replaces the stale, overly granular README with a focused developer guide covering prerequisites, setup, key commands, build pipeline, architecture, and testing patterns. Cuts file trees, module tables, and end-user content that belongs elsewhere. Also adds a third step to the CLAUDE.md "expose a new function" checklist — updating google.d.ts when adding client-callable server functions. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: add .worktrees/ to .gitignore Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * updating model * Feature/gemini tools (#32) * chore: add .worktrees/ to .gitignore Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add server/types.ts with Gemini API types and GeminiTool union Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: fix misleading GeminiTool comment (type is exported) * feat: add ToolId to shared types, add tools field to RunConfig/PrepRecipeParams/PrepRecipeResult Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: add DriveFileInfo to server/types.ts (missed during shared cleanup) * refactor: move RecipeParams and RecipeFieldConfig to client/types.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: fix imports after type reorganization Routes server imports (AppConfig, GeminiInlineData, GeminiRequest, GeminiFunctionDeclaration, DriveFileInfo) from shared/types to server/types, and client RecipeParams from shared/types to client/types. Adds temporary casts and SavedState adjustment to keep typecheck clean until Tasks 5/7/8 refactor the dependent logic. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: replace TOOL_REGISTRY with unified Record<ToolId, GeminiTool> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add client/tools.ts with TOOL_CATALOG and ToolCatalogEntry Introduces the client-side tool catalog that provides display metadata (id, name, description) for tools shown in the sidebar TagList. Hardcoded at build time — no RPC needed since the tool list is static compiled code. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: update buildGeminiPayload to resolve ToolId[] via TOOL_REGISTRY Replaces the old flat function_declarations pass-through with a two-phase split: grounding tools produce { [id]: {} } entries and function tools produce a { function_declarations: [...] } entry, all assembled into the correct Gemini REST payload shape. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: update SSI to validate ToolId and support all tool types Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add tools parameter to runInference * test: remove stray tools assertion from inlineData test * feat: thread tools through runBatchAI and prepRecipe * feat: extend TagList to support label/value items alongside plain strings * feat: add tools TagList to ConfigureAIRunPanel * test: fix coverage thresholds and improve test quality * docs: add gemini-tools design doc and implementation plan --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * Feature/gemini tools (#33) * feat: add server/types.ts with Gemini API types and GeminiTool union Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: fix misleading GeminiTool comment (type is exported) * feat: add ToolId to shared types, add tools field to RunConfig/PrepRecipeParams/PrepRecipeResult Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: add DriveFileInfo to server/types.ts (missed during shared cleanup) * refactor: move RecipeParams and RecipeFieldConfig to client/types.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: fix imports after type reorganization Routes server imports (AppConfig, GeminiInlineData, GeminiRequest, GeminiFunctionDeclaration, DriveFileInfo) from shared/types to server/types, and client RecipeParams from shared/types to client/types. Adds temporary casts and SavedState adjustment to keep typecheck clean until Tasks 5/7/8 refactor the dependent logic. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: replace TOOL_REGISTRY with unified Record<ToolId, GeminiTool> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add client/tools.ts with TOOL_CATALOG and ToolCatalogEntry Introduces the client-side tool catalog that provides display metadata (id, name, description) for tools shown in the sidebar TagList. Hardcoded at build time — no RPC needed since the tool list is static compiled code. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: update buildGeminiPayload to resolve ToolId[] via TOOL_REGISTRY Replaces the old flat function_declarations pass-through with a two-phase split: grounding tools produce { [id]: {} } entries and function tools produce a { function_declarations: [...] } entry, all assembled into the correct Gemini REST payload shape. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: update SSI to validate ToolId and support all tool types Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add tools parameter to runInference * test: remove stray tools assertion from inlineData test * feat: thread tools through runBatchAI and prepRecipe * feat: extend TagList to support label/value items alongside plain strings * feat: add tools TagList to ConfigureAIRunPanel * test: fix coverage thresholds and improve test quality * docs: add gemini-tools design doc and implementation plan * feat: add url_context and code_execution grounding tools --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: update CLAUDE.md to reflect gemini-tools architecture (#34) * docs: update CLAUDE.md to reflect gemini-tools architecture (PRs #32/#33) Updates module dependency graph and adds a dedicated Tool System section covering ToolId, TOOL_REGISTRY, TOOL_CATALOG, GeminiTool discriminated union, payload assembly, and the three-file checklist for adding new tools. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add GitHub PR workaround for TLS certificate issue Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * Feature: grounding metadata presentation and rich cell text (#35) * feat: add GeminiResponse types to server/types.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: callGeminiAPI returns GeminiResponse Update callGeminiAPI to return GeminiResponse instead of string. Assembles text from all text parts, extracts code execution pairs, and captures groundingMetadata from the REST response. Update invokeGemini return type and apply .text at call sites in customFunctions and inference that still need string; invokeGemini test updated in next task. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: update invokeGemini tests for GeminiResponse return type Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: runInference returns GeminiResponse | null Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add grounding metadata enrichment design and implementation plan * Feature/grounding metadata presentation refinements (#36) * docs: add grounding metadata presentation design * docs: add grounding metadata presentation implementation plan * feat: add GeminiGroundingSupport type and RunConfig.includeGrounding Also updates SavedState in ConfigureAIRunPanel to exclude includeGrounding from the Required<Omit<...>> so the type remains consistent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add getCitations pure helper to api.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add getUngroundedSpans pure helper to api.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: correct Span index boundaries and add edge case tests for getUngroundedSpans Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add getAllSources pure helper to api.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add includeGrounding checkbox to ConfigureAIRunPanel Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: code quality improvements to includeGrounding checkbox Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: exercise event-driven label update in includeGrounding test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: renderInference/renderGrounding in runBatchAI; RichTextValue output Add renderInference and renderGrounding helper functions to src/server/index.ts. Update runBatchAI to write RichTextValue with inline citation hyperlinks via setRichTextValue, and optionally write a {outputCol}_grounding column with source attribution, search queries, unverified spans, or code execution output. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: safe citation merging, per-row error fallback, and source hyperlink scoping in index.ts - Merge overlapping citation ranges before calling setLinkUrl to prevent Apps Script exceptions - Add comment noting code_execution + google_search co-activation constraint on text offsets - Wrap render/write block in per-row try/catch with plain-text fallback so one bad row doesn't abort the batch - Scope source title hyperlinking to the Sources section only, avoiding false matches elsewhere in fullText - Push newly created output column name to headers array, matching the grounding column pattern Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add grounding presentation refinements plan * docs: add markdown rendering task to refinements plan * refactor: remove getUngroundedSpans — Unverified section dropped from grounding column Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add rich-text.ts — pure CellContent/TextRange builders for inference and grounding cells Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: use running cursor for duplicate source titles; document merge URI decision; tighten test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: strip api.ts to pure HTTP adapter — getCitations/getAllSources moved to rich-text.ts - Remove Citation interface, getCitations, getAllSources from api.ts - Remove GeminiGroundingSupport from api.ts imports (no longer needed there) - Export getCitations and getAllSources from rich-text.ts (were private) - Update index.ts import to pull getCitations/getAllSources from rich-text.ts - Remove getCitations and getAllSources describe blocks from api.test.ts - Remove unused GeminiResponse type import from api.test.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: toCellValue in index.ts; renderInference/renderGrounding replaced by pure rich-text.ts builders Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: move grounding checkbox into Tools section with conditional visibility Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: add per-file coverage threshold for rich-text.ts New file src/server/rich-text.ts was added without a corresponding coverageThreshold entry in jest.config.cjs. Adds thresholds based on observed coverage (statements 99.29%, branches 87.15%, functions 100%), set ~5 points below observed per project convention. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: add retrievedContext coverage; document codePairs early-return design decision Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: rewrite refinements plan to incorporate rich-text.ts architecture * fix: use camelCase executableCode/codeExecutionResult keys — Gemini REST API returns camelCase * style: code badge for grounding column name; clean up checkbox label --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: allow deselecting tags in SingleTagList (#37) Clicking an already-selected tag in system prompt or AI output fields had no effect because selectOnly() always re-added the selected class. Now captures wasSelected before clearing, matching the toggle semantics used by TagList. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: rewrite markdown parser with line-oriented approach (#39) * refactor: rewrite parseMarkdown with line-oriented approach Fixes bullet list (* and -) misparse and adds [text](url) inline link support. processInline helper separates structural stripping (headings, bullets) from inline pattern matching, making the parser easier to reason about and remove if needed. posMap citation remapping preserved. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: guard zero-width url ranges and handle ) in URLs processInline: skip pushing a url TextRange when link text is empty (Sheets API rejects zero-width ranges). Also use lastIndexOf to find the closing ) so URLs containing literal parentheses (e.g. Wikipedia links) are not truncated. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: add markdown parser rewrite design plan Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: normalise absent startIndex to 0 in getCitations (#40) Gemini omits startIndex from grounding segment when its value is 0 (proto3 default). This caused setRichTextValue to receive an undefined index, throw, and silently fall back to plain text — also dropping the grounding column write. Normalise with ?? 0 at the parse boundary. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: add apply markdown checkbox to Configure AI Run panel (#41) * feat: add rawOutput checkbox to Configure AI Run panel When checked, runBatchAI writes result.text directly via setValue, bypassing markdown parsing and rich text formatting. The grounding column is unaffected. Saved state persists the checkbox value across navigation. Renames .grounding-hint CSS class to .checkbox-option so both checkboxes share the same styling. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: replace rawOutput with applyMarkdown, decouple grounding write Inverts the checkbox semantics: unchecked (default) writes plain text, checked applies markdown parsing and rich text formatting. Grounding column now writes independently of the formatting setting — a user can have raw output in the inference column while still getting the grounding column. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: move apply markdown checkbox into output column section Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * small verbiage change * ? --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: migrate to Editor Add-on deployment pipeline (#42) * chore: track .clasp.json as stable add-on config * chore: remove container-bound dev/prod clasp configs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: simplify deploy scripts for single-script add-on model * chore: add release.sh for versioned Marketplace deployment * docs: update CLAUDE.md for single-script add-on deploy model Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: update README for add-on deploy model and add Code Lifecycle section Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: update oauth scopes for editor add-on * feat: add onInstall handler and update onOpen signature for editor add-on * docs: add editor add-on deploy pipeline design and plan * fix: remove unused event params from onOpen and onInstall * chore: set Marketplace deployment ID in release.sh * minor husky thing * feat: enforce main-only release and document branch workflow in README * chore: add PR template with manual QA checklist * chore: add CODEOWNERS * fix: update onOpen stub example in README to match signature * minor release doc * docs: flag single-developer assumption in Code Lifecycle section * updating release process --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 464d917 commit 0459a2a

96 files changed

Lines changed: 23476 additions & 982 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.clasp.dev.json

Lines changed: 0 additions & 4 deletions
This file was deleted.

.clasp.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"scriptId": "1x1qwECFShOjQ-HaHslRjURQmWji08Xw4w_NTQOfMsC4H5TYcw0Flpsi6",
2+
"scriptId": "1hIyBphS_JoSvdSy6jF-D8YKqYnuxz6nmG_wcYFwe7ahbun7YFqhSyzOm",
33
"rootDir": "./dist"
4-
}
4+
}

.clasp.prod.json

Lines changed: 0 additions & 4 deletions
This file was deleted.

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* @aaronbrezel

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
## Summary
2+
3+
-
4+
-
5+
6+
## Manual QA
7+
8+
> Complete for PRs targeting `main`. Mark N/A with reason for PRs targeting `develop`.
9+
10+
- [ ] Add-on menu appears after opening a Sheet
11+
- [ ] Sidebar opens without errors
12+
- [ ] Import Drive Links — imports files from a folder
13+
- [ ] Extract Text — extracts text from a Doc/PDF/image
14+
- [ ] Sample Rows — samples rows reproducibly with a seed
15+
- [ ] Run AI — batch inference completes and writes output column
16+
- [ ] Tested on a Sheet the tester does not own (shared access)
17+
18+
## Notes
19+
20+
<!-- Anything reviewers or QA testers should know -->

.github/workflows/lint-typecheck-format-test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ name: Lint/Typecheck/Format/Test
22

33
on:
44
push:
5-
branches: [main]
5+
branches: [main, develop]
66
pull_request:
7-
branches: [main]
7+
branches: [main, develop]
88

99
jobs:
1010
lint-typecheck-format-test:

.gitignore

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@ coverage/
88
# Clasp auth (contains tokens — never commit)
99
.clasprc.json
1010

11-
# Active clasp config (generated by deploy scripts)
12-
.clasp.json
13-
1411
# IDE
1512
.vscode/
1613
.idea/
@@ -24,4 +21,7 @@ Thumbs.db
2421
.env.local
2522

2623
# Gemini CLI internal
27-
.gemini/
24+
.gemini/
25+
26+
# Git worktrees
27+
.worktrees/

.husky/pre-commit

100644100755
File mode changed.

CLAUDE.md

Lines changed: 167 additions & 17 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 95 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,141 +1,132 @@
1-
# SSI Drive & AI Tools
1+
# SSI Toolkit
22

3-
Google Apps Script toolkit for importing Drive files, extracting text, sampling data, and running multimodal AI inference — developed locally with TypeScript, Rollup, and Clasp.
4-
5-
## Project Structure
6-
7-
```
8-
gas-project/
9-
├── src/
10-
│ ├── server/
11-
│ │ ├── index.ts # Entry point — menu, tools, global exposure
12-
│ │ ├── config.ts # Central CONFIG object
13-
│ │ ├── api.ts # Gemini API calls via UrlFetchApp
14-
│ │ ├── drive.ts # Drive Advanced Service + text extraction
15-
│ │ ├── dialog.ts # HTML template for AI source dialog
16-
│ │ └── utils.ts # Pure helpers (extractId, seededRandom, etc.)
17-
│ └── shared/
18-
│ └── types.ts # TypeScript interfaces
19-
├── __tests__/
20-
│ ├── utils.test.ts # Tests for pure helpers
21-
│ ├── api.test.ts # Tests for Gemini API with mocked globals
22-
│ ├── drive.test.ts # Tests for Drive text extraction
23-
│ └── menu.test.ts # Tests for onOpen / menu registration
24-
├── dist/ # Build output (clasp pushes from here)
25-
├── appsscript.json # Manifest: scopes, Drive Advanced Service
26-
├── .clasp.dev.json # Dev script ID
27-
├── .clasp.prod.json # Prod script ID
28-
├── rollup.config.js # Bundler config
29-
├── tsconfig.json # TypeScript config (ES2019/V8 target)
30-
├── jest.config.cjs # Test runner
31-
├── .eslintrc.json # Linting
32-
└── .prettierrc # Formatting
33-
```
34-
35-
### Module Breakdown
36-
37-
| Module | What it does | Apps Script globals used |
38-
|--------|-------------|------------------------|
39-
| `config.ts` | Column names, model name, limits | None |
40-
| `api.ts` | `callGeminiAPI()` — text and multimodal | `UrlFetchApp`, `DriveApp`, `Utilities` |
41-
| `drive.ts` | `extractTextUniversal()`, OCR via Doc conversion | `DriveApp`, `Drive` (Advanced), `DocumentApp` |
42-
| `dialog.ts` | HTML string for the modal dialog | None |
43-
| `utils.ts` | ID extraction, link validation, seeded RNG, recursive file listing | `DriveApp` (only `getAllFilesRecursive`) |
44-
| `index.ts` | Menu creation, 4 tools, exports wired to footer stubs | `SpreadsheetApp`, `HtmlService`, `PropertiesService` |
3+
Google Apps Script add-on built with TypeScript, bundled by Rollup, and deployed via clasp. It exposes a spreadsheet inference toolkit to Google Sheets. All source lives locally — the build pipeline compiles and pushes to Apps Script. Avoid making changes in the online Apps Script editor; they'll be overwritten on the next deploy.
454

465
## Prerequisites
476

48-
- Node.js 18+ and npm
49-
- Apps Script API enabled: https://script.google.com/home/usersettings
50-
- Drive Advanced Service enabled in the Apps Script editor (Services > + > Drive API v3)
7+
- Node.js 22+
8+
- Apps Script API enabled at [script.google.com/home/usersettings](https://script.google.com/home/usersettings)
9+
- The SSI Toolkit Apps Script add-on project (script ID in `.clasp.json`)
10+
- A Gemini API key (required for the Run AI tool)
5111

5212
## Setup
5313

5414
```bash
55-
# 1. Install dependencies
5615
npm install
16+
npm run clasp:login # authenticate with Google
17+
```
5718

58-
# 2. Authenticate with Google
59-
npm run clasp:login
60-
61-
# 3. Edit .clasp.dev.json with your script ID
62-
# (from Extensions > Apps Script in your Google Sheet)
63-
64-
# 4. Set your Gemini API key in Script Properties:
65-
# Apps Script editor > Project Settings > Script Properties
66-
# Key: GEMINI_API_KEY Value: your-key-here
19+
Set `GEMINI_API_KEY` as a Script Property in Apps Script > Project Settings > Script Properties, then deploy:
6720

68-
# 5. Build and deploy
69-
npm run deploy:dev
21+
```bash
22+
npm run deploy # build and push to HEAD
7023
```
7124

72-
## Development
25+
## Key Commands
7326

7427
```bash
75-
npm run build # Clean build to dist/
76-
npm run build:watch # Continuous rebuild on file changes
77-
npm run deploy:dev # Build + push to dev script
78-
npm run deploy:prod # Build + push to prod script
79-
npm run deploy:watch:dev # Continuous build + push watch (dev)
80-
npm run deploy:watch:prod # Continuous build + push watch (prod)
81-
82-
npm test # Run tests
83-
npm run test:watch # Run tests in watch mode
84-
npm run test:coverage # Run tests with coverage + enforce per-file thresholds
85-
86-
npm run lint # Lint TypeScript
87-
npm run lint:fix # Lint with auto-fix
88-
npm run format # Format with Prettier
89-
npm run format:check # Check formatting without modifying files
90-
npm run typecheck # Type-check without building
91-
92-
npm run clasp:open # Open the Apps Script editor in browser
93-
npm run clasp:logs # Tail execution logs from Apps Script
28+
# Build
29+
npm run build # clean build to dist/
30+
npm run build:watch # rebuild on file changes
31+
32+
# Deploy
33+
npm run deploy # build + push to HEAD (development)
34+
npm run deploy:watch # continuous build + push watch
35+
36+
# Test
37+
npm test # run all tests
38+
npm run test:watch # watch mode
39+
npm run test:coverage # with per-file coverage thresholds
40+
41+
# Quality
42+
npm run lint # ESLint
43+
npm run typecheck # type-check without building
44+
npm run format:check # check Prettier formatting
45+
46+
# Utilities
47+
npm run clasp:open # open Apps Script editor in browser
48+
npm run clasp:logs # tail execution logs
9449
```
9550

96-
Run a single test file: `npx jest __tests__/utils.test.ts`
51+
Run a single test file: `npx jest __tests__/api.test.ts`
9752
Run a single test by name: `npx jest -t "extractId"`
9853

99-
## How the Build Works
54+
## Code Lifecycle
55+
56+
SSI Toolkit uses a single Apps Script project with two deployment states:
57+
58+
**HEAD** is the active development surface. `npm run deploy` pushes your local build here. You can test at HEAD using Apps Script's built-in test deployments (Deploy → Test deployments in the script editor) without affecting anyone who has the add-on installed. The "SSI Toolkit (dev)" Document is pre-populated with useful test data.
59+
60+
**Versioned deployment** is what Marketplace-installed users run. It is a pinned snapshot that only changes when a human explicitly runs `scripts/release.sh` from `main`.
61+
62+
### Branch workflow
63+
64+
```
65+
feature-branch → develop (PR + code review)
66+
develop → main (PR containing manual QA instructions = release gate)
67+
main (run ./scripts/release.sh to publish)
68+
```
69+
70+
Feature work happens on branches, merged to `develop` via PR. When ready to ship, `develop` is merged to `main` via a PR containing manual QA instructions — that merge is the release gate. Only then is `scripts/release.sh` run from `main`, which builds and pushes to HEAD, snapshots it as a new immutable version, and repoints the Marketplace deployment.
10071

10172
```
102-
src/**/*.ts Rollup (TS + node-resolve) → dist/index.js → clasp push → Apps Script
73+
./scripts/release.shbuilds, pushes to HEAD, and promotes to Marketplace (human-only, main only)
10374
```
10475

105-
Rollup bundles everything into a single IIFE assigned to `_GASEntry`. Apps Script has no module system and can only discover top-level functions in the global scope. The `footer` field in `rollup.config.js` bridges this gap by appending plain global stubs that delegate into the IIFE:
76+
`scripts/release.sh` enforces the `main` requirement — it will exit with an error if run from any other branch.
77+
78+
> **Note for future contributors:** This pipeline assumes a single developer. `npm run deploy` pushes to a shared HEAD — concurrent development will cause conflicts. This should be revisited before a second developer joins the project.
79+
80+
## Build Pipeline
81+
82+
The build produces two outputs from a single `rollup.config.js` array:
83+
84+
**Server**`src/server/index.ts``dist/index.js` (IIFE format). Apps Script has no module system and only discovers top-level global functions. The Rollup `footer` field appends plain stubs that delegate into the IIFE:
10685

10786
```js
108-
function onOpen(e) { _GASEntry.onOpen(e); }
109-
function showSourceDialog() { _GASEntry.showSourceDialog(); }
110-
// ... one stub per public entry point
87+
function onOpen() { _GASEntry.onOpen(); }
11188
```
11289

113-
**To expose a new function to Apps Script, you must do both:**
114-
1. `export` it from `src/server/index.ts`
115-
2. Add a matching global stub in the `footer` of `rollup.config.js`
90+
To expose a new function to Apps Script, you must both `export` it from `index.ts` and add a matching stub in the `rollup.config.js` footer. Skipping the stub means Apps Script can't find it.
91+
92+
**Client**`src/client/sidebar-entry.ts``dist/Sidebar.html`. HtmlService only serves `.html` files, so all JS and CSS must be inlined at build time. A custom Rollup plugin handles this: it compiles the client bundle, reads `src/Sidebar.html` and `src/client/sidebar.css`, replaces `{{STYLES}}` and `{{SCRIPTS}}` placeholders, and emits the final HTML asset.
11693

117-
If you skip step 2, the function will exist in the bundle but Apps Script won't be able to discover or call it.
94+
`appsscript.json` is copied into `dist/` as part of the build — clasp needs the manifest alongside the bundled JS.
11895

119-
## Tool 4 — Run AI: Required Columns
96+
## Architecture
12097

121-
`runBatchAI` maps columns by header name. The active sheet must contain these exact headers (case-sensitive):
98+
The codebase has two separate TypeScript environments with a hard boundary between them:
12299

123-
| Column header | Purpose |
124-
|---|---|
125-
| `source_drive` | Drive file link (multimodal mode) |
126-
| `source_text` | Plain text input (text mode) |
127-
| `system_prompt` | System prompt for each row |
128-
| `user_prompt` | User prompt for each row |
129-
| `ai_inference` | Output column (written by the tool) |
100+
**Server** (`src/server/`) runs on Google's infrastructure. `index.ts` is the only file that touches Apps Script UI globals (`SpreadsheetApp`, `HtmlService`, `PropertiesService`). Everything else — API calls, Drive operations, utilities — is written as pure functions with no GAS globals, which keeps them testable.
130101

131-
The Gemini API key must be stored as a Script Property (`GEMINI_API_KEY`) in Apps Script > Project Settings > Script Properties before Tool 4 will run.
102+
**Client** (`src/client/`) runs in the browser inside the sidebar HtmlService iframe. `services.ts` is the connective tissue between server and client — it wraps every `google.script.run` call as a Promise so the rest of the client code never touches the GAS boundary directly.
132103

133-
## Key Notes
104+
`google.script.run` is injected by GAS's HtmlService at runtime and exposes whatever functions exist in the global scope of the deployed script — meaning the footer stubs from `rollup.config.js`. There are no built-in TypeScript types for it, so `src/client/google.d.ts` is a hand-maintained declaration file that tells the compiler what's available. It is not auto-generated: if you add a new server function and forget to update `google.d.ts`, the client will typecheck against stale declarations and only fail at runtime.
134105

135-
**Drive Advanced Service:** The `extractTextUniversal` function uses `Drive.Files.create()` and `Drive.Files.remove()` (v3 API) for PDF/image OCR. This is the Drive *Advanced Service*, not `DriveApp`. It must be enabled separately in the Apps Script editor AND is declared in `appsscript.json` under `enabledAdvancedServices`.
106+
The client uses a lightweight panel/router system: `Router` manages a navigation stack, and each `Panel` implementation handles its own render and state. `recipes.ts` holds the registry of named recipes that drive the `RecipePanel` generic panel.
136107

137-
**appsscript.json must be in dist/:** Clasp needs the manifest alongside the bundled JS. The build script handles this automatically — it runs `rimraf dist`, Rollup, then copies `appsscript.json` into `dist/`. No manual copy needed.
108+
When adding new functionality, keep this layering intact — GAS globals in `index.ts`, `google.script.run` in `services.ts`, business logic in pure modules that can be unit tested.
138109

139-
**Custom functions have limited permissions:** The `GEMINI()` custom function (if you add one) cannot access `PropertiesService`, so passing an API key to it requires a different pattern (hardcoded config, cache, or trigger-based pre-fetch).
110+
## Testing
111+
112+
Jest with ts-jest. Tests live in `__tests__/`.
113+
114+
**Mocking GAS globals:** Set properties on `globalThis` *before* importing the module under test, since imports execute immediately:
115+
116+
```ts
117+
(globalThis as any).UrlFetchApp = { fetch: jest.fn() };
118+
const { callGeminiAPI } = await import("../src/server/api");
119+
```
120+
121+
**Mocking `google.script.run`:** Capture the success/failure handlers registered by the function under test, then invoke them manually to simulate GAS callbacks:
122+
123+
```ts
124+
let capturedSuccess: (v: unknown) => void;
125+
mockRun.withSuccessHandler.mockImplementation((fn) => {
126+
capturedSuccess = fn;
127+
return mockRun;
128+
});
129+
// later: capturedSuccess(mockValue)
130+
```
140131

141-
**Localhost during development:** `UrlFetchApp` runs on Google's servers. To hit localhost, you need a tunnel (ngrok, Cloudflare Tunnel) or a deployed staging endpoint.
132+
Coverage is enforced per-file — run `npm run test:coverage` to check thresholds.

0 commit comments

Comments
 (0)