mod auth;
pub mod logging;
pub mod mcp;
mod paths;
mod provider;
pub mod reasoning;
mod ui;
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use crate::prompts::{SessionMode, default_system_prompt, gateway_system_prompt};
use crate::theme::ThemeName;
use crate::tooling::ToolPermission;
use self::reasoning::ThinkingMatcher;
pub use auth::{ActiveModel, AuthStore, ModelSummary, ProviderAuth};
pub use logging::LogConfig;
pub use mcp::{McpConfig, McpServerConfig};
pub use paths::ConfigPaths;
pub use provider::{ApiType, ModelConfig, ProviderConfig, ProviderSource};
pub use ui::UiConfig;
const BUNDLED_PRESETS_TOML: &str = include_str!("../../presets.toml");
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AppConfig {
pub default_provider: String,
pub default_model: String,
#[serde(default = "default_theme")]
pub theme: String,
#[serde(default)]
pub ui: UiConfig,
#[serde(default)]
pub logging: LogConfig,
#[serde(default)]
pub providers: BTreeMap<String, ProviderConfig>,
#[serde(default)]
pub instructions: Vec<String>,
#[serde(default)]
pub skills: Vec<String>,
#[serde(default, skip_serializing_if = "mcp::McpConfig::is_empty")]
pub mcp: McpConfig,
#[serde(default)]
pub permissions: PermissionConfig,
#[serde(default)]
pub notifications: NotificationConfig,
#[serde(default)]
pub gateway: GatewayConfig,
#[serde(default)]
pub rtk: RtkConfig,
#[serde(default)]
pub agent: AgentConfig,
#[serde(skip)]
pub bundled_providers: BTreeMap<String, ProviderConfig>,
}
fn default_theme() -> String {
ThemeName::Dark.as_str().to_string()
}
impl Default for AppConfig {
fn default() -> Self {
Self {
default_provider: "openai".to_string(),
default_model: "gpt-4o-mini".to_string(),
theme: ThemeName::Dark.as_str().to_string(),
ui: UiConfig::default(),
logging: LogConfig::default(),
providers: BTreeMap::new(),
instructions: Vec::new(),
skills: Vec::new(),
mcp: McpConfig::default(),
permissions: PermissionConfig::default(),
notifications: NotificationConfig::default(),
gateway: GatewayConfig::default(),
rtk: RtkConfig::default(),
agent: AgentConfig::default(),
bundled_providers: bundled_provider_catalog().unwrap_or_default(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct GatewayConfig {
#[serde(default)]
pub telegram: TelegramGatewayConfig,
#[serde(default)]
pub qq: QQGatewayConfig,
#[serde(default)]
pub default_provider: String,
#[serde(default)]
pub default_model: String,
#[serde(default = "default_gateway_session_persistence")]
pub session_persistence: bool,
}
fn default_gateway_session_persistence() -> bool {
true
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RtkConfig {
#[serde(default = "default_rtk_enabled")]
pub enabled: bool,
#[serde(skip)]
pub installed: bool,
}
impl Default for RtkConfig {
fn default() -> Self {
Self {
enabled: default_rtk_enabled(),
installed: false,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AgentConfig {
#[serde(default = "default_agent_enabled")]
pub enabled: bool,
#[serde(default)]
pub default_subagent_model: String,
#[serde(default)]
pub default_subagent_provider: String,
#[serde(default = "default_max_depth")]
pub max_depth: usize,
#[serde(default = "default_max_sessions_per_agent")]
pub max_sessions_per_agent: usize,
#[serde(default)]
pub models: BTreeMap<String, String>,
}
fn default_agent_enabled() -> bool {
true
}
fn default_max_depth() -> usize {
3
}
fn default_max_sessions_per_agent() -> usize {
5
}
impl AgentConfig {
pub fn model_for(&self, agent_type: &str) -> Option<&str> {
self.models.get(agent_type).map(|s| s.as_str())
}
pub fn default_model(&self) -> Option<&str> {
let m = self.default_subagent_model.trim();
if m.is_empty() { None } else { Some(m) }
}
}
impl Default for AgentConfig {
fn default() -> Self {
Self {
enabled: true,
default_subagent_model: String::new(),
default_subagent_provider: String::new(),
max_depth: 3,
max_sessions_per_agent: 5,
models: BTreeMap::new(),
}
}
}
fn default_rtk_enabled() -> bool {
true
}
pub fn check_rtk_installed() -> bool {
std::process::Command::new("rtk")
.arg("--version")
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TelegramGatewayConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub allowlist: Vec<String>,
#[serde(default = "default_telegram_poll_timeout_secs")]
pub poll_timeout_secs: u64,
}
fn default_telegram_poll_timeout_secs() -> u64 {
30
}
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct QQGatewayConfig {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub allowlist: Vec<String>,
#[serde(default)]
pub sandbox: bool,
}
impl Default for TelegramGatewayConfig {
fn default() -> Self {
Self {
enabled: false,
allowlist: Vec::new(),
poll_timeout_secs: default_telegram_poll_timeout_secs(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NotificationConfig {
#[serde(default = "default_true")]
pub enabled: bool,
#[serde(default)]
pub method: String,
#[serde(default)]
pub condition: String,
}
fn default_true() -> bool {
true
}
impl Default for NotificationConfig {
fn default() -> Self {
Self {
enabled: true,
method: "auto".to_string(),
condition: "unfocused".to_string(),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
pub struct PermissionSettings {
#[serde(default)]
pub read: bool,
#[serde(default)]
pub search: bool,
#[serde(default)]
pub write: bool,
#[serde(default)]
pub edit: bool,
#[serde(default)]
pub execute: bool,
#[serde(default)]
pub session: bool,
}
impl PermissionSettings {
pub fn is_allowed(&self, permission: ToolPermission) -> bool {
match permission {
ToolPermission::Read => self.read,
ToolPermission::Search => self.search,
ToolPermission::Write => self.write,
ToolPermission::Edit => self.edit,
ToolPermission::Execute => self.execute,
ToolPermission::Session => self.session,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PermissionConfig {
#[serde(default)]
pub plan: PermissionSettings,
#[serde(default)]
pub build: PermissionSettings,
}
impl PermissionConfig {
pub fn is_allowed(&self, mode: SessionMode, permission: ToolPermission) -> bool {
match mode {
SessionMode::Plan => self.plan.is_allowed(permission),
SessionMode::Build => self.build.is_allowed(permission),
}
}
}
impl Default for PermissionConfig {
fn default() -> Self {
Self {
plan: PermissionSettings {
read: true,
search: true,
write: false,
edit: false,
execute: true,
session: true,
},
build: PermissionSettings {
read: true,
search: true,
write: true,
edit: true,
execute: true,
session: true,
},
}
}
}
impl AppConfig {
pub fn load_or_create(paths: &ConfigPaths) -> Result<Self> {
paths.ensure_directories()?;
let rtk_installed = check_rtk_installed();
if !paths.config_file.exists() {
let example = Self::example_toml();
std::fs::write(&paths.config_file, example)
.with_context(|| format!("failed to write {}", paths.config_file.display()))?;
let mut config: Self = toml::from_str(example).with_context(|| {
format!("failed to parse generated {}", paths.config_file.display())
})?;
config.rtk.installed = rtk_installed;
if !rtk_installed {
config.rtk.enabled = false;
}
return config.attach_bundled_providers();
}
let contents = std::fs::read_to_string(&paths.config_file)
.with_context(|| format!("failed to read {}", paths.config_file.display()))?;
let mut config: Self = toml::from_str(&contents)
.with_context(|| format!("failed to parse {}", paths.config_file.display()))?;
config.rtk.installed = rtk_installed;
if !rtk_installed {
config.rtk.enabled = false;
}
config.attach_bundled_providers()
}
pub fn save(&self, paths: &ConfigPaths) -> Result<()> {
paths.ensure_directories()?;
let contents = toml::to_string_pretty(self).context("failed to serialize config")?;
std::fs::write(&paths.config_file, contents)
.with_context(|| format!("failed to write {}", paths.config_file.display()))?;
Ok(())
}
pub fn example_toml() -> &'static str {
r#"# TiDev configuration
# Bundled provider presets ship with the binary and do not need to be copied here.
# Add your own providers below if you want custom endpoints.
# `theme` can be one of: dark, light, nord, one-dark, catppuccin, solarized, orng, github, material.
theme = "dark"
default_provider = "openai"
default_model = "gpt-4o-mini"
# Optional custom instruction files or glob patterns to include in the system prompt.
# Example: instructions = ["docs/style.md", "packages/*/AGENTS.md"]
instructions = []
# Optional additional skill sources. Each entry can be a local path or an HTTP(S) URL to a SKILL.md file.
# Example: skills = ["https://example.com/skills/git-release/SKILL.md"]
skills = []
# Optional logging configuration.
# Set enabled = true to enable file logging.
# level can be: DEBUG, INFO, WARN, ERROR
# max_size_mb: max log file size before rotation (default: 10)
# max_files: number of rotated log files to keep (default: 5)
#[logging]
#enabled = false
#level = "INFO"
#max_size_mb = 10
#max_files = 5
# RTK (Rust Token Killer) configuration.
# When enabled, command outputs are compressed to save tokens.
# RTK must be installed on your system for this to work.
# If RTK is not installed, this setting is ignored.
#[rtk]
#enabled = true
# Optional permission settings by mode.
# By default plan mode allows read/search/session/execute (shell, but only for read-only commands) and build mode allows all permissions.
#permissions = { plan = { read = true, search = true, session = true, execute = true, write = false, edit = false }, build = { read = true, search = true, session = true, write = true, edit = true, execute = true } }
# MCP servers can be declared here. Supported transports: stdio, streamable HTTP, and SSE.
# [mcp.servers.my_server]
# kind = "stdio"
# command = "npx"
# args = ["-y", "@modelcontextprotocol/server-filesystem", "."]
#
# [mcp.servers.remote]
# kind = "http"
# url = "https://example.com/mcp"
#
# [mcp.servers.events]
# kind = "sse"
# url = "https://example.com/sse"
[mcp]
[ui]
sidebar_width = 30
welcome_width = 72
max_input_lines = 6
# Scroll speed multiplier (default: 3)
# scroll_speed = 3
[notifications]
# Enable notifications (default: true)
# enabled = true
# Notification method: "auto", "osc9", or "bel" (default: "auto")
# method = "auto"
# When to notify: "unfocused" or "always" (default: "unfocused")
# condition = "unfocused"
[gateway.telegram]
enabled = false
# allowlist can contain Telegram user IDs or chat IDs as strings.
allowlist = []
poll_timeout_secs = 30
"#
}
fn attach_bundled_providers(mut self) -> Result<Self> {
self.bundled_providers = bundled_provider_catalog()?;
Ok(self)
}
fn provider(&self, provider_id: &str) -> Option<&ProviderConfig> {
self.providers
.get(provider_id)
.or_else(|| self.bundled_providers.get(provider_id))
}
pub fn provider_source(&self, provider_id: &str) -> Option<ProviderSource> {
if self.providers.contains_key(provider_id) {
Some(ProviderSource::User)
} else if self.bundled_providers.contains_key(provider_id) {
Some(ProviderSource::Bundled)
} else {
None
}
}
pub fn provider_exists(&self, provider_id: &str) -> bool {
self.provider_source(provider_id).is_some()
}
pub fn available_models(&self) -> Vec<ModelSummary> {
let mut models = Vec::new();
for provider_id in self.provider_ids() {
let Some(provider) = self.provider(&provider_id) else {
continue;
};
for (model_id, model) in &provider.models {
models.push(ModelSummary {
provider_id: provider_id.clone(),
provider_display_name: provider.display_name.clone(),
model_id: model_id.clone(),
model_display_name: model.display_name.clone(),
base_url: provider.base_url.clone(),
context_window: model.context_window,
max_output_tokens: model.max_output_tokens,
});
}
}
models
}
pub fn connected_models(&self, auth: &AuthStore) -> Vec<ModelSummary> {
self.available_models()
.into_iter()
.filter(|summary| auth.api_key(&summary.provider_id).is_some())
.collect()
}
pub fn resolve_active_model(&self, auth: &AuthStore) -> Result<ActiveModel> {
self.resolve_model(auth, None)
}
pub fn resolve_active_model_for_gateway(&self, auth: &AuthStore) -> Result<ActiveModel> {
let provider_id = if !self.gateway.default_provider.is_empty() {
&self.gateway.default_provider
} else {
&self.default_provider
};
let model_id = if !self.gateway.default_model.is_empty() {
&self.gateway.default_model
} else {
&self.default_model
};
let mut model = self.resolve_model_by_ids(auth, provider_id, model_id)?;
model.system_prompt = gateway_system_prompt();
Ok(model)
}
pub fn resolve_provider_default_model(
&self,
auth: &AuthStore,
provider_id: &str,
) -> Result<ActiveModel> {
let provider = self
.provider(provider_id)
.with_context(|| format!("unknown provider '{provider_id}'"))?;
let model_id = provider
.models
.keys()
.next()
.cloned()
.with_context(|| format!("provider '{provider_id}' has no configured models"))?;
self.resolve_model_by_ids(auth, provider_id, &model_id)
}
pub fn resolve_model(&self, auth: &AuthStore, query: Option<&str>) -> Result<ActiveModel> {
let (provider_id, model_id) = match query.map(str::trim).filter(|value| !value.is_empty()) {
Some(query) => self.resolve_model_key(query)?,
None => (self.default_provider.clone(), self.default_model.clone()),
};
self.resolve_model_by_ids(auth, &provider_id, &model_id)
}
pub fn resolve_agent_active_model(
&self,
auth: &AuthStore,
agent_type: &str,
) -> Result<Option<ActiveModel>> {
let Some(model_str) = self.agent.model_for(agent_type).or_else(|| self.agent.default_model()) else {
return Ok(None);
};
let (provider_id, model_id) = if let Some(slash_pos) = model_str.find('/') {
let provider = &model_str[..slash_pos];
let model = &model_str[slash_pos + 1..];
(provider.to_string(), model.to_string())
} else {
(self.default_provider.clone(), model_str.to_string())
};
self.resolve_model_by_ids(auth, &provider_id, &model_id)
.map(Some)
.or_else(|e| {
crate::log_warn!(
"failed to resolve agent model '{}' for '{}': {}",
model_str,
agent_type,
e
);
Ok(None)
})
}
pub fn set_agent_model(&mut self, paths: &ConfigPaths, agent_type: &str, model_str: &str) -> Result<()> {
if model_str.is_empty() {
self.agent.models.remove(agent_type);
} else {
self.agent.models.insert(agent_type.to_string(), model_str.to_string());
}
self.save(paths)
}
pub fn agent_model_label(&self, agent_type: &str) -> Option<&str> {
self.agent.models.get(agent_type).map(|s| s.as_str())
}
pub fn agent_model_display(&self, agent_type: &str) -> String {
self.agent_model_label(agent_type)
.map(|s| s.to_string())
.unwrap_or_else(|| "<inherit>".to_string())
}
pub fn resolve_model_by_ids(
&self,
auth: &AuthStore,
provider_id: &str,
model_id: &str,
) -> Result<ActiveModel> {
let provider = self
.provider(provider_id)
.with_context(|| format!("unknown provider '{provider_id}'"))?;
let model = provider
.models
.get(model_id)
.with_context(|| format!("unknown model '{model_id}' for provider '{provider_id}'"))?;
let api_key = self.resolve_api_key(auth, provider_id);
let api_type = provider
.api_type
.as_deref()
.map(ApiType::parse)
.unwrap_or_default();
let request_model_id = model
.request_model_id
.clone()
.unwrap_or_else(|| model_id.to_string());
let thinking_level = if let Some(ref rid) = model.request_model_id {
ThinkingMatcher::match_for_model(rid)
} else {
let display_name = model.display_name.clone();
if display_name.is_empty() {
ThinkingMatcher::match_for_model(model_id)
} else {
ThinkingMatcher::match_for_model(&display_name)
}
};
Ok(ActiveModel {
provider_id: provider_id.to_string(),
provider_display_name: provider.display_name.clone(),
base_url: provider.base_url.clone(),
api_type,
model_id: model_id.to_string(),
request_model_id,
display_name: model.display_name.clone(),
context_window: model.context_window,
max_output_tokens: model.max_output_tokens,
temperature: model.temperature,
supports_images: model.supports_images,
system_prompt: model
.system_prompt
.clone()
.filter(|prompt| !prompt.trim().is_empty())
.unwrap_or_else(default_system_prompt),
api_key,
extra_body: model.extra_body.clone(),
thinking_level,
})
}
pub fn default_model_summary(&self) -> Result<ModelSummary> {
let provider = self
.provider(&self.default_provider)
.with_context(|| format!("unknown default provider '{}'", self.default_provider))?;
let model = provider
.models
.get(&self.default_model)
.with_context(|| format!("unknown default model '{}'", self.default_model))?;
Ok(ModelSummary {
provider_id: self.default_provider.clone(),
provider_display_name: provider.display_name.clone(),
model_id: self.default_model.clone(),
model_display_name: model.display_name.clone(),
base_url: provider.base_url.clone(),
context_window: model.context_window,
max_output_tokens: model.max_output_tokens,
})
}
fn resolve_model_key(&self, query: &str) -> Result<(String, String)> {
if let Some((provider, model)) = query.split_once(':').or_else(|| query.split_once('/')) {
let provider = provider.trim();
let model = model.trim();
if provider.is_empty() || model.is_empty() {
bail!("model selector '{query}' must be in provider:model or provider/model form");
}
return Ok((provider.to_string(), model.to_string()));
}
let mut matches = Vec::new();
for provider_id in self.provider_ids() {
if let Some(provider) = self.provider(&provider_id)
&& provider.models.contains_key(query)
{
matches.push((provider_id.clone(), query.to_string()));
}
}
match matches.len() {
0 => bail!("unknown model '{query}'"),
1 => Ok(matches.remove(0)),
_ => {
let choices = matches
.into_iter()
.map(|(provider_id, model_id)| format!("{provider_id}:{model_id}"))
.collect::<Vec<_>>()
.join(", ");
bail!("model '{query}' is ambiguous; use one of: {choices}");
}
}
}
fn resolve_api_key(&self, auth: &AuthStore, provider_id: &str) -> Option<String> {
auth.providers
.get(provider_id)
.and_then(|entry| entry.api_key.clone())
.filter(|value| !value.trim().is_empty())
}
pub fn provider_ids(&self) -> Vec<String> {
let mut provider_ids = BTreeSet::new();
for provider_id in self.providers.keys().chain(self.bundled_providers.keys()) {
provider_ids.insert(provider_id.clone());
}
provider_ids.into_iter().collect()
}
pub fn provider_display_name(&self, provider_id: &str) -> Option<&str> {
self.provider(provider_id)
.map(|provider| provider.display_name.as_str())
}
pub fn set_theme(&mut self, theme: ThemeName) {
self.theme = theme.as_str().to_string();
}
pub fn theme_name(&self) -> ThemeName {
ThemeName::parse(&self.theme).unwrap_or(ThemeName::Dark)
}
}
#[derive(Clone, Debug, Deserialize)]
struct BundledProviderCatalog {
#[serde(default)]
providers: BTreeMap<String, ProviderConfig>,
}
fn bundled_provider_catalog() -> Result<BTreeMap<String, ProviderConfig>> {
let catalog: BundledProviderCatalog =
toml::from_str(BUNDLED_PRESETS_TOML).context("failed to parse bundled provider catalog")?;
Ok(catalog.providers)
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeMap;
#[test]
fn bundled_provider_catalog_loads() {
let catalog = bundled_provider_catalog().expect("bundled catalog should parse");
assert!(catalog.contains_key("deepseek"));
}
#[test]
fn app_config_uses_bundled_provider_ids() {
let config = AppConfig::default();
assert!(config.provider_ids().contains(&"deepseek".to_string()));
assert_eq!(
config.provider_source("deepseek"),
Some(ProviderSource::Bundled)
);
}
#[test]
fn connected_models_only_includes_providers_with_api_keys() {
let mut config = AppConfig::default();
config.providers.insert(
"provider_one".to_string(),
ProviderConfig {
display_name: "Provider One".to_string(),
base_url: "https://api.provider.one".to_string(),
api_type: None,
models: BTreeMap::from([(
"model-a".to_string(),
ModelConfig {
display_name: "Model A".to_string(),
context_window: 1024,
max_output_tokens: 1024,
temperature: 0.7,
system_prompt: None,
supports_streaming: true,
supports_images: false,
extra_body: None,
request_model_id: None,
},
)]),
},
);
config.providers.insert(
"provider_two".to_string(),
ProviderConfig {
display_name: "Provider Two".to_string(),
base_url: "https://api.provider.two".to_string(),
api_type: None,
models: BTreeMap::from([(
"model-b".to_string(),
ModelConfig {
display_name: "Model B".to_string(),
context_window: 1024,
max_output_tokens: 1024,
temperature: 0.7,
system_prompt: None,
supports_streaming: true,
supports_images: false,
extra_body: None,
request_model_id: None,
},
)]),
},
);
let mut auth = AuthStore::default();
auth.set_api_key("provider_one", "sk-test-key");
let connected = config.connected_models(&auth);
assert_eq!(connected.len(), 1);
assert_eq!(connected[0].provider_id, "provider_one");
assert_eq!(connected[0].model_id, "model-a");
}
#[test]
fn user_provider_overrides_bundled_preset() {
let mut config = AppConfig::default();
config.providers.insert(
"deepseek".to_string(),
ProviderConfig {
display_name: "Custom DeepSeek".to_string(),
base_url: "https://example.com/v1".to_string(),
api_type: None,
models: BTreeMap::new(),
},
);
assert_eq!(
config.provider_source("deepseek"),
Some(ProviderSource::User)
);
assert_eq!(
config.provider_display_name("deepseek"),
Some("Custom DeepSeek")
);
assert_eq!(
config
.provider_ids()
.iter()
.filter(|id| *id == "deepseek")
.count(),
1
);
}
#[test]
fn default_permission_config_matches_mode_expectations() {
let permissions = PermissionConfig::default();
assert!(permissions.is_allowed(SessionMode::Plan, ToolPermission::Read));
assert!(permissions.is_allowed(SessionMode::Plan, ToolPermission::Search));
assert!(permissions.is_allowed(SessionMode::Plan, ToolPermission::Session));
assert!(permissions.is_allowed(SessionMode::Plan, ToolPermission::Execute));
assert!(!permissions.is_allowed(SessionMode::Plan, ToolPermission::Write));
assert!(!permissions.is_allowed(SessionMode::Plan, ToolPermission::Edit));
assert!(permissions.is_allowed(SessionMode::Build, ToolPermission::Read));
assert!(permissions.is_allowed(SessionMode::Build, ToolPermission::Search));
assert!(permissions.is_allowed(SessionMode::Build, ToolPermission::Session));
assert!(permissions.is_allowed(SessionMode::Build, ToolPermission::Write));
assert!(permissions.is_allowed(SessionMode::Build, ToolPermission::Edit));
assert!(permissions.is_allowed(SessionMode::Build, ToolPermission::Execute));
}
#[test]
fn gateway_mode_uses_separate_system_prompt() {
use crate::prompts::{default_system_prompt, gateway_system_prompt};
let auth = AuthStore::default();
let mut config = AppConfig::default();
config.providers = config.bundled_providers.clone();
config.default_provider = "deepseek".to_string();
config.default_model = "deepseek-v4-flash".to_string();
let tui_model = config.resolve_active_model(&auth).unwrap();
let gateway_model = config.resolve_active_model_for_gateway(&auth).unwrap();
assert_eq!(
gateway_model.system_prompt,
gateway_system_prompt(),
"gateway model should use gateway_system_prompt"
);
assert_ne!(
gateway_model.system_prompt, tui_model.system_prompt,
"gateway model should have different system_prompt from tui model"
);
assert_eq!(
tui_model.system_prompt,
default_system_prompt(),
"tui model should use default_system_prompt"
);
}
}