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
4 changes: 3 additions & 1 deletion apps/maple-agent/app/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ operations! {
end_queued_message_edit(user_id: (&str), session_id: (&str), queue_id: (&str)) -> ();
cancel_run(user_id: (&str), run_id: (&str)) -> ();
available_model_ids(user_id: (&str)) -> Vec<String>;
model_catalog(user_id: (&str)) -> maple_sdk::ModelCatalogResponse;
model_catalog_entry(user_id: (&str), model: (&str)) -> Option<maple_agent::agent::CatalogEntry>;
audio_capabilities(user_id: (&str)) -> maple_agent::agent::AudioCapabilities;
synthesize_speech(user_id: (&str), text: (String), voice: (String), speed: (f32)) -> Vec<u8>;
Expand Down Expand Up @@ -378,13 +379,14 @@ impl AgentBackend {
params: impl Serialize,
) -> Result<R, String> {
self.ensure_active()?;
// Speech, account UI and each viewer's remote-sidebar preferences
// Catalog presentation, speech, account UI and remote-sidebar preferences
// belong to the controller.
// All task operations use the selected host's separate connection.
let client = if let Some(controller) = &self.controller
&& matches!(
method,
"audio_capabilities"
| "model_catalog"
| "synthesize_speech"
| "transcribe_audio"
| "plan_usage"
Expand Down
11 changes: 11 additions & 0 deletions apps/maple-agent/app/src/backend/local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2397,6 +2397,17 @@ impl LocalBackend {
.await
}

pub async fn model_catalog(
&self,
user_id: &str,
) -> Result<maple_sdk::ModelCatalogResponse, String> {
self.service
.handle_for_user(user_id)
.await?
.model_catalog()
.await
}

/// What the catalog says about a model: its vision and context window.
/// None when the catalog cannot be read.
pub async fn model_catalog_entry(
Expand Down
75 changes: 71 additions & 4 deletions apps/maple-agent/app/src/ui/chat/composer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,44 @@ use crate::ui::theme;
use crate::ui::titlebar;
use crate::ui::widgets;

/// Research's compact label precedence, projected once when the catalog loads.
/// IDs remain the keys used for selection and persisted tasks.
pub(super) fn catalog_model_labels(
catalog: &maple_sdk::ModelCatalogResponse,
) -> std::collections::HashMap<String, String> {
let label = |short: Option<&str>, full: Option<&str>, id: &str| {
short
.filter(|value| !value.trim().is_empty())
.or_else(|| full.filter(|value| !value.trim().is_empty()))
.unwrap_or(id)
.to_string()
};
catalog
.data
.iter()
.map(|model| {
(
model.id.clone(),
label(
model.short_name.as_deref(),
model.display_name.as_deref(),
&model.id,
),
)
})
.chain(catalog.aliases.iter().map(|alias| {
(
alias.id.clone(),
label(
alias.short_name.as_deref(),
alias.label.as_deref(),
&alias.id,
),
)
}))
.collect()
}

/// Recent projects for the header menu. The default Maple workspace stays
/// listed even when it sits past the recent cap.
fn header_project_paths(recent_roots: &[RecentProjectRoot]) -> Vec<String> {
Expand Down Expand Up @@ -253,7 +291,7 @@ impl ChatScreen {
let pick = model.clone();
let item = MenuItem::new(
SharedString::from(format!("model-{model}")),
model.clone(),
self.model_label(&model).to_string(),
move |this: &mut Self, _: &mut Window, cx: &mut Context<Self>| {
this.pick_model(pick.clone(), cx);
},
Expand Down Expand Up @@ -813,8 +851,10 @@ impl ChatScreen {
let drafts = &self.draft_images;
let model_label = self
.selected_model
.clone()
.unwrap_or_else(|| "Model".to_string());
.as_deref()
.map(|model| self.model_label(model))
.unwrap_or("Model")
.to_string();
div()
.w_full()
.flex()
Expand Down Expand Up @@ -1273,7 +1313,34 @@ fn chip(

#[cfg(test)]
mod tests {
use super::{RecentProjectRoot, header_project_paths};
use super::{RecentProjectRoot, catalog_model_labels, header_project_paths};

#[test]
fn model_labels_follow_research_catalog_precedence() {
let catalog = serde_json::from_value(serde_json::json!({
"object": "list",
"data": [
{"id": "model-a", "short_name": "Compact", "display_name": "Full name"},
{"id": "model-b", "display_name": "Server supplied name"},
{"id": "model-c", "short_name": " ", "display_name": "Fallback name"},
{"id": "model-d"}
],
"aliases": [
{"id": "alias-a", "short_name": "Alias", "label": "Full alias", "target_model": "model-a"},
{"id": "alias-b", "label": "Automatic", "target_model": "model-b"},
{"id": "alias-c", "short_name": "", "label": " ", "target_model": "model-c"}
]
}))
.unwrap();
let labels = catalog_model_labels(&catalog);
assert_eq!(labels["model-a"], "Compact");
assert_eq!(labels["model-b"], "Server supplied name");
assert_eq!(labels["model-c"], "Fallback name");
assert_eq!(labels["model-d"], "model-d");
assert_eq!(labels["alias-a"], "Alias");
assert_eq!(labels["alias-b"], "Automatic");
assert_eq!(labels["alias-c"], "alias-c");
}

#[test]
fn the_header_keeps_the_owners_default_workspace_past_the_recent_cap() {
Expand Down
25 changes: 25 additions & 0 deletions apps/maple-agent/app/src/ui/chat/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,8 @@ pub struct ChatScreen {
/// The catalog's vision and context window per model id, looked up
/// when a model is chosen.
model_catalog: HashMap<String, CatalogEntry>,
/// Friendly labels from the advanced catalog; selection always uses IDs.
model_labels: HashMap<String, String>,
/// MCP servers with their enabled state for the selected task.
session_mcp: Vec<AgentSessionMcpServer>,
/// Count of enabled servers in `session_mcp`, for the composer chip.
Expand Down Expand Up @@ -1283,6 +1285,7 @@ impl ChatScreen {
draft_counter: 0,
image_picking: false,
model_catalog: HashMap::new(),
model_labels: HashMap::new(),
session_mcp: Vec::new(),
mcp_enabled_count: 0,
composer_expanded: false,
Expand Down Expand Up @@ -1963,7 +1966,25 @@ impl ChatScreen {
.unwrap_or_else(|| "Choose folder".to_string())
}

fn refresh_model_labels(&self, cx: &mut Context<Self>) {
let backend = self.backend.clone();
let user_id = self.user_id.clone();
self.call(
async move { backend.model_catalog(&user_id).await },
cx,
|this, result, cx| {
if let Ok(catalog) = result {
this.model_labels = composer::catalog_model_labels(&catalog);
cx.notify();
}
},
);
}

fn refresh_models(&self, cx: &mut Context<Self>) {
// Presentation metadata loads independently so failure or latency does
// not affect the existing model list or default selection.
self.refresh_model_labels(cx);
let backend = self.backend.clone();
let user_id = self.user_id.clone();
let env_model = backend.configured_model();
Expand Down Expand Up @@ -5161,6 +5182,10 @@ impl ChatScreen {
}
}

fn model_label<'a>(&'a self, model: &'a str) -> &'a str {
self.model_labels.get(model).map_or(model, String::as_str)
}

fn model_menu_models(&self) -> Vec<String> {
if self.model_locked {
self.selected_model.iter().cloned().collect()
Expand Down
4 changes: 4 additions & 0 deletions apps/maple-agent/app/src/ui/chat/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2742,6 +2742,10 @@ mod state_tests {
this.default_model = Some("glm-5-3".into());
this.selected_model = Some("glm-5-3".into());

this.model_labels.insert("kimi-k3".into(), "Kimi K3".into());
assert_eq!(this.model_label("kimi-k3"), "Kimi K3");
assert_eq!(this.model_label("unknown-model"), "unknown-model");

let mut glm = summary("glm-task", "GLM");
glm.model = Some("glm-5-3".into());
glm.message_count = 2;
Expand Down
8 changes: 8 additions & 0 deletions apps/maple-agent/crates/maple-agent/src/agent/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,14 @@ impl AgentRuntimeHandle {
Ok(Arc::clone(&self.runtime().await?.api))
}

/// Full presentation metadata from the advanced catalog, including labels
/// for concrete models and aliases. Does not select or add any models.
pub async fn model_catalog(&self) -> Result<maple_sdk::ModelCatalogResponse, String> {
let catalog = self.api().await?.model_catalog().await?;
self.verify_generation().await?;
Ok(catalog)
}

/// The models a task may pick, the runtime's default first.
pub async fn available_model_ids(&self) -> Result<Vec<String>, String> {
let runtime = self.runtime().await?;
Expand Down
36 changes: 36 additions & 0 deletions sdk/rust/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,10 @@ impl ModelReasoning {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CatalogModel {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub short_name: Option<String>,
#[serde(default = "default_model_object")]
pub object: String,
#[serde(skip_serializing_if = "Option::is_none")]
Expand Down Expand Up @@ -771,6 +775,10 @@ pub struct CatalogCapabilities {
pub struct CatalogAlias {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub short_name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub capabilities: Option<CatalogCapabilities>,
Expand Down Expand Up @@ -1392,6 +1400,34 @@ pub enum AgentSseEvent {

#[cfg(test)]
mod tests {
#[test]
fn catalog_preserves_optional_model_and_alias_labels() {
let catalog: super::ModelCatalogResponse = serde_json::from_value(serde_json::json!({
"object": "list",
"data": [
{"id": "model-a", "short_name": "Compact", "display_name": "Full name"},
{"id": "legacy-model"}
],
"aliases": [
{"id": "auto:quick", "short_name": "Quick", "label": "Automatic quick"},
{"id": "legacy-alias"}
]
}))
.unwrap();
assert_eq!(catalog.data[0].short_name.as_deref(), Some("Compact"));
assert_eq!(catalog.data[0].display_name.as_deref(), Some("Full name"));
assert_eq!(catalog.aliases[0].short_name.as_deref(), Some("Quick"));
assert_eq!(catalog.aliases[0].label.as_deref(), Some("Automatic quick"));
assert!(catalog.data[1].short_name.is_none());
assert!(catalog.data[1].display_name.is_none());
assert!(catalog.aliases[1].short_name.is_none());
assert!(catalog.aliases[1].label.is_none());
let serialized = serde_json::to_value(catalog).unwrap();
assert_eq!(serialized["data"][0]["short_name"], "Compact");
assert!(serialized["data"][1].get("short_name").is_none());
assert!(serialized["aliases"][1].get("label").is_none());
}

use super::*;
use serde_json::json;

Expand Down
Loading