From c3f4fb781499f4afe0956a275d2b3dc3f13814c4 Mon Sep 17 00:00:00 2001 From: Chris Scott <99081550+chriswritescode-dev@users.noreply.github.com> Date: Sun, 23 Nov 2025 22:17:21 -0500 Subject: [PATCH 001/433] Initial commit --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..148bc605 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Chris Scott + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 800cd9c2633221729934dda8907449c04ed83dbf Mon Sep 17 00:00:00 2001 From: Chris Scott Date: Sun, 23 Nov 2025 23:45:38 -0500 Subject: [PATCH 002/433] add demo videos to README --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index d1ebf65b..058c0ce4 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,17 @@ A full-stack web application for running [OpenCode](https://github.com/sst/openc - **File Browser** - Browse, edit, and manage files in your workspaces - **Push PRs to GitHub** - Create and push pull requests directly from your phone on the go +## Demo Videos + +### File Context +https://github.com/user-attachments/assets/a5b2a5c1-b601-4b05-9de9-2b387e21b3f2 + +### File Editing +https://github.com/user-attachments/assets/6689f0ca-be30-4b89-9545-e18afee1a76e + +### Demo +https://github.com/user-attachments/assets/b67c5022-a7b5-4263-80f7-91fb0eff7cee + ## Coming Soon - **Authentication** - User authentication and session management From fe3d585e697c517a43e8bcb3bf696e8b30a1a2d0 Mon Sep 17 00:00:00 2001 From: Chris Scott Date: Sun, 23 Nov 2025 23:46:37 -0500 Subject: [PATCH 003/433] reorder demo videos --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 058c0ce4..b81c9616 100644 --- a/README.md +++ b/README.md @@ -14,14 +14,14 @@ A full-stack web application for running [OpenCode](https://github.com/sst/openc ## Demo Videos -### File Context -https://github.com/user-attachments/assets/a5b2a5c1-b601-4b05-9de9-2b387e21b3f2 +### Demo +https://github.com/user-attachments/assets/b67c5022-a7b5-4263-80f7-91fb0eff7cee ### File Editing https://github.com/user-attachments/assets/6689f0ca-be30-4b89-9545-e18afee1a76e -### Demo -https://github.com/user-attachments/assets/b67c5022-a7b5-4263-80f7-91fb0eff7cee +### File Context +https://github.com/user-attachments/assets/a5b2a5c1-b601-4b05-9de9-2b387e21b3f2 ## Coming Soon From 49050a0568aa22b2538340841fe4301dfdb8837a Mon Sep 17 00:00:00 2001 From: Chris Scott Date: Sun, 23 Nov 2025 23:50:41 -0500 Subject: [PATCH 004/433] convert demo videos to GIFs --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b81c9616..aa09ca4b 100644 --- a/README.md +++ b/README.md @@ -15,13 +15,13 @@ A full-stack web application for running [OpenCode](https://github.com/sst/openc ## Demo Videos ### Demo -https://github.com/user-attachments/assets/b67c5022-a7b5-4263-80f7-91fb0eff7cee +![Demo](https://github.com/chriswritescode-dev/opencode-web/releases/download/0.2.5/demo1.gif) ### File Editing -https://github.com/user-attachments/assets/6689f0ca-be30-4b89-9545-e18afee1a76e +![File Editing](https://github.com/chriswritescode-dev/opencode-web/releases/download/0.2.5/edit-file.gif) ### File Context -https://github.com/user-attachments/assets/a5b2a5c1-b601-4b05-9de9-2b387e21b3f2 +![File Context](https://github.com/chriswritescode-dev/opencode-web/releases/download/0.2.5/file-context.gif) ## Coming Soon From f0e8325b47186a4a47377ac67a5d27e56134bdd5 Mon Sep 17 00:00:00 2001 From: Chris Scott Date: Mon, 24 Nov 2025 00:17:05 -0500 Subject: [PATCH 005/433] fix: centralize API URL configuration in constants - Move API base URL to single constant in @/constants/api - Update all API modules to import from centralized location - Ensure consistent port 8080 usage across frontend - Support VITE_API_URL environment variable for flexible deployment --- frontend/src/api/providers.ts | 3 +-- frontend/src/api/repos.ts | 3 ++- frontend/src/api/settings.ts | 3 +-- frontend/src/components/file-browser/FileBrowser.tsx | 3 ++- frontend/src/components/file-browser/FilePreview.tsx | 3 ++- frontend/src/constants/api.ts | 6 +++++- frontend/vite.config.ts | 7 ++++++- 7 files changed, 19 insertions(+), 9 deletions(-) diff --git a/frontend/src/api/providers.ts b/frontend/src/api/providers.ts index c12a1a8d..325add66 100644 --- a/frontend/src/api/providers.ts +++ b/frontend/src/api/providers.ts @@ -1,7 +1,6 @@ import { settingsApi } from "./settings"; import axios from "axios"; - -const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:5001"; +import { API_BASE_URL } from "@/constants/api"; export interface Model { id: string; diff --git a/frontend/src/api/repos.ts b/frontend/src/api/repos.ts index 28d5014f..187f7dce 100644 --- a/frontend/src/api/repos.ts +++ b/frontend/src/api/repos.ts @@ -1,6 +1,7 @@ import type { Repo } from './types' +import { API_BASE_URL } from '@/constants/api' -const API_BASE = import.meta.env.VITE_API_URL ?? 'http://localhost:5001' +const API_BASE = API_BASE_URL export async function createRepo( repoUrl: string, diff --git a/frontend/src/api/settings.ts b/frontend/src/api/settings.ts index f157bc8f..8dd0fa7f 100644 --- a/frontend/src/api/settings.ts +++ b/frontend/src/api/settings.ts @@ -7,8 +7,7 @@ import type { CreateOpenCodeConfigRequest, UpdateOpenCodeConfigRequest } from './types/settings' - -const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:5001' +import { API_BASE_URL } from '@/constants/api' export const settingsApi = { getSettings: async (userId = 'default'): Promise => { diff --git a/frontend/src/components/file-browser/FileBrowser.tsx b/frontend/src/components/file-browser/FileBrowser.tsx index 5ccdf4b1..6bb91a54 100644 --- a/frontend/src/components/file-browser/FileBrowser.tsx +++ b/frontend/src/components/file-browser/FileBrowser.tsx @@ -8,8 +8,9 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' import { FolderOpen, Upload, RefreshCw, X } from 'lucide-react' import type { FileInfo } from '@/types/files' +import { API_BASE_URL } from '@/constants/api' -const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:5001' +const API_BASE = API_BASE_URL // Hook to detect mobile screens const useIsMobile = () => { diff --git a/frontend/src/components/file-browser/FilePreview.tsx b/frontend/src/components/file-browser/FilePreview.tsx index 28f5ec9d..3854c50e 100644 --- a/frontend/src/components/file-browser/FilePreview.tsx +++ b/frontend/src/components/file-browser/FilePreview.tsx @@ -2,8 +2,9 @@ import { useState } from 'react' import { Button } from '@/components/ui/button' import { Download, Copy, X, Edit3, Save, X as XIcon } from 'lucide-react' import type { FileInfo } from '@/types/files' +import { API_BASE_URL } from '@/constants/api' -const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:5001' +const API_BASE = API_BASE_URL interface FilePreviewProps { file: FileInfo diff --git a/frontend/src/constants/api.ts b/frontend/src/constants/api.ts index 15ce9d6c..47523c00 100644 --- a/frontend/src/constants/api.ts +++ b/frontend/src/constants/api.ts @@ -1 +1,5 @@ -export const OPENCODE_API_ENDPOINT = '/api/opencode' \ No newline at end of file +export const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080' + +export const OPENCODE_API_ENDPOINT = import.meta.env.VITE_API_URL + ? `${import.meta.env.VITE_API_URL}/api/opencode` + : '/api/opencode' \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 6c6d5dc3..1aac6d41 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -17,7 +17,12 @@ export default defineConfig({ server: { host: '0.0.0.0', port: 5173, - proxy: { + proxy: process.env.VITE_API_URL ? { + '/api': { + target: process.env.VITE_API_URL, + changeOrigin: true, + }, + } : { '/api': { target: 'http://localhost:8080', changeOrigin: true, From bcc1536650320de8b2e9c644626808f8cd80cc2f Mon Sep 17 00:00:00 2001 From: Chris Scott Date: Mon, 24 Nov 2025 00:19:36 -0500 Subject: [PATCH 006/433] fix: remove hardcoded port, require VITE_API_URL env var - Remove default localhost:8080 fallback from API_BASE_URL - Make VITE_API_URL required for proper functionality - Update .env.example to clarify required configuration - Ensure no hardcoded ports exist in frontend --- .env.example | 4 ++-- frontend/src/constants/api.ts | 2 +- frontend/vite.config.ts | 7 +------ 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index 4ca94e85..967e083d 100644 --- a/.env.example +++ b/.env.example @@ -44,9 +44,9 @@ MAX_UPLOAD_SIZE_MB=50 DEBUG=false # ============================================ -# Frontend Configuration (for development) +# Frontend Configuration (REQUIRED) # ============================================ # Backend API URL - frontend connects to this -# Note: In development, Vite proxies /api requests to the backend +# Must include full URL with port (e.g., http://localhost:8080) VITE_API_URL=http://localhost:8080 diff --git a/frontend/src/constants/api.ts b/frontend/src/constants/api.ts index 47523c00..5819cce9 100644 --- a/frontend/src/constants/api.ts +++ b/frontend/src/constants/api.ts @@ -1,4 +1,4 @@ -export const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080' +export const API_BASE_URL = import.meta.env.VITE_API_URL export const OPENCODE_API_ENDPOINT = import.meta.env.VITE_API_URL ? `${import.meta.env.VITE_API_URL}/api/opencode` diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 1aac6d41..7201a61e 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -22,12 +22,7 @@ export default defineConfig({ target: process.env.VITE_API_URL, changeOrigin: true, }, - } : { - '/api': { - target: 'http://localhost:8080', - changeOrigin: true, - }, - }, + } : undefined, }, build: { assetsInlineLimit: 4096, From 8e1911cfc1574962115cc8f598d382236b58a394 Mon Sep 17 00:00:00 2001 From: Chris Scott Date: Mon, 24 Nov 2025 00:24:22 -0500 Subject: [PATCH 007/433] update dev setup script --- scripts/setup-dev.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/setup-dev.sh b/scripts/setup-dev.sh index 11164340..f4fac5b4 100755 --- a/scripts/setup-dev.sh +++ b/scripts/setup-dev.sh @@ -36,7 +36,7 @@ fi echo "✅ OpenCode TUI is installed" # Create workspace directory if it doesn't exist -WORKSPACE_PATH="${HOME}/.opencode-workspace" +WORKSPACE_PATH="./workspace" if [ ! -d "$WORKSPACE_PATH" ]; then echo "📁 Creating workspace directory at $WORKSPACE_PATH..." mkdir -p "$WORKSPACE_PATH/repos" @@ -70,4 +70,4 @@ echo "" echo "🚀 To start development:" echo " npm run dev # Start both backend and frontend" echo " npm run dev:backend # Start backend only" -echo " npm run dev:frontend # Start frontend only" \ No newline at end of file +echo " npm run dev:frontend # Start frontend only" From 809d539f9e91c55169e8a0879830496842080043 Mon Sep 17 00:00:00 2001 From: Chris Scott Date: Mon, 24 Nov 2025 00:27:06 -0500 Subject: [PATCH 008/433] fix Docker API URL configuration - Use relative URLs for Docker deployment - Default API_BASE_URL to '/api' for containerized environment - Remove undefined VITE_API_URL causing broken API calls --- frontend/src/constants/api.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/src/constants/api.ts b/frontend/src/constants/api.ts index 5819cce9..4fc1c11d 100644 --- a/frontend/src/constants/api.ts +++ b/frontend/src/constants/api.ts @@ -1,5 +1,3 @@ -export const API_BASE_URL = import.meta.env.VITE_API_URL +export const API_BASE_URL = import.meta.env.VITE_API_URL || '/api' -export const OPENCODE_API_ENDPOINT = import.meta.env.VITE_API_URL - ? `${import.meta.env.VITE_API_URL}/api/opencode` - : '/api/opencode' \ No newline at end of file +export const OPENCODE_API_ENDPOINT = '/api/opencode' \ No newline at end of file From a98f0c82264e3d1d6f2042c4d13b8e00f884d613 Mon Sep 17 00:00:00 2001 From: Chris Scott Date: Mon, 24 Nov 2025 00:28:45 -0500 Subject: [PATCH 009/433] fix double API prefix in URLs - Remove /api prefix from API_BASE_URL to prevent /api/api/ paths - Keep API_BASE_URL as base URL only, endpoints add their own /api prefix --- frontend/src/constants/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/constants/api.ts b/frontend/src/constants/api.ts index 4fc1c11d..09a1be26 100644 --- a/frontend/src/constants/api.ts +++ b/frontend/src/constants/api.ts @@ -1,3 +1,3 @@ -export const API_BASE_URL = import.meta.env.VITE_API_URL || '/api' +export const API_BASE_URL = import.meta.env.VITE_API_URL || '' export const OPENCODE_API_ENDPOINT = '/api/opencode' \ No newline at end of file From 1ea059be27ac4297deb3c1516ba7e3ea659a05cc Mon Sep 17 00:00:00 2001 From: Chris Scott Date: Mon, 24 Nov 2025 10:37:18 -0500 Subject: [PATCH 010/433] fix: add fixed width to mode toggle button to prevent layout shift --- frontend/src/components/message/PromptInput.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/message/PromptInput.tsx b/frontend/src/components/message/PromptInput.tsx index a3bddf36..676e082b 100644 --- a/frontend/src/components/message/PromptInput.tsx +++ b/frontend/src/components/message/PromptInput.tsx @@ -495,9 +495,9 @@ export function PromptInput({
{isBashMode && (
From 7e16626b806b072fb48948003b51ed5d7d9ce353 Mon Sep 17 00:00:00 2001 From: Chris Scott Date: Mon, 24 Nov 2025 11:55:16 -0500 Subject: [PATCH 011/433] update database path from ./backend/data to ./data --- .env.example | 4 ++-- Dockerfile | 4 ++-- backend/src/config.ts | 2 +- docker-compose.dev.yml | 4 ++-- docker-compose.yml | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index 967e083d..372f0b06 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,7 @@ # ============================================ # Backend Server (Hono API) # ============================================ -PORT=8080 +PORT=5001 HOST=0.0.0.0 # ============================================ @@ -15,7 +15,7 @@ OPENCODE_SERVER_PORT=5551 # ============================================ # Database # ============================================ -DATABASE_PATH=./backend/data/opencode.db +DATABASE_PATH=./data/opencode.db # ============================================ # Workspace Configuration diff --git a/Dockerfile b/Dockerfile index c15d026e..e6688a4b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -48,7 +48,7 @@ ENV NODE_ENV=production ENV HOST=0.0.0.0 ENV PORT=5001 ENV OPENCODE_SERVER_PORT=5551 -ENV DATABASE_PATH=/app/backend/data/opencode.db +ENV DATABASE_PATH=/app/data/opencode.db ENV WORKSPACE_PATH=/workspace COPY --from=builder /app/backend/dist ./backend/dist @@ -61,7 +61,7 @@ COPY --from=builder /app/frontend/dist ./frontend/dist COPY scripts/docker-entrypoint.sh /docker-entrypoint.sh RUN chmod +x /docker-entrypoint.sh -RUN mkdir -p /workspace /app/backend/data +RUN mkdir -p /workspace /app/data EXPOSE 5001 diff --git a/backend/src/config.ts b/backend/src/config.ts index a99f6061..fa62f47b 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -6,7 +6,7 @@ export const ENV = { PORT: parseInt(process.env.PORT || '5001'), OPENCODE_SERVER_PORT: parseInt(process.env.OPENCODE_SERVER_PORT || '5551'), HOST: process.env.HOST || '0.0.0.0', - DATABASE_PATH: process.env.DATABASE_PATH || './backend/data/opencode.db', + DATABASE_PATH: process.env.DATABASE_PATH || './data/opencode.db', WORKSPACE_PATH: process.env.WORKSPACE_PATH || '~/.opencode-workspace', PROCESS_START_WAIT_MS: parseInt(process.env.PROCESS_START_WAIT_MS || '2000'), PROCESS_VERIFY_WAIT_MS: parseInt(process.env.PROCESS_VERIFY_WAIT_MS || '1000'), diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 6dd1e719..a6743c05 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -13,14 +13,14 @@ services: - HOST=0.0.0.0 - PORT=5001 - OPENCODE_SERVER_PORT=5551 - - DATABASE_PATH=/app/backend/data/opencode.db + - DATABASE_PATH=/app/data/opencode.db - WORKSPACE_PATH=/workspace - DEBUG=true volumes: - ./backend:/app/backend - ./shared:/app/shared - opencode-workspace-dev:/workspace - - opencode-data-dev:/app/backend/data + - opencode-data-dev:/app/data restart: unless-stopped frontend: diff --git a/docker-compose.yml b/docker-compose.yml index 7a318007..ab53c91f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,7 @@ services: - HOST=0.0.0.0 - PORT=5001 - OPENCODE_SERVER_PORT=5551 - - DATABASE_PATH=/app/backend/data/opencode.db + - DATABASE_PATH=/app/data/opencode.db - WORKSPACE_PATH=/workspace - PROCESS_START_WAIT_MS=2000 - PROCESS_VERIFY_WAIT_MS=1000 @@ -24,7 +24,7 @@ services: - DEBUG=false volumes: - opencode-workspace:/workspace - - opencode-data:/app/backend/data + - opencode-data:/app/data restart: unless-stopped healthcheck: test: ["CMD", "curl", "-f", "http://localhost:5001/api/health"] From edb367b6f8638489078ab74e8402976e5efe5061 Mon Sep 17 00:00:00 2001 From: Chris Scott Date: Tue, 25 Nov 2025 09:39:56 -0500 Subject: [PATCH 012/433] refactor: consolidate environment config and API handling - Update database paths and port configuration - Remove backend config.ts and sessions route - Add mobile file preview and files API - Clean up shared constants and provider hooks --- .env.example | 25 ++- backend/src/config.ts | 24 --- backend/src/db/queries.ts | 2 +- backend/src/db/schema.ts | 4 + backend/src/index.ts | 45 ++++- backend/src/routes/repos.ts | 20 +- backend/src/routes/sessions.ts | 154 ---------------- backend/src/routes/settings.ts | 16 ++ backend/src/services/auth.ts | 2 +- backend/src/services/file-operations.ts | 2 +- backend/src/services/files.ts | 3 +- .../src/services/opencode-single-server.ts | 24 +-- backend/src/services/proxy.ts | 4 +- backend/src/services/repo.ts | 2 +- backend/src/utils/logger.ts | 6 +- frontend/src/api/files.ts | 23 +++ frontend/src/api/providers.ts | 1 + .../components/file-browser/FileBrowser.tsx | 173 +++++------------- .../file-browser/FileBrowserSheet.tsx | 27 ++- .../components/file-browser/FilePreview.tsx | 12 +- .../file-browser/MobileFilePreviewModal.tsx | 57 ++++++ .../components/settings/AddProviderDialog.tsx | 2 +- .../components/settings/ProviderSettings.tsx | 8 +- frontend/src/constants/api.ts | 4 +- frontend/src/hooks/useProviders.ts | 58 ------ frontend/src/pages/RepoDetail.tsx | 2 +- frontend/src/pages/SessionDetail.tsx | 2 +- frontend/vite.config.ts | 35 ++-- scripts/generate-openapi.ts | 9 +- shared/package.json | 5 +- shared/src/constants.ts | 62 ------- shared/src/index.ts | 2 +- 32 files changed, 302 insertions(+), 513 deletions(-) delete mode 100644 backend/src/config.ts delete mode 100644 backend/src/routes/sessions.ts create mode 100644 frontend/src/api/files.ts create mode 100644 frontend/src/components/file-browser/MobileFilePreviewModal.tsx delete mode 100644 frontend/src/hooks/useProviders.ts delete mode 100644 shared/src/constants.ts diff --git a/.env.example b/.env.example index 372f0b06..2ea5ed9e 100644 --- a/.env.example +++ b/.env.example @@ -1,16 +1,21 @@ # OpenCode WebUI Configuration # Copy this file to .env and customize as needed +# Default values are defined in: shared/src/config/defaults.ts # ============================================ -# Backend Server (Hono API) +# Backend Server Configuration # ============================================ PORT=5001 HOST=0.0.0.0 +CORS_ORIGIN=http://localhost:5173 +NODE_ENV=development +LOG_LEVEL=info # ============================================ # OpenCode Server # ============================================ OPENCODE_SERVER_PORT=5551 +OPENCODE_HOST=127.0.0.1 # ============================================ # Database @@ -20,8 +25,7 @@ DATABASE_PATH=./data/opencode.db # ============================================ # Workspace Configuration # ============================================ -# Local workspace path for repositories and configs -WORKSPACE_PATH=~./workspace +WORKSPACE_PATH=./workspace # ============================================ # Timeouts (milliseconds) @@ -32,9 +36,8 @@ HEALTH_CHECK_INTERVAL_MS=5000 HEALTH_CHECK_TIMEOUT_MS=30000 # ============================================ -# File Upload Limits +# File Limits (MB) # ============================================ -# File size limits in MB MAX_FILE_SIZE_MB=50 MAX_UPLOAD_SIZE_MB=50 @@ -44,9 +47,11 @@ MAX_UPLOAD_SIZE_MB=50 DEBUG=false # ============================================ -# Frontend Configuration (REQUIRED) +# Frontend Configuration (Vite) +# These are optional - frontend uses defaults if not set # ============================================ -# Backend API URL - frontend connects to this -# Must include full URL with port (e.g., http://localhost:8080) -VITE_API_URL=http://localhost:8080 - +# VITE_API_URL=http://localhost:5001 +# VITE_SERVER_PORT=5001 +# VITE_OPENCODE_PORT=5551 +# VITE_MAX_FILE_SIZE_MB=50 +# VITE_MAX_UPLOAD_SIZE_MB=50 diff --git a/backend/src/config.ts b/backend/src/config.ts deleted file mode 100644 index fa62f47b..00000000 --- a/backend/src/config.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { config } from 'dotenv' - -config() - -export const ENV = { - PORT: parseInt(process.env.PORT || '5001'), - OPENCODE_SERVER_PORT: parseInt(process.env.OPENCODE_SERVER_PORT || '5551'), - HOST: process.env.HOST || '0.0.0.0', - DATABASE_PATH: process.env.DATABASE_PATH || './data/opencode.db', - WORKSPACE_PATH: process.env.WORKSPACE_PATH || '~/.opencode-workspace', - PROCESS_START_WAIT_MS: parseInt(process.env.PROCESS_START_WAIT_MS || '2000'), - PROCESS_VERIFY_WAIT_MS: parseInt(process.env.PROCESS_VERIFY_WAIT_MS || '1000'), - HEALTH_CHECK_INTERVAL_MS: parseInt(process.env.HEALTH_CHECK_INTERVAL_MS || '5000'), - HEALTH_CHECK_TIMEOUT_MS: parseInt(process.env.HEALTH_CHECK_TIMEOUT_MS || '30000'), - MAX_FILE_SIZE_MB: parseInt(process.env.MAX_FILE_SIZE_MB || '50'), - MAX_UPLOAD_SIZE_MB: parseInt(process.env.MAX_UPLOAD_SIZE_MB || '50'), - SANDBOX_TTL_HOURS: parseInt(process.env.SANDBOX_TTL_HOURS || '24'), - CLEANUP_INTERVAL_MINUTES: parseInt(process.env.CLEANUP_INTERVAL_MINUTES || '60'), - DEBUG: process.env.DEBUG === 'true', - VITE_API_URL: process.env.VITE_API_URL || 'http://localhost:5001', - VITE_ANTHROPIC_API_KEY: process.env.VITE_ANTHROPIC_API_KEY || '', - VITE_OPENAI_API_KEY: process.env.VITE_OPENAI_API_KEY || '', - VITE_GOOGLE_API_KEY: process.env.VITE_GOOGLE_API_KEY || '' -} \ No newline at end of file diff --git a/backend/src/db/queries.ts b/backend/src/db/queries.ts index fbe4ae7a..91c28326 100644 --- a/backend/src/db/queries.ts +++ b/backend/src/db/queries.ts @@ -1,6 +1,6 @@ import type { Database } from 'bun:sqlite' import type { Repo, CreateRepoInput } from '../types/repo' -import { getReposPath } from '../../../shared/src/constants' +import { getReposPath } from '../config' import path from 'path' export interface RepoRow { diff --git a/backend/src/db/schema.ts b/backend/src/db/schema.ts index 49018d7e..98727400 100644 --- a/backend/src/db/schema.ts +++ b/backend/src/db/schema.ts @@ -53,6 +53,10 @@ export function initializeDatabase(dbPath: string = './data/opencode.db'): Datab runMigrations(db) + // Force database file creation by performing a write + db.prepare('INSERT OR IGNORE INTO user_preferences (user_id, preferences, updated_at) VALUES (?, ?, ?)') + .run('default', '{}', Date.now()) + logger.info('Database initialized successfully') return db diff --git a/backend/src/index.ts b/backend/src/index.ts index 250ae1bf..1fa341a0 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -6,7 +6,7 @@ import { initializeDatabase } from './db/schema' import { createRepoRoutes } from './routes/repos' import { createSettingsRoutes } from './routes/settings' import { createHealthRoutes } from './routes/health' -import { createSessionRoutes } from './routes/sessions' + import { createFileRoutes } from './routes/files' import { createProvidersRoutes } from './routes/providers' import { ensureDirectoryExists } from './services/file-operations' @@ -14,12 +14,16 @@ import { opencodeServerManager } from './services/opencode-single-server' import { cleanupOrphanedDirectories } from './services/repo' import { proxyRequest } from './services/proxy' import { logger } from './utils/logger' -import { ENV } from './config' -import { getWorkspacePath, getReposPath, getConfigPath } from '../../shared/src/constants' - -await import('dotenv/config') +import { + getWorkspacePath, + getReposPath, + getConfigPath, + getDatabasePath, + ENV +} from './config' -const { PORT, HOST, DATABASE_PATH: DB_PATH } = ENV +const { PORT, HOST } = ENV.SERVER +const DB_PATH = getDatabasePath() const app = new Hono() @@ -49,7 +53,6 @@ try { app.route('/api/repos', createRepoRoutes(db)) app.route('/api/settings', createSettingsRoutes(db)) app.route('/api/health', createHealthRoutes(db)) -app.route('/api/sessions', createSessionRoutes()) app.route('/api/files', createFileRoutes(db)) app.route('/api/providers', createProvidersRoutes()) @@ -58,7 +61,7 @@ app.all('/api/opencode/*', async (c) => { return proxyRequest(request) }) -const isProduction = process.env.NODE_ENV === 'production' +const isProduction = ENV.SERVER.NODE_ENV === 'production' if (isProduction) { app.use('/*', async (c, next) => { @@ -87,6 +90,30 @@ if (isProduction) { } }) }) + + app.get('/api/network-info', async (c) => { + const os = await import('os') + const interfaces = os.networkInterfaces() + const ips = Object.values(interfaces) + .flat() + .filter(info => info && !info.internal && info.family === 'IPv4') + .map(info => info!.address) + + const requestHost = c.req.header('host') || `localhost:${PORT}` + const protocol = c.req.header('x-forwarded-proto') || 'http' + + return c.json({ + host: HOST, + port: PORT, + requestHost, + protocol, + availableIps: ips, + apiUrls: [ + `${protocol}://localhost:${PORT}`, + ...ips.map(ip => `${protocol}://${ip}:${PORT}`) + ] + }) + }) } let isShuttingDown = false @@ -114,4 +141,4 @@ serve({ hostname: HOST, }) -logger.info(`🚀 OpenCode WebUI API running on http://${HOST}:${PORT}`) \ No newline at end of file +logger.info(`🚀 OpenCode WebUI API running on http://${HOST}:${PORT}`) diff --git a/backend/src/routes/repos.ts b/backend/src/routes/repos.ts index d9681a25..d879169a 100644 --- a/backend/src/routes/repos.ts +++ b/backend/src/routes/repos.ts @@ -4,10 +4,11 @@ import * as db from '../db/queries' import * as repoService from '../services/repo' import { SettingsService } from '../services/settings' import { writeFileContent } from '../services/file-operations' +import { opencodeServerManager } from '../services/opencode-single-server' import { logger } from '../utils/logger' import { withTransactionAsync } from '../db/transactions' import path from 'path' -import { getReposPath } from '../../../shared/src/constants' +import { getReposPath, getWorkspacePath } from '../config' export function createRepoRoutes(database: Database) { const app = new Hono() @@ -138,12 +139,25 @@ export function createRepoRoutes(database: Database) { } const workingDir = path.join(getReposPath(), repo.localPath) - const configPath = `${workingDir}/opencode.json` + const workspaceConfigPath = `${getWorkspacePath()}/opencode.json` + const repoConfigPath = `${workingDir}/opencode.json` + + // Write to workspace as main config + await writeFileContent(workspaceConfigPath, configContent) + + // Also write to repo directory for repo-specific usage + await writeFileContent(repoConfigPath, configContent) - await writeFileContent(configPath, configContent) db.updateRepoConfigName(database, id, configName) logger.info(`Switched config for repo ${id} to '${configName}'`) + logger.info(`Updated workspace config: ${workspaceConfigPath}`) + logger.info(`Updated repo config: ${repoConfigPath}`) + + // Restart OpenCode server to pick up new workspace config + logger.info('Restarting OpenCode server due to workspace config change') + await opencodeServerManager.stop() + await opencodeServerManager.start() const updatedRepo = db.getRepoById(database, id) return c.json(updatedRepo) diff --git a/backend/src/routes/sessions.ts b/backend/src/routes/sessions.ts deleted file mode 100644 index 671e8451..00000000 --- a/backend/src/routes/sessions.ts +++ /dev/null @@ -1,154 +0,0 @@ -import { Hono } from 'hono' -import { logger } from '../utils/logger' -import { ENV } from '../config' - -const OPENCODE_SERVER_PORT = ENV.OPENCODE_SERVER_PORT - -export function createSessionRoutes() { - const app = new Hono() - - app.post('/', async (c) => { - const body = await c.req.json() - - try { - const response = await fetch(`http://localhost:${OPENCODE_SERVER_PORT}/session`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - }) - - if (!response.ok) { - logger.error(`Session creation failed: ${response.status} ${response.statusText}`) - return c.json({ error: 'Session creation failed', status: response.status }, response.status as any) - } - - const data = await response.json() - return c.json(data) - } catch (error) { - logger.error('Failed to create session:', error) - return c.json({ error: 'Failed to create session' }, 500) - } - }) - - app.post('/:sessionID/command', async (c) => { - const sessionID = c.req.param('sessionID') - const body = await c.req.json() - - try { - const response = await fetch(`http://localhost:${OPENCODE_SERVER_PORT}/session/${sessionID}/command`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - }) - - if (!response.ok) { - logger.error(`Command failed: ${response.status} ${response.statusText}`) - return c.json({ error: 'Command failed', status: response.status }, response.status as any) - } - - const data = await response.json() - return c.json(data) - } catch (error) { - logger.error('Failed to send command:', error) - return c.json({ error: 'Failed to send command' }, 500) - } - }) - - app.post('/:sessionID/shell', async (c) => { - const sessionID = c.req.param('sessionID') - const body = await c.req.json() - - try { - const response = await fetch(`http://localhost:${OPENCODE_SERVER_PORT}/session/${sessionID}/shell`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - }) - - if (!response.ok) { - logger.error(`Shell command failed: ${response.status} ${response.statusText}`) - return c.json({ error: 'Shell command failed', status: response.status }, response.status as any) - } - - const data = await response.json() - return c.json(data) - } catch (error) { - logger.error('Failed to send shell command:', error) - return c.json({ error: 'Failed to send shell command' }, 500) - } - }) - - app.post('/:sessionID/abort', async (c) => { - const sessionID = c.req.param('sessionID') - - try { - const response = await fetch(`http://localhost:${OPENCODE_SERVER_PORT}/session/${sessionID}/abort`, { - method: 'POST', - }) - - if (!response.ok) { - logger.error(`Abort failed: ${response.status} ${response.statusText}`) - return c.json({ error: 'Abort failed', status: response.status }, response.status as any) - } - - const data = await response.json() - return c.json(data) - } catch (error) { - logger.error('Failed to abort session:', error) - return c.json({ error: 'Failed to abort session' }, 500) - } - }) - - app.get('/:sessionID/message', async (c) => { - const sessionID = c.req.param('sessionID') - - try { - const response = await fetch(`http://localhost:${OPENCODE_SERVER_PORT}/session/${sessionID}/message`) - - if (!response.ok) { - logger.error(`Get messages failed: ${response.status} ${response.statusText}`) - return c.json({ error: 'Get messages failed', status: response.status }, response.status as any) - } - - const data = await response.json() - return c.json(data) - } catch (error) { - logger.error('Failed to get messages:', error) - return c.json({ error: 'Failed to get messages' }, 500) - } - }) - - app.post('/:sessionID/message', async (c) => { - const sessionID = c.req.param('sessionID') - const body = await c.req.json() - - try { - const response = await fetch(`http://localhost:${OPENCODE_SERVER_PORT}/session/${sessionID}/message`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(body), - }) - - if (!response.ok) { - logger.error(`Send message failed: ${response.status} ${response.statusText}`) - return c.json({ error: 'Send message failed', status: response.status }, response.status as any) - } - - const data = await response.json() - return c.json(data) - } catch (error) { - logger.error('Failed to send message:', error) - return c.json({ error: 'Failed to send message' }, 500) - } - }) - - return app -} diff --git a/backend/src/routes/settings.ts b/backend/src/routes/settings.ts index 1a0c6b79..158e0188 100644 --- a/backend/src/routes/settings.ts +++ b/backend/src/routes/settings.ts @@ -2,6 +2,7 @@ import { Hono } from 'hono' import { z } from 'zod' import type { Database } from 'bun:sqlite' import { SettingsService } from '../services/settings' +import { opencodeServerManager } from '../services/opencode-single-server' import { UserPreferencesSchema, OpenCodeConfigSchema, @@ -98,6 +99,14 @@ export function createSettingsRoutes(db: Database) { const validated = CreateOpenCodeConfigSchema.parse(body) const config = settingsService.createOpenCodeConfig(validated, userId) + + // Restart OpenCode server if this is the new default config + if (validated.isDefault) { + logger.info('Restarting OpenCode server with new default config') + await opencodeServerManager.stop() + await opencodeServerManager.start() + } + return c.json(config) } catch (error) { logger.error('Failed to create OpenCode config:', error) @@ -120,6 +129,13 @@ export function createSettingsRoutes(db: Database) { return c.json({ error: 'Config not found' }, 404) } + // Restart OpenCode server if config was set as default + if (validated.isDefault) { + logger.info('Restarting OpenCode server with updated default config') + await opencodeServerManager.stop() + await opencodeServerManager.start() + } + return c.json(config) } catch (error) { logger.error('Failed to update OpenCode config:', error) diff --git a/backend/src/services/auth.ts b/backend/src/services/auth.ts index 2474165a..7aac4409 100644 --- a/backend/src/services/auth.ts +++ b/backend/src/services/auth.ts @@ -1,6 +1,6 @@ import { promises as fs } from 'fs' import path from 'path' -import { getAuthPath } from '../../../shared/src/constants' +import { getAuthPath } from '../config' import { logger } from '../utils/logger' import { AuthCredentialsSchema } from '../../../shared/src/schemas/auth' import type { z } from 'zod' diff --git a/backend/src/services/file-operations.ts b/backend/src/services/file-operations.ts index 406fd937..cfa19294 100644 --- a/backend/src/services/file-operations.ts +++ b/backend/src/services/file-operations.ts @@ -1,7 +1,7 @@ import { promises as fs } from 'fs' import path from 'path' import { logger } from '../utils/logger' -import { getWorkspacePath, getReposPath } from '../../../shared/src/constants' +import { getReposPath } from '../config' export async function readFileContent(filePath: string): Promise { try { diff --git a/backend/src/services/files.ts b/backend/src/services/files.ts index 5a9bdb53..72a9c6c7 100644 --- a/backend/src/services/files.ts +++ b/backend/src/services/files.ts @@ -11,8 +11,7 @@ import { getFileStats, listDirectory } from './file-operations' -import { FILE_LIMITS, ALLOWED_MIME_TYPES } from '../../../shared/src/constants' -import { getReposPath } from '../../../shared/src/constants' +import { FILE_LIMITS, ALLOWED_MIME_TYPES, getReposPath } from '../config' const SHARED_WORKSPACE_BASE = getReposPath() diff --git a/backend/src/services/opencode-single-server.ts b/backend/src/services/opencode-single-server.ts index e53eff81..b2417bf4 100644 --- a/backend/src/services/opencode-single-server.ts +++ b/backend/src/services/opencode-single-server.ts @@ -1,11 +1,9 @@ -import { spawn } from 'child_process' -import { logger } from '../utils/logger' -import { getWorkspacePath } from '../../../shared/src/constants' -import { execSync } from 'child_process' -import { ENV } from '../config' +import { spawn, execSync } from 'child_process' import path from 'path' +import { logger } from '../utils/logger' +import { getWorkspacePath, ENV } from '../config' -const OPENCODE_SERVER_PORT = ENV.OPENCODE_SERVER_PORT +const OPENCODE_SERVER_PORT = ENV.OPENCODE.PORT const OPENCODE_SERVER_DIRECTORY = getWorkspacePath() class OpenCodeServerManager { @@ -29,7 +27,7 @@ class OpenCodeServerManager { return } - const isDevelopment = process.env.NODE_ENV !== 'production' + const isDevelopment = ENV.SERVER.NODE_ENV !== 'production' const existingProcesses = await this.findProcessesByPort(OPENCODE_SERVER_PORT) if (existingProcesses.length > 0) { @@ -66,29 +64,25 @@ class OpenCodeServerManager { } } - logger.info(`Starting OpenCode server on port ${OPENCODE_SERVER_PORT} (${isDevelopment ? 'development' : 'production'} mode)`) logger.info(`OpenCode server working directory: ${OPENCODE_SERVER_DIRECTORY}`) logger.info(`OpenCode will use ?directory= parameter for session isolation`) - const hostname = isDevelopment ? '0.0.0.0' : '127.0.0.1' this.serverProcess = spawn( 'opencode', - ['serve', '--port', OPENCODE_SERVER_PORT.toString(), '--hostname', hostname], + ['serve', '--port', OPENCODE_SERVER_PORT.toString(), '--hostname', '127.0.0.1'], { cwd: OPENCODE_SERVER_DIRECTORY, detached: !isDevelopment, stdio: isDevelopment ? 'inherit' : 'ignore', env: { ...process.env, - XDG_DATA_HOME: path.join(OPENCODE_SERVER_DIRECTORY, '.opencode/state') + XDG_DATA_HOME: path.join(OPENCODE_SERVER_DIRECTORY, '.opencode/state'), + OPENCODE_CONFIG: path.join(OPENCODE_SERVER_DIRECTORY, '.config/opencode/opencode.json') } } ) - if (!isDevelopment) { - this.serverProcess.unref() - } this.serverPid = this.serverProcess.pid logger.info(`OpenCode server started with PID ${this.serverPid}`) @@ -131,7 +125,7 @@ class OpenCodeServerManager { async checkHealth(): Promise { try { - const response = await fetch(`http://localhost:${OPENCODE_SERVER_PORT}/doc`, { + const response = await fetch(`http://127.0.0.1:${OPENCODE_SERVER_PORT}/doc`, { signal: AbortSignal.timeout(3000) }) return response.ok diff --git a/backend/src/services/proxy.ts b/backend/src/services/proxy.ts index 3dc3b20d..d86d40a0 100644 --- a/backend/src/services/proxy.ts +++ b/backend/src/services/proxy.ts @@ -1,7 +1,7 @@ import { logger } from '../utils/logger' import { ENV } from '../config' -const OPENCODE_SERVER_URL = `http://localhost:${ENV.OPENCODE_SERVER_PORT}` +const OPENCODE_SERVER_URL = `http://127.0.0.1:${ENV.OPENCODE.PORT}` export async function proxyRequest(request: Request) { const url = new URL(request.url) @@ -44,4 +44,4 @@ export async function proxyRequest(request: Request) { headers: { 'Content-Type': 'application/json' }, }) } -} \ No newline at end of file +} diff --git a/backend/src/services/repo.ts b/backend/src/services/repo.ts index 3c301dc5..b9d748a4 100644 --- a/backend/src/services/repo.ts +++ b/backend/src/services/repo.ts @@ -5,7 +5,7 @@ import type { Database } from 'bun:sqlite' import type { Repo, CreateRepoInput } from '../types/repo' import { logger } from '../utils/logger' import { SettingsService } from './settings' -import { getReposPath } from '../../../shared/src/constants' +import { getReposPath } from '../config' import path from 'path' export async function cloneRepo( diff --git a/backend/src/utils/logger.ts b/backend/src/utils/logger.ts index 288a84b6..8122a956 100644 --- a/backend/src/utils/logger.ts +++ b/backend/src/utils/logger.ts @@ -1,3 +1,5 @@ +import { ENV } from '../config' + type LogLevel = 'info' | 'warn' | 'error' | 'debug' class Logger { @@ -7,7 +9,7 @@ class Logger { this.prefix = prefix } - private format(level: LogLevel, message: string, ...args: unknown[]): string { + private format(level: LogLevel, message: string): string { const timestamp = new Date().toISOString() const prefixStr = this.prefix ? `[${this.prefix}] ` : '' return `[${timestamp}] [${level.toUpperCase()}] ${prefixStr}${message}` @@ -26,7 +28,7 @@ class Logger { } debug(message: string, ...args: unknown[]): void { - if (process.env.DEBUG) { + if (ENV.LOGGING.DEBUG) { console.debug(this.format('debug', message), ...args) } } diff --git a/frontend/src/api/files.ts b/frontend/src/api/files.ts new file mode 100644 index 00000000..a987e111 --- /dev/null +++ b/frontend/src/api/files.ts @@ -0,0 +1,23 @@ +import { useQuery } from '@tanstack/react-query' +import { API_BASE_URL } from '@/constants/api' +import type { FileInfo } from '@/types/files' + +const API_BASE = API_BASE_URL + +async function fetchFile(path: string): Promise { + const response = await fetch(`${API_BASE}/api/files/${path}`) + + if (!response.ok) { + throw new Error(`Failed to load file: ${response.statusText}`) + } + + return response.json() +} + +export function useFile(path: string | undefined) { + return useQuery({ + queryKey: ['file', path], + queryFn: () => path ? fetchFile(path) : Promise.reject(new Error('No file path provided')), + enabled: !!path, + }) +} \ No newline at end of file diff --git a/frontend/src/api/providers.ts b/frontend/src/api/providers.ts index 325add66..4dac7b2f 100644 --- a/frontend/src/api/providers.ts +++ b/frontend/src/api/providers.ts @@ -39,6 +39,7 @@ export interface Provider { env: string[]; npm?: string; models: Record; + options?: Record; } export interface ProviderWithModels { diff --git a/frontend/src/components/file-browser/FileBrowser.tsx b/frontend/src/components/file-browser/FileBrowser.tsx index 6bb91a54..d19666ac 100644 --- a/frontend/src/components/file-browser/FileBrowser.tsx +++ b/frontend/src/components/file-browser/FileBrowser.tsx @@ -2,33 +2,18 @@ import { useState, useEffect, useRef, useCallback } from 'react' import { FileTree } from './FileTree' import { FileOperations } from './FileOperations' import { FilePreview } from './FilePreview' +import { MobileFilePreviewModal } from './MobileFilePreviewModal' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' -import { FolderOpen, Upload, RefreshCw, X } from 'lucide-react' +import { FolderOpen, Upload, RefreshCw } from 'lucide-react' import type { FileInfo } from '@/types/files' import { API_BASE_URL } from '@/constants/api' +import { useMobile } from '@/hooks/useMobile' +import { useFile } from '@/api/files' -const API_BASE = API_BASE_URL -// Hook to detect mobile screens -const useIsMobile = () => { - const [isMobile, setIsMobile] = useState(false) - useEffect(() => { - const checkIsMobile = () => { - setIsMobile(window.innerWidth < 768) - } - - checkIsMobile() - window.addEventListener('resize', checkIsMobile) - - return () => window.removeEventListener('resize', checkIsMobile) - }, []) - - return isMobile -} interface FileBrowserProps { basePath?: string @@ -48,42 +33,31 @@ export function FileBrowser({ basePath = '', onFileSelect, embedded = false, ini const [isPreviewModalOpen, setIsPreviewModalOpen] = useState(false) const dropZoneRef = useRef(null) - const isMobile = useIsMobile() - - useEffect(() => { - if (initialSelectedFile) { - const loadInitialFile = async () => { - try { - const url = `${API_BASE}/api/files/${initialSelectedFile}` - - const response = await fetch(url) - - if (response.ok) { - const fileData = await response.json() - setSelectedFile(fileData) - if (isMobile) { - setIsPreviewModalOpen(true) - } - } else { - const errorText = await response.text() - console.error('[FileBrowser] Failed to load file:', errorText) - setError(`Failed to load file: ${errorText}`) - } - } catch (err) { - console.error('[FileBrowser] Failed to load initial file:', err) - setError(err instanceof Error ? err.message : 'Failed to load file') - } - } - loadInitialFile() + const isMobile = useMobile() + + const { data: initialFileData, error: initialFileError } = useFile(initialSelectedFile) + +useEffect(() => { + if (initialFileData) { + setSelectedFile(initialFileData) + if (isMobile) { + setIsPreviewModalOpen(true) } - }, [initialSelectedFile, isMobile, basePath]) + } +}, [initialFileData, isMobile]) + +useEffect(() => { + if (initialFileError) { + setError(initialFileError.message) + } +}, [initialFileError]) const loadFiles = async (path: string) => { setLoading(true) setError(null) try { - const response = await fetch(`${API_BASE}/api/files/${path}`) + const response = await fetch(`${API_BASE_URL}/api/files/${path}`) if (!response.ok) { throw new Error(`Failed to load files: ${response.statusText}`) } @@ -107,7 +81,7 @@ export function FileBrowser({ basePath = '', onFileSelect, embedded = false, ini // Fetch the full file content when selecting a file setLoading(true) try { - const response = await fetch(`${API_BASE}/api/files/${file.path}`) + const response = await fetch(`${API_BASE_URL}/api/files/${file.path}`) if (!response.ok) { throw new Error(`Failed to load file: ${response.statusText}`) } @@ -133,14 +107,6 @@ export function FileBrowser({ basePath = '', onFileSelect, embedded = false, ini setSelectedFile(null) } - const handleTouchStart = (e: React.TouchEvent) => { - e.preventDefault() - } - - const handleTouchEnd = (e: React.TouchEvent) => { - e.preventDefault() - } - const handleDirectoryClick = (path: string) => { loadFiles(path) } @@ -154,7 +120,7 @@ export function FileBrowser({ basePath = '', onFileSelect, embedded = false, ini formData.append('file', files[0]) try { - const response = await fetch(`${API_BASE}/api/files/${currentPath}`, { + const response = await fetch(`${API_BASE_URL}/api/files/${currentPath}`, { method: 'POST', body: formData, }) @@ -171,7 +137,7 @@ export function FileBrowser({ basePath = '', onFileSelect, embedded = false, ini const handleCreateFile = async (name: string, type: 'file' | 'folder') => { try { - const response = await fetch(`${API_BASE}/api/files/${currentPath}/${name}`, { + const response = await fetch(`${API_BASE_URL}/api/files/${currentPath}/${name}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type, content: type === 'file' ? '' : undefined }), @@ -189,7 +155,7 @@ export function FileBrowser({ basePath = '', onFileSelect, embedded = false, ini const handleDelete = async (path: string) => { try { - const response = await fetch(`${API_BASE}/api/files/${path}`, { + const response = await fetch(`${API_BASE_URL}/api/files/${path}`, { method: 'DELETE', }) @@ -206,7 +172,7 @@ export function FileBrowser({ basePath = '', onFileSelect, embedded = false, ini const handleRename = async (oldPath: string, newPath: string) => { try { - const response = await fetch(`${API_BASE}/api/files/${oldPath}`, { + const response = await fetch(`${API_BASE_URL}/api/files/${oldPath}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ newPath }), @@ -305,11 +271,11 @@ export function FileBrowser({ basePath = '', onFileSelect, embedded = false, ini )} {/* Mobile: Full width file listing, Desktop: Split view */} -
-
+
+
setSearchQuery(e.target.value)} className="flex-1" @@ -330,7 +296,7 @@ export function FileBrowser({ basePath = '', onFileSelect, embedded = false, ini
)} -
+
{loading ? (
@@ -352,7 +318,7 @@ export function FileBrowser({ basePath = '', onFileSelect, embedded = false, ini {/* Desktop only: Preview panel */} {!isMobile && ( -
+
{selectedFile && !selectedFile.isDirectory ? ( ) : ( @@ -365,17 +331,12 @@ export function FileBrowser({ basePath = '', onFileSelect, embedded = false, ini
{/* Mobile: File Preview Modal */} - {isMobile && selectedFile && !selectedFile.isDirectory && ( - !open && handleCloseModal()}> - -
- -
-
-
- )} +
) } @@ -410,19 +371,6 @@ export function FileBrowser({ basePath = '', onFileSelect, embedded = false, ini
-
- setSearchQuery(e.target.value)} - className="flex-1" - /> - -
- {error && (
{error} @@ -430,12 +378,12 @@ onUpload={handleUpload} )} - + {/* Mobile: Full width file listing, Desktop: Split view */} -
+
setSearchQuery(e.target.value)} className="flex-1" @@ -452,7 +400,7 @@ onUpload={handleUpload}
) : ( -
+
+
{selectedFile && !selectedFile.isDirectory ? ( ) : ( @@ -483,34 +431,11 @@ onUpload={handleUpload} {/* Mobile: File Preview Modal */} - {isMobile && selectedFile && !selectedFile.isDirectory && ( - !open && handleCloseModal()}> - - -
-
-
- - {selectedFile.name} - - -
-
- -
-
-
- )} +
) -} \ No newline at end of file +} diff --git a/frontend/src/components/file-browser/FileBrowserSheet.tsx b/frontend/src/components/file-browser/FileBrowserSheet.tsx index db3868f2..ee10a90e 100644 --- a/frontend/src/components/file-browser/FileBrowserSheet.tsx +++ b/frontend/src/components/file-browser/FileBrowserSheet.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react' +import { useEffect, useState } from 'react' import { FileBrowser } from './FileBrowser' import { Button } from '@/components/ui/button' import { X } from 'lucide-react' @@ -12,6 +12,7 @@ interface FileBrowserSheetProps { } export function FileBrowserSheet({ isOpen, onClose, basePath = '', repoName, initialSelectedFile }: FileBrowserSheetProps) { + const [isEditing, setIsEditing] = useState(false) useEffect(() => { const handleEscape = (e: KeyboardEvent) => { @@ -20,13 +21,19 @@ export function FileBrowserSheet({ isOpen, onClose, basePath = '', repoName, ini } } + const handleEditModeChange = (event: CustomEvent<{ isEditing: boolean }>) => { + setIsEditing(event.detail.isEditing) + } + if (isOpen) { document.addEventListener('keydown', handleEscape) + document.addEventListener('editModeChange', handleEditModeChange as EventListener) document.body.style.overflow = 'hidden' } return () => { document.removeEventListener('keydown', handleEscape) + document.removeEventListener('editModeChange', handleEditModeChange as EventListener) document.body.style.overflow = 'unset' } }, [isOpen, onClose]) @@ -49,14 +56,16 @@ export function FileBrowserSheet({ isOpen, onClose, basePath = '', repoName, ini )}
- + {!isEditing && ( + + )}
diff --git a/frontend/src/components/file-browser/FilePreview.tsx b/frontend/src/components/file-browser/FilePreview.tsx index 3854c50e..4c62772c 100644 --- a/frontend/src/components/file-browser/FilePreview.tsx +++ b/frontend/src/components/file-browser/FilePreview.tsx @@ -69,6 +69,8 @@ export function FilePreview({ file, hideHeader = false, isMobileModal = false, o const content = decodeBase64(file.content) setEditContent(content) setViewMode('edit') + const event = new CustomEvent('editModeChange', { detail: { isEditing: true } }) + window.dispatchEvent(event) } catch (err) { console.error('Failed to load content for editing:', err) } @@ -89,6 +91,8 @@ export function FilePreview({ file, hideHeader = false, isMobileModal = false, o } setViewMode('preview') + const editEvent = new CustomEvent('editModeChange', { detail: { isEditing: false } }) + window.dispatchEvent(editEvent) const event = new CustomEvent('fileSaved', { detail: { path: file.path, content: editContent } }) window.dispatchEvent(event) } catch (err) { @@ -101,6 +105,8 @@ export function FilePreview({ file, hideHeader = false, isMobileModal = false, o const handleCancel = () => { setEditContent('') setViewMode('preview') + const event = new CustomEvent('editModeChange', { detail: { isEditing: false } }) + window.dispatchEvent(event) } const isTextFile = file.mimeType?.startsWith('text/') || @@ -127,7 +133,7 @@ export function FilePreview({ file, hideHeader = false, isMobileModal = false, o