Commit Graph
10 Commits
Author SHA1 Message Date
Palash Debnath 93025d9a81 fix(dictation): stop the widget stranding an empty square, repair the swept data dirs (#1398)
The dictation hotkey could leave a blank dark square stuck on the desktop with no way to dismiss it. Three defects compounded: the tray listener's effect depended on [state], so it detached across an await on every state change and a press landing in that gap was lost; an idle pill renders null, so the window Rust had already shown was empty; and the opaque chrome background made that empty window a hard-edged square. Nothing could hide it — dismiss() is only reachable from the X button, Esc, or a post-session timer, none of which exist for a session that never started.

Fixed at the invariant rather than the call sites: the listener subscribes once for the component's lifetime, the widget window's chrome background is transparent, and an idle-but-visible window reconciles itself to hidden. The reconcile is polled (a dropped press changes no React state, so there is nothing to key an effect off) and aborts if its effect is torn down mid-check, so it can never hide a dictation that has just started.

Also in scope:

- The rename sweep had repointed three data-dir literals at a brand-named directory that does not exist, so smoke-test.sh verified a directory the backend never writes and desktop-prod.sh silently stopped clearing backend state on Windows. Both invisible on macOS, where they are usually run. A guard test now pins the assignments specifically.
- The dictation model picker's download sizes were wrong for all seven models, in both directions — Parakeet TDT v3 (the recommended default) understated 180 MB against an actual 670 MB, while the low-RAM fallbacks were overstated threefold, discouraging exactly the choice that would have helped. Measured from the published repos and pinned by a test.
- The 0.6B Parakeet models now decode on more threads, capped by host cores and still overridable.
- uninstall.ps1 gained a UTF-8 BOM (Windows PowerShell 5.1 mis-decodes its non-ASCII output without one), and sponsor.yml lost its last OmniVoice references.
2026-08-07 05:39:41 +05:30
Palash Debnath 5cab8e0149 feat: rename the product to VoiceStudio (previously OmniVoice-Studio)
Renames what users see. The app, the installers, the window title, the
docs and all 21 locales now say VoiceStudio, with "(previously
OmniVoice-Studio)" noted near the title of each doc surface so people
recognise it.

Deliberately NOT renamed, because renaming any of them silently breaks
an existing install — there is no legacy-path fallback anywhere in this
codebase:

  - bundle identifier com.debpalash.omnivoice-studio (MSI UpgradeCode,
    macOS TCC grants, managed venv, WebView localStorage, the
    single-instance lock)
  - data directories OmniVoice / .omnivoice and omnivoice.db
  - the ~150 OMNIVOICE_* environment variables
  - the X-OmniVoice-* HTTP headers (a wire protocol)
  - the published Docker image paths
  - the OmniVoice ENGINE, which is a model name and not this product

tests/test_identity_paths_survive_the_rename.py pins every one of those
so a future well-meaning sweep cannot orphan a user's library.

