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 @@
- - - - -