Both found live while building gui/tests/dod's DoD harness, not from reading
the code — see that commit for how.
RpcClient::stop() left conn_ dangling after joining the worker thread: the
thread's own finish() flushes the DeferredDelete stop()'s
connect(&thread_, &QThread::finished, conn_, &QObject::deleteLater) already
posted, so conn_ is gone by the time stop() returns, but nothing cleared the
pointer. Any caller that calls stop() and later lets the client destruct
(the harness's own client.stop() at shutdown; also plain, correct API usage)
hit a double-free in the destructor's leftover `delete conn_`. Caught by
ASan on the very first run that actually exercised the stop-then-destroy
path.
requestInitialList() also called download.list with a hardcoded
`{"limit": 1000}`, silently capping the table at 1000 rows no matter how
many the daemon actually has — download.list.schema.json's own description
says "the GUI pages", not "the GUI takes it all in one call". The
scroll-60fps DoD gate refused to run against mockd --tasks 10000 rather
than "pass" against a 1000-row table, which is what surfaced it.
requestInitialList() now pages (5000 per call, the schema's own max) until
`total` is satisfied, then resets the model once with everything.
Separately: RpcConnection's session.subscribe list never included
event.settings.changed or event.grabber.progress, even though RpcClient has
carried signals for both since the Options/Grabber work — session.subscribe
"replaces the previous selection" and "nothing is delivered until this is
called", so both events were being silently dropped by any real daemon that
enforces the subscription (mockd does; verified live with a second
subscribed client actually receiving event.settings.changed after this
fix, round-tripped through a real veloxd's settings.set). GrabberWizard's
5 s poll fallback is exactly why this went unnoticed until now — it covered
for the missing push the whole time.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NSCdCWFXBTSBBK3MzWtJiC
Continues the build order past the Add-URL/File-Info/Progress dialog flow.
- OptionsDialog: General/Save To/Connection/Downloads/Proxy/Sounds tabs, every
control bound to a real settings.* key (contracts/schema/types/Settings.
schema.json). The spec's File Types and Site Logins tabs have no settings.*
backing (categories go through category.upsert, credentials through the
Secret Service) so they don't exist here — a tab either binds to a real key
or isn't shipped. diffChanged() sends only what actually changed, matching
settings.set's "changed[] names exactly what took effect" contract.
- SchedulerDialog: per-queue schedule (schedule.get/set) plus maxConcurrent/
onComplete (queue.upsert), Start Now/Stop. Queue.schema.json already carries
the schedule so queue.list alone seeds the window.
- SpeedLimiterDialog: the live global limiter (limiter.get/set) — a different
thing from Options' downloads.speedLimit* default. buildParams() enforces
the schema's "0 with enabled true must not be offered".
- BatchDialog: clipboard-blob and {start..end}-wildcard tabs sharing one
category/queue/start-mode footer into download.addBatch.
- GrabberWizard: 4-step QWizard (project label -> start URL/depth/filters ->
file-type filter -> review), grabber.start feeding a poll+event.grabber.
progress-driven review page, Finish = grabber.harvest for the checked files.
- TrayIcon: active-count tooltip, Show/Add URL/Pause All/Resume All/Speed
Limiter submenu/Quit. Quit only closes the GUI — there is no RPC to stop
veloxd itself, filed as a new gap in daemon-requests-m1.md. MainWindow now
also hides to tray instead of closing when general.minimizeToTray is set.
Every dialog's non-widget logic (diffChanged, buildSchedule, buildParams,
parseUrlBlob/expandWildcard/buildAddBatchParams, buildFileTypes/
buildStartParams/buildHarvestParams) is a static pure function with its own
test, same shape as FileInfoDialog::buildSpec from the previous round.
Verified end-to-end against a running mockd under ASan+UBSan: all five
surfaces render real data (settings.get values, queue.list's two seeded
queues, limiter.get, a live grabber.start/status crawl returning 3 files) with
no sanitizer reports. gui-check (non-ASan) and dev (ASan+UBSan) presets both
build the whole repo clean; all gui-labeled ctest targets pass.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NSCdCWFXBTSBBK3MzWtJiC
Protocol.hpp gains method-name constants for every RPC the next batch of dialogs
needs (settings.*, limiter.*, schedule.*, queue.*, download.addBatch, grabber.*)
and RpcClient re-broadcasts the two events nothing consumed yet:
event.settings.changed and event.grabber.progress. No behaviour change on its
own — OptionsDialog, SchedulerDialog and GrabberWizard are what actually call
these, landing next.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NSCdCWFXBTSBBK3MzWtJiC
Wires up the three dialogs from build order step 5 (docs/03-gui-spec.md
§§2-3) and the MainWindow slots that were declared but never implemented:
- AddUrlDialog: clipboard prefill is the explicit path docs/06 R2 calls
for (no passive monitoring, not advertised).
- FileInfoDialog: async download.probe never blocks the UI; ends by
calling download.add itself (Now / Later / Add to Queue). buildSpec()
is a pure static so the optional-field-omission logic is unit-testable
without touching a widget.
- ProgressDialog: non-modal, WA_DeleteOnClose, driven by the taskProgress/
taskStateChanged signals RpcClient already re-broadcasts; download.get
seeds state once for a dialog opened mid-transfer. Hosts SegmentBarsWidget
and SpeedGraphWidget.
MainWindow: openAddUrlDialog/openPropertiesForSelection/showTableContextMenu
now have bodies; category.list/queue.list responses are cached so File Info
can populate its category combo and queue menu without a second round trip.
The row context menu covers what already exists (Resume/Pause/Stop/Delete/
Properties) and deliberately leaves out Open/Open With/Move-Rename/
Redownload/Add to Queue — those need dialogs later build-order steps haven't
reached yet.
util/Format.hpp: pulled the bytes/rate/eta formatting out of MainWindow and
DownloadTableModel once the dialogs wanted the same strings a third time.
Verified end-to-end against a running mockd (category.list/queue.list,
download.probe, download.add, download.get, and live event.task.progress/
event.task.state) under ASan+UBSan: all three dialogs render correctly
against real fixture data and the flow runs clean with no leaks or
sanitizer reports.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NSCdCWFXBTSBBK3MzWtJiC
Per-connection progress bars and the 60 s rolling speed graph the progress
dialog needs (docs/03-gui-spec.md §3). Both reuse row/widget state across
ticks instead of rebuilding, matching the discipline DownloadTableModel
already uses for progress patches.
SpeedGraphWidget keeps a fixed ring buffer and one reused QPainterPath —
no allocation in paintEvent or addSample. Fixed a real bug found while
writing tst_speedgraphwidget: the elapsed timer was started in the
constructor, so the very first sample after construction would silently
wait up to 1 s to be recorded instead of landing immediately.
Tested against mockd (both offline via QTest/offscreen, and manually
against a running mockd instance through ProgressDialog once that lands).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NSCdCWFXBTSBBK3MzWtJiC
Pointed the existing RPC client at a real veloxd instance (isolated
HOME/XDG_RUNTIME_DIR/XDG_DATA_HOME — never touched the real ~/Downloads),
added a real download.add against tools/testserver, and watched it render
live end to end with no GUI code changes:
- handshake, subscribe, category.list/queue.list all match what DAEMON
reported
- the full event.task.state sequence and batched event.task.progress
both applied correctly by DownloadTableModel
- the written file's SHA-256 matches the server's reference
Filed gui/docs/daemon-requests-m1.md: TaskSummary.sizeBytes is never
populated by this daemon build, even in download.get after the task
completes with the exact byte count already on disk. Not a GUI bug —
ProgressDelegate and the model already do the documented right thing
when size is unknown (fall back to plain text, no bar) — but it means
every task renders without a percentage against the real daemon today.
mockd always supplies sizeBytes so this doesn't block current GUI work;
flagging before the M1 GUI<->daemon integration pass.
mockd stays the primary harness for the unhappy paths
(--slow/--flaky/--drop-connection) that a real daemon won't misbehave on
command for.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016Ne28kx4VreeBWZv82Nksd
- CategoryPanel: the left tree — All Downloads / Unfinished / Finished,
then Categories and Queues populated from category.list / queue.list,
with per-node task counts. Selecting a node emits a TaskSelection.
- DownloadFilterProxy: QSortFilterProxyModel keyed off that selection.
Client-side for M1 (the whole list fits); asTaskFilter() exposes the
equivalent TaskFilter for a server-side download.list once paging lands.
- MainWindow: menu bar (Tasks / Downloads / View / Help) sharing QAction
objects with the toolbar; QSplitter [panel | table]; Delete with a
confirm; Resume/Pause All; View menu toggles the panel. Actions
disabled while offline.
- Counts are computed off a throttled 400 ms timer, not the 4 Hz progress
path. Fixed a debounce-vs-throttle bug found in the first screenshot:
restarting the timer on every progress tick meant it never fired and
the status bar sat at "0 of 0 downloads".
- First-run column widths that fit the content.
- tst_downloadfilterproxy: nodes filter to their own rows, the
Finished/Unfinished split is correct, and the filter is dynamic (a row
that finishes leaves the Unfinished node with no re-list). Verified
against mockd --tasks 400 (screenshot: tree counts 75/84/89/83/69 sum
to 400, status bar "400 of 400, 21 active").
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016Ne28kx4VreeBWZv82Nksd
Review feedback on the filed request:
- R1: gui/CMakeLists.txt is on main now, so root's
find_package(Qt6 ... Svg REQUIRED) is live — a missing SVG dev package
is a hard configure failure for the whole project, not a skipped guard.
Added the reason CI hasn't caught it: ubuntu-latest is 24.04 (where
libqt6svg6-dev likely resolves), the project targets 26.04 (where it
does not). Wrong name + runner/target release mismatch = the class of
bug PKG/QA owns is currently unobservable in CI. That's the argument
for R2, folded in.
- R3: corrected — CI does build the GUI and runs its three ctests under
the ci preset. What has no home is the non-unit-test DoD: 10k-row
60fps, flat RSS over a 10-minute run, and mockd
--slow/--flaky/--drop-connection recovery. Asked for those specifically.
- gui/docs/ext-requests-m1.md: CLAUDE.md §3 says the no-download-logic
rule applies to extension/ too; GUI made its half an executable ctest,
EXT's half is still prose. Suggested the ESLint equivalent for the
existing extension-lint job.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016Ne28kx4VreeBWZv82Nksd
Follow-up hardening after a review noted the RTL "check" verified nothing
(a .ts stub that no test loads), matching a session-wide pattern of
checks written against what should be true rather than what would break.
- Split the non-main() code into velox-gui-lib (STATIC) so tests link the
real widgets/models, not a reimplementation.
- tst_rtl: builds the real MainWindow, flips layoutDirection, asserts the
direction propagates to the central widget AND that the offline-banner
QHBoxLayout actually mirrors (label x-position LTR vs RTL differs by
>100px). Verified it fails when the banner is pinned LtR.
- gui_no_download_logic: a ctest that greps gui/src for curl_*/pwrite/
sqlite/QSqlDatabase/QNetworkAccessManager and fails on a hit — CLAUDE.md
§3 as an executable check. Verified it fails when a curl_ token is added.
- Still uncovered (noted, not claimed): that the translation catalogue
loads and the right context/strings resolve at runtime.
Not covered here because the files are PKG/QA-owned: tools/bootstrap.sh
ships a package name that does not exist on 26.04 (libqt6svg6-dev; the
real one is qt6-svg-dev), and --check validates pkg-config outcomes
rather than the apt names it would install. Both, plus the same name in
AGENT-PKG-QA.md and the README, are written up apply-ready in
gui/docs/pkg-qa-requests-m1.md.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016Ne28kx4VreeBWZv82Nksd
First vertical slice of velox-gui, built entirely against tools/mockd
(no daemon dependency):
- rpc/: RpcConnection runs a QLocalSocket on a worker thread with
newline-delimited JSON-RPC framing, drives the session.hello /
session.subscribe handshake, and reconnects with exponential backoff
(250 ms -> 8 s). RpcClient is the main-thread face: marshals calls onto
the worker, delivers replies as main-thread callbacks, re-emits server
notifications as typed Qt signals, and issues the one-shot download.list
on reaching Connected.
- models/DownloadTableModel: QAbstractTableModel over TaskSummary. A
progress batch is a row patch with a narrow dataChanged over the value
columns only; beginResetModel() is reserved for the initial load and a
reconnect resync.
- widgets/ProgressDelegate: in-cell progress bar for the Status column.
- mainwindow/MainWindow: the table, a status-bar connection dot, an
offline banner instead of a modal, dialog-free pause/resume/stop
actions, and QSettings column/geometry persistence.
- gui/CMakeLists.txt links velox::proto (never velox::core, ADR 0009) and
self-guards on the veloxproto target so main keeps configuring if it is
ever absent again.
- i18n from the first commit: every string via tr(), plus an Arabic .ts
stub for the RTL check.
- tests/: headless QTest for the model — proves the progress patch is a
narrow dataChanged and never resets the model.
Verified end-to-end against `mockd --tasks 300`: handshake, initial list,
live progress batches applied to the model, and a clean
Reconnecting -> Connected recovery when mockd is bounced mid-run.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016Ne28kx4VreeBWZv82Nksd
Lays out Velox Download Manager (IDM-class download manager for Ubuntu
26.04) as a monorepo ready for parallel lane development. No implementation
code by design.
- docs/: architecture, roadmap M0-M7, IDM-parity GUI spec, engine design,
Firefox extension spec, risks/spikes, packaging
- contracts/: wire-contract skeleton (JSON Schema + fixture templates) —
the single synchronization point between lanes
- docs/agents/: one brief per lane (PROTO, CORE, DAEMON, GUI, EXT, PKG/QA)
with owned directories, build order and definition of done
- CLAUDE.md: rules of engagement — lane ownership, layering, non-negotiables
- CMake scaffolding with dev/tsan/release/ci presets
Two environment findings shape the design: Firefox here is the Mozilla snap
(native-messaging risk, so the extension carries a loopback-WebSocket
fallback), and Wayland forbids passive clipboard monitoring (so clipboard
capture is explicit-action-first).
Co-Authored-By: Claude Opus 5 <[email protected]>