Skip to content

Commit 5154824

Browse files
LynricsyWineFoxDev
andcommitted
feat(dashboard): 🔭 discover OMP models and sessions
Co-authored-by: Wine Fox <fox@ling.plus>
1 parent 57316b1 commit 5154824

2 files changed

Lines changed: 231 additions & 10 deletions

File tree

packages/dashboard/src-tauri/src/commands.rs

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,6 +1036,86 @@ pub fn parse_pi_models_output(text: &str) -> Vec<String> {
10361036
models.into_iter().collect()
10371037
}
10381038

1039+
/// Parse `omp models --json`, preserving selectors with scoped or nested model IDs.
1040+
pub fn parse_omp_models_output(text: &str) -> Vec<String> {
1041+
let Ok(value) = serde_json::from_str::<serde_json::Value>(text) else {
1042+
return Vec::new();
1043+
};
1044+
let Some(entries) = value.get("models").and_then(serde_json::Value::as_array) else {
1045+
return Vec::new();
1046+
};
1047+
1048+
let mut models = std::collections::BTreeSet::new();
1049+
for entry in entries {
1050+
if let Some(selector) = entry.get("selector").and_then(serde_json::Value::as_str) {
1051+
if !selector.is_empty() {
1052+
models.insert(selector.to_string());
1053+
continue;
1054+
}
1055+
}
1056+
if let (Some(provider), Some(id)) = (
1057+
entry.get("provider").and_then(serde_json::Value::as_str),
1058+
entry.get("id").and_then(serde_json::Value::as_str),
1059+
) {
1060+
if !provider.is_empty() && !id.is_empty() {
1061+
models.insert(format!("{provider}/{id}"));
1062+
}
1063+
}
1064+
}
1065+
models.into_iter().collect()
1066+
}
1067+
1068+
async fn get_available_omp_models() -> Vec<String> {
1069+
let candidates = if cfg!(target_os = "windows") {
1070+
let appdata = std::env::var("APPDATA").unwrap_or_default();
1071+
let mut list = Vec::new();
1072+
if !appdata.is_empty() {
1073+
list.push(format!("{}\\npm\\omp.cmd", appdata));
1074+
list.push(format!("{}\\npm\\omp.exe", appdata));
1075+
}
1076+
list.push("omp".to_string());
1077+
list.push("omp.exe".to_string());
1078+
list
1079+
} else {
1080+
let home = std::env::var("HOME").unwrap_or_default();
1081+
vec![
1082+
format!("{}/.bun/bin/omp", home),
1083+
"omp".to_string(),
1084+
format!("{}/.local/bin/omp", home),
1085+
"/usr/local/bin/omp".to_string(),
1086+
"/opt/homebrew/bin/omp".to_string(),
1087+
format!("{}/.local/share/mise/shims/omp", home),
1088+
format!("{}/.asdf/shims/omp", home),
1089+
format!("{}/.volta/bin/omp", home),
1090+
]
1091+
};
1092+
1093+
for bin in &candidates {
1094+
if let Some(text) = run_bounded_binary(bin, &["models", "--json"]).await {
1095+
let models = parse_omp_models_output(&text);
1096+
if !models.is_empty() {
1097+
return models;
1098+
}
1099+
}
1100+
}
1101+
1102+
if cfg!(target_os = "windows") {
1103+
if let Some(bin) = resolve_via_where("omp").await {
1104+
if let Some(text) = run_bounded_binary(&bin, &["models", "--json"]).await {
1105+
let models = parse_omp_models_output(&text);
1106+
if !models.is_empty() {
1107+
return models;
1108+
}
1109+
}
1110+
}
1111+
}
1112+
1113+
if let Some(text) = run_via_login_shell("omp models --json".to_string()).await {
1114+
return parse_omp_models_output(&text);
1115+
}
1116+
Vec::new()
1117+
}
1118+
10391119
#[tauri::command]
10401120
pub async fn get_available_pi_models() -> Vec<String> {
10411121
// GUI apps on macOS don't inherit shell PATH; try common locations.
@@ -1108,6 +1188,10 @@ pub async fn get_available_pi_models() -> Vec<String> {
11081188
}
11091189
}
11101190

