Skip to content

Fix review findings across drawer, clipboard write-through, desktop and release automation - #32

Merged
ami3go merged 2 commits into
mainfrom
fix/review-findings-v2
Aug 20, 2026
Merged

Fix review findings across drawer, clipboard write-through, desktop and release automation#32
ami3go merged 2 commits into
mainfrom
fix/review-findings-v2

Conversation

@ami3go

@ami3go ami3go commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Second review pass over the v0.11.6/v0.11.7 range — the areas the first pass didn't reach, reviewed by four sequential workers. 19 of 30 confirmed findings fixed; one deliberately not, explained at the bottom.

The one that mattered most

verify_static_client returned FAIL on the committed tree before this change: assets/main.js and assets/browser-support.js checksums were left stale by an earlier commit. CI never caught it, because the release gate runs npm run build first — which rewrites both dist/ and the manifest, making the drift invisible in CI and real everywhere else.

Clipboard write-through

The Copy button recognises the controller's reply by matching the status line against exact strings. Rewording one at a setStatus call site silently broke it — no exception, no console error, and a green suite, because the tests pinned only the main.ts side. Those four headlines now live in src/clipboard-status.ts and both modules import them.

Also fixed: the execCommand fallback had no generation guard (a superseded copy stole focus and wrote over a newer request, and copied the live selection rather than the text it validated); the 1200 ms timeout overwrote real terminal statuses, replacing "Clipboard skipped — exceeds N KiB" with a misdiagnosis; Ctrl+C had no session guard, reinstating a message the controller deliberately suppresses; and the fallback now restores focus where it was instead of parking it on the canvas, which sends the next keystroke to Android.

Drawer

openGroup forced focus to the canvas two frames after the Clipboard group opened. Its comment justified this by controller.ts focusing the fallback textarea — a listener deleted when Ctrl+V moved to a document-level paste handler. The guard outlived its cause and now only steals focus from someone typing into the textarea, injecting their keystrokes into the phone.

The @import of -core.css also carried no ?v=, leaving 531 lines of drawer layout outside cache-busting; a stale copy after upgrade renders every drawer group stacked and visible.

Desktop

The Summary "Local URL" was a live hyperlink in every server state while the header Open button beside it was correctly gated — stop the server and a blue link leads to ERR_CONNECTION_REFUSED. Plus _status_failed left that Open button enabled indefinitely, missing QPushButton#headerIconButton:disabled/:pressed rules (QSS specificity is CSS2.1, so the id selector outranked the bare pseudo-class rules), a setFixedSize(38,38) fighting a 36px QSS box, and a no-op setAlignment.

Release automation

pages.yml pinned four actions to mutable tags while every other workflow pins to SHAs, holding pages: write + id-token: write. Now pinned. release.yml had contents: write + actions: write workflow-wide (now job-scoped), selected the release request with head -n 1 over sorted paths (two requests published only the first; touching an old one would attempt a version downgrade), had no workflow_dispatch, and no timeouts on a job holding a global concurrency slot.

Tests

Three assertions were actively harmful and are fixed: header-brand-layout used [\s\S]* spanning the whole file, so deleting the property under test was satisfied by a copy elsewhere; header-status-indicator asserted drawerSource.includes("back"), unconditionally true (the file contains callback, fallback, background); test_layout_regression guarded a regression by exact local variable name, so reintroducing the footer button as open_btn would have passed.

Worth noting: AGENTS.md:14 and STABILITY_CONTRACT.md:52 both say "Prefer behavioral tests over source-text assertions" — and were added in the same range as the tests above.

Not fixed, deliberately

release.yml still pushes the version bump to main before any gate runs. That is what left v0.11.5 as a changelog entry with no tag and no release, and what forced nine hand-written recovery commits for v0.11.6. Fixing it means restructuring prepare/publish so the gate runs against the prepared commit before it reaches main — a real redesign of automation that can't be tested from here and has already wedged main twice. It should be done deliberately, not blind. The workflow_dispatch added here at least restores the manual recovery path that was missing.

