Commit Graph
395 Commits
Author SHA1 Message Date
shadowdao 23364f412e Merge remote-tracking branch 'origin/main' into feat/project-notes 2026-09-02 12:04:17 -07:00
jknapp 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: #49
v0.4.18-mac v0.4.18-win v0.4.18
2026-09-02 18:53:20 +00:00
shadowdaoandClaude Opus 5 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
2026-09-02 11:34:02 -07:00
shadowdaoandClaude Opus 5 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
2026-09-02 09:08:57 -07:00
shadowdaoandClaude Opus 5 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
2026-09-02 09:08:50 -07:00
shadowdaoandClaude Opus 5 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
2026-09-01 14:23:55 -07:00
shadowdaoandClaude Opus 5 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
2026-09-01 13:47:01 -07:00
shadowdaoandClaude Opus 5 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
2026-09-01 13:47:01 -07:00
shadowdaoandClaude Opus 5 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
2026-09-01 13:40:40 -07:00
shadowdao 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.
2026-09-01 13:22:35 -07:00
shadowdao 31e8f9df5f Add the notes dock 2026-09-01 13:15:13 -07:00
shadowdao 3704064006 Add the Notes tab 2026-09-01 13:08:17 -07:00
shadowdao f79a44e0a8 Add the send-to-agent button 2026-09-01 13:02:22 -07:00
shadowdao 5a8e24ccbe Extract the Claude newline sequence and the session display name 2026-09-01 12:57:29 -07:00
shadowdao 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.
2026-09-01 12:50:23 -07:00
shadowdao 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.
2026-09-01 12:45:35 -07:00
shadowdao cd3160b1cd Add the notes hook and its IPC wrappers 2026-09-01 12:38:33 -07:00
shadowdao 60abff1717 Expose notes over IPC and drop them with the project 2026-09-01 12:34:35 -07:00
shadowdao cc767bd544 Add a per-project notes store 2026-09-01 12:28:09 -07:00
shadowdaoandClaude Opus 5 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
2026-09-01 12:00:52 -07:00
shadowdaoandClaude Opus 5 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
2026-09-01 11:48:31 -07:00
jknapp 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
v0.4.17-mac v0.4.17-win v0.4.17
2026-08-28 20:21:01 +00:00
jknapp 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
2026-08-28 20:20:54 +00:00
shadowdaoandClaude Opus 5 7a5c0c1f13 Retire the Arch package, document AppImage desktop integration
Secret Scan / scan (push) Successful in 8s
Secret Scan / scan (pull_request) Successful in 8s
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
2026-08-28 13:11:26 -07:00
shadowdaoandClaude Opus 5 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
2026-08-28 12:51:18 -07:00
jknapp 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
2026-08-27 23:15:00 +00:00
shadow-test 6cc48b3266 Document the Wayland icon-cache-needs-relogin gotcha
Secret Scan / scan (push) Successful in 4s
Secret Scan / scan (pull_request) Successful in 4s
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.
2026-08-27 15:53:19 -07:00
jknapp 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
2026-08-27 22:37:19 +00:00
jknapp 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
2026-08-27 22:21:58 +00:00
shadow-test f2cfc0be8f Also attach the Arch package to the matching Gitea release
Secret Scan / scan (push) Successful in 10s
Secret Scan / scan (pull_request) Successful in 7s
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.
2026-08-27 15:14:54 -07:00
shadow-test 99c9dd3cc2 Add an Installation section — nothing told a new user how to get the app
Secret Scan / scan (push) Successful in 4s
Secret Scan / scan (pull_request) Successful in 4s
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.
2026-08-27 15:06:43 -07:00
jknapp 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
v0.4.16-mac v0.4.16-win v0.4.16
2026-08-27 21:53:42 +00:00
jknapp 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
2026-08-27 21:50:54 +00:00
jknapp adf9e7d603 Merge branch 'main' into fix/aur-render-expression-collision
Secret Scan / scan (push) Successful in 5s
Secret Scan / scan (pull_request) Successful in 6s
2026-08-27 21:50:20 +00:00
shadow-test 3c8296843f Skip AUR for now — attach the built Arch package to the GitHub release
Secret Scan / scan (push) Successful in 5s
Secret Scan / scan (pull_request) Successful in 5s
Publishing to the AUR needs a maintainer AUR account and its SSH key
registered as a secret here, neither of which exists yet. Rather than
leave the workflow permanently failing at that last step, it now stops
short of AUR and instead uploads the built .pkg.tar.zst to the same
GitHub release it built from, as a plain downloadable asset (`pacman -U`
to install). The AUR-push step is still in this file's git history if
that setup happens later.

Renamed publish-aur-package.yml -> publish-arch-package.yml to match.
The render/validate steps are unchanged; new here is capturing the exact
built package filename from inside the build container (makepkg is the
only thing that actually knows it) and an upload step that follows the
same create-or-reuse-release, strip-upload_url, POST-octet-stream pattern
build-app.yml and backfill-releases.yml already use for GitHub assets,
plus a delete-existing-asset-first step so a re-dispatch for an
already-packaged version replaces rather than 422s.

Verified with a real Docker run end to end: rendered a real PKGBUILD,
built a real (synthetic) .deb through makepkg + namcap in an archlinux
container, confirmed the container exits 0, and confirmed the exact
package filename it captures (triple-c-bin-<version>-1-x86_64.pkg.tar.zst)
round-trips out via docker cp intact.
2026-08-27 14:48:39 -07:00
jknapp 7489516df3 Merge pull request 'Fix PKGBUILD render silently no-op'ing on every AUR publish run' (#41) from fix/aur-render-expression-collision into main
Secret Scan / scan (push) Successful in 9s
Reviewed-on: #41
2026-08-27 21:40:05 +00:00
shadow-test 6dcdeb89cb Fix PKGBUILD render silently no-op'ing on every AUR publish run
Secret Scan / scan (push) Successful in 4s
Secret Scan / scan (pull_request) Successful in 4s
The "Render PKGBUILD" step's Python heredoc built its old_source match
string via an f-string, escaping literal braces as `${{pkgver}}` — which
put that exact four-character sequence directly in this workflow file's
own YAML text. Gitea Actions scans a run: block for `${{ ... }}` and tries
to evaluate whatever's inside as one of its own expressions before the
shell ever sees the script; "pkgver" isn't a valid expression context, so
every run has been failing that interpolation and emptying the step
instead of raising anything visible there. The next step's `makepkg` then
failed with "PKGBUILD does not exist" — the actual point of failure was
one step earlier and unrelated to AUR credentials.

Rebuilt the same match string with a "$" variable and plain concatenation
so the file's own text never contains the trigger sequence. Verified by
extracting the exact heredoc and running it standalone against the real
PKGBUILD template — renders identically to the intended output.
2026-08-27 14:29:27 -07:00
shadow-test 97e58db3c1 Close gateway-secret desync, TOCTOU, and undisclosed custom-image gaps
Secret Scan / scan (push) Successful in 6s
Build App (Preview) / compute-version (pull_request) Successful in 5s
Secret Scan / scan (pull_request) Successful in 5s
Build App (Preview) / create-release (pull_request) Successful in 2s
Build App (Preview) / build-macos (pull_request) Successful in 2m41s
Build App (Preview) / build-windows (pull_request) Successful in 4m53s
Build App (Preview) / build-linux (pull_request) Successful in 7m5s
Build App (Preview) / prune-previews (pull_request) Successful in 1s
Round 4 review findings:

- Disclose and warn on a custom Docker image the import would set (HIGH):
  it's the image every project container is created from, so an
  undisclosed change here was a sharper version of the redirected-base-URL
  problem round 3 already flagged for the model backends.
- Recreate a running gateway container when an import restores a new
  secret with the shape unchanged (MEDIUM): reconcile_gateway's shape
  comparison can't see a secret-only change, so the container would
  otherwise keep serving old key material indefinitely.
- Report keychain write failures back to the caller instead of only
  logging them (MEDIUM): apply_settings_import now returns
  SettingsImportOutcome with secret_restore_warnings so a partial restore
  can't read as unqualified success.
- Pin a hash of the previewed file's ciphertext and refuse to apply if it
  changed on disk (MEDIUM): closes a TOCTOU between preview and apply.
- Sanitize and cap every free-form string a preview surfaces, and move the
  warning boxes above the replace list in the UI (MEDIUM): an unbounded
  base URL or image name could otherwise push the security warnings below
  the scroll fold.
- Validate the Docker socket path on import the same as the SSH key and CA
  cert paths (LOW): it was the one mounted host path validate_settings_update
  didn't cover.
- Fix ExportedSecrets::is_empty() to treat whitespace-only as blank, like
  every other secret-presence check in this feature (LOW).
- Authenticate the file header as AEAD associated data (LOW, defense in
  depth) and correct two doc comments that overstated the password not
  being cached.
2026-08-27 14:24:06 -07:00
shadow-test a606e3ab20 Validate settings imports before writing secrets; disclose base URLs
Secret Scan / scan (push) Successful in 14s
Build App (Preview) / compute-version (pull_request) Successful in 7s
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 2m43s
Build App (Preview) / build-windows (pull_request) Successful in 4m59s
Build App (Preview) / build-linux (pull_request) Successful in 7m29s
Build App (Preview) / prune-previews (pull_request) Successful in 1s
A rejected import (bad env var name, disallowed host path) used to leave
keychain secrets already overwritten while the settings themselves stayed
unchanged. apply_settings_import now runs update_settings's validation
(extracted into validate_settings_update) before any secret write.

Also from this review round: sharpened two format-version tests that
previously passed against the pre-fix code too, added a direct test for
split_settings_and_secrets, warned on a dormant web terminal token even
when the terminal import leaves it off, matched the password-length check
to the frontend's unit of measure, zeroized the export plaintext buffer,
and surfaced non-blank Ollama/llama.cpp/OpenAI-compatible/gateway base
URLs in the import preview so a traffic redirect isn't silent.
2026-08-27 13:13:48 -07:00
shadow-testandClaude Sonnet 5 925e51e435 Fix a real credential-leak vector a review found, plus four smaller issues
Secret Scan / scan (push) Successful in 12s
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 2m41s
Build App (Preview) / build-windows (pull_request) Successful in 4m51s
Build App (Preview) / build-linux (pull_request) Successful in 5m12s
Build App (Preview) / prune-previews (pull_request) Successful in 1s
The headline finding: WebTerminalSettings::access_token is a live bearer
credential for a server that binds every interface, stored as a plain
field on AppSettings — which this feature was exporting and importing
wholesale as if it were as inert as a port number. A crafted export file
could set web_terminal.enabled and access_token together, and importing
it (with no more warning than any other setting change) would silently
stand up a LAN-listening terminal server with an attacker-known token on
the victim's next launch.

Fixed by carving the token out into ExportedSecrets, same as the other
three global secrets, with the same "only overwrite what the import
actually has" treatment — except that has to be done by hand here, since
this one lives inside the AppSettings blob that gets replaced wholesale
rather than in the keychain. Added SettingsImportPreview::
enables_web_terminal so "this turns on a listening service" gets its own
visible warning in the confirmation modal rather than hiding inside a
generic "settings replaced" bullet list.

Also fixed:

- read_and_decrypt checked format_version only after attempting to parse
  the full payload, so a future version bump that isn't
  deserialize-compatible would fail on the shape mismatch before the
  version check ever ran — and serde's type-mismatch errors quote the
  offending value inline, which is a real leak path since the plaintext
  here can hold a live credential. Now probes just the version field
  first, and neither error path interpolates the underlying serde message
  into what the user sees.
- apply_settings_import cleared the pending-import path before it could
  fail, so a rejected import (an invalid host path, anything
  update_settings validates) dead-ended the modal with no way back except
  cancelling and reopening the file picker. The path is now only cleared
  on success.
- Secrets are restored before the settings replace runs, not after —
  replacing settings is what triggers reconcile_gateway, and restoring
  secrets afterward left a real window where a gateway recreation
  happened against the destination's stale keys.
