···4444 ) sessions []
4545end
46464747+(** Strip any @**name** mention from the start of content.
4848+ This handles display names like @**Poe** that don't match email patterns. *)
4949+let strip_leading_mention content =
5050+ let s = String.trim content in
5151+ if String.length s >= 5 && String.sub s 0 3 = "@**" then
5252+ match String.index_from_opt s 3 '*' with
5353+ | Some i when i + 1 < String.length s && s.[i+1] = '*' ->
5454+ (* Found closing **, strip the mention *)
5555+ String.trim (String.sub s (i + 2) (String.length s - i - 2))
5656+ | _ -> s
5757+ else s
5858+4759let run_git_pull ~proc ~cwd =
4860 Log.info (fun m -> m "Pulling latest changes from remote");
4961 Eio.Switch.run @@ fun sw ->
···159171 let message = match scope with
160172 | Session.Channel { stream; topic } ->
161173 Zulip.Message.create ~type_:`Channel ~to_:[stream] ~topic ~content ()
162162- | Session.Direct { user_email } ->
174174+ | Session.Direct { user_email; _ } ->
163175 Zulip.Message.create ~type_:`Direct ~to_:[user_email] ~content ()
164176 in
165177 let _resp = Zulip.Messages.send client message in
···369381 else
370382 let session_lines = active_sessions |> List.map (fun (scope, activated_at) ->
371383 let session = Session.load storage ~scope ~now in
372372- let scope_str = Session.scope_to_string scope in
384384+ let scope_mention = Session.scope_to_mention scope in
373385 let active_for = format_duration (now -. activated_at) in
374386 let stats = Session.stats session in
375375- Printf.sprintf " - **%s**: %s (active for %s)" scope_str stats active_for
387387+ Printf.sprintf " - %s: %s (active for %s)" scope_mention stats active_for
376388 ) in
377389 Printf.sprintf "- Active sessions (%d):\n%s"
378390 (List.length active_sessions)
···480492 if sender_email = bot_email then Zulip_bot.Response.silent
481493 else
482494 let scope = Session.scope_of_message msg in
483483- let is_mentioned = Zulip_bot.Message.is_mentioned msg ~user_email:bot_email in
495495+ (* Use Zulip's "mentioned" flag rather than pattern matching on email -
496496+ the flag correctly handles display name mentions like @**Poe** *)
497497+ let is_mentioned = List.mem "mentioned" (Zulip_bot.Message.flags msg) in
484498 let is_private = Zulip_bot.Message.is_private msg in
485499486500 (* Check if this is a message we should respond to *)
···490504491505 let client = Zulip_bot.Storage.client storage in
492506 let content =
493493- Zulip_bot.Message.strip_mention msg ~user_email:bot_email
494494- |> String.trim
507507+ Zulip_bot.Message.content msg
508508+ |> strip_leading_mention
495509 in
496510 Log.info (fun m -> m "Received message (mentioned): %s" content);
497511 match Commands.parse content with
+9-5
lib/session.ml
···3737(** Session scope - either a channel+topic or a DM with a user *)
3838type scope =
3939 | Channel of { stream : string; topic : string }
4040- | Direct of { user_email : string }
4040+ | Direct of { user_email : string; user_full_name : string }
41414242let scope_to_key = function
4343 | Channel { stream; topic } ->
4444 Printf.sprintf "poe:session:channel:%s:%s" stream topic
4545- | Direct { user_email } -> Printf.sprintf "poe:session:dm:%s" user_email
4545+ | Direct { user_email; _ } -> Printf.sprintf "poe:session:dm:%s" user_email
46464747let scope_to_string = function
4848 | Channel { stream; topic } -> Printf.sprintf "channel %s > %s" stream topic
4949- | Direct { user_email } -> Printf.sprintf "DM with %s" user_email
4949+ | Direct { user_full_name; _ } -> Printf.sprintf "DM with %s" user_full_name
5050+5151+let scope_to_mention = function
5252+ | Channel { stream; topic } -> Printf.sprintf "#**%s>%s**" stream topic
5353+ | Direct { user_full_name; _ } -> Printf.sprintf "@**%s**" user_full_name
50545155(** Session data stored in Zulip bot storage *)
5256type t = { turns : turn list; created_at : float; updated_at : float }
···7074let scope_of_message (msg : Zulip_bot.Message.t) : scope =
7175 match msg with
7276 | Zulip_bot.Message.Private { common; _ } ->
7373- Direct { user_email = common.sender_email }
7777+ Direct { user_email = common.sender_email; user_full_name = common.sender_full_name }
7478 | Zulip_bot.Message.Stream { common = _; display_recipient; subject; _ } ->
7579 Channel { stream = display_recipient; topic = subject }
7680 | Zulip_bot.Message.Unknown { common; _ } ->
7781 (* Fall back to treating as DM *)
7878- Direct { user_email = common.sender_email }
8282+ Direct { user_email = common.sender_email; user_full_name = common.sender_full_name }
79838084(** Load session from storage *)
8185let load storage ~scope ~now : t =
+6-1
lib/session.mli
···2020(** Session scope - either a channel+topic or a DM with a user *)
2121type scope =
2222 | Channel of { stream : string; topic : string }
2323- | Direct of { user_email : string }
2323+ | Direct of { user_email : string; user_full_name : string }
24242525val scope_to_string : scope -> string
2626(** [scope_to_string scope] returns a human-readable description of the scope. *)
2727+2828+val scope_to_mention : scope -> string
2929+(** [scope_to_mention scope] returns a Zulip-compatible linked mention.
3030+ For channels, returns [#**stream>topic**] format.
3131+ For DMs, returns [DM with `email`] format. *)
27322833(** Session data *)
2934type t