show others' read receipts on each message in prompt

This commit is contained in:
Damocles 2026-04-30 22:54:41 +02:00
parent f05ce0ee2b
commit 5b91de6339

View file

@ -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);
}
}