tui: filter panel pods by workspace
This commit is contained in:
parent
e2e76d3beb
commit
3b634d66ca
|
|
@ -100,6 +100,8 @@ pub struct PodMetadata {
|
||||||
pub pod_name: String,
|
pub pod_name: String,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub active: Option<PodActiveSegmentRef>,
|
pub active: Option<PodActiveSegmentRef>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub workspace_root: Option<PathBuf>,
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
pub spawned_children: Vec<PodSpawnedChild>,
|
pub spawned_children: Vec<PodSpawnedChild>,
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
|
|
@ -116,12 +118,18 @@ impl PodMetadata {
|
||||||
Self {
|
Self {
|
||||||
pod_name: pod_name.into(),
|
pod_name: pod_name.into(),
|
||||||
active,
|
active,
|
||||||
|
workspace_root: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: Vec::new(),
|
peers: Vec::new(),
|
||||||
resolved_manifest_snapshot: None,
|
resolved_manifest_snapshot: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_workspace_root(mut self, workspace_root: PathBuf) -> Self {
|
||||||
|
self.workspace_root = Some(workspace_root);
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sync persistence backend for Pod metadata.
|
/// Sync persistence backend for Pod metadata.
|
||||||
|
|
|
||||||
|
|
@ -1108,6 +1108,7 @@ mod tests {
|
||||||
let parent = PodMetadata {
|
let parent = PodMetadata {
|
||||||
pod_name: "parent".into(),
|
pod_name: "parent".into(),
|
||||||
active: None,
|
active: None,
|
||||||
|
workspace_root: None,
|
||||||
spawned_children: vec![
|
spawned_children: vec![
|
||||||
child("child-live", &live_socket),
|
child("child-live", &live_socket),
|
||||||
child("child-stale", &stale_socket),
|
child("child-stale", &stale_socket),
|
||||||
|
|
@ -1127,6 +1128,7 @@ mod tests {
|
||||||
session_id,
|
session_id,
|
||||||
active_child_segment,
|
active_child_segment,
|
||||||
)),
|
)),
|
||||||
|
workspace_root: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: Vec::new(),
|
peers: Vec::new(),
|
||||||
|
|
@ -1140,6 +1142,7 @@ mod tests {
|
||||||
session_id,
|
session_id,
|
||||||
active_child_segment,
|
active_child_segment,
|
||||||
)),
|
)),
|
||||||
|
workspace_root: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: Vec::new(),
|
peers: Vec::new(),
|
||||||
|
|
@ -1150,6 +1153,7 @@ mod tests {
|
||||||
.write(&PodMetadata {
|
.write(&PodMetadata {
|
||||||
pod_name: "child-pending".into(),
|
pod_name: "child-pending".into(),
|
||||||
active: Some(PodActiveSegmentRef::pending_segment(pending_session_id)),
|
active: Some(PodActiveSegmentRef::pending_segment(pending_session_id)),
|
||||||
|
workspace_root: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: Vec::new(),
|
peers: Vec::new(),
|
||||||
|
|
@ -1163,6 +1167,7 @@ mod tests {
|
||||||
session_id,
|
session_id,
|
||||||
new_segment_id(),
|
new_segment_id(),
|
||||||
)),
|
)),
|
||||||
|
workspace_root: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: Vec::new(),
|
peers: Vec::new(),
|
||||||
|
|
@ -1173,6 +1178,7 @@ mod tests {
|
||||||
.write(&PodMetadata {
|
.write(&PodMetadata {
|
||||||
pod_name: "peer".into(),
|
pod_name: "peer".into(),
|
||||||
active: None,
|
active: None,
|
||||||
|
workspace_root: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: vec![pod_store::PodPeer {
|
peers: vec![pod_store::PodPeer {
|
||||||
|
|
@ -1366,6 +1372,7 @@ mod tests {
|
||||||
.write(&PodMetadata {
|
.write(&PodMetadata {
|
||||||
pod_name: "source".into(),
|
pod_name: "source".into(),
|
||||||
active: None,
|
active: None,
|
||||||
|
workspace_root: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: vec![pod_store::PodPeer {
|
peers: vec![pod_store::PodPeer {
|
||||||
|
|
@ -1405,6 +1412,7 @@ mod tests {
|
||||||
.write(&PodMetadata {
|
.write(&PodMetadata {
|
||||||
pod_name: "source".into(),
|
pod_name: "source".into(),
|
||||||
active: None,
|
active: None,
|
||||||
|
workspace_root: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: vec![pod_store::PodPeer {
|
peers: vec![pod_store::PodPeer {
|
||||||
|
|
@ -1417,6 +1425,7 @@ mod tests {
|
||||||
.write(&PodMetadata {
|
.write(&PodMetadata {
|
||||||
pod_name: "target".into(),
|
pod_name: "target".into(),
|
||||||
active: None,
|
active: None,
|
||||||
|
workspace_root: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: vec![pod_store::PodPeer {
|
peers: vec![pod_store::PodPeer {
|
||||||
|
|
@ -1519,6 +1528,7 @@ mod tests {
|
||||||
.write(&PodMetadata {
|
.write(&PodMetadata {
|
||||||
pod_name: "source".into(),
|
pod_name: "source".into(),
|
||||||
active: None,
|
active: None,
|
||||||
|
workspace_root: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: vec![pod_store::PodPeer {
|
peers: vec![pod_store::PodPeer {
|
||||||
|
|
@ -1531,6 +1541,7 @@ mod tests {
|
||||||
.write(&PodMetadata {
|
.write(&PodMetadata {
|
||||||
pod_name: "target".into(),
|
pod_name: "target".into(),
|
||||||
active: None,
|
active: None,
|
||||||
|
workspace_root: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: vec![pod_store::PodPeer {
|
peers: vec![pod_store::PodPeer {
|
||||||
|
|
@ -1631,6 +1642,7 @@ mod tests {
|
||||||
.write(&PodMetadata {
|
.write(&PodMetadata {
|
||||||
pod_name: "source".into(),
|
pod_name: "source".into(),
|
||||||
active: None,
|
active: None,
|
||||||
|
workspace_root: None,
|
||||||
spawned_children: vec![child("target", &socket)],
|
spawned_children: vec![child("target", &socket)],
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: Vec::new(),
|
peers: Vec::new(),
|
||||||
|
|
|
||||||
|
|
@ -924,7 +924,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pod_metadata(&self, active: Option<PodActiveSegmentRef>) -> PodMetadata {
|
fn pod_metadata(&self, active: Option<PodActiveSegmentRef>) -> PodMetadata {
|
||||||
pod_metadata_for_manifest(&self.manifest, active)
|
pod_metadata_for_manifest(&self.manifest, &self.workspace_root, active)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_pod_metadata_pending(&self) -> Result<(), PodError> {
|
fn write_pod_metadata_pending(&self) -> Result<(), PodError> {
|
||||||
|
|
@ -4319,9 +4319,11 @@ fn request_config_from_worker_manifest(wm: &WorkerManifest) -> RequestConfig {
|
||||||
|
|
||||||
fn pod_metadata_for_manifest(
|
fn pod_metadata_for_manifest(
|
||||||
manifest: &PodManifest,
|
manifest: &PodManifest,
|
||||||
|
workspace_root: &Path,
|
||||||
active: Option<PodActiveSegmentRef>,
|
active: Option<PodActiveSegmentRef>,
|
||||||
) -> PodMetadata {
|
) -> PodMetadata {
|
||||||
let mut metadata = PodMetadata::new(manifest.pod.name.clone(), active);
|
let mut metadata = PodMetadata::new(manifest.pod.name.clone(), active)
|
||||||
|
.with_workspace_root(workspace_root.to_path_buf());
|
||||||
if should_persist_resolved_manifest_snapshot(manifest) {
|
if should_persist_resolved_manifest_snapshot(manifest) {
|
||||||
metadata.resolved_manifest_snapshot = serde_json::to_value(manifest).ok();
|
metadata.resolved_manifest_snapshot = serde_json::to_value(manifest).ok();
|
||||||
}
|
}
|
||||||
|
|
@ -5328,7 +5330,7 @@ permission = "read"
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(manifest.profile.is_none());
|
assert!(manifest.profile.is_none());
|
||||||
assert!(
|
assert!(
|
||||||
pod_metadata_for_manifest(&manifest, None)
|
pod_metadata_for_manifest(&manifest, Path::new("/snapshot/workspace"), None)
|
||||||
.resolved_manifest_snapshot
|
.resolved_manifest_snapshot
|
||||||
.is_none()
|
.is_none()
|
||||||
);
|
);
|
||||||
|
|
@ -5361,7 +5363,7 @@ permission = "read"
|
||||||
config: None,
|
config: None,
|
||||||
}];
|
}];
|
||||||
|
|
||||||
let metadata = pod_metadata_for_manifest(&manifest, None);
|
let metadata = pod_metadata_for_manifest(&manifest, Path::new("/snapshot/workspace"), None);
|
||||||
let snapshot = metadata
|
let snapshot = metadata
|
||||||
.resolved_manifest_snapshot
|
.resolved_manifest_snapshot
|
||||||
.expect("plugin-resolved manifest should be snapshotted");
|
.expect("plugin-resolved manifest should be snapshotted");
|
||||||
|
|
|
||||||
|
|
@ -378,6 +378,7 @@ mod tests {
|
||||||
.write(&PodMetadata {
|
.write(&PodMetadata {
|
||||||
pod_name: "orchestrator".into(),
|
pod_name: "orchestrator".into(),
|
||||||
active: None,
|
active: None,
|
||||||
|
workspace_root: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: Vec::new(),
|
peers: Vec::new(),
|
||||||
|
|
@ -388,6 +389,7 @@ mod tests {
|
||||||
.write(&PodMetadata {
|
.write(&PodMetadata {
|
||||||
pod_name: "companion".into(),
|
pod_name: "companion".into(),
|
||||||
active: None,
|
active: None,
|
||||||
|
workspace_root: None,
|
||||||
spawned_children: Vec::new(),
|
spawned_children: Vec::new(),
|
||||||
reclaimed_children: Vec::new(),
|
reclaimed_children: Vec::new(),
|
||||||
peers: Vec::new(),
|
peers: Vec::new(),
|
||||||
|
|
|
||||||
|
|
@ -3398,12 +3398,13 @@ async fn load_pod_list(
|
||||||
let live = read_reachable_live_pod_infos(&store)
|
let live = read_reachable_live_pod_infos(&store)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
Ok(PodList::from_sources(
|
Ok(PodList::from_workspace_sources(
|
||||||
PodVisibilitySource::ResumePicker,
|
PodVisibilitySource::ResumePicker,
|
||||||
stored,
|
stored,
|
||||||
live,
|
live,
|
||||||
selected_name,
|
selected_name,
|
||||||
max_entries,
|
max_entries,
|
||||||
|
¤t_workspace_root(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -9390,6 +9391,7 @@ branch = "orchestration/custom-panel"
|
||||||
active_session_id: None,
|
active_session_id: None,
|
||||||
active_segment_id: None,
|
active_segment_id: None,
|
||||||
updated_at,
|
updated_at,
|
||||||
|
workspace_root: None,
|
||||||
preview: None,
|
preview: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::collections::BTreeMap;
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
@ -65,6 +65,56 @@ impl PodList {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_workspace_sources(
|
||||||
|
source: PodVisibilitySource,
|
||||||
|
stored: Vec<StoredPodInfo>,
|
||||||
|
live: Vec<LivePodInfo>,
|
||||||
|
selected_name: Option<String>,
|
||||||
|
max_entries: usize,
|
||||||
|
workspace_root: &Path,
|
||||||
|
) -> Self {
|
||||||
|
let current_workspace = workspace_root_key(workspace_root);
|
||||||
|
let mut current_names = BTreeSet::new();
|
||||||
|
let stored: Vec<_> = stored
|
||||||
|
.into_iter()
|
||||||
|
.filter(|info| {
|
||||||
|
let matches = info
|
||||||
|
.workspace_root
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|root| workspace_root_key(root) == current_workspace);
|
||||||
|
if matches {
|
||||||
|
current_names.insert(info.pod_name.clone());
|
||||||
|
}
|
||||||
|
matches
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let live = live
|
||||||
|
.into_iter()
|
||||||
|
.filter(|info| current_names.contains(&info.pod_name))
|
||||||
|
.collect();
|
||||||
|
Self::from_sources(source, stored, live, selected_name, max_entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn filter_for_workspace(&self, workspace_root: &Path) -> Self {
|
||||||
|
let current_workspace = workspace_root_key(workspace_root);
|
||||||
|
let entries: Vec<_> = self
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.filter(|entry| entry_belongs_to_workspace(entry, ¤t_workspace))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
let selected_name = self
|
||||||
|
.selected_name
|
||||||
|
.as_ref()
|
||||||
|
.filter(|name| entries.iter().any(|entry| entry.name == **name))
|
||||||
|
.cloned()
|
||||||
|
.or_else(|| entries.first().map(|entry| entry.name.clone()));
|
||||||
|
Self {
|
||||||
|
entries,
|
||||||
|
selected_name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn selected_index(&self) -> usize {
|
pub(crate) fn selected_index(&self) -> usize {
|
||||||
self.selected_name
|
self.selected_name
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -82,6 +132,18 @@ impl PodList {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn workspace_root_key(path: &Path) -> PathBuf {
|
||||||
|
path.canonicalize().unwrap_or_else(|_| path.to_path_buf())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn entry_belongs_to_workspace(entry: &PodListEntry, current_workspace: &Path) -> bool {
|
||||||
|
entry
|
||||||
|
.stored
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|stored| stored.workspace_root.as_deref())
|
||||||
|
.is_some_and(|root| workspace_root_key(root) == current_workspace)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub(crate) enum PodVisibilitySource {
|
pub(crate) enum PodVisibilitySource {
|
||||||
ResumePicker,
|
ResumePicker,
|
||||||
|
|
@ -210,6 +272,7 @@ pub(crate) struct StoredPodInfo {
|
||||||
pub active_session_id: Option<SessionId>,
|
pub active_session_id: Option<SessionId>,
|
||||||
pub active_segment_id: Option<SegmentId>,
|
pub active_segment_id: Option<SegmentId>,
|
||||||
pub updated_at: u64,
|
pub updated_at: u64,
|
||||||
|
pub workspace_root: Option<PathBuf>,
|
||||||
pub preview: Option<String>,
|
pub preview: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -348,6 +411,7 @@ fn stored_info_from_metadata(
|
||||||
active_session_id,
|
active_session_id,
|
||||||
active_segment_id,
|
active_segment_id,
|
||||||
updated_at: summary.updated_at,
|
updated_at: summary.updated_at,
|
||||||
|
workspace_root: metadata.workspace_root,
|
||||||
preview: summary.preview,
|
preview: summary.preview,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -359,6 +423,7 @@ fn corrupt_stored_info(pod_name: String, message: String) -> StoredPodInfo {
|
||||||
active_session_id: None,
|
active_session_id: None,
|
||||||
active_segment_id: None,
|
active_segment_id: None,
|
||||||
updated_at: 0,
|
updated_at: 0,
|
||||||
|
workspace_root: None,
|
||||||
preview: Some(format!("metadata: {}", trim_one_line(&message, 48))),
|
preview: Some(format!("metadata: {}", trim_one_line(&message, 48))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1061,6 +1126,7 @@ mod tests {
|
||||||
active_session_id: Some(session_id),
|
active_session_id: Some(session_id),
|
||||||
active_segment_id: None,
|
active_segment_id: None,
|
||||||
updated_at: 0,
|
updated_at: 0,
|
||||||
|
workspace_root: None,
|
||||||
preview: Some("[pending segment]".to_string()),
|
preview: Some("[pending segment]".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1072,6 +1138,7 @@ mod tests {
|
||||||
active_session_id: None,
|
active_session_id: None,
|
||||||
active_segment_id: None,
|
active_segment_id: None,
|
||||||
updated_at,
|
updated_at,
|
||||||
|
workspace_root: None,
|
||||||
preview: None,
|
preview: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1170,4 +1237,72 @@ mod tests {
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn stopped_info_for_workspace(pod_name: &str, workspace_root: &Path) -> StoredPodInfo {
|
||||||
|
let mut info = stopped_info_with_updated_at(pod_name, 10);
|
||||||
|
info.workspace_root = Some(workspace_root.to_path_buf());
|
||||||
|
info
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workspace_sources_include_current_and_hide_external_or_unknown_pods() {
|
||||||
|
let current = tempdir().unwrap();
|
||||||
|
let external = tempdir().unwrap();
|
||||||
|
|
||||||
|
let list = PodList::from_workspace_sources(
|
||||||
|
SOURCE,
|
||||||
|
vec![
|
||||||
|
stopped_info_for_workspace("current", current.path()),
|
||||||
|
stopped_info_for_workspace("current-orchestrator", current.path()),
|
||||||
|
stopped_info_for_workspace("other-workspace", external.path()),
|
||||||
|
stopped_info_with_updated_at("legacy-unknown", 10),
|
||||||
|
corrupt_stored_info("corrupt".to_string(), "invalid metadata".to_string()),
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
live_info("current", PodStatus::Idle),
|
||||||
|
live_info("current-orchestrator", PodStatus::Running),
|
||||||
|
live_info("other-workspace", PodStatus::Idle),
|
||||||
|
live_info("legacy-unknown", PodStatus::Idle),
|
||||||
|
live_info("live-only", PodStatus::Idle),
|
||||||
|
],
|
||||||
|
None,
|
||||||
|
10,
|
||||||
|
current.path(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let names = list
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.map(|entry| entry.name.as_str())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(names, vec!["current", "current-orchestrator"]);
|
||||||
|
assert!(list.entries.iter().all(|entry| entry.actions.can_open));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workspace_sources_use_workspace_metadata_not_cwd_or_live_presence() {
|
||||||
|
let current = tempdir().unwrap();
|
||||||
|
let worktree_cwd = current.path().join(".worktree/impl");
|
||||||
|
|
||||||
|
let list = PodList::from_workspace_sources(
|
||||||
|
SOURCE,
|
||||||
|
vec![stopped_info_for_workspace("ticket-role", current.path())],
|
||||||
|
vec![live_info("ticket-role", PodStatus::Idle)],
|
||||||
|
None,
|
||||||
|
10,
|
||||||
|
&worktree_cwd,
|
||||||
|
);
|
||||||
|
assert!(list.entries.is_empty());
|
||||||
|
|
||||||
|
let list = PodList::from_workspace_sources(
|
||||||
|
SOURCE,
|
||||||
|
vec![stopped_info_for_workspace("ticket-role", current.path())],
|
||||||
|
vec![live_info("ticket-role", PodStatus::Idle)],
|
||||||
|
None,
|
||||||
|
10,
|
||||||
|
current.path(),
|
||||||
|
);
|
||||||
|
assert_eq!(list.entries[0].name, "ticket-role");
|
||||||
|
assert!(list.entries[0].actions.can_open);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -918,6 +918,8 @@ fn build_workspace_panel_with_registry_model(
|
||||||
pods: &PodList,
|
pods: &PodList,
|
||||||
registry: &PanelRegistrySnapshot,
|
registry: &PanelRegistrySnapshot,
|
||||||
) -> WorkspacePanelViewModel {
|
) -> WorkspacePanelViewModel {
|
||||||
|
let pods = pods.filter_for_workspace(workspace_root);
|
||||||
|
let pods = &pods;
|
||||||
match ticket_config_availability(workspace_root) {
|
match ticket_config_availability(workspace_root) {
|
||||||
TicketConfigAvailability::Absent => {}
|
TicketConfigAvailability::Absent => {}
|
||||||
TicketConfigAvailability::Usable => {
|
TicketConfigAvailability::Usable => {
|
||||||
|
|
@ -1685,7 +1687,7 @@ fn excerpt(markdown: &str, max_chars: usize) -> Option<String> {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::pod_list::{LivePodInfo, PodEntrySummary};
|
use crate::pod_list::{LivePodInfo, PodEntrySummary, StoredPodInfo};
|
||||||
use crate::role_session_registry::{PanelRegistryStore, RelatedTicketRef, RoleSessionOrigin};
|
use crate::role_session_registry::{PanelRegistryStore, RelatedTicketRef, RoleSessionOrigin};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
@ -1829,10 +1831,22 @@ mod tests {
|
||||||
.unwrap_or_else(|| panic!("missing row for {title}"))
|
.unwrap_or_else(|| panic!("missing row for {title}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn live_pods(names: &[&str]) -> PodList {
|
fn live_pods(workspace_root: &Path, names: &[&str]) -> PodList {
|
||||||
|
let stored = names
|
||||||
|
.iter()
|
||||||
|
.map(|name| StoredPodInfo {
|
||||||
|
pod_name: (*name).to_string(),
|
||||||
|
metadata_state: StoredMetadataState::Present,
|
||||||
|
active_session_id: None,
|
||||||
|
active_segment_id: None,
|
||||||
|
updated_at: 1,
|
||||||
|
workspace_root: Some(workspace_root.to_path_buf()),
|
||||||
|
preview: None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
PodList::from_sources(
|
PodList::from_sources(
|
||||||
crate::pod_list::PodVisibilitySource::ResumePicker,
|
crate::pod_list::PodVisibilitySource::ResumePicker,
|
||||||
vec![],
|
stored,
|
||||||
names
|
names
|
||||||
.iter()
|
.iter()
|
||||||
.map(|name| LivePodInfo {
|
.map(|name| LivePodInfo {
|
||||||
|
|
@ -1855,7 +1869,7 @@ mod tests {
|
||||||
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
let backend = LocalTicketBackend::new(temp.path().join(".yoi/tickets"));
|
||||||
create_ticket(&backend, "Hidden Without Config", |_| {});
|
create_ticket(&backend, "Hidden Without Config", |_| {});
|
||||||
|
|
||||||
let model = build_workspace_panel(temp.path(), &live_pods(&["idle"]));
|
let model = build_workspace_panel(temp.path(), &live_pods(temp.path(), &["idle"]));
|
||||||
|
|
||||||
assert!(model.header.diagnostics.is_empty());
|
assert!(model.header.diagnostics.is_empty());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -2069,7 +2083,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let model = build_workspace_panel_with_registry(
|
let model = build_workspace_panel_with_registry(
|
||||||
temp.path(),
|
temp.path(),
|
||||||
&live_pods(&["ready-intake"]),
|
&live_pods(temp.path(), &["ready-intake"]),
|
||||||
®istry.snapshot().unwrap(),
|
®istry.snapshot().unwrap(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -2187,7 +2201,7 @@ mod tests {
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let model = build_workspace_panel(temp.path(), &live_pods(&["idle"]));
|
let model = build_workspace_panel(temp.path(), &live_pods(temp.path(), &["idle"]));
|
||||||
|
|
||||||
let diagnostics = model.header.diagnostics.join("\n");
|
let diagnostics = model.header.diagnostics.join("\n");
|
||||||
assert!(diagnostics.contains("Ticket config is unusable"));
|
assert!(diagnostics.contains("Ticket config is unusable"));
|
||||||
|
|
@ -2420,7 +2434,10 @@ mod tests {
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let pods = live_pods(&["claimed-intake", "shared-intake", &preticket_pod]);
|
let pods = live_pods(
|
||||||
|
temp.path(),
|
||||||
|
&["claimed-intake", "shared-intake", &preticket_pod],
|
||||||
|
);
|
||||||
let model =
|
let model =
|
||||||
build_workspace_panel_with_registry(temp.path(), &pods, ®istry.snapshot().unwrap());
|
build_workspace_panel_with_registry(temp.path(), &pods, ®istry.snapshot().unwrap());
|
||||||
|
|
||||||
|
|
@ -2484,7 +2501,7 @@ mod tests {
|
||||||
|
|
||||||
let model = build_workspace_panel_with_registry(
|
let model = build_workspace_panel_with_registry(
|
||||||
temp.path(),
|
temp.path(),
|
||||||
&live_pods(&["ticket-claimed-intake"]),
|
&live_pods(temp.path(), &["ticket-claimed-intake"]),
|
||||||
®istry,
|
®istry,
|
||||||
);
|
);
|
||||||
let row = model
|
let row = model
|
||||||
|
|
@ -2692,4 +2709,104 @@ mod tests {
|
||||||
OrchestratorLifecyclePlan::ReportLive
|
OrchestratorLifecyclePlan::ReportLive
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn mixed_workspace_pods(current: &Path, external: &Path) -> PodList {
|
||||||
|
let stored = vec![
|
||||||
|
StoredPodInfo {
|
||||||
|
pod_name: "current".to_string(),
|
||||||
|
metadata_state: StoredMetadataState::Present,
|
||||||
|
active_session_id: None,
|
||||||
|
active_segment_id: None,
|
||||||
|
updated_at: 10,
|
||||||
|
workspace_root: Some(current.to_path_buf()),
|
||||||
|
preview: None,
|
||||||
|
},
|
||||||
|
StoredPodInfo {
|
||||||
|
pod_name: "current-coder".to_string(),
|
||||||
|
metadata_state: StoredMetadataState::Present,
|
||||||
|
active_session_id: None,
|
||||||
|
active_segment_id: None,
|
||||||
|
updated_at: 20,
|
||||||
|
workspace_root: Some(current.to_path_buf()),
|
||||||
|
preview: None,
|
||||||
|
},
|
||||||
|
StoredPodInfo {
|
||||||
|
pod_name: "external".to_string(),
|
||||||
|
metadata_state: StoredMetadataState::Present,
|
||||||
|
active_session_id: None,
|
||||||
|
active_segment_id: None,
|
||||||
|
updated_at: 30,
|
||||||
|
workspace_root: Some(external.to_path_buf()),
|
||||||
|
preview: None,
|
||||||
|
},
|
||||||
|
StoredPodInfo {
|
||||||
|
pod_name: "legacy".to_string(),
|
||||||
|
metadata_state: StoredMetadataState::Present,
|
||||||
|
active_session_id: None,
|
||||||
|
active_segment_id: None,
|
||||||
|
updated_at: 40,
|
||||||
|
workspace_root: None,
|
||||||
|
preview: None,
|
||||||
|
},
|
||||||
|
StoredPodInfo {
|
||||||
|
pod_name: "corrupt".to_string(),
|
||||||
|
metadata_state: StoredMetadataState::Corrupt("bad metadata".to_string()),
|
||||||
|
active_session_id: None,
|
||||||
|
active_segment_id: None,
|
||||||
|
updated_at: 50,
|
||||||
|
workspace_root: None,
|
||||||
|
preview: Some("metadata: bad metadata".to_string()),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
let live = [
|
||||||
|
"current",
|
||||||
|
"current-coder",
|
||||||
|
"external",
|
||||||
|
"legacy",
|
||||||
|
"live-only",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.map(|name| LivePodInfo {
|
||||||
|
pod_name: (*name).to_string(),
|
||||||
|
socket_path: PathBuf::from(format!("/tmp/{name}.sock")),
|
||||||
|
status: Some(PodStatus::Idle),
|
||||||
|
reachable: true,
|
||||||
|
segment_id: None,
|
||||||
|
summary: PodEntrySummary::default(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
PodList::from_sources(
|
||||||
|
crate::pod_list::PodVisibilitySource::ResumePicker,
|
||||||
|
stored,
|
||||||
|
live,
|
||||||
|
None,
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn workspace_panel_filters_pod_rows_to_current_workspace_metadata() {
|
||||||
|
let current = TempDir::new().unwrap();
|
||||||
|
let external = TempDir::new().unwrap();
|
||||||
|
let pods = mixed_workspace_pods(current.path(), external.path());
|
||||||
|
|
||||||
|
let model = build_workspace_panel(current.path(), &pods);
|
||||||
|
let pod_names = model
|
||||||
|
.rows
|
||||||
|
.iter()
|
||||||
|
.filter_map(|row| match &row.key {
|
||||||
|
PanelRowKey::Pod(name) => Some(name.as_str()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(pod_names, vec!["current-coder", "current"]);
|
||||||
|
assert!(
|
||||||
|
model
|
||||||
|
.rows
|
||||||
|
.iter()
|
||||||
|
.filter(|row| matches!(row.key, PanelRowKey::Pod(_)))
|
||||||
|
.all(|row| row.next_action == Some(NextUserAction::OpenPod))
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user