Compare commits

...
Author SHA1 Message Date
leookun 2ff74bc6a8 feat: webfetch 2026-08-30 11:54:32 +08:00
leookun 66ac94d5b1 feat: implement web cache for persisting and serving fetched content
- Introduced a new `WebCache` module to manage web content caching.
- Added functionality to store fetched content and serve it via a dedicated route.
- Integrated web cache into the search module for improved content retrieval.
- Implemented database migration management with detailed diagnostics for better error handling during startup.
- Updated the SQLite store to utilize the new migration system for enhanced database management.
2026-08-30 04:29:39 +08:00
leookun fd7998e43c feat: enhance desktop application startup diagnostics and logging
- Introduced a new `startup` module to capture and manage startup diagnostics.
- Implemented logging functionality with daily rotation and error reporting for application startup failures.
- Updated the main application run function to return an exit code based on startup success or failure.
- Added new dependencies for logging and diagnostics in `Cargo.toml`.
- Increased request timeout in the control service for improved stability.
- Expanded maximum search results in the federation module for better search capabilities.
2026-08-30 03:19:01 +08:00
leookun 44e2d8057a feat: initialize server structure and database schema
- Added initial server setup with Cargo.toml defining dependencies and project structure.
- Created build.rs for generating protobuf bindings and validating wire contracts.
- Established database schema with initial migration files for conversations, messages, and runs.
- Introduced tools and prompts for Cursor functionality, enhancing user interaction capabilities.
2026-08-30 01:33:39 +08:00
leookun d200b3791d new plan 2026-08-29 22:05:04 +08:00
leookun 279e6bb07c refactor: align project directory architecture 2026-08-29 20:45:51 +08:00
leookun 88f258f184 feat: enhance Makefile for Windows and non-Windows desktop builds
- Added conditional logic in the Makefile to support Tauri signing key generation for Windows environments.
- Updated the build-desktop target to ensure proper signing key handling across different operating systems.
2026-08-28 23:36:27 +08:00
leokun c983124288 Merge pull request #374 from zwenooo/codex/fix-cursor-reasoning-parameter
fix: align Cursor reasoning parameter metadata
2026-08-28 23:22:06 +08:00
leokun 6447fd998a Merge pull request #367 from jiah0231/fix/runtime-unsupported-tool-recovery
修复:运行中遇到已移除/未知工具时避免 Agent 中断Fix/runtime unsupported tool recovery
2026-08-28 23:20:32 +08:00
leookun 5cc2401ce2 feat: add first valid response timing and related metrics
- Introduced `first_valid_response_ms` and `ttfr_ms` to track the timing of the first valid response in LLM calls.
- Updated relevant interfaces and components to display and utilize the new metrics, including CallDetails, CallTable, and LatencyChart.
- Enhanced the database schema to accommodate the new timing fields.
- Implemented logic in the service layer to record the first valid response during model interactions.
2026-08-28 22:56:39 +08:00
leokun 5a0bc2e0e9 fix: persist each provider retry as its own llm_calls row
Retry attempts now finish the failed call, start a new call id, and record the next request so call history shows intermediate failures.
2026-08-28 21:09:45 +08:00
leokun 6a3570a95a refactor: implement retry mechanism for API calls in Anthropic, OpenAI Chat, and OpenAI Responses providers
- Replaced direct API call handling with a retry mechanism using `send_with_retry`.
- Improved error handling and response management for better reliability in network requests.
2026-08-28 20:37:52 +08:00
leokun 921bcfd247 feat: implement automatic call refresh on CallsPage
- Added a refreshCalls function to appStore to fetch updated call data.
- Integrated useEffect in CallsPage to periodically refresh calls every 2 seconds and on visibility change.
2026-08-28 19:46:37 +08:00
leokun e27b989002 Fix compaction projection and MCP image truncation 2026-08-28 18:54:27 +08:00
leokun e874d68b79 docs: update README and documentation to clarify model configuration and restart requirements
- Enhanced instructions for restarting Cursor after upgrades or initial model configuration to ensure proper functionality.
- Updated references from "Troubleshooting" to "Frequently Asked Questions" for better alignment with user needs.
- Improved clarity in the installation and usage steps across multiple language versions.
2026-08-28 16:12:06 +08:00
zwen befb18d9c3 fix: align Cursor reasoning parameter metadata 2026-08-28 15:24:12 +08:00
leokun d0033a768b Merge pull request #369 from leookun/fix/stream-lifecycle-reliability
fix: repair stream lifecycle reliability
2026-08-28 13:02:16 +08:00
jiah0231 2c32dad271 test: cover AwaitShell emitted during active run 2026-08-28 12:21:45 +08:00
jiah0231 85241a0413 docs: describe AwaitShell as runtime unsupported tool 2026-08-28 12:21:34 +08:00
jiah0231 edc28de86b 修复:兼容已移除工具,避免旧会话 Resume 中断
兼容 v0.1.5-beta.1 删除 AwaitShell 后的旧会话 Resume,并将未知/已移除工具降级为模型可见失败结果,避免整个 Agent Run 被 Protocol Error 直接终止。
2026-08-28 12:12:50 +08:00
619 changed files with 47071 additions and 62162 deletions
+2 -2
View File
@@ -206,7 +206,7 @@ jobs:
env:
VERSION: ${{ needs.prepare.outputs.version }}
run: |
node scripts/release/generate-legacy-update.mjs \
node .github/scripts/generate-legacy-update.mjs \
--version "${VERSION}" \
--repository "${GITHUB_REPOSITORY}" \
--assets-dir legacy-update \
@@ -231,7 +231,7 @@ jobs:
-H "Accept: application/octet-stream" \
"repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" \
> tauri-update/latest.json
node scripts/release/normalize-tauri-update.mjs \
node .github/scripts/normalize-tauri-update.mjs \
--manifest tauri-update/latest.json \
--release tauri-update/release.json \
--repository "${GITHUB_REPOSITORY}" \
+16
View File
@@ -7,3 +7,19 @@
- Prefer established, well-maintained libraries when they reduce overall complexity or improve reliability. Do not reimplement common functionality without a clear reason.
- Lean on the dependencies already in the project before writing your own implementation or adding packages. Do not assume a library lacks a capability without checking its documentation and types.
- Make architectural decisions for the long term. Do not accept a stopgap that only works for now and is meant to be replaced later.
## Communication
- Lead with the conclusion. Keep responses direct and omit filler, repeated context, generic explanations, and narration of obvious steps.
- Match the level requested by the user: discuss architecture as architecture, behavior as behavior, and code details only when they materially support the answer.
- For architecture, refactoring, and module-boundary discussions, communicate primarily with annotated directory trees and ASCII architecture/data-flow diagrams instead of long prose.
- In directory trees, annotate every relevant directory and file with its single responsibility. When discussing code size, include line counts in the comments:
- use measured line counts for existing code;
- use clearly marked approximate targets such as `≈300 lines` for proposed code;
- state whether tests, generated code, and blank lines are excluded.
- Show the complete main execution path, including inputs, ownership boundaries, runtime loops, persistence, outputs, and extension points. Make the direction of data and control flow explicit.
- Clearly separate the current structure from the proposed structure. Explicitly list modules that move, merge, split, or are deleted.
- Use the user's vocabulary consistently. Do not introduce new domain terms when existing plain-language terms are sufficient; when an implementation name must be mentioned, distinguish it from the architecture concept.
- For stateful or concurrent behavior, show timing, ownership, state boundaries, and before/during/after behavior explicitly. Distinguish continuation within the same lifecycle from creation of a new lifecycle.
- Prefer compact trees, flow diagrams, state matrices, and mappings when they communicate the structure more clearly than paragraphs.
- Tie recommendations to the actual repository structure and code. Do not present a speculative target architecture as if it already exists.
Generated
+114
View File
@@ -97,6 +97,28 @@ dependencies = [
"x11rb",
]
[[package]]
name = "ashpd"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39"
dependencies = [
"async-fs",
"async-net",
"enumflags2",
"futures-channel",
"futures-util",
"rand 0.9.5",
"raw-window-handle",
"serde",
"serde_repr",
"url",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"zbus",
]
[[package]]
name = "asn1-rs"
version = "0.7.2"
@@ -186,6 +208,17 @@ dependencies = [
"slab",
]
[[package]]
name = "async-fs"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5"
dependencies = [
"async-lock",
"blocking",
"futures-lite",
]
[[package]]
name = "async-io"
version = "2.6.0"
@@ -215,6 +248,17 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "async-net"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7"
dependencies = [
"async-io",
"blocking",
"futures-lite",
]
[[package]]
name = "async-process"
version = "2.5.0"
@@ -1132,6 +1176,7 @@ version = "0.1.5-beta.1"
dependencies = [
"axum",
"cursor-server",
"rfd",
"serde",
"serde_json",
"tauri",
@@ -1145,6 +1190,7 @@ dependencies = [
"tokio",
"tokio-util",
"tracing",
"tracing-appender",
"tracing-subscriber",
"url",
]
@@ -1520,6 +1566,15 @@ dependencies = [
"syn 3.0.3",
]
[[package]]
name = "dlib"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a"
dependencies = [
"libloading",
]
[[package]]
name = "dlopen2"
version = "0.8.2"
@@ -4555,6 +4610,12 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "pollster"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
[[package]]
name = "portable-atomic"
version = "1.15.0"
@@ -5196,6 +5257,30 @@ dependencies = [
"web-sys",
]
[[package]]
name = "rfd"
version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed"
dependencies = [
"ashpd",
"block2",
"dispatch2",
"js-sys",
"log",
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
"pollster",
"raw-window-handle",
"urlencoding",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"windows-sys 0.59.0",
]
[[package]]
name = "ring"
version = "0.17.14"
@@ -6289,6 +6374,12 @@ dependencies = [
"serde_json",
]
[[package]]
name = "symlink"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a"
[[package]]
name = "syn"
version = "1.0.109"
@@ -7262,6 +7353,19 @@ dependencies = [
"tracing-core",
]
[[package]]
name = "tracing-appender"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c"
dependencies = [
"crossbeam-channel",
"symlink",
"thiserror 2.0.20",
"time",
"tracing-subscriber",
]
[[package]]
name = "tracing-attributes"
version = "0.1.31"
@@ -7636,6 +7740,12 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "urlencoding"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "urlpattern"
version = "0.3.0"
@@ -7852,6 +7962,7 @@ dependencies = [
"cc",
"downcast-rs",
"rustix",
"scoped-tls",
"smallvec",
"wayland-sys",
]
@@ -7910,6 +8021,8 @@ version = "0.31.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be"
dependencies = [
"dlib",
"log",
"pkg-config",
]
@@ -9028,6 +9141,7 @@ dependencies = [
"endi",
"enumflags2",
"serde",
"url",
"winnow 1.0.4",
"zcheapstr",
"zvariant_derive",
+1 -1
View File
@@ -3,7 +3,7 @@ members = [
"server",
"apps/desktop/src-tauri",
"crates/semble-core",
"benchmarks/semble",
"support/benchmarks/semble",
]
resolver = "2"
+1 -1
View File
@@ -20,7 +20,7 @@ COPY apps/desktop/src-tauri/src/ apps/desktop/src-tauri/src/
COPY apps/desktop/src-tauri/capabilities/ apps/desktop/src-tauri/capabilities/
COPY apps/desktop/src-tauri/icons/ apps/desktop/src-tauri/icons/
COPY apps/desktop/src-tauri/tauri.conf.json apps/desktop/src-tauri/tauri.conf.json
COPY scripts/cursor-proto/proto/ scripts/cursor-proto/proto/
COPY protocols/cursor/ protocols/cursor/
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/local/cargo/git \
--mount=type=cache,target=/src/target \
+8
View File
@@ -30,6 +30,13 @@ build-docs:
build-server:
cargo build --release --package cursor-server --bin cursor-server
ifeq ($(OS),Windows_NT)
$(LOCAL_TAURI_SIGNING_KEY):
@powershell -NoProfile -Command "New-Item -ItemType Directory -Force -Path '$(dir $@)' | Out-Null; & '$(CURDIR)/apps/desktop/node_modules/.bin/tauri.cmd' signer generate --ci --write-keys '$@'"
build-desktop: $(LOCAL_TAURI_SIGNING_KEY)
@node -e "const { spawnSync } = require('node:child_process'); const result = spawnSync(process.execPath, ['node_modules/@tauri-apps/cli/tauri.js', 'build', '--bundles', 'nsis'], { cwd: 'apps/desktop', stdio: 'inherit', env: { ...process.env, TAURI_SIGNING_PRIVATE_KEY: process.argv[1], TAURI_SIGNING_PRIVATE_KEY_PASSWORD: '' } }); process.exit(result.status ?? 1)" "$(LOCAL_TAURI_SIGNING_KEY)"
else
$(LOCAL_TAURI_SIGNING_KEY):
@install -d -m 700 "$(dir $@)"
@apps/desktop/node_modules/.bin/tauri signer generate --ci --write-keys "$@" >/dev/null
@@ -37,6 +44,7 @@ $(LOCAL_TAURI_SIGNING_KEY):
build-desktop: $(LOCAL_TAURI_SIGNING_KEY)
TAURI_SIGNING_PRIVATE_KEY="$(LOCAL_TAURI_SIGNING_KEY)" TAURI_SIGNING_PRIVATE_KEY_PASSWORD="" npm --prefix apps/desktop run tauri:build
endif
build-docker:
docker build --tag cursor-byok:local .
+29 -7
View File
@@ -43,12 +43,12 @@ cursor-byok 是一个开源的本地模型网关。它在你的设备上运行
2. 启动 cursor-byok,打开 **Cursor 配置**,按提示初始化本地 CA(证书颁发机构)。
3. 在模型设置中添加模型,填写服务地址、API Key 和模型名称,然后保存并运行 **测试**。
4. 确认测试通过后,保持 cursor-byok 运行。
5. 打开或重启 Cursor,在模型列表中选择已配置的模型,开始使用 Agent。
5. **首次升级 Cursor 或首次配置模型后,完全退出并重新启动 Cursor,然后新开一个对话**。在模型列表中选择已配置的模型,开始使用 Agent。
完整的安装步骤、配置说明和故障处理,请参阅[中文使用指南](https://docs.leokun.cn/zh/docs)。
完整的安装步骤、配置说明和常见问题,请参阅[中文使用指南](https://docs.leokun.cn/zh/docs)。
> [!TIP]
> 首次完成配置后,建议完全退出并重新启动 Cursor,再新开一个对话。使用自定义模型时,请在模型列表中手动选择该模型,不要选择 **Auto**。
> 首次升级 Cursor 或首次完成配置后,必须完全退出并重新启动 Cursor,再新开一个对话。配置前已经打开的对话不会加载新连接;使用自定义模型时,请在模型列表中手动选择该模型,不要选择 **Auto**。
## 模型配置
@@ -120,13 +120,35 @@ API Key、模型配置和应用设置保存在本机。模型请求仍会发送
```text
cursor-byok/
├── apps/
│ ├── desktop/ # React、Vite、Tauri 桌面应用
│ ├── desktop/
│ │ ├── src/
│ │ │ ├── features/ # 首页、模型、调用记录与设置
│ │ │ ├── shell/ # 窗口框架、页面布局与广告外壳
│ │ │ ├── shared/ # UI、虚拟列表、状态、API 与平台能力
│ │ │ ├── i18n/ # 本地化运行时与语言目录
│ │ │ └── styles/ # 全局主题与排版令牌
│ │ └── src-tauri/ # Tauri 桌面生命周期
│ └── docs/ # Next.js 与 Fumadocs 中文/英文文档站
├── server/ # 本地服务、Cursor 请求处理和模型转发
├── server/
│ ├── src/
│ │ ├── cursor/ # Cursor 协议适配
│ │ ├── run/ # 通用 Agent Runtime 与内部 Port
│ │ ├── provider/ # 模型供应商适配
│ │ ├── model/ # 聚合领域模型
│ │ ├── store/ # SQLite Repository
│ │ ├── control/ # 管理面 API
│ │ ├── harness/ # Cursor 本地集成
│ │ └── search/ # Web 与 Semble 搜索接入
│ ├── prompt/
│ └── migrations/
├── crates/
│ └── semble-core/ # 本地代码索引与搜索核心库
├── cursor-proto/ # Cursor 协议定义
├── benchmarks/ # 代码搜索和索引基准测试
├── protocols/
│ └── cursor/ # Cursor 协议定义的唯一来源
├── support/ # 仓库辅助设施
│ ├── cursor-protocol-extractor/ # Cursor 协议提取与 Go 代码生成工具
│ ├── cursor-capture/ # Cursor 协议抓取和调试工具
│ └── benchmarks/ # 代码搜索和索引基准测试
├── images/ # README 展示图片
├── Cargo.toml # Rust 工作区配置
└── Makefile # 常用开发、检查和构建命令
+3 -2
View File
@@ -45,9 +45,10 @@ You can connect OpenAI- and Anthropic-compatible services, customize endpoints,
1. Download the latest build for your platform from [GitHub Releases](https://github.com/leookun/cursor-byok/releases/latest).
2. Launch cursor-byok, open **Model Settings**, and enter the endpoint, API key, and model ID.
3. Test the model configuration. Once it passes, return to the dashboard and start the service.
4. Open Cursor, select the configured model, and start using Agent.
4. Test the model configuration. Once it passes, return to the dashboard and start the service.
5. After upgrading Cursor or configuring a model for the first time, quit Cursor completely and restart it, then start a new conversation and select the configured model.
For complete installation steps, system configuration, and troubleshooting, see the [User Guide](https://docs.leokun.cn).
For complete installation steps, system configuration, and Frequently Asked Questions, see the [User Guide](https://docs.leokun.cn).
## Model Management
+2
View File
@@ -14,6 +14,7 @@ tauri-build = { version = "2", features = [] }
[dependencies]
axum = "0.8"
cursor-server = { path = "../../../server" }
rfd = "0.15"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tauri = { version = "2", features = ["tray-icon"] }
@@ -26,5 +27,6 @@ tauri-plugin-updater = "2"
tokio = { version = "1", features = ["time"] }
tokio-util = "0.7"
tracing = "0.1"
tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
url = "2"
+28 -13
View File
@@ -1,5 +1,5 @@
use std::{
process::Command,
process::{Command, ExitCode},
sync::{
atomic::{AtomicBool, Ordering},
Mutex,
@@ -19,7 +19,6 @@ use tauri::{
};
use tauri_plugin_opener::OpenerExt;
use tokio_util::sync::CancellationToken;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[cfg(dev)]
use cursor_server::config::ConsoleSource;
@@ -27,6 +26,7 @@ use cursor_server::{App, Config, Result};
#[cfg(not(dev))]
use crate::frontend;
use crate::startup::{self, StartupDiagnostics};
use crate::tray;
pub(crate) const MAIN_WINDOW_LABEL: &str = "main";
@@ -141,16 +141,23 @@ fn create_main_window(
builder.build()
}
pub fn run() {
let started_by_autostart = std::env::args_os().any(|arg| arg == AUTOSTART_ARG);
pub fn run() -> ExitCode {
let diagnostics = match StartupDiagnostics::initialize() {
Ok(diagnostics) => diagnostics,
Err(error) => {
startup::report_logging_failure(error.as_ref());
return ExitCode::FAILURE;
}
};
tracing::info!(
version = env!("CARGO_PKG_VERSION"),
os = std::env::consts::OS,
architecture = std::env::consts::ARCH,
log_directory = %diagnostics.log_directory().display(),
"desktop starting"
);
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "cursor_server=info".into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
let started_by_autostart = std::env::args_os().any(|arg| arg == AUTOSTART_ARG);
let app = tauri::Builder::default()
.invoke_handler(tauri::generate_handler![open_terminal_with_command])
@@ -218,8 +225,14 @@ pub fn run() {
tray::create(app)?;
Ok(())
})
.build(tauri::generate_context!())
.expect("failed to build Cursor BYOK desktop app");
.build(tauri::generate_context!());
let app = match app {
Ok(app) => app,
Err(error) => {
diagnostics.report_fatal(&error);
return ExitCode::FAILURE;
}
};
app.run(|app, event| match event {
RunEvent::WindowEvent {
@@ -264,4 +277,6 @@ pub fn run() {
} => tray::show_main_window(app),
_ => {}
});
ExitCode::SUCCESS
}
+1
View File
@@ -1,6 +1,7 @@
mod desktop;
#[cfg(not(dev))]
mod frontend;
mod startup;
mod tray;
pub use desktop::run;
+2 -2
View File
@@ -1,5 +1,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
cursor_byok_desktop::run();
fn main() -> std::process::ExitCode {
cursor_byok_desktop::run()
}
+139
View File
@@ -0,0 +1,139 @@
//! Captures startup diagnostics before the database and desktop runtime initialize.
use std::{error::Error, path::PathBuf};
use rfd::{MessageButtons, MessageDialog, MessageLevel};
use tracing_appender::{
non_blocking::WorkerGuard,
rolling::{RollingFileAppender, Rotation},
};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
const LOG_DIRECTORY_NAME: &str = "logs";
const LOG_FILE_PREFIX: &str = "cursor-byok";
const LOG_FILE_SUFFIX: &str = "log";
const RETAINED_LOG_FILES: usize = 15;
type BoxError = Box<dyn Error + Send + Sync>;
pub(crate) struct StartupDiagnostics {
log_directory: PathBuf,
_writer_guard: WorkerGuard,
}
impl StartupDiagnostics {
pub(crate) fn initialize() -> Result<Self, BoxError> {
let log_directory = cursor_server::config::managed_data_dir()?.join(LOG_DIRECTORY_NAME);
std::fs::create_dir_all(&log_directory)?;
let file_appender = RollingFileAppender::builder()
.rotation(Rotation::DAILY)
.filename_prefix(LOG_FILE_PREFIX)
.filename_suffix(LOG_FILE_SUFFIX)
.max_log_files(RETAINED_LOG_FILES)
.build(&log_directory)?;
let (file_writer, writer_guard) = tracing_appender::non_blocking(file_appender);
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "cursor_byok_desktop=info,cursor_server=info".into());
tracing_subscriber::registry()
.with(filter)
.with(tracing_subscriber::fmt::layer())
.with(
tracing_subscriber::fmt::layer()
.with_ansi(false)
.with_writer(file_writer),
)
.try_init()?;
Ok(Self {
log_directory,
_writer_guard: writer_guard,
})
}
pub(crate) fn log_directory(&self) -> &std::path::Path {
&self.log_directory
}
pub(crate) fn report_fatal(&self, error: &(dyn Error + 'static)) {
let details = error_chain(error);
tracing::error!(
error = %details,
log_directory = %self.log_directory.display(),
"desktop failed to start"
);
show_fatal_dialog(&details, Some(&self.log_directory));
}
}
pub(crate) fn report_logging_failure(error: &(dyn Error + 'static)) {
let details = error_chain(error);
eprintln!("Cursor BYOK failed to initialize logging: {details}");
show_fatal_dialog(&details, None);
}
fn show_fatal_dialog(details: &str, log_directory: Option<&std::path::Path>) {
let log_guidance = match log_directory {
Some(directory) => format!(
"日志目录 / Log directory:\n{}\n\n请将最新的日志文件发送给开发者。\nPlease send the latest log file to the developer.",
directory.display()
),
None => "日志系统也未能启动,因此没有生成日志文件。\nLogging also failed to initialize, so no log file was created."
.to_owned(),
};
let description = format!(
"Cursor BYOK 无法启动 / failed to start.\n\n错误 / Error:\n{details}\n\n{log_guidance}"
);
let _ = MessageDialog::new()
.set_level(MessageLevel::Error)
.set_title("Cursor BYOK 启动失败 / Startup Error")
.set_description(description)
.set_buttons(MessageButtons::Ok)
.show();
}
fn error_chain(error: &(dyn Error + 'static)) -> String {
let mut details = error.to_string();
let mut source = error.source();
while let Some(cause) = source {
details.push_str("\nCaused by: ");
details.push_str(&cause.to_string());
source = cause.source();
}
details
}
#[cfg(test)]
mod tests {
use super::*;
use std::fmt;
#[derive(Debug)]
struct OuterError(std::io::Error);
impl fmt::Display for OuterError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("database initialization failed")
}
}
impl Error for OuterError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.0)
}
}
#[test]
fn fatal_report_includes_the_complete_error_chain() {
let error = OuterError(std::io::Error::new(
std::io::ErrorKind::PermissionDenied,
"database file is read-only",
));
assert_eq!(
error_chain(&error),
"database initialization failed\nCaused by: database file is read-only"
);
}
}
+11 -11
View File
@@ -1,16 +1,16 @@
import { useEffect, useRef } from "react";
import { HashRouter, Navigate, Route, Routes } from "react-router-dom";
import { MessageProvider } from "./components/ui/MessageProvider";
import { useMessage } from "./components/ui/message";
import { AppFrame } from "./layouts/AppFrame";
import { AppLayout } from "./layouts/AppLayout";
import { CallsPage } from "./pages/CallsPage";
import { CallDetailsPage } from "./pages/CallDetailsPage";
import { CursorSettingsPage } from "./pages/CursorSettingsPage";
import { HomePage } from "./pages/HomePage";
import { SettingsPage } from "./pages/SettingsPage";
import { useAppStore } from "./store/appStore";
import { updateStore } from "./store/updateStore";
import { MessageProvider } from "./shared/ui/MessageProvider";
import { useMessage } from "./shared/ui/message";
import { AppFrame } from "./shell/AppFrame";
import { AppLayout } from "./shell/AppLayout";
import { CallsPage } from "./features/calls/CallsPage";
import { CallDetailsPage } from "./features/calls/CallDetailsPage";
import { CursorSettingsPage } from "./features/models/CursorSettingsPage";
import { HomePage } from "./features/home/HomePage";
import { SettingsPage } from "./features/settings/SettingsPage";
import { useAppStore } from "./shared/store/appStore";
import { updateStore } from "./shared/store/updateStore";
export function App() {
return (
+3 -2
View File
@@ -8,7 +8,7 @@ import type {
ProxySettings,
StatisticsStorage,
TabSettings,
} from "../api";
} from "../shared/api";
const API_ROOT = "/__byok-api__/api";
const FIXED_NOW = Date.UTC(2026, 7, 27, 8, 0, 0);
@@ -55,6 +55,7 @@ const calls: LlmCall[] = Array.from({ length: 24 }, (_, index) => {
finish_reason: failed ? null : "stop",
created_at_ms: FIXED_NOW - index * 3 * 60_000,
ttfb_ms: 210 + index * 13,
ttfr_ms: 290 + index * 15,
ttft_ms: 370 + index * 17,
duration_ms: failed ? 812 : 1_420 + index * 71,
input_tokens: 4_800 + index * 337,
@@ -116,7 +117,7 @@ export function installDemoApi() {
}
if (path === "/models/import-v0049") return json({ imported: 0, skipped: 0, total: 0 });
if (/^\/models\/[^/]+\/test\/[^/]+$/.test(path) && method === "POST") {
return json({ duration_ms: 1_284, first_text_ms: 418, output_tokens: 42, tokens_per_second: 38.6, tokens_estimated: false, output: "Mock connectivity test passed." });
return json({ duration_ms: 1_284, first_valid_response_ms: 418, output_tokens: 42, tokens_per_second: 38.6, tokens_estimated: false, output: "Mock connectivity test passed." });
}
if (/^\/models\/[^/]+\/test\/[^/]+$/.test(path) || /^\/models\/[^/]+$/.test(path)) {
return method === "DELETE" ? empty() : json(models[0]);
@@ -1,4 +1,4 @@
@use "../styles/typography" as type;
@use "../../styles/typography" as type;
.root {
section { min-width: 0; }
@@ -1,6 +1,6 @@
import type { CallDetail } from "../api";
import { JsonEditor } from "./ui/JsonEditor";
import { Tabs, type TabItem } from "./ui/Tabs";
import type { CallDetail } from "../../shared/api";
import { JsonEditor } from "../../shared/ui/JsonEditor";
import { Tabs, type TabItem } from "../../shared/ui/Tabs";
import styles from "./CallDetails.module.scss";
const show = (value: string | number | null) => value ?? "-";
@@ -32,6 +32,7 @@ export function CallDetails({ detail }: { detail: CallDetail }) {
["Created At", `${call.created_at_ms} · ${new Date(call.created_at_ms).toLocaleString()}`],
[t("耗时"), timing(call.duration_ms)],
["TTFB", timing(call.ttfb_ms)],
["TTFR", timing(call.ttfr_ms)],
["TTFT", timing(call.ttft_ms)],
["Input Token", show(call.input_tokens)],
["Output Token", show(call.output_tokens)],
@@ -1,4 +1,4 @@
@use "../styles/typography" as type;
@use "../../styles/typography" as type;
.root {
width: 100%;
@@ -1,10 +1,10 @@
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { api, type CallDetail } from "../api";
import { CallDetails } from "../components/CallDetails";
import { TitledCard } from "../components/ui/TitledCard";
import { api, type CallDetail } from "../../shared/api";
import { CallDetails } from "./CallDetails";
import { TitledCard } from "../../shared/ui/TitledCard";
import styles from "./CallDetailsPage.module.scss";
import { ScrollableContent } from "../components/virtual/ScrollableContent";
import { ScrollableContent } from "../../shared/virtual/ScrollableContent";
export function CallDetailsPage() {
const { callId = "" } = useParams();
@@ -1,4 +1,4 @@
@use "../styles/typography" as type;
@use "../../styles/typography" as type;
.status {
padding: 3px 7px;
@@ -1,9 +1,9 @@
import type { LlmCall } from "../api";
import controls from "./ui/Controls.module.scss";
import { DataTable, type DataTableColumn } from "./ui/DataTable";
import { Icon } from "./ui/Icon";
import { TooltipTrigger } from "./ui/TooltipTrigger";
import { eyeIcon } from "./ui/icons";
import type { LlmCall } from "../../shared/api";
import controls from "../../shared/ui/Controls.module.scss";
import { DataTable, type DataTableColumn } from "../../shared/ui/DataTable";
import { Icon } from "../../shared/ui/Icon";
import { TooltipTrigger } from "../../shared/ui/TooltipTrigger";
import { eyeIcon } from "../../shared/ui/icons";
import styles from "./CallTable.module.scss";
const value = (input: string | number | null) => input ?? "-";
@@ -87,6 +87,11 @@ export function CallTable({ calls, onDetails }: { calls: LlmCall[]; onDetails: (
header: "TTFB",
render: (call) => milliseconds(call.ttfb_ms),
},
{
key: "ttfr",
header: "TTFR",
render: (call) => milliseconds(call.ttfr_ms),
},
{
key: "ttft",
header: "TTFT",
@@ -0,0 +1,34 @@
import { useEffect } from "react";
import { CallTable } from "./CallTable";
import { PageContent } from "../../shell/layout/PageContent";
import { appStore, useAppStore } from "../../shared/store/appStore";
import styles from "./CallsPage.module.scss";
const CALL_REFRESH_INTERVAL_MS = 2_000;
export function CallsPage() {
const { calls } = useAppStore();
useEffect(() => {
let disposed = false;
const refreshCalls = () => {
if (!disposed && document.visibilityState === "visible") {
void appStore.refreshCalls();
}
};
refreshCalls();
const interval = window.setInterval(refreshCalls, CALL_REFRESH_INTERVAL_MS);
window.addEventListener("focus", refreshCalls);
document.addEventListener("visibilitychange", refreshCalls);
return () => {
disposed = true;
window.clearInterval(interval);
window.removeEventListener("focus", refreshCalls);
document.removeEventListener("visibilitychange", refreshCalls);
};
}, []);
const content = <div className={styles.page}><CallTable calls={calls} onDetails={(call) => void appStore.openCallDetails(call.call_id)} /></div>;
return <PageContent fixed title={t("调用")} contentClassName={styles.pageContent} sections={[{ key: "calls", estimatedHeight: 720, content }]} />;
}
@@ -1,15 +1,15 @@
import { useEffect, useState } from "react";
import { api, type Overview } from "../api";
import { ContributionCalendarChart } from "../components/charts/ContributionCalendarChart";
import { DailyTokenUsageChart } from "../components/charts/DailyTokenUsageChart";
import { HomeMetrics } from "../components/metrics/HomeMetrics";
import { PageContent } from "../components/layout/PageContent";
import type { VirtualPageSection } from "../components/layout/VirtualPage";
import { OverviewTimeRangeFilter, type OverviewRangePreset } from "../components/overview/OverviewTimeRangeFilter";
import { PageActions } from "../layouts/PageActions";
import { appStore, useAppStore } from "../store/appStore";
import { formatTimeInput, parseTimeInput } from "../utils/parseTimeInput";
import { claudeIcon, openAiIcon } from "../components/ui/icons";
import { api, type Overview } from "../../shared/api";
import { ContributionCalendarChart } from "./charts/ContributionCalendarChart";
import { DailyTokenUsageChart } from "./charts/DailyTokenUsageChart";
import { HomeMetrics } from "./metrics/HomeMetrics";
import { PageContent } from "../../shell/layout/PageContent";
import type { VirtualPageSection } from "../../shell/layout/VirtualPage";
import { OverviewTimeRangeFilter, type OverviewRangePreset } from "./overview/OverviewTimeRangeFilter";
import { PageActions } from "../../shell/PageActions";
import { appStore, useAppStore } from "../../shared/store/appStore";
import { formatTimeInput, parseTimeInput } from "../../shared/utils/parseTimeInput";
import { claudeIcon, openAiIcon } from "../../shared/ui/icons";
type TimeRange = { startMs: number; endMs: number };
@@ -1,4 +1,4 @@
@use "../../styles/typography" as type;
@use "../../../styles/typography" as type;
.root {
width: 100%;
@@ -1,8 +1,8 @@
import { useLayoutEffect, useMemo, useRef, useState } from "react";
import { init, Rect, type ElementEvent } from "zrender";
import type { Locale } from "../../i18n/runtime";
import { useI18n } from "../../i18n/store";
import { Tooltip, type TooltipAnchor } from "../ui/Tooltip";
import type { Locale } from "../../../i18n/runtime";
import { useI18n } from "../../../i18n/store";
import { Tooltip, type TooltipAnchor } from "../../../shared/ui/Tooltip";
import styles from "./ContributionCalendarChart.module.scss";
export type ContributionDay = {
@@ -1,4 +1,4 @@
@use "../../styles/typography" as type;
@use "../../../styles/typography" as type;
.root {
--daily-token-tooltip-font-size: #{type.$font-size-2xs};
@@ -1,7 +1,7 @@
import type { EChartsCoreOption } from "echarts/core";
import { useMemo, useState } from "react";
import type { TokenUsageGranularity } from "../../api";
import { formatCompactInteger } from "../../utils/numberFormat";
import type { TokenUsageGranularity } from "../../../shared/api";
import { formatCompactInteger } from "../../../shared/utils/numberFormat";
import { EChart } from "./EChart";
import styles from "./DailyTokenUsageChart.module.scss";
@@ -1,18 +1,19 @@
import type { EChartsCoreOption } from "echarts/core";
import type { LlmCall } from "../../api";
import type { LlmCall } from "../../../shared/api";
import { EChart } from "./EChart";
export function LatencyChart({ calls }: { calls: LlmCall[] }) {
const points = calls.slice(0, 20).reverse();
const option: EChartsCoreOption = {
animationDuration: 280,
color: ["#d79a62", "#ad84cf"],
color: ["#d79a62", "#ad84cf", "#72a8d8"],
grid: { top: 22, right: 18, bottom: 30, left: 48 },
legend: { top: 0, right: 8, textStyle: { color: "#999" } },
tooltip: { trigger: "axis" },
xAxis: { type: "category", data: points.map((call) => new Date(call.created_at_ms).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })), axisLabel: { color: "#888" }, axisLine: { lineStyle: { color: "#5555" } } },
yAxis: { type: "value", axisLabel: { color: "#888", formatter: "{value} ms" }, splitLine: { lineStyle: { color: "#8882" } } },
series: [
{ name: "TTFR", type: "line", smooth: true, symbol: "none", data: points.map((call) => call.ttfr_ms ?? 0) },
{ name: "TTFT", type: "line", smooth: true, symbol: "none", data: points.map((call) => call.ttft_ms ?? 0) },
{ name: t("总耗时"), type: "line", smooth: true, symbol: "none", data: points.map((call) => call.duration_ms ?? 0) },
],
@@ -1,5 +1,5 @@
import type { EChartsCoreOption } from "echarts/core";
import type { LlmCall } from "../../api";
import type { LlmCall } from "../../../shared/api";
import { EChart } from "./EChart";
export function TokenTrendChart({ calls }: { calls: LlmCall[] }) {
@@ -1,4 +1,4 @@
@use "../../styles/typography" as type;
@use "../../../styles/typography" as type;
.root {
position: relative;
@@ -1,4 +1,4 @@
@use "../../styles/typography" as type;
@use "../../../styles/typography" as type;
.scroller {
width: 100%;
@@ -1,8 +1,8 @@
import { useState } from "react";
import { formatCompactInteger, formatInteger } from "../../utils/numberFormat";
import { Icon } from "../ui/Icon";
import { Tooltip, type TooltipAnchor } from "../ui/Tooltip";
import { informationOutlineIcon } from "../ui/icons";
import { formatCompactInteger, formatInteger } from "../../../shared/utils/numberFormat";
import { Icon } from "../../../shared/ui/Icon";
import { Tooltip, type TooltipAnchor } from "../../../shared/ui/Tooltip";
import { informationOutlineIcon } from "../../../shared/ui/icons";
import { CacheHitRateChart } from "./CacheHitRateChart";
import styles from "./HomeMetrics.module.scss";
@@ -1,4 +1,4 @@
@use "../../styles/typography" as type;
@use "../../../styles/typography" as type;
.root,
.presets {
@@ -1,12 +1,12 @@
import { autoUpdate, computePosition, flip, offset, shift, size } from "@floating-ui/dom";
import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { parseTimeInput } from "../../utils/parseTimeInput";
import controls from "../ui/Controls.module.scss";
import { Icon } from "../ui/Icon";
import { MultiSelect, type MultiSelectOption } from "../ui/MultiSelect";
import { TooltipTrigger } from "../ui/TooltipTrigger";
import { refreshIcon } from "../ui/icons";
import { parseTimeInput } from "../../../shared/utils/parseTimeInput";
import controls from "../../../shared/ui/Controls.module.scss";
import { Icon } from "../../../shared/ui/Icon";
import { MultiSelect, type MultiSelectOption } from "../../../shared/ui/MultiSelect";
import { TooltipTrigger } from "../../../shared/ui/TooltipTrigger";
import { refreshIcon } from "../../../shared/ui/icons";
import styles from "./OverviewTimeRangeFilter.module.scss";
export type OverviewRangePreset = "ten-minutes" | "hour" | "today" | "week" | "month" | "custom";
@@ -1,6 +1,6 @@
import { createContext, useContext, type ReactNode } from "react";
import { useAppStore } from "../../store/appStore";
import controls from "../ui/Controls.module.scss";
import { useAppStore } from "../../shared/store/appStore";
import controls from "../../shared/ui/Controls.module.scss";
import styles from "./CursorSettings.module.scss";
const CaReady = createContext(false);
@@ -1,11 +1,11 @@
import type { IconifyIcon } from "@iconify/react/offline";
import { useEffect, useRef } from "react";
import Sortable from "sortablejs";
import type { Model } from "../../api";
import { Button } from "../ui/Button";
import { Card } from "../ui/Card";
import { Icon } from "../ui/Icon";
import { claudeIcon, dragIcon, flatColorOrganizationIcon, openAiIcon } from "../ui/icons";
import type { Model } from "../../shared/api";
import { Button } from "../../shared/ui/Button";
import { Card } from "../../shared/ui/Card";
import { Icon } from "../../shared/ui/Icon";
import { claudeIcon, dragIcon, flatColorOrganizationIcon, openAiIcon } from "../../shared/ui/icons";
import { CursorModelTestResult, type CursorModelTestState } from "./CursorModelTestResult";
import styles from "./CursorSettings.module.scss";
@@ -1,13 +1,13 @@
import type { ModelInput, ModelType } from "../../api";
import { defaultCustomHeadersText } from "../../utils/modelDefaults";
import { modelPresets, presetEndpoint, trimTrailingSlash, type ModelPreset } from "../../utils/modelPresets";
import { Button } from "../ui/Button";
import { Checkbox } from "../ui/Checkbox";
import { FormField, SecretTextInput, TextInput } from "../ui/FormControls";
import { JsonEditor } from "../ui/JsonEditor";
import { Combobox, Select } from "../ui/Select";
import { Switch } from "../ui/Switch";
import { claudeIcon, openAiIcon } from "../ui/icons";
import type { ModelInput, ModelType } from "../../shared/api";
import { defaultCustomHeadersText } from "../../shared/utils/modelDefaults";
import { modelPresets, presetEndpoint, trimTrailingSlash, type ModelPreset } from "../../shared/utils/modelPresets";
import { Button } from "../../shared/ui/Button";
import { Checkbox } from "../../shared/ui/Checkbox";
import { FormField, SecretTextInput, TextInput } from "../../shared/ui/FormControls";
import { JsonEditor } from "../../shared/ui/JsonEditor";
import { Combobox, Select } from "../../shared/ui/Select";
import { Switch } from "../../shared/ui/Switch";
import { claudeIcon, openAiIcon } from "../../shared/ui/icons";
import { CursorPresetChips } from "./CursorPresetChips";
import styles from "./CursorSettings.module.scss";
@@ -1,7 +1,7 @@
import type { ModelConnectivityResult } from "../../api";
import { Icon } from "../ui/Icon";
import { TooltipTrigger } from "../ui/TooltipTrigger";
import { informationOutlineIcon } from "../ui/icons";
import type { ModelConnectivityResult } from "../../shared/api";
import { Icon } from "../../shared/ui/Icon";
import { TooltipTrigger } from "../../shared/ui/TooltipTrigger";
import { informationOutlineIcon } from "../../shared/ui/icons";
import styles from "./CursorModelTestResult.module.scss";
export type CursorModelTestState =
@@ -21,7 +21,7 @@ export function CursorModelTestResult({ state, testing = false }: { state?: Curs
const detail = success
? t("速度 {speed} tokens/s · 首字 {firstText} ms · 总耗时 {duration} ms · 输出 {tokens} tokens{estimated} · 返回:{output}", {
speed: formatSpeed(state.result.tokens_per_second),
firstText: state.result.first_text_ms ?? "--",
firstText: state.result.first_valid_response_ms ?? "--",
duration: state.result.duration_ms,
tokens: state.result.output_tokens,
estimated: state.result.tokens_estimated ? t("(估算)") : "",
@@ -1,5 +1,5 @@
import type { ModelType } from "../../api";
import { modelPresets, presetEndpoint, trimTrailingSlash, type ModelPreset } from "../../utils/modelPresets";
import type { ModelType } from "../../shared/api";
import { modelPresets, presetEndpoint, trimTrailingSlash, type ModelPreset } from "../../shared/utils/modelPresets";
import styles from "./CursorPresetChips.module.scss";
/** 常用服务商预设:点击按当前协议类型自动填充对应端点与默认模型 */
@@ -1,21 +1,21 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { api, type Model, type ModelInput } from "../api";
import { CursorCaGate, CursorCaProvider, CursorModelGate, CursorModelProvider } from "../components/cursor/CursorGates";
import { CursorModelCards, cursorModelGroups, type CursorModelGrouping } from "../components/cursor/CursorModelCards";
import { CursorModelEditor, emptyCursorModelDraft, type CursorModelDraft } from "../components/cursor/CursorModelEditor";
import { CursorModelTestResult, type CursorModelTestState } from "../components/cursor/CursorModelTestResult";
import styles from "../components/cursor/CursorSettings.module.scss";
import { PageContent } from "../components/layout/PageContent";
import { LegacyModelImport } from "../components/models/LegacyModelImport";
import { ConfirmDialog } from "../components/ui/ConfirmDialog";
import controls from "../components/ui/Controls.module.scss";
import { Icon } from "../components/ui/Icon";
import { Modal } from "../components/ui/Modal";
import { TooltipTrigger } from "../components/ui/TooltipTrigger";
import { addIcon } from "../components/ui/icons";
import { useMessage } from "../components/ui/message";
import { PageActions } from "../layouts/PageActions";
import { appStore, useAppStore } from "../store/appStore";
import { api, type Model, type ModelInput } from "../../shared/api";
import { CursorCaGate, CursorCaProvider, CursorModelGate, CursorModelProvider } from "./CursorGates";
import { CursorModelCards, cursorModelGroups, type CursorModelGrouping } from "./CursorModelCards";
import { CursorModelEditor, emptyCursorModelDraft, type CursorModelDraft } from "./CursorModelEditor";
import { CursorModelTestResult, type CursorModelTestState } from "./CursorModelTestResult";
import styles from "./CursorSettings.module.scss";
import { PageContent } from "../../shell/layout/PageContent";
import { LegacyModelImport } from "./LegacyModelImport";
import { ConfirmDialog } from "../../shared/ui/ConfirmDialog";
import controls from "../../shared/ui/Controls.module.scss";
import { Icon } from "../../shared/ui/Icon";
import { Modal } from "../../shared/ui/Modal";
import { TooltipTrigger } from "../../shared/ui/TooltipTrigger";
import { addIcon } from "../../shared/ui/icons";
import { useMessage } from "../../shared/ui/message";
import { PageActions } from "../../shell/PageActions";
import { appStore, useAppStore } from "../../shared/store/appStore";
export function CursorSettingsPage() {
const { models, cursorHarness, cursorBusy } = useAppStore();
@@ -1,8 +1,8 @@
import { useState, type ReactNode } from "react";
import { api, type LegacyModelImportPreview } from "../../api";
import { appStore } from "../../store/appStore";
import { ConfirmDialog } from "../ui/ConfirmDialog";
import { useMessage } from "../ui/message";
import { api, type LegacyModelImportPreview } from "../../shared/api";
import { appStore } from "../../shared/store/appStore";
import { ConfirmDialog } from "../../shared/ui/ConfirmDialog";
import { useMessage } from "../../shared/ui/message";
import styles from "./LegacyModelImport.module.scss";
type LegacyModelImportControl = {
@@ -8,12 +8,12 @@ import {
writeAutostart,
writeDockIconVisibility,
writeSilentStart,
} from "../../native/appLifecycle";
import { updateStore, useUpdateStore } from "../../store/updateStore";
import { Button } from "../ui/Button";
import { Switch } from "../ui/Switch";
import { TitledCard } from "../ui/TitledCard";
import { useMessage } from "../ui/message";
} from "../../shared/native/appLifecycle";
import { updateStore, useUpdateStore } from "../../shared/store/updateStore";
import { Button } from "../../shared/ui/Button";
import { Switch } from "../../shared/ui/Switch";
import { TitledCard } from "../../shared/ui/TitledCard";
import { useMessage } from "../../shared/ui/message";
import styles from "./AppLifecycleSettingsCard.module.scss";
export function AppLifecycleSettingsCard() {
@@ -1,9 +1,9 @@
import type { ProxySettings, ProxySettingsInput } from "../../api";
import { Button } from "../ui/Button";
import { Checkbox } from "../ui/Checkbox";
import { TextInput } from "../ui/FormControls";
import { Select } from "../ui/Select";
import { TitledCard } from "../ui/TitledCard";
import type { ProxySettings, ProxySettingsInput } from "../../shared/api";
import { Button } from "../../shared/ui/Button";
import { Checkbox } from "../../shared/ui/Checkbox";
import { TextInput } from "../../shared/ui/FormControls";
import { Select } from "../../shared/ui/Select";
import { TitledCard } from "../../shared/ui/TitledCard";
import styles from "./ProxySettingsCard.module.scss";
export function ProxySettingsCard({
@@ -1,4 +1,4 @@
@use "../styles/typography" as type;
@use "../../styles/typography" as type;
.page {
display: grid;
@@ -41,6 +41,15 @@
}
}
.textButton {
padding: 4px;
color: var(--vscode-textLink-foreground);
background: transparent;
border: 0;
&:hover { color: var(--vscode-textLink-activeForeground); }
}
.importRow {
display: flex;
align-items: center;
@@ -59,15 +68,6 @@
}
}
.textButton {
padding: 4px;
color: var(--vscode-textLink-foreground);
background: transparent;
border: 0;
&:hover { color: var(--vscode-textLink-activeForeground); }
}
.confirmContent {
display: grid;
gap: 8px;
@@ -155,6 +155,7 @@
align-items: stretch;
flex-direction: column;
}
}
.themeActions .selected {
@@ -1,20 +1,20 @@
import { useEffect, useState } from "react";
import { api, type ProxySettings, type ProxySettingsInput, type StatisticsStorage, type StatisticsStorageScope, type TabSettings } from "../api";
import { PageContent } from "../components/layout/PageContent";
import { LegacyModelImport } from "../components/models/LegacyModelImport";
import { AppLifecycleSettingsCard } from "../components/settings/AppLifecycleSettingsCard";
import { ProxySettingsCard } from "../components/settings/ProxySettingsCard";
import { TabSettingsCard } from "../components/settings/TabSettingsCard";
import { Button } from "../components/ui/Button";
import { Checkbox } from "../components/ui/Checkbox";
import { ConfirmDialog } from "../components/ui/ConfirmDialog";
import { FormField, TextInput } from "../components/ui/FormControls";
import { Select } from "../components/ui/Select";
import { TitledCard } from "../components/ui/TitledCard";
import { setLocalePreference, useI18n, type LocalePreference } from "../i18n/store";
import { useMessage } from "../components/ui/message";
import { appStore, useAppStore } from "../store/appStore";
import { themeOptions } from "../theme/theme";
import { api, type ProxySettings, type ProxySettingsInput, type StatisticsStorage, type StatisticsStorageScope, type TabSettings } from "../../shared/api";
import { PageContent } from "../../shell/layout/PageContent";
import { LegacyModelImport } from "../models/LegacyModelImport";
import { AppLifecycleSettingsCard } from "./AppLifecycleSettingsCard";
import { ProxySettingsCard } from "./ProxySettingsCard";
import { TabSettingsCard } from "./TabSettingsCard";
import { Button } from "../../shared/ui/Button";
import { Checkbox } from "../../shared/ui/Checkbox";
import { ConfirmDialog } from "../../shared/ui/ConfirmDialog";
import { FormField, TextInput } from "../../shared/ui/FormControls";
import { Select } from "../../shared/ui/Select";
import { TitledCard } from "../../shared/ui/TitledCard";
import { setLocalePreference, useI18n, type LocalePreference } from "../../i18n/store";
import { useMessage } from "../../shared/ui/message";
import { appStore, useAppStore } from "../../shared/store/appStore";
import { themeOptions } from "../../shared/theme/theme";
import styles from "./SettingsPage.module.scss";
export function SettingsPage() {
@@ -1,10 +1,10 @@
import cursorIconUrl from "../../assets/icons/cursor.svg";
import type { TabMode, TabSettings } from "../../api";
import { Button } from "../ui/Button";
import { TextInput } from "../ui/FormControls";
import { Icon } from "../ui/Icon";
import { Select } from "../ui/Select";
import { TitledCard } from "../ui/TitledCard";
import cursorIconUrl from "../../shared/assets/icons/cursor.svg";
import type { TabMode, TabSettings } from "../../shared/api";
import { Button } from "../../shared/ui/Button";
import { TextInput } from "../../shared/ui/FormControls";
import { Icon } from "../../shared/ui/Icon";
import { Select } from "../../shared/ui/Select";
import { TitledCard } from "../../shared/ui/TitledCard";
import styles from "./TabSettingsCard.module.scss";
export function TabSettingsCard({
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -2,8 +2,8 @@ import ReactDOM from "react-dom/client";
import "../node_modules/monaco-editor/min/vs/editor/editor.main.css";
import { I18nRoot } from "./i18n/I18nRoot";
import { initializeI18n } from "./i18n/store";
import { appStore } from "./store/appStore";
import { applyTheme } from "./theme/theme";
import { appStore } from "./shared/store/appStore";
import { applyTheme } from "./shared/theme/theme";
import "./styles/globals.scss";
initializeI18n();
-10
View File
@@ -1,10 +0,0 @@
import { CallTable } from "../components/CallTable";
import { PageContent } from "../components/layout/PageContent";
import { appStore, useAppStore } from "../store/appStore";
import styles from "./CallsPage.module.scss";
export function CallsPage() {
const { calls } = useAppStore();
const content = <div className={styles.page}><CallTable calls={calls} onDetails={(call) => void appStore.openCallDetails(call.call_id)} /></div>;
return <PageContent fixed title={t("调用")} contentClassName={styles.pageContent} sections={[{ key: "calls", estimatedHeight: 720, content }]} />;
}
@@ -1,5 +1,5 @@
import type { AdRuntime } from "./components/ads/types";
import type { Locale } from "./i18n/runtime";
import type { AdRuntime } from "../shell/ads/types";
import type { Locale } from "../i18n/runtime";
export type ModelType = "openai" | "anthropic";
@@ -86,7 +86,7 @@ export interface LegacyModelImportResult {
export interface ModelConnectivityResult {
duration_ms: number;
first_text_ms: number | null;
first_valid_response_ms: number | null;
output_tokens: number;
tokens_per_second: number;
tokens_estimated: boolean;
@@ -197,6 +197,7 @@ export interface LlmCall {
created_at_ms: number;
ttfb_ms: number | null;
ttft_ms: number | null;
ttfr_ms: number | null;
duration_ms: number | null;
input_tokens: number | null;
output_tokens: number | null;

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Before

Width:  |  Height:  |  Size: 711 B

After

Width:  |  Height:  |  Size: 711 B

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

@@ -177,6 +177,14 @@ export const appStore = {
}
},
async refreshCalls() {
try {
update({ calls: await api.calls() });
} catch (cause) {
update({ error: cause instanceof Error ? cause.message : String(cause) });
}
},
async openCallDetails(callId: string) {
await perform(() => api.openCallDetails(callId));
},

Some files were not shown because too many files have changed in this diff Show More