|
|
@@ -47,6 +47,9 @@ struct WindowGeometryKey {
|
|
|
struct ConfigKey {
|
|
|
position: String,
|
|
|
size: String,
|
|
|
+ gif_pack: String,
|
|
|
+ loop_style: String,
|
|
|
+ speed_bits: u32,
|
|
|
}
|
|
|
|
|
|
fn grid_cols(n: usize) -> usize {
|
|
|
@@ -78,16 +81,54 @@ fn config_key(config: Option<&CompanionConfigState>) -> Option<ConfigKey> {
|
|
|
config.map(|cfg| ConfigKey {
|
|
|
position: cfg.position.clone(),
|
|
|
size: cfg.size.clone(),
|
|
|
+ gif_pack: normalized_gif_pack(&cfg.gif_pack).to_string(),
|
|
|
+ loop_style: normalized_loop_style(&cfg.loop_style).to_string(),
|
|
|
+ speed_bits: normalized_speed(cfg.speed).to_bits(),
|
|
|
})
|
|
|
}
|
|
|
|
|
|
-fn apply_config(key: Option<&ConfigKey>, position: &mut String, size: &mut f32) {
|
|
|
+fn normalized_gif_pack(pack: &str) -> &str {
|
|
|
+ match pack {
|
|
|
+ "default" => "default",
|
|
|
+ _ => "default",
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+fn normalized_loop_style(style: &str) -> &str {
|
|
|
+ match style {
|
|
|
+ "smooth" => "smooth",
|
|
|
+ _ => "classic",
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+fn normalized_speed(speed: f32) -> f32 {
|
|
|
+ if speed.is_finite() {
|
|
|
+ speed.clamp(0.25, 4.0)
|
|
|
+ } else {
|
|
|
+ 1.0
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+fn apply_config(
|
|
|
+ key: Option<&ConfigKey>,
|
|
|
+ position: &mut String,
|
|
|
+ size: &mut f32,
|
|
|
+ gif_pack: &mut String,
|
|
|
+ loop_style: &mut String,
|
|
|
+ speed: &mut f32,
|
|
|
+) {
|
|
|
if let Some(cfg) = key {
|
|
|
*position = cfg.position.clone();
|
|
|
*size = size_from_config(&cfg.size);
|
|
|
+ *gif_pack = cfg.gif_pack.clone();
|
|
|
+ *loop_style = cfg.loop_style.clone();
|
|
|
+ *speed = f32::from_bits(cfg.speed_bits);
|
|
|
} else {
|
|
|
*position = "bottom-right".to_string();
|
|
|
*size = DEFAULT_SIZE;
|
|
|
+ *gif_pack = "default".to_string();
|
|
|
+ *loop_style = "classic".to_string();
|
|
|
+ *speed = 1.0;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -127,6 +168,23 @@ fn restore_window_position(pos: [f32; 2], screen: [f32; 2], win: [f32; 2]) -> [f
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+fn stack_window_position(
|
|
|
+ position: [f32; 2],
|
|
|
+ anchor: &str,
|
|
|
+ rank: usize,
|
|
|
+ screen: [f32; 2],
|
|
|
+ win: [f32; 2],
|
|
|
+) -> [f32; 2] {
|
|
|
+ let offset = (rank.min(8) as f32) * 18.0;
|
|
|
+ let stacked = match anchor {
|
|
|
+ "bottom-left" => [position[0] + offset, position[1] - offset],
|
|
|
+ "top-right" => [position[0] - offset, position[1] + offset],
|
|
|
+ "top-left" => [position[0] + offset, position[1] + offset],
|
|
|
+ _ => [position[0] - offset, position[1] - offset],
|
|
|
+ };
|
|
|
+ clamp_window_position(stacked, screen, win)
|
|
|
+}
|
|
|
+
|
|
|
fn canonical_project_key(cwd: &str) -> String {
|
|
|
std::path::Path::new(cwd)
|
|
|
.canonicalize()
|
|
|
@@ -167,12 +225,14 @@ fn choose_session(sessions: &[SessionInfo]) -> Option<usize> {
|
|
|
sessions
|
|
|
.iter()
|
|
|
.enumerate()
|
|
|
+ .rev()
|
|
|
.find(|(_, s)| s.status == "waiting-input")
|
|
|
.map(|(i, _)| i)
|
|
|
.or_else(|| {
|
|
|
sessions
|
|
|
.iter()
|
|
|
.enumerate()
|
|
|
+ .rev()
|
|
|
.find(|(_, s)| s.active_agents.iter().any(|agent| agent != "intro"))
|
|
|
.map(|(i, _)| i)
|
|
|
})
|
|
|
@@ -180,24 +240,40 @@ fn choose_session(sessions: &[SessionInfo]) -> Option<usize> {
|
|
|
sessions
|
|
|
.iter()
|
|
|
.enumerate()
|
|
|
+ .rev()
|
|
|
.find(|(_, s)| s.status == "busy")
|
|
|
.map(|(i, _)| i)
|
|
|
})
|
|
|
- .or_else(|| sessions.first().map(|_| 0))
|
|
|
+ .or_else(|| sessions.last().map(|_| sessions.len() - 1))
|
|
|
+}
|
|
|
+
|
|
|
+fn choose_owned_session(sessions: &[SessionInfo], owner_session_id: Option<&str>) -> Option<usize> {
|
|
|
+ if let Some(owner_session_id) = owner_session_id {
|
|
|
+ return sessions
|
|
|
+ .iter()
|
|
|
+ .position(|session| session.session_id == owner_session_id);
|
|
|
+ }
|
|
|
+
|
|
|
+ choose_session(sessions)
|
|
|
}
|
|
|
|
|
|
pub struct CompanionApp {
|
|
|
state_path: std::path::PathBuf,
|
|
|
+ owner_session_id: Option<String>,
|
|
|
sessions: Vec<SessionInfo>,
|
|
|
gifs: Gifs,
|
|
|
rx: Receiver<()>,
|
|
|
registered: bool,
|
|
|
size: f32,
|
|
|
+ gif_pack: String,
|
|
|
+ loop_style: String,
|
|
|
+ speed: f32,
|
|
|
screen: [f32; 2],
|
|
|
position: String,
|
|
|
has_modern_config: bool,
|
|
|
applied_config: Option<ConfigKey>,
|
|
|
applied_geometry: Option<WindowGeometryKey>,
|
|
|
+ last_logged_selection: Option<String>,
|
|
|
window_positions: std::collections::BTreeMap<String, WindowPositionState>,
|
|
|
project_keys: std::collections::BTreeMap<String, String>,
|
|
|
drag_project_key: Option<String>,
|
|
|
@@ -207,30 +283,53 @@ pub struct CompanionApp {
|
|
|
impl CompanionApp {
|
|
|
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
|
|
let state_path = crate::state::state_file_path();
|
|
|
+ let owner_session_id = std::env::var("OH_MY_OPENCODE_SLIM_COMPANION_SESSION_ID")
|
|
|
+ .ok()
|
|
|
+ .filter(|session_id| !session_id.trim().is_empty());
|
|
|
let state = read_state(&state_path);
|
|
|
+ crate::log::debug(format!(
|
|
|
+ "app new owner={:?} initial_sessions={}",
|
|
|
+ owner_session_id,
|
|
|
+ state.sessions.len()
|
|
|
+ ));
|
|
|
let sessions = state.sessions;
|
|
|
let window_positions = state.window_positions;
|
|
|
|
|
|
let mut initial_size = DEFAULT_SIZE;
|
|
|
let mut position = "bottom-right".to_string();
|
|
|
+ let mut gif_pack = "default".to_string();
|
|
|
+ let mut loop_style = "classic".to_string();
|
|
|
+ let mut speed = 1.0;
|
|
|
let has_modern_config = state.config.is_some();
|
|
|
let applied_config = config_key(state.config.as_ref());
|
|
|
- apply_config(applied_config.as_ref(), &mut position, &mut initial_size);
|
|
|
+ apply_config(
|
|
|
+ applied_config.as_ref(),
|
|
|
+ &mut position,
|
|
|
+ &mut initial_size,
|
|
|
+ &mut gif_pack,
|
|
|
+ &mut loop_style,
|
|
|
+ &mut speed,
|
|
|
+ );
|
|
|
|
|
|
let rx = start_watcher(state_path.clone());
|
|
|
|
|
|
Self {
|
|
|
state_path,
|
|
|
+ owner_session_id,
|
|
|
sessions,
|
|
|
gifs: Gifs::new(),
|
|
|
rx,
|
|
|
registered: false,
|
|
|
size: initial_size,
|
|
|
+ gif_pack,
|
|
|
+ loop_style,
|
|
|
+ speed,
|
|
|
screen: primary_size(),
|
|
|
position,
|
|
|
has_modern_config,
|
|
|
applied_config,
|
|
|
applied_geometry: None,
|
|
|
+ last_logged_selection: None,
|
|
|
window_positions,
|
|
|
project_keys: std::collections::BTreeMap::new(),
|
|
|
drag_project_key: None,
|
|
|
@@ -243,6 +342,12 @@ impl CompanionApp {
|
|
|
while self.rx.try_recv().is_ok() {}
|
|
|
let state = read_state(&self.state_path);
|
|
|
self.sessions = state.sessions;
|
|
|
+ crate::log::debug(format!(
|
|
|
+ "state update owner={:?} sessions={} config={:?}",
|
|
|
+ self.owner_session_id,
|
|
|
+ self.sessions.len(),
|
|
|
+ state.config
|
|
|
+ ));
|
|
|
self.window_positions = state.window_positions;
|
|
|
self.project_keys
|
|
|
.retain(|cwd, _| self.sessions.iter().any(|session| &session.cwd == cwd));
|
|
|
@@ -250,7 +355,14 @@ impl CompanionApp {
|
|
|
let next_config = config_key(state.config.as_ref());
|
|
|
let config_changed = self.applied_config != next_config;
|
|
|
if config_changed {
|
|
|
- apply_config(next_config.as_ref(), &mut self.position, &mut self.size);
|
|
|
+ apply_config(
|
|
|
+ next_config.as_ref(),
|
|
|
+ &mut self.position,
|
|
|
+ &mut self.size,
|
|
|
+ &mut self.gif_pack,
|
|
|
+ &mut self.loop_style,
|
|
|
+ &mut self.speed,
|
|
|
+ );
|
|
|
self.applied_config = next_config;
|
|
|
}
|
|
|
return config_changed;
|
|
|
@@ -306,7 +418,18 @@ impl eframe::App for CompanionApp {
|
|
|
|
|
|
self.size = ctx.data(|d| d.get_temp(egui::Id::new(SIZE_KEY)).unwrap_or(self.size));
|
|
|
|
|
|
- let Some(selected_idx) = choose_session(&self.sessions) else {
|
|
|
+ let Some(selected_idx) =
|
|
|
+ choose_owned_session(&self.sessions, self.owner_session_id.as_deref())
|
|
|
+ else {
|
|
|
+ if self.owner_session_id.is_some() {
|
|
|
+ crate::log::debug(format!(
|
|
|
+ "close owner session missing owner={:?} sessions={}",
|
|
|
+ self.owner_session_id,
|
|
|
+ self.sessions.len()
|
|
|
+ ));
|
|
|
+ ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
|
|
+ return;
|
|
|
+ }
|
|
|
egui::CentralPanel::default()
|
|
|
.frame(egui::Frame::none().fill(egui::Color32::TRANSPARENT))
|
|
|
.show(ctx, |ui| {
|
|
|
@@ -319,15 +442,31 @@ impl eframe::App for CompanionApp {
|
|
|
};
|
|
|
|
|
|
let session = self.sessions[selected_idx].clone();
|
|
|
+ let selection_log_key = format!(
|
|
|
+ "{}|{}|{}|{:?}",
|
|
|
+ session.session_id, session.cwd, session.status, session.active_agents
|
|
|
+ );
|
|
|
+ if self.last_logged_selection.as_ref() != Some(&selection_log_key) {
|
|
|
+ crate::log::debug(format!(
|
|
|
+ "selected owner={:?} idx={} session_id={} cwd={} status={} agents={:?}",
|
|
|
+ self.owner_session_id,
|
|
|
+ selected_idx,
|
|
|
+ session.session_id,
|
|
|
+ session.cwd,
|
|
|
+ session.status,
|
|
|
+ session.active_agents
|
|
|
+ ));
|
|
|
+ self.last_logged_selection = Some(selection_log_key);
|
|
|
+ }
|
|
|
let project_key = self.project_key_for(&session.cwd);
|
|
|
let saved_position = self.window_positions.get(&project_key).copied();
|
|
|
let agent_uris: Vec<String> = if session.active_agents.is_empty() {
|
|
|
- vec![self.gifs.uri("intro")]
|
|
|
+ vec![self.gifs.uri("intro", &self.gif_pack)]
|
|
|
} else {
|
|
|
session
|
|
|
.active_agents
|
|
|
.iter()
|
|
|
- .map(|agent| self.gifs.uri(agent))
|
|
|
+ .map(|agent| self.gifs.uri(agent, &self.gif_pack))
|
|
|
.collect()
|
|
|
};
|
|
|
let n = agent_uris.len().max(1);
|
|
|
@@ -350,7 +489,26 @@ impl eframe::App for CompanionApp {
|
|
|
ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(egui::vec2(win_w, win_h)));
|
|
|
let pos = saved_position
|
|
|
.map(|pos| restore_window_position([pos.x, pos.y], self.screen, [win_w, win_h]))
|
|
|
- .unwrap_or_else(|| place_window(&self.position, self.screen, [win_w, win_h]));
|
|
|
+ .unwrap_or_else(|| {
|
|
|
+ stack_window_position(
|
|
|
+ place_window(&self.position, self.screen, [win_w, win_h]),
|
|
|
+ &self.position,
|
|
|
+ selected_idx,
|
|
|
+ self.screen,
|
|
|
+ [win_w, win_h],
|
|
|
+ )
|
|
|
+ });
|
|
|
+ crate::log::debug(format!(
|
|
|
+ "geometry owner={:?} session_id={} saved_position={:?} pos={:?} win=({}, {}) screen={:?} selected_idx={}",
|
|
|
+ self.owner_session_id,
|
|
|
+ session.session_id,
|
|
|
+ saved_position,
|
|
|
+ pos,
|
|
|
+ win_w,
|
|
|
+ win_h,
|
|
|
+ self.screen,
|
|
|
+ selected_idx
|
|
|
+ ));
|
|
|
ctx.send_viewport_cmd(egui::ViewportCommand::OuterPosition(egui::pos2(
|
|
|
pos[0], pos[1],
|
|
|
)));
|
|
|
@@ -401,6 +559,11 @@ impl eframe::App for CompanionApp {
|
|
|
)
|
|
|
.show(ctx, |ui| {
|
|
|
ui.spacing_mut().item_spacing = egui::Vec2::ZERO;
|
|
|
+ // egui_extras' GIF image loader controls playback from the GIF
|
|
|
+ // bytes, so speed and loopStyle are currently plumbed through
|
|
|
+ // state for future support but not applied at render time.
|
|
|
+ let _speed = self.speed;
|
|
|
+ let _loop_style = &self.loop_style;
|
|
|
render_session(ui, ctx, &session, &agent_uris, self.size, win_w, win_h);
|
|
|
});
|
|
|
|