diff --git a/crates/daemon/assets/index.html b/crates/daemon/assets/index.html index 189330bd..4174e4de 100644 --- a/crates/daemon/assets/index.html +++ b/crates/daemon/assets/index.html @@ -4273,7 +4273,9 @@ } switch (event.type) { case "message": - appendMessage(event.role || "assistant", event.text || ""); + if (shouldRenderChatMessage(event.role || "assistant", event.text || "")) { + appendMessage(event.role || "assistant", event.text || ""); + } break; case "tool_use": appendToolUse(event.tool, event.args); @@ -4400,6 +4402,23 @@ target.appendChild(row); } +function currentSession() { + return state.sessions.find((s) => s.id === state.currentId) || null; +} + +function shouldRenderChatMessage(role, text) { + const s = currentSession(); + if (!s || s.harness !== "codex") return true; + const trimmed = String(text || "").trimStart(); + if (role === "assistant" && trimmed.startsWith("")) return false; + if ( + role === "user" && + trimmed.startsWith("# AGENTS.md instructions for ") && + trimmed.includes("\n") + ) return false; + return true; +} + // --- terminal-mode tool-call synthesis (issue #134) ---------------------- // Zarvis emits tool calls as structured events, not PTY bytes. Render an // inline xterm representation that mirrors the desktop TUI's tool block. diff --git a/crates/daemon/src/session.rs b/crates/daemon/src/session.rs index e3b842b1..4db398d0 100644 --- a/crates/daemon/src/session.rs +++ b/crates/daemon/src/session.rs @@ -395,7 +395,11 @@ struct PtyInputCapture { buf: String, /// 0 = not in an escape; 1 = saw ESC; 2 = saw ESC[ (CSI); 3 = saw ESC O (SS3). esc: u8, - triggered: bool, + last_was_cr: bool, +} + +fn should_record_pty_user_message(harness: &str) -> bool { + matches!(harness, "claude" | "antigravity") } impl SessionEntry { @@ -2258,10 +2262,27 @@ impl SessionManager { .get_entry(id) .await .ok_or_else(|| anyhow!("session not found: {}", id))?; - // Capture the user's first typed line for auto-title (shell / - // claude / codex interactive sessions don't echo a Message event - // back to the daemon; this is the only place we see their input). - self.feed_pty_input_capture(&entry, &bytes).await; + // Capture submitted PTY lines before forwarding them. Some interactive + // harnesses do not echo user text as structured `Message` events, so + // chat-mode transcript history otherwise loses those turns. + let input_lines = self.capture_pty_input_lines(&entry, &bytes).await; + let harness = entry.summary.read().await.harness.clone(); + for line in input_lines { + if should_record_pty_user_message(&harness) { + self.handle_event( + &entry, + SessionEvent::Message { + role: MessageRole::User, + text: line, + }, + ) + .await; + } else if !entry.title_gen_attempted.load(Ordering::SeqCst) + && line.chars().count() >= 2 + { + self.maybe_spawn_auto_title(entry.clone(), line); + } + } let adapter = entry .adapter .lock() @@ -2277,39 +2298,43 @@ impl SessionManager { Ok(()) } - /// Feed PTY-input bytes through a minimal terminal-input parser - /// (printable ASCII + backspace + CR/LF; CSI/SS3 sequences skipped). - /// On the first CR/LF, hand the accumulated line to the auto-title - /// path. After the first trigger this becomes a no-op for the - /// session's lifetime. - async fn feed_pty_input_capture(&self, entry: &Arc, bytes: &[u8]) { - // Cheap early-outs before taking the per-session lock. - if entry.title_gen_attempted.load(Ordering::SeqCst) { - return; - } + /// Feed PTY-input bytes through a minimal terminal-input parser (printable + /// ASCII + backspace + CR/LF; CSI/SS3 sequences skipped) and return every + /// submitted non-empty line. The parser is intentionally small: it is for + /// transcript/user-title capture, not full terminal editing semantics. + async fn capture_pty_input_lines( + &self, + entry: &Arc, + bytes: &[u8], + ) -> Vec { let mut cap = entry.pty_input_capture.lock().await; - if cap.triggered { - return; - } + let mut lines = Vec::new(); for &b in bytes { match cap.esc { 0 => match b { + b'\n' if cap.last_was_cr => { + cap.last_was_cr = false; + } b'\r' | b'\n' => { let s = cap.buf.trim().to_string(); - cap.triggered = true; + cap.last_was_cr = b == b'\r'; cap.buf.clear(); - drop(cap); if s.chars().count() >= 2 { - self.maybe_spawn_auto_title(entry.clone(), s); + lines.push(s); } - return; } 0x1b => cap.esc = 1, 0x08 | 0x7f => { + cap.last_was_cr = false; cap.buf.pop(); } - _ if (0x20..0x7f).contains(&b) => cap.buf.push(b as char), - _ => {} + _ if (0x20..0x7f).contains(&b) => { + cap.last_was_cr = false; + cap.buf.push(b as char); + } + _ => { + cap.last_was_cr = false; + } }, 1 => match b { b'[' => cap.esc = 2, @@ -2329,6 +2354,7 @@ impl SessionManager { _ => cap.esc = 0, } } + lines } /// Record that a given client kind just acted on a session's