diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7fc1898..bfa96eb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -332,6 +332,41 @@ jobs: includeUpdaterJson: true retryAttempts: 2 + build-docker: + needs: create-draft-release + name: Build Docker image + runs-on: ubuntu-22.04 + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract version from tag + id: version + run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: | + ghcr.io/${{ github.repository }}:${{ steps.version.outputs.version }} + ghcr.io/${{ github.repository }}:latest + cache-from: type=gha + cache-to: type=gha,mode=max + publish-release: needs: - create-draft-release diff --git a/AGENTS.md b/AGENTS.md index 0c10d58..e6c9914 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,11 +4,12 @@ This file provides guidance to Code Agent when working with code in this reposit ## 项目概述 -codeg 是一个桌面应用,用于聚合和浏览本地 AI 编码代理的会话记录。它从多个代理(Claude Code、Codex、OpenCode)的本地文件系统中读取会话数据,统一格式后在 UI 中展示。 +codeg 是一个多模式应用,支持桌面客户端和独立服务器部署,用于聚合和浏览本地 AI 编码代理的会话记录。它从多个代理(Claude Code、Codex、OpenCode、Gemini CLI、OpenClaw、Cline)的本地文件系统中读取会话数据,统一格式后在 UI 中展示。 ## 技术栈 - **桌面运行时**: Tauri 2(Rust 后端 + webview 前端) +- **服务器运行时**: 独立 Rust 二进制(Axum HTTP + WebSocket,无需 Tauri/GUI) - **前端**: Next.js 16(静态导出模式)+ React 19 + TypeScript(strict) - **样式**: Tailwind CSS v4 + shadcn/ui(radix-maia 风格) - **包管理器**: pnpm @@ -16,7 +17,7 @@ codeg 是一个桌面应用,用于聚合和浏览本地 AI 编码代理的会 ## 开发命令 ```bash -# 启动完整应用(Tauri + Next.js Turbopack 开发服务器) +# 启动完整桌面应用(Tauri + Next.js Turbopack 开发服务器) pnpm tauri dev # 仅启动前端 @@ -28,11 +29,18 @@ pnpm build # 构建 Tauri 桌面应用 pnpm tauri build +# 启动独立服务器(无需 Tauri/GUI) +pnpm server:dev + +# 构建服务器 release 二进制 +pnpm server:build + # Lint 检查 pnpm eslint . # Rust 检查(在 src-tauri/ 目录下执行) -cargo check +cargo check # 桌面模式(默认) +cargo check --bin codeg-server --no-default-features # 服务器模式 cargo clippy cargo build ``` @@ -41,31 +49,59 @@ cargo build ## 架构 +### 双模式运行 + +项目通过 Cargo feature flags 支持两种运行模式: + +- **`tauri-runtime`(默认)**:完整桌面应用,包含 Tauri 窗口管理、系统通知、自动更新等 +- **无 feature(`--no-default-features`)**:独立服务器模式,仅编译 Axum HTTP API + WebSocket + +### 共享核心 + +- **`app_state.rs`** — `AppState` 共享状态结构,两种模式通过 `EventEmitter` 枚举区分事件发射方式 +- **`web/event_bridge.rs`** — `EventEmitter::Tauri(AppHandle)` 或 `EventEmitter::WebOnly(Arc)` +- **`web/router.rs`** — Axum 路由,接受 `Arc` +- **`web/handlers/`** — 146 个 HTTP API 端点,全部使用 `Extension>` + ### Rust 后端(`src-tauri/src/`) 后端负责读取和解析本地文件系统上的代理会话文件: +- **`app_state.rs`** — 共享状态(db、连接管理器、终端管理器、事件广播器) - **`models/`** — 共享数据结构 - **`parsers/`** — 每个代理一个解析器 -- **`commands/sessions.rs`** — 暴露给前端的 Tauri 命令 +- **`commands/`** — 业务逻辑(`_core` 函数供两种模式共用,`#[tauri::command]` 函数仅桌面模式) +- **`web/`** — Axum HTTP API + WebSocket + 静态文件服务 +- **`acp/`** — Agent Client Protocol 连接管理 +- **`terminal/`** — PTY 终端管理 +- **`db/`** — SeaORM + SQLite ### 前端(`src/`) -- **`lib/types.ts`** — Rust 模型的 TypeScript 镜像。`AgentType` 为 `"claude_code" | "codex" | "open_code"`(snake_case,与 Rust serde 一致) -- **`lib/tauri.ts`** — 对每个 Tauri 命令的类型化 `invoke()` 封装 +- **`lib/transport/`** — Transport 抽象层,自动检测 Tauri/Web 环境切换 `invoke()`/`fetch()` +- **`lib/types.ts`** — Rust 模型的 TypeScript 镜像 - **`app/`** — Next.js 页面,不使用动态路由 +- **`app/login/`** — Web 模式 token 登录页 - **`components/`** — 项目组件 - **`components/ui/`** — shadcn 组件 ### 数据流 -前端调用 `invoke()` → Tauri 命令 → 解析器读取本地文件 → 返回 `SessionSummary[]` / `SessionDetail` → React 渲染 +桌面模式:前端 `invoke()` → Tauri 命令 → 业务逻辑 → 返回数据 +服务器模式:前端 `fetch()` → Axum HTTP API → 同一业务逻辑 → 返回 JSON + +### 条件编译约定 + +- `#[cfg(feature = "tauri-runtime")]` — 仅桌面模式编译(Tauri 窗口、通知、`tauri::State` 参数等) +- `#[cfg_attr(feature = "tauri-runtime", tauri::command)]` — 函数始终可用,仅在桌面模式标记为 Tauri 命令 +- `_core` 后缀函数 — 接受普通引用参数(`&AppDatabase`、`&EventEmitter`),供 Web handlers 和 Tauri 命令共用 ## 关键约束 - **仅支持静态导出**:`next.config.ts` 设置 `output: "export"`,不支持动态路由(`[param]`),必须使用查询参数替代 - **路径别名**:`@/*` 映射到 `./src/*`,导入写法为 `@/lib/utils`、`@/components/ui/button` - **Rust serde 约定**:`AgentType` 序列化为 snake_case(`claude_code`、`open_code`)。Tauri 命令参数在 JS 侧使用 camelCase,Rust 侧使用 snake_case +- **服务器部署**:通过环境变量配置(`CODEG_PORT`、`CODEG_HOST`、`CODEG_TOKEN`、`CODEG_DATA_DIR`、`CODEG_STATIC_DIR`) ## 代码风格 diff --git a/CLAUDE.md b/CLAUDE.md index 842aa54..cf0311d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,11 +4,12 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## 项目概述 -codeg 是一个桌面应用,用于聚合和浏览本地 AI 编码代理的会话记录。它从多个代理(Claude Code、Codex、OpenCode)的本地文件系统中读取会话数据,统一格式后在 UI 中展示。 +codeg 是一个多模式应用,支持桌面客户端和独立服务器部署,用于聚合和浏览本地 AI 编码代理的会话记录。它从多个代理(Claude Code、Codex、OpenCode、Gemini CLI、OpenClaw、Cline)的本地文件系统中读取会话数据,统一格式后在 UI 中展示。 ## 技术栈 - **桌面运行时**: Tauri 2(Rust 后端 + webview 前端) +- **服务器运行时**: 独立 Rust 二进制(Axum HTTP + WebSocket,无需 Tauri/GUI) - **前端**: Next.js 16(静态导出模式)+ React 19 + TypeScript(strict) - **样式**: Tailwind CSS v4 + shadcn/ui(radix-maia 风格) - **包管理器**: pnpm @@ -16,7 +17,7 @@ codeg 是一个桌面应用,用于聚合和浏览本地 AI 编码代理的会 ## 开发命令 ```bash -# 启动完整应用(Tauri + Next.js Turbopack 开发服务器) +# 启动完整桌面应用(Tauri + Next.js Turbopack 开发服务器) pnpm tauri dev # 仅启动前端 @@ -28,11 +29,18 @@ pnpm build # 构建 Tauri 桌面应用 pnpm tauri build +# 启动独立服务器(无需 Tauri/GUI) +pnpm server:dev + +# 构建服务器 release 二进制 +pnpm server:build + # Lint 检查 pnpm eslint . # Rust 检查(在 src-tauri/ 目录下执行) -cargo check +cargo check # 桌面模式(默认) +cargo check --bin codeg-server --no-default-features # 服务器模式 cargo clippy cargo build ``` @@ -41,31 +49,59 @@ cargo build ## 架构 +### 双模式运行 + +项目通过 Cargo feature flags 支持两种运行模式: + +- **`tauri-runtime`(默认)**:完整桌面应用,包含 Tauri 窗口管理、系统通知、自动更新等 +- **无 feature(`--no-default-features`)**:独立服务器模式,仅编译 Axum HTTP API + WebSocket + +### 共享核心 + +- **`app_state.rs`** — `AppState` 共享状态结构,两种模式通过 `EventEmitter` 枚举区分事件发射方式 +- **`web/event_bridge.rs`** — `EventEmitter::Tauri(AppHandle)` 或 `EventEmitter::WebOnly(Arc)` +- **`web/router.rs`** — Axum 路由,接受 `Arc` +- **`web/handlers/`** — 146 个 HTTP API 端点,全部使用 `Extension>` + ### Rust 后端(`src-tauri/src/`) 后端负责读取和解析本地文件系统上的代理会话文件: +- **`app_state.rs`** — 共享状态(db、连接管理器、终端管理器、事件广播器) - **`models/`** — 共享数据结构 - **`parsers/`** — 每个代理一个解析器 -- **`commands/sessions.rs`** — 暴露给前端的 Tauri 命令 +- **`commands/`** — 业务逻辑(`_core` 函数供两种模式共用,`#[tauri::command]` 函数仅桌面模式) +- **`web/`** — Axum HTTP API + WebSocket + 静态文件服务 +- **`acp/`** — Agent Client Protocol 连接管理 +- **`terminal/`** — PTY 终端管理 +- **`db/`** — SeaORM + SQLite ### 前端(`src/`) -- **`lib/types.ts`** — Rust 模型的 TypeScript 镜像。`AgentType` 为 `"claude_code" | "codex" | "open_code"`(snake_case,与 Rust serde 一致) -- **`lib/tauri.ts`** — 对每个 Tauri 命令的类型化 `invoke()` 封装 +- **`lib/transport/`** — Transport 抽象层,自动检测 Tauri/Web 环境切换 `invoke()`/`fetch()` +- **`lib/types.ts`** — Rust 模型的 TypeScript 镜像 - **`app/`** — Next.js 页面,不使用动态路由 +- **`app/login/`** — Web 模式 token 登录页 - **`components/`** — 项目组件 - **`components/ui/`** — shadcn 组件 ### 数据流 -前端调用 `invoke()` → Tauri 命令 → 解析器读取本地文件 → 返回 `SessionSummary[]` / `SessionDetail` → React 渲染 +桌面模式:前端 `invoke()` → Tauri 命令 → 业务逻辑 → 返回数据 +服务器模式:前端 `fetch()` → Axum HTTP API → 同一业务逻辑 → 返回 JSON + +### 条件编译约定 + +- `#[cfg(feature = "tauri-runtime")]` — 仅桌面模式编译(Tauri 窗口、通知、`tauri::State` 参数等) +- `#[cfg_attr(feature = "tauri-runtime", tauri::command)]` — 函数始终可用,仅在桌面模式标记为 Tauri 命令 +- `_core` 后缀函数 — 接受普通引用参数(`&AppDatabase`、`&EventEmitter`),供 Web handlers 和 Tauri 命令共用 ## 关键约束 - **仅支持静态导出**:`next.config.ts` 设置 `output: "export"`,不支持动态路由(`[param]`),必须使用查询参数替代 - **路径别名**:`@/*` 映射到 `./src/*`,导入写法为 `@/lib/utils`、`@/components/ui/button` - **Rust serde 约定**:`AgentType` 序列化为 snake_case(`claude_code`、`open_code`)。Tauri 命令参数在 JS 侧使用 camelCase,Rust 侧使用 snake_case +- **服务器部署**:通过环境变量配置(`CODEG_PORT`、`CODEG_HOST`、`CODEG_TOKEN`、`CODEG_DATA_DIR`、`CODEG_STATIC_DIR`) ## 代码风格 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5e32873 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,37 @@ +# Stage 1: Build Next.js static export +FROM node:22-alpine AS frontend +RUN corepack enable +WORKDIR /app +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile +COPY . . +RUN pnpm build + +# Stage 2: Build Rust server binary +FROM rust:1.82-bookworm AS backend +WORKDIR /app +COPY src-tauri/ ./src-tauri/ +WORKDIR /app/src-tauri +RUN cargo build --release --bin codeg-server --no-default-features + +# Stage 3: Runtime +FROM debian:bookworm-slim +RUN apt-get update && apt-get install -y \ + libsqlite3-0 \ + git \ + openssh-client \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=backend /app/src-tauri/target/release/codeg-server /usr/local/bin/codeg-server +COPY --from=frontend /app/out /app/web + +ENV CODEG_STATIC_DIR=/app/web +ENV CODEG_DATA_DIR=/data +ENV CODEG_PORT=3080 +ENV CODEG_HOST=0.0.0.0 + +EXPOSE 3080 +VOLUME /data + +CMD ["codeg-server"] diff --git a/README.md b/README.md index 6738585..406545d 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![License](https://img.shields.io/github/license/xintaofei/codeg)](./LICENSE) [![Tauri](https://img.shields.io/badge/Tauri-2.x-24C8DB)](https://tauri.app/) [![Next.js](https://img.shields.io/badge/Next.js-16-black)](https://nextjs.org/) +[![Docker](https://img.shields.io/badge/Docker-ready-2496ED)](./Dockerfile)

English | @@ -47,6 +48,8 @@ MCP/Skills management, and integrated Git/file/terminal workflows. - Skills management (global and project scope) - Git remote account management (GitHub and other Git servers) - Web service mode — access Codeg from any browser for remote work +- **Standalone server deployment** — run `codeg-server` on any Linux/macOS server, access via browser +- **Docker support** — deploy with `docker compose up` for zero-config server setup - Integrated engineering loop (file tree, diff, git changes, commit, terminal) ## Project Boot @@ -105,7 +108,7 @@ Current writable targets: - Node.js `>=22` (recommended) - pnpm `>=10` - Rust stable (2021 edition) -- Tauri 2 build dependencies +- Tauri 2 build dependencies (desktop mode only) Linux (Debian/Ubuntu) example: @@ -123,18 +126,24 @@ sudo apt-get install -y \ ```bash pnpm install +# Frontend static export to out/ +pnpm build + # Full desktop app (Tauri + Next.js) pnpm tauri dev # Frontend only pnpm dev -# Frontend static export to out/ -pnpm build - # Desktop build pnpm tauri build +# Standalone server (no Tauri/GUI required) +pnpm server:dev + +# Build server release binary +pnpm server:build + # Lint pnpm eslint . @@ -144,22 +153,71 @@ cargo clippy cargo build ``` +### Server Deployment + +Codeg can run as a standalone web server without a desktop environment. + +#### Option 1: Direct binary + +```bash +# Build the server binary +cd src-tauri +cargo build --release --bin codeg-server --no-default-features + +# Run it +CODEG_PORT=3080 CODEG_STATIC_DIR=../out ./target/release/codeg-server +``` + +Environment variables: + +| Variable | Default | Description | +| --- | --- | --- | +| `CODEG_PORT` | `3080` | HTTP port | +| `CODEG_HOST` | `0.0.0.0` | Bind address | +| `CODEG_TOKEN` | *(random)* | Auth token (printed to stderr on start) | +| `CODEG_DATA_DIR` | `~/.local/share/codeg` | SQLite database directory | +| `CODEG_STATIC_DIR` | `./web` or `./out` | Next.js static export directory | + +#### Option 2: Docker + +```bash +# Build and run +docker compose up -d + +# Or build manually +docker build -t codeg . +docker run -p 3080:3080 -v codeg-data:/data codeg +``` + ## Architecture ```text Next.js 16 (Static Export) + React 19 | - | invoke() + | invoke() (desktop) / fetch() + WebSocket (web) v -Tauri 2 Commands (Rust) - |- ACP Manager - |- Parsers (local session ingestion) - |- Git / File Tree / Terminal runtime - |- MCP marketplace + local config writer - |- SeaORM + SQLite + ┌─────────────────────────┐ + │ Transport Abstraction │ + │ (Tauri IPC or HTTP/WS) │ + └─────────────────────────┘ | v -Local Filesystem / Local Agent Data / Git Repos +┌─── Tauri Desktop ───┐ ┌─── codeg-server ───┐ +│ Tauri 2 Commands │ │ Axum HTTP + WS │ +│ (window management) │ │ (standalone mode) │ +└──────────┬───────────┘ └──────────┬──────────┘ + └──────────┬───────────────┘ + v + Shared Rust Core + |- AppState + |- ACP Manager + |- Parsers (session ingestion) + |- Git / File Tree / Terminal + |- MCP marketplace + config + |- SeaORM + SQLite + | + v + Local Filesystem / Git Repos ``` ## Constraints @@ -174,6 +232,7 @@ Local Filesystem / Local Agent Data / Git Repos - Local-first by default for parsing, storage, and project operations - Network access happens only on user-triggered actions - System proxy support for enterprise environments +- Web service mode uses token-based authentication ## License diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..6565f77 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +services: + codeg: + build: . + ports: + - "3080:3080" + volumes: + - codeg-data:/data + environment: + - CODEG_TOKEN=${CODEG_TOKEN:-} + - CODEG_PORT=3080 + - CODEG_HOST=0.0.0.0 + +volumes: + codeg-data: diff --git a/docs/readme/README.ar.md b/docs/readme/README.ar.md index be4a7b3..4aab258 100644 --- a/docs/readme/README.ar.md +++ b/docs/readme/README.ar.md @@ -4,6 +4,7 @@ [![License](https://img.shields.io/github/license/xintaofei/codeg)](../../LICENSE) [![Tauri](https://img.shields.io/badge/Tauri-2.x-24C8DB)](https://tauri.app/) [![Next.js](https://img.shields.io/badge/Next.js-16-black)](https://nextjs.org/) +[![Docker](https://img.shields.io/badge/Docker-ready-2496ED)](../../Dockerfile)

English | @@ -46,6 +47,8 @@ OpenClaw، وغيرها) في تطبيق سطح مكتب وخدمة ويب — - إدارة Skills (نطاق عام ونطاق المشروع) - إدارة حسابات Git البعيدة (GitHub وخوادم Git الأخرى) - وضع خدمة الويب — الوصول إلى Codeg من أي متصفح للعمل عن بُعد +- **نشر خادم مستقل** — شغّل `codeg-server` على أي خادم Linux/macOS، والوصول عبر المتصفح +- **دعم Docker** — انشر باستخدام `docker compose up` لإعداد الخادم بدون تكوين - حلقة هندسية متكاملة (شجرة الملفات، الفروقات، تغييرات git، الإيداع، الطرفية) ## مُنشئ المشروع @@ -104,7 +107,7 @@ OpenClaw، وغيرها) في تطبيق سطح مكتب وخدمة ويب — - Node.js `>=22` (مُوصى به) - pnpm `>=10` - Rust stable (2021 edition) -- تبعيات بناء Tauri 2 +- تبعيات بناء Tauri 2 (وضع سطح المكتب فقط) مثال على Linux (Debian/Ubuntu): @@ -122,18 +125,24 @@ sudo apt-get install -y \ ```bash pnpm install +# تصدير ثابت للواجهة الأمامية إلى out/ +pnpm build + # تطبيق سطح المكتب الكامل (Tauri + Next.js) pnpm tauri dev # الواجهة الأمامية فقط pnpm dev -# تصدير ثابت للواجهة الأمامية إلى out/ -pnpm build - # بناء تطبيق سطح المكتب pnpm tauri build +# خادم مستقل (بدون Tauri/واجهة رسومية) +pnpm server:dev + +# بناء الملف التنفيذي للخادم +pnpm server:build + # فحص الأكواد pnpm eslint . @@ -143,22 +152,71 @@ cargo clippy cargo build ``` +### نشر الخادم + +يمكن تشغيل Codeg كخادم ويب مستقل بدون بيئة سطح مكتب. + +#### الخيار 1: الملف التنفيذي المباشر + +```bash +# بناء الملف التنفيذي للخادم +cd src-tauri +cargo build --release --bin codeg-server --no-default-features + +# التشغيل +CODEG_PORT=3080 CODEG_STATIC_DIR=../out ./target/release/codeg-server +``` + +متغيرات البيئة: + +| المتغير | الافتراضي | الوصف | +| --- | --- | --- | +| `CODEG_PORT` | `3080` | منفذ HTTP | +| `CODEG_HOST` | `0.0.0.0` | عنوان الربط | +| `CODEG_TOKEN` | *(عشوائي)* | رمز المصادقة (يُطبع في stderr عند البدء) | +| `CODEG_DATA_DIR` | `~/.local/share/codeg` | دليل قاعدة بيانات SQLite | +| `CODEG_STATIC_DIR` | `./web` أو `./out` | دليل التصدير الثابت لـ Next.js | + +#### الخيار 2: Docker + +```bash +# البناء والتشغيل +docker compose up -d + +# أو البناء يدويًا +docker build -t codeg . +docker run -p 3080:3080 -v codeg-data:/data codeg +``` + ## الهندسة المعمارية ```text Next.js 16 (Static Export) + React 19 | - | invoke() + | invoke() (desktop) / fetch() + WebSocket (web) v -Tauri 2 Commands (Rust) - |- ACP Manager - |- Parsers (local session ingestion) - |- Git / File Tree / Terminal runtime - |- MCP marketplace + local config writer - |- SeaORM + SQLite + ┌─────────────────────────┐ + │ Transport Abstraction │ + │ (Tauri IPC or HTTP/WS) │ + └─────────────────────────┘ | v -Local Filesystem / Local Agent Data / Git Repos +┌─── Tauri Desktop ───┐ ┌─── codeg-server ───┐ +│ Tauri 2 Commands │ │ Axum HTTP + WS │ +│ (window management) │ │ (standalone mode) │ +└──────────┬───────────┘ └──────────┬──────────┘ + └──────────┬───────────────┘ + v + Shared Rust Core + |- AppState + |- ACP Manager + |- Parsers (session ingestion) + |- Git / File Tree / Terminal + |- MCP marketplace + config + |- SeaORM + SQLite + | + v + Local Filesystem / Git Repos ``` ## القيود @@ -173,6 +231,7 @@ Local Filesystem / Local Agent Data / Git Repos - محلي أولاً بشكل افتراضي للتحليل والتخزين وعمليات المشروع - الوصول إلى الشبكة يحدث فقط عند الإجراءات التي يبدأها المستخدم - دعم بروكسي النظام لبيئات المؤسسات +- وضع خدمة الويب يستخدم مصادقة قائمة على الرموز ## الترخيص diff --git a/docs/readme/README.de.md b/docs/readme/README.de.md index 4a9c96a..342d5a3 100644 --- a/docs/readme/README.de.md +++ b/docs/readme/README.de.md @@ -4,6 +4,7 @@ [![License](https://img.shields.io/github/license/xintaofei/codeg)](../../LICENSE) [![Tauri](https://img.shields.io/badge/Tauri-2.x-24C8DB)](https://tauri.app/) [![Next.js](https://img.shields.io/badge/Next.js-16-black)](https://nextjs.org/) +[![Docker](https://img.shields.io/badge/Docker-ready-2496ED)](../../Dockerfile)

English | @@ -48,6 +49,8 @@ Git/Datei/Terminal-Workflows. - Skills-Verwaltung (global und projektbezogen) - Git-Remote-Kontoverwaltung (GitHub und andere Git-Server) - Webdienst-Modus — Zugriff auf Codeg über jeden Browser für Remote-Arbeit +- Standalone-Server-Bereitstellung — codeg-server auf jedem Linux/macOS-Server ausführen, Zugriff über den Browser +- Docker-Unterstützung — Bereitstellung mit docker compose up für konfigurationsfreien Serverbetrieb - Integrierter Engineering-Kreislauf (Dateibaum, Diff, Git-Änderungen, Commit, Terminal) ## Projekt-Starter @@ -106,7 +109,7 @@ Aktuelle beschreibbare Ziele: - Node.js `>=22` (empfohlen) - pnpm `>=10` - Rust stable (2021 edition) -- Tauri-2-Build-Abhängigkeiten +- Tauri-2-Build-Abhängigkeiten (nur Desktop-Modus) Linux-Beispiel (Debian/Ubuntu): @@ -124,18 +127,24 @@ sudo apt-get install -y \ ```bash pnpm install +# Frontend-Statikexport nach out/ +pnpm build + # Vollständige Desktop-App (Tauri + Next.js) pnpm tauri dev # Nur Frontend pnpm dev -# Frontend-Statikexport nach out/ -pnpm build - # Desktop-Build pnpm tauri build +# Standalone-Server (kein Tauri/GUI erforderlich) +pnpm server:dev + +# Server-Release-Binary erstellen +pnpm server:build + # Lint pnpm eslint . @@ -145,22 +154,60 @@ cargo clippy cargo build ``` +## Server-Bereitstellung + +Codeg kann als eigenständiger Webserver ohne Tauri-Abhängigkeiten oder GUI betrieben werden. + +### Option 1: Direktes Binary + +```bash +pnpm server:build +./target/release/codeg-server +``` + +| Variable | Standardwert | Beschreibung | +| --- | --- | --- | +| CODEG_PORT | 3080 | HTTP-Port | +| CODEG_HOST | 0.0.0.0 | Bind-Adresse | +| CODEG_TOKEN | (zufällig) | Authentifizierungstoken | +| CODEG_DATA_DIR | ~/.local/share/codeg | SQLite-Datenbankverzeichnis | +| CODEG_STATIC_DIR | ./web oder ./out | Next.js-Statikexport-Verzeichnis | + +### Option 2: Docker + +```bash +docker compose up +``` + ## Architektur ```text Next.js 16 (Static Export) + React 19 | - | invoke() + | invoke() (desktop) / fetch() + WebSocket (web) v -Tauri 2 Commands (Rust) - |- ACP Manager - |- Parsers (local session ingestion) - |- Git / File Tree / Terminal runtime - |- MCP marketplace + local config writer - |- SeaORM + SQLite + ┌─────────────────────────┐ + │ Transport Abstraction │ + │ (Tauri IPC or HTTP/WS) │ + └─────────────────────────┘ | v -Local Filesystem / Local Agent Data / Git Repos +┌─── Tauri Desktop ───┐ ┌─── codeg-server ───┐ +│ Tauri 2 Commands │ │ Axum HTTP + WS │ +│ (window management) │ │ (standalone mode) │ +└──────────┬───────────┘ └──────────┬──────────┘ + └──────────┬───────────────┘ + v + Shared Rust Core + |- AppState + |- ACP Manager + |- Parsers (session ingestion) + |- Git / File Tree / Terminal + |- MCP marketplace + config + |- SeaORM + SQLite + | + v + Local Filesystem / Git Repos ``` ## Einschränkungen @@ -175,6 +222,7 @@ Local Filesystem / Local Agent Data / Git Repos - Standardmäßig lokal für Analyse, Speicherung und Projektoperationen - Netzwerkzugriff erfolgt nur bei benutzergesteuerten Aktionen - Systemproxy-Unterstützung für Unternehmensumgebungen +- Der Webdienst-Modus verwendet tokenbasierte Authentifizierung ## Lizenz diff --git a/docs/readme/README.es.md b/docs/readme/README.es.md index c2d3800..ac1bf74 100644 --- a/docs/readme/README.es.md +++ b/docs/readme/README.es.md @@ -4,6 +4,7 @@ [![License](https://img.shields.io/github/license/xintaofei/codeg)](../../LICENSE) [![Tauri](https://img.shields.io/badge/Tauri-2.x-24C8DB)](https://tauri.app/) [![Next.js](https://img.shields.io/badge/Next.js-16-black)](https://nextjs.org/) +[![Docker](https://img.shields.io/badge/Docker-ready-2496ED)](../../Dockerfile)

