diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b9ec1d3..2d897ec 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -26,6 +26,14 @@ on:
- patch
- minor
- major
+ mark_critical:
+ description: "Mandatory security update (clients can't dismiss the prompt)"
+ type: boolean
+ default: false
+ advisory_url:
+ description: "Optional advisory/release link shown on the security banner"
+ type: string
+ default: ""
# Never run two releases at once — they race on the version bump + tag.
concurrency:
@@ -212,18 +220,18 @@ jobs:
src-tauri/target/**/release/bundle/**/*.sig
build-flatpak:
- # Wrap the .deb from the `build` job into a distro-agnostic .flatpak bundle
- # (installs on Alpine, Arch, Fedora, Ubuntu, …). x86_64 ONLY: the builder
- # image `bilelmoussaoui/flatpak-github-actions:gnome-47` ships no arm64
- # manifest, so `docker pull` on an arm64 runner fails outright — native
- # aarch64 flatpak isn't buildable with this toolchain. arm64 Linux users are
- # covered by the .deb/.rpm/.AppImage arm64 artifacts; only the arm64 .flatpak
- # is omitted. Revisit if the image gains arm64 (or via QEMU cross-build).
- # The `gnome-47` tag MUST match `runtime-version` in flatpak/ai.canonic.app.yml.
+ # Wrap each arch's .deb (from the `build` job) into a distro-agnostic
+ # .flatpak bundle (installs on Alpine, Arch, Fedora, Ubuntu, …). Built
+ # NATIVELY per-arch on matching runners: x86_64 on ubuntu-latest, aarch64 on
+ # ubuntu-24.04-arm. We do NOT use the `bilelmoussaoui/...:gnome-47` container
+ # because it ships no arm64 image (docker pull fails on arm64 runners) —
+ # instead flatpak-builder is apt-installed and the GNOME runtime pulled from
+ # Flathub, which works identically on both arches. The runtime version below
+ # MUST match `runtime-version` in flatpak/ai.canonic.app.yml.
needs:
- compute-version
- build
- timeout-minutes: 30
+ timeout-minutes: 45
strategy:
fail-fast: false
matrix:
@@ -232,14 +240,33 @@ jobs:
artifact: release-linux-x64
deb_glob: "*_amd64.deb"
arch: x86_64
+ - runner: ubuntu-24.04-arm
+ artifact: release-linux-arm64
+ deb_glob: "*_arm64.deb"
+ arch: aarch64
runs-on: ${{ matrix.runner }}
- container:
- image: bilelmoussaoui/flatpak-github-actions:gnome-47
- options: --privileged
steps:
- name: Checkout code
uses: actions/checkout@v6
+ - name: Install Flatpak toolchain
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y flatpak flatpak-builder
+ # GitHub's ubuntu-24.04 images restrict unprivileged user namespaces
+ # via AppArmor, which breaks flatpak-builder's bwrap sandbox. Relax it
+ # (no-op if the knob doesn't exist on the runner image).
+ sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || true
+
+ - name: Install GNOME runtime + SDK (native arch)
+ # The manifest builds against org.gnome.{Platform,Sdk}//47. Flathub
+ # serves both x86_64 and aarch64, so this resolves to the runner's arch.
+ run: |
+ flatpak remote-add --if-not-exists --user \
+ flathub https://flathub.org/repo/flathub.flatpakrepo
+ flatpak install --user -y --noninteractive flathub \
+ org.gnome.Platform//47 org.gnome.Sdk//47
+
- name: Download Linux build (.deb)
uses: actions/download-artifact@v8
with:
@@ -265,12 +292,13 @@ jobs:
flatpak/ai.canonic.app.metainfo.xml
- name: Build Flatpak bundle
- uses: flatpak/flatpak-github-actions/flatpak-builder@401fe28a8384095fc1531b9d320b292f0ee45adb # v6.7
- with:
- bundle: canonic_${{ needs.compute-version.outputs.version }}_${{ matrix.arch }}.flatpak
- manifest-path: flatpak/ai.canonic.app.yml
- # Per-arch cache so the two matrix legs don't clobber each other.
- cache-key: flatpak-builder-${{ matrix.arch }}-${{ github.sha }}
+ env:
+ BUNDLE: canonic_${{ needs.compute-version.outputs.version }}_${{ matrix.arch }}.flatpak
+ run: |
+ flatpak-builder --user --force-clean --disable-rofiles-fuse \
+ --install-deps-from=flathub \
+ --repo=flatpak-repo flatpak-build flatpak/ai.canonic.app.yml
+ flatpak build-bundle flatpak-repo "$BUNDLE" ai.canonic.app master
- name: Upload Flatpak artifact
uses: actions/upload-artifact@v7
@@ -358,16 +386,24 @@ jobs:
# with its signed bundle URL + detached signature (read from the .sig
# files). VERSION/REPO are passed via env (never interpolated into the
# script) so untrusted input can't reach the shell or node.
+ # MARK_CRITICAL/ADVISORY_URL flag a mandatory security update: from the
+ # workflow_dispatch input, or a `security-critical` label on the merged
+ # PR. They become unsigned manifest fields the client enforces (the
+ # binary is still signature-verified, so this can only force-with-update).
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN || secrets.GITHUB_TOKEN }}
VERSION: ${{ needs.compute-version.outputs.version }}
REPO: Canonical-AI/canonic
+ MARK_CRITICAL: ${{ github.event.inputs.mark_critical == 'true' || contains(github.event.pull_request.labels.*.name, 'security-critical') }}
+ ADVISORY_URL: ${{ github.event.inputs.advisory_url }}
run: |
node <<'EOF'
const fs = require('fs');
const path = require('path');
const version = process.env.VERSION;
const repo = process.env.REPO;
+ const critical = process.env.MARK_CRITICAL === 'true';
+ const advisory = process.env.ADVISORY_URL || '';
// Recursively collect every downloaded artifact file.
const files = [];
@@ -408,12 +444,22 @@ jobs:
throw new Error('no updater artifacts found — check createUpdaterArtifacts + signing secrets');
}
- fs.writeFileSync('latest.json', JSON.stringify({
+ const manifest = {
version,
notes: `See https://github.com/${repo}/releases/tag/v${version}`,
pub_date: new Date().toISOString(),
platforms,
- }, null, 2));
+ };
+ // Custom fields the client reads via Update.raw_json to force a
+ // mandatory, non-dismissible security update.
+ if (critical) {
+ manifest.critical = true;
+ manifest.severity = 'critical';
+ manifest.advisory =
+ advisory || `https://github.com/${repo}/releases/tag/v${version}`;
+ }
+
+ fs.writeFileSync('latest.json', JSON.stringify(manifest, null, 2));
console.log(fs.readFileSync('latest.json', 'utf8'));
EOF
gh release upload "v$VERSION" latest.json --repo "$REPO" --clobber
diff --git a/README.md b/README.md
index 85e9c92..66d671b 100644
--- a/README.md
+++ b/README.md
@@ -66,12 +66,13 @@ Full usage guide, keyboard shortcuts, and feature walkthroughs:
curl -fsSL https://raw.githubusercontent.com/Canonical-AI/canonic/main/install.sh | sh
```
-Detects your OS and architecture, downloads the right build, and installs it.
+Detects your OS, architecture, and Linux distro, downloads the right build, and installs it.
-- **macOS** (Apple Silicon) → `/Applications/Canonic.app`
-- **Linux** (x64 / arm64) → `~/.local/bin/canonic` (AppImage)
+- **macOS** (Apple Silicon) → `/Applications/canonic.app`
+- **Linux** (glibc, x64 / arm64) → `~/.local/bin/canonic` (AppImage)
+- **Arch / Alpine** (x64 / arm64) → Flatpak `ai.canonic.app` (AppImage can't run on musl-libc Alpine; Arch ships no FUSE by default)
-To pin a specific version: `curl -fsSL ... | sh -s -- --version v0.1.2-alpha`
+To pin a specific version: `curl -fsSL ... | sh -s -- --version v0.2.2-alpha`
@@ -84,35 +85,41 @@ brew install --cask canonic
### Linux
-**AppImage**
+**AppImage** (most glibc distros — Ubuntu, Fedora, …):
```bash
-chmod +x canonic-*.AppImage
-./canonic-*.AppImage
+chmod +x canonic_*.AppImage
+./canonic_*.AppImage
```
-**Flatpak** (recommended on Alpine-based / postmarketOS):
+**Flatpak** (Alpine / postmarketOS / Arch — and any musl-libc distro; x86_64 & arm64):
```bash
-flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
-flatpak install --user canonic-*.flatpak
+flatpak remote-add --if-not-exists --user flathub https://flathub.org/repo/flathub.flatpakrepo
+flatpak install --user canonic_*.flatpak
flatpak run ai.canonic.app
```
-**Pacman** (Arch / postmarketOS):
+**Debian / Ubuntu** (`.deb`):
```bash
-sudo pacman -U canonic-*.pacman
+sudo apt install ./canonic_*.deb
+```
+
+**Fedora / RHEL** (`.rpm`):
+
+```bash
+sudo dnf install ./canonic_*.rpm
```
### Direct download (all platforms)
Grab the latest build for your platform from the [Releases](https://github.com/Canonical-AI/canonic/releases) page:
-* **macOS** — `.dmg` (Apple Silicon / Intel)
+* **macOS** — `.dmg` (universal — Apple Silicon & Intel)
* **Windows** — `.exe`
-* **Linux x64** — `.AppImage`, `.flatpak`, `.deb`, `.pacman`, `.tar.gz`
-* **Linux arm64** — `.AppImage`, `.flatpak`, `.deb`, `.pacman`, `.tar.gz` (ARM Arch, postmarketOS, PinePhone, Raspberry Pi)
+* **Linux x64** — `.AppImage`, `.flatpak`, `.deb`, `.rpm`
+* **Linux arm64** — `.AppImage`, `.deb`, `.flatpak` (Raspberry Pi, PinePhone, postmarketOS, ARM servers)
diff --git a/docs/specs/REQUIREMENTS.md b/docs/specs/REQUIREMENTS.md
index af1aa2f..30d9e6b 100644
--- a/docs/specs/REQUIREMENTS.md
+++ b/docs/specs/REQUIREMENTS.md
@@ -640,6 +640,21 @@ Source of truth for product requirements. When a requirement changes, update thi
when: the comments panel opens
then: the version filter is set to "All versions" and every unresolved comment is shown
+* scenario: anchor text still present highlights normally
+ given: a comment whose quoted anchor text still appears in the open document
+ when: the comments panel is shown
+ then: the comment highlights in the document and shows no "text changed" warning
+
+* scenario: anchor text changed shows a warning
+ given: a comment whose quoted anchor text no longer appears in the open document (it was edited)
+ when: the comments panel is shown
+ then: the comment is marked stale and shows a "Text has changed" badge instead of a missing highlight
+
+* scenario: resolved or short anchors are never flagged stale
+ given: a resolved comment, or a comment whose quoted text is shorter than 2 characters
+ when: anchors are evaluated against the document
+ then: the comment is never marked stale
+
***
## Browser Commenting (BCM)
@@ -812,6 +827,11 @@ Source of truth for product requirements. When a requirement changes, update thi
when: an action is selected
then: the trigger `/` character is removed from the document before the block is inserted
+* scenario: insert today's date
+ given: the slash menu is open at the root
+ when: the user selects "Today's date" (or types `today` and presses Enter)
+ then: the current date is inserted at the caret in ISO 8601 format (YYYY-MM-DD) using the local calendar day, and the trigger `/` is removed
+
### Agent slash commands
* scenario: root menu lists Review and Build
@@ -2016,6 +2036,38 @@ GNOME runtime carries glibc and webkit2gtk inside the sandbox.
when: the pipeline reaches finalize
then: no version bump, tag, or release is published (finalize depends on the Flatpak job)
+## One-Line Installer (PKG-INS)
+
+`install.sh` is the curl-pipe-sh installer. It detects OS, CPU architecture, and
+Linux distro, then downloads the matching release asset (names match Tauri's
+bundle output, e.g. `canonic__universal.dmg`,
+`canonic__amd64.AppImage`, `canonic__x86_64.flatpak`).
+
+* scenario: install on macOS
+ given: a user runs the installer on macOS (Apple Silicon)
+ when: the script detects `Darwin`
+ then: it downloads `canonic__universal.dmg`, mounts it, copies the `.app` into `/Applications`, and clears the Gatekeeper quarantine flag
+
+* scenario: install on a glibc Linux distro
+ given: a user on a glibc distro (e.g. Ubuntu, Fedora) that is neither Arch nor Alpine
+ when: the script detects the distro
+ then: it downloads the AppImage (`amd64` on x86_64, `aarch64` on arm64) to `~/.local/bin/canonic` and marks it executable
+
+* scenario: install on Arch or Alpine
+ given: a user on Arch or Alpine, x86_64 or arm64 (detected via `/etc/os-release` ID/ID_LIKE, or `/etc/arch-release` / `/etc/alpine-release`)
+ when: the script selects an install method
+ then: it installs the Flatpak matching the CPU (`canonic__x86_64.flatpak` or `canonic__aarch64.flatpak`) — adds the Flathub remote, then `flatpak install --user` — because Alpine is musl-libc and Arch ships no FUSE for AppImages
+
+* scenario: Flatpak missing on Arch or Alpine
+ given: an Arch or Alpine user without `flatpak` installed
+ when: the installer reaches the Flatpak step
+ then: it exits with a message telling them to install flatpak (`doas apk add flatpak` / `sudo pacman -S flatpak`) and re-run
+
+* scenario: pin a specific version
+ given: a user passes `--version `
+ when: the script resolves the version
+ then: it skips the GitHub "latest release" lookup and installs the named tag
+
## Auto-Update (UPD)
Canonic updates itself in place using the Tauri updater. On launch it checks the
@@ -2238,11 +2290,33 @@ pinned in `tauri.conf.json`, so an unsigned or tampered package is rejected.
when: the app starts
then: no greeting is shown and the stored target is cleared
+### Mandatory security updates
+
+* scenario: a critical update can't be dismissed
+ given: the release manifest marks the update `critical`
+ when: the update is detected
+ then: the over-editor banner and the left-panel widget both show a red "Security update required" notice with no close button (overriding the usual dismiss)
+
+* scenario: advisory link is shown for security updates
+ given: a critical update whose manifest includes an advisory URL
+ when: the security banner is shown
+ then: it includes an "Advisory" link that opens the advisory in the browser
+
+* scenario: forcing is best-effort and non-destructive
+ given: the critical flag lives in the unsigned part of the manifest
+ when: the client enforces it
+ then: it only nags/forces-with-update — the downloaded binary is still signature-verified before install, so a tampered flag can never run a malicious build
+
+* scenario: releases can be flagged critical from CI
+ given: a release is published
+ when: the `mark_critical` workflow input is true or the merged PR has the `security-critical` label
+ then: `latest.json` is written with `critical: true`, `severity: critical`, and an advisory URL
+
### Demo Mode
* scenario: update flow is demoable end to end
given: the app is in demo mode
- when: the user downloads and restarts the fake update
- then: progress animates, then an "Updated to v0.3.0" greeting with a release-notes link is shown — no real binary or network is touched
+ when: the user downloads and restarts the fake (critical) update
+ then: a non-dismissible "Security update required" banner is shown, progress animates, then an "Updated to v0.3.0" greeting with a release-notes link — no real binary or network is touched
*Last updated: 2026-06-10*
diff --git a/install.sh b/install.sh
index f2d1e64..5aed620 100755
--- a/install.sh
+++ b/install.sh
@@ -3,7 +3,7 @@
# curl -fsSL https://raw.githubusercontent.com/Canonical-AI/canonic/main/install.sh | sh
#
# Optional: pin a version
-# curl -fsSL ... | sh -s -- --version v0.1.2-alpha
+# curl -fsSL ... | sh -s -- --version v0.2.2-alpha
set -u
@@ -53,7 +53,39 @@ case "$ARCH" in
esac
if [ "$PLATFORM" = "macos" ] && [ "$ARCH_NORM" != "arm64" ]; then
- err "Canonic only builds for Apple Silicon (arm64) on macOS. Intel Macs are not supported."
+ # The macOS .dmg is a universal binary, but the installer is only verified on
+ # Apple Silicon. Intel Macs can use the .dmg from the Releases page directly.
+ warn "Intel Macs are not officially supported; the universal .dmg may still work."
+fi
+
+# --- Linux distro detection --------------------------------------------
+# Arch and Alpine get the Flatpak instead of the AppImage (both arches):
+# • Alpine is musl-libc — the glibc AppImage will not run at all.
+# • Arch ships no FUSE by default, so AppImages need extra setup; the Flatpak
+# bundles its own GNOME runtime and just works.
+# Flatpak bundles are built for both x86_64 and aarch64, so no per-arch fallback.
+INSTALL_METHOD="appimage" # default for Linux
+DISTRO_PRETTY="Linux"
+
+if [ "$PLATFORM" = "linux" ]; then
+ DISTRO_ID=""
+ DISTRO_LIKE=""
+ if [ -r /etc/os-release ]; then
+ # Parse rather than `source` so we don't clobber $VERSION (os-release
+ # defines its own VERSION=).
+ DISTRO_ID=$(grep -E '^ID=' /etc/os-release | head -n1 | cut -d= -f2 | tr -d '"')
+ DISTRO_LIKE=$(grep -E '^ID_LIKE=' /etc/os-release | head -n1 | cut -d= -f2 | tr -d '"')
+ DISTRO_PRETTY=$(grep -E '^PRETTY_NAME=' /etc/os-release | head -n1 | cut -d= -f2 | tr -d '"')
+ fi
+ [ -f /etc/alpine-release ] && DISTRO_ID="alpine"
+ [ -f /etc/arch-release ] && [ -z "$DISTRO_ID" ] && DISTRO_ID="arch"
+ [ -z "$DISTRO_PRETTY" ] && DISTRO_PRETTY="Linux"
+
+ case " ${DISTRO_ID} ${DISTRO_LIKE} " in
+ *" alpine "* | *" arch "*)
+ INSTALL_METHOD="flatpak"
+ ;;
+ esac
fi
# --- resolve version ---------------------------------------------------
@@ -65,69 +97,108 @@ if [ -z "$VERSION" ]; then
|| err "Could not fetch latest release from GitHub API"
fi
+# Release assets embed the bare version (no leading "v"); the tag keeps it.
+VERSION_NOV="${VERSION#v}"
+
info "Installing Canonic ${VERSION} for ${PLATFORM}/${ARCH_NORM}"
-# --- pick the right asset -----------------------------------------------
-case "${PLATFORM}-${ARCH_NORM}" in
- macos-arm64)
- ASSET="canonic-arm64.zip"
- ASSET_TYPE="zip"
- ;;
- linux-x86_64)
- ASSET="canonic-x86_64.AppImage"
- ASSET_TYPE="appimage"
+# --- pick the right asset ----------------------------------------------
+# Names match Tauri's bundle output (productName=canonic), exactly as attached
+# to each GitHub release.
+case "$PLATFORM" in
+ macos)
+ ASSET="canonic_${VERSION_NOV}_universal.dmg"
;;
- linux-arm64)
- ASSET="canonic-arm64.AppImage"
- ASSET_TYPE="appimage"
+ linux)
+ if [ "$INSTALL_METHOD" = "flatpak" ]; then
+ # Flatpak bundles use x86_64 / aarch64 in their filename.
+ [ "$ARCH_NORM" = "x86_64" ] && FLATPAK_ARCH="x86_64" || FLATPAK_ARCH="aarch64"
+ ASSET="canonic_${VERSION_NOV}_${FLATPAK_ARCH}.flatpak"
+ elif [ "$ARCH_NORM" = "x86_64" ]; then
+ ASSET="canonic_${VERSION_NOV}_amd64.AppImage"
+ else
+ ASSET="canonic_${VERSION_NOV}_aarch64.AppImage"
+ fi
;;
esac
DOWNLOAD_URL="https://github.com/${REPO}/releases/download/${VERSION}/${ASSET}"
-# --- download -----------------------------------------------------------
+# --- download ----------------------------------------------------------
TMPDIR=$(mktemp -d)
-trap 'rm -rf "$TMPDIR"' EXIT
+MOUNT=""
+cleanup() {
+ [ -n "$MOUNT" ] && hdiutil detach "$MOUNT" >/dev/null 2>&1
+ rm -rf "$TMPDIR"
+}
+trap cleanup EXIT
info "Downloading ${ASSET}…"
curl -fsSL --progress-bar -o "${TMPDIR}/${ASSET}" "$DOWNLOAD_URL" \
- || err "Download failed"
+ || err "Download failed (${DOWNLOAD_URL})"
+
+# --- install -----------------------------------------------------------
+LAUNCH_HINT="canonic"
-# --- install ------------------------------------------------------------
case "$PLATFORM" in
macos)
- info "Extracting…"
- unzip -qo "${TMPDIR}/${ASSET}" -d "$TMPDIR" || err "Unzip failed"
+ info "Mounting ${ASSET}…"
+ MOUNT=$(mktemp -d)
+ hdiutil attach -nobrowse -quiet -mountpoint "$MOUNT" "${TMPDIR}/${ASSET}" \
+ || err "Could not mount DMG"
+
+ APP_SRC=$(find "$MOUNT" -maxdepth 1 -name '*.app' | head -n1)
+ [ -n "$APP_SRC" ] || err "No .app found inside the DMG"
+ APP_NAME=$(basename "$APP_SRC")
- APP_NAME="Canonic.app"
if [ -d "/Applications/${APP_NAME}" ]; then
- warn "Existing Canonic.app found in /Applications — replacing"
+ warn "Existing /Applications/${APP_NAME} found — replacing"
rm -rf "/Applications/${APP_NAME}"
fi
- info "Installing to /Applications/Canonic.app…"
- mv "${TMPDIR}/${APP_NAME}" /Applications/ \
- || err "Could not move to /Applications (try running with sudo)"
+ info "Installing to /Applications/${APP_NAME}…"
+ cp -R "$APP_SRC" /Applications/ \
+ || err "Could not copy to /Applications (try running with sudo)"
# Remove quarantine flag so Gatekeeper doesn't block it
- if ! xattr -d com.apple.quarantine "/Applications/${APP_NAME}" 2>/dev/null; then
+ if ! xattr -dr com.apple.quarantine "/Applications/${APP_NAME}" 2>/dev/null; then
warn "Could not clear quarantine flag. You may need to right-click → Open the app."
fi
+ LAUNCH_HINT="open -a ${APP_NAME%.app}"
;;
linux)
- INSTALL_DIR="${HOME}/.local/bin"
- mkdir -p "$INSTALL_DIR"
-
- info "Installing to ${INSTALL_DIR}/canonic…"
- cp "${TMPDIR}/${ASSET}" "${INSTALL_DIR}/canonic" \
- || err "Could not copy to ${INSTALL_DIR}"
- chmod +x "${INSTALL_DIR}/canonic"
-
- if ! echo "$PATH" | tr ':' '\n' | grep -Fqx "$INSTALL_DIR"; then
- warn "${INSTALL_DIR} is not in your PATH."
- echo " Add this to your shell config (~/.bashrc, ~/.zshrc, etc.):"
- echo " export PATH=\"\$HOME/.local/bin:\$PATH\""
+ if [ "$INSTALL_METHOD" = "flatpak" ]; then
+ command -v flatpak >/dev/null 2>&1 || err "Flatpak is required on ${DISTRO_PRETTY} but isn't installed.
+ Install it, then re-run this script:
+ Alpine: doas apk add flatpak
+ Arch: sudo pacman -S flatpak"
+
+ # The bundle depends on the GNOME runtime; Flathub provides it.
+ info "Ensuring Flathub remote…"
+ flatpak remote-add --if-not-exists --user \
+ flathub https://flathub.org/repo/flathub.flatpakrepo \
+ || warn "Could not add Flathub remote; runtime download may fail."
+
+ info "Installing Canonic Flatpak…"
+ flatpak install --user -y "${TMPDIR}/${ASSET}" \
+ || err "flatpak install failed"
+ LAUNCH_HINT="flatpak run ai.canonic.app"
+ else
+ INSTALL_DIR="${HOME}/.local/bin"
+ mkdir -p "$INSTALL_DIR"
+
+ info "Installing to ${INSTALL_DIR}/canonic…"
+ cp "${TMPDIR}/${ASSET}" "${INSTALL_DIR}/canonic" \
+ || err "Could not copy to ${INSTALL_DIR}"
+ chmod +x "${INSTALL_DIR}/canonic"
+
+ if ! echo "$PATH" | tr ':' '\n' | grep -Fqx "$INSTALL_DIR"; then
+ warn "${INSTALL_DIR} is not in your PATH."
+ echo " Add this to your shell config (~/.bashrc, ~/.zshrc, etc.):"
+ echo " export PATH=\"\$HOME/.local/bin:\$PATH\""
+ fi
+ LAUNCH_HINT="canonic"
fi
;;
esac
@@ -135,6 +206,6 @@ esac
echo ""
printf " ${GREEN}✓${NC} Canonic ${BOLD}${VERSION}${NC} installed successfully!\n"
echo ""
-echo " Launch it from your app launcher (macOS) or run:"
-echo " ${BOLD}canonic${NC}"
+echo " Launch it from your app launcher, or run:"
+echo " ${BOLD}${LAUNCH_HINT}${NC}"
echo ""
diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png
index a3ca3da..1382566 100644
Binary files a/src-tauri/icons/128x128.png and b/src-tauri/icons/128x128.png differ
diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png
index dcc926b..15a6cdb 100644
Binary files a/src-tauri/icons/128x128@2x.png and b/src-tauri/icons/128x128@2x.png differ
diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png
index 0a280dc..256b7af 100644
Binary files a/src-tauri/icons/32x32.png and b/src-tauri/icons/32x32.png differ
diff --git a/src-tauri/icons/64x64.png b/src-tauri/icons/64x64.png
index 66047b6..29d9d89 100644
Binary files a/src-tauri/icons/64x64.png and b/src-tauri/icons/64x64.png differ
diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png
index b8c1a4f..48926bf 100644
Binary files a/src-tauri/icons/icon.png and b/src-tauri/icons/icon.png differ
diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs
index e34a5ac..a1fed75 100644
--- a/src-tauri/src/commands.rs
+++ b/src-tauri/src/commands.rs
@@ -2677,10 +2677,27 @@ pub async fn update_check(app: tauri::AppHandle) -> Result {
let updater = app.updater().map_err(|e| e.to_string())?;
match updater.check().await {
Ok(Some(update)) => {
+ // Custom (unsigned) manifest fields drive security forcing. The
+ // binary itself is still signature-verified at install, so these
+ // flags can only nag/force-with-update — never run a bad binary.
+ // `critical: true` makes the update mandatory for every client old
+ // enough to see it (the plugin only returns one when out of date).
+ let raw = &update.raw_json;
+ let mandatory = raw
+ .get("critical")
+ .and_then(|v| v.as_bool())
+ .unwrap_or(false);
+ let advisory = raw
+ .get("advisory")
+ .and_then(|v| v.as_str())
+ .unwrap_or("")
+ .to_string();
let info = json!({
"version": update.version.clone(),
"notes": update.body.clone(),
"date": update.date.map(|d| d.to_string()),
+ "mandatory": mandatory,
+ "advisory": advisory,
});
*pending_update().lock().unwrap() =
Some(PendingUpdate { update, bytes: None });
diff --git a/src/components/editor/MilkdownEditor.vue b/src/components/editor/MilkdownEditor.vue
index a828663..4c66b03 100644
--- a/src/components/editor/MilkdownEditor.vue
+++ b/src/components/editor/MilkdownEditor.vue
@@ -9,6 +9,7 @@
import { watch, ref, provide, computed, onUnmounted, reactive } from "vue";
import { Milkdown, useEditor } from "@milkdown/vue";
import { useAppStore } from "../../store";
+import { findStaleCommentIds } from "../../utils/comment-anchors.js";
import {
Editor,
rootCtx,
@@ -1300,11 +1301,14 @@ const { loading, get } = useEditor((root) =>
...prev,
editable: () => !editorReadonly.value,
}));
- ctx.get(listenerCtx).markdownUpdated((_, markdown) => {
+ ctx.get(listenerCtx).markdownUpdated((lctx, markdown) => {
const fixed = stripEmptyLineBr(
markdown.replace(/\\\[\\\[/g, "[["),
);
emit("update", fixed);
+ // Re-evaluate which anchors still match after the edit.
+ const view = lctx.get(editorViewCtx);
+ if (view) reportStaleAnchors(view, props.comments || []);
});
ctx.update(prosePluginsCtx, (plugins) => [
...plugins,
@@ -1364,9 +1368,19 @@ function dispatchHighlights(comments) {
const view = ctx.get(editorViewCtx);
if (!view) return;
view.dispatch(view.state.tr.setMeta(HIGHLIGHT_KEY, comments || []));
+ reportStaleAnchors(view, comments || []);
});
}
+// The editor is the source of truth for whether a comment highlights: it
+// searches the same block-separated plain text buildDecorations uses. Report
+// comments whose anchor text is no longer found so the panel can flag them.
+function reportStaleAnchors(view, comments) {
+ const doc = view.state.doc;
+ const plain = doc.textBetween(0, doc.content.size, "\n", "\n");
+ store.setStaleCommentIds(findStaleCommentIds(plain, comments));
+}
+
watch(loading, (isLoading) => {
if (!isLoading) dispatchHighlights(props.comments);
});
diff --git a/src/components/editor/slash-menu/SlashMenuTooltip.vue b/src/components/editor/slash-menu/SlashMenuTooltip.vue
index 44ba29d..de1ee5c 100644
--- a/src/components/editor/slash-menu/SlashMenuTooltip.vue
+++ b/src/components/editor/slash-menu/SlashMenuTooltip.vue
@@ -72,6 +72,7 @@ import {
Hammer,
Bot,
Settings,
+ Calendar,
} from "lucide-vue-next";
import { useAppStore } from "../../../store";
@@ -90,6 +91,12 @@ let onCloseCallback = null;
const MENU_DATA = {
main: [
{ id: "insert", label: "Insert", icon: Type, submenu: "insert" },
+ {
+ id: "today",
+ label: "Today's date",
+ icon: Calendar,
+ action: "insert-today",
+ },
{
id: "review",
label: "Review with agent",
diff --git a/src/components/editor/slash-menu/index.js b/src/components/editor/slash-menu/index.js
index 4713e22..303cb27 100644
--- a/src/components/editor/slash-menu/index.js
+++ b/src/components/editor/slash-menu/index.js
@@ -11,6 +11,15 @@ export function slashTriggerAllowed({ parentOffset, charBefore }) {
return charBefore === " ";
}
+// Today's date in ISO 8601 (YYYY-MM-DD), built from local calendar components
+// so it never drifts a day near midnight (toISOString would use UTC).
+export function formatToday(date = new Date()) {
+ const y = date.getFullYear();
+ const m = String(date.getMonth() + 1).padStart(2, "0");
+ const d = String(date.getDate()).padStart(2, "0");
+ return `${y}-${m}-${d}`;
+}
+
export const createSlashMenuPlugin = (tooltipRef) => {
let lastModITime = 0;
@@ -161,6 +170,9 @@ function handleAction(view, pos, action) {
tr.replaceWith(mappedPos, mappedPos, schema.nodes.hr.create());
}
break;
+ case "insert-today":
+ tr.insertText(formatToday(), mappedPos, mappedPos);
+ break;
case "blockquote":
case "bullet-list":
case "ordered-list":
diff --git a/src/components/layout/MainLayout.vue b/src/components/layout/MainLayout.vue
index d719026..b3992af 100644
--- a/src/components/layout/MainLayout.vue
+++ b/src/components/layout/MainLayout.vue
@@ -24,34 +24,6 @@