@ondeinference / cli / commits / e185469

Refactor API credentials and improve model name resolution

- Add GresIQ API key/secret constants and use them in gresiq.rs - Refactor code for clarity and formatting consistency - Update model assignment to resolve and display model names immediately - Improve fallback logic for model name display in UI - Add ondeinference-apps.png asset

keypair34 committed Apr 22, 2026 at 13:25 UTC e185469c0f18eebdedac845fa3f387d41ad5291b
10 files changed +266 -109
.github/workflows/release-pypi.yml
+6 -5
@@ -16,6 +16,10 @@ permissions:
16
17 env:
18 CARGO_TERM_COLOR: always
19 + ONDE_APP_ID: ${{ secrets.ONDE_APP_ID }}
20 + ONDE_APP_SECRET: ${{ secrets.ONDE_APP_SECRET }}
21 + GRESIQ_API_KEY: ${{ secrets.GRESIQ_API_KEY }}
22 + GRESIQ_API_SECRET: ${{ secrets.GRESIQ_API_SECRET }}
23
24 jobs:
25 build-wheels:
@@ -47,11 +51,7 @@ jobs:
51 OS: windows-2022,
52 TARGET: aarch64-pc-windows-msvc,
53 }
50 - - {
51 - NAME: darwin-x64,
52 - OS: macos-latest,
53 - TARGET: x86_64-apple-darwin,
54 - }
54 + - { NAME: darwin-x64, OS: macos-latest, TARGET: x86_64-apple-darwin }
55 - {
56 NAME: darwin-arm64,
57 OS: macos-latest,
@@ -103,6 +103,7 @@ jobs:
103 working-directory: pypi
104 args: --release --locked --compatibility pypi --out dist
105 before-script-linux: yum install -y perl-core
106 + docker-options: -e ONDE_APP_ID -e ONDE_APP_SECRET -e GRESIQ_API_KEY -e GRESIQ_API_SECRET
107
108 - name: Upload wheel artifact
109 uses: actions/upload-artifact@v4
.gitignore
+1
@@ -1,2 +1,3 @@
1 /target
2 /pypi/dist
3 +.env
Cargo.lock
+7
@@ -425,6 +425,12 @@ dependencies = [
425 "syn 2.0.117",
426 ]
427
428 +[[package]]
429 +name = "dotenvy"
430 +version = "0.15.7"
431 +source = "registry+https://github.com/rust-lang/crates.io-index"
432 +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
433 +
434 [[package]]
435 name = "either"
436 version = "1.15.0"
@@ -1142,6 +1148,7 @@ dependencies = [
1148 "anyhow",
1149 "crossterm",
1150 "dirs",
1151 + "dotenvy",
1152 "futures",
1153 "libc",
1154 "log",
Cargo.toml
+3
@@ -16,6 +16,9 @@ authors = ["Seto Elkahfi <hej@setoelkahfi.se>"]
16 name = "onde"
17 path = "src/main.rs"
18
19 +[build-dependencies]
20 +dotenvy = "0.15"
21 +
22 [dependencies]
23 anyhow = "1"
24 crossterm = { version = "0.28", features = ["event-stream"] }
assets/ondeinference-apps.png
Binary files /dev/null and b/assets/ondeinference-apps.png differ
build.rs new
+34
@@ -0,0 +1,34 @@
1 +fn main() {
2 + // Load .env if present (local dev). In CI the vars are injected directly
3 + // as environment variables, so this is a no-op there.
4 + let _ = dotenvy::dotenv();
5 +
6 + for var in [
7 + "ONDE_APP_ID",
8 + "ONDE_APP_SECRET",
9 + "GRESIQ_API_KEY",
10 + "GRESIQ_API_SECRET",
11 + ] {
12 + // Re-run the build script if any of these change.
13 + println!("cargo:rerun-if-env-changed={var}");
14 +
15 + match std::env::var(var) {
16 + Ok(val) => {
17 + // Forward the value to the compilation unit so env!() resolves.
18 + println!("cargo:rustc-env={var}={val}");
19 + }
20 + Err(_) => {
21 + // Fail the build loudly rather than silently baking in an
22 + // empty string. Add the missing variable to .env (local) or
23 + // to the CI environment (GitHub Actions secrets).
24 + eprintln!("error: required build variable `{var}` is not set.");
25 + eprintln!(" → Add it to .env for local builds.");
26 + eprintln!(" → Add it as a GitHub Actions secret for CI builds.");
27 + std::process::exit(1);
28 + }
29 + }
30 + }
31 +
32 + // Rebuild whenever .env itself changes.
33 + println!("cargo:rerun-if-changed=.env");
34 +}
pypi/README.md
+72 -32
@@ -1,11 +1,12 @@
1 <p align="center">
2 - <img src="https://raw.githubusercontent.com/ondeinference/onde/main/assets/onde-inference-logo.svg" alt="Onde Inference" width="96">
2 + <img src="https://raw.githubusercontent.com/ondeinference/onde/main/assets/onde-inference-logo.svg" alt="Onde Inference" width="80">
3 </p>
4
5 -<h1 align="center">onde-cli</h1>
5 +<h1 align="center">onde</h1>
6
7 <p align="center">
8 - <strong>Terminal UI for <a href="https://ondeinference.com">Onde Inference</a> — sign up, sign in, and manage your account from the command line.</strong>
8 + <strong>Manage your <a href="https://ondeinference.com">Onde Inference</a> apps from the terminal.</strong><br>
9 + Sign up, sign in, assign models — no browser required.
10 </p>
11
12 <p align="center">
@@ -15,6 +16,14 @@
16 <a href="https://github.com/ondeinference/onde-cli/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-MIT%20%2F%20Apache--2.0-235843?style=flat-square&labelColor=17211D" alt="License"></a>
17 </p>
18
19 +<br>
20 +
21 +<p align="center">
22 + <img src="https://raw.githubusercontent.com/ondeinference/onde-cli/main/assets/screenshot.png" alt="onde CLI — apps list with model assignments" width="720">
23 +</p>
24 +
25 +<br>
26 +
27 ---
28
29 ## Install
@@ -23,27 +32,56 @@
32 pip install onde-cli
33 ```
34
26 -This installs the native `onde` executable for your platform directly — no compiler, no runtime dependencies, no Node.js.
35 +Installs the native `onde` binary for your platform — no compiler, no Node.js, no runtime dependencies.
36
28 -## Usage
37 +## Quick start
38
39 ```sh
40 onde
41 ```
42
34 -Opens a terminal UI. Create an account or sign in to your Onde Inference account.
43 +A full terminal UI opens. Create an account or sign in, then manage your apps and model assignments without leaving the terminal.
44
45 ## Keys
46
38 -| Key | Action |
39 -|----------|-----------------------|
40 -| `Tab` | Move between fields |
41 -| `Enter` | Submit / sign out |
42 -| `Ctrl+L` | Go to sign in |
43 -| `Ctrl+N` | Go to new account |
44 -| `Ctrl+C` | Quit |
47 +### Auth screen
48 +
49 +| Key | Action |
50 +|-----------|---------------------------|
51 +| `Tab` | Move between fields |
52 +| `Enter` | Submit form |
53 +| `Ctrl+L` | Switch to Sign in |
54 +| `Ctrl+N` | Switch to Create account |
55 +| `Ctrl+C` | Quit |
56 +
57 +### Apps list
58 +
59 +| Key | Action |
60 +|-----------|---------------------------|
61 +| `↑` `↓` | Navigate apps |
62 +| `Enter` | Open app detail |
63 +| `n` | Create new app |
64 +| `s` | Sign out |
65 +| `Ctrl+C` | Quit |
66 +
67 +### App detail
68 +
69 +| Key | Action |
70 +|-----------|---------------------------|
71 +| `m` | Assign / change model |
72 +| `r` | Rename app |
73 +| `s` | Sign out |
74 +| `Esc` | Back to apps list |
75 +
76 +### Model picker
77 +
78 +| Key | Action |
79 +|-----------|---------------------------|
80 +| `↑` `↓` | Navigate models |
81 +| `Enter` | Assign selected model |
82 +| `Esc` | Cancel |
83
46 -## Other Installation Methods
84 +## Other installation methods
85
86 ### Cargo
87
@@ -60,32 +98,34 @@ cargo build --release
98 ./target/release/onde
99 ```
100
63 -## Platform Support
101 +## Platform support
102
65 -Pre-built native binaries are shipped for:
103 +Pre-built native binaries ship for every major platform:
104
67 -| Platform | Architecture |
68 -|---------------|---------------|
69 -| macOS | arm64, x64 |
70 -| Linux (glibc) | arm64, x64 |
71 -| Windows | arm64, x64 |
105 +| Platform | Architecture |
106 +|---------------|--------------|
107 +| macOS | arm64, x64 |
108 +| Linux (glibc) | arm64, x64 |
109 +| Windows | arm64, x64 |
110
73 -## Debug
111 +## Debug logs
112
75 -Logs are written to `~/.cache/onde/debug.log`. Nothing is written to the terminal output.
113 +Logs are written to `~/.cache/onde/debug.log`. Nothing touches the terminal output — ratatui owns the screen exclusively while the TUI is open.
114
115 ## Related
116
79 -- [Swift SDK](https://github.com/ondeinference/onde-swift)
80 -- [Flutter SDK](https://pub.dev/packages/onde_inference)
81 -- [React Native SDK](https://www.npmjs.com/package/@ondeinference/react-native)
82 -- [Onde Rust crate](https://crates.io/crates/onde)
117 +| SDK | Install |
118 +|-----|---------|
119 +| [Swift SDK](https://github.com/ondeinference/onde-swift) | Swift Package Manager |
120 +| [Flutter SDK](https://pub.dev/packages/onde_inference) | `flutter pub add onde_inference` |
121 +| [React Native SDK](https://www.npmjs.com/package/@ondeinference/react-native) | `npm i @ondeinference/react-native` |
122 +| [Rust crate](https://crates.io/crates/onde) | `cargo add onde` |
123
84 -## Source & Issues
124 +## Source & issues
125
86 -This is a native binary distributed via PyPI. The source code lives at
87 -[github.com/ondeinference/onde-cli](https://github.com/ondeinference/onde-cli).
88 -Please report bugs and feature requests there.
126 +This package ships a pre-built native binary. Source lives at
127 +[github.com/ondeinference/onde-cli](https://github.com/ondeinference/onde-cli) —
128 +file bugs and feature requests there.
129
130 ## License
131
@@ -97,5 +137,5 @@ Dual-licensed under **MIT** and **Apache 2.0**.
137 ---
138
139 <p align="center">
100 - <sub>© 2026 <a href="https://ondeinference.com">Onde Inference</a></sub>
140 + <sub>Built by <a href="https://ondeinference.com">Onde Inference</a> · © 2026</sub>
141 </p>
\ No newline at end of file
src/app.rs
+71 -26
@@ -17,8 +17,14 @@ use {
17 pub use smbcloud_gresiq_sdk::{OndeApp, OndeModel};
18
19 // Onde is a tenant in smbCloud Auth. These identify this CLI to the backend.
20 -pub(crate) const ONDE_APP_ID: &str = "e1098adf-9859-43bf-ae98-514cf66252a5";
21 -pub(crate) const ONDE_APP_SECRET: &str = "5bc60b32-c072-40ce-a91f-b289fdee075d";
20 +// smbCloud Auth credentials — baked in at compile time from the environment.
21 +// Set these in .env for local builds; inject as secrets in CI.
22 +pub(crate) const ONDE_APP_ID: &str = env!("ONDE_APP_ID");
23 +pub(crate) const ONDE_APP_SECRET: &str = env!("ONDE_APP_SECRET");
24 +
25 +// GresIQ API credentials for the Onde tenant — distinct from Auth above.
26 +pub(crate) const GRESIQ_API_KEY: &str = env!("GRESIQ_API_KEY");
27 +pub(crate) const GRESIQ_API_SECRET: &str = env!("GRESIQ_API_SECRET");
28
29 fn credentials() -> ClientCredentials<'static> {
30 ClientCredentials {
@@ -62,13 +68,22 @@ pub struct Status {
68
69 impl Status {
70 pub fn neutral(msg: impl Into<String>) -> Self {
65 - Self { tone: StatusTone::Neutral, message: msg.into() }
71 + Self {
72 + tone: StatusTone::Neutral,
73 + message: msg.into(),
74 + }
75 }
76 pub fn success(msg: impl Into<String>) -> Self {
68 - Self { tone: StatusTone::Success, message: msg.into() }
77 + Self {
78 + tone: StatusTone::Success,
79 + message: msg.into(),
80 + }
81 }
82 pub fn error(msg: impl Into<String>) -> Self {
71 - Self { tone: StatusTone::Error, message: msg.into() }
83 + Self {
84 + tone: StatusTone::Error,
85 + message: msg.into(),
86 + }
87 }
88 }
89
@@ -238,7 +253,10 @@ impl App {
253 self.busy = false;
254 self.status = Status::error(msg);
255 }
241 - AuthEvent::AppRenamedOk { app_index, new_name } => {
256 + AuthEvent::AppRenamedOk {
257 + app_index,
258 + new_name,
259 + } => {
260 self.busy = false;
261 self.renaming_app = false;
262 self.rename_input.clear();
@@ -262,12 +280,25 @@ impl App {
280 self.busy = false;
281 self.status = Status::error(msg);
282 }
265 - AuthEvent::ModelAssigned { app_index, model_id } => {
283 + AuthEvent::ModelAssigned {
284 + app_index,
285 + model_id,
286 + } => {
287 self.busy = false;
288 self.screen = Screen::AppDetail;
289 self.assigning_for_app_index = None;
290 if let Some(onde_app) = self.apps.get_mut(app_index) {
291 + // Resolve the display name from the loaded models list so
292 + // the apps list and detail view update immediately without
293 + // a round-trip. Falls back to the raw ID if not found.
294 + let resolved_name = self
295 + .models
296 + .iter()
297 + .find(|m| m.id == model_id)
298 + .and_then(|m| m.name.clone())
299 + .unwrap_or_else(|| model_id.clone());
300 onde_app.current_model_id = Some(model_id);
301 + onde_app.active_model = Some(resolved_name);
302 }
303 self.status = Status::success("Model assigned.");
304 }
@@ -296,9 +327,7 @@ pub async fn run(terminal: &mut DefaultTerminal) -> Result<()> {
327 }
328 Err(_) => {
329 token::clear();
299 - let _ = tx2.send(AuthEvent::Failed(
300 - "Session expired. Sign in again.".into(),
301 - ));
330 + let _ = tx2.send(AuthEvent::Failed("Session expired. Sign in again.".into()));
331 }
332 }
333 });
@@ -370,7 +399,10 @@ fn handle_key(
399
400 // while a request is in flight only Ctrl+C is allowed
401 if app.busy {
373 - if matches!((key.code, key.modifiers), (Char('c'), KeyModifiers::CONTROL)) {
402 + if matches!(
403 + (key.code, key.modifiers),
404 + (Char('c'), KeyModifiers::CONTROL)
405 + ) {
406 app.should_quit = true;
407 }
408 return;
@@ -411,8 +443,12 @@ fn handle_key_auth(
443 submit(app, tx);
444 }
445 (Backspace, _) => match app.focus {
414 - Focus::Email => { app.email.pop(); }
415 - Focus::Password => { app.password.pop(); }
446 + Focus::Email => {
447 + app.email.pop();
448 + }
449 + Focus::Password => {
450 + app.password.pop();
451 + }
452 },
453 (Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => match app.focus {
454 Focus::Email => app.email.push(c),
@@ -467,10 +503,14 @@ fn handle_key_apps(
503 app.screen = Screen::AppDetail;
504 app.renaming_app = false;
505 app.rename_input.clear();
470 - let app_name = app.apps.get(app.apps_cursor)
506 + let app_name = app
507 + .apps
508 + .get(app.apps_cursor)
509 .map(|a| a.name.as_str())
510 .unwrap_or("app");
473 - app.status = Status::neutral(format!("{app_name} — m · model r · rename Esc · back"));
511 + app.status = Status::neutral(format!(
512 + "{app_name} — m · model r · rename Esc · back"
513 + ));
514 }
515 }
516 (Char('n'), KeyModifiers::NONE) => {
@@ -526,7 +566,9 @@ fn handle_key_app_detail(
566 open_model_picker(app, tx);
567 }
568 (Char('r'), KeyModifiers::NONE) => {
529 - let current_name = app.apps.get(app.apps_cursor)
569 + let current_name = app
570 + .apps
571 + .get(app.apps_cursor)
572 .map(|a| a.name.clone())
573 .unwrap_or_default();
574 app.rename_input = current_name;
@@ -561,7 +603,10 @@ fn submit_rename_app(app: &mut App, tx: mpsc::UnboundedSender<AuthEvent>) {
603 tokio::spawn(async move {
604 match crate::gresiq::rename_app(&token, &onde_app_id, &name).await {
605 Ok(_) => {
564 - let _ = tx.send(AuthEvent::AppRenamedOk { app_index, new_name: name });
606 + let _ = tx.send(AuthEvent::AppRenamedOk {
607 + app_index,
608 + new_name: name,
609 + });
610 }
611 Err(e) => {
612 let _ = tx.send(AuthEvent::AppRenameFailed(e.to_string()));
@@ -688,7 +733,10 @@ fn submit_assign_model(app: &mut App, tx: mpsc::UnboundedSender<AuthEvent>) {
733 tokio::spawn(async move {
734 match crate::gresiq::assign_model(&token, &onde_app_id, &model_id).await {
735 Ok(()) => {
691 - let _ = tx.send(AuthEvent::ModelAssigned { app_index, model_id });
736 + let _ = tx.send(AuthEvent::ModelAssigned {
737 + app_index,
738 + model_id,
739 + });
740 }
741 Err(e) => {
742 let _ = tx.send(AuthEvent::ModelAssignFailed(e.to_string()));
@@ -717,13 +765,8 @@ fn submit(app: &mut App, tx: mpsc::UnboundedSender<AuthEvent>) {
765 tokio::spawn(async move {
766 let event = match mode {
767 Mode::Signup => {
720 - match signup_with_client(
721 - Environment::Production,
722 - credentials(),
723 - email,
724 - password,
725 - )
726 - .await
768 + match signup_with_client(Environment::Production, credentials(), email, password)
769 + .await
770 {
771 Ok(result) => AuthEvent::SignupOk(result.message),
772 Err(e) => AuthEvent::Failed(extract_error(&e)),
@@ -752,7 +795,9 @@ fn submit(app: &mut App, tx: mpsc::UnboundedSender<AuthEvent>) {
795 Ok(user) => user.email,
796 Err(_) => email,
797 };
755 - AuthEvent::SigninOk(Profile { email: profile_email })
798 + AuthEvent::SigninOk(Profile {
799 + email: profile_email,
800 + })
801 }
802 Ok(AccountStatus::Incomplete { .. }) => AuthEvent::Failed(
803 "Check your email first — we sent a confirmation link when you signed up."
src/gresiq.rs
+21 -27
@@ -2,22 +2,25 @@
2 //! need to pass the user's access token.
3 use smbcloud_gresiq_sdk::{Environment, OndeApp, OndeModel};
4
5 +fn api_key() -> &'static str {
6 + crate::app::GRESIQ_API_KEY
7 +}
8 +
9 +fn api_secret() -> &'static str {
10 + crate::app::GRESIQ_API_SECRET
11 +}
12 +
13 pub async fn load_apps(token: &str) -> anyhow::Result<Vec<OndeApp>> {
6 - smbcloud_gresiq_sdk::list_apps(
7 - &Environment::Production,
8 - crate::app::ONDE_APP_ID,
9 - crate::app::ONDE_APP_SECRET,
10 - token,
11 - )
12 - .await
13 - .map_err(|e| anyhow::anyhow!("{e}"))
14 + smbcloud_gresiq_sdk::list_apps(&Environment::Production, api_key(), api_secret(), token)
15 + .await
16 + .map_err(|e| anyhow::anyhow!("{e}"))
17 }
18
19 pub async fn create_app(token: &str, name: &str) -> anyhow::Result<OndeApp> {
20 smbcloud_gresiq_sdk::create_app(
21 &Environment::Production,
19 - crate::app::ONDE_APP_ID,
20 - crate::app::ONDE_APP_SECRET,
22 + api_key(),
23 + api_secret(),
24 token,
25 name,
26 )
@@ -26,25 +29,16 @@ pub async fn create_app(token: &str, name: &str) -> anyhow::Result<OndeApp> {
29 }
30
31 pub async fn load_models(token: &str) -> anyhow::Result<Vec<OndeModel>> {
29 - smbcloud_gresiq_sdk::list_models(
30 - &Environment::Production,
31 - crate::app::ONDE_APP_ID,
32 - crate::app::ONDE_APP_SECRET,
33 - token,
34 - )
35 - .await
36 - .map_err(|e| anyhow::anyhow!("{e}"))
32 + smbcloud_gresiq_sdk::list_models(&Environment::Production, api_key(), api_secret(), token)
33 + .await
34 + .map_err(|e| anyhow::anyhow!("{e}"))
35 }
36
39 -pub async fn assign_model(
40 - token: &str,
41 - onde_app_id: &str,
42 - model_id: &str,
43 -) -> anyhow::Result<()> {
37 +pub async fn assign_model(token: &str, onde_app_id: &str, model_id: &str) -> anyhow::Result<()> {
38 smbcloud_gresiq_sdk::assign_model(
39 &Environment::Production,
46 - crate::app::ONDE_APP_ID,
47 - crate::app::ONDE_APP_SECRET,
40 + api_key(),
41 + api_secret(),
42 token,
43 onde_app_id,
44 model_id,
@@ -56,8 +50,8 @@ pub async fn assign_model(
50 pub async fn rename_app(token: &str, onde_app_id: &str, new_name: &str) -> anyhow::Result<OndeApp> {
51 smbcloud_gresiq_sdk::rename_app(
52 &Environment::Production,
59 - crate::app::ONDE_APP_ID,
60 - crate::app::ONDE_APP_SECRET,
53 + api_key(),
54 + api_secret(),
55 token,
56 onde_app_id,
57 new_name,
src/ui.rs
+51 -19
@@ -134,14 +134,28 @@ fn render_form(frame: &mut Frame, app: &App, area: Rect) {
134 Paragraph::new("Email").style(Style::new().fg(C_MUTED)),
135 rows[7],
136 );
137 - render_input(frame, app, &app.email, Focus::Email, "name@company.com", rows[8]);
137 + render_input(
138 + frame,
139 + app,
140 + &app.email,
141 + Focus::Email,
142 + "name@company.com",
143 + rows[8],
144 + );
145
146 frame.render_widget(
147 Paragraph::new("Password").style(Style::new().fg(C_MUTED)),
148 rows[10],
149 );
150 let masked = "•".repeat(app.password.len());
144 - render_input(frame, app, &masked, Focus::Password, "Minimum 8 characters", rows[11]);
151 + render_input(
152 + frame,
153 + app,
154 + &masked,
155 + Focus::Password,
156 + "Minimum 8 characters",
157 + rows[11],
158 + );
159
160 let (primary_label, secondary_label) = match app.mode {
161 Mode::Signup => ("[ Create account ]", "I already have an account Ctrl+L"),
@@ -174,11 +188,19 @@ fn render_tabs(frame: &mut Frame, app: &App, area: Rect) {
188 let inactive = Style::new().fg(C_MUTED).bg(C_SURFACE_STRONG);
189
190 frame.render_widget(
177 - Paragraph::new(" Create account ").style(if app.mode == Mode::Signup { active } else { inactive }),
191 + Paragraph::new(" Create account ").style(if app.mode == Mode::Signup {
192 + active
193 + } else {
194 + inactive
195 + }),
196 cols[0],
197 );
198 frame.render_widget(
181 - Paragraph::new(" Sign in ").style(if app.mode == Mode::Signin { active } else { inactive }),
199 + Paragraph::new(" Sign in ").style(if app.mode == Mode::Signin {
200 + active
201 + } else {
202 + inactive
203 + }),
204 cols[2],
205 );
206 }
@@ -235,8 +257,8 @@ fn render_create_input(frame: &mut Frame, value: &str, area: Rect) {
257 frame.render_widget(block, area);
258 frame.render_widget(Paragraph::new(value).style(Style::new().fg(C_TEXT)), inner);
259
238 - let cursor_x = (inner.x + value.chars().count() as u16)
239 - .min(inner.x + inner.width.saturating_sub(1));
260 + let cursor_x =
261 + (inner.x + value.chars().count() as u16).min(inner.x + inner.width.saturating_sub(1));
262 frame.set_cursor_position((cursor_x, inner.y));
263 }
264
@@ -284,8 +306,7 @@ fn render_apps(frame: &mut Frame, app: &App, area: Rect) {
306 render_status(frame, app, top[2]);
307
308 frame.render_widget(
287 - Paragraph::new(" Name Status Model")
288 - .style(Style::new().fg(C_MUTED)),
309 + Paragraph::new(" Name Status Model").style(Style::new().fg(C_MUTED)),
310 top[4],
311 );
312
@@ -314,8 +335,7 @@ fn render_apps(frame: &mut Frame, app: &App, area: Rect) {
335 );
336 render_create_input(frame, &app.new_app_name, bottom[3]);
337 frame.render_widget(
317 - Paragraph::new("Enter · create Esc · cancel")
318 - .style(Style::new().fg(C_MUTED)),
338 + Paragraph::new("Enter · create Esc · cancel").style(Style::new().fg(C_MUTED)),
339 bottom[4],
340 );
341 } else {
@@ -327,8 +347,7 @@ fn render_apps(frame: &mut Frame, app: &App, area: Rect) {
347
348 render_apps_list(frame, app, bottom[0]);
349 frame.render_widget(
330 - Paragraph::new("n · new Enter · open s · sign out")
331 - .style(Style::new().fg(C_MUTED)),
350 + Paragraph::new("n · new Enter · open s · sign out").style(Style::new().fg(C_MUTED)),
351 bottom[1],
352 );
353 }
@@ -387,12 +406,20 @@ fn render_apps_list(frame: &mut Frame, app: &App, area: Rect) {
406 // Falls back to "–" if the app has no model assigned or the model isn’t in
407 // the loaded list yet (models load lazily on first visit to the picker).
408 fn resolve_model_name<'a>(app: &'a App, onde_app: &'a OndeApp) -> &'a str {
390 - onde_app
409 + // Prefer the name the API returned directly on the app object.
410 + if let Some(name) = onde_app.active_model.as_deref() {
411 + return name;
412 + }
413 + // Fall back to a lookup in the lazily-loaded models list.
414 + if let Some(name) = onde_app
415 .current_model_id
416 .as_deref()
417 .and_then(|id| app.models.iter().find(|m| m.id == id))
418 .and_then(|m| m.name.as_deref())
395 - .unwrap_or("–")
419 + {
420 + return name;
421 + }
422 + "No model assigned yet"
423 }
424
425 // app detail screen
@@ -441,8 +468,7 @@ fn render_app_detail(frame: &mut Frame, app: &App, area: Rect) {
468 );
469 render_rename_input(frame, &app.rename_input, rows[14]);
470 frame.render_widget(
444 - Paragraph::new("Enter · save Esc · cancel")
445 - .style(Style::new().fg(C_MUTED)),
471 + Paragraph::new("Enter · save Esc · cancel").style(Style::new().fg(C_MUTED)),
472 rows[15],
473 );
474 } else {
@@ -487,7 +513,13 @@ fn render_app_detail_header(frame: &mut Frame, onde_app: &OndeApp, status_str: &
513 frame.render_widget(Paragraph::new(line), area);
514 }
515
490 -fn render_detail_field(frame: &mut Frame, label: &str, value: &str, label_area: Rect, value_area: Rect) {
516 +fn render_detail_field(
517 + frame: &mut Frame,
518 + label: &str,
519 + value: &str,
520 + label_area: Rect,
521 + value_area: Rect,
522 +) {
523 frame.render_widget(
524 Paragraph::new(label).style(Style::new().fg(C_MUTED)),
525 label_area,
@@ -509,8 +541,8 @@ fn render_rename_input(frame: &mut Frame, value: &str, area: Rect) {
541 frame.render_widget(block, area);
542 frame.render_widget(Paragraph::new(value).style(Style::new().fg(C_TEXT)), inner);
543
512 - let cursor_x = (inner.x + value.chars().count() as u16)
513 - .min(inner.x + inner.width.saturating_sub(1));
544 + let cursor_x =
545 + (inner.x + value.chars().count() as u16).min(inner.x + inner.width.saturating_sub(1));
546 frame.set_cursor_position((cursor_x, inner.y));
547 }
548