Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ UNINSTALL_FORM_URL=
SUBTURTLE_API_URL=
SUBTURTLE_DASHBOARD_URL=
GOOGLE_OAUTH_CLIENT_ID=
# dev/agent only β€” set to "true" in CCW or local dev builds to expose the
# email+password form in the popup. Leave empty/false for stable + dev release
# builds (form stays hidden, real users continue to use Google OAuth).
ENABLE_PASSWORD_AUTH=
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ jobs:
SUBTURTLE_API_URL=http://localhost:4173
SUBTURTLE_DASHBOARD_URL=http://localhost:4173/_dashboard_stub
GOOGLE_OAUTH_CLIENT_ID=ci_e2e_stub_oauth_client
ENABLE_PASSWORD_AUTH=true
EOF

- name: Build extension
Expand Down Expand Up @@ -169,6 +170,7 @@ jobs:
SUBTURTLE_API_URL=${SUBTURTLE_API_URL}
SUBTURTLE_DASHBOARD_URL=${SUBTURTLE_DASHBOARD_URL}
GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID}
ENABLE_PASSWORD_AUTH=false
EOF

- name: Bump versions for build
Expand Down
8 changes: 8 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"chrome-extension-tester": {
"command": "npx",
"args": ["-y", "chrome-extension-tester-mcp@^2.1"]
}
}
}
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
# [1.12.0](https://github.com/codebridger/subturtle-extension-apps/compare/v1.11.2...v1.12.0) (2026-05-26)


### Bug Fixes

* **save-modal:** break circular import to console-crane store ([ab00130](https://github.com/codebridger/subturtle-extension-apps/commit/ab00130647d72703147f8a96df6faa8e3b1fbbe5))
* **save-modal:** refetch bundle options so post-save chip shows title ([25499da](https://github.com/codebridger/subturtle-extension-apps/commit/25499da35925bebeb854f20973c3ae39b4dbd6f2))


### Features

* announce extension presence on dashboard origins for install nudge [#86](https://github.com/codebridger/subturtle-extension-apps/issues/86)exkh0z3 ([69dcf1b](https://github.com/codebridger/subturtle-extension-apps/commit/69dcf1bc0c13f293c01592545ec2ccb1111d5f3e)), closes [#86exkh0z3](https://github.com/codebridger/subturtle-extension-apps/issues/86exkh0z3)
* **console-crane:** practice + flashcard-preview pages, near-translation actions ([224b9da](https://github.com/codebridger/subturtle-extension-apps/commit/224b9da79e8b9d2f094ae3f63b1f8524fcb78299))
* **practice-now:** emphasize practiced phrase + cover login flows ([8ff3408](https://github.com/codebridger/subturtle-extension-apps/commit/8ff340831d3ca35dcbc97ddfcf7088f7f382582b))
* **practice-now:** open config to logged-out users + clearer CTAs ([2f09e05](https://github.com/codebridger/subturtle-extension-apps/commit/2f09e0577de43a55d995e9277446d313b058beef))
* **practice-now:** voice session config + dashboard deep-link ([db1a3fc](https://github.com/codebridger/subturtle-extension-apps/commit/db1a3fc08f2579c2d6c958a2e8c289c95e592339))
* **save-modal:** chunk highlights, AI advice chat, bundle suggestion ([9954c22](https://github.com/codebridger/subturtle-extension-apps/commit/9954c22f8822f5a06912247d0098d937c20ec6b5))
* **save-modal:** in-field bundle chips with dirty-aware save + inline removal ([374cbb4](https://github.com/codebridger/subturtle-extension-apps/commit/374cbb4b0c957c5def27d6b9f0211b3ed4fe17f5))
* **save-modal:** per-chunk definitions, merged pronunciation, reorder save ([f766040](https://github.com/codebridger/subturtle-extension-apps/commit/f76604023886a6d88b18831c9e492ef8253e11b7))
* **saved-phrase:** DB-first lookup, reuse stored translation, no AI re-call ([1315cc8](https://github.com/codebridger/subturtle-extension-apps/commit/1315cc86c3421ab64560e68e70d0b282c662d3ba))

## [1.11.2](https://github.com/codebridger/subturtle-extension-apps/compare/v1.11.1...v1.11.2) (2026-05-06)


Expand Down
128 changes: 127 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ And the `SettingsObject` type in [src/common/types/messaging.ts](src/common/type
- **The mount root in Nibble must not block the page.** Set `width: 0; height: 0; position: fixed; top: 0; left: 0`. Children use their own `position: fixed` to position themselves relative to the viewport.
- **Theme dark class lives on `.subturtle-scope`, not `<html>`.** Tailwind's `dark:` rules are rewritten by `postcss-prefix-selector` to `.subturtle-scope.dark ...` β€” so the same element must carry both classes. The settings store handles this and a `MutationObserver` keeps Vue Teleport subtrees in sync.
- **`src/stores/profile.ts` imports types from a sibling repo.** The path `../../../dashboard-app/frontend/types/database.type` resolves to a directory _next to_ this repo's root, not inside it. The actual repo is [`codebridger/subturtle-dashboard-app`](https://github.com/codebridger/subturtle-dashboard-app); local builds work because devs check both repos out side-by-side. CI clones the dashboard repo into `../dashboard-app/` before `yarn build` runs (see [.github/workflows/release.yml](.github/workflows/release.yml)). Don't try to "fix" the import to a relative-internal path or vendor the file β€” both will drift.
- **Playwright Chromium download isn't on CCW's Trusted allowlist.** The chrome-extension-tester-mcp's `postinstall` runs `playwright install chromium`, which pulls from `cdn.playwright.dev` / `playwright.download.prss.microsoft.com`. CCW environments must use Custom network access with those hosts added β€” see [Β§ Cloud agent workflow](#cloud-agent-workflow-claude-code-on-the-web). The setup script caches Chromium into the VM snapshot so per-session cost is zero.

## Adding things

Expand Down Expand Up @@ -312,6 +313,8 @@ tests/
selection-popup.test.ts # @mousedown.prevent.stop regression
nibble-surface.test.ts # bridge-driven hide/show
translate-card.test.ts # popup translate input flow
login-password.test.ts # popup password form (ENABLE_PASSWORD_AUTH=true)
login-password-disabled.test.ts # popup password form hidden when flag is unset
e2e/
extension-fixture.ts # chromium.launchPersistentContext + extension load
server.mjs # static fixtures HTTP server
Expand All @@ -320,12 +323,132 @@ tests/
nibble-flow.spec.ts # content script mounting + Persian emitOpen
console-crane-lifecycle.spec.ts # modal stays open while Nibble toggles off
translate-flow.spec.ts # full Persian translate-and-save with page.route stubs
password-login.spec.ts # popup password form end-to-end with stubbed /user/login
visual-scale.spec.ts # rem→px rewrite regression net
```

### Test totals

79 unit / component tests across 9 files; 11 E2E specs across 5 files. Full suite runs in ~15s once Playwright's Chromium is warm.
138 unit / component tests across 19 files; 16 E2E specs across 6 files. Full suite runs in ~20s once Playwright's Chromium is warm.

## Cloud agent workflow (Claude Code on the Web)

Lets a cloud Claude agent on [Claude Code on the Web (CCW)](https://code.claude.com/docs/en/web-quickstart) clone the repo, build the extension, install it into a headless Chromium, log in with username/password against the live dev server at `https://dev.dashboard.subturtle.app/`, and drive popup + content scripts with screenshots β€” no Google OAuth, no local backend.

The agent path uses **only** the [chrome-extension-tester-mcp](https://github.com/BHUVAN-RJ/chrome-extension-testing-mcp) MCP server (declared in [.mcp.json](.mcp.json)). It is independent of the `tests/e2e/` Playwright fixture and shares no code with it; CI verify runs the spec, the agent runs the MCP.

### One-time CCW environment setup

Done once per CCW environment from [claude.ai/code](https://claude.ai/code) β€” these settings live in the cloud UI, not the repo.

**Network access:** `Custom`, keeping the Trusted defaults plus Playwright's Chromium-download hosts. Without these, the setup script's `npm install -g chrome-extension-tester-mcp` hangs while pulling Chromium:
```
cdn.playwright.dev
playwright.download.prss.microsoft.com
```

**Environment variables** (`.env` format, no quotes):
```
ENABLE_PASSWORD_AUTH=true
SUBTURTLE_API_URL=https://dev.dashboard.subturtle.app
SUBTURTLE_DASHBOARD_URL=https://dev.dashboard.subturtle.app
AGENT_EMAIL=<provided by user>
AGENT_PASSWORD=<provided by user>
# stubs sufficient for the build, not used by the agent flow:
MIXPANEL_PROJECT_TOKEN=dev_stub
MIXPANEL_API_HOST=https://api-js.mixpanel.com
GOOGLE_TRANSLATE_KEY=dev_stub
GOOGLE_TRANSLATE_PROXY_URL=https://translate.googleapis.com
UNINSTALL_FORM_URL=https://example.com/uninstall
GOOGLE_OAUTH_CLIENT_ID=dev_stub
```

`AGENT_EMAIL` / `AGENT_PASSWORD` are not consumed by the build β€” they exist so the agent's Bash step can reference `$AGENT_EMAIL` / `$AGENT_PASSWORD` without hardcoding into the prompt. They must match an account that exists on the dev server (the agent does not register; the human or dashboard team seeds the account).

**Setup script** (runs as root on Ubuntu 24.04, cached as a VM snapshot β€” first session ~5 min, subsequent ones reuse the cache):
```bash
#!/bin/bash
set -e

cd "${CLAUDE_PROJECT_DIR:-/workspace}"

# 1. Install extension deps.
yarn install --frozen-lockfile

# 2. Materialize .env.production from CCW env vars (dotenv-webpack's safe:true
# requires every key in .env.example to be present at build time).
cat > .env.production <<EOF
MIXPANEL_PROJECT_TOKEN=${MIXPANEL_PROJECT_TOKEN}
MIXPANEL_API_HOST=${MIXPANEL_API_HOST}
GOOGLE_TRANSLATE_KEY=${GOOGLE_TRANSLATE_KEY}
GOOGLE_TRANSLATE_PROXY_URL=${GOOGLE_TRANSLATE_PROXY_URL}
UNINSTALL_FORM_URL=${UNINSTALL_FORM_URL}
SUBTURTLE_API_URL=${SUBTURTLE_API_URL}
SUBTURTLE_DASHBOARD_URL=${SUBTURTLE_DASHBOARD_URL}
GOOGLE_OAUTH_CLIENT_ID=${GOOGLE_OAUTH_CLIENT_ID}
ENABLE_PASSWORD_AUTH=${ENABLE_PASSWORD_AUTH}
EOF

# 3. Build the extension once into dist/.
NODE_ENV=production yarn build

# 4. Install the MCP globally so Playwright Chromium is downloaded once
# into the cached snapshot. npx in .mcp.json resolves to this install.
npm install -g chrome-extension-tester-mcp@^2.1
```

### Driving the extension via the MCP

Once the environment is set up and the agent session starts, the cached snapshot already has `dist/` built and Playwright Chromium installed. The full login + screenshot loop becomes:

```
# 1. Load the unpacked extension into headless Chromium.
load_extension({ extension_path: "$PWD/dist" })

# 2. Get a JWT from the dev server with the credentials in CCW env vars.
# modular-rest's authentication.login POSTs to /user/login.
# The password MUST be base64-encoded β€” modular-rest's client library
# does this internally, but raw curl has to pre-encode. Without it
# the server returns HTTP 412 {"status":"error","e":{}}.
PW_B64=$(printf '%s' "$AGENT_PASSWORD" | base64)
curl -sX POST "$SUBTURTLE_API_URL/user/login" \
-H 'Content-Type: application/json' \
-d "{\"idType\":\"email\",\"id\":\"$AGENT_EMAIL\",\"password\":\"$PW_B64\"}"
# β†’ { "status": "success", "token": "<jwt>" }

# 3. Inject the JWT into chrome.storage.sync β€” same slot background.ts:62
# reads on every load. The extension is now "logged in".
extension_storage({ action: "set", area: "sync", data: { token: "<jwt>" } })

# 4. Open the popup and screenshot the logged-in view.
interact_with_popup({ action: "open" })
take_screenshot({ output_path: ".agent/popup.png" })
# Expected: "Logged In Successfully!" view (LoginView.vue:57-68 v-else branch).
```

The MCP exposes 14 tools β€” others worth knowing about: `inspect_dom` (eval JS in a page), `monitor_network` (capture requests during navigation), `send_message_to_background` (drive `chrome.runtime.onMessage` listeners), `get_service_worker_logs` (read background SW console output), `run_assertion` (returns structured PASS/FAIL). Full reference: the [chrome-extension-tester-mcp README](https://github.com/BHUVAN-RJ/chrome-extension-testing-mcp).

### Why password auth exists in this build

`ENABLE_PASSWORD_AUTH` gates the email + password form in [src/popup/views/LoginView.vue](src/popup/views/LoginView.vue) at build time via `dotenv-webpack`. CCW builds set it true so the agent (or a human dev) can log in by typing credentials; stable + dev release builds in [.github/workflows/release.yml](.github/workflows/release.yml) set it false so production users see only Google OAuth. The agent's direct-API path doesn't need the UI, but the UI is what makes manual testing possible.

### How auth works under the hood

The agent's direct-API path POSTs `/user/login` (via `curl`) and gets a JWT. Injecting that JWT into `chrome.storage.sync["token"]` (via the MCP's `extension_storage` tool) lands it in the *same slot* the post-OAuth `StoreUserTokenMessage` path uses β€” see [src/background.ts:62](src/background.ts) for the read side. modular-rest's client, the profile store, the translate service, and ConsoleCrane all see no difference between an OAuth-issued token and a password-issued token.

### Local dev fallback (no CCW, no MCP)

```bash
echo "ENABLE_PASSWORD_AUTH=true" >> .env.production # plus the other 8 keys
yarn build && yarn dev # webpack --watch
# Load dist/ at chrome://extensions, click the extension icon, use the form.
```

The popup form drives `authentication.login` from `@modular-rest/client`, hitting whatever `SUBTURTLE_API_URL` points at. No MCP, no Playwright β€” just the same UI a real user would see.

### Boundary

The agent path uses only the MCP. `tests/e2e/` is the testing ground for CI verify and stays untouched by agent tooling; the two never share code. If you need to add a new agent capability, route it through the MCP's tools or a new MCP β€” not through the Playwright fixture.

## Verification checklist

Expand All @@ -341,6 +464,7 @@ Automated:
- Per-host Nibble toggle persists and normalizes (`www.` strip, case fold, dedup). β†’ [tests/settings-host.test.ts](tests/settings-host.test.ts).
- ConsoleCrane on Persian / CJK / emoji inputs throws no `InvalidCharacterError` from `btoa` β€” covered at the encode level, the bridge level, and the full select-and-save flow. β†’ [tests/route-params.test.ts](tests/route-params.test.ts), [tests/e2e/nibble-flow.spec.ts](tests/e2e/nibble-flow.spec.ts), [tests/e2e/translate-flow.spec.ts](tests/e2e/translate-flow.spec.ts).
- Visual scale is consistent on default-html-fontsize and 24px-html-fontsize hosts (postcss `rem→px` rewrite regression net). → [tests/e2e/visual-scale.spec.ts](tests/e2e/visual-scale.spec.ts).
- Password login form: build-flag gating, validation, success path lands JWT in `chrome.storage.sync["token"]`, 401 surfaces inline error. β†’ [tests/login-password.test.ts](tests/login-password.test.ts), [tests/login-password-disabled.test.ts](tests/login-password-disabled.test.ts), [tests/e2e/password-login.spec.ts](tests/e2e/password-login.spec.ts).

Still manual:

Expand Down Expand Up @@ -374,3 +498,5 @@ Still manual:
- Playwright fixtures server: [tests/e2e/server.mjs](tests/e2e/server.mjs)
- Typecheck wrapper (with upstream-error filter): [scripts/typecheck.mjs](scripts/typecheck.mjs)
- Vue 3 SFC ambient declaration: [src/vue-shim.d.ts](src/vue-shim.d.ts)
- MCP server config (chrome-extension-tester for CCW): [.mcp.json](.mcp.json)
- Popup LoginView (password form + OAuth buttons): [src/popup/views/LoginView.vue](src/popup/views/LoginView.vue)
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "subturtle-extension",
"version": "1.12.0-dev.3",
"version": "1.12.0",
"private": true,
"scripts": {
"dev": "webpack --watch",
Expand Down Expand Up @@ -60,4 +60,4 @@
"vue": "3.5.17",
"vue-router": "4.5.1"
}
}
}
Loading
Loading