From 447ec8ab992943a5461e99bd78d4739eb33b81e4 Mon Sep 17 00:00:00 2001 From: Julien Cabillot Date: Mon, 27 Apr 2026 16:09:15 -0400 Subject: [PATCH] sync --- .../workflows/build-macos-arm64-dmg.yml | 76 + src/.github/workflows/release.yml | 177 +- src/.gitignore | 1 + src/.opencode/package-lock.json | 376 + .../skills/locale-ui-patterns/SKILL.md | 128 + src/AGENTS.md | 244 +- src/CHANGELOG.md | 186 +- src/README.md | 5 + src/bun.lock | 976 +- src/docs/REVERSE_PROXY.md | 337 + src/docs/TAURI_TO_ELECTRON_CUTOVER.md | 349 + src/package.json | 14 +- src/packages/desktop/noop-dist/index.html | 15 +- src/packages/desktop/package.json | 2 +- src/packages/desktop/src-tauri/Cargo.lock | 2021 ++- src/packages/desktop/src-tauri/Cargo.toml | 27 +- src/packages/desktop/src-tauri/src/main.rs | 1769 +- .../desktop/src-tauri/src/remote_ssh.rs | 79 +- .../desktop/src-tauri/tauri.conf.json | 6 +- .../docs/content/docs/reverse-proxy.mdx | 347 + src/packages/docs/sidebar.config.json | 1 + src/packages/electron/.gitignore | 13 + src/packages/electron/main.mjs | 2372 +++ src/packages/electron/package.json | 98 + src/packages/electron/preload.mjs | 146 + .../electron/resources/entitlements.mac.plist | 35 + .../electron/resources/icons/app-icon.png | Bin 0 -> 24006 bytes .../electron/resources/icons/app-icon.svg | 33 + .../electron/resources/icons/dev-icon.icns | Bin 0 -> 173094 bytes .../electron/resources/icons/dev-icon.png | Bin 0 -> 66751 bytes .../electron/resources/icons/icon.icns | Bin 0 -> 160844 bytes .../electron/resources/icons/icon.png | Bin 0 -> 55246 bytes .../electron/scripts/build-web-assets.mjs | 75 + src/packages/electron/scripts/bundle-main.mjs | 44 + .../electron/scripts/electron-dev.mjs | 131 + .../electron/scripts/finalize-latest-yml.mjs | 113 + .../electron/scripts/rebuild-native.mjs | 30 + src/packages/electron/ssh-manager.mjs | 1245 ++ src/packages/ui/package.json | 31 +- src/packages/ui/src/App.tsx | 753 +- .../ui/src/assets/icons/file-types/json.svg | 2 +- .../ui/src/assets/icons/file-types/sprite.svg | 6 +- .../src/components/auth/SessionAuthGate.tsx | 309 +- .../chat/AgentMentionAutocomplete.tsx | 25 +- .../src/components/chat/ChangedFilesList.tsx | 65 + .../ui/src/components/chat/ChatContainer.tsx | 864 +- .../ui/src/components/chat/ChatEmptyState.tsx | 46 +- .../src/components/chat/ChatErrorBoundary.tsx | 49 +- .../ui/src/components/chat/ChatInput.tsx | 1513 +- .../ui/src/components/chat/ChatMessage.tsx | 130 +- .../components/chat/CommandAutocomplete.tsx | 121 +- .../ui/src/components/chat/DiffPreview.tsx | 51 +- .../ui/src/components/chat/FileAttachment.tsx | 44 +- .../chat/FileMentionAutocomplete.tsx | 173 +- .../src/components/chat/MarkdownRenderer.tsx | 1541 +- .../components/chat/MarkdownRendererImpl.tsx | 1553 ++ .../ui/src/components/chat/MessageList.tsx | 1189 +- .../src/components/chat/MobileAgentButton.tsx | 10 +- .../src/components/chat/MobileModelButton.tsx | 3 +- .../chat/MobileSessionStatusBar.tsx | 157 +- .../ui/src/components/chat/ModelControls.tsx | 1803 +- .../src/components/chat/PendingChangesBar.tsx | 150 + .../ui/src/components/chat/PermissionCard.tsx | 112 +- .../src/components/chat/PermissionRequest.tsx | 20 +- .../chat/PermissionToastActions.tsx | 30 +- .../ui/src/components/chat/QuestionCard.tsx | 73 +- .../components/chat/QueuedMessageChips.tsx | 19 +- .../src/components/chat/SkillAutocomplete.tsx | 3 +- .../ui/src/components/chat/StatusChip.tsx | 30 +- .../ui/src/components/chat/StatusRow.tsx | 137 +- .../components/chat/StatusRowContainer.tsx | 44 + .../ui/src/components/chat/TimelineDialog.tsx | 75 +- .../chat/TurnChangedFilesDropdown.tsx | 130 + .../components/chat/UnifiedControlsDrawer.tsx | 258 - .../ui/src/components/chat/changedFiles.ts | 195 + .../components/chat/changedFilesPopover.ts | 10 + .../chat/components/ScrollToBottomButton.tsx | 6 +- .../chat/components/TurnActivity.tsx | 1 + .../components/chat/components/TurnList.tsx | 13 +- .../chat/hooks/useChatTimelineController.ts | 339 +- .../chat/hooks/useChatTurnNavigation.ts | 15 +- .../chat/hooks/useStreamingTextThrottle.ts | 10 +- .../components/chat/hooks/useTurnRecords.ts | 64 +- .../components/chat/lib/blockingRequests.ts | 5 - .../chat/lib/turns/historySignals.ts | 2 +- .../chat/lib/turns/projectTurnActivity.ts | 55 +- .../chat/lib/turns/projectTurnRecords.ts | 132 +- .../chat/lib/turns/stabilizeTurnProjection.ts | 22 + .../components/chat/lib/turns/stageTurns.ts | 4 +- .../ui/src/components/chat/lib/turns/types.ts | 1 + .../components/chat/lib/turns/windowTurns.ts | 113 + .../components/chat/message/MessageBody.tsx | 889 +- .../chat/message/TextSelectionMenu.tsx | 263 +- .../chat/message/ToolOutputDialog.tsx | 79 +- .../chat/message/normalizeUserDisplayParts.ts | 11 +- .../chat/message/parts/AssistantTextPart.tsx | 38 +- .../chat/message/parts/BusyDots.tsx | 25 + .../chat/message/parts/DOCUMENTATION.md | 2 +- .../message/parts/GenericStatusSpinner.tsx | 56 - .../chat/message/parts/JustificationBlock.tsx | 3 + .../message/parts/MinDurationShineText.tsx | 80 +- .../chat/message/parts/ProgressiveGroup.tsx | 250 +- .../chat/message/parts/ReasoningPart.tsx | 45 +- .../chat/message/parts/ToolPart.tsx | 930 +- .../chat/message/parts/UserTextPart.tsx | 65 +- .../chat/message/parts/WorkingPlaceholder.tsx | 35 +- .../resolveFallbackTaskSessionId.test.js | 253 + .../parts/resolveFallbackTaskSessionId.ts | 103 + .../components/chat/message/renderCompare.ts | 305 + .../components/chat/message/toolRenderers.tsx | 143 +- .../components/chat/mobileControlsUtils.ts | 18 + .../components/comments/InlineCommentCard.tsx | 12 +- .../comments/InlineCommentInput.tsx | 21 +- .../comments/useInlineCommentController.ts | 12 +- .../desktop/DesktopHostSwitcher.tsx | 243 +- .../components/desktop/OpenInAppButton.tsx | 24 +- .../components/layout/BottomTerminalDock.tsx | 19 +- .../ui/src/components/layout/ContextPanel.tsx | 55 +- .../components/layout/ContextSidebarTab.tsx | 65 +- .../ui/src/components/layout/Header.tsx | 1483 +- .../ui/src/components/layout/MainLayout.tsx | 446 +- .../layout/ProjectActionsButton.tsx | 63 +- .../components/layout/ProjectEditDialog.tsx | 52 +- .../ui/src/components/layout/RightSidebar.tsx | 31 +- .../components/layout/RightSidebarTabs.tsx | 110 +- .../ui/src/components/layout/Sidebar.tsx | 19 +- .../layout/SidebarContextSummary.tsx | 61 - .../components/layout/SidebarFilesTree.tsx | 178 +- .../ui/src/components/layout/VSCodeLayout.tsx | 270 +- .../ui/src/components/mcp/McpDropdown.tsx | 92 +- .../src/components/multirun/AgentSelector.tsx | 10 +- .../components/multirun/BranchSelector.tsx | 18 +- .../components/multirun/ModelMultiSelect.tsx | 39 +- .../components/multirun/MultiRunLauncher.tsx | 271 +- .../components/onboarding/ChooserScreen.tsx | 422 + .../onboarding/DesktopConnectionRecovery.tsx | 138 + .../onboarding/LocalSetupScreen.tsx | 382 + .../onboarding/OnboardingScreen.tsx | 417 +- .../components/onboarding/RecoveryScreen.tsx | 129 + .../onboarding/RemoteConnectionForm.tsx | 320 + .../onboarding/desktopRecoveryConfig.test.ts | 248 + .../onboarding/desktopRecoveryConfig.ts | 138 + .../onboarding/desktopRecoveryRouting.test.ts | 49 + .../onboarding/desktopRecoveryRouting.ts | 32 + .../components/providers/ThemeProvider.tsx | 5 +- .../sections/SectionPlaceholder.tsx | 4 +- .../components/sections/agents/AgentsPage.tsx | 206 +- .../sections/agents/AgentsSidebar.tsx | 74 +- .../sections/agents/ModelSelector.tsx | 71 +- .../sections/commands/AgentSelector.tsx | 50 +- .../sections/commands/CommandsPage.tsx | 77 +- .../sections/commands/CommandsSidebar.tsx | 77 +- .../GitIdentityEditorDialog.tsx | 108 +- .../sections/git-identities/GitPage.tsx | 56 +- .../magic-prompts/MagicPromptsPage.tsx | 369 + .../magic-prompts/MagicPromptsSidebar.tsx | 90 + .../sections/mcp/McpOAuthCallbackPage.tsx | 124 + .../src/components/sections/mcp/McpPage.tsx | 1481 +- .../components/sections/mcp/McpSidebar.tsx | 129 +- .../src/components/sections/mcp/mcpImport.ts | 339 + .../src/components/sections/mcp/mcpOAuth.ts | 82 + .../sections/openchamber/AboutSettings.tsx | 37 +- .../sections/openchamber/DefaultsSettings.tsx | 61 +- .../openchamber/DesktopNetworkSettings.tsx | 196 + .../sections/openchamber/GitHubSettings.tsx | 64 +- .../sections/openchamber/GitSettings.tsx | 83 +- .../openchamber/KeyboardShortcutsSettings.tsx | 48 +- .../openchamber/NotificationSettings.tsx | 252 +- .../sections/openchamber/OpenChamberPage.tsx | 29 +- .../openchamber/OpenChamberVisualSettings.tsx | 842 +- .../openchamber/OpenCodeCliSettings.tsx | 40 +- .../sections/openchamber/PasskeySettings.tsx | 263 + .../openchamber/SessionRetentionSettings.tsx | 82 +- .../sections/openchamber/TunnelSettings.tsx | 379 +- .../sections/openchamber/VoiceSettings.tsx | 479 +- .../openchamber/WorktreeSectionContent.tsx | 41 +- .../projects/ProjectActionsSection.tsx | 56 +- .../sections/projects/ProjectsPage.tsx | 59 +- .../sections/projects/ProjectsSidebar.tsx | 42 +- .../sections/providers/ProvidersPage.tsx | 179 +- .../sections/providers/ProvidersSidebar.tsx | 18 +- .../remote-instances/RemoteInstancesPage.tsx | 390 +- .../RemoteInstancesSidebar.tsx | 61 +- .../shared/SettingsProjectSelector.tsx | 8 +- .../sections/shared/SettingsSidebarItem.tsx | 4 +- .../components/sections/skills/SkillsPage.tsx | 174 +- .../sections/skills/SkillsSidebar.tsx | 76 +- .../skills/catalog/AddCatalogDialog.tsx | 71 +- .../skills/catalog/InstallConflictsDialog.tsx | 35 +- .../skills/catalog/InstallFromRepoDialog.tsx | 128 +- .../skills/catalog/InstallSkillDialog.tsx | 66 +- .../skills/catalog/SkillsCatalogPage.tsx | 71 +- .../sections/usage/PaceIndicator.tsx | 28 +- .../components/sections/usage/UsagePage.tsx | 48 +- .../sections/usage/UsageSidebar.tsx | 26 +- .../components/session/BranchPickerDialog.tsx | 90 +- .../session/DirectoryExplorerDialog.tsx | 780 +- .../src/components/session/DirectoryTree.tsx | 437 +- .../session/GitHubIntegrationDialog.tsx | 64 +- .../session/GitHubIssuePickerDialog.tsx | 156 +- .../session/GitHubPrPickerDialog.tsx | 122 +- .../components/session/NewWorktreeDialog.tsx | 890 +- .../session/ProjectNotesTodoPanel.tsx | 547 +- .../session/SaveProjectPlanDialog.tsx | 73 + .../session/ScheduledTaskEditorDialog.tsx | 1519 ++ .../session/ScheduledTasksDialog.tsx | 637 + .../src/components/session/SessionDialogs.tsx | 259 +- .../components/session/SessionFolderItem.tsx | 26 +- .../src/components/session/SessionSidebar.tsx | 955 +- .../src/components/session/TodoSendDialog.tsx | 238 + .../session/sidebar/BulkActionBar.tsx | 136 + .../session/sidebar/ConfirmDialogs.tsx | 136 +- .../session/sidebar/SessionGroupSection.tsx | 190 +- .../session/sidebar/SessionNodeItem.tsx | 581 +- .../sidebar/SidebarActivitySections.tsx | 16 +- .../session/sidebar/SidebarFooter.tsx | 19 +- .../session/sidebar/SidebarHeader.tsx | 182 +- .../session/sidebar/SidebarProjectsList.tsx | 11 +- .../session/sidebar/activitySections.ts | 17 + .../sidebar/hooks/useDirectoryStatusProbe.ts | 195 +- .../sidebar/hooks/useProjectRepoStatus.ts | 84 +- .../sidebar/hooks/useProjectSessionLists.ts | 24 +- .../sidebar/hooks/useSessionActions.ts | 52 +- .../sidebar/hooks/useSessionGrouping.ts | 43 +- .../sidebar/hooks/useSessionPrefetch.ts | 53 +- .../hooks/useSessionSidebarSections.ts | 12 +- .../sidebar/hooks/useSidebarPersistence.ts | 8 +- .../session/sidebar/sortableItems.tsx | 60 +- .../src/components/session/sidebar/utils.tsx | 14 + .../components/terminal/TerminalViewport.tsx | 10 +- .../ui/src/components/ui/AboutDialog.tsx | 37 +- .../ui/src/components/ui/CommandPalette.tsx | 428 +- .../src/components/ui/ConfigUpdateOverlay.tsx | 4 +- .../src/components/ui/ContextUsageDisplay.tsx | 22 +- .../ui/src/components/ui/ErrorBoundary.tsx | 62 +- .../ui/src/components/ui/HelpDialog.tsx | 114 +- .../ui/src/components/ui/JsonTreeView.tsx | 102 + .../ui/src/components/ui/JsonTreeViewer.tsx | 304 + .../ui/src/components/ui/MemoryDebugPanel.tsx | 455 +- .../src/components/ui/MobileOverlayPanel.tsx | 9 +- .../ui/src/components/ui/OpenChamberLogo.tsx | 17 +- .../components/ui/OpenCodeStatusDialog.tsx | 24 +- .../ui/src/components/ui/OverlayScrollbar.tsx | 95 +- .../ui/src/components/ui/QuickOpenDialog.tsx | 257 + .../ui/src/components/ui/ScrollShadow.tsx | 13 +- .../src/components/ui/ScrollableOverlay.tsx | 5 +- .../ui/src/components/ui/UpdateDialog.tsx | 44 +- src/packages/ui/src/components/ui/button.tsx | 77 +- .../ui/src/components/ui/checkbox.tsx | 86 +- .../ui/src/components/ui/collapsible.tsx | 44 +- src/packages/ui/src/components/ui/command.tsx | 9 +- src/packages/ui/src/components/ui/dialog.tsx | 96 +- .../ui/src/components/ui/dropdown-menu.tsx | 268 +- .../ui/src/components/ui/fancy-button.tsx | 134 + .../ui/src/components/ui/grid-loader.tsx | 40 - src/packages/ui/src/components/ui/input.tsx | 10 +- .../ui/src/components/ui/number-input.tsx | 10 +- src/packages/ui/src/components/ui/radio.tsx | 28 +- .../ui/src/components/ui/scroll-area.tsx | 22 +- src/packages/ui/src/components/ui/select.tsx | 267 +- .../ui/src/components/ui/separator.tsx | 8 +- src/packages/ui/src/components/ui/slot.tsx | 50 + src/packages/ui/src/components/ui/sonner.tsx | 138 +- .../src/components/ui/sortable-tabs-strip.tsx | 62 +- src/packages/ui/src/components/ui/switch.tsx | 18 +- src/packages/ui/src/components/ui/text.tsx | 36 +- .../ui/src/components/ui/textarea.tsx | 234 +- src/packages/ui/src/components/ui/toggle.tsx | 8 +- src/packages/ui/src/components/ui/tooltip.tsx | 93 +- .../ui/src/components/views/ChatView.tsx | 4 +- .../ui/src/components/views/DiffView.tsx | 194 +- .../ui/src/components/views/FilesView.tsx | 762 +- .../ui/src/components/views/GitView.tsx | 336 +- .../src/components/views/GoToLineDialog.tsx | 256 + .../src/components/views/MultiRunWindow.tsx | 41 +- .../src/components/views/PierreDiffViewer.tsx | 17 +- .../ui/src/components/views/PlanView.tsx | 415 +- .../ui/src/components/views/SettingsView.tsx | 130 +- .../src/components/views/SettingsWindow.tsx | 39 +- .../ui/src/components/views/TerminalView.tsx | 192 +- .../views/agent-manager/AgentGroupDetail.tsx | 199 +- .../agent-manager/AgentManagerEmptyState.tsx | 271 +- .../agent-manager/AgentManagerSidebar.tsx | 150 +- .../views/agent-manager/AgentManagerView.tsx | 127 +- .../components/views/git/AIHighlightsBox.tsx | 17 +- .../views/git/BranchIntegrationSection.tsx | 69 +- .../components/views/git/BranchSelector.tsx | 32 +- .../ui/src/components/views/git/ChangeRow.tsx | 42 +- .../components/views/git/ChangesSection.tsx | 435 +- .../src/components/views/git/CommitInput.tsx | 39 +- .../components/views/git/CommitSection.tsx | 37 +- .../components/views/git/ConflictDialog.tsx | 95 +- .../components/views/git/GitEmptyState.tsx | 10 +- .../ui/src/components/views/git/GitHeader.tsx | 11 +- .../components/views/git/HistoryCommitRow.tsx | 16 +- .../components/views/git/HistorySection.tsx | 16 +- .../views/git/InProgressOperationBanner.tsx | 22 +- .../views/git/IntegrateCommitsSection.tsx | 137 +- .../views/git/PullRequestSection.tsx | 320 +- .../src/components/views/git/StashDialog.tsx | 29 +- .../src/components/views/git/SyncActions.tsx | 32 +- .../views/git/WorktreeBranchDisplay.tsx | 8 +- .../ui/src/components/voice/VoiceProvider.tsx | 18 +- .../components/voice/VoiceStatusIndicator.tsx | 23 +- .../ui/src/contexts/RuntimeAPIProvider.tsx | 143 +- .../ui/src/contexts/ThemeSystemContext.tsx | 20 +- .../ui/src/hooks/useAssistantStatus.ts | 122 +- src/packages/ui/src/hooks/useBrowserVoice.ts | 183 +- .../ui/src/hooks/useChatScrollManager.ts | 330 +- .../ui/src/hooks/useChatSearchDirectory.ts | 20 +- .../ui/src/hooks/useDetectedWorktreeRoot.ts | 116 + src/packages/ui/src/hooks/useEdgeSwipe.ts | 8 +- .../ui/src/hooks/useEffectiveDirectory.ts | 35 +- src/packages/ui/src/hooks/useEventStream.ts | 2676 --- .../ui/src/hooks/useFileSystemAccess.ts | 4 +- .../ui/src/hooks/useFontPreferences.ts | 8 +- .../hooks/useGitHubPrBackgroundTracking.ts | 503 - src/packages/ui/src/hooks/useGitPolling.tsx | 10 - .../ui/src/hooks/useGitPollingHook.ts | 141 - .../ui/src/hooks/useKeyboardShortcuts.ts | 72 +- src/packages/ui/src/hooks/useMenuActions.ts | 111 +- src/packages/ui/src/hooks/useMessageTTS.ts | 44 +- src/packages/ui/src/hooks/useModelLists.ts | 2 +- src/packages/ui/src/hooks/usePlanDetection.ts | 48 + .../ui/src/hooks/usePwaInstallPrompt.ts | 10 + .../ui/src/hooks/usePwaManifestSync.ts | 7 +- .../ui/src/hooks/useQueuedMessageAutoSend.ts | 58 +- src/packages/ui/src/hooks/useRouter.ts | 18 +- src/packages/ui/src/hooks/useScrollEngine.ts | 91 +- .../ui/src/hooks/useServerSessionStatus.ts | 334 - src/packages/ui/src/hooks/useServerTTS.ts | 49 +- .../ui/src/hooks/useSessionActivity.ts | 71 +- .../ui/src/hooks/useSessionAutoCleanup.ts | 89 +- .../ui/src/hooks/useSessionStatusBootstrap.ts | 53 +- .../ui/src/hooks/useTimelineStaging.ts | 141 + src/packages/ui/src/hooks/useVoiceContext.ts | 40 +- .../hooks/useWindowControlsOverlayLayout.ts | 105 + src/packages/ui/src/hooks/useWindowTitle.ts | 15 +- src/packages/ui/src/index.css | 288 +- src/packages/ui/src/lib/api/types.ts | 51 +- src/packages/ui/src/lib/appearanceAutoSave.ts | 26 + src/packages/ui/src/lib/chunkLoadRecovery.ts | 93 + .../ui/src/lib/codemirror/flexokiTheme.ts | 2 - .../src/lib/codemirror/languageByExtension.ts | 120 +- src/packages/ui/src/lib/concurrency.ts | 27 + src/packages/ui/src/lib/debug.ts | 107 +- src/packages/ui/src/lib/desktop.ts | 164 +- src/packages/ui/src/lib/desktopBoot.test.ts | 372 + src/packages/ui/src/lib/desktopBoot.ts | 301 + src/packages/ui/src/lib/desktopHosts.ts | 32 +- src/packages/ui/src/lib/desktopNative.ts | 162 + src/packages/ui/src/lib/device.ts | 15 +- src/packages/ui/src/lib/exportSession.ts | 199 + src/packages/ui/src/lib/fontLoader.ts | 59 + src/packages/ui/src/lib/fontOptions.ts | 142 +- .../src/lib/git/integrateWorktreeCommits.ts | 22 +- src/packages/ui/src/lib/gitApi.ts | 116 +- src/packages/ui/src/lib/gitApiHttp.ts | 50 +- src/packages/ui/src/lib/i18n/context.tsx | 31 + src/packages/ui/src/lib/i18n/index.ts | 4 + .../ui/src/lib/i18n/messages/en.settings.ts | 1516 ++ src/packages/ui/src/lib/i18n/messages/en.ts | 2092 +++ .../ui/src/lib/i18n/messages/es.settings.ts | 1516 ++ src/packages/ui/src/lib/i18n/messages/es.ts | 2091 +++ .../ui/src/lib/i18n/messages/ko.settings.ts | 1516 ++ src/packages/ui/src/lib/i18n/messages/ko.ts | 2091 +++ .../src/lib/i18n/messages/pt-BR.settings.ts | 1516 ++ .../ui/src/lib/i18n/messages/pt-BR.ts | 2091 +++ .../ui/src/lib/i18n/messages/uk.settings.ts | 1516 ++ src/packages/ui/src/lib/i18n/messages/uk.ts | 2091 +++ .../src/lib/i18n/messages/zh-CN.settings.ts | 1516 ++ .../ui/src/lib/i18n/messages/zh-CN.ts | 2091 +++ src/packages/ui/src/lib/i18n/react-context.ts | 14 + src/packages/ui/src/lib/i18n/runtime.ts | 112 + src/packages/ui/src/lib/i18n/store.ts | 92 + src/packages/ui/src/lib/i18n/useI18n.ts | 11 + src/packages/ui/src/lib/jsonTreeUtils.ts | 222 + src/packages/ui/src/lib/magicPrompts.ts | 736 + .../ui/src/lib/messages/messageText.ts | 18 + .../ui/src/lib/messages/synthetic.test.ts | 123 + src/packages/ui/src/lib/openCodeStatus.ts | 38 +- src/packages/ui/src/lib/openInApps.ts | 1 + src/packages/ui/src/lib/openchamberConfig.ts | 440 +- src/packages/ui/src/lib/openchamberEvents.ts | 168 + src/packages/ui/src/lib/opencode/client.ts | 681 +- src/packages/ui/src/lib/passkeys.ts | 200 + src/packages/ui/src/lib/persistence.ts | 246 +- src/packages/ui/src/lib/projectId.ts | 18 + src/packages/ui/src/lib/projectResolution.ts | 69 + .../ui/src/lib/quota/providers/index.ts | 2 + src/packages/ui/src/lib/quota/utils.ts | 5 +- src/packages/ui/src/lib/scheduledTasksApi.ts | 123 + src/packages/ui/src/lib/search/fuzzySearch.ts | 129 + src/packages/ui/src/lib/sessionEvents.ts | 15 + src/packages/ui/src/lib/settings/metadata.ts | 11 +- .../ui/src/lib/shiki/appThemeRegistry.ts | 4 - src/packages/ui/src/lib/shortcuts.ts | 32 +- src/packages/ui/src/lib/terminalApi.ts | 733 +- src/packages/ui/src/lib/theme/cssGenerator.ts | 19 +- .../ui/src/lib/theme/themes/amoled-dark.json | 12 +- .../ui/src/lib/theme/themes/amoled-light.json | 12 +- .../ui/src/lib/theme/themes/aura-dark.json | 17 +- .../ui/src/lib/theme/themes/aura-light.json | 17 +- .../ui/src/lib/theme/themes/ayu-dark.json | 17 +- .../ui/src/lib/theme/themes/ayu-light.json | 17 +- .../src/lib/theme/themes/carbonfox-dark.json | 17 +- .../src/lib/theme/themes/carbonfox-light.json | 17 +- .../src/lib/theme/themes/catppuccin-dark.json | 17 +- .../lib/theme/themes/catppuccin-light.json | 17 +- .../ui/src/lib/theme/themes/cursor-dark.json | 12 +- .../ui/src/lib/theme/themes/cursor-light.json | 12 +- .../ui/src/lib/theme/themes/dracula-dark.json | 17 +- .../src/lib/theme/themes/dracula-light.json | 17 +- .../themes/fields-of-the-shire-dark.json | 23 +- .../themes/fields-of-the-shire-light.json | 19 +- .../ui/src/lib/theme/themes/flexoki-dark.json | 19 +- .../src/lib/theme/themes/flexoki-light.json | 19 +- .../ui/src/lib/theme/themes/github-dark.json | 12 +- .../ui/src/lib/theme/themes/github-light.json | 12 +- .../ui/src/lib/theme/themes/gruvbox-dark.json | 17 +- .../src/lib/theme/themes/gruvbox-light.json | 17 +- .../src/lib/theme/themes/kanagawa-dark.json | 19 +- .../src/lib/theme/themes/kanagawa-light.json | 19 +- .../lib/theme/themes/lucent-orng-dark.json | 12 +- .../lib/theme/themes/lucent-orng-light.json | 12 +- .../ui/src/lib/theme/themes/mono-dark.json | 19 +- .../ui/src/lib/theme/themes/mono-light.json | 19 +- .../src/lib/theme/themes/mono-plus-dark.json | 18 +- .../src/lib/theme/themes/mono-plus-light.json | 18 +- .../ui/src/lib/theme/themes/monokai-dark.json | 17 +- .../src/lib/theme/themes/monokai-light.json | 17 +- .../src/lib/theme/themes/nightowl-dark.json | 17 +- .../src/lib/theme/themes/nightowl-light.json | 17 +- .../ui/src/lib/theme/themes/nord-dark.json | 17 +- .../ui/src/lib/theme/themes/nord-light.json | 17 +- .../ui/src/lib/theme/themes/oc-2-dark.json | 12 +- .../ui/src/lib/theme/themes/oc-2-light.json | 12 +- .../src/lib/theme/themes/onedarkpro-dark.json | 17 +- .../lib/theme/themes/onedarkpro-light.json | 17 +- .../ui/src/lib/theme/themes/orng-dark.json | 12 +- .../ui/src/lib/theme/themes/orng-light.json | 12 +- .../src/lib/theme/themes/rosepine-dark.json | 12 +- .../src/lib/theme/themes/rosepine-light.json | 12 +- .../lib/theme/themes/shadesofpurple-dark.json | 12 +- .../theme/themes/shadesofpurple-light.json | 12 +- .../src/lib/theme/themes/solarized-dark.json | 17 +- .../src/lib/theme/themes/solarized-light.json | 17 +- .../src/lib/theme/themes/tokyonight-dark.json | 17 +- .../lib/theme/themes/tokyonight-light.json | 17 +- .../ui/src/lib/theme/themes/vercel-dark.json | 12 +- .../ui/src/lib/theme/themes/vercel-light.json | 12 +- .../ui/src/lib/theme/themes/vesper-dark.json | 17 +- .../ui/src/lib/theme/themes/vesper-light.json | 17 +- .../lib/theme/themes/vitesse-dark-dark.json | 12 +- .../lib/theme/themes/vitesse-light-light.json | 12 +- .../ui/src/lib/theme/themes/zenburn-dark.json | 12 +- .../src/lib/theme/themes/zenburn-light.json | 12 +- src/packages/ui/src/lib/utils.ts | 29 +- .../ui/src/lib/voice/audioStreamService.ts | 346 + .../ui/src/lib/voice/realtimeClientTools.ts | 15 +- src/packages/ui/src/lib/voice/summarize.ts | 46 +- .../ui/src/lib/worktreeSessionCreator.ts | 25 +- .../ui/src/lib/worktrees/branchSearch.ts | 67 + .../ui/src/lib/worktrees/worktreeManager.ts | 159 +- src/packages/ui/src/main.tsx | 91 +- src/packages/ui/src/stores/DOCUMENTATION.md | 235 + src/packages/ui/src/stores/contextStore.ts | 167 - src/packages/ui/src/stores/globalSessions.ts | 49 +- .../ui/src/stores/messageQueueStore.ts | 8 + src/packages/ui/src/stores/messageStore.ts | 3006 --- src/packages/ui/src/stores/permissionStore.ts | 409 +- src/packages/ui/src/stores/questionStore.ts | 123 - src/packages/ui/src/stores/sessionStore.ts | 1750 -- .../ui/src/stores/types/sessionTypes.ts | 26 +- .../ui/src/stores/useAgentGroupsStore.ts | 951 +- src/packages/ui/src/stores/useConfigStore.ts | 500 +- .../ui/src/stores/useDirectoryStore.ts | 4 +- .../ui/src/stores/useFeatureFlagsStore.ts | 11 + .../ui/src/stores/useGitHubAuthStore.ts | 35 +- .../ui/src/stores/useGitHubPrStatusStore.ts | 157 +- src/packages/ui/src/stores/useGitStore.ts | 412 +- .../ui/src/stores/useGlobalSessionsStore.ts | 349 + .../ui/src/stores/useMagicPromptsStore.ts | 20 + .../ui/src/stores/useMcpConfigStore.ts | 142 +- src/packages/ui/src/stores/useMcpStore.ts | 125 +- .../ui/src/stores/useMultiRunStore.ts | 12 +- .../ui/src/stores/useProjectsStore.ts | 60 +- .../ui/src/stores/useSessionFoldersStore.ts | 173 +- .../src/stores/useSessionMultiSelectStore.ts | 157 + src/packages/ui/src/stores/useSessionStore.ts | 1484 -- .../ui/src/stores/useTerminalStore.ts | 50 +- src/packages/ui/src/stores/useTodoStore.ts | 98 - .../ui/src/stores/useTodosPersistStore.ts | 64 + src/packages/ui/src/stores/useUIStore.ts | 156 +- src/packages/ui/src/stores/useUpdateStore.ts | 4 + .../ui/src/stores/utils/messageUtils.ts | 70 + .../stores/utils/permissionAutoAccept.test.ts | 98 + .../src/stores/utils/permissionAutoAccept.ts | 99 +- .../ui/src/stores/utils/streamDebug.ts | 238 + src/packages/ui/src/styles/design-system.css | 59 +- src/packages/ui/src/styles/mobile.css | 88 +- src/packages/ui/src/styles/typography.css | 12 +- src/packages/ui/src/sync/DOCUMENTATION.md | 230 + .../sync/__tests__/event-pipeline.bench.js | 347 + .../src/sync/__tests__/event-pipeline.test.js | 1040 ++ .../src/sync/__tests__/event-reducer.test.js | 200 + .../src/sync/__tests__/live-aggregate.test.js | 99 + src/packages/ui/src/sync/binary.ts | 46 + src/packages/ui/src/sync/bootstrap.ts | 280 + src/packages/ui/src/sync/child-store.ts | 226 + src/packages/ui/src/sync/content-cache.ts | 93 + src/packages/ui/src/sync/debug.ts | 83 + src/packages/ui/src/sync/event-pipeline.ts | 634 + src/packages/ui/src/sync/event-reducer.ts | 454 + src/packages/ui/src/sync/eviction.ts | 28 + src/packages/ui/src/sync/global-sync-store.ts | 27 + src/packages/ui/src/sync/index.ts | 158 + src/packages/ui/src/sync/input-store.ts | 79 + src/packages/ui/src/sync/live-aggregate.ts | 212 + .../ui/src/sync/notification-store.ts | 170 + src/packages/ui/src/sync/optimistic.ts | 127 + src/packages/ui/src/sync/persist-cache.ts | 116 + .../ui/src/sync/reconnect-recovery.test.ts | 69 + .../ui/src/sync/reconnect-recovery.ts | 62 + src/packages/ui/src/sync/retry.ts | 55 + src/packages/ui/src/sync/sanitize.ts | 86 + src/packages/ui/src/sync/selection-store.ts | 99 + .../ui/src/sync/session-actions.test.ts | 241 + src/packages/ui/src/sync/session-actions.ts | 726 + src/packages/ui/src/sync/session-cache.ts | 61 + .../ui/src/sync/session-prefetch-cache.ts | 113 + .../ui/src/sync/session-ui-store.test.js | 191 + src/packages/ui/src/sync/session-ui-store.ts | 1190 ++ .../sync/session-worktree-contract.test.js | 660 + .../ui/src/sync/session-worktree-contract.ts | 237 + .../src/sync/session-worktree-store.test.js | 71 + .../ui/src/sync/session-worktree-store.ts | 34 + src/packages/ui/src/sync/streaming.ts | 131 + src/packages/ui/src/sync/submit.ts | 143 + src/packages/ui/src/sync/sync-context.tsx | 2054 +++ src/packages/ui/src/sync/sync-refs.ts | 104 + src/packages/ui/src/sync/types.ts | 150 + src/packages/ui/src/sync/use-sync.ts | 413 + src/packages/ui/src/sync/viewport-store.ts | 49 + src/packages/ui/src/sync/voice-store.ts | 23 + src/packages/ui/src/types/bun-test.d.ts | 29 + src/packages/ui/src/types/desktop.d.ts | 4 + src/packages/ui/src/types/quota.ts | 4 +- src/packages/ui/src/types/streamdown.d.ts | 36 - src/packages/ui/src/types/theme.ts | 8 - src/packages/ui/src/types/worktree.ts | 14 + src/packages/vscode/CHANGELOG.md | 77 + src/packages/vscode/package.json | 4 +- .../vscode/src/AgentManagerPanelProvider.ts | 17 +- src/packages/vscode/src/ChatViewProvider.ts | 118 +- src/packages/vscode/src/DOCUMENTATION.md | 61 + .../vscode/src/SessionEditorPanelProvider.ts | 5 +- .../vscode/src/bridge-config-runtime.ts | 631 + .../vscode/src/bridge-fs-helpers-runtime.ts | 563 + src/packages/vscode/src/bridge-fs-runtime.ts | 498 + .../vscode/src/bridge-git-process-runtime.ts | 105 + src/packages/vscode/src/bridge-git-runtime.ts | 438 + .../vscode/src/bridge-git-special-runtime.ts | 455 + .../src/bridge-localfs-proxy-runtime.ts | 117 + .../vscode/src/bridge-proxy-runtime.ts | 180 + .../vscode/src/bridge-settings-runtime.ts | 387 + .../vscode/src/bridge-system-runtime.ts | 558 + src/packages/vscode/src/bridge.ts | 3903 +--- src/packages/vscode/src/gitService.ts | 157 + src/packages/vscode/src/opencode-ready.ts | 59 + src/packages/vscode/src/opencode.ts | 113 +- src/packages/vscode/src/opencodeConfig.ts | 21 +- src/packages/vscode/src/pathUtils.ts | 9 + src/packages/vscode/src/quotaProviders.ts | 382 + .../vscode/src/sessionActivityWatcher.ts | 8 + src/packages/vscode/src/sseProxy.ts | 124 +- src/packages/vscode/src/webviewHtml.ts | 10 +- src/packages/vscode/webview/api/bridge.ts | 5 + src/packages/vscode/webview/api/files.ts | 29 +- src/packages/vscode/webview/api/git.ts | 36 + src/packages/vscode/webview/api/streamPerf.ts | 88 + src/packages/vscode/webview/main.tsx | 227 +- src/packages/web/bin/cli.js | 167 +- src/packages/web/bin/cli.test.js | 2 +- src/packages/web/index.html | 74 +- src/packages/web/package.json | 82 +- src/packages/web/public/site.webmanifest | 1 - .../web/server/TERMINAL_INPUT_WS_PROTOCOL.md | 44 - .../web/server/TERMINAL_WS_PROTOCOL.md | 48 + src/packages/web/server/index.js | 15137 +--------------- .../server/lib/event-stream/DOCUMENTATION.md | 61 + .../lib/event-stream/directory-ws-bridge.js | 185 + .../web/server/lib/event-stream/global-hub.js | 143 + .../lib/event-stream/global-ws-bridge.js | 206 + .../web/server/lib/event-stream/index.js | 24 + .../web/server/lib/event-stream/protocol.js | 100 + .../server/lib/event-stream/protocol.test.js | 114 + .../web/server/lib/event-stream/runtime.js | 180 + .../server/lib/event-stream/runtime.test.js | 512 + .../lib/event-stream/upstream-reader.js | 204 + .../lib/event-stream/upstream-reader.test.js | 159 + .../web/server/lib/fs/DOCUMENTATION.md | 36 + src/packages/web/server/lib/fs/routes.js | 831 + src/packages/web/server/lib/fs/search.js | 238 + .../web/server/lib/git/DOCUMENTATION.md | 1 + src/packages/web/server/lib/git/routes.js | 906 + src/packages/web/server/lib/git/service.js | 178 +- .../web/server/lib/github/DOCUMENTATION.md | 1 + .../web/server/lib/github/pr-status.js | 21 +- src/packages/web/server/lib/github/routes.js | 1349 ++ .../web/server/lib/magic-prompts/routes.js | 63 + .../web/server/lib/magic-prompts/runtime.js | 119 + .../server/lib/notifications/DOCUMENTATION.md | 70 +- .../lib/notifications/emitter-runtime.js | 102 + .../web/server/lib/notifications/index.js | 3 + .../web/server/lib/notifications/message.js | 23 +- .../server/lib/notifications/message.test.js | 17 +- .../server/lib/notifications/push-runtime.js | 304 + .../web/server/lib/notifications/routes.js | 315 + .../web/server/lib/notifications/runtime.js | 517 + .../lib/notifications/template-runtime.js | 415 + .../web/server/lib/opencode/DOCUMENTATION.md | 325 +- .../server/lib/opencode/auth-state-runtime.js | 88 + .../server/lib/opencode/bootstrap-runtime.js | 130 + .../server/lib/opencode/cli-entry-runtime.js | 43 + .../web/server/lib/opencode/cli-options.js | 128 + .../lib/opencode/config-entity-routes.js | 370 + .../web/server/lib/opencode/core-routes.js | 284 + .../server/lib/opencode/core-routes.test.js | 26 + .../web/server/lib/opencode/env-config.js | 72 + .../web/server/lib/opencode/env-runtime.js | 1082 ++ .../lib/opencode/feature-routes-runtime.js | 242 + .../server/lib/opencode/hmr-state-runtime.js | 85 + src/packages/web/server/lib/opencode/index.js | 2 +- .../web/server/lib/opencode/lifecycle.js | 781 + .../web/server/lib/opencode/lifecycle.test.js | 166 + src/packages/web/server/lib/opencode/mcp.js | 76 +- .../server/lib/opencode/network-runtime.js | 98 + .../server/lib/opencode/openchamber-routes.js | 313 + .../opencode/opencode-resolution-runtime.js | 71 + .../web/server/lib/opencode/path-utils.js | 100 + .../server/lib/opencode/path-utils.test.js | 71 + .../lib/opencode/project-directory-runtime.js | 124 + .../lib/opencode/project-icon-routes.js | 397 + src/packages/web/server/lib/opencode/proxy.js | 420 + .../lib/opencode/pwa-manifest-routes.js | 257 + .../web/server/lib/opencode/routes.js | 298 + .../lib/opencode/server-startup-runtime.js | 138 + .../lib/opencode/server-utils-runtime.js | 173 + .../lib/opencode/server-utils-runtime.test.js | 123 + .../server/lib/opencode/session-runtime.js | 343 + .../lib/opencode/session-runtime.test.js | 68 + .../server/lib/opencode/settings-helpers.js | 653 + .../lib/opencode/settings-helpers.test.js | 54 + .../settings-normalization-runtime.js | 428 + .../server/lib/opencode/settings-runtime.js | 825 + .../web/server/lib/opencode/shared.js | 21 +- .../server/lib/opencode/shutdown-runtime.js | 132 + .../web/server/lib/opencode/skill-routes.js | 707 + .../lib/opencode/startup-pipeline-runtime.js | 128 + .../lib/opencode/static-routes-runtime.js | 65 + .../web/server/lib/opencode/theme-runtime.js | 167 + .../lib/opencode/tunnel-wiring-runtime.js | 94 + .../web/server/lib/opencode/watcher.js | 115 + .../web/server/lib/opencode/watcher.test.js | 239 + .../web/server/lib/package-manager.js | 368 +- .../web/server/lib/projects/project-config.js | 565 + .../lib/projects/project-config.test.js | 144 + .../web/server/lib/projects/project-id.js | 13 + .../web/server/lib/quota/DOCUMENTATION.md | 3 + src/packages/web/server/lib/quota/index.js | 3 +- .../web/server/lib/quota/providers/copilot.js | 2 +- .../web/server/lib/quota/providers/index.js | 16 + .../quota/providers/minimax-cn-coding-plan.js | 151 +- .../quota/providers/minimax-coding-plan.js | 150 +- .../lib/quota/providers/minimax-shared.js | 136 - .../quota/providers/zhipuai-coding-plan.js | 133 + .../web/server/lib/quota/providers/zhipuai.js | 114 + src/packages/web/server/lib/quota/routes.js | 27 + .../lib/scheduled-tasks/DOCUMENTATION.md | 44 + .../web/server/lib/scheduled-tasks/routes.js | 231 + .../web/server/lib/scheduled-tasks/runtime.js | 749 + .../lib/scheduled-tasks/runtime.test.js | 100 + .../server/lib/security/request-security.js | 115 + .../web/server/lib/session-folders/routes.js | 57 + .../web/server/lib/terminal/DOCUMENTATION.md | 122 +- src/packages/web/server/lib/terminal/index.js | 35 +- .../lib/terminal/output-replay-buffer.js | 66 + .../lib/terminal/output-replay-buffer.test.js | 66 + .../web/server/lib/terminal/runtime.js | 801 + ...ws-protocol.js => terminal-ws-protocol.js} | 24 +- ...l.test.js => terminal-ws-protocol.test.js} | 73 +- .../web/server/lib/text/DOCUMENTATION.md | 35 + .../web/server/lib/text/summarization.js | 240 + .../web/server/lib/tts/DOCUMENTATION.md | 24 +- src/packages/web/server/lib/tts/base-url.js | 62 + .../web/server/lib/tts/capability-runtime.js | 31 + src/packages/web/server/lib/tts/index.js | 5 +- src/packages/web/server/lib/tts/routes.js | 275 + src/packages/web/server/lib/tts/service.js | 52 +- src/packages/web/server/lib/tts/stt.js | 75 + .../web/server/lib/tts/summarization.js | 171 - .../web/server/lib/tunnels/DOCUMENTATION.md | 18 + .../web/server/lib/tunnels/managed-config.js | 201 + src/packages/web/server/lib/tunnels/routes.js | 605 + .../web/server/lib/ui-auth/DOCUMENTATION.md | 38 + .../lib/{opencode => ui-auth}/ui-auth.js | 189 +- .../web/server/lib/ui-auth/ui-passkeys.js | 545 + .../web/server/opencode-proxy.test.js | 151 + src/packages/web/server/proxy-headers.js | 61 + src/packages/web/server/proxy-headers.test.js | 58 + src/packages/web/server/sse-routes.test.js | 152 + src/packages/web/src/api/files.ts | 29 + src/packages/web/src/api/git.ts | 2 + src/packages/web/src/api/notifications.ts | 62 +- src/packages/web/src/main.tsx | 83 +- src/packages/web/vite.config.ts | 5 +- src/scripts/bump-version.mjs | 1 + src/scripts/dev-web-hmr.mjs | 20 +- src/vite.config.ts | 2 +- 720 files changed, 118619 insertions(+), 48612 deletions(-) create mode 100644 src/.opencode/package-lock.json create mode 100644 src/.opencode/skills/locale-ui-patterns/SKILL.md create mode 100644 src/docs/REVERSE_PROXY.md create mode 100644 src/docs/TAURI_TO_ELECTRON_CUTOVER.md create mode 100644 src/packages/docs/content/docs/reverse-proxy.mdx create mode 100644 src/packages/electron/.gitignore create mode 100644 src/packages/electron/main.mjs create mode 100644 src/packages/electron/package.json create mode 100644 src/packages/electron/preload.mjs create mode 100644 src/packages/electron/resources/entitlements.mac.plist create mode 100644 src/packages/electron/resources/icons/app-icon.png create mode 100644 src/packages/electron/resources/icons/app-icon.svg create mode 100644 src/packages/electron/resources/icons/dev-icon.icns create mode 100644 src/packages/electron/resources/icons/dev-icon.png create mode 100644 src/packages/electron/resources/icons/icon.icns create mode 100644 src/packages/electron/resources/icons/icon.png create mode 100644 src/packages/electron/scripts/build-web-assets.mjs create mode 100644 src/packages/electron/scripts/bundle-main.mjs create mode 100644 src/packages/electron/scripts/electron-dev.mjs create mode 100644 src/packages/electron/scripts/finalize-latest-yml.mjs create mode 100644 src/packages/electron/scripts/rebuild-native.mjs create mode 100644 src/packages/electron/ssh-manager.mjs create mode 100644 src/packages/ui/src/components/chat/ChangedFilesList.tsx create mode 100644 src/packages/ui/src/components/chat/MarkdownRendererImpl.tsx create mode 100644 src/packages/ui/src/components/chat/PendingChangesBar.tsx create mode 100644 src/packages/ui/src/components/chat/StatusRowContainer.tsx create mode 100644 src/packages/ui/src/components/chat/TurnChangedFilesDropdown.tsx delete mode 100644 src/packages/ui/src/components/chat/UnifiedControlsDrawer.tsx create mode 100644 src/packages/ui/src/components/chat/changedFiles.ts create mode 100644 src/packages/ui/src/components/chat/changedFilesPopover.ts create mode 100644 src/packages/ui/src/components/chat/message/parts/BusyDots.tsx delete mode 100644 src/packages/ui/src/components/chat/message/parts/GenericStatusSpinner.tsx create mode 100644 src/packages/ui/src/components/chat/message/parts/__tests__/resolveFallbackTaskSessionId.test.js create mode 100644 src/packages/ui/src/components/chat/message/parts/resolveFallbackTaskSessionId.ts create mode 100644 src/packages/ui/src/components/chat/message/renderCompare.ts delete mode 100644 src/packages/ui/src/components/layout/SidebarContextSummary.tsx create mode 100644 src/packages/ui/src/components/onboarding/ChooserScreen.tsx create mode 100644 src/packages/ui/src/components/onboarding/DesktopConnectionRecovery.tsx create mode 100644 src/packages/ui/src/components/onboarding/LocalSetupScreen.tsx create mode 100644 src/packages/ui/src/components/onboarding/RecoveryScreen.tsx create mode 100644 src/packages/ui/src/components/onboarding/RemoteConnectionForm.tsx create mode 100644 src/packages/ui/src/components/onboarding/desktopRecoveryConfig.test.ts create mode 100644 src/packages/ui/src/components/onboarding/desktopRecoveryConfig.ts create mode 100644 src/packages/ui/src/components/onboarding/desktopRecoveryRouting.test.ts create mode 100644 src/packages/ui/src/components/onboarding/desktopRecoveryRouting.ts create mode 100644 src/packages/ui/src/components/sections/magic-prompts/MagicPromptsPage.tsx create mode 100644 src/packages/ui/src/components/sections/magic-prompts/MagicPromptsSidebar.tsx create mode 100644 src/packages/ui/src/components/sections/mcp/McpOAuthCallbackPage.tsx create mode 100644 src/packages/ui/src/components/sections/mcp/mcpImport.ts create mode 100644 src/packages/ui/src/components/sections/mcp/mcpOAuth.ts create mode 100644 src/packages/ui/src/components/sections/openchamber/DesktopNetworkSettings.tsx create mode 100644 src/packages/ui/src/components/sections/openchamber/PasskeySettings.tsx create mode 100644 src/packages/ui/src/components/session/SaveProjectPlanDialog.tsx create mode 100644 src/packages/ui/src/components/session/ScheduledTaskEditorDialog.tsx create mode 100644 src/packages/ui/src/components/session/ScheduledTasksDialog.tsx create mode 100644 src/packages/ui/src/components/session/TodoSendDialog.tsx create mode 100644 src/packages/ui/src/components/session/sidebar/BulkActionBar.tsx create mode 100644 src/packages/ui/src/components/ui/JsonTreeView.tsx create mode 100644 src/packages/ui/src/components/ui/JsonTreeViewer.tsx create mode 100644 src/packages/ui/src/components/ui/QuickOpenDialog.tsx create mode 100644 src/packages/ui/src/components/ui/fancy-button.tsx delete mode 100644 src/packages/ui/src/components/ui/grid-loader.tsx create mode 100644 src/packages/ui/src/components/ui/slot.tsx create mode 100644 src/packages/ui/src/components/views/GoToLineDialog.tsx create mode 100644 src/packages/ui/src/hooks/useDetectedWorktreeRoot.ts delete mode 100644 src/packages/ui/src/hooks/useEventStream.ts delete mode 100644 src/packages/ui/src/hooks/useGitHubPrBackgroundTracking.ts delete mode 100644 src/packages/ui/src/hooks/useGitPolling.tsx delete mode 100644 src/packages/ui/src/hooks/useGitPollingHook.ts create mode 100644 src/packages/ui/src/hooks/usePlanDetection.ts delete mode 100644 src/packages/ui/src/hooks/useServerSessionStatus.ts create mode 100644 src/packages/ui/src/hooks/useTimelineStaging.ts create mode 100644 src/packages/ui/src/hooks/useWindowControlsOverlayLayout.ts create mode 100644 src/packages/ui/src/lib/chunkLoadRecovery.ts create mode 100644 src/packages/ui/src/lib/concurrency.ts create mode 100644 src/packages/ui/src/lib/desktopBoot.test.ts create mode 100644 src/packages/ui/src/lib/desktopBoot.ts create mode 100644 src/packages/ui/src/lib/desktopNative.ts create mode 100644 src/packages/ui/src/lib/exportSession.ts create mode 100644 src/packages/ui/src/lib/fontLoader.ts create mode 100644 src/packages/ui/src/lib/i18n/context.tsx create mode 100644 src/packages/ui/src/lib/i18n/index.ts create mode 100644 src/packages/ui/src/lib/i18n/messages/en.settings.ts create mode 100644 src/packages/ui/src/lib/i18n/messages/en.ts create mode 100644 src/packages/ui/src/lib/i18n/messages/es.settings.ts create mode 100644 src/packages/ui/src/lib/i18n/messages/es.ts create mode 100644 src/packages/ui/src/lib/i18n/messages/ko.settings.ts create mode 100644 src/packages/ui/src/lib/i18n/messages/ko.ts create mode 100644 src/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts create mode 100644 src/packages/ui/src/lib/i18n/messages/pt-BR.ts create mode 100644 src/packages/ui/src/lib/i18n/messages/uk.settings.ts create mode 100644 src/packages/ui/src/lib/i18n/messages/uk.ts create mode 100644 src/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts create mode 100644 src/packages/ui/src/lib/i18n/messages/zh-CN.ts create mode 100644 src/packages/ui/src/lib/i18n/react-context.ts create mode 100644 src/packages/ui/src/lib/i18n/runtime.ts create mode 100644 src/packages/ui/src/lib/i18n/store.ts create mode 100644 src/packages/ui/src/lib/i18n/useI18n.ts create mode 100644 src/packages/ui/src/lib/jsonTreeUtils.ts create mode 100644 src/packages/ui/src/lib/magicPrompts.ts create mode 100644 src/packages/ui/src/lib/messages/synthetic.test.ts create mode 100644 src/packages/ui/src/lib/openchamberEvents.ts create mode 100644 src/packages/ui/src/lib/passkeys.ts create mode 100644 src/packages/ui/src/lib/projectId.ts create mode 100644 src/packages/ui/src/lib/projectResolution.ts create mode 100644 src/packages/ui/src/lib/scheduledTasksApi.ts create mode 100644 src/packages/ui/src/lib/search/fuzzySearch.ts create mode 100644 src/packages/ui/src/lib/voice/audioStreamService.ts create mode 100644 src/packages/ui/src/lib/worktrees/branchSearch.ts create mode 100644 src/packages/ui/src/stores/DOCUMENTATION.md delete mode 100644 src/packages/ui/src/stores/messageStore.ts delete mode 100644 src/packages/ui/src/stores/questionStore.ts delete mode 100644 src/packages/ui/src/stores/sessionStore.ts create mode 100644 src/packages/ui/src/stores/useFeatureFlagsStore.ts create mode 100644 src/packages/ui/src/stores/useGlobalSessionsStore.ts create mode 100644 src/packages/ui/src/stores/useMagicPromptsStore.ts create mode 100644 src/packages/ui/src/stores/useSessionMultiSelectStore.ts delete mode 100644 src/packages/ui/src/stores/useSessionStore.ts delete mode 100644 src/packages/ui/src/stores/useTodoStore.ts create mode 100644 src/packages/ui/src/stores/useTodosPersistStore.ts create mode 100644 src/packages/ui/src/stores/utils/permissionAutoAccept.test.ts create mode 100644 src/packages/ui/src/sync/DOCUMENTATION.md create mode 100644 src/packages/ui/src/sync/__tests__/event-pipeline.bench.js create mode 100644 src/packages/ui/src/sync/__tests__/event-pipeline.test.js create mode 100644 src/packages/ui/src/sync/__tests__/event-reducer.test.js create mode 100644 src/packages/ui/src/sync/__tests__/live-aggregate.test.js create mode 100644 src/packages/ui/src/sync/binary.ts create mode 100644 src/packages/ui/src/sync/bootstrap.ts create mode 100644 src/packages/ui/src/sync/child-store.ts create mode 100644 src/packages/ui/src/sync/content-cache.ts create mode 100644 src/packages/ui/src/sync/debug.ts create mode 100644 src/packages/ui/src/sync/event-pipeline.ts create mode 100644 src/packages/ui/src/sync/event-reducer.ts create mode 100644 src/packages/ui/src/sync/eviction.ts create mode 100644 src/packages/ui/src/sync/global-sync-store.ts create mode 100644 src/packages/ui/src/sync/index.ts create mode 100644 src/packages/ui/src/sync/input-store.ts create mode 100644 src/packages/ui/src/sync/live-aggregate.ts create mode 100644 src/packages/ui/src/sync/notification-store.ts create mode 100644 src/packages/ui/src/sync/optimistic.ts create mode 100644 src/packages/ui/src/sync/persist-cache.ts create mode 100644 src/packages/ui/src/sync/reconnect-recovery.test.ts create mode 100644 src/packages/ui/src/sync/reconnect-recovery.ts create mode 100644 src/packages/ui/src/sync/retry.ts create mode 100644 src/packages/ui/src/sync/sanitize.ts create mode 100644 src/packages/ui/src/sync/selection-store.ts create mode 100644 src/packages/ui/src/sync/session-actions.test.ts create mode 100644 src/packages/ui/src/sync/session-actions.ts create mode 100644 src/packages/ui/src/sync/session-cache.ts create mode 100644 src/packages/ui/src/sync/session-prefetch-cache.ts create mode 100644 src/packages/ui/src/sync/session-ui-store.test.js create mode 100644 src/packages/ui/src/sync/session-ui-store.ts create mode 100644 src/packages/ui/src/sync/session-worktree-contract.test.js create mode 100644 src/packages/ui/src/sync/session-worktree-contract.ts create mode 100644 src/packages/ui/src/sync/session-worktree-store.test.js create mode 100644 src/packages/ui/src/sync/session-worktree-store.ts create mode 100644 src/packages/ui/src/sync/streaming.ts create mode 100644 src/packages/ui/src/sync/submit.ts create mode 100644 src/packages/ui/src/sync/sync-context.tsx create mode 100644 src/packages/ui/src/sync/sync-refs.ts create mode 100644 src/packages/ui/src/sync/types.ts create mode 100644 src/packages/ui/src/sync/use-sync.ts create mode 100644 src/packages/ui/src/sync/viewport-store.ts create mode 100644 src/packages/ui/src/sync/voice-store.ts create mode 100644 src/packages/ui/src/types/bun-test.d.ts delete mode 100644 src/packages/ui/src/types/streamdown.d.ts create mode 100644 src/packages/vscode/src/DOCUMENTATION.md create mode 100644 src/packages/vscode/src/bridge-config-runtime.ts create mode 100644 src/packages/vscode/src/bridge-fs-helpers-runtime.ts create mode 100644 src/packages/vscode/src/bridge-fs-runtime.ts create mode 100644 src/packages/vscode/src/bridge-git-process-runtime.ts create mode 100644 src/packages/vscode/src/bridge-git-runtime.ts create mode 100644 src/packages/vscode/src/bridge-git-special-runtime.ts create mode 100644 src/packages/vscode/src/bridge-localfs-proxy-runtime.ts create mode 100644 src/packages/vscode/src/bridge-proxy-runtime.ts create mode 100644 src/packages/vscode/src/bridge-settings-runtime.ts create mode 100644 src/packages/vscode/src/bridge-system-runtime.ts create mode 100644 src/packages/vscode/src/opencode-ready.ts create mode 100644 src/packages/vscode/src/pathUtils.ts create mode 100644 src/packages/vscode/webview/api/streamPerf.ts delete mode 100644 src/packages/web/server/TERMINAL_INPUT_WS_PROTOCOL.md create mode 100644 src/packages/web/server/TERMINAL_WS_PROTOCOL.md create mode 100644 src/packages/web/server/lib/event-stream/DOCUMENTATION.md create mode 100644 src/packages/web/server/lib/event-stream/directory-ws-bridge.js create mode 100644 src/packages/web/server/lib/event-stream/global-hub.js create mode 100644 src/packages/web/server/lib/event-stream/global-ws-bridge.js create mode 100644 src/packages/web/server/lib/event-stream/index.js create mode 100644 src/packages/web/server/lib/event-stream/protocol.js create mode 100644 src/packages/web/server/lib/event-stream/protocol.test.js create mode 100644 src/packages/web/server/lib/event-stream/runtime.js create mode 100644 src/packages/web/server/lib/event-stream/runtime.test.js create mode 100644 src/packages/web/server/lib/event-stream/upstream-reader.js create mode 100644 src/packages/web/server/lib/event-stream/upstream-reader.test.js create mode 100644 src/packages/web/server/lib/fs/DOCUMENTATION.md create mode 100644 src/packages/web/server/lib/fs/routes.js create mode 100644 src/packages/web/server/lib/fs/search.js create mode 100644 src/packages/web/server/lib/git/routes.js create mode 100644 src/packages/web/server/lib/github/routes.js create mode 100644 src/packages/web/server/lib/magic-prompts/routes.js create mode 100644 src/packages/web/server/lib/magic-prompts/runtime.js create mode 100644 src/packages/web/server/lib/notifications/emitter-runtime.js create mode 100644 src/packages/web/server/lib/notifications/push-runtime.js create mode 100644 src/packages/web/server/lib/notifications/routes.js create mode 100644 src/packages/web/server/lib/notifications/runtime.js create mode 100644 src/packages/web/server/lib/notifications/template-runtime.js create mode 100644 src/packages/web/server/lib/opencode/auth-state-runtime.js create mode 100644 src/packages/web/server/lib/opencode/bootstrap-runtime.js create mode 100644 src/packages/web/server/lib/opencode/cli-entry-runtime.js create mode 100644 src/packages/web/server/lib/opencode/cli-options.js create mode 100644 src/packages/web/server/lib/opencode/config-entity-routes.js create mode 100644 src/packages/web/server/lib/opencode/core-routes.js create mode 100644 src/packages/web/server/lib/opencode/core-routes.test.js create mode 100644 src/packages/web/server/lib/opencode/env-config.js create mode 100644 src/packages/web/server/lib/opencode/env-runtime.js create mode 100644 src/packages/web/server/lib/opencode/feature-routes-runtime.js create mode 100644 src/packages/web/server/lib/opencode/hmr-state-runtime.js create mode 100644 src/packages/web/server/lib/opencode/lifecycle.js create mode 100644 src/packages/web/server/lib/opencode/lifecycle.test.js create mode 100644 src/packages/web/server/lib/opencode/network-runtime.js create mode 100644 src/packages/web/server/lib/opencode/openchamber-routes.js create mode 100644 src/packages/web/server/lib/opencode/opencode-resolution-runtime.js create mode 100644 src/packages/web/server/lib/opencode/path-utils.js create mode 100644 src/packages/web/server/lib/opencode/path-utils.test.js create mode 100644 src/packages/web/server/lib/opencode/project-directory-runtime.js create mode 100644 src/packages/web/server/lib/opencode/project-icon-routes.js create mode 100644 src/packages/web/server/lib/opencode/proxy.js create mode 100644 src/packages/web/server/lib/opencode/pwa-manifest-routes.js create mode 100644 src/packages/web/server/lib/opencode/routes.js create mode 100644 src/packages/web/server/lib/opencode/server-startup-runtime.js create mode 100644 src/packages/web/server/lib/opencode/server-utils-runtime.js create mode 100644 src/packages/web/server/lib/opencode/server-utils-runtime.test.js create mode 100644 src/packages/web/server/lib/opencode/session-runtime.js create mode 100644 src/packages/web/server/lib/opencode/session-runtime.test.js create mode 100644 src/packages/web/server/lib/opencode/settings-helpers.js create mode 100644 src/packages/web/server/lib/opencode/settings-helpers.test.js create mode 100644 src/packages/web/server/lib/opencode/settings-normalization-runtime.js create mode 100644 src/packages/web/server/lib/opencode/settings-runtime.js create mode 100644 src/packages/web/server/lib/opencode/shutdown-runtime.js create mode 100644 src/packages/web/server/lib/opencode/skill-routes.js create mode 100644 src/packages/web/server/lib/opencode/startup-pipeline-runtime.js create mode 100644 src/packages/web/server/lib/opencode/static-routes-runtime.js create mode 100644 src/packages/web/server/lib/opencode/theme-runtime.js create mode 100644 src/packages/web/server/lib/opencode/tunnel-wiring-runtime.js create mode 100644 src/packages/web/server/lib/opencode/watcher.js create mode 100644 src/packages/web/server/lib/opencode/watcher.test.js create mode 100644 src/packages/web/server/lib/projects/project-config.js create mode 100644 src/packages/web/server/lib/projects/project-config.test.js create mode 100644 src/packages/web/server/lib/projects/project-id.js delete mode 100644 src/packages/web/server/lib/quota/providers/minimax-shared.js create mode 100644 src/packages/web/server/lib/quota/providers/zhipuai-coding-plan.js create mode 100644 src/packages/web/server/lib/quota/providers/zhipuai.js create mode 100644 src/packages/web/server/lib/quota/routes.js create mode 100644 src/packages/web/server/lib/scheduled-tasks/DOCUMENTATION.md create mode 100644 src/packages/web/server/lib/scheduled-tasks/routes.js create mode 100644 src/packages/web/server/lib/scheduled-tasks/runtime.js create mode 100644 src/packages/web/server/lib/scheduled-tasks/runtime.test.js create mode 100644 src/packages/web/server/lib/security/request-security.js create mode 100644 src/packages/web/server/lib/session-folders/routes.js create mode 100644 src/packages/web/server/lib/terminal/output-replay-buffer.js create mode 100644 src/packages/web/server/lib/terminal/output-replay-buffer.test.js create mode 100644 src/packages/web/server/lib/terminal/runtime.js rename src/packages/web/server/lib/terminal/{input-ws-protocol.js => terminal-ws-protocol.js} (59%) rename src/packages/web/server/lib/terminal/{input-ws-protocol.test.js => terminal-ws-protocol.test.js} (57%) create mode 100644 src/packages/web/server/lib/text/DOCUMENTATION.md create mode 100644 src/packages/web/server/lib/text/summarization.js create mode 100644 src/packages/web/server/lib/tts/base-url.js create mode 100644 src/packages/web/server/lib/tts/capability-runtime.js create mode 100644 src/packages/web/server/lib/tts/routes.js create mode 100644 src/packages/web/server/lib/tts/stt.js delete mode 100644 src/packages/web/server/lib/tts/summarization.js create mode 100644 src/packages/web/server/lib/tunnels/DOCUMENTATION.md create mode 100644 src/packages/web/server/lib/tunnels/managed-config.js create mode 100644 src/packages/web/server/lib/tunnels/routes.js create mode 100644 src/packages/web/server/lib/ui-auth/DOCUMENTATION.md rename src/packages/web/server/lib/{opencode => ui-auth}/ui-auth.js (68%) create mode 100644 src/packages/web/server/lib/ui-auth/ui-passkeys.js create mode 100644 src/packages/web/server/opencode-proxy.test.js create mode 100644 src/packages/web/server/proxy-headers.js create mode 100644 src/packages/web/server/proxy-headers.test.js create mode 100644 src/packages/web/server/sse-routes.test.js diff --git a/src/.github/workflows/build-macos-arm64-dmg.yml b/src/.github/workflows/build-macos-arm64-dmg.yml index 551ccca..71aeaa0 100644 --- a/src/.github/workflows/build-macos-arm64-dmg.yml +++ b/src/.github/workflows/build-macos-arm64-dmg.yml @@ -123,3 +123,79 @@ jobs: name: dmg-${{ inputs.macos_version }}-arm64 path: artifacts/*.dmg retention-days: 7 + + build-macos-dmg-arm64-electron: + name: Build Electron DMG (arm64, ${{ inputs.macos_version }}) + runs-on: ${{ inputs.macos_version }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref || github.ref }} + + - name: Setup bun + uses: oven-sh/setup-bun@v2 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Install Apple Certificate + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + run: | + KEYCHAIN_PATH=$RUNNER_TEMP/electron-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" + + 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: Build Electron app (arm64) + working-directory: packages/electron + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + ELECTRON_BUILDER_ARCH: arm64 + run: | + bun run build:web-assets + bun run bundle:main + bun run rebuild:native + ./node_modules/.bin/electron-builder --mac --arm64 --publish=never + + - name: Prepare DMG artifact + run: | + set -euo pipefail + mkdir -p artifacts + DMG_PATH="packages/electron/dist/*.dmg" + if ls $DMG_PATH 1> /dev/null 2>&1; then + DMG_FILE=$(ls $DMG_PATH | head -n 1) + DMG_NAME="OpenChamber_Electron_${{ inputs.macos_version }}_arm64.dmg" + cp "$DMG_FILE" "artifacts/$DMG_NAME" + else + echo "Error: DMG file not found at $DMG_PATH" + exit 1 + fi + + - name: Upload DMG artifact + uses: actions/upload-artifact@v4 + with: + name: dmg-electron-${{ inputs.macos_version }}-arm64 + path: artifacts/*.dmg + retention-days: 7 diff --git a/src/.github/workflows/release.yml b/src/.github/workflows/release.yml index f29c3c1..b1865b1 100644 --- a/src/.github/workflows/release.yml +++ b/src/.github/workflows/release.yml @@ -494,11 +494,174 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + build-desktop-electron-macos: + needs: create-release + runs-on: macos-26 + strategy: + fail-fast: false + matrix: + include: + - target: aarch64-apple-darwin + arch: arm64 + platform: darwin-aarch64 + - target: x86_64-apple-darwin + arch: x64 + platform: darwin-x86_64 + steps: + - uses: actions/checkout@v4 + + - name: Setup bun + uses: oven-sh/setup-bun@v2 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Install Apple Certificate + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + run: | + KEYCHAIN_PATH=$RUNNER_TEMP/electron-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" + + 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: Build Electron app + working-directory: packages/electron + env: + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + # rebuild-native.mjs reads this to target the right arch when + # cross-building (runner is arm64; x64 matrix needs the hint). + ELECTRON_BUILDER_ARCH: ${{ matrix.arch }} + run: | + bun run build:web-assets + bun run bundle:main + # npmRebuild=false in package.json, so electron-builder won't + # recompile native deps on its own — we must rebuild against the + # target Electron ABI before packaging, otherwise better-sqlite3/ + # node-pty/bun-pty crash on require inside the packaged app. + bun run rebuild:native + ./node_modules/.bin/electron-builder --mac --${{ matrix.arch }} --publish=never + + - name: Verify signature + entitlements + notarization + run: | + set -euo pipefail + + APP_DIR="packages/electron/dist/mac" + [ -d "packages/electron/dist/mac-arm64" ] && APP_DIR="packages/electron/dist/mac-arm64" + + APP_PATH=$(find "$APP_DIR" -maxdepth 2 -name "*.app" -print -quit) + if [ -z "$APP_PATH" ]; then + echo "Error: .app not found under packages/electron/dist/mac*" + ls -la packages/electron/dist/ + exit 1 + fi + + echo "Verifying $APP_PATH" + codesign -vv --deep --strict "$APP_PATH" + + # Require hardened runtime + CS_INFO=$(codesign -dv --verbose=4 "$APP_PATH" 2>&1) + echo "$CS_INFO" + if ! echo "$CS_INFO" | grep -q "flags=.*runtime"; then + echo "Error: hardened runtime flag missing" + exit 1 + fi + + # Require notary ticket stapled + xcrun stapler validate "$APP_PATH" + + ENTITLEMENTS=$(codesign -d --entitlements :- "$APP_PATH" 2>&1 || true) + if echo "$ENTITLEMENTS" | grep -q "com.apple.security.app-sandbox"; then + echo "Error: app sandbox entitlement is present" + exit 1 + fi + for key in \ + com.apple.security.cs.allow-jit \ + com.apple.security.cs.allow-unsigned-executable-memory \ + com.apple.security.cs.disable-library-validation + do + if ! echo "$ENTITLEMENTS" | grep -q "$key"; then + echo "Error: required entitlement missing: $key" + exit 1 + fi + done + + - name: Upload DMG / ZIP / blockmaps to release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ needs.create-release.outputs.version }} + files: | + packages/electron/dist/*.dmg + packages/electron/dist/*.zip + packages/electron/dist/*.blockmap + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload per-arch latest-mac.yml for merge + uses: actions/upload-artifact@v4 + with: + name: latest-yml-${{ matrix.target }} + path: packages/electron/dist/latest-mac.yml + retention-days: 1 + + combine-electron-manifests: + needs: [create-release, build-desktop-electron-macos] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Download per-arch latest-mac.yml + uses: actions/download-artifact@v4 + with: + pattern: latest-yml-*-apple-darwin + path: artifacts + + - name: Finalize combined latest-mac.yml + env: + LATEST_YML_DIR: ${{ github.workspace }}/artifacts + GH_REPO: ${{ github.repository }} + OPENCHAMBER_VERSION: ${{ needs.create-release.outputs.version }} + run: node packages/electron/scripts/finalize-latest-yml.mjs + + - name: Upload combined latest-mac.yml to release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ needs.create-release.outputs.version }} + files: ${{ runner.temp }}/latest-mac.yml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + finalize-release: - needs: [create-release, build-desktop-macos, publish-npm, combine-manifests] + needs: [create-release, build-desktop-macos, build-desktop-electron-macos, publish-npm, combine-manifests, combine-electron-manifests] runs-on: ubuntu-latest env: DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} + DISCORD_UPDATE_ROLE_ID: ${{ secrets.DISCORD_UPDATE_ROLE_ID }} steps: - name: Publish release uses: softprops/action-gh-release@v2 @@ -514,11 +677,14 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ needs.create-release.outputs.version }} REPOSITORY: ${{ github.repository }} + UPDATE_ROLE_ID: ${{ env.DISCORD_UPDATE_ROLE_ID }} run: | node - <<'NODE' (async () => { const tag = `v${process.env.VERSION}`; const repo = process.env.REPOSITORY; + const rawRoleId = (process.env.UPDATE_ROLE_ID || '').trim(); + const updateRoleId = /^\d+$/.test(rawRoleId) ? rawRoleId : ''; const releaseRes = await fetch(`https://api.github.com/repos/${repo}/releases/tags/${tag}`, { headers: { @@ -534,9 +700,18 @@ jobs: const release = await releaseRes.json(); const description = (release.body || `OpenChamber ${tag} released.`).slice(0, 4096); + const mention = updateRoleId ? `<@&${updateRoleId}>` : ''; const payload = { username: 'OpenChamber Releases', + ...(mention ? { content: mention } : {}), + ...(updateRoleId + ? { + allowed_mentions: { + roles: [updateRoleId], + }, + } + : {}), embeds: [ { title: release.name || `OpenChamber ${tag}`, diff --git a/src/.gitignore b/src/.gitignore index 0565d35..db16936 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -28,6 +28,7 @@ local-dev* *.sw? .opencode/plans/* .hive +docs/personal/* # Build outputs build/ diff --git a/src/.opencode/package-lock.json b/src/.opencode/package-lock.json new file mode 100644 index 0000000..fc32819 --- /dev/null +++ b/src/.opencode/package-lock.json @@ -0,0 +1,376 @@ +{ + "name": ".opencode", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@opencode-ai/plugin": "1.4.10" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@opencode-ai/plugin": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.4.10.tgz", + "integrity": "sha512-35Za2LT2oNWnBoonmPjN1Z9PB4+ir2a6GbZ3nIZQQL/96mqzTRkT1FqUkQc3bdMmfT1R1rqOd5aMzkIXMqC7dA==", + "license": "MIT", + "dependencies": { + "@opencode-ai/sdk": "1.4.10", + "effect": "4.0.0-beta.48", + "zod": "4.1.8" + }, + "peerDependencies": { + "@opentui/core": ">=0.1.100", + "@opentui/solid": ">=0.1.100" + }, + "peerDependenciesMeta": { + "@opentui/core": { + "optional": true + }, + "@opentui/solid": { + "optional": true + } + } + }, + "node_modules/@opencode-ai/sdk": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.4.10.tgz", + "integrity": "sha512-Yaddcs/COp0hwiCxgobSZyDUN0nHgkEFL4bG0BQxwd52SGAysOr6A6L0ihfkuhVx0kbi9eXWgZk4ydNOrnur5w==", + "license": "MIT", + "dependencies": { + "cross-spawn": "7.0.6" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/effect": { + "version": "4.0.0-beta.48", + "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.48.tgz", + "integrity": "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "fast-check": "^4.6.0", + "find-my-way-ts": "^0.1.6", + "ini": "^6.0.0", + "kubernetes-types": "^1.30.0", + "msgpackr": "^1.11.9", + "multipasta": "^0.2.7", + "toml": "^4.1.1", + "uuid": "^13.0.0", + "yaml": "^2.8.3" + } + }, + "node_modules/fast-check": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.6.0.tgz", + "integrity": "sha512-h7H6Dm0Fy+H4ciQYFxFjXnXkzR2kr9Fb22c0UBpHnm59K2zpr2t13aPTHlltFiNT6zuxp6HMPAVVvgur4BLdpA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "node_modules/find-my-way-ts": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz", + "integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==", + "license": "MIT" + }, + "node_modules/ini": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/kubernetes-types": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz", + "integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==", + "license": "Apache-2.0" + }, + "node_modules/msgpackr": { + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.9.tgz", + "integrity": "sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/multipasta": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz", + "integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==", + "license": "MIT" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pure-rand": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz", + "integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/toml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz", + "integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.1.8", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/src/.opencode/skills/locale-ui-patterns/SKILL.md b/src/.opencode/skills/locale-ui-patterns/SKILL.md new file mode 100644 index 0000000..ff57255 --- /dev/null +++ b/src/.opencode/skills/locale-ui-patterns/SKILL.md @@ -0,0 +1,128 @@ +--- +name: locale-ui-patterns +description: Use when creating or modifying OpenChamber UI text, labels, buttons, placeholders, aria labels, empty states, toasts, dialogs, settings copy, navigation labels, or any user-facing strings. +--- + +# Locale UI Patterns + +## Core Rule + +User-facing UI text must go through `@/lib/i18n`; do not hardcode English strings in components. + +Use this skill for any React UI change that adds or edits visible text, accessible labels, placeholders, tooltips, toasts, dialogs, settings labels, navigation labels, or empty/error states. + +## Required Flow + +1. Add or reuse a key in `packages/ui/src/lib/i18n/messages/en.ts`. +2. Add the same key to every non-English dictionary in `packages/ui/src/lib/i18n/messages/`. +3. In components, call `const { t } = useI18n()` from `@/lib/i18n` and render `t('key')`. +4. For locale names or language picker labels, use `label(locale)` from `useI18n()`. +5. Keep locale state in `packages/ui/src/lib/i18n/*`; do not add locale fields to broad stores like `useUIStore`. +6. Do not remount the app to update language. Components must re-render through `useI18n()`. + +## Component Usage Rules + +- Import from `@/lib/i18n`, not deep files. +- Keep `t(...)` calls inside React render/hook scope so locale changes re-render text. +- Do not resolve translated text at module scope. +- For static option arrays, store `labelKey` / `descriptionKey`; resolve with `t(...)` inside the component. +- For non-React helpers, pass translated strings in from the component or pass `t` explicitly. + +## Key Style + +Use stable semantic keys, not English text as keys. + +Keys should describe location + UI role + meaning. They should not encode current copy wording. + +Use existing nearby naming when extending a surface. If no nearby pattern exists, choose a short path that mirrors the UI ownership. + +Namespaces like `layout.*`, `settings.*`, `chat.*`, `git.*`, `session.*`, `toast.*`, and `dialog.*` are examples, not a fixed exhaustive list. + +Good: +```ts +'settings.appearance.language.label': 'Language' +'layout.mainTab.chat': 'Chat' +'chat.input.placeholder': 'Ask OpenChamber...' +``` + +Bad: +```ts +'Language': 'Language' +'chatLabel': 'Chat' +'askOpenChamberDotDotDot': 'Ask OpenChamber...' +``` + +Avoid overly generic keys unless the text is truly global and context-independent. Prefer specific keys when button meaning can vary by surface. + +## Parameters + +Use `{name}` placeholders for dynamic values. + +```ts +'toast.language.changed': 'Language changed to {language}' +``` + +```tsx +t('toast.language.changed', { language: label(locale) }) +``` + +Do not pass grammar fragments as params. Never use params like `{suffix}`, `{plural}`, `{article}`, `{prefix}`, `{dateSuffix}`, or pieces of words/sentences. + +Bad: +```tsx +t('dialog.delete.description', { count, suffix: count === 1 ? '' : 's' }) +``` + +Good: +```tsx +count === 1 + ? t('dialog.delete.descriptionSingle', { count }) + : t('dialog.delete.descriptionPlural', { count }) +``` + +Plural/count-dependent text must use separate complete-message keys unless all supported locales can use one identical complete sentence. Placeholders are only for real values (`{count}`, `{name}`, `{path}`), not grammar. + +Optional clauses must also be complete-message keys. Do not build a sentence by injecting a translated phrase into another translated sentence. + +Bad: +```tsx +t('dialog.delete.description', { + dateLabel: date ? t('dialog.delete.dateSuffix', { date }) : '', +}) +``` + +Good: +```tsx +date + ? t('dialog.delete.descriptionWithDate', { count, date }) + : t('dialog.delete.description', { count }) +``` + +## What Counts As UI Text + +- Button and menu labels +- Settings labels and descriptions +- Placeholder text +- Tooltip content +- Dialog titles/descriptions/actions +- Toast title/description/action labels +- Empty/error/loading states +- `aria-label`, `title`, image `alt` text when user-facing + +## Exceptions + +Do not translate: + +- Product names: `OpenChamber`, `OpenCode`, `GitHub` +- Protocol/tool acronyms: `MCP`, `SSE`, `WebSocket`, `API` +- Model/provider names +- File paths, command names, environment variables +- User/generated content + +## Review Checklist + +- No new hardcoded user-facing English in changed UI files. +- Every new key exists in all dictionaries. +- No locale state added to broad/shared stores. +- No full app remount for locale changes. +- Locale switch preserves current UI state. diff --git a/src/AGENTS.md b/src/AGENTS.md index c601cac..35dd9de 100644 --- a/src/AGENTS.md +++ b/src/AGENTS.md @@ -1,88 +1,130 @@ # OpenChamber - AI Agent Reference (verified) ## Core purpose + OpenChamber provides UI runtimes (web/desktop/VS Code) for interacting with an OpenCode server (local auto-start or remote URL). UI uses HTTP + SSE via `@opencode-ai/sdk`. ## Runtime architecture (IMPORTANT) -- `Desktop` is a thin Tauri shell that starts the web server sidecar and loads the web UI from `http://127.0.0.1:`. -- All backend logic lives in `packages/web/server/*` (and `packages/vscode/*` for the VS Code runtime). Desktop Rust is not a feature backend. -- Tauri is used only for stable native integrations: menu, dialog (open folder), notifications, updater, deep-links. + +- `Desktop` (Electron) boots the web server **in the same Node process** as the Electron main, then loads the web UI from `http://127.0.0.1:`. No sidecar subprocess. +- `Desktop` (Tauri, legacy) still spawns `openchamber-server` as a bun-compiled sidecar binary. Kept only for auto-update compatibility with existing Tauri installs. +- All backend logic lives in `packages/web/server/*` (and `packages/vscode/*` for the VS Code runtime). The native shell is not a feature backend. +- The shell is used only for stable native integrations: menu, dialog (open folder), notifications, updater, deep-links, quit confirmation. + +### Desktop shell: Electron is the target, Tauri is legacy + +- **New desktop work goes into `packages/electron/`.** This is the forward path. +- `packages/desktop/` (Tauri) is kept running in parallel only to preserve auto-update for existing installs until the cutover. Do **not** add features to it; do **not** port bug fixes back unless they actually affect currently-released Tauri users. +- Desktop-side changes (IPC handlers, native integrations, window/quit/notification behavior) land in `packages/electron/main.mjs` + `packages/electron/preload.mjs`. The `__TAURI__` shim exposed by the preload keeps the shared UI working against both shells, so renderer-side code should not branch on shell type. +- Electron imports the server via `@openchamber/web/server/index.js` (workspace dep) and calls `startWebUiServer({...})`. The returned handle has `getPort()` / `stop()`. Notifications flow via an `onDesktopNotification` callback injected at startup — no stdout-parsing IPC. +- Build/release: both shells ship in the same GitHub release today (`.github/workflows/release.yml`). The one-shot Tauri → Electron auto-update migration is documented in `docs/TAURI_TO_ELECTRON_CUTOVER.md`; run that when the user decides to flip. +- After the cutover ships and stabilises, `packages/desktop/` is deleted; this note collapses back to "Desktop is Electron". ## Tech stack (source of truth: `package.json`, resolved: `bun.lock`) + - Runtime/tooling: Bun (`package.json` `packageManager`), Node >=20 (`package.json` `engines`) - UI: React, TypeScript, Vite, Tailwind v4 - State: Zustand (`packages/ui/src/stores/`) -- UI primitives: Radix UI (`package.json` deps), HeroUI (`package.json` deps), Remixicon (`package.json` deps) +- UI primitives: Base UI (`@base-ui/react`, primary source for dropdown/select/dialog/menu/tooltip/etc. — wrappers live in `packages/ui/src/components/ui/`), Radix UI (`package.json` deps, legacy usages being migrated), HeroUI (`package.json` deps), Remixicon (`package.json` deps) - Server: Express (`packages/web/server/index.js`) -- Desktop: Tauri v2 (`packages/desktop/src-tauri/`) +- Desktop (forward): Electron 41 (`packages/electron/`) +- Desktop (legacy, maintenance-only): Tauri v2 (`packages/desktop/src-tauri/`) - VS Code: extension + webview (`packages/vscode/`) ## Monorepo layout + Workspaces are `packages/*` (see `package.json`). + - Shared UI: `packages/ui` - Web app + server + CLI: `packages/web` -- Desktop app (Tauri): `packages/desktop` +- Desktop shell (Electron — forward): `packages/electron` +- Desktop shell (Tauri — legacy, maintenance-only): `packages/desktop` - VS Code extension: `packages/vscode` ## Documentation map + Before changing any mapped module, read its module documentation first. ### web + Web runtime and server implementation for OpenChamber. #### lib + Server-side integration modules used by API routes and runtime services. ##### quota + Quota provider registry, dispatch, and provider integrations for usage endpoints. + - Module docs: `packages/web/server/lib/quota/DOCUMENTATION.md` ##### git + Git repository operations for the web server runtime. + - Module docs: `packages/web/server/lib/git/DOCUMENTATION.md` ##### github + GitHub authentication, OAuth device flow, Octokit client factory, and repository URL parsing. + - Module docs: `packages/web/server/lib/github/DOCUMENTATION.md` ##### opencode + OpenCode server integration utilities including config management, provider authentication, and UI authentication. + - Module docs: `packages/web/server/lib/opencode/DOCUMENTATION.md` ##### notifications + Notification message preparation utilities for system notifications, including text truncation and optional summarization. + - Module docs: `packages/web/server/lib/notifications/DOCUMENTATION.md` ##### terminal + WebSocket protocol utilities for terminal input handling including message normalization, control frame parsing, and rate limiting. + - Module docs: `packages/web/server/lib/terminal/DOCUMENTATION.md` ##### tts + Server-side text-to-speech services and summarization helpers for `/api/tts/*` endpoints. + - Module docs: `packages/web/server/lib/tts/DOCUMENTATION.md` ##### skills-catalog + Skills catalog management including discovery, installation, and configuration of agent skill packages. + - Module docs: `packages/web/server/lib/skills-catalog/DOCUMENTATION.md` ## Build / dev commands (verified) + All scripts are in `package.json`. + - Validate: `bun run type-check`, `bun run lint` - Build all: `bun run build` -- Desktop build: `bun run desktop:build` +- Desktop build (Electron — primary): `bun run electron:build` +- Desktop dev (Electron): `bun run electron:dev` +- Desktop build (Tauri — legacy): `bun run desktop:build` - VS Code build: `bun run vscode:build` - Release smoke build: `bun run release:test` (shell script: `scripts/test-release-build.sh`) ## Runtime entry points + - Web bootstrap: `packages/web/src/main.tsx` - Web server: `packages/web/server/index.js` - Web CLI: `packages/web/bin/cli.js` (package bin: `packages/web/package.json`) -- Desktop: Tauri entry `packages/desktop/src-tauri/src/main.rs` (spawns web server sidecar + loads web UI) -- Tauri backend: `packages/desktop/src-tauri/src/main.rs` +- Desktop (Electron — primary): `packages/electron/main.mjs` (boots the web server in-process via `startWebUiServer`, loads web UI over loopback; preload at `packages/electron/preload.mjs` exposes the `__TAURI__` IPC shim so shared UI code is shell-agnostic) +- Desktop (Tauri — legacy): `packages/desktop/src-tauri/src/main.rs` - VS Code extension host: `packages/vscode/src/extension.ts` - VS Code webview bootstrap: `packages/vscode/webview/main.tsx` ## OpenCode integration + - UI client wrapper: `packages/ui/src/lib/opencode/client.ts` (imports `@opencode-ai/sdk/v2`) - SSE hookup: `packages/ui/src/hooks/useEventStream.ts` - Web server embeds/starts OpenCode server: `packages/web/server/index.js` (`createOpencodeServer`) @@ -90,6 +132,7 @@ All scripts are in `package.json`. - External server support: Set `OPENCODE_HOST` (full base URL, e.g. `http://hostname:4096`) or `OPENCODE_PORT`, plus `OPENCODE_SKIP_START=true`, to connect to existing OpenCode instance ## Key UI patterns (reference files) + - Settings shell: `packages/ui/src/components/views/SettingsView.tsx` - Settings shared primitives: `packages/ui/src/components/sections/shared/` - Settings sections: `packages/ui/src/components/sections/` (incl `skills/`) @@ -98,27 +141,74 @@ All scripts are in `package.json`. - Terminal UI: `packages/ui/src/components/terminal/` (uses `ghostty-web`) ## External / system integrations (active) + - Git: `packages/ui/src/lib/gitApi.ts`, `packages/web/server/index.js` (`simple-git`) - Terminal PTY: `packages/web/server/index.js` (`bun-pty`/`node-pty`) - Skills catalog: `packages/web/server/lib/skills-catalog/`, UI: `packages/ui/src/components/sections/skills/` ## Agent constraints + - Do not modify `../opencode` (separate repo). - Do not run git/GitHub commands unless explicitly asked. -- Keep baseline green (run `bun run type-check`, `bun run lint`, `bun run build` before finalizing changes). +- Keep baseline green (run `bun run type-check`, `bun run lint` before finalizing changes). + +## Agent code of conduct + +- Prefer the smallest correct change. +- Preserve working behavior before improving structure. +- Do not add cleverness where a direct implementation is enough. +- Do not infer critical state from weak signals when a stronger source exists. +- Do not encode policy only in UI; enforce it in core logic. +- Do not hide data loss, partial failure, or fallback behavior. Make it explicit in code. +- Finish work end-to-end: implementation, verification, and cleanup. ## Development rules + - Keep diffs tight; avoid drive-by refactors. -- Backend changes: keep web/desktop/vscode runtimes consistent (if relevant). -- Follow local precedent; search nearby code first. -- TypeScript: avoid `any`/blind casts; keep ESLint/TS green. -- React: prefer function components + hooks; class only when needed (e.g. error boundaries). -- Control flow: avoid nested ternaries; prefer early returns + `if/else`/`switch`. -- Styling: Tailwind v4; typography via `packages/ui/src/lib/typography.ts`; theme vars via `packages/ui/src/lib/theme/`. -- Shared UI patterns: for "series of items + divider + series of items" layouts, use shared UI primitives instead of duplicating ad-hoc markup in feature components. -- Toasts: use custom toast wrapper from `@/components/ui` (backed by `packages/ui/src/components/ui/toast.ts`); do not import `sonner` directly in feature code. +- Follow local precedent; inspect nearby code before introducing new patterns. +- Backend changes: keep web, desktop, and VS Code behavior consistent when they share contracts. +- TypeScript: avoid `any`, blind casts, and shape guessing. +- React: prefer function components + hooks; use classes only when required. +- Control flow: prefer early returns and explicit branching over nested ternaries. +- Styling: Tailwind v4, typography via `packages/ui/src/lib/typography.ts`, theme vars via `packages/ui/src/lib/theme/`. +- Shared UI patterns: reuse shared primitives before introducing feature-local markup patterns. +- Toasts: use the wrapper from `@/components/ui`; do not import `sonner` directly in feature code. - No new deps unless asked. -- Never add secrets (`.env`, keys) or log sensitive data. +- Never add secrets or log sensitive data. + +## Architecture patterns + +### Thin entrypoints, focused modules + +- Keep orchestration entrypoints thin: `index.js`, bridge files, bootstrap files, provider roots. +- Move route, domain, and runtime logic into focused modules with clear ownership. +- Prefer dependency injection over hidden module coupling. +- Add or update module documentation when ownership changes. + +### Strong source of truth + +- Prefer deterministic state over heuristics. +- Use live server/session state for live activity. Do not let historical anomalies masquerade as current execution. +- If a fallback is necessary, scope it narrowly to the active entity and treat it as temporary. +- Restore derived UI state from authoritative records. Example: restore model or agent from the latest user message, not assistant-side guesses. + +### Live state vs historical state + +- Derive live UI behavior from live state channels, not persisted history. +- Use historical records to restore context, not to infer that work is still in progress. +- If live state is delayed, use the narrowest possible transient fallback and clear it as soon as authoritative state arrives. + +### Cross-runtime parity + +- If web defines a route or payload contract that shared UI depends on, keep VS Code and desktop parity where applicable. +- Shared behavior differences must be intentional and visible in code. +- Do not ship a web-only assumption into shared UI. + +### Partial-failure-safe flows + +- Cross-directory and multi-entity operations must tolerate partial failure. +- Prefer per-item results, rollback paths, or resumable cleanup over all-or-nothing assumptions. +- Never leave optimistic state or local caches stranded after failure. ## CLI Parity and Safety Policy (MANDATORY) @@ -158,6 +248,7 @@ are defined in the `clack-cli-patterns` skill and should not be duplicated here. When working on terminal CLI commands, prompts, or output formatting, agents **MUST** study the Clack CLI skill first. **Before starting terminal CLI work:** + ``` skill({ name: "clack-cli-patterns" }) ``` @@ -169,12 +260,127 @@ Scope: terminal CLI only (for example `packages/web/bin/*`). Do not apply this r When working on any UI components, styling, or visual changes, agents **MUST** study the theme system skill first. **Before starting any UI work:** + ``` skill({ name: "theme-system" }) ``` This skill contains all color tokens, semantic logic, decision tree, and usage patterns. All UI colors must use theme tokens - never hardcoded values or Tailwind color classes. +## Performance rules (MANDATORY) + +These rules exist because violating them has caused measurable regressions (render cascades, memory bloat, UI jank). They apply to all UI and sync layer work. + +### Shared-store render discipline + +- **Treat common stores as render fanout boundaries.** An unnecessary reference change in shared state can re-render large parts of the app. +- **Do not put high-frequency state in broadly consumed stores.** Fast-changing state should live in narrow stores with narrow subscribers. +- **Update only the fields that changed.** Preserve references for untouched state branches. +- **Prefer leaf selectors over container selectors.** Subscribe to the smallest stable value that satisfies the component. +- **Isolate hot consumers.** If a value changes often and only a few components need it, move it to a narrower store or consume it in a memoized child. +- **Do not subscribe shell/layout components to broad live collections.** If a shell only needs one field, entity, or derived flag, subscribe to that instead of the whole collection. +- **Treat provider roots as global hot paths.** A top-level provider must not subscribe to high-frequency data unless the feature is actually enabled and the subscription is essential. + +### Zustand referential equality + +Zustand skips re-renders when a selector returns the same reference (`Object.is`). Every new object/array reference triggers a re-render in every subscriber. + +- **Never spread all state fields in an update.** Only create new references for fields that actually changed. A `message.part.delta` event should not clone `session`, `permission`, etc. +- **Select leaf values, not containers.** `useStore((s) => s.permission[sessionID])` is correct. `useStore((s) => s.permission)` subscribes to every permission change across all sessions. +- **Preserve references when merging.** If prepending older messages, keep existing message object references. Only add truly new items. Return the original array if nothing was added. +- **For derived collections, preserve item identity when presentation-relevant fields are unchanged.** Reuse previous item references for unchanged rows/items and move high-frequency live fields to narrow per-item selectors. + +### Store splitting + +A single store with N properties means every subscriber re-evaluates on every state change. Split stores by change frequency and subscriber set. + +- **Group state by how often it changes.** Streaming state (updated 60/sec) must not live with user preferences (updated on click). +- **Group state by who reads it.** If only 2 components need a value, it belongs in a store that only those 2 subscribe to. +- **Cross-store reads use `.getState()`.** Actions in one store that need another store call `useOtherStore.getState()` — imperative, no subscription. +- **Never add unrelated state to an existing store** just because it's convenient. Create a new store. + +### Event pipeline and SSE + +- **Gate expensive operations on the hot path.** During streaming, `message.part.delta` and `message.part.updated` fire ~60/sec. Any `findIndex`, `filter`, or iteration added to these handlers multiplies across every event. Gate behind a cheap boolean check first (e.g., check `next[0]` before scanning the array). +- **Skip no-op updates.** If an incoming event doesn't change the state (same role, same finish, same timestamps), return `false` from the reducer to avoid creating new references. +- **Coalesce by key.** Same-entity events (e.g., repeated `session.status` for the same session) should replace earlier ones in the queue, not accumulate. +- **Preserve event ordering semantics.** Reducers and queues must not let stale deltas or out-of-order events corrupt the latest state. +- **Do not widen live-activity fallbacks.** A fallback for delayed status should inspect only the current trailing entity, not arbitrary historical records. + +### Polling payload fidelity + +- **Do not let lightweight polling erase rich fields.** If light mode omits fields (e.g., `diffStats`), preserve previous rich data until a heavy follow-up fetch lands. +- **Use two-phase polling.** Run cheap change detection first; only run heavy status fetches for directories that actually changed. + +### Optimistic updates + +- **Use the shadow Map pattern.** Insert optimistic data into the store for instant UI, AND register it in a separate tracking Map. Cleanup happens deterministically via `mergeOptimisticPage` on the next data fetch — not via heuristics in the event reducer. +- **Pass client-generated IDs to the server.** Use the same ID format as the server (hex-encoded timestamps). Pass `messageID` to `promptAsync` so the server echoes back the same ID. This prevents duplicates and enables in-place replacement. +- **Rollback on error.** Remove the optimistic entry from both the store and the shadow Map. +- **Stabilize bridge callbacks.** When wiring hook callbacks into module-level refs, use stable ref wrappers so effects do not loop on changing function identities. + +### Session/input consistency + +- **Capture send config at queue time.** Queue items must include provider/model/agent/variant snapshot; do not re-resolve from mutable live state at send time. +- **Keep server-selected attachments sendable.** Preserve server-backed file selections in queue/submit flows and convert them to proper `file://` URLs before sending. +- **Do not let text input state repaint unrelated chrome.** Typing should not force unrelated controls, menus, indicators, or toolbars to re-render on every keystroke. +- **Extract slow-changing chrome from hot input paths.** If controls do not depend on the current text value, move them behind memoized boundaries with stable callbacks. + +### Bootstrap resilience + +- **Treat startup 502/503 as transient.** Retry bootstrap/session-list flows with bounded retries/intervals, especially in VS Code where API readiness can lag bridge startup. +- **Use polling recovery when failures are swallowed.** If an async loader resolves without throwing on failure, recover with interval retries gated by loaded-state checks. + +### Scroll and DOM + +- **Never use `await waitForFrames()` for scroll preservation.** Frames of visible scroll jump are unacceptable. Use `useLayoutEffect` to adjust scroll synchronously after React commits DOM — before the browser paints. +- **Capture scroll state before the state change, restore in layout effect.** The pattern: save `scrollHeight`/`scrollTop` into a ref before triggering the update, consume it in `useLayoutEffect` on the rendered output. +- **Do not let viewport resizes masquerade as content growth.** Viewport-height changes must not trigger the same scroll compensation logic used for actual content growth. +- **Disable or narrow native/browser scroll anchoring when custom scroll logic exists.** Browser anchoring and app-managed pinning/follow logic will fight and produce jiggle. +- **Autosize textareas without transient collapse on growth.** Avoid `height='auto'` shrink/expand cycles on every character when the content only grew; this creates visible layout bounce. + +### List ordering and view consistency + +- **Do not sort structural lists directly from high-churn live fields.** If live updates are frequent, sorting directly from them causes reorder thrash and wide rerender cascades. +- **If live recency is required, freeze order during high-frequency updates and apply a one-shot reorder only at an intentional lifecycle edge.** Choose the lifecycle edge explicitly instead of letting every intermediate update reshuffle the UI. +- **Use one ordering source for all views of the same data.** Different views of the same entities must derive from the same ranked list or rank map; do not let each surface re-derive ordering independently. +- **Do not mix global snapshots and local live snapshots without an explicit reconciliation policy.** If multiple data sources feed one view, define which fields win and how they merge. + +### Component isolation + +- **Extract high-frequency hook consumers into separate components.** If a hook re-evaluates 60/sec (e.g., streaming status), wrap its consumer in a `React.memo` child component so the parent doesn't re-render. +- **Use custom `React.memo` comparators for message rows.** Compare render-relevant fields (role, finish, parts count, part IDs) — not object references. + +### Caching and memory + +- **Cap in-memory caches with both count and byte limits.** Entry count alone doesn't prevent memory bloat from large files. Use dual-constraint LRU (e.g., 40 entries OR 20MB). +- **Set store session limits to match loaded data.** If bootstrap loads N sessions, set `limit >= N`. Otherwise the next SSE event triggers trimming that silently removes sessions. +- **Invalidate caches on mutations.** File content cache must clear entries on write, delete, rename. Prefetch cache must clear on session eviction. +- **Use TTLs to prevent redundant fetches.** If a session was fetched <15s ago, skip re-fetching — SSE events keep it current. + +### Directory context + +- **Never cache directory strings in closures.** Directory can change at any time (worktree switch). Read it dynamically from `opencodeClient.getDirectory()` at call time. +- **Pass directory hints when the source of truth isn't available yet.** Newly created sessions aren't in the sync store until SSE delivers them. Pass the known directory as a parameter instead of relying on lookup. + +## Regression-prevention checklist + +- When adding fallback logic, ask: can stale persisted data keep this path active forever? +- When deriving UI state, ask: is this live state, historical state, or inferred state? +- When adding store fields, ask: who reads this, how often does it change, and should it live elsewhere? +- When touching polling or bootstrap, ask: can a lighter payload erase richer existing data? +- When handling optimistic updates, ask: where is rollback, reconciliation, and duplicate prevention? +- When changing shared routes or state contracts, ask: what breaks in web, desktop, and VS Code? +- When fixing a bug with a heuristic, prefer narrowing the heuristic over widening it. + +## Validation expectations + +- Run `bun run type-check` and `bun run lint` before finalizing. +- For hot-path changes, verify behavior under streaming or repeated events, not just static render. +- For sync or startup changes, verify fresh load, retry/failure, and restart behavior. +- For session changes, verify create, stream, abort, permission, archive/delete, and revisit flows when relevant. + ## Recent changes + - Releases + high-level changes: `CHANGELOG.md` - Recent commits: `git log --oneline` (latest tags: `v1.4.6`, `v1.4.5`) diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md index 28d9265..a2e7c79 100644 --- a/src/CHANGELOG.md +++ b/src/CHANGELOG.md @@ -4,6 +4,106 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [1.9.9] - 2026-04-26 + +- UI/Localization: added a localization foundation with translated interface strings for Spanish, Brazilian Portuguese, Ukrainian, and Simplified Chinese. +- Settings/Appearance: added selectable interface and code fonts with 10 choices each. +- Chat/Workflow: added keyboard turn navigation, widened chat content, and introduced a local workspace review and summarize slash commands for faster review handoff. +- Chat/Mobile: improved mention and autocomplete behavior with complete results, clearer active-tab scoping, and less context-switching while drafting prompts. +- Chat/Tasks: todo list progress now updates live as task status changes, and task/model status hints are steadier during active runs (thanks to @Yabuku-xD). +- Files/Editor: added an "Open files in preview mode" setting and improved multi-file edit/diff safety so review flows stay cleaner (thanks to @daveotero). +- Reliability/Performance: improved cold start and streaming responsiveness with lazy-loaded heavy components, chunk-load recovery, lower re-render churn, and safer reconnect/local-stream recovery (thanks to @Yabuku-xD, @jwcrystal, @vhqtvn). +- Desktop/Web/Mobile: improved Electron update restart behavior, PWA service-worker notifications, mobile keyboard handling, and the Add Project panel flow (thanks to @Jovines, @vhqtvn). + +## [1.9.8] - 2026-04-22 + +- Sessions/Reliability: fixed parent-child session sync during reconnects and navigation, so status and progress stay aligned in complex session trees (thanks to @jwcrystal). +- Settings/Sync: settings updates now sync more reliably across clients, and sidebar session pagination is steadier in larger workspaces. +- Sessions/Folders: folder changes now persist through server-backed endpoints, improving consistency across environments and path setups. +- Notifications: permission notifications are now suppressed when auto-accept is enabled, reducing noise during trusted runs. +- Chat/Files: improved changed-files handling in chat and restored quick file-open flows from pending changes, so jump-to-edit stays fast (thanks to @jwcrystal). +- UI: improved bottom scroll shadow behavior and hide the tasks row when there is no active work for a cleaner conversation view. +- Reliability/Desktop: improved live event-stream recovery after transient stalls, wait briefly before failing chat actions during reconnects, and persist Electron server logs for easier disconnect debugging. +- Desktop/macOS: System color mode now tracks OS theme changes, traffic-light controls stay visible after dock restore, and update restart/changelog handling is more reliable. +- Chat/Commands: added `/summary` slash command for a non-destructive session summary - optional topic hint after the command focuses the output, and the prompt is customizable under Settings: Magic Prompts. + +## [1.9.7] - 2026-04-22 + +- Desktop: added an Electron desktop runtime in parallel with the current Tauri app, with Electron planned to become the default path in an upcoming release. +- Plans/Notes/Todos: added editable project plans from assistant messages, external plan upload, configurable planning magic prompts, and quicker note/todo handoff into new sessions or worktrees. +- Chat/Files: you can now drag files and folders from the file tree into chat, with improved `@folder` autocomplete for faster context building (thanks to @youfch). +- Sessions/UI: added bulk session selection in the sidebar and fixed pinned sessions so they persist reliably after reloads (thanks to @yart). +- Files/Git: added a file-change summary bar and auto-refresh for open files changed outside the app, improving review flow and keeping editors in sync (thanks to @jwcrystal). +- Git/Worktrees: improved branch/worktree reliability by allowing checkout with uncommitted changes, tightening worktree cache invalidation, and reducing incorrect remote prefetches (thanks to @jwcrystal, @jasonalsing). +- Settings/MCP: improved MCP auth flow with better remote-config support and clearer diagnostics, and aligned config resolution with OpenCode behavior for more predictable setup (thanks to @daveotero, @cyan). +- Reliability/Chat: hardened bootstrap and stream-connection recovery, preserved session/connect state more reliably, and reduced streaming UI churn for smoother long runs. +- Web/PWA: added install orientation controls and fixed loopback-origin handling for web push notifications in local setups (thanks to @vhqtvn, @yart). + +## [1.9.6] - 2026-04-17 + +- Reliability/Streaming: switched live message events to a WebSocket-first transport with SSE fallback, added response compression, and hardened proxy/compression handling so long runs stay smoother on slower or proxied networks (thanks to @geekifan, @jwcrystal). +- Sessions/Scheduled Tasks: added scheduled task creation and management with locale-aware scheduling, so recurring prompts run at the right local time without manual re-entry. +- Sessions/Worktrees: enforced session worktree isolation and tightened session-switch safety, reducing cross-worktree mix-ups when resuming chats or running Git actions (thanks to @jwcrystal). +- Files: added a full Go to Line workflow (toolbar + shortcut + dialog) and a new Copy Relative Path action, making in-editor navigation and path sharing much faster (thanks to @coldbrow). +- Files: file trees now auto-refresh when files change outside the app, so new, renamed, or updated files appear without manual reloads (thanks to @jwcrystal). +- Chat/Export: added export session as Markdown and improved empty-state/export behavior, making conversation handoff and documentation cleaner (thanks to @coldbrow). +- Chat/Requests: restored blocking request visibility in sub-sessions, scoped auto-approve to the active session tree, and reduced noisy auto-approved notifications during multi-session work. +- Desktop: added quick open and a LAN access toggle, plus safer quit behavior around scheduled tasks for smoother local-network and day-to-day desktop workflows (thanks to @An-jinu). +- Chat/Markdown: added LaTeX rendering support for clearer math and technical notation in messages (thanks to @ricautomation). +- Settings/Skills: skills are now sorted within groups so larger skill lists are easier to scan (thanks to @roctom). + +## [1.9.5] - 2026-04-14 + +- Security/Auth: added passkey sign-in for protected instances and new 1-week/30-day session expiration options, so teams can enforce stronger access controls with flexible login persistence (thanks to @daveotero, @pm0u). +- Voice: added OpenAI-compatible custom server support for both text-to-speech and speech-to-text, including configurable TTS model/pitch/volume and stricter custom URL validation for safer setup (thanks to @ablepharus). +- Chat/Tool Output: added an interactive tree viewer for structured outputs and fixed JSON quote rendering, making large payloads easier to inspect and copy accurately (thanks to @yaozhenghangma). +- Chat/Reliability: fixed question-tool content disappearing after refresh and hardened subagent/session recovery paths, reducing silent failures and stuck task states (thanks to @jwcrystal). +- Sync/Performance: optimized multi-session streaming with per-directory queues, event coalescing, and parts-gap recovery to keep live updates smoother under heavy activity (thanks to @jwcrystal). +- Sessions/UI: kept active sessions visible in Recent, auto-expanded parent groups when opening subagent sessions, and hid empty archived/folder sections for cleaner navigation (thanks to @jwcrystal). +- Git/UI: restored Git changes panel visibility and sidebar sync, so change review stays available and consistent while switching contexts (thanks to @jwcrystal). +- Desktop/Startup: delivered a more guided first-launch and smart recovery flow, plus startup and remote-window interaction fixes to reduce early-session friction (thanks to @jwcrystal). +- Usage: added Zhipu AI Coding Plan tracking and restored model-variant compatibility with older OpenCode runtimes for more reliable quota reporting and model selection (thanks to @cainiao1992, @Chi-square-test). + +## [1.9.4] - 2026-04-07 + +- Settings/Magic Prompts: added a dedicated Magic Prompts page with editable templates for commit/PR generation, PR and issue reviews, failed-check/comment analysis, and merge/cherry-pick conflict resolution. +- Chat/Performance: reduced streaming render churn across the app, so long responses stay smoother with less UI jitter during active runs. +- Chat/Scrolling: fixed jumpy follow behavior and restored stable bottom-resume/live-compaction updates, so staying on the latest output is more reliable. +- Reliability/Streaming: improved reconnect, retry, and directory-aware event routing to reduce stuck session/subagent states after transient disconnects (thanks to @jwcrystal, @daveotero). +- Chat/Tool Output: LSP diagnostics now render directly in tool output, making inline error review faster while iterating (thanks to @yulia-ivashko). +- Models: added defensive handling for missing model pricing/capability metadata so model controls fail less often with incomplete provider data (thanks to @Chi-square-test). +- Desktop/Performance: removed costly window translucency and reduced duplicate notification triggers for a cooler, less noisy desktop experience. +- Startup/Remote: restored remote provider startup behavior and tightened host/port detection to reduce false startup failures. +- Usage: refreshed MiniMax CN coding-plan quota data for more accurate usage reporting (thanks to @nzlov). + +## [1.9.3] - 2026-03-01 + +- Security/Chat: user messages now escape raw HTML by default, so pasted markup is shown safely as text instead of being interpreted by the renderer (thanks to @kalac2232). +- Desktop/Performance: reduced Tauri shell CPU/GPU overhead to keep the Desktop app cooler and smoother during longer sessions. +- Sessions/Drafts: draft chat config now stays synced with the selected draft target directory, reducing wrong-model or wrong-agent carryover when switching draft context (thanks to @hkay-dev). +- VSCode/Files: added file stat support in the extension bridge so markdown-related file checks resolve more reliably before opening or rendering (thanks to @geekifan). +- Chat/Models: added arrow-key navigation for thinking-mode selection in model controls, making keyboard model tuning faster during prompt setup (thanks to @daveotero). +- Files: added HTML preview support in the file viewer, so `.html` files can be inspected visually without leaving OpenChamber (thanks to @nguyenngothuong). +- Chat: improved error message readability with clearer styling and safer word-wrapping, so failures are easier to scan without layout breakage (thanks to @nguyenngothuong). +- Chat/JSON: added an interactive JSON tree viewer with collapse/expand controls and richer color cues for easier inspection of large structured outputs (thanks to @nguyenngothuong). +- Mobile/Settings: fixed lingering settings drawers and removed extra top spacing for a cleaner, less obstructed mobile layout (thanks to @Jovines). +- Git/Worktrees: fixed worktree detection and reset stale integration state when switching contexts, reducing wrong-target behavior in worktree flows (thanks to @jwcrystal). +- Desktop/Settings: window vibrancy now correctly controls macOS window transparency, and settings copy now clarifies when full transparency changes take effect. +- Reliability/Proxy: hardened OpenCode proxy header handling (including identity-encoding normalization, compression-header cleanup, hop-by-hop response-header stripping) and suppressed expected SSE close noise, improving stream stability and reducing false proxy errors (thanks to @jwcrystal, @Jovines, @JiwaniZakir, @shekohex). +- Reliability/Proxy: restored proxied chat event streaming so live responses continue working when OpenChamber is deployed behind a proxy. +- Terminal/Reliability: switched terminal transport to a pure WebSocket path with fallback handling, improving responsiveness and stability for interactive terminal sessions (thanks to @geekifan). +- Usage/Providers: added ZhipuAI quota tracking and fixed MiniMax coding-plan and GitHub Copilot overusage calculations for more accurate usage reporting (thanks to @kalac2232, @baruchvitorino, @ebrainte). + +## [1.9.2] - 2026-03-31 + +- Chat/Performance: rebuilt live session sync and streaming updates to cut render churn, reduce CPU spikes, and keep long-running chats smoother and more stable across runtimes. +- Worktrees/Multi-Run: added instant draft-first worktree creation and redesigned the multi-run launcher with a cleaner, faster flow for parallel runs. +- VSCode/UI: polished the extension chat and sidebar with improved spacing/tooltips, a resizable sessions pane, and better file-to-chat mention flows from Explorer. +- Models/Providers: improved custom provider model metadata loading and caching so model details stay more complete and consistent (thanks to @ZeppLu). +- CLI/Server: added `--foreground` for process-manager deployments, made managed server hostname configurable, and added an explicit `--host` option with safer localhost defaults (thanks to @colinmollenhour, @rapidrabbit76, @yulia-ivashko). +- Docker/Deployments: improved container defaults for broader compatibility, including UID 1000 user behavior, non-fatal SSH key generation, and better localhost detection in container networking (thanks to @yulia-ivashko). +- Web/PWA: fixed manifest behavior behind Cloudflare Access so install flows work more reliably in protected environments (thanks to @arthurfiorette). + ## [1.9.1] - 2026-03-20 - Sessions/UI: restored Project Notes access in the sidebar, polished notes/todo editing, and fixed project action overlap so project controls stay reachable for non-git directories. @@ -16,7 +116,6 @@ All notable changes to this project will be documented in this file. - Desktop: improved stale server-process cleanup on startup and fixed external link opening behavior for more predictable app interactions (thanks to @jwcrystal). - Usage: added MiniMax Weekly quota provider support for broader usage tracking coverage (thanks to @nzlov). - ## [1.9.0] - 2026-03-20 - UI/Navigation: delivered a major sidebar redesign with clearer hierarchy, unified action patterns, and improved session organization for better navigation through multiple projects (thanks to @yulia-ivashko). @@ -33,7 +132,6 @@ All notable changes to this project will be documented in this file. - Desktop/macOS: lowered the minimum supported macOS version to Ventura (13.0), expanding compatibility on older systems (thanks to @craigharman). - Updates/Reliability: unified update-check behavior across runtimes for more consistent update availability checks. - ## [1.8.7] - 2026-03-13 - CLI: fixed a startup regression in global npm/bun installs where wrapper or symlinked `openchamber` entrypoints could exit without output on commands like `--version` or `status`. @@ -41,8 +139,6 @@ All notable changes to this project will be documented in this file. - Windows/Web: daemon startup and Git operations no longer flash extra console windows, making background workflows less distracting (thanks to @SergioChan). - Deployment/Docker: improved `docker run` startup behavior and entrypoint handling so containerized installs start more reliably (thanks to @nzlov). - - ## [1.8.6] - 2026-03-13 - Tunnel/CLI: rebuilt tunnel workflows around clearer managed modes and provider-aware lifecycle commands, with safer startup checks, improved diagnostics, and cleaner CLI output for everyday remote access (thanks to @yulia-ivashko). @@ -68,7 +164,6 @@ All notable changes to this project will be documented in this file. - Tunnel/CLI: fixed one-time Cloudflare tunnel connect links in CLI output for `--try-cf-tunnel`, so remote collaborators can use the printed URL/QR flow successfully (thanks to @plfavreau). - Mobile/PWA: respected OS rotation lock by removing forced orientation behavior in the web app shell (thanks to @theluckystrike). - ## [1.8.4] - 2026-03-04 - Chat: added clickable file-path links in assistant messages (including line targeting), so you can jump from answer text straight to the exact file location (thanks to @yulia-ivashko). @@ -89,7 +184,6 @@ All notable changes to this project will be documented in this file. - UI: interactive controls now consistently show pointer cursors, improving click affordance and reducing ambiguous hover states (thanks to @KJdotIO). - Security/Reliability: hardened terminal auth, tightened skill-file path protections, and reduced sensitive request logging exposure for safer day-to-day usage (thanks to @yulia-ivashko). - ## [1.8.3] - 2026-03-02 - Chat: added user-message display controls for plain-text rendering and sticky headers, so you can tune readability to match your preferences. @@ -103,7 +197,6 @@ All notable changes to this project will be documented in this file. - Settings: reorganized chat display settings into a more compact two-column layout, so more new options are easier to navigate. - Mobile/UI: fixed session-title overflow in compact headers so running/unread indicators and actions remain visible (thanks to @iamhenry). - ## [1.8.2] - 2026-03-01 - Updates: hardened the self-update flow with safer release handling and fallback behavior, reducing failed or stuck updates. @@ -116,12 +209,10 @@ All notable changes to this project will be documented in this file. - Notifications/Voice: consolidated TTS and summarization service wiring for steadier text-to-speech and summary flows (thanks to @nelsonPires5). - Deployment: fixed Docker build/runtime issues for more reliable containerized setups (thanks to @nzlov). - ## [1.8.1] - 2026-02-28 - Web/Auth: fixed an issue where non-tunnel browser sessions could incorrectly show a tunnel-only lock screen; normal auth flow now appears unless a tunnel is actually active. - ## [1.8.0] - 2026-02-28 - Desktop: added SSH remote instance support with dedicated lifecycle and UX flows, so you can work against remote machines more reliably (thanks to @shekohex). @@ -147,7 +238,6 @@ All notable changes to this project will be documented in this file. - Usage: added MiniMax coding-plan quota provider support for broader usage tracking coverage (thanks to @nzlov). - Usage: added Ollama Cloud quota provider support for broader usage tracking coverage (thanks to @iamhenry). - ## [1.7.5] - 2026-02-25 - UI: moved projects into a dedicated sidebar rail and tightened the layout so switching projects and sessions feels faster. @@ -159,7 +249,6 @@ All notable changes to this project will be documented in this file. - Web: added `OPENCODE_HOST` support so you can connect directly to an external OpenCode server using a full base URL (thanks to @colinmollenhour). - Web/Mobile: fixed in-app update flow in containerized setups so updates apply correctly. - ## [1.7.4] - 2026-02-24 - Settings: redesigned the settings workspace with flatter, more consistent page layouts so configuration is faster to scan and edit. @@ -176,7 +265,6 @@ All notable changes to this project will be documented in this file. - Desktop: improved remote instance URL handling for more reliable host/query matching (thanks to @shekohex). - Files: added C, C++, and Go language support for syntax-aware rendering in code-heavy workflows (thanks to @fomenks). - ## [1.7.3] - 2026-02-21 - Settings: added customizable keyboard shortcuts for chat actions, panel toggles, and services, so you can better match OpenChamber to your workflow (thanks to @nelsonPires5). @@ -188,7 +276,6 @@ All notable changes to this project will be documented in this file. - Reliability: improved startup environment detection by capturing login-shell environment snapshots, reducing missing PATH/tool issues on launch. - Reliability: refactored OpenCode config/auth integration into domain modules for steadier provider auth and command loading flows (thanks to @nelsonPires5). - ## [1.7.2] - 2026-02-20 - Chat: question prompts now guide you to unanswered items before submit, making tool-question flows faster. @@ -198,7 +285,6 @@ All notable changes to this project will be documented in this file. - Settings: model variant options now refresh correctly in draft/new-session flows, avoiding stale selections. - Reliability: provider auth failures now show clearer re-auth guidance when tokens expire, making recovery faster (thanks to @yulia-ivashko). - ## [1.7.1] - 2026-02-18 - Chat: slash commands now follow server command semantics (including multiline arguments), so command behavior is more consistent with OpenCode CLI. @@ -210,17 +296,15 @@ All notable changes to this project will be documented in this file. - Mobile: fixed accidental abort right after tapping Send on touch devices, reducing interrupted responses (thanks to @shekohex). - Maintenance: removed deprecated GitHub Actions cloud runtime assets and docs to reduce setup confusion (thanks to @yulia-ivashko). - ## [1.7.0] - 2026-02-17 - Chat: improved live streaming with part-delta updates and smarter auto-follow scrolling, so long responses stay readable while they generate. - Chat: Mermaid diagrams now render inline in assistant messages, with quick copy/download actions for easier sharing. - UI: added a context overview panel with token usage, cost breakdown, and raw message inspection to make session debugging easier. - Sessions: project icon and color customizations now persist reliably across restarts. -**- Reliability: managed local OpenCode runtimes now use rotated secure auth and tighter lifecycle control across runtimes, reducing stale-process and reconnect issues (thanks to @yulia-ivashko).** + **- Reliability: managed local OpenCode runtimes now use rotated secure auth and tighter lifecycle control across runtimes, reducing stale-process and reconnect issues (thanks to @yulia-ivashko).** - Git/GitHub: improved backend reliability for repository and auth operations, helping branch and PR flows stay more predictable (thanks to @nelsonPires5). - ## [1.6.9] - 2026-02-16 - **UI: redesigned the workspace shell with a context panel, tabbed sidebars, and quicker navigation across chat, files, and reviews, so daily workflows feel more focused.** @@ -237,7 +321,6 @@ All notable changes to this project will be documented in this file. - Desktop: improved day-to-day polish with restored desktop window geometry and posiotion (thanks to @yulia-ivashko). - Mobile: fixes for small-screen editor, terminal, and layout overlap issues (thanks to @gsxdsm, @nelsonPires5). - ## [1.6.8] - 2026-02-12 - Chat: added drag-and-drop attachments with inline image previews, so sharing screenshots and files in prompts feels much faster and more reliable. @@ -249,7 +332,6 @@ All notable changes to this project will be documented in this file. - Desktop: fixed project selection in opened remote instances. - Desktop: fixed opened remote instances that use HTTP (helpful for instances under tunneling). - ## [1.6.7] - 2026-02-10 - Voice: added built-in voice input and read-aloud responses with multiple providers, so you can drive chats hands-free when typing is slower (thanks to @gsxdsm). @@ -277,7 +359,6 @@ All notable changes to this project will be documented in this file. - Mobile: fixed chat input layout issues on small screens (thanks to @nelsonPires5). - Reliability: fixed OpenCode auth pass-through and proxy env handling to reduce intermittent connection/auth issues (thanks to @gsxdsm). - ## [1.6.5] - 2026-02-6 - Settings: added an OpenCode CLI path override so you can point OpenChamber at a custom/local CLI install. @@ -290,7 +371,6 @@ All notable changes to this project will be documented in this file. - UI: added Vitesse Dark and Vitesse Light theme presets. - Reliability: improved OpenCode binary resolution and HOME-path handling across runtimes for steadier local startup. - ## [1.6.4] - 2026-02-5 - Desktop: switch between local and remote OpenChamber instances, plus a thinner runtime for better feature parity and fewer desktop-only quirks. @@ -304,7 +384,6 @@ All notable changes to this project will be documented in this file. - Web: fixed missing icon when installing the Android PWA (thanks to @nelsonPires5). - GitHub: PR description generation supports optional extra context for better summaries (thanks to @nelsonPires5). - ## [1.6.3] - 2026-02-2 - Web: improved server readiness check to use the `/global/health` endpoint for more reliable startup detection. @@ -312,7 +391,6 @@ All notable changes to this project will be documented in this file. - VSCode: improved server health check with the proper health API endpoint and increased timeout for steadier startup (thanks to @wienans). - Settings: dialog no longer persists open/closed state across app restarts. - ## [1.6.2] - 2026-02-1 - Usage: new multi-provider quota dashboard to monitor API usage across OpenAI, Google, and z.ai (thanks to @nelsonPires5). @@ -324,7 +402,6 @@ All notable changes to this project will be documented in this file. - Worktrees: workspace path now resolves correctly when using git worktrees (thanks to @nelsonPires5). - Projects: fixed directory creation outside workspace in the Add Project modal (thanks to @nelsonPires5). - ## [1.6.1] - 2026-01-30 - Chat: added Stop button to cancel generation mid-response. @@ -337,7 +414,6 @@ All notable changes to this project will be documented in this file. - Git: commit message generation now includes untracked files and handles git diff --no-index comparisons more reliably (thanks to @MrLYC). - Desktop: improved macOS window chrome and header spacing, including steadier traffic lights on older macOS versions (thanks to @yulia-ivashko). - ## [1.6.0] - 2026-01-29 - Chat: added message stall detection with automatic soft resync for more reliable message delivery. @@ -349,7 +425,6 @@ All notable changes to this project will be documented in this file. - Web: session activity tracking now works consistently across browser tabs. - Reliability: plans directory no longer errors when missing. - ## [1.5.9] - 2026-01-28 - Worktrees: migrated to Opencode SDK worktree implementation; sessions in worktrees are now completely isolated. @@ -360,7 +435,6 @@ All notable changes to this project will be documented in this file. - UI: Files, Diff, Git, and Terminal now follow the active session/worktree directory, including new-session drafts. - Web: plan lists no longer error when the plans directory is missing. - ## [1.5.8] - 2026-01-26 - Plans: new Plan/Build mode switching support with dedicated Plan content view with per-session context. @@ -373,14 +447,12 @@ All notable changes to this project will be documented in this file. - Activity: added a text-justification setting for activity summaries (thanks to @iyangdianfeng). - Reliability: file lists and message sends handle missing directories and transient errors more gracefully. - ## [1.5.7] - 2026-01-24 - GitHub: PR panel supports fork PR detection by branch name. - GitHub: Git tab PR panel can send failed checks/comments to chat with hidden context; added check details dialog with Actions step breakdown. - Web: GitHub auth flow fixes. - ## [1.5.6] - 2026-01-24 - GitHub: connect your account in Settings with device-flow auth to enable GitHub tools. @@ -389,7 +461,6 @@ All notable changes to this project will be documented in this file. - Git: manage pull requests in the Git view with AI-generated descriptions, status checks, ready-for-review, and merge actions. - Mobile: fixed CommandAutocomplete dropdown scrolling (thanks to @nelsonPires5). - ## [1.5.5] - 2026-01-23 - Navigation: URLs now sync the active session, tab, settings, and diff state for shareable links and reliable back/forward (thanks to @TaylorBeeston). @@ -398,7 +469,6 @@ All notable changes to this project will be documented in this file. - Web: push notifications no longer fire when a window is visible, avoiding duplicate alerts. - Web: improved push subscription handling across multiple windows for more reliable delivery. - ## [1.5.4] - 2026-01-22 - Chat: new Apply Patch tool UI with diff preview for patch-based edits. @@ -409,7 +479,6 @@ All notable changes to this project will be documented in this file. - Web: added Background notifications for PWA. - Reliability: connect to external OpenCode servers without auto-start and fixed subagent crashes (thanks to @TaylorBeeston). - ## [1.5.3] - 2026-01-20 - Files: edit files inline with syntax highlighting, draft protection, and save/discard flow. @@ -422,7 +491,6 @@ All notable changes to this project will be documented in this file. - Git: generated commit messages now auto-pick a gitmoji when enabled (thanks to @TheRealAshik). - Performance: faster filesystem/search operations and general stability improvements (thanks to @TheRealAshik). - ## [1.5.2] - 2026-01-17 - Sessions: added branch picker dialog to start new worktree sessions from local branches (thanks to @nilskroe). @@ -434,13 +502,11 @@ All notable changes to this project will be documented in this file. - VSCode: tuned layout breakpoint and server readiness timeout for steadier startup. - Reliability: improved OpenCode process cleanup to reduce orphaned servers. - ## [1.5.1] - 2026-01-16 - Desktop: fixed orphaned OpenCode processes not being cleaned up on restart or exit. - Opencode: fixed issue with reloading configuration was killing the app - ## [1.5.0] - 2026-01-16 - UI: added a new Files tab to browse workspace files directly from the interface. @@ -453,7 +519,6 @@ All notable changes to this project will be documented in this file. - Stability: fixed heartbeat race condition causing session stalls during long tasks (thanks to @tybradle). - Desktop: fixed commands for worktree setup access to PATH. - ## [1.4.9] - 2026-01-14 - VSCode: added session editor panel to view sessions alongside files. @@ -462,7 +527,6 @@ All notable changes to this project will be documented in this file. - Mobile: fixed iOS keyboard safe area padding for home indicator bar (thanks to @Jovines). - Upload: increased attachment size limit to 50MB with automatic image compression to 2048px for large files. - ## [1.4.8] - 2026-01-14 - Git Identities: added token-based authentication support with ~/.git-credentials discovery and import. @@ -474,13 +538,11 @@ All notable changes to this project will be documented in this file. - Reliability: improved project state preservation on validation failures (thanks to @vio1ator) and refined server health monitoring. - Stability: added graceful shutdown handling for the server process (thanks to @vio1ator). - ## [1.4.7] - 2026-01-10 - Skills: added ClawdHub integration as built-in market for skills. - Web: fixed issues in terminal - ## [1.4.6] - 2026-01-09 - VSCode/Web: switch opencode cli management to SDK. @@ -488,7 +550,6 @@ All notable changes to this project will be documented in this file. - Shortcuts: switched agent cycling shortcut from Shift + TAB to TAB again. - Chat: added question tool support with a rich UI for interaction. - ## [1.4.5] - 2026-01-08 - Chat: added support for model variants (thinking effort). @@ -499,7 +560,6 @@ All notable changes to this project will be documented in this file. - MCP: added ability to dynamically enabled/disabled configured MCP. - Web: refactored project adding UI with autocomplete. - ## [1.4.4] - 2026-01-08 - Agent Manager / Multi Run: select agent per worktree session (thanks to @wienans). @@ -515,7 +575,6 @@ All notable changes to this project will be documented in this file. - Tunnel: added QR code and password URL for Cloudflare tunnel (thanks to @martindonadieu). - Model selector: fixed dropdowns not responding to viewport size. - ## [1.4.3] - 2026-01-04 - VS Code extension: added Agent Manager panel to run the same prompt across up to 5 models in parallel (thanks to @wienans). @@ -523,7 +582,6 @@ All notable changes to this project will be documented in this file. - Added "Open subAgent session" button on task tool outputs to quickly navigate to child sessions (thanks to @aptdnfapt). - VS Code extension: improved activation reliability and error handling. - ## [1.4.2] - 2026-01-02 - Added timeline dialog (`/timeline` command or Cmd/Ctrl+T) for navigating, reverting, and forking from any point in the conversation (thanks to @aptdnfapt). @@ -532,7 +590,6 @@ All notable changes to this project will be documented in this file. - Desktop app: keyboard shortcuts now use Cmd on macOS and Ctrl on web/other platforms (thanks to @sakhnyuk). - Migrated to OpenCode SDK v2 with improved API types and streaming. - ## [1.4.1] - 2026-01-02 - Added the ability to select the same model multiple times in multi-agent runs for response comparison. @@ -545,7 +602,6 @@ All notable changes to this project will be documented in this file. - Terminal: improved terminal performance and stability by switching to the Ghostty-based terminal renderer, while keeping the existing terminal UX and per-directory sessions. - Terminal: fixed several issues with terminal session restore and rendering under heavy output, including switching directories and long-running TUI apps. - ## [1.4.0] - 2026-01-01 - Added the ability to run multiple agents from a single prompt, with each agent working in an isolated worktree. @@ -557,14 +613,12 @@ All notable changes to this project will be documented in this file. - Chat: now shows clearer error messages when agent messages fail. - Sidebar: improved readability for sticky headers with a dynamic background. - ## [1.3.9] - 2025-12-30 - - Added skills management to settings with the ability to create, edit, and delete skills (make sure you have the latest OpenCode version for skills support). +- Added skills management to settings with the ability to create, edit, and delete skills (make sure you have the latest OpenCode version for skills support). - Added Skills catalog functionality for discovering and installing skills from external sources. - VS Code extension: added right-click context menu with "Add to Context," "Explain," and "Improve Code" actions (thanks to @wienans). - ## [1.3.8] - 2025-12-29 - Added Intel Mac (x86_64) support for the desktop application (thanks to @rothnic). @@ -575,7 +629,6 @@ All notable changes to this project will be documented in this file. - Fixed scroll position persistence for active conversation turns across session switches. - Refactored Agents/Commands management with ability to configure project/user scopes. - ## [1.3.7] - 2025-12-28 - Redesigned Settings as a full-screen view with tabbed navigation. @@ -585,13 +638,11 @@ All notable changes to this project will be documented in this file. - Improved session activity status handling and message step completion logic. - Introduced enchanced VSCode extension settings with dynamic layout based on width. - ## [1.3.6] - 2025-12-27 - Added the ability to manage (connect/disconnect) providers in settings. - Adjusted auto-summarization visuals in chat. - ## [1.3.5] - 2025-12-26 - Added Nushell support for operations with Opencode CLI. @@ -603,14 +654,12 @@ All notable changes to this project will be documented in this file. - Added Discord links in the about section. - Added settings for choosing the default model/agent to start with in a new session. - ## [1.3.4] - 2025-12-25 - Diff view now loads reliably even with large files and slow networks. - Fixed getting diffs for worktree files. - VS Code extension: improved type checking and editor integration. - ## [1.3.3] - 2025-12-25 - Updated OpenCode SDK to 1.0.185 across all app versions. @@ -621,13 +670,11 @@ All notable changes to this project will be documented in this file. - Chat UI: improved turn grouping/activity rendering and fixed message metadata/agent selection propagation. - Chat UI: improved agent activity status behavior and reduced image thumbnail sizes for better readability. - ## [1.3.2] - 2025-12-22 - Fixed new bug session when switching directories - Updated Opencode SDK to the latest version - ## [1.3.1] - 2025-12-22 - New chats no longer create a session until you send your first message. @@ -635,7 +682,6 @@ All notable changes to this project will be documented in this file. - Fixed mobile and VSCode sessions handling - Updated app identity with new logo and icons across all platforms. - ## [1.3.0] - 2025-12-21 - Added revert functionality in chat for user messages. @@ -646,21 +692,18 @@ All notable changes to this project will be documented in this file. - Adjusted VSCode extension theme mapping and model selection view. - Polished file autocomplete experience. - ## [1.2.9] - 2025-12-20 - Session auto‑cleanup feature with configurable retention for each app version including VSCode extension. - Ability to update web package from mobile/PWA view in setting. - A lot of different optimization for a long sessions. - ## [1.2.8] - 2025-12-19 - Introduced update mechanism for web version that doesn't need any cli interaction. - Added installation script for web version with package managed detection. - Update and restart of web server now support automatic pick-up of previously set parameters like port or password. - ## [1.2.7] - 2025-12-19 - Comprehensive macOS native menu bar entries. @@ -668,14 +711,12 @@ All notable changes to this project will be documented in this file. - Improved theme consistency across dropdown menus, selects, and command palette. - Introduced keyboard shortcuts help menu and quick actions menu. - ## [1.2.6] - 2025-12-19 - Added write/create tool preview in permission cards with syntax highlighting. - More descriptive assistant status messages with tool-specific and varied idle phrases. - Polished Git view layout - ## [1.2.5] - 2025-12-19 - Polished chat expirience for longer session. @@ -685,13 +726,11 @@ All notable changes to this project will be documented in this file. - Fixed untracked files in new directories not showing individually. - Smoother session rename experience. - ## [1.2.4] - 2025-12-18 - MacOS app menu entries for Check for update and for creating bug/request in Help section. - For Mobile added settings, improved terminal scrolling, fixed app layout positioning. - ## [1.2.3] - 2025-12-17 - Added image preview support in Diff tab (shows original/modified images instead of base64 code). @@ -699,28 +738,24 @@ All notable changes to this project will be documented in this file. - Optimized git polling and background diff+syntax pre-warm for instant Diff tab open. - Optomized reloading unaffected diffs. - ## [1.2.2] - 2025-12-17 - Agent Task tool now renders progressively with live duration and completed sub-tools summary. - Unified markdown rendering between assistant messages and tool outputs. - Reduced markdown header sizes for better visual balance. - ## [1.2.1] - 2025-12-16 - Todo task tracking: collapsible status row showing AI's current task and progress. - Switched "Detailed" tool output mode to only open the 'task', 'edit', 'multiedit', 'write', 'bash' tools for better performance. - ## [1.2.0] - 2025-12-15 - Favorite & recent models for quick access in model selection. - Tool call expansion settings: collapsed, activity, or detailed modes. - Font size & spacing controls (50-200% scaling) in Appearance Settings. - Settings page access within VSCode extension. -Thanks to @theblazehen for contributing these features! - + Thanks to @theblazehen for contributing these features! ## [1.1.6] - 2025-12-15 @@ -728,27 +763,23 @@ Thanks to @theblazehen for contributing these features! - Improved mobile experience: simplified header, better diff file selector. - Redesigned password-protected session unlock screen. - ## [1.1.5] - 2025-12-15 - Enhanced file attachment features performance. - Added fuzzy search feature for file mentioning with @ in chat. - Optimized input area layout. - ## [1.1.4] - 2025-12-15 - Flexoki themes for Shiki syntax highlighting for consistency with the app color schema. - Enchanced VSCode extension theming with editor themes. - Fixed mobile view model/agent selection. - ## [1.1.3] - 2025-12-14 - Replaced Monaco diff editor with Pierre/diffs for better performance. - Added line wrap toggle in diff view with dynamic layout switching (auto-inline when narrow). - ## [1.1.2] - 2025-12-13 - Moved VS Code extension to activity bar (left sidebar). @@ -756,13 +787,11 @@ Thanks to @theblazehen for contributing these features! - Removed redundant VS Code commands. - Enhanced UserTextPart styling. - ## [1.1.1] - 2025-12-13 - Adjusted model/agent selection alignment. - Fixed user message rendering issues. - ## [1.1.0] - 2025-12-13 - Added assistant answer fork flow so users can start a new session from an assistant plan/response with inherited context. @@ -770,7 +799,6 @@ Thanks to @theblazehen for contributing these features! - Improved scroll performance with force flag and RAF placeholder. - Added git polling backoff optimization. - ## [1.0.9] - 2025-12-08 - Added directory picker on first launch to reduce macOS permission prompts. @@ -778,48 +806,40 @@ Thanks to @theblazehen for contributing these features! - Improved update dialog UI with inline version display. - Added macOS folder access usage descriptions. - ## [1.0.8] - 2025-12-08 - Added fallback detection for OpenCode CLI in ~/.opencode/bin. - Added window focus after app restart/update. - Adapted traffic lights position and corner radius for older macOS versions. - ## [1.0.7] - 2025-12-08 - Optimized Opencode binary detection. - Adjusted app update experience. - ## [1.0.6] - 2025-12-08 - Enhance shell environment detection. - ## [1.0.5] - 2025-12-07 - Fixed "Load older messages" incorrectly scrolling to bottom. - Fixed page refresh getting stuck on splash screen. - Disabled devtools and page refresh in production builds. - ## [1.0.4] - 2025-12-07 - Optimized desktop app start time - ## [1.0.3] - 2025-12-07 - Updated onboarding UI. - Updated sidebar styles. - ## [1.0.2] - 2025-12-07 - Updated MacOS window design to the latest one. - ## [1.0.1] - 2025-12-07 - Initial public release of OpenChamber web and desktop packages in a unified monorepo. diff --git a/src/README.md b/src/README.md index bbe6f9d..593c849 100644 --- a/src/README.md +++ b/src/README.md @@ -241,6 +241,11 @@ environment: Managed-local path note: `OPENCHAMBER_TUNNEL_CONFIG` must point to a path inside the container user home (`/home/openchamber/...`). If your Cloudflare config references a credentials JSON file, that file path must also be accessible inside the container (mount with `volumes`). +### Reverse proxy notes + +- For a complete reverse proxy setup guide, see [`docs/REVERSE_PROXY.md`](./docs/REVERSE_PROXY.md). +- Website docs source lives at `packages/docs/content/docs/reverse-proxy.mdx`. + ### Tunnel behavior notes - OpenChamber supports one active tunnel per running instance (port). diff --git a/src/bun.lock b/src/bun.lock index daaa0f7..7c0efe1 100644 --- a/src/bun.lock +++ b/src/bun.lock @@ -5,6 +5,7 @@ "": { "name": "openchamber-monorepo", "dependencies": { + "@base-ui/react": "^1.4.0", "@codemirror/autocomplete": "^6.20.0", "@codemirror/commands": "^6.10.1", "@codemirror/lang-cpp": "^6.0.3", @@ -19,11 +20,11 @@ "@codemirror/lang-sql": "^6.10.0", "@codemirror/lang-xml": "^6.1.0", "@codemirror/lang-yaml": "^6.1.2", - "@codemirror/language": "^6.12.1", + "@codemirror/language": "6.12.2", "@codemirror/lint": "^6.9.2", "@codemirror/search": "^6.6.0", "@codemirror/state": "^6.5.4", - "@codemirror/view": "^6.39.13", + "@codemirror/view": "6.39.13", "@fontsource/ibm-plex-mono": "^5.2.7", "@fontsource/ibm-plex-sans": "^5.1.1", "@heroui/scroll-shadow": "^2.3.18", @@ -32,7 +33,7 @@ "@ibm/plex": "^6.4.1", "@lezer/highlight": "^1.2.3", "@octokit/rest": "^22.0.1", - "@opencode-ai/sdk": "^1.3.0", + "@opencode-ai/sdk": "^1.4.25", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", @@ -94,17 +95,33 @@ }, "packages/desktop": { "name": "@openchamber/desktop", - "version": "1.9.1", + "version": "1.9.8", "devDependencies": { "@tauri-apps/cli": "^2", "@types/node": "^24.3.1", "typescript": "~5.8.3", }, }, + "packages/electron": { + "name": "@openchamber/electron", + "version": "1.9.8", + "dependencies": { + "@openchamber/web": "workspace:*", + "electron-context-menu": "^4.1.2", + "electron-log": "^5.4.3", + "electron-updater": "^6.8.3", + }, + "devDependencies": { + "@electron/rebuild": "^3.7.0", + "electron": "^41.2.1", + "electron-builder": "^26.0.0", + }, + }, "packages/ui": { "name": "@openchamber/ui", - "version": "1.9.1", + "version": "1.9.8", "dependencies": { + "@base-ui/react": "^1.4.0", "@codemirror/autocomplete": "^6.20.0", "@codemirror/commands": "^6.10.1", "@codemirror/lang-cpp": "^6.0.3", @@ -119,13 +136,13 @@ "@codemirror/lang-sql": "^6.10.0", "@codemirror/lang-xml": "^6.1.0", "@codemirror/lang-yaml": "^6.1.2", - "@codemirror/language": "^6.12.1", + "@codemirror/language": "6.12.2", "@codemirror/language-data": "^6.5.2", "@codemirror/legacy-modes": "^6.5.2", "@codemirror/lint": "^6.9.2", "@codemirror/search": "^6.6.0", "@codemirror/state": "^6.5.4", - "@codemirror/view": "^6.39.13", + "@codemirror/view": "6.39.13", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", @@ -133,20 +150,10 @@ "@fontsource/ibm-plex-sans": "^5.1.1", "@ibm/plex": "^6.4.1", "@lezer/highlight": "^1.2.3", - "@opencode-ai/sdk": "^1.3.0", + "@opencode-ai/sdk": "^1.4.25", "@pierre/diffs": "1.1.0-beta.13", - "@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-switch": "^1.2.6", - "@radix-ui/react-toggle": "^1.1.10", - "@radix-ui/react-tooltip": "^1.2.8", "@remixicon/react": "^4.7.0", - "@streamdown/code": "^1.0.2", + "@simplewebauthn/browser": "13.3.0", "@tanstack/react-virtual": "^3.13.18", "@types/react-syntax-highlighter": "^15.5.13", "beautiful-mermaid": "^1.1.3", @@ -154,12 +161,16 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "codemirror-lang-elixir": "^4.0.0", + "dompurify": "^3.2.7", "express": "^5.1.0", "fuse.js": "^7.1.0", "ghostty-web": "^0.4.0", "heic2any": "^0.0.4", "html-to-image": "^1.11.13", "http-proxy-middleware": "^3.0.5", + "katex": "^0.16.21", + "marked": "^17.0.3", + "morphdom": "^2.7.7", "motion": "^12.23.24", "next-themes": "^0.4.6", "prismjs": "^1.30.0", @@ -167,9 +178,11 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "react-syntax-highlighter": "^15.6.6", + "rehype-katex": "^7.0.1", + "remark-math": "^6.0.0", + "remend": "^1.2.1", "simple-git": "^3.28.0", "sonner": "^2.0.7", - "streamdown": "^2.2.0", "strip-json-comments": "^5.0.3", "tailwind-merge": "^3.3.1", "yaml": "^2.8.1", @@ -179,7 +192,7 @@ "devDependencies": { "@eslint/js": "^9.33.0", "@tailwindcss/postcss": "^4.0.0", - "@tauri-apps/api": "^2.9.0", + "@tauri-apps/api": "^2.10.1", "@types/node": "^24.3.1", "@types/prismjs": "^1.26.6", "@types/qrcode": "^1.5.5", @@ -205,10 +218,10 @@ }, "packages/vscode": { "name": "openchamber", - "version": "1.9.1", + "version": "1.9.8", "dependencies": { "@openchamber/ui": "workspace:*", - "@opencode-ai/sdk": "^1.3.0", + "@opencode-ai/sdk": "^1.4.25", "adm-zip": "^0.5.16", "jsonc-parser": "^3.3.1", "react": "^19.1.1", @@ -228,19 +241,42 @@ }, "packages/web": { "name": "@openchamber/web", - "version": "1.9.1", + "version": "1.9.8", "bin": { "openchamber": "./bin/cli.js", }, "dependencies": { "@clack/prompts": "^1.1.0", + "@octokit/rest": "^22.0.1", + "@opencode-ai/sdk": "^1.4.25", + "@simplewebauthn/server": "13.3.0", + "adm-zip": "^0.5.16", + "better-sqlite3": "^11.7.0", + "bun-pty": "^0.4.5", + "compression": "^1.8.1", + "cron-parser": "^4.9.0", + "express": "^5.1.0", + "http-proxy-middleware": "^3.0.5", + "jose": "^6.1.3", + "jsonc-parser": "^3.3.1", + "luxon": "^3.5.0", + "node-pty": "1.2.0-beta.12", + "openai": "^4.79.0", + "qrcode-terminal": "^0.12.0", + "reflect-metadata": "^0.2.2", + "simple-git": "^3.28.0", + "web-push": "^3.6.7", + "ws": "^8.18.3", + "yaml": "^2.8.1", + }, + "devDependencies": { + "@base-ui/react": "^1.4.0", "@codemirror/lang-cpp": "^6.0.3", "@codemirror/lang-go": "^6.0.1", + "@eslint/js": "^9.33.0", "@fontsource/ibm-plex-mono": "^5.2.7", "@fontsource/ibm-plex-sans": "^5.1.1", "@ibm/plex": "^6.4.1", - "@octokit/rest": "^22.0.1", - "@opencode-ai/sdk": "^1.3.0", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", @@ -251,52 +287,38 @@ "@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", - "adm-zip": "^0.5.16", - "bun-pty": "^0.4.5", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "cmdk": "^1.1.1", - "express": "^5.1.0", - "ghostty-web": "0.3.0", - "http-proxy-middleware": "^3.0.5", - "jose": "^6.1.3", - "jsonc-parser": "^3.3.1", - "next-themes": "^0.4.6", - "node-pty": "1.2.0-beta.12", - "openai": "^4.79.0", - "qrcode-terminal": "^0.12.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", - "web-push": "^3.6.7", - "ws": "^8.18.3", - "yaml": "^2.8.1", - "zustand": "^5.0.8", - }, - "devDependencies": { - "@eslint/js": "^9.33.0", + "@simplewebauthn/browser": "13.3.0", "@tailwindcss/postcss": "^4.0.0", "@types/adm-zip": "^0.5.7", "@types/node": "^24.3.1", "@types/react": "^19.1.10", "@types/react-dom": "^19.1.7", + "@types/react-syntax-highlighter": "^15.5.13", + "@types/supertest": "^7.2.0", "@vitejs/plugin-react": "^5.0.0", "autoprefixer": "^10.4.21", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", "concurrently": "^9.2.1", "cors": "^2.8.5", "cross-env": "^7.0.3", "eslint": "^9.33.0", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.20", + "ghostty-web": "0.3.0", "globals": "^16.3.0", + "next-themes": "^0.4.6", "nodemon": "^3.1.7", + "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", + "sonner": "^2.0.7", + "strip-json-comments": "^5.0.3", + "supertest": "^7.2.2", + "tailwind-merge": "^3.3.1", "tailwindcss": "^4.0.0", "tsx": "^4.20.6", "tw-animate-css": "^1.3.8", @@ -304,7 +326,9 @@ "typescript-eslint": "^8.39.1", "vite": "^7.1.2", "vite-plugin-pwa": "^1.0.3", + "vitest": "^4.1.4", "workbox-window": "^7.4.0", + "zustand": "^5.0.8", }, }, }, @@ -313,6 +337,8 @@ "@codemirror/view": "6.39.13", }, "packages": { + "7zip-bin": ["7zip-bin@5.2.0", "", {}, "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A=="], + "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], "@apideck/better-ajv-errors": ["@apideck/better-ajv-errors@0.3.6", "", { "dependencies": { "json-schema": "^0.4.0", "jsonpointer": "^5.0.0", "leven": "^3.1.0" }, "peerDependencies": { "ajv": ">=8" } }, "sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA=="], @@ -519,7 +545,7 @@ "@babel/preset-modules": ["@babel/preset-modules@0.1.6-no-external-plugins", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/types": "^7.4.4", "esutils": "^2.0.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA=="], - "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], @@ -527,6 +553,10 @@ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + "@base-ui/react": ["@base-ui/react@1.4.0", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@base-ui/utils": "0.2.7", "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@date-fns/tz": "^1.2.0", "@types/react": "^17 || ^18 || ^19", "date-fns": "^4.0.0", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-QcqdVbr/+ba2/RAKJIV1PV6S02Q5+r6a4Eym8ndBw+ZbBILkkmQAyRxXCg/pArrHnkrGeU8goe26aw0h6eE8pg=="], + + "@base-ui/utils": ["@base-ui/utils@0.2.7", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-nXYKhiL/0JafyJE8PfcflipGftOftlIwKd72rU15iZ1M5yqgg5J9P8NHU71GReDuXco5MJA/eVQqUT5WRqX9sA=="], + "@clack/core": ["@clack/core@1.1.0", "", { "dependencies": { "sisteransi": "^1.0.5" } }, "sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA=="], "@clack/prompts": ["@clack/prompts@1.1.0", "", { "dependencies": { "@clack/core": "1.1.0", "sisteransi": "^1.0.5" } }, "sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g=="], @@ -591,6 +621,10 @@ "@codemirror/view": ["@codemirror/view@6.39.13", "", { "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-QBO8ZsgJLCbI28KdY0/oDy5NQLqOQVZCozBknxc2/7L98V+TVYFHnfaCsnGh1U+alpd2LOkStVwYY7nW2R1xbw=="], + "@date-fns/tz": ["@date-fns/tz@1.4.1", "", {}, "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA=="], + + "@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="], + "@dnd-kit/accessibility": ["@dnd-kit/accessibility@3.1.1", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw=="], "@dnd-kit/core": ["@dnd-kit/core@6.3.1", "", { "dependencies": { "@dnd-kit/accessibility": "^3.1.1", "@dnd-kit/utilities": "^3.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ=="], @@ -599,6 +633,24 @@ "@dnd-kit/utilities": ["@dnd-kit/utilities@3.2.2", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg=="], + "@electron/asar": ["@electron/asar@3.4.1", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA=="], + + "@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="], + + "@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="], + + "@electron/node-gyp": ["@electron/node-gyp@github:electron/node-gyp#06b29aa", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "glob": "^8.1.0", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.2.1", "nopt": "^6.0.0", "proc-log": "^2.0.1", "semver": "^7.3.5", "tar": "^6.2.1", "which": "^2.0.2" }, "bin": "./bin/node-gyp.js" }, "electron-node-gyp-06b29aa"], + + "@electron/notarize": ["@electron/notarize@2.5.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.1", "promise-retry": "^2.0.1" } }, "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A=="], + + "@electron/osx-sign": ["@electron/osx-sign@1.3.3", "", { "dependencies": { "compare-version": "^0.1.2", "debug": "^4.3.4", "fs-extra": "^10.0.0", "isbinaryfile": "^4.0.8", "minimist": "^1.2.6", "plist": "^3.0.5" }, "bin": { "electron-osx-flat": "bin/electron-osx-flat.js", "electron-osx-sign": "bin/electron-osx-sign.js" } }, "sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg=="], + + "@electron/rebuild": ["@electron/rebuild@3.7.2", "", { "dependencies": { "@electron/node-gyp": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", "@malept/cross-spawn-promise": "^2.0.0", "chalk": "^4.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", "fs-extra": "^10.0.0", "got": "^11.7.0", "node-abi": "^3.45.0", "node-api-version": "^0.2.0", "ora": "^5.1.0", "read-binary-file-arch": "^1.0.6", "semver": "^7.3.5", "tar": "^6.0.5", "yargs": "^17.0.1" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-19/KbIR/DAxbsCkiaGMXIdPnMCJLkcf8AvGnduJtWBs/CBwiAjY1apCqOLVxrXg+rtXFCngbXhBanWjxLUt1Mg=="], + + "@electron/universal": ["@electron/universal@2.0.3", "", { "dependencies": { "@electron/asar": "^3.3.1", "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", "dir-compare": "^4.2.0", "fs-extra": "^11.1.1", "minimatch": "^9.0.3", "plist": "^3.1.0" } }, "sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g=="], + + "@electron/windows-sign": ["@electron/windows-sign@1.2.2", "", { "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", "fs-extra": "^11.1.1", "minimist": "^1.2.8", "postject": "^1.0.0-alpha.6" }, "bin": { "electron-windows-sign": "bin/electron-windows-sign.js" } }, "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.24.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q=="], @@ -669,13 +721,13 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], - "@floating-ui/core": ["@floating-ui/core@1.7.4", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg=="], + "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="], - "@floating-ui/dom": ["@floating-ui/dom@1.7.5", "", { "dependencies": { "@floating-ui/core": "^1.7.4", "@floating-ui/utils": "^0.2.10" } }, "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg=="], + "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="], - "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.7", "", { "dependencies": { "@floating-ui/dom": "^1.7.5" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg=="], + "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="], - "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], + "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], "@fontsource/ibm-plex-mono": ["@fontsource/ibm-plex-mono@5.2.7", "", {}, "sha512-MKAb8qV+CaiMQn2B0dIi1OV3565NYzp3WN5b4oT6LTkk+F0jR6j0ZN+5BKJiIhffDC3rtBULsYZE65+0018z9w=="], @@ -691,6 +743,8 @@ "@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.6.2", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA=="], + "@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="], + "@heroui/react-rsc-utils": ["@heroui/react-rsc-utils@2.1.9", "", { "peerDependencies": { "react": ">=18 || >=19.0.0-rc.0" } }, "sha512-e77OEjNCmQxE9/pnLDDb93qWkX58/CcgIqdNAczT/zUP+a48NxGq2A2WRimvc1uviwaNL2StriE2DmyZPyYW7Q=="], "@heroui/react-utils": ["@heroui/react-utils@2.1.14", "", { "dependencies": { "@heroui/react-rsc-utils": "2.1.9", "@heroui/shared-utils": "2.1.12" }, "peerDependencies": { "react": ">=18 || >=19.0.0-rc.0" } }, "sha512-hhKklYKy9sRH52C9A8P0jWQ79W4MkIvOnKBIuxEMHhigjfracy0o0lMnAUdEsJni4oZKVJYqNGdQl+UVgcmeDA=="], @@ -707,6 +761,8 @@ "@heroui/use-data-scroll-overflow": ["@heroui/use-data-scroll-overflow@2.2.13", "", { "dependencies": { "@heroui/shared-utils": "2.1.12" }, "peerDependencies": { "react": ">=18 || >=19.0.0-rc.0" } }, "sha512-zboLXO1pgYdzMUahDcVt5jf+l1jAQ/D9dFqr7AxWLfn6tn7/EgY0f6xIrgWDgJnM0U3hKxVeY13pAeB4AFTqTw=="], + "@hexagon/base64": ["@hexagon/base64@1.1.28", "", {}, "sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw=="], + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="], @@ -729,6 +785,8 @@ "@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="], + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -745,6 +803,8 @@ "@kwsites/promise-deferred": ["@kwsites/promise-deferred@1.1.1", "", {}, "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="], + "@levischuck/tiny-cbor": ["@levischuck/tiny-cbor@0.2.11", "", {}, "sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow=="], + "@lezer/common": ["@lezer/common@1.5.1", "", {}, "sha512-6YRVG9vBkaY7p1IVxL4s44n5nUnaNnGM2/AckNgYOnxTG2kWh1vR8BMxPseWPjRNpb5VtXnMpeYAEAADoRV1Iw=="], "@lezer/cpp": ["@lezer/cpp@1.1.5", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.0.0" } }, "sha512-DIhSXmYtJKLehrjzDFN+2cPt547ySQ41nA8yqcDf/GxMc+YM736xqltFkvADL2M0VebU5I+3+4ks2Vv+Kyq3Aw=="], @@ -779,14 +839,26 @@ "@lezer/yaml": ["@lezer/yaml@1.0.4", "", { "dependencies": { "@lezer/common": "^1.2.0", "@lezer/highlight": "^1.0.0", "@lezer/lr": "^1.4.0" } }, "sha512-2lrrHqxalACEbxIbsjhqGpSW8kWpUKuY6RHgnSAFZa6qK62wvnPxA8hGOwOoDbwHcOFs5M4o27mjGu+P7TvBmw=="], + "@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@2.0.0", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg=="], + + "@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="], + "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="], + "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@npmcli/agent": ["@npmcli/agent@3.0.0", "", { "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.1", "lru-cache": "^10.0.1", "socks-proxy-agent": "^8.0.3" } }, "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q=="], + + "@npmcli/fs": ["@npmcli/fs@2.1.2", "", { "dependencies": { "@gar/promisify": "^1.1.3", "semver": "^7.3.5" } }, "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ=="], + + "@npmcli/move-file": ["@npmcli/move-file@2.0.1", "", { "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ=="], + "@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], "@octokit/core": ["@octokit/core@7.0.6", "", { "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.3", "@octokit/request": "^10.0.6", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", "before-after-hook": "^4.0.0", "universal-user-agent": "^7.0.0" } }, "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q=="], @@ -813,14 +885,44 @@ "@openchamber/desktop": ["@openchamber/desktop@workspace:packages/desktop"], + "@openchamber/electron": ["@openchamber/electron@workspace:packages/electron"], + "@openchamber/ui": ["@openchamber/ui@workspace:packages/ui"], "@openchamber/web": ["@openchamber/web@workspace:packages/web"], - "@opencode-ai/sdk": ["@opencode-ai/sdk@1.3.0", "", {}, "sha512-5WyYEpcV6Zk9otXOMIrvZRbJm1yxt/c8EXSBn1p6Sw1yagz8HRljkoUTJFxzD0x2+/6vAZItr3OrXDZfE+oA2g=="], + "@opencode-ai/sdk": ["@opencode-ai/sdk@1.14.19", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-9sTGsi8/HlBBeaWfsUjdJ2yi/SqpRvqSld0IFXc3ldaPb1w1uIPvgCGzhlHYQtqatXxSaX5lTN7zpudMaE21aw=="], + + "@paralleldrive/cuid2": ["@paralleldrive/cuid2@2.3.1", "", { "dependencies": { "@noble/hashes": "^1.1.5" } }, "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw=="], + + "@peculiar/asn1-android": ["@peculiar/asn1-android@2.6.0", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-cBRCKtYPF7vJGN76/yG8VbxRcHLPF3HnkoHhKOZeHpoVtbMYfY9ROKtH3DtYUY9m8uI1Mh47PRhHf2hSK3xcSQ=="], + + "@peculiar/asn1-cms": ["@peculiar/asn1-cms@2.6.1", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", "@peculiar/asn1-x509-attr": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw=="], + + "@peculiar/asn1-csr": ["@peculiar/asn1-csr@2.6.1", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w=="], + + "@peculiar/asn1-ecc": ["@peculiar/asn1-ecc@2.6.1", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g=="], + + "@peculiar/asn1-pfx": ["@peculiar/asn1-pfx@2.6.1", "", { "dependencies": { "@peculiar/asn1-cms": "^2.6.1", "@peculiar/asn1-pkcs8": "^2.6.1", "@peculiar/asn1-rsa": "^2.6.1", "@peculiar/asn1-schema": "^2.6.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw=="], + + "@peculiar/asn1-pkcs8": ["@peculiar/asn1-pkcs8@2.6.1", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw=="], + + "@peculiar/asn1-pkcs9": ["@peculiar/asn1-pkcs9@2.6.1", "", { "dependencies": { "@peculiar/asn1-cms": "^2.6.1", "@peculiar/asn1-pfx": "^2.6.1", "@peculiar/asn1-pkcs8": "^2.6.1", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", "@peculiar/asn1-x509-attr": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw=="], + + "@peculiar/asn1-rsa": ["@peculiar/asn1-rsa@2.6.1", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA=="], + + "@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.6.0", "", { "dependencies": { "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg=="], + + "@peculiar/asn1-x509": ["@peculiar/asn1-x509@2.6.1", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } }, "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA=="], + + "@peculiar/asn1-x509-attr": ["@peculiar/asn1-x509-attr@2.6.1", "", { "dependencies": { "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ=="], + + "@peculiar/x509": ["@peculiar/x509@1.14.3", "", { "dependencies": { "@peculiar/asn1-cms": "^2.6.0", "@peculiar/asn1-csr": "^2.6.0", "@peculiar/asn1-ecc": "^2.6.0", "@peculiar/asn1-pkcs9": "^2.6.0", "@peculiar/asn1-rsa": "^2.6.0", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.0", "pvtsutils": "^1.3.6", "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "tsyringe": "^4.10.0" } }, "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA=="], "@pierre/diffs": ["@pierre/diffs@1.1.0-beta.13", "", { "dependencies": { "@shikijs/transformers": "^3.0.0", "diff": "8.0.3", "hast-util-to-html": "9.0.5", "lru_map": "0.4.1", "shiki": "^3.0.0" }, "peerDependencies": { "react": "^18.3.1 || ^19.0.0", "react-dom": "^18.3.1 || ^19.0.0" } }, "sha512-D35rxDu5V7XHX5aVGU6PF12GhscL+I+9QYgxK/i3h0d2XSirAxDdVNm49aYwlOhgmdvL0NbS1IHxPswVB5yJvw=="], + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], @@ -869,8 +971,6 @@ "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], - "@radix-ui/react-switch": ["@radix-ui/react-switch@1.2.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ=="], - "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="], "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], @@ -1025,14 +1125,22 @@ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@simplewebauthn/browser": ["@simplewebauthn/browser@13.3.0", "", {}, "sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ=="], + + "@simplewebauthn/server": ["@simplewebauthn/server@13.3.0", "", { "dependencies": { "@hexagon/base64": "^1.1.27", "@levischuck/tiny-cbor": "^0.2.2", "@peculiar/asn1-android": "^2.6.0", "@peculiar/asn1-ecc": "^2.6.1", "@peculiar/asn1-rsa": "^2.6.1", "@peculiar/asn1-schema": "^2.6.0", "@peculiar/asn1-x509": "^2.6.1", "@peculiar/x509": "^1.14.3" } }, "sha512-MLHYFrYG8/wK2i+86XMhiecK72nMaHKKt4bo+7Q1TbuG9iGjlSdfkPWKO5ZFE/BX+ygCJ7pr8H/AJeyAj1EaTQ=="], + + "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@2.3.0", "", {}, "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg=="], - "@streamdown/code": ["@streamdown/code@1.0.3", "", { "dependencies": { "shiki": "^3.19.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0" } }, "sha512-3Ym5TCLcGhrHY2qBaUVWpqNRtxnZvqh4Y5Qm/pTIKA4AmEWwAAoYjZnxG7mOsvOpWVWiDwETjUtchNL1XzQEAw=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@surma/rollup-plugin-off-main-thread": ["@surma/rollup-plugin-off-main-thread@2.2.3", "", { "dependencies": { "ejs": "^3.1.6", "json5": "^2.2.0", "magic-string": "^0.25.0", "string.prototype.matchall": "^4.0.6" } }, "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ=="], "@swc/helpers": ["@swc/helpers@0.5.19", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-QamiFeIK3txNjgUTNppE6MiG3p7TdninpZu0E0PbqVh1a9FNLT2FRhisaa4NcaX52XVhA5l7Pk58Ft7Sqi/2sA=="], + "@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="], + "@tailwindcss/node": ["@tailwindcss/node@4.2.1", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.1" } }, "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg=="], "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.1", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.1", "@tailwindcss/oxide-darwin-arm64": "4.2.1", "@tailwindcss/oxide-darwin-x64": "4.2.1", "@tailwindcss/oxide-freebsd-x64": "4.2.1", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", "@tailwindcss/oxide-linux-x64-musl": "4.2.1", "@tailwindcss/oxide-wasm32-wasi": "4.2.1", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw=="], @@ -1103,6 +1211,8 @@ "@textlint/types": ["@textlint/types@15.5.2", "", { "dependencies": { "@textlint/ast-node-types": "15.5.2" } }, "sha512-sJOrlVLLXp4/EZtiWKWq9y2fWyZlI8GP+24rnU5avtPWBIMm/1w97yzKrAqYF8czx2MqR391z5akhnfhj2f/AQ=="], + "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="], + "@types/adm-zip": ["@types/adm-zip@0.5.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], @@ -1113,22 +1223,40 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], + "@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/cookiejar": ["@types/cookiejar@2.1.5", "", {}, "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q=="], + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/dom-speech-recognition": ["@types/dom-speech-recognition@0.0.7", "", {}, "sha512-NjiUoJbBlKhyufNsMZLSp+pbPNtPAFnR738RCJvtZy/HVQ2TZjmqpMyaeOSMXgxdfZM60nt8QGbtfmQrJAH2sw=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], + "@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="], + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/http-cache-semantics": ["@types/http-cache-semantics@4.2.0", "", {}, "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q=="], + "@types/http-proxy": ["@types/http-proxy@1.17.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + "@types/katex": ["@types/katex@0.16.8", "", {}, "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg=="], + + "@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="], + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + "@types/methods": ["@types/methods@1.1.4", "", {}, "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ=="], + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], "@types/node": ["@types/node@24.10.15", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-BgjLoRuSr0MTI5wA6gMw9Xy0sFudAaUuvrnjgGx9wZ522fYYLA5SYJ+1Y30vTcJEG+DRCyDHx/gzQVfofYzSdg=="], @@ -1137,6 +1265,8 @@ "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], + "@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="], + "@types/prismjs": ["@types/prismjs@1.26.6", "", {}, "sha512-vqlvI7qlMvcCBbVe0AKAb4f97//Hy0EBTaiW8AalRnG/xAN5zOiWWyrNqNXeq8+KAuvRewjCVY1+IPxk4RdNYw=="], "@types/qrcode": ["@types/qrcode@1.5.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw=="], @@ -1149,14 +1279,24 @@ "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="], + "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], + "@types/sarif": ["@types/sarif@2.1.7", "", {}, "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ=="], + "@types/superagent": ["@types/superagent@8.1.9", "", { "dependencies": { "@types/cookiejar": "^2.1.5", "@types/methods": "^1.1.4", "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-pTVjI73witn+9ILmoJdajHGW2jkSaOzhiFYF1Rd3EQ94kymLqB9PjD9ISg7WaALC7+dCHT0FGe9T2LktLq/3GQ=="], + + "@types/supertest": ["@types/supertest@7.2.0", "", { "dependencies": { "@types/methods": "^1.1.4", "@types/superagent": "^8.1.0" } }, "sha512-uh2Lv57xvggst6lCqNdFAmDSvoMG7M/HDtX4iUCquxQ5EGPtaPM5PL5Hmi7LCvOG8db7YaCPNJEeoI8s/WzIQw=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@types/verror": ["@types/verror@1.10.11", "", {}, "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg=="], + "@types/vscode": ["@types/vscode@1.109.0", "", {}, "sha512-0Pf95rnwEIwDbmXGC08r0B4TQhAbsHQ5UyTIgVgoieDe4cOnf92usuR5dEczb6bTKEp7ziZH4TV1TRGPPCExtw=="], + "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/type-utils": "8.56.1", "@typescript-eslint/utils": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg=="], @@ -1183,6 +1323,20 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@5.1.4", "", { "dependencies": { "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", "react-refresh": "^0.18.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-VIcFLdRi/VYRU8OL/puL7QXMYafHmqOnwTZY50U1JPlCNj30PxCMx65c494b1K9be9hX83KVt0+gTEwTWLqToA=="], + "@vitest/expect": ["@vitest/expect@4.1.5", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.5", "@vitest/utils": "4.1.5", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw=="], + + "@vitest/mocker": ["@vitest/mocker@4.1.5", "", { "dependencies": { "@vitest/spy": "4.1.5", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@4.1.5", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g=="], + + "@vitest/runner": ["@vitest/runner@4.1.5", "", { "dependencies": { "@vitest/utils": "4.1.5", "pathe": "^2.0.3" } }, "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ=="], + + "@vitest/snapshot": ["@vitest/snapshot@4.1.5", "", { "dependencies": { "@vitest/pretty-format": "4.1.5", "@vitest/utils": "4.1.5", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ=="], + + "@vitest/spy": ["@vitest/spy@4.1.5", "", {}, "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ=="], + + "@vitest/utils": ["@vitest/utils@4.1.5", "", { "dependencies": { "@vitest/pretty-format": "4.1.5", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug=="], + "@vscode/vsce": ["@vscode/vsce@3.7.1", "", { "dependencies": { "@azure/identity": "^4.1.0", "@secretlint/node": "^10.1.2", "@secretlint/secretlint-formatter-sarif": "^10.1.2", "@secretlint/secretlint-rule-no-dotenv": "^10.1.2", "@secretlint/secretlint-rule-preset-recommend": "^10.1.2", "@vscode/vsce-sign": "^2.0.0", "azure-devops-node-api": "^12.5.0", "chalk": "^4.1.2", "cheerio": "^1.0.0-rc.9", "cockatiel": "^3.1.2", "commander": "^12.1.0", "form-data": "^4.0.0", "glob": "^11.0.0", "hosted-git-info": "^4.0.2", "jsonc-parser": "^3.2.0", "leven": "^3.1.0", "markdown-it": "^14.1.0", "mime": "^1.3.4", "minimatch": "^3.0.3", "parse-semver": "^1.1.1", "read": "^1.0.7", "secretlint": "^10.1.2", "semver": "^7.5.2", "tmp": "^0.2.3", "typed-rest-client": "^1.8.4", "url-join": "^4.0.1", "xml2js": "^0.5.0", "yauzl": "^2.3.1", "yazl": "^2.2.2" }, "optionalDependencies": { "keytar": "^7.7.0" }, "bin": { "vsce": "vsce" } }, "sha512-OTm2XdMt2YkpSn2Nx7z2EJtSuhRHsTPYsSK59hr3v8jRArK+2UEoju4Jumn1CmpgoBLGI6ReHLJ/czYltNUW3g=="], "@vscode/vsce-sign": ["@vscode/vsce-sign@2.0.9", "", { "optionalDependencies": { "@vscode/vsce-sign-alpine-arm64": "2.0.6", "@vscode/vsce-sign-alpine-x64": "2.0.6", "@vscode/vsce-sign-darwin-arm64": "2.0.6", "@vscode/vsce-sign-darwin-x64": "2.0.6", "@vscode/vsce-sign-linux-arm": "2.0.6", "@vscode/vsce-sign-linux-arm64": "2.0.6", "@vscode/vsce-sign-linux-x64": "2.0.6", "@vscode/vsce-sign-win32-arm64": "2.0.6", "@vscode/vsce-sign-win32-x64": "2.0.6" } }, "sha512-8IvaRvtFyzUnGGl3f5+1Cnor3LqaUWvhaUjAYO8Y39OUYlOf3cRd+dowuQYLpZcP3uwSG+mURwjEBOSq4SOJ0g=="], @@ -1205,8 +1359,12 @@ "@vscode/vsce-sign-win32-x64": ["@vscode/vsce-sign-win32-x64@2.0.6", "", { "os": "win32", "cpu": "x64" }, "sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ=="], + "@xmldom/xmldom": ["@xmldom/xmldom@0.8.13", "", {}, "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw=="], + "@yarnpkg/lockfile": ["@yarnpkg/lockfile@1.1.0", "", {}, "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ=="], + "abbrev": ["abbrev@1.1.1", "", {}, "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="], + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], @@ -1221,8 +1379,12 @@ "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], + "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="], + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + "ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], + "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -1231,6 +1393,10 @@ "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + "app-builder-bin": ["app-builder-bin@5.0.0-alpha.12", "", {}, "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w=="], + + "app-builder-lib": ["app-builder-lib@26.8.1", "", { "dependencies": { "@develar/schema-utils": "~2.6.5", "@electron/asar": "3.4.1", "@electron/fuses": "^1.8.0", "@electron/get": "^3.0.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.3", "@electron/rebuild": "^4.0.3", "@electron/universal": "2.0.3", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", "async-exit-hook": "^2.0.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chromium-pickle-js": "^0.2.0", "ci-info": "4.3.1", "debug": "^4.3.4", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", "electron-publish": "26.8.1", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "isbinaryfile": "^5.0.0", "jiti": "^2.4.2", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lazy-val": "^1.0.5", "minimatch": "^10.0.3", "plist": "3.1.0", "proper-lockfile": "^4.1.2", "resedit": "^1.7.0", "semver": "~7.7.3", "tar": "^7.5.7", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0", "which": "^5.0.0" }, "peerDependencies": { "dmg-builder": "26.8.1", "electron-builder-squirrel-windows": "26.8.1" } }, "sha512-p0Im/Dx5C4tmz8QEE1Yn4MkuPC8PrnlRneMhWJj7BBXQfNTJUshM/bp3lusdEsDbvvfJZpXWnYesgSLvwtM2Zw=="], + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], @@ -1239,12 +1405,22 @@ "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], + "asn1.js": ["asn1.js@5.4.1", "", { "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", "minimalistic-assert": "^1.0.0", "safer-buffer": "^2.1.0" } }, "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA=="], + "asn1js": ["asn1js@3.0.7", "", { "dependencies": { "pvtsutils": "^1.3.6", "pvutils": "^1.1.3", "tslib": "^2.8.1" } }, "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ=="], + + "assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + "async-exit-hook": ["async-exit-hook@2.0.1", "", {}, "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw=="], + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], @@ -1277,10 +1453,14 @@ "before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], + "better-sqlite3": ["better-sqlite3@11.10.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ=="], + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], "binaryextensions": ["binaryextensions@6.11.0", "", { "dependencies": { "editions": "^6.21.0" } }, "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw=="], + "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], "bn.js": ["bn.js@4.12.3", "", {}, "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g=="], @@ -1289,6 +1469,8 @@ "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], + "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], + "boundary": ["boundary@2.0.0", "", {}, "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA=="], "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], @@ -1305,12 +1487,22 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "builder-util": ["builder-util@26.8.1", "", { "dependencies": { "7zip-bin": "~5.2.0", "@types/debug": "^4.1.6", "app-builder-bin": "5.0.0-alpha.12", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "cross-spawn": "^7.0.6", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "js-yaml": "^4.1.0", "sanitize-filename": "^1.6.3", "source-map-support": "^0.5.19", "stat-mode": "^1.0.0", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0" } }, "sha512-pm1lTYbGyc90DHgCDO7eo8Rl4EqKLciayNbZqGziqnH9jrlKe8ZANGdityLZU+pJh16dfzjAx2xQq9McuIPEtw=="], + + "builder-util-runtime": ["builder-util-runtime@9.5.1", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ=="], + "bun-pty": ["bun-pty@0.4.8", "", {}, "sha512-rO70Mrbr13+jxHHHu2YBkk2pNqrJE5cJn29WE++PUr+GFA0hq/VgtQPZANJ8dJo6d7XImvBk37Innt8GM7O28w=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + "cacache": ["cacache@16.1.3", "", { "dependencies": { "@npmcli/fs": "^2.1.0", "@npmcli/move-file": "^2.0.0", "chownr": "^2.0.0", "fs-minipass": "^2.1.0", "glob": "^8.0.1", "infer-owner": "^1.0.4", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "mkdirp": "^1.0.4", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^9.0.0", "tar": "^6.1.11", "unique-filename": "^2.0.0" } }, "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ=="], + + "cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="], + + "cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="], + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -1325,6 +1517,8 @@ "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="], + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "character-entities": ["character-entities@1.2.4", "", {}, "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw=="], @@ -1341,14 +1535,28 @@ "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], - "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + "chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], + + "chromium-pickle-js": ["chromium-pickle-js@0.2.0", "", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="], "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + "clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="], + + "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], + + "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + + "cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="], + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], + + "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], @@ -1371,10 +1579,18 @@ "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], - "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + "commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], "common-tags": ["common-tags@1.8.2", "", {}, "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA=="], + "compare-version": ["compare-version@0.1.2", "", {}, "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A=="], + + "component-emitter": ["component-emitter@1.3.1", "", {}, "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ=="], + + "compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="], + + "compression": ["compression@1.8.1", "", { "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" } }, "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w=="], + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="], @@ -1389,12 +1605,22 @@ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + "cookiejar": ["cookiejar@2.1.4", "", {}, "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="], + "core-js-compat": ["core-js-compat@3.48.0", "", { "dependencies": { "browserslist": "^4.28.1" } }, "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q=="], + "core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + "crc": ["crc@3.8.0", "", { "dependencies": { "buffer": "^5.1.0" } }, "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ=="], + "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], + "cron-parser": ["cron-parser@4.9.0", "", { "dependencies": { "luxon": "^3.2.1" } }, "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q=="], + + "cross-dirname": ["cross-dirname@0.1.0", "", {}, "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q=="], + "cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -1413,6 +1639,8 @@ "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + "date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], @@ -1433,6 +1661,10 @@ "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], + "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], + + "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="], + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], @@ -1447,24 +1679,42 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="], + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "dezalgo": ["dezalgo@1.0.4", "", { "dependencies": { "asap": "^2.0.0", "wrappy": "1" } }, "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig=="], + "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], + "dir-compare": ["dir-compare@4.2.0", "", { "dependencies": { "minimatch": "^3.0.5", "p-limit": "^3.1.0 " } }, "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ=="], + + "dmg-builder": ["dmg-builder@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" }, "optionalDependencies": { "dmg-license": "^1.0.11" } }, "sha512-glMJgnTreo8CFINujtAhCgN96QAqApDMZ8Vl1r8f0QT8QprvC1UCltV4CcWj20YoIyLZx6IUskaJZ0NV8fokcg=="], + + "dmg-license": ["dmg-license@1.0.11", "", { "dependencies": { "@types/plist": "^3.0.1", "@types/verror": "^1.10.3", "ajv": "^6.10.0", "crc": "^3.8.0", "iconv-corefoundation": "^1.1.7", "plist": "^3.0.4", "smart-buffer": "^4.0.2", "verror": "^1.10.0" }, "os": "darwin", "bin": { "dmg-license": "bin/dmg-license.js" } }, "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q=="], + "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], + "dompurify": ["dompurify@3.3.3", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA=="], + "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + + "dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], "editions": ["editions@6.22.0", "", { "dependencies": { "version-range": "^4.15.0" } }, "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ=="], @@ -1473,14 +1723,36 @@ "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], + "electron": ["electron@41.2.1", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^24.9.0", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-teeRThiYGTPKf/2yOW7zZA1bhb91KEQ4yLBPOg7GxpmnkLFLugKgQaAKOrCgdzwsXh/5mFIfmkm+4+wACJKwaA=="], + + "electron-builder": ["electron-builder@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "ci-info": "^4.2.0", "dmg-builder": "26.8.1", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "cli.js", "install-app-deps": "install-app-deps.js" } }, "sha512-uWhx1r74NGpCagG0ULs/P9Nqv2nsoo+7eo4fLUOB8L8MdWltq9odW/uuLXMFCDGnPafknYLZgjNX0ZIFRzOQAw=="], + + "electron-builder-squirrel-windows": ["electron-builder-squirrel-windows@26.8.1", "", { "dependencies": { "app-builder-lib": "26.8.1", "builder-util": "26.8.1", "electron-winstaller": "5.4.0" } }, "sha512-o288fIdgPLHA76eDrFADHPoo7VyGkDCYbLV1GzndaMSAVBoZrGvM9m2IehdcVMzdAZJ2eV9bgyissQXHv5tGzA=="], + + "electron-context-menu": ["electron-context-menu@4.1.2", "", { "dependencies": { "cli-truncate": "^4.0.0", "electron-dl": "^4.0.0", "electron-is-dev": "^3.0.1" } }, "sha512-9xYTUV0oRqKL50N9W71IrXNdVRB0LuBp3R1zkUdUc2wfIa2/QZwYYj5RLuO7Tn7ZSLVIaO3X6u+EIBK+cBvzrQ=="], + + "electron-dl": ["electron-dl@4.0.0", "", { "dependencies": { "ext-name": "^5.0.0", "pupa": "^3.1.0", "unused-filename": "^4.0.1" } }, "sha512-USiB9816d2JzKv0LiSbreRfTg5lDk3lWh0vlx/gugCO92ZIJkHVH0UM18EHvKeadErP6Xn4yiTphWzYfbA2Ong=="], + + "electron-is-dev": ["electron-is-dev@3.0.1", "", {}, "sha512-8TjjAh8Ec51hUi3o4TaU0mD3GMTOESi866oRNavj9A3IQJ7pmv+MJVmdZBFGw4GFT36X7bkqnuDNYvkQgvyI8Q=="], + + "electron-log": ["electron-log@5.4.3", "", {}, "sha512-sOUsM3LjZdugatazSQ/XTyNcw8dfvH1SYhXWiJyfYodAAKOZdHs0txPiLDXFzOZbhXgAgshQkshH2ccq0feyLQ=="], + + "electron-publish": ["electron-publish@26.8.1", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "builder-util": "26.8.1", "builder-util-runtime": "9.5.1", "chalk": "^4.1.2", "form-data": "^4.0.5", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-q+jrSTIh/Cv4eGZa7oVR+grEJo/FoLMYBAnSL5GCtqwUpr1T+VgKB/dn1pnzxIxqD8S/jP1yilT9VrwCqINR4w=="], + "electron-to-chromium": ["electron-to-chromium@1.5.302", "", {}, "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg=="], + "electron-updater": ["electron-updater@6.8.3", "", { "dependencies": { "builder-util-runtime": "9.5.1", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "~7.7.3", "tiny-typed-emitter": "^2.1.0" } }, "sha512-Z6sgw3jgbikWKXei1ENdqFOxBP0WlXg3TtKfz0rgw2vIZFJUyI4pD7ZN7jrkm7EoMK+tcm/qTnPUdqfZukBlBQ=="], + + "electron-winstaller": ["electron-winstaller@5.4.0", "", { "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", "fs-extra": "^7.0.1", "lodash": "^4.17.21", "temp": "^0.9.0" }, "optionalDependencies": { "@electron/windows-sign": "^1.1.2" } }, "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg=="], + "elkjs": ["elkjs@0.11.0", "", {}, "sha512-u4J8h9mwEDaYMqo0RYJpqNMFDoMK7f+pu4GjcV+N8jIC7TRdORgzkfSjTJemhqONFfH6fBI3wpysgWbhgVWIXw=="], "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + "encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="], + "encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="], "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], @@ -1489,24 +1761,34 @@ "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="], + "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + "es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="], + "esbuild": ["esbuild@0.24.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.24.2", "@esbuild/android-arm": "0.24.2", "@esbuild/android-arm64": "0.24.2", "@esbuild/android-x64": "0.24.2", "@esbuild/darwin-arm64": "0.24.2", "@esbuild/darwin-x64": "0.24.2", "@esbuild/freebsd-arm64": "0.24.2", "@esbuild/freebsd-x64": "0.24.2", "@esbuild/linux-arm": "0.24.2", "@esbuild/linux-arm64": "0.24.2", "@esbuild/linux-ia32": "0.24.2", "@esbuild/linux-loong64": "0.24.2", "@esbuild/linux-mips64el": "0.24.2", "@esbuild/linux-ppc64": "0.24.2", "@esbuild/linux-riscv64": "0.24.2", "@esbuild/linux-s390x": "0.24.2", "@esbuild/linux-x64": "0.24.2", "@esbuild/netbsd-arm64": "0.24.2", "@esbuild/netbsd-x64": "0.24.2", "@esbuild/openbsd-arm64": "0.24.2", "@esbuild/openbsd-x64": "0.24.2", "@esbuild/sunos-x64": "0.24.2", "@esbuild/win32-arm64": "0.24.2", "@esbuild/win32-ia32": "0.24.2", "@esbuild/win32-x64": "0.24.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "escape-goat": ["escape-goat@4.0.0", "", {}, "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], @@ -1531,7 +1813,7 @@ "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], - "estree-walker": ["estree-walker@1.0.1", "", {}, "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], @@ -1543,10 +1825,22 @@ "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + + "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + "ext-list": ["ext-list@2.2.2", "", { "dependencies": { "mime-db": "^1.28.0" } }, "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA=="], + + "ext-name": ["ext-name@5.0.0", "", { "dependencies": { "ext-list": "^2.0.0", "sort-keys-length": "^1.0.0" } }, "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], + + "extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="], + "fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -1557,6 +1851,8 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], @@ -1569,6 +1865,8 @@ "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], + "filelist": ["filelist@1.0.6", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], @@ -1597,6 +1895,8 @@ "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], + "formidable": ["formidable@3.5.4", "", { "dependencies": { "@paralleldrive/cuid2": "^2.2.2", "dezalgo": "^1.0.4", "once": "^1.4.0" } }, "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], @@ -1609,6 +1909,10 @@ "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + "fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -1625,6 +1929,8 @@ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], @@ -1633,6 +1939,8 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], + "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], @@ -1645,6 +1953,8 @@ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + "global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="], + "globals": ["globals@16.5.0", "", {}, "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ=="], "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], @@ -1653,6 +1963,8 @@ "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + "got": ["got@11.8.6", "", { "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", "cacheable-request": "^7.0.2", "decompress-response": "^6.0.0", "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", "p-cancelable": "^2.0.0", "responselike": "^2.0.0" } }, "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g=="], + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], @@ -1669,19 +1981,23 @@ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "hast-util-from-dom": ["hast-util-from-dom@5.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="], + + "hast-util-from-html": ["hast-util-from-html@2.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.1.0", "hast-util-from-parse5": "^8.0.0", "parse5": "^7.0.0", "vfile": "^6.0.0", "vfile-message": "^4.0.0" } }, "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw=="], + + "hast-util-from-html-isomorphic": ["hast-util-from-html-isomorphic@2.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-from-dom": "^5.0.0", "hast-util-from-html": "^2.0.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw=="], + "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], + "hast-util-is-element": ["hast-util-is-element@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g=="], + "hast-util-parse-selector": ["hast-util-parse-selector@2.2.5", "", {}, "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ=="], - "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], - - "hast-util-sanitize": ["hast-util-sanitize@5.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "unist-util-position": "^5.0.0" } }, "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg=="], - "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="], - "hast-util-to-parse5": ["hast-util-to-parse5@8.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA=="], + "hast-util-to-text": ["hast-util-to-text@4.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "hast-util-is-element": "^3.0.0", "unist-util-find-after": "^5.0.0" } }, "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A=="], "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], @@ -1703,6 +2019,8 @@ "htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="], + "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], "http-proxy": ["http-proxy@1.18.1", "", { "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" } }, "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ=="], @@ -1711,12 +2029,16 @@ "http-proxy-middleware": ["http-proxy-middleware@3.0.5", "", { "dependencies": { "@types/http-proxy": "^1.17.15", "debug": "^4.3.6", "http-proxy": "^1.18.1", "is-glob": "^4.0.3", "is-plain-object": "^5.0.0", "micromatch": "^4.0.8" } }, "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg=="], + "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="], + "http_ece": ["http_ece@1.2.0", "", {}, "sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA=="], "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], + "iconv-corefoundation": ["iconv-corefoundation@1.1.7", "", { "dependencies": { "cli-truncate": "^2.1.0", "node-addon-api": "^1.6.3" }, "os": "darwin" }, "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ=="], + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], "idb": ["idb@7.1.1", "", {}, "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ=="], @@ -1731,8 +2053,14 @@ "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + "index-to-position": ["index-to-position@1.2.0", "", {}, "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw=="], + "infer-owner": ["infer-owner@1.0.4", "", {}, "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A=="], + + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], @@ -1743,6 +2071,8 @@ "intl-messageformat": ["intl-messageformat@10.7.18", "", { "dependencies": { "@formatjs/ecma402-abstract": "2.3.6", "@formatjs/fast-memoize": "2.2.7", "@formatjs/icu-messageformat-parser": "2.11.4", "tslib": "^2.8.0" } }, "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g=="], + "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "is-alphabetical": ["is-alphabetical@1.0.4", "", {}, "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg=="], @@ -1787,6 +2117,10 @@ "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], + "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], + + "is-lambda": ["is-lambda@1.0.1", "", {}, "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ=="], + "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], "is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="], @@ -1821,6 +2155,8 @@ "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], + "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], @@ -1831,6 +2167,8 @@ "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + "isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], "istextorbinary": ["istextorbinary@9.5.0", "", { "dependencies": { "binaryextensions": "^6.11.0", "editions": "^6.21.0", "textextensions": "^6.11.0" } }, "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw=="], @@ -1859,6 +2197,8 @@ "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], + "json-with-bigint": ["json-with-bigint@3.5.7", "", {}, "sha512-7ei3MdAI5+fJPVnKlW77TKNKwQ5ppSzWvhPuSuINT/GYW9ZOC1eRKOuhV9yHG5aEsUPj9BBx5JIekkmoLHxZOw=="], "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], @@ -1877,12 +2217,16 @@ "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], + "katex": ["katex@0.16.45", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA=="], + "keytar": ["keytar@7.9.0", "", { "dependencies": { "node-addon-api": "^4.3.0", "prebuild-install": "^7.0.1" } }, "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ=="], "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], "klaw-sync": ["klaw-sync@6.0.0", "", { "dependencies": { "graceful-fs": "^4.1.11" } }, "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ=="], + "lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="], + "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], @@ -1921,10 +2265,14 @@ "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="], + "lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="], + "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], + "lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="], + "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="], @@ -1941,22 +2289,32 @@ "lodash.truncate": ["lodash.truncate@4.4.2", "", {}, "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw=="], + "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], + "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], + "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], + "lowlight": ["lowlight@1.20.0", "", { "dependencies": { "fault": "^1.0.0", "highlight.js": "~10.7.0" } }, "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw=="], "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], "lru_map": ["lru_map@0.4.1", "", {}, "sha512-I+lBvqMMFfqaV8CJCISjI3wbjmwVu/VyOoU7+qtu9d7ioW5klMgsTTiUOUp+DJvfTTzKXoPbyC6YfgkNcyPSOg=="], + "luxon": ["luxon@3.7.2", "", {}, "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew=="], + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + "make-fetch-happen": ["make-fetch-happen@10.2.1", "", { "dependencies": { "agentkeepalive": "^4.2.1", "cacache": "^16.1.0", "http-cache-semantics": "^4.1.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-fetch": "^2.0.3", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.3", "promise-retry": "^2.0.1", "socks-proxy-agent": "^7.0.0", "ssri": "^9.0.0" } }, "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w=="], + "markdown-it": ["markdown-it@14.1.1", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="], "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], "marked": ["marked@17.0.3", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-jt1v2ObpyOKR8p4XaUJVk3YWRJ5n+i4+rjQopxvV32rSndTJXvIzuUdWWIy/1pFQMkQmvTXawzDNqOH/CUmx6A=="], + "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], @@ -1975,6 +2333,8 @@ "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], + "mdast-util-math": ["mdast-util-math@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "longest-streak": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.1.0", "unist-util-remove-position": "^5.0.0" } }, "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w=="], + "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="], "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="], @@ -1997,6 +2357,8 @@ "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], @@ -2015,6 +2377,8 @@ "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], + "micromark-extension-math": ["micromark-extension-math@3.1.0", "", { "dependencies": { "@types/katex": "^0.16.0", "devlop": "^1.0.0", "katex": "^0.16.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg=="], + "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], @@ -2061,6 +2425,8 @@ "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], "minimalistic-assert": ["minimalistic-assert@1.0.1", "", {}, "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="], @@ -2069,10 +2435,26 @@ "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + "minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + + "minipass-collect": ["minipass-collect@1.0.2", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA=="], + + "minipass-fetch": ["minipass-fetch@2.1.2", "", { "dependencies": { "minipass": "^3.1.6", "minipass-sized": "^1.0.3", "minizlib": "^2.1.2" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA=="], + + "minipass-flush": ["minipass-flush@1.0.7", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA=="], + + "minipass-pipeline": ["minipass-pipeline@1.2.4", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A=="], + + "minipass-sized": ["minipass-sized@1.0.3", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g=="], + + "minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], + + "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + "morphdom": ["morphdom@2.7.8", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="], + "motion": ["motion@12.34.3", "", { "dependencies": { "framer-motion": "^12.34.3", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-xZIkBGO7v/Uvm+EyaqYd+9IpXu0sZqLywVlGdCFrrMiaO9JI4Kx51mO9KlHSWwll+gZUVY5OJsWgYI5FywJ/tw=="], "motion-dom": ["motion-dom@12.34.3", "", { "dependencies": { "motion-utils": "^12.29.2" } }, "sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ=="], @@ -2089,7 +2471,7 @@ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], @@ -2097,10 +2479,14 @@ "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + "node-api-version": ["node-api-version@0.2.1", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q=="], + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + "node-gyp": ["node-gyp@11.5.0", "", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "graceful-fs": "^4.2.6", "make-fetch-happen": "^14.0.3", "nopt": "^8.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5", "tar": "^7.4.3", "tinyglobby": "^0.2.12", "which": "^5.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ=="], + "node-pty": ["node-pty@1.2.0-beta.12", "", { "dependencies": { "node-addon-api": "^7.1.0" } }, "sha512-uExTCG/4VmSJa4+TjxFwPXv8BfacmfFEBL6JpxCMDghcwqzvD0yTcGmZ1fKOK6HY33tp0CelLblqTECJizc+Yw=="], "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], @@ -2109,10 +2495,14 @@ "nodemon": ["nodemon@3.1.14", "", { "dependencies": { "chokidar": "^3.5.2", "debug": "^4", "ignore-by-default": "^1.0.1", "minimatch": "^10.2.1", "pstree.remy": "^1.1.8", "semver": "^7.5.3", "simple-update-notifier": "^2.0.0", "supports-color": "^5.5.0", "touch": "^3.1.0", "undefsafe": "^2.0.5" }, "bin": { "nodemon": "bin/nodemon.js" } }, "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw=="], + "nopt": ["nopt@6.0.0", "", { "dependencies": { "abbrev": "^1.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g=="], + "normalize-package-data": ["normalize-package-data@6.0.2", "", { "dependencies": { "hosted-git-info": "^7.0.0", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" } }, "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + "normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="], + "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -2123,10 +2513,16 @@ "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + "on-headers": ["on-headers@1.1.0", "", {}, "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A=="], + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], + "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], @@ -2139,8 +2535,12 @@ "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], + "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="], + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], @@ -2171,6 +2571,8 @@ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], @@ -2181,12 +2583,18 @@ "path-type": ["path-type@6.0.0", "", {}, "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ=="], + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pe-library": ["pe-library@0.4.1", "", {}, "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw=="], + "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + "plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="], + "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="], "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], @@ -2197,6 +2605,8 @@ "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + "postject": ["postject@1.0.0-alpha.6", "", { "dependencies": { "commander": "^9.4.0" }, "bin": { "postject": "dist/cli.js" } }, "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A=="], + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], @@ -2205,6 +2615,16 @@ "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], + "proc-log": ["proc-log@2.0.1", "", {}, "sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw=="], + + "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], + + "promise-inflight": ["promise-inflight@1.0.1", "", {}, "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="], + + "promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="], + + "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], + "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], @@ -2217,6 +2637,12 @@ "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], + "pupa": ["pupa@3.3.0", "", { "dependencies": { "escape-goat": "^4.0.0" } }, "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA=="], + + "pvtsutils": ["pvtsutils@1.3.6", "", { "dependencies": { "tslib": "^2.8.1" } }, "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg=="], + + "pvutils": ["pvutils@1.1.5", "", {}, "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA=="], + "qrcode": ["qrcode@1.5.4", "", { "dependencies": { "dijkstrajs": "^1.0.1", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, "bin": { "qrcode": "bin/qrcode" } }, "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg=="], "qrcode-terminal": ["qrcode-terminal@0.12.0", "", { "bin": { "qrcode-terminal": "./bin/qrcode-terminal.js" } }, "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ=="], @@ -2225,6 +2651,8 @@ "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], + "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], @@ -2253,12 +2681,16 @@ "read": ["read@1.0.7", "", { "dependencies": { "mute-stream": "~0.0.4" } }, "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ=="], + "read-binary-file-arch": ["read-binary-file-arch@1.0.6", "", { "dependencies": { "debug": "^4.3.4" }, "bin": { "read-binary-file-arch": "cli.js" } }, "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg=="], + "read-pkg": ["read-pkg@9.0.1", "", { "dependencies": { "@types/normalize-package-data": "^2.4.3", "normalize-package-data": "^6.0.0", "parse-json": "^8.0.0", "type-fest": "^4.6.0", "unicorn-magic": "^0.1.0" } }, "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA=="], "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + "reflect-metadata": ["reflect-metadata@0.2.2", "", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="], + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], "refractor": ["refractor@3.6.0", "", { "dependencies": { "hastscript": "^6.0.0", "parse-entities": "^2.0.0", "prismjs": "~1.27.0" } }, "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA=="], @@ -2281,14 +2713,12 @@ "regjsparser": ["regjsparser@0.13.0", "", { "dependencies": { "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q=="], - "rehype-harden": ["rehype-harden@1.1.8", "", { "dependencies": { "unist-util-visit": "^5.0.0" } }, "sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw=="], - - "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], - - "rehype-sanitize": ["rehype-sanitize@6.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-sanitize": "^5.0.0" } }, "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg=="], + "rehype-katex": ["rehype-katex@7.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/katex": "^0.16.0", "hast-util-from-html-isomorphic": "^2.0.0", "hast-util-to-text": "^4.0.0", "katex": "^0.16.0", "unist-util-visit-parents": "^6.0.0", "vfile": "^6.0.0" } }, "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA=="], "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], + "remark-math": ["remark-math@6.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-math": "^3.0.0", "micromark-extension-math": "^3.0.0", "unified": "^11.0.0" } }, "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA=="], + "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="], @@ -2305,14 +2735,30 @@ "requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="], + "resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="], + + "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="], + + "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], + + "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + + "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], + "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], @@ -2333,6 +2779,8 @@ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "sanitize-filename": ["sanitize-filename@1.6.4", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg=="], + "sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], @@ -2341,8 +2789,12 @@ "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], + "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], @@ -2373,7 +2825,9 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], @@ -2389,12 +2843,22 @@ "slash": ["slash@2.0.0", "", {}, "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A=="], - "slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="], + "slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], + + "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], "smob": ["smob@1.6.1", "", {}, "sha512-KAkBqZl3c2GvNgNhcoyJae1aKldDW0LO279wF9bk1PnluRTETKBq0WyzRXxEhoQLk56yHaOY4JCBEKDuJIET5g=="], + "socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="], + + "socks-proxy-agent": ["socks-proxy-agent@7.0.0", "", { "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", "socks": "^2.6.2" } }, "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww=="], + "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="], + "sort-keys": ["sort-keys@1.1.2", "", { "dependencies": { "is-plain-obj": "^1.0.0" } }, "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg=="], + + "sort-keys-length": ["sort-keys-length@1.0.1", "", { "dependencies": { "sort-keys": "^1.0.0" } }, "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw=="], + "source-map": ["source-map@0.8.0-beta.0", "", { "dependencies": { "whatwg-url": "^7.0.0" } }, "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA=="], "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], @@ -2413,14 +2877,24 @@ "spdx-license-ids": ["spdx-license-ids@3.0.23", "", {}, "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw=="], + "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], + + "ssri": ["ssri@9.0.1", "", { "dependencies": { "minipass": "^3.1.1" } }, "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "std-env": ["std-env@4.1.0", "", {}, "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ=="], + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], - "streamdown": ["streamdown@2.3.0", "", { "dependencies": { "clsx": "^2.1.1", "hast-util-to-jsx-runtime": "^2.3.6", "html-url-attributes": "^3.0.1", "marked": "^17.0.1", "rehype-harden": "^1.1.8", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remend": "1.2.1", "tailwind-merge": "^3.4.0", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "unist-util-visit-parents": "^6.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-OqS3by/lt91lSicE8RQP2nTsYI6Q/dQgGP2vcyn9YesCmRHhNjswAuBAZA1z0F4+oBU3II/eV51LqjCqwTb1lw=="], - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA=="], "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], @@ -2437,6 +2911,8 @@ "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-comments": ["strip-comments@2.0.1", "", {}, "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw=="], "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], @@ -2449,6 +2925,12 @@ "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], + "sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="], + + "superagent": ["superagent@10.3.0", "", { "dependencies": { "component-emitter": "^1.3.1", "cookiejar": "^2.1.4", "debug": "^4.3.7", "fast-safe-stringify": "^2.1.1", "form-data": "^4.0.5", "formidable": "^3.5.4", "methods": "^1.1.2", "mime": "2.6.0", "qs": "^6.14.1" } }, "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ=="], + + "supertest": ["supertest@7.2.2", "", { "dependencies": { "cookie-signature": "^1.2.2", "methods": "^1.1.2", "superagent": "^10.3.0" } }, "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA=="], + "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "supports-hyperlinks": ["supports-hyperlinks@3.2.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig=="], @@ -2465,12 +2947,18 @@ "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], + "tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], + "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "temp": ["temp@0.9.4", "", { "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" } }, "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA=="], + "temp-dir": ["temp-dir@2.0.0", "", {}, "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg=="], + "temp-file": ["temp-file@3.4.0", "", { "dependencies": { "async-exit-hook": "^2.0.1", "fs-extra": "^10.0.0" } }, "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg=="], + "tempy": ["tempy@0.6.0", "", { "dependencies": { "is-stream": "^2.0.0", "temp-dir": "^2.0.0", "type-fest": "^0.16.0", "unique-string": "^2.0.0" } }, "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw=="], "terminal-link": ["terminal-link@4.0.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "supports-hyperlinks": "^3.2.0" } }, "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA=="], @@ -2481,10 +2969,22 @@ "textextensions": ["textextensions@6.11.0", "", { "dependencies": { "editions": "^6.21.0" } }, "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ=="], + "tiny-async-pool": ["tiny-async-pool@1.3.0", "", { "dependencies": { "semver": "^5.5.0" } }, "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA=="], + + "tiny-typed-emitter": ["tiny-typed-emitter@2.1.0", "", {}, "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="], + "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="], + "tmp-promise": ["tmp-promise@3.0.3", "", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], @@ -2499,12 +2999,16 @@ "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + "truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="], + "ts-api-utils": ["ts-api-utils@2.4.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], + "tsyringe": ["tsyringe@4.10.0", "", { "dependencies": { "tslib": "^1.9.3" } }, "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw=="], + "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], @@ -2555,12 +3059,20 @@ "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + "unique-filename": ["unique-filename@2.0.1", "", { "dependencies": { "unique-slug": "^3.0.0" } }, "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A=="], + + "unique-slug": ["unique-slug@3.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w=="], + "unique-string": ["unique-string@2.0.0", "", { "dependencies": { "crypto-random-string": "^2.0.0" } }, "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg=="], + "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], + "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + "unist-util-remove-position": ["unist-util-remove-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q=="], + "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="], @@ -2573,6 +3085,8 @@ "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + "unused-filename": ["unused-filename@4.0.1", "", { "dependencies": { "escape-string-regexp": "^5.0.0", "path-exists": "^5.0.0" } }, "sha512-ZX6U1J04K1FoSUeoX1OicAhw4d0aro2qo+L8RhJkiGTNtBNkd/Fi1Wxoc9HzcVu6HfOzm0si/N15JjxFmD1z6A=="], + "upath": ["upath@1.2.0", "", {}, "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg=="], "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], @@ -2585,6 +3099,10 @@ "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], + "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], + + "utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="], + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], @@ -2593,6 +3111,8 @@ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="], + "version-range": ["version-range@4.15.0", "", {}, "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg=="], "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], @@ -2605,8 +3125,12 @@ "vite-plugin-pwa": ["vite-plugin-pwa@1.2.0", "", { "dependencies": { "debug": "^4.3.6", "pretty-bytes": "^6.1.1", "tinyglobby": "^0.2.10", "workbox-build": "^7.4.0", "workbox-window": "^7.4.0" }, "peerDependencies": { "@vite-pwa/assets-generator": "^1.0.0", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["@vite-pwa/assets-generator"] }, "sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw=="], + "vitest": ["vitest@4.1.5", "", { "dependencies": { "@vitest/expect": "4.1.5", "@vitest/mocker": "4.1.5", "@vitest/pretty-format": "4.1.5", "@vitest/runner": "4.1.5", "@vitest/snapshot": "4.1.5", "@vitest/spy": "4.1.5", "@vitest/utils": "4.1.5", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.5", "@vitest/browser-preview": "4.1.5", "@vitest/browser-webdriverio": "4.1.5", "@vitest/coverage-istanbul": "4.1.5", "@vitest/coverage-v8": "4.1.5", "@vitest/ui": "4.1.5", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg=="], + "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], + "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], + "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], "web-push": ["web-push@3.6.7", "", { "dependencies": { "asn1.js": "^5.3.0", "http_ece": "1.2.0", "https-proxy-agent": "^7.0.0", "jws": "^4.0.0", "minimist": "^1.2.5" }, "bin": { "web-push": "src/cli.js" } }, "sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A=="], @@ -2633,6 +3157,8 @@ "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], "workbox-background-sync": ["workbox-background-sync@7.4.0", "", { "dependencies": { "idb": "^7.0.1", "workbox-core": "7.4.0" } }, "sha512-8CB9OxKAgKZKyNMwfGZ1XESx89GryWTfI+V5yEj8sHjFH8MFelUwYXEyldEK6M6oKMmn807GoJFUEA1sC4XS9w=="], @@ -2669,6 +3195,8 @@ "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], @@ -2719,6 +3247,28 @@ "@babel/preset-env/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], + + "@electron/asar/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "@electron/fuses/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + + "@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@electron/node-gyp/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], + + "@electron/notarize/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + + "@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="], + + "@electron/universal/fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], + + "@electron/universal/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "@electron/windows-sign/fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], @@ -2727,6 +3277,14 @@ "@heroui/theme/tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="], + "@isaacs/fs-minipass/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + + "@npmcli/agent/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "@npmcli/agent/socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], + "@openchamber/ui/ghostty-web": ["ghostty-web@0.4.0", "", {}, "sha512-0puDBik2qapbD/QQBW9o5ZHfXnZBqZWx/ctBiVtKZ6ZLds4NYb+wZuw1cRLXZk9zYovIQ908z3rvFhexAvc5Hg=="], "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], @@ -2753,6 +3311,8 @@ "@rollup/pluginutils/@types/estree": ["@types/estree@0.0.39", "", {}, "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw=="], + "@rollup/pluginutils/estree-walker": ["estree-walker@1.0.1", "", {}, "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg=="], + "@rollup/pluginutils/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "@rollup/pluginutils/rollup": ["rollup@2.80.0", "", { "optionalDependencies": { "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ=="], @@ -2779,40 +3339,90 @@ "@textlint/linter-formatter/pluralize": ["pluralize@2.0.0", "", {}, "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw=="], + "@types/plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + "@vscode/vsce/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + + "accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "app-builder-lib/@electron/get": ["@electron/get@3.1.0", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ=="], + + "app-builder-lib/@electron/rebuild": ["@electron/rebuild@4.0.3", "", { "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", "got": "^11.7.0", "graceful-fs": "^4.2.11", "node-abi": "^4.2.0", "node-api-version": "^0.2.1", "node-gyp": "^11.2.0", "ora": "^5.1.0", "read-binary-file-arch": "^1.0.6", "semver": "^7.3.5", "tar": "^7.5.6", "yargs": "^17.0.1" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA=="], + + "app-builder-lib/ci-info": ["ci-info@4.3.1", "", {}, "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA=="], + + "app-builder-lib/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + + "app-builder-lib/tar": ["tar@7.5.13", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng=="], + + "app-builder-lib/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], + "babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "cacache/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], + + "cacache/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "cacache/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "cacache/p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="], + "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + "cli-truncate/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], + "cmdk/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], + "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "decode-named-character-reference/character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + "dmg-builder/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "electron-builder/ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], + + "electron-publish/mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], + + "electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + + "encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "encoding-sniffer/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "filelist/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], + "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + "glob/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + "globby/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "globby/slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="], + "hast-util-from-dom/hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], + "hast-util-from-parse5/hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], "hastscript/@types/hast": ["@types/hast@2.3.10", "", { "dependencies": { "@types/unist": "^2" } }, "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw=="], @@ -2823,10 +3433,22 @@ "hastscript/space-separated-tokens": ["space-separated-tokens@1.1.5", "", {}, "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA=="], + "iconv-corefoundation/cli-truncate": ["cli-truncate@2.1.0", "", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="], + + "iconv-corefoundation/node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="], + "is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], "keytar/node-addon-api": ["node-addon-api@4.3.0", "", {}, "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ=="], + "make-fetch-happen/http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="], + + "make-fetch-happen/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + + "make-fetch-happen/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "make-fetch-happen/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "markdown-it/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], @@ -2835,6 +3457,28 @@ "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "minipass-collect/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-fetch/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "node-gyp/make-fetch-happen": ["make-fetch-happen@14.0.3", "", { "dependencies": { "@npmcli/agent": "^3.0.0", "cacache": "^19.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^4.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^5.0.0", "promise-retry": "^2.0.1", "ssri": "^12.0.0" } }, "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ=="], + + "node-gyp/nopt": ["nopt@8.1.0", "", { "dependencies": { "abbrev": "^3.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A=="], + + "node-gyp/proc-log": ["proc-log@5.0.0", "", {}, "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ=="], + + "node-gyp/tar": ["tar@7.5.13", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng=="], + + "node-gyp/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], + "node-sarif-builder/fs-extra": ["fs-extra@11.3.3", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg=="], "nodemon/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], @@ -2853,10 +3497,18 @@ "path-scurry/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], + "path-scurry/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "plist/xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], + + "postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], + "qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + "react-syntax-highlighter/@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + "read-pkg/type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], "read-pkg/unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="], @@ -2865,24 +3517,58 @@ "refractor/prismjs": ["prismjs@1.27.0", "", {}, "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA=="], + "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "serialize-error/type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], + + "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], + + "socks-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + + "sort-keys/is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="], + "source-map/whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="], "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "ssri/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "stringify-entities/character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + "superagent/mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], + "supports-hyperlinks/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "table/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + "table/slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="], + + "tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + + "temp/mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + + "temp/rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="], + "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + "tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + "tsx/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + "tsyringe/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + + "unused-filename/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], + + "unused-filename/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="], + "vite/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], "whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "workbox-build/@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + "workbox-build/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], "workbox-build/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], @@ -2897,6 +3583,14 @@ "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "@electron/node-gyp/glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], + + "@electron/universal/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "@rollup/plugin-node-resolve/@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@secretlint/config-loader/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], @@ -2905,16 +3599,54 @@ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + "app-builder-lib/@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + + "app-builder-lib/@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "app-builder-lib/@electron/rebuild/node-abi": ["node-abi@4.28.0", "", { "dependencies": { "semver": "^7.6.3" } }, "sha512-Qfp5XZL1cJDOabOT8H5gnqMTmM4NjvYzHp4I/Kt/Sl76OVkOBBHRFlPspGV0hYvMoqQsypFjT/Yp7Km0beXW9g=="], + + "app-builder-lib/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + + "app-builder-lib/tar/chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "app-builder-lib/tar/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "app-builder-lib/tar/minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + + "app-builder-lib/tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "app-builder-lib/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], + + "cacache/glob/minimatch": ["minimatch@5.1.9", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw=="], + + "cli-truncate/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "cli-truncate/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "electron-winstaller/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "electron-winstaller/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + "filelist/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "glob/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + "hast-util-from-dom/hastscript/hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], + "hast-util-from-parse5/hastscript/hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], "hastscript/@types/hast/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + "iconv-corefoundation/cli-truncate/slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="], + + "make-fetch-happen/http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + + "make-fetch-happen/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "mdast-util-mdx-jsx/parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], "mdast-util-mdx-jsx/parse-entities/character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], @@ -2927,6 +3659,28 @@ "mdast-util-mdx-jsx/parse-entities/is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + "node-gyp/make-fetch-happen/cacache": ["cacache@19.0.1", "", { "dependencies": { "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", "glob": "^10.2.2", "lru-cache": "^10.0.1", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^12.0.0", "tar": "^7.4.3", "unique-filename": "^4.0.0" } }, "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ=="], + + "node-gyp/make-fetch-happen/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "node-gyp/make-fetch-happen/minipass-fetch": ["minipass-fetch@4.0.1", "", { "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", "minizlib": "^3.0.1" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ=="], + + "node-gyp/make-fetch-happen/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "node-gyp/make-fetch-happen/ssri": ["ssri@12.0.0", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ=="], + + "node-gyp/nopt/abbrev": ["abbrev@3.0.1", "", {}, "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg=="], + + "node-gyp/tar/chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "node-gyp/tar/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "node-gyp/tar/minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + + "node-gyp/tar/yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "node-gyp/which/isexe": ["isexe@3.1.5", "", {}, "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w=="], + "nodemon/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], "nodemon/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], @@ -2949,6 +3703,8 @@ "table/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "temp/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "tsx/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], "tsx/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], @@ -3051,20 +3807,70 @@ "workbox-build/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "@electron/node-gyp/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "app-builder-lib/@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], + + "app-builder-lib/@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + + "app-builder-lib/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "mdast-util-mdx-jsx/parse-entities/is-alphanumerical/is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], + "node-gyp/make-fetch-happen/cacache/@npmcli/fs": ["@npmcli/fs@4.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q=="], + + "node-gyp/make-fetch-happen/cacache/fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="], + + "node-gyp/make-fetch-happen/cacache/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + + "node-gyp/make-fetch-happen/cacache/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "node-gyp/make-fetch-happen/cacache/minipass-collect": ["minipass-collect@2.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw=="], + + "node-gyp/make-fetch-happen/cacache/unique-filename": ["unique-filename@4.0.0", "", { "dependencies": { "unique-slug": "^5.0.0" } }, "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ=="], + + "node-gyp/make-fetch-happen/minipass-fetch/minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + "nodemon/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "qrcode/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], "qrcode/yargs/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], + "node-gyp/make-fetch-happen/cacache/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "node-gyp/make-fetch-happen/cacache/glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "node-gyp/make-fetch-happen/cacache/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "node-gyp/make-fetch-happen/cacache/unique-filename/unique-slug": ["unique-slug@5.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg=="], + "qrcode/yargs/find-up/locate-path/p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], + "node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "node-gyp/make-fetch-happen/cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "qrcode/yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "node-gyp/make-fetch-happen/cacache/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], } } diff --git a/src/docs/REVERSE_PROXY.md b/src/docs/REVERSE_PROXY.md new file mode 100644 index 0000000..b0c368d --- /dev/null +++ b/src/docs/REVERSE_PROXY.md @@ -0,0 +1,337 @@ +# Reverse Proxy Setup + +Use this guide when running OpenChamber behind Nginx, Nginx Proxy Manager, Caddy, Cloudflare, or another reverse proxy. + +## Before you proxy it + +1. Confirm OpenChamber works directly first. +2. Open `http://:3000` or your custom port from the same network. +3. Only add the reverse proxy after the direct connection works. + +## What the proxy must support + +- WebSockets for live message transport: + - `/api/event/ws` + - `/api/global/event/ws` + - `/api/terminal/ws` +- SSE without buffering: + - `/api/event` + - `/api/global/event` + - `/api/notifications/stream` + - `/api/openchamber/events` + - `/api/terminal/:sessionId/stream` +- Large request bodies for attachments and file operations +- Long-lived read timeouts for live streams and terminal sessions + +## Rules that matter + +- Enable WebSocket proxying. +- Disable buffering on SSE routes. +- Disable gzip on the proxy if OpenChamber is already compressing responses. +- Keep compression enabled in only one layer. +- Forward normal proxy headers such as `Host`, `X-Forwarded-For`, and `X-Forwarded-Proto`. +- Increase body size limits if users upload files. + +## Quick checklist + +- OpenChamber reachable directly on LAN +- WebSockets enabled in the proxy +- SSE routes have buffering off +- `gzip off` on the proxy host, or proxy compression disabled another way +- `client_max_body_size` large enough for attachments +- `proxy_read_timeout` long enough for streams + +## Example: Nginx + +
+Show example config + +```nginx +client_max_body_size 50M; +client_body_buffer_size 50M; +proxy_request_buffering off; + +proxy_http_version 1.1; +proxy_set_header Connection ""; +proxy_set_header Host $host; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; +proxy_set_header X-Forwarded-Host $host; + +gzip off; + +location = /api/terminal/ws { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} + +location = /api/global/event/ws { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} + +location = /api/event/ws { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} + +location ~ ^/api/(event|global/event|notifications/stream|openchamber/events)$ { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} + +location ~ ^/api/terminal/.+/stream$ { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} + +location /api { + proxy_pass http://127.0.0.1:3000; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} + +location / { + proxy_pass http://127.0.0.1:3000; +} +``` + +
+ +## Example: Nginx Proxy Manager + +
+Show Advanced tab example + +```nginx +client_max_body_size 50M; +client_body_buffer_size 50M; +proxy_request_buffering off; + +proxy_http_version 1.1; +proxy_set_header Connection ""; +proxy_set_header Host $host; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; +proxy_set_header X-Forwarded-Host $host; + +gzip off; + +location = /api/terminal/ws { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location = /api/global/event/ws { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location = /api/event/ws { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location = /api/event { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location = /api/global/event { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location = /api/notifications/stream { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location = /api/openchamber/events { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location ~ ^/api/terminal/.+/stream$ { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location /api { + proxy_pass http://127.0.0.1:3000; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location / { + proxy_pass http://127.0.0.1:3000; +} +``` + +
+ +Also enable `Websockets Support` in Nginx Proxy Manager for this host. + +## Common failure signs + +### Page loads, but sending messages fails + +- WebSockets are not enabled in the proxy +- `/api/event/ws` or `/api/global/event/ws` is not passing through correctly + +### Notifications or live status do not update + +- one of the SSE routes is buffered or cached +- `X-Accel-Buffering "no"` is missing + +### File uploads fail + +- `client_max_body_size` is too small + +### Everything works locally, but breaks only behind the proxy + +- the proxy is compressing and buffering live traffic +- the proxy is missing WebSocket support + +## Example: Caddy + +
+Show example config + +```caddy +reverse_proxy 127.0.0.1:3000 { + # WebSocket support is automatic in Caddy + + # Flush SSE responses immediately + flush_interval -1 + + # Pass through Host and proxy headers + header_up Host {host} + header_up X-Real-IP {remote_host} + header_up X-Forwarded-For {remote_host} + header_up X-Forwarded-Proto {scheme} + + # Increase timeouts for long-lived streams + transport http { + read_timeout 3600s + write_timeout 3600s + } +} +``` + +
+ +Caddy handles WebSocket upgrades automatically — no extra configuration needed. The `flush_interval -1` directive ensures SSE chunks are forwarded immediately without buffering. + +## CDN and double-compression warning + +If you place a CDN (such as Cloudflare) in front of your reverse proxy, be aware of double compression: + +- OpenChamber compresses HTTP responses with gzip (threshold 1 KB). +- Cloudflare and other CDNs also compress responses by default. +- This can cause double-compressed responses or incorrect `Content-Encoding` headers. + +To avoid this, disable compression at **one** layer: + +- **Cloudflare:** Rules → Compression → disable (or use "Passthrough" mode). +- **Nginx:** `gzip off` (already shown in the examples above). +- **Caddy:** Caddy does not re-compress by default if the upstream already sends compressed content. + +SSE streaming routes are excluded from compression by OpenChamber, but the CDN may still buffer them. Check your CDN documentation for how to disable buffering on SSE paths. diff --git a/src/docs/TAURI_TO_ELECTRON_CUTOVER.md b/src/docs/TAURI_TO_ELECTRON_CUTOVER.md new file mode 100644 index 0000000..28a21b5 --- /dev/null +++ b/src/docs/TAURI_TO_ELECTRON_CUTOVER.md @@ -0,0 +1,349 @@ +# Tauri → Electron auto-update cutover + +> Self-contained playbook. The branch and conversation where this plan was +> designed will not be around when the cutover happens — read this file top to +> bottom and execute; do not assume prior context. + +## What this is + +OpenChamber historically shipped as a Tauri app. A parallel Electron shell was +added on branch `electron-app` (merged to `main` as part of a larger migration). +Since then, both desktop shells have been released in the same GitHub release +and each has its own auto-update channel: + +| Shell | Manifest | Update format | Secret used to sign | +|----------|-------------------|---------------------|---------------------| +| Tauri | `latest.json` | `.tar.gz` + `.sig` | `TAURI_SIGNING_PRIVATE_KEY` (minisign) | +| Electron | `latest-mac.yml` | `.zip` + `blockmap` | Developer ID codesign (APPLE_* secrets) | + +Existing Tauri installs keep their own auto-update path (`latest.json`). +Electron installs auto-update through `latest-mac.yml`. They coexist without +conflict because filenames and manifests differ. + +At some point the user wants to **stop maintaining the Tauri build** and make +the Tauri installs migrate themselves into Electron via auto-update. This +document describes how to do that in a single "transition release". + +## The core trick + +Tauri's updater downloads whatever `.tar.gz` the `latest.json` points at, +verifies the minisign signature, unpacks the contents **over** the existing +`.app` directory, and restarts. It does **not** introspect the payload — it +just replaces files. + +So: produce a `.tar.gz` of the Electron `.app`, sign it with the existing +Tauri minisign key, point `latest.json` at it. Tauri users receive the update, +their `OpenChamber.app` becomes the Electron bundle in-place, and next launch +starts Electron. Subsequent updates go through `latest-mac.yml` +(electron-updater). One-way migration, one-shot workflow change. + +## Prerequisites before running the cutover + +Check all of these before making any release: + +1. **Electron has shipped stable through its own `latest-mac.yml` path for at + least 2 releases.** Verify: + ``` + gh release list --repo btriapitsyn/openchamber + gh release view vX.Y.Z --repo btriapitsyn/openchamber \ + | grep -E 'OpenChamber-.*\.zip|latest-mac\.yml' + ``` + A user on Electron should have successfully auto-updated at least once. + If not, pause and stabilise that path first — don't stack risk. + +2. **`~/.config/openchamber/settings.json` is still the shared state path.** + Tauri `src-tauri/src/main.rs:settings_file_path` and Electron + `packages/electron/main.mjs:settingsFilePath` must both resolve to + `$HOME/.config/openchamber/settings.json`. If either has moved, data parity + breaks and this migration loses user data. Audit both paths, update the + non-migrated shell to match before proceeding. + +3. **Electron `appId` is `dev.openchamber.desktop`** (check + `packages/electron/package.json` `build.appId`). Tauri identifier is + `ai.opencode.openchamber`. These differ intentionally — it means macOS + LaunchServices will re-register after the in-place replace. That's fine but + see "Risks" below. + +4. **All GitHub secrets still valid:** `APPLE_CERTIFICATE`, + `APPLE_CERTIFICATE_PASSWORD`, `APPLE_ID`, `APPLE_PASSWORD`, `APPLE_TEAM_ID`, + `TAURI_SIGNING_PRIVATE_KEY`, `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`. A + workflow_dispatch dry-run should succeed before the real tag. + +5. **`minisign` CLI is available on the macOS runner** (or installable via + brew). Used to sign the Electron tarball with the Tauri key. + +## Release workflow changes + +The file to edit: `.github/workflows/release.yml`. + +Today it has these jobs (simplified): + +``` +create-release +├── build-desktop-macos (Tauri .dmg/.tar.gz/.tar.gz.sig) +├── build-desktop-electron-macos (Electron .dmg/.zip/blockmap/latest-mac.yml) +├── publish-npm +├── combine-manifests (merges Tauri per-arch JSONs → latest.json) +├── combine-electron-manifests (merges Electron per-arch YMLs → latest-mac.yml) +└── finalize-release +``` + +### Step 1 — Remove the Tauri build + +Delete these jobs entirely: +- `build-desktop-macos` +- `combine-manifests` + +They are replaced by the repackage job (below). `finalize-release` `needs:` +list must be updated to drop both. + +### Step 2 — Add a repackage job + +Insert after `build-desktop-electron-macos`: + +```yaml +repackage-electron-as-tauri-update: + needs: [create-release, build-desktop-electron-macos] + runs-on: macos-26 + strategy: + fail-fast: false + matrix: + include: + - arch: arm64 + platform: darwin-aarch64 + - arch: x64 + platform: darwin-x86_64 + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + - uses: actions/setup-node@v4 + with: + node-version: '20' + + # Pull the signed+notarized Electron .app that build-desktop-electron-macos + # already produced. Either re-download the dmg and mount+copy the .app, or + # (cleaner) modify build-desktop-electron-macos to upload the .app itself + # as an artifact so this job can download it. Prefer the latter — adds one + # `actions/upload-artifact@v4` step uploading `packages/electron/dist/mac-/OpenChamber.app`. + + - name: Download signed Electron .app + uses: actions/download-artifact@v4 + with: + name: electron-app-${{ matrix.arch }} + path: staged + + - name: Install minisign + run: brew install minisign + + - name: Tar and sign Electron .app as Tauri update payload + env: + TAURI_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + VERSION: ${{ needs.create-release.outputs.version }} + run: | + set -euo pipefail + cd staged + # The tarball name convention Tauri's updater expects. Must end in + # `.app.tar.gz`. Name stays stable — Tauri updater does not care about + # the inner .app name. + TARBALL="OpenChamber.app.tar.gz" + tar -czf "$TARBALL" OpenChamber.app + + # minisign needs the private key written to a file and a non-interactive + # password via -W (or env). The key in the secret is a minisign secret + # key block (base64-ish multi-line blob). Write to a file verbatim. + echo "$TAURI_KEY" > ../tauri-signing.key + echo "$TAURI_KEY_PASSWORD" | minisign -S -s ../tauri-signing.key \ + -m "$TARBALL" -W + + # Rename per platform so the release has distinct names for arm64/x64. + mv "$TARBALL" "OpenChamber-${VERSION}-${{ matrix.platform }}.app.tar.gz" + mv "${TARBALL}.minisig" "OpenChamber-${VERSION}-${{ matrix.platform }}.app.tar.gz.sig" + + - name: Generate Tauri latest-.json + env: + VERSION: ${{ needs.create-release.outputs.version }} + REPO: ${{ github.repository }} + run: | + SIG=$(cat staged/OpenChamber-${VERSION}-${{ matrix.platform }}.app.tar.gz.sig) + TAR=OpenChamber-${VERSION}-${{ matrix.platform }}.app.tar.gz + cat > staged/latest-${{ matrix.platform }}.json < @@ -114,7 +101,7 @@ - + diff --git a/src/packages/desktop/package.json b/src/packages/desktop/package.json index fa8e39a..8c9cce3 100644 --- a/src/packages/desktop/package.json +++ b/src/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@openchamber/desktop", - "version": "1.9.1", + "version": "1.9.9", "private": true, "type": "module", "desktopPrerequisites": [ diff --git a/src/packages/desktop/src-tauri/Cargo.lock b/src/packages/desktop/src-tauri/Cargo.lock index 97ff092..e6eeed6 100644 --- a/src/packages/desktop/src-tauri/Cargo.lock +++ b/src/packages/desktop/src-tauri/Cargo.lock @@ -14,7 +14,7 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "once_cell", "version_check", ] @@ -71,9 +71,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.100" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arbitrary" @@ -92,10 +92,12 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "ashpd" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df" +checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39" dependencies = [ + "async-fs", + "async-net", "enumflags2", "futures-channel", "futures-util", @@ -103,7 +105,6 @@ dependencies = [ "raw-window-handle", "serde", "serde_repr", - "tokio", "url", "wayland-backend", "wayland-client", @@ -137,9 +138,9 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.3" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" dependencies = [ "async-task", "concurrent-queue", @@ -149,6 +150,17 @@ dependencies = [ "slab", ] +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + [[package]] name = "async-io" version = "2.6.0" @@ -169,15 +181,26 @@ dependencies = [ [[package]] name = "async-lock" -version = "3.4.1" +version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ "event-listener", "event-listener-strategy", "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + [[package]] name = "async-process" version = "2.5.0" @@ -204,7 +227,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -239,7 +262,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -289,6 +312,21 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitflags" version = "1.3.2" @@ -297,9 +335,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" dependencies = [ "serde_core", ] @@ -325,22 +363,13 @@ 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", + "objc2", ] [[package]] @@ -358,25 +387,26 @@ dependencies = [ [[package]] name = "borsh" -version = "1.5.7" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" dependencies = [ "borsh-derive", + "bytes", "cfg_aliases", ] [[package]] name = "borsh-derive" -version = "1.5.7" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" dependencies = [ "once_cell", - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -402,17 +432,18 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "byte-unit" -version = "5.1.6" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cd29c3c585209b0cbc7309bfe3ed7efd8c84c21b7af29c8bfae908f8777174" +checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d" dependencies = [ "rust_decimal", + "schemars 1.2.1", "serde", "utf8-width", ] @@ -441,9 +472,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.24.0" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "byteorder" @@ -453,9 +484,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] @@ -466,7 +497,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cairo-sys-rs", "glib", "libc", @@ -487,9 +518,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" dependencies = [ "serde_core", ] @@ -514,7 +545,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -524,14 +555,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" dependencies = [ "serde", - "toml 0.9.8", + "toml 0.9.12+spec-1.1.0", ] [[package]] name = "cc" -version = "1.2.46" +version = "1.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" dependencies = [ "find-msvc-tools", "shlex", @@ -578,9 +609,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "num-traits", @@ -641,11 +672,11 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-graphics" -version = "0.24.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation", "core-graphics-types", "foreign-types", @@ -658,7 +689,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation", "libc", ] @@ -723,6 +754,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + [[package]] name = "cssparser-macros" version = "0.6.1" @@ -730,7 +774,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -740,14 +784,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "darling" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ "darling_core", "darling_macro", @@ -755,34 +799,33 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "darling_macro" -version = "0.21.3" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", "serde_core", @@ -796,7 +839,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -809,7 +852,28 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.110", + "syn 2.0.117", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", ] [[package]] @@ -843,22 +907,16 @@ dependencies = [ "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" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.10.0", - "block2 0.6.2", + "bitflags 2.11.0", + "block2", "libc", - "objc2 0.6.3", + "objc2", ] [[package]] @@ -869,23 +927,23 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "dlib" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" dependencies = [ - "libloading 0.8.9", + "libloading", ] [[package]] name = "dlopen2" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" dependencies = [ "dlopen2_derive", "libc", @@ -895,13 +953,28 @@ dependencies = [ [[package]] name = "dlopen2_derive" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser 0.36.0", + "foldhash 0.2.0", + "html5ever 0.38.0", + "precomputed-hash", + "selectors 0.36.1", + "tendril 0.5.0", ] [[package]] @@ -921,9 +994,9 @@ dependencies = [ [[package]] name = "dtoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" [[package]] name = "dtoa-short" @@ -948,14 +1021,14 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "embed-resource" -version = "3.0.6" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" +checksum = "63a1d0de4f2249aa0ff5884d7080814f446bb241a559af6c170a41e878ed2d45" dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.9.8", + "toml 0.9.12+spec-1.1.0", "vswhom", "winreg", ] @@ -977,9 +1050,9 @@ dependencies = [ [[package]] name = "endi" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" [[package]] name = "enumflags2" @@ -999,7 +1072,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -1020,9 +1093,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" dependencies = [ "serde", "serde_core", @@ -1096,27 +1169,26 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" dependencies = [ "cfg-if", "libc", "libredox", - "windows-sys 0.60.2", ] [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.5" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -1128,6 +1200,18 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.5.0" @@ -1146,7 +1230,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -1182,25 +1266,24 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", - "futures-sink", ] [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1209,9 +1292,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -1228,32 +1311,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-io", @@ -1262,7 +1345,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1397,9 +1479,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "js-sys", @@ -1417,11 +1499,24 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + [[package]] name = "gio" version = "0.18.4" @@ -1460,7 +1555,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "futures-channel", "futures-core", "futures-executor", @@ -1488,7 +1583,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -1567,7 +1662,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -1581,9 +1676,18 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -1617,18 +1721,27 @@ checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" dependencies = [ "log", "mac", - "markup5ever", + "markup5ever 0.14.1", "match_token", ] [[package]] -name = "http" -version = "1.3.1" +name = "html5ever" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever 0.38.0", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -1701,14 +1814,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.18" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -1725,9 +1837,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1749,9 +1861,9 @@ dependencies = [ [[package]] name = "ico" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" dependencies = [ "byteorder", "png", @@ -1805,9 +1917,9 @@ checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ "icu_collections", "icu_locale_core", @@ -1819,9 +1931,9 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" @@ -1838,6 +1950,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -1878,12 +1996,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "serde", "serde_core", ] @@ -1899,15 +2017,15 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.9" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" dependencies = [ "memchr", "serde", @@ -1934,9 +2052,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "javascriptcore-rs" @@ -1970,7 +2088,7 @@ dependencies = [ "cesu8", "cfg-if", "combine", - "jni-sys", + "jni-sys 0.3.1", "log", "thiserror 1.0.69", "walkdir", @@ -1979,16 +2097,40 @@ dependencies = [ [[package]] name = "jni-sys" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -2021,7 +2163,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "serde", "unicode-segmentation", ] @@ -2032,17 +2174,17 @@ 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", + "cssparser 0.29.6", + "html5ever 0.29.1", + "indexmap 2.13.0", + "selectors 0.24.0", ] [[package]] -name = "lazy_static" -version = "1.5.0" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libappindicator" @@ -2064,15 +2206,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" dependencies = [ "gtk-sys", - "libloading 0.7.4", + "libloading", "once_cell", ] [[package]] name = "libc" -version = "0.2.177" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libloading" @@ -2084,32 +2226,23 @@ dependencies = [ "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" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", - "redox_syscall", + "plain", + "redox_syscall 0.7.3", ] [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -2128,9 +2261,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" dependencies = [ "value-bag", ] @@ -2149,13 +2282,13 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" [[package]] name = "mac-notification-sys" -version = "0.6.9" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fd3f75411f4725061682ed91f131946e912859d0044d39c4ec0aac818d7621" +checksum = "29a16783dd1a47849b8c8133c9cd3eb2112cfbc6901670af3dba47c8bbfb07d3" dependencies = [ "cc", - "objc2 0.6.3", - "objc2-foundation 0.3.2", + "objc2", + "objc2-foundation", "time", ] @@ -2168,9 +2301,20 @@ dependencies = [ "log", "phf 0.11.3", "phf_codegen 0.11.3", - "string_cache", - "string_cache_codegen", - "tendril", + "string_cache 0.8.9", + "string_cache_codegen 0.5.4", + "tendril 0.4.3", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril 0.5.0", + "web_atoms", ] [[package]] @@ -2181,7 +2325,7 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -2192,9 +2336,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memoffset" @@ -2213,9 +2357,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minisign-verify" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e856fdd13623a2f5f2f54676a4ee49502a96a80ef4a62bcedd23d52427c44d43" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" [[package]] name = "miniz_oxide" @@ -2229,9 +2373,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", @@ -2248,14 +2392,14 @@ dependencies = [ "dpi", "gtk", "keyboard-types", - "objc2 0.6.3", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", "once_cell", "png", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", "windows-sys 0.60.2", ] @@ -2265,8 +2409,8 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.10.0", - "jni-sys", + "bitflags 2.11.0", + "jni-sys 0.3.1", "log", "ndk-sys", "num_enum", @@ -2286,7 +2430,7 @@ version = "0.6.0+11769913" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" dependencies = [ - "jni-sys", + "jni-sys 0.3.1", ] [[package]] @@ -2295,19 +2439,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[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", - "libc", - "memoffset", -] - [[package]] name = "nodrop" version = "0.1.14" @@ -2316,9 +2447,9 @@ checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" [[package]] name = "notify-rust" -version = "4.11.7" +version = "4.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6442248665a5aa2514e794af3b39661a8e73033b1cc5e59899e1276117ee4400" +checksum = "21af20a1b50be5ac5861f74af1a863da53a11c38684d9818d82f1c42f7fdc6c2" dependencies = [ "futures-lite", "log", @@ -2330,9 +2461,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-traits" @@ -2345,9 +2476,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", "rustversion", @@ -2355,14 +2486,14 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -2374,27 +2505,11 @@ dependencies = [ "libc", ] -[[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" +version = "0.6.4" 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" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", "objc2-exception-helper", @@ -2406,41 +2521,11 @@ 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", + "bitflags 2.11.0", + "block2", + "objc2", "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", + "objc2-foundation", ] [[package]] @@ -2449,9 +2534,9 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "dispatch2", - "objc2 0.6.3", + "objc2", ] [[package]] @@ -2460,48 +2545,13 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "dispatch2", - "objc2 0.6.3", + "objc2", "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" @@ -2517,28 +2567,16 @@ 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", + "bitflags 2.11.0", + "block2", "libc", - "objc2 0.6.3", + "objc2", "objc2-core-foundation", ] @@ -2548,8 +2586,8 @@ 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", + "bitflags 2.11.0", + "objc2", "objc2-core-foundation", ] @@ -2559,45 +2597,20 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a1e6550c4caed348956ce3370c9ffeca70bb1dbed4fa96112e7c6170e074586" dependencies = [ - "objc2 0.6.3", + "objc2", "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", + "bitflags 2.11.0", + "objc2", "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", + "objc2-foundation", ] [[package]] @@ -2606,9 +2619,10 @@ 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", + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", ] [[package]] @@ -2617,8 +2631,8 @@ 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", + "bitflags 2.11.0", + "objc2", "objc2-core-foundation", ] @@ -2628,10 +2642,10 @@ 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", + "bitflags 2.11.0", + "objc2", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", ] [[package]] @@ -2640,21 +2654,21 @@ 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", + "bitflags 2.11.0", + "block2", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", "objc2-javascript-core", "objc2-security", ] [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "open" @@ -2670,12 +2684,15 @@ dependencies = [ [[package]] name = "openchamber-desktop" -version = "1.9.1" +version = "1.9.9" dependencies = [ "anyhow", "base64 0.22.1", "log", - "reqwest", + "objc2", + "objc2-web-kit", + "reqwest 0.12.28", + "rfd 0.15.4", "serde", "serde_json", "tauri", @@ -2687,9 +2704,14 @@ dependencies = [ "tauri-plugin-updater", "tokio", "url", - "window-vibrancy 0.7.1", ] +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "option-ext" version = "0.2.0" @@ -2722,12 +2744,12 @@ 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", + "objc2-foundation", "objc2-osa-kit", "serde", "serde_json", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -2779,7 +2801,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", "windows-link 0.2.1", ] @@ -2826,6 +2848,17 @@ dependencies = [ "phf_shared 0.11.3", ] +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", +] + [[package]] name = "phf_codegen" version = "0.8.0" @@ -2846,6 +2879,16 @@ dependencies = [ "phf_shared 0.11.3", ] +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + [[package]] name = "phf_generator" version = "0.8.0" @@ -2876,6 +2919,16 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + [[package]] name = "phf_macros" version = "0.10.0" @@ -2900,7 +2953,20 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -2927,14 +2993,23 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher 1.0.1", + "siphasher 1.0.2", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher 1.0.2", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -2944,9 +3019,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ "atomic-waker", "fastrand", @@ -2959,6 +3034,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "plist" version = "1.8.0" @@ -2966,7 +3047,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", - "indexmap 2.12.0", + "indexmap 2.13.0", "quick-xml 0.38.4", "serde", "time", @@ -2999,6 +3080,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + [[package]] name = "potential_utf" version = "0.1.4" @@ -3029,6 +3116,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -3051,11 +3148,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.23.7", + "toml_edit 0.25.8+spec-1.1.0", ] [[package]] @@ -3090,9 +3187,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -3135,6 +3232,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +dependencies = [ + "memchr", +] + [[package]] name = "quinn" version = "0.11.9" @@ -3149,7 +3255,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -3157,9 +3263,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.13" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", "getrandom 0.3.4", @@ -3170,7 +3276,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.17", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -3192,9 +3298,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.42" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -3205,6 +3311,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "radium" version = "0.7.0" @@ -3243,7 +3355,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha 0.9.0", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -3273,7 +3385,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core 0.9.3", + "rand_core 0.9.5", ] [[package]] @@ -3291,14 +3403,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] name = "rand_core" -version = "0.9.3" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ "getrandom 0.3.4", ] @@ -3333,7 +3445,16 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags 2.11.0", ] [[package]] @@ -3342,9 +3463,9 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", - "thiserror 2.0.17", + "thiserror 2.0.18", ] [[package]] @@ -3364,14 +3485,14 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "regex" -version = "1.12.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -3381,9 +3502,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -3392,9 +3513,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "rend" @@ -3407,15 +3528,13 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.24" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", - "futures-channel", "futures-core", - "futures-util", "http", "http-body", "http-body-util", @@ -3435,6 +3554,44 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "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", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -3444,7 +3601,6 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots", ] [[package]] @@ -3454,22 +3610,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" dependencies = [ "ashpd", - "block2 0.6.2", + "block2", + "dispatch2", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "pollster", + "raw-window-handle", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", "dispatch2", "glib-sys", "gobject-sys", "gtk-sys", "js-sys", "log", - "objc2 0.6.3", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", "raw-window-handle", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3480,7 +3659,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.16", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -3488,9 +3667,9 @@ dependencies = [ [[package]] name = "rkyv" -version = "0.7.45" +version = "0.7.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" dependencies = [ "bitvec", "bytecheck", @@ -3506,9 +3685,9 @@ dependencies = [ [[package]] name = "rkyv_derive" -version = "0.7.45" +version = "0.7.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" dependencies = [ "proc-macro2", "quote", @@ -3517,9 +3696,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.39.0" +version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" +checksum = "2ce901f9a19d251159075a4c37af514c3b8ef99c22e02dd8c19161cf397ee94a" dependencies = [ "arrayvec", "borsh", @@ -3529,13 +3708,14 @@ dependencies = [ "rkyv", "serde", "serde_json", + "wasm-bindgen", ] [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -3548,11 +3728,11 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -3561,9 +3741,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.35" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "once_cell", "ring", @@ -3574,20 +3754,59 @@ dependencies = [ ] [[package]] -name = "rustls-pki-types" -version = "1.13.0" +name = "rustls-native-certs" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "web-time", "zeroize", ] [[package]] -name = "rustls-webpki" -version = "0.103.8" +name = "rustls-platform-verifier" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "ring", "rustls-pki-types", @@ -3602,9 +3821,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -3615,6 +3834,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "schemars" version = "0.8.22" @@ -3644,9 +3872,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", @@ -3663,7 +3891,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -3684,6 +3912,29 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.24.0" @@ -3691,14 +3942,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" dependencies = [ "bitflags 1.3.2", - "cssparser", - "derive_more", + "cssparser 0.29.6", + "derive_more 0.99.20", "fxhash", "log", "phf 0.8.0", "phf_codegen 0.8.0", "precomputed-hash", - "servo_arc", + "servo_arc 0.2.0", + "smallvec", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.11.0", + "cssparser 0.36.0", + "derive_more 2.1.1", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen 0.13.1", + "precomputed-hash", + "rustc-hash", + "servo_arc 0.4.3", "smallvec", ] @@ -3751,7 +4021,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -3762,20 +4032,20 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -3786,7 +4056,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -3800,9 +4070,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392" +checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" dependencies = [ "serde_core", ] @@ -3821,17 +4091,17 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.12.0", + "indexmap 2.13.0", "schemars 0.9.0", - "schemars 1.1.0", + "schemars 1.2.1", "serde_core", "serde_json", "serde_with_macros", @@ -3840,14 +4110,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.0" +version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -3869,7 +4139,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -3882,6 +4152,15 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "sha2" version = "0.10.9" @@ -3933,18 +4212,19 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "simdutf8" @@ -3960,15 +4240,15 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -3978,34 +4258,34 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "softbuffer" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" dependencies = [ "bytemuck", - "cfg_aliases", - "core-graphics", - "foreign-types", "js-sys", - "log", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-quartz-core 0.2.2", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", "raw-window-handle", - "redox_syscall", + "redox_syscall 0.5.18", + "tracing", "wasm-bindgen", "web-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4040,12 +4320,6 @@ 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" @@ -4059,6 +4333,18 @@ dependencies = [ "serde", ] +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", +] + [[package]] name = "string_cache_codegen" version = "0.5.4" @@ -4071,6 +4357,18 @@ dependencies = [ "quote", ] +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.11.1" @@ -4107,9 +4405,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.110" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -4133,7 +4431,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -4151,35 +4449,33 @@ dependencies = [ [[package]] name = "tao" -version = "0.34.5" +version = "0.34.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7" +checksum = "9103edf55f2da3c82aea4c7fab7c4241032bfeea0e71fa557d98e00e7ce7cc20" dependencies = [ - "bitflags 2.10.0", - "block2 0.6.2", + "bitflags 2.11.0", + "block2", "core-foundation", "core-graphics", "crossbeam-channel", - "dispatch", + "dispatch2", "dlopen2", "dpi", "gdkwayland-sys", "gdkx11-sys", "gtk", "jni", - "lazy_static", "libc", "log", "ndk", "ndk-context", "ndk-sys", - "objc2 0.6.3", + "objc2", "objc2-app-kit", - "objc2-foundation 0.3.2", + "objc2-foundation", "once_cell", "parking_lot", "raw-window-handle", - "scopeguard", "tao-macros", "unicode-segmentation", "url", @@ -4197,7 +4493,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -4208,9 +4504,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" dependencies = [ "filetime", "libc", @@ -4225,9 +4521,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.9.4" +version = "2.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15524fc7959bfcaa051ba6d0b3fb1ef18e978de2176c7c6acb977f7fd14d35c7" +checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d" dependencies = [ "anyhow", "bytes", @@ -4245,15 +4541,15 @@ dependencies = [ "log", "mime", "muda", - "objc2 0.6.3", + "objc2", "objc2-app-kit", - "objc2-foundation 0.3.2", + "objc2-foundation", "objc2-ui-kit", "objc2-web-kit", "percent-encoding", "plist", "raw-window-handle", - "reqwest", + "reqwest 0.13.2", "serde", "serde_json", "serde_repr", @@ -4264,21 +4560,21 @@ dependencies = [ "tauri-runtime", "tauri-runtime-wry", "tauri-utils", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", "tray-icon", "url", "webkit2gtk", "webview2-com", - "window-vibrancy 0.6.0", + "window-vibrancy", "windows", ] [[package]] name = "tauri-build" -version = "2.5.3" +version = "2.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17fcb8819fd16463512a12f531d44826ce566f486d7ccd211c9c8cebdaec4e08" +checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d" dependencies = [ "anyhow", "cargo_toml", @@ -4292,15 +4588,15 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml 0.9.8", + "toml 0.9.12+spec-1.1.0", "walkdir", ] [[package]] name = "tauri-codegen" -version = "2.5.2" +version = "2.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa9844cefcf99554a16e0a278156ae73b0d8680bbc0e2ad1e4287aadd8489cf" +checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29" dependencies = [ "base64 0.22.1", "brotli", @@ -4314,9 +4610,9 @@ dependencies = [ "serde", "serde_json", "sha2", - "syn 2.0.110", + "syn 2.0.117", "tauri-utils", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "url", "uuid", @@ -4325,23 +4621,23 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.5.2" +version = "2.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3764a12f886d8245e66b7ee9b43ccc47883399be2019a61d80cf0f4117446fde" +checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", "tauri-codegen", "tauri-utils", ] [[package]] name = "tauri-plugin" -version = "2.5.1" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076c78a474a7247c90cad0b6e87e593c4c620ed4efdb79cbe0214f0021f6c39d" +checksum = "ddde7d51c907b940fb573006cdda9a642d6a7c8153657e88f8a5c3c9290cd4aa" dependencies = [ "anyhow", "glob", @@ -4350,33 +4646,33 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml 0.9.8", + "toml 0.9.12+spec-1.1.0", "walkdir", ] [[package]] name = "tauri-plugin-dialog" -version = "2.4.2" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "313f8138692ddc4a2127c4c9607d616a46f5c042e77b3722450866da0aad2f19" +checksum = "9204b425d9be8d12aa60c2a83a289cf7d1caae40f57f336ed1155b3a5c0e359b" dependencies = [ "log", "raw-window-handle", - "rfd", + "rfd 0.16.0", "serde", "serde_json", "tauri", "tauri-plugin", "tauri-plugin-fs", - "thiserror 2.0.17", + "thiserror 2.0.18", "url", ] [[package]] name = "tauri-plugin-fs" -version = "2.4.4" +version = "2.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47df422695255ecbe7bac7012440eddaeefd026656171eac9559f5243d3230d9" +checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804" dependencies = [ "anyhow", "dunce", @@ -4389,30 +4685,30 @@ dependencies = [ "tauri", "tauri-plugin", "tauri-utils", - "thiserror 2.0.17", - "toml 0.9.8", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", "url", ] [[package]] name = "tauri-plugin-log" -version = "2.7.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5709c792b8630290b5d9811a1f8fe983dd925fc87c7fc7f4923616458cd00b6" +checksum = "7545bd67f070a4500432c826e2e0682146a1d6712aee22a2786490156b574d93" dependencies = [ "android_logger", "byte-unit", "fern", "log", - "objc2 0.6.3", - "objc2-foundation 0.3.2", + "objc2", + "objc2-foundation", "serde", "serde_json", "serde_repr", "swift-rs", "tauri", "tauri-plugin", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", ] @@ -4430,16 +4726,16 @@ dependencies = [ "serde_repr", "tauri", "tauri-plugin", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "url", ] [[package]] name = "tauri-plugin-shell" -version = "2.3.3" +version = "2.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c374b6db45f2a8a304f0273a15080d98c70cde86178855fc24653ba657a1144c" +checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" dependencies = [ "encoding_rs", "log", @@ -4452,15 +4748,15 @@ dependencies = [ "shared_child", "tauri", "tauri-plugin", - "thiserror 2.0.17", + "thiserror 2.0.18", "tokio", ] [[package]] name = "tauri-plugin-updater" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27cbc31740f4d507712550694749572ec0e43bdd66992db7599b89fbfd6b167b" +checksum = "3fe8e9bebd88fc222938ffdfbdcfa0307081423bd01e3252fc337d8bde81fc61" dependencies = [ "base64 0.22.1", "dirs", @@ -4472,7 +4768,8 @@ dependencies = [ "minisign-verify", "osakit", "percent-encoding", - "reqwest", + "reqwest 0.13.2", + "rustls", "semver", "serde", "serde_json", @@ -4480,7 +4777,7 @@ dependencies = [ "tauri", "tauri-plugin", "tempfile", - "thiserror 2.0.17", + "thiserror 2.0.18", "time", "tokio", "url", @@ -4490,23 +4787,23 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.9.2" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f766fe9f3d1efc4b59b17e7a891ad5ed195fa8d23582abb02e6c9a01137892" +checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2" dependencies = [ "cookie", "dpi", "gtk", "http", "jni", - "objc2 0.6.3", + "objc2", "objc2-ui-kit", "objc2-web-kit", "raw-window-handle", "serde", "serde_json", "tauri-utils", - "thiserror 2.0.17", + "thiserror 2.0.18", "url", "webkit2gtk", "webview2-com", @@ -4515,17 +4812,16 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.9.2" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7950f3bde6bcca6655bc5e76d3d6ec587ceb81032851ab4ddbe1f508bdea2729" +checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" dependencies = [ "gtk", "http", "jni", "log", - "objc2 0.6.3", + "objc2", "objc2-app-kit", - "objc2-foundation 0.3.2", "once_cell", "percent-encoding", "raw-window-handle", @@ -4542,9 +4838,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.8.1" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a423c51176eb3616ee9b516a9fa67fed5f0e78baaba680e44eb5dd2cc37490" +checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d" dependencies = [ "anyhow", "brotli", @@ -4552,7 +4848,7 @@ dependencies = [ "ctor", "dunce", "glob", - "html5ever", + "html5ever 0.29.1", "http", "infer", "json-patch", @@ -4570,8 +4866,8 @@ dependencies = [ "serde_json", "serde_with", "swift-rs", - "thiserror 2.0.17", - "toml 0.9.8", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", "url", "urlpattern", "uuid", @@ -4586,7 +4882,7 @@ checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" dependencies = [ "dunce", "embed-resource", - "toml 0.9.8", + "toml 0.9.12+spec-1.1.0", ] [[package]] @@ -4596,19 +4892,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" dependencies = [ "quick-xml 0.37.5", - "thiserror 2.0.17", + "thiserror 2.0.18", "windows", "windows-version", ] [[package]] name = "tempfile" -version = "3.23.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -4625,6 +4921,16 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -4636,11 +4942,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.17", + "thiserror-impl 2.0.18", ] [[package]] @@ -4651,25 +4957,25 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "time" -version = "0.3.44" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -4677,22 +4983,22 @@ dependencies = [ "num-conv", "num_threads", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -4710,9 +5016,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" dependencies = [ "tinyvec_macros", ] @@ -4725,20 +5031,30 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.48.0" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ "bytes", "libc", "mio", "pin-project-lite", - "signal-hook-registry", "socket2", - "tracing", + "tokio-macros", "windows-sys 0.61.2", ] +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "tokio-rustls" version = "0.26.4" @@ -4751,9 +5067,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ "bytes", "futures-core", @@ -4776,17 +5092,17 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.8" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.13.0", "serde_core", - "serde_spanned 1.0.3", - "toml_datetime 0.7.3", + "serde_spanned 1.1.0", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 0.7.13", + "winnow 0.7.15", ] [[package]] @@ -4800,9 +5116,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.3" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" dependencies = [ "serde_core", ] @@ -4813,7 +5138,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.13.0", "toml_datetime 0.6.3", "winnow 0.5.40", ] @@ -4824,7 +5149,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.13.0", "serde", "serde_spanned 0.6.9", "toml_datetime 0.6.3", @@ -4833,36 +5158,36 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.23.7" +version = "0.25.8+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c" dependencies = [ - "indexmap 2.12.0", - "toml_datetime 0.7.3", + "indexmap 2.13.0", + "toml_datetime 1.1.0+spec-1.1.0", "toml_parser", - "winnow 0.7.13", + "winnow 1.0.0", ] [[package]] name = "toml_parser" -version = "1.0.4" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" dependencies = [ - "winnow 0.7.13", + "winnow 1.0.0", ] [[package]] name = "toml_writer" -version = "1.0.4" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" +checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -4875,11 +5200,11 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytes", "futures-util", "http", @@ -4905,9 +5230,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -4916,43 +5241,43 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] [[package]] name = "tray-icon" -version = "0.21.2" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d5572781bee8e3f994d7467084e1b1fd7a93ce66bd480f8156ba89dee55a2b" +checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" dependencies = [ "crossbeam-channel", "dirs", "libappindicator", "muda", - "objc2 0.6.3", + "objc2", "objc2-app-kit", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation 0.3.2", + "objc2-foundation", "once_cell", "png", "serde", - "thiserror 2.0.17", + "thiserror 2.0.18", "windows-sys 0.60.2", ] @@ -4976,13 +5301,13 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "uds_windows" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -5028,15 +5353,21 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "untrusted" @@ -5046,16 +5377,23 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "urlpattern" version = "0.3.0" @@ -5076,9 +5414,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "utf8-width" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" [[package]] name = "utf8_iter" @@ -5088,21 +5426,21 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.18.1" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.2", "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] [[package]] name = "value-bag" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" [[package]] name = "version-compare" @@ -5169,18 +5507,27 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.1+wasi-0.2.4" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a" dependencies = [ "cfg-if", "once_cell", @@ -5191,22 +5538,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.55" +version = "0.4.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +checksum = "2d1faf851e778dfa54db7cd438b70758eba9755cb47403f3496edd7c8fc212f0" dependencies = [ - "cfg-if", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5214,31 +5558,53 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93" dependencies = [ "unicode-ident", ] [[package]] -name = "wasm-streams" -version = "0.4.2" +name = "wasm-encoder" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" dependencies = [ "futures-util", "js-sys", @@ -5248,10 +5614,22 @@ dependencies = [ ] [[package]] -name = "wayland-backend" -version = "0.3.11" +name = "wasmparser" +version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" dependencies = [ "cc", "downcast-rs", @@ -5263,11 +5641,11 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.11" +version = "0.31.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "rustix", "wayland-backend", "wayland-scanner", @@ -5275,11 +5653,11 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.32.9" +version = "0.32.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -5287,20 +5665,20 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.7" +version = "0.31.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" dependencies = [ "proc-macro2", - "quick-xml 0.37.5", + "quick-xml 0.39.2", "quote", ] [[package]] name = "wayland-sys" -version = "0.31.7" +version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" dependencies = [ "dlib", "log", @@ -5309,9 +5687,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.82" +version = "0.3.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +checksum = "84cde8507f4d7cfcb1185b8cb5890c494ffea65edbe1ba82cfd63661c805ed94" dependencies = [ "js-sys", "wasm-bindgen", @@ -5328,10 +5706,22 @@ dependencies = [ ] [[package]] -name = "webkit2gtk" -version = "2.0.1" +name = "web_atoms" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" +checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" +dependencies = [ + "phf 0.13.1", + "phf_codegen 0.13.1", + "string_cache 0.9.0", + "string_cache_codegen 0.6.1", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" dependencies = [ "bitflags 1.3.2", "cairo-rs", @@ -5353,9 +5743,9 @@ dependencies = [ [[package]] name = "webkit2gtk-sys" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" dependencies = [ "bitflags 1.3.2", "cairo-sys-rs", @@ -5372,19 +5762,28 @@ dependencies = [ ] [[package]] -name = "webpki-roots" -version = "1.0.4" +name = "webpki-root-certs" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] [[package]] name = "webview2-com" -version = "0.38.0" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", @@ -5396,22 +5795,22 @@ dependencies = [ [[package]] name = "webview2-com-macros" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] name = "webview2-com-sys" -version = "0.38.0" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ - "thiserror 2.0.17", + "thiserror 2.0.18", "windows", "windows-core 0.61.2", ] @@ -5453,30 +5852,15 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" dependencies = [ - "objc2 0.6.3", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", "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" @@ -5544,7 +5928,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -5555,7 +5939,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -5876,9 +6260,18 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" dependencies = [ "memchr", ] @@ -5895,9 +6288,91 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.46.0" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" @@ -5907,30 +6382,29 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wry" -version = "0.53.5" +version = "0.54.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728b7d4c8ec8d81cab295e0b5b8a4c263c0d41a785fb8f8c4df284e5411140a2" +checksum = "e5a8135d8676225e5744de000d4dff5a082501bf7db6a1c1495034f8c314edbc" dependencies = [ "base64 0.22.1", - "block2 0.6.2", + "block2", "cookie", "crossbeam-channel", "dirs", + "dom_query", "dpi", "dunce", "gdkx11", "gtk", - "html5ever", "http", "javascriptcore-rs", "jni", - "kuchikiki", "libc", "ndk", - "objc2 0.6.3", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.2", + "objc2-foundation", "objc2-ui-kit", "objc2-web-kit", "once_cell", @@ -5939,7 +6413,7 @@ dependencies = [ "sha2", "soup3", "tao-macros", - "thiserror 2.0.17", + "thiserror 2.0.18", "url", "webkit2gtk", "webkit2gtk-sys", @@ -6009,15 +6483,15 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", "synstructure", ] [[package]] name = "zbus" -version = "5.12.0" +version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" dependencies = [ "async-broadcast", "async-executor", @@ -6033,16 +6507,16 @@ dependencies = [ "futures-core", "futures-lite", "hex", - "nix", + "libc", "ordered-stream", + "rustix", "serde", "serde_repr", - "tokio", "tracing", "uds_windows", "uuid", "windows-sys 0.61.2", - "winnow 0.7.13", + "winnow 0.7.15", "zbus_macros", "zbus_names", "zvariant", @@ -6050,14 +6524,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.12.0" +version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", "zbus_names", "zvariant", "zvariant_utils", @@ -6065,34 +6539,33 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.2.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ "serde", - "static_assertions", - "winnow 0.7.13", + "winnow 0.7.15", "zvariant", ] [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -6112,7 +6585,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", "synstructure", ] @@ -6152,7 +6625,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", ] [[package]] @@ -6163,47 +6636,53 @@ checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" dependencies = [ "arbitrary", "crc32fast", - "indexmap 2.12.0", + "indexmap 2.13.0", "memchr", ] [[package]] -name = "zvariant" -version = "5.8.0" +name = "zmij" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zvariant" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" dependencies = [ "endi", "enumflags2", "serde", "url", - "winnow 0.7.13", + "winnow 0.7.15", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.8.0" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.117", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "3.2.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.110", - "winnow 0.7.13", + "syn 2.0.117", + "winnow 0.7.15", ] diff --git a/src/packages/desktop/src-tauri/Cargo.toml b/src/packages/desktop/src-tauri/Cargo.toml index 3afbd71..de8e351 100644 --- a/src/packages/desktop/src-tauri/Cargo.toml +++ b/src/packages/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openchamber-desktop" -version = "1.9.1" +version = "1.9.9" edition = "2021" publish = false @@ -16,20 +16,27 @@ devtools = ["tauri/devtools"] anyhow = "1.0.86" base64 = "0.22.1" log = "0.4.28" -reqwest = { version = "0.12.4", default-features = false, features = ["rustls-tls", "blocking"] } +reqwest = { version = "0.12.4", default-features = false, features = ["rustls-tls", "json"] } serde = { version = "1.0.210", features = ["derive"] } serde_json = "1.0.143" -tauri = { version = "2.9.4", features = ["macos-private-api"] } -tauri-plugin-dialog = "2.4.2" -tauri-plugin-log = "2.7.1" -tauri-plugin-shell = "2.3.3" +tauri = { version = "2.10.3", features = ["macos-private-api"] } +tauri-plugin-dialog = "2.6.0" +tauri-plugin-log = "2.8.0" +tauri-plugin-shell = "2.3.5" tauri-plugin-notification = "2.3.3" -tauri-plugin-updater = "2" -tokio = { version = "1.38", features = ["rt-multi-thread", "time"] } +tauri-plugin-updater = "2.10.0" +tokio = { version = "1.38", features = ["rt-multi-thread", "time", "macros", "sync"] } url = "2.5" [build-dependencies] -tauri-build = { version = "2.5.3", features = [] } +tauri-build = { version = "2.5.6", features = [] } + +[profile.release] +lto = "thin" +codegen-units = 1 +strip = true [target.'cfg(target_os = "macos")'.dependencies] -window-vibrancy = "0.7.1" +objc2 = "0.6" +objc2-web-kit = "0.3" +rfd = "0.15" diff --git a/src/packages/desktop/src-tauri/src/main.rs b/src/packages/desktop/src-tauri/src/main.rs index a5335de..16182c8 100644 --- a/src/packages/desktop/src-tauri/src/main.rs +++ b/src/packages/desktop/src-tauri/src/main.rs @@ -14,20 +14,31 @@ use std::{ path::{Path, PathBuf}, }; use std::{ - net::TcpListener, + net::{TcpListener, UdpSocket}, process::Command, sync::{ - atomic::{AtomicU64, Ordering}, + atomic::{AtomicU32, AtomicU64, Ordering}, Mutex, }, time::Duration, }; -use tauri::utils::config::BackgroundThrottlingPolicy; use tauri::{Emitter, Manager, WebviewUrl, WebviewWindowBuilder}; + +/// Disable pinch-to-zoom / magnification gestures on macOS to avoid accidental +/// zoom and the continuous gesture event processing overhead. #[cfg(target_os = "macos")] -use window_vibrancy::{ - apply_vibrancy, clear_vibrancy, NSVisualEffectMaterial, -}; +fn disable_pinch_zoom(window: &tauri::WebviewWindow) { + let _ = window.with_webview(|webview| unsafe { + use objc2::rc::Retained; + use objc2_web_kit::WKWebView; + let wk_webview: Retained = + Retained::retain(webview.inner().cast()).unwrap(); + wk_webview.setAllowsMagnification(false); + }); +} + +#[cfg(not(target_os = "macos"))] +fn disable_pinch_zoom(_window: &tauri::WebviewWindow) {} /// Global counter for generating unique window labels. static WINDOW_COUNTER: AtomicU64 = AtomicU64::new(1); @@ -104,6 +115,8 @@ const MENU_ITEM_SETTINGS_ID: &str = "menu_settings"; #[cfg(target_os = "macos")] const MENU_ITEM_COMMAND_PALETTE_ID: &str = "menu_command_palette"; #[cfg(target_os = "macos")] +const MENU_ITEM_QUICK_OPEN_ID: &str = "menu_quick_open"; +#[cfg(target_os = "macos")] const MENU_ITEM_NEW_SESSION_ID: &str = "menu_new_session"; #[cfg(target_os = "macos")] const MENU_ITEM_WORKTREE_CREATOR_ID: &str = "menu_worktree_creator"; @@ -141,6 +154,8 @@ const MENU_ITEM_REQUEST_FEATURE_ID: &str = "menu_request_feature"; const MENU_ITEM_JOIN_DISCORD_ID: &str = "menu_join_discord"; #[cfg(target_os = "macos")] const MENU_ITEM_CLEAR_CACHE_ID: &str = "menu_clear_cache"; +#[cfg(target_os = "macos")] +const MENU_ITEM_QUIT_ID: &str = "menu_quit"; #[cfg(target_os = "macos")] const GITHUB_BUG_REPORT_URL: &str = @@ -151,6 +166,198 @@ const GITHUB_FEATURE_REQUEST_URL: &str = #[cfg(target_os = "macos")] const DISCORD_INVITE_URL: &str = "https://discord.gg/ZYRSdnwwKA"; +static QUIT_CONFIRMED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); +static QUIT_CONFIRMATION_PENDING: std::sync::atomic::AtomicBool = + std::sync::atomic::AtomicBool::new(false); +static QUIT_RISK_HAS_ENABLED_SCHEDULED_TASKS: std::sync::atomic::AtomicBool = + std::sync::atomic::AtomicBool::new(false); +static QUIT_RISK_HAS_RUNNING_SCHEDULED_TASKS: std::sync::atomic::AtomicBool = + std::sync::atomic::AtomicBool::new(false); +static QUIT_RISK_ENABLED_SCHEDULED_TASKS_COUNT: AtomicU32 = AtomicU32::new(0); +static QUIT_RISK_RUNNING_SCHEDULED_TASKS_COUNT: AtomicU32 = AtomicU32::new(0); +static QUIT_RISK_HAS_ACTIVE_TUNNEL: std::sync::atomic::AtomicBool = + std::sync::atomic::AtomicBool::new(false); +static QUIT_RISK_POLLER_STARTED: std::sync::atomic::AtomicBool = + std::sync::atomic::AtomicBool::new(false); + +#[cfg(target_os = "macos")] +const QUIT_RISK_POLL_INTERVAL: Duration = Duration::from_secs(5); + +#[cfg(target_os = "macos")] +fn should_require_quit_confirmation() -> bool { + use std::sync::atomic::Ordering; + + QUIT_RISK_HAS_ACTIVE_TUNNEL.load(Ordering::Relaxed) + || QUIT_RISK_HAS_RUNNING_SCHEDULED_TASKS.load(Ordering::Relaxed) + || QUIT_RISK_HAS_ENABLED_SCHEDULED_TASKS.load(Ordering::Relaxed) +} + +#[cfg(target_os = "macos")] +fn quit_confirmation_message() -> String { + use std::sync::atomic::Ordering; + + let has_active_tunnel = QUIT_RISK_HAS_ACTIVE_TUNNEL.load(Ordering::Relaxed); + let running_tasks_count = QUIT_RISK_RUNNING_SCHEDULED_TASKS_COUNT.load(Ordering::Relaxed); + let enabled_tasks_count = QUIT_RISK_ENABLED_SCHEDULED_TASKS_COUNT.load(Ordering::Relaxed); + + let mut reasons: Vec = Vec::new(); + if has_active_tunnel { + reasons.push("an active tunnel".to_string()); + } + if running_tasks_count > 0 { + reasons.push(format!( + "{} running scheduled task{}", + running_tasks_count, + if running_tasks_count == 1 { "" } else { "s" } + )); + } + if enabled_tasks_count > 0 { + reasons.push(format!( + "{} enabled scheduled task{}", + enabled_tasks_count, + if enabled_tasks_count == 1 { "" } else { "s" } + )); + } + + if reasons.is_empty() { + "Background processes (sidecar, SSH sessions) will be stopped.".to_string() + } else { + format!( + "OpenChamber detected {}. Quitting now will stop sidecar/background processes and may interrupt pending work.", + reasons.join(", ") + ) + } +} + +#[cfg(target_os = "macos")] +const NS_TERMINATE_CANCEL: isize = 0; +#[cfg(target_os = "macos")] +const NS_TERMINATE_NOW: isize = 1; + +#[cfg(target_os = "macos")] +unsafe extern "C-unwind" fn application_should_terminate_with_confirmation( + _: &objc2::runtime::AnyObject, + _: objc2::runtime::Sel, + _: *mut std::ffi::c_void, +) -> isize { + use std::sync::atomic::Ordering; + + if QUIT_CONFIRMED.load(Ordering::SeqCst) { + return NS_TERMINATE_NOW; + } + + if !should_require_quit_confirmation() { + QUIT_CONFIRMED.store(true, Ordering::SeqCst); + return NS_TERMINATE_NOW; + } + + if QUIT_CONFIRMATION_PENDING.swap(true, Ordering::SeqCst) { + return NS_TERMINATE_CANCEL; + } + + let message = quit_confirmation_message(); + let confirmed = matches!( + rfd::MessageDialog::new() + .set_title("Quit OpenChamber?") + .set_description(&message) + .set_level(rfd::MessageLevel::Warning) + .set_buttons(rfd::MessageButtons::OkCancel) + .show(), + rfd::MessageDialogResult::Ok | rfd::MessageDialogResult::Yes + ); + + QUIT_CONFIRMATION_PENDING.store(false, Ordering::SeqCst); + + if confirmed { + QUIT_CONFIRMED.store(true, Ordering::SeqCst); + NS_TERMINATE_NOW + } else { + NS_TERMINATE_CANCEL + } +} + +#[cfg(target_os = "macos")] +fn install_macos_quit_confirmation_hook() { + use objc2::ffi; + use objc2::runtime::{AnyClass, AnyObject, Imp, Sel}; + use std::ffi::CStr; + + unsafe { + let Some(delegate_class) = AnyClass::get(CStr::from_bytes_with_nul_unchecked( + b"TaoAppDelegateParent\0", + )) else { + log::warn!("[desktop] TaoAppDelegateParent class not found; dock Quit confirmation hook skipped"); + return; + }; + + let selector = Sel::register(c"applicationShouldTerminate:"); + if !ffi::class_getInstanceMethod(delegate_class, selector).is_null() { + return; + } + + let imp: Imp = std::mem::transmute( + application_should_terminate_with_confirmation + as unsafe extern "C-unwind" fn(&AnyObject, Sel, *mut std::ffi::c_void) -> isize, + ); + + let added = ffi::class_addMethod( + delegate_class as *const _ as *mut _, + selector, + imp, + b"q@:@\0".as_ptr().cast(), + ); + + if !added.as_bool() { + log::warn!("[desktop] failed to install applicationShouldTerminate hook"); + } + } +} + +#[cfg(not(target_os = "macos"))] +fn install_macos_quit_confirmation_hook() {} + +#[cfg(target_os = "macos")] +fn request_quit_with_confirmation(app: &tauri::AppHandle) { + use std::sync::atomic::Ordering; + use tauri_plugin_dialog::{DialogExt, MessageDialogButtons}; + + if !should_require_quit_confirmation() { + QUIT_CONFIRMED.store(true, Ordering::SeqCst); + app.exit(0); + return; + } + + if QUIT_CONFIRMATION_PENDING.swap(true, Ordering::SeqCst) { + return; + } + + // When app has only hidden windows (common after closing last window), + // ensure at least one window is visible so native dialog reliably appears. + let windows = app.webview_windows(); + let has_visible = windows.values().any(|w| w.is_visible().unwrap_or(false)); + if !has_visible { + if let Some(hidden) = windows.values().find(|w| !w.is_visible().unwrap_or(true)) { + let _ = hidden.show(); + let _ = hidden.set_focus(); + } + } + + let message = quit_confirmation_message(); + let handle = app.clone(); + app.dialog() + .message(message) + .title("Quit OpenChamber?") + .buttons(MessageDialogButtons::OkCancel) + .kind(tauri_plugin_dialog::MessageDialogKind::Warning) + .show(move |confirmed| { + QUIT_CONFIRMATION_PENDING.store(false, Ordering::SeqCst); + if confirmed { + QUIT_CONFIRMED.store(true, Ordering::SeqCst); + handle.exit(0); + } + }); +} + #[cfg(target_os = "macos")] fn build_macos_menu( app: &tauri::AppHandle, @@ -190,6 +397,14 @@ fn build_macos_menu( Some("Cmd+K"), )?; + let quick_open = MenuItem::with_id( + app, + MENU_ITEM_QUICK_OPEN_ID, + "Quick Open…", + true, + Some("Cmd+P"), + )?; + let new_window = MenuItem::with_id( app, MENU_ITEM_NEW_WINDOW_ID, @@ -277,7 +492,7 @@ fn build_macos_menu( MENU_ITEM_TOGGLE_MEMORY_DEBUG_ID, "Toggle Memory Debug", true, - Some("Cmd+Shift+D"), + Some("CmdOrCtrl+Shift+D"), )?; let help_dialog = MenuItem::with_id( @@ -377,13 +592,20 @@ fn build_macos_menu( &PredefinedMenuItem::separator(app)?, &settings, &command_palette, + &quick_open, &PredefinedMenuItem::separator(app)?, &PredefinedMenuItem::services(app, None)?, &PredefinedMenuItem::separator(app)?, &PredefinedMenuItem::hide(app, None)?, &PredefinedMenuItem::hide_others(app, None)?, &PredefinedMenuItem::separator(app)?, - &PredefinedMenuItem::quit(app, None)?, + &MenuItem::with_id( + app, + MENU_ITEM_QUIT_ID, + format!("Quit {}", pkg_info.name), + true, + Some("Cmd+Q"), + )?, ], )?, &Submenu::with_items( @@ -1175,9 +1397,11 @@ fn is_app_bundle_installed(bundle_name: &str) -> bool { const SIDECAR_NAME: &str = "openchamber-server"; const SIDECAR_NOTIFY_PREFIX: &str = "[OpenChamberDesktopNotify] "; const HEALTH_TIMEOUT: Duration = Duration::from_secs(20); -const HEALTH_POLL_INTERVAL: Duration = Duration::from_millis(250); +const HEALTH_POLL_INITIAL_INTERVAL: Duration = Duration::from_millis(250); +const HEALTH_POLL_MAX_INTERVAL: Duration = Duration::from_millis(2000); const LOCAL_SIDECAR_HEALTH_TIMEOUT: Duration = Duration::from_secs(8); -const LOCAL_SIDECAR_HEALTH_POLL_INTERVAL: Duration = Duration::from_millis(100); +const LOCAL_SIDECAR_HEALTH_POLL_INITIAL_INTERVAL: Duration = Duration::from_millis(100); +const LOCAL_SIDECAR_HEALTH_POLL_MAX_INTERVAL: Duration = Duration::from_millis(1000); const STARTUP_REMOTE_PROBE_SOFT_TIMEOUT: Duration = Duration::from_secs(2); const STARTUP_REMOTE_PROBE_HARD_TIMEOUT: Duration = Duration::from_secs(10); @@ -1190,20 +1414,46 @@ const MIN_RESTORE_WINDOW_HEIGHT: u32 = 560; const LOCAL_HOST_ID: &str = "local"; +/// Synthetic host ID used when the boot target is forced via +/// `OPENCHAMBER_SERVER_URL` (no config-based host entry). +const ENV_OVERRIDE_HOST_ID: &str = "__env"; + +/// Synthetic host ID used when a window is opened at an explicit URL +/// via `desktop_new_window_at_url` (no config-based host entry). +const DIRECT_URL_HOST_ID: &str = "__direct"; + +/// Compare two URL strings for "same server" identity using normalized +/// origin + path. This avoids misclassification when one URL has a +/// trailing slash and the other does not (e.g. `OPENCHAMBER_SERVER_URL` +/// pointing at the local sidecar without a trailing `/`). +fn same_server_url(a: &str, b: &str) -> bool { + let parsed_a = url::Url::parse(a); + let parsed_b = url::Url::parse(b); + match (parsed_a, parsed_b) { + (Ok(a), Ok(b)) => { + a.origin() == b.origin() + && a.path().trim_end_matches('/') == b.path().trim_end_matches('/') + } + _ => a == b, + } +} + #[derive(Default)] struct SidecarState { child: Mutex>, url: Mutex>, } -/// Holds the initialization script and local origin, shared across all windows. +/// Holds per-window initialization scripts and a global local origin. +/// Each window gets its own init script (containing the correct boot outcome +/// for that window's target URL), so page reloads re-inject the right data. #[derive(Default)] struct DesktopUiInjectionState { - script: Mutex>, + /// Init scripts keyed by window label. Each window's script contains + /// the correct `__OPENCHAMBER_DESKTOP_BOOT_OUTCOME__` for that window. + scripts: Mutex>, + /// Local origin — shared across all windows since the sidecar is global. local_origin: Mutex>, - /// Host URLs that were probed unreachable (e.g. at startup). - /// `open_new_window` checks this to avoid opening windows at dead hosts. - unreachable_hosts: Mutex>, } /// Tracks the set of currently-focused window labels. @@ -1254,6 +1504,52 @@ struct DesktopHost { struct DesktopHostsConfig { hosts: Vec, default_host_id: Option, + #[serde(default)] + initial_host_choice_completed: bool, +} + +/// Input type for `desktop_hosts_set`. Fields may be omitted to preserve +/// existing stored values, ensuring backward-compatible callers don't +/// accidentally reset onboarding state. +#[derive(Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct DesktopHostsConfigInput { + hosts: Vec, + default_host_id: Option, + #[serde(default)] + initial_host_choice_completed: Option, +} + +/// Process-wide mutex serializing all read-modify-write operations on the +/// desktop `settings.json`. This prevents concurrent writers (host config, +/// local port, window state, vibrancy, onboarding flag) from clobbering +/// each other's independent fields. +static SETTINGS_FILE_MUTEX: Mutex<()> = Mutex::new(()); + +/// Merge a partial input into an existing config, preserving fields that +/// the caller omitted (`None`). This is the single source of truth for +/// the merge semantics used by `desktop_hosts_set`. +fn merge_desktop_hosts_config( + existing: &DesktopHostsConfig, + input: &DesktopHostsConfigInput, +) -> DesktopHostsConfig { + DesktopHostsConfig { + hosts: input.hosts.clone(), + default_host_id: input.default_host_id.clone(), + initial_host_choice_completed: input + .initial_host_choice_completed + .unwrap_or(existing.initial_host_choice_completed), + } +} + +/// Atomic read-merge-write: reads existing config from `path`, merges +/// `input` into it, and writes the result — all while holding the process +/// lock. Tests and the `desktop_hosts_set` command share this path. +fn write_desktop_hosts_config_input_to_path(path: &Path, input: &DesktopHostsConfigInput) -> Result<()> { + let _guard = SETTINGS_FILE_MUTEX.lock().expect("desktop hosts mutex"); + let existing = read_desktop_hosts_config_from_path(path); + let merged = merge_desktop_hosts_config(&existing, input); + write_desktop_hosts_config_to_path(path, &merged) } #[derive(Clone, Serialize, Deserialize)] @@ -1334,13 +1630,14 @@ fn settings_file_path() -> PathBuf { .join("settings.json") } +fn read_desktop_settings_json() -> Option { + fs::read_to_string(settings_file_path()) + .ok() + .and_then(|raw| serde_json::from_str::(&raw).ok()) +} + fn read_desktop_local_port_from_disk() -> Option { - let path = settings_file_path(); - let raw = fs::read_to_string(path).ok(); - let parsed = raw - .as_deref() - .and_then(|s| serde_json::from_str::(s).ok()); - parsed + read_desktop_settings_json() .as_ref() .and_then(|v| v.get("desktopLocalPort")) .and_then(|v| v.as_u64()) @@ -1354,6 +1651,7 @@ fn read_desktop_local_port_from_disk() -> Option { } fn write_desktop_local_port_to_disk(port: u16) -> Result<()> { + let _guard = SETTINGS_FILE_MUTEX.lock().expect("settings file mutex"); let path = settings_file_path(); if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; @@ -1395,6 +1693,12 @@ fn read_desktop_hosts_config_from_path(path: &Path) -> DesktopHostsConfig { .and_then(|v| v.as_str()) .map(|s| s.to_string()); + let initial_host_choice_completed = parsed + .as_ref() + .and_then(|v| v.get("desktopInitialHostChoiceCompleted")) + .and_then(|v| v.as_bool()) + .unwrap_or(false); + let mut hosts: Vec = Vec::new(); if let serde_json::Value::Array(items) = hosts_value { for item in items { @@ -1420,6 +1724,7 @@ fn read_desktop_hosts_config_from_path(path: &Path) -> DesktopHostsConfig { DesktopHostsConfig { hosts, default_host_id: default_value, + initial_host_choice_completed, } } @@ -1438,6 +1743,7 @@ fn read_desktop_window_state_from_disk() -> Option { } fn write_desktop_window_state_to_disk(state: &DesktopWindowState) -> Result<()> { + let _guard = SETTINGS_FILE_MUTEX.lock().expect("settings file mutex"); let path = settings_file_path(); if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; @@ -1458,10 +1764,6 @@ fn write_desktop_window_state_to_disk(state: &DesktopWindowState) -> Result<()> Ok(()) } -fn write_desktop_hosts_config_to_disk(config: &DesktopHostsConfig) -> Result<()> { - write_desktop_hosts_config_to_path(&settings_file_path(), config) -} - fn write_desktop_hosts_config_to_path(path: &Path, config: &DesktopHostsConfig) -> Result<()> { if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; @@ -1503,19 +1805,252 @@ fn write_desktop_hosts_config_to_path(path: &Path, config: &DesktopHostsConfig) Some(id) if !id.trim().is_empty() => serde_json::Value::String(id.trim().to_string()), _ => serde_json::Value::Null, }; + root["desktopInitialHostChoiceCompleted"] = + serde_json::Value::Bool(config.initial_host_choice_completed); fs::write(&path, serde_json::to_string_pretty(&root)?)?; Ok(()) } +// ── Boot outcome resolution ── + +/// Authoritative desktop boot outcome injected into the webview as +/// `window.__OPENCHAMBER_DESKTOP_BOOT_OUTCOME__`. +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct DesktopBootOutcome { + target: Option, // "local" | "remote" | null + status: String, // "ok" | "not-configured" | "unreachable" | "wrong-service" | "missing" + #[serde(skip_serializing_if = "Option::is_none")] + host_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + url: Option, +} + +/// Probe status classification for boot resolution. +#[derive(Clone, Copy, PartialEq, Eq)] +enum ProbeClass { + Ok, + Auth, + Unreachable, + WrongService, + NoProbe, +} + +impl ProbeClass { + fn from_probe(probe: Option<&HostProbeResult>) -> Self { + match probe { + Some(p) if p.status == "ok" => ProbeClass::Ok, + Some(p) if p.status == "auth" => ProbeClass::Auth, + Some(p) if p.status == "wrong-service" => ProbeClass::WrongService, + Some(_) => ProbeClass::Unreachable, + None => ProbeClass::NoProbe, + } + } +} + +/// Result of the shared soft+hard probe policy. +struct ProbeWithRetryResult { + /// Whether the target is navigable (ok or auth). + navigable: bool, + /// The final probe result, if available. + probe: Option, +} + +/// Shared probe policy: soft probe first, hard retry on failure. +/// Used by both startup and open_new_window for consistency. +async fn probe_with_retry(url: &str) -> ProbeWithRetryResult { + let soft_probe = + probe_host_with_timeout(url, STARTUP_REMOTE_PROBE_SOFT_TIMEOUT).await; + + let (navigable, final_probe) = match &soft_probe { + Ok(probe) if matches!(probe.status.as_str(), "ok" | "auth") => { + (true, Some(probe.clone())) + } + Ok(_) => { + log::warn!( + "[desktop] host slow/unreachable ({}), retrying with extended timeout", + url + ); + match probe_host_with_timeout(url, STARTUP_REMOTE_PROBE_HARD_TIMEOUT).await { + Ok(hard_probe) if matches!(hard_probe.status.as_str(), "ok" | "auth") => { + (true, Some(hard_probe)) + } + Ok(hard_probe) => (false, Some(hard_probe)), + Err(_) => (false, None), + } + } + Err(_) => { + log::warn!( + "[desktop] host errored ({}), retrying with extended timeout", + url + ); + match probe_host_with_timeout(url, STARTUP_REMOTE_PROBE_HARD_TIMEOUT).await { + Ok(hard_probe) if matches!(hard_probe.status.as_str(), "ok" | "auth") => { + (true, Some(hard_probe)) + } + Ok(hard_probe) => (false, Some(hard_probe)), + Err(_) => (false, None), + } + } + }; + + ProbeWithRetryResult { + navigable, + probe: final_probe, + } +} + +/// Determine the boot outcome from the desktop hosts config, optional probe +/// result, local server availability, and optional env-forced URL. +/// +/// When `env_target_url` is `Some`, it overrides the config-based default +/// host selection. The returned outcome always describes the actual boot +/// target, including env-forced remotes. +/// +/// This is the single source of truth for boot resolution logic. Both the +/// initial startup and `open_new_window` should delegate to this function +/// for consistency. +fn resolve_boot_outcome( + cfg: &DesktopHostsConfig, + probe: Option<&HostProbeResult>, + local_available: bool, + env_target_url: Option<&str>, +) -> DesktopBootOutcome { + let probe_class = ProbeClass::from_probe(probe); + + // Env-forced URL takes precedence over config. This is its own + // authoritative branch — never falls through to config-based resolution. + if let Some(env_url) = env_target_url { + return match probe_class { + ProbeClass::Ok | ProbeClass::Auth | ProbeClass::NoProbe => DesktopBootOutcome { + target: Some("remote".to_string()), + status: "ok".to_string(), + host_id: Some(ENV_OVERRIDE_HOST_ID.to_string()), + url: Some(env_url.to_string()), + }, + ProbeClass::WrongService => DesktopBootOutcome { + target: Some("remote".to_string()), + status: "wrong-service".to_string(), + host_id: Some(ENV_OVERRIDE_HOST_ID.to_string()), + url: Some(env_url.to_string()), + }, + ProbeClass::Unreachable => DesktopBootOutcome { + target: Some("remote".to_string()), + status: "unreachable".to_string(), + host_id: Some(ENV_OVERRIDE_HOST_ID.to_string()), + url: Some(env_url.to_string()), + }, + }; + } + + // No default host configured + let default_id = cfg.default_host_id.as_deref().unwrap_or(""); + if default_id.is_empty() { + // Whether or not choice is completed, no default means not-configured + return DesktopBootOutcome { + target: None, + status: "not-configured".to_string(), + host_id: None, + url: None, + }; + } + + // Default is local + if default_id == LOCAL_HOST_ID { + if local_available { + return DesktopBootOutcome { + target: Some("local".to_string()), + status: "ok".to_string(), + host_id: None, + url: None, + }; + } + return DesktopBootOutcome { + target: Some("local".to_string()), + status: "unreachable".to_string(), + host_id: None, + url: None, + }; + } + + // Default is a remote host — find it + let host = cfg + .hosts + .iter() + .find(|h| h.id == default_id); + + let Some(host) = host else { + return DesktopBootOutcome { + target: Some("remote".to_string()), + status: "missing".to_string(), + host_id: Some(default_id.to_string()), + url: None, + }; + }; + + let host_id = host.id.clone(); + let host_url = host.url.clone(); + + match probe_class { + ProbeClass::Ok | ProbeClass::Auth => DesktopBootOutcome { + target: Some("remote".to_string()), + status: "ok".to_string(), + host_id: Some(host_id), + url: Some(host_url), + }, + ProbeClass::WrongService => DesktopBootOutcome { + target: Some("remote".to_string()), + status: "wrong-service".to_string(), + host_id: Some(host_id), + url: Some(host_url), + }, + ProbeClass::Unreachable => DesktopBootOutcome { + target: Some("remote".to_string()), + status: "unreachable".to_string(), + host_id: Some(host_id), + url: Some(host_url), + }, + ProbeClass::NoProbe => { + // No probe result and choice already completed — treat as recovery + // (the probe hasn't happened yet, but the user has made a choice, + // so this shouldn't normally occur in practice). + DesktopBootOutcome { + target: Some("remote".to_string()), + status: "unreachable".to_string(), + host_id: Some(host_id), + url: Some(host_url), + } + } + } +} + +/// Compute the boot outcome to display when the local server fails to start. +/// +/// This ensures the UI leaves the splash screen and shows an appropriate +/// chooser/recovery state instead of hanging. It delegates to the existing +/// `resolve_boot_outcome` with `local_available = false` and no probe. +fn compute_local_startup_failure_boot_outcome(cfg: &DesktopHostsConfig) -> DesktopBootOutcome { + resolve_boot_outcome(cfg, None, false, None) +} + +/// Build the init script for the startup failure fallback case. +/// +/// Uses an empty `local_origin` since the local server is not running; +/// the UI can fall back to `window.location.origin` when needed. +fn build_startup_failure_init_script(boot_outcome: &DesktopBootOutcome) -> String { + build_init_script("", Some(boot_outcome)) +} + #[tauri::command] fn desktop_hosts_get() -> Result { Ok(read_desktop_hosts_config_from_disk()) } #[tauri::command] -fn desktop_hosts_set(config: DesktopHostsConfig) -> Result<(), String> { - write_desktop_hosts_config_to_disk(&config).map_err(|err| err.to_string()) +fn desktop_hosts_set(input: DesktopHostsConfigInput) -> Result<(), String> { + write_desktop_hosts_config_input_to_path(&settings_file_path(), &input) + .map_err(|err| err.to_string()) } #[derive(Clone, Serialize)] @@ -1562,9 +2097,129 @@ async fn probe_host_with_timeout(url: &str, timeout: Duration) -> Result Option { + let deadline = std::time::Instant::now() + timeout; + let mut interval = initial_interval; + let mut last_probe: Option = None; + + while std::time::Instant::now() < deadline { + match probe_host_with_timeout(url, max_interval).await { + Ok(probe) if matches!(probe.status.as_str(), "ok" | "auth") => { + return Some(probe); + } + Ok(probe) => { + last_probe = Some(probe); + } + Err(_) => {} + } + + tokio::time::sleep(interval).await; + interval = (interval * 2).min(max_interval); + } + + last_probe +} + +#[cfg(target_os = "macos")] +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ScheduledTasksQuitRiskResponse { + has_enabled_scheduled_tasks: bool, + has_running_scheduled_tasks: bool, + #[serde(default)] + enabled_scheduled_tasks_count: u32, + #[serde(default)] + running_scheduled_tasks_count: u32, +} + +#[cfg(target_os = "macos")] +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct TunnelStatusResponse { + active: bool, +} + +#[cfg(target_os = "macos")] +async fn refresh_quit_risk_flags(local_base_url: &str) { + use std::sync::atomic::Ordering; + + let trimmed = local_base_url.trim_end_matches('/'); + if trimmed.is_empty() { + return; + } + + let client = match reqwest::Client::builder() + .no_proxy() + .timeout(Duration::from_secs(2)) + .build() + { + Ok(client) => client, + Err(_) => return, + }; + + let scheduled_url = format!("{trimmed}/api/openchamber/scheduled-tasks/status"); + let tunnel_url = format!("{trimmed}/api/openchamber/tunnel/status"); + + let scheduled_future = client.get(scheduled_url).send(); + let tunnel_future = client.get(tunnel_url).send(); + let (scheduled_result, tunnel_result) = tokio::join!(scheduled_future, tunnel_future); + + if let Ok(response) = scheduled_result { + if response.status().is_success() { + if let Ok(payload) = response.json::().await { + let enabled_count = payload.enabled_scheduled_tasks_count; + let running_count = payload.running_scheduled_tasks_count; + QUIT_RISK_ENABLED_SCHEDULED_TASKS_COUNT.store(enabled_count, Ordering::Relaxed); + QUIT_RISK_RUNNING_SCHEDULED_TASKS_COUNT.store(running_count, Ordering::Relaxed); + QUIT_RISK_HAS_ENABLED_SCHEDULED_TASKS + .store(payload.has_enabled_scheduled_tasks || enabled_count > 0, Ordering::Relaxed); + QUIT_RISK_HAS_RUNNING_SCHEDULED_TASKS + .store(payload.has_running_scheduled_tasks || running_count > 0, Ordering::Relaxed); + } + } + } + + if let Ok(response) = tunnel_result { + if response.status().is_success() { + if let Ok(payload) = response.json::().await { + QUIT_RISK_HAS_ACTIVE_TUNNEL.store(payload.active, Ordering::Relaxed); + } + } + } +} + +#[cfg(target_os = "macos")] +fn start_quit_risk_poller(local_base_url: String) { + use std::sync::atomic::Ordering; + + if QUIT_RISK_POLLER_STARTED.swap(true, Ordering::SeqCst) { + return; + } + + tauri::async_runtime::spawn(async move { + loop { + refresh_quit_risk_flags(&local_base_url).await; + tokio::time::sleep(QUIT_RISK_POLL_INTERVAL).await; + } + }); +} + +#[cfg(not(target_os = "macos"))] +fn start_quit_risk_poller(_local_base_url: String) {} + +/// Uses the same probe_with_retry policy as startup/new-window (soft + hard) +/// so that first-launch/recovery remote connect accepts slow-but-valid hosts. #[tauri::command] async fn desktop_host_probe(url: String) -> Result { - probe_host_with_timeout(&url, STARTUP_REMOTE_PROBE_SOFT_TIMEOUT).await + let result = probe_with_retry(&url).await; + result + .probe + .ok_or_else(|| "Probe failed".to_string()) } #[derive(Clone, Serialize)] @@ -1742,7 +2397,12 @@ fn maybe_show_sidecar_notification(app: &tauri::AppHandle, payload: SidecarNotif let _ = builder.show(); } -async fn wait_for_health_with(url: &str, timeout: Duration, poll_interval: Duration) -> bool { +async fn wait_for_health_with( + url: &str, + timeout: Duration, + initial_interval: Duration, + max_interval: Duration, +) -> bool { let client = match reqwest::Client::builder().no_proxy().build() { Ok(c) => c, Err(_) => return false, @@ -1750,6 +2410,7 @@ async fn wait_for_health_with(url: &str, timeout: Duration, poll_interval: Durat let deadline = std::time::Instant::now() + timeout; let health_url = format!("{}/health", url.trim_end_matches('/')); + let mut interval = initial_interval; while std::time::Instant::now() < deadline { if let Ok(resp) = client.get(&health_url).send().await { @@ -1757,14 +2418,15 @@ async fn wait_for_health_with(url: &str, timeout: Duration, poll_interval: Durat return true; } } - tokio::time::sleep(poll_interval).await; + tokio::time::sleep(interval).await; + interval = (interval * 2).min(max_interval); } false } async fn wait_for_health(url: &str) -> bool { - wait_for_health_with(url, HEALTH_TIMEOUT, HEALTH_POLL_INTERVAL).await + wait_for_health_with(url, HEALTH_TIMEOUT, HEALTH_POLL_INITIAL_INTERVAL, HEALTH_POLL_MAX_INTERVAL).await } fn kill_sidecar(app: tauri::AppHandle) { @@ -1774,16 +2436,28 @@ fn kill_sidecar(app: tauri::AppHandle) { let sidecar_url = state.url.lock().expect("sidecar url mutex").clone(); if let Some(url) = sidecar_url { - let shutdown_url = format!("{}/api/system/shutdown", url.trim_end_matches('/')); - if let Ok(client) = reqwest::blocking::Client::builder() - .no_proxy() - .timeout(Duration::from_millis(1500)) - .build() - { - if let Ok(resp) = client.post(shutdown_url).send() { - if resp.status().is_success() { - std::thread::sleep(Duration::from_millis(100)); - } + // Attempt graceful shutdown via a raw HTTP POST to avoid pulling in + // reqwest::blocking (and its extra thread pool) just for this one call. + if let Ok(parsed) = url::Url::parse(&url) { + let host = parsed.host_str().unwrap_or("127.0.0.1"); + let port = parsed.port().unwrap_or(80); + let path = "/api/system/shutdown"; + if let Ok(mut stream) = + std::net::TcpStream::connect_timeout( + &format!("{host}:{port}").parse().unwrap(), + Duration::from_millis(1500), + ) + { + use std::io::Write; + let _ = stream.set_write_timeout(Some(Duration::from_millis(1500))); + let _ = stream.set_read_timeout(Some(Duration::from_millis(1500))); + let request = format!( + "POST {path} HTTP/1.1\r\nHost: {host}:{port}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" + ); + let _ = stream.write_all(request.as_bytes()); + let _ = stream.flush(); + // Brief pause to let the sidecar begin its shutdown sequence. + std::thread::sleep(Duration::from_millis(100)); } } } @@ -1875,27 +2549,10 @@ async fn spawn_local_server(app: &tauri::AppHandle) -> Result { } }); + let desktop_settings = read_desktop_settings_json(); + let opencode_binary_from_settings: Option = (|| { - let data_dir = env::var("OPENCHAMBER_DATA_DIR") - .ok() - .and_then(|v| { - let t = v.trim().to_string(); - if t.is_empty() { - None - } else { - Some(PathBuf::from(t)) - } - }) - .or_else(|| { - resolved_home_dir_path - .as_ref() - .map(|home| home.join(".config").join("openchamber")) - }); - let data_dir = data_dir?; - let settings_path = data_dir.join("settings.json"); - let raw = fs::read_to_string(&settings_path).ok()?; - let json = serde_json::from_str::(&raw).ok()?; - let value = json.get("opencodeBinary")?.as_str()?.trim(); + let value = desktop_settings.as_ref()?.get("opencodeBinary")?.as_str()?.trim(); if value.is_empty() { return None; } @@ -1919,6 +2576,13 @@ async fn spawn_local_server(app: &tauri::AppHandle) -> Result { Some(candidate) })(); + let sidecar_bind_host = desktop_settings + .as_ref() + .and_then(|value| value.get("desktopLanAccessEnabled")) + .and_then(|value| value.as_bool()) + .map(|enabled| if enabled { "0.0.0.0" } else { "127.0.0.1" }) + .unwrap_or("127.0.0.1"); + let mut push_unique = |value: String| { let trimmed = value.trim(); if trimmed.is_empty() { @@ -1995,7 +2659,7 @@ async fn spawn_local_server(app: &tauri::AppHandle) -> Result { .sidecar(SIDECAR_NAME) .map_err(|err| anyhow!("Failed to resolve sidecar '{SIDECAR_NAME}': {err}"))? .args(["--port", &port.to_string()]) - .env("OPENCHAMBER_HOST", "127.0.0.1") + .env("OPENCHAMBER_HOST", sidecar_bind_host) .env("OPENCHAMBER_DIST_DIR", dist_dir.clone()) .env("OPENCHAMBER_RUNTIME", "desktop") .env("OPENCHAMBER_DESKTOP_NOTIFY", "true") @@ -2068,7 +2732,8 @@ async fn spawn_local_server(app: &tauri::AppHandle) -> Result { if !wait_for_health_with( &url, LOCAL_SIDECAR_HEALTH_TIMEOUT, - LOCAL_SIDECAR_HEALTH_POLL_INTERVAL, + LOCAL_SIDECAR_HEALTH_POLL_INITIAL_INTERVAL, + LOCAL_SIDECAR_HEALTH_POLL_MAX_INTERVAL, ) .await { @@ -2253,9 +2918,12 @@ fn desktop_new_window(app: tauri::AppHandle) -> Result<(), String> { /// Open a new window pointed at a specific URL (used by the host switcher UI). /// -/// IMPORTANT: Must remain synchronous -- see `desktop_new_window` doc comment. +/// For remote URLs (not matching local origin), probes the host and only opens +/// the window if the probe returns `ok` or `auth`. Falls back to local if the +/// remote is non-navigable. Window creation is dispatched to the main thread +/// and its result is propagated back to the caller. #[tauri::command] -fn desktop_new_window_at_url(app: tauri::AppHandle, url: String) -> Result<(), String> { +async fn desktop_new_window_at_url(app: tauri::AppHandle, url: String) -> Result<(), String> { // Validate scheme to prevent file://, data:, javascript: etc. let parsed = url::Url::parse(&url).map_err(|e| format!("Invalid URL: {e}"))?; match parsed.scheme() { @@ -2274,7 +2942,68 @@ fn desktop_new_window_at_url(app: tauri::AppHandle, url: String) -> Result<(), S }) .ok_or_else(|| "Local origin not yet known (sidecar may still be starting)".to_string())?; - create_window(&app, &url, &local_origin, false).map_err(|e| e.to_string()) + // If the URL is local, create the window directly. + if same_server_url(&url, &local_origin) { + let boot_outcome = DesktopBootOutcome { + target: Some("local".to_string()), + status: "ok".to_string(), + host_id: None, + url: None, + }; + let (tx, rx) = tokio::sync::oneshot::channel(); + let handle = app.clone(); + app.run_on_main_thread(move || { + let result = create_window(&handle, &url, &local_origin, Some(&boot_outcome), false) + .map_err(|e| e.to_string()); + let _ = tx.send(result); + }) + .map_err(|e| e.to_string())?; + return rx.await.map_err(|_| "Window creation cancelled".to_string())?; + } + + // Remote URL: probe with shared retry policy before opening. + let result = probe_with_retry(&url).await; + + let (final_url, boot_outcome) = if result.navigable { + let outcome = DesktopBootOutcome { + target: Some("remote".to_string()), + status: "ok".to_string(), + host_id: Some(DIRECT_URL_HOST_ID.to_string()), + url: Some(url.clone()), + }; + (url, outcome) + } else { + log::info!( + "[desktop] new_window_at_url: remote ({}) probe returned non-navigable status, falling back to local", + url + ); + let local_fallback = format!("{}/", local_origin); + let outcome = match &result.probe { + Some(probe) if probe.status == "wrong-service" => DesktopBootOutcome { + target: Some("remote".to_string()), + status: "wrong-service".to_string(), + host_id: Some(DIRECT_URL_HOST_ID.to_string()), + url: Some(url), + }, + _ => DesktopBootOutcome { + target: Some("remote".to_string()), + status: "unreachable".to_string(), + host_id: Some(DIRECT_URL_HOST_ID.to_string()), + url: Some(url), + }, + }; + (local_fallback, outcome) + }; + + let (tx, rx) = tokio::sync::oneshot::channel(); + let handle = app.clone(); + app.run_on_main_thread(move || { + let result = create_window(&handle, &final_url, &local_origin, Some(&boot_outcome), false) + .map_err(|e| e.to_string()); + let _ = tx.send(result); + }) + .map_err(|e| e.to_string())?; + rx.await.map_err(|_| "Window creation cancelled".to_string())? } /// Read a file and return its content as base64 with mime type detection. @@ -2334,6 +3063,45 @@ fn desktop_read_file(path: String) -> Result { }) } +#[tauri::command] +async fn desktop_save_markdown_file( + app: tauri::AppHandle, + default_file_name: String, + content: String, +) -> Result, String> { + use tauri_plugin_dialog::DialogExt; + + let trimmed_file_name = default_file_name.trim(); + if trimmed_file_name.is_empty() { + return Err("Default file name is required".to_string()); + } + + let (tx, rx) = tokio::sync::oneshot::channel(); + app.dialog() + .file() + .add_filter("Markdown", &["md"]) + .set_file_name(trimmed_file_name) + .save_file(move |file_path| { + let _ = tx.send(file_path); + }); + + let Some(file_path) = rx + .await + .map_err(|_| "Save dialog was closed unexpectedly".to_string())? + else { + return Ok(None); + }; + + let path = file_path + .into_path() + .map_err(|_| "Selected export path is not a local filesystem path".to_string())?; + + std::fs::write(&path, content) + .map_err(|error| format!("Failed to save exported session: {error}"))?; + + Ok(Some(path.to_string_lossy().to_string())) +} + #[derive(Serialize)] struct FileContent { mime: String, @@ -2389,16 +3157,19 @@ fn macos_major_version() -> Option { /// Build the initialization script injected into every webview window. /// This is computed once and reused for all windows. -fn build_init_script(local_origin: &str) -> String { +fn build_init_script(local_origin: &str, boot_outcome: Option<&DesktopBootOutcome>) -> String { let home = std::env::var(if cfg!(windows) { "USERPROFILE" } else { "HOME" }).unwrap_or_default(); let macos_major = macos_major_version().unwrap_or(0); let home_json = serde_json::to_string(&home).unwrap_or_else(|_| "\"\"".into()); let local_json = serde_json::to_string(local_origin).unwrap_or_else(|_| "\"\"".into()); + let boot_outcome_json = boot_outcome + .and_then(|o| serde_json::to_string(o).ok()) + .unwrap_or_else(|| "undefined".to_string()); let mut init_script = format!( - "(function(){{try{{window.__OPENCHAMBER_HOME__={home_json};window.__OPENCHAMBER_MACOS_MAJOR__={macos_major};window.__OPENCHAMBER_LOCAL_ORIGIN__={local_json};}}catch(_e){{}}}})();" + "(function(){{try{{window.__OPENCHAMBER_HOME__={home_json};window.__OPENCHAMBER_MACOS_MAJOR__={macos_major};window.__OPENCHAMBER_LOCAL_ORIGIN__={local_json};window.__OPENCHAMBER_DESKTOP_BOOT_OUTCOME__={boot_outcome_json};}}catch(_e){{}}}})();" ); // Cleanup: older builds injected a native-ish Instance switcher button into pages. @@ -2426,9 +3197,7 @@ fn parse_theme_override(theme_mode: Option<&str>, theme_variant: Option<&str>) - } fn read_desktop_theme_override() -> Option { - let settings = fs::read_to_string(settings_file_path()) - .ok() - .and_then(|raw| serde_json::from_str::(&raw).ok()); + let settings = read_desktop_settings_json(); let use_system_theme = settings .as_ref() @@ -2452,22 +3221,42 @@ fn read_desktop_theme_override() -> Option { parse_theme_override(theme_mode, theme_variant) } -#[cfg(target_os = "macos")] -fn apply_macos_window_vibrancy(window: &tauri::WebviewWindow) { - let _ = clear_vibrancy(window); +fn detect_desktop_lan_ipv4() -> Option { + let socket = UdpSocket::bind("0.0.0.0:0").ok()?; + socket.connect("8.8.8.8:80").ok()?; + let address = socket.local_addr().ok()?; + let ip = address.ip(); - if let Err(error) = apply_vibrancy( - window, - NSVisualEffectMaterial::Sidebar, - None, - None, - ) { - log::warn!("[desktop:vibrancy] Failed to apply macOS vibrancy: {error}"); + if ip.is_loopback() { + return None; + } + + match ip { + std::net::IpAddr::V4(ipv4) => Some(ipv4.to_string()), + std::net::IpAddr::V6(_) => None, } } -#[cfg(not(target_os = "macos"))] -fn apply_macos_window_vibrancy(_window: &tauri::WebviewWindow) {} +/// Apply platform-specific window builder configuration. +fn apply_platform_window_config>( + builder: WebviewWindowBuilder<'_, tauri::Wry, M>, +) -> WebviewWindowBuilder<'_, tauri::Wry, M> { + #[cfg(target_os = "macos")] + let builder = builder + .hidden_title(true) + .title_bar_style(tauri::TitleBarStyle::Overlay) + .traffic_light_position(tauri::Position::Logical(tauri::LogicalPosition { + x: 17.0, + y: 26.0, + })); + + #[cfg(target_os = "windows")] + let builder = builder.additional_browser_args( + "--proxy-bypass-list=<-loopback> --disable-features=msWebOOUI,msPdfOOUI,msSmartScreenProtection", + ); + + builder +} #[tauri::command] fn desktop_set_window_theme( @@ -2484,6 +3273,11 @@ fn desktop_set_window_theme( Ok(()) } +#[tauri::command] +fn desktop_get_lan_address() -> Option { + detect_desktop_lan_ipv4() +} + fn is_window_state_visible(app: &tauri::AppHandle, state: &DesktopWindowState) -> bool { if state.width == 0 || state.height == 0 { return false; @@ -2602,16 +3396,22 @@ fn create_window( app: &tauri::AppHandle, url: &str, local_origin: &str, + boot_outcome: Option<&DesktopBootOutcome>, restore_geometry: bool, ) -> Result<()> { let parsed = url::Url::parse(url).map_err(|err| anyhow!("Invalid URL: {err}"))?; let label = next_window_label(app); - let init_script = build_init_script(local_origin); + let init_script = build_init_script(local_origin, boot_outcome); - // Store the init script and local origin so new windows and page reloads can reuse it. + // Store the init script under this window's label so page reloads + // re-inject the correct boot outcome for this window. if let Some(state) = app.try_state::() { - *state.script.lock().expect("desktop ui injection mutex") = Some(init_script.clone()); + state + .scripts + .lock() + .expect("desktop ui injection mutex") + .insert(label.clone(), init_script.clone()); *state .local_origin .lock() @@ -2630,8 +3430,9 @@ fn create_window( .min_inner_size(MIN_WINDOW_WIDTH as f64, MIN_WINDOW_HEIGHT as f64) .decorations(true) .visible(false) - .initialization_script(&init_script) - .background_throttling(BackgroundThrottlingPolicy::Disabled); + .initialization_script(&init_script); + + builder = apply_platform_window_config(builder); let apply_restored_state = restored_state .as_ref() @@ -2646,21 +3447,9 @@ fn create_window( .position(state.x as f64, state.y as f64); } - #[cfg(target_os = "macos")] - { - builder = builder - .transparent(true) - .hidden_title(true) - .title_bar_style(tauri::TitleBarStyle::Overlay) - .traffic_light_position(tauri::Position::Logical(tauri::LogicalPosition { - x: 17.0, - y: 26.0, - })); - } - let window = builder.build()?; let _ = window.set_theme(read_desktop_theme_override()); - apply_macos_window_vibrancy(&window); + disable_pinch_zoom(&window); if let Some(state) = restored_state.as_ref().filter(|_| apply_restored_state) { if state.maximized || state.fullscreen { @@ -2693,8 +3482,9 @@ fn create_startup_window(app: &tauri::AppHandle, restore_geometry: bool) -> Resu .min_inner_size(MIN_WINDOW_WIDTH as f64, MIN_WINDOW_HEIGHT as f64) .decorations(true) .visible(true) - .initialization_script(&splash_script) - .background_throttling(BackgroundThrottlingPolicy::Disabled); + .initialization_script(&splash_script); + + builder = apply_platform_window_config(builder); let apply_restored_state = restored_state .as_ref() @@ -2709,21 +3499,9 @@ fn create_startup_window(app: &tauri::AppHandle, restore_geometry: bool) -> Resu .position(state.x as f64, state.y as f64); } - #[cfg(target_os = "macos")] - { - builder = builder - .transparent(true) - .hidden_title(true) - .title_bar_style(tauri::TitleBarStyle::Overlay) - .traffic_light_position(tauri::Position::Logical(tauri::LogicalPosition { - x: 17.0, - y: 26.0, - })); - } - let window = builder.build()?; let _ = window.set_theme(read_desktop_theme_override()); - apply_macos_window_vibrancy(&window); + disable_pinch_zoom(&window); if let Some(state) = restored_state.as_ref().filter(|_| apply_restored_state) { if state.maximized || state.fullscreen { @@ -2820,12 +3598,21 @@ fn build_startup_splash_script() -> String { ) } -fn activate_main_window(app: &tauri::AppHandle, url: &str, local_origin: &str) -> Result<()> { +fn activate_main_window( + app: &tauri::AppHandle, + url: &str, + local_origin: &str, + boot_outcome: Option<&DesktopBootOutcome>, +) -> Result<()> { let parsed = url::Url::parse(url).map_err(|err| anyhow!("Invalid URL: {err}"))?; - let init_script = build_init_script(local_origin); + let init_script = build_init_script(local_origin, boot_outcome); if let Some(state) = app.try_state::() { - *state.script.lock().expect("desktop ui injection mutex") = Some(init_script); + state + .scripts + .lock() + .expect("desktop ui injection mutex") + .insert("main".to_string(), init_script); *state .local_origin .lock() @@ -2838,7 +3625,7 @@ fn activate_main_window(app: &tauri::AppHandle, url: &str, local_origin: &str) - return Ok(()); } - create_window(app, url, local_origin, true) + create_window(app, url, local_origin, boot_outcome, true) } /// Open a new window pointed at the default host (local or configured default). @@ -2877,7 +3664,7 @@ fn open_new_window(app: &tauri::AppHandle) { return; }; - // Resolve the URL the same way as initial setup: default host or local. + // Resolve the URL the same way as initial setup: env override, then default host, else local. let local_url = app .try_state::() .and_then(|state| state.url.lock().expect("sidecar url mutex").clone()) @@ -2890,52 +3677,118 @@ fn open_new_window(app: &tauri::AppHandle) { local_url.clone() }; - let mut target_url = local_ui_url.clone(); + let env_target = std::env::var("OPENCHAMBER_SERVER_URL") + .ok() + .and_then(|raw| normalize_server_url(&raw)) + .filter(|url| !same_server_url(url, &local_ui_url)); let cfg = read_desktop_hosts_config_from_disk(); - if let Some(default_id) = cfg.default_host_id { + + let target_url = if let Some(ref env_url) = env_target { + env_url.clone() + } else if let Some(default_id) = cfg.default_host_id.as_deref() { if default_id == LOCAL_HOST_ID { - target_url = local_ui_url.clone(); - } else if let Some(host) = cfg.hosts.into_iter().find(|h| h.id == default_id) { - target_url = host.url; + local_ui_url.clone() + } else { + cfg.hosts + .iter() + .find(|h| h.id == default_id) + .map(|h| h.url.clone()) + .unwrap_or(local_ui_url.clone()) } + } else { + local_ui_url.clone() + }; + + // Compute boot outcome for the new window (no probe yet for sync local case). + let boot_outcome = resolve_boot_outcome( + &cfg, + None, + true, + env_target.as_deref(), + ); + + // If the target is local, create the window immediately on this (main) thread. + if same_server_url(&target_url, &local_ui_url) { + if let Err(err) = create_window(app, &target_url, &local_origin, Some(&boot_outcome), false) { + log::error!("[desktop] failed to create new window: {err}"); + } + return; } - // If this host was previously probed unreachable (e.g. at startup), fall back to local. - if target_url != local_ui_url { - let is_cached_unreachable = app - .try_state::() - .map(|state| { - state - .unreachable_hosts - .lock() - .expect("unreachable hosts mutex") - .contains(&target_url) - }) - .unwrap_or(false); + // For remote hosts, probe asynchronously then dispatch window creation + // back to the main thread via run_on_main_thread (required on macOS). + // Uses the same probe_with_retry policy as startup (soft + hard). + let handle = app.clone(); + let cfg_snapshot = cfg.clone(); + let env_target_snapshot = env_target.clone(); + tauri::async_runtime::spawn(async move { + let result = probe_with_retry(&target_url).await; - if is_cached_unreachable { + let final_url = if result.navigable { + target_url + } else { log::info!( - "[desktop] new window: default host ({}) cached as unreachable, using local", + "[desktop] new window: default host ({}) probe returned non-navigable status, using local", target_url ); - target_url = local_ui_url; - } - } + local_ui_url + }; - if let Err(err) = create_window(app, &target_url, &local_origin, false) { - log::error!("[desktop] failed to create new window: {err}"); - } + // Recompute boot outcome with actual probe result, using the + // same config/env snapshot that chose this window's target. + let final_boot_outcome = resolve_boot_outcome( + &cfg_snapshot, + result.probe.as_ref(), + true, + env_target_snapshot.as_deref(), + ); + + let local = local_origin; + let handle_clone = handle.clone(); + if let Err(err) = handle.run_on_main_thread(move || { + if let Err(err) = create_window(&handle_clone, &final_url, &local, Some(&final_boot_outcome), false) { + log::error!("[desktop] failed to create new window: {err}"); + } + }) { + log::error!("[desktop] failed to dispatch window creation to main thread: {err}"); + } + }); } fn main() { + // Ensure localhost traffic never routes through a system/VPN proxy. + for key in ["NO_PROXY", "no_proxy"] { + let existing = env::var(key).unwrap_or_default(); + let loopback = ["127.0.0.1", "localhost", "::1"]; + let missing: Vec<&str> = loopback + .iter() + .filter(|addr| !existing.split(',').any(|part| part.trim() == **addr)) + .copied() + .collect(); + if !missing.is_empty() { + let merged = if existing.is_empty() { + missing.join(",") + } else { + format!("{},{}", existing, missing.join(",")) + }; + env::set_var(key, &merged); + } + } + let log_builder = tauri_plugin_log::Builder::default() .level(log::LevelFilter::Info) .clear_targets() - .targets([ - tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout), - tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Webview), - ]); + .targets(if cfg!(debug_assertions) { + vec![ + tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout), + tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Webview), + ] + } else { + vec![tauri_plugin_log::Target::new( + tauri_plugin_log::TargetKind::Stdout, + )] + }); let builder = tauri::Builder::default() .manage(SidecarState::default()) @@ -2951,8 +3804,9 @@ fn main() { .plugin(log_builder.build()) .on_page_load(|window, _payload| { if let Some(state) = window.app_handle().try_state::() { - if let Ok(guard) = state.script.lock() { - if let Some(script) = guard.as_ref() { + let label = window.label().to_string(); + if let Ok(guard) = state.scripts.lock() { + if let Some(script) = guard.get(&label) { let _ = window.eval(script); } } @@ -3031,6 +3885,10 @@ fn main() { dispatch_menu_action(app, "command-palette"); return; } + if id == MENU_ITEM_QUICK_OPEN_ID { + dispatch_menu_action(app, "quick-open"); + return; + } if id == MENU_ITEM_NEW_SESSION_ID { dispatch_menu_action(app, "new-session"); @@ -3104,6 +3962,10 @@ fn main() { }); return; } + if id == MENU_ITEM_QUIT_ID { + request_quit_with_confirmation(app); + return; + } } }) .on_window_event(|window, event| { @@ -3117,19 +3979,16 @@ fn main() { } if let tauri::WindowEvent::Destroyed = event { - // Clean up focus tracking for the destroyed window. if let Some(state) = app.try_state::() { state.remove_window(&label); } - // If this was the last window, kill the sidecar and exit. - let remaining = app.webview_windows().len(); - if remaining == 0 { - if let Some(state) = app.try_state::() { - state.shutdown_all(&app); - } - kill_sidecar(app.clone()); - app.exit(0); + if let Some(state) = app.try_state::() { + state + .scripts + .lock() + .expect("desktop ui injection mutex") + .remove(&label); } } @@ -3137,8 +3996,19 @@ fn main() { schedule_window_state_persist(window.clone(), false); } - if let tauri::WindowEvent::CloseRequested { .. } = event { + if let tauri::WindowEvent::CloseRequested { api, .. } = event { schedule_window_state_persist(window.clone(), true); + + let remaining_visible = app + .webview_windows() + .values() + .filter(|w| w.is_visible().unwrap_or(false)) + .count(); + + if remaining_visible <= 1 { + api.prevent_close(); + let _ = window.hide(); + } } }) .invoke_handler(tauri::generate_handler![ @@ -3154,10 +4024,12 @@ fn main() { desktop_filter_installed_apps, desktop_get_installed_apps, desktop_fetch_app_icons, + desktop_save_markdown_file, desktop_hosts_get, desktop_hosts_set, desktop_host_probe, desktop_set_window_theme, + desktop_get_lan_address, remote_ssh::desktop_ssh_instances_get, remote_ssh::desktop_ssh_instances_set, remote_ssh::desktop_ssh_import_hosts, @@ -3176,6 +4048,28 @@ fn main() { } tauri::async_runtime::spawn(async move { + // Helper: inject a fallback boot outcome when the local server + // cannot start, so the UI leaves the splash and shows + // chooser/recovery instead of hanging on a white screen. + let handle_for_fallback = handle.clone(); + let inject_startup_failure = |err: String| { + log::error!("[desktop] failed to start local server: {err}"); + let cfg = read_desktop_hosts_config_from_disk(); + let boot_outcome = compute_local_startup_failure_boot_outcome(&cfg); + let init_script = build_startup_failure_init_script(&boot_outcome); + if let Some(state) = handle_for_fallback.try_state::() + { + state + .scripts + .lock() + .expect("desktop ui injection mutex") + .insert("main".to_string(), init_script.clone()); + } + if let Some(window) = handle_for_fallback.get_webview_window("main") { + let _ = window.eval(&init_script); + } + }; + let local_url = if cfg!(debug_assertions) { let dev_url = "http://127.0.0.1:3901".to_string(); if wait_for_health(&dev_url).await { @@ -3184,7 +4078,7 @@ fn main() { match spawn_local_server(&handle).await { Ok(local) => local, Err(err) => { - log::error!("[desktop] failed to start local server: {err}"); + inject_startup_failure(err.to_string()); return; } } @@ -3193,7 +4087,7 @@ fn main() { match spawn_local_server(&handle).await { Ok(local) => local, Err(err) => { - log::error!("[desktop] failed to start local server: {err}"); + inject_startup_failure(err.to_string()); return; } } @@ -3216,6 +4110,7 @@ fn main() { if let Some(state) = handle.try_state::() { *state.url.lock().expect("sidecar url mutex") = Some(local_url.clone()); } + start_quit_risk_poller(local_url.clone()); let local_origin = url::Url::parse(&local_ui_url) .ok() @@ -3223,66 +4118,88 @@ fn main() { .unwrap_or_else(|| local_ui_url.clone()); // Selected host: env override first, then desktop default host, else local. + // If env override points to the local server, ignore it and use + // config-based resolution instead. let env_target = std::env::var("OPENCHAMBER_SERVER_URL") .ok() - .and_then(|raw| normalize_server_url(&raw)); + .and_then(|raw| normalize_server_url(&raw)) + .filter(|url| !same_server_url(url, &local_ui_url)); - let mut initial_url = env_target.unwrap_or_else(|| local_ui_url.clone()); + let mut initial_url = env_target.as_deref().unwrap_or(&local_ui_url).to_string(); - if initial_url == local_ui_url { - let cfg = read_desktop_hosts_config_from_disk(); - if let Some(default_id) = cfg.default_host_id { - if default_id == LOCAL_HOST_ID { - initial_url = local_ui_url.clone(); - } else if let Some(host) = cfg.hosts.into_iter().find(|h| h.id == default_id) { - initial_url = host.url; + // Compute boot outcome and legacy-upgrade if needed. + let cfg = read_desktop_hosts_config_from_disk(); + + if env_target.is_none() { + if let Some(default_id) = cfg.default_host_id.as_deref() { + if default_id != LOCAL_HOST_ID { + if let Some(host) = cfg.hosts.iter().find(|h| h.id == default_id) { + initial_url = host.url.clone(); + } } } } - if initial_url != local_ui_url { - let failed_url = initial_url.clone(); - let soft_probe = - probe_host_with_timeout(&initial_url, STARTUP_REMOTE_PROBE_SOFT_TIMEOUT).await; + // If remote, probe and fall back to local if unreachable. + // Use the shared probe_with_retry policy (soft + hard). + let final_probe: Option = if !same_server_url(&initial_url, &local_ui_url) { + let result = probe_with_retry(&initial_url).await; - let remote_reachable = match soft_probe { - Ok(probe) if probe.status != "unreachable" => true, - Ok(_) | Err(_) => { - log::warn!( - "[desktop] startup host slow/unreachable ({}), retrying with extended timeout", - initial_url - ); - - match probe_host_with_timeout( - &initial_url, - STARTUP_REMOTE_PROBE_HARD_TIMEOUT, - ) - .await - { - Ok(probe) if probe.status != "unreachable" => true, - Ok(_) | Err(_) => false, - } - } - }; - - if !remote_reachable { + if !result.navigable { log::warn!( "[desktop] startup host unreachable after retries ({}), falling back to local ({})", initial_url, local_ui_url ); initial_url = local_ui_url.clone(); - if let Some(state) = handle.try_state::() { - state - .unreachable_hosts - .lock() - .expect("unreachable hosts mutex") - .insert(failed_url); - } } - } - if let Err(err) = activate_main_window(&handle, &initial_url, &local_origin) { + result.probe + } else { + None + }; + + // Probe the local server to verify opencode is actually running. + // spawn_local_server only confirms the sidecar web server responded + // HTTP 200 — it does not check whether opencode CLI is ready. + let local_available = match wait_for_local_opencode_ready_with( + &local_url, + LOCAL_SIDECAR_HEALTH_TIMEOUT, + LOCAL_SIDECAR_HEALTH_POLL_INITIAL_INTERVAL, + LOCAL_SIDECAR_HEALTH_POLL_MAX_INTERVAL, + ) + .await + { + Some(probe) if matches!(probe.status.as_str(), "ok" | "auth") => { + log::info!("[desktop] local opencode verified (status={})", probe.status); + true + } + Some(probe) => { + log::warn!( + "[desktop] local server up but opencode not ready (status={}), treating as unavailable", + probe.status + ); + false + } + None => { + log::warn!("[desktop] local opencode probe failed, treating as unavailable"); + false + } + }; + + let boot_outcome = resolve_boot_outcome( + &cfg, + final_probe.as_ref(), + local_available, + env_target.as_deref(), + ); + + if let Err(err) = activate_main_window( + &handle, + &initial_url, + &local_origin, + Some(&boot_outcome), + ) { log::error!("[desktop] failed to activate main window: {err}"); } }); @@ -3295,10 +4212,18 @@ fn main() { .build(tauri::generate_context!()) .expect("failed to build Tauri application"); + install_macos_quit_confirmation_hook(); + app.run(|app_handle, event| { match event { - tauri::RunEvent::ExitRequested { .. } => { - // Best-effort cleanup; never block shutdown. + tauri::RunEvent::ExitRequested { api, .. } => { + use std::sync::atomic::Ordering; + if !QUIT_CONFIRMED.load(Ordering::SeqCst) { + api.prevent_exit(); + #[cfg(target_os = "macos")] + request_quit_with_confirmation(app_handle); + return; + } if let Some(state) = app_handle.try_state::() { state.shutdown_all(app_handle); } @@ -3315,9 +4240,18 @@ fn main() { has_visible_windows, .. } => { - // macOS: clicking dock icon when no windows are open opens a new one. if !has_visible_windows { - open_new_window(app_handle); + let windows = app_handle.webview_windows(); + let hidden = windows + .values() + .find(|w| !w.is_visible().unwrap_or(true)); + if let Some(w) = hidden { + let _ = w.show(); + let _ = w.set_focus(); + } else { + drop(windows); + open_new_window(app_handle); + } } } _ => {} @@ -3328,6 +4262,10 @@ fn main() { #[cfg(test)] mod tests { use super::*; + use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }; use std::time::{SystemTime, UNIX_EPOCH}; fn unique_settings_path(test_name: &str) -> PathBuf { @@ -3365,6 +4303,7 @@ mod tests { url: "https://example.com?coder_session_token=xxxxxx".to_string(), }], default_host_id: Some("remote-1".to_string()), + initial_host_choice_completed: false, }; write_desktop_hosts_config_to_path(&path, &config).expect("write config"); @@ -3378,4 +4317,412 @@ mod tests { ); assert_eq!(read_back.default_host_id.as_deref(), Some("remote-1")); } + + #[test] + fn read_hosts_config_defaults_initial_choice_flag_to_false() { + let path = unique_settings_path("desktop-hosts-default-flag"); + std::fs::write(&path, r#"{"desktopHosts":[],"desktopDefaultHostId":null}"#).unwrap(); + + let cfg = read_desktop_hosts_config_from_path(&path); + let _ = fs::remove_file(&path); + assert_eq!(cfg.initial_host_choice_completed, false); + } + + #[test] + fn write_and_read_hosts_config_preserves_initial_choice_flag() { + let path = unique_settings_path("desktop-hosts-preserve-flag"); + let cfg = DesktopHostsConfig { + hosts: vec![], + default_host_id: Some(LOCAL_HOST_ID.to_string()), + initial_host_choice_completed: true, + }; + + write_desktop_hosts_config_to_path(&path, &cfg).unwrap(); + let reread = read_desktop_hosts_config_from_path(&path); + let _ = fs::remove_file(&path); + + assert_eq!(reread.default_host_id.as_deref(), Some(LOCAL_HOST_ID)); + assert!(reread.initial_host_choice_completed); + } + + #[test] + fn omitted_initial_choice_flag_preserves_stored_true() { + let path = unique_settings_path("desktop-hosts-omit-preserves"); + + // Seed: write config with initialHostChoiceCompleted = true + let seed = DesktopHostsConfig { + hosts: vec![DesktopHost { + id: "remote-1".to_string(), + label: "Remote".to_string(), + url: "https://example.com".to_string(), + }], + default_host_id: Some("remote-1".to_string()), + initial_host_choice_completed: true, + }; + write_desktop_hosts_config_to_path(&path, &seed).unwrap(); + + // Call the production merge-and-write path with omitted field + let input = DesktopHostsConfigInput { + hosts: vec![], + default_host_id: Some("local".to_string()), + initial_host_choice_completed: None, + }; + write_desktop_hosts_config_input_to_path(&path, &input).unwrap(); + + let reread = read_desktop_hosts_config_from_path(&path); + let _ = fs::remove_file(&path); + + // The stored true must be preserved, not reset to false + assert!(reread.initial_host_choice_completed); + } + + // --- Task 2: probe validation tests --- + + /// Spawn a tiny HTTP server on a random port that responds with `status_code` + /// and `body`. Returns the base URL (e.g. `http://127.0.0.1:{port}`). + async fn spawn_test_http_server(status_code: u16, body: &str) -> String { + use tokio::net::TcpListener; + let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind test server"); + let port = listener.local_addr().unwrap().port(); + let body_owned = body.to_string(); + + tokio::spawn(async move { + loop { + let (mut stream, _) = tokio::select! { + res = listener.accept() => { res.expect("accept") } + else => break, + }; + use tokio::io::AsyncWriteExt; + let response = format!( + "HTTP/1.1 {status_code} OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body_owned}", + body_owned.len() + ); + let _ = stream.write_all(response.as_bytes()).await; + } + }); + + format!("http://127.0.0.1:{port}") + } + + #[tokio::test] + async fn probe_returns_wrong_service_for_generic_http_200_health() { + let url = spawn_test_http_server(200, r#"{"status":"ok","uptime":42}"#).await; + // Give the server a moment to start listening + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + + let result = probe_host_with_timeout(&url, Duration::from_secs(2)) + .await + .expect("probe should not error"); + assert_eq!(result.status, "wrong-service"); + } + + #[tokio::test] + async fn probe_returns_ok_for_valid_openchamber_health_payload() { + let url = spawn_test_http_server(200, r#"{"openCodeRunning":true}"#).await; + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + + let result = probe_host_with_timeout(&url, Duration::from_secs(2)) + .await + .expect("probe should not error"); + assert_eq!(result.status, "ok"); + } + + #[tokio::test] + async fn probe_returns_auth_for_401_health() { + let url = spawn_test_http_server(401, r#"{"message":"unauthorized"}"#).await; + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + + let result = probe_host_with_timeout(&url, Duration::from_secs(2)) + .await + .expect("probe should not error"); + assert_eq!(result.status, "auth"); + } + + async fn spawn_flaky_openchamber_health_server() -> String { + use tokio::net::TcpListener; + + let listener = TcpListener::bind("127.0.0.1:0").await.expect("bind flaky test server"); + let port = listener.local_addr().unwrap().port(); + let request_count = Arc::new(AtomicUsize::new(0)); + + tokio::spawn({ + let request_count = Arc::clone(&request_count); + async move { + loop { + let (mut stream, _) = tokio::select! { + res = listener.accept() => { res.expect("accept") } + else => break, + }; + + let count = request_count.fetch_add(1, Ordering::SeqCst); + let body = if count == 0 { + r#"{"status":"ok","openCodeRunning":false,"isOpenCodeReady":false}"# + } else { + r#"{"status":"ok","openCodeRunning":true,"isOpenCodeReady":true}"# + }; + + use tokio::io::AsyncWriteExt; + let response = format!( + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ); + let _ = stream.write_all(response.as_bytes()).await; + } + } + }); + + format!("http://127.0.0.1:{port}") + } + + #[tokio::test] + async fn wait_for_local_opencode_ready_retries_until_health_payload_is_ready() { + let url = spawn_flaky_openchamber_health_server().await; + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + + let result = wait_for_local_opencode_ready_with( + &url, + Duration::from_millis(200), + Duration::from_millis(10), + Duration::from_millis(20), + ) + .await + .expect("probe result"); + + assert_eq!(result.status, "ok"); + } + + #[tokio::test] + async fn wait_for_local_opencode_ready_returns_last_probe_when_server_never_becomes_ready() { + let url = spawn_test_http_server( + 200, + r#"{"status":"ok","openCodeRunning":false,"isOpenCodeReady":false}"#, + ) + .await; + tokio::time::sleep(std::time::Duration::from_millis(50)).await; + + let result = wait_for_local_opencode_ready_with( + &url, + Duration::from_millis(120), + Duration::from_millis(10), + Duration::from_millis(20), + ) + .await + .expect("probe result"); + + assert_eq!(result.status, "wrong-service"); + } + + // --- Task 3: boot outcome resolution tests --- + + fn make_config( + hosts: Vec<(&str, &str, &str)>, + default_host_id: Option<&str>, + initial_host_choice_completed: bool, + ) -> DesktopHostsConfig { + DesktopHostsConfig { + hosts: hosts + .into_iter() + .map(|(id, label, url)| DesktopHost { + id: id.to_string(), + label: label.to_string(), + url: url.to_string(), + }) + .collect(), + default_host_id: default_host_id.map(|s| s.to_string()), + initial_host_choice_completed, + } + } + + #[test] + fn resolve_boot_outcome_returns_first_launch_when_no_default_and_choice_not_completed() { + let cfg = make_config(vec![], None, false); + let probe: Option<&HostProbeResult> = None; + let outcome = resolve_boot_outcome(&cfg, probe, true, None); + assert_eq!(outcome.target, None); + assert_eq!(outcome.status, "not-configured"); + } + + #[test] + fn resolve_boot_outcome_returns_recovery_no_default_host_when_choice_completed_but_default_missing() { + let cfg = make_config( + vec![("remote-a", "Remote A", "https://a.test")], + None, + true, + ); + let probe: Option<&HostProbeResult> = None; + let outcome = resolve_boot_outcome(&cfg, probe, true, None); + assert_eq!(outcome.target, None); + assert_eq!(outcome.status, "not-configured"); + } + + #[test] + fn resolve_boot_outcome_returns_recovery_missing_default_host_when_default_id_has_no_matching_host() { + let cfg = make_config(vec![], Some("gone-1"), true); + let probe: Option<&HostProbeResult> = None; + let outcome = resolve_boot_outcome(&cfg, probe, true, None); + assert_eq!(outcome.target, Some("remote".to_string())); + assert_eq!(outcome.status, "missing"); + assert_eq!(outcome.host_id.as_deref(), Some("gone-1")); + } + + #[test] + fn resolve_boot_outcome_returns_main_local_when_default_is_local_and_available() { + let cfg = make_config(vec![], Some("local"), true); + let probe: Option<&HostProbeResult> = None; + let outcome = resolve_boot_outcome(&cfg, probe, true, None); + assert_eq!(outcome.target, Some("local".to_string())); + assert_eq!(outcome.status, "ok"); + } + + #[test] + fn resolve_boot_outcome_returns_recovery_local_unavailable_when_local_is_default_but_unavailable() { + let cfg = make_config(vec![], Some("local"), true); + let probe: Option<&HostProbeResult> = None; + let outcome = resolve_boot_outcome(&cfg, probe, false, None); + assert_eq!(outcome.target, Some("local".to_string())); + assert_eq!(outcome.status, "unreachable"); + } + + #[test] + fn resolve_boot_outcome_returns_main_remote_when_probe_is_ok() { + let cfg = make_config( + vec![("remote-a", "Remote A", "https://a.test")], + Some("remote-a"), + true, + ); + let probe = HostProbeResult { + status: "ok".to_string(), + latency_ms: 10, + }; + let outcome = resolve_boot_outcome(&cfg, Some(&probe), true, None); + assert_eq!(outcome.target, Some("remote".to_string())); + assert_eq!(outcome.status, "ok"); + assert_eq!(outcome.host_id.as_deref(), Some("remote-a")); + assert_eq!(outcome.url.as_deref(), Some("https://a.test")); + } + + #[test] + fn resolve_boot_outcome_returns_main_remote_when_probe_is_auth() { + let cfg = make_config( + vec![("remote-a", "Remote A", "https://a.test")], + Some("remote-a"), + true, + ); + let probe = HostProbeResult { + status: "auth".to_string(), + latency_ms: 10, + }; + let outcome = resolve_boot_outcome(&cfg, Some(&probe), true, None); + assert_eq!(outcome.target, Some("remote".to_string())); + assert_eq!(outcome.status, "ok"); + } + + #[test] + fn resolve_boot_outcome_returns_recovery_remote_unreachable_when_probe_fails() { + let cfg = make_config( + vec![("remote-a", "Remote A", "https://a.test")], + Some("remote-a"), + true, + ); + let probe = HostProbeResult { + status: "unreachable".to_string(), + latency_ms: 2000, + }; + let outcome = resolve_boot_outcome(&cfg, Some(&probe), true, None); + assert_eq!(outcome.target, Some("remote".to_string())); + assert_eq!(outcome.status, "unreachable"); + assert_eq!(outcome.host_id.as_deref(), Some("remote-a")); + } + + #[test] + fn resolve_boot_outcome_returns_recovery_remote_wrong_service_when_probe_says_wrong_service() { + let cfg = make_config( + vec![("remote-a", "Remote A", "https://a.test")], + Some("remote-a"), + true, + ); + let probe = HostProbeResult { + status: "wrong-service".to_string(), + latency_ms: 50, + }; + let outcome = resolve_boot_outcome(&cfg, Some(&probe), true, None); + assert_eq!(outcome.target, Some("remote".to_string())); + assert_eq!(outcome.status, "wrong-service"); + assert_eq!(outcome.host_id.as_deref(), Some("remote-a")); + } + + #[test] + fn resolve_boot_outcome_no_probe_but_remote_default_returns_unreachable() { + // Remote default but no probe result yet — treat as unreachable + // (probe hasn't happened yet, but user has already chosen a remote) + let cfg = make_config( + vec![("remote-a", "Remote A", "https://a.test")], + Some("remote-a"), + false, + ); + let probe: Option<&HostProbeResult> = None; + let outcome = resolve_boot_outcome(&cfg, probe, true, None); + assert_eq!(outcome.target, Some("remote".to_string())); + assert_eq!(outcome.status, "unreachable"); + assert_eq!(outcome.host_id.as_deref(), Some("remote-a")); + } + + // --- Startup failure fallback boot outcome tests --- + + #[test] + fn startup_failure_returns_recovery_local_unavailable_when_default_is_local() { + let cfg = make_config(vec![], Some("local"), true); + let outcome = compute_local_startup_failure_boot_outcome(&cfg); + assert_eq!(outcome.target, Some("local".to_string())); + assert_eq!(outcome.status, "unreachable"); + } + + #[test] + fn startup_failure_returns_first_launch_when_no_default_and_choice_not_completed() { + let cfg = make_config(vec![], None, false); + let outcome = compute_local_startup_failure_boot_outcome(&cfg); + assert_eq!(outcome.target, None); + assert_eq!(outcome.status, "not-configured"); + } + + #[test] + fn startup_failure_returns_recovery_no_default_host_when_choice_completed_but_no_default() { + let cfg = make_config(vec![], None, true); + let outcome = compute_local_startup_failure_boot_outcome(&cfg); + assert_eq!(outcome.target, None); + assert_eq!(outcome.status, "not-configured"); + } + + #[test] + fn startup_failure_never_returns_main_outcome() { + // When the local server fails to start, the fallback outcome must + // never be a "main-*" variant because the startup-failure path + // only injects globals into the already-open startup window — it + // does NOT navigate to a remote URL. A "main-*" outcome would + // gate splash dismissal on initialization and hang. + let cfg = make_config(vec![], None, false); + let outcome = compute_local_startup_failure_boot_outcome(&cfg); + assert!( + outcome.status != "ok", + "startup failure fallback must not return main-* outcome, got: {:?}", + outcome + ); + } + + #[test] + fn startup_failure_init_script_contains_boot_outcome_json() { + let cfg = make_config(vec![], Some("local"), true); + let outcome = compute_local_startup_failure_boot_outcome(&cfg); + let script = build_startup_failure_init_script(&outcome); + // The script must contain the serialized boot outcome JSON. + assert!( + script.contains(r#""target":"local""#) && script.contains(r#""status":"unreachable""#), + "init script should embed the structured boot outcome" + ); + // It must also set __OPENCHAMBER_DESKTOP_BOOT_OUTCOME__ + assert!( + script.contains("__OPENCHAMBER_DESKTOP_BOOT_OUTCOME__"), + "init script must set __OPENCHAMBER_DESKTOP_BOOT_OUTCOME__" + ); + } } diff --git a/src/packages/desktop/src-tauri/src/remote_ssh.rs b/src/packages/desktop/src-tauri/src/remote_ssh.rs index 141519a..af37c8d 100644 --- a/src/packages/desktop/src-tauri/src/remote_ssh.rs +++ b/src/packages/desktop/src-tauri/src/remote_ssh.rs @@ -22,6 +22,12 @@ const DEFAULT_READY_TIMEOUT_SEC: u64 = 30; const DEFAULT_RECONNECT_MAX_ATTEMPTS: u32 = 5; const MAX_LOG_LINES_PER_INSTANCE: usize = 1200; +/// Monitor starts with fast polling and relaxes to steady-state after stabilization. +const MONITOR_INITIAL_POLL_SECS: u64 = 2; +const MONITOR_STEADY_POLL_SECS: u64 = 10; +/// Number of healthy ticks before switching from initial to steady-state polling. +const MONITOR_STABILIZE_TICKS: u32 = 5; + #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DesktopSshInstancesConfig { @@ -1027,6 +1033,7 @@ fn wait_for_master_ready( master: &mut Child, ) -> Result<()> { let deadline = std::time::Instant::now() + Duration::from_secs(timeout_sec as u64); + let mut poll_ms: u64 = 250; while std::time::Instant::now() < deadline { let args = vec![ "-o".to_string(), @@ -1056,7 +1063,8 @@ fn wait_for_master_ready( return Err(anyhow!(stderr.trim().to_string())); } - std::thread::sleep(Duration::from_millis(250)); + std::thread::sleep(Duration::from_millis(poll_ms)); + poll_ms = (poll_ms * 2).min(2000); } Err(anyhow!("SSH ControlMaster connection timed out")) @@ -1552,19 +1560,34 @@ fn is_local_tunnel_reachable(local_port: u16) -> bool { } fn wait_local_forward_ready(local_port: u16) -> Result<()> { - let client = reqwest::blocking::Client::builder() - .timeout(Duration::from_millis(1000)) - .no_proxy() - .build()?; let deadline = std::time::Instant::now() + Duration::from_secs(DEFAULT_READY_TIMEOUT_SEC); - let target = format!("http://127.0.0.1:{local_port}/health"); + let addr: std::net::SocketAddr = format!("127.0.0.1:{local_port}").parse()?; + let mut poll_ms: u64 = 250; while std::time::Instant::now() < deadline { - if let Ok(response) = client.get(&target).send() { - if response.status().is_success() || response.status().as_u16() == 401 { - return Ok(()); + if let Ok(mut stream) = + TcpStream::connect_timeout(&addr, Duration::from_millis(1000)) + { + use std::io::{Read as IoRead, Write}; + let _ = stream.set_read_timeout(Some(Duration::from_millis(1000))); + let _ = stream.set_write_timeout(Some(Duration::from_millis(1000))); + let request = format!( + "GET /health HTTP/1.1\r\nHost: 127.0.0.1:{local_port}\r\nConnection: close\r\n\r\n" + ); + if stream.write_all(request.as_bytes()).is_ok() { + let mut buf = [0u8; 32]; + if let Ok(n) = stream.read(&mut buf) { + let head = std::str::from_utf8(&buf[..n]).unwrap_or(""); + // Match "HTTP/1.x 2xx" or "HTTP/1.x 401" + if head.starts_with("HTTP/1.") + && (head.contains(" 2") || head.contains(" 401")) + { + return Ok(()); + } + } } } - std::thread::sleep(Duration::from_millis(250)); + std::thread::sleep(Duration::from_millis(poll_ms)); + poll_ms = (poll_ms * 2).min(2000); } Err(anyhow!( "Timed out waiting for forwarded OpenChamber health" @@ -2353,8 +2376,14 @@ impl DesktopSshManagerInner { let inner = Arc::clone(self); let id_for_task = id.clone(); let handle = tauri::async_runtime::spawn(async move { + let mut healthy_ticks: u32 = 0; loop { - tokio::time::sleep(Duration::from_secs(2)).await; + let poll_secs = if healthy_ticks >= MONITOR_STABILIZE_TICKS { + MONITOR_STEADY_POLL_SECS + } else { + MONITOR_INITIAL_POLL_SECS + }; + tokio::time::sleep(Duration::from_secs(poll_secs)).await; let mut dropped_reason: Option = None; let mut detached_notice: Option = None; @@ -2424,18 +2453,21 @@ impl DesktopSshManagerInner { ); } } else if session.master_detached { - if !is_control_master_alive(&session.parsed, &session.control_path) { - if is_local_tunnel_reachable(session.local_port) { - if detached_notice.is_none() { - detached_notice = Some( - "SSH ControlMaster check failed but local tunnel is still reachable" - .to_string(), - ); - } - } else { - dropped_reason = - Some("SSH ControlMaster is not reachable".to_string()); - } + // Fast path: check local tunnel first (cheap TCP probe) + // before spawning an SSH subprocess for control master check. + if is_local_tunnel_reachable(session.local_port) { + // Tunnel is alive — skip the expensive SSH check entirely. + } else if !is_control_master_alive( + &session.parsed, + &session.control_path, + ) { + dropped_reason = + Some("SSH ControlMaster is not reachable".to_string()); + } else { + detached_notice = Some( + "Local tunnel unreachable but ControlMaster is alive" + .to_string(), + ); } } else if let Some(status) = session.master.try_wait().ok().flatten() { if status.success() @@ -2471,6 +2503,7 @@ impl DesktopSshManagerInner { } if dropped_reason.is_none() { + healthy_ticks = healthy_ticks.saturating_add(1); continue; } diff --git a/src/packages/desktop/src-tauri/tauri.conf.json b/src/packages/desktop/src-tauri/tauri.conf.json index 853ba53..2a57718 100644 --- a/src/packages/desktop/src-tauri/tauri.conf.json +++ b/src/packages/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/schema.json", "productName": "OpenChamber", - "version": "1.9.1", + "version": "1.9.9", "identifier": "ai.opencode.openchamber", "build": { "beforeDevCommand": "node ./scripts/dev-web-server.mjs", @@ -15,7 +15,6 @@ "label": "main", "create": false, "title": "OpenChamber", - "transparent": true, "width": 1280, "height": 800, "resizable": true, @@ -28,8 +27,7 @@ "y": 26 }, "dragDropEnabled": false, - "visible": false, - "backgroundThrottling": "disabled" + "visible": false } ], "security": { diff --git a/src/packages/docs/content/docs/reverse-proxy.mdx b/src/packages/docs/content/docs/reverse-proxy.mdx new file mode 100644 index 0000000..4085690 --- /dev/null +++ b/src/packages/docs/content/docs/reverse-proxy.mdx @@ -0,0 +1,347 @@ +--- +title: Reverse Proxy +description: Configure OpenChamber correctly behind Nginx, Nginx Proxy Manager, or another reverse proxy. +--- + +# Reverse Proxy + +Use this page if you run OpenChamber behind Nginx, Nginx Proxy Manager, Caddy, Cloudflare, or another reverse proxy. + +## Before you proxy it + +1. Confirm OpenChamber works directly first. +2. Open `http://:3000` or your custom port from the same network. +3. Only add the reverse proxy after the direct connection works. + +## What the proxy must support + +- WebSockets for live message transport: + - `/api/event/ws` + - `/api/global/event/ws` + - `/api/terminal/ws` +- SSE without buffering: + - `/api/event` + - `/api/global/event` + - `/api/notifications/stream` + - `/api/openchamber/events` + - `/api/terminal/:sessionId/stream` +- Large request bodies for attachments and file operations +- Long-lived read timeouts for live streams and terminal sessions + +## Rules that matter + +- Enable WebSocket proxying. +- Disable buffering on SSE routes. +- Disable gzip on the proxy if OpenChamber is already compressing responses. +- Keep compression enabled in only one layer. +- Forward normal proxy headers such as `Host`, `X-Forwarded-For`, and `X-Forwarded-Proto`. +- Increase body size limits if users upload files. + +## Quick checklist + +- OpenChamber reachable directly on LAN +- WebSockets enabled in the proxy +- SSE routes have buffering off +- `gzip off` on the proxy host, or proxy compression disabled another way +- `client_max_body_size` large enough for attachments +- `proxy_read_timeout` long enough for streams + +## Example: Nginx + +
+Show example config + +```nginx +client_max_body_size 50M; +client_body_buffer_size 50M; +proxy_request_buffering off; + +proxy_http_version 1.1; +proxy_set_header Connection ""; +proxy_set_header Host $host; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; +proxy_set_header X-Forwarded-Host $host; + +gzip off; + +location = /api/terminal/ws { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} + +location = /api/global/event/ws { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} + +location = /api/event/ws { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} + +location ~ ^/api/(event|global/event|notifications/stream|openchamber/events)$ { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} + +location ~ ^/api/terminal/.+/stream$ { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} + +location /api { + proxy_pass http://127.0.0.1:3000; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; +} + +location / { + proxy_pass http://127.0.0.1:3000; +} +``` + +
+ +## Example: Nginx Proxy Manager + +
+Show Advanced tab example + +```nginx +client_max_body_size 50M; +client_body_buffer_size 50M; +proxy_request_buffering off; + +proxy_http_version 1.1; +proxy_set_header Connection ""; +proxy_set_header Host $host; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; +proxy_set_header X-Forwarded-Host $host; + +gzip off; + +location = /api/terminal/ws { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location = /api/global/event/ws { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location = /api/event/ws { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location = /api/event { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location = /api/global/event { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location = /api/notifications/stream { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location = /api/openchamber/events { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location ~ ^/api/terminal/.+/stream$ { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Accept "text/event-stream"; + proxy_set_header Cache-Control "no-cache"; + proxy_buffering off; + proxy_cache off; + gzip off; + add_header X-Accel-Buffering "no" always; + add_header Cache-Control "no-cache, no-transform" always; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location /api { + proxy_pass http://127.0.0.1:3000; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + proxy_connect_timeout 30s; +} + +location / { + proxy_pass http://127.0.0.1:3000; +} +``` + +
+ +Also enable `Websockets Support` in Nginx Proxy Manager for this host. + +## Common failure signs + +### Page loads, but sending messages fails + +- WebSockets are not enabled in the proxy +- `/api/event/ws` or `/api/global/event/ws` is not passing through correctly + +### Notifications or live status do not update + +- one of the SSE routes is buffered or cached +- `X-Accel-Buffering "no"` is missing + +### File uploads fail + +- `client_max_body_size` is too small + +### Everything works locally, but breaks only behind the proxy + +- the proxy is compressing and buffering live traffic +- the proxy is missing WebSocket support + +## Example: Caddy + +
+Show example config + +```caddy +reverse_proxy 127.0.0.1:3000 { + # WebSocket support is automatic in Caddy + + # Flush SSE responses immediately + flush_interval -1 + + # Pass through Host and proxy headers + header_up Host {host} + header_up X-Real-IP {remote_host} + header_up X-Forwarded-For {remote_host} + header_up X-Forwarded-Proto {scheme} + + # Increase timeouts for long-lived streams + transport http { + read_timeout 3600s + write_timeout 3600s + } +} +``` + +
+ +Caddy handles WebSocket upgrades automatically — no extra configuration needed. The `flush_interval -1` directive ensures SSE chunks are forwarded immediately without buffering. + +## CDN and double-compression warning + +If you place a CDN (such as Cloudflare) in front of your reverse proxy, be aware of double compression: + +- OpenChamber compresses HTTP responses with gzip (threshold 1 KB). +- Cloudflare and other CDNs also compress responses by default. +- This can cause double-compressed responses or incorrect `Content-Encoding` headers. + +To avoid this, disable compression at **one** layer: + +- **Cloudflare:** Rules → Compression → disable (or use "Passthrough" mode). +- **Nginx:** `gzip off` (already shown in the examples above). +- **Caddy:** Caddy does not re-compress by default if the upstream already sends compressed content. + +SSE streaming routes are excluded from compression by OpenChamber, but the CDN may still buffer them. Check your CDN documentation for how to disable buffering on SSE paths. + +## Related + +- [Tunnels](/tunnels/) +- [Troubleshooting](/troubleshooting/) diff --git a/src/packages/docs/sidebar.config.json b/src/packages/docs/sidebar.config.json index 2e3d8c4..1f1c4ae 100644 --- a/src/packages/docs/sidebar.config.json +++ b/src/packages/docs/sidebar.config.json @@ -18,6 +18,7 @@ { "label": "Help", "items": [ + { "label": "Reverse Proxy", "link": "/reverse-proxy/" }, { "label": "Troubleshooting", "link": "/troubleshooting/" } ] } diff --git a/src/packages/electron/.gitignore b/src/packages/electron/.gitignore new file mode 100644 index 0000000..2f8dd8d --- /dev/null +++ b/src/packages/electron/.gitignore @@ -0,0 +1,13 @@ +# Dependencies +node_modules/ + +# Electron build output +dist/ +dist-bundle/ + +# Generated packaging resources +resources/web-dist/ +resources/sidecar/ + +# OS-specific +.DS_Store diff --git a/src/packages/electron/main.mjs b/src/packages/electron/main.mjs new file mode 100644 index 0000000..d2b85e0 --- /dev/null +++ b/src/packages/electron/main.mjs @@ -0,0 +1,2372 @@ +import { app, BrowserWindow, dialog, ipcMain, Menu, nativeTheme, Notification, session, shell } from 'electron'; +import contextMenu from 'electron-context-menu'; +import log from 'electron-log/main.js'; +import dgram from 'node:dgram'; +import fs from 'node:fs'; +import fsp from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { execFile, spawn, spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { promisify } from 'node:util'; +import updaterPkg from 'electron-updater'; +import { ElectronSshManager } from './ssh-manager.mjs'; + +const execFileAsync = promisify(execFile); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const isDev = process.env.OPENCHAMBER_ELECTRON_DEV === '1' || !app.isPackaged; + +const DEEP_LINK_PROTOCOL = 'openchamber'; +const APP_USER_MODEL_ID = 'dev.openchamber.desktop'; + +if (!app.requestSingleInstanceLock()) { + app.exit(0); + process.exit(0); +} + +// Set the product name early so electron-log derives its log directory as +// ~/Library/Logs/OpenChamber/ (not ~/Library/Logs/@openchamber/electron/). +app.setName('OpenChamber'); +app.setAppUserModelId(APP_USER_MODEL_ID); +app.commandLine.appendSwitch('proxy-bypass-list', '<-loopback>'); + +try { + process.chdir(os.homedir()); +} catch { +} + +log.initialize(); +log.transports.file.maxSize = 5 * 1024 * 1024; +log.transports.file.level = 'info'; +log.transports.console.level = isDev ? 'debug' : 'warn'; + +// The in-process web server runs in this same Node process and uses plain +// `console.log/warn/error`. Without piping console through electron-log, +// that output never lands in ~/Library/Logs/OpenChamber/main.log and we +// can't diagnose issues (e.g. OpenCode lifecycle, SSE disconnects) after +// the fact. Route all console calls through electron-log so server-side +// diagnostics are persisted. +Object.assign(console, log.functions); + +const LOG_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000; +try { + const logPath = log.transports.file.getFile().path; + const logDir = path.dirname(logPath); + const cutoff = Date.now() - LOG_MAX_AGE_MS; + for (const entry of fs.readdirSync(logDir)) { + const candidate = path.join(logDir, entry); + try { + const info = fs.statSync(candidate); + if (info.isFile() && info.mtimeMs < cutoff) { + fs.unlinkSync(candidate); + } + } catch { + } + } +} catch { +} + +try { + if (!app.isDefaultProtocolClient(DEEP_LINK_PROTOCOL)) { + app.setAsDefaultProtocolClient(DEEP_LINK_PROTOCOL); + } +} catch (error) { + // log.* not yet initialized at this point; fall back to console. + console.warn('[electron] failed to register deep-link protocol:', error); +} + +const readAppMetadata = () => { + const candidates = [ + path.join(__dirname, 'package.json'), + path.join(__dirname, '..', 'package.json'), + path.join(app.getAppPath?.() || '', 'package.json'), + ].filter(Boolean); + for (const candidate of candidates) { + try { + const raw = fs.readFileSync(candidate, 'utf8'); + const parsed = JSON.parse(raw); + if (parsed?.name === '@openchamber/electron' && typeof parsed.version === 'string') { + return { name: parsed.name, version: parsed.version }; + } + } catch { + } + } + return { name: '@openchamber/electron', version: app.getVersion() }; +}; + +const APP_METADATA = readAppMetadata(); +const APP_VERSION = APP_METADATA.version; + +const DEFAULT_DESKTOP_PORT = 57123; +const MIN_WINDOW_WIDTH = 800; +const MIN_WINDOW_HEIGHT = 520; +const MIN_RESTORE_WINDOW_WIDTH = 900; +const MIN_RESTORE_WINDOW_HEIGHT = 560; +const LOCAL_HOST_ID = 'local'; +const ENV_OVERRIDE_HOST_ID = '__env'; +const CHANGELOG_URL = 'https://raw.githubusercontent.com/btriapitsyn/openchamber/main/CHANGELOG.md'; +const UPDATE_METADATA_URL = 'https://github.com/btriapitsyn/openchamber/releases/latest/download/latest.json'; +const GITHUB_BUG_REPORT_URL = 'https://github.com/btriapitsyn/openchamber/issues/new?template=bug_report.yml'; +const GITHUB_FEATURE_REQUEST_URL = 'https://github.com/btriapitsyn/openchamber/issues/new?template=feature_request.yml'; +const DISCORD_INVITE_URL = 'https://discord.gg/ZYRSdnwwKA'; +const INSTALLED_APPS_CACHE_TTL_SECS = 60 * 60 * 24; +const INSTALLED_APPS_CACHE_FILE = 'discovered-apps.json'; + +const { autoUpdater } = updaterPkg; + +const state = { + serverHandle: null, + sidecarUrl: null, + localOrigin: null, + bootOutcome: null, + initScript: null, + mainWindow: null, + quitRequested: false, + quitConfirmed: false, + quitConfirmationPending: false, + installingUpdate: false, + quitRiskPollerStarted: false, + pendingUpdate: null, + unreachableHosts: new Set(), + windowCounter: 1, + focusedWindowIds: new Set(), + windowGeometryRevisions: new Map(), + sshStatuses: new Map(), + sshLogs: new Map(), +}; + +const QUIT_RISK_POLL_INTERVAL_MS = 5_000; +const quitRisk = { + hasActiveTunnel: false, + hasRunningScheduledTasks: false, + hasEnabledScheduledTasks: false, + runningScheduledTasksCount: 0, + enabledScheduledTasksCount: 0, +}; + +const shouldRequireQuitConfirmation = () => + quitRisk.hasActiveTunnel + || quitRisk.hasRunningScheduledTasks + || quitRisk.hasEnabledScheduledTasks; + +const quitConfirmationMessage = () => { + const reasons = []; + if (quitRisk.hasActiveTunnel) { + reasons.push('an active tunnel'); + } + if (quitRisk.runningScheduledTasksCount > 0) { + reasons.push(`${quitRisk.runningScheduledTasksCount} running scheduled task${quitRisk.runningScheduledTasksCount === 1 ? '' : 's'}`); + } + if (quitRisk.enabledScheduledTasksCount > 0) { + reasons.push(`${quitRisk.enabledScheduledTasksCount} enabled scheduled task${quitRisk.enabledScheduledTasksCount === 1 ? '' : 's'}`); + } + if (reasons.length === 0) { + return 'Background processes (sidecar, SSH sessions) will be stopped.'; + } + return `OpenChamber detected ${reasons.join(', ')}. Quitting now will stop sidecar/background processes and may interrupt pending work.`; +}; + +const prepareForQuit = ({ installingUpdate = false } = {}) => { + state.quitRequested = true; + state.quitConfirmed = true; + state.installingUpdate = installingUpdate; + state.quitConfirmationPending = false; + + if (state.mainWindow && !state.mainWindow.isDestroyed()) { + try { + debounceWindowStatePersist(state.mainWindow, true); + } catch { + } + } + + if (!installingUpdate) { + try { + killSidecar(); + } catch { + } + void sshManager.shutdownAll().catch(() => {}); + } +}; + +const performConfirmedQuit = () => { + if (state.quitConfirmed) return; + prepareForQuit(); + + // Safety net: force-exit if normal quit sequence stalls (e.g. background + // handles in electron-updater / fetch refs) after a short grace period. + const safety = setTimeout(() => { + app.exit(0); + }, 1500); + if (typeof safety?.unref === 'function') safety.unref(); + + app.quit(); +}; + +const requestQuitWithConfirmation = async () => { + if (!shouldRequireQuitConfirmation()) { + performConfirmedQuit(); + return; + } + + if (state.quitConfirmationPending) { + return; + } + state.quitConfirmationPending = true; + + const windows = BrowserWindow.getAllWindows().filter((window) => !window.isDestroyed()); + const visible = windows.find((window) => window.isVisible()); + if (!visible) { + const hidden = windows.find((window) => !window.isVisible()); + if (hidden) { + hidden.show(); + hidden.focus(); + } + } + + try { + const result = await dialog.showMessageBox({ + type: 'warning', + title: 'Quit OpenChamber?', + message: 'Quit OpenChamber?', + detail: quitConfirmationMessage(), + buttons: ['Quit', 'Cancel'], + defaultId: 1, + cancelId: 1, + }); + state.quitConfirmationPending = false; + if (result.response === 0) { + performConfirmedQuit(); + } + } catch (error) { + state.quitConfirmationPending = false; + log.warn('[electron] quit confirmation dialog failed:', error); + } +}; + +const refreshQuitRiskFlags = async () => { + const base = typeof state.sidecarUrl === 'string' ? state.sidecarUrl.trim().replace(/\/$/, '') : ''; + if (!base) return; + + const scheduledUrl = `${base}/api/openchamber/scheduled-tasks/status`; + const tunnelUrl = `${base}/api/openchamber/tunnel/status`; + + const fetchJson = async (url) => { + try { + const response = await fetch(url, { signal: AbortSignal.timeout(2_000) }); + if (!response.ok) return null; + return await response.json(); + } catch { + return null; + } + }; + + const [scheduled, tunnel] = await Promise.all([fetchJson(scheduledUrl), fetchJson(tunnelUrl)]); + + if (scheduled && typeof scheduled === 'object') { + const enabledCount = Number(scheduled.enabledScheduledTasksCount ?? 0); + const runningCount = Number(scheduled.runningScheduledTasksCount ?? 0); + quitRisk.enabledScheduledTasksCount = Number.isFinite(enabledCount) ? enabledCount : 0; + quitRisk.runningScheduledTasksCount = Number.isFinite(runningCount) ? runningCount : 0; + quitRisk.hasEnabledScheduledTasks = Boolean(scheduled.hasEnabledScheduledTasks) || quitRisk.enabledScheduledTasksCount > 0; + quitRisk.hasRunningScheduledTasks = Boolean(scheduled.hasRunningScheduledTasks) || quitRisk.runningScheduledTasksCount > 0; + } + + if (tunnel && typeof tunnel === 'object') { + quitRisk.hasActiveTunnel = Boolean(tunnel.active); + } +}; + +const startQuitRiskPoller = () => { + if (process.platform !== 'darwin') return; + if (state.quitRiskPollerStarted) return; + state.quitRiskPollerStarted = true; + + const loop = async () => { + while (!state.quitConfirmed && !state.quitRequested) { + await refreshQuitRiskFlags(); + if (state.quitConfirmed || state.quitRequested) break; + await new Promise((resolve) => { + const timer = setTimeout(resolve, QUIT_RISK_POLL_INTERVAL_MS); + if (typeof timer?.unref === 'function') timer.unref(); + }); + } + }; + void loop(); +}; + +const settingsFilePath = () => { + if (typeof process.env.OPENCHAMBER_DATA_DIR === 'string' && process.env.OPENCHAMBER_DATA_DIR.trim()) { + return path.join(process.env.OPENCHAMBER_DATA_DIR.trim(), 'settings.json'); + } + return path.join(os.homedir(), '.config', 'openchamber', 'settings.json'); +}; + +const sshManager = new ElectronSshManager({ + settingsFilePath: settingsFilePath(), + appVersion: APP_VERSION, + emit: (event, detail) => emitToAllWindows(event, detail), +}); + +const readJsonFile = (filePath) => { + try { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); + } catch (error) { + if (error && error.code === 'ENOENT') return {}; + // Parse errors can happen if a concurrent writer just truncated the file + // and hasn't finished writing yet. Log loudly so we notice, then return + // {} as before. Writes are atomic (tmp + rename) so this race is rare. + log.warn?.('[electron] failed to read JSON file', filePath, error); + return {}; + } +}; + +const writeJsonFile = async (filePath, data) => { + await fsp.mkdir(path.dirname(filePath), { recursive: true }); + // Atomic: write to a temp file then rename. Readers never see a partial + // JSON file that could parse-error and get coerced to {}. + const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + await fsp.writeFile(tmp, JSON.stringify(data, null, 2)); + await fsp.rename(tmp, filePath); +}; + +const readSettingsRoot = () => { + const root = readJsonFile(settingsFilePath()); + return root && typeof root === 'object' && !Array.isArray(root) ? root : {}; +}; + +// Serializes read-modify-write of the settings file within this process. +// Multiple call sites (spawnLocalServer, writeDesktopHostsConfig, theme +// preference saves, ssh manager imports, etc.) would otherwise have their +// RMW pairs interleave across awaits, letting one writer's stale copy +// overwrite another writer's just-persisted changes. +let settingsMutationChain = Promise.resolve(); +const mutateSettingsRoot = (mutator) => { + const next = settingsMutationChain.then(async () => { + const current = readSettingsRoot(); + const result = await mutator(current); + const nextRoot = result ?? current; + await writeJsonFile(settingsFilePath(), nextRoot); + }); + // Keep the chain alive even if one mutator throws. + settingsMutationChain = next.catch(() => {}); + return next; +}; + +const writeSettingsRoot = async (root) => writeJsonFile(settingsFilePath(), root); + +const normalizeHostUrl = (raw) => { + const trimmed = typeof raw === 'string' ? raw.trim() : ''; + if (!trimmed) return null; + try { + const parsed = new URL(trimmed); + if (!['http:', 'https:'].includes(parsed.protocol)) return null; + parsed.hash = ''; + return parsed.toString(); + } catch { + return null; + } +}; + +const sanitizeHostUrlForStorage = (raw) => normalizeHostUrl(raw); + +const readDesktopHostsConfig = () => { + const root = readSettingsRoot(); + const hostsRaw = Array.isArray(root.desktopHosts) ? root.desktopHosts : []; + const hosts = hostsRaw + .map((entry) => { + const id = typeof entry?.id === 'string' ? entry.id.trim() : ''; + const url = sanitizeHostUrlForStorage(entry?.url); + if (!id || id === LOCAL_HOST_ID || !url) return null; + const label = typeof entry?.label === 'string' && entry.label.trim() ? entry.label.trim() : url; + return { id, label, url }; + }) + .filter(Boolean); + + return { + hosts, + defaultHostId: typeof root.desktopDefaultHostId === 'string' && root.desktopDefaultHostId.trim() + ? root.desktopDefaultHostId.trim() + : null, + initialHostChoiceCompleted: root.desktopInitialHostChoiceCompleted === true, + }; +}; + +const writeDesktopHostsConfig = async (config) => { + await mutateSettingsRoot((root) => { + root.desktopHosts = Array.isArray(config?.hosts) + ? config.hosts + .map((entry) => { + const id = typeof entry?.id === 'string' ? entry.id.trim() : ''; + const url = sanitizeHostUrlForStorage(entry?.url); + if (!id || id === LOCAL_HOST_ID || !url) return null; + return { + id, + label: typeof entry?.label === 'string' && entry.label.trim() ? entry.label.trim() : url, + url, + }; + }) + .filter(Boolean) + : []; + root.desktopDefaultHostId = typeof config?.defaultHostId === 'string' && config.defaultHostId.trim() + ? config.defaultHostId.trim() + : null; + if (typeof config?.initialHostChoiceCompleted === 'boolean') { + root.desktopInitialHostChoiceCompleted = config.initialHostChoiceCompleted; + } + }); +}; + +const readWindowState = () => { + const stateValue = readSettingsRoot().desktopWindowState; + return stateValue && typeof stateValue === 'object' ? stateValue : null; +}; + +const writeWindowState = async (browserWindow) => { + if (!browserWindow || browserWindow.isDestroyed()) return; + if (!state.mainWindow || browserWindow.id !== state.mainWindow.id) return; + + const bounds = browserWindow.getBounds(); + await mutateSettingsRoot((root) => { + root.desktopWindowState = { + x: bounds.x, + y: bounds.y, + width: Math.max(bounds.width, MIN_WINDOW_WIDTH), + height: Math.max(bounds.height, MIN_WINDOW_HEIGHT), + maximized: browserWindow.isMaximized(), + fullscreen: browserWindow.isFullScreen(), + }; + }); +}; + +const debounceWindowStatePersist = (browserWindow, immediate = false) => { + if (!browserWindow || browserWindow.isDestroyed()) return; + const key = String(browserWindow.id); + const revision = (state.windowGeometryRevisions.get(key) || 0) + 1; + state.windowGeometryRevisions.set(key, revision); + + const persist = async () => { + if (state.windowGeometryRevisions.get(key) !== revision) return; + await writeWindowState(browserWindow); + }; + + if (immediate) { + void persist(); + return; + } + + setTimeout(() => { + void persist(); + }, 300); +}; + +const buildHealthUrl = (url) => { + try { + const parsed = new URL(url); + parsed.pathname = `${parsed.pathname.replace(/\/$/, '') || ''}/health`; + return parsed.toString(); + } catch { + return null; + } +}; + +const probeHostWithTimeout = async (url, timeoutMs) => { + const healthUrl = buildHealthUrl(url); + if (!healthUrl) { + throw new Error('Invalid URL'); + } + + const started = Date.now(); + try { + const response = await fetch(healthUrl, { signal: AbortSignal.timeout(timeoutMs) }); + const status = response.status; + return { + status: status >= 200 && status < 300 ? 'ok' : (status === 401 || status === 403 ? 'auth' : 'unreachable'), + latencyMs: Date.now() - started, + }; + } catch { + return { status: 'unreachable', latencyMs: Date.now() - started }; + } +}; + +const waitForHealth = async (url, timeoutMs = 20_000, initialPollMs = 250, maxPollMs = 2000) => { + const deadline = Date.now() + timeoutMs; + let pollMs = initialPollMs; + while (Date.now() < deadline) { + try { + const response = await fetch(buildHealthUrl(url), { signal: AbortSignal.timeout(Math.min(pollMs * 4, 1500)) }); + if (response.ok) { + return true; + } + } catch { + } + await new Promise((resolve) => setTimeout(resolve, pollMs)); + pollMs = Math.min(pollMs * 2, maxPollMs); + } + return false; +}; + +const pickUnusedPort = async () => { + const net = await import('node:net'); + return await new Promise((resolve, reject) => { + const server = net.createServer(); + server.listen(0, '127.0.0.1', () => { + const address = server.address(); + const port = typeof address === 'object' && address ? address.port : 0; + server.close(() => resolve(port)); + }); + server.on('error', reject); + }); +}; + +const isPortFree = async (port) => { + if (!Number.isFinite(port) || port <= 0) return false; + const net = await import('node:net'); + return await new Promise((resolve) => { + const test = net.createServer(); + const done = (value) => { + try { test.close(); } catch {} + resolve(value); + }; + test.once('error', () => done(false)); + test.listen(port, '127.0.0.1', () => done(true)); + }); +}; + +// Return the LAN IPv4 of the interface that routes to the public internet. +// UDP "connect" is a kernel-side route lookup — no packet actually goes out — +// and it picks the same interface as a real outbound connection, which is what +// a phone on the same Wi-Fi needs to reach us. Falls back to scanning +// os.networkInterfaces() if the socket trick fails (e.g. no default route). +const detectLanIPv4Address = async () => { + const ip = await new Promise((resolve) => { + const socket = dgram.createSocket('udp4'); + const finish = (value) => { + try { socket.close(); } catch {} + resolve(value); + }; + socket.once('error', () => finish(null)); + try { + socket.connect(80, '8.8.8.8', (error) => { + if (error) return finish(null); + try { + const addr = socket.address(); + finish(addr && typeof addr.address === 'string' ? addr.address : null); + } catch { + finish(null); + } + }); + } catch { + finish(null); + } + }); + if (ip && ip !== '0.0.0.0' && !ip.startsWith('127.')) return ip; + + for (const entries of Object.values(os.networkInterfaces() || {})) { + for (const entry of entries || []) { + if (entry.family === 'IPv4' && !entry.internal && entry.address) { + return entry.address; + } + } + } + return null; +}; + +const buildLocalUrl = (port) => `http://127.0.0.1:${port}`; + +const resourceRoot = () => isDev ? path.join(__dirname, 'resources') : process.resourcesPath; +const resolveWebDistDir = () => path.join(resourceRoot(), 'web-dist'); + +const normalizeNotificationInput = (raw) => { + if (!raw || typeof raw !== 'object') return {}; + // UI IPC path wraps in { payload: {...} }; sidecar stdout path is flat. + if (raw.payload && typeof raw.payload === 'object') { + return { ...raw, ...raw.payload }; + } + return raw; +}; + +const isAnyWindowFocused = () => + BrowserWindow.getAllWindows().some( + (window) => !window.isDestroyed() && window.isFocused(), + ); + +const focusForegroundWindow = () => { + const windows = BrowserWindow.getAllWindows().filter((window) => !window.isDestroyed()); + if (windows.length === 0) return; + const target = state.mainWindow && !state.mainWindow.isDestroyed() + ? state.mainWindow + : windows.find((window) => window.isVisible()) || windows[0]; + // macOS: bring the app to foreground FIRST. When the window is minimized + // to the Dock or hidden via Cmd+H, the app is in the background, and + // subsequent window.show/restore/focus calls won't pull it forward + // unless app.focus runs first. + if (process.platform === 'darwin') app.focus({ steal: true }); + if (target.isMinimized()) target.restore(); + target.show(); + target.focus(); + if (typeof target.moveTop === 'function') target.moveTop(); +}; + +// Keep references to live notifications so they aren't garbage-collected +// before the OS fires click/close. On macOS, losing the JS reference causes +// click events to silently stop firing after ~1 min. +// See https://blog.bloomca.me/2025/02/22/electron-mac-notifications +const activeNotifications = new Set(); + +const maybeShowNativeNotification = (rawInput) => { + const payload = normalizeNotificationInput(rawInput); + const requireHidden = Boolean(payload.requireHidden ?? payload.require_hidden); + + if (requireHidden && isAnyWindowFocused()) { + return; + } + + if (!Notification.isSupported()) { + return; + } + + const title = typeof payload.title === 'string' && payload.title.trim() + ? payload.title.trim() + : 'OpenChamber'; + const body = typeof payload.body === 'string' ? payload.body : ''; + const sessionId = typeof payload.sessionId === 'string' && payload.sessionId.trim() + ? payload.sessionId.trim() + : null; + + const notification = new Notification({ + title, + body, + silent: false, + ...(process.platform === 'darwin' ? { sound: 'Glass' } : {}), + }); + + activeNotifications.add(notification); + const release = () => { activeNotifications.delete(notification); }; + + notification.on('click', () => { + focusForegroundWindow(); + if (sessionId) { + emitToAllWindows('openchamber:open-session', { sessionId }); + } + release(); + }); + notification.on('close', release); + notification.on('failed', release); + + notification.show(); +}; + +const mapUpdaterProgressEvent = (payload) => ({ + event: payload.event, + data: payload.data, +}); + +const SHELL_ENV_TIMEOUT_MS = 5_000; +let cachedShellEnv = null; +let shellEnvProbed = false; + +const isNushell = (shell) => { + const name = path.basename(shell).toLowerCase(); + return name === 'nu' || name === 'nu.exe'; +}; + +const parseShellEnv = (buf) => { + const result = {}; + for (const line of buf.toString('utf8').split('\0')) { + if (!line) continue; + const idx = line.indexOf('='); + if (idx <= 0) continue; + result[line.slice(0, idx)] = line.slice(idx + 1); + } + return result; +}; + +const probeShellEnv = (shell, mode) => { + const result = spawnSync(shell, [mode, '-c', 'env -0'], { + stdio: ['ignore', 'pipe', 'ignore'], + timeout: SHELL_ENV_TIMEOUT_MS, + windowsHide: true, + }); + if (result.error || result.status !== 0) return null; + const env = parseShellEnv(result.stdout); + return Object.keys(env).length > 0 ? env : null; +}; + +// Finder-launched apps on macOS inherit a minimal PATH (no /opt/homebrew, mise, asdf, etc.). +// Probe the user's login shell once so the sidecar sees the same PATH / tool env as `$SHELL -il`. +const loadShellEnv = () => { + if (shellEnvProbed) return cachedShellEnv; + shellEnvProbed = true; + if (process.platform === 'win32') return null; + const shell = process.env.SHELL || '/bin/sh'; + if (isNushell(shell)) return null; + cachedShellEnv = probeShellEnv(shell, '-il') || probeShellEnv(shell, '-l'); + return cachedShellEnv; +}; + +// Merge the user's login-shell env (PATH, etc.) into this process before we +import { pathLooksUserConfigured, mergePathValues } from '@openchamber/web/server/lib/opencode/path-utils.js'; + +// import/start the server in-process. The server and its children (opencode +// CLI, git, etc.) inherit process.env directly now — there is no sidecar +// subprocess to hand a custom env to. +const inheritUserShellEnv = () => { + const shellEnv = loadShellEnv(); + if (!shellEnv) return; + + const homeDir = os.homedir(); + const currentPath = process.env.PATH || ''; + const currentPathLooksUserConfigured = pathLooksUserConfigured(currentPath, homeDir, ':'); + + for (const [key, value] of Object.entries(shellEnv)) { + if (key === 'PATH') continue; + if (typeof process.env[key] === 'undefined') { + process.env[key] = value; + } + } + + const shellPath = typeof shellEnv.PATH === 'string' ? shellEnv.PATH : ''; + if (!currentPathLooksUserConfigured && shellPath) { + process.env.PATH = mergePathValues(shellPath, currentPath, ':'); + } +}; + +const spawnLocalServer = async () => { + inheritUserShellEnv(); + + const settings = readSettingsRoot(); + const storedPort = Number.isFinite(settings.desktopLocalPort) ? settings.desktopLocalPort : null; + // When the user enables "Desktop Network Access" we bind on all interfaces + // so phones/tablets on the same Wi-Fi can reach the app. UI shows a clear + // warning and persists the flag via /api/config/settings. + const lanAccessEnabled = settings.desktopLanAccessEnabled === true; + const bindHost = lanAccessEnabled ? '0.0.0.0' : '127.0.0.1'; + + // Probe before starting the server — main() in the server module sets up a + // lot of global state before binding, and calling it twice after a listen + // failure would double-wire runtimes. Pick a known-free port in one shot. + const candidates = [storedPort, DEFAULT_DESKTOP_PORT].filter((v) => Number.isFinite(v) && v > 0); + let chosenPort = 0; + for (const candidate of candidates) { + if (await isPortFree(candidate)) { + chosenPort = candidate; + break; + } + } + if (chosenPort === 0) { + chosenPort = await pickUnusedPort(); + } + + // The server module reads ENV_DESKTOP_NOTIFY / OPENCHAMBER_DIST_DIR / + // OPENCHAMBER_RUNTIME at import time (top-level const), so these must be + // set before the first import. After this point, the same env is used by + // both the Electron main and the server running inside it. + process.env.OPENCHAMBER_HOST = bindHost; + process.env.OPENCHAMBER_DIST_DIR = resolveWebDistDir(); + process.env.OPENCHAMBER_RUNTIME = 'desktop'; + process.env.OPENCHAMBER_DESKTOP_NOTIFY = 'true'; + process.env.NO_PROXY = process.env.NO_PROXY || 'localhost,127.0.0.1'; + process.env.no_proxy = process.env.no_proxy || 'localhost,127.0.0.1'; + + const { startWebUiServer } = await import('@openchamber/web/server/index.js'); + + const handle = await startWebUiServer({ + port: chosenPort, + host: bindHost, + attachSignals: false, + exitOnShutdown: false, + onDesktopNotification: (payload) => maybeShowNativeNotification(payload), + }); + + const port = handle.getPort(); + const url = buildLocalUrl(port); + + state.serverHandle = handle; + state.sidecarUrl = url; + + await mutateSettingsRoot((root) => { + root.desktopLocalPort = port; + }); + + return url; +}; + +const killSidecar = () => { + if (state.serverHandle) { + try { + const result = state.serverHandle.stop({ exitProcess: false }); + if (result && typeof result.then === 'function') { + result.catch(() => {}); + } + } catch { + } + state.serverHandle = null; + } + state.sidecarUrl = null; +}; + +const macosMajorVersion = () => { + if (process.platform !== 'darwin') return 0; + const result = spawnSync('/usr/bin/sw_vers', ['-productVersion'], { encoding: 'utf8' }); + const raw = (result.stdout || '').trim(); + const [majorRaw, minorRaw] = raw.split('.'); + const major = Number.parseInt(majorRaw || '0', 10); + const minor = Number.parseInt(minorRaw || '0', 10); + return major === 10 ? minor : major; +}; + +const buildInitScript = (localOrigin, bootOutcome) => { + const home = JSON.stringify(os.homedir() || ''); + const local = JSON.stringify(localOrigin || ''); + const macVersion = macosMajorVersion(); + const outcome = JSON.stringify(bootOutcome ?? null); + return [ + '(function(){', + `try{window.__OPENCHAMBER_HOME__=${home};window.__OPENCHAMBER_MACOS_MAJOR__=${macVersion};window.__OPENCHAMBER_LOCAL_ORIGIN__=${local};var __oc_bo=${outcome};if(__oc_bo){window.__OPENCHAMBER_DESKTOP_BOOT_OUTCOME__=__oc_bo;}}catch(_e){}`, + '}())', + ].join(''); +}; + +const computeBootOutcome = ({ envTargetUrl, probe, config, localAvailable }) => { + if (envTargetUrl) { + const status = probe && probe.status === 'unreachable' ? 'unreachable' : 'ok'; + return { target: 'remote', status, hostId: ENV_OVERRIDE_HOST_ID, url: envTargetUrl }; + } + + const defaultId = config.defaultHostId || ''; + if (!defaultId) { + return { target: null, status: 'not-configured' }; + } + + if (defaultId === LOCAL_HOST_ID) { + return localAvailable + ? { target: 'local', status: 'ok' } + : { target: 'local', status: 'unreachable' }; + } + + const host = config.hosts.find((entry) => entry.id === defaultId); + if (!host) { + return { target: 'remote', status: 'missing', hostId: defaultId }; + } + + const status = probe && probe.status === 'unreachable' ? 'unreachable' : 'ok'; + return { target: 'remote', status, hostId: host.id, url: host.url }; +}; + +const buildStartupSplashHtml = () => { + const settings = readSettingsRoot(); + const splashBgLight = typeof settings.splashBgLight === 'string' ? settings.splashBgLight.trim() : '#f5f5f4'; + const splashFgLight = typeof settings.splashFgLight === 'string' ? settings.splashFgLight.trim() : '#1c1917'; + const splashBgDark = typeof settings.splashBgDark === 'string' ? settings.splashBgDark.trim() : '#0c0a09'; + const splashFgDark = typeof settings.splashFgDark === 'string' ? settings.splashFgDark.trim() : '#fafaf9'; + + return ` + + + + + + + +
OpenChamber
+ + `; +}; + +const isBenignNavigationAbort = (error) => { + if (!error || typeof error !== 'object') { + return false; + } + + if (error.errno === -3) { + return true; + } + + const message = typeof error.message === 'string' ? error.message : ''; + return message.includes('ERR_ABORTED') || message.includes(' (-3) loading '); +}; + +const navigateWindow = async (browserWindow, url, { allowAbort = false } = {}) => { + try { + await browserWindow.loadURL(url); + } catch (error) { + if (allowAbort && isBenignNavigationAbort(error)) { + return; + } + throw error; + } +}; + +const emitToWindow = (browserWindow, event, detail) => { + if (!browserWindow || browserWindow.isDestroyed()) return; + browserWindow.webContents.send('openchamber:emit', { event, detail }); +}; + +const emitToAllWindows = (event, detail) => { + for (const browserWindow of BrowserWindow.getAllWindows()) { + emitToWindow(browserWindow, event, detail); + } +}; + +const pendingDeepLinks = []; + +const parseDeepLink = (raw) => { + if (typeof raw !== 'string') return null; + const trimmed = raw.trim(); + if (!trimmed) return null; + try { + const url = new URL(trimmed); + if (url.protocol !== `${DEEP_LINK_PROTOCOL}:`) return null; + const type = url.hostname; + if (!type) return null; + const segments = url.pathname.split('/').filter(Boolean); + const value = segments.length > 0 + ? decodeURIComponent(segments.join('/')) + : ''; + return { type, value }; + } catch { + return null; + } +}; + +const switchToHostById = async (rawId) => { + const id = typeof rawId === 'string' ? rawId.trim() : ''; + if (!id) return; + const config = readDesktopHostsConfig(); + let targetUrl = null; + if (id === LOCAL_HOST_ID) { + targetUrl = state.sidecarUrl || state.localOrigin; + } else { + const host = config.hosts.find((entry) => entry.id === id); + if (!host) { + log.warn('[electron] deep-link host not found:', id); + return; + } + targetUrl = host.url; + } + if (!targetUrl) { + log.warn('[electron] deep-link host has no target URL:', id); + return; + } + const bootOutcome = id === LOCAL_HOST_ID + ? { target: 'local', status: 'ok' } + : { target: 'remote', status: 'ok', hostId: id, url: targetUrl }; + log.info('[electron] switching to host', { id, bootOutcome }); + await activateMainWindow(targetUrl, state.localOrigin, bootOutcome); +}; + +const dispatchDeepLink = (link) => { + if (!link) return; + log.info('[electron] dispatching deep-link', { type: link.type, valueLen: link.value?.length || 0 }); + if (link.type === 'session' && link.value) { + emitToAllWindows('openchamber:open-session', { sessionId: link.value }); + return; + } + if (link.type === 'project' && link.value) { + emitToAllWindows('openchamber:open-project', { projectPath: link.value }); + return; + } + if (link.type === 'host' && link.value) { + void switchToHostById(link.value); + return; + } + log.warn('[electron] unknown deep-link action:', link.type); +}; + +const flushPendingDeepLinks = () => { + while (pendingDeepLinks.length > 0) { + dispatchDeepLink(pendingDeepLinks.shift()); + } +}; + +const isMainWindowReadyForDeepLink = () => + Boolean(state.mainWindow) + && !state.mainWindow.isDestroyed() + && !state.mainWindow.webContents.isLoading(); + +const handleDeepLinks = (urls) => { + for (const raw of urls) { + const parsed = parseDeepLink(raw); + if (!parsed) continue; + if (isMainWindowReadyForDeepLink()) { + dispatchDeepLink(parsed); + } else { + pendingDeepLinks.push(parsed); + } + } +}; + +const extractInitialDeepLinks = () => + process.argv.filter((arg) => typeof arg === 'string' && arg.startsWith(`${DEEP_LINK_PROTOCOL}://`)); + +const dispatchDomEventToWindow = (browserWindow, event, detail) => { + if (!browserWindow || browserWindow.isDestroyed()) return; + + const eventLiteral = JSON.stringify(event); + const script = detail === undefined + ? `window.dispatchEvent(new Event(${eventLiteral}));` + : `window.dispatchEvent(new CustomEvent(${eventLiteral}, { detail: ${JSON.stringify(detail)} }));`; + + void browserWindow.webContents.executeJavaScript(script, true).catch(() => {}); +}; + +const getMenuTargetWindow = () => { + const focused = BrowserWindow.getFocusedWindow(); + if (focused && !focused.isDestroyed()) return focused; + if (state.mainWindow && !state.mainWindow.isDestroyed()) return state.mainWindow; + const [firstWindow] = BrowserWindow.getAllWindows(); + return firstWindow && !firstWindow.isDestroyed() ? firstWindow : null; +}; + +const dispatchMenuAction = (action) => { + const target = getMenuTargetWindow(); + emitToWindow(target, 'openchamber:menu-action', action); + dispatchDomEventToWindow(target, 'openchamber:menu-action', action); +}; + +const dispatchCheckForUpdates = () => { + emitToAllWindows('openchamber:check-for-updates'); + for (const browserWindow of BrowserWindow.getAllWindows()) { + dispatchDomEventToWindow(browserWindow, 'openchamber:check-for-updates'); + } +}; + +const nextWindowLabel = () => { + const value = state.windowCounter++; + return value === 1 ? 'main' : `main-${value}`; +}; + +const readThemeSource = () => { + const settings = readSettingsRoot(); + // themeMode is the user's intent; themeVariant is only the resolved + // concrete appearance at persist time. When mode === 'system', we must + // follow the OS even if variant was saved as a specific value. + if (settings.themeMode === 'system' || settings.useSystemTheme === true) return 'system'; + if (settings.themeMode === 'light') return 'light'; + if (settings.themeMode === 'dark') return 'dark'; + if (settings.themeVariant === 'light') return 'light'; + if (settings.themeVariant === 'dark') return 'dark'; + return 'system'; +}; + +const createBrowserWindow = ({ label, restoreGeometry, url }) => { + const saved = restoreGeometry ? readWindowState() : null; + const useSaved = saved && typeof saved.width === 'number' && typeof saved.height === 'number'; + const desktopLocalOrigin = state.localOrigin || ''; + const desktopHome = os.homedir() || ''; + const desktopMacosMajor = String(macosMajorVersion()); + const options = { + title: 'OpenChamber', + width: useSaved ? Math.max(saved.width, MIN_RESTORE_WINDOW_WIDTH) : 1280, + height: useSaved ? Math.max(saved.height, MIN_RESTORE_WINDOW_HEIGHT) : 800, + minWidth: MIN_WINDOW_WIDTH, + minHeight: MIN_WINDOW_HEIGHT, + show: false, + backgroundColor: '#151313', + // Tauri used an overlay title bar with explicit traffic-light placement. + // Electron's hiddenInset adds its own extra inset, which leaves the controls + // visibly lower than the app header. Use a plain hidden title bar instead. + titleBarStyle: process.platform === 'darwin' ? 'hidden' : 'default', + trafficLightPosition: process.platform === 'darwin' ? { x: 16, y: 17 } : undefined, + webPreferences: { + additionalArguments: [ + `--openchamber-local-origin=${desktopLocalOrigin}`, + `--openchamber-home=${desktopHome}`, + `--openchamber-macos-major=${desktopMacosMajor}`, + `--openchamber-boot-outcome=${JSON.stringify(state.bootOutcome || null)}`, + ], + preload: isDev ? path.join(__dirname, 'preload.mjs') : path.join(app.getAppPath(), 'preload.mjs'), + backgroundThrottling: true, + contextIsolation: true, + nodeIntegration: false, + // sandbox must stay off: the preload uses contextBridge + ipcRenderer + // from Electron's Node layer. contextIsolation + nodeIntegration:false + // keep the renderer world walled off from Node. Do NOT flip to true — + // the preload would fail to load and __TAURI__ would go undefined. + sandbox: false, + }, + }; + + const browserWindow = new BrowserWindow(options); + browserWindow.__ocLabel = label || nextWindowLabel(); + + if (useSaved && Number.isFinite(saved.x) && Number.isFinite(saved.y)) { + browserWindow.setPosition(saved.x, saved.y); + } + + if (useSaved && saved.maximized) { + browserWindow.maximize(); + } + + browserWindow.on('focus', () => { + state.focusedWindowIds.add(browserWindow.id); + }); + browserWindow.on('blur', () => { + state.focusedWindowIds.delete(browserWindow.id); + }); + + // Traffic lights disappear during dock-restore animation when using + // titleBarStyle:'hidden' + custom trafficLightPosition. macOS caches a + // snapshot of the window at miniaturize time and plays it during the + // genie-restore animation. We re-assert button position on 'minimize' + // (before the snapshot) and 'restore'/'show'/'focus' to cover other + // transient reset states AppKit puts the buttons in. + if (process.platform === 'darwin') { + const refreshTrafficLights = () => { + if (browserWindow.isDestroyed()) return; + try { + browserWindow.setWindowButtonVisibility(true); + browserWindow.setTrafficLightPosition({ x: 16, y: 17 }); + } catch {} + }; + browserWindow.on('minimize', refreshTrafficLights); + browserWindow.on('restore', () => { + refreshTrafficLights(); + setTimeout(refreshTrafficLights, 250); + }); + browserWindow.on('show', refreshTrafficLights); + browserWindow.on('focus', refreshTrafficLights); + } + + browserWindow.on('resize', () => { + emitToWindow(browserWindow, 'openchamber:window-resized'); + debounceWindowStatePersist(browserWindow, false); + }); + browserWindow.on('move', () => { + debounceWindowStatePersist(browserWindow, false); + }); + browserWindow.on('close', (event) => { + if (process.platform === 'darwin' && !state.quitRequested) { + const remainingVisible = BrowserWindow.getAllWindows().filter( + (window) => !window.isDestroyed() && window.isVisible(), + ).length; + + if (remainingVisible <= 1) { + debounceWindowStatePersist(browserWindow, true); + event.preventDefault(); + browserWindow.hide(); + return; + } + } + + debounceWindowStatePersist(browserWindow, true); + }); + browserWindow.on('closed', () => { + state.focusedWindowIds.delete(browserWindow.id); + if (state.mainWindow && browserWindow.id === state.mainWindow.id) { + state.mainWindow = null; + } + if (BrowserWindow.getAllWindows().length === 0) { + if (!state.installingUpdate) { + killSidecar(); + } + if (process.platform !== 'darwin') { + app.quit(); + } + } + }); + + // Any navigation target that isn't our own UI (local server / configured + // desktop hosts) should open in the user's default browser, not spawn + // another Electron window loading arbitrary web content. + const isAllowedNavigationUrl = (raw) => { + try { + const url = new URL(raw); + if (url.protocol === 'file:' || url.protocol === 'about:' || url.protocol === 'devtools:') return true; + if (url.protocol !== 'http:' && url.protocol !== 'https:') return false; + const hostname = url.hostname; + if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') return true; + if (state.localOrigin) { + try { + if (new URL(state.localOrigin).origin === url.origin) return true; + } catch { + } + } + const hosts = readDesktopHostsConfig()?.hosts || []; + for (const entry of hosts) { + if (typeof entry?.url !== 'string') continue; + try { + if (new URL(entry.url).origin === url.origin) return true; + } catch { + } + } + return false; + } catch { + return false; + } + }; + + browserWindow.webContents.setWindowOpenHandler(({ url }) => { + if (isAllowedNavigationUrl(url)) { + return { action: 'allow' }; + } + void shell.openExternal(url).catch(() => {}); + return { action: 'deny' }; + }); + + browserWindow.webContents.on('will-navigate', (event, url) => { + if (isAllowedNavigationUrl(url)) return; + event.preventDefault(); + void shell.openExternal(url).catch(() => {}); + }); + + browserWindow.webContents.setZoomFactor(1); + browserWindow.webContents.on('zoom-changed', () => { + browserWindow.webContents.setZoomFactor(1); + }); + + browserWindow.webContents.on('dom-ready', () => { + if (state.initScript) { + void browserWindow.webContents.executeJavaScript(state.initScript).catch(() => {}); + } + }); + + browserWindow.webContents.on('did-finish-load', () => { + browserWindow.webContents.setZoomFactor(1); + if (state.mainWindow && browserWindow.id === state.mainWindow.id && pendingDeepLinks.length > 0) { + const timer = setTimeout(flushPendingDeepLinks, 400); + if (typeof timer?.unref === 'function') timer.unref(); + } + }); + + browserWindow.once('ready-to-show', () => { + browserWindow.show(); + browserWindow.focus(); + }); + + if (url) { + void navigateWindow(browserWindow, url); + } else { + void navigateWindow( + browserWindow, + `data:text/html;charset=utf-8,${encodeURIComponent(buildStartupSplashHtml())}`, + { allowAbort: true }, + ); + } + + return browserWindow; +}; + +const activateMainWindow = async (url, localOrigin, bootOutcome) => { + state.localOrigin = localOrigin; + state.bootOutcome = bootOutcome ?? null; + state.initScript = buildInitScript(localOrigin, state.bootOutcome); + + const mainWindow = state.mainWindow; + if (mainWindow && !mainWindow.isDestroyed()) { + await navigateWindow(mainWindow, url, { allowAbort: true }); + mainWindow.show(); + mainWindow.focus(); + return mainWindow; + } + + state.mainWindow = createBrowserWindow({ + label: 'main', + restoreGeometry: true, + url, + }); + return state.mainWindow; +}; + +const createAdditionalWindow = async (url) => { + if (!state.localOrigin) { + return null; + } + const browserWindow = createBrowserWindow({ + label: nextWindowLabel(), + restoreGeometry: false, + url, + }); + return browserWindow; +}; + +const resolveInitialUrl = async () => { + const localUrl = isDev && await waitForHealth('http://127.0.0.1:3901', 5_000, 100) + ? 'http://127.0.0.1:3901' + : await spawnLocalServer(); + + const localUiUrl = isDev && await waitForHealth('http://127.0.0.1:5173', 8_000, 100) + ? 'http://127.0.0.1:5173' + : localUrl; + + state.sidecarUrl = localUrl; + const localAvailable = Boolean(localUrl); + + const localOrigin = new URL(localUiUrl).origin; + let initialUrl = localUiUrl; + let remoteProbe = null; + + const envTarget = normalizeHostUrl(process.env.OPENCHAMBER_SERVER_URL || ''); + const config = readDesktopHostsConfig(); + if (envTarget) { + initialUrl = envTarget; + } else if (config.defaultHostId && config.defaultHostId !== LOCAL_HOST_ID) { + const host = config.hosts.find((entry) => entry.id === config.defaultHostId); + if (host?.url) { + initialUrl = host.url; + } + } + + if (initialUrl !== localUiUrl) { + remoteProbe = await probeHostWithTimeout(initialUrl, 2_000); + if (remoteProbe.status === 'unreachable') { + remoteProbe = await probeHostWithTimeout(initialUrl, 10_000); + } + if (remoteProbe.status === 'unreachable') { + state.unreachableHosts.add(initialUrl); + initialUrl = localUiUrl; + } + } + + const bootOutcome = computeBootOutcome({ + envTargetUrl: envTarget || null, + probe: remoteProbe, + config, + localAvailable, + }); + + return { initialUrl, localOrigin, localUiUrl, bootOutcome }; +}; + +const compareSemver = (left, right) => { + const a = String(left || '').replace(/^v/, '').split('.').map((value) => Number.parseInt(value || '0', 10)); + const b = String(right || '').replace(/^v/, '').split('.').map((value) => Number.parseInt(value || '0', 10)); + const length = Math.max(a.length, b.length); + for (let index = 0; index < length; index += 1) { + const diff = (a[index] || 0) - (b[index] || 0); + if (diff !== 0) return diff; + } + return 0; +}; + +const parseGithubRepo = () => { + return { owner: 'btriapitsyn', repo: 'openchamber' }; +}; + +const setupAutoUpdater = () => { + if (!app.isPackaged) { + return; + } + autoUpdater.autoDownload = false; + autoUpdater.autoInstallOnAppQuit = false; + autoUpdater.allowPrerelease = false; + autoUpdater.fullChangelog = true; + autoUpdater.disableWebInstaller = false; + autoUpdater.logger = log; + + const { owner, repo } = parseGithubRepo(); + autoUpdater.setFeedURL({ + provider: 'github', + owner, + repo, + }); + + autoUpdater.on('download-progress', (progress) => { + emitToAllWindows('openchamber:update-progress', mapUpdaterProgressEvent({ + event: 'Progress', + data: { + chunkLength: Math.max(0, Math.round(progress.bytesPerSecond || 0)), + downloaded: Math.round(progress.transferred || 0), + total: Math.round(progress.total || 0), + }, + })); + }); + + autoUpdater.on('update-downloaded', (info) => { + log.info(`[electron] update-downloaded version=${info?.version || 'unknown'}`); + if (state.pendingUpdate) { + state.pendingUpdate.downloaded = true; + } + }); + + autoUpdater.on('error', (err) => { + log.error('[electron] autoUpdater error', err); + }); +}; + +const parseRelevantChangelogNotes = async (fromVersion, toVersion) => { + try { + const response = await fetch(CHANGELOG_URL, { signal: AbortSignal.timeout(10_000) }); + if (!response.ok) return null; + const changelog = await response.text(); + const sections = changelog.split(/^##\s+\[/m).slice(1); + const relevant = []; + for (const section of sections) { + const version = section.split(']')[0]; + if (compareSemver(version, fromVersion) > 0 && compareSemver(version, toVersion) <= 0) { + relevant.push(`## [${section}`.trim()); + } + } + return relevant.length > 0 ? relevant.join('\n\n') : null; + } catch { + return null; + } +}; + +const buildInstalledAppsCachePath = () => path.join(path.dirname(settingsFilePath()), INSTALLED_APPS_CACHE_FILE); + +// Async variants. sips + mdfind via spawnSync blocked the Electron main event +// loop for 2-3s on boot (22 OPEN_IN_APPS × ~200 ms each). Use execFile promises +// so each child-process wait yields to the loop and the UI stays responsive. +const pathExists = async (candidate) => { + try { + await fsp.access(candidate); + return true; + } catch { + return false; + } +}; + +const resolveAppBundlePath = async (appName) => { + if (process.platform !== 'darwin') return null; + const bundleName = appName.endsWith('.app') ? appName : `${appName}.app`; + const candidates = [ + `/Applications/${bundleName}`, + `/System/Applications/${bundleName}`, + `/System/Applications/Utilities/${bundleName}`, + path.join(os.homedir(), 'Applications', bundleName), + ]; + for (const candidate of candidates) { + if (await pathExists(candidate)) return candidate; + } + try { + const { stdout } = await execFileAsync('mdfind', ['-name', bundleName], { encoding: 'utf8' }); + const first = (stdout || '').split('\n').map((line) => line.trim()).find(Boolean); + return first || null; + } catch { + return null; + } +}; + +const isAppBundleInstalled = async (appName) => Boolean(await resolveAppBundlePath(appName)); + +const iconToDataUrl = async (iconPath, appName) => { + if (!iconPath || !(await pathExists(iconPath))) return null; + const safeName = String(appName || 'app').replace(/[^a-z0-9]/gi, '_'); + const tempPath = path.join(os.tmpdir(), `openchamber-icon-${safeName}-${Date.now()}.png`); + try { + await execFileAsync('sips', ['-s', 'format', 'png', '-Z', '32', iconPath, '--out', tempPath], { stdio: 'ignore' }); + } catch { + return null; + } + if (!(await pathExists(tempPath))) return null; + try { + const bytes = await fsp.readFile(tempPath); + return `data:image/png;base64,${bytes.toString('base64')}`; + } finally { + await fsp.rm(tempPath, { force: true }).catch(() => {}); + } +}; + +const resolveAppIconPath = async (appPath) => { + if (!appPath || !(await pathExists(appPath))) return null; + const resourcesPath = path.join(appPath, 'Contents', 'Resources'); + if (!(await pathExists(resourcesPath))) return null; + let entries; + try { + entries = await fsp.readdir(resourcesPath); + } catch { + return null; + } + const icon = entries.find((entry) => entry.toLowerCase().endsWith('.icns')); + return icon ? path.join(resourcesPath, icon) : null; +}; + +const buildInstalledApps = async (apps) => { + const seen = new Set(); + const names = apps + .map((raw) => String(raw || '').trim()) + .filter((raw) => raw && !seen.has(raw) && seen.add(raw)); + const results = []; + for (const name of names) { + const appPath = await resolveAppBundlePath(name); + if (!appPath) continue; + const iconDataUrl = await iconToDataUrl(await resolveAppIconPath(appPath), name); + results.push({ name, iconDataUrl }); + } + return results; +}; + +const parseSshConfigImports = () => { + const sshConfigPath = path.join(os.homedir(), '.ssh', 'config'); + if (!fs.existsSync(sshConfigPath)) return []; + const lines = fs.readFileSync(sshConfigPath, 'utf8').split(/\r?\n/); + const results = []; + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#') || !trimmed.toLowerCase().startsWith('host ')) { + continue; + } + const hosts = trimmed.slice(5).trim().split(/\s+/).filter(Boolean); + for (const host of hosts) { + results.push({ + host, + pattern: /[*?]/.test(host), + source: sshConfigPath, + sshCommand: `ssh ${host}`, + }); + } + } + return results; +}; + +const readDesktopSshInstances = () => { + const root = readSettingsRoot(); + return { instances: Array.isArray(root.desktopSshInstances) ? root.desktopSshInstances : [] }; +}; + +const writeDesktopSshInstances = async (config) => { + const nextInstances = Array.isArray(config?.instances) ? config.instances : []; + await mutateSettingsRoot((root) => { + root.desktopSshInstances = nextInstances; + }); + return { instances: nextInstances }; +}; + +const updateHostUrlForSshInstance = async (id, label, localUrl) => { + const config = readDesktopHostsConfig(); + const nextHosts = config.hosts.filter((entry) => entry.id !== id); + nextHosts.push({ id, label, url: localUrl }); + await writeDesktopHostsConfig({ hosts: nextHosts, defaultHostId: config.defaultHostId }); +}; + +const JETBRAINS_APP_IDS = new Set([ + 'pycharm', + 'intellij', + 'webstorm', + 'phpstorm', + 'rider', + 'rustrover', + 'android-studio', +]); + +const CLI_BY_APP_ID = { + vscode: 'code', + cursor: 'cursor', + vscodium: 'codium', + windsurf: 'windsurf', + zed: 'zed', +}; + +const buildOpenProjectSpecs = ({ projectPath, appId, appName }) => { + if (appId === 'finder') { + return [{ program: 'open', args: [projectPath] }]; + } + + if (appId === 'terminal' || appId === 'iterm2' || appId === 'ghostty') { + return [{ program: 'open', args: ['-a', appName, projectPath] }]; + } + + const specs = []; + + const cli = CLI_BY_APP_ID[appId]; + if (cli) { + specs.push({ program: cli, args: ['-n', projectPath] }); + } + + if (JETBRAINS_APP_IDS.has(appId)) { + specs.push({ program: 'open', args: ['-na', appName, '--args', projectPath] }); + } + + specs.push({ program: 'open', args: ['-a', appName, projectPath] }); + return specs; +}; + +const buildOpenFileSpecs = ({ filePath, appId, appName }) => { + if (appId === 'finder') { + return [{ program: 'open', args: ['-R', filePath] }]; + } + + const parentDir = path.dirname(filePath); + if (appId === 'terminal' || appId === 'iterm2' || appId === 'ghostty') { + return [{ program: 'open', args: ['-a', appName, parentDir] }]; + } + + const specs = []; + + const cli = CLI_BY_APP_ID[appId]; + if (cli) { + specs.push({ program: cli, args: [filePath] }); + } + + specs.push({ program: 'open', args: ['-a', appName, filePath] }); + return specs; +}; + +const runSpecChain = (specs, appName) => { + const failures = []; + for (const spec of specs) { + const result = spawnSync(spec.program, spec.args, { stdio: 'ignore' }); + if (result.error) { + failures.push(`${spec.program}: ${result.error.message}`); + continue; + } + if (result.status === 0) { + return; + } + failures.push(`${spec.program} exited ${result.status}`); + } + throw new Error(`Failed to open in ${appName}: ${failures.join('; ')}`); +}; + +const handleInvoke = async (browserWindow, command, args = {}) => { + switch (command) { + case 'desktop_start_window_drag': + return null; + + case 'desktop_is_window_fullscreen': + return Boolean(browserWindow?.isFullScreen()); + + case 'desktop_set_window_title': + if (browserWindow && typeof args.title === 'string') { + browserWindow.setTitle(args.title); + } + return null; + + case 'desktop_get_app_version': + return APP_VERSION; + + case 'desktop_save_markdown_file': { + const defaultPath = typeof args.defaultFileName === 'string' ? args.defaultFileName.trim() : ''; + if (!defaultPath) { + throw new Error('Default file name is required'); + } + + const content = typeof args.content === 'string' ? args.content : ''; + const result = await dialog.showSaveDialog(browserWindow || undefined, { + defaultPath, + filters: [{ name: 'Markdown', extensions: ['md'] }], + }); + if (result.canceled || !result.filePath) { + return null; + } + + await fsp.writeFile(result.filePath, content, 'utf8'); + return result.filePath; + } + + case 'desktop_read_file': { + const rawPath = typeof args.path === 'string' ? args.path : ''; + if (!rawPath) throw new Error('Path is required'); + // Defense in depth behind the IPC origin gate: even our own UI (or a + // prompt-injected agent) can't read credential stores. Resolve the + // path, require it under $HOME or tmpdir, and refuse known secret dirs + // / dotfiles commonly holding keys. + const filePath = path.resolve(rawPath); + const home = os.homedir() || ''; + const tmp = os.tmpdir() || ''; + const underHome = home && (filePath === home || filePath.startsWith(home + path.sep)); + const underTmp = tmp && (filePath === tmp || filePath.startsWith(tmp + path.sep)); + if (!underHome && !underTmp) { + throw new Error('File is outside the allowed workspace'); + } + const DENIED_SEGMENTS = ['.ssh', '.aws', '.gnupg', '.gpg', '.config/gh', '.config/openchamber/credentials']; + const relFromHome = underHome ? filePath.slice(home.length + 1) : ''; + const relNormalized = relFromHome.split(path.sep).join('/'); + if (DENIED_SEGMENTS.some((segment) => relNormalized === segment || relNormalized.startsWith(`${segment}/`))) { + throw new Error('Access to this path is not allowed'); + } + const basename = path.basename(filePath).toLowerCase(); + if (basename === '.env' || basename.startsWith('.env.') || basename.endsWith('.pem') || basename.endsWith('.key')) { + throw new Error('Access to this path is not allowed'); + } + const stats = await fsp.stat(filePath); + if (stats.size > 50 * 1024 * 1024) { + throw new Error('File is too large. Maximum size is 50MB.'); + } + const bytes = await fsp.readFile(filePath); + const ext = path.extname(filePath).toLowerCase(); + const mime = ({ + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.webp': 'image/webp', + '.svg': 'image/svg+xml', + '.bmp': 'image/bmp', + '.ico': 'image/x-icon', + '.pdf': 'application/pdf', + '.txt': 'text/plain', + '.md': 'text/markdown', + '.json': 'application/json', + '.js': 'text/javascript', + '.ts': 'text/typescript', + '.tsx': 'text/typescript-jsx', + '.jsx': 'text/javascript-jsx', + '.html': 'text/html', + '.css': 'text/css', + '.py': 'text/x-python', + })[ext] || 'application/octet-stream'; + return { mime, base64: bytes.toString('base64'), size: bytes.length }; + } + + case 'desktop_notify': + maybeShowNativeNotification(args); + return null; + + case 'desktop_clear_cache': + await session.defaultSession.clearStorageData(); + for (const browserWindow of BrowserWindow.getAllWindows()) { + browserWindow.webContents.reload(); + } + return null; + + case 'desktop_open_path': { + const targetPath = typeof args.path === 'string' ? args.path.trim() : ''; + const appName = typeof args.app === 'string' ? args.app.trim() : ''; + if (!targetPath) throw new Error('Path is required'); + if (process.platform === 'darwin') { + const openArgs = appName ? ['-a', appName, targetPath] : [targetPath]; + spawn('open', openArgs, { detached: true, stdio: 'ignore' }).unref(); + return null; + } + await shell.openPath(targetPath); + return null; + } + + case 'desktop_open_external_url': { + const target = typeof args.url === 'string' ? args.url.trim() : ''; + if (!target) throw new Error('URL is required'); + + const parsed = new URL(target); + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + throw new Error('Only HTTP URLs can be opened externally'); + } + + await shell.openExternal(parsed.toString()); + return null; + } + + case 'desktop_reveal_path': { + const targetPath = typeof args.path === 'string' ? args.path.trim() : ''; + if (!targetPath) { + throw new Error('Path is required'); + } + + const stats = await fsp.stat(targetPath).catch(() => null); + if (stats?.isDirectory()) { + await shell.openPath(targetPath); + return null; + } + + shell.showItemInFolder(targetPath); + return null; + } + + case 'desktop_open_in_app': { + if (process.platform !== 'darwin') { + throw new Error('desktop_open_in_app is only supported on macOS'); + } + const projectPath = typeof args.projectPath === 'string' ? args.projectPath.trim() : ''; + const appId = typeof args.appId === 'string' ? args.appId.trim().toLowerCase() : ''; + const appName = typeof args.appName === 'string' ? args.appName.trim() : ''; + if (!projectPath || !appId || !appName) { + throw new Error('Project path, app id, and app name are required'); + } + runSpecChain(buildOpenProjectSpecs({ projectPath, appId, appName }), appName); + return null; + } + + case 'desktop_open_file_in_app': { + if (process.platform !== 'darwin') { + throw new Error('desktop_open_file_in_app is only supported on macOS'); + } + const filePath = typeof args.filePath === 'string' ? args.filePath.trim() : ''; + const appId = typeof args.appId === 'string' ? args.appId.trim().toLowerCase() : ''; + const appName = typeof args.appName === 'string' ? args.appName.trim() : ''; + if (!filePath || !appId || !appName) { + throw new Error('File path, app id, and app name are required'); + } + runSpecChain(buildOpenFileSpecs({ filePath, appId, appName }), appName); + return null; + } + + case 'desktop_filter_installed_apps': { + if (process.platform !== 'darwin') { + throw new Error('desktop_filter_installed_apps is only supported on macOS'); + } + if (!Array.isArray(args.apps)) return []; + const results = await Promise.all( + args.apps.map(async (appName) => (await isAppBundleInstalled(String(appName))) ? String(appName) : null) + ); + return results.filter(Boolean); + } + + case 'desktop_fetch_app_icons': { + if (process.platform !== 'darwin') { + throw new Error('desktop_fetch_app_icons is only supported on macOS'); + } + const names = Array.isArray(args.apps) ? args.apps : []; + const results = []; + for (const name of names) { + const appPath = await resolveAppBundlePath(String(name)); + if (!appPath) continue; + const dataUrl = await iconToDataUrl(await resolveAppIconPath(appPath), String(name)); + if (dataUrl) results.push({ app: String(name), dataUrl }); + } + return results; + } + + case 'desktop_get_installed_apps': { + if (process.platform !== 'darwin') { + throw new Error('desktop_get_installed_apps is only supported on macOS'); + } + const cachePath = buildInstalledAppsCachePath(); + const now = Math.floor(Date.now() / 1000); + let cache = null; + try { + cache = JSON.parse(await fsp.readFile(cachePath, 'utf8')); + } catch { + } + const cachedApps = Array.isArray(cache?.apps) ? cache.apps : []; + const hasCache = Boolean(cache); + const isCacheStale = !cache || (now - Number(cache.updatedAt || 0)) > INSTALLED_APPS_CACHE_TTL_SECS; + const refresh = async () => { + const apps = await buildInstalledApps(Array.isArray(args.apps) ? args.apps : []); + await fsp.mkdir(path.dirname(cachePath), { recursive: true }); + await fsp.writeFile(cachePath, JSON.stringify({ updatedAt: now, apps }, null, 2)); + emitToAllWindows('openchamber:installed-apps-updated', apps); + }; + if (!hasCache || isCacheStale || args.force === true) { + void refresh(); + } + return { apps: cachedApps, hasCache, isCacheStale }; + } + + case 'desktop_hosts_get': + return readDesktopHostsConfig(); + + case 'desktop_hosts_set': { + await writeDesktopHostsConfig(args.input || args.config || {}); + const updatedConfig = readDesktopHostsConfig(); + const envTarget = normalizeHostUrl(process.env.OPENCHAMBER_SERVER_URL || ''); + state.bootOutcome = computeBootOutcome({ + envTargetUrl: envTarget || null, + probe: null, + config: updatedConfig, + localAvailable: Boolean(state.sidecarUrl || state.localOrigin), + }); + state.initScript = buildInitScript(state.localOrigin, state.bootOutcome); + log.info('[electron] hosts config updated, recomputed bootOutcome', state.bootOutcome); + return null; + } + + case 'desktop_host_probe': + return probeHostWithTimeout(String(args.url || ''), 2_000); + + case 'desktop_set_window_theme': { + const mode = typeof args.themeMode === 'string' ? args.themeMode : ''; + const variant = typeof args.themeVariant === 'string' ? args.themeVariant : ''; + // Priority order: themeMode expresses the user's intent (including + // "follow OS"). Variant is just the resolved variant at send time; + // when mode === 'system' with variant === 'dark' (because OS is + // currently dark), we must still pin themeSource to 'system' so + // Chromium keeps reacting to OS theme changes. + if (mode === 'system') { + nativeTheme.themeSource = 'system'; + } else if (mode === 'light') { + nativeTheme.themeSource = 'light'; + } else if (mode === 'dark') { + nativeTheme.themeSource = 'dark'; + } else if (variant === 'light') { + nativeTheme.themeSource = 'light'; + } else if (variant === 'dark') { + nativeTheme.themeSource = 'dark'; + } else { + nativeTheme.themeSource = 'system'; + } + return null; + } + + case 'desktop_set_vibrancy': { + // Vibrancy (macOS blur) is not supported in the Electron shell — the + // Tauri build used NSVisualEffectView via Tauri plugin, Electron has + // no equivalent for our titleBarStyle:'hidden' setup. Persist the + // disabled state so settings UI reflects it; args.enabled is ignored. + await mutateSettingsRoot((root) => { + root.desktopVibrancy = false; + }); + return { enabled: false, requiresRestart: false }; + } + + case 'desktop_check_for_updates': { + const currentVersion = APP_VERSION; + let payload = null; + try { + const response = await fetch(UPDATE_METADATA_URL, { signal: AbortSignal.timeout(10_000) }); + payload = await response.json(); + } catch { + } + + let updateResult = null; + try { + updateResult = await autoUpdater.checkForUpdates(); + } catch { + } + + const updateInfo = updateResult?.updateInfo; + const nextVersion = + (typeof updateInfo?.version === 'string' && updateInfo.version) || + (typeof payload?.version === 'string' && payload.version) || + currentVersion; + const available = compareSemver(nextVersion, currentVersion) > 0; + const body = + (typeof payload?.notes === 'string' && payload.notes.trim() ? payload.notes : null) || + (typeof updateInfo?.releaseNotes === 'string' && updateInfo.releaseNotes.trim() ? updateInfo.releaseNotes : null) || + await parseRelevantChangelogNotes(currentVersion, nextVersion); + state.pendingUpdate = available ? { version: nextVersion, metadata: payload, electronUpdate: updateResult } : null; + return { + available, + currentVersion, + version: available ? nextVersion : null, + body: body || null, + date: + (typeof updateInfo?.releaseDate === 'string' && updateInfo.releaseDate) || + (typeof payload?.pub_date === 'string' ? payload.pub_date : null), + }; + } + + case 'desktop_download_and_install_update': + if (!state.pendingUpdate) { + throw new Error('No pending update'); + } + emitToAllWindows('openchamber:update-progress', mapUpdaterProgressEvent({ + event: 'Started', + data: { + contentLength: null, + }, + })); + if (!state.pendingUpdate.electronUpdate) { + throw new Error('Electron updater metadata is not available for this build'); + } + if (!state.pendingUpdate.downloaded) { + await new Promise((resolve, reject) => { + let settled = false; + const cleanup = () => { + autoUpdater.off('update-downloaded', onDownloaded); + autoUpdater.off('error', onError); + }; + const finish = (callback, value) => { + if (settled) return; + settled = true; + cleanup(); + callback(value); + }; + const onDownloaded = () => finish(resolve, null); + const onError = (error) => finish(reject, error); + autoUpdater.on('update-downloaded', onDownloaded); + autoUpdater.on('error', onError); + Promise.resolve(autoUpdater.downloadUpdate()).catch((error) => finish(reject, error)); + }); + } + emitToAllWindows('openchamber:update-progress', mapUpdaterProgressEvent({ + event: 'Finished', + data: {}, + })); + return null; + + case 'desktop_restart': { + const applyUpdate = Boolean(state.pendingUpdate?.downloaded && app.isPackaged); + log.info(`[electron] desktop_restart applyUpdate=${applyUpdate} packaged=${app.isPackaged}`); + if (applyUpdate && process.platform === 'darwin' && typeof app.isInApplicationsFolder === 'function') { + try { + if (!app.isInApplicationsFolder()) { + throw new Error('Desktop update requires OpenChamber.app to be installed in /Applications'); + } + } catch (error) { + log.warn('[electron] desktop_restart blocked', error); + throw error; + } + } + if (applyUpdate) { + // Match the working updater pattern closely: only bypass the macOS + // hide-on-close / quit-confirmation guards, leave the rest of the + // updater-driven quit/install sequence alone. + state.quitRequested = true; + state.installingUpdate = true; + state.quitConfirmationPending = false; + if (state.mainWindow && !state.mainWindow.isDestroyed()) { + try { + debounceWindowStatePersist(state.mainWindow, true); + } catch { + } + } + } + // Defer so the IPC reply flushes before the app starts shutting down. + // Without this, quitAndInstall() can race with the renderer's pending + // invoke and the restart appears to do nothing from the UI side. + setImmediate(() => { + try { + if (applyUpdate) { + autoUpdater.quitAndInstall(); + } else { + app.relaunch(); + app.exit(0); + } + } catch (err) { + log.error('[electron] desktop_restart failed', err); + } + }); + return null; + } + + case 'desktop_get_lan_address': + return await detectLanIPv4Address(); + + case 'desktop_new_window': { + const config = readDesktopHostsConfig(); + const localUiUrl = state.sidecarUrl || state.localOrigin; + let targetUrl = localUiUrl; + if (config.defaultHostId && config.defaultHostId !== LOCAL_HOST_ID) { + const host = config.hosts.find((entry) => entry.id === config.defaultHostId); + if (host?.url && !state.unreachableHosts.has(host.url)) { + targetUrl = host.url; + } + } + await createAdditionalWindow(targetUrl); + return null; + } + + case 'desktop_new_window_at_url': { + const targetUrl = normalizeHostUrl(String(args.url || '')); + if (!targetUrl) { + throw new Error('Invalid URL'); + } + await createAdditionalWindow(targetUrl); + return null; + } + + case 'desktop_ssh_instances_get': + return sshManager.readInstances(); + + case 'desktop_ssh_instances_set': + await sshManager.setInstances(args.config || {}); + return null; + + case 'desktop_ssh_import_hosts': + return await sshManager.importHosts(); + + case 'desktop_ssh_connect': { + const id = String(args.id || '').trim(); + await sshManager.connect(id); + return null; + } + + case 'desktop_ssh_disconnect': { + const id = String(args.id || '').trim(); + await sshManager.disconnect(id); + return null; + } + + case 'desktop_ssh_status': { + const id = String(args.id || '').trim(); + return await sshManager.statusesWithDefaults(id || undefined); + } + + case 'desktop_ssh_logs': + return sshManager.logsForInstance(String(args.id || '').trim(), Number(args.limit) || 200); + + case 'desktop_ssh_logs_clear': + sshManager.clearLogsForInstance(String(args.id || '').trim()); + return null; + + default: + throw new Error(`Unknown desktop command: ${command}`); + } +}; + +const buildMacMenu = () => { + const dispatchAction = (action) => dispatchMenuAction(action); + const handleCopyAction = () => { + BrowserWindow.getFocusedWindow()?.webContents.copy(); + dispatchAction('copy'); + }; + + return Menu.buildFromTemplate([ + { + label: app.name, + submenu: [ + { role: 'about' }, + { + label: 'Check for Updates', + click: () => dispatchCheckForUpdates(), + }, + { type: 'separator' }, + { label: 'Settings', accelerator: 'Cmd+,', click: () => dispatchAction('settings') }, + { label: 'Command Palette', accelerator: 'Cmd+K', click: () => dispatchAction('command-palette') }, + { label: 'Quick Open…', accelerator: 'Cmd+P', click: () => dispatchAction('quick-open') }, + { type: 'separator' }, + { role: 'services' }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideOthers' }, + { type: 'separator' }, + { role: 'quit' }, + ], + }, + { + label: 'File', + submenu: [ + { label: 'New Window', accelerator: 'Cmd+Shift+Alt+N', click: () => void handleInvoke(null, 'desktop_new_window') }, + { type: 'separator' }, + { label: 'New Session', accelerator: 'Cmd+N', click: () => dispatchAction('new-session') }, + { label: 'New Worktree', accelerator: 'Cmd+Shift+N', click: () => dispatchAction('new-worktree-session') }, + { type: 'separator' }, + { label: 'Add Workspace', click: () => dispatchAction('change-workspace') }, + { type: 'separator' }, + { role: 'close' }, + ], + }, + { + label: 'Edit', + submenu: [ + { role: 'undo' }, + { role: 'redo' }, + { type: 'separator' }, + { role: 'cut' }, + { label: 'Copy', accelerator: 'Cmd+C', click: () => handleCopyAction() }, + { role: 'paste' }, + { role: 'selectAll' }, + ], + }, + { + label: 'View', + submenu: [ + { label: 'Git', accelerator: 'Cmd+G', click: () => dispatchAction('open-git-tab') }, + { label: 'Diff', accelerator: 'Cmd+E', click: () => dispatchAction('open-diff-tab') }, + { label: 'Files', click: () => dispatchAction('open-files-tab') }, + { label: 'Terminal', accelerator: 'Cmd+T', click: () => dispatchAction('open-terminal-tab') }, + { type: 'separator' }, + { label: 'Light Theme', click: () => dispatchAction('theme-light') }, + { label: 'Dark Theme', click: () => dispatchAction('theme-dark') }, + { label: 'System Theme', click: () => dispatchAction('theme-system') }, + { type: 'separator' }, + { label: 'Toggle Session Sidebar', accelerator: 'Cmd+L', click: () => dispatchAction('toggle-sidebar') }, + { label: 'Toggle Memory Debug', accelerator: 'Cmd+Shift+D', click: () => dispatchAction('toggle-memory-debug') }, + { type: 'separator' }, + { role: 'togglefullscreen' }, + ], + }, + { + label: 'Window', + submenu: [ + { role: 'minimize' }, + { role: 'zoom' }, + { type: 'separator' }, + { role: 'close' }, + ], + }, + { + label: 'Help', + submenu: [ + { label: 'Keyboard Shortcuts', accelerator: 'Cmd+.', click: () => dispatchAction('help-dialog') }, + { label: 'Show Diagnostics', accelerator: 'Cmd+Shift+L', click: () => dispatchAction('download-logs') }, + { type: 'separator' }, + { label: 'Clear Cache', click: () => void handleInvoke(null, 'desktop_clear_cache') }, + { type: 'separator' }, + { label: 'Report a Bug', click: () => shell.openExternal(GITHUB_BUG_REPORT_URL) }, + { label: 'Request a Feature', click: () => shell.openExternal(GITHUB_FEATURE_REQUEST_URL) }, + { type: 'separator' }, + { label: 'Join Discord', click: () => shell.openExternal(DISCORD_INVITE_URL) }, + ], + }, + ]); +}; + +contextMenu({ + showInspectElement: isDev, + showSaveImageAs: true, + showCopyImage: true, + showCopyLink: true, +}); + +// All desktop_* IPC and dialog:open run with full Electron main privileges +// (fs access, shell.openPath, spawn, app.relaunch, …). The preload shim is +// injected into every webContents in the window, including remote hosts the +// user switches to via DesktopHostSwitcher. Without a gate, a malicious +// remote page could read arbitrary local files, open arbitrary apps, etc. +// +// Strategy: commands fall into two buckets by capability, not by origin. +// Window/host-switcher operations (probe a URL, open a new window, set +// title, read the hosts list) are safe for any renderer. Filesystem, +// shell.openPath, installed-app scans, app relaunch, and file dialogs +// are gated to local senders — even the user's own remote UI shouldn't +// need them, and a compromised remote can't use them either. +const isLocalSender = (webContents) => { + try { + const raw = typeof webContents?.getURL === 'function' ? webContents.getURL() : ''; + if (!raw) return false; + if (raw.startsWith('file://') || raw === 'about:blank') return true; + const url = new URL(raw); + if (url.protocol !== 'http:' && url.protocol !== 'https:') return false; + const hostname = url.hostname; + if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') return true; + if (state.localOrigin) { + try { + const allowed = new URL(state.localOrigin); + if (allowed.origin === url.origin) return true; + } catch { + } + } + return false; + } catch { + return false; + } +}; + +const COMMANDS_SAFE_FOR_REMOTE = new Set([ + 'desktop_hosts_get', + 'desktop_host_probe', + 'desktop_new_window', + 'desktop_new_window_at_url', + 'desktop_set_window_title', + 'desktop_set_window_theme', + 'desktop_is_window_fullscreen', + 'desktop_start_window_drag', + 'desktop_get_app_version', + 'desktop_get_lan_address', +]); + +ipcMain.handle('openchamber:invoke', async (event, command, args) => { + if (!isLocalSender(event.sender) && !COMMANDS_SAFE_FOR_REMOTE.has(command)) { + log.warn(`[ipc] rejected ${command} from non-local origin: ${event.sender?.getURL?.() || '(unknown)'}`); + throw new Error('IPC not available for this origin'); + } + const browserWindow = BrowserWindow.fromWebContents(event.sender); + return handleInvoke(browserWindow, command, args); +}); + +ipcMain.handle('openchamber:dialog:open', async (event, options) => { + // Native file dialogs expose absolute local paths; never grant to remote. + if (!isLocalSender(event.sender)) { + log.warn(`[ipc] rejected dialog:open from non-local origin: ${event.sender?.getURL?.() || '(unknown)'}`); + throw new Error('IPC not available for this origin'); + } + const browserWindow = BrowserWindow.fromWebContents(event.sender); + const result = await dialog.showOpenDialog(browserWindow || undefined, { + title: typeof options?.title === 'string' ? options.title : undefined, + filters: Array.isArray(options?.filters) + ? options.filters + .filter((filter) => filter && typeof filter === 'object') + .map((filter) => ({ + name: typeof filter.name === 'string' && filter.name.trim().length > 0 ? filter.name : 'Files', + extensions: Array.isArray(filter.extensions) + ? filter.extensions.filter((extension) => typeof extension === 'string' && extension.trim().length > 0) + : [], + })) + : undefined, + properties: [ + options?.directory ? 'openDirectory' : 'openFile', + options?.multiple ? 'multiSelections' : null, + 'createDirectory', + ].filter(Boolean), + }); + if (result.canceled) return null; + if (options?.multiple) return result.filePaths; + return result.filePaths[0] || null; +}); + +app.on('window-all-closed', () => { + if (process.platform === 'darwin' && !state.quitRequested) { + return; + } + + if (!state.installingUpdate) { + killSidecar(); + void sshManager.shutdownAll(); + } + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +app.on('before-quit', (event) => { + if (state.quitConfirmed || state.installingUpdate || process.platform !== 'darwin') { + state.quitRequested = true; + return; + } + event.preventDefault(); + void requestQuitWithConfirmation(); +}); + +app.on('second-instance', (_event, argv) => { + const urls = Array.isArray(argv) + ? argv.filter((arg) => typeof arg === 'string' && arg.startsWith(`${DEEP_LINK_PROTOCOL}://`)) + : []; + if (urls.length > 0) handleDeepLinks(urls); + focusForegroundWindow(); +}); + +app.on('open-url', (event, url) => { + event.preventDefault(); + handleDeepLinks([url]); +}); + +app.on('activate', async () => { + const windows = BrowserWindow.getAllWindows().filter((window) => !window.isDestroyed()); + if (windows.length > 0) { + const visibleWindow = windows.find((window) => window.isVisible()); + const targetWindow = visibleWindow || state.mainWindow || windows[0]; + if (targetWindow.isMinimized()) targetWindow.restore(); + targetWindow.show(); + targetWindow.focus(); + return; + } + + if (state.localOrigin) { + const config = readDesktopHostsConfig(); + const localUiUrl = state.sidecarUrl || state.localOrigin; + const host = config.defaultHostId && config.defaultHostId !== LOCAL_HOST_ID + ? config.hosts.find((entry) => entry.id === config.defaultHostId) + : null; + const targetUrl = host?.url && !state.unreachableHosts.has(host.url) ? host.url : localUiUrl; + await createAdditionalWindow(targetUrl); + } +}); + +app.whenReady().then(async () => { + log.info('[electron] app starting', { + version: APP_VERSION, + packaged: app.isPackaged, + platform: process.platform, + arch: process.arch, + }); + nativeTheme.themeSource = readThemeSource(); + setupAutoUpdater(); + + if (process.platform === 'darwin') { + Menu.setApplicationMenu(buildMacMenu()); + } + + const initial = extractInitialDeepLinks(); + if (initial.length > 0) handleDeepLinks(initial); + + const { initialUrl, localOrigin, bootOutcome } = await resolveInitialUrl(); + await activateMainWindow(initialUrl, localOrigin, bootOutcome); + startQuitRiskPoller(); +}).catch((error) => { + log.error('[electron] startup failed:', error); + app.exit(1); +}); diff --git a/src/packages/electron/package.json b/src/packages/electron/package.json new file mode 100644 index 0000000..95080cc --- /dev/null +++ b/src/packages/electron/package.json @@ -0,0 +1,98 @@ +{ + "name": "@openchamber/electron", + "version": "1.9.9", + "private": true, + "description": "Electron desktop runtime for OpenChamber", + "author": "OpenChamber", + "type": "module", + "main": "./dist-bundle/main.mjs", + "dependencies": { + "@openchamber/web": "workspace:*", + "electron-context-menu": "^4.1.2", + "electron-log": "^5.4.3", + "electron-updater": "^6.8.3" + }, + "devDependencies": { + "@electron/rebuild": "^3.7.0", + "electron": "^41.2.1", + "electron-builder": "^26.0.0" + }, + "desktopPrerequisites": [ + "Electron runtime dependencies installed via bun install", + "Bun available for sidecar compilation", + "macOS build tools installed for notarized packaging" + ], + "scripts": { + "dev": "node ./scripts/electron-dev.mjs", + "build:web-assets": "node ./scripts/build-web-assets.mjs", + "build": "bun -e \"process.exit(0)\"", + "bundle:main": "bun ./scripts/bundle-main.mjs", + "rebuild:native": "node ./scripts/rebuild-native.mjs", + "package": "bun run build:web-assets && bun run bundle:main && bun run rebuild:native && electron-builder", + "finalize:latest-yml": "node ./scripts/finalize-latest-yml.mjs", + "type-check": "node --check ./main.mjs && node --check ./preload.mjs", + "lint": "node -e \"process.exit(0)\"" + }, + "build": { + "appId": "dev.openchamber.desktop", + "productName": "OpenChamber", + "files": [ + "dist-bundle/main.mjs", + "preload.mjs" + ], + "extraResources": [ + { + "from": "resources/web-dist", + "to": "web-dist" + } + ], + "directories": { + "buildResources": "resources/icons", + "output": "dist" + }, + "artifactName": "${productName}-${version}-${arch}.${ext}", + "npmRebuild": false, + "mac": { + "category": "public.app-category.developer-tools", + "icon": "resources/icons/icon.icns", + "hardenedRuntime": true, + "gatekeeperAssess": false, + "entitlements": "resources/entitlements.mac.plist", + "entitlementsInherit": "resources/entitlements.mac.plist", + "notarize": true, + "target": [ + "dmg", + "zip" + ] + }, + "dmg": { + "sign": true, + "title": "${productName} ${version}", + "backgroundColor": "#FFFCF0", + "iconSize": 100, + "iconTextSize": 13, + "window": { + "width": 540, + "height": 340 + }, + "contents": [ + { + "x": 180, + "y": 140, + "type": "file" + }, + { + "x": 360, + "y": 140, + "type": "link", + "path": "/Applications" + } + ] + }, + "publish": { + "provider": "github", + "owner": "btriapitsyn", + "repo": "openchamber" + } + } +} diff --git a/src/packages/electron/preload.mjs b/src/packages/electron/preload.mjs new file mode 100644 index 0000000..2de3f08 --- /dev/null +++ b/src/packages/electron/preload.mjs @@ -0,0 +1,146 @@ +import { contextBridge, ipcRenderer } from 'electron'; + +const eventListeners = new Map(); + +const readArgValue = (name) => { + const prefix = `${name}=`; + const entry = process.argv.find((value) => typeof value === 'string' && value.startsWith(prefix)); + if (!entry) { + return ''; + } + return entry.slice(prefix.length); +}; + +const localOrigin = readArgValue('--openchamber-local-origin'); +const homeDirectory = readArgValue('--openchamber-home'); +const macosMajorRaw = readArgValue('--openchamber-macos-major'); +const macosMajor = Number.parseInt(macosMajorRaw, 10); + +// Preload re-executes on every cross-origin navigation (we run with +// sandbox:false, per-document). Two separate concerns to balance: +// - __OPENCHAMBER_ELECTRON__ is a shell-identity flag (no capability). +// Remote UIs still need it so isDesktopShell() returns true and the +// window renders with desktop affordances (DesktopHostSwitcher, +// title bar offsets, etc.). Expose unconditionally. +// - __TAURI__ is the IPC channel to the main process. Remote pages must +// not get it — otherwise any page loaded via DesktopHostSwitcher could +// read local files, open apps, relaunch, etc. Expose only on local +// pages (loopback / state.localOrigin / file:// for dev). +// Everything driven by localOrigin (home dir, macOS hints) also stays +// local-only since it leaks info about the Electron host machine. +const currentOrigin = (() => { + try { + return typeof location !== 'undefined' ? location.origin : ''; + } catch { + return ''; + } +})(); +const isLoopbackOrigin = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/i.test(currentOrigin); +const isLocalPage = currentOrigin === 'null' + || isLoopbackOrigin + || (localOrigin && currentOrigin === localOrigin); + +// Remote pages need __OPENCHAMBER_LOCAL_ORIGIN__ so the HostSwitcher knows +// the URL of the Local entry (isDesktopLocalOriginActive() falls back to +// window.location.origin otherwise — wrong on remote). Low risk: the value +// is just "http://127.0.0.1:" which is not exploitable without the +// IPC channel, and CORS on the local server prevents remote-origin fetches. +if (localOrigin) { + contextBridge.exposeInMainWorld('__OPENCHAMBER_LOCAL_ORIGIN__', localOrigin); +} + +// Home directory leaks the OS username — keep local-only. Remote pages +// operate on the REMOTE server's filesystem, local home is irrelevant +// (and would be misleading if consumed as a workspace hint). +if (isLocalPage && homeDirectory) { + contextBridge.exposeInMainWorld('__OPENCHAMBER_HOME__', homeDirectory); +} + +// macOS major version drives window chrome offsets (traffic lights) — UI +// presentation only, safe to expose. +if (Number.isFinite(macosMajor) && macosMajor > 0) { + contextBridge.exposeInMainWorld('__OPENCHAMBER_MACOS_MAJOR__', macosMajor); +} + +contextBridge.exposeInMainWorld('__OPENCHAMBER_ELECTRON__', { + runtime: 'electron', +}); + +// Note: bootOutcome must stay writable from the main world's initScript so +// re-navigations (host switch via deep link) can refresh it. contextBridge- +// exposed globals are read-only, which blocks that update — rely solely on +// the main-process initScript injection (dispatched on did-finish-load). + +const addListener = (event, handler) => { + const listeners = eventListeners.get(event) || new Set(); + listeners.add(handler); + eventListeners.set(event, listeners); + + return () => { + const current = eventListeners.get(event); + if (!current) { + return; + } + current.delete(handler); + if (current.size === 0) { + eventListeners.delete(event); + } + }; +}; + +const dispatchNativeEvent = (event, detail) => { + const listeners = eventListeners.get(event); + if (listeners) { + for (const listener of listeners) { + try { + listener({ payload: detail }); + } catch (error) { + console.error(`[electron:preload] listener failed for ${event}:`, error); + } + } + } + + try { + const domEvent = detail === undefined + ? new Event(event) + : new CustomEvent(event, { detail }); + window.dispatchEvent(domEvent); + } catch (error) { + console.error(`[electron:preload] failed to dispatch DOM event ${event}:`, error); + } +}; + +// Main-process events are read-only notifications (update progress, +// window focus, etc.) — safe to deliver to any page rendered in this +// webContents. The events themselves don't grant capability. +ipcRenderer.on('openchamber:emit', (_evt, payload) => { + if (!payload || typeof payload !== 'object') { + return; + } + + const event = typeof payload.event === 'string' ? payload.event : ''; + if (!event) { + return; + } + + dispatchNativeEvent(event, payload.detail); +}); + +// __TAURI__ is exposed on all pages; the main-process gate in +// ipcMain.handle('openchamber:invoke') decides per-command what is safe +// for non-local callers (window/host-switcher ops yes, file/shell ops +// no). See COMMANDS_SAFE_FOR_REMOTE in main.mjs. +contextBridge.exposeInMainWorld('__TAURI__', { + core: { + invoke: (cmd, args) => ipcRenderer.invoke('openchamber:invoke', cmd, args || {}), + }, + dialog: { + open: (options) => ipcRenderer.invoke('openchamber:dialog:open', options || {}), + }, + shell: { + open: (url) => ipcRenderer.invoke('openchamber:invoke', 'desktop_open_external_url', { url }), + }, + event: { + listen: async (event, handler) => addListener(event, handler), + }, +}); diff --git a/src/packages/electron/resources/entitlements.mac.plist b/src/packages/electron/resources/entitlements.mac.plist new file mode 100644 index 0000000..03649c4 --- /dev/null +++ b/src/packages/electron/resources/entitlements.mac.plist @@ -0,0 +1,35 @@ + + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-executable-page-protection + + com.apple.security.cs.disable-library-validation + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.device.audio-input + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.files.user-selected.read-write + + com.apple.security.inherit + + + diff --git a/src/packages/electron/resources/icons/app-icon.png b/src/packages/electron/resources/icons/app-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c3f9a038f17fe5305f67038915c7b236f002eea4 GIT binary patch literal 24006 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 literal 0 HcmV?d00001 diff --git a/src/packages/electron/resources/icons/app-icon.svg b/src/packages/electron/resources/icons/app-icon.svg new file mode 100644 index 0000000..f271cda --- /dev/null +++ b/src/packages/electron/resources/icons/app-icon.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/packages/electron/resources/icons/dev-icon.icns b/src/packages/electron/resources/icons/dev-icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..04ac8d54096e7a3d66a5d9ffec0d34e7399ace29 GIT binary patch literal 173094 zcmdRVRZtyKwB^0H1`8hC-90$L-QC?GXo5SzgF6Hb?!n#N-6goYPv_Otd#`GyrsiXQ z=A-G`ef!qgeK@`M+H0-0GO_;(g2<<|GGSo_fe+DAZDg1WiBTNq6I#~fxw|wAgF(r02>~#fk2QsU=Sp* z2md>l1M#2V3gtll*XMs1iYtj!0THlBi3zKEfKRn8EY)LG35)0!Y5|SeGNF(DJz|Yx zn05HRpQ&e%wJR4$r%o5h_`=MX%-XQ9u*7_VzaVu(0)2`J&a+bF{diw)Bx}ax+2@0Z zpCx+8vRDgo+aZta2-UezX#m@^wUJXygDKoZQ=xyqk3%YKFol(1yT}QDnF^*fvIo7YUWi3GI&_;r?@mjvE4R=s1Ri2U;YM7FOJ1tPyb-qt%`MSx8ltyz_+ zg@sH676Z9}fPPI)4V1XGUWZRx1Bb)9uGL%qhZz0EtUUz37$U*`K%N-!X6Q~{=f9)4 zQqs~t7Z){CIc$zcp#~aC$ zh%r4`tX3}9=D_83Db{UqlUrdZe)1!dnb<#Gs2r&>>PZM~MIjPUWgXd>CA{iNh8h3~ zDbaH9sYzx5rw4z1iO9EKYYw<%fJGrRt;HIy-P`VqV6~hiK3w@znIYi4%z2#nIg{5_ z88_ieDt#ml6=xjm4NWw-b2lz);?qx&>6(-EHl5`LJ33^1&IC+4b+}zizNm(+tn_p` zTwGk`dTTZNjXL#WMPW+W6k-&@&x+sPV%_(K6KNw@X;zXHg$I-430Xj8!pV$!Kf#bN zl2LdZrqlUSB2-$oF_&2%%Jti{sXiY+#FB`T^YiOfEmNc39xo*KMPMVUL)BR3mJP*I zmD(N|?k=Ju!A)1Q5kP{v@W3ve1_c*I3faT6zg6ZM-(H`wnaA(A4?hs_lIyj&-QIY# zxSg8rk7s%n^xb=hejq@&;SN9Gc17K#Q7J68Z+NbqI4WW?g58`cQmAn|)z+I|{^P&| z{N@*fcJZRTEtmc3Bw_zk#rRkK6fTFX)fRUJ;D1Enu--aFV(y81l}M|01xn1wpr6PV zydNYZa8;s89^t^Ind z)%5hV>iBxwcjUj|z|;*8YM@$o0)vjaduaZ^-Rfv6n}k(5r=4|)My0)_%%V&Vk6$ri zFY8>6Fr??*NviE)l`vHUrCb`EyQ|wt+}V7&4(qoY%dUm|JFM?^XRA%l?{D5s-p>uA zDJ-H~f{sGN$xKW;12GHkyJpoj9`$_v{ZaVGrr++*^tSS)bm+?+K34H7LOnn;egqxO zmF}mW!b7<`1JgD;bk-Y=VV&^GgYc|9mUF%Ph zm6AezV3#W3-8@dTioMAvat}{|AS5pz9jzoUMz2w+KbQGQL`OUduR#0=@x@Sr@_4>H z-h3?0)6a6Px!!iYb%||giI58Ad&-ok)+%!*pS%3k)s@TmX6vOWSg1$ykX*wL686J^ ze1&?sb^sH<=lxk`pKvHLmE;HLNGm(47(E`z7Q-$G5+*7U#$;PE2C}HzQ0PtwVG9oj zcjp&`)n?aHSg3RSbN!75yYxESCFOy;pA<5)S{2muPT!HSK@}R{j(CYT#PLyG3kPGjA{h;uZfH!yr6flKR#Z_JB-@w^~Yf`Xv*uWy3t`YcHpnIjP7XhO zM;mtwYJ`|7QKJh)<@$bi!lL{-B(lsR-TZ0A*RL1r|J*D#@l#u`1 zK@~Cg${z==$jC5T%Q9!d1!L&WWg&?;hgkxmJN!cFL|T~I)@XMbjFaKVzBBH?DVT3W zQ#{16#_c`QTfe(3$!^=lz+nk0^1E18aEr`!8dN`|@S{=PN@kJ;@WAb14+77}6uzhJ zFMXG`cGVhGyzf_Nn|PqJE6v!kIf4v~*&gAMQ#RLHO`X+2S7gwYTXU3Nv&(OL!!j28 zNXOwGey9$VST`=oVZ7z-hDHd#upPF!GVS_ddYy(*m$TI+AZ_*#pBK&jX8F{Tt9pi| zj`2n`}I&5Aa#x0uM{cz3B+RG8DyIJbufA|1!W#`Yk-dWk$ z{M5NQsY-hL;Tq$y*(-z7kGd|I@Sh9ztIeo#>S3Npf5Z9<>)F~TikfwC zvhqh^CG~NOPd#a?ClpacJ3DpD$m}jOmAtSk@x^mXQqHeVaazLLcb2WNC=ZBWg`cczbfI?RoGgR;_dgDy=%%Q zF!6_ghkXbQ;z>9iW-Y1n^)?Q(aZ~E+Z}h)^|L*Ql!pcHWNavKZdayvaTHHR3jsJcX zD1g%0&j;g&1h-V?MN%Pr0TUplL@D9v7jQ{WLqpSDD@f?U-ftA@uss`I8$9)QWxV6D zQRipp10s}7D^_8r6n>PE(YJIf=PXns&lwY^ET*Tz@yx+H2^sH$UIWNmpUDmr;ZEPG z(@*eP{<}dh;}tO6H_WN2sl%ayGAY3Gf9RcZyBaciH~Bc7?Jo@W6RuZ!wAjt=G5Zu% zEu`7g&5(YTLx#(-azuTC=RyPmH))DL5T$=I$V0@yvBpQfsmQ&8&a34VaWiM$r=g<; zk#GCjSG(MXP6`%x(r&d7{nl(@rZP2N97pPF$Pw+-fdf78WxYqG*{&=_P-vCSJaVq~ zy231TO3YG@XeX;vsN0v_a@J- z4)XDTDJuS>$iptf`H}3|iNF4VNU1OKVih8W0ahl= zTp$qk*#8F3`T>{u{Rf=w5ilA74*s9OS-<}VXa9`=l8lCO2O?nlpKvx4x>2nUYhVDt zxVI|IN;fnJSZ)FSYkD{-)D08%U$TRPgZOk5x&?VkWa1FejZ|51|W|9qm(96?t7xZCrb4=z;R zd1jbw5${X$JuVr&ji83mrb zgwU|AT0V)hW!>m53hMBi6x`9;5WK@B_||+(W7wv&rME+K}*FM9$BLkK5BKwrSnF za_>oZ3ZonPksX|7K2|P~1BK|}Sp>U`c!EAJzB?Pkm%0sDLZcgElKMr<8;s?Tdxml| z><1sj44P*kXVHIRp+x@B(RpxF*)88~nvY|hA4NLt5PQ7UM{?J%( z?B(~YQlNuopZJcl<{G6w1d%C-oYC*4e+Ba*i%Rwd5_|}arPvO~`OZJP&udvU5&6Eb zOqNQ-5E_}j&g(b?zq5^V=dHMJc(3IM2kHV)G!UXRYb0b-TG3(F&vC>UI_JP+*rPaX zXAWVuob5$*r7dMll8KvTnaF+B$p=7GT?D=_mp+B5=0Dn*8aD&pj!G*(?A5RzRcCDb zCqJujGwQRGMT8e4@%MD#fNUF6l_^Ge!Z(*FQJ;%E1DPbx1B#J^6RE$8jW z?u$mS3OE=aqtIUB6$EhGuT^41V!Mt5j2|>zK3U~I2{(Qt3uC$7i4nZ}eo`Hp{q5*C znR!Lsw6uO;TgGS|N|2W4USgg{n)TzF+sarxrJ}>$_~vz@VtTb@NCW#XC`0DnVr*~@ z4V2I8t%mQ@VbL?AUb7fd1`x$7(5Ad(`%WwW9Y|#kYP*@1;dlCyzGmXP&{X>?sUNK4arvL8J}e!2w|1UDWq6u8p8` z2BQ@C**_aT$0&R!%#~)R)N5a`^=3DmM;J)te2#L_O)qe?*+q5Gl~U_9ayKenw}yy3 z4hxcBLh$WzSR~CZ)9o5keP3@Es9+`?1Squ@;*c!hTQB?ZeXhs29DWWw9ao+kcwK%$ z{K*w#y1e&-f-Zfo`?3$)=l%8menyr}t~aG1Nqcz1 z`+hA4iduJ zYnkKv0?&(fm6SGj7_!&fgM-_pSAqnKE_ardlCeKuP~@{pD<}<#~j$ zg?{r(+K`v*;3QfOosb1;OoY5Yt64<4Y-!4Sxd;ft_2zofB!k3v3!JwwcO+JcDJMVaJ->@pmY*lX{{RPaj^;*Py+%LfxE0g?LSik5FUW`<5cAh5opECU|pFybD)I{_Qfd}jysWwuIG zT_XZj(N#}Jr4ol9(f{ClGxtzxV-ezk>VzBrqrqLm2y$RbFlw;l zQgbx-wTJkG?}DCt@F5|$X&CmNFmLCIL&xc#6??wQf?c>u5A8C-ERXGQ{gF`;FIECa znBuWA`kHRbOT=&;$y5ee%JY6S5FF$*P?p;aJhp<#x)qO$y=O%bO@c@FLj9g{t;QE( z@&q4%yeINB3gc0@5h5i8quq?cf1`++<}qR#*=t~#8~k}>3|J0PLDbQ7hY-twn=$-% zniI-(eN<%+yn=D5@rtP`Fr@lW^5%f!CWE%44L<8&7fWinhCD)X&)KNmw#d2^<0f znqS8_Amz3__ob;;oqCk$KRs~Xu*)@?y{m;K*nM2lxo3lu!_J^N_l|PX(ja71amoV3Ka?K zzNp-knd!RUEm`oLf*rM+-IO=PH){A}DTzZuTZ_`|d5gM=X5>kV{B;|s<;VW;9Tn%T zV(HkTHDrXtYxOM2Ty;tjZLt@)h=w3JC9Dz4V|>>@?zk$I(30*Vi>?+alF$70ko2KlZVTrt4*8|@+aJf`ggIsQDT%Vi?H@*=(+w;3 z_*a3HLab3DZ^_fp#kLHPTbi6;MLEP^ak+>>J0R`FgiVA*i=yzeFHgqNm|{SnrQUO` zI*P_G1xbG_=wXv-ERIRl%OPVH-V}sz1g__r;-%c7O5}b_fshw!K&CihB^A6-yx;w^ z{_R*5gbLa`uI%{G{O-X}$DmgzEr-4lmMNEwYLTC|$V{EzC!8r8(Yfyb@Ydnskrhw7 zX}Z4jrt-n@%d3o+Onkk6;0Io6OZHSJ=1tS16a+uGemuVMyVI$c+9%kVNWUSuOsQ>A zkIg_7feXI#)~^^*zY6>(>MlAVQIM8DvjK|^Z#Xm`7Us4D zT*4976D1=E6(TZ&nX?PgAgHjNY<0MF8j+n@NSg``NA1Q#wQlChNN~t!6o6TbB*B^K z{gYTL8vYtcoo?w`eCre{l+WNtZ5gTXH2c}@=~q`eP?7SMtzr3!<%AmWhQC9FtMxrB zk|u-5xxSL)X1+M+p+HK?nWTTBH*%lCwa7s0XPUj@p4plB4cfH?ZwDCeK)Stta_j`_ ztJF)PPFCiHd`U#yHAWw^b2jjY53t*>@v4DWVbvQ!JS95Pm5JaF_|X3a8C(x9+&?y;GMu!_fZFOXh?k@Q@SF z*mc^ppJjh@FeUsOU8Ag6Jv1=O^1SW=ojn|ntdVWv^UN6MitQG@ku%t8O8}?22_fT6Nl0JEcka&Ayrk!xa9W|A}s@OYdY+Y2<{^eT?_5dK&bgWqBMM21K zPFhYnLV)~@em$M!Aj~I>|9*7mMZ}pM*1mC?J_vU8Td{AkL$7CbD*s{`t;I|TZhXC4 z5!K!$;|~_~F10HBE+$UFP6=;=y(!Wh1&2%n61T*GzvMXjz=2U%?C>Gm!24G1&k$ug zcNF&O2vf&rsXvBRem@A@bcF#3q=Y<1Dj(oo{+NagCFNUIaLZWh15rqKmgEMnV1ze||4X4kLU zq*LblCIiAWZ5C{9nairrX8ra;fCoypjB895(2XDsvA@9oTpY6m!hWSw_ zE~JB27WTb5Fj3TQmS^R3VFC?Tp=oa5*+FfP_jfP$7?>FMx&ET%N3%TFbZc-8T26U~ z40KMEj1Vp8H>l1Dq3&|KhyggT%Yd+Jec;H1!8!-r> zqs3@luK|9#&aG_b^#czw-`jGpQFU}NKZRR8pbnZ@x=u+D(~oo5gU*c>Bp%Lrw4e%F z^qNZYzgkFx;n?!CYmSAEs>0_=F3+B)|CEr5OwG%N^TUe$w_BtFk(7j!TWh$4nOXG}V?D4m6O~C{l1y)x_s(*~MHs6Dj!+(jF%!dN(zE zTWcMfY`a6|)KZ(J?}OoFZd8z(~5n`3ivRGvdSIbK!{!Jmn{- z{1Bix{L+BcJDNmg7P|(b!WZ`+O8L_QGyy&9qNzi!tjYM=B9_V#_Blropg3ykpsqnu zSUx~WIWfItLxsopg6qE6>Je=eCnTDjg=7^yH+@_^Y!6fvAC~5!;ai!^{lbufVh@DP zTG+|0HGu%jVtUQQ9>t!>9?wcZ)C@CzNd^_gE|7z?Ir^JCI{Nsp64j2{mJ9mcua%v_ z)JUZ-E`Gorq`~`P&O?EZvsufrqV>MIAa1@0T0Aa|#As=TX&+mu2+jf_2nFkPj|IP$loE2|RWp6bb! zgUZe7husW_GAVRzW~IzvOJF(hYax&ee1!8)oEg@$>97Z|AlTnhglAGp#S>_&Lkg8c zvNG(!@DgpfpOsh2V(J^~FS@|G%sP21{cBSaLb zz@bL6jZ~>TL^mzw6JL-pf>^=vu1J2~2HVa|!mKEfYR;~0{#$B@M4u9vuyIX0g!5LF zN%EpZ*;40K;fNdjWxG9bqa&gGpJ5t7-PRVZI>ho{A^ZvyaNgt2F!K|qFjT}qnVuesj?!e>LVE6I%$o?l9Hvk#_` zTZI^f6LzfAr?_vPFib+yDU@GbSDWly@hxazjUu#kp5&$)LO0s*1gB z8wRbqO82efP^q3J^wa%05#zTUdXUr6n%PBb`NzLWwgpnK@bPXQQfD&t+XhG!B(R}^>DrE(vy@DFp&*f z3=38zv;wRdD>eysJeNpCJX+%Y?FU>^@c_m12Rt8Ke{a3FVQ0aWXX6)Rbx5iq3CY%y z3PT0&2mD<;Xw-Rebjb^f3s4?XnsZbA_fPysz>JV7gWS{6y)loWoqIr7y?MZv>F z1EFKp#8{fyVe;#f4EVIGq7uu34eQW%27Y%~lq_g`eZ@uyTizzdJcRhESxjtmytd^` zxBREhhFSXMxaR>WGTArT`t`l{VgJ;O;)QxZnw(HU1u0uurO%aJ@l(#d_g-O)uR=B? zw7}H)nzM!^4z2P|yh}wIsFy8?*=6Bfz}fqDRVDb8mdM|dYGK)5=@Ke2HL6~4@Llp< zX)ikAyZ-yYU?twH7-+iy1C1MTU8^FV(f*)wETjArD(EcbRqaFYI8k|PtJUTI4Kw}U zxNAJ+&G%&obOI6_>XUyI`hSu6ENmbU7vcX(=Kr&qfI^XVP-{8$ODA8*NpCf2n=#_!@U}MfnSDWH3tw=b_G)<} zmexs(4e;gGo9~9pRmy!DpTM>GS%#Ou{hDV}Q_~O=57p2R57oF9GZYe7v=jCJhn;^K z*~Oo<{Y6q-N-5UDAH2a8hi?x%F^6w2*As^WpT3ULU@EMNHTO?(G*mS7NxDvmNUDY# zxW1MhA9uW-I%F3|3C`cnYXVfXvJ(;Q8Q0nhYn|M_dSFZeR+eaQd&=4SCzD8)zZg^Z zQ|Cip9_#`Gk8FhDiUl3LOS8|9-ddkJMW0?%Dm3~~-)7qwL<;%dfF5op&;5Ko;>Y5G zdLCm@NR~{;z?g#;3wQ(fYUjlL4o%V5aSNed&EP@Hdl$NI_HDL6+=KUL*ACU%K%Lv?fn~3PCg-7#kf>Ws z6)-FEQF96!3OSs=94bC6&=&Zk6{9?Em!Py2zfa5wCexj@sQ(tF*&Bs|e7IdOz*d4h zMcz6VXu$EOdQk>dv4;q;K(Rw!NcF<$t8efsy}wI%GhhVtB$DP})^@xGw9qae%`cyKM9Z`s4?R1kb+&*MI)u@0&S>qSkLEb27r;DC_@^u>wA zFIt~A8OwR`Eooqgs}`nQo4FZ(@Nup=7(on6PkpWRCz@kI!BpXv^2X@POM=h4`<&L= z2;na4v?Sb&68M07d)qQ3W7lT`3w02IuoLm1Fqy-?>-+w^L0+zUUYHFIo22?UXc0bR z|6z*lN{1OL4H=9qs~;-leS`M8uDbAjOub*zGc64aH#+31N5iZ!?zF5)KtGOZi3jB@ z6R_a(eEP>XhVQIVu2O2T4oN6zLk3&rtswFtz0g-jSXzj90BT)hT1?Y_L%HXY@D< zBrC*Pioy|A&N9xu^V#71T)4`NnQY>}k!q!t4oWIJ-o8ysUls53$<;zl9&|KR*tkX&lxDd$ zv?BY5kaao>jc($taZMrpv6|ILH-;e&$QI5MQ1K;u3vriwHY7teq$J+n*aX_tjSjR8 zx?B7;0Z7!#+OrPf%`$jNEF}Lj(j0Jf$t!JBXUENRmU*$op9zrIjSk+WyMhMlS)}yZ z^(E&Et&9%ZJ9pY?*7fWC#=SKRT(c_-vCucf$Rem=Rn8}_3O=Y`L_E&g!6fr zC@aVd?D^!O1doV*FP2xjHC{+mI9S}vA};yzY&aXi?T*`d9sA8re|vz%xnBU<_MLUF zYq@uN=1^_lm8)k&uv^nCR=N~n)wn_R+I8uQImmgZK9+Zdti%naP#o(h6l9|i_6~5& zP5@&c%?C95Yx~*0>xY4WdKkQ762rgxkAkI4HAQ!DF*}`x|CROznTHAKAzJI#H!JX7 zYW3I5blwh0s2{yY_3@4Kls>PXR#I9bFg6q!|8F$!5%qmHV(_U7K+h7oPYS~}{ac1v zR+`VMpl{)oyD6P<(`elJy&Be8WjZ*1;_qL+=5@GlYF%qvXFf2MF9B+yTwU;{NUV{Q2|DbpQIF#%;J=Q!sy@ z?mb;rvjEL@xk0wWqpmM7uc>{fPfJfV4aBwMp%(>T&*b?|+3zQO-)1sB|E8V|5bf-1 z(O37PpH;M+IRjeSA043c&kI)4X{sIP9ocvNCmyvvz--hg;zJoibbs9el%zeP`l0Gp zUA*<5+isF?F?BR%p2%v{9AZ+9dr3MkF7Jm$+46D%JxFYgz(U!u1LTJa01OIb@@C_+ znuhTKStm1EMaJ7bHyFz~^pkU#&Ud@>n7L0ig`{fhkjo0J)rh7n+*Peyr3!0`FgZ#c zq1@{;#-OkGkj3qn5L;x6eA{7Us{r?$d_?Ee4Nv9rkMLaiI>Em-tDbm@cnW7j2W!vgL zEFY<^vhI}ckFOs_gV$_~&k##8cLLI{Y8jrydvN@C`?$9q6~e)zHb^;^Es;sYsCz)? zytdgDxJDL}BZ`S4dwIWPLG%ZRleh3K5!K9)Q}E>?u;ro~aqV(|=v?6GAnySdGh*_x zs^`O3y7Ic83MO_L2x>QAkFW55)Ho0u>C%cn#37r2BzeBPNEY>1qy4%`LJE2ZSbxti zS#*`Htgd4WOm+61eqj5K*SoI=_kmwE_mkiH#%X$-1Is)AQR;e*UC_~M{+c;u`1Xyt z8suqT$9cTbO`urvx1BbrS{bo2k;00Ky={T}5r4N+?LsUQO_&ceg8HY;zPJ&tIP$uX z-tMVs=*mwC^0+s-OJ$*6yIlwRLqZnNcQB_*P{7mxaN2&gvJTT3sFm{^-!AE8(c2=bA`#3y^Aav&nz6zT3NBbiuX&HFo0|Jd1BI z++P+z0LzYK@a?c*wq^|ITF(Gt*9G6ME^eld)AjGqG~smqK?X?l`YnX+<2P~D4iY3G zl_{1vtE^cU=iG1Zlj45rl@3C>K@vpXvl!UYyjI{H0&y+&TRgi~409?R>`d>wr8B_5 zg*(xBxxX)ut&A+V}xjEAySCA7vlaCH!_kV$lA4u8`gKi_LF|MOx~gt?p51 zpYm>mdulk;xZ=9l*K2LhY3$t&4stS~Fq?Pic5K?nCfXPpSA<>i@LzH3Sctxo(@rr< z(s~K&_(yMwPn-9}@v=Y~7D3GHaIH&nIi~+px$VII^8mwego%QM(1959Wp;jI z?wF5cBLoLBJTK~s2eW;--#sh{aS!jK3(zT^PlsjK^NZ9C5c#g!)z7I%ANsbz1xI5K z_qY8NTJpdyARdlf>&%bu*W1c#Zw18IMnJ(DsjfwB*Bf%Lz4;IrApTzMGBz9bjUxzL z?DW1KHq3s_rO7liq*S;BXuAY6o{{dxVB;We#XBSNFPpGVQ#?m#)YyBX*D>`k*alu( zBWd_i!%yU9`DM26)iQoU@p_=Em|z`;wz+hI^XB7<^&p-Eilm0j?V#7l)Zdl7y-i0r z;Ykp7?!V7e`QxZy2brm?CC>z9m=bXp~l!Bom)LxrW1uxBuGV3UEu=l zdSO}yE?3IICh;18ZooQu0pwmi(v|NM)E-4ynS#s|TJID;hp8|WK0DRf0)(^I6`*w} zebtnwkSV`Kf!N3?_iL0N@+r1LIKN*^U(b3_tRYtxnHIj!(HvrO&n0{a> zBatha7ohKkm%(8y(d7GNFNN)rh3}F9L)DS8Xej^H)eAKb2d^%EKu!^?8Ll=z8pI2e zfv6Je7RtHI{W-3D#J7bHkR1l3kD|0&!8NXJ@2}^+3gkqRE}aDvC26?Q)FZVM0_kNy zv4b8Vt=$p(Ad7AnqyD<4Tzz!rqN-}v$RdNZS*TmUI@0$$JHh>D1XDYADo{BQ-Hw0+ z%`|mqfG*zq&5e8biw9tIz@XVCS7xTsY^Vs@xb^i!mJb1p1%*^WC=T$HSO zjG7gI_`(uL@^c`?z2wopwj;fCL#`jBEo3Qaf@WHnE-74ONR?$>@*6#Mj?#I3YvZ^1 zM~3J!JNTfRTq%KOxMfW6=?kt}CQy`zQ z`u)63&6lhE^B8J&&kT+b8@sqRt_Vs(s(lSmrzw~76iypiXXNr9lLj}ke9O^}y@nY~ zewhMZ9ccy)5)rz+C6Aq`2lsr(~t7t>6WXr7;R6^-37vRiE;o*LKmzBj8zp#hVyVhq}^VkhI~H1)~)-@@J#W;C-Z&~9<;}@dvkPI@O;4_D7JLm{MpB>oitjYtqm zZk4VihU^uAs{fuhw=hSeqCPO>ogmM7>oxi#+Rc}Vfu_}s+%r}#Myr7@0!a8ZJIeO_ zKMIPnq)m`=a1`r(2;s6}sLdSGUf%$VNr#C;+P#flVXQ$)6cht5I~*dKey#<*i8PvQ zh{G^9HST8*$AHOl#rEK=YVUti4X7L?SATC^6hC(?Z2Sw0oj8I1wAB8uEIUvG5Tl$4mn}j}`dvD2 zw(yd6=z9@j9%RA^k(jGI2IWkRxJNoD%e7=v+16^`t{CG+nZN677-z_jEiza{cz=OR zf9R58Q73C_()=7)yXc>zYYFDJ`RVZDiU2`NWk=zGwu#DEDE z?^L0oh42tGpO5&sZqIDg1EiBT&?bcIdQ)8$s#MwP@wvUdkK)9dFc1GE63h1Of-{7J zD$;r#q^(L*xw;}ZG7$PLLK`?{@a(Ky2Os!g%- zP2Ik|$;TWiM|bl_0Sf;KUyK+ zh|q`YaDlsY7iKt|PkWUKXjSKxDuHxKD28s4Z(S=*r!CgiPzthTu6WMfvx5B|VJ1U< zL!2^Us3nhX4C@7-tYJ++^&qxga4{-~%!b8ZNkcR_R)YU5A>yARPd>u%N7q_H?zoeL zu#5ay2g+6U9yiqliYNkc1Oi9A{>q<3G(@#5Tv9LPHmE?o-XlhY^OAA_4h*SY&G$e; z1tt=y->CTO-R^*t6T%7-=g5CfN$AHD>>X0E`-S1~Lali&FZ2fW$!>7VLU=Dpv3)TU zEm9A$a1vF34}*o2iQo{`^9yN&#!lgq3TRI{>u$XiCYZJZ%ye(X<%!skMzkVv*9$bqBStS^B4q-bF4&@!0^ONnZ+9E*`YHPm*3Bw9(HtRlK z?NLu#eMqNzkCp->ez za#p(H{vwJH3n;y4*SfDKi?PAuP?!5^vptGH(3s2d3vm1`eZEu)*0iYnGH0pDI|tZB zbZ&IpL^k3Lr-|VEdBCU&%T{f{SJXF1##4j4hRW#0k7WG?JO>8gbfE{p|CR-CGdmd{ zos|_>X>SO!hR9;^*)3ydM!TrWv8R;mkNTVr>p1S~6pJhK+h$5GoeajxwbLQ?%uN*# z(?VM2)OE&ymzySD?>pXa2|l}A)w$D#`vUF28sP3bBzK|pC^pw#*Dm5cY~tg=y`?%H zY*82s7kpnqjbuN2`Q59HGrCikxfEw>*CXwbhMg6`5;wGn}Zq z68mx{Nf%v}7oL`Yie^Mq*_sFOHgPst`$l+W6eMj$#GBtb*60rj%S!{zlLQlF%}1Gu z@_vshKxR$h|GUv;B{L(2+is0V>XdG%@WkB^0?Hif%5-;EWs(4}A7&{fWv1#I{^*zx zc;=RRd8Hm5)SbY^HZv6qHOd4m9?=wj!5yfU>A0N+YK$)tADn@^<2!_Xh<8ZU7wz;% zMd%>#MSzSmLG{gVNKoAl|IypcozK5-YJGwe)wA3>Xrm_hH|Ik;1YEA^oVK(Z0p$}O!yw^IR-c{!7#oZu%hXlj zdF*$G`(p@V4v>blc&Z+4G=Q}je(#v!0hw7)z_$rD?Hjy5B`1?9QeKry5Ievb@It&H z2_`WnB0Q>VnPD0@M58C?Z(#V;*Ot`r&WP)>es=yOGyYS`zW>*dkw`|}D4Ojl9_{%Q zLp09uK0xKnJN;rf?yl<$qi6&9wkF;!dheG6p)pTS9pb_ReqITxTV1AG@h}bhSf1Wc+_!UdJ`*t?s~R=+i>_MHkF3^UR5AQ!66? z9NZ(F4wA*V+yuKJ1NTfUSvbdv?Yf}*H40n2x*QA9MZ2l+N`#39{jv7i5F(Kt5dw|a zz)%kXayxcX`kzeQ_E9g?_JIv{X{H=0gd5WZG=IoG$B9vJhTxC{6ErsI#b9Eab4##OVjL1Mn z_Obgkha)KRO9W=^cPcU7%o75AJ7%5UoJn@%-zK%^_070Y@irTt%b0S#hiFfW?7Was3w_^mRpE4|#o zf$R{4Fw?`Dh}loSsR`36mp~BC;1URx_fD(A`DAWWV{T$F}lgC{pZ(} zs*VpsK;uYqgcC$8pt&iF`@k-qoAhuETX3&fHOkEPnW}5Gx#GG7-TUpmACzIUVkhK! zQxgC;<$yahR=+`%%tb=={5t!2av|Ap^zh_!s508h*F@W?aMYKiq?6q>lF(1v4ClM} z9lyMNqn71W?q*qw_=T+C_b3ul6wMLLq-6}`ygI{AC3sAoc!yU@m5TP3{x}$P1k>G{ z%3r}%lFI35Q6qy*Km}W~aR~7`vwh;xZE>?bmaHZc6}@Ub|@+Tj7O`i3%Wv2^GC%-%9$rQ*|+K6+r{WKq13Sb-GEWE za=&QY0VeCW1fsg4xi4|m)20zL0bv666gi0%V8OJp#y}bWrY%^^%zGcVq})pPt^BzJ z`VtIkI=Crr`BhFu;}cvj>5DwK0ZZgHndv-CvrbLx#q}CBoDEeN*7Y$GASK=9)P%Z~ zQ98glcK-b5(^y!vqMbNGW{r=3aNwuNdn!PQ%5_hUlyTy#yki44XSrsfRue>#SlY#bTf0TS=gt* zk)|*3Hcs$&8E(|AXMFSDP-U8g)11!$>CN4l;V*pDy%FmzTyFaynM>!7e{^u0lgjcR zL$PIO$1ARbDki_ffTrLhG-en@{tyBlVxmqcOwo^p`*7n`J`vEP(JV@RCphtH>!s$L zpz60BqW23od=B+mRsFCBH4( zKr0j$vjP)AD{tH)6$5T^Qy+%wt^tL=0`9mIN6~&a?e--n%@V_n2~Iq- zy%Gty+fHcg^~ek4PF6C8&Q_dD+{a78UfvFUGZ{KjzP>*kkj19uy7v2qHVEQ=D4#R@ zhdEqBAxA4br>0oU`tmW)1(j7VBXfQ_!~E($NnwyQhk~1$r0A35)%99PIY_cY`CVv#v4sN>tiJtAWjUy;i? zJXJ+UEQ-)aRODV&DY!_`FOg;xmS3u3Ui=G>yF@8VxuJwdRSPlDP${#COK^)9S>@Lz zl5t+8}9t7gV;2zMDGKe)q_;>9CV0aU4fzr%L`69ja#*yX#Xo@BCrSQ$!)kFdH>Gi7I&~93BD0wQz-> z9K?)A6IKKkWMvENCr7`9?VT`A5xn>?bHg}S(VR>EB6~Hf)5t(C(3el-w}Y?vNUX>y_4CX9TA3|{dkA=Qh+FgTum}q{Oq9w?+$a+ zBdzEx&PWKDOm7FWc*Mm|ZcRk+;nxZ3%J?tsh&!TQn67yd0o3_NufS{Y0}NJx z)jkyQEZ=F}Osgzp7ln%Vk#Q~$@VHK9oK7pzLYbkm72)Ixhh8G79BRgnFgZv^BoGAV z3$<3)74$+=cGE+N_ex0t?>_k4sQnN2-a0DE=xrOF8HN&RrI7}e4hiW{LcpS=Q%XQY zLUJetL_k2gK|s1ux&)-Vh7yqOu3^re-}|0*&ic-`&L3xee|%>x)-s-fXJ+=ZpJ(rV z@9Vy=>x$v&k&D|d*K0nU8*$Bi_CPa>ACYYn=4RsO2_e@ZT2^?b)4A@@7DPH*K|u{I z^W*ahUy*Wuqr_{O;Z4|o?_rX8G==B{oZazoP*I!++XTn-;>_CWfp#4~V%a9#?>qiJ zbmTQ=^WKOPo7ynqI>BaKjd+Dsi)*6r)Pt#qY6&u$iW?Nqlf(X4tk zCHrdOS9mZyVAtnn2I-yT!G3bP$CNsLFv|>;>N0URyK0rN#6>Zz$(`=;zyp$wlsa{Z zeQWC8MgZ9r{kcR>P`)i($CV}T^m);-hm zVWM}fIMhjGiIXqqQ$1*S3zG-zNxxtX`6bh7}BI zpAn#o>=K4

h7|bIF|(IWG3|ayQ-8GW=jsY_SC48N+D9|#b7BAJq&5E{ea zCv`S=5+c#2*NFoY#_D`5?lR(6DGHx$^zrGaNU40Nu51czdCKCQb#}@sQ`2t`6C`{& z_a%K|$`L?~wWy5m#K@0v6Wd@=8cH>tr#|o~MQ{pgL`?S2fh=IXDs&ADqR&j@3H@K7 z6|ZZ*X{<9c$cPhjO1j+C*~lsM@OS6!Q{5wt`{wMeY+Py0YzDC#*zLrL7+XZ|93vf0 zXgmz^zZ|H&zfhLfoh^;xQ8^4I#bkf|Bz2kCbfQ2Le=HAr9Moxjrt2ru}Dx`>R z?Zm2j5}2XiT09)8f(p*meb*-XCG%6oC%^J#_8&RjTCIQIW?w`8{k8Z(=&K5r2gaAZ z*5CNyU6=*-gWD?`dB&G`R9KxFPP!I1efLSg0(HkQW{RZq7OWj2Fq))AEuSU<+7bOJ z_Pa1#AQ0Rr)dxA6?Id@J!2tXJ^09Vygj5A{Q@!^KX4dtBOywEwMzeSKzrl(4H&$6g z4SR%X0-Vv~4W2nQ_=Z>Z2Kk9h&*0$j5u~i+qf87Kl>d`ceLvb`O&!0CseN(0K1PHZ zeGQr1sMcbUPcz3f{^mC}A$Zv*(D=>DAETWlcQGH$CU1dH=(~yO6AwF^2Lf=F$DmLC zJbNW4&**{a6T?K^FE@Q7F^jfnzk%CXfqO@^m@=H!{2el>L31EdhJeRBeSY-lHj(l&kxYKaa zR|v=#_FztQai~9v*%-|HL~9mW<(o-Pv2{pnCjqK_YC$@uV+&*&?Exz8GUtQ#;=rpp z{-#bWc>EN3cnSnH!pGh>UhaRcU5b1%9r=W3Elg~gYL3(xz9qp%N0=3=Fpv%2nc}dx z7$)9VSkiLCzB7U;Olsgk+qufP(5tgXJE|u8+O6efu<^I5#XwyDN#1pHm50&-otISn zPyjRxR=f&QHGA7;xtGCBeTK>UV}_{+P$f74d3uG!NGUlZ>(~K`n}~Ze;4-%{$u1F~ z>>~1qHA(?_0)WVI5mcwr@uW%}`vC@894}b*QG`-7&$u`Kvx?)m{v;YgpO#TZh z7~X=!OC8c8)?gaE&3NDUu1YU0_1GDM_i6fn(%yObMeEyj2Ypt)$^^qco24Doq>6NZ zFO+IYq!AT_SZQTKT;rgc>TeO4XmQhdLAGClhWio}S#9?S_8;#1zEl_cz1R2$_p$$v zHD;wLh5o+&=)7h9o2`xK;sl*i9nkTsqj)i&CS{gm$0HuU;IQ6ZI#Ra?_vZCz_N zFkzhh0{QpxqqSiUiS75qnRs%UpekdMztH@mQ_hJ=Cb6W_6+8_!AYswE1*o#yfOi){ zfk|w+f*SsqTJ+TgY7?Q5{#3!J^R2m_0AgTX=pifvlTzm;IG*)z5cwNG{x~twnY9d> z{TWmHpGug;&6Gi{V{Sl_Mo4u8=xx~5ldlthVUo9!pnP`FL5;5&ZV&PeyZgz2@i16z zA|7^rHebPwg~u2`#5bz(@)sr{EeQb3BBMs{zk60EfXLU!4T>%7Q@xHIH}6c884Cr` zkTu(5Uz{l&Og6s))WK=M6p1^xixZKX2Nd5)H839NyPHQSrAP?#Gc;Mbw)JqY#agDKeGd3jvUj%5TSScuU zjG?Zrgd%RsG9DjY!oJ&eh?+qRbd!nXbZiSNFW(?Y%@VA$*CK638f6nh2Uh@B^SBK9 zk~2(BGfJt%;*Se}&(4ocVwY!J=e{iW-X)HDJz@m<*E3u>Ab=37w@ zSdbr0da0DR?|fJytp>!ziwCRu#SJ$5zEnz@ZKZuV+=f0VXA$EjNTY%ol83^W1XE)% zH-^OE0K018(G?NL*xDN05Ly&ISe>(ou#TlfgqVZm|svPEos_@*I3h4@{bo6AB$Wu1C&7=0wyVoT9SrO z@<$GMy+Q%dAik-t4O0LU#r^`cSx*j20)7geixiNcBsyyX3D&;`8qIi&2rlfHhXos@ zEoL{ue>f)y_F)L@dPjegty4ZMTXVJ$+g*W*vGzblCfP6BgeH*tw$tbrU+xbUOg7=+ z6*%xqem}_6xJ4j(XA*QgD&>T`dODK2+$?*u;D~x{lI)Rq_@p|QDj4c1)KVbFU|1e0 z1Z~wh5AEJALjc9QH@llSO9IXi#6Z?|xZY2GjyPiRNy&z0w6VW2ARmxUrA`nf5{M zM?T26a|46avARR7AVIk>uIw56*tRGj z;sM6>m{|c&z5CI-(|?MR?tqheqzt1dJ=~(eWuds^lQ+8~l7YEfc_ZUA)P{Bnfx2t7 zCc+3>f$6X5TA}wi+inr?-6$Y^?(YRc1kal7XyO1nNp&qhS=r0D#~m;~nUVBYD~rVr z8Nuvj&!urjaz)3`lO~W6WmU#m!N;0E3(+`_`9S@e+e@wY^OIshfgnDaF#v^c-bfP4 z6)tjPs>-;Qjt0uli+XtLyDk2*w}!n6j_>Mm8TWotYx}U0ymB5FZ+|mG-Ld6F@{zeB zp3;_(u0*b$KA8fHfS!f9sB2QVb|Hk5F1x4{5n;DG-)#Tmd(3{DVnRi{{3c3e5mw&J z!*Jp0x%M(fXC6TqcU$U=Kb$C(2Q$Yp?bgM~Os~o2n2(}|BiLk$;-2PXg}zx*(v*~# ziNXqKIZW{991QU9Q} zeg5aKGi4%n-EO=3FdIX|AC1&|(j2^j?RyF#0i;4N;8%8#a+&gKRh-M=8o>Xs|x!=i- z?bI`vT0YHW(qmG&S5HtW-$Cu^V0(QHjVWH6HfjUlB!3>deDgrfnP@b=CQqm=yYR`#m~uN61Q`FAqu!P{MbRAEJG^Wx8&4QF=8q ze`aR9oIr!{D$9z9J3{7L()0208nQK<2~BJqW2J~*8D@hEg_|+fa)*&yvdpvr3M?No zCR?qn&2ytWl{mf@qXH_d&q#PbMh5YRp8vek71x>zcQS6s^$33)=~z9Hloz@3BNEHm z!Q4Rbf}6CVnDz>LfKW-WhKY1GMCT!+P!BN|H_4rhktm91lk_03Hs{FrrP7Gz#lMb~ zsa?jIa31tl*W4-(4qy1gemqSVi4BEsB8FFqzW9t4@=^$!y^Edk$MJ7K5y+7McJ?{d zIZ46H{$TO8h#iMSb})dQ}{bj$%mFliy!`N#6<-@N3L*F!HwFAT)tj5<7HTXEA1h6Lo(rY5r=R{oRNv7{pL>CBn%IIepK57<+i zk-d!m3CfH;^>3%Dj+5s}ltVMPMxfBY8&1F7J~ndnd&8-lIFio4 zU)EFE-5;`^Ki9u3X*1DpZvh#3ksMl{SUg^j>>lD9sa=!zC9UH|!!{}ABm5C5ki1&i z(oVcae!^(m@N(z5AN)piC{8um_hfiPUuE7pZMbEMJd-qqu{7mCOn~ssk!$N*>p+Bb zh41c_&dH`WP;-6t z&7K^#OsUA2X4T+dWK`qQbNT+b)&);y_e^{1T~$&DlMDmpu?Xi(gsJrgs_q^Ys1K6(y_t+c1!n`}^?+eQkCZs?SUm>2# z)tamXZdArj^TJS1Ex3G|AnXImDJVr~tCK-xh77dxhE5FBRweRANjn_UEccQrf;P@u zn?X_o8>x%-nbW`yy_J3YyRA>u-oTwHbus;;_$C)T=0cn2gpU{%PdD|w(&lPcZK(Sj z3Oga5i&tPZ^!g{ars^vxP1=q}Gqtj1CI*M?ZOkRU-%3Z%S+l*>eT_eGTO(N+o!exn zHC#yk=yEpbZZUrido`NSH-TBksQajz_yNm|R(yHK+>171Ms9JeP6su9L_fgT?mj%j4y5_&5+_o6yqtNQH63nyd6~Dk!5) zE0nE>`%$sVuc>5mqi8IdQL_psTca}chI~gpN~W&zX{_-5 zpp@n%Jp&PyQ_rx4)VufmDU_nca7g%KjV||cQry=cj9BUAHS5rXQl?M~;weqkbHWUQ zQaX6=ZQCS=NK}A2@l4NLXRjZD=zcEHesa=FiE|%r1N6}E0^^ZA;d4?y{y>Dn#q>e2 zWbs(z>IrAf$BmZPspJK_(}{n;MxC)DDJbc@kNwBi3!9Pe&doQ^SMNw0iqu)1s4}Nr zHP!4A80`EYmdLqSta-fZu7(!b46^qsM#|~&BZ}_kqdK&vD{8WbACwnazdsrpTUc6$ za8Z+R=~|h-Fq4oOK;m2f*M+N(c`2WLy z6#_6cF|u=-$E1<{F@}@&f3f5EM6cO#|635~uGw+d?6_-o+%-GynjLq|j=N^ZU9;n^ z*>TtGxNCOYH9PK_9e2%+yJp8-v*WJWao6m)Yj)fEwX2)H#TtGxNCOYH9PK_ z9e2%+yJp8-v*WJWao6m)Yj)fVfLJ$aSQTBzR+*b&M(w`Rmg%Gg( z+}hpU+uPmUETlw0pjfq^(AfDf2!v`E9PCg)Ahei+Jvwl(4-WQe|EC8B;8zFWS9F+j z_P_z<4mjAQ0$-T_2@S2mLO=*83paQ74v+SBw|=tt|91nvLcT%*(V!WpSE$P?H2UiD z>Jkl_fxbYXu{Ii8wh(BaOEd(6zC@q4)|Z9nx1NGS@EW}lKI{_`Z9W*jf&R~H;{7(H8@qdxvdP|8@RW!327+>$pF6 z3-kK$3Vq!Cv%adl`2>x11%BX%#@f#NvF!&MhlRdEU-^O`U!hO^LF=!t;eVI=U~R?^ z9&vBm|EE17c%UJ$|FTCsbQ?Jbul`rqBf|b~_K28ofPm+?G7tihf&7;};$|{_-M3bB zN5{E*2J`Qy0gYaBPhzsTNM9)Z*v!b~;#T?-`JL5c^2V#oPBOW#N|P2B$}t%lRb>YU zsV6^&cVdwBD-uq9A5spI`s)mHJZe|sSK>ePO9;-L^|>|}nMEtez_0&Y_rU+c9=Ok5 zqW{}x%;-sQlaUXqGO}=I3J9v|ZK1DD8Y{Jd^tpGDowO}C(6+Ok+!?P*wE-eabmN6@ ze9*C|n!7kVu2+P_nfRpI`}hop;WD5%fN*=;75b!3LPtc63yKd3woBOc zn~e1s1u{5ZPPI2Cx*Dblw^$n!QC?mb@Yv8fs@?WJS~Ak#J#!7%sOsfoWyRfC|~@$<&j)P7?TzqhE{?o$%;?K zI&;nC;{*1ng}%j@gf$0=(|Imh)N=8r{BVIX60biJ2Ri$x9Zy$Y}foOpFUq zV(-3x({dm6E=ILd(o3{D2b1{1tsmo z_1$kl(0>-S-YqjggjNtllpc$#Q{BBy4SM#`fH9DZJHQ#UDo*CM(Yq+KYyGS=l(oaR z%0Nou3nRbVsAk-e9`B+`V}f>xMV%@8e$ME?CW(NMKzQd~`2dZzEg)Fw5=UHr_n z`sLA9+uX4F`PODm4XQD0+v20Tb}#~uH5x$)-<%Le@|fdHzQn4Y+Oso}`>YrFI}$=- zR*npA_~<33UioO%=A?J}PNUW(zi`kUZg6LvV1^_mX2|^GlPfJeH;EPEK|&T%&^P#c zZtVFKwiX;HsbhnuaeSB$U!pWheL0=H``$@5~;zWIAEi%%aw{BMZAtYjdk1&%#Zop? zyQU?dhA%kk9#KMS?l6HXv4XkqFHPS>mhL?fe|%ih*Y}vcrs*D65*#5RlD9SSX_Cmh zGWk-ZO1gd|aV2N{81rC1ql0%NPILiGQM`A zUq$z51;4_c6P$kt-)!5l!^)2N5?@T=^*tmF+|m63C71oV3H4L?LUZKERZ;zw1m74p zczlKwA$AA@Q7^NO{_+CaL(;W;lQJ7tC}YPK&;V*=uoCCz;Uk5;^K_k2#|)AowY2G& zCdPc~Rm+Z{)M10gQ=4i}&v5m*sI35>k6U%bw);?m!vmR~RlU`1R=<)gz&UDo z^qFGxUQg7MoqI!&rL*A zuU&j)sEqYT{vR6zE>{`lLx_|uuI5C$OzjW{{Mf{FPRT9bGH%~^mn6yNcJjEAk@0f} z2pDhB-OYf;u&wBm38I?Ruv2V=h88Ialr%R0B8Q+iYpgWeRvt5IYW=34S6s%(NQ>Yy z{?<6+(ss1DgeHkig`;X6SQ5Y_P6e^J;Ag^$Lz6KowM?s3QQ(oxthMD zVJpIHbE%Po{-yGNq6@}Gcvk>obo10B-~<#x)iaVIi((7X78*zD-5)rR(TY zDw~7>4-B*(3a4@^q%gLqC48HF8&-omt@4T+Itei!Bz$&(?6*QfU5A%au|*B;V@?)h zHLk|`iS*fWo1WomFY5hU_+(vc2x2}lU6na{wHPjaXJ9FniO+x?bMgmf#9*abH*)XP z#tv8E6BnTj&L(JRw!t2+@M$;WXCCnMVlGY)Dg<@$ylM^WPcGP)NA6$#9GNksLEKLm zaVhed>~qenJ}zE(=LH5K=0RkHS-}cON;2qEi>qm-#<1Pr*xX@(riElzBDzT{ zsB7vM7GowQ8TbulhzA!Zyuh>?>*J&jm)ec9O;ewkD#(3nS!8KnNwQ5)y}1}Z~ZU;og70J_Q)VHgQ^Ngv0>So%1O2U_%O`>z?FrOIjsDZSiikpe@iT|lK18L9mtBD# z?;ItRVF)`8q}W3p<YINLG`(R1GQ6(3x|I zP&3APioBB6m~SH$21SwV6{@Iv7xLNW-El2(3^e`Uo*=1Bkhj+LLGCi z(nBbP>llu)+QqN2u#3?s!&$j+#aY-8f-^qxGX|DXpY)oT3e4IC3Xo%o}ern)mBq_;}{-*!{d|=4bbO1!PYwFL;EiQ}MqU>cfnRbYRW} zB_S^RJ6V4k<%G`7Zs3C*>t;>$qmS3e_$kXBe|CD}Da>~!8gIf60W4PWB? zsR~{+HN42C1I<~O);-wt=-vRWW5tNwP7T6YY$+Rki!FUE|xXz=1W(|uHF5WbsQTrw6<$xg@jNBZC9 zGh(wKyFg@>ureiGeQ=&)+ULE00NJkpbds1;4-mC=u>biN=Ku)1T4+pRRCM33kNkPR zH8olQgqC%l))xAP3T!Zw-i5*5U{83X3Q?024V(^?vR@Bgv_+Cb-efvBur*TJWoj$Z zXR*SpS(@_DKyi{EWBB_ENVfLo042O1As8*=VVw7?1I-0M(e<=D=skl485k$?lBHhf zz`CX~nfOY{l={FR8+_*GY(E}yo?a;KU#;!hRUms4;;}nWS)OS_1Eu8UEIiP=F2~T` zga=M1!?jW51vSb&@PWul^YH+m$}*%;rWubqS2*uIXp`_^pUbH9ps?qI!Yi{i2Y0JG zcxip+d>6o^*bJkRdH}q;;y@_qMc*t@e4HVd?@1TJ%mF;qWYHDV! zwr8SlrA>TavUnoKhijzBK(7ObvpuVkf)pA6sb!QvUe(nEg(OF1#DqeOwb96_$KHR4 z5~eQtmB$Cw7FeHEz2nb({6zqDaUz0I&6oXLzSmBh*x+@MAZ#~9Re?p8M-XB}+|2}? zWkdQ{zABvD9;0lJ1q;T4gQmcCX<5~2R8u=1h}R~j`do>vI$|V!yU}BM4%jmtL`ff` zU_fwF0HZt|Mu;=0u`G(W>pdwnZWBa1(VycO%@k%C13rBlRb5Gz!j2Yok4vFeKzMm; zjbpTG6HjR$*qGbbtYxSDX;e>ssO}~0xS(**LclB9+6<&qMc#>ty%T*i!B-Bn#p8fB z>9i*rbyCz5ZJ-Pf@r4k`BaJpDeq}iEM~i4%sG?|Xw9%t)G8>qy8-J|})I3ec=M0^{&t4^m2F;3TeI-V|^r63A%dIsswjpIgv% z#tQ^VLYLlMm=$#M;uyW>{tFKWf!)3XE&;I3eYiIqJ@w z>xy?iu)2}W00T|6MsHLPu)N#Yu0PD$ObrGWZVmY4>_0a$u3C+!G9D3Ba}T7+`rk;U zKAP>~fSoZnc^FqhQJejr_m4tZXGD%*2m{odEp2)45SC#i4e)G&;_+E$2%~`?Z!hp> zPR>j1^kNED@`0aOj~6AMjW=Ftu9Zrrs8JW>zgc+#;q_zi+?DNIOvY`FQh)d-35X-V z24R1!mkf!TMW2YT0CZl^)~Ngf_#;b+%=Lf1xcWf>DY)-Mo`2qPS{xIKUf8yAzbeB! z4L$*5T!7_q=-d1&u=DeL6>E8T0!9H6m{#6f214LMn`fD*u0#4=zfM1Ff}RK_`JRDv z`QC9OpX5QOSZC5GX}KnRjwIvFCfy&5Wd$CqV1tGSvyhV!xo}cqN}*g4e8CNw(Dbns zk25G9^+_^N?Jo+2_!F-E$Vqu;;{Lq(6MNrs`T88NL!T+kIQB6T_PlwMimz!JD2F%e zkp|GWOQn4{SeCUQu*kY(F5!Y%z?$@2wkKL|tVnZj)CgTlR>B}2S6V1^=qN-e_#q@a z-B^ zKY^Nf(!l6*Nxnklij$@9l)+>+f~xO^ zy!mk(@@CPwG7A0R*z&nhz>Ros*vH3MYkUjuFAc$MP*rEZLp_oLLC?QLM7XO zFYiGnSm=)IGV5(c%ev+mNaqG3m5-QV0Ot+7#3;2!en%ZvEv_8U2x z5USVTtD`F4DVfEpxg=laUH<*V;Ii#82c&<~VC5WrlV`wHSbWddrLG-|0Y}@&s}f@m z-;?p^v)_6+MH;XuQs*?TbT74)@VlSc^GGpGm4I*JxV}83XqWnys57hytonJAqpkT` zcIDsTh@0*Ro977ij#^!AnRQQCmO7-wUbVWK^K0y8PC^xA!N+&V z&~HEAu;s99ose)CW26`loL=YXoJXFw6G=SEzv7{lAe?Ga-UbghRSSn9U>LdR3qv;Z$MCy8b$iiY|b!5@@a87mJ=mDu0qxc9;MI!2i z0mH=m-Cky^{#Y#wMYrEi72GX&Gi-B4Bsc`bt>0ig5hxE3Y4S`!cG^YPlNn~By5otb z_RoL<%&o>HyKRS_H^xO=D zLPyPGTW*c5H7KAJUnrpJFv`YN#3}Jq2m-{^Q-?F=Ihj~Z7@CfiPqt@nyNJTG_O+DK zRPjXHd3bj6cm0@AW_fT3t?Ku$$(I}x(ASTsAr#i(syd|~_W0KiOx)#BbK`lM-CG8! zKpFWpP~TSjE#))9E^imkhX{wmIX!`Jvb6O2mcEoe^F>MPXr5-&f0m} zWYI?7hZ??@0?VYYU=K@${pwO9aMcyl{*_xBe;FSt#DpsT^x+Pj51rV%EWx5T6D5Dq zJVCp(OrP~CtV}SbA_u@np+@+~?{NU~-ey}ub)4da(WvO5pS3}xg;cr0*i>%i( z0)Kp-i)B~c0k?Yf_>7WEhve|>$e`P<_}Nm_0Y$b*E&{)26X{#hit)J;=O?MzYI)ua zqRQHVRyrngkBFCaD$Ei0pyzB1NY@0U*zzCM12$C62p;4(&~P%(DtoehQVS+CRBKSy z)zL-bUx9natqHiek9&gcjx2VYw)D0~|HBQga$r#(eJJIl5iu(TnY(eSgE0`e*#MV& zfjU3x4Wu+&t$+cM{*VejCcY9%T!a^oD?{s;(h<+m_KF0 zA&Ag^)~|rj;Nhw78*1a3VixP>yEBBIwDni#8`~4c;ECM<7t}Y*{P&YG4KE!I>LoVx zJj1{$tgL12tERrLkWr2@WTLO3GwKbC<~V_Y5Q;a(?|hVv69vV>`N>zGnMWN-tC6rF357 z{ro&bbSg-ATm1{f_ym1Yj#c~lVFs#R=zZ_>c=Hag50Jdy?wrJgD$~E5{O>5l8TO^R zZcROKG;#O8AJf#+fWi~+`P$s?s!#05J}1`gGo+( zVy}T?gyV^d-kg8@Q{}wG@p(_+XYv|EgUeu}$A^c_WDWc$!HTa}Y4Z=p$M&Ov>FCAm zG-=x0E9f4_h2f?(HbUf`>&oeNbb=Y8N_H%aI5Q;m~MG$YUH^W_?fOF>kIT% z=P`+|`*y>Hy`cTHC|JWTM4cTI*u6D2RoxXtSu6M*Z-`2br^#Yr{LW}LStEt+xi{Ma zla;qHYHx}vV7i&cBjK{C2cF>ZH-v%s~!=RVTvZ_V++k& zO>yEH)8Uq6W=)0_R$mE*6Z5@H8M0%&Oat(=u&h;7->c@n`D#q{>g!i}t%G`x6Jh0> z`U;7|$G~eQ*@-iBnR%oEOS@NUa~D_}+`B*sIiChO|CKC7_O470G+{B%GC!XjVuehb z(XPY^6?T%1z6?p@t4m@XIoLeNJSJA2?*Ftl^RAnFrf_JiQ9_XY@-t6O=ZC-fs!vRD zrJP2dEB-?5vwq|`J~FxRD`^F0ML7q1Btyh+@N`^t=Og5a9OWKQoy*`QzUY2}E^9%P zT$Jn4tf#O;=Sh)wKdt{$m9Oq8BJv*a0f0i%oOjEi?mfxcL@wte;BKHILd0_OV~x|YcL4oz z?uxqjR&E_s&ND2hi6o!4G`^m`;H5YPbnE*#{*e%kZ;zq^*4Pww zfN?R&CU+PZjlaTy?UBaEwWFT6%~$k-VlBKjQ=SLC$!Q8{nIL=+d)^+=P~`T3wY>>| zB5z*zB~e%A@yvnUh5bY4^`$DDE7Xop&O7nDc+a{7WAPzoV79lS$x8qi{t79o#tO+w z*V8h9=hvuZvIaNYrP*h9uKzy2 zomWSGQ}El)4_ z+=Nw*m7fvMT2)2QsOyGO2rmygB+iy5D?I_s--eZ6%JL2Cgx+_fN1LCT=Dvw@M=q1we=kXn;8XIKeQ_86D*pFTKhd!nu$JffYoVhc?gf*@T&dp8w^JXRhX)5 zJUcFn|8&Q_lm58}*ueYSgh0oEVUOzH?!elBWWk#6UfoWSGFt3 zXz(-|5;1UGp$8d)uvXZWamS>#v8yF4B@IdJwUJNGE-;KQ)@1{~ob2bBkGE@C)#T83 z-vmgX2ESh7LeKzjplOV^{;4 z%|o=`(w!(=S)gscYO~tCmda&Gk!4VX28AwiX6~{&spH=y3hvhV~iDdyJwyqFX z(h49QXAYdJ9rP8iqu);cP*p0u*c7{UyZwTn#56JS^Mrw65NRyfD0=pmQDh?y>Gm9!s z{RQ`%ERG2Zsj3+!OJsqgJ~7HNuqJ`Qwu?YG!tG4atH#KYGyq$>!l90o!XoOn_9r6x z>E}XP`dO@MJkQ0x_2UYXjcvply8rMs;Agk3*r?kuk!+lCLH@eW`lGP!aY&jB7B10y zlTYP)F*?r^)4tXKlNb!tNp4B?>iLJGYBqc&h)$%UDR2T{ zU6!6M5>KmD%18G^9~E#2>^FE_=04x{DCH0mQkXLf)Xz~*R+IJsd^dzfq7+9c;dZvN(v&3ne`Lm84^gJ*0R)5*{ z>81iKrO%7EG3Q8!;b?&szH%ndLPt?GnkiYR9L3)EtOQ_J+c?T(@Hz+xUFK-sXadNL z-9Phtgn}vn8!ulJl(y6SAUBquVC61`BeE)$UVUH%kH5*A(z+T3hC@};+!ylX`3j+( zL+c-U1wFurB-^zCN2SQz+m2Qjn%@}Ii0o87p)EulYCjLQ{QcP~plw-r&c)MU&2}~Y z4)B#5;`*An!D}A)5Psj{^-?lO9<#>j4vH~2FIwr%FT7!7iHW?W!f$HZiW|zh^vixF zMkakJlE@E|=7DFe#L`O(W3T4?QKtZjp4s}4`74;EuyRayn-w=l49q;=evX#zEQ;#4 z1U=CB`OljQk9j)#f$$?b)Lrd<)kH{%XHnCt6bMN8_zyCAlEudht&XQ`d8-|0%H){O zX`-0+W`ZR;Lip^WqQArsU82@L z0MLzBiX}xxuKG_Tl^FPvAilIernS5E%Wc{LY!zPvLo}DiegQVw4R{|x^lb}!hn-}b z!R9TE!|6FN2Y{!(OkJzXBbo;`KkEnWXKLar+Ko=gX~?VXb;)v)n^HXIuKFk&2so=dYSd}MN z)F*`l{|IT<{EB*lbk7^#j9@b9N|tAI_>fny?!kZFqyGH2@>@Y_f_DNJ#Z$Yv5Ozcl zcA-C}S5BhtjV*c$5E|TjY^Pq(_f+yoL*#5RyZXMFxb!blCj|G6=?5JTs7*3u-ays; zSO<|T1@p@K(Ah8g0rXP)oh&cyE@>(mDDS7j5prMq_Ql8`WpE0&U#V?zKPM7blr(aQ z-8C)?~NVLuhj$hH2oriPMd+s*(z2y%1_?rL*5XriH++&y{TbB<2 zZberfMsxD`W@VgToWPAJtEb<6KDbNeetEl9X-CTq#6?%1sdhk3G+5;52`1-ws3^7A z{w4qZli7wj?~AP&DYVHa`YJac>8{qxrH?ZH%Q-&it7VWs4SSbAFJYBIBzgFZ15bg} z%8%gLZ^F9l&_8G}o4K_65m53JFl+xaa*`ku>9WEW6^>_Z&e@No+^HsmkWIOO9Qz!f zH<`!Tzt~=Ytu1RLra!fKO3t|>c``jbbKlNCj^XrqJo#S|4kaq(_OAmaZ3dy{&o_1UO(ma)x>zSvSzg#6$e>pvdbehuhhGd zXZFJB<};Ygc5P;A+cQlf;OF~;M{MC%na}C$q`USu5utdf39E~c z)d#*%1<25SZPOqzZ<=8G$L~KY(kgm&b=HsC>_NtHU>1mZCo3H0X6a|9fDhJ8 z;SGN3z-TdKo%cebuw4R@xZ_;Bs5w@Xk<(i%u!Q$eVVPDv1`-r&0!g@Y)u;~A=q+AbVp)B41xcJmZ9({^c5y_lx53v z@UGqoeCcau^(RZ;%D1iD#Nx@JV$vcenesfJGP?M6ZvxNe+gKh0{fWPF1dlXt&z854 z-r6DH;F2TT`eMQw7PZTztb(kiY~fHboKk|x+SxlA5C-Yu5lR9Ipa zP)F9e#{c+6{`x{ME*xh1(~``EYU2!K!G4ysDkt3XN|k$yvHDE*f(YMc3NC3qb#Jni z1JVCw@8A7E1`&W)j#zj%{Y}~rB4yDx7t|_a3NHU6YH7k7$yc9M zK`81i=K+9`8AkdQDUodt{;L*A9&)Luj-lYj;V~;13H;e-kH5xiwg+cEKlk*j6m|YF zleB$1p7ZgkerP}avy@XV@;rCP=yX!X=^cpPa8nEBHQ-1&x94sBI9nu&3_zIck&p_cV)xb1T077ho39b{|haunsL!{&1WUdOLt~zw0s;Oe;i%9@cKKw zBJ=9aj&o6E-|ylb1o<=rXLszN<19CKJv2YOGZ5i@pCR_Qa~|ZxEaj3H6}VYwNLz?9 zf2d^Gh3q(3K?nFqT||YL_%fh>PZ{so?O5GS?b?3=$qiKxol}N%(lsz>WIs*aX1~HX z$hK&jExQVRl*Kms=N8NI-R{|5)EGdHlg8z%loqDDUx%ZCA>(^!Bp+3Gj7XGJpVucZfvu%uV+&mQeo;C_}n$Ss&nxlNQsGUf#46>b&~3*CIw zRYk0+HCy2gBDr*KPH5Wt$Gxby6WRW*4Uj`Dd}zj|O_EJn$s9#Wt~`X(rxKE~r(Jh= zD#_TwcrhL7pJ#yI=ItT8v zK~V)EMam>*lgK<G2RJ?w-7eCSAG4yTcczKNG zoyM+5g3ch*Cr%5kGFVWWz%}j(Ig%h}y{GIobMmr<$iw57b|=CoLCAUm?%R$p6nZmP z3zVtp!({s2E^j{u6*A^)xVWAJ#302+P5iIr7oOp$CrLaj;m0RqQd8*5rE=qDm=jn^ zA5u14-cd9qYj-5OGb23xR$gR}G6Xy=NT^OoIZY_bx1B~&Qs&LK`uw+^eIeI_$lf%9 z1yY3MEf8~dwGFBSTibPlPhfB<MJ5_NUVRzKaBD!T1(b}l&CoEvcrZBU!=`?cT2nS6Yo zha&fC&r=0vRj(Bb8vB^*-?(A-6+ScU1SujOQhh5m=2GY2BXU zLwr24&Hl~hKF>5>YXqAC`&8-R$IpBXep{Siv9(9!#) zcz8(5KY4BaTJt^D-(TS)oKnO%?_Z=IE(tbGn+gU;)+*x~m#uZ! zGT3Kiu5w&i@GC4TpxnkYS=(b&ks@8FZ#lbgR>-ZC53?zqO+6}7DhvPGZ{Nq_@9-HS zQJj7%kjq=#aBd^`e)X9@`b?B8pXcVEw}16=)agYW+e^11x@(5#;z%fX^jC$b@FFyS z-DW3d*neBG6T(>(#O=q-K^rD@zN*3CdawieKiGTgps4#cd~{h>*d?S?KtMuDQ0WqZ zrKLjz6r@`TK}u=~r9&E|K}0}F5fF4~32CIe6zP)Ye7?T#JLm5+^E+qe%&Jt7U79yap2~F$mKI{tzOH2fB)TWAEkz~Zy;Q(J^P`d&9}Fq%H;VIstxZH9&9#|5tdBJ zwk4`|e0V~n0;b&5llsR1P2Sekk@xpGmJu&CbMo@Y;(ceDHbYO+@^ip-HS^= znf@cv_RT@3i#(F4j161$Dw+qI80$zCq+)o&Pur3}-Z&C4n_T?YK8F2Mm31Um+~Pa= zvJ&6z4|<07#miRe;JucLmDiQK$#U;lV_Qw4q~lJF2)bA=rmD)MF>CL z4C96B-t$r9tZ1;5ct5up!_@Y>lOsl4toR%Gew3OvnT-c9H!^0&p}glmx6iFG9?o6# z3hMf@UfYTKCScv&)t39b=MqKl4a_AWtFMIt*;g`PVi_-U=?SiW>;FD1kUK?feM=Qv zygsK~TwNNo%u%e&$9zzFAyCRMDu)@?Tt+F6{|wX>QlU)b+ZEL_)f3y)6Hn^j)`$;d z0BSNkfvcUpJ@{#9Y>k4A3UH6o(+{yIUu{Ex5;Fq_f>nv!WU&Uo?KKJP_>z{9*ZKPP z{tWlfYCE5$?(FEFB+1BZ&dJ_07GydQ!HPYg551KPik%a8fkm+uKJ#J!br0#dQ3uPN85avHh-5Fz$CCUF-o?^4HXVNVu;Xav)hP->MvvBWO;k3_+ptp0;dU*H8g#wD83^7v!IGx6QM4-luNqVcTH4%oTkIC z-$*+wor^MWWHZA1=*4%!8pg7?4L7xsh>Oki4O9pvAA0(MJW+hmd=AQo8`U}9MsX?5 z0{S#3{h#K$sH80Y_IbGbfSL#?2z?oS7T^Tgz7{Plu!Q*bAH?t{A8Yd8c>BcUi4Jb* zx4EpRqqbY=9G@T0s%k3rrOEw9I9c$^bT*c0)m@!46N{1yu{)WX*Fm;Ps-8SUG9MOx z@hjO}YWsO5J4fp@nDq)=@3CuwCxAXl{R`riqrourKXG}-Ws#Y9-IwMfh5yATYcW9^ z&l^YPR$uwIlj$@6?RsD&MLYA-u{ng|V>if-I)~OOYRNtZZ@EN2(XHYnhs_Decn4T*YW3dP%WJs&lX^R;%K8n_IrJux8(woothY z&l@573a0R6JF@Gxr~sSZkFHr8PM}m=S(a=)Bv)CQc*}s`!@W@gY}^b~>W*U0PL(6S z*TI!^C-*Yi^$0|q)#YaNR}|+|>;962NV5ex^-uB+B#Iu*>L6QvkbK3p^Y?-Z7DKom zG3cJ>396mc32ZJ2qEas(-E8BOtyiS{h146V+j$zwN01k4K<`tQoOuH;E7^c2Yn6o8 z+v8A7JR+OE)u%Ym4VbuM#OVb5&b(c~Uuoe}tN`AU5E;^{CRRZmBC}9^s!?x_$E(2ac>u-L-QWR+Ydb8oa zdM|hCZ;ixmqZ1tM!~JKxiWUCvWLB^8R*@<_J!Vcb7sqBu8E8M+3no`noU(}M_-*oV z@RP}2H~Ph|?BF~0u;r>hLq*g&9J*By;!8EbT~ZfYV2{Ux90ZkhL*SC_AS0d55S$>{ zMgY!Dt2UH49)h2Jf|XV0v{Z(-IflX%75Gh*(v4j%ugUEKDeN>s;}Ip56l2u5*?l}m zL3w7B7gC)lYhBS?)}WQS`sJXs=Z+#c|IS9POs3P)Cwq@vUj;XC+N{KjO4WPI*Mnuo>teG#W8HKmXht$Lu2zp(;ir*VkI>sJg26FMPP>u|n zDFBX$meMrtI(Lx{3E9uT{H#wD7aV*Pnz_GRFYEHk2e}uKN*4rKhu{eT zyNB^SHQ@4N{cLZ>O&iDJ6=(z7TzKUJ`}v0%m2kc9V1LTX(FL$_s%@N%hG?Cq4Hw^b zlSF~arE8IDM~COODCP2U+4j<1iB{K_m1J3J9#eorvslG8V%&xmhzj!Qc$;LjQf1rA zswF(F{w$tjxJT}XMtU!75`bk6nT+SBs)Nj-_V@^7)sqzi6~A>t5jG0_RIst$hW5Ri+FJk>Lsq`sqdr4GsB zL4hWG$D`V%diC(0Z)Rru93V~#K}i_Z2swf{z1N#});!by7K%fsUvR4_PLj+;P+foD z{da$h_f`wFbiwBlGh%Db5Jmz)wt}NiJ&Zwudns{z+xgdAp6SK-2cb|yf@3iHuY(^9 zL<+PE62)B61%|%XXw@(#tVI(5!|!xIx~eFd&B2<}IAfl&j0b_eJf;DhuREI@(tD085vz z=k88~0mXm_7#M*Y1E0&5HYUG(wjC+WrWLZ&(PXgoBRM9Y@Zji+-;8QhS3>YZm>!jK ztrZ_0fh9o-;9HD%RyFBIO0>lUrKUW%eTF~c#SwoqkS)-PlG4>m)AF)p!&L=m894W@ zC}o^fpk%9AruTN}_!Z0pVbsT`99?fjdeSi_m%~J=McFU1-x@rj|0Di8FD|VwcR5`x zJ@&}$Ip8a~$MKBAr>4ELJKHQ8JsqN2m_^IfV&40} zuF{P~ts$y}c()C!olLWfW)OD~wCuhlr@RN|bIWmw3orE}IBmJ&KxRXFdFyLHT@|-x`Yw0YD@#_>9{0gNjB>^7Yg6e(e9(ke`hi@rvG0=N z5wG)v!dd@h{FEs%jAkolv%PqRSP1yC$m9vt$OPe4=~sYP-nwv+ILsaieD`#tdP zi-b1`mbbtnVPg&7GNQF0TL> z%Q1ZbY~7kxHI4;&fQs;E^bZb=JbheO((>9vGEN?F&wdm?teDu6uOi%Jizd3-c_cd6 zf@DH6#*_WPZ{w@HQ4a7&XNb;CnThI!Z5QI8r#2|5YY1k$Yp9`yL*7p zrMrN>m!2xD*YkZ>Qk8PV++-wp3E^H&KQ0#&cAYsU>lyZN;OI|D<|9|Baoq!T6R(%{ zMKK5tgp#1j1eNKHiw(E(E_N8uL)nF*_~pkpQZA)Lj1)ENM=8BVIs-NqaUrqSdm1TI z&6aDz+&q#>_ogIG0Msks4yasn*X^IhlLo7()@H|P4VF`ZGGhj!lKCLQnrhEisdM?5 z!GIgo=nHKDMn>f(+Yy?NAk+~hl(`>3>@dYV#l{9TG+IQY9Tudlry$`ZCnRQ~L(-Sk zc&Xv(r2)~YPR=qG0{i-?Cmkz^cw!3~TAkWjk}F+Vk>loRW8^c|pxL$fSO4JY+VuI^ zXIcP7SFE>*zD7h>vX*$7J-P72`-sW(1>bjKB^sh~%U1CK0*pJm+Jwvqr{fva+{S1y zSwoMvicd3G3IiO93>8+xfI7h~90 z^aLf+5Ym&;`Gmn*M&!ym()dL4FIBzSopcgjP&SIOQsn=w8h3E1!mplCs9t<5-OIVa z0gZ7TG{%nOmo3|AXOd|~-uMF8VlpiFf%&}`AdDutw(T@1;JPv>XHV=Q4RilMBVdN4 zej&pW^gwdE{Dpi);2K`yS*2sDF2JRFP{f9M13fYl>^T&&Oct4$XXx1DDeqnN$qIh3 zqquT&H@0OkN)-06eONWDm6qhVVAcN>t4=||Bsf1+cK}|(u1vle2i-esQ6zI;6D}L` zap;yjZJ8mZd{9m87FMIa*tyQ(tFA+yPs`&wA{4|b$ae7AB$IJ%=<1W=*{KU7H$u_b zpeoBL_)Nl)5070wiqxUBfuQ^S#i{dDp(j-b$DH4o^bNr>aVTY{UjflKr7a`YmVo5y zGxke*BeR^|?em{#kq)A@ra5`H2KYk{eS8hIrvZKBBai7Y+L%;{-m-u3?ObsE^m3a=oR9C=G5`pO?XJKXW}Rsr zBW)-&_ku+UiMS&8!_i`3JUbeCOSULCy|U8YD$rr{8hT5cR)@;1!wKsE*Wmi=ak%J3 zvzMWG43=tUW^-oR&3vR$8Oa9&m4GAQ8>QBNo+{$#AUe(%YxlF*7WtvejqFtxP0*3# zHD}|+%l?L-%QS{NwpK$OS$ao7C=7%WL|N?CJ@NGeF+tOw`|oLEz8$R;$q~WR!kE4j z+nIIuaWKygV9XtwJ#EZRc|6wSDLWFw`8|I00(!kY=w$eFSAFgWk6g8BPTThcAK#K2 z*&6RG!&#{(T%#?(;U5#>rga>e4)pUkBmL{mAc7^NRs}?f`FT9d%(i+E) z+eb=-w_tW|ET^W76pD?aq}PS2IJ`G$JCYKWx-?D-<`g4nX4%iqPLBz_RGUj(0CMrM zXYSZvQo&dkYV!f_)2yqNa)rtSqhQ}*B>8g*D!iF$2(ixAM=C{n<(O{9Nq?7cNf<6E z6;qrBAu7Gp)M%QK??LhuQ)-7H1X2*mKk7lTfNQOA2rW0kj>^JLvw>BdVK#`aWMeq)YZV4RD%Usp#R~xha=Sby;ff!Rq$l6w?b)0Y}grMkoT1C(+1}kSQGD7oX5dRXb(F1ys2j6aC^8%$= z3;(F_)DNO$m@Ul>vQ?69ulv%cH1knW(e$XO&e zW(%IN8-M~r{$Q#a+FRju8FZ@HUKN);>P`@vesS=mZ8Wg>K zg$H!zwv4-Rd5XbREmj*of|fQ1DLQG0W_QC~e*c^E5oSIB0N}G~$dSCK#F}&^9~CvF zkp-(ki0)VV^>a^=j=oPhVx(?`9`y=W`?g*XG>|zVajgr?W@@DTJ9jUC|8St+T!#z{ zn5NXd%Xkq-BXKP`3{TwlePz>E$hQ+rc#GMe4a%C8E6JE$?vPv8xX0Ttk47mf=OySq zYG-h1!baQUSCQ4t!~|_G=a3k2$_hpE21($5%#x*?x`-N3IkyE-S^u?cC!T)w(&n?fQR3G=TEYv&ylF>x5i!exd*z;+xO6L^tIWQpWt-c$15uN zTI+(>RfWCrE{$?kC%9tUgN+qK=JmsuCG2yRpHw<3qX#tHEa!An^tL`h#cF;vjBnUE z1I z7eG|lbWUK;cw!3s_??P9a*bd{c+5%rXCM7UqlPZ;59Uj}dr&DC24 z+tzAWgIDwT8K}3$-+c4k*2W%Azw;*8&)8zXdhUQ_#jQ0CU+jGN^qZHf)EVvArzgwa ze}NE3r}Pk(d>b?(xgMWXBU&+(ODHl|QsRFjj6$=Y8o zPni(Dop2j0_P68vg*9y8G@sWAE-j^JGY@BCH6=qA!=p&ce_&Voi%MnJ4TwBXo1?j| z>3!jqlMO6dijhm6f-pG}M#+h%po@iQfDV4&^F9Qr+oft>MloYl^r{G)P8x}}K!>C7 zDn&4>OjnK>`@$-q;!!T@isl&|cKmtC>P`EPGp0NbwejzY8p@KD6g%JX}4jt33w~C;}m473tVL=K?m#G5i48$DWG` z`GBZ%=cnE(gZyF-o&T@vkEX)%=)%$bNE1d36B@=4ajbcLqxR27>6gb~B+`nER!Qo# z3~ms?jgq0Jut`b3=xY*P;NoRyzOt-1LK;CY)?edi)l$M+FMTgTY!SqKd3F8|>Yd=L zv;wcm4MVcThYejKUAQheFGESpeL^J?aeD2N!fpPoofLk{FqcU$h%js(_h7MT)eDZE zJ)`0A4f-t~nCo-a5%OD{;@}Mi0oy}Re8*s>ub4Qdmyssg#nAQum@x?^oHguvFJ+M)pBoWZ zWT-YlAvYB?Hw91hr%Te@d0rsyD++M&3a8n|!4bj(AKxg2IeyyDUz}tm7-Kgl+7i1! zO>()C&v}|o=Dw#&0=TiX?LCLYLtwA$RLw>ZGs=kqOQ5dn(<^~;7yxj@K=m_oSuAtx zG&fo8Gu4zE;&51@=gs41DXN=!F_kS>3`8?eM=mm{_7Y?(1Trsb8BuUrLA$(WPiKDw zH6xB$!`mjFp67Q7;*{Z}zv=E%&t{XMjtnE8y`e0Y znNMUJVfud=UAp+xOOZn?h|Bj<9iEcE!L#0Mmp!NQHvuOdOlm)f!3sWZSM(uZ zUFFv}nE9b}nippjW zQp@Og%4eB@f#q5P$^d9yy#0cdf>1(?56o^0G%#P7OxoG;s5NIylW;3>BdfIy>N=~b z7Iv8aTL0!`^a!a0fz|>6axMYfR)gnb%Vf-jbu5}<;KL{E^nGR_m&4TC4IZeTBpEt~ z0tjf^$~ioB#BOE~&uf+1$Rm3vYgunjYa_*0vcwac%e*J8<_CI>2;VQ=IL;2fwo;{9 zZM`=3i9uj($9jybRvnQ839&@JaSlc6B1XO_kk8Q2t=o-W{VqRZdUs|G#B{gg`Rum6 ziQp4-v-s4lcPl_o9Dy_w>$8aMDX$G7h;ZMy`wF9DL=l1pM;yYYg9WaI;#D%g+2$*D zZPh8N;&=WVh5Gc)zo1@uC)xva%VZ7k9z1x_tJZ00HMcBa+s_*KXAWEew1%|7d4K@l zYxu0?a`!Y|R6Kc1We+1XoJ?L|ILoc`=~>8CG4=LUc*qCgcZWs3W>-Jt1CSNifEkyY&o>0W4Dg$a zE4uGkfsSN{J#}6>-AyejZd<=g(UPxtUyB z63Qq3D%e{tQu$9nVLx3|Z8%bn9h>)q*4+>#1>FmM-^%8LxW<#&A_27lOdP zPcs=*lVeJQaX$^htAv4zRs?EQd=GwQZ=@L4NWL1~{N&cr&}qNO-pU!MF#ebcNan8t z-R&yIi9_dq2H%@QV2|#p$Bt|%_6z`3kaQ4$eRCOfh$x)E8Q(`h?v`9yL&dJ@bi8pQ z#rmx3TBnZ;jUS-shp*wEG)EViu9AzUK+T@7?sgQ{ZFl5@kiXjqCem7cFY(`HbQA6g z;ekB*uJ(#F|RebC5^sU?2I*)0t z!W$>N`_A|D$YJi?ZAf#Y{W|$1oznJ74rM5M1zD=s{Z2ZNz-sqbU$^8;2X~2}Bj^HM zsO<*Tlr+$N(anf=6;?<5iv5Hx>#^w3uiSJ3bm5t1P6rSCkOMr>c?3Ank?h!@jxz}( z#1=C~7xQ4h&U1%l+6&OjU+)}6fX<6;=8s;ZJBpoI?b8E&8Wi`0@$B(fzxe3fq0ooc zhz`I7YGAFBGoX#nm3zH~`tmPoZ<EJ~Kav=oF7QO$Vy&PnB3j)(~LAJ@hCWNLq~8?z0M=(t%4b*z`C2% z`eNWv)6#NP-&;D$k6D#Rk*Y0%cD@e2eWJOH7Q3C}zyY*~E`TH9A-4Z5ON<GFHjb+(bYG7*)nW#Of1s^h*Opz!dsR(r_9$oJ{i>>++&F`1Wy_}F3gpJv{1u- zxjgl>;zt))v{o6uN48B@q^nR#^{YE%bi!RvKMm=2*Bbxt=bl18Hz~=Lj+A4;PTggU zz~5R8u3*$TeDD@h$RN2Y_+4zrcC7M!{K5gyrs*6q%szz%&dZDV+A?QAk4r^Aby$c9OTOmY+e=(pQB;Z@i8gl=wfAG5h zFN@|0Sm~Gi=n4t`Bc{RL{q>F_`<_3Xr9;?Tn$01qe@A!iZ@OXgWbO! z=R52;Nzs{^{v3!4tsYsY%P=1E%AjP|FQAu;AX37TE!@ ztbal`mf*d06cnfb_q8$&4?Qcm6O8bJV7dR$lnl4~xGiTa^00$;klI zy)C%=<-cFGZ)DDXpUGGMYo1uF}0{C7PpLLi=o?6>g$ zE{9NXS7@@3ez1cCgTNoOCz$RINJ8)5@?p_r0qf=e?MJw$COWg2bZ(HSrQccT^^uJ1 z7=+jle9Aul6I*gdDEmb0xu}V;${!aA!sN%Fb5;NM*H$4CM-u~}+JqP2JQ*r;Fh7ouM&PU+uu&UoWPXFD@ArQ6Xxm(BoCO{rK z^7@3+iT*c5ePBjHy4A)0qj4pdWdqI7SpNNn|9*q%4w&P<(Hrmn`Dl3NCOFC=@tQYKj|Jd@CeRZ;7@+~Heu9`?BjYK= ztslsiB^2;Edj5{o#00v)?lIy`*$etYZQZ{I-^Yo88~6uk9ZLHeR;WM=<}-$`aIU%( z{Jw{mcR}}nAm|}483oLlE}+3+4XDX<_QV$Qn$+Py6fA4qdd|4i#Q*pB=MqR2VL}|r zEv+AHK5a`ZcK8@>4h0xjRu8>?fi?=p-%Eg{C<3l8UXal80l^erB*7|0hbSkdIuJt| zL4k~FYVq}T#oACmNN|QcM7x^7ZG>4_cu|QLOe)GDjZ+@54--a#pg@j=t0xF&C0ro7 zd?1>T@YmvY@TDW+#^;HaQLZLae@ z)OaKOqdtj#d69us33&*WrfYV(#eYEXTPt}h#oOl)4BqYb*pQnCDQGwNJ%T~}CnQn= zb$dTN@rgIqM9s++fDmX20yYa3Yc5C0XnV?mRHE)l(Z|}tLr@P@1ptF;_?24|iN^cx zN5)Z+!eigT3Sd|QK#2&TOnp9$TDc7{^Hhss3`U2-M62Q zp4@VMuypog4>ztaKH}{nK3b zDk4bQM}*z!sLheXW;z=3+*cE(G(&qy`Q2{kTzd;q?>v`+Z zsudmrpdoImD%{YS2l*5b2fL-e$7x9Z*4=*XXe00ylt9+gcms+2t*Xy0CDi+KfRnh2~a&_5(GuncbS2zW*b5zG&kC|SX1d^GfEsztdr{4HI+B<+uffb#?Yhaj{aL98`DAGtbsU&A$!W}IX~#o=XcRm}F=bl3)g*A9J?IOtIB zg1FFkkK{jyJVG>{s+Sr`P_RIK)=%c5i3Hvr0?eb*G*`Ei7>+o@mVF1^ew3@aG}dzT z56sBo^zqBdLdNcNBq_j=t;PL5$y4Cp`#=1YT?yOpts*&C1-L}&fvicsDCn)YB;s&y z=wyn?D;hAm!)Ule3PM9H4w$fg-!D)bntTN+#iN9BFW-B=XPSxL+q&&^GiiiQZmm;T z)zOp2X4C1y380UN--7pSZa5$Q0r3mS%rQp|{3s85^e4NaE}(U^rptz)F}=VjWI7n+ z_pI^ayuGSvaBi&5S2H#+KfjOvPPjzBIT&?&BjRYZz_q3B2Cy&HjJu;y!%+C-U%Tz- zh)Py|u&6?1_LnXw*MIH!5Qq_A@bJ5U_FtBH;A>kh^H}78&l#iNNgqSx;>js= zbwUN??=8(Q-GQFO4^WdyGiMSc(%w^Seb`US28(~bqgYZmOfXpda9|f;TcipFyRs87 zD!1g#TpA)tkp4S_GcWk|Jpr*0xjOdmB-Dv*qUtsKk~&q)Wl8QU-(PWvTR^-4N(RXQ z6tW#+Ov&d5;UnfqnjDbN$nQKq*qmJ_5s+4vq5K#GDswgi<(4WtApPZBbwa8x-U5j4 zg6NJX$lwe&KLCX_@xLR?7q+JE06_J2AmD7eGyescObjebGKLE7i)M-E^lb&Xs3|yy z=d-+|Kx_6=R^UZvulyj%7D>zpLQoK40rGPim|_4Rr$TyCy}`1S3O=7F-QExnd;uqc z-5iu4X?Ub6pKM%xF~)H6ad}hQqT)mGp}4A`xk72g3m68Nbv}} z%jN~(92vL8rx8q$%Zfn&UMf40-xcU4d{g;P`UDZREdc_XC2-S<06;%TJ4FC`P+85| zn|JU^YMW~_v(O?FPJs>YHShhyu5IxK^N7Uh!HrYF3^o)fTRN6Dx7B7+aeM5Ar-Hsf z9~VGE`odd3QWtX~PPLZ-H0e^H>mJ4Sm`WA;>@NZa7-ixzfPTqsLEFJMJo=?r`P)yy zl`n2^3fQ34_1fSJU@P?QWPvuUdC+gmoNIulkxizcR!98w$?WA)ufOY7;7!2Z0;3Qv zhlZnBpLSeI02ErGAUHq*aD5+*bx|YU!NF-02JCvs~Fdmq?tQ!c*lc& zx4fxPrHc5o65YXET>3k%m(X0_aWsIBk4&9k(|YahtOi|zdj z!< z;(a43G^(%t-Ip6oYrr2q-oK&_HS9@rY^E!&wz3R!`G_`Uwrm!)9B284@ztj-0x7W(?Og*K^Q6Wd%t* zQ2W7U`(v6dAnf;xz$tWZv!8)7`x`y*@wdbDM97#ylL4y_V7A}svGKSJk!!C{$Ho*_ z+qKYAR1tt)S;M+piC*MYyMfUOdrPUp$!*5puaj=>1@=7^wcD3j3!ms9lJT-fAR|mfLdBZG1iBp+{T+x-tS+p09}s}U#dyD z&!Tr8D<$<4!-E*u`)7a%+}d5dS7_!{Cm`EL{pDGU3An#yL)gW6?89p_-8M$ba>bUC z%>$Zt=Yoi+hp#I}=_F5Ci1>9YTfb}@>=J^W8}T|-q5YwT+H7PU6XxnU*c9%HXS`QQ zl!_6MNEp0^+>J&WQ*ZkAIr-+gmvv9ZTH`KdWw0=r5}iYJwrMA(J@fjzug0$??t6v@ zV(kA+hvnuyNa9z^1?u31e1<}mQ9|;L1cB+Z z5tBhLF5^0A#O>QH)RK+VlbpUv*)=}qQG6hF%cs@nizw`M7kZzEgB`*RrPP!o&-?0KSxfo3w3iSk=sM6bCQ5kQye&$I zo*9hCCO9<7@VWzYNqfs0$a7w{tI(D80Fu|n8>n2OHdX?(+IpKH7F$c3mOIe+e19Bh zbH&d}d@9eXEG6|8?2GmrgOkT9LP)R*(RaXg`B*dSNlE=rayubAmS*m2v!b`008?Bn z+6e?r$^zdGdk1;900l1BL;vr98?)()VFO1A0jC-C4ydW!QPdv9{w+KCso<_5>i>(G ziAuK&sC6Y40jUnxkD_7opYV!4RHfcutcaBY9ftu;7ok==FyutKrorQv|6k&wgpKl_HuSVMK%~V2j(h(fVjTXK+ON%6W(c1O!kXp7hI%y#2w>{uB=$EKhJZ;Z z_F?0ZQXe&;G;B5|fn^MqQ#S+(h0gWhB%^i#x|%521O$&QD)4+A*N zCypyw6Gn9Cdp|)40hBv|PG9O9ogB%G(k7s}>mC48u)d2gouy9|XgG>aD&Q*ZAsYw8 z>|Xl@D9r@olA#BR$wnl1fwI#32Yxw^?xwsf^Kr_abc=<>R4$bT{$#9DW#sr^$fdE3 z^pG~mjTGhw9I<&y-6<^iu2 zK}$0!)){;t^(;F4c780S21ZPUhsP)n)I!Q8*RW@gC_CTsv&Dfc5`<@D?%l^g2I|?f zaarZ?dD@$OWh|LuF8OcjM04`v8Ss)#BZ2N0xS!i-m-%diSrK1BpO&}?796EX&|&-b z4K*HG7^d{pPdw{eOIAkZ169h0?^VyQqcct4Dml@lwPqQy=NJsi-5Ui5NP|7GApm+_ z#Z;7&eUD(@xP5#A949Jh`7J`mmpOu5?j;)y9s_>sGkXWxTREi zhDu%eWmYr4T8{?pGeg;BnnjEofK&Aq!5=q+|mKaIM z3bsKKRxdsF*Jz&Kx4i8Mx50Ci5 zQ{$Vz21L?>5v6C**R=VAFSHMR(B^>Yi(^FA!p|iFBVv&Y0Wc~QW8+0*psGbs;zF;~ zWUD$TXY(zuOMQ z@*{Z<{XRu(gl+e{2ZG_;-auWREUa8e=a(GSCRHau#vFl8qNMDI=|tw zN(SxJ-A-TA6P|^bP-rK5FTGW#{{t~Y(q;mOB?8uh@*ca5w%F6oG$u{^fSdWgr@YBS z#MAQ^L*2LhZvQzpJMmgUz{b(50}%#=3Cl>;WiX&029 zUb(41gYXv|A=D&{+MDI70Suqpsywee3(j-dbr0oEt}{_RLR9*ERm+*We<+v51+U`K z__&g4b}Z=%hmo522t|LE;zi!9+Sl|955gGRe+2K459Y-|NX6vpeok&az$L}L?E=|y zVF3DZu9P;_dC_}T3X8o$F`A;zPY3zzWCDAk4#q}W*$z-)k}Wzb;7#B$hgv-LF^$m5L z57;UPPG=c2DspO&kVD{@KNY?=Bu9p$|50t} zMdY`g)RHvwsS?fii76WrYaZ*Ir3$Awn@q({uO(d)adfkp6D+|H@Cz_ z(OC?JOp0nA7Ig=6;g3z;0woC1rh6k?p=f%&-4fV%YsP%xkXCk+hZX zZY#m?*tsNy>;|3IpO)<^l6E&U2mwxrUiZU`Mkq0Vv~qxf>4u&kV05S(^nG?T4m8g! z2tED;G(6N!XykqtK2Z73yd=XKKlmhls|xT)e$p`$yRb|EsVg^~n$cang^5KrLz%^! zr{$rv0P+;)LvxPB1bs0KxB^rU%%3AKuYRV!v!$95`DPwa2vUGh2~Ee9*v;5`f-Ru< zJ}?tDxVk99F$?c%q0cc5t(faSEUCI>?u*O8d4VRluqBQ{26MvsvBM(TqPv$Gm zymAWiYG);kWDmT5N+99*yj1xU41K}=`ji9RR*5%B*n~0E5Q8Dd5?&YC?-b)o3NLK} zS9v0_prBP34F_zYUlMaYr1*r5tVlK}JR+^Vlky-kqJ#F@9q}+@nu=SZqrZa%c(SyA78F>b1IIhig2cf$V z=wwzX^?|K>J>2^SP}?QYY$&KdU$M*>3nZKts-yvj<{Id97{52k2*`0e>e#REa@Xdz zzIpm-s*>HS2iMGo^>~PTbF*IbWq)O`abP0Y)wfj4@d=YT6;}{eGd*x z?-ZV^ui}%+)%y3Z$s`Z$GheK{znyoBz^$4#Dix|txU(}J!_=8hz^O`mfmTMM=Mw-5 zz6kI)S7E&5)v6U*Z;&6K0KITN(9HqCi>iyjzk&p#19VA-NxF1?z;6eRYbkxOO}%5g#h%Z*WajmJ^W*@=kAF$oT?ccT9gJb z-b&KLl+%ds0veM>2xkSXMw%pfH;~LH$##lMg>-+a7?2@Bxh%(DZHF=kTeWMr*i63x126fh|QARKN-cF zy*+9YNSe08C^K)C-h;VpEhEn7d*VTzpVU5`B+$+Odz6J-2K)8?-^9iRvRn_v=osCw zs{DtoOSLKe%~-L`=Rkh4wztXSlJ|y0n$eZp6w|OTLAlYAW1b>|UvxzG zUugRxH`PajmcJ?N|BXem>N&$Q8 ziljzmf2A)o#ei_rsw;Wc@DJtf15k@4t@{%@`i%WK)OJqHQ`h^nYSe07rCwZxzyCZe z2|X%a`TeH}%Mg$pr^j&# z+4eVkA{IE5HdNG0SJ+W3n)~Y{pQWNTG|RS);M&<}yj&{(C5|zu(mIm7hUmFjKobL2h=5kPNCxY!N zjsFrKVeA>zP$P99*HR|NBM|BbpK8r}6CgKxO91QWEO-H~4PGce_zikrnY9^}HWU?p zJ=db3oOxX4buKbqe?AaadLS)-+ZGSz{F7D%DkN00od{Bzi`i0)w2Da2?aySs@QH4DFWrp0mA>TcNdNpf21_8_AilMjL* zX>LJ7!0u-S`KrnAh;K$^x(V>80LbZeJf4N_OsQ#zo(s{gKBfZk*4&+O4JM}8DZF6m z^Sm_l=<#Oahoz}~_wMr~!&sQ+&jmN`8RsvTd+!Gtx7Xvd>6!8%Z$5sucM7$>+Olcx zUpyI(I{vWwb2z#3XGP(Qgp(^6LJRW@V>>@T%ZGuv-_)h#vqn(7eR{h$_Fud|VTmEd zgP-X)eN(JxkE?p}7LRi{$4ztM$5~R-z!N(mor@8^|16T?$xU8cxBY#%znrnZ^H90) z^82fR_RwS{4n*v^r*vSwZsaTklGftiuTa9dV0lsy{RRD+!esmN zZ>nQk;GmfEJuEY6&W<<>UR>qkZ7G$$su$jIN9}^%Z;k(O1zOOUXA`%V&PsL9tw*PY z4wHm{WUl3|(YO-(Sjmg=9%D*)BA$}pm4BvB%s}>)dH?(Lb$RWHfJ^rcUL)|1Y-D3S(vR$en_i=3ZrkTwXBpBLAG7=$RB~$n@@HcgNB#E7zdhMDR*(fU zX9US_&!39-8SD@Ao%u*VTz+&!dHT;GN{)#cQGEY1_lKhCKa|>Oa#v(KM{mAqQI5a0 zwtS5n!Rc(0xca;XqA4uAfe{OeA5Pl`eo43SJHIvE!@YE@0t@MV0VH@EtiFu;>Xv^~ ztPU)PrVPA16&Cl6Auh8_%3xeW>D9rHSMRG}<=GG1KCJ0~9yk6n(!=;%^uLZ$9w^=` zm2*Uh- zIEqh@1D`99)J%8WF`Hrd&uitFsNk07>w;eP+-hej zm}A~DxrdaNr`TnsaSSV9G7==*%JI-fgJj}(QskoY`NB*slU zpMnXUCGM{<=jc1`r}B!-T`x}W5SbNxjCN~I4<%JPn-+2JzouKl1xFwwKB0H&9{Db3 zZcCur{qVeimk)e%$;;O5dk>|%<=FD?%m*@x7#JR$n93$ivs@9S1f<~(sxUy~qkBWLM zDX!5Ba##;hGVzYV*$$WCcIT_BPY?6IElf9k9%sN@Gpj1(65mK{4j=V=H-WNwYjGD3 zj!;L%HyhWc2}mg)&NjQh%jepY!7^ZsI5V=NR;wM^*Sn(X#C@iTMFW%C!4rH&i)rK^ zb8N|UxO1?cORiF`!*=mLJxH*H-R!C4pjz}p+!ZUm9KQu#`WAhCE6JWmV|P4p*1e4~ zCqMEhtN>M8n=FVBW>@aqkL8zKT(Pi(Mk*aGaJcM!;MNh%b}@oM<_F#CbxdkQ=Uln@ zw*>v9xv1Ea`?qsl5lC_xaewDC?$<7}4c>hl+3_N5tmjwv5h?Eo7IeeEU-WZxJNQ6F zMWtb#q0;I7RyylC8iN%@#y4j=2$x;ol6&RHdHwkw(grp-KjC&m>FCL}OM~@}Q}~aR zEyGAml_5JAxdnJ+vvF0v{wh2=kJ-sj*RrrUN*o@3lCKXYhykTT zdA=c*Ss%Ca(35E_iQ$3S(pQuXY&Wt>PqK0p_2-2xjHF#WGtbXech7%UWEovsP+av28&l`08;7Mq>cskvj>(2@ z|2P}wKl{TkBjN$S3Jd(3QTaHR_4HH9?APY{P-piUi+6mz@x2`AEOmdeP(R1S$CQ4a zKij|F8Ez`SL){DeHSv_mh)+U7;?wir{o=6)or?G(%O>I_HD*`w)lfcg#1XTm+L73F zyr1MLYp18gmTiaMu6fGJYCYI$8){EV_y}kA9{y@=K59-Fy4*`sV8=s45I$n+n1j!~Y@xC@S5aTi4L)(xoibds|9G>0_V(OKze zp;?h7?#w?MKa!57dXkPLyLTPO>Zd9jz@yTf{4FRFx*6$TxA}^`_>XhxRO0r7P~s)s zd@WwRW?ug-s$`B)>Y(3gj2A7&J{szKCliND+}L20O7ELABgORjIG#_J(>KS_a(-js zWV}2PJNsn7I4;mr{%FVTD#1=UjgpDYg-5K1apQ6GKEeZj>gVs8*9`?%JA#~Fp4+#c z3sutDw~Q0nz$zDMe}sx*RRXh40IpK0{ER&+e-#mYA|iq#F1 z2^v?@XcE~HEe=k)+|`fRo`ZtnSM(uqA64i#x>G7KT|bpWo2bd$npCOO?bpq-%Jl7b z1!#YQLk>EZz<$I1<^PX^@D6oC_)<2@39WGB5)AyOB(E;_QN|>|)|^)iXEpzip!y%0 zD(?Td5%_;J0ywLAoYg$eY941bkF%P`S9%nU=vzo_Q&Eu@* zaaQx72#&Lw$63wetmbi6^Ej({oYg$eY941bkF%P`S9%nU= zvzo_Q&Eu@*aaQw+IIDS_)jZB>9%nU=vzo_Q&Eu@*aaQv9%nU=vzo_Q&Eu@*aaQv)%?F#&HsDV{J&St|9jQ^zgNxwd)55ESIz%>)%?F#&HsDV{J&St|9jQ^ zzgNxwd(}Mb|BEk{|G$s<|KU~h!Vm~TRa#44W(oo$mUam7g4&4$L7*a^!|^a|A@ef` z^y+dH9@@+B6aqco7LAAILH!^Q=@>lp;1c-ZaV#EU2R^;Qr#JZY0-s+0FMj^Mj~%$r zV?PM=@<9w9A`O0k=7F)b9S5Ui=mq0lzWNLTV=lyoEVAJ4Eutwe2}`4E}o1( ze0lfnZbR7L2U{@Mt>g!_czoI2xP{HNy8q+g&Mug`(_`=N+sjM;J9vyRps*GUdWT{U z9~FGCy0pB!UjN^jzYqHE5aC{>Z~HDE`#%n~9{BDO;d4^|&qFwTSjXRonnLk}?SO#Q z$KfO6v4`kir=gdRZ)`?J{yGTO28UY>arlbI?Ba9Mc2?GQKih_^;*QQdI*7;jJAu6e z2X}WzP>%h0a0217gssAXt*Wq0*)Pc1d|^=uwh9Nu6SfzF$3MZ5;CGW99@*F>@8bRb z`+G2LKiqLVe%8xt7JLy01A}-9%n3*M1(*%^1FUWbhl?Z_79Jmhg@N1uGr4~z_s``1 zp`0HCVB`OfQO@e_rHc?Kyf;WgP5vY)11SUoIjN{{Lkj|dgCF4##4+%JV&((v00MOc zi~~~G!88wkxpDut;saGx$a(NH0s;-NIywOgeq#WCAP}N7D1-=nLcxEKG}!N>a7Y@_ zpPva|>>XWThCq-I#T(K(ZqTJsvTEJCm_595iR*D%Rc;xD4bIjVt$8QR#DnDI&v$>k zaop^jKNlBAzmz7mu=oqE0N!lO)n1kRr#UV+@O)L*;)pJ?)Cg9K#yc0hbuMjo6#PTNe@OWMAta1^ z;M>qM;`Tip0t=wq!z>mp@7~x>mT=YOzx7(DzkysL|Fw_NEtax~{<{6j1Rv~mY-Kg> zedcpXV>zks74qMSJWegtHPm$OQ9$98NWg)1X!i{3_Vn@I>3EmL@jk)rDe{9{lzNCH z{x%C<1Mi)WiV#_7#t*-@=xp})_h(sfES+-{1E>1Rl14&A&rzu(S@&TWu8f&Nqbjv7 zkyG4rv)l`eBnNI!N)O=&hEj3;sNbApBB$Qvxxz3t`tH&VbZh9AtR=J9F&0OshAv(FW*t9z=qUW|Yd<`%eA9#vX8f_1F`!U3txo}ht0=SV|Mw9hePk9o-BFJMOm_88|_ zFwZdE&z2qkxAly%6T;r9Yp;F0Zpo8f7~CaIG?Rw--Xn{ID7fHzD#37AR@*!*@Yift z_A$*>Z+))G<8P@bZd!5u7e%H8i{;1F52d_W))VNqEtomij$^&NaZOq<7^3_f5>}r* z#!3gje5y;Ts~I~hh2M{o#CljO5gA0_9$` z&xlJ)v>UjNC_lK){0f^W9LJZ2LW4Qs9Jk*LU{{vHjcu6&?MFv1A6ywOeR1gozmM2`C=E6|t!45nuN*&XOLxvyQ+k<(~Vs zw3J9zLEn=Y`Tn?51FMHZp?b0 zQ%TAI`>&9KR3RR)zC2NeSv|viW{sg0{bGmRELR2%N1m^IDOY)+_-=DJrcxhuJnU^W z?&Bd#{9VqgzwbLi31hKjpAhpV=O7}7HkDBY63#od(D%{4Vo+%>S5p_F zD;MfWk9%JpzXAN`$cd;gq$#5NF|~6f2f0CLb?OnFglFhzaB9*L-IL%dH15%x^x+-n zUYD&NDZAXpn(K|fHwm$Kp;)#(v7na?U-#+Lr#g(`)h0GHk_4Qqg#KX$e@xZIhLh?c z!})mFF=F*zD4$6Js?RSo$x=kxCtc{?3GbVShP*f%nUj)Rpoxft1j;0f({QGtJ}43F ztU^VKd_YQnmh<%^p=1AzmlC0f99J&r+SSTbju*JDEww_Lr~g`-^%l6TR{yBbfI1Y* zaZ59xz{GD{J-r*-RTRjw5=^_LxToNv2ajWK-GjSL7Y+ zOj@lg9p`GzJUCbPgmp2)=C?^eeC>%PyW#cNO2_(4On2$2X)q2ZVg<-FqS&Yzq8JFcU(VfdciyP3jN7d5563%~#tMP>t=xbV@T$(FSy6rR5xfWN1?zGVI;IRY+mS5QwSSl5d;>9WZrc9u zML|--CHv$~2pCmvMJFKk#7RzOoJ8#Lf-O{6Z3_-VKM0H%;Kx3*s=vN%GCu@YOZ$By zh&rdbc0Jm+P4K{4^(gl5rJ#|)a0N)#tM^u~195CTRTiBosq@Vr7c{A8{tUB`^iY0$ z^Lj=v?Ae?p+BV3R^Q0^*8I&4w$8^2VjcnPeRtTsk!JKu762KgW z)(RdPizle?QSlg~sUS^NYnC$bEqZT0+qO(hk&8^ijVr$m8w!6+zo($S{H7Q7VAdFJ z!A@xcm4h^vtp*;l!V*Rc(&_QN+q`63EcE!}Zzm0Y4-(8jyr;AS^FRo>eX1lehkb(N zA`DG0GYM|vp=aB78>=V$C@fR@_xB+%9}=$&;_aSpNaw*V)}O2sl@Johpl9ITWxB2^ zoc!E?&(tAs6IMdIlfkNki;|>t@6Q5Egx7^W90#&Rsl)BFQJaI!t2)+ zYHyupp&~@}UsGzifwVt|mBl*?T)5zMC3<#~2|jtgo&oY`S!{9#jL!K?%^ph*Jzj90 z$nbEp@~;#10X(GWzt+jqa}t_|c%xcXd-93J*aTqJ`$)@-1E{Z+$?G~ z@!DT`1kw%`;-pkxl5nb+nMjmUAqIqD>i*<;t5AV0*Bax)qAohe_0$x*KQ4+q4g0#~ zJ!9e=Gv8#UBgc$*3M`RT#de|10J(~Z?+wAX>UT|HR@bV@vLOGnDJZ;_PeWNAa^YQG zjta@10C_~C9-0@jzFgIJv#`!90-SaS(_XH*z@YB;XH7Dw-HdbLPQANniJ7Em52`g~ z5X?-T$I!e&KQKvL5FE|@@SMR{_r{MuyMNay-N5`a6}Emt}=`i@7texKYE%Ng^Q*udX2O z7olq&g~#o5!|*Xm(E1z93sz9)c{+(R_=exf0SYO!pk zSPx_Cdo3upr9MzH`frYr{^+q{{d5kfK<~Nw<9CuiYf}a4poJmqKXW&Th;58xjSmI; zn=AIDC2XD=*Aai&FVgf)PFqRK3+iHn*17gHd*@_F;O{IU#)K8coE1TmlTV zoL57$A>ob4dpoBq*D_>Giy+pJrILyF2>4b;T?X!5Qr@ky@HCIt|BJ|!$a)c|xBWqJ zA(=yn8ii>9WL$^0MI9iH&qFO;T^NRJ!_#9OFL6CjQHvDOrV5Gx{SvceSW%rj0r$W0^Hu{ zo*GGlZ%yJYYxY3BMJ7L08=CZI(b4DwnBYU|iO1fZRZUoppo{$AoSvTE@DJL8IQ1?w zJgUz@?eV#DpZ<6yl9dt)uJvYxx=TjNjx-BKdXrU|fT2&;w?>G;olxKph5o0v74e^5 z{$;<7@<>6Po)z^(GiJSE?}loYB4JK_DbD#u5-v-xie%H4z$lw#IGFxgh9`8lb65QZ z`_I*XPvgt*f|GM0VH2j}UY+#5Al3j**}N8nW7h`;=T#u zx|T;zx652H-a~u|ug);08U9S+_FvzB%2})#4WXY`K;!9k{rmjtYr!Jy^iMdNfa%ng z8XwkW`48-rn3Dcw+25uO z49|=#AJ0Xy)>+J#Q|m+)qvX12(b5S3=F$-D-z1(DjT|Ai$y#FB@^&mJ z7kRoj#R;Iu7=YaejPg>;b zDRQCmTKTa>!J^=V)(`zSMq)iOPL99)fC=hNQIi22oPP1-2h~<9TK-4305BBpsC=31 zF)_Rkw$kckVhH}lHuH}^esF_3jUIQ>23}B`TF7K{sxOHS?7!9gJbE~f9)GtaSQn}% zyZ+Y#pHl-{-`TU<>$rXQTcdFJF)=Wo`n;7ui;I}{hiP(SA}QT8!hfxn8p1GZ^-Zo7 zHQHK;yOQIjbn!9dMPL9HBE%0-0GW+BtSbcnSbPf=R~nNp-@0n2|HlX?a*!sf zu#t%emN#hWfl)k~&qGZ0nF!Gck*xV4WYZr0bzLpNwAEIY0jmmCZ3-x|1C|esykaD- z*0+o+9E_7p2_NWD`s>?C^g~&T(4>N1$3Za`UZ^-Xu%HToN8GGmFQ#O;;_<`mEpLp${k9k}^=ODmj=_@w3qBAoZJM+;ys+6?av#a za4dNZL1TSG;jWh?&1L??Q;2U341@D7DLlR=TjRUiG$y})0&w{kFpX;Ehb;Xec#`jK z0yymh(zL($!Xs)B7(Fm&Q;e!6`>d!wE5TT0yarznh}|#}&Us@L65{rUaDr?h4W{S) z-g{R%wNZr1loV0~RYwMp_ji5yaUPp-{@h=_%t|TYTeO8etNJfhru(m8tMMG^ZbW99Gd9Gh*F)pR1k9rP*o^?P60K)y8h^1%G}4O@ zK7_pOQTAR2$Z2V^i^Qy!X3gz`Ce*Q8EBIlCnMdIPN^XA-#=+9}I_50?`I9G4=*h7< zDN$DeSI#PErm!qBK8)xJDBqK+_ZR%j?*sFYyrX9(6jfn5K~{%PnbW}PLmn9kdVm~d z_FcW~A!fG$wN&XnE%?7MT$7$1*G4IUBPXL9h(^=FHR$SfT|HC1$ zY1FJ;()7m;ZDHS~X}qXS;b`^9M|{B6?58HB>}uqy{~G>1&WqCHn}c5#PNXDRf`Ol_ z^bE02!cmLr2fzykR&_7n&Rjc61tW!^*c!o9uZ^I?MHBpU)x@jrU*G5!Brtg{z16Dm zaOO3tbm~b^@jTj69N$G{dFY4ZL{J*wub&2e?M_}+O&sO-SIun&H%BhOw_Ixvm+eu} zV>O=b!L)qAwkW@2C#T;wpN)RsA+EfoYW}wKhcv4Y+T^|gRgCKhY!9cwE9s<@@y~k z(-=vMXF3A$cQN%v(V5Lf3O?*BpR1jCNaWjNY5sTTr-xA3-u12%Q&qI3TD}h?|IS@R18|2mS zNodc3-Igqm=G?`u4*l*>jq{n*N7LydF0~&Q=#;`&JFVE%t!zr=sJvuf9xjXD_}M;d za+>qV8gK9y%iAN2H8eEV;$IP^N5jm>|{@kr>ofJmn%)HAt>wS&tZ(&iqTv>Z|};x zuh#4~4$w^;k+HyBG>+WfblA7~ibuu{fzJEzh#Nn{rRnun;jM+gDE-e@&X5jWLm5rP6t^tuIge=hFL5x@7Hz z8!B*u0?Qit5^c==p^F%bO8xfUT~`afXPq3Aav`0$aszLQrK87l?yX%UEbnn>VgPFM ztBB|3p;5)2h=_q>{lM|L(W3ccfKery$P-2;Np+w|%@a zZVX%YGwoX)??d&R)*~a>(1jcvb)-L`@$z6YqY3UT#2%ft6OlEiv|oOw@p z2|b{aAfzbnyl{^8e6heXRLCm`Y-2g=n&&88u|JtFrED=6+>`CFET?1%ajn_)6QNm; z&)+1(Ign}6l$g7=4wqPSm$sqw!~MzVm3nhjZ;o5$WVe2x0bdCO>ER8V*Tb+{$`DhM z%NEca$h6ArOc2J;0A#O4u2;k}j$okJ9k)1G6mK_L>25HWJwG|_Q3oQGanDq;>zLUv z(uav)%>nQ&2J>>+ACyW+FeKQMu-CVZZ1Um4H_C;GBFvQ0zjYZWLJLtGG>{yw=3)eJwXZx_KRRpa5+Ywj~;q?AGjd8i_PLc z2GzpAA$B@dZvfI`Os&s7 zviKHHju(*AP|8s2bJ+`CJ>-Um@-pfKNfG%LR!W>=>pxg*CM$pop~bBF4H|nEc;OGm zUKcPdr!7(Pj19QL?V3At&_=RRPmui-L#94EfUj>wI#M~(UK!~bdsi1r9 zxG|qL1jbFC3p3is%V5pC*^<7$A|_lq?VN9cL0RxgE0&9%tNOZ~o zn|S|+GkWLJc!i(@>q|y|-7Mo@7O2_Mnias=zWE4jzOA5aOn1wJLc=#BM}7g%($~Fi^m!ovRl6zEjd*i(E|?I&^dA^{qG4 z3D$+{10*dA)96&2%+o8i2Xvo&elrZGQuIp=a3<68p!bqNRx*!(^xKu znkrJ;5>#NP5jv{&qB*LUUK)xC;!LYH|Z6nrG2f_sVE4Lcgk1F z3O13ECG^ZC@ScbO<)vA;lS0RK2k}iLVZT1Xonm6EcgH**H__^ex~`+X-H&&-raU~H zmD4q z!1z3|92eM)mbuNM+v7`2c@20GOGVlZ!7%EhxED;b2jWt-7!KUh{?T~&(pwPW+rGTK z^7C-BlfFiRMu`a{B9FuE)+qxDc^~*tdf!VcPNL)FF^TrIcfR-&5xk*Z&sPlNI-_Kw zoK@UH;1hDGs3wfu5|=9MS`11t<|rk1NIODQu=pLL+ohtZZj&VQUFm)?f+o27V%h!g zHYq5ayC4;%lK&l@%kCTZ=7-*9>byeQT_d}iLZFFEDie!6*~%haQO))&ONw5CXJIPP zKzXUyQ}-jD*M6Kj!l4S1AtSrt$17j1bULj}qPo(g$8^0*YfNKrM-^!hOL9li&V<_R|F9dKWW6D7>9-lTMB^G6?M?knM*$!B@I`H^8 zib<(=QqjMpY$fU;;0_}`2g@5F$!nx0kB-#XyMSR^YQMMxER%_p_P4g*Jt^A zNlzULb?1YnrlNYt^dke7k1i)(Y3GJJ+C`x?j@V^YeY`m7#BD6&)}57`4mo5X<#0%+ ze;g>iYgKzbpjY%4#{A@qWA0J}$9LvAZ8BsujcUbY{* zw!fK*U+QBz1X$ieh=!%eNf2Oz=iSd9g^(7Fv?sfri0Dv4n z^jOncX}H8xC~(ldGO?x7pT>1QSaa8PdvDaW)+hpX`AIf(f{wz$dmfjDf!{t{WD!%;AWm+ys!;v{IJpYD^B zP!MA9TNgW>T;V~JnB0R&`)=@d-F zV#}m^5-NrY(&5^TgD$M$Z()dB7jO|9v|!cIKw&KXIUGpE(Wu+wI{(U`tWYIc{FY5^ z9hZ|4DgV~(C3a7+>OHmIp1A|&7ZC=HYak-1HR_*8cfCN%jz*q`Z+T1;l3va{OPfy& ziZE6==GRh|%~mi|?@$>o9l~66puV_7IQ3L!vr)B!^<>mX8-0$v_*qgRsK$`SH)15hr!SB?7JWjd)%uk5>yb}srjy+fbLQ9XQR?(1L6*|0&Cwp{E)Hd zCh~sIs0r5z0lO#4z2+Z&PeN&sXs@TD1u>Tf`!XuZ6Japk7lPW2Q+Ejov;?ES55stB zj(M(LuQ=RjuPGQctlXg84MNKOFAYwN3&;8pi&nKGzQ6tMnNxswxjFrMZEiFh*w=9CnRu5tE)tz! z5NcuUHp6})1ote%3DoxL$bDXsK%-4XNMlyJ6XP3Yr@*f)cc}%;2&?8xGL*CqiXTcK zND#f@7<|I5p0f1yevwAHOyNz3F-h2Pnf)%N64b#Dt}T4dRlm0W)M>8ESD1NZjywa5 z&l2vkD1@HcV)&V9`8s#N5dH!d8KMuLB@m{Y@O~{$0HxIP#0*FPYYd{ zn;b|g*c-Y@$IAg>&+IHT_nxYf$%Q7RjNZA%2;!N-v-$uQA0EHs6mcL^676Rl?y&F4 zQv8MMtZGehA53j(0zjgu2mtF|cg9UM;UWMXmml`=h^;r$xHF6PUx9-T0ye~jUMt9~ zzXrvvy>sM^@_gy2Bj^HK{Pc@KMcttV}E4MQ!$d5 zOGpY?^_)w9kZUahe({r&XK4KgtAZC96#ks6SUwlZ^hR+FudLtj{QFsI$fdbE%=$yO z8oVwaOQ&WhEL8})(371I90m#u4zyBI{Q(VrHCmx4079;j8vH6jCL< z-(448o9#>xR<@Z%gL-rQVg`)hU=KkVWVJvM-JBWjhq*#ydJ_F5DZ@v1>J?F%58?O; zXtMI0ZHddyHwy`X+4NWdmb<#>*-5ETy@rpAqK9h@KXaw9d0t!H@*w`<{iMXES4*u2 z)jC9{PTVQAc{yaAKmb;Scj$0kyXfg1&H`JY)=Eb#I5d}S9e*QzLWc7O;YJp|VT>Ye z-n*mRG+Zhz6``zZHeG4HL&2-1duakK3^!Oeu_ zkZvsCu{&j^N`$m;glZ_A1M+%N0WCN}5~QF+%?j0c9?B81=#RRu3h{dVBgW;wN{|=r zP^I%5k(es`@!HzBMR_w&m5ig~*S{DuO!0-XtvHVZ7;>y`fxf?nfy=GXv)A>((t(uK z!j-*K4%}&y4QOOUhql*bR9$00H^VB*`%16jd#gIfYbu`zLwQG693srfKKj9`H^1{F z<|!42oI+>HbwEJROY7a-hazrkS$+Rtb=kBY4N>YWY7ujsQ2?To!ug-gE#JWQrqmY+ zLmk3A&8WJ4VWIc0X*9kqaxUlH#4|6_d;%R+hj6ER(fLMI169h4-<#@eXG=Y{A66`v z4wvtB7A10AuQG26Hs?^X`T7iWC){9Y_l?C*b4x{$308L*EPnE#BWNPNPZ1vm#;u4+ ztduhnKO!K_ReG+Fz07BBK~rW&wV%kSdO$1gO}9ju^UYT|_2u8uw)wdu7se3Oao34g z$h=h>=oGh09>#vKA2$Yg6K!FIOBt(jwXfLhkjMtBhoe)mB5+Lk1c z;%I2zCsYO(q@xF3Z_-fol7T?}Q9wa_alxe4(8fUMCeU2W#Dj?7cmzDK@yq|b-0)uSeI*` z)EO)ae@)2#9#eP0b42i!jPxu%eHw}CbrG5ce z(K7wVdAk64C$O*Rz=C(RAXHfm6{kCr<$pWBSV}wMJZRM1BqcaNO$_{nThGWjmVghB z7YoYb_NsYmyMZ>C2l&DTgHjcnMi_zVO#pm=SS~#FYUKKafXJH=V;USoaq0N@%pX() z5ov*y zrd(?fcS_6R_hdUZAf67Hn3jb@UcE+nmM!5gnpCjYSF!7POrk|ymcRGQpd9GVu3a=C z1UfZ@&1mHiHZI+0udBY!~H_9L*uhW@VEZv^^Ltwm=L#4KfLz6y* zquWB5JMSMTw#V_GSx9{{m>#0~A@8Dc+=ZxifvPVRYc}UY3n(=5v^ZzYIaonhia>Kg z9$K+a;Cp%}_ddkH(SF|9t~l!j1UiE0L>ORIti-Scbj+X$$|ym|YSPepwQ^Bley}K2 z=Z)^yiXjA#H_V{aCMt-AyDgG!#n!zBV8mPE6YVj)(Y4{}O~7!U)c?dBSkwaPumJ}P zeK|7T5LC3)@rgtYE<*{Fg7Z*SNe{>QqVh>nf+lnDzWmeknzmvxPSf9hqhOQ z+Oh^n2c#^!So8FXQp(!u_;`e^2ex+1Q8OW+PZD#?0-}3IiA`G!FM<2>n$%VwWsRI9N5K6xk$?xcj>NS8uqRu_pERlo2^K4o z97+dsDoZ^BoiH5-{b(fQ&u$W~%?ec&Y6Gd%bXK(5(U-^dOV3ocXk#p zK+SDxJwj>e1mo2&dAhp`{q4i&Hz`nie*9OL06+km)=J&A_);HJWd2S+HgtesL@Y01 z6A$VrX<={ayQDskdHp2lt(%8HvYJy#w7I$cA5aTi-AX0$6(!Tj)4I7*y{Tqz6bA4@ ztpAh&OTD?rY_jWZmioLDw$pHGPKOtT)-xSRuWoeD44wws?ks)FHxG*yX76ZY!N^a% zps7xa&y+7t9b~39oZf@vx))pv`e7`x=q zCIj(h_f@N;slRg5WUc!oaBjP;hB8f1-!;R?{ZRH!8!4mMAOSue8G!z(Xk2XSE3zvR zVGa}9Ce*^-6xAk(wffF!y|RXt&U{|__P$AOqTEF#RdQ5WBZjYem|_s~%#AYe4H|ft zE0CH3nuT#$pTC}h?0O7bEt@GS^2EyAKs2hE0_HMCXA1PsLPvUhjpy!84m*Io=n(u+ z%{zps9<-_)Xg=?ca2}K~X7yEAXYXizayj9)-LKc=<9gJBw@-R8L(Jr(X$Ln!5SGl= zhaF|zEz0$!hwq5%mQG?pC2!+=duKEwNaF2l_j3sG*Je?O9NxpDQJ^n;M)+Y&zHyCy z!d2Ua6CZ(ymGx-q^Vq8-Kmf&Q=zRgrupw3px1A;-PvV7yI~NMXJ@O@wckg~%j5rR3 zo3sDU+p|%4T*pnD;geUb2UJMO$dW_C(C=u9o6aqpA8BtuL%8R9xpNG3Irm1rnU?Etdogt_i5K?GSy-Fi>BKJ zZRZ)n4dJAuq|;Bx1SlQ7r=#Q_^RgE^+SOrQ>>t&;Z_c!Wwh*E>0Z*uqtS?~+7YZ}* zEv(`LHAhXgCvz)3*{G;M+2XvZtGO*-5y*~oY^(+2$rH708Uk$#9e=jP8j*(GeoP5w z3@QXxt54QInu;xj8+3le#Mk_+<7}OeCKOfgeBVlz^u|o<$Z2pJ5z03Ig@TV5h%}qF z0IZeP)WoD269-4%WzGP!Tbz#i2jB#mN7xS%&07nDMS?(GdUr(=~cz@am`H({)@JSoqR`ouQnB}RrU&V8Df?M6g%Pp5eA z+;IoIqq@pJTWOv9b6!U)+v%#V_fbqPKP((^6wX5~(Q%QLi-lFwQxQ^d~RSg_(kFjqhy)I2JII9 z@HLKJF$hF=3xM(Vj`K${o&Zj;YU#2)-<3ofJA=eWjRy5}(I3bvSG`v~-AxV?Z-7>< zrktnMTRqC=Bp1TLCWQGDl;(jV6I7Sg>5ABt@jF+yr=BYdM8)#y1!D^*2^Au8N>g|} zlvNW2RGNjedv@4cwFi_DgvtYPu&!Yd2rU=%KeU{AMv#?(dV3#~D713bn15|E_+`qd zKG}Qcn{ftjnO3lr_0;FEG3H57o%0CFpGn7XvG9gCjNHobkfC?yVj9} z5M<8b`vODtOw^f`k_D=I;yJ`2zv=~S;&#B?DfFOFmRz|1O;NA}@6ZM1lCCR;^CK0G zEy667;#Cl!I}LJIxQ?A7}c{fslP zqcX@&;=mo1TmP%kuqfUIJp(^&jqcpOeOeK#nAayq^6iP&f!)_RnPHolE^YPeoU^H@ z+POm$dKva(KqHl#0GX*!P|m}L{A!?s>(O~C$sBteKAi-O54s3{4SL=K;Y(^u6CeHV ze-1rEbyLHu1mwzb@@L%}4MvUVH+gZ2*BBTHs^2FvK{xtw&EvL1(sqHTeOFx57jUGoQOu-MhScEIlR1R_C%c%qpbqERjQYp7_Qpzd}-Qg7HZQ9e)a^Y z?z%RNwW4Rk7=)jLeu=j`H&T-Igtn&$63dW_w*d2KrE@e8#x89)WUH~!!M*6Z;PqUp z5C_Lzi$Sko4rpYAF=X~3Y(km8)C+P``y5D8d5VDuw7*@1;`ct-3A-3P`Nn6ZVm1o| zkr7+|6mH#!?@cNE$1JLZFWfF>)qNX(DnHD)93<~&VzsC>o<&rB*qicY1Kk7q1g#BV zg5;Wk7tq}=(Suv-szNTNcBT8AgHC8z+~!Gu3>B~p2{ym=VPk?nrY#m31y~mi^n)4w z8>X{iOkzEry@X(PO!qi(0JrJz$1oPDv8qf4{aXO` zgN4Z>C_L7tJyy1^(ya}^%{@R-dMX?(=o>b#=YaCO2Z^w^?7ZhUtMgr@%q6k=x$3Da zI`3a%e(1PTo`BT?k=VLW%sXl;mr}feMSfg~4LZTdNz6w168(m+@co+ZJ;5O4`BHbB zea&{9q4#$GJK*`q1&0!iVS_Qb;=VU}>P~KXZ_LD5;@iodU*2xmkEejLNB&YIWZc%9a6_7*TJNeaDc22b%=iY$tJ+rh}3c-;33D4N`} zqy2nE5%M%W=_d0z$h-Q@vTxXp#)Pj2T)&=+I!hxOl_{MC~TLJG%DoMsh%*w^yz)-`4*JOlL7Q~Gb>0``vZuQ$f_j{*0jMgr6%mh15$#L zHBkoG$H(o@Hj#+z4qm+Ou@y4V0{L-Z3|=BO8nQ>O_<;iaHjw(Aibt)!pLXz@U{(28@akR6feE2;!9L<(uk_Q` z7DgdUR+qB9&?_CL-so$UEkMvKvrFDu98?E#XpFofF14+X0R-#yNGb+L1h z!$(^Mr>G&dnp%`Fce0`2AsTMAQwECTB~z|-2g!y`-8VB(I!0t+ZtqU7s$ZqTD~O?dEll+zCsXPw|#w#XVZzs`0&5k=rhBIo(ykdurn zoieuswv|@ig7jD(NY+26&7W74olmy-uEt6sxt9j|`nZ!Jk|p1NrjiW%fY7*l&-Yd; zCc&JN=NDn?zAgu26+bYy=7MTmAfsT9jQ`C>0Yn)8gg@H&45D1AAGU?J!A04N-Gy7-Ll+JgIX9 zzCr8Y-n7jo`^?SpE}?_g{)*T*6q>kd)0za@HNN>;BG`DKjGyf!L}U2$1YVuj(kyIW zG%psuci#Ak(B*psc=cn+5T}L&Ku8aU!E=#xxwToO<&~3_Cwb3;3_HndqxJES*&%88 z!D8o6+RiDv#fOJW{HrFNYo6bwl$B)a?-OK*ZSqyg(FF=Rs8hZag4+i+awpg%wds<1 z{O+}gObe#FcK-$7e2Pem*-Kh{>PR(t)?;8wBMavUrymD#9OLIO2{h-C9>8|o9hLcX zUQx1Wu=8sYEJ)I;Jd{zSuyJ-RYzq;}B&J0mAb|DW^I%{`D%%#&4W|?VtqB^FViHA& zS;~7Cw>hNbJJgDfET}fd;0NVNLYAJ4E)b{z8nqZGyCx6YebtRGWh(@jK`(%Nzotb0?@BxS69o^%0#Z$ct;58GsUj_#w()kLyu$RX2aqJKnz0 z##f|37;|rqA)|R~L~e?QQil}Y3-myL(E9>%`}?@Veb6o2Y#5^Y#l19k7Boqmw@7;Q zEarUE^*BR`ft;aygEF-XL6snNHVh$|uD)vf-EFZP>@jQgsRRumM3ncxC9E_>4n%Z- z<>^!o;M$;K^13Xz)|%=&v(!^1eFtN0iT^9qDVUj=mF8}E!L>LD zV<`N43?~U=AXEo|AgJhNtDBsWax(Yz(0CW$zc<5S+p*Esnc*jc=X>{9|% zNdwA{V@;YPHZ*#aTvMNkvfVr5+inoG|E)u%ePi)S?9(>mbua4kp2|P2C?o2yJHv$B z!%dMZ{>BT;4=sDj5M|&Qh6^VNp7BhsfgRo9Oai@m&Qw;?q9ch{WW+)C233q9zpSKk zY0#AG>36kdgHIUV;Xw2ciM1W(&cU|0DBb*Cqx7&&mBgUh?d=DkW@hEy*eS0Wu$_hE zJiR$Uth4lj;2Gjiss4E1CwiVr8f({fLkXKl?{U}zWsnsSqV{7-)9%U|OS6%Rdzwu( zV>{o{>28hSzj@tyj>a;nCy8o4J8?#Gp!N`M4{SKT}x1xFftB{6A38P9?WG(~&O z?%1LZnVaK^j_WitpwOQ8z%9)mhA!XkZ%p`6 zkP>#7XXgrTz`$OtVsHAtq&`Z;wYNq6$$KgX!XvFmds~1rB}L!GN{fCZ1%%+g9tghz zo`9Y+DTzc04Y@<3(utogd1IUcGEZ<5`Zqle;(75+(WqZzV-|Oc1U<|LdOVzL6Xi!+6!@1-nZgan*h7gcpUiK*QYxwX z;?iwzEs}+GaBo+NFT;dRI$r^^v-|PrRg-f70IzxE)sTS_JjC3_gl$$JI+{Kbg4wAYNr}`?w}^l2^?@ zSHF(ms8UD2?}nD!Q!qG0tDym@Dpi%5eK%kk>-33tdM(Q+l;x>v>4F@Y+ z?e`jW9#m`!rrosX;k+SGZ@5uo6d6NCI$$?6oSP{h5eIDIUD=`W^CZawK%MT-e0kXe z8b*>jC^oXnxt5*?r7ztBC25B@EQ$a*Su)Nn6fu(pIZ(DJrdc*^gMW{u4X6Z<&vU$r z-WlCIEN<8nJ{{wD`zKui*XkrAruang!Z%liM_R_i0(b%5#}u2H%VS+&mxsa@5WRC6 z&fgT+luv_ZPV8mySe)$j>wT-M65E*oyVD0$?<3%xX+`(PY>536Wh^<%dyJO=H?uXnoNrTzSdZAjdyEv6cWSu|e+7EpY%$;K!Q@DcSbHDz`&!eb$u zvz_sPTyehh{wCDs6R( z9m*O*1Cfe~GdIJPAW`7q=)ln0p$L##f&9245k*C}Gew0D% zQx55}t0&=0pT0P}}b90&OuH)Lj2B_TD=l>$iUcMifb;qC$2XMOIna zBN39l%U;==LL?HR$jp|Ry)%l2y)T=PEqg!j^V0VozyF`-_55-Fb>I4OU7vBD?{OT* z`#4*VoV3R0`t^B@OK^9kT~TnS%;=n7z|}H|CHBjGt%-4QbT6h)-cMnU5ZK$$KMQ3H zND)7$aoq^~nKn=N_8Rn1241IY6rmsYd@p+%mFuG=_lbI_m?-35NDFGNPA`ZaVAfGE zjwAJOEWmiqrkVeu;LZqXJ?kRMr~@Ph?l}#BMly5#{7OM1yRA!nC+Hlf^gV&i@s5jr zf1Fr~%Qu*adnejIy{GblXL_kaCMy(QsFCZnD?sKK3hH5D@Jo7)d#>-d}3EpLTrwgRGx5Ptt`eL z|I@&v4zchQ+;_x7#nMrL#*{rmQMiISL7ygM)M_>`D6`liAdm#O%FF&YXPxFxIaYrz z=IiDfPNr1f!2-n54l&%#RXrD}NAHwXe_F*gbH?$G$||J6eO@%}`20AHYgDr-5snN#bS@WOOz=D~Y&D(R+g?}~ zSLQjbf&cmCnH$1p&P0$vpD+BGX9CH{YnY_*FdPWpQTx$LIF9xfcTrpu11?#ZSaGf}blbi~p}H~BquE=wP^pf1f5~5kR>Po1 zh*?s4D)iJUQC>W1cIZT5hA0GAVD$+w2p0jD`Idb> zw)t@db?lwXtfo+)sD=_a;}LlZIelV#k5b_K*Rj~fHP71JZKkc)e$o!$+mAo?WK&cW zW`JQK+HK0HBnkzMjLQshj)14%?w$*vPQQX~&s+We${eDCw0sam-;4#_4k}>$Gv$>) zV`Fw-pgRcu7kZKjHKV#vE7XeFT}M6hGcg>RnO98D^&P6w;Dfzkb%8#R?oS{!ZgNrf z6|5y)K3{fq4dEkZ^98G>eWdDf-?cCj)YhVjrx1tkUU`i^c&+x=3HkdgKi}TauM5Eq zzckvQsRMcVPt?$kxJB`ZZ$B~7)Q`U!3lTY?!yVKw0==+Bi~qlh3rRg5)@H8)v9#Ia zVLyE)<_&hV-L&0S-xRF^vNSL{^p;Ou$Db=|a-nbFfjU)n7h!{K#%I2>?*#^aampun zMsXMh6$yBj% zhxHO}l~R==pJ|TkbEcFd*OvAmKYl~Ha73rwdxUQn!fdoqGI@S7B=k-}#FGy|W{?HC zNEHB@D0Ly>s}znXe(a)&CSa2fQbU(VKQV=}JhcNM84@OkwC7N(vs;gfBK80^<6p^Z z_UsI!{>yI4B_^I_h)?=>(t1=^@n$Ys3m*_~5qqU627dQNM#4;eRZ!{Dr$O3(k z2eo}Nl!HR6>jshkb9m4+Qqc1pRHY}@tq?TomO8dK-A5%zDdWO2%MtYr%_D$RXlrlC zukj%ej1f@6u(Jql2PAgwv8kz-=fObEEK8N3OqaH*g8Otw8d4%6r6PW^J3h<%ra!0@ zTEI4ui0hQlsJ-mm-UrgY1&e1gb2>O&P zufdn&M~&*CFW>z4m-pA?a-Yh|Dn`wjufO8c%5S`^y?SZ+47cUr zwO4|B#YJaSCJdRE-*Nr za<7H2OO#J7Y|}XO({oicBt5HIU|kh=)dLnqS^G80h}e6}^3`|AzzpE3zwRw~Rv)i1 z^SXa5_x5|Ps{KCG>_;(G>=*1}JhHOM+(ag2iFMp?PDDL@bUW%8tpMM{rn>ir0f+Xc zA6A7R_Wp+Xi*$O{SBo$}6wvQ5)~u@Q1ny*{}bGPYefLzBbI3M7ZG+-L7Swo-Hu98RiAEq(U>*<}c)U z#foj)>8@cBtFa#$HQX-LmS^Nr%!M}?#pvGoed zHBbN+cgFwwVq`|j0T4n8(Fox`dL^q}>J^FJb52cGI+LXrXFSb3Ow{|c0sK&njmkf1 z_Wc{ml0gDnMp~G&wplFRj!+J#=$UvkH<}T1Q`GJ(5TmF+C`pgG@s#lluFFq1Sm}fH zYY@<%rZK%z*oQH*?@p7^5fVi2BM};Cu?lT9qz|Z1c1NUnJhs|n$43L$?3t=L+njAzWGG=Xc6vk0J)(Jff zDaL}%tJA+^B{E>W=xt*P1PaqsGahD#$L_79Ao%+B=|O<-$APo9y>j8H_5UH4rmbqS zX^4ETQ=izF`tWM}t$vU-Q{=vHu2Cxk8iT8ouD-V_ZSlEBGYR_8ypT<9*TR9$qzJ7m zTpyzu9dM(L>o`0E%Mx-MU84ZqzZ6vT?YqKFNLJ2$dJf1AvKiB$MS^R_+iD2!yicnYP0+S6%J@Z*MR`Fx<57 zLyehO@IHCZkLTzK_3^LvG(12ek=t;yJ?S7kgeXKlG>imKh46#b2a~elqHeCMsz)%i zgq0S%4x4uH~fjZS@Y%`URH3iTpq5`m137UT42_z zz*;)RKq>sW7v8&Ddue3`Lu-vBc8a25&{1)PLUMd7Tg>^X_>Cr6X-u$ zA=6`?D1bkD4Q5Yy*3ai&)QW>PJif>pKI;#vwbW;SFEq?nw56(MJ z(dX`F^Y&BZ5T}p|?#Mf@&0dDi>y*rR;%3h2@7*6&8D%4HzGi=PW_DuB^fN_jzW0Y+ z3@Fs&M$|S7&rC>NjIPsrdu6w5X!qXZ=9oG2Bi{($328ffQChT{B6xwN%P2ra+)i|J zkU5$mjOg9gGerAGvX~dOl8IYwE+G$r_gf z_s{3%s>T^Q4l`Xpsgz?L2k;aQgamS(qw`zEon4P$@$Ch{0!3{H<=64X(0X5!{f)1J z7o;e@(uTX~6?U&7RqV#ts|XOo=*BLHE~gPMXeJo*7BG;J;#vgMeTemAPr@s-EF^xO zdCpQql#1H|<_4W=h%OeBDNpO?l<@;|yh_0;b6XsGlM1E=7`D4It9D}l;WLuvB9=o} z0c>FDdAks8EOV*HC>f+D^|Tk8DOTnO)B&Jp<%PRoRCSNPD_+Gw!q<3bH(5R`eT3Z2- z-(YF99*tnJ?wwruR#X;gKVrd%_axfaVCLw~T2a-iuV@NPNJEx`fXO=c^;HBcOY0@t z&odJIYJ!=UaY5YX-(Y8cau)FgurS_%=M z_9+;0c4L8A)?^lqu6$#~%a<-ma)>2hEM^X&I$>}NXU*xtbN+=u?!m>v+!=r?FQhPS z@j^I!1FFFL_XKxsLkw;#7`vDYB5ZSc7N`!dw?)3-)eEUq+hmYMIp!rs=h%jb^*z&U8#h}K@?X4-hQHk z1o)mpzejsh>B#<;-Hg z<5-{`BQJKW$Q3eHa5fzb0(f53vAk=h^}E@9jU$(Pih7e6Ihufh0Ws^|o3@G4zAkga z7^e%A`{`J7VUlx>inQlFcC0~fwpP^my2rs$ zno5UQx9>W{=5O^L(%Sepsl0HYu)_D0eodIDs)H(^Zv!CQ3cEWwliWL_V=f43vV3H3 zEm>ghD+mLj|6e#Ojp%%t%p;LWHFs2WE9GKXoPa58=*+XbwwGY00T z$?0?~4b13S)qS?18oehJkYN0@BFz$0n91Gmw6QqSia#Qm`8|RyB7n72(j8Jalxn=> z6uRC_i=7X(of@xW{#e&|6VZ20KjyUmybp~nByFh2CLRZizR9gliFLn%1Z9f%c$rtF zGGDej!dT%r7;MvBkv4YJjRZh5>-uYe2^ODc1;;8OJa~ajaVL%O@T^bdl)OBBMJX8c6=B7J4>kJ8AG|M2 zC}Npk9S@azVP5riO_LJXSLKC-mSm}%2~nl&Z^P3CY3B38gA2+UH{LD#zEVG9=hZpRZ&Bj;U?;@m zGw<>d#C?B2EYf!c+V1rV*Q3WCLN6>3ObfvqMtdV1ch6bOb-?VFF(pV8)*j-_NzgIi zMtwKWz+j?DT|P9wjuI+)@~fO!TOI$$yl={;3NOKDW-%c*PG7d`u9}F>Mi*xb4)zfR5r8mFHr^4i` z!Zmoq_0e+e?aVqNB>U?g``sn8A^u6U%E6flRI3fG{-kLd}bqh31QQ{ht7 z(^U-AgSEwybd>m7kN&GhfY8T???P}2euLw2KW}ujk+AMz^+bS{P?LVEPDQspfNJU}( zLAA|QMPVg?t#a|&+ezAmhfjA|K4n_5&>OdNYuuh)m&vlLVjlNlPSS^{1SuUjEn@;_(W5oIh zbVSJSwl1fg@GPz%ftk@LtohNOoOb3iyy`zP!Iow)>U5#w_sni{HP1{^kffcl+>!vM z&gSYRcszP0g~jh>IdcQ#gRBV5B=hGwT1xFqFFqH2oSsLmO(WfX}d zf&6p%?FN9zgUCdGvWG#h@NEyU5Rbr^xsj z8BXrVS9o+Z6qvLca(W;ljg$S{Ff3`0)cf{^j|h@sM!O1X_uTtV;crJ9V!l2>6MIZJ zjwD!3&gngmwRwWUFSq9QHy$)GwKl{*nWr-uzd5!j*OVX}NzVV`=F2~N4IN>Z-90$Y~y!U3e4CtbKPtONxUz}2pi<@art^$;<~kY(C?=mUmb85A}JzvR;Lvz z=tl+uAcV6?CMFi&!nch|r})yIppVL_A@PuLRYvibgaIwTM^_d{YNehiEeReQAV;Hd zSHa`2csS5QbSXfnpc{l0)<#9jA60Hl?~HMuKAUG)K-qcj)|DuzQZ;Ebgf}WCy?IHTV5j0nC6!>9 zR_5hi<-YbQ)|%M&%Cc_wWFv1vi(3RiyI1b`JEfm&sHS1x;eiEdaDuz?Sabf@t7 zj;4|p3^vSA7{4N>N@Ei8e*xZn{fyeeb&0FE`DYRZGCMQ-?$fRhIA~wuY|ZR*u5B*1 z!hI874yF(zdJh|GFTFC1b>p%fHUf+|ZTAXeE`bs|v@QiX_I`Z<&gV)4Ou>9`B#X5x zZ1E;X-BV9wy(}>4lS*cFF-{KSvNmh1c2l;%C&t%=tpF#Kwcmce|M%s5?-yc}m!(sU ztn(l|UsResOEU`_qc-y76;9N>vpX+K6;%K;9x3o!`MHnRp(7UtmXdyX=DUA`cl$^h zC-u+~xRMp;)B^nJWYwG2$^PB`YO2B`e(9v0fxWt;NX6ReL*dEJf42|zEF_> zJRi=F7Zx(j<5-0{Fe?0SU(pKAzhz-QIg3`BjXBI=jacS} zSR6yX0a&l+gnE4_vD%A-*l$+is+ZL8;;`FO z`?v&_Pj(J;;5}hbd@>$J&yxSJ97YN=Wn3-Zwku!8;3^&n6}=P$QA**X?-UUkt~2%k z3{?#PBKW=I?Iwdc-ZWKa{dX9KGSvU_QVJ6qy4adNsmv}E&@X03&F9!M=(;SMp;3~T zl$a>`hMw91g!jS>qMSX_x7f+mg{rzuNMPFzHk_RJ&xC3qdPw%9F6Y|1~ zclg9=0M{VTZw8vp8V18bif4C+P9*KCs+kI}7D*E_Ek~YAU+zw@-%?tMe|}zCP$h&< zG20qOQFW)@e`e|N=TA#te)vwY(slDA2!^wci&Jik_#PvYDgvLI`Ulu~Pk%Vzg)VpU z)zk^tCfs$Ub6PWpR^~9>ny%9W8s&~lIVzMagKgaWYP3QD?-Abp`G%XiRE%NTJ(Wk~ zYRA}mn=ac4wOsrFVr2G$I{|-MXlPy5=L;b7-k*#ezWCH#uJe}ioE%us-Eoj;ht<*& zz8d2)f8&kr4P-*_61|Gok!IlKp+Jx?AOw0<;mrJLeSVUGFIjRN4`xcN$He90nU$_V z@*)n1nH#Da`lT0~Ti3yxv@A7QPVrS{#d@lR{&qXAF36+(-FYZadw?%O?F>{={EuOJ zqaxLOX?%ltYF&|dD$VX=QDV575o=!D$S(H<*gD;j?_Nq_X(HJoeG?WzUaHvNCxrnrB{~~lfCPg2Hm&K z6*a9BNCwC=?KrZ^=1W_?3bc+kH#n|3ul5?o_5zxF@v2(>CG%Z&a_-|81uD5WbE#$i zMfF!72yg?ZL3PX4h^KqBx|S98hA;#RhMT?ASMZijwFtW+VO<3@gGA9Wuv?I~3s_9K!WSl17OONnQUs5D2i4KtwZ?nw zazZRR;JsvI5CWl=Z<8OPavL434EqLk9VG6Me3zmV)L1J4vfBC1TT^S|PU z9GHz)N^QYJLi8QOP-F*x0}nE3p)7493n0eWz8SLM$RsH8G0;Ddk{n)#7Vwp zScCSkZ)bB^mNOVqNy&rtZSebi#~KD3wHTt;9Cy{GtcqwR#Eah;$q+tMP=UtZ9EB3r ze}*@Sol?^i4vp&TIL-J?B>VOiS%s@SnMV>7D8xsPQs1_}mhW5vj6|v-x$`Da;}vz_!K;f5WRD}g!tsz5WX|C)QSdNG*YFC z2(R9cA8duEKgA#ukZ%0C8d65GC8N~JHBIk~<|2xr@3Xm<;Mdk?m1>JRrJfSW z<{;%;^!x=;@7pOU>)9)qbkLL=%}r;T$vh$iNdbF7uz{?9BF_L1A$|zLs1)up9`O?T zLukb5UU5eoCLq(4+uG$>(wrqS*osXsXCYdHpisqF%dT|G;MzBl0A`b){xCx*pxAXp zeug8{Qw#Vg#iD>qq?z0aVk5RqyWsQU&n~7wy$yN&BcN3FVC3d4Cc#e=RvI@IlnLl-kI(($BS=&4wZn~%Y5V$$cKwu^aD763 zSJ-UpsHuq{<*#_L4cpem*!|7eHwT4F68x z;a1yj)1=g{@sWxZBZ^T)u=aWVF5M7m*}_qKDZ5$@*;Mw&vxe%_0s+ry-vXl{pCGW4 z>Vx*x7+RDJ~9$H6CR!rI3dwL3GR7J5cBp0aRL+*;RYaDx&u+@%aP6?XL&iRVW3VI-i=Nb*Trz z!hS@y(m7AUDfM(x2!mz$l5h0P`&-td-n-zBO=S zZrjQ$$YiR3eU!x@MYd976X@p1LM&h^?w*-Z0vKRQA{}6dtxXOg({sq8zx z16w#y#SsSx{O;24OlNPqX!SUdfTeH3{6@nH&Hb&|)ixP6AF&;nQCh0tyr{$ww|Wx1 zKTV;llizX(W@_#*tpNlmY-3iMXF!gLhcWF0WqQTh}`w%ml7CrS_rCaGnnoBCK}u5$r^X9AYNIj}$9eG5N> zY>F6A=~Ac4_!!>9X?a@9&h7NQK@63+X5#O5)dE^1C^7lr3Ks2}9h_!8*EPm}>#=!TUQzmR$AoY`p5NlQ%@iS1sHe$wV2){&N!HO~(0oNWyBcjs* ztbc5CQy$D4n@7kiM#O)fUf+fp*iUd0UUSE$VXB!cmpuqZS`;482mupD8B6DGkbQ+) z@i}>Yk{u!THrbJzs;}z!8Eo{2zAu?z6Q=H?w)ok!R~Ckb{!IKdMYtIW2YqKS`%2Tz&>u^Q9Y52P5mZpw^`PbU9{5NT_H7q#rl(``O+Sp3SWYJ>?_u$^n3wws}*dBS*X~oySw{B z+c))E>xV(9pbH;vU81(JIXXExNzt;9Nx`oq({MGbc(k8%B4wVyeqqqOw7PP{mkPkO zM_Ho}OnZ?P-5#(qPqIoP&pw4V^(APOgIg2Kz|E?k=oE`u$DrG~W)#0mVOnx~yL`4o z{W^?)^gt@%LuHo+!0c;)p!$f7z|`8F_lsy9k4tgvuMmgK1wH{l!JI16*$=)HUuiR1 zN2F-l0m7m_AOJM#v#`xje3!Ndt!n_@+#`$J&N)xof$)7+(rpxTKYP-hO@ht~R zeXkPEiLY(6%z!p7gI5=*3!7$wH@JbQVfRI08`!oGL`FrUNTm z<|4wQSLbJSXXOi(8{G6p+8 z53sfG9?;D+8`lV6lanLq^smyW8s+NOte!b1#bIz!U>v^tqMoBr&$L z{wz7L6c$Sp?PSV3H#2+^)kvo2WBHu^Fk#^(9K5rj2c|o8qxTdvD9@oKBmh~)oe@RG z&X>;Th$*fSW?*K7rDNkDzr+#aWlCp>P4dCYsr>L~`y=R|y~WNxXiB(8^0JPC$95_h z0Z}80`(a_H9v=BHPaAqsuCQp(`TL7gmq)K~!G?sz3E^(>I&mGqc&r5ZWU&^M4+HZ| zB%v7-&g`=nAiyh7r4L^Jwx?Va#;@{@?2VkxKsW8E*UHzc$Ql@0Kw*Y06oLn zeWWF&2W-yGReB>=aoZe@8V3~+4#3!~dfo z`38XBnGkQ1DVzs==SRV!TSVoVCnt0HPe7tPJ-MM?7<}yQh%Mo^LQLH}p-?GtIrOm) z134$sL<`1==iAs$JpBf3%anT7DsFkEUJRkTp`OyVwC2m-3D+zK%MB}k?w?fBL5V8; z!PN|KGs{%{^riEDN0uh0gX@9a9+cwmGJ3GJ+9G18W`2-SqWim~QJfQap1<$c77hg9 zPvAJz2f9@Yi6Cn0-w}j&jNLr7cc&lf=9WT*&LrHxs;4wpApICbK!QGH3LQ>fuZ!`( zfbJ7)OXe^^n0IaJ^h#xtNg@Ptmvn$%gFxas>`)<@LG;?^ZU{C{z@+O0pFx z@$=Nk6bl;V=6@r0_~Z(((}nL*-2rk|!pk(P_*z|yZN;i1_!A8JcR4szrHhFz6WwF1=H*jg4>LC`c;`Q_3f>kAh_8mXV#H=KbLfv z+NGm}3y32{C;zSp9R!>c8oUGu;-$60RTSFDS_tj~rcyJ2Q~R9MAtfQ;q20|EJhgn1 zyF)8RL7mR`AtANihJ>_2)43VT2?ikTEThx8_c98WU7Xy#-PeKks!|7)fNZ8{MW9xN zb1wk1H8c-CULG;G_XC$Fa+D+zn(3C{_GEAi!b}0-HSf)1&KFsxNOX4UhA4*gd6x0a zcllKF3~sPEr*s7aH)~N-7u_WU&r1p(U{u;+^+rZmaFH( zzqc`#oJWOlKTn}AP_;EUDi->|Z6+Q~eY^^yqN7(EGr~Jz7ay|Y0FwL};PuU@YLkIu zCPa9vL5K4Zsym|7+a*ssH`9+3vVkwTrd{WQN#{#~GT@$ngw&TJo>@992e{AT7b==u zr`o~nfx$*1FoTHh3}m4&c$|{5+(*C~Z;4!@{BsQN*kp$f5d7Gw(7JMj_3 zUB9@u_*@WxoO3sgf-3`goWh$rK?QT&Rw%A%s4+2-<0V8(U;89%4NkavQ%QgWCOxA# zO(`1 z2||rF{MBGX0G1IMBQ8tb`qEcg43rBC7H&A5$0p&4tQ-}zn^t-$8e+)}->F^Yu6v)K ztuY3GxgKCqGr=`aJfomaVDet901a!_8!LqjIZKfwX3%<(I@CJ`1VTQ*@k&KqT;EF@ zXrzV}ggA2`6&+>C0k)OE;YRwy4O#9117b>612<3GrXbe52d^p|mqv5W7~JBvX_af= zItkTq?%!PG-ipybv~*hjYBT7_Bh{vys7klRmBMN~(>Z7;;}+c61jp-#yh1WLhTh6HCW@+^|Ov?)bqTr1cuX zpde6AU>l#T?h{4m)le2@jWen{UV(j1b-9v-{^Bs6{#4NWe z-VA{a#TVslEgCh+Ct1vEPU3JhFd4EFY<+7OM2;))TI#8dP{z_sjRlDJlFzBjiEuW6 z7ZZEuyM=)=*yyP1jGKpPn;ZclD6!kA%en$v)JV2&u0b<%cX`?ADI0)I$Gz8HqmEpK zuT9a`gg%XxeA*&DJQ-muI9_$2u?Rl1EE9EWzItkBG%DT`O2)zfI1o=>kpiAQ*@po!Gt^S!8|A_*ub*~4~?a4TTzmmQUx zjC5}XwFkwmXT?W@bg+@6=_>58a#21ADrAdLNb(Jkmz#86aiAn@YpXZ)CucT;x*rnH zT?uZ`?`o;k$x&(mm-OMmPUy}RWqgk%$9(xr<3tg2Y+Gu5qMhjIGiTOt2|MC7L+GCG`AXCMgWFudn7>~*9@tZIH|1C*IM zCInw4&Ab4>@(GWu51=f<7SA@&Utmx;bA*Lio!Fwgb@Fb z9cCA#O#Q(8!~RuF&;u<}@;O7q1h-~8B{vDWxogEFb-go*Pg_0Tq0o*wb6v+1P8Io{ z-$Q$F%z_hA`ZQ}iVM5P~Oy1lAK&83wY*qOO5RdsMllKKi(Izox2}NavSU4bRyWAn> z`_h*TlH)>PPD}?%3(|JHktB;QSOdYgrUJa@Py|Boox^lTBkEMYfVu5&_EnqvPB%9B4x{x6E!z-Eo->nXXbYTfyUWnD3A0 zoDeg=b~2_PB(PDp9kSAVm`tS&@UBN8@e(z>A1j{A-rlI#3imW|wj=(7Dn?em^tyP2YA;YHx!wW8ss=#mLZw6c_N zJ2%o(il&3%(5=WVH&X3O7gmLuB4Nbgmn*wA^NUCm#qT7D8lci@ocjw+LR_3X529lq-!Fn_Sf|H&^cXn zWc2XbPDGj=KMh-+C(6^r1FX4})$a&{iW=Oapelcc7G z007AIqnGo^WFWGcLyI3jjRSZn=@T$eum0+3_SSKjQ@;~;B*a{|<%j>yM`$OjN2`4J++Y0=-4y1f23nQZ{g zGi)k=Mk&awE(bG89Cv}q6>})8}2B=wmJlvnd>`oQ=9fdl$QfOIw~L+(ySLpjV2CWCCH67VrUc49@RLC z#$WsG1%b(Qfb%k7Viy`IT9X2~=v%+qUd8PkP3=rnyQc+UKr%(x;1|$5ngNmnSo>+q zgQK88)QF%~Pbmg;%t7@BPm@4EoQ;r)Au6CX{n9%tA$HfknEFUMY~ieNIFTY0pcBpk zwFUFk8Fa7>V%)-+Whg91uxIvo3Oz+180^n*7kyxc5b7$jmr%!FW~K1c9&nqFKs*$> zZQEyi*PS->0)9XhSSR6tMsR+vnb2m>LGwh?}* zMfUjkO{{Th<50{#?`(MRVb&P=NoG}UnP8(H?~^)3qdIa4FX+L2ltWD7!85s-NBmE% zX2uV3Z0BA?&MdvNi0cad7+UtW=#r0|lS3PyY{MFNm2MyOm*ybS72J#C*-ZozA1pZ{ ze0r2-e)=VQPH1H^7Cfm$H6~02<^pk|2H8V4ljk5x5gMQl=w*n(ZR`t6!%M#pj*MmI z5SCcka+D-8)hs(*ITDz2?B~=nRC4u<<|gMFF!{#%SMC^rC()zkm;rUH{tt2oKFS?B z+)PDW&JVS8tfVDryLJt6#5vW5NJy{<2U&vppPQCx(?%Yk8H&@*D0PRYlo+lb0>DQ0Q#>BJ(q2<2gSsIW5? zgqY)1#DKSO6?7P2+jKZg4R>#}PWYW0_Q!w6Iyg)cV8hZR6zryPvqkG6aaDMWQX8K_ zT5T+7>#1~AQch&D8(IV5nIF`=PaI(fV>c-S9ppT`&>!^3SpPhM(##H^X5P+boq#I> z`NZ{~J#yWX{&KfXoaaz76!J+_P5@ak}Bsf*r}%W9pCQT?U~F?VS89xZgeoF zZ03!>3$zYJrgB5iU2L-ez`%u4*ec2A=Aq6@b|0#xOlQu9v%wIe3WwVa(U)GL5gzw| zZ73QSjF&4d@@ZE}4~ALOmbOL6CKe>2%sDWCgMo;64dO;}cpx_3-EE9V$HjtW5cYLL z%ML}p^k+)!dpVD!FHqWv%-pmiS>)=_*YbY>3}c>v&;{8bHFl8xwE~(IbGJ`qi#y}q z2`KJJOv&UYW8EcU4)Qq_ypLS#$86Y7qr4oaHVq507f{W+6&?J65YSX0!-{AOPPg*& zCUJ&?W%w76R-9I2XH_q}mMPcV@f85g+t(J?VaCsQ{?NNp4z3{dqCJHULu1@PC&`7{ zEIKkOuNog>49sWL&UFm}yvQ;Q_;>+~5;rMjG)cxH-d}j8J|nC28${%Y%0Pp*PfC-5 z?mHWc+KnO19L8*B6bF&+2UJkOyjfr-#fQw*lSiypPi(TH-7%@62Kg0gWoQ}fQLRBwTH5|5fEhe`9mWq*r!LxgRFBL zpAeFuOB=4iUokFo&f150znh(p0pr)q2Fw=-IX5tbVU>$7@p}ZtP#5r>Ao`gRmaPbg z+vD~_;Je&T=Mufg-cF1#BL#NZ;J2XzcQwuUu;+1Be-J{g6 z!9cfI4QC=OTYWG5x{BLTD*(<)@hQ6=pj}V-Lj8u#_6M!o%4?`=e{=}9V&tRu0TL9x zT$2>>hpQ2@LxzrI^sKHYT8x+*-ErhXKY1#8am*Y7bci#bbcG_>xevjD9b*Dl^kE+> zgRM=DUrZ6e!{HhrYe-#g2K|LnviBG&w#od!Ma$DqmghM-nnF}|7-spdXMW38+rf5U z=s-U2^XtHd{LL=T)BtsLUH&3bZj8v5ZZ4qd3^BqND<03b5*c?*0} zyRdVN@AM8BcbLE^YaY_U{S(wYg_6-)zp2KBJ;8lgR;U7Fc@Wl$h!s@rez!5ZN@tzh z6xF5`$|NoDj8UU^rh=-W;4ekQ0>@|k#dqNL*?|rdN^Z^5#lHG%y!54cIKnaQf+oup zA4%tzZtzO%__hxxeE&JsM^K*!eQg=!W|5g%*sK~YHhQaZFfHq=KvwWlX-4vIsd*nt zO|pWslaAQCRvXHuhWqERfyB$iz^Tw&ll?II=@%DQl^^Fm}ZF&tlX3heh@`*@O z;qCHI0+;R$T~kbc32sNdN+$OcC13N1eYQBr!G2ZnvNoD=PhitVovKh#`aW1r=fEIN zJ#Pr@yjIdtXtfJK0~cj7T@eTY?s>THmZ&Dn@rpn={iO#4QJDazB$JcB~9nr6zpP zSwQvn5+BD9GN@Jzy#b2dk08*D_lfVj>U<8` zwnE5|7~naKrUj}|xdzL}Yabrtq6dX5oQQ(mGoB{(LvZ?>I-CiS=G)(nMj}rG8x5Q{ zui~8B$1l)8$z~qSE}gzY1+IJ_?OpC{-&e1Ke|^56Z;yv<)ZvrefvOvRe&L@|PRR=r zNGDQI0Z1ab8xz5XMmKUIE6DNQ^>WE$luc`(_VZTu(F)qXOAS>^g^J%UOAj)!lbam8j1&*~CB;AC#@YdufKS}54 z!*r!aaND;8A!o%@%W-NR9kk5B{GYX>Lwn^ght(mIULGL(6rbZ4Jo#G`c~<+t?kT>0 z@%+CZ>kj~$9;1U%($?TbrPZK7mRJU&vAg?YfjeCg z=klPkd>%yju7s@+{Ik&;Vn;OZ7u8>HvDdH1un57pA8z&$R9sgDQX_jWT8%$nbPFvTVOkB`479p zvc!JGB#bFpxig+W$V=5u1h(IIX0od+((AMDfB51=OZ)xEd*er`^?#t%iY=W1RH+xn zerW|(yQA??R5JYWRu>!U9*;EoAC!e;k^!(W^Np5N4qjdpBf{FPFuMo>pRaj9*$ytR zq?~(5grbRUuYmfLz5-kE5%Az^;PiV?2L`u50Z;C{MR*>q0_d-%jp6JN1Gs@>S36w- z?sM)T2iKw;;cj%mTp!?qv0i7`inL&jtvo-b$ z=OBJABIB;Hk7OsYe`tUYJ>azbAwfWz6E$e#;N(XxFzcj(aV5ZtudpwF^Fn1la1F>h z;j1eB1?*)u5TWCe{)rip`hX}qs4T|;d6(SfhsB2BcKotYq~7}EaPWi_2*6F<1c=8f z-~@ic9MAi$UD53X=yMAJ!-WytQ0U|MTWc<2t0Oou5xk;>+U~I&K}Vv|1*ZZJ-~pfq zQt2qe=JVz~mN}LEX7|XkGdh4WKdPydjXQWWqGIreN{=HClDj027kKunOH03iaQjn9 zX2I?f*BjPLO`z>{CJ8OZzC1F&a=6rnSUeo;vvs79pDR~x>1J7lZJS&s-}%i7=t+Ax zv9cE`kiwb)eC(mQ@LYyY4705|T;CwazCBz>o=KKqU=HkLJ%lCN_3P^SM!Eo)fx^EO zonJk6o)q_BnR`}m0xa=<$WP+n=V5U=sKC|$@Azu&=85LzKyiomST5w9GHZIWgNI1Q zgVNtJ)e_;s&;3O2RcUp=Y2=1Ftquey4Q`VH-F>j6mVxf$sXFoMV8sFH0RG%? zM2i~`G61Po-3`e73&A2h`5Kf4?+R%T3JbWwcYwIl*|_!ILaPOSWDN%3c?RkOh(4?$ z3x`{#_uwybp#@e$QkY$yp1;|T&>#yg^vCL>nZN7xt`O%Y_-?!S&Miy|?w7#+xK>;E z)%W1}3(-}=t{t6e9z0m2e|HC3ME>wGZ?Atk`S>#!;RKluvPu`-u6=ty$Oi^oG`^a`%7K|dnJRBb9jE_>nmK~L(0JCEbH za~bDvFg|!dkUdTTnkuRr&MW?Z?x+DN3IF%||9d`vSNQ+d19s>9Z$12PJ^W3k|Ez~R z@ew){J~wE+WLQh^kNy)#hh6Z>1vkzg*%zWqBPc#ZmzEOo4ABAqnK_0%ApBZg_|H#7 zA4`Ud5X83z$4~tCuj9Z!zyDh*{`+r4g`oDpzwk~33oQIc_VL8WnfyQT9tZCP@+k0< znjrhXS&t64nitaTKbhg+_V7`9&XNn(m zBmVdjc!o-)IudbNrD1SWL z|7#h*s_-I2#ZNVpjo*L#Dt18x;|x>(_o{)42oH)bhU<;g=!zoif6GaXNnjkYDM8Q%?aLfAS4rsE|5|Z}A7P zjA!*LBD0~&0W`1yWPVOqSQxF#nIRZUV?u+~-l5;<>8%93Kyju92A*a};jlRTd71xx zES4&R?ueXl%5o2+i~8WU$WcM51OK#wGNNS@uO4V`z=_BJv?)b^$zkC1W~93M=gRr; zwy5?&jj0aEVTQN@azMEw*IyMJ9 z*e8HG>dmTV(F_54)$Ce2lTFcUXr!*dV})Bl`2)=9=JuW5hS8gUQR;Nccpw-E?95VF zUj{r!4Ne)K1u(UUQ6ebOJ(lfnT&}LF28`Pr+@S2o0=dB~aaZsBA{>H6w(OP%{4_XH zkmFEV)H9y&D-i;OQNOa8<2KME1&E@A^BOpA1E2KHa@pf=F=2Jd>d&~8da+eJn1^7% z#P&=P;TSmJSseKpbV8C2K8)z5s`oj51g3mljf-GWe+>A4{&AXH-sgn7j`+>k=GYv3 zRMQnOgE$P9Qc(b9^32=qO|#d*y;ni5*=ZtqO|Jv&e<57@S4};TQpKxRBj6DL+&~vT z{C~pY<004OKqP@v2D-j|Xxfpq!mtrw!tz=jK%A|Pd&x8{ak!s4;sx;jCwv#`e*T3& z!1awN?*NKkMq~qk$OuI(u-3)?{z9-uJ@5gJiPfdV0}QAD4t^5?rl$!%U;O`R??3!% z?*Bh<{B$ZUS{g)&5+aFq(MhGDrM*juhW6eat3eA5r6DbCDn%0wN<)*jhDv+T?tCB5 z;`09e{)F%M<953)uFL6lUa#lt`Fza#{r+eM&e{|R5;*SFg>u&vYNx}%>$mVj-sA?J z6R8GHYcavJq;44D*xeT;2XA86q(sf@9~W#Mw;izVFM9*6@&Sz_b2hEqwhsAbJI|lY znIzzWN@IY`b#$4}6KU~cJiYbYS%2xu1US8jxsa2!=FvA?5L z2k8!DC6i3v-TDozyE~m166AK#u-(+lwnxOFxk1T4xLR0V3f`n5w|mf30#?1f_QU6{ zx2+CjFCT|1$?b-h9K@$04J-CB;e2V4#h3R`eY}AP^d}>sZyg|nf#og+8b@hx$POTr zR*oM*M!vzKWkZ`5vEJTLR^Dh%K<$&c1WrJ==9+Q<=1>@hfcZPE(fXQVN%%XhtJ z;9d{WEKGIN=Hr(90+M<972YC~#3kE|J2)Hfi4!}Mu-7vDIh$MXy?*ySTgE^`KWLVxP6PY zo(ang#*D3d&`GOXO1KR*_kU6ra*V(P4?s=YXIv^L7rj_OX08w(`Wuvrr=Y3&uO&Og zWe;3bduHQ5M$F;<@J+E9=;Mzs0Jz{lL*&V>Z*1?E;l7E`eGjJ4oyCsPt19)Q5-^-b zrw&0?fd{~4%LmR-HvpCuc|s6#6oNVFD>y%5(9JZf+!*D}0pRX;YhG&Z@A|-vD2LG` zX`Q~kGqdK`1BvkuP&1q_^d>2hHg?U-Fj<-+PQ)p#5p?}DZ^B(r`M5 zlbkicnG->?cy}>t=4%xJU!z*6F<+!lg6;`IxNgHgw-ZS|Auo^%HZn+pEN{9)EDyjkWzU&29Np9n&NHm7i zHCLD6i0g$nx48p@IS1tK1YzWnnD0i(UnnBhr z0B)AVipaoD_yZ`*txFT?zd+5WYpJ-QO;y}#&jymdYUhrD)A9v1XyMq8(~)B{H`Vh7 zpp4vefv~YS_vtP(!a4^i?@~Gc>oKajU)HJK>1p}DX!3)%^}TJY$F{T)rh*J!N`>ae zvtBzmzRv{CxNO;`-NVra!W1R{l7#{%5^oAP%Qd!dMx*jozuckNYd|UlSk3U5eS$Y* zScUDYc9bok9&nzhsPO;{Lj!lz1Xgy65;sW=a~Cggw)`rNM_>LF4vGDxTLe(u%0%6# z_dE~u0-N>oOpCbRf>P5o=)D#G!7%Ndzv_!P>$dSnTZx-fH#(setTVN6Yx$Xga|Il} z7E>V@7sk9H&IW1MKQymg0S>fsra;lhCW96EgV@AGU6q!&12xa}8elWu)jxC2+W02( z&helacO`(g{>l+1PS4cZHgJR&8YnDjOqEDRPO8C}*8z~p1bE_gsbG?ZXaJjL`&QmhAsApns1ZyTdk-7c~m|K!LOq zrXFaModp(8J{QPQxts!a%8pZ+gB-YOwwHE4a6rLkngb8Ye*9=}j=qpKABZILovMX7 zl;UC@3n~9DO|m3(?Mt#u+WpBoHm}c`gNWpsJ476bjoLB)6iL=IMni6cNT!UB&-!J;EeWvkv{-*tw+D+zF-d$}{w1{h{`zTuDLi!4dH{W@=(4qT zV93&<4-f6r0Il-1o(6PiT2EG;l=B>QXWM3}{YKR6XG~x8c}L)QfpA9++7d`I03@og zfA#=GL!1V4(I?bvAqJnOl(Fue0v;QhR|}}zxSbWE2V;gaZ8~s;1Gq&+ftgC}HY+d( z$dwU|rMzhAZzYOwvVSl?tJrmw z?LB4-K)*`MHZHHBpkvw4NK32SV{~ukqH3}yknvn$=$`7>thp(8r+;d4gYr0m;Ggmv z6X}WFKz|rK!4ExT4U35s&KeL;`wtoX9;J4MsJf}gCTMLyN{XQhTv9^sN>A<@h==p zr{P(DePg39o<*S;gg(qxlCT?Di-o`Pp@0=fT2)mV5Cxz#+E2jQs4O@a*E6~xk)3+moYIW#W5(|1RSwL_9>7Ika zXA+)mm<;IMz7D|K!{4(j@1jOB`~RvMu>%MB`)fs>`jMRF1*DpV5H zAly=N)!X5m+91HHe#301?f4T`)5$!^g{+WT^8d$rLZy1UBwfPr5nNeGyn1;=smtUK z?pmOA$6WaG1k|+v`0_{5iJY^*-^dA}qQUu=7TFE-cRT|q{^7#6({NlzKRv4Z;~)Py zKj7;Ax-`?49+qQN-fh*M@tx}uEXgm*rCmT0Edt2x_3J1We|QDwvV}BT(41_TdGcI! zaduQ#bBq9Seh(Pii&^@%%gblaA;11V42E2i)8$L8EwOfhmfse()4U04rBr=b5dXLS zd3^^$^-%|z(EwLpgvQ`AtGzZ=%j;+jibh4qn0CHi)2~6xTt+pgFzRzJ5O7Coq#BoX zi){_X0O1FsgTlYo*(%(D6S^H40cSwWb1CCbpf6`Zlv8vTVBO)EnU1XJEHjZuOGs4# zV%o;Pivi;%tKK-T>_@bEga(U$QKWIcBK-?&Emf9$=Lt12pS8k2k(L}eQMv1BN~Cg7 zBDJk}SQY7Sb-L(CW&iQaqmR@1Kog@S`6r+?oh&FuPY9YKrzcJ^1@d;6LJwLZ%V`5d z@q~+1lhoV83S{7#xoP(xL1bq|*TmPV1CA#V2jrCx)5*Z*EdU4jNz)&cLti5NZJJ7= zEzr~5{zS6#49I9&;=52ZG^1AmKi1YB{2NckmJg@5_EFF_U(@FFt4a*eL*=C){fGCFcRS9#lh zZVeQ$Ujnj*8qhh-r_EeBt9?^vv7! zjVFjaS8(?mFs4f&QN0ml=Oql`MaPfk#93~nwWSM7osHrzh>Wyf((Oj?g=sr50`br5 z5akM#zxH4Wmdztr9lji2nf?T1muUmhbVQxw1vJ1(Iu;I5Z$2jS=WM|4{X^Upe}f}P zq+Lfo(*3!n{b@V^?0X>F{&2)oZ@jq;ksFbGyspRyi7!XJHDy?hf%kfOHBpwP6*h<*u$+Hex2Ism$0Q6Ti82&o}r zSjJb1=4^nLuplImU8lCs^kMuOSb}?<`G3-NK&734B~DnIf;y!E1uAGg<8lVhogj?P z(ta7T{~VVy)p%E4;xr_1f0X+eATdzw65bSW}uex8YPgFT6xpkz%D$2ZjdqJ z94d7Hwiy`pFxvs{)>tQecEI;wmmfN>K%Tb(y^B|%Mq4YuIciDmj zGW+Bm2*U!R^4$7dq7i%NkG*tGaAjfLdl+RNl_b79j9bDb)aMU6Nb=R+oU%b`xGU z%_MQJ_hO^%H6~u8gQEK_u4Vz2Fs(K#W|jBtE?Z?)tUNa!c$@kl);jgN$M#&LyvKgn zjFc{0;A9Hu2VT3UHVrgG^Zxyb|S0K(Tv44Q?hq5U_eknn58>$d2uF zVfn8Ts9F>tNr}JN`7vaN@g#d&tUsdK8;W&M+Vcg`z7^PMKy`K$?!^53 z37WCiLVTe9S&fjoC~4#Yu$t}A5rj%(j{x()*?N=iUp-vM!=DSv?6xqW+pf+Fuv0UG$E_a2nz}&IBH}m!$c&+r2khS+8^gl zvTZ3Q3@p1D)e{OR?Wk@&q*) zU5I`Z^NM!%#eK+i?Hzo@;a~v41=ZkSNYB~!G^?VUD$gOK)q+XSu=oi80Div%yLX@$ z_E@-9;w8`Ar6*P0HV&0Hi+5`+QZ%~YOa30^i?H3~#wR%3k%>s^ZifLWB12yaI+$;R!RKmvqDQwjM*ruw1r?hlfW*)GnGxnu9wjY+Y ztU=<)8psSiH-W>M7Hb0Ag)^+`g?joFAdrh;i|9gG$B;E<=o^f6aaYo?)Z8X)aOS z>@WLQUUk0GJ9td-&NhDCNjp>`P`M_hdxF;aqmv4 z1izcHWU2MCRLCTJ(|Z=8{cq>*14Z`A^C_S=@0Je+O;>}RC$E;mRhA+x$3Djfp0oi= z6|;qrPur$dR5K5yI&z*w7kZrqYswZxA#ut`Y|#mYm)Y4AFf<8Cg9$_}vW{94&<_B9X=P8Q0Q17Cv&iiKld!V=JM z8=)Jd#@uj{|Fc-Lg`15Obce$s!|>^_d5gG#kO5r#S<(7--fQMf9VDJN0OQHmL4OX# zasGOqKX*!XU~a#2S0VC8R7T}`->f2!8nESPMTl~3o+^pJ``Ei+BYdi6dgn2iL8=bk zs12lnSKpoP_~^MmlQkEGtH0XS(_&3K$>vau@BaP%bh`wL1ipIKgmu(uX6 zOere9qbGnGFo?<)>{i${Rt+CNkRnv9j7z9R{X{^`(*%7urTc1!;>i9R+e<|2wdgbx zK~g2+VM_R5C!yQJwqE{97EuU+<{WB&GLLC-p|fE9WgS}qWRE?N%1^I2(69G5u@1@G z5^Xm5z}QzUsEHw}ch>EW?!#6tP;)=o@Z?yK$38-0)lzhq&v2BVbv+kZYr#Yk`2!MIuP#^{~O?48+rbJNq6oR|u#_g$b9n;Fl8 z%6uRczSN{;lk@*y=F>e?)lf|fhOHW7mb*^{T4V2Vg% zft6LopDqJ|Gx6T63=TW!b@23-B(|!CpOyr>MWS&T^o2(4g{q)4Xh8ZEP{-jZ`v78& zrsg7D{ayh=ACF)Cl8_S)4}GK`t+Ew zs_(M!5K%CO71~XP<1hM<59CAxCng?MLH+p~bP5a*4V*I=@#8=AT|n6@vbMSJXQ0?_ zp}Ur4yjc$iad@Yu6%DU`@TMLTHTZV~k;iYz4#9}AC{@{ zGi{(7QOBy?*Zg}8E#4$s+(BmA1KNUXsyBWlN zwCh+m8SEhTnfpF0xlYC#9-uunJ{4)xxIbwLqgzfF9!FzIru)l#kQ?$BBianjh)1K{ zY@m6XI~0cmR}v*83Xf}MsUgoAz6&A<%c|fSDG$c5yN)cs=ci4LJ)_Hn_NaV+hN8`k zcbYsi?+63^@%{~(?LpYQ!&F2I&i&#~1IMf6Aa6dyedZzN0=xgsS0sDxzv7@g61-qf zdj+>?fYW^(R4qw#^%~vP@Z%Q#$4GY3k~|Xje;3Xih1+exg#G4D_(`E0&e{aKF!ma7hJ5iHu!;6o-7T+wU(CBgvw7C!ZdM9un96XT>G%`OdtLB zu3nGzV4tNjUb&e5!9!+swSw5=MC-ba3=y zQYY3U*S3PgZ^mQ;q)1QK4*_UEDbD7#?i#d$zm-&%WDJr>CCOk{9oMLM%kuf6FA(-T zE;MwgVYGz(9Flseagq0hdG*DlR;)*VB9Z_5*cJ@#>Yj%mg8a&IF~{=_??@+QVnHiH z5eA&8#6mi=R@fpVYZX$Z2&>&*>P*kEm6?@2B2{p|lPk{k{)#yueUe25tqk z(3C-h-7!|g>AL7hxKwav1JtP!FNLx7l}O+c4%l$C?7{LjFCGH^TKx&$2;KT9GSWr* zvrNIArOSKk8ip2ga8 zOr$dPChx!@c4NlyF}WU5-j+%~LKE4QFXRwKL^cTzKd2Qa09l4n{XQ_!`Jzc96eXb&Ch?8j=kkB)+1-O*=NQn#EuLyu`W}qm@&m!fUQg<-KJJR3eS#y z{-&{)*c(U})S|**K&|M)M~*fluW8M8aRRs+fhg?x$?Sqwz%k@ClGC!c*b>2+WSW)q z*oE=*Zx1pYdiZ2r^h)ko3fhc3Tmn_umrDmyf$aM7C6l{6y+|4$G5BqDqQi}uDa-wt!K(w?N$_=>?j3t{j(qatl#OQK`3PJSQ9 zO412K-JIL6$O?3lOH8CZ3eI#)Xej%3JuVgIft=?7xmGwRZROb2@+j>r_hzsDwvv&` zlvp;Or7shojwLFB^sToZsMjb^DxD%BZ&x35`<3mwS|o^3bv^Kg2KQ~6Llj#?`_cN& zn4QRC>~{}khZFi05>>!?d>9HQu=e1TICIaXBuy-&(Q=6!Bgfi6${-5Dug*+U&v?58 z-?=JcniSBmN)@V@4=KOUPXve2;x2AO#W$aEwZ32$y9={s`}NhqA(}dV|EP)j7!1Cj zd_<%gawD71C$?ZjQ-7S4e#IA6SSIJcd6R^fZPw*kj7tM<9P|102*NYVo-`QzSU(=ZI(!eGE`Vh(BjTh2cBMH?9lhdA#@l zuV1tmlc(vuQBe7-==Z!R5tc-Ob{}M!-2gvn0|ml12(w4&FRnT5h6%T(o*xeMhXc%e;%MU0_!!du1^Tu0Zk_MU1 zVN~shPti>Oc3-kA+Hj>mOoVM9g?{-{Wf=I%56w@0XwfDc{2uTl)b`GR24aeMSC5to zUi|0?s0=6vuz6b4&jyvHTszSN{N`^uj*BqiS9Q7 zY9YonB8{Z@J97S2S)>b3M{{0xXYvJz3{+*OOhfZc32dB$*^C?bfzCecn-E^mL##c2 z*s&+NaCDto92#qub*yfR(3v@Tg8wBg9GzmU(=g^T=C@W($T|c99KU0k7!M`&k=rZH zAC&dS(P2HP7a^+Os(t!H{CrXh5ATf<8aNatBYlJ% zOG3UR$=+<)Bt-u7f{*8LPt%2K9qm$!2j>i3zoj}f--1{3@fBXFV_NaLM*`}6$A*gh zh$#e|MJg0+UAV#{AB#f2r?&M8vw^*2{bkE;oz65oI)R)fJxAgOo9cm!qa?$m9n=85t!`+&~w{ac{yY%fvrY2 zt%E3$Avy4$-$+GcQGJH?CR#`Zo#!12m!g*6jto!khtQ|Fue*E97sg@uy-&^8rpjN$vRNu*#IGwOiMR0#gL_oGVh zf=ad5-i#O7gh6=3z)b$L+7LY%TuJXJt$LK<)&O@e*AIvxf?{%-xOWjL5?cto+Mu8v z&%hZ8&e_;`>BsV0sySE}R+ioAg~>9FOK=Wc+U38$__AwYt+Til!sG)D>yg(SA;sh0 z^=g4WX7#)cS|C=EA@3WQB(5WD? z)FQS1Si69x-;c0);5Gx&Oa`Sm-GYMX0b}xO!%&k-gQjB2P2<)mv-->WzB5jVmpxuw zMh432c=#BhIrD1yNo{j58P=~>KCE4>xyW=HeaWftE(MsoaQ}diXML zJCXkMA!-dQlUZE(0G;5hc^d+##1F)L5AC=H>o5ACS zkiHYit5Hxljs;9()akOh_dl+6rx+}pKfF-9<$=ind0M4ie6CYuSpy2dGH`qVr!+ahDkqVJ7CrB zdwm@X>QKsXbZ>NDkFyEZXE{4Se1uomqeYS|71I42T~byVxa%4Em;)VeN=q}_%ZvCh zs_$OIo^9aX_c|PQeTxPjl!$*#peZ#62HyXO%Z+?>9xC1;aaFALRYW0KjJ=1F#(4Hd zho=P`@)c(F7>TgVEB~$V?bTz{qnto+DKPd0YDTYGVTFjWeoQ4IYsc;rHsTHiGo8Vx zIxl9QEjd(oa#4FVlNlCQbuUEWp3g%B6_BYhYy2gCtn~3T3Qs)tmp~T8Gb%Fmx`XO7 zz-7O@LG9HxFj*yQ@jV+>70DcV;`t~%+(;+@s|4vORkg>LQ39dy%7)4=Ot4+DHrYPE zYg9w}55>t%;3Q!1w6NJf#!rhM6Knne4!>5YB0{j=eb^a*tto(*dCZ_k_ww2!kcj{j zsRSXJEgfI^!D@J_X@0237Xsulc8qW~SQbopnHoDU!L99N;)4Dsu(%4g@g3Dnhg|*g zW24KZ(dM8W5%~s-oEqW6w(t7wdvivlks~(2(A= zFyE!;mJnd^a0|XePU{T*)BNZ)qs(3{I^-8EGa~PZ@FjRT9&PpthQ%#n{%HNuZ}gISWN0o59M_f%~Feugu~II>2Um3CEiSc5qwH@2b~nVZHoI^LDN>jGGS>!oKQA| zGxFSdOIgf;PHVri20mn^Qa~!1BjdSG1x9%TP>l%e&T3-N(pT~Z>g1&Q{uwgfFn&4>w6LP~>OA7`fC0&^fW{9;SdAv}#m`)0{l!xYK4c&X71s};Tp9$k zOFZXf_rf|FVbx9ZeXPne4K};s7~3DHJ=8l)x3z~c_f`wq^@4cuXfb2ggf4qto2FI3 zu{;HT9#E^W3(%=Haq?MI^)4R3-EZ9vXThatF?YcLV~BoHn6)$?<|htNh7#?%?kDh8 zNYsoNN4cpx_QG~A7xa>;o)1{&&2xXx3#-VUuZJ(Y=e5--ls9n!pamT>Jx$CYVb{Bl zcKyKgJ6t0F!xz1E(7A=0(mGQGBOOHb`1%eC0NizTonTS3~h?H**|yF`TV zps$OfGLTC4ge{oab=`0OJnv9Y0AWGC5}w9*ig>W;*)0n8R-O6HHB^z;ub=YHXR{cECCsH z^loS1(YnW#n%h;5mp@Xe!3UJ1(m=umnD~=k#-D=fPb9Hi_AYIX53lHx?#2{GmI~ah zA@4n8wx`L@T4~1dD7a1^l7;TsXah(2;aV4uT=x?_(8Y^;-RIiljGUx+am8aahGYT- z&L|5(9{R-9)a_!2FVb3Kxhz>r_+)CjmmI$BEpAZT!6?3y^jC&C<8F+zd_kQ-ty41h zsE->5)!)1WiKu3Edh`xgYw2HnuNZG)S52(8=R*RSi%`#i?tEuk_APEOe55W$g9WSqXFI*j2SMGN52RSh$X)*)EL6ZKw%jcOz;Jf^* zg0XpCeUq&>U?0-Wb>N(vMyo`1)D%wj4XW+)*i$ zq*I!`DKF&Bh)}<~nc1FIfltph>ri9b68KLi1|}{vf3o{62z8OAR9>GG+hv}v$`mw{ zDkPE&vao{3Js-FISPo;LrQ@)^{;ghpp214MnP&&+@g6D7EK6K9+w!pcxocW4iAbF;t8e(8S_(;1UHMQrvs*Er=9-DppJ zs0NfOXvdZD-m+U|-h;~(%jGlKCikrb0N3vfSTi9lLyIePui$_~frg^@05PU#X@=G- zn%e}E)E;Gd(SQ$gM$kC7?N0VJNfg9bv!e#<613`5popFJ>Cs|Lwv8qhm36YS=bGFq zAdA&J?2!N;Wu*-S=N7p1oSGr$BNO>Pj0KU^3EfL$guEkt%=2Z>AIDU~CGnz^r(gmo zA`&RAPZt}rW)Wgtnl5u+kAq6hy1;S~F{~PSp{9wa^A_Q7YwPQ%%={>d5;9zag;=7) z#P;k3)(ebYs##M)G=B% zZ33|WXQr<_i7QRJ$bAt$bec9$j5{5w0NjvTaUh_Eq3ZJv?eB=+4a1|oyB+FS;aOEi zHDOI2v(K55%umx}>7<;%vh#NpeC;)*9aN|5eP~L|un$(liaUIk1_D~L6y$u=l|@n~ z8I*;;-&q2FRVt0oxD-m}4Yeg0kAM>pqO9)tFc7x&A=@@vzi;vyB`&0&2qJ?`*Nwx6 zw4ZZlp~Qy!C|vOj3J1Q^_zCxUI~DQv0yay7MZ(v+8#3<3#k}@X)qL2Oa=e!k4-c&R zaowMR5TV7#=50Mo4Ov5F(30HLT&32VM<_1FB@JWwOyba{F9dhvA|53`VFFY{bjV(S z_OG>LiP`qYZeza~3rozjIhvA5sX9}UDiT>7wzbr)J8o3*XiPV?<|rF7&F-nV3dw$G zpR^&Yu^Tj#wJUVy?8f5VjADiCj67$(2C^27?Zqh2+s&eLKRk0~v8bbLCN-F9A11?{ zI9GCI^*Wn+=b&%^vgEip((PL9NQ7+Ke>u0)#ChHsm^h#uCb8vSzqr{2s&@&8EL70< zA6HvFRnK!dzhWUHJ}z z!tydnH=*<%a*{z1_==CU1yJ?DAD2qXBt1sRiAZ?i%tKPC2cdD+=tMuW+2%x|^z`qk zewNJr@J8OMn*iV)H9alE&i@{_AYBwEKw(1!I$cT69gaUmmMNBlmblS4^n^vCjk2xd z3C${dpBFrBLnWryW>bz2-DP6t@Itq5@d`I%KPK`UrkE(9g#VF~-}IE@lQ&ZbZ#NOF zy}sRsYcbNTs2hB8QI;`fynYDPDTwmw18Ol<91#^7qoRTF@DUfLed6x+V7?!hnu2QR=`r;)3e4S;$$ZZ-VH{#@X=W%-%e$1%2uq0=kj@CoowLU%Xp%c+y-Yw=AnE zlqcz;iRw9qCu!T>wt`2AL1PF}nY!H**rNP~9;1;HFvqggV@oMA60JlqT?|`1;cA2d ztAXO9!qc?Gn4E$f=_WqRoJz#Z0~5nNYC%kUIpKp^Joyl*IYR*U5wshTUU+g6jCLb}dRF~vHIp+Kk00GDppIQ@3$8INZ*$s(W`0IaOXCTJp z-RBpun)~sy^Wq3B277)o?rB#W0MNQtb`ija7|o|@Zi%BpFC)pUXIH73H@m^8uhF)1 z%ca#}8e6kD($~dUbR~$5q6C{HtJqhrzVW3wWa-OHZfZ9&b4FMPBNcs{BF$I`x7Q#A zoR5ucc3W;<|6#W9a_0@$;`beAJ2d7MPnfa&q21Bsx1t@->*VC+NMjy6x< z$$Vf`ETNtNM9f(ZLIeYpoC@0jwg!b4QA?BkY1-%Dm1Zjrs(e``)X6nxPsud9Om%?-!x!pl%lM{IEEHw^po_1rRd%zw{e}*(E<44A&GsrU;JyoUt zYgKoC1X@g9!v$*@a`&w@ts|!+U=OnKeY2Fo@4I3kq-MlozY^TMzMa1QdHJD~pF=_k zVn;SBo?kg$rUQOC*e2GI`2ODd#faZQzjFSy07{ zHgP5J;)P$1lm^c*SMr#G6aRbfxErKHlzk5Ks~cqTAr~i7sba9Io>Yu%#!oFmA1vkF z4LAWEhQCkQcgo()d23+rM2&53%G+OtRg4%Pp==jwhlg*VcXT>i;8y*=9SC*zbMsF& zF!6*Y0bHblD=98gFRMkz43p%Sk|^UY#>|qO|M$WqEgf1Oo%nO(kI}YLS?G=IJYa=Cxs!|0q(z*ZVhF{>EHzf8? zyp3WyvY5+kzqcyaZm3D?%r49>={0%jm)18puAu(cpUq^2V!U2)jB4Y!jNwDrdk^)V zUp5OO^6!#)E0DR|bUG*53c2>3rM8Hy`t@aXK;mKC&0>X;ORG?_xnkz_DiaK#Z;Jkr zd*9<9NPYXa8r+=r+RhuYvYr(e`KXFaz+93FDHq>mBF5MSI_tMv>lAk=`TTn|{JMkh z;I%VMeB&BBS;~R}9EV}Iy!y*u$jTvA|1CIv(dL8 z=^xUgoBQhD%`lvrTY*n6-7;g)yGz=cX+L!_FCX{x>y~kfVKSwPMhVNsV}F;0z2y}C z+x)6~mz*e}kv_6H3v6InaP{74`oKp$g>qst9_M;A(;WZj;>YK1dY^7`TPHMQ&GxD> zqnDptn{BEIa8|H`m>tCYKz_xitQ^tWb|# z+0{d|>i3nV+c~E%Z~c2(N_uxIkFkNV!CNe;aZ<1m8*}uZy{!qFosbi|^wCZ8_|0ek zHjR!tzHTAfBqA->a#tuvi$1)ma>fF`29lwXWrGxNvnu1lY})<*bDYbnq~_namCA*= zUSObWmxn_XuUCq733)VlOU_Fsqf3+B;PAi8kLOh74KL16M`YjY&=*->)`ia}Qkb{g z#0&4Q*f&AU3w)+Z0WrJPe>-hFsmgF(L}*~n&Xvqe)j0&Rh4zy9%ekIU8JP2mI`smo zhxZ??JFy)V>}=2T3?-*8^!!NO+WPuobNW)!1lF%(>7cn>(kHCL(JH!#ioM^cEgU;v zp(@4B{%>)_ublGo^?u``=DZdCy6g-otxgCWfsR4<7YFgemZ71}O43Vfj>+^C{|-zC z<*5*5znPW9%I@+qxg;gncHJa7F%?wRm@1|S+Xqj#a%bOs!vC|Wmq`~Ia~2o~o5b}R}BU{5n%XwotbSW#c;-@^8l^%{T^}Ca< zjU+>yYZ@s8R@pI0%y#i_J0`)mlBCs+b+}D#M0cJVLIv+K8t!AX$J$oG87w^^_thQc zW;gk}lkm2TB^NF{4-Mvju75JasDlaYS@*Q|`nZ{B6>4~?|dvu61LZ9To5lIolZ!Ss~mqF z{6b}!A_fbIlwK)I%_mpAln6d@d(CO?y!Zd@(|Cpw-U|V?sfz=jxKv&3PSz__nj6g#5ucZ5gPC$y*nHWvMa00@sh{hus6>wLpG}bvqT1MP+`5z}&D}Ic*|F^PwR3oJu zegdto5zbrIn_3yJqu`0AfA967AsJHezZ#JnXgJV+yKnlRzcrD;?}z(RP3h70Zfz#X zJPtt!_v_dgucNfbCI>Egx}_`*<0fDIR%mYevtD>TDg#!?QR(EzDm(3N&%7u*iFupO z(~&}k_W1e{A0byCDH_*efIj_Mtn;MTWzz|FbqY}vPz1o49*`vC+eEa7g(^15z6D2|Oqxr5V zp~#1(J#8o#lcA)ZHU5)iD5cUT#oNx`&!ErO=)WU6&W-={WkQ0hwa-xVWaQy|N>qfy z*0%iu!@4yc(Z7G9EMv9Rv%|;+)v3_?F50`I?me^aoZ=?ax|(OW*j4-KV(xWDX(f|Jus7Ln;yFqU*tbNcSSM~} z{J3Ojq}lQJ&e|mhUym>Oooyv{$;cL^*Lea_>`C?cgLv9NKWZUq7N&^@E5AsO|F_K} z8DjP)j#1w)ROgH*C8+}w=%zuWMwEPL}J!AfH+ zZhpMs>5Qeez{G8^O&1153VpGBC{mLq5`THc?htEieZj8yMj8zX8L15H+W&FF&M=4JQH=7NFg%b9EnfH5Djj54NVgR?1 zT6jm8`y?&FzB6=Cf+dV_Fw9T?TQK?Gb>*+}nLZ}Iq((D7lve9eNEk{f<9>7}E^GD*cjzH2p zDU%KUAboiLP$qq`@ZVd!OQje_P4J)_b{lq({QiN`MKlmn-q|0qM`XgQ>Q~Sfx zYla2;RFAp*4RM@tgIlAlgp)%K@?`beocLTis00Zpy*P4|s+zqjH$pjdpKSEO^sj$S zjrl&@e=uoJiE1C4u$;Sr)a{uRK zC^4*PdT+fEUTAPyC$v|4o6GoDal?y7=ZU17hnAQqFYIhx?Xc{ygLG+wHlVP!mp^F_ zc9oux_57*d{kni2)r2DebCs;ZR8j8pYpVuQ?J=Mtp%b>_Bnm!PURkC-(SU-1wC7p;h?N zcCH^MPXc0q9oB@#o%(2tEuJxVw=ictTldUDr0kzn)VW)A%YCbJd}@F;Od_NXhI%)* zfmu?8?;@LLR7&E-^6i`J6!vu%4-KvBAM6Zv9Ql*RwriQlw{N@~A5jUedkRn) zEgJ!ir#Km~85)J(?cIz#M#5s>B?H<%$;&89=Uz0qWogF8hry8Y2B|8`?xAF%gumJ&FDI=Azq6x%$alfNojg64 z;5Q;CHQ9@p+@^yQ@E_9VI`Y?*lrY@zXL1ZtfF*_meFXktfqyU<;v^yrG5k)1{$CRI zzrSTqBL4Sh^ug?Vri);rB9MvFp_tY-Jf5q|V6?KKBDMERAg4CN{`gsT*+%lw_oNZW z--oE#9zW{G?EBE5#9lHeuvUfa=x};%&9!M^x9i2nW}*Z-nG5Z~oZyZW0l2 za&n#iJ)k$~@mtS&)~!-{pq?;ABy?j%;Y4#lK!8SEBNJg|WrbDyMsn2R%*;#-A+AS2 zNT^ug^5Gkg+}zyk_Rpzs&3BA)Fu(q__46lpFXgN;n~A;H?3&Q^O=8!^-PubYKYaMW z-z=Q=k}aS0meAKuwqjlJt)#A&Sn@M5LZzEDTywRN@18&BU^#e@LEqfmoPmjHS2#08 zK_dm1(MQe%H}fO*etv{=w+SE!ERMp+n^J!qf zxRd?)Xpy1KBfL%78@E~y^~Ccnf!V`nCY& z* z@kA_aY-!J)9S**K|53?}8#gZZj{W#?oQvz;R}N=i-_R9>jr~&B_6I6lz8s{&_;Q(3 zGUT_*zzHI9_Rf%y5Pm*B$%2A{J2w-bJb4xs#gv(mk<{C3z;ftNy1zf>aBOU>LS0>5 zdvC8|baXV<+tV}i<*6QKia~y985z3LDB&ONU%p6w`t*rkL`1%=t*yPQOSYqo{wGN}_nl5oN@BONut=Jlv!~p%Cp9gNi;|i;O5iK*;?JM;KPD|ibtG;5 z73Jgtr_hUI?($mM7djd|2-!(0?x|^{pPD&dke; z9;Q0bO?ByfpWA05btckXyXtLTgAk(k)KanW0UQ-pc-@_2&P`HITT826GsJ}q%e7k; z>nAD6oD(QknU!@vDk>_Ys3>yERg96D`4Ox|;Lxl0aVB0~aSICz$2mBpeiT)_4v3O+ zvOE~~bX3P!;4OxTH<)LGA8hjct&5#=xn6+ z$M{#b`smvo=v6EPR}v;=_tr<#%vJkM2Ht*of+Q5HqOc;n((f=WU`9Pk+AuwLLJxLU zxmQS&&=W#0aUv$`jz-d&@~uzENd0FnZLiybO}k)ff78;*`OKMiA}lcp=`J!#9Zpws zM<+`=TV2kpj%F5?@Lw97&UW@XoOY&G=4Q@L^cbRhNyfA^%IcTRJ}=6B4;*Al zO7h>;dWik-5xt!&p&!)Q`J=g$fwH=^rI|B4$W=#=R literal 0 HcmV?d00001 diff --git a/src/packages/electron/resources/icons/dev-icon.png b/src/packages/electron/resources/icons/dev-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..bab0ca21ca11b65f91c81829863ba035857cb782 GIT binary patch literal 66751 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 ?)ZU+`JF{HUMVq3#D6a;` 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 z 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 literal 0 HcmV?d00001 diff --git a/src/packages/electron/resources/icons/icon.icns b/src/packages/electron/resources/icons/icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..b6def08054d43652c4d469fcfa6de7ae9fb6b02e GIT binary patch literal 160844 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 literal 0 HcmV?d00001 diff --git a/src/packages/electron/resources/icons/icon.png b/src/packages/electron/resources/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b9c1331d951b7825160d3683605a45308cfb9e6b GIT binary patch literal 55246 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

  • Wad7s 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�Eb8EGH+ZFiC4wILwDbzGc_0 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 literal 0 HcmV?d00001 diff --git a/src/packages/electron/scripts/build-web-assets.mjs b/src/packages/electron/scripts/build-web-assets.mjs new file mode 100644 index 0000000..9cca352 --- /dev/null +++ b/src/packages/electron/scripts/build-web-assets.mjs @@ -0,0 +1,75 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const repoRoot = path.resolve(__dirname, '..', '..', '..'); +const webDir = path.join(repoRoot, 'packages', 'web'); +const electronDir = path.join(repoRoot, 'packages', 'electron'); + +const resourcesDir = path.join(electronDir, 'resources'); +const resourcesWebDistDir = path.join(resourcesDir, 'web-dist'); +const webDistDir = path.join(webDir, 'dist'); + +const run = (cmd, args, cwd) => { + const result = spawnSync(cmd, args, { cwd, stdio: 'inherit' }); + if (result.error) throw result.error; + if (result.status !== 0) { + throw new Error(`Command failed: ${cmd} ${args.join(' ')}`); + } +}; + +const resolveBun = () => { + if (typeof process.env.BUN === 'string' && process.env.BUN.trim()) { + return process.env.BUN.trim(); + } + const result = spawnSync('/bin/bash', ['-lc', 'command -v bun'], { encoding: 'utf8' }); + const resolved = (result.stdout || '').trim(); + return resolved || 'bun'; +}; + +const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +const removeDir = async (target) => { + for (let attempt = 0; attempt < 5; attempt += 1) { + try { + await fs.rm(target, { recursive: true, force: true }); + return; + } catch (error) { + if (attempt === 4) throw error; + if (!['ENOTEMPTY', 'EBUSY', 'EPERM'].includes(error?.code)) throw error; + await sleep(100 * (attempt + 1)); + } + } +}; + +const copyDir = async (src, dst) => { + await fs.mkdir(dst, { recursive: true }); + const entries = await fs.readdir(src, { withFileTypes: true }); + for (const entry of entries) { + const from = path.join(src, entry.name); + const to = path.join(dst, entry.name); + if (entry.isDirectory()) { + await copyDir(from, to); + } else { + await fs.copyFile(from, to); + } + } +}; + +const bunExe = resolveBun(); + +console.log('[electron] building web UI dist...'); +run(bunExe, ['run', 'build'], webDir); + +console.log('[electron] staging packaged resources...'); +await fs.mkdir(resourcesDir, { recursive: true }); +const stagedWebDistDir = await fs.mkdtemp(path.join(resourcesDir, 'web-dist-staging-')); +await copyDir(webDistDir, stagedWebDistDir); +await removeDir(resourcesWebDistDir); +await fs.rename(stagedWebDistDir, resourcesWebDistDir); + +console.log(`[electron] web assets ready: ${resourcesWebDistDir}`); diff --git a/src/packages/electron/scripts/bundle-main.mjs b/src/packages/electron/scripts/bundle-main.mjs new file mode 100644 index 0000000..e8d06d3 --- /dev/null +++ b/src/packages/electron/scripts/bundle-main.mjs @@ -0,0 +1,44 @@ +/** + * Bundle main.mjs into a single file. Small electron-* helper deps are + * inlined; everything else — including the in-process web server + * (@openchamber/web) and native modules — stays external so it resolves + * from node_modules at runtime inside the packaged app. + * + * Why external matters: packages/web/server pulls in bun-pty, which has + * a top-level `import { dlopen } from "bun:ffi"`. If we inline it here, + * Node's ESM loader sees `bun:ffi` at package load time and crashes with + * ERR_UNSUPPORTED_ESM_URL_SCHEME before any runtime guard can skip it. + * Leaving @openchamber/web external means the conditional + * `if (isBunRuntime) await import('bun-pty')` stays dynamic and is never + * reached under Electron. + */ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const root = path.resolve(__dirname, '..'); + +const result = await Bun.build({ + entrypoints: [path.join(root, 'main.mjs')], + outdir: path.join(root, 'dist-bundle'), + target: 'node', + format: 'esm', + external: [ + 'electron', + '@openchamber/web', + '@openchamber/web/*', + 'bun-pty', + 'node-pty', + 'better-sqlite3', + ], + minify: false, + sourcemap: 'none', + naming: '[name].mjs', +}); + +if (!result.success) { + for (const msg of result.logs) console.error(msg); + process.exit(1); +} + +console.log('[electron] main.mjs bundled -> dist-bundle/main.mjs'); diff --git a/src/packages/electron/scripts/electron-dev.mjs b/src/packages/electron/scripts/electron-dev.mjs new file mode 100644 index 0000000..46c2ce4 --- /dev/null +++ b/src/packages/electron/scripts/electron-dev.mjs @@ -0,0 +1,131 @@ +#!/usr/bin/env node +import { spawn } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '../../..'); +const electronDir = path.join(repoRoot, 'packages/electron'); + +function spawnProcess(command, args, options = {}) { + return spawn(command, args, { + cwd: repoRoot, + env: { ...process.env, OPENCHAMBER_ELECTRON_DEV: '1' }, + stdio: 'inherit', + detached: process.platform !== 'win32', + ...options, + }); +} + +function waitForExit(child, timeoutMs) { + return new Promise((resolve) => { + if (!child || child.exitCode !== null || child.signalCode !== null) { + resolve(); + return; + } + + const onExit = () => { + clearTimeout(timer); + resolve(); + }; + + const timer = setTimeout(() => { + child.off('exit', onExit); + resolve(); + }, timeoutMs); + + child.once('exit', onExit); + }); +} + +function signalChild(child, signal) { + if (!child || child.exitCode !== null || child.signalCode !== null) { + return; + } + + try { + if (process.platform !== 'win32') { + process.kill(-child.pid, signal); + return; + } + } catch { + } + + try { + child.kill(signal); + } catch { + } +} + +async function stopChildTree(child) { + if (!child || child.exitCode !== null || child.signalCode !== null) { + return; + } + + signalChild(child, 'SIGINT'); + await waitForExit(child, 2500); + + if (child.exitCode === null && child.signalCode === null) { + signalChild(child, 'SIGTERM'); + await waitForExit(child, 2500); + } + + if (child.exitCode === null && child.signalCode === null) { + signalChild(child, 'SIGKILL'); + await waitForExit(child, 1000); + } +} + +async function main() { + const devServer = spawnProcess('node', ['./scripts/dev-web-hmr.mjs'], { + env: { + ...process.env, + OPENCHAMBER_ELECTRON_DEV: '1', + OPENCHAMBER_HMR_UI_PORT: '5173', + OPENCHAMBER_HMR_API_PORT: '3901', + OPENCHAMBER_DISABLE_PWA_DEV: '1', + }, + }); + const electron = spawnProcess('npx', ['electron', './main.mjs'], { cwd: electronDir }); + + let cleaning = false; + const teardown = async (code) => { + if (cleaning) { + return; + } + cleaning = true; + + await Promise.all([stopChildTree(electron), stopChildTree(devServer)]); + process.exit(typeof code === 'number' ? code : 0); + }; + + const onChildExit = (label) => (code, signal) => { + if (code !== 0 || signal) { + console.warn(`[electron:dev] ${label} exited with code ${code ?? 'null'} signal ${signal ?? 'none'}.`); + } + void teardown(code ?? 1); + }; + + devServer.on('exit', onChildExit('dev server')); + electron.on('exit', onChildExit('electron')); + devServer.on('error', (error) => { + console.error('[electron:dev] failed to start dev server:', error); + void teardown(1); + }); + electron.on('error', (error) => { + console.error('[electron:dev] failed to start electron:', error); + void teardown(1); + }); + + for (const [signal, exitCode] of Object.entries({ SIGINT: 130, SIGTERM: 143, SIGQUIT: 131, SIGHUP: 129 })) { + process.on(signal, () => { + void teardown(exitCode); + }); + } +} + +main().catch((error) => { + console.error('[electron:dev] unexpected error:', error); + process.exit(1); +}); diff --git a/src/packages/electron/scripts/finalize-latest-yml.mjs b/src/packages/electron/scripts/finalize-latest-yml.mjs new file mode 100644 index 0000000..68b2de1 --- /dev/null +++ b/src/packages/electron/scripts/finalize-latest-yml.mjs @@ -0,0 +1,113 @@ +#!/usr/bin/env node +import fs from 'node:fs/promises'; +import path from 'node:path'; + +const dir = process.env.LATEST_YML_DIR; +const repo = process.env.GH_REPO; +const version = process.env.OPENCHAMBER_VERSION; + +if (!dir) throw new Error('LATEST_YML_DIR is required'); +if (!repo) throw new Error('GH_REPO is required'); +if (!version) throw new Error('OPENCHAMBER_VERSION is required'); + +const parse = (content) => { + const lines = content.split('\n'); + let releaseDate = ''; + let parsedVersion = ''; + const files = []; + let current; + + const flush = () => { + if (current?.url && current?.sha512 && current?.size) { + files.push(current); + } + current = undefined; + }; + + for (const line of lines) { + const trimmed = line.trim(); + const indented = line.startsWith(' ') || line.startsWith(' -'); + if (line.startsWith('version:')) { + parsedVersion = line.slice('version:'.length).trim(); + } else if (line.startsWith('releaseDate:')) { + releaseDate = line.slice('releaseDate:'.length).trim().replace(/^'|'$/g, ''); + } else if (trimmed.startsWith('- url:')) { + flush(); + current = { url: trimmed.slice('- url:'.length).trim() }; + } else if (indented && current && trimmed.startsWith('sha512:')) { + current.sha512 = trimmed.slice('sha512:'.length).trim(); + } else if (indented && current && trimmed.startsWith('size:')) { + current.size = Number(trimmed.slice('size:'.length).trim()); + } else if (indented && current && trimmed.startsWith('blockMapSize:')) { + current.blockMapSize = Number(trimmed.slice('blockMapSize:'.length).trim()); + } else if (!indented && current) { + flush(); + } + } + + flush(); + return { version: parsedVersion, releaseDate, files }; +}; + +const serialize = (data) => { + const lines = [`version: ${data.version}`, 'files:']; + for (const file of data.files) { + lines.push(` - url: ${file.url}`); + lines.push(` sha512: ${file.sha512}`); + lines.push(` size: ${file.size}`); + if (file.blockMapSize) { + lines.push(` blockMapSize: ${file.blockMapSize}`); + } + } + lines.push(`releaseDate: '${data.releaseDate}'`); + return `${lines.join('\n')}\n`; +}; + +const read = async (subdir, filename) => { + const filePath = path.join(dir, subdir, filename); + try { + return parse(await fs.readFile(filePath, 'utf8')); + } catch { + return null; + } +}; + +const output = {}; + +const winX64 = await read('latest-yml-x86_64-pc-windows-msvc', 'latest.yml'); +const winArm64 = await read('latest-yml-aarch64-pc-windows-msvc', 'latest.yml'); +if (winX64 || winArm64) { + const base = winArm64 || winX64; + output['latest.yml'] = serialize({ + version: base.version, + files: [...(winArm64?.files || []), ...(winX64?.files || [])], + releaseDate: base.releaseDate, + }); +} + +const linuxX64 = await read('latest-yml-x86_64-unknown-linux-gnu', 'latest-linux.yml'); +if (linuxX64) output['latest-linux.yml'] = serialize(linuxX64); + +const linuxArm64 = await read('latest-yml-aarch64-unknown-linux-gnu', 'latest-linux-arm64.yml'); +if (linuxArm64) output['latest-linux-arm64.yml'] = serialize(linuxArm64); + +const macX64 = await read('latest-yml-x86_64-apple-darwin', 'latest-mac.yml'); +const macArm64 = await read('latest-yml-aarch64-apple-darwin', 'latest-mac.yml'); +if (macX64 || macArm64) { + const base = macArm64 || macX64; + output['latest-mac.yml'] = serialize({ + version: base.version, + files: [...(macArm64?.files || []), ...(macX64?.files || [])], + releaseDate: base.releaseDate, + }); +} + +const tag = `v${version}`; +const tmp = process.env.RUNNER_TEMP || '/tmp'; +for (const [filename, content] of Object.entries(output)) { + const outputPath = path.join(tmp, filename); + await fs.writeFile(outputPath, content); + console.log(`prepared ${outputPath} for upload to ${repo} release ${tag}`); +} + +console.log('finalized latest yml files'); diff --git a/src/packages/electron/scripts/rebuild-native.mjs b/src/packages/electron/scripts/rebuild-native.mjs new file mode 100644 index 0000000..eb381df --- /dev/null +++ b/src/packages/electron/scripts/rebuild-native.mjs @@ -0,0 +1,30 @@ +#!/usr/bin/env node +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createRequire } from 'node:module'; +import { rebuild } from '@electron/rebuild'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const electronDir = path.resolve(__dirname, '..'); +const repoRoot = path.resolve(electronDir, '..', '..'); +const require = createRequire(import.meta.url); + +const electronPkg = require('electron/package.json'); +const electronVersion = electronPkg.version; + +console.log(`[electron] rebuilding native modules against Electron ${electronVersion}...`); + +// Rebuild against the hoisted root node_modules (bun workspace layout). +// force=true re-links regardless of cached state; prebuild-install lookup is +// bypassed by @electron/rebuild in favor of direct node-gyp builds. +await rebuild({ + buildPath: repoRoot, + electronVersion, + force: true, + arch: process.env.ELECTRON_BUILDER_ARCH || process.arch, + onlyModules: ['better-sqlite3', 'node-pty', 'bun-pty'], +}); + +console.log('[electron] native modules rebuilt successfully'); diff --git a/src/packages/electron/ssh-manager.mjs b/src/packages/electron/ssh-manager.mjs new file mode 100644 index 0000000..e21ab19 --- /dev/null +++ b/src/packages/electron/ssh-manager.mjs @@ -0,0 +1,1245 @@ +import fs from 'node:fs'; +import fsp from 'node:fs/promises'; +import net from 'node:net'; +import os from 'node:os'; +import path from 'node:path'; +import { spawn } from 'node:child_process'; + +const LOCAL_HOST_ID = 'local'; +const DEFAULT_CONNECTION_TIMEOUT_SEC = 60; +const DEFAULT_LOCAL_BIND_HOST = '127.0.0.1'; +const DEFAULT_CONTROL_PERSIST_SEC = 300; +const DEFAULT_READY_TIMEOUT_SEC = 30; +const DEFAULT_RECONNECT_MAX_ATTEMPTS = 5; +const MAX_LOG_LINES_PER_INSTANCE = 1200; + +const MONITOR_INITIAL_POLL_MS = 2000; +const MONITOR_STEADY_POLL_MS = 10000; +const MONITOR_STABILIZE_TICKS = 5; +const SSH_STATUS_EVENT = 'openchamber:ssh-instance-status'; + +const nowMillis = () => Date.now(); + +const shellQuote = (value) => `'${String(value).replace(/'/g, `'\\''`)}'`; + +const hasGlobWildcard = (value) => /[*?]/.test(value); + +const expandSshIncludeToken = (token, baseDir) => { + const trimmed = String(token || '').trim(); + if (!trimmed) return []; + + const expandedHome = trimmed.startsWith('~/') + ? path.join(os.homedir(), trimmed.slice(2)) + : (trimmed === '~' ? os.homedir() : trimmed); + const resolved = path.isAbsolute(expandedHome) + ? expandedHome + : path.resolve(baseDir, expandedHome); + + if (!hasGlobWildcard(resolved)) { + return fs.existsSync(resolved) ? [resolved] : []; + } + + const dir = path.dirname(resolved); + const namePattern = path.basename(resolved); + if (hasGlobWildcard(dir) || !fs.existsSync(dir)) { + return []; + } + + const matcher = new RegExp(`^${namePattern + .replace(/[.+^${}()|[\]\\]/g, '\\$&') + .replace(/\*/g, '.*') + .replace(/\?/g, '.')}$`); + + try { + return fs.readdirSync(dir) + .filter((name) => matcher.test(name)) + .map((name) => path.join(dir, name)) + .filter((candidate) => fs.existsSync(candidate)) + .sort((left, right) => left.localeCompare(right)); + } catch { + return []; + } +}; + +const readJsonRoot = (settingsFilePath) => { + try { + const parsed = JSON.parse(fs.readFileSync(settingsFilePath, 'utf8')); + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}; + } catch { + return {}; + } +}; + +const writeJsonRoot = async (settingsFilePath, root) => { + await fsp.mkdir(path.dirname(settingsFilePath), { recursive: true }); + // Atomic write: concurrent readers (main.mjs, web server) would otherwise + // see partial JSON and readJsonRoot()'s catch would silently coerce to {}, + // causing the next read-modify-write to wipe the entire settings file. + const tmp = `${settingsFilePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + await fsp.writeFile(tmp, JSON.stringify(root, null, 2)); + await fsp.rename(tmp, settingsFilePath); +}; + +const defaultTrue = () => true; + +const sanitizeBindHost = (raw) => { + const trimmed = typeof raw === 'string' ? raw.trim() : ''; + if (!trimmed) return DEFAULT_LOCAL_BIND_HOST; + return ['127.0.0.1', 'localhost', '0.0.0.0'].includes(trimmed) ? trimmed : DEFAULT_LOCAL_BIND_HOST; +}; + +const splitShellWords = (input) => { + const tokens = []; + let current = ''; + let inSingle = false; + let inDouble = false; + const chars = [...String(input)]; + + for (let index = 0; index < chars.length; index += 1) { + const ch = chars[index]; + if (ch === '\\' && !inSingle) { + index += 1; + if (index < chars.length) current += chars[index]; + continue; + } + if (ch === '\'' && !inDouble) { + inSingle = !inSingle; + continue; + } + if (ch === '"' && !inSingle) { + inDouble = !inDouble; + continue; + } + if (/\s/.test(ch) && !inSingle && !inDouble) { + if (current) { + tokens.push(current); + current = ''; + } + continue; + } + current += ch; + } + + if (inSingle || inDouble) { + throw new Error('Unclosed quote in SSH command'); + } + if (current) tokens.push(current); + return tokens; +}; + +const isDisallowedPrimaryFlag = (token) => { + return ['-M', '-S', '-O', '-N', '-t', '-T', '-f', '-G', '-W', '-v', '-V', '-q', '-n', '-s', '-e', '-E', '-g'].includes(token); +}; + +const hasDisallowedOOption = (value) => { + const lower = String(value).trim().toLowerCase(); + return ['controlmaster', 'controlpath', 'controlpersist', 'batchmode', 'proxycommand'].some((prefix) => lower.startsWith(prefix)); +}; + +const parseSshCommand = (raw) => { + const tokens = splitShellWords(raw); + if (tokens.length === 0) { + throw new Error('SSH command is empty'); + } + + if (tokens[0] === 'ssh') { + tokens.shift(); + } + + if (tokens.length === 0) { + throw new Error('SSH command must include destination'); + } + + const allowedFlags = new Set(['-4', '-6', '-A', '-a', '-C', '-K', '-k', '-X', '-x', '-Y', '-y']); + const allowedWithValues = ['-B', '-b', '-c', '-D', '-F', '-I', '-i', '-J', '-l', '-m', '-o', '-P', '-p', '-R']; + + const args = []; + let destination = null; + for (let index = 0; index < tokens.length;) { + const token = tokens[index]; + if (destination) { + throw new Error(`SSH command has unsupported trailing argument: ${token}`); + } + + if (!token.startsWith('-')) { + destination = token.trim(); + index += 1; + continue; + } + + if (isDisallowedPrimaryFlag(token)) { + throw new Error(`SSH option ${token} is not allowed`); + } + + if (allowedFlags.has(token)) { + args.push(token); + index += 1; + continue; + } + + let matched = false; + for (const option of allowedWithValues) { + if (token === option) { + const value = tokens[index + 1]; + if (!value) { + throw new Error(`SSH option ${option} requires a value`); + } + if (option === '-o' && hasDisallowedOOption(value)) { + throw new Error(`SSH option -o ${value} is not allowed`); + } + args.push(token, value); + index += 2; + matched = true; + break; + } + + if (token.startsWith(option) && token.length > option.length) { + const value = token.slice(option.length); + if (option === '-o' && hasDisallowedOOption(value)) { + throw new Error(`SSH option -o ${value} is not allowed`); + } + args.push(token); + index += 1; + matched = true; + break; + } + } + + if (!matched) { + throw new Error(`Unsupported SSH option: ${token}`); + } + } + + if (!destination) { + throw new Error('SSH command must include destination'); + } + + return { destination, args }; +}; + +const runOutput = async (command, args, options = {}) => { + return await new Promise((resolve, reject) => { + const child = spawn(command, args, { + stdio: ['pipe', 'pipe', 'pipe'], + ...options, + }); + + let stdout = ''; + let stderr = ''; + child.stdout?.on('data', (chunk) => { + stdout += chunk.toString(); + }); + child.stderr?.on('data', (chunk) => { + stderr += chunk.toString(); + }); + child.on('error', reject); + child.on('close', (code) => { + resolve({ code: typeof code === 'number' ? code : -1, stdout, stderr }); + }); + }); +}; + +const buildSshArgs = (parsed, preDestinationArgs = [], remoteCommand = null) => { + const args = [...parsed.args, ...preDestinationArgs, parsed.destination]; + if (remoteCommand) args.push(remoteCommand); + return args; +}; + +const runRemoteCommand = async (parsed, controlPath, script, timeoutSec = DEFAULT_CONNECTION_TIMEOUT_SEC) => { + const args = buildSshArgs(parsed, [ + '-o', 'ControlMaster=no', + '-o', `ControlPath=${controlPath}`, + '-o', `ConnectTimeout=${timeoutSec}`, + '-T', + ], `sh -lc ${shellQuote(script)}`); + const { code, stdout, stderr } = await runOutput('ssh', args); + if (code !== 0) { + throw new Error((stderr || stdout || 'Remote command failed').trim()); + } + return stdout; +}; + +const controlMasterOperation = async (parsed, controlPath, op) => { + return await runOutput('ssh', buildSshArgs(parsed, [ + '-o', 'ControlMaster=no', + '-o', `ControlPath=${controlPath}`, + '-o', 'BatchMode=yes', + '-o', 'ConnectTimeout=3', + '-O', op, + ])); +}; + +const isControlMasterAlive = async (parsed, controlPath) => { + const { code } = await controlMasterOperation(parsed, controlPath, 'check'); + return code === 0; +}; + +const stopControlMasterBestEffort = async (parsed, controlPath) => { + try { + await controlMasterOperation(parsed, controlPath, 'exit'); + } catch { + } +}; + +const askpassScriptContent = () => `#!/bin/bash +PROMPT="$1" + +if [[ -n "$OPENCHAMBER_SSH_ASKPASS_VALUE" ]]; then + if [[ "$PROMPT" == *"assword"* || "$PROMPT" == *"passphrase"* ]]; then + printf '%s\\n' "$OPENCHAMBER_SSH_ASKPASS_VALUE" + exit 0 + fi +fi + +DEFAULT_ANSWER="" +HIDDEN_INPUT="true" + +if [[ "$PROMPT" == *"yes/no"* ]]; then + DEFAULT_ANSWER="yes" + HIDDEN_INPUT="false" +fi + +if command -v osascript >/dev/null 2>&1; then + /usr/bin/osascript <<'APPLESCRIPT' "$PROMPT" "$DEFAULT_ANSWER" "$HIDDEN_INPUT" +on run argv + set promptText to item 1 of argv + set defaultAnswer to item 2 of argv + set hiddenInput to item 3 of argv + + try + if hiddenInput is "true" then + set response to display dialog promptText default answer defaultAnswer with hidden answer buttons {"Cancel", "OK"} default button "OK" + else + set response to display dialog promptText default answer defaultAnswer buttons {"Cancel", "OK"} default button "OK" + end if + return text returned of response + on error + error number -128 + end try +end run +APPLESCRIPT + exit $? +fi + +printf '%s\\n' "$DEFAULT_ANSWER" +`; + +const writeAskpassScript = async (scriptPath) => { + await fsp.writeFile(scriptPath, askpassScriptContent(), { mode: 0o700 }); + await fsp.chmod(scriptPath, 0o700); +}; + +const randomPortCandidate = (seed) => { + let hash = 0; + const source = `${seed}:${Date.now()}`; + for (let index = 0; index < source.length; index += 1) { + hash = ((hash << 5) - hash + source.charCodeAt(index)) | 0; + } + const base = 20000; + const span = 30000; + return base + Math.abs(hash % span); +}; + +const pickUnusedLocalPort = async () => { + return await new Promise((resolve, reject) => { + const server = net.createServer(); + server.listen(0, '127.0.0.1', () => { + const address = server.address(); + const port = typeof address === 'object' && address ? address.port : 0; + server.close((error) => error ? reject(error) : resolve(port)); + }); + server.on('error', reject); + }); +}; + +const isLocalPortAvailable = async (bindHost, port) => { + return await new Promise((resolve) => { + const server = net.createServer(); + server.once('error', () => resolve(false)); + server.listen(port, bindHost, () => { + server.close(() => resolve(true)); + }); + }); +}; + +const isLocalTunnelReachable = async (localPort) => { + return await new Promise((resolve) => { + const socket = net.createConnection({ host: '127.0.0.1', port: localPort }); + const finish = (value) => { + socket.destroy(); + resolve(value); + }; + socket.once('connect', () => finish(true)); + socket.once('error', () => finish(false)); + socket.setTimeout(500, () => finish(false)); + }); +}; + +const waitLocalForwardReady = async (localPort) => { + const deadline = Date.now() + (DEFAULT_READY_TIMEOUT_SEC * 1000); + let pollMs = 250; + while (Date.now() < deadline) { + try { + const response = await fetch(`http://127.0.0.1:${localPort}/health`, { signal: AbortSignal.timeout(1000) }); + if (response.ok || response.status === 401) { + return; + } + } catch { + } + await new Promise((resolve) => setTimeout(resolve, pollMs)); + pollMs = Math.min(pollMs * 2, 2000); + } + throw new Error('Timed out waiting for forwarded OpenChamber health'); +}; + +const parseVersionToken = (raw) => { + for (const token of String(raw).split(/\s+/)) { + let candidate = token.trim().replace(/^v/, ''); + candidate = candidate.replace(/[,)]+$/g, ''); + const parts = candidate.split('.'); + if (parts.length >= 2 && parts.every((part) => /^\d+$/.test(part))) { + return candidate; + } + } + return null; +}; + +const parseProbeStatusLine = (line, prefix) => { + if (!line || !line.startsWith(prefix)) return null; + const value = Number.parseInt(line.slice(prefix.length).trim(), 10); + return Number.isFinite(value) ? value : null; +}; + +const isAuthHttpStatus = (status) => status === 401 || status === 403; +const isLivenessHttpStatus = (status) => (status >= 200 && status <= 299) || isAuthHttpStatus(status); + +export class ElectronSshManager { + constructor(options) { + this.settingsFilePath = options.settingsFilePath; + this.appVersion = options.appVersion; + this.emit = options.emit; + this.logs = new Map(); + this.statuses = new Map(); + this.sessions = new Map(); + this.monitorTimers = new Map(); + this.reconnectAttempts = new Map(); + this.connectAttempts = new Map(); + this.connecting = new Map(); + } + + appendLogWithLevel(id, level, message) { + const line = `[${nowMillis()}] [${level}] ${message}`; + const current = this.logs.get(id) || []; + current.push(line); + if (current.length > MAX_LOG_LINES_PER_INSTANCE) { + current.splice(0, current.length - MAX_LOG_LINES_PER_INSTANCE); + } + this.logs.set(id, current); + } + + appendLog(id, message) { + this.appendLogWithLevel(id, 'INFO', message); + } + + appendAttemptSeparator(id, connectAttempt, retryAttempt) { + const scope = retryAttempt > 0 ? `retry ${retryAttempt}` : 'manual'; + this.appendLogWithLevel(id, 'INFO', `---------------- attempt #${connectAttempt} (${scope}) ----------------`); + } + + statusSnapshotForInstance(id) { + return this.statuses.get(id) || { + id, + phase: 'idle', + detail: null, + localUrl: null, + localPort: null, + remotePort: null, + startedByUs: false, + retryAttempt: 0, + requiresUserAction: false, + updatedAtMs: nowMillis(), + }; + } + + setStatus(id, phase, detail = null, localUrl = null, localPort = null, remotePort = null, startedByUs = false, retryAttempt = 0, requiresUserAction = false) { + const level = phase === 'error' ? 'ERROR' : (phase === 'degraded' ? 'WARN' : 'INFO'); + this.appendLogWithLevel( + id, + level, + `phase=${JSON.stringify(phase)} detail=${detail || ''} retry=${retryAttempt} requires_user_action=${requiresUserAction}`, + ); + + const status = { + id, + phase, + detail, + localUrl, + localPort, + remotePort, + startedByUs, + retryAttempt, + requiresUserAction, + updatedAtMs: nowMillis(), + }; + this.statuses.set(id, status); + this.emit(SSH_STATUS_EVENT, status); + } + + clearRetryAttempt(id) { + this.reconnectAttempts.delete(id); + } + + nextRetryAttempt(id) { + const next = (this.reconnectAttempts.get(id) || 0) + 1; + this.reconnectAttempts.set(id, next); + return next; + } + + currentRetryAttempt(id) { + return this.reconnectAttempts.get(id) || 0; + } + + nextConnectAttempt(id) { + const next = (this.connectAttempts.get(id) || 0) + 1; + this.connectAttempts.set(id, next); + return next; + } + + logsForInstance(id, limit = 200) { + const lines = [...(this.logs.get(id) || [])]; + return limit > 0 && lines.length > limit ? lines.slice(-limit) : lines; + } + + clearLogsForInstance(id) { + this.logs.delete(id); + } + + parseSshConfigCandidates(filePath, source, visited = new Set()) { + const resolvedPath = path.resolve(filePath); + if (visited.has(resolvedPath) || !fs.existsSync(resolvedPath)) return []; + visited.add(resolvedPath); + + const content = fs.readFileSync(resolvedPath, 'utf8'); + const candidates = []; + const baseDir = path.dirname(resolvedPath); + for (const line of content.split(/\r?\n/)) { + const trimmed = (line.split('#')[0] || '').trim(); + if (!trimmed) continue; + + if (/^include(?:\s|$)/i.test(trimmed)) { + const includeExpr = trimmed.replace(/^include\s+/i, '').trim(); + if (!includeExpr) continue; + let includeTokens = []; + try { + includeTokens = splitShellWords(includeExpr); + } catch { + includeTokens = includeExpr.split(/\s+/).filter(Boolean); + } + for (const includeToken of includeTokens) { + const includePaths = expandSshIncludeToken(includeToken, baseDir); + for (const includePath of includePaths) { + candidates.push(...this.parseSshConfigCandidates(includePath, source, visited)); + } + } + continue; + } + + if (!/^host(?:\s|$)/i.test(trimmed)) continue; + const rest = trimmed.replace(/^host\s+/i, '').trim(); + if (!rest) continue; + for (const token of rest.split(/\s+/)) { + const host = token.trim(); + if (!host || host.startsWith('!') || host === '*') continue; + candidates.push({ + host, + pattern: /[*?]/.test(host), + source, + sshCommand: `ssh ${host}`, + }); + } + } + return candidates; + } + + async importHosts() { + const candidates = [ + ...this.parseSshConfigCandidates(path.join(os.homedir(), '.ssh', 'config'), 'user'), + ...this.parseSshConfigCandidates('/etc/ssh/ssh_config', 'global'), + ]; + const seen = new Set(); + return candidates + .filter((item) => !seen.has(item.host) && seen.add(item.host)) + .sort((left, right) => left.host.localeCompare(right.host)); + } + + readInstances() { + const root = readJsonRoot(this.settingsFilePath); + return { instances: Array.isArray(root.desktopSshInstances) ? root.desktopSshInstances : [] }; + } + + async setInstances(config) { + const root = readJsonRoot(this.settingsFilePath); + const instances = Array.isArray(config?.instances) ? config.instances.map((instance) => this.sanitizeInstance(instance)) : []; + root.desktopSshInstances = instances; + + const previousIds = new Set((Array.isArray(root.desktopHosts) ? root.desktopHosts : []).map((entry) => String(entry?.id || '').trim()).filter(Boolean)); + const hosts = Array.isArray(root.desktopHosts) ? root.desktopHosts.filter(Boolean) : []; + const nextIds = new Set(instances.map((instance) => instance.id)); + + const filteredHosts = hosts.filter((entry) => { + const id = String(entry?.id || '').trim(); + return id && !(previousIds.has(id) && !nextIds.has(id)); + }); + + for (const instance of instances) { + const label = instance.nickname?.trim() || instance.sshParsed?.destination || instance.id; + const existing = filteredHosts.find((entry) => entry?.id === instance.id); + if (existing) { + existing.label = label; + if (!existing.url || !String(existing.url).trim()) { + existing.url = 'http://127.0.0.1/'; + } + } else { + filteredHosts.push({ id: instance.id, label, url: 'http://127.0.0.1/' }); + } + } + + root.desktopHosts = filteredHosts; + if (typeof root.desktopDefaultHostId === 'string' && previousIds.has(root.desktopDefaultHostId) && !nextIds.has(root.desktopDefaultHostId)) { + root.desktopDefaultHostId = LOCAL_HOST_ID; + } + + await writeJsonRoot(this.settingsFilePath, root); + } + + sanitizeStoredSecret(secret) { + if (!secret || typeof secret !== 'object') return undefined; + return { + enabled: Boolean(secret.enabled), + store: secret.store === 'settings' ? 'settings' : 'never', + ...(typeof secret.value === 'string' && secret.value.trim() ? { value: secret.value } : {}), + }; + } + + sanitizeForward(forward) { + const id = typeof forward?.id === 'string' ? forward.id.trim() : ''; + if (!id) return null; + const type = forward?.type === 'remote' || forward?.type === 'dynamic' ? forward.type : 'local'; + const normalized = { + id, + enabled: forward?.enabled !== false, + type, + ...(forward?.localHost ? { localHost: sanitizeBindHost(forward.localHost) } : {}), + ...(Number.isFinite(forward?.localPort) ? { localPort: Number(forward.localPort) } : {}), + ...(forward?.remoteHost ? { remoteHost: String(forward.remoteHost).trim() || '127.0.0.1' } : {}), + ...(Number.isFinite(forward?.remotePort) ? { remotePort: Number(forward.remotePort) } : {}), + }; + + if (type === 'local' || type === 'remote') { + if (!normalized.localPort || !normalized.remotePort) return null; + normalized.remoteHost = normalized.remoteHost || '127.0.0.1'; + normalized.localHost = normalized.localHost || '127.0.0.1'; + } + if (type === 'dynamic' && !normalized.localPort) { + return null; + } + return normalized; + } + + sanitizeInstance(instance) { + const id = typeof instance?.id === 'string' ? instance.id.trim() : ''; + const sshCommand = typeof instance?.sshCommand === 'string' ? instance.sshCommand.trim() : ''; + if (!id || id === LOCAL_HOST_ID) { + throw new Error('SSH instance id is required'); + } + if (!sshCommand) { + throw new Error('SSH command is required'); + } + + const parsed = parseSshCommand(sshCommand); + const seen = new Set(); + const portForwards = Array.isArray(instance?.portForwards) + ? instance.portForwards + .map((forward) => this.sanitizeForward(forward)) + .filter((forward) => forward && !seen.has(forward.id) && seen.add(forward.id)) + : []; + + return { + id, + ...(typeof instance?.nickname === 'string' && instance.nickname.trim() ? { nickname: instance.nickname.trim() } : {}), + sshCommand, + sshParsed: parsed, + connectionTimeoutSec: Number.isFinite(instance?.connectionTimeoutSec) && Number(instance.connectionTimeoutSec) > 0 + ? Number(instance.connectionTimeoutSec) + : DEFAULT_CONNECTION_TIMEOUT_SEC, + remoteOpenchamber: { + mode: instance?.remoteOpenchamber?.mode === 'external' ? 'external' : 'managed', + keepRunning: instance?.remoteOpenchamber?.keepRunning !== false, + ...(Number.isFinite(instance?.remoteOpenchamber?.preferredPort) ? { preferredPort: Number(instance.remoteOpenchamber.preferredPort) } : {}), + installMethod: ['npm', 'bun', 'download_release', 'upload_bundle'].includes(instance?.remoteOpenchamber?.installMethod) + ? instance.remoteOpenchamber.installMethod + : 'bun', + uploadBundleOverSsh: Boolean(instance?.remoteOpenchamber?.uploadBundleOverSsh), + }, + localForward: { + bindHost: sanitizeBindHost(instance?.localForward?.bindHost), + ...(Number.isFinite(instance?.localForward?.preferredLocalPort) ? { preferredLocalPort: Number(instance.localForward.preferredLocalPort) } : {}), + }, + auth: { + ...(this.sanitizeStoredSecret(instance?.auth?.sshPassword) ? { sshPassword: this.sanitizeStoredSecret(instance.auth.sshPassword) } : {}), + ...(this.sanitizeStoredSecret(instance?.auth?.openchamberPassword) ? { openchamberPassword: this.sanitizeStoredSecret(instance.auth.openchamberPassword) } : {}), + }, + portForwards, + }; + } + + async updateHostUrl(instanceId, label, localUrl) { + const root = readJsonRoot(this.settingsFilePath); + const hosts = Array.isArray(root.desktopHosts) ? root.desktopHosts : []; + const existing = hosts.find((entry) => entry?.id === instanceId); + if (existing) { + existing.label = label; + existing.url = localUrl; + } else { + hosts.push({ id: instanceId, label, url: localUrl }); + } + root.desktopHosts = hosts; + await writeJsonRoot(this.settingsFilePath, root); + } + + async persistLocalPort(instanceId, localPort) { + const root = readJsonRoot(this.settingsFilePath); + const instances = Array.isArray(root.desktopSshInstances) ? root.desktopSshInstances : []; + for (const instance of instances) { + if (instance?.id !== instanceId) continue; + instance.localForward = instance.localForward && typeof instance.localForward === 'object' ? instance.localForward : {}; + instance.localForward.preferredLocalPort = localPort; + } + root.desktopSshInstances = instances; + await writeJsonRoot(this.settingsFilePath, root); + } + + async resolveSshConfig(parsed) { + const { code, stdout, stderr } = await runOutput('ssh', buildSshArgs(parsed, ['-G'])); + if (code !== 0) { + throw new Error(stderr.trim() || 'Failed to resolve SSH config'); + } + const map = new Map(); + for (const line of stdout.split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed) continue; + const [key, ...rest] = trimmed.split(' '); + if (!key || rest.length === 0) continue; + map.set(key.toLowerCase(), rest.join(' ').trim()); + } + return map; + } + + ensureSessionDir(id) { + const base = path.join(path.dirname(this.settingsFilePath), 'ssh', id); + fs.mkdirSync(base, { recursive: true }); + return base; + } + + controlPathForInstance(id) { + let hash = 0; + for (const char of id) { + hash = ((hash << 5) - hash + char.charCodeAt(0)) | 0; + } + return path.join(os.tmpdir(), `ocssh-${Math.abs(hash).toString(16)}.sock`); + } + + async spawnMasterProcess(parsed, controlPath, askpassPath, sshPassword) { + const child = spawn('ssh', buildSshArgs(parsed, [ + '-o', 'ControlMaster=yes', + '-o', `ControlPath=${controlPath}`, + '-o', `ControlPersist=${DEFAULT_CONTROL_PERSIST_SEC}`, + '-N', + ]), { + stdio: ['ignore', 'pipe', 'pipe'], + env: { + ...process.env, + SSH_ASKPASS_REQUIRE: 'force', + SSH_ASKPASS: askpassPath, + DISPLAY: '1', + ...(sshPassword ? { OPENCHAMBER_SSH_ASKPASS_VALUE: sshPassword.trim() } : {}), + }, + }); + return child; + } + + async waitForMasterReady(parsed, controlPath, timeoutSec, master) { + const deadline = Date.now() + (timeoutSec * 1000); + let pollMs = 250; + while (Date.now() < deadline) { + const { code } = await runOutput('ssh', buildSshArgs(parsed, [ + '-o', 'ControlMaster=no', + '-o', `ControlPath=${controlPath}`, + '-O', 'check', + ])); + if (code === 0) return; + + const exited = master.exitCode; + if (typeof exited === 'number') { + throw new Error('SSH master process exited before ready'); + } + await new Promise((resolve) => setTimeout(resolve, pollMs)); + pollMs = Math.min(pollMs * 2, 2000); + } + throw new Error('SSH ControlMaster connection timed out'); + } + + configuredOpenChamberPassword(instance) { + const secret = instance?.auth?.openchamberPassword; + return secret?.enabled && typeof secret.value === 'string' && secret.value.trim() ? secret.value.trim() : null; + } + + async remoteCommandExists(parsed, controlPath, commandName) { + try { + const output = await runRemoteCommand(parsed, controlPath, `command -v ${commandName} >/dev/null 2>&1 && echo yes || echo no`); + return output.trim() === 'yes'; + } catch { + return false; + } + } + + async currentRemoteOpenChamberVersion(parsed, controlPath) { + try { + const output = await runRemoteCommand(parsed, controlPath, 'openchamber --version 2>/dev/null || true'); + return parseVersionToken(output); + } catch { + return null; + } + } + + async installOpenChamberManaged(parsed, controlPath, version, preferred) { + const hasBun = await this.remoteCommandExists(parsed, controlPath, 'bun'); + const hasNpm = await this.remoteCommandExists(parsed, controlPath, 'npm'); + const commands = []; + + if (preferred === 'bun') { + if (hasBun) commands.push(`bun add -g @openchamber/web@${version}`); + if (hasNpm) commands.push(`npm install -g @openchamber/web@${version}`); + } else if (preferred === 'npm') { + if (hasNpm) commands.push(`npm install -g @openchamber/web@${version}`); + if (hasBun) commands.push(`bun add -g @openchamber/web@${version}`); + } else { + if (hasBun) commands.push(`bun add -g @openchamber/web@${version}`); + if (hasNpm) commands.push(`npm install -g @openchamber/web@${version}`); + } + + if (commands.length === 0) { + throw new Error('Remote host has neither bun nor npm available'); + } + + let lastError = null; + for (const command of commands) { + try { + await runRemoteCommand(parsed, controlPath, command); + return; + } catch (error) { + lastError = error; + } + } + throw lastError || new Error('Failed to install OpenChamber on remote host'); + } + + async probeRemoteSystemInfo(parsed, controlPath, port, openchamberPassword) { + const authPayload = openchamberPassword ? JSON.stringify({ password: openchamberPassword }) : '{}'; + const authEnabled = openchamberPassword ? '1' : '0'; + const script = `AUTH_STATUS=0; INFO_STATUS=0; HEALTH_STATUS=0; BODY_FILE="$(mktemp)"; COOKIE_FILE="$(mktemp)"; cleanup(){ rm -f "$BODY_FILE" "$COOKIE_FILE"; }; trap cleanup EXIT; if command -v curl >/dev/null 2>&1; then if [ "${authEnabled}" = "1" ]; then AUTH_STATUS="$(curl -sS --max-time 3 -o /dev/null -w '%{http_code}' -c "$COOKIE_FILE" -H 'content-type: application/json' --data ${shellQuote(authPayload)} http://127.0.0.1:${port}/auth/session || true)"; if [ "$AUTH_STATUS" = "200" ]; then INFO_STATUS="$(curl -sS --max-time 3 -b "$COOKIE_FILE" -o "$BODY_FILE" -w '%{http_code}' http://127.0.0.1:${port}/api/system/info || true)"; else INFO_STATUS="$(curl -sS --max-time 3 -o "$BODY_FILE" -w '%{http_code}' http://127.0.0.1:${port}/api/system/info || true)"; fi; else INFO_STATUS="$(curl -sS --max-time 3 -o "$BODY_FILE" -w '%{http_code}' http://127.0.0.1:${port}/api/system/info || true)"; fi; HEALTH_STATUS="$(curl -sS --max-time 3 -o /dev/null -w '%{http_code}' http://127.0.0.1:${port}/health || true)"; elif command -v wget >/dev/null 2>&1; then wget -qO "$BODY_FILE" http://127.0.0.1:${port}/api/system/info >/dev/null 2>&1; if [ $? -eq 0 ]; then INFO_STATUS=200; fi; wget -qO- http://127.0.0.1:${port}/health >/dev/null 2>&1; if [ $? -eq 0 ]; then HEALTH_STATUS=200; fi; else exit 127; fi; printf 'INFO_STATUS=%s\\nAUTH_STATUS=%s\\nHEALTH_STATUS=%s\\n' "$INFO_STATUS" "$AUTH_STATUS" "$HEALTH_STATUS"; cat "$BODY_FILE" 2>/dev/null || true`; + const output = await runRemoteCommand(parsed, controlPath, script); + const lines = output.split(/\r?\n/); + const infoStatus = parseProbeStatusLine(lines[0], 'INFO_STATUS=') || 0; + const authStatus = parseProbeStatusLine(lines[1], 'AUTH_STATUS=') || 0; + const healthStatus = parseProbeStatusLine(lines[2], 'HEALTH_STATUS=') || 0; + const body = lines.slice(3).join('\n'); + + if (isLivenessHttpStatus(infoStatus)) { + if (isAuthHttpStatus(infoStatus)) { + if (openchamberPassword && authStatus !== 200) { + throw new Error(`Remote OpenChamber requires UI authentication and configured password was rejected (auth status ${authStatus})`); + } + if (isLivenessHttpStatus(healthStatus)) return {}; + throw new Error('Remote OpenChamber requires UI authentication on /api/system/info; configure OpenChamber UI password'); + } + } else if (isLivenessHttpStatus(healthStatus)) { + return {}; + } else { + throw new Error(`Remote OpenChamber probe failed (info status ${infoStatus}, health status ${healthStatus})`); + } + + try { + return JSON.parse(body); + } catch { + return {}; + } + } + + async remoteServerRunning(parsed, controlPath, port, openchamberPassword) { + try { + await this.probeRemoteSystemInfo(parsed, controlPath, port, openchamberPassword); + return true; + } catch { + return false; + } + } + + async startRemoteServerManaged(parsed, controlPath, instance, desiredPort) { + let envPrefix = 'OPENCHAMBER_RUNTIME=ssh-remote'; + const secret = this.configuredOpenChamberPassword(instance); + if (secret) { + envPrefix += ` OPENCHAMBER_UI_PASSWORD=${shellQuote(secret)}`; + } + const output = await runRemoteCommand(parsed, controlPath, `${envPrefix} openchamber serve --daemon --hostname 127.0.0.1 --port ${desiredPort}`); + const port = output.split(/\s+/).map((token) => Number.parseInt(token, 10)).find((value) => Number.isFinite(value)); + return port || desiredPort; + } + + async stopRemoteServerBestEffort(parsed, controlPath, remotePort) { + try { + await runRemoteCommand( + parsed, + controlPath, + `if command -v curl >/dev/null 2>&1; then curl -fsS -X POST http://127.0.0.1:${remotePort}/api/system/shutdown >/dev/null 2>&1 || true; elif command -v wget >/dev/null 2>&1; then wget -qO- --method=POST http://127.0.0.1:${remotePort}/api/system/shutdown >/dev/null 2>&1 || true; fi`, + ); + } catch { + } + } + + async spawnMainForward(parsed, controlPath, bindHost, localPort, remotePort) { + return spawn('ssh', buildSshArgs(parsed, [ + '-o', 'ControlMaster=no', + '-o', `ControlPath=${controlPath}`, + '-N', + '-L', `${bindHost}:${localPort}:127.0.0.1:${remotePort}`, + ]), { + stdio: ['ignore', 'ignore', 'pipe'], + }); + } + + async spawnExtraForward(parsed, controlPath, forward) { + const args = [ + '-o', 'ControlMaster=no', + '-o', `ControlPath=${controlPath}`, + '-O', 'forward', + ]; + if (forward.type === 'local') { + args.push('-L', `${forward.localHost || '127.0.0.1'}:${forward.localPort}:${forward.remoteHost || '127.0.0.1'}:${forward.remotePort}`); + } else if (forward.type === 'remote') { + args.push('-R', `${forward.remoteHost || '127.0.0.1'}:${forward.remotePort}:${forward.localHost || '127.0.0.1'}:${forward.localPort}`); + } else { + args.push('-D', `${forward.localHost || '127.0.0.1'}:${forward.localPort}`); + } + const { code, stdout, stderr } = await runOutput('ssh', buildSshArgs(parsed, args)); + if (code !== 0) { + throw new Error((stderr || stdout || `Failed to configure extra SSH forward ${forward.id}`).trim()); + } + } + + async ensureRemoteServer(instance, parsed, controlPath) { + if (instance.remoteOpenchamber.mode === 'external') { + if (!instance.remoteOpenchamber.preferredPort) { + throw new Error('External mode requires a preferred remote OpenChamber port'); + } + const port = instance.remoteOpenchamber.preferredPort; + this.setStatus(instance.id, 'server_detecting', 'Probing external OpenChamber server', null, null, port, false, 0, false); + await this.probeRemoteSystemInfo(parsed, controlPath, port, this.configuredOpenChamberPassword(instance)); + return { remotePort: port, startedByUs: false }; + } + + this.setStatus(instance.id, 'remote_probe', 'Checking remote OpenChamber installation'); + const installedVersion = await this.currentRemoteOpenChamberVersion(parsed, controlPath); + if (!installedVersion) { + this.setStatus(instance.id, 'installing', 'Installing OpenChamber on remote host'); + await this.installOpenChamberManaged(parsed, controlPath, this.appVersion, instance.remoteOpenchamber.installMethod); + } else if (installedVersion !== this.appVersion) { + this.setStatus(instance.id, 'updating', `Updating remote OpenChamber from ${installedVersion} to ${this.appVersion}`); + await this.installOpenChamberManaged(parsed, controlPath, this.appVersion, instance.remoteOpenchamber.installMethod); + } + + this.setStatus(instance.id, 'server_detecting', 'Detecting managed OpenChamber server'); + let remotePort = instance.remoteOpenchamber.preferredPort || null; + let startedByUs = false; + if (remotePort && !(await this.remoteServerRunning(parsed, controlPath, remotePort, this.configuredOpenChamberPassword(instance)))) { + remotePort = null; + } + if (!remotePort) { + this.setStatus(instance.id, 'server_starting', 'Starting managed OpenChamber server'); + const desiredPort = instance.remoteOpenchamber.preferredPort || randomPortCandidate(instance.id); + remotePort = await this.startRemoteServerManaged(parsed, controlPath, instance, desiredPort); + startedByUs = true; + } + if (!(await this.remoteServerRunning(parsed, controlPath, remotePort, this.configuredOpenChamberPassword(instance)))) { + throw new Error('Managed OpenChamber server failed to become reachable'); + } + return { remotePort, startedByUs }; + } + + async disconnectInternal(id, reportIdle) { + const timer = this.monitorTimers.get(id); + if (timer) { + clearTimeout(timer); + this.monitorTimers.delete(id); + } + + const session = this.sessions.get(id); + this.sessions.delete(id); + + if (session) { + if (session.startedByUs && session.instance.remoteOpenchamber.mode === 'managed' && !session.instance.remoteOpenchamber.keepRunning) { + await this.stopRemoteServerBestEffort(session.parsed, session.controlPath, session.remotePort); + } + await stopControlMasterBestEffort(session.parsed, session.controlPath); + for (const child of [session.mainForward, session.master]) { + try { + child.kill('SIGTERM'); + } catch { + } + } + try { + await fsp.rm(session.controlPath, { force: true }); + } catch { + } + try { + await fsp.rm(path.join(session.sessionDir, 'askpass.sh'), { force: true }); + } catch { + } + } + + this.clearRetryAttempt(id); + if (reportIdle) { + this.setStatus(id, 'idle', null, null, null, null, false, 0, false); + } + } + + async connectBlocking(instance) { + const id = instance.id; + this.setStatus(id, 'config_resolved', 'Resolving SSH command'); + const parsed = instance.sshParsed || parseSshCommand(instance.sshCommand); + await this.resolveSshConfig(parsed); + + this.setStatus(id, 'auth_check', 'Checking SSH connectivity'); + const sessionDir = this.ensureSessionDir(id); + const controlPath = this.controlPathForInstance(id); + try { await fsp.rm(controlPath, { force: true }); } catch {} + const askpassPath = path.join(sessionDir, 'askpass.sh'); + await writeAskpassScript(askpassPath); + + this.setStatus(id, 'master_connecting', 'Establishing SSH ControlMaster'); + const sshPassword = instance.auth?.sshPassword?.enabled ? instance.auth.sshPassword.value : null; + const master = await this.spawnMasterProcess(parsed, controlPath, askpassPath, sshPassword); + await this.waitForMasterReady(parsed, controlPath, instance.connectionTimeoutSec || DEFAULT_CONNECTION_TIMEOUT_SEC, master); + + this.setStatus(id, 'remote_probe', 'Probing remote platform'); + const remoteOs = (await runRemoteCommand(parsed, controlPath, 'uname -s', instance.connectionTimeoutSec || DEFAULT_CONNECTION_TIMEOUT_SEC)).trim().toLowerCase(); + if (!['linux', 'darwin'].includes(remoteOs)) { + master.kill('SIGTERM'); + throw new Error(`Unsupported remote OS: ${remoteOs}`); + } + + const { remotePort, startedByUs } = await this.ensureRemoteServer(instance, parsed, controlPath); + this.setStatus(id, 'forwarding', 'Setting up port forwards', null, null, remotePort, startedByUs, 0, false); + + const bindHost = sanitizeBindHost(instance.localForward?.bindHost); + let localPort = Number(instance.localForward?.preferredLocalPort) || 0; + if (!localPort) { + localPort = await pickUnusedLocalPort(); + } + if (!(await isLocalPortAvailable(bindHost, localPort))) { + localPort = await pickUnusedLocalPort(); + } + + const mainForward = await this.spawnMainForward(parsed, controlPath, bindHost, localPort, remotePort); + let mainForwardDetached = false; + await new Promise((resolve) => setTimeout(resolve, 250)); + if (typeof mainForward.exitCode === 'number') { + if (mainForward.exitCode === 0) { + mainForwardDetached = true; + this.appendLogWithLevel(id, 'INFO', 'Main tunnel helper exited after ControlMaster handoff'); + } else { + master.kill('SIGTERM'); + throw new Error(`Failed to start main port forward (status: ${mainForward.exitCode})`); + } + } + + const extraErrors = []; + for (const forward of instance.portForwards.filter((item) => item.enabled)) { + try { + await this.spawnExtraForward(parsed, controlPath, forward); + if (forward.type === 'local' && forward.localPort) { + await new Promise((resolve) => setTimeout(resolve, 100)); + if (!(await isLocalTunnelReachable(forward.localPort))) { + extraErrors.push(`${forward.id}: local listener 127.0.0.1:${forward.localPort} is not reachable`); + } + } + } catch (error) { + extraErrors.push(`${forward.id}: ${error instanceof Error ? error.message : String(error)}`); + } + } + + await waitLocalForwardReady(localPort); + + const localUrl = `http://127.0.0.1:${localPort}`; + const label = instance.nickname?.trim() || parsed.destination || id; + await this.updateHostUrl(id, label, localUrl); + if (instance.localForward?.preferredLocalPort !== localPort) { + await this.persistLocalPort(id, localPort); + } + + this.sessions.set(id, { + instance, + parsed, + sessionDir, + controlPath, + localPort, + remotePort, + startedByUs, + master, + masterDetached: false, + mainForward, + mainForwardDetached, + }); + + this.clearRetryAttempt(id); + this.setStatus( + id, + 'ready', + extraErrors.length === 0 ? 'SSH instance is ready' : `SSH instance is ready with forward warnings: ${extraErrors.join('; ')}`, + localUrl, + localPort, + remotePort, + startedByUs, + 0, + false, + ); + this.spawnMonitor(id); + } + + spawnMonitor(id) { + const existing = this.monitorTimers.get(id); + if (existing) clearTimeout(existing); + let healthyTicks = 0; + const tick = async () => { + const session = this.sessions.get(id); + if (!session) { + this.monitorTimers.delete(id); + return; + } + + let droppedReason = null; + let detachedNotice = null; + + if (!session.mainForwardDetached) { + if (typeof session.mainForward.exitCode === 'number') { + if (session.mainForward.exitCode === 0) { + session.mainForwardDetached = true; + detachedNotice = 'Main tunnel helper exited after ControlMaster handoff'; + } else { + droppedReason = `Main SSH forward exited (${session.mainForward.exitCode})`; + } + } + } + + if (!droppedReason) { + if (session.mainForwardDetached) { + // Fast path: cheap TCP probe before expensive SSH subprocess + if (await isLocalTunnelReachable(session.localPort)) { + // Tunnel alive — skip SSH check + } else if (!await isControlMasterAlive(session.parsed, session.controlPath)) { + droppedReason = 'SSH ControlMaster is not reachable'; + } else { + detachedNotice = 'Local tunnel unreachable but ControlMaster is alive'; + } + } + } + + if (detachedNotice) { + this.appendLogWithLevel(id, 'INFO', detachedNotice); + } + if (!droppedReason) { + healthyTicks++; + const pollMs = healthyTicks >= MONITOR_STABILIZE_TICKS ? MONITOR_STEADY_POLL_MS : MONITOR_INITIAL_POLL_MS; + this.monitorTimers.set(id, setTimeout(tick, pollMs)); + return; + } + + this.appendLogWithLevel(id, 'WARN', droppedReason); + await this.disconnectInternal(id, false); + const attempt = this.nextRetryAttempt(id); + if (attempt > DEFAULT_RECONNECT_MAX_ATTEMPTS) { + this.setStatus(id, 'error', `${droppedReason}. Retry limit reached`, null, null, null, false, attempt, true); + return; + } + + this.setStatus(id, 'degraded', `${droppedReason}. Reconnecting`, null, null, null, false, attempt, false); + const delayMs = Math.min((2 ** Math.max(attempt - 1, 0)) * 1000 + (nowMillis() % 700) + 100, 30000); + await new Promise((resolve) => setTimeout(resolve, delayMs)); + try { + await this.connect(id); + } catch (error) { + this.setStatus(id, 'error', error instanceof Error ? error.message : String(error), null, null, null, false, attempt, true); + } + }; + this.monitorTimers.set(id, setTimeout(tick, MONITOR_INITIAL_POLL_MS)); + } + + async connect(id) { + const trimmed = String(id || '').trim(); + if (!trimmed || trimmed === LOCAL_HOST_ID) { + throw new Error('SSH instance id is required'); + } + + if (this.connecting.has(trimmed)) { + this.appendLogWithLevel(trimmed, 'INFO', 'Connection already in progress'); + return this.connecting.get(trimmed); + } + + const instance = this.readInstances().instances.find((entry) => entry?.id === trimmed); + if (!instance) { + throw new Error('SSH instance not found'); + } + + const retryAttempt = this.currentRetryAttempt(trimmed); + const connectAttempt = this.nextConnectAttempt(trimmed); + this.appendAttemptSeparator(trimmed, connectAttempt, retryAttempt); + this.appendLog(trimmed, 'Starting SSH connection'); + await this.disconnectInternal(trimmed, false); + + const task = this.connectBlocking(this.sanitizeInstance(instance)) + .catch(async (error) => { + this.setStatus(trimmed, 'error', error instanceof Error ? error.message : String(error), null, null, null, false, 0, true); + await this.disconnectInternal(trimmed, false); + throw error; + }) + .finally(() => { + this.connecting.delete(trimmed); + }); + this.connecting.set(trimmed, task); + return task; + } + + async disconnect(id) { + const trimmed = String(id || '').trim(); + if (!trimmed || trimmed === LOCAL_HOST_ID) { + throw new Error('SSH instance id is required'); + } + await this.disconnectInternal(trimmed, true); + } + + async statusesWithDefaults(id) { + if (id) { + return [this.statusSnapshotForInstance(id)]; + } + return this.readInstances().instances + .map((instance) => this.statusSnapshotForInstance(instance.id)) + .sort((left, right) => left.id.localeCompare(right.id)); + } + + async shutdownAll() { + const ids = [...new Set([...this.sessions.keys(), ...this.connecting.keys(), ...this.monitorTimers.keys()])]; + for (const id of ids) { + await this.disconnectInternal(id, false); + } + } +} diff --git a/src/packages/ui/package.json b/src/packages/ui/package.json index c55e71c..a9a9f79 100644 --- a/src/packages/ui/package.json +++ b/src/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@openchamber/ui", - "version": "1.9.1", + "version": "1.9.9", "private": true, "type": "module", "main": "src/main.tsx", @@ -25,13 +25,13 @@ "@codemirror/lang-sql": "^6.10.0", "@codemirror/lang-xml": "^6.1.0", "@codemirror/lang-yaml": "^6.1.2", - "@codemirror/language": "^6.12.1", + "@codemirror/language": "6.12.2", "@codemirror/language-data": "^6.5.2", "@codemirror/legacy-modes": "^6.5.2", "@codemirror/lint": "^6.9.2", "@codemirror/search": "^6.6.0", "@codemirror/state": "^6.5.4", - "@codemirror/view": "^6.39.13", + "@codemirror/view": "6.39.13", "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", @@ -39,20 +39,11 @@ "@fontsource/ibm-plex-sans": "^5.1.1", "@ibm/plex": "^6.4.1", "@lezer/highlight": "^1.2.3", - "@opencode-ai/sdk": "^1.3.0", + "@opencode-ai/sdk": "^1.4.25", "@pierre/diffs": "1.1.0-beta.13", - "@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-switch": "^1.2.6", - "@radix-ui/react-toggle": "^1.1.10", - "@radix-ui/react-tooltip": "^1.2.8", + "@base-ui/react": "^1.4.0", "@remixicon/react": "^4.7.0", - "@streamdown/code": "^1.0.2", + "@simplewebauthn/browser": "13.3.0", "@tanstack/react-virtual": "^3.13.18", "@types/react-syntax-highlighter": "^15.5.13", "beautiful-mermaid": "^1.1.3", @@ -60,12 +51,18 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "codemirror-lang-elixir": "^4.0.0", + "dompurify": "^3.2.7", "express": "^5.1.0", "fuse.js": "^7.1.0", "ghostty-web": "^0.4.0", "heic2any": "^0.0.4", "html-to-image": "^1.11.13", "http-proxy-middleware": "^3.0.5", + "katex": "^0.16.21", + "marked": "^17.0.3", + "morphdom": "^2.7.7", + "rehype-katex": "^7.0.1", + "remark-math": "^6.0.0", "motion": "^12.23.24", "next-themes": "^0.4.6", "prismjs": "^1.30.0", @@ -73,9 +70,9 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "react-syntax-highlighter": "^15.6.6", + "remend": "^1.2.1", "simple-git": "^3.28.0", "sonner": "^2.0.7", - "streamdown": "^2.2.0", "strip-json-comments": "^5.0.3", "tailwind-merge": "^3.3.1", "yaml": "^2.8.1", @@ -85,7 +82,7 @@ "devDependencies": { "@eslint/js": "^9.33.0", "@tailwindcss/postcss": "^4.0.0", - "@tauri-apps/api": "^2.9.0", + "@tauri-apps/api": "^2.10.1", "@types/node": "^24.3.1", "@types/prismjs": "^1.26.6", "@types/qrcode": "^1.5.5", diff --git a/src/packages/ui/src/App.tsx b/src/packages/ui/src/App.tsx index 13875df..250a3a8 100644 --- a/src/packages/ui/src/App.tsx +++ b/src/packages/ui/src/App.tsx @@ -5,31 +5,45 @@ import { AgentManagerView } from '@/components/views/agent-manager'; import { ChatView } from '@/components/views'; import { FireworksProvider } from '@/contexts/FireworksContext'; import { Toaster } from '@/components/ui/sonner'; +import { Button } from '@/components/ui/button'; import { MemoryDebugPanel } from '@/components/ui/MemoryDebugPanel'; +import { setStreamPerfEnabled } from '@/stores/utils/streamDebug'; import { ErrorBoundary } from '@/components/ui/ErrorBoundary'; -import { useEventStream } from '@/hooks/useEventStream'; +// useEventStream removed — replaced by SyncProvider + SyncBridge import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts'; import { useMenuActions } from '@/hooks/useMenuActions'; import { useSessionStatusBootstrap } from '@/hooks/useSessionStatusBootstrap'; -import { useServerSessionStatus } from '@/hooks/useServerSessionStatus'; import { useSessionAutoCleanup } from '@/hooks/useSessionAutoCleanup'; import { useQueuedMessageAutoSend } from '@/hooks/useQueuedMessageAutoSend'; import { useRouter } from '@/hooks/useRouter'; import { usePushVisibilityBeacon } from '@/hooks/usePushVisibilityBeacon'; import { usePwaManifestSync } from '@/hooks/usePwaManifestSync'; import { usePwaInstallPrompt } from '@/hooks/usePwaInstallPrompt'; +import { useWindowControlsOverlayLayout } from '@/hooks/useWindowControlsOverlayLayout'; import { useWindowTitle } from '@/hooks/useWindowTitle'; -import { useGitHubPrBackgroundTracking } from '@/hooks/useGitHubPrBackgroundTracking'; -import { GitPollingProvider } from '@/hooks/useGitPolling'; import { useConfigStore } from '@/stores/useConfigStore'; import { hasModifier } from '@/lib/utils'; -import { isDesktopLocalOriginActive, isDesktopShell } from '@/lib/desktop'; -import { OnboardingScreen } from '@/components/onboarding/OnboardingScreen'; -import { useSessionStore } from '@/stores/useSessionStore'; +import { isDesktopLocalOriginActive, isDesktopShell, isTauriShell, restartDesktopApp } from '@/lib/desktop'; +import { + getInjectedBootOutcome, + getBootInjectionStatus, + resolveDesktopBootView, + canDismissInitialLoading, + shouldRestartDesktopBootFlow, + type BootInjectionStatus, + type DesktopBootView, +} from '@/lib/desktopBoot'; +import type { RecoveryVariant } from '@/components/onboarding/DesktopConnectionRecovery'; +import { useSessionUIStore } from '@/sync/session-ui-store'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; +import { useProjectsStore } from '@/stores/useProjectsStore'; import { opencodeClient } from '@/lib/opencode/client'; +import { SyncProvider, useSessions } from '@/sync/sync-context'; +import { useSync } from '@/sync/use-sync'; +import { setOptimisticRefs } from '@/sync/session-actions'; import { useFontPreferences } from '@/hooks/useFontPreferences'; import { CODE_FONT_OPTION_MAP, DEFAULT_MONO_FONT, DEFAULT_UI_FONT, UI_FONT_OPTION_MAP } from '@/lib/fontOptions'; +import { loadMonoFont, loadUiFont } from '@/lib/fontLoader'; import { ConfigUpdateOverlay } from '@/components/ui/ConfigUpdateOverlay'; import { AboutDialog } from '@/components/ui/AboutDialog'; import { RuntimeAPIProvider } from '@/contexts/RuntimeAPIProvider'; @@ -37,15 +51,23 @@ import { registerRuntimeAPIs } from '@/contexts/runtimeAPIRegistry'; import { VoiceProvider } from '@/components/voice'; import { useUIStore } from '@/stores/useUIStore'; import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore'; +import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore'; import type { RuntimeAPIs } from '@/lib/api/types'; import { TooltipProvider } from '@/components/ui/tooltip'; +import { QuickOpenDialog } from '@/components/ui/QuickOpenDialog'; +import { McpOAuthCallbackPage } from '@/components/sections/mcp/McpOAuthCallbackPage'; +import { MCP_OAUTH_CALLBACK_PATH } from '@/components/sections/mcp/mcpOAuth'; +import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery'; +import { useI18n } from '@/lib/i18n'; -const CLI_MISSING_ERROR_REGEX = - /ENOENT|spawn\s+opencode|Unable\s+to\s+locate\s+the\s+opencode\s+CLI|OpenCode\s+CLI\s+not\s+found|opencode(\.exe)?\s+not\s+found|opencode(\.exe)?:\s*command\s+not\s+found|not\s+recognized\s+as\s+an\s+internal\s+or\s+external\s+command|env:\s*['"]?(node|bun)['"]?:\s*No\s+such\s+file\s+or\s+directory|(node|bun):\s*No\s+such\s+file\s+or\s+directory/i; -const CLI_ONBOARDING_HEALTH_POLL_MS = 1500; +// Lazy-loaded heavy views — loaded on demand to reduce initial bundle size. +const OnboardingScreen = lazyWithChunkRecovery(() => + import('@/components/onboarding/OnboardingScreen').then((m) => ({ default: m.OnboardingScreen })), +); const AboutDialogWrapper: React.FC = () => { - const { isAboutDialogOpen, setAboutDialogOpen } = useUIStore(); + const isAboutDialogOpen = useUIStore((s) => s.isAboutDialogOpen); + const setAboutDialogOpen = useUIStore((s) => s.setAboutDialogOpen); return ( { ); }; +const StartupInitializationRecovery: React.FC<{ + onRetry: () => void; + isRetrying: boolean; +}> = ({ onRetry, isRetrying }) => { + const { t } = useI18n(); + + return ( +
    +
    +
    +

    {t('startup.initRecovery.title')}

    +

    {t('startup.initRecovery.description')}

    +
    + +
    +
    + ); +}; + type AppProps = { apis: RuntimeAPIs; }; @@ -94,16 +137,80 @@ const readEmbeddedSessionChatConfig = (): EmbeddedSessionChatConfig | null => { }; }; +const isMcpOAuthCallbackPath = (): boolean => { + if (typeof window === 'undefined') { + return false; + } + + return window.location.pathname === MCP_OAUTH_CALLBACK_PATH; +}; + +const EmbeddedSessionSelectionGate: React.FC<{ + embeddedSessionChat: EmbeddedSessionChatConfig | null; + isVSCodeRuntime: boolean; +}> = ({ embeddedSessionChat, isVSCodeRuntime }) => { + const sessions = useSessions(); + const currentSessionId = useSessionUIStore((state) => state.currentSessionId); + const setCurrentSession = useSessionUIStore((state) => state.setCurrentSession); + + React.useEffect(() => { + if (!embeddedSessionChat || isVSCodeRuntime) { + return; + } + + if (currentSessionId === embeddedSessionChat.sessionId) { + return; + } + + if (!sessions.some((session) => session.id === embeddedSessionChat.sessionId)) { + return; + } + + void setCurrentSession(embeddedSessionChat.sessionId); + }, [currentSessionId, embeddedSessionChat, isVSCodeRuntime, sessions, setCurrentSession]); + + return null; +}; + +const SyncOptimisticBridge: React.FC = () => { + const sync = useSync(); + const addRef = React.useRef(sync.optimistic.add); + const removeRef = React.useRef(sync.optimistic.remove); + addRef.current = sync.optimistic.add; + removeRef.current = sync.optimistic.remove; + + React.useEffect(() => { + setOptimisticRefs( + (input) => addRef.current(input), + (input) => removeRef.current(input), + ); + }, []); + + return null; +}; + +function SyncAppEffects({ embeddedBackgroundWorkEnabled }: { + embeddedBackgroundWorkEnabled: boolean; +}) { + usePwaManifestSync(); + useWindowControlsOverlayLayout(); + useSessionAutoCleanup(embeddedBackgroundWorkEnabled); + useQueuedMessageAutoSend(embeddedBackgroundWorkEnabled); + useKeyboardShortcuts(); + + return ; +} + function App({ apis }: AppProps) { - const { initializeApp, isInitialized, isConnected } = useConfigStore(); + const initializeApp = useConfigStore((s) => s.initializeApp); + const isInitialized = useConfigStore((s) => s.isInitialized); + const isConnected = useConfigStore((s) => s.isConnected); const providersCount = useConfigStore((state) => state.providers.length); const agentsCount = useConfigStore((state) => state.agents.length); const loadProviders = useConfigStore((state) => state.loadProviders); const loadAgents = useConfigStore((state) => state.loadAgents); - const { error, clearError, loadSessions } = useSessionStore(); - const currentSessionId = useSessionStore((state) => state.currentSessionId); - const setCurrentSession = useSessionStore((state) => state.setCurrentSession); - const sessions = useSessionStore((state) => state.sessions); + const error = useSessionUIStore((s) => s.error); + const clearError = useSessionUIStore((s) => s.clearError); const currentDirectory = useDirectoryStore((state) => state.currentDirectory); const setDirectory = useDirectoryStore((state) => state.setDirectory); const isSwitchingDirectory = useDirectoryStore((state) => state.isSwitchingDirectory); @@ -111,12 +218,33 @@ function App({ apis }: AppProps) { const { uiFont, monoFont } = useFontPreferences(); const refreshGitHubAuthStatus = useGitHubAuthStore((state) => state.refreshStatus); const [isVSCodeRuntime, setIsVSCodeRuntime] = React.useState(() => apis.runtime.isVSCode); - const [showCliOnboarding, setShowCliOnboarding] = React.useState(false); const [isEmbeddedVisible, setIsEmbeddedVisible] = React.useState(true); + const [initRetryExhausted, setInitRetryExhausted] = React.useState(false); + const [initRetryEpoch, setInitRetryEpoch] = React.useState(0); + const [manualInitRetrying, setManualInitRetrying] = React.useState(false); const isDesktopRuntime = React.useMemo(() => isDesktopShell(), []); + const setPlanModeEnabled = useFeatureFlagsStore((state) => state.setPlanModeEnabled); + const [bootInjectionStatus, setBootInjectionStatus] = React.useState(() => { + return getBootInjectionStatus(); + }); + const [bootView, setBootView] = React.useState(() => { + const outcome = getInjectedBootOutcome(); + return outcome !== null + ? resolveDesktopBootView({ isDesktopShell: true, bootOutcome: outcome }) + : null; + }); const appReadyDispatchedRef = React.useRef(false); + const initializationInFlightRef = React.useRef(false); const embeddedSessionChat = React.useMemo(() => readEmbeddedSessionChatConfig(), []); const embeddedBackgroundWorkEnabled = !embeddedSessionChat || isEmbeddedVisible; + const isMcpOAuthCallback = React.useMemo(() => isMcpOAuthCallbackPath(), []); + + React.useEffect(() => { + setStreamPerfEnabled(showMemoryDebug); + return () => { + setStreamPerfEnabled(false); + }; + }, [showMemoryDebug]); React.useEffect(() => { setIsVSCodeRuntime(apis.runtime.isVSCode); @@ -135,8 +263,6 @@ function App({ apis }: AppProps) { void refreshGitHubAuthStatus(apis.github, { force: true }); }, [apis.github, embeddedSessionChat, refreshGitHubAuthStatus]); - useGitHubPrBackgroundTracking(embeddedBackgroundWorkEnabled ? apis.github : undefined, apis.git); - React.useEffect(() => { if (typeof document === 'undefined') { return; @@ -144,6 +270,8 @@ function App({ apis }: AppProps) { 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; + void loadUiFont(uiFont); + void loadMonoFont(monoFont); root.style.setProperty('--font-sans', uiStack); root.style.setProperty('--font-heading', uiStack); @@ -157,25 +285,55 @@ function App({ apis }: AppProps) { } }, [uiFont, monoFont]); + const bootOutcomeKnown = bootInjectionStatus === 'valid'; + const bootViewIsMain = bootView?.screen === 'main'; + + // Splash dismissal: use the authoritative loading gate from desktopBoot. + // Desktop shells strictly require a valid boot outcome before dismissing. + // Non-main outcomes (chooser/recovery) can dismiss without waiting for init. 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); + if (!canDismissInitialLoading({ + isDesktopShell: isDesktopRuntime, + isInitialized, + bootOutcomeKnown, + bootViewIsMain, + })) { + return; } - }, [isInitialized]); + const timer = setTimeout(() => { + const loadingElement = document.getElementById('initial-loading'); + if (loadingElement) { + loadingElement.classList.add('fade-out'); + setTimeout(() => { + loadingElement.remove(); + }, 300); + } + }, 150); + + return () => clearTimeout(timer); + }, [isDesktopRuntime, isInitialized, bootOutcomeKnown, bootViewIsMain]); + + // Deterministic malformed handling: update splash text so the user + // sees a specific error instead of a generic spinner, but do NOT + // dismiss the splash (that only happens on a valid outcome). React.useEffect(() => { + if (!isDesktopRuntime || bootInjectionStatus !== 'malformed') { + return; + } + + const loadingElement = document.getElementById('initial-loading'); + if (loadingElement) { + loadingElement.textContent = 'Desktop startup failed — please restart the app.'; + } + }, [isDesktopRuntime, bootInjectionStatus]); + + // Non-desktop fallback: remove splash after 5 seconds even if init stalls. + React.useEffect(() => { + if (isDesktopRuntime) { + return; + } + const fallbackTimer = setTimeout(() => { const loadingElement = document.getElementById('initial-loading'); if (loadingElement && !isInitialized) { @@ -187,7 +345,29 @@ function App({ apis }: AppProps) { }, 5000); return () => clearTimeout(fallbackTimer); - }, [isInitialized]); + }, [isDesktopRuntime, isInitialized]); + + React.useEffect(() => { + let cancelled = false; + + const run = async () => { + const res = await fetch('/health', { method: 'GET' }).catch(() => null); + if (!res || !res.ok || cancelled) return; + const data = (await res.json().catch(() => null)) as null | { + planModeExperimentalEnabled?: unknown; + }; + if (!data || cancelled) return; + const raw = data.planModeExperimentalEnabled; + const enabled = raw === true || raw === 1 || raw === '1' || raw === 'true'; + setPlanModeEnabled(enabled); + }; + + void run(); + + return () => { + cancelled = true; + }; + }, [setPlanModeEnabled]); React.useEffect(() => { const init = async () => { @@ -196,76 +376,138 @@ function App({ apis }: AppProps) { if (isVSCodeRuntime) { return; } - await initializeApp(); + if (initializationInFlightRef.current) { + return; + } + initializationInFlightRef.current = true; + try { + await initializeApp(); + } finally { + initializationInFlightRef.current = false; + } }; init(); }, [initializeApp, isVSCodeRuntime]); - const startupRecoveryInProgressRef = React.useRef(false); - const startupRecoveryLastAttemptRef = React.useRef(0); - React.useEffect(() => { - if (isVSCodeRuntime) { - return; - } - if (!isConnected) { - return; - } - if (providersCount > 0 && agentsCount > 0) { - return; - } - if (startupRecoveryInProgressRef.current) { - return; - } + if (isVSCodeRuntime || isInitialized) return; - const now = Date.now(); - if (now - startupRecoveryLastAttemptRef.current < 750) { - return; - } + let active = true; + let retryTimer: ReturnType | undefined; + let retryCount = 0; + const MAX_RETRIES = 10; + const BASE_DELAY_MS = 1000; - startupRecoveryLastAttemptRef.current = now; - startupRecoveryInProgressRef.current = true; - - const repair = async () => { - try { - if (providersCount === 0) { - await loadProviders(); - } - if (agentsCount === 0) { - await loadAgents(); - } - } catch { - // Keep UI responsive; we'll retry on next cycle. - } finally { - startupRecoveryInProgressRef.current = false; + const retryInitialization = async () => { + if (!active) return; + if (retryCount >= MAX_RETRIES) { + setInitRetryExhausted(true); + return; } + const state = useConfigStore.getState(); + if (state.isInitialized) { + setInitRetryExhausted(false); + return; + } + if (initializationInFlightRef.current) { + retryTimer = setTimeout(retryInitialization, BASE_DELAY_MS); + return; + } + + retryCount += 1; + initializationInFlightRef.current = true; + try { + await state.initializeApp(); + } finally { + initializationInFlightRef.current = false; + } + + const next = useConfigStore.getState(); + if (!active) return; + if (next.isInitialized) { + setInitRetryExhausted(false); + return; + } + if (retryCount >= MAX_RETRIES) { + setInitRetryExhausted(true); + return; + } + const delay = Math.min(BASE_DELAY_MS * Math.pow(2, retryCount - 1), 16000); + retryTimer = setTimeout(retryInitialization, delay); }; - void repair(); - }, [agentsCount, isConnected, isVSCodeRuntime, loadAgents, loadProviders, providersCount]); + retryTimer = setTimeout(retryInitialization, BASE_DELAY_MS); + + return () => { + active = false; + if (retryTimer) clearTimeout(retryTimer); + }; + }, [initRetryEpoch, isInitialized, isVSCodeRuntime]); + + React.useEffect(() => { + if (isInitialized) { + setInitRetryExhausted(false); + } + }, [isInitialized]); + + React.useEffect(() => { + if (!initRetryExhausted) return; + + const loadingElement = document.getElementById('initial-loading'); + if (loadingElement) { + loadingElement.classList.add('fade-out'); + setTimeout(() => { + loadingElement.remove(); + }, 300); + } + }, [initRetryExhausted]); + + // Startup recovery: poll until providers AND agents are loaded. + // loadProviders/loadAgents resolve normally even on failure (errors swallowed), + // so a reactive effect can't detect failure — we need an interval. + React.useEffect(() => { + if (isVSCodeRuntime || !isConnected) return; + if (providersCount > 0 && agentsCount > 0) return; + + let active = true; + let retries = 0; + const MAX_RETRIES = 15; + const attempt = async () => { + const state = useConfigStore.getState(); + if (state.providers.length > 0 && state.agents.length > 0) return; + try { + if (state.providers.length === 0) await loadProviders(); + if (useConfigStore.getState().agents.length === 0) await loadAgents(); + } catch { /* retry next interval */ } + }; + + void attempt(); + const id = setInterval(() => { + if (!active) return; + if (++retries >= MAX_RETRIES) { clearInterval(id); return; } + void attempt(); + }, 2000); + return () => { active = false; clearInterval(id); }; + }, [isConnected, isVSCodeRuntime, loadAgents, loadProviders, providersCount, agentsCount]); React.useEffect(() => { if (isSwitchingDirectory) { return; } - const syncDirectoryAndSessions = async () => { - // VS Code runtime loads sessions via VSCodeLayout bootstrap to avoid startup races. - if (isVSCodeRuntime) { - return; - } + // VS Code runtime loads sessions via VSCodeLayout bootstrap to avoid startup races. + if (isVSCodeRuntime) { + return; + } - if (!isConnected) { - return; - } - opencodeClient.setDirectory(currentDirectory); + if (!isConnected) { + return; + } + opencodeClient.setDirectory(currentDirectory); - await loadSessions(); - }; - - syncDirectoryAndSessions(); - }, [currentDirectory, isSwitchingDirectory, loadSessions, isConnected, isVSCodeRuntime]); + // Session loading is handled by the sync system's bootstrap — no manual loadSessions needed. + }, [currentDirectory, isSwitchingDirectory, isConnected, isVSCodeRuntime]); React.useEffect(() => { if (!embeddedSessionChat || typeof window === 'undefined') { @@ -317,22 +559,6 @@ function App({ apis }: AppProps) { setDirectory(embeddedSessionChat.directory, { showOverlay: false }); }, [currentDirectory, embeddedSessionChat, isVSCodeRuntime, setDirectory]); - React.useEffect(() => { - if (!embeddedSessionChat || isVSCodeRuntime) { - return; - } - - if (currentSessionId === embeddedSessionChat.sessionId) { - return; - } - - if (!sessions.some((session) => session.id === embeddedSessionChat.sessionId)) { - return; - } - - void setCurrentSession(embeddedSessionChat.sessionId); - }, [currentSessionId, embeddedSessionChat, isVSCodeRuntime, sessions, setCurrentSession]); - React.useEffect(() => { if (!embeddedSessionChat || typeof window === 'undefined') { return; @@ -356,6 +582,40 @@ function App({ apis }: AppProps) { }; }, [embeddedSessionChat]); + React.useEffect(() => { + if (typeof window === 'undefined') return; + + const handler = (event: Event) => { + const detail = (event as CustomEvent<{ sessionId?: string }>).detail; + const sessionId = typeof detail?.sessionId === 'string' ? detail.sessionId.trim() : ''; + if (!sessionId) return; + void useSessionUIStore.getState().setCurrentSession(sessionId); + }; + + window.addEventListener('openchamber:open-session', handler as EventListener); + return () => window.removeEventListener('openchamber:open-session', handler as EventListener); + }, []); + + React.useEffect(() => { + if (typeof window === 'undefined') return; + + const handler = (event: Event) => { + const detail = (event as CustomEvent<{ projectPath?: string }>).detail; + const projectPath = typeof detail?.projectPath === 'string' ? detail.projectPath.trim() : ''; + if (!projectPath) return; + const projectsStore = useProjectsStore.getState(); + const existing = projectsStore.projects.find((project) => project.path === projectPath); + if (existing) { + projectsStore.setActiveProject(existing.id); + } else { + projectsStore.addProject(projectPath); + } + }; + + window.addEventListener('openchamber:open-project', handler as EventListener); + return () => window.removeEventListener('openchamber:open-project', handler as EventListener); + }, []); + React.useEffect(() => { if (typeof window === 'undefined') return; if (!isInitialized || isSwitchingDirectory) return; @@ -365,22 +625,17 @@ function App({ apis }: AppProps) { window.dispatchEvent(new Event('openchamber:app-ready')); }, [isInitialized, isSwitchingDirectory]); - useEventStream({ enabled: embeddedBackgroundWorkEnabled }); + // useEventStream replaced by SyncProvider + SyncBridge - // Server-authoritative session status polling - // Replaces SSE-dependent status updates with reliable HTTP polling - useServerSessionStatus({ enabled: embeddedBackgroundWorkEnabled }); + // Session attention now handled by notification-store via SSE events (session.idle/session.error) usePushVisibilityBeacon({ enabled: embeddedBackgroundWorkEnabled }); - usePwaManifestSync(); usePwaInstallPrompt(); useWindowTitle(); useRouter(); - useKeyboardShortcuts(); - const handleToggleMemoryDebug = React.useCallback(() => { setShowMemoryDebug(prev => !prev); }, []); @@ -388,8 +643,6 @@ function App({ apis }: AppProps) { useMenuActions(handleToggleMemoryDebug); useSessionStatusBootstrap({ enabled: embeddedBackgroundWorkEnabled }); - useSessionAutoCleanup({ enabled: embeddedBackgroundWorkEnabled }); - useQueuedMessageAutoSend({ enabled: embeddedBackgroundWorkEnabled }); React.useEffect(() => { if (embeddedSessionChat) { @@ -397,14 +650,19 @@ function App({ apis }: AppProps) { } const handleKeyDown = (e: KeyboardEvent) => { - if (hasModifier(e) && e.shiftKey && e.key === 'D') { + const isDebugShortcut = hasModifier(e) + && e.shiftKey + && !e.altKey + && (e.code === 'KeyD' || e.key.toLowerCase() === 'd'); + + if (isDebugShortcut) { e.preventDefault(); setShowMemoryDebug(prev => !prev); } }; - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); + window.addEventListener('keydown', handleKeyDown, true); + return () => window.removeEventListener('keydown', handleKeyDown, true); }, [embeddedSessionChat]); React.useEffect(() => { @@ -418,58 +676,141 @@ function App({ apis }: AppProps) { } }, [clearError, embeddedSessionChat, error]); + // Poll for the injected boot outcome until it becomes available (desktop only). + // The Rust backend sets window.__OPENCHAMBER_DESKTOP_BOOT_OUTCOME__ once the + // sidecar reaches a stable state. We poll with exponential backoff to handle + // potential race conditions during startup and config writes. React.useEffect(() => { - if (embeddedSessionChat) { - return; - } - - if (!isDesktopShell() || !isDesktopLocalOriginActive()) { + if (!isDesktopRuntime || bootInjectionStatus !== 'not-injected') { return; } let cancelled = false; - const run = async () => { - const res = await fetch('/health', { method: 'GET' }).catch(() => null); - if (!res || !res.ok || cancelled) return; - const data = (await res.json().catch(() => null)) as null | { - openCodeRunning?: unknown; - isOpenCodeReady?: unknown; - opencodeBinaryResolved?: unknown; - lastOpenCodeError?: unknown; - }; - if (!data || cancelled) return; - const openCodeRunning = data.openCodeRunning === true; - const isOpenCodeReady = data.isOpenCodeReady === true; - const resolvedBinary = typeof data.opencodeBinaryResolved === 'string' ? data.opencodeBinaryResolved.trim() : ''; - const hasResolvedBinary = resolvedBinary.length > 0; - const err = typeof data.lastOpenCodeError === 'string' ? data.lastOpenCodeError : ''; - const cliMissing = - !openCodeRunning && - (CLI_MISSING_ERROR_REGEX.test(err) || (!hasResolvedBinary && !isOpenCodeReady)); - setShowCliOnboarding(cliMissing); + let attempts = 0; + const BASE_INTERVAL = 200; + const MAX_INTERVAL = 2000; + const MAX_ATTEMPTS = 50; // 10 seconds total (200ms * 50 with exponential backoff cap) + + const pollWithBackoff = () => { + if (cancelled) return; + + attempts++; + const status = getBootInjectionStatus(); + + if (status !== 'not-injected') { + cancelled = true; + setBootInjectionStatus(status); + + if (status === 'valid') { + const outcome = getInjectedBootOutcome(); + if (outcome) { + setBootView(resolveDesktopBootView({ isDesktopShell: true, bootOutcome: outcome })); + } + } + // If status is 'malformed', we keep the splash visible with error text + // handled by the separate useEffect below + return; + } + + // Exponential backoff with cap + const nextInterval = Math.min(BASE_INTERVAL * Math.pow(1.1, attempts), MAX_INTERVAL); + + if (attempts >= MAX_ATTEMPTS) { + // Max attempts reached - keep polling but show error + const loadingElement = document.getElementById('initial-loading'); + if (loadingElement && !loadingElement.textContent?.includes('taking longer')) { + loadingElement.textContent = 'Desktop startup is taking longer than expected...'; + } + } + + window.setTimeout(pollWithBackoff, nextInterval); }; - void run(); - const interval = window.setInterval(() => { - void run(); - }, CLI_ONBOARDING_HEALTH_POLL_MS); + // Start polling + window.setTimeout(pollWithBackoff, BASE_INTERVAL); return () => { cancelled = true; - window.clearInterval(interval); }; - }, [embeddedSessionChat]); + }, [isDesktopRuntime, bootInjectionStatus]); + + const handleDesktopBootDismiss = React.useCallback(async () => { + if (shouldRestartDesktopBootFlow({ + isTauriShell: isTauriShell(), + isDesktopLocalOriginActive: isDesktopLocalOriginActive(), + })) { + await restartDesktopApp(); + return; + } - const handleCliAvailable = React.useCallback(() => { - setShowCliOnboarding(false); window.location.reload(); }, []); - if (showCliOnboarding) { + const handleManualInitRetry = React.useCallback(async () => { + if (manualInitRetrying || initializationInFlightRef.current) return; + + setInitRetryExhausted(false); + setManualInitRetrying(true); + initializationInFlightRef.current = true; + try { + await useConfigStore.getState().initializeApp(); + } finally { + initializationInFlightRef.current = false; + setManualInitRetrying(false); + } + + if (!useConfigStore.getState().isInitialized) { + setInitRetryEpoch((value) => value + 1); + } + }, [manualInitRetrying]); + + // Map boot outcome kind to recovery variant + const mapBootViewToRecoveryVariant = (view: DesktopBootView): RecoveryVariant | undefined => { + if (view.screen === 'recovery') { + return view.variant; + } + return undefined; + }; + + // Desktop boot view routing. + // When the boot outcome resolves to a non-main screen (chooser, recovery), + // render OnboardingScreen with appropriate mode/variant. + if (isDesktopRuntime && bootView && bootView.screen !== 'main') { + // First-launch chooser + if (bootView.screen === 'chooser') { + return ( + +
    + }> + { + // Switch to remote tab - handled internally by OnboardingScreen + }} + /> + +
    +
    + ); + } + + // Recovery screens + const recoveryVariant = mapBootViewToRecoveryVariant(bootView); + const hostUrl = bootView.screen === 'recovery' && 'url' in bootView ? bootView.url : undefined; + return (
    - + }> + +
    ); @@ -478,14 +819,37 @@ function App({ apis }: AppProps) { if (embeddedSessionChat) { return ( - - -
    - - -
    -
    -
    + + + +
    + + + + +
    +
    +
    +
    +
    + ); + } + + if (isMcpOAuthCallback) { + return ( + + + + ); + } + + if (initRetryExhausted && !isInitialized && !isVSCodeRuntime && !embeddedSessionChat) { + return ( + + { void handleManualInitRetry(); }} + isRetrying={manualInitRetrying} + /> ); } @@ -493,62 +857,79 @@ function App({ apis }: AppProps) { // VS Code runtime - simplified layout without git/terminal views if (isVSCodeRuntime) { // Check if this is the Agent Manager panel - const panelType = typeof window !== 'undefined' - ? (window as { __OPENCHAMBER_PANEL_TYPE__?: 'chat' | 'agentManager' }).__OPENCHAMBER_PANEL_TYPE__ + const panelType = typeof window !== 'undefined' + ? (window as { __OPENCHAMBER_PANEL_TYPE__?: 'chat' | 'agentManager' }).__OPENCHAMBER_PANEL_TYPE__ : 'chat'; - + if (panelType === 'agentManager') { return ( - - -
    - - -
    -
    -
    -
    - ); - } - - return ( - - - + +
    - + +
    -
    -
    + + +
    + ); + } + + return ( + + + + + +
    + + + +
    +
    +
    +
    +
    ); } + // Always mount the full provider tree to avoid remounts when isInitialized + // flips from false → true. FireworksProvider and VoiceProvider are lightweight + // shells; their heavy children are only activated when actually needed. + const isBootShell = !isInitialized && !isDesktopRuntime; + return ( - - + +
    + - - - {showMemoryDebug && ( - setShowMemoryDebug(false)} /> + {!isBootShell && ( + <> + + + + {showMemoryDebug && ( + setShowMemoryDebug(false)} /> + )} + )}
    -
    -
    + +
    ); } diff --git a/src/packages/ui/src/assets/icons/file-types/json.svg b/src/packages/ui/src/assets/icons/file-types/json.svg index 2590b94..9200a60 100644 --- a/src/packages/ui/src/assets/icons/file-types/json.svg +++ b/src/packages/ui/src/assets/icons/file-types/json.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/packages/ui/src/assets/icons/file-types/sprite.svg b/src/packages/ui/src/assets/icons/file-types/sprite.svg index c46adad..fcfbfb0 100644 --- a/src/packages/ui/src/assets/icons/file-types/sprite.svg +++ b/src/packages/ui/src/assets/icons/file-types/sprite.svg @@ -4533,10 +4533,10 @@ - - + + - + diff --git a/src/packages/ui/src/components/auth/SessionAuthGate.tsx b/src/packages/ui/src/components/auth/SessionAuthGate.tsx index 4fcbcad..779a8f5 100644 --- a/src/packages/ui/src/components/auth/SessionAuthGate.tsx +++ b/src/packages/ui/src/components/auth/SessionAuthGate.tsx @@ -1,14 +1,28 @@ import React from 'react'; import { RiLockLine, RiLockUnlockLine, RiLoader4Line } from '@remixicon/react'; +import { browserSupportsWebAuthn } from '@simplewebauthn/browser'; import { Button } from '@/components/ui/button'; +import { Checkbox } from '@/components/ui/checkbox'; import { Input } from '@/components/ui/input'; +import { toast } from '@/components/ui'; import { isDesktopShell, isVSCodeRuntime } from '@/lib/desktop'; import { syncDesktopSettings, initializeAppearancePreferences } from '@/lib/persistence'; import { applyPersistedDirectoryPreferences } from '@/lib/directoryPersistence'; import { DesktopHostSwitcherInline } from '@/components/desktop/DesktopHostSwitcher'; import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo'; +import { useI18n } from '@/lib/i18n'; +import { + authenticateWithPasskey, + cancelPasskeyCeremony, + defaultPasskeyStatus, + fetchPasskeyStatus, + isPasskeyCeremonyAbort, + type PasskeyStatus, + registerCurrentDevicePasskey, +} from '@/lib/passkeys'; const STATUS_CHECK_ENDPOINT = '/auth/session'; +const TRUST_DEVICE_STORAGE_KEY = 'openchamber.uiAuth.trustDevice'; const fetchSessionStatus = async (): Promise => { console.log('[Frontend Auth] Checking session status...'); @@ -23,7 +37,14 @@ const fetchSessionStatus = async (): Promise => { return response; }; -const submitPassword = async (password: string): Promise => { +const readStoredTrustDevice = (): boolean => { + if (typeof window === 'undefined') { + return false; + } + return window.localStorage.getItem(TRUST_DEVICE_STORAGE_KEY) === 'true'; +}; + +const submitPassword = async (password: string, trustDevice: boolean): Promise => { console.log('[Frontend Auth] Submitting password...'); const response = await fetch(STATUS_CHECK_ENDPOINT, { method: 'POST', @@ -32,7 +53,7 @@ const submitPassword = async (password: string): Promise => { 'Content-Type': 'application/json', Accept: 'application/json', }, - body: JSON.stringify({ password }), + body: JSON.stringify({ password, trustDevice }), }); console.log('[Frontend Auth] Password submit response:', response.status, response.statusText); return response; @@ -64,11 +85,12 @@ const AuthShell: React.FC<{ children: React.ReactNode }> = ({ children }) => ( const LoadingScreen: React.FC = () => (
    - +
    ); const ErrorScreen: React.FC = ({ onRetry, errorType = 'network', retryAfter }) => { + const { t } = useI18n(); const isRateLimit = errorType === 'rate-limit'; const minutes = retryAfter ? Math.ceil(retryAfter / 60) : 1; @@ -77,16 +99,18 @@ const ErrorScreen: React.FC = ({ onRetry, errorType = 'network

    - {isRateLimit ? 'Too many attempts' : 'Unable to reach server'} + {isRateLimit ? t('sessionAuth.error.rateLimitTitle') : t('sessionAuth.error.networkTitle')}

    {isRateLimit - ? `Please wait ${minutes} minute${minutes > 1 ? 's' : ''} before trying again.` - : "We couldn't verify the UI session. Check that the service is running and try again."} + ? (minutes > 1 + ? t('sessionAuth.error.rateLimitDescriptionPlural', { minutes }) + : t('sessionAuth.error.rateLimitDescriptionSingle', { minutes })) + : t('sessionAuth.error.networkDescription')}

    @@ -106,6 +130,7 @@ interface ErrorScreenProps { } export const SessionAuthGate: React.FC = ({ children }) => { + const { t } = useI18n(); const vscodeRuntime = React.useMemo(() => isVSCodeRuntime(), []); const skipAuth = vscodeRuntime; const showHostSwitcher = React.useMemo(() => isDesktopShell() && !vscodeRuntime, [vscodeRuntime]); @@ -115,9 +140,66 @@ export const SessionAuthGate: React.FC = ({ children }) => const [errorMessage, setErrorMessage] = React.useState(''); const [retryAfter, setRetryAfter] = React.useState(undefined); const [isTunnelLocked, setIsTunnelLocked] = React.useState(false); + const [passkeyStatus, setPasskeyStatus] = React.useState(defaultPasskeyStatus); + const [supportsPasskeys, setSupportsPasskeys] = React.useState(false); + const [isPasskeyBusy, setIsPasskeyBusy] = React.useState(false); + const [trustDevice, setTrustDevice] = React.useState(() => readStoredTrustDevice()); + const [activePasskeyAction, setActivePasskeyAction] = React.useState<'auth' | 'register' | null>(null); const passwordInputRef = React.useRef(null); const hasResyncedRef = React.useRef(skipAuth); + React.useEffect(() => { + if (typeof window === 'undefined') { + return; + } + window.localStorage.setItem(TRUST_DEVICE_STORAGE_KEY, trustDevice ? 'true' : 'false'); + }, [trustDevice]); + + const refreshPasskeyStatus = React.useCallback(async () => { + if (skipAuth) { + return defaultPasskeyStatus; + } + + try { + const nextStatus = await fetchPasskeyStatus(); + setPasskeyStatus(nextStatus); + return nextStatus; + } catch { + setPasskeyStatus(defaultPasskeyStatus); + return defaultPasskeyStatus; + } + }, [skipAuth]); + + React.useEffect(() => { + let cancelled = false; + + if (skipAuth) { + return; + } + + void (async () => { + try { + if (!window.isSecureContext || !browserSupportsWebAuthn()) { + if (!cancelled) { + setSupportsPasskeys(false); + } + return; + } + if (!cancelled) { + setSupportsPasskeys(true); + } + } catch { + if (!cancelled) { + setSupportsPasskeys(false); + } + } + })(); + + return () => { + cancelled = true; + }; + }, [skipAuth]); + const checkStatus = React.useCallback(async () => { if (skipAuth) { console.log('[Frontend Auth] VSCode runtime, skipping auth'); @@ -125,16 +207,12 @@ export const SessionAuthGate: React.FC = ({ children }) => return; } - // 检查 cookie 是否存在 - const cookies = document.cookie; - const hasAccessToken = cookies.includes('oc_ui_session='); - const hasRefreshToken = cookies.includes('oc_ui_refresh='); - console.log('[Frontend Auth] Cookies check - access:', hasAccessToken, 'refresh:', hasRefreshToken); - console.log('[Frontend Auth] All cookies:', cookies.split(';').map(c => c.trim().split('=')[0])); - setState((prev) => (prev === 'authenticated' ? prev : 'pending')); try { - const response = await fetchSessionStatus(); + const [response, latestPasskeyStatus] = await Promise.all([ + fetchSessionStatus(), + refreshPasskeyStatus(), + ]); const responseText = await response.text(); console.log('[Frontend Auth] Raw response:', response.status, responseText); @@ -158,6 +236,7 @@ export const SessionAuthGate: React.FC = ({ children }) => console.warn('[Frontend Auth] Debug info:', data.debug); } setIsTunnelLocked(data.tunnelLocked === true); + setPasskeyStatus(latestPasskeyStatus); setState('locked'); setRetryAfter(undefined); return; @@ -182,7 +261,7 @@ export const SessionAuthGate: React.FC = ({ children }) => setState('error'); setIsTunnelLocked(false); } - }, [skipAuth]); + }, [refreshPasskeyStatus, skipAuth]); React.useEffect(() => { if (skipAuth) { @@ -220,6 +299,28 @@ export const SessionAuthGate: React.FC = ({ children }) => const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); + await handlePasswordUnlock(false); + }; + + const registerPasskeyForCurrentSession = React.useCallback(async () => { + setActivePasskeyAction('register'); + setIsPasskeyBusy(true); + try { + await registerCurrentDevicePasskey(); + } finally { + setActivePasskeyAction(null); + setIsPasskeyBusy(false); + } + await refreshPasskeyStatus(); + }, [refreshPasskeyStatus]); + + const cancelActivePasskey = React.useCallback(() => { + cancelPasskeyCeremony(); + setActivePasskeyAction(null); + setIsPasskeyBusy(false); + }, []); + + const handlePasswordUnlock = React.useCallback(async (enrollPasskey: boolean) => { if (isTunnelLocked) { return; } @@ -227,28 +328,43 @@ export const SessionAuthGate: React.FC = ({ children }) => return; } + if (isPasskeyBusy) { + cancelActivePasskey(); + } + setIsSubmitting(true); setErrorMessage(''); try { - const response = await submitPassword(password); + const response = await submitPassword(password, trustDevice); if (response.ok) { console.log('[Frontend Auth] Login successful'); - // 检查登录后 cookie 是否被设置 - const cookies = document.cookie; - const hasAccessToken = cookies.includes('oc_ui_session='); - const hasRefreshToken = cookies.includes('oc_ui_refresh='); - console.log('[Frontend Auth] After login - access:', hasAccessToken, 'refresh:', hasRefreshToken); - console.log('[Frontend Auth] All cookies after login:', cookies.split(';').map(c => c.trim().split('=')[0]).filter(Boolean)); setPassword(''); setIsTunnelLocked(false); + if (enrollPasskey && supportsPasskeys) { + try { + await registerPasskeyForCurrentSession(); + toast.success(t('sessionAuth.toast.passkeyAdded')); + setState('authenticated'); + return; + } catch (error) { + if (isPasskeyCeremonyAbort(error)) { + toast.message(t('sessionAuth.toast.passkeySetupCanceled')); + } else { + const message = error instanceof Error ? error.message : t('sessionAuth.error.passkeySetupFailed'); + toast.error(message); + } + setState('authenticated'); + return; + } + } setState('authenticated'); return; } if (response.status === 401) { console.warn('[Frontend Auth] Login failed: Invalid password'); - setErrorMessage('Incorrect password. Try again.'); + setErrorMessage(t('sessionAuth.error.incorrectPassword')); setIsTunnelLocked(false); setState('locked'); return; @@ -264,18 +380,86 @@ export const SessionAuthGate: React.FC = ({ children }) => } console.error('[Frontend Auth] Login failed: Unexpected response', response.status); - setErrorMessage('Unexpected response from server.'); + setErrorMessage(t('sessionAuth.error.unexpectedResponse')); setIsTunnelLocked(false); setState('error'); } catch (error) { console.warn('Failed to submit UI password:', error); - setErrorMessage('Network error. Check connection and retry.'); + setErrorMessage(t('sessionAuth.error.networkRetry')); setIsTunnelLocked(false); setState('error'); } finally { setIsSubmitting(false); } - }; + }, [cancelActivePasskey, isPasskeyBusy, isSubmitting, isTunnelLocked, password, registerPasskeyForCurrentSession, supportsPasskeys, t, trustDevice]); + + const handlePasskeyUnlock = React.useCallback(async () => { + if (isSubmitting || !supportsPasskeys) { + return; + } + + if (isPasskeyBusy) { + cancelActivePasskey(); + return; + } + + setIsPasskeyBusy(true); + setActivePasskeyAction('auth'); + setErrorMessage(''); + + try { + await authenticateWithPasskey(trustDevice); + + setPassword(''); + setState('authenticated'); + } catch (error) { + if (isPasskeyCeremonyAbort(error)) { + setErrorMessage(''); + } else { + const message = error instanceof Error ? error.message : t('sessionAuth.error.passkeySignInCanceled'); + setErrorMessage(message); + } + } finally { + setActivePasskeyAction(null); + setIsPasskeyBusy(false); + } + }, [cancelActivePasskey, isPasskeyBusy, isSubmitting, supportsPasskeys, t, trustDevice]); + + const handlePasskeySetupOnly = React.useCallback(async () => { + if (isSubmitting || isTunnelLocked || !supportsPasskeys) { + return; + } + + if (isPasskeyBusy) { + cancelActivePasskey(); + return; + } + + if (state !== 'authenticated') { + if (!password) { + setErrorMessage(t('sessionAuth.error.enterPasswordForPasskey')); + return; + } + await handlePasswordUnlock(true); + return; + } + + setErrorMessage(''); + try { + await registerPasskeyForCurrentSession(); + toast.success(t('sessionAuth.toast.passkeyAdded')); + } catch (error) { + if (isPasskeyCeremonyAbort(error)) { + toast.message(t('sessionAuth.toast.passkeySetupCanceled')); + return; + } + const message = error instanceof Error ? error.message : t('sessionAuth.error.passkeySetupFailed'); + toast.error(message); + } + }, [cancelActivePasskey, handlePasswordUnlock, isPasskeyBusy, isSubmitting, isTunnelLocked, password, registerPasskeyForCurrentSession, state, supportsPasskeys, t]); + + const canOfferPasskeySetup = supportsPasskeys && passkeyStatus.enabled; + const canUsePasskey = canOfferPasskeySetup && passkeyStatus.hasPasskeys; if (state === 'pending') { return ; @@ -295,17 +479,35 @@ export const SessionAuthGate: React.FC = ({ children }) =>

    - {isTunnelLocked ? 'Tunnel access required' : 'Unlock OpenChamber'} + {isTunnelLocked ? t('sessionAuth.locked.tunnelTitle') : t('sessionAuth.locked.unlockTitle')}

    {isTunnelLocked - ? 'Open this tunnel using the one-time connect link from the desktop app.' - : 'This session is password-protected.'} + ? t('sessionAuth.locked.tunnelDescription') + : t('sessionAuth.locked.passwordDescription')}

    {!isTunnelLocked && ( -
    + + {canUsePasskey && ( + + )}
    @@ -314,7 +516,7 @@ export const SessionAuthGate: React.FC = ({ children }) => ref={passwordInputRef} type="password" autoComplete="current-password" - placeholder="Enter password" + placeholder={t('sessionAuth.password.placeholder')} value={password} onChange={(event) => { setPassword(event.target.value); @@ -332,7 +534,7 @@ export const SessionAuthGate: React.FC = ({ children }) => type="submit" size="icon" disabled={!password || isSubmitting} - aria-label={isSubmitting ? 'Unlocking' : 'Unlock'} + aria-label={isSubmitting ? t('sessionAuth.actions.unlockingAria') : t('sessionAuth.actions.unlockAria')} > {isSubmitting ? ( @@ -341,6 +543,45 @@ export const SessionAuthGate: React.FC = ({ children }) => )}
    + {canOfferPasskeySetup ? ( +
    + + +
    + ) : ( + + )} {errorMessage && (

    {errorMessage} @@ -353,7 +594,7 @@ export const SessionAuthGate: React.FC = ({ children }) =>

    - Use Local if remote is unreachable. + {t('sessionAuth.locked.hostSwitcherHint')}

    )} diff --git a/src/packages/ui/src/components/chat/AgentMentionAutocomplete.tsx b/src/packages/ui/src/components/chat/AgentMentionAutocomplete.tsx index 3a1aa39..98cc461 100644 --- a/src/packages/ui/src/components/chat/AgentMentionAutocomplete.tsx +++ b/src/packages/ui/src/components/chat/AgentMentionAutocomplete.tsx @@ -3,6 +3,7 @@ import { cn, fuzzyMatch } from '@/lib/utils'; import { useConfigStore } from '@/stores/useConfigStore'; import { useAgentsStore, isAgentBuiltIn, type AgentWithExtras } from '@/stores/useAgentsStore'; import { ScrollableOverlay } from '@/components/ui/ScrollableOverlay'; +import { useI18n } from '@/lib/i18n'; interface AgentInfo { name: string; @@ -40,13 +41,15 @@ export const AgentMentionAutocomplete = React.forwardRef { + const { t } = useI18n(); const containerRef = React.useRef(null); const [selectedIndex, setSelectedIndex] = React.useState(0); const [agents, setAgents] = React.useState([]); const itemRefs = React.useRef<(HTMLDivElement | null)[]>([]); const ignoreTabClickRef = React.useRef(false); - const { getVisibleAgents } = useConfigStore(); - const { agents: agentsWithMetadata, loadAgents } = useAgentsStore(); + const getVisibleAgents = useConfigStore((state) => state.getVisibleAgents); + const agentsWithMetadata = useAgentsStore((state) => state.agents); + const loadAgents = useAgentsStore((state) => state.loadAgents); React.useEffect(() => { if (agentsWithMetadata.length === 0) { @@ -156,7 +159,7 @@ export const AgentMentionAutocomplete = React.forwardRef#{agent.name} {isSystem ? ( - system + {t('chat.agentMentionAutocomplete.badge.system')} ) : agent.scope ? ( ([ + { id: 'commands' as const, label: t('chat.autocomplete.tabs.commands') }, + { id: 'agents' as const, label: t('chat.autocomplete.tabs.agents') }, + { id: 'files' as const, label: t('chat.autocomplete.tabs.files') }, + ]), [t]); + return (
    - {([ - { id: 'commands' as const, label: 'Commands' }, - { id: 'agents' as const, label: 'Agents' }, - { id: 'files' as const, label: 'Files' }, - ]).map((tab) => ( + {tabs.map((tab) => (
    ); diff --git a/src/packages/ui/src/components/chat/ChangedFilesList.tsx b/src/packages/ui/src/components/chat/ChangedFilesList.tsx new file mode 100644 index 0000000..bc8f670 --- /dev/null +++ b/src/packages/ui/src/components/chat/ChangedFilesList.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { FileTypeIcon } from '@/components/icons/FileTypeIcon'; +import { type ChangedFileEntry, getDisplayPath, getFileStats } from './changedFiles'; +import { useI18n } from '@/lib/i18n'; + +interface ChangedFilesListProps { + files: ChangedFileEntry[]; + currentDirectory: string; + onOpenFile: (file: ChangedFileEntry) => void; +} + +export const ChangedFilesList: React.FC = ({ files, currentDirectory, onOpenFile }) => { + const { t } = useI18n(); + return ( + <> +
    + {t('chat.changedFiles.title')} + {files.length} +
    + +
    + {files.map((file, index) => { + const { fileName, dirPart } = getDisplayPath(file, currentDirectory); + const stats = getFileStats(file); + + return ( + + ); + })} +
    + + ); +}; diff --git a/src/packages/ui/src/components/chat/ChatContainer.tsx b/src/packages/ui/src/components/chat/ChatContainer.tsx index d072445..ff3401a 100644 --- a/src/packages/ui/src/components/chat/ChatContainer.tsx +++ b/src/packages/ui/src/components/chat/ChatContainer.tsx @@ -1,23 +1,23 @@ import React from 'react'; import { RiArrowLeftLine } from '@remixicon/react'; -import { useShallow } from 'zustand/react/shallow'; -import type { Message, Part } from '@opencode-ai/sdk/v2'; +import type { Message, Part, Session } from '@opencode-ai/sdk/v2'; import { ChatInput } from './ChatInput'; -import { useSessionStore } from '@/stores/useSessionStore'; import { useUIStore } from '@/stores/useUIStore'; import { Skeleton } from '@/components/ui/skeleton'; import ChatEmptyState from './ChatEmptyState'; import MessageList, { type MessageListHandle } from './MessageList'; +import { PermissionCard } from './PermissionCard'; +import { QuestionCard } from './QuestionCard'; +import { StatusRowContainer } from './StatusRowContainer'; import ScrollToBottomButton from './components/ScrollToBottomButton'; import { ScrollShadow } from '@/components/ui/ScrollShadow'; -import { useChatScrollManager } from '@/hooks/useChatScrollManager'; +import { useChatScrollManager, type AnimationHandlers, type ContentChangeReason } from '@/hooks/useChatScrollManager'; import { useChatTimelineController } from './hooks/useChatTimelineController'; import { useChatTurnNavigation } from './hooks/useChatTurnNavigation'; import { useDeviceInfo } from '@/lib/device'; import { Button } from '@/components/ui/button'; import { OverlayScrollbar } from '@/components/ui/OverlayScrollbar'; -import { TimelineDialog } from './TimelineDialog'; import type { PermissionRequest } from '@/types/permission'; import type { QuestionRequest } from '@/types/question'; import { cn } from '@/lib/utils'; @@ -26,11 +26,99 @@ import { flattenBlockingRequests, } from './lib/blockingRequests'; +// New sync system imports +import { useSessionUIStore } from '@/sync/session-ui-store'; +import { useViewportStore } from '@/sync/viewport-store'; +import { useStreamingStore } from '@/sync/streaming'; +import { + useSessionMessageCount, + useSessionMessageRecords, + useSessions, + useDirectorySync, + useSessionStatus, +} from '@/sync/sync-context'; +import { useSync } from '@/sync/use-sync'; +import { usePlanDetection } from '@/hooks/usePlanDetection'; +import { getAllSyncSessions } from '@/sync/sync-refs'; +import { useI18n } from '@/lib/i18n'; + const EMPTY_MESSAGES: Array<{ info: Message; parts: Part[] }> = []; const EMPTY_PERMISSIONS: PermissionRequest[] = []; const EMPTY_QUESTIONS: QuestionRequest[] = []; const IDLE_SESSION_STATUS = { type: 'idle' as const }; const SESSION_RESELECTED_EVENT = 'openchamber:session-reselected'; +const DEFAULT_RETRY_MESSAGE = 'Quota limit reached. Retrying automatically.'; +const CHAT_SCROLL_STYLE = { + overflowAnchor: 'none', + overscrollBehavior: 'contain', + overscrollBehaviorY: 'contain', +} as const; +const CHAT_NAVIGATION_IGNORED_TARGET_SELECTOR = [ + 'a[href]', + 'button', + 'input', + 'select', + 'textarea', + '[contenteditable="true"]', + '[role="button"]', + '[role="combobox"]', + '[role="dialog"]', + '[role="listbox"]', + '[role="menu"]', + '[role="menuitem"]', + '[role="option"]', + '[role="textbox"]', + '[data-radix-popper-content-wrapper]', +].join(','); +type SessionMessageRecord = { info: Message; parts: Part[] }; + +const isHTMLElement = (target: EventTarget | null): target is HTMLElement => { + return target instanceof HTMLElement; +}; + +const shouldIgnoreChatNavigationTarget = (target: EventTarget | null): boolean => { + if (!isHTMLElement(target)) { + return false; + } + + return Boolean(target.closest(CHAT_NAVIGATION_IGNORED_TARGET_SELECTOR)); +}; + +const shouldIgnoreChatNavigationForFocus = (activeElement: Element | null, scrollContainer: HTMLElement | null): boolean => { + if (typeof document === 'undefined') { + return true; + } + + if (!activeElement || activeElement === document.body || activeElement === document.documentElement) { + return true; + } + + if (shouldIgnoreChatNavigationTarget(activeElement)) { + return true; + } + + return !scrollContainer?.contains(activeElement); +}; + +const hasBlockingChatOverlay = (): boolean => { + const { + isAboutDialogOpen, + isCommandPaletteOpen, + isHelpDialogOpen, + isImagePreviewOpen, + isMultiRunLauncherOpen, + isSessionSwitcherOpen, + isSettingsDialogOpen, + } = useUIStore.getState(); + + return isAboutDialogOpen + || isCommandPaletteOpen + || isHelpDialogOpen + || isImagePreviewOpen + || isMultiRunLauncherOpen + || isSessionSwitcherOpen + || isSettingsDialogOpen; +}; type HydratingToolSkeletonRow = { id: string; @@ -38,6 +126,162 @@ type HydratingToolSkeletonRow = { detailWidth: string; }; +type ChatViewportProps = { + currentSessionId: string; + isDesktopExpandedInput: boolean; + isMobile: boolean; + stickyUserHeader: boolean; + scrollRef: React.RefObject; + messageListRef: React.RefObject; + turnStart: number; + pendingRevealWork: boolean; + renderedMessages: SessionMessageRecord[]; + hasMoreAboveTurns: boolean; + isLoadingOlder: boolean; + sessionIsWorking: boolean; + streamingMessageId: string | null; + activeStreamingPhase: import('./message/types').StreamPhase | null; + retryOverlay: { + sessionId: string; + message: string; + confirmedAt?: number; + fallbackTimestamp?: number; + } | null; + handleMessageContentChange: (reason?: ContentChangeReason) => void; + getAnimationHandlers: (messageId: string) => AnimationHandlers; + handleLoadOlder: () => void; + scrollToBottom: (options?: { instant?: boolean; force?: boolean }) => void; + sessionQuestions: QuestionRequest[]; + sessionPermissions: PermissionRequest[]; + isProgrammaticFollowActive: boolean; +}; + +const ChatViewport = React.memo(({ + currentSessionId, + isDesktopExpandedInput, + isMobile, + stickyUserHeader, + scrollRef, + messageListRef, + turnStart, + pendingRevealWork, + renderedMessages, + hasMoreAboveTurns, + isLoadingOlder, + sessionIsWorking, + streamingMessageId, + activeStreamingPhase, + retryOverlay, + handleMessageContentChange, + getAnimationHandlers, + handleLoadOlder, + scrollToBottom, + sessionQuestions, + sessionPermissions, + isProgrammaticFollowActive, +}: ChatViewportProps) => { + const focusScrollContainer = React.useCallback((event: React.MouseEvent) => { + if (event.defaultPrevented || shouldIgnoreChatNavigationTarget(event.target)) { + return; + } + + if (typeof window !== 'undefined' && window.getSelection()?.type === 'Range') { + return; + } + + scrollRef.current?.focus({ preventScroll: true }); + }, [scrollRef]); + + return ( +
    +
    + +
    + + {(sessionQuestions.length > 0 || sessionPermissions.length > 0) && ( +
    + {sessionQuestions.map((question) => ( + + ))} + {sessionPermissions.map((permission) => ( + + ))} +
    + )} + +
    + +
    + + + + +
    +
    + ); +}, (prev, next) => { + return prev.currentSessionId === next.currentSessionId + && prev.isDesktopExpandedInput === next.isDesktopExpandedInput + && prev.isMobile === next.isMobile + && prev.stickyUserHeader === next.stickyUserHeader + && prev.scrollRef === next.scrollRef + && prev.messageListRef === next.messageListRef + && prev.turnStart === next.turnStart + && prev.pendingRevealWork === next.pendingRevealWork + && prev.renderedMessages === next.renderedMessages + && prev.hasMoreAboveTurns === next.hasMoreAboveTurns + && prev.isLoadingOlder === next.isLoadingOlder + && prev.sessionIsWorking === next.sessionIsWorking + && prev.streamingMessageId === next.streamingMessageId + && prev.activeStreamingPhase === next.activeStreamingPhase + && prev.retryOverlay === next.retryOverlay + && prev.handleMessageContentChange === next.handleMessageContentChange + && prev.getAnimationHandlers === next.getAnimationHandlers + && prev.handleLoadOlder === next.handleLoadOlder + && prev.scrollToBottom === next.scrollToBottom + && prev.sessionQuestions === next.sessionQuestions + && prev.sessionPermissions === next.sessionPermissions + && prev.isProgrammaticFollowActive === next.isProgrammaticFollowActive; +}); + +ChatViewport.displayName = 'ChatViewport'; + const HYDRATING_SKELETON_ITEMS: Array<{ id: number; toolRows: HydratingToolSkeletonRow[]; @@ -71,101 +315,179 @@ const HYDRATING_SKELETON_ITEMS: Array<{ ]; export const ChatContainer: React.FC = () => { - const { - currentSessionId, - loadMessages, - loadMoreMessages, - updateViewportAnchor, - openNewSessionDraft, - setCurrentSession, - newSessionDraft, - } = useSessionStore( - useShallow((state) => ({ - currentSessionId: state.currentSessionId, - loadMessages: state.loadMessages, - loadMoreMessages: state.loadMoreMessages, - updateViewportAnchor: state.updateViewportAnchor, - openNewSessionDraft: state.openNewSessionDraft, - setCurrentSession: state.setCurrentSession, - newSessionDraft: state.newSessionDraft, - })) + const { t } = useI18n(); + // Session UI state + const currentSessionId = useSessionUIStore((s) => s.currentSessionId); + const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft); + const setCurrentSession = useSessionUIStore((s) => s.setCurrentSession); + const newSessionDraft = useSessionUIStore((s) => s.newSessionDraft); + const updateViewportAnchor = useViewportStore((s) => s.updateViewportAnchor); + const isSyncing = useViewportStore((s) => s.isSyncing); + const sessionMemoryStateMap = useViewportStore((s) => s.sessionMemoryState); + + // Sync actions + const sync = useSync(); + const loadMessages = React.useCallback( + (sessionId: string) => sync.syncSession(sessionId), + [sync], + ); + const loadMoreMessages = React.useCallback( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + (sessionId: string, _direction: 'up' | 'down') => sync.loadMore(sessionId), + [sync], ); - const { isSyncing, messageStreamStates, sessionMemoryStateMap } = useSessionStore( - useShallow((state) => ({ - isSyncing: state.isSyncing, - messageStreamStates: state.messageStreamStates, - sessionMemoryStateMap: state.sessionMemoryState, - })) - ); + // UI store + const isExpandedInput = useUIStore((state) => state.isExpandedInput); + const stickyUserHeader = useUIStore((state) => state.stickyUserHeader); + const chatRenderMode = useUIStore((state) => state.chatRenderMode); - const { - isTimelineDialogOpen, - setTimelineDialogOpen, - isExpandedInput, - stickyUserHeader, - chatRenderMode, - } = useUIStore(); - - const sessionMessages = useSessionStore( + // Streaming state + const streamingMessageId = useStreamingStore( React.useCallback( - (state) => (currentSessionId ? state.messages.get(currentSessionId) ?? EMPTY_MESSAGES : EMPTY_MESSAGES), - [currentSessionId] - ) + (s) => (currentSessionId ? s.streamingMessageIds.get(currentSessionId) ?? null : null), + [currentSessionId], + ), + ); + const activeStreamingPhase = useStreamingStore( + React.useCallback( + (s) => { + if (!streamingMessageId) return null; + return s.messageStreamStates.get(streamingMessageId)?.phase ?? null; + }, + [streamingMessageId], + ), + ); + const sessionMessageCount = useSessionMessageCount(currentSessionId ?? ''); + const hasLoadedSessionMessages = useDirectorySync( + React.useCallback( + (state) => (currentSessionId ? state.message[currentSessionId] !== undefined : false), + [currentSessionId], + ), + ); + // Messages from sync system + const sessionMessageRecords = useSessionMessageRecords(currentSessionId ?? ''); + const sessionMessages = currentSessionId ? sessionMessageRecords : EMPTY_MESSAGES; + + // Sessions from sync system + const sessions = useSessions(); + + // Plan detection - watches messages for plan creation and signals store + usePlanDetection(currentSessionId ?? ''); + + // Session status from sync system + const sessionStatusForCurrent = useSessionStatus(currentSessionId ?? '') ?? IDLE_SESSION_STATUS; + + // Permissions & questions from sync system + const allPermissions = useDirectorySync( + React.useCallback((s) => s.permission ?? {}, []), + ); + const allQuestions = useDirectorySync( + React.useCallback((s) => s.question ?? {}, []), ); - const sessions = useSessionStore((state) => state.sessions); + // Convert Record → Map for blockingRequests helpers + const permissionsMap = React.useMemo(() => { + const m = new Map(); + for (const [k, v] of Object.entries(allPermissions)) m.set(k, v as PermissionRequest[]); + return m; + }, [allPermissions]); - const blockingRequestState = useSessionStore( - useShallow((state) => ({ - sessions: state.sessions, - permissions: state.permissions, - questions: state.questions, - })) - ); + const questionsMap = React.useMemo(() => { + const m = new Map(); + for (const [k, v] of Object.entries(allQuestions)) m.set(k, v as QuestionRequest[]); + return m; + }, [allQuestions]); const scopedSessionIds = React.useMemo( () => collectVisibleSessionIdsForBlockingRequests( - blockingRequestState.sessions.map((session) => ({ id: session.id, parentID: session.parentID })), + sessions.map((session) => ({ id: session.id, parentID: session.parentID })), currentSessionId, ), - [blockingRequestState.sessions, currentSessionId] + [sessions, currentSessionId], ); const sessionPermissions = React.useMemo(() => { if (scopedSessionIds.length === 0) return EMPTY_PERMISSIONS; - return flattenBlockingRequests(blockingRequestState.permissions, scopedSessionIds); - }, [blockingRequestState.permissions, scopedSessionIds]); + return flattenBlockingRequests(permissionsMap, scopedSessionIds); + }, [permissionsMap, scopedSessionIds]); const sessionQuestions = React.useMemo(() => { if (scopedSessionIds.length === 0) return EMPTY_QUESTIONS; - return flattenBlockingRequests(blockingRequestState.questions, scopedSessionIds); - }, [blockingRequestState.questions, scopedSessionIds]); + return flattenBlockingRequests(questionsMap, scopedSessionIds); + }, [questionsMap, scopedSessionIds]); + const sessionIsWorking = React.useMemo(() => { + if (!currentSessionId || sessionPermissions.length > 0 || sessionQuestions.length > 0) { + return false; + } - const historyMeta = useSessionStore( - React.useCallback( - (state) => (currentSessionId ? state.sessionHistoryMeta.get(currentSessionId) ?? null : null), - [currentSessionId] - ) - ); + if (streamingMessageId || activeStreamingPhase) { + return true; + } - const streamingMessageId = useSessionStore( - React.useCallback( - (state) => (currentSessionId ? state.streamingMessageIds.get(currentSessionId) ?? null : null), - [currentSessionId] - ) - ); + const statusType = sessionStatusForCurrent.type ?? 'idle'; + if (statusType === 'busy' || statusType === 'retry') { + return true; + } - const sessionStatusForCurrent = useSessionStore( - React.useCallback( - (state) => (currentSessionId ? state.sessionStatus?.get(currentSessionId) ?? IDLE_SESSION_STATUS : IDLE_SESSION_STATUS), - [currentSessionId] - ) - ); + const lastMessage = sessionMessages[sessionMessages.length - 1]?.info as Message | undefined; + return Boolean( + lastMessage + && lastMessage.role === 'assistant' + && typeof (lastMessage as { time?: { completed?: number } }).time?.completed !== 'number', + ); + }, [activeStreamingPhase, currentSessionId, sessionMessages, sessionPermissions.length, sessionQuestions.length, sessionStatusForCurrent.type, streamingMessageId]); + const activeRetryStatus = React.useMemo(() => { + if (!currentSessionId || sessionStatusForCurrent.type !== 'retry') { + return null; + } - const hasSessionMessagesEntry = useSessionStore( - React.useCallback((state) => (currentSessionId ? state.messages.has(currentSessionId) : false), [currentSessionId]) - ); + const rawMessage = typeof (sessionStatusForCurrent as { message?: string }).message === 'string' + ? (((sessionStatusForCurrent as { message?: string }).message) ?? '').trim() + : ''; + + return { + sessionId: currentSessionId, + message: rawMessage || DEFAULT_RETRY_MESSAGE, + confirmedAt: (sessionStatusForCurrent as { confirmedAt?: number }).confirmedAt, + }; + }, [currentSessionId, sessionStatusForCurrent]); + const [retryFallbackTimestamp, setRetryFallbackTimestamp] = React.useState(0); + const retryFallbackSessionRef = React.useRef(null); + + React.useEffect(() => { + if (!activeRetryStatus || typeof activeRetryStatus.confirmedAt === 'number') { + retryFallbackSessionRef.current = null; + setRetryFallbackTimestamp(0); + return; + } + + if (retryFallbackSessionRef.current !== activeRetryStatus.sessionId) { + retryFallbackSessionRef.current = activeRetryStatus.sessionId; + setRetryFallbackTimestamp(Date.now()); + } + }, [activeRetryStatus]); + + const retryOverlay = React.useMemo(() => { + if (!activeRetryStatus) { + return null; + } + + return { + ...activeRetryStatus, + fallbackTimestamp: retryFallbackTimestamp, + }; + }, [activeRetryStatus, retryFallbackTimestamp]); + + // History metadata — use sync's hasMore/isLoading + const historyMeta = React.useMemo(() => { + if (!currentSessionId) return null; + return { + limit: sessionMessages.length, + complete: !sync.hasMore(currentSessionId), + loading: sync.isLoading(currentSessionId), + }; + }, [currentSessionId, sessionMessages.length, sync]); const { isMobile } = useDeviceInfo(); const draftOpen = Boolean(newSessionDraft?.open); @@ -173,24 +495,19 @@ export const ChatContainer: React.FC = () => { const messageListRef = React.useRef(null); const parentSession = React.useMemo(() => { - if (!currentSessionId) { - return null; - } - + if (!currentSessionId) return null; const current = sessions.find((session) => session.id === currentSessionId); const parentID = current?.parentID; - if (!parentID) { - return null; - } - - return sessions.find((session) => session.id === parentID) ?? null; + if (!parentID) return null; + return sessions.find((session) => session.id === parentID) + ?? getAllSyncSessions().find((session) => session.id === parentID) + ?? null; }, [currentSessionId, sessions]); const handleReturnToParentSession = React.useCallback(() => { - if (!parentSession) { - return; - } - void setCurrentSession(parentSession.id); + if (!parentSession) return; + const parentDirectory = (parentSession as Session & { directory?: string | null }).directory ?? null; + setCurrentSession(parentSession.id, parentDirectory); }, [parentSession, setCurrentSession]); const returnToParentButton = parentSession ? ( @@ -200,11 +517,13 @@ export const ChatContainer: React.FC = () => { size="xs" onClick={handleReturnToParentSession} className="absolute left-3 top-3 z-20 !font-normal bg-[var(--surface-background)]/95" - aria-label="Return to parent session" - title={parentSession.title?.trim() ? `Return to: ${parentSession.title}` : 'Return to parent session'} + aria-label={t('chat.container.returnToParent.aria')} + title={parentSession.title?.trim() + ? t('chat.container.returnToParent.titleNamed', { title: parentSession.title }) + : t('chat.container.returnToParent.title')} > - Parent + {t('chat.container.returnToParent.label')} ) : null; @@ -219,83 +538,146 @@ export const ChatContainer: React.FC = () => { }, [sessionPermissions, sessionQuestions]); const activeTurnChangeRef = React.useRef<(turnId: string | null) => void>(() => {}); + const handleActiveTurnChange = React.useCallback((turnId: string | null) => { + activeTurnChangeRef.current(turnId); + }, []); const { scrollRef, handleMessageContentChange, getAnimationHandlers, + prepareForBottomResume, scrollToBottom, - releasePinnedScroll, isPinned, isOverflowing, isProgrammaticFollowActive, } = useChatScrollManager({ currentSessionId, - sessionMessages, - streamingMessageId, + sessionMessageCount, + sessionIsWorking, sessionMemoryState: sessionMemoryStateMap, updateViewportAnchor, isSyncing, isMobile, chatRenderMode, - messageStreamStates, sessionPermissions: sessionBlockingCards, - onActiveTurnChange: (turnId) => { - activeTurnChangeRef.current(turnId); - }, + onActiveTurnChange: handleActiveTurnChange, }); + const viewportMessages = sessionMessages; + const timelineController = useChatTimelineController({ sessionId: currentSessionId, - messages: sessionMessages, + messages: viewportMessages, historyMeta, scrollRef, messageListRef, loadMoreMessages, + prepareForBottomResume, scrollToBottom, isPinned, isOverflowing, }); - const { resumeToBottomInstant } = timelineController; + const { loadEarlier, resumeToBottomInstant } = timelineController; + + const runLatestInstantResume = React.useCallback(async () => { + if (!currentSessionId) { + scrollToBottom({ instant: true, force: true }); + return; + } + await resumeToBottomInstant(); + }, [currentSessionId, resumeToBottomInstant, scrollToBottom]); + + const resumeToLatestInstant = React.useCallback(() => { + void runLatestInstantResume(); + }, [runLatestInstantResume]); React.useEffect(() => { activeTurnChangeRef.current = timelineController.handleActiveTurnChange; }, [timelineController.handleActiveTurnChange]); + React.useEffect(() => { + if (sessionPermissions.length === 0 && sessionQuestions.length === 0) { + return; + } + handleMessageContentChange('permission'); + }, [handleMessageContentChange, sessionPermissions, sessionQuestions]); + + const handleLoadOlder = React.useCallback(() => { + void loadEarlier(); + }, [loadEarlier]); + const navigation = useChatTurnNavigation({ sessionId: currentSessionId, turnIds: timelineController.turnIds, activeTurnId: timelineController.activeTurnId, scrollToTurn: timelineController.scrollToTurn, scrollToMessage: timelineController.scrollToMessage, - resumeToBottom: timelineController.resumeToBottom, + resumeToBottom: timelineController.resumeToBottomInstant, }); React.useEffect(() => { - if (typeof window === 'undefined' || !currentSessionId) { + if (typeof window === 'undefined' || !currentSessionId || isDesktopExpandedInput) { return; } - const handleSessionReselected = (event: Event) => { - const customEvent = event as CustomEvent; - if (customEvent.detail !== currentSessionId) { + const handleChatTurnKeyDown = (event: KeyboardEvent) => { + if (event.defaultPrevented || event.isComposing) { return; } - resumeToBottomInstant(); + if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') { + return; + } + + if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) { + return; + } + + const { activeMainTab } = useUIStore.getState(); + if (activeMainTab !== 'chat' || hasBlockingChatOverlay()) { + return; + } + + const scrollContainer = scrollRef.current; + if (shouldIgnoreChatNavigationForFocus(document.activeElement, scrollContainer)) { + return; + } + + if (shouldIgnoreChatNavigationTarget(event.target)) { + return; + } + + event.preventDefault(); + const offset = event.key === 'ArrowUp' ? -1 : 1; + void navigation.scrollByTurnOffset(offset, { resumePastEnd: false }); + }; + + window.addEventListener('keydown', handleChatTurnKeyDown); + return () => { + window.removeEventListener('keydown', handleChatTurnKeyDown); + }; + }, [currentSessionId, isDesktopExpandedInput, navigation, scrollRef]); + + React.useEffect(() => { + if (typeof window === 'undefined' || !currentSessionId) return; + + const handleSessionReselected = (event: Event) => { + const customEvent = event as CustomEvent; + if (customEvent.detail !== currentSessionId) return; + if (isPinned || !isOverflowing || isProgrammaticFollowActive) return; + void resumeToBottomInstant(); }; window.addEventListener(SESSION_RESELECTED_EVENT, handleSessionReselected as EventListener); return () => { window.removeEventListener(SESSION_RESELECTED_EVENT, handleSessionReselected as EventListener); }; - }, [currentSessionId, resumeToBottomInstant]); + }, [currentSessionId, isOverflowing, isPinned, isProgrammaticFollowActive, resumeToBottomInstant]); React.useLayoutEffect(() => { const container = scrollRef.current; - if (!container) { - return; - } + if (!container) return; const updateChatScrollHeight = () => { container.style.setProperty('--chat-scroll-height', `${container.clientHeight}px`); @@ -329,37 +711,56 @@ export const ChatContainer: React.FC = () => { }; }, [currentSessionId, isDesktopExpandedInput, scrollRef]); - const hasHistoryMetadata = React.useMemo(() => { - return Boolean(historyMeta); - }, [historyMeta]); + const lastScrolledSessionRef = React.useRef(null); const isSessionHydrating = Boolean(currentSessionId) - && (!hasSessionMessagesEntry || !hasHistoryMetadata || historyMeta?.loading === true); + && !hasLoadedSessionMessages; React.useEffect(() => { if (!currentSessionId) { return; } - const hasSessionMessages = hasSessionMessagesEntry; - if (hasSessionMessages && hasHistoryMetadata) { + if (lastScrolledSessionRef.current === currentSessionId) { return; } + const hasHashTarget = typeof window !== 'undefined' && window.location.hash.length > 0; + if (hasHashTarget) { + lastScrolledSessionRef.current = currentSessionId; + return; + } + + lastScrolledSessionRef.current = currentSessionId; + + if (typeof window === 'undefined') { + resumeToLatestInstant(); + return; + } + + window.requestAnimationFrame(() => { + resumeToLatestInstant(); + }); + }, [currentSessionId, resumeToLatestInstant]); + + React.useEffect(() => { + if (!currentSessionId) return; + if (hasLoadedSessionMessages) return; + const load = async () => { await loadMessages(currentSessionId).finally(() => { const statusType = sessionStatusForCurrent.type ?? 'idle'; const isActivePhase = statusType === 'busy' || statusType === 'retry'; const hasHashTarget = typeof window !== 'undefined' && window.location.hash.length > 0; - const shouldSkipScroll = (isActivePhase && isPinned) || hasHashTarget; + const shouldSkipScroll = hasHashTarget || (isActivePhase && isPinned); if (!shouldSkipScroll) { if (typeof window === 'undefined') { - scrollToBottom({ instant: true }); + resumeToLatestInstant(); } else { window.requestAnimationFrame(() => { - scrollToBottom({ instant: true }); + resumeToLatestInstant(); }); } } @@ -367,41 +768,35 @@ export const ChatContainer: React.FC = () => { }; void load(); - }, [currentSessionId, hasHistoryMetadata, hasSessionMessagesEntry, isPinned, loadMessages, scrollToBottom, sessionMessages.length, sessionStatusForCurrent.type]); + }, [currentSessionId, hasLoadedSessionMessages, isPinned, loadMessages, resumeToLatestInstant, sessionStatusForCurrent.type]); - if (!currentSessionId && !draftOpen) { - return ( -
    - -
    - ); - } + if (!currentSessionId && !draftOpen) { + return ( +
    + +
    + ); + } - if (!currentSessionId && draftOpen) { - return ( -
    - {!isDesktopExpandedInput ? ( -
    - -
    - ) : null} + if (!currentSessionId && draftOpen) { + return ( +
    + {!isDesktopExpandedInput ? ( +
    + +
    + ) : null}
    - -
    -
    + isDesktopExpandedInput + ? 'flex-1 min-h-0 bg-background' + : 'bg-background' + )} + > + +
    +
    ); } @@ -409,23 +804,20 @@ export const ChatContainer: React.FC = () => { return null; } - if (isSessionHydrating && sessionMessages.length === 0 && !streamingMessageId) { - return ( -
    - {returnToParentButton} -
    + {returnToParentButton} +
    -
    +
    {HYDRATING_SKELETON_ITEMS.map((item) => (
    @@ -457,26 +849,23 @@ export const ChatContainer: React.FC = () => {
    - -
    + isDesktopExpandedInput + ? 'flex-1 min-h-0 bg-background' + : 'bg-background' + )} + > + +
    ); } - if (sessionMessages.length === 0 && !streamingMessageId) { - return ( -
    - {returnToParentButton} -
    + {returnToParentButton} +
    {
    - -
    + isDesktopExpandedInput + ? 'flex-1 min-h-0 bg-background' + : 'bg-background' + )} + > + +
    ); } - return ( -
    - {returnToParentButton} -
    -
    - -
    - { - void timelineController.loadEarlier(); - }} - scrollToBottom={scrollToBottom} - scrollRef={scrollRef} - /> -
    -
    - -
    -
    + return ( +
    + {returnToParentButton} +
    - {!isDesktopExpandedInput && sessionMessages.length > 0 && ( - + {!isDesktopExpandedInput && sessionMessages.length > 0 && ( + )} - +
    - - { - releasePinnedScroll(); - return navigation.scrollToMessageId(messageId, { behavior: 'smooth', updateHash: false }); - }} - onScrollByTurnOffset={(offset) => { - releasePinnedScroll(); - void navigation.scrollByTurnOffset(offset); - }} - onResumeToLatest={navigation.resumeToLatest} - />
    ); }; diff --git a/src/packages/ui/src/components/chat/ChatEmptyState.tsx b/src/packages/ui/src/components/chat/ChatEmptyState.tsx index 00c30ee..e5ad6e9 100644 --- a/src/packages/ui/src/components/chat/ChatEmptyState.tsx +++ b/src/packages/ui/src/components/chat/ChatEmptyState.tsx @@ -1,45 +1,29 @@ import React from 'react'; import { OpenChamberLogo } from '@/components/ui/OpenChamberLogo'; -import { TextLoop } from '@/components/ui/TextLoop'; import { useThemeSystem } from '@/contexts/useThemeSystem'; - -const phrases = [ - "Fix the failing tests", - "Refactor this to be more readable", - "Add form validation", - "Optimize this function", - "Write tests for this", - "Explain how this works", - "Add a new feature", - "Help me debug this", - "Review my code", - "Simplify this logic", - "Add error handling", - "Create a new component", - "Update the documentation", - "Find the bug here", - "Improve performance", - "Add type definitions", -]; +import { useGlobalSyncStore } from '@/sync/global-sync-store'; +import { useI18n } from '@/lib/i18n'; const ChatEmptyState: React.FC = () => { + const { t } = useI18n(); const { currentTheme } = useThemeSystem(); + const initError = useGlobalSyncStore((s) => s.error); - // Use theme's muted foreground for secondary text const textColor = currentTheme?.colors?.surface?.mutedForeground || 'var(--muted-foreground)'; return (
    - - - {phrases.map((phrase) => ( - "{phrase}…" - ))} - + + {initError ? ( +
    + {t('chat.emptyState.opencodeUnreachable')} + + {initError.message} + +
    + ) : ( + {t('chat.emptyState.startNewChat')} + )}
    ); }; diff --git a/src/packages/ui/src/components/chat/ChatErrorBoundary.tsx b/src/packages/ui/src/components/chat/ChatErrorBoundary.tsx index c2fc26a..29d3e12 100644 --- a/src/packages/ui/src/components/chat/ChatErrorBoundary.tsx +++ b/src/packages/ui/src/components/chat/ChatErrorBoundary.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { RiChat3Line, RiRestartLine } from '@remixicon/react'; import { Button } from '../ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; +import { useI18n } from '@/lib/i18n'; interface ChatErrorBoundaryState { hasError: boolean; @@ -14,8 +15,21 @@ interface ChatErrorBoundaryProps { sessionId?: string; } -export class ChatErrorBoundary extends React.Component { - constructor(props: ChatErrorBoundaryProps) { +interface ChatErrorBoundaryTexts { + title: string; + description: string; + sessionLabel: string; + detailsSummary: string; + resetAction: string; + persistentHint: string; +} + +interface ChatErrorBoundaryViewProps extends ChatErrorBoundaryProps { + texts: ChatErrorBoundaryTexts; +} + +class ChatErrorBoundaryView extends React.Component { + constructor(props: ChatErrorBoundaryViewProps) { super(props); this.state = { hasError: false }; } @@ -44,24 +58,24 @@ export class ChatErrorBoundary extends React.Component - Chat Error + {this.props.texts.title}

    - The chat interface encountered an error. This might be due to a temporary network issue or corrupted message data. + {this.props.texts.description}

    {this.props.sessionId && (
    - Session: {this.props.sessionId} + {this.props.texts.sessionLabel}: {this.props.sessionId}
    )} {this.state.error && (
    - Error details -
    +                  {this.props.texts.detailsSummary}
    +                  
                         {this.state.error.toString()}
                       
    @@ -70,12 +84,12 @@ export class ChatErrorBoundary extends React.Component
    - If the problem persists, try refreshing the page. + {this.props.texts.persistentHint}
    @@ -86,3 +100,20 @@ export class ChatErrorBoundary extends React.Component + ); +} diff --git a/src/packages/ui/src/components/chat/ChatInput.tsx b/src/packages/ui/src/components/chat/ChatInput.tsx index dd1526d..24b724d 100644 --- a/src/packages/ui/src/components/chat/ChatInput.tsx +++ b/src/packages/ui/src/components/chat/ChatInput.tsx @@ -16,37 +16,40 @@ import { RiSendPlane2Line, } from '@remixicon/react'; import { BrowserVoiceButton } from '@/components/voice'; -import { useSessionStore } from '@/stores/useSessionStore'; -import { useSessionStore as useSessionManagementStore } from '@/stores/sessionStore'; +// sessionStore removed — currentSessionId comes from useSessionUIStore import { useConfigStore } from '@/stores/useConfigStore'; import { useUIStore } from '@/stores/useUIStore'; import { useMessageQueueStore, type QueuedMessage } from '@/stores/messageQueueStore'; +import { useSessionUIStore } from '@/sync/session-ui-store'; +import { useSelectionStore } from '@/sync/selection-store'; +import { useInputStore } from '@/sync/input-store'; import type { AttachedFile } from '@/stores/types/sessionTypes'; +import * as sessionActions from '@/sync/session-actions'; +import { useUserMessageHistory } from '@/sync/sync-context'; import { useInlineCommentDraftStore, type InlineCommentDraft } from '@/stores/useInlineCommentDraftStore'; import { appendInlineComments } from '@/lib/messages/inlineComments'; +import { renderMagicPrompt } from '@/lib/magicPrompts'; import { AttachedFilesList } from './FileAttachment'; import { QueuedMessageChips } from './QueuedMessageChips'; import { FileMentionAutocomplete, type FileMentionHandle } from './FileMentionAutocomplete'; -import { CommandAutocomplete, type CommandAutocompleteHandle } from './CommandAutocomplete'; +import { CommandAutocomplete, type CommandAutocompleteHandle, type CommandInfo } from './CommandAutocomplete'; import { SkillAutocomplete, type SkillAutocompleteHandle } from './SkillAutocomplete'; import { cn, formatDirectoryName, isMacOS } from '@/lib/utils'; import { ModelControls } from './ModelControls'; -import { UnifiedControlsDrawer } from './UnifiedControlsDrawer'; import { parseAgentMentions } from '@/lib/messages/agentMentions'; import { StatusRow } from './StatusRow'; +import { PendingChangesBar } from './PendingChangesBar'; import { MobileAgentButton } from './MobileAgentButton'; import { MobileModelButton } from './MobileModelButton'; import { MobileSessionStatusBar } from './MobileSessionStatusBar'; -import { useAssistantStatus } from '@/hooks/useAssistantStatus'; import { useCurrentSessionActivity } from '@/hooks/useSessionActivity'; import { toast } from '@/components/ui'; -import { useFileStore } from '@/stores/fileStore'; -import { useMessageStore } from '@/stores/messageStore'; +// useMessageStore removed — messages now come from sync system import { isTauriShell, isVSCodeRuntime } from '@/lib/desktop'; import { isIMECompositionEvent } from '@/lib/ime'; import { StopIcon } from '@/components/icons/StopIcon'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; -import type { MobileControlsPanel } from './mobileControlsUtils'; +import { getCycledPrimaryAgentName, type MobileControlsPanel } from './mobileControlsUtils'; import { DropdownMenu, DropdownMenuContent, @@ -61,10 +64,14 @@ import { useChatSearchDirectory } from '@/hooks/useChatSearchDirectory'; import { opencodeClient } from '@/lib/opencode/client'; import { useProjectsStore } from '@/stores/useProjectsStore'; import { PROJECT_COLOR_MAP, PROJECT_ICON_MAP, getProjectIconImageUrl } from '@/lib/projectMeta'; -import { useGitBranches, useGitStore } from '@/stores/useGitStore'; +import { useGitBranches, useGitStore, useIsGitRepo } from '@/stores/useGitStore'; +import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { createWorktreeDraft } from '@/lib/worktreeSessionCreator'; +import { buildSessionTargetOptions } from '@/sync/session-worktree-contract'; import { usePermissionStore } from '@/stores/permissionStore'; +import { extractGitChangedFiles } from './changedFiles'; +import { useI18n } from '@/lib/i18n'; const MAX_VISIBLE_TEXTAREA_LINES = 8; const EMPTY_QUEUE: QueuedMessage[] = []; @@ -81,6 +88,28 @@ const VS_CODE_DROP_DATA_TYPES = [ const FILE_URI_PREFIX = 'file://'; +const encodeFilePath = (filepath: string): string => { + let normalized = filepath.replace(/\\/g, '/'); + if (/^[A-Za-z]:/.test(normalized)) { + normalized = `/${normalized}`; + } + return normalized + .split('/') + .map((segment, index) => { + if (index === 1 && /^[A-Za-z]:$/.test(segment)) return segment; + return encodeURIComponent(segment); + }) + .join('/'); +}; + +const toServerFileUrl = (filepath: string): string => { + const normalized = filepath.replace(/\\/g, '/').trim(); + if (normalized.toLowerCase().startsWith(FILE_URI_PREFIX)) { + return normalized; + } + return `file://${encodeFilePath(normalized)}`; +}; + const isLikelyAbsolutePath = (value: string): boolean => ( value.startsWith('/') || value.startsWith('\\\\') @@ -198,6 +227,373 @@ const getProjectIconColor = (projectColor?: string | null): string | undefined = return PROJECT_COLOR_MAP[projectColor] ?? undefined; }; +const MemoModelControls = React.memo(ModelControls); +const MemoBrowserVoiceButton = React.memo(BrowserVoiceButton); +const MemoMobileAgentButton = React.memo(MobileAgentButton); +const MemoMobileModelButton = React.memo(MobileModelButton); +const MemoStatusRow = React.memo(StatusRow); + +type ComposerAttachmentControlsProps = { + isMobile: boolean; + isVSCode: boolean; + footerIconButtonClass: string; + iconSizeClass: string; + fileInputRef: React.RefObject; + handleLocalFileSelect: (event: React.ChangeEvent) => void | Promise; + handlePickLocalFiles: () => void; + handleOpenCommandMenu: () => void; + openIssuePicker: () => void; + openPrPicker: () => void; + onOpenSettings?: () => void; +}; + +const ComposerAttachmentControls = React.memo(function ComposerAttachmentControls(props: ComposerAttachmentControlsProps) { + const { t } = useI18n(); + const { + isMobile, + isVSCode, + footerIconButtonClass, + iconSizeClass, + fileInputRef, + handleLocalFileSelect, + handlePickLocalFiles, + handleOpenCommandMenu, + openIssuePicker, + openPrPicker, + onOpenSettings, + } = props; + + return ( +
    + {isMobile ? ( + + ) : null} + + +
    + {isVSCode ? ( + + ) : ( + + + + + + { + requestAnimationFrame(handlePickLocalFiles); + }} + > + + {t('chat.chatInput.actions.attachFiles')} + + { + requestAnimationFrame(openIssuePicker); + }} + > + + {t('chat.chatInput.actions.linkGithubIssue')} + + { + requestAnimationFrame(openPrPicker); + }} + > + + {t('chat.chatInput.actions.linkGithubPr')} + + + + )} +
    + + {onOpenSettings ? ( + + ) : null} +
    + ); +}, (prev, next) => ( + prev.isMobile === next.isMobile + && prev.isVSCode === next.isVSCode + && prev.footerIconButtonClass === next.footerIconButtonClass + && prev.iconSizeClass === next.iconSizeClass + && prev.onOpenSettings === next.onOpenSettings +)); + +type PermissionAutoAcceptButtonProps = { + footerIconButtonClass: string; + iconSizeClass: string; + permissionScopeSessionId: string | null; + permissionAutoAcceptEnabled: boolean; + handlePermissionAutoAcceptToggle: () => void; + withTooltip?: boolean; +}; + +const PermissionAutoAcceptButton = React.memo(function PermissionAutoAcceptButton(props: PermissionAutoAcceptButtonProps) { + const { t } = useI18n(); + const { + footerIconButtonClass, + iconSizeClass, + permissionScopeSessionId, + permissionAutoAcceptEnabled, + handlePermissionAutoAcceptToggle, + withTooltip = false, + } = props; + + const ariaLabel = permissionAutoAcceptEnabled + ? t('chat.chatInput.permissionAutoAccept.disable') + : t('chat.chatInput.permissionAutoAccept.enable'); + const tooltipLabel = permissionAutoAcceptEnabled + ? t('chat.chatInput.permissionAutoAccept.on') + : t('chat.chatInput.permissionAutoAccept.off'); + + const button = ( + + ); + + if (!withTooltip) { + return button; + } + + return ( + + + {button} + + + {tooltipLabel} + + + ); +}); + +type FocusModeButtonProps = { + footerIconButtonClass: string; + iconSizeClass: string; + isExpandedInput: boolean; + onToggle: () => void; +}; + +const FocusModeButton = React.memo(function FocusModeButton(props: FocusModeButtonProps) { + const { footerIconButtonClass, iconSizeClass, isExpandedInput, onToggle } = props; + const { t } = useI18n(); + + return ( + + + + + +
    + {t('chat.chatInput.focusMode.label')} + + {isMacOS() ? '⌘⇧E' : 'Ctrl+Shift+E'} + +
    +
    +
    + ); +}); + +type ComposerActionButtonsProps = { + isMobile: boolean; + footerIconButtonClass: string; + sendIconSizeClass: string; + stopIconSizeClass: string; + canSend: boolean; + canAbort: boolean; + hasContent: boolean; + currentSessionId: string | null; + newSessionDraftOpen: boolean; + onPrimaryAction: () => void; + onQueueMessage: () => void; + onAbort: () => void; +}; + +const ComposerActionButtons = React.memo(function ComposerActionButtons(props: ComposerActionButtonsProps) { + const { + isMobile, + footerIconButtonClass, + sendIconSizeClass, + stopIconSizeClass, + canSend, + canAbort, + hasContent, + currentSessionId, + newSessionDraftOpen, + onPrimaryAction, + onQueueMessage, + onAbort, + } = props; + const { t } = useI18n(); + + const sendButton = ( + + ); + + if (!canAbort) { + return sendButton; + } + + return ( +
    + {hasContent ? ( + + ) : null} + +
    + ); +}, (prev, next) => ( + prev.isMobile === next.isMobile + && prev.footerIconButtonClass === next.footerIconButtonClass + && prev.sendIconSizeClass === next.sendIconSizeClass + && prev.stopIconSizeClass === next.stopIconSizeClass + && prev.canSend === next.canSend + && prev.canAbort === next.canAbort + && prev.hasContent === next.hasContent + && prev.currentSessionId === next.currentSessionId + && prev.newSessionDraftOpen === next.newSessionDraftOpen +)); + const appendWithLineBreaks = (base: string, next: string): string => { const separator = !base ? '' @@ -266,14 +662,46 @@ const saveStoredDraft = (sessionId: string | null, draft: string): void => { } }; -export const ChatInput: React.FC = ({ onOpenSettings, scrollToBottom }) => { +// Per-session confirmed mentions key — tracks which @mentions are confirmed (blue) vs plain text +const getConfirmedMentionsKey = (sessionId: string | null): string => + `openchamber_chat_confirmed_mentions_${sessionId ?? 'new'}`; + +const saveConfirmedMentions = (sessionId: string | null, mentions: Set): void => { + try { + if (mentions.size > 0) { + localStorage.setItem(getConfirmedMentionsKey(sessionId), JSON.stringify([...mentions])); + } else { + localStorage.removeItem(getConfirmedMentionsKey(sessionId)); + } + } catch { + // Ignore localStorage errors + } +}; + +const loadConfirmedMentions = (sessionId: string | null): Set => { + try { + const raw = localStorage.getItem(getConfirmedMentionsKey(sessionId)); + if (raw) { + const parsed = JSON.parse(raw); + if (Array.isArray(parsed)) { + return new Set(parsed.filter((v): v is string => typeof v === 'string')); + } + } + } catch { + // Ignore localStorage errors + } + return new Set(); +}; + +const ChatInputComponent: React.FC = ({ onOpenSettings, scrollToBottom }) => { + const { t } = useI18n(); // Track if we restored a draft on mount (for text selection) const initialDraftRef = React.useRef(null); // Track initial session ID (captured at mount time for draft restoration) const initialSessionIdRef = React.useRef(null); const [message, setMessage] = React.useState(() => { // Read per-session draft at mount time using the current session from the store - const sessionId = useSessionStore.getState().currentSessionId; + const sessionId = useSessionUIStore.getState().currentSessionId; initialSessionIdRef.current = sessionId; const draft = getStoredDraft(sessionId); if (draft) { @@ -281,8 +709,14 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo } return draft; }); + // Restore confirmed mentions from localStorage on mount + const confirmedMentionsRef = React.useRef>(loadConfirmedMentions(initialSessionIdRef.current)); + // Helper: check if a mention path looks like a file/folder (has path separators, extension, or was explicitly confirmed) + const isConfirmedFilePath = (text: string): boolean => + text.includes('/') || text.includes('\\') || text.includes('.') || confirmedMentionsRef.current.has(text); const [inputMode, setInputMode] = React.useState<'normal' | 'shell'>('normal'); const [isDragging, setIsDragging] = React.useState(false); + const [isInternalDrag, setIsInternalDrag] = React.useState(false); const [showFileMention, setShowFileMention] = React.useState(false); const [mentionQuery, setMentionQuery] = React.useState(''); const [showCommandAutocomplete, setShowCommandAutocomplete] = React.useState(false); @@ -291,13 +725,15 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const [showSkillAutocomplete, setShowSkillAutocomplete] = React.useState(false); const [skillQuery, setSkillQuery] = React.useState(''); const [textareaSize, setTextareaSize] = React.useState<{ height: number; maxHeight: number } | null>(null); - const [mobileControlsOpen, setMobileControlsOpen] = React.useState(false); const [mobileControlsPanel, setMobileControlsPanel] = React.useState(null); // Message history navigation state (up/down arrow to recall previous messages) const [historyIndex, setHistoryIndex] = React.useState(-1); // -1 = not browsing, 0+ = index from most recent const [draftMessage, setDraftMessage] = React.useState(''); // Preserves input when entering history mode const textareaRef = React.useRef(null); + const cursorPosRef = React.useRef(0); + const previousMessageLengthRef = React.useRef(message.length); const dropZoneRef = React.useRef(null); + const dragEnterCountRef = React.useRef(0); const suppressNextFileDropTextInsertRef = React.useRef(false); const suppressNextFileDropTextInsertTimeoutRef = React.useRef | null>(null); const pendingDroppedAbsolutePathsRef = React.useRef([]); @@ -313,54 +749,77 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const lastPersistedDraftRef = React.useRef>(new Map()); const currentSessionIdForDraftRef = React.useRef(null); - const sendMessage = useSessionStore((state) => state.sendMessage); - const currentSessionId = useSessionStore((state) => state.currentSessionId); - const newSessionDraft = useSessionStore((state) => state.newSessionDraft); + // TODO: port sendMessage to session-actions (complex — creates sessions, handles attachments, etc.) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const sendMessage = React.useRef((...args: any[]) => + Promise.resolve((useSessionUIStore.getState().sendMessage as (...a: unknown[]) => unknown)(...args)), + ).current; + const currentSessionId = useSessionUIStore((s) => s.currentSessionId); + const currentDirectory = useDirectoryStore((s) => s.currentDirectory); + const newSessionDraft = useSessionUIStore((s) => s.newSessionDraft); const newSessionDraftOpen = Boolean(newSessionDraft?.open); - const setNewSessionDraftTarget = useSessionStore((state) => state.setNewSessionDraftTarget); - const availableWorktreesByProject = useSessionStore((state) => state.availableWorktreesByProject); - const abortCurrentOperation = useSessionStore((state) => state.abortCurrentOperation); - const acknowledgeSessionAbort = useSessionStore((state) => state.acknowledgeSessionAbort); - const abortPromptSessionId = useSessionStore((state) => state.abortPromptSessionId); - const clearAbortPrompt = useSessionStore((state) => state.clearAbortPrompt); - const attachedFiles = useSessionStore((state) => state.attachedFiles); - const addAttachedFile = useSessionStore((state) => state.addAttachedFile); - const clearAttachedFiles = useSessionStore((state) => state.clearAttachedFiles); - const saveSessionAgentSelection = useSessionStore((state) => state.saveSessionAgentSelection); - const consumePendingInputText = useSessionStore((state) => state.consumePendingInputText); - const setPendingInputText = useSessionStore((state) => state.setPendingInputText); - const pendingInputText = useSessionStore((state) => state.pendingInputText); - const consumePendingSyntheticParts = useSessionStore((state) => state.consumePendingSyntheticParts); - const currentManagementSessionId = useSessionManagementStore((state) => state.currentSessionId); + const setNewSessionDraftTarget = useSessionUIStore((s) => s.setNewSessionDraftTarget); + const availableWorktreesByProject = useSessionUIStore((s) => s.availableWorktreesByProject); + const abortPromptSessionId = useSessionUIStore((s) => s.abortPromptSessionId); + const clearAbortPrompt = useSessionUIStore((s) => s.clearAbortPrompt); + const attachedFiles = useInputStore((s) => s.attachedFiles); + const addAttachedFile = useInputStore((s) => s.addAttachedFile); + const clearAttachedFiles = useInputStore((s) => s.clearAttachedFiles); + const saveSessionAgentSelection = useSelectionStore((s) => s.saveSessionAgentSelection); + const consumePendingInputText = useInputStore((s) => s.consumePendingInputText); + const setPendingInputText = useInputStore((s) => s.setPendingInputText); + const pendingInputText = useInputStore((s) => s.pendingInputText); + const consumePendingSyntheticParts = useInputStore((s) => s.consumePendingSyntheticParts); + const acknowledgeSessionAbort = useSessionUIStore((s) => s.acknowledgeSessionAbort); + const abortCurrentOperation = React.useCallback( + (sessionIdOverride?: string) => sessionActions.abortCurrentOperation(sessionIdOverride ?? currentSessionId ?? ''), + [currentSessionId], + ); + const currentManagementSessionId = currentSessionId; const projects = useProjectsStore((state) => state.projects); const activeProjectId = useProjectsStore((state) => state.activeProjectId); const setActiveProjectIdOnly = useProjectsStore((state) => state.setActiveProjectIdOnly); - const { currentProviderId, currentModelId, currentVariant, currentAgentName, setAgent, getVisibleAgents } = useConfigStore(); + const currentProviderId = useConfigStore((state) => state.currentProviderId); + const currentModelId = useConfigStore((state) => state.currentModelId); + const currentVariant = useConfigStore((state) => state.currentVariant); + const currentAgentName = useConfigStore((state) => state.currentAgentName); + const setAgent = useConfigStore((state) => state.setAgent); + const getVisibleAgents = useConfigStore((state) => state.getVisibleAgents); const agents = getVisibleAgents(); - const primaryAgents = React.useMemo(() => agents.filter((agent) => agent.mode === 'primary'), [agents]); - const { isMobile, inputBarOffset, isKeyboardOpen, setTimelineDialogOpen, cornerRadius, persistChatDraft, inputSpellcheckEnabled, isExpandedInput, setExpandedInput } = useUIStore(); - const { working } = useAssistantStatus(); + const isMobile = useUIStore((state) => state.isMobile); + const inputBarOffset = useUIStore((state) => state.inputBarOffset); + const persistChatDraft = useUIStore((state) => state.persistChatDraft); + const inputSpellcheckEnabled = useUIStore((state) => state.inputSpellcheckEnabled); + const isExpandedInput = useUIStore((state) => state.isExpandedInput); + const setExpandedInput = useUIStore((state) => state.setExpandedInput); const { git: runtimeGit } = useRuntimeAPIs(); const { currentTheme } = useThemeSystem(); const chatSearchDirectory = useChatSearchDirectory(); + const isGitRepo = useIsGitRepo(currentDirectory); + const currentGitStatus = useGitStore((state) => + currentDirectory ? state.directories.get(currentDirectory)?.status ?? null : null, + ); const [showAbortStatus, setShowAbortStatus] = React.useState(false); - const [textareaScrollTop, setTextareaScrollTop] = React.useState(0); const setSessionAutoAccept = usePermissionStore((state) => state.setSessionAutoAccept); + const composerHighlightRef = React.useRef(null); const isDesktopExpanded = isExpandedInput && !isMobile; - const chatInputRadius = 'var(--radius-lg)'; + const chatInputRadius = 'var(--radius-xl)'; - const sendableAttachedFiles = React.useMemo( - () => attachedFiles.filter((file) => file.source !== 'server'), - [attachedFiles], + const sendableAttachedFiles = attachedFiles; + + const knownAgentNames = React.useMemo( + () => new Set(agents.map((agent) => agent.name.toLowerCase())), + [agents] ); + const knownAgentNamesRef = React.useRef(knownAgentNames); + knownAgentNamesRef.current = knownAgentNames; const hasInlineMentionForHighlight = React.useMemo(() => { if (!message || !message.includes('@') || inputMode === 'shell') { return false; } - const knownAgentNames = new Set(agents.map((agent) => agent.name.toLowerCase())); const mentionRegex = /@([^\s]+)/g; let match: RegExpExecArray | null; while ((match = mentionRegex.exec(message)) !== null) { @@ -376,12 +835,12 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo if (knownAgentNames.has(mentionPath.toLowerCase())) { return true; } - if (mentionPath.includes('/') || mentionPath.includes('\\') || mentionPath.includes('.')) { + if (isConfirmedFilePath(mentionPath)) { return true; } } return false; - }, [agents, inputMode, message]); + }, [inputMode, message, knownAgentNames]); const highlightedComposerContent = React.useMemo(() => { if (!hasInlineMentionForHighlight) { @@ -389,7 +848,6 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo } const parts: Array<{ text: string; mentionKind: 'none' | 'file' | 'agent' }> = []; - const knownAgentNames = new Set(agents.map((agent) => agent.name.toLowerCase())); const mentionRegex = /@([^\s]+)/g; let lastIndex = 0; let match: RegExpExecArray | null; @@ -405,7 +863,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const isFileMention = isBoundary && mention.length > 0 && !knownAgentNames.has(mention.toLowerCase()) - && (mention.includes('/') || mention.includes('\\') || mention.includes('.')); + && isConfirmedFilePath(mention); if (start > lastIndex) { parts.push({ text: message.slice(lastIndex, start), mentionKind: 'none' }); @@ -422,12 +880,16 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo } return parts; - }, [agents, hasInlineMentionForHighlight, message]); + }, [hasInlineMentionForHighlight, message, knownAgentNames]); const sanitizeAttachmentsForSend = React.useCallback( (files: AttachedFile[] | undefined): AttachedFile[] => (files ?? []) - .filter((file) => file.source !== 'server') - .map((file) => ({ ...file })), + .map((file) => ({ + ...file, + dataUrl: file.source === 'server' && file.serverPath + ? toServerFileUrl(file.serverPath) + : file.dataUrl, + })), [], ); @@ -438,7 +900,6 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const clientDirectory = opencodeClient.getDirectory() || ''; const root = (chatSearchDirectory || clientDirectory).replace(/\\/g, '/').replace(/\/+$/, ''); - const knownAgentNames = new Set(agents.map((agent) => agent.name.toLowerCase())); const seenPaths = new Set(); const attachments: AttachedFile[] = []; @@ -461,11 +922,11 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo continue; } - if (knownAgentNames.has(mentionPath.toLowerCase())) { + if (knownAgentNamesRef.current.has(mentionPath.toLowerCase())) { continue; } - const looksLikeFilePath = mentionPath.includes('/') || mentionPath.includes('\\') || mentionPath.includes('.'); + const looksLikeFilePath = isConfirmedFilePath(mentionPath); if (!looksLikeFilePath) { continue; } @@ -498,7 +959,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo filename, mimeType: 'text/plain', size: 0, - dataUrl: normalizedServerPath, + dataUrl: toServerFileUrl(normalizedServerPath), source: 'server', serverPath: normalizedServerPath, }); @@ -508,7 +969,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo sanitizedText: rawText, attachments, }; - }, [agents, chatSearchDirectory]); + }, [chatSearchDirectory]); const [autocompleteOverlayPosition, setAutocompleteOverlayPosition] = React.useState(null); const abortTimeoutRef = React.useRef | null>(null); const prevWasAbortedRef = React.useRef(false); @@ -563,29 +1024,9 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const consumeDrafts = useInlineCommentDraftStore((state) => state.consumeDrafts); const hasDrafts = draftCount > 0; - // User message history for up/down arrow navigation - // Get raw messages from store (stable reference) - const sessionMessages = useMessageStore( - React.useCallback( - (state) => (currentSessionId ? state.messages.get(currentSessionId) : undefined), - [currentSessionId] - ) - ); - // Derive user message history with useMemo to avoid infinite re-renders - const userMessageHistory = React.useMemo(() => { - if (!sessionMessages) return []; - return sessionMessages - .filter((m) => m.info.role === 'user') - .map((m) => { - const textPart = m.parts.find((p) => p.type === 'text'); - if (textPart && 'text' in textPart) { - return String(textPart.text); - } - return ''; - }) - .filter((text) => text.length > 0) - .reverse(); // Most recent first - }, [sessionMessages]); + // User message history for up/down arrow navigation. + // Keep this on a narrow hook instead of full session message records. + const userMessageHistory = useUserMessageHistory(currentSessionId ?? ""); // Keep messageRef in sync with message state React.useEffect(() => { @@ -604,6 +1045,15 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo } saveStoredDraft(sessionId, draft); + // Only persist confirmed mentions that are actually present in the draft text + const activeMentions = new Set(); + for (const mention of confirmedMentionsRef.current) { + if (draft.includes(`@${mention}`)) { + activeMentions.add(mention); + } + } + confirmedMentionsRef.current = activeMentions; + saveConfirmedMentions(sessionId, activeMentions); lastPersistedDraftRef.current.set(key, draft); }, []); @@ -656,6 +1106,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo // Restore draft for the session we're entering const newDraft = getStoredDraft(currentSessionId); setMessage(newDraft); + confirmedMentionsRef.current = loadConfirmedMentions(currentSessionId); if (newDraft) { requestAnimationFrame(() => { textareaRef.current?.select(); @@ -664,6 +1115,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo } else { // Persist disabled: clear input without saving setMessage(''); + confirmedMentionsRef.current = new Set(); } } }, [clearPendingDraftPersist, currentSessionId, persistChatDraft, persistDraftImmediately]); @@ -723,98 +1175,16 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo // Session activity for queue availability and controls const { phase: sessionPhase } = useCurrentSessionActivity(); - const handleTextareaPointerDownCapture = React.useCallback((event: React.PointerEvent) => { - if (!isMobile) { - return; - } - - if (event.pointerType !== 'touch') { - return; - } - - const textarea = textareaRef.current; - if (!textarea) { - return; - } - - if (document.activeElement === textarea) { - return; - } - - // Prevent iOS from scrolling the page to reveal the input. - event.preventDefault(); - event.stopPropagation(); - - const scroller = document.scrollingElement; - if (scroller && scroller.scrollTop !== 0) { - scroller.scrollTop = 0; - } - if (window.scrollY !== 0) { - window.scrollTo(0, 0); - } - - try { - textarea.focus({ preventScroll: true }); - } catch { - textarea.focus(); - } - - const len = textarea.value.length; - try { - textarea.setSelectionRange(len, len); - } catch { - // ignored - } - }, [isMobile]); - - const handleOpenMobileControls = React.useCallback(() => { - if (!isMobile) { - return; - } - - if (mobileControlsOpen) { - setMobileControlsOpen(false); - return; - } - - setMobileControlsPanel(null); - - if (isKeyboardOpen) { - textareaRef.current?.blur(); - requestAnimationFrame(() => { - setMobileControlsOpen(true); - }); - return; - } - - setMobileControlsOpen(true); - }, [isMobile, isKeyboardOpen, mobileControlsOpen]); - - const handleCloseMobileControls = React.useCallback(() => { - setMobileControlsOpen(false); - }, []); - const handleOpenMobilePanel = React.useCallback((panel: MobileControlsPanel) => { if (!isMobile) { return; } - setMobileControlsOpen(false); textareaRef.current?.blur(); requestAnimationFrame(() => { setMobileControlsPanel(panel); }); }, [isMobile]); - const handleReturnToUnifiedControls = React.useCallback(() => { - if (!isMobile) { - return; - } - setMobileControlsPanel(null); - requestAnimationFrame(() => { - setMobileControlsOpen(true); - }); - }, [isMobile]); - // Consume pending input text (e.g., from revert action) React.useEffect(() => { if (pendingInputText !== null) { @@ -839,11 +1209,11 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo } }, [pendingInputText, consumePendingInputText]); - const hasContent = message.trim() || sendableAttachedFiles.length > 0 || hasDrafts; + const hasContent = message.trim().length > 0 || sendableAttachedFiles.length > 0 || hasDrafts; const hasQueuedMessages = queuedMessages.length > 0; const canSend = hasContent || hasQueuedMessages; - const canAbort = working.isWorking; + const canAbort = sessionPhase !== 'idle'; // Keep a ref to handleSubmit so callbacks don't depend on it. type SubmitOptions = { @@ -866,9 +1236,18 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo addToQueue(currentSessionId, { content: messageToQueue, attachments: attachmentsToQueue.length > 0 ? attachmentsToQueue : undefined, + sendConfig: currentProviderId && currentModelId ? { + providerID: currentProviderId, + modelID: currentModelId, + agent: currentAgentName ?? undefined, + variant: currentVariant ?? undefined, + } : undefined, }); // Clear input and attachments + // Note: confirmedMentionsRef is NOT cleared here because queued messages + // are processed later in handleSubmit which reads the ref via extractInlineFileMentions. + // The ref is cleared in handleSubmit after all queued messages are sent. setMessage(''); if (attachmentsToQueue.length > 0) { clearAttachedFiles(); @@ -877,7 +1256,30 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo if (!isMobile) { textareaRef.current?.focus(); } - }, [hasContent, currentSessionId, message, sendableAttachedFiles, sanitizeAttachmentsForSend, addToQueue, clearAttachedFiles, isMobile, consumeDrafts]); + }, [hasContent, currentSessionId, message, sendableAttachedFiles, sanitizeAttachmentsForSend, addToQueue, clearAttachedFiles, isMobile, consumeDrafts, currentProviderId, currentModelId, currentAgentName, currentVariant]); + + const handleQueuedMessageEdit = React.useCallback((content: string) => { + setMessage(content); + setTimeout(() => { + textareaRef.current?.focus(); + }, 0); + }, []); + + const handleOpenAgentPanel = React.useCallback(() => { + setMobileControlsPanel('agent'); + }, []); + + const handleToggleExpandedInput = React.useCallback(() => { + setExpandedInput(!isExpandedInput); + }, [isExpandedInput, setExpandedInput]); + + const openIssuePicker = React.useCallback(() => { + setIssuePickerOpen(true); + }, []); + + const openPrPicker = React.useCallback(() => { + setPrPickerOpen(true); + }, []); const handleSubmit = async (options?: SubmitOptions) => { const queuedOnly = options?.queuedOnly ?? false; @@ -888,9 +1290,6 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo return; } - // Re-pin and scroll to bottom when sending - scrollToBottom?.({ instant: true, force: true }); - if (!currentProviderId || !currentModelId) { console.warn('Cannot send message: provider or model not selected'); return; @@ -1012,8 +1411,10 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo } if (!queuedOnly) { setMessage(''); + confirmedMentionsRef.current.clear(); // Clear per-session draft on submit saveStoredDraft(currentSessionId, ''); + saveConfirmedMentions(currentSessionId, confirmedMentionsRef.current); // Reset message history navigation state setHistoryIndex(-1); setDraftMessage(''); @@ -1037,25 +1438,82 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo .split(/\s+/)[0] ?.toLowerCase(); - // NEW: /undo - revert to last message (populates input with reverted message text) if (commandName === 'undo' && currentSessionId) { - await useSessionStore.getState().handleSlashUndo(currentSessionId); - // Don't clear message - pendingInputText will populate it with reverted message + await useSessionUIStore.getState().handleSlashUndo(currentSessionId); scrollToBottom?.({ instant: true, force: true }); - return; // Don't send to assistant + return; } - // NEW: /redo - unrevert or partial redo (populates input with message text) else if (commandName === 'redo' && currentSessionId) { - await useSessionStore.getState().handleSlashRedo(currentSessionId); - // Don't clear message - pendingInputText will populate it + await useSessionUIStore.getState().handleSlashRedo(currentSessionId); scrollToBottom?.({ instant: true, force: true }); - return; // Don't send to assistant + return; } - // NEW: /timeline - open timeline dialog - else if (commandName === 'timeline' && currentSessionId) { - setTimelineDialogOpen(true); - setMessage(''); - return; // Don't send to assistant + else if (commandName === 'compact' && currentSessionId) { + try { + await sessionActions.waitForConnectionOrThrow(); + const { opencodeClient } = await import('@/lib/opencode/client'); + const sdk = opencodeClient.getSdkClient(); + const configState = useConfigStore.getState(); + await sdk.session.summarize({ + sessionID: currentSessionId, + modelID: configState.currentModelId || '', + providerID: configState.currentProviderId || '', + }); + } catch (error) { + toast.error(error instanceof Error ? error.message : t('chat.chatInput.toast.compactFailed')); + } + return; + } + else if (commandName === 'summary' && currentSessionId) { + try { + await sessionActions.waitForConnectionOrThrow(); + // Everything after `/summary ` is an optional topic hint + // the user wants the summary focused on. + const topic = normalizedCommand.replace(/^\/summary\b/i, '').trim(); + const topicLine = topic ? ` focused on: ${topic}` : ''; + const topicBlock = topic + ? `The user asked you to focus this summary on: ${topic}. Prioritize that topic; mention unrelated threads only in passing.` + : ''; + const visibleText = await renderMagicPrompt('session.summary.visible', { topic_line: topicLine }); + const instructionsText = await renderMagicPrompt('session.summary.instructions', { topic_block: topicBlock }); + await sendMessage( + visibleText, + currentProviderId, + currentModelId, + currentAgentName, + [], + agentMentionName, + [{ text: instructionsText, synthetic: true }], + currentVariant, + inputMode, + ); + scrollToBottom?.({ instant: true, force: true }); + } catch (error) { + toast.error(error instanceof Error ? error.message : t('chat.chatInput.toast.summaryFailed')); + } + return; + } + else if (commandName === 'workspace-review' && (currentSessionId || newSessionDraftOpen)) { + try { + await sessionActions.waitForConnectionOrThrow(); + const visibleText = await renderMagicPrompt('session.review.visible'); + const instructionsText = await renderMagicPrompt('session.review.instructions'); + await sendMessage( + visibleText, + currentProviderId, + currentModelId, + currentAgentName, + [], + agentMentionName, + [{ text: instructionsText, synthetic: true }], + currentVariant, + inputMode, + ); + scrollToBottom?.({ instant: true, force: true }); + } catch (error) { + toast.error(error instanceof Error ? error.message : t('chat.chatInput.toast.reviewFailed')); + } + return; } } @@ -1065,7 +1523,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo ...additionalParts.flatMap(p => p.attachments ?? []), ]; - void sendMessage( + const sendPromise = sendMessage( primaryText, currentProviderId, currentModelId, @@ -1075,7 +1533,17 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo additionalParts.length > 0 ? additionalParts : undefined, currentVariant, inputMode - ).then(() => { + ); + + if (typeof window === 'undefined') { + scrollToBottom?.({ instant: true, force: true }); + } else { + window.requestAnimationFrame(() => { + scrollToBottom?.({ instant: true, force: true }); + }); + } + + void sendPromise.then(() => { // Clear linked issue after successful message send if (linkedIssue) { setLinkedIssue(null); @@ -1106,25 +1574,25 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo normalized === 'failed to send message'; if (normalized.includes('payload too large') || normalized.includes('413') || normalized.includes('entity too large')) { - toast.error('Attachments are too large to send. Please try reducing the number or size of images.'); + toast.error(t('chat.chatInput.toast.attachmentsTooLarge')); if (allAttachments.length > 0) { - useFileStore.setState({ attachedFiles: allAttachments }); + useInputStore.setState({ attachedFiles: allAttachments }); } return; } if (isSoftNetworkError) { if (allAttachments.length > 0) { - useFileStore.setState({ attachedFiles: allAttachments }); - toast.error('Failed to send attachments. Try fewer files or smaller images.'); + useInputStore.setState({ attachedFiles: allAttachments }); + toast.error(t('chat.chatInput.toast.sendAttachmentsFailed')); } return; } if (allAttachments.length > 0) { - useFileStore.setState({ attachedFiles: allAttachments }); + useInputStore.setState({ attachedFiles: allAttachments }); } - toast.error(rawMessage || 'Message failed to send. Attachments restored.'); + toast.error(rawMessage || t('chat.chatInput.toast.messageSendFailed')); }); if (!isMobile) { @@ -1182,10 +1650,13 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo } const token = message.slice(tokenStart, tokenEnd); + const mentionContent = token.slice(1); const looksLikeFileMention = FILE_MENTION_TOKEN.test(token) - && (token.includes('/') || token.includes('\\') || token.includes('.')); + && !knownAgentNamesRef.current.has(mentionContent.toLowerCase()) + && isConfirmedFilePath(mentionContent); if (looksLikeFileMention) { + confirmedMentionsRef.current.delete(mentionContent); const removeUntil = message[tokenEnd] === ' ' ? tokenEnd + 1 : tokenEnd; const nextMessage = `${message.slice(0, tokenStart)}${message.slice(removeUntil)}`; e.preventDefault(); @@ -1461,33 +1932,37 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo }, [abortCurrentOperation, clearAbortPrompt, currentSessionId, startAbortIndicator]); const handleCycleAgent = React.useCallback(() => { - if (primaryAgents.length <= 1) return; + const nextAgentName = getCycledPrimaryAgentName(agents, currentAgentName); + if (!nextAgentName) return; - const currentIndex = primaryAgents.findIndex(agent => agent.name === currentAgentName); - const nextIndex = (currentIndex + 1) % primaryAgents.length; - const nextAgent = primaryAgents[nextIndex]; - - setAgent(nextAgent.name); + setAgent(nextAgentName); if (currentSessionId) { - saveSessionAgentSelection(currentSessionId, nextAgent.name); + saveSessionAgentSelection(currentSessionId, nextAgentName); } - }, [primaryAgents, currentAgentName, currentSessionId, setAgent, saveSessionAgentSelection]); + }, [agents, currentAgentName, currentSessionId, setAgent, saveSessionAgentSelection]); - const adjustTextareaHeight = React.useCallback(() => { + const adjustTextareaHeight = React.useCallback((options?: { allowShrink?: boolean }) => { const textarea = textareaRef.current; if (!textarea) { return; } + const previousScrollTop = textarea.scrollTop; + if (isDesktopExpanded) { textarea.style.height = '100%'; textarea.style.maxHeight = 'none'; setTextareaSize(null); + if (textarea.scrollTop !== previousScrollTop) { + textarea.scrollTop = previousScrollTop; + } return; } - textarea.style.height = 'auto'; + if (options?.allowShrink ?? true) { + textarea.style.height = 'auto'; + } const view = textarea.ownerDocument?.defaultView; const computedStyle = view ? view.getComputedStyle(textarea) : null; @@ -1506,6 +1981,9 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo textarea.style.height = `${nextHeight}px`; textarea.style.maxHeight = `${maxHeight}px`; + if (textarea.scrollTop !== previousScrollTop) { + textarea.scrollTop = previousScrollTop; + } setTextareaSize((prev) => { if (prev && prev.height === nextHeight && prev.maxHeight === maxHeight) { @@ -1516,7 +1994,9 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo }, [isDesktopExpanded]); React.useLayoutEffect(() => { - adjustTextareaHeight(); + const allowShrink = message.length < previousMessageLengthRef.current; + previousMessageLengthRef.current = message.length; + adjustTextareaHeight({ allowShrink }); }, [adjustTextareaHeight, message, isMobile]); const updateAutocompleteState = React.useCallback((value: string, cursorPosition: number) => { @@ -1575,7 +2055,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const isWordBoundary = !charBefore || /\s/.test(charBefore); if (isWordBoundary && !textAfterAt.includes(' ') && !textAfterAt.includes('\n')) { setMentionQuery(textAfterAt); - setAutocompleteTab('agents'); + setAutocompleteTab((current) => current === 'files' ? 'files' : 'agents'); setShowFileMention(true); } else { setShowFileMention(false); @@ -1618,22 +2098,31 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo // ignored } } + const cursorPosition = textarea?.selectionStart ?? message.length; + const textBeforeCursor = message.substring(0, cursorPosition); + const lastAtSymbol = textBeforeCursor.lastIndexOf('@'); + const nextMentionQuery = lastAtSymbol !== -1 + ? textBeforeCursor.substring(lastAtSymbol + 1).replace(/[\s\n].*$/, '') + : ''; + setAutocompleteTab(tab); setCommandQuery(''); - setMentionQuery(''); if (tab === 'commands') { + setMentionQuery(''); applyAutocompletePrefix('/'); } if (tab === 'agents') { + setMentionQuery(nextMentionQuery); applyAutocompletePrefix('@'); } if (tab === 'files') { + setMentionQuery(nextMentionQuery); applyAutocompletePrefix('@'); } setShowSkillAutocomplete(false); setShowCommandAutocomplete(tab === 'commands'); setShowFileMention(tab === 'agents' || tab === 'files'); - }, [applyAutocompletePrefix, isMobile, setAutocompleteTab, setCommandQuery, setMentionQuery, setShowCommandAutocomplete, setShowFileMention, setShowSkillAutocomplete]); + }, [applyAutocompletePrefix, isMobile, message, setAutocompleteTab, setCommandQuery, setMentionQuery, setShowCommandAutocomplete, setShowFileMention, setShowSkillAutocomplete]); const handleOpenCommandMenu = React.useCallback(() => { if (!isMobile) { @@ -1807,10 +2296,10 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo await addAttachedFile(file); } catch (error) { console.error('Clipboard image attach failed', error); - toast.error(error instanceof Error ? error.message : 'Failed to attach image from clipboard'); + toast.error(error instanceof Error ? error.message : t('chat.chatInput.toast.clipboardAttachFailed')); } } - }, [addAttachedFile, currentSessionId, newSessionDraftOpen, insertTextAtSelection]); + }, [addAttachedFile, currentSessionId, newSessionDraftOpen, insertTextAtSelection, t]); const handleFileSelect = (file: { name: string; path: string; relativePath?: string }) => { @@ -1822,6 +2311,8 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo ? file.relativePath.trim() : (toProjectRelativeMentionPath(file.path) || file.name); + confirmedMentionsRef.current.add(mentionPath); + if (lastAtSymbol !== -1) { const newMessage = message.substring(0, lastAtSymbol) + @@ -1936,7 +2427,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo textareaRef.current?.focus(); }; - const handleCommandSelect = (command: { name: string; description?: string; agent?: string; model?: string }) => { + const handleCommandSelect = (command: CommandInfo) => { setMessage(`/${command.name} `); @@ -1975,7 +2466,6 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo React.useEffect(() => { if (!isMobile) { - setMobileControlsOpen(false); setMobileControlsPanel(null); } }, [isMobile]); @@ -1999,6 +2489,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo if (lowerTypes.includes('files')) return true; if (lowerTypes.includes('text/uri-list')) return true; if (lowerTypes.includes('codefiles')) return true; + if (lowerTypes.includes('application/x-openchamber-file-path')) return true; if (lowerTypes.some((type) => type.includes('vnd.code.tree'))) return true; } @@ -2098,20 +2589,25 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const addVSCodeDroppedUrisAsMentions = React.useCallback((uris: string[]) => { if (uris.length === 0) return; - const mentions = Array.from(new Set(uris + const paths = uris .map((entry) => normalizeDroppedPath(entry)) .map((entry) => toProjectRelativeMentionPath(entry)) .map((entry) => entry.trim().replace(/^\.\//, '')) - .filter((entry) => entry.length > 0) - .map((entry) => `@${entry}`))); + .filter((entry) => entry.length > 0); + + for (const p of paths) { + confirmedMentionsRef.current.add(p); + } + + const mentions = Array.from(new Set(paths.map((entry) => `@${entry}`))); if (mentions.length === 0) { return; } setPendingInputText(mentions.join(' '), 'append-inline'); - toast.success(`Added ${mentions.length} file mention${mentions.length > 1 ? 's' : ''}`); - }, [normalizeDroppedPath, setPendingInputText, toProjectRelativeMentionPath]); + toast.success(t('chat.chatInput.toast.addedFileMentions', { count: mentions.length })); + }, [normalizeDroppedPath, setPendingInputText, t, toProjectRelativeMentionPath]); const handleDragEnter = (e: React.DragEvent) => { if (!hasDraggedFiles(e.dataTransfer)) { @@ -2119,6 +2615,11 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo } e.preventDefault(); e.stopPropagation(); + dragEnterCountRef.current++; + const isInternal = e.dataTransfer.types?.includes('application/x-openchamber-file-path') ?? false; + if (isInternal !== isInternalDrag) { + setIsInternalDrag(isInternal); + } if ((currentSessionId || newSessionDraftOpen) && !isDragging) { setIsDragging(true); } @@ -2139,13 +2640,24 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); - if (e.currentTarget === e.target) { + dragEnterCountRef.current--; + if (dragEnterCountRef.current <= 0) { + dragEnterCountRef.current = 0; setIsDragging(false); + setIsInternalDrag(false); clearDropTextSuppression(); } }; + const handleDragEnd = () => { + dragEnterCountRef.current = 0; + setIsDragging(false); + setIsInternalDrag(false); + clearDropTextSuppression(); + }; + const handleDrop = async (e: React.DragEvent) => { + dragEnterCountRef.current = 0; const draggedFiles = hasDraggedFiles(e.dataTransfer); if (!draggedFiles) { clearDropTextSuppression(); @@ -2157,6 +2669,37 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo if (!currentSessionId && !newSessionDraftOpen) return; + // Internal drag: file tree → chat input (relative path as @mention) + const internalPath = e.dataTransfer.getData('application/x-openchamber-file-path'); + if (internalPath && internalPath !== '.') { + confirmedMentionsRef.current.add(internalPath); + const mention = `@${internalPath}`; + const textarea = textareaRef.current; + const currentMessage = messageRef.current; + if (textarea) { + const pos = textarea.selectionStart ?? cursorPosRef.current; + const end = textarea.selectionEnd ?? pos; + const before = currentMessage.slice(0, pos); + const after = currentMessage.slice(end); + const needSpaceBefore = before.length > 0 && !/\s$/.test(before); + const needSpaceAfter = after.length > 0 && !/^\s/.test(after); + const insert = `${needSpaceBefore ? ' ' : ''}${mention}${needSpaceAfter ? ' ' : ''}`; + const nextMessage = `${before}${insert}${after}`; + setMessage(nextMessage); + requestAnimationFrame(() => { + const cursorPos = pos + insert.length; + textarea.selectionStart = cursorPos; + textarea.selectionEnd = cursorPos; + cursorPosRef.current = cursorPos; + textarea.focus(); + }); + } else { + setMessage((prev) => appendInlineText(prev, mention)); + } + clearDropTextSuppression(); + return; + } + const files = collectDroppedFiles(e.dataTransfer); if (files.length === 0 && isVSCodeRuntime()) { @@ -2179,7 +2722,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo await addAttachedFile(file); } catch (error) { console.error('File attach failed', error); - toast.error(error instanceof Error ? error.message : 'Failed to attach file'); + toast.error(error instanceof Error ? error.message : t('chat.chatInput.toast.attachFileFailed')); } } } @@ -2187,15 +2730,15 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo }; const handleDropCapture = (e: React.DragEvent) => { - if (!isVSCodeRuntime()) { - return; - } if (!hasDraggedFiles(e.dataTransfer)) { return; } - suppressNextFileDropTextInsertRef.current = true; - scheduleDropTextSuppressionExpiry(); + // Prevent native textarea drop text insertion for all runtimes e.preventDefault(); + if (isVSCodeRuntime()) { + suppressNextFileDropTextInsertRef.current = true; + scheduleDropTextSuppressionExpiry(); + } }; // Tauri desktop: handle native file drops via onDragDropEvent @@ -2287,7 +2830,9 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo await addAttachedFile(file); } catch (error) { console.error('Failed to attach dropped file:', path, error); - toast.error(`Failed to attach ${path.split(/[\\/]/).pop() || 'file'}`); + toast.error(t('chat.chatInput.toast.attachNamedFailed', { + name: path.split(/[\\/]/).pop() || t('chat.chatInput.fileFallback'), + })); } } } @@ -2309,7 +2854,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo cancelled = true; if (unlisten) unlisten(); }; - }, [addAttachedFile, normalizeDroppedPath]); + }, [addAttachedFile, normalizeDroppedPath, t]); const fileInputRef = React.useRef(null); @@ -2321,10 +2866,10 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo await addAttachedFile(file); } catch (error) { console.error('File attach failed', error); - toast.error(error instanceof Error ? error.message : 'Failed to attach file'); + toast.error(error instanceof Error ? error.message : t('chat.chatInput.toast.attachFileFailed')); } } - }, [addAttachedFile]); + }, [addAttachedFile, t]); const handleVSCodePickFiles = React.useCallback(async () => { try { @@ -2337,7 +2882,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const summary = skipped .map((s: { name?: string; reason?: string }) => `${s?.name || 'file'}: ${s?.reason || 'skipped'}`) .join('\n'); - toast.error(`Some files were skipped:\n${summary}`); + toast.error(t('chat.chatInput.toast.someFilesSkipped', { summary })); } const asFiles = picked @@ -2366,9 +2911,9 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo } } catch (error) { console.error('VS Code file pick failed', error); - toast.error(error instanceof Error ? error.message : 'Failed to pick files in VS Code'); + toast.error(error instanceof Error ? error.message : t('chat.chatInput.toast.vscodePickFailed')); } - }, [attachFiles]); + }, [attachFiles, t]); const handlePickLocalFiles = React.useCallback(() => { if (isVSCodeRuntime()) { @@ -2442,6 +2987,8 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo }; }, [fetchBranches, runtimeGit, selectedDraftProject, selectedDraftProjectBranches?.all, selectedDraftProjectPath, showDraftTargetSelectors]); + const selectedDraftProjectCurrentBranch = selectedDraftProjectBranches?.current?.trim() ?? ''; + const projectRootBranchOption = React.useMemo(() => { if (!selectedDraftProject) { return null; @@ -2450,25 +2997,20 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo if (!value) { return null; } - const projectRootBranch = selectedDraftProjectBranches?.current?.trim() ?? ''; - if (!projectRootBranch) { + if (!selectedDraftProjectCurrentBranch) { return null; } return { value, - label: projectRootBranch, + label: selectedDraftProjectCurrentBranch, }; - }, [selectedDraftProject, selectedDraftProjectBranches]); + }, [selectedDraftProject, selectedDraftProjectCurrentBranch]); const worktreeBranchOptions = React.useMemo(() => { if (!selectedDraftProject) { - return [] as Array<{ value: string; label: string }>; + return []; } - const seen = new Set(); - const options: Array<{ value: string; label: string }> = []; - const rootValue = projectRootBranchOption?.value ?? null; - const worktrees = (() => { if (!selectedDraftProjectPath) { return []; @@ -2478,23 +3020,13 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo ?? []; })(); - worktrees - .slice() - .sort((a, b) => a.branch.localeCompare(b.branch)) - .forEach((worktree) => { - const normalizedValue = normalizePath(worktree.path); - if (!normalizedValue || normalizedValue === rootValue || seen.has(normalizedValue)) { - return; - } - seen.add(normalizedValue); - options.push({ - value: normalizedValue, - label: worktree.branch?.trim() || formatDirectoryName(worktree.path), - }); - }); - - return options; - }, [availableWorktreesByProject, projectRootBranchOption?.value, selectedDraftProject, selectedDraftProjectPath]); + return buildSessionTargetOptions({ + projectRoot: normalizePath(selectedDraftProject.path) ?? '', + rootBranch: selectedDraftProjectCurrentBranch, + worktrees, + pendingBootstrapDirectory: newSessionDraft?.bootstrapPendingDirectory ?? null, + }); + }, [availableWorktreesByProject, newSessionDraft?.bootstrapPendingDirectory, selectedDraftProject, selectedDraftProjectCurrentBranch, selectedDraftProjectPath]); const selectedDraftDirectory = React.useMemo( () => normalizePath(newSessionDraft?.bootstrapPendingDirectory ?? null) @@ -2543,6 +3075,13 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo return draftBranchItems.find((item) => item.value === selectedValue)?.label ?? formatDirectoryName(selectedValue); }, [draftBranchItems, selectedDraftDirectory]); + const hasPendingChanges = React.useMemo(() => { + if (isGitRepo !== true || !currentGitStatus || currentGitStatus.isClean) { + return false; + } + return extractGitChangedFiles(currentGitStatus.files, currentGitStatus.diffStats, currentDirectory).length > 0; + }, [currentDirectory, currentGitStatus, isGitRepo]); + const selectedDraftBranchIsKnown = React.useMemo(() => { if (!selectedDraftDirectory) { return true; @@ -2560,7 +3099,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo if (!selectedDraftDirectory || !selectedDraftBranchIsKnown) { return; } - useSessionStore.getState().setDraftPreserveDirectoryOverride(false); + useSessionUIStore.getState().setDraftPreserveDirectoryOverride(false); }, [newSessionDraft?.open, newSessionDraft?.preserveDirectoryOverride, selectedDraftBranchIsKnown, selectedDraftDirectory]); const shouldShowDraftBranchSelector = React.useMemo(() => { @@ -2574,7 +3113,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo }, [isDiscoveringDraftBranches, projectRootBranchOption, worktreeBranchOptions.length]); const handleDraftProjectChange = React.useCallback((projectId: string) => { - const draft = useSessionStore.getState().newSessionDraft; + const draft = useSessionUIStore.getState().newSessionDraft; if (draft?.pendingWorktreeRequestId || draft?.bootstrapPendingDirectory || draft?.preserveDirectoryOverride) { return; } @@ -2592,7 +3131,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo }, [activeProjectId, projects, setActiveProjectIdOnly, setNewSessionDraftTarget]); const handleDraftDirectoryChange = React.useCallback((directory: string) => { - const draft = useSessionStore.getState().newSessionDraft; + const draft = useSessionUIStore.getState().newSessionDraft; if (draft?.pendingWorktreeRequestId || draft?.bootstrapPendingDirectory || draft?.preserveDirectoryOverride) { return; } @@ -2678,252 +3217,18 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo const handlePermissionAutoAcceptToggle = React.useCallback(() => { if (!permissionScopeSessionId) { - toast.error('Open a session first'); + toast.error(t('chat.chatInput.toast.openSessionFirst')); return; } const nextEnabled = !permissionAutoAcceptEnabled; setSessionAutoAccept(permissionScopeSessionId, nextEnabled).catch(() => { - toast.error('Failed to toggle permission auto-accept'); + toast.error(t('chat.chatInput.toast.togglePermissionAutoAcceptFailed')); }); - }, [permissionAutoAcceptEnabled, permissionScopeSessionId, setSessionAutoAccept]); - - const permissionAutoAcceptAriaLabel = permissionAutoAcceptEnabled - ? 'Disable permission auto-accept' - : 'Enable permission auto-accept'; - const permissionAutoAcceptTooltipLabel = permissionAutoAcceptEnabled - ? 'Permission auto-accept: on' - : 'Permission auto-accept: off'; - - const permissionAutoAcceptButton = ( - - ); - - const permissionAutoAcceptButtonWithTooltip = ( - - - {permissionAutoAcceptButton} - - - {permissionAutoAcceptTooltipLabel} - - - ); - - // Send button - respects queue mode setting - const sendButton = ( - - ); - - // Queue button for adding message to queue while working - const queueButton = ( - - ); - - // Stop button replaces send button when working - const stopButton = ( - - ); - - // Action buttons area: either send button, or stop (+ optional queue button floating above) - const actionButtons = canAbort ? ( -
    - {hasContent && queueButton} - {stopButton} -
    - ) : ( - sendButton - ); - - const attachmentMenu = ( - <> - - -
    - {isVSCode ? ( - - ) : ( - - - - - - { - requestAnimationFrame(() => handlePickLocalFiles()); - }} - > - - Attach files - - { - requestAnimationFrame(() => { - setIssuePickerOpen(true); - }); - }} - > - - Link GitHub Issue - - { - requestAnimationFrame(() => { - setPrPickerOpen(true); - }); - }} - > - - Link GitHub PR - - - - )} -
    - - ); - - const settingsButton = onOpenSettings ? ( - - ) : null; - - const attachmentsControls = ( -
    - {isMobile ? ( - - ) : null} - {attachmentMenu} - {settingsButton} -
    - ); - - const workingStatusText = working.statusText; + }, [permissionAutoAcceptEnabled, permissionScopeSessionId, setSessionAutoAccept, t]); React.useEffect(() => { - const pendingAbortBanner = Boolean(working.wasAborted); + const pendingAbortBanner = Boolean(abortPromptSessionId) && abortPromptSessionId === currentSessionId; if (!prevWasAbortedRef.current && pendingAbortBanner && !showAbortStatus) { startAbortIndicator(); if (currentSessionId) { @@ -2932,11 +3237,11 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo } prevWasAbortedRef.current = pendingAbortBanner; }, [ + abortPromptSessionId, acknowledgeSessionAbort, currentSessionId, showAbortStatus, startAbortIndicator, - working.wasAborted, ]); React.useEffect(() => { @@ -2955,20 +3260,14 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo className={cn( "relative pt-0 pb-4", isDesktopExpanded && 'flex h-full min-h-0 flex-col pt-4', - isMobile && isKeyboardOpen ? "ios-keyboard-safe-area" : "bottom-safe-area" + isMobile && 'bottom-safe-area' )} - data-keyboard-avoid="true" - style={isMobile && inputBarOffset > 0 && !isKeyboardOpen ? { marginBottom: `${inputBarOffset}px` } : undefined} + style={isMobile && inputBarOffset > 0 ? { marginBottom: `${inputBarOffset}px` } : undefined} > -
    +
    { - setMessage(content); - setTimeout(() => { - textareaRef.current?.focus(); - }, 0); - }} + onEditMessage={handleQueuedMessageEdit} /> {hasDrafts && (
    @@ -2979,7 +3278,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo borderColor: currentTheme?.colors?.interactive?.border, }} > - Review comments: + {t('chat.chatInput.reviewComments')} {draftCount} @@ -3005,7 +3304,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo #{linkedIssue.number} {linkedIssue.author && ( - by {linkedIssue.author.login} + {t('chat.chatInput.linked.byAuthor', { author: linkedIssue.author.login })} )} @@ -3018,7 +3317,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo rel="noopener noreferrer" onClick={(e) => e.stopPropagation()} className="flex items-center justify-center h-6 w-6 hover:bg-[var(--interactive-hover)] rounded-full transition-colors" - aria-label="Open issue in browser" + aria-label={t('chat.chatInput.linked.issue.openInBrowserAria')} > @@ -3028,7 +3327,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo setLinkedIssue(null); }} className="flex items-center justify-center h-6 w-6 hover:bg-[var(--interactive-hover)] rounded-full transition-colors cursor-pointer" - aria-label="Remove linked issue" + aria-label={t('chat.chatInput.linked.issue.removeAria')} > @@ -3051,9 +3350,9 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo /> )} - PR #{linkedPr.number} + {t('chat.chatInput.linked.pr.number', { number: linkedPr.number })} {linkedPr.author && ( - by {linkedPr.author.login} + {t('chat.chatInput.linked.byAuthor', { author: linkedPr.author.login })} )} @@ -3069,7 +3368,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo rel="noopener noreferrer" onClick={(e) => e.stopPropagation()} className="flex items-center justify-center h-6 w-6 hover:bg-[var(--interactive-hover)] rounded-full transition-colors" - aria-label="Open pull request in browser" + aria-label={t('chat.chatInput.linked.pr.openInBrowserAria')} > @@ -3079,7 +3378,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo setLinkedPr(null); }} className="flex items-center justify-center h-6 w-6 hover:bg-[var(--interactive-hover)] rounded-full transition-colors cursor-pointer" - aria-label="Remove linked pull request" + aria-label={t('chat.chatInput.linked.pr.removeAria')} > @@ -3087,17 +3386,11 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo
    )} - } /> {showDraftTargetSelectors && selectedDraftProject ? (
    @@ -3107,7 +3400,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo > {renderProjectLabelWithIcon(selectedDraftProject)} @@ -3129,16 +3422,16 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo > - {selectedDraftBranchLabel ?? 'Branch'} + {selectedDraftBranchLabel ?? t('chat.chatInput.branch')} {projectRootBranchOption ? ( - Project root + {t('chat.chatInput.projectRoot')} {projectRootBranchOption.label} @@ -3147,19 +3440,19 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo {projectRootBranchOption ? : null}
    - Worktrees + {t('chat.chatInput.worktrees')}
    {worktreeBranchOptions.map((option) => ( - {option.label} + {option.pending ? '⏳ ' : ''}{option.label} ))}
    @@ -3194,22 +3487,25 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo onDragOver={handleDragOver} onDragLeave={handleDragLeave} onDrop={handleDrop} + onDragEnd={handleDragEnd} > {isDragging && ( -
    +
    -

    Drop files here to attach

    +

    + {isInternalDrag ? t('chat.chatInput.drop.insertMention') : t('chat.chatInput.drop.attachFiles')} +

    )} @@ -3291,7 +3587,7 @@ export const ChatInput: React.FC = ({ onOpenSettings, scrollToBo : 'pt-4 pb-2', inputMode === 'shell' ? 'font-mono' : 'typography-markdown md:typography-ui-label', )} - style={{ transform: `translateY(-${textareaScrollTop}px)` }} + ref={composerHighlightRef} > {highlightedComposerContent.map((part, index) => ( = ({ onOpenSettings, scrollToBo
    )}