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,