Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
72782fc
fix(remote_desktop_view): publish NSFilenamesPboardType and keep inst…
feigeCode Oct 8, 2026
ed070f0
fix(remote_desktop_view): write the macOS file clipboard with the cla…
feigeCode Oct 8, 2026
663a657
fix(personal-sync): 本地条目消失后冲突不再永远解不开
feigeCode Oct 8, 2026
034fb82
fix(personal-sync): 本地删除连接/分组/凭据时清掉遗留的同步冲突
feigeCode Oct 8, 2026
a4007df
fix(remote_desktop_view): 消掉 macOS 文件剪贴板里被 -D warnings 拦下的两处告警
feigeCode Oct 8, 2026
30b25a6
feat(personal-sync): 本地删除遇上「远端之后又改过」时挂冲突而不是抹掉
feigeCode Oct 8, 2026
4c8fe22
feat(personal-sync): 目录/Git/WebDAV 后端补上真正的跨实例互斥
feigeCode Oct 8, 2026
7a4fa5c
fix(sftp): 短读不再让整条下载失败(issue #363)
feigeCode Oct 9, 2026
bcede3b
fix(db_view): 刷新节点时不再先清空子树(issue #362)
feigeCode Oct 9, 2026
7cd6de8
fix(terminal): SSH 首连不再因设备还没回收旧会话而直接报错
feigeCode Oct 9, 2026
76cb55f
style: 本机 rustfmt 重排 52 个文件(无逻辑改动)
feigeCode Oct 9, 2026
9bc237c
test(sftp): 假 SFTP 服务器复现 #363 短读(issue #363)
feigeCode Oct 9, 2026
f695265
fix(terminal): 文件传输面板开关状态可辨、补「定位到终端目录」(issue #359)
feigeCode Oct 9, 2026
508e98c
fix(public_mcp): 线上工具名净化,Grok 不再整批丢工具(issue #194)
feigeCode Oct 9, 2026
66cd7a8
fix(terminal): 历史提示下拉背景透出 28% 导致看不清,压到 4%(issue #73)
feigeCode Oct 9, 2026
10237c5
feat(terminal): 弹层背景不透明度可配置,默认 0.96(issue #73)
feigeCode Oct 9, 2026
fdfe845
Merge branch 'main' into dev
feigeCode Oct 9, 2026
86797ff
perf(db_view): 执行记录面板改用 uniform_list 虚拟化(issue #368)
feigeCode Oct 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,13 @@
- **验证方式**:覆盖手动确认卡片包含“安全确认、二次审批、自动执行”说明,自动执行卡片不含误导提示,ACP 后续 Public MCP 请求进入 Dialog 队列并展示完整脱敏参数,以及 Auto High/Critical 直接执行。
- **适用范围**:`crates/ai_chat_view/src/acp/*`、`agent_transcript.rs`、`agent_view.rs`、`crates/public_mcp/src/permissions.rs`、`main/src/public_mcp_approval*`。

- **标题**:Public MCP 的线上工具名必须净化,内部工具 id 保持带点分层
- **触发信号**:Grok 这类 OpenAI 兼容客户端已经连上 Public MCP,却一个工具都列不出来(工具数为 0);或新增带点工具 id 后,同一个工具在 ACP 与 MCP 两个入口显示成不同的名字。
- **根因 / 约束**:Grok 注册函数时只接受 `[A-Za-z0-9_-]`,而 Navop 内部工具 id 用 `.` 分层(`ssh.command.poll`),带点的名字会在客户端注册阶段被整批丢弃;同时 Claude 等客户端接受 `.`,线上名字不能只按一种客户端的偏好来定。ACP 侧早已由 `ToolName` / `ToolNameAllocator` 完成净化,两条路各写一套会让同一工具在两个入口名字不一致。
- **正确做法**:内部 id 一律不改(避免污染审批、审计与大量引用);把 `PublicMcpToolRegistry::client_tools()` / `client_tool()` / `resolve_client_tool_name()` 作为唯一对外汇合点,复用 `agent_runtime::tools::{ToolName, ToolNameAllocator}`,并保持「先按内部 id 排序」的分配顺序,冲突(`sample.echo` 与 `sample_echo`)才会可复现且与 ACP 同名;`tools/call` 同时接受净化名与内部 id。对外暴露工具时不要直接读 `tools()`。
- **验证方式**:协议层用例断言 `tools/list` 返回的每个名字都落在 `[A-Za-z0-9_-]` 内、且用净化名能成功执行 `tools/call`;单元用例覆盖 `sample.echo` / `sample_echo` 的冲突消解、对照表排序与双向解析;带点的内部 id 调用保持可用。
- **适用范围**:`crates/public_mcp/src/tools/registry.rs`、`crates/public_mcp/src/protocol.rs`、`crates/agent_runtime/src/tools/spec.rs`、`crates/public_mcp/src/tools/agent_runtime_adapter.rs`。

- **标题**:macOS 自定义标题栏中的可拖元素必须由应用显式接管标题栏拖动
- **触发信号**:透明标题栏或 tab 栏中,按钮、输入框或 tab 的拖动被解释为窗口移动;为了规避问题出现 `allow_tab_drag = !is_macos` 一类平台禁用逻辑。
- **根因 / 约束**:GPUI 的 `stop_propagation()` 和 `prevent_default()` 只影响 GPUI 事件传播,不能阻止 AppKit 把透明标题栏视为系统 window-move region。把 `NSWindow.isMovable` 设为 false 虽能规避抢事件,但会禁用 macOS Window 菜单的平铺与窗口管理快捷键。
Expand Down
6 changes: 2 additions & 4 deletions crates/agent_runtime/src/planner/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,10 +382,8 @@ mod tests {

#[test]
fn ensure_user_message_keeps_existing_user_message() {
let messages = normalize_system_messages(vec![
Message::system("主系统提示"),
Message::user("你好"),
]);
let messages =
normalize_system_messages(vec![Message::system("主系统提示"), Message::user("你好")]);

let messages = ensure_user_message(messages);

Expand Down
10 changes: 3 additions & 7 deletions crates/ai_chat_view/src/acp/connection/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,8 @@ impl AcpConnection {
let result = match tokio::time::timeout(timeout, future.as_mut()).await {
Ok(result) => Some(result),
Err(_elapsed) => {
let _ = connection.send_notification(CancelNotification::new(
acp_session_id.clone(),
));
let _ = connection
.send_notification(CancelNotification::new(acp_session_id.clone()));
let _ = tokio::time::timeout(cancel_grace, future.as_mut()).await;
None
}
Expand Down Expand Up @@ -179,10 +178,7 @@ struct PromptContext {
async fn finish_prompt(
context: PromptContext,
result: Option<
Result<
agent_client_protocol::schema::PromptResponse,
agent_client_protocol::Error,
>,
Result<agent_client_protocol::schema::PromptResponse, agent_client_protocol::Error>,
>,
) {
let closed_error = connection_closed_error(&context.agent_id, &context.agent_name, None);
Expand Down
1 change: 1 addition & 0 deletions crates/ai_chat_view/src/default_panel_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ fn resource_catalog_refreshes_only_for_connection_list_changes() {
&ConnectionDataEvent::ConnectionDeleted {
connection_id: 1,
cloud_id: None,
last_synced_at: None,
},
));
assert!(!should_refresh_resource_catalog(
Expand Down
2 changes: 1 addition & 1 deletion crates/core/examples/create_ftp_test_connections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use anyhow::{Context, Result};
use one_core::crypto;
use one_core::storage::connection::SqliteConnection;
use one_core::storage::traits::Repository;
use one_core::storage::{ConnectionRepository, StoredConnection, SshParams, get_db_path};
use one_core::storage::{ConnectionRepository, SshParams, StoredConnection, get_db_path};

const FTP_NAME: &str = "FTP测试-本地pure-ftpd";
const FTPS_NAME: &str = "FTPS测试-本地pure-ftpd";
Expand Down
12 changes: 9 additions & 3 deletions crates/core/src/cloud_sync/personal/configured_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,7 @@ where
) -> Result<Self, SyncStoreError> {
match backend {
PersonalSyncBackendKind::Folder => Ok(Self::Folder(DirectorySyncStore::new(root))),
PersonalSyncBackendKind::Git => {
Ok(Self::new_git(root, runner, git_auto_push))
}
PersonalSyncBackendKind::Git => Ok(Self::new_git(root, runner, git_auto_push)),
PersonalSyncBackendKind::Webdav => {
let Some(settings) = webdav else {
return Err(SyncStoreError::NotConfigured);
Expand Down Expand Up @@ -168,4 +166,12 @@ where
Self::Webdav(store) => store.acquire_lock(owner).await,
}
}

async fn release_lock(&self, lock: &SyncStoreLock) -> Result<(), SyncStoreError> {
match self {
Self::Folder(store) => store.release_lock(lock).await,
Self::Git(store) => store.release_lock(lock).await,
Self::Webdav(store) => store.release_lock(lock).await,
}
}
}
76 changes: 52 additions & 24 deletions crates/core/src/cloud_sync/personal/conflict_resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,14 @@ where
conflict: &PersonalSyncConflict,
strategy: ConflictResolution,
) -> Result<(), SyncStoreError> {
// 冲突里存档的 `local_snapshot` 只是「检测到冲突那一刻」的快照:本地条目可能
// 已经被删掉(用户在界面里删了它),也可能换了一行。所以解决冲突前必须按
// `(data_type, 云端 record_id)` 现场重解析本地条目 —— 与 planner / worker
// 判定「本地是否有这条记录」的口径完全一致。
let local = self.current_local_item(conflict).await?;
match strategy {
ConflictResolution::UseCloud => self.use_cloud(conflict).await?,
ConflictResolution::UseLocal => self.use_local(conflict).await?,
ConflictResolution::UseCloud => self.use_cloud(conflict, local.as_ref()).await?,
ConflictResolution::UseLocal => self.use_local(conflict, local.as_ref()).await?,
ConflictResolution::KeepBoth => {
return Err(SyncStoreError::Conflict(
"personal sync keep-both resolution needs a local copy API".to_string(),
Expand All @@ -42,28 +47,62 @@ where
self.clear_conflict(conflict)
}

async fn use_cloud(&self, conflict: &PersonalSyncConflict) -> Result<(), SyncStoreError> {
/// 按云端记录现场找回本地条目;本地已不存在时返回 `None`。
async fn current_local_item(
&self,
conflict: &PersonalSyncConflict,
) -> Result<Option<PersonalSyncItemSnapshot>, SyncStoreError> {
Ok(self
.local
.list_items()
.await?
.into_iter()
.find(|item| is_same_cloud_record(item, conflict)))
}

async fn use_cloud(
&self,
conflict: &PersonalSyncConflict,
local: Option<&PersonalSyncItemSnapshot>,
) -> Result<(), SyncStoreError> {
let remote = required_remote_snapshot(conflict)?;
let local = optional_local_snapshot(conflict)?;
if remote.deleted_at.is_some() {
if let Some(local) = local.as_ref() {
// 云端也已经是墓碑:本地有就跟着删,没有就只剩清冲突。
if let Some(local) = local {
self.local.delete_item(local).await?;
}
return Ok(());
}
self.local.apply_remote(&remote, local.as_ref()).await?;
if let Some(local) = local.as_ref() {
// 本地条目已不存在时 `apply_remote` 会走插入(远端胜出),这正是「使用远程版本」
// 应有的语义,以前因为硬解 `local_snapshot` 里的本地行 id 而直接报错。
self.local.apply_remote(&remote, local).await?;
if let Some(local) = local {
self.local
.mark_synced(&local.local_id, &remote.id, remote.updated_at / 1000)
.await?;
}
Ok(())
}

async fn use_local(&self, conflict: &PersonalSyncConflict) -> Result<(), SyncStoreError> {
let local = required_local_snapshot(conflict)?;
async fn use_local(
&self,
conflict: &PersonalSyncConflict,
local: Option<&PersonalSyncItemSnapshot>,
) -> Result<(), SyncStoreError> {
let remote = required_remote_snapshot(conflict)?;
let mut record = self.local.export_item(&local).await?;
let Some(local) = local else {
// 本地条目已经不在了 ⇒「使用本地版本」= 保留本地的删除意图:把云端记录也
// 标记为删除。以前这里会直接报 "connection not found",用户点哪个按钮都出不来。
return self
.store
.tombstone_record(
&conflict.data_type,
&conflict.record_id,
Some(remote.version),
)
.await;
};
let mut record = self.local.export_item(local).await?;
record.id = conflict.record_id.clone();
let stored = self
.store
Expand All @@ -85,20 +124,9 @@ where
}
}

fn required_local_snapshot(
conflict: &PersonalSyncConflict,
) -> Result<PersonalSyncItemSnapshot, SyncStoreError> {
parse_snapshot(conflict.local_snapshot.as_deref(), "local")
}

fn optional_local_snapshot(
conflict: &PersonalSyncConflict,
) -> Result<Option<PersonalSyncItemSnapshot>, SyncStoreError> {
conflict
.local_snapshot
.as_deref()
.map(|snapshot| parse_snapshot(Some(snapshot), "local"))
.transpose()
fn is_same_cloud_record(item: &PersonalSyncItemSnapshot, conflict: &PersonalSyncConflict) -> bool {
item.data_type == conflict.data_type
&& item.cloud_id.as_deref() == Some(conflict.record_id.as_str())
}

fn required_remote_snapshot(
Expand Down
Loading
Loading