Linux .deb users install a new package name and should apt remove
omnivoice-studio; that note is in the changelog.
2026-08-07 01:30:58 +05:30
4aa9abe22a docs+scripts: install fixes — desktop-prod tauri resolution, Ubuntu white-screen guidance, honest GPU/prereq docs (#960 #961 #962) (#964)
* docs+scripts: install fixes — desktop-prod tauri resolution, Ubuntu white-screen guidance, honest GPU/prereq docs (#960 #961 #962)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(changelog): add the install-fixes batch under [Unreleased] (#964)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: mergetest <test@local>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 19:34:01 +05:30
Palash DebnathandClaude Opus 4.8 f555fe0720 fix(bootstrap): surface setuptools-repair failures + verify pkg_resources (follow-up to #253) (#254)
* fix(bootstrap): surface setuptools-repair failures + verify pkg_resources (follow-up to #253)

Three gaps flagged by review bots on PR #253 are addressed:

1. **Layer-2 repair result captured** (`bootstrap.rs` ~line 481): the
   `let _ = run_streaming(...)` that silently discarded network/permission
   failures from the targeted `uv pip install setuptools>=75,<80` is replaced
   with a `match` block that logs `log::info!` on success and `log::error!`
   on failure (consistent with the Layer-3 path).

2. **Post-repair re-verification added** (`bootstrap.rs`): after the targeted
   install in Layer 2, a second `import pkg_resources` check is run. If
   pkg_resources is *still* absent, a `log::error!` with an actionable
   remediation message is emitted before returning. This closes the gap where
   bootstrap handed back a known-bad venv that caused the dubbing crash (#248)
   with no clear signal in the log.

3. **Test strengthened** (`bootstrap.rs` `setuptools_repair_uses_correct_specifier`):
   the test now mirrors the exact `&[&str]` slice used in both repair branches
   and asserts `repair_args[2] == "setuptools>=75,<80"` as a single positional
   argument. This catches the split-arg regression the review bot identified
   (e.g. `["setuptools>=75", ",<80"]`) which would silently install the latest
   setuptools and leave pkg_resources absent.

4. **Smoke-test INST-01/02 hardened** (`scripts/smoke-test.sh`): exports
   `UV_PYTHON_PREFERENCE=only-system`, `UV_HTTP_TIMEOUT=120`, and
   `UV_HTTP_RETRIES=5` before the `uv run` import checks so that failures
   reflect real bootstrap regressions, not harness-network timeouts.

`cargo test bootstrap` → 5 passed, 0 failed.

Closes review findings on #253. Related: #248.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(bootstrap): fail clearly when pkg_resources repair fails (PR #254 review)

- ensure_venv_ready now returns None (via fail()) when pkg_resources is still
  missing after the targeted setuptools repair, instead of returning a venv that
  crashes on the first ASR/dub call. The 'pkg_resources' message routes to the
  PKG_RESOURCES_MISSING failure mapping for a clear, doc-linked remediation.
- smoke-test.sh: correct the comment (timeout+retry vars, not a non-existent index var).

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 22:50:37 +05:30
Palash DebnathandClaude Opus 4.8 c98b0aaf6f fix(bootstrap): guarantee pkg_resources in backend venv (closes #248) (#253)
Root cause: the existing-venv fast-path in ensure_venv_ready() only
checks `import uvicorn` before returning — it never verified that
`pkg_resources` (dropped by setuptools≥80, issue #224) was present.
Users who installed before commit 675cc20 (Jun 1) had setuptools 82.x
in their venv; the app launched fine but any dubbing/transcription run
immediately crashed with `ModuleNotFoundError: No module named
'pkg_resources'` via the whisperx → ctranslate2 → import chain.
Reinstalling didn't help because the app re-detected uvicorn and skipped
the repair sync entirely.

Fix (three-layer defence):

1. **Existing-venv health check**: if uvicorn imports but
   `import pkg_resources` fails, fall through to the repair-sync path
   instead of returning the broken venv. Logs a clear message
   distinguishing "uvicorn missing" from "pkg_resources missing (#248)".

2. **Post-repair verification**: after the repair sync succeeds, run
   `import pkg_resources` again; if still absent, run a targeted
   `uv pip install "setuptools>=75,<80"` to install it directly without
   re-resolving the full dependency tree.

3. **Post-fresh-sync verification** (belt-and-suspenders): same
   pkg_resources check + targeted pip-install added after every
   fresh-install `uv sync`, catching the edge case where the bundled
   uv.lock is absent and uv resolves setuptools≥80 from scratch.

All three paths use the same scrub_python_env + apply_uv_http_env
guards already applied elsewhere; safe on macOS/Linux/Windows.

Also adds `setuptools_repair_uses_correct_specifier` unit test and
updates the INST-01 smoke-test comment to reference #248.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 22:22:28 +05:30
Palash DebnathandClaude Opus 4.8 e3d7815df0 fix(scripts): Windows (Git Bash/MSYS) support across install/run/smoke-test (#164) (#166)
Follow-up to #165 — the same uname Darwin/Linux-only pattern in the other
dev scripts:
- smoke-test.sh: add MINGW*/MSYS*/CYGWIN* detection + Windows data paths
  (%APPDATA%/%LOCALAPPDATA%, per backend/core/config.py) + .exe binary suffix.
- run.sh: Windows detection + %LOCALAPPDATA% log dir; backend launch already
  uses 'uv run' which is cross-platform.
- install.sh: detect Windows and print a clear 'use the .msi / WSL' message
  (the brew/apt system-dep installer can't run on native Windows) instead of
  silently treating it as Linux.
build-omnivoice-tts.sh already handled windows-x86_64; record-reference.sh
already guards macOS-only with a clear message.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 13:35:25 +05:30
Palash DebnathandClaude Opus 4.8 898f41a57d fix(windows): gate torch.compile on Triton + ASR critical-path smoke (plan-02, closes #65) (#138)
* fix(windows): gate torch.compile on Triton availability (#129, closes #65)

plan-02. torch.compile(mode="reduce-overhead") needs Triton at runtime;
Triton has no Windows wheel, so the old `device=="cuda"`-only guard in
model_manager.py failed on Windows+CUDA and surfaced as a confusing "OOM"
(#65). Inference-time, hard to diagnose.

- engine_env.should_torch_compile(device): requires CUDA + find_spec("triton")
  + the existing perf.torch_compile_disabled setting being off; logs the skip
  reason at INFO and falls back to eager.
- model_manager.py call site uses it instead of the bare cuda check.
- smoke-test.sh INST-02: import torch + ctranslate2 + whisperx (full ASR path)
  so a missing transitive dep fails the build instead of crashing mid-
  transcription (#116). Runs in the CI smoke-matrix on Win/macOS/Linux.

setuptools>=75.0 (fix-sequence step 1) already pinned (#58). Linux/CUDA+Triton
behaviour unchanged.

Tests (TDD): tests/test_torch_compile_gate.py (4). Closes #65; addresses
#129/#116.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(windows): also gate subprocess torch.compile on Triton (Greptile #138)

Greptile flagged that the in-process gate left a parallel gap: engine
subprocesses honour TORCH_COMPILE_DISABLE, but build_engine_env() only set
it on the user's Performance toggle — so a Triton-absent host (Windows, or
macOS) still exposed subprocess engines to the same crash this PR fixes
in-process.

- build_engine_env(): set TORCH_COMPILE_DISABLE=1 when the user disabled
  compile OR Triton is unavailable (find_spec), cross-platform — mirrors
  should_torch_compile(). Drops the Windows-only scoping (and the now-unused
  `import sys`).
- Refreshed the stale module docstring.
- 3 new tests cover the subprocess gate (triton-missing, triton-present,
  user-opt-out). 7/7 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* revert(engine_env): keep subprocess TORCH_COMPILE_DISABLE user-driven

Reverts the build_engine_env() broadening from the previous commit. Auto-
disabling subprocess torch.compile on Triton-absence conflicts with a
deliberate, tested contract (test_perf_settings: Windows + flag-off ⇒ no
injection; non-Windows ⇒ never inject) — the subprocess var is intentionally
under the user's explicit control.

The #65 fix is the in-process should_torch_compile() gate (unchanged here),
which IS automatic and fully tested. Pushing back on the subprocess auto-gate
as a separate, deliberate contract change rather than forcing it through by
rewriting established tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 10:22:19 +05:30
Palash DebnathandClaude Opus 4.7 c32041289d Phase 1 Wave 3: AppImage launcher + .deb ffprobe + Docker LAN + Gatekeeper probe (closes #54, #56, #76, #80) (#93)
* fix(appimage): conditional WEBKIT_DISABLE_COMPOSITING_MODE launcher (#56)

WebKitGTK 2.44.x and 2.46.x have a compositing-path regression on Wayland
that blanks the AppImage's first paint on Fedora 44 / Ubuntu 24.04. Setting
WEBKIT_DISABLE_COMPOSITING_MODE=1 forces the software fallback that works,
but blindly setting it on healthy WebKit versions (2.48+) regresses those.

This wave adds a conditional AppRun launcher that detects the WebKit
version via pkg-config and only sets the env var on the broken ranges
(plus a fail-safe when pkg-config is absent or the version is unknown).
The launcher is injected into Tauri's AppImage staging dir via a
beforeBundleCommand hook — see .planning/decisions/apprun-strategy.md for
the spike outcome and rationale (Strategy B chosen).

Phase 1 Wave 3 — Plan 01-03 Task 1. Closes #56 frontend half.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(deb): relocate bundled ffprobe out of /usr/bin to avoid conflicts (#76)

Prior versions placed the bundled ffprobe at /usr/bin/ffprobe via Tauri's
externalBin, which overwrites the system ffprobe on Ubuntu 26.04 and
collides with apt-installed media-package ffprobe.

Relocate the .deb-bundled ffprobe to /usr/lib/omnivoice-studio/bin/ffprobe
via bundle.linux.deb.files, plus defensive maintainer scripts:
  - preinst:  ensure target dir exists for upgrade flows
  - postinst: remove legacy /usr/bin/ffprobe ONLY when dpkg confirms our
              package owns it (never touches a user's distro ffprobe)
  - postrm:   clean up the relocated path tree on purge/remove

Rust side (tools.rs::resolve_ffprobe) now probes the new path on Linux,
and backend spawn (backend.rs) carries both FFPROBE_PATH (legacy alias)
and OMNIVOICE_FFPROBE_PATH (canonical) into the backend env. Python side
(ffmpeg_utils.resolve_ffprobe) reads OMNIVOICE_FFPROBE_PATH first, falls
back to FFPROBE_PATH, then to shutil.which("ffprobe").

6 new unit tests cover the env-cascade resolution.

Phase 1 Wave 3 — Plan 01-03 Task 2. Closes #76.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(frontend): centralised apiBase resolver for Docker LAN access (#80)

Docker / LAN browser users hit the preview API at the LAN host's IP, not
their local machine — the prior frontend/src/utils/media.js:20 hardcoded
http://localhost:3900, which from a LAN client resolved to the client
machine itself.

Centralise via frontend/src/utils/apiBase.ts:
  1. VITE_OMNIVOICE_API override (Docker compose / dev) always wins.
  2. Tauri webview → http://localhost:3900 (unchanged behaviour).
  3. Plain browser → ${window.location.protocol}//${window.location.hostname}:3900
     (follows the page's origin — closes #80).
  4. SSR / no-window → http://localhost:3900 (safe fallback).

Grep-sweep confirmed media.js:20 was the only hardcode site (Assumption
A4 in 01-RESEARCH.md verified). 6 new vitest cases cover the resolver.

Phase 1 Wave 3 — Plan 01-03 Task 3. Closes #80 frontend half.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(backend): macOS Gatekeeper quarantine probe + INST-01 guard (#54)

Adds backend/core/gatekeeper_detect.py which walks up from sys.executable
to find the .app bundle and runs `xattr -l` to check for the quarantine
extended attribute (com.apple.quarantine). On detection, the lifespan
startup probe logs a structured warning and emits a system_error event
through the existing event bus with error_class="GATEKEEPER_QUARANTINE",
which Wave 2's React ErrorBoundary turns into a docs deeplink.

Detection is informational only — we never auto-run `xattr -cr` (the app
itself is quarantined and cannot fix its own state per Anti-Pattern in
01-RESEARCH.md). Users get a clear pointer to the workaround docs.

GET /system/quarantine-status exposes the structured payload so the
frontend can poll on first load.

INST-01 (setuptools>=75.0 pin from PR #62) gains a PR-time guard in
tests/backend/test_pyproject.py + a user-observable smoke check in
scripts/smoke-test.sh (pkg_resources + whisperx import).

7 gatekeeper tests + 1 pyproject test added — all pass.

Phase 1 Wave 3 — Plan 01-03 Task 4. Closes #54 backend half (Wave 2 owns
the docs page + ErrorBoundary deeplink wiring).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 05:53:15 +05:30
Palash DebnathandClaude Opus 4.7 a1ef66c321 Stability pass: DB leaks, App.jsx hooks refactor, desktop bootstrap (#49)
* fix: eliminate DB connection leaks, race conditions, and deprecated asyncio API

## DB Connection Leaks (P0)
- Convert 38 raw get_db() calls to db_conn() context manager across 14 router files
- Connections are now guaranteed to close even when exceptions are raised
- profiles.py create_profile: clean up orphaned audio file if DB insert fails
- profiles.py lock_profile: consolidate 3 separate conn.close() error paths

## Race Condition (P1)
- Add _dub_jobs_lock (threading.Lock) to protect _dub_jobs dict in dub_pipeline.py
- get_job/put_job now thread-safe for concurrent dub sessions

## asyncio Deprecation (P2)
- Replace 23 asyncio.get_event_loop() calls with asyncio.get_running_loop()
- Prevents DeprecationWarning on Python 3.12+ and future breakage on 3.14

## Quick Fixes
- gallery.py preview_voice: remove filesystem path from error response (P2)
- dub_pipeline.py parse_vtt_segments: remove redundant `import re` inside loop (P3)
- gallery.py _init_gallery_db: use db_conn() context manager (P2)

* refactor: extract hooks, centralize isTauri, add pytest-cov

## Frontend
- Extract useTTS hook (150 LOC) — TTS generation, streaming, audio ingestion
- Extract useProfiles hook (219 LOC) — voice profile CRUD, lock/unlock, preview
- Centralize isTauri detection: dialog.js, VoiceGallery.jsx, Settings.jsx
  now import from utils/media.js instead of 4 different detection patterns

## Backend
- Add pytest-cov to dev dependencies
- Baseline coverage: 39% across backend/ (214 tests pass)
- Add .coverage to .gitignore

* feat: add Vitest + checkJs, extract useDubWorkflow + useAppData hooks

## Frontend Testing (new)
- Set up Vitest with jsdom environment + @testing-library/react
- 11 tests: utils (isTauri, formatTime, constants) + Zustand store (mode, text, dubStep, pill)
- Scripts: 'test' (vitest run), 'test:watch' (vitest), 'test:legacy' (node runner)

## App.jsx Decomposition (continued)
- Extract useDubWorkflow hook (387 LOC) — upload, ingest, transcribe SSE,
  translate, generate SSE, abort, stop, cleanup
- Extract useAppData hook (181 LOC) — data loading, localStorage persistence,
  WebSocket real-time updates, model-status pill management

## TypeScript checkJs
- Enable checkJs: true in tsconfig.json for IDE-level type checking
- 947 existing errors (informational, not blocking builds)
- noImplicitAny remains false to avoid blocking

* ci: add Vitest step, fix useProfiles duplicate state

## CI
- Add 'Run Vitest (frontend)' step — runs 11 unit tests
- Override --checkJs false in CI typecheck to avoid 947 pre-existing errors
- Rename legacy test step for clarity

## Hooks
- Fix useProfiles to accept loadProfiles from parent (useAppData)
  instead of managing its own duplicate profiles array

* refactor: wire hooks into App.jsx — 2067 → 1129 LOC (-45%)

App.jsx now delegates to extracted hooks instead of inline logic:
- useAppData: data loading, localStorage, WebSocket, model pill
- useProfiles: voice profile CRUD, lock/unlock, preview
- useTTS: generation, streaming, audio ingestion
- useDubWorkflow: upload, transcribe SSE, translate, generate SSE

988 lines removed. All handler logic lives in focused,
independently testable hooks. Store selectors and render
JSX stay in App.jsx as the shell.

Verified: vite build clean, 11 frontend + 214 backend tests pass.

* feat: show real-time percentage on model loading pill

Backend: register hf_progress listener during _load_model_sync()
so download/weight-loading tqdm events update _loading_detail with
a progress percentage (0-99%). get_model_status() now includes a
'progress' field that the frontend polls.

Frontend: useAppData reads msQuery.data.progress and calls
setPillProgress() — the FloatingPill already renders the percentage
text and progress bar width from this value.

* fix: prevent FileNotFoundError in desktop bundle during model init

transformers >=4.52 calls _can_set_experts_implementation() and
_can_set_attn_implementation() during PreTrainedModel.__init__,
which open the class source file via open(class_file). In a Tauri
desktop bundle, module.__file__ points to a path that doesn't
exist on disk, causing:

  FileNotFoundError: .../omnivoice/models/omnivoice.py

Override both classmethods on OmniVoice to return static values
without filesystem access. OmniVoice doesn't use MoE experts
(return False), but does support flex/flash attn (return True).

* fix: sync source dirs on every bootstrap, not just first run

The Tauri bootstrap previously only copied omnivoice/ and backend/
to Application Support on the first run. Subsequent app updates
kept using stale source files, preventing bug fixes from landing.

Now ensure_venv_ready() always syncs both directories from the
bundle resources before returning, even when the venv is healthy.
This fixes the FileNotFoundError crash where the old omnivoice.py
lacked the _can_set_experts_implementation override.

* ui: premium setup wizard polish

- Primary button: solid gradient fill with hover glow + lift + press
- Stepper nav: connected pills with glow ring on active step
- Welcome cards: glassmorphism with stagger-in animations, lucide icons,
  left-border accent strip, hover translate
- Preflight panel: colored icon pill backgrounds, stagger-slide entrance
- Step transitions: fade+slide animation via keyed wrapper
- Footnote: shortened paths (~/ notation), Reveal in Finder button
- Recommendation banner: gradient background with accent glow
- Compact spacing throughout for denser, professional layout

* fix: kill zombie backend on clean+retry bootstrap

When clean_and_retry_bootstrap removes the project dir, any old
uvicorn process still running from the deleted paths remains alive
on port 3900. The subsequent retry_bootstrap sees the port is
healthy and attaches to the zombie instead of re-bootstrapping.

Now explicitly kill any process on the backend port after cleaning,
before calling retry_bootstrap.

* feat: integrate speaker clones into dubbing interface, sanitize system environment variables for subprocesses, and improve FFMPEG binary path resolution.

* fix: restore docker compose default + drop dead setSeed call

- deploy/docker-compose.yml: remove profiles: ["cpu"] from the default
  service so `docker compose up` matches the comment on line 5. With the
  profile present, no service auto-started.

- frontend/src/App.jsx: drop the setSeed call in restoreHistory. The
  selector was never reintroduced after the App.jsx hooks split, and
  there is no seed state in the store — seeds are generated fresh per
  call in useTTS and only read from history items for display.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review — async detection, dub stream, bootstrap fail-fast

- backend/services/tts_backend.py: invert async-context detection in
  _ensure_loaded. The previous code unconditionally caught its own
  diagnostic RuntimeError and then called asyncio.run() inside a
  running loop, masking the intended error message.

- frontend/src/hooks/useDubWorkflow.js: require a terminal `done` event
  before reporting dub success. Without this, a dropped stream after
  partial progress would flip the UI to `done`, refresh history, and
  play the completion ping as if generation finished.

- frontend/src/hooks/useDubWorkflow.js: restore the previous step when
  tasksCancel() fails. The UI was getting stuck in `stopping` forever
  on cancel errors.

- frontend/src-tauri/src/bootstrap.rs: fail-fast when source sync fails
  after the existing directory has already been removed. The previous
  warn-and-continue path could leave the install with no backend/ or
  omnivoice/ sources and defer the failure to backend startup with a
  cryptic error.

- backend/api/routers/generation.py: add `from e` to the ValueError →
  HTTPException re-raise (Ruff B904).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: preserve % suffix in TTS generation timer

The 100ms timer in useTTS was rewriting generationTime to a plain
elapsed-seconds string, which immediately wiped the "(xx%)" download
suffix written on the next iteration of the response-body loop. The
real-time percentage was flickering on/off as a result.

Read the previous value inside the setter and reattach any existing
percent suffix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:49:04 +05:30
Palash Debnath 0ecbf136e7 refactor: codebase cleanup & root folder reorganization (#38)
refactor: codebase cleanup & root folder reorganization
2026-05-03 03:12:07 +05:30