Reduce Linux setup ambiguity and surface desktop input/message failures honestly

Clarify ONNX Runtime guidance with direct-open install hints, restore desktop WebRTC VAD visibility, map mouse side buttons through focused PTT capture/runtime paths, and wait for server acks before showing chat sends as successful.

Constraint: Linux release UX must stay functional when ONNX Runtime is optional and GNOME portal availability varies
Rejected: Keep desktop VAD locked to Silero only | misleads users when ONNX Runtime is skipped
Confidence: medium
Scope-risk: moderate
Directive: Preserve the protocol send-ack wait path for chat so UI success always tracks real server acceptance
Tested: flutter analyze lib/main.dart lib/widgets/chat_views.dart lib/widgets/input_dialogs.dart lib/widgets/startup_dependency_screen.dart; flutter test test/widgets/input_dialogs_test.dart test/widgets/chat_views_test.dart test/services/startup_dependency_check_test.dart test/widgets/startup_dependency_screen_test.dart test/widgets/voice_settings_controls_test.dart test/widgets/audio_processing_config_state_test.dart; cargo test -p chanora_protocol --lib; cargo test -p chanora_audio ptt_backends --lib
Not-tested: Live manual GNOME portal rebind/global PTT on a real desktop session; observer-bot chat against a live server after the sender-name fallback change
This commit is contained in:
Edison Jwa
2026-05-25 11:55:10 +09:00
parent 5c8c6df16b
commit b8df25a195
16 changed files with 1635 additions and 391 deletions
@@ -105,7 +105,7 @@ fn is_gnome_on_wayland() -> bool {
trait BlockingGlobalShortcuts {
/// Version property (read on the blocking proxy as a fast
/// reachability probe).
#[zbus(property)]
#[zbus(property, name = "version")]
fn version(&self) -> zbus::Result<u32>;
}
+75 -39
View File
@@ -618,6 +618,14 @@ async fn connection_task(
std::time::Instant,
),
> = HashMap::new();
let mut pending_text_messages: HashMap<
MessageHandle,
(
MessageTarget,
oneshot::Sender<Result<(), ProtocolError>>,
std::time::Instant,
),
> = HashMap::new();
let mut voice_activity: HashMap<u64, Instant> = HashMap::new();
// Main loop: pump events, service requests, forward voice.
@@ -726,28 +734,7 @@ async fn connection_task(
if let Some((_target_channel, reply, _deadline)) =
pending_moves.remove(&handle)
{
let mapped = match result {
Ok(()) => Ok(()),
Err(cmd_err) => {
// tsclientlib's CommandError carries a
// typed `TsError` (the canonical TS3
// error code) plus an optional missing
// permission. We convert to our typed
// ProtocolError::ServerRejected so the
// upper layers can render a localised
// explanation by code instead of a
// generic backend string.
let code = cmd_err.error as u32;
let message = cmd_err.error.to_string();
info!(
target: "chanora_protocol",
code,
message = %message,
"server rejected client_move"
);
Err(ProtocolError::ServerRejected { code, message })
}
};
let mapped = map_command_result(result, "client_move");
if let Some(reply) = reply {
let _ = reply.send(mapped);
} else if let Err(err) = mapped {
@@ -757,6 +744,11 @@ async fn connection_task(
"client_move completed in background with error"
);
}
} else if let Some((_target, reply, _deadline)) =
pending_text_messages.remove(&handle)
{
let mapped = map_command_result(result, "text_message");
let _ = reply.send(mapped);
}
}
_ => { /* book / message / other events: ignore */ }
@@ -801,6 +793,24 @@ async fn connection_task(
}
}
}
if !pending_text_messages.is_empty() {
let now = std::time::Instant::now();
let expired: Vec<MessageHandle> = pending_text_messages
.iter()
.filter_map(|(handle, (_, _, deadline))| {
if now >= *deadline {
Some(*handle)
} else {
None
}
})
.collect();
for handle in expired {
if let Some((_target, reply, _)) = pending_text_messages.remove(&handle) {
let _ = reply.send(Err(ProtocolError::Timeout));
}
}
}
// 3. Service at most one control request (non-blocking).
match rx.try_recv() {
@@ -850,10 +860,15 @@ async fn connection_task(
message,
target,
reply,
}) => {
let r = send_text_message(&mut con, &message, target);
let _ = reply.send(r);
}
}) => match send_text_message(&mut con, &message, target) {
Ok(handle) => {
let deadline = std::time::Instant::now() + Duration::from_secs(3);
pending_text_messages.insert(handle, (target, reply, deadline));
}
Err(e) => {
let _ = reply.send(Err(e));
}
},
Ok(Request::Disconnect(reply)) => {
let _ = con.disconnect(DisconnectOptions::new());
con.events().for_each(|_| future::ready(())).await;
@@ -957,43 +972,43 @@ fn send_text_message(
con: &mut Connection,
message: &str,
target: MessageTarget,
) -> Result<(), ProtocolError> {
) -> Result<MessageHandle, ProtocolError> {
use tsproto_types::TextMessageTargetMode;
match target {
MessageTarget::Server => {
send_text_to_mode(con, message, TextMessageTargetMode::Server, "server")?;
}
MessageTarget::Server => send_text_to_mode(con, message, TextMessageTargetMode::Server, "server"),
MessageTarget::Channel => {
// Fix: previously channel messages were sent via
// state.server.send_textmessage() which always uses
// TextMessageTargetMode::Server. Now correctly uses
// TextMessageTargetMode::Channel so the message is
// scoped to the current channel, not server-wide.
send_text_to_mode(con, message, TextMessageTargetMode::Channel, "channel")?;
send_text_to_mode(con, message, TextMessageTargetMode::Channel, "channel")
}
MessageTarget::Client(client_id) => {
let state = con
.get_state()
.map_err(|e| ProtocolError::Backend(format!("get_state: {e}")))?;
let client = find_client_by_id(state.clients.values(), client_id)?;
client
let handle = client
.send_textmessage(message)
.send(con)
.send_with_result(con)
.map_err(|e| ProtocolError::Backend(format!("send_textmessage(client): {e}")))?;
info!(target: "chanora_protocol", len = message.len(), ?target, "text message queued");
Ok(handle)
}
MessageTarget::Poke(client_id) => {
let state = con
.get_state()
.map_err(|e| ProtocolError::Backend(format!("get_state: {e}")))?;
let client = find_client_by_id(state.clients.values(), client_id)?;
client
let handle = client
.poke(message)
.send(con)
.send_with_result(con)
.map_err(|e| ProtocolError::Backend(format!("poke: {e}")))?;
info!(target: "chanora_protocol", len = message.len(), ?target, "text message queued");
Ok(handle)
}
}
info!(target: "chanora_protocol", len = message.len(), ?target, "text message sent");
Ok(())
}
fn send_text_to_mode(
@@ -1001,7 +1016,7 @@ fn send_text_to_mode(
message: &str,
target: tsproto_types::TextMessageTargetMode,
label: &str,
) -> Result<(), ProtocolError> {
) -> Result<MessageHandle, ProtocolError> {
use ts_bookkeeping::messages::c2s;
c2s::OutSendTextMessageMessage::new(&mut std::iter::once(c2s::OutSendTextMessagePart {
@@ -1009,10 +1024,31 @@ fn send_text_to_mode(
target_client_id: None,
message: message.into(),
}))
.send(con)
.send_with_result(con)
.map_err(|e| ProtocolError::Backend(format!("send_textmessage({label}): {e}")))
}
fn map_command_result(
result: Result<(), tsclientlib::CommandError>,
action: &str,
) -> Result<(), ProtocolError> {
match result {
Ok(()) => Ok(()),
Err(cmd_err) => {
let code = cmd_err.error as u32;
let message = cmd_err.error.to_string();
info!(
target: "chanora_protocol",
action,
code,
message = %message,
"server rejected command"
);
Err(ProtocolError::ServerRejected { code, message })
}
}
}
fn find_client_by_id<'a>(
clients: impl IntoIterator<Item = &'a Client>,
client_id: u64,