@myrialabs/clopen 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +6 -0
- package/.github/workflows/release.yml +60 -0
- package/.github/workflows/test.yml +40 -0
- package/CONTRIBUTING.md +499 -0
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/backend/index.ts +156 -0
- package/backend/lib/chat/helpers.ts +42 -0
- package/backend/lib/chat/index.ts +2 -0
- package/backend/lib/chat/stream-manager.ts +1126 -0
- package/backend/lib/database/README.md +77 -0
- package/backend/lib/database/index.ts +119 -0
- package/backend/lib/database/migrations/001_create_projects_table.ts +31 -0
- package/backend/lib/database/migrations/002_create_chat_sessions_table.ts +33 -0
- package/backend/lib/database/migrations/003_create_messages_table.ts +32 -0
- package/backend/lib/database/migrations/004_create_prompt_templates_table.ts +34 -0
- package/backend/lib/database/migrations/005_create_settings_table.ts +24 -0
- package/backend/lib/database/migrations/006_add_user_to_messages.ts +58 -0
- package/backend/lib/database/migrations/007_create_stream_states_table.ts +41 -0
- package/backend/lib/database/migrations/008_create_message_snapshots_table.ts +62 -0
- package/backend/lib/database/migrations/009_add_delta_snapshot_fields.ts +41 -0
- package/backend/lib/database/migrations/010_add_soft_delete_and_branch_support.ts +70 -0
- package/backend/lib/database/migrations/011_git_like_commit_graph.ts +156 -0
- package/backend/lib/database/migrations/012_add_file_change_statistics.ts +41 -0
- package/backend/lib/database/migrations/013_checkpoint_tree_state.ts +118 -0
- package/backend/lib/database/migrations/014_add_engine_to_sessions.ts +18 -0
- package/backend/lib/database/migrations/015_add_model_to_sessions.ts +18 -0
- package/backend/lib/database/migrations/016_create_user_projects_table.ts +34 -0
- package/backend/lib/database/migrations/017_add_current_session_to_user_projects.ts +32 -0
- package/backend/lib/database/migrations/018_create_claude_accounts_table.ts +24 -0
- package/backend/lib/database/migrations/019_add_claude_account_to_sessions.ts +18 -0
- package/backend/lib/database/migrations/020_add_snapshot_tree_hash.ts +32 -0
- package/backend/lib/database/migrations/021_drop_prompt_templates_table.ts +33 -0
- package/backend/lib/database/migrations/index.ts +154 -0
- package/backend/lib/database/queries/checkpoint-queries.ts +87 -0
- package/backend/lib/database/queries/engine-queries.ts +75 -0
- package/backend/lib/database/queries/index.ts +9 -0
- package/backend/lib/database/queries/message-queries.ts +472 -0
- package/backend/lib/database/queries/project-queries.ts +117 -0
- package/backend/lib/database/queries/session-queries.ts +271 -0
- package/backend/lib/database/queries/settings-queries.ts +34 -0
- package/backend/lib/database/queries/snapshot-queries.ts +326 -0
- package/backend/lib/database/queries/utils-queries.ts +59 -0
- package/backend/lib/database/seeders/index.ts +13 -0
- package/backend/lib/database/seeders/settings_seeder.ts +84 -0
- package/backend/lib/database/utils/connection.ts +174 -0
- package/backend/lib/database/utils/index.ts +4 -0
- package/backend/lib/database/utils/migration-runner.ts +118 -0
- package/backend/lib/database/utils/seeder-runner.ts +121 -0
- package/backend/lib/engine/adapters/claude/environment.ts +164 -0
- package/backend/lib/engine/adapters/claude/error-handler.ts +60 -0
- package/backend/lib/engine/adapters/claude/index.ts +1 -0
- package/backend/lib/engine/adapters/claude/path-utils.ts +38 -0
- package/backend/lib/engine/adapters/claude/stream.ts +177 -0
- package/backend/lib/engine/adapters/opencode/index.ts +2 -0
- package/backend/lib/engine/adapters/opencode/message-converter.ts +862 -0
- package/backend/lib/engine/adapters/opencode/server.ts +104 -0
- package/backend/lib/engine/adapters/opencode/stream.ts +755 -0
- package/backend/lib/engine/index.ts +196 -0
- package/backend/lib/engine/types.ts +58 -0
- package/backend/lib/files/file-operations.ts +478 -0
- package/backend/lib/files/file-reading.ts +308 -0
- package/backend/lib/files/file-watcher.ts +383 -0
- package/backend/lib/files/path-browsing.ts +382 -0
- package/backend/lib/git/git-executor.ts +88 -0
- package/backend/lib/git/git-parser.ts +411 -0
- package/backend/lib/git/git-service.ts +505 -0
- package/backend/lib/mcp/README.md +1144 -0
- package/backend/lib/mcp/config.ts +316 -0
- package/backend/lib/mcp/index.ts +35 -0
- package/backend/lib/mcp/project-context.ts +236 -0
- package/backend/lib/mcp/servers/browser-automation/actions.ts +156 -0
- package/backend/lib/mcp/servers/browser-automation/browser.ts +419 -0
- package/backend/lib/mcp/servers/browser-automation/index.ts +791 -0
- package/backend/lib/mcp/servers/browser-automation/inspection.ts +501 -0
- package/backend/lib/mcp/servers/helper.ts +143 -0
- package/backend/lib/mcp/servers/index.ts +45 -0
- package/backend/lib/mcp/servers/weather/get-temperature.ts +56 -0
- package/backend/lib/mcp/servers/weather/index.ts +31 -0
- package/backend/lib/mcp/stdio-server.ts +103 -0
- package/backend/lib/mcp/types.ts +65 -0
- package/backend/lib/preview/browser/browser-audio-capture.ts +86 -0
- package/backend/lib/preview/browser/browser-console-manager.ts +263 -0
- package/backend/lib/preview/browser/browser-dialog-handler.ts +222 -0
- package/backend/lib/preview/browser/browser-interaction-handler.ts +421 -0
- package/backend/lib/preview/browser/browser-mcp-control.ts +415 -0
- package/backend/lib/preview/browser/browser-native-ui-handler.ts +512 -0
- package/backend/lib/preview/browser/browser-navigation-tracker.ts +104 -0
- package/backend/lib/preview/browser/browser-pool.ts +357 -0
- package/backend/lib/preview/browser/browser-preview-service.ts +882 -0
- package/backend/lib/preview/browser/browser-tab-manager.ts +935 -0
- package/backend/lib/preview/browser/browser-video-capture.ts +695 -0
- package/backend/lib/preview/browser/scripts/audio-stream.ts +292 -0
- package/backend/lib/preview/browser/scripts/cursor-tracking.ts +85 -0
- package/backend/lib/preview/browser/scripts/video-stream.ts +438 -0
- package/backend/lib/preview/browser/types.ts +359 -0
- package/backend/lib/preview/index.ts +24 -0
- package/backend/lib/project/index.ts +2 -0
- package/backend/lib/project/status-manager.ts +182 -0
- package/backend/lib/shared/index.ts +2 -0
- package/backend/lib/shared/port-utils.ts +25 -0
- package/backend/lib/shared/process-manager.ts +281 -0
- package/backend/lib/snapshot/blob-store.ts +227 -0
- package/backend/lib/snapshot/gitignore.ts +307 -0
- package/backend/lib/snapshot/helpers.ts +397 -0
- package/backend/lib/snapshot/snapshot-service.ts +483 -0
- package/backend/lib/terminal/helpers.ts +14 -0
- package/backend/lib/terminal/index.ts +8 -0
- package/backend/lib/terminal/pty-manager.ts +4 -0
- package/backend/lib/terminal/pty-session-manager.ts +387 -0
- package/backend/lib/terminal/shell-utils.ts +313 -0
- package/backend/lib/terminal/stream-manager.ts +293 -0
- package/backend/lib/tunnel/global-tunnel-manager.ts +243 -0
- package/backend/lib/tunnel/project-tunnel-manager.ts +311 -0
- package/backend/lib/user/helpers.ts +87 -0
- package/backend/lib/utils/ws.ts +944 -0
- package/backend/lib/vite-dev.ts +353 -0
- package/backend/middleware/cors.ts +15 -0
- package/backend/middleware/error-handler.ts +49 -0
- package/backend/middleware/logger.ts +9 -0
- package/backend/types/api.ts +24 -0
- package/backend/ws/README.md +1505 -0
- package/backend/ws/chat/background.ts +198 -0
- package/backend/ws/chat/index.ts +21 -0
- package/backend/ws/chat/stream.ts +707 -0
- package/backend/ws/engine/claude/accounts.ts +401 -0
- package/backend/ws/engine/claude/index.ts +13 -0
- package/backend/ws/engine/claude/status.ts +43 -0
- package/backend/ws/engine/index.ts +14 -0
- package/backend/ws/engine/opencode/index.ts +11 -0
- package/backend/ws/engine/opencode/status.ts +30 -0
- package/backend/ws/engine/utils.ts +36 -0
- package/backend/ws/files/index.ts +30 -0
- package/backend/ws/files/read.ts +189 -0
- package/backend/ws/files/search.ts +453 -0
- package/backend/ws/files/watch.ts +124 -0
- package/backend/ws/files/write.ts +143 -0
- package/backend/ws/git/branch.ts +106 -0
- package/backend/ws/git/commit.ts +39 -0
- package/backend/ws/git/conflict.ts +68 -0
- package/backend/ws/git/diff.ts +69 -0
- package/backend/ws/git/index.ts +24 -0
- package/backend/ws/git/log.ts +41 -0
- package/backend/ws/git/remote.ts +214 -0
- package/backend/ws/git/staging.ts +84 -0
- package/backend/ws/git/status.ts +90 -0
- package/backend/ws/index.ts +69 -0
- package/backend/ws/mcp/index.ts +61 -0
- package/backend/ws/messages/crud.ts +74 -0
- package/backend/ws/messages/index.ts +14 -0
- package/backend/ws/preview/browser/cleanup.ts +129 -0
- package/backend/ws/preview/browser/console.ts +114 -0
- package/backend/ws/preview/browser/interact.ts +513 -0
- package/backend/ws/preview/browser/mcp.ts +129 -0
- package/backend/ws/preview/browser/native-ui.ts +235 -0
- package/backend/ws/preview/browser/stats.ts +55 -0
- package/backend/ws/preview/browser/tab-info.ts +126 -0
- package/backend/ws/preview/browser/tab.ts +166 -0
- package/backend/ws/preview/browser/webcodecs.ts +293 -0
- package/backend/ws/preview/index.ts +146 -0
- package/backend/ws/projects/crud.ts +113 -0
- package/backend/ws/projects/index.ts +25 -0
- package/backend/ws/projects/presence.ts +46 -0
- package/backend/ws/projects/status.ts +116 -0
- package/backend/ws/sessions/crud.ts +327 -0
- package/backend/ws/sessions/index.ts +33 -0
- package/backend/ws/settings/crud.ts +112 -0
- package/backend/ws/settings/index.ts +14 -0
- package/backend/ws/snapshot/index.ts +17 -0
- package/backend/ws/snapshot/restore.ts +173 -0
- package/backend/ws/snapshot/timeline.ts +141 -0
- package/backend/ws/system/index.ts +14 -0
- package/backend/ws/system/operations.ts +49 -0
- package/backend/ws/terminal/index.ts +40 -0
- package/backend/ws/terminal/persistence.ts +153 -0
- package/backend/ws/terminal/session.ts +382 -0
- package/backend/ws/terminal/stream.ts +79 -0
- package/backend/ws/tunnel/index.ts +14 -0
- package/backend/ws/tunnel/operations.ts +91 -0
- package/backend/ws/types.ts +20 -0
- package/backend/ws/user/crud.ts +156 -0
- package/backend/ws/user/index.ts +14 -0
- package/bin/clopen.ts +307 -0
- package/bun.lock +1352 -0
- package/frontend/App.svelte +34 -0
- package/frontend/app.css +313 -0
- package/frontend/lib/app-environment.ts +10 -0
- package/frontend/lib/components/chat/ChatInterface.svelte +407 -0
- package/frontend/lib/components/chat/formatters/ErrorMessage.svelte +57 -0
- package/frontend/lib/components/chat/formatters/MessageFormatter.svelte +224 -0
- package/frontend/lib/components/chat/formatters/TextMessage.svelte +395 -0
- package/frontend/lib/components/chat/formatters/Tools.svelte +70 -0
- package/frontend/lib/components/chat/formatters/index.ts +3 -0
- package/frontend/lib/components/chat/input/ChatInput.svelte +421 -0
- package/frontend/lib/components/chat/input/components/ChatInputActions.svelte +78 -0
- package/frontend/lib/components/chat/input/components/DragDropOverlay.svelte +30 -0
- package/frontend/lib/components/chat/input/components/EditModeIndicator.svelte +33 -0
- package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +619 -0
- package/frontend/lib/components/chat/input/components/FileAttachmentPreview.svelte +48 -0
- package/frontend/lib/components/chat/input/components/LoadingIndicator.svelte +31 -0
- package/frontend/lib/components/chat/input/composables/use-animations.svelte.ts +201 -0
- package/frontend/lib/components/chat/input/composables/use-chat-actions.svelte.ts +148 -0
- package/frontend/lib/components/chat/input/composables/use-file-handling.svelte.ts +216 -0
- package/frontend/lib/components/chat/input/composables/use-input-state.svelte.ts +357 -0
- package/frontend/lib/components/chat/input/composables/use-textarea-resize.svelte.ts +57 -0
- package/frontend/lib/components/chat/message/ChatMessage.svelte +478 -0
- package/frontend/lib/components/chat/message/ChatMessages.svelte +541 -0
- package/frontend/lib/components/chat/message/DateSeparator.svelte +86 -0
- package/frontend/lib/components/chat/message/MessageBubble.svelte +86 -0
- package/frontend/lib/components/chat/message/MessageHeader.svelte +157 -0
- package/frontend/lib/components/chat/modal/DebugModal.svelte +59 -0
- package/frontend/lib/components/chat/modal/TokenUsageModal.svelte +124 -0
- package/frontend/lib/components/chat/shared/index.ts +2 -0
- package/frontend/lib/components/chat/shared/utils.ts +116 -0
- package/frontend/lib/components/chat/tools/BashOutputTool.svelte +35 -0
- package/frontend/lib/components/chat/tools/BashTool.svelte +46 -0
- package/frontend/lib/components/chat/tools/CustomMcpTool.svelte +139 -0
- package/frontend/lib/components/chat/tools/EditTool.svelte +48 -0
- package/frontend/lib/components/chat/tools/ExitPlanModeTool.svelte +32 -0
- package/frontend/lib/components/chat/tools/GlobTool.svelte +51 -0
- package/frontend/lib/components/chat/tools/GrepTool.svelte +90 -0
- package/frontend/lib/components/chat/tools/KillShellTool.svelte +26 -0
- package/frontend/lib/components/chat/tools/ListMcpResourcesTool.svelte +31 -0
- package/frontend/lib/components/chat/tools/NotebookEditTool.svelte +38 -0
- package/frontend/lib/components/chat/tools/ReadMcpResourceTool.svelte +34 -0
- package/frontend/lib/components/chat/tools/ReadTool.svelte +41 -0
- package/frontend/lib/components/chat/tools/TaskTool.svelte +64 -0
- package/frontend/lib/components/chat/tools/TodoWriteTool.svelte +75 -0
- package/frontend/lib/components/chat/tools/WebFetchTool.svelte +35 -0
- package/frontend/lib/components/chat/tools/WebSearchTool.svelte +84 -0
- package/frontend/lib/components/chat/tools/WriteTool.svelte +33 -0
- package/frontend/lib/components/chat/tools/components/CodeBlock.svelte +79 -0
- package/frontend/lib/components/chat/tools/components/DiffBlock.svelte +408 -0
- package/frontend/lib/components/chat/tools/components/FileHeader.svelte +45 -0
- package/frontend/lib/components/chat/tools/components/InfoLine.svelte +19 -0
- package/frontend/lib/components/chat/tools/components/StatsBadges.svelte +27 -0
- package/frontend/lib/components/chat/tools/components/TerminalCommand.svelte +54 -0
- package/frontend/lib/components/chat/tools/components/index.ts +7 -0
- package/frontend/lib/components/chat/tools/index.ts +26 -0
- package/frontend/lib/components/chat/widgets/FloatingTodoList.svelte +249 -0
- package/frontend/lib/components/chat/widgets/TokenUsage.svelte +78 -0
- package/frontend/lib/components/checkpoint/TimelineModal.svelte +391 -0
- package/frontend/lib/components/checkpoint/timeline/TimelineEdge.svelte +26 -0
- package/frontend/lib/components/checkpoint/timeline/TimelineGraph.svelte +87 -0
- package/frontend/lib/components/checkpoint/timeline/TimelineNode.svelte +108 -0
- package/frontend/lib/components/checkpoint/timeline/TimelineVersionGroup.svelte +59 -0
- package/frontend/lib/components/checkpoint/timeline/animation.ts +168 -0
- package/frontend/lib/components/checkpoint/timeline/config.ts +44 -0
- package/frontend/lib/components/checkpoint/timeline/graph-builder.ts +304 -0
- package/frontend/lib/components/checkpoint/timeline/types.ts +65 -0
- package/frontend/lib/components/checkpoint/timeline/utils.ts +53 -0
- package/frontend/lib/components/common/Alert.svelte +139 -0
- package/frontend/lib/components/common/AvatarBubble.svelte +56 -0
- package/frontend/lib/components/common/Button.svelte +71 -0
- package/frontend/lib/components/common/Card.svelte +102 -0
- package/frontend/lib/components/common/Checkbox.svelte +48 -0
- package/frontend/lib/components/common/Dialog.svelte +249 -0
- package/frontend/lib/components/common/FolderBrowser.svelte +843 -0
- package/frontend/lib/components/common/Icon.svelte +58 -0
- package/frontend/lib/components/common/Input.svelte +72 -0
- package/frontend/lib/components/common/Lightbox.svelte +233 -0
- package/frontend/lib/components/common/LoadingScreen.svelte +52 -0
- package/frontend/lib/components/common/LoadingSpinner.svelte +48 -0
- package/frontend/lib/components/common/Modal.svelte +177 -0
- package/frontend/lib/components/common/ModalProvider.svelte +28 -0
- package/frontend/lib/components/common/ModelSelector.svelte +110 -0
- package/frontend/lib/components/common/MonacoEditor.svelte +569 -0
- package/frontend/lib/components/common/NotificationToast.svelte +113 -0
- package/frontend/lib/components/common/PageTemplate.svelte +76 -0
- package/frontend/lib/components/common/ProjectUserAvatars.svelte +79 -0
- package/frontend/lib/components/common/Select.svelte +98 -0
- package/frontend/lib/components/common/Textarea.svelte +80 -0
- package/frontend/lib/components/common/ThemeToggle.svelte +44 -0
- package/frontend/lib/components/common/lucide-icons.ts +1642 -0
- package/frontend/lib/components/common/material-icons.ts +1082 -0
- package/frontend/lib/components/common/xterm/XTerm.svelte +796 -0
- package/frontend/lib/components/common/xterm/index.ts +16 -0
- package/frontend/lib/components/common/xterm/terminal-config.ts +68 -0
- package/frontend/lib/components/common/xterm/types.ts +31 -0
- package/frontend/lib/components/common/xterm/xterm-service.ts +353 -0
- package/frontend/lib/components/files/FileNode.svelte +384 -0
- package/frontend/lib/components/files/FileTree.svelte +681 -0
- package/frontend/lib/components/files/FileViewer.svelte +728 -0
- package/frontend/lib/components/files/SearchResults.svelte +303 -0
- package/frontend/lib/components/git/BranchManager.svelte +458 -0
- package/frontend/lib/components/git/ChangesSection.svelte +107 -0
- package/frontend/lib/components/git/CommitForm.svelte +76 -0
- package/frontend/lib/components/git/ConflictResolver.svelte +158 -0
- package/frontend/lib/components/git/DiffViewer.svelte +364 -0
- package/frontend/lib/components/git/FileChangeItem.svelte +97 -0
- package/frontend/lib/components/git/GitButton.svelte +33 -0
- package/frontend/lib/components/git/GitLog.svelte +361 -0
- package/frontend/lib/components/git/GitModal.svelte +80 -0
- package/frontend/lib/components/history/HistoryModal.svelte +563 -0
- package/frontend/lib/components/history/HistoryView.svelte +615 -0
- package/frontend/lib/components/index.ts +34 -0
- package/frontend/lib/components/preview/browser/BrowserPreview.svelte +549 -0
- package/frontend/lib/components/preview/browser/components/Canvas.svelte +1058 -0
- package/frontend/lib/components/preview/browser/components/ConsolePanel.svelte +757 -0
- package/frontend/lib/components/preview/browser/components/Container.svelte +450 -0
- package/frontend/lib/components/preview/browser/components/ContextMenu.svelte +236 -0
- package/frontend/lib/components/preview/browser/components/SelectDropdown.svelte +224 -0
- package/frontend/lib/components/preview/browser/components/Toolbar.svelte +339 -0
- package/frontend/lib/components/preview/browser/components/VirtualCursor.svelte +36 -0
- package/frontend/lib/components/preview/browser/core/cleanup.svelte.ts +155 -0
- package/frontend/lib/components/preview/browser/core/coordinator.svelte.ts +837 -0
- package/frontend/lib/components/preview/browser/core/interactions.svelte.ts +113 -0
- package/frontend/lib/components/preview/browser/core/mcp-handlers.svelte.ts +296 -0
- package/frontend/lib/components/preview/browser/core/native-ui-handlers.svelte.ts +391 -0
- package/frontend/lib/components/preview/browser/core/stream-handler.svelte.ts +231 -0
- package/frontend/lib/components/preview/browser/core/tab-manager.svelte.ts +210 -0
- package/frontend/lib/components/preview/browser/core/tab-operations.svelte.ts +239 -0
- package/frontend/lib/components/preview/index.ts +2 -0
- package/frontend/lib/components/settings/SettingsModal.svelte +235 -0
- package/frontend/lib/components/settings/SettingsView.svelte +36 -0
- package/frontend/lib/components/settings/appearance/AppearanceSettings.svelte +51 -0
- package/frontend/lib/components/settings/appearance/LayoutPresetSettings.svelte +160 -0
- package/frontend/lib/components/settings/appearance/LayoutPreview.svelte +76 -0
- package/frontend/lib/components/settings/engines/AIEnginesSettings.svelte +917 -0
- package/frontend/lib/components/settings/general/AdvancedSettings.svelte +187 -0
- package/frontend/lib/components/settings/general/DataManagementSettings.svelte +203 -0
- package/frontend/lib/components/settings/general/GeneralSettings.svelte +10 -0
- package/frontend/lib/components/settings/model/ModelSettings.svelte +357 -0
- package/frontend/lib/components/settings/notifications/NotificationSettings.svelte +205 -0
- package/frontend/lib/components/settings/user/UserSettings.svelte +197 -0
- package/frontend/lib/components/terminal/Terminal.svelte +368 -0
- package/frontend/lib/components/terminal/TerminalTabs.svelte +87 -0
- package/frontend/lib/components/terminal/TerminalView.svelte +55 -0
- package/frontend/lib/components/tunnel/TunnelActive.svelte +142 -0
- package/frontend/lib/components/tunnel/TunnelButton.svelte +54 -0
- package/frontend/lib/components/tunnel/TunnelInactive.svelte +284 -0
- package/frontend/lib/components/tunnel/TunnelModal.svelte +47 -0
- package/frontend/lib/components/tunnel/TunnelQRCode.svelte +49 -0
- package/frontend/lib/components/workspace/DesktopNavigator.svelte +382 -0
- package/frontend/lib/components/workspace/MobileNavigator.svelte +403 -0
- package/frontend/lib/components/workspace/PanelContainer.svelte +100 -0
- package/frontend/lib/components/workspace/PanelHeader.svelte +505 -0
- package/frontend/lib/components/workspace/ViewMenu.svelte +162 -0
- package/frontend/lib/components/workspace/WorkspaceLayout.svelte +169 -0
- package/frontend/lib/components/workspace/layout/DesktopLayout.svelte +15 -0
- package/frontend/lib/components/workspace/layout/MobileLayout.svelte +17 -0
- package/frontend/lib/components/workspace/layout/split-pane/Container.svelte +42 -0
- package/frontend/lib/components/workspace/layout/split-pane/Handle.svelte +85 -0
- package/frontend/lib/components/workspace/layout/split-pane/Layout.svelte +37 -0
- package/frontend/lib/components/workspace/panels/ChatPanel.svelte +274 -0
- package/frontend/lib/components/workspace/panels/FilesPanel.svelte +1261 -0
- package/frontend/lib/components/workspace/panels/GitPanel.svelte +1560 -0
- package/frontend/lib/components/workspace/panels/PreviewPanel.svelte +150 -0
- package/frontend/lib/components/workspace/panels/TerminalPanel.svelte +73 -0
- package/frontend/lib/constants/preview.ts +45 -0
- package/frontend/lib/services/chat/chat.service.ts +704 -0
- package/frontend/lib/services/chat/index.ts +7 -0
- package/frontend/lib/services/notification/global-stream-monitor.ts +86 -0
- package/frontend/lib/services/notification/index.ts +8 -0
- package/frontend/lib/services/notification/push.service.ts +144 -0
- package/frontend/lib/services/notification/sound.service.ts +127 -0
- package/frontend/lib/services/preview/browser/browser-console.service.ts +61 -0
- package/frontend/lib/services/preview/browser/browser-webcodecs.service.ts +1499 -0
- package/frontend/lib/services/preview/browser/mcp-integration.svelte.ts +67 -0
- package/frontend/lib/services/preview/index.ts +23 -0
- package/frontend/lib/services/project/index.ts +8 -0
- package/frontend/lib/services/project/status.service.ts +159 -0
- package/frontend/lib/services/snapshot/snapshot.service.ts +47 -0
- package/frontend/lib/services/terminal/background/index.ts +130 -0
- package/frontend/lib/services/terminal/background/session-restore.ts +274 -0
- package/frontend/lib/services/terminal/background/stream-manager.ts +286 -0
- package/frontend/lib/services/terminal/index.ts +14 -0
- package/frontend/lib/services/terminal/persistence.service.ts +260 -0
- package/frontend/lib/services/terminal/project.service.ts +953 -0
- package/frontend/lib/services/terminal/session.service.ts +364 -0
- package/frontend/lib/services/terminal/terminal.service.ts +369 -0
- package/frontend/lib/stores/core/app.svelte.ts +117 -0
- package/frontend/lib/stores/core/files.svelte.ts +73 -0
- package/frontend/lib/stores/core/presence.svelte.ts +48 -0
- package/frontend/lib/stores/core/projects.svelte.ts +317 -0
- package/frontend/lib/stores/core/sessions.svelte.ts +383 -0
- package/frontend/lib/stores/features/claude-accounts.svelte.ts +58 -0
- package/frontend/lib/stores/features/models.svelte.ts +89 -0
- package/frontend/lib/stores/features/settings.svelte.ts +87 -0
- package/frontend/lib/stores/features/terminal.svelte.ts +701 -0
- package/frontend/lib/stores/features/tunnel.svelte.ts +161 -0
- package/frontend/lib/stores/features/user.svelte.ts +96 -0
- package/frontend/lib/stores/ui/chat-input.svelte.ts +57 -0
- package/frontend/lib/stores/ui/chat-model.svelte.ts +61 -0
- package/frontend/lib/stores/ui/dialog.svelte.ts +59 -0
- package/frontend/lib/stores/ui/edit-mode.svelte.ts +214 -0
- package/frontend/lib/stores/ui/notification.svelte.ts +166 -0
- package/frontend/lib/stores/ui/settings-modal.svelte.ts +88 -0
- package/frontend/lib/stores/ui/theme.svelte.ts +179 -0
- package/frontend/lib/stores/ui/workspace.svelte.ts +754 -0
- package/frontend/lib/types/native-ui.ts +73 -0
- package/frontend/lib/utils/chat/date-separator.ts +39 -0
- package/frontend/lib/utils/chat/message-grouper.ts +219 -0
- package/frontend/lib/utils/chat/message-processor.ts +135 -0
- package/frontend/lib/utils/chat/tool-handler.ts +161 -0
- package/frontend/lib/utils/chat/virtual-scroll.svelte.ts +142 -0
- package/frontend/lib/utils/click-outside.ts +20 -0
- package/frontend/lib/utils/context-manager.ts +257 -0
- package/frontend/lib/utils/file-icon-mappings.ts +769 -0
- package/frontend/lib/utils/folder-icon-mappings.ts +1030 -0
- package/frontend/lib/utils/git-status.ts +68 -0
- package/frontend/lib/utils/platform.ts +113 -0
- package/frontend/lib/utils/port-check.ts +65 -0
- package/frontend/lib/utils/terminalFormatter.ts +207 -0
- package/frontend/lib/utils/theme.ts +6 -0
- package/frontend/lib/utils/tree-visualizer.ts +320 -0
- package/frontend/lib/utils/ws.ts +44 -0
- package/frontend/main.ts +13 -0
- package/index.html +70 -0
- package/package.json +111 -0
- package/scripts/generate-icons.ts +87 -0
- package/scripts/pre-publish-check.sh +142 -0
- package/scripts/setup-hooks.sh +134 -0
- package/scripts/validate-branch-name.sh +47 -0
- package/scripts/validate-commit-msg.sh +42 -0
- package/shared/constants/engines.ts +134 -0
- package/shared/types/database/connection.ts +16 -0
- package/shared/types/database/index.ts +6 -0
- package/shared/types/database/schema.ts +141 -0
- package/shared/types/engine/index.ts +45 -0
- package/shared/types/filesystem/index.ts +22 -0
- package/shared/types/git.ts +171 -0
- package/shared/types/messaging/index.ts +239 -0
- package/shared/types/messaging/tool.ts +526 -0
- package/shared/types/network/api.ts +18 -0
- package/shared/types/network/index.ts +5 -0
- package/shared/types/stores/app.ts +23 -0
- package/shared/types/stores/dialog.ts +21 -0
- package/shared/types/stores/index.ts +3 -0
- package/shared/types/stores/settings.ts +15 -0
- package/shared/types/terminal/index.ts +44 -0
- package/shared/types/ui/components.ts +61 -0
- package/shared/types/ui/icons.ts +23 -0
- package/shared/types/ui/index.ts +22 -0
- package/shared/types/ui/notifications.ts +14 -0
- package/shared/types/ui/theme.ts +12 -0
- package/shared/types/websocket/index.ts +43 -0
- package/shared/types/window.d.ts +13 -0
- package/shared/utils/anonymous-user.ts +168 -0
- package/shared/utils/async.ts +10 -0
- package/shared/utils/diff-calculator.ts +184 -0
- package/shared/utils/file-type-detection.ts +166 -0
- package/shared/utils/logger.ts +158 -0
- package/shared/utils/message-formatter.ts +79 -0
- package/shared/utils/path.ts +47 -0
- package/shared/utils/ws-client.ts +768 -0
- package/shared/utils/ws-server.ts +660 -0
- package/static/audio/notification.ogg +0 -0
- package/static/favicon.svg +8 -0
- package/static/fonts/dm-sans/dm-sans-italic-latin-ext.woff2 +0 -0
- package/static/fonts/dm-sans/dm-sans-italic-latin.woff2 +0 -0
- package/static/fonts/dm-sans/dm-sans-normal-latin-ext.woff2 +0 -0
- package/static/fonts/dm-sans/dm-sans-normal-latin.woff2 +0 -0
- package/static/fonts/dm-sans.css +96 -0
- package/svelte.config.js +20 -0
- package/vite.config.ts +33 -0
|
@@ -0,0 +1,1261 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
// Persistent state that survives component destruction (mobile/desktop switch)
|
|
3
|
+
const projectFileStates = new Map<string, any>();
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
import { projectState } from '$frontend/lib/stores/core/projects.svelte';
|
|
8
|
+
import FileTree from '$frontend/lib/components/files/FileTree.svelte';
|
|
9
|
+
import FileViewer from '$frontend/lib/components/files/FileViewer.svelte';
|
|
10
|
+
import Icon from '$frontend/lib/components/common/Icon.svelte';
|
|
11
|
+
import Alert from '$frontend/lib/components/common/Alert.svelte';
|
|
12
|
+
import Dialog from '$frontend/lib/components/common/Dialog.svelte';
|
|
13
|
+
import type { FileNode } from '$shared/types/filesystem';
|
|
14
|
+
import { debug } from '$shared/utils/logger';
|
|
15
|
+
import { onMount, onDestroy } from 'svelte';
|
|
16
|
+
import ws from '$frontend/lib/utils/ws';
|
|
17
|
+
import { showConfirm } from '$frontend/lib/stores/ui/dialog.svelte';
|
|
18
|
+
import { getFileIcon } from '$frontend/lib/utils/file-icon-mappings';
|
|
19
|
+
import type { IconName } from '$shared/types/ui/icons';
|
|
20
|
+
|
|
21
|
+
// Props
|
|
22
|
+
interface Props {
|
|
23
|
+
showMobileHeader?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { showMobileHeader = false }: Props = $props();
|
|
27
|
+
|
|
28
|
+
// State
|
|
29
|
+
const hasActiveProject = $derived(projectState.currentProject !== null);
|
|
30
|
+
const projectPath = $derived(projectState.currentProject?.path || '');
|
|
31
|
+
const projectId = $derived(projectState.currentProject?.id || '');
|
|
32
|
+
|
|
33
|
+
// File watcher state
|
|
34
|
+
let isWatching = $state(false);
|
|
35
|
+
let watchDebounceTimer = $state<ReturnType<typeof setTimeout> | null>(null);
|
|
36
|
+
|
|
37
|
+
let projectFiles = $state<FileNode[]>([]);
|
|
38
|
+
let isLoading = $state(false);
|
|
39
|
+
let isInitialLoad = $state(true);
|
|
40
|
+
let error = $state('');
|
|
41
|
+
let expandedFolders = $state(new Set<string>());
|
|
42
|
+
let viewMode = $state<'tree' | 'viewer'>('tree');
|
|
43
|
+
|
|
44
|
+
// ============================
|
|
45
|
+
// Tab System
|
|
46
|
+
// ============================
|
|
47
|
+
interface EditorTab {
|
|
48
|
+
file: FileNode;
|
|
49
|
+
currentContent: string;
|
|
50
|
+
savedContent: string;
|
|
51
|
+
isLoading: boolean;
|
|
52
|
+
externallyChanged?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let openTabs = $state<EditorTab[]>([]);
|
|
56
|
+
let activeTabPath = $state<string | null>(null);
|
|
57
|
+
let wordWrapEnabled = $state(false);
|
|
58
|
+
|
|
59
|
+
const activeTab = $derived(openTabs.find(t => t.file.path === activeTabPath) || null);
|
|
60
|
+
const modifiedFilePaths = $derived(new Set(
|
|
61
|
+
openTabs.filter(t => t.currentContent !== t.savedContent).map(t => t.file.path)
|
|
62
|
+
));
|
|
63
|
+
|
|
64
|
+
// Display content for FileViewer (only updated on tab switch/load, not on every keystroke)
|
|
65
|
+
let displayContent = $state('');
|
|
66
|
+
let displaySavedContent = $state('');
|
|
67
|
+
let displayFile = $state<FileNode | null>(null);
|
|
68
|
+
let displayLoading = $state(false);
|
|
69
|
+
let displayTargetLine = $state<number | undefined>(undefined);
|
|
70
|
+
let displayExternallyChanged = $state(false);
|
|
71
|
+
|
|
72
|
+
// Sync display state when active tab changes
|
|
73
|
+
$effect(() => {
|
|
74
|
+
if (activeTab) {
|
|
75
|
+
displayFile = activeTab.file;
|
|
76
|
+
displayContent = activeTab.currentContent;
|
|
77
|
+
displaySavedContent = activeTab.savedContent;
|
|
78
|
+
displayLoading = activeTab.isLoading;
|
|
79
|
+
displayExternallyChanged = activeTab.externallyChanged || false;
|
|
80
|
+
} else {
|
|
81
|
+
displayFile = null;
|
|
82
|
+
displayContent = '';
|
|
83
|
+
displaySavedContent = '';
|
|
84
|
+
displayLoading = false;
|
|
85
|
+
displayExternallyChanged = false;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Dialog state (replaces manual modals)
|
|
90
|
+
let dialogOpen = $state(false);
|
|
91
|
+
let dialogType = $state<'rename' | 'new-file' | 'new-folder'>('rename');
|
|
92
|
+
let dialogValue = $state('');
|
|
93
|
+
let dialogTargetFile = $state<FileNode | null>(null);
|
|
94
|
+
let dialogParentPath = $state<string | null>(null);
|
|
95
|
+
|
|
96
|
+
const dialogConfig = $derived.by(() => {
|
|
97
|
+
switch (dialogType) {
|
|
98
|
+
case 'rename':
|
|
99
|
+
return {
|
|
100
|
+
title: `Rename ${dialogTargetFile?.type === 'directory' ? 'Folder' : 'File'}`,
|
|
101
|
+
message: 'Enter the new name:',
|
|
102
|
+
placeholder: dialogTargetFile?.name || '',
|
|
103
|
+
confirmText: 'Rename'
|
|
104
|
+
};
|
|
105
|
+
case 'new-file':
|
|
106
|
+
return {
|
|
107
|
+
title: 'Create New File',
|
|
108
|
+
message: 'Enter the name for the new file:',
|
|
109
|
+
placeholder: 'filename.txt',
|
|
110
|
+
confirmText: 'Create'
|
|
111
|
+
};
|
|
112
|
+
case 'new-folder':
|
|
113
|
+
return {
|
|
114
|
+
title: 'Create New Folder',
|
|
115
|
+
message: 'Enter the name for the new folder:',
|
|
116
|
+
placeholder: 'folder-name',
|
|
117
|
+
confirmText: 'Create'
|
|
118
|
+
};
|
|
119
|
+
default:
|
|
120
|
+
return {
|
|
121
|
+
title: 'Input',
|
|
122
|
+
message: 'Enter value:',
|
|
123
|
+
placeholder: '',
|
|
124
|
+
confirmText: 'OK'
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Alert state
|
|
130
|
+
let showAlert = $state(false);
|
|
131
|
+
let alertMessage = $state('');
|
|
132
|
+
let alertTitle = $state('Error');
|
|
133
|
+
let alertType = $state<'info' | 'success' | 'warning' | 'error'>('error');
|
|
134
|
+
|
|
135
|
+
// projectFileStates is at module level to survive component destruction (mobile/desktop switch)
|
|
136
|
+
let lastProjectPath = $state<string>('');
|
|
137
|
+
|
|
138
|
+
// Container width detection for 2-column layout
|
|
139
|
+
let containerRef = $state<HTMLDivElement | null>(null);
|
|
140
|
+
let containerWidth = $state(0);
|
|
141
|
+
const TWO_COLUMN_THRESHOLD = 800;
|
|
142
|
+
|
|
143
|
+
// FileTree ref
|
|
144
|
+
let fileTreeRef = $state<any>(null);
|
|
145
|
+
const isTwoColumnMode = $derived(containerWidth >= TWO_COLUMN_THRESHOLD);
|
|
146
|
+
|
|
147
|
+
// Tree state preservation
|
|
148
|
+
let treeScrollContainer = $state<HTMLElement | null>(null);
|
|
149
|
+
let savedScrollPosition = 0;
|
|
150
|
+
|
|
151
|
+
// Cache key for sessionStorage
|
|
152
|
+
const getCacheKey = (path: string) => `files_cache_${path}`;
|
|
153
|
+
|
|
154
|
+
function saveFilesToCache(path: string, files: FileNode[]) {
|
|
155
|
+
try {
|
|
156
|
+
const cacheData = { files, timestamp: Date.now() };
|
|
157
|
+
sessionStorage.setItem(getCacheKey(path), JSON.stringify(cacheData));
|
|
158
|
+
} catch (err) {
|
|
159
|
+
debug.error('file', 'Failed to save files cache:', err);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function loadFilesFromCache(path: string): FileNode[] | null {
|
|
164
|
+
try {
|
|
165
|
+
const cached = sessionStorage.getItem(getCacheKey(path));
|
|
166
|
+
if (!cached) return null;
|
|
167
|
+
const cacheData = JSON.parse(cached);
|
|
168
|
+
if (Date.now() - cacheData.timestamp > 3600000) {
|
|
169
|
+
sessionStorage.removeItem(getCacheKey(path));
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
const convertDates = (file: any): FileNode => ({
|
|
173
|
+
...file,
|
|
174
|
+
modified: new Date(file.modified),
|
|
175
|
+
children: file.children?.map(convertDates)
|
|
176
|
+
});
|
|
177
|
+
return cacheData.files.map(convertDates);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
debug.error('file', 'Failed to load files cache:', err);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ============================
|
|
185
|
+
// File Loading
|
|
186
|
+
// ============================
|
|
187
|
+
|
|
188
|
+
async function loadProjectFiles(preserveState = false) {
|
|
189
|
+
if (!hasActiveProject) {
|
|
190
|
+
projectFiles = [];
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (isInitialLoad && !preserveState) {
|
|
195
|
+
const cachedFiles = loadFilesFromCache(projectPath);
|
|
196
|
+
if (cachedFiles) {
|
|
197
|
+
projectFiles = cachedFiles;
|
|
198
|
+
debug.log('file', 'Loaded files from cache, fetching fresh data...');
|
|
199
|
+
isLoading = false;
|
|
200
|
+
} else {
|
|
201
|
+
isLoading = true;
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
if (!preserveState) {
|
|
205
|
+
isLoading = true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let savedExpandedFolders: Set<string> | null = null;
|
|
210
|
+
if (preserveState) {
|
|
211
|
+
savedExpandedFolders = new Set(expandedFolders);
|
|
212
|
+
if (treeScrollContainer) {
|
|
213
|
+
savedScrollPosition = treeScrollContainer.scrollTop;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
error = '';
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
const requestData: any = { project_path: projectPath };
|
|
221
|
+
if (preserveState && savedExpandedFolders && savedExpandedFolders.size > 0) {
|
|
222
|
+
requestData.expanded = Array.from(savedExpandedFolders).join(',');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const data = await ws.http('files:list-tree', requestData);
|
|
226
|
+
|
|
227
|
+
const convertToFileNode = (apiFile: unknown): FileNode => {
|
|
228
|
+
if (typeof apiFile !== 'object' || apiFile === null) {
|
|
229
|
+
throw new Error('Invalid file object');
|
|
230
|
+
}
|
|
231
|
+
const file = apiFile as Record<string, unknown>;
|
|
232
|
+
return {
|
|
233
|
+
name: String(file.name || ''),
|
|
234
|
+
path: String(file.path || ''),
|
|
235
|
+
type: file.type === 'directory' ? 'directory' : 'file',
|
|
236
|
+
size: typeof file.size === 'number' ? file.size : 0,
|
|
237
|
+
modified: file.modified ? new Date(String(file.modified)) : new Date(),
|
|
238
|
+
children: Array.isArray(file.children) ? file.children.map(convertToFileNode) : undefined
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const rootFile = convertToFileNode(data);
|
|
243
|
+
projectFiles = rootFile.type === 'directory' && rootFile.children ? rootFile.children : [rootFile];
|
|
244
|
+
|
|
245
|
+
saveFilesToCache(projectPath, projectFiles);
|
|
246
|
+
|
|
247
|
+
if (isInitialLoad) {
|
|
248
|
+
isInitialLoad = false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (preserveState && savedExpandedFolders) {
|
|
252
|
+
expandedFolders = savedExpandedFolders;
|
|
253
|
+
requestAnimationFrame(() => {
|
|
254
|
+
requestAnimationFrame(() => {
|
|
255
|
+
if (treeScrollContainer) {
|
|
256
|
+
treeScrollContainer.scrollTop = savedScrollPosition;
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
} catch (err) {
|
|
262
|
+
error = err instanceof Error ? err.message : 'Failed to load files';
|
|
263
|
+
projectFiles = [];
|
|
264
|
+
} finally {
|
|
265
|
+
isLoading = false;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function loadDirectoryContents(dirPath: string): Promise<FileNode[]> {
|
|
270
|
+
try {
|
|
271
|
+
const data = await ws.http('files:list-directory', { dir_path: dirPath });
|
|
272
|
+
if (Array.isArray(data)) {
|
|
273
|
+
return data.map((item: any) => ({
|
|
274
|
+
...item,
|
|
275
|
+
modified: new Date(item.modified)
|
|
276
|
+
}));
|
|
277
|
+
}
|
|
278
|
+
return [];
|
|
279
|
+
} catch (error) {
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function updateFileTreeChildren(files: FileNode[], targetPath: string, newChildren: FileNode[]): FileNode[] {
|
|
285
|
+
return files.map((file) => {
|
|
286
|
+
if (file.path === targetPath) return { ...file, children: newChildren };
|
|
287
|
+
if (file.type === 'directory' && file.children) {
|
|
288
|
+
return { ...file, children: updateFileTreeChildren(file.children, targetPath, newChildren) };
|
|
289
|
+
}
|
|
290
|
+
return file;
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function findFileInTree(files: FileNode[], targetPath: string): FileNode | null {
|
|
295
|
+
for (const file of files) {
|
|
296
|
+
if (file.path === targetPath) return file;
|
|
297
|
+
if (file.type === 'directory' && file.children) {
|
|
298
|
+
const found = findFileInTree(file.children, targetPath);
|
|
299
|
+
if (found) return found;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ============================
|
|
306
|
+
// Tab Operations
|
|
307
|
+
// ============================
|
|
308
|
+
|
|
309
|
+
function openFileInTab(file: FileNode, targetLine?: number) {
|
|
310
|
+
if (file.type === 'directory') return;
|
|
311
|
+
|
|
312
|
+
// Check if tab already exists
|
|
313
|
+
const existingTab = openTabs.find(t => t.file.path === file.path);
|
|
314
|
+
if (existingTab) {
|
|
315
|
+
activeTabPath = file.path;
|
|
316
|
+
displayTargetLine = targetLine;
|
|
317
|
+
if (!isTwoColumnMode) viewMode = 'viewer';
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Create new tab
|
|
322
|
+
const newTab: EditorTab = {
|
|
323
|
+
file,
|
|
324
|
+
currentContent: '',
|
|
325
|
+
savedContent: '',
|
|
326
|
+
isLoading: true
|
|
327
|
+
};
|
|
328
|
+
openTabs = [...openTabs, newTab];
|
|
329
|
+
activeTabPath = file.path;
|
|
330
|
+
displayTargetLine = targetLine;
|
|
331
|
+
if (!isTwoColumnMode) viewMode = 'viewer';
|
|
332
|
+
|
|
333
|
+
// Load content
|
|
334
|
+
loadTabContent(file.path);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function loadTabContent(filePath: string): Promise<boolean> {
|
|
338
|
+
try {
|
|
339
|
+
const data = await ws.http('files:read-file', { file_path: filePath });
|
|
340
|
+
const content = data.content || '';
|
|
341
|
+
openTabs = openTabs.map(t =>
|
|
342
|
+
t.file.path === filePath
|
|
343
|
+
? { ...t, currentContent: content, savedContent: content, isLoading: false }
|
|
344
|
+
: t
|
|
345
|
+
);
|
|
346
|
+
// Update display if this is the active tab
|
|
347
|
+
if (filePath === activeTabPath) {
|
|
348
|
+
displayContent = content;
|
|
349
|
+
displaySavedContent = content;
|
|
350
|
+
displayLoading = false;
|
|
351
|
+
}
|
|
352
|
+
return true;
|
|
353
|
+
} catch (err) {
|
|
354
|
+
openTabs = openTabs.map(t =>
|
|
355
|
+
t.file.path === filePath
|
|
356
|
+
? { ...t, isLoading: false }
|
|
357
|
+
: t
|
|
358
|
+
);
|
|
359
|
+
if (filePath === activeTabPath) {
|
|
360
|
+
displayLoading = false;
|
|
361
|
+
}
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function selectTab(path: string) {
|
|
367
|
+
activeTabPath = path;
|
|
368
|
+
displayTargetLine = undefined;
|
|
369
|
+
|
|
370
|
+
// Directly sync display state for immediate editor update
|
|
371
|
+
const tab = openTabs.find(t => t.file.path === path);
|
|
372
|
+
if (tab) {
|
|
373
|
+
displayFile = tab.file;
|
|
374
|
+
displayContent = tab.currentContent;
|
|
375
|
+
displaySavedContent = tab.savedContent;
|
|
376
|
+
displayLoading = tab.isLoading;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Scroll to active file in tree (without auto-expanding parent folders)
|
|
381
|
+
function scrollToActiveFile(filePath: string) {
|
|
382
|
+
if (!filePath || !projectPath) return;
|
|
383
|
+
|
|
384
|
+
// Auto-scroll to the selected file in the tree after DOM update (only if visible)
|
|
385
|
+
requestAnimationFrame(() => {
|
|
386
|
+
requestAnimationFrame(() => {
|
|
387
|
+
const selectedEl = treeScrollContainer?.querySelector('.selected');
|
|
388
|
+
if (selectedEl && treeScrollContainer) {
|
|
389
|
+
const containerRect = treeScrollContainer.getBoundingClientRect();
|
|
390
|
+
const elRect = selectedEl.getBoundingClientRect();
|
|
391
|
+
const padding = 40;
|
|
392
|
+
|
|
393
|
+
if (elRect.top < containerRect.top + padding) {
|
|
394
|
+
treeScrollContainer.scrollTop -= (containerRect.top + padding - elRect.top);
|
|
395
|
+
} else if (elRect.bottom > containerRect.bottom - padding) {
|
|
396
|
+
treeScrollContainer.scrollTop += (elRect.bottom - containerRect.bottom + padding);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function closeTab(path: string) {
|
|
404
|
+
const idx = openTabs.findIndex(t => t.file.path === path);
|
|
405
|
+
if (idx === -1) return;
|
|
406
|
+
|
|
407
|
+
openTabs = openTabs.filter(t => t.file.path !== path);
|
|
408
|
+
|
|
409
|
+
if (activeTabPath === path) {
|
|
410
|
+
if (openTabs.length > 0) {
|
|
411
|
+
// Activate the nearest tab
|
|
412
|
+
const newIdx = Math.min(idx, openTabs.length - 1);
|
|
413
|
+
activeTabPath = openTabs[newIdx].file.path;
|
|
414
|
+
} else {
|
|
415
|
+
activeTabPath = null;
|
|
416
|
+
if (!isTwoColumnMode) viewMode = 'tree';
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function handleEditorContentChange(newContent: string) {
|
|
422
|
+
if (!activeTabPath) return;
|
|
423
|
+
openTabs = openTabs.map(t =>
|
|
424
|
+
t.file.path === activeTabPath
|
|
425
|
+
? { ...t, currentContent: newContent }
|
|
426
|
+
: t
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ============================
|
|
431
|
+
// Tree Manipulation Helpers (Optimistic UI)
|
|
432
|
+
// ============================
|
|
433
|
+
|
|
434
|
+
function updateNodePathInTree(files: FileNode[], oldPath: string, newPath: string): FileNode[] {
|
|
435
|
+
return files.map((file) => {
|
|
436
|
+
if (file.path === oldPath) {
|
|
437
|
+
const newName = newPath.split(/[\\/]/).pop() || file.name;
|
|
438
|
+
return { ...file, path: newPath, name: newName };
|
|
439
|
+
} else if (file.type === 'directory' && file.children) {
|
|
440
|
+
return { ...file, children: updateNodePathInTree(file.children, oldPath, newPath) };
|
|
441
|
+
}
|
|
442
|
+
return file;
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function removeNodeFromTree(files: FileNode[], targetPath: string): FileNode[] {
|
|
447
|
+
return files
|
|
448
|
+
.filter((file) => file.path !== targetPath)
|
|
449
|
+
.map((file) => {
|
|
450
|
+
if (file.type === 'directory' && file.children) {
|
|
451
|
+
return { ...file, children: removeNodeFromTree(file.children, targetPath) };
|
|
452
|
+
}
|
|
453
|
+
return file;
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function addNodeToTree(files: FileNode[], parentPath: string | null, newNode: FileNode): FileNode[] {
|
|
458
|
+
if (!parentPath) {
|
|
459
|
+
return sortFileNodes([...files, newNode]);
|
|
460
|
+
}
|
|
461
|
+
return files.map((file) => {
|
|
462
|
+
if (file.path === parentPath && file.type === 'directory') {
|
|
463
|
+
return { ...file, children: sortFileNodes([...(file.children || []), newNode]) };
|
|
464
|
+
} else if (file.type === 'directory' && file.children) {
|
|
465
|
+
return { ...file, children: addNodeToTree(file.children, parentPath, newNode) };
|
|
466
|
+
}
|
|
467
|
+
return file;
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function duplicateNodeInTree(files: FileNode[], sourcePath: string, targetPath: string): FileNode[] {
|
|
472
|
+
const sourceNode = findFileInTree(files, sourcePath);
|
|
473
|
+
if (!sourceNode) return files;
|
|
474
|
+
const newName = targetPath.split(/[\\/]/).pop() || sourceNode.name;
|
|
475
|
+
const duplicateNode: FileNode = { ...sourceNode, path: targetPath, name: newName };
|
|
476
|
+
const pathParts = targetPath.split(/[\\/]/);
|
|
477
|
+
pathParts.pop();
|
|
478
|
+
const parentPath = pathParts.length > 0 ? pathParts.join(targetPath.includes('\\') ? '\\' : '/') : null;
|
|
479
|
+
return addNodeToTree(files, parentPath, duplicateNode);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function moveNodeInTree(files: FileNode[], sourcePath: string, targetPath: string): FileNode[] {
|
|
483
|
+
const sourceNode = findFileInTree(files, sourcePath);
|
|
484
|
+
if (!sourceNode) return files;
|
|
485
|
+
const newTree = removeNodeFromTree(files, sourcePath);
|
|
486
|
+
const newName = targetPath.split(/[\\/]/).pop() || sourceNode.name;
|
|
487
|
+
const movedNode: FileNode = { ...sourceNode, path: targetPath, name: newName };
|
|
488
|
+
const pathParts = targetPath.split(/[\\/]/);
|
|
489
|
+
pathParts.pop();
|
|
490
|
+
const parentPath = pathParts.length > 0 ? pathParts.join(targetPath.includes('\\') ? '\\' : '/') : null;
|
|
491
|
+
return addNodeToTree(newTree, parentPath, movedNode);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function sortFileNodes(nodes: FileNode[]): FileNode[] {
|
|
495
|
+
return nodes.sort((a, b) => {
|
|
496
|
+
if (a.type !== b.type) return a.type === 'directory' ? -1 : 1;
|
|
497
|
+
return a.name.localeCompare(b.name);
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function generateUniqueFilename(basePath: string, filename: string): string {
|
|
502
|
+
const separator = basePath.includes('\\') ? '\\' : '/';
|
|
503
|
+
let targetPath = `${basePath}${separator}${filename}`;
|
|
504
|
+
const existingFile = findFileInTree(projectFiles, targetPath);
|
|
505
|
+
if (!existingFile) return targetPath;
|
|
506
|
+
const lastDotIndex = filename.lastIndexOf('.');
|
|
507
|
+
const name = lastDotIndex > 0 ? filename.substring(0, lastDotIndex) : filename;
|
|
508
|
+
const extension = lastDotIndex > 0 ? filename.substring(lastDotIndex) : '';
|
|
509
|
+
let counter = 1;
|
|
510
|
+
while (true) {
|
|
511
|
+
const numberedFilename = `${name} (${counter})${extension}`;
|
|
512
|
+
targetPath = `${basePath}${separator}${numberedFilename}`;
|
|
513
|
+
if (!findFileInTree(projectFiles, targetPath)) return targetPath;
|
|
514
|
+
counter++;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// ============================
|
|
519
|
+
// Alert/Dialog Helpers
|
|
520
|
+
// ============================
|
|
521
|
+
|
|
522
|
+
function showErrorAlert(message: string, title = 'Error') {
|
|
523
|
+
alertType = 'error';
|
|
524
|
+
alertTitle = title;
|
|
525
|
+
alertMessage = message;
|
|
526
|
+
showAlert = true;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function openDialog(type: 'rename' | 'new-file' | 'new-folder', file?: FileNode, parentPath?: string | null) {
|
|
530
|
+
dialogType = type;
|
|
531
|
+
dialogTargetFile = file || null;
|
|
532
|
+
dialogParentPath = parentPath ?? null;
|
|
533
|
+
dialogValue = type === 'rename' ? (file?.name || '') : '';
|
|
534
|
+
dialogOpen = true;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function closeDialog() {
|
|
538
|
+
dialogOpen = false;
|
|
539
|
+
dialogValue = '';
|
|
540
|
+
dialogTargetFile = null;
|
|
541
|
+
dialogParentPath = null;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
async function handleDialogConfirm(value?: string) {
|
|
545
|
+
const trimmedValue = (value || dialogValue || '').trim();
|
|
546
|
+
if (!trimmedValue) {
|
|
547
|
+
closeDialog();
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
switch (dialogType) {
|
|
552
|
+
case 'rename':
|
|
553
|
+
await performRename(trimmedValue);
|
|
554
|
+
break;
|
|
555
|
+
case 'new-file':
|
|
556
|
+
await performCreateNew('file', trimmedValue);
|
|
557
|
+
break;
|
|
558
|
+
case 'new-folder':
|
|
559
|
+
await performCreateNew('folder', trimmedValue);
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
closeDialog();
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function performRename(newName: string) {
|
|
566
|
+
const file = dialogTargetFile;
|
|
567
|
+
if (!file || newName === file.name) return;
|
|
568
|
+
|
|
569
|
+
try {
|
|
570
|
+
const pathParts = file.path.split(/[\\/]/);
|
|
571
|
+
pathParts[pathParts.length - 1] = newName;
|
|
572
|
+
const newPath = pathParts.join(file.path.includes('\\') ? '\\' : '/');
|
|
573
|
+
const oldPath = file.path;
|
|
574
|
+
|
|
575
|
+
const existingFile = findFileInTree(projectFiles, newPath);
|
|
576
|
+
if (existingFile) {
|
|
577
|
+
showErrorAlert('A file or folder with this name already exists.', 'Rename Failed');
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Optimistic UI
|
|
582
|
+
projectFiles = updateNodePathInTree(projectFiles, oldPath, newPath);
|
|
583
|
+
|
|
584
|
+
// Update tab if open
|
|
585
|
+
openTabs = openTabs.map(t => {
|
|
586
|
+
if (t.file.path === oldPath) {
|
|
587
|
+
return { ...t, file: { ...t.file, path: newPath, name: newName } };
|
|
588
|
+
}
|
|
589
|
+
return t;
|
|
590
|
+
});
|
|
591
|
+
if (activeTabPath === oldPath) {
|
|
592
|
+
activeTabPath = newPath;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
await ws.http('files:rename', { oldPath, newPath });
|
|
596
|
+
} catch (error) {
|
|
597
|
+
debug.error('file', 'Failed to rename file:', error);
|
|
598
|
+
showErrorAlert(error instanceof Error ? error.message : 'Unknown error', 'Rename Failed');
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
async function performCreateNew(type: 'file' | 'folder', name: string) {
|
|
603
|
+
try {
|
|
604
|
+
const parentPath = dialogParentPath;
|
|
605
|
+
const separator = (parentPath || projectPath).includes('\\') ? '\\' : '/';
|
|
606
|
+
const fullPath = parentPath
|
|
607
|
+
? `${parentPath}${separator}${name}`
|
|
608
|
+
: `${projectPath}${separator}${name}`;
|
|
609
|
+
|
|
610
|
+
const newNode: FileNode = {
|
|
611
|
+
name,
|
|
612
|
+
path: fullPath,
|
|
613
|
+
type: type === 'folder' ? 'directory' : 'file',
|
|
614
|
+
size: 0,
|
|
615
|
+
modified: new Date(),
|
|
616
|
+
children: type === 'folder' ? [] : undefined
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
// Optimistic UI
|
|
620
|
+
projectFiles = addNodeToTree(projectFiles, parentPath, newNode);
|
|
621
|
+
|
|
622
|
+
if (parentPath && !expandedFolders.has(parentPath)) {
|
|
623
|
+
expandedFolders.add(parentPath);
|
|
624
|
+
expandedFolders = new Set(expandedFolders);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (type === 'file') {
|
|
628
|
+
await ws.http('files:create-file', { filePath: fullPath, content: '' });
|
|
629
|
+
} else {
|
|
630
|
+
await ws.http('files:create-directory', { dirPath: fullPath });
|
|
631
|
+
}
|
|
632
|
+
} catch (error) {
|
|
633
|
+
debug.error('file', 'Failed to create new item:', error);
|
|
634
|
+
showErrorAlert(error instanceof Error ? error.message : 'Unknown error', 'Create Failed');
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// ============================
|
|
639
|
+
// Clipboard
|
|
640
|
+
// ============================
|
|
641
|
+
|
|
642
|
+
let clipboard = $state<{ file: FileNode; operation: 'copy' | 'cut' } | null>(null);
|
|
643
|
+
|
|
644
|
+
async function pasteFile(targetFolder: FileNode) {
|
|
645
|
+
if (!clipboard) return;
|
|
646
|
+
try {
|
|
647
|
+
const { file: sourceFile, operation } = clipboard;
|
|
648
|
+
let basePath: string;
|
|
649
|
+
if (targetFolder.type === 'directory') {
|
|
650
|
+
basePath = targetFolder.path;
|
|
651
|
+
} else {
|
|
652
|
+
const pathParts = targetFolder.path.split(/[\\/]/);
|
|
653
|
+
pathParts.pop();
|
|
654
|
+
basePath = pathParts.join(targetFolder.path.includes('\\') ? '\\' : '/');
|
|
655
|
+
}
|
|
656
|
+
const targetPath = generateUniqueFilename(basePath, sourceFile.name);
|
|
657
|
+
|
|
658
|
+
if (operation === 'copy') {
|
|
659
|
+
projectFiles = duplicateNodeInTree(projectFiles, sourceFile.path, targetPath);
|
|
660
|
+
if (targetFolder.type === 'directory' && !expandedFolders.has(targetFolder.path)) {
|
|
661
|
+
expandedFolders.add(targetFolder.path);
|
|
662
|
+
expandedFolders = new Set(expandedFolders);
|
|
663
|
+
}
|
|
664
|
+
await ws.http('files:duplicate', { sourcePath: sourceFile.path, targetPath });
|
|
665
|
+
} else if (operation === 'cut') {
|
|
666
|
+
projectFiles = moveNodeInTree(projectFiles, sourceFile.path, targetPath);
|
|
667
|
+
if (targetFolder.type === 'directory' && !expandedFolders.has(targetFolder.path)) {
|
|
668
|
+
expandedFolders.add(targetFolder.path);
|
|
669
|
+
expandedFolders = new Set(expandedFolders);
|
|
670
|
+
}
|
|
671
|
+
const savedClipboard = clipboard;
|
|
672
|
+
clipboard = null;
|
|
673
|
+
try {
|
|
674
|
+
await ws.http('files:rename', { oldPath: sourceFile.path, newPath: targetPath });
|
|
675
|
+
} catch (err) {
|
|
676
|
+
projectFiles = moveNodeInTree(projectFiles, targetPath, sourceFile.path);
|
|
677
|
+
clipboard = savedClipboard;
|
|
678
|
+
throw err;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
} catch (error) {
|
|
682
|
+
debug.error('file', 'Failed to paste file:', error);
|
|
683
|
+
showErrorAlert(error instanceof Error ? error.message : 'Unknown error', 'Paste Failed');
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
async function pasteToRoot() {
|
|
688
|
+
if (!clipboard || !projectPath) return;
|
|
689
|
+
try {
|
|
690
|
+
const { file: sourceFile, operation } = clipboard;
|
|
691
|
+
const targetPath = generateUniqueFilename(projectPath, sourceFile.name);
|
|
692
|
+
|
|
693
|
+
if (operation === 'copy') {
|
|
694
|
+
projectFiles = duplicateNodeInTree(projectFiles, sourceFile.path, targetPath);
|
|
695
|
+
await ws.http('files:duplicate', { sourcePath: sourceFile.path, targetPath });
|
|
696
|
+
} else if (operation === 'cut') {
|
|
697
|
+
projectFiles = moveNodeInTree(projectFiles, sourceFile.path, targetPath);
|
|
698
|
+
const savedClipboard = clipboard;
|
|
699
|
+
clipboard = null;
|
|
700
|
+
try {
|
|
701
|
+
await ws.http('files:rename', { oldPath: sourceFile.path, newPath: targetPath });
|
|
702
|
+
} catch (err) {
|
|
703
|
+
projectFiles = moveNodeInTree(projectFiles, targetPath, sourceFile.path);
|
|
704
|
+
clipboard = savedClipboard;
|
|
705
|
+
throw err;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
} catch (error) {
|
|
709
|
+
debug.error('file', 'Failed to paste to root:', error);
|
|
710
|
+
showErrorAlert(error instanceof Error ? error.message : 'Unknown error', 'Paste Failed');
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// ============================
|
|
715
|
+
// File Actions
|
|
716
|
+
// ============================
|
|
717
|
+
|
|
718
|
+
async function handleFolderToggle(folderPath: string) {
|
|
719
|
+
if (expandedFolders.has(folderPath)) {
|
|
720
|
+
expandedFolders.delete(folderPath);
|
|
721
|
+
} else {
|
|
722
|
+
expandedFolders.add(folderPath);
|
|
723
|
+
const folder = findFileInTree(projectFiles, folderPath);
|
|
724
|
+
if (folder && folder.type === 'directory' && (!folder.children || folder.children.length === 0)) {
|
|
725
|
+
const children = await loadDirectoryContents(folderPath);
|
|
726
|
+
projectFiles = updateFileTreeChildren(projectFiles, folderPath, children);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
expandedFolders = new Set(expandedFolders);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function handleFileSelect(file: FileNode) {
|
|
733
|
+
if (file.type === 'file') {
|
|
734
|
+
openFileInTab(file);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
async function handleFileOpen(filePath: string, lineNumber?: number) {
|
|
739
|
+
let file = findFileInTree(projectFiles, filePath);
|
|
740
|
+
if (!file) {
|
|
741
|
+
const fileName = filePath.split(/[/\\]/).pop() || 'Untitled';
|
|
742
|
+
file = { name: fileName, path: filePath, type: 'file', size: 0, modified: new Date() };
|
|
743
|
+
}
|
|
744
|
+
openFileInTab(file, lineNumber);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
async function handleFileAction(action: string, file: FileNode) {
|
|
748
|
+
switch (action) {
|
|
749
|
+
case 'copy-path':
|
|
750
|
+
navigator.clipboard.writeText(file.path);
|
|
751
|
+
break;
|
|
752
|
+
case 'copy-relative-path': {
|
|
753
|
+
let relativePath = file.path;
|
|
754
|
+
if (projectPath && file.path.startsWith(projectPath)) {
|
|
755
|
+
relativePath = file.path.substring(projectPath.length);
|
|
756
|
+
if (relativePath.startsWith('/') || relativePath.startsWith('\\')) {
|
|
757
|
+
relativePath = relativePath.substring(1);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
navigator.clipboard.writeText(relativePath);
|
|
761
|
+
break;
|
|
762
|
+
}
|
|
763
|
+
case 'rename':
|
|
764
|
+
openDialog('rename', file);
|
|
765
|
+
break;
|
|
766
|
+
case 'copy':
|
|
767
|
+
clipboard = { file, operation: 'copy' };
|
|
768
|
+
break;
|
|
769
|
+
case 'cut':
|
|
770
|
+
clipboard = { file, operation: 'cut' };
|
|
771
|
+
break;
|
|
772
|
+
case 'paste':
|
|
773
|
+
if (clipboard) await pasteFile(file);
|
|
774
|
+
break;
|
|
775
|
+
case 'new-file':
|
|
776
|
+
openDialog('new-file', undefined, file.type === 'directory' ? file.path : null);
|
|
777
|
+
break;
|
|
778
|
+
case 'new-folder':
|
|
779
|
+
openDialog('new-folder', undefined, file.type === 'directory' ? file.path : null);
|
|
780
|
+
break;
|
|
781
|
+
case 'duplicate':
|
|
782
|
+
await duplicateFile(file);
|
|
783
|
+
break;
|
|
784
|
+
case 'delete':
|
|
785
|
+
await deleteFile(file);
|
|
786
|
+
break;
|
|
787
|
+
case 'refresh':
|
|
788
|
+
await refreshAll();
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
async function deleteFile(file: FileNode) {
|
|
794
|
+
const confirmMessage = file.type === 'directory'
|
|
795
|
+
? `Are you sure you want to delete the folder "${file.name}" and all its contents?`
|
|
796
|
+
: `Are you sure you want to delete "${file.name}"?`;
|
|
797
|
+
|
|
798
|
+
const confirmed = await showConfirm({
|
|
799
|
+
title: file.type === 'directory' ? 'Delete Folder' : 'Delete File',
|
|
800
|
+
message: confirmMessage,
|
|
801
|
+
type: 'error',
|
|
802
|
+
confirmText: 'Delete',
|
|
803
|
+
cancelText: 'Cancel'
|
|
804
|
+
});
|
|
805
|
+
if (!confirmed) return;
|
|
806
|
+
|
|
807
|
+
try {
|
|
808
|
+
const deletedNode = findFileInTree(projectFiles, file.path);
|
|
809
|
+
projectFiles = removeNodeFromTree(projectFiles, file.path);
|
|
810
|
+
|
|
811
|
+
// Close tab if open
|
|
812
|
+
closeTab(file.path);
|
|
813
|
+
|
|
814
|
+
try {
|
|
815
|
+
await ws.http('files:delete', { filePath: file.path, force: file.type === 'directory' });
|
|
816
|
+
} catch (err) {
|
|
817
|
+
if (deletedNode) {
|
|
818
|
+
const pathParts = file.path.split(/[\\/]/);
|
|
819
|
+
pathParts.pop();
|
|
820
|
+
const parentPath = pathParts.length > 0 ? pathParts.join(file.path.includes('\\') ? '\\' : '/') : null;
|
|
821
|
+
projectFiles = addNodeToTree(projectFiles, parentPath, deletedNode);
|
|
822
|
+
}
|
|
823
|
+
throw err;
|
|
824
|
+
}
|
|
825
|
+
} catch (error) {
|
|
826
|
+
debug.error('file', 'Failed to delete file:', error);
|
|
827
|
+
showErrorAlert(error instanceof Error ? error.message : 'Unknown error', 'Delete Failed');
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
async function duplicateFile(file: FileNode) {
|
|
832
|
+
try {
|
|
833
|
+
const pathParts = file.path.split(/[\\/]/);
|
|
834
|
+
const fileName = pathParts.pop() || file.name;
|
|
835
|
+
const parentPath = pathParts.join(file.path.includes('\\') ? '\\' : '/');
|
|
836
|
+
const targetPath = generateUniqueFilename(parentPath, fileName);
|
|
837
|
+
|
|
838
|
+
// Optimistic UI
|
|
839
|
+
projectFiles = duplicateNodeInTree(projectFiles, file.path, targetPath);
|
|
840
|
+
|
|
841
|
+
await ws.http('files:duplicate', { sourcePath: file.path, targetPath });
|
|
842
|
+
} catch (error) {
|
|
843
|
+
debug.error('file', 'Failed to duplicate file:', error);
|
|
844
|
+
showErrorAlert(error instanceof Error ? error.message : 'Unknown error', 'Duplicate Failed');
|
|
845
|
+
// Reload to revert optimistic update
|
|
846
|
+
await loadProjectFiles(true);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function createNewFileInRoot() {
|
|
851
|
+
openDialog('new-file', undefined, null);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function createNewFolderInRoot() {
|
|
855
|
+
openDialog('new-folder', undefined, null);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Save file
|
|
859
|
+
async function saveFile(filePath: string, content: string) {
|
|
860
|
+
await ws.http('files:write-file', { filePath, content });
|
|
861
|
+
// Update tab's savedContent and clear external change flag
|
|
862
|
+
openTabs = openTabs.map(t =>
|
|
863
|
+
t.file.path === filePath
|
|
864
|
+
? { ...t, savedContent: content, externallyChanged: false }
|
|
865
|
+
: t
|
|
866
|
+
);
|
|
867
|
+
// Update display if active tab
|
|
868
|
+
if (filePath === activeTabPath) {
|
|
869
|
+
displaySavedContent = content;
|
|
870
|
+
displayExternallyChanged = false;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Force reload active tab from server (discard local changes)
|
|
875
|
+
async function forceReloadTab() {
|
|
876
|
+
if (!activeTabPath) return;
|
|
877
|
+
const success = await loadTabContent(activeTabPath);
|
|
878
|
+
if (success) {
|
|
879
|
+
openTabs = openTabs.map(t =>
|
|
880
|
+
t.file.path === activeTabPath
|
|
881
|
+
? { ...t, externallyChanged: false }
|
|
882
|
+
: t
|
|
883
|
+
);
|
|
884
|
+
displayExternallyChanged = false;
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Refresh all
|
|
889
|
+
async function refreshAll(preserveState = false) {
|
|
890
|
+
await loadProjectFiles(preserveState);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// ============================
|
|
894
|
+
// Effects
|
|
895
|
+
// ============================
|
|
896
|
+
|
|
897
|
+
// Load files when project changes
|
|
898
|
+
$effect(() => {
|
|
899
|
+
if (hasActiveProject) {
|
|
900
|
+
// Save current project state before switching
|
|
901
|
+
if (lastProjectPath && lastProjectPath !== projectPath) {
|
|
902
|
+
projectFileStates.set(lastProjectPath, {
|
|
903
|
+
openTabs: [...openTabs],
|
|
904
|
+
activeTabPath,
|
|
905
|
+
expandedFolders: new Set(expandedFolders),
|
|
906
|
+
viewMode
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
if (lastProjectPath !== projectPath) {
|
|
911
|
+
isInitialLoad = true;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
loadProjectFiles();
|
|
915
|
+
|
|
916
|
+
// Restore previous state for this project
|
|
917
|
+
if (projectPath) {
|
|
918
|
+
const savedState = projectFileStates.get(projectPath);
|
|
919
|
+
if (savedState) {
|
|
920
|
+
openTabs = savedState.openTabs;
|
|
921
|
+
activeTabPath = savedState.activeTabPath;
|
|
922
|
+
expandedFolders = savedState.expandedFolders;
|
|
923
|
+
viewMode = savedState.viewMode;
|
|
924
|
+
} else {
|
|
925
|
+
openTabs = [];
|
|
926
|
+
activeTabPath = null;
|
|
927
|
+
viewMode = 'tree';
|
|
928
|
+
}
|
|
929
|
+
lastProjectPath = projectPath;
|
|
930
|
+
}
|
|
931
|
+
} else {
|
|
932
|
+
openTabs = [];
|
|
933
|
+
activeTabPath = null;
|
|
934
|
+
viewMode = 'tree';
|
|
935
|
+
lastProjectPath = '';
|
|
936
|
+
isInitialLoad = true;
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
// Start/stop file watcher when project changes
|
|
941
|
+
$effect(() => {
|
|
942
|
+
if (hasActiveProject && projectId && projectPath) {
|
|
943
|
+
ws.emit('files:watch', { projectPath });
|
|
944
|
+
debug.log('file', `Started watching project: ${projectId}`);
|
|
945
|
+
|
|
946
|
+
return () => {
|
|
947
|
+
ws.emit('files:unwatch', {});
|
|
948
|
+
isWatching = false;
|
|
949
|
+
debug.log('file', `Stopped watching project: ${projectId}`);
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
// Listen for file change events
|
|
955
|
+
$effect(() => {
|
|
956
|
+
if (!hasActiveProject || !projectId) return;
|
|
957
|
+
|
|
958
|
+
// Accumulate changes across multiple events within the debounce window
|
|
959
|
+
let accumulatedChanges: Array<{ path: string; type: 'created' | 'modified' | 'deleted'; timestamp: string }> = [];
|
|
960
|
+
|
|
961
|
+
const unsubChanges = ws.on('files:changed', (payload) => {
|
|
962
|
+
if (payload.projectId !== projectId) return;
|
|
963
|
+
|
|
964
|
+
// Accumulate changes from all events before debounce fires
|
|
965
|
+
accumulatedChanges.push(...payload.changes);
|
|
966
|
+
|
|
967
|
+
if (watchDebounceTimer) clearTimeout(watchDebounceTimer);
|
|
968
|
+
|
|
969
|
+
watchDebounceTimer = setTimeout(async () => {
|
|
970
|
+
const changes = [...accumulatedChanges];
|
|
971
|
+
accumulatedChanges = [];
|
|
972
|
+
|
|
973
|
+
// Reload file tree
|
|
974
|
+
await loadProjectFiles(true);
|
|
975
|
+
|
|
976
|
+
// Refresh all open tabs after tree reload
|
|
977
|
+
const tabsSnapshot = [...openTabs];
|
|
978
|
+
for (const tab of tabsSnapshot) {
|
|
979
|
+
const hasUnsavedChanges = tab.currentContent !== tab.savedContent;
|
|
980
|
+
|
|
981
|
+
if (hasUnsavedChanges) {
|
|
982
|
+
// Tab has unsaved changes — check if file changed externally
|
|
983
|
+
try {
|
|
984
|
+
const data = await ws.http('files:read-file', { file_path: tab.file.path });
|
|
985
|
+
const serverContent = data.content || '';
|
|
986
|
+
if (serverContent !== tab.savedContent) {
|
|
987
|
+
openTabs = openTabs.map(t =>
|
|
988
|
+
t.file.path === tab.file.path
|
|
989
|
+
? { ...t, externallyChanged: true }
|
|
990
|
+
: t
|
|
991
|
+
);
|
|
992
|
+
if (tab.file.path === activeTabPath) {
|
|
993
|
+
displayExternallyChanged = true;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
} catch {
|
|
997
|
+
// File deleted/renamed while user has unsaved changes — keep tab open
|
|
998
|
+
}
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// Tab has no unsaved changes — auto-sync
|
|
1003
|
+
const success = await loadTabContent(tab.file.path);
|
|
1004
|
+
if (!success) {
|
|
1005
|
+
// File no longer exists — try to detect rename
|
|
1006
|
+
const tabDir = tab.file.path.replace(/\\/g, '/').split('/').slice(0, -1).join('/');
|
|
1007
|
+
|
|
1008
|
+
// Find a 'created' change in the same parent directory
|
|
1009
|
+
const renameTarget = changes.find(c => {
|
|
1010
|
+
if (c.type !== 'created') return false;
|
|
1011
|
+
const cDir = c.path.replace(/\\/g, '/').split('/').slice(0, -1).join('/');
|
|
1012
|
+
return cDir === tabDir;
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
if (renameTarget) {
|
|
1016
|
+
const newPath = renameTarget.path;
|
|
1017
|
+
const newName = newPath.split(/[\\/]/).pop() || tab.file.name;
|
|
1018
|
+
const oldPath = tab.file.path;
|
|
1019
|
+
|
|
1020
|
+
// Update tab path and name
|
|
1021
|
+
openTabs = openTabs.map(t =>
|
|
1022
|
+
t.file.path === oldPath
|
|
1023
|
+
? { ...t, file: { ...t.file, path: newPath, name: newName } }
|
|
1024
|
+
: t
|
|
1025
|
+
);
|
|
1026
|
+
if (activeTabPath === oldPath) {
|
|
1027
|
+
activeTabPath = newPath;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// Load content from new path
|
|
1031
|
+
await loadTabContent(newPath);
|
|
1032
|
+
} else {
|
|
1033
|
+
// No rename target found — file was truly deleted
|
|
1034
|
+
closeTab(tab.file.path);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
watchDebounceTimer = null;
|
|
1040
|
+
}, 500);
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
const unsubWatching = ws.on('files:watching', (payload) => {
|
|
1044
|
+
if (payload.projectId !== projectId) return;
|
|
1045
|
+
isWatching = payload.watching;
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
const unsubError = ws.on('files:watch-error', (payload) => {
|
|
1049
|
+
if (payload.projectId !== projectId) return;
|
|
1050
|
+
debug.error('file', `Watch error: ${payload.error}`);
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
return () => {
|
|
1054
|
+
unsubChanges();
|
|
1055
|
+
unsubWatching();
|
|
1056
|
+
unsubError();
|
|
1057
|
+
if (watchDebounceTimer) clearTimeout(watchDebounceTimer);
|
|
1058
|
+
};
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
// Scroll to active file in tree when active tab changes
|
|
1062
|
+
$effect(() => {
|
|
1063
|
+
if (activeTabPath) {
|
|
1064
|
+
scrollToActiveFile(activeTabPath);
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
// Sync view state when switching between 1-column and 2-column modes
|
|
1069
|
+
let prevTwoColumnMode = $state<boolean | null>(null);
|
|
1070
|
+
$effect(() => {
|
|
1071
|
+
if (prevTwoColumnMode !== null && prevTwoColumnMode !== isTwoColumnMode) {
|
|
1072
|
+
if (!isTwoColumnMode) {
|
|
1073
|
+
// Switching from 2-column to 1-column:
|
|
1074
|
+
// If there's an active tab, show the viewer; otherwise show tree
|
|
1075
|
+
if (activeTabPath && openTabs.length > 0) {
|
|
1076
|
+
viewMode = 'viewer';
|
|
1077
|
+
} else {
|
|
1078
|
+
viewMode = 'tree';
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
// Switching from 1-column to 2-column: both are shown, no action needed
|
|
1082
|
+
}
|
|
1083
|
+
prevTwoColumnMode = isTwoColumnMode;
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
// Save state to persistent storage on component destruction (mobile/desktop switch)
|
|
1087
|
+
onDestroy(() => {
|
|
1088
|
+
if (projectPath) {
|
|
1089
|
+
// When in 2-column mode, compute correct viewMode for 1-column restoration
|
|
1090
|
+
let savedViewMode = viewMode;
|
|
1091
|
+
if (isTwoColumnMode) {
|
|
1092
|
+
savedViewMode = (activeTabPath && openTabs.length > 0) ? 'viewer' : 'tree';
|
|
1093
|
+
}
|
|
1094
|
+
projectFileStates.set(projectPath, {
|
|
1095
|
+
openTabs: [...openTabs],
|
|
1096
|
+
activeTabPath,
|
|
1097
|
+
expandedFolders: new Set(expandedFolders),
|
|
1098
|
+
viewMode: savedViewMode
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
// Monitor container width for responsive layout
|
|
1104
|
+
onMount(() => {
|
|
1105
|
+
if (containerRef && typeof ResizeObserver !== 'undefined') {
|
|
1106
|
+
const resizeObserver = new ResizeObserver((entries) => {
|
|
1107
|
+
for (const entry of entries) {
|
|
1108
|
+
containerWidth = entry.contentRect.width;
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
resizeObserver.observe(containerRef);
|
|
1112
|
+
return () => resizeObserver.disconnect();
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
// ============================
|
|
1117
|
+
// Panel Actions Export
|
|
1118
|
+
// ============================
|
|
1119
|
+
|
|
1120
|
+
export const panelActions = {
|
|
1121
|
+
setViewMode: (mode: 'tree' | 'viewer') => {
|
|
1122
|
+
if (!isTwoColumnMode) viewMode = mode;
|
|
1123
|
+
},
|
|
1124
|
+
getViewMode: () => viewMode,
|
|
1125
|
+
canShowViewer: () => openTabs.length > 0,
|
|
1126
|
+
isTwoColumnMode: () => isTwoColumnMode
|
|
1127
|
+
};
|
|
1128
|
+
</script>
|
|
1129
|
+
|
|
1130
|
+
<!-- Tab Bar Snippet -->
|
|
1131
|
+
{#snippet tabBar()}
|
|
1132
|
+
{#if openTabs.length > 0}
|
|
1133
|
+
<div class="flex items-center border-b border-slate-200 dark:border-slate-700 bg-slate-50/80 dark:bg-slate-800/50 overflow-x-auto flex-shrink-0">
|
|
1134
|
+
{#each openTabs as tab (tab.file.path)}
|
|
1135
|
+
{@const isActive = tab.file.path === activeTabPath}
|
|
1136
|
+
{@const isModified = tab.currentContent !== tab.savedContent}
|
|
1137
|
+
<div
|
|
1138
|
+
class="flex items-center gap-1.5 px-3 py-2 text-xs border-r border-slate-200/50 dark:border-slate-700/50 whitespace-nowrap transition-colors flex-shrink-0 cursor-pointer {isActive
|
|
1139
|
+
? 'bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100'
|
|
1140
|
+
: 'text-slate-500 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 hover:text-slate-700 dark:hover:text-slate-300'}"
|
|
1141
|
+
onclick={() => selectTab(tab.file.path)}
|
|
1142
|
+
>
|
|
1143
|
+
<Icon name={getFileIcon(tab.file.name) as IconName} class="w-3.5 h-3.5 flex-shrink-0" />
|
|
1144
|
+
<span class="truncate max-w-28">{tab.file.name}</span>
|
|
1145
|
+
{#if isModified}
|
|
1146
|
+
<span class="w-1.5 h-1.5 rounded-full bg-amber-500 dark:bg-amber-600 flex-shrink-0"></span>
|
|
1147
|
+
{/if}
|
|
1148
|
+
<button
|
|
1149
|
+
class="flex p-0.5 hover:bg-slate-200 dark:hover:bg-slate-700 rounded flex-shrink-0 opacity-60 hover:opacity-100"
|
|
1150
|
+
onclick={(e) => { e.stopPropagation(); closeTab(tab.file.path); }}
|
|
1151
|
+
title="Close tab"
|
|
1152
|
+
>
|
|
1153
|
+
<Icon name="lucide:x" class="w-3 h-3" />
|
|
1154
|
+
</button>
|
|
1155
|
+
</div>
|
|
1156
|
+
{/each}
|
|
1157
|
+
</div>
|
|
1158
|
+
{/if}
|
|
1159
|
+
{/snippet}
|
|
1160
|
+
|
|
1161
|
+
<div class="h-full flex flex-col bg-transparent" bind:this={containerRef}>
|
|
1162
|
+
{#if !hasActiveProject}
|
|
1163
|
+
<div class="flex-1 flex flex-col items-center justify-center gap-3 text-slate-600 dark:text-slate-500 text-sm">
|
|
1164
|
+
<Icon name="lucide:folder" class="w-10 h-10 opacity-30" />
|
|
1165
|
+
<span>No project selected</span>
|
|
1166
|
+
</div>
|
|
1167
|
+
{:else if isLoading && projectFiles.length === 0}
|
|
1168
|
+
<div class="flex-1 flex flex-col items-center justify-center gap-3 text-slate-600 dark:text-slate-500 text-sm">
|
|
1169
|
+
<div class="w-6 h-6 border-2 border-slate-200 dark:border-slate-800 border-t-violet-600 rounded-full animate-spin"></div>
|
|
1170
|
+
<span>Loading files...</span>
|
|
1171
|
+
</div>
|
|
1172
|
+
{:else if error}
|
|
1173
|
+
<div class="flex-1 flex flex-col items-center justify-center gap-3 text-red-500 text-sm">
|
|
1174
|
+
<Icon name="lucide:circle-x" class="w-8 h-8" />
|
|
1175
|
+
<span>{error}</span>
|
|
1176
|
+
<button
|
|
1177
|
+
onclick={() => loadProjectFiles()}
|
|
1178
|
+
class="py-1.5 px-3.5 bg-violet-500/10 dark:bg-violet-500/15 border border-violet-500/20 rounded-md text-violet-600 text-xs cursor-pointer transition-all duration-150 hover:bg-violet-500/20 dark:hover:bg-violet-500/25"
|
|
1179
|
+
>Retry</button>
|
|
1180
|
+
</div>
|
|
1181
|
+
{:else}
|
|
1182
|
+
<div class="flex-1 overflow-hidden">
|
|
1183
|
+
<!-- Unified layout: always render both Tree and Viewer to preserve internal state -->
|
|
1184
|
+
<div class="h-full flex">
|
|
1185
|
+
<!-- Tree panel: always rendered, hidden via CSS in 1-column viewer mode -->
|
|
1186
|
+
<div
|
|
1187
|
+
class={isTwoColumnMode
|
|
1188
|
+
? 'w-80 flex-shrink-0 h-full overflow-hidden border-r border-slate-200 dark:border-slate-700'
|
|
1189
|
+
: (viewMode === 'tree' ? 'w-full h-full overflow-hidden' : 'hidden')}
|
|
1190
|
+
>
|
|
1191
|
+
<div class="h-full overflow-auto" bind:this={treeScrollContainer}>
|
|
1192
|
+
<FileTree
|
|
1193
|
+
bind:this={fileTreeRef}
|
|
1194
|
+
files={projectFiles}
|
|
1195
|
+
selectedFile={displayFile}
|
|
1196
|
+
activeFilePath={activeTabPath}
|
|
1197
|
+
{expandedFolders}
|
|
1198
|
+
onFileSelect={handleFileSelect}
|
|
1199
|
+
onFileAction={handleFileAction}
|
|
1200
|
+
onFileOpen={handleFileOpen}
|
|
1201
|
+
onToggle={handleFolderToggle}
|
|
1202
|
+
hasClipboard={clipboard !== null}
|
|
1203
|
+
onPasteToRoot={pasteToRoot}
|
|
1204
|
+
onNewFileInRoot={createNewFileInRoot}
|
|
1205
|
+
onNewFolderInRoot={createNewFolderInRoot}
|
|
1206
|
+
onRefresh={refreshAll}
|
|
1207
|
+
modifiedFiles={modifiedFilePaths}
|
|
1208
|
+
/>
|
|
1209
|
+
</div>
|
|
1210
|
+
</div>
|
|
1211
|
+
|
|
1212
|
+
<!-- Editor panel: always rendered, hidden via CSS in 1-column tree mode -->
|
|
1213
|
+
<div
|
|
1214
|
+
class={isTwoColumnMode
|
|
1215
|
+
? 'flex-1 h-full overflow-hidden flex flex-col'
|
|
1216
|
+
: (viewMode === 'viewer' ? 'w-full h-full flex flex-col' : 'hidden')}
|
|
1217
|
+
>
|
|
1218
|
+
{@render tabBar()}
|
|
1219
|
+
<div class="flex-1 overflow-hidden">
|
|
1220
|
+
<FileViewer
|
|
1221
|
+
file={displayFile}
|
|
1222
|
+
content={displayContent}
|
|
1223
|
+
savedContent={displaySavedContent}
|
|
1224
|
+
isLoading={displayLoading}
|
|
1225
|
+
error=""
|
|
1226
|
+
onSave={saveFile}
|
|
1227
|
+
targetLine={displayTargetLine}
|
|
1228
|
+
onContentChange={handleEditorContentChange}
|
|
1229
|
+
wordWrap={wordWrapEnabled}
|
|
1230
|
+
onToggleWordWrap={() => { wordWrapEnabled = !wordWrapEnabled; }}
|
|
1231
|
+
externallyChanged={displayExternallyChanged}
|
|
1232
|
+
onForceReload={forceReloadTab}
|
|
1233
|
+
/>
|
|
1234
|
+
</div>
|
|
1235
|
+
</div>
|
|
1236
|
+
</div>
|
|
1237
|
+
</div>
|
|
1238
|
+
{/if}
|
|
1239
|
+
|
|
1240
|
+
<!-- Dialog for Rename / Create -->
|
|
1241
|
+
<Dialog
|
|
1242
|
+
bind:isOpen={dialogOpen}
|
|
1243
|
+
type="info"
|
|
1244
|
+
title={dialogConfig.title}
|
|
1245
|
+
message={dialogConfig.message}
|
|
1246
|
+
bind:inputValue={dialogValue}
|
|
1247
|
+
inputPlaceholder={dialogConfig.placeholder}
|
|
1248
|
+
confirmText={dialogConfig.confirmText}
|
|
1249
|
+
onConfirm={handleDialogConfirm}
|
|
1250
|
+
onClose={closeDialog}
|
|
1251
|
+
/>
|
|
1252
|
+
|
|
1253
|
+
<!-- Alert Component -->
|
|
1254
|
+
<Alert
|
|
1255
|
+
bind:isOpen={showAlert}
|
|
1256
|
+
title={alertTitle}
|
|
1257
|
+
message={alertMessage}
|
|
1258
|
+
type={alertType}
|
|
1259
|
+
onClose={() => { showAlert = false; }}
|
|
1260
|
+
/>
|
|
1261
|
+
</div>
|