支持在历史会话中分叉出新会话
This commit is contained in:
@@ -46,7 +46,7 @@ sea-orm-migration = { version = "1.1", features = ["sqlx-sqlite", "runtime-tokio
|
|||||||
toml = "0.8"
|
toml = "0.8"
|
||||||
notify = "6"
|
notify = "6"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
agent-client-protocol-schema = { version = "0.10", features = ["unstable_session_usage"] }
|
agent-client-protocol-schema = { version = "0.10", features = ["unstable_session_usage", "unstable_session_fork"] }
|
||||||
kill_tree = { version = "0.2", features = ["tokio"] }
|
kill_tree = { version = "0.2", features = ["tokio"] }
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ pub enum ConnectionCommand {
|
|||||||
request_id: String,
|
request_id: String,
|
||||||
option_id: String,
|
option_id: String,
|
||||||
},
|
},
|
||||||
|
Fork {
|
||||||
|
reply: tokio::sync::oneshot::Sender<Result<crate::acp::types::ForkResultInfo, AcpError>>,
|
||||||
|
},
|
||||||
Disconnect,
|
Disconnect,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -494,6 +497,7 @@ async fn run_connection(
|
|||||||
let pending_perms: PendingPermissions = Arc::new(tokio::sync::Mutex::new(HashMap::new()));
|
let pending_perms: PendingPermissions = Arc::new(tokio::sync::Mutex::new(HashMap::new()));
|
||||||
let terminal_runtime = Arc::new(TerminalRuntime::new());
|
let terminal_runtime = Arc::new(TerminalRuntime::new());
|
||||||
let cwd = resolve_working_dir(working_dir.as_deref());
|
let cwd = resolve_working_dir(working_dir.as_deref());
|
||||||
|
let cwd_string = cwd.to_string_lossy().to_string();
|
||||||
let file_system_runtime = Arc::new(FileSystemRuntime::new(cwd.clone()));
|
let file_system_runtime = Arc::new(FileSystemRuntime::new(cwd.clone()));
|
||||||
|
|
||||||
let conn_id = connection_id.clone();
|
let conn_id = connection_id.clone();
|
||||||
@@ -617,6 +621,16 @@ async fn run_connection(
|
|||||||
&init_resp.agent_capabilities.prompt_capabilities,
|
&init_resp.agent_capabilities.prompt_capabilities,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let supports_fork = init_resp
|
||||||
|
.agent_capabilities
|
||||||
|
.session_capabilities
|
||||||
|
.fork
|
||||||
|
.is_some();
|
||||||
|
eprintln!(
|
||||||
|
"[ACP] Agent capabilities: load_session={}, fork={}",
|
||||||
|
init_resp.agent_capabilities.load_session, supports_fork
|
||||||
|
);
|
||||||
|
|
||||||
// Emit connected status
|
// Emit connected status
|
||||||
let _ = handle.emit(
|
let _ = handle.emit(
|
||||||
"acp://event",
|
"acp://event",
|
||||||
@@ -689,26 +703,93 @@ async fn run_connection(
|
|||||||
&perms,
|
&perms,
|
||||||
&mut cmd_rx,
|
&mut cmd_rx,
|
||||||
terminal_runtime.clone(),
|
terminal_runtime.clone(),
|
||||||
|
&cwd_string,
|
||||||
|
supports_fork,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
terminal_runtime.release_all_for_session(&sid).await;
|
terminal_runtime.release_all_for_session(&sid).await;
|
||||||
loop_result
|
drop(session);
|
||||||
|
handle_fork_or_exit(
|
||||||
|
loop_result,
|
||||||
|
&conn_id,
|
||||||
|
&handle,
|
||||||
|
&perms,
|
||||||
|
&mut cmd_rx,
|
||||||
|
terminal_runtime.clone(),
|
||||||
|
&cwd,
|
||||||
|
&cwd_string,
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
// session/load failed (e.g. ephemeral forked session).
|
||||||
|
// Fall back to session/new so the tab still works.
|
||||||
|
eprintln!(
|
||||||
|
"[ACP] session/load failed ({}), falling back to session/new",
|
||||||
|
e
|
||||||
|
);
|
||||||
let _ = handle.emit(
|
let _ = handle.emit(
|
||||||
"acp://event",
|
"acp://event",
|
||||||
AcpEvent::Error {
|
AcpEvent::Error {
|
||||||
connection_id: conn_id.clone(),
|
connection_id: conn_id.clone(),
|
||||||
message: format!("Failed to load session: {e}"),
|
message: format!("Failed to load session, starting new: {e}"),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
Err(e)
|
let new_resp = cx
|
||||||
|
.send_request_to(Agent, NewSessionRequest::new(cwd.clone()))
|
||||||
|
.block_task()
|
||||||
|
.await?;
|
||||||
|
let fallback_sid = new_resp.session_id.0.to_string();
|
||||||
|
let initial_config_options = new_resp.config_options.clone();
|
||||||
|
let mut session =
|
||||||
|
cx.attach_session(new_resp, Default::default())?;
|
||||||
|
let _ = handle.emit(
|
||||||
|
"acp://event",
|
||||||
|
AcpEvent::SessionStarted {
|
||||||
|
connection_id: conn_id.clone(),
|
||||||
|
session_id: fallback_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);
|
||||||
|
|
||||||
|
let loop_result = run_conversation_loop(
|
||||||
|
&mut session,
|
||||||
|
&conn_id,
|
||||||
|
&handle,
|
||||||
|
&perms,
|
||||||
|
&mut cmd_rx,
|
||||||
|
terminal_runtime.clone(),
|
||||||
|
&cwd_string,
|
||||||
|
supports_fork,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
terminal_runtime
|
||||||
|
.release_all_for_session(&fallback_sid)
|
||||||
|
.await;
|
||||||
|
drop(session);
|
||||||
|
handle_fork_or_exit(
|
||||||
|
loop_result,
|
||||||
|
&conn_id,
|
||||||
|
&handle,
|
||||||
|
&perms,
|
||||||
|
&mut cmd_rx,
|
||||||
|
terminal_runtime.clone(),
|
||||||
|
&cwd,
|
||||||
|
&cwd_string,
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Create new session
|
// Create new session
|
||||||
let new_resp = cx
|
let new_resp = cx
|
||||||
.send_request_to(Agent, NewSessionRequest::new(cwd))
|
.send_request_to(Agent, NewSessionRequest::new(cwd.clone()))
|
||||||
.block_task()
|
.block_task()
|
||||||
.await?;
|
.await?;
|
||||||
let sid = new_resp.session_id.0.to_string();
|
let sid = new_resp.session_id.0.to_string();
|
||||||
@@ -732,10 +813,23 @@ async fn run_connection(
|
|||||||
&perms,
|
&perms,
|
||||||
&mut cmd_rx,
|
&mut cmd_rx,
|
||||||
terminal_runtime.clone(),
|
terminal_runtime.clone(),
|
||||||
|
&cwd_string,
|
||||||
|
supports_fork,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
terminal_runtime.release_all_for_session(&sid).await;
|
terminal_runtime.release_all_for_session(&sid).await;
|
||||||
loop_result
|
drop(session);
|
||||||
|
handle_fork_or_exit(
|
||||||
|
loop_result,
|
||||||
|
&conn_id,
|
||||||
|
&handle,
|
||||||
|
&perms,
|
||||||
|
&mut cmd_rx,
|
||||||
|
terminal_runtime.clone(),
|
||||||
|
&cwd,
|
||||||
|
&cwd_string,
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
@@ -1197,7 +1291,96 @@ fn map_prompt_blocks(blocks: Vec<PromptInputBlock>) -> Vec<ContentBlock> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Result when the conversation loop exits due to a fork request.
|
||||||
|
struct ForkExitInfo {
|
||||||
|
fork_response: sacp::schema::ForkSessionResponse,
|
||||||
|
original_session_id: String,
|
||||||
|
reply: tokio::sync::oneshot::Sender<Result<crate::acp::types::ForkResultInfo, AcpError>>,
|
||||||
|
connection: ConnectionTo<Agent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// After `run_conversation_loop` returns, handle normal exit or fork transition.
|
||||||
|
///
|
||||||
|
/// When fork is requested, the original session has already been dropped by the
|
||||||
|
/// caller. We attach to the forked session (S2) directly using the
|
||||||
|
/// `ForkSessionResponse` — no separate `session/load` is needed because S2 was
|
||||||
|
/// just created in-memory by the agent on this connection.
|
||||||
|
async fn handle_fork_or_exit(
|
||||||
|
loop_result: Result<Option<ForkExitInfo>, sacp::Error>,
|
||||||
|
conn_id: &str,
|
||||||
|
handle: &tauri::AppHandle,
|
||||||
|
perms: &PendingPermissions,
|
||||||
|
cmd_rx: &mut mpsc::Receiver<ConnectionCommand>,
|
||||||
|
terminal_runtime: Arc<TerminalRuntime>,
|
||||||
|
_cwd: &std::path::Path,
|
||||||
|
cwd_string: &str,
|
||||||
|
) -> Result<(), sacp::Error> {
|
||||||
|
let fork_info = match loop_result {
|
||||||
|
Ok(Some(info)) => info,
|
||||||
|
Ok(None) => return Ok(()),
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
|
|
||||||
|
let cx = fork_info.connection;
|
||||||
|
let fork_resp = fork_info.fork_response;
|
||||||
|
let new_sid = fork_resp.session_id.0.to_string();
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"[ACP] Fork transition: attaching to forked session {} (original: {})",
|
||||||
|
new_sid, fork_info.original_session_id
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reply success to the frontend
|
||||||
|
let _ = fork_info.reply.send(Ok(crate::acp::types::ForkResultInfo {
|
||||||
|
forked_session_id: new_sid.clone(),
|
||||||
|
original_session_id: fork_info.original_session_id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Build a NewSessionResponse from the ForkSessionResponse so we can
|
||||||
|
// attach directly — the forked session is already live on this process.
|
||||||
|
let initial_config_options = fork_resp.config_options.clone();
|
||||||
|
let new_resp = NewSessionResponse::new(fork_resp.session_id)
|
||||||
|
.modes(fork_resp.modes)
|
||||||
|
.config_options(fork_resp.config_options)
|
||||||
|
.meta(fork_resp.meta);
|
||||||
|
let mut session = cx.attach_session(new_resp, Default::default())?;
|
||||||
|
|
||||||
|
let _ = handle.emit(
|
||||||
|
"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);
|
||||||
|
|
||||||
|
let loop_result = run_conversation_loop(
|
||||||
|
&mut session,
|
||||||
|
conn_id,
|
||||||
|
handle,
|
||||||
|
perms,
|
||||||
|
cmd_rx,
|
||||||
|
terminal_runtime.clone(),
|
||||||
|
cwd_string,
|
||||||
|
true, // fork already succeeded on this process
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
terminal_runtime.release_all_for_session(&new_sid).await;
|
||||||
|
drop(session);
|
||||||
|
|
||||||
|
// Recursively handle nested forks
|
||||||
|
Box::pin(handle_fork_or_exit(
|
||||||
|
loop_result, conn_id, handle, perms, cmd_rx, terminal_runtime, _cwd, cwd_string,
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
/// Main conversation command loop: wait for frontend commands and process them.
|
/// Main conversation command loop: wait for frontend commands and process them.
|
||||||
|
///
|
||||||
|
/// Returns `Ok(None)` on normal exit (disconnect / channel closed) or
|
||||||
|
/// `Ok(Some(ForkExitInfo))` when the loop should be restarted on a forked session.
|
||||||
async fn run_conversation_loop<'a>(
|
async fn run_conversation_loop<'a>(
|
||||||
session: &mut sacp::ActiveSession<'a, Agent>,
|
session: &mut sacp::ActiveSession<'a, Agent>,
|
||||||
conn_id: &str,
|
conn_id: &str,
|
||||||
@@ -1205,7 +1388,9 @@ async fn run_conversation_loop<'a>(
|
|||||||
perms: &PendingPermissions,
|
perms: &PendingPermissions,
|
||||||
cmd_rx: &mut mpsc::Receiver<ConnectionCommand>,
|
cmd_rx: &mut mpsc::Receiver<ConnectionCommand>,
|
||||||
terminal_runtime: Arc<TerminalRuntime>,
|
terminal_runtime: Arc<TerminalRuntime>,
|
||||||
) -> Result<(), sacp::Error> {
|
cwd: &str,
|
||||||
|
supports_fork: bool,
|
||||||
|
) -> Result<Option<ForkExitInfo>, sacp::Error> {
|
||||||
loop {
|
loop {
|
||||||
// Wait for either a user command or a session update (e.g. available_commands_update)
|
// Wait for either a user command or a session update (e.g. available_commands_update)
|
||||||
let cmd = loop {
|
let cmd = loop {
|
||||||
@@ -1565,12 +1750,45 @@ async fn run_conversation_loop<'a>(
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some(ConnectionCommand::Fork { reply }) => {
|
||||||
|
if !supports_fork {
|
||||||
|
let _ = reply.send(Err(AcpError::protocol(
|
||||||
|
"This agent does not support session/fork".to_string(),
|
||||||
|
)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let cx = session.connection();
|
||||||
|
let sid = session.session_id().clone();
|
||||||
|
eprintln!(
|
||||||
|
"[ACP] Sending session/fork for session_id={} cwd={}",
|
||||||
|
sid.0, cwd
|
||||||
|
);
|
||||||
|
let result = crate::acp::fork::fork_session(&cx, &sid, cwd).await;
|
||||||
|
match result {
|
||||||
|
Ok(fork_response) => {
|
||||||
|
eprintln!(
|
||||||
|
"[ACP] Fork succeeded: new_session_id={}",
|
||||||
|
fork_response.session_id.0
|
||||||
|
);
|
||||||
|
return Ok(Some(ForkExitInfo {
|
||||||
|
fork_response,
|
||||||
|
original_session_id: sid.0.to_string(),
|
||||||
|
reply,
|
||||||
|
connection: cx,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[ACP] Fork failed: {e}");
|
||||||
|
let _ = reply.send(Err(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some(ConnectionCommand::Disconnect) | None => {
|
Some(ConnectionCommand::Disconnect) | None => {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serialize a Vec<ToolCallContent> into a human-readable text string.
|
/// Serialize a Vec<ToolCallContent> into a human-readable text string.
|
||||||
|
|||||||
35
src-tauri/src/acp/fork.rs
Normal file
35
src-tauri/src/acp/fork.rs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
//! ACP `session/fork` support via raw JSON-RPC messages.
|
||||||
|
//!
|
||||||
|
//! The `sacp` crate does not yet provide typed request/response types for
|
||||||
|
//! `session/fork`, so we use `UntypedMessage` (the same pattern used for
|
||||||
|
//! `session/set_config_option` in connection.rs).
|
||||||
|
|
||||||
|
use sacp::schema::{ForkSessionRequest, ForkSessionResponse, SessionId};
|
||||||
|
use sacp::{Agent, ConnectionTo, UntypedMessage};
|
||||||
|
|
||||||
|
use crate::acp::error::AcpError;
|
||||||
|
|
||||||
|
/// Send a `session/fork` request over an existing ACP connection.
|
||||||
|
///
|
||||||
|
/// Returns the full `ForkSessionResponse` so the caller can attach directly
|
||||||
|
/// without a separate `session/load` round-trip.
|
||||||
|
pub async fn fork_session(
|
||||||
|
cx: &ConnectionTo<Agent>,
|
||||||
|
session_id: &SessionId,
|
||||||
|
cwd: &str,
|
||||||
|
) -> Result<ForkSessionResponse, AcpError> {
|
||||||
|
let req = ForkSessionRequest::new(session_id.clone(), cwd);
|
||||||
|
let untyped_req = UntypedMessage::new("session/fork", &req)
|
||||||
|
.map_err(|e| AcpError::protocol(format!("Failed to build fork request: {e}")))?;
|
||||||
|
|
||||||
|
let raw_response: serde_json::Value = cx
|
||||||
|
.send_request_to(Agent, untyped_req)
|
||||||
|
.block_task()
|
||||||
|
.await
|
||||||
|
.map_err(|e| AcpError::protocol(format!("session/fork failed: {e}")))?;
|
||||||
|
|
||||||
|
let response: ForkSessionResponse = serde_json::from_value(raw_response)
|
||||||
|
.map_err(|e| AcpError::protocol(format!("Failed to parse fork response: {e}")))?;
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ use tokio::sync::Mutex;
|
|||||||
|
|
||||||
use crate::acp::connection::{spawn_agent_connection, AgentConnection, ConnectionCommand};
|
use crate::acp::connection::{spawn_agent_connection, AgentConnection, ConnectionCommand};
|
||||||
use crate::acp::error::AcpError;
|
use crate::acp::error::AcpError;
|
||||||
use crate::acp::types::{ConnectionInfo, PromptInputBlock};
|
use crate::acp::types::{ConnectionInfo, ForkResultInfo, PromptInputBlock};
|
||||||
use crate::models::agent::AgentType;
|
use crate::models::agent::AgentType;
|
||||||
|
|
||||||
pub struct ConnectionManager {
|
pub struct ConnectionManager {
|
||||||
@@ -143,6 +143,24 @@ impl ConnectionManager {
|
|||||||
.map_err(|_| AcpError::ProcessExited)
|
.map_err(|_| AcpError::ProcessExited)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn fork_session(&self, conn_id: &str) -> Result<ForkResultInfo, AcpError> {
|
||||||
|
let cmd_tx = {
|
||||||
|
let connections = self.connections.lock().await;
|
||||||
|
let conn = connections
|
||||||
|
.get(conn_id)
|
||||||
|
.ok_or_else(|| AcpError::ConnectionNotFound(conn_id.into()))?;
|
||||||
|
conn.cmd_tx.clone()
|
||||||
|
};
|
||||||
|
let (reply_tx, reply_rx) = tokio::sync::oneshot::channel();
|
||||||
|
cmd_tx
|
||||||
|
.send(ConnectionCommand::Fork { reply: reply_tx })
|
||||||
|
.await
|
||||||
|
.map_err(|_| AcpError::ProcessExited)?;
|
||||||
|
reply_rx
|
||||||
|
.await
|
||||||
|
.map_err(|_| AcpError::protocol("Fork reply channel closed".to_string()))?
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn disconnect(&self, conn_id: &str) -> Result<(), AcpError> {
|
pub async fn disconnect(&self, conn_id: &str) -> Result<(), AcpError> {
|
||||||
let cmd_tx = {
|
let cmd_tx = {
|
||||||
let mut connections = self.connections.lock().await;
|
let mut connections = self.connections.lock().await;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
pub mod binary_cache;
|
pub mod binary_cache;
|
||||||
pub mod connection;
|
pub mod connection;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
pub mod fork;
|
||||||
pub mod file_system_runtime;
|
pub mod file_system_runtime;
|
||||||
pub mod manager;
|
pub mod manager;
|
||||||
pub mod preflight;
|
pub mod preflight;
|
||||||
|
|||||||
@@ -289,3 +289,10 @@ pub struct AvailableCommandInfo {
|
|||||||
pub description: String,
|
pub description: String,
|
||||||
pub input_hint: Option<String>,
|
pub input_hint: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ForkResultInfo {
|
||||||
|
pub forked_session_id: String,
|
||||||
|
pub original_session_id: String,
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use crate::acp::preflight::{self, PreflightResult};
|
|||||||
use crate::acp::registry;
|
use crate::acp::registry;
|
||||||
use crate::acp::types::{
|
use crate::acp::types::{
|
||||||
AcpAgentInfo, AgentSkillContent, AgentSkillItem, AgentSkillLayout, AgentSkillLocation,
|
AcpAgentInfo, AgentSkillContent, AgentSkillItem, AgentSkillLayout, AgentSkillLocation,
|
||||||
AgentSkillScope, AgentSkillsListResult, ConnectionInfo, PromptInputBlock,
|
AgentSkillScope, AgentSkillsListResult, ConnectionInfo, ForkResultInfo, PromptInputBlock,
|
||||||
};
|
};
|
||||||
use crate::db::service::agent_setting_service;
|
use crate::db::service::agent_setting_service;
|
||||||
use crate::db::AppDatabase;
|
use crate::db::AppDatabase;
|
||||||
@@ -1367,6 +1367,14 @@ pub async fn acp_cancel(
|
|||||||
manager.cancel(&connection_id).await
|
manager.cancel(&connection_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn acp_fork(
|
||||||
|
connection_id: String,
|
||||||
|
manager: State<'_, ConnectionManager>,
|
||||||
|
) -> Result<ForkResultInfo, AcpError> {
|
||||||
|
manager.fork_session(&connection_id).await
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn acp_respond_permission(
|
pub async fn acp_respond_permission(
|
||||||
connection_id: String,
|
connection_id: String,
|
||||||
|
|||||||
@@ -264,6 +264,7 @@ pub fn run() {
|
|||||||
acp_commands::acp_set_mode,
|
acp_commands::acp_set_mode,
|
||||||
acp_commands::acp_set_config_option,
|
acp_commands::acp_set_config_option,
|
||||||
acp_commands::acp_cancel,
|
acp_commands::acp_cancel,
|
||||||
|
acp_commands::acp_fork,
|
||||||
acp_commands::acp_respond_permission,
|
acp_commands::acp_respond_permission,
|
||||||
acp_commands::acp_disconnect,
|
acp_commands::acp_disconnect,
|
||||||
acp_commands::acp_list_connections,
|
acp_commands::acp_list_connections,
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ interface ChatInputProps {
|
|||||||
isEditingQueueItem?: boolean
|
isEditingQueueItem?: boolean
|
||||||
onSaveQueueEdit?: (draft: PromptDraft) => void
|
onSaveQueueEdit?: (draft: PromptDraft) => void
|
||||||
onCancelQueueEdit?: () => void
|
onCancelQueueEdit?: () => void
|
||||||
|
onForkSend?: (draft: PromptDraft, modeId?: string | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatInput({
|
export function ChatInput({
|
||||||
@@ -71,6 +72,7 @@ export function ChatInput({
|
|||||||
isEditingQueueItem,
|
isEditingQueueItem,
|
||||||
onSaveQueueEdit,
|
onSaveQueueEdit,
|
||||||
onCancelQueueEdit,
|
onCancelQueueEdit,
|
||||||
|
onForkSend,
|
||||||
}: ChatInputProps) {
|
}: ChatInputProps) {
|
||||||
const t = useTranslations("Folder.chat.chatInput")
|
const t = useTranslations("Folder.chat.chatInput")
|
||||||
const isConnected = status === "connected"
|
const isConnected = status === "connected"
|
||||||
@@ -116,6 +118,7 @@ export function ChatInput({
|
|||||||
isEditingQueueItem={isEditingQueueItem}
|
isEditingQueueItem={isEditingQueueItem}
|
||||||
onSaveQueueEdit={onSaveQueueEdit}
|
onSaveQueueEdit={onSaveQueueEdit}
|
||||||
onCancelQueueEdit={onCancelQueueEdit}
|
onCancelQueueEdit={onCancelQueueEdit}
|
||||||
|
onForkSend={onForkSend}
|
||||||
placeholder={
|
placeholder={
|
||||||
isConnecting
|
isConnecting
|
||||||
? t("connecting")
|
? t("connecting")
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ interface ConversationShellProps {
|
|||||||
isEditingQueueItem?: boolean
|
isEditingQueueItem?: boolean
|
||||||
onSaveQueueEdit?: (draft: PromptDraft) => void
|
onSaveQueueEdit?: (draft: PromptDraft) => void
|
||||||
onCancelQueueEdit?: () => void
|
onCancelQueueEdit?: () => void
|
||||||
|
onForkSend?: (draft: PromptDraft, modeId?: string | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConversationShell({
|
export function ConversationShell({
|
||||||
@@ -88,6 +89,7 @@ export function ConversationShell({
|
|||||||
isEditingQueueItem,
|
isEditingQueueItem,
|
||||||
onSaveQueueEdit,
|
onSaveQueueEdit,
|
||||||
onCancelQueueEdit,
|
onCancelQueueEdit,
|
||||||
|
onForkSend,
|
||||||
}: ConversationShellProps) {
|
}: ConversationShellProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 flex-col">
|
<div className="flex h-full min-h-0 flex-col">
|
||||||
@@ -129,6 +131,7 @@ export function ConversationShell({
|
|||||||
isEditingQueueItem={isEditingQueueItem}
|
isEditingQueueItem={isEditingQueueItem}
|
||||||
onSaveQueueEdit={onSaveQueueEdit}
|
onSaveQueueEdit={onSaveQueueEdit}
|
||||||
onCancelQueueEdit={onCancelQueueEdit}
|
onCancelQueueEdit={onCancelQueueEdit}
|
||||||
|
onForkSend={onForkSend}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -15,14 +15,22 @@ import {
|
|||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
import {
|
import {
|
||||||
Check,
|
Check,
|
||||||
|
ChevronUp,
|
||||||
Ellipsis,
|
Ellipsis,
|
||||||
FileSearch,
|
FileSearch,
|
||||||
|
GitFork,
|
||||||
ListPlus,
|
ListPlus,
|
||||||
Plus,
|
Plus,
|
||||||
Send,
|
Send,
|
||||||
Square,
|
Square,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react"
|
} from "lucide-react"
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { matchShortcutEvent } from "@/lib/keyboard-shortcuts"
|
import { matchShortcutEvent } from "@/lib/keyboard-shortcuts"
|
||||||
import { useShortcutSettings } from "@/hooks/use-shortcut-settings"
|
import { useShortcutSettings } from "@/hooks/use-shortcut-settings"
|
||||||
@@ -76,6 +84,7 @@ interface MessageInputProps {
|
|||||||
isEditingQueueItem?: boolean
|
isEditingQueueItem?: boolean
|
||||||
onSaveQueueEdit?: (draft: PromptDraft) => void
|
onSaveQueueEdit?: (draft: PromptDraft) => void
|
||||||
onCancelQueueEdit?: () => void
|
onCancelQueueEdit?: () => void
|
||||||
|
onForkSend?: (draft: PromptDraft, modeId?: string | null) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResourceInputAttachment {
|
interface ResourceInputAttachment {
|
||||||
@@ -280,6 +289,7 @@ export function MessageInput({
|
|||||||
isEditingQueueItem = false,
|
isEditingQueueItem = false,
|
||||||
onSaveQueueEdit,
|
onSaveQueueEdit,
|
||||||
onCancelQueueEdit,
|
onCancelQueueEdit,
|
||||||
|
onForkSend,
|
||||||
}: MessageInputProps) {
|
}: MessageInputProps) {
|
||||||
const t = useTranslations("Folder.chat.messageInput")
|
const t = useTranslations("Folder.chat.messageInput")
|
||||||
const tQueue = useTranslations("Folder.chat.messageQueue")
|
const tQueue = useTranslations("Folder.chat.messageQueue")
|
||||||
@@ -960,6 +970,24 @@ export function MessageInput({
|
|||||||
effectiveDraftStorageKey,
|
effectiveDraftStorageKey,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const handleForkSendClick = useCallback(() => {
|
||||||
|
if (!onForkSend) return
|
||||||
|
const draft = buildDraft()
|
||||||
|
if (!draft) return
|
||||||
|
onForkSend(draft, showModeSelector ? effectiveModeId : null)
|
||||||
|
if (effectiveDraftStorageKey) {
|
||||||
|
clearMessageInputDraft(effectiveDraftStorageKey)
|
||||||
|
}
|
||||||
|
setText("")
|
||||||
|
setAttachments([])
|
||||||
|
}, [
|
||||||
|
onForkSend,
|
||||||
|
buildDraft,
|
||||||
|
effectiveModeId,
|
||||||
|
showModeSelector,
|
||||||
|
effectiveDraftStorageKey,
|
||||||
|
])
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: React.KeyboardEvent) => {
|
(e: React.KeyboardEvent) => {
|
||||||
if (
|
if (
|
||||||
@@ -1288,6 +1316,35 @@ export function MessageInput({
|
|||||||
<Square className="h-4 w-4" />
|
<Square className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
) : onForkSend ? (
|
||||||
|
<div className="absolute right-2 bottom-2 flex items-center">
|
||||||
|
<Button
|
||||||
|
onClick={handleSend}
|
||||||
|
disabled={disabled || !hasSendableContent}
|
||||||
|
size="icon"
|
||||||
|
className="rounded-r-none"
|
||||||
|
title={t("send")}
|
||||||
|
>
|
||||||
|
<Send className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
disabled={disabled || !hasSendableContent}
|
||||||
|
size="icon"
|
||||||
|
className="rounded-l-none border-l border-primary-foreground/20 w-6"
|
||||||
|
>
|
||||||
|
<ChevronUp className="h-3 w-3" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" side="top">
|
||||||
|
<DropdownMenuItem onSelect={handleForkSendClick}>
|
||||||
|
<GitFork className="h-4 w-4" />
|
||||||
|
{t("forkAndSend")}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
|
|||||||
@@ -24,10 +24,12 @@ import { ConversationShell } from "@/components/chat/conversation-shell"
|
|||||||
import { AgentSelector } from "@/components/chat/agent-selector"
|
import { AgentSelector } from "@/components/chat/agent-selector"
|
||||||
import { ChatInput } from "@/components/chat/chat-input"
|
import { ChatInput } from "@/components/chat/chat-input"
|
||||||
import {
|
import {
|
||||||
|
acpFork,
|
||||||
createConversation,
|
createConversation,
|
||||||
openSettingsWindow,
|
openSettingsWindow,
|
||||||
updateConversationExternalId,
|
updateConversationExternalId,
|
||||||
updateConversationStatus,
|
updateConversationStatus,
|
||||||
|
updateConversationTitle,
|
||||||
} from "@/lib/tauri"
|
} from "@/lib/tauri"
|
||||||
import { useConversationRuntime } from "@/contexts/conversation-runtime-context"
|
import { useConversationRuntime } from "@/contexts/conversation-runtime-context"
|
||||||
import { useConversationDetail } from "@/hooks/use-conversation-detail"
|
import { useConversationDetail } from "@/hooks/use-conversation-detail"
|
||||||
@@ -137,7 +139,8 @@ const ConversationTabView = memo(function ConversationTabView({
|
|||||||
const tWelcome = useTranslations("Folder.chat.welcomeInputPanel")
|
const tWelcome = useTranslations("Folder.chat.welcomeInputPanel")
|
||||||
const sharedT = useTranslations("Folder.chat.shared")
|
const sharedT = useTranslations("Folder.chat.shared")
|
||||||
const { folder, folderId, refreshConversations } = useFolderContext()
|
const { folder, folderId, refreshConversations } = useFolderContext()
|
||||||
const { bindConversationTab, setTabRuntimeConversationId } = useTabContext()
|
const { tabs, bindConversationTab, setTabRuntimeConversationId } =
|
||||||
|
useTabContext()
|
||||||
const { setSessionStats } = useSessionStats()
|
const { setSessionStats } = useSessionStats()
|
||||||
const {
|
const {
|
||||||
appendOptimisticTurn,
|
appendOptimisticTurn,
|
||||||
@@ -608,6 +611,70 @@ const ConversationTabView = memo(function ConversationTabView({
|
|||||||
handleSendRef.current = handleSend
|
handleSendRef.current = handleSend
|
||||||
}, [handleSend])
|
}, [handleSend])
|
||||||
|
|
||||||
|
const handleForkSend = useCallback(
|
||||||
|
async (draft: PromptDraft, selectedModeIdArg?: string | null) => {
|
||||||
|
const connectionId = conn.connectionId
|
||||||
|
if (!connectionId || connStatus !== "connected") return
|
||||||
|
try {
|
||||||
|
const { forkedSessionId, originalSessionId } = await acpFork(
|
||||||
|
connectionId
|
||||||
|
)
|
||||||
|
const persistedId = dbConvIdRef.current
|
||||||
|
if (persistedId != null) {
|
||||||
|
const currentTab = tabs.find((tab) => tab.id === tabId)
|
||||||
|
const currentTitle =
|
||||||
|
currentTab?.title || detail?.summary.title || t("newConversation")
|
||||||
|
// Point current conversation at S2 (forked) and add fork tag
|
||||||
|
await updateConversationExternalId(persistedId, forkedSessionId)
|
||||||
|
await updateConversationTitle(
|
||||||
|
persistedId,
|
||||||
|
`[Fork] ${currentTitle}`
|
||||||
|
)
|
||||||
|
// Save original S1 as a separate conversation with original title
|
||||||
|
const s1ConvId = await createConversation(
|
||||||
|
folderId,
|
||||||
|
selectedAgent,
|
||||||
|
currentTitle
|
||||||
|
)
|
||||||
|
await updateConversationExternalId(s1ConvId, originalSessionId)
|
||||||
|
await updateConversationStatus(s1ConvId, "pending_review")
|
||||||
|
}
|
||||||
|
// Update runtime session id to S2
|
||||||
|
sessionIdRef.current = forkedSessionId
|
||||||
|
setExternalId(effectiveConversationId, forkedSessionId)
|
||||||
|
|
||||||
|
await refreshConversations()
|
||||||
|
// Now send the message on the forked session (S2)
|
||||||
|
handleSend(draft, selectedModeIdArg)
|
||||||
|
} catch (err) {
|
||||||
|
toast.error(
|
||||||
|
t("forkSessionFailed", {
|
||||||
|
error:
|
||||||
|
err instanceof Error
|
||||||
|
? err.message
|
||||||
|
: typeof err === "object" && err !== null
|
||||||
|
? JSON.stringify(err)
|
||||||
|
: String(err),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
conn.connectionId,
|
||||||
|
connStatus,
|
||||||
|
detail?.summary.title,
|
||||||
|
effectiveConversationId,
|
||||||
|
folderId,
|
||||||
|
handleSend,
|
||||||
|
refreshConversations,
|
||||||
|
selectedAgent,
|
||||||
|
setExternalId,
|
||||||
|
t,
|
||||||
|
tabId,
|
||||||
|
tabs,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
const handleOpenAgentsSettings = useCallback(() => {
|
const handleOpenAgentsSettings = useCallback(() => {
|
||||||
openSettingsWindow("agents", { agentType: selectedAgent }).catch((err) => {
|
openSettingsWindow("agents", { agentType: selectedAgent }).catch((err) => {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -757,6 +824,11 @@ const ConversationTabView = memo(function ConversationTabView({
|
|||||||
isEditingQueueItem={mqEditingItemId != null}
|
isEditingQueueItem={mqEditingItemId != null}
|
||||||
onSaveQueueEdit={handleSaveQueueEdit}
|
onSaveQueueEdit={handleSaveQueueEdit}
|
||||||
onCancelQueueEdit={handleQueueCancelEdit}
|
onCancelQueueEdit={handleQueueCancelEdit}
|
||||||
|
onForkSend={
|
||||||
|
connStatus === "connected" && hasPersistedConversation
|
||||||
|
? handleForkSend
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{isWelcomeMode ? (
|
{isWelcomeMode ? (
|
||||||
<div className="flex h-full min-h-0 flex-col items-center justify-center">
|
<div className="flex h-full min-h-0 flex-col items-center justify-center">
|
||||||
|
|||||||
@@ -624,7 +624,10 @@
|
|||||||
"reloaded": "تمت إعادة تحميل المحادثة",
|
"reloaded": "تمت إعادة تحميل المحادثة",
|
||||||
"reload": "إعادة تحميل",
|
"reload": "إعادة تحميل",
|
||||||
"newConversation": "محادثة جديدة",
|
"newConversation": "محادثة جديدة",
|
||||||
"closeConversation": "إغلاق المحادثة"
|
"closeConversation": "إغلاق المحادثة",
|
||||||
|
"forkSession": "تفريع الجلسة",
|
||||||
|
"forkSessionSuccess": "تم تفريع الجلسة بنجاح",
|
||||||
|
"forkSessionFailed": "فشل في تفريع الجلسة: {error}"
|
||||||
},
|
},
|
||||||
"conversationCard": {
|
"conversationCard": {
|
||||||
"untitledConversation": "محادثة بدون عنوان",
|
"untitledConversation": "محادثة بدون عنوان",
|
||||||
@@ -1197,7 +1200,8 @@
|
|||||||
"loadingSettings": "جارٍ تحميل الإعدادات...",
|
"loadingSettings": "جارٍ تحميل الإعدادات...",
|
||||||
"loadingMode": "جارٍ تحميل الوضع...",
|
"loadingMode": "جارٍ تحميل الوضع...",
|
||||||
"cancel": "إلغاء",
|
"cancel": "إلغاء",
|
||||||
"send": "إرسال"
|
"send": "إرسال",
|
||||||
|
"forkAndSend": "تفريع وإرسال"
|
||||||
},
|
},
|
||||||
"messageQueue": {
|
"messageQueue": {
|
||||||
"addToQueue": "إضافة للقائمة",
|
"addToQueue": "إضافة للقائمة",
|
||||||
|
|||||||
@@ -624,7 +624,10 @@
|
|||||||
"reloaded": "Konversation neu geladen",
|
"reloaded": "Konversation neu geladen",
|
||||||
"reload": "Neu laden",
|
"reload": "Neu laden",
|
||||||
"newConversation": "Neue Konversation",
|
"newConversation": "Neue Konversation",
|
||||||
"closeConversation": "Konversation schließen"
|
"closeConversation": "Konversation schließen",
|
||||||
|
"forkSession": "Sitzung forken",
|
||||||
|
"forkSessionSuccess": "Sitzung erfolgreich geforkt",
|
||||||
|
"forkSessionFailed": "Sitzung konnte nicht geforkt werden: {error}"
|
||||||
},
|
},
|
||||||
"conversationCard": {
|
"conversationCard": {
|
||||||
"untitledConversation": "Unbenannte Konversation",
|
"untitledConversation": "Unbenannte Konversation",
|
||||||
@@ -1197,7 +1200,8 @@
|
|||||||
"loadingSettings": "Einstellungen werden geladen...",
|
"loadingSettings": "Einstellungen werden geladen...",
|
||||||
"loadingMode": "Modus wird geladen...",
|
"loadingMode": "Modus wird geladen...",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"send": "Senden"
|
"send": "Senden",
|
||||||
|
"forkAndSend": "Fork & Senden"
|
||||||
},
|
},
|
||||||
"messageQueue": {
|
"messageQueue": {
|
||||||
"addToQueue": "Zur Warteschlange",
|
"addToQueue": "Zur Warteschlange",
|
||||||
|
|||||||
@@ -624,7 +624,10 @@
|
|||||||
"reloaded": "Conversation reloaded",
|
"reloaded": "Conversation reloaded",
|
||||||
"reload": "Reload",
|
"reload": "Reload",
|
||||||
"newConversation": "New Conversation",
|
"newConversation": "New Conversation",
|
||||||
"closeConversation": "Close Conversation"
|
"closeConversation": "Close Conversation",
|
||||||
|
"forkSession": "Fork Session",
|
||||||
|
"forkSessionSuccess": "Session forked successfully",
|
||||||
|
"forkSessionFailed": "Failed to fork session: {error}"
|
||||||
},
|
},
|
||||||
"conversationCard": {
|
"conversationCard": {
|
||||||
"untitledConversation": "Untitled conversation",
|
"untitledConversation": "Untitled conversation",
|
||||||
@@ -1197,7 +1200,8 @@
|
|||||||
"loadingSettings": "Loading settings...",
|
"loadingSettings": "Loading settings...",
|
||||||
"loadingMode": "Loading mode...",
|
"loadingMode": "Loading mode...",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"send": "Send"
|
"send": "Send",
|
||||||
|
"forkAndSend": "Fork & Send"
|
||||||
},
|
},
|
||||||
"messageQueue": {
|
"messageQueue": {
|
||||||
"addToQueue": "Queue message",
|
"addToQueue": "Queue message",
|
||||||
|
|||||||
@@ -624,7 +624,10 @@
|
|||||||
"reloaded": "Conversación recargada",
|
"reloaded": "Conversación recargada",
|
||||||
"reload": "Recargar",
|
"reload": "Recargar",
|
||||||
"newConversation": "Nueva conversación",
|
"newConversation": "Nueva conversación",
|
||||||
"closeConversation": "Cerrar conversación"
|
"closeConversation": "Cerrar conversación",
|
||||||
|
"forkSession": "Bifurcar sesión",
|
||||||
|
"forkSessionSuccess": "Sesión bifurcada exitosamente",
|
||||||
|
"forkSessionFailed": "Error al bifurcar la sesión: {error}"
|
||||||
},
|
},
|
||||||
"conversationCard": {
|
"conversationCard": {
|
||||||
"untitledConversation": "Conversación sin título",
|
"untitledConversation": "Conversación sin título",
|
||||||
@@ -1197,7 +1200,8 @@
|
|||||||
"loadingSettings": "Cargando ajustes...",
|
"loadingSettings": "Cargando ajustes...",
|
||||||
"loadingMode": "Cargando modo...",
|
"loadingMode": "Cargando modo...",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"send": "Enviar"
|
"send": "Enviar",
|
||||||
|
"forkAndSend": "Fork y Enviar"
|
||||||
},
|
},
|
||||||
"messageQueue": {
|
"messageQueue": {
|
||||||
"addToQueue": "Agregar a la cola",
|
"addToQueue": "Agregar a la cola",
|
||||||
|
|||||||
@@ -624,7 +624,10 @@
|
|||||||
"reloaded": "Conversation rechargée",
|
"reloaded": "Conversation rechargée",
|
||||||
"reload": "Recharger",
|
"reload": "Recharger",
|
||||||
"newConversation": "Nouvelle conversation",
|
"newConversation": "Nouvelle conversation",
|
||||||
"closeConversation": "Fermer la conversation"
|
"closeConversation": "Fermer la conversation",
|
||||||
|
"forkSession": "Dupliquer la session",
|
||||||
|
"forkSessionSuccess": "Session dupliquée avec succès",
|
||||||
|
"forkSessionFailed": "Échec de la duplication de la session : {error}"
|
||||||
},
|
},
|
||||||
"conversationCard": {
|
"conversationCard": {
|
||||||
"untitledConversation": "Conversation sans titre",
|
"untitledConversation": "Conversation sans titre",
|
||||||
@@ -1197,7 +1200,8 @@
|
|||||||
"loadingSettings": "Chargement des paramètres...",
|
"loadingSettings": "Chargement des paramètres...",
|
||||||
"loadingMode": "Chargement du mode...",
|
"loadingMode": "Chargement du mode...",
|
||||||
"cancel": "Annuler",
|
"cancel": "Annuler",
|
||||||
"send": "Envoyer"
|
"send": "Envoyer",
|
||||||
|
"forkAndSend": "Fork & Envoyer"
|
||||||
},
|
},
|
||||||
"messageQueue": {
|
"messageQueue": {
|
||||||
"addToQueue": "Mettre en file",
|
"addToQueue": "Mettre en file",
|
||||||
|
|||||||
@@ -624,7 +624,10 @@
|
|||||||
"reloaded": "会話を再読み込みしました",
|
"reloaded": "会話を再読み込みしました",
|
||||||
"reload": "再読み込み",
|
"reload": "再読み込み",
|
||||||
"newConversation": "新しい会話",
|
"newConversation": "新しい会話",
|
||||||
"closeConversation": "会話を閉じる"
|
"closeConversation": "会話を閉じる",
|
||||||
|
"forkSession": "セッションをフォーク",
|
||||||
|
"forkSessionSuccess": "セッションのフォークに成功しました",
|
||||||
|
"forkSessionFailed": "セッションのフォークに失敗しました:{error}"
|
||||||
},
|
},
|
||||||
"conversationCard": {
|
"conversationCard": {
|
||||||
"untitledConversation": "無題の会話",
|
"untitledConversation": "無題の会話",
|
||||||
@@ -1197,7 +1200,8 @@
|
|||||||
"loadingSettings": "設定を読み込み中...",
|
"loadingSettings": "設定を読み込み中...",
|
||||||
"loadingMode": "モードを読み込み中...",
|
"loadingMode": "モードを読み込み中...",
|
||||||
"cancel": "キャンセル",
|
"cancel": "キャンセル",
|
||||||
"send": "送信"
|
"send": "送信",
|
||||||
|
"forkAndSend": "フォークして送信"
|
||||||
},
|
},
|
||||||
"messageQueue": {
|
"messageQueue": {
|
||||||
"addToQueue": "キューに追加",
|
"addToQueue": "キューに追加",
|
||||||
|
|||||||
@@ -624,7 +624,10 @@
|
|||||||
"reloaded": "대화를 다시 불러왔습니다",
|
"reloaded": "대화를 다시 불러왔습니다",
|
||||||
"reload": "다시 불러오기",
|
"reload": "다시 불러오기",
|
||||||
"newConversation": "새 대화",
|
"newConversation": "새 대화",
|
||||||
"closeConversation": "대화 닫기"
|
"closeConversation": "대화 닫기",
|
||||||
|
"forkSession": "세션 포크",
|
||||||
|
"forkSessionSuccess": "세션 포크 성공",
|
||||||
|
"forkSessionFailed": "세션 포크 실패: {error}"
|
||||||
},
|
},
|
||||||
"conversationCard": {
|
"conversationCard": {
|
||||||
"untitledConversation": "제목 없는 대화",
|
"untitledConversation": "제목 없는 대화",
|
||||||
@@ -1197,7 +1200,8 @@
|
|||||||
"loadingSettings": "설정 불러오는 중...",
|
"loadingSettings": "설정 불러오는 중...",
|
||||||
"loadingMode": "모드 불러오는 중...",
|
"loadingMode": "모드 불러오는 중...",
|
||||||
"cancel": "취소",
|
"cancel": "취소",
|
||||||
"send": "보내기"
|
"send": "보내기",
|
||||||
|
"forkAndSend": "포크 & 전송"
|
||||||
},
|
},
|
||||||
"messageQueue": {
|
"messageQueue": {
|
||||||
"addToQueue": "대기열에 추가",
|
"addToQueue": "대기열에 추가",
|
||||||
|
|||||||
@@ -624,7 +624,10 @@
|
|||||||
"reloaded": "Conversa recarregada",
|
"reloaded": "Conversa recarregada",
|
||||||
"reload": "Recarregar",
|
"reload": "Recarregar",
|
||||||
"newConversation": "Nova conversa",
|
"newConversation": "Nova conversa",
|
||||||
"closeConversation": "Fechar conversa"
|
"closeConversation": "Fechar conversa",
|
||||||
|
"forkSession": "Bifurcar sessão",
|
||||||
|
"forkSessionSuccess": "Sessão bifurcada com sucesso",
|
||||||
|
"forkSessionFailed": "Falha ao bifurcar a sessão: {error}"
|
||||||
},
|
},
|
||||||
"conversationCard": {
|
"conversationCard": {
|
||||||
"untitledConversation": "Conversa sem título",
|
"untitledConversation": "Conversa sem título",
|
||||||
@@ -1197,7 +1200,8 @@
|
|||||||
"loadingSettings": "Carregando configurações...",
|
"loadingSettings": "Carregando configurações...",
|
||||||
"loadingMode": "Carregando modo...",
|
"loadingMode": "Carregando modo...",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"send": "Enviar"
|
"send": "Enviar",
|
||||||
|
"forkAndSend": "Fork & Enviar"
|
||||||
},
|
},
|
||||||
"messageQueue": {
|
"messageQueue": {
|
||||||
"addToQueue": "Adicionar à fila",
|
"addToQueue": "Adicionar à fila",
|
||||||
|
|||||||
@@ -624,7 +624,10 @@
|
|||||||
"reloaded": "当前会话已重新加载",
|
"reloaded": "当前会话已重新加载",
|
||||||
"reload": "重新加载",
|
"reload": "重新加载",
|
||||||
"newConversation": "新建会话",
|
"newConversation": "新建会话",
|
||||||
"closeConversation": "关闭会话"
|
"closeConversation": "关闭会话",
|
||||||
|
"forkSession": "分叉会话",
|
||||||
|
"forkSessionSuccess": "会话分叉成功",
|
||||||
|
"forkSessionFailed": "会话分叉失败:{error}"
|
||||||
},
|
},
|
||||||
"conversationCard": {
|
"conversationCard": {
|
||||||
"untitledConversation": "未命名会话",
|
"untitledConversation": "未命名会话",
|
||||||
@@ -1197,7 +1200,8 @@
|
|||||||
"loadingSettings": "正在加载设置...",
|
"loadingSettings": "正在加载设置...",
|
||||||
"loadingMode": "正在加载模式...",
|
"loadingMode": "正在加载模式...",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"send": "发送"
|
"send": "发送",
|
||||||
|
"forkAndSend": "分叉发送"
|
||||||
},
|
},
|
||||||
"messageQueue": {
|
"messageQueue": {
|
||||||
"addToQueue": "加入队列",
|
"addToQueue": "加入队列",
|
||||||
|
|||||||
@@ -624,7 +624,10 @@
|
|||||||
"reloaded": "目前會話已重新載入",
|
"reloaded": "目前會話已重新載入",
|
||||||
"reload": "重新載入",
|
"reload": "重新載入",
|
||||||
"newConversation": "新增會話",
|
"newConversation": "新增會話",
|
||||||
"closeConversation": "關閉會話"
|
"closeConversation": "關閉會話",
|
||||||
|
"forkSession": "分叉會話",
|
||||||
|
"forkSessionSuccess": "會話分叉成功",
|
||||||
|
"forkSessionFailed": "會話分叉失敗:{error}"
|
||||||
},
|
},
|
||||||
"conversationCard": {
|
"conversationCard": {
|
||||||
"untitledConversation": "未命名會話",
|
"untitledConversation": "未命名會話",
|
||||||
@@ -1197,7 +1200,8 @@
|
|||||||
"loadingSettings": "正在載入設定...",
|
"loadingSettings": "正在載入設定...",
|
||||||
"loadingMode": "正在載入模式...",
|
"loadingMode": "正在載入模式...",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"send": "傳送"
|
"send": "傳送",
|
||||||
|
"forkAndSend": "分叉發送"
|
||||||
},
|
},
|
||||||
"messageQueue": {
|
"messageQueue": {
|
||||||
"addToQueue": "加入佇列",
|
"addToQueue": "加入佇列",
|
||||||
|
|||||||
@@ -119,6 +119,15 @@ export async function acpCancel(connectionId: string): Promise<void> {
|
|||||||
return invoke("acp_cancel", { connectionId })
|
return invoke("acp_cancel", { connectionId })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ForkResult {
|
||||||
|
forkedSessionId: string
|
||||||
|
originalSessionId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function acpFork(connectionId: string): Promise<ForkResult> {
|
||||||
|
return invoke("acp_fork", { connectionId })
|
||||||
|
}
|
||||||
|
|
||||||
export async function acpRespondPermission(
|
export async function acpRespondPermission(
|
||||||
connectionId: string,
|
connectionId: string,
|
||||||
requestId: string,
|
requestId: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user