@sigit / siti / commits / 2c1796f

Fix windows microsoft store

paydii committed Aug 7, 2026 at 20:44 UTC 2c1796f01ab781b2faaa2b427dd493aef803542e
11 files changed +315 -69
.github/workflows/release-windows-microsoft-store.yml
+42 -11
@@ -6,21 +6,22 @@ on:
6 - release/windows-microsoft-store
7 workflow_dispatch:
8
9 -# Push to the release branch above to build and draft-release the Windows
10 -# Microsoft Store artifacts. The .msi/.exe are for direct download; the MSIX is
11 -# what gets submitted to Partner Center (the Store re-signs it), which is the
12 -# only form that carries the package identity Store services need.
9 +# Push to the release branch above to build, sign, and draft-release the
10 +# Windows Microsoft Store artifacts. The signed .msi/.exe are for direct
11 +# download; the MSIX is what gets submitted to Partner Center (the Store
12 +# re-signs it), which is the only form that carries the package identity Store
13 +# services need.
14 jobs:
15 publish-tauri:
16 permissions:
17 contents: write
18 runs-on: windows-latest
18 - # Job-level env so the MSIX steps' own `if:` conditions can read these — a
19 - # step-level env is not in scope for that same step's `if`. When the secrets
20 - # are absent the MSIX steps skip and the unsigned .msi/.exe still ship.
19 + # Job-level env so the MSIX steps' own `if:` conditions can read these.
20 + # When the MSIX identity secrets are absent the MSIX steps skip and the
21 + # signed .msi/.exe still ship.
22 env:
23 MSIX_PUBLISHER: ${{ secrets.MSIX_PUBLISHER }}
23 - MSIX_IDENTITY_NAME: ${{ secrets.MSIX_IDENTITY_NAME }}
24 + MSIX_IDENTITY_NAME: ${{ secrets.MSIX_SITI_IDENTITY_NAME }}
25 steps:
26 - uses: actions/checkout@v7
27
@@ -45,6 +46,12 @@ jobs:
46 with:
47 workspaces: src-tauri
48
49 + # The enabled Windows chat backend pulls in native crypto/C dependencies.
50 + # NASM keeps their x64 assembly path available on the Windows runner.
51 + - name: Install NASM
52 + run: choco install nasm -y --no-progress
53 + shell: pwsh
54 +
55 # Validate the MSIX manifest before the long Rust build rather than after
56 # it. makeappx checks the manifest against its schema regardless of what
57 # the payload files contain, so a stub exe is enough to catch a schema
@@ -96,7 +103,30 @@ jobs:
103
104 - name: Build App
105 shell: pwsh
99 - run: pnpm tauri build -c src-tauri/tauri.prod-windows-microsoftstore.conf.json
106 + run: |
107 + pnpm tauri build -c src-tauri/tauri.prod-windows-microsoftstore.conf.json
108 + # Move installer artifacts to a dedicated input directory for
109 + # CodeSignTool. The MSIX is packed separately below and deliberately
110 + # left unsigned for Partner Center.
111 + mkdir "${env:GITHUB_WORKSPACE}\src-tauri\target\release\bundle\to-sign"
112 + mkdir "${env:GITHUB_WORKSPACE}\src-tauri\target\release\bundle\signed"
113 + Move-Item "${env:GITHUB_WORKSPACE}\src-tauri\target\release\bundle\msi\*" "${env:GITHUB_WORKSPACE}\src-tauri\target\release\bundle\to-sign\" -Force
114 + Move-Item "${env:GITHUB_WORKSPACE}\src-tauri\target\release\bundle\nsis\*" "${env:GITHUB_WORKSPACE}\src-tauri\target\release\bundle\to-sign\" -Force
115 +
116 + - name: Sign Artifact with CodeSignTool
117 + uses: sslcom/esigner-codesign@develop
118 + with:
119 + command: batch_sign
120 + username: ${{ secrets.ES_USERNAME }}
121 + password: ${{ secrets.ES_PASSWORD }}
122 + credential_id: ${{ secrets.ES_CREDENTIAL_ID }}
123 + totp_secret: ${{ secrets.ES_TOTP_SECRET }}
124 + dir_path: ${GITHUB_WORKSPACE}\src-tauri\target\release\bundle\to-sign
125 + output_path: ${GITHUB_WORKSPACE}\src-tauri\target\release\bundle\signed
126 + malware_block: false
127 + clean_logs: true
128 + jvm_max_memory: 1024M
129 + signing_method: v1
130
131 # The .msi/.exe above are for direct download only. A Store MSI/EXE
132 # submission is just a hosted installer URL, so it never gets package
@@ -119,11 +149,12 @@ jobs:
149 - name: Release
150 uses: softprops/action-gh-release@v2
151 with:
152 + tag_name: "windows-microsoft-store-siti-ai-v${{ env.VERSION }}"
153 draft: true
154 prerelease: false
155 body: "See the assets to download this version and install."
156 name: "Siti AI v${{ env.VERSION }} (Windows Microsoft Store)"
157 files: |
127 - src-tauri/target/release/bundle/msi/*.msi
128 - src-tauri/target/release/bundle/nsis/*.exe
158 + src-tauri/target/release/bundle/signed/*.msi
159 + src-tauri/target/release/bundle/signed/*.exe
160 src-tauri/target/release/bundle/msix/*.msix
src-tauri/src/chat/command_clear_history.rs
+19 -4
@@ -4,11 +4,21 @@
4
5 use log::debug;
6
7 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
7 +#[cfg(any(
8 + target_os = "macos",
9 + target_os = "ios",
10 + target_os = "android",
11 + target_os = "windows"
12 +))]
13 use {super::ENGINE, log::info};
14
15 /// Clear the conversation history but keep the model loaded.
11 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
16 +#[cfg(any(
17 + target_os = "macos",
18 + target_os = "ios",
19 + target_os = "android",
20 + target_os = "windows"
21 +))]
22 #[tauri::command]
23 pub async fn chat_clear_history() -> Result<String, String> {
24 if ENGINE.is_loaded().await {
@@ -22,9 +32,14 @@ pub async fn chat_clear_history() -> Result<String, String> {
32 }
33 }
34
25 -#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "android")))]
35 +#[cfg(not(any(
36 + target_os = "macos",
37 + target_os = "ios",
38 + target_os = "android",
39 + target_os = "windows"
40 +)))]
41 #[tauri::command]
42 pub async fn chat_clear_history() -> Result<String, String> {
28 - debug!("Siti chat is only supported on macOS, iOS, and Android for now.");
43 + debug!("Siti chat is not supported on this platform.");
44 Ok("No chat model loaded.".to_string())
45 }
src-tauri/src/chat/command_get_history.rs
+18 -3
@@ -4,11 +4,21 @@
4
5 use super::ChatMessagePayload;
6
7 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
7 +#[cfg(any(
8 + target_os = "macos",
9 + target_os = "ios",
10 + target_os = "android",
11 + target_os = "windows"
12 +))]
13 use {super::ENGINE, onde::inference::ChatRole};
14
15 /// Return the full conversation history.
11 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
16 +#[cfg(any(
17 + target_os = "macos",
18 + target_os = "ios",
19 + target_os = "android",
20 + target_os = "windows"
21 +))]
22 #[tauri::command]
23 pub async fn chat_get_history() -> Vec<ChatMessagePayload> {
24 ENGINE
@@ -26,7 +36,12 @@ pub async fn chat_get_history() -> Vec<ChatMessagePayload> {
36 .collect()
37 }
38
29 -#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "android")))]
39 +#[cfg(not(any(
40 + target_os = "macos",
41 + target_os = "ios",
42 + target_os = "android",
43 + target_os = "windows"
44 +)))]
45 #[tauri::command]
46 pub async fn chat_get_history() -> Vec<ChatMessagePayload> {
47 Vec::new()
src-tauri/src/chat/command_get_status.rs
+18 -3
@@ -4,11 +4,21 @@
4
5 use super::ChatStatusResponse;
6
7 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
7 +#[cfg(any(
8 + target_os = "macos",
9 + target_os = "ios",
10 + target_os = "android",
11 + target_os = "windows"
12 +))]
13 use {super::ENGINE, crate::constants::ChatStatus, onde::inference::EngineStatus};
14
15 /// Get the current status of the chat model and conversation.
11 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
16 +#[cfg(any(
17 + target_os = "macos",
18 + target_os = "ios",
19 + target_os = "android",
20 + target_os = "windows"
21 +))]
22 #[tauri::command]
23 pub async fn chat_get_status() -> ChatStatusResponse {
24 let info = ENGINE.info().await;
@@ -27,7 +37,12 @@ pub async fn chat_get_status() -> ChatStatusResponse {
37 }
38 }
39
30 -#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "android")))]
40 +#[cfg(not(any(
41 + target_os = "macos",
42 + target_os = "ios",
43 + target_os = "android",
44 + target_os = "windows"
45 +)))]
46 #[tauri::command]
47 pub async fn chat_get_status() -> ChatStatusResponse {
48 use crate::constants::ChatStatus;
src-tauri/src/chat/command_list_models.rs
+15 -5
@@ -27,15 +27,20 @@ fn sanitize_description(desc: &str) -> String {
27 .replace("siGit Code", "coding tasks")
28 }
29
30 -/// On Android the catalogue text is already accurate, so pass it through.
31 -#[cfg(target_os = "android")]
30 +/// On non-Apple builds the catalogue text is already accurate, so pass it through.
31 +#[cfg(any(target_os = "android", target_os = "windows"))]
32 fn sanitize_description(desc: &str) -> String {
33 desc.to_string()
34 }
35
36 /// List the supported on-device models, smallest first, with the currently
37 /// selected model marked. Used to populate the Settings model dropdown.
38 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
38 +#[cfg(any(
39 + target_os = "macos",
40 + target_os = "ios",
41 + target_os = "android",
42 + target_os = "windows"
43 +))]
44 #[tauri::command]
45 pub async fn chat_list_models() -> Vec<ModelInfo> {
46 use super::{config_for_model_id, is_model_downloaded, SELECTED_MODEL};
@@ -88,9 +93,14 @@ pub async fn chat_list_models() -> Vec<ModelInfo> {
93 models
94 }
95
91 -#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "android")))]
96 +#[cfg(not(any(
97 + target_os = "macos",
98 + target_os = "ios",
99 + target_os = "android",
100 + target_os = "windows"
101 +)))]
102 #[tauri::command]
103 pub async fn chat_list_models() -> Vec<ModelInfo> {
94 - log::debug!("On-device models are only supported on macOS, iOS, and Android.");
104 + log::debug!("On-device models are not supported on this platform.");
105 Vec::new()
106 }
src-tauri/src/chat/command_load_model.rs
+20 -5
@@ -7,7 +7,12 @@
7
8 use tauri::AppHandle;
9
10 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
10 +#[cfg(any(
11 + target_os = "macos",
12 + target_os = "ios",
13 + target_os = "android",
14 + target_os = "windows"
15 +))]
16 use {
17 super::{
18 emit_chat_status, fmt_duration, resolved_model_config, sampling_config, CHAT_SYSTEM_PROMPT,
@@ -17,7 +22,12 @@ use {
22 };
23
24 /// Load the platform-default chat model into memory.
20 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
25 +#[cfg(any(
26 + target_os = "macos",
27 + target_os = "ios",
28 + target_os = "android",
29 + target_os = "windows"
30 +))]
31 #[tauri::command]
32 pub async fn chat_load_model(app: AppHandle) -> Result<String, String> {
33 let config = resolved_model_config();
@@ -48,9 +58,14 @@ pub async fn chat_load_model(app: AppHandle) -> Result<String, String> {
58 Ok(msg)
59 }
60
51 -#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "android")))]
61 +#[cfg(not(any(
62 + target_os = "macos",
63 + target_os = "ios",
64 + target_os = "android",
65 + target_os = "windows"
66 +)))]
67 #[tauri::command]
68 pub async fn chat_load_model(_app: AppHandle) -> Result<String, String> {
54 - log::debug!("Chat model is only supported on macOS, iOS, and Android for now.");
55 - Err("Chat model is only supported on macOS, iOS, and Android for now.".to_string())
69 + log::debug!("Chat model is not supported on this platform.");
70 + Err("Chat model is not supported on this platform.".to_string())
71 }
src-tauri/src/chat/command_remove_model.rs
+20 -5
@@ -6,7 +6,12 @@
6
7 use tauri::AppHandle;
8
9 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
9 +#[cfg(any(
10 + target_os = "macos",
11 + target_os = "ios",
12 + target_os = "android",
13 + target_os = "windows"
14 +))]
15 use {
16 super::{emit_chat_status, resolved_model_config, ENGINE, SELECTED_MODEL},
17 crate::constants::ChatStatus,
@@ -15,7 +20,12 @@ use {
20
21 /// Delete the locally cached weights for `model_id` (a HuggingFace repo id from
22 /// `chat_list_models`). Unloads the model first if it is currently loaded.
18 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
23 +#[cfg(any(
24 + target_os = "macos",
25 + target_os = "ios",
26 + target_os = "android",
27 + target_os = "windows"
28 +))]
29 #[tauri::command]
30 pub async fn chat_remove_model(app: AppHandle, model_id: String) -> Result<String, String> {
31 // If we're deleting the model that's currently loaded/selected, unload it
@@ -43,9 +53,14 @@ pub async fn chat_remove_model(app: AppHandle, model_id: String) -> Result<Strin
53 Ok(msg)
54 }
55
46 -#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "android")))]
56 +#[cfg(not(any(
57 + target_os = "macos",
58 + target_os = "ios",
59 + target_os = "android",
60 + target_os = "windows"
61 +)))]
62 #[tauri::command]
63 pub async fn chat_remove_model(_app: AppHandle, _model_id: String) -> Result<String, String> {
49 - log::debug!("Model management is only supported on macOS, iOS, and Android.");
50 - Err("Model management is only supported on macOS, iOS, and Android for now.".to_string())
64 + log::debug!("Model management is not supported on this platform.");
65 + Err("Model management is not supported on this platform.".to_string())
66 }
src-tauri/src/chat/command_send_message.rs
+26 -6
@@ -7,10 +7,20 @@
7 use super::*;
8 use tauri::AppHandle;
9
10 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
10 +#[cfg(any(
11 + target_os = "macos",
12 + target_os = "ios",
13 + target_os = "android",
14 + target_os = "windows"
15 +))]
16 use {log::info, tauri::Emitter};
17
13 -#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "android")))]
18 +#[cfg(not(any(
19 + target_os = "macos",
20 + target_os = "ios",
21 + target_os = "android",
22 + target_os = "windows"
23 +)))]
24 use log::debug;
25
26 /// Send a user message to Siti and receive an assistant reply.
@@ -27,7 +37,12 @@ use log::debug;
37 /// while the command is still running, especially on budget SoCs where
38 /// GGUF inference takes 60-300 seconds. Emitting an event from a spawned
39 /// task sidesteps the GC entirely.
30 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
40 +#[cfg(any(
41 + target_os = "macos",
42 + target_os = "ios",
43 + target_os = "android",
44 + target_os = "windows"
45 +))]
46 #[tauri::command]
47 pub async fn chat_send_message(app: AppHandle, message: String) -> Result<(), String> {
48 // ── 1. Lazy-load the model if not already cached ──────────────────────
@@ -105,9 +120,14 @@ pub async fn chat_send_message(app: AppHandle, message: String) -> Result<(), St
120 Ok(())
121 }
122
108 -#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "android")))]
123 +#[cfg(not(any(
124 + target_os = "macos",
125 + target_os = "ios",
126 + target_os = "android",
127 + target_os = "windows"
128 +)))]
129 #[tauri::command]
130 pub async fn chat_send_message(_app: AppHandle, _message: String) -> Result<(), String> {
111 - debug!("Siti chat is only supported on macOS, iOS, and Android for now.");
112 - Err("Siti chat is only supported on macOS, iOS, and Android for now.".to_string())
131 + debug!("Siti chat is not supported on this platform.");
132 + Err("Siti chat is not supported on this platform.".to_string())
133 }
src-tauri/src/chat/command_set_model.rs
+20 -5
@@ -9,7 +9,12 @@
9
10 use tauri::AppHandle;
11
12 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
12 +#[cfg(any(
13 + target_os = "macos",
14 + target_os = "ios",
15 + target_os = "android",
16 + target_os = "windows"
17 +))]
18 use {
19 super::{
20 emit_chat_status, fmt_duration, resolve_model_id, resolved_model_config, sampling_config,
@@ -22,7 +27,12 @@ use {
27 /// Switch Siti to the model identified by `model_id` (a HuggingFace repo id
28 /// from `chat_list_models`). The new model is loaded asynchronously; watch the
29 /// `chat_status_changed` event for `Loading` → `Ready`/`Error`.
25 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
30 +#[cfg(any(
31 + target_os = "macos",
32 + target_os = "ios",
33 + target_os = "android",
34 + target_os = "windows"
35 +))]
36 #[tauri::command]
37 pub async fn chat_set_model(app: AppHandle, model_id: String) -> Result<(), String> {
38 // ── 1. Validate & resolve the requested model ────────────────────────
@@ -79,9 +89,14 @@ pub async fn chat_set_model(app: AppHandle, model_id: String) -> Result<(), Stri
89 Ok(())
90 }
91
82 -#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "android")))]
92 +#[cfg(not(any(
93 + target_os = "macos",
94 + target_os = "ios",
95 + target_os = "android",
96 + target_os = "windows"
97 +)))]
98 #[tauri::command]
99 pub async fn chat_set_model(_app: AppHandle, _model_id: String) -> Result<(), String> {
85 - log::debug!("Model switching is only supported on macOS, iOS, and Android.");
86 - Err("Model switching is only supported on macOS, iOS, and Android for now.".to_string())
100 + log::debug!("Model switching is not supported on this platform.");
101 + Err("Model switching is not supported on this platform.".to_string())
102 }
src-tauri/src/chat/command_unload_model.rs
+19 -4
@@ -2,7 +2,12 @@
2
3 use {log::debug, tauri::AppHandle};
4
5 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
5 +#[cfg(any(
6 + target_os = "macos",
7 + target_os = "ios",
8 + target_os = "android",
9 + target_os = "windows"
10 +))]
11 use {
12 super::{emit_chat_status, resolved_model_config, ENGINE},
13 crate::constants::ChatStatus,
@@ -10,7 +15,12 @@ use {
15 };
16
17 /// Unload the chat model from memory to free resources.
13 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
18 +#[cfg(any(
19 + target_os = "macos",
20 + target_os = "ios",
21 + target_os = "android",
22 + target_os = "windows"
23 +))]
24 #[tauri::command]
25 pub async fn chat_unload_model(app: AppHandle) -> Result<String, String> {
26 if ENGINE.is_loaded().await {
@@ -25,9 +35,14 @@ pub async fn chat_unload_model(app: AppHandle) -> Result<String, String> {
35 }
36 }
37
28 -#[cfg(not(any(target_os = "macos", target_os = "ios", target_os = "android")))]
38 +#[cfg(not(any(
39 + target_os = "macos",
40 + target_os = "ios",
41 + target_os = "android",
42 + target_os = "windows"
43 +)))]
44 #[tauri::command]
45 pub async fn chat_unload_model(_app: AppHandle) -> Result<String, String> {
31 - debug!("Chat model is only supported on macOS, iOS, and Android.");
46 + debug!("Chat model is not supported on this platform.");
47 Ok("No chat model was loaded.".to_string())
48 }
src-tauri/src/chat/mod.rs
+98 -18
@@ -41,7 +41,12 @@ use {
41 serde::{Deserialize, Serialize},
42 };
43
44 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
44 +#[cfg(any(
45 + target_os = "macos",
46 + target_os = "ios",
47 + target_os = "android",
48 + target_os = "windows"
49 +))]
50 use {
51 crate::events::{EVENT_CHAT_REPLY, EVENT_CHAT_STATUS_CHANGED},
52 log::error,
@@ -123,7 +128,12 @@ pub struct ModelInfo {
128 // `ChatEngine` from `onde` handles all model lifecycle, history, and inference.
129 // It is `Send + Sync` and manages its own internal mutex.
130
126 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
131 +#[cfg(any(
132 + target_os = "macos",
133 + target_os = "ios",
134 + target_os = "android",
135 + target_os = "windows"
136 +))]
137 pub(crate) static ENGINE: Lazy<ChatEngine> = Lazy::new(ChatEngine::new);
138
139 // ── Model selection & configuration ──────────────────────────────────────────
@@ -133,19 +143,29 @@ pub(crate) static ENGINE: Lazy<ChatEngine> = Lazy::new(ChatEngine::new);
143 // config and the sampling config. When nothing has been chosen yet, Siti
144 // falls back to a general-purpose chat model appropriate for the platform:
145 // - iOS / Android → Qwen 2.5 1.5B (~941 MB, fits mobile memory budgets)
136 -// - macOS → Qwen 2.5 3B (~1.93 GB, more headroom on desktop)
146 +// - macOS/Windows → Qwen 2.5 3B (~1.93 GB, more headroom on desktop)
147 //
148 // `config_for_model_id` handles per-platform details (e.g. `tok_model_id` on
139 -// Android) via the onde `GgufModelConfig` constructors.
149 +// mobile) via the onde `GgufModelConfig` constructors.
150
151 /// The model id currently selected for Siti. Defaults to the platform-
152 /// appropriate general chat model; updated by `chat_set_model`.
143 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
153 +#[cfg(any(
154 + target_os = "macos",
155 + target_os = "ios",
156 + target_os = "android",
157 + target_os = "windows"
158 +))]
159 pub(crate) static SELECTED_MODEL: Lazy<std::sync::Mutex<String>> =
160 Lazy::new(|| std::sync::Mutex::new(siti_default_config().model_id));
161
162 /// Siti's default general-purpose chat model for the current platform.
148 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
163 +#[cfg(any(
164 + target_os = "macos",
165 + target_os = "ios",
166 + target_os = "android",
167 + target_os = "windows"
168 +))]
169 pub(crate) fn siti_default_config() -> GgufModelConfig {
170 if cfg!(any(target_os = "ios", target_os = "android")) {
171 GgufModelConfig::qwen25_1_5b()
@@ -156,7 +176,12 @@ pub(crate) fn siti_default_config() -> GgufModelConfig {
176
177 /// Map a HuggingFace repo id to a loadable [`GgufModelConfig`], or `None` if
178 /// the id is not a model the onde engine knows how to load.
159 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
179 +#[cfg(any(
180 + target_os = "macos",
181 + target_os = "ios",
182 + target_os = "android",
183 + target_os = "windows"
184 +))]
185 pub(crate) fn config_for_model_id(id: &str) -> Option<GgufModelConfig> {
186 use crate::inference::models as m;
187 let cfg = if id == m::BARTOWSKI_QWEN25_0_5B_INSTRUCT_GGUF {
@@ -233,14 +258,24 @@ pub(crate) fn gemma2_2b_isq_config() -> IsqModelConfig {
258
259 /// A model selection resolved to the concrete engine config and load path it
260 /// needs. Most models are GGUF; Gemma is the sole ISQ model (macOS only).
236 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
261 +#[cfg(any(
262 + target_os = "macos",
263 + target_os = "ios",
264 + target_os = "android",
265 + target_os = "windows"
266 +))]
267 pub(crate) enum ResolvedModel {
268 Gguf(GgufModelConfig),
269 #[cfg(target_os = "macos")]
270 Isq(IsqModelConfig),
271 }
272
243 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
273 +#[cfg(any(
274 + target_os = "macos",
275 + target_os = "ios",
276 + target_os = "android",
277 + target_os = "windows"
278 +))]
279 impl ResolvedModel {
280 /// Human-friendly display name for the resolved model.
281 pub(crate) fn display_name(&self) -> String {
@@ -268,7 +303,12 @@ impl ResolvedModel {
303
304 /// Resolve a HuggingFace repo id to a loadable [`ResolvedModel`], or `None` if
305 /// the id is not a model Siti can load on this platform.
271 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
306 +#[cfg(any(
307 + target_os = "macos",
308 + target_os = "ios",
309 + target_os = "android",
310 + target_os = "windows"
311 +))]
312 pub(crate) fn resolve_model_id(id: &str) -> Option<ResolvedModel> {
313 #[cfg(target_os = "macos")]
314 if id == GEMMA2_2B_IT_ISQ_ID {
@@ -279,7 +319,12 @@ pub(crate) fn resolve_model_id(id: &str) -> Option<ResolvedModel> {
319
320 /// Return the resolved model (GGUF or ISQ) for the current selection, falling
321 /// back to the platform-default GGUF model if the selection is unknown.
282 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
322 +#[cfg(any(
323 + target_os = "macos",
324 + target_os = "ios",
325 + target_os = "android",
326 + target_os = "windows"
327 +))]
328 pub(crate) fn resolved_model_config() -> ResolvedModel {
329 let id = SELECTED_MODEL.lock().map(|g| g.clone()).unwrap_or_default();
330 resolve_model_id(&id).unwrap_or_else(|| ResolvedModel::Gguf(siti_default_config()))
@@ -289,7 +334,12 @@ pub(crate) fn resolved_model_config() -> ResolvedModel {
334
335 /// Fraction of `expected_size_bytes` that must be present on disk for a model
336 /// to count as fully downloaded (mirrors onde's own completeness threshold).
292 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
337 +#[cfg(any(
338 + target_os = "macos",
339 + target_os = "ios",
340 + target_os = "android",
341 + target_os = "windows"
342 +))]
343 const DOWNLOAD_COMPLETE_THRESHOLD: f64 = 0.99;
344
345 /// Sum the byte size of every real file directly and recursively under `path`.
@@ -297,7 +347,12 @@ const DOWNLOAD_COMPLETE_THRESHOLD: f64 = 0.99;
347 /// Only used against the HF cache's `blobs/` directory, which holds the actual
348 /// downloaded files (not the `snapshots/` symlink/hard-link views), so nothing
349 /// is double-counted.
300 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
350 +#[cfg(any(
351 + target_os = "macos",
352 + target_os = "ios",
353 + target_os = "android",
354 + target_os = "windows"
355 +))]
356 fn blobs_dir_size(path: &std::path::Path) -> u64 {
357 let mut total = 0;
358 if let Ok(entries) = std::fs::read_dir(path) {
@@ -318,7 +373,12 @@ fn blobs_dir_size(path: &std::path::Path) -> u64 {
373 /// A model counts as downloaded when its cache `blobs/` directory holds at
374 /// least [`DOWNLOAD_COMPLETE_THRESHOLD`] of the model's expected size, which
375 /// excludes partial/interrupted downloads.
321 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
376 +#[cfg(any(
377 + target_os = "macos",
378 + target_os = "ios",
379 + target_os = "android",
380 + target_os = "windows"
381 +))]
382 pub(crate) fn is_model_downloaded(id: &str, expected_size_bytes: u64) -> bool {
383 if expected_size_bytes == 0 {
384 return false;
@@ -340,7 +400,12 @@ pub(crate) fn is_model_downloaded(id: &str, expected_size_bytes: u64) -> bool {
400 /// larger `max_tokens` budget because their `<think>…</think>` block can
401 /// consume hundreds of tokens before the visible reply begins, and too small
402 /// a budget yields empty replies.
343 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
403 +#[cfg(any(
404 + target_os = "macos",
405 + target_os = "ios",
406 + target_os = "android",
407 + target_os = "windows"
408 +))]
409 pub(crate) fn sampling_config() -> SamplingConfig {
410 let id = SELECTED_MODEL.lock().map(|g| g.clone()).unwrap_or_default();
411 let is_qwen3 = id.contains("Qwen3") || id.contains("Qwen_Qwen3");
@@ -371,7 +436,12 @@ pub(crate) fn sampling_config() -> SamplingConfig {
436 }
437
438 /// Default system prompt for Siti, the private on-device personal assistant.
374 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
439 +#[cfg(any(
440 + target_os = "macos",
441 + target_os = "ios",
442 + target_os = "android",
443 + target_os = "windows"
444 +))]
445 pub(crate) const CHAT_SYSTEM_PROMPT: &str = r#"You are Siti, a friendly and capable personal assistant that runs entirely on the user's device. Nothing the user says ever leaves the device.
446
447 You help with everyday things: answering questions, brainstorming, drafting and editing text, summarising, explaining ideas in plain terms, and planning.
@@ -383,7 +453,12 @@ You run locally, so you may not know about very recent events. When you are not
453 // ── Helpers ──────────────────────────────────────────────────────────────────
454
455 /// Format a `Duration` as `Xm Ys` or just `Ys` when under a minute.
386 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
456 +#[cfg(any(
457 + target_os = "macos",
458 + target_os = "ios",
459 + target_os = "android",
460 + target_os = "windows"
461 +))]
462 pub(crate) fn fmt_duration(d: std::time::Duration) -> String {
463 let total_secs = d.as_secs_f64();
464 let mins = (total_secs / 60.0).floor() as u64;
@@ -396,7 +471,12 @@ pub(crate) fn fmt_duration(d: std::time::Duration) -> String {
471 }
472
473 /// Emit a chat status event to the frontend.
399 -#[cfg(any(target_os = "macos", target_os = "ios", target_os = "android"))]
474 +#[cfg(any(
475 + target_os = "macos",
476 + target_os = "ios",
477 + target_os = "android",
478 + target_os = "windows"
479 +))]
480 pub(crate) fn emit_chat_status(
481 app: &AppHandle,
482 status: ChatStatus,