bingocode 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/desktop/README.md +30 -0
  2. package/desktop/bunfig.toml +1 -0
  3. package/desktop/index.html +17 -0
  4. package/desktop/package.json +55 -0
  5. package/desktop/pnpm-lock.yaml +3832 -0
  6. package/desktop/public/app-icon.jpg +0 -0
  7. package/desktop/public/fonts/inter-latin-ext.woff2 +0 -0
  8. package/desktop/public/fonts/inter-latin.woff2 +0 -0
  9. package/desktop/public/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
  10. package/desktop/public/fonts/jetbrains-mono-latin.woff2 +0 -0
  11. package/desktop/public/fonts/manrope-latin-ext.woff2 +0 -0
  12. package/desktop/public/fonts/manrope-latin.woff2 +0 -0
  13. package/desktop/public/fonts/material-symbols-outlined.woff2 +0 -0
  14. package/desktop/public/icons/bilibili.svg +1 -0
  15. package/desktop/public/icons/douyin.svg +1 -0
  16. package/desktop/public/icons/github.svg +3 -0
  17. package/desktop/public/icons/xiaohongshu.svg +1 -0
  18. package/desktop/scripts/build-macos-arm64.sh +270 -0
  19. package/desktop/scripts/build-sidecars.ts +183 -0
  20. package/desktop/scripts/build-windows-x64.ps1 +295 -0
  21. package/desktop/scripts/scan-missing-imports.ts +235 -0
  22. package/desktop/sidecars/claude-sidecar.ts +156 -0
  23. package/desktop/src/App.tsx +5 -0
  24. package/desktop/src/__tests__/agentsSettings.test.tsx +349 -0
  25. package/desktop/src/__tests__/pages.test.tsx +290 -0
  26. package/desktop/src/__tests__/skillsSettings.test.tsx +205 -0
  27. package/desktop/src/api/adapters.ts +12 -0
  28. package/desktop/src/api/agents.ts +36 -0
  29. package/desktop/src/api/cliTasks.ts +28 -0
  30. package/desktop/src/api/client.ts +63 -0
  31. package/desktop/src/api/computerUse.ts +76 -0
  32. package/desktop/src/api/filesystem.ts +30 -0
  33. package/desktop/src/api/hahaOAuth.ts +38 -0
  34. package/desktop/src/api/models.ts +28 -0
  35. package/desktop/src/api/providers.ts +63 -0
  36. package/desktop/src/api/search.ts +29 -0
  37. package/desktop/src/api/sessions.ts +56 -0
  38. package/desktop/src/api/settings.ts +20 -0
  39. package/desktop/src/api/skills.ts +19 -0
  40. package/desktop/src/api/tasks.ts +36 -0
  41. package/desktop/src/api/teams.ts +44 -0
  42. package/desktop/src/api/websocket.ts +164 -0
  43. package/desktop/src/components/chat/AskUserQuestion.tsx +268 -0
  44. package/desktop/src/components/chat/AssistantMessage.tsx +29 -0
  45. package/desktop/src/components/chat/AttachmentGallery.tsx +113 -0
  46. package/desktop/src/components/chat/ChatInput.tsx +622 -0
  47. package/desktop/src/components/chat/CodeViewer.tsx +161 -0
  48. package/desktop/src/components/chat/ComputerUsePermissionModal.test.tsx +174 -0
  49. package/desktop/src/components/chat/ComputerUsePermissionModal.tsx +311 -0
  50. package/desktop/src/components/chat/DiffViewer.tsx +157 -0
  51. package/desktop/src/components/chat/FileSearchMenu.tsx +198 -0
  52. package/desktop/src/components/chat/ImageGalleryModal.tsx +91 -0
  53. package/desktop/src/components/chat/InlineImageGallery.tsx +106 -0
  54. package/desktop/src/components/chat/InlineTaskSummary.tsx +60 -0
  55. package/desktop/src/components/chat/MermaidRenderer.test.tsx +98 -0
  56. package/desktop/src/components/chat/MermaidRenderer.tsx +361 -0
  57. package/desktop/src/components/chat/MessageActionBar.tsx +27 -0
  58. package/desktop/src/components/chat/MessageList.test.tsx +313 -0
  59. package/desktop/src/components/chat/MessageList.tsx +249 -0
  60. package/desktop/src/components/chat/PermissionDialog.tsx +262 -0
  61. package/desktop/src/components/chat/SessionTaskBar.test.tsx +99 -0
  62. package/desktop/src/components/chat/SessionTaskBar.tsx +159 -0
  63. package/desktop/src/components/chat/StreamingIndicator.tsx +41 -0
  64. package/desktop/src/components/chat/TerminalChrome.tsx +35 -0
  65. package/desktop/src/components/chat/ThinkingBlock.tsx +87 -0
  66. package/desktop/src/components/chat/ToolCallBlock.tsx +247 -0
  67. package/desktop/src/components/chat/ToolCallGroup.tsx +617 -0
  68. package/desktop/src/components/chat/ToolResultBlock.tsx +107 -0
  69. package/desktop/src/components/chat/UserMessage.tsx +38 -0
  70. package/desktop/src/components/chat/chatBlocks.test.tsx +136 -0
  71. package/desktop/src/components/chat/clipboard.ts +25 -0
  72. package/desktop/src/components/chat/composerUtils.test.ts +55 -0
  73. package/desktop/src/components/chat/composerUtils.ts +149 -0
  74. package/desktop/src/components/controls/ModelSelector.tsx +156 -0
  75. package/desktop/src/components/controls/PermissionModeSelector.tsx +229 -0
  76. package/desktop/src/components/layout/AppShell.tsx +107 -0
  77. package/desktop/src/components/layout/ContentRouter.tsx +27 -0
  78. package/desktop/src/components/layout/ProjectFilter.tsx +126 -0
  79. package/desktop/src/components/layout/Sidebar.test.tsx +158 -0
  80. package/desktop/src/components/layout/Sidebar.tsx +384 -0
  81. package/desktop/src/components/layout/StatusBar.tsx +31 -0
  82. package/desktop/src/components/layout/TabBar.test.tsx +136 -0
  83. package/desktop/src/components/layout/TabBar.tsx +318 -0
  84. package/desktop/src/components/layout/TitleBar.tsx +96 -0
  85. package/desktop/src/components/layout/WindowControls.test.tsx +69 -0
  86. package/desktop/src/components/layout/WindowControls.tsx +89 -0
  87. package/desktop/src/components/markdown/MarkdownRenderer.test.tsx +100 -0
  88. package/desktop/src/components/markdown/MarkdownRenderer.tsx +229 -0
  89. package/desktop/src/components/settings/ClaudeOfficialLogin.tsx +107 -0
  90. package/desktop/src/components/shared/Button.tsx +63 -0
  91. package/desktop/src/components/shared/CopyButton.tsx +58 -0
  92. package/desktop/src/components/shared/DirectoryPicker.tsx +316 -0
  93. package/desktop/src/components/shared/Dropdown.tsx +91 -0
  94. package/desktop/src/components/shared/Input.tsx +38 -0
  95. package/desktop/src/components/shared/Modal.tsx +65 -0
  96. package/desktop/src/components/shared/ProjectContextChip.tsx +30 -0
  97. package/desktop/src/components/shared/Spinner.tsx +30 -0
  98. package/desktop/src/components/shared/Textarea.tsx +38 -0
  99. package/desktop/src/components/shared/Toast.tsx +47 -0
  100. package/desktop/src/components/shared/UpdateChecker.tsx +90 -0
  101. package/desktop/src/components/skills/SkillDetail.test.tsx +89 -0
  102. package/desktop/src/components/skills/SkillDetail.tsx +403 -0
  103. package/desktop/src/components/skills/SkillList.tsx +254 -0
  104. package/desktop/src/components/tasks/DayOfWeekPicker.tsx +57 -0
  105. package/desktop/src/components/tasks/NewTaskModal.tsx +407 -0
  106. package/desktop/src/components/tasks/PromptEditor.tsx +74 -0
  107. package/desktop/src/components/tasks/TaskEmptyState.tsx +30 -0
  108. package/desktop/src/components/tasks/TaskList.tsx +46 -0
  109. package/desktop/src/components/tasks/TaskRow.tsx +253 -0
  110. package/desktop/src/components/tasks/TaskRunsPanel.tsx +195 -0
  111. package/desktop/src/components/teams/TeamStatusBar.tsx +147 -0
  112. package/desktop/src/config/providerPresets.ts +78 -0
  113. package/desktop/src/config/spinnerVerbs.ts +193 -0
  114. package/desktop/src/hooks/useKeyboardShortcuts.ts +60 -0
  115. package/desktop/src/i18n/index.ts +54 -0
  116. package/desktop/src/i18n/locales/en.ts +670 -0
  117. package/desktop/src/i18n/locales/zh.ts +670 -0
  118. package/desktop/src/lib/__tests__/cronDescribe.test.ts +93 -0
  119. package/desktop/src/lib/cronDescribe.ts +188 -0
  120. package/desktop/src/lib/desktopRuntime.ts +54 -0
  121. package/desktop/src/lib/parseRunOutput.ts +79 -0
  122. package/desktop/src/main.tsx +13 -0
  123. package/desktop/src/mocks/data.ts +202 -0
  124. package/desktop/src/pages/ActiveSession.test.tsx +181 -0
  125. package/desktop/src/pages/ActiveSession.tsx +219 -0
  126. package/desktop/src/pages/AdapterSettings.tsx +375 -0
  127. package/desktop/src/pages/AgentTeams.tsx +200 -0
  128. package/desktop/src/pages/ComputerUseSettings.tsx +420 -0
  129. package/desktop/src/pages/EmptySession.tsx +518 -0
  130. package/desktop/src/pages/NewTaskModal.tsx +346 -0
  131. package/desktop/src/pages/ScheduledTasks.tsx +66 -0
  132. package/desktop/src/pages/ScheduledTasksEmpty.tsx +152 -0
  133. package/desktop/src/pages/ScheduledTasksList.tsx +416 -0
  134. package/desktop/src/pages/SessionControls.tsx +460 -0
  135. package/desktop/src/pages/Settings.tsx +1448 -0
  136. package/desktop/src/pages/ToolInspection.tsx +235 -0
  137. package/desktop/src/stores/adapterStore.ts +106 -0
  138. package/desktop/src/stores/agentStore.ts +34 -0
  139. package/desktop/src/stores/chatStore.test.ts +505 -0
  140. package/desktop/src/stores/chatStore.ts +850 -0
  141. package/desktop/src/stores/cliTaskStore.ts +152 -0
  142. package/desktop/src/stores/hahaOAuthStore.test.ts +77 -0
  143. package/desktop/src/stores/hahaOAuthStore.ts +97 -0
  144. package/desktop/src/stores/providerStore.ts +101 -0
  145. package/desktop/src/stores/sessionStore.test.ts +63 -0
  146. package/desktop/src/stores/sessionStore.ts +102 -0
  147. package/desktop/src/stores/settingsStore.ts +120 -0
  148. package/desktop/src/stores/skillStore.ts +51 -0
  149. package/desktop/src/stores/tabStore.ts +169 -0
  150. package/desktop/src/stores/taskStore.ts +68 -0
  151. package/desktop/src/stores/teamStore.ts +344 -0
  152. package/desktop/src/stores/uiStore.ts +100 -0
  153. package/desktop/src/stores/updateStore.test.ts +71 -0
  154. package/desktop/src/stores/updateStore.ts +221 -0
  155. package/desktop/src/theme/globals.css +465 -0
  156. package/desktop/src/types/adapter.ts +33 -0
  157. package/desktop/src/types/chat.ts +152 -0
  158. package/desktop/src/types/cliTask.ts +24 -0
  159. package/desktop/src/types/provider.ts +62 -0
  160. package/desktop/src/types/session.ts +27 -0
  161. package/desktop/src/types/settings.ts +22 -0
  162. package/desktop/src/types/skill.ts +38 -0
  163. package/desktop/src/types/task.ts +56 -0
  164. package/desktop/src/types/team.ts +38 -0
  165. package/desktop/src-tauri/Cargo.lock +5549 -0
  166. package/desktop/src-tauri/Cargo.toml +20 -0
  167. package/desktop/src-tauri/app-icon.svg +13 -0
  168. package/desktop/src-tauri/build.rs +3 -0
  169. package/desktop/src-tauri/capabilities/default.json +106 -0
  170. package/desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml +5 -0
  171. package/desktop/src-tauri/icons/android/values/ic_launcher_background.xml +4 -0
  172. package/desktop/src-tauri/icons/icon.icns +0 -0
  173. package/desktop/src-tauri/icons/icon.ico +0 -0
  174. package/desktop/src-tauri/src/lib.rs +408 -0
  175. package/desktop/src-tauri/src/main.rs +6 -0
  176. package/desktop/src-tauri/tauri.conf.json +78 -0
  177. package/desktop/src-tauri/tauri.macos.conf.json +18 -0
  178. package/desktop/src-tauri/tauri.release-ci.json +5 -0
  179. package/desktop/src-tauri/tauri.windows.conf.json +16 -0
  180. package/desktop/src-tauri/windows-installer-hooks.nsh +17 -0
  181. package/desktop/tsconfig.json +25 -0
  182. package/desktop/vite.config.ts +26 -0
  183. package/desktop/vitest.config.ts +18 -0
  184. package/package.json +1 -1
  185. package/src/commands/desktop/desktop.tsx +9 -0
  186. package/src/commands/desktop/index.ts +26 -0
