lattice_agent/log/
modes.rs1use lattice_mode::{
23 BufferStoreHandle, CapabilitySet, LifecycleFuture, Mode, ModeContext, ModeId, ModeKind,
24 OptionOverrideSet, Subscription,
25};
26
27pub struct AiLogMode;
30
31impl AiLogMode {
32 pub fn mode_id() -> ModeId {
33 ModeId::new("ai-log-mode")
34 }
35}
36
37impl Mode for AiLogMode {
38 type Guard = Option<Subscription>;
39 fn id(&self) -> ModeId {
40 Self::mode_id()
41 }
42 fn kind(&self) -> ModeKind {
43 ModeKind::Major
44 }
45 fn options(&self) -> OptionOverrideSet {
46 lattice_config::overrides! {
47 lattice_config::ReadOnly = true,
48 lattice_config::NoFile = true,
49 }
50 }
51 fn required_capabilities(&self) -> CapabilitySet {
52 CapabilitySet::empty()
53 }
54
55 fn on_activate(&self, ctx: ModeContext) -> LifecycleFuture<'_, Self::Guard> {
56 Box::pin(async move {
57 let buffer_id = lattice_core::BufferId(ctx.buffer_id().0 as u32);
58 let Some(store) = ctx.service::<BufferStoreHandle>() else {
59 return Ok(None);
60 };
61 let Some(name) = store.name_for(buffer_id) else {
62 return Ok(None);
63 };
64 let Some(key) = super::buffer_names::parse_ai_log_name(&name) else {
65 return Ok(None);
66 };
67 let Some(handle) = store.handle_for(buffer_id) else {
68 return Ok(None);
69 };
70 let Ok(runtime) = tokio::runtime::Handle::try_current() else {
71 return Ok(None);
72 };
73
74 if let Some(logger) = ctx.service::<super::ai_log::AiLogger>() {
79 let snap = logger.snapshot_session(&key);
80 let mut text = String::new();
81 for record in snap.iter() {
82 let line = super::ai_log::format_ai_log_line(
83 record.timestamp,
84 Some(&key),
85 super::ai_log::level_tag(record.level),
86 record.source.tag(),
87 &record.message,
88 );
89 text.push_str(&line);
90 text.push('\n');
91 }
92 if !text.is_empty() {
93 let snapshot = handle.snapshot();
94 let last_line = snapshot.buffer.rope_line_count().saturating_sub(1);
97 let line_text = snapshot.buffer.line(last_line).unwrap_or_default();
98 let pos = lattice_protocol::position::Position::new(
99 last_line,
100 line_text.len() as u32,
101 );
102 let edit = lattice_protocol::edit::Edit::insert(pos, text);
103 let handle_seed = handle.clone();
104 runtime.spawn(async move {
105 let _ = handle_seed.apply_edit_batch(vec![edit]).await;
106 });
107 }
108 }
109
110 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<super::ai_log::AiLogPushed>();
111 let sub_id = ctx
112 .events()
113 .subscribe_typed::<super::ai_log::AiLogPushed>(tx);
114 let bus_handle = ctx.events_handle();
115
116 let filter_key = key.clone();
117 runtime.spawn(async move {
118 while let Some(first) = rx.recv().await {
119 let mut batch: Vec<super::ai_log::AiLogPushed> = vec![first];
120 while let Ok(more) = rx.try_recv() {
121 batch.push(more);
122 }
123 let mut text = String::new();
124 for event in batch
125 .iter()
126 .filter(|e| e.session.as_ref() == Some(&filter_key))
127 {
128 let line = super::ai_log::format_ai_log_line(
129 event.timestamp,
130 Some(&filter_key),
131 &event.level,
132 &event.source,
133 &event.message,
134 );
135 text.push_str(&line);
136 text.push('\n');
137 }
138 if text.is_empty() {
139 continue;
140 }
141 let snap = handle.snapshot();
142 let last_line = snap.buffer.rope_line_count().saturating_sub(1);
145 let line_text = snap.buffer.line(last_line).unwrap_or_default();
146 let pos = lattice_protocol::position::Position::new(
147 last_line,
148 line_text.len() as u32,
149 );
150 let edit = lattice_protocol::edit::Edit::insert(pos, text);
151 let _ = handle.apply_edit_batch(vec![edit]).await;
152 }
153 });
154
155 Ok(Some(Subscription::new(bus_handle, sub_id)))
156 })
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn mode_id_is_ai_log_mode() {
166 assert_eq!(AiLogMode::mode_id(), ModeId::new("ai-log-mode"));
167 }
168
169 #[test]
170 fn options_are_read_only_and_no_file() {
171 let overrides = AiLogMode.options();
172 let has_true = |type_id: std::any::TypeId| {
173 overrides.iter().any(|ov| {
174 ov.option_type_id == type_id && ov.downcast_value::<bool>() == Some(&true)
175 })
176 };
177 assert!(
178 has_true(std::any::TypeId::of::<lattice_config::ReadOnly>()),
179 "expected ReadOnly = true override"
180 );
181 assert!(
182 has_true(std::any::TypeId::of::<lattice_config::NoFile>()),
183 "expected NoFile = true override"
184 );
185 }
186
187 #[test]
188 fn kind_is_major_with_no_capability_requirements() {
189 assert_eq!(AiLogMode.kind(), ModeKind::Major);
190 assert_eq!(AiLogMode.required_capabilities(), CapabilitySet::empty());
191 }
192}