Skip to content

feat: client panel-router architecture with component-based sidebar#25

Closed
aaronbrezel wants to merge 93 commits into
mainfrom
develop
Closed

feat: client panel-router architecture with component-based sidebar#25
aaronbrezel wants to merge 93 commits into
mainfrom
develop

Conversation

@aaronbrezel

Copy link
Copy Markdown
Member

Summary

  • Router navigation stack: Replaces the old two-panel toggle with a Router class that manages a push/pop navigation history, enabling panels to go back to previous panels with their state restored.
  • Self-contained components: TagList, SingleTagList, RowRange, and LockableField each own their own DOM subtree, reducing coupling between sidebar logic and HTML structure.
  • services.ts as GAS boundary: All google.script.run calls are wrapped as Promises in a single module with a cached header fetch, making panel code fully unit-testable without GAS globals.
  • Panels: ToolListPanel (tool list), ConfigureAIRunPanel (AI run config with savedState), RecipesListPanel stub, and DocumentSummarizationPanel stub.
  • Cleanup: Deleted old sidebar.ts, sidebar-entry.ts monolith, and all associated tests/fixtures. Updated jest.config.cjs and tsconfig.client.json accordingly.

Architecture

sidebar-entry.ts (init)
└── Router (push/pop stack)
    ├── ToolListPanel          — tool buttons, dispatches via services.ts
    ├── ConfigureAIRunPanel    — column selectors + row range + Run AI button
    ├── RecipesListPanel       — stub
    └── DocumentSummarizationPanel — stub ("Coming soon")

services.ts                   — google.script.run → Promise wrappers + header cache
components/
  tag-list.ts                 — multi-select column tags
  single-tag-list.ts          — single-select + new column option
  row-range.ts                — all/range radio + number inputs
  lockable-field.ts           — locked-by-default input/textarea

Test Coverage

162 tests across 14 suites. Per-file coverage thresholds enforced in jest.config.cjs.

Manual Testing Steps

Prerequisites:

  • Deploy to dev with npm run deploy:dev (requires .clasp.dev.json and GAS authorization)
  • Open the bound spreadsheet, then Extensions → SSI Tools → Open Sidebar

1. Sidebar loads — tool list renders

  • Expected: Sidebar opens showing three buttons: "Import Drive Links", "Extract Text", "Sample Rows", and "Run AI Inference"
  • Expected: No console errors in the browser dev tools (F12 → Extensions → Apps Script → Console not available client-side; check via Ctrl+Shift+J in the sidebar context)

2. Navigate to Configure AI Run

  • Click "Run AI Inference"
  • Expected: Panel transitions to the AI run configuration view
  • Expected: A "← Back" button appears at the top
  • Expected: Column selectors appear (User Prompt Columns, Drive File Columns, System Prompt Column, Output Column)

3. Headers load into column selectors

  • Expected: After a brief load, column headers from the active sheet populate the tag lists
  • Expected: "User Prompt Columns" shows checkboxes for each header
  • Expected: "System Prompt Column" and "Output Column" show radio buttons

4. savedState roundtrip — Back and return preserves selections

  • Select some columns in the AI run panel (e.g., check a user prompt column)
  • Click "← Back" → returns to tool list
  • Click "Run AI Inference" again
  • Expected: Previously selected columns are not restored (fresh navigation from tool list passes no savedState; savedState is only restored when navigating back from a deeper panel)

5. Back button at root does nothing

  • On the tool list, there is no Back button
  • If the sidebar is refreshed (GAS reloads the page), it always starts at the tool list

6. Run AI (happy path)

  • On a sheet with columns: source_drive, source_text, system_prompt, user_prompt, ai_inference
  • Set GEMINI_API_KEY in Apps Script → Project Settings → Script Properties
  • Select the appropriate columns in the Configure AI Run panel
  • Choose row range and click "Run AI"
  • Expected: Button shows "⏳ Running…" and is disabled while running
  • Expected: On success, sidebar returns to tool list

7. Run AI (error path)

  • Intentionally provide an invalid Gemini API key via Script Properties
  • Click "Run AI"
  • Expected: Button shows "⏳ Running…" while running, then re-enables on failure
  • Expected: An alert() dialog appears with the error message

8. Recipes stub

  • Click "Recipes" on the tool list (if visible — may not be wired yet; verify RecipesListPanel is registered)
  • Expected: Navigates to a "Recipes" panel with a "Document Summarization" button
  • Click "Document Summarization"
  • Expected: Panel shows "Coming soon."
  • Click Back twice to return to the tool list

🤖 Generated with Claude Code

aaronbrezel and others added 30 commits February 18, 2026 15:05
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* 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>
* 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>
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>
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>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…orized_user type to clasprc

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t 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>
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>
aaronbrezel and others added 28 commits February 24, 2026 14:52
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>
…ent 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>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…onfig.client.json

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
)

* 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>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…erialization

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… header cache

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lue()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…put, handles custom savedState values

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…getValue()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…with unlock toggle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nc assertions, use globalThis.alert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…and Promise-based GAS calls

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ailure tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…in test helper

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
@aaronbrezel

Copy link
Copy Markdown
Member Author

Closing — will re-open as feature branch targeting develop instead.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant