From cf6853389601ca4be4d106e2b2a39c8b1eabe81e Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Tue, 31 Mar 2026 14:09:58 +1100 Subject: [PATCH] feat: add branch-based dev icons and stable ports for worktree disambiguation (#19) * feat: add branch-based dev icon for worktree disambiguation Add a Swift script that generates a modified .icns file with a blue badge containing the current branch name. When running in a git worktree, the `just dev` and `just dev-debug` recipes automatically detect this and generate a badged icon, making it easy to distinguish between multiple dev instances in the dock. - scripts/generate-dev-icon.swift: Generates badged icons with text wrapping on hyphens, multi-size iconset generation via iconutil - justfile: dev and dev-debug recipes now detect worktrees and pass icon override config to Tauri via --config JSON merge - Generated icons go to src-tauri/target/dev-icons/ (already gitignored) * feat: derive stable dev port from working directory for worktree disambiguation Each worktree gets a stable, unique port derived from the SHA-256 hash of the working directory path (range 10000-64999). This prevents port conflicts when running multiple dev instances simultaneously. - justfile: dev and dev-debug recipes compute VITE_PORT and pass it via --config JSON merge to override devUrl and beforeDevCommand - vite.config.ts: server port and HMR port read from VITE_PORT env var, falling back to 1520 for standalone frontend dev (just dev-frontend) - Icon config in dev recipe now merges into existing TAURI_CONFIG via python3 JSON manipulation instead of replacing it - Icon config in dev-debug recipe appends a separate --config flag --- ui/goose2/justfile | 58 ++++++- ui/goose2/scripts/generate-dev-icon.swift | 197 ++++++++++++++++++++++ ui/goose2/vite.config.ts | 4 +- 3 files changed, 255 insertions(+), 4 deletions(-) create mode 100755 ui/goose2/scripts/generate-dev-icon.swift diff --git a/ui/goose2/justfile b/ui/goose2/justfile index dad2fb35ef..fe69dbec06 100644 --- a/ui/goose2/justfile +++ b/ui/goose2/justfile @@ -71,11 +71,65 @@ test-e2e-all: # Start the desktop app in dev mode dev: - pnpm tauri dev + #!/usr/bin/env bash + set -euo pipefail + + # Derive a stable port from the working directory so the same worktree + # always gets the same port. This avoids changing TAURI_CONFIG between + # runs, which would invalidate Cargo's build cache and trigger a full + # Rust rebuild every time. + VITE_PORT=$(python3 -c "import hashlib,os; h=int(hashlib.sha256(os.getcwd().encode()).hexdigest(),16); print(10000 + h % 55000)") + export VITE_PORT + TAURI_CONFIG="{\"build\":{\"devUrl\":\"http://localhost:${VITE_PORT}\",\"beforeDevCommand\":{\"script\":\"exec ./node_modules/.bin/vite --port ${VITE_PORT} --strictPort\",\"cwd\":\"..\",\"wait\":false}}}" + + # In worktrees, generate a labeled icon so you can tell instances apart + if git rev-parse --is-inside-work-tree &>/dev/null; then + GIT_DIR=$(git rev-parse --git-dir) + if [[ "$GIT_DIR" == *".git/worktrees/"* ]]; then + BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) + WORKTREE_LABEL="${BRANCH_NAME##*/}" + + ICON_DIR="$(pwd)/src-tauri/target/dev-icons" + mkdir -p "$ICON_DIR" + DEV_ICON="$ICON_DIR/icon.icns" + + if swift scripts/generate-dev-icon.swift src-tauri/icons/icon.icns "$DEV_ICON" "$WORKTREE_LABEL"; then + echo "🌳 Worktree: ${WORKTREE_LABEL}" + TAURI_CONFIG=$(python3 -c "import json,sys; a=json.loads(sys.argv[1]); a['bundle']={'icon':['$DEV_ICON']}; print(json.dumps(a))" "$TAURI_CONFIG") + fi + fi + fi + + pnpm tauri dev --config "$TAURI_CONFIG" # Start the desktop app with dev config dev-debug: - pnpm tauri dev --config src-tauri/tauri.dev.conf.json + #!/usr/bin/env bash + set -euo pipefail + + VITE_PORT=$(python3 -c "import hashlib,os; h=int(hashlib.sha256(os.getcwd().encode()).hexdigest(),16); print(10000 + h % 55000)") + export VITE_PORT + EXTRA_CONFIG="--config {\"build\":{\"devUrl\":\"http://localhost:${VITE_PORT}\",\"beforeDevCommand\":{\"script\":\"exec ./node_modules/.bin/vite --port ${VITE_PORT} --strictPort\",\"cwd\":\"..\",\"wait\":false}}}" + + # In worktrees, generate a labeled icon so you can tell instances apart + if git rev-parse --is-inside-work-tree &>/dev/null; then + GIT_DIR=$(git rev-parse --git-dir) + if [[ "$GIT_DIR" == *".git/worktrees/"* ]]; then + BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) + WORKTREE_LABEL="${BRANCH_NAME##*/}" + + ICON_DIR="$(pwd)/src-tauri/target/dev-icons" + mkdir -p "$ICON_DIR" + DEV_ICON="$ICON_DIR/icon.icns" + + if swift scripts/generate-dev-icon.swift src-tauri/icons/icon.icns "$DEV_ICON" "$WORKTREE_LABEL"; then + echo "🌳 Worktree: ${WORKTREE_LABEL}" + EXTRA_CONFIG="$EXTRA_CONFIG --config {\"bundle\":{\"icon\":[\"$DEV_ICON\"]}}" + fi + fi + fi + + pnpm tauri dev --config src-tauri/tauri.dev.conf.json $EXTRA_CONFIG # Start only the frontend dev server dev-frontend: diff --git a/ui/goose2/scripts/generate-dev-icon.swift b/ui/goose2/scripts/generate-dev-icon.swift new file mode 100755 index 0000000000..bca8c60b73 --- /dev/null +++ b/ui/goose2/scripts/generate-dev-icon.swift @@ -0,0 +1,197 @@ +#!/usr/bin/env swift + +import AppKit +import Foundation + +// Generate a dev icon with worktree name badge +// Usage: generate-dev-icon.swift