1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
// crates/atomcode-tuix/src/modals/model_picker.rs
//
// `/model` modal — provider list picker.
//
// Holds the provider list sorted alphabetically with the current default
// first. Up/Down navigates, Enter selects (persists to config + notifies
// agent), Esc cancels. Renders as a MenuPayload above the input box.
use anyhow::Result;
use atomcode_core::config::Config;
use crossterm::event::{KeyCode, KeyModifiers};
use super::{Modal, ModalAction};
use crate::event_loop::{build_status, save_and_reload, Buffer, LoopCtx};
use crate::render::{MenuPayload, Renderer, UiLine};
use crate::state::UiState;
pub struct ModelPicker {
pub providers: Vec<String>,
pub selected: usize,
}
impl ModelPicker {
pub fn open(config: &Config) -> Self {
let mut providers: Vec<String> = config.providers.keys().cloned().collect();
providers.sort();
// Put the current default at top for quick re-confirmation.
let cur = config.default_provider.clone();
if let Some(idx) = providers.iter().position(|p| *p == cur) {
providers.swap(0, idx);
}
Self {
providers,
selected: 0,
}
}
}
impl Modal for ModelPicker {
fn handle_key(
&mut self,
code: KeyCode,
_mods: KeyModifiers,
buf: &mut Buffer,
state: &mut UiState,
ctx: &mut LoopCtx,
renderer: &mut dyn Renderer,
) -> Result<ModalAction> {
match code {
KeyCode::Up => {
self.selected = self.selected.saturating_sub(1);
self.draw(buf, state, ctx, renderer);
Ok(ModalAction::Continue)
}
KeyCode::Down => {
let max = self.providers.len().saturating_sub(1);
if self.selected < max {
self.selected += 1;
}
self.draw(buf, state, ctx, renderer);
Ok(ModalAction::Continue)
}
KeyCode::Enter => {
let chosen = self.providers[self.selected].clone();
let display = ctx
.config
.providers
.get(&chosen)
.map(|p| p.model.clone())
.unwrap_or_else(|| chosen.clone());
ctx.config.default_provider = chosen.clone();
ctx.model_name = display.clone();
// Persist to config.toml + notify agent. Without this,
// the switch lives only in memory and the next startup
// reverts to whatever was last saved.
save_and_reload(ctx, renderer);
// Clear any stale drift warning tied to the PREVIOUS
// active provider — if the new provider is also CodingPlan,
// the re-fire below will repopulate the slot with a
// correct warning (or leave it clean).
if let Ok(mut g) = ctx.monitor_warning.lock() {
*g = None;
}
// Same treatment for the usage slot: switching providers
// invalidates the previous CodingPlan's quota snapshot.
// Clearing here makes `build_usage_hint` short-circuit to
// None until a fresh fetch lands; otherwise the user would
// briefly see the OLD plan's percent on the new provider.
if let Ok(mut g) = ctx.usage_slot.lock() {
*g = None;
}
// Re-fire the drift check if the new provider is also
// CodingPlan-managed. Bypasses the 15-min cooldown on
// purpose — explicit user action deserves a fresh read.
if crate::event_loop::monitor::is_codingplan_provider(&chosen) {
ctx.monitor_last_check_at = Some(std::time::Instant::now());
crate::event_loop::monitor::spawn_check(
ctx.config.clone(),
ctx.model_name.clone(),
ctx.monitor_warning.clone(),
ctx.wake_tx.clone(),
);
// Mirror: re-fire usage check too. 30s cooldown is
// bypassed because the user just made an explicit
// switch — they want fresh data, not stale 30s data.
ctx.usage_last_check_at = Some(std::time::Instant::now());
crate::event_loop::usage_monitor::spawn_check(
ctx.usage_slot.clone(),
ctx.wake_tx.clone(),
);
}
renderer.render(UiLine::CommandOutput(
crate::i18n::t(crate::i18n::Msg::ModelSwitched { provider: &chosen, model: &display }).into_owned(),
));
renderer.flush();
Ok(ModalAction::Close)
}
KeyCode::Esc => Ok(ModalAction::Close),
_ => Ok(ModalAction::Continue),
}
}
fn draw(&self, buf: &Buffer, state: &UiState, ctx: &LoopCtx, renderer: &mut dyn Renderer) {
let items: Vec<(String, String)> = self
.providers
.iter()
.map(|name| {
let desc = ctx
.config
.providers
.get(name)
.map(|c| format!("{} · {}", c.provider_type, c.model))
.unwrap_or_default();
(name.clone(), desc)
})
.collect();
let payload = MenuPayload {
items,
selected: self.selected,
kind: crate::render::MenuKind::SlashCommand,
};
renderer.render(UiLine::InputPrompt {
buf: buf.text.clone(),
cursor_byte: buf.cursor,
menu: Some(payload),
status: build_status(state, ctx),
attachments: Vec::new(),
});
renderer.flush();
}
}