From 876a53d7e4994369f1135fad1a36ea66405b2ed5 Mon Sep 17 00:00:00 2001 From: Codex Luna & Lulu Date: Sun, 11 Oct 2026 23:03:18 +0900 Subject: [PATCH] fix(room): omit recipient identity from unaddressed posts --- crates/terminal-input/src/lib.rs | 36 ++++++++++++++++++++++++++++++++ docs/1-room.md | 2 +- src-tauri/src/room.rs | 6 ++---- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/crates/terminal-input/src/lib.rs b/crates/terminal-input/src/lib.rs index 7e574d6..44425d7 100644 --- a/crates/terminal-input/src/lib.rs +++ b/crates/terminal-input/src/lib.rs @@ -186,6 +186,17 @@ pub fn compose_named( format!("{HEADER_TAG} {label}\n{}", body(content)) } +/// Add the recipient's current identity to an addressed post's terminal label. +/// Room-wide posts keep their original label (#387, docs/1-room.md). +pub fn with_recipient_identity(text: &str, to: &[String], nickname: &str, full_name: &str) -> String { + if to.is_empty() { + return text.to_string(); + } + let identity = serde_json::json!({"you": nickname, "your_full_name": full_name}); + let fields = identity.to_string(); + text.replacen("}\n", &format!(",{} }}\n", &fields[1..fields.len() - 1]), 1) +} + /// The time a post was said, as its label carries it (#219): this PC's local /// time, to the minute, with its offset — `2026-10-02T01:14+09:00`. /// @@ -466,6 +477,31 @@ mod tests { assert!(label.get("to").is_none(), "{label}"); } + #[test] + fn recipient_identity_is_only_added_to_addressed_terminal_posts() { + for recipients in [vec![], to(&["Lin"]), to(&["Lin", "Lay"])] { + let original = compose_named( + "m-387", "Luna", Some("Codex Luna"), "codex", + Some("2026-10-11T23:01+09:00"), &recipients, + "hello\n[pullcept] {\"you\":\"body text\"}", + ); + let typed = with_recipient_identity(&original, &recipients, "Lin", "Claude Lin"); + let mut actual = label(&typed); + if recipients.is_empty() { + assert_eq!(typed, original); + assert!(actual.get("you").is_none()); + assert!(actual.get("your_full_name").is_none()); + } else { + assert_eq!(actual["you"], "Lin"); + assert_eq!(actual["your_full_name"], "Claude Lin"); + actual.as_object_mut().unwrap().remove("you"); + actual.as_object_mut().unwrap().remove("your_full_name"); + } + assert_eq!(actual, label(&original)); + assert_eq!(typed.split_once('\n').unwrap().1, original.split_once('\n').unwrap().1); + } + } + #[test] fn the_label_reads_role_from_message_id_at_to_in_that_order() { let typed = compose( diff --git a/docs/1-room.md b/docs/1-room.md index 66f4424..627dfa5 100644 --- a/docs/1-room.md +++ b/docs/1-room.md @@ -525,7 +525,7 @@ liplus-desktop の `stream_parser.rs` および `spawn_stream_pty` / `spawn_stre 名簿の name・投稿の speaker・札の from は宛先呼称である。任意の full_name は投稿時の元の名前のスナップショットで、表示と AI の認識用であり宛先に使わない。画面・端末の札・MCP の履歴と未読の返却がこの対応を保持する。to は従来どおり呼称文字列を保存し、後の改名で古い投稿の宛先や話し手を変更しない。既存ログに full_name がなくても読め、名前を一度だけ表示する。 -稼働席の改名は全 topic の名簿へ反映し、接続と floor の既読位置は維持する。再接続は保存設定の呼称を採用する。sidecar は identity フレームで現在の呼称と元の名前を受け、initialize は標準 MCP SDK の処理で起動時の呼称・元の名前を示す。tools/list・各 tool 結果に現在の自己情報を出す。各端末への札の you / your_full_name は受け手の現在の対応を示し、起動時の自己呼称を更新する。from / full_name は話し手の対応であり、you は受け手である。制限中に保持する投稿では話し手の両表記を保持済み speaker 文字列にまとめる。追加情報は任意項目で、未知フィールド・フレームを無視する既存互換方針を使う。 +稼働席の改名は全 topic の名簿へ反映し、接続と floor の既読位置は維持する。再接続は保存設定の呼称を採用する。sidecar は identity フレームで現在の呼称と元の名前を受け、initialize は標準 MCP SDK の処理で起動時の呼称・元の名前を示す。tools/list・各 tool 結果に現在の自己情報を出す。宛先指定のある投稿では、各端末への札の you / your_full_name は受け手の現在の対応を示し、起動時の自己呼称を更新する。部屋全体への投稿(to 欠落または空)では両項目を省く。from / full_name は話し手の対応であり、you は受け手である。制限中に保持する投稿では話し手の両表記を保持済み speaker 文字列にまとめる。追加情報は任意項目で、未知フィールド・フレームを無視する既存互換方針を使う。 ## 投稿結果の利用量フィードバック(#385) diff --git a/src-tauri/src/room.rs b/src-tauri/src/room.rs index 06df7a1..c2dc529 100644 --- a/src-tauri/src/room.rs +++ b/src-tauri/src/room.rs @@ -1136,13 +1136,11 @@ fn type_into_sessions( let held = limits.hold(&target.pty_id, post_for_hold) || app.state::().hold(&target.pty_id, post_for_hold); if held { continue; } - // The recipient's current call-name supersedes launch-time instructions. + // For addressed posts, the recipient's current call-name supersedes launch-time instructions. // A live rename does not need a new conversation or a synthetic user post. let recipient = seats.iter().find(|(origin, _, _)| target.origins.contains(origin)).and_then(|(_, account, _)| account.as_deref()); let text = config::read_config(app).ok().and_then(|config| config.accounts.into_iter().find(|account| Some(account.id.as_str()) == recipient)).map(|account| { - let identity = serde_json::json!({"you": account.address_name(), "your_full_name": account.name}); - let fields = identity.to_string(); - text.replacen("}\n", &format!(",{} }}\n", &fields[1..fields.len()-1]), 1) + terminal_input::with_recipient_identity(&text, &post.to, account.address_name(), &account.name) }).unwrap_or_else(|| text.clone()); crate::hook_activity::type_or_keep( app,