Skip to content

Commit 5c48db0

Browse files
authored
feat(tui): discover server via service.json, verify server.info, and format tagged RPC errors (#93)
Resolve the RPC endpoint from `<home>/service.json` (`CODEWORK_HOME_DIR` or `~/.codework`) when `CODEWORK_RPC_SERVER` is unset, probe `server.info` on startup to confirm `pid` and `version` match the active registration, and format `_tag`-discriminated RPC errors into human-readable messages.
1 parent 20b4b21 commit 5c48db0

4 files changed

Lines changed: 924 additions & 20 deletions

File tree

‎packages/tui/src/app.rs‎

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ use crate::prompt::Prompt;
1717
use crate::rpc::{RpcClient, StreamEvent};
1818
use crate::theme::{self, Palette};
1919
use crate::tool::ToolCall;
20-
use crate::types::{HistoryEntry, ModelList, Part, SessionInfo, ToolResultContent, UsageReport};
20+
use crate::types::{
21+
HistoryEntry, ModelList, Part, ServerInfo, SessionInfo, ToolResultContent, UsageReport,
22+
};
2123

2224
/// What a leading slash offers. Only commands the app can actually perform are listed, and an
2325
/// entry's name is what choosing it matches on.
@@ -144,6 +146,8 @@ pub struct App {
144146
pub popup: Option<Popup>,
145147
/// A line of plain truth about the prompt or RPC status, shown in place of the key hints.
146148
pub notice: Option<String>,
149+
/// Verified server identity (`pid` and `version`) from `server.info`.
150+
pub server_info: Option<ServerInfo>,
147151
/// Active server session, once created or attached.
148152
pub session: Option<SessionInfo>,
149153
/// Available models and the resolved settings default from `model.list`.
@@ -184,6 +188,7 @@ impl App {
184188
prompt: Prompt::default(),
185189
popup: None,
186190
notice: None,
191+
server_info: None,
187192
session: None,
188193
models: None,
189194
model: None,
@@ -198,6 +203,7 @@ impl App {
198203
keymap: Keymap::new(),
199204
tx,
200205
};
206+
app.spawn_server_info();
201207
app.spawn_list_models();
202208
app
203209
}
@@ -476,6 +482,14 @@ impl App {
476482

477483
fn handle_rpc(&mut self, event: RpcEvent) -> bool {
478484
match event {
485+
RpcEvent::ServerInfoLoaded(Ok(info)) => {
486+
self.server_info = Some(info);
487+
true
488+
}
489+
RpcEvent::ServerInfoLoaded(Err(err)) => {
490+
self.notice = Some(err);
491+
true
492+
}
479493
RpcEvent::ModelsLoaded(Ok(models)) => {
480494
self.models = Some(models);
481495
let active_model = self.model_label();
@@ -907,6 +921,16 @@ impl App {
907921
false
908922
}
909923

924+
fn spawn_server_info(&self) {
925+
let tx = self.tx.clone();
926+
if let Ok(handle) = tokio::runtime::Handle::try_current() {
927+
handle.spawn(async move {
928+
let res = RpcClient::default().verify_server().await;
929+
let _ = tx.send(AppEvent::Rpc(RpcEvent::ServerInfoLoaded(res)));
930+
});
931+
}
932+
}
933+
910934
fn spawn_list_models(&self) {
911935
let host_dir = self.active_host_dir();
912936
let tx = self.tx.clone();
@@ -1754,4 +1778,30 @@ mod tests {
17541778
assert_eq!(ids, vec!["off", "low", "medium", "high"]);
17551779
assert_eq!(menu.selected().map(|e| e.id.as_ref()), Some("high"));
17561780
}
1781+
1782+
#[test]
1783+
fn records_server_info_and_surfaces_mismatch_notice() {
1784+
let mut app = App::default();
1785+
assert!(app.server_info.is_none());
1786+
assert!(app.notice.is_none());
1787+
1788+
app.update(AppEvent::Rpc(RpcEvent::ServerInfoLoaded(Ok(ServerInfo {
1789+
pid: 4242,
1790+
version: "0.3.0".to_string(),
1791+
}))));
1792+
assert_eq!(
1793+
app.server_info,
1794+
Some(ServerInfo {
1795+
pid: 4242,
1796+
version: "0.3.0".to_string(),
1797+
})
1798+
);
1799+
assert!(app.notice.is_none());
1800+
1801+
let mismatch = "Server at ws://127.0.0.1:7533/rpc/v1 runs version 0.2.0, expected 0.3.0 from service.json".to_string();
1802+
app.update(AppEvent::Rpc(RpcEvent::ServerInfoLoaded(Err(
1803+
mismatch.clone(),
1804+
))));
1805+
assert_eq!(app.notice.as_deref(), Some(mismatch.as_str()));
1806+
}
17571807
}

‎packages/tui/src/event.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ use crossterm::event::{Event, KeyEvent, KeyEventKind, MouseEventKind};
1010
use tokio::sync::mpsc;
1111

1212
use crate::rpc::StreamEvent;
13-
use crate::types::{HistoryEntry, ModelList, SessionInfo};
13+
use crate::types::{HistoryEntry, ModelList, ServerInfo, SessionInfo};
1414

1515
/// Asynchronous results arriving from the RPC server.
1616
#[derive(Debug, Clone, PartialEq)]
1717
pub enum RpcEvent {
1818
Stream(StreamEvent),
19+
ServerInfoLoaded(Result<ServerInfo, String>),
1920
ModelsLoaded(Result<ModelList, String>),
2021
SessionsLoaded(Result<Vec<SessionInfo>, String>),
2122
SessionHistoryLoaded(Result<Vec<HistoryEntry>, String>),

0 commit comments

Comments
 (0)