English | @@ -48,6 +49,8 @@ flujos integrados de Git/archivos/terminal. - Gestión de Skills (ámbito global y por proyecto) - Gestión de cuentas remotas de Git (GitHub y otros servidores Git) - Modo de servicio web — accede a Codeg desde cualquier navegador para trabajo remoto +- Despliegue como servidor independiente — ejecuta codeg-server en cualquier servidor Linux/macOS, accede desde el navegador +- Soporte de Docker — despliega con docker compose up para una configuración sin esfuerzo - Ciclo de ingeniería integrado (árbol de archivos, diff, cambios git, commit, terminal) ## Inicio de Proyecto @@ -106,7 +109,7 @@ Destinos de escritura actuales: - Node.js `>=22` (recomendado) - pnpm `>=10` - Rust stable (2021 edition) -- Dependencias de compilación de Tauri 2 +- Dependencias de compilación de Tauri 2 (solo modo escritorio) Ejemplo para Linux (Debian/Ubuntu): @@ -124,18 +127,24 @@ sudo apt-get install -y \ ```bash pnpm install +# Exportación estática del frontend a out/ +pnpm build + # Aplicación de escritorio completa (Tauri + Next.js) pnpm tauri dev # Solo frontend pnpm dev -# Exportación estática del frontend a out/ -pnpm build - # Compilación de escritorio pnpm tauri build +# Servidor independiente (sin Tauri/GUI necesario) +pnpm server:dev + +# Compilar binario de servidor para producción +pnpm server:build + # Lint pnpm eslint . @@ -145,22 +154,60 @@ cargo clippy cargo build ``` +## Despliegue del servidor + +Codeg puede ejecutarse como un servidor web independiente sin dependencias de Tauri ni GUI. + +### Opción 1: Binario directo + +```bash +pnpm server:build +./target/release/codeg-server +``` + +| Variable | Valor por defecto | Descripción | +| --- | --- | --- | +| CODEG_PORT | 3080 | Puerto HTTP | +| CODEG_HOST | 0.0.0.0 | Dirección de enlace | +| CODEG_TOKEN | (aleatorio) | Token de autenticación | +| CODEG_DATA_DIR | ~/.local/share/codeg | Directorio de base de datos SQLite | +| CODEG_STATIC_DIR | ./web o ./out | Directorio de exportación estática de Next.js | + +### Opción 2: Docker + +```bash +docker compose up +``` + ## Arquitectura ```text Next.js 16 (Static Export) + React 19 | - | invoke() + | invoke() (desktop) / fetch() + WebSocket (web) v -Tauri 2 Commands (Rust) - |- ACP Manager - |- Parsers (local session ingestion) - |- Git / File Tree / Terminal runtime - |- MCP marketplace + local config writer - |- SeaORM + SQLite + ┌─────────────────────────┐ + │ Transport Abstraction │ + │ (Tauri IPC or HTTP/WS) │ + └─────────────────────────┘ | v -Local Filesystem / Local Agent Data / Git Repos +┌─── Tauri Desktop ───┐ ┌─── codeg-server ───┐ +│ Tauri 2 Commands │ │ Axum HTTP + WS │ +│ (window management) │ │ (standalone mode) │ +└──────────┬───────────┘ └──────────┬──────────┘ + └──────────┬───────────────┘ + v + Shared Rust Core + |- AppState + |- ACP Manager + |- Parsers (session ingestion) + |- Git / File Tree / Terminal + |- MCP marketplace + config + |- SeaORM + SQLite + | + v + Local Filesystem / Git Repos ``` ## Restricciones @@ -175,6 +222,7 @@ Local Filesystem / Local Agent Data / Git Repos - Enfoque local por defecto para análisis, almacenamiento y operaciones de proyecto - El acceso a la red solo ocurre mediante acciones iniciadas por el usuario - Soporte de proxy del sistema para entornos empresariales +- El modo de servicio web utiliza autenticación basada en tokens ## Licencia diff --git a/docs/readme/README.fr.md b/docs/readme/README.fr.md index ea6d883..d64a8d3 100644 --- a/docs/readme/README.fr.md +++ b/docs/readme/README.fr.md @@ -4,6 +4,7 @@ [![License](https://img.shields.io/github/license/xintaofei/codeg)](../../LICENSE) [![Tauri](https://img.shields.io/badge/Tauri-2.x-24C8DB)](https://tauri.app/) [![Next.js](https://img.shields.io/badge/Next.js-16-black)](https://nextjs.org/) +[![Docker](https://img.shields.io/badge/Docker-ready-2496ED)](../../Dockerfile)

English | @@ -46,6 +47,8 @@ parallèle via `git worktree`, gestion MCP/Skills et workflows intégrés Git/fi - Gestion des Skills (portée globale et projet) - Gestion des comptes distants Git (GitHub et autres serveurs Git) - Mode service web — accédez à Codeg depuis n'importe quel navigateur pour le travail à distance +- Déploiement en serveur autonome — exécutez codeg-server sur n'importe quel serveur Linux/macOS, accédez via le navigateur +- Support Docker — déployez avec docker compose up pour une configuration serveur sans effort - Boucle d'ingénierie intégrée (arborescence de fichiers, diff, changements git, commit, terminal) ## Lanceur de projet @@ -104,7 +107,7 @@ Cibles en écriture actuelles : - Node.js `>=22` (recommandé) - pnpm `>=10` - Rust stable (2021 edition) -- Dépendances de build Tauri 2 +- Dépendances de build Tauri 2 (mode bureau uniquement) Exemple Linux (Debian/Ubuntu) : @@ -122,18 +125,24 @@ sudo apt-get install -y \ ```bash pnpm install +# Export statique du frontend vers out/ +pnpm build + # Application de bureau complète (Tauri + Next.js) pnpm tauri dev # Frontend uniquement pnpm dev -# Export statique du frontend vers out/ -pnpm build - # Build de l'application de bureau pnpm tauri build +# Serveur autonome (sans Tauri/GUI requis) +pnpm server:dev + +# Compiler le binaire serveur pour la production +pnpm server:build + # Lint pnpm eslint . @@ -143,22 +152,60 @@ cargo clippy cargo build ``` +## Déploiement du serveur + +Codeg peut fonctionner comme un serveur web autonome sans dépendances Tauri ni interface graphique. + +### Option 1 : Binaire direct + +```bash +pnpm server:build +./target/release/codeg-server +``` + +| Variable | Valeur par défaut | Description | +| --- | --- | --- | +| CODEG_PORT | 3080 | Port HTTP | +| CODEG_HOST | 0.0.0.0 | Adresse de liaison | +| CODEG_TOKEN | (aléatoire) | Jeton d'authentification | +| CODEG_DATA_DIR | ~/.local/share/codeg | Répertoire de base de données SQLite | +| CODEG_STATIC_DIR | ./web ou ./out | Répertoire d'export statique Next.js | + +### Option 2 : Docker + +```bash +docker compose up +``` + ## Architecture ```text Next.js 16 (Static Export) + React 19 | - | invoke() + | invoke() (desktop) / fetch() + WebSocket (web) v -Tauri 2 Commands (Rust) - |- ACP Manager - |- Parsers (local session ingestion) - |- Git / File Tree / Terminal runtime - |- MCP marketplace + local config writer - |- SeaORM + SQLite + ┌─────────────────────────┐ + │ Transport Abstraction │ + │ (Tauri IPC or HTTP/WS) │ + └─────────────────────────┘ | v -Local Filesystem / Local Agent Data / Git Repos +┌─── Tauri Desktop ───┐ ┌─── codeg-server ───┐ +│ Tauri 2 Commands │ │ Axum HTTP + WS │ +│ (window management) │ │ (standalone mode) │ +└──────────┬───────────┘ └──────────┬──────────┘ + └──────────┬───────────────┘ + v + Shared Rust Core + |- AppState + |- ACP Manager + |- Parsers (session ingestion) + |- Git / File Tree / Terminal + |- MCP marketplace + config + |- SeaORM + SQLite + | + v + Local Filesystem / Git Repos ``` ## Contraintes @@ -173,6 +220,7 @@ Local Filesystem / Local Agent Data / Git Repos - Local-first par défaut pour l'analyse, le stockage et les opérations sur le projet - L'accès réseau ne se produit que lors d'actions déclenchées par l'utilisateur - Prise en charge du proxy système pour les environnements d'entreprise +- Le mode service web utilise l'authentification par jeton ## Licence diff --git a/docs/readme/README.ja.md b/docs/readme/README.ja.md index 16e7b59..4a06ddd 100644 --- a/docs/readme/README.ja.md +++ b/docs/readme/README.ja.md @@ -4,6 +4,7 @@ [![License](https://img.shields.io/github/license/xintaofei/codeg)](../../LICENSE) [![Tauri](https://img.shields.io/badge/Tauri-2.x-24C8DB)](https://tauri.app/) [![Next.js](https://img.shields.io/badge/Next.js-16-black)](https://nextjs.org/) +[![Docker](https://img.shields.io/badge/Docker-ready-2496ED)](../../Dockerfile)

English | @@ -46,6 +47,8 @@ Git/ファイル/ターミナル連携ワークフローを提供します。 - Skills 管理(グローバルおよびプロジェクトスコープ) - Git リモートアカウント管理(GitHub およびその他の Git サーバー) - Web サービスモード — ブラウザから Codeg にアクセスでき、リモートワークに対応 +- **スタンドアロンサーバーデプロイ** — 任意の Linux/macOS サーバーで `codeg-server` を実行し、ブラウザからアクセス +- **Docker サポート** — `docker compose up` でゼロ設定のサーバーセットアップが可能 - 統合エンジニアリングループ(ファイルツリー、Diff、Git 変更、コミット、ターミナル) ## プロジェクトブート @@ -104,7 +107,7 @@ Git/ファイル/ターミナル連携ワークフローを提供します。 - Node.js `>=22`(推奨) - pnpm `>=10` - Rust stable(2021 edition) -- Tauri 2 ビルド依存パッケージ +- Tauri 2 ビルド依存パッケージ(デスクトップモードのみ) Linux(Debian/Ubuntu)の例: @@ -122,18 +125,24 @@ sudo apt-get install -y \ ```bash pnpm install +# フロントエンド静的エクスポート(out/ へ) +pnpm build + # デスクトップアプリ全体(Tauri + Next.js) pnpm tauri dev # フロントエンドのみ pnpm dev -# フロントエンド静的エクスポート(out/ へ) -pnpm build - # デスクトップビルド pnpm tauri build +# スタンドアロンサーバー(Tauri/GUI 不要) +pnpm server:dev + +# サーバーリリースバイナリのビルド +pnpm server:build + # Lint pnpm eslint . @@ -143,22 +152,71 @@ cargo clippy cargo build ``` +### サーバーデプロイ + +Codeg はデスクトップ環境なしでスタンドアロン Web サーバーとして実行できます。 + +#### オプション 1: バイナリ直接実行 + +```bash +# サーバーバイナリのビルド +cd src-tauri +cargo build --release --bin codeg-server --no-default-features + +# 実行 +CODEG_PORT=3080 CODEG_STATIC_DIR=../out ./target/release/codeg-server +``` + +環境変数: + +| 変数 | デフォルト | 説明 | +| --- | --- | --- | +| `CODEG_PORT` | `3080` | HTTP ポート | +| `CODEG_HOST` | `0.0.0.0` | バインドアドレス | +| `CODEG_TOKEN` | *(ランダム)* | 認証トークン(起動時に stderr に出力) | +| `CODEG_DATA_DIR` | `~/.local/share/codeg` | SQLite データベースディレクトリ | +| `CODEG_STATIC_DIR` | `./web` or `./out` | Next.js 静的エクスポートディレクトリ | + +#### オプション 2: Docker + +```bash +# ビルドして実行 +docker compose up -d + +# または手動でビルド +docker build -t codeg . +docker run -p 3080:3080 -v codeg-data:/data codeg +``` + ## アーキテクチャ ```text Next.js 16 (Static Export) + React 19 | - | invoke() + | invoke() (desktop) / fetch() + WebSocket (web) v -Tauri 2 Commands (Rust) - |- ACP Manager - |- Parsers (local session ingestion) - |- Git / File Tree / Terminal runtime - |- MCP marketplace + local config writer - |- SeaORM + SQLite + ┌─────────────────────────┐ + │ Transport Abstraction │ + │ (Tauri IPC or HTTP/WS) │ + └─────────────────────────┘ | v -Local Filesystem / Local Agent Data / Git Repos +┌─── Tauri Desktop ───┐ ┌─── codeg-server ───┐ +│ Tauri 2 Commands │ │ Axum HTTP + WS │ +│ (window management) │ │ (standalone mode) │ +└──────────┬───────────┘ └──────────┬──────────┘ + └──────────┬───────────────┘ + v + Shared Rust Core + |- AppState + |- ACP Manager + |- Parsers (session ingestion) + |- Git / File Tree / Terminal + |- MCP marketplace + config + |- SeaORM + SQLite + | + v + Local Filesystem / Git Repos ``` ## 制約事項 @@ -173,6 +231,7 @@ Local Filesystem / Local Agent Data / Git Repos - 解析、ストレージ、プロジェクト操作はデフォルトでローカルファースト - ネットワークアクセスはユーザーが明示的に操作した場合のみ発生 - エンタープライズ環境向けのシステムプロキシサポート +- Web サービスモードではトークンベースの認証を使用 ## ライセンス diff --git a/docs/readme/README.ko.md b/docs/readme/README.ko.md index 331548a..67418b5 100644 --- a/docs/readme/README.ko.md +++ b/docs/readme/README.ko.md @@ -4,6 +4,7 @@ [![License](https://img.shields.io/github/license/xintaofei/codeg)](../../LICENSE) [![Tauri](https://img.shields.io/badge/Tauri-2.x-24C8DB)](https://tauri.app/) [![Next.js](https://img.shields.io/badge/Next.js-16-black)](https://nextjs.org/) +[![Docker](https://img.shields.io/badge/Docker-ready-2496ED)](../../Dockerfile)

English | @@ -46,6 +47,8 @@ Git/파일/터미널 통합 워크플로를 제공합니다. - Skills 관리 (글로벌 및 프로젝트 범위) - Git 원격 계정 관리 (GitHub 및 기타 Git 서버) - Web 서비스 모드 — 브라우저에서 Codeg에 접속하여 원격 작업 가능 +- **독립형 서버 배포** — 모든 Linux/macOS 서버에서 `codeg-server`를 실행하고 브라우저로 접속 +- **Docker 지원** — `docker compose up`으로 설정 없이 서버 구축 가능 - 통합 엔지니어링 루프 (파일 트리, Diff, Git 변경사항, 커밋, 터미널) ## 프로젝트 부트 @@ -104,7 +107,7 @@ Git/파일/터미널 통합 워크플로를 제공합니다. - Node.js `>=22` (권장) - pnpm `>=10` - Rust stable (2021 edition) -- Tauri 2 빌드 의존성 +- Tauri 2 빌드 의존성 (데스크톱 모드만 해당) Linux (Debian/Ubuntu) 예시: @@ -122,18 +125,24 @@ sudo apt-get install -y \ ```bash pnpm install +# 프론트엔드 정적 내보내기 (out/) +pnpm build + # 전체 데스크톱 앱 (Tauri + Next.js) pnpm tauri dev # 프론트엔드만 pnpm dev -# 프론트엔드 정적 내보내기 (out/) -pnpm build - # 데스크톱 빌드 pnpm tauri build +# 독립형 서버 (Tauri/GUI 불필요) +pnpm server:dev + +# 서버 릴리스 바이너리 빌드 +pnpm server:build + # Lint pnpm eslint . @@ -143,22 +152,71 @@ cargo clippy cargo build ``` +### 서버 배포 + +Codeg는 데스크톱 환경 없이 독립형 웹 서버로 실행할 수 있습니다. + +#### 옵션 1: 바이너리 직접 실행 + +```bash +# 서버 바이너리 빌드 +cd src-tauri +cargo build --release --bin codeg-server --no-default-features + +# 실행 +CODEG_PORT=3080 CODEG_STATIC_DIR=../out ./target/release/codeg-server +``` + +환경 변수: + +| 변수 | 기본값 | 설명 | +| --- | --- | --- | +| `CODEG_PORT` | `3080` | HTTP 포트 | +| `CODEG_HOST` | `0.0.0.0` | 바인드 주소 | +| `CODEG_TOKEN` | *(랜덤)* | 인증 토큰 (시작 시 stderr에 출력) | +| `CODEG_DATA_DIR` | `~/.local/share/codeg` | SQLite 데이터베이스 디렉토리 | +| `CODEG_STATIC_DIR` | `./web` or `./out` | Next.js 정적 내보내기 디렉토리 | + +#### 옵션 2: Docker + +```bash +# 빌드 및 실행 +docker compose up -d + +# 또는 수동으로 빌드 +docker build -t codeg . +docker run -p 3080:3080 -v codeg-data:/data codeg +``` + ## 아키텍처 ```text Next.js 16 (Static Export) + React 19 | - | invoke() + | invoke() (desktop) / fetch() + WebSocket (web) v -Tauri 2 Commands (Rust) - |- ACP Manager - |- Parsers (local session ingestion) - |- Git / File Tree / Terminal runtime - |- MCP marketplace + local config writer - |- SeaORM + SQLite + ┌─────────────────────────┐ + │ Transport Abstraction │ + │ (Tauri IPC or HTTP/WS) │ + └─────────────────────────┘ | v -Local Filesystem / Local Agent Data / Git Repos +┌─── Tauri Desktop ───┐ ┌─── codeg-server ───┐ +│ Tauri 2 Commands │ │ Axum HTTP + WS │ +│ (window management) │ │ (standalone mode) │ +└──────────┬───────────┘ └──────────┬──────────┘ + └──────────┬───────────────┘ + v + Shared Rust Core + |- AppState + |- ACP Manager + |- Parsers (session ingestion) + |- Git / File Tree / Terminal + |- MCP marketplace + config + |- SeaORM + SQLite + | + v + Local Filesystem / Git Repos ``` ## 제약 사항 @@ -173,6 +231,7 @@ Local Filesystem / Local Agent Data / Git Repos - 파싱, 저장, 프로젝트 작업은 기본적으로 로컬 우선 - 네트워크 접근은 사용자가 명시적으로 작업을 실행할 때만 발생 - 엔터프라이즈 환경을 위한 시스템 프록시 지원 +- 웹 서비스 모드에서는 토큰 기반 인증 사용 ## 라이선스 diff --git a/docs/readme/README.pt.md b/docs/readme/README.pt.md index 6f3c96d..a887918 100644 --- a/docs/readme/README.pt.md +++ b/docs/readme/README.pt.md @@ -4,6 +4,7 @@ [![License](https://img.shields.io/github/license/xintaofei/codeg)](../../LICENSE) [![Tauri](https://img.shields.io/badge/Tauri-2.x-24C8DB)](https://tauri.app/) [![Next.js](https://img.shields.io/badge/Next.js-16-black)](https://nextjs.org/) +[![Docker](https://img.shields.io/badge/Docker-ready-2496ED)](../../Dockerfile)

English | @@ -46,6 +47,8 @@ paralelo via `git worktree`, gerenciamento de MCP/Skills e fluxos integrados de - Gerenciamento de Skills (escopo global e por projeto) - Gerenciamento de contas remotas Git (GitHub e outros servidores Git) - Modo de serviço web — acesse o Codeg de qualquer navegador para trabalho remoto +- **Implantação de servidor standalone** — execute `codeg-server` em qualquer servidor Linux/macOS, acesse via navegador +- **Suporte a Docker** — implante com `docker compose up` para configuração zero do servidor - Ciclo de engenharia integrado (árvore de arquivos, diff, alterações git, commit, terminal) ## Inicializador de Projeto @@ -104,7 +107,7 @@ Alvos de escrita atuais: - Node.js `>=22` (recomendado) - pnpm `>=10` - Rust stable (2021 edition) -- Dependências de build do Tauri 2 +- Dependências de build do Tauri 2 (somente modo desktop) Exemplo Linux (Debian/Ubuntu): @@ -122,18 +125,24 @@ sudo apt-get install -y \ ```bash pnpm install +# Exportação estática do frontend para out/ +pnpm build + # Aplicativo desktop completo (Tauri + Next.js) pnpm tauri dev # Apenas frontend pnpm dev -# Exportação estática do frontend para out/ -pnpm build - # Build do aplicativo desktop pnpm tauri build +# Servidor standalone (sem Tauri/GUI necessário) +pnpm server:dev + +# Build do binário do servidor +pnpm server:build + # Lint pnpm eslint . @@ -143,22 +152,71 @@ cargo clippy cargo build ``` +### Implantação do servidor + +O Codeg pode ser executado como um servidor web standalone sem ambiente desktop. + +#### Opção 1: Binário direto + +```bash +# Build do binário do servidor +cd src-tauri +cargo build --release --bin codeg-server --no-default-features + +# Executar +CODEG_PORT=3080 CODEG_STATIC_DIR=../out ./target/release/codeg-server +``` + +Variáveis de ambiente: + +| Variável | Padrão | Descrição | +| --- | --- | --- | +| `CODEG_PORT` | `3080` | Porta HTTP | +| `CODEG_HOST` | `0.0.0.0` | Endereço de bind | +| `CODEG_TOKEN` | *(aleatório)* | Token de autenticação (impresso no stderr ao iniciar) | +| `CODEG_DATA_DIR` | `~/.local/share/codeg` | Diretório do banco de dados SQLite | +| `CODEG_STATIC_DIR` | `./web` ou `./out` | Diretório de exportação estática do Next.js | + +#### Opção 2: Docker + +```bash +# Build e execução +docker compose up -d + +# Ou build manual +docker build -t codeg . +docker run -p 3080:3080 -v codeg-data:/data codeg +``` + ## Arquitetura ```text Next.js 16 (Static Export) + React 19 | - | invoke() + | invoke() (desktop) / fetch() + WebSocket (web) v -Tauri 2 Commands (Rust) - |- ACP Manager - |- Parsers (local session ingestion) - |- Git / File Tree / Terminal runtime - |- MCP marketplace + local config writer - |- SeaORM + SQLite + ┌─────────────────────────┐ + │ Transport Abstraction │ + │ (Tauri IPC or HTTP/WS) │ + └─────────────────────────┘ | v -Local Filesystem / Local Agent Data / Git Repos +┌─── Tauri Desktop ───┐ ┌─── codeg-server ───┐ +│ Tauri 2 Commands │ │ Axum HTTP + WS │ +│ (window management) │ │ (standalone mode) │ +└──────────┬───────────┘ └──────────┬──────────┘ + └──────────┬───────────────┘ + v + Shared Rust Core + |- AppState + |- ACP Manager + |- Parsers (session ingestion) + |- Git / File Tree / Terminal + |- MCP marketplace + config + |- SeaORM + SQLite + | + v + Local Filesystem / Git Repos ``` ## Restrições @@ -173,6 +231,7 @@ Local Filesystem / Local Agent Data / Git Repos - Local-first por padrão para análise, armazenamento e operações do projeto - O acesso à rede ocorre apenas em ações iniciadas pelo usuário - Suporte a proxy do sistema para ambientes corporativos +- O modo de serviço web usa autenticação baseada em token ## Licença diff --git a/docs/readme/README.zh-CN.md b/docs/readme/README.zh-CN.md index 27617ef..f5d5779 100644 --- a/docs/readme/README.zh-CN.md +++ b/docs/readme/README.zh-CN.md @@ -4,6 +4,7 @@ [![License](https://img.shields.io/github/license/xintaofei/codeg)](../../LICENSE) [![Tauri](https://img.shields.io/badge/Tauri-2.x-24C8DB)](https://tauri.app/) [![Next.js](https://img.shields.io/badge/Next.js-16-black)](https://nextjs.org/) +[![Docker](https://img.shields.io/badge/Docker-ready-2496ED)](../../Dockerfile)

