5d16b5713dce1ea24952e54319ec714a77eee488
412
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5d16b5713d |
Give BuildKit the host's network, so it can reach the runner's cache
Secret Scan / scan (push) Successful in 4s
Build App (Preview) / compute-version (pull_request) Successful in 6s
Secret Scan / scan (pull_request) Successful in 6s
Build App (Preview) / create-release (pull_request) Successful in 2s
Build App (Preview) / build-macos (pull_request) Successful in 2m45s
Build App (Preview) / build-windows (pull_request) Successful in 4m45s
Build App (Preview) / build-linux (pull_request) Successful in 7m38s
Build App (Preview) / prune-previews (pull_request) Successful in 4s
Build Container / build-container (pull_request) Successful in 14m48s
The multi-arch build needs the `docker-container` driver — the plain `docker` driver cannot do linux/amd64+linux/arm64 — and that driver runs BuildKit in its own container on Docker's default bridge. act_runner advertises ACTIONS_CACHE_URL as an address the *job* container can reach, and nothing teaches the BuildKit container about it. So the job could reach 192.168.1.126:40649 while the container actually making the cache request could not. That is also why no other workflow here hit this: it is the only one using buildx. The rest make their cache calls from the job container act_runner set up. `no route to host` is EHOSTUNREACH — a firewall rejecting, not a missing route — which is what a default firewalld zone does to traffic from the docker bridge, and the runner registers under the stock RHEL/Fedora hostname. Sharing the host's namespace sidesteps it: the cache address becomes local to BuildKit. No effect on runners where this already worked. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0145mQi9NZiCDrznBUEEDE4n |
||
|
|
c02c02cbfc |
Never fail a container build because the cache was unreachable
Secret Scan / scan (push) Successful in 4s
Build App (Preview) / compute-version (pull_request) Successful in 3s
Secret Scan / scan (pull_request) Successful in 4s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-macos (pull_request) Successful in 2m41s
Build App (Preview) / build-windows (pull_request) Successful in 4m52s
Build App (Preview) / build-linux (pull_request) Successful in 8m0s
Build App (Preview) / prune-previews (pull_request) Successful in 2s
Build Container / build-container (pull_request) Successful in 10m21s
Every layer of both architectures built. The job then died exporting to act_runner's emulated GitHub Actions cache service, which it could not route to: `GetCacheEntryDownloadURL ... dial tcp 192.168.1.126:40649: no route to host`. On a pull_request `push:` is false, so this job pushes nothing and the cache is its only output — which means a network problem between the buildx `docker-container` builder and the runner host threw away a complete, successful validation of the Dockerfile on linux/amd64 and linux/arm64. A cache is an optimisation; it must degrade to "slow", never to "red". Only the exporter needs the flag. The import is already non-fatal — the build ran all 37 layers after warning it could not read the cache. This does not fix the routing itself, so builds stay uncached until that is sorted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0145mQi9NZiCDrznBUEEDE4n |
||
|
|
c0e4c87cec |
Give the mouse back, retire the follow controls, update Claude per session
Secret Scan / scan (push) Successful in 4s
Build App (Preview) / compute-version (pull_request) Successful in 3s
Secret Scan / scan (pull_request) Successful in 4s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-macos (pull_request) Successful in 2m44s
Build App (Preview) / build-linux (pull_request) Successful in 5m8s
Build App (Preview) / build-windows (pull_request) Successful in 6m28s
Build App (Preview) / prune-previews (pull_request) Successful in 2s
Build Container / build-container (pull_request) Failing after 14m49s
Three things the terminal was getting wrong. **A program that grabs the mouse and dies used to freeze the tab.** A TUI sets DECSET ?1000/?1002/?1003; if it exits without resetting them, xterm keeps routing clicks, drags and — under ?1003 — every pointer *move* to the PTY. Text selection dies and escape bytes flood the prompt. The only exit was closing the tab. `TerminalView` now reconciles a flag against `term.modes.mouseTrackingMode` in the `term.write()` callback — the mode only changes because the container printed a sequence, so one check per write catches every transition with no polling — and `Ctrl+Shift+X` or a status-bar button writes the resets back through `term.write`, never `sendInput`: the reset belongs to xterm's parser, and a still-live TUI told about it would just re-grab on its next repaint. The control is in the status bar deliberately. Mouse tracking is the *normal* state of htop, vim, lazygit and Claude Code, so a badge over the terminal would be on screen for the whole life of those programs and would swallow clicks aimed at their own top-right corner. `macOptionClickForcesSelection` is also on now: xterm's force-select is Shift everywhere except macOS, where it is Option and is gated behind that option, which defaults to false — so until now Mac users had no way to select text while a program held the mouse. **"Following" and "Jump to Current" are gone.** Claude Code draws on the alternate screen, which has no scrollback, so `viewportY` always equalled `baseY` and neither control could do anything. They did still work in bash tabs; xterm's native follow covers that, and the per-write `scrollToBottom()` went with them because it fought exactly that. What remains, on activate and after a refit, now samples `viewportY >= baseY` *before* the fit, so opening the Notes dock no longer yanks a reader to the tail. **`claude update` runs before every Claude session, not just at container start.** Containers here stop/start and often just keep running, so a long-lived one never re-checked. Both copies take the same flock: the entrypoint prints "container ready" only after its own update finishes, so opening a tab immediately would otherwise run two updaters against the same ~/.claude/bin, with `|| echo` hiding a half-written install one line before `exec claude` ran it. This turns the non-Bedrock path from a bare argv into a `bash -c` wrapper, so flags and session names are shell-interpolated now and must go through `shell_quote_arg`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0145mQi9NZiCDrznBUEEDE4n |
||
|
|
3aec2998d8 |
Merge pull request 'Anchor the update channel tag, and stop shipping a duplicate AppImage' (#52) from fix/update-channel-durability into main
Build App / compute-version (push) Successful in 3s
Secret Scan / scan (push) Successful in 4s
Build App / build-macos (push) Successful in 2m44s
Build App / build-linux (push) Successful in 4m48s
Build App / build-windows (push) Successful in 4m52s
Build App / create-tag (push) Successful in 4s
Build App / sync-to-github (push) Successful in 7s
|
||
|
|
019fb403d5 |
Merge remote-tracking branch 'origin/main' into fix/update-channel-durability
Secret Scan / scan (push) Successful in 4s
Build App (Preview) / compute-version (pull_request) Successful in 3s
Secret Scan / scan (pull_request) Successful in 3s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-macos (pull_request) Successful in 2m42s
Build App (Preview) / build-linux (pull_request) Successful in 4m51s
Build App (Preview) / build-windows (pull_request) Successful in 4m54s
Build App (Preview) / prune-previews (pull_request) Successful in 1s
|
||
|
|
b21a568bf5 |
Merge pull request 'Install from the lockfile, so CI cannot be broken by someone else's release' (#53) from fix/ci-npm-lockfile into main
Build App / compute-version (push) Successful in 4s
Secret Scan / scan (push) Successful in 3s
Build App / build-macos (push) Successful in 2m42s
Build App / build-windows (push) Successful in 4m53s
Build App / build-linux (push) Successful in 5m0s
Build App / create-tag (push) Successful in 3s
Build App / sync-to-github (push) Successful in 13s
|
||
|
|
f41b1d9054 |
Install from the lockfile, so CI cannot be broken by someone else's release
Secret Scan / scan (push) Successful in 4s
Build App (Preview) / compute-version (pull_request) Successful in 3s
Secret Scan / scan (pull_request) Successful in 3s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-macos (pull_request) Successful in 2m40s
Build App (Preview) / build-windows (pull_request) Successful in 4m52s
Build App (Preview) / build-linux (pull_request) Successful in 5m0s
Build App (Preview) / prune-previews (pull_request) Successful in 2s
`build-linux` fails before `tauri build` runs, on every workflow, at "Install
frontend dependencies":
npm error Cannot read properties of null (reading 'edgesOut')
Reproduced exactly on the first attempt by running the step's own commands
locally on the same Node 22.23.2 the runner installs. The debug log gives the
frame the CI output omits:
at #loadPeerSet (.../@npmcli/arborist/lib/arborist/build-ideal-tree.js:1289:38)
It is a null dereference in npm 10.9.8's peer-set resolver, reached through
vite → @vitejs/devtools → @vitejs/devtools-vitest → vitest@* →
@vitest/browser-playwright → vitest@4.1.11 → jsdom@* → canvas.
**Nothing in this repo changed to cause it.** The step deleted
`package-lock.json` before installing, so every build re-resolved the entire
tree against the registry against ranges like `vitest@*`. A dependency
published a version that produces a peer graph npm cannot resolve, and our CI
broke — the same command succeeded fifteen hours earlier for 0.4.21. That is
the real defect: the build was never reproducible, and the crash is only how we
found out.
So Linux installs with `npm ci`, from the committed lockfile, like Windows
already did. macOS moves too — it kept the lockfile but still ran `npm
install`, which is free to re-resolve; all three platforms now install
identically and none can re-resolve mid-release.
**The reason the lockfile was being deleted is obsolete, not ignored.**
|
||
|
|
d38736007f |
Take the re-review: distinguish "absent" from "unreachable"
Secret Scan / scan (push) Successful in 3s
Build App (Preview) / compute-version (pull_request) Successful in 3s
Secret Scan / scan (pull_request) Successful in 4s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-linux (pull_request) Failing after 2m3s
Build App (Preview) / build-macos (pull_request) Successful in 2m41s
Build App (Preview) / build-windows (pull_request) Successful in 4m56s
Build App (Preview) / prune-previews (pull_request) Skipped
Second review of this branch. Two blockers and one real defect I had papered over with a true-but-misleading claim. **`make_latest` was missing from the republish path.** The create path sends `"make_latest": "false"` so the channel cannot displace the versioned release on the releases page. The reuse path — taken on every run after the first — omitted it, and the API's documented default for a publish transition is `true`. So the second release would have quietly promoted `linux-latest` to the repository's Latest release: a release whose own body says "for a specific version, use the versioned releases instead". Now sent on both paths. `tag_name` is re-sent deliberately and now says so in a comment — the API removes the tag when a PATCH omits it, and this branch exists because a tag disappeared. **A transient Gitea error would have cost the whole release.** `curl -sf` fails identically for "404, the tag is genuinely absent" and "503, Gitea is briefly unreachable", and both landed in the create branch. Creating a tag that already exists returns 409, which aborted the last step of `build-linux` — and `create-tag` and `sync-to-github` both depend on it, so no version tag and no GitHub sync at all. The failure message also read "the tag does not exist" when Gitea had merely been unreachable. Now a `case` on the HTTP code — 200 leave alone, 404 create, anything else fail loudly with the real code — the same idiom `Upload to Gitea release` already uses two steps above. `422 already_exists` on the release POST is likewise a recoverable answer, not a reason to lose a release. **The empty `Categories=` was still shipping, and my claim hid it.** I wrote that the guard "asserts the absence of an empty value rather than the presence of any filled one" — true of the regex, false of the artifact. The AppDir root `.desktop` is a *symlink* into usr/share/applications, so `sed -i` replaced the link with a regular file and left the real entry empty; the guard globbed the root only, so it saw the copy it had just written and passed. Verified on the real artifact: two divergent entries, and the one that shipped was empty. Fixed with `--follow-symlinks`, both locations globbed, and the guard turned into a positive assertion over every entry — which also closes its missing-key and unmatched-glob holes. Both entries now read `Categories=Development;Utility;`. Also taken: the duplicate-AppImage check moves to a precondition, since as a post-mortem it let the script repack and overwrite the versioned artifact before failing, and it silently selected by glob order, i.e. the older version — it now refuses in under a second; assets are deleted and re-uploaded one at a time, because deleting both up front left a fresh AppImage with no .zsync if the second upload failed, which silently stops every client; and the success line no longer claims a fallback was kept when there was nothing to demote. Left as informational, with the reasoning recorded rather than acted on: `--retry-all-errors` retries permanent 4xx (fail-closed, matches the repo's other upload steps); the release list is unpaginated (a GraphQL lookup by pending tag name is the durable fix, but 7 releases is decades from the cliff, and the 422 handling above covers the failure mode); process-substitution failure is invisible to `mapfile` (fail-closed downstream). Verified against the real 0.4.19 artifact — happy path, no AppImage, two AppImages, and an AppDir rebuilt with the bundled library removed. shellcheck clean at warning level on both scripts. appimagetool now reports the AppStream metadata found. Nothing here is CI-proven, and that is worth stating plainly: `build-linux` fails on this branch before `tauri build` even runs, at "Install frontend dependencies" with `npm error Cannot read properties of null (reading 'edgesOut')` — confirmed in the logs of jobs 5644 and 5636. Unrelated to this change and tracked separately, but it means the finalizer has never executed in CI on either commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011YPqHpjV4EL6RNEwrRKqQm |
||
|
|
63f282bef6 |
Fix the review findings: never destroy a working anchor
Secret Scan / scan (push) Successful in 4s
Build App (Preview) / compute-version (pull_request) Successful in 3s
Secret Scan / scan (pull_request) Successful in 4s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-linux (pull_request) Failing after 1m49s
Build App (Preview) / build-macos (pull_request) Successful in 2m41s
Build App (Preview) / build-windows (pull_request) Successful in 4m55s
Build App (Preview) / prune-previews (pull_request) Skipped
An adversarial review of the previous commit found six real problems and corrected one of my claims. Taking all of it. **The anchoring could kill the channel it exists to protect.** It did DELETE-then-POST so the tag would name the current build. If the POST failed for any transient reason the script aborted having already deleted the anchor a previous run put there, and the next mirror run pruned GitHub's copy — a transient Gitea error converting a healthy channel into a dead one, which is strictly worse than the step not existing. There was also a real window between the two calls with no tag at all. The DELETE bought nothing. The update string resolves the tag by *name* and the assets hang off the release object, so nothing about the channel depends on which commit the tag points at; moving it changes only the source-zip link. It existed solely to get past a 409, since Gitea's POST /tags has no force semantics. Now the tag is created if absent and otherwise left alone, which removes the window too. **My "no window where the two disagree" claim was wrong, and it is the third time in this area I have asserted something I had not established.** The release POST sets no `target_commitish`, so GitHub creates its tag at its own default-branch HEAD, not at `GITEA_SHA`; the two agree only because `sync_on_commit` pushes main minutes earlier. And the DELETE actively created the window. What the ordering genuinely buys is narrower: if anchoring fails, the script aborts before creating a GitHub release that would be orphaned. **Orphaned drafts were invisible to the release lookup.** GitHub demotes a release to a draft when its tag is deleted, and `/releases/tags/` never returns drafts — precisely the state every mirror run left behind. The by-tag lookup reported "absent" while 86 MB drafts accumulated, one per release. The lookup now reads the authenticated list, republishes the newest, and deletes the rest. **A guard that could not catch what it named.** The update-info assertion was a substring match on the tag, so it passed for a wrong host, path, filename or transport — verified: an `evil.example.com/.../linux-latest/...` string passes the old check and fails the new one. Now a fixed full-string match. Also from the review: an absent bundled library no longer exits early, because that skipped the metadata *and* left `update-channel/` uncreated, killing the publish step on a missing directory and taking the tag and mirror jobs with it; the Categories guard asserts the absence of an empty value rather than the presence of any filled one; the channel directory is cleared before use so a stale zsync cannot satisfy an existence check while describing the previous build; the AppImage count uses a glob array, since `ls | wc -l` aborted under pipefail before the message it promised could print; uploads carry the retry/http1.1 hardening this repo's other upload steps already learned to need; verification compares served size against built size, because a status code only proves something is served; and the release workflow now fails on empty artifacts instead of publishing a release with no AppImage. The metainfo file is installed as `Triple-C.appdata.xml`. appimagetool derives the name it looks for from the .desktop basename, so under the id-based name it warned the metadata was missing on every build while this script reported it present. Now it prints "AppStream upstream metadata found in usr/share/metainfo/Triple-C.appdata.xml" — the AppStream id inside the file is unchanged and is what identifies the component. Two review hypotheses did not hold and nothing was changed for them: `set -e` does not abort on a failing `&&` list mid-script, and my claim of a `trap` reassignment was wrong — there is one trap, installed once. Verified against the real 0.4.19 artifact: exit 0, one AppImage beside the release, channel pair in its own directory, appimagetool reporting the metadata found, and the wayland fallback intact. Guards exercised individually — the duplicate one bites, the exact-match one rejects an impostor carrying the tag, the empty directory reports cleanly, and all four publisher preconditions refuse rather than half-publishing. Header parsing for the size check was tested against a real redirecting GitHub asset URL. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011YPqHpjV4EL6RNEwrRKqQm |
||
|
|
d561ce03d5 |
Anchor the update channel tag, and stop shipping a duplicate AppImage
Secret Scan / scan (push) Successful in 6s
Build App (Preview) / compute-version (pull_request) Successful in 3s
Secret Scan / scan (pull_request) Successful in 4s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-linux (pull_request) Failing after 1m49s
Build App (Preview) / build-macos (pull_request) Successful in 2m57s
Build App (Preview) / build-windows (pull_request) Successful in 16m16s
Build App (Preview) / prune-previews (pull_request) Skipped
Two defects in the update channel, both visible in 0.4.20 and 0.4.21. **The channel tag does not survive.** `publish-update-channel.sh` created the GitHub release, uploaded both assets and verified each URL returned 200 — the job log shows it succeeding at 00:38. By 13:04 the tag was gone and every installed copy was checking a 404. Gitea push-mirrors this repo to GitHub every four hours, and a mirror push deletes remote refs with no local counterpart. `linux-latest` was created by GitHub's release API and never existed as a Gitea tag, so the mirror removed it. Versioned tags were never affected because `create-tag` creates them in Gitea first. So the tag is now anchored in Gitea, and before the GitHub release rather than after, so there is no window where the two disagree. Its absence fails the step instead of warning, because it is the only thing keeping the channel alive. Worth stating plainly: publishing correctly is not evidence the channel still works, and the verification that passed at 00:38 could not have caught a failure that arrives twelve hours later. **Every release carried the AppImage twice.** The channel's stable-named copy sat beside the versioned one, where the release job's `*.AppImage` glob picked it up — so v0.4.21 published `Triple-C_0.4.21_amd64.AppImage` and `Triple-C_x86_64.AppImage`, byte-identical at 86,686,200 bytes each, and `sync-to-github` copied both to the mirror. 80 MB of duplicate per release, under a name that reads like a different build. That is how it was noticed. The channel pair now lives in `bundle/appimage/update-channel/`, out of the glob's reach, and a guard fails the build if more than one AppImage is left beside the release. Verified by planting a second one: it fails. One appimagetool quirk found while moving it — zsyncmake writes the .zsync into the working directory, not beside the image it describes, so it has to be collected rather than assumed in place. The existing guard caught that too. Verified against the real 0.4.19 artifact: exactly one AppImage at top level, the channel pair in its own directory, update string still resolving to the fixed tag, and the wayland fallback intact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011YPqHpjV4EL6RNEwrRKqQm |
||
|
|
670450ccfd |
Merge pull request 'Make the AppImage updatable, and drop the deb and rpm' (#51) from feat/appimage-update-metadata into main
Build App / compute-version (push) Successful in 2s
Secret Scan / scan (push) Successful in 3s
Build App / build-macos (push) Successful in 2m48s
Build App / build-windows (push) Successful in 4m53s
Build App / build-linux (push) Successful in 5m11s
Build App / create-tag (push) Successful in 4s
Build App / sync-to-github (push) Successful in 11s
|
||
|
|
a0b9f1e19b |
Merge pull request 'Let the host's libwayland-client win in the AppImage' (#50) from fix/appimage-wayland-client into main
Build App / compute-version (push) Successful in 3s
Secret Scan / scan (push) Successful in 4s
Build App / build-macos (push) Successful in 2m42s
Build App / build-windows (push) Successful in 4m54s
Build App / build-linux (push) Successful in 5m32s
Build App / create-tag (push) Successful in 3s
Build App / sync-to-github (push) Successful in 12s
Reviewed-on: #50v0.4.20-mac v0.4.20-win v0.4.20 |
||
|
|
9fadfbc37a |
Make the AppImage updatable, and drop the deb and rpm
Secret Scan / scan (push) Successful in 4s
Build App (Preview) / compute-version (pull_request) Successful in 4s
Secret Scan / scan (pull_request) Successful in 3s
Build App (Preview) / create-release (pull_request) Successful in 2s
Build App (Preview) / build-macos (pull_request) Successful in 2m43s
Build App (Preview) / build-windows (pull_request) Successful in 4m51s
Build App (Preview) / build-linux (pull_request) Successful in 5m19s
Build App (Preview) / prune-previews (pull_request) Successful in 1s
An AppImage manager can adopt the current build but never update it: the image carries no update information, which is the string that tells such a tool where to look for a newer one. It also carries no AppStream metadata, so a manager has nothing to show but a filename — appimagetool has been warning about that on every build — and linuxdeploy leaves `Categories=` empty, which files the app nowhere in a desktop menu. All three are fixed while the image is already unpacked for the wayland fix, so the cost is a few lines rather than a second pass. `unbundle-wayland-client.sh` is now `finalize-appimage.sh`, since it does more than unbundle. The update URL is a **fixed** `linux-latest` tag on the GitHub mirror, which is where updates are pulled from — deliberately not `releases/latest`. `latest` follows whichever release is newest, and the Gitea-to-GitHub backfill creates one GitHub release per Gitea tag, including the `-win` and `-mac` tags that carry no AppImage. A URL that can resolve to a release with no AppImage in it fails on users' machines and nowhere else. The output is named for that tag too, and that is not cosmetic: zsync records a *relative* filename which a client resolves against the .zsync URL it fetched, so a versioned name would send every client after the build it already has. Verified by reading the generated header — `Filename: Triple-C_x86_64 .AppImage` — and the image's own `.upd_info` section, which is where the tag actually lives. My first guard checked the .zsync for the tag and failed correctly, which is how that distinction got found rather than shipped. Range requests were confirmed against the mirror before building on them: 206 with a correct content-range, so updates are real deltas rather than an 85 MB re-download. The .deb and .rpm go. They are two more artifacts to build, publish and keep working for an audience already served by the one file that runs on every distribution, and neither could ever self-update — which is now the difference that matters. Older releases keep theirs. The Linux job passes `--bundles appimage` rather than changing `tauri.conf.json`, so macOS and Windows are untouched. Verified against the real 0.4.19 artifact: it repacks, the AppStream file and filled-in Categories land inside the image, the update string resolves to the fixed tag, and the wayland fallback still holds. Both publisher failure paths refuse rather than half-publishing — no token, and missing artifacts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011YPqHpjV4EL6RNEwrRKqQm |
||
|
|
a3bdf6f4da |
Let the host's libwayland-client win in the AppImage
Secret Scan / scan (push) Successful in 3s
Build App (Preview) / compute-version (pull_request) Successful in 3s
Secret Scan / scan (pull_request) Successful in 3s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-macos (pull_request) Successful in 2m52s
Build App (Preview) / build-linux (pull_request) Successful in 5m20s
Build App (Preview) / build-windows (pull_request) Successful in 5m21s
Build App (Preview) / prune-previews (pull_request) Successful in 1s
The AppImage came up blank on CachyOS with `Could not create default EGL display: EGL_BAD_PARAMETER. Aborting...`, and the DMA-BUF workaround already in `main.rs` did not help — verified by finding the flag compiled into the shipped 0.4.19 binary, where it runs unconditionally on Linux. It is a different fault with the same error text. linuxdeploy bundles `libwayland-client.so.0` as a GTK dependency and `AppRun.wrapped` puts the bundled directory ahead of the host's, so the host's Mesa resolves against our copy. `libEGL_mesa.so.0` — the driver libglvnd's `libEGL.so.1` dlopens — has a hard DT_NEEDED on that library, so when its symbols will not resolve the driver never loads, glvnd is left with none, and `eglGetDisplay` reports no display. That is why `GDK_BACKEND=x11` does not dodge it, and why the symptom is a bad-parameter error rather than a link failure. Bisected on the reporter's machine against the released artifact — removing `libwayland-client.so.0` from the AppDir cleared the abort, while removing `libwayland-egl` or `libepoxy` did not. The bundled copy (Ubuntu 22.04, wayland 1.20) is missing eleven symbols their wayland 1.26 exports, including `wl_proxy_get_display`, `wl_proxy_get_queue`, `wl_display_create_queue_with_name` and `wl_fixes_interface`. Bundling a newer wayland would defer this, not fix it: the floor is set by the host's Mesa, which updates independently of our releases, so any version we pick is one release away from being too old again. This is a host-coupled library like libGL and libdrm — the only correct version is the host's. So the copy is demoted rather than deleted. It moves off the loader path into `usr/lib/wayland-fallback`, and a hook adds that directory back only when the host has no libwayland-client of its own — so a host without one still starts. The ordering is safe because `AppRun.wrapped` appends the inherited LD_LIBRARY_PATH after its own entries, making the hook a fallback and never an override. AppRun sources hooks by name rather than globbing, so it is patched to source this one. X11-only hosts are unaffected: libwayland-client is a package dependency of Mesa and GTK, so it is present even on a machine with no display server at all — confirmed on a headless container with neither DISPLAY nor WAYLAND_DISPLAY set. And the AppImage already runs as an X11 client everywhere, since linuxdeploy's own hook forces GDK_BACKEND=x11. Verified against the real 0.4.19 artifact rather than a synthetic AppDir: it repacks, the binary and AppRun survive, and both hook branches were exercised — host-has-it leaves LD_LIBRARY_PATH untouched, and a debian:12-slim container with no wayland at all engages the fallback. The comment in `main.rs` quoted this exact error as one the DMA-BUF flag fixes. That claim sent this investigation down the wrong path first, so it is corrected rather than left to do it again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011YPqHpjV4EL6RNEwrRKqQm |
||
|
|
dc9cdd1760 |
Merge pull request 'Add a per-project Notes tab with a send-to-agent action' (#48) from feat/project-notes into main
Build App / compute-version (push) Successful in 4s
Secret Scan / scan (push) Successful in 5s
Build App / build-macos (push) Successful in 2m44s
Build App / build-windows (push) Successful in 4m58s
Build App / build-linux (push) Successful in 5m13s
Build App / create-tag (push) Successful in 4s
Build App / sync-to-github (push) Successful in 10s
|
||
|
|
c16f0d5b70 |
Put the cursor in the terminal after sending a note
Secret Scan / scan (push) Successful in 6s
Build App (Preview) / compute-version (pull_request) Successful in 3s
Secret Scan / scan (pull_request) Successful in 4s
Build App (Preview) / create-release (pull_request) Successful in 2s
Build App (Preview) / build-macos (pull_request) Successful in 2m44s
Build App (Preview) / build-windows (pull_request) Successful in 5m3s
Build App (Preview) / build-linux (pull_request) Successful in 5m21s
Build App (Preview) / prune-previews (pull_request) Successful in 1s
Sending already switched to the target terminal's tab, which looks like it should be enough: `TerminalView` focuses xterm whenever a terminal becomes active. But that effect keys off `active`, so it only fires on a *change* — and the dock's ordinary case is sending to the terminal already on screen. `setActiveTabKey` writes the key that is already set, nothing changes, no effect re-runs, and focus stays on the Send button. The note is sitting in the prompt and the user still has to click the terminal before pressing Enter. So the send now asks for focus explicitly, through a one-shot request in the store that `TerminalView` consumes and clears — the shape `pendingHomeTab` already uses. Clearing is not tidiness: hold the id and the second send to the same terminal writes a value that is already there, which is precisely the no-op this exists to fix. Focus is requested only on success. A failed send toasts and leaves the user where they are, because there is nothing in the prompt to press Enter on. The three `TerminalView` tests give focus away after mounting before making any assertion, so what they observe is the request landing and never the focus that `active` already grants on mount — which would pass with the feature absent. 752 tests pass, 62 files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011YPqHpjV4EL6RNEwrRKqQm |
||
|
|
3239057f8f |
Give the dock its own compact notes layout
Secret Scan / scan (push) Successful in 5s
Build App (Preview) / compute-version (pull_request) Successful in 3s
Secret Scan / scan (pull_request) Successful in 3s
Build App (Preview) / create-release (pull_request) Successful in 1s
Build App (Preview) / build-macos (pull_request) Successful in 2m44s
Build App (Preview) / build-windows (pull_request) Successful in 4m55s
Build App (Preview) / build-linux (pull_request) Successful in 5m29s
Build App (Preview) / prune-previews (pull_request) Successful in 1s
The dock was showing `NotesPanel`, which is a master/detail layout: a column of titles beside an editor. The previous commit made that survive dock width; it did not make it right. At 352px the layout still spends roughly 356px of height on chrome — dock header, panel header, title strip, a button row that wraps, and a paragraph of help — before the body gets a pixel. So the dock now shows one note. The title field names what is open and the chevron beside it switches; New and Delete move into the overflow menu; the help text goes. Chrome drops to about 112px and the body takes the rest. The two surfaces are now different components, which contradicts a docstring I wrote — "shared so the two cannot drift into different behaviour". That claim was about behaviour, and behaviour was never in the layout: it is in `useNotes` for the cache and its write ordering, and now in `useNoteDraft`, extracted here so when a keystroke becomes a save is defined in exactly one place. Only the layout diverges. `NotesPanel.shared.test.tsx` gets stronger for it — it now mounts the dock panel and the tab panel together, which is what the app actually does, instead of the same component twice. `NoteSwitcher` is not `OverflowMenu` despite the shape being close: that keys items by label, and notes are addressed by id, so two untitled notes — the ordinary case — would collapse into one row. It is also not a `combobox`; an input plus a listbox button is two honest controls, where the role would owe active-descendant tracking and filtering that nothing here needs. `SendToAgentButton` picks up `useUnavailable` from #49, which is what its `disabled` plus explanatory `title` was already asking for. Four tests moved from `toBeDisabled()` to the new contract, and one of them — "does nothing for an empty note" — turned out never to have asserted that it does nothing. It does now, for click and for Enter, which is the guard the swap needs. It also gains `dropUp`, and that is load-bearing rather than cosmetic: the dock clips its own overflow, so a session menu opening downward from a button on the bottom edge is drawn outside the panel and never seen. 744 tests pass, 62 files. As before, jsdom has no layout engine: that the dock now reads as compact is not something the suite can tell you. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011YPqHpjV4EL6RNEwrRKqQm |
||
|
|
23364f412e | Merge remote-tracking branch 'origin/main' into feat/project-notes | ||
|
|
b24807bd5f |
Merge pull request 'Keep disabled controls in the accessibility tree so their reason is announced' (#49) from fix/disabled-control-accessibility into main
Build App / compute-version (push) Successful in 5s
Secret Scan / scan (push) Successful in 3s
Build App / build-macos (push) Successful in 2m44s
Build App / build-windows (push) Successful in 4m55s
Build App / build-linux (push) Successful in 5m20s
Build App / create-tag (push) Successful in 4s
Build App / sync-to-github (push) Successful in 10s
Reviewed-on: #49v0.4.18-mac v0.4.18-win v0.4.18 |
||
|
|
0f3fff92f4 |
Lay the notes panel out by its own width, not the window's
Secret Scan / scan (push) Successful in 5s
Build App (Preview) / compute-version (pull_request) Successful in 4s
Secret Scan / scan (pull_request) Successful in 5s
Build App (Preview) / create-release (pull_request) Successful in 3s
Build App (Preview) / build-macos (pull_request) Successful in 2m45s
Build App (Preview) / build-windows (pull_request) Successful in 4m58s
Build App (Preview) / build-linux (pull_request) Successful in 5m13s
Build App (Preview) / prune-previews (pull_request) Successful in 1s
The panel splits master/detail unconditionally: a 192px title column beside the editor. That fits the Project Home tab and does not fit the dock. At the dock's 352px default the editor gets 157px, and its action row wants ~200px, so the Delete button lands outside the dock's `overflow-hidden` with no scrollbar to reach it, and the textarea collapses to a two-word column. The two surfaces differ in width while sharing a viewport, so this is a container query rather than a `md:` breakpoint — a viewport query reads the window and hands both surfaces the same answer, which is wrong for one of them. Tailwind v4 has these in core; verified as real `@container (min-width: 32rem)` rules in the built CSS, since a variant that silently compiles to nothing looks identical in review. The threshold is arithmetic: side by side needs the 192px list, an editor wide enough for its own buttons (~280px), and the divider. `@lg` (512px) is the first stop clearing ~473px. Below it the titles become a capped strip above the editor, so the note being written keeps the height. The action row now wraps, which is the part that holds at *any* width rather than on one side of a threshold: the buttons are a group that does not shrink, the title field shrinks to 96px, and past that the title takes one row and the buttons the next. Nothing can be pushed out of the panel. Not covered by the suite — jsdom has no layout engine, so 711 tests pass before and after. This needs eyes on the dock at its minimum, default and maximum widths. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011YPqHpjV4EL6RNEwrRKqQm |
||
|
|
1eb91a35eb |
Give the terminal and Add Project buttons a reason a screen reader can hear
Secret Scan / scan (push) Successful in 10s
Build App (Preview) / compute-version (pull_request) Successful in 6s
Secret Scan / scan (pull_request) Successful in 10s
Build App (Preview) / create-release (pull_request) Successful in 3s
Build App (Preview) / build-macos (pull_request) Successful in 2m41s
Build App (Preview) / build-windows (pull_request) Successful in 4m56s
Build App (Preview) / build-linux (pull_request) Successful in 6m47s
Build App (Preview) / prune-previews (pull_request) Successful in 1s
Both were the defect the new hook exists for. The sidebar's Claude terminal button is disabled whenever the container is not running and never said so — its `title` names the action, so the precondition appeared nowhere in the accessibility tree at all. Add Project's submit button is disabled while an add is in flight, and its only signal is the label swapping to "Adding…" on an element a screen reader can no longer reach. The submit button needs a second guard the hook cannot supply: Enter inside a text field submits a form without touching the submit button, so `handleSubmit` now returns early while loading. Without it, swapping `disabled` for `aria-disabled` would have turned an accessibility fix into a double-submit bug. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011YPqHpjV4EL6RNEwrRKqQm |
||
|
|
aa0a574091 |
Announce unavailable controls instead of hiding them behind disabled
Native `disabled` removes an element from the tab order and from the accessibility tree, so any explanation of why a control cannot be used is delivered only to a sighted user with a mouse. `useUnavailable` is the way out: `aria-disabled` keeps the control focusable and announced, `aria-describedby` carries the reason, and — because `aria-disabled` is advisory and blocks nothing — the hook hands back the click and Enter/Space guards along with the attributes, so a call site cannot take the announcement without the guard. `Button` gets it as an opt-in `unavailable` / `unavailableReason` pair. Opt-in matters: 37 files render this button and none of them change. The `aria-disabled:` class mirrors exist because Tailwind's `disabled:` variant only matches the native attribute, which this pattern deliberately omits. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011YPqHpjV4EL6RNEwrRKqQm |
||
|
|
2708772bf9 |
Order the notes cache by sequence, not by who resolves last
Secret Scan / scan (push) Successful in 4s
Build App (Preview) / compute-version (pull_request) Successful in 5s
Secret Scan / scan (pull_request) Successful in 6s
Build App (Preview) / create-release (pull_request) Successful in 4s
Build App (Preview) / build-macos (pull_request) Successful in 2m48s
Build App (Preview) / build-linux (pull_request) Successful in 6m2s
Build App (Preview) / build-windows (pull_request) Successful in 6m4s
Build App (Preview) / prune-previews (pull_request) Successful in 4s
One gesture puts two requests in flight. With the tab already loaded, clicking the dock toggle while the textarea has focus fires `blur` -> `saveNote` and the dock's mount -> `list_notes` in the same tick. The save finishes and its re-read writes the post-save list; the mount's read -- issued earlier, still out -- then lands its pre-save snapshot on top, and both panels show stale text until something else refreshes. `mutationChains` could not have caught this: it orders a project's writes against each other and the mount load is a read outside it. Putting the read on the chain would work, but it buys correctness with latency the user feels -- a panel mount waiting behind `save_note`'s double-fsync write -- and leaves a "mutation chain" holding reads. The two requests are not competing for a resource; the loser's result is simply older. So every write into `notesByProject[p]` now claims a per-project sequence when the request behind it is issued, and `commitNotes` drops one whose sequence predates what is already cached. Reads take their sequence at issue time, since being ordered by resolution is the bug. Local patches -- the filter behind a confirmed delete, the prepend behind a failed re-read -- take a fresh one at commit time, because they are authoritative then rather than derived from an earlier read, and anything still in flight behind them is genuinely stale. A failed read commits under its *own* sequence, not a fresh one, so its empty list cannot beat a later read that has the real answer. `isCurrent()` stays, and is not folded in. It guards `setSaveState`, not the cache: it asks whether this *panel* is still showing the project a save was made for, which a per-project counter cannot answer -- two panels on one project share every sequence value. Ordering and panel identity are two questions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HjL1E2JFNctUqCYotUwqqb |
||
|
|
436b6dd470 |
Send a lone CR through the newline transform, and say what pinned is
`toClaudePayload` matched `/\r?\n/`, so a bare CR that is not part of a CRLF went through verbatim — and a bare CR *submits* in a Claude prompt and *runs* the line in a shell, which is the terminator the function's own contract says it never appends. A `<textarea>` cannot produce one, but `load_in` returns whatever a hand-edited or externally written notes file holds, so the guarantee has to cover that rather than only what the editor can type. `Note.pinned` is persisted and sorted on, but nothing in the app sets it: there is no pin control and no indicator. The spec stated the ordering rule as though pinning existed and §8 did not list it, so the spec is amended to say `pinned` is reserved and inert in v1, and pinning is added to the out-of-scope list. No UI is added — a user-facing affordance does not belong in a fix wave. Also renamed NotesPanel.test.tsx's "deletes the selected note and falls back to another": `useNotes` is mocked in that file and the mocked list never changes, so the fallback was never exercised. A name that claims coverage which is absent is worse than an absent test, because it makes the gap invisible. The real assertion now lives against the real hook in NotesPanel.shared.test.tsx. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HjL1E2JFNctUqCYotUwqqb |
||
|
|
5c47656444 |
Cache notes in one place, and serialise a project's writes
Implements the design spec's §2 — "notes cached in zustand keyed by
project id" — which the plan substituted with a hook-local `useState`.
Sharing the `NotesPanel` *component* between the Project Home sub-tab and
the dock did not share the *cache*. Both resolve to the same project, so
two panels mount two `useNotes(P)`, each with its own list. Edit a note
in the dock and blur; the tab's copy is still pre-edit, and the tab's
next blur commits `{...staleRecord, title, body}` — the dock's edit gone
from disk with no error and no indicator. That is the feature's own
primary workflow: take notes in the dock while the agent runs, which is
the reason the dock exists, then go back to the tab.
`notesByProject` plus a per-project in-flight flag now hold the list.
Both surfaces render from one array; two panels mounting for one project
make one read; and because the write is keyed by project, a response
that lands after the user has moved on updates the project it belongs to
rather than whichever is on screen. This is also the boundary §8 says a
detached notes window needs.
Three more bugs in the same code, fixed with it:
- Delete-after-edit could resurrect the note. Clicking Delete with the
textarea focused fires blur first, so `save_note` and `delete_note` go
out back to back; Rust's `write_lock` stops them interleaving but does
not order them, and a delete that wins the lock is undone by the
upsert behind it. A project's mutations now go through one promise
chain, module-scoped for the reason `useTerminal`'s input queue is.
- An unsaved draft vanished when any other note was saved, because the
re-read replaced the list with the backend's. "New note" now persists,
so the backend owns the row from the start — chosen over merging local
drafts because a local-only row in a *shared* cache would exist in the
panel that made it and nowhere else.
- The save outcome was reported for the wrong project after a switch:
the guard covered only the list replacement, so the new project's
SaveIndicator flashed "Saved ✓" for the old project's write. The
indicator now resets on a project change and reports only its own.
`NotesPanel` also re-seeds its draft when the *stored* text of the note
it has selected changes, so an edit made in the other surface reaches
the editor and not only the list. It never overwrites something
half-typed; that still blurs into a last-writer-wins save, as any
blur-commit editor does.
NotesPanel.shared.test.tsx is the configuration none of the existing
tests had: two panels, one project, the real hook. Four of its six
assertions fail against the previous implementation.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HjL1E2JFNctUqCYotUwqqb
|
||
|
|
be47c5edfd |
Cap the corrupt-notes copies and put a version envelope on disk
Two things the plan dropped from the design spec's §1.
`keep_corrupt_copy`'s only guard was "does this second's copy already
exist", so a persistently unparseable file minted a full copy of the
user's prose every time the clock ticked over — and `list_notes` runs on
*every* NotesPanel mount, i.e. every project switch, every
dock-follows-tab change, every sub-tab toggle. A minute of clicking
between two projects was ~60 copies. `MAX_CORRUPT_BACKUPS`,
`corrupt_backups_full()` and the three-outcome `Kept` enum come across
from `migration_store` whole, including the reason the cap is asked
*before* the copy (so it is not implemented by writing a file and
deleting it again, and so the surviving copies are the oldest ones) and
the reason the log line must not claim a backup that was never written.
The file itself is now `{ version, notes }` rather than a bare array.
It costs nothing today and gets permanently more expensive once files
exist in the field. No released build has written notes, so there is no
migration path — but a bare array is still *read*, because declaring a
perfectly readable file corrupt is the one outcome this store exists to
avoid, and a developer's own notes are prose nothing else has a copy of.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HjL1E2JFNctUqCYotUwqqb
|
||
|
|
037ed78570 |
Test the dock's load-path clamp and keyboard resize direction
- notesDockWidth store initialization now clamps/defaults a bad localStorage value on load, not just on write (verified this fails without the clamp). - The keyboard resize test asserts the exact widened/narrowed value instead of just that the setter was called, so a swapped or inverted arrow-key branch would be caught. |
||
|
|
31e8f9df5f | Add the notes dock | ||
|
|
3704064006 | Add the Notes tab | ||
|
|
f79a44e0a8 | Add the send-to-agent button | ||
|
|
5a8e24ccbe | Extract the Claude newline sequence and the session display name | ||
|
|
a1f4eee9a3 |
Fix critical cross-project data corruption bug in useNotes hook
When a save is in flight for project A and the user switches to project B before it resolves, the stale closure still has projectId=A. When A's save resolves, the post-save re-read of listNotes(projectId) runs with the stale closed-over projectId, and setNotes(reloaded) overwrites B's displayed notes with A's list—the same cross-project contamination class as Finding 2 but reintroduced through the fix itself. Fix: Add a currentProjectId ref updated on every render, and guard both saveNote and deleteNote callbacks with a check before replacing/filtering the whole list. If the project changed while the async operation was in flight, bail out of the state update but still report success (the operation itself succeeded on the backend; only the stale list update is skipped). Added test: a save in flight for one project, a switch to another, then the first save resolving—asserts the second project's notes are still displayed. |
||
|
|
b6ba6deb09 |
Fix critical data corruption and stale-data bugs in useNotes hook
- Finding 1 (saveNote): After a successful save, re-read the canonical list from the backend instead of patching in place. A successful save stamps a new updated_at, and the backend sorts by updated_at descending, so the record's position has changed and positional patching would disagree with what a reload would show. If the re-read fails, keep the save reported as successful and leave the existing list alone. - Finding 2 (stale notes): Clear notes on projectId change (not only when empty) and on load failure. Previously, switching from project A to project B would leave A's notes on screen until B's fetch resolved, and if a user edited one, A's note would be written into B's notes file—cross-project data corruption. If a load fails, A's notes stay visible under B indefinitely. - Added four new tests covering these scenarios: projectId change clears old notes, failed load leaves no stale notes, saving a new note ends with the backend's list, and saves re-read the list rather than patching. |
||
|
|
cd3160b1cd | Add the notes hook and its IPC wrappers | ||
|
|
60abff1717 | Expose notes over IPC and drop them with the project | ||
|
|
cc767bd544 | Add a per-project notes store | ||
|
|
221e7566c3 |
Plan the project Notes implementation
Seven tasks, each ending in a testable deliverable: the store, the IPC surface, the hook, the two shared helpers, the send button, the tab, and the dock. Two extractions are folded in rather than left for later, both because this feature would otherwise duplicate knowledge that is already written down. `\x1b\r` becomes `lib/claudeInput.ts` so the hard-won comment in `TerminalView` stays the single source of truth for a sequence that must never be "simplified" to `\n`. The session display-name rule becomes `lib/sessionName.ts`, which is a fix rather than a precaution: the rule is currently written twice inside `MainTabs.tsx`, both copies local and non-exported, and the send-target picker would have made three. The spec is also corrected in three places against what the code actually does. `migration_store` is a free-function module with no struct, so the notes store is too, and the "read-modify-write under the store's Mutex" line described a shape that file does not have — the upsert takes an explicit process-wide write lock instead, and the read path takes none. `useProjectSave` has no debounce; its only timer is a 2500 ms reset of the "Saved" label. And the storage section now specifies the durable write `migration_store` uses — fsync the file, rename, fsync the directory — rather than `projects_store`'s bare rename, because notes are prose nothing else holds a copy of. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HjL1E2JFNctUqCYotUwqqb |
||
|
|
e58e2cdaf7 |
Design a per-project Notes tab with a send-to-agent action
Notes are discrete, addressable items with a button that puts one into a running Claude session's prompt. That is deliberately not what `claude_instructions` does — that field is *ambient*, merged into the container's CLAUDE.md on every start and always in context. Nor is it a `NOTES.md` in the workspace, which the agent can read but the user cannot, once the container is stopped. Discrete items, fired on demand, readable with the container down, is the gap neither of those covers. Storage is one file per project under the app data dir, following `migration_store.rs` rather than living on the `Project` record: that record is rewritten on every blur by the debounced save path, so notes there would mean the whole project list is rewritten per keystroke-batch and a note edit could clobber a Config edit. `migration_store.rs` already documents that reasoning for itself. Two findings are worth more than the design they support. **Newlines already have a verified answer.** A note body has newlines; typed as raw keystrokes each one submits a separate prompt, so a note would arrive as N truncated messages. `TerminalView.tsx` already sends `\x1b\r` for Shift+Enter and its comment states those are the in-band bytes, not a guess, with an explicit warning against simplifying to `\n` because a shell would run the line. Send-to-agent reuses that sequence through one shared helper, and — from the same comment — only offers `claude` sessions as targets, since bash's readline has no binding for it and merely bells. **The dock cannot widen the OS window.** A throwaway Tauri app was built and run on KDE Plasma to find out, because the app has no window-geometry code to reason from. Under XWayland every test passed exactly. Under native Wayland the same binary asked +420 and got +600, moved the height +276 without being asked, compounded that offset on every call, and ended reporting 5400x2900 on a 4800x2700 monitor. Worse, `outer_position()` did not fail — it returned `Ok(0,0)` for a window that was not at 0,0, so a "cannot determine position, do not grow" fallback never fires. A clean failure could have been handled; a plausible wrong answer cannot be detected from the value itself. AppImages get XWayland because linuxdeploy-plugin-gtk forces GDK_BACKEND=x11; the .deb and .rpm do not. The split is therefore by *packaging*, not platform — two users on identical hardware would see different behavior. So the dock takes space inward on every backend, which also costs nothing: the ResizeObserver in `TerminalView.tsx` already reflows xterm and resizes the container PTY on width change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HjL1E2JFNctUqCYotUwqqb |
||
|
|
ed1dc8502c |
Merge pull request 'Retire the Arch package, document AppImage desktop integration' (#47) from chore/retire-arch-packaging into main
Secret Scan / scan (push) Successful in 5s
|
||
|
|
bd08ce8be2 |
Merge pull request 'Fix terminal input reordering and Linux terminal rendering' (#46) from fix/terminal-input-ordering-and-linux-rendering into main
Build App / compute-version (push) Successful in 3s
Secret Scan / scan (push) Successful in 4s
Build App / build-macos (push) Successful in 2m43s
Build App / build-windows (push) Successful in 4m56s
Build App / build-linux (push) Successful in 5m28s
Build App / create-tag (push) Successful in 3s
Build App / sync-to-github (push) Successful in 11s
|
||
|
|
7a5c0c1f13 |
Retire the Arch package, document AppImage desktop integration
The `triple-c-bin` package was never on the AUR, so installing it meant
downloading a file and running `pacman -U` — the same gesture as making an
AppImage executable, for a second artifact to keep building. And being
`workflow_dispatch`-only it reached 1 release in 28 (only v0.4.16 has a
`.pkg.tar.zst`), while HOW-TO-USE.md told Arch and CachyOS users to download
it from every release. A distribution channel that is absent 27 times out of
28 is worse than not promising one.
`packaging/arch/` and `.gitea/workflows/publish-arch-package.yml` are
preserved whole on `hold/arch-packaging`, the same way the disk panel and
drag-out work were held rather than deleted. What would make an Arch package
worth having is an AUR account and its SSH key as a repo secret — both
one-time manual steps that never happened; the workflow's own header already
said as much about its AUR push step.
This also closes the gap that prompted the review: nothing validated the
PKGBUILD until someone manually dispatched the workflow, making it the only
packaging path with no CI coverage. Removing it removes the untested surface
rather than adding a job to test something nobody installs.
In its place, `scripts/install-appimage.sh` does what a package manager's
install hooks would. An AppImage carries a `.desktop` entry and icons inside
itself, but nothing on the host reads them, so it never appears in the app
launcher. The script extracts the bundled icons into the user's icon theme
and writes a launcher entry — no sudo, nothing outside `~/.local/share`, and
the AppImage itself is never copied or moved.
Two details it gets right on purpose:
* The `Exec` line is rewritten, not copied. The bundled entry says
`Exec=triple-c`, which resolves only inside the running AppImage's own
mount — a verbatim copy gives a launcher entry that starts nothing.
* Extraction uses `--appimage-extract`, which needs no FUSE, so the script
works on a machine where *running* the AppImage would first need
`fuse2` installed. That requirement is now documented too: Arch and
CachyOS do not ship FUSE 2 by default.
Verified against the real artifact — the AppImage from this repo's own
preview-3a49a67 release: 4 icon sizes install, `desktop-file-validate` passes
with no warnings, `--uninstall` leaves nothing behind, and shellcheck is
clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApLYH6ybHwQFkMCtKuHrrV
|
||
|
|
3a49a67c1f |
Fix terminal input reordering and Linux terminal rendering
Secret Scan / scan (push) Successful in 4s
Build App (Preview) / compute-version (pull_request) Successful in 4s
Secret Scan / scan (pull_request) Successful in 4s
Build App (Preview) / create-release (pull_request) Successful in 2s
Build App (Preview) / build-macos (pull_request) Successful in 2m41s
Build App (Preview) / build-linux (pull_request) Successful in 5m25s
Build App (Preview) / build-windows (pull_request) Successful in 5m32s
Build App (Preview) / prune-previews (pull_request) Successful in 8s
Two separate defects behind the same report: typing in a container terminal
is sluggish on Linux, and a backspace can land *after* the characters typed
behind it.
The web terminal was the control that separated them. It shares the Docker
exec, the PTY, `exec_manager`, the input channel and its serial writer task,
and xterm.js itself — and it does not exhibit either symptom. Only three
things differ, and each accounts for part of the report.
**Input ordering.** Every keystroke was its own `invoke("terminal_input")`.
That command is `async`, so Tauri spawns each one as an independent task, and
those tasks then race for the session mutex in `ExecSessionManager::send_input`
— nothing preserved the order the bytes were typed in. The serial writer
downstream cannot help, because the order is already lost before anything
reaches the channel. The web terminal gets ordering for free by awaiting
`send_input` inline in a single WebSocket reader loop.
`useTerminal` now holds a per-session queue: one write in flight at a time,
the next only after the previous resolves. Anything typed meanwhile coalesces
into the next chunk, which also collapses a burst of typing into a couple of
IPC round trips rather than one per key. The queue is module scope, not hook
scope, because `useTerminal()` is called from several components — a per-hook
queue would leave speech-to-text, image paste and typing racing each other.
Each caller's promise still settles only when its own bytes have gone, so
`await sendInput(...)` keeps its meaning.
**The DMA-BUF escape hatch did not exist.** `apply_webkit_wayland_workaround`
left any pre-set value alone, including `0`, on a stated assumption that
WebKitGTK reads the variable as a boolean. It reads presence, so
`WEBKIT_DISABLE_DMABUF_RENDERER=0` disabled DMA-BUF exactly like `=1`, and no
value a user could set got the accelerated path back. `0`/`false`/`no`/empty
now remove the variable, which is the only thing WebKitGTK reads as enabled.
The default is unchanged: unset still means disabled on Linux.
**WebGL does not degrade to canvas here.** The comment on that workaround
assumed `@xterm/addon-webgl` would fall back to the canvas renderer once
DMA-BUF was off. Its constructor throws only when WebGL is *absent*, and with
DMA-BUF disabled WebGL is still present — served by software rasterisation.
So the addon loads and every frame is rendered on the CPU, slower than the
canvas renderer it was assumed to fall back to. `AppSettings::terminal_gpu_
rendering` decides whether it loads at all: `None` is auto (on for macOS and
Windows, off on Linux), `Some(_)` forces it either way from Settings →
Terminal. `Option<bool>` rather than `bool` so the zero value means "we
choose" instead of pinning every existing settings file to one answer.
Verified: 643 frontend tests and 530 Rust tests pass, clippy clean, secret
scan clean. The Linux rendering half needs confirming on a real desktop —
neither symptom reproduces in a headless container.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApLYH6ybHwQFkMCtKuHrrV
|
||
|
|
88d6bed6db |
Merge pull request 'Document the Wayland icon-cache-needs-relogin gotcha' (#45) from docs/wayland-icon-cache-note into main
Secret Scan / scan (push) Successful in 6s
|
||
|
|
6cc48b3266 |
Document the Wayland icon-cache-needs-relogin gotcha
A user hit this after installing the new Arch/CachyOS package (triple-c#34): icon missing in the app menu, taskbar, and titlebar alike, with no error in the app's own log. Root cause has nothing to do with the app or its packaging — GNOME/KDE cache the installed-app list and resolved icons in the shell process's memory at startup, and Wayland has no equivalent to X11's soft shell-restart trick to force a live reload. Logging out and back in fixed it for them. |
||
|
|
0fad306c25 |
Merge pull request 'Add an Installation section to HOW-TO-USE.md' (#43) from docs/installation-instructions into main
Secret Scan / scan (push) Successful in 6s
|
||
|
|
8beb62b12c |
Merge pull request 'Mirror the Arch package to the Gitea release too' (#44) from fix/arch-package-mirror-to-gitea into main
Secret Scan / scan (push) Successful in 4s
|
||
|
|
f2cfc0be8f |
Also attach the Arch package to the matching Gitea release
The workflow only ever uploaded to the GitHub release — the Gitea release for the same version (the plain, unsuffixed vX.Y.Z tag build-app.yml's Linux job creates, which already holds the .deb/.rpm/.AppImage) never got it, so it looked missing to anyone checking releases on Gitea instead of GitHub. New step mirrors build-app.yml's own Gitea upload step exactly: same get-or-create-by-tag, delete-existing-asset, upload-as-octet-stream shape, same REGISTRY_TOKEN secret. Verified the read side (release lookup, asset listing) against the real v0.4.16 release before writing this — resolves to the correct release id and correctly finds no existing asset yet. |
||
|
|
99c9dd3cc2 |
Add an Installation section — nothing told a new user how to get the app
HOW-TO-USE.md's Prerequisites jumped straight to Docker and a Claude Code account, assuming Triple-C was already installed; the app itself had no download/install instructions anywhere in the docs. Covers all six release assets, including the new Arch/CachyOS .pkg.tar.zst (triple-c#34) that publish-arch-package.yml now attaches to each release. |
||
|
|
dd48baac8a |
Merge pull request 'Add password-encrypted settings export/import' (#40) from feat/settings-export-import into main
Build App / compute-version (push) Successful in 5s
Secret Scan / scan (push) Successful in 6s
Build App / build-macos (push) Successful in 2m41s
Build App / build-windows (push) Successful in 4m50s
Build App / build-linux (push) Successful in 8m3s
Build App / create-tag (push) Successful in 21s
Build App / sync-to-github (push) Successful in 14s
|
||
|
|
e63318e04a |
Merge pull request 'Skip AUR for now, attach Arch package as a GitHub release asset' (#42) from fix/aur-render-expression-collision into main
Secret Scan / scan (push) Successful in 6s
Reviewed-on: #42 |