Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/actions/updater-flags/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Resolve updater signing
description: >-
Decides whether this build should produce signed in-app updater artifacts.
`bundle.createUpdaterArtifacts` is a hard error when TAURI_SIGNING_PRIVATE_KEY
is missing, so it lives in tauri.updater.conf.json and is only merged in when
the secret actually exists. Without it, builds are byte-identical to before and
the app reports the `unconfigured` update status instead of offering a download
it could never verify.

outputs:
config:
description: >-
Either the `--config <path>` flag to append to `tauri build`, or empty.
value: ${{ steps.resolve.outputs.config }}
signed:
description: '"true" when updater artifacts will be produced.'
value: ${{ steps.resolve.outputs.signed }}

runs:
using: composite
steps:
- id: resolve
shell: bash
run: |
set -euo pipefail

if [[ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]]; then
echo 'config=--config src-tauri/tauri.updater.conf.json' >> "$GITHUB_OUTPUT"
echo 'signed=true' >> "$GITHUB_OUTPUT"
echo "Updater artifacts enabled for this build."
else
echo 'config=' >> "$GITHUB_OUTPUT"
echo 'signed=false' >> "$GITHUB_OUTPUT"
echo "::notice title=No updater signing key::TAURI_SIGNING_PRIVATE_KEY is not set, so this build ships without in-app update artifacts. Generate a keypair per docs/SIGNING.md to enable them."
fi
83 changes: 80 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ env:
CARGO_HTTP_MULTIPLEXING: 'false'
CARGO_HTTP_TIMEOUT: '60'
CARGO_NET_RETRY: '10'
# Signs the in-app updater artifacts (minisign; unrelated to OS code signing).
# Absent until the keypair exists, and every job below degrades to a plain
# unsigned build when it is — see the "Resolve updater signing" steps.
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}

jobs:
macos:
Expand All @@ -40,7 +45,16 @@ jobs:
workspaces: src-tauri

- run: bun install --frozen-lockfile
- run: bun run build

- id: updater
uses: ./.github/actions/updater-flags

# These three mirror the `build` script in package.json. They are spelled out
# here only so the updater `--config` flag can be injected into the middle
# one; keep them in step with that script.
- run: bun run typecheck
- run: bun run tauri build --bundles app ${{ steps.updater.outputs.config }}
- run: bun run dmg

