Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ec35dda
update Tauri to 2.12, raise Rust to 1.90, argon2 0.6, Vitest 5 and n…
ztcs-software Sep 30, 2026
c409894
Rename update notes caption to "Release notes" to stop duplicate "Wha…
ztcs-software Sep 30, 2026
6605d18
add /stop, /find, /import, /label, /folder, /tone, /set, /unload, /th…
ztcs-software Sep 30, 2026
024575a
complete model, space, snippet and persona names in the slash palett…
ztcs-software Sep 30, 2026
2eef0f2
add sandboxed HTML preview to the code canvas
ztcs-software Sep 30, 2026
b2434f3
add sandboxed HTML preview to the code canvas
ztcs-software Sep 30, 2026
135708c
switch release workflow to tauri-action v1
ztcs-software Oct 1, 2026
7f5d0ed
Add Prompt gallery link to the Snippets page
ztcs-software Oct 1, 2026
fcf4745
Fix bugs in Library retrieval, indexer and composer
ztcs-software Oct 18, 2026
e9df47b
fix(macos): guard IME Enter, use Cmd-only shortcuts, reopen from Dock…
ztcs-software Oct 18, 2026
630a9e5
fix(macos): use a template tray icon so the menu bar shows the fish, …
ztcs-software Oct 18, 2026
a6549a8
feat(macos): use native window buttons over the custom title bar
ztcs-software Oct 18, 2026
9878c1b
feat(macos): hide the window on close and lock it when an app lock is…
ztcs-software Oct 18, 2026
2a5e2f3
feat(library): open datasets as their own page with a Spaces-style br…
ztcs-software Oct 20, 2026
30b9575
fix(library): keep the embedding index correct through outages, refus…
ztcs-software Oct 20, 2026
dbac9f2
fix(library): keep unreadable Space uploads out of prompts, stop lost…
ztcs-software Oct 20, 2026
4ea11ed
docs: document the Library in README, FEATURES and TROUBLESHOOTING, a…
ztcs-software Oct 20, 2026
92e083e
fix(library): report only sources that fit, flag Library read failure…
ztcs-software Oct 20, 2026
78c1e82
fix(library): keep store lists consistent under racing reads, scale e…
ztcs-software Oct 20, 2026
7e8a20c
fix(spaces,library,snippets): start new chats from a Space's page, se…
ztcs-software Oct 21, 2026
e596911
fix(navigation): move chats between Spaces, add Library items to a Sp…
ztcs-software Oct 21, 2026
af9574b
fix(consistency): unify action wording, dialog labels/buttons/Enter h…
ztcs-software Oct 21, 2026
60977cc
feat(spaces): gather a Space's default model, memory auto-save and so…
ztcs-software Oct 21, 2026
8723c0d
fix(spaces,library,snippets): keep slash commands on a Space's page o…
ztcs-software Oct 21, 2026
ccc3580
fix(library): send dataset images resized and named, unify the model …
ztcs-software Oct 28, 2026
fade615
fix model view width
ztcs-software Oct 28, 2026
b820764
fix: close open confirms on lock, read the lock state from a marker f…
ztcs-software Oct 29, 2026
b48aacc
fix: address the medium-severity findings from the 1.6.0 review — per…
ztcs-software Oct 29, 2026
d9a8db7
fix: address the low-severity findings from the 1.6.0 review — provid…
ztcs-software Oct 29, 2026
9256317
fix: address the remaining 1.6.0 review findings — scripts-off HTML p…
ztcs-software Oct 30, 2026
3d2cb2b
feat: add app, Library, Space and per-chat image scaling (off, max px…
ztcs-software Oct 31, 2026
fa6a9f0
docs: replace the README screenshot with an animated 1.6.0 demo
ztcs-software Oct 31, 2026
e8e1203
Update to Loach v1.6.0
ztcs-software Nov 4, 2026
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
21 changes: 15 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ jobs:
- run: npm ci
- run: npm run build
- run: npm test
# React hooks rules only (rules-of-hooks, exhaustive-deps) — see
# .oxlintrc.json. oxlint rather than ESLint: typescript-eslint can't
# parse with TypeScript 7.
- run: npm run lint

rust:
name: cargo test
Expand All @@ -66,7 +70,7 @@ jobs:
# Pinned to the same toolchain as release.yml so a surprise rustc
# release can't make CI and the release build disagree. Bump both
# together.
- uses: dtolnay/rust-toolchain@1.88.0
- uses: dtolnay/rust-toolchain@1.90.0

- uses: Swatinem/rust-cache@v2
with:
Expand All @@ -82,16 +86,18 @@ jobs:
# flag in src/ollama_launch.rs. A mistake in one of those would pass every
# PR check and first fail inside release.yml's build matrix, potentially
# after another platform's job has already pushed the tag (which then
# forces the delete-tag-and-draft recovery dance). `check` rather than
# `test`: the point is that the platform's code compiles, and the tests
# themselves are platform-independent.
# forces the delete-tag-and-draft recovery dance). The whole suite isn't
# run here — most tests are platform-independent — but the workspace file
# tools' are not: drive letters, reserved names, 8.3 short names and
# junctions only exist on Windows, and their confinement tests never ran
# anywhere else. (The job keeps its name: branch protection matches it.)
rust-windows:
name: cargo check (windows)
runs-on: windows-latest
steps:
- uses: actions/checkout@v7

- uses: dtolnay/rust-toolchain@1.88.0
- uses: dtolnay/rust-toolchain@1.90.0

- uses: Swatinem/rust-cache@v2
with:
Expand All @@ -100,6 +106,9 @@ jobs:
- run: cargo check --locked --all-targets
working-directory: src-tauri

- run: cargo test --locked --lib tools::fs
working-directory: src-tauri

# macOS-only compile check. Until this existed, the macOS target was built
# for the FIRST time inside release.yml's matrix — so a mistake in a
# `#[cfg(target_os = "macos")]` arm (the `open -a` launch path in
Expand All @@ -120,7 +129,7 @@ jobs:
steps:
- uses: actions/checkout@v7

- uses: dtolnay/rust-toolchain@1.88.0
- uses: dtolnay/rust-toolchain@1.90.0

- uses: Swatinem/rust-cache@v2
with:
Expand Down
219 changes: 205 additions & 14 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
should_release: ${{ steps.gate.outputs.should_release }}
release_body: ${{ steps.notes.outputs.body }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0 # need full history to inspect existing tags

Expand Down Expand Up @@ -100,6 +100,19 @@ jobs:
echo 'RELEASE_NOTES_EOF'
} >> "$GITHUB_OUTPUT"

# Push the tag before anything builds. The gate above keys on it, and
# nothing else creates it in time: tauri-action only drafts the release,
# and GitHub creates a draft's tag when the draft is published. Without
# this, a push to main between the draft and Publish passed the gate
# again and rebuilt the draft's assets from the newer commit.
- name: Tag the release commit
if: steps.gate.outputs.should_release == 'true'
run: |
set -euo pipefail
TAG="v${{ steps.gate.outputs.version }}"
git tag "$TAG" "$GITHUB_SHA"
git push origin "refs/tags/$TAG"

build:
needs: prepare
if: needs.prepare.outputs.should_release == 'true'
Expand Down Expand Up @@ -145,11 +158,15 @@ jobs:
git curl ca-certificates xz-utils sudo
git config --global --add safe.directory '*'

- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Tauri-action needs full history to compute the right tag base
# for the auto-created tag. Matches `prepare`'s checkout.
fetch-depth: 0
# Nothing in this job pushes (tauri-action talks to the API with
# GITHUB_TOKEN), so don't leave the token in .git/config for
# `npm ci` install scripts and build scripts to find.
persist-credentials: false

# Runs as root inside the container, so no sudo. build-essential,
# pkg-config and libssl-dev come preinstalled on the runner image but
Expand All @@ -173,7 +190,7 @@ jobs:
wget \
desktop-file-utils

- uses: actions/setup-node@v7
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 22
cache: npm
Expand All @@ -185,18 +202,120 @@ jobs:
# opaque failure that costs hours to diagnose. Bump this manually
# when intentionally adopting a new toolchain.
#
# 1.88.0 is the floor imposed by current transitive deps: `toml 0.9` /
# `serde_spanned 1.1` need edition2024 (1.85+), `icu_*` need 1.86+,
# and `darling`, `image`, `serde_with`, `time` need 1.88+.
- uses: dtolnay/rust-toolchain@1.88.0
# 1.90.0 is the floor imposed by current deps: `tauri` 2.12 and its
# plugins, `muda` and `tray-icon` need 1.90+; before them `darling`,
# `image`, `serde_with`, `time` set it at 1.88.
#
# Every action is pinned to a commit, not a tag: a moved tag would run
# new code in a job that holds the updater signing key.
- uses: dtolnay/rust-toolchain@7dcc700092a1a8b9c17ba0e140b4e34e93000946 # 1.90.0
with:
toolchain: 1.90.0

- uses: Swatinem/rust-cache@v2
- uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 # v2.9.3
with:
workspaces: src-tauri

# `tauri build` runs cargo without --locked, so a Cargo.toml change
# merged without its Cargo.lock refresh would ship whatever versions
# resolve today. Fail first instead, as CI's `cargo test --locked` does.
- name: Check Cargo.lock is current
working-directory: src-tauri
shell: bash
run: cargo metadata --locked --format-version 1 > /dev/null

- run: npm ci

- uses: tauri-apps/tauri-action@v0
# The frontend is built here, apart from `tauri build`, which would run
# it as its beforeBuildCommand with the updater signing key in the
# environment — where vite, its plugins and every other dev dependency
# could read it. The tauri-action step skips that hook (see its
# `--config`). TAURI_ENV_PLATFORM is the one variable the CLI would
# have set that vite.config.ts reads (it picks the build target).
- name: Build the frontend
run: npm run build
env:
TAURI_ENV_PLATFORM: ${{ matrix.platform == 'windows-latest' && 'windows' || (matrix.platform == 'macos-14' && 'darwin' || 'linux') }}

# Signs the macOS app with a stable self-signed identity instead of the
# ad-hoc one in tauri.conf.json, once the two MACOS_SIGNING_* secrets
# exist. Gatekeeper treats both alike (neither is notarized); the
# Keychain doesn't. It ties Loach's saved items (API key, app-lock hash)
# to the signature's designated requirement, and an ad-hoc requirement
# is the binary's own hash — so after every update macOS took Loach for
# a different app and asked for the login password before it could read
# its own secrets. A certificate's requirement stays the same across
# builds. (Installs updating from an ad-hoc build are asked once more.)
#
# Not tauri's own APPLE_CERTIFICATE import: that only looks for
# Apple-issued certificates ("Developer ID Application:" …) and fails on
# a self-signed one. Exporting APPLE_SIGNING_IDENTITY instead makes the
# bundler run plain `codesign -s <identity>` against the keychain search
# list, skipping notarization as it does now. Any problem here falls
# back to the ad-hoc signature with a warning rather than failing the
# leg — by then another leg may already have pushed the tag.
#
# One-time setup, with OpenSSL (in Git Bash, prefix each command with
# MSYS_NO_PATHCONV=1 so `-subj` isn't rewritten into a Windows path):
# openssl req -x509 -newkey rsa:2048 -sha256 -days 3650 -nodes \
# -keyout loach-signing.key -out loach-signing.crt \
# -subj "/CN=Loach Code Signing" \
# -addext "keyUsage=critical,digitalSignature" \
# -addext "extendedKeyUsage=critical,codeSigning"
# openssl pkcs12 -export -inkey loach-signing.key -in loach-signing.crt \
# -keypbe PBE-SHA1-3DES -certpbe PBE-SHA1-3DES -macalg sha1 \
# -out loach-signing.p12
# (SHA1/3DES because macOS `security import` rejects OpenSSL 3's default
# PKCS#12 encryption.) Store `openssl base64 -A -in loach-signing.p12` as
# MACOS_SIGNING_CERTIFICATE and the export password as
# MACOS_SIGNING_CERTIFICATE_PASSWORD. Keep the .p12 as private as the
# updater key — whoever holds it can sign an app the Keychain takes for
# Loach — and keep it: a replacement costs every install another prompt.
- name: Import macOS signing identity
if: matrix.platform == 'macos-14'
env:
MACOS_SIGNING_CERTIFICATE: ${{ secrets.MACOS_SIGNING_CERTIFICATE }}
MACOS_SIGNING_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_SIGNING_CERTIFICATE_PASSWORD }}
run: |
# The default shell runs with -e; every failure below is handled
# explicitly so it can fall back to ad-hoc instead.
set +e -uo pipefail
adhoc() {
echo "::warning::$1 — this build keeps the ad-hoc signature."
exit 0
}
if [ -z "$MACOS_SIGNING_CERTIFICATE" ]; then
echo "::notice::MACOS_SIGNING_CERTIFICATE not set — keeping the ad-hoc signature."
exit 0
fi
KC="$RUNNER_TEMP/loach-signing.keychain-db"
KC_PW=$(openssl rand -hex 16)
P12="$RUNNER_TEMP/loach-signing.p12"
trap 'rm -f "$P12"' EXIT
printf '%s' "$MACOS_SIGNING_CERTIFICATE" | base64 --decode > "$P12" \
|| adhoc "MACOS_SIGNING_CERTIFICATE isn't valid base64"
security create-keychain -p "$KC_PW" "$KC" || adhoc "couldn't create a keychain"
# No auto-lock: the release build can outlast any timeout.
security set-keychain-settings "$KC"
security unlock-keychain -p "$KC_PW" "$KC"
security import "$P12" -k "$KC" -P "$MACOS_SIGNING_CERTIFICATE_PASSWORD" -T /usr/bin/codesign \
|| adhoc "couldn't import the certificate (wrong password, or not a SHA1/3DES .p12?)"
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KC_PW" "$KC" >/dev/null \
|| adhoc "couldn't let codesign use the key"
security list-keychains -d user -s "$KC" $(security list-keychains -d user | tr -d '"')
# Self-signed, so it's listed as untrusted, which codesign doesn't
# mind. The SHA-1 names it unambiguously.
ID=$(security find-identity -p codesigning "$KC" | grep -m1 -oE '[0-9A-F]{40}') \
|| adhoc "no code-signing identity in MACOS_SIGNING_CERTIFICATE"
# Sign a scratch binary the way the bundler will before relying on it.
cp /usr/bin/true "$RUNNER_TEMP/probe"
codesign --force -s "$ID" --options runtime "$RUNNER_TEMP/probe" \
&& codesign --verify "$RUNNER_TEMP/probe" \
|| adhoc "codesign refused the identity"
codesign -d -r- "$RUNNER_TEMP/probe"
echo "APPLE_SIGNING_IDENTITY=$ID" >> "$GITHUB_ENV"

