feat(tui) /personality (#9718)

## Summary
Adds /personality selector in the TUI, which leverages the new core
interface in #9644

Notes:
- We are doing some of our own state management for model_info loading
here, but not sure if that's ideal. open to opinions on simpler
approach, but would like to avoid blocking on a larger refactor
- Right now, the `/personality` selector just hides when the model
doesn't support it. we can update this behavior down the line

## Testing
- [x] Tested locally
- [x] Added snapshot tests
This commit is contained in:
Dylan Hurd
2026-01-25 21:59:42 -08:00
committed by GitHub
parent d27f2533a9
commit 031bafd1fb
23 changed files with 421 additions and 32 deletions

View File

@@ -2,6 +2,7 @@ use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use strum_macros::Display;
use strum_macros::EnumIter;
use ts_rs::TS;
use crate::openai_models::ReasoningEffort;
@@ -78,6 +79,7 @@ pub enum SandboxMode {
TS,
PartialOrd,
Ord,
EnumIter,
)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]

View File

@@ -78,6 +78,9 @@ pub struct ModelPreset {
pub default_reasoning_effort: ReasoningEffort,
/// Supported reasoning effort options.
pub supported_reasoning_efforts: Vec<ReasoningEffortPreset>,
/// Whether this model supports personality-specific instructions.
#[serde(default)]
pub supports_personality: bool,
/// Whether this is the default model for new users.
pub is_default: bool,
/// recommended upgrade model
@@ -214,6 +217,12 @@ impl ModelInfo {
})
}
pub fn supports_personality(&self) -> bool {
self.model_instructions_template
.as_ref()
.is_some_and(ModelInstructionsTemplate::supports_personality)
}
pub fn get_model_instructions(&self, personality: Option<Personality>) -> String {
if let Some(personality) = personality
&& let Some(template) = &self.model_instructions_template
@@ -249,6 +258,13 @@ impl ModelInstructionsTemplate {
fn has_personality_placeholder(&self) -> bool {
self.template.contains(PERSONALITY_PLACEHOLDER)
}
fn supports_personality(&self) -> bool {
self.has_personality_placeholder()
&& self.personality_messages.as_ref().is_some_and(|messages| {
Personality::iter().all(|personality| messages.0.contains_key(&personality))
})
}
}
// serializes as a dictionary from personality to message
@@ -280,6 +296,7 @@ pub struct ModelsResponse {
// convert ModelInfo to ModelPreset
impl From<ModelInfo> for ModelPreset {
fn from(info: ModelInfo) -> Self {
let supports_personality = info.supports_personality();
ModelPreset {
id: info.slug.clone(),
model: info.slug.clone(),
@@ -289,6 +306,7 @@ impl From<ModelInfo> for ModelPreset {
.default_reasoning_level
.unwrap_or(ReasoningEffort::None),
supported_reasoning_efforts: info.supported_reasoning_levels.clone(),
supports_personality,
is_default: false, // default is the highest priority available model
upgrade: info.upgrade.as_ref().map(|upgrade| ModelUpgrade {
id: upgrade.model.clone(),