diff --git a/.gitignore b/.gitignore index 8f9ceab5..3e661dce 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ e2e-*.png # Desktop build output desktop/dist/ +desktop/build-artifacts/ desktop/src-tauri/binaries/ desktop/src-tauri/target/ desktop/src-tauri/gen/ @@ -57,3 +58,4 @@ sidebar-snapshot.md # VitePress docs/.vitepress/dist docs/.vitepress/cache +runtime/__pycache__ \ No newline at end of file diff --git a/desktop/scripts/build-macos-arm64.sh b/desktop/scripts/build-macos-arm64.sh index 5ec896a7..ee211a25 100755 --- a/desktop/scripts/build-macos-arm64.sh +++ b/desktop/scripts/build-macos-arm64.sh @@ -7,7 +7,10 @@ DESKTOP_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" REPO_ROOT="$(cd "${DESKTOP_DIR}/.." && pwd)" TARGET_TRIPLE="aarch64-apple-darwin" -OUTPUT_DIR="${DESKTOP_DIR}/src-tauri/target/release/bundle/dmg" +TAURI_TARGET_DIR="${DESKTOP_DIR}/src-tauri/target" +CANONICAL_OUTPUT_DIR="${DESKTOP_DIR}/build-artifacts/macos-arm64" +APP_BUNDLE_NAME="Claude Code Haha.app" +APP_BUNDLE_ID="com.claude-code-haha.desktop" usage() { cat <<'EOF' @@ -19,7 +22,7 @@ Usage: 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 DMG output directory in Finder after a successful build. + OPEN_OUTPUT=1 Open the canonical artifact output directory in Finder after a successful build. Examples: ./desktop/scripts/build-macos-arm64.sh @@ -43,7 +46,7 @@ if [[ "$(uname -m)" != "arm64" ]]; then exit 1 fi -for command in bun cargo rustc; do +for command in bun cargo rustc codesign hdiutil; do if ! command -v "${command}" >/dev/null 2>&1; then echo "[build-macos-arm64] Missing required command: ${command}" >&2 exit 1 @@ -65,7 +68,7 @@ TAURI_ARGS=( --target "${TARGET_TRIPLE}" --bundles - dmg + app,dmg --ci ) @@ -84,16 +87,113 @@ echo "[build-macos-arm64] Building DMG for ${TARGET_TRIPLE}..." "${TAURI_ARGS[@]}" ) -LATEST_DMG="$(find "${OUTPUT_DIR}" -maxdepth 1 -type f -name '*.dmg' | sort | tail -n 1 || true)" +TARGETED_DMG_DIR="${TAURI_TARGET_DIR}/${TARGET_TRIPLE}/release/bundle/dmg" +FALLBACK_DMG_DIR="${TAURI_TARGET_DIR}/release/bundle/dmg" +TARGETED_APP_DIR="${TAURI_TARGET_DIR}/${TARGET_TRIPLE}/release/bundle/macos" +FALLBACK_APP_DIR="${TAURI_TARGET_DIR}/release/bundle/macos" +LEGACY_BUNDLE_ROOT="${TAURI_TARGET_DIR}/release/bundle" + +mkdir -p "${CANONICAL_OUTPUT_DIR}" +find "${CANONICAL_OUTPUT_DIR}" -mindepth 1 -maxdepth 1 -exec rm -rf {} + + +find_latest_file() { + local search_dir="$1" + local pattern="$2" + if [[ -d "${search_dir}" ]]; then + find "${search_dir}" -maxdepth 1 -type f -name "${pattern}" | sort | tail -n 1 + fi +} + +find_latest_dir() { + local search_dir="$1" + local pattern="$2" + if [[ -d "${search_dir}" ]]; then + find "${search_dir}" -maxdepth 1 -type d -name "${pattern}" | sort | tail -n 1 + fi +} + +LATEST_DMG="$(find_latest_file "${TARGETED_DMG_DIR}" '*.dmg')" +if [[ -z "${LATEST_DMG}" ]]; then + LATEST_DMG="$(find_latest_file "${FALLBACK_DMG_DIR}" '*.dmg')" +fi + +LATEST_APP="$(find_latest_dir "${TARGETED_APP_DIR}" '*.app')" +if [[ -z "${LATEST_APP}" ]]; then + LATEST_APP="$(find_latest_dir "${FALLBACK_APP_DIR}" '*.app')" +fi + +sign_app_bundle() { + local app_bundle="$1" + local sidecar_path="${app_bundle}/Contents/MacOS/claude-sidecar" + + if [[ -f "${sidecar_path}" ]]; then + codesign --force --sign - --identifier "${APP_BUNDLE_ID}.sidecar" --timestamp=none "${sidecar_path}" + fi + + codesign --force --deep --sign - --identifier "${APP_BUNDLE_ID}" --timestamp=none "${app_bundle}" +} + +build_canonical_dmg() { + local app_bundle="$1" + local dmg_output="$2" + local staging_dir + + staging_dir="$(mktemp -d "${TMPDIR:-/tmp}/cc-haha-dmg.XXXXXX")" + cp -R "${app_bundle}" "${staging_dir}/" + ln -s /Applications "${staging_dir}/Applications" + + hdiutil create \ + -volname "Claude Code Haha" \ + -srcfolder "${staging_dir}" \ + -ov \ + -format UDZO \ + "${dmg_output}" >/dev/null + + rm -rf "${staging_dir}" +} + +if [[ -n "${LATEST_DMG}" ]]; then + cp -f "${LATEST_DMG}" "${CANONICAL_OUTPUT_DIR}/" +fi + +if [[ -n "${LATEST_APP}" ]]; then + cp -R "${LATEST_APP}" "${CANONICAL_OUTPUT_DIR}/" + sign_app_bundle "${CANONICAL_OUTPUT_DIR}/${APP_BUNDLE_NAME}" + rm -f "${CANONICAL_OUTPUT_DIR}/"*.dmg + build_canonical_dmg \ + "${CANONICAL_OUTPUT_DIR}/${APP_BUNDLE_NAME}" \ + "${CANONICAL_OUTPUT_DIR}/$(basename "${LATEST_DMG:-Claude Code Haha_0.1.0_aarch64.dmg}")" +fi + +cat > "${CANONICAL_OUTPUT_DIR}/BUILD_INFO.txt" <&2 + echo "[build-macos-arm64] No DMG found in ${TARGETED_DMG_DIR} or ${FALLBACK_DMG_DIR}" >&2 fi -if [[ "${OPEN_OUTPUT:-0}" == "1" ]]; then - open "${OUTPUT_DIR}" +if [[ -n "${LATEST_APP}" ]]; then + echo "[build-macos-arm64] App source: ${LATEST_APP}" +else + echo "[build-macos-arm64] No .app found in ${TARGETED_APP_DIR} or ${FALLBACK_APP_DIR}" >&2 +fi + +echo "[build-macos-arm64] Canonical output: ${CANONICAL_OUTPUT_DIR}" +echo "[build-macos-arm64] Removed legacy bundle dir: ${LEGACY_BUNDLE_ROOT}" + +if [[ "${OPEN_OUTPUT:-0}" == "1" ]]; then + open "${CANONICAL_OUTPUT_DIR}" fi diff --git a/desktop/scripts/build-sidecars.ts b/desktop/scripts/build-sidecars.ts index d98597df..3bf453be 100644 --- a/desktop/scripts/build-sidecars.ts +++ b/desktop/scripts/build-sidecars.ts @@ -155,6 +155,29 @@ async function compileExecutable({ throw new Error(`[build-sidecars] Failed to compile ${productName}:\n${logs}`) } - const outputPath = result.outputs[0]?.path - console.log(`[build-sidecars] ${productName} -> ${outputPath ?? outfileBase}`) + const outputPath = result.outputs[0]?.path ?? outfileBase + console.log(`[build-sidecars] ${productName} -> ${outputPath}`) + + // macOS Apple System Policy (ASP) requires valid code signatures on all + // executables. Bun-compiled binaries ship with an invalid/empty signature + // that causes "load code signature error 4" and SIGKILL at launch. + // Fix: strip the broken signature, then ad-hoc sign. + if (process.platform === 'darwin') { + console.log(`[build-sidecars] ad-hoc signing ${outputPath} for macOS ...`) + const strip = Bun.spawn(['codesign', '--remove-signature', outputPath], { + stdout: 'inherit', + stderr: 'inherit', + }) + await strip.exited + + const sign = Bun.spawn( + ['codesign', '--sign', '-', '--force', '--timestamp=none', outputPath], + { stdout: 'inherit', stderr: 'inherit' }, + ) + const signExit = await sign.exited + if (signExit !== 0) { + throw new Error(`[build-sidecars] ad-hoc codesign failed for ${outputPath} (exit ${signExit})`) + } + console.log(`[build-sidecars] ad-hoc signed ${outputPath}`) + } } diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 744fda3c..b7735708 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -53,6 +53,22 @@ "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico" - ] + ], + "macOS": { + "dmg": { + "appPosition": { + "x": 180, + "y": 170 + }, + "applicationFolderPosition": { + "x": 480, + "y": 170 + }, + "windowSize": { + "width": 660, + "height": 400 + } + } + } } } diff --git a/desktop/src/api/client.ts b/desktop/src/api/client.ts index 3d9d07e0..6b2e570e 100644 --- a/desktop/src/api/client.ts +++ b/desktop/src/api/client.ts @@ -24,14 +24,14 @@ export class ApiError extends Error { } } -async function request(method: string, path: string, body?: unknown): Promise { +async function request(method: string, path: string, body?: unknown, options?: { timeout?: number }): Promise { const url = `${baseUrl}${path}` const headers: Record = { 'Content-Type': 'application/json', } const controller = new AbortController() - const timeout = setTimeout(() => controller.abort(), 30_000) + const timeout = setTimeout(() => controller.abort(), options?.timeout ?? 30_000) try { const res = await fetch(url, { method, @@ -55,8 +55,8 @@ async function request(method: string, path: string, body?: unknown): Promise } export const api = { - get: (path: string) => request('GET', path), - post: (path: string, body?: unknown) => request('POST', path, body), + get: (path: string, options?: { timeout?: number }) => request('GET', path, undefined, options), + post: (path: string, body?: unknown, options?: { timeout?: number }) => request('POST', path, body, options), put: (path: string, body?: unknown) => request('PUT', path, body), patch: (path: string, body?: unknown) => request('PATCH', path, body), delete: (path: string) => request('DELETE', path), diff --git a/desktop/src/i18n/locales/en.ts b/desktop/src/i18n/locales/en.ts index 0ce41e71..0e2ef2ca 100644 --- a/desktop/src/i18n/locales/en.ts +++ b/desktop/src/i18n/locales/en.ts @@ -233,6 +233,46 @@ export const en = { 'settings.about.author': 'Author', 'settings.about.socialMedia': 'Social Media', + // Settings > Computer Use + 'settings.tab.computerUse': 'Computer Use', + 'settings.computerUse.title': 'Computer Use', + 'settings.computerUse.description': 'Allow Claude to take screenshots, click, type, and control your computer. Requires Python 3 and macOS accessibility permissions.', + 'settings.computerUse.notSupported': 'Computer Use is only supported on macOS.', + 'settings.computerUse.python': 'Python 3', + 'settings.computerUse.pythonNotFound': 'Not installed. Please install Python 3 first.', + 'settings.computerUse.pythonFound': 'Installed', + 'settings.computerUse.venv': 'Virtual Environment', + 'settings.computerUse.venvReady': 'Ready', + 'settings.computerUse.venvNotReady': 'Not created', + 'settings.computerUse.deps': 'Dependencies', + 'settings.computerUse.depsReady': 'Installed', + 'settings.computerUse.depsNotReady': 'Not installed', + 'settings.computerUse.accessibility': 'Accessibility Permission', + 'settings.computerUse.screenRecording': 'Screen Recording Permission', + 'settings.computerUse.permGranted': 'Granted', + 'settings.computerUse.permDenied': 'Denied — open System Settings > Privacy & Security to grant', + 'settings.computerUse.permUnknown': 'Setup environment first to check', + 'settings.computerUse.permScreenRecordingUnknownSoft': 'Auto-detection can be unreliable here; if System Settings already shows it enabled, Computer Use can usually proceed', + 'settings.computerUse.permScreenRecordingManual': 'Cannot auto-detect — please verify in System Settings', + 'settings.computerUse.setupBtn': 'Install Environment', + 'settings.computerUse.setupRunning': 'Installing...', + 'settings.computerUse.setupSuccess': 'Environment setup complete!', + 'settings.computerUse.setupFail': 'Setup failed', + 'settings.computerUse.allReady': 'All checks passed. Computer Use is ready.', + 'settings.computerUse.recheckBtn': 'Recheck Status', + 'settings.computerUse.requirementsLabel': 'Required packages', + 'settings.computerUse.appsTitle': 'Authorized Apps', + 'settings.computerUse.appsDescription': 'Pre-authorize apps for Computer Use. Authorized apps can be controlled by Claude without runtime prompts.', + 'settings.computerUse.appsLoading': 'Loading installed apps...', + 'settings.computerUse.appsEmpty': 'No installed apps found. Please setup the environment first.', + 'settings.computerUse.appsSearch': 'Search apps...', + 'settings.computerUse.appsSaved': 'Saved', + 'settings.computerUse.openAccessibility': 'Open Accessibility Settings', + 'settings.computerUse.openScreenRecording': 'Open Screen Recording Settings', + 'settings.computerUse.permRestartHint': 'After granting, restart the app for it to take effect.', + 'settings.computerUse.flagClipboard': 'Clipboard Access', + 'settings.computerUse.flagSystemKeys': 'System Key Combos', + // Settings > General 'settings.general.languageTitle': 'Language', 'settings.general.languageDescription': 'Choose the display language for the application.', diff --git a/desktop/src/i18n/locales/zh.ts b/desktop/src/i18n/locales/zh.ts index 548a64cf..866508ee 100644 --- a/desktop/src/i18n/locales/zh.ts +++ b/desktop/src/i18n/locales/zh.ts @@ -235,6 +235,46 @@ export const zh: Record = { 'settings.about.author': '作者', 'settings.about.socialMedia': '社交媒体', + // Settings > Computer Use + 'settings.tab.computerUse': 'Computer Use', + 'settings.computerUse.title': 'Computer Use', + 'settings.computerUse.description': '允许 Claude 截屏、点击、打字并控制你的电脑。需要 Python 3 和 macOS 辅助功能权限。', + 'settings.computerUse.notSupported': 'Computer Use 仅支持 macOS。', + 'settings.computerUse.python': 'Python 3', + 'settings.computerUse.pythonNotFound': '未安装,请先安装 Python 3。', + 'settings.computerUse.pythonFound': '已安装', + 'settings.computerUse.venv': '虚拟环境', + 'settings.computerUse.venvReady': '已就绪', + 'settings.computerUse.venvNotReady': '未创建', + 'settings.computerUse.deps': '依赖包', + 'settings.computerUse.depsReady': '已安装', + 'settings.computerUse.depsNotReady': '未安装', + 'settings.computerUse.accessibility': '辅助功能权限', + 'settings.computerUse.screenRecording': '屏幕录制权限', + 'settings.computerUse.permGranted': '已授权', + 'settings.computerUse.permDenied': '未授权 — 请前往「系统设置 > 隐私与安全性」授权', + 'settings.computerUse.permUnknown': '请先安装环境后检测', + 'settings.computerUse.permScreenRecordingUnknownSoft': '自动检测结果不稳定;如果系统设置里已授权,通常可以直接使用', + 'settings.computerUse.permScreenRecordingManual': '无法自动检测 — 请在系统设置中手动确认已授权', + 'settings.computerUse.setupBtn': '安装环境', + 'settings.computerUse.setupRunning': '安装中...', + 'settings.computerUse.setupSuccess': '环境安装完成!', + 'settings.computerUse.setupFail': '安装失败', + 'settings.computerUse.allReady': '所有检查通过,Computer Use 已就绪。', + 'settings.computerUse.recheckBtn': '重新检测', + 'settings.computerUse.requirementsLabel': '需要的 Python 包', + 'settings.computerUse.appsTitle': '已授权应用', + 'settings.computerUse.appsDescription': '预先授权 App,Claude 将可以直接控制这些 App,无需运行时弹窗确认。', + 'settings.computerUse.appsLoading': '加载已安装的 App...', + 'settings.computerUse.appsEmpty': '未找到已安装的 App,请先安装环境。', + 'settings.computerUse.appsSearch': '搜索 App...', + 'settings.computerUse.appsSaved': '已保存', + 'settings.computerUse.openAccessibility': '打开辅助功能设置', + 'settings.computerUse.openScreenRecording': '打开屏幕录制设置', + 'settings.computerUse.permRestartHint': '授权后需重启 App 才能生效。', + 'settings.computerUse.flagClipboard': '剪贴板访问', + 'settings.computerUse.flagSystemKeys': '系统快捷键', + // Settings > General 'settings.general.languageTitle': '语言', 'settings.general.languageDescription': '选择应用程序的显示语言。', diff --git a/desktop/src/pages/Settings.tsx b/desktop/src/pages/Settings.tsx index 32344e98..07468330 100644 --- a/desktop/src/pages/Settings.tsx +++ b/desktop/src/pages/Settings.tsx @@ -18,6 +18,7 @@ import { MarkdownRenderer } from '../components/markdown/MarkdownRenderer' import { useSkillStore } from '../stores/skillStore' import { SkillList } from '../components/skills/SkillList' import { SkillDetail } from '../components/skills/SkillDetail' +import { ComputerUseSettings } from './ComputerUseSettings' import { useUIStore, type SettingsTab } from '../stores/uiStore' export function Settings() { @@ -43,6 +44,7 @@ export function Settings() { setActiveTab('adapters')} /> setActiveTab('agents')} /> setActiveTab('skills')} /> + setActiveTab('computerUse')} />
setActiveTab('about')} /> @@ -57,6 +59,7 @@ export function Settings() { {activeTab === 'adapters' && } {activeTab === 'agents' && } {activeTab === 'skills' && } + {activeTab === 'computerUse' && } {activeTab === 'about' && }
diff --git a/desktop/src/stores/uiStore.ts b/desktop/src/stores/uiStore.ts index 2682f50e..57904645 100644 --- a/desktop/src/stores/uiStore.ts +++ b/desktop/src/stores/uiStore.ts @@ -7,7 +7,7 @@ export type Toast = { duration?: number } -export type SettingsTab = 'providers' | 'permissions' | 'general' | 'adapters' | 'agents' | 'skills' | 'about' +export type SettingsTab = 'providers' | 'permissions' | 'general' | 'adapters' | 'agents' | 'skills' | 'computerUse' | 'about' type ActiveView = 'code' | 'scheduled' | 'terminal' | 'history' | 'settings'