Commit graph

168 commits

Author SHA1 Message Date
Yun Chan
3a46437f28 fix(sync,rag): keep restored rows, reconcile after tombstone pruning, reject partial knowledge docs, surface RAG indexing failures 2026-09-28 02:16:14 +09:00
Yun Chan
f4f9653361 fix(conversation): make realtime sessions cancellable and recoverable 2026-09-28 00:54:03 +09:00
Yun Chan
a85b24d385 fix(meeting): persist transcript segment edits, restore live recording view, and route settings deep links by tab id 2026-09-28 00:54:03 +09:00
Yun Chan
a8983c583a fix(history): require confirmation before clearing all history 2026-09-28 00:54:02 +09:00
Yun Chan
819bc9d789 fix(support): stop faking refund approvals and ticket submissions in the support modal 2026-09-28 00:54:02 +09:00
Yun Chan
e243e5282c fix(mobile-history): drop history cache writes that outlive the auth scope 2026-09-28 00:54:01 +09:00
Yun Chan
2576f4af7b fix(mobile-queue): arm retries at the earliest deadline and stop losing or replaying queued audio 2026-09-28 00:54:01 +09:00
Yun Chan
ace946d5c7 fix(mobile-recorder): scope recorder control to its owner and keep Android recordings on stop failure 2026-09-28 00:54:01 +09:00
Yun Chan
06e96c58f4 fix(sound): apply the Sound switch at once instead of after a restart 2026-09-28 00:53:47 +09:00
Yun Chan
91d4b4974c fix(tts): pass Windows TTS text via env and let stop() only cancel its own playback 2026-09-28 00:53:46 +09:00
Yun Chan
9cd81b48c1 refactor(keybinding): extract chord state machine into core and fix AltGr guard 2026-09-28 00:53:46 +09:00
Yun Chan
de1e8a82a4 fix(dictionary): let an edit clear the pronunciation and report duplicate renames 2026-09-28 00:53:46 +09:00
Yun Chan
9d5d043e8b fix(license): keep signed offline keys across restarts and cloud sign-out 2026-09-28 00:53:45 +09:00
Yun Chan
95aa95e986 fix(memo): sanitize the tag used in memo export file names 2026-09-28 00:53:45 +09:00
Yun Chan
524d390bc2 fix(runtime): stage runtime installs strictly, time out only on stalls, and make reinstall actually reinstall 2026-09-28 00:53:44 +09:00
Yun Chan
ba0dbbd813 refactor(instructions): share instruction template rendering via @d3ro/core 2026-09-28 00:53:44 +09:00
Yun Chan
d311e8123f fix(llm): keep system prompts on premium chat, reject incomplete streams, stop double-charging quota 2026-09-28 00:53:44 +09:00
Yun Chan
d96601a283 fix(file-transcription): provision ffmpeg before converting so clean installs stop failing with ENOENT 2026-09-28 00:53:43 +09:00
Yun Chan
c5b3bdedb6 fix(voice): let a click on the RecordingTip actually cancel the recording 2026-09-28 00:53:43 +09:00
Yun Chan
83cf9133ac refactor(desktop): session-scoped voice runtime, STT/LLM ports, caption ownership and meeting export fixes 2026-09-28 00:53:43 +09:00
Yun Chan
0adedf5e7b fix(text-insert): restore every clipboard format after paste and never wipe non-text clipboards 2026-09-28 00:53:42 +09:00
Yun Chan
190b6db284 refactor(suggestion): drive suggestion decisions from one core step and split SuggestionService collaborators 2026-09-28 00:53:42 +09:00
Yun Chan
9aa7302944 fix(sync): bind sync engine to its user DB, scope instructions/templates per account, harden navigation 2026-09-28 00:53:42 +09:00
Yun Chan
1b8fe445f3 fix(suggestion): stop generating mid-typing and let a pause bring a suggestion at once
With a 100 ms trigger left in the installed config, generation started in the
gaps between keystrokes; the next key ended the session silently, and that
session still counted against the 5 s minimum interval, so the moment the
user actually paused nothing came (rate-limited). The log showed a
generate-then-vanish cycle every 5-6 s.

