feat: add is_active conversaiton

This commit is contained in:
Mojtaba Chenani
2026-08-13 23:43:19 +02:00
parent da0a744caa
commit 4180dd6808
4 changed files with 80 additions and 31 deletions
+63 -29
View File
@@ -94,6 +94,7 @@ where
pub client: ChatClient<T, R, S>,
events: Receiver<Event>,
pub state: AppState,
is_active: bool,
/// Ephemeral command output — not persisted, cleared on chat switch.
command_output: Vec<DisplayMessage>,
pub input: String,
@@ -120,24 +121,29 @@ where
let state = Self::load_state(&state_path);
let chat_count = state.chats.len();
let status = if chat_count > 0 {
format!(
"Welcome back, {user_name}! {chat_count} chat(s) loaded. Type /help for commands."
)
} else {
let status = if chat_count == 0 {
format!("Welcome, {user_name}! Type /help for commands.")
} else {
format!(
"Welcome back, {user_name}! {chat_count} chat(s) loaded — read-only from a previous \
session; start a new /dm or /new to chat. Type /help."
)
};
Ok(Self {
let mut app = Self {
client,
events,
state,
is_active: false,
command_output: Vec::new(),
input: String::new(),
status,
user_name: user_name.to_string(),
state_path,
})
};
app.state.active_chat = None;
app.show_chats_list();
Ok(app)
}
fn load_state(path: &Path) -> AppState {
@@ -172,6 +178,10 @@ where
}
fn set_active_chat(&mut self, chat_id: Option<String>) {
self.is_active = chat_id
.as_deref()
.map(|id| self.client.has_conversation(id))
.unwrap_or(false);
self.state.active_chat = chat_id;
self.command_output.clear();
}
@@ -190,6 +200,39 @@ where
self.set_active_chat(Some(chat_id));
}
pub fn is_active(&self) -> bool {
self.is_active
}
/// Render the chat list; restored (dead) chats are flagged read-only.
fn show_chats_list(&mut self) {
self.command_output.clear();
let sessions: Vec<_> = self.state.chats.values().cloned().collect();
if sessions.is_empty() {
self.add_system_message("No chats yet. Use /dm or /new to start one.");
return;
}
self.add_system_message(&format!("── Your Chats ({}) ──", sessions.len()));
for s in &sessions {
let active = self.state.active_chat.as_deref() == Some(&s.chat_id);
let read_only = !self.client.has_conversation(&s.chat_id);
let mut tags = String::new();
if active {
tags.push_str(" (active)");
}
if read_only {
tags.push_str(" (read-only)");
}
let label = format!(
" • [{}] {} ({}){tags}",
s.kind.badge(),
s.display_name(),
&s.chat_id[..8.min(s.chat_id.len())]
);
self.add_system_message(&label);
}
}
/// Find a chat_id by nickname (exact) or chat_id prefix.
fn resolve_chat_id(&self, query: &str) -> Option<&str> {
// Exact nickname match first.
@@ -273,6 +316,13 @@ where
.clone()
.ok_or_else(|| anyhow::anyhow!("No active chat. Use /dm or /new first."))?;
if !self.is_active {
anyhow::bail!(
"This conversation is from a previous session and can't receive messages yet \
— chats don't persist across restart. Start a new one with /dm or /new."
);
}
self.client
.send_message(&chat_id, content.as_bytes())
.map_err(|e| anyhow::anyhow!("{e:?}"))?;
@@ -473,29 +523,13 @@ where
Ok(Some(format!("Nickname set to '{args}'")))
}
"/chats" => {
let sessions: Vec<_> = self.state.chats.values().cloned().collect();
if sessions.is_empty() {
Ok(Some(
"No chats yet. Use /dm or /new to start one.".to_string(),
))
self.show_chats_list();
let n = self.state.chats.len();
Ok(Some(if n == 0 {
"No chats yet".to_string()
} else {
self.add_system_message(&format!("── Your Chats ({}) ──", sessions.len()));
for s in &sessions {
let marker = if self.state.active_chat.as_deref() == Some(&s.chat_id) {
" (active)"
} else {
""
};
let label = format!(
" • [{}] {} ({}){marker}",
s.kind.badge(),
s.display_name(),
&s.chat_id[..8.min(s.chat_id.len())]
);
self.add_system_message(&label);
}
Ok(Some(format!("{} chat(s)", sessions.len())))
}
format!("{n} chat(s)")
}))
}
"/switch" => {
if args.is_empty() {
+7 -2
View File
@@ -69,9 +69,14 @@ where
let title = match app.current_session() {
Some(session) => {
let id = &session.chat_id[..8.min(session.chat_id.len())];
let ro = if app.is_active() {
""
} else {
" [read-only]"
};
match &session.nickname {
Some(name) => format!(" 💬 Chat: {}{name} ({id}) ", app.user_name),
None => format!(" 💬 Chat: {} ↔ ({id}) ", app.user_name),
Some(name) => format!(" 💬 Chat: {}{name} ({id}){ro} ", app.user_name),
None => format!(" 💬 Chat: {} ↔ ({id}){ro} ", app.user_name),
}
}
None => format!(" 💬 {} — no active chat ", app.user_name),
+5
View File
@@ -325,6 +325,11 @@ impl<'a, S: ExternalServices + 'static> Core<S> {
Ok(convos)
}
/// Whether the conversation is loaded and usable.
pub fn is_conversation_active(&self, convo_id: &str) -> bool {
self.cached_convos.contains_key(convo_id)
}
pub fn take_missing_messages(&self) -> Vec<MissingMessage> {
self.services.causal.take_missing()
}
+5
View File
@@ -273,6 +273,11 @@ where
self.core.lock().list_conversations().map_err(Into::into)
}
/// Whether the conversation is live and usable this session.
pub fn has_conversation(&self, convo_id: &str) -> bool {
self.core.lock().is_conversation_active(convo_id)
}
/// Encrypt and send `content` to an existing conversation. The core
/// publishes the outbound envelope.
pub fn send_message(&mut self, convo_id: &str, content: &[u8]) -> Result<(), ClientError> {