Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -134,3 +134,5 @@ coverage/
# Git worktrees
.worktrees/
worktrees/
vibehq-hub/
.docx
7 changes: 7 additions & 0 deletions crates/codirigent-core/src/persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ pub struct PersistentSession {
pub status: SessionStatus,
/// Working directory.
pub working_directory: PathBuf,
/// Requested shell for this session. `None` means Auto.
#[serde(default)]
pub shell: Option<String>,
/// Current task if any.
pub current_task: Option<TaskId>,
/// Git worktree path if using worktrees.
Expand Down Expand Up @@ -108,6 +111,7 @@ impl PersistentSession {
name: session.name.clone(),
status: session.status,
working_directory: session.working_directory.clone(),
shell: session.shell.clone(),
current_task: session.current_task.clone(),
worktree_path: None,
context_usage: session.context_usage,
Expand Down Expand Up @@ -183,6 +187,7 @@ impl PersistentSession {
name: self.name.clone(),
status: SessionStatus::Idle, // Reset status on restore
working_directory: self.working_directory.clone(),
shell: self.shell.clone(),
current_task: self.current_task.clone(),
context_usage: None, // Reset on restore
created_at: self.started_at,
Expand Down Expand Up @@ -569,6 +574,7 @@ mod tests {
#[test]
fn test_persistent_session_roundtrip() {
let mut session = Session::new(SessionId(1), "Test".to_string(), PathBuf::from("/tmp"));
session.shell = Some("bash".to_string());
session.group = Some("backend".to_string());
session.color = Some("#FF0000".to_string());

Expand All @@ -578,6 +584,7 @@ mod tests {
assert_eq!(restored.id, session.id);
assert_eq!(restored.name, session.name);
assert_eq!(restored.working_directory, session.working_directory);
assert_eq!(restored.shell, Some("bash".to_string()));
assert_eq!(restored.group, session.group);
assert_eq!(restored.color, session.color);
// Status should be reset to Idle
Expand Down
50 changes: 50 additions & 0 deletions crates/codirigent-core/src/types/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,56 @@ impl LayoutNode {
}
}

/// Adjust the split ratio for the divider between two child subtrees.
///
/// Finds the split node whose first subtree contains `first_slot` and whose
/// second subtree contains `second_slot`, then updates that split's ratio.
/// Returns `None` if no such split exists.
pub fn set_ratio_for_divider(
&self,
first_slot: SlotId,
second_slot: SlotId,
new_ratio: f32,
) -> Option<LayoutNode> {
let clamped = new_ratio.clamp(0.1, 0.9);
match self {
LayoutNode::Leaf { .. } => None,
LayoutNode::Split {
direction,
ratio,
first,
second,
} => {
if first.contains_slot(first_slot) && second.contains_slot(second_slot) {
Some(LayoutNode::Split {
direction: *direction,
ratio: clamped,
first: first.clone(),
second: second.clone(),
})
} else if let Some(new_first) =
first.set_ratio_for_divider(first_slot, second_slot, new_ratio)
{
Some(LayoutNode::Split {
direction: *direction,
ratio: *ratio,
first: Box::new(new_first),
second: second.clone(),
})
} else {
second
.set_ratio_for_divider(first_slot, second_slot, new_ratio)
.map(|new_second| LayoutNode::Split {
direction: *direction,
ratio: *ratio,
first: first.clone(),
second: Box::new(new_second),
})
}
}
}
}

fn direct_child_has_slot(&self, child: &LayoutNode, target: SlotId) -> bool {
matches!(child, LayoutNode::Leaf { slot } if *slot == target)
}
Expand Down
2 changes: 1 addition & 1 deletion crates/codirigent-core/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub use git::{GitChangeKind, GitChangedFile, GitRepoInfo};
pub use ids::{SessionId, TaskId};
pub use layout::{GridPosition, LayoutMode, LayoutNode, SlotId, SplitDirection};
pub use session::{CodexExecutionMode, Session};
pub use state::{AppState, QueueState, WindowState};
pub use state::{AppState, PaneId, PaneStackState, PaneTabGroup, QueueState, WindowState};
pub use status::{ContextThresholdState, SessionStatus, ShellState, TaskPriority, TaskStatus};
pub use task::{RetryConfig, Task, VerificationConfig};
pub use verification::{TestFailure, TestResults, VerificationResult};
Expand Down
4 changes: 4 additions & 0 deletions crates/codirigent-core/src/types/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ pub struct Session {
pub status: SessionStatus,
/// Working directory for this session.
pub working_directory: PathBuf,
/// Requested shell for this session. `None` means Auto.
#[serde(default)]
pub shell: Option<String>,
/// Currently assigned task, if any.
pub current_task: Option<TaskId>,
/// Context window usage (0.0 - 1.0), if available.
Expand Down Expand Up @@ -73,6 +76,7 @@ impl Session {
name,
status: SessionStatus::default(),
working_directory,
shell: None,
current_task: None,
context_usage: None,
created_at: chrono::Utc::now(),
Expand Down
45 changes: 43 additions & 2 deletions crates/codirigent-core/src/types/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,45 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

use super::ids::TaskId;
use super::layout::LayoutMode;
use super::ids::{SessionId, TaskId};
use super::layout::{LayoutMode, SlotId};
use super::session::Session;

/// Persistent identifier for a visible pane.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub enum PaneId {
/// Grid-layout pane by stable cell index.
GridCell {
/// Zero-based grid cell index in row-major order.
index: usize,
},
/// Split-tree pane by slot identifier.
SplitSlot {
/// Stable split-tree slot identifier.
slot: SlotId,
},
}

/// Persistent tab state for a visible pane.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PaneTabGroup {
/// Pane that owns the tab stack.
pub pane: PaneId,
/// Ordered session IDs in the tab strip.
pub session_ids: Vec<SessionId>,
/// Active session currently rendered in the pane.
pub active_session_id: SessionId,
}

/// Persisted ordered pane stack state, including hidden stacks.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PaneStackState {
/// Ordered session IDs in the stack.
pub session_ids: Vec<SessionId>,
/// Active session currently rendered when the stack is visible.
pub active_session_id: SessionId,
}

/// Persisted window position and size.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WindowState {
Expand All @@ -29,6 +64,12 @@ pub struct AppState {
pub sessions: Vec<Session>,
/// Current layout mode.
pub layout: LayoutMode,
/// Persisted per-pane tab stacks.
#[serde(default)]
pub pane_tab_groups: Vec<PaneTabGroup>,
/// Persisted pane stacks in workspace order, including hidden stacks.
#[serde(default)]
pub pane_stacks: Vec<PaneStackState>,
/// Last updated timestamp.
pub updated_at: Option<chrono::DateTime<chrono::Utc>>,
/// Saved window position and size.
Expand Down
6 changes: 6 additions & 0 deletions crates/codirigent-core/tests/persistence_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ fn test_save_and_load_state() {
name: "Test Session".to_string(),
status: SessionStatus::Idle,
working_directory: temp.path().to_path_buf(),
shell: None,
current_task: None,
context_usage: None,
created_at: chrono::Utc::now(),
Expand Down Expand Up @@ -90,6 +91,7 @@ fn test_overwrite_state() {
name: "Session 1".to_string(),
status: SessionStatus::Idle,
working_directory: temp.path().to_path_buf(),
shell: None,
current_task: None,
context_usage: None,
created_at: chrono::Utc::now(),
Expand All @@ -115,6 +117,7 @@ fn test_overwrite_state() {
name: "Session 2".to_string(),
status: SessionStatus::Idle,
working_directory: temp.path().to_path_buf(),
shell: None,
current_task: None,
context_usage: None,
created_at: chrono::Utc::now(),
Expand Down Expand Up @@ -256,6 +259,7 @@ fn test_multiple_checkpoints_independent() {
name: "State 1".to_string(),
status: SessionStatus::Idle,
working_directory: temp.path().to_path_buf(),
shell: None,
current_task: None,
context_usage: None,
created_at: chrono::Utc::now(),
Expand All @@ -278,6 +282,7 @@ fn test_multiple_checkpoints_independent() {
name: "State 2".to_string(),
status: SessionStatus::Idle,
working_directory: temp.path().to_path_buf(),
shell: None,
current_task: None,
context_usage: None,
created_at: chrono::Utc::now(),
Expand Down Expand Up @@ -370,6 +375,7 @@ fn test_session_to_persistent_conversion() {
name: "Test".to_string(),
status: SessionStatus::Working,
working_directory: PathBuf::from("/tmp"),
shell: None,
current_task: None,
context_usage: Some(0.5),
created_at: chrono::Utc::now(),
Expand Down
1 change: 1 addition & 0 deletions crates/codirigent-session/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,7 @@ impl SessionManager for DefaultSessionManager {

// Create session metadata
let mut session = Session::new(id, name, working_dir.clone());
session.shell = shell.filter(|value| !value.is_empty());

// Detect git info for the working directory
session.git_info = self
Expand Down
4 changes: 4 additions & 0 deletions crates/codirigent-ui/src/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,8 @@ impl CodirigentIntegration {
let state = AppState {
sessions,
layout: codirigent_core::LayoutMode::default(),
pane_tab_groups: Vec::new(),
pane_stacks: Vec::new(),
updated_at: Some(chrono::Utc::now()),
window_bounds: None,
};
Expand Down Expand Up @@ -562,6 +564,8 @@ impl CodirigentIntegration {
let state = AppState {
sessions,
layout: codirigent_core::LayoutMode::default(),
pane_tab_groups: Vec::new(),
pane_stacks: Vec::new(),
updated_at: Some(chrono::Utc::now()),
window_bounds: None,
};
Expand Down
23 changes: 23 additions & 0 deletions crates/codirigent-ui/src/layout/split.rs
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,29 @@ mod tests {
assert!(layout.divider_at_point(Point::new(100.0, 400.0)).is_none());
}

#[test]
fn test_split_layout_divider_at_point_finds_nested_parent_divider() {
let root = LayoutNode::Split {
direction: SplitDirection::Horizontal,
ratio: 0.5,
first: Box::new(LayoutNode::Split {
direction: SplitDirection::Vertical,
ratio: 0.5,
first: Box::new(LayoutNode::Leaf { slot: SlotId(0) }),
second: Box::new(LayoutNode::Leaf { slot: SlotId(1) }),
}),
second: Box::new(LayoutNode::Leaf { slot: SlotId(2) }),
};
let layout = SplitLayout::new(root, Bounds::from_size(1000.0, 800.0), 4.0);

let divider = layout
.divider_at_point(Point::new(499.0, 400.0))
.expect("expected nested parent divider");
assert_eq!(divider.first_slot, SlotId(0));
assert_eq!(divider.second_slot, SlotId(2));
assert_eq!(divider.direction, SplitDirection::Horizontal);
}

#[test]
fn test_split_layout_asymmetric() {
// 2 stacked on left + 1 full-height on right
Expand Down
Loading
Loading