sigit-code 1.7.0__tar.gz → 1.7.2__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.0 → sigit_code-1.7.2}/Cargo.lock +2 -2
- {sigit_code-1.7.0 → sigit_code-1.7.2}/Cargo.toml +2 -2
- {sigit_code-1.7.0 → sigit_code-1.7.2}/PKG-INFO +1 -1
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/Cargo.toml +1 -1
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/README.md +2 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/src/chat.rs +145 -9
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/src/headless.rs +8 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/src/inline_tool_calls.rs +307 -36
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/src/main.rs +84 -27
- sigit_code-1.7.2/crates/cli/src/program_status.rs +149 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/src/setup.rs +117 -102
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/src/tools.rs +119 -66
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/tests/headless_mode.rs +72 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/sigit-agent/Cargo.toml +1 -0
- sigit_code-1.7.2/crates/sigit-agent/README.md +70 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/README.md +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/src/account.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/src/backend.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/src/client_fs.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/src/client_terminal.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/src/commands.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/src/frontmatter.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/src/harness_markup.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/src/hooks.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/src/instructions.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/src/mcp.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/src/models.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/src/permissions.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/src/provider.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/src/repetition.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/src/session_store.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/src/settings.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/src/skills.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/src/subagents.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/src/workspace.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/tests/acp_audio.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/tests/acp_client_fs.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/tests/acp_client_terminal.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/tests/acp_concurrent_sessions.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/tests/acp_elicitation.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/tests/acp_embedded_context.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/tests/acp_endpoint_errors.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/tests/acp_error_codes.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/tests/acp_harness_markup.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/tests/acp_images.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/tests/acp_logout.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/tests/acp_multi_root.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/tests/acp_permissions.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/tests/acp_session_list.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/tests/acp_session_load.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/tests/acp_stop_reasons.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/tests/acp_tool_call_name.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/tests/acp_tool_stdin.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/cli/tests/version_flag.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/sigit-agent/src/auth.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/sigit-agent/src/browser_auth.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/sigit-agent/src/credentials.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/sigit-agent/src/lib.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/sigit-agent/src/profile.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/sigit-agent/src/prompt.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/crates/sigit-agent/tests/embedded.rs +0 -0
- {sigit_code-1.7.0 → sigit_code-1.7.2}/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.2"
|
|
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.2"
|
|
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.2"
|
|
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.2", 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"] }
|
|
@@ -8,7 +8,7 @@ license.workspace = true
|
|
|
8
8
|
repository.workspace = true
|
|
9
9
|
homepage.workspace = true
|
|
10
10
|
readme = "README.md"
|
|
11
|
-
keywords = ["cli", "ai", "coding-agent", "llm", "acp"
|
|
11
|
+
keywords = ["cli", "ai", "coding-agent", "llm", "acp"]
|
|
12
12
|
categories = ["command-line-utilities"]
|
|
13
13
|
authors.workspace = true
|
|
14
14
|
# `src/bin/` holds test-only helper binaries (the MCP stdio stub used by the
|
|
@@ -157,6 +157,8 @@ Run `sigit` in a terminal and you get the same model and system prompt as the ed
|
|
|
157
157
|
|
|
158
158
|
Terminal mode currently needs Unix terminal behavior, so it works on macOS and Linux only.
|
|
159
159
|
|
|
160
|
+
The interactive UI emits [OSC 7501 program status](https://www.superlogical.com/rex/docs/build/program-status) reports so supporting terminals can show working, waiting for permission or input, browser sign-in, completion, and errors across tabs. User-driven quit reports idle; terminals expire active work on process exit and retain completion or error reports. Status sequences are never written to headless/JSON or ACP output.
|
|
161
|
+
|
|
160
162
|
## Headless and CI mode
|
|
161
163
|
|
|
162
164
|
Use `sigit run` to execute the same agent from scripts, CI, or another siGit Factory client:
|
|
@@ -519,6 +519,7 @@ mod tui {
|
|
|
519
519
|
use crate::models::{
|
|
520
520
|
InferenceKind, ModelCacheHealth, ModelPickerItem, ModelSource, build_model_picker_items,
|
|
521
521
|
};
|
|
522
|
+
use crate::program_status::{Reporter, Status};
|
|
522
523
|
use crate::session_store::SessionEntry;
|
|
523
524
|
use ratatui::{
|
|
524
525
|
Frame,
|
|
@@ -731,6 +732,8 @@ mod tui {
|
|
|
731
732
|
// ── App state ─────────────────────────────────────────────────────────────
|
|
732
733
|
|
|
733
734
|
struct App {
|
|
735
|
+
program_status: Reporter,
|
|
736
|
+
outcome: Status,
|
|
734
737
|
messages: Vec<ChatMessage>,
|
|
735
738
|
input: String,
|
|
736
739
|
cursor: usize,
|
|
@@ -855,7 +858,11 @@ mod tui {
|
|
|
855
858
|
}
|
|
856
859
|
|
|
857
860
|
impl App {
|
|
858
|
-
fn new(
|
|
861
|
+
fn new(
|
|
862
|
+
load_model_name: String,
|
|
863
|
+
backend: Arc<dyn InferenceBackend>,
|
|
864
|
+
program_status: Reporter,
|
|
865
|
+
) -> Self {
|
|
859
866
|
let is_remote = backend.is_remote();
|
|
860
867
|
let items = build_model_picker_items();
|
|
861
868
|
let tool_calling = items
|
|
@@ -890,6 +897,8 @@ mod tui {
|
|
|
890
897
|
model_load_cancelled: false,
|
|
891
898
|
switching_model_id: None,
|
|
892
899
|
download_progress: None,
|
|
900
|
+
program_status,
|
|
901
|
+
outcome: Status::Idle,
|
|
893
902
|
is_loading: true,
|
|
894
903
|
load_tick: 0,
|
|
895
904
|
load_error: None,
|
|
@@ -959,6 +968,34 @@ mod tui {
|
|
|
959
968
|
self.history_sessions.get(self.history_index)
|
|
960
969
|
}
|
|
961
970
|
|
|
971
|
+
fn finish_login(&mut self, result: Result<String, String>) {
|
|
972
|
+
self.outcome = if result.is_ok() {
|
|
973
|
+
Status::Done
|
|
974
|
+
} else {
|
|
975
|
+
Status::Error
|
|
976
|
+
};
|
|
977
|
+
self.program_status.report(self.outcome);
|
|
978
|
+
self.messages.push(ChatMessage::system(
|
|
979
|
+
result.unwrap_or_else(|message| message),
|
|
980
|
+
));
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
fn status(&self) -> Status {
|
|
984
|
+
if self.quit {
|
|
985
|
+
Status::Idle
|
|
986
|
+
} else if self.load_error.is_some() {
|
|
987
|
+
Status::Error
|
|
988
|
+
} else if self.pending_approval.is_some() {
|
|
989
|
+
Status::Permission
|
|
990
|
+
} else if self.is_loading || self.is_busy() || self.inference_rx.is_some() {
|
|
991
|
+
Status::Working
|
|
992
|
+
} else if self.show_model_picker || self.history_pending_delete.is_some() {
|
|
993
|
+
Status::Question
|
|
994
|
+
} else {
|
|
995
|
+
self.outcome
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
962
999
|
fn is_busy(&self) -> bool {
|
|
963
1000
|
self.is_streaming() || self.thinking || self.switching_model
|
|
964
1001
|
}
|
|
@@ -1000,11 +1037,13 @@ mod tui {
|
|
|
1000
1037
|
}
|
|
1001
1038
|
|
|
1002
1039
|
fn start_thinking(&mut self) {
|
|
1040
|
+
self.outcome = Status::Idle;
|
|
1003
1041
|
self.thinking = true;
|
|
1004
1042
|
self.thinking_tick = 0;
|
|
1005
1043
|
}
|
|
1006
1044
|
|
|
1007
1045
|
fn stop_thinking(&mut self) {
|
|
1046
|
+
self.outcome = Status::Idle;
|
|
1008
1047
|
self.thinking = false;
|
|
1009
1048
|
self.inference_rx = None;
|
|
1010
1049
|
// Dropping a pending reply channel reads as a denial on the
|
|
@@ -1634,6 +1673,10 @@ mod tui {
|
|
|
1634
1673
|
};
|
|
1635
1674
|
if app.history_pending_delete.as_deref() == Some(id.as_str()) {
|
|
1636
1675
|
crate::session_store::delete(&id);
|
|
1676
|
+
// A plan must not outlive its conversation. The id is
|
|
1677
|
+
// the live session's key when the user deletes the
|
|
1678
|
+
// thread the TUI is currently running.
|
|
1679
|
+
crate::tools::clear_todos(&id);
|
|
1637
1680
|
app.refresh_history();
|
|
1638
1681
|
app.history_notice = Some(format!("Deleted session '{id}'."));
|
|
1639
1682
|
} else {
|
|
@@ -2837,6 +2880,8 @@ mod tui {
|
|
|
2837
2880
|
crate::permissions::reset_session(crate::permissions::TUI_SESSION);
|
|
2838
2881
|
// The saved session must not resurrect what the user just wiped.
|
|
2839
2882
|
crate::session_store::delete(TUI_STORE_SESSION);
|
|
2883
|
+
// The plan belongs to the conversation, so the wipe drops it too.
|
|
2884
|
+
crate::tools::clear_todos(crate::permissions::TUI_SESSION);
|
|
2840
2885
|
app.context_tokens_used = None;
|
|
2841
2886
|
app.messages.push(ChatMessage::system(format!(
|
|
2842
2887
|
"Cleared {cleared} turn(s). History is empty.",
|
|
@@ -2984,6 +3029,7 @@ mod tui {
|
|
|
2984
3029
|
)));
|
|
2985
3030
|
}
|
|
2986
3031
|
None => {
|
|
3032
|
+
app.outcome = Status::Auth;
|
|
2987
3033
|
app.messages.push(ChatMessage::system(
|
|
2988
3034
|
"siGit Code Cloud needs an account. Use \
|
|
2989
3035
|
`/login <email> <password>`, or create one at sigit.si.",
|
|
@@ -3035,13 +3081,22 @@ mod tui {
|
|
|
3035
3081
|
}
|
|
3036
3082
|
},
|
|
3037
3083
|
SlashCommand::Login(arg) => {
|
|
3038
|
-
let
|
|
3084
|
+
let password_login = arg
|
|
3085
|
+
.as_deref()
|
|
3086
|
+
.and_then(crate::account::parse_login_args)
|
|
3087
|
+
.is_some();
|
|
3088
|
+
app.program_status.report(if password_login {
|
|
3089
|
+
Status::Working
|
|
3090
|
+
} else {
|
|
3091
|
+
Status::Auth
|
|
3092
|
+
});
|
|
3093
|
+
let result = match arg.as_deref().and_then(crate::account::parse_login_args) {
|
|
3039
3094
|
Some((email, password)) => {
|
|
3040
3095
|
match crate::account::authenticate(&email, &password).await {
|
|
3041
|
-
Ok(email) => format!(
|
|
3096
|
+
Ok(email) => Ok(format!(
|
|
3042
3097
|
"Signed in as {email}. siGit Code Cloud applies to your next session."
|
|
3043
|
-
),
|
|
3044
|
-
Err(error) => format!("Login failed: {error}"),
|
|
3098
|
+
)),
|
|
3099
|
+
Err(error) => Err(format!("Login failed: {error}")),
|
|
3045
3100
|
}
|
|
3046
3101
|
}
|
|
3047
3102
|
// A bare `/login` opens the browser instead of asking for
|
|
@@ -3053,7 +3108,7 @@ mod tui {
|
|
|
3053
3108
|
crate::browser_login_message().await
|
|
3054
3109
|
}
|
|
3055
3110
|
};
|
|
3056
|
-
app.
|
|
3111
|
+
app.finish_login(result);
|
|
3057
3112
|
}
|
|
3058
3113
|
SlashCommand::Logout => {
|
|
3059
3114
|
let message = crate::account::end_session().await;
|
|
@@ -3430,13 +3485,22 @@ mod tui {
|
|
|
3430
3485
|
backend: Arc<dyn InferenceBackend>,
|
|
3431
3486
|
load_rx: std_mpsc::Receiver<Result<(), String>>,
|
|
3432
3487
|
load_model_name: String,
|
|
3488
|
+
program_status: Reporter,
|
|
3433
3489
|
) -> Result<()>
|
|
3434
3490
|
where
|
|
3435
3491
|
// ratatui 0.30 gives each backend its own error type; `?` into anyhow needs it
|
|
3436
3492
|
// to be thread-safe.
|
|
3437
3493
|
B::Error: Send + Sync + 'static,
|
|
3438
3494
|
{
|
|
3439
|
-
event_loop(
|
|
3495
|
+
event_loop(
|
|
3496
|
+
terminal,
|
|
3497
|
+
engine,
|
|
3498
|
+
backend,
|
|
3499
|
+
load_rx,
|
|
3500
|
+
load_model_name,
|
|
3501
|
+
program_status,
|
|
3502
|
+
)
|
|
3503
|
+
.await
|
|
3440
3504
|
}
|
|
3441
3505
|
|
|
3442
3506
|
async fn event_loop<B: ratatui::backend::Backend>(
|
|
@@ -3445,13 +3509,14 @@ mod tui {
|
|
|
3445
3509
|
backend: Arc<dyn InferenceBackend>,
|
|
3446
3510
|
load_rx: std_mpsc::Receiver<Result<(), String>>,
|
|
3447
3511
|
load_model_name: String,
|
|
3512
|
+
program_status: Reporter,
|
|
3448
3513
|
) -> Result<()>
|
|
3449
3514
|
where
|
|
3450
3515
|
// ratatui 0.30 gives each backend its own error type; `?` into anyhow needs it
|
|
3451
3516
|
// to be thread-safe.
|
|
3452
3517
|
B::Error: Send + Sync + 'static,
|
|
3453
3518
|
{
|
|
3454
|
-
let mut app = App::new(load_model_name, backend);
|
|
3519
|
+
let mut app = App::new(load_model_name, backend, program_status);
|
|
3455
3520
|
let mut event_stream = EventStream::new();
|
|
3456
3521
|
|
|
3457
3522
|
// 10 fps is plenty for spinners
|
|
@@ -3476,6 +3541,7 @@ mod tui {
|
|
|
3476
3541
|
if let Some(rx) = app.model_load_rx.as_mut() {
|
|
3477
3542
|
match rx.try_recv() {
|
|
3478
3543
|
Ok(ModelLoadUpdate::Loaded(model_name)) => {
|
|
3544
|
+
app.outcome = Status::Done;
|
|
3479
3545
|
engine.clear_history().await;
|
|
3480
3546
|
app.context_tokens_used = None;
|
|
3481
3547
|
if let Some(tc) = app.pending_tool_calling.take() {
|
|
@@ -3521,6 +3587,7 @@ mod tui {
|
|
|
3521
3587
|
}
|
|
3522
3588
|
}
|
|
3523
3589
|
Ok(ModelLoadUpdate::Error(error)) => {
|
|
3590
|
+
app.outcome = Status::Error;
|
|
3524
3591
|
app.switching_model = false;
|
|
3525
3592
|
app.switching_model_id = None;
|
|
3526
3593
|
app.download_progress = None;
|
|
@@ -3538,6 +3605,7 @@ mod tui {
|
|
|
3538
3605
|
app.model_load_cancelled = false;
|
|
3539
3606
|
app.model_load_rx = None;
|
|
3540
3607
|
if !was_cancelled {
|
|
3608
|
+
app.outcome = Status::Error;
|
|
3541
3609
|
app.messages.push(ChatMessage::system(
|
|
3542
3610
|
"error loading model: loader task disconnected".to_string(),
|
|
3543
3611
|
));
|
|
@@ -3546,6 +3614,10 @@ mod tui {
|
|
|
3546
3614
|
}
|
|
3547
3615
|
}
|
|
3548
3616
|
|
|
3617
|
+
// Polling model loads can change state immediately before select
|
|
3618
|
+
// goes idle, so report after applying those updates.
|
|
3619
|
+
app.program_status.report(app.status());
|
|
3620
|
+
|
|
3549
3621
|
if app.quit {
|
|
3550
3622
|
break;
|
|
3551
3623
|
}
|
|
@@ -3593,6 +3665,7 @@ mod tui {
|
|
|
3593
3665
|
}
|
|
3594
3666
|
Some(InferenceUpdate::Response(text)) => {
|
|
3595
3667
|
app.stop_thinking();
|
|
3668
|
+
app.outcome = Status::Done;
|
|
3596
3669
|
app.messages.push(ChatMessage::assistant(text));
|
|
3597
3670
|
}
|
|
3598
3671
|
Some(InferenceUpdate::ContextUsage(tokens)) => {
|
|
@@ -3601,6 +3674,7 @@ mod tui {
|
|
|
3601
3674
|
Some(InferenceUpdate::Error(msg)) => {
|
|
3602
3675
|
app.finalize_stream();
|
|
3603
3676
|
app.stop_thinking();
|
|
3677
|
+
app.outcome = Status::Error;
|
|
3604
3678
|
app.messages.push(ChatMessage::system(format!("error: {msg}")));
|
|
3605
3679
|
}
|
|
3606
3680
|
Some(InferenceUpdate::ApprovalRequest { tool, args, reply }) => {
|
|
@@ -3621,6 +3695,7 @@ mod tui {
|
|
|
3621
3695
|
// task finished, possibly with no text to show
|
|
3622
3696
|
app.finalize_stream();
|
|
3623
3697
|
app.stop_thinking();
|
|
3698
|
+
app.outcome = Status::Done;
|
|
3624
3699
|
}
|
|
3625
3700
|
}
|
|
3626
3701
|
}
|
|
@@ -3703,6 +3778,9 @@ mod tui {
|
|
|
3703
3778
|
};
|
|
3704
3779
|
|
|
3705
3780
|
if let Event::Key(key) = event {
|
|
3781
|
+
if key.kind == KeyEventKind::Press {
|
|
3782
|
+
app.outcome = Status::Idle;
|
|
3783
|
+
}
|
|
3706
3784
|
// loading phase — only quit keys work
|
|
3707
3785
|
if app.is_loading {
|
|
3708
3786
|
if key.kind == KeyEventKind::Press {
|
|
@@ -3810,6 +3888,10 @@ mod tui {
|
|
|
3810
3888
|
app.stop_thinking();
|
|
3811
3889
|
app.messages.push(ChatMessage::system("(cancelled)"));
|
|
3812
3890
|
}
|
|
3891
|
+
// A visible stream can have cleared `thinking`;
|
|
3892
|
+
// cancellation must drop its receiver and clear
|
|
3893
|
+
// thinking so the tab cannot stay on working.
|
|
3894
|
+
app.stop_thinking();
|
|
3813
3895
|
if app.switching_model {
|
|
3814
3896
|
// flag before drop so Disconnected handler stays quiet
|
|
3815
3897
|
app.model_load_cancelled = true;
|
|
@@ -3947,8 +4029,62 @@ mod tui {
|
|
|
3947
4029
|
}
|
|
3948
4030
|
#[cfg(test)]
|
|
3949
4031
|
mod tui_tests {
|
|
3950
|
-
use super::{
|
|
4032
|
+
use super::{
|
|
4033
|
+
App, OpenAiBackend, Status, compact_tokens, context_gauge_color, context_gauge_label,
|
|
4034
|
+
};
|
|
3951
4035
|
use ratatui::style::Color;
|
|
4036
|
+
use std::sync::Arc;
|
|
4037
|
+
|
|
4038
|
+
#[test]
|
|
4039
|
+
fn login_status_uses_the_result_instead_of_message_wording() {
|
|
4040
|
+
let backend = Arc::new(OpenAiBackend::new("http://localhost/v1", "", "test", None));
|
|
4041
|
+
let mut app = App::new(
|
|
4042
|
+
"test".into(),
|
|
4043
|
+
backend,
|
|
4044
|
+
super::Reporter::new(std::io::sink()),
|
|
4045
|
+
);
|
|
4046
|
+
app.finish_loading();
|
|
4047
|
+
app.finish_login(Ok("Welcome back.".into()));
|
|
4048
|
+
assert_eq!(app.status(), Status::Done);
|
|
4049
|
+
app.finish_login(Err("Signed in could not be completed.".into()));
|
|
4050
|
+
assert_eq!(app.status(), Status::Error);
|
|
4051
|
+
}
|
|
4052
|
+
|
|
4053
|
+
#[test]
|
|
4054
|
+
fn program_status_tracks_loading_approval_completion_and_cancellation() {
|
|
4055
|
+
let backend = Arc::new(OpenAiBackend::new("http://localhost/v1", "", "test", None));
|
|
4056
|
+
let mut app = App::new(
|
|
4057
|
+
"test".into(),
|
|
4058
|
+
backend,
|
|
4059
|
+
super::Reporter::new(std::io::sink()),
|
|
4060
|
+
);
|
|
4061
|
+
assert_eq!(app.status(), Status::Working);
|
|
4062
|
+
app.finish_loading();
|
|
4063
|
+
assert_eq!(app.status(), Status::Idle);
|
|
4064
|
+
app.start_thinking();
|
|
4065
|
+
assert_eq!(app.status(), Status::Working);
|
|
4066
|
+
let (reply, _) = tokio::sync::oneshot::channel();
|
|
4067
|
+
app.pending_approval = Some(("edit_file".into(), reply));
|
|
4068
|
+
assert_eq!(app.status(), Status::Permission);
|
|
4069
|
+
app.pending_approval = None;
|
|
4070
|
+
assert_eq!(app.status(), Status::Working);
|
|
4071
|
+
app.stop_thinking();
|
|
4072
|
+
assert_eq!(app.status(), Status::Idle);
|
|
4073
|
+
app.outcome = Status::Done;
|
|
4074
|
+
assert_eq!(app.status(), Status::Done);
|
|
4075
|
+
app.outcome = Status::Error;
|
|
4076
|
+
assert_eq!(app.status(), Status::Error);
|
|
4077
|
+
app.start_thinking();
|
|
4078
|
+
assert_eq!(app.status(), Status::Working);
|
|
4079
|
+
app.stop_thinking();
|
|
4080
|
+
app.show_model_picker = true;
|
|
4081
|
+
assert_eq!(app.status(), Status::Question);
|
|
4082
|
+
app.show_model_picker = false;
|
|
4083
|
+
app.set_load_error("failed".into());
|
|
4084
|
+
assert_eq!(app.status(), Status::Error);
|
|
4085
|
+
app.quit = true;
|
|
4086
|
+
assert_eq!(app.status(), Status::Idle);
|
|
4087
|
+
}
|
|
3952
4088
|
|
|
3953
4089
|
#[test]
|
|
3954
4090
|
fn context_gauge_reports_share_of_the_window() {
|
|
@@ -526,6 +526,14 @@ async fn run_prompt(
|
|
|
526
526
|
|
|
527
527
|
let allow_tool_calls = round < config.max_tool_rounds;
|
|
528
528
|
|
|
529
|
+
// The round that reaches the cap is forced to answer in text. Say so,
|
|
530
|
+
// or the model spends its last reply narrating the step it can no
|
|
531
|
+
// longer take.
|
|
532
|
+
if !allow_tool_calls && let Some(last) = tool_results.last_mut() {
|
|
533
|
+
last.content
|
|
534
|
+
.push_str(&crate::headless_round_cap_note(config.max_tool_rounds));
|
|
535
|
+
}
|
|
536
|
+
|
|
529
537
|
// Whatever this round says starts a new paragraph rather than
|
|
530
538
|
// continuing the sentence the tool calls interrupted.
|
|
531
539
|
reply.interrupt();
|