fix local office resource editing

- add local-folder OnlyOffice sign/callback writeback and edit-tab handling

- align main resource tabs, attachment edit menu, slash isolation, and filetree context behavior

- record Sidex/Hermes gap reviews and Reasonix task checklists
This commit is contained in:
lix-2026
2026-05-21 05:40:06 +08:00
parent a29d9868f6
commit eba1010191
50 changed files with 4309 additions and 283 deletions
+92 -18
View File
@@ -196,6 +196,7 @@ pub fn acp_event_to_sse(event: AcpSessionEvent) -> Option<SseEvent> {
kind,
status,
raw_input,
locations,
} => Some(SseEvent {
event: "tool.started".into(),
data: json!({
@@ -204,6 +205,7 @@ pub fn acp_event_to_sse(event: AcpSessionEvent) -> Option<SseEvent> {
"kind": kind,
"status": status,
"input": raw_input,
"locations": locations,
}),
}),
AcpSessionEvent::ToolCallUpdate {
@@ -238,25 +240,30 @@ pub fn acp_event_to_sse(event: AcpSessionEvent) -> Option<SseEvent> {
tool_name,
params,
decision,
} => Some(SseEvent {
event: if decision == "allowed" {
"permission.allowed".into()
} else {
"permission.denied".into()
},
data: json!({
"permissionId": permission_id,
"toolName": tool_name,
"params": params,
"decision": decision,
}),
} => {
let event = match decision.as_str() {
"allowed" => "permission.allowed",
"requested" => "permission.requested",
_ => "permission.denied",
};
Some(SseEvent {
event: event.into(),
data: json!({
"permissionId": permission_id,
"toolName": tool_name,
"params": params,
"decision": decision,
}),
})
}
AcpSessionEvent::SessionInfoUpdate { title } => Some(SseEvent {
event: "session.info.updated".into(),
data: json!({ "title": title }),
}),
AcpSessionEvent::PlanUpdate { entries } => Some(SseEvent {
event: "plan.updated".into(),
data: json!({ "entries": entries }),
}),
AcpSessionEvent::SessionInfoUpdate { .. } => {
None // Not forwarded to frontend
}
AcpSessionEvent::PlanUpdate { .. } => {
None // Not forwarded (Phase C)
}
AcpSessionEvent::Disconnected { reason } if reason == "session closed" => None,
AcpSessionEvent::Disconnected { reason } => Some(SseEvent {
event: "run.failed".into(),
@@ -327,6 +334,37 @@ mod tests {
assert_eq!(sse.data["decision"], "denied");
}
#[test]
fn acp_permission_requested_emits_permission_requested_event() {
let event = AcpSessionEvent::PermissionRequest {
permission_id: "perm_2".into(),
tool_name: "mnote.page.get".into(),
params: json!({"documentId": "doc_2"}),
decision: "requested".into(),
};
let sse = acp_event_to_sse(event).expect("permission requested should be forwarded");
assert_eq!(sse.event, "permission.requested");
assert_eq!(sse.data["permissionId"], "perm_2");
assert_eq!(sse.data["toolName"], "mnote.page.get");
assert_eq!(sse.data["decision"], "requested");
}
#[test]
fn acp_permission_allowed_emits_permission_allowed_event() {
let event = AcpSessionEvent::PermissionRequest {
permission_id: "perm_3".into(),
tool_name: "mnote.page.save".into(),
params: json!({"documentId": "doc_1"}),
decision: "allowed".into(),
};
let sse = acp_event_to_sse(event).expect("permission allowed should be forwarded");
assert_eq!(sse.event, "permission.allowed");
assert_eq!(sse.data["permissionId"], "perm_3");
assert_eq!(sse.data["decision"], "allowed");
}
#[test]
fn acp_tool_events_keep_detail_for_collapsible_ui() {
let started = acp_event_to_sse(AcpSessionEvent::ToolCall {
@@ -335,12 +373,18 @@ mod tests {
kind: "read".into(),
status: crate::acp_types::ToolCallStatus::InProgress,
raw_input: Some(json!({"documentId": "doc_1", "includeBody": true})),
locations: vec!["/mnt/Data1T/mnote/src/main.rs".into()],
})
.expect("tool start");
assert_eq!(started.event, "tool.started");
assert_eq!(started.data["tool"], "mnote.page.get");
assert_eq!(started.data["status"], "in_progress");
assert_eq!(started.data["input"]["documentId"], "doc_1");
assert_eq!(
started.data["locations"][0],
"/mnt/Data1T/mnote/src/main.rs"
);
assert_eq!(started.data["locations"].as_array().unwrap().len(), 1);
let completed = acp_event_to_sse(AcpSessionEvent::ToolCallUpdate {
tool_call_id: "tool_1".into(),
@@ -367,4 +411,34 @@ mod tests {
assert_eq!(running.event, "tool.started");
assert_eq!(running.data["status"], "in_progress");
}
#[test]
fn acp_session_info_update_emits_session_info_updated_sse() {
let sse = acp_event_to_sse(AcpSessionEvent::SessionInfoUpdate {
title: "我的新会话标题".into(),
})
.expect("session info update should be forwarded");
assert_eq!(sse.event, "session.info.updated");
assert_eq!(sse.data["title"], "我的新会话标题");
}
#[test]
fn acp_plan_update_emits_plan_updated_sse() {
let entries = vec![
"步骤 1:读取文件".into(),
"步骤 2:修改配置".into(),
"步骤 3:验证更改".into(),
];
let sse = acp_event_to_sse(AcpSessionEvent::PlanUpdate {
entries: entries.clone(),
})
.expect("plan update should be forwarded");
assert_eq!(sse.event, "plan.updated");
let sse_entries: Vec<String> =
serde_json::from_value(sse.data["entries"].clone()).unwrap_or_default();
assert_eq!(sse_entries.len(), 3);
assert_eq!(sse_entries[0], "步骤 1:读取文件");
assert_eq!(sse_entries[1], "步骤 2:修改配置");
assert_eq!(sse_entries[2], "步骤 3:验证更改");
}
}