From 986e8e961d5bbdd0c717d153734543d4b2398824 Mon Sep 17 00:00:00 2001 From: Richard McCorkle Date: Mon, 18 May 2026 16:32:29 +0200 Subject: [PATCH] chore: stop tracking docs/superpowers workflow docs The superpowers specs/plans are internal workflow scaffolding, not distributable project docs. Untrack them and add docs/superpowers/ to .gitignore. Files are kept locally. This also clears a GitHub secret-scanning false positive: a spec quoted YouTube's public InnerTube key, which matches the AIzaSy* Google-API-key pattern. Co-Authored-By: Claude Opus 4.7 --- .gitignore | 3 + .../2026-05-16-obsidian-review-remediation.md | 495 ----------------- .../plans/2026-05-16-processing-spinner.md | 333 ----------- .../2026-05-16-secret-storage-api-keys.md | 315 ----------- ...2026-05-16-tubesage-ui-native-alignment.md | 156 ------ ...26-05-17-reduce-vault-clipboard-surface.md | 384 ------------- ...026-05-18-copy-button-tooltips-diagrams.md | 185 ------- .../2026-05-18-mobile-template-viewer.md | 120 ---- ...service-info-openrouter-desktop-spinner.md | 323 ----------- .../2026-05-18-tier-a-native-components.md | 199 ------- ...26-05-18-transcript-fallback-ios-player.md | 516 ------------------ ...5-14-archon-graphified-workflows-design.md | 212 ------- ...26-05-16-secret-storage-api-keys-design.md | 167 ------ ...7-reduce-vault-clipboard-surface-design.md | 95 ---- ...18-copy-button-tooltips-diagrams-design.md | 96 ---- ...026-05-18-mobile-template-viewer-design.md | 44 -- ...-info-openrouter-desktop-spinner-design.md | 72 --- ...6-05-18-tier-a-native-components-design.md | 72 --- ...8-transcript-fallback-ios-player-design.md | 127 ----- 19 files changed, 3 insertions(+), 3911 deletions(-) delete mode 100644 docs/superpowers/plans/2026-05-16-obsidian-review-remediation.md delete mode 100644 docs/superpowers/plans/2026-05-16-processing-spinner.md delete mode 100644 docs/superpowers/plans/2026-05-16-secret-storage-api-keys.md delete mode 100644 docs/superpowers/plans/2026-05-16-tubesage-ui-native-alignment.md delete mode 100644 docs/superpowers/plans/2026-05-17-reduce-vault-clipboard-surface.md delete mode 100644 docs/superpowers/plans/2026-05-18-copy-button-tooltips-diagrams.md delete mode 100644 docs/superpowers/plans/2026-05-18-mobile-template-viewer.md delete mode 100644 docs/superpowers/plans/2026-05-18-service-info-openrouter-desktop-spinner.md delete mode 100644 docs/superpowers/plans/2026-05-18-tier-a-native-components.md delete mode 100644 docs/superpowers/plans/2026-05-18-transcript-fallback-ios-player.md delete mode 100644 docs/superpowers/specs/2026-05-14-archon-graphified-workflows-design.md delete mode 100644 docs/superpowers/specs/2026-05-16-secret-storage-api-keys-design.md delete mode 100644 docs/superpowers/specs/2026-05-17-reduce-vault-clipboard-surface-design.md delete mode 100644 docs/superpowers/specs/2026-05-18-copy-button-tooltips-diagrams-design.md delete mode 100644 docs/superpowers/specs/2026-05-18-mobile-template-viewer-design.md delete mode 100644 docs/superpowers/specs/2026-05-18-service-info-openrouter-desktop-spinner-design.md delete mode 100644 docs/superpowers/specs/2026-05-18-tier-a-native-components-design.md delete mode 100644 docs/superpowers/specs/2026-05-18-transcript-fallback-ios-player-design.md diff --git a/.gitignore b/.gitignore index ac91fc2..0f24352 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,9 @@ Style2CSS.txt # Chromium user data (contains browsing session data) chromium-user-data/ +# Superpowers workflow docs (specs/plans) — internal scaffolding, not for distribution +docs/superpowers/ + # Personal deploy script — references local vault paths; not for distribution scripts/deploy.sh .fallow/ diff --git a/docs/superpowers/plans/2026-05-16-obsidian-review-remediation.md b/docs/superpowers/plans/2026-05-16-obsidian-review-remediation.md deleted file mode 100644 index 95c192f..0000000 --- a/docs/superpowers/plans/2026-05-16-obsidian-review-remediation.md +++ /dev/null @@ -1,495 +0,0 @@ -# Obsidian Community Portal Review Remediation — Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Clear the Obsidian Community Portal review warnings for TubeSage so a new release (v1.2.28) passes the automated scan. - -**Architecture:** The review (commit `9338b51`, v1.2.27) produced 8 categories of findings. They split into three workstreams by risk and verifiability: **Phase 1** — mechanical fixes that are concretely planned here and ship in one v1.2.28 release; **Phase 2** — the CSS `!important` / `:has()` refactor, which can only be verified by visually loading the plugin in Obsidian, so it is documented as a procedure, not pre-baked code; **Phase 3** — the `setInterval` finding, already investigated and resolved (it is a disclosure, folded into Phase 1 Task 5). - -**Tech Stack:** TypeScript, esbuild, GitHub Actions, Obsidian plugin API. - ---- - -## Investigation findings (resolved before planning) - -- **langsmith ×3 advisories** — already fixed on `main`. `npm ls langsmith` resolves `langsmith@0.6.3` via the `package.json` override `"langsmith": ">=0.6.0"`. 0.6.3 ≥ all three advisory thresholds (0.4.6 / 0.5.18 / 0.5.19). The warning is stale because the review scanned `9338b51` (the 1.2.27 release commit), which predates the override bump. **Cutting v1.2.28 from current `main` clears it — no code change.** -- **Build verification mismatch** — a fresh `npm ci && npm run build` from commit `9338b51` produces a `main.js` that is **byte-identical** to the released 1.2.27 asset (1,712,967 bytes, `cmp` confirms IDENTICAL). The build *is* reproducible from the committed lockfile. The portal's mismatch is because `release.yml` uses `npm install` (unpinned) rather than `npm ci` (locked). Fix: switch to `npm ci` — Task 4. -- **setInterval + network** — two `setInterval` calls exist only in bundled dependencies, none in TubeSage source: (1) `p-queue`'s concurrency timer (no network); (2) `langsmith`'s cache-refresh loop. `langsmith` is LangChain's optional tracing library, pulled in transitively via `@langchain/core`. It is dormant unless LangSmith tracing is explicitly enabled (env vars / API key) — TubeSage never does this. Fix: disclosure paragraph in README — Task 5. - ---- - -# PHASE 1 — Mechanical fixes (one v1.2.28 release) - -## Task 1: Add a GitHub-recognizable LICENSE file - -The repo has `MIT-license-tubesage.md`, but GitHub's license detector needs a standard filename (`LICENSE`) containing only recognized license text. The existing file also bundles a "YouTube Content Usage Disclaimer" which breaks detection. Keep `MIT-license-tubesage.md` (the plugin displays it in-app); add a clean `LICENSE`. - -**Files:** -- Create: `LICENSE` - -- [ ] **Step 1: Create the LICENSE file** - -Create `LICENSE` with exactly this content (standard MIT, no markdown headers, no extra sections): - -``` -MIT License - -Copyright (c) 2024-2026 Richard McCorkle - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -``` - -- [ ] **Step 2: Verify** - -Run: `head -1 LICENSE` -Expected: `MIT License` - -- [ ] **Step 3: Commit** - -```bash -git add LICENSE -git commit -m "chore: add standard LICENSE file for GitHub license detection" -``` - -## Task 2: Remove unnecessary type assertion in fetch-shim.ts - -`src/utils/fetch-shim.ts:174` has `(init.body as ArrayBufferView).buffer`. In the ternary's false branch, `init.body` is already narrowed to `ArrayBufferView` by the enclosing `ArrayBuffer.isView()` guard, so the assertion is redundant (flagged by `@typescript-eslint/no-unnecessary-type-assertion`). - -**Files:** -- Modify: `src/utils/fetch-shim.ts:170-174` - -- [ ] **Step 1: Verify the lint warning reproduces locally** - -Run: `npm run lint 2>&1 | grep -A1 fetch-shim` -Expected: a `no-unnecessary-type-assertion` warning at line 174. - -- [ ] **Step 2: Remove the assertion** - -In `src/utils/fetch-shim.ts`, change this block: - -```typescript - } else if (init.body instanceof ArrayBuffer || ArrayBuffer.isView(init.body)) { - // Handle binary data - options.arrayBuffer = init.body instanceof ArrayBuffer - ? init.body - : (init.body as ArrayBufferView).buffer; -``` - -to: - -```typescript - } else if (init.body instanceof ArrayBuffer || ArrayBuffer.isView(init.body)) { - // Handle binary data - options.arrayBuffer = init.body instanceof ArrayBuffer - ? init.body - : init.body.buffer; -``` - -- [ ] **Step 3: Verify build + lint both pass** - -Run: `npm run build && npm run lint 2>&1 | grep fetch-shim || echo "fetch-shim clean"` -Expected: build succeeds (tsc + esbuild), and `fetch-shim clean` prints. - -- [ ] **Step 4: Commit** - -```bash -git add src/utils/fetch-shim.ts -git commit -m "fix: remove unnecessary type assertion in fetch-shim" -``` - -## Task 3: Fix CSS-lint quick wins (duplicate selectors + shorthand) - -Three duplicate selectors and one redundant shorthand. These are safe edits — they merge declarations into the canonical block. - -**Files:** -- Modify: `styles.css` (lines 361-395, 504+, 653+, 1016-1022, 1048) - -- [ ] **Step 1: Merge `.tubesage-settings-info-icon-with-tooltip::after`** - -In the block at line 361, change `white-space: nowrap;` to `white-space: normal;` and add `width: max-content;` after it. Then delete the duplicate block (the `/* Handle long tooltips by allowing wrapping */` comment plus its rule, lines 390-395): - -Delete: -```css -/* Handle long tooltips by allowing wrapping */ -.tubesage-settings-info-icon-with-tooltip::after { - white-space: normal; - width: max-content; - max-width: 300px; -} -``` - -The line-380 declaration inside the 361 block changes from: -```css - white-space: nowrap; -``` -to: -```css - white-space: normal; - width: max-content; -``` - -- [ ] **Step 2: Merge `.tubesage-license-container`** - -Add `font-family: var(--font-monospace);` as the last declaration of the canonical block at line 504. Then delete the duplicate at lines 1016-1018: - -Delete: -```css -.tubesage-license-container { - font-family: var(--font-monospace); -} -``` - -- [ ] **Step 3: Merge `.tubesage-readme-container`** - -Add `font-family: var(--font-interface);` as the last declaration of the canonical block at line 653. Then delete the duplicate at lines 1020-1022: - -Delete: -```css -.tubesage-readme-container { - font-family: var(--font-interface); -} -``` - -- [ ] **Step 4: Fix redundant margin shorthand** - -At line 1048 (`.tubesage-custom-params-header`), change: -```css - margin: 0 0 15px 0; -``` -to: -```css - margin: 0 0 15px; -``` - -- [ ] **Step 5: Verify no duplicate selectors remain (for these three)** - -Run: `grep -nc "tubesage-license-container {" styles.css; grep -nc "tubesage-readme-container {" styles.css` -Expected: each selector now appears exactly once. - -- [ ] **Step 6: Commit** - -```bash -git add styles.css -git commit -m "fix: dedupe CSS selectors and redundant margin shorthand" -``` - -## Task 4: Make release builds reproducible + add artifact attestation - -`release.yml` uses `npm install` (unpinned) and creates releases without provenance attestation. Switch to `npm ci` (verified to reproduce the artifact byte-for-byte), add `actions/attest-build-provenance`, and modernize the action versions. - -**Files:** -- Modify: `.github/workflows/release.yml` - -- [ ] **Step 1: Replace `.github/workflows/release.yml` with this content** - -```yaml -name: Release Obsidian plugin - -on: - push: - tags: - - "*" - -jobs: - build: - runs-on: ubuntu-latest - permissions: - contents: write - id-token: write - attestations: write - steps: - - uses: actions/checkout@v4 - - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: "20.x" - - - name: Build plugin - run: | - npm ci - npm run build - - - name: Generate artifact attestation - uses: actions/attest-build-provenance@v2 - with: - subject-path: | - main.js - styles.css - manifest.json - - - name: Create release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - tag="${GITHUB_REF#refs/tags/}" - - if gh release view "$tag" >/dev/null 2>&1; then - echo "Release $tag already exists, skipping draft creation" - exit 0 - fi - - gh release create "$tag" \ - --title="$tag" \ - --draft \ - main.js styles.css manifest.json README.md LICENSE MIT-license-tubesage.md templates/YouTubeTranscript.md -``` - -Changes: `checkout@v3→v4`, `setup-node@v3→v4`, `node 18.x→20.x`, `npm install→npm ci`, added attestation step + `id-token`/`attestations` permissions, added `LICENSE` to release assets. - -- [ ] **Step 2: Verify YAML parses** - -Run: `python3 -c "import yaml,sys; yaml.safe_load(open('.github/workflows/release.yml')); print('valid')"` -Expected: `valid` - -- [ ] **Step 3: Commit** - -```bash -git add .github/workflows/release.yml -git commit -m "ci: use npm ci for reproducible builds and add artifact attestation" -``` - -## Task 5: De-promote the README + fix license link + disclose setInterval - -The portal flagged "excessive promotional language." Rewrite README.md factually: drop superlatives ("powerful", "cutting-edge", "state-of-the-art", "intelligent", "seamlessly", "Smart", "Advanced"), drop emoji section headers, drop the stale "Recent Updates (v1.0.6)" section and the closing marketing line. Fix the license link (`MIT-license-tubesage.md` → `LICENSE`). Add a factual disclosure paragraph about the bundled `langsmith` background timer. - -**Files:** -- Modify: `README.md` (full rewrite) - -- [ ] **Step 1: Replace README.md with the de-promoted version** - -Replace the entire file with this content: - -````markdown -# TubeSage - -TubeSage is an Obsidian plugin that converts YouTube videos into structured notes using large language models. It extracts transcripts, generates summaries, and can add timestamped links back to specific moments in the video. - -## Quick Start - -### Installation - -**Community Plugins:** Install through Obsidian's Settings → Community plugins browser once TubeSage is listed in the directory. - -**Manual install from a GitHub release:** -1. Open the [latest release](https://github.com/rmccorkl/TubeSage/releases/latest) page. -2. From the **Assets** section, download `main.js`, `manifest.json`, and `styles.css`. - *Do not* download "Source code (zip)" or "Source code (tar.gz)" — those are source archives and will not load as a plugin. -3. In your vault, create the folder `.obsidian/plugins/tubesage/`. -4. Move the three downloaded files into that `tubesage/` folder. -5. In Obsidian, go to Settings → Community plugins, toggle off "Restricted mode" if needed, refresh the installed-plugins list, and enable **TubeSage**. -6. Accept the license terms in TubeSage's settings panel. -7. Configure API keys for your preferred LLM provider. -8. (For YouTube notes) Install the [Templater plugin](https://github.com/SilentVoid13/Templater) for template-driven formatting. - -### Requirements -- [Obsidian](https://obsidian.md/) v1.2.0+ -- [Templater plugin](https://github.com/SilentVoid13/Templater) (required for template functionality) -- An API key for at least one LLM provider: - - OpenAI - - Anthropic - - Google (Gemini) - - OpenRouter - - Ollama (local models) -- A YouTube Data API key (optional — required only for channel/playlist processing) - -## Features - -- Transcript extraction from YouTube videos with fallback methods -- Summary generation using a configurable LLM provider -- Optional timestamp links added to section headings, linking to the moment in the video -- Works on desktop and mobile Obsidian -- Batch processing of YouTube channels and playlists -- Multi-provider support: OpenAI, Anthropic, Google Gemini, OpenRouter, and Ollama -- A cross-platform fetch shim so all providers work on mobile without Node.js dependencies - -## Configuration - -### 1. License acceptance -Accept the MIT license terms in the settings panel before using the plugin. Use the "View License" button to read the full text. - -### 2. LLM provider setup -Choose and configure a provider in settings: - -- **OpenAI** — models such as GPT-4o, GPT-4, GPT-3.5-Turbo. -- **Anthropic** — Claude 3 family models. -- **Google Gemini** — Gemini Pro models. -- **OpenRouter** — gateway access to models from multiple vendors. -- **Ollama** — local open-source models; runs offline, requires Ollama installed and running. - -### 3. Other settings -- **Summary modes**: Fast (brief) or Extensive (detailed). -- **Timestamp processing**: enable or disable automatic timestamp links. -- **Batch processing**: sequential or parallel processing for collections. -- **Performance monitoring**: optional logging of processing times. - -## Usage - -### Basic workflow -1. Click the YouTube icon in the ribbon, or use the Command Palette. -2. Paste a YouTube video URL. -3. Choose a summary mode and folder location. -4. Run processing. -5. The resulting note appears in the chosen folder. - -### Batch processing (channels/playlists) -1. Configure a YouTube Data API key in settings. -2. Paste a channel or playlist URL. -3. Set the number of videos to process. -4. Choose sequential or parallel processing. - -### Timestamp navigation -When enabled, each section heading includes a link that opens the YouTube video at the corresponding moment. - -## Technical Architecture - -- **Main plugin** (`main.ts`): coordinator and UI. -- **Transcript extractor** (`src/youtube-transcript.ts`): multi-method extraction with mobile fallbacks. -- **LLM factory** (`src/llm/llm-factory.ts`): constructs the provider client. -- **Transcript summarizer** (`src/llm/transcript-summarizer.ts`): orchestrates summarization. -- **LangChain client** (`src/llm/langchain-client.ts`): unified interface for cloud providers. -- **Fetch shim** (`src/utils/fetch-shim.ts`): cross-platform HTTP via Obsidian's `requestUrl`. -- **Timestamp processor** (`src/utils/timestamp-utils.ts`): timestamp link generation. -- **Error utilities** (`src/utils/error-utils.ts`): error categorization and retry logic. - -Architecture diagrams are in the `docs/` directory: -- [Workflow Diagram](docs/workflow-diagram.md) -- [Data Flow Diagram](docs/data-flow-diagram.md) - -## Privacy & Security - -- API calls to LLM providers and YouTube use HTTPS. -- No user data is stored on servers operated by the plugin author. -- Ollama can be used for fully local, offline processing. -- The plugin bundles `langsmith` as a transitive dependency of `@langchain/core`. `langsmith` is LangChain's optional tracing library and contains a background cache-refresh timer. TubeSage never enables LangSmith tracing and never sets a LangSmith API key, so this code stays dormant and performs no background network transmission. The only network requests TubeSage makes are to the LLM provider you configure and to YouTube. - -## Troubleshooting - -- **API key errors**: verify keys are configured correctly in settings. -- **Mobile processing issues**: ensure a stable connection, or use Ollama for offline processing. -- **Timestamp link failures**: check video availability and URL format. -- **Batch processing limits**: monitor YouTube API quota usage. - -## License - -This project is licensed under the MIT License — see the [LICENSE](LICENSE) file. A YouTube content-usage disclaimer is included in [MIT-license-tubesage.md](MIT-license-tubesage.md). - -## Support & Contribution - -- **GitHub Issues**: bug reports and feature requests. -- **Code contributions**: fork the repository and open a pull request. - ---- - -**GitHub Repository**: [https://github.com/rmccorkl/TubeSage](https://github.com/rmccorkl/TubeSage) -```` - -- [ ] **Step 2: Verify the license link and no leftover emoji headers** - -Run: `grep -n "MIT-license-tubesage.md)" README.md; grep -cE '^#+ .*[🚀✨🤖📱⚡🔧📋🏗🎯📚💼🎓📖🔄🔐🎨🆘📄🤝]' README.md` -Expected: the license-link grep shows the disclaimer reference line only; the emoji-header count is `0`. - -- [ ] **Step 3: Commit** - -```bash -git add README.md -git commit -m "docs: de-promote README, fix license link, disclose bundled langsmith timer" -``` - -## Task 6: Bump version to 1.2.28 - -**Files:** -- Modify: `manifest.json`, `package.json` - -- [ ] **Step 1: Bump `manifest.json`** - -Change `"version": "1.2.27"` to `"version": "1.2.28"`. - -- [ ] **Step 2: Bump `package.json`** - -Change `"version": "1.2.27"` to `"version": "1.2.28"`. - -- [ ] **Step 3: Verify a clean reproducible build** - -Run: `npm ci && npm run build && echo "build OK"` -Expected: `build OK`. (`postbuild` runs `npm run audit:fallow` — if fallow flags anything, address separately; the build itself must succeed.) - -- [ ] **Step 4: Commit** - -```bash -git add manifest.json package.json -git commit -m "chore: bump version to 1.2.28" -``` - -## Task 7: Tag and release v1.2.28 - -This follows the user's established release flow (see memory `feedback_release_workflow.md`): push commits, push tag, the CI workflow builds and drafts the release, then publish it. - -- [ ] **Step 1: Push commits and tag** - -```bash -git push origin main -git tag 1.2.28 -git push origin 1.2.28 -``` - -- [ ] **Step 2: Wait for the release workflow, then publish** - -The `release.yml` workflow builds with `npm ci`, generates attestation, and creates a **draft** release. Verify it succeeded: - -Run: `gh run list --workflow="Release Obsidian plugin" --limit 1` -Then publish the draft: -```bash -gh release edit 1.2.28 --draft=false -``` - -- [ ] **Step 3: Verify the published release has the required assets** - -Run: `gh release view 1.2.28 --json assets -q '.assets[].name'` -Expected: includes `main.js`, `manifest.json`, `styles.css`. - ---- - -# PHASE 2 — CSS `!important` + `:has()` refactor (approach, not pre-baked code) - -**Why this is separate:** the review flagged ~100 lines using `!important` and 6 uses of `:has()`. Removing `!important` is only correct if the rule still wins against Obsidian's theme styles after removal — and that can only be confirmed by loading the plugin in Obsidian and visually inspecting the settings panel and modals. Pre-writing 100 line-by-line replacements would be fake planning. **This phase cannot be auto-verified; it requires manual visual testing in Obsidian.** - -**Procedure:** - -1. **Create an isolated worktree** for this work (`superpowers:using-git-worktrees`). -2. **`:has()` first — eliminate it via a class.** The 6 `:has(.tubesage-prompt-textarea)` rules style a `.setting-item` based on a child. Instead, find where the prompt-textarea settings are constructed in `main.ts` and call `setting.settingEl.addClass('tubesage-prompt-setting')` (or equivalent) at construction. Then rewrite the 6 selectors from `.setting-item:has(.tubesage-prompt-textarea)` to `.setting-item.tubesage-prompt-setting`. This is deterministic and removes the `:has()` performance warning entirely. -3. **`!important` — remove then re-qualify.** Delete every `!important`, rebuild, load the plugin in Obsidian, and inspect: settings panel headings, provider rows, prompt textareas, and the license/readme/template modals. For each rule that now loses to the theme, raise specificity deliberately — stack the plugin's own class (`.tubesage-x.tubesage-x`) or add a parent scope (`.workspace-leaf-content .tubesage-x`) — rather than restoring `!important`. Many rules (e.g. `.tubesage-readme-modal-size.modal`) already have enough specificity and only need the `!important` deleted. -4. **Verify visually** in Obsidian desktop *and* mobile (or the mobile emulator), since several `!important` rules live inside the `@media (max-width: 768px)` block. -5. Ship in a follow-up release (v1.2.29) — do not block Phase 1 / v1.2.28 on it. - -**Risk:** medium-high. The settings UI is the user-facing surface; a botched specificity change is visible. Worktree isolation + visual testing is mandatory. - ---- - -# PHASE 3 — setInterval finding (RESOLVED) - -No code task. Investigation complete (see "Investigation findings" above). The disclosure is delivered by Phase 1 Task 5's README Privacy section. If the portal still flags it after v1.2.28, the fallback is to respond on the portal dashboard citing the disclosure — `langsmith` cannot be removed without dropping `@langchain/core`, which the architecture mandate requires. - ---- - -## Self-review - -- **Spec coverage:** License ✓ (T1); type assertion ✓ (T2); duplicate selectors + shorthand ✓ (T3); build verification + attestation ✓ (T4); README promo language ✓ (T5); langsmith ✓ (resolved, ships via T6/T7 release); setInterval ✓ (T5 disclosure + Phase 3); `!important` + `:has()` → Phase 2. All 8 review categories accounted for. -- **Placeholder scan:** none — every step has concrete content. Phase 2 is deliberately an approach (justified inline), not placeholder tasks. -- **Type consistency:** Task 2's edit keeps `options.arrayBuffer`'s assignment type-correct (verified by the Step 3 build gate). No cross-task symbol mismatches. diff --git a/docs/superpowers/plans/2026-05-16-processing-spinner.md b/docs/superpowers/plans/2026-05-16-processing-spinner.md deleted file mode 100644 index e208393..0000000 --- a/docs/superpowers/plans/2026-05-16-processing-spinner.md +++ /dev/null @@ -1,333 +0,0 @@ -# Processing Spinner Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Replace TubeSage's in-modal CSS pulse-bar animation with a Braille-dots spinner — in the status bar on desktop (adapted from TubeSage-Wiki-Pro), in-modal on mobile (which has no status bar). - -**Architecture:** A single platform-aware `ProcessingSpinner` class. `start()` mounts the spinner — a status-bar item on desktop, a text element inside the processing modal on mobile — and animates the same Braille frames via `setInterval`. The two existing pulse-bar call sites in `main.ts` switch to it; the pulse CSS is removed. - -**Tech Stack:** TypeScript, esbuild, Obsidian plugin API (`addStatusBarItem`, `Platform`). - -**Testing note:** No automated test harness exists (`npm test` is undefined). Verification is `npm run build` plus the manual checks in Task 4. - ---- - -## File structure - -| File | Change | -|---|---| -| `src/utils/processing-spinner.ts` | New — the `ProcessingSpinner` class | -| `main.ts` | Replace 2 pulse-bar blocks (~3137, ~3461) with `ProcessingSpinner`; stop it in a `finally` | -| `styles.css` | Remove `.pulse-container` / `.pulse-bar` / `@keyframes pulse` (lines ~261-293 and the `@media` variant ~1068-1084); add `.tubesage-spinner` | - ---- - -## Task 1: Create the ProcessingSpinner class - -**Files:** -- Create: `src/utils/processing-spinner.ts` - -- [ ] **Step 1: Write the file** - -Create `src/utils/processing-spinner.ts` with exactly: - -```typescript -import { Plugin, Platform } from "obsidian"; - -// Braille-dots spinner — ten frames, animates smoothly. Ticks every 100ms -// while a long-running operation is active so the user sees the plugin is -// alive even during a single long LLM call. -const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; -const SPINNER_INTERVAL_MS = 100; - -/** - * Platform-aware processing spinner. - * - * Desktop: drives a status-bar item (Obsidian's bottom status bar). - * Mobile: Obsidian has no status bar, so it renders a text element inside - * the supplied modal content element instead. Same Braille frames either way. - * - * Lifecycle: `start()` mounts and animates; `setLabel(text)` updates the - * per-step label and bumps the call counter; `stop()` removes the spinner - * and clears the interval. Safe to call `stop()` more than once. - */ -export class ProcessingSpinner { - private statusBarItem: HTMLElement | null = null; - private modalSpinnerEl: HTMLElement | null = null; - private callCount = 0; - private currentLabel: string; - private spinnerFrame = 0; - private spinnerHandle: number | null = null; - - constructor( - private readonly plugin: Plugin, - private readonly prefix: string, - private readonly modalContentEl: HTMLElement, - initialLabel = "starting…", - ) { - this.currentLabel = initialLabel; - } - - /** Mounts the spinner (status bar on desktop, in-modal on mobile) and starts animating. */ - start(): void { - if (Platform.isMobile) { - this.modalSpinnerEl = this.modalContentEl.createDiv({ cls: "tubesage-spinner" }); - } else { - this.statusBarItem = this.plugin.addStatusBarItem(); - } - this.render(); - this.spinnerHandle = activeWindow.setInterval(() => { - this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length; - this.render(); - }, SPINNER_INTERVAL_MS); - } - - /** Update the per-step label and bump the call counter. */ - setLabel(label: string): void { - if (label) this.currentLabel = label; - this.callCount += 1; - this.render(); - } - - /** Removes the spinner and stops the animation. Safe to call more than once. */ - stop(): void { - if (this.spinnerHandle !== null) { - activeWindow.clearInterval(this.spinnerHandle); - this.spinnerHandle = null; - } - if (this.statusBarItem) { - this.statusBarItem.remove(); - this.statusBarItem = null; - } - if (this.modalSpinnerEl) { - this.modalSpinnerEl.remove(); - this.modalSpinnerEl = null; - } - } - - private render(): void { - const spinner = SPINNER_FRAMES[this.spinnerFrame]; - const text = `${spinner} ${this.prefix} · ${this.currentLabel} · #${this.callCount}`; - if (this.statusBarItem) this.statusBarItem.setText(text); - if (this.modalSpinnerEl) this.modalSpinnerEl.setText(text); - } -} -``` - -- [ ] **Step 2: Verify build** - -Run: `npm run build` -Expected: build succeeds (file compiles; unused until Task 2). - -- [ ] **Step 3: Commit** - -```bash -git add src/utils/processing-spinner.ts -git commit -m "feat: add platform-aware ProcessingSpinner (Braille status-bar / in-modal)" -``` - -## Task 2: Wire ProcessingSpinner into the two processing flows - -`main.ts` creates a `.pulse-container` with 5 `.pulse-bar` divs in two places: -`beginCollectionProcessing` (~line 3137) and the single-video processing flow -(~line 3461). Both run inside an `async` method with a `try { this.isProcessing = true; ... }`. - -**Files:** -- Modify: `main.ts` — import (line 1 area), and the two blocks - -- [ ] **Step 1: Add the import** - -Near the other `src/utils` imports at the top of `main.ts`, add: - -```typescript -import { ProcessingSpinner } from "./src/utils/processing-spinner"; -``` - -(Match the existing relative-path style of neighboring `src/` imports in `main.ts`.) - -- [ ] **Step 2: Replace the first pulse block (`beginCollectionProcessing`, ~3137-3145)** - -Replace: - -```typescript - // Create a minimal container just for centering the pulse bars - const pulseContainerEl = contentEl.createDiv({ - cls: 'pulse-container' - }); - - // Just create the pulse bars - for (let i = 0; i < 5; i++) { - pulseContainerEl.createDiv({ cls: 'pulse-bar' }); - } -``` - -with: - -```typescript - // Braille-dots processing spinner (status bar on desktop, in-modal on mobile) - const spinner = new ProcessingSpinner(this.plugin, 'Processing collection', contentEl); - spinner.start(); -``` - -- [ ] **Step 3: Ensure the first spinner stops** - -`beginCollectionProcessing` wraps its work in `try { ... }`. Find the matching -`catch`/end of that `try` block and ensure the method stops the spinner on every -exit path by adding a `finally` block (or extending the existing one) that calls: - -```typescript - } finally { - spinner.stop(); - } -``` - -If a `finally` already exists, add `spinner.stop();` to it. The `spinner` const is -in scope for the whole `try` because it is declared at the top of the `try` body. - -- [ ] **Step 4: Replace the second pulse block (single-video flow, ~3461-3469)** - -Replace the identical block: - -```typescript - // Create a minimal container just for centering the pulse bars - const pulseContainerEl = contentEl.createDiv({ - cls: 'pulse-container' - }); - - // Just create the pulse bars - for (let i = 0; i < 5; i++) { - pulseContainerEl.createDiv({ cls: 'pulse-bar' }); - } -``` - -with: - -```typescript - // Braille-dots processing spinner (status bar on desktop, in-modal on mobile) - const spinner = new ProcessingSpinner(this.plugin, 'Processing video', contentEl); - spinner.start(); -``` - -- [ ] **Step 5: Ensure the second spinner stops** - -As in Step 3, ensure the single-video flow's `try` block has a `finally` that calls -`spinner.stop();` on every exit path. - -- [ ] **Step 6: Verify build** - -Run: `npm run build` -Expected: build succeeds; no `pulse-container` / `pulse-bar` references remain in `main.ts` -(`grep -c "pulse-" main.ts` returns 0). - -- [ ] **Step 7: Commit** - -```bash -git add main.ts -git commit -m "feat: use ProcessingSpinner in collection and single-video flows" -``` - -## Task 3: Remove pulse CSS, add the mobile spinner rule - -**Files:** -- Modify: `styles.css` - -- [ ] **Step 1: Delete the main pulse rules** - -Remove this entire block (currently ~lines 261-293): - -```css -.pulse-container { - display: flex; - justify-content: center; - align-items: center; - margin: 0 auto; - height: 40px; - border: none; - background: none; - box-shadow: none; - width: fit-content; - max-width: 100%; -} - -.pulse-bar { - width: 8px; - height: 40px; - margin: 0 3px; - border-radius: 4px; - background-color: var(--interactive-accent); - animation: pulse 1.5s ease-in-out infinite; - display: inline-block; -} - -.pulse-bar:nth-child(1) { animation-delay: 0s; } -.pulse-bar:nth-child(2) { animation-delay: 0.2s; } -.pulse-bar:nth-child(3) { animation-delay: 0.4s; } -.pulse-bar:nth-child(4) { animation-delay: 0.6s; } -.pulse-bar:nth-child(5) { animation-delay: 0.8s; } - -@keyframes pulse { - 0%, 100% { height: 5px; opacity: 0.3; } - 50% { height: 40px; opacity: 1; } -} -``` - -- [ ] **Step 2: Delete the `@media` pulse rules** - -Inside the `@media (max-width: 768px)` block, remove the `.pulse-container`, -`.pulse-bar`, and `@keyframes pulse` rules (and their explanatory comments). Read -the file to find their exact current lines — leave the rest of the `@media` block intact. - -- [ ] **Step 3: Add the mobile spinner rule** - -Add this rule (place it where the `.pulse-container` block was): - -```css -.tubesage-spinner { - display: flex; - justify-content: center; - align-items: center; - margin: 0 auto; - min-height: 40px; - font-family: var(--font-monospace); - font-size: 14px; - color: var(--text-normal); - text-align: center; -} -``` - -- [ ] **Step 4: Verify** - -Run: `grep -c "pulse" styles.css` -Expected: `0`. -Run: `npm run build` -Expected: build succeeds. - -- [ ] **Step 5: Commit** - -```bash -git add styles.css -git commit -m "fix: remove pulse-bar CSS, add tubesage-spinner rule" -``` - -## Task 4: Manual verification - -- [ ] **Step 1: Build** — `npm run build` succeeds. -- [ ] **Step 2: Desktop** — process a video in desktop Obsidian; confirm a Braille spinner - animates in the status bar and disappears when processing ends. -- [ ] **Step 3: Mobile** — process a video in mobile Obsidian; confirm a Braille spinner - animates inside the processing modal (no status bar there) and disappears when done. -- [ ] **Step 4: No leak** — after processing completes on desktop, confirm the status-bar - item is gone (not left behind). - ---- - -## Self-review - -- **Spec coverage:** desktop status-bar spinner (Task 1 + 2), mobile in-modal spinner - (Task 1 `Platform.isMobile` branch + Task 3 CSS), pulse removal (Task 3). Covered. -- **Placeholder scan:** Steps 3, 5, and Task 3 Step 2 reference "find the exact lines" - rather than pre-quoting — this is deliberate: the surrounding `try`/`finally` and the - `@media` block contents must be read live so the edit is correct against current code. - Every code-producing step has complete code. -- **Type consistency:** `ProcessingSpinner` constructor `(plugin, prefix, modalContentEl, initialLabel?)` - is used consistently in Task 2 with 3 args. `start()`/`stop()` names match between Tasks 1 and 2. diff --git a/docs/superpowers/plans/2026-05-16-secret-storage-api-keys.md b/docs/superpowers/plans/2026-05-16-secret-storage-api-keys.md deleted file mode 100644 index 9cb7432..0000000 --- a/docs/superpowers/plans/2026-05-16-secret-storage-api-keys.md +++ /dev/null @@ -1,315 +0,0 @@ -# Secret Storage for API Keys — Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Move TubeSage's OpenAI/Anthropic/Google/OpenRouter API keys out of the plaintext `data.json` into Obsidian's `app.secretStorage`, with a one-time migration that scrubs existing keys. - -**Architecture:** `settings.apiKeys` stays as a runtime object but its 4 cloud-provider entries are never persisted — they are populated from `secretStorage` on load and stripped before every save. Existing read sites are untouched; only load, save, and the API-key settings UI change. Spec: `docs/superpowers/specs/2026-05-16-secret-storage-api-keys-design.md`. - -**Tech Stack:** TypeScript, esbuild, Obsidian plugin API (`app.secretStorage`, `@since 1.11.4`). - -**Note on testing:** The project has no automated test harness (`npm test` is not defined). Verification for every task is `npm run build` (runs `tsc -noEmit -skipLibCheck` + esbuild) plus the manual checks in Task 6. - ---- - -## File structure - -| File | Change | -|---|---| -| `main.ts` | Add module constants; add `persist()` helper; route saves through it; extend `loadSettings()` with migration + population; update `createProviderApiKeyRow` onChange | -| `manifest.json` | `minAppVersion` `1.2.0` → `1.11.4` | -| `package.json` | obsidian devDependency `^1.8.7` → `^1.11.4` | - -No new files. `transcript-summarizer.ts` and `llm-factory.ts` are not touched. - ---- - -## Task 1: Add module constants for cloud providers and secret IDs - -**Files:** -- Modify: `main.ts` (immediately after the `DEFAULT_SETTINGS` object, which ends near line 460) - -- [ ] **Step 1: Add the constants** - -After the `DEFAULT_SETTINGS` declaration (find the line `};` that closes `const DEFAULT_SETTINGS`), add: - -```typescript -// Cloud LLM providers whose API keys are secrets stored in Obsidian's -// secret storage. 'ollama' is intentionally excluded — its apiKeys entry -// is a server URL, not a secret, and stays in data.json. -const CLOUD_PROVIDERS = ['openai', 'anthropic', 'google', 'openrouter'] as const; - -// Secret-storage IDs, one per cloud provider. IDs must be lowercase -// alphanumeric with optional dashes (required by SecretStorage.setSecret). -const SECRET_IDS: Record = { - openai: 'tubesage-openai-key', - anthropic: 'tubesage-anthropic-key', - google: 'tubesage-google-key', - openrouter: 'tubesage-openrouter-key', -}; -``` - -- [ ] **Step 2: Verify build** - -Run: `npm run build` -Expected: build succeeds (the constants are unused so far — that is fine; `tsc` does not error on unused module-level `const`). - -- [ ] **Step 3: Commit** - -```bash -git add main.ts -git commit -m "feat: add cloud-provider and secret-id constants" -``` - -## Task 2: Add a `persist()` helper that strips cloud keys before saving - -The plugin currently writes settings via `this.saveData(this.settings)` in two places — `saveSettings()` and the `customModelLimits` migration inside `loadSettings()`. Both must go through a helper that removes the 4 cloud keys so `data.json` never contains them. - -**Files:** -- Modify: `main.ts` — `saveSettings()` (near line 676) and the `customModelLimits` migration save (near line 672) - -- [ ] **Step 1: Add the `persist()` method** - -Directly above the existing `async saveSettings()` method, add: - -```typescript - /** - * Persist settings to data.json with cloud-provider API keys stripped out. - * Cloud keys live in Obsidian secret storage, never in data.json. - */ - private async persist(): Promise { - const sanitizedApiKeys: Record = { - ollama: this.settings.apiKeys.ollama ?? DEFAULT_SETTINGS.apiKeys.ollama, - }; - const toSave = { ...this.settings, apiKeys: sanitizedApiKeys }; - await this.saveData(toSave); - } -``` - -- [ ] **Step 2: Route `saveSettings()` through `persist()`** - -Replace the body of `saveSettings()`: - -```typescript - async saveSettings() { - await this.saveData(this.settings); - this.initializeSummarizer(); - } -``` - -with: - -```typescript - async saveSettings() { - await this.persist(); - this.initializeSummarizer(); - } -``` - -- [ ] **Step 3: Route the `customModelLimits` migration save through `persist()`** - -Inside `loadSettings()`, in the `customModelLimits` cleanup block, find: - -```typescript - await this.saveData(this.settings); -``` - -(it is the line right after the `for (const key of polluted)` delete loop). Replace it with: - -```typescript - await this.persist(); -``` - -- [ ] **Step 4: Verify build** - -Run: `npm run build` -Expected: build succeeds. - -- [ ] **Step 5: Commit** - -```bash -git add main.ts -git commit -m "feat: strip cloud API keys from persisted settings via persist() helper" -``` - -## Task 3: Migrate and populate cloud keys in `loadSettings()` - -On load: migrate any cloud keys still in `data.json` into secret storage, then populate the runtime `settings.apiKeys` cloud entries from secret storage. If `data.json` contained any cloud keys, persist once to scrub them. - -**Files:** -- Modify: `main.ts` — `loadSettings()`, after the `customModelLimits` migration block, before the method's closing brace - -- [ ] **Step 1: Add the migration + population block** - -In `loadSettings()`, after the entire `customModelLimits` cleanup `if (polluted.length > 0) { ... }` block and before the closing `}` of `loadSettings()`, add: - -```typescript - // --- API key secret-storage migration --------------------------------- - // Cloud-provider keys live in Obsidian secret storage, not data.json. - // 1. Migrate any key still present in data.json into secret storage. - // 2. Populate the runtime settings.apiKeys cloud entries from storage. - // 3. If data.json held any cloud key, persist once to scrub it out. - const dataApiKeys: Record = - isRecord(loadedSettings.apiKeys) ? (loadedSettings.apiKeys as Record) : {}; - let hadCloudKeysInData = false; - for (const provider of CLOUD_PROVIDERS) { - const fromData = dataApiKeys[provider]; - if (typeof fromData === 'string' && fromData.trim() !== '') { - hadCloudKeysInData = true; - if (!this.app.secretStorage.getSecret(SECRET_IDS[provider])) { - this.app.secretStorage.setSecret(SECRET_IDS[provider], fromData); - } - } - } - for (const provider of CLOUD_PROVIDERS) { - this.settings.apiKeys[provider] = - this.app.secretStorage.getSecret(SECRET_IDS[provider]) ?? ''; - } - if (hadCloudKeysInData) { - logger.info('[migration] Moved cloud API keys to secret storage; scrubbing data.json'); - await this.persist(); - } - // ---------------------------------------------------------------------- -``` - -Note: `isRecord` and `logger` are already defined/imported in `main.ts` (used elsewhere in `loadSettings()`). - -- [ ] **Step 2: Verify build** - -Run: `npm run build` -Expected: build succeeds. - -- [ ] **Step 3: Commit** - -```bash -git add main.ts -git commit -m "feat: migrate and load cloud API keys from secret storage" -``` - -## Task 4: Write cloud keys to secret storage from the settings UI - -The API-key text field's `onChange` currently writes only to `settings.apiKeys[provider]`. For cloud providers it must also write secret storage. The field's description text is updated to be accurate. - -**Files:** -- Modify: `main.ts` — `createProviderApiKeyRow` (near lines 4283-4309) - -- [ ] **Step 1: Update the description text** - -In `createProviderApiKeyRow`, replace: - -```typescript - .setDesc('Stored in plugin data; available to all summarisation flows.') -``` - -with: - -```typescript - .setDesc( - provider === 'ollama' - ? 'Server URL, stored in plugin data.' - : 'Stored in Obsidian secret storage, not in plugin data.' - ) -``` - -- [ ] **Step 2: Update the `onChange` handler** - -Replace: - -```typescript - .onChange((value: string) => { - void (async () => { - this.plugin.settings.apiKeys[provider] = value; - await this.plugin.saveSettings(); - })(); - }); -``` - -with: - -```typescript - .onChange((value: string) => { - void (async () => { - this.plugin.settings.apiKeys[provider] = value; - if (CLOUD_PROVIDERS.includes(provider as typeof CLOUD_PROVIDERS[number])) { - this.plugin.app.secretStorage.setSecret(SECRET_IDS[provider], value); - } - await this.plugin.saveSettings(); - })(); - }); -``` - -The `setValue(this.plugin.settings.apiKeys[provider])` call above it is unchanged — the in-memory value was populated from secret storage in `loadSettings()`. - -- [ ] **Step 3: Verify build** - -Run: `npm run build` -Expected: build succeeds. - -- [ ] **Step 4: Commit** - -```bash -git add main.ts -git commit -m "feat: write cloud API keys to secret storage from settings UI" -``` - -## Task 5: Bump `minAppVersion` and the obsidian devDependency - -`app.secretStorage` is `@since 1.11.4`, so the plugin can no longer support older Obsidian. - -**Files:** -- Modify: `manifest.json`, `package.json` - -- [ ] **Step 1: Bump `manifest.json`** - -Change `"minAppVersion": "1.2.0"` to `"minAppVersion": "1.11.4"`. - -- [ ] **Step 2: Bump `package.json`** - -In `devDependencies`, change `"obsidian": "^1.8.7"` to `"obsidian": "^1.11.4"`. - -- [ ] **Step 3: Verify build** - -Run: `npm install && npm run build` -Expected: `npm install` updates the lockfile if needed; build succeeds (the installed obsidian types are already 1.12.3, which contains `SecretStorage`). - -- [ ] **Step 4: Commit** - -```bash -git add manifest.json package.json package-lock.json -git commit -m "chore: require Obsidian 1.11.4 for secret storage API" -``` - -## Task 6: Manual verification - -No code changes — this is the acceptance check. Build, deploy to a test vault, and confirm behavior. - -- [ ] **Step 1: Build** - -Run: `npm run build` -Expected: succeeds. - -- [ ] **Step 2: Fresh-key check** - -Load the plugin in an Obsidian 1.11.4+ vault. In settings, enter an OpenAI API key. Confirm: -- `data.json` (`.obsidian/plugins/tubesage/data.json`) does NOT contain the key — its `apiKeys` object should hold only `ollama`. -- The key resolves after an Obsidian restart (still shown in settings, summaries still authenticate). - -- [ ] **Step 3: Migration check** - -Start from a `data.json` whose `apiKeys` contains a non-empty `openai` key (simulating an upgrade). Load the plugin. Confirm: -- After load, `data.json`'s `apiKeys` no longer contains the `openai` key (only `ollama` remains). -- The key still works (settings shows it; a summary authenticates). - -- [ ] **Step 4: Ollama untouched** - -Confirm the Ollama server URL still saves to and loads from `data.json` as before. - -- [ ] **Step 5: Run a summary** with a cloud provider to confirm end-to-end auth works. - ---- - -## Self-review - -- **Spec coverage:** Secret IDs → Task 1. Runtime `apiKeys` + load/save behavior → Tasks 2, 3. Migration → Task 3. Settings UI → Task 4. Version bumps → Task 5. Edge cases (`getSecret` null → `''`) → handled by `?? ''` in Task 3 and the in-memory value in Task 4. All spec sections covered. -- **Placeholder scan:** none — every step has concrete code and exact commands. -- **Type consistency:** `CLOUD_PROVIDERS` and `SECRET_IDS` (Task 1) are referenced consistently in Tasks 3 and 4. `persist()` (Task 2) is called in Tasks 2 and 3. `SECRET_IDS` is typed `Record` so `SECRET_IDS[provider]` indexing in Tasks 3-4 type-checks. diff --git a/docs/superpowers/plans/2026-05-16-tubesage-ui-native-alignment.md b/docs/superpowers/plans/2026-05-16-tubesage-ui-native-alignment.md deleted file mode 100644 index 549af13..0000000 --- a/docs/superpowers/plans/2026-05-16-tubesage-ui-native-alignment.md +++ /dev/null @@ -1,156 +0,0 @@ -# TubeSage UI Native Alignment Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Tighten the TubeSage plugin UI so it reads as a native Obsidian surface: replace hand-rolled controls with Obsidian's component primitives, kill theme-breaking hardcoded values, and put spacing on Obsidian's variable scale. - -**Architecture:** Three sequential tasks ordered so component replacement happens before the CSS sweep (replacing custom controls deletes CSS, so the final CSS pass runs on what survives). Surfaces: the create-note `Modal`, the `PluginSettingTab`, then a whole-file `styles.css` design-system pass. No behavior changes; only presentation and the markup that produces it. - -**Tech Stack:** TypeScript, Obsidian Plugin API (`Setting`, `ToggleComponent`, `mod-cta`), esbuild. Verification is `npm run build` (tsc strict + esbuild bundle). There is no UI test framework; each task ends with a build and a manual checklist. - -**Design rules (apply in every task):** -- Never hardcode a color. Obsidian theme variables only: `--text-accent`, `--text-normal`, `--text-muted`, `--text-on-accent`, `--background-*`, `--interactive-accent`. -- Never `#fff` / `#000` / `white` / `black`. Toggle knobs and accents come from theme variables or native components. -- Spacing uses Obsidian's scale, not ad-hoc pixels: `--size-4-1` (4px), `--size-4-2` (8px), `--size-4-3` (12px), `--size-4-4` (16px), `--size-4-5` (20px), `--size-4-6` (24px). Map existing values to the nearest step (15px→`--size-4-4`, 10px→`--size-4-2` or `--size-4-3`, 5px→`--size-4-1`). -- Radius: `--radius-s` for inputs/buttons/small controls, `--radius-m` for panels/containers. Replace the ad-hoc `4px/5px/6px/8px` mix. -- Shadows: `--shadow-s` / `--shadow-l`. No hand-rolled `rgba()` box-shadows. -- No inline styles set via JS (`attr: { style: ... }`). The project CLAUDE.md bans this. Use a CSS class. -- Prefer Obsidian native components over custom CSS replicas. `Setting`, `ToggleComponent`, `ButtonComponent`, and the `mod-cta` class already match the theme in every Obsidian theme. -- Do not change any logic, settings keys, event behavior, or copy. Presentation only. - ---- - -### Task 1: Modal native components - -The create-note modal (`YouTubeTranscriptModal`, `main.ts:2767`) is a hand-built form. It ships a CSS-only toggle switch and a custom-styled process button that both duplicate Obsidian primitives, plus five inline-style strings that duplicate classes already in `styles.css`. - -**Files:** -- Modify: `main.ts` — `YouTubeTranscriptModal.buildInputStage()` approx `main.ts:2835-3015` -- Modify: `styles.css` — toggle and process-button rules - -- [ ] **Step 1: Replace the custom toggle switch with a native Obsidian toggle** - -In `buildInputStage()` (`main.ts` ~2987-3014) the "Fast summary mode" control is built from `