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
1 change: 1 addition & 0 deletions .beads/interactions.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -1089,3 +1089,4 @@
{"id":"int-01d27f504ff5922eb0cd89703b5830e0","kind":"field_change","created_at":"2026-08-03T22:01:18.3227Z","actor":"Val Alexander","issue_id":"cave-2qyqu","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Merged via PR #4298 (landed as 81ea2d5b31). Select All in the session picker now covers only visible sessions via visibleLocalThreads, derived from entries so it cannot drift from what renders; bulk actions still operate on the explicit selection. Also mirrored /auto into the iOS slash catalog as .desktopOnly, unbreaking ios-slash-commands on main."}}
{"id":"int-f96ee9a954c81e3c21296e47b45bbed2","kind":"field_change","created_at":"2026-08-03T22:05:28.47423Z","actor":"Val Alexander","issue_id":"cave-2i9dq","extra":{"field":"assignee","new_value":"Val Alexander","old_value":""}}
{"id":"int-80d43de3534a7839b71c4963802514d5","kind":"field_change","created_at":"2026-08-03T22:09:12.700068Z","actor":"Val Alexander","issue_id":"cave-m1qd0","extra":{"field":"status","new_value":"blocked","old_value":"in_progress"}}
{"id":"int-ace6a979c6318ecf25a0d0f290ce7bad","kind":"field_change","created_at":"2026-08-04T02:39:12.347907Z","actor":"lou","issue_id":"cave-3g5","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented and verified distinct ocd development identity, bundle ID, icon geometry, and native app wrapper while preserving production branding."}}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"scripts": {
"dev": "node --experimental-strip-types server.ts",
"dev:app": "bash scripts/dev-app.sh",
"dev:icons": "node scripts/generate-dev-app-icons.mjs",
"mobile:ios:sim": "bash scripts/ios-simulator.sh",
"uninstall:app": "bash scripts/uninstall-app.sh",
"mobile:tailscale": "bash scripts/mobile-tailscale.sh start",
Expand Down
33 changes: 32 additions & 1 deletion scripts/dev-app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,23 @@ fi
# must never start another one. This makes initial startup fail in the terminal
# rather than presenting a black native window.
cat >"$TAURI_OVERRIDE_CONFIG" <<CONF
{"build":{"beforeDevCommand":null,"devUrl":"${dev_url}"}}
{
"productName": "ocd",
"identifier": "ai.opencoven.cave.dev",
"build": {
"beforeDevCommand": null,
"devUrl": "${dev_url}"
},
"bundle": {
"icon": [
"icons/dev/32x32.png",
"icons/dev/128x128.png",
"icons/dev/128x128@2x.png",
"icons/dev/icon.icns",
"icons/dev/icon.ico"
]
}
}
CONF

watch_dev_server() {
Expand All @@ -186,6 +202,21 @@ watch_dev_server() {
done
}

# `tauri dev` normally executes a raw Cargo binary on macOS, so LaunchServices
# sees a generic `app` process with no bundle ID or application icon. A Cargo
# runner places only the development executable inside a lightweight .app
# wrapper; release builds continue through the ordinary Tauri bundler.
if [ "$(uname -s)" = "Darwin" ]; then
rust_host="$(rustc -vV | sed -n 's/^host: //p')"
if [ -z "$rust_host" ]; then
echo "[dev:app] ERROR: could not determine the Rust host target" >&2
exit 1
fi
runner_variable="CARGO_TARGET_$(printf '%s' "$rust_host" | tr '[:lower:]-' '[:upper:]_')_RUNNER"
export "${runner_variable}=$PWD/scripts/tauri-dev-macos-runner.sh"
echo "[dev:app] macOS identity: ocd (ai.opencoven.cave.dev)"
fi

pnpm exec tauri dev --config "$TAURI_OVERRIDE_CONFIG" "$@" &
tauri_pid=$!

Expand Down
76 changes: 73 additions & 3 deletions scripts/dev-app.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,88 @@ assert.match(
);
assert.match(
source,
/"beforeDevCommand":null,"devUrl":"\$\{dev_url\}"/,
/"beforeDevCommand": null,[\s\S]*?"devUrl": "\$\{dev_url\}"/,
"Tauri must not launch a second server after the launcher has verified the first root document",
);

assert.match(
source,
/"productName": "ocd"/,
"the development shell must have an unmistakable application name",
);
assert.match(
source,
/"identifier": "ai\.opencoven\.cave\.dev"/,
"the development shell must use a distinct OS and WebView data identity",
);
for (const icon of ["32x32.png", "128x128.png", "128x128@2x.png", "icon.icns", "icon.ico"]) {
assert.match(
source,
new RegExp(`"icons/dev/${icon.replace(".", "\\.")}"`),
`the development override must select ${icon}`,
);
assert.ok(
readFileSync(new URL(`../src-tauri/icons/dev/${icon}`, import.meta.url)).length > 0,
`${icon} must be a non-empty development icon asset`,
);
}
assert.notDeepEqual(
readFileSync(new URL("../src-tauri/icons/dev/128x128.png", import.meta.url)),
readFileSync(new URL("../src-tauri/icons/128x128.png", import.meta.url)),
"the development icon must remain visually distinct from the production icon",
);

const productionConfig = JSON.parse(
readFileSync(new URL("../src-tauri/tauri.conf.json", import.meta.url), "utf8"),
);
assert.equal(productionConfig.productName, "CovenCave");
assert.equal(productionConfig.identifier, "ai.opencoven.cave");
assert.ok(
productionConfig.bundle.icon.every((icon) => !icon.includes("icons/dev/")),
"production bundles must keep the production icon set",
);

const nativeWindows = ["tauri_setup.rs", "window_geometry.rs"]
.map((file) => readFileSync(new URL(`../src-tauri/src/${file}`, import.meta.url), "utf8"))
.join("\n");
assert.doesNotMatch(
nativeWindows,
/\.title\("CovenCave(?: Quick Chat| Notch)?"\)/,
"native window titles must follow the configured product name in both development and production",
);
assert.match(
nativeWindows,
/app\.config\(\)\.product_name/,
"native window titles must be derived from Tauri's active product configuration",
);

assert.match(
source,
/if \[ "\$\(uname -s\)" = "Darwin" \]; then[\s\S]*?CARGO_TARGET_[\s\S]*?_RUNNER[\s\S]*?tauri-dev-macos-runner\.sh[\s\S]*?pnpm exec tauri dev/,
"macOS development must launch through the app-bundle runner instead of a generic raw binary",
);
const macosRunner = readFileSync(
new URL("./tauri-dev-macos-runner.sh", import.meta.url),
"utf8",
);
assert.match(macosRunner, /ocd\.app/);
assert.match(macosRunner, /<string>ocd<\/string>/);
assert.match(macosRunner, /<string>ai\.opencoven\.cave\.dev<\/string>/);
assert.match(macosRunner, /icons\/dev\/icon\.icns/);
assert.match(
macosRunner,
/exec "\$bundle_executable" "\$@"/,
"the runner must preserve Cargo's child-process lifecycle for Tauri's watcher and teardown",
);

assert.match(
source,
/if \[ -n "\$\{COVEN_CAVE_AUTH_TOKEN:-\}" \]; then[\s\S]*?encodeURIComponent\(process\.env\.COVEN_CAVE_AUTH_TOKEN\)[\s\S]*?dev_url\+="#covenCaveToken=\$\{sidecar_token_fragment\}"/,
"an inherited sidecar token must reach the desktop webview through the URL hash",
);
assert.match(
source,
/"devUrl":"\$\{dev_url\}"/,
/"devUrl": "\$\{dev_url\}"/,
"both launcher paths must use the token-bearing dev URL",
);

Expand Down Expand Up @@ -77,7 +147,7 @@ assert.match(
);
assert.match(
source,
/initial_timeout_ms=\$\(\(DEV_SERVER_GRACE_SECONDS \* 1000\)\)[\s\S]*?origin_is_ready "\$dev_port" "\$initial_timeout_ms"[\s\S]*?desktop shell was not opened[\s\S]*?beforeDevCommand":null[\s\S]*?pnpm exec tauri dev/,
/initial_timeout_ms=\$\(\(DEV_SERVER_GRACE_SECONDS \* 1000\)\)[\s\S]*?origin_is_ready "\$dev_port" "\$initial_timeout_ms"[\s\S]*?desktop shell was not opened[\s\S]*?beforeDevCommand": null[\s\S]*?pnpm exec tauri dev/,
"the launcher must validate the root document before opening Tauri, avoiding an initial black window",
);
assert.match(
Expand Down
111 changes: 111 additions & 0 deletions scripts/generate-dev-app-icons.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { spawnSync } from "node:child_process";
import {
copyFile,
mkdir,
mkdtemp,
readFile,
rm,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

import sharp from "sharp";

const repoRoot = dirname(dirname(fileURLToPath(import.meta.url)));
const iconRoot = join(repoRoot, "src-tauri", "icons");
const outputRoot = join(iconRoot, "dev");
const sourceIcon = join(iconRoot, "icon-source-1024.png");
const generatedFiles = [
"32x32.png",
"128x128.png",
"128x128@2x.png",
"icon.icns",
"icon.ico",
];

const canvasSize = 1024;
const artworkSize = 824;
const artworkInset = (canvasSize - artworkSize) / 2;

function superellipsePath(size, exponent = 5, segments = 256) {
const radius = size / 2;
const points = [];
for (let index = 0; index <= segments; index += 1) {
const angle = (index / segments) * Math.PI * 2;
const cosine = Math.cos(angle);
const sine = Math.sin(angle);
const x = radius + radius * Math.sign(cosine) * Math.abs(cosine) ** (2 / exponent);
const y = radius + radius * Math.sign(sine) * Math.abs(sine) ** (2 / exponent);
points.push(`${index === 0 ? "M" : "L"}${x.toFixed(2)} ${y.toFixed(2)}`);
}
return `${points.join(" ")} Z`;
}

// Map the production icon's black background to OpenCoven lavender while
// preserving its white mark and grayscale antialiasing.
const background = [0x68, 0x59, 0xac];
const scale = background.map((channel) => (255 - channel) / 255);
const mask = Buffer.from(
`<svg xmlns="http://www.w3.org/2000/svg" width="${artworkSize}" height="${artworkSize}" viewBox="0 0 ${artworkSize} ${artworkSize}"><path d="${superellipsePath(artworkSize)}" fill="white"/></svg>`,
);
const temporaryRoot = await mkdtemp(join(tmpdir(), "coven-cave-dev-icons-"));
const tintedSource = join(temporaryRoot, "icon-source-1024.png");
const generatedRoot = join(temporaryRoot, "generated");

try {
await sharp(sourceIcon)
.linear(scale, background)
.resize(artworkSize, artworkSize)
.composite([{ input: mask, blend: "dest-in" }])
.extend({
top: artworkInset,
bottom: artworkInset,
left: artworkInset,
right: artworkInset,
background: { r: 0, g: 0, b: 0, alpha: 0 },
})
.png()
.toFile(tintedSource);

let outputIsCurrent = false;
try {
const [nextSource, existingSource, ...existingIcons] = await Promise.all([
readFile(tintedSource),
readFile(join(outputRoot, "icon-source-1024.png")),
...generatedFiles.map((file) => readFile(join(outputRoot, file))),
]);
outputIsCurrent =
nextSource.equals(existingSource) &&
existingIcons.every((icon) => icon.length > 0);
} catch {
outputIsCurrent = false;
}

if (outputIsCurrent) {
console.log(`Development app icons are up to date in ${outputRoot}`);
} else {
const pnpm = process.platform === "win32" ? "pnpm.cmd" : "pnpm";
const result = spawnSync(
pnpm,
["exec", "tauri", "icon", tintedSource, "--output", generatedRoot],
{ cwd: repoRoot, encoding: "utf8" },
);
if (result.status !== 0) {
process.stderr.write(result.stderr || result.stdout);
process.exitCode = result.status ?? 1;
throw new Error("Tauri could not generate the development icon set");
}

await mkdir(outputRoot, { recursive: true });
await copyFile(tintedSource, join(outputRoot, "icon-source-1024.png"));
await Promise.all(
generatedFiles.map((file) =>
copyFile(join(generatedRoot, file), join(outputRoot, file)),
),
);
console.log(`Generated development app icons in ${outputRoot}`);
}
} finally {
await rm(temporaryRoot, { recursive: true, force: true });
}
65 changes: 65 additions & 0 deletions scripts/tauri-dev-macos-runner.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# Give the raw `cargo run` executable a real macOS development identity.

set -euo pipefail

if [ "$#" -lt 1 ]; then
echo "[dev:app] ERROR: macOS runner expected the compiled executable" >&2
exit 1
fi

binary="$1"
shift
case "$binary" in
/*) ;;
*) binary="$PWD/$binary" ;;
esac

binary_dir="$(cd "$(dirname "$binary")" && pwd)"
repo_root="$(cd "$(dirname "$0")/.." && pwd)"
bundle_dir="$binary_dir/ocd.app"
contents_dir="$bundle_dir/Contents"
macos_dir="$contents_dir/MacOS"
resources_dir="$contents_dir/Resources"
bundle_executable="$macos_dir/ocd"

mkdir -p "$macos_dir" "$resources_dir"
cp "$binary" "$bundle_executable"
chmod +x "$bundle_executable"
cp "$repo_root/src-tauri/icons/dev/icon.icns" "$resources_dir/icon.icns"

cat >"$contents_dir/Info.plist" <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>ocd</string>
<key>CFBundleExecutable</key>
<string>ocd</string>
<key>CFBundleIconFile</key>
<string>icon.icns</string>
<key>CFBundleIdentifier</key>
<string>ai.opencoven.cave.dev</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>ocd</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.0.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.developer-tools</string>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>
PLIST

/usr/bin/plutil -lint "$contents_dir/Info.plist" >/dev/null
exec "$bundle_executable" "$@"
Binary file added src-tauri/icons/dev/128x128.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src-tauri/icons/dev/128x128@2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src-tauri/icons/dev/32x32.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src-tauri/icons/dev/icon-source-1024.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src-tauri/icons/dev/icon.icns
Binary file not shown.
Binary file added src-tauri/icons/dev/icon.ico
Binary file not shown.
17 changes: 14 additions & 3 deletions src-tauri/src/tauri_setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,12 +213,17 @@ pub fn run() {
tauri::WebviewUrl::App("index.html".into())
};

let product_name = app
.config()
.product_name
.clone()
.unwrap_or_else(|| "CovenCave".to_string());
tauri::WebviewWindowBuilder::new(
app,
"main",
webview_url,
)
.title("CovenCave")
.title(product_name)
.build()?;

Ok(())
Expand Down Expand Up @@ -287,6 +292,12 @@ pub fn run() {
#[cfg(desktop)]
builder
.setup(move |app| {
let product_name = app
.config()
.product_name
.clone()
.unwrap_or_else(|| "CovenCave".to_string());

// The updater's Windows pre-exit path clears the application
// resource table after validating the package and before starting
// msiexec. Dropping this guard stops/reaps the sidecar even though
Expand Down Expand Up @@ -342,7 +353,7 @@ pub fn run() {
#[cfg(target_os = "windows")]
{
WebviewWindowBuilder::new(app, "main", WebviewUrl::App("startup.html".into()))
.title("CovenCave")
.title(product_name.clone())
.inner_size(1320.0, 820.0)
.min_inner_size(960.0, 600.0)
.resizable(true)
Expand Down Expand Up @@ -372,7 +383,7 @@ pub fn run() {
remember_main_startup_url(&main_url);
let mut main_window =
WebviewWindowBuilder::new(app, "main", WebviewUrl::External(main_url))
.title("CovenCave")
.title(product_name.clone())
.inner_size(1320.0, 820.0)
.min_inner_size(960.0, 600.0)
.resizable(true)
Expand Down
Loading
Loading