sigit-code 1.7.5__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.
Files changed (67) hide show
  1. {sigit_code-1.7.5 → sigit_code-1.7.7}/Cargo.lock +2 -2
  2. {sigit_code-1.7.5 → sigit_code-1.7.7}/Cargo.toml +2 -2
  3. {sigit_code-1.7.5 → sigit_code-1.7.7}/PKG-INFO +1 -1
  4. sigit_code-1.7.7/crates/cli/README.md +90 -0
  5. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/account.rs +43 -11
  6. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/chat.rs +171 -87
  7. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/headless.rs +73 -16
  8. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/main.rs +460 -92
  9. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/mcp.rs +19 -0
  10. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/permissions.rs +3 -1
  11. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/provider.rs +24 -0
  12. sigit_code-1.7.7/crates/cli/src/splash.rs +551 -0
  13. sigit_code-1.7.7/crates/cli/src/startup.rs +238 -0
  14. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/tools.rs +350 -11
  15. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_permissions.rs +73 -1
  16. sigit_code-1.7.5/crates/cli/README.md +0 -234
  17. {sigit_code-1.7.5 → sigit_code-1.7.7}/README.md +0 -0
  18. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/Cargo.toml +0 -0
  19. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/backend.rs +0 -0
  20. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/client_fs.rs +0 -0
  21. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/client_terminal.rs +0 -0
  22. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/commands.rs +0 -0
  23. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/frontmatter.rs +0 -0
  24. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/harness_markup.rs +0 -0
  25. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/hooks.rs +0 -0
  26. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/inline_tool_calls.rs +0 -0
  27. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/instructions.rs +0 -0
  28. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/models.rs +0 -0
  29. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/program_status.rs +0 -0
  30. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/repetition.rs +0 -0
  31. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/session_store.rs +0 -0
  32. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/settings.rs +0 -0
  33. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/setup.rs +0 -0
  34. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/skills.rs +0 -0
  35. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/subagents.rs +0 -0
  36. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/workspace.rs +0 -0
  37. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_audio.rs +0 -0
  38. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_client_fs.rs +0 -0
  39. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_client_terminal.rs +0 -0
  40. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_cloud_reauth.rs +0 -0
  41. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_concurrent_sessions.rs +0 -0
  42. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_elicitation.rs +0 -0
  43. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_embedded_context.rs +0 -0
  44. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_endpoint_errors.rs +0 -0
  45. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_error_codes.rs +0 -0
  46. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_harness_markup.rs +0 -0
  47. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_images.rs +0 -0
  48. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_logout.rs +0 -0
  49. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_multi_root.rs +0 -0
  50. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_session_list.rs +0 -0
  51. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_session_load.rs +0 -0
  52. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_stop_reasons.rs +0 -0
  53. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_tool_call_name.rs +0 -0
  54. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_tool_stdin.rs +0 -0
  55. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/headless_mode.rs +0 -0
  56. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/version_flag.rs +0 -0
  57. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/sigit-agent/Cargo.toml +0 -0
  58. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/sigit-agent/README.md +0 -0
  59. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/sigit-agent/assets/sign-in-logo.png +0 -0
  60. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/sigit-agent/src/auth.rs +0 -0
  61. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/sigit-agent/src/browser_auth.rs +0 -0
  62. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/sigit-agent/src/credentials.rs +0 -0
  63. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/sigit-agent/src/lib.rs +0 -0
  64. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/sigit-agent/src/profile.rs +0 -0
  65. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/sigit-agent/src/prompt.rs +0 -0
  66. {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/sigit-agent/tests/embedded.rs +0 -0
  67. {sigit_code-1.7.5 → 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.5"
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.5"
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.5"
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.5", default-features = false, path = "crates/sigit-agent" }
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"] }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sigit-code
3
- Version: 1.7.5
3
+ Version: 1.7.7
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -0,0 +1,90 @@
1
+ <p align="center">
2
+ <img src="docs/assets/logo.png" alt="siGit Code" width="96">
3
+ </p>
4
+
5
+ <h1 align="center">siGit Code</h1>
6
+
7
+ <p align="center"><strong>The coding agent that runs where your code lives.</strong></p>
8
+ <p align="center">A modular on-device inference LLM coding agent. Core component of siGit Factory.</p>
9
+
10
+ <p align="center">
11
+ <a href="https://crates.io/crates/sigit"><img src="https://img.shields.io/crates/v/sigit?style=flat-square&labelColor=17211D&color=235843" alt="Crates.io"></a>
12
+ <a href="https://pypi.org/project/sigit-code/"><img src="https://img.shields.io/pypi/v/sigit-code?style=flat-square&labelColor=17211D&color=235843" alt="PyPI"></a>
13
+ <a href="https://www.npmjs.com/package/@getsigit/sigit"><img src="https://img.shields.io/npm/v/@getsigit/sigit?style=flat-square&labelColor=17211D&color=235843" alt="npm"></a>
14
+ <a href="https://snapcraft.io/sigit"><img src="https://snapcraft.io/sigit/badge.svg" alt="sigit"></a>
15
+ <a href="https://sigit.si/code"><img src="https://img.shields.io/badge/sigit.si%2Fcode-235843?style=flat-square&labelColor=17211D" alt="Website"></a>
16
+ <a href="https://github.com/getsigit/sigit/blob/main/LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-235843?style=flat-square&labelColor=17211D" alt="License"></a>
17
+ </p>
18
+
19
+ <p align="center">
20
+ <a href="https://www.nuget.org/packages/SiGit.Code"><img src="https://img.shields.io/nuget/v/SiGit.Code?style=flat-square&labelColor=17211D&color=235843&label=nuget" alt="NuGet"></a>
21
+ <a href="https://github.com/microsoft/winget-pkgs/tree/master/manifests/g/getSigit/siGitCode"><img src="https://img.shields.io/winget/v/getSigit.siGitCode?style=flat-square&labelColor=17211D&color=235843" alt="winget"></a>
22
+ <a href="https://marketplace.visualstudio.com/items?itemName=getsigit.sigit-code"><img src="https://img.shields.io/badge/VS%20Code-Marketplace-235843?style=flat-square&labelColor=17211D" alt="VS Code Marketplace"></a>
23
+ <a href="https://pypi.org/project/sigit-code/"><img src="https://img.shields.io/pypi/dm/sigit-code?style=flat-square&labelColor=17211D&color=235843&label=pypi%20downloads" alt="PyPI downloads"></a>
24
+ <a href="https://github.com/getsigit/sigit/releases"><img src="https://img.shields.io/github/v/release/getsigit/sigit?style=flat-square&labelColor=17211D&color=235843" alt="Release"></a>
25
+ <a href="https://github.com/getsigit/sigit/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/getsigit/sigit/ci.yml?style=flat-square&labelColor=17211D&label=ci" alt="CI"></a>
26
+ <a href="https://github.com/getsigit/sigit/releases"><img src="https://img.shields.io/github/downloads/getsigit/sigit/total?style=flat-square&labelColor=17211D&color=235843" alt="GitHub downloads"></a>
27
+ </p>
28
+
29
+ siGit Code runs on your machine and works in any codebase. Inference runs on-device by default through the [Onde Inference on-device inference SDK](https://ondeinference.com/sdk), or on siGit Code Cloud when you choose it. You need no API key to start, and it works with any editor that speaks the [Agent Client Protocol](https://agentclientprotocol.com). The agent is Apache-2.0. siGit Code Cloud and [siGit Factory](https://factory.sigit.si/) are the hosted, paid side.
30
+
31
+ ## Install
32
+
33
+ ```sh
34
+ cargo install sigit
35
+ brew tap getsigit/tap && brew trust --tap getsigit/tap && brew install sigit
36
+ npm install -g @getsigit/sigit
37
+ ```
38
+
39
+ More ways, including pip, NuGet, winget, Scoop and Linux packages, are in [docs/install.md](docs/install.md).
40
+
41
+ ## Get siGit anywhere
42
+
43
+ The command-line agent and the desktop and mobile app are separate products. Each cell links to where it ships.
44
+
45
+ | Desktop and mobile apps | macOS | Windows | Linux | iOS | Android |
46
+ |---|:-:|:-:|:-:|:-:|:-:|
47
+ | [App Store](https://apps.apple.com/app/id6753018849) | [✓](https://apps.apple.com/app/id6753018849) | | | [✓](https://apps.apple.com/app/id6753018849) | |
48
+ | [Google Play](https://play.google.com/store/apps/details?id=xyz.smbcloud.SiGit) | | | | | [✓](https://play.google.com/store/apps/details?id=xyz.smbcloud.SiGit) |
49
+ | [Microsoft Store](https://apps.microsoft.com/detail/9p69ph1k7nxx) | | [✓](https://apps.microsoft.com/detail/9p69ph1k7nxx) | | | |
50
+ | [Snap Store](https://snapcraft.io/sigit) | | | [✓](https://snapcraft.io/sigit) | | |
51
+ | [Direct download](https://getsigit.5mb.app/download) | [✓](https://getsigit.5mb.app/download) | [✓](https://getsigit.5mb.app/download) | [✓](https://getsigit.5mb.app/download) | | |
52
+
53
+ | Command line | macOS | Windows | Linux |
54
+ |---|:-:|:-:|:-:|
55
+ | [crates.io](https://crates.io/crates/sigit) | ✓ | ✓ | ✓ |
56
+ | [Homebrew](docs/install.md) | ✓ | | |
57
+ | [npm](https://www.npmjs.com/package/@getsigit/sigit) | ✓ | ✓ | ✓ |
58
+ | [PyPI and uv](https://pypi.org/project/sigit-code/) | ✓ | ✓ | ✓ |
59
+ | [NuGet](https://www.nuget.org/packages/SiGit.Code) | ✓ | ✓ | ✓ |
60
+ | [winget](docs/install.md) | | ✓ | |
61
+ | [Scoop](https://github.com/getsigit/scoop-bucket) | | ✓ | |
62
+ | [deb and rpm](https://github.com/getsigit/sigit/releases) | | | ✓ |
63
+
64
+ `sudo snap install sigit` installs the desktop app, not the terminal agent. The app currently uses siGit Code Cloud for inference, while the command line runs models on-device. Details are in [docs/distribution.md](docs/distribution.md).
65
+
66
+ Every channel, kept current: https://getsigit.5mb.app/
67
+
68
+ ## Use it your way
69
+
70
+ - **In your editor.** Any ACP client starts `sigit --acp`. See [ACP support](docs/acp.md).
71
+ - **In the terminal.** Run `sigit` for a full-screen chat UI. See [terminal mode](docs/terminal.md).
72
+ - **In scripts and CI.** `sigit run "<prompt>"` gives you JSONL events and exit codes. See [headless mode](docs/headless.md).
73
+
74
+ Setup guides for Zed, Xcode, VS Code and the JetBrains IDEs are in [docs/editors.md](docs/editors.md).
75
+
76
+ ## Docs
77
+
78
+ [Install](docs/install.md), [editors](docs/editors.md), [ACP](docs/acp.md), [terminal](docs/terminal.md), [headless](docs/headless.md), [hooks](docs/hooks.md), [MCP](docs/mcp.md), [telemetry](docs/telemetry.md), [distribution](docs/distribution.md). The index is [docs/README.md](docs/README.md).
79
+
80
+ ## Contributing
81
+
82
+ Issues and pull requests are welcome. Open one against `development`.
83
+
84
+ ## License
85
+
86
+ [Apache 2.0](https://github.com/getsigit/sigit/blob/main/LICENSE)
87
+
88
+ ## Copyright
89
+
90
+ © 2026 [siGit Code & Deploy](https://sigit.si).
@@ -129,14 +129,32 @@ pub async fn end_session() -> String {
129
129
  }
130
130
  }
131
131
 
132
- /// One-line description of the current session, verifying the token if reachable.
133
- pub async fn status_line() -> String {
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 "Not signed in. Use `/login` to use siGit Code Cloud.".to_string();
149
+ return SessionState::SignedOut;
136
150
  };
137
151
 
138
152
  let url = format!("{}/api/v1/user", api_base().trim_end_matches('/'));
139
- match reqwest::Client::new()
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
- format!("Signed in to siGit Code Cloud as {email}.")
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
- Ok(response) => format!(
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
  }
@@ -167,6 +167,13 @@ pub(crate) fn tool_title(name: &str, arguments_json: &str) -> String {
167
167
  str_arg("path")
168
168
  }
169
169
  "search_files" | "glob" => str_arg("pattern"),
170
+ "command_output" | "monitor_command" | "kill_command" => {
171
+ let id = args.as_ref().and_then(|a| a.get("task_id")?.as_u64());
172
+ id.map(|id| match crate::tools::background_task_command(id) {
173
+ Some(command) => format!("task {id} (`{command}`)"),
174
+ None => format!("task {id}"),
175
+ })
176
+ }
170
177
  _ => None,
171
178
  };
172
179
  (name.to_string(), summary)
@@ -513,6 +520,7 @@ mod tui {
513
520
  use onde::inference::{ChatEngine, SamplingConfig};
514
521
 
515
522
  use super::{RepoItem, Tab};
523
+ use crate::account::SessionState;
516
524
  use crate::backend::{
517
525
  InferenceBackend, LocalBackend, OpenAiBackend, TokenChunk, ToolResult, ToolSpec,
518
526
  };
@@ -521,6 +529,10 @@ mod tui {
521
529
  };
522
530
  use crate::program_status::{Reporter, Status};
523
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
+ };
524
536
  use ratatui::{
525
537
  Frame,
526
538
  layout::{Constraint, Layout, Position},
@@ -538,8 +550,6 @@ mod tui {
538
550
  User,
539
551
  Assistant,
540
552
  System,
541
- /// rainbow-colored banner art
542
- Banner,
543
553
  /// a collapsible tool-call entry (`text` holds the title line)
544
554
  Tool,
545
555
  }
@@ -589,16 +599,6 @@ mod tui {
589
599
  }
590
600
  }
591
601
 
592
- fn banner(text: impl Into<String>) -> Self {
593
- Self {
594
- role: Role::Banner,
595
- text: text.into(),
596
- think_block: None,
597
- tool_detail: None,
598
- tool_result: None,
599
- }
600
- }
601
-
602
602
  fn tool_call(title: impl Into<String>, detail: impl Into<String>) -> Self {
603
603
  Self {
604
604
  role: Role::Tool,
@@ -764,6 +764,13 @@ mod tui {
764
764
  load_error: Option<String>,
765
765
  load_start: Instant,
766
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>,
767
774
 
768
775
  // ── Model picker state ────────────────────────────────────────────────
769
776
  show_model_picker: bool,
@@ -825,21 +832,6 @@ mod tui {
825
832
  repo_rx: Option<mpsc::UnboundedReceiver<RepoUpdate>>,
826
833
  }
827
834
 
828
- const BANNER_ART: &str = "\
829
- 77777777777777777777777777777777777777777777777777777777777777777777777777777777777777777777
830
- 77777777322222222222222222222222222222223777389969902208431358831999699051111177777777777777
831
- 1111111125555555555555555555555511113222311159 5002 088 3081771691111111111111
832
- 1111111111111111111111111111131136841 1482853332007 05 9043332891 400811111111111
833
- 1111111111111111111111111111111201 109 304 40 00 79 100041111111111
834
- 333333255555555555555555555552392 102 503 90 7000000005 903 0000023333333333
835
- 333333245454545454545454545433381 7600000 302 61 780 109 20009533333333333
836
- 3333333333333333333333333333333402 7001 08 761 202 902 90003333333333333
837
- 2222255555555555555555555555250899901 49 304 403 08 108 300042222222222222
838
- 2222222222222222222222222222269 106 03 901 06 505 402 000052222222222222
839
- 2222255555555555555555555555299 708 1002 80 00 90852222222222222
840
- 55555555555555555555555555555560953258000866660000051140866908666600008966900065555555555555
841
- 88888888888888888888888888888888888888888888888888888888888888888888888888888888888888888888";
842
-
843
835
  const THINKING_FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
844
836
 
845
837
  fn rich_text_spans(text: &str, base_style: Style, bold_style: Style) -> Vec<Span<'static>> {
@@ -863,21 +855,10 @@ mod tui {
863
855
  backend: Arc<dyn InferenceBackend>,
864
856
  program_status: Reporter,
865
857
  ) -> Self {
866
- let is_remote = backend.is_remote();
867
- let items = build_model_picker_items();
868
- let tool_calling = items
869
- .iter()
870
- .find(|m| m.display_name == load_model_name)
871
- .map(|m| m.tool_calling)
872
- .unwrap_or(true);
873
- // For a remote provider the passed-in name is authoritative; the
874
- // persisted local selection must not override it (or the title would
875
- // show an on-device model while requests go to the cloud).
876
- let current_model_name = if is_remote {
877
- load_model_name.clone()
878
- } else {
879
- crate::setup::load_selected_model_name().unwrap_or_else(|| load_model_name.clone())
880
- };
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.
881
862
  Self {
882
863
  messages: Vec::new(),
883
864
  input: String::new(),
@@ -904,11 +885,15 @@ mod tui {
904
885
  load_error: None,
905
886
  load_start: Instant::now(),
906
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,
907
892
  show_model_picker: false,
908
893
  model_picker_index: 0,
909
- model_picker_items: items,
910
- current_model_name,
911
- tool_calling,
894
+ model_picker_items: Vec::new(),
895
+ current_model_name: load_model_name.clone(),
896
+ tool_calling: true,
912
897
  context_tokens_used: None,
913
898
  show_thinking: false,
914
899
  tools_expanded: false,
@@ -920,7 +905,7 @@ mod tui {
920
905
  history_notice: None,
921
906
  cloud_lines: None,
922
907
  cloud_rx: None,
923
- repo: detect_sigit_repo(),
908
+ repo: None,
924
909
  repo_section: RepoSection::Issues,
925
910
  repo_issues: RepoData::NotFetched,
926
911
  repo_prs: RepoData::NotFetched,
@@ -1101,16 +1086,54 @@ mod tui {
1101
1086
  self.download_progress = Some((downloaded, expected));
1102
1087
  }
1103
1088
 
1104
- /// switch to chat phase and show the welcome banner
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
1105
1124
  fn finish_loading(&mut self) {
1106
1125
  self.is_loading = false;
1107
- for line in BANNER_ART.lines() {
1108
- self.messages.push(ChatMessage::banner(line));
1109
- }
1110
- self.messages.push(ChatMessage::system(""));
1111
1126
  self.messages.push(ChatMessage::system(
1112
1127
  "In this world, nothing can be said to be certain, except death and taxes. ~ Pak Sigit",
1113
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
+ }
1114
1137
  if self.backend.is_remote() {
1115
1138
  self.messages.push(ChatMessage::system(format!(
1116
1139
  "Current model: {}",
@@ -1124,6 +1147,11 @@ mod tui {
1124
1147
  self.current_model_name
1125
1148
  )));
1126
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
+ }
1127
1155
  self.messages
1128
1156
  .push(ChatMessage::system("Type /help for commands."));
1129
1157
  }
@@ -1548,21 +1576,6 @@ mod tui {
1548
1576
 
1549
1577
  // ── Repo tab (issues / pull requests via the official MCP server) ─────────
1550
1578
 
1551
- /// `owner/name` when the current directory's `origin` remote lives on the
1552
- /// sigit.si host (per `SIGIT_API_URL`). Runs `git remote get-url origin`
1553
- /// once at startup; any failure (no git, no repo, no origin) → `None`.
1554
- fn detect_sigit_repo() -> Option<String> {
1555
- let output = std::process::Command::new("git")
1556
- .args(["remote", "get-url", "origin"])
1557
- .output()
1558
- .ok()?;
1559
- if !output.status.success() {
1560
- return None;
1561
- }
1562
- let url = String::from_utf8_lossy(&output.stdout);
1563
- super::parse_repo_remote(url.trim(), &super::sigit_host())
1564
- }
1565
-
1566
1579
  /// Fetch both Repo-tab lists on spawned tasks feeding `repo_rx` — the same
1567
1580
  /// spawned-fetch pattern as the Cloud tab, so the render loop never blocks
1568
1581
  /// on the network. Replacing the channel drops any in-flight fetch's
@@ -2118,6 +2131,11 @@ mod tui {
2118
2131
  fn render(frame: &mut Frame, app: &mut App) {
2119
2132
  let area = frame.area();
2120
2133
 
2134
+ if app.is_loading && app.load_error.is_none() {
2135
+ render_splash(frame, app, area);
2136
+ return;
2137
+ }
2138
+
2121
2139
  if app.is_loading {
2122
2140
  let zones = Layout::vertical([
2123
2141
  Constraint::Length(1),
@@ -2240,9 +2258,58 @@ mod tui {
2240
2258
  }
2241
2259
  }
2242
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
+
2243
2310
  fn render_loading_title(frame: &mut Frame, app: &App, area: ratatui::layout::Rect) {
2244
2311
  const SPINNER: &[&str] = &["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
2245
- let spin = SPINNER[(app.load_tick as usize) % SPINNER.len()];
2312
+ let spin = SPINNER[(app.load_tick as usize / 3) % SPINNER.len()];
2246
2313
  let label = format!(" siGit {} loading {}… ", spin, app.load_model_name);
2247
2314
  let line = Line::from(Span::styled(
2248
2315
  label,
@@ -2436,22 +2503,6 @@ mod tui {
2436
2503
  lines.push(Line::from(Span::styled(format!(" {entry_line}"), dim)));
2437
2504
  }
2438
2505
  }
2439
- Role::Banner => {
2440
- let palette = [
2441
- Color::Red,
2442
- Color::Yellow,
2443
- Color::Green,
2444
- Color::Cyan,
2445
- Color::Blue,
2446
- Color::Magenta,
2447
- ];
2448
- let mut spans = Vec::new();
2449
- for (i, ch) in msg.text.chars().enumerate() {
2450
- let color = palette[i % palette.len()];
2451
- spans.push(Span::styled(ch.to_string(), Style::default().fg(color)));
2452
- }
2453
- lines.push(Line::from(spans));
2454
- }
2455
2506
  Role::System => {
2456
2507
  for text_line in msg.text.split('\n') {
2457
2508
  let trimmed = text_line.trim();
@@ -3521,15 +3572,16 @@ mod tui {
3521
3572
  {
3522
3573
  let mut app = App::new(load_model_name, backend, program_status);
3523
3574
  let mut event_stream = EventStream::new();
3575
+ let mut startup_rx = startup::spawn();
3524
3576
 
3525
- // 10 fps is plenty for spinners
3526
- let mut ticker = interval(Duration::from_millis(100));
3577
+ // ~30 fps while the splash plays; the loading phase is the only user
3578
+ let mut ticker = interval(Duration::from_millis(33));
3527
3579
 
3528
3580
  loop {
3529
3581
  // ── Poll the loader channel (non-blocking) ────────────────────────
3530
3582
  if app.is_loading {
3531
3583
  match load_rx.try_recv() {
3532
- Ok(Ok(())) => app.finish_loading(),
3584
+ Ok(Ok(())) => app.load_ready = true,
3533
3585
  Ok(Err(e)) => app.set_load_error(e),
3534
3586
  Err(std_mpsc::TryRecvError::Empty) => {}
3535
3587
  Err(std_mpsc::TryRecvError::Disconnected) => {
@@ -3538,6 +3590,24 @@ mod tui {
3538
3590
  }
3539
3591
  }
3540
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
+
3541
3611
  // redraw every iteration
3542
3612
  terminal.draw(|frame| render(frame, &mut app))?;
3543
3613
 
@@ -3630,7 +3700,7 @@ mod tui {
3630
3700
  tokio::select! {
3631
3701
  biased;
3632
3702
 
3633
- // ── Spinner tick (loading phase only) ─────────────────────────
3703
+ // ── Splash / spinner tick (loading phase only) ────────────────
3634
3704
  _ = ticker.tick(), if app.is_loading => {
3635
3705
  app.tick();
3636
3706
  }
@@ -3793,6 +3863,9 @@ mod tui {
3793
3863
  || key.code == KeyCode::Char('d'))
3794
3864
  {
3795
3865
  app.quit = true;
3866
+ } else {
3867
+ // any other key skips the splash
3868
+ app.splash.skip();
3796
3869
  }
3797
3870
  }
3798
3871
  continue;
@@ -4145,6 +4218,17 @@ mod tests {
4145
4218
  );
4146
4219
  }
4147
4220
 
4221
+ #[test]
4222
+ fn tool_title_task_tools_name_the_task() {
4223
+ for name in ["command_output", "monitor_command", "kill_command"] {
4224
+ assert_eq!(
4225
+ tool_title(name, r#"{"task_id":999999}"#),
4226
+ format!("{name} · task 999999")
4227
+ );
4228
+ }
4229
+ assert_eq!(tool_title("monitor_command", "{}"), "monitor_command");
4230
+ }
4231
+
4148
4232
  #[test]
4149
4233
  fn tool_title_file_tools_show_the_path() {
4150
4234
  for name in [