- uses: tauri-apps/tauri-action@1deb371b0cd8bd54025b384f1cd735e725c4060f # v1.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
Expand All @@ -205,15 +324,87 @@ jobs:
# provide; this makes it self-extract instead. Harmless elsewhere.
APPIMAGE_EXTRACT_AND_RUN: 1
with:
# tauri-action will create + push this tag at the commit being built
# if it doesn't already exist. First matrix job to finish creates
# the release; subsequent jobs upload their assets to the same one.
# The `prepare` job already pushed this tag at the commit being
# built. First matrix job to finish creates the release; subsequent
# jobs upload their assets to the same one.
tagName: v${{ needs.prepare.outputs.version }}
releaseName: 'Loach v${{ needs.prepare.outputs.version }}'
releaseBody: ${{ needs.prepare.outputs.release_body }}
releaseDraft: true
# Anything with a pre-release qualifier (e.g. `-alpha`, `-beta`,
# `-rc.1`) is flagged prerelease; bare `1.2.3` ships as stable.
prerelease: ${{ contains(needs.prepare.outputs.version, '-') }}
includeUpdaterJson: true
args: ${{ matrix.args }}
# Since v1 the `url`s in latest.json are API asset links
# (api.github.com/.../releases/assets/<id>), stable across the
# draft -> publish switch. Every shipped install (updater plugin
# 2.10.1+) downloads them fine: it sends Accept:
# application/octet-stream and follows the redirect to the file.
uploadUpdaterJson: true
# prebuilt-frontend.conf.json empties beforeBuildCommand: the
# frontend was built in its own step, without the signing key.
# (A file, not inline JSON, so no shell or argument splitting can
# mangle its quotes. TAURI_CONFIG doesn't reach the build hooks.)
# Cargo's build scripts still see the key — the bundler signs in
# this same `tauri build`.
args: ${{ matrix.args }} --config src-tauri/prebuilt-frontend.conf.json

