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:
@@ -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>;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user