English | @@ -46,6 +47,8 @@ OpenClaw、Cline 等)统一到桌面应用与 Web 服务中——通过浏览 - Skills 管理(全局与项目级) - Git 远程账号管理(支持 GitHub 及其它 Git 服务器) - Web 服务模式 — 开启后可在浏览器中访问 Codeg,支持远程工作 +- **独立服务器部署** — 在任意 Linux/macOS 服务器上运行 `codeg-server`,通过浏览器访问 +- **Docker 支持** — 使用 `docker compose up` 部署,零配置启动服务 - 集成工程闭环(文件树、Diff、Git 变更、提交、终端) ## 项目启动器 @@ -104,7 +107,7 @@ OpenClaw、Cline 等)统一到桌面应用与 Web 服务中——通过浏览 - Node.js `>=22`(推荐) - pnpm `>=10` - Rust stable(2021 edition) -- Tauri 2 构建依赖 +- Tauri 2 构建依赖(仅桌面模式) Linux(Debian/Ubuntu)示例: @@ -122,18 +125,24 @@ sudo apt-get install -y \ ```bash pnpm install +# 前端静态导出到 out/ +pnpm build + # 完整桌面应用(Tauri + Next.js) pnpm tauri dev # 仅前端 pnpm dev -# 前端静态导出到 out/ -pnpm build - # 桌面应用构建 pnpm tauri build +# 独立服务器(无需 Tauri/GUI) +pnpm server:dev + +# 构建服务器发布二进制 +pnpm server:build + # Lint pnpm eslint . @@ -143,22 +152,71 @@ cargo clippy cargo build ``` +### 服务器部署 + +Codeg 可以作为独立 Web 服务器运行,无需桌面环境。 + +#### 方式一:直接运行二进制 + +```bash +# 构建服务器二进制 +cd src-tauri +cargo build --release --bin codeg-server --no-default-features + +# 运行 +CODEG_PORT=3080 CODEG_STATIC_DIR=../out ./target/release/codeg-server +``` + +环境变量: + +| 变量 | 默认值 | 说明 | +| --- | --- | --- | +| `CODEG_PORT` | `3080` | HTTP 端口 | +| `CODEG_HOST` | `0.0.0.0` | 绑定地址 | +| `CODEG_TOKEN` | *(随机)* | 认证令牌(启动时输出到 stderr) | +| `CODEG_DATA_DIR` | `~/.local/share/codeg` | SQLite 数据库目录 | +| `CODEG_STATIC_DIR` | `./web` 或 `./out` | Next.js 静态导出目录 | + +#### 方式二:Docker + +```bash +# 构建并运行 +docker compose up -d + +# 或手动构建 +docker build -t codeg . +docker run -p 3080:3080 -v codeg-data:/data codeg +``` + ## 架构 ```text Next.js 16 (Static Export) + React 19 | - | invoke() + | invoke() (desktop) / fetch() + WebSocket (web) v -Tauri 2 Commands (Rust) - |- ACP Manager - |- Parsers (local session ingestion) - |- Git / File Tree / Terminal runtime - |- MCP marketplace + local config writer - |- SeaORM + SQLite + ┌─────────────────────────┐ + │ Transport Abstraction │ + │ (Tauri IPC or HTTP/WS) │ + └─────────────────────────┘ | v -Local Filesystem / Local Agent Data / Git Repos +┌─── Tauri Desktop ───┐ ┌─── codeg-server ───┐ +│ Tauri 2 Commands │ │ Axum HTTP + WS │ +│ (window management) │ │ (standalone mode) │ +└──────────┬───────────┘ └──────────┬──────────┘ + └──────────┬───────────────┘ + v + Shared Rust Core + |- AppState + |- ACP Manager + |- Parsers (session ingestion) + |- Git / File Tree / Terminal + |- MCP marketplace + config + |- SeaORM + SQLite + | + v + Local Filesystem / Git Repos ``` ## 开发约束 @@ -173,6 +231,7 @@ Local Filesystem / Local Agent Data / Git Repos - 默认本地优先:解析、存储、项目操作均在本地完成 - 仅在用户主动触发时才访问网络 - 支持系统代理,适配企业网络环境 +- Web 服务模式使用基于令牌的身份认证 ## 许可证 diff --git a/docs/readme/README.zh-TW.md b/docs/readme/README.zh-TW.md index dce6263..565bb9f 100644 --- a/docs/readme/README.zh-TW.md +++ b/docs/readme/README.zh-TW.md @@ -4,6 +4,7 @@ [![License](https://img.shields.io/github/license/xintaofei/codeg)](../../LICENSE) [![Tauri](https://img.shields.io/badge/Tauri-2.x-24C8DB)](https://tauri.app/) [![Next.js](https://img.shields.io/badge/Next.js-16-black)](https://nextjs.org/) +[![Docker](https://img.shields.io/badge/Docker-ready-2496ED)](../../Dockerfile)