- uses: actions/upload-artifact@v4
with:
Expand All @@ -49,6 +63,18 @@ jobs:
path: src-tauri/target/release/bundle/dmg/*.dmg
if-no-files-found: error

# The in-app updater installs this tarball, not the DMG: it replaces the
# bundle in place, so it must not be wrapped in a disk image.
- if: steps.updater.outputs.signed == 'true'
uses: actions/upload-artifact@v4
with:
name: aether-macos-updater
retention-days: 14
path: |
src-tauri/target/release/bundle/macos/*.app.tar.gz
src-tauri/target/release/bundle/macos/*.app.tar.gz.sig
if-no-files-found: error

linux:
name: Linux (x86_64)
runs-on: ubuntu-latest
Expand All @@ -71,7 +97,13 @@ jobs:

- run: bun install --frozen-lockfile
- run: bun run typecheck:web
- run: bun run tauri build --bundles deb,appimage
# Unit tests are platform-independent, so one runner is enough.
- run: bun run test

- id: updater
uses: ./.github/actions/updater-flags

- run: bun run tauri build --bundles deb,appimage ${{ steps.updater.outputs.config }}
- run: scripts/normalize-deb-package.sh src-tauri/target/release/bundle/deb/*.deb

- uses: actions/upload-artifact@v4
Expand All @@ -83,6 +115,17 @@ jobs:
src-tauri/target/release/bundle/appimage/*.AppImage
if-no-files-found: error

# AppImage only. A .deb install is owned by the package manager, so ÆTHER
# refuses to self-update there (see updater_install_support in lib.rs) and
# publishing a signature for one would be misleading.
- if: steps.updater.outputs.signed == 'true'
uses: actions/upload-artifact@v4
with:
name: aether-linux-x86_64-updater
retention-days: 14
path: src-tauri/target/release/bundle/appimage/*.AppImage.sig
if-no-files-found: error

linux-arm64:
name: Linux (ARM64)
runs-on: ubuntu-24.04-arm
Expand Down Expand Up @@ -132,7 +175,11 @@ jobs:

- run: bun install --frozen-lockfile
- run: bun run typecheck:web
- run: bun run tauri build --bundles nsis

- id: updater
uses: ./.github/actions/updater-flags

- run: bun run tauri build --bundles nsis ${{ steps.updater.outputs.config }}

- uses: actions/upload-artifact@v4
with:
Expand All @@ -143,6 +190,14 @@ jobs:
src-tauri/target/release/bundle/msi/*.msi
if-no-files-found: error

- if: steps.updater.outputs.signed == 'true'
uses: actions/upload-artifact@v4
with:
name: aether-windows-x86_64-updater
retention-days: 14
path: src-tauri/target/release/bundle/nsis/*.exe.sig
if-no-files-found: error

android:
name: Android (APK)
if: github.event_name == 'workflow_dispatch'
Expand Down Expand Up @@ -196,6 +251,10 @@ jobs:
permissions:
contents: write
steps:
# Needed for scripts/updater-manifest.sh; this job previously only consumed
# artifacts and so did not check out the repository.
- uses: actions/checkout@v4

- name: Download all build artifacts
uses: actions/download-artifact@v4
with:
Expand Down Expand Up @@ -230,6 +289,24 @@ jobs:
copy_first_from_artifact "aether-linux-x86_64" "*.AppImage" "AETHER_amd64.AppImage"
copy_first_from_artifact "aether-linux-arm64" "*.deb" "AETHER_arm64.deb"

# The updater downloads this tarball rather than the DMG. Renamed off
# "ÆTHER.app.tar.gz" because a non-ASCII release asset name has to be
# percent-encoded in latest.json, and that is a needless way to fail.
copy_first_from_artifact "aether-macos-updater" "*.app.tar.gz" "AETHER_macOS.app.tar.gz"

# Writes nothing when no signing key produced signatures, which is the normal
# state before the keypair exists — the release then publishes installers
# only and the app reports the `unconfigured` update status.
- name: Build updater manifest
shell: bash
run: |
scripts/updater-manifest.sh \
"$GITHUB_REF_NAME" "$GITHUB_REPOSITORY" artifacts release-assets/latest.json

if [[ ! -f release-assets/latest.json ]]; then
echo "::warning title=No updater manifest::No updater signatures were produced, so latest.json is not published and in-app updates stay disabled for this release."
fi

- name: Create release and attach installers
uses: softprops/action-gh-release@v2
with:
Expand Down
45 changes: 45 additions & 0 deletions .planning/debug/resolved/ice-interaction-crystallization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
status: resolved
trigger: "iCE card clicks teleport cards to the top-left instead of centering with a slight zoom; Ordered Topics centers without zoom; crystallization intermittently fails for Quantum; percentage labels and Open in Library should be removed."
created: 2026-07-25T20:55:39+0200
updated: 2026-07-25T21:38:00+0200
---

## Symptoms

- expected: Clicking a canvas card or Ordered Topics entry centers that card and slightly zooms the canvas.
- actual: Canvas-card clicks teleport the raised card to the top-left. Ordered Topics centers without zoom.
- errors: The app reports "Crystallization failed." Intermittently reproducible with "Quantum".
- timeline: These behaviors worked on the main branch and regressed during the overhaul.
- reproduction: Generate an iCE map, click canvas cards and Ordered Topics entries, then repeatedly crystallize "Quantum".
- requested cleanup: Remove floating percentage labels and the "Open in Library" action.

## Current Focus

- hypothesis: confirmed
- test: complete
- expecting: fixed
- next_action: user verification in the native app
- reasoning_checkpoint: The first fix moved handlers onto the foreignObject content but left the unsupported transform path in place. It addressed neither the WebKit relocation trigger nor native SVG hit testing.
- tdd_checkpoint: Added a regression test for recovering complete iceberg items from truncated model output.

## Evidence

- Both canvas cards and the canvas pan handler received events through an SVG `foreignObject`. WebKit did not reliably resolve the previous ancestor guard across that namespace boundary.
- Ordered Topics already used the correct `selectItem` and `focusItem` path. Canvas cards now call the same path directly from their native HTML button.
- The normalizer required a closing array bracket. A model response cut off during its final item discarded every complete item generated before it.
- Generation had a single attempt and a 4200-token output ceiling despite requesting up to 45 richly annotated items.
- The first interaction fix left selected, hover, and focus transforms on the `foreignObject`. That is the WebKit operation responsible for relocating cards to SVG origin.
- Direct pointer handling inside the embedded HTML remained dependent on events crossing the HTML/SVG namespace boundary.

## Eliminated

- The pan/zoom centering formula was not the source of the top-left card jump.
- Topic coverage lookup was not involved in canvas selection.

## Resolution

- root_cause: Canvas presses crossed an unreliable HTML-in-SVG event boundary, while selected and focused states transformed the `foreignObject` and triggered WebKit's top-left relocation bug. The 172% focus target was also too weak to communicate focus consistently. Truncated local-model JSON had no recovery or retry path.
- fix: Made embedded HTML presentation-only, added a native SVG hit rectangle, restored SVG-native click and keyboard handling, removed every local transform from the `foreignObject`, and raised the shared canvas/sidebar focus target to 190%. Also added balanced-object recovery for truncated arrays, one compact low-temperature retry, a 5200-token generation allowance, concrete backend errors, and the requested UI removals.
- verification: `bun run typecheck:web`, `bun run lint`, `bun run build:vite`, and `cargo test --lib` all pass. Rust result: 64 passed, 0 failed, 2 ignored. The new truncated-output regression test passes.
- files_changed: src/renderer/src/components/Crystallizer.tsx, src/renderer/src/assets/styles/crystallizer.css, src/renderer/src/App.tsx, src-tauri/src/iceberg.rs, src-tauri/src/inference.rs, src-tauri/src/lib.rs
Loading
Loading