Refactor model picker logic into platform-independent module
Move ModelSource, ModelPickerItem, and build_model_picker_items to src/models.rs for use on all platforms. Update imports and references accordingly. Update installation instructions and VSCode integration examples in README files.
paydii committed
Apr 25, 2026 at 21:24 UTC
31afcdb0dbc09417f42802542e2e917579ff37ce
6 files changed
+167
-118
README.md
+19
-1
@@ -2,7 +2,7 @@
2
3
[](https://crates.io/crates/sigit)
4
5
-A coding agent for [smbCloud](https://smbcloud.xyz/) that runs entirely on your machine. No API keys. No cloud round-trips. The model lives in your local HuggingFace cache.
5
+A coding agent for [smbCloud](https://smbcloud.xyz/) that runs entirely on your machine. No API keys. No cloud round-trips.
6
7
siGit is meant to be a general coding agent, but it is especially good in smbCloud codebases. It already knows the rough shape of the platform: Rust workspaces with focused crates, Rails services, deploy flows, auth boundaries, and platform-managed services like GresIQ. In smbCloud repos, that means it can usually give more grounded answers with less back-and-forth.
8
@@ -30,6 +30,8 @@ Outside smbCloud, it should still behave like a normal coding agent and not forc
30
31
## Install
32
33
+Install siGit Code with cargo, Homebrew, PyPi, or NPM:
34
+
35
```sh
36
cargo install sigit
37
```
@@ -57,6 +59,22 @@ Add to `~/.config/zed/settings.json`:
59
60
Use the full absolute path. `~` will not be expanded here.
61
62
+## VSCode via ACP Client extension
63
+
64
+Install [ACP client](https://marketplace.visualstudio.com/items?itemName=formulahendry.acp-client):
65
+
66
+```json
67
+{
68
+ "acp.agents": {
69
+ "siGit Code": {
70
+ "command": "sigit",
71
+ "args": [],
72
+ "env": {}
73
+ }
74
+ }
75
+}
76
+```
77
+
78
## Terminal mode
79
80
If you run `sigit` directly in a terminal, it opens an interactive chat UI. It uses the same model and system prompt as the editor integration, so it is useful for quick questions when you do not want to open Zed first.
npm/sigit/README.md
+4
-3
@@ -24,8 +24,9 @@ The right binary for your platform gets pulled in automatically. Works on macOS
24
25
| Method | Command |
26
|---|---|
27
-| **Homebrew** | `brew install getsigit/homebrew-tap/sigit` |
28
-| **pip** | `pip install sigit` |
27
+| **Homebrew** | `brew tap getsigit/tap && brew install sigit` |
28
+| **pip** | `pip install sigit-code` |
29
+| **uv** | `uvx --from sigit-code sigit` |
30
| **Cargo** | `cargo install sigit` |
31
32
---
@@ -82,4 +83,4 @@ Add siGit as an agent in Zed by adding this to your settings:
83
84
## Copyright
85
85
-2026 smbCloud (Splitfire AB).
\ No newline at end of file
86
+2026 smbCloud (Splitfire AB).
pypi/README.md
+9
-11
@@ -23,8 +23,11 @@
23
24
## Install
25
26
+Use `pip` or `uv`:
27
+
28
```sh
29
pip install sigit-code
30
+uvx --from sigit-code sigit
31
```
32
33
Installs the native `sigit` binary for your platform — no compiler, no Node.js, no runtime dependencies.
@@ -45,15 +48,10 @@ siGit works as an [ACP-compatible](https://github.com/nicobailon/agent-client-pr
48
49
```json
50
{
48
- "agent": {
49
- "profiles": {
50
- "sigit": {
51
- "provider": "acp",
52
- "binary": {
53
- "path": "sigit",
54
- "args": ["--acp"]
55
- }
56
- }
51
+ "agent_servers": {
52
+ "siGit Code": {
53
+ "type": "custom",
54
+ "command": "/absolute/path/to/sigit"
55
}
56
}
57
}
@@ -66,7 +64,7 @@ Then select **sigit** as your agent profile in the Zed assistant panel.
64
| Method | Command |
65
|--------|---------|
66
| npm | `npm install -g @smbcloud/sigit` |
69
-| Homebrew | `brew install getsigit/sigit/sigit` |
67
+| Homebrew | `brew tap getsigit/tap && brew install sigit` |
68
| Cargo | `cargo install sigit` |
69
70
### From source
@@ -104,4 +102,4 @@ Licensed under **Apache 2.0**.
102
103
## Copyright
104
107
-2026 smbCloud (Splitfire AB).
\ No newline at end of file
105
+2026 smbCloud (Splitfire AB).
src/chat.rs
+5
-96
@@ -17,11 +17,9 @@ use std::sync::mpsc as std_mpsc;
17
use anyhow::Result;
18
use crossterm::event::{Event, EventStream, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
19
use futures::StreamExt;
20
-use onde::inference::{
21
- ChatEngine, GgufModelConfig, SamplingConfig, StreamChunk, ToolDefinition, ToolResult,
22
-};
20
+use onde::inference::{ChatEngine, SamplingConfig, StreamChunk, ToolDefinition, ToolResult};
21
24
-use crate::setup::{DiscoveredModel, ModelCacheHealth};
22
+use crate::models::{ModelCacheHealth, ModelPickerItem, ModelSource, build_model_picker_items};
23
use ratatui::{
24
Frame,
25
layout::{Constraint, Layout, Position},
@@ -379,98 +377,9 @@ fn wrapped_line_count(text: &str, role: Role, width: usize) -> u16 {
377
378
// ── Model table ──────────────────────────────────────────────────────────────
379
382
-#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
383
-enum ModelSource {
384
- Onde,
385
- HuggingFace,
386
- Fallback,
387
-}
388
-
389
-#[derive(Clone)]
390
-pub(crate) struct ModelPickerItem {
391
- pub(crate) display_name: String,
392
- pub(crate) description: String,
393
- pub(crate) tool_calling: bool,
394
- pub(crate) max_tokens: u64,
395
- pub(crate) config: GgufModelConfig,
396
- pub(crate) source_label: String,
397
- brand_mark: &'static str,
398
- source: ModelSource,
399
- pub(crate) cache_health: ModelCacheHealth,
400
-}
401
-
402
-pub(crate) fn build_model_picker_items() -> Vec<ModelPickerItem> {
403
- let mut items = Vec::new();
404
-
405
- for discovered in crate::setup::discover_local_models() {
406
- if let Some(item) = discovered_model_to_picker_item(discovered) {
407
- items.push(item);
408
- }
409
- }
410
-
411
- if items.is_empty() {
412
- let config = GgufModelConfig::platform_default();
413
- let tool_calling = config.display_name == "Qwen 3 4B (Q4_K_M)";
414
- let max_tokens = if tool_calling { 4096 } else { 512 };
415
-
416
- items.push(ModelPickerItem {
417
- display_name: config.display_name.clone(),
418
- description: config.approx_memory.clone(),
419
- tool_calling,
420
- max_tokens,
421
- config,
422
- source_label: "Platform default".to_string(),
423
- brand_mark: "◎",
424
- source: ModelSource::Fallback,
425
- cache_health: ModelCacheHealth::Complete,
426
- });
427
- }
428
-
429
- items.sort_by(|left, right| {
430
- left.source
431
- .cmp(&right.source)
432
- .then_with(|| left.display_name.cmp(&right.display_name))
433
- });
434
- items
435
-}
436
-
437
-fn discovered_model_to_picker_item(model: DiscoveredModel) -> Option<ModelPickerItem> {
438
- let source_label = if model.from_app_group {
439
- "Onde".to_string()
440
- } else {
441
- "HuggingFace".to_string()
442
- };
443
-
444
- let config = match model.model_id.as_str() {
445
- "bartowski/Qwen_Qwen3-4B-GGUF" => GgufModelConfig::qwen3_4b(),
446
- "bartowski/Qwen_Qwen3-8B-GGUF" => GgufModelConfig::qwen3_8b(),
447
- "bartowski/Qwen2.5-3B-Instruct-GGUF" => GgufModelConfig::qwen25_3b(),
448
- "bartowski/Qwen2.5-1.5B-Instruct-GGUF" => GgufModelConfig::qwen25_1_5b(),
449
- "bartowski/Qwen2.5-Coder-3B-Instruct-GGUF" => GgufModelConfig::qwen25_coder_3b(),
450
- "bartowski/Qwen2.5-Coder-1.5B-Instruct-GGUF" => GgufModelConfig::qwen25_coder_1_5b(),
451
- _ => return None,
452
- };
453
-
454
- let tool_calling = model.model_id == "bartowski/Qwen_Qwen3-4B-GGUF"
455
- || model.model_id == "bartowski/Qwen_Qwen3-8B-GGUF";
456
- let max_tokens = if tool_calling { 4096 } else { 512 };
457
-
458
- Some(ModelPickerItem {
459
- display_name: config.display_name.clone(),
460
- description: config.approx_memory.clone(),
461
- tool_calling,
462
- max_tokens,
463
- config,
464
- source_label,
465
- brand_mark: if model.from_app_group { "◉" } else { "○" },
466
- source: if model.from_app_group {
467
- ModelSource::Onde
468
- } else {
469
- ModelSource::HuggingFace
470
- },
471
- cache_health: model.cache_health,
472
- })
473
-}
380
+// ModelSource, ModelPickerItem, and build_model_picker_items live in
381
+// crate::models so they are available on all platforms (including Windows),
382
+// not just unix where this chat module is compiled.
383
384
fn render_model_picker(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) {
385
let popup = centered_rect(82, 72, area);
src/main.rs
+8
-7
@@ -42,6 +42,7 @@
42
43
#[cfg(unix)]
44
mod chat;
45
+mod models;
46
mod setup;
47
mod tools;
48
@@ -351,7 +352,7 @@ impl SiGitAgent {
352
agent_client_protocol::Error::new(-32603, format!("model switch failed: {error}"))
353
})?;
354
354
- if let Some(item) = chat::build_model_picker_items()
355
+ if let Some(item) = models::build_model_picker_items()
356
.iter()
357
.find(|item| item.config.model_id == new_config.model_id)
358
&& let Err(err) = setup::save_selected_model(&setup::SelectedModel {
@@ -388,7 +389,7 @@ const MODEL_CONFIG_ID: &str = "sigit-model";
389
390
/// Build the `SessionConfigOption` list for model selection.
391
fn build_model_config_options(current_model: &GgufModelConfig) -> Vec<SessionConfigOption> {
391
- let items = chat::build_model_picker_items();
392
+ let items = models::build_model_picker_items();
393
394
let options: Vec<SessionConfigSelectOption> = items
395
.iter()
@@ -428,7 +429,7 @@ fn build_model_config_options(current_model: &GgufModelConfig) -> Vec<SessionCon
429
430
/// Look up the GgufModelConfig for a given model_id value from the picker items.
431
fn resolve_model_config(model_id: &str) -> Option<(GgufModelConfig, u64)> {
431
- let items = chat::build_model_picker_items();
432
+ let items = models::build_model_picker_items();
433
items
434
.into_iter()
435
.find(|item| {
@@ -467,7 +468,7 @@ fn parse_slash(input: &str) -> Option<SlashCommand> {
468
}
469
470
fn format_models_list(current_model: &GgufModelConfig) -> String {
470
- let items = chat::build_model_picker_items();
471
+ let items = models::build_model_picker_items();
472
if items.is_empty() {
473
return "No local models found. siGit will use the platform default model.".to_string();
474
}
@@ -576,7 +577,7 @@ async fn exec_slash_acp(
577
.await;
578
}
579
SlashCommand::Models(Some(number)) => {
579
- let items = chat::build_model_picker_items();
580
+ let items = models::build_model_picker_items();
581
let index = number.saturating_sub(1);
582
match items.get(index).cloned() {
583
None => {
@@ -1201,7 +1202,7 @@ async fn run_interactive(tty: std::fs::File, mut cleanup_tty: std::fs::File) ->
1202
let config = startup_selection
1203
.as_ref()
1204
.and_then(|selection| {
1204
- chat::build_model_picker_items()
1205
+ models::build_model_picker_items()
1206
.into_iter()
1207
.find(|item| {
1208
selection
@@ -1283,7 +1284,7 @@ async fn run_acp_server() -> anyhow::Result<()> {
1284
let config = startup_selection
1285
.as_ref()
1286
.and_then(|selection| {
1286
- chat::build_model_picker_items()
1287
+ models::build_model_picker_items()
1288
.into_iter()
1289
.find(|item| {
1290
selection
src/models.rs
new
+122
@@ -0,0 +1,122 @@
1
+//! Platform-independent model picker types and item construction.
2
+//!
3
+//! This module is available on all target platforms (Windows, macOS, Linux).
4
+//! The TUI rendering code in `chat.rs` (unix-only) re-uses these types
5
+//! rather than defining them inline.
6
+
7
+use onde::inference::GgufModelConfig;
8
+
9
+use crate::setup::DiscoveredModel;
10
+
11
+pub(crate) use crate::setup::ModelCacheHealth;
12
+
13
+// ── Types ─────────────────────────────────────────────────────────────────────
14
+
15
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
16
+pub(crate) enum ModelSource {
17
+ Onde,
18
+ HuggingFace,
19
+ Fallback,
20
+}
21
+
22
+#[derive(Clone)]
23
+pub(crate) struct ModelPickerItem {
24
+ pub(crate) display_name: String,
25
+ pub(crate) description: String,
26
+ pub(crate) tool_calling: bool,
27
+ pub(crate) max_tokens: u64,
28
+ pub(crate) config: GgufModelConfig,
29
+ pub(crate) source_label: String,
30
+ pub(crate) brand_mark: &'static str,
31
+ pub(crate) source: ModelSource,
32
+ pub(crate) cache_health: ModelCacheHealth,
33
+}
34
+
35
+// ── Builder ───────────────────────────────────────────────────────────────────
36
+
37
+/// Build the full list of available model picker items from the local cache.
38
+///
39
+/// Items are sourced from:
40
+/// 1. The Onde app-group model cache (macOS shared container).
41
+/// 2. The HuggingFace hub cache (`HF_HUB_CACHE` / `HF_HOME` / `~/.cache/huggingface/hub`).
42
+///
43
+/// If no models are discovered at all, a single fallback entry for the
44
+/// platform-default model is returned so the picker is never empty.
45
+///
46
+/// Items are sorted by source priority (Onde first, then HuggingFace, then
47
+/// Fallback) and then alphabetically by display name within each group.
48
+pub(crate) fn build_model_picker_items() -> Vec<ModelPickerItem> {
49
+ let mut items = Vec::new();
50
+
51
+ for discovered in crate::setup::discover_local_models() {
52
+ if let Some(item) = discovered_model_to_picker_item(discovered) {
53
+ items.push(item);
54
+ }
55
+ }
56
+
57
+ if items.is_empty() {
58
+ let config = GgufModelConfig::platform_default();
59
+ let tool_calling = config.display_name == "Qwen 3 4B (Q4_K_M)";
60
+ let max_tokens = if tool_calling { 4096 } else { 512 };
61
+
62
+ items.push(ModelPickerItem {
63
+ display_name: config.display_name.clone(),
64
+ description: config.approx_memory.clone(),
65
+ tool_calling,
66
+ max_tokens,
67
+ config,
68
+ source_label: "Platform default".to_string(),
69
+ brand_mark: "◎",
70
+ source: ModelSource::Fallback,
71
+ cache_health: ModelCacheHealth::Complete,
72
+ });
73
+ }
74
+
75
+ items.sort_by(|left, right| {
76
+ left.source
77
+ .cmp(&right.source)
78
+ .then_with(|| left.display_name.cmp(&right.display_name))
79
+ });
80
+
81
+ items
82
+}
83
+
84
+// ── Internal helpers ──────────────────────────────────────────────────────────
85
+
86
+fn discovered_model_to_picker_item(model: DiscoveredModel) -> Option<ModelPickerItem> {
87
+ let source_label = if model.from_app_group {
88
+ "Onde".to_string()
89
+ } else {
90
+ "HuggingFace".to_string()
91
+ };
92
+
93
+ let config = match model.model_id.as_str() {
94
+ "bartowski/Qwen_Qwen3-4B-GGUF" => GgufModelConfig::qwen3_4b(),
95
+ "bartowski/Qwen_Qwen3-8B-GGUF" => GgufModelConfig::qwen3_8b(),
96
+ "bartowski/Qwen2.5-3B-Instruct-GGUF" => GgufModelConfig::qwen25_3b(),
97
+ "bartowski/Qwen2.5-1.5B-Instruct-GGUF" => GgufModelConfig::qwen25_1_5b(),
98
+ "bartowski/Qwen2.5-Coder-3B-Instruct-GGUF" => GgufModelConfig::qwen25_coder_3b(),
99
+ "bartowski/Qwen2.5-Coder-1.5B-Instruct-GGUF" => GgufModelConfig::qwen25_coder_1_5b(),
100
+ _ => return None,
101
+ };
102
+
103
+ let tool_calling = model.model_id == "bartowski/Qwen_Qwen3-4B-GGUF"
104
+ || model.model_id == "bartowski/Qwen_Qwen3-8B-GGUF";
105
+ let max_tokens = if tool_calling { 4096 } else { 512 };
106
+
107
+ Some(ModelPickerItem {
108
+ display_name: config.display_name.clone(),
109
+ description: config.approx_memory.clone(),
110
+ tool_calling,
111
+ max_tokens,
112
+ config,
113
+ source_label,
114
+ brand_mark: if model.from_app_group { "◉" } else { "○" },
115
+ source: if model.from_app_group {
116
+ ModelSource::Onde
117
+ } else {
118
+ ModelSource::HuggingFace
119
+ },
120
+ cache_health: model.cache_health,
121
+ })
122
+}