fix: make local desktop builds reflect current UI state

The desktop test package was able to reuse stale Tauri target output even after the frontend and sidecar were rebuilt, which made UI changes look missing after a build. The local macOS build script now clears that cache by default while retaining an opt-in incremental path.

The project selector also no longer repeats the active branch because branch and worktree are now first-class launch controls beside it.

Constraint: Local macOS test packages must prioritize freshness over incremental build speed
Rejected: Keep branch text in the project chip | duplicated the dedicated branch selector and made the launch row harder to scan
Confidence: high
Scope-risk: narrow
Directive: Keep branch selection behavior in RepositoryLaunchControls; DirectoryPicker should only identify the project
Tested: cd desktop && bun run test -- DirectoryPicker.test.tsx
Tested: cd desktop && bun run lint
Tested: SKIP_INSTALL=1 ./desktop/scripts/build-macos-arm64.sh
Tested: Opened built app and confirmed sidecar /health returned ok
This commit is contained in:
程序员阿江(Relakkes) 2026-05-07 15:57:40 +08:00
parent 7ae885a114
commit a340e41046
3 changed files with 67 additions and 23 deletions

View File

@ -23,6 +23,10 @@ Environment:
SKIP_INSTALL=1 Skip `bun install` in the repo root and desktop app.
SIGN_BUILD=1 Remove the default `--no-sign` flag and allow signed builds.
OPEN_OUTPUT=1 Open the canonical artifact output directory in Finder after a successful build.
PRESERVE_TAURI_TARGET=1
Keep Tauri/Rust target cache for a faster incremental build.
By default this script removes the macOS target cache so
packaged WebView assets cannot be silently reused.
Examples:
./desktop/scripts/build-macos-arm64.sh
@ -62,27 +66,47 @@ if [[ "${SKIP_INSTALL:-0}" != "1" ]]; then
fi
# ── 清理 + 显式预热前端 / sidecar ────────────────────────────
# 之前遇到过"改了 src/ 代码,build 的 .app 里 sidecar 还是旧的"的诡异
# case —— 根因是 Bun.build / Tauri bundler 某一层的缓存把旧 sidecar
# binary 复用进了新 .app(.app 被删干净了也救不回来,因为 binary 源在
# desktop/src-tauri/binaries/)
# 之前遇到过两类"改了源码,build 出来的 .app 还是旧行为"的诡异 case:
# 1) Bun.build / Tauri bundler 某一层缓存把旧 sidecar binary 复用进新 .app
# 2) Tauri/Rust target 缓存复用旧 claude-code-desktop,导致新 dist 没被嵌进去
# 第二类尤其隐蔽: dist 是新的,sidecar 也是新的,但 WebView 运行的还是旧前端
#
# 这里做三件事强制 fresh build:
# 1) 硬删 sidecar 源 binary + Tauri bundle 目录 + 前端 dist
# 默认做四件事强制 fresh build:
# 1) 硬删 sidecar 源 binary + Tauri target/bundle 目录 + 前端 dist
# 2) 显式跑 bun run build + bun run build:sidecars
# 3) tauri build 用 --config 覆盖 beforeBuildCommand 为 true(no-op),
# 避免 sidecar 被重复编译浪费 ~10s
# 4) 复制到 canonical output 前再次清空输出目录
# 任一步失败,整个脚本立即退出(set -e)。
echo "[build-macos-arm64] Cleaning stale sidecar binaries and bundle output..."
echo "[build-macos-arm64] Cleaning stale sidecar binaries, frontend output, and Tauri bundle cache..."
rm -rf "${DESKTOP_DIR}/src-tauri/binaries/claude-sidecar-"*
rm -rf "${DESKTOP_DIR}/src-tauri/target/${TARGET_TRIPLE}/release/bundle"
rm -rf "${DESKTOP_DIR}/src-tauri/target/release/bundle"
rm -rf "${DESKTOP_DIR}/dist"
rm -f "${DESKTOP_DIR}/tsconfig.tsbuildinfo"
rm -rf "${DESKTOP_DIR}/src-tauri/target/${TARGET_TRIPLE}/release/build/claude-code-desktop-"*
rm -rf "${DESKTOP_DIR}/src-tauri/target/${TARGET_TRIPLE}/release/.fingerprint/claude-code-desktop-"*
rm -f "${DESKTOP_DIR}/src-tauri/target/${TARGET_TRIPLE}/release/deps/claude_code_desktop-"*
rm -f "${DESKTOP_DIR}/src-tauri/target/${TARGET_TRIPLE}/release/deps/libclaude_code_desktop-"*
if [[ "${PRESERVE_TAURI_TARGET:-0}" == "1" ]]; then
echo "[build-macos-arm64] PRESERVE_TAURI_TARGET=1: keeping Rust dependency cache, clearing app-specific artifacts only..."
rm -rf "${DESKTOP_DIR}/src-tauri/target/${TARGET_TRIPLE}/release/bundle"
rm -rf "${DESKTOP_DIR}/src-tauri/target/release/bundle"
rm -f "${DESKTOP_DIR}/src-tauri/target/${TARGET_TRIPLE}/release/claude-code-desktop"
rm -f "${DESKTOP_DIR}/src-tauri/target/release/claude-code-desktop"
find "${DESKTOP_DIR}/src-tauri/target/${TARGET_TRIPLE}/release/build" \
-maxdepth 1 -name 'claude-code-desktop-*' -exec rm -rf {} + 2>/dev/null || true
find "${DESKTOP_DIR}/src-tauri/target/${TARGET_TRIPLE}/release/.fingerprint" \
-maxdepth 1 -name 'claude-code-desktop-*' -exec rm -rf {} + 2>/dev/null || true
find "${DESKTOP_DIR}/src-tauri/target/${TARGET_TRIPLE}/release/deps" \
-maxdepth 1 \( -name 'claude_code_desktop-*' -o -name 'libclaude_code_desktop-*' \) -exec rm -f {} + 2>/dev/null || true
else
echo "[build-macos-arm64] Removing Tauri target cache for ${TARGET_TRIPLE} to force fresh embedded frontend assets..."
rm -rf "${DESKTOP_DIR}/src-tauri/target/${TARGET_TRIPLE}"
rm -rf "${DESKTOP_DIR}/src-tauri/target/release/bundle"
rm -f "${DESKTOP_DIR}/src-tauri/target/release/claude-code-desktop"
find "${DESKTOP_DIR}/src-tauri/target/release/build" \
-maxdepth 1 -name 'claude-code-desktop-*' -exec rm -rf {} + 2>/dev/null || true
find "${DESKTOP_DIR}/src-tauri/target/release/.fingerprint" \
-maxdepth 1 -name 'claude-code-desktop-*' -exec rm -rf {} + 2>/dev/null || true
find "${DESKTOP_DIR}/src-tauri/target/release/deps" \
-maxdepth 1 \( -name 'claude_code_desktop-*' -o -name 'libclaude_code_desktop-*' \) -exec rm -f {} + 2>/dev/null || true
fi
echo "[build-macos-arm64] Rebuilding frontend (tsc + vite)..."
(cd "${DESKTOP_DIR}" && bun run build)

