feat: sensorium ambient-context injection, wheel-scroll, click-to-copy fix
Sensorium: a new sensorium::ambient_line() builds a one-line ambient sense — current date/time and the connecting user — prepended to the user message every turn, for both the primary (send_with_signals) and the subconscious (subconscious_tool_loop). The subconscious no longer guesses the date; ledger entries get stamped with the real time. TUI scroll-wheel: the chat viewport now responds to mouse ScrollUp/ ScrollDown, routed through the same scroll field the arrow keys move (3 lines per notch). TUI click-to-copy fix: copy_message_at was hit-testing against stale line indices — span_spans recorded message positions against the pre-wrap buffer, but wrap_lines reflows it and shifts every index past a wrap point. draw_messages now builds a prefix-sum remap from pre- to post-wrap indices and translates the spans before storing them. arboard itself was fine; the bug was the bookkeeping.
This commit is contained in:
parent
ebb0bf4e65
commit
1a95139261
5 changed files with 62 additions and 19 deletions
|
|
@ -664,20 +664,24 @@ impl Backend for LocalBackend {
|
|||
.get(conversation_id)
|
||||
.map(|s| s.agent_id.clone());
|
||||
|
||||
// Ambient sense rides in front of every turn — the date/time and who
|
||||
// is present — so she is never guessing what year it is.
|
||||
let ambient = crate::core::sensorium::ambient_line();
|
||||
|
||||
let user_text = if let Some(agent_id) = session_agent_id {
|
||||
let surfacings = drain_intrusive_surfacings(&self.server, &agent_id).await;
|
||||
if surfacings.is_empty() {
|
||||
text.to_string()
|
||||
format!("{}\n{}", ambient, text)
|
||||
} else {
|
||||
let prelude = surfacings
|
||||
.iter()
|
||||
.map(|line| line.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
format!("{}\n{}", prelude, text)
|
||||
format!("{}\n{}\n{}", ambient, prelude, text)
|
||||
}
|
||||
} else {
|
||||
text.to_string()
|
||||
format!("{}\n{}", ambient, text)
|
||||
};
|
||||
|
||||
self.server.sessions.add_message(
|
||||
|
|
|
|||
|
|
@ -17,6 +17,17 @@
|
|||
use tokio::sync::mpsc;
|
||||
use tracing::debug;
|
||||
|
||||
/// A single line of ambient sense the agent receives with every turn:
|
||||
/// what time it is, who is present. Prepended to the user message so
|
||||
/// both the primary and the subconscious are oriented in time.
|
||||
pub fn ambient_line() -> String {
|
||||
let datetime = chrono::Local::now().format("%Y-%m-%d %H:%M").to_string();
|
||||
let user = std::env::var("USER")
|
||||
.or_else(|_| std::env::var("USERNAME"))
|
||||
.unwrap_or_else(|_| "someone".to_string());
|
||||
format!("[ ambient sense - {} - {} is here ]", datetime, user)
|
||||
}
|
||||
|
||||
/// Bandwidth classification for interface capability
|
||||
/// Higher bandwidth = richer telemetry and animation
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
|
|
|
|||
|
|
@ -482,15 +482,19 @@ Resolve entries: `[YYYY-MM-DD HH:MM] RESOLVED — note`"#;
|
|||
.map(|a| a.name)
|
||||
.unwrap_or_else(|_| "the primary".to_string());
|
||||
|
||||
// Ambient sense rides in front of the subconscious turn too — same
|
||||
// date/time and presence orientation the primary receives.
|
||||
let ambient = crate::core::sensorium::ambient_line();
|
||||
|
||||
let user_content = if user_message.is_empty() {
|
||||
format!(
|
||||
"{} responded:\n\n{}",
|
||||
primary_name, primary_response
|
||||
"{}\n{} responded:\n\n{}",
|
||||
ambient, primary_name, primary_response
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"User said:\n{}\n\n{} responded:\n{}",
|
||||
user_message, primary_name, primary_response
|
||||
"{}\nUser said:\n{}\n\n{} responded:\n{}",
|
||||
ambient, user_message, primary_name, primary_response
|
||||
)
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -515,18 +515,25 @@ impl App {
|
|||
};
|
||||
}
|
||||
Event::Mouse(m) => {
|
||||
// Left-click on a message bubble copies it to the
|
||||
// clipboard (the ⧉ in the bubble title is the cue).
|
||||
if self.current_screen == Screen::Chat
|
||||
&& matches!(
|
||||
m.kind,
|
||||
crossterm::event::MouseEventKind::Down(
|
||||
crossterm::event::MouseButton::Left
|
||||
)
|
||||
)
|
||||
{
|
||||
use crossterm::event::{MouseButton, MouseEventKind};
|
||||
if self.current_screen == Screen::Chat {
|
||||
if let Some(chat) = self.chat.as_mut() {
|
||||
chat.copy_message_at(m.column, m.row);
|
||||
match m.kind {
|
||||
// Left-click on a message bubble copies it
|
||||
// to the clipboard (the cue in the title).
|
||||
MouseEventKind::Down(MouseButton::Left) => {
|
||||
chat.copy_message_at(m.column, m.row);
|
||||
}
|
||||
// Wheel rides the same scroll field the
|
||||
// arrow keys move — a notch is three lines.
|
||||
MouseEventKind::ScrollUp => {
|
||||
chat.scroll = chat.scroll.saturating_add(3);
|
||||
}
|
||||
MouseEventKind::ScrollDown => {
|
||||
chat.scroll = chat.scroll.saturating_sub(3);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2276,8 +2276,25 @@ fn draw_messages(f: &mut Frame, state: &ChatState, area: Rect) {
|
|||
// notices, raw text, anything that bypassed bubble pre-wrap) gets
|
||||
// wrapped here. After this, `lines.len()` equals the visible line
|
||||
// count — Paragraph's wrap becomes a no-op and scroll math holds.
|
||||
//
|
||||
// Wrapping can split one pre-wrap line into several, which shifts every
|
||||
// line index that follows. `span_spans` was recorded against the
|
||||
// pre-wrap buffer, so build a prefix-sum remap from pre-wrap index to
|
||||
// post-wrap index and translate the spans — otherwise a mouse click
|
||||
// lands on the wrong bubble (or none).
|
||||
let visible_width = area.width.saturating_sub(0) as usize;
|
||||
let lines = markdown::wrap_lines(lines, visible_width);
|
||||
let mut wrap_remap: Vec<usize> = Vec::with_capacity(lines.len() + 1);
|
||||
let mut wrapped_lines: Vec<Line<'static>> = Vec::with_capacity(lines.len());
|
||||
for line in lines {
|
||||
wrap_remap.push(wrapped_lines.len());
|
||||
wrapped_lines.extend(markdown::wrap_line(line, visible_width));
|
||||
}
|
||||
wrap_remap.push(wrapped_lines.len());
|
||||
let lines = wrapped_lines;
|
||||
for (_, s, e) in span_spans.iter_mut() {
|
||||
*s = wrap_remap.get(*s).copied().unwrap_or(*s);
|
||||
*e = wrap_remap.get(*e).copied().unwrap_or(*e);
|
||||
}
|
||||
|
||||
// Trim trailing empty lines from the count (each bubble appends a
|
||||
// blank separator; the last one shouldn't push the final real line
|
||||
|
|
|
|||
Loading…
Reference in a new issue