@@ -0,0 +1,20 @@
1
+ [package]
2
+ name = "claude-code-desktop"
3
+ version = "0.1.3"
4
+ edition = "2021"
5
+
6
+ [lib]
7
+ name = "claude_code_desktop_lib"
8
+ crate-type = ["staticlib", "cdylib", "rlib"]
9
+
10
+ [build-dependencies]
11
+ tauri-build = { version = "2", features = [] }
12
+
13
+ [dependencies]
14
+ tauri = { version = "2", features = [] }
15
+ tauri-plugin-shell = "2"
16
+ tauri-plugin-dialog = "2"
17
+ tauri-plugin-process = "2"
18
+ tauri-plugin-updater = "2"
19
+ serde = { version = "1", features = ["derive"] }
20
+ serde_json = "1"
@@ -0,0 +1,13 @@
1
+ <svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="512" height="512" rx="132" fill="#1F1714"/>
3
+ <rect x="42" y="42" width="428" height="428" rx="112" fill="url(#surface)"/>
4
+ <path d="M352 178C327 153 292 140 252 140C166 140 108 197 108 256C108 314 166 372 252 372C291 372 326 360 351 335L316 298C300 315 278 324 253 324C197 324 159 287 159 256C159 224 197 188 253 188C279 188 301 198 317 215L352 178Z" fill="#FFF7F0"/>
5
+ <path d="M381 166L401 146L438 183L418 203L381 166Z" fill="#D07A57"/>
6
+ <path d="M369 224L395 198L419 222L393 248L369 224Z" fill="#F0B08E"/>
7
+ <defs>
8
+ <linearGradient id="surface" x1="66" y1="78" x2="432" y2="436" gradientUnits="userSpaceOnUse">
9
+ <stop stop-color="#A85E42"/>
10
+ <stop offset="1" stop-color="#7A4330"/>
11
+ </linearGradient>
12
+ </defs>
13
+ </svg>
@@ -0,0 +1,3 @@
1
+ fn main() {
2
+ tauri_build::build()
3
+ }
@@ -0,0 +1,106 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/nicegui/nicegui/main/nicegui/static/tauri-capabilities-schema-v2.json",
3
+ "identifier": "default",
4
+ "description": "Default capabilities for Claude Code Desktop",
5
+ "windows": ["main"],
6
+ "permissions": [
7
+ "core:default",
8
+ "core:window:allow-close",
9
+ "core:window:allow-minimize",
10
+ "core:window:allow-start-dragging",
11
+ "core:window:allow-toggle-maximize",
12
+ "shell:allow-open",
13
+ {
14
+ "identifier": "shell:allow-execute",
15
+ "allow": [
16
+ {
17
+ "args": [
18
+ "server",
19
+ { "validator": "regex", "value": "^(--port|--host|--config)=.+$" }
20
+ ],
21
+ "name": "binaries/claude-sidecar",
22
+ "sidecar": true
23
+ },
24
+ {
25
+ "args": [
26
+ "adapters",
27
+ { "validator": "regex", "value": "^(feishu|telegram|lark|slack)(:.+)?$" }
28
+ ],
29
+ "name": "binaries/claude-sidecar",
30
+ "sidecar": true
31
+ },
32
+ {
33
+ "args": [
34
+ "cli",
35
+ { "validator": "regex", "value": "^.+$" }
36
+ ],
37
+ "name": "binaries/claude-sidecar",
38
+ "sidecar": true
39
+ }
40
+ ]
41
+ },
42
+ {
43
+ "identifier": "shell:allow-spawn",
44
+ "allow": [
45
+ {
46
+ "args": [
47
+ "server",
48
+ { "validator": "regex", "value": "^(--port|--host|--config)=.+$" }
49
+ ],
50
+ "name": "binaries/claude-sidecar",
51
+ "sidecar": true
52
+ },
53
+ {
54
+ "args": [
55
+ "adapters",
56
+ { "validator": "regex", "value": "^(feishu|telegram|lark|slack)(:.+)?$" }
57
+ ],
58
+ "name": "binaries/claude-sidecar",
59
+ "sidecar": true
60
+ },
61
+ {
62
+ "args": [
63
+ "cli",
64
+ { "validator": "regex", "value": "^.+$" }
65
+ ],
66
+ "name": "binaries/claude-sidecar",
67
+ "sidecar": true
68
+ }
69
+ ]
70
+ },
71
+ {
72
+ "identifier": "shell:allow-kill",
73
+ "allow": [
74
+ {
75
+ "args": [
76
+ "server",
77
+ { "validator": "regex", "value": "^(--port|--host|--config)=.+$" }
78
+ ],
79
+ "name": "binaries/claude-sidecar",
80
+ "sidecar": true
81
+ },
82
+ {
83
+ "args": [
84
+ "adapters",
85
+ { "validator": "regex", "value": "^(feishu|telegram|lark|slack)(:.+)?$" }
86
+ ],
87
+ "name": "binaries/claude-sidecar",
88
+ "sidecar": true
89
+ },
90
+ {
91
+ "args": [
92
+ "cli",
93
+ { "validator": "regex", "value": "^.+$" }
94
+ ],
95
+ "name": "binaries/claude-sidecar",
96
+ "sidecar": true
97
+ }
98
+ ]
99
+ },
100
+ "dialog:allow-open",
101
+ "dialog:allow-save",
102
+ "process:allow-exit",
103
+ "process:allow-restart",
104
+ "updater:default"
105
+ ]
106
+ }
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
3
+ <foreground android:drawable="@mipmap/ic_launcher_foreground"/>
4
+ <background android:drawable="@color/ic_launcher_background"/>
5
+ </adaptive-icon>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <resources>
3
+ <color name="ic_launcher_background">#fff</color>
4
+ </resources>
Binary file
@@ -0,0 +1,408 @@
1
+ use std::{
2
+ io::{Error as IoError, ErrorKind},
3
+ net::{SocketAddr, TcpListener, TcpStream},
4
+ path::PathBuf,
5
+ sync::Mutex,
6
+ time::{Duration, Instant},
7
+ };
8
+
9
+ #[cfg(target_os = "macos")]
10
+ use tauri::menu::{MenuBuilder, MenuItemBuilder, SubmenuBuilder};
11
+ #[cfg(target_os = "macos")]
12
+ use tauri::Emitter;
13
+ use tauri::{AppHandle, Manager, RunEvent, State};
14
+ use tauri_plugin_shell::{
15
+ process::{CommandChild, CommandEvent},
16
+ ShellExt,
17
+ };
18
+
19
+ #[derive(Default)]
20
+ struct ServerState(Mutex<ServerStatus>);
21
+
22
+ struct ServerRuntime {
23
+ url: String,
24
+ child: CommandChild,
25
+ }
26
+
27
+ #[derive(Default)]
28
+ struct ServerStatus {
29
+ runtime: Option<ServerRuntime>,
30
+ startup_error: Option<String>,
31
+ }
32
+
33
+ /// 与 ServerState 平级的 adapter 子进程状态。
34
+ ///
35
+ /// adapter sidecar(claude-sidecar adapters --feishu --telegram)的生命周期
36
+ /// 跟 server 不同:它没有 HTTP 端口可探活,没配凭据时会自己干净退出,
37
+ /// 而且需要支持运行时热重启 —— 用户在设置页保存飞书 / Telegram 凭据后,
38
+ /// 前端会通过 invoke('restart_adapters_sidecar') 来重启它,让新凭据生效。
39
+ #[derive(Default)]
40
+ struct AdapterState(Mutex<Option<CommandChild>>);
41
+
42
+ #[tauri::command]
43
+ fn get_server_url(state: State<'_, ServerState>) -> Result<String, String> {
44
+ let guard = state
45
+ .0
46
+ .lock()
47
+ .map_err(|_| "desktop server state is unavailable".to_string())?;
48
+
49
+ if let Some(runtime) = guard.runtime.as_ref() {
50
+ return Ok(runtime.url.clone());
51
+ }
52
+
53
+ Err(guard
54
+ .startup_error
55
+ .clone()
56
+ .unwrap_or_else(|| "desktop server did not start".to_string()))
57
+ }
58
+
59
+ /// 前端在设置页保存飞书 / Telegram 凭据后调用,触发 adapter sidecar 热重启。
60
+ ///
61
+ /// 流程:
62
+ /// 1. kill 当前 adapter 子进程(如果在跑)
63
+ /// 2. spawn 新的 adapter 子进程
64
+ /// 3. 新 sidecar 内部的 loadConfig() 会读到最新的 ~/.claude/adapters.json
65
+ /// 并重新建立 WebSocket 连接到飞书 / Telegram
66
+ ///
67
+ /// 凭据缺失时 sidecar 自己会 warn + skip + 退出,所以这里不需要前置检查。
68
+ #[tauri::command]
69
+ fn restart_adapters_sidecar(app: AppHandle) -> Result<(), String> {
70
+ stop_adapters_sidecar(&app);
71
+ spawn_and_track_adapters_sidecar(&app);
72
+ Ok(())
73
+ }
74
+
75
+ fn reserve_local_port() -> Result<u16, String> {
76
+ let listener =
77
+ TcpListener::bind("127.0.0.1:0").map_err(|err| format!("bind local port: {err}"))?;
78
+ let port = listener
79
+ .local_addr()
80
+ .map_err(|err| format!("read local port: {err}"))?
81
+ .port();
82
+ drop(listener);
83
+ Ok(port)
84
+ }
85
+
86
+ fn wait_for_server(url_host: &str, port: u16) -> Result<(), String> {
87
+ let addr: SocketAddr = format!("{url_host}:{port}")
88
+ .parse()
89
+ .map_err(|err| format!("parse server address: {err}"))?;
90
+ let deadline = Instant::now() + Duration::from_secs(10);
91
+
92
+ while Instant::now() < deadline {
93
+ if TcpStream::connect_timeout(&addr, Duration::from_millis(200)).is_ok() {
94
+ return Ok(());
95
+ }
96
+ std::thread::sleep(Duration::from_millis(150));
97
+ }
98
+
99
+ Err(format!(
100
+ "desktop server did not start listening on {url_host}:{port} within 10 seconds"
101
+ ))
102
+ }
103
+
104
+ fn resolve_app_root(_app: &AppHandle) -> Result<PathBuf, String> {
105
+ // 历史用途:此前 sidecar launcher 用 dynamic file:// import 加载磁盘上
106
+ // 的 src/server/index.ts 和 preload.ts,所以 Tauri 必须把整个 src/ +
107
+ // node_modules/ 当 Resource 一起 ship 到 .app/Contents/Resources/app/。
108
+ //
109
+ // 现在 launcher 改成静态 import + bun build --compile 整棵静态打进二进制,
110
+ // sidecar 不再读磁盘上的 src/ 或 node_modules/。CLAUDE_APP_ROOT 现在
111
+ // 只剩一个名义上的"app 安装根目录"作用,给 conversationService 在
112
+ // spawn CLI 子进程时通过 --app-root 透传。
113
+ //
114
+ // 我们直接用当前可执行文件所在目录作为 app_root:
115
+ // Dev: desktop/src-tauri/target/<profile>/ (rust 跑出来的 binary 那一层)
116
+ // Prod: <App>.app/Contents/MacOS/ (sidecar 二进制的同级目录)
117
+ let exe = std::env::current_exe()
118
+ .map_err(|err| format!("resolve current exe path: {err}"))?;
119
+ let dir = exe
120
+ .parent()
121
+ .ok_or_else(|| "current exe has no parent dir".to_string())?
122
+ .to_path_buf();
123
+ Ok(dir)
124
+ }
125
+
126
+ fn start_server_sidecar(app: &AppHandle) -> Result<ServerRuntime, String> {
127
+ let host = "127.0.0.1";
128
+ let port = reserve_local_port()?;
129
+ let url = format!("http://{host}:{port}");
130
+ let app_root = resolve_app_root(app)?;
131
+ let app_root_arg = app_root.to_string_lossy().to_string();
132
+
133
+ // 单一合并 sidecar:第一个参数选 server / cli / adapters 模式。
134
+ let sidecar = app
135
+ .shell()
136
+ .sidecar("claude-sidecar")
137
+ .map_err(|err| format!("resolve sidecar: {err}"))?
138
+ .args([
139
+ "server",
140
+ "--app-root",
141
+ &app_root_arg,
142
+ "--host",
143
+ host,
144
+ "--port",
145
+ &port.to_string(),
146
+ ]);
147
+
148
+ let (mut rx, child) = sidecar
149
+ .spawn()
150
+ .map_err(|err| format!("spawn server sidecar: {err}"))?;
151
+
152
+ tauri::async_runtime::spawn(async move {
153
+ while let Some(event) = rx.recv().await {
154
+ match event {
155
+ CommandEvent::Stdout(line) => {
156
+ let line = String::from_utf8_lossy(&line);
157
+ println!("[claude-server] {}", line.trim_end());
158
+ }
159
+ CommandEvent::Stderr(line) => {
160
+ let line = String::from_utf8_lossy(&line);
161
+ eprintln!("[claude-server] {}", line.trim_end());
162
+ }
163
+ _ => {}
164
+ }
165
+ }
166
+ });
167
+
168
+ wait_for_server(host, port)?;
169
+
170
+ Ok(ServerRuntime { url, child })
171
+ }
172
+
173
+ fn stop_server_sidecar(app: &AppHandle) {
174
+ let Some(state) = app.try_state::<ServerState>() else {
175
+ return;
176
+ };
177
+
178
+ let Ok(mut guard) = state.0.lock() else {
179
+ return;
180
+ };
181
+
182
+ if let Some(runtime) = guard.runtime.take() {
183
+ let _ = runtime.child.kill();
184
+ }
185
+ }
186
+
187
+ /// 启动 adapter sidecar。返回 Result 主要为了把"无法 spawn"和"spawn 后立刻
188
+ /// 退出(凭据缺失)"区分开 —— 后者不算错误,是正常 default 状态。
189
+ fn start_adapters_sidecar(app: &AppHandle) -> Result<CommandChild, String> {
190
+ let app_root = resolve_app_root(app)?;
191
+ let app_root_arg = app_root.to_string_lossy().to_string();
192
+
193
+ // adapter 内部的 WsBridge 默认连 ws://127.0.0.1:3456,但桌面端的 server
194
+ // 用的是 reserve_local_port() 拿到的动态端口。这里把实际端口通过
195
+ // ADAPTER_SERVER_URL env var 传过去 —— adapters/common/config.ts 的
196
+ // loadConfig() 会读它。
197
+ //
198
+ // 如果 server 还没起来 / 没拿到 URL,回退到 3456 作为最后兜底(adapter
199
+ // 自己有重连逻辑,等 server 上线就能连上)。
200
+ let server_http_url = app
201
+ .try_state::<ServerState>()
202
+ .and_then(|state| {
203
+ state
204
+ .0
205
+ .lock()
206
+ .ok()
207
+ .and_then(|guard| guard.runtime.as_ref().map(|r| r.url.clone()))
208
+ })
209
+ .unwrap_or_else(|| "http://127.0.0.1:3456".to_string());
210
+ // WsBridge 直接 `new WebSocket('${serverUrl}/ws/...')`,必须传 ws://;
211
+ // 不会自动从 http 转。
212
+ let server_ws_url = if let Some(rest) = server_http_url.strip_prefix("http://") {
213
+ format!("ws://{rest}")
214
+ } else if let Some(rest) = server_http_url.strip_prefix("https://") {
215
+ format!("wss://{rest}")
216
+ } else {
217
+ server_http_url.clone()
218
+ };
219
+
220
+ let sidecar = app
221
+ .shell()
222
+ .sidecar("claude-sidecar")
223
+ .map_err(|err| format!("resolve sidecar: {err}"))?
224
+ .env("ADAPTER_SERVER_URL", &server_ws_url)
225
+ .args([
226
+ "adapters",
227
+ "--app-root",
228
+ &app_root_arg,
229
+ "--feishu",
230
+ "--telegram",
231
+ ]);
232
+
233
+ let (mut rx, child) = sidecar
234
+ .spawn()
235
+ .map_err(|err| format!("spawn adapter sidecar: {err}"))?;
236
+
237
+ // 用一个 async task 把 sidecar 的 stdout/stderr 转发出来。它退出时
238
+ // 整个 task 也会自然结束。
239
+ tauri::async_runtime::spawn(async move {
240
+ while let Some(event) = rx.recv().await {
241
+ match event {
242
+ CommandEvent::Stdout(line) => {
243
+ let line = String::from_utf8_lossy(&line);
244
+ println!("[claude-adapters] {}", line.trim_end());
245
+ }
246
+ CommandEvent::Stderr(line) => {
247
+ let line = String::from_utf8_lossy(&line);
248
+ eprintln!("[claude-adapters] {}", line.trim_end());
249
+ }
250
+ CommandEvent::Terminated(payload) => {
251
+ // exit code != 0 是常态:用户没配凭据时 sidecar 内部会
252
+ // warn + skip + process.exit(1)。这里只 info 一行,
253
+ // 不要当错误冒泡。
254
+ println!(
255
+ "[claude-adapters] sidecar exited (code={:?}, signal={:?})",
256
+ payload.code, payload.signal
257
+ );
258
+ }
259
+ _ => {}
260
+ }
261
+ }
262
+ });
263
+
264
+ Ok(child)
265
+ }
266
+
267
+ /// spawn adapter sidecar 并把 child handle 存进 AdapterState。
268
+ /// 在启动 + 重启路径里复用,集中处理"无法 spawn"的日志。
269
+ fn spawn_and_track_adapters_sidecar(app: &AppHandle) {
270
+ match start_adapters_sidecar(app) {
271
+ Ok(child) => {
272
+ if let Some(state) = app.try_state::<AdapterState>() {
273
+ if let Ok(mut guard) = state.0.lock() {
274
+ *guard = Some(child);
275
+ }
276
+ }
277
+ }
278
+ Err(err) => {
279
+ eprintln!("[desktop] failed to start adapter sidecar: {err}");
280
+ }
281
+ }
282
+ }
283
+
284
+ fn stop_adapters_sidecar(app: &AppHandle) {
285
+ let Some(state) = app.try_state::<AdapterState>() else {
286
+ return;
287
+ };
288
+ let Ok(mut guard) = state.0.lock() else {
289
+ return;
290
+ };
291
+ if let Some(child) = guard.take() {
292
+ let _ = child.kill();
293
+ }
294
+ }
295
+
296
+ #[cfg_attr(mobile, tauri::mobile_entry_point)]
297
+ pub fn run() {
298
+ let builder = tauri::Builder::default()
299
+ .manage(ServerState::default())
300
+ .manage(AdapterState::default())
301
+ .plugin(tauri_plugin_shell::init())
302
+ .plugin(tauri_plugin_dialog::init())
303
+ .plugin(tauri_plugin_process::init())
304
+ .plugin(tauri_plugin_updater::Builder::new().build())
305
+ .invoke_handler(tauri::generate_handler![
306
+ get_server_url,
307
+ restart_adapters_sidecar
308
+ ]);
309
+
310
+ // macOS: native menu bar (traffic-light overlay style)
311
+ #[cfg(target_os = "macos")]
312
+ let builder = builder
313
+ .menu(|app| {
314
+ let about_item = MenuItemBuilder::with_id("nav_about", "关于 Claude Code Haha")
315
+ .build(app)?;
316
+ let settings_item = MenuItemBuilder::with_id("nav_settings", "设置...")
317
+ .accelerator("CmdOrCtrl+,")
318
+ .build(app)?;
319
+
320
+ let app_submenu = SubmenuBuilder::new(app, "Claude Code Haha")
321
+ .item(&about_item)
322
+ .separator()
323
+ .item(&settings_item)
324
+ .separator()
325
+ .services()
326
+ .separator()
327
+ .hide()
328
+ .hide_others()
329
+ .show_all()
330
+ .separator()
331
+ .quit()
332
+ .build()?;
333
+
334
+ let edit_submenu = SubmenuBuilder::new(app, "Edit")
335
+ .undo()
336
+ .redo()
337
+ .separator()
338
+ .cut()
339
+ .copy()
340
+ .paste()
341
+ .select_all()
342
+ .build()?;
343
+
344
+ let view_submenu = SubmenuBuilder::new(app, "View")
345
+ .fullscreen()
346
+ .build()?;
347
+
348
+ let window_submenu = SubmenuBuilder::new(app, "Window")
349
+ .minimize()
350
+ .maximize()
351
+ .close_window()
352
+ .build()?;
353
+
354
+ MenuBuilder::new(app)
355
+ .item(&app_submenu)
356
+ .item(&edit_submenu)
357
+ .item(&view_submenu)
358
+ .item(&window_submenu)
359
+ .build()
360
+ })
361
+ .on_menu_event(|app, event| match event.id().as_ref() {
362
+ "nav_about" => {
363
+ let _ = app.emit("native-menu-navigate", "about");
364
+ }
365
+ "nav_settings" => {
366
+ let _ = app.emit("native-menu-navigate", "settings");
367
+ }
368
+ _ => {}
369
+ });
370
+
371
+ let app = builder
372
+ .setup(|app| {
373
+ let state = app.state::<ServerState>();
374
+ let mut guard = state
375
+ .0
376
+ .lock()
377
+ .map_err(|_| IoError::new(ErrorKind::Other, "server state lock poisoned"))?;
378
+
379
+ match start_server_sidecar(&app.handle()) {
380
+ Ok(runtime) => {
381
+ guard.runtime = Some(runtime);
382
+ guard.startup_error = None;
383
+ }
384
+ Err(err) => {
385
+ eprintln!("[desktop] failed to start local server: {err}");
386
+ guard.runtime = None;
387
+ guard.startup_error = Some(err);
388
+ }
389
+ }
390
+ drop(guard);
391
+
392
+ // server 起来之后再起 adapter sidecar —— start_adapters_sidecar
393
+ // 内部会从 ServerState 读 server URL 注入 ADAPTER_SERVER_URL env,
394
+ // 让 adapter 连上动态端口。
395
+ spawn_and_track_adapters_sidecar(&app.handle());
396
+
397
+ Ok(())
398
+ })
399
+ .build(tauri::generate_context!())
400
+ .expect("error while building tauri application");
401
+
402
+ app.run(|app_handle, event| {
403
+ if matches!(event, RunEvent::Exit | RunEvent::ExitRequested { .. }) {
404
+ stop_server_sidecar(app_handle);
405
+ stop_adapters_sidecar(app_handle);
406
+ }
407
+ });
408
+ }
@@ -0,0 +1,6 @@
1
+ // Prevents additional console window on Windows in release
2
+ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
3
+
4
+ fn main() {
5
+ claude_code_desktop_lib::run()
6
+ }
@@ -0,0 +1,78 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/nicegui/nicegui/main/nicegui/static/tauri-schema-v2.json",
3
+ "productName": "Claude Code Haha",
4
+ "version": "0.1.3",
5
+ "identifier": "com.claude-code-haha.desktop",
6
+ "build": {
7
+ "frontendDist": "../dist",
8
+ "devUrl": "http://localhost:1420",
9
+ "beforeDevCommand": "bun run build:sidecars && bun run dev",
10
+ "beforeBuildCommand": "bun run build && bun run build:sidecars"
11
+ },
12
+ "app": {
13
+ "windows": [
14
+ {
15
+ "title": "Claude Code Haha",
16
+ "width": 1440,
17
+ "height": 960,
18
+ "minWidth": 960,
19
+ "minHeight": 640,
20
+ "decorations": true,
21
+ "transparent": false,
22
+ "acceptFirstMouse": true
23
+ }
24
+ ],
25
+ "security": {
26
+ "dangerousDisableAssetCspModification": ["style-src"],
27
+ "csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: asset: https://asset.localhost; font-src 'self' data:; connect-src 'self' ws://127.0.0.1:* http://127.0.0.1:* ws://localhost:* http://localhost:*; media-src 'self' blob:"
28
+ }
29
+ },
30
+ "plugins": {
31
+ "updater": {
32
+ "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDlCOUIwRDExQTc5RTFGMzYKUldRMkg1Nm5FUTJibTJ2cGlHY0pkL0dGemxXMUlzc01pVTVMM1U3WGpmWUtrUC8wK2ErSXhLKzEK",
33
+ "endpoints": [
34
+ "https://github.com/NanmiCoder/cc-haha/releases/latest/download/latest.json"
35
+ ],
36
+ "windows": {
37
+ "installMode": "passive"
38
+ }
39
+ }
40
+ },
41
+ "bundle": {
42
+ "active": true,
43
+ "targets": "all",
44
+ "createUpdaterArtifacts": true,
45
+ "windows": {
46
+ "nsis": {
47
+ "installerHooks": "windows-installer-hooks.nsh"
48
+ }
49
+ },
50
+ "externalBin": [
51
+ "binaries/claude-sidecar"
52
+ ],
53
+ "resources": [],
54
+ "icon": [
55
+ "icons/32x32.png",
56
+ "icons/128x128.png",
57
+ "icons/128x128@2x.png",
58
+ "icons/icon.icns",
59
+ "icons/icon.ico"
60
+ ],
61
+ "macOS": {
62
+ "dmg": {
63
+ "appPosition": {
64
+ "x": 180,
65
+ "y": 170
66
+ },
67
+ "applicationFolderPosition": {
68
+ "x": 480,
69
+ "y": 170
70
+ },
71
+ "windowSize": {
72
+ "width": 660,
73
+ "height": 400
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "app": {
3
+ "windows": [
4
+ {
5
+ "title": "Claude Code Haha",
6
+ "width": 1440,
7
+ "height": 960,
8
+ "minWidth": 960,
9
+ "minHeight": 640,
10
+ "decorations": true,
11
+ "titleBarStyle": "Overlay",
12
+ "hiddenTitle": true,
13
+ "transparent": false,
14
+ "acceptFirstMouse": true
15
+ }
16
+ ]
17
+ }
18
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "bundle": {
3
+ "createUpdaterArtifacts": true
4
+ }
5
+ }