# tauri-action merges each leg's entries into latest.json by downloading,
# editing and re-uploading it, so two legs finishing together can drop one
# platform's entries. Check the merged file once every leg is done: a gap
# fails here, loudly, instead of reaching installs as an update that never
# arrives. (The fix is re-running the build leg for the missing platform.)
verify-updater-json:
needs: [prepare, build]
runs-on: ubuntu-24.04
steps:
- name: Check latest.json lists every platform
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ needs.prepare.outputs.version }}
run: |
set -euo pipefail
TAG="v$VERSION"
# Drafts aren't reachable by tag, so find it in the release list.
ASSET_ID=$(gh api "repos/$GITHUB_REPOSITORY/releases" --paginate \
--jq ".[] | select(.tag_name == \"$TAG\") | .assets[] | select(.name == \"latest.json\") | .id" | head -n1)
if [ -z "$ASSET_ID" ]; then
echo "::error::No latest.json on the $TAG release"
exit 1
fi
gh api -H "Accept: application/octet-stream" \
"repos/$GITHUB_REPOSITORY/releases/assets/$ASSET_ID" > latest.json

ANNOUNCED=$(jq -r '.version' latest.json)
if [ "${ANNOUNCED#v}" != "$VERSION" ]; then
echo "::error::latest.json announces $ANNOUNCED, not $VERSION"
exit 1
fi
problems=()
# The keys CLAUDE.md's release checklist expects (v1.2.3's set).
for key in windows-x86_64 windows-x86_64-nsis \
linux-x86_64 linux-x86_64-appimage linux-x86_64-deb linux-x86_64-rpm \
darwin-aarch64 darwin-aarch64-app; do
sig=$(jq -r --arg k "$key" '.platforms[$k].signature // ""' latest.json)
url=$(jq -r --arg k "$key" '.platforms[$k].url // ""' latest.json)
if [ -z "$sig" ] || [ -z "$url" ]; then
problems+=("$key: missing")
continue
fi
# The signed version must match the announced one — updater 2.13+
# rejects a mismatch (SignedVersionMismatch).
comment=$(printf '%s' "$sig" | base64 -d | grep -m1 '^trusted comment:' || true)
case "$comment" in
*"version:$VERSION"*) ;;
*) problems+=("$key: signed for another version ($comment)") ;;
esac
# Installs on updater < 2.10 read only the plain key, and must
# get the AppImage, never a .deb/.rpm.
if [ "$key" = linux-x86_64 ] && ! printf '%s' "$comment" | grep -q 'file:[^[:space:]]*\.AppImage'; then
problems+=("linux-x86_64: not the AppImage ($comment)")
fi
done
if [ ${#problems[@]} -gt 0 ]; then
printf '::error::%s\n' "${problems[@]}"
exit 1
fi
echo "latest.json lists every platform, signed for $VERSION"
18 changes: 18 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react"],
"categories": {
"correctness": "off",
"suspicious": "off",
"pedantic": "off",
"perf": "off",
"style": "off",
"restriction": "off",
"nursery": "off"
},
"rules": {
"react/rules-of-hooks": "error",
"react/exhaustive-deps": "error"
},
"ignorePatterns": ["dist/**", "src-tauri/**"]
}
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,13 @@ RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*

# Pinned to the toolchain CI uses (dtolnay/rust-toolchain@1.88.0) rather than
# Pinned to the toolchain CI uses (dtolnay/rust-toolchain 1.90.0) rather than
# `stable`, for the same reason CI pins it: a surprise rustc release shouldn't
# be able to break a build that worked yesterday. Minimal profile — the build
# image has no use for rustfmt/clippy. Keep in step with `rust-version` in
# src-tauri/Cargo.toml.
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --default-toolchain 1.88.0 --profile minimal
| sh -s -- -y --default-toolchain 1.90.0 --profile minimal

WORKDIR /src

Expand Down
Loading
Loading