The trigger delay now has a 500 ms floor and settings revision 6 resets a
stored value below it to 600 ms. A session ended because the user kept typing
no longer blocks the next request by the minimum interval (per-minute and
daily caps still apply), and that dismissal is logged.
2026-09-27 22:55:26 +09:00
Yun Chan
7e0c20b510 fix(suggestion): keep a shown suggestion while the user picks it, and suggest in terminals
A candidate list was cleared about two seconds after it appeared: once typing
paused, the not-typing rule dismissed the visible overlay, so a click or
Ctrl+Alt+Enter found nothing to insert. The rule now only stops new
requests; a visible overlay stays until it is accepted, dismissed or goes
stale.

Terminals were excluded from suggestions because the whole screen buffer
reads as the input and the last line is usually a status bar. The input line
is now extracted (Claude Code/Codex >, starship, PowerShell, cmd and POSIX
prompts, wrapped continuation lines) and used as the prefix; no prompt means
no suggestion. Terminals stay out of phrase learning. Accepting with nothing
shown and successful inserts are now logged.
2026-09-27 22:43:22 +09:00
Yun Chan
796916ba92 release: ship v1.9.0 with transcript segments and preset prompts on the phone
Some checks failed
ci / 정본·보안·린트·타입·테스트 (push) Successful in 53s
ci / 모바일 린트·타입·Jest (push) Successful in 45s
ci / Supabase Edge Functions + Cloudflare Worker (push) Successful in 24s
ci / .NET API 서버 테스트 (push) Successful in 17s
deploy-site / deploy (push) Successful in 53s
ci / 워크스페이스 빌드 검증 (push) Successful in 35s
release / release-windows (push) Failing after 9m17s
portable-unsigned / portable-windows (push) Failing after 28m49s
Desktop transcript edits, auto-polish and speaker labels now reach the phone,
which draws meetings from transcript segments, and prompt edits of the four
shared preset commands are used by the phone's commands.

Bumps the product version to 1.9.0 (Android/iOS build 1090000).
2026-09-27 16:24:48 +09:00
Yun Chan
2aac10fc5d feat(desktop): send meeting transcript segments and preset prompt edits to the phone
The phone draws a meeting from its transcript segments before the edited
transcript, so desktop edits, auto-polish and diarization never showed there.
Every desktop transcript change now rebuilds the meeting's segments from its
[MM:SS] [speaker] lines and trims the rest; the line parser moves to
@d3ro/core/meeting-transcript and the meeting view uses it too.

Prompt edits of the four desktop presets that exist on the phone update the
server preset row (a reset restores its default; {{targetLanguage}} is sent
as English, the only target on both sides), and edits made on another desktop
come back. The free-prompt preset has no phone counterpart and stays local.
2026-09-27 16:24:25 +09:00
Yun Chan
3f1fb8eed5 release: ship v1.8.0 with knowledge, recording and settings sync
Some checks failed
ci / 정본·보안·린트·타입·테스트 (push) Successful in 54s
ci / 모바일 린트·타입·Jest (push) Successful in 42s
ci / Supabase Edge Functions + Cloudflare Worker (push) Successful in 22s
ci / .NET API 서버 테스트 (push) Successful in 17s
deploy-site / deploy (push) Successful in 35s
ci / 워크스페이스 빌드 검증 (push) Successful in 37s
release / release-windows (push) Failing after 6m8s
portable-unsigned / portable-windows (push) Failing after 14m8s
Knowledge documents move between desktop, phone and web as source text and
are indexed on each device; desktop recordings upload to the shared storage
and any recording plays from the history card; language, theme, auto-polish
and the active command follow the phone. Document text survives a missing
embedding model so it can be indexed later.

Bumps the product version to 1.8.0 (Android/iOS build 1080000).
2026-09-27 14:45:19 +09:00
Yun Chan
9a8f7e6aa6 feat(desktop): sync knowledge, recordings and shared settings; play any recording
Knowledge documents travel as source-text chunks; each surface embeds them
with its own model, the server index is requested through embed-chunks, and
documents from the phone are stored without a file and indexed from their
chunks. Chunk text is now kept when local embedding fails, so reindexing no
longer needs the original file.

