Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
93 commits
Select commit Hold shift + click to select a range
5e15c85
docs: add sidebar feature parity design doc
aaronbrezel Feb 18, 2026
646faa1
docs: add sidebar feature parity implementation plan
aaronbrezel Feb 18, 2026
b8fd1d8
build: copy Sidebar.html to dist/ alongside appsscript.json
aaronbrezel Feb 18, 2026
566d33f
feat: add Sidebar.html for persistent sidebar UI
aaronbrezel Feb 18, 2026
06292b4
build: update rollup footer stubs for showSidebar and runTool
aaronbrezel Feb 18, 2026
995385b
test: update menu tests for sidebar UX (failing)
aaronbrezel Feb 18, 2026
a56166c
feat: replace menu with sidebar — add showSidebar and runTool
aaronbrezel Feb 18, 2026
ab5c763
fix: restore importDriveLinks, fix test mocks for runTool dispatch
aaronbrezel Feb 18, 2026
f508cf9
test: add beforeEach clearAllMocks to runTool describe block
aaronbrezel Feb 18, 2026
90f186b
fix: pass event explicitly in Sidebar run() to avoid window.event global
aaronbrezel Feb 18, 2026
ed7b302
fix: allow .html files through .claspignore so Sidebar.html is pushed
aaronbrezel Feb 18, 2026
6bea2cd
Merge remote-tracking branch 'origin' into develop
aaronbrezel Feb 18, 2026
adee3bd
feat: replace menu with persistent sidebar (feature parity) (#12) (#13)
aaronbrezel Feb 18, 2026
e669ec3
Dev default branch (#14)
aaronbrezel Feb 18, 2026
9b8b237
Merge branch 'develop' of github.com:propublica/gas-ssi-toolkit into …
aaronbrezel Feb 18, 2026
ee883d2
docs: add GitHub Actions clasp deployment design
aaronbrezel Feb 19, 2026
a2e6531
docs: add GitHub Actions clasp deployment implementation plan
aaronbrezel Feb 19, 2026
2ace4b2
feat: add deploy workflow with WIF authentication
aaronbrezel Feb 19, 2026
50dcc67
fix: use npx clasp to ensure local binary resolves in CI
aaronbrezel Feb 19, 2026
a200b73
adding visible test change
aaronbrezel Feb 19, 2026
25c319b
removing test change
aaronbrezel Feb 20, 2026
fd9f696
updating worklow for debugging purposes
aaronbrezel Feb 20, 2026
c14fa54
rmoving worklow for debugging purposes
aaronbrezel Feb 20, 2026
f25555c
fix: use access_token format to bridge WIF credentials into clasp
aaronbrezel Feb 20, 2026
bc88dcb
fix: write clasprc in v3 format (tokens.default) not legacy v1 format
aaronbrezel Feb 20, 2026
277cb03
fix: clear GOOGLE_APPLICATION_CREDENTIALS in deploy step and add auth…
aaronbrezel Feb 20, 2026
407df59
fix: provide all required UserRefreshClient fields in clasprc to pass…
aaronbrezel Feb 20, 2026
65ff777
test change
aaronbrezel Feb 20, 2026
138b4fe
docs: update deploy design doc with implementation history and curren…
aaronbrezel Feb 20, 2026
783e30b
chore: remove deploy.yml pending fallback auth implementation
aaronbrezel Feb 20, 2026
efbad5d
feat: replace callGeminiAPI with GeminiRequest options object interfa…
aaronbrezel Feb 20, 2026
d2c8157
feat: upgrade GeminiRequest.inlineData to array for multi-file suppor…
aaronbrezel Feb 23, 2026
290cd34
feat: add SSI Sheets custom function (text-only) (#18)
aaronbrezel Feb 23, 2026
94e8fcd
quick docs update
aaronbrezel Feb 23, 2026
4882a8b
feat: add inlineData parameter to SSI custom function (deferred — Dri…
aaronbrezel Feb 23, 2026
cefdc92
Revert "feat: add inlineData parameter to SSI custom function (deferr…
aaronbrezel Feb 23, 2026
8867e93
missing docs
aaronbrezel Feb 23, 2026
469e0fa
docs: add design for consolidating Gemini call path
aaronbrezel Feb 23, 2026
7ce09e3
docs: update consolidation plan to include runInference handler
aaronbrezel Feb 23, 2026
7459e20
refactor: move flattenArg to utils.ts
aaronbrezel Feb 23, 2026
3d824e5
refactor: extract TOOL_REGISTRY to tools.ts
aaronbrezel Feb 23, 2026
9368906
feat: add invokeGemini as single Gemini entry point
aaronbrezel Feb 23, 2026
38d662e
fix: update api.test.ts header comment to reflect PropertiesService mock
aaronbrezel Feb 23, 2026
3b9a643
docs: update inference handler to return string|null, no SpreadsheetA…
aaronbrezel Feb 23, 2026
6357bb7
feat: add runInference unified inference handler
aaronbrezel Feb 23, 2026
c02a0e1
fix: flattenArg returns [] for empty string scalar, remove redundant …
aaronbrezel Feb 24, 2026
42277d6
refactor: SSI delegates to invokeGemini, imports from utils and tools
aaronbrezel Feb 24, 2026
0ce2fee
refactor: runBatchAI loop delegates to runInference
aaronbrezel Feb 24, 2026
8f0b28e
refactor: consolidate Gemini call path (#21)
aaronbrezel Feb 24, 2026
0bb792c
Merge branch 'develop' of github.com:propublica/gas-ssi-toolkit into …
aaronbrezel Feb 24, 2026
9844cd8
feat: dynamic column mapping for Run AI (#22)
aaronbrezel Feb 24, 2026
9954835
docs: sidebar refactor design — client TS/CSS split with RunConfig au…
aaronbrezel Feb 24, 2026
ebc5e08
docs: sidebar refactor implementation plan
aaronbrezel Feb 24, 2026
ca1972c
feat: add google.d.ts type stub and DOM lib for client code
aaronbrezel Feb 24, 2026
4af3c61
fix: use tsconfig.client.json for client types, fix withFailureHandle…
aaronbrezel Feb 24, 2026
1d03f22
feat: extract Sidebar CSS to sidebar.css, drop Google Fonts
aaronbrezel Feb 24, 2026
2af0cc7
feat: add sidebar.ts skeleton with exported stubs
aaronbrezel Feb 24, 2026
2d14426
fix: correct import path in google.d.ts
aaronbrezel Feb 24, 2026
e1e4408
feat: implement buildTagList with tests
aaronbrezel Feb 24, 2026
adf6da9
fix: pin jest-environment-jsdom to jest 29 major version
aaronbrezel Feb 24, 2026
06f79ce
feat: implement buildSingleTagList with tests
aaronbrezel Feb 24, 2026
ae8cd51
feat: implement handleRowRangeChange with tests
aaronbrezel Feb 24, 2026
6853a24
feat: implement applyPreset with tests
aaronbrezel Feb 24, 2026
68cc280
feat: implement assembleRunConfig with tests
aaronbrezel Feb 24, 2026
546adde
fix: applyPreset reveals new-col-input for __new__ preset; add sideba…
aaronbrezel Feb 24, 2026
dfad38d
fix: restrict tsconfig types to prevent jsdom MimeType collision
aaronbrezel Feb 24, 2026
31a73f4
fix: declare global in google.d.ts and clear inherited exclude in cli…
aaronbrezel Feb 24, 2026
efe874b
feat: add showAIPanel, hideAIPanel, dispatchTool, runAI, init
aaronbrezel Feb 24, 2026
279ada9
feat: convert Sidebar.html to build template with id-wired buttons
aaronbrezel Feb 24, 2026
b8d15e7
feat: add inlineSidebarHtml Rollup plugin and client build config
aaronbrezel Feb 24, 2026
e9823d1
chore: remove manual Sidebar.html copy from build scripts
aaronbrezel Feb 24, 2026
babede2
chore: remove redundant tsconfig.test.client.json, consolidate to tsc…
aaronbrezel Feb 24, 2026
c4823d6
fix: suppress sourcemap warning in client Rollup config
aaronbrezel Feb 24, 2026
bd3fe21
feat: sidebar TypeScript/CSS refactor with RunConfig autopopulation (…
aaronbrezel Feb 25, 2026
7e386c9
Merge branch 'develop' of github.com:propublica/gas-ssi-toolkit into …
aaronbrezel Feb 25, 2026
b46d55a
chore: add component and panel test directories to jest and tsconfig
aaronbrezel Feb 25, 2026
6c6ab52
feat: add client navigation types (PanelId, NavigationContext, Panel)
aaronbrezel Feb 25, 2026
dc0516a
feat: add Router with push/pop navigation stack and per-panel state s…
aaronbrezel Feb 25, 2026
9e4aa2a
feat: add services module wrapping google.script.run as Promises with…
aaronbrezel Feb 25, 2026
a971c32
docs: update CLAUDE.md client architecture to reflect services.ts as …
aaronbrezel Feb 25, 2026
05cdd72
feat: add TagList component — multi-select, self-contained DOM, getVa…
aaronbrezel Feb 25, 2026
0909fcc
feat: add SingleTagList component — single-select, owns new-column in…
aaronbrezel Feb 25, 2026
2a46bf6
feat: add RowRange component — radio + range inputs, self-contained, …
aaronbrezel Feb 25, 2026
84c26d9
fix: add missing RowRange tests and unique radio group name per instance
aaronbrezel Feb 25, 2026
ec2ce17
feat: add LockableField component — locked-by-default input/textarea …
aaronbrezel Feb 25, 2026
239b0c9
feat: add ToolListPanel with navigate callbacks and tool dispatch
aaronbrezel Feb 25, 2026
4453dc3
fix: add missing ToolListPanel button-mapping tests, double-flush asy…
aaronbrezel Feb 25, 2026
95dd7d1
fix: set btn.disabled in ToolListPanel loading state
aaronbrezel Feb 25, 2026
ffbcff5
feat: add ConfigureAIRunPanel with component-based form, savedState, …
aaronbrezel Feb 25, 2026
37672da
test: assert button text restoration in ConfigureAIRunPanel success/f…
aaronbrezel Feb 25, 2026
85408d4
refactor: export SavedState from ConfigureAIRunPanel; use typed cast …
aaronbrezel Feb 25, 2026
2cc538f
feat: add RecipesListPanel and DocumentSummarizationPanel stubs
aaronbrezel Feb 25, 2026
d5d28a2
feat: migrate sidebar to panel-router architecture; cleanup old code
aaronbrezel Feb 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .clasp.dev.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"scriptId": "1x1qwECFShOjQ-HaHslRjURQmWji08Xw4w_NTQOfMsC4H5TYcw0Flpsi6",
"rootDir": "./dist"
}
}
2 changes: 1 addition & 1 deletion .clasp.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"scriptId": "1x1qwECFShOjQ-HaHslRjURQmWji08Xw4w_NTQOfMsC4H5TYcw0Flpsi6",
"rootDir": "./dist"
}
}
4 changes: 2 additions & 2 deletions .clasp.prod.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"scriptId": "YOUR_PROD_SCRIPT_ID_HERE",
"scriptId": "1bxxBbNIGH4B5MAi9FOedadRPf_ycnSFAXxiZ_ZLMsZqiUFOY90qyNxhx",
"rootDir": "./dist"
}
}
4 changes: 2 additions & 2 deletions .github/workflows/lint-typecheck-format-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: Lint/Typecheck/Format/Test

on:
push:
branches: [main]
branches: [main, develop]
pull_request:
branches: [main]
branches: [main, develop]

jobs:
lint-typecheck-format-test:
Expand Down
104 changes: 92 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ npm run lint # ESLint on src/
npm run lint:fix # ESLint with auto-fix
npm run format # Prettier on src/ (rewrites files)
npm run format:check # Check Prettier formatting without modifying files
npm run typecheck # TypeScript type check without building (tsc --noEmit)
npm run typecheck # TypeScript type check without building (server + client tsconfigs)
npm run deploy:dev # Build + clasp push to dev script
npm run deploy:prod # Build + clasp push to prod script
npm run deploy:watch:dev # Continuous build + clasp push watch (dev)
Expand All @@ -41,13 +41,17 @@ Run a single test by name: `npx jest -t "extractId"`

### Build Pipeline

The build produces two outputs via a `rollup.config.js` array:

**Config 1 — Server bundle:**

`src/server/index.ts` → Rollup (IIFE format, assigned to `_GASEntry`) → `dist/index.js`

Apps Script has no module system — it only sees top-level functions in the global scope. Rollup wraps everything in an IIFE assigned to `_GASEntry`, so exports from `index.ts` are not directly visible. The `footer` field in `rollup.config.js` bridges this gap by appending plain global function stubs that delegate into the IIFE:

```js
function onOpen(e) { _GASEntry.onOpen(e); }
function showSourceDialog() { _GASEntry.showSourceDialog(); }
function showSidebar() { _GASEntry.showSidebar(); }
// ... one stub per public entry point
```

Expand All @@ -57,29 +61,105 @@ function showSourceDialog() { _GASEntry.showSourceDialog(); }

If you skip step 2, the function will exist in the bundle but Apps Script won't be able to discover or call it.

**Custom functions (callable from spreadsheet cells) require one extra step:**
3. Add a JSDoc comment with `@customfunction` directly on the stub in `rollup.config.js`

The TypeScript-level JSDoc is compiled away by Rollup and does not appear on the global stub. Google Sheets only registers a function as a custom function when `@customfunction` is present in a JSDoc comment on the **global** declaration — the one in the footer. Without it the function executes correctly when called explicitly but does not appear in autocomplete and is not recognized as a custom function by Sheets.

**Config 2 — Client bundle → `dist/Sidebar.html`:**

`src/client/sidebar-entry.ts` → Rollup (IIFE) → `inlineSidebarHtml` plugin → `dist/Sidebar.html`

HtmlService can only serve `.html` files — all JavaScript and CSS must be inlined at build time. The custom `inlineSidebarHtml` Rollup plugin handles this:
1. Compiles `sidebar-entry.ts` to an intermediate JS chunk
2. Reads `src/Sidebar.html` (the template), `src/client/sidebar.css`
3. Replaces `{{STYLES}}` with `<style>…css…</style>` and `{{SCRIPTS}}` with `<script>…js…</script>`
4. Emits `dist/Sidebar.html` as an asset
5. Deletes the intermediate `.js` chunk so clasp never pushes it as a `.gs` file

The `src/Sidebar.html` template is also read at test time by `__tests__/helpers/sidebar-fixtures.ts` to keep DOM fixtures structurally in sync with the real template.

### Module Dependency Graph

**Server:**
```
src/server/index.ts (entry point — menu, 4 tool orchestrators, UI handlers, re-exports custom functions)
├── src/server/config.ts (CONFIG object: API key property name, model, column names, limits)
├── src/server/api.ts (callGeminiAPI, buildGeminiPayload — pure HTTP adapter via UrlFetchApp)
├── src/server/drive.ts (extractTextUniversal, fetchAndEncodeFile, checkDriveService)
├── src/server/dialog.ts (HTML_TEMPLATE string for AI mode selection modal)
├── src/server/utils.ts (extractId, isValidDriveLink, createSeededRandom, getAllFilesRecursive, sampleRows, truncateText)
├── src/server/customFunctions.ts (SSI — Sheets custom function; TOOL_REGISTRY for named tool declarations)
└── src/shared/types.ts (shared interfaces: AppConfig, AIMode, ColumnMap, GeminiRequest, etc.)
```

**Client:**
```
src/server/index.ts (entry point — menu, 4 tool orchestrators, UI handlers)
├── src/server/config.ts (CONFIG object: API key property name, model, column names, limits)
├── src/server/api.ts (callGeminiAPI — text or multimodal via UrlFetchApp + base64)
├── src/server/drive.ts (extractTextUniversal, checkDriveService — OCR via Drive Advanced Service)
├── src/server/dialog.ts (HTML_TEMPLATE string for AI mode selection modal)
├── src/server/utils.ts (extractId, isValidDriveLink, createSeededRandom, getAllFilesRecursive, sampleRows, truncateText, getAIContext)
└── src/shared/types.ts (shared interfaces: AppConfig, AIMode, ColumnMap, AIContext variants)
src/client/sidebar-entry.ts (thin init — creates Router, registers panels, calls router.start())
└── src/client/router.ts (Router class — push/pop navigation stack)
└── src/client/services.ts (GAS boundary — wraps google.script.run as Promises, header cache)
└── src/client/panels/ (panel classes — mount/unmount lifecycle)
└── src/client/components/ (reusable UI components — TagList, SingleTagList, RowRange, LockableField)
└── src/shared/types.ts

src/client/google.d.ts (compile-time type stub for google.script.run — uses declare global{} pattern)
src/client/sidebar.css (sidebar styles — inlined into dist/Sidebar.html at build time)
src/Sidebar.html (sidebar template — {{STYLES}} and {{SCRIPTS}} placeholders replaced at build time)
```

Source files use relative imports (e.g. `../shared/types`). The `@server/*` and `@shared/*` aliases are **Jest-only** (mapped in `jest.config.cjs`) and are not available in TypeScript source.

Only `index.ts` should reference Google Apps Script UI services (SpreadsheetApp, HtmlService, PropertiesService). Other modules use injected values or specific GAS globals documented in their headers.
Only `index.ts` should reference Google Apps Script UI services (SpreadsheetApp, HtmlService, PropertiesService). On the client side, only `services.ts` calls `google.script.run` (wrapping each call as a Promise); `sidebar-entry.ts` is a thin init file that creates the Router and calls `router.start()`.

### TypeScript Configuration

Two tsconfigs for two build environments:

- **`tsconfig.json`** — server build. Targets ES2019, no DOM lib, excludes `src/client/`.
- **`tsconfig.client.json`** — client build and client tests. Extends base, adds `"lib": ["ES2019", "DOM"]`, sets `rootDir: "."` (covers both `src/` and `__tests__/`). Includes precise file patterns: `src/client/**/*.ts`, `src/shared/**/*.ts`, and the three client-side test files.

`npm run typecheck` runs both: `tsc --noEmit && tsc -p tsconfig.client.json --noEmit`.

**Note on types:** `tsconfig.client.json` uses `"types": ["google-apps-script", "jest"]` — do **not** add `"node"` here, as it causes `MimeType` collisions with the google-apps-script types. When a file needs Node.js types (e.g. `readFileSync`), use a triple-slash directive at the top of that file: `/// <reference types="node" />`.

### Testing

Jest with ts-jest preset. Tests live in `__tests__/`. Path aliases `@server/*` and `@shared/*` are mapped in `jest.config.cjs`.

**Pattern for mocking GAS globals:** Declare mocks (UrlFetchApp, DriveApp, SpreadsheetApp, etc.) as `globalThis` properties **before** importing the module under test, since imports execute immediately.

**Coverage:** Run `npm run test:coverage` to collect coverage and enforce per-file thresholds. Coverage is opt-in — the pre-commit hook runs `jest --bail` without `--coverage`. `src/server/index.ts` is excluded from coverage collection: `onOpen` and `openQuickstartDoc` are tested in `menu.test.ts`, but the four tool orchestrators are deeply coupled to SpreadsheetApp UI globals and are not unit-tested. See `docs/plans/2026-02-18-testing-coverage-design.md` for full rationale.
**Client-side mock pattern:** For `google.script.run`, capture the success/failure handlers registered by the function under test:

```ts
const mockRun = {
withSuccessHandler: jest.fn().mockReturnThis(),
withFailureHandler: jest.fn().mockReturnThis(),
getSheetHeaders: jest.fn(),
// ...
};
(globalThis as unknown as { google: unknown }).google = { script: { run: mockRun } };

let capturedSuccess: (v: unknown) => void;
beforeEach(() => {
mockRun.withSuccessHandler.mockImplementation((fn) => { capturedSuccess = fn; return mockRun; });
});
// Then invoke capturedSuccess(...) / capturedFailure(...) to simulate GAS callbacks.
```

**Shared DOM fixtures:** `__tests__/helpers/sidebar-fixtures.ts` exports:
- `FULL_SIDEBAR_HTML` — the sidebar HTML template read from `src/Sidebar.html` at test time (placeholders stripped), so tests stay structurally in sync with the real template without manual drift.
- `setupConfigPanel(headers?)` — sets `document.body.innerHTML` and populates all tag containers.
- `setupWithSelections(opts)` — calls `setupConfigPanel` then pre-selects values via `applyPreset`.

The `__tests__/helpers/` directory is excluded from test discovery via `testPathIgnorePatterns` in `jest.config.cjs`.

**Coverage:** Run `npm run test:coverage` to collect coverage and enforce per-file thresholds. Coverage is opt-in — the pre-commit hook runs `jest --bail` without `--coverage`.

Two boundary files are excluded from high thresholds:
- `src/server/index.ts` — excluded from coverage collection entirely. The four tool orchestrators are deeply coupled to SpreadsheetApp UI globals and are not unit-tested.
- `src/client/sidebar-entry.ts` — included in collection with lower per-file thresholds. The four exported functions (`showAIPanel`, `hideAIPanel`, `dispatchTool`, `runAI`) are fully tested. `init()` and its inner `addEventListener` arrow functions run at module load time before `beforeEach` sets up the DOM, so they are never invoked.

See `docs/plans/2026-02-18-testing-coverage-design.md` and `docs/plans/2026-02-24-sidebar-entry-testing-design.md` for full rationale.

**CI:** `.github/workflows/lint-typecheck-format-test.yml` runs on push to `main` and PRs targeting `main`: lint → typecheck → format check → test with coverage.

Expand All @@ -103,7 +183,7 @@ The Gemini API key must be set as a Script Property (`GEMINI_API_KEY`) in Apps S
- **No Node.js built-ins** — everything runs on Google's servers
- `appsscript.json` must be in `dist/` for clasp push (the build script copies it)
- Drive Advanced Service must be enabled in the Apps Script editor AND declared in `appsscript.json`
- `PropertiesService` is not available in custom functions (only in menu-triggered functions)
- `PropertiesService.getScriptProperties()` is available in custom functions once the add-on has been authorized by the user (opening the menu triggers authorization)
- `.clasp.json` is generated at deploy time by copying `.clasp.dev.json` or `.clasp.prod.json`

## Code Style
Expand Down
Loading