Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 2 additions & 15 deletions clawpal-core/src/ssh/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,18 +96,6 @@ impl SshSession {
if config.host.trim().is_empty() {
return Err(SshError::InvalidConfig("host is empty".to_string()));
}
if config.auth_method.trim().eq_ignore_ascii_case("password")
&& config
.password
.as_deref()
.map(str::trim)
.filter(|v| !v.is_empty())
.is_none()
{
return Err(SshError::InvalidConfig(
"password auth selected but password is empty".to_string(),
));
}
let backend = match connect_and_auth(config, passphrase).await {
Ok((handle, _)) => Backend::Russh {
handle: Arc::new(handle),
Expand Down Expand Up @@ -457,11 +445,10 @@ async fn connect_and_auth(
.map_err(|e| SshError::Connect(e.to_string()))?;

if config.auth_method.trim().eq_ignore_ascii_case("password") {
let password = config
.password
.as_deref()
let password = passphrase
.map(str::trim)
.filter(|v| !v.is_empty())
.or_else(|| config.password.as_deref().map(str::trim).filter(|v| !v.is_empty()))
.ok_or_else(|| {
SshError::InvalidConfig("password auth selected but password is empty".to_string())
})?;
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ uuid = { version = "1.11.0", features = ["v4"] }
chrono = { version = "0.4.38", features = ["clock"] }
base64 = "0.22"
ed25519-dalek = { version = "2", features = ["pkcs8", "pem"] }
tokio = { version = "1", features = ["sync", "process", "macros"] }
tokio = { version = "1", features = ["sync", "process", "macros", "io-util"] }
tokio-tungstenite = { version = "0.24", features = ["rustls-tls-webpki-roots"] }
futures-util = "0.3"
shellexpand = "3.1"
Expand Down
28 changes: 21 additions & 7 deletions src-tauri/src/doctor_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ use tauri::{AppHandle, Emitter, State};

use crate::doctor_runtime_bridge::emit_runtime_event;
use crate::models::resolve_paths;
use crate::runtime::types::{
RuntimeAdapter, RuntimeDomain, RuntimeError, RuntimeEvent, RuntimeSessionKey,
};
use crate::runtime::types::{RuntimeDomain, RuntimeError, RuntimeEvent, RuntimeSessionKey};
use crate::runtime::zeroclaw::adapter::ZeroclawDoctorAdapter;
use crate::runtime::zeroclaw::install_adapter::ZeroclawInstallAdapter;
use crate::ssh::SshConnectionPool;
Expand Down Expand Up @@ -106,7 +104,11 @@ pub async fn doctor_start_diagnosis(
session_key.clone(),
);
let adapter = ZeroclawDoctorAdapter;
match adapter.start(&key, &context) {
let app_clone = app.clone();
let on_delta = move |text: &str| {
emit_runtime_event(&app_clone, RuntimeEvent::chat_delta(text.to_string()));
};
match adapter.start_streaming(&key, &context, on_delta).await {
Ok(events) => {
for ev in events {
register_runtime_invoke(&ev);
Expand Down Expand Up @@ -145,7 +147,11 @@ pub async fn doctor_send_message(
session_key.clone(),
);
let adapter = ZeroclawDoctorAdapter;
match adapter.send(&key, &message) {
let app_clone = app.clone();
let on_delta = move |text: &str| {
emit_runtime_event(&app_clone, RuntimeEvent::chat_delta(text.to_string()));
};
match adapter.send_streaming(&key, &message, on_delta).await {
Ok(events) => {
for ev in events {
register_runtime_invoke(&ev);
Expand Down Expand Up @@ -256,10 +262,18 @@ pub async fn doctor_approve_invoke(
agent_id.clone(),
session_key.clone(),
);
let app_clone = app.clone();
let on_delta = move |text: &str| {
emit_runtime_event(&app_clone, RuntimeEvent::chat_delta(text.to_string()));
};
let send_result = if is_install {
ZeroclawInstallAdapter.send(&key, &result_text)
ZeroclawInstallAdapter
.send_streaming(&key, &result_text, on_delta)
.await
} else {
ZeroclawDoctorAdapter.send(&key, &result_text)
ZeroclawDoctorAdapter
.send_streaming(&key, &result_text, on_delta)
.await
};
let events = match handle_runtime_send_result(rt_domain.as_str(), send_result) {
Ok(events) => events,
Expand Down
14 changes: 11 additions & 3 deletions src-tauri/src/install_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use tauri::AppHandle;

use crate::doctor_commands::register_runtime_invoke;
use crate::doctor_runtime_bridge::emit_runtime_event;
use crate::runtime::types::{RuntimeAdapter, RuntimeDomain, RuntimeEvent, RuntimeSessionKey};
use crate::runtime::types::{RuntimeDomain, RuntimeEvent, RuntimeSessionKey};
use crate::runtime::zeroclaw::install_adapter::ZeroclawInstallAdapter;

#[tauri::command]
Expand All @@ -22,7 +22,11 @@ pub async fn install_start_session(
session_key.clone(),
);
let adapter = ZeroclawInstallAdapter;
match adapter.start(&key, &context) {
let app_clone = app.clone();
let on_delta = move |text: &str| {
emit_runtime_event(&app_clone, RuntimeEvent::chat_delta(text.to_string()));
};
match adapter.start_streaming(&key, &context, on_delta).await {
Ok(events) => {
for ev in events {
register_runtime_invoke(&ev);
Expand Down Expand Up @@ -55,7 +59,11 @@ pub async fn install_send_message(
session_key.clone(),
);
let adapter = ZeroclawInstallAdapter;
match adapter.send(&key, &message) {
let app_clone = app.clone();
let on_delta = move |text: &str| {
emit_runtime_event(&app_clone, RuntimeEvent::chat_delta(text.to_string()));
};
match adapter.send_streaming(&key, &message, on_delta).await {
Ok(events) => {
for ev in events {
register_runtime_invoke(&ev);
Expand Down
54 changes: 54 additions & 0 deletions src-tauri/src/runtime/zeroclaw/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use serde_json::Value;

use super::process::run_zeroclaw_message;
use super::session::{append_history, build_prompt_with_history, reset_history};
use super::streaming::run_zeroclaw_streaming_turn;

pub struct ZeroclawDoctorAdapter;

Expand Down Expand Up @@ -116,6 +117,59 @@ impl ZeroclawDoctorAdapter {
}
}

impl ZeroclawDoctorAdapter {
pub async fn start_streaming<F>(
&self,
key: &RuntimeSessionKey,
message: &str,
on_delta: F,
) -> Result<Vec<RuntimeEvent>, RuntimeError>
where
F: Fn(&str) + Send + Sync + 'static,
{
let prompt = Self::doctor_domain_prompt(key, message);
let assistant_events = run_zeroclaw_streaming_turn(
key,
&prompt,
true,
None,
on_delta,
Self::normalize_doctor_output,
Self::parse_tool_intent,
Self::map_error,
)
.await?;
let session_key = key.storage_key();
append_history(&session_key, "system", &prompt);
Comment on lines +141 to +143

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Persist system prompt before streaming the first turn

In start_streaming, the call to run_zeroclaw_streaming_turn completes before the system prompt is written to history, but run_zeroclaw_streaming_turn itself appends the assistant reply immediately (streaming.rs), so the stored transcript order becomes assistant→system instead of system→assistant. Subsequent build_prompt_with_history* calls replay that reversed order, which can distort later turns because prior assistant content is shown before the governing system instruction; the same ordering bug appears in both doctor and install streaming start paths.

Useful? React with 👍 / 👎.

Ok(assistant_events)
}

pub async fn send_streaming<F>(
&self,
key: &RuntimeSessionKey,
message: &str,
on_delta: F,
) -> Result<Vec<RuntimeEvent>, RuntimeError>
where
F: Fn(&str) + Send + Sync + 'static,
{
let prompt = build_prompt_with_history(&key.storage_key(), message);
let guarded = Self::doctor_domain_prompt(key, &prompt);
let assistant_events = run_zeroclaw_streaming_turn(
key,
&guarded,
false,
Some(message),
on_delta,
Self::normalize_doctor_output,
Self::parse_tool_intent,
Self::map_error,
)
.await?;
Ok(assistant_events)
}
}

impl RuntimeAdapter for ZeroclawDoctorAdapter {
fn engine_name(&self) -> &'static str {
"zeroclaw"
Expand Down
58 changes: 58 additions & 0 deletions src-tauri/src/runtime/zeroclaw/install_adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use serde_json::json;

use super::process::run_zeroclaw_message;
use super::session::{append_history, build_prompt_with_history_preamble, reset_history};
use super::streaming::run_zeroclaw_streaming_turn;

pub struct ZeroclawInstallAdapter;

Expand Down Expand Up @@ -67,6 +68,63 @@ impl ZeroclawInstallAdapter {
}
}

impl ZeroclawInstallAdapter {
pub async fn start_streaming<F>(
&self,
key: &RuntimeSessionKey,
message: &str,
on_delta: F,
) -> Result<Vec<RuntimeEvent>, RuntimeError>
where
F: Fn(&str) + Send + Sync + 'static,
{
let session_key = key.storage_key();
reset_history(&session_key);
let prompt = Self::install_domain_prompt(key, message);
let assistant_events = run_zeroclaw_streaming_turn(
key,
&prompt,
true,
None,
on_delta,
|text| text,
Self::parse_tool_intent,
Self::map_error,
)
.await?;
append_history(&session_key, "system", &prompt);
Ok(assistant_events)
}

pub async fn send_streaming<F>(
&self,
key: &RuntimeSessionKey,
message: &str,
on_delta: F,
) -> Result<Vec<RuntimeEvent>, RuntimeError>
where
F: Fn(&str) + Send + Sync + 'static,
{
let session_key = key.storage_key();
append_history(&session_key, "user", message);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid double-writing install user turns to history

send_streaming appends the user message before building the prompt, and then passes Some(message) into run_zeroclaw_streaming_turn, which appends the same user turn again (streaming.rs does this in its user_message branch). This means every install follow-up is stored twice, so subsequent prompts contain duplicated user history and quickly bloat/warp the install conversation state.

Useful? React with 👍 / 👎.

let preamble = format!("{}\n", crate::prompt_templates::install_history_preamble());
let prompt = build_prompt_with_history_preamble(&session_key, message, &preamble);
let guarded = Self::install_domain_prompt(key, &prompt);
let assistant_events = run_zeroclaw_streaming_turn(
key,
&guarded,
false,
Some(message),
on_delta,
|text| text,
Self::parse_tool_intent,
Self::map_error,
)
.await?;
Ok(assistant_events)
}
}

impl RuntimeAdapter for ZeroclawInstallAdapter {
fn engine_name(&self) -> &'static str {
"zeroclaw"
Expand Down
1 change: 1 addition & 0 deletions src-tauri/src/runtime/zeroclaw/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod adapter;
mod streaming;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BS: cargo fmt 要求 mod streaming 按字母序排在 pub mod session 之后。跑 cargo fmt --all 会自动修复。

pub mod install_adapter;
pub mod process;
pub mod sanitize;
Expand Down
Loading
Loading