@sigitcode-old / sigit / commits / 980f80d
Fork 1

I'm siGit, running on siGit Code & Deploy platform, browsing siGit Code coding agent repo.

feat: show /login, /logout and /whoami as system notices

These three answered as plain assistant messages. They now go out as a completed think tool call, the lightbulb row Zed draws for system status. A browser /login reply has several lines and a URL, so the new send_system_notice puts the first line in the card title and the rest in its content. The URL stays visible and clickable. The whoami test reads the tool card instead of message chunks. The TUI already renders these as system messages and is unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

Seto Elkahfi committed Oct 29, 2026 at 17:30 UTC 980f80daf73aaf4cefa6b1aa274e31240df49b81
2 files changed +71 -14
crates/cli/src/main.rs
+54 -3
@@ -433,6 +433,19 @@ fn tool_arguments_content(arguments_json: &str) -> ToolCallContent {
433 fenced_code_block(language, &chat::pretty_tool_arguments(arguments_json)).into()
434 }
435
436 +/// Splits a notice into its title (the first non-empty line) and the body
437 +/// (everything after it), if there is any.
438 +fn split_notice(text: &str) -> (String, Option<String>) {
439 + let text = text.trim();
440 + match text.split_once('\n') {
441 + Some((title, rest)) if !rest.trim().is_empty() => {
442 + (title.trim().to_string(), Some(rest.trim().to_string()))
443 + }
444 + Some((title, _)) => (title.trim().to_string(), None),
445 + None => (text.to_string(), None),
446 + }
447 +}
448 +
449 /// Wraps a tool's output as the display content of its ACP tool-call card
450 /// (issue #78). Capped via [`chat::cap_output_preview`] — display only, the
451 /// model still gets the full output — and fenced so plain-text output (or a
@@ -1661,6 +1674,28 @@ impl SiGitAgent {
1674 ))
1675 }
1676
1677 + /// A system notice that may run to several lines: the first line is the
1678 + /// card's title and the rest is its body, so a sign-in URL stays visible
1679 + /// and clickable. A single line is the same as `send_system_status`.
1680 + fn send_system_notice(
1681 + &self,
1682 + cx: &ConnectionTo<Client>,
1683 + session_id: SessionId,
1684 + text: impl AsRef<str>,
1685 + ) -> agent_client_protocol::Result<()> {
1686 + let (title, body) = split_notice(text.as_ref());
1687 + let mut call = ToolCall::new(format!("system-status-{}", uuid::Uuid::new_v4()), title)
1688 + .kind(ToolKind::Think)
1689 + .status(ToolCallStatus::Completed);
1690 + if let Some(body) = body {
1691 + call = call.content(vec![body.into()]);
1692 + }
1693 + cx.send_notification(SessionNotification::new(
1694 + session_id,
1695 + SessionUpdate::ToolCall(call),
1696 + ))
1697 + }
1698 +
1699 /// Run one inference turn (`fut`) while concurrently forwarding any streamed
1700 /// tokens to the editor. The sink receiver is drained as the future runs, so
1701 /// chunks reach the client live rather than all at once when it resolves.
@@ -5987,15 +6022,15 @@ async fn exec_slash_acp(
6022 .await
6023 .unwrap_or_else(|message| message),
6024 };
5990 - agent.send_assistant_message(cx, session_id, message).ok();
6025 + agent.send_system_notice(cx, session_id, message).ok();
6026 }
6027 SlashCommand::Logout => {
6028 let message = agent.sign_out(cx).await;
5994 - agent.send_assistant_message(cx, session_id, message).ok();
6029 + agent.send_system_notice(cx, session_id, message).ok();
6030 }
6031 SlashCommand::Whoami => {
6032 let message = account::status_line().await;
5998 - agent.send_assistant_message(cx, session_id, message).ok();
6033 + agent.send_system_notice(cx, session_id, message).ok();
6034 }
6035 SlashCommand::Reload => {
6036 agent.handle_reload(cx, session_id).await;
@@ -6992,6 +7027,22 @@ async fn main() -> anyhow::Result<()> {
7027
7028 #[cfg(test)]
7029 mod tests {
7030 + #[test]
7031 + fn notices_split_into_title_and_body() {
7032 + assert_eq!(super::split_notice("Signed in"), ("Signed in".into(), None));
7033 + assert_eq!(
7034 + super::split_notice("Signed in\n"),
7035 + ("Signed in".into(), None)
7036 + );
7037 + assert_eq!(
7038 + super::split_notice("Open this page:\nhttps://x.test/a\nthen return"),
7039 + (
7040 + "Open this page:".into(),
7041 + Some("https://x.test/a\nthen return".into())
7042 + )
7043 + );
7044 + }
7045 +
7046 use super::*;
7047
7048 /// Compares path components so the tests pass on both Unix (forward slashes)
crates/cli/tests/acp_logout.rs
+17 -11
@@ -124,28 +124,34 @@ impl AgentUnderTest {
124 }
125 }
126
127 - /// Send `text` as a prompt and return the agent message text it streamed
128 - /// before the turn ended.
129 - fn prompt_text(&mut self, session_id: &str, text: &str) -> String {
127 + /// Send `text` as a prompt and return the system notice it produced: the
128 + /// completed `think` tool card's title, then its content text if any.
129 + fn prompt_notice(&mut self, session_id: &str, text: &str) -> String {
130 let id = self.request(
131 "session/prompt",
132 json!({"sessionId": session_id, "prompt": [{"type": "text", "text": text}]}),
133 );
134 let deadline = Instant::now() + TIMEOUT;
135 - let mut reply = String::new();
135 + let mut notice = String::new();
136 loop {
137 let remaining = deadline.saturating_duration_since(Instant::now());
138 let Ok(message) = self.incoming.recv_timeout(remaining) else {
139 panic!("timed out waiting for the response to prompt {id}");
140 };
141 - if message["method"] == "session/update"
142 - && message["params"]["update"]["sessionUpdate"] == "agent_message_chunk"
143 - && let Some(chunk) = message["params"]["update"]["content"]["text"].as_str()
144 - {
145 - reply.push_str(chunk);
141 + let update = &message["params"]["update"];
142 + if message["method"] == "session/update" && update["sessionUpdate"] == "tool_call" {
143 + assert_eq!(update["kind"], "think", "{update}");
144 + assert_eq!(update["status"], "completed", "{update}");
145 + notice.push_str(update["title"].as_str().unwrap_or_default());
146 + for item in update["content"].as_array().into_iter().flatten() {
147 + if let Some(body) = item["content"]["text"].as_str() {
148 + notice.push('\n');
149 + notice.push_str(body);
150 + }
151 + }
152 }
153 if message["id"] == id && message.get("method").is_none() {
148 - return reply;
154 + return notice;
155 }
156 }
157 }
@@ -231,7 +237,7 @@ fn whoami_checks_the_session_against_the_user_endpoint() {
237 .expect("session id")
238 .to_string();
239
234 - let reply = agent.prompt_text(&session_id, "/whoami");
240 + let reply = agent.prompt_notice(&session_id, "/whoami");
241 assert!(
242 reply.contains("Signed in to siGit Code Cloud as dev@example.com"),
243 "{reply}"