Recordings upload to the mobile storage contract (audio bucket under the
user's folder plus an audio_files row, 50 MiB cap, a Settings > Cloud
toggle) and are removed with their record. The history card gains a play
button that uses the local file or, for phone recordings, a signed URL.

Language (ko/en), system/light/dark theme, auto-polish and the active user
command follow the phone's user_settings with its revision rule; changes that
arrive from the phone reach the open window.
2026-09-27 14:44:56 +09:00
Yun Chan
3d9faedf7d release: ship v1.7.0 with two-way cloud sync and streaming captions
Some checks failed
ci / 정본·보안·린트·타입·테스트 (push) Successful in 52s
ci / 모바일 린트·타입·Jest (push) Successful in 41s
ci / Supabase Edge Functions + Cloudflare Worker (push) Successful in 24s
ci / .NET API 서버 테스트 (push) Successful in 11s
deploy-site / deploy (push) Successful in 44s
ci / 워크스페이스 빌드 검증 (push) Successful in 35s
release / release-windows (push) Failing after 6m37s
portable-unsigned / portable-windows (push) Failing after 21m42s
Signing in now keeps history, dictionary, meetings with memos and documents,
memo tags, user commands and templates the same on desktop, phone and web,
with offline changes queued, deletions shared and phone edits arriving live.
The desktop registers in the phone's device list and signs out when
disconnected there; local-mode records move into the first account.

Live captions stream about a second behind speech, finish lines on pauses,
are polished in context by the local model and can use their own speech
model (the speech engine is downloaded again once, minimum 1.7.0).

Fixes suggestion paste on Ctrl+Alt+Enter, click acceptance, typing detection
in growing chat boxes, one-way cloud sync and failing account exports.

Bumps the product version to 1.7.0 (Android/iOS build 1070000).
2026-09-27 14:08:18 +09:00
Yun Chan
53fcdf6305 fix(mobile): list desktop and web devices instead of failing the screen
normalizeRegisteredDevice accepted only android/ios, so one desktop row made
the whole Devices screen fail. Accept the server platform list and show the
platform on each card.
2026-09-27 14:04:49 +09:00
Yun Chan
0a4f5aee64 feat(desktop): two-way cloud sync with mobile and web
Rewrites the desktop mirror as services/sync/SyncEngine: a persistent
outbox, per-account server-clock keyset cursors with paging, pulls that never
overwrite unsent local edits, deletions both ways through sync_tombstones and
per-row failure isolation. It now covers history titles and favorites,
dictionary, every meeting's memos and documents, memo tags, user commands and
dictation/meeting templates, and registers the desktop as a device that the
phone can disconnect.

Fixes shipped defects: the first pull after sign-in fetched nothing, only
the first meeting's children were pushed, team meetings leaked into the
personal database and lost team_id on re-push, and Realtime never connected
because Electron's Node 20 has no global WebSocket (ws is now the transport).
Anonymous local-mode records are imported into the first account that signs
in. The settings sync section is translated and shows pending/rejected
changes; synced screens reload on app:dataChanged.
2026-09-27 14:04:49 +09:00
Yun Chan
0d92a4a853 fix(rag): do not mark a document indexed when no chunk could be embedded
Some checks failed
ci / 정본·보안·린트·타입·테스트 (push) Successful in 51s
ci / 모바일 린트·타입·Jest (push) Successful in 37s
ci / Supabase Edge Functions + Cloudflare Worker (push) Successful in 26s
ci / .NET API 서버 테스트 (push) Successful in 14s
deploy-site / deploy (push) Failing after 15s
ci / 워크스페이스 빌드 검증 (push) Failing after 11m7s
With the embedding server unavailable every chunk failed, yet the document
was stored as indexed=true with 0 chunks, so the knowledge base listed it as
searchable while queries could never match it. The red use-case test caught
this; it had been written off as an environment failure.

Now a run with zero embedded chunks leaves indexed=false and throws
RAGEmbeddingFailed (surfaced by reindex, logged by addDocument).

Tests that only hold on the Windows developer machine now declare it: the
bundled SoX binary and PowerShell device discovery run on win32 only, and
the sidecar venv test runs only when sidecar/.venv exists. The Linux Forgejo
runner skips them instead of failing.
2026-09-26 21:10:40 +09:00
Yun Chan
eedd127ea7 refactor(billing): remove Stripe; payments are Payple (web) and Google Play (mobile)
Some checks failed
ci / 정본·보안·린트·타입·테스트 (push) Failing after 1m13s
ci / 워크스페이스 빌드 검증 (push) Has been skipped
ci / 모바일 린트·타입·Jest (push) Failing after 1m4s
ci / Supabase Edge Functions + Cloudflare Worker (push) Successful in 37s
ci / .NET API 서버 테스트 (push) Successful in 27s
deploy-site / deploy (push) Failing after 20s
Stripe is not used. Keeping its checkout, portal and webhook paths meant a
second payment provider, a second return-URL format and dead UI.

- Delete the stripe-checkout, stripe-portal and stripe-webhook functions and
  their config; billing-catalog serves Payple prices only, and the web parser
  rejects a catalog that still mixes in Stripe prices.
- Web: drop the Stripe checkout/portal buttons, provider toggle and return
  notices; billing shows Payple only. Past rows with provider='stripe' are
  still displayed ("Stripe (종료)") with a support contact instead of a portal.
- Desktop: delete the Stripe checkout modal, payment IPC channels, preload
  namespace and their types; "Remove ads with Pro" opens the web billing page
  via license.openBilling. Support/refund copy names Payple.
- billingUrl() loses the Stripe-only success/canceled result option; the
  Deno contract is regenerated.
- Migrations and the DB's accepted provider values are untouched (history).
- Docs and the backlog record the removal (MON-04, EXT-STRIPE-01, GAP-BILL-03).

Verified: typecheck (desktop/web/admin/api-client/mobile), contract:check,
deno check all functions, deno test 80/80, desktop 1478/1480 on the Electron
runtime (2 known environment failures), web and admin builds, release
metadata and mobile boundary self-tests, eslint on changed files.
2026-09-26 20:56:18 +09:00
Yun Chan
cd9d199dbf chore: remove duplicate copies of the site, download center and installers (WS-C)
The landing page, download pages, invite page, assetlinks and installers
existed in two or three places; only site/ and the Forgejo feed are served.

- apps/api-server/wwwroot: delete the stale site build, download/invite
  pages, .well-known copy, legacy static admin and 1.0.0 binaries. The API
  no longer serves static files (UseStaticFiles/fallbacks and the apk/zip
  blocker removed); the Next admin is the only admin UI.
- Delete 19 tracked installers/packages (~568 MiB) under site/public/releases,
  apps/web/public/releases and wwwroot/releases; .gitignore blocks them.
