Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions crates/terminal-input/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
///
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion docs/1-room.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
6 changes: 2 additions & 4 deletions src-tauri/src/room.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1136,13 +1136,11 @@ fn type_into_sessions(
let held = limits.hold(&target.pty_id, post_for_hold)
|| app.state::<crate::claude_limit::ClaudeLimits>().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,
Expand Down
Loading