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,
|
_ => 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
|
// Tell the room we're "typing" while claude thinks. Best-effort; no
|
||||||
// hard fail if it doesn't go through.
|
// hard fail if it doesn't go through.
|
||||||
if let Err(e) = room.typing_notice(true).await {
|
if let Err(e) = room.typing_notice(true).await {
|
||||||
tracing::debug!(room = %room_id, "failed to send typing start: {e}");
|
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 {
|
if let Err(e) = room.typing_notice(false).await {
|
||||||
tracing::debug!(room = %room_id, "failed to send typing stop: {e}");
|
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.
|
/// 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`
|
/// 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 {
|
match item {
|
||||||
TimelineItem::Message {
|
TimelineItem::Message {
|
||||||
event_id,
|
event_id,
|
||||||
|
|
@ -861,7 +876,20 @@ fn render_timeline_item(prompt: &mut String, item: &TimelineItem) {
|
||||||
let ts_str = format_ts(*ts);
|
let ts_str = format_ts(*ts);
|
||||||
let id = short_event_id(event_id);
|
let id = short_event_id(event_id);
|
||||||
let prefix = if *is_self { "(you) " } else { "" };
|
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 {
|
TimelineItem::Reaction {
|
||||||
sender,
|
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
|
/// Resolve a (possibly shortened/ellipsized) event id to a full one by
|
||||||
/// looking up against the timeline. Returns the matching message's full
|
/// looking up against the timeline. Returns the matching message's full
|
||||||
/// event id if found.
|
/// event id if found.
|
||||||
|
|
@ -978,6 +1067,7 @@ async fn invoke_claude(
|
||||||
timeline: &[TimelineItem],
|
timeline: &[TimelineItem],
|
||||||
seen_idx: usize,
|
seen_idx: usize,
|
||||||
model: &str,
|
model: &str,
|
||||||
|
read_markers: &std::collections::HashMap<OwnedEventId, Vec<OwnedUserId>>,
|
||||||
) -> anyhow::Result<Vec<ClaudeDoc>> {
|
) -> anyhow::Result<Vec<ClaudeDoc>> {
|
||||||
let identity_dir = paths::identity_dir();
|
let identity_dir = paths::identity_dir();
|
||||||
let identity_str = identity_dir.to_string_lossy();
|
let identity_str = identity_dir.to_string_lossy();
|
||||||
|
|
@ -1016,7 +1106,7 @@ async fn invoke_claude(
|
||||||
if !old.is_empty() {
|
if !old.is_empty() {
|
||||||
writeln!(prompt, "\n[previously seen events — for context]").unwrap();
|
writeln!(prompt, "\n[previously seen events — for context]").unwrap();
|
||||||
for item in old {
|
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();
|
writeln!(prompt, "(none)").unwrap();
|
||||||
} else {
|
} else {
|
||||||
for item in new {
|
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