View File

@ -1,4 +1,4 @@
import { render, screen } from '@testing-library/react'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import '@testing-library/jest-dom'
@ -15,6 +15,7 @@ vi.mock('../../api/filesystem', () => ({
}))
import { DirectoryPicker } from './DirectoryPicker'
import { sessionsApi } from '../../api/sessions'
describe('DirectoryPicker', () => {
it('uses the source repository name as the fallback label for desktop worktree paths', () => {
@ -28,4 +29,32 @@ describe('DirectoryPicker', () => {
expect(screen.getByRole('button')).toHaveTextContent('checkout')
expect(screen.getByRole('button')).not.toHaveTextContent('desktop-feature-rail-12345678')
})
it('does not duplicate the branch in the selected project chip', async () => {
vi.mocked(sessionsApi.getRecentProjects).mockResolvedValue({
projects: [{
projectPath: '/workspace/project',
realPath: '/workspace/project',
projectName: 'project',
repoName: 'NanmiCoder/OpenCutSkill',
branch: 'main',
isGit: true,
modifiedAt: '2026-05-07T00:00:00.000Z',
sessionCount: 1,
}],
})
render(
<DirectoryPicker
value="/workspace/project"
onChange={vi.fn()}
/>,
)
fireEvent.click(screen.getByRole('button'))
const trigger = await waitFor(() => screen.getAllByRole('button', { name: /NanmiCoder\/OpenCutSkill/ })[0])
expect(trigger).toHaveTextContent('NanmiCoder/OpenCutSkill')
expect(trigger).not.toHaveTextContent('main')
})
})

View File

@ -164,15 +164,6 @@ export function DirectoryPicker({ value, onChange }: Props) {
<span className="font-medium text-[var(--color-text-primary)]">
{selectedProject?.repoName || selectedProject?.projectName || projectNameFromPath(value)}
</span>
{selectedProject?.branch && (
<>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" className="text-[var(--color-text-tertiary)]">
<circle cx="18" cy="18" r="3" /><circle cx="6" cy="6" r="3" />
<path d="M13 6h3a2 2 0 0 1 2 2v7" /><line x1="6" y1="9" x2="6" y2="21" />
</svg>
<span className="text-[var(--color-text-tertiary)]">{selectedProject.branch}</span>
</>
)}
<span className="material-symbols-outlined text-[12px] text-[var(--color-text-tertiary)]">expand_more</span>
</button>
) : (