Verification

No node/npm or pytest available. dist/ was hand-applied and the manifest regenerated (95 files, passing).

Ran directly: 26 fixture-free tests, 0 failures — 9 clipboard contract tests including a new one pinning the sentinel values, plus clipboard server-mode, toolbar layout, phase 9, desktop layout and Windows creation-kwargs.

Not run: anything needing PyQt, node, or a browser. None of the UI changes have been seen rendered.

One trap found along the way: .gitignore:125 has a blanket dist/ rule, so new build outputs are silently ignored while existing ones stay tracked. The two new files needed git add -f.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XfT5Z3Z24bjC8qtqERGpMQ

ami3go and others added 2 commits August 20, 2026 13:18
…nd release automation

Second review pass over the v0.11.6/v0.11.7 range, run as four sequential
workers over the areas the first pass did not reach.

Clipboard write-through (apps/web-client):

- The four status headlines the Copy button depends on are now defined once
  in src/clipboard-status.ts and imported by both controller.ts and main.ts.
  bindAndroidCopyWriteThrough recognises the controller's reply by matching
  the status line against exact strings, so rewording one at a setStatus call
  site silently stopped the Copy button writing to the PC clipboard -- with a
  green suite, because the existing tests pinned only the main.ts side.
- The execCommand fallback had no generation guard, so a superseded copy still
  stole focus and wrote its result over a newer request. It also copied the
  live textarea selection rather than the text it had validated, so the string
  placed on the clipboard could differ from the one the status claimed.
- On timeout the write-through overwrote whatever terminal status the
  controller had produced. A disconnect ("Stream stopped") or an over-limit
  clipboard ("Clipboard skipped ... exceeds N KiB") was replaced with "Copy
  not confirmed", swapping an accurate diagnosis for a wrong one. It now only
  claims failure when nothing else has explained the silence.
- Ctrl+C had no session guard, reinstating the "not confirmed" message that
  controller.ts deliberately suppresses when no device is attached.
- The fallback now restores focus to wherever it was instead of assuming the
  canvas, since parking focus there sends the next keystroke to Android.
- Added an event.repeat guard, and replaced a byte-for-byte copy of
  clipboardShortcut with an import of the tested original.

Drawer:

- openGroup forced focus to the mirrored canvas two frames after the Clipboard
  group opened. Its comment justified this by controller.ts focusing the
  fallback textarea -- a listener deleted when Ctrl+V moved to a document-level
  paste handler. The guard outlived its cause and now only steals focus from a
  user typing into the textarea, injecting their keystrokes into Android.
- The @import of droidwebdisplay-main-drawer-core.css carried no ?v=, leaving
  531 lines of drawer layout outside the cache-busting the entry sheet uses. A
  stale copy after an upgrade renders every drawer group stacked and visible.

Desktop:

- The Summary "Local URL" was a live hyperlink in every server state while the
  header Open button beside it was correctly gated, so stopping the server left
  a blue link that leads to ERR_CONNECTION_REFUSED. It is now plain text unless
  the server can serve it, and the colour comes from the palette instead of an
  inline literal that silently overrode the QLabel#summaryUrl rule.
- _status_failed left the header Open button enabled indefinitely, because only
  _apply_snapshot gated it and that path does not run on a probe failure.
- Added QPushButton#headerIconButton:disabled and :pressed. QSS specificity is
  CSS2.1, so the id selector outranked the bare pseudo-class rules and a
  disabled header button kept its enabled background with no press feedback.
- Removed setFixedSize(38, 38), which fought the 36px box in theme.py.
- Removed a no-op summary_layout.setAlignment(Qt.AlignTop): the one-argument
  form positions a layout within a parent layout item, and this one is
  installed directly on the card. addStretch(1) below is what actually works.

Release automation:

- pages.yml pinned four actions to mutable tags while every other workflow
  pins to a commit SHA, and it holds pages: write plus id-token: write. All
  four are now pinned.
- release.yml granted contents: write and actions: write workflow-wide. Both
  are now job-scoped; actions: write reaches only publish, which needs it.
  It also permits deleting the workflow runs and artifacts that constitute the
  gate evidence.
- The release request was selected with `head -n 1` over path-sorted output.
  Two request files in one push published only the alphabetically first, and a
  push touching an old request file would attempt a version downgrade. It now
  requires exactly one ADDED file and fails loudly otherwise.
- Added workflow_dispatch. prepare self-aborts once VERSION already matches, so
  a failed publish previously had no re-entry point -- which is why v0.11.6
  needed nine hand-written recovery commits.
- Added timeout-minutes to both jobs; publish holds a global concurrency slot
  and could hold it for the six-hour default.

Tests and docs:

- Two new tests were vacuous. header-brand-layout used [\s\S]* spanning the
  whole file, so deleting the property under test was satisfied by a copy of it
  elsewhere; both are now anchored to the rule body. header-status-indicator
  asserted drawerSource.includes("back"), which is unconditionally true because
  the file contains callback, fallback and background.
- test_layout_regression pinned the no-op setAlignment call and guarded a
  regression by exact local variable name, so reintroducing the footer button
  as `open_btn` would have passed. It now asserts the user-visible string.
- Removed an assertion on a message that no longer exists anywhere in the repo.
- docs/contracts/CLIPBOARD.md described a manual-Copy flow the code does not
  implement, and so failed to protect the one thing that keeps breaking. It now
  documents the real mechanism, why the write must stay inside the gesture, the
  status-string coupling, and the listener-ordering dependency.

verify_static_client returned FAIL on the committed tree before this change:
assets/main.js and assets/browser-support.js checksums were left stale by an
earlier commit. CI did not catch it because the gate runs npm run build first,
which rewrites both dist/ and the manifest. Regenerated, and the two new
clipboard-status files added, so the manifest now covers 95 files and passes.

NOT fixed, deliberately: release.yml still pushes the version bump to main
before any gate runs, which is what left v0.11.5 as a changelog entry with no
tag and no release. Fixing it means restructuring prepare/publish so the gate
runs against the prepared commit before it reaches main. That is a real
redesign of untestable-from-here automation that has already wedged main twice,
and it should be done deliberately rather than blind. The workflow_dispatch
added here at least provides the manual recovery path that was missing.

No node/npm or pytest available. dist/ was hand-applied and the manifest
regenerated. Executed directly and passing: 9 clipboard contract tests
(including a new one pinning the sentinel values), plus the clipboard
server-mode, toolbar layout, phase 9, desktop layout and Windows
creation-kwargs suites -- 26 fixture-free tests, 0 failures. Not run: anything
needing PyQt, node, or a browser. None of the UI changes have been seen
rendered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfT5Z3Z24bjC8qtqERGpMQ
My own tightened assertion was wrong: the control names are unquoted
object keys (`back: 'navigation'`), so requiring surrounding quotes
failed for every key except 'running-app-icon'.

Both loops now extract the object literal they are about --
`actionPaths` and `controlActions` -- and match a key within it. That
fixes the failure and also closes the original hole properly: searching
the whole 600-line file made `includes("back")` unconditionally true via
callback/fallback/background, and a bare `${action}:` matched any object
key anywhere. Verified in both directions: every real key matches, and
`{ fallback: 1, callback: 2, background: 3 }` does not satisfy "back".

running-app-select is asserted separately since it is bound directly
rather than through the controlActions map.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XfT5Z3Z24bjC8qtqERGpMQ
@ami3go
ami3go merged commit 2efe326 into main Aug 20, 2026
3 checks passed
@ami3go
ami3go deleted the fix/review-findings-v2 branch August 20, 2026 11:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant