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.
- {sigit_code-1.7.5 → sigit_code-1.7.7}/Cargo.lock +2 -2
- {sigit_code-1.7.5 → sigit_code-1.7.7}/Cargo.toml +2 -2
- {sigit_code-1.7.5 → sigit_code-1.7.7}/PKG-INFO +1 -1
- sigit_code-1.7.7/crates/cli/README.md +90 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/account.rs +43 -11
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/chat.rs +171 -87
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/headless.rs +73 -16
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/main.rs +460 -92
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/mcp.rs +19 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/permissions.rs +3 -1
- {sigit_code-1.7.5 → 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.5 → sigit_code-1.7.7}/crates/cli/src/tools.rs +350 -11
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_permissions.rs +73 -1
- sigit_code-1.7.5/crates/cli/README.md +0 -234
- {sigit_code-1.7.5 → sigit_code-1.7.7}/README.md +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/Cargo.toml +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/backend.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/client_fs.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/client_terminal.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/commands.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/frontmatter.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/harness_markup.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/hooks.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/inline_tool_calls.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/instructions.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/models.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/program_status.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/repetition.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/session_store.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/settings.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/setup.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/skills.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/subagents.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/src/workspace.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_audio.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_client_fs.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_client_terminal.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_cloud_reauth.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_concurrent_sessions.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_elicitation.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_embedded_context.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_endpoint_errors.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_error_codes.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_harness_markup.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_images.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_logout.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_multi_root.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_session_list.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_session_load.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_stop_reasons.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_tool_call_name.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/acp_tool_stdin.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/headless_mode.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/cli/tests/version_flag.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/sigit-agent/Cargo.toml +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/sigit-agent/README.md +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/sigit-agent/assets/sign-in-logo.png +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/sigit-agent/src/auth.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/sigit-agent/src/browser_auth.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/sigit-agent/src/credentials.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/sigit-agent/src/lib.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/sigit-agent/src/profile.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/sigit-agent/src/prompt.rs +0 -0
- {sigit_code-1.7.5 → sigit_code-1.7.7}/crates/sigit-agent/tests/embedded.rs +0 -0
- {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.
|
|
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"] }
|
|
@@ -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
|
-
///
|
|
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
|
}
|
|
@@ -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
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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:
|
|
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:
|
|
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
|
-
///
|
|
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
|
-
//
|
|
3526
|
-
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));
|
|
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.
|
|
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
|
-
// ──
|
|
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 [
|