From 4b2edf73188e5dc63cc6a1deb71c4c5eb0f87de2 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Sun, 7 Dec 2025 19:32:53 +0200 Subject: [PATCH] Initial public release --- .env | 1 + .github/workflows/opencode.yml | 29 + .github/workflows/release.yml | 269 + .gitignore | 27 + .nvmrc | 1 + AGENTS.md | 138 + CHANGELOG.md | 13 + CONTRIBUTING.md | 41 + LICENSE | 21 + README.md | 86 + components.json | 21 + docs/.gitkeep | 0 eslint.config.js | 23 + fix-deprecation.js | 81 + package.json | 118 + packages/desktop/.gitignore | 14 + packages/desktop/README.md | 36 + packages/desktop/index.html | 64 + packages/desktop/package.json | 38 + .../ibm-plex-mono-latin-600-normal.woff2 | Bin 0 -> 15620 bytes .../ibm-plex-mono-latin-700-normal.woff2 | Bin 0 -> 14908 bytes packages/desktop/scripts/desktop-dev.mjs | 88 + packages/desktop/scripts/opencode-cli.mjs | 237 + packages/desktop/src-tauri/Cargo.lock | 6606 ++++++++++ packages/desktop/src-tauri/Cargo.toml | 52 + packages/desktop/src-tauri/Info.plist | 10 + packages/desktop/src-tauri/build.rs | 3 + .../src-tauri/capabilities/default.json | 42 + .../src-tauri/icons/app-icon-checkpoint.svg | 25 + packages/desktop/src-tauri/icons/app-icon.png | Bin 0 -> 7163 bytes packages/desktop/src-tauri/icons/app-icon.svg | 35 + .../desktop/src-tauri/icons/icon-rgba.png | Bin 0 -> 19168 bytes packages/desktop/src-tauri/icons/icon.icns | Bin 0 -> 159393 bytes packages/desktop/src-tauri/icons/icon.png | Bin 0 -> 108389 bytes .../src-tauri/src/assistant_notifications.rs | 280 + .../desktop/src-tauri/src/commands/files.rs | 443 + .../desktop/src-tauri/src/commands/git.rs | 1673 +++ .../desktop/src-tauri/src/commands/logs.rs | 25 + .../desktop/src-tauri/src/commands/mod.rs | 7 + .../src-tauri/src/commands/notifications.rs | 37 + .../src-tauri/src/commands/permissions.rs | 209 + .../src-tauri/src/commands/settings.rs | 343 + .../src-tauri/src/commands/terminal.rs | 308 + packages/desktop/src-tauri/src/lib.rs | 0 packages/desktop/src-tauri/src/logging.rs | 20 + packages/desktop/src-tauri/src/main.rs | 1084 ++ .../desktop/src-tauri/src/opencode_config.rs | 794 ++ .../desktop/src-tauri/src/opencode_manager.rs | 577 + .../desktop/src-tauri/src/session_activity.rs | 326 + .../desktop/src-tauri/src/window_state.rs | 167 + packages/desktop/src-tauri/tauri.conf.json | 60 + packages/desktop/src/api/diagnostics.ts | 32 + packages/desktop/src/api/files.ts | 114 + packages/desktop/src/api/git.ts | 230 + packages/desktop/src/api/index.ts | 57 + packages/desktop/src/api/notifications.ts | 58 + packages/desktop/src/api/permissions.ts | 49 + packages/desktop/src/api/settings.ts | 58 + packages/desktop/src/api/terminal.ts | 123 + packages/desktop/src/api/tools.ts | 22 + packages/desktop/src/api/updater.ts | 105 + packages/desktop/src/lib/bridge.ts | 157 + .../desktop/src/lib/tauriCallbackManager.ts | 308 + packages/desktop/src/main.tsx | 254 + packages/desktop/tsconfig.json | 25 + packages/desktop/vite.config.ts | 75 + packages/ui/package.json | 77 + packages/ui/src/App.css | 42 + packages/ui/src/App.tsx | 188 + .../ui/src/assets/provider-logos/gocode.svg | 2 + .../assets/provider-logos/kimi-for-coding.svg | 1 + .../src/components/auth/SessionAuthGate.tsx | 258 + .../chat/AgentMentionAutocomplete.tsx | 152 + .../ui/src/components/chat/ChatContainer.tsx | 255 + .../ui/src/components/chat/ChatEmptyState.tsx | 13 + .../src/components/chat/ChatErrorBoundary.tsx | 88 + packages/ui/src/components/chat/ChatInput.tsx | 882 ++ .../ui/src/components/chat/ChatMessage.tsx | 750 ++ .../components/chat/CommandAutocomplete.tsx | 261 + .../ui/src/components/chat/FileAttachment.tsx | 293 + .../chat/FileMentionAutocomplete.tsx | 241 + .../src/components/chat/MarkdownRenderer.tsx | 346 + .../ui/src/components/chat/MessageList.tsx | 95 + .../ui/src/components/chat/ModelControls.tsx | 1907 +++ .../ui/src/components/chat/PermissionCard.tsx | 525 + .../src/components/chat/PermissionRequest.tsx | 128 + .../src/components/chat/ServerFilePicker.tsx | 559 + .../src/components/chat/StreamingTextDiff.tsx | 0 .../components/chat/hooks/useTurnGrouping.ts | 390 + .../chat/message/DiffViewToggle.tsx | 39 + .../chat/message/FadeInOnReveal.tsx | 56 + .../components/chat/message/MessageBody.tsx | 1038 ++ .../components/chat/message/MessageHeader.tsx | 78 + .../chat/message/ToolOutputDialog.tsx | 563 + .../components/chat/message/messageRole.ts | 34 + .../src/components/chat/message/partUtils.ts | 54 + .../chat/message/parts/AssistantTextPart.tsx | 81 + .../chat/message/parts/JustificationBlock.tsx | 51 + .../chat/message/parts/MigratingPart.tsx | 31 + .../chat/message/parts/ProgressiveGroup.tsx | 259 + .../chat/message/parts/ReasoningPart.tsx | 181 + .../chat/message/parts/ToolPart.tsx | 765 ++ .../chat/message/parts/UserTextPart.tsx | 357 + .../chat/message/parts/WorkingPlaceholder.tsx | 404 + .../components/chat/message/toolRenderers.tsx | 642 + .../ui/src/components/chat/message/types.ts | 24 + packages/ui/src/components/layout/Header.tsx | 478 + .../ui/src/components/layout/MainLayout.tsx | 161 + .../src/components/layout/SettingsDialog.tsx | 244 + packages/ui/src/components/layout/Sidebar.tsx | 218 + .../layout/SidebarContextSummary.tsx | 61 + .../onboarding/OnboardingScreen.tsx | 139 + .../components/providers/ThemeProvider.tsx | 34 + .../sections/SectionPlaceholder.tsx | 42 + .../components/sections/agents/AgentsPage.tsx | 649 + .../sections/agents/AgentsSidebar.tsx | 318 + .../sections/agents/ModelSelector.tsx | 300 + .../sections/commands/AgentSelector.tsx | 154 + .../sections/commands/CommandsPage.tsx | 312 + .../sections/commands/CommandsSidebar.tsx | 260 + .../git-identities/GitIdentitiesPage.tsx | 374 + .../git-identities/GitIdentitiesSidebar.tsx | 240 + .../sections/providers/ProvidersPage.tsx | 11 + .../sections/providers/ProvidersSidebar.tsx | 11 + .../sections/settings/AppearanceSettings.tsx | 126 + .../sections/settings/SettingsPage.tsx | 14 + .../session/DirectoryExplorerDialog.tsx | 287 + .../src/components/session/DirectoryTree.tsx | 1028 ++ .../src/components/session/SessionDialogs.tsx | 858 ++ .../src/components/session/SessionSidebar.tsx | 1074 ++ .../components/terminal/TerminalViewport.tsx | 366 + .../ui/src/components/ui/CommandPalette.tsx | 204 + .../src/components/ui/ConfigUpdateOverlay.tsx | 38 + .../src/components/ui/ContextUsageDisplay.tsx | 73 + .../ui/src/components/ui/ErrorBoundary.tsx | 82 + .../src/components/ui/FireworksAnimation.tsx | 101 + packages/ui/src/components/ui/HelpDialog.tsx | 153 + .../ui/src/components/ui/MemoryDebugPanel.tsx | 194 + .../src/components/ui/MobileOverlayPanel.tsx | 119 + .../ui/src/components/ui/OpenChamberLogo.tsx | 89 + .../ui/src/components/ui/OpenCodeIcon.tsx | 35 + .../ui/src/components/ui/OpenCodeLogo.tsx | 41 + .../ui/src/components/ui/OverlayScrollbar.tsx | 210 + .../ui/src/components/ui/ProviderLogo.tsx | 37 + .../ui/src/components/ui/ScrollShadow.tsx | 142 + .../src/components/ui/ScrollableOverlay.tsx | 60 + .../ui/src/components/ui/UpdateDialog.tsx | 159 + packages/ui/src/components/ui/alert.tsx | 66 + .../ui/src/components/ui/animated-tabs.tsx | 118 + .../ui/src/components/ui/button-large.tsx | 21 + .../ui/src/components/ui/button-small.tsx | 21 + packages/ui/src/components/ui/button.tsx | 61 + packages/ui/src/components/ui/card.tsx | 92 + packages/ui/src/components/ui/collapsible.tsx | 31 + packages/ui/src/components/ui/command.tsx | 232 + packages/ui/src/components/ui/dialog.tsx | 157 + .../ui/src/components/ui/dropdown-menu.tsx | 255 + packages/ui/src/components/ui/input.tsx | 21 + packages/ui/src/components/ui/scroll-area.tsx | 56 + packages/ui/src/components/ui/select.tsx | 188 + packages/ui/src/components/ui/separator.tsx | 28 + packages/ui/src/components/ui/skeleton.tsx | 13 + packages/ui/src/components/ui/sonner.tsx | 39 + packages/ui/src/components/ui/text.tsx | 211 + packages/ui/src/components/ui/textarea.tsx | 27 + packages/ui/src/components/ui/toggle.tsx | 47 + packages/ui/src/components/ui/tooltip.tsx | 64 + .../ui/src/components/ui/typewriter-text.tsx | 61 + packages/ui/src/components/views/ChatView.tsx | 14 + packages/ui/src/components/views/DiffView.tsx | 491 + packages/ui/src/components/views/GitView.tsx | 1534 +++ .../src/components/views/MonacoDiffViewer.tsx | 151 + .../ui/src/components/views/TerminalView.tsx | 784 ++ packages/ui/src/components/views/index.ts | 4 + packages/ui/src/constants/sidebar.ts | 62 + packages/ui/src/contexts/FireworksContext.tsx | 35 + .../ui/src/contexts/RuntimeAPIProvider.tsx | 7 + .../ui/src/contexts/ThemeSystemContext.tsx | 424 + packages/ui/src/contexts/runtimeAPIContext.ts | 4 + .../ui/src/contexts/runtimeAPIRegistry.ts | 9 + .../ui/src/contexts/theme-system-context.ts | 19 + packages/ui/src/contexts/useThemeSystem.ts | 15 + packages/ui/src/hooks/useAssistantStatus.ts | 339 + packages/ui/src/hooks/useAssistantTyping.ts | 229 + packages/ui/src/hooks/useAvailableTools.ts | 49 + packages/ui/src/hooks/useChatScrollManager.ts | 518 + packages/ui/src/hooks/useDebouncedValue.ts | 17 + packages/ui/src/hooks/useDesktopServerInfo.ts | 38 + packages/ui/src/hooks/useEdgeSwipe.ts | 132 + packages/ui/src/hooks/useEventStream.ts | 1169 ++ packages/ui/src/hooks/useFileSystemAccess.ts | 41 + packages/ui/src/hooks/useFireworks.ts | 53 + packages/ui/src/hooks/useFontPreferences.ts | 13 + packages/ui/src/hooks/useGitPolling.tsx | 10 + packages/ui/src/hooks/useGitPollingHook.ts | 31 + packages/ui/src/hooks/useKeyboardShortcuts.ts | 208 + packages/ui/src/hooks/useMessageSync.ts | 252 + packages/ui/src/hooks/useProviderLogo.ts | 61 + packages/ui/src/hooks/useRuntimeAPIs.ts | 18 + packages/ui/src/hooks/useScrollEngine.ts | 211 + packages/ui/src/hooks/useSessionActivity.ts | 53 + .../ui/src/hooks/useSessionStatusBootstrap.ts | 43 + packages/ui/src/hooks/useUpdateCheck.ts | 201 + packages/ui/src/index.css | 1293 ++ packages/ui/src/lib/agentColors.ts | 37 + packages/ui/src/lib/api/types.ts | 396 + packages/ui/src/lib/appearanceAutoSave.ts | 59 + packages/ui/src/lib/appearancePersistence.ts | 100 + packages/ui/src/lib/codeTheme.ts | 352 + packages/ui/src/lib/configSync.ts | 62 + packages/ui/src/lib/configUpdate.ts | 69 + packages/ui/src/lib/debug.ts | 488 + packages/ui/src/lib/desktop.ts | 276 + packages/ui/src/lib/device.ts | 211 + packages/ui/src/lib/directoryPersistence.ts | 27 + packages/ui/src/lib/fontOptions.ts | 38 + packages/ui/src/lib/git/worktreeService.ts | 167 + packages/ui/src/lib/gitApi.ts | 237 + packages/ui/src/lib/gitApiHttp.ts | 515 + packages/ui/src/lib/messageCompletion.ts | 110 + .../ui/src/lib/messageCursorPersistence.ts | 157 + packages/ui/src/lib/messageFreshness.ts | 90 + packages/ui/src/lib/messages/agentMentions.ts | 75 + packages/ui/src/lib/messages/synthetic.ts | 16 + packages/ui/src/lib/opencode/client.ts | 1066 ++ .../ui/src/lib/permissions/editModeColors.ts | 30 + .../lib/permissions/editPermissionDefaults.ts | 101 + packages/ui/src/lib/persistence.ts | 270 + packages/ui/src/lib/sessionEvents.ts | 56 + packages/ui/src/lib/terminalApi.ts | 233 + packages/ui/src/lib/terminalTheme.ts | 91 + packages/ui/src/lib/theme/cssGenerator.ts | 758 ++ .../ui/src/lib/theme/monacoThemeGenerator.ts | 445 + .../ui/src/lib/theme/syntaxThemeGenerator.ts | 208 + .../ui/src/lib/theme/themes/flexoki-dark.ts | 197 + .../ui/src/lib/theme/themes/flexoki-light.ts | 197 + packages/ui/src/lib/theme/themes/index.ts | 21 + packages/ui/src/lib/toolHelpers.ts | 330 + packages/ui/src/lib/typography.ts | 322 + packages/ui/src/lib/typographyWatcher.ts | 23 + packages/ui/src/lib/utils.ts | 102 + packages/ui/src/main.tsx | 117 + packages/ui/src/stores/contextStore.ts | 515 + packages/ui/src/stores/fileStore.ts | 235 + packages/ui/src/stores/messageStore.ts | 2260 ++++ packages/ui/src/stores/permissionStore.ts | 152 + packages/ui/src/stores/sessionStore.ts | 1058 ++ packages/ui/src/stores/types/sessionTypes.ts | 171 + packages/ui/src/stores/useAgentsStore.ts | 417 + packages/ui/src/stores/useCommandsStore.ts | 397 + packages/ui/src/stores/useConfigStore.ts | 564 + packages/ui/src/stores/useDirectoryStore.ts | 477 + packages/ui/src/stores/useFileSearchStore.ts | 129 + .../ui/src/stores/useGitIdentitiesStore.ts | 180 + packages/ui/src/stores/useGitStore.ts | 391 + packages/ui/src/stores/useSessionStore.ts | 453 + packages/ui/src/stores/useTerminalStore.ts | 175 + packages/ui/src/stores/useUIStore.ts | 258 + packages/ui/src/stores/utils/contextUtils.ts | 22 + packages/ui/src/stores/utils/messageUtils.ts | 68 + .../ui/src/stores/utils/permissionUtils.ts | 54 + packages/ui/src/stores/utils/safeStorage.ts | 121 + packages/ui/src/stores/utils/streamDebug.ts | 8 + .../ui/src/stores/utils/streamingUtils.ts | 56 + packages/ui/src/stores/utils/tokenUtils.ts | 51 + packages/ui/src/styles/fireworks.css | 77 + packages/ui/src/styles/fonts.ts | 9 + packages/ui/src/types/desktop.d.ts | 32 + .../ui/src/types/electron-context-menu.d.ts | 35 + packages/ui/src/types/index.ts | 66 + packages/ui/src/types/permission.ts | 20 + packages/ui/src/types/streamdown.d.ts | 22 + packages/ui/src/types/theme.ts | 290 + packages/ui/src/types/tool.ts | 53 + packages/ui/src/types/worktree.ts | 21 + packages/ui/src/vite-env.d.ts | 15 + packages/ui/tsconfig.json | 22 + packages/web/.gitignore | 2 + packages/web/README.md | 34 + packages/web/bin/cli.js | 561 + packages/web/index.html | 194 + packages/web/package.json | 92 + .../web/public/apple-touch-icon-120x120.png | Bin 0 -> 858 bytes .../web/public/apple-touch-icon-152x152.png | Bin 0 -> 990 bytes .../web/public/apple-touch-icon-167x167.png | Bin 0 -> 999 bytes .../web/public/apple-touch-icon-180x180.png | Bin 0 -> 811 bytes packages/web/public/apple-touch-icon.png | Bin 0 -> 811 bytes packages/web/public/apple-touch-icon.svg | 18 + packages/web/public/favicon-16.png | Bin 0 -> 600 bytes packages/web/public/favicon-32.png | Bin 0 -> 582 bytes packages/web/public/logo-dark.svg | 4 + packages/web/public/logo-light.svg | 4 + packages/web/public/site.webmanifest | 36 + packages/web/public/vite.svg | 1 + packages/web/server/index.d.ts | 28 + packages/web/server/index.js | 3038 +++++ .../web/server/lib/git-identity-storage.js | 108 + packages/web/server/lib/git-service.js | 899 ++ packages/web/server/lib/opencode-config.js | 471 + .../web/server/lib/opencode-config.js.d.ts | 12 + packages/web/server/lib/ui-auth.js | 266 + packages/web/src/api/files.ts | 70 + packages/web/src/api/git.ts | 41 + packages/web/src/api/index.ts | 19 + packages/web/src/api/notifications.ts | 32 + packages/web/src/api/permissions.ts | 15 + packages/web/src/api/settings.ts | 58 + packages/web/src/api/terminal.ts | 56 + packages/web/src/api/tools.ts | 22 + packages/web/src/main.tsx | 13 + packages/web/tsconfig.json | 25 + packages/web/vite.config.ts | 69 + pnpm-lock.yaml | 10241 ++++++++++++++++ pnpm-workspace.yaml | 10 + postcss.config.js | 5 + scripts/bump-version.mjs | 64 + tsconfig.json | 21 + vite-theme-plugin.ts | 8 + vite.config.ts | 54 + 319 files changed, 81600 insertions(+) create mode 100644 .env create mode 100644 .github/workflows/opencode.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 .nvmrc create mode 100644 AGENTS.md create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 components.json create mode 100644 docs/.gitkeep create mode 100644 eslint.config.js create mode 100644 fix-deprecation.js create mode 100644 package.json create mode 100644 packages/desktop/.gitignore create mode 100644 packages/desktop/README.md create mode 100644 packages/desktop/index.html create mode 100644 packages/desktop/package.json create mode 100644 packages/desktop/public/ibm-plex-mono-latin-600-normal.woff2 create mode 100644 packages/desktop/public/ibm-plex-mono-latin-700-normal.woff2 create mode 100644 packages/desktop/scripts/desktop-dev.mjs create mode 100644 packages/desktop/scripts/opencode-cli.mjs create mode 100644 packages/desktop/src-tauri/Cargo.lock create mode 100644 packages/desktop/src-tauri/Cargo.toml create mode 100644 packages/desktop/src-tauri/Info.plist create mode 100644 packages/desktop/src-tauri/build.rs create mode 100644 packages/desktop/src-tauri/capabilities/default.json create mode 100644 packages/desktop/src-tauri/icons/app-icon-checkpoint.svg create mode 100644 packages/desktop/src-tauri/icons/app-icon.png create mode 100644 packages/desktop/src-tauri/icons/app-icon.svg create mode 100644 packages/desktop/src-tauri/icons/icon-rgba.png create mode 100644 packages/desktop/src-tauri/icons/icon.icns create mode 100644 packages/desktop/src-tauri/icons/icon.png create mode 100644 packages/desktop/src-tauri/src/assistant_notifications.rs create mode 100644 packages/desktop/src-tauri/src/commands/files.rs create mode 100644 packages/desktop/src-tauri/src/commands/git.rs create mode 100644 packages/desktop/src-tauri/src/commands/logs.rs create mode 100644 packages/desktop/src-tauri/src/commands/mod.rs create mode 100644 packages/desktop/src-tauri/src/commands/notifications.rs create mode 100644 packages/desktop/src-tauri/src/commands/permissions.rs create mode 100644 packages/desktop/src-tauri/src/commands/settings.rs create mode 100644 packages/desktop/src-tauri/src/commands/terminal.rs create mode 100644 packages/desktop/src-tauri/src/lib.rs create mode 100644 packages/desktop/src-tauri/src/logging.rs create mode 100644 packages/desktop/src-tauri/src/main.rs create mode 100644 packages/desktop/src-tauri/src/opencode_config.rs create mode 100644 packages/desktop/src-tauri/src/opencode_manager.rs create mode 100644 packages/desktop/src-tauri/src/session_activity.rs create mode 100644 packages/desktop/src-tauri/src/window_state.rs create mode 100644 packages/desktop/src-tauri/tauri.conf.json create mode 100644 packages/desktop/src/api/diagnostics.ts create mode 100644 packages/desktop/src/api/files.ts create mode 100644 packages/desktop/src/api/git.ts create mode 100644 packages/desktop/src/api/index.ts create mode 100644 packages/desktop/src/api/notifications.ts create mode 100644 packages/desktop/src/api/permissions.ts create mode 100644 packages/desktop/src/api/settings.ts create mode 100644 packages/desktop/src/api/terminal.ts create mode 100644 packages/desktop/src/api/tools.ts create mode 100644 packages/desktop/src/api/updater.ts create mode 100644 packages/desktop/src/lib/bridge.ts create mode 100644 packages/desktop/src/lib/tauriCallbackManager.ts create mode 100644 packages/desktop/src/main.tsx create mode 100644 packages/desktop/tsconfig.json create mode 100644 packages/desktop/vite.config.ts create mode 100644 packages/ui/package.json create mode 100644 packages/ui/src/App.css create mode 100644 packages/ui/src/App.tsx create mode 100644 packages/ui/src/assets/provider-logos/gocode.svg create mode 100644 packages/ui/src/assets/provider-logos/kimi-for-coding.svg create mode 100644 packages/ui/src/components/auth/SessionAuthGate.tsx create mode 100644 packages/ui/src/components/chat/AgentMentionAutocomplete.tsx create mode 100644 packages/ui/src/components/chat/ChatContainer.tsx create mode 100644 packages/ui/src/components/chat/ChatEmptyState.tsx create mode 100644 packages/ui/src/components/chat/ChatErrorBoundary.tsx create mode 100644 packages/ui/src/components/chat/ChatInput.tsx create mode 100644 packages/ui/src/components/chat/ChatMessage.tsx create mode 100644 packages/ui/src/components/chat/CommandAutocomplete.tsx create mode 100644 packages/ui/src/components/chat/FileAttachment.tsx create mode 100644 packages/ui/src/components/chat/FileMentionAutocomplete.tsx create mode 100644 packages/ui/src/components/chat/MarkdownRenderer.tsx create mode 100644 packages/ui/src/components/chat/MessageList.tsx create mode 100644 packages/ui/src/components/chat/ModelControls.tsx create mode 100644 packages/ui/src/components/chat/PermissionCard.tsx create mode 100644 packages/ui/src/components/chat/PermissionRequest.tsx create mode 100644 packages/ui/src/components/chat/ServerFilePicker.tsx create mode 100644 packages/ui/src/components/chat/StreamingTextDiff.tsx create mode 100644 packages/ui/src/components/chat/hooks/useTurnGrouping.ts create mode 100644 packages/ui/src/components/chat/message/DiffViewToggle.tsx create mode 100644 packages/ui/src/components/chat/message/FadeInOnReveal.tsx create mode 100644 packages/ui/src/components/chat/message/MessageBody.tsx create mode 100644 packages/ui/src/components/chat/message/MessageHeader.tsx create mode 100644 packages/ui/src/components/chat/message/ToolOutputDialog.tsx create mode 100644 packages/ui/src/components/chat/message/messageRole.ts create mode 100644 packages/ui/src/components/chat/message/partUtils.ts create mode 100644 packages/ui/src/components/chat/message/parts/AssistantTextPart.tsx create mode 100644 packages/ui/src/components/chat/message/parts/JustificationBlock.tsx create mode 100644 packages/ui/src/components/chat/message/parts/MigratingPart.tsx create mode 100644 packages/ui/src/components/chat/message/parts/ProgressiveGroup.tsx create mode 100644 packages/ui/src/components/chat/message/parts/ReasoningPart.tsx create mode 100644 packages/ui/src/components/chat/message/parts/ToolPart.tsx create mode 100644 packages/ui/src/components/chat/message/parts/UserTextPart.tsx create mode 100644 packages/ui/src/components/chat/message/parts/WorkingPlaceholder.tsx create mode 100644 packages/ui/src/components/chat/message/toolRenderers.tsx create mode 100644 packages/ui/src/components/chat/message/types.ts create mode 100644 packages/ui/src/components/layout/Header.tsx create mode 100644 packages/ui/src/components/layout/MainLayout.tsx create mode 100644 packages/ui/src/components/layout/SettingsDialog.tsx create mode 100644 packages/ui/src/components/layout/Sidebar.tsx create mode 100644 packages/ui/src/components/layout/SidebarContextSummary.tsx create mode 100644 packages/ui/src/components/onboarding/OnboardingScreen.tsx create mode 100644 packages/ui/src/components/providers/ThemeProvider.tsx create mode 100644 packages/ui/src/components/sections/SectionPlaceholder.tsx create mode 100644 packages/ui/src/components/sections/agents/AgentsPage.tsx create mode 100644 packages/ui/src/components/sections/agents/AgentsSidebar.tsx create mode 100644 packages/ui/src/components/sections/agents/ModelSelector.tsx create mode 100644 packages/ui/src/components/sections/commands/AgentSelector.tsx create mode 100644 packages/ui/src/components/sections/commands/CommandsPage.tsx create mode 100644 packages/ui/src/components/sections/commands/CommandsSidebar.tsx create mode 100644 packages/ui/src/components/sections/git-identities/GitIdentitiesPage.tsx create mode 100644 packages/ui/src/components/sections/git-identities/GitIdentitiesSidebar.tsx create mode 100644 packages/ui/src/components/sections/providers/ProvidersPage.tsx create mode 100644 packages/ui/src/components/sections/providers/ProvidersSidebar.tsx create mode 100644 packages/ui/src/components/sections/settings/AppearanceSettings.tsx create mode 100644 packages/ui/src/components/sections/settings/SettingsPage.tsx create mode 100644 packages/ui/src/components/session/DirectoryExplorerDialog.tsx create mode 100644 packages/ui/src/components/session/DirectoryTree.tsx create mode 100644 packages/ui/src/components/session/SessionDialogs.tsx create mode 100644 packages/ui/src/components/session/SessionSidebar.tsx create mode 100644 packages/ui/src/components/terminal/TerminalViewport.tsx create mode 100644 packages/ui/src/components/ui/CommandPalette.tsx create mode 100644 packages/ui/src/components/ui/ConfigUpdateOverlay.tsx create mode 100644 packages/ui/src/components/ui/ContextUsageDisplay.tsx create mode 100644 packages/ui/src/components/ui/ErrorBoundary.tsx create mode 100644 packages/ui/src/components/ui/FireworksAnimation.tsx create mode 100644 packages/ui/src/components/ui/HelpDialog.tsx create mode 100644 packages/ui/src/components/ui/MemoryDebugPanel.tsx create mode 100644 packages/ui/src/components/ui/MobileOverlayPanel.tsx create mode 100644 packages/ui/src/components/ui/OpenChamberLogo.tsx create mode 100644 packages/ui/src/components/ui/OpenCodeIcon.tsx create mode 100644 packages/ui/src/components/ui/OpenCodeLogo.tsx create mode 100644 packages/ui/src/components/ui/OverlayScrollbar.tsx create mode 100644 packages/ui/src/components/ui/ProviderLogo.tsx create mode 100644 packages/ui/src/components/ui/ScrollShadow.tsx create mode 100644 packages/ui/src/components/ui/ScrollableOverlay.tsx create mode 100644 packages/ui/src/components/ui/UpdateDialog.tsx create mode 100644 packages/ui/src/components/ui/alert.tsx create mode 100644 packages/ui/src/components/ui/animated-tabs.tsx create mode 100644 packages/ui/src/components/ui/button-large.tsx create mode 100644 packages/ui/src/components/ui/button-small.tsx create mode 100644 packages/ui/src/components/ui/button.tsx create mode 100644 packages/ui/src/components/ui/card.tsx create mode 100644 packages/ui/src/components/ui/collapsible.tsx create mode 100644 packages/ui/src/components/ui/command.tsx create mode 100644 packages/ui/src/components/ui/dialog.tsx create mode 100644 packages/ui/src/components/ui/dropdown-menu.tsx create mode 100644 packages/ui/src/components/ui/input.tsx create mode 100644 packages/ui/src/components/ui/scroll-area.tsx create mode 100644 packages/ui/src/components/ui/select.tsx create mode 100644 packages/ui/src/components/ui/separator.tsx create mode 100644 packages/ui/src/components/ui/skeleton.tsx create mode 100644 packages/ui/src/components/ui/sonner.tsx create mode 100644 packages/ui/src/components/ui/text.tsx create mode 100644 packages/ui/src/components/ui/textarea.tsx create mode 100644 packages/ui/src/components/ui/toggle.tsx create mode 100644 packages/ui/src/components/ui/tooltip.tsx create mode 100644 packages/ui/src/components/ui/typewriter-text.tsx create mode 100644 packages/ui/src/components/views/ChatView.tsx create mode 100644 packages/ui/src/components/views/DiffView.tsx create mode 100644 packages/ui/src/components/views/GitView.tsx create mode 100644 packages/ui/src/components/views/MonacoDiffViewer.tsx create mode 100644 packages/ui/src/components/views/TerminalView.tsx create mode 100644 packages/ui/src/components/views/index.ts create mode 100644 packages/ui/src/constants/sidebar.ts create mode 100644 packages/ui/src/contexts/FireworksContext.tsx create mode 100644 packages/ui/src/contexts/RuntimeAPIProvider.tsx create mode 100644 packages/ui/src/contexts/ThemeSystemContext.tsx create mode 100644 packages/ui/src/contexts/runtimeAPIContext.ts create mode 100644 packages/ui/src/contexts/runtimeAPIRegistry.ts create mode 100644 packages/ui/src/contexts/theme-system-context.ts create mode 100644 packages/ui/src/contexts/useThemeSystem.ts create mode 100644 packages/ui/src/hooks/useAssistantStatus.ts create mode 100644 packages/ui/src/hooks/useAssistantTyping.ts create mode 100644 packages/ui/src/hooks/useAvailableTools.ts create mode 100644 packages/ui/src/hooks/useChatScrollManager.ts create mode 100644 packages/ui/src/hooks/useDebouncedValue.ts create mode 100644 packages/ui/src/hooks/useDesktopServerInfo.ts create mode 100644 packages/ui/src/hooks/useEdgeSwipe.ts create mode 100644 packages/ui/src/hooks/useEventStream.ts create mode 100644 packages/ui/src/hooks/useFileSystemAccess.ts create mode 100644 packages/ui/src/hooks/useFireworks.ts create mode 100644 packages/ui/src/hooks/useFontPreferences.ts create mode 100644 packages/ui/src/hooks/useGitPolling.tsx create mode 100644 packages/ui/src/hooks/useGitPollingHook.ts create mode 100644 packages/ui/src/hooks/useKeyboardShortcuts.ts create mode 100644 packages/ui/src/hooks/useMessageSync.ts create mode 100644 packages/ui/src/hooks/useProviderLogo.ts create mode 100644 packages/ui/src/hooks/useRuntimeAPIs.ts create mode 100644 packages/ui/src/hooks/useScrollEngine.ts create mode 100644 packages/ui/src/hooks/useSessionActivity.ts create mode 100644 packages/ui/src/hooks/useSessionStatusBootstrap.ts create mode 100644 packages/ui/src/hooks/useUpdateCheck.ts create mode 100644 packages/ui/src/index.css create mode 100644 packages/ui/src/lib/agentColors.ts create mode 100644 packages/ui/src/lib/api/types.ts create mode 100644 packages/ui/src/lib/appearanceAutoSave.ts create mode 100644 packages/ui/src/lib/appearancePersistence.ts create mode 100644 packages/ui/src/lib/codeTheme.ts create mode 100644 packages/ui/src/lib/configSync.ts create mode 100644 packages/ui/src/lib/configUpdate.ts create mode 100644 packages/ui/src/lib/debug.ts create mode 100644 packages/ui/src/lib/desktop.ts create mode 100644 packages/ui/src/lib/device.ts create mode 100644 packages/ui/src/lib/directoryPersistence.ts create mode 100644 packages/ui/src/lib/fontOptions.ts create mode 100644 packages/ui/src/lib/git/worktreeService.ts create mode 100644 packages/ui/src/lib/gitApi.ts create mode 100644 packages/ui/src/lib/gitApiHttp.ts create mode 100644 packages/ui/src/lib/messageCompletion.ts create mode 100644 packages/ui/src/lib/messageCursorPersistence.ts create mode 100644 packages/ui/src/lib/messageFreshness.ts create mode 100644 packages/ui/src/lib/messages/agentMentions.ts create mode 100644 packages/ui/src/lib/messages/synthetic.ts create mode 100644 packages/ui/src/lib/opencode/client.ts create mode 100644 packages/ui/src/lib/permissions/editModeColors.ts create mode 100644 packages/ui/src/lib/permissions/editPermissionDefaults.ts create mode 100644 packages/ui/src/lib/persistence.ts create mode 100644 packages/ui/src/lib/sessionEvents.ts create mode 100644 packages/ui/src/lib/terminalApi.ts create mode 100644 packages/ui/src/lib/terminalTheme.ts create mode 100644 packages/ui/src/lib/theme/cssGenerator.ts create mode 100644 packages/ui/src/lib/theme/monacoThemeGenerator.ts create mode 100644 packages/ui/src/lib/theme/syntaxThemeGenerator.ts create mode 100644 packages/ui/src/lib/theme/themes/flexoki-dark.ts create mode 100644 packages/ui/src/lib/theme/themes/flexoki-light.ts create mode 100644 packages/ui/src/lib/theme/themes/index.ts create mode 100644 packages/ui/src/lib/toolHelpers.ts create mode 100644 packages/ui/src/lib/typography.ts create mode 100644 packages/ui/src/lib/typographyWatcher.ts create mode 100644 packages/ui/src/lib/utils.ts create mode 100644 packages/ui/src/main.tsx create mode 100644 packages/ui/src/stores/contextStore.ts create mode 100644 packages/ui/src/stores/fileStore.ts create mode 100644 packages/ui/src/stores/messageStore.ts create mode 100644 packages/ui/src/stores/permissionStore.ts create mode 100644 packages/ui/src/stores/sessionStore.ts create mode 100644 packages/ui/src/stores/types/sessionTypes.ts create mode 100644 packages/ui/src/stores/useAgentsStore.ts create mode 100644 packages/ui/src/stores/useCommandsStore.ts create mode 100644 packages/ui/src/stores/useConfigStore.ts create mode 100644 packages/ui/src/stores/useDirectoryStore.ts create mode 100644 packages/ui/src/stores/useFileSearchStore.ts create mode 100644 packages/ui/src/stores/useGitIdentitiesStore.ts create mode 100644 packages/ui/src/stores/useGitStore.ts create mode 100644 packages/ui/src/stores/useSessionStore.ts create mode 100644 packages/ui/src/stores/useTerminalStore.ts create mode 100644 packages/ui/src/stores/useUIStore.ts create mode 100644 packages/ui/src/stores/utils/contextUtils.ts create mode 100644 packages/ui/src/stores/utils/messageUtils.ts create mode 100644 packages/ui/src/stores/utils/permissionUtils.ts create mode 100644 packages/ui/src/stores/utils/safeStorage.ts create mode 100644 packages/ui/src/stores/utils/streamDebug.ts create mode 100644 packages/ui/src/stores/utils/streamingUtils.ts create mode 100644 packages/ui/src/stores/utils/tokenUtils.ts create mode 100644 packages/ui/src/styles/fireworks.css create mode 100644 packages/ui/src/styles/fonts.ts create mode 100644 packages/ui/src/types/desktop.d.ts create mode 100644 packages/ui/src/types/electron-context-menu.d.ts create mode 100644 packages/ui/src/types/index.ts create mode 100644 packages/ui/src/types/permission.ts create mode 100644 packages/ui/src/types/streamdown.d.ts create mode 100644 packages/ui/src/types/theme.ts create mode 100644 packages/ui/src/types/tool.ts create mode 100644 packages/ui/src/types/worktree.ts create mode 100644 packages/ui/src/vite-env.d.ts create mode 100644 packages/ui/tsconfig.json create mode 100644 packages/web/.gitignore create mode 100644 packages/web/README.md create mode 100755 packages/web/bin/cli.js create mode 100644 packages/web/index.html create mode 100644 packages/web/package.json create mode 100644 packages/web/public/apple-touch-icon-120x120.png create mode 100644 packages/web/public/apple-touch-icon-152x152.png create mode 100644 packages/web/public/apple-touch-icon-167x167.png create mode 100644 packages/web/public/apple-touch-icon-180x180.png create mode 100644 packages/web/public/apple-touch-icon.png create mode 100644 packages/web/public/apple-touch-icon.svg create mode 100644 packages/web/public/favicon-16.png create mode 100644 packages/web/public/favicon-32.png create mode 100644 packages/web/public/logo-dark.svg create mode 100644 packages/web/public/logo-light.svg create mode 100644 packages/web/public/site.webmanifest create mode 100644 packages/web/public/vite.svg create mode 100644 packages/web/server/index.d.ts create mode 100644 packages/web/server/index.js create mode 100644 packages/web/server/lib/git-identity-storage.js create mode 100644 packages/web/server/lib/git-service.js create mode 100644 packages/web/server/lib/opencode-config.js create mode 100644 packages/web/server/lib/opencode-config.js.d.ts create mode 100644 packages/web/server/lib/ui-auth.js create mode 100644 packages/web/src/api/files.ts create mode 100644 packages/web/src/api/git.ts create mode 100644 packages/web/src/api/index.ts create mode 100644 packages/web/src/api/notifications.ts create mode 100644 packages/web/src/api/permissions.ts create mode 100644 packages/web/src/api/settings.ts create mode 100644 packages/web/src/api/terminal.ts create mode 100644 packages/web/src/api/tools.ts create mode 100644 packages/web/src/main.tsx create mode 100644 packages/web/tsconfig.json create mode 100644 packages/web/vite.config.ts create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 postcss.config.js create mode 100644 scripts/bump-version.mjs create mode 100644 tsconfig.json create mode 100644 vite-theme-plugin.ts create mode 100644 vite.config.ts diff --git a/.env b/.env new file mode 100644 index 00000000..c0d66521 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +NODE_ENV=development diff --git a/.github/workflows/opencode.yml b/.github/workflows/opencode.yml new file mode 100644 index 00000000..1f4ca012 --- /dev/null +++ b/.github/workflows/opencode.yml @@ -0,0 +1,29 @@ +name: opencode + +on: + issue_comment: + types: [created] + +jobs: + opencode: + if: | + contains(github.event.comment.body, ' /oc') || + startsWith(github.event.comment.body, '/oc') || + contains(github.event.comment.body, ' /opencode') || + startsWith(github.event.comment.body, '/opencode') + runs-on: ubuntu-latest + permissions: + id-token: write + contents: write + pull-requests: write + issues: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run opencode + uses: sst/opencode/github@latest + env: + ZHIPU_API_KEY: ${{ secrets.ZHIPU_API_KEY }} + with: + model: zai-coding-plan/glm-4.6 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..c6158055 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,269 @@ +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + version: + description: 'Version to release (e.g., 0.1.0)' + required: true + type: string + dry_run: + description: 'Dry run (skip publishing)' + required: false + default: false + type: boolean + +env: + CARGO_INCREMENTAL: 0 + RUST_BACKTRACE: short + +permissions: + contents: write + +jobs: + create-release: + runs-on: ubuntu-latest + outputs: + release_id: ${{ steps.create_release.outputs.id }} + release_upload_url: ${{ steps.create_release.outputs.upload_url }} + version: ${{ steps.get_version.outputs.version }} + steps: + - uses: actions/checkout@v4 + + - name: Get version + id: get_version + run: | + if [[ -n "${{ github.event.inputs.version }}" ]]; then + echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT + elif [[ "${{ github.ref }}" == refs/tags/* ]]; then + echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + else + echo "version=0.0.0-dev" >> $GITHUB_OUTPUT + fi + + - name: Extract changelog for release + env: + VERSION: ${{ steps.get_version.outputs.version }} + run: | + node - <<'NODE' + const fs = require('fs'); + const version = process.env.VERSION; + const changelogPath = 'CHANGELOG.md'; + if (!fs.existsSync(changelogPath)) { + throw new Error('CHANGELOG.md not found; add it before releasing.'); + } + const changelog = fs.readFileSync(changelogPath, 'utf8'); + const sections = changelog.split(/^## /m); + const section = sections.find(s => s.startsWith('[' + version + ']')); + if (!section) { + throw new Error('Changelog section [' + version + '] not found. Add a section like "## [' + version + '] - YYYY-MM-DD".'); + } + const content = ('## ' + section).trim(); + fs.mkdirSync('artifacts', { recursive: true }); + fs.writeFileSync('artifacts/release-notes.md', content + '\n'); + NODE + + - name: Create GitHub Release + id: create_release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ steps.get_version.outputs.version }} + draft: true + generate_release_notes: false + body_path: artifacts/release-notes.md + name: OpenChamber v${{ steps.get_version.outputs.version }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + build-desktop-macos: + needs: create-release + runs-on: macos-14 + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + + - name: Rust cache + uses: swatinem/rust-cache@v2 + with: + workspaces: packages/desktop/src-tauri + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Install Apple Certificate + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + run: | + # Create temporary keychain + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + KEYCHAIN_PASSWORD=$(openssl rand -base64 32) + + security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + + # Import certificate + echo "$APPLE_CERTIFICATE" | base64 --decode > $RUNNER_TEMP/certificate.p12 + security import $RUNNER_TEMP/certificate.p12 \ + -P "$APPLE_CERTIFICATE_PASSWORD" \ + -A -t cert -f pkcs12 \ + -k "$KEYCHAIN_PATH" + + security list-keychain -d user -s "$KEYCHAIN_PATH" + security set-key-partition-list -S apple-tool:,apple:,codesign: \ + -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + + - name: Set up notarization credentials + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + run: | + # Validate secrets are set + if [ -z "$APPLE_ID" ] || [ -z "$APPLE_TEAM_ID" ] || [ -z "$APPLE_PASSWORD" ]; then + echo "Error: Missing Apple notarization credentials" + exit 1 + fi + + xcrun notarytool store-credentials "openchamber-notarize" \ + --apple-id "$APPLE_ID" \ + --team-id "$APPLE_TEAM_ID" \ + --password "$APPLE_PASSWORD" + + - name: Build UI package + run: pnpm -C packages/ui run build + + - name: Build Desktop app + run: pnpm desktop:build + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + + - name: Prepare release artifacts + run: | + mkdir -p artifacts + VERSION="${{ needs.create-release.outputs.version }}" + + # Copy DMG + cp packages/desktop/src-tauri/target/release/bundle/dmg/*.dmg artifacts/ 2>/dev/null || true + + # Copy tar.gz and signature for updater + cp packages/desktop/src-tauri/target/release/bundle/macos/*.tar.gz artifacts/ 2>/dev/null || true + cp packages/desktop/src-tauri/target/release/bundle/macos/*.tar.gz.sig artifacts/ 2>/dev/null || true + + - name: Generate update manifest + run: | + VERSION="${{ needs.create-release.outputs.version }}" + + # Find the signature file + SIG_FILE=$(find artifacts -name "*.tar.gz.sig" | head -1) + if [ -f "$SIG_FILE" ]; then + SIGNATURE=$(cat "$SIG_FILE") + else + SIGNATURE="" + fi + + # Find the tar.gz file name + TAR_FILE=$(find artifacts -name "*.tar.gz" ! -name "*.sig" | head -1) + TAR_NAME=$(basename "$TAR_FILE" 2>/dev/null || echo "OpenChamber.app.tar.gz") + + cat > artifacts/latest.json << EOF + { + "version": "${VERSION}", + "notes": "See release notes at https://github.com/${{ github.repository }}/releases/tag/v${VERSION}", + "pub_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)", + "platforms": { + "darwin-aarch64": { + "signature": "${SIGNATURE}", + "url": "https://github.com/${{ github.repository }}/releases/download/v${VERSION}/${TAR_NAME}" + } + } + } + EOF + + echo "Generated latest.json:" + cat artifacts/latest.json + + - name: Upload release assets + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ needs.create-release.outputs.version }} + files: | + artifacts/*.dmg + artifacts/*.tar.gz + artifacts/*.tar.gz.sig + artifacts/latest.json + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + publish-npm: + needs: create-release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + registry-url: 'https://registry.npmjs.org' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build packages + run: pnpm run build + + - name: Create npm tarball + working-directory: packages/web + run: npm pack + + - name: Upload npm tarball to release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ needs.create-release.outputs.version }} + files: packages/web/*.tgz + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish to npm + if: ${{ github.event.inputs.dry_run != 'true' }} + working-directory: packages/web + run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + + finalize-release: + needs: [create-release, build-desktop-macos, publish-npm] + runs-on: ubuntu-latest + steps: + - name: Publish release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ needs.create-release.outputs.version }} + draft: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4ac51bd5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Logs +logs +*.log +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +node_modules +dist +dist-ssr +release +*.local +*.tgz +/npm +/tsc +/openchamber@* +local-dev* +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..a24992d1 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +lts diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..7d5d0c2f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,138 @@ +# OpenChamber - AI Agent & Contributor Reference + +Technical reference for AI coding agents and human contributors working on this project. + +## Core Purpose + +Web and desktop interface for OpenCode AI coding agent. Provides cross-device continuity, remote accessibility, and a unified chat interface using the OpenCode API backend. + +## Tech Stack + +- **React 19.1.1**: Modern React with concurrent features +- **TypeScript 5.8.3**: Full type safety +- **Vite 7.1.2**: Build tool with HMR and proxy +- **Tailwind CSS v4.0.0**: Latest `@import` syntax +- **Zustand 5.0.8**: State management with persistence +- **@opencode-ai/sdk**: Official OpenCode SDK with typed endpoints and SSE +- **@remixicon/react**: Icon system +- **@radix-ui primitives**: Accessible component foundations + +## Architecture Overview (Monorepo) + +Workspaces: +- `packages/ui` - Shared UI components and stores +- `packages/web` - Web runtime, Express server, CLI +- `packages/desktop` - Tauri desktop app with native APIs + +### Core Components (UI) +In `packages/ui/src/components/`: ChatContainer, MessageList, ChatMessage, StreamingAnimatedText, ChatInput, FileAttachment, ModelControls, PermissionCard, SessionList, SessionSwitcherDialog, DirectoryTree, DirectoryExplorerDialog, MainLayout, Header, Sidebar, SettingsDialog, AgentsPage, CommandsPage, GitIdentitiesPage, ProvidersPage, SessionsPage, SettingsPage, CommandPalette, HelpDialog, ConfigUpdateOverlay, ContextUsageDisplay, ErrorBoundary, MemoryDebugPanel, MobileOverlayPanel, ThemeDemo, ThemeSwitcher. + +In `packages/ui/src/components/views/`: ChatView, GitView, DiffView, TerminalView. + +In `packages/ui/src/components/terminal/`: TerminalViewport + +### State Management (UI) +In `packages/ui/src/stores/`: ConfigStore, SessionStore, DirectoryStore, UIStore, FileStore, MessageStore, ContextStore, PermissionStore, AgentsStore, CommandsStore, GitIdentitiesStore, TerminalStore + +### OpenCode SDK Integration (UI) +In `packages/ui/src/lib/opencode/`: client.ts wrapper around `@opencode-ai/sdk` with directory-aware API calls, SDK methods (session.*, message.*, agent.*, provider.*, config.*, project.*, path.*), AsyncGenerator SSE streaming (2 retry attempts, 500ms->8s backoff), automatic directory parameter injection. + +In `packages/ui/src/hooks/`: useEventStream.ts for real-time SSE connection management. + +### Web Runtime (server/CLI) +Express server and CLI in `packages/web`: API adapters in `packages/web/src/api`, server in `packages/web/server/index.js` (git/terminal/config), UI bundle imported from `@openchamber/ui`. + +### Desktop Runtime (Tauri) +Native desktop app in `packages/desktop`: Tauri backend in `src-tauri/` (Rust), frontend API adapters in `src/api/` (settings, permissions, diagnostics, files, git, terminal, notifications, tools), bridge layer in `src/lib/` for Tauri IPC communication. + +## Development Commands + +### Code Validation +Always validate changes before committing: + +```bash +pnpm -r type-check # TypeScript validation +pnpm -r lint # ESLint checks +pnpm -r build # Production build +``` + +### Building +```bash +pnpm run build # Build all packages +pnpm run desktop:build # Build desktop app +``` + +## Key Patterns + +### Section-Based Navigation +Modular section architecture with dedicated pages and sidebars. Sections: Agents, Commands, Git Identities, Providers, Sessions, Settings. Independent state management and routing. + +### File Attachments +Drag-and-drop upload with 10MB limit (`FileAttachment.tsx`), Data URL encoding, type validation with fallbacks, integrated via `useFileStore.addAttachedFile()`. + +### Theme System +In `packages/ui/src/lib/theme/`: TypeScript-based themes (Flexoki Light and Dark), CSS variable generation, component-specific theming, Tailwind CSS v4 integration. + +### Typography System +In `packages/ui/src/lib/`: Semantic typography with 6 CSS variables, theme-independent scales. **CRITICAL**: Always use semantic typography classes, never hardcoded font sizes. + +### Streaming Architecture +SDK-managed SSE with AsyncGenerator, temp->real ID swap, pending-user guards, empty-response detection via `window.__opencodeDebug`. + +## Development Guidelines + +### Lint & Type Safety + +- Never land code that introduces new ESLint or TypeScript errors +- Run `pnpm run lint` and `pnpm run type-check` before finalizing changes +- Adding `eslint-disable` requires justification in a comment explaining why typing is impossible +- Do **not** use `any` or `unknown` casts as escape hatches; build narrow adapter interfaces instead +- Refactors or new features must keep existing lint/type baselines green + +### Theme Integration + +- Check theme definitions before adding colors or font sizes to new components +- Always use theme-defined typography classes, never hardcoded font sizes +- Reference existing theme colors instead of adding new ones +- Ensure new components support both light and dark themes +- Use theme-generated CSS variables for dynamic styling + +### Code Standards + +- **Functional components**: Exclusive use of function components with hooks +- **Custom hooks**: Extract logic for reusability +- **Type-first development**: Comprehensive TypeScript usage +- **Component composition**: Prefer composition over inheritance + +## Feature Implementation Map + +### Directory & File System +`packages/ui/src/components/session/`: DirectoryTree, DirectoryExplorerDialog +`packages/ui/src/stores/`: DirectoryStore +Backend: `packages/web/server/index.js` with `listLocalDirectory()`, `getFilesystemHome()` + +### Session Switcher +`SessionSwitcherDialog.tsx`: Collapsible date groups, mobile parity with MobileOverlayPanel, Git worktree and shared session chips, streaming indicators. + +### Settings & Configuration +`packages/ui/src/components/sections/`: AgentsPage, CommandsPage, GitIdentitiesPage, ProvidersPage, SessionsPage, SettingsPage +Related stores: useAgentsStore, useCommandsStore, useConfigStore, useGitIdentitiesStore + +### Git Operations +`packages/ui/src/components/views/`: GitView, DiffView +`packages/ui/src/stores/`: useGitIdentitiesStore +Backend: `packages/ui/src/lib/gitApi.ts` + `packages/web/server/index.js` (simple-git wrapper) + +### Terminal +`packages/ui/src/components/views/`: TerminalView +`packages/ui/src/components/terminal/`: TerminalViewport (Xterm.js with FitAddon) +`packages/ui/src/stores/`: useTerminalStore +Backend: `packages/web/server/index.js` (node-pty wrapper with SSE) + +### Theme System +`packages/ui/src/lib/theme/`: themes (2 definitions), cssGenerator, syntaxThemeGenerator +`packages/ui/src/components/providers/`: ThemeProvider + +### Mobile & UX +`packages/ui/src/components/ui/`: MobileOverlayPanel +`packages/ui/src/hooks/`: useEdgeSwipe, useChatScrollManager diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..6aec091b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +## [Unreleased] + +- Pending + +## [1.0.0] - 2025-12-07 + +- Initial public release of OpenChamber web and desktop packages in a unified monorepo. +- Added GitHub Actions release pipeline with macOS signing/notarization, npm publish, and release asset uploads. +- Introduced OpenCode agent chat experience with section-based navigation, theming, and session persistence. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..e14fcde9 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,41 @@ +# Contributing to OpenChamber + +## Quick Start + +```bash +git clone https://github.com/btriapitsyn/openchamber.git +cd openchamber +pnpm install +pnpm run dev:web:full # Web development +pnpm run desktop:dev # Desktop development (Tauri) +``` + +## Before Submitting + +```bash +pnpm -r type-check # Must pass +pnpm -r lint # Must pass +pnpm -r build # Must succeed +``` + +## Code Style + +- Functional React components only +- TypeScript strict mode - no `any` without justification +- Use existing theme colors/typography - don't add new ones +- Components must support light and dark themes + +## Pull Requests + +1. Fork and create a branch +2. Make changes +3. Run validation commands above +4. Submit PR with clear description of what and why + +## Project Structure + +See [AGENTS.md](./AGENTS.md) for detailed architecture reference. + +## Questions? + +Open an issue. diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..6a962170 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Bohdan Triapitsyn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..213accfe --- /dev/null +++ b/README.md @@ -0,0 +1,86 @@ +# OpenChamber + +Web and desktop interface for the [OpenCode](https://opencode.ai) AI coding agent. Works alongside the OpenCode TUI. + +The OpenCode team is actively working on their own desktop app. I still decided to release this project as a fan-made alternative. + +It was entirely built with OpenCode tool - first with the TUI version, then with the first usable version of OpenChamber, which I then used to build the rest. + +The whole project was built entirely with AI coding agents under my supervision. It started as a hobby project and proof of concept that AI agents can create genuinely usable software. + +## Why use OpenChamber? + +- **Cross-device continuity**: Start in TUI, continue on tablet/phone, return to terminal - same session +- **Remote access**: Use OpenCode from anywhere via browser +- **Familiarity**: A visual alternative for developers who prefer GUI workflows + +## Features + +- Integrated terminal +- Git operations with identity management and AI commit message generation +- Beautiful themes (Flexoki Light/Dark) with dynamic CSS variable system +- Mobile-optimized with edge-swipe gestures, terminal control and optimizations all around +- Git worktrees operations with isolating sessions within them +- Memory optimizations with LRU eviction +- Rich permission cards with syntax-highlighted operation previews +- Smart tool visualization (inline diffs, file trees, results highlighting) +- Per-agent permission mode control (ask, allow, full) adjustable per-session +- Familiar diff viewer like you're used to in VSCode +- Built-in OpenCode agent/command management + +## Installation + +### CLI (Web Server) + +```bash +pnpm add -g openchamber + +openchamber # Start on port 3000 +openchamber --port 8080 # Custom port +openchamber --daemon # Background mode +openchamber --ui-password secret # Password-protect UI +openchamber stop # Stop server +``` + +### Desktop App (macOS) + +Download from [Releases](https://github.com/btriapitsyn/openchamber/releases). + +## Prerequisites + +- [OpenCode CLI](https://opencode.ai) installed and running (`opencode serve`) +- Node.js 20+ + +## Development + +```bash +git clone https://github.com/btriapitsyn/openchamber.git +cd openchamber +pnpm install + +pnpm run dev:web:full # Web development +pnpm run desktop:dev # Desktop development (Tauri) +pnpm run build # Production build +``` + +See [CONTRIBUTING.md](./CONTRIBUTING.md) for guidelines. + +## Tech Stack + +React 19, TypeScript, Vite 7, Tailwind CSS v4, Zustand, Radix UI, @opencode-ai/sdk, Express, Tauri (desktop) + +## Acknowledgments + +Independent project, not affiliated with OpenCode team. + +**Special thanks to:** + +- [OpenCode](https://opencode.ai) - For the excellent API and extensible architecture +- [Flexoki](https://github.com/kepano/flexoki) - Beautiful color scheme by [Steph Ango](https://stephango.com/flexoki) +- [Tauri](https://github.com/tauri-apps/tauri) - Desktop application framework +- [David Hill](https://x.com/iamdavidhill) - who inspired me to release this without [overthinking](https://x.com/iamdavidhill/status/1993648326450020746?s=20) +- My wife, who created a beautiful firework animation for the app while testing it for the first time + +## License + +MIT diff --git a/components.json b/components.json new file mode 100644 index 00000000..73afbdbc --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/docs/.gitkeep b/docs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..552d2124 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { globalIgnores } from 'eslint/config' + +export default tseslint.config([ + globalIgnores(['dist', '.openchamber']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs['recommended-latest'], + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/fix-deprecation.js b/fix-deprecation.js new file mode 100644 index 00000000..059058e5 --- /dev/null +++ b/fix-deprecation.js @@ -0,0 +1,81 @@ +#!/usr/bin/env node + +/** + * Fix for http-proxy package util._extend deprecation warning + * This script patches the http-proxy package to use Object.assign instead of util._extend + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +function fixHttpProxyDeprecation() { + try { + // Find the http-proxy package in node_modules + const httpProxyDir = path.join(__dirname, 'node_modules', 'http-proxy', 'lib', 'http-proxy'); + const indexPath = path.join(httpProxyDir, 'index.js'); + const commonPath = path.join(httpProxyDir, 'common.js'); + + if (!fs.existsSync(indexPath) || !fs.existsSync(commonPath)) { + return; + } + + // Patch index.js + let needsPatch = false; + + if (fs.existsSync(indexPath)) { + let content = fs.readFileSync(indexPath, 'utf8'); + + let indexPatched = false; + + if (content.includes("require('util')._extend")) { + content = content.replace( + /extend\s*=\s*require\('util'\)\._extend,/, + "extend = Object.assign," + ); + indexPatched = true; + } + + if (content.includes("require('util').inherits")) { + content = content.replace( + /require\('util'\)\.inherits\((\w+),\s*(\w+)\);/, + "Object.setPrototypeOf($1.prototype, $2.prototype);" + ); + indexPatched = true; + } + + if (indexPatched) { + fs.writeFileSync(indexPath, content, 'utf8'); + needsPatch = true; + } + } + + // Patch common.js + if (fs.existsSync(commonPath)) { + let content = fs.readFileSync(commonPath, 'utf8'); + + let commonPatched = false; + + if (content.includes("require('util')._extend")) { + content = content.replace( + /extend\s*=\s*require\('util'\)\._extend,/, + "extend = Object.assign," + ); + commonPatched = true; + } + + if (commonPatched) { + fs.writeFileSync(commonPath, content, 'utf8'); + needsPatch = true; + } + } + } catch (error) { + // Silently handle errors - functionality is not affected + } +} + +// Run the fix +fixHttpProxyDeprecation(); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 00000000..7b0b1675 --- /dev/null +++ b/package.json @@ -0,0 +1,118 @@ +{ + "name": "openchamber-monorepo", + "version": "1.0.0", + "description": "OpenChamber monorepo workspace for web, ui, and desktop runtimes", + "private": true, + "type": "module", + "packageManager": "pnpm@10.22.0", + "workspaces": [ + "packages/*" + ], + "engines": { + "node": ">=20.0.0" + }, + "keywords": [ + "opencode", + "ai", + "coding", + "openchamber", + "cli" + ], + "author": "Bohdan Triapitsyn", + "license": "MIT", + "scripts": { + "build": "pnpm --if-present run --recursive --filter \"./packages/*\" build", + "build:web": "pnpm -C packages/web run build", + "build:ui": "pnpm -C packages/ui run build", + "build:desktop": "pnpm -C packages/desktop run build", + "type-check": "pnpm --if-present run --recursive --filter \"./packages/*\" type-check", + "type-check:web": "pnpm -C packages/web run type-check", + "type-check:ui": "pnpm -C packages/ui run type-check", + "type-check:desktop": "pnpm -C packages/desktop run type-check", + "lint": "pnpm --if-present run --recursive --filter \"./packages/*\" lint", + "lint:web": "pnpm -C packages/web run lint", + "lint:ui": "pnpm -C packages/ui run lint", + "lint:desktop": "pnpm -C packages/desktop run lint", + "clean": "pnpm --if-present run --recursive --filter \"./packages/*\" clean", + "dev:web": "pnpm -C packages/web run build:watch", + "dev:web:server": "pnpm -C packages/web run dev:server:watch", + "dev:web:full": "concurrently -n \"api,build\" -c \"cyan,magenta\" \"pnpm -C packages/web run dev:server:watch\" \"pnpm -C packages/web run build:watch\"", + "start:web": "pnpm -C packages/web run start", + "pack:web": "pnpm -C packages/web pack --pack-destination .", + "desktop:start-cli": "node ./packages/desktop/scripts/opencode-cli.mjs start", + "desktop:stop-cli": "node ./packages/desktop/scripts/opencode-cli.mjs stop", + "desktop:dev": "node ./packages/desktop/scripts/desktop-dev.mjs", + "desktop:build": "pnpm -C packages/desktop build && pnpm -C packages/desktop tauri build", + "desktop:lint": "pnpm -C packages/desktop run lint && cargo fmt --manifest-path packages/desktop/src-tauri/Cargo.toml -- --check && cargo clippy --manifest-path packages/desktop/src-tauri/Cargo.toml -- -D warnings", + "desktop:type-check": "pnpm -C packages/desktop run type-check && cargo fmt --manifest-path packages/desktop/src-tauri/Cargo.toml -- --check && cargo clippy --manifest-path packages/desktop/src-tauri/Cargo.toml -- -D warnings", + "version:bump": "node scripts/bump-version.mjs", + "release:prepare": "pnpm run build && pnpm run type-check && pnpm run lint" + }, + "dependencies": { + "@fontsource/ibm-plex-mono": "^5.2.7", + "@fontsource/ibm-plex-sans": "^5.1.1", + "@heroui/scroll-shadow": "^2.3.18", + "@heroui/system": "^2.4.23", + "@heroui/theme": "^2.4.23", + "@ibm/plex": "^6.4.1", + "@opencode-ai/sdk": "^1.0.133", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-tooltip": "^1.2.8", + "@remixicon/react": "^4.7.0", + "@types/react-syntax-highlighter": "^15.5.13", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.3.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "electron-context-menu": "^4.1.1", + "electron-store": "^11.0.2", + "express": "^5.1.0", + "http-proxy-middleware": "^3.0.5", + "next-themes": "^0.4.6", + "node-pty": "^1.0.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "react-markdown": "^10.1.0", + "react-syntax-highlighter": "^15.6.6", + "remark-gfm": "^4.0.1", + "simple-git": "^3.28.0", + "sonner": "^2.0.7", + "strip-json-comments": "^5.0.3", + "tailwind-merge": "^3.3.1", + "yaml": "^2.8.1", + "zustand": "^5.0.8" + }, + "devDependencies": { + "@eslint/js": "^9.33.0", + "@tailwindcss/postcss": "^4.0.0", + "@types/node": "^24.3.1", + "@types/react": "^19.1.10", + "@types/react-dom": "^19.1.7", + "@vitejs/plugin-react": "^5.0.0", + "autoprefixer": "^10.4.21", + "concurrently": "^9.2.1", + "cors": "^2.8.5", + "cross-env": "^7.0.3", + "electron": "^38.2.0", + "electron-builder": "^24.13.3", + "eslint": "^9.33.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", + "nodemon": "^3.1.7", + "tailwindcss": "^4.0.0", + "tsx": "^4.20.6", + "tw-animate-css": "^1.3.8", + "typescript": "~5.8.3", + "typescript-eslint": "^8.39.1", + "vite": "^7.1.2" + } +} diff --git a/packages/desktop/.gitignore b/packages/desktop/.gitignore new file mode 100644 index 00000000..1d5fa289 --- /dev/null +++ b/packages/desktop/.gitignore @@ -0,0 +1,14 @@ +# Vite build output +dist/ + +# Tauri build artifacts +src-tauri/target/ + +# Tauri generated code +src-tauri/gen/ + +# OpenCode CLI state tracking +.opencode-cli-state.json + +# OS-specific +.DS_Store diff --git a/packages/desktop/README.md b/packages/desktop/README.md new file mode 100644 index 00000000..a6b3a533 --- /dev/null +++ b/packages/desktop/README.md @@ -0,0 +1,36 @@ +# @openchamber/desktop + +Desktop application for the [OpenCode](https://opencode.ai) AI coding agent. Built with Tauri. + +## Installation + +Download from [Releases](https://github.com/btriapitsyn/openchamber/releases). + +Currently available for macOS (Apple Silicon). + +## Prerequisites + +- [OpenCode CLI](https://opencode.ai) installed + +## Features + +- Native macOS app with auto-updates +- Integrated terminal +- Git operations with identity management and AI commit message generation +- Beautiful themes (Flexoki Light/Dark) +- Rich permission cards with syntax-highlighted operation previews +- Smart tool visualization (inline diffs, file trees, results highlighting) +- Per-agent permission mode control + +## Development + +```bash +git clone https://github.com/btriapitsyn/openchamber.git +cd openchamber +pnpm install +pnpm run desktop:dev +``` + +## License + +MIT diff --git a/packages/desktop/index.html b/packages/desktop/index.html new file mode 100644 index 00000000..769ba619 --- /dev/null +++ b/packages/desktop/index.html @@ -0,0 +1,64 @@ + + + + + + + + OpenChamber Desktop + + + +
+
OpenChamber
+
+ + + diff --git a/packages/desktop/package.json b/packages/desktop/package.json new file mode 100644 index 00000000..7d646389 --- /dev/null +++ b/packages/desktop/package.json @@ -0,0 +1,38 @@ +{ + "name": "@openchamber/desktop", + "version": "1.0.0", + "private": true, + "type": "module", + "desktopPrerequisites": [ + "Rust stable toolchain (via rustup)", + "Xcode Command Line Tools installed", + "Tauri CLI installed (cargo install tauri-cli@^2)" + ], + "scripts": { + "tauri": "tauri", + "dev": "vite dev --host 127.0.0.1 --port 1421", + "build": "vite build", + "preview": "vite preview --host 127.0.0.1 --port 5051", + "type-check": "tsc --noEmit", + "lint": "eslint \"./src/**/*.{ts,tsx}\" --config ../../eslint.config.js" + }, + "dependencies": { + "@tauri-apps/plugin-notification": "^2.3.3", + "@tauri-apps/plugin-process": "^2", + "@tauri-apps/plugin-updater": "^2", + "@openchamber/ui": "workspace:*", + "react": "^19.1.1", + "react-dom": "^19.1.1" + }, + "devDependencies": { + "@tauri-apps/cli": "^2", + "@tauri-apps/api": "^2.9.1", + "@tauri-apps/plugin-dialog": "^2.4.2", + "@types/node": "^24.3.1", + "@types/react": "^19.1.10", + "@types/react-dom": "^19.1.7", + "@vitejs/plugin-react": "^5.0.0", + "typescript": "~5.8.3", + "vite": "^7.1.2" + } +} diff --git a/packages/desktop/public/ibm-plex-mono-latin-600-normal.woff2 b/packages/desktop/public/ibm-plex-mono-latin-600-normal.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..67aeeb0104a02cf060b813abf6dcb6fdb97b7512 GIT binary patch literal 15620 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 literal 0 HcmV?d00001 diff --git a/packages/desktop/public/ibm-plex-mono-latin-700-normal.woff2 b/packages/desktop/public/ibm-plex-mono-latin-700-normal.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..5d7ff0c5f53380486db43fd746bd66cf3511ccf6 GIT binary patch literal 14908 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 uPxY;>+eP#z&}WNNefTrzTlyRNw!YK5DY@ { + if (cleaning) { + return; + } + cleaning = true; + + const stopChild = (child, label) => { + if (!child || child.killed) { + return; + } + try { + child.kill('SIGINT'); + } catch (error) { + console.warn(`[desktop:dev] Failed to stop ${label}:`, error); + } + }; + + stopChild(tauriProcess, 'Tauri dev process'); + + await stopCli({ silent: true }).catch((error) => { + console.warn('[desktop:dev] Failed to stop OpenCode CLI:', error); + }); + + process.exit(typeof code === 'number' ? code : 0); + }; + + const handleChildExit = (childName) => (code, signal) => { + if (code !== 0 || signal) { + console.warn(`[desktop:dev] ${childName} exited with code ${code ?? 'null'} signal ${signal ?? 'none'}.`); + } + teardown(code).catch((error) => { + console.error('[desktop:dev] Cleanup error:', error); + process.exit(code ?? 1); + }); + }; + + tauriProcess.on('exit', handleChildExit('Tauri dev process')); + const errorHandler = (label) => (error) => { + console.error(`[desktop:dev] Failed to start ${label}:`, error); + teardown(1).catch(() => process.exit(1)); + }; + + tauriProcess.on('error', errorHandler('Tauri dev process')); + + const signalExitCodes = { + SIGINT: 130, + SIGTERM: 143, + SIGQUIT: 131, + }; + + Object.entries(signalExitCodes).forEach(([signal, exitCode]) => { + process.on(signal, () => { + teardown(exitCode).catch(() => process.exit(exitCode)); + }); + }); +} + +main().catch((error) => { + console.error('[desktop:dev] Unexpected error:', error); + process.exit(1); +}); diff --git a/packages/desktop/scripts/opencode-cli.mjs b/packages/desktop/scripts/opencode-cli.mjs new file mode 100644 index 00000000..199fc967 --- /dev/null +++ b/packages/desktop/scripts/opencode-cli.mjs @@ -0,0 +1,237 @@ +#!/usr/bin/env node +import { spawn } from 'node:child_process'; +import { access, readFile, unlink, writeFile } from 'node:fs/promises'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const desktopDir = path.resolve(__dirname, '..'); +const stateFile = path.join(desktopDir, '.opencode-cli-state.json'); +const DEFAULT_BIN_CANDIDATES = [ + process.env.OPENCHAMBER_OPENCODE_PATH, + process.env.OPENCHAMBER_OPENCODE_BIN, + process.env.OPENCODE_PATH, + process.env.OPENCODE_BINARY, + '/opt/homebrew/bin/opencode', + '/usr/local/bin/opencode', + '/usr/bin/opencode', + path.join(os.homedir(), '.local/bin/opencode'), +].filter(Boolean); +const CLI_ARGS_ENV = process.env.OPENCHAMBER_OPENCODE_ARGS; +const DEFAULT_ARGS = CLI_ARGS_ENV + ? parseArgs(CLI_ARGS_ENV) + : ['api']; + +function parseArgs(raw) { + if (!raw || typeof raw !== 'string') { + return []; + } + const trimmed = raw.trim(); + if (!trimmed) { + return []; + } + if (trimmed.startsWith('[')) { + try { + const parsed = JSON.parse(trimmed); + if (Array.isArray(parsed) && parsed.every((item) => typeof item === 'string')) { + return parsed; + } + } catch { + // fall through to whitespace split + } + } + return trimmed.split(/\s+/g); +} + +async function fileExists(targetPath) { + try { + await access(targetPath, fs.constants.X_OK); + return true; + } catch { + return false; + } +} + +async function resolveCliPath() { + for (const candidate of DEFAULT_BIN_CANDIDATES) { + if (candidate && await fileExists(candidate)) { + return candidate; + } + } + + const envPath = process.env.PATH || ''; + for (const segment of envPath.split(path.delimiter)) { + const candidate = path.join(segment, 'opencode'); + if (await fileExists(candidate)) { + return candidate; + } + } + + throw new Error('Unable to locate the OpenCode CLI. Set OPENCHAMBER_OPENCODE_PATH to the executable.'); +} + +async function readState() { + try { + const raw = await readFile(stateFile, 'utf8'); + const data = JSON.parse(raw); + if (typeof data?.pid === 'number') { + return data; + } + } catch { + // ignore + } + return null; +} + +function isProcessAlive(pid) { + if (!pid || typeof pid !== 'number') { + return false; + } + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +} + +async function writeState(pid) { + await writeFile(stateFile, JSON.stringify({ pid }), 'utf8'); +} + +async function removeStateFile() { + try { + await unlink(stateFile); + } catch { + // already removed + } +} + +function spawnCli(cliPath, args) { + const env = { + ...process.env, + OPENCHAMBER_OPENCODE_PORT: process.env.OPENCHAMBER_OPENCODE_PORT || process.env.OPENCODE_PORT || process.env.OPENCHAMBER_INTERNAL_PORT || '0', + }; + const cwd = process.env.OPENCHAMBER_OPENCODE_CWD || process.cwd(); + + const child = spawn(cliPath, args.length > 0 ? args : DEFAULT_ARGS, { + cwd, + env, + detached: true, + stdio: 'ignore', + }); + + child.unref(); + return child; +} + +export async function startCli({ silent = false } = {}) { + const existing = await readState(); + if (existing?.pid && isProcessAlive(existing.pid)) { + if (!silent) { + console.log(`[desktop:start-cli] OpenCode CLI already running (pid ${existing.pid}).`); + } + return existing.pid; + } + + const cliPath = await resolveCliPath(); + const child = spawnCli(cliPath, DEFAULT_ARGS); + await writeState(child.pid); + if (!silent) { + console.log(`[desktop:start-cli] OpenCode CLI started (${cliPath}) pid ${child.pid}.`); + } + return child.pid; +} + +export async function stopCli({ silent = false } = {}) { + const state = await readState(); + if (!state?.pid) { + if (!silent) { + console.log('[desktop:stop-cli] No OpenCode CLI PID recorded.'); + } + return; + } + + const { pid } = state; + if (!isProcessAlive(pid)) { + await removeStateFile(); + if (!silent) { + console.log('[desktop:stop-cli] CLI already stopped.'); + } + return; + } + + try { + process.kill(pid, 'SIGTERM'); + } catch (error) { + if (!silent) { + console.error(`[desktop:stop-cli] Failed to send SIGTERM to pid ${pid}:`, error); + } + } + + const timeoutMs = 5000; + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + if (!isProcessAlive(pid)) { + await removeStateFile(); + if (!silent) { + console.log('[desktop:stop-cli] OpenCode CLI stopped.'); + } + return; + } + await new Promise((resolve) => setTimeout(resolve, 250)); + } + + try { + process.kill(pid, 'SIGKILL'); + if (!silent) { + console.warn(`[desktop:stop-cli] Forced termination sent to pid ${pid}.`); + } + } catch (error) { + if (!silent) { + console.error(`[desktop:stop-cli] Unable to terminate pid ${pid}:`, error); + } + } finally { + await removeStateFile(); + } +} + +async function main() { + const [, , command] = process.argv; + if (!command || command === '--help' || command === '-h') { + console.log('Usage: node opencode-cli.mjs '); + process.exit(0); + } + + if (command === 'start') { + await startCli(); + return; + } + if (command === 'stop') { + await stopCli(); + return; + } + if (command === 'status') { + const state = await readState(); + if (state?.pid && isProcessAlive(state.pid)) { + console.log(`OpenCode CLI running (pid ${state.pid}).`); + } else { + console.log('OpenCode CLI not running.'); + } + process.exit(0); + return; + } + + console.error(`Unknown command: ${command}`); + process.exit(1); +} + +if (import.meta.url === pathToFileURL(process.argv[1] || '').href) { + main().catch((error) => { + console.error('[desktop:opencode-cli] Unexpected error:', error); + process.exit(1); + }); +} diff --git a/packages/desktop/src-tauri/Cargo.lock b/packages/desktop/src-tauri/Cargo.lock new file mode 100644 index 00000000..74269d1e --- /dev/null +++ b/packages/desktop/src-tauri/Cargo.lock @@ -0,0 +1,6606 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_logger" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" +dependencies = [ + "android_log-sys", + "env_filter", + "log", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "ashpd" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +dependencies = [ + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.2", + "raw-window-handle", + "serde", + "serde_repr", + "tokio", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-compression" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e86f6d3dc9dc4352edeea6b8e499e13e3f5dc3b964d7ca5fd411415a3498473" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-executor" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" +dependencies = [ + "axum-core", + "axum-macros", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.3", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "borsh" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +dependencies = [ + "borsh-derive", + "cfg_aliases 0.2.1", +] + +[[package]] +name = "borsh-derive" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +dependencies = [ + "once_cell", + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "byte-unit" +version = "5.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cd29c3c585209b0cbc7309bfe3ed7efd8c84c21b7af29c8bfae908f8777174" +dependencies = [ + "rust_decimal", + "serde", + "utf8-width", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.10.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.8", +] + +[[package]] +name = "cc" +version = "1.2.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +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 = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "compression-codecs" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302266479cb963552d11bd042013a58ef1adc56768016c8b82b4199488f2d4ad" +dependencies = [ + "brotli", + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.29.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf 0.10.1", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.110", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.110", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.110", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.110", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "libc", + "objc2 0.6.3", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading 0.8.9", +] + +[[package]] +name = "dlopen2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.9.8", + "vswhom", + "winreg 0.55.0", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "env_filter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fern" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" +dependencies = [ + "log", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "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.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.10.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever", + "match_token", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-util" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +dependencies = [ + "equivalent", + "hashbrown 0.16.0", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.10.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kuchikiki" +version = "0.8.8-speedreader" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" +dependencies = [ + "cssparser", + "html5ever", + "indexmap 2.12.0", + "selectors", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading 0.7.4", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.177" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.10.0", + "libc", + "redox_syscall", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +dependencies = [ + "value-bag", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "mac-notification-sys" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fd3f75411f4725061682ed91f131946e912859d0044d39c4ec0aac818d7621" +dependencies = [ + "cc", + "objc2 0.6.3", + "objc2-foundation 0.3.2", + "time", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minisign-verify" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e856fdd13623a2f5f2f54676a4ee49502a96a80ef4a62bcedd23d52427c44d43" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "png", + "serde", + "thiserror 2.0.17", + "windows-sys 0.60.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.10.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +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.10.0", + "cfg-if", + "cfg_aliases 0.1.1", + "libc", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases 0.2.1", + "libc", + "memoffset", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "notify-rust" +version = "4.11.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6442248665a5aa2514e794af3b39661a8e73033b1cc5e59899e1276117ee4400" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "libc", + "objc2 0.6.3", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2 0.6.3", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "libc", + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-javascript-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" +dependencies = [ + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-osa-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.10.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.10.0", + "objc2 0.6.3", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-javascript-core", + "objc2-security", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "open" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "openchamber-desktop" +version = "0.0.0" +dependencies = [ + "anyhow", + "axum", + "chrono", + "dirs 5.0.1", + "fastrand", + "futures-util", + "log", + "nix 0.28.0", + "objc", + "objc2 0.6.3", + "objc2-foundation 0.3.2", + "once_cell", + "parking_lot", + "portable-pty", + "portpicker", + "regex", + "reqwest", + "serde", + "serde_json", + "serde_yaml", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-fs", + "tauri-plugin-log", + "tauri-plugin-notification", + "tauri-plugin-shell", + "tauri-plugin-updater", + "tokio", + "tokio-util", + "tower-http 0.5.2", + "uuid", + "window-vibrancy 0.7.1", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "osakit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" +dependencies = [ + "objc2 0.6.3", + "objc2-foundation 0.3.2", + "objc2-osa-kit", + "serde", + "serde_json", + "thiserror 2.0.17", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared 0.8.0", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.1", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64 0.22.1", + "indexmap 2.12.0", + "quick-xml 0.38.4", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[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 0.28.0", + "serial2", + "shared_library", + "shell-words", + "winapi", + "winreg 0.10.1", +] + +[[package]] +name = "portpicker" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be97d76faf1bfab666e1375477b23fde79eccf0276e9b63b92a39d676a889ba9" +dependencies = [ + "rand 0.8.5", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.7", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases 0.2.1", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.17", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.17", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases 0.2.1", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 2.0.17", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.12.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +dependencies = [ + "async-compression", + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http 0.6.6", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "ashpd", + "block2 0.6.2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rust_decimal" +version = "1.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.110", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "selectors" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.12.0", + "schemars 0.9.0", + "schemars 1.1.0", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.12.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "serial2" +version = "0.2.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cc76fa68e25e771492ca1e3c53d447ef0be3093e05cd3b47f4b712ba10c6f3c" +dependencies = [ + "cfg-if", + "libc", + "winapi", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "servo_arc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +dependencies = [ + "bytemuck", + "cfg_aliases 0.2.1", + "core-graphics", + "foreign-types", + "js-sys", + "log", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", + "raw-window-handle", + "redox_syscall", + "wasm-bindgen", + "web-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.110" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.34.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" +dependencies = [ + "bitflags 2.10.0", + "block2 0.6.2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dispatch", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "once_cell", + "parking_lot", + "raw-window-handle", + "scopeguard", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15524fc7959bfcaa051ba6d0b3fb1ef18e978de2176c7c6acb977f7fd14d35c7" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs 6.0.0", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.17", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy 0.6.0", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17fcb8819fd16463512a12f531d44826ce566f486d7ccd211c9c8cebdaec4e08" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs 6.0.0", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "toml 0.9.8", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa9844cefcf99554a16e0a278156ae73b0d8680bbc0e2ad1e4287aadd8489cf" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.110", + "tauri-utils", + "thiserror 2.0.17", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3764a12f886d8245e66b7ee9b43ccc47883399be2019a61d80cf0f4117446fde" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.110", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076c78a474a7247c90cad0b6e87e593c4c620ed4efdb79cbe0214f0021f6c39d" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "toml 0.9.8", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "313f8138692ddc4a2127c4c9607d616a46f5c042e77b3722450866da0aad2f19" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.17", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47df422695255ecbe7bac7012440eddaeefd026656171eac9559f5243d3230d9" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.17", + "toml 0.9.8", + "url", +] + +[[package]] +name = "tauri-plugin-log" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5709c792b8630290b5d9811a1f8fe983dd925fc87c7fc7f4923616458cd00b6" +dependencies = [ + "android_logger", + "byte-unit", + "fern", + "log", + "objc2 0.6.3", + "objc2-foundation 0.3.2", + "serde", + "serde_json", + "serde_repr", + "swift-rs", + "tauri", + "tauri-plugin", + "thiserror 2.0.17", + "time", +] + +[[package]] +name = "tauri-plugin-notification" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc" +dependencies = [ + "log", + "notify-rust", + "rand 0.9.2", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "thiserror 2.0.17", + "time", + "url", +] + +[[package]] +name = "tauri-plugin-shell" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c374b6db45f2a8a304f0273a15080d98c70cde86178855fc24653ba657a1144c" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.17", + "tokio", +] + +[[package]] +name = "tauri-plugin-updater" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b" +dependencies = [ + "base64 0.22.1", + "dirs 6.0.0", + "flate2", + "futures-util", + "http", + "infer", + "log", + "minisign-verify", + "osakit", + "percent-encoding", + "reqwest", + "semver", + "serde", + "serde_json", + "tar", + "tauri", + "tauri-plugin", + "tempfile", + "thiserror 2.0.17", + "time", + "tokio", + "url", + "windows-sys 0.60.2", + "zip", +] + +[[package]] +name = "tauri-runtime" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f766fe9f3d1efc4b59b17e7a891ad5ed195fa8d23582abb02e6c9a01137892" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2 0.6.3", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.17", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7950f3bde6bcca6655bc5e76d3d6ec587ceb81032851ab4ddbe1f508bdea2729" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-foundation 0.3.2", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a423c51176eb3616ee9b516a9fa67fed5f0e78baaba680e44eb5dd2cc37490" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "html5ever", + "http", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.3", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.17", + "toml 0.9.8", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" +dependencies = [ + "dunce", + "embed-resource", + "toml 0.9.8", +] + +[[package]] +name = "tauri-winrt-notification" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" +dependencies = [ + "quick-xml 0.37.5", + "thiserror 2.0.17", + "windows", + "windows-version", +] + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +dependencies = [ + "indexmap 2.12.0", + "serde_core", + "serde_spanned 1.0.3", + "toml_datetime 0.7.3", + "toml_parser", + "toml_writer", + "winnow 0.7.13", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.12.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.12.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +dependencies = [ + "indexmap 2.12.0", + "toml_datetime 0.7.3", + "toml_parser", + "winnow 0.7.13", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow 0.7.13", +] + +[[package]] +name = "toml_writer" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d5572781bee8e3f994d7467084e1b1fd7a93ce66bd480f8156ba89dee55a2b" +dependencies = [ + "crossbeam-channel", + "dirs 6.0.0", + "libappindicator", + "muda", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "once_cell", + "png", + "serde", + "thiserror 2.0.17", + "windows-sys 0.60.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +dependencies = [ + "getrandom 0.3.4", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "value-bag" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.110", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wayland-backend" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +dependencies = [ + "bitflags 2.10.0", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +dependencies = [ + "bitflags 2.10.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +dependencies = [ + "proc-macro2", + "quick-xml 0.37.5", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webpki-roots" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webview2-com" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" +dependencies = [ + "thiserror 2.0.17", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "window-vibrancy" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "010797bd7c40396fbc59d3105089fed0885fe267a0ef4a0a4646df54e28647f6" +dependencies = [ + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "raw-window-handle", + "windows-sys 0.60.2", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "wry" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2" +dependencies = [ + "base64 0.22.1", + "block2 0.6.2", + "cookie", + "crossbeam-channel", + "dirs 6.0.0", + "dpi", + "dunce", + "gdkx11", + "gtk", + "html5ever", + "http", + "javascriptcore-rs", + "jni", + "kuchikiki", + "libc", + "ndk", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.17", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "nix 0.30.1", + "ordered-stream", + "serde", + "serde_repr", + "tokio", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.13", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.110", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.7.13", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "indexmap 2.12.0", + "memchr", +] + +[[package]] +name = "zvariant" +version = "5.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" +dependencies = [ + "endi", + "enumflags2", + "serde", + "url", + "winnow 0.7.13", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.110", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.110", + "winnow 0.7.13", +] diff --git a/packages/desktop/src-tauri/Cargo.toml b/packages/desktop/src-tauri/Cargo.toml new file mode 100644 index 00000000..74456b1f --- /dev/null +++ b/packages/desktop/src-tauri/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "openchamber-desktop" +version = "1.0.0" +edition = "2021" +publish = false + +[lib] +name = "openchamber_desktop" +path = "src/lib.rs" + +[[bin]] +name = "openchamber-desktop" +path = "src/main.rs" + +[dependencies] +anyhow = "1.0.86" +axum = { version = "0.8.4", features = ["macros"] } +chrono = { version = "0.4", features = ["serde"] } +dirs = "5.0" +fastrand = "2.0" +futures-util = "0.3" +log = "0.4.28" +nix = { version = "0.28", features = ["signal"] } +objc = "0.2.7" +objc2 = "0.6.3" +objc2-foundation = { version = "0.3.2", features = ["NSProcessInfo", "NSString", "NSObjCRuntime"] } +once_cell = "1.19" +parking_lot = "0.12.3" +portable-pty = "0.9.0" +portpicker = "0.1.1" +regex = "1.10.4" +reqwest = { version = "0.12.4", default-features = false, features = ["json", "stream", "rustls-tls", "gzip", "brotli", "deflate"] } +serde = { version = "1.0.210", features = ["derive"] } +serde_json = "1.0.143" +serde_yaml = "0.9" +tauri = { version = "2.9.4", features = ["macos-private-api", "devtools" ] } +tauri-plugin-dialog = "2.4.2" +tauri-plugin-fs = "2.4.4" +tauri-plugin-log = "2.7.1" +tauri-plugin-shell = "2.3.3" +tokio = { version = "1.38", features = ["macros", "rt-multi-thread", "process", "signal", "sync", "time", "fs"] } +tower-http = { version = "0.5.2", features = ["cors"] } +uuid = { version = "1.18.1", features = ["v4"] } +tokio-util = { version = "0.7", features = ["io"] } +tauri-plugin-notification = "2.3.3" +tauri-plugin-updater = "2" + +[build-dependencies] +tauri-build = { version = "2.5.3", features = [] } + +[target.'cfg(target_os = "macos")'.dependencies] +window-vibrancy = "0.7.1" diff --git a/packages/desktop/src-tauri/Info.plist b/packages/desktop/src-tauri/Info.plist new file mode 100644 index 00000000..09babc54 --- /dev/null +++ b/packages/desktop/src-tauri/Info.plist @@ -0,0 +1,10 @@ + + + + + NSSupportsAutomaticTermination + + NSSupportsSuddenTermination + + + diff --git a/packages/desktop/src-tauri/build.rs b/packages/desktop/src-tauri/build.rs new file mode 100644 index 00000000..261851f6 --- /dev/null +++ b/packages/desktop/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build(); +} diff --git a/packages/desktop/src-tauri/capabilities/default.json b/packages/desktop/src-tauri/capabilities/default.json new file mode 100644 index 00000000..c6973567 --- /dev/null +++ b/packages/desktop/src-tauri/capabilities/default.json @@ -0,0 +1,42 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Default capabilities for OpenChamber desktop runtime", + "windows": ["main"], + "permissions": [ + "core:default", + "core:window:default", + "core:window:allow-close", + "core:window:allow-set-title", + "core:window:allow-set-size", + "core:window:allow-set-position", + "core:window:allow-start-dragging", + "core:webview:default", + "core:webview:allow-webview-close", + "shell:allow-open", + "shell:allow-execute", + "dialog:allow-open", + "dialog:allow-save", + "dialog:allow-message", + "dialog:allow-ask", + "dialog:allow-confirm", + "fs:allow-read-text-file", + "fs:allow-read-file", + "fs:allow-write-text-file", + "fs:allow-write-file", + "fs:allow-read-dir", + "fs:allow-exists", + "fs:allow-create", + "fs:allow-mkdir", + "fs:allow-remove", + "fs:scope-app-index", + "fs:scope-home", + "notification:default", + "notification:allow-is-permission-granted", + "notification:allow-request-permission", + "notification:allow-notify", + "updater:default", + "updater:allow-check", + "updater:allow-download-and-install" + ] +} diff --git a/packages/desktop/src-tauri/icons/app-icon-checkpoint.svg b/packages/desktop/src-tauri/icons/app-icon-checkpoint.svg new file mode 100644 index 00000000..a95cb607 --- /dev/null +++ b/packages/desktop/src-tauri/icons/app-icon-checkpoint.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/desktop/src-tauri/icons/app-icon.png b/packages/desktop/src-tauri/icons/app-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2ca311873a539d4e8859db055ddc5df221ac352d GIT binary patch literal 7163 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/desktop/src-tauri/icons/icon-rgba.png b/packages/desktop/src-tauri/icons/icon-rgba.png new file mode 100644 index 0000000000000000000000000000000000000000..11311999941ed402625953fabca20117ccb862bd GIT binary patch literal 19168 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 SRq@}e)3@|Z^h&NfJo_K;=tScH literal 0 HcmV?d00001 diff --git a/packages/desktop/src-tauri/icons/icon.icns b/packages/desktop/src-tauri/icons/icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..932b0a9c59ae961335c943f3e89a1d1222e7beee GIT binary patch literal 159393 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 z
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 T^_dh4_|d<7?NZ(a+duvv85Ur{ literal 0 HcmV?d00001 diff --git a/packages/desktop/src-tauri/src/assistant_notifications.rs b/packages/desktop/src-tauri/src/assistant_notifications.rs new file mode 100644 index 00000000..2fc1bb6c --- /dev/null +++ b/packages/desktop/src-tauri/src/assistant_notifications.rs @@ -0,0 +1,280 @@ +use std::{collections::HashSet, time::Duration}; + +use anyhow::Result; +use futures_util::TryStreamExt; +use log::{debug, info, warn}; +use reqwest::Client; +use serde::Deserialize; +use serde_json::Value; +use tauri::{AppHandle, Manager}; +use tauri_plugin_notification::NotificationExt; +use tokio::{io::AsyncBufReadExt, sync::Mutex}; +use tokio_util::io::StreamReader; + +use crate::DesktopRuntime; + +#[derive(Deserialize)] +struct EventEnvelope { + #[serde(rename = "type")] + event_type: String, + #[serde(default)] + properties: Value, +} + +pub fn spawn_assistant_notifications( + app: AppHandle, + runtime: DesktopRuntime, +) -> tauri::async_runtime::JoinHandle<()> { + tauri::async_runtime::spawn(async move { + let client = Client::builder() + // Give SSE a very long overall timeout so idle periods don't abort the stream. + .timeout(Duration::from_secs(24 * 60 * 60)) + .tcp_keepalive(Some(Duration::from_secs(30))) + .build() + .expect("failed to build reqwest client"); + + let mut shutdown_rx = runtime.subscribe_shutdown(); + let notified_messages = Mutex::new(HashSet::::new()); + + loop { + tokio::select! { + _ = shutdown_rx.recv() => { + info!("[desktop:notify] Shutdown received, stopping SSE listener"); + break; + } + _ = async { + if let Err(err) = run_once(&app, &runtime, &client, ¬ified_messages).await { + warn!("[desktop:notify] SSE loop error: {err:?}"); + } + tokio::time::sleep(Duration::from_secs(2)).await; + } => {} + } + } + }) +} + +async fn run_once( + app: &AppHandle, + runtime: &DesktopRuntime, + client: &Client, + notified_messages: &Mutex>, +) -> Result<()> { + let opencode = runtime.opencode_manager(); + + let port = match opencode.current_port() { + Some(port) => port, + None => { + warn!("[desktop:notify] OpenCode port unavailable; will retry"); + tokio::time::sleep(Duration::from_secs(2)).await; + return Ok(()); + } + }; + + let prefix = opencode.api_prefix(); + let mut url = format!("http://127.0.0.1:{port}{}/event", prefix); + + if let Some(dir) = opencode.get_working_directory().to_str().map(|s| s.to_string()) { + let mut parsed = reqwest::Url::parse(&url)?; + parsed + .query_pairs_mut() + .append_pair("directory", &dir); + url = parsed.to_string(); + } + + debug!("[desktop:notify] Connecting SSE for notifications: {url}"); + + let response = client + .get(&url) + .header("accept", "text/event-stream") + .header("accept-encoding", "identity") + .send() + .await?; + + debug!( + "[desktop:notify] SSE response status={} headers={:?}", + response.status(), + response.headers() + ); + + if !response.status().is_success() { + warn!( + "[desktop:notify] SSE connect failed with status {}", + response.status() + ); + tokio::time::sleep(Duration::from_secs(2)).await; + return Ok(()); + } + + let stream = response + .bytes_stream() + .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err)); + let mut reader = StreamReader::new(stream); + let mut buf = Vec::new(); + let mut data_lines: Vec = Vec::new(); + + loop { + buf.clear(); + let bytes_read = match reader.read_until(b'\n', &mut buf).await { + Ok(n) => n, + Err(err) => { + warn!("[desktop:notify] Read error in SSE stream: {err:?}"); + return Err(err.into()); + } + }; + if bytes_read == 0 { + break; + } + + let line = match std::str::from_utf8(&buf) { + Ok(s) => s.trim_end_matches(&['\r', '\n'][..]).to_string(), + Err(err) => { + warn!("[desktop:notify] Non-UTF8 SSE chunk: {err}"); + continue; + } + }; + + if line.is_empty() { + if data_lines.is_empty() { + continue; + } + let raw = data_lines.join("\n"); + data_lines.clear(); + + match serde_json::from_str::(&raw) { + Ok(event) => handle_event(app, event, notified_messages).await, + Err(err) => { + warn!("[desktop:notify] Failed to parse SSE data: {err}; raw={raw}"); + } + } + continue; + } + + if let Some(rest) = line.strip_prefix("data:") { + data_lines.push(rest.trim_start().to_string()); + } + } + + Ok(()) +} + +async fn handle_event( + app: &AppHandle, + event: EventEnvelope, + notified_messages: &Mutex>, +) { + if event.event_type.as_str() != "message.updated" { + return; + } + + let Some(info) = event.properties.get("info") else { + return; + }; + + let role = info.get("role").and_then(Value::as_str).unwrap_or_default(); + if role != "assistant" { + return; + } + + let finish = info.get("finish").and_then(Value::as_str); + if finish != Some("stop") { + return; + } + + let message_id = match info.get("id").and_then(Value::as_str) { + Some(id) => id.to_string(), + None => return, + }; + + { + let mut notified = notified_messages.lock().await; + if notified.contains(&message_id) { + return; + } + notified.insert(message_id.clone()); + } + + let raw_mode = info + .get("mode") + .and_then(Value::as_str) + .filter(|s| !s.is_empty()) + .unwrap_or("agent"); + let raw_model = info + .get("modelID") + .and_then(Value::as_str) + .filter(|s| !s.is_empty()) + .unwrap_or("assistant"); + + let title = format!("{} agent is ready", format_mode(raw_mode)); + let body = format!("{} completed the task", format_model_id(raw_model)); + + let should_notify = app + .get_webview_window("main") + .map(|window| { + let focused = window.is_focused().unwrap_or(false); + let minimized = window.is_minimized().unwrap_or(false); + // Only notify when the app is not in the foreground or is minimized + !focused || minimized + }) + .unwrap_or(true); + + if should_notify { + let _ = app + .notification() + .builder() + .title(title) + .body(body) + .sound("Glass") + .show(); + } +} + +fn format_mode(raw: &str) -> String { + if raw.is_empty() { + return "Agent".to_string(); + } + raw.split(&['-', '_', ' '][..]) + .filter(|s| !s.is_empty()) + .map(capitalize) + .collect::>() + .join(" ") +} + +fn format_model_id(raw: &str) -> String { + if raw.is_empty() { + return "Assistant".to_string(); + } + + let tokens: Vec<&str> = raw.split(&['-', '_'][..]).collect(); + let mut result: Vec = Vec::new(); + let mut i = 0; + + while i < tokens.len() { + let current = tokens[i]; + + if current.chars().all(|c| c.is_ascii_digit()) { + if i + 1 < tokens.len() && tokens[i + 1].chars().all(|c| c.is_ascii_digit()) { + let combined = format!("{}.{}", current, tokens[i + 1]); + result.push(combined); + i += 2; + continue; + } + } + + result.push(current.to_string()); + i += 1; + } + + result + .into_iter() + .map(|part| capitalize(&part)) + .collect::>() + .join(" ") +} + +fn capitalize(s: &str) -> String { + let mut chars = s.chars(); + match chars.next() { + Some(first) => first.to_uppercase().collect::() + chars.as_str(), + None => String::new(), + } +} diff --git a/packages/desktop/src-tauri/src/commands/files.rs b/packages/desktop/src-tauri/src/commands/files.rs new file mode 100644 index 00000000..9969a690 --- /dev/null +++ b/packages/desktop/src-tauri/src/commands/files.rs @@ -0,0 +1,443 @@ +use crate::{DesktopRuntime, SettingsStore}; +use serde::Serialize; +use std::{ + collections::{HashSet, VecDeque}, + path::{Path, PathBuf}, + time::UNIX_EPOCH, +}; +use tokio::fs; + +const DEFAULT_FILE_SEARCH_LIMIT: usize = 60; +const MAX_FILE_SEARCH_LIMIT: usize = 400; +const FILE_SEARCH_MAX_CONCURRENCY: usize = 5; +const FILE_SEARCH_EXCLUDED_DIRS: &[&str] = &[ + "node_modules", + ".git", + "dist", + "build", + ".next", + ".turbo", + ".cache", + "coverage", + "tmp", + "logs", +]; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FileListEntry { + name: String, + path: String, + is_directory: bool, + is_file: bool, + is_symbolic_link: bool, + size: Option, + modified_time: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DirectoryListResult { + directory: String, + path: String, + entries: Vec, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateDirectoryResponse { + success: bool, + path: String, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FileSearchHit { + name: String, + path: String, + relative_path: String, + extension: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SearchFilesResponse { + root: String, + count: usize, + files: Vec, +} + +#[derive(Debug)] +enum FsCommandError { + NotFound, + AccessDenied, + NotDirectory, + OutsideWorkspace, + Other(String), +} + +impl FsCommandError { + fn to_list_message(&self) -> String { + match self { + FsCommandError::NotFound => "Directory not found".to_string(), + FsCommandError::AccessDenied | FsCommandError::OutsideWorkspace => { + "Access to directory denied".to_string() + } + FsCommandError::NotDirectory => "Specified path is not a directory".to_string(), + FsCommandError::Other(message) => { + let _ = message; + "Failed to list directory".to_string() + } + } + } + + fn to_search_message(&self) -> String { + match self { + FsCommandError::NotFound => "Directory not found".to_string(), + FsCommandError::AccessDenied | FsCommandError::OutsideWorkspace => { + "Access to directory denied".to_string() + } + FsCommandError::NotDirectory => "Specified path is not a directory".to_string(), + FsCommandError::Other(message) => { + let _ = message; + "Failed to search files".to_string() + } + } + } + + fn to_create_message(&self) -> String { + match self { + FsCommandError::AccessDenied | FsCommandError::OutsideWorkspace => { + "Access to directory denied".to_string() + } + FsCommandError::NotDirectory => "Parent path must be a directory".to_string(), + FsCommandError::Other(message) => { + let _ = message; + "Failed to create directory".to_string() + } + FsCommandError::NotFound => "Parent directory not found".to_string(), + } + } +} + +impl From for FsCommandError { + fn from(error: std::io::Error) -> Self { + match error.kind() { + std::io::ErrorKind::NotFound => FsCommandError::NotFound, + std::io::ErrorKind::PermissionDenied => FsCommandError::AccessDenied, + _ => FsCommandError::Other(error.to_string()), + } + } +} + +#[tauri::command] +pub async fn list_directory( + path: Option, + state: tauri::State<'_, DesktopRuntime>, +) -> Result { + let workspace_root = resolve_workspace_root(state.settings()).await; + let resolved_path = resolve_sandboxed_path(path, workspace_root.as_ref()) + .await + .map_err(|err| err.to_list_message())?; + + let metadata = fs::metadata(&resolved_path) + .await + .map_err(|err| FsCommandError::from(err).to_list_message())?; + + if !metadata.is_dir() { + return Err(FsCommandError::NotDirectory.to_list_message()); + } + + // Re-check boundary after canonicalization to guard against traversal + if let Some(root) = &workspace_root { + if !resolved_path.starts_with(root) { + return Err(FsCommandError::OutsideWorkspace.to_list_message()); + } + } + + let mut entries = Vec::new(); + let mut dir_entries = fs::read_dir(&resolved_path) + .await + .map_err(|err| FsCommandError::from(err).to_list_message())?; + + while let Some(entry) = dir_entries + .next_entry() + .await + .map_err(|err| FsCommandError::from(err).to_list_message())? + { + let file_type = entry + .file_type() + .await + .map_err(|err| FsCommandError::from(err).to_list_message())?; + + let entry_path = entry.path(); + let name = entry.file_name().to_string_lossy().to_string(); + + let mut is_directory = file_type.is_dir(); + let is_symlink = file_type.is_symlink(); + + if !is_directory && is_symlink { + if let Ok(link_meta) = fs::metadata(&entry_path).await { + is_directory = link_meta.is_dir(); + } + } + + let metadata = fs::metadata(&entry_path).await.ok(); + let size = metadata + .as_ref() + .filter(|meta| meta.is_file()) + .map(|meta| meta.len()); + let modified_time = metadata + .and_then(|meta| meta.modified().ok()) + .and_then(|mtime| mtime.duration_since(UNIX_EPOCH).ok()) + .map(|duration| duration.as_millis() as i64); + + entries.push(FileListEntry { + name, + path: normalize_path(&entry_path), + is_directory, + is_file: file_type.is_file(), + is_symbolic_link: is_symlink, + size, + modified_time, + }); + } + + Ok(DirectoryListResult { + directory: normalize_path(&resolved_path), + path: normalize_path(&resolved_path), + entries, + }) +} + +#[tauri::command] +pub async fn search_files( + directory: Option, + query: Option, + max_results: Option, + state: tauri::State<'_, DesktopRuntime>, +) -> Result { + let workspace_root = resolve_workspace_root(state.settings()).await; + let resolved_root = resolve_sandboxed_path(directory, workspace_root.as_ref()) + .await + .map_err(|err| err.to_search_message())?; + + let limit = clamp_search_limit(max_results); + let normalized_query = query.unwrap_or_default().trim().to_lowercase(); + let match_all = normalized_query.is_empty(); + + let mut files = Vec::new(); + let mut queue = VecDeque::new(); + let mut visited = HashSet::new(); + + queue.push_back(resolved_root.clone()); + visited.insert(resolved_root.clone()); + + while !queue.is_empty() && files.len() < limit { + for _ in 0..FILE_SEARCH_MAX_CONCURRENCY { + let Some(dir) = queue.pop_front() else { + break; + }; + + let mut entries = match fs::read_dir(&dir).await { + Ok(entries) => entries, + Err(_) => continue, + }; + + while let Ok(Some(entry)) = entries.next_entry().await { + let Ok(file_type) = entry.file_type().await else { + continue; + }; + + let name = entry.file_name(); + let name_str = name.to_string_lossy(); + if name_str.is_empty() || name_str.starts_with('.') { + continue; + } + + let entry_path = entry.path(); + if file_type.is_dir() { + if should_skip_directory(&name_str) { + continue; + } + if visited.insert(entry_path.clone()) && files.len() < limit { + queue.push_back(entry_path); + } + continue; + } + + if !file_type.is_file() { + continue; + } + + let relative_path = relative_path(&resolved_root, &entry_path); + if !match_all { + let lowercase_name = name_str.to_lowercase(); + let lowercase_path = relative_path.to_lowercase(); + if !lowercase_name.contains(&normalized_query) + && !lowercase_path.contains(&normalized_query) + { + continue; + } + } + + let extension = entry_path + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.to_lowercase()); + + files.push(FileSearchHit { + name: name_str.to_string(), + path: normalize_path(&entry_path), + relative_path: relative_path.replace('\\', "/"), + extension, + }); + + if files.len() >= limit { + break; + } + } + + if files.len() >= limit { + break; + } + } + } + + Ok(SearchFilesResponse { + root: normalize_path(&resolved_root), + count: files.len(), + files, + }) +} + +#[tauri::command] +pub async fn create_directory( + path: String, + state: tauri::State<'_, DesktopRuntime>, +) -> Result { + let trimmed = path.trim(); + if trimmed.is_empty() { + return Err("Path is required".to_string()); + } + + let workspace_root = resolve_workspace_root(state.settings()).await; + let resolved_path = resolve_creatable_path(trimmed, workspace_root.as_ref()) + .await + .map_err(|err| err.to_create_message())?; + + fs::create_dir_all(&resolved_path) + .await + .map_err(|err| FsCommandError::from(err).to_create_message())?; + + Ok(CreateDirectoryResponse { + success: true, + path: normalize_path(&resolved_path), + }) +} + +async fn resolve_sandboxed_path( + path: Option, + workspace_root: Option<&PathBuf>, +) -> Result { + let candidate_input = path + .as_ref() + .map(|value| value.trim()) + .filter(|value| !value.is_empty()); + + let candidate_path = match (candidate_input, workspace_root) { + (Some(value), _) => PathBuf::from(value), + (None, Some(root)) => root.clone(), + (None, None) => default_home_directory(), + }; + + let resolved = if candidate_path.is_absolute() { + candidate_path + } else if let Some(root) = workspace_root { + root.join(candidate_path) + } else { + default_home_directory().join(candidate_path) + }; + + let canonicalized = fs::canonicalize(&resolved) + .await + .map_err(FsCommandError::from)?; + + if let Some(root) = workspace_root { + if !canonicalized.starts_with(root) { + return Err(FsCommandError::OutsideWorkspace); + } + } + + Ok(canonicalized) +} + +async fn resolve_creatable_path( + path: &str, + workspace_root: Option<&PathBuf>, +) -> Result { + let candidate = PathBuf::from(path); + if candidate.as_os_str().is_empty() { + return Err(FsCommandError::Other("Path is required".to_string())); + } + + let absolute = if candidate.is_absolute() { + candidate + } else if let Some(root) = workspace_root { + root.join(candidate) + } else { + default_home_directory().join(candidate) + }; + + let parent = absolute.parent().ok_or(FsCommandError::NotDirectory)?; + + let canonical_parent = fs::canonicalize(parent) + .await + .map_err(FsCommandError::from)?; + + if let Some(root) = workspace_root { + if !canonical_parent.starts_with(root) { + return Err(FsCommandError::OutsideWorkspace); + } + } + + Ok(absolute) +} + +async fn resolve_workspace_root(settings: &SettingsStore) -> Option { + if let Ok(Some(last_dir)) = settings.last_directory().await { + if let Ok(canonicalized) = fs::canonicalize(&last_dir).await { + return Some(canonicalized); + } + } + None +} + +fn default_home_directory() -> PathBuf { + dirs::home_dir().unwrap_or_else(|| PathBuf::from("/")) +} + +fn clamp_search_limit(value: Option) -> usize { + let limit = value.unwrap_or(DEFAULT_FILE_SEARCH_LIMIT); + limit.clamp(1, MAX_FILE_SEARCH_LIMIT) +} + +fn should_skip_directory(name: &str) -> bool { + if name.starts_with('.') { + return true; + } + FILE_SEARCH_EXCLUDED_DIRS + .iter() + .any(|dir| dir.eq_ignore_ascii_case(name)) +} + +fn normalize_path(path: &Path) -> String { + path.to_string_lossy().replace('\\', "/") +} + +fn relative_path(root: &Path, target: &Path) -> String { + target + .strip_prefix(root) + .map(|relative| normalize_path(relative)) + .unwrap_or_else(|_| normalize_path(target)) +} diff --git a/packages/desktop/src-tauri/src/commands/git.rs b/packages/desktop/src-tauri/src/commands/git.rs new file mode 100644 index 00000000..a824b4e5 --- /dev/null +++ b/packages/desktop/src-tauri/src/commands/git.rs @@ -0,0 +1,1673 @@ +use crate::{DesktopRuntime, SettingsStore}; +use anyhow::{anyhow, Context, Result}; +use log::{error, info, warn}; +use regex::Regex; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::{HashMap, HashSet}; +use std::path::{Path, PathBuf}; +use std::sync::LazyLock; +use tauri::State; +use tokio::fs; +use tokio::process::Command; + +const GIT_IDENTITY_STORAGE_FILE: &str = "git-identities.json"; + +// --- Structs mirroring TypeScript types --- + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct GitStatusFile { + pub path: String, + pub index: String, + pub working_dir: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct GitStatus { + pub current: String, + pub tracking: Option, + pub ahead: i32, + pub behind: i32, + pub files: Vec, + pub is_clean: bool, + pub diff_stats: Option>, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct DiffStat { + pub insertions: i32, + pub deletions: i32, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct GitBranchDetails { + pub current: bool, + pub name: String, + pub commit: String, + pub label: String, + pub tracking: Option, + pub ahead: Option, + pub behind: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct GitBranch { + pub all: Vec, + pub current: String, + pub branches: HashMap, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct GitCommitSummary { + pub changes: i32, + pub insertions: i32, + pub deletions: i32, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct GitCommitResult { + pub success: bool, + pub commit: String, + pub branch: String, + pub summary: GitCommitSummary, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct GitPushResult { + pub success: bool, + pub pushed: Vec, + pub repo: String, + #[serde(rename = "ref")] + pub ref_: Option, // "ref" is a keyword in Rust +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct GitPushRef { + pub local: String, + pub remote: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct GitPullResult { + pub success: bool, + pub summary: GitCommitSummary, + pub files: Vec, + pub insertions: i32, + pub deletions: i32, +} + +fn parse_shortstat(output: &str) -> GitCommitSummary { + let mut summary = GitCommitSummary { + changes: 0, + insertions: 0, + deletions: 0, + }; + + for line in output + .split('\n') + .map(|line| line.trim()) + .filter(|line| !line.is_empty()) + { + for part in line.split(',') { + let token = part.trim(); + if token.is_empty() { + continue; + } + + if token.contains("file changed") { + if let Some(value) = token.split_whitespace().next() { + summary.changes = value.parse().unwrap_or(0); + } + } else if token.contains("insertion") { + if let Some(value) = token.split_whitespace().next() { + summary.insertions = value.parse().unwrap_or(0); + } + } else if token.contains("deletion") { + if let Some(value) = token.split_whitespace().next() { + summary.deletions = value.parse().unwrap_or(0); + } + } + } + } + + summary +} + +async fn get_head_hash(root: &Path) -> Result { + let output = run_git(&["rev-parse", "HEAD"], root).await?; + Ok(output.trim().to_string()) +} + +async fn get_current_branch_name(root: &Path) -> Result { + let output = run_git(&["rev-parse", "--abbrev-ref", "HEAD"], root).await?; + Ok(output.trim().to_string()) +} + +async fn collect_shortstat_for_range(root: &Path, range: &str) -> Result { + let args = ["diff", "--shortstat", range]; + let output = run_git(&args, root).await.unwrap_or_default(); + Ok(parse_shortstat(&output)) +} + +async fn collect_changed_files_for_range(root: &Path, range: &str) -> Result> { + let args = ["diff", "--name-only", range]; + let output = run_git(&args, root).await.unwrap_or_default(); + Ok(output + .lines() + .map(|line| line.trim()) + .filter(|line| !line.is_empty()) + .map(|line| line.to_string()) + .collect()) +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct GitIdentityProfile { + pub id: String, + pub name: String, + pub user_name: String, + pub user_email: String, + pub ssh_key: Option, + pub color: Option, + pub icon: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct GitIdentityProfilesWrapper { + pub profiles: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct GitIdentitySummary { + pub user_name: Option, + pub user_email: Option, + pub ssh_command: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct GitLogEntry { + pub hash: String, + pub date: String, + pub message: String, + pub refs: String, + pub body: String, + #[serde(rename = "author_name")] + pub author_name: String, + #[serde(rename = "author_email")] + pub author_email: String, + pub files_changed: i32, + pub insertions: i32, + pub deletions: i32, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct GitLogResponse { + pub all: Vec, + pub latest: Option, + pub total: i32, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct GitWorktreeInfo { + pub worktree: String, + pub head: Option, + pub branch: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct GeneratedCommitMessage { + pub subject: String, + pub highlights: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct CommitFileEntry { + pub path: String, + pub insertions: i32, + pub deletions: i32, + pub is_binary: bool, + pub change_type: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GitCommitFilesResponse { + pub files: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CommitMessageResponse { + pub message: GeneratedCommitMessage, +} + +// --- Constants & Regexes --- + +static WORKTREE_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"^worktree (.+)$").unwrap()); +static HEAD_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"^HEAD (.+)$").unwrap()); +static BRANCH_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"^branch (.+)$").unwrap()); +static FILES_CHANGED_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"(\d+)\s+files?\s+changed").unwrap()); +static INSERTIONS_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"(\d+)\s+insertions?\(\+\)").unwrap()); +static DELETIONS_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"(\d+)\s+deletions?\(-\)").unwrap()); + +// --- Helpers --- + +async fn run_git(args: &[&str], cwd: &Path) -> Result { + run_git_with_allowed_exit(args, cwd, &[]).await +} + +async fn run_git_with_allowed_exit( + args: &[&str], + cwd: &Path, + allowed_codes: &[i32], +) -> Result { + let output = Command::new("git") + .args(args) + .current_dir(cwd) + .env("GIT_OPTIONAL_LOCKS", "0") + .env("LC_ALL", "C") + .output() + .await + .context("Failed to execute git command")?; + + if !output.status.success() { + if let Some(code) = output.status.code() { + if allowed_codes.contains(&code) { + return Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()); + } + } + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + return Err(anyhow!("{}", stderr)); + } + + Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) +} + +fn append_git_option(args: &mut Vec, value: &Value) { + match value { + Value::Null => {} + Value::Bool(false) => {} + Value::Bool(true) => {} + Value::Number(num) => args.push(num.to_string()), + Value::String(text) => { + let trimmed = text.trim(); + if !trimmed.is_empty() { + args.push(trimmed.to_string()); + } + } + Value::Array(items) => { + for item in items { + append_git_option(args, item); + } + } + Value::Object(map) => append_git_option_map(args, map), + } +} + +fn append_git_option_map(args: &mut Vec, map: &serde_json::Map) { + for (key, value) in map { + let flag = key.trim(); + if flag.is_empty() { + continue; + } + + match value { + Value::Null | Value::Bool(true) => args.push(flag.to_string()), + Value::Bool(false) => {} + Value::String(text) => args.push(format!("{flag}={text}")), + Value::Number(num) => args.push(format!("{flag}={num}")), + Value::Array(items) => { + if items.is_empty() { + args.push(flag.to_string()); + } else { + for item in items { + match item { + Value::Null | Value::Bool(true) => args.push(flag.to_string()), + Value::Bool(false) => {} + Value::String(text) => args.push(format!("{flag}={text}")), + Value::Number(num) => args.push(format!("{flag}={num}")), + other => append_git_option(args, other), + } + } + } + } + other => args.push(format!("{flag}={other}")), + } + } +} + +// Removed unused resolve_workspace_root function + +async fn validate_git_path(path: &str, _settings: &SettingsStore) -> Result { + let path_buf = PathBuf::from(path); + if !path_buf.exists() { + return Err(anyhow!("Directory does not exist: {}", path)); + } + + if !path_buf.is_absolute() { + return Err(anyhow!("Path must be absolute")); + } + + Ok(path_buf) +} + +// --- Identity Storage --- + +async fn get_identity_storage_path() -> Result { + let mut path = dirs::home_dir().ok_or_else(|| anyhow!("Could not find home directory"))?; + path.push(".config"); + path.push("openchamber"); + fs::create_dir_all(&path).await?; + path.push(GIT_IDENTITY_STORAGE_FILE); + Ok(path) +} + +async fn load_identities() -> Result> { + let path = get_identity_storage_path().await?; + info!("Loading identities from {:?}", path); + + if !path.exists() { + info!("Identities file does not exist at {:?}", path); + return Ok(Vec::new()); + } + + let content = fs::read_to_string(&path).await?; + info!("Read {} bytes from identities file", content.len()); + + let wrapper: serde_json::Value = match serde_json::from_str(&content) { + Ok(w) => w, + Err(e) => { + error!("Failed to parse identities JSON: {}", e); + return Err(e.into()); + } + }; + + // Handle both array and object wrapper format if needed, but spec says object with profiles array + if let Some(profiles) = wrapper.get("profiles") { + match serde_json::from_value::>(profiles.clone()) { + Ok(p) => { + info!("Successfully loaded {} profiles", p.len()); + Ok(p) + } + Err(e) => { + error!("Failed to deserialize profiles array: {}", e); + // Log the failing JSON segment for debugging + warn!("Profiles JSON: {}", profiles); + Err(e.into()) + } + } + } else { + warn!("No 'profiles' key found in identities JSON"); + Ok(Vec::new()) + } +} + +async fn save_identities(profiles: Vec) -> Result<()> { + let path = get_identity_storage_path().await?; + let wrapper = GitIdentityProfilesWrapper { profiles }; + let content = serde_json::to_string_pretty(&wrapper)?; + fs::write(path, content).await?; + Ok(()) +} + +// --- Commands --- + +#[tauri::command] +pub async fn check_is_git_repository( + directory: String, + state: State<'_, DesktopRuntime>, +) -> Result { + let path = validate_git_path(&directory, state.settings()) + .await + .map_err(|e| e.to_string())?; + let git_dir = path.join(".git"); + Ok(git_dir.exists()) +} + +#[tauri::command] +pub async fn get_git_status( + directory: String, + state: State<'_, DesktopRuntime>, +) -> Result { + let path = validate_git_path(&directory, state.settings()) + .await + .map_err(|e| e.to_string())?; + + // 1. Get porcelain status + let status_output = run_git(&["status", "--porcelain", "-b", "-z"], &path) + .await + .map_err(|e| e.to_string())?; + + // Parse status output + let mut files = Vec::new(); + let mut current = String::new(); + let mut tracking = None; + let mut ahead = 0; + let mut behind = 0; + + let entries: Vec<&str> = status_output.split('\0').collect(); + + for entry in entries { + if entry.is_empty() { + continue; + } + + if entry.starts_with("## ") { + // Branch info: ## main...origin/main [ahead 1, behind 2] + let branch_line = &entry[3..]; + if let Some((local, remote_part)) = branch_line.split_once("...") { + current = local.to_string(); + // Parse remote part for ahead/behind + // Format: origin/main [ahead 1, behind 2] or origin/main + if let Some(bracket_start) = remote_part.find('[') { + tracking = Some(remote_part[..bracket_start].trim().to_string()); + let stats = &remote_part[bracket_start + 1..remote_part.len() - 1]; // inside brackets + for part in stats.split(", ") { + if let Some(val) = part.strip_prefix("ahead ") { + ahead = val.parse().unwrap_or(0); + } else if let Some(val) = part.strip_prefix("behind ") { + behind = val.parse().unwrap_or(0); + } + } + } else { + tracking = Some(remote_part.trim().to_string()); + } + } else { + // No remote or initial commit + current = branch_line.to_string(); + } + continue; + } + + // File entries: XY PATH or R ORIG_PATH -> PATH + if entry.len() >= 4 { + let index_status = &entry[0..1]; + let working_status = &entry[1..2]; + let file_path = &entry[3..]; + + // Simple-git parsing logic approximation + files.push(GitStatusFile { + path: file_path.to_string(), + index: index_status.trim().to_string(), + working_dir: working_status.trim().to_string(), + }); + } + } + + // 2. Get diff stats (staged and unstaged) + let mut diff_stats = HashMap::new(); + + let collect_stats = |output: String| { + let mut stats = HashMap::new(); + for line in output.lines() { + let parts: Vec<&str> = line.split('\t').collect(); + if parts.len() >= 3 { + let insertions = if parts[0] == "-" { + 0 + } else { + parts[0].parse().unwrap_or(0) + }; + let deletions = if parts[1] == "-" { + 0 + } else { + parts[1].parse().unwrap_or(0) + }; + let path = parts[2].to_string(); + stats.insert( + path, + DiffStat { + insertions, + deletions, + }, + ); + } + } + stats + }; + + let staged_stats_raw = run_git(&["diff", "--cached", "--numstat"], &path) + .await + .unwrap_or_default(); + let working_stats_raw = run_git(&["diff", "--numstat"], &path) + .await + .unwrap_or_default(); + + let staged_stats = collect_stats(staged_stats_raw); + let working_stats = collect_stats(working_stats_raw); + + // Merge stats + let mut all_paths: HashSet = staged_stats.keys().cloned().collect(); + all_paths.extend(working_stats.keys().cloned()); + + for p in all_paths { + let s = staged_stats.get(&p).unwrap_or(&DiffStat { + insertions: 0, + deletions: 0, + }); + let w = working_stats.get(&p).unwrap_or(&DiffStat { + insertions: 0, + deletions: 0, + }); + diff_stats.insert( + p, + DiffStat { + insertions: s.insertions + w.insertions, + deletions: s.deletions + w.deletions, + }, + ); + } + + // 3. Handle new/untracked files (manual calculation if needed, or skip if complex) + // Node implementation does manual read. For now, let's assume files with '??' or 'A' + // might need stats if they aren't in numstat. + // NOTE: untracked files don't show up in `git diff --numstat`. + // We can try `wc -l` logic but Rust fs read is safer. + + for file in &files { + if (file.working_dir == "?" || file.index == "A") && !diff_stats.contains_key(&file.path) { + let full_path = path.join(&file.path); + if let Ok(metadata) = fs::metadata(&full_path).await { + if metadata.is_file() { + if let Ok(content) = fs::read_to_string(&full_path).await { + let lines = content.lines().count() as i32; + diff_stats.insert( + file.path.clone(), + DiffStat { + insertions: lines, + deletions: 0, + }, + ); + } + } + } + } + } + + Ok(GitStatus { + current, + tracking, + ahead, + behind, + is_clean: files.is_empty(), + files, + diff_stats: Some(diff_stats), + }) +} + +#[tauri::command] +pub async fn get_git_diff( + directory: String, + path_str: String, + staged: Option, + context_lines: Option, + state: State<'_, DesktopRuntime>, +) -> Result { + let root = validate_git_path(&directory, state.settings()) + .await + .map_err(|e| e.to_string())?; + + let mut args = vec!["diff", "--no-color"]; + let context = format!("-U{}", context_lines.unwrap_or(3)); + args.push(&context); + + if staged.unwrap_or(false) { + args.push("--cached"); + } + + args.push("--"); + args.push(&path_str); + + let output = run_git(&args, &root).await.unwrap_or_default(); + + if output.trim().is_empty() && !staged.unwrap_or(false) { + // Try --no-index for untracked files + // git diff --no-index -- /dev/null path + let full_path = root.join(&path_str); + if full_path.exists() { + let args_no_index = vec![ + "diff", + "--no-color", + &context, + "--no-index", + "--", + "/dev/null", + &path_str, + ]; + return run_git_with_allowed_exit(&args_no_index, &root, &[1]) + .await + .map_err(|e| e.to_string()); + } + } + + Ok(output) +} + +#[tauri::command] +pub async fn get_git_file_diff( + directory: String, + path_str: String, + state: State<'_, DesktopRuntime>, +) -> Result<(String, String), String> { + use tokio::fs; + + let root = validate_git_path(&directory, state.settings()) + .await + .map_err(|e| e.to_string())?; + + // Original from HEAD + let original_spec = format!("HEAD:{}", path_str); + let original_args = vec!["show", original_spec.as_str()]; + let original = run_git_with_allowed_exit(&original_args, &root, &[0, 128]) + .await + .unwrap_or_default(); + + // Modified from working tree (if file exists) + let full_path = root.join(&path_str); + let modified = if let Ok(metadata) = fs::metadata(&full_path).await { + if metadata.is_file() { + fs::read_to_string(&full_path).await.unwrap_or_default() + } else { + String::new() + } + } else { + String::new() + }; + + Ok((original, modified)) +} + +#[tauri::command] +pub async fn revert_git_file( + directory: String, + file_path: String, + state: State<'_, DesktopRuntime>, +) -> Result<(), String> { + let root = validate_git_path(&directory, state.settings()) + .await + .map_err(|e| e.to_string())?; + + // Check if tracked + let is_tracked = run_git(&["ls-files", "--error-unmatch", &file_path], &root) + .await + .is_ok(); + + if !is_tracked { + // Clean untracked + let _ = run_git(&["clean", "-f", "-d", "--", &file_path], &root).await; + // Fallback fs remove if git clean failed (e.g. ignored files) + let full_path = root.join(&file_path); + if full_path.exists() { + if full_path.is_dir() { + let _ = fs::remove_dir_all(full_path).await; + } else { + let _ = fs::remove_file(full_path).await; + } + } + } else { + // Restore staged + let _ = run_git(&["restore", "--staged", &file_path], &root).await; + // Restore working + let _ = run_git(&["restore", &file_path], &root).await; + } + + Ok(()) +} + +#[tauri::command] +pub async fn is_linked_worktree( + directory: String, + state: State<'_, DesktopRuntime>, +) -> Result { + let root = validate_git_path(&directory, state.settings()) + .await + .map_err(|e| e.to_string())?; + let git_dir = run_git(&["rev-parse", "--git-dir"], &root) + .await + .unwrap_or_default(); + let common_dir = run_git(&["rev-parse", "--git-common-dir"], &root) + .await + .unwrap_or_default(); + Ok(git_dir.trim() != common_dir.trim()) +} + +#[tauri::command] +pub async fn get_git_branches( + directory: String, + state: State<'_, DesktopRuntime>, +) -> Result { + let root = validate_git_path(&directory, state.settings()) + .await + .map_err(|e| e.to_string())?; + + // Discover actual remote heads so we can drop stale remote-tracking refs + let allowed_remote_heads: Option> = match run_git(&["ls-remote", "--heads", "origin"], &root).await { + Ok(ls_remote) => { + let mut set = HashSet::new(); + for line in ls_remote.lines() { + if let Some((_, ref_name)) = line.split_once('\t') { + if let Some(stripped) = ref_name.trim().strip_prefix("refs/heads/") { + set.insert(stripped.to_string()); + } + } + } + Some(set) + } + Err(err) => { + warn!("Failed to list remote heads: {}", err); + None + } + }; + + // Structured for-each-ref output so we can mark remotes consistently with the web runtime + let output = run_git( + &[ + "for-each-ref", + "--format=%(refname)|%(refname:short)|%(objectname)|%(upstream:short)|%(HEAD)|%(upstream:track)", + "refs/heads", + "refs/remotes", + ], + &root, + ) + .await + .map_err(|e| e.to_string())?; + + let mut all = Vec::new(); + let mut current_branch = String::new(); + let mut branches = HashMap::new(); + + for line in output.lines() { + let parts: Vec<&str> = line.split('|').collect(); + if parts.len() < 6 { + continue; + } + + let full_ref = parts[0].trim(); + let short_name = parts[1].trim(); + let commit = parts[2].to_string(); + let upstream = parts[3].trim(); + let is_current = parts[4] == "*"; + let track_info = parts[5]; + + let is_remote = full_ref.starts_with("refs/remotes/"); + + let normalized_name = if is_remote { + let (remote_name, branch_name) = match short_name.split_once('/') { + Some(parts) => parts, + None => continue, // skip malformed remote ref without branch + }; + + if branch_name == "HEAD" { + continue; + } + + if let Some(allowed) = &allowed_remote_heads { + if !allowed.contains(branch_name) { + continue; + } + } + + format!("remotes/{}/{}", remote_name, branch_name) + } else { + short_name.to_string() + }; + + let tracking = if upstream.is_empty() { + None + } else { + Some(upstream.to_string()) + }; + + if is_current { + current_branch = normalized_name.clone(); + } + all.push(normalized_name.clone()); + + let mut ahead = None; + let mut behind = None; + + // Parse track info like "[ahead 1, behind 2]" + if !track_info.is_empty() { + let content = track_info.trim_matches(|c| c == '[' || c == ']'); + for part in content.split(", ") { + if let Some(val) = part.strip_prefix("ahead ") { + ahead = val.parse().ok(); + } else if let Some(val) = part.strip_prefix("behind ") { + behind = val.parse().ok(); + } + } + } + + branches.insert( + normalized_name.clone(), + GitBranchDetails { + current: is_current, + name: normalized_name, + commit, + label: short_name.to_string(), + tracking, + ahead, + behind, + }, + ); + } + + Ok(GitBranch { + all, + current: current_branch, + branches, + }) +} + +#[tauri::command] +pub async fn delete_git_branch( + directory: String, + branch: String, + force: Option, + state: State<'_, DesktopRuntime>, +) -> Result<(), String> { + let root = validate_git_path(&directory, state.settings()) + .await + .map_err(|e| e.to_string())?; + let flag = if force.unwrap_or(false) { "-D" } else { "-d" }; + run_git(&["branch", flag, &branch], &root) + .await + .map_err(|e| e.to_string())?; + Ok(()) +} + +#[tauri::command] +pub async fn delete_remote_branch( + directory: String, + branch: String, + remote: Option, + state: State<'_, DesktopRuntime>, +) -> Result<(), String> { + let root = validate_git_path(&directory, state.settings()) + .await + .map_err(|e| e.to_string())?; + let remote_name = remote.unwrap_or_else(|| "origin".to_string()); + + // branch might be refs/heads/foo or just foo + let clean_branch = branch.trim_start_matches("refs/heads/"); + + run_git(&["push", &remote_name, "--delete", clean_branch], &root) + .await + .map_err(|e| e.to_string())?; + Ok(()) +} + +#[tauri::command] +pub async fn list_git_worktrees( + directory: String, + state: State<'_, DesktopRuntime>, +) -> Result, String> { + let root = validate_git_path(&directory, state.settings()) + .await + .map_err(|e| e.to_string())?; + let output = run_git(&["worktree", "list", "--porcelain"], &root) + .await + .map_err(|e| e.to_string())?; + + let mut worktrees = Vec::new(); + let mut current = GitWorktreeInfo { + worktree: String::new(), + head: None, + branch: None, + }; + + for line in output.lines() { + if let Some(cap) = WORKTREE_REGEX.captures(line) { + if !current.worktree.is_empty() { + worktrees.push(current.clone()); + current = GitWorktreeInfo { + worktree: String::new(), + head: None, + branch: None, + }; + } + current.worktree = cap[1].to_string(); + } else if let Some(cap) = HEAD_REGEX.captures(line) { + current.head = Some(cap[1].to_string()); + } else if let Some(cap) = BRANCH_REGEX.captures(line) { + current.branch = Some(cap[1].trim_start_matches("refs/heads/").to_string()); + } else if line.is_empty() { + if !current.worktree.is_empty() { + worktrees.push(current.clone()); + current = GitWorktreeInfo { + worktree: String::new(), + head: None, + branch: None, + }; + } + } + } + if !current.worktree.is_empty() { + worktrees.push(current); + } + + Ok(worktrees) +} + +#[tauri::command] +pub async fn add_git_worktree( + directory: String, + path_str: String, + branch: String, + create_branch: Option, + state: State<'_, DesktopRuntime>, +) -> Result<(), String> { + let root = validate_git_path(&directory, state.settings()) + .await + .map_err(|e| e.to_string())?; + + let mut args = vec!["worktree", "add"]; + if create_branch.unwrap_or(false) { + args.push("-b"); + args.push(&branch); + } + args.push(&path_str); + + if !create_branch.unwrap_or(false) { + args.push(&branch); + } + + run_git(&args, &root).await.map_err(|e| e.to_string())?; + Ok(()) +} + +#[tauri::command] +pub async fn remove_git_worktree( + directory: String, + path_str: String, + force: Option, + state: State<'_, DesktopRuntime>, +) -> Result<(), String> { + let root = validate_git_path(&directory, state.settings()) + .await + .map_err(|e| e.to_string())?; + let mut args = vec!["worktree", "remove", &path_str]; + if force.unwrap_or(false) { + args.push("--force"); + } + run_git(&args, &root).await.map_err(|e| e.to_string())?; + Ok(()) +} + +#[tauri::command] +pub async fn ensure_openchamber_ignored( + directory: String, + state: State<'_, DesktopRuntime>, +) -> Result<(), String> { + let root = validate_git_path(&directory, state.settings()) + .await + .map_err(|e| e.to_string())?; + let exclude_path = root.join(".git/info/exclude"); + + if let Some(parent) = exclude_path.parent() { + fs::create_dir_all(parent) + .await + .map_err(|e| e.to_string())?; + } + + let entry = "/.openchamber/\n"; + let mut content = fs::read_to_string(&exclude_path).await.unwrap_or_default(); + + if !content.contains("/.openchamber/") { + if !content.ends_with('\n') && !content.is_empty() { + content.push('\n'); + } + content.push_str(entry); + fs::write(&exclude_path, content) + .await + .map_err(|e| e.to_string())?; + } + Ok(()) +} + +#[tauri::command] +pub async fn create_git_commit( + directory: String, + message: String, + add_all: Option, + files: Option>, + state: State<'_, DesktopRuntime>, +) -> Result { + let root = validate_git_path(&directory, state.settings()) + .await + .map_err(|e| e.to_string())?; + + if add_all.unwrap_or(false) { + run_git(&["add", "."], &root) + .await + .map_err(|e| e.to_string())?; + } else if let Some(file_list) = files { + if !file_list.is_empty() { + let mut args = vec!["add"]; + args.extend(file_list.iter().map(|s| s.as_str())); + run_git(&args, &root).await.map_err(|e| e.to_string())?; + } + } + + run_git(&["commit", "-m", &message], &root) + .await + .map_err(|e| e.to_string())?; + + let commit_hash = get_head_hash(&root).await.map_err(|e| e.to_string())?; + let branch_name = get_current_branch_name(&root) + .await + .unwrap_or_else(|_| "HEAD".to_string()); + + let stat_output = run_git(&["log", "-1", "--pretty=", "--shortstat"], &root) + .await + .unwrap_or_default(); + let summary = parse_shortstat(&stat_output); + + Ok(GitCommitResult { + success: true, + commit: commit_hash, + branch: branch_name, + summary, + }) +} + +#[tauri::command] +pub async fn git_push( + directory: String, + remote: Option, + branch: Option, + options: Option, + state: State<'_, DesktopRuntime>, +) -> Result { + let root = validate_git_path(&directory, state.settings()) + .await + .map_err(|e| e.to_string())?; + let remote_name = remote.unwrap_or_else(|| "origin".to_string()); + let mut branch_name = branch.unwrap_or_default(); + + let mut args = vec!["push".to_string(), remote_name.clone()]; + if branch_name.is_empty() { + branch_name = get_current_branch_name(&root).await.unwrap_or_default(); + } + if !branch_name.is_empty() { + args.push(branch_name.clone()); + } + + if let Some(extra) = options.as_ref() { + append_git_option(&mut args, extra); + } + + let arg_refs: Vec<&str> = args.iter().map(|value| value.as_str()).collect(); + + // TODO: Streaming? Frontend types.ts defines GitPushResult, but doesn't mention streaming response for this call, + // but Stage 2 plan says "streaming progress events for long operations". + // Implementing simple await for now as `simple-git` wrapper does in `git-service.js`. + + run_git(&arg_refs, &root).await.map_err(|e| e.to_string())?; + + Ok(GitPushResult { + success: true, + pushed: if branch_name.is_empty() { + vec![] + } else { + vec![GitPushRef { + local: branch_name.clone(), + remote: format!("{}/{}", remote_name, branch_name), + }] + }, + repo: remote_name, + ref_: if branch_name.is_empty() { + None + } else { + Some(branch_name) + }, + }) +} + +#[tauri::command] +pub async fn git_pull( + directory: String, + remote: Option, + branch: Option, + state: State<'_, DesktopRuntime>, +) -> Result { + let root = validate_git_path(&directory, state.settings()) + .await + .map_err(|e| e.to_string())?; + let r = remote.unwrap_or_else(|| "origin".to_string()); + let b = branch.unwrap_or_default(); + + let mut args = vec!["pull", &r]; + if !b.is_empty() { + args.push(&b); + } + + let previous_head = get_head_hash(&root).await.ok(); + + run_git(&args, &root).await.map_err(|e| e.to_string())?; + + let (summary, files) = if let Some(previous) = previous_head { + let new_head = get_head_hash(&root).await.unwrap_or(previous.clone()); + if new_head != previous { + let range = format!("{previous}..{new_head}"); + let summary = collect_shortstat_for_range(&root, &range) + .await + .unwrap_or_else(|_| GitCommitSummary { + changes: 0, + insertions: 0, + deletions: 0, + }); + let files = collect_changed_files_for_range(&root, &range) + .await + .unwrap_or_default(); + (summary, files) + } else { + ( + GitCommitSummary { + changes: 0, + insertions: 0, + deletions: 0, + }, + vec![], + ) + } + } else { + ( + GitCommitSummary { + changes: 0, + insertions: 0, + deletions: 0, + }, + vec![], + ) + }; + + Ok(GitPullResult { + success: true, + summary: summary.clone(), + files, + insertions: summary.insertions, + deletions: summary.deletions, + }) +} + +#[tauri::command] +pub async fn git_fetch( + directory: String, + remote: Option, + state: State<'_, DesktopRuntime>, +) -> Result<(), String> { + let root = validate_git_path(&directory, state.settings()) + .await + .map_err(|e| e.to_string())?; + let r = remote.unwrap_or_else(|| "origin".to_string()); + run_git(&["fetch", &r], &root) + .await + .map_err(|e| e.to_string())?; + Ok(()) +} + +#[tauri::command] +pub async fn checkout_branch( + directory: String, + branch: String, + state: State<'_, DesktopRuntime>, +) -> Result<(), String> { + let root = validate_git_path(&directory, state.settings()) + .await + .map_err(|e| e.to_string())?; + run_git(&["checkout", &branch], &root) + .await + .map_err(|e| e.to_string())?; + Ok(()) +} + +#[tauri::command] +pub async fn create_branch( + directory: String, + name: String, + start_point: Option, + state: State<'_, DesktopRuntime>, +) -> Result<(), String> { + let root = validate_git_path(&directory, state.settings()) + .await + .map_err(|e| e.to_string())?; + let start = start_point.unwrap_or_else(|| "HEAD".to_string()); + run_git(&["checkout", "-b", &name, &start], &root) + .await + .map_err(|e| e.to_string())?; + Ok(()) +} + +#[tauri::command] +pub async fn get_git_log( + directory: String, + max_count: Option, + from: Option, + to: Option, + file: Option, + state: State<'_, DesktopRuntime>, +) -> Result { + let root = validate_git_path(&directory, state.settings()) + .await + .map_err(|e| e.to_string())?; + + let max = max_count.unwrap_or(50).to_string(); + let mut args = vec![ + "log", + "--max-count", + &max, + "--date=iso", + "--pretty=format:%H%x1f%an%x1f%ae%x1f%ad%x1f%s%x1e", + "--shortstat", + ]; + + let range; + if let (Some(f), Some(t)) = (&from, &to) { + range = format!("{}..{}", f, t); + args.push(&range); + } else if let Some(f) = &from { + range = format!("{}..HEAD", f); + args.push(&range); + } else if let Some(t) = &to { + args.push(t); + } + + if let Some(f) = &file { + args.push("--"); + args.push(f); + } + + let output = run_git(&args, &root).await.map_err(|e| e.to_string())?; + + let mut entries = Vec::new(); + let entries_raw: Vec<&str> = output.split('\x1e').collect(); + + let mut current_header = if !entries_raw.is_empty() { + entries_raw[0].trim() + } else { + "" + }; + + for i in 1..entries_raw.len() { + let chunk = entries_raw[i]; + + if current_header.is_empty() { + break; + } + + let header_parts: Vec<&str> = current_header.split('\x1f').collect(); + if header_parts.len() < 5 { + // Try to recover next header anyway before skipping + // But current_header is invalid, so we can't push an entry. + // We still need to update current_header for the next loop. + } else { + let hash = header_parts[0]; + let name = header_parts[1]; + let email = header_parts[2]; + let date = header_parts[3]; + let subject = header_parts[4]; + + let mut files_changed = 0; + let mut insertions = 0; + let mut deletions = 0; + + if let Some(cap) = FILES_CHANGED_REGEX.captures(chunk) { + files_changed = cap[1].parse().unwrap_or(0); + } + if let Some(cap) = INSERTIONS_REGEX.captures(chunk) { + insertions = cap[1].parse().unwrap_or(0); + } + if let Some(cap) = DELETIONS_REGEX.captures(chunk) { + deletions = cap[1].parse().unwrap_or(0); + } + + entries.push(GitLogEntry { + hash: hash.to_string(), + author_name: name.to_string(), + author_email: email.to_string(), + date: date.to_string(), + message: subject.to_string(), + body: String::new(), + refs: String::new(), + files_changed, + insertions, + deletions, + }); + } + + // Find next header by looking for the line containing \x1f (separator used in format) + // The chunk contains stats then the next header. + current_header = ""; + for line in chunk.lines().rev() { + let trimmed = line.trim(); + if !trimmed.is_empty() && trimmed.contains('\x1f') { + current_header = trimmed; + break; + } + } + } + + if entries.is_empty() && !output.is_empty() { + for line in output.lines() { + let parts: Vec<&str> = line.split('\x1f').collect(); + if parts.len() >= 5 { + entries.push(GitLogEntry { + hash: parts[0].to_string(), + author_name: parts[1].to_string(), + author_email: parts[2].to_string(), + date: parts[3].to_string(), + message: parts[4].to_string(), + body: "".to_string(), + refs: "".to_string(), + files_changed: 0, + insertions: 0, + deletions: 0, + }); + } + } + } + + Ok(GitLogResponse { + all: entries.clone(), + latest: entries.first().cloned(), + total: entries.len() as i32, + }) +} + +#[tauri::command] +pub async fn get_commit_files( + directory: String, + hash: String, + state: State<'_, DesktopRuntime>, +) -> Result { + let root = validate_git_path(&directory, state.settings()) + .await + .map_err(|e| e.to_string())?; + + // Get numstat for insertions/deletions per file + let numstat_output = run_git(&["show", "--numstat", "--format=", &hash], &root) + .await + .map_err(|e| e.to_string())?; + + let mut files = Vec::new(); + + for line in numstat_output.lines() { + let parts: Vec<&str> = line.split('\t').collect(); + if parts.len() < 3 { + continue; + } + + let insertions_raw = parts[0]; + let deletions_raw = parts[1]; + let file_path = parts[2..].join("\t"); + + if file_path.is_empty() { + continue; + } + + // Binary files show '-' for stats + let is_binary = insertions_raw == "-" && deletions_raw == "-"; + let insertions = if insertions_raw == "-" { + 0 + } else { + insertions_raw.parse().unwrap_or(0) + }; + let deletions = if deletions_raw == "-" { + 0 + } else { + deletions_raw.parse().unwrap_or(0) + }; + + files.push(CommitFileEntry { + path: file_path, + insertions, + deletions, + is_binary, + change_type: "M".to_string(), // Default, will update below + }); + } + + // Get accurate change types using --name-status + let name_status_output = run_git(&["show", "--name-status", "--format=", &hash], &root) + .await + .unwrap_or_default(); + + let mut status_map: HashMap = HashMap::new(); + for line in name_status_output.lines() { + let parts: Vec<&str> = line.split('\t').collect(); + if parts.len() >= 2 { + let status = parts[0].chars().next().unwrap_or('M').to_string(); + let path = parts.last().unwrap_or(&"").to_string(); + status_map.insert(path, status); + } + } + + // Update change types + for file in &mut files { + let base_path = if file.path.contains(" => ") { + file.path + .split(" => ") + .last() + .unwrap_or(&file.path) + .replace(['{', '}'], "") + } else { + file.path.clone() + }; + + if let Some(status) = status_map.get(&base_path).or_else(|| status_map.get(&file.path)) { + file.change_type = status.clone(); + } + } + + Ok(GitCommitFilesResponse { files }) +} + +#[tauri::command] +pub async fn get_git_identities() -> Result, String> { + load_identities().await.map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn create_git_identity( + profile: GitIdentityProfile, +) -> Result { + let mut profiles = load_identities().await.map_err(|e| e.to_string())?; + if profiles.iter().any(|p| p.id == profile.id) { + return Err(format!("Profile with ID {} already exists", profile.id)); + } + profiles.push(profile.clone()); + save_identities(profiles).await.map_err(|e| e.to_string())?; + Ok(profile) +} + +#[tauri::command] +pub async fn update_git_identity( + id: String, + updates: GitIdentityProfile, +) -> Result { + let mut profiles = load_identities().await.map_err(|e| e.to_string())?; + if let Some(idx) = profiles.iter().position(|p| p.id == id) { + profiles[idx] = updates.clone(); + save_identities(profiles).await.map_err(|e| e.to_string())?; + Ok(updates) + } else { + Err(format!("Profile with ID {} not found", id)) + } +} + +#[tauri::command] +pub async fn delete_git_identity(id: String) -> Result<(), String> { + let mut profiles = load_identities().await.map_err(|e| e.to_string())?; + let len = profiles.len(); + profiles.retain(|p| p.id != id); + if profiles.len() == len { + return Err(format!("Profile with ID {} not found", id)); + } + save_identities(profiles).await.map_err(|e| e.to_string())?; + Ok(()) +} + +#[tauri::command] +pub async fn get_current_git_identity( + directory: String, + state: State<'_, DesktopRuntime>, +) -> Result { + let root = validate_git_path(&directory, state.settings()) + .await + .map_err(|e| e.to_string())?; + + let user_name = run_git(&["config", "user.name"], &root).await.ok(); + let user_email = run_git(&["config", "user.email"], &root).await.ok(); + let ssh_command = run_git(&["config", "core.sshCommand"], &root).await.ok(); + + Ok(GitIdentitySummary { + user_name: user_name.filter(|s| !s.is_empty()), + user_email: user_email.filter(|s| !s.is_empty()), + ssh_command: ssh_command.filter(|s| !s.is_empty()), + }) +} + +#[tauri::command] +pub async fn set_git_identity( + directory: String, + profile_id: String, + state: State<'_, DesktopRuntime>, +) -> Result { + let root = validate_git_path(&directory, state.settings()) + .await + .map_err(|e| e.to_string())?; + let profiles = load_identities().await.map_err(|e| e.to_string())?; + + let profile = profiles + .into_iter() + .find(|p| p.id == profile_id) + .ok_or_else(|| format!("Profile {} not found", profile_id))?; + + run_git( + &["config", "--local", "user.name", &profile.user_name], + &root, + ) + .await + .map_err(|e| e.to_string())?; + run_git( + &["config", "--local", "user.email", &profile.user_email], + &root, + ) + .await + .map_err(|e| e.to_string())?; + + if let Some(key) = &profile.ssh_key { + let cmd = format!("ssh -i {}", key); + run_git(&["config", "--local", "core.sshCommand", &cmd], &root) + .await + .map_err(|e| e.to_string())?; + } else { + let _ = run_git(&["config", "--local", "--unset", "core.sshCommand"], &root).await; + } + + Ok(profile) +} + +#[tauri::command] +pub async fn generate_commit_message( + directory: String, + files: Vec, + state: State<'_, DesktopRuntime>, +) -> Result { + let _root = validate_git_path(&directory, state.settings()) + .await + .map_err(|e| e.to_string())?; + + // 1. Collect diffs + let mut diff_summaries = String::new(); + for file in files { + if let Ok(diff) = + get_git_diff(directory.clone(), file.clone(), None, None, state.clone()).await + { + let trimmed = if diff.len() > 4000 { + format!("{}\n...", &diff[..4000]) + } else { + diff + }; + diff_summaries.push_str(&format!("FILE: {}\n{}\n\n", file, trimmed)); + } + } + + if diff_summaries.is_empty() { + return Err("No diffs available for selected files".to_string()); + } + + // 2. Construct prompt (matching server/index.js) + let prompt = format!( + r#"You are drafting git commit notes for this codebase. Respond in JSON of the shape {{"subject": string, "highlights": string[]}} (ONLY the JSON in response, no markdown wrappers or anything except JSON) with these rules: +- subject follows our convention: type[optional-scope]: summary (examples: "feat: add diff virtualization", "fix(chat): restore enter key handling") +- allowed types: feat, fix, chore, style, refactor, perf, docs, test, build, ci (choose the best match or fallback to chore) +- summary must be imperative, concise, <= 70 characters, no trailing punctuation +- scope is optional; include only when obvious from filenames/folders; do not invent scopes +- focus on the most impactful user-facing change; if multiple capabilities ship together, align the subject with the dominant theme and use highlights to cover the other major outcomes +- highlights array should contain 2-3 plain sentences (<= 90 chars each) that describe distinct features or UI changes users will notice (e.g. "Add per-file revert action in Changes list"). Avoid subjective benefit statements, marketing tone, repeating the subject, or referencing helper function names. Highlight additions such as new controls/buttons, new actions (e.g. revert), or stored state changes explicitly. Skip highlights if fewer than two meaningful points exist. +- text must be plain (no markdown bullets); each highlight should start with an uppercase verb + +Diff summary: +{}"#, + diff_summaries + ); + + // 3. Call API + let client = Client::new(); + let res = client + .post("https://opencode.ai/zen/v1/chat/completions") + .json(&serde_json::json!({ + "model": "big-pickle", + "messages": [{ "role": "user", "content": prompt }], + "max_tokens": 3000, + "stream": false, + "reasoning": { + "effort": "low" + } + })) + .send() + .await + .map_err(|e| e.to_string())?; + + if !res.status().is_success() { + return Err(format!("API request failed: {}", res.status())); + } + + let body: serde_json::Value = res.json().await.map_err(|e| e.to_string())?; + let raw_content = body["choices"][0]["message"]["content"] + .as_str() + .unwrap_or("") + .trim(); + + // 4. Parse JSON + // Strip markdown code blocks if present + let cleaned = raw_content + .trim_start_matches("```json") + .trim_start_matches("```") + .trim_end_matches("```") + .trim(); + + let message: GeneratedCommitMessage = + serde_json::from_str(cleaned).map_err(|e| format!("Failed to parse AI response: {}", e))?; + + Ok(CommitMessageResponse { message }) +} diff --git a/packages/desktop/src-tauri/src/commands/logs.rs b/packages/desktop/src-tauri/src/commands/logs.rs new file mode 100644 index 00000000..8482b3eb --- /dev/null +++ b/packages/desktop/src-tauri/src/commands/logs.rs @@ -0,0 +1,25 @@ +use crate::logging::log_file_path; +use serde::Serialize; +use tokio::fs; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DesktopLogFile { + pub file_name: String, + pub content: String, +} + +#[tauri::command] +pub async fn fetch_desktop_logs() -> Result { + let path = log_file_path().ok_or_else(|| "Log location unavailable".to_string())?; + let content = fs::read_to_string(&path) + .await + .map_err(|err| format!("Failed to read log file: {err}"))?; + let file_name = path + .file_name() + .and_then(|value| value.to_str()) + .unwrap_or("desktop.log") + .to_string(); + + Ok(DesktopLogFile { file_name, content }) +} diff --git a/packages/desktop/src-tauri/src/commands/mod.rs b/packages/desktop/src-tauri/src/commands/mod.rs new file mode 100644 index 00000000..f877e819 --- /dev/null +++ b/packages/desktop/src-tauri/src/commands/mod.rs @@ -0,0 +1,7 @@ +pub mod files; +pub mod git; +pub mod logs; +pub mod permissions; +pub mod settings; +pub mod terminal; +pub mod notifications; diff --git a/packages/desktop/src-tauri/src/commands/notifications.rs b/packages/desktop/src-tauri/src/commands/notifications.rs new file mode 100644 index 00000000..7ed331cf --- /dev/null +++ b/packages/desktop/src-tauri/src/commands/notifications.rs @@ -0,0 +1,37 @@ +use serde::Deserialize; +use tauri::{AppHandle, Runtime}; +use tauri_plugin_notification::NotificationExt; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct NotificationPayload { + pub title: Option, + pub body: Option, +} + +#[tauri::command] +pub async fn desktop_notify( + app: AppHandle, + payload: Option, +) -> Result { + let title = payload + .as_ref() + .and_then(|p| p.title.as_deref()) + .unwrap_or("OpenChamber"); + let body = payload + .as_ref() + .and_then(|p| p.body.as_deref()) + .unwrap_or("Task completed"); + + match app + .notification() + .builder() + .title(title) + .body(body) + .sound("Glass") + .show() + { + Ok(_) => Ok(true), + Err(e) => Err(e.to_string()), + } +} diff --git a/packages/desktop/src-tauri/src/commands/permissions.rs b/packages/desktop/src-tauri/src/commands/permissions.rs new file mode 100644 index 00000000..0ed35905 --- /dev/null +++ b/packages/desktop/src-tauri/src/commands/permissions.rs @@ -0,0 +1,209 @@ +use log::{info, warn}; +use serde::{Deserialize, Serialize}; +use tauri::AppHandle; +use tauri::State; + +use crate::DesktopRuntime; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DirectoryPermissionRequest { + path: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DirectoryPermissionResult { + success: bool, + path: Option, + error: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StartAccessingResult { + success: bool, + error: Option, +} + +/// Process directory selection from frontend +/// Updates settings with lastDirectory +/// OpenCode restart is triggered separately via /api/opencode/directory endpoint +#[tauri::command] +pub async fn process_directory_selection( + path: String, + state: State<'_, DesktopRuntime>, +) -> Result { + use std::path::PathBuf; + + // Validate directory exists + let path_buf = PathBuf::from(&path); + if !path_buf.exists() { + return Ok(DirectoryPermissionResult { + success: false, + path: None, + error: Some("Directory does not exist".to_string()), + }); + } + + if !path_buf.is_dir() { + return Ok(DirectoryPermissionResult { + success: false, + path: None, + error: Some("Path is not a directory".to_string()), + }); + } + + // Update settings with lastDirectory + let mut settings = state + .settings() + .load() + .await + .map_err(|e| format!("Failed to load settings: {}", e))?; + + if let Some(obj) = settings.as_object_mut() { + obj.insert( + "lastDirectory".to_string(), + serde_json::Value::String(path.clone()), + ); + } + + state + .settings() + .save(settings) + .await + .map_err(|e| format!("Failed to save updated settings: {}", e))?; + + info!( + "[permissions] Updated settings with lastDirectory: {}", + path + ); + + Ok(DirectoryPermissionResult { + success: true, + path: Some(path), + error: None, + }) +} + +/// Legacy directory picker command (frontend handles actual dialog) +#[tauri::command] +pub async fn pick_directory( + _app_handle: AppHandle, + _state: State<'_, DesktopRuntime>, +) -> Result { + Ok(DirectoryPermissionResult { + success: false, + path: None, + error: Some( + "Use requestDirectoryAccess instead - it handles native dialog properly".to_string(), + ), + }) +} + +/// Request directory access (desktop implementation) +/// For unsandboxed apps, just validates the path is accessible +#[tauri::command] +pub async fn request_directory_access( + request: DirectoryPermissionRequest, + _state: State<'_, DesktopRuntime>, +) -> Result { + let path = request.path; + + let path_buf = std::path::PathBuf::from(&path); + if !path_buf.exists() { + return Ok(DirectoryPermissionResult { + success: false, + path: None, + error: Some("Directory does not exist".to_string()), + }); + } + + if !path_buf.is_dir() { + return Ok(DirectoryPermissionResult { + success: false, + path: None, + error: Some("Path is not a directory".to_string()), + }); + } + + // For unsandboxed apps, no bookmark needed - just verify access + match std::fs::read_dir(&path_buf) { + Ok(_) => Ok(DirectoryPermissionResult { + success: true, + path: Some(path), + error: None, + }), + Err(e) => Ok(DirectoryPermissionResult { + success: false, + path: None, + error: Some(format!("Cannot access directory: {}", e)), + }), + } +} + +/// Start accessing directory (desktop implementation) +#[tauri::command] +pub async fn start_accessing_directory( + path: String, + _state: State<'_, DesktopRuntime>, +) -> Result { + // Check if directory exists and is accessible + let path_buf = std::path::PathBuf::from(&path); + + if !path_buf.exists() { + return Ok(StartAccessingResult { + success: false, + error: Some("Directory does not exist".to_string()), + }); + } + + if !path_buf.is_dir() { + return Ok(StartAccessingResult { + success: false, + error: Some("Path is not a directory".to_string()), + }); + } + + // Try to read the directory to verify access + match std::fs::read_dir(&path_buf) { + Ok(_) => { + info!("Successfully started accessing directory: {}", path); + Ok(StartAccessingResult { + success: true, + error: None, + }) + } + Err(e) => { + warn!("Failed to access directory {}: {}", path, e); + Ok(StartAccessingResult { + success: false, + error: Some(format!("Failed to access directory: {}", e)), + }) + } + } +} + +/// Stop accessing directory (desktop implementation) +#[tauri::command] +pub async fn stop_accessing_directory( + _path: String, + _state: State<'_, DesktopRuntime>, +) -> Result { + // For Stage 1, just confirm the operation + // Full implementation would call stopAccessingSecurityScopedResource + info!("Stopped accessing directory"); + Ok(StartAccessingResult { + success: true, + error: None, + }) +} + +/// Restore bookmarks on app startup (no-op for unsandboxed apps) +#[tauri::command] +pub async fn restore_bookmarks_on_startup(_state: State<'_, DesktopRuntime>) -> Result<(), String> { + // For unsandboxed apps, no bookmarks needed + // Directory access is restored from settings.lastDirectory + info!("[permissions] Bookmark restore not needed for unsandboxed app"); + Ok(()) +} diff --git a/packages/desktop/src-tauri/src/commands/settings.rs b/packages/desktop/src-tauri/src/commands/settings.rs new file mode 100644 index 00000000..d8167d1e --- /dev/null +++ b/packages/desktop/src-tauri/src/commands/settings.rs @@ -0,0 +1,343 @@ +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use std::collections::HashSet; +use tauri::State; + +use crate::DesktopRuntime; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SettingsLoadResult { + settings: Value, + source: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RestartResult { + restarted: bool, +} + +/// Load settings from disk (matches Express handler behavior) +#[tauri::command] +pub async fn load_settings(state: State<'_, DesktopRuntime>) -> Result { + let settings = state + .settings() + .load() + .await + .map_err(|e| format!("Failed to load settings: {}", e))?; + + Ok(SettingsLoadResult { + settings, + source: "desktop".to_string(), + }) +} + +/// Save settings to disk with merge logic matching Express implementation +#[tauri::command] +pub async fn save_settings( + changes: Value, + state: State<'_, DesktopRuntime>, +) -> Result { + // Load current settings + let current = state + .settings() + .load() + .await + .map_err(|e| format!("Failed to load current settings: {}", e))?; + + // Sanitize incoming changes + let sanitized_changes = sanitize_settings_update(&changes); + + // Merge changes into current settings + let merged = merge_persisted_settings(¤t, &sanitized_changes); + + // Save merged settings + state + .settings() + .save(merged.clone()) + .await + .map_err(|e| format!("Failed to save settings: {}", e))?; + + // Format response + Ok(format_settings_response(&merged)) +} + +/// Restart OpenCode CLI (matches Express /api/config/reload) +#[tauri::command] +pub async fn restart_opencode(state: State<'_, DesktopRuntime>) -> Result { + state + .opencode + .restart() + .await + .map_err(|e| format!("Failed to restart OpenCode: {}", e))?; + + Ok(RestartResult { restarted: true }) +} + +/// Sanitize settings update payload (port of Express sanitizeSettingsUpdate) +fn sanitize_settings_update(payload: &Value) -> Value { + let mut result = json!({}); + + if let Some(obj) = payload.as_object() { + let result_obj = result.as_object_mut().unwrap(); + + // String fields + if let Some(Value::String(s)) = obj.get("themeId") { + if !s.is_empty() { + result_obj.insert("themeId".to_string(), json!(s)); + } + } + if let Some(Value::String(s)) = obj.get("themeVariant") { + if s == "light" || s == "dark" { + result_obj.insert("themeVariant".to_string(), json!(s)); + } + } + if let Some(Value::String(s)) = obj.get("lightThemeId") { + if !s.is_empty() { + result_obj.insert("lightThemeId".to_string(), json!(s)); + } + } + if let Some(Value::String(s)) = obj.get("darkThemeId") { + if !s.is_empty() { + result_obj.insert("darkThemeId".to_string(), json!(s)); + } + } + if let Some(Value::String(s)) = obj.get("lastDirectory") { + if !s.is_empty() { + result_obj.insert("lastDirectory".to_string(), json!(s)); + } + } + if let Some(Value::String(s)) = obj.get("homeDirectory") { + if !s.is_empty() { + result_obj.insert("homeDirectory".to_string(), json!(s)); + } + } + if let Some(Value::String(s)) = obj.get("uiFont") { + if !s.is_empty() { + result_obj.insert("uiFont".to_string(), json!(s)); + } + } + if let Some(Value::String(s)) = obj.get("monoFont") { + if !s.is_empty() { + result_obj.insert("monoFont".to_string(), json!(s)); + } + } + if let Some(Value::String(s)) = obj.get("markdownDisplayMode") { + if !s.is_empty() { + result_obj.insert("markdownDisplayMode".to_string(), json!(s)); + } + } + + // Boolean fields + if let Some(Value::Bool(b)) = obj.get("useSystemTheme") { + result_obj.insert("useSystemTheme".to_string(), json!(b)); + } + if let Some(Value::Bool(b)) = obj.get("showReasoningTraces") { + result_obj.insert("showReasoningTraces".to_string(), json!(b)); + } + + // Array fields + if let Some(arr) = obj.get("approvedDirectories") { + result_obj.insert( + "approvedDirectories".to_string(), + normalize_string_array(arr), + ); + } + if let Some(arr) = obj.get("securityScopedBookmarks") { + result_obj.insert( + "securityScopedBookmarks".to_string(), + normalize_string_array(arr), + ); + } + if let Some(arr) = obj.get("pinnedDirectories") { + result_obj.insert("pinnedDirectories".to_string(), normalize_string_array(arr)); + } + + // Typography sizes object (partial) + if let Some(typo) = obj.get("typographySizes") { + if let Some(sanitized) = sanitize_typography_sizes_partial(typo) { + result_obj.insert("typographySizes".to_string(), sanitized); + } + } + } + + result +} + +/// Merge persisted settings (port of Express mergePersistedSettings) +fn merge_persisted_settings(current: &Value, changes: &Value) -> Value { + let mut result = current.clone(); + + if let (Some(result_obj), Some(changes_obj)) = (result.as_object_mut(), changes.as_object()) { + // First apply all changes + for (key, value) in changes_obj { + result_obj.insert(key.clone(), value.clone()); + } + + // Build approvedDirectories from base + additional + let base_approved = if let Some(arr) = changes_obj.get("approvedDirectories") { + extract_string_vec(arr) + } else if let Some(arr) = current.get("approvedDirectories") { + extract_string_vec(arr) + } else { + vec![] + }; + + let mut additional_approved = vec![]; + if let Some(Value::String(s)) = changes_obj.get("lastDirectory") { + if !s.is_empty() { + additional_approved.push(s.clone()); + } + } + if let Some(Value::String(s)) = changes_obj.get("homeDirectory") { + if !s.is_empty() { + additional_approved.push(s.clone()); + } + } + + let mut approved_set: HashSet = base_approved.into_iter().collect(); + for item in additional_approved { + approved_set.insert(item); + } + let approved_vec: Vec = approved_set.into_iter().collect(); + result_obj.insert("approvedDirectories".to_string(), json!(approved_vec)); + + // Security scoped bookmarks + let base_bookmarks = if let Some(arr) = changes_obj.get("securityScopedBookmarks") { + extract_string_vec(arr) + } else if let Some(arr) = current.get("securityScopedBookmarks") { + extract_string_vec(arr) + } else { + vec![] + }; + let bookmarks_set: HashSet = base_bookmarks.into_iter().collect(); + let bookmarks_vec: Vec = bookmarks_set.into_iter().collect(); + result_obj.insert("securityScopedBookmarks".to_string(), json!(bookmarks_vec)); + + // Merge typography sizes if present + if changes_obj.contains_key("typographySizes") { + let current_typo = current + .get("typographySizes") + .and_then(|v| v.as_object()) + .cloned() + .unwrap_or_default(); + let changes_typo = changes_obj + .get("typographySizes") + .and_then(|v| v.as_object()) + .cloned() + .unwrap_or_default(); + + let mut merged_typo = current_typo; + for (key, value) in changes_typo { + merged_typo.insert(key, value); + } + result_obj.insert("typographySizes".to_string(), json!(merged_typo)); + } + } + + result +} + +/// Format settings response (port of Express formatSettingsResponse) +fn format_settings_response(settings: &Value) -> Value { + let mut result = sanitize_settings_update(settings); + + if let Some(obj) = result.as_object_mut() { + // Ensure array fields are normalized + obj.insert( + "approvedDirectories".to_string(), + normalize_string_array(settings.get("approvedDirectories").unwrap_or(&json!([]))), + ); + obj.insert( + "securityScopedBookmarks".to_string(), + normalize_string_array( + settings + .get("securityScopedBookmarks") + .unwrap_or(&json!([])), + ), + ); + obj.insert( + "pinnedDirectories".to_string(), + normalize_string_array(settings.get("pinnedDirectories").unwrap_or(&json!([]))), + ); + + // Typography sizes + if let Some(sanitized_typo) = sanitize_typography_sizes_partial( + settings.get("typographySizes").unwrap_or(&json!(null)), + ) { + obj.insert("typographySizes".to_string(), sanitized_typo); + } + + // showReasoningTraces with fallback + let show_reasoning = settings + .get("showReasoningTraces") + .and_then(|v| v.as_bool()) + .or_else(|| { + // Get showReasoningTraces from sanitized result instead of the current mutable borrow + if let Some(Value::Bool(b)) = obj.get("showReasoningTraces") { + Some(*b) + } else { + None + } + }) + .unwrap_or(false); + obj.insert("showReasoningTraces".to_string(), json!(show_reasoning)); + } + + result +} + +/// Normalize string array helper +fn normalize_string_array(input: &Value) -> Value { + if let Some(arr) = input.as_array() { + let strings: Vec = arr + .iter() + .filter_map(|v| v.as_str()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .collect(); + let unique: HashSet = strings.into_iter().collect(); + json!(unique.into_iter().collect::>()) + } else { + json!([]) + } +} + +/// Sanitize typography sizes partial helper +fn sanitize_typography_sizes_partial(input: &Value) -> Option { + if let Some(obj) = input.as_object() { + let mut result = serde_json::Map::new(); + let mut populated = false; + + for key in &["markdown", "code", "uiHeader", "uiLabel", "meta", "micro"] { + if let Some(Value::String(s)) = obj.get(*key) { + if !s.is_empty() { + result.insert(key.to_string(), json!(s)); + populated = true; + } + } + } + + if populated { + Some(json!(result)) + } else { + None + } + } else { + None + } +} + +/// Extract string vector from JSON value +fn extract_string_vec(value: &Value) -> Vec { + if let Some(arr) = value.as_array() { + arr.iter() + .filter_map(|v| v.as_str()) + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .collect() + } else { + vec![] + } +} diff --git a/packages/desktop/src-tauri/src/commands/terminal.rs b/packages/desktop/src-tauri/src/commands/terminal.rs new file mode 100644 index 00000000..bca4563c --- /dev/null +++ b/packages/desktop/src-tauri/src/commands/terminal.rs @@ -0,0 +1,308 @@ +use log::error; +use portable_pty::{Child, CommandBuilder, MasterPty, NativePtySystem, PtySize, PtySystem}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::HashMap, + env, + io::{Read, Write}, + path::{Path, PathBuf}, + sync::{Arc, Mutex}, + thread, +}; +use tauri::{Emitter, State, Window}; + +const DEFAULT_SHELL: &str = "/bin/zsh"; +const DEFAULT_TERM: &str = "xterm-256color"; +const DEFAULT_COLORTERM: &str = "truecolor"; +const DEFAULT_LOCALE: &str = "en_US.UTF-8"; +const TERM_PROGRAM_NAME: &str = "OpenChamber"; +const TERM_PROGRAM_VERSION: &str = env!("CARGO_PKG_VERSION"); + +pub struct TerminalSession { + pub master: Box, + pub writer: Arc>>, + pub child: Arc>>, +} + +pub struct TerminalState { + pub sessions: Arc>>, +} + +impl TerminalState { + pub fn new() -> Self { + Self { + sessions: Arc::new(Mutex::new(HashMap::new())), + } + } +} + +#[derive(Deserialize)] +pub struct CreateTerminalPayload { + pub cols: u16, + pub rows: u16, + pub cwd: Option, +} + +#[derive(Serialize)] +pub struct CreateTerminalResponse { + pub session_id: String, +} + +#[tauri::command] +pub async fn create_terminal_session( + payload: CreateTerminalPayload, + state: State<'_, TerminalState>, + window: Window, +) -> Result { + let pty_system = NativePtySystem::default(); + let size = PtySize { + rows: payload.rows, + cols: payload.cols, + pixel_width: 0, + pixel_height: 0, + }; + + let working_dir = resolve_working_directory(payload.cwd.as_deref())?; + let shell_path = resolve_shell(); + + let mut cmd = CommandBuilder::new(&shell_path); + if shell_accepts_login_flag(&shell_path) { + cmd.arg("-l"); + } + if let Some(cwd) = working_dir.to_str() { + cmd.cwd(cwd); + } + apply_terminal_environment(&mut cmd, &shell_path); + + let pair = pty_system.openpty(size).map_err(|e| e.to_string())?; + let child = pair + .slave + .spawn_command(cmd) + .map_err(|e| format!("Failed to spawn shell: {e}"))?; + drop(pair.slave); + + let reader = pair + .master + .try_clone_reader() + .map_err(|e| format!("Failed to clone PTY reader: {e}"))?; + let writer = Arc::new(Mutex::new( + pair.master + .take_writer() + .map_err(|e| format!("Failed to take PTY writer: {e}"))?, + )); + let master = pair.master; + let child = Arc::new(Mutex::new(child)); + + let session_id = uuid::Uuid::new_v4().to_string(); + state.sessions.lock().unwrap().insert( + session_id.clone(), + TerminalSession { + master, + writer: writer.clone(), + child: child.clone(), + }, + ); + + spawn_reader_thread(reader, window.clone(), session_id.clone()); + spawn_exit_watcher(child, window, state.sessions.clone(), session_id.clone()); + + Ok(CreateTerminalResponse { session_id }) +} + +#[tauri::command] +pub async fn send_terminal_input( + session_id: String, + data: String, + state: State<'_, TerminalState>, +) -> Result<(), String> { + let sessions = state.sessions.lock().unwrap(); + let Some(session) = sessions.get(&session_id) else { + return Err("Terminal session not found".to_string()); + }; + + let mut writer = session + .writer + .lock() + .map_err(|_| "Terminal busy".to_string())?; + writer + .write_all(data.as_bytes()) + .map_err(|e| format!("Failed to write to terminal: {e}"))?; + writer + .flush() + .map_err(|e| format!("Failed to flush terminal input: {e}"))?; + Ok(()) +} + +#[tauri::command] +pub async fn resize_terminal( + session_id: String, + cols: u16, + rows: u16, + state: State<'_, TerminalState>, +) -> Result<(), String> { + let mut sessions = state.sessions.lock().unwrap(); + let Some(session) = sessions.get_mut(&session_id) else { + return Err("Terminal session not found".to_string()); + }; + + session + .master + .resize(PtySize { + rows, + cols, + pixel_width: 0, + pixel_height: 0, + }) + .map_err(|e| format!("Failed to resize terminal: {e}"))?; + Ok(()) +} + +#[tauri::command] +pub async fn close_terminal( + session_id: String, + state: State<'_, TerminalState>, +) -> Result<(), String> { + let session = { + let mut sessions = state.sessions.lock().unwrap(); + sessions.remove(&session_id) + }; + + if let Some(session) = session { + if let Ok(mut child) = session.child.lock() { + let _ = child.kill(); + } + } + + Ok(()) +} + +fn spawn_reader_thread(mut reader: Box, window: Window, session_id: String) { + thread::spawn(move || { + let mut buffer = [0u8; 4096]; + let event_name = format!("terminal://{}", session_id); + loop { + match reader.read(&mut buffer) { + Ok(0) => break, + Ok(n) => { + let data = String::from_utf8_lossy(&buffer[..n]).to_string(); + if data.is_empty() { + continue; + } + + if let Err(error) = + window.emit(&event_name, serde_json::json!({ "type": "data", "data": data })) + { + error!("Failed to emit terminal data: {error}"); + break; + } + } + Err(error) => { + error!("Terminal read error: {error}"); + break; + } + } + } + }); +} + +fn spawn_exit_watcher( + child: Arc>>, + window: Window, + sessions: Arc>>, + session_id: String, +) { + thread::spawn(move || { + let status = { + let mut guard = child.lock().expect("terminal child poisoned"); + guard.wait() + }; + + let (exit_code, signal) = match status { + Ok(status) => ( + status.exit_code() as i32, + status.signal().map(|sig| sig.to_string()), + ), + Err(err) => { + error!("Failed to wait for terminal exit: {err}"); + (1, Some("Terminal crashed".to_string())) + } + }; + + let event_name = format!("terminal://{}", session_id); + let payload = serde_json::json!({ + "type": "exit", + "exitCode": exit_code, + "signal": signal + }); + let _ = window.emit(&event_name, payload); + + let mut sessions = sessions.lock().unwrap(); + sessions.remove(&session_id); + }); +} + +fn resolve_shell() -> String { + env::var("SHELL") + .ok() + .filter(|value| !value.trim().is_empty()) + .unwrap_or_else(|| DEFAULT_SHELL.to_string()) +} + +fn shell_accepts_login_flag(shell_path: &str) -> bool { + let shell_name = Path::new(shell_path) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(shell_path) + .to_lowercase(); + + matches!( + shell_name.as_str(), + name if name.contains("zsh") + || name.contains("bash") + || name.contains("sh") + || name.contains("fish") + || name.contains("ksh") + ) +} + +fn resolve_working_directory(input: Option<&str>) -> Result { + let maybe_path = input + .map(|value| PathBuf::from(value)) + .or_else(|| dirs::home_dir()); + + let Some(path) = maybe_path else { + return Err("Unable to determine working directory".to_string()); + }; + + if !path.exists() || !path.is_dir() { + return Err(format!( + "Working directory is not accessible: {}", + path.display() + )); + } + + Ok(path) +} + +fn apply_terminal_environment(cmd: &mut CommandBuilder, shell_path: &str) { + cmd.env( + "TERM", + env::var("TERM").unwrap_or_else(|_| DEFAULT_TERM.to_string()), + ); + cmd.env( + "COLORTERM", + env::var("COLORTERM").unwrap_or_else(|_| DEFAULT_COLORTERM.to_string()), + ); + cmd.env( + "LC_ALL", + env::var("LC_ALL").unwrap_or_else(|_| DEFAULT_LOCALE.to_string()), + ); + cmd.env( + "LANG", + env::var("LANG").unwrap_or_else(|_| DEFAULT_LOCALE.to_string()), + ); + cmd.env("TERM_PROGRAM", TERM_PROGRAM_NAME); + cmd.env("TERM_PROGRAM_VERSION", TERM_PROGRAM_VERSION); + cmd.env("OPENCHAMBER_DESKTOP", "1"); + cmd.env("SHELL", shell_path); +} diff --git a/packages/desktop/src-tauri/src/lib.rs b/packages/desktop/src-tauri/src/lib.rs new file mode 100644 index 00000000..e69de29b diff --git a/packages/desktop/src-tauri/src/logging.rs b/packages/desktop/src-tauri/src/logging.rs new file mode 100644 index 00000000..dbc2dba9 --- /dev/null +++ b/packages/desktop/src-tauri/src/logging.rs @@ -0,0 +1,20 @@ +use std::path::PathBuf; + +#[cfg(target_os = "macos")] +const PLATFORM_LOG_SEGMENTS: &[&str] = &["Library", "Logs", "OpenChamber"]; +#[cfg(not(target_os = "macos"))] +const PLATFORM_LOG_SEGMENTS: &[&str] = &[".config", "openchamber", "logs"]; + +pub fn log_directory() -> Option { + let mut path = dirs::home_dir()?; + for segment in PLATFORM_LOG_SEGMENTS { + path.push(segment); + } + Some(path) +} + +pub fn log_file_path() -> Option { + let mut dir = log_directory()?; + dir.push("desktop.log"); + Some(dir) +} diff --git a/packages/desktop/src-tauri/src/main.rs b/packages/desktop/src-tauri/src/main.rs new file mode 100644 index 00000000..9172e40b --- /dev/null +++ b/packages/desktop/src-tauri/src/main.rs @@ -0,0 +1,1084 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +mod commands; +mod logging; +mod assistant_notifications; +mod session_activity; +mod opencode_config; +mod opencode_manager; +mod window_state; + +use std::{collections::HashMap, path::PathBuf, sync::Arc, time::{Duration, Instant}}; + +use anyhow::{anyhow, Result}; +use axum::{ + body::{to_bytes, Body}, + extract::{OriginalUri, State}, + http::{Method, Request, Response, StatusCode}, + response::IntoResponse, + routing::{any, get, post}, + Json, Router, +}; +use assistant_notifications::spawn_assistant_notifications; +use session_activity::spawn_session_activity_tracker; +use commands::files::{create_directory, list_directory, search_files}; +use commands::git::{ + add_git_worktree, check_is_git_repository, checkout_branch, create_branch, create_git_commit, + create_git_identity, delete_git_branch, delete_git_identity, delete_remote_branch, + ensure_openchamber_ignored, generate_commit_message, get_commit_files, get_current_git_identity, + get_git_branches, get_git_diff, get_git_file_diff, get_git_identities, get_git_log, get_git_status, + git_fetch, git_pull, git_push, is_linked_worktree, list_git_worktrees, remove_git_worktree, + revert_git_file, set_git_identity, update_git_identity, +}; +use commands::logs::fetch_desktop_logs; +use commands::permissions::{ + pick_directory, process_directory_selection, request_directory_access, + restore_bookmarks_on_startup, start_accessing_directory, stop_accessing_directory, +}; +use commands::notifications::desktop_notify; +use commands::settings::{load_settings, restart_opencode, save_settings}; +use commands::terminal::{ + close_terminal, create_terminal_session, resize_terminal, send_terminal_input, TerminalState, +}; +use futures_util::StreamExt as FuturesStreamExt; +use log::{error, info, warn}; +use opencode_manager::OpenCodeManager; +use portpicker::pick_unused_port; +use reqwest::{header, Body as ReqwestBody, Client}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use tauri::{Emitter, Manager, WebviewWindow}; +use tauri_plugin_dialog::init as dialog_plugin; +use tauri_plugin_fs::init as fs_plugin; +use tauri_plugin_log::{Target, TargetKind}; +use tauri_plugin_notification::init as notification_plugin; +use tauri_plugin_shell::init as shell_plugin; +use tokio::{ + fs, + net::TcpListener, + sync::{broadcast, Mutex}, +}; +use tower_http::cors::CorsLayer; +use window_state::{load_window_state, persist_window_state, WindowStateManager}; + +#[cfg(target_os = "macos")] +use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial}; + +const PROXY_BODY_LIMIT: usize = 32 * 1024 * 1024; // 32MB +const CLIENT_RELOAD_DELAY_MS: u64 = 800; +const MODELS_DEV_API_URL: &str = "https://models.dev/api.json"; +const MODELS_METADATA_CACHE_TTL: Duration = Duration::from_secs(5 * 60); +const MODELS_METADATA_REQUEST_TIMEOUT: Duration = Duration::from_secs(8); + +#[derive(Clone)] +pub(crate) struct DesktopRuntime { + server_port: u16, + shutdown_tx: broadcast::Sender<()>, + opencode: Arc, + settings: Arc, +} + +impl DesktopRuntime { + async fn initialize() -> Result { + let settings = Arc::new(SettingsStore::new()?); + + // Read lastDirectory from settings before starting OpenCode + let initial_dir = settings.last_directory().await.ok().flatten(); + + let opencode = Arc::new(OpenCodeManager::new_with_directory(initial_dir.clone())); + + // Try to start OpenCode if CLI is available + if opencode.is_cli_available() { + if let Err(e) = opencode.ensure_running().await { + warn!("[desktop] Failed to start OpenCode: {}", e); + } + } else { + info!("[desktop] OpenCode CLI not available - running in limited mode"); + } + + let client = Client::builder().build()?; + + let (shutdown_tx, shutdown_rx) = broadcast::channel(2); + let server_port = + pick_unused_port().ok_or_else(|| anyhow!("No free port available"))? as u16; + let server_state = ServerState { + client, + opencode: opencode.clone(), + server_port, + directory_change_lock: Arc::new(Mutex::new(())), + models_metadata_cache: Arc::new(Mutex::new(ModelsMetadataCache::default())), + }; + + spawn_http_server(server_port, server_state, shutdown_rx); + + Ok(Self { + server_port, + shutdown_tx, + opencode, + settings, + }) + } + + async fn shutdown(&self) { + let _ = self.shutdown_tx.send(()); + let _ = self.opencode.shutdown().await; + } + + pub(crate) fn settings(&self) -> &SettingsStore { + self.settings.as_ref() + } + + pub(crate) fn subscribe_shutdown(&self) -> broadcast::Receiver<()> { + self.shutdown_tx.subscribe() + } + + pub(crate) fn opencode_manager(&self) -> Arc { + self.opencode.clone() + } +} + +#[derive(Clone)] +struct ServerState { + client: Client, + opencode: Arc, + server_port: u16, + directory_change_lock: Arc>, + models_metadata_cache: Arc>, +} + +#[derive(Default)] +struct ModelsMetadataCache { + payload: Option, + fetched_at: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct ConfigActionResponse { + success: bool, + requires_reload: bool, + message: String, + reload_delay_ms: u64, +} + +#[derive(Serialize)] +struct ConfigErrorResponse { + error: String, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct ConfigMetadataResponse { + name: String, + sources: opencode_config::ConfigSources, + is_built_in: bool, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct HealthResponse { + status: &'static str, + server_port: u16, + opencode_port: Option, + api_prefix: String, + is_opencode_ready: bool, + cli_available: bool, +} + +#[derive(Serialize)] +struct ServerInfoPayload { + server_port: u16, + opencode_port: Option, + api_prefix: String, + cli_available: bool, +} + +#[tauri::command] +async fn desktop_server_info( + state: tauri::State<'_, DesktopRuntime>, +) -> Result { + Ok(ServerInfoPayload { + server_port: state.server_port, + opencode_port: state.opencode.current_port(), + api_prefix: state.opencode.api_prefix(), + cli_available: state.opencode.is_cli_available(), + }) +} + +#[tauri::command] +async fn desktop_restart_opencode(state: tauri::State<'_, DesktopRuntime>) -> Result<(), String> { + state + .opencode + .restart() + .await + .map_err(|err| err.to_string()) +} + +#[tauri::command] +async fn desktop_open_devtools(window: WebviewWindow) -> Result<(), String> { + window.open_devtools(); + Ok(()) +} + +#[cfg(target_os = "macos")] +fn prevent_app_nap() { + use objc2_foundation::{NSActivityOptions, NSProcessInfo, NSString}; + + // NSActivityUserInitiated (0x00FFFFFF) | NSActivityLatencyCritical (0xFF00000000) + let options = NSActivityOptions(0x00FFFFFF | 0xFF00000000); + let reason = NSString::from_str("Prevent App Nap"); + + let process_info = NSProcessInfo::processInfo(); + let activity = process_info.beginActivityWithOptions_reason(options, &reason); + + // Leak the activity token to keep it active indefinitely + std::mem::forget(activity); + + info!("[macos] App Nap prevention enabled via objc2"); +} + +fn main() { + let mut log_builder = tauri_plugin_log::Builder::default() + .level(log::LevelFilter::Info) + .clear_targets() + .target(Target::new(TargetKind::Stdout)) + .target(Target::new(TargetKind::Webview)); + + if let Some(dir) = logging::log_directory() { + log_builder = log_builder.target(Target::new(TargetKind::Folder { + path: dir, + file_name: Some("desktop".into()), + })); + } + + let app = tauri::Builder::default() + .plugin(shell_plugin()) + .plugin(dialog_plugin()) + .plugin(fs_plugin()) + .plugin(notification_plugin()) + .plugin(tauri_plugin_updater::Builder::new().build()) + .plugin(log_builder.build()) + .setup(|app| { + #[cfg(target_os = "macos")] + prevent_app_nap(); + + let runtime = tauri::async_runtime::block_on(DesktopRuntime::initialize())?; + app.manage(runtime.clone()); + + // Background assistant completion notifications (SSE) independent of the webview lifecycle + let app_handle = app.app_handle(); + spawn_assistant_notifications(app_handle.clone(), runtime.clone()); + spawn_session_activity_tracker(app_handle.clone(), runtime.clone()); + + if let Some(window) = app.get_webview_window("main") { + #[cfg(target_os = "macos")] + { + if let Err(error) = + apply_vibrancy(&window, NSVisualEffectMaterial::Sidebar, None, Some(24.0)) + { + warn!("[desktop:vibrancy] Failed to apply macOS vibrancy: {}", error); + } else { + info!("[desktop:vibrancy] Applied macOS Sidebar vibrancy to main window"); + } + } + } + + app.manage(TerminalState::new()); + + let stored_state = tauri::async_runtime::block_on(load_window_state()).unwrap_or(None); + let manager = WindowStateManager::new(stored_state.clone().unwrap_or_default()); + app.manage(manager.clone()); + + if let Some(saved) = stored_state { + if let Some(window) = app.get_webview_window("main") { + let _ = window_state::apply_window_state(&window, &saved); + } + } + + // Restore bookmarks on startup (macOS security-scoped access) + // We'll do this synchronously within the setup to avoid lifetime issues + tauri::async_runtime::block_on(async { + if let Err(e) = + restore_bookmarks_on_startup(app.state::().clone()).await + { + warn!("Failed to restore bookmarks on startup: {}", e); + } + }); + + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + desktop_server_info, + desktop_restart_opencode, + desktop_open_devtools, + load_settings, + save_settings, + restart_opencode, + list_directory, + search_files, + create_directory, + request_directory_access, + start_accessing_directory, + stop_accessing_directory, + pick_directory, + restore_bookmarks_on_startup, + process_directory_selection, + check_is_git_repository, + get_git_status, + get_git_diff, + get_git_file_diff, + revert_git_file, + is_linked_worktree, + get_git_branches, + delete_git_branch, + delete_remote_branch, + list_git_worktrees, + add_git_worktree, + remove_git_worktree, + ensure_openchamber_ignored, + create_git_commit, + git_push, + git_pull, + git_fetch, + checkout_branch, + create_branch, + get_git_log, + get_commit_files, + get_git_identities, + create_git_identity, + update_git_identity, + delete_git_identity, + get_current_git_identity, + set_git_identity, + generate_commit_message, + create_terminal_session, + send_terminal_input, + resize_terminal, + close_terminal, + fetch_desktop_logs, + desktop_notify, + ]) + .on_window_event(|window, event| { + let window_state_manager = window.state::().inner().clone(); + + match event { + tauri::WindowEvent::Focused(true) => { + // Clear dock badge and underlying badge state when the window gains focus + let _ = window.set_badge_count(None); + let _ = window.app_handle().emit("openchamber:clear-badge-sessions", ()); + } + tauri::WindowEvent::Moved(position) => { + let is_maximized = window.is_maximized().unwrap_or(false); + window_state_manager.update_position( + position.x as f64, + position.y as f64, + is_maximized, + ); + } + tauri::WindowEvent::Resized(size) => { + let is_maximized = window.is_maximized().unwrap_or(false); + window_state_manager.update_size( + size.width as f64, + size.height as f64, + is_maximized, + ); + } + tauri::WindowEvent::CloseRequested { api, .. } => { + api.prevent_close(); + let runtime = window.state::().inner().clone(); + let window_handle = window.clone(); + let manager_clone = window_state_manager.clone(); + tauri::async_runtime::spawn(async move { + if let Err(err) = persist_window_state(&window_handle, &manager_clone).await + { + warn!("Failed to persist window state: {}", err); + } + runtime.shutdown().await; + let _ = window_handle.app_handle().exit(0); + }); + } + _ => {} + } + }) + .build(tauri::generate_context!()) + .expect("failed to build Tauri application"); + + app.run(|_app_handle, _event| {}); +} + + +fn spawn_http_server(port: u16, state: ServerState, shutdown_rx: broadcast::Receiver<()>) { + tauri::async_runtime::spawn(async move { + if let Err(error) = run_http_server(port, state, shutdown_rx).await { + error!("[desktop:http] server stopped: {error:?}"); + } + }); +} + +async fn run_http_server( + port: u16, + state: ServerState, + mut shutdown_rx: broadcast::Receiver<()>, +) -> Result<()> { + let router = Router::new() + .route("/health", get(health_handler)) + .route("/api/openchamber/models-metadata", get(models_metadata_handler)) + .route("/api/opencode/directory", post(change_directory_handler)) + .route("/api", any(proxy_to_opencode)) + .route("/api/{*rest}", any(proxy_to_opencode)) + .with_state(state) + .layer(CorsLayer::permissive()); + + let addr = format!("127.0.0.1:{port}"); + let listener = TcpListener::bind(&addr).await?; + info!("[desktop:http] listening on http://{addr}"); + + axum::serve(listener, router) + .with_graceful_shutdown(async move { + let _ = shutdown_rx.recv().await; + }) + .await?; + + Ok(()) +} + +async fn health_handler(State(state): State) -> Json { + Json(HealthResponse { + status: "ok", + server_port: state.server_port, + opencode_port: state.opencode.current_port(), + api_prefix: state.opencode.api_prefix(), + is_opencode_ready: state.opencode.is_ready(), + cli_available: opencode_manager::check_cli_exists(), + }) +} + +async fn models_metadata_handler(State(state): State) -> Result, StatusCode> { + let now = Instant::now(); + let cached_payload: Option = { + let cache = state.models_metadata_cache.lock().await; + if let (Some(payload), Some(fetched_at)) = (&cache.payload, cache.fetched_at) { + if now.duration_since(fetched_at) < MODELS_METADATA_CACHE_TTL { + return Ok(Json(payload.clone())); + } + } + cache.payload.clone() + }; + + let response = state + .client + .get(MODELS_DEV_API_URL) + .header(header::ACCEPT, "application/json") + .timeout(MODELS_METADATA_REQUEST_TIMEOUT) + .send() + .await + .map_err(|error| { + warn!("[desktop:http] Failed to fetch models metadata: {error}"); + StatusCode::BAD_GATEWAY + })?; + + if !response.status().is_success() { + warn!( + "[desktop:http] models.dev responded with status {}", + response.status() + ); + if let Some(payload) = cached_payload { + return Ok(Json(payload)); + } + return Err(StatusCode::BAD_GATEWAY); + } + + let payload = response.json::().await.map_err(|error| { + warn!("[desktop:http] Failed to parse models.dev payload: {error}"); + StatusCode::BAD_GATEWAY + })?; + + { + let mut cache = state.models_metadata_cache.lock().await; + cache.payload = Some(payload.clone()); + cache.fetched_at = Some(Instant::now()); + } + + Ok(Json(payload)) +} + +#[derive(Deserialize)] +struct DirectoryChangeRequest { + path: String, +} + +#[derive(Serialize)] +struct DirectoryChangeResponse { + success: bool, + restarted: bool, + path: String, +} + +fn json_response(status: StatusCode, payload: T) -> Response { + (status, Json(payload)).into_response() +} + +fn config_error_response(status: StatusCode, message: impl Into) -> Response { + json_response(status, ConfigErrorResponse { + error: message.into(), + }) +} + +async fn parse_request_payload(req: Request) -> Result, Response> { + let (_, body) = req.into_parts(); + let body_bytes = to_bytes(body, PROXY_BODY_LIMIT) + .await + .map_err(|_| config_error_response(StatusCode::BAD_REQUEST, "Invalid request body"))?; + + if body_bytes.is_empty() { + return Ok(HashMap::new()); + } + + serde_json::from_slice::>(&body_bytes) + .map_err(|_| config_error_response(StatusCode::BAD_REQUEST, "Malformed JSON payload")) +} + +async fn refresh_opencode_after_config_change( + state: &ServerState, + reason: &str, +) -> Result<(), Response> { + info!("[desktop:config] Restarting OpenCode after {}", reason); + state + .opencode + .restart() + .await + .map_err(|err| config_error_response( + StatusCode::INTERNAL_SERVER_ERROR, + format!("Failed to restart OpenCode: {}", err), + ))?; + Ok(()) +} + +async fn handle_agent_route( + state: &ServerState, + method: Method, + req: Request, + name: String, +) -> Result, StatusCode> { + match method { + Method::GET => { + match opencode_config::get_agent_sources(&name).await { + Ok(sources) => Ok(json_response( + StatusCode::OK, + ConfigMetadataResponse { + name, + is_built_in: !sources.md.exists && !sources.json.exists, + sources, + }, + )), + Err(err) => { + error!("[desktop:config] Failed to read agent sources: {}", err); + Ok(config_error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to read agent configuration", + )) + } + } + } + Method::POST => { + let payload = match parse_request_payload(req).await { + Ok(data) => data, + Err(resp) => return Ok(resp), + }; + + match opencode_config::create_agent(&name, &payload).await { + Ok(()) => { + if let Err(resp) = + refresh_opencode_after_config_change(state, "agent creation").await + { + return Ok(resp); + } + + Ok(json_response( + StatusCode::OK, + ConfigActionResponse { + success: true, + requires_reload: true, + message: format!( + "Agent {} created successfully. Reloading interface...", + name + ), + reload_delay_ms: CLIENT_RELOAD_DELAY_MS, + }, + )) + } + Err(err) => { + error!("[desktop:config] Failed to create agent {}: {}", name, err); + Ok(config_error_response( + StatusCode::INTERNAL_SERVER_ERROR, + err.to_string(), + )) + } + } + } + Method::PATCH => { + let payload = match parse_request_payload(req).await { + Ok(data) => data, + Err(resp) => return Ok(resp), + }; + + match opencode_config::update_agent(&name, &payload).await { + Ok(()) => { + if let Err(resp) = + refresh_opencode_after_config_change(state, "agent update").await + { + return Ok(resp); + } + + Ok(json_response( + StatusCode::OK, + ConfigActionResponse { + success: true, + requires_reload: true, + message: format!( + "Agent {} updated successfully. Reloading interface...", + name + ), + reload_delay_ms: CLIENT_RELOAD_DELAY_MS, + }, + )) + } + Err(err) => { + error!("[desktop:config] Failed to update agent {}: {}", name, err); + Ok(config_error_response( + StatusCode::INTERNAL_SERVER_ERROR, + err.to_string(), + )) + } + } + } + Method::DELETE => match opencode_config::delete_agent(&name).await { + Ok(()) => { + if let Err(resp) = + refresh_opencode_after_config_change(state, "agent deletion").await + { + return Ok(resp); + } + + Ok(json_response( + StatusCode::OK, + ConfigActionResponse { + success: true, + requires_reload: true, + message: format!( + "Agent {} deleted successfully. Reloading interface...", + name + ), + reload_delay_ms: CLIENT_RELOAD_DELAY_MS, + }, + )) + } + Err(err) => { + error!("[desktop:config] Failed to delete agent {}: {}", name, err); + Ok(config_error_response( + StatusCode::INTERNAL_SERVER_ERROR, + err.to_string(), + )) + } + }, + _ => Ok(StatusCode::METHOD_NOT_ALLOWED.into_response()), + } +} + +async fn handle_command_route( + state: &ServerState, + method: Method, + req: Request, + name: String, +) -> Result, StatusCode> { + match method { + Method::GET => { + match opencode_config::get_command_sources(&name).await { + Ok(sources) => Ok(json_response( + StatusCode::OK, + ConfigMetadataResponse { + name, + is_built_in: !sources.md.exists && !sources.json.exists, + sources, + }, + )), + Err(err) => { + error!("[desktop:config] Failed to read command sources: {}", err); + Ok(config_error_response( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to read command configuration", + )) + } + } + } + Method::POST => { + let payload = match parse_request_payload(req).await { + Ok(data) => data, + Err(resp) => return Ok(resp), + }; + + match opencode_config::create_command(&name, &payload).await { + Ok(()) => { + if let Err(resp) = + refresh_opencode_after_config_change(state, "command creation").await + { + return Ok(resp); + } + + Ok(json_response( + StatusCode::OK, + ConfigActionResponse { + success: true, + requires_reload: true, + message: format!( + "Command {} created successfully. Reloading interface...", + name + ), + reload_delay_ms: CLIENT_RELOAD_DELAY_MS, + }, + )) + } + Err(err) => { + error!("[desktop:config] Failed to create command {}: {}", name, err); + Ok(config_error_response( + StatusCode::INTERNAL_SERVER_ERROR, + err.to_string(), + )) + } + } + } + Method::PATCH => { + let payload = match parse_request_payload(req).await { + Ok(data) => data, + Err(resp) => return Ok(resp), + }; + + match opencode_config::update_command(&name, &payload).await { + Ok(()) => { + if let Err(resp) = + refresh_opencode_after_config_change(state, "command update").await + { + return Ok(resp); + } + + Ok(json_response( + StatusCode::OK, + ConfigActionResponse { + success: true, + requires_reload: true, + message: format!( + "Command {} updated successfully. Reloading interface...", + name + ), + reload_delay_ms: CLIENT_RELOAD_DELAY_MS, + }, + )) + } + Err(err) => { + error!("[desktop:config] Failed to update command {}: {}", name, err); + Ok(config_error_response( + StatusCode::INTERNAL_SERVER_ERROR, + err.to_string(), + )) + } + } + } + Method::DELETE => match opencode_config::delete_command(&name).await { + Ok(()) => { + if let Err(resp) = + refresh_opencode_after_config_change(state, "command deletion").await + { + return Ok(resp); + } + + Ok(json_response( + StatusCode::OK, + ConfigActionResponse { + success: true, + requires_reload: true, + message: format!( + "Command {} deleted successfully. Reloading interface...", + name + ), + reload_delay_ms: CLIENT_RELOAD_DELAY_MS, + }, + )) + } + Err(err) => { + error!("[desktop:config] Failed to delete command {}: {}", name, err); + let status = if err.to_string().contains("not found") { + StatusCode::NOT_FOUND + } else { + StatusCode::INTERNAL_SERVER_ERROR + }; + Ok(config_error_response(status, err.to_string())) + } + }, + _ => Ok(StatusCode::METHOD_NOT_ALLOWED.into_response()), + } +} + +async fn handle_config_routes( + state: ServerState, + path: &str, + method: Method, + req: Request, +) -> Result, StatusCode> { + if let Some(name) = path.strip_prefix("/api/config/agents/") { + let trimmed = name.trim(); + if trimmed.is_empty() { + return Ok(config_error_response( + StatusCode::BAD_REQUEST, + "Agent name is required", + )); + } + return handle_agent_route(&state, method, req, trimmed.to_string()).await; + } + + if let Some(name) = path.strip_prefix("/api/config/commands/") { + let trimmed = name.trim(); + if trimmed.is_empty() { + return Ok(config_error_response( + StatusCode::BAD_REQUEST, + "Command name is required", + )); + } + return handle_command_route(&state, method, req, trimmed.to_string()).await; + } + + if path == "/api/config/reload" && method == Method::POST { + if let Err(resp) = + refresh_opencode_after_config_change(&state, "manual configuration reload").await + { + return Ok(resp); + } + + return Ok(json_response( + StatusCode::OK, + ConfigActionResponse { + success: true, + requires_reload: true, + message: "Configuration reloaded successfully. Refreshing interface..." + .to_string(), + reload_delay_ms: CLIENT_RELOAD_DELAY_MS, + }, + )); + } + + Ok(StatusCode::NOT_FOUND.into_response()) +} + +async fn change_directory_handler( + State(state): State, + Json(payload): Json, +) -> Result, StatusCode> { + // Acquire lock to prevent concurrent directory changes + let _lock = state.directory_change_lock.lock().await; + + let requested_path = payload.path.trim(); + if requested_path.is_empty() { + warn!("[desktop:http] ERROR: Empty path provided"); + return Err(StatusCode::BAD_REQUEST); + } + + let resolved_path = PathBuf::from(requested_path); + + // Validate directory exists and is accessible + match fs::metadata(&resolved_path).await { + Ok(metadata) => { + if !metadata.is_dir() { + warn!( + "[desktop:http] ERROR: Path is not a directory: {:?}", + resolved_path + ); + return Err(StatusCode::BAD_REQUEST); + } + } + Err(err) => { + warn!( + "[desktop:http] ERROR: Cannot access path: {:?} - {}", + resolved_path, err + ); + return Err(StatusCode::NOT_FOUND); + } + } + + let current_dir = state.opencode.get_working_directory(); + let is_running = state.opencode.current_port().is_some(); + + // If already on this directory and OpenCode is running, no restart needed + if current_dir == resolved_path && is_running { + return Ok(Json(DirectoryChangeResponse { + success: true, + restarted: false, + path: resolved_path.to_string_lossy().to_string(), + })); + } + + info!("[desktop:http] Changing directory to {:?}", resolved_path); + + // Update working directory and restart OpenCode + state + .opencode + .set_working_directory(resolved_path.clone()) + .await + .map_err(|e| { + error!( + "[desktop:http] ERROR: Failed to set working directory: {}", + e + ); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + state.opencode.restart().await.map_err(|e| { + error!("[desktop:http] ERROR: Failed to restart OpenCode: {}", e); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + Ok(Json(DirectoryChangeResponse { + success: true, + restarted: true, + path: resolved_path.to_string_lossy().to_string(), + })) +} + +async fn proxy_to_opencode( + State(state): State, + original: OriginalUri, + req: Request, +) -> Result, StatusCode> { + let origin_path = original.0.path().to_string(); + let method = req.method().clone(); + + let is_desktop_config_route = origin_path.starts_with("/api/config/agents/") + || origin_path.starts_with("/api/config/commands/") + || origin_path == "/api/config/reload"; + + if is_desktop_config_route { + return handle_config_routes(state, &origin_path, method, req).await; + } + + let port = state.opencode.current_port().ok_or_else(|| { + error!("[desktop:http] PROXY FAILED: OpenCode not running (no port)"); + StatusCode::SERVICE_UNAVAILABLE + })?; + + let query = original.0.query(); + let rewritten_path = state.opencode.rewrite_path(&origin_path); + let mut target = format!("http://127.0.0.1:{port}{rewritten_path}"); + if let Some(q) = query { + target.push('?'); + target.push_str(q); + } + + let (parts, body) = req.into_parts(); + let method = parts.method.clone(); + let mut builder = state.client.request(method, &target); + + let mut headers = parts.headers; + headers.insert(header::HOST, format!("127.0.0.1:{port}").parse().unwrap()); + if headers + .get(header::ACCEPT) + .and_then(|v| v.to_str().ok()) + .map(|val| val.contains("text/event-stream")) + .unwrap_or(false) + { + headers.insert(header::CONNECTION, "keep-alive".parse().unwrap()); + } + + for (key, value) in headers.iter() { + if key == &header::CONTENT_LENGTH { + continue; + } + builder = builder.header(key, value); + } + + let body_bytes = to_bytes(body, PROXY_BODY_LIMIT) + .await + .map_err(|_| StatusCode::BAD_GATEWAY)?; + + let response = if body_bytes.is_empty() { + builder.send().await.map_err(|_| StatusCode::BAD_GATEWAY)? + } else { + builder + .body(ReqwestBody::from(body_bytes)) + .send() + .await + .map_err(|_| StatusCode::BAD_GATEWAY)? + }; + + let status = response.status(); + let mut resp_builder = Response::builder().status(status); + for (key, value) in response.headers() { + if key.as_str().eq_ignore_ascii_case("connection") { + continue; + } + resp_builder = resp_builder.header(key, value); + } + + let stream = response.bytes_stream().map(|chunk| { + chunk + .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err)) + .map(axum::body::Bytes::from) + }); + let body = Body::from_stream(stream); + resp_builder.body(body).map_err(|_| StatusCode::BAD_GATEWAY) +} + +#[derive(Clone)] +pub(crate) struct SettingsStore { + path: PathBuf, + guard: Arc>, +} + +impl SettingsStore { + pub(crate) fn new() -> Result { + // Use ~/.config/openchamber for consistency with Electron/web versions + let home = dirs::home_dir().ok_or_else(|| anyhow!("No home directory"))?; + let mut dir = home; + dir.push(".config"); + dir.push("openchamber"); + std::fs::create_dir_all(&dir).ok(); + dir.push("settings.json"); + Ok(Self { + path: dir, + guard: Arc::new(Mutex::new(())), + }) + } + + pub(crate) async fn load(&self) -> Result { + let _lock = self.guard.lock().await; + match fs::read(&self.path).await { + Ok(bytes) => { + let value = + serde_json::from_slice(&bytes).unwrap_or(Value::Object(Default::default())); + Ok(value) + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + Ok(Value::Object(Default::default())) + } + Err(err) => Err(err.into()), + } + } + + pub(crate) async fn save(&self, payload: Value) -> Result<()> { + let _lock = self.guard.lock().await; + if let Some(parent) = self.path.parent() { + fs::create_dir_all(parent).await.ok(); + } + let bytes = serde_json::to_vec_pretty(&payload)?; + fs::write(&self.path, bytes).await?; + Ok(()) + } + + pub(crate) async fn last_directory(&self) -> Result> { + let settings = self.load().await?; + let candidate = settings + .get("lastDirectory") + .and_then(|value| value.as_str()) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(PathBuf::from); + Ok(candidate) + } +} diff --git a/packages/desktop/src-tauri/src/opencode_config.rs b/packages/desktop/src-tauri/src/opencode_config.rs new file mode 100644 index 00000000..a3b3208b --- /dev/null +++ b/packages/desktop/src-tauri/src/opencode_config.rs @@ -0,0 +1,794 @@ +use anyhow::{anyhow, Result}; +use log::info; +use once_cell::sync::Lazy; +use regex::Regex; +use serde::Serialize; +use serde_json::{Map, Value}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use tokio::fs; + +static PROMPT_FILE_PATTERN: Lazy = + Lazy::new(|| Regex::new(r"(?i)^\{file:(.+)\}$").expect("valid regex")); + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SourceInfo { + pub exists: bool, + pub path: Option, + pub fields: Vec, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ConfigSources { + pub md: SourceInfo, + pub json: SourceInfo, +} + +/// Get OpenCode config directory path +fn get_config_dir() -> PathBuf { + dirs::home_dir() + .expect("Cannot determine home directory") + .join(".config") + .join("opencode") +} + +/// Get agent directory path +fn get_agent_dir() -> PathBuf { + get_config_dir().join("agent") +} + +/// Get command directory path +fn get_command_dir() -> PathBuf { + get_config_dir().join("command") +} + +/// Get config file path +fn get_config_file() -> PathBuf { + get_config_dir().join("opencode.json") +} + +/// Ensure required directories exist +async fn ensure_dirs() -> Result<()> { + let config_dir = get_config_dir(); + let agent_dir = get_agent_dir(); + let command_dir = get_command_dir(); + + fs::create_dir_all(&config_dir).await?; + fs::create_dir_all(&agent_dir).await?; + fs::create_dir_all(&command_dir).await?; + + Ok(()) +} + +/// Check if a value is a prompt file reference like {file:./prompts/agent.txt} +fn is_prompt_file_reference(value: &str) -> bool { + PROMPT_FILE_PATTERN.is_match(value.trim()) +} + +/// Resolve a prompt file reference to an absolute path +fn resolve_prompt_file_path(reference: &str) -> Option { + let trimmed = reference.trim(); + let captures = PROMPT_FILE_PATTERN.captures(trimmed)?; + let target = captures.get(1)?.as_str().trim(); + + if target.is_empty() { + return None; + } + + let path = if target.starts_with("./") { + get_config_dir().join(&target[2..]) + } else if Path::new(target).is_absolute() { + PathBuf::from(target) + } else { + get_config_dir().join(target) + }; + + Some(path) +} + +/// Write content to a prompt file +async fn write_prompt_file(file_path: &Path, content: &str) -> Result<()> { + if let Some(parent) = file_path.parent() { + fs::create_dir_all(parent).await?; + } + fs::write(file_path, content).await?; + info!("Updated prompt file: {}", file_path.display()); + Ok(()) +} + +/// Strip JSON comments from content +fn strip_json_comments(content: &str) -> String { + let mut result = String::new(); + let mut in_string = false; + let mut escape_next = false; + let mut chars = content.chars().peekable(); + + while let Some(ch) = chars.next() { + if escape_next { + result.push(ch); + escape_next = false; + continue; + } + + if ch == '\\' && in_string { + result.push(ch); + escape_next = true; + continue; + } + + if ch == '"' { + in_string = !in_string; + result.push(ch); + continue; + } + + if !in_string { + if ch == '/' { + if let Some(&next_ch) = chars.peek() { + if next_ch == '/' { + // Line comment - skip until end of line + chars.next(); // consume the second '/' + while let Some(c) = chars.next() { + if c == '\n' { + result.push('\n'); + break; + } + } + continue; + } else if next_ch == '*' { + // Block comment - skip until */ + chars.next(); // consume the '*' + let mut prev = ' '; + while let Some(c) = chars.next() { + if prev == '*' && c == '/' { + break; + } + prev = c; + } + continue; + } + } + } + } + + result.push(ch); + } + + result +} + +/// Read opencode.json configuration file +pub async fn read_config() -> Result { + let config_file = get_config_file(); + + if !config_file.exists() { + return Ok(Value::Object(serde_json::Map::new())); + } + + let content = fs::read_to_string(&config_file).await?; + let normalized = strip_json_comments(&content).trim().to_string(); + + if normalized.is_empty() { + return Ok(Value::Object(serde_json::Map::new())); + } + + serde_json::from_str(&normalized).map_err(|e| anyhow!("Failed to parse config: {}", e)) +} + +/// Write opencode.json configuration file with backup +pub async fn write_config(config: &Value) -> Result<()> { + let config_file = get_config_file(); + + // Create/overwrite single backup before writing + if config_file.exists() { + let file_name = config_file + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| anyhow!("Invalid config file name"))?; + + let backup_path = config_file.with_file_name(format!("{file_name}.openchamber.backup")); + fs::copy(&config_file, &backup_path).await?; + info!("Created config backup: {}", backup_path.display()); + } + + let json_string = serde_json::to_string_pretty(config)?; + fs::write(&config_file, json_string).await?; + info!("Successfully wrote config file"); + + Ok(()) +} + +/// Markdown file data +#[derive(Debug)] +struct MdData { + frontmatter: HashMap, + body: String, +} + +/// Parse markdown file with YAML frontmatter +async fn parse_md_file(file_path: &Path) -> Result { + let content = fs::read_to_string(file_path).await?; + + // Match YAML frontmatter: ---\n...\n---\n + let re = Regex::new(r"(?s)^---\r?\n(.*?)\r?\n---\r?\n(.*)$").expect("valid regex"); + + if let Some(captures) = re.captures(&content) { + let yaml_str = captures.get(1).map(|m| m.as_str()).unwrap_or(""); + let body = captures.get(2).map(|m| m.as_str()).unwrap_or("").trim(); + + let frontmatter: HashMap = + serde_yaml::from_str(yaml_str).unwrap_or_default(); + + Ok(MdData { + frontmatter, + body: body.to_string(), + }) + } else { + // No frontmatter, treat entire content as body + Ok(MdData { + frontmatter: HashMap::new(), + body: content.trim().to_string(), + }) + } +} + +/// Write markdown file with YAML frontmatter +async fn write_md_file( + file_path: &Path, + frontmatter: &HashMap, + body: &str, +) -> Result<()> { + let yaml_str = serde_yaml::to_string(frontmatter)?; + let content = format!("---\n{}---\n\n{}", yaml_str, body); + + fs::write(file_path, content).await?; + info!("Successfully wrote markdown file: {}", file_path.display()); + + Ok(()) +} + +/// Get information about where agent configuration is stored +pub async fn get_agent_sources(agent_name: &str) -> Result { + ensure_dirs().await?; + + let md_path = get_agent_dir().join(format!("{}.md", agent_name)); + let md_exists = md_path.exists(); + + let mut md_fields = Vec::new(); + if md_exists { + let md_data = parse_md_file(&md_path).await?; + md_fields.extend(md_data.frontmatter.keys().cloned()); + if !md_data.body.trim().is_empty() { + md_fields.push("prompt".to_string()); + } + } + + let config = read_config().await?; + let json_section = config + .get("agent") + .and_then(|v| v.as_object()) + .and_then(|obj| obj.get(agent_name)); + + let json_fields = json_section + .and_then(|value| value.as_object()) + .map(|obj| obj.keys().cloned().collect::>()) + .unwrap_or_default(); + + let sources = ConfigSources { + md: SourceInfo { + exists: md_exists, + path: md_exists.then(|| md_path.display().to_string()), + fields: md_fields, + }, + json: SourceInfo { + exists: json_section.is_some(), + path: Some(get_config_file().display().to_string()), + fields: json_fields, + }, + }; + + Ok(sources) +} + +/// Create new agent as .md file +pub async fn create_agent(agent_name: &str, config: &HashMap) -> Result<()> { + ensure_dirs().await?; + + let md_path = get_agent_dir().join(format!("{}.md", agent_name)); + + // Check if agent already exists + if md_path.exists() { + return Err(anyhow!("Agent {} already exists as .md file", agent_name)); + } + + let existing_config = read_config().await?; + if let Some(agents) = existing_config.get("agent").and_then(|v| v.as_object()) { + if agents.contains_key(agent_name) { + return Err(anyhow!( + "Agent {} already exists in opencode.json", + agent_name + )); + } + } + + // Extract prompt from config + let mut frontmatter = config.clone(); + let prompt = frontmatter + .remove("prompt") + .and_then(|v| v.as_str().map(|s| s.to_string())) + .unwrap_or_default(); + + // Write .md file + write_md_file(&md_path, &frontmatter, &prompt).await?; + info!("Created new agent: {}", agent_name); + + Ok(()) +} + +/// Update existing agent using field-level logic +pub async fn update_agent(agent_name: &str, updates: &HashMap) -> Result<()> { + ensure_dirs().await?; + + let md_path = get_agent_dir().join(format!("{}.md", agent_name)); + let md_exists = md_path.exists(); + + let mut md_data = if md_exists { + Some(parse_md_file(&md_path).await?) + } else { + None + }; + + let mut config = read_config().await?; + let mut existing_agent = config + .get("agent") + .and_then(|v| v.as_object()) + .and_then(|obj| obj.get(agent_name)) + .and_then(|v| v.as_object()) + .cloned() + .unwrap_or_else(Map::new); + let had_json_fields = !existing_agent.is_empty(); + + let mut md_modified = false; + let mut json_modified = false; + + for (field, value) in updates.iter() { + // Handle explicit removals (null payload) for scalar/frontmatter/JSON fields + if value.is_null() { + if md_exists { + if let Some(ref mut data) = md_data { + if data.frontmatter.remove(field).is_some() { + md_modified = true; + } + } + } + if existing_agent.remove(field).is_some() { + json_modified = true; + } + continue; + } + + // Special handling for prompt field + if field == "prompt" { + let normalized_value = value.as_str().unwrap_or("").to_string(); + + if md_exists { + if let Some(ref mut data) = md_data { + data.body = normalized_value.clone(); + md_modified = true; + } + } else if let Some(prompt_ref) = existing_agent.get("prompt").and_then(|v| v.as_str()) + { + if is_prompt_file_reference(prompt_ref) { + if let Some(prompt_file_path) = resolve_prompt_file_path(prompt_ref) { + write_prompt_file(&prompt_file_path, &normalized_value).await?; + } else { + return Err(anyhow!( + "Invalid prompt file reference for agent {}", + agent_name + )); + } + continue; + } + } + + // Write prompt directly to JSON entry (file ref or inline string) + existing_agent.insert("prompt".to_string(), Value::String(normalized_value)); + json_modified = true; + + continue; + } + + // Check where field is currently defined + let in_md = md_data + .as_ref() + .map(|data| data.frontmatter.contains_key(field)) + .unwrap_or(false); + let in_json = existing_agent.contains_key(field); + + if in_md { + // Update in .md frontmatter + if let Some(ref mut data) = md_data { + data.frontmatter.insert(field.clone(), value.clone()); + md_modified = true; + } + } else if in_json { + // Update in opencode.json while preserving existing fields + existing_agent.insert(field.clone(), value.clone()); + json_modified = true; + } else { + // Field not defined - apply priority rules + if md_exists && !existing_agent.is_empty() { + // Both exist → add to opencode.json (higher priority) without dropping other keys + existing_agent.insert(field.clone(), value.clone()); + json_modified = true; + } else if md_exists { + // Only .md exists → add to frontmatter + if let Some(ref mut data) = md_data { + data.frontmatter.insert(field.clone(), value.clone()); + md_modified = true; + } + } else { + // Only JSON or built-in → add/create section in opencode.json + existing_agent.insert(field.clone(), value.clone()); + json_modified = true; + } + } + } + + // Write changes + if md_modified { + if let Some(data) = md_data { + write_md_file(&md_path, &data.frontmatter, &data.body).await?; + } + } + + if json_modified { + // Avoid creating a new JSON section for agents that already live exclusively in .md + if md_exists && !had_json_fields { + json_modified = false; + } + } + + if json_modified { + if !config.is_object() { + config = Value::Object(Map::new()); + } + + let config_obj = config.as_object_mut().unwrap(); + let agents_entry = config_obj + .entry("agent".to_string()) + .or_insert_with(|| Value::Object(Map::new())); + + if !agents_entry.is_object() { + *agents_entry = Value::Object(Map::new()); + } + + let agents_obj = agents_entry.as_object_mut().unwrap(); + agents_obj.insert(agent_name.to_string(), Value::Object(existing_agent)); + + write_config(&config).await?; + } + + info!( + "Updated agent: {} (md: {}, json: {})", + agent_name, md_modified, json_modified + ); + + Ok(()) +} + +/// Delete agent configuration +pub async fn delete_agent(agent_name: &str) -> Result<()> { + let md_path = get_agent_dir().join(format!("{}.md", agent_name)); + let mut deleted = false; + + // 1. Delete .md file if exists + if md_path.exists() { + fs::remove_file(&md_path).await?; + info!("Deleted agent .md file: {}", md_path.display()); + deleted = true; + } + + // 2. Remove section from opencode.json if exists + let mut config = read_config().await?; + if let Some(agents) = config.get_mut("agent").and_then(|v| v.as_object_mut()) { + if agents.remove(agent_name).is_some() { + write_config(&config).await?; + info!("Removed agent from opencode.json: {}", agent_name); + deleted = true; + } + } + + // 3. If nothing was deleted (built-in agent), disable it + if !deleted { + if !config.is_object() { + config = Value::Object(serde_json::Map::new()); + } + let config_obj = config.as_object_mut().unwrap(); + if !config_obj.contains_key("agent") { + config_obj.insert("agent".to_string(), Value::Object(serde_json::Map::new())); + } + let agents = config_obj.get_mut("agent").unwrap(); + if !agents.is_object() { + *agents = Value::Object(serde_json::Map::new()); + } + let mut disable_obj = serde_json::Map::new(); + disable_obj.insert("disable".to_string(), Value::Bool(true)); + agents + .as_object_mut() + .unwrap() + .insert(agent_name.to_string(), Value::Object(disable_obj)); + write_config(&config).await?; + info!("Disabled built-in agent: {}", agent_name); + } + + Ok(()) +} + +/// Get information about where command configuration is stored +pub async fn get_command_sources(command_name: &str) -> Result { + ensure_dirs().await?; + + let md_path = get_command_dir().join(format!("{}.md", command_name)); + let md_exists = md_path.exists(); + + let mut md_fields = Vec::new(); + if md_exists { + let md_data = parse_md_file(&md_path).await?; + md_fields.extend(md_data.frontmatter.keys().cloned()); + if !md_data.body.trim().is_empty() { + md_fields.push("template".to_string()); + } + } + + let config = read_config().await?; + let json_section = config + .get("command") + .and_then(|v| v.as_object()) + .and_then(|obj| obj.get(command_name)); + + let json_fields = json_section + .and_then(|value| value.as_object()) + .map(|obj| obj.keys().cloned().collect::>()) + .unwrap_or_default(); + + let sources = ConfigSources { + md: SourceInfo { + exists: md_exists, + path: md_exists.then(|| md_path.display().to_string()), + fields: md_fields, + }, + json: SourceInfo { + exists: json_section.is_some(), + path: Some(get_config_file().display().to_string()), + fields: json_fields, + }, + }; + + Ok(sources) +} + +/// Create new command as .md file +pub async fn create_command(command_name: &str, config: &HashMap) -> Result<()> { + ensure_dirs().await?; + + let md_path = get_command_dir().join(format!("{}.md", command_name)); + + // Check if command already exists + if md_path.exists() { + return Err(anyhow!( + "Command {} already exists as .md file", + command_name + )); + } + + let existing_config = read_config().await?; + if let Some(commands) = existing_config.get("command").and_then(|v| v.as_object()) { + if commands.contains_key(command_name) { + return Err(anyhow!( + "Command {} already exists in opencode.json", + command_name + )); + } + } + + // Extract template from config + let mut frontmatter = config.clone(); + let template = frontmatter + .remove("template") + .and_then(|v| v.as_str().map(|s| s.to_string())) + .unwrap_or_default(); + + // Write .md file + write_md_file(&md_path, &frontmatter, &template).await?; + info!("Created new command: {}", command_name); + + Ok(()) +} + +/// Update existing command using field-level logic +pub async fn update_command( + command_name: &str, + updates: &HashMap, +) -> Result<()> { + ensure_dirs().await?; + + let md_path = get_command_dir().join(format!("{}.md", command_name)); + let md_exists = md_path.exists(); + + let mut md_data = if md_exists { + Some(parse_md_file(&md_path).await?) + } else { + None + }; + + let mut config = read_config().await?; + let mut existing_command = config + .get("command") + .and_then(|v| v.as_object()) + .and_then(|obj| obj.get(command_name)) + .and_then(|v| v.as_object()) + .cloned() + .unwrap_or_else(Map::new); + let had_json_fields = !existing_command.is_empty(); + + let mut md_modified = false; + let mut json_modified = false; + + for (field, value) in updates.iter() { + // Handle explicit removals (null payload) for scalar/frontmatter/JSON fields + if value.is_null() { + if md_exists { + if let Some(ref mut data) = md_data { + if data.frontmatter.remove(field).is_some() { + md_modified = true; + } + } + } + if existing_command.remove(field).is_some() { + json_modified = true; + } + continue; + } + + // Special handling for template field + if field == "template" { + let normalized_value = value.as_str().unwrap_or("").to_string(); + + if md_exists { + if let Some(ref mut data) = md_data { + data.body = normalized_value.clone(); + md_modified = true; + } + continue; + } else if let Some(template_ref) = existing_command.get("template").and_then(|v| v.as_str()) { + if is_prompt_file_reference(template_ref) { + if let Some(template_file_path) = resolve_prompt_file_path(template_ref) { + write_prompt_file(&template_file_path, &normalized_value).await?; + } else { + return Err(anyhow!( + "Invalid template file reference for command {}", + command_name + )); + } + continue; + } + } + + // Write template directly to JSON entry (file ref or inline string) + existing_command.insert("template".to_string(), Value::String(normalized_value)); + json_modified = true; + + continue; + } + + // Check where field is currently defined + let in_md = md_data + .as_ref() + .map(|data| data.frontmatter.contains_key(field)) + .unwrap_or(false); + let in_json = existing_command.contains_key(field); + + if in_md { + // Update in .md frontmatter + if let Some(ref mut data) = md_data { + data.frontmatter.insert(field.clone(), value.clone()); + md_modified = true; + } + } else if in_json { + // Update in opencode.json while preserving existing fields + existing_command.insert(field.clone(), value.clone()); + json_modified = true; + } else { + // Field not defined - apply priority rules + if md_exists && !existing_command.is_empty() { + // Both exist → add to opencode.json (higher priority) + existing_command.insert(field.clone(), value.clone()); + json_modified = true; + } else if md_exists { + // Only .md exists → add to frontmatter + if let Some(ref mut data) = md_data { + data.frontmatter.insert(field.clone(), value.clone()); + md_modified = true; + } + } else { + // Only JSON or built-in → add/create section in opencode.json + existing_command.insert(field.clone(), value.clone()); + json_modified = true; + } + } + } + + // Write changes + if md_modified { + if let Some(data) = md_data { + write_md_file(&md_path, &data.frontmatter, &data.body).await?; + } + } + + if json_modified { + // Avoid creating a new JSON section for commands that already live exclusively in .md + if md_exists && !had_json_fields { + json_modified = false; + } + } + + if json_modified { + if !config.is_object() { + config = Value::Object(Map::new()); + } + + let config_obj = config.as_object_mut().unwrap(); + let commands_entry = config_obj + .entry("command".to_string()) + .or_insert_with(|| Value::Object(Map::new())); + + if !commands_entry.is_object() { + *commands_entry = Value::Object(Map::new()); + } + + let commands_obj = commands_entry.as_object_mut().unwrap(); + commands_obj.insert(command_name.to_string(), Value::Object(existing_command)); + + write_config(&config).await?; + } + + info!( + "Updated command: {} (md: {}, json: {})", + command_name, md_modified, json_modified + ); + + Ok(()) +} + +/// Delete command configuration +pub async fn delete_command(command_name: &str) -> Result<()> { + let md_path = get_command_dir().join(format!("{}.md", command_name)); + let mut deleted = false; + + // 1. Delete .md file if exists + if md_path.exists() { + fs::remove_file(&md_path).await?; + info!("Deleted command .md file: {}", md_path.display()); + deleted = true; + } + + // 2. Remove section from opencode.json if exists + let mut config = read_config().await?; + if let Some(commands) = config.get_mut("command").and_then(|v| v.as_object_mut()) { + if commands.remove(command_name).is_some() { + write_config(&config).await?; + info!("Removed command from opencode.json: {}", command_name); + deleted = true; + } + } + + // 3. If nothing was deleted, throw error + if !deleted { + return Err(anyhow!("Command \"{}\" not found", command_name)); + } + + Ok(()) +} diff --git a/packages/desktop/src-tauri/src/opencode_manager.rs b/packages/desktop/src-tauri/src/opencode_manager.rs new file mode 100644 index 00000000..4f8f2240 --- /dev/null +++ b/packages/desktop/src-tauri/src/opencode_manager.rs @@ -0,0 +1,577 @@ +use anyhow::{anyhow, Result}; +use log::{debug, info, warn}; +use once_cell::sync::Lazy; +use parking_lot::RwLock; +use regex::Regex; +use reqwest::Client; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + time::Duration, +}; +use tokio::{ + io::{AsyncBufReadExt, BufReader}, + process::{Child, Command}, + sync::Mutex, + time::timeout, +}; + +static URL_REGEX: Lazy = Lazy::new(|| { + Regex::new(r#"https?://[^:\s]+:(?P\d+)(?P/[^\s"']*)?"#).expect("valid regex") +}); + +const FIRST_SIGNAL_TIMEOUT_MS: u64 = 750; +const READY_CHECK_TIMEOUT_MS: u64 = 20000; +const READY_CHECK_INTERVAL_MS: u64 = 400; + +#[derive(Clone)] +pub struct OpenCodeManager { + binary: Option, + args: Vec, + env: HashMap, + working_dir: Arc>, + desired_port: u16, + child: Arc>>, + port: Arc>>, + api_prefix: Arc>, + is_ready: Arc, + shutting_down: Arc, + http_client: Client, +} + +fn normalize_api_prefix(prefix: &str) -> String { + let trimmed = prefix.trim(); + if trimmed.is_empty() || trimmed == "/" { + return String::new(); + } + + let mut normalized = trimmed.trim_end_matches('/').to_string(); + if !normalized.starts_with('/') { + normalized.insert(0, '/'); + } + normalized +} + +impl OpenCodeManager { + pub fn new_with_directory(initial_dir: Option) -> Self { + let desired_port = std::env::var("OPENCHAMBER_OPENCODE_PORT") + .ok() + .and_then(|raw| raw.parse::().ok()) + .unwrap_or(0); + + let binary = resolve_opencode_binary(); + + if let Some(ref bin) = binary { + if !Path::new(bin).is_absolute() { + info!("[desktop:opencode] using PATH-resolved binary: {}", bin); + } else { + info!("[desktop:opencode] using binary: {}", bin); + } + } else { + warn!("[desktop:opencode] OpenCode CLI not found - app will run in limited mode"); + } + + let mut args = vec![ + "serve".to_string(), + "--port".to_string(), + desired_port.to_string(), + ]; + if let Ok(config) = std::env::var("OPENCHAMBER_OPENCODE_CONFIG") { + if !config.is_empty() { + args.push("--config".to_string()); + args.push(config); + } + } + + let env = build_augmented_env(); + let working_dir = initial_dir + .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))); + + info!( + "[desktop:opencode] Initial working directory: {:?}", + working_dir + ); + + Self { + binary, + args, + env, + working_dir: Arc::new(RwLock::new(working_dir)), + desired_port, + child: Arc::new(Mutex::new(None)), + port: Arc::new(RwLock::new(None)), + api_prefix: Arc::new(RwLock::new(String::new())), + is_ready: Arc::new(AtomicBool::new(false)), + shutting_down: Arc::new(AtomicBool::new(false)), + http_client: Client::builder() + .timeout(Duration::from_secs(5)) + .build() + .unwrap(), + } + } + + pub fn is_cli_available(&self) -> bool { + self.binary.is_some() + } + + pub async fn ensure_running(&self) -> Result<()> { + if self.binary.is_none() { + return Err(anyhow!("OpenCode CLI is not available")); + } + + let mut guard = self.child.lock().await; + if let Some(child) = guard.as_mut() { + if child.try_wait()?.is_none() && self.is_ready.load(Ordering::SeqCst) { + return Ok(()); + } + } + + self.is_ready.store(false, Ordering::SeqCst); + let child = self.spawn_process().await?; + *guard = Some(child); + drop(guard); + + // Wait for port detection from logs + if self.desired_port == 0 { + self.wait_for_port_detection().await?; + } + + // Detect API prefix early so proxy can forward correctly + let _ = self.detect_api_prefix().await; + + // Wait for OpenCode to become ready by polling endpoints + self.wait_for_ready().await?; + + self.is_ready.store(true, Ordering::SeqCst); + if let Some(port) = self.current_port() { + info!("[desktop:opencode] ready on port {port}"); + } + Ok(()) + } + + pub async fn restart(&self) -> Result<()> { + info!("[desktop:opencode] restarting..."); + self.is_ready.store(false, Ordering::SeqCst); + + self.graceful_stop().await?; + + // Brief delay to let OS release resources + tokio::time::sleep(Duration::from_millis(250)).await; + + // Reset state + if self.desired_port == 0 { + *self.port.write() = None; + } + *self.api_prefix.write() = String::new(); + + self.ensure_running().await + } + + pub async fn shutdown(&self) -> Result<()> { + self.shutting_down.store(true, Ordering::SeqCst); + self.is_ready.store(false, Ordering::SeqCst); + self.graceful_stop().await + } + + pub async fn set_working_directory(&self, new_dir: PathBuf) -> Result<()> { + *self.working_dir.write() = new_dir; + Ok(()) + } + + pub fn get_working_directory(&self) -> PathBuf { + self.working_dir.read().clone() + } + + async fn detect_api_prefix(&self) -> Result<()> { + let Some(port) = self.current_port() else { + return Err(anyhow!("Cannot detect API prefix without port")); + }; + + // Try empty prefix first (OpenCode default), then /api (some installations) + let candidates = ["", "/api"]; + for candidate in candidates { + let base = if candidate.is_empty() { + format!("http://127.0.0.1:{port}") + } else { + format!("http://127.0.0.1:{port}{candidate}") + }; + + let url = format!("{base}/config"); + match self.http_client.get(&url).send().await { + Ok(resp) if resp.status().is_success() => { + // Validate it's actually JSON config, not HTML + if let Ok(text) = resp.text().await { + if text.trim().starts_with('{') || text.trim().starts_with('[') { + info!("[desktop:opencode] Detected API prefix: {:?}", candidate); + *self.api_prefix.write() = normalize_api_prefix(candidate); + return Ok(()); + } + } + } + _ => continue, + } + } + + info!("[desktop:opencode] No API prefix detected, using empty prefix"); + *self.api_prefix.write() = String::new(); + Ok(()) + } + + pub fn current_port(&self) -> Option { + *self.port.read() + } + + pub fn api_prefix(&self) -> String { + self.api_prefix.read().clone() + } + + pub fn is_ready(&self) -> bool { + self.is_ready.load(Ordering::SeqCst) + } + + pub fn rewrite_path(&self, incoming_path: &str) -> String { + // Strip /api prefix to get OpenCode path + let result = incoming_path + .strip_prefix("/api") + .map(|rest| if rest.is_empty() { "/" } else { rest }) + .unwrap_or(incoming_path) + .to_string(); + + debug!( + "[opencode_manager] rewrite_path: '{}' -> '{}'", + incoming_path, result + ); + result + } + + async fn spawn_process(&self) -> Result { + let binary = self.binary.as_ref().ok_or_else(|| { + anyhow!("Cannot spawn process: OpenCode CLI is not available") + })?; + + info!( + "[desktop:opencode] launching {} {:?}", + binary, self.args + ); + + let working_dir = self.working_dir.read().clone(); + let mut cmd = Command::new(binary); + cmd.args(&self.args) + .current_dir(&working_dir) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::piped()) + .kill_on_drop(false); + + for (key, value) in &self.env { + cmd.env(key, value); + } + + let mut child = cmd.spawn().map_err(|e| { + if e.kind() == std::io::ErrorKind::NotFound { + anyhow!( + "OpenCode binary '{}' not found. Set OPENCODE_BINARY or ensure it's in PATH.", + binary + ) + } else { + anyhow!("Failed to spawn OpenCode: {}", e) + } + })?; + + // Set port immediately if pre-configured + if self.desired_port > 0 { + *self.port.write() = Some(self.desired_port); + } + + // Wait for first signal (stdout/stderr) within 750ms to confirm startup + let first_signal_received = Arc::new(AtomicBool::new(false)); + + if let Some(stdout) = child.stdout.take() { + let signal_flag = first_signal_received.clone(); + self.spawn_output_reader(stdout, "stdout", move || { + signal_flag.store(true, Ordering::SeqCst); + }); + } + + if let Some(stderr) = child.stderr.take() { + let signal_flag = first_signal_received.clone(); + self.spawn_output_reader(stderr, "stderr", move || { + signal_flag.store(true, Ordering::SeqCst); + }); + } + + // Wait for first signal or timeout + let start = std::time::Instant::now(); + while start.elapsed() < Duration::from_millis(FIRST_SIGNAL_TIMEOUT_MS) { + if first_signal_received.load(Ordering::SeqCst) { + break; + } + if let Ok(Some(_)) = child.try_wait() { + return Err(anyhow!("OpenCode process exited immediately after spawn")); + } + tokio::time::sleep(Duration::from_millis(50)).await; + } + + Ok(child) + } + + fn spawn_output_reader( + &self, + stream: impl tokio::io::AsyncRead + Unpin + Send + 'static, + label: &'static str, + on_first_line: F, + ) where + F: FnOnce() + Send + 'static, + { + let manager = self.clone(); + let first_line_flag = Arc::new(Mutex::new(Some(on_first_line))); + + tauri::async_runtime::spawn(async move { + let reader = BufReader::new(stream); + let mut lines = reader.lines(); + while let Ok(Some(line)) = lines.next_line().await { + // Trigger first signal callback + if let Some(callback) = first_line_flag.lock().await.take() { + callback(); + } + + debug!("[opencode:{label}] {line}"); + manager.ingest_output_line(&line); + } + }); + } + + fn ingest_output_line(&self, line: &str) { + if let Some(captures) = URL_REGEX.captures(line) { + if let Some(port_match) = captures + .name("port") + .and_then(|m| m.as_str().parse::().ok()) + { + *self.port.write() = Some(port_match); + } + + if let Some(path_match) = captures.name("path") { + let value = path_match.as_str(); + if !value.is_empty() && value != "/" { + *self.api_prefix.write() = value.to_string(); + } + } + } + } + + async fn wait_for_port_detection(&self) -> Result<()> { + let start = std::time::Instant::now(); + let timeout_duration = Duration::from_secs(15); + + while start.elapsed() < timeout_duration { + if self.current_port().is_some() { + return Ok(()); + } + tokio::time::sleep(Duration::from_millis(100)).await; + } + + Err(anyhow!("OpenCode did not report port within 15 seconds")) + } + + async fn wait_for_ready(&self) -> Result<()> { + let Some(port) = self.current_port() else { + return Err(anyhow!("Cannot check readiness without port")); + }; + + let deadline = tokio::time::Instant::now() + Duration::from_millis(READY_CHECK_TIMEOUT_MS); + let mut last_error: Option = None; + + while tokio::time::Instant::now() < deadline { + let api_prefix = self.api_prefix(); + + // Try /health, /config, /agent endpoints + match self.check_endpoints(port, &api_prefix).await { + Ok(()) => { + // Once ready, attempt to detect and persist the API prefix for proxying + let _ = self.detect_api_prefix().await; + return Ok(()); + } + Err(e) => { + last_error = Some(e.to_string()); + } + } + + tokio::time::sleep(Duration::from_millis(READY_CHECK_INTERVAL_MS)).await; + } + + Err(anyhow!( + "OpenCode not ready after {}ms: {}", + READY_CHECK_TIMEOUT_MS, + last_error.unwrap_or_else(|| "no error details".to_string()) + )) + } + + async fn check_endpoints(&self, port: u16, prefix: &str) -> Result<()> { + let base_url = format!("http://127.0.0.1:{port}{prefix}"); + + // Check /health + let health_url = format!("{base_url}/health"); + let health_resp = self.http_client.get(&health_url).send().await?; + if !health_resp.status().is_success() { + return Err(anyhow!("/health returned {}", health_resp.status())); + } + + // Check /config + let config_url = format!("{base_url}/config"); + let config_resp = self.http_client.get(&config_url).send().await?; + if !config_resp.status().is_success() { + return Err(anyhow!("/config returned {}", config_resp.status())); + } + + // Check /agent + let agent_url = format!("{base_url}/agent"); + let agent_resp = self.http_client.get(&agent_url).send().await?; + if !agent_resp.status().is_success() { + return Err(anyhow!("/agent returned {}", agent_resp.status())); + } + + Ok(()) + } + + async fn graceful_stop(&self) -> Result<()> { + let mut guard = self.child.lock().await; + let Some(mut child) = guard.take() else { + return Ok(()); + }; + + if child.try_wait()?.is_some() { + // Already exited + return Ok(()); + } + + // SIGTERM + #[cfg(unix)] + { + use nix::{ + sys::signal::{kill, Signal}, + unistd::Pid, + }; + if let Some(id) = child.id() { + let _ = kill(Pid::from_raw(id as i32), Signal::SIGTERM); + info!("[desktop:opencode] sent SIGTERM"); + } + } + #[cfg(windows)] + { + let _ = child.kill().await; + } + + // Wait 3 seconds for graceful exit + match timeout(Duration::from_secs(3), child.wait()).await { + Ok(_) => { + info!("[desktop:opencode] exited gracefully"); + return Ok(()); + } + Err(_) => { + warn!("[desktop:opencode] did not exit after SIGTERM, sending SIGKILL"); + } + } + + // SIGKILL + let _ = child.kill().await; + + // Wait up to 5 seconds for hard kill + match timeout(Duration::from_secs(5), child.wait()).await { + Ok(_) => { + info!("[desktop:opencode] exited after SIGKILL"); + } + Err(_) => { + warn!("[desktop:opencode] unresponsive after SIGKILL, continuing anyway"); + } + } + + Ok(()) + } +} + +/// Check if CLI binary exists (can be called dynamically for polling) +pub fn check_cli_exists() -> bool { + if std::env::var("OPENCHAMBER_DISABLE_CLI").is_ok() { + return false; + } + resolve_opencode_binary().is_some() +} + +fn resolve_opencode_binary() -> Option { + if std::env::var("OPENCHAMBER_DISABLE_CLI").is_ok() { + return None; + } + + // Check explicit override + if let Ok(value) = std::env::var("OPENCODE_BINARY") { + if !value.is_empty() && Path::new(&value).exists() { + info!("[desktop:opencode] using binary from OPENCODE_BINARY: {}", value); + return Some(value); + } + } + + // Find in PATH + if let Ok(output) = std::process::Command::new("which") + .arg("opencode") + .output() + { + if output.status.success() { + let path = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if !path.is_empty() { + info!("[desktop:opencode] found binary in PATH: {}", path); + return Some(path); + } + } + } + + warn!("[desktop:opencode] opencode binary not found in PATH"); + None +} + +fn build_augmented_env() -> HashMap { + let mut env: HashMap = std::env::vars().collect(); + if let Ok(login_path) = detect_login_shell_path() { + let current = env.get("PATH").cloned().unwrap_or_default(); + env.insert("PATH".to_string(), merge_paths(&login_path, ¤t)); + } + env +} + +fn merge_paths(login_path: &str, current: &str) -> String { + let mut segments = Vec::new(); + let mut seen = std::collections::HashSet::new(); + + for part in login_path.split(':').chain(current.split(':')) { + if part.is_empty() || seen.contains(part) { + continue; + } + seen.insert(part.to_string()); + segments.push(part); + } + + segments.join(":") +} + +fn detect_login_shell_path() -> Result { + #[cfg(not(unix))] + { + Err(anyhow!("login shell path unsupported")) + } + #[cfg(unix)] + { + use std::process::Command; + let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".into()); + let output = Command::new(&shell) + .arg("-lic") + .arg("echo -n $PATH") + .output()?; + if output.status.success() { + Ok(String::from_utf8_lossy(&output.stdout).to_string()) + } else { + Err(anyhow!("shell PATH detection failed")) + } + } +} diff --git a/packages/desktop/src-tauri/src/session_activity.rs b/packages/desktop/src-tauri/src/session_activity.rs new file mode 100644 index 00000000..dfedd9d0 --- /dev/null +++ b/packages/desktop/src-tauri/src/session_activity.rs @@ -0,0 +1,326 @@ +use std::{ + collections::HashMap, + sync::Arc, + time::Duration, +}; + +use anyhow::Result; +use futures_util::TryStreamExt; +use log::{debug, info, warn}; +use reqwest::Client; +use serde::Deserialize; +use serde_json::Value; +use tauri::{AppHandle, Emitter}; +use tokio::sync::Mutex; +use tokio_util::io::StreamReader; + +use crate::DesktopRuntime; + +#[derive(Deserialize)] +struct EventEnvelope { + #[serde(rename = "type")] + event_type: String, + #[serde(default)] + properties: Value, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum ActivityPhase { + Idle, + Busy, + Cooldown, +} + +pub fn spawn_session_activity_tracker( + app: AppHandle, + runtime: DesktopRuntime, +) -> tauri::async_runtime::JoinHandle<()> { + tauri::async_runtime::spawn(async move { + let client = Client::builder() + .timeout(Duration::from_secs(24 * 60 * 60)) + .tcp_keepalive(Some(Duration::from_secs(30))) + .build() + .expect("failed to build reqwest client"); + + let mut shutdown_rx = runtime.subscribe_shutdown(); + let phases = Arc::new(Mutex::new(HashMap::::new())); + let cooldowns = Arc::new(Mutex::new(HashMap::>::new())); + + loop { + tokio::select! { + _ = shutdown_rx.recv() => { + info!("[desktop:activity] Shutdown received, stopping SSE listener"); + break; + } + _ = async { + // Reset stale phases to idle before connecting so UI doesn't stay stuck on "working" after wake. + reset_and_emit_all_phases(&app, phases.clone(), cooldowns.clone()).await; + + if let Err(err) = run_once(&app, &runtime, &client, phases.clone(), cooldowns.clone()).await { + warn!("[desktop:activity] SSE loop error: {err:?}"); + } + tokio::time::sleep(Duration::from_secs(2)).await; + } => {} + } + } + }) +} + +async fn run_once( + app: &AppHandle, + runtime: &DesktopRuntime, + client: &Client, + phases: Arc>>, + cooldowns: Arc>>>, +) -> Result<()> { + let opencode = runtime.opencode_manager(); + + let port = match opencode.current_port() { + Some(port) => port, + None => { + warn!("[desktop:activity] OpenCode port unavailable; will retry"); + tokio::time::sleep(Duration::from_secs(2)).await; + return Ok(()); + } + }; + + let prefix = opencode.api_prefix(); + let mut url = format!("http://127.0.0.1:{port}{}/event", prefix); + + if let Some(dir) = opencode.get_working_directory().to_str().map(|s| s.to_string()) { + let mut parsed = reqwest::Url::parse(&url)?; + parsed + .query_pairs_mut() + .append_pair("directory", &dir); + url = parsed.to_string(); + } + + debug!("[desktop:activity] Connecting SSE for activity phases: {url}"); + + let response = client + .get(&url) + .header("accept", "text/event-stream") + .header("accept-encoding", "identity") + .send() + .await?; + + debug!( + "[desktop:activity] SSE response status={} headers={:?}", + response.status(), + response.headers() + ); + + if !response.status().is_success() { + warn!( + "[desktop:activity] SSE connect failed with status {}", + response.status() + ); + tokio::time::sleep(Duration::from_secs(2)).await; + return Ok(()); + } + + use tokio::io::AsyncBufReadExt; + + let stream = response + .bytes_stream() + .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err)); + let mut reader = StreamReader::new(stream); + let mut buf = Vec::new(); + let mut data_lines: Vec = Vec::new(); + + loop { + buf.clear(); + let bytes_read = match reader.read_until(b'\n', &mut buf).await { + Ok(n) => n, + Err(err) => { + warn!("[desktop:activity] Read error in SSE stream: {err:?}"); + return Err(err.into()); + } + }; + if bytes_read == 0 { + break; + } + + let line = match std::str::from_utf8(&buf) { + Ok(s) => s.trim_end_matches(&['\r', '\n'][..]).to_string(), + Err(err) => { + warn!("[desktop:activity] Non-UTF8 SSE chunk: {err}"); + continue; + } + }; + + if line.is_empty() { + if data_lines.is_empty() { + continue; + } + let raw = data_lines.join("\n"); + data_lines.clear(); + + match serde_json::from_str::(&raw) { + Ok(event) => handle_event(app, event, phases.clone(), cooldowns.clone()).await, + Err(err) => { + warn!("[desktop:activity] Failed to parse SSE data: {err}; raw={raw}"); + } + } + continue; + } + + if let Some(rest) = line.strip_prefix("data:") { + data_lines.push(rest.trim_start().to_string()); + } + } + + Ok(()) +} + +async fn handle_event( + app: &AppHandle, + event: EventEnvelope, + phases: Arc>>, + cooldowns: Arc>>>, +) { + match event.event_type.as_str() { + "session.status" => { + let session_id = event + .properties + .get("sessionID") + .and_then(Value::as_str) + .map(|s| s.to_string()); + let status = event + .properties + .get("status") + .and_then(|s| s.get("type")) + .and_then(Value::as_str); + + if let (Some(id), Some(status_type)) = (session_id, status) { + let phase = if status_type == "busy" || status_type == "retry" { + ActivityPhase::Busy + } else { + ActivityPhase::Idle + }; + set_phase(app, &id, phase, phases.clone(), cooldowns.clone()).await; + } + } + "message.updated" => { + if let Some(info) = event.properties.get("info") { + let role = info.get("role").and_then(Value::as_str).unwrap_or_default(); + if role != "assistant" { + return; + } + + let finish = info.get("finish").and_then(Value::as_str); + if finish != Some("stop") { + return; + } + + let session_id = info + .get("sessionID") + .and_then(Value::as_str) + .map(|s| s.to_string()); + + if let Some(id) = session_id { + // If current phase is busy, move to cooldown for 2s then idle + let current = { phases.lock().await.get(&id).cloned() }; + if matches!(current, Some(ActivityPhase::Busy)) { + set_phase(app, &id, ActivityPhase::Cooldown, phases.clone(), cooldowns.clone()).await; + + let app_clone = app.clone(); + let phases_clone = phases.clone(); + let cooldowns_clone = cooldowns.clone(); + let id_clone = id.clone(); + let handle = tauri::async_runtime::spawn(async move { + tokio::time::sleep(Duration::from_secs(2)).await; + let current = { phases_clone.lock().await.get(&id_clone).cloned() }; + if matches!(current, Some(ActivityPhase::Cooldown)) { + set_phase(&app_clone, &id_clone, ActivityPhase::Idle, phases_clone, cooldowns_clone).await; + } + }); + + // Store cooldown handle to cancel if phase changes earlier + let mut cd = cooldowns.lock().await; + if let Some(prev) = cd.remove(&id) { + prev.abort(); + } + cd.insert(id, handle); + } + } + } + } + _ => {} + } +} + +async fn set_phase( + app: &AppHandle, + session_id: &str, + phase: ActivityPhase, + phases: Arc>>, + cooldowns: Arc>>>, +) { + { + let mut map = phases.lock().await; + let current = map.get(session_id); + if current == Some(&phase) { + return; + } + map.insert(session_id.to_string(), phase.clone()); + + // Cancel cooldown timer when leaving cooldown + if !matches!(phase, ActivityPhase::Cooldown) { + if let Some(handle) = cooldowns.lock().await.remove(session_id) { + handle.abort(); + } + } + } + + // Emit to webview so UI stays in sync + let payload = serde_json::json!({ + "sessionId": session_id, + "phase": match phase { + ActivityPhase::Idle => "idle", + ActivityPhase::Busy => "busy", + ActivityPhase::Cooldown => "cooldown", + } + }); + + let _ = app.emit("openchamber:session-activity", payload); +} + +async fn reset_and_emit_all_phases( + app: &AppHandle, + phases: Arc>>, + cooldowns: Arc>>>, +) { + // Cancel any cooldown timers and set all phases to idle to avoid stale "busy" after wake. + { + let mut cd = cooldowns.lock().await; + for handle in cd.values() { + handle.abort(); + } + cd.clear(); + } + + let snapshot = { + let mut guard = phases.lock().await; + for value in guard.values_mut() { + *value = ActivityPhase::Idle; + } + guard.clone() + }; + + if snapshot.is_empty() { + return; + } + + for (session_id, phase) in snapshot { + let payload = serde_json::json!({ + "sessionId": session_id, + "phase": match phase { + ActivityPhase::Idle => "idle", + ActivityPhase::Busy => "busy", + ActivityPhase::Cooldown => "cooldown", + } + }); + let _ = app.emit("openchamber:session-activity", payload); + } +} diff --git a/packages/desktop/src-tauri/src/window_state.rs b/packages/desktop/src-tauri/src/window_state.rs new file mode 100644 index 00000000..5bd81745 --- /dev/null +++ b/packages/desktop/src-tauri/src/window_state.rs @@ -0,0 +1,167 @@ +use anyhow::{anyhow, Result}; +use serde::{Deserialize, Serialize}; +use std::{ + path::PathBuf, + sync::{Arc, Mutex}, +}; +use tauri::{LogicalPosition, LogicalSize, WebviewWindow, Window}; +use tokio::fs as async_fs; + +const WINDOW_STATE_FILE: &str = "window-state.json"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WindowState { + pub width: f64, + pub height: f64, + pub x: f64, + pub y: f64, + pub is_maximized: bool, +} + +impl Default for WindowState { + fn default() -> Self { + Self { + width: 1280.0, + height: 800.0, + x: 0.0, + y: 0.0, + is_maximized: false, + } + } +} + +#[derive(Serialize, Deserialize)] +struct WindowStateFile { + #[serde(rename = "windowState")] + pub window_state: WindowState, +} + +#[derive(Clone)] +pub struct WindowStateManager { + inner: Arc>, +} + +impl WindowStateManager { + pub fn new(initial: WindowState) -> Self { + Self { + inner: Arc::new(Mutex::new(initial)), + } + } + + pub fn snapshot(&self) -> WindowState { + self.inner.lock().expect("window state poisoned").clone() + } + + pub fn update_position(&self, x: f64, y: f64, is_maximized: bool) { + if is_maximized { + return; + } + if let Ok(mut state) = self.inner.lock() { + if !state.is_maximized { + state.x = x; + state.y = y; + } + } + } + + pub fn update_size(&self, width: f64, height: f64, is_maximized: bool) { + if let Ok(mut state) = self.inner.lock() { + if !is_maximized { + state.width = width; + state.height = height; + } + state.is_maximized = is_maximized; + } + } +} + +fn state_file_path() -> Result { + let mut path = dirs::home_dir().ok_or_else(|| anyhow!("No home directory"))?; + path.push(".config"); + path.push("openchamber"); + path.push(WINDOW_STATE_FILE); + Ok(path) +} + +pub async fn load_window_state() -> Result> { + let path = state_file_path()?; + match async_fs::read(&path).await { + Ok(bytes) => { + let file: WindowStateFile = serde_json::from_slice(&bytes)?; + Ok(Some(file.window_state)) + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(err) => Err(err.into()), + } +} + +pub async fn save_window_state(state: &WindowState) -> Result<()> { + let path = state_file_path()?; + if let Some(parent) = path.parent() { + async_fs::create_dir_all(parent).await?; + } + let payload = WindowStateFile { + window_state: state.clone(), + }; + let data = serde_json::to_vec_pretty(&payload)?; + async_fs::write(&path, data).await?; + Ok(()) +} + +pub fn apply_window_state(window: &WebviewWindow, state: &WindowState) -> Result<()> { + let mut normalized = state.clone(); + clamp_to_visible_region(window, &mut normalized); + + if normalized.width > 0.0 && normalized.height > 0.0 { + let _ = window.set_size(LogicalSize::new(normalized.width, normalized.height)); + } + let _ = window.set_position(LogicalPosition::new(normalized.x, normalized.y)); + if state.is_maximized { + let _ = window.maximize(); + } else { + let _ = window.unmaximize(); + } + Ok(()) +} + +pub async fn persist_window_state(window: &Window, manager: &WindowStateManager) -> Result<()> { + let mut snapshot = manager.snapshot(); + let is_maximized = window.is_maximized().unwrap_or(snapshot.is_maximized); + snapshot.is_maximized = is_maximized; + + if !is_maximized { + let scale_factor = window.scale_factor().unwrap_or(1.0); + if let Ok(size) = window.outer_size() { + let logical: LogicalSize = size.to_logical(scale_factor); + snapshot.width = logical.width.max(200.0); + snapshot.height = logical.height.max(200.0); + } + if let Ok(position) = window.outer_position() { + let logical: LogicalPosition = position.to_logical(scale_factor); + snapshot.x = logical.x; + snapshot.y = logical.y; + } + } + + save_window_state(&snapshot).await +} + +fn clamp_to_visible_region(window: &WebviewWindow, state: &mut WindowState) { + let monitor = match window.current_monitor() { + Ok(Some(monitor)) => monitor, + _ => return, + }; + let scale_factor = monitor.scale_factor(); + let monitor_size: LogicalSize = monitor.size().to_logical(scale_factor); + let monitor_position: LogicalPosition = monitor.position().to_logical(scale_factor); + + state.width = state.width.clamp(400.0, monitor_size.width); + state.height = state.height.clamp(300.0, monitor_size.height); + + let max_x = monitor_position.x + (monitor_size.width - state.width).max(0.0); + let max_y = monitor_position.y + (monitor_size.height - state.height).max(0.0); + + state.x = state.x.clamp(monitor_position.x, max_x); + state.y = state.y.clamp(monitor_position.y, max_y); +} diff --git a/packages/desktop/src-tauri/tauri.conf.json b/packages/desktop/src-tauri/tauri.conf.json new file mode 100644 index 00000000..efaf61d6 --- /dev/null +++ b/packages/desktop/src-tauri/tauri.conf.json @@ -0,0 +1,60 @@ +{ + "$schema": "../node_modules/@tauri-apps/cli/schema.json", + "productName": "OpenChamber", + "version": "1.0.0", + "identifier": "ai.opencode.openchamber", + "build": { + "beforeDevCommand": "pnpm dev", + "beforeBuildCommand": "pnpm build", + "devUrl": "http://127.0.0.1:1421", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "label": "main", + "title": "OpenChamber", + "transparent": true, + "width": 1280, + "height": 800, + "resizable": true, + "fullscreen": false, + "decorations": true, + "hiddenTitle": true, + "titleBarStyle": "Overlay", + "trafficLightPosition": { + "x": 17, + "y": 26 + }, + "visible": true, + "backgroundThrottling": "disabled" + } + ], + "security": { + "csp": null + }, + "macOSPrivateApi": true + }, + "bundle": { + "active": true, + "icon": [ + "icons/icon.icns", + "icons/icon.png" + ], + "macOS": { + "exceptionDomain": "localhost", + "minimumSystemVersion": "14.0", + "signingIdentity": null, + "infoPlist": "Info.plist" + }, + "createUpdaterArtifacts": true + }, + "plugins": { + "updater": { + "endpoints": [ + "https://github.com/btriapitsyn/openchamber/releases/latest/download/latest.json" + ], + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEU0NjI5NDJGNEU0QzFEMTYKUldRV0hVeE9MNVJpNUdRemdsbm8wQ2YxQkU4KzBOOEg3TkpXZzIzb244N3Y0R3I4N2FtUk1NMUEK" + } + } +} diff --git a/packages/desktop/src/api/diagnostics.ts b/packages/desktop/src/api/diagnostics.ts new file mode 100644 index 00000000..169df787 --- /dev/null +++ b/packages/desktop/src/api/diagnostics.ts @@ -0,0 +1,32 @@ + +import type { DiagnosticsAPI } from '@openchamber/ui/lib/api/types'; + +type LogResponse = { + fileName?: string; + content?: string; +}; + +const normalizePayload = (payload: LogResponse): { fileName: string; content: string } => ({ + fileName: typeof payload.fileName === 'string' && payload.fileName.trim().length > 0 ? payload.fileName : 'desktop.log', + content: typeof payload.content === 'string' ? payload.content : '', +}); + +export const createDesktopDiagnosticsAPI = (): DiagnosticsAPI => ({ + async downloadLogs() { + try { + const { safeInvoke } = await import('../lib/tauriCallbackManager'); + const result = await safeInvoke('fetch_desktop_logs', {}, { + timeout: 10000, + onCancel: () => { + console.warn('[DiagnosticsAPI] Fetch desktop logs operation timed out'); + } + }); + return normalizePayload(result ?? {}); + } catch (error) { + if (error instanceof Error) { + throw error; + } + throw new Error('Failed to download desktop logs'); + } + }, +}); diff --git a/packages/desktop/src/api/files.ts b/packages/desktop/src/api/files.ts new file mode 100644 index 00000000..27f3e11c --- /dev/null +++ b/packages/desktop/src/api/files.ts @@ -0,0 +1,114 @@ + +import { safeInvoke } from '../lib/tauriCallbackManager'; +import type { DirectoryListResult, FileSearchQuery, FileSearchResult, FilesAPI } from '@openchamber/ui/lib/api/types'; + +type ListDirectoryResponse = DirectoryListResult & { + path?: string; + entries: Array< + DirectoryListResult['entries'][number] & { + isFile?: boolean; + isSymbolicLink?: boolean; + } + >; +}; + +type SearchFilesResponse = { + root: string; + count: number; + files: Array<{ + name: string; + path: string; + relativePath: string; + extension?: string; + }>; +}; + +const normalizePath = (path: string): string => path.replace(/\\/g, '/'); + +const normalizeDirectoryPayload = (result: ListDirectoryResponse): DirectoryListResult => ({ + directory: normalizePath(result.directory || result.path || ''), + entries: Array.isArray(result.entries) + ? result.entries.map((entry) => ({ + name: entry.name || '', + path: normalizePath(entry.path || ''), + isDirectory: entry.isDirectory ?? false, + size: entry.size ?? 0, + modified: (entry as { modified?: string }).modified ?? new Date().toISOString(), + })) + : [], +}); + +export const createDesktopFilesAPI = (): FilesAPI => ({ + async listDirectory(path: string): Promise { + try { + const result = await safeInvoke('list_directory', { + path: normalizePath(path), + includeHidden: false + }, { + timeout: 10000, + onCancel: () => { + console.warn('[FilesAPI] List directory operation timed out'); + } + }); + + return normalizeDirectoryPayload(result); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(message || 'Failed to list directory'); + } + }, + + async search(payload: FileSearchQuery): Promise { + try { + const normalizedDirectory = + typeof payload.directory === 'string' && payload.directory.length > 0 + ? normalizePath(payload.directory) + : undefined; + + const result = await safeInvoke('search_files', { + directory: normalizedDirectory, + query: payload.query, + max_results: payload.maxResults || 100 + }, { + timeout: 15000, + onCancel: () => { + console.warn('[FilesAPI] Search files operation timed out'); + } + }); + + if (!result || !Array.isArray(result.files)) { + return []; + } + + return result.files.map((file) => ({ + path: normalizePath(file.path), + preview: file.relativePath ? [normalizePath(file.relativePath)] : undefined, + })); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(message || 'Failed to search files'); + } + }, + + async createDirectory(path: string): Promise<{ success: boolean; path: string }> { + try { + const normalizedPath = normalizePath(path); + const result = await safeInvoke<{ success: boolean; path: string }>('create_directory', { + path: normalizedPath + }, { + timeout: 5000, + onCancel: () => { + console.warn('[FilesAPI] Create directory operation timed out'); + } + }); + + return { + success: Boolean(result?.success), + path: result?.path ? normalizePath(result.path) : normalizedPath, + }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(message || 'Failed to create directory'); + } + }, +}); \ No newline at end of file diff --git a/packages/desktop/src/api/git.ts b/packages/desktop/src/api/git.ts new file mode 100644 index 00000000..2edb2c68 --- /dev/null +++ b/packages/desktop/src/api/git.ts @@ -0,0 +1,230 @@ + +import { safeInvoke } from '../lib/tauriCallbackManager'; +import type { + GitAPI, + GitStatus, + GitDiffResponse, + GetGitDiffOptions, + GitFileDiffResponse, + GitBranch, + GitDeleteBranchPayload, + GitDeleteRemoteBranchPayload, + GeneratedCommitMessage, + GitWorktreeInfo, + GitAddWorktreePayload, + GitRemoveWorktreePayload, + CreateGitCommitOptions, + GitCommitResult, + GitPushResult, + GitPullResult, + GitLogOptions, + GitLogResponse, + GitCommitFilesResponse, + GitIdentitySummary, + GitIdentityProfile +} from '@openchamber/ui/lib/api/types'; + +async function safeGitInvoke(command: string, args?: Record): Promise { + try { + return await safeInvoke(command, args, { + timeout: 120000, + onCancel: () => { + console.warn(`[GitAPI] Git operation ${command} did not complete within 120s; it may still be running.`); + } + }); + } catch (error) { + const message = typeof error === 'string' ? error : (error as Error).message || 'Unknown error'; + throw new Error(message); + } +} + +export const createDesktopGitAPI = (): GitAPI => ({ + async checkIsGitRepository(directory: string): Promise { + return safeGitInvoke('check_is_git_repository', { directory }); + }, + + async getGitStatus(directory: string): Promise { + return safeGitInvoke('get_git_status', { directory }); + }, + + async getGitDiff(directory: string, options: GetGitDiffOptions): Promise { + const diff = await safeGitInvoke('get_git_diff', { + directory, + pathStr: options.path, + staged: options.staged, + contextLines: options.contextLines + }); + return { diff }; + }, + + async getGitFileDiff(directory: string, options: { path: string }): Promise { + const [original, modified] = await safeGitInvoke<[string, string]>('get_git_file_diff', { + directory, + pathStr: options.path, + }); + return { + original: original ?? '', + modified: modified ?? '', + path: options.path, + }; + }, + + async revertGitFile(directory: string, filePath: string): Promise { + return safeGitInvoke('revert_git_file', { directory, filePath }); + }, + + async isLinkedWorktree(directory: string): Promise { + return safeGitInvoke('is_linked_worktree', { directory }); + }, + + async getGitBranches(directory: string): Promise { + return safeGitInvoke('get_git_branches', { directory }); + }, + + async deleteGitBranch(directory: string, payload: GitDeleteBranchPayload): Promise<{ success: boolean }> { + await safeGitInvoke('delete_git_branch', { + directory, + branch: payload.branch, + force: payload.force + }); + return { success: true }; + }, + + async deleteRemoteBranch(directory: string, payload: GitDeleteRemoteBranchPayload): Promise<{ success: boolean }> { + await safeGitInvoke('delete_remote_branch', { + directory, + branch: payload.branch, + remote: payload.remote + }); + return { success: true }; + }, + + async generateCommitMessage(directory: string, files: string[]): Promise<{ message: GeneratedCommitMessage }> { + const response = await safeGitInvoke<{ message: GeneratedCommitMessage }>('generate_commit_message', { + directory, + files + }); + return response; + }, + + async listGitWorktrees(directory: string): Promise { + return safeGitInvoke('list_git_worktrees', { directory }); + }, + + async addGitWorktree(directory: string, payload: GitAddWorktreePayload): Promise<{ success: boolean; path: string; branch: string }> { + await safeGitInvoke('add_git_worktree', { + directory, + pathStr: payload.path, + branch: payload.branch, + createBranch: payload.createBranch + }); + return { success: true, path: payload.path, branch: payload.branch }; + }, + + async removeGitWorktree(directory: string, payload: GitRemoveWorktreePayload): Promise<{ success: boolean }> { + await safeGitInvoke('remove_git_worktree', { + directory, + pathStr: payload.path, + force: payload.force + }); + return { success: true }; + }, + + async ensureOpenChamberIgnored(directory: string): Promise { + return safeGitInvoke('ensure_openchamber_ignored', { directory }); + }, + + async createGitCommit(directory: string, message: string, options?: CreateGitCommitOptions): Promise { + return safeGitInvoke('create_git_commit', { + directory, + message, + addAll: options?.addAll, + files: options?.files + }); + }, + + async gitPush(directory: string, options?: { remote?: string; branch?: string; options?: string[] | Record }): Promise { + return safeGitInvoke('git_push', { + directory, + remote: options?.remote, + branch: options?.branch, + options: options?.options + }); + }, + + async gitPull(directory: string, options?: { remote?: string; branch?: string }): Promise { + return safeGitInvoke('git_pull', { + directory, + remote: options?.remote, + branch: options?.branch + }); + }, + + async gitFetch(directory: string, options?: { remote?: string; branch?: string }): Promise<{ success: boolean }> { + await safeGitInvoke('git_fetch', { + directory, + remote: options?.remote + }); + return { success: true }; + }, + + async checkoutBranch(directory: string, branch: string): Promise<{ success: boolean; branch: string }> { + await safeGitInvoke('checkout_branch', { directory, branch }); + return { success: true, branch }; + }, + + async createBranch(directory: string, name: string, startPoint?: string): Promise<{ success: boolean; branch: string }> { + await safeGitInvoke('create_branch', { + directory, + name, + startPoint + }); + return { success: true, branch: name }; + }, + + async getGitLog(directory: string, options?: GitLogOptions): Promise { + return safeGitInvoke('get_git_log', { + directory, + maxCount: options?.maxCount, + from: options?.from, + to: options?.to, + file: options?.file + }); + }, + + async getCommitFiles(directory: string, hash: string): Promise { + return safeGitInvoke('get_commit_files', { + directory, + hash + }); + }, + + async getCurrentGitIdentity(directory: string): Promise { + try { + return await safeGitInvoke('get_current_git_identity', { directory }); + } catch { + return null; + } + }, + + async setGitIdentity(directory: string, profileId: string): Promise<{ success: boolean; profile: GitIdentityProfile }> { + const profile = await safeGitInvoke('set_git_identity', { directory, profileId }); + return { success: true, profile }; + }, + + async getGitIdentities(): Promise { + return safeGitInvoke('get_git_identities'); + }, + + async createGitIdentity(profile: GitIdentityProfile): Promise { + return safeGitInvoke('create_git_identity', { profile }); + }, + + async updateGitIdentity(id: string, updates: GitIdentityProfile): Promise { + return safeGitInvoke('update_git_identity', { id, updates }); + }, + + async deleteGitIdentity(id: string): Promise { + return safeGitInvoke('delete_git_identity', { id }); + }, +}); diff --git a/packages/desktop/src/api/index.ts b/packages/desktop/src/api/index.ts new file mode 100644 index 00000000..949c3110 --- /dev/null +++ b/packages/desktop/src/api/index.ts @@ -0,0 +1,57 @@ +import type { RuntimeAPIs, TerminalHandlers } from '@openchamber/ui/lib/api/types'; +import { createDesktopTerminalAPI } from './terminal'; +import { createDesktopGitAPI } from './git'; +import { createDesktopFilesAPI } from './files'; +import { createDesktopSettingsAPI } from './settings'; +import { createDesktopPermissionsAPI } from './permissions'; +import { createDesktopDiagnosticsAPI } from './diagnostics'; +import { createDesktopNotificationsAPI } from './notifications'; +import { createDesktopToolsAPI } from './tools'; + +const activeTerminalConnections = new Set(); + +export const createDesktopAPIs = (): RuntimeAPIs & { cleanup?: () => void } => { + const terminalAPI = createDesktopTerminalAPI(); + const originalConnect = terminalAPI.connect.bind(terminalAPI); + + const wrappedTerminalAPI = { + ...terminalAPI, + connect: (sessionId: string, handlers: TerminalHandlers) => { + activeTerminalConnections.add(sessionId); + const connection = originalConnect(sessionId, handlers); + + const originalClose = connection.close; + return { + ...connection, + close: () => { + activeTerminalConnections.delete(sessionId); + originalClose(); + }, + }; + }, + }; + + return { + runtime: { platform: 'desktop', isDesktop: true, label: 'tauri-bootstrap' }, + terminal: wrappedTerminalAPI, + git: createDesktopGitAPI(), + files: createDesktopFilesAPI(), + settings: createDesktopSettingsAPI(), + permissions: createDesktopPermissionsAPI(), + notifications: createDesktopNotificationsAPI(), + diagnostics: createDesktopDiagnosticsAPI(), + tools: createDesktopToolsAPI(), + cleanup: () => { + console.info('[DesktopAPIs] Performing cleanup...'); + + const activeConnections = Array.from(activeTerminalConnections); + activeConnections.forEach(sessionId => { + console.info(`[DesktopAPIs] Closing terminal session: ${sessionId}`); + + activeTerminalConnections.delete(sessionId); + }); + + console.info(`[DesktopAPIs] Cleanup completed, closed ${activeConnections.length} terminal connections`); + }, + }; +}; diff --git a/packages/desktop/src/api/notifications.ts b/packages/desktop/src/api/notifications.ts new file mode 100644 index 00000000..71f6f0b6 --- /dev/null +++ b/packages/desktop/src/api/notifications.ts @@ -0,0 +1,58 @@ + +import type { NotificationsAPI, NotificationPayload } from '@openchamber/ui/lib/api/types'; +import { isPermissionGranted, requestPermission } from '@tauri-apps/plugin-notification'; +import { safeInvoke } from '../lib/tauriCallbackManager'; + +export const requestInitialNotificationPermission = async (): Promise => { + try { + if (typeof window !== 'undefined' && 'Notification' in window) { + const permission = await Notification.requestPermission(); + if (permission !== 'granted') { + console.warn('[notifications] Notification permission not granted'); + } + } + } catch (error) { + console.error('[notifications] Failed to request permission:', error); + } +}; + +export const createDesktopNotificationsAPI = (): NotificationsAPI => ({ + async notifyAgentCompletion(payload?: NotificationPayload): Promise { + try { + let granted = await isPermissionGranted(); + if (!granted) { + const permission = await requestPermission(); + granted = permission === 'granted'; + } + + if (!granted) { + console.warn('[notifications] Cannot send notification: Permission denied'); + return false; + } + + await safeInvoke( + 'desktop_notify', + { payload }, + { + timeout: 5000, + onCancel: () => { + console.warn('[NotificationsAPI] Notify operation timed out'); + }, + }, + ); + return true; + } catch (error) { + console.error('[notifications] Failed to send notification:', error); + return false; + } + }, + + async canNotify(): Promise { + try { + return await isPermissionGranted(); + } catch (error) { + console.warn('[notifications] Failed to check notification permission:', error); + return false; + } + } +}); diff --git a/packages/desktop/src/api/permissions.ts b/packages/desktop/src/api/permissions.ts new file mode 100644 index 00000000..32e874c7 --- /dev/null +++ b/packages/desktop/src/api/permissions.ts @@ -0,0 +1,49 @@ +import type { DirectoryPermissionRequest, DirectoryPermissionResult, PermissionsAPI, StartAccessingResult } from '@openchamber/ui/lib/api/types'; + +export const createDesktopPermissionsAPI = (): PermissionsAPI => ({ + async requestDirectoryAccess(request: DirectoryPermissionRequest): Promise { + try { + const { safeInvoke } = await import('../lib/tauriCallbackManager'); + const result = await safeInvoke('request_directory_access', { request }, { + timeout: 30000, + onCancel: () => { + console.warn('[PermissionsAPI] Request directory access operation timed out'); + } + }); + return result; + } catch (error) { + console.error('[desktop] Error requesting directory access:', error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + }, + async startAccessingDirectory(path: string): Promise { + try { + const { safeInvoke } = await import('../lib/tauriCallbackManager'); + const result = await safeInvoke('start_accessing_directory', { path }, { + timeout: 10000, + onCancel: () => { + console.warn('[PermissionsAPI] Start accessing directory operation timed out'); + } + }); + return result; + } catch (error) { + console.error('[desktop] Error starting directory access:', error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + }, + async stopAccessingDirectory(path: string): Promise { + try { + const { safeInvoke } = await import('../lib/tauriCallbackManager'); + const result = await safeInvoke('stop_accessing_directory', { path }, { + timeout: 5000, + onCancel: () => { + console.warn('[PermissionsAPI] Stop accessing directory operation timed out'); + } + }); + return result; + } catch (error) { + console.error('[desktop] Error stopping directory access:', error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + }, +}); diff --git a/packages/desktop/src/api/settings.ts b/packages/desktop/src/api/settings.ts new file mode 100644 index 00000000..69049d2d --- /dev/null +++ b/packages/desktop/src/api/settings.ts @@ -0,0 +1,58 @@ +import type { SettingsAPI, SettingsLoadResult, SettingsPayload } from '@openchamber/ui/lib/api/types'; + +const sanitizePayload = (data: unknown): SettingsPayload => { + if (!data || typeof data !== 'object') { + return {}; + } + return data as SettingsPayload; +}; + +export const createDesktopSettingsAPI = (): SettingsAPI => ({ + async load(): Promise { + try { + const { safeInvoke } = await import('../lib/tauriCallbackManager'); + const result = await safeInvoke<{ settings: unknown; source: 'desktop' | 'web' }>('load_settings', {}, { + timeout: 5000, + onCancel: () => { + console.warn('[SettingsAPI] Load settings operation timed out'); + } + }); + return { + settings: sanitizePayload(result.settings), + source: result.source, + }; + } catch (error) { + throw new Error(`Failed to load settings: ${error instanceof Error ? error.message : String(error)}`); + } + }, + + async save(changes: Partial): Promise { + try { + const { safeInvoke } = await import('../lib/tauriCallbackManager'); + const result = await safeInvoke('save_settings', { changes }, { + timeout: 5000, + onCancel: () => { + console.warn('[SettingsAPI] Save settings operation timed out'); + } + }); + return sanitizePayload(result); + } catch (error) { + throw new Error(`Failed to save settings: ${error instanceof Error ? error.message : String(error)}`); + } + }, + + async restartOpenCode(): Promise<{ restarted: boolean }> { + try { + const { safeInvoke } = await import('../lib/tauriCallbackManager'); + const result = await safeInvoke<{ restarted: boolean }>('restart_opencode', {}, { + timeout: 10000, + onCancel: () => { + console.warn('[SettingsAPI] Restart OpenCode operation timed out'); + } + }); + return { restarted: result.restarted }; + } catch (error) { + throw new Error(`Failed to restart OpenCode: ${error instanceof Error ? error.message : String(error)}`); + } + }, +}); diff --git a/packages/desktop/src/api/terminal.ts b/packages/desktop/src/api/terminal.ts new file mode 100644 index 00000000..2a8b960c --- /dev/null +++ b/packages/desktop/src/api/terminal.ts @@ -0,0 +1,123 @@ +import { safeInvoke, safeListen } from '../lib/tauriCallbackManager'; +import type { + TerminalAPI, + TerminalHandlers, + CreateTerminalOptions, + ResizeTerminalPayload, + TerminalSession, + TerminalStreamEvent +} from '@openchamber/ui/lib/api/types'; + +async function safeTerminalInvoke(command: string, args?: Record): Promise { + try { + return await safeInvoke(command, args, { + timeout: 10000, + onCancel: () => { + console.warn(`[TerminalAPI] Command ${command} timed out`); + } + }); + } catch (error) { + const message = typeof error === 'string' ? error : (error as Error).message || 'Unknown error'; + throw new Error(message); + } +} + +export const createDesktopTerminalAPI = (): TerminalAPI => ({ + async createSession(options: CreateTerminalOptions): Promise { + const cols = options.cols ?? 80; + const rows = options.rows ?? 24; + + const res = await safeTerminalInvoke<{ session_id: string }>('create_terminal_session', { + payload: { + cols, + rows, + cwd: options.cwd + } + }); + + return { + sessionId: res.session_id, + cols, + rows + }; + }, + + connect(sessionId: string, handlers: TerminalHandlers) { + let unlistenFn: (() => void) | undefined; + let cancelled = false; + let isConnected = false; + + const stopListening = () => { + if (unlistenFn) { + unlistenFn(); + unlistenFn = undefined; + isConnected = false; + } + }; + + const startListening = async () => { + try { + const unlisten = await safeListen(`terminal://${sessionId}`, (event) => { + if (cancelled) { + return; + } + + handlers.onEvent(event.payload); + + if (event.payload?.type === 'exit') { + stopListening(); + } + }); + + if (cancelled) { + unlisten(); + return; + } + + unlistenFn = unlisten; + isConnected = true; + handlers.onEvent({ type: 'connected' }); + } catch (err) { + console.error('Failed to listen to terminal events:', err); + if (!cancelled) { + handlers.onError?.(err instanceof Error ? err : new Error(String(err))); + } + } + }; + + startListening(); + + return { + close: () => { + cancelled = true; + stopListening(); + }, + isConnected: () => isConnected, + }; + }, + + async sendInput(sessionId: string, input: string): Promise { + await safeTerminalInvoke('send_terminal_input', { + + sessionId, + session_id: sessionId, + data: input, + }); + }, + + async resize(payload: ResizeTerminalPayload): Promise { + await safeTerminalInvoke('resize_terminal', { + sessionId: payload.sessionId, + session_id: payload.sessionId, + cols: payload.cols, + rows: payload.rows, + }); + }, + + async close(sessionId: string): Promise { + await safeTerminalInvoke('close_terminal', { + sessionId, + session_id: sessionId, + }); + }, +}); diff --git a/packages/desktop/src/api/tools.ts b/packages/desktop/src/api/tools.ts new file mode 100644 index 00000000..97f373a6 --- /dev/null +++ b/packages/desktop/src/api/tools.ts @@ -0,0 +1,22 @@ +import type { ToolsAPI } from '@openchamber/ui/lib/api/types'; + +export const createDesktopToolsAPI = (): ToolsAPI => ({ + async getAvailableTools(): Promise { + + const response = await fetch('/api/experimental/tool/ids'); + + if (!response.ok) { + throw new Error(`Tools API returned ${response.status} ${response.statusText}`); + } + + const data = await response.json(); + + if (!Array.isArray(data)) { + throw new Error('Tools API returned invalid data format'); + } + + return data + .filter((tool: unknown): tool is string => typeof tool === 'string' && tool !== 'invalid') + .sort(); + }, +}); diff --git a/packages/desktop/src/api/updater.ts b/packages/desktop/src/api/updater.ts new file mode 100644 index 00000000..1dc99bdc --- /dev/null +++ b/packages/desktop/src/api/updater.ts @@ -0,0 +1,105 @@ +export interface UpdateInfo { + available: boolean; + version?: string; + currentVersion: string; + body?: string; + date?: string; +} + +export interface UpdateProgress { + downloaded: number; + total?: number; +} + +interface Update { + version: string; + body?: string; + date?: string; + downloadAndInstall: ( + onEvent?: (event: DownloadEvent) => void + ) => Promise; +} + +type DownloadEvent = + | { event: 'Started'; data: { contentLength?: number } } + | { event: 'Progress'; data: { chunkLength: number } } + | { event: 'Finished' }; + +let cachedUpdate: Update | null = null; + +export async function checkForUpdates(): Promise { + try { + const { check } = await import('@tauri-apps/plugin-updater'); + const update = await check(); + cachedUpdate = update; + + if (!update) { + return { + available: false, + currentVersion: await getCurrentVersion(), + }; + } + + return { + available: true, + version: update.version, + currentVersion: await getCurrentVersion(), + body: update.body ?? undefined, + date: update.date ?? undefined, + }; + } catch (error) { + console.error('[updater] Failed to check for updates:', error); + return { + available: false, + currentVersion: await getCurrentVersion(), + }; + } +} + +export async function downloadUpdate( + onProgress?: (progress: UpdateProgress) => void +): Promise { + let update = cachedUpdate; + if (!update) { + const { check } = await import('@tauri-apps/plugin-updater'); + const checked = await check(); + if (!checked) { + throw new Error('No update available'); + } + update = checked; + cachedUpdate = checked; + } + + let downloaded = 0; + let total: number | undefined; + + await update.downloadAndInstall((event: DownloadEvent) => { + switch (event.event) { + case 'Started': + total = event.data.contentLength; + onProgress?.({ downloaded: 0, total }); + break; + case 'Progress': + downloaded += event.data.chunkLength; + onProgress?.({ downloaded, total }); + break; + case 'Finished': + onProgress?.({ downloaded: total ?? downloaded, total }); + break; + } + }); +} + +export async function restartToUpdate(): Promise { + const { relaunch } = await import('@tauri-apps/plugin-process'); + await relaunch(); +} + +async function getCurrentVersion(): Promise { + try { + const { getVersion } = await import('@tauri-apps/api/app'); + return await getVersion(); + } catch { + return 'unknown'; + } +} diff --git a/packages/desktop/src/lib/bridge.ts b/packages/desktop/src/lib/bridge.ts new file mode 100644 index 00000000..56cb8231 --- /dev/null +++ b/packages/desktop/src/lib/bridge.ts @@ -0,0 +1,157 @@ + +import { safeInvoke, cleanupAllTauriCallbacks } from './tauriCallbackManager'; + +type ServerInfo = { + server_port: number; + opencode_port?: number | null; + api_prefix?: string | null; + cli_available?: boolean; +}; + +declare global { + interface Window { + __OPENCHAMBER_DESKTOP_SERVER__?: { + origin: string; + opencodePort: number | null; + apiPrefix: string; + cliAvailable: boolean; + }; + } +} + +let bridgePromise: Promise | null = null; + +export function initializeDesktopBridge(): Promise { + if (!bridgePromise) { + bridgePromise = setupBridge(); + } + return bridgePromise; +} + +async function setupBridge(): Promise { + try { + const info = await safeInvoke('desktop_server_info', {}, { + timeout: 10000, + onCancel: () => { + console.warn('[Bridge] Server info request timed out'); + } + }); + const origin = `http://127.0.0.1:${info.server_port}`; + + window.__OPENCHAMBER_DESKTOP_SERVER__ = { + origin, + opencodePort: info.opencode_port ?? null, + apiPrefix: info.api_prefix ?? '', + cliAvailable: info.cli_available ?? false, + }; + + patchFetch(origin); + patchEventSource(origin); + + const cleanupDevtools = registerDevtoolsShortcut(); + + if (typeof window !== 'undefined') { + (window as { __openchamberCleanup?: () => void }).__openchamberCleanup = () => { + cleanupDevtools(); + }; + } + } catch (error) { + console.error('[bridge] Failed to initialize bridge:', error); + + if (typeof window !== 'undefined' && (window as { __openchamberCleanup?: () => void }).__openchamberCleanup) { + try { + (window as { __openchamberCleanup?: () => void }).__openchamberCleanup?.(); + } catch (cleanupError) { + console.warn('[bridge] Cleanup during failed initialization failed:', cleanupError); + } + delete (window as { __openchamberCleanup?: () => void }).__openchamberCleanup; + } + + cleanupAllTauriCallbacks(); + + throw error; + } +} + +function patchFetch(origin: string) { + const originalFetch = window.fetch.bind(window); + + const rewrite = (value: string): string => { + if (value.startsWith('http://') || value.startsWith('https://')) { + return value; + } + if (value.startsWith('//')) { + return `http:${value}`; + } + if (value.startsWith('/')) { + return `${origin}${value}`; + } + return value; + }; + + window.fetch = (input: RequestInfo | URL, init?: RequestInit) => { + if (typeof input === 'string') { + return originalFetch(rewrite(input), init); + } + + if (input instanceof Request) { + const rewritten = rewrite(input.url); + if (rewritten === input.url) { + return originalFetch(input, init); + } + const cloned = new Request(rewritten, input); + return originalFetch(cloned, init); + } + + if (input instanceof URL) { + return originalFetch(rewrite(input.toString()), init); + } + + return originalFetch(input, init); + }; +} + +function patchEventSource(origin: string) { + if (typeof window.EventSource === 'undefined') { + return; + } + + const OriginalEventSource = window.EventSource; + + class DesktopEventSource extends OriginalEventSource { + constructor(url: string | URL, eventSourceInit?: EventSourceInit) { + const normalized = typeof url === 'string' ? url : url.toString(); + super(normalized.startsWith('/') ? `${origin}${normalized}` : normalized, eventSourceInit); + } + } + + Object.defineProperty(DesktopEventSource, 'name', { value: 'DesktopEventSource' }); + Object.setPrototypeOf(DesktopEventSource.prototype, OriginalEventSource.prototype); + Object.setPrototypeOf(DesktopEventSource, OriginalEventSource); + + window.EventSource = DesktopEventSource as unknown as typeof EventSource; +} + +function registerDevtoolsShortcut() { + const handler = (event: KeyboardEvent) => { + const key = event.key?.toLowerCase(); + if ((event.metaKey || event.ctrlKey) && event.altKey && key === 'i') { + event.preventDefault(); + + const devtoolsPromise = safeInvoke('desktop_open_devtools', {}, { + timeout: 2000, + onCancel: () => { + console.warn('[Bridge] Devtools invocation timed out'); + } + }); + devtoolsPromise.catch(() => { + + }); + } + }; + window.addEventListener('keydown', handler); + + return () => { + window.removeEventListener('keydown', handler); + }; +} diff --git a/packages/desktop/src/lib/tauriCallbackManager.ts b/packages/desktop/src/lib/tauriCallbackManager.ts new file mode 100644 index 00000000..0e706cb6 --- /dev/null +++ b/packages/desktop/src/lib/tauriCallbackManager.ts @@ -0,0 +1,308 @@ + + +import { invoke } from '@tauri-apps/api/core'; +import { listen, type UnlistenFn } from '@tauri-apps/api/event'; + +interface PendingCallback { + id: string; + timestamp: number; + type: 'invoke' | 'listen'; + cleanup?: () => void; + timeout?: NodeJS.Timeout; +} + +interface CallbackManagerConfig { + maxCallbackAge?: number; + cleanupInterval?: number; + invokeTimeout?: number; + listenTimeout?: number; +} + +class TauriCallbackManager { + private callbacks = new Map(); + private isShuttingDown = false; + private cleanupTimer?: NodeJS.Timeout; + private config: Required; + private windowUnloadHandler?: () => void; + + constructor(config: CallbackManagerConfig = {}) { + this.config = { + maxCallbackAge: 30000, + cleanupInterval: 5000, + invokeTimeout: 10000, + listenTimeout: 30000, + ...config, + }; + + this.setupWindowUnloadHandler(); + this.startCleanupTimer(); + } + + register(callback: Omit): string { + if (this.isShuttingDown) { + console.warn('[TauriCallbackManager] Attempted to register callback during shutdown'); + return callback.id; + } + + const fullCallback: PendingCallback = { + ...callback, + timestamp: Date.now(), + }; + + this.callbacks.set(callback.id, fullCallback); + + if (callback.type === 'listen' && this.config.listenTimeout > 0) { + const timeout = setTimeout(() => { + this.cleanupCallback(callback.id, 'timeout'); + }, this.config.listenTimeout); + fullCallback.timeout = timeout; + } + + return callback.id; + } + + unregister(callbackId: string): void { + const callback = this.callbacks.get(callbackId); + if (!callback) { + return; + } + + if (callback.timeout) { + clearTimeout(callback.timeout); + } + + if (callback.cleanup) { + try { + callback.cleanup(); + } catch (error) { + console.warn('[TauriCallbackManager] Cleanup function failed:', error); + } + } + + this.callbacks.delete(callbackId); + } + + private cleanupCallback(callbackId: string, reason: 'timeout' | 'shutdown' | 'expired'): void { + const callback = this.callbacks.get(callbackId); + if (!callback) { + return; + } + + if (reason === 'expired') { + console.warn(`[TauriCallbackManager] Callback ${callbackId} expired and was cleaned up`); + } + + this.unregister(callbackId); + } + + cleanupAll(): void { + this.isShuttingDown = true; + + if (this.cleanupTimer) { + clearInterval(this.cleanupTimer); + this.cleanupTimer = undefined; + } + + const callbackIds = Array.from(this.callbacks.keys()); + callbackIds.forEach(id => this.cleanupCallback(id, 'shutdown')); + + this.callbacks.clear(); + } + + private startCleanupTimer(): void { + this.cleanupTimer = setInterval(() => { + if (this.isShuttingDown) { + return; + } + + const now = Date.now(); + const expiredCallbacks: string[] = []; + + this.callbacks.forEach((callback, id) => { + const age = now - callback.timestamp; + if (age > this.config.maxCallbackAge) { + expiredCallbacks.push(id); + } + }); + + expiredCallbacks.forEach(id => this.cleanupCallback(id, 'expired')); + }, this.config.cleanupInterval); + } + + private setupWindowUnloadHandler(): void { + if (typeof window === 'undefined') { + return; + } + + this.windowUnloadHandler = () => { + console.info('[TauriCallbackManager] Window unloading, cleaning up callbacks...'); + this.cleanupAll(); + }; + + window.addEventListener('beforeunload', this.windowUnloadHandler); + window.addEventListener('pagehide', this.windowUnloadHandler); + } + + removeWindowHandlers(): void { + if (this.windowUnloadHandler && typeof window !== 'undefined') { + window.removeEventListener('beforeunload', this.windowUnloadHandler); + window.removeEventListener('pagehide', this.windowUnloadHandler); + this.windowUnloadHandler = undefined; + } + } + + getStats(): { total: number; invoke: number; listen: number } { + const stats = { total: 0, invoke: 0, listen: 0 }; + + this.callbacks.forEach(callback => { + stats.total++; + stats[callback.type]++; + }); + + return stats; + } +} + +let globalCallbackManager: TauriCallbackManager | null = null; + +export function getTauriCallbackManager(config?: CallbackManagerConfig): TauriCallbackManager { + if (!globalCallbackManager) { + globalCallbackManager = new TauriCallbackManager(config); + } + return globalCallbackManager; +} + +export async function safeInvoke( + command: string, + args?: Record, + options?: { + timeout?: number; + onCancel?: () => void; + } +): Promise { + const manager = getTauriCallbackManager(); + const callbackId = `invoke:${command}:${Date.now()}:${Math.random().toString(36).slice(2)}`; + + let timeoutHandle: NodeJS.Timeout | undefined; + let settled = false; + + manager.register({ + id: callbackId, + type: 'invoke', + }); + + const clearAndUnregister = () => { + if (timeoutHandle) { + clearTimeout(timeoutHandle); + timeoutHandle = undefined; + } + manager.unregister(callbackId); + }; + + if (!options?.timeout || options.timeout <= 0) { + try { + const result = await invoke(command, args); + clearAndUnregister(); + return result; + } catch (error) { + clearAndUnregister(); + throw error; + } + } + + return new Promise((resolve, reject) => { + timeoutHandle = setTimeout(() => { + if (settled) { + return; + } + settled = true; + + console.warn(`[safeInvoke] Command ${command} timed out after ${options.timeout}ms`); + try { + options.onCancel?.(); + } catch (error) { + console.warn('[safeInvoke] onCancel handler threw:', error); + } + + clearAndUnregister(); + reject(new Error(`Command ${command} timed out after ${options.timeout}ms`)); + }, options.timeout); + + invoke(command, args) + .then((result) => { + if (settled) { + + return; + } + settled = true; + clearAndUnregister(); + resolve(result); + }) + .catch((error) => { + if (settled) { + return; + } + settled = true; + clearAndUnregister(); + reject(error); + }); + }); +} + +export async function safeListen( + event: string, + handler: (event: { payload: T }) => void, + options?: { + timeout?: number; + onCancel?: () => void; + } +): Promise { + const manager = getTauriCallbackManager(); + const callbackId = `listen:${event}:${Date.now()}:${Math.random().toString(36).slice(2)}`; + + try { + + manager.register({ + id: callbackId, + type: 'listen', + cleanup: options?.onCancel, + }); + + const unlisten = await listen(event, (event) => { + + const currentManager = getTauriCallbackManager(); + if (currentManager.getStats().total === 0) { + return; + } + + try { + handler(event); + } catch (error) { + console.error(`[safeListen] Handler error for event ${event}:`, error); + } + }); + + const enhancedUnlisten = () => { + try { + unlisten(); + } catch (error) { + console.warn(`[safeListen] Failed to unlisten from ${event}:`, error); + } + manager.unregister(callbackId); + }; + + return enhancedUnlisten; + } catch (error) { + + manager.unregister(callbackId); + throw error; + } +} + +export function cleanupAllTauriCallbacks(): void { + if (globalCallbackManager) { + globalCallbackManager.cleanupAll(); + globalCallbackManager.removeWindowHandlers(); + globalCallbackManager = null; + } +} diff --git a/packages/desktop/src/main.tsx b/packages/desktop/src/main.tsx new file mode 100644 index 00000000..c27e1344 --- /dev/null +++ b/packages/desktop/src/main.tsx @@ -0,0 +1,254 @@ +import { createDesktopAPIs } from './api'; +import { requestInitialNotificationPermission } from './api/notifications'; +import { checkForUpdates, downloadUpdate, restartToUpdate, type UpdateInfo, type UpdateProgress } from './api/updater'; +import { initializeDesktopBridge } from './lib/bridge'; + +import { invoke } from '@tauri-apps/api/core'; +import { listen } from '@tauri-apps/api/event'; +import type { RuntimeAPIs } from '@openchamber/ui/lib/api/types'; +import type { DesktopApi, DesktopSettings } from '@openchamber/ui/lib/desktop'; +import '@openchamber/ui/index.css'; +import '@openchamber/ui/styles/fonts'; + +if (!(window as typeof globalThis & { process?: unknown }).process) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (window as typeof globalThis & { process?: any }).process = { + env: {}, + platform: 'darwin', + version: 'v20.0.0', + versions: {}, + cwd: () => '/', + nextTick: (fn: () => void) => Promise.resolve().then(() => fn()), + }; +} + +declare global { + interface Window { + __OPENCHAMBER_RUNTIME_APIS__?: RuntimeAPIs; + __OPENCHAMBER_HOME__?: string; + opencodeDesktop?: DesktopApi; + } +} + +const cleanupFunctions: Array<() => void | Promise> = []; + +try { + await initializeDesktopBridge(); + + const activityUnlisten = await listen('openchamber:session-activity', (event) => { + window.dispatchEvent(new CustomEvent('openchamber:session-activity', { detail: event.payload })); + }); + cleanupFunctions.push(() => activityUnlisten()); + + requestInitialNotificationPermission().catch(err => { + console.error('[main] Failed to request notification permission:', err); + }); + + window.__OPENCHAMBER_RUNTIME_APIS__ = createDesktopAPIs(); + + cleanupFunctions.push(() => { + console.info('[main] Cleaning up runtime APIs'); + + if (window.__OPENCHAMBER_RUNTIME_APIS__) { + /* cleanup placeholder */ + } + }); + +} catch (error) { + console.error('[main] FATAL: Failed to initialize desktop runtime:', error); + + for (const cleanup of cleanupFunctions) { + try { + const result = cleanup(); + if (result instanceof Promise) { + await result; + } + } catch (cleanupError) { + console.warn('[main] Cleanup function failed during error handling:', cleanupError); + } + } + + document.body.innerHTML = ` +
+

Desktop Runtime Initialization Failed

+
+${error instanceof Error ? error.stack : String(error)}
+      
+

Press Cmd+Option+I to open DevTools for more details

+
+ `; + throw error; +} + +let homeDirectory: string | undefined; +try { + const { homeDir } = await import('@tauri-apps/api/path'); + homeDirectory = await homeDir(); +} catch { + homeDirectory = undefined; +} + +if (homeDirectory) { + window.__OPENCHAMBER_HOME__ = homeDirectory; +} + +window.opencodeDesktop = { + homeDirectory, + async getServerInfo() { + const server = window.__OPENCHAMBER_DESKTOP_SERVER__; + return { + webPort: server?.origin ? parseInt(server.origin.split(':')[2] || '0', 10) : null, + openCodePort: server?.opencodePort ?? null, + host: '127.0.0.1', + ready: true, + cliAvailable: server?.cliAvailable ?? false, + }; + }, + async getSettings(): Promise { + try { + const result = await invoke<{ settings: DesktopSettings; source: string }>('load_settings'); + return result.settings; + } catch (error) { + console.error('[desktop] Error loading settings:', error); + return {} as DesktopSettings; + } + }, + async updateSettings(changes: Partial): Promise { + try { + const result = await invoke('save_settings', { changes }); + return result; + } catch (error) { + console.error('[desktop] Error updating settings:', error); + return {}; + } + }, + async restartOpenCode() { + try { + await invoke('restart_opencode'); + return { success: true }; + } catch (error) { + console.error('[desktop] Error restarting OpenCode:', error); + return { success: false }; + } + }, + async shutdown() { + return { success: false }; + }, + async getHomeDirectory() { + return { success: true, path: homeDirectory || null }; + }, + markRendererReady() { + + }, + async requestDirectoryAccess() { + try { + + const { open } = await import('@tauri-apps/plugin-dialog'); + const selected = await open({ + directory: true, + multiple: false, + title: 'Select Working Directory' + }); + + if (!selected || typeof selected !== 'string') { + return { success: false, error: 'Directory selection cancelled' }; + } + + const result = await invoke<{ success: boolean; path?: string; error?: string }>('process_directory_selection', { + path: selected + }); + + return result; + } catch (error) { + console.error('[desktop] Error requesting directory access:', error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + }, + async startAccessingDirectory(directoryPath: string) { + try { + const result = await invoke<{ success: boolean; error?: string }>('start_accessing_directory', { path: directoryPath }); + return result; + } catch (error) { + console.error('[desktop] Error starting directory access:', error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + }, + + async stopAccessingDirectory(directoryPath: string) { + try { + const result = await invoke<{ success: boolean; error?: string }>('stop_accessing_directory', { path: directoryPath }); + return result; + } catch (error) { + console.error('[desktop] Error stopping directory access:', error); + return { success: false, error: error instanceof Error ? error.message : String(error) }; + } + }, + async notifyAssistantCompletion(payload) { + try { + const { createDesktopNotificationsAPI } = await import('./api/notifications'); + const result = await createDesktopNotificationsAPI().notifyAgentCompletion(payload); + return { success: result }; + } catch (error) { + console.error('[desktop] Error sending notification:', error); + return { success: false }; + } + }, + async checkForUpdates(): Promise { + return checkForUpdates(); + }, + async downloadUpdate(onProgress?: (progress: UpdateProgress) => void): Promise { + return downloadUpdate(onProgress); + }, + async restartToUpdate(): Promise { + return restartToUpdate(); + } +}; + +console.info('[main] window.opencodeDesktop assigned'); + +if (typeof window !== 'undefined') { + const handleBeforeUnload = () => { + console.info('[main] App is unloading, performing cleanup...'); + + cleanupFunctions.forEach((cleanup) => { + try { + const result = cleanup(); + if (result instanceof Promise) { + + result.catch(cleanupError => { + console.warn('[main] Cleanup function failed during unload:', cleanupError); + }); + } + } catch (cleanupError) { + console.warn('[main] Cleanup function failed during unload:', cleanupError); + } + }); + + console.info('[main] Cleanup initiated'); + }; + + window.addEventListener('beforeunload', handleBeforeUnload); + + window.addEventListener('pagehide', handleBeforeUnload); + + cleanupFunctions.push(() => { + window.removeEventListener('beforeunload', handleBeforeUnload); + window.removeEventListener('pagehide', handleBeforeUnload); + }); +} + +try { + await import('@openchamber/ui/main'); +} catch (error) { + console.error('[main] FATAL: Failed to load UI module:', error); + document.body.innerHTML = ` +
+

UI Module Load Failed

+
+${error instanceof Error ? error.stack : String(error)}
+      
+

Check DevTools console for details

+
+ `; + throw error; +} diff --git a/packages/desktop/tsconfig.json b/packages/desktop/tsconfig.json new file mode 100644 index 00000000..994cb50c --- /dev/null +++ b/packages/desktop/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "verbatimModuleSyntax": true, + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "jsx": "react-jsx", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "baseUrl": ".", + "types": ["vite/client"], + "paths": { + "@/*": ["../ui/src/*"], + "@desktop/*": ["./src/*"], + "@openchamber/ui/*": ["../ui/src/*"], + "@openchamber/desktop/*": ["./src/*"] + } + }, + "include": ["src", "../ui/src", "../ui/src/types/**/*"] +} diff --git a/packages/desktop/vite.config.ts b/packages/desktop/vite.config.ts new file mode 100644 index 00000000..16557984 --- /dev/null +++ b/packages/desktop/vite.config.ts @@ -0,0 +1,75 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { themeStoragePlugin } from '../../vite-theme-plugin'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +export default defineConfig({ + root: path.resolve(__dirname, '.'), + plugins: [react(), themeStoragePlugin()], + resolve: { + alias: { + '@desktop': path.resolve(__dirname, './src'), + '@openchamber/ui': path.resolve(__dirname, '../ui/src'), + '@': path.resolve(__dirname, '../ui/src'), + '@opencode-ai/sdk': path.resolve(__dirname, '../../node_modules/@opencode-ai/sdk/dist/client.js'), + }, + }, + define: { + 'process.env': {}, + 'process.platform': JSON.stringify('darwin'), + 'process.version': JSON.stringify('v20.0.0'), + 'process.versions': JSON.stringify({}), + global: 'globalThis', + }, + optimizeDeps: { + include: ['@opencode-ai/sdk'], + exclude: [ + '@tauri-apps/plugin-dialog', + '@tauri-apps/api/core', + '@tauri-apps/api/path', + ], + }, + server: { + host: '127.0.0.1', + port: 1421, + strictPort: true, + hmr: { + protocol: 'ws', + host: '127.0.0.1', + port: 1421, + }, + }, + build: { + outDir: path.resolve(__dirname, 'dist'), + emptyOutDir: true, + chunkSizeWarningLimit: 1200, + rollupOptions: { + output: { + manualChunks(id) { + if (!id.includes('node_modules')) return undefined; + + const match = id.split('node_modules/')[1]; + if (!match) return undefined; + + const segments = match.split('/'); + const packageName = match.startsWith('@') ? `${segments[0]}/${segments[1]}` : segments[0]; + + if (packageName === 'react' || packageName === 'react-dom') return 'vendor-react'; + if (packageName === 'zustand' || packageName === 'zustand/middleware') return 'vendor-zustand'; + + if (packageName === '@opencode-ai/sdk') return 'vendor-opencode-sdk'; + if (packageName.includes('remark') || packageName.includes('rehype') || packageName === 'react-markdown') return 'vendor-markdown'; + if (packageName.startsWith('@radix-ui')) return 'vendor-radix'; + if (packageName.includes('react-syntax-highlighter') || packageName.includes('highlight.js')) return 'vendor-syntax'; + if (packageName.startsWith('@tauri-apps')) return 'vendor-tauri'; + + const sanitized = packageName.replace(/^@/, '').replace(/\//g, '-'); + return `vendor-${sanitized}`; + }, + }, + }, + }, +}); diff --git a/packages/ui/package.json b/packages/ui/package.json new file mode 100644 index 00000000..a66c8ad2 --- /dev/null +++ b/packages/ui/package.json @@ -0,0 +1,77 @@ +{ + "name": "@openchamber/ui", + "version": "1.0.0", + "private": true, + "type": "module", + "main": "src/main.tsx", + "scripts": { + "dev": "tsc --noEmit --watch", + "build": "tsc --noEmit", + "type-check": "tsc --noEmit", + "lint": "eslint \"./src/**/*.{ts,tsx}\" --config ../../eslint.config.js" + }, + "dependencies": { + "@fontsource/ibm-plex-mono": "^5.2.7", + "@fontsource/ibm-plex-sans": "^5.1.1", + "@ibm/plex": "^6.4.1", + "@opencode-ai/sdk": "^1.0.65", + "@radix-ui/react-collapsible": "^1.1.12", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-toggle": "^1.1.10", + "@radix-ui/react-tooltip": "^1.2.8", + "@remixicon/react": "^4.7.0", + "@types/react-syntax-highlighter": "^15.5.13", + "@xterm/addon-fit": "^0.10.0", + "@xterm/xterm": "^5.3.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "express": "^5.1.0", + "http-proxy-middleware": "^3.0.5", + "motion": "^12.23.24", + "next-themes": "^0.4.6", + "node-pty": "^1.0.0", + "react": "^19.1.1", + "react-dom": "^19.1.1", + "@monaco-editor/react": "^4.6.0", + "react-syntax-highlighter": "^15.6.6", + "simple-git": "^3.28.0", + "sonner": "^2.0.7", + "streamdown": "^1.6.10", + "strip-json-comments": "^5.0.3", + "tailwind-merge": "^3.3.1", + "yaml": "^2.8.1", + "zustand": "^5.0.8" + }, + "devDependencies": { + "@eslint/js": "^9.33.0", + "@tailwindcss/postcss": "^4.0.0", + "@tauri-apps/api": "^2.9.0", + "@types/node": "^24.3.1", + "@types/react": "^19.1.10", + "@types/react-dom": "^19.1.7", + "@vitejs/plugin-react": "^5.0.0", + "autoprefixer": "^10.4.21", + "concurrently": "^9.2.1", + "cors": "^2.8.5", + "cross-env": "^7.0.3", + "electron": "^38.2.0", + "electron-builder": "^24.13.3", + "eslint": "^9.33.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.20", + "globals": "^16.3.0", + "nodemon": "^3.1.7", + "tailwindcss": "^4.0.0", + "tsx": "^4.20.6", + "tw-animate-css": "^1.3.8", + "typescript": "~5.8.3", + "typescript-eslint": "^8.39.1", + "vite": "^7.1.2" + } +} diff --git a/packages/ui/src/App.css b/packages/ui/src/App.css new file mode 100644 index 00000000..b9d355df --- /dev/null +++ b/packages/ui/src/App.css @@ -0,0 +1,42 @@ +#root { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; + transition: filter 300ms; +} +.logo:hover { + filter: drop-shadow(0 0 2em #646cffaa); +} +.logo.react:hover { + filter: drop-shadow(0 0 2em #61dafbaa); +} + +@keyframes logo-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@media (prefers-reduced-motion: no-preference) { + a:nth-of-type(2) .logo { + animation: logo-spin infinite 20s linear; + } +} + +.card { + padding: 2em; +} + +.read-the-docs { + color: #888; +} diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx new file mode 100644 index 00000000..54457498 --- /dev/null +++ b/packages/ui/src/App.tsx @@ -0,0 +1,188 @@ +import React from 'react'; +import { MainLayout } from '@/components/layout/MainLayout'; +import { FireworksProvider } from '@/contexts/FireworksContext'; +import { Toaster } from '@/components/ui/sonner'; +import { MemoryDebugPanel } from '@/components/ui/MemoryDebugPanel'; +import { ErrorBoundary } from '@/components/ui/ErrorBoundary'; +import { useEventStream } from '@/hooks/useEventStream'; +import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts'; +import { useMessageSync } from '@/hooks/useMessageSync'; +import { useSessionStatusBootstrap } from '@/hooks/useSessionStatusBootstrap'; +import { GitPollingProvider } from '@/hooks/useGitPolling'; +import { useConfigStore } from '@/stores/useConfigStore'; +import { useSessionStore } from '@/stores/useSessionStore'; +import { useDirectoryStore } from '@/stores/useDirectoryStore'; +import { opencodeClient } from '@/lib/opencode/client'; +import { useFontPreferences } from '@/hooks/useFontPreferences'; +import { CODE_FONT_OPTION_MAP, DEFAULT_MONO_FONT, DEFAULT_UI_FONT, UI_FONT_OPTION_MAP } from '@/lib/fontOptions'; +import { ConfigUpdateOverlay } from '@/components/ui/ConfigUpdateOverlay'; +import { RuntimeAPIProvider } from '@/contexts/RuntimeAPIProvider'; +import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; +import { OnboardingScreen } from '@/components/onboarding/OnboardingScreen'; +import { isCliAvailable } from '@/lib/desktop'; +import type { RuntimeAPIs } from '@/lib/api/types'; + +type AppProps = { + apis: RuntimeAPIs; +}; + +function App({ apis }: AppProps) { + const { initializeApp, loadProviders, isInitialized } = useConfigStore(); + const { error, clearError, loadSessions } = useSessionStore(); + const currentDirectory = useDirectoryStore((state) => state.currentDirectory); + const isSwitchingDirectory = useDirectoryStore((state) => state.isSwitchingDirectory); + const [showMemoryDebug, setShowMemoryDebug] = React.useState(false); + const { uiFont, monoFont } = useFontPreferences(); + const [isDesktopRuntime, setIsDesktopRuntime] = React.useState(() => apis.runtime.isDesktop); + const [cliAvailable, setCliAvailable] = React.useState(() => { + if (!apis.runtime.isDesktop) return true; + return isCliAvailable(); + }); + + React.useEffect(() => { + setIsDesktopRuntime(apis.runtime.isDesktop); + }, [apis.runtime.isDesktop]); + + React.useEffect(() => { + registerRuntimeAPIs(apis); + return () => registerRuntimeAPIs(null); + }, [apis]); + + React.useEffect(() => { + if (typeof document === 'undefined') { + return; + } + const root = document.documentElement; + const uiStack = UI_FONT_OPTION_MAP[uiFont]?.stack ?? UI_FONT_OPTION_MAP[DEFAULT_UI_FONT].stack; + const monoStack = CODE_FONT_OPTION_MAP[monoFont]?.stack ?? CODE_FONT_OPTION_MAP[DEFAULT_MONO_FONT].stack; + + root.style.setProperty('--font-sans', uiStack); + root.style.setProperty('--font-heading', uiStack); + root.style.setProperty('--font-family-sans', uiStack); + root.style.setProperty('--font-mono', monoStack); + root.style.setProperty('--font-family-mono', monoStack); + root.style.setProperty('--ui-regular-font-weight', '400'); + + if (document.body) { + document.body.style.fontFamily = uiStack; + } + }, [uiFont, monoFont]); + + React.useEffect(() => { + if (isInitialized) { + const hideInitialLoading = () => { + const loadingElement = document.getElementById('initial-loading'); + if (loadingElement) { + loadingElement.classList.add('fade-out'); + + setTimeout(() => { + loadingElement.remove(); + }, 300); + } + }; + + const timer = setTimeout(hideInitialLoading, 150); + return () => clearTimeout(timer); + } + }, [isInitialized]); + + React.useEffect(() => { + const fallbackTimer = setTimeout(() => { + const loadingElement = document.getElementById('initial-loading'); + if (loadingElement && !isInitialized) { + loadingElement.classList.add('fade-out'); + setTimeout(() => { + loadingElement.remove(); + }, 300); + } + }, 5000); + + return () => clearTimeout(fallbackTimer); + }, [isInitialized]); + + React.useEffect(() => { + const init = async () => { + await initializeApp(); + await loadProviders(); + }; + + init(); + }, [initializeApp, loadProviders]); + + React.useEffect(() => { + if (isSwitchingDirectory) { + return; + } + + const syncDirectoryAndSessions = async () => { + opencodeClient.setDirectory(currentDirectory); + + await loadSessions(); + }; + + syncDirectoryAndSessions(); + }, [currentDirectory, isSwitchingDirectory, loadSessions]); + + useEventStream(); + + useKeyboardShortcuts(); + + useMessageSync(); + + useSessionStatusBootstrap(); + + React.useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'M') { + e.preventDefault(); + setShowMemoryDebug(prev => !prev); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, []); + + React.useEffect(() => { + if (error) { + + setTimeout(() => clearError(), 5000); + } + }, [error, clearError]); + + const handleCliAvailable = React.useCallback(() => { + setCliAvailable(true); + window.location.reload(); + }, []); + + if (isDesktopRuntime && !cliAvailable) { + return ( + +
+ +
+
+ ); + } + + return ( + + + + +
+ + + + {showMemoryDebug && ( + setShowMemoryDebug(false)} /> + )} +
+
+
+
+
+ ); +} + +export default App; diff --git a/packages/ui/src/assets/provider-logos/gocode.svg b/packages/ui/src/assets/provider-logos/gocode.svg new file mode 100644 index 00000000..7af2fbdf --- /dev/null +++ b/packages/ui/src/assets/provider-logos/gocode.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/packages/ui/src/assets/provider-logos/kimi-for-coding.svg b/packages/ui/src/assets/provider-logos/kimi-for-coding.svg new file mode 100644 index 00000000..fb56ac10 --- /dev/null +++ b/packages/ui/src/assets/provider-logos/kimi-for-coding.svg @@ -0,0 +1 @@ +MoonshotAI \ No newline at end of file diff --git a/packages/ui/src/components/auth/SessionAuthGate.tsx b/packages/ui/src/components/auth/SessionAuthGate.tsx new file mode 100644 index 00000000..15be9273 --- /dev/null +++ b/packages/ui/src/components/auth/SessionAuthGate.tsx @@ -0,0 +1,258 @@ +import React from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { OpenCodeIcon } from '@/components/ui/OpenCodeIcon'; +import { isDesktopRuntime } from '@/lib/desktop'; +import { syncDesktopSettings, initializeAppearancePreferences } from '@/lib/persistence'; +import { applyPersistedDirectoryPreferences } from '@/lib/directoryPersistence'; + +const STATUS_CHECK_ENDPOINT = '/auth/session'; + +const fetchSessionStatus = async (): Promise => { + return fetch(STATUS_CHECK_ENDPOINT, { + method: 'GET', + credentials: 'include', + headers: { + Accept: 'application/json', + }, + }); +}; + +const submitPassword = async (password: string): Promise => { + return fetch(STATUS_CHECK_ENDPOINT, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: JSON.stringify({ password }), + }); +}; + +const AuthShell: React.FC<{ children: React.ReactNode }> = ({ children }) => ( +
+
+
+
+ {children} +
+
+); + +const LoadingScreen: React.FC<{ message?: string }> = ({ message = 'Preparing workspace…' }) => ( + +
+

{message}

+
+
+); + +const ErrorScreen: React.FC<{ onRetry: () => void }> = ({ onRetry }) => ( + +
+
+
+ +
+

Unable to reach server

+

+ We couldn't verify the UI session. Check that the service is running and try again. +

+ +
+
+
+); + +interface SessionAuthGateProps { + children: React.ReactNode; +} + +type GateState = 'pending' | 'authenticated' | 'locked' | 'error'; + +export const SessionAuthGate: React.FC = ({ children }) => { + const desktopRuntime = React.useMemo(() => isDesktopRuntime(), []); + const [state, setState] = React.useState(() => (desktopRuntime ? 'authenticated' : 'pending')); + const [password, setPassword] = React.useState(''); + const [isSubmitting, setIsSubmitting] = React.useState(false); + const [errorMessage, setErrorMessage] = React.useState(''); + const passwordInputRef = React.useRef(null); + const hasResyncedRef = React.useRef(desktopRuntime); + + const checkStatus = React.useCallback(async () => { + if (desktopRuntime) { + setState('authenticated'); + return; + } + + setState((prev) => (prev === 'authenticated' ? prev : 'pending')); + try { + const response = await fetchSessionStatus(); + if (response.ok) { + setState('authenticated'); + setErrorMessage(''); + return; + } + if (response.status === 401) { + setState('locked'); + return; + } + setState('error'); + } catch (error) { + console.warn('Failed to check session status:', error); + setState('error'); + } + }, [desktopRuntime]); + + React.useEffect(() => { + if (desktopRuntime) { + return; + } + void checkStatus(); + }, [checkStatus, desktopRuntime]); + + React.useEffect(() => { + if (!desktopRuntime && state === 'locked') { + hasResyncedRef.current = false; + } + }, [desktopRuntime, state]); + + React.useEffect(() => { + if (state === 'locked' && passwordInputRef.current) { + passwordInputRef.current.focus(); + passwordInputRef.current.select(); + } + }, [state]); + + React.useEffect(() => { + if (desktopRuntime) { + return; + } + if (state === 'authenticated' && !hasResyncedRef.current) { + hasResyncedRef.current = true; + void (async () => { + await syncDesktopSettings(); + await initializeAppearancePreferences(); + await applyPersistedDirectoryPreferences(); + })(); + } + }, [desktopRuntime, state]); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + if (!password || isSubmitting) { + return; + } + + setIsSubmitting(true); + setErrorMessage(''); + + try { + const response = await submitPassword(password); + if (response.ok) { + setPassword(''); + setState('authenticated'); + return; + } + + if (response.status === 401) { + setErrorMessage('Incorrect password. Try again.'); + setState('locked'); + return; + } + + setErrorMessage('Unexpected response from server.'); + setState('error'); + } catch (error) { + console.warn('Failed to submit UI password:', error); + setErrorMessage('Network error. Check connection and retry.'); + setState('error'); + } finally { + setIsSubmitting(false); + } + }; + + if (state === 'pending') { + return ; + } + + if (state === 'error') { + return void checkStatus()} />; + } + + if (state === 'locked') { + return ( + +
+
+
+ +
+
+

Unlock OpenChamber

+

+ Enter the password configured for this web session. +

+
+
+ +
+
+ + { + setPassword(event.target.value); + if (errorMessage) { + setErrorMessage(''); + } + }} + aria-invalid={Boolean(errorMessage) || undefined} + aria-describedby={errorMessage ? 'oc-ui-auth-error' : undefined} + disabled={isSubmitting} + /> + {errorMessage && ( +

+ {errorMessage} +

+ )} +
+ + +
+
+
+ ); + } + + return <>{children}; +}; diff --git a/packages/ui/src/components/chat/AgentMentionAutocomplete.tsx b/packages/ui/src/components/chat/AgentMentionAutocomplete.tsx new file mode 100644 index 00000000..a241222b --- /dev/null +++ b/packages/ui/src/components/chat/AgentMentionAutocomplete.tsx @@ -0,0 +1,152 @@ +import React from 'react'; +import { cn } from '@/lib/utils'; +import { useConfigStore } from '@/stores/useConfigStore'; +import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; + +interface AgentInfo { + name: string; + description?: string; + mode?: string | null; +} + +export interface AgentMentionAutocompleteHandle { + handleKeyDown: (key: string) => void; +} + +interface AgentMentionAutocompleteProps { + searchQuery: string; + onAgentSelect: (agentName: string) => void; + onClose: () => void; +} + +const isMentionable = (mode?: string | null): boolean => { + if (!mode) { + return false; + } + return mode !== 'primary'; +}; + +export const AgentMentionAutocomplete = React.forwardRef(({ + searchQuery, + onAgentSelect, + onClose, +}, ref) => { + const containerRef = React.useRef(null); + const [selectedIndex, setSelectedIndex] = React.useState(0); + const [agents, setAgents] = React.useState([]); + const { agents: allAgents } = useConfigStore(); + + React.useEffect(() => { + const filtered = allAgents + .filter((agent) => isMentionable(agent.mode)) + .map((agent) => ({ + name: agent.name, + description: agent.description, + mode: agent.mode ?? undefined, + })); + + const normalizedQuery = searchQuery.trim().toLowerCase(); + const matches = normalizedQuery.length + ? filtered.filter((agent) => agent.name.toLowerCase().includes(normalizedQuery)) + : filtered; + + matches.sort((a, b) => a.name.localeCompare(b.name)); + + setAgents(matches); + setSelectedIndex(0); + }, [allAgents, searchQuery]); + + React.useEffect(() => { + const handlePointerDown = (event: MouseEvent | TouchEvent) => { + const target = event.target as Node | null; + if (!target || !containerRef.current) { + return; + } + if (!containerRef.current.contains(target)) { + onClose(); + } + }; + + document.addEventListener('pointerdown', handlePointerDown, true); + return () => { + document.removeEventListener('pointerdown', handlePointerDown, true); + }; + }, [onClose]); + + React.useImperativeHandle(ref, () => ({ + handleKeyDown: (key: string) => { + if (key === 'Escape') { + onClose(); + return; + } + + if (!agents.length) { + return; + } + + if (key === 'ArrowDown') { + setSelectedIndex((prev) => (prev + 1) % agents.length); + return; + } + + if (key === 'ArrowUp') { + setSelectedIndex((prev) => (prev - 1 + agents.length) % agents.length); + return; + } + + if (key === 'Enter' || key === 'Tab') { + const agent = agents[(selectedIndex + agents.length) % agents.length]; + if (agent) { + onAgentSelect(agent.name); + } + } + }, + }), [agents, onAgentSelect, onClose, selectedIndex]); + + const renderAgent = (agent: AgentInfo, index: number) => ( +
onAgentSelect(agent.name)} + onMouseEnter={() => setSelectedIndex(index)} +> +
+
+ #{agent.name} +
+ {agent.description && ( +
+ {agent.description} +
+ )} +
+
+ ); + + return ( +
+ + {agents.length ? ( +
+ {agents.map((agent, index) => renderAgent(agent, index))} +
+ ) : ( +
+ No agents found +
+ )} +
+
+ ↑↓ navigate • Enter select • Esc close +
+
+ ); +}); + +AgentMentionAutocomplete.displayName = 'AgentMentionAutocomplete'; diff --git a/packages/ui/src/components/chat/ChatContainer.tsx b/packages/ui/src/components/chat/ChatContainer.tsx new file mode 100644 index 00000000..f6a5964a --- /dev/null +++ b/packages/ui/src/components/chat/ChatContainer.tsx @@ -0,0 +1,255 @@ +import React from 'react'; +import { RiArrowDownLine } from '@remixicon/react'; + +import { ChatInput } from './ChatInput'; +import { useSessionStore } from '@/stores/useSessionStore'; +import { Skeleton } from '@/components/ui/skeleton'; +import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo'; +import MessageList from './MessageList'; +import { ScrollShadow } from '@/components/ui/ScrollShadow'; +import { useChatScrollManager } from '@/hooks/useChatScrollManager'; +import { useDeviceInfo } from '@/lib/device'; +import { Button } from '@/components/ui/button'; +import { OverlayScrollbar } from '@/components/ui/OverlayScrollbar'; + +export const ChatContainer: React.FC = () => { + const { + currentSessionId, + messages, + permissions, + streamingMessageIds, + isLoading, + loadMessages, + loadMoreMessages, + updateViewportAnchor, + sessionMemoryState, + isSyncing, + messageStreamStates, + trimToViewportWindow, + sessionActivityPhase, + } = useSessionStore(); + + const streamingMessageId = React.useMemo(() => { + if (!currentSessionId) return null; + return streamingMessageIds.get(currentSessionId) ?? null; + }, [currentSessionId, streamingMessageIds]); + + const { isMobile } = useDeviceInfo(); + + const sessionMessages = React.useMemo(() => { + + return currentSessionId ? messages.get(currentSessionId) || [] : []; + }, [currentSessionId, messages]); + + const sessionPermissions = React.useMemo(() => { + return currentSessionId ? permissions.get(currentSessionId) || [] : []; + }, [currentSessionId, permissions]); + + const { + scrollRef, + handleMessageContentChange, + getAnimationHandlers, + showScrollButton, + scrollToBottom, + spacerHeight, + pendingAnchorId, + hasActiveAnchor, + } = useChatScrollManager({ + currentSessionId, + sessionMessages, + streamingMessageId, + sessionMemoryState, + updateViewportAnchor, + isSyncing, + isMobile, + messageStreamStates, + sessionPermissions, + trimToViewportWindow, + sessionActivityPhase, + }); + + const memoryState = React.useMemo(() => { + if (!currentSessionId) { + return null; + } + return sessionMemoryState.get(currentSessionId) ?? null; + }, [currentSessionId, sessionMemoryState]); + const hasMoreAbove = Boolean(memoryState?.hasMoreAbove); + const [isLoadingOlder, setIsLoadingOlder] = React.useState(false); + React.useEffect(() => { + setIsLoadingOlder(false); + }, [currentSessionId]); + + const lastScrolledSessionRef = React.useRef(null); + React.useLayoutEffect(() => { + if (!currentSessionId || currentSessionId === lastScrolledSessionRef.current) { + return; + } + lastScrolledSessionRef.current = currentSessionId; + + const container = scrollRef.current; + if (container) { + container.scrollTop = container.scrollHeight - container.clientHeight; + } + }, [currentSessionId, scrollRef]); + + const handleLoadOlder = React.useCallback(async () => { + if (!currentSessionId || isLoadingOlder) { + return; + } + + const container = scrollRef.current; + const prevHeight = container?.scrollHeight ?? null; + const prevTop = container?.scrollTop ?? null; + + setIsLoadingOlder(true); + try { + await loadMoreMessages(currentSessionId, 'up'); + if (container && prevHeight !== null && prevTop !== null) { + const heightDiff = container.scrollHeight - prevHeight; + container.scrollTop = prevTop + heightDiff; + } + } finally { + setIsLoadingOlder(false); + } + }, [currentSessionId, isLoadingOlder, loadMoreMessages, scrollRef]); + + React.useEffect(() => { + if (!currentSessionId) { + return; + } + + const hasSessionMessages = messages.has(currentSessionId); + const existingMessages = hasSessionMessages ? messages.get(currentSessionId) ?? [] : []; + + if (existingMessages.length > 0) { + return; + } + + const load = async () => { + try { + await loadMessages(currentSessionId); + } finally { + if (typeof window === 'undefined') { + scrollToBottom(); + } else { + window.requestAnimationFrame(() => { + scrollToBottom(); + }); + } + } + }; + + void load(); + }, [currentSessionId, loadMessages, messages, scrollToBottom]); + + if (!currentSessionId) { + return ( +
+
+ +
+
+ ); + } + + if (isLoading && sessionMessages.length === 0 && !streamingMessageId) { + const hasMessagesEntry = messages.has(currentSessionId); + if (!hasMessagesEntry) { + return ( +
+
+
+ {[1, 2, 3].map((i) => ( +
+ +
+ + +
+
+ ))} +
+
+ +
+ ); + } + } + + if (sessionMessages.length === 0 && !streamingMessageId) { + return ( +
+
+ +
+
+ +
+
+ ); + } + + return ( +
+
+ +
+ +
+ + {} + {spacerHeight > 0 && hasActiveAnchor && ( + + + +
+
+ +
+ {showScrollButton && sessionMessages.length > 0 && ( +
+ +
+ )} + +
+
+ ); +}; diff --git a/packages/ui/src/components/chat/ChatEmptyState.tsx b/packages/ui/src/components/chat/ChatEmptyState.tsx new file mode 100644 index 00000000..0d01c2e8 --- /dev/null +++ b/packages/ui/src/components/chat/ChatEmptyState.tsx @@ -0,0 +1,13 @@ +import React from 'react'; + +import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo'; + +const ChatEmptyState: React.FC = () => { + return ( +
+ +
+ ); +}; + +export default React.memo(ChatEmptyState); diff --git a/packages/ui/src/components/chat/ChatErrorBoundary.tsx b/packages/ui/src/components/chat/ChatErrorBoundary.tsx new file mode 100644 index 00000000..1d86a179 --- /dev/null +++ b/packages/ui/src/components/chat/ChatErrorBoundary.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { RiChat3Line, RiRestartLine } from '@remixicon/react'; +import { Button } from '../ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; + +interface ChatErrorBoundaryState { + hasError: boolean; + error?: Error; + errorInfo?: React.ErrorInfo; +} + +interface ChatErrorBoundaryProps { + children: React.ReactNode; + sessionId?: string; +} + +export class ChatErrorBoundary extends React.Component { + constructor(props: ChatErrorBoundaryProps) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error: Error): ChatErrorBoundaryState { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + this.setState({ error, errorInfo }); + + if (process.env.NODE_ENV === 'development') { + console.error('Chat error caught by boundary:', error, errorInfo); + } + } + + handleReset = () => { + this.setState({ hasError: false, error: undefined, errorInfo: undefined }); + }; + + render() { + if (this.state.hasError) { + return ( +
+ + + + + Chat Error + + + +

+ The chat interface encountered an error. This might be due to a temporary network issue or corrupted message data. +

+ + {this.props.sessionId && ( +
+ Session: {this.props.sessionId} +
+ )} + + {this.state.error && ( +
+ Error details +
+                    {this.state.error.toString()}
+                  
+
+ )} + +
+ +
+ +
+ If the problem persists, try refreshing the page. +
+
+
+
+ ); + } + + return this.props.children; + } +} \ No newline at end of file diff --git a/packages/ui/src/components/chat/ChatInput.tsx b/packages/ui/src/components/chat/ChatInput.tsx new file mode 100644 index 00000000..a7a16407 --- /dev/null +++ b/packages/ui/src/components/chat/ChatInput.tsx @@ -0,0 +1,882 @@ +import React from 'react'; +import { Textarea } from '@/components/ui/textarea'; +import { RiAiAgentLine, RiCloseCircleLine, RiFileUploadLine, RiSendPlane2Line } from '@remixicon/react'; +import { useSessionStore } from '@/stores/useSessionStore'; +import { useConfigStore } from '@/stores/useConfigStore'; +import { useUIStore } from '@/stores/useUIStore'; +import type { EditPermissionMode } from '@/stores/types/sessionTypes'; +import { getEditModeColors } from '@/lib/permissions/editModeColors'; +import { FileAttachmentButton, AttachedFilesList } from './FileAttachment'; +import { FileMentionAutocomplete, type FileMentionHandle } from './FileMentionAutocomplete'; +import { CommandAutocomplete, type CommandAutocompleteHandle } from './CommandAutocomplete'; +import { AgentMentionAutocomplete, type AgentMentionAutocompleteHandle } from './AgentMentionAutocomplete'; +import { cn } from '@/lib/utils'; +import { ServerFilePicker } from './ServerFilePicker'; +import { ModelControls } from './ModelControls'; +import { parseAgentMentions } from '@/lib/messages/agentMentions'; +import { WorkingPlaceholder } from './message/parts/WorkingPlaceholder'; +import { useAssistantStatus } from '@/hooks/useAssistantStatus'; +import { toast } from 'sonner'; +import { useFileStore } from '@/stores/fileStore'; +import { calculateEditPermissionUIState, type BashPermissionSetting } from '@/lib/permissions/editPermissionDefaults'; + +const MAX_VISIBLE_TEXTAREA_LINES = 8; + +interface ChatInputProps { + onOpenSettings?: () => void; + scrollToBottom?: (options?: { instant?: boolean }) => void; +} + +const isPrimaryMode = (mode?: string) => mode === 'primary' || mode === 'all' || mode === undefined || mode === null; + +export const ChatInput: React.FC = ({ onOpenSettings, scrollToBottom }) => { + const [message, setMessage] = React.useState(''); + const [isDragging, setIsDragging] = React.useState(false); + const [showFileMention, setShowFileMention] = React.useState(false); + const [mentionQuery, setMentionQuery] = React.useState(''); + const [showCommandAutocomplete, setShowCommandAutocomplete] = React.useState(false); + const [commandQuery, setCommandQuery] = React.useState(''); + const [showAgentAutocomplete, setShowAgentAutocomplete] = React.useState(false); + const [agentQuery, setAgentQuery] = React.useState(''); + const [textareaSize, setTextareaSize] = React.useState<{ height: number; maxHeight: number } | null>(null); + const textareaRef = React.useRef(null); + const dropZoneRef = React.useRef(null); + const mentionRef = React.useRef(null); + const commandRef = React.useRef(null); + const agentRef = React.useRef(null); + + const sendMessage = useSessionStore((state) => state.sendMessage); + const currentSessionId = useSessionStore((state) => state.currentSessionId); + const abortCurrentOperation = useSessionStore((state) => state.abortCurrentOperation); + const acknowledgeSessionAbort = useSessionStore((state) => state.acknowledgeSessionAbort); + const abortPromptSessionId = useSessionStore((state) => state.abortPromptSessionId); + const abortPromptExpiresAt = useSessionStore((state) => state.abortPromptExpiresAt); + const clearAbortPrompt = useSessionStore((state) => state.clearAbortPrompt); + const attachedFiles = useSessionStore((state) => state.attachedFiles); + const addAttachedFile = useSessionStore((state) => state.addAttachedFile); + const addServerFile = useSessionStore((state) => state.addServerFile); + const clearAttachedFiles = useSessionStore((state) => state.clearAttachedFiles); + const saveSessionAgentSelection = useSessionStore((state) => state.saveSessionAgentSelection); + + const { currentProviderId, currentModelId, currentAgentName, agents, setAgent } = useConfigStore(); + const { isMobile } = useUIStore(); + const { working } = useAssistantStatus(); + const [showAbortStatus, setShowAbortStatus] = React.useState(false); + const abortTimeoutRef = React.useRef | null>(null); + const prevWasAbortedRef = React.useRef(false); + + const currentAgent = React.useMemo(() => { + if (!currentAgentName) { + return undefined; + } + return agents.find((agent) => agent.name === currentAgentName); + }, [agents, currentAgentName]); + + const agentDefaultEditMode = React.useMemo(() => { + const agentPermissionRaw = currentAgent?.permission?.edit; + let defaultMode: EditPermissionMode = 'ask'; + + if (agentPermissionRaw === 'allow' || agentPermissionRaw === 'ask' || agentPermissionRaw === 'deny' || agentPermissionRaw === 'full') { + defaultMode = agentPermissionRaw; + } + + const editToolConfigured = currentAgent ? (currentAgent.tools?.['edit'] !== false) : false; + if (!currentAgent || !editToolConfigured) { + defaultMode = 'deny'; + } + + return defaultMode; + }, [currentAgent]); + + const sessionAgentEditOverride = useSessionStore( + React.useCallback((state) => { + if (!currentSessionId || !currentAgentName) { + return undefined; + } + const sessionMap = state.sessionAgentEditModes.get(currentSessionId); + return sessionMap?.get(currentAgentName); + }, [currentSessionId, currentAgentName]) + ); + + const agentWebfetchPermission = currentAgent?.permission?.webfetch; + const agentBashPermission = currentAgent?.permission?.bash as BashPermissionSetting | undefined; + + const permissionUiState = React.useMemo(() => calculateEditPermissionUIState({ + agentDefaultEditMode, + webfetchPermission: agentWebfetchPermission, + bashPermission: agentBashPermission, + }), [agentDefaultEditMode, agentWebfetchPermission, agentBashPermission]); + + const selectionContextReady = Boolean(currentSessionId && currentAgentName); + + const effectiveEditPermission = React.useMemo(() => { + if (selectionContextReady && sessionAgentEditOverride && permissionUiState.modeAvailability[sessionAgentEditOverride]) { + return sessionAgentEditOverride; + } + return permissionUiState.cascadeDefaultMode; + }, [permissionUiState, selectionContextReady, sessionAgentEditOverride]); + + const chatInputAccent = React.useMemo(() => getEditModeColors(effectiveEditPermission), [effectiveEditPermission]); + + const chatInputWrapperStyle = React.useMemo(() => { + if (!chatInputAccent) { + return undefined; + } + return { + borderColor: chatInputAccent.border ?? chatInputAccent.text, + borderWidth: chatInputAccent.borderWidth ?? 1, + }; + }, [chatInputAccent]); + + const hasContent = message.trim() || attachedFiles.length > 0; + + const canAbort = working.isWorking; + + const isAbortPromptActive = React.useMemo(() => { + if (!currentSessionId) return false; + return abortPromptSessionId === currentSessionId && Boolean(abortPromptExpiresAt); + }, [abortPromptSessionId, abortPromptExpiresAt, currentSessionId]); + const canShowAbortButton = canAbort && (isMobile || isAbortPromptActive); + + const handleSubmit = async (e?: React.FormEvent) => { + e?.preventDefault(); + + if (!hasContent || !currentSessionId) return; + + const messageToSend = message.replace(/^\n+|\n+$/g, ''); + + scrollToBottom?.({ instant: true }); + + const normalizedCommand = messageToSend.trimStart(); + if (normalizedCommand.startsWith('/')) { + const commandName = normalizedCommand + .slice(1) + .trim() + .split(/\s+/)[0] + ?.toLowerCase(); + if (commandName === 'summarize') { + scrollToBottom?.({ instant: true }); + } + } + + if (!currentProviderId || !currentModelId) { + + console.warn('Cannot send message: provider or model not selected'); + return; + } + + const { sanitizedText, mention } = parseAgentMentions(messageToSend, agents); + const agentMentionName = mention?.name; + + const attachmentsToSend = attachedFiles.map((file) => ({ ...file })); + if (attachmentsToSend.length > 0) { + clearAttachedFiles(); + } + + setMessage(''); + + await sendMessage(sanitizedText, currentProviderId, currentModelId, currentAgentName, attachmentsToSend, agentMentionName) + .catch((error: unknown) => { + const rawMessage = + error instanceof Error + ? error.message + : typeof error === 'string' + ? error + : String(error ?? ''); + const normalized = rawMessage.toLowerCase(); + + console.error('Message send failed:', rawMessage || error); + + const isSoftNetworkError = + normalized.includes('timeout') || + normalized.includes('timed out') || + normalized.includes('may still be processing') || + normalized.includes('being processed') || + normalized.includes('failed to fetch') || + normalized.includes('networkerror') || + normalized.includes('network error') || + normalized.includes('gateway timeout') || + normalized === 'failed to send message'; + + if (isSoftNetworkError) { + + return; + } + + if (attachmentsToSend.length > 0) { + useFileStore.setState({ attachedFiles: attachmentsToSend }); + } + toast.error(rawMessage || 'Message failed to send. Attachments restored.'); + }); + + textareaRef.current?.focus(); + + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + + if (showCommandAutocomplete && commandRef.current) { + if (e.key === 'Enter' || e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'Escape' || e.key === 'Tab') { + e.preventDefault(); + commandRef.current.handleKeyDown(e.key); + return; + } + } + + if (showAgentAutocomplete && agentRef.current) { + if (e.key === 'Enter' || e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'Escape' || e.key === 'Tab') { + e.preventDefault(); + agentRef.current.handleKeyDown(e.key); + return; + } + } + + if (showFileMention && mentionRef.current) { + if (e.key === 'Enter' || e.key === 'ArrowUp' || e.key === 'ArrowDown' || e.key === 'Escape' || e.key === 'Tab') { + e.preventDefault(); + mentionRef.current.handleKeyDown(e.key); + return; + } + } + + if (e.key === 'Tab' && !showCommandAutocomplete && !showFileMention) { + e.preventDefault(); + cycleAgent(); + return; + } + + if (e.key === 'Enter' && !e.shiftKey && !isMobile) { + e.preventDefault(); + handleSubmit(); + } + }; + + const startAbortIndicator = React.useCallback(() => { + if (abortTimeoutRef.current) { + clearTimeout(abortTimeoutRef.current); + abortTimeoutRef.current = null; + } + + setShowAbortStatus(true); + + abortTimeoutRef.current = setTimeout(() => { + setShowAbortStatus(false); + abortTimeoutRef.current = null; + }, 1800); + }, []); + + const handleAbort = React.useCallback(() => { + clearAbortPrompt(); + startAbortIndicator(); + + void abortCurrentOperation(); + }, [abortCurrentOperation, clearAbortPrompt, startAbortIndicator]); + + const cycleAgent = () => { + const primaryAgents = agents.filter(agent => isPrimaryMode(agent.mode)); + + if (primaryAgents.length <= 1) return; + + const currentIndex = primaryAgents.findIndex(agent => agent.name === currentAgentName); + const nextIndex = (currentIndex + 1) % primaryAgents.length; + const nextAgent = primaryAgents[nextIndex]; + + setAgent(nextAgent.name); + + if (currentSessionId) { + + saveSessionAgentSelection(currentSessionId, nextAgent.name); + } + }; + + const adjustTextareaHeight = React.useCallback(() => { + const textarea = textareaRef.current; + if (!textarea) { + return; + } + + textarea.style.height = 'auto'; + + const view = textarea.ownerDocument?.defaultView; + const computedStyle = view ? view.getComputedStyle(textarea) : null; + const lineHeight = computedStyle ? parseFloat(computedStyle.lineHeight) : NaN; + const paddingTop = computedStyle ? parseFloat(computedStyle.paddingTop) : NaN; + const paddingBottom = computedStyle ? parseFloat(computedStyle.paddingBottom) : NaN; + const fallbackLineHeight = 22; + const fallbackPadding = 16; + const paddingTotal = Number.isNaN(paddingTop) || Number.isNaN(paddingBottom) + ? fallbackPadding + : paddingTop + paddingBottom; + const targetLineHeight = Number.isNaN(lineHeight) ? fallbackLineHeight : lineHeight; + const maxHeight = targetLineHeight * MAX_VISIBLE_TEXTAREA_LINES + paddingTotal; + const scrollHeight = textarea.scrollHeight || textarea.offsetHeight; + const nextHeight = Math.min(scrollHeight, maxHeight); + + textarea.style.height = `${nextHeight}px`; + textarea.style.maxHeight = `${maxHeight}px`; + + setTextareaSize((prev) => { + if (prev && prev.height === nextHeight && prev.maxHeight === maxHeight) { + return prev; + } + return { height: nextHeight, maxHeight }; + }); + }, []); + + React.useLayoutEffect(() => { + adjustTextareaHeight(); + }, [adjustTextareaHeight, message, isMobile]); + + const updateAutocompleteState = React.useCallback((value: string, cursorPosition: number) => { + if (value.startsWith('/')) { + const firstSpace = value.indexOf(' '); + const firstNewline = value.indexOf('\n'); + const commandEnd = Math.min( + firstSpace === -1 ? value.length : firstSpace, + firstNewline === -1 ? value.length : firstNewline + ); + + if (cursorPosition <= commandEnd && firstSpace === -1) { + const commandText = value.substring(1, commandEnd); + setCommandQuery(commandText); + setShowCommandAutocomplete(true); + setShowFileMention(false); + setShowAgentAutocomplete(false); + } else { + setShowCommandAutocomplete(false); + } + return; + } + + setShowCommandAutocomplete(false); + + const textBeforeCursor = value.substring(0, cursorPosition); + + const lastHashSymbol = textBeforeCursor.lastIndexOf('#'); + if (lastHashSymbol !== -1) { + const charBefore = lastHashSymbol > 0 ? textBeforeCursor[lastHashSymbol - 1] : null; + const textAfterHash = textBeforeCursor.substring(lastHashSymbol + 1); + const hasSeparator = textAfterHash.includes(' ') || textAfterHash.includes('\n'); + const isWordBoundary = !charBefore || /\s/.test(charBefore); + + if (isWordBoundary && !hasSeparator) { + setAgentQuery(textAfterHash); + setShowAgentAutocomplete(true); + setShowFileMention(false); + return; + } + } + + setShowAgentAutocomplete(false); + setAgentQuery(''); + + const lastAtSymbol = textBeforeCursor.lastIndexOf('@'); + if (lastAtSymbol !== -1) { + const textAfterAt = textBeforeCursor.substring(lastAtSymbol + 1); + if (!textAfterAt.includes(' ') && !textAfterAt.includes('\n')) { + setMentionQuery(textAfterAt); + setShowFileMention(true); + } else { + setShowFileMention(false); + } + } else { + setShowFileMention(false); + } + }, [setAgentQuery, setCommandQuery, setMentionQuery, setShowAgentAutocomplete, setShowCommandAutocomplete, setShowFileMention]); + + const insertTextAtSelection = React.useCallback((text: string) => { + if (!text) { + return; + } + + const textarea = textareaRef.current; + if (!textarea) { + const nextValue = message + text; + setMessage(nextValue); + updateAutocompleteState(nextValue, nextValue.length); + requestAnimationFrame(() => adjustTextareaHeight()); + return; + } + + const start = textarea.selectionStart ?? message.length; + const end = textarea.selectionEnd ?? message.length; + const nextValue = `${message.substring(0, start)}${text}${message.substring(end)}`; + setMessage(nextValue); + const cursorPosition = start + text.length; + + requestAnimationFrame(() => { + const currentTextarea = textareaRef.current; + if (currentTextarea) { + currentTextarea.selectionStart = cursorPosition; + currentTextarea.selectionEnd = cursorPosition; + } + adjustTextareaHeight(); + }); + + updateAutocompleteState(nextValue, cursorPosition); + }, [adjustTextareaHeight, message, updateAutocompleteState]); + + const handleTextChange = (e: React.ChangeEvent) => { + const value = e.target.value; + const cursorPosition = e.target.selectionStart ?? value.length; + setMessage(value); + adjustTextareaHeight(); + updateAutocompleteState(value, cursorPosition); + }; + + const handlePaste = React.useCallback(async (e: React.ClipboardEvent) => { + const fileMap = new Map(); + + Array.from(e.clipboardData.files || []).forEach(file => { + if (file.type.startsWith('image/')) { + fileMap.set(`${file.name}-${file.size}`, file); + } + }); + + Array.from(e.clipboardData.items || []).forEach(item => { + if (item.kind === 'file' && item.type.startsWith('image/')) { + const file = item.getAsFile(); + if (file) { + fileMap.set(`${file.name}-${file.size}`, file); + } + } + }); + + const imageFiles = Array.from(fileMap.values()); + if (imageFiles.length === 0) { + return; + } + + if (!currentSessionId) { + return; + } + + e.preventDefault(); + + const pastedText = e.clipboardData.getData('text'); + if (pastedText) { + insertTextAtSelection(pastedText); + } + + let attachedCount = 0; + + for (const file of imageFiles) { + const sizeBefore = useSessionStore.getState().attachedFiles.length; + try { + await addAttachedFile(file); + const sizeAfter = useSessionStore.getState().attachedFiles.length; + if (sizeAfter > sizeBefore) { + attachedCount += 1; + } + } catch (error) { + console.error('Clipboard image attach failed', error); + toast.error(error instanceof Error ? error.message : 'Failed to attach image from clipboard'); + } + } + + if (attachedCount > 0) { + toast.success(`Attached ${attachedCount} image${attachedCount > 1 ? 's' : ''} from clipboard`); + } + }, [addAttachedFile, currentSessionId, insertTextAtSelection]); + + const handleFileSelect = (file: { name: string; path: string }) => { + + const cursorPosition = textareaRef.current?.selectionStart || 0; + const textBeforeCursor = message.substring(0, cursorPosition); + const lastAtSymbol = textBeforeCursor.lastIndexOf('@'); + + if (lastAtSymbol !== -1) { + const newMessage = + message.substring(0, lastAtSymbol) + + file.name + + message.substring(cursorPosition); + setMessage(newMessage); + } + + setShowFileMention(false); + setMentionQuery(''); + + textareaRef.current?.focus(); + }; + + const handleAgentSelect = (agentName: string) => { + const textarea = textareaRef.current; + const cursorPosition = textarea?.selectionStart ?? message.length; + const textBeforeCursor = message.substring(0, cursorPosition); + const lastHashSymbol = textBeforeCursor.lastIndexOf('#'); + + if (lastHashSymbol !== -1) { + const newMessage = + message.substring(0, lastHashSymbol) + + `#${agentName} ` + + message.substring(cursorPosition); + setMessage(newMessage); + + const nextCursor = lastHashSymbol + agentName.length + 2; + requestAnimationFrame(() => { + if (textareaRef.current) { + textareaRef.current.selectionStart = nextCursor; + textareaRef.current.selectionEnd = nextCursor; + } + adjustTextareaHeight(); + updateAutocompleteState(newMessage, nextCursor); + }); + } + + setShowAgentAutocomplete(false); + setAgentQuery(''); + + textareaRef.current?.focus(); + }; + + const handleCommandSelect = (command: { name: string; description?: string; agent?: string; model?: string }) => { + + setMessage(`/${command.name} `); + + const textareaElement = textareaRef.current as HTMLTextAreaElement & { _commandMetadata?: typeof command }; + if (textareaElement) { + textareaElement._commandMetadata = command; + } + + setShowCommandAutocomplete(false); + setCommandQuery(''); + + setTimeout(() => { + if (textareaRef.current) { + textareaRef.current.focus(); + textareaRef.current.setSelectionRange(textareaRef.current.value.length, textareaRef.current.value.length); + } + }, 0); + }; + + React.useEffect(() => { + + if (currentSessionId && textareaRef.current && !isMobile) { + textareaRef.current.focus(); + } + }, [currentSessionId, isMobile]); + + React.useEffect(() => { + if (abortPromptSessionId && abortPromptSessionId !== currentSessionId) { + clearAbortPrompt(); + } + }, [abortPromptSessionId, currentSessionId, clearAbortPrompt]); + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (currentSessionId && !isDragging) { + setIsDragging(true); + } + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (e.currentTarget === e.target) { + setIsDragging(false); + } + }; + + const handleDrop = async (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + + if (!currentSessionId) return; + + const files = Array.from(e.dataTransfer.files); + let attachedCount = 0; + + for (const file of files) { + const sizeBefore = useSessionStore.getState().attachedFiles.length; + try { + await addAttachedFile(file); + const sizeAfter = useSessionStore.getState().attachedFiles.length; + if (sizeAfter > sizeBefore) { + attachedCount += 1; + } + } catch (error) { + console.error('File attach failed', error); + toast.error(error instanceof Error ? error.message : 'Failed to attach file'); + } + } + + if (attachedCount > 0) { + toast.success(`Attached ${attachedCount} file${attachedCount > 1 ? 's' : ''}`); + } + }; + + const handleServerFilesSelected = React.useCallback(async (files: Array<{ path: string; name: string }>) => { + let attachedCount = 0; + + for (const file of files) { + const sizeBefore = useSessionStore.getState().attachedFiles.length; + try { + await addServerFile(file.path, file.name); + const sizeAfter = useSessionStore.getState().attachedFiles.length; + if (sizeAfter > sizeBefore) { + attachedCount += 1; + } + } catch (error) { + console.error('Server file attach failed', error); + toast.error(error instanceof Error ? error.message : 'Failed to attach file'); + } + } + + if (attachedCount > 0) { + toast.success(`Attached ${attachedCount} file${attachedCount > 1 ? 's' : ''}`); + } + }, [addServerFile]); + + const footerGapClass = 'gap-x-1.5 gap-y-0'; + const footerPaddingClass = isMobile ? 'px-1.5 py-1.5' : 'px-2.5 py-1.5'; + const footerHeightClass = isMobile ? 'h-9 w-9' : 'h-7 w-7'; + const iconSizeClass = isMobile ? 'h-5 w-5' : 'h-[18px] w-[18px]'; + + const iconButtonBaseClass = cn( + footerHeightClass, + 'flex items-center justify-center text-muted-foreground transition-none outline-none focus:outline-none flex-shrink-0' + ); + + const actionButton = ( + + ); + + const projectFileButton = ( + + + + ); + + const settingsButton = onOpenSettings ? ( + + ) : null; + + const attachmentsControls = ( + <> + + {projectFileButton} + {settingsButton} + + ); + + const workingStatusText = working.statusText; + + React.useEffect(() => { + const pendingAbortBanner = Boolean(working.wasAborted); + if (!prevWasAbortedRef.current && pendingAbortBanner && !showAbortStatus) { + startAbortIndicator(); + if (currentSessionId) { + acknowledgeSessionAbort(currentSessionId); + } + } + prevWasAbortedRef.current = pendingAbortBanner; + }, [ + acknowledgeSessionAbort, + currentSessionId, + showAbortStatus, + startAbortIndicator, + working.wasAborted, + ]); + + React.useEffect(() => { + return () => { + if (abortTimeoutRef.current) { + clearTimeout(abortTimeoutRef.current); + abortTimeoutRef.current = null; + } + }; + }, []); + + const shouldRenderPlaceholder = !showAbortStatus && (working.wasAborted || !working.abortActive); + + return ( + +
+
+
+ {showAbortStatus ? ( +
+ + +
+ ) : shouldRenderPlaceholder ? ( + + ) : null} +
+ + {canShowAbortButton ? ( +
+ {isMobile ? ( + + ) : ( + + )} +
+ ) : null} +
+
+ {isDragging && ( +
+
+ +

Drop files here to attach

+
+
+ )} + +
+ {} + {showCommandAutocomplete && ( + setShowCommandAutocomplete(false)} + /> + )} + {} + {showAgentAutocomplete && ( + setShowAgentAutocomplete(false)} + /> + )} + {} + {showFileMention && ( + setShowFileMention(false)} + /> + )} +