支持无GUI的Server运行模式
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<WebEventBroadcaster>)`
|
||||
- **`web/router.rs`** — Axum 路由,接受 `Arc<AppState>`
|
||||
- **`web/handlers/`** — 146 个 HTTP API 端点,全部使用 `Extension<Arc<AppState>>`
|
||||
|
||||
### 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`)
|
||||
|
||||
## 代码风格
|
||||
|
||||
|
||||
@@ -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<WebEventBroadcaster>)`
|
||||
- **`web/router.rs`** — Axum 路由,接受 `Arc<AppState>`
|
||||
- **`web/handlers/`** — 146 个 HTTP API 端点,全部使用 `Extension<Arc<AppState>>`
|
||||
|
||||
### 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`)
|
||||
|
||||
## 代码风格
|
||||
|
||||
|
||||
+37
@@ -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"]
|
||||
@@ -4,6 +4,7 @@
|
||||
[](./LICENSE)
|
||||
[](https://tauri.app/)
|
||||
[](https://nextjs.org/)
|
||||
[](./Dockerfile)
|
||||
|
||||
<p>
|
||||
<strong>English</strong> |
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
+71
-12
@@ -4,6 +4,7 @@
|
||||
[](../../LICENSE)
|
||||
[](https://tauri.app/)
|
||||
[](https://nextjs.org/)
|
||||
[](../../Dockerfile)
|
||||
|
||||
<p>
|
||||
<a href="../../README.md">English</a> |
|
||||
@@ -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
|
||||
- محلي أولاً بشكل افتراضي للتحليل والتخزين وعمليات المشروع
|
||||
- الوصول إلى الشبكة يحدث فقط عند الإجراءات التي يبدأها المستخدم
|
||||
- دعم بروكسي النظام لبيئات المؤسسات
|
||||
- وضع خدمة الويب يستخدم مصادقة قائمة على الرموز
|
||||
|
||||
## الترخيص
|
||||
|
||||
|
||||
+60
-12
@@ -4,6 +4,7 @@
|
||||
[](../../LICENSE)
|
||||
[](https://tauri.app/)
|
||||
[](https://nextjs.org/)
|
||||
[](../../Dockerfile)
|
||||
|
||||
<p>
|
||||
<a href="../../README.md">English</a> |
|
||||
@@ -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
|
||||
|
||||
|
||||
+60
-12
@@ -4,6 +4,7 @@
|
||||
[](../../LICENSE)
|
||||
[](https://tauri.app/)
|
||||
[](https://nextjs.org/)
|
||||
[](../../Dockerfile)
|
||||
|
||||
<p>
|
||||
<a href="../../README.md">English</a> |
|
||||
@@ -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
|
||||
|
||||
|
||||
+60
-12
@@ -4,6 +4,7 @@
|
||||
[](../../LICENSE)
|
||||
[](https://tauri.app/)
|
||||
[](https://nextjs.org/)
|
||||
[](../../Dockerfile)
|
||||
|
||||
<p>
|
||||
<a href="../../README.md">English</a> |
|
||||
@@ -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
|
||||
|
||||
|
||||
+71
-12
@@ -4,6 +4,7 @@
|
||||
[](../../LICENSE)
|
||||
[](https://tauri.app/)
|
||||
[](https://nextjs.org/)
|
||||
[](../../Dockerfile)
|
||||
|
||||
<p>
|
||||
<a href="../../README.md">English</a> |
|
||||
@@ -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 サービスモードではトークンベースの認証を使用
|
||||
|
||||
## ライセンス
|
||||
|
||||
|
||||
+71
-12
@@ -4,6 +4,7 @@
|
||||
[](../../LICENSE)
|
||||
[](https://tauri.app/)
|
||||
[](https://nextjs.org/)
|
||||
[](../../Dockerfile)
|
||||
|
||||
<p>
|
||||
<a href="../../README.md">English</a> |
|
||||
@@ -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
|
||||
- 파싱, 저장, 프로젝트 작업은 기본적으로 로컬 우선
|
||||
- 네트워크 접근은 사용자가 명시적으로 작업을 실행할 때만 발생
|
||||
- 엔터프라이즈 환경을 위한 시스템 프록시 지원
|
||||
- 웹 서비스 모드에서는 토큰 기반 인증 사용
|
||||
|
||||
## 라이선스
|
||||
|
||||
|
||||
+71
-12
@@ -4,6 +4,7 @@
|
||||
[](../../LICENSE)
|
||||
[](https://tauri.app/)
|
||||
[](https://nextjs.org/)
|
||||
[](../../Dockerfile)
|
||||
|
||||
<p>
|
||||
<a href="../../README.md">English</a> |
|
||||
@@ -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
|
||||
|
||||
|
||||
+71
-12
@@ -4,6 +4,7 @@
|
||||
[](../../LICENSE)
|
||||
[](https://tauri.app/)
|
||||
[](https://nextjs.org/)
|
||||
[](../../Dockerfile)
|
||||
|
||||
<p>
|
||||
<a href="../../README.md">English</a> |
|
||||
@@ -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 服务模式使用基于令牌的身份认证
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
+71
-12
@@ -4,6 +4,7 @@
|
||||
[](../../LICENSE)
|
||||
[](https://tauri.app/)
|
||||
[](https://nextjs.org/)
|
||||
[](../../Dockerfile)
|
||||
|
||||
<p>
|
||||
<a href="../../README.md">English</a> |
|
||||
@@ -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 服務模式使用基於令牌的身份認證
|
||||
|
||||
## 授權
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
+34
-11
@@ -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"
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
tauri_build::build();
|
||||
}
|
||||
|
||||
@@ -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<String, String>,
|
||||
connection_id: &str,
|
||||
app_handle: &tauri::AppHandle,
|
||||
emitter: &EventEmitter,
|
||||
) -> Result<AcpAgent, AcpError> {
|
||||
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<String>,
|
||||
runtime_env: BTreeMap<String, String>,
|
||||
owner_window_label: String,
|
||||
app_handle: tauri::AppHandle,
|
||||
emitter: EventEmitter,
|
||||
) -> Result<AgentConnection, AcpError> {
|
||||
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::<ConnectionCommand>(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<SessionModeState>,
|
||||
) {
|
||||
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<SessionConfigOption>,
|
||||
) {
|
||||
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<Vec<SessionConfigOption>>,
|
||||
) {
|
||||
// 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<String>,
|
||||
session_id: Option<String>,
|
||||
mut cmd_rx: mpsc::Receiver<ConnectionCommand>,
|
||||
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<RequestPermissionResponse>,
|
||||
_cx: ConnectionTo<Agent>| {
|
||||
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<T: sacp::JsonRpcResponse>(
|
||||
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<Agent>,
|
||||
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<String, TrackedTerminalToolCall>,
|
||||
) {
|
||||
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<Option<ForkExitInfo>, sacp::Error>,
|
||||
conn_id: &str,
|
||||
handle: &tauri::AppHandle,
|
||||
emitter: &EventEmitter,
|
||||
perms: &PendingPermissions,
|
||||
cmd_rx: &mut mpsc::Receiver<ConnectionCommand>,
|
||||
terminal_runtime: Arc<TerminalRuntime>,
|
||||
@@ -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<ConnectionCommand>,
|
||||
terminal_runtime: Arc<TerminalRuntime>,
|
||||
@@ -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<PlanEntryInfo> {
|
||||
/// 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<AvailableCommandInfo> = 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(),
|
||||
|
||||
@@ -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<Mutex<HashMap<String, AgentConnection>>>,
|
||||
@@ -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<String>,
|
||||
runtime_env: BTreeMap<String, String>,
|
||||
owner_window_label: String,
|
||||
app_handle: tauri::AppHandle,
|
||||
emitter: EventEmitter,
|
||||
) -> Result<String, AcpError> {
|
||||
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?;
|
||||
|
||||
|
||||
@@ -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<WebEventBroadcaster>,
|
||||
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()
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
@@ -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<AgentType>,
|
||||
) {
|
||||
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<bool>,
|
||||
@@ -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<String>,
|
||||
@@ -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<PromptInputBlock>,
|
||||
@@ -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<Vec<ConnectionInfo>, 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<crate::acp::types::AcpAgentStatus, AcpError> {
|
||||
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<crate::acp::types::AcpAgentStatus, AcpError> {
|
||||
acp_get_agent_status_core(agent_type, &db).await
|
||||
}
|
||||
|
||||
pub(crate) async fn acp_list_agents_core(
|
||||
db: &AppDatabase,
|
||||
) -> Result<Vec<AcpAgentInfo>, 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<Vec<AcpAgentInfo>, 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<String>,
|
||||
codex_config_toml: Option<String>,
|
||||
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<String>,
|
||||
db: &AppDatabase,
|
||||
app: &tauri::AppHandle,
|
||||
emitter: &EventEmitter,
|
||||
) -> Result<String, AcpError> {
|
||||
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<String>,
|
||||
db: State<'_, AppDatabase>,
|
||||
app: tauri::AppHandle,
|
||||
) -> Result<String, AcpError> {
|
||||
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<AgentType>,
|
||||
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<String>,
|
||||
@@ -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,
|
||||
|
||||
@@ -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<AgentType>,
|
||||
search: Option<String>,
|
||||
@@ -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<Vec<FolderInfo>, AppCommandError> {
|
||||
tokio::task::spawn_blocking(move || -> Result<Vec<FolderInfo>, AppCommandError> {
|
||||
let all_conversations = list_conversations_sync(None, None, None, None);
|
||||
@@ -170,7 +171,7 @@ pub async fn list_folders() -> Result<Vec<FolderInfo>, AppCommandError> {
|
||||
})?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn get_stats() -> Result<AgentStats, AppCommandError> {
|
||||
tokio::task::spawn_blocking(move || -> Result<AgentStats, AppCommandError> {
|
||||
let all_conversations = list_conversations_sync(None, None, None, None);
|
||||
@@ -183,7 +184,7 @@ pub async fn get_stats() -> Result<AgentStats, AppCommandError> {
|
||||
})?
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn get_sidebar_data() -> Result<SidebarData, AppCommandError> {
|
||||
tokio::task::spawn_blocking(move || -> Result<SidebarData, AppCommandError> {
|
||||
let all_conversations = list_conversations_sync(None, None, None, None);
|
||||
@@ -231,7 +232,8 @@ fn compute_folders(all_conversations: &[ConversationSummary]) -> Vec<FolderInfo>
|
||||
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<String> {
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
+163
-126
@@ -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<Vec<FolderHistoryEntry>, 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<Option<String>, 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<Option<String>, 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<GitPullResult, AppCommandError> {
|
||||
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<GitCredentials>,
|
||||
db: tauri::State<'_, AppDatabase>,
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> Result<GitPullResult, AppCommandError> {
|
||||
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<String>,
|
||||
@@ -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<bool, AppCommandError> {
|
||||
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<String, AppCommandError> {
|
||||
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<GitCredentials>,
|
||||
db: tauri::State<'_, AppDatabase>,
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> Result<String, AppCommandError> {
|
||||
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<GitPushInfo, AppCommandError> {
|
||||
// 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<GitPushInfo, AppCommandError>
|
||||
}
|
||||
|
||||
pub(crate) async fn git_push_core(
|
||||
app: &tauri::AppHandle,
|
||||
data_dir: &std::path::Path,
|
||||
emitter: &EventEmitter,
|
||||
folder_id: Option<i32>,
|
||||
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::<i32>().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<Vec<String>, 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<Vec<String>, AppCommandEr
|
||||
Ok(branches)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn git_stash_push(
|
||||
path: String,
|
||||
message: Option<String>,
|
||||
@@ -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<String>,
|
||||
@@ -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<Vec<GitStashEntry>, 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<Vec<GitStashEntry>, 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<String, AppCommandError> {
|
||||
let output = crate::process::tokio_command("git")
|
||||
.args(["stash", "clear"])
|
||||
@@ -1313,7 +1335,7 @@ pub async fn git_stash_clear(path: String) -> Result<String, AppCommandError> {
|
||||
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<bool>,
|
||||
@@ -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<bool, AppCommandError> {
|
||||
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<bool, AppComma
|
||||
Ok(output.status.success())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn git_diff(path: String, file: Option<String>) -> Result<String, AppCommandError> {
|
||||
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<String>) -> Result<String, AppC
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn git_diff_with_branch(
|
||||
path: String,
|
||||
branch: String,
|
||||
@@ -1470,7 +1492,7 @@ pub async fn git_diff_with_branch(
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn git_show_diff(
|
||||
path: String,
|
||||
commit: String,
|
||||
@@ -1502,7 +1524,7 @@ pub async fn git_show_diff(
|
||||
Ok(String::from_utf8_lossy(&output.stdout).to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn git_show_file(
|
||||
path: String,
|
||||
file: String,
|
||||
@@ -1534,7 +1556,7 @@ pub async fn git_show_file(
|
||||
}
|
||||
|
||||
pub(crate) async fn git_commit_core(
|
||||
app: &tauri::AppHandle,
|
||||
emitter: &EventEmitter,
|
||||
folder_id: Option<i32>,
|
||||
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::<i32>().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<String>) -> Result<(), AppCommandError> {
|
||||
if files.is_empty() {
|
||||
return Ok(());
|
||||
@@ -1701,7 +1725,7 @@ pub async fn git_add_files(path: String, files: Vec<String>) -> Result<(), AppCo
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn git_list_all_branches(path: String) -> Result<GitBranchList, AppCommandError> {
|
||||
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<GitBranchList, AppCom
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn git_list_remotes(path: String) -> Result<Vec<GitRemote>, 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<String, AppCommandError> {
|
||||
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<String, AppCommandError> {
|
||||
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<Vec<String>, 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<notify::Event>,
|
||||
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::<notify::Event>();
|
||||
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<usize>,
|
||||
@@ -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<usize>,
|
||||
@@ -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<u32>,
|
||||
@@ -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,
|
||||
|
||||
@@ -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<Vec<LocalMcpServer>, AppCommandError> {
|
||||
scan_local_servers()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn mcp_list_marketplaces() -> Result<Vec<McpMarketplaceProvider>, AppCommandError> {
|
||||
Ok(vec![
|
||||
McpMarketplaceProvider {
|
||||
@@ -146,7 +146,7 @@ pub async fn mcp_list_marketplaces() -> Result<Vec<McpMarketplaceProvider>, AppC
|
||||
])
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn mcp_search_marketplace(
|
||||
provider_id: String,
|
||||
query: Option<String>,
|
||||
@@ -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<McpAppType>,
|
||||
@@ -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<Vec<McpAppType>>,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<SystemProxySettings, AppCommandError> {
|
||||
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<SystemLanguageSettings, AppCommandError> {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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<String>,
|
||||
@@ -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<Vec<TerminalInfo>, TerminalError> {
|
||||
Ok(manager.list_with_exit_check(Some(&app_handle)))
|
||||
let emitter = EventEmitter::Tauri(app_handle);
|
||||
Ok(manager.list_with_exit_check(Some(&emitter)))
|
||||
}
|
||||
|
||||
@@ -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<GitDetectResult, AppCommandError> {
|
||||
detect_git_core(&db.conn).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn test_git_path(path: String) -> Result<GitDetectResult, AppCommandError> {
|
||||
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<GitSettings, AppCommandError> {
|
||||
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<GitHubAccountsSettings, AppCommandError> {
|
||||
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<Option<String>, 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<String>,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[cfg_attr(feature = "tauri-runtime", tauri::command)]
|
||||
pub async fn validate_github_token(
|
||||
server_url: String,
|
||||
token: String,
|
||||
|
||||
@@ -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<String>,
|
||||
|
||||
+343
-343
@@ -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<i32> {
|
||||
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::<i32>().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<i32> {
|
||||
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::<i32>().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::<db::AppDatabase>();
|
||||
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::<db::AppDatabase>();
|
||||
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::SettingsWindowState>() {
|
||||
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::CommitWindowState>() {
|
||||
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::MergeWindowState>() {
|
||||
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::<db::AppDatabase>();
|
||||
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::<db::AppDatabase>();
|
||||
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::<ConnectionManager>() {
|
||||
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::SettingsWindowState>() {
|
||||
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::CommitWindowState>() {
|
||||
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::MergeWindowState>() {
|
||||
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::<db::AppDatabase>() {
|
||||
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::<ConnectionManager>() {
|
||||
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::<db::AppDatabase>() {
|
||||
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::<TerminalManager>() {
|
||||
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::WebServerState>() {
|
||||
web::do_stop_web_server(&ws);
|
||||
}
|
||||
if let Some(tm) = app.try_state::<TerminalManager>() {
|
||||
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::<ConnectionManager>() {
|
||||
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::<web::WebServerState>() {
|
||||
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::<TerminalManager>() {
|
||||
tm.kill_all();
|
||||
}
|
||||
// Disconnect all ACP agent connections (kills agent process trees).
|
||||
if let Some(cm) = app.try_state::<ConnectionManager>() {
|
||||
tauri::async_runtime::block_on(cm.disconnect_all());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
pub use tauri_app::run;
|
||||
|
||||
@@ -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<Vec<u8>>,
|
||||
@@ -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<String, TerminalError> {
|
||||
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<TerminalInfo> {
|
||||
pub fn list_with_exit_check(&self, emitter: Option<&EventEmitter>) -> Vec<TerminalInfo> {
|
||||
let mut terminals = self.terminals.lock().unwrap();
|
||||
let mut exited_terminal_ids: Vec<String> = 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<dyn Write + Send>, rx: mpsc::Receiver<Vec<u8>>) {
|
||||
fn read_loop(
|
||||
mut reader: Box<dyn Read + Send>,
|
||||
terminal_id: String,
|
||||
app_handle: &tauri::AppHandle,
|
||||
emitter: &EventEmitter,
|
||||
terminals: &Arc<Mutex<HashMap<String, TerminalInstance>>>,
|
||||
) {
|
||||
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());
|
||||
}
|
||||
|
||||
@@ -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<WebEventBroadcaster>),
|
||||
}
|
||||
|
||||
/// 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::<WebEventBroadcaster>() {
|
||||
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::<Arc<WebEventBroadcaster>>() {
|
||||
web.send(event, &payload);
|
||||
}
|
||||
}
|
||||
EventEmitter::WebOnly(broadcaster) => {
|
||||
broadcaster.send(event, &payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<AgentTypeParams>,
|
||||
) -> Result<Json<AcpAgentStatus>, AppCommandError> {
|
||||
let db = app.state::<crate::db::AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<Vec<AcpAgentInfo>>, AppCommandError> {
|
||||
let db = app.state::<crate::db::AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<AcpConnectParams>,
|
||||
) -> Result<Json<String>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
let manager = app.state::<ConnectionManager>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<AcpDisconnectParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let manager = app.state::<ConnectionManager>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<AcpPromptParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let manager = app.state::<ConnectionManager>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<AcpSetModeParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let manager = app.state::<ConnectionManager>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<AcpSetConfigOptionParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let manager = app.state::<ConnectionManager>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<AcpConnectionIdParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let manager = app.state::<ConnectionManager>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<AcpConnectionIdParams>,
|
||||
) -> Result<Json<ForkResultInfo>, AppCommandError> {
|
||||
let manager = app.state::<ConnectionManager>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<AcpRespondPermissionParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let manager = app.state::<ConnectionManager>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<Vec<ConnectionInfo>>, AppCommandError> {
|
||||
let manager = app.state::<ConnectionManager>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<AcpUpdateAgentPreferencesParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<AgentTypeParams>,
|
||||
) -> Result<Json<()>, 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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<AgentTypeParams>,
|
||||
) -> Result<Json<Option<String>>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<AcpPrepareNpxAgentParams>,
|
||||
) -> Result<Json<String>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<AgentTypeParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<AcpReorderAgentsParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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(()))
|
||||
|
||||
@@ -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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<ListFolderConversationsParams>,
|
||||
) -> Result<Json<Vec<DbConversationSummary>>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<GetFolderConversationParams>,
|
||||
) -> Result<Json<DbConversationDetail>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<ImportLocalConversationsParams>,
|
||||
) -> Result<Json<ImportResult>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<CreateConversationParams>,
|
||||
) -> Result<Json<i32>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<UpdateConversationStatusParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<UpdateConversationTitleParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<DeleteConversationParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<UpdateConversationExternalIdParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
let db = &state.db;
|
||||
conversation_service::update_external_id(&db.conn, params.conversation_id, params.external_id)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
|
||||
@@ -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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<FolderIdParams>,
|
||||
) -> Result<Json<Vec<FolderCommandInfo>>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<CreateFolderCommandParams>,
|
||||
) -> Result<Json<FolderCommandInfo>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<UpdateFolderCommandParams>,
|
||||
) -> Result<Json<FolderCommandInfo>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<DeleteFolderCommandParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<ReorderFolderCommandsParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<BootstrapFolderCommandsParams>,
|
||||
) -> Result<Json<Vec<FolderCommandInfo>>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
let db = &state.db;
|
||||
|
||||
let existing = folder_command_service::list_by_folder(&db.conn, params.folder_id)
|
||||
.await
|
||||
|
||||
@@ -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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<Vec<FolderHistoryEntry>>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<Vec<FolderHistoryEntry>>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<FolderIdParams>,
|
||||
) -> Result<Json<FolderDetail>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<AddFolderParams>,
|
||||
) -> Result<Json<FolderHistoryEntry>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<FolderIdParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<SaveFolderOpenedConversationsParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<RootPathParams>,
|
||||
) -> Result<Json<()>, 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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<AddFolderParams>,
|
||||
) -> Result<Json<FolderHistoryEntry>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<SetFolderParentBranchParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<AddFolderParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
let db = &state.db;
|
||||
folder_service::remove_folder(&db.conn, ¶ms.path)
|
||||
.await
|
||||
.map_err(AppCommandError::from)?;
|
||||
|
||||
@@ -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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<GitPullParams>,
|
||||
) -> Result<Json<folder_commands::GitPullResult>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<GitFetchParams>,
|
||||
) -> Result<Json<String>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<GitPushParams>,
|
||||
) -> Result<Json<folder_commands::GitPushResult>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<GitCommitParams>,
|
||||
) -> Result<Json<folder_commands::GitCommitResult>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<GitFetchRemoteParams>,
|
||||
) -> Result<Json<String>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<CloneRepositoryParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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(()))
|
||||
|
||||
@@ -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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<SystemProxySettings>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<SystemLanguageSettings>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<UpdateProxySettingsParams>,
|
||||
) -> Result<Json<SystemProxySettings>, AppCommandError> {
|
||||
let settings = params.settings;
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<UpdateLanguageSettingsParams>,
|
||||
) -> Result<Json<SystemLanguageSettings>, AppCommandError> {
|
||||
let settings = params.settings;
|
||||
let db = app.state::<AppDatabase>();
|
||||
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(),
|
||||
);
|
||||
|
||||
@@ -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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<TerminalSpawnParams>,
|
||||
) -> Result<Json<String>, AppCommandError> {
|
||||
let manager = app.state::<TerminalManager>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<TerminalWriteParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let manager = app.state::<TerminalManager>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<TerminalResizeParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let manager = app.state::<TerminalManager>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<TerminalIdParams>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let manager = app.state::<TerminalManager>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<Vec<TerminalInfo>>, AppCommandError> {
|
||||
let manager = app.state::<TerminalManager>();
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<GitDetectResult>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<GitSettings>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<UpdateGitSettingsParams>,
|
||||
) -> Result<Json<GitSettings>, AppCommandError> {
|
||||
let settings = params.settings;
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<GitHubAccountsSettings>, AppCommandError> {
|
||||
let db = app.state::<AppDatabase>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<UpdateGitHubAccountsParams>,
|
||||
) -> Result<Json<GitHubAccountsSettings>, AppCommandError> {
|
||||
let settings = params.settings;
|
||||
let db = app.state::<AppDatabase>();
|
||||
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())
|
||||
|
||||
@@ -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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<Option<WebServerInfo>>, AppCommandError> {
|
||||
let state = app.state::<WebServerState>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
Json(params): Json<StartWebServerParams>,
|
||||
) -> Result<Json<WebServerInfo>, AppCommandError> {
|
||||
let state = app.state::<WebServerState>();
|
||||
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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<()>, AppCommandError> {
|
||||
let state = app.state::<WebServerState>();
|
||||
do_stop_web_server(&state);
|
||||
do_stop_web_server(&state.web_server_state);
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
|
||||
+111
-21
@@ -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<Option<tauri::async_runtime::JoinHandle<()>>>,
|
||||
handle: Mutex<Option<tokio::task::JoinHandle<()>>>,
|
||||
port: AtomicU16,
|
||||
token: Mutex<String>,
|
||||
running: std::sync::atomic::AtomicBool,
|
||||
@@ -40,11 +40,13 @@ pub struct WebServerInfo {
|
||||
pub addresses: Vec<String>,
|
||||
}
|
||||
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
|
||||
// ── 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<AppState>,
|
||||
static_dir: PathBuf,
|
||||
port: Option<u16>,
|
||||
host: Option<String>,
|
||||
) -> Result<WebServerInfo, AppCommandError> {
|
||||
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<WebServ
|
||||
|
||||
// ── Tauri commands (thin wrappers) ──
|
||||
|
||||
#[cfg(feature = "tauri-runtime")]
|
||||
#[tauri::command]
|
||||
pub async fn start_web_server(
|
||||
app: tauri::AppHandle,
|
||||
@@ -183,9 +208,73 @@ pub async fn start_web_server(
|
||||
port: Option<u16>,
|
||||
host: Option<String>,
|
||||
) -> Result<WebServerInfo, AppCommandError> {
|
||||
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::<crate::db::AppDatabase>().conn.clone(),
|
||||
},
|
||||
connection_manager: (*app.state::<crate::acp::manager::ConnectionManager>()).clone_ref(),
|
||||
terminal_manager: (*app.state::<crate::terminal::manager::TerminalManager>()).clone_ref(),
|
||||
event_broadcaster: app.state::<Arc<crate::web::event_bridge::WebEventBroadcaster>>().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>,
|
||||
|
||||
@@ -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<AppState>, 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 {
|
||||
|
||||
@@ -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<tauri::AppHandle>,
|
||||
Extension(state): Extension<Arc<AppState>>,
|
||||
) -> 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::<WebEventBroadcaster>();
|
||||
let mut rx = broadcaster.subscribe();
|
||||
async fn handle_ws_connection(mut socket: WebSocket, state: Arc<AppState>) {
|
||||
let mut rx = state.event_broadcaster.subscribe();
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
|
||||
Reference in New Issue
Block a user