- apps/web: delete the download/releases pages, desktop-release.ts, the
  download.html and assetlinks copies, and the accept-invite page (invites
  are only issued to the site's /accept-invite/). e2e specs call the /app
  base path and check the /download redirect instead.
- scripts: delete the retired release/NAS site scripts, drop the web target
  from sync-version, and check assetlinks in site/public only.
- Delete the unused Dockerfile.admin (apps/admin/Dockerfile is used).

Policy: docs/REFACTOR_POLICY.md Wave 3, W3-5 and W3-6.
2026-09-26 15:48:52 +09:00
Yun Chan
b6fe588a7c feat(web): serve the web app under /app and send every billing link there (WS-B)
apps/web was never deployed, so /billing on the public domain returned the
landing page and d3ro.dev (desktop "upgrade") did not resolve.

- apps/web runs with basePath /app and output standalone; /download and
  /releases redirect to the site's #download. A Dockerfile and a d3ro-web
  compose service (port 3002) deploy it to the NAS with the other images.
- The site bridge worker forwards /app/* to WEB_APP_ORIGIN (the tunnel host)
  and rewrites upstream redirects; everything else still goes to Pages.
  With no origin configured /app answers 503 instead of the landing page.
- Desktop upgrade, desktop Stripe return, mobile subscription management,
  the web checkout/portal returns and the site all use billingUrl(); the
  return query is success=1 / canceled=1, which the billing page reads.
  The billing page highlights ?tier=pro|pro_plus, and signing in from a
  billing link returns to the same plan.
- auth/callback pins the redirect origin in production and rejects
  protocol-relative next= values (open redirect).
- Mobile legal links use SITE_URLS (fixes the missing slash on /terms).
- Compose drops the unused NEXT_PUBLIC_API_URL and the dead wwwroot legal
  mounts; deploy scripts add the web image and the SUPABASE_* values the NAS
  compose already required; .dockerignore keeps app .env files out of images.
- Supabase auth redirects allow /app/** (remote dashboard must match).

Policy: docs/REFACTOR_POLICY.md Wave 3, W3-3 and W3-4.
2026-09-26 15:48:30 +09:00
Yun Chan
88f24d84a1 refactor(core): keep plan prices, cloud quotas and public URLs in one contract (WS-A)
Prices, quotas and site URLs were copied by hand into the edge functions,
admin, desktop and the landing site, and the copies disagreed (Payple billed
9,900/29,900 KRW, admin labels said 12,900/24,900 KRW and $9.9/$19.9, the
site said 2,900/8,900 KRW).

- packages/core/src/plan-catalog.ts is the single source for PLAN_PRICE_KRW
  (Free 0 / Pro 2,900 / Pro+ 8,900 a month) and PLAN_QUOTA.
- packages/core/src/web-urls.ts is the single source for the public origin,
  the /app web-app base path, SITE_URLS and billingUrl().
- Deno cannot bundle packages/core, so scripts/ci/sync-core-contract.mjs
  generates _shared/core-contract.generated.ts; `npm run contract:check`
  fails on drift (same pattern as version:sync).
- Payple checkout, renewal and webhook amount checks now bill the catalog
  price, so existing subscribers move to the new price at their next renewal.
  quota.ts, team-contract.ts and the tests read the generated values.
- Admin MRR/ARR is computed in KRW from the catalog; license labels, the
  release link and desktop PREMIUM_LLM limits derive from core; the site
  imports prices and quotas directly.

Policy: docs/REFACTOR_POLICY.md Wave 3, W3-1 and W3-2.
2026-09-26 15:48:18 +09:00
Yun Chan
ad6bb70c20 fix(settings): show the live caption model under the dictation model
The caption model selector now sits directly under the dictation model in
the STT tab's Local Whisper card, where people look for it, and stays in the
General tab's caption section as well; both use one shared component. The
first selector is renamed to 'Dictation model' so the two are told apart.
2026-09-24 22:08:05 +09:00
Yun Chan
da0da98285 fix(suggestion): paste after the shortcut keys are released and keep typing detection stable
Ctrl+Alt+Enter pasted while Ctrl+Alt were still down, so the target app got
Ctrl+Alt+V; accepting now closes the panel and waits for the modifiers to be
released. Candidates are accepted on pointer press because the list is
redrawn as new candidates stream in, which swallowed clicks.

The typing gate identified the focused field by its bounds, so chat boxes
that grow while typing looked like a new field on every keystroke and were
reported as "not typing". Fields are now keyed by window, control type and
name, and a mouse click re-baselines the text instead. The decision log
includes both gate values.

The live-caption model selector moves to the caption section of the General
tab, next to the other caption settings.
2026-09-24 22:01:10 +09:00
Yun Chan
4b0f685941 feat(caption): let live captions use their own speech model
The speech engine now keeps an auxiliary model next to the dictation model
and transcribes with whichever the request names, reloading it once if the
engine restarted. Settings > STT gains a live-caption model so captions can
run on large-v3-turbo while dictation keeps its own model. The runtime
minimum rises to 1.7.0 because older engines would silently ignore the
model choice.

Suggestion paging moves to Up/Down: the page follows the selection and the
last item waits while more candidates are being generated. The Left/Right
page shortcuts are removed; they did nothing until a page had filled and
clash with Intel's display-rotation hotkeys.
2026-09-24 21:46:54 +09:00
Yun Chan
39b8e7448e feat(caption): stream live captions and polish finished lines in context
Replaces the fixed six-second batches with a streaming track per audio
source: the uncommitted audio is re-recognised every second and sent as a
partial with its agreed (stable) prefix, a short pause finalises the line,
and long unbroken speech is committed at Whisper segment boundaries. Idle
audio is trimmed so silence cannot produce invented sentences.

Finished lines are corrected by the local model against the previous lines
and replaced in place; edits that change too much are rejected. The
behaviour can be switched off in Settings.
2026-09-24 20:22:21 +09:00
Yun Chan
2fe20fa7b5 release: ship v1.6.0 with paged suggestions and a cleaner phrase memory
Some checks failed
deploy-site / deploy (push) Failing after 39s
release / release-windows (push) Failing after 3m41s
portable-unsigned / portable-windows (push) Failing after 12m23s
Next-sentence suggestions now arrive one at a time up to twelve, shown three
per page with Ctrl+Alt+Up/Down to move, Left/Right to page, Enter to accept
and Esc to close; old default bindings migrate and the panel guide follows the
live bindings. The overlay is redesigned, stays put while candidates stream
and sits outside the input box when no caret is reported.

The personal phrase memory stops learning from terminals, code editors and
the coding-agent hub, ignores symbol-heavy lines and empty-field placeholders,
and prunes existing entries that break those rules.

Fixes suggestion keys starting dictation, installs stuck on a pre-1.5.0
speech engine without the focus endpoint, Ollama runner windows flashing
while typing, the speech engine starting twice, and cold-model timeouts.
Live captions can be dragged to a remembered position and show a waiting
notice until the first line arrives.

Bumps the product version to 1.6.0 (Android/iOS build 1060000).
2026-09-24 19:56:28 +09:00
Yun Chan
5c11ee2fde release: ship v1.5.0 with on-device writing suggestions
Some checks failed
deploy-site / deploy (push) Failing after 33s
portable-unsigned / portable-windows (push) Failing after 4m7s
release / release-windows (push) Failing after 3m16s
Adds next-sentence suggestions while typing, weekly input insights and a
personal phrase memory to the desktop app, and fixes custom instructions so
they process the text instead of inserting the instruction's own wording.
Local model requests are now bounded and individually cancellable.

Bumps the product version to 1.5.0 (Android/iOS build 1050000), refreshes the
landing and web download links, and records the new INPUT feature rows and the
open verification gaps in the infrastructure map.
2026-09-23 16:04:27 +09:00
Yun Chan
99f06c253c fix(llm): stop leaking instruction prompts instead of processed text
Running a custom instruction (translate, summarise, rewrite, explain code,
free prompt) inserted the instruction's own wording instead of the result.
Two faults stacked:

The instruction was passed as the text to process, leaving the system-prompt
argument empty. `BASE_SYSTEM_PROMPTS` has no `custom` key, so resolution fell
back to `refine` without saying so, and the model dutifully polished the
instruction it had been handed. The transcript never reached it.

And only `{{text}}` was substituted, which none of the five built-in
instructions use — they carry `{{targetLanguage}}`, `{{userPrompt}}`, or no
placeholder at all. The substitution was a no-op from the day it was written:
the presets landed ten hours before the code that expected them.

- Instruction prompts now go to the system-prompt argument and the transcript
  to the text argument. Instructions that spell out `{{text}}` keep their old
  meaning, so hand-written ones still work.
- `renderInstructionPrompt` resolves `{{text}}`, `{{userPrompt}}` and
  `{{targetLanguage}}` in one place, and warns by name when a placeholder is
  left standing rather than letting it reach the model.
- `resolveSystemPrompt` no longer drops silently to `refine` for `custom`.
- Voice shortcuts no longer die at the `defaultLLMAction === 'none'` gate; an
  explicitly named instruction outranks the default. Without one, `none` still
  passes the transcript through untouched.
- `translate` receives its target language instead of relying on a default two
  call frames away. It is still always English — `AppConfig` has no key for it,
  and neither `language` (UI locale) nor `sttLanguage` (source language) can
  stand in. Choosing a target language needs a setting and is not in this fix.
- Chains ran instructions with placeholders intact; they share the same
  resolution now.
- The command screen's pipeline bench called `llm.generate`, which preload does
  not expose, so every run threw and the catch showed the input back as if it
  had succeeded. It uses `llm.process` now, over the same path production
  takes, and a failure reads as a failure.

Present since the feature shipped: the custom-instruction path has never
worked. Plain actions (refine, summarise, grammar, expand) were unaffected and
are now covered by tests so they stay that way.
2026-09-21 14:39:26 +09:00
Yun Chan
30d51c952f chore(release): v1.4.0
Shortcuts gain several bindings per action, mouse buttons and a searchable
picker, and the stored shortcut schema changes shape, so this is a minor bump.
Existing shortcuts migrate on first launch.

Play release notes state that this release carries no mobile feature changes;
the work is desktop-only.
2026-09-21 13:41:59 +09:00
Yun Chan
4ad1ae6ed4 feat(keybinding): several shortcuts per action, mouse buttons, searchable picker
Shortcuts were defined in four places that drifted apart: per-action IPC channel
pairs, a hand-written VK table in the service, a second one in the renderer, and
three copies of the keycap styling. Adding an action meant editing all of them,
so two shortcuts stayed hardcoded in bootstrap and one had no settings entry at
all.

packages/core/src/keybinding.ts is now the single source for the binding type,
the selectable key catalog, the action catalog, normalization, validation,
conflict detection, display labels, search and deserialization. Main, preload
and renderer all read from it; nothing redefines keys or rules locally.

- Each action holds a list of bindings instead of one. AppConfig's four
  *Shortcut fields collapse into a single keyBindings map, migrated on launch.
- Mouse buttons can be bound. Left click is refused, right/middle need a
  modifier, side buttons are free. uiohook cannot swallow events, so the
  original click still fires and the UI says so.
- Keys can be picked from a grouped dropdown with a search box, not only by
  recording a keypress.
- HOTKEY's 14 channels become KEYBINDING's 9, taking the action as a parameter,
  so actions no longer multiply channels. The history and command popups moved
  out of bootstrap into ordinary actions.
- displayLabel is gone; labels derive from the binding and follow the app
  language and platform.

Fixes found on the way:
- Double-press hands-free was unreachable: lookup returned only the first
  matching action, and dictation shares its default binding.
- Reserved-combination checks compared joined key names, so a different modifier
  order let Ctrl+C through.
- Disabling shortcuts released every global registration in the process,
  including the popup ones, and never restored them.
- Enabling shortcuts after starting disabled left nothing registered.
- The dashboard stored the caption event payload instead of the state in it.
2026-09-21 13:41:47 +09:00
Yun Chan
ee1deb64cf fix(release): point the download centers at the published installer
Some checks failed
deploy-site / deploy (push) Failing after 52s
The download contract on both the web console and the landing site still
advertised 1.2.0, so the install button pointed at
D3RO-Voice-Setup-1.2.0-x64.exe, which the feed answers with 404 while 1.3.7
is live. Wire both contract files into version:sync so the installer version
and release date follow the version SSOT, and set them to the published 1.3.7.

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4.1-flash
2026-09-19 08:41:37 +09:00
Yun Chan
05f0aaa660 chore(release): 1.3.7
Some checks failed
release / release-windows (push) Failing after 6m51s
portable-unsigned / portable-windows (push) Failing after 16m51s
💘 Generated with Crush

Assisted-by: Crush:deepseek-v4.1-flash
2026-09-19 08:24:13 +09:00
Yun Chan
ae7efb6acf fix(desktop): render popup overlays in packaged builds
Popup pages loaded their scripts as classic <script src> tags, which the
renderer build never bundles, so an installed app rendered only the static
markup: the recording tip stayed at 0:00 with no wave bars and live captions
showed nothing.

- declare popup scripts as modules so the build emits them, and fail
  packaging when a renderer page references an asset that was never produced
- hold popup IPC until the renderer has loaded and re-assert visibility on
  every show, so a popup hidden once still appears next time
- surface popup renderer console and load failures in the main log

💘 Generated with Crush

Assisted-by: Crush:deepseek-v4.1-flash
2026-09-19 08:24:03 +09:00
Yun Chan
6766cb8c8e chore(release): 1.3.6
Some checks failed
deploy-site / deploy (push) Failing after 1m37s
release / release-windows (push) Failing after 6m6s
portable-unsigned / portable-windows (push) Failing after 18m52s
2026-09-18 20:58:52 +09:00