- The 8-character password minimum was frontend-only; export_settings now
  enforces it too, since that's the actual boundary a weak password has
  to cross. The derived key and decrypted plaintext are wrapped in
  zeroize::Zeroizing (already in the tree via aes-gcm).

Added test coverage the review named as missing: format-version
ordering, the generic-error-message guarantee, non_blank's blank-vs-
absent handling, and the new web-terminal preview/warning behavior on
both sides of the IPC boundary.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGjXq6fqtAFHdbhk4f3PfZ
2026-08-27 12:16:43 -07:00
shadow-testandClaude Sonnet 5 722d9aeff1 Add password-encrypted settings export/import
Secret Scan / scan (push) Successful in 8s
Build App (Preview) / compute-version (pull_request) Successful in 6s
Secret Scan / scan (pull_request) Successful in 9s
Build App (Preview) / create-release (pull_request) Successful in 5s
Build App (Preview) / build-macos (pull_request) Successful in 2m41s
Build App (Preview) / build-windows (pull_request) Successful in 4m59s
Build App (Preview) / build-linux (pull_request) Successful in 6m29s
Build App (Preview) / prune-previews (pull_request) Successful in 1s
Closes #35. Exports the host environment — global AppSettings (already
the non-secret shape persisted to settings.json) plus the global secrets
that live in the OS keychain instead (the shared Claude Code OAuth login,
the model gateway's provider API key and master key) — to one
password-encrypted file, and restores it on another machine.
Per-project settings, per-project secrets, and Docker volumes are
deliberately out of scope; this is not a project backup.

Designed with the user in issue #35's comments: global settings only, no
docker volumes, the password is the lock/key, and the export is portable
as one file.

Crypto (storage/settings_crypto.rs): Argon2id derives a 256-bit key from
the password (memory-hard, meaningfully resistant to GPU/ASIC
brute-forcing in a way PBKDF2 at any reasonable iteration count is not),
AES-256-GCM does the actual encryption. A wrong password fails GCM's
authentication tag rather than producing silent garbage. Salt and nonce
are random per export and stored in the clear in the file header — their
job is uniqueness, not secrecy.

The save/open dialogs are opened from Rust, matching the boundary
file_commands.rs's pick_save_path/pick_files_to_upload already establish:
a frontend-driven dialog handing Rust a host path is the exact shape of
bug that produced this app's past criticals. preview_settings_import
resolves the chosen import path itself and remembers it
(AppState::pending_settings_import) so apply_settings_import re-reads the
same file without a path crossing back over IPC. The password is
re-entered rather than cached between preview and apply, so nothing here
holds decrypted plaintext in memory for longer than one command's
execution; the preview returned to the frontend carries counts and
presence flags only, never a secret value.

Import replaces settings wholesale (an import is "restore this
environment"), but only writes secrets actually present in the file — an
absent secret means "the source machine never had this configured," not
"delete this on import."

Added storage::secure::store_gateway_master_key and get_gateway_master_key
(read-only, unlike get_or_create_gateway_master_key which mints one as a
side effect) since neither existed and import needs to restore an exact
captured value rather than mint a new random one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGjXq6fqtAFHdbhk4f3PfZ
2026-08-27 11:57:16 -07:00
jknapp 81b1cfba09 Merge pull request 'Add a native Arch/CachyOS package via its own AUR publish workflow' (#39) from feat/arch-aur-package into main
Secret Scan / scan (push) Successful in 5s
v0.4.15-mac v0.4.15-win v0.4.15
2026-08-27 18:30:00 +00:00
jknapp ca6028bbb3 Merge pull request 'Work around WebKitGTK EGL crash on Wayland' (#38) from fix/wayland-webkit-egl-crash into main
Build App / compute-version (push) Successful in 3s
Secret Scan / scan (push) Successful in 3s
Build App / build-macos (push) Successful in 2m50s
Build App / build-windows (push) Successful in 4m46s
Build App / build-linux (push) Successful in 6m35s
Build App / create-tag (push) Successful in 3s
Build App / sync-to-github (push) Successful in 12s
2026-08-27 18:29:51 +00:00
shadow-testandClaude Sonnet 5 b3d07bda09 Fix real workflow bugs a review found: dead bind mount, blind error gate
Secret Scan / scan (push) Successful in 6s
Secret Scan / scan (pull_request) Successful in 6s
A review found the "Validate with makepkg and namcap" step's bind mount
(docker run -v "$PWD/rendered:/work") would very likely fail on Gitea's
own act_runner: a containerized job's $PWD isn't a path the daemon's host
can resolve, so the mount would silently attach an empty directory
instead of failing loudly — the same class of problem noted elsewhere for
this exact environment. Switched to docker create + docker cp (in and
back out) + docker start -a, the pattern already validated locally, which
works regardless of where the daemon actually lives.

Also found and fixed, most severe first:

- The namcap error gate (`grep -q "^[a-zA-Z0-9_-]*bin E:"`) only matched
  one of namcap's two line shapes for reporting an error
  ("triple-c-bin E: ...") and missed the other ("PKGBUILD
  (triple-c-bin) E: ...") entirely — confirmed by reproducing both against
  a real namcap run. The PKGBUILD-level half of the safety net was dead.
  Replaced with a plain `grep -q " E: "`, confirmed to match both real
  shapes (and a split-package variant) and nothing else.
- package()'s `ar x "Triple-C_${pkgver}_amd64.deb"` named the asset
  literally, defeating the whole point of the resolve step discovering
  the real filename from the release instead of assuming a pattern — a
  future Tauri bundler naming change would still break here with an
  opaque error. Changed to `ar x ./*_amd64.deb`, which `source=()` already
  guarantees matches exactly one file.
- `pacman -Sy` before installing packages is the canonical Arch partial-
  upgrade footgun; changed to `pacman -Syu --noconfirm --needed`.
- `${{ inputs.version }}` was interpolated directly into a shell step
  instead of routed through `env:`, unlike every other step in the file.
- `git push origin master` assumes the local branch name after cloning a
  brand-new (not-yet-created) AUR repo's empty state is `master`, which
  depends on the runner's own `init.defaultBranch` if the server sends no
  symref. `git push origin HEAD:master` is unambiguous either way.
- The private key was written with a plain redirect then chmod'd after,
  leaving a window where it's world-readable; now created at its final
  mode first via `install -m 600 /dev/null`. Added `-o IdentitiesOnly=yes`
  so a runner ssh-agent can't offer a different key first.
- Added GH_PAT auth to the api.github.com calls, matching every other
  workflow in this repo, to avoid the unauthenticated 60/hour rate limit.
- Fixed two comments: the `options` comment credited `!debug` for
  suppressing the empty debug-package directory, when it's actually
  `!strip` doing that (verified in a real build); and documented in the
  README that a hand-edit made directly in the AUR repo is silently
  reverted by the next dispatch, since every run renders fresh from this
  repo's template.

All of the above re-verified with the same real end-to-end methodology as
the original commit: real makepkg build, real namcap lint (clean), and
the exact updated docker create/cp/start sequence run against a live
container.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGjXq6fqtAFHdbhk4f3PfZ
2026-08-27 11:24:31 -07:00
shadow-testandClaude Sonnet 5 e025a7441a Add a native Arch/CachyOS package via its own AUR publish workflow
Secret Scan / scan (push) Successful in 27s
Secret Scan / scan (pull_request) Successful in 10s
Part of triple-c#34's third ask ("I would like to also have an
Arch/CachyOS native version as well"), addressed separately from the
Wayland crash fix (fix/wayland-webkit-egl-crash) since it's an unrelated
feature, not a bug.

packaging/arch/PKGBUILD is a "-bin" AUR package repackaging the same .deb
build-app.yml already produces — no Rust/Node toolchain needed to install
it, and the user gets exactly the binary the project ships and tests.
Verified end to end against a real release (v0.4.14) rather than going by
Tauri's generic docs: downloaded the actual .deb, ldd'd the actual binary
to ground-truth `depends` (dropped `pango` and `libayatana-appindicator`
from an earlier draft — the first is already pulled in transitively by
gtk3, the second was never linked at all since this app has no tray icon
or menu), and ran a real makepkg/namcap/pacman -U cycle. namcap caught a
real issue this way (missing license file under
/usr/share/licenses/triple-c-bin/), now fixed by fetching LICENSE
alongside the .deb.

.gitea/workflows/publish-aur-package.yml does the actual publishing:
given a version (or "latest"), it finds that release's real Linux asset
on GitHub, downloads it, computes real checksums, renders the PKGBUILD
template, validates the result with makepkg and namcap inside a real
Arch container, and pushes to AUR. workflow_dispatch only, deliberately —
the same reasoning that killed sync-release.yml in triple-c#32 (releases
are assembled by build-app.yml across three separate platform jobs, so
there's no single automatic event that fires only once the Linux .deb
this needs actually exists) applies here too.

Requires a repo secret this workflow cannot set up itself:
AUR_SSH_PRIVATE_KEY, from an AUR account that has already created (or
been given co-maintainer access to) triple-c-bin — both one-time manual
steps on aur.archlinux.org. Until that secret exists, the workflow fails
loudly at the push step rather than silently doing nothing. See
packaging/arch/README.md for the full maintenance flow.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGjXq6fqtAFHdbhk4f3PfZ
2026-08-27 11:10:55 -07:00
shadow-testandClaude Sonnet 5 8f62949902 Correct two overclaims in the Wayland workaround's comment
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 2s
Build App (Preview) / build-macos (pull_request) Successful in 2m38s
Build App (Preview) / build-windows (pull_request) Successful in 4m48s
Build App (Preview) / build-linux (pull_request) Successful in 6m49s
Build App (Preview) / prune-previews (pull_request) Successful in 1s
Review found: "nothing this app's UI depends on" is backwards — the
terminal's @xterm/addon-webgl renderer is exactly the GPU compositing path
this setting disables, it just degrades gracefully (the addon's own
construction already handles WebGL being unavailable) rather than
crashing. And the "not simply Wayland vs X11" justification for going
unconditional doesn't hold up: WAYLAND_DISPLAY is exported into an
XWayland client's environment too, so gating on it would have caught that
case as well — the real reason to go unconditional is that there's no
reliable heuristic for the thing that actually matters (which
Mesa/driver/compositor combination is affected), not that the naive gate
misses XWayland specifically.

Also noted, not changed: the env var leaks to whatever the app spawns
afterwards (a cold-launched default browser via xdg-open), and the "=0
re-enables it" parenthetical isn't verified against WebKitGTK's own
source, so softened to say what's actually guaranteed (an already-set
value is left alone) rather than assume presence-vs-boolean parsing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGjXq6fqtAFHdbhk4f3PfZ
2026-08-27 10:57:45 -07:00
shadow-testandClaude Sonnet 5 6354cb42b2 Work around WebKitGTK's EGL crash on Wayland (triple-c#34)
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 2m39s
Build App (Preview) / build-windows (pull_request) Successful in 4m45s
Build App (Preview) / build-linux (pull_request) Successful in 5m10s
Build App (Preview) / prune-previews (pull_request) Successful in 3s
Reported on CachyOS/Arch with Wayland: the app aborts immediately with
"Could not create default EGL display: EGL_BAD_PARAMETER. Aborting."
printed straight to stderr by WebKitGTK's own C code, before Triple-C's
own logging even gets a chance to say anything useful about it.

This is WebKitGTK's DMA-BUF renderer (its default accelerated-compositing
path since 2.42) failing on some Mesa/driver/compositor combinations. Set
WEBKIT_DISABLE_DMABUF_RENDERER=1 unconditionally on Linux before the Tauri
builder runs, which is where GTK/WebKitGTK actually read it — there's no
reliable way to detect the affected combination ahead of time (reports of
this exact failure exist under XWayland too, not just pure Wayland
sessions), and WebKitGTK's fallback compositing path costs some rendering
performance this app's UI doesn't need. Left alone if a user has already
set the variable themselves.

Does not address the other two things filed under the same issue (links
not opening on the host, and a request for a native Arch/CachyOS package)
— those need more information / are a separate scope, respectively.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGjXq6fqtAFHdbhk4f3PfZ
2026-08-27 10:45:24 -07:00
jknapp 9b55a12b32 Merge pull request 'Make preview versions monotonic and distinguishable from production' (#37) from fix/preview-version-numbering into main
Build App / compute-version (push) Successful in 4s
Secret Scan / scan (push) Successful in 3s
Build App / build-macos (push) Successful in 2m41s
Build App / build-windows (push) Successful in 4m50s
Build App / build-linux (push) Successful in 6m27s
Build App / create-tag (push) Successful in 3s
Build App / sync-to-github (push) Successful in 11s
2026-08-27 17:41:38 +00:00
shadow-testandClaude Sonnet 5 049232099b Dedupe the preview-build predicate, fix two comment inaccuracies
Secret Scan / scan (push) Successful in 24s
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 1s
Build App (Preview) / build-macos (pull_request) Successful in 2m39s
Build App (Preview) / build-windows (pull_request) Successful in 4m44s
Build App (Preview) / build-linux (pull_request) Successful in 6m17s
Build App (Preview) / prune-previews (pull_request) Successful in 1s
Final review pass gave this a clean bill of health overall but named
three small things:

- get_app_version() and check_for_updates() each read
  option_env!("TRIPLE_C_BUILD_SUFFIX") independently with slightly
  different idioms — if one were ever edited alone, the About panel and
  the update check could silently disagree about whether this is a
  preview build. Extracted preview_build_suffix() as the single place
  that reads and classifies it.
- pick_update's doc comment described the unparseable-tag case as a
  `-preview.<sha>` suffix; the actual tag build-app-preview.yml creates is
  `preview-<sha>` (no version, no dot) — already correct in the
  neighboring GitHubRelease::prerelease comment, just not here.
- That same prerelease comment claimed defence against a preview release
  leaking through backfill-releases.yml, but a preview's tag already fails
  semver parsing on its own — this field's actual job is the case parsing
  can't catch: a normally-tagged release someone flags prerelease on
  Gitea (a hotfix candidate, an RC) that a backfill would otherwise mirror
  as-is.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGjXq6fqtAFHdbhk4f3PfZ
2026-08-27 10:34:56 -07:00
shadow-testandClaude Sonnet 5 945883bb9d Actually offer a preview the release it precedes, and fix two more gaps
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 2m39s
Build App (Preview) / build-windows (pull_request) Successful in 4m50s
Build App (Preview) / build-linux (pull_request) Successful in 7m25s
Build App (Preview) / prune-previews (pull_request) Successful in 6s
An Opus review of the previous commit found its headline claim didn't
hold: a preview and the release it precedes compute to the identical
numeric version by construction, but check_for_updates compared with a
strict `>` against the bare CARGO_PKG_VERSION (never the suffixed display
string), so `(0,4,13) > (0,4,13)` is false and the release was never
offered. Plain semver ordering doesn't make a `-preview.<sha>` suffix sort
below the same numeric release on its own here, since the comparison
never sees the suffix at all.

pick_update now takes is_preview_build, derived from whether
TRIPLE_C_BUILD_SUFFIX was baked in, and relaxes that one comparison to
`>=` — so "a release exists at my own number" reads as an update. A
production build still requires strictly newer.

Also: ported build-app.yml's `git tag --points-at HEAD` guard into the
preview version computation. Without it, workflow_dispatch (which this
workflow allows on main, not just PR builds) run on a commit a release
was already cut from would compute one past that release — reintroducing
"preview outranks production" through the manual-dispatch door. And
corrected two comments that claimed the prerelease filter was currently a
no-op: backfill-releases.yml mirrors every Gitea release to GitHub
unfiltered, prerelease flag included, so it's real defence-in-depth
against a dispatched backfill leaking a preview release, not a no-op.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FGjXq6fqtAFHdbhk4f3PfZ
2026-08-27 10:24:24 -07:00