1191+
let omp_models = get_available_omp_models().await;
1192+
if !omp_models.is_empty() {
1193+
return omp_models;
1194+
}
11111195
// Last resort: resolve `pi` through the user's login shell. This is the
11121196
// universal fallback for version managers (mise/nvm/fnm) that install into
11131197
// per-version dirs the candidates above can't enumerate — the login shell
@@ -1318,9 +1402,9 @@ pub fn get_db_health(state: State<'_, AppState>) -> db::DbHealth {
13181402
#[cfg(test)]
13191403
mod tests {
13201404
use super::{
1321-
opencode_desktop_detected_for_env, parse_pi_models_output, pick_first_line,
1322-
prepare_embedding_probe_options, run_bounded_binary, windows_opencode_candidates,
1323-
DesktopPlatform, OpencodeDesktopEnv, OPENCODE_DESKTOP_APP_IDS,
1405+
opencode_desktop_detected_for_env, parse_omp_models_output, parse_pi_models_output,
1406+
pick_first_line, prepare_embedding_probe_options, run_bounded_binary,
1407+
windows_opencode_candidates, DesktopPlatform, OpencodeDesktopEnv, OPENCODE_DESKTOP_APP_IDS,
13241408
};
13251409
use crate::embedding_probe::EmbeddingProbeOutcome;
13261410
use std::path::{Path, PathBuf};
@@ -1576,6 +1660,19 @@ mod tests {
15761660
assert_eq!(result, vec!["anthropic/claude-sonnet-4-5"]);
15771661
}
15781662

1663+
#[test]
1664+
fn test_parse_omp_models_output_preserves_selectors() {
1665+
let input = r#"{"models":[{"provider":"anthropic","id":"claude-opus-4-5","selector":"anthropic/claude-opus-4-5"},{"provider":"modal","id":"@modal/qwen/model-v1","selector":"modal/@modal/qwen/model-v1"},{"provider":"openai","id":"fallback/model"}]}"#;
1666+
assert_eq!(
1667+
parse_omp_models_output(input),
1668+
vec![
1669+
"anthropic/claude-opus-4-5",
1670+
"modal/@modal/qwen/model-v1",
1671+
"openai/fallback/model",
1672+
]
1673+
);
1674+
}
1675+
15791676
#[test]
15801677
fn test_pick_first_line() {
15811678
assert_eq!(pick_first_line(""), None);

packages/dashboard/src-tauri/src/pi_sessions.rs

Lines changed: 131 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,18 +90,109 @@ pub fn pi_sessions_root() -> Option<PathBuf> {
9090
Some(dirs::home_dir()?.join(".pi/agent/sessions"))
9191
}
9292

93-
pub fn scan_pi_session_dir() -> Vec<PiSessionMeta> {
94-
let Some(root) = pi_sessions_root() else {
95-
return Vec::new();
93+
fn normalize_omp_profile(value: Option<std::ffi::OsString>) -> Option<std::ffi::OsString> {
94+
let value = value?;
95+
let trimmed = value.to_string_lossy().trim().to_string();
96+
if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("default") {
97+
None
98+
} else {
99+
Some(trimmed.into())
100+
}
101+
}
102+
103+
fn resolve_omp_profile(
104+
omp_profile: Option<std::ffi::OsString>,
105+
pi_profile: Option<std::ffi::OsString>,
106+
) -> Option<std::ffi::OsString> {
107+
normalize_omp_profile(match omp_profile {
108+
Some(value) => Some(value),
109+
None => pi_profile,
110+
})
111+
}
112+
113+
fn active_omp_profile() -> Option<std::ffi::OsString> {
114+
resolve_omp_profile(
115+
std::env::var_os("OMP_PROFILE"),
116+
std::env::var_os("PI_PROFILE"),
117+
)
118+
}
119+
120+
fn append_omp_profile_roots(roots: &mut Vec<PathBuf>, profiles_dir: &Path, xdg: bool) {
121+
let Ok(entries) = fs::read_dir(profiles_dir) else {
122+
return;
96123
};
97-
scan_pi_session_dir_at(&root)
124+
for entry in entries.flatten() {
125+
if entry.file_type().is_ok_and(|kind| kind.is_dir()) {
126+
roots.push(if xdg {
127+
entry.path().join("sessions")
128+
} else {
129+
entry.path().join("agent/sessions")
130+
});
131+
}
132+
}
98133
}
99134

100-
pub fn scan_pi_cache_session_dir() -> Vec<PiSessionMeta> {
101-
let Some(root) = pi_sessions_root() else {
135+
fn pi_compatible_session_roots() -> Vec<PathBuf> {
136+
if let Ok(root) = test_root().read() {
137+
if let Some(path) = root.clone() {
138+
return vec![path];
139+
}
140+
}
141+
142+
let Some(home) = dirs::home_dir() else {
102143
return Vec::new();
103144
};
104-
scan_pi_cache_session_dir_at(&root)
145+
let mut roots = vec![home.join(".pi/agent/sessions")];
146+
147+
if let Some(agent_dir) = std::env::var_os("PI_CODING_AGENT_DIR").filter(|v| !v.is_empty()) {
148+
roots.push(PathBuf::from(agent_dir).join("sessions"));
149+
}
150+
151+
let config_dir = std::env::var_os("PI_CONFIG_DIR")
152+
.filter(|v| !v.is_empty())
153+
.map(PathBuf::from)
154+
.unwrap_or_else(|| PathBuf::from(".omp"));
155+
let profile = active_omp_profile();
156+
let profile_suffix = profile
157+
.as_ref()
158+
.map(|name| PathBuf::from("profiles").join(name));
159+
append_omp_profile_roots(&mut roots, &home.join(&config_dir).join("profiles"), false);
160+
161+
let mut omp_agent = home.join(&config_dir);
162+
if let Some(suffix) = &profile_suffix {
163+
omp_agent.push(suffix);
164+
}
165+
roots.push(omp_agent.join("agent/sessions"));
166+
167+
if let Some(xdg_data) = std::env::var_os("XDG_DATA_HOME").filter(|v| !v.is_empty()) {
168+
let app_root = PathBuf::from(xdg_data).join("omp");
169+
append_omp_profile_roots(&mut roots, &app_root.join("profiles"), true);
170+
let mut xdg_root = app_root;
171+
if let Some(suffix) = profile_suffix {
172+
xdg_root.push(suffix);
173+
}
174+
if xdg_root.exists() {
175+
roots.push(xdg_root.join("sessions"));
176+
}
177+
}
178+
179+
let mut seen = HashSet::new();
180+
roots.retain(|path| seen.insert(path.clone()));
181+
roots
182+
}
183+
184+
pub fn scan_pi_session_dir() -> Vec<PiSessionMeta> {
185+
pi_compatible_session_roots()
186+
.into_iter()
187+
.flat_map(|root| scan_pi_session_dir_at(&root))
188+
.collect()
189+
}
190+
191+
pub fn scan_pi_cache_session_dir() -> Vec<PiSessionMeta> {
192+
pi_compatible_session_roots()
193+
.into_iter()
194+
.flat_map(|root| scan_pi_cache_session_dir_at(&root))
195+
.collect()
105196
}
106197

107198
fn scan_pi_cache_session_dir_at(root: &Path) -> Vec<PiSessionMeta> {
@@ -603,6 +694,39 @@ mod tests {
603694
path
604695
}
605696

697+
#[test]
698+
fn discovers_named_omp_profile_session_roots() {
699+
let dir = tempfile::tempdir().unwrap();
700+
fs::create_dir_all(dir.path().join("profiles/work/agent/sessions")).unwrap();
701+
fs::create_dir_all(dir.path().join("profiles/personal/agent/sessions")).unwrap();
702+
fs::write(dir.path().join("profiles/not-a-directory"), "").unwrap();
703+
let mut roots = Vec::new();
704+
append_omp_profile_roots(&mut roots, &dir.path().join("profiles"), false);
705+
roots.sort();
706+
assert_eq!(
707+
roots,
708+
vec![
709+
dir.path().join("profiles/personal/agent/sessions"),
710+
dir.path().join("profiles/work/agent/sessions"),
711+
]
712+
);
713+
}
714+
715+
#[test]
716+
fn omp_profile_empty_and_default_select_the_default_profile() {
717+
assert_eq!(normalize_omp_profile(Some("".into())), None);
718+
assert_eq!(normalize_omp_profile(Some(" ".into())), None);
719+
assert_eq!(normalize_omp_profile(Some("default".into())), None);
720+
assert_eq!(
721+
normalize_omp_profile(Some(" work ".into())),
722+
Some("work".into())
723+
);
724+
assert_eq!(
725+
resolve_omp_profile(Some("".into()), Some("work".into())),
726+
None
727+
);
728+
}
729+
606730
#[test]
607731
fn round_trip_small_pi_jsonl_fixture() {
608732
clear_caches_for_tests();

0 commit comments

Comments
 (0)