English | @@ -46,6 +47,8 @@ OpenClaw、Cline 等)整合到桌面應用與 Web 服務中——透過瀏覽 - Skills 管理(全域與專案級) - Git 遠端帳號管理(支援 GitHub 及其他 Git 伺服器) - Web 服務模式 — 開啟後可在瀏覽器中存取 Codeg,支援遠端工作 +- **獨立伺服器部署** — 在任意 Linux/macOS 伺服器上執行 `codeg-server`,透過瀏覽器存取 +- **Docker 支援** — 使用 `docker compose up` 部署,零設定啟動服務 - 整合工程閉環(檔案樹、Diff、Git 變更、提交、終端) ## 專案啟動器 @@ -104,7 +107,7 @@ OpenClaw、Cline 等)整合到桌面應用與 Web 服務中——透過瀏覽 - Node.js `>=22`(建議) - pnpm `>=10` - Rust stable(2021 edition) -- Tauri 2 建置依賴 +- Tauri 2 建置依賴(僅桌面模式) Linux(Debian/Ubuntu)範例: @@ -122,18 +125,24 @@ sudo apt-get install -y \ ```bash pnpm install +# 前端靜態匯出到 out/ +pnpm build + # 完整桌面應用(Tauri + Next.js) pnpm tauri dev # 僅前端 pnpm dev -# 前端靜態匯出到 out/ -pnpm build - # 桌面應用建置 pnpm tauri build +# 獨立伺服器(無需 Tauri/GUI) +pnpm server:dev + +# 建置伺服器發行版二進位檔 +pnpm server:build + # Lint pnpm eslint . @@ -143,22 +152,71 @@ cargo clippy cargo build ``` +### 伺服器部署 + +Codeg 可以作為獨立 Web 伺服器執行,無需桌面環境。 + +#### 方式一:直接執行二進位檔 + +```bash +# 建置伺服器二進位檔 +cd src-tauri +cargo build --release --bin codeg-server --no-default-features + +# 執行 +CODEG_PORT=3080 CODEG_STATIC_DIR=../out ./target/release/codeg-server +``` + +環境變數: + +| 變數 | 預設值 | 說明 | +| --- | --- | --- | +| `CODEG_PORT` | `3080` | HTTP 連接埠 | +| `CODEG_HOST` | `0.0.0.0` | 綁定位址 | +| `CODEG_TOKEN` | *(隨機)* | 認證令牌(啟動時輸出到 stderr) | +| `CODEG_DATA_DIR` | `~/.local/share/codeg` | SQLite 資料庫目錄 | +| `CODEG_STATIC_DIR` | `./web` 或 `./out` | Next.js 靜態匯出目錄 | + +#### 方式二:Docker + +```bash +# 建置並執行 +docker compose up -d + +# 或手動建置 +docker build -t codeg . +docker run -p 3080:3080 -v codeg-data:/data codeg +``` + ## 架構 ```text Next.js 16 (Static Export) + React 19 | - | invoke() + | invoke() (desktop) / fetch() + WebSocket (web) v -Tauri 2 Commands (Rust) - |- ACP Manager - |- Parsers (local session ingestion) - |- Git / File Tree / Terminal runtime - |- MCP marketplace + local config writer - |- SeaORM + SQLite + ┌─────────────────────────┐ + │ Transport Abstraction │ + │ (Tauri IPC or HTTP/WS) │ + └─────────────────────────┘ | v -Local Filesystem / Local Agent Data / Git Repos +┌─── Tauri Desktop ───┐ ┌─── codeg-server ───┐ +│ Tauri 2 Commands │ │ Axum HTTP + WS │ +│ (window management) │ │ (standalone mode) │ +└──────────┬───────────┘ └──────────┬──────────┘ + └──────────┬───────────────┘ + v + Shared Rust Core + |- AppState + |- ACP Manager + |- Parsers (session ingestion) + |- Git / File Tree / Terminal + |- MCP marketplace + config + |- SeaORM + SQLite + | + v + Local Filesystem / Git Repos ``` ## 開發約束 @@ -173,6 +231,7 @@ Local Filesystem / Local Agent Data / Git Repos - 預設本地優先:解析、儲存、專案操作均在本地完成 - 僅在使用者主動觸發時才存取網路 - 支援系統代理,適配企業網路環境 +- Web 服務模式使用基於令牌的身份認證 ## 授權 diff --git a/package.json b/package.json index 065b4bb..52b7297 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ "build": "next build", "lint": "eslint", "tauri": "tauri", + "server:build": "cd src-tauri && cargo build --release --bin codeg-server --no-default-features", + "server:dev": "cd src-tauri && cargo run --bin codeg-server --no-default-features", "postinstall": "node -e \"const fs=require('fs');fs.cpSync('node_modules/monaco-editor/min/vs','public/vs',{recursive:true,force:true});const p='public/vs/loader.js';fs.writeFileSync(p,fs.readFileSync(p,'utf8').replace(/\\n\\/\\/# sourceMappingURL=.*/,''))\"" }, "dependencies": { diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index e24fae5..90f5103 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -4,6 +4,7 @@ version = "0.4.7" description = "Agent Code Generation App" authors = ["feitao"] edition = "2021" +default-run = "codeg" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -14,13 +15,36 @@ edition = "2021" name = "codeg_lib" crate-type = ["staticlib", "cdylib", "rlib"] +[features] +default = ["tauri-runtime"] +tauri-runtime = [ + "dep:tauri", + "dep:tauri-plugin-opener", + "dep:tauri-plugin-dialog", + "dep:fix-path-env", + "dep:tauri-build", + "dep:tauri-plugin-window-state", + "dep:tauri-plugin-updater", + "dep:tauri-plugin-process", + "dep:tauri-plugin-notification", +] + +[[bin]] +name = "codeg" +path = "src/main.rs" + +[[bin]] +name = "codeg-server" +path = "src/bin/codeg_server.rs" +required-features = [] + [build-dependencies] -tauri-build = { version = "2", features = [] } +tauri-build = { version = "2", features = [], optional = true } [dependencies] -tauri = { version = "2", features = [] } -tauri-plugin-opener = "2" -tauri-plugin-dialog = "2" +tauri = { version = "2", features = [], optional = true } +tauri-plugin-opener = { version = "2", optional = true } +tauri-plugin-dialog = { version = "2", optional = true } serde = { version = "1", features = ["derive"] } serde_json = "1" chrono = { version = "0.4", features = ["serde"] } @@ -29,7 +53,7 @@ dirs = "6" walkdir = "2" sacp = "11.0.0-alpha.1" sacp-tokio = "11.0.0-alpha.1" -tokio = { version = "1", features = ["process", "io-util", "sync", "macros", "rt", "net"] } +tokio = { version = "1", features = ["process", "io-util", "sync", "macros", "rt", "net", "rt-multi-thread"] } uuid = { version = "1", features = ["v4"] } futures = "0.3" reqwest = { version = "0.12", features = ["stream", "json"] } @@ -40,7 +64,7 @@ zip = "2" regex = "1" portable-pty = "0.8" urlencoding = "2" -fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs", version = "0.0.0" } +fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs", version = "0.0.0", optional = true } sea-orm = { version = "1.1", features = ["sqlx-sqlite", "runtime-tokio-rustls", "macros"] } sea-orm-migration = { version = "1.1", features = ["sqlx-sqlite", "runtime-tokio-rustls"] } toml = "0.8" @@ -54,11 +78,10 @@ axum = { version = "0.8", features = ["ws"] } tower-http = { version = "0.6", features = ["fs", "cors"] } [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] -tauri-plugin-window-state = "2" -tauri-plugin-updater = "2" -tauri-plugin-process = "2" -tauri-plugin-notification = "2" - +tauri-plugin-window-state = { version = "2", optional = true } +tauri-plugin-updater = { version = "2", optional = true } +tauri-plugin-process = { version = "2", optional = true } +tauri-plugin-notification = { version = "2", optional = true } [target.'cfg(target_os = "macos")'.dependencies] mac-notification-sys = "0.6" diff --git a/src-tauri/build.rs b/src-tauri/build.rs index d860e1e..651de27 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -1,3 +1,4 @@ fn main() { - tauri_build::build() + #[cfg(feature = "tauri-runtime")] + tauri_build::build(); } diff --git a/src-tauri/src/acp/connection.rs b/src-tauri/src/acp/connection.rs index b6511a2..a8bc6e0 100644 --- a/src-tauri/src/acp/connection.rs +++ b/src-tauri/src/acp/connection.rs @@ -38,6 +38,7 @@ use crate::acp::types::{ }; use crate::models::agent::AgentType; use crate::network::proxy; +use crate::web::event_bridge::EventEmitter; const DEFAULT_COMMAND_COLOR_ENV: [(&str, &str); 1] = [("CLICOLOR_FORCE", "1")]; @@ -115,7 +116,7 @@ async fn build_agent( agent_type: AgentType, runtime_env: &BTreeMap, connection_id: &str, - app_handle: &tauri::AppHandle, + emitter: &EventEmitter, ) -> Result { let meta = registry::get_agent_meta(agent_type); debug_assert_eq!(meta.agent_type, agent_type); @@ -195,7 +196,7 @@ async fn build_agent( .is_some(); if !has_cached_binary { crate::web::event_bridge::emit_event( - app_handle, + emitter, "acp://event", AcpEvent::StatusChanged { connection_id: connection_id.into(), @@ -235,10 +236,10 @@ pub async fn spawn_agent_connection( session_id: Option, runtime_env: BTreeMap, owner_window_label: String, - app_handle: tauri::AppHandle, + emitter: EventEmitter, ) -> Result { crate::web::event_bridge::emit_event( - &app_handle, + &emitter, "acp://event", AcpEvent::StatusChanged { connection_id: connection_id.clone(), @@ -246,11 +247,11 @@ pub async fn spawn_agent_connection( }, ); - let agent = build_agent(agent_type, &runtime_env, &connection_id, &app_handle).await?; + let agent = build_agent(agent_type, &runtime_env, &connection_id, &emitter).await?; let (cmd_tx, cmd_rx) = mpsc::channel::(32); let conn_id = connection_id.clone(); - let handle = app_handle.clone(); + let emitter_clone = emitter.clone(); tokio::spawn(async move { let result = run_connection( @@ -259,13 +260,13 @@ pub async fn spawn_agent_connection( working_dir, session_id, cmd_rx, - handle.clone(), + emitter_clone.clone(), ) .await; if let Err(e) = result { crate::web::event_bridge::emit_event( - &handle, + &emitter_clone, "acp://event", AcpEvent::Error { connection_id: conn_id.clone(), @@ -275,7 +276,7 @@ pub async fn spawn_agent_connection( } crate::web::event_bridge::emit_event( - &handle, + &emitter_clone, "acp://event", AcpEvent::StatusChanged { connection_id: conn_id, @@ -314,12 +315,12 @@ fn map_session_modes(mode_state: &SessionModeState) -> SessionModeStateInfo { fn emit_session_modes( connection_id: &str, - app_handle: &tauri::AppHandle, + emitter: &EventEmitter, modes: &Option, ) { if let Some(mode_state) = modes { crate::web::event_bridge::emit_event( - app_handle, + emitter, "acp://event", AcpEvent::SessionModes { connection_id: connection_id.into(), @@ -416,12 +417,12 @@ fn map_session_config_options( fn emit_session_config_options_values( connection_id: &str, - app_handle: &tauri::AppHandle, + emitter: &EventEmitter, config_options: Vec, ) { let mapped = map_session_config_options(&config_options); crate::web::event_bridge::emit_event( - app_handle, + emitter, "acp://event", AcpEvent::SessionConfigOptions { connection_id: connection_id.into(), @@ -432,7 +433,7 @@ fn emit_session_config_options_values( fn emit_session_config_options( connection_id: &str, - app_handle: &tauri::AppHandle, + emitter: &EventEmitter, config_options: &Option>, ) { // Always emit one config-options snapshot after session attach. @@ -440,12 +441,12 @@ fn emit_session_config_options( // and return `None`; emitting an empty list lets the frontend settle // loading state instead of waiting forever. let options = config_options.clone().unwrap_or_default(); - emit_session_config_options_values(connection_id, app_handle, options); + emit_session_config_options_values(connection_id, emitter, options); } -fn emit_selectors_ready(connection_id: &str, app_handle: &tauri::AppHandle) { +fn emit_selectors_ready(connection_id: &str, emitter: &EventEmitter) { crate::web::event_bridge::emit_event( - app_handle, + emitter, "acp://event", AcpEvent::SelectorsReady { connection_id: connection_id.into(), @@ -455,11 +456,11 @@ fn emit_selectors_ready(connection_id: &str, app_handle: &tauri::AppHandle) { fn emit_prompt_capabilities( connection_id: &str, - app_handle: &tauri::AppHandle, + emitter: &EventEmitter, capabilities: &sacp::schema::PromptCapabilities, ) { crate::web::event_bridge::emit_event( - app_handle, + emitter, "acp://event", AcpEvent::PromptCapabilities { connection_id: connection_id.into(), @@ -494,7 +495,7 @@ async fn run_connection( working_dir: Option, session_id: Option, mut cmd_rx: mpsc::Receiver, - app_handle: tauri::AppHandle, + emitter: EventEmitter, ) -> Result<(), AcpError> { let pending_perms: PendingPermissions = Arc::new(tokio::sync::Mutex::new(HashMap::new())); let terminal_runtime = Arc::new(TerminalRuntime::new()); @@ -503,7 +504,7 @@ async fn run_connection( let file_system_runtime = Arc::new(FileSystemRuntime::new(cwd.clone())); let conn_id = connection_id.clone(); - let handle = app_handle.clone(); + let emitter_clone = emitter.clone(); let perms = pending_perms.clone(); Client @@ -512,13 +513,13 @@ async fn run_connection( .on_receive_request( { let conn_id = conn_id.clone(); - let handle = handle.clone(); + let emitter_inner = emitter_clone.clone(); let perms = perms.clone(); let perm_cwd = cwd_string.clone(); async move |req: RequestPermissionRequest, responder: Responder, _cx: ConnectionTo| { - handle_permission_request(&conn_id, &handle, &perms, &perm_cwd, req, responder).await; + handle_permission_request(&conn_id, &emitter_inner, &perms, &perm_cwd, req, responder).await; Ok(()) } }, @@ -620,7 +621,7 @@ async fn run_connection( let init_resp = cx.send_request_to(Agent, init_request).block_task().await?; emit_prompt_capabilities( &conn_id, - &handle, + &emitter_clone, &init_resp.agent_capabilities.prompt_capabilities, ); @@ -636,7 +637,7 @@ async fn run_connection( // Emit fork support capability crate::web::event_bridge::emit_event( - &handle, + &emitter_clone, "acp://event", AcpEvent::ForkSupported { connection_id: conn_id.clone(), @@ -649,7 +650,7 @@ async fn run_connection( // Prompts sent before run_conversation_loop are buffered in // the cmd_rx channel and processed as soon as the loop starts. crate::web::event_bridge::emit_event( - &handle, + &emitter_clone, "acp://event", AcpEvent::StatusChanged { connection_id: conn_id.clone(), @@ -683,7 +684,7 @@ async fn run_connection( drained += 1; if let SessionMessage::SessionMessage(dispatch) = msg { let cid = conn_id.clone(); - let h = handle.clone(); + let h = emitter_clone.clone(); let _ = MatchDispatch::new(dispatch) .if_notification(async |notif: SessionNotification| { if matches!( @@ -703,21 +704,21 @@ async fn run_connection( } crate::web::event_bridge::emit_event( - &handle, + &emitter_clone, "acp://event", AcpEvent::SessionStarted { connection_id: conn_id.clone(), session_id: sid.clone(), }, ); - emit_session_modes(&conn_id, &handle, session.modes()); - emit_session_config_options(&conn_id, &handle, &initial_config_options); - emit_selectors_ready(&conn_id, &handle); + emit_session_modes(&conn_id, &emitter_clone, session.modes()); + emit_session_config_options(&conn_id, &emitter_clone, &initial_config_options); + emit_selectors_ready(&conn_id, &emitter_clone); let loop_result = run_conversation_loop( &mut session, &conn_id, - &handle, + &emitter_clone, &perms, &mut cmd_rx, terminal_runtime.clone(), @@ -730,7 +731,7 @@ async fn run_connection( handle_fork_or_exit( loop_result, &conn_id, - &handle, + &emitter_clone, &perms, &mut cmd_rx, terminal_runtime.clone(), @@ -753,7 +754,7 @@ async fn run_connection( // support session resume (e.g. Cline). if !err_str.contains("Method not found") { crate::web::event_bridge::emit_event( - &handle, + &emitter_clone, "acp://event", AcpEvent::Error { connection_id: conn_id.clone(), @@ -772,25 +773,25 @@ async fn run_connection( let mut session = cx.attach_session(new_resp, Default::default())?; crate::web::event_bridge::emit_event( - &handle, + &emitter_clone, "acp://event", AcpEvent::SessionStarted { connection_id: conn_id.clone(), session_id: fallback_sid.clone(), }, ); - emit_session_modes(&conn_id, &handle, session.modes()); + emit_session_modes(&conn_id, &emitter_clone, session.modes()); emit_session_config_options( &conn_id, - &handle, + &emitter_clone, &initial_config_options, ); - emit_selectors_ready(&conn_id, &handle); + emit_selectors_ready(&conn_id, &emitter_clone); let loop_result = run_conversation_loop( &mut session, &conn_id, - &handle, + &emitter_clone, &perms, &mut cmd_rx, terminal_runtime.clone(), @@ -805,7 +806,7 @@ async fn run_connection( handle_fork_or_exit( loop_result, &conn_id, - &handle, + &emitter_clone, &perms, &mut cmd_rx, terminal_runtime.clone(), @@ -825,21 +826,21 @@ async fn run_connection( let initial_config_options = new_resp.config_options.clone(); let mut session = cx.attach_session(new_resp, Default::default())?; crate::web::event_bridge::emit_event( - &handle, + &emitter_clone, "acp://event", AcpEvent::SessionStarted { connection_id: conn_id.clone(), session_id: sid.clone(), }, ); - emit_session_modes(&conn_id, &handle, session.modes()); - emit_session_config_options(&conn_id, &handle, &initial_config_options); - emit_selectors_ready(&conn_id, &handle); + emit_session_modes(&conn_id, &emitter_clone, session.modes()); + emit_session_config_options(&conn_id, &emitter_clone, &initial_config_options); + emit_selectors_ready(&conn_id, &emitter_clone); let loop_result = run_conversation_loop( &mut session, &conn_id, - &handle, + &emitter_clone, &perms, &mut cmd_rx, terminal_runtime.clone(), @@ -852,7 +853,7 @@ async fn run_connection( handle_fork_or_exit( loop_result, &conn_id, - &handle, + &emitter_clone, &perms, &mut cmd_rx, terminal_runtime.clone(), @@ -869,7 +870,7 @@ async fn run_connection( /// Store the permission responder and emit event to frontend. async fn handle_permission_request( conn_id: &str, - handle: &tauri::AppHandle, + emitter: &EventEmitter, perms: &PendingPermissions, cwd: &str, req: RequestPermissionRequest, @@ -929,7 +930,7 @@ async fn handle_permission_request( perms.lock().await.insert(request_id.clone(), responder); crate::web::event_bridge::emit_event( - handle, + emitter, "acp://event", AcpEvent::PermissionRequest { connection_id: conn_id.into(), @@ -963,7 +964,7 @@ fn respond_file_system_request( async fn set_session_mode( session: &mut sacp::ActiveSession<'_, Agent>, conn_id: &str, - handle: &tauri::AppHandle, + emitter: &EventEmitter, mode_id: String, ) -> Result<(), sacp::Error> { let req = SetSessionModeRequest::new(session.session_id().clone(), mode_id.clone()); @@ -974,7 +975,7 @@ async fn set_session_mode( .await?; crate::web::event_bridge::emit_event( - handle, + emitter, "acp://event", AcpEvent::ModeChanged { connection_id: conn_id.into(), @@ -989,7 +990,7 @@ async fn set_session_config_option( cx: &ConnectionTo, session_id: &SessionId, conn_id: &str, - handle: &tauri::AppHandle, + emitter: &EventEmitter, config_id: String, value_id: String, ) -> Result<(), sacp::Error> { @@ -1004,7 +1005,7 @@ async fn set_session_config_option( sacp::util::internal_error(format!("Failed to parse config option response: {e}")) })?; - emit_session_config_options_values(conn_id, handle, response.config_options); + emit_session_config_options_values(conn_id, emitter, response.config_options); Ok(()) } @@ -1221,13 +1222,13 @@ async fn poll_terminal_tool_call_output( fn emit_terminal_output_update( connection_id: &str, - app_handle: &tauri::AppHandle, + emitter: &EventEmitter, tool_call_id: &str, output: String, append: bool, ) { crate::web::event_bridge::emit_event( - app_handle, + emitter, "acp://event", AcpEvent::ToolCallUpdate { connection_id: connection_id.into(), @@ -1246,7 +1247,7 @@ async fn poll_tracked_terminal_tool_calls( terminal_runtime: &TerminalRuntime, session_id: &SessionId, connection_id: &str, - app_handle: &tauri::AppHandle, + emitter: &EventEmitter, tracked: &mut HashMap, ) { if tracked.is_empty() { @@ -1286,7 +1287,7 @@ async fn poll_tracked_terminal_tool_calls( if let Some(output) = poll_result.output { emit_terminal_output_update( connection_id, - app_handle, + emitter, &tool_call_id, output, poll_result.append, @@ -1374,7 +1375,7 @@ struct ForkExitInfo { async fn handle_fork_or_exit( loop_result: Result, sacp::Error>, conn_id: &str, - handle: &tauri::AppHandle, + emitter: &EventEmitter, perms: &PendingPermissions, cmd_rx: &mut mpsc::Receiver, terminal_runtime: Arc, @@ -1412,21 +1413,21 @@ async fn handle_fork_or_exit( let mut session = cx.attach_session(new_resp, Default::default())?; crate::web::event_bridge::emit_event( - handle, + emitter, "acp://event", AcpEvent::SessionStarted { connection_id: conn_id.to_string(), session_id: new_sid.clone(), }, ); - emit_session_modes(conn_id, handle, session.modes()); - emit_session_config_options(conn_id, handle, &initial_config_options); - emit_selectors_ready(conn_id, handle); + emit_session_modes(conn_id, emitter, session.modes()); + emit_session_config_options(conn_id, emitter, &initial_config_options); + emit_selectors_ready(conn_id, emitter); let loop_result = run_conversation_loop( &mut session, conn_id, - handle, + emitter, perms, cmd_rx, terminal_runtime.clone(), @@ -1439,7 +1440,7 @@ async fn handle_fork_or_exit( // Recursively handle nested forks Box::pin(handle_fork_or_exit( - loop_result, conn_id, handle, perms, cmd_rx, terminal_runtime, _cwd, cwd_string, + loop_result, conn_id, emitter, perms, cmd_rx, terminal_runtime, _cwd, cwd_string, )) .await } @@ -1452,7 +1453,7 @@ async fn handle_fork_or_exit( async fn run_conversation_loop<'a>( session: &mut sacp::ActiveSession<'a, Agent>, conn_id: &str, - handle: &tauri::AppHandle, + emitter: &EventEmitter, perms: &PendingPermissions, cmd_rx: &mut mpsc::Receiver, terminal_runtime: Arc, @@ -1469,7 +1470,7 @@ async fn run_conversation_loop<'a>( match update { Ok(SessionMessage::SessionMessage(dispatch)) => { let cid = conn_id.to_string(); - let h = handle.clone(); + let h = emitter.clone(); let cwd_opt = Some(cwd); let _ = MatchDispatch::new(dispatch) .if_notification( @@ -1494,7 +1495,7 @@ async fn run_conversation_loop<'a>( let prompt_blocks = map_prompt_blocks(blocks); if prompt_blocks.is_empty() { crate::web::event_bridge::emit_event( - handle, + emitter, "acp://event", AcpEvent::Error { connection_id: conn_id.into(), @@ -1505,7 +1506,7 @@ async fn run_conversation_loop<'a>( } crate::web::event_bridge::emit_event( - handle, + emitter, "acp://event", AcpEvent::StatusChanged { connection_id: conn_id.into(), @@ -1513,7 +1514,7 @@ async fn run_conversation_loop<'a>( }, ); - // Clone connection handle and session ID before entering the + // Clone connection and session ID before entering the // select loop so we can send CancelNotification without // conflicting with session.read_update()'s mutable borrow. let cx = session.connection(); @@ -1551,7 +1552,7 @@ async fn run_conversation_loop<'a>( match update { SessionMessage::SessionMessage(dispatch) => { let cid = conn_id.to_string(); - let h = handle.clone(); + let h = emitter.clone(); let runtime = terminal_runtime.clone(); let session_id = sid.clone(); let cwd_opt = Some(cwd); @@ -1588,7 +1589,7 @@ async fn run_conversation_loop<'a>( terminal_runtime.as_ref(), &sid, conn_id, - handle, + emitter, &mut tracked_terminal_tool_calls, ) .await; @@ -1599,7 +1600,7 @@ async fn run_conversation_loop<'a>( _ => "unknown", }; crate::web::event_bridge::emit_event( - handle, + emitter, "acp://event", AcpEvent::TurnComplete { connection_id: conn_id.into(), @@ -1619,7 +1620,7 @@ async fn run_conversation_loop<'a>( terminal_runtime.as_ref(), &sid, conn_id, - handle, + emitter, &mut tracked_terminal_tool_calls, ) .await; @@ -1630,7 +1631,7 @@ async fn run_conversation_loop<'a>( _ => "unknown", }; crate::web::event_bridge::emit_event( - handle, + emitter, "acp://event", AcpEvent::TurnComplete { connection_id: conn_id.into(), @@ -1645,7 +1646,7 @@ async fn run_conversation_loop<'a>( terminal_runtime.as_ref(), &sid, conn_id, - handle, + emitter, &mut tracked_terminal_tool_calls, ) .await; @@ -1668,7 +1669,7 @@ async fn run_conversation_loop<'a>( match cx.send_request_to(Agent, req).block_task().await { Ok(_) => { crate::web::event_bridge::emit_event( - handle, + emitter, "acp://event", AcpEvent::ModeChanged { connection_id: conn_id.into(), @@ -1678,7 +1679,7 @@ async fn run_conversation_loop<'a>( } Err(e) => { crate::web::event_bridge::emit_event( - handle, + emitter, "acp://event", AcpEvent::Error { connection_id: conn_id.into(), @@ -1696,14 +1697,14 @@ async fn run_conversation_loop<'a>( &cx, &sid, conn_id, - handle, + emitter, config_id, value_id, ) .await { crate::web::event_bridge::emit_event( - handle, + emitter, "acp://event", AcpEvent::Error { connection_id: conn_id.into(), @@ -1734,10 +1735,10 @@ async fn run_conversation_loop<'a>( } // Immediately emit TurnComplete so the frontend // transitions out of "prompting" and the user can - // send new messages. Don't wait for the agent — + // send new messages. Don't wait for the agent -- // it may be slow to respond or not respond at all. crate::web::event_bridge::emit_event( - handle, + emitter, "acp://event", AcpEvent::TurnComplete { connection_id: conn_id.into(), @@ -1788,7 +1789,7 @@ async fn run_conversation_loop<'a>( } crate::web::event_bridge::emit_event( - handle, + emitter, "acp://event", AcpEvent::StatusChanged { connection_id: conn_id.into(), @@ -1808,9 +1809,9 @@ async fn run_conversation_loop<'a>( } } Some(ConnectionCommand::SetMode { mode_id }) => { - if let Err(e) = set_session_mode(session, conn_id, handle, mode_id).await { + if let Err(e) = set_session_mode(session, conn_id, emitter, mode_id).await { crate::web::event_bridge::emit_event( - handle, + emitter, "acp://event", AcpEvent::Error { connection_id: conn_id.into(), @@ -1826,10 +1827,10 @@ async fn run_conversation_loop<'a>( let cx = session.connection(); let sid = session.session_id().clone(); if let Err(e) = - set_session_config_option(&cx, &sid, conn_id, handle, config_id, value_id).await + set_session_config_option(&cx, &sid, conn_id, emitter, config_id, value_id).await { crate::web::event_bridge::emit_event( - handle, + emitter, "acp://event", AcpEvent::Error { connection_id: conn_id.into(), @@ -2036,7 +2037,7 @@ fn map_plan_entries(plan: &Plan) -> Vec { /// Convert a SessionUpdate into AcpEvent(s) and emit to frontend. fn emit_conversation_update( connection_id: &str, - app_handle: &tauri::AppHandle, + emitter: &EventEmitter, update: SessionUpdate, cwd: Option<&str>, ) { @@ -2050,7 +2051,7 @@ fn emit_conversation_update( .. }) => { crate::web::event_bridge::emit_event( - app_handle, + emitter, "acp://event", AcpEvent::ContentDelta { connection_id: connection_id.into(), @@ -2066,7 +2067,7 @@ fn emit_conversation_update( .. }) => { crate::web::event_bridge::emit_event( - app_handle, + emitter, "acp://event", AcpEvent::Thinking { connection_id: connection_id.into(), @@ -2084,7 +2085,7 @@ fn emit_conversation_update( let raw_output = json_value_to_text(&tc.raw_output) .map(|text| structurize_live_output(&text)); crate::web::event_bridge::emit_event( - app_handle, + emitter, "acp://event", AcpEvent::ToolCall { connection_id: connection_id.into(), @@ -2109,7 +2110,7 @@ fn emit_conversation_update( let raw_output = json_value_to_text(&tcu.fields.raw_output) .map(|text| structurize_live_output(&text)); crate::web::event_bridge::emit_event( - app_handle, + emitter, "acp://event", AcpEvent::ToolCallUpdate { connection_id: connection_id.into(), @@ -2125,7 +2126,7 @@ fn emit_conversation_update( } SessionUpdate::CurrentModeUpdate(update) => { crate::web::event_bridge::emit_event( - app_handle, + emitter, "acp://event", AcpEvent::ModeChanged { connection_id: connection_id.into(), @@ -2135,7 +2136,7 @@ fn emit_conversation_update( } SessionUpdate::Plan(plan) => { crate::web::event_bridge::emit_event( - app_handle, + emitter, "acp://event", AcpEvent::PlanUpdate { connection_id: connection_id.into(), @@ -2144,7 +2145,7 @@ fn emit_conversation_update( ); } SessionUpdate::ConfigOptionUpdate(update) => { - emit_session_config_options_values(connection_id, app_handle, update.config_options); + emit_session_config_options_values(connection_id, emitter, update.config_options); } SessionUpdate::AvailableCommandsUpdate(update) => { let commands: Vec = update @@ -2163,7 +2164,7 @@ fn emit_conversation_update( }) .collect(); crate::web::event_bridge::emit_event( - app_handle, + emitter, "acp://event", AcpEvent::AvailableCommands { connection_id: connection_id.into(), @@ -2173,7 +2174,7 @@ fn emit_conversation_update( } SessionUpdate::UsageUpdate(update) => { crate::web::event_bridge::emit_event( - app_handle, + emitter, "acp://event", AcpEvent::UsageUpdate { connection_id: connection_id.into(), diff --git a/src-tauri/src/acp/manager.rs b/src-tauri/src/acp/manager.rs index e49dd8c..ec45df5 100644 --- a/src-tauri/src/acp/manager.rs +++ b/src-tauri/src/acp/manager.rs @@ -7,6 +7,7 @@ use crate::acp::connection::{spawn_agent_connection, AgentConnection, Connection use crate::acp::error::AcpError; use crate::acp::types::{ConnectionInfo, ForkResultInfo, PromptInputBlock}; use crate::models::agent::AgentType; +use crate::web::event_bridge::EventEmitter; pub struct ConnectionManager { connections: Arc>>, @@ -19,6 +20,13 @@ impl ConnectionManager { } } + /// Returns a shallow clone sharing the same underlying connection map. + pub fn clone_ref(&self) -> Self { + Self { + connections: self.connections.clone(), + } + } + pub async fn spawn_agent( &self, agent_type: AgentType, @@ -26,7 +34,7 @@ impl ConnectionManager { session_id: Option, runtime_env: BTreeMap, owner_window_label: String, - app_handle: tauri::AppHandle, + emitter: EventEmitter, ) -> Result { let connection_id = uuid::Uuid::new_v4().to_string(); eprintln!( @@ -41,7 +49,7 @@ impl ConnectionManager { session_id, runtime_env, owner_window_label, - app_handle, + emitter, ) .await?; diff --git a/src-tauri/src/app_state.rs b/src-tauri/src/app_state.rs new file mode 100644 index 0000000..fc241e1 --- /dev/null +++ b/src-tauri/src/app_state.rs @@ -0,0 +1,26 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use crate::acp::manager::ConnectionManager; +use crate::db::AppDatabase; +use crate::terminal::manager::TerminalManager; +use crate::web::event_bridge::{EventEmitter, WebEventBroadcaster}; +use crate::web::WebServerState; + +pub struct AppState { + pub db: AppDatabase, + pub connection_manager: ConnectionManager, + pub terminal_manager: TerminalManager, + pub event_broadcaster: Arc, + pub emitter: EventEmitter, + pub data_dir: PathBuf, + pub web_server_state: WebServerState, +} + +pub fn default_connection_manager() -> ConnectionManager { + ConnectionManager::new() +} + +pub fn default_terminal_manager() -> TerminalManager { + TerminalManager::new() +} diff --git a/src-tauri/src/bin/codeg_server.rs b/src-tauri/src/bin/codeg_server.rs new file mode 100644 index 0000000..6a27891 --- /dev/null +++ b/src-tauri/src/bin/codeg_server.rs @@ -0,0 +1,82 @@ +use std::path::PathBuf; +use std::sync::Arc; + +use codeg_lib::app_state::AppState; +use codeg_lib::web::event_bridge::{EventEmitter, WebEventBroadcaster}; +use codeg_lib::web::{ + find_static_dir_standalone, generate_random_token, get_local_addresses, WebServerState, +}; + +#[tokio::main] +async fn main() { + let port: u16 = std::env::var("CODEG_PORT") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(3080); + let host = std::env::var("CODEG_HOST").unwrap_or_else(|_| "0.0.0.0".to_string()); + let token = std::env::var("CODEG_TOKEN").unwrap_or_else(|_| generate_random_token()); + let data_dir = std::env::var("CODEG_DATA_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| default_data_dir()); + let static_dir_env = std::env::var("CODEG_STATIC_DIR").ok(); + + let static_dir = find_static_dir_standalone(static_dir_env.as_deref()); + let app_version = env!("CARGO_PKG_VERSION"); + + eprintln!("[SERVER] codeg-server v{}", app_version); + eprintln!("[SERVER] Data directory: {}", data_dir.display()); + eprintln!("[SERVER] Static directory: {}", static_dir.display()); + + // Initialize database + let db = codeg_lib::db::init_database(&data_dir, app_version) + .await + .expect("Failed to initialize database"); + + // Create shared broadcaster + let broadcaster = Arc::new(WebEventBroadcaster::new()); + let emitter = EventEmitter::WebOnly(broadcaster.clone()); + + // Build AppState + let state = Arc::new(AppState { + db, + connection_manager: codeg_lib::app_state::default_connection_manager(), + terminal_manager: codeg_lib::app_state::default_terminal_manager(), + event_broadcaster: broadcaster, + emitter, + data_dir, + web_server_state: WebServerState::new(), + }); + + // Build router + let router = codeg_lib::web::router::build_router(state, token.clone(), static_dir); + + // Bind + let addr = format!("{}:{}", host, port); + let listener = tokio::net::TcpListener::bind(&addr) + .await + .unwrap_or_else(|e| { + eprintln!("[SERVER] Failed to bind {}: {}", addr, e); + std::process::exit(1); + }); + + let actual_port = listener.local_addr().map(|a| a.port()).unwrap_or(port); + let addresses = get_local_addresses(actual_port); + + eprintln!("[SERVER] Token: {}", token); + eprintln!("[SERVER] Listening on:"); + for addr in &addresses { + eprintln!(" {}", addr); + } + + // Start serving + if let Err(e) = axum::serve(listener, router).await { + eprintln!("[SERVER] Server error: {}", e); + std::process::exit(1); + } +} + +fn default_data_dir() -> PathBuf { + dirs::data_dir() + .map(|d| d.join("codeg")) + .unwrap_or_else(|| PathBuf::from(".codeg-data")) +} diff --git a/src-tauri/src/commands/acp.rs b/src-tauri/src/commands/acp.rs index a8acf34..68234f0 100644 --- a/src-tauri/src/commands/acp.rs +++ b/src-tauri/src/commands/acp.rs @@ -3,6 +3,7 @@ use std::fs; use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; +#[cfg(feature = "tauri-runtime")] use tauri::State; use crate::acp::binary_cache; @@ -17,6 +18,7 @@ use crate::acp::types::{ use crate::db::service::agent_setting_service; use crate::db::AppDatabase; use crate::models::agent::AgentType; +use crate::web::event_bridge::EventEmitter; const ACP_AGENTS_UPDATED_EVENT: &str = "app://acp-agents-updated"; @@ -28,12 +30,12 @@ struct AcpAgentsUpdatedEventPayload { } fn emit_acp_agents_updated( - app: &tauri::AppHandle, + emitter: &EventEmitter, reason: &'static str, agent_type: Option, ) { crate::web::event_bridge::emit_event( - app, + emitter, ACP_AGENTS_UPDATED_EVENT, AcpAgentsUpdatedEventPayload { reason, agent_type }, ); @@ -1346,7 +1348,7 @@ pub(crate) fn build_runtime_env_from_setting( merged } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn acp_preflight( agent_type: AgentType, force_refresh: Option, @@ -1357,7 +1359,8 @@ pub async fn acp_preflight( Ok(preflight::run_preflight(agent_type).await) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn acp_connect( agent_type: AgentType, working_dir: Option, @@ -1400,6 +1403,7 @@ pub async fn acp_connect( } } + let emitter = EventEmitter::Tauri(app_handle); manager .spawn_agent( agent_type, @@ -1407,12 +1411,13 @@ pub async fn acp_connect( session_id, runtime_env, window.label().to_string(), - app_handle, + emitter, ) .await } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn acp_prompt( connection_id: String, blocks: Vec, @@ -1421,7 +1426,8 @@ pub async fn acp_prompt( manager.send_prompt(&connection_id, blocks).await } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn acp_set_mode( connection_id: String, mode_id: String, @@ -1430,7 +1436,8 @@ pub async fn acp_set_mode( manager.set_mode(&connection_id, mode_id).await } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn acp_set_config_option( connection_id: String, config_id: String, @@ -1442,7 +1449,8 @@ pub async fn acp_set_config_option( .await } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn acp_cancel( connection_id: String, manager: State<'_, ConnectionManager>, @@ -1450,7 +1458,8 @@ pub async fn acp_cancel( manager.cancel(&connection_id).await } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn acp_fork( connection_id: String, manager: State<'_, ConnectionManager>, @@ -1458,7 +1467,8 @@ pub async fn acp_fork( manager.fork_session(&connection_id).await } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn acp_respond_permission( connection_id: String, request_id: String, @@ -1470,7 +1480,8 @@ pub async fn acp_respond_permission( .await } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn acp_disconnect( connection_id: String, manager: State<'_, ConnectionManager>, @@ -1478,17 +1489,17 @@ pub async fn acp_disconnect( manager.disconnect(&connection_id).await } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn acp_list_connections( manager: State<'_, ConnectionManager>, ) -> Result, AcpError> { Ok(manager.list_connections().await) } -#[tauri::command] -pub async fn acp_get_agent_status( +pub(crate) async fn acp_get_agent_status_core( agent_type: AgentType, - db: tauri::State<'_, AppDatabase>, + db: &AppDatabase, ) -> Result { let platform = registry::current_platform(); let meta = registry::get_agent_meta(agent_type); @@ -1523,9 +1534,17 @@ pub async fn acp_get_agent_status( }) } -#[tauri::command] -pub async fn acp_list_agents( +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] +pub async fn acp_get_agent_status( + agent_type: AgentType, db: tauri::State<'_, AppDatabase>, +) -> Result { + acp_get_agent_status_core(agent_type, &db).await +} + +pub(crate) async fn acp_list_agents_core( + db: &AppDatabase, ) -> Result, AcpError> { let platform = registry::current_platform(); let agent_types = registry::all_acp_agents(); @@ -1660,7 +1679,15 @@ pub async fn acp_list_agents( Ok(agents) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] +pub async fn acp_list_agents( + db: tauri::State<'_, AppDatabase>, +) -> Result, AcpError> { + acp_list_agents_core(&db).await +} + +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn acp_clear_binary_cache(agent_type: AgentType) -> Result<(), AcpError> { let meta = registry::get_agent_meta(agent_type); if matches!( @@ -1682,7 +1709,7 @@ pub(crate) async fn acp_update_agent_preferences_core( codex_auth_json: Option, codex_config_toml: Option, db: &AppDatabase, - app: &tauri::AppHandle, + emitter: &EventEmitter, ) -> Result<(), AcpError> { let default = agent_setting_service::AgentDefaultInput { agent_type, @@ -1737,7 +1764,7 @@ pub(crate) async fn acp_update_agent_preferences_core( codex_config_toml.as_deref(), )?; } - emit_acp_agents_updated(app, "preferences_updated", Some(agent_type)); + emit_acp_agents_updated(emitter, "preferences_updated", Some(agent_type)); return Ok(()); } @@ -1748,7 +1775,7 @@ pub(crate) async fn acp_update_agent_preferences_core( if let Some(raw) = config_json.as_deref() { persist_agent_local_config_json(agent_type, Some(raw))?; } - emit_acp_agents_updated(app, "preferences_updated", Some(agent_type)); + emit_acp_agents_updated(emitter, "preferences_updated", Some(agent_type)); return Ok(()); } @@ -1756,7 +1783,7 @@ pub(crate) async fn acp_update_agent_preferences_core( if let Some(raw) = config_json.as_deref() { persist_cline_local_config(Some(raw))?; } - emit_acp_agents_updated(app, "preferences_updated", Some(agent_type)); + emit_acp_agents_updated(emitter, "preferences_updated", Some(agent_type)); return Ok(()); } @@ -1775,11 +1802,12 @@ pub(crate) async fn acp_update_agent_preferences_core( let local_patch_json = serde_json::to_string(&local_patch_value) .map_err(|e| AcpError::protocol(format!("serialize local patch failed: {e}")))?; persist_agent_local_config_json(agent_type, Some(local_patch_json.as_str()))?; - emit_acp_agents_updated(app, "preferences_updated", Some(agent_type)); + emit_acp_agents_updated(emitter, "preferences_updated", Some(agent_type)); Ok(()) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] #[allow(clippy::too_many_arguments)] pub async fn acp_update_agent_preferences( agent_type: AgentType, @@ -1792,15 +1820,16 @@ pub async fn acp_update_agent_preferences( db: State<'_, AppDatabase>, app: tauri::AppHandle, ) -> Result<(), AcpError> { + let emitter = EventEmitter::Tauri(app); acp_update_agent_preferences_core( agent_type, enabled, env, config_json, opencode_auth_json, - codex_auth_json, codex_config_toml, &db, &app, + codex_auth_json, codex_config_toml, &db, &emitter, ).await } pub(crate) async fn acp_download_agent_binary_core( agent_type: AgentType, - app: &tauri::AppHandle, + emitter: &EventEmitter, ) -> Result<(), AcpError> { let meta = registry::get_agent_meta(agent_type); match meta.distribution { @@ -1823,7 +1852,7 @@ pub(crate) async fn acp_download_agent_binary_core( let _ = binary_cache::ensure_binary_for_agent(agent_type, version, fallback.url, cmd) .await?; - emit_acp_agents_updated(app, "binary_downloaded", Some(agent_type)); + emit_acp_agents_updated(emitter, "binary_downloaded", Some(agent_type)); Ok(()) } registry::AgentDistribution::Npx { .. } => Err( @@ -1832,12 +1861,14 @@ pub(crate) async fn acp_download_agent_binary_core( } } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn acp_download_agent_binary( agent_type: AgentType, app: tauri::AppHandle, ) -> Result<(), AcpError> { - acp_download_agent_binary_core(agent_type, &app).await + let emitter = EventEmitter::Tauri(app); + acp_download_agent_binary_core(agent_type, &emitter).await } pub(crate) async fn acp_detect_agent_local_version_core( @@ -1863,7 +1894,8 @@ pub(crate) async fn acp_detect_agent_local_version_core( Ok(fallback) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn acp_detect_agent_local_version( agent_type: AgentType, db: State<'_, AppDatabase>, @@ -1875,7 +1907,7 @@ pub(crate) async fn acp_prepare_npx_agent_core( agent_type: AgentType, registry_version: Option, db: &AppDatabase, - app: &tauri::AppHandle, + emitter: &EventEmitter, ) -> Result { let meta = registry::get_agent_meta(agent_type); match meta.distribution { @@ -1918,7 +1950,7 @@ pub(crate) async fn acp_prepare_npx_agent_core( ) .await .map_err(|e| AcpError::protocol(e.to_string()))?; - emit_acp_agents_updated(app, "npx_prepared", Some(agent_type)); + emit_acp_agents_updated(emitter, "npx_prepared", Some(agent_type)); Ok(resolved) } registry::AgentDistribution::Binary { .. } => Err(AcpError::protocol( @@ -1927,20 +1959,22 @@ pub(crate) async fn acp_prepare_npx_agent_core( } } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn acp_prepare_npx_agent( agent_type: AgentType, registry_version: Option, db: State<'_, AppDatabase>, app: tauri::AppHandle, ) -> Result { - acp_prepare_npx_agent_core(agent_type, registry_version, &db, &app).await + let emitter = EventEmitter::Tauri(app); + acp_prepare_npx_agent_core(agent_type, registry_version, &db, &emitter).await } pub(crate) async fn acp_uninstall_agent_core( agent_type: AgentType, db: &AppDatabase, - app: &tauri::AppHandle, + emitter: &EventEmitter, ) -> Result<(), AcpError> { let meta = registry::get_agent_meta(agent_type); match meta.distribution { @@ -1955,23 +1989,25 @@ pub(crate) async fn acp_uninstall_agent_core( agent_setting_service::set_installed_version(&db.conn, agent_type, None) .await .map_err(|e| AcpError::protocol(e.to_string()))?; - emit_acp_agents_updated(app, "agent_uninstalled", Some(agent_type)); + emit_acp_agents_updated(emitter, "agent_uninstalled", Some(agent_type)); Ok(()) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn acp_uninstall_agent( agent_type: AgentType, db: State<'_, AppDatabase>, app: tauri::AppHandle, ) -> Result<(), AcpError> { - acp_uninstall_agent_core(agent_type, &db, &app).await + let emitter = EventEmitter::Tauri(app); + acp_uninstall_agent_core(agent_type, &db, &emitter).await } pub(crate) async fn acp_reorder_agents_core( agent_types: &[AgentType], db: &AppDatabase, - app: &tauri::AppHandle, + emitter: &EventEmitter, ) -> Result<(), AcpError> { if agent_types.is_empty() { return Ok(()); @@ -1986,20 +2022,22 @@ pub(crate) async fn acp_reorder_agents_core( AcpError::protocol(message) } })?; - emit_acp_agents_updated(app, "agent_reordered", None); + emit_acp_agents_updated(emitter, "agent_reordered", None); Ok(()) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn acp_reorder_agents( agent_types: Vec, db: State<'_, AppDatabase>, app: tauri::AppHandle, ) -> Result<(), AcpError> { - acp_reorder_agents_core(&agent_types, &db, &app).await + let emitter = EventEmitter::Tauri(app); + acp_reorder_agents_core(&agent_types, &db, &emitter).await } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn acp_list_agent_skills( agent_type: AgentType, workspace_path: Option, @@ -2065,7 +2103,7 @@ pub async fn acp_list_agent_skills( }) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn acp_read_agent_skill( agent_type: AgentType, scope: AgentSkillScope, @@ -2088,7 +2126,7 @@ pub async fn acp_read_agent_skill( Ok(AgentSkillContent { skill, content }) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn acp_save_agent_skill( agent_type: AgentType, scope: AgentSkillScope, @@ -2148,7 +2186,7 @@ pub async fn acp_save_agent_skill( Ok(skill) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn acp_delete_agent_skill( agent_type: AgentType, scope: AgentSkillScope, diff --git a/src-tauri/src/commands/conversations.rs b/src-tauri/src/commands/conversations.rs index aefde52..1144817 100644 --- a/src-tauri/src/commands/conversations.rs +++ b/src-tauri/src/commands/conversations.rs @@ -13,7 +13,8 @@ use crate::parsers::openclaw::OpenClawParser; use crate::parsers::opencode::OpenCodeParser; use crate::parsers::{path_eq_for_matching, AgentParser, ParseError}; -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn list_folder_conversations( db: tauri::State<'_, AppDatabase>, folder_id: i32, @@ -115,7 +116,7 @@ fn list_conversations_sync( all_conversations } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn list_conversations( agent_type: Option, search: Option, @@ -132,7 +133,7 @@ pub async fn list_conversations( }) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn get_conversation( agent_type: AgentType, conversation_id: String, @@ -158,7 +159,7 @@ pub async fn get_conversation( })? } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn list_folders() -> Result, AppCommandError> { tokio::task::spawn_blocking(move || -> Result, AppCommandError> { let all_conversations = list_conversations_sync(None, None, None, None); @@ -170,7 +171,7 @@ pub async fn list_folders() -> Result, AppCommandError> { })? } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn get_stats() -> Result { tokio::task::spawn_blocking(move || -> Result { let all_conversations = list_conversations_sync(None, None, None, None); @@ -183,7 +184,7 @@ pub async fn get_stats() -> Result { })? } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn get_sidebar_data() -> Result { tokio::task::spawn_blocking(move || -> Result { let all_conversations = list_conversations_sync(None, None, None, None); @@ -231,7 +232,8 @@ fn compute_folders(all_conversations: &[ConversationSummary]) -> Vec folders } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn import_local_conversations( db: tauri::State<'_, AppDatabase>, folder_id: i32, @@ -351,7 +353,8 @@ pub async fn get_folder_conversation_core( }) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn get_folder_conversation( db: tauri::State<'_, AppDatabase>, conversation_id: i32, @@ -382,7 +385,8 @@ pub async fn create_conversation_core( Ok(model.id) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn create_conversation( db: tauri::State<'_, AppDatabase>, folder_id: i32, @@ -411,7 +415,8 @@ async fn detect_git_branch(path: &str) -> Option { Some(branch) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn update_conversation_status( db: tauri::State<'_, AppDatabase>, conversation_id: i32, @@ -426,7 +431,8 @@ pub async fn update_conversation_status( .map_err(AppCommandError::from) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn update_conversation_title( db: tauri::State<'_, AppDatabase>, conversation_id: i32, @@ -437,7 +443,8 @@ pub async fn update_conversation_title( .map_err(AppCommandError::from) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn update_conversation_external_id( db: tauri::State<'_, AppDatabase>, conversation_id: i32, @@ -448,7 +455,8 @@ pub async fn update_conversation_external_id( .map_err(AppCommandError::from) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn delete_conversation( db: tauri::State<'_, AppDatabase>, conversation_id: i32, diff --git a/src-tauri/src/commands/folder_commands.rs b/src-tauri/src/commands/folder_commands.rs index 3656dcf..f69ae60 100644 --- a/src-tauri/src/commands/folder_commands.rs +++ b/src-tauri/src/commands/folder_commands.rs @@ -72,7 +72,8 @@ pub(crate) fn load_package_scripts_as_commands(folder_path: &str) -> Vec<(String commands } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn list_folder_commands( db: tauri::State<'_, AppDatabase>, folder_id: i32, @@ -80,7 +81,8 @@ pub async fn list_folder_commands( folder_command_service::list_by_folder(&db.conn, folder_id).await } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn create_folder_command( db: tauri::State<'_, AppDatabase>, folder_id: i32, @@ -90,7 +92,8 @@ pub async fn create_folder_command( folder_command_service::create(&db.conn, folder_id, &name, &command).await } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn update_folder_command( db: tauri::State<'_, AppDatabase>, id: i32, @@ -101,7 +104,8 @@ pub async fn update_folder_command( folder_command_service::update(&db.conn, id, name, command, sort_order).await } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn delete_folder_command( db: tauri::State<'_, AppDatabase>, id: i32, @@ -109,7 +113,8 @@ pub async fn delete_folder_command( folder_command_service::delete(&db.conn, id).await } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn reorder_folder_commands( db: tauri::State<'_, AppDatabase>, folder_id: i32, @@ -118,7 +123,8 @@ pub async fn reorder_folder_commands( folder_command_service::reorder(&db.conn, folder_id, ids).await } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn bootstrap_folder_commands_from_package_json( db: tauri::State<'_, AppDatabase>, folder_id: i32, diff --git a/src-tauri/src/commands/folders.rs b/src-tauri/src/commands/folders.rs index 29d2b3d..d441ba3 100644 --- a/src-tauri/src/commands/folders.rs +++ b/src-tauri/src/commands/folders.rs @@ -14,6 +14,7 @@ use serde::Serialize; use tokio::sync::Semaphore; use walkdir::WalkDir; +#[cfg(feature = "tauri-runtime")] use tauri::Manager; use crate::app_error::AppCommandError; @@ -21,6 +22,7 @@ use crate::db::error::DbError; use crate::db::service::folder_service; use crate::db::AppDatabase; use crate::models::{FolderDetail, FolderHistoryEntry, GitCredentials, OpenedConversation}; +use crate::web::event_bridge::EventEmitter; /// Configure a git command for remote operations: /// - Always disable interactive prompts (prevent hanging in a GUI app) @@ -31,9 +33,9 @@ async fn prepare_remote_git_cmd( repo_path: &str, credentials: Option<&GitCredentials>, db: &AppDatabase, - app_handle: &tauri::AppHandle, + data_dir: &std::path::Path, ) { - prepare_remote_git_cmd_with_remote(cmd, repo_path, None, credentials, db, app_handle).await; + prepare_remote_git_cmd_with_remote(cmd, repo_path, None, credentials, db, data_dir).await; } /// Same as `prepare_remote_git_cmd` but allows specifying a remote name @@ -44,29 +46,27 @@ async fn prepare_remote_git_cmd_with_remote( remote_name: Option<&str>, credentials: Option<&GitCredentials>, db: &AppDatabase, - app_handle: &tauri::AppHandle, + data_dir: &std::path::Path, ) { cmd.env("GIT_TERMINAL_PROMPT", "0") .stdin(Stdio::null()); - if let Ok(data_dir) = app_handle.path().app_data_dir() { - if let Some(creds) = credentials { - // Explicit credentials provided (e.g. from credential dialog) - if let Ok(askpass) = crate::git_credential::ensure_askpass_script(&data_dir) { - crate::git_credential::inject_credentials( - cmd, - &creds.username, - &creds.password, - &askpass, - ); - } - } else { - // Fall back to stored accounts, matching against the specified remote - crate::git_credential::try_inject_for_repo_remote( - cmd, repo_path, remote_name, &db.conn, &data_dir, - ) - .await; + if let Some(creds) = credentials { + // Explicit credentials provided (e.g. from credential dialog) + if let Ok(askpass) = crate::git_credential::ensure_askpass_script(data_dir) { + crate::git_credential::inject_credentials( + cmd, + &creds.username, + &creds.password, + &askpass, + ); } + } else { + // Fall back to stored accounts, matching against the specified remote + crate::git_credential::try_inject_for_repo_remote( + cmd, repo_path, remote_name, &db.conn, data_dir, + ) + .await; } } @@ -76,24 +76,22 @@ async fn prepare_remote_git_cmd_for_url( clone_url: &str, credentials: Option<&GitCredentials>, db: &AppDatabase, - app_handle: &tauri::AppHandle, + data_dir: &std::path::Path, ) { cmd.env("GIT_TERMINAL_PROMPT", "0") .stdin(Stdio::null()); - if let Ok(data_dir) = app_handle.path().app_data_dir() { - if let Some(creds) = credentials { - if let Ok(askpass) = crate::git_credential::ensure_askpass_script(&data_dir) { - crate::git_credential::inject_credentials( - cmd, - &creds.username, - &creds.password, - &askpass, - ); - } - } else { - crate::git_credential::try_inject_for_url(cmd, clone_url, &db.conn, &data_dir).await; + if let Some(creds) = credentials { + if let Ok(askpass) = crate::git_credential::ensure_askpass_script(data_dir) { + crate::git_credential::inject_credentials( + cmd, + &creds.username, + &creds.password, + &askpass, + ); } + } else { + crate::git_credential::try_inject_for_url(cmd, clone_url, &db.conn, data_dir).await; } } @@ -462,7 +460,8 @@ async fn estimate_push_commit_count(path: &str) -> usize { parse_count_from_output(&output.stdout).unwrap_or(0) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn get_folder( db: tauri::State<'_, AppDatabase>, folder_id: i32, @@ -472,7 +471,8 @@ pub async fn get_folder( .ok_or_else(|| DbError::Migration(format!("Folder {} not found", folder_id))) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn load_folder_history( db: tauri::State<'_, AppDatabase>, ) -> Result, AppCommandError> { @@ -481,7 +481,8 @@ pub async fn load_folder_history( .map_err(AppCommandError::from) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn add_folder_to_history( db: tauri::State<'_, AppDatabase>, path: String, @@ -513,7 +514,8 @@ pub(crate) async fn set_folder_parent_branch_core( Ok(()) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn set_folder_parent_branch( db: tauri::State<'_, AppDatabase>, path: String, @@ -522,7 +524,8 @@ pub async fn set_folder_parent_branch( set_folder_parent_branch_core(&db.conn, &path, parent_branch).await } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn remove_folder_from_history( db: tauri::State<'_, AppDatabase>, path: String, @@ -532,7 +535,8 @@ pub async fn remove_folder_from_history( .map_err(AppCommandError::from) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn save_folder_opened_conversations( db: tauri::State<'_, AppDatabase>, folder_id: i32, @@ -541,7 +545,7 @@ pub async fn save_folder_opened_conversations( folder_service::save_opened_conversations(&db.conn, folder_id, items).await } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn create_folder_directory(path: String) -> Result<(), AppCommandError> { std::fs::create_dir_all(&path).map_err(AppCommandError::io) } @@ -551,7 +555,7 @@ pub(crate) async fn clone_repository_core( target_dir: &str, credentials: Option<&GitCredentials>, db: &AppDatabase, - app_handle: &tauri::AppHandle, + data_dir: &std::path::Path, ) -> Result<(), AppCommandError> { if url.trim().is_empty() || target_dir.trim().is_empty() { return Err(AppCommandError::invalid_input( @@ -561,7 +565,7 @@ pub(crate) async fn clone_repository_core( let mut cmd = crate::process::tokio_command("git"); cmd.args(["clone", url, target_dir]); - prepare_remote_git_cmd_for_url(&mut cmd, url, credentials, db, app_handle).await; + prepare_remote_git_cmd_for_url(&mut cmd, url, credentials, db, data_dir).await; let output = cmd .output() @@ -584,7 +588,8 @@ pub(crate) async fn clone_repository_core( Ok(()) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn clone_repository( url: String, target_dir: String, @@ -592,7 +597,10 @@ pub async fn clone_repository( db: tauri::State<'_, AppDatabase>, app_handle: tauri::AppHandle, ) -> Result<(), AppCommandError> { - clone_repository_core(&url, &target_dir, credentials.as_ref(), &db, &app_handle).await + let data_dir = app_handle.path().app_data_dir().map_err(|e| { + AppCommandError::external_command("Failed to resolve app data dir", e.to_string()) + })?; + clone_repository_core(&url, &target_dir, credentials.as_ref(), &db, &data_dir).await } fn classify_git_clone_error(stderr: &str) -> AppCommandError { @@ -643,7 +651,7 @@ fn classify_git_clone_error(stderr: &str) -> AppCommandError { AppCommandError::external_command("Git clone failed", stderr.to_string()) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn get_git_branch(path: String) -> Result, AppCommandError> { let output = crate::process::tokio_command("git") .args(["rev-parse", "--abbrev-ref", "HEAD"]) @@ -679,7 +687,7 @@ pub async fn get_git_branch(path: String) -> Result, AppCommandEr Ok(None) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_init(path: String) -> Result<(), AppCommandError> { let output = crate::process::tokio_command("git") .args(["init"]) @@ -698,14 +706,14 @@ pub(crate) async fn git_pull_core( path: &str, credentials: Option<&GitCredentials>, db: &AppDatabase, - app_handle: &tauri::AppHandle, + data_dir: &std::path::Path, ) -> Result { let head_before = get_head_hash(path).await?; // Step 1: fetch from remote let mut fetch_cmd = crate::process::tokio_command("git"); fetch_cmd.args(["fetch"]).current_dir(path); - prepare_remote_git_cmd(&mut fetch_cmd, path, credentials, db, app_handle).await; + prepare_remote_git_cmd(&mut fetch_cmd, path, credentials, db, data_dir).await; let fetch_output = fetch_cmd .output() @@ -823,20 +831,24 @@ pub(crate) async fn git_pull_core( }) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_pull( path: String, credentials: Option, db: tauri::State<'_, AppDatabase>, app_handle: tauri::AppHandle, ) -> Result { - git_pull_core(&path, credentials.as_ref(), &db, &app_handle).await + let data_dir = app_handle.path().app_data_dir().map_err(|e| { + AppCommandError::external_command("Failed to resolve app data dir", e.to_string()) + })?; + git_pull_core(&path, credentials.as_ref(), &db, &data_dir).await } /// Start a merge with the upstream branch (used by merge workspace after pull conflict detection). /// This recreates the conflict state so that :1:, :2:, :3: stage entries are available. /// If `upstream_commit` is provided, merge against that specific commit instead of `@{u}`. -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_start_pull_merge( path: String, upstream_commit: Option, @@ -862,7 +874,7 @@ pub async fn git_start_pull_merge( Ok(()) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_has_merge_head(path: String) -> Result { let output = crate::process::tokio_command("git") .args(["rev-parse", "--verify", "MERGE_HEAD"]) @@ -877,11 +889,11 @@ pub(crate) async fn git_fetch_core( path: &str, credentials: Option<&GitCredentials>, db: &AppDatabase, - app_handle: &tauri::AppHandle, + data_dir: &std::path::Path, ) -> Result { let mut cmd = crate::process::tokio_command("git"); cmd.args(["fetch", "--all"]).current_dir(path); - prepare_remote_git_cmd(&mut cmd, path, credentials, db, app_handle).await; + prepare_remote_git_cmd(&mut cmd, path, credentials, db, data_dir).await; let output = cmd .output() @@ -894,17 +906,21 @@ pub(crate) async fn git_fetch_core( Ok(String::from_utf8_lossy(&output.stderr).trim().to_string()) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_fetch( path: String, credentials: Option, db: tauri::State<'_, AppDatabase>, app_handle: tauri::AppHandle, ) -> Result { - git_fetch_core(&path, credentials.as_ref(), &db, &app_handle).await + let data_dir = app_handle.path().app_data_dir().map_err(|e| { + AppCommandError::external_command("Failed to resolve app data dir", e.to_string()) + })?; + git_fetch_core(&path, credentials.as_ref(), &db, &data_dir).await } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_push_info(path: String) -> Result { // Get current branch name let branch_output = crate::process::tokio_command("git") @@ -941,7 +957,8 @@ pub async fn git_push_info(path: String) -> Result } pub(crate) async fn git_push_core( - app: &tauri::AppHandle, + data_dir: &std::path::Path, + emitter: &EventEmitter, folder_id: Option, path: &str, remote: Option<&str>, @@ -990,13 +1007,13 @@ pub(crate) async fn git_push_core( let mut cmd = crate::process::tokio_command("git"); cmd.args(["push", "--set-upstream", target_remote, &branch]) .current_dir(path); - prepare_remote_git_cmd_with_remote(&mut cmd, path, Some(target_remote), credentials, db, app).await; + prepare_remote_git_cmd_with_remote(&mut cmd, path, Some(target_remote), credentials, db, data_dir).await; cmd.output().await.map_err(AppCommandError::io)? } else { let mut cmd = crate::process::tokio_command("git"); cmd.args(["push", target_remote, &branch]) .current_dir(path); - prepare_remote_git_cmd_with_remote(&mut cmd, path, Some(target_remote), credentials, db, app).await; + prepare_remote_git_cmd_with_remote(&mut cmd, path, Some(target_remote), credentials, db, data_dir).await; cmd.output().await.map_err(AppCommandError::io)? }; @@ -1008,7 +1025,7 @@ pub(crate) async fn git_push_core( if let Some(folder_id) = folder_id { crate::web::event_bridge::emit_event( - app, + emitter, "folder://git-push-succeeded", GitPushSucceededEvent { folder_id, @@ -1024,7 +1041,8 @@ pub(crate) async fn git_push_core( }) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_push( app: tauri::AppHandle, window: tauri::WebviewWindow, @@ -1037,10 +1055,14 @@ pub async fn git_push( .label() .strip_prefix("push-") .and_then(|value| value.parse::().ok()); - git_push_core(&app, folder_id, &path, remote.as_deref(), credentials.as_ref(), &db).await + let data_dir = app.path().app_data_dir().map_err(|e| { + AppCommandError::external_command("Failed to resolve app data dir", e.to_string()) + })?; + let emitter = EventEmitter::Tauri(app.clone()); + git_push_core(&data_dir, &emitter, folder_id, &path, remote.as_deref(), credentials.as_ref(), &db).await } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_new_branch( path: String, branch_name: String, @@ -1067,7 +1089,7 @@ pub async fn git_new_branch( Ok(()) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_worktree_add( path: String, branch_name: String, @@ -1112,7 +1134,7 @@ pub async fn git_worktree_add( Ok(()) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_checkout(path: String, branch_name: String) -> Result<(), AppCommandError> { let output = crate::process::tokio_command("git") .args(["checkout", &branch_name]) @@ -1127,7 +1149,7 @@ pub async fn git_checkout(path: String, branch_name: String) -> Result<(), AppCo Ok(()) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_list_branches(path: String) -> Result, AppCommandError> { let output = crate::process::tokio_command("git") .args(["branch", "--format=%(refname:short)"]) @@ -1148,7 +1170,7 @@ pub async fn git_list_branches(path: String) -> Result, AppCommandEr Ok(branches) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_stash_push( path: String, message: Option, @@ -1177,7 +1199,7 @@ pub async fn git_stash_push( Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_stash_pop( path: String, stash_ref: Option, @@ -1201,7 +1223,7 @@ pub async fn git_stash_pop( Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_stash_list(path: String) -> Result, AppCommandError> { let output = crate::process::tokio_command("git") .args(["stash", "list", "--format=%gd||%gs||%ci"]) @@ -1262,7 +1284,7 @@ pub async fn git_stash_list(path: String) -> Result, AppComma Ok(entries) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_stash_apply( path: String, stash_ref: String, @@ -1280,7 +1302,7 @@ pub async fn git_stash_apply( Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_stash_drop( path: String, stash_ref: String, @@ -1298,7 +1320,7 @@ pub async fn git_stash_drop( Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_stash_clear(path: String) -> Result { let output = crate::process::tokio_command("git") .args(["stash", "clear"]) @@ -1313,7 +1335,7 @@ pub async fn git_stash_clear(path: String) -> Result { Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_stash_show( path: String, stash_ref: String, @@ -1345,7 +1367,7 @@ pub async fn git_stash_show( Ok(entries) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_status( path: String, show_all_untracked: Option, @@ -1379,7 +1401,7 @@ pub async fn git_status( Ok(entries) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_is_tracked(path: String, file: String) -> Result { let literal_file = to_git_literal_pathspec(&file); let output = crate::process::tokio_command("git") @@ -1393,7 +1415,7 @@ pub async fn git_is_tracked(path: String, file: String) -> Result) -> Result { let literal_file = file.as_deref().map(to_git_literal_pathspec); let mut args = vec!["diff".to_string(), "HEAD".to_string()]; @@ -1428,7 +1450,7 @@ pub async fn git_diff(path: String, file: Option) -> Result, conn: &sea_orm::DatabaseConnection, path: &str, @@ -1587,7 +1609,7 @@ pub(crate) async fn git_commit_core( if let Some(folder_id) = folder_id { crate::web::event_bridge::emit_event( - app, + emitter, "folder://git-commit-succeeded", GitCommitSucceededEvent { folder_id, @@ -1599,7 +1621,8 @@ pub(crate) async fn git_commit_core( Ok(GitCommitResult { committed_files }) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_commit( app: tauri::AppHandle, window: tauri::WebviewWindow, @@ -1612,10 +1635,11 @@ pub async fn git_commit( .label() .strip_prefix("commit-") .and_then(|value| value.parse::().ok()); - git_commit_core(&app, folder_id, &db.conn, &path, &message, &files).await + let emitter = EventEmitter::Tauri(app.clone()); + git_commit_core(&emitter, folder_id, &db.conn, &path, &message, &files).await } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_rollback_file(path: String, file: String) -> Result<(), AppCommandError> { let target = file.trim(); if target.is_empty() { @@ -1678,7 +1702,7 @@ pub async fn git_rollback_file(path: String, file: String) -> Result<(), AppComm Ok(()) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_add_files(path: String, files: Vec) -> Result<(), AppCommandError> { if files.is_empty() { return Ok(()); @@ -1701,7 +1725,7 @@ pub async fn git_add_files(path: String, files: Vec) -> Result<(), AppCo Ok(()) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_list_all_branches(path: String) -> Result { let local_fut = crate::process::tokio_command("git") .args(["branch", "--format=%(refname:short)"]) @@ -1775,7 +1799,7 @@ pub async fn git_list_all_branches(path: String) -> Result Result, AppCommandError> { let output = crate::process::tokio_command("git") .args(["remote", "-v"]) @@ -1815,11 +1839,11 @@ pub(crate) async fn git_fetch_remote_core( name: &str, credentials: Option<&GitCredentials>, db: &AppDatabase, - app_handle: &tauri::AppHandle, + data_dir: &std::path::Path, ) -> Result { let mut cmd = crate::process::tokio_command("git"); cmd.args(["fetch", name]).current_dir(path); - prepare_remote_git_cmd_with_remote(&mut cmd, path, Some(name), credentials, db, app_handle).await; + prepare_remote_git_cmd_with_remote(&mut cmd, path, Some(name), credentials, db, data_dir).await; let output = cmd .output() @@ -1832,7 +1856,8 @@ pub(crate) async fn git_fetch_remote_core( Ok(String::from_utf8_lossy(&output.stderr).trim().to_string()) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_fetch_remote( path: String, name: String, @@ -1840,10 +1865,13 @@ pub async fn git_fetch_remote( db: tauri::State<'_, AppDatabase>, app_handle: tauri::AppHandle, ) -> Result { - git_fetch_remote_core(&path, &name, credentials.as_ref(), &db, &app_handle).await + let data_dir = app_handle.path().app_data_dir().map_err(|e| { + AppCommandError::external_command("Failed to resolve app data dir", e.to_string()) + })?; + git_fetch_remote_core(&path, &name, credentials.as_ref(), &db, &data_dir).await } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_add_remote( path: String, name: String, @@ -1862,7 +1890,7 @@ pub async fn git_add_remote( Ok(()) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_remove_remote(path: String, name: String) -> Result<(), AppCommandError> { let output = crate::process::tokio_command("git") .args(["remote", "remove", &name]) @@ -1877,7 +1905,7 @@ pub async fn git_remove_remote(path: String, name: String) -> Result<(), AppComm Ok(()) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_set_remote_url( path: String, name: String, @@ -1896,7 +1924,7 @@ pub async fn git_set_remote_url( Ok(()) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_merge( path: String, branch_name: String, @@ -1946,7 +1974,7 @@ pub async fn git_merge( }) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_rebase( path: String, branch_name: String, @@ -1979,7 +2007,7 @@ pub async fn git_rebase( }) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_delete_branch( path: String, branch_name: String, @@ -1999,12 +2027,12 @@ pub async fn git_delete_branch( Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_list_conflicts(path: String) -> Result, AppCommandError> { detect_conflicts(&path).await } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_conflict_file_versions( path: String, file: String, @@ -2047,7 +2075,7 @@ pub async fn git_conflict_file_versions( }) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_resolve_conflict( path: String, file: String, @@ -2075,7 +2103,7 @@ pub async fn git_resolve_conflict( Ok(()) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_abort_operation( path: String, operation: String, @@ -2106,7 +2134,7 @@ pub async fn git_abort_operation( Ok(()) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_continue_operation( path: String, operation: String, @@ -2420,7 +2448,7 @@ impl WatchEventBatch { } } - fn emit(&self, app: &tauri::AppHandle, root_display: &str) { + fn emit(&self, emitter: &EventEmitter, root_display: &str) { if self.is_empty() { return; } @@ -2445,13 +2473,13 @@ impl WatchEventBatch { full_reload: self.overflowed, }; - crate::web::event_bridge::emit_event(app, "folder://file-tree-changed", payload); + crate::web::event_bridge::emit_event(emitter, "folder://file-tree-changed", payload); } } fn run_file_watch_event_loop( event_rx: mpsc::Receiver, - app: tauri::AppHandle, + emitter: EventEmitter, root_display: String, root_canonical: PathBuf, ) { @@ -2484,21 +2512,21 @@ fn run_file_watch_event_loop( }; if should_flush { - batch.emit(&app, &root_display); + batch.emit(&emitter, &root_display); batch.clear(); batch_started_at = None; } } Err(mpsc::RecvTimeoutError::Timeout) => { if !batch.is_empty() { - batch.emit(&app, &root_display); + batch.emit(&emitter, &root_display); batch.clear(); batch_started_at = None; } } Err(mpsc::RecvTimeoutError::Disconnected) => { if !batch.is_empty() { - batch.emit(&app, &root_display); + batch.emit(&emitter, &root_display); } break; } @@ -2543,9 +2571,8 @@ fn validate_new_name(new_name: &str) -> Result<&str, AppCommandError> { Ok(trimmed) } -#[tauri::command] -pub async fn start_file_tree_watch( - app: tauri::AppHandle, +pub(crate) async fn start_file_tree_watch_core( + emitter: EventEmitter, root_path: String, ) -> Result<(), AppCommandError> { let root = PathBuf::from(&root_path); @@ -2568,12 +2595,12 @@ pub async fn start_file_tree_watch( let root_display_for_worker = root_path.clone(); let root_display_for_error = root_path.clone(); let root_canonical_for_worker = root_canonical.clone(); - let app_for_worker = app.clone(); + let emitter_for_worker = emitter; let (event_tx, event_rx) = mpsc::channel::(); let mut worker = Some(std::thread::spawn(move || { run_file_watch_event_loop( event_rx, - app_for_worker, + emitter_for_worker, root_display_for_worker, root_canonical_for_worker, ) @@ -2644,7 +2671,17 @@ pub async fn start_file_tree_watch( Ok(()) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] +pub async fn start_file_tree_watch( + app: tauri::AppHandle, + root_path: String, +) -> Result<(), AppCommandError> { + let emitter = EventEmitter::Tauri(app); + start_file_tree_watch_core(emitter, root_path).await +} + +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn stop_file_tree_watch(root_path: String) -> Result<(), AppCommandError> { let root = PathBuf::from(&root_path); let key = canonicalize_watch_root(&root) @@ -2894,7 +2931,7 @@ where })? } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn get_file_tree( path: String, max_depth: Option, @@ -3030,7 +3067,7 @@ pub async fn get_file_tree( Ok(dir_children.remove(&root).unwrap_or_default()) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn read_file_base64( path: String, max_bytes: Option, @@ -3071,7 +3108,7 @@ pub async fn read_file_base64( .await } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn read_file_preview( root_path: String, path: String, @@ -3101,7 +3138,7 @@ pub async fn read_file_preview( .await } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn read_file_for_edit( root_path: String, path: String, @@ -3142,7 +3179,7 @@ pub async fn read_file_for_edit( .await } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn save_file_content( root_path: String, path: String, @@ -3242,7 +3279,7 @@ fn build_local_copy_file_name(original_name: &str, attempt: usize) -> String { } } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn save_file_copy( root_path: String, path: String, @@ -3333,7 +3370,7 @@ pub async fn save_file_copy( .await } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn rename_file_tree_entry( root_path: String, path: String, @@ -3382,7 +3419,7 @@ pub async fn rename_file_tree_entry( Ok(rel) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn delete_file_tree_entry( root_path: String, path: String, @@ -3412,7 +3449,7 @@ pub async fn delete_file_tree_entry( Ok(()) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn create_file_tree_entry( root_path: String, path: String, @@ -3475,7 +3512,7 @@ pub async fn create_file_tree_entry( Ok(rel) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_log( path: String, limit: Option, @@ -3594,7 +3631,7 @@ pub async fn git_log( }) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn git_commit_branches( path: String, commit: String, diff --git a/src-tauri/src/commands/mcp.rs b/src-tauri/src/commands/mcp.rs index 521ed23..9300a6c 100644 --- a/src-tauri/src/commands/mcp.rs +++ b/src-tauri/src/commands/mcp.rs @@ -124,12 +124,12 @@ pub struct McpMarketplaceServerDetail { pub spec: Value, } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn mcp_scan_local() -> Result, AppCommandError> { scan_local_servers() } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn mcp_list_marketplaces() -> Result, AppCommandError> { Ok(vec![ McpMarketplaceProvider { @@ -146,7 +146,7 @@ pub async fn mcp_list_marketplaces() -> Result, AppC ]) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn mcp_search_marketplace( provider_id: String, query: Option, @@ -164,7 +164,7 @@ pub async fn mcp_search_marketplace( } } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn mcp_get_marketplace_server_detail( provider_id: String, server_id: String, @@ -286,7 +286,7 @@ pub async fn mcp_get_marketplace_server_detail( } } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn mcp_install_from_marketplace( provider_id: String, server_id: String, @@ -334,7 +334,7 @@ pub async fn mcp_install_from_marketplace( }) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn mcp_upsert_local_server( server_id: String, spec: Value, @@ -368,7 +368,7 @@ pub async fn mcp_upsert_local_server( }) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn mcp_set_server_apps( server_id: String, apps: Vec, @@ -391,7 +391,7 @@ pub async fn mcp_set_server_apps( find_local_server(&server_id) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn mcp_remove_server( server_id: String, apps: Option>, diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index c230ac6..566e480 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -7,5 +7,7 @@ pub mod project_boot; pub mod system_settings; pub mod terminal; pub mod version_control; +#[cfg(feature = "tauri-runtime")] pub mod notification; +#[cfg(feature = "tauri-runtime")] pub mod windows; diff --git a/src-tauri/src/commands/notification.rs b/src-tauri/src/commands/notification.rs index 9155e7f..7c4ac8e 100644 --- a/src-tauri/src/commands/notification.rs +++ b/src-tauri/src/commands/notification.rs @@ -1,8 +1,10 @@ +#[cfg(feature = "tauri-runtime")] use tauri::AppHandle; use crate::app_error::AppCommandError; -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn send_notification( #[allow(unused_variables)] app: AppHandle, title: String, diff --git a/src-tauri/src/commands/project_boot.rs b/src-tauri/src/commands/project_boot.rs index d0efda3..d6c62ef 100644 --- a/src-tauri/src/commands/project_boot.rs +++ b/src-tauri/src/commands/project_boot.rs @@ -46,7 +46,7 @@ async fn detect_one(name: &str) -> PackageManagerInfo { } } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn detect_package_manager(name: String) -> PackageManagerInfo { detect_one(&name).await } @@ -55,7 +55,7 @@ pub async fn detect_package_manager(name: String) -> PackageManagerInfo { // Project creation // --------------------------------------------------------------------------- -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn create_shadcn_project( project_name: String, template: String, diff --git a/src-tauri/src/commands/system_settings.rs b/src-tauri/src/commands/system_settings.rs index 00903e2..d0a4b1e 100644 --- a/src-tauri/src/commands/system_settings.rs +++ b/src-tauri/src/commands/system_settings.rs @@ -1,4 +1,5 @@ use sea_orm::DatabaseConnection; +#[cfg(feature = "tauri-runtime")] use tauri::State; use crate::app_error::AppCommandError; @@ -82,14 +83,16 @@ pub(crate) async fn load_system_language_settings( }) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn get_system_proxy_settings( db: State<'_, AppDatabase>, ) -> Result { load_system_proxy_settings(&db.conn).await } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn update_system_proxy_settings( settings: SystemProxySettings, db: State<'_, AppDatabase>, @@ -108,14 +111,16 @@ pub async fn update_system_proxy_settings( Ok(normalized) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn get_system_language_settings( db: State<'_, AppDatabase>, ) -> Result { load_system_language_settings(&db.conn).await } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn update_system_language_settings( settings: SystemLanguageSettings, db: State<'_, AppDatabase>, @@ -130,7 +135,8 @@ pub async fn update_system_language_settings( .await .map_err(AppCommandError::from)?; - crate::web::event_bridge::emit_event(&app, LANGUAGE_SETTINGS_UPDATED_EVENT, settings.clone()); + let emitter = crate::web::event_bridge::EventEmitter::Tauri(app); + crate::web::event_bridge::emit_event(&emitter, LANGUAGE_SETTINGS_UPDATED_EVENT, settings.clone()); Ok(settings) } diff --git a/src-tauri/src/commands/terminal.rs b/src-tauri/src/commands/terminal.rs index e3fd0f3..6de55b8 100644 --- a/src-tauri/src/commands/terminal.rs +++ b/src-tauri/src/commands/terminal.rs @@ -1,12 +1,15 @@ use std::collections::HashMap; +#[cfg(feature = "tauri-runtime")] use tauri::Manager; +#[cfg(feature = "tauri-runtime")] use tauri::State; use crate::git_credential; use crate::terminal::error::TerminalError; use crate::terminal::manager::{SpawnOptions, TerminalManager}; use crate::terminal::types::TerminalInfo; +use crate::web::event_bridge::EventEmitter; /// Build extra env vars for the terminal session. /// @@ -57,7 +60,8 @@ pub(crate) fn prepare_credential_env( Some(env) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn terminal_spawn( working_dir: String, initial_command: Option, @@ -83,11 +87,12 @@ pub async fn terminal_spawn( extra_env, temp_files: vec![], }, - app_handle, + EventEmitter::Tauri(app_handle), ) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub fn terminal_write( terminal_id: String, data: String, @@ -96,7 +101,8 @@ pub fn terminal_write( manager.write(&terminal_id, data.as_bytes()) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub fn terminal_resize( terminal_id: String, cols: u16, @@ -106,7 +112,8 @@ pub fn terminal_resize( manager.resize(&terminal_id, cols, rows) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub fn terminal_kill( terminal_id: String, manager: State<'_, TerminalManager>, @@ -114,10 +121,12 @@ pub fn terminal_kill( manager.kill(&terminal_id) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub fn terminal_list( manager: State<'_, TerminalManager>, app_handle: tauri::AppHandle, ) -> Result, TerminalError> { - Ok(manager.list_with_exit_check(Some(&app_handle))) + let emitter = EventEmitter::Tauri(app_handle); + Ok(manager.list_with_exit_check(Some(&emitter))) } diff --git a/src-tauri/src/commands/version_control.rs b/src-tauri/src/commands/version_control.rs index 8aa0bfe..3496c6d 100644 --- a/src-tauri/src/commands/version_control.rs +++ b/src-tauri/src/commands/version_control.rs @@ -1,4 +1,5 @@ use serde::Deserialize; +#[cfg(feature = "tauri-runtime")] use tauri::State; use crate::app_error::AppCommandError; @@ -102,14 +103,15 @@ pub(crate) async fn detect_git_core( } } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn detect_git( db: State<'_, AppDatabase>, ) -> Result { detect_git_core(&db.conn).await } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn test_git_path(path: String) -> Result { let trimmed = path.trim(); if trimmed.is_empty() { @@ -138,14 +140,16 @@ async fn load_git_settings( } } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn get_git_settings( db: State<'_, AppDatabase>, ) -> Result { load_git_settings(&db.conn).await } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn update_git_settings( settings: GitSettings, db: State<'_, AppDatabase>, @@ -182,14 +186,16 @@ async fn load_github_accounts( } } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn get_github_accounts( db: State<'_, AppDatabase>, ) -> Result { load_github_accounts(&db.conn).await } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn update_github_accounts( settings: GitHubAccountsSettings, db: State<'_, AppDatabase>, @@ -210,7 +216,7 @@ pub async fn update_github_accounts( // Keyring token management // --------------------------------------------------------------------------- -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn save_account_token( account_id: String, token: String, @@ -219,14 +225,14 @@ pub async fn save_account_token( .map_err(|e| AppCommandError::io_error("Failed to save token to keyring").with_detail(e)) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn get_account_token( account_id: String, ) -> Result, AppCommandError> { Ok(crate::keyring_store::get_token(&account_id)) } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn delete_account_token( account_id: String, ) -> Result<(), AppCommandError> { @@ -244,7 +250,7 @@ struct GitHubUserResponse { avatar_url: Option, } -#[tauri::command] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn validate_github_token( server_url: String, token: String, diff --git a/src-tauri/src/commands/windows.rs b/src-tauri/src/commands/windows.rs index e665bc2..36934a5 100644 --- a/src-tauri/src/commands/windows.rs +++ b/src-tauri/src/commands/windows.rs @@ -137,7 +137,8 @@ fn resolve_settings_target(section: Option<&str>, agent_type: Option<&str>) -> S route.to_string() } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn list_open_folders( app: AppHandle, db: tauri::State<'_, AppDatabase>, @@ -165,7 +166,8 @@ pub async fn list_open_folders( Ok(open_folders) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn focus_folder_window(app: AppHandle, folder_id: i32) -> Result<(), AppCommandError> { let windows = app.webview_windows(); for (label, window) in &windows { @@ -186,7 +188,8 @@ pub async fn focus_folder_window(app: AppHandle, folder_id: i32) -> Result<(), A ) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn open_folder_window( app: AppHandle, db: tauri::State<'_, AppDatabase>, @@ -233,7 +236,8 @@ pub async fn open_folder_window( Ok(()) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn open_commit_window( app: AppHandle, window: tauri::WebviewWindow, @@ -294,7 +298,8 @@ pub async fn open_commit_window( Ok(()) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn open_settings_window( app: AppHandle, window: tauri::WebviewWindow, @@ -387,7 +392,8 @@ impl MergeWindowState { } } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn open_merge_window( app: AppHandle, window: tauri::WebviewWindow, @@ -505,8 +511,9 @@ pub async fn cleanup_dangling_merge(app: &AppHandle, merge_window_label: &str) { .output() .await; + let emitter = crate::web::event_bridge::EventEmitter::Tauri(app.clone()); crate::web::event_bridge::emit_event( - app, + &emitter, "folder://merge-aborted", serde_json::json!({ "folder_id": folder_id }), ); @@ -531,7 +538,8 @@ pub fn open_welcome_window(app: &AppHandle) -> Result<(), AppCommandError> { Ok(()) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn open_stash_window( app: AppHandle, db: tauri::State<'_, AppDatabase>, @@ -570,7 +578,8 @@ pub async fn open_stash_window( Ok(()) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn open_push_window( app: AppHandle, db: tauri::State<'_, AppDatabase>, @@ -609,7 +618,8 @@ pub async fn open_push_window( Ok(()) } -#[tauri::command] +#[cfg(feature = "tauri-runtime")] +#[cfg_attr(feature = "tauri-runtime", tauri::command)] pub async fn open_project_boot_window( app: AppHandle, source: Option, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 09d8207..f33bb26 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,7 +1,8 @@ mod acp; mod app_error; +pub mod app_state; mod commands; -mod db; +pub mod db; pub mod git_credential; pub mod keyring_store; mod models; @@ -9,368 +10,367 @@ mod network; mod parsers; mod process; mod terminal; -mod web; +pub mod web; -use std::sync::atomic::{AtomicBool, Ordering}; +#[cfg(feature = "tauri-runtime")] +mod tauri_app { + use std::sync::atomic::{AtomicBool, Ordering}; -use acp::manager::ConnectionManager; -use commands::{ - acp as acp_commands, conversations, folder_commands, folders, mcp as mcp_commands, - notification, project_boot, system_settings, terminal as terminal_commands, version_control, - windows, -}; -use tauri::Manager; -use terminal::manager::TerminalManager; + use crate::acp::manager::ConnectionManager; + use crate::commands::{ + acp as acp_commands, conversations, folder_commands, folders, mcp as mcp_commands, + notification, project_boot, system_settings, terminal as terminal_commands, version_control, + windows, + }; + use crate::terminal::manager::TerminalManager; + use crate::{db, network, process, web}; + use tauri::Manager; -static APP_QUITTING: AtomicBool = AtomicBool::new(false); + static APP_QUITTING: AtomicBool = AtomicBool::new(false); -fn get_folder_id_from_url(window: &tauri::Window) -> Option { - let webview = window.get_webview_window(window.label())?; - let url = webview.url().ok()?; - url.query_pairs() - .find(|(key, _)| key == "id") - .and_then(|(_, value)| value.parse::().ok()) -} - -#[cfg_attr(mobile, tauri::mobile_entry_point)] -pub fn run() { - if let Err(err) = fix_path_env::fix() { - eprintln!("[PATH] fix_path_env failed: {err}"); + fn get_folder_id_from_url(window: &tauri::Window) -> Option { + let webview = window.get_webview_window(window.label())?; + let url = webview.url().ok()?; + url.query_pairs() + .find(|(key, _)| key == "id") + .and_then(|(_, value)| value.parse::().ok()) } - process::ensure_node_in_path(); - tauri::Builder::default() - .plugin(tauri_plugin_window_state::Builder::new().build()) - .plugin(tauri_plugin_opener::init()) - .plugin(tauri_plugin_dialog::init()) - .plugin(tauri_plugin_updater::Builder::new().build()) - .plugin(tauri_plugin_process::init()) - .plugin(tauri_plugin_notification::init()) - .manage(ConnectionManager::new()) - .manage(TerminalManager::new()) - .manage(windows::SettingsWindowState::new()) - .manage(windows::CommitWindowState::new()) - .manage(windows::MergeWindowState::new()) - .manage(web::WebServerState::new()) - .manage(web::event_bridge::WebEventBroadcaster::new()) - .setup(|app| { - let app_data_dir = app.path().app_data_dir()?; - let app_version = env!("CARGO_PKG_VERSION"); - let database = - tauri::async_runtime::block_on(db::init_database(&app_data_dir, app_version)) - .map_err(|e| e.to_string())?; - app.manage(database); + #[cfg_attr(mobile, tauri::mobile_entry_point)] + pub fn run() { + if let Err(err) = fix_path_env::fix() { + eprintln!("[PATH] fix_path_env failed: {err}"); + } + process::ensure_node_in_path(); - // Restore and apply saved system proxy settings before any network operation. - let db = app.state::(); - match tauri::async_runtime::block_on(system_settings::load_system_proxy_settings( - &db.conn, - )) { - Ok(settings) => { - let _ = network::proxy::apply_system_proxy_settings(&settings); - } - Err(err) => { - eprintln!("[Settings] failed to load system proxy settings: {err}"); - } - } + tauri::Builder::default() + .plugin(tauri_plugin_window_state::Builder::new().build()) + .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_updater::Builder::new().build()) + .plugin(tauri_plugin_process::init()) + .plugin(tauri_plugin_notification::init()) + .manage(ConnectionManager::new()) + .manage(TerminalManager::new()) + .manage(windows::SettingsWindowState::new()) + .manage(windows::CommitWindowState::new()) + .manage(windows::MergeWindowState::new()) + .manage(web::WebServerState::new()) + .manage(std::sync::Arc::new(web::event_bridge::WebEventBroadcaster::new())) + .setup(|app| { + let app_data_dir = app.path().app_data_dir()?; + let app_version = env!("CARGO_PKG_VERSION"); + let database = + tauri::async_runtime::block_on(db::init_database(&app_data_dir, app_version)) + .map_err(|e| e.to_string())?; + app.manage(database); - // Restore previously open folders or show welcome - let db = app.state::(); - let open_folders = tauri::async_runtime::block_on( - db::service::folder_service::list_open_folders(&db.conn), - ) - .unwrap_or_default(); - - if open_folders.is_empty() { - let _ = windows::open_welcome_window(app.handle()); - } else { - for entry in &open_folders { - let label = windows::folder_window_label(entry.id); - let url = tauri::WebviewUrl::App(format!("folder?id={}", entry.id).into()); - let builder = tauri::WebviewWindowBuilder::new(app, &label, url) - .title(&entry.name) - .inner_size(1260.0, 860.0) - .min_inner_size(900.0, 600.0); - let _ = windows::apply_platform_window_style(builder).build(); - } - } - - Ok(()) - }) - .on_window_event(|window, event| { - let label = window.label().to_string(); - - if label == "settings" - && matches!( - event, - tauri::WindowEvent::CloseRequested { .. } | tauri::WindowEvent::Destroyed - ) - { - let app = window.app_handle(); - if let Some(state) = app.try_state::() { - windows::restore_windows_after_settings(app, &state); - } - } - - if label.starts_with("commit-") - && matches!( - event, - tauri::WindowEvent::CloseRequested { .. } | tauri::WindowEvent::Destroyed - ) - { - let app = window.app_handle(); - if let Some(state) = app.try_state::() { - windows::restore_window_after_commit(app, &state, &label); - } - } - - if label.starts_with("merge-") - && matches!( - event, - tauri::WindowEvent::CloseRequested { .. } | tauri::WindowEvent::Destroyed - ) - { - let app = window.app_handle(); - if let Some(state) = app.try_state::() { - windows::restore_window_after_merge(app, &state, &label); - } - // Clean up dangling merge state (MERGE_HEAD) if window closed - // without completing or aborting the merge - let app_clone = window.app_handle().clone(); - let label_clone = label.clone(); - tauri::async_runtime::spawn(async move { - windows::cleanup_dangling_merge(&app_clone, &label_clone).await; - }); - } - - if label == "project-boot" - && matches!( - event, - tauri::WindowEvent::CloseRequested { .. } | tauri::WindowEvent::Destroyed - ) - { - let app = window.app_handle(); - if !APP_QUITTING.load(Ordering::Relaxed) { - let has_other = app - .webview_windows() - .keys() - .any(|l| *l != label && *l != "settings"); - if !has_other { - let _ = windows::open_welcome_window(app); + // Restore and apply saved system proxy settings before any network operation. + let db = app.state::(); + match tauri::async_runtime::block_on(system_settings::load_system_proxy_settings( + &db.conn, + )) { + Ok(settings) => { + let _ = network::proxy::apply_system_proxy_settings(&settings); + } + Err(err) => { + eprintln!("[Settings] failed to load system proxy settings: {err}"); } } - } - if let tauri::WindowEvent::CloseRequested { .. } = event { - if label.starts_with("folder-") { + // Restore previously open folders or show welcome + let db = app.state::(); + let open_folders = tauri::async_runtime::block_on( + db::service::folder_service::list_open_folders(&db.conn), + ) + .unwrap_or_default(); + + if open_folders.is_empty() { + let _ = windows::open_welcome_window(app.handle()); + } else { + for entry in &open_folders { + let label = windows::folder_window_label(entry.id); + let url = tauri::WebviewUrl::App(format!("folder?id={}", entry.id).into()); + let builder = tauri::WebviewWindowBuilder::new(app, &label, url) + .title(&entry.name) + .inner_size(1260.0, 860.0) + .min_inner_size(900.0, 600.0); + let _ = windows::apply_platform_window_style(builder).build(); + } + } + + Ok(()) + }) + .on_window_event(|window, event| { + let label = window.label().to_string(); + + if label == "settings" + && matches!( + event, + tauri::WindowEvent::CloseRequested { .. } | tauri::WindowEvent::Destroyed + ) + { let app = window.app_handle(); - if let Some(cm) = app.try_state::() { - let disconnected = - tauri::async_runtime::block_on(cm.disconnect_by_owner_window(&label)); - eprintln!( - "[ACP] folder window closing label={} disconnected_connections={}", - label, disconnected - ); + if let Some(state) = app.try_state::() { + windows::restore_windows_after_settings(app, &state); } + } - // Only mark folder as closed if user is closing individual window, - // not when the entire app is quitting (so folders reopen on next launch) + if label.starts_with("commit-") + && matches!( + event, + tauri::WindowEvent::CloseRequested { .. } | tauri::WindowEvent::Destroyed + ) + { + let app = window.app_handle(); + if let Some(state) = app.try_state::() { + windows::restore_window_after_commit(app, &state, &label); + } + } + + if label.starts_with("merge-") + && matches!( + event, + tauri::WindowEvent::CloseRequested { .. } | tauri::WindowEvent::Destroyed + ) + { + let app = window.app_handle(); + if let Some(state) = app.try_state::() { + windows::restore_window_after_merge(app, &state, &label); + } + let app_clone = window.app_handle().clone(); + let label_clone = label.clone(); + tauri::async_runtime::spawn(async move { + windows::cleanup_dangling_merge(&app_clone, &label_clone).await; + }); + } + + if label == "project-boot" + && matches!( + event, + tauri::WindowEvent::CloseRequested { .. } | tauri::WindowEvent::Destroyed + ) + { + let app = window.app_handle(); if !APP_QUITTING.load(Ordering::Relaxed) { - if let Some(folder_id) = get_folder_id_from_url(window) { - if let Some(db) = app.try_state::() { - let _ = tauri::async_runtime::block_on( - db::service::folder_service::set_folder_open( - &db.conn, folder_id, false, - ), - ); - } + let has_other = app + .webview_windows() + .keys() + .any(|l| *l != label && *l != "settings"); + if !has_other { + let _ = windows::open_welcome_window(app); } } + } - // Kill terminal sessions owned by this folder window. + if let tauri::WindowEvent::CloseRequested { .. } = event { + if label.starts_with("folder-") { + let app = window.app_handle(); + if let Some(cm) = app.try_state::() { + let disconnected = + tauri::async_runtime::block_on(cm.disconnect_by_owner_window(&label)); + eprintln!( + "[ACP] folder window closing label={} disconnected_connections={}", + label, disconnected + ); + } + + if !APP_QUITTING.load(Ordering::Relaxed) { + if let Some(folder_id) = get_folder_id_from_url(window) { + if let Some(db) = app.try_state::() { + let _ = tauri::async_runtime::block_on( + db::service::folder_service::set_folder_open( + &db.conn, folder_id, false, + ), + ); + } + } + } + + if let Some(tm) = app.try_state::() { + let killed = tm.kill_by_owner_window(&label); + eprintln!( + "[TERM] folder window closing label={} killed_terminals={}", + label, killed + ); + } + let has_other_folder = app + .webview_windows() + .keys() + .any(|l| l.starts_with("folder-") && *l != label); + if !has_other_folder && !APP_QUITTING.load(Ordering::Relaxed) { + let _ = windows::open_welcome_window(app); + } + } + } + }) + .invoke_handler(tauri::generate_handler![ + conversations::list_conversations, + conversations::get_conversation, + conversations::list_folder_conversations, + conversations::import_local_conversations, + conversations::get_folder_conversation, + conversations::list_folders, + conversations::get_stats, + conversations::get_sidebar_data, + conversations::create_conversation, + conversations::update_conversation_status, + conversations::update_conversation_title, + conversations::update_conversation_external_id, + conversations::delete_conversation, + folders::load_folder_history, + folders::get_folder, + folders::add_folder_to_history, + folders::set_folder_parent_branch, + folders::remove_folder_from_history, + folders::create_folder_directory, + folders::clone_repository, + folders::get_git_branch, + folders::git_init, + folders::git_pull, + folders::git_start_pull_merge, + folders::git_has_merge_head, + folders::git_fetch, + folders::git_push_info, + folders::git_push, + folders::git_new_branch, + folders::git_worktree_add, + folders::git_checkout, + folders::git_list_branches, + folders::git_stash_push, + folders::git_stash_pop, + folders::git_stash_list, + folders::git_stash_apply, + folders::git_stash_drop, + folders::git_stash_clear, + folders::git_stash_show, + folders::git_status, + folders::git_is_tracked, + folders::git_diff, + folders::git_diff_with_branch, + folders::git_show_diff, + folders::git_show_file, + folders::git_commit, + folders::git_rollback_file, + folders::git_add_files, + folders::git_list_all_branches, + folders::git_list_remotes, + folders::git_fetch_remote, + folders::git_add_remote, + folders::git_remove_remote, + folders::git_set_remote_url, + folders::git_merge, + folders::git_rebase, + folders::git_delete_branch, + folders::git_list_conflicts, + folders::git_conflict_file_versions, + folders::git_resolve_conflict, + folders::git_abort_operation, + folders::git_continue_operation, + folders::save_folder_opened_conversations, + folders::start_file_tree_watch, + folders::stop_file_tree_watch, + folders::get_file_tree, + folders::read_file_base64, + folders::read_file_preview, + folders::read_file_for_edit, + folders::save_file_content, + folders::save_file_copy, + folders::rename_file_tree_entry, + folders::delete_file_tree_entry, + folders::create_file_tree_entry, + folders::git_log, + folders::git_commit_branches, + windows::open_folder_window, + windows::open_commit_window, + windows::open_settings_window, + windows::list_open_folders, + windows::focus_folder_window, + windows::open_merge_window, + windows::open_stash_window, + windows::open_push_window, + windows::open_project_boot_window, + project_boot::detect_package_manager, + project_boot::create_shadcn_project, + system_settings::get_system_proxy_settings, + system_settings::update_system_proxy_settings, + system_settings::get_system_language_settings, + system_settings::update_system_language_settings, + version_control::detect_git, + version_control::test_git_path, + version_control::get_git_settings, + version_control::update_git_settings, + version_control::get_github_accounts, + version_control::validate_github_token, + version_control::update_github_accounts, + version_control::save_account_token, + version_control::get_account_token, + version_control::delete_account_token, + acp_commands::acp_preflight, + acp_commands::acp_connect, + acp_commands::acp_prompt, + acp_commands::acp_set_mode, + acp_commands::acp_set_config_option, + acp_commands::acp_cancel, + acp_commands::acp_fork, + acp_commands::acp_respond_permission, + acp_commands::acp_disconnect, + acp_commands::acp_list_connections, + acp_commands::acp_list_agents, + acp_commands::acp_get_agent_status, + acp_commands::acp_clear_binary_cache, + acp_commands::acp_download_agent_binary, + acp_commands::acp_detect_agent_local_version, + acp_commands::acp_prepare_npx_agent, + acp_commands::acp_uninstall_agent, + acp_commands::acp_update_agent_preferences, + acp_commands::acp_reorder_agents, + acp_commands::acp_list_agent_skills, + acp_commands::acp_read_agent_skill, + acp_commands::acp_save_agent_skill, + acp_commands::acp_delete_agent_skill, + folder_commands::list_folder_commands, + folder_commands::create_folder_command, + folder_commands::update_folder_command, + folder_commands::delete_folder_command, + folder_commands::reorder_folder_commands, + folder_commands::bootstrap_folder_commands_from_package_json, + terminal_commands::terminal_spawn, + terminal_commands::terminal_write, + terminal_commands::terminal_resize, + terminal_commands::terminal_kill, + terminal_commands::terminal_list, + mcp_commands::mcp_scan_local, + mcp_commands::mcp_list_marketplaces, + mcp_commands::mcp_search_marketplace, + mcp_commands::mcp_get_marketplace_server_detail, + mcp_commands::mcp_install_from_marketplace, + mcp_commands::mcp_upsert_local_server, + mcp_commands::mcp_set_server_apps, + mcp_commands::mcp_remove_server, + notification::send_notification, + web::start_web_server, + web::stop_web_server, + web::get_web_server_status, + ]) + .build(tauri::generate_context!()) + .expect("error while building tauri application") + .run(|app, event| { + if let tauri::RunEvent::ExitRequested { .. } = event { + APP_QUITTING.store(true, Ordering::Relaxed); + if let Some(ws) = app.try_state::() { + web::do_stop_web_server(&ws); + } if let Some(tm) = app.try_state::() { - let killed = tm.kill_by_owner_window(&label); - eprintln!( - "[TERM] folder window closing label={} killed_terminals={}", - label, killed - ); + tm.kill_all(); } - let has_other_folder = app - .webview_windows() - .keys() - .any(|l| l.starts_with("folder-") && *l != label); - if !has_other_folder && !APP_QUITTING.load(Ordering::Relaxed) { - let _ = windows::open_welcome_window(app); + if let Some(cm) = app.try_state::() { + tauri::async_runtime::block_on(cm.disconnect_all()); } } - } - }) - .invoke_handler(tauri::generate_handler![ - conversations::list_conversations, - conversations::get_conversation, - conversations::list_folder_conversations, - conversations::import_local_conversations, - conversations::get_folder_conversation, - conversations::list_folders, - conversations::get_stats, - conversations::get_sidebar_data, - conversations::create_conversation, - conversations::update_conversation_status, - conversations::update_conversation_title, - conversations::update_conversation_external_id, - conversations::delete_conversation, - folders::load_folder_history, - folders::get_folder, - folders::add_folder_to_history, - folders::set_folder_parent_branch, - folders::remove_folder_from_history, - folders::create_folder_directory, - folders::clone_repository, - folders::get_git_branch, - folders::git_init, - folders::git_pull, - folders::git_start_pull_merge, - folders::git_has_merge_head, - folders::git_fetch, - folders::git_push_info, - folders::git_push, - folders::git_new_branch, - folders::git_worktree_add, - folders::git_checkout, - folders::git_list_branches, - folders::git_stash_push, - folders::git_stash_pop, - folders::git_stash_list, - folders::git_stash_apply, - folders::git_stash_drop, - folders::git_stash_clear, - folders::git_stash_show, - folders::git_status, - folders::git_is_tracked, - folders::git_diff, - folders::git_diff_with_branch, - folders::git_show_diff, - folders::git_show_file, - folders::git_commit, - folders::git_rollback_file, - folders::git_add_files, - folders::git_list_all_branches, - folders::git_list_remotes, - folders::git_fetch_remote, - folders::git_add_remote, - folders::git_remove_remote, - folders::git_set_remote_url, - folders::git_merge, - folders::git_rebase, - folders::git_delete_branch, - folders::git_list_conflicts, - folders::git_conflict_file_versions, - folders::git_resolve_conflict, - folders::git_abort_operation, - folders::git_continue_operation, - folders::save_folder_opened_conversations, - folders::start_file_tree_watch, - folders::stop_file_tree_watch, - folders::get_file_tree, - folders::read_file_base64, - folders::read_file_preview, - folders::read_file_for_edit, - folders::save_file_content, - folders::save_file_copy, - folders::rename_file_tree_entry, - folders::delete_file_tree_entry, - folders::create_file_tree_entry, - folders::git_log, - folders::git_commit_branches, - windows::open_folder_window, - windows::open_commit_window, - windows::open_settings_window, - windows::list_open_folders, - windows::focus_folder_window, - windows::open_merge_window, - windows::open_stash_window, - windows::open_push_window, - windows::open_project_boot_window, - project_boot::detect_package_manager, - project_boot::create_shadcn_project, - system_settings::get_system_proxy_settings, - system_settings::update_system_proxy_settings, - system_settings::get_system_language_settings, - system_settings::update_system_language_settings, - version_control::detect_git, - version_control::test_git_path, - version_control::get_git_settings, - version_control::update_git_settings, - version_control::get_github_accounts, - version_control::validate_github_token, - version_control::update_github_accounts, - version_control::save_account_token, - version_control::get_account_token, - version_control::delete_account_token, - acp_commands::acp_preflight, - acp_commands::acp_connect, - acp_commands::acp_prompt, - acp_commands::acp_set_mode, - acp_commands::acp_set_config_option, - acp_commands::acp_cancel, - acp_commands::acp_fork, - acp_commands::acp_respond_permission, - acp_commands::acp_disconnect, - acp_commands::acp_list_connections, - acp_commands::acp_list_agents, - acp_commands::acp_get_agent_status, - acp_commands::acp_clear_binary_cache, - acp_commands::acp_download_agent_binary, - acp_commands::acp_detect_agent_local_version, - acp_commands::acp_prepare_npx_agent, - acp_commands::acp_uninstall_agent, - acp_commands::acp_update_agent_preferences, - acp_commands::acp_reorder_agents, - acp_commands::acp_list_agent_skills, - acp_commands::acp_read_agent_skill, - acp_commands::acp_save_agent_skill, - acp_commands::acp_delete_agent_skill, - folder_commands::list_folder_commands, - folder_commands::create_folder_command, - folder_commands::update_folder_command, - folder_commands::delete_folder_command, - folder_commands::reorder_folder_commands, - folder_commands::bootstrap_folder_commands_from_package_json, - terminal_commands::terminal_spawn, - terminal_commands::terminal_write, - terminal_commands::terminal_resize, - terminal_commands::terminal_kill, - terminal_commands::terminal_list, - mcp_commands::mcp_scan_local, - mcp_commands::mcp_list_marketplaces, - mcp_commands::mcp_search_marketplace, - mcp_commands::mcp_get_marketplace_server_detail, - mcp_commands::mcp_install_from_marketplace, - mcp_commands::mcp_upsert_local_server, - mcp_commands::mcp_set_server_apps, - mcp_commands::mcp_remove_server, - notification::send_notification, - web::start_web_server, - web::stop_web_server, - web::get_web_server_status, - ]) - .build(tauri::generate_context!()) - .expect("error while building tauri application") - .run(|app, event| { - if let tauri::RunEvent::ExitRequested { .. } = event { - APP_QUITTING.store(true, Ordering::Relaxed); - // Stop the embedded web server if running. - if let Some(ws) = app.try_state::() { - let _ = tauri::async_runtime::block_on(web::stop_web_server(ws)); - } - // Kill all terminal sessions to prevent orphaned processes. - if let Some(tm) = app.try_state::() { - tm.kill_all(); - } - // Disconnect all ACP agent connections (kills agent process trees). - if let Some(cm) = app.try_state::() { - tauri::async_runtime::block_on(cm.disconnect_all()); - } - } - }); + }); + } } + +#[cfg(feature = "tauri-runtime")] +pub use tauri_app::run; diff --git a/src-tauri/src/terminal/manager.rs b/src-tauri/src/terminal/manager.rs index 4efce53..8ded91a 100644 --- a/src-tauri/src/terminal/manager.rs +++ b/src-tauri/src/terminal/manager.rs @@ -9,6 +9,7 @@ use portable_pty::{native_pty_system, CommandBuilder, MasterPty, PtySize}; use super::error::TerminalError; use super::types::{TerminalEvent, TerminalInfo}; +use crate::web::event_bridge::EventEmitter; struct TerminalInstance { write_tx: mpsc::Sender>, @@ -148,11 +149,18 @@ impl TerminalManager { } } + /// Returns a shallow clone sharing the same underlying terminal map. + pub fn clone_ref(&self) -> Self { + Self { + terminals: self.terminals.clone(), + } + } + #[allow(clippy::too_many_arguments)] pub fn spawn_with_id( &self, opts: SpawnOptions, - app_handle: tauri::AppHandle, + emitter: EventEmitter, ) -> Result { let pty_system = native_pty_system(); @@ -227,7 +235,7 @@ impl TerminalManager { std::thread::Builder::new() .name(format!("pty-reader-{}", &id_for_writer[..8])) .spawn(move || { - read_loop(reader, id_for_reader, &app_handle, &terminals_ref); + read_loop(reader, id_for_reader, &emitter, &terminals_ref); }) .map_err(|e| TerminalError::SpawnFailed(e.to_string()))?; @@ -274,7 +282,7 @@ impl TerminalManager { Ok(()) } - pub fn list_with_exit_check(&self, app_handle: Option<&tauri::AppHandle>) -> Vec { + pub fn list_with_exit_check(&self, emitter: Option<&EventEmitter>) -> Vec { let mut terminals = self.terminals.lock().unwrap(); let mut exited_terminal_ids: Vec = Vec::new(); @@ -308,9 +316,9 @@ impl TerminalManager { drop(terminals); - if let Some(handle) = app_handle { + if let Some(emitter) = emitter { for terminal_id in exited_terminal_ids { - emit_terminal_exit_event(handle, &terminal_id); + emit_terminal_exit_event(emitter, &terminal_id); } } @@ -392,7 +400,7 @@ fn write_loop(mut writer: Box, rx: mpsc::Receiver>) { fn read_loop( mut reader: Box, terminal_id: String, - app_handle: &tauri::AppHandle, + emitter: &EventEmitter, terminals: &Arc>>, ) { let output_event = format!("terminal://output/{}", terminal_id); @@ -407,7 +415,7 @@ fn read_loop( terminal_id: terminal_id.clone(), data, }; - crate::web::event_bridge::emit_event(app_handle, &output_event, event.clone()); + crate::web::event_bridge::emit_event(emitter, &output_event, event.clone()); } Err(_) => break, } @@ -418,14 +426,14 @@ fn read_loop( cleanup_temp_files(&mut instance.temp_files); } - emit_terminal_exit_event(app_handle, &terminal_id); + emit_terminal_exit_event(emitter, &terminal_id); } -fn emit_terminal_exit_event(app_handle: &tauri::AppHandle, terminal_id: &str) { +fn emit_terminal_exit_event(emitter: &EventEmitter, terminal_id: &str) { let exit_event = format!("terminal://exit/{}", terminal_id); let event = TerminalEvent { terminal_id: terminal_id.to_string(), data: String::new(), }; - crate::web::event_bridge::emit_event(app_handle, &exit_event, event.clone()); + crate::web::event_bridge::emit_event(emitter, &exit_event, event.clone()); } diff --git a/src-tauri/src/web/event_bridge.rs b/src-tauri/src/web/event_bridge.rs index 0fa35d8..f6d12a5 100644 --- a/src-tauri/src/web/event_bridge.rs +++ b/src-tauri/src/web/event_bridge.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use serde::Serialize; use tokio::sync::broadcast; @@ -34,15 +36,33 @@ impl WebEventBroadcaster { } } -/// Unified event emission: sends to both Tauri webview and Web clients. +/// Abstraction over event emission targets. +/// In Tauri mode, events go to both webview and WebSocket clients. +/// In standalone server mode, events only go to WebSocket clients. +#[derive(Clone)] +pub enum EventEmitter { + #[cfg(feature = "tauri-runtime")] + Tauri(tauri::AppHandle), + WebOnly(Arc), +} + +/// Unified event emission: sends to both Tauri webview and Web clients (if applicable). pub fn emit_event( - app: &tauri::AppHandle, + emitter: &EventEmitter, event: &str, payload: impl Serialize + Clone, ) { - use tauri::{Emitter, Manager}; - let _ = app.emit(event, payload.clone()); - if let Some(web) = app.try_state::() { - web.send(event, &payload); + match emitter { + #[cfg(feature = "tauri-runtime")] + EventEmitter::Tauri(app) => { + use tauri::{Emitter, Manager}; + let _ = app.emit(event, payload.clone()); + if let Some(web) = app.try_state::>() { + web.send(event, &payload); + } + } + EventEmitter::WebOnly(broadcaster) => { + broadcaster.send(event, &payload); + } } } diff --git a/src-tauri/src/web/handlers/acp.rs b/src-tauri/src/web/handlers/acp.rs index 6d6685c..4d69565 100644 --- a/src-tauri/src/web/handlers/acp.rs +++ b/src-tauri/src/web/handlers/acp.rs @@ -1,10 +1,9 @@ use std::collections::BTreeMap; +use std::sync::Arc; use axum::{extract::Extension, Json}; use serde::Deserialize; -use tauri::Manager; -use crate::acp::manager::ConnectionManager; use crate::acp::preflight::PreflightResult; use crate::acp::registry; use crate::acp::types::{ @@ -12,9 +11,9 @@ use crate::acp::types::{ AgentSkillsListResult, ConnectionInfo, ForkResultInfo, }; use crate::app_error::AppCommandError; +use crate::app_state::AppState; use crate::commands::acp as acp_commands; use crate::db::service::agent_setting_service; -use crate::db::AppDatabase; use crate::models::agent::AgentType; #[derive(Deserialize)] @@ -24,21 +23,21 @@ pub struct AgentTypeParams { } pub async fn acp_get_agent_status( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); - let result = acp_commands::acp_get_agent_status(params.agent_type, db) + let db = &state.db; + let result = acp_commands::acp_get_agent_status_core(params.agent_type, db) .await .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; Ok(Json(result)) } pub async fn acp_list_agents( - Extension(app): Extension, + Extension(state): Extension>, ) -> Result>, AppCommandError> { - let db = app.state::(); - let result = acp_commands::acp_list_agents(db) + let db = &state.db; + let result = acp_commands::acp_list_agents_core(db) .await .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; Ok(Json(result)) @@ -53,11 +52,11 @@ pub struct AcpConnectParams { } pub async fn acp_connect( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); - let manager = app.state::(); + let db = &state.db; + let manager = &state.connection_manager; let meta = registry::get_agent_meta(params.agent_type); let setting = agent_setting_service::get_by_agent_type(&db.conn, params.agent_type) @@ -94,6 +93,7 @@ pub async fn acp_connect( } } + let emitter = state.emitter.clone(); let connection_id = manager .spawn_agent( params.agent_type, @@ -101,7 +101,7 @@ pub async fn acp_connect( params.session_id, runtime_env, "web".to_string(), - app.clone(), + emitter, ) .await .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; @@ -116,10 +116,10 @@ pub struct AcpDisconnectParams { } pub async fn acp_disconnect( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let manager = app.state::(); + let manager = &state.connection_manager; manager .disconnect(¶ms.connection_id) .await @@ -135,10 +135,10 @@ pub struct AcpPromptParams { } pub async fn acp_prompt( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let manager = app.state::(); + let manager = &state.connection_manager; manager .send_prompt(¶ms.connection_id, params.blocks) .await @@ -279,10 +279,10 @@ pub struct AcpSetModeParams { } pub async fn acp_set_mode( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let manager = app.state::(); + let manager = &state.connection_manager; manager .set_mode(¶ms.connection_id, params.mode_id) .await @@ -299,10 +299,10 @@ pub struct AcpSetConfigOptionParams { } pub async fn acp_set_config_option( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let manager = app.state::(); + let manager = &state.connection_manager; manager .set_config_option(¶ms.connection_id, params.config_id, params.value_id) .await @@ -311,10 +311,10 @@ pub async fn acp_set_config_option( } pub async fn acp_cancel( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let manager = app.state::(); + let manager = &state.connection_manager; manager .cancel(¶ms.connection_id) .await @@ -323,10 +323,10 @@ pub async fn acp_cancel( } pub async fn acp_fork( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let manager = app.state::(); + let manager = &state.connection_manager; let result = manager .fork_session(¶ms.connection_id) .await @@ -343,10 +343,10 @@ pub struct AcpRespondPermissionParams { } pub async fn acp_respond_permission( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let manager = app.state::(); + let manager = &state.connection_manager; manager .respond_permission(¶ms.connection_id, ¶ms.request_id, ¶ms.option_id) .await @@ -355,9 +355,9 @@ pub async fn acp_respond_permission( } pub async fn acp_list_connections( - Extension(app): Extension, + Extension(state): Extension>, ) -> Result>, AppCommandError> { - let manager = app.state::(); + let manager = &state.connection_manager; let result = manager.list_connections().await; Ok(Json(result)) } @@ -377,10 +377,11 @@ pub struct AcpUpdateAgentPreferencesParams { } pub async fn acp_update_agent_preferences( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; + let emitter = state.emitter.clone(); acp_commands::acp_update_agent_preferences_core( params.agent_type, params.enabled, @@ -390,7 +391,7 @@ pub async fn acp_update_agent_preferences( params.codex_auth_json, params.codex_config_toml, &db, - &app, + &emitter, ) .await .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; @@ -398,20 +399,21 @@ pub async fn acp_update_agent_preferences( } pub async fn acp_download_agent_binary( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - acp_commands::acp_download_agent_binary_core(params.agent_type, &app) + let emitter = state.emitter.clone(); + acp_commands::acp_download_agent_binary_core(params.agent_type, &emitter) .await .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; Ok(Json(())) } pub async fn acp_detect_agent_local_version( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result>, AppCommandError> { - let db = app.state::(); + let db = &state.db; let result = acp_commands::acp_detect_agent_local_version_core(params.agent_type, &db.conn) .await @@ -427,15 +429,16 @@ pub struct AcpPrepareNpxAgentParams { } pub async fn acp_prepare_npx_agent( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; + let emitter = state.emitter.clone(); let result = acp_commands::acp_prepare_npx_agent_core( params.agent_type, params.registry_version, &db, - &app, + &emitter, ) .await .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; @@ -443,11 +446,12 @@ pub async fn acp_prepare_npx_agent( } pub async fn acp_uninstall_agent( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); - acp_commands::acp_uninstall_agent_core(params.agent_type, &db, &app) + let db = &state.db; + let emitter = state.emitter.clone(); + acp_commands::acp_uninstall_agent_core(params.agent_type, &db, &emitter) .await .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; Ok(Json(())) @@ -460,11 +464,12 @@ pub struct AcpReorderAgentsParams { } pub async fn acp_reorder_agents( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); - acp_commands::acp_reorder_agents_core(¶ms.agent_types, &db, &app) + let db = &state.db; + let emitter = state.emitter.clone(); + acp_commands::acp_reorder_agents_core(¶ms.agent_types, &db, &emitter) .await .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; Ok(Json(())) diff --git a/src-tauri/src/web/handlers/conversations.rs b/src-tauri/src/web/handlers/conversations.rs index a70558c..0e72ed0 100644 --- a/src-tauri/src/web/handlers/conversations.rs +++ b/src-tauri/src/web/handlers/conversations.rs @@ -1,11 +1,12 @@ +use std::sync::Arc; + use axum::{extract::Extension, Json}; use serde::Deserialize; -use tauri::Manager; use crate::app_error::AppCommandError; +use crate::app_state::AppState; use crate::commands::conversations as conv_commands; use crate::db::service::{conversation_service, folder_service, import_service}; -use crate::db::AppDatabase; use crate::models::*; #[derive(Deserialize)] @@ -19,10 +20,10 @@ pub struct ListFolderConversationsParams { } pub async fn list_folder_conversations( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result>, AppCommandError> { - let db = app.state::(); + let db = &state.db; let result = conversation_service::list_by_folder( &db.conn, params.folder_id, @@ -80,10 +81,10 @@ pub struct GetFolderConversationParams { } pub async fn get_folder_conversation( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; let result = conv_commands::get_folder_conversation_core(&db.conn, params.conversation_id).await?; Ok(Json(result)) @@ -111,10 +112,10 @@ pub struct ImportLocalConversationsParams { } pub async fn import_local_conversations( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; let folder = folder_service::get_folder_by_id(&db.conn, params.folder_id) .await .map_err(AppCommandError::from)? @@ -134,10 +135,10 @@ pub struct CreateConversationParams { } pub async fn create_conversation( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; let result = conv_commands::create_conversation_core( &db.conn, params.folder_id, @@ -156,10 +157,10 @@ pub struct UpdateConversationStatusParams { } pub async fn update_conversation_status( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; let status_enum: crate::db::entities::conversation::ConversationStatus = serde_json::from_value(serde_json::Value::String(params.status)).map_err(|e| { AppCommandError::invalid_input("Invalid conversation status").with_detail(e.to_string()) @@ -178,10 +179,10 @@ pub struct UpdateConversationTitleParams { } pub async fn update_conversation_title( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; conversation_service::update_title(&db.conn, params.conversation_id, params.title) .await .map_err(AppCommandError::from)?; @@ -195,10 +196,10 @@ pub struct DeleteConversationParams { } pub async fn delete_conversation( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; conversation_service::soft_delete(&db.conn, params.conversation_id) .await .map_err(AppCommandError::from)?; @@ -213,10 +214,10 @@ pub struct UpdateConversationExternalIdParams { } pub async fn update_conversation_external_id( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; conversation_service::update_external_id(&db.conn, params.conversation_id, params.external_id) .await .map_err(AppCommandError::from)?; diff --git a/src-tauri/src/web/handlers/folder_commands.rs b/src-tauri/src/web/handlers/folder_commands.rs index df375a6..7756bad 100644 --- a/src-tauri/src/web/handlers/folder_commands.rs +++ b/src-tauri/src/web/handlers/folder_commands.rs @@ -1,10 +1,11 @@ +use std::sync::Arc; + use axum::{extract::Extension, Json}; use serde::Deserialize; -use tauri::Manager; use crate::app_error::AppCommandError; +use crate::app_state::AppState; use crate::db::service::folder_command_service; -use crate::db::AppDatabase; use crate::models::*; // --------------------------------------------------------------------------- @@ -52,10 +53,10 @@ pub struct ReorderFolderCommandsParams { // --------------------------------------------------------------------------- pub async fn list_folder_commands( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result>, AppCommandError> { - let db = app.state::(); + let db = &state.db; let result = folder_command_service::list_by_folder(&db.conn, params.folder_id) .await .map_err(AppCommandError::from)?; @@ -63,10 +64,10 @@ pub async fn list_folder_commands( } pub async fn create_folder_command( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; let result = folder_command_service::create( &db.conn, params.folder_id, @@ -79,10 +80,10 @@ pub async fn create_folder_command( } pub async fn update_folder_command( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; let result = folder_command_service::update( &db.conn, params.id, @@ -96,10 +97,10 @@ pub async fn update_folder_command( } pub async fn delete_folder_command( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; folder_command_service::delete(&db.conn, params.id) .await .map_err(AppCommandError::from)?; @@ -107,10 +108,10 @@ pub async fn delete_folder_command( } pub async fn reorder_folder_commands( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; folder_command_service::reorder(&db.conn, params.folder_id, params.ids) .await .map_err(AppCommandError::from)?; @@ -125,10 +126,10 @@ pub struct BootstrapFolderCommandsParams { } pub async fn bootstrap_folder_commands_from_package_json( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result>, AppCommandError> { - let db = app.state::(); + let db = &state.db; let existing = folder_command_service::list_by_folder(&db.conn, params.folder_id) .await diff --git a/src-tauri/src/web/handlers/folders.rs b/src-tauri/src/web/handlers/folders.rs index 2ebe3a5..df11c88 100644 --- a/src-tauri/src/web/handlers/folders.rs +++ b/src-tauri/src/web/handlers/folders.rs @@ -1,11 +1,12 @@ +use std::sync::Arc; + use axum::{extract::Extension, Json}; use serde::{Deserialize, Serialize}; -use tauri::Manager; use crate::app_error::AppCommandError; +use crate::app_state::AppState; use crate::commands::folders as folder_commands; use crate::db::service::folder_service; -use crate::db::AppDatabase; use crate::models::*; #[derive(Deserialize)] @@ -15,9 +16,9 @@ pub struct FolderIdParams { } pub async fn load_folder_history( - Extension(app): Extension, + Extension(state): Extension>, ) -> Result>, AppCommandError> { - let db = app.state::(); + let db = &state.db; let result = folder_service::list_folders(&db.conn) .await .map_err(AppCommandError::from)?; @@ -25,9 +26,9 @@ pub async fn load_folder_history( } pub async fn list_open_folders( - Extension(app): Extension, + Extension(state): Extension>, ) -> Result>, AppCommandError> { - let db = app.state::(); + let db = &state.db; let result = folder_service::list_open_folders(&db.conn) .await .map_err(AppCommandError::from)?; @@ -35,10 +36,10 @@ pub async fn list_open_folders( } pub async fn get_folder( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; let folder = folder_service::get_folder_by_id(&db.conn, params.folder_id) .await .map_err(AppCommandError::from)? @@ -55,10 +56,10 @@ pub struct AddFolderParams { /// Web equivalent of `open_folder_window`: adds the folder to DB and returns its ID. /// The web client then navigates to `/folder?id=N` itself. pub async fn open_folder_window( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; let entry = folder_service::add_folder(&db.conn, ¶ms.path) .await .map_err(AppCommandError::from)?; @@ -66,10 +67,10 @@ pub async fn open_folder_window( } pub async fn close_folder_window( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; folder_service::set_folder_open(&db.conn, params.folder_id, false) .await .map_err(AppCommandError::from)?; @@ -86,10 +87,10 @@ pub struct SaveFolderOpenedConversationsParams { } pub async fn save_folder_opened_conversations( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; folder_service::save_opened_conversations(&db.conn, params.folder_id, params.items) .await .map_err(AppCommandError::from)?; @@ -130,10 +131,10 @@ pub struct RootPathParams { } pub async fn start_file_tree_watch( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - folder_commands::start_file_tree_watch(app, params.root_path).await?; + folder_commands::start_file_tree_watch_core(state.emitter.clone(), params.root_path).await?; Ok(Json(())) } @@ -254,10 +255,10 @@ pub struct SetFolderParentBranchParams { } pub async fn add_folder_to_history( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; let result = folder_service::add_folder(&db.conn, ¶ms.path) .await .map_err(AppCommandError::from)?; @@ -265,20 +266,20 @@ pub async fn add_folder_to_history( } pub async fn set_folder_parent_branch( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; folder_commands::set_folder_parent_branch_core(&db.conn, ¶ms.path, params.parent_branch) .await?; Ok(Json(())) } pub async fn remove_folder_from_history( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; folder_service::remove_folder(&db.conn, ¶ms.path) .await .map_err(AppCommandError::from)?; diff --git a/src-tauri/src/web/handlers/git.rs b/src-tauri/src/web/handlers/git.rs index 92bf6b7..2e848a0 100644 --- a/src-tauri/src/web/handlers/git.rs +++ b/src-tauri/src/web/handlers/git.rs @@ -1,10 +1,11 @@ +use std::sync::Arc; + use axum::{extract::Extension, Json}; use serde::Deserialize; -use tauri::Manager; use crate::app_error::AppCommandError; +use crate::app_state::AppState; use crate::commands::folders as folder_commands; -use crate::db::AppDatabase; use crate::models::GitCredentials; use super::folders::PathParams; @@ -499,15 +500,15 @@ pub struct GitPullParams { } pub async fn git_pull( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; let result = folder_commands::git_pull_core( ¶ms.path, params.credentials.as_ref(), - &db, - &app, + db, + &state.data_dir, ) .await?; Ok(Json(result)) @@ -521,15 +522,15 @@ pub struct GitFetchParams { } pub async fn git_fetch( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; let result = folder_commands::git_fetch_core( ¶ms.path, params.credentials.as_ref(), - &db, - &app, + db, + &state.data_dir, ) .await?; Ok(Json(result)) @@ -545,17 +546,19 @@ pub struct GitPushParams { } pub async fn git_push( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; + let emitter = state.emitter.clone(); let result = folder_commands::git_push_core( - &app, + &state.data_dir, + &emitter, params.folder_id, ¶ms.path, params.remote.as_deref(), params.credentials.as_ref(), - &db, + db, ) .await?; Ok(Json(result)) @@ -571,12 +574,13 @@ pub struct GitCommitParams { } pub async fn git_commit( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; + let emitter = state.emitter.clone(); let result = folder_commands::git_commit_core( - &app, + &emitter, params.folder_id, &db.conn, ¶ms.path, @@ -596,16 +600,16 @@ pub struct GitFetchRemoteParams { } pub async fn git_fetch_remote( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; let result = folder_commands::git_fetch_remote_core( ¶ms.path, ¶ms.name, params.credentials.as_ref(), - &db, - &app, + db, + &state.data_dir, ) .await?; Ok(Json(result)) @@ -620,16 +624,16 @@ pub struct CloneRepositoryParams { } pub async fn clone_repository( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; folder_commands::clone_repository_core( ¶ms.url, ¶ms.target_dir, params.credentials.as_ref(), - &db, - &app, + db, + &state.data_dir, ) .await?; Ok(Json(())) diff --git a/src-tauri/src/web/handlers/system_settings.rs b/src-tauri/src/web/handlers/system_settings.rs index 4645ae1..30d357f 100644 --- a/src-tauri/src/web/handlers/system_settings.rs +++ b/src-tauri/src/web/handlers/system_settings.rs @@ -1,11 +1,12 @@ +use std::sync::Arc; + use axum::{extract::Extension, Json}; use serde::Deserialize; -use tauri::Manager; use crate::app_error::AppCommandError; +use crate::app_state::AppState; use crate::commands::system_settings as settings_commands; use crate::db::service::app_metadata_service; -use crate::db::AppDatabase; use crate::models::*; use crate::network::proxy; @@ -32,17 +33,17 @@ pub struct UpdateLanguageSettingsParams { // --------------------------------------------------------------------------- pub async fn get_system_proxy_settings( - Extension(app): Extension, + Extension(state): Extension>, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; let settings = settings_commands::load_system_proxy_settings(&db.conn).await?; Ok(Json(settings)) } pub async fn get_system_language_settings( - Extension(app): Extension, + Extension(state): Extension>, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; let settings = settings_commands::load_system_language_settings(&db.conn).await?; Ok(Json(settings)) @@ -53,11 +54,11 @@ pub async fn get_system_language_settings( // --------------------------------------------------------------------------- pub async fn update_system_proxy_settings( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { let settings = params.settings; - let db = app.state::(); + let db = &state.db; // TODO: call normalize_proxy_settings once it is made pub(crate) in // commands/system_settings.rs. For now the frontend validates the URL. @@ -79,11 +80,11 @@ pub async fn update_system_proxy_settings( } pub async fn update_system_language_settings( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { let settings = params.settings; - let db = app.state::(); + let db = &state.db; let serialized = serde_json::to_string(&settings).map_err(|e| { AppCommandError::invalid_input("Failed to serialize language settings") @@ -99,7 +100,7 @@ pub async fn update_system_language_settings( .map_err(AppCommandError::from)?; crate::web::event_bridge::emit_event( - &app, + &state.emitter, LANGUAGE_SETTINGS_UPDATED_EVENT, settings.clone(), ); diff --git a/src-tauri/src/web/handlers/terminal.rs b/src-tauri/src/web/handlers/terminal.rs index 683cdcd..3687324 100644 --- a/src-tauri/src/web/handlers/terminal.rs +++ b/src-tauri/src/web/handlers/terminal.rs @@ -1,10 +1,12 @@ +use std::sync::Arc; + use axum::{extract::Extension, Json}; use serde::Deserialize; -use tauri::Manager; use crate::app_error::AppCommandError; +use crate::app_state::AppState; use crate::commands::terminal::prepare_credential_env; -use crate::terminal::manager::{SpawnOptions, TerminalManager}; +use crate::terminal::manager::SpawnOptions; use crate::terminal::types::TerminalInfo; // --------------------------------------------------------------------------- @@ -44,18 +46,13 @@ pub struct TerminalResizeParams { // --------------------------------------------------------------------------- pub async fn terminal_spawn( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let manager = app.state::(); + let manager = &state.terminal_manager; let terminal_id = uuid::Uuid::new_v4().to_string(); - let app_data_dir = app - .path() - .app_data_dir() - .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; - - let extra_env = prepare_credential_env(&app_data_dir); + let extra_env = prepare_credential_env(&state.data_dir); let id = manager .spawn_with_id( @@ -67,7 +64,7 @@ pub async fn terminal_spawn( extra_env, temp_files: vec![], }, - app.clone(), + state.emitter.clone(), ) .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; @@ -75,10 +72,10 @@ pub async fn terminal_spawn( } pub async fn terminal_write( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let manager = app.state::(); + let manager = &state.terminal_manager; manager .write(¶ms.terminal_id, params.data.as_bytes()) .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; @@ -86,10 +83,10 @@ pub async fn terminal_write( } pub async fn terminal_resize( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let manager = app.state::(); + let manager = &state.terminal_manager; manager .resize(¶ms.terminal_id, params.cols, params.rows) .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; @@ -97,10 +94,10 @@ pub async fn terminal_resize( } pub async fn terminal_kill( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let manager = app.state::(); + let manager = &state.terminal_manager; manager .kill(¶ms.terminal_id) .map_err(|e| AppCommandError::task_execution_failed(e.to_string()))?; @@ -108,9 +105,9 @@ pub async fn terminal_kill( } pub async fn terminal_list( - Extension(app): Extension, + Extension(state): Extension>, ) -> Result>, AppCommandError> { - let manager = app.state::(); - let result = manager.list_with_exit_check(Some(&app)); + let manager = &state.terminal_manager; + let result = manager.list_with_exit_check(Some(&state.emitter)); Ok(Json(result)) } diff --git a/src-tauri/src/web/handlers/version_control.rs b/src-tauri/src/web/handlers/version_control.rs index 6151932..6608715 100644 --- a/src-tauri/src/web/handlers/version_control.rs +++ b/src-tauri/src/web/handlers/version_control.rs @@ -1,11 +1,12 @@ +use std::sync::Arc; + use axum::{extract::Extension, Json}; use serde::Deserialize; -use tauri::Manager; use crate::app_error::AppCommandError; +use crate::app_state::AppState; use crate::commands::version_control as vc_commands; use crate::db::service::app_metadata_service; -use crate::db::AppDatabase; use crate::models::*; const GIT_SETTINGS_KEY: &str = "git_settings"; @@ -56,9 +57,9 @@ pub struct UpdateGitHubAccountsParams { // --------------------------------------------------------------------------- pub async fn detect_git( - Extension(app): Extension, + Extension(state): Extension>, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; let result = vc_commands::detect_git_core(&db.conn).await?; Ok(Json(result)) } @@ -75,9 +76,9 @@ pub async fn test_git_path( // --------------------------------------------------------------------------- pub async fn get_git_settings( - Extension(app): Extension, + Extension(state): Extension>, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; let raw = app_metadata_service::get_value(&db.conn, GIT_SETTINGS_KEY) .await .map_err(AppCommandError::from)?; @@ -93,11 +94,11 @@ pub async fn get_git_settings( } pub async fn update_git_settings( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { let settings = params.settings; - let db = app.state::(); + let db = &state.db; let serialized = serde_json::to_string(&settings).map_err(|e| { AppCommandError::invalid_input("Failed to serialize git settings") .with_detail(e.to_string()) @@ -115,9 +116,9 @@ pub async fn update_git_settings( // --------------------------------------------------------------------------- pub async fn get_github_accounts( - Extension(app): Extension, + Extension(state): Extension>, ) -> Result, AppCommandError> { - let db = app.state::(); + let db = &state.db; let raw = app_metadata_service::get_value(&db.conn, GITHUB_ACCOUNTS_KEY) .await .map_err(AppCommandError::from)?; @@ -135,11 +136,11 @@ pub async fn get_github_accounts( } pub async fn update_github_accounts( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { let settings = params.settings; - let db = app.state::(); + let db = &state.db; let serialized = serde_json::to_string(&settings).map_err(|e| { AppCommandError::invalid_input("Failed to serialize GitHub accounts") .with_detail(e.to_string()) diff --git a/src-tauri/src/web/handlers/web_server.rs b/src-tauri/src/web/handlers/web_server.rs index 3d35c6c..98301bb 100644 --- a/src-tauri/src/web/handlers/web_server.rs +++ b/src-tauri/src/web/handlers/web_server.rs @@ -1,16 +1,16 @@ +use std::sync::Arc; + use axum::{extract::Extension, Json}; use serde::{Deserialize, Serialize}; -use tauri::Manager; use crate::app_error::AppCommandError; -use crate::web::{do_get_web_server_status, do_start_web_server, do_stop_web_server}; -use crate::web::{WebServerInfo, WebServerState}; +use crate::app_state::AppState; +use crate::web::{do_get_web_server_status, do_stop_web_server, WebServerInfo}; pub async fn get_web_server_status( - Extension(app): Extension, + Extension(state): Extension>, ) -> Result>, AppCommandError> { - let state = app.state::(); - Ok(Json(do_get_web_server_status(&state))) + Ok(Json(do_get_web_server_status(&state.web_server_state))) } #[derive(Deserialize)] @@ -21,19 +21,27 @@ pub struct StartWebServerParams { } pub async fn start_web_server( - Extension(app): Extension, + Extension(state): Extension>, Json(params): Json, ) -> Result, AppCommandError> { - let state = app.state::(); - let info = do_start_web_server(&app, &state, params.port, params.host).await?; - Ok(Json(info)) + // In web mode, the server is already running (this handler itself is served by it). + // This endpoint is mainly useful in Tauri mode. Return current status as a noop. + let ws = &state.web_server_state; + if ws.running.load(std::sync::atomic::Ordering::Relaxed) { + if let Some(info) = do_get_web_server_status(ws) { + return Ok(Json(info)); + } + } + Err(AppCommandError::new( + crate::app_error::AppErrorCode::InvalidInput, + "Cannot start web server from within web mode", + )) } pub async fn stop_web_server( - Extension(app): Extension, + Extension(state): Extension>, ) -> Result, AppCommandError> { - let state = app.state::(); - do_stop_web_server(&state); + do_stop_web_server(&state.web_server_state); Ok(Json(())) } diff --git a/src-tauri/src/web/mod.rs b/src-tauri/src/web/mod.rs index 6a93ec1..de30669 100644 --- a/src-tauri/src/web/mod.rs +++ b/src-tauri/src/web/mod.rs @@ -7,15 +7,15 @@ pub mod ws; use std::net::SocketAddr; use std::path::PathBuf; use std::sync::atomic::{AtomicU16, Ordering}; -use std::sync::Mutex; +use std::sync::{Arc, Mutex}; use serde::Serialize; -use tauri::Manager; use crate::app_error::{AppCommandError, AppErrorCode}; +use crate::app_state::AppState; pub struct WebServerState { - handle: Mutex>>, + handle: Mutex>>, port: AtomicU16, token: Mutex, running: std::sync::atomic::AtomicBool, @@ -40,11 +40,13 @@ pub struct WebServerInfo { pub addresses: Vec, } -pub(crate) fn generate_random_token() -> String { +pub fn generate_random_token() -> String { uuid::Uuid::new_v4().to_string().replace('-', "") } -pub(crate) fn find_static_dir(app: &tauri::AppHandle) -> PathBuf { +#[cfg(feature = "tauri-runtime")] +pub(crate) fn find_static_dir_tauri(app: &tauri::AppHandle) -> PathBuf { + use tauri::Manager; // 1. Production: bundle.resources copies out/ → web/ inside the resource directory. let resource = app.path().resource_dir().ok(); if let Some(ref dir) = resource { @@ -60,8 +62,11 @@ pub(crate) fn find_static_dir(app: &tauri::AppHandle) -> PathBuf { } } - // 2. Dev mode: "out/" is at the project root, which is one level above src-tauri/. - // The Cargo manifest dir at compile time gives us the src-tauri/ path. + find_static_dir_fallback() +} + +pub(crate) fn find_static_dir_fallback() -> PathBuf { + // Dev mode: "out/" is at the project root, which is one level above src-tauri/. let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let project_out = manifest_dir.parent().map(|p| p.join("out")); if let Some(ref out) = project_out { @@ -71,7 +76,7 @@ pub(crate) fn find_static_dir(app: &tauri::AppHandle) -> PathBuf { } } - // 3. Fallback: current working directory / out + // Fallback: current working directory / out let cwd_out = std::env::current_dir() .map(|d| d.join("out")) .unwrap_or_else(|_| PathBuf::from("out")); @@ -82,7 +87,26 @@ pub(crate) fn find_static_dir(app: &tauri::AppHandle) -> PathBuf { cwd_out } -pub(crate) fn get_local_addresses(port: u16) -> Vec { +pub fn find_static_dir_standalone(explicit: Option<&str>) -> PathBuf { + if let Some(dir) = explicit { + let p = PathBuf::from(dir); + if p.join("index.html").exists() { + eprintln!("[WEB] Serving static files from CODEG_STATIC_DIR: {}", p.display()); + return p; + } + } + + // Try ./web/ + let web = PathBuf::from("web"); + if web.join("index.html").exists() { + eprintln!("[WEB] Serving static files from ./web/: {}", web.display()); + return web; + } + + find_static_dir_fallback() +} + +pub fn get_local_addresses(port: u16) -> Vec { let mut addrs = vec![format!("http://127.0.0.1:{}", port)]; // Try to get LAN IPs if let Ok(interfaces) = std::net::UdpSocket::bind("0.0.0.0:0") { @@ -98,13 +122,14 @@ pub(crate) fn get_local_addresses(port: u16) -> Vec { // ── Core logic (shared by Tauri commands and web handlers) ── -pub(crate) async fn do_start_web_server( - app: &tauri::AppHandle, - state: &WebServerState, +pub(crate) async fn do_start_web_server_with_state( + app_state: Arc, + static_dir: PathBuf, port: Option, host: Option, ) -> Result { - if state.running.load(Ordering::Relaxed) { + let ws = &app_state.web_server_state; + if ws.running.load(Ordering::Relaxed) { return Err(AppCommandError::new( AppErrorCode::AlreadyExists, "Web server is already running", @@ -115,8 +140,7 @@ pub(crate) async fn do_start_web_server( let host = host.unwrap_or_else(|| "0.0.0.0".to_string()); let token = generate_random_token(); - let static_dir = find_static_dir(app); - let router = router::build_router(app.clone(), token.clone(), static_dir); + let router = router::build_router(app_state.clone(), token.clone(), static_dir); let addr: SocketAddr = format!("{}:{}", host, port) .parse() @@ -131,16 +155,16 @@ pub(crate) async fn do_start_web_server( let actual_port = listener.local_addr().map(|a| a.port()).unwrap_or(port); eprintln!("[WEB] Starting web server on {}", addr); - let handle = tauri::async_runtime::spawn(async move { + let handle = tokio::spawn(async move { if let Err(e) = axum::serve(listener, router).await { eprintln!("[WEB] Server error: {}", e); } }); - *state.handle.lock().unwrap() = Some(handle); - state.port.store(actual_port, Ordering::Relaxed); - *state.token.lock().unwrap() = token.clone(); - state.running.store(true, Ordering::Relaxed); + *ws.handle.lock().unwrap() = Some(handle); + ws.port.store(actual_port, Ordering::Relaxed); + *ws.token.lock().unwrap() = token.clone(); + ws.running.store(true, Ordering::Relaxed); let addresses = get_local_addresses(actual_port); Ok(WebServerInfo { @@ -176,6 +200,7 @@ pub(crate) fn do_get_web_server_status(state: &WebServerState) -> Option, host: Option, ) -> Result { - do_start_web_server(&app, &state, port, host).await + // In Tauri mode, we still need to start via the legacy path because + // the full AppState isn't easily available from tauri::State here. + // The embedded web server uses Tauri's resource directory for static files. + use tauri::Manager; + + let ws = &*state; + if ws.running.load(Ordering::Relaxed) { + return Err(AppCommandError::new( + AppErrorCode::AlreadyExists, + "Web server is already running", + )); + } + + let port_val = port.unwrap_or(3080); + let host_val = host.unwrap_or_else(|| "0.0.0.0".to_string()); + let token = generate_random_token(); + + let static_dir = find_static_dir_tauri(&app); + + // Build AppState for the router + let app_state = Arc::new(AppState { + db: crate::db::AppDatabase { + conn: app.state::().conn.clone(), + }, + connection_manager: (*app.state::()).clone_ref(), + terminal_manager: (*app.state::()).clone_ref(), + event_broadcaster: app.state::>().inner().clone(), + emitter: crate::web::event_bridge::EventEmitter::Tauri(app.clone()), + data_dir: app.path().app_data_dir().unwrap_or_default(), + web_server_state: WebServerState::new(), // placeholder; not used by handlers + }); + + let router = router::build_router(app_state, token.clone(), static_dir); + + let addr: SocketAddr = format!("{}:{}", host_val, port_val) + .parse() + .map_err(|e: std::net::AddrParseError| { + AppCommandError::invalid_input("Invalid host/port").with_detail(e.to_string()) + })?; + + let listener = tokio::net::TcpListener::bind(addr).await.map_err(|e| { + AppCommandError::io_error("Failed to bind address").with_detail(e.to_string()) + })?; + + let actual_port = listener.local_addr().map(|a| a.port()).unwrap_or(port_val); + eprintln!("[WEB] Starting web server on {}", addr); + + let handle = tokio::spawn(async move { + if let Err(e) = axum::serve(listener, router).await { + eprintln!("[WEB] Server error: {}", e); + } + }); + + *ws.handle.lock().unwrap() = Some(handle); + ws.port.store(actual_port, Ordering::Relaxed); + *ws.token.lock().unwrap() = token.clone(); + ws.running.store(true, Ordering::Relaxed); + + let addresses = get_local_addresses(actual_port); + Ok(WebServerInfo { + port: actual_port, + token, + addresses, + }) } +#[cfg(feature = "tauri-runtime")] #[tauri::command] pub async fn stop_web_server( state: tauri::State<'_, WebServerState>, @@ -194,6 +283,7 @@ pub async fn stop_web_server( Ok(()) } +#[cfg(feature = "tauri-runtime")] #[tauri::command] pub async fn get_web_server_status( state: tauri::State<'_, WebServerState>, diff --git a/src-tauri/src/web/router.rs b/src-tauri/src/web/router.rs index ea50fd1..4cafcdb 100644 --- a/src-tauri/src/web/router.rs +++ b/src-tauri/src/web/router.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use axum::{ extract::Extension, http::{StatusCode, Uri}, @@ -10,8 +12,9 @@ use tower_http::cors::{Any, CorsLayer}; use tower_http::services::{ServeDir, ServeFile}; use super::{auth, handlers, ws}; +use crate::app_state::AppState; -pub fn build_router(app: tauri::AppHandle, token: String, static_dir: std::path::PathBuf) -> Router { +pub fn build_router(state: Arc, token: String, static_dir: std::path::PathBuf) -> Router { let cors = CorsLayer::new() .allow_origin(Any) .allow_methods(Any) @@ -234,7 +237,7 @@ pub fn build_router(app: tauri::AppHandle, token: String, static_dir: std::path: .fallback_service(fallback) .layer(html_rewrite) .layer(cors) - .layer(Extension(app)) + .layer(Extension(state)) } async fn health_check() -> impl IntoResponse { diff --git a/src-tauri/src/web/ws.rs b/src-tauri/src/web/ws.rs index 817953e..8ab487e 100644 --- a/src-tauri/src/web/ws.rs +++ b/src-tauri/src/web/ws.rs @@ -1,22 +1,22 @@ +use std::sync::Arc; + use axum::{ extract::{Extension, WebSocketUpgrade}, response::IntoResponse, }; use axum::extract::ws::{Message, WebSocket}; -use tauri::Manager; -use super::event_bridge::WebEventBroadcaster; +use crate::app_state::AppState; pub async fn ws_handler( ws: WebSocketUpgrade, - Extension(app): Extension, + Extension(state): Extension>, ) -> impl IntoResponse { - ws.on_upgrade(|socket| handle_ws_connection(socket, app)) + ws.on_upgrade(|socket| handle_ws_connection(socket, state)) } -async fn handle_ws_connection(mut socket: WebSocket, app: tauri::AppHandle) { - let broadcaster = app.state::(); - let mut rx = broadcaster.subscribe(); +async fn handle_ws_connection(mut socket: WebSocket, state: Arc) { + let mut rx = state.event_broadcaster.subscribe(); loop { tokio::select! {