refactor: carry a session's source on AgentEvent and tidy the review fixes

The Codex router reads source off the parsed event instead of parsing the
hook body again. The command scan reads each real directory once rather than
capping depth, labels capitalise through one helper (alias_label no longer
slices bytes), the live input rows are set where Covering is decided, and
the transcript is opened once per poll.
This commit is contained in:
l0ng-ai
2026-10-01 22:28:01 +08:00
parent da0add62e2
commit faca75dfbb
5 changed files with 59 additions and 42 deletions
+11 -11
View File
@@ -153,8 +153,7 @@ fn build_hook_body(agent: &str, event: &str, stdin_json: &str) -> String {
("cwd", "cwd"),
// Goose spells the working directory its own way.
("cwd", "working_dir"),
// What started a session (startup, resume, clear…): routes a Codex
// report — see `daemon::pane::codex_report_target`.
// What started a session — see `cli_agent::AgentEvent::source`.
("source", "source"),
] {
if let Some(v) = payload
@@ -345,14 +344,10 @@ fn turn_reply_model(path: &Path, reply: &str, prompt_id: Option<&str>) -> Option
let deadline = std::time::Instant::now() + TRANSCRIPT_CATCH_UP;
let mut read = None;
loop {
let len = std::fs::metadata(path).ok().map(|m| m.len());
if len != read {
read = len;
if let Some(model) =
transcript_tail(path).and_then(|t| reply_model_in(&t, reply, prompt_id))
{
return Some(model);
}
if let Some(model) =
transcript_tail(path, &mut read).and_then(|t| reply_model_in(&t, reply, prompt_id))
{
return Some(model);
}
if std::time::Instant::now() >= deadline {
return None;
@@ -361,10 +356,15 @@ fn turn_reply_model(path: &Path, reply: &str, prompt_id: Option<&str>) -> Option
}
}
fn transcript_tail(path: &Path) -> Option<String> {
/// The transcript's last [`TRANSCRIPT_TAIL`] bytes, unless it is still the
/// length `read` last saw — then nothing new is in it.
fn transcript_tail(path: &Path, read: &mut Option<u64>) -> Option<String> {
use std::io::{Seek as _, SeekFrom};
let mut file = std::fs::File::open(path).ok()?;
let len = file.metadata().ok()?.len();
if read.replace(len) == Some(len) {
return None;
}
file.seek(SeekFrom::Start(len.saturating_sub(TRANSCRIPT_TAIL)))
.ok()?;
let mut tail = Vec::new();
+14
View File
@@ -1201,6 +1201,9 @@ pub struct AgentEvent {
pub session_id: Option<String>,
pub message: Option<String>,
pub cwd: Option<std::path::PathBuf>,
/// What started the session, on a session start that says: `startup`,
/// `resume`, `clear` (Codex's `/new`), `compact`.
pub source: Option<String>,
pub readout: AgentReadout,
}
@@ -1227,6 +1230,8 @@ pub fn parse_agent_event_body(json: &[u8]) -> Option<AgentEvent> {
message: Option<String>,
#[serde(default)]
cwd: Option<String>,
#[serde(default)]
source: Option<String>,
#[serde(flatten)]
readout: AgentReadout,
}
@@ -1240,6 +1245,7 @@ pub fn parse_agent_event_body(json: &[u8]) -> Option<AgentEvent> {
session_id: nonempty(w.session_id),
message: nonempty(w.message),
cwd: nonempty(w.cwd).map(std::path::PathBuf::from),
source: nonempty(w.source),
readout: w.readout,
})
}
@@ -1258,6 +1264,7 @@ mod tests {
session_id: None,
message: None,
cwd: None,
source: None,
readout,
};
state.apply_event(&event(AgentReadout {
@@ -1769,6 +1776,7 @@ mod tests {
session_id: id.map(String::from),
message: msg.map(String::from),
cwd: None,
source: None,
readout: Default::default(),
};
@@ -1832,6 +1840,7 @@ mod tests {
session_id: Some("sid".into()),
message: None,
cwd: None,
source: None,
readout: Default::default(),
});
assert_eq!(s.status, AgentStatus::Idle);
@@ -1848,6 +1857,7 @@ mod tests {
session_id: None,
message: None,
cwd: None,
source: None,
readout: Default::default(),
};
@@ -1882,6 +1892,7 @@ mod tests {
session_id: None,
message: None,
cwd: None,
source: None,
readout: Default::default(),
};
@@ -1934,6 +1945,7 @@ mod tests {
session_id: None,
message: None,
cwd: None,
source: None,
readout: Default::default(),
};
@@ -1971,6 +1983,7 @@ mod tests {
session_id: None,
message: None,
cwd: None,
source: None,
readout: Default::default(),
};
@@ -2003,6 +2016,7 @@ mod tests {
session_id: None,
message: None,
cwd: cwd.map(PathBuf::from),
source: None,
readout: Default::default(),
};
+1
View File
@@ -6042,6 +6042,7 @@ mod tests {
session_id: None,
message: None,
cwd: None,
source: None,
readout: Default::default(),
}],
None,
+1 -4
View File
@@ -146,10 +146,7 @@ fn codex_report_pane(
named,
event.session_id.as_deref(),
event.cwd.as_deref(),
// Codex's SessionStart says what started the session; `clear` is /new.
serde_json::from_str::<serde_json::Value>(body)
.ok()
.is_some_and(|body| body["source"] == "clear"),
event.source.as_deref() == Some("clear"),
)?;
let pane = panes.into_iter().find(|p| p.id == target)?;
Some((pane, event))
+32 -27
View File
@@ -1855,21 +1855,23 @@ fn custom_commands(cwd: Option<&Path>, home: Option<&Path>) -> Vec<(String, Stri
),
];
// Directories are followed through symlinks, as Claude Code follows
// them; the bound keeps a link back up the tree from walking forever.
const MAX_DEPTH: usize = 8;
// them; each is read once, so a link back up the tree ends the walk
// instead of looping it.
let mut seen = std::collections::HashSet::new();
for (dir, scope) in roots {
let Some(dir) = dir else { continue };
let mut stack = vec![(dir, 0)];
while let Some((dir, depth)) = stack.pop() {
let mut stack = vec![dir];
while let Some(dir) = stack.pop() {
if !std::fs::canonicalize(&dir).is_ok_and(|real| seen.insert(real)) {
continue;
}
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if depth < MAX_DEPTH {
stack.push((path, depth + 1));
}
stack.push(path);
} else if path.extension().is_some_and(|e| e == "md")
&& let Some(stem) = path.file_stem().and_then(|s| s.to_str())
{
@@ -2170,17 +2172,20 @@ fn model_alias(model: &str) -> Option<&'static str> {
fn effort_label(level: &str) -> String {
match level {
"xhigh" => "Extra high".into(),
// A level is whatever the environment or the settings file said, so
// its first character need not be one byte.
_ => {
let mut chars = level.chars();
chars.next().map_or_else(String::new, |first| {
first.to_uppercase().chain(chars).collect()
})
}
_ => capitalized(level),
}
}
/// `text` with its first character upper-cased. A level is whatever the
/// environment or the settings file said, so that character need not be one
/// byte.
fn capitalized(text: &str) -> String {
let mut chars = text.chars();
chars.next().map_or_else(String::new, |first| {
first.to_uppercase().chain(chars).collect()
})
}
/// How the model list spells an alias.
fn alias_label(alias: &str) -> String {
if alias == "default" {
@@ -2190,7 +2195,7 @@ fn alias_label(alias: &str) -> String {
Some(family) => (family, true),
None => (alias, false),
};
let mut name = family[..1].to_uppercase() + &family[1..];
let mut name = capitalized(family);
if long {
name.push_str(" · 1M");
}
@@ -2275,15 +2280,16 @@ impl TerminalView {
/// and resizing their pane each time the box comes and goes would have
/// them redraw for a row of padding.
pub(super) fn composer_reserved_rows(&self) -> usize {
if !matches!(self.presence(), Presence::Covering(_)) {
let Some(c) = self
.composer
.as_ref()
.filter(|c| matches!(c.presence, Presence::Covering(_)))
else {
return 0;
}
};
if !self.agent().is_some_and(makes_room) {
return 0;
}
let Some(c) = self.composer.as_ref() else {
return 0;
};
let Some(frame) = c.frame_height.get() else {
return 0;
};
@@ -2619,13 +2625,13 @@ impl TerminalView {
Some(a) if !covers(a) => Presence::Docked,
Some(_) if asking => Presence::SteppedAside,
Some(_) => match &c.area {
Some(area) => Presence::Covering(screen_lines.saturating_sub(area.top + offset)),
Some(area) => {
c.area_rows = screen_lines.saturating_sub(area.top);
Presence::Covering(c.area_rows.saturating_sub(offset))
}
None => Presence::SteppedAside,
},
};
if let Some(area) = &c.area {
c.area_rows = screen_lines.saturating_sub(area.top);
}
if let Some(agent) = agent
&& c.named != Some(agent)
@@ -2646,8 +2652,7 @@ impl TerminalView {
c.refocus = c.presence == Presence::SteppedAside;
self.composer_focused = false;
window.focus(&self.focus_handle, cx);
} else if shown && ((c.refocus || c.grab) && !self.composer_focused || !c.typed.is_empty())
{
} else if shown {
let take_focus = (c.refocus || c.grab) && !self.composer_focused;
if take_focus {
c.refocus = false;