diff --git a/README.md b/README.md
index 97b930cd..eceaf511 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# Claude Code Haha
diff --git a/desktop/bun.lock b/desktop/bun.lock
index 52d20da9..13c4092d 100644
--- a/desktop/bun.lock
+++ b/desktop/bun.lock
@@ -7,6 +7,8 @@
"dependencies": {
"@tailwindcss/typography": "^0.5.19",
"@types/dompurify": "^3.2.0",
+ "@xterm/addon-fit": "^0.11.0",
+ "@xterm/xterm": "^6.0.0",
"dompurify": "^3.3.3",
"lucide-react": "^0.469.0",
"marked": "^15.0.7",
@@ -470,6 +472,10 @@
"@vitest/utils": ["@vitest/utils@3.2.4", "https://registry.npmmirror.com/@vitest/utils/-/utils-3.2.4.tgz", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
+ "@xterm/addon-fit": ["@xterm/addon-fit@0.11.0", "https://registry.npmmirror.com/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", {}, "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g=="],
+
+ "@xterm/xterm": ["@xterm/xterm@6.0.0", "https://registry.npmmirror.com/@xterm/xterm/-/xterm-6.0.0.tgz", {}, "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg=="],
+
"acorn": ["acorn@8.16.0", "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"agent-base": ["agent-base@7.1.4", "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
diff --git a/desktop/package.json b/desktop/package.json
index b43d7818..7cd33e83 100644
--- a/desktop/package.json
+++ b/desktop/package.json
@@ -18,6 +18,8 @@
"dependencies": {
"@tailwindcss/typography": "^0.5.19",
"@types/dompurify": "^3.2.0",
+ "@xterm/addon-fit": "^0.11.0",
+ "@xterm/xterm": "^6.0.0",
"dompurify": "^3.3.3",
"lucide-react": "^0.469.0",
"marked": "^15.0.7",
diff --git a/desktop/public/app-icon.jpg b/desktop/public/app-icon.jpg
deleted file mode 100644
index 03098747..00000000
Binary files a/desktop/public/app-icon.jpg and /dev/null differ
diff --git a/desktop/public/app-icon.png b/desktop/public/app-icon.png
index 4f415f61..02f20c4c 100644
Binary files a/desktop/public/app-icon.png and b/desktop/public/app-icon.png differ
diff --git a/desktop/public/app-icon.svg b/desktop/public/app-icon.svg
new file mode 100644
index 00000000..582d3c54
--- /dev/null
+++ b/desktop/public/app-icon.svg
@@ -0,0 +1,17 @@
+
diff --git a/desktop/scripts/build-sidecars.ts b/desktop/scripts/build-sidecars.ts
index 5c01096a..a46293dc 100644
--- a/desktop/scripts/build-sidecars.ts
+++ b/desktop/scripts/build-sidecars.ts
@@ -162,21 +162,25 @@ async function compileExecutable({
// 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}`)
+ await adHocSignMacBinary(outputPath)
}
}
+
+async function adHocSignMacBinary(outputPath: string) {
+ 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/sidecars/claude-sidecar.ts b/desktop/sidecars/claude-sidecar.ts
index db070432..cd0937f0 100644
--- a/desktop/sidecars/claude-sidecar.ts
+++ b/desktop/sidecars/claude-sidecar.ts
@@ -17,18 +17,21 @@
* launcher-only 参数。
*/
+import { parseLauncherArgs, resolveSidecarInvocation } from './launcherRouting'
+
const rawArgs = process.argv.slice(2)
-if (rawArgs.length === 0) {
+const invocation = resolveSidecarInvocation(rawArgs)
+if (!invocation.mode) {
console.error('claude-sidecar: missing mode argument (expected "server", "cli" or "adapters")')
process.exit(2)
}
-const mode = rawArgs[0]!
-const restArgs = rawArgs.slice(1)
+const mode = invocation.mode
+const restArgs = invocation.restArgs
if (mode === 'adapters') {
await runAdapters(restArgs)
} else {
- const { appRoot, args } = parseLauncherArgs(restArgs)
+ const { appRoot, args } = parseLauncherArgs(restArgs, invocation.defaultAppRoot)
process.env.CLAUDE_APP_ROOT = appRoot
process.env.CALLER_DIR ||= process.cwd()
@@ -133,24 +136,3 @@ async function runAdapters(rawArgs: string[]): Promise
{
// / grammY long-polling)持有 event loop,自然不会退出。这里不需要额外
// setInterval 兜底。两个 adapter 自己注册的 SIGINT handler 都会触发。
}
-
-function parseLauncherArgs(rawArgs: string[]): { appRoot: string; args: string[] } {
- const nextArgs: string[] = []
- let appRoot: string | null = process.env.CLAUDE_APP_ROOT ?? null
-
- for (let index = 0; index < rawArgs.length; index++) {
- const arg = rawArgs[index]
- if (arg === '--app-root') {
- appRoot = rawArgs[index + 1] ?? null
- index += 1
- continue
- }
- nextArgs.push(arg!)
- }
-
- if (!appRoot) {
- throw new Error('Missing --app-root for claude-sidecar')
- }
-
- return { appRoot, args: nextArgs }
-}
diff --git a/desktop/sidecars/launcherRouting.test.ts b/desktop/sidecars/launcherRouting.test.ts
new file mode 100644
index 00000000..84d26637
--- /dev/null
+++ b/desktop/sidecars/launcherRouting.test.ts
@@ -0,0 +1,54 @@
+import { describe, expect, it } from 'vitest'
+
+import { parseLauncherArgs, resolveSidecarInvocation } from './launcherRouting'
+
+describe('resolveSidecarInvocation', () => {
+ it('keeps explicit sidecar modes unchanged', () => {
+ expect(
+ resolveSidecarInvocation(
+ ['server', '--host', '127.0.0.1'],
+ '/tmp/claude-sidecar',
+ ),
+ ).toEqual({
+ mode: 'server',
+ restArgs: ['--host', '127.0.0.1'],
+ defaultAppRoot: null,
+ })
+ })
+
+ it('defaults claude-haha invocations to cli mode', () => {
+ expect(
+ resolveSidecarInvocation(
+ ['plugin', 'install', 'demo'],
+ '/Users/demo/.local/bin/claude-haha',
+ ),
+ ).toEqual({
+ mode: 'cli',
+ restArgs: ['plugin', 'install', 'demo'],
+ defaultAppRoot: '/Users/demo/.local/bin',
+ })
+ })
+})
+
+describe('parseLauncherArgs', () => {
+ it('falls back to the provided default app root', () => {
+ expect(
+ parseLauncherArgs(['plugin', 'install', 'demo'], '/Users/demo/.local/bin'),
+ ).toEqual({
+ appRoot: '/Users/demo/.local/bin',
+ args: ['plugin', 'install', 'demo'],
+ })
+ })
+
+ it('lets explicit app root override the default', () => {
+ expect(
+ parseLauncherArgs(
+ ['--app-root', '/tmp/app', 'plugin', 'install', 'demo'],
+ '/Users/demo/.local/bin',
+ ),
+ ).toEqual({
+ appRoot: '/tmp/app',
+ args: ['plugin', 'install', 'demo'],
+ })
+ })
+})
diff --git a/desktop/sidecars/launcherRouting.ts b/desktop/sidecars/launcherRouting.ts
new file mode 100644
index 00000000..2af59c6a
--- /dev/null
+++ b/desktop/sidecars/launcherRouting.ts
@@ -0,0 +1,64 @@
+import path from 'node:path'
+
+export type SidecarMode = 'server' | 'cli' | 'adapters'
+
+const EXPLICIT_MODES = new Set(['server', 'cli', 'adapters'])
+const DESKTOP_CLI_NAMES = new Set(['claude-haha', 'claude-haha.exe'])
+
+export function resolveSidecarInvocation(
+ rawArgs: string[],
+ execPath: string = process.execPath,
+ envAppRoot: string | null = process.env.CLAUDE_APP_ROOT ?? null,
+): {
+ mode: SidecarMode | null
+ restArgs: string[]
+ defaultAppRoot: string | null
+} {
+ const explicitMode = rawArgs[0]
+ if (explicitMode && EXPLICIT_MODES.has(explicitMode as SidecarMode)) {
+ return {
+ mode: explicitMode as SidecarMode,
+ restArgs: rawArgs.slice(1),
+ defaultAppRoot: envAppRoot,
+ }
+ }
+
+ const execName = path.basename(execPath).toLowerCase()
+ if (DESKTOP_CLI_NAMES.has(execName)) {
+ return {
+ mode: 'cli',
+ restArgs: rawArgs,
+ defaultAppRoot: envAppRoot ?? path.dirname(execPath),
+ }
+ }
+
+ return {
+ mode: null,
+ restArgs: rawArgs,
+ defaultAppRoot: envAppRoot,
+ }
+}
+
+export function parseLauncherArgs(
+ rawArgs: string[],
+ defaultAppRoot: string | null = process.env.CLAUDE_APP_ROOT ?? null,
+): { appRoot: string; args: string[] } {
+ const nextArgs: string[] = []
+ let appRoot: string | null = defaultAppRoot
+
+ for (let index = 0; index < rawArgs.length; index++) {
+ const arg = rawArgs[index]
+ if (arg === '--app-root') {
+ appRoot = rawArgs[index + 1] ?? null
+ index += 1
+ continue
+ }
+ nextArgs.push(arg!)
+ }
+
+ if (!appRoot) {
+ throw new Error('Missing --app-root for claude-sidecar')
+ }
+
+ return { appRoot, args: nextArgs }
+}
diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock
index 98b02845..8933a60e 100644
--- a/desktop/src-tauri/Cargo.lock
+++ b/desktop/src-tauri/Cargo.lock
@@ -309,6 +309,12 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+[[package]]
+name = "cfg_aliases"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
+
[[package]]
name = "chrono"
version = "0.4.44"
@@ -325,6 +331,8 @@ dependencies = [
name = "claude-code-desktop"
version = "0.1.5"
dependencies = [
+ "anyhow",
+ "portable-pty",
"serde",
"serde_json",
"tauri",
@@ -675,6 +683,12 @@ dependencies = [
"tendril 0.5.0",
]
+[[package]]
+name = "downcast-rs"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
+
[[package]]
name = "dpi"
version = "0.1.2"
@@ -722,7 +736,7 @@ dependencies = [
"rustc_version",
"toml 0.9.12+spec-1.1.0",
"vswhom",
- "winreg",
+ "winreg 0.55.0",
]
[[package]]
@@ -792,6 +806,17 @@ dependencies = [
"rustc_version",
]
+[[package]]
+name = "filedescriptor"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d"
+dependencies = [
+ "libc",
+ "thiserror 1.0.69",
+ "winapi",
+]
+
[[package]]
name = "filetime"
version = "0.2.27"
@@ -1771,6 +1796,12 @@ dependencies = [
"selectors 0.24.0",
]
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
[[package]]
name = "leb128fmt"
version = "0.1.0"
@@ -2009,6 +2040,18 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+[[package]]
+name = "nix"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
+dependencies = [
+ "bitflags 2.11.1",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+]
+
[[package]]
name = "nodrop"
version = "0.1.14"
@@ -2533,6 +2576,27 @@ dependencies = [
"miniz_oxide",
]
+[[package]]
+name = "portable-pty"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e"
+dependencies = [
+ "anyhow",
+ "bitflags 1.3.2",
+ "downcast-rs",
+ "filedescriptor",
+ "lazy_static",
+ "libc",
+ "log",
+ "nix",
+ "serial2",
+ "shared_library",
+ "shell-words",
+ "winapi",
+ "winreg 0.10.1",
+]
+
[[package]]
name = "potential_utf"
version = "0.1.5"
@@ -3291,6 +3355,17 @@ dependencies = [
"syn 2.0.117",
]
+[[package]]
+name = "serial2"
+version = "0.2.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcdbc46aa3882ec3d48ec2b5abcb4f0d863a13d7599265f3faa6d851f23c12f3"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "winapi",
+]
+
[[package]]
name = "serialize-to-javascript"
version = "0.1.2"
@@ -3354,6 +3429,22 @@ dependencies = [
"windows-sys 0.60.2",
]
+[[package]]
+name = "shared_library"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11"
+dependencies = [
+ "lazy_static",
+ "libc",
+]
+
+[[package]]
+name = "shell-words"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
+
[[package]]
name = "shlex"
version = "1.3.0"
@@ -5242,6 +5333,15 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "winreg"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
+dependencies = [
+ "winapi",
+]
+
[[package]]
name = "winreg"
version = "0.55.0"
diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml
index 03f749e0..98f28b9d 100644
--- a/desktop/src-tauri/Cargo.toml
+++ b/desktop/src-tauri/Cargo.toml
@@ -18,3 +18,5 @@ tauri-plugin-process = "2"
tauri-plugin-updater = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
+anyhow = "1.0.102"
+portable-pty = "0.9.0"
diff --git a/desktop/src-tauri/app-icon-macos.svg b/desktop/src-tauri/app-icon-macos.svg
new file mode 100644
index 00000000..af11a9e2
--- /dev/null
+++ b/desktop/src-tauri/app-icon-macos.svg
@@ -0,0 +1,42 @@
+
diff --git a/desktop/src-tauri/app-icon.svg b/desktop/src-tauri/app-icon.svg
index 2ddf7d13..f4f73e67 100644
--- a/desktop/src-tauri/app-icon.svg
+++ b/desktop/src-tauri/app-icon.svg
@@ -1,67 +1,16 @@
-