sigit-code 1.7.6__tar.gz → 1.7.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sigit_code-1.7.6 → sigit_code-1.7.7}/Cargo.lock +2 -2
- {sigit_code-1.7.6 → sigit_code-1.7.7}/Cargo.toml +2 -2
- {sigit_code-1.7.6 → sigit_code-1.7.7}/PKG-INFO +1 -1
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/account.rs +43 -11
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/chat.rs +153 -87
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/headless.rs +73 -16
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/main.rs +7 -3
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/mcp.rs +19 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/provider.rs +24 -0
- sigit_code-1.7.7/crates/cli/src/splash.rs +551 -0
- sigit_code-1.7.7/crates/cli/src/startup.rs +238 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/README.md +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/Cargo.toml +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/README.md +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/backend.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/client_fs.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/client_terminal.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/commands.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/frontmatter.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/harness_markup.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/hooks.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/inline_tool_calls.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/instructions.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/models.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/permissions.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/program_status.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/repetition.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/session_store.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/settings.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/setup.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/skills.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/subagents.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/tools.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/src/workspace.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/tests/acp_audio.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/tests/acp_client_fs.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/tests/acp_client_terminal.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/tests/acp_cloud_reauth.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/tests/acp_concurrent_sessions.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/tests/acp_elicitation.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/tests/acp_embedded_context.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/tests/acp_endpoint_errors.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/tests/acp_error_codes.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/tests/acp_harness_markup.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/tests/acp_images.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/tests/acp_logout.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/tests/acp_multi_root.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/tests/acp_permissions.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/tests/acp_session_list.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/tests/acp_session_load.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/tests/acp_stop_reasons.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/tests/acp_tool_call_name.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/tests/acp_tool_stdin.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/tests/headless_mode.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/cli/tests/version_flag.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/sigit-agent/Cargo.toml +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/sigit-agent/README.md +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/sigit-agent/assets/sign-in-logo.png +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/sigit-agent/src/auth.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/sigit-agent/src/browser_auth.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/sigit-agent/src/credentials.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/sigit-agent/src/lib.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/sigit-agent/src/profile.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/sigit-agent/src/prompt.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/crates/sigit-agent/tests/embedded.rs +0 -0
- {sigit_code-1.7.6 → sigit_code-1.7.7}/pyproject.toml +0 -0
|
@@ -5945,7 +5945,7 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
|
|
5945
5945
|
|
|
5946
5946
|
[[package]]
|
|
5947
5947
|
name = "sigit"
|
|
5948
|
-
version = "1.7.
|
|
5948
|
+
version = "1.7.7"
|
|
5949
5949
|
dependencies = [
|
|
5950
5950
|
"agent-client-protocol",
|
|
5951
5951
|
"anyhow",
|
|
@@ -5976,7 +5976,7 @@ dependencies = [
|
|
|
5976
5976
|
|
|
5977
5977
|
[[package]]
|
|
5978
5978
|
name = "sigit-agent"
|
|
5979
|
-
version = "1.7.
|
|
5979
|
+
version = "1.7.7"
|
|
5980
5980
|
dependencies = [
|
|
5981
5981
|
"agent-client-protocol",
|
|
5982
5982
|
"anyhow",
|
|
@@ -3,7 +3,7 @@ resolver = "3"
|
|
|
3
3
|
members = ["crates/cli", "crates/sigit-*"]
|
|
4
4
|
|
|
5
5
|
[workspace.package]
|
|
6
|
-
version = "1.7.
|
|
6
|
+
version = "1.7.7"
|
|
7
7
|
edition = "2024"
|
|
8
8
|
license = "Apache-2.0"
|
|
9
9
|
repository = "https://github.com/getsigit/sigit"
|
|
@@ -16,7 +16,7 @@ authors = ["Seto Elkahfi <seto@sigit.si>", "paydii <193906237+paydii@users.norep
|
|
|
16
16
|
# crates.io, so this resolves from the registry rather than the onde-ed checkout next door.
|
|
17
17
|
ed-acp = { version = "1.3.0", default-features = false }
|
|
18
18
|
ed-acp-testkit = "1.3.0"
|
|
19
|
-
sigit-agent = { version = "1.7.
|
|
19
|
+
sigit-agent = { version = "1.7.7", default-features = false, path = "crates/sigit-agent" }
|
|
20
20
|
|
|
21
21
|
# ACP protocol SDK
|
|
22
22
|
agent-client-protocol = { version = "2.2", features = ["unstable_session_fork"] }
|
|
@@ -129,14 +129,32 @@ pub async fn end_session() -> String {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
///
|
|
133
|
-
|
|
132
|
+
/// What the stored session amounts to right now.
|
|
133
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
134
|
+
pub enum SessionState {
|
|
135
|
+
/// No credentials are stored.
|
|
136
|
+
SignedOut,
|
|
137
|
+
/// The server accepted the token.
|
|
138
|
+
Valid { email: String },
|
|
139
|
+
/// The server rejected the token (HTTP status).
|
|
140
|
+
Expired { status: u16 },
|
|
141
|
+
/// Credentials are stored but the server could not be reached in time.
|
|
142
|
+
Offline { email: String },
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Look up the stored session and verify its token, giving up on the network
|
|
146
|
+
/// after `timeout`.
|
|
147
|
+
pub async fn check_session(timeout: std::time::Duration) -> SessionState {
|
|
134
148
|
let Some(creds) = credentials::load() else {
|
|
135
|
-
return
|
|
149
|
+
return SessionState::SignedOut;
|
|
136
150
|
};
|
|
137
151
|
|
|
138
152
|
let url = format!("{}/api/v1/user", api_base().trim_end_matches('/'));
|
|
139
|
-
|
|
153
|
+
let client = reqwest::Client::builder()
|
|
154
|
+
.timeout(timeout)
|
|
155
|
+
.build()
|
|
156
|
+
.unwrap_or_default();
|
|
157
|
+
match client
|
|
140
158
|
.get(&url)
|
|
141
159
|
.bearer_auth(&creds.access_token)
|
|
142
160
|
.send()
|
|
@@ -150,14 +168,28 @@ pub async fn status_line() -> String {
|
|
|
150
168
|
.and_then(|me| me.email)
|
|
151
169
|
.or(creds.email)
|
|
152
170
|
.unwrap_or_else(|| "(unknown)".to_string());
|
|
153
|
-
|
|
171
|
+
SessionState::Valid { email }
|
|
172
|
+
}
|
|
173
|
+
Ok(response) => SessionState::Expired {
|
|
174
|
+
status: response.status().as_u16(),
|
|
175
|
+
},
|
|
176
|
+
Err(_) => SessionState::Offline {
|
|
177
|
+
email: creds.email.unwrap_or_else(|| "(unknown)".to_string()),
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/// One-line description of the current session, verifying the token if reachable.
|
|
183
|
+
pub async fn status_line() -> String {
|
|
184
|
+
match check_session(std::time::Duration::from_secs(10)).await {
|
|
185
|
+
SessionState::SignedOut => {
|
|
186
|
+
"Not signed in. Use `/login` to use siGit Code Cloud.".to_string()
|
|
187
|
+
}
|
|
188
|
+
SessionState::Valid { email } => format!("Signed in to siGit Code Cloud as {email}."),
|
|
189
|
+
SessionState::Expired { status } => {
|
|
190
|
+
format!("Session may be expired (HTTP {status}). Use `/login` again.")
|
|
154
191
|
}
|
|
155
|
-
|
|
156
|
-
"Session may be expired (HTTP {}). Use `/login` again.",
|
|
157
|
-
response.status().as_u16()
|
|
158
|
-
),
|
|
159
|
-
Err(_) => {
|
|
160
|
-
let email = creds.email.unwrap_or_else(|| "(unknown)".to_string());
|
|
192
|
+
SessionState::Offline { email } => {
|
|
161
193
|
format!("Signed in as {email} (could not reach siGit Code Cloud to verify).")
|
|
162
194
|
}
|
|
163
195
|
}
|
|
@@ -520,6 +520,7 @@ mod tui {
|
|
|
520
520
|
use onde::inference::{ChatEngine, SamplingConfig};
|
|
521
521
|
|
|
522
522
|
use super::{RepoItem, Tab};
|
|
523
|
+
use crate::account::SessionState;
|
|
523
524
|
use crate::backend::{
|
|
524
525
|
InferenceBackend, LocalBackend, OpenAiBackend, TokenChunk, ToolResult, ToolSpec,
|
|
525
526
|
};
|
|
@@ -528,6 +529,10 @@ mod tui {
|
|
|
528
529
|
};
|
|
529
530
|
use crate::program_status::{Reporter, Status};
|
|
530
531
|
use crate::session_store::SessionEntry;
|
|
532
|
+
use crate::splash::{self, Phase, Splash, Wordmark};
|
|
533
|
+
use crate::startup::{
|
|
534
|
+
self, Check, Checks, Report, account_check, models_check, tools_check, workspace_check,
|
|
535
|
+
};
|
|
531
536
|
use ratatui::{
|
|
532
537
|
Frame,
|
|
533
538
|
layout::{Constraint, Layout, Position},
|
|
@@ -545,8 +550,6 @@ mod tui {
|
|
|
545
550
|
User,
|
|
546
551
|
Assistant,
|
|
547
552
|
System,
|
|
548
|
-
/// rainbow-colored banner art
|
|
549
|
-
Banner,
|
|
550
553
|
/// a collapsible tool-call entry (`text` holds the title line)
|
|
551
554
|
Tool,
|
|
552
555
|
}
|
|
@@ -596,16 +599,6 @@ mod tui {
|
|
|
596
599
|
}
|
|
597
600
|
}
|
|
598
601
|
|
|
599
|
-
fn banner(text: impl Into<String>) -> Self {
|
|
600
|
-
Self {
|
|
601
|
-
role: Role::Banner,
|
|
602
|
-
text: text.into(),
|
|
603
|
-
think_block: None,
|
|
604
|
-
tool_detail: None,
|
|
605
|
-
tool_result: None,
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
|
|
609
602
|
fn tool_call(title: impl Into<String>, detail: impl Into<String>) -> Self {
|
|
610
603
|
Self {
|
|
611
604
|
role: Role::Tool,
|
|
@@ -771,6 +764,13 @@ mod tui {
|
|
|
771
764
|
load_error: Option<String>,
|
|
772
765
|
load_start: Instant,
|
|
773
766
|
load_model_name: String,
|
|
767
|
+
/// The backend is up. The splash still decides when the loading view ends.
|
|
768
|
+
load_ready: bool,
|
|
769
|
+
/// The launch splash and the checks it waits on.
|
|
770
|
+
splash: Splash,
|
|
771
|
+
checks: Checks,
|
|
772
|
+
/// What the account check found, for the welcome message.
|
|
773
|
+
session: Option<SessionState>,
|
|
774
774
|
|
|
775
775
|
// ── Model picker state ────────────────────────────────────────────────
|
|
776
776
|
show_model_picker: bool,
|
|
@@ -832,21 +832,6 @@ mod tui {
|
|
|
832
832
|
repo_rx: Option<mpsc::UnboundedReceiver<RepoUpdate>>,
|
|
833
833
|
}
|
|
834
834
|
|
|
835
|
-
const BANNER_ART: &str = "\
|
|
836
|
-
77777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777
|
|
837
|
-
77777777322222222222222222222222222222223777389969902208431358831999699051111177777777777777
|
|
838
|
-
1111111125555555555555555555555511113222311159 5002 088 3081771691111111111111
|
|
839
|
-
1111111111111111111111111111131136841 1482853332007 05 9043332891 400811111111111
|
|
840
|
-
1111111111111111111111111111111201 109 304 40 00 79 100041111111111
|
|
841
|
-
333333255555555555555555555552392 102 503 90 7000000005 903 0000023333333333
|
|
842
|
-
333333245454545454545454545433381 7600000 302 61 780 109 20009533333333333
|
|
843
|
-
3333333333333333333333333333333402 7001 08 761 202 902 90003333333333333
|
|
844
|
-
2222255555555555555555555555250899901 49 304 403 08 108 300042222222222222
|
|
845
|
-
2222222222222222222222222222269 106 03 901 06 505 402 000052222222222222
|
|
846
|
-
2222255555555555555555555555299 708 1002 80 00 90852222222222222
|
|
847
|
-
55555555555555555555555555555560953258000866660000051140866908666600008966900065555555555555
|
|
848
|
-
88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888";
|
|
849
|
-
|
|
850
835
|
const THINKING_FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
851
836
|
|
|
852
837
|
fn rich_text_spans(text: &str, base_style: Style, bold_style: Style) -> Vec<Span<'static>> {
|
|
@@ -870,21 +855,10 @@ mod tui {
|
|
|
870
855
|
backend: Arc<dyn InferenceBackend>,
|
|
871
856
|
program_status: Reporter,
|
|
872
857
|
) -> Self {
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
.find(|m| m.display_name == load_model_name)
|
|
878
|
-
.map(|m| m.tool_calling)
|
|
879
|
-
.unwrap_or(true);
|
|
880
|
-
// For a remote provider the passed-in name is authoritative; the
|
|
881
|
-
// persisted local selection must not override it (or the title would
|
|
882
|
-
// show an on-device model while requests go to the cloud).
|
|
883
|
-
let current_model_name = if is_remote {
|
|
884
|
-
load_model_name.clone()
|
|
885
|
-
} else {
|
|
886
|
-
crate::setup::load_selected_model_name().unwrap_or_else(|| load_model_name.clone())
|
|
887
|
-
};
|
|
858
|
+
// The model scan, the saved selection and the repo lookup used to run
|
|
859
|
+
// here, before the first frame. They run behind the splash now and
|
|
860
|
+
// land in `apply_startup`; until then the picker is empty and the
|
|
861
|
+
// passed-in name stands.
|
|
888
862
|
Self {
|
|
889
863
|
messages: Vec::new(),
|
|
890
864
|
input: String::new(),
|
|
@@ -911,11 +885,15 @@ mod tui {
|
|
|
911
885
|
load_error: None,
|
|
912
886
|
load_start: Instant::now(),
|
|
913
887
|
load_model_name: load_model_name.clone(),
|
|
888
|
+
load_ready: false,
|
|
889
|
+
splash: Splash::new(std::time::Instant::now()),
|
|
890
|
+
checks: Checks::new(),
|
|
891
|
+
session: None,
|
|
914
892
|
show_model_picker: false,
|
|
915
893
|
model_picker_index: 0,
|
|
916
|
-
model_picker_items:
|
|
917
|
-
current_model_name,
|
|
918
|
-
tool_calling,
|
|
894
|
+
model_picker_items: Vec::new(),
|
|
895
|
+
current_model_name: load_model_name.clone(),
|
|
896
|
+
tool_calling: true,
|
|
919
897
|
context_tokens_used: None,
|
|
920
898
|
show_thinking: false,
|
|
921
899
|
tools_expanded: false,
|
|
@@ -927,7 +905,7 @@ mod tui {
|
|
|
927
905
|
history_notice: None,
|
|
928
906
|
cloud_lines: None,
|
|
929
907
|
cloud_rx: None,
|
|
930
|
-
repo:
|
|
908
|
+
repo: None,
|
|
931
909
|
repo_section: RepoSection::Issues,
|
|
932
910
|
repo_issues: RepoData::NotFetched,
|
|
933
911
|
repo_prs: RepoData::NotFetched,
|
|
@@ -1108,16 +1086,54 @@ mod tui {
|
|
|
1108
1086
|
self.download_progress = Some((downloaded, expected));
|
|
1109
1087
|
}
|
|
1110
1088
|
|
|
1111
|
-
///
|
|
1089
|
+
/// Take one startup check's answer.
|
|
1090
|
+
fn apply_startup(&mut self, report: Report) {
|
|
1091
|
+
match report {
|
|
1092
|
+
Report::Account(state) => {
|
|
1093
|
+
self.checks.account = account_check(&state);
|
|
1094
|
+
self.session = Some(state);
|
|
1095
|
+
}
|
|
1096
|
+
Report::Models { items, selected } => {
|
|
1097
|
+
self.tool_calling = items
|
|
1098
|
+
.iter()
|
|
1099
|
+
.find(|m| m.display_name == self.load_model_name)
|
|
1100
|
+
.map(|m| m.tool_calling)
|
|
1101
|
+
.unwrap_or(true);
|
|
1102
|
+
// For a remote provider the passed-in name is authoritative; the
|
|
1103
|
+
// persisted local selection must not override it (or the title
|
|
1104
|
+
// would show an on-device model while requests go to the cloud).
|
|
1105
|
+
if !self.backend.is_remote()
|
|
1106
|
+
&& let Some(name) = selected
|
|
1107
|
+
{
|
|
1108
|
+
self.current_model_name = name;
|
|
1109
|
+
}
|
|
1110
|
+
self.checks.models = models_check(&items);
|
|
1111
|
+
self.model_picker_items = items;
|
|
1112
|
+
}
|
|
1113
|
+
Report::Tools(overview) => {
|
|
1114
|
+
self.checks.tools = tools_check(overview.as_ref());
|
|
1115
|
+
}
|
|
1116
|
+
Report::Workspace(repo) => {
|
|
1117
|
+
self.checks.workspace = workspace_check(repo.as_deref());
|
|
1118
|
+
self.repo = repo;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/// switch to chat phase and show the welcome notes
|
|
1112
1124
|
fn finish_loading(&mut self) {
|
|
1113
1125
|
self.is_loading = false;
|
|
1114
|
-
for line in BANNER_ART.lines() {
|
|
1115
|
-
self.messages.push(ChatMessage::banner(line));
|
|
1116
|
-
}
|
|
1117
|
-
self.messages.push(ChatMessage::system(""));
|
|
1118
1126
|
self.messages.push(ChatMessage::system(
|
|
1119
1127
|
"In this world, nothing can be said to be certain, except death and taxes. ~ Pak Sigit",
|
|
1120
1128
|
));
|
|
1129
|
+
for (label, check) in self.checks.rows() {
|
|
1130
|
+
match check {
|
|
1131
|
+
Check::Ok(d) | Check::Note(d) | Check::Warn(d) => self
|
|
1132
|
+
.messages
|
|
1133
|
+
.push(ChatMessage::system(format!("{label}: {d}"))),
|
|
1134
|
+
Check::Pending => {}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1121
1137
|
if self.backend.is_remote() {
|
|
1122
1138
|
self.messages.push(ChatMessage::system(format!(
|
|
1123
1139
|
"Current model: {}",
|
|
@@ -1131,6 +1147,11 @@ mod tui {
|
|
|
1131
1147
|
self.current_model_name
|
|
1132
1148
|
)));
|
|
1133
1149
|
}
|
|
1150
|
+
if matches!(self.session, Some(SessionState::Expired { .. })) {
|
|
1151
|
+
self.messages.push(ChatMessage::system(
|
|
1152
|
+
"Your siGit Code Cloud session has expired. Use /login to sign in again.",
|
|
1153
|
+
));
|
|
1154
|
+
}
|
|
1134
1155
|
self.messages
|
|
1135
1156
|
.push(ChatMessage::system("Type /help for commands."));
|
|
1136
1157
|
}
|
|
@@ -1555,21 +1576,6 @@ mod tui {
|
|
|
1555
1576
|
|
|
1556
1577
|
// ── Repo tab (issues / pull requests via the official MCP server) ─────────
|
|
1557
1578
|
|
|
1558
|
-
/// `owner/name` when the current directory's `origin` remote lives on the
|
|
1559
|
-
/// sigit.si host (per `SIGIT_API_URL`). Runs `git remote get-url origin`
|
|
1560
|
-
/// once at startup; any failure (no git, no repo, no origin) → `None`.
|
|
1561
|
-
fn detect_sigit_repo() -> Option<String> {
|
|
1562
|
-
let output = std::process::Command::new("git")
|
|
1563
|
-
.args(["remote", "get-url", "origin"])
|
|
1564
|
-
.output()
|
|
1565
|
-
.ok()?;
|
|
1566
|
-
if !output.status.success() {
|
|
1567
|
-
return None;
|
|
1568
|
-
}
|
|
1569
|
-
let url = String::from_utf8_lossy(&output.stdout);
|
|
1570
|
-
super::parse_repo_remote(url.trim(), &super::sigit_host())
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
1579
|
/// Fetch both Repo-tab lists on spawned tasks feeding `repo_rx` — the same
|
|
1574
1580
|
/// spawned-fetch pattern as the Cloud tab, so the render loop never blocks
|
|
1575
1581
|
/// on the network. Replacing the channel drops any in-flight fetch's
|
|
@@ -2125,6 +2131,11 @@ mod tui {
|
|
|
2125
2131
|
fn render(frame: &mut Frame, app: &mut App) {
|
|
2126
2132
|
let area = frame.area();
|
|
2127
2133
|
|
|
2134
|
+
if app.is_loading && app.load_error.is_none() {
|
|
2135
|
+
render_splash(frame, app, area);
|
|
2136
|
+
return;
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2128
2139
|
if app.is_loading {
|
|
2129
2140
|
let zones = Layout::vertical([
|
|
2130
2141
|
Constraint::Length(1),
|
|
@@ -2247,9 +2258,58 @@ mod tui {
|
|
|
2247
2258
|
}
|
|
2248
2259
|
}
|
|
2249
2260
|
|
|
2261
|
+
/// The launch splash: the wordmark with a caption under it. The startup
|
|
2262
|
+
/// checks still gate it, and their results land in the chat's info lines.
|
|
2263
|
+
/// Too small a terminal gets the caption without the art.
|
|
2264
|
+
fn render_splash(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) {
|
|
2265
|
+
let now = std::time::Instant::now();
|
|
2266
|
+
let reduced = app.splash.reduced();
|
|
2267
|
+
let tc = app.splash.truecolor;
|
|
2268
|
+
let t = if reduced {
|
|
2269
|
+
Duration::from_secs(2)
|
|
2270
|
+
} else {
|
|
2271
|
+
app.splash.elapsed(now)
|
|
2272
|
+
};
|
|
2273
|
+
let ms = t.as_millis() as f32;
|
|
2274
|
+
let exit = app.splash.exit_progress(now);
|
|
2275
|
+
let vis = 1.0 - exit;
|
|
2276
|
+
|
|
2277
|
+
// the wordmark, a gap, the caption
|
|
2278
|
+
let art = app
|
|
2279
|
+
.splash
|
|
2280
|
+
.banner
|
|
2281
|
+
.size(area.width)
|
|
2282
|
+
.filter(|(_, h)| area.height >= h + 2);
|
|
2283
|
+
let art_h = art.map_or(0, |(_, h)| h + 1);
|
|
2284
|
+
let total = art_h + 1;
|
|
2285
|
+
let top = area.y + area.height.saturating_sub(total) / 2;
|
|
2286
|
+
let rect = |y: u16, h: u16| ratatui::layout::Rect::new(area.x, y, area.width, h);
|
|
2287
|
+
|
|
2288
|
+
if let Some((_, h)) = art {
|
|
2289
|
+
frame.render_widget(
|
|
2290
|
+
Wordmark {
|
|
2291
|
+
banner: &app.splash.banner,
|
|
2292
|
+
t,
|
|
2293
|
+
exit,
|
|
2294
|
+
still: reduced,
|
|
2295
|
+
truecolor: tc,
|
|
2296
|
+
},
|
|
2297
|
+
rect(top, h),
|
|
2298
|
+
);
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
let caption_y = top + art_h;
|
|
2302
|
+
let caption = Paragraph::new(Line::from(Span::styled(
|
|
2303
|
+
"siGit Code & Deploy",
|
|
2304
|
+
Style::default().fg(splash::fade(splash::MUTED, vis * (ms / 300.0).min(1.0), tc)),
|
|
2305
|
+
)))
|
|
2306
|
+
.alignment(ratatui::layout::Alignment::Center);
|
|
2307
|
+
frame.render_widget(caption, rect(caption_y, 1));
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2250
2310
|
fn render_loading_title(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) {
|
|
2251
2311
|
const SPINNER: &[&str] = &["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
|
|
2252
|
-
let spin = SPINNER[(app.load_tick as usize) % SPINNER.len()];
|
|
2312
|
+
let spin = SPINNER[(app.load_tick as usize / 3) % SPINNER.len()];
|
|
2253
2313
|
let label = format!(" siGit {} loading {}… ", spin, app.load_model_name);
|
|
2254
2314
|
let line = Line::from(Span::styled(
|
|
2255
2315
|
label,
|
|
@@ -2443,22 +2503,6 @@ mod tui {
|
|
|
2443
2503
|
lines.push(Line::from(Span::styled(format!(" {entry_line}"), dim)));
|
|
2444
2504
|
}
|
|
2445
2505
|
}
|
|
2446
|
-
Role::Banner => {
|
|
2447
|
-
let palette = [
|
|
2448
|
-
Color::Red,
|
|
2449
|
-
Color::Yellow,
|
|
2450
|
-
Color::Green,
|
|
2451
|
-
Color::Cyan,
|
|
2452
|
-
Color::Blue,
|
|
2453
|
-
Color::Magenta,
|
|
2454
|
-
];
|
|
2455
|
-
let mut spans = Vec::new();
|
|
2456
|
-
for (i, ch) in msg.text.chars().enumerate() {
|
|
2457
|
-
let color = palette[i % palette.len()];
|
|
2458
|
-
spans.push(Span::styled(ch.to_string(), Style::default().fg(color)));
|
|
2459
|
-
}
|
|
2460
|
-
lines.push(Line::from(spans));
|
|
2461
|
-
}
|
|
2462
2506
|
Role::System => {
|
|
2463
2507
|
for text_line in msg.text.split('\n') {
|
|
2464
2508
|
let trimmed = text_line.trim();
|
|
@@ -3528,15 +3572,16 @@ mod tui {
|
|
|
3528
3572
|
{
|
|
3529
3573
|
let mut app = App::new(load_model_name, backend, program_status);
|
|
3530
3574
|
let mut event_stream = EventStream::new();
|
|
3575
|
+
let mut startup_rx = startup::spawn();
|
|
3531
3576
|
|
|
3532
|
-
//
|
|
3533
|
-
let mut ticker = interval(Duration::from_millis(
|
|
3577
|
+
// ~30 fps while the splash plays; the loading phase is the only user
|
|
3578
|
+
let mut ticker = interval(Duration::from_millis(33));
|
|
3534
3579
|
|
|
3535
3580
|
loop {
|
|
3536
3581
|
// ── Poll the loader channel (non-blocking) ────────────────────────
|
|
3537
3582
|
if app.is_loading {
|
|
3538
3583
|
match load_rx.try_recv() {
|
|
3539
|
-
Ok(Ok(())) => app.
|
|
3584
|
+
Ok(Ok(())) => app.load_ready = true,
|
|
3540
3585
|
Ok(Err(e)) => app.set_load_error(e),
|
|
3541
3586
|
Err(std_mpsc::TryRecvError::Empty) => {}
|
|
3542
3587
|
Err(std_mpsc::TryRecvError::Disconnected) => {
|
|
@@ -3545,6 +3590,24 @@ mod tui {
|
|
|
3545
3590
|
}
|
|
3546
3591
|
}
|
|
3547
3592
|
|
|
3593
|
+
// Startup checks report as they finish, and keep reporting after a
|
|
3594
|
+
// skip: the picker and the Repo tab fill in behind the chat.
|
|
3595
|
+
while let Ok(report) = startup_rx.try_recv() {
|
|
3596
|
+
app.apply_startup(report);
|
|
3597
|
+
}
|
|
3598
|
+
// The splash leaves once the backend is up and every check has
|
|
3599
|
+
// reported (and never before its minimum), or on a key press.
|
|
3600
|
+
if app.is_loading
|
|
3601
|
+
&& app.load_ready
|
|
3602
|
+
&& app.load_error.is_none()
|
|
3603
|
+
&& app
|
|
3604
|
+
.splash
|
|
3605
|
+
.update(std::time::Instant::now(), app.checks.all_done())
|
|
3606
|
+
== Phase::Done
|
|
3607
|
+
{
|
|
3608
|
+
app.finish_loading();
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3548
3611
|
// redraw every iteration
|
|
3549
3612
|
terminal.draw(|frame| render(frame, &mut app))?;
|
|
3550
3613
|
|
|
@@ -3637,7 +3700,7 @@ mod tui {
|
|
|
3637
3700
|
tokio::select! {
|
|
3638
3701
|
biased;
|
|
3639
3702
|
|
|
3640
|
-
// ──
|
|
3703
|
+
// ── Splash / spinner tick (loading phase only) ────────────────
|
|
3641
3704
|
_ = ticker.tick(), if app.is_loading => {
|
|
3642
3705
|
app.tick();
|
|
3643
3706
|
}
|
|
@@ -3800,6 +3863,9 @@ mod tui {
|
|
|
3800
3863
|
|| key.code == KeyCode::Char('d'))
|
|
3801
3864
|
{
|
|
3802
3865
|
app.quit = true;
|
|
3866
|
+
} else {
|
|
3867
|
+
// any other key skips the splash
|
|
3868
|
+
app.splash.skip();
|
|
3803
3869
|
}
|
|
3804
3870
|
}
|
|
3805
3871
|
continue;
|
|
@@ -30,7 +30,7 @@ use crate::{permissions, provider, session_store, settings, tools};
|
|
|
30
30
|
pub const USAGE: &str = "Usage: sigit run \"<prompt>\" [--cwd <dir>] [--add-dir <dir>]... \
|
|
31
31
|
[--output text|jsonl] [--quiet] [--resume <session-id>] \
|
|
32
32
|
[--allow-tool <name>]... [--deny-tool <name>]... \
|
|
33
|
-
[--max-tool-rounds <n>]\n \
|
|
33
|
+
[--max-tool-rounds <n>] [--model <tier>]\n \
|
|
34
34
|
sigit -p \"<prompt>\" [same options]";
|
|
35
35
|
|
|
36
36
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
|
@@ -67,6 +67,9 @@ pub struct HeadlessConfig {
|
|
|
67
67
|
/// asked for its final message. `--max-tool-rounds`, else
|
|
68
68
|
/// `SIGIT_MAX_TOOL_ROUNDS`, else the built-in cap.
|
|
69
69
|
pub max_tool_rounds: usize,
|
|
70
|
+
/// siGit Code Cloud tier named by `--model`. When set it wins over the
|
|
71
|
+
/// provider override and the local-inference setting.
|
|
72
|
+
pub cloud_tier: Option<&'static str>,
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
/// Upper bound for `--max-tool-rounds` / `SIGIT_MAX_TOOL_ROUNDS`.
|
|
@@ -147,6 +150,7 @@ pub fn parse_args(args: &[String]) -> Result<Option<HeadlessConfig>, String> {
|
|
|
147
150
|
let mut allow_tools: Vec<String> = Vec::new();
|
|
148
151
|
let mut deny_tools: Vec<String> = Vec::new();
|
|
149
152
|
let mut max_tool_rounds: Option<usize> = None;
|
|
153
|
+
let mut cloud_tier: Option<&'static str> = None;
|
|
150
154
|
|
|
151
155
|
let mut iter = args.iter().skip(usize::from(run_command));
|
|
152
156
|
while let Some(arg) = iter.next() {
|
|
@@ -211,6 +215,21 @@ pub fn parse_args(args: &[String]) -> Result<Option<HeadlessConfig>, String> {
|
|
|
211
215
|
.ok_or_else(|| "--max-tool-rounds requires a number".to_string())?;
|
|
212
216
|
max_tool_rounds = Some(parse_tool_rounds(value, "--max-tool-rounds")?);
|
|
213
217
|
}
|
|
218
|
+
"--model" => {
|
|
219
|
+
let value = iter
|
|
220
|
+
.next()
|
|
221
|
+
.ok_or_else(|| "--model requires a cloud tier".to_string())?;
|
|
222
|
+
if cloud_tier.is_some() {
|
|
223
|
+
return Err("--model was given more than once".to_string());
|
|
224
|
+
}
|
|
225
|
+
let tier = provider::cloud_tier_named(value).ok_or_else(|| {
|
|
226
|
+
format!(
|
|
227
|
+
"unknown cloud tier: {value} (expected one of: {})",
|
|
228
|
+
provider::CLOUD_TIERS.join(", ")
|
|
229
|
+
)
|
|
230
|
+
})?;
|
|
231
|
+
cloud_tier = Some(tier);
|
|
232
|
+
}
|
|
214
233
|
// A mistyped flag must not become the prompt. A prompt that
|
|
215
234
|
// really starts with '-' can still go through -p.
|
|
216
235
|
other if other.starts_with('-') => return Err(format!("unknown argument: {other}")),
|
|
@@ -258,6 +277,7 @@ pub fn parse_args(args: &[String]) -> Result<Option<HeadlessConfig>, String> {
|
|
|
258
277
|
allow_tools,
|
|
259
278
|
deny_tools,
|
|
260
279
|
max_tool_rounds,
|
|
280
|
+
cloud_tier,
|
|
261
281
|
}))
|
|
262
282
|
}
|
|
263
283
|
|
|
@@ -312,22 +332,32 @@ pub async fn run(config: HeadlessConfig) -> i32 {
|
|
|
312
332
|
// first, else the signed-in cloud tier when local inference is off (what
|
|
313
333
|
// `apply_startup_inference_mode` does at every ACP session entry). There is
|
|
314
334
|
// never an implicit on-device load — a fresh process has no model in memory
|
|
315
|
-
// and headless mode must not silently download gigabytes.
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
335
|
+
// and headless mode must not silently download gigabytes. `--model` is the
|
|
336
|
+
// exception to that order: a tier named on the command line wins over the
|
|
337
|
+
// override and the setting, the way a flag wins over the environment.
|
|
338
|
+
let provider_cfg = match config.cloud_tier {
|
|
339
|
+
Some(tier) => provider::cloud_tier_provider(tier),
|
|
340
|
+
None => provider::active_provider().or_else(|| {
|
|
341
|
+
if settings::local_inference_enabled() {
|
|
342
|
+
None
|
|
343
|
+
} else {
|
|
344
|
+
provider::cloud_tier_provider(provider::DEFAULT_CLOUD_TIER)
|
|
345
|
+
}
|
|
346
|
+
}),
|
|
347
|
+
};
|
|
323
348
|
let Some(cfg) = provider_cfg else {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
349
|
+
let message = match config.cloud_tier {
|
|
350
|
+
Some(tier) => format!(
|
|
351
|
+
"--model {tier} needs a siGit Code Cloud session. Sign in with `sigit login`."
|
|
352
|
+
),
|
|
353
|
+
None => "headless mode needs a remote inference provider — running on-device would \
|
|
354
|
+
require loading (and possibly downloading) a local model, which headless \
|
|
355
|
+
mode never does implicitly. Set OPENAI_BASE_URL and OPENAI_API_KEY (or \
|
|
356
|
+
configure providers.toml), or sign in with `sigit login` and turn local \
|
|
357
|
+
inference off."
|
|
358
|
+
.to_string(),
|
|
359
|
+
};
|
|
360
|
+
emit_error(&config, &message);
|
|
331
361
|
return 1;
|
|
332
362
|
};
|
|
333
363
|
|
|
@@ -814,6 +844,32 @@ mod tests {
|
|
|
814
844
|
assert!(resolve_max_tool_rounds(None, Some("0")).is_err());
|
|
815
845
|
}
|
|
816
846
|
|
|
847
|
+
#[test]
|
|
848
|
+
fn model_flag_names_a_cloud_tier() {
|
|
849
|
+
let config = parse_args(&args(&["run", "go"])).unwrap().unwrap();
|
|
850
|
+
assert_eq!(config.cloud_tier, None);
|
|
851
|
+
let config = parse_args(&args(&["run", "go", "--model", "prism"]))
|
|
852
|
+
.unwrap()
|
|
853
|
+
.unwrap();
|
|
854
|
+
assert_eq!(config.cloud_tier, Some("prism"));
|
|
855
|
+
// The wire id works too, so a model id copied from logs is accepted.
|
|
856
|
+
let config = parse_args(&args(&["-p", "go", "--model", "onde-zenith"]))
|
|
857
|
+
.unwrap()
|
|
858
|
+
.unwrap();
|
|
859
|
+
assert_eq!(config.cloud_tier, Some("zenith"));
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
#[test]
|
|
863
|
+
fn model_flag_rejects_unknown_and_repeated_tiers() {
|
|
864
|
+
let error = parse_args(&args(&["run", "go", "--model", "gpt-4o"])).unwrap_err();
|
|
865
|
+
assert!(error.starts_with("unknown cloud tier: gpt-4o"), "{error}");
|
|
866
|
+
assert!(error.contains("prism"), "{error}");
|
|
867
|
+
assert_eq!(
|
|
868
|
+
parse_args(&args(&["run", "go", "--model", "apex", "--model", "flare"])),
|
|
869
|
+
Err("--model was given more than once".to_string())
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
|
|
817
873
|
#[test]
|
|
818
874
|
fn wrap_up_prompt_names_the_cap_only_when_it_was_reached() {
|
|
819
875
|
let capped = wrap_up_prompt(true, 24);
|
|
@@ -877,6 +933,7 @@ mod tests {
|
|
|
877
933
|
assert!(parse_args(&args(&["-p", "x", "--deny-tool"])).is_err());
|
|
878
934
|
assert!(parse_args(&args(&["run", "x", "--output"])).is_err());
|
|
879
935
|
assert!(parse_args(&args(&["run", "x", "--resume"])).is_err());
|
|
936
|
+
assert!(parse_args(&args(&["run", "x", "--model"])).is_err());
|
|
880
937
|
}
|
|
881
938
|
|
|
882
939
|
#[test]
|
|
@@ -51,6 +51,10 @@ mod session_store;
|
|
|
51
51
|
mod settings;
|
|
52
52
|
mod setup;
|
|
53
53
|
mod skills;
|
|
54
|
+
#[cfg(unix)]
|
|
55
|
+
mod splash;
|
|
56
|
+
#[cfg(unix)]
|
|
57
|
+
mod startup;
|
|
54
58
|
mod subagents;
|
|
55
59
|
mod tools;
|
|
56
60
|
mod workspace;
|
|
@@ -6922,9 +6926,9 @@ async fn main() -> anyhow::Result<()> {
|
|
|
6922
6926
|
let (tty, cleanup_tty) = redirect_output_to_log()?;
|
|
6923
6927
|
init_logging(true);
|
|
6924
6928
|
setup::setup_shared_model_cache();
|
|
6925
|
-
//
|
|
6926
|
-
//
|
|
6927
|
-
|
|
6929
|
+
// MCP discovery (incl. the official server) runs behind the launch
|
|
6930
|
+
// splash (`startup.rs`), so a slow server never leaves the terminal
|
|
6931
|
+
// blank before the first frame.
|
|
6928
6932
|
run_interactive(tty, cleanup_tty).await
|
|
6929
6933
|
}
|
|
6930
6934
|
#[cfg(not(unix))]
|