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