| Age | Commit message (Collapse) | Author |
|
Add Firefox built-in PDF Viewer support using only public WebExtensions
APIs: a selection context menu ("選択したテキストを翻訳") starts a
translation and the toolbar Action Popup shows the streaming result.
No content-script injection into the privileged PDF Viewer, no PDF.js
internals, no separate windows, no custom viewer (Specifications.md §4.6).
Specification alignment (Phase 1)
- Specifications.md: new requirements PDF-01..04 (PDF selection
translation), CTX-01..03 (context menu translation), POP-01..06
(popup two modes); SEL-06 updated to route PDF through the menus API;
section 13.4 adds a 14-item manual PDF verification checklist; section
15 removes "Firefox内蔵PDF Viewerへの対応" from out-of-scope
- Design.md: architecture diagram with the menus path, menu-translation
component, menus permission, popup translation-result state machine
(idle/starting/streaming/completed/error/cancelled), shared translation
state (§9.2), context-menu integration phase, test plan and API audit
checklist items (menus permission scope, no PDF Viewer internals)
- Security.md: menus permission restricted to selection-context
translation; PDF Viewer treated as privileged browser UI (no injection,
no wrappedJSObject/cloneInto/exportFunction, no internal DOM access);
menus.OnClickData.selectionText treated as untrusted input passing the
same validation pipeline; popup result rendering stays textContent-only
Background (Phase 2)
- translation-service.js (new): the single translation pipeline shared by
every entry point - selection validation (type/trim/empty/control
chars/max length), prompt building, Ollama request, session/abort, and
the shared translation state (requestId, sourceText, translatedText,
status, model, error, startedAt, source). "source" is metadata only; a
PDF-specific translation engine does not exist
- menu-translation.js (new): validates menus.OnClickData.selectionText
and starts a sink-less translation observed via GET_POPUP_STATE
- message-router.js: routes port translations through the shared service;
new runtime messages GET_POPUP_STATE and REGENERATE_LAST; popup-state
port handler broadcasts live state (initial push + every chunk/status
change) so an open popup shows streaming progress in real time
- index.js: registers the selection context menu (contexts: ["selection"]
only, no menus.onShown) and calls browser.action.openPopup()
synchronously inside the click user gesture, catching failures so a
popup-open problem never breaks the translation itself
- manifest.json: permissions now ["storage", "menus"]
- settings-service.js: __resetCacheForTests hook for test isolation
Popup two-mode UI (Phase 3)
- popup.html/js/css: mode nav (設定 / 翻訳). Translation mode shows
status, model, streaming text (textContent only), user-safe error, and
copy (Clipboard API only, no execCommand fallback), regenerate (new
requestId reusing the stored source text) and a settings shortcut.
Closing the popup never cancels the request; reopening shows the
latest text and continues receiving chunks live. Initial mode: active
(starting/streaming) translation wins, otherwise the last tab the user
picked is restored (storage.local.popupMode, default settings)
Fresh-install model auto-selection
- translation-service.js ensureModelConfigured(): when storage.model is
empty, pick the first model from the running Ollama and persist it
before translating, mirroring the popup/options auto-select. A user who
never opened any settings UI can translate immediately; when Ollama is
unreachable a clear OLLAMA_UNAVAILABLE message is shown
User-safe error details
- detailForUser(): strips URLs (no internal host names echoed to the UI),
caps length at 120 chars, drops full JSON response bodies, and hides
meaningless low-level network errors so the popover/popup shows the
base message plus at most a short, safe detail
Tests: 85 passed, 0 failed
- selectionText validation (untrusted input pipeline)
- menu selection -> translation -> popup state streaming/completed
- empty selection rejected without starting a request
- streaming status transitions observed through GET_POPUP_STATE
- Ollama unreachable surfaces a user-safe error into popup state
- newer request replaces older state (abort semantics)
- regenerateLast reuses source text with a fresh requestId
- popup-state port receives live broadcasts; wrong port names ignored
- fresh-install auto-select persists the first Ollama model
- error detail sanitization (URL stripping, length cap, JSON drop)
- popup/options smoke tests updated for the two-mode markup
- setupRouter resets the shared settings cache for test isolation
Docs: README documents the two-mode popup and PDF flow; Testing-Results
records automated results and the manual PDF checklist (to be executed).
No git-side surprises: this commit was created explicitly on request
(requirement 18 of the PDF task had deferred commits until now).
|
|
The advanced settings labels (num_keep, num_predict, top_k, ...) had
variable widths, so each numeric input started at a different x
position. Give the labels a fixed 150px width in the #advanced-fields
rows so all inputs line up in a single column.
Verified in a live DOM: after the rule applies, all 10 labels share the
same width (150px) and all 10 inputs start at the same x position.
Results: 73 passed, 0 failed. dist rebuilt.
|
|
The selection action icon appeared as a thin vertical bar on real
Firefox although the same markup rendered correctly in Chromium-based
test environments. The previous implementation wrapped the SVG in a
<button> with display:flex + align-items/justify-content center and
sized the SVG only through CSS (.btn svg { width/height }). Firefox
collapses this combination inside a shadow root, leaving just the
button border visible as a bar.
Hardened implementation:
- the SVG itself is now the clickable element (role=button, tabindex=0)
with explicit width="28" height="28" ATTRIBUTES, not CSS-only sizing
- explicit viewBox + white circle background (with border stroke) drawn
as SVG primitives so the glyph reads on dark pages and no button
styling quirks can collapse it
- :host gets display:block and .wrap gets explicit 28x28 dimensions
- removed the <button> wrapper and flex centering entirely
Visual verification: rendered the new icon in a live page and confirmed
a 32x32 circular icon with the translate glyph. Results: 73 passed,
0 failed. dist rebuilt.
|
|
Root cause of "設定値が不正です (モデルが設定されていません)" despite the
settings UI showing a selected model:
- fillModels(models, selected="") never set option.selected when no model
was saved yet. The browser then auto-selects the first option visually,
so the UI displayed e.g. "glm-5.3:cloud" while storage.model remained
empty. Translation therefore failed with the no-model error until the
user pressed Save, which materialized the UI value into storage.
Fixes
- popup.js/options.js fillModels: when no model is saved and the list is
non-empty, explicitly select the first model so the UI value always
matches what will be used
- popup.js: after the model list loads, persist the auto-selected model
if storage still has none
- options.js loadSettings: same sync after loadModels, so opening the
options page also heals the empty-model state
Results: 73 passed, 0 failed. dist rebuilt.
|
|
Translation error ("設定値が不正です") diagnosis
- The INVALID_SETTINGS code collapsed two unrelated failures into one
opaque message: (a) no model configured in the background settings
cache, and (b) HTTP 400/403 from Ollama with the actual reason in the
response body. Verified against a live Ollama server: the configured
cloud models (glm-5.3:cloud etc.) stream fine from a plain page, so
the generic message gave no clue which path fired.
- ollama-client.js: read the Ollama error body on non-OK responses and
append its message ("model is required" etc.) to the thrown error
- message-router.js: no-model-configured now has its own explicit
message ("モデルが設定されていません...") and concrete detail reasons
are appended to the base message in the popover, e.g.
設定値が不正です。... (Ollama /api/chat failed: 400 - ...)
Action icon disappearing
- The scroll handler hid the action button whenever the popover was
hidden, even with a live selection, leaving no way to translate until
the user re-selected. It now repositions the icon while a selection
exists and only hides it when the selection is gone (Design 5.4).
Results: 73 passed, 0 failed. dist rebuilt.
|
|
Root cause of the broken options page (audit finding):
- options.js imported CONTEXT_PRESETS from settings-schema.js, but that
module never exported it. A named-import mismatch in ES modules is a
SyntaxError, so the entire options page script silently never executed.
This is why the settings page rendered its static HTML but was dead:
no language options, no model list, no save handler.
Fixes
- options.js: import CONTEXT_PRESETS from shared/constants.js (where it
is defined) instead of settings-schema.js
- options.js: save flow now merges the validated form over the stored
settings (sanitize again) so fields saved by the popup in the meantime
are preserved instead of being dropped
- popup.js: populate language options inside loadSettings and sanitize
settings before every save, matching the options page behavior
- settings-schema.js: add normalizeOllamaHost() and use it in
sanitizeSettings so trailing-slash variants are stored identically
- content.js: consolidate popover state - remove the duplicate outer
lastSourceText and keep regenerate source inside the popover module;
regenerate now calls portClient.translate directly
Test hardening (would have caught the original bug)
- tests/ui-smoke-tests.js: new smoke tests that install a fake browser
API, run the real popup.js / options.js page scripts as ES modules
against representative markup, and assert that language options are
populated, the model select is filled from the mocked model list, and
the stored host is reflected. The runner result elements are preserved
across the sandboxed DOM swap
- test.html: include the smoke test module
Results: 73 passed, 0 failed (71 existing + 2 smoke). dist rebuilt.
|
|
version)
Implement the complete initial version of a Firefox WebExtension (Manifest V3)
that translates selected text on web pages using a local Ollama LLM and shows
the result in a small popover near the selection.
Specification alignment (Phase 0)
- Unified Specifications.md / Design.md / Security.md on the same approach:
Ollama REST API + Fetch API + NDJSON streaming (no ollama-js SDK dependency)
- Fixed host_permissions to http://localhost/* and http://127.0.0.1/* because
Firefox match patterns do not support port numbers (bug 1362809/1468162);
the actual connection target is restricted to localhost/127.0.0.1 + any port
by application-side validation (defense in depth, OLL-09)
- Clipboard: use navigator.clipboard.writeText() only inside the copy button
click (user activation); no deprecated execCommand fallback
Implementation (zero dependencies, vanilla JS, no npm/Node required)
- manifest.json: MV3, background.scripts + type:module (event page, no
service_worker), action popup, options_ui, content_scripts for http/https,
permissions: storage + localhost host permissions only
- src/shared: constants (ports, limits, presets, error codes), message
validation, settings schema (defaults / merge / sanitize / buildOptions /
toApiThinking), system prompt builder (fixed prompt first + user prompt)
- src/background: message-router (runtime.Port streaming + runtime.onMessage
for model list), translation-session (single active request, abort previous
on new request, stale chunk suppression), ollama-client (fetch against
/api/tags and /api/chat with buffering NDJSON line parser tolerant to chunk
boundaries inside JSON objects, AbortController, host re-validation before
every request, HTTP status to error-code mapping), settings-service
(storage.local load/save with validation and fallback to defaults)
- src/content/content.js: single classic script (Firefox does not support ES
modules in content scripts) with selection detection (selectionchange +
mouseup + keyup, debounced), Shadow DOM action button, Shadow DOM popover
with HIDDEN/LOADING/STREAMING/COMPLETED/ERROR state machine, port client
with requestId-based stale chunk filtering, enabled-state sync via
storage.onChanged; all dynamic text rendered via textContent only
- src/popup + src/options: full settings UI (enable toggle, Ollama host,
model list from Ollama with saved-model warning, target language, thinking
mode, temperature slider 0.0-2.0 step 0.1, context length presets
4k/8k/16k/32k/128k/256k, seed random/fixed with integer value, user system
prompt, advanced numeric options with steppers, stop sequences textarea)
- icons generated by tools/make-icons.ps1 (System.Drawing, no external tools)
- build.ps1 packages manifest.json + icons/ + src/ into dist zip, excluding
tests/, doc/, ollama-js/ and tools/
Tests (71 passed, 0 failed)
- In-browser test runner (tests/test.html) with custom describe/it/assert
framework; no npm, no Node.js
- Unit: constants sync between content mirror and shared module, Ollama host
validation (localhost/127.0.0.1 + any port only; rejects external hosts,
LAN IPs, https, credentials, paths), settings schema defaults and fallback,
temperature 0.1-step validation, context length presets, seed modes,
advanced option types, buildOptions omission of unset values and absence of
tfs_z/mirostat fields, system prompt composition order, message validation
with selection length boundary values
- Integration: NDJSON streaming across awkward byte boundaries, thinking
chunk forwarding, malformed line tolerance, error payload mapping, think/
options inclusion rules, model list, TranslationSession abort semantics,
settings round-trip with storage.onChanged cache refresh, full port flow
(started/chunk/completed), newer request aborts older with stale chunk
discard, malformed message rejection, OLLAMA_UNAVAILABLE and
INVALID_SETTINGS error paths, untrusted sender rejection
Security audit (Design.md section 24 checklist: all pass)
- No innerHTML/outerHTML/document.write/eval/new Function/localStorage
- No remote JS/CSS/fonts/CDN resources; no inline scripts in extension pages
- Ollama access limited to background; content scripts never fetch Ollama
- Background validates message type/length/requestId; single concurrent
translation enforced
- Clipboard API only (no execCommand fallback)
- Minimal permissions: storage + localhost host permissions
Docs
- README.md with build/load/usage instructions, API list, permission rationale
- doc/Testing-Results.md with automated test results, security audit results,
and a 14-item manual Firefox verification checklist (to be executed on a
real Firefox + Ollama environment)
|