show others' read receipts on each message in prompt
This commit is contained in:
parent
f05ce0ee2b
commit
5b91de6339
1 changed files with 96 additions and 6 deletions
102
src/main.rs
102
src/main.rs
|
|
@ -638,13 +638,24 @@ async fn process_loop(state: Arc<Mutex<DaemonState>>, client: Client) {
|
|||
_ => None,
|
||||
});
|
||||
|
||||
// Compute who has read which message
|
||||
let read_markers = compute_read_markers(&room, &timeline, &own_user).await;
|
||||
|
||||
// Tell the room we're "typing" while claude thinks. Best-effort; no
|
||||
// hard fail if it doesn't go through.
|
||||
if let Err(e) = room.typing_notice(true).await {
|
||||
tracing::debug!(room = %room_id, "failed to send typing start: {e}");
|
||||
}
|
||||
|
||||
let invoke_result = invoke_claude(&room_id, &room_name, &timeline, seen_idx, &model).await;
|
||||
let invoke_result = invoke_claude(
|
||||
&room_id,
|
||||
&room_name,
|
||||
&timeline,
|
||||
seen_idx,
|
||||
&model,
|
||||
&read_markers,
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Err(e) = room.typing_notice(false).await {
|
||||
tracing::debug!(room = %room_id, "failed to send typing stop: {e}");
|
||||
|
|
@ -846,9 +857,13 @@ async fn load_timeline(
|
|||
}
|
||||
|
||||
/// Render one timeline item into the prompt.
|
||||
/// Messages: `[ts] $eid... [(you) ]@user: body`
|
||||
/// Messages: `[ts] $eid... [(you) ]@user: body [read by: ...]`
|
||||
/// Reactions: `[ts] [(you) ]@user reacted to $eid... with KEY`
|
||||
fn render_timeline_item(prompt: &mut String, item: &TimelineItem) {
|
||||
fn render_timeline_item(
|
||||
prompt: &mut String,
|
||||
item: &TimelineItem,
|
||||
read_markers: &std::collections::HashMap<OwnedEventId, Vec<OwnedUserId>>,
|
||||
) {
|
||||
match item {
|
||||
TimelineItem::Message {
|
||||
event_id,
|
||||
|
|
@ -861,7 +876,20 @@ fn render_timeline_item(prompt: &mut String, item: &TimelineItem) {
|
|||
let ts_str = format_ts(*ts);
|
||||
let id = short_event_id(event_id);
|
||||
let prefix = if *is_self { "(you) " } else { "" };
|
||||
writeln!(prompt, "[{ts_str}] {id} {prefix}{sender}: {body}").unwrap();
|
||||
let readers_str = match read_markers.get(event_id) {
|
||||
Some(rs) if !rs.is_empty() => {
|
||||
let mut sorted = rs.clone();
|
||||
sorted.sort();
|
||||
let names: Vec<String> = sorted.iter().map(|u| u.to_string()).collect();
|
||||
format!(" [read by: {}]", names.join(", "))
|
||||
}
|
||||
_ => String::new(),
|
||||
};
|
||||
writeln!(
|
||||
prompt,
|
||||
"[{ts_str}] {id} {prefix}{sender}: {body}{readers_str}"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
TimelineItem::Reaction {
|
||||
sender,
|
||||
|
|
@ -893,6 +921,67 @@ fn short_event_id(id: &OwnedEventId) -> String {
|
|||
}
|
||||
}
|
||||
|
||||
/// For each message in the timeline, compute the list of OTHER users who
|
||||
/// have a read receipt at or after that message. Self is excluded.
|
||||
async fn compute_read_markers(
|
||||
room: &Room,
|
||||
timeline: &[TimelineItem],
|
||||
own_user: &OwnedUserId,
|
||||
) -> std::collections::HashMap<OwnedEventId, Vec<OwnedUserId>> {
|
||||
use matrix_sdk::ruma::events::receipt::ReceiptType;
|
||||
|
||||
// Collect unique non-self users from the timeline (senders + reactors)
|
||||
let mut users: Vec<OwnedUserId> = timeline
|
||||
.iter()
|
||||
.filter(|t| !t.is_self())
|
||||
.map(|t| t.sender().clone())
|
||||
.collect();
|
||||
users.sort();
|
||||
users.dedup();
|
||||
|
||||
// Build position map for messages in timeline: event_id -> index
|
||||
let positions: std::collections::HashMap<OwnedEventId, usize> = timeline
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, t)| match t {
|
||||
TimelineItem::Message { event_id, .. } => Some((event_id.clone(), i)),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
// For each user, find their receipt position. Then for every message
|
||||
// at or before that position, add them as a reader.
|
||||
let mut readers: std::collections::HashMap<OwnedEventId, Vec<OwnedUserId>> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
for user in &users {
|
||||
if user == own_user {
|
||||
continue;
|
||||
}
|
||||
let receipt_eid = match room
|
||||
.load_user_receipt(ReceiptType::Read, ReceiptThread::Unthreaded, user)
|
||||
.await
|
||||
{
|
||||
Ok(Some((eid, _))) => eid,
|
||||
_ => continue,
|
||||
};
|
||||
let Some(&user_pos) = positions.get(&receipt_eid) else {
|
||||
continue;
|
||||
};
|
||||
// Mark this user as a reader for all messages at index <= user_pos
|
||||
for item in timeline.iter().take(user_pos + 1) {
|
||||
if let TimelineItem::Message { event_id, .. } = item {
|
||||
readers
|
||||
.entry(event_id.clone())
|
||||
.or_default()
|
||||
.push(user.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readers
|
||||
}
|
||||
|
||||
/// Resolve a (possibly shortened/ellipsized) event id to a full one by
|
||||
/// looking up against the timeline. Returns the matching message's full
|
||||
/// event id if found.
|
||||
|
|
@ -978,6 +1067,7 @@ async fn invoke_claude(
|
|||
timeline: &[TimelineItem],
|
||||
seen_idx: usize,
|
||||
model: &str,
|
||||
read_markers: &std::collections::HashMap<OwnedEventId, Vec<OwnedUserId>>,
|
||||
) -> anyhow::Result<Vec<ClaudeDoc>> {
|
||||
let identity_dir = paths::identity_dir();
|
||||
let identity_str = identity_dir.to_string_lossy();
|
||||
|
|
@ -1016,7 +1106,7 @@ async fn invoke_claude(
|
|||
if !old.is_empty() {
|
||||
writeln!(prompt, "\n[previously seen events — for context]").unwrap();
|
||||
for item in old {
|
||||
render_timeline_item(&mut prompt, item);
|
||||
render_timeline_item(&mut prompt, item, read_markers);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1025,7 +1115,7 @@ async fn invoke_claude(
|
|||
writeln!(prompt, "(none)").unwrap();
|
||||
} else {
|
||||
for item in new {
|
||||
render_timeline_item(&mut prompt, item);
|
||||
render_timeline_item(&mut prompt, item, read_markers);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue