Skip to content

Commit 4affc48

Browse files
authored
Merge pull request #70 from 7df-lab/dev/tool0516
Add diff, improve ui.
2 parents eb999c6 + 2882a62 commit 4affc48

41 files changed

Lines changed: 7398 additions & 1952 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060

6161
- [Installation](#-installation)
6262
- [Quick Start](#-quick-start)
63+
- [Configuration](#%EF%B8%8F-configuration)
6364
- [FAQ](#-faq)
6465
- [Contributing](#-contributing)
6566
- [References](#-references)

crates/cli/src/agent_command.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use devo_core::ModelCatalog;
66
use devo_core::PresetModelCatalog;
77
use devo_core::ProviderConfigFile;
88
use devo_core::ResolvedProviderSettings;
9+
use devo_core::SessionId;
910
use devo_core::load_config;
1011
use devo_core::project_config_key;
1112
use devo_core::resolve_provider_settings;
@@ -23,7 +24,11 @@ use devo_utils::find_devo_home;
2324
/// when a provider config already exists. `log_level` is forwarded to the
2425
/// background server process, and `model_override` replaces the resolved model
2526
/// for this session without mutating the stored provider config.
26-
pub(crate) async fn run_agent(force_onboarding: bool, log_level: Option<&str>) -> Result<()> {
27+
pub(crate) async fn run_agent(
28+
force_onboarding: bool,
29+
log_level: Option<&str>,
30+
initial_session_id: Option<SessionId>,
31+
) -> Result<devo_tui::AppExit> {
2732
let cwd = std::env::current_dir()?;
2833
let config_home = find_devo_home().context("could not determine devo home directory")?;
2934
let model_catalog = PresetModelCatalog::load_from_config(&config_home, Some(&cwd))?;
@@ -55,6 +60,7 @@ pub(crate) async fn run_agent(force_onboarding: bool, log_level: Option<&str>) -
5560
run_interactive_tui(InteractiveTuiConfig {
5661
// initial_session corresponding fields at top of `config.toml`.
5762
initial_session: InitialTuiSession {
63+
session_id: initial_session_id,
5864
model,
5965
provider: wire_api,
6066
thinking_selection: model_thinking_selection,
@@ -68,7 +74,6 @@ pub(crate) async fn run_agent(force_onboarding: bool, log_level: Option<&str>) -
6874
show_model_onboarding: onboarding_mode,
6975
})
7076
.await
71-
.map(|_| ())
7277
}
7378

7479
/// Resolves the initial provider settings and whether onboarding should be shown.

crates/cli/src/main.rs

Lines changed: 165 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use devo_core::AppConfigLoader;
88
use devo_core::FileSystemAppConfigLoader;
99
use devo_core::LoggingBootstrap;
1010
use devo_core::LoggingRuntime;
11+
use devo_core::SessionId;
1112
use devo_core::UpdateCheckOutcome;
1213
use devo_core::UpdateChecker;
1314
use devo_core::format_update_notification;
@@ -52,6 +53,86 @@ fn main() -> Result<()> {
5253
devo_arg0::run_as(|_paths| async { run_cli().await })
5354
}
5455

56+
fn format_with_separators(value: usize) -> String {
57+
let digits = value.to_string();
58+
let mut out = String::new();
59+
for (index, ch) in digits.chars().rev().enumerate() {
60+
if index > 0 && index % 3 == 0 {
61+
out.push(',');
62+
}
63+
out.push(ch);
64+
}
65+
out.chars().rev().collect()
66+
}
67+
68+
fn format_token_usage_line(exit: &devo_tui::AppExit, color_enabled: bool) -> Option<String> {
69+
let total = exit.total_input_tokens + exit.total_output_tokens;
70+
let non_cached_input = exit
71+
.total_input_tokens
72+
.saturating_sub(exit.total_cache_read_tokens);
73+
if total == 0 && exit.total_cache_read_tokens == 0 {
74+
return None;
75+
}
76+
let total_value = format_with_separators(total);
77+
let input_value = format_with_separators(non_cached_input);
78+
let output_value = format_with_separators(exit.total_output_tokens);
79+
let cached_suffix = if exit.total_cache_read_tokens > 0 {
80+
let cached_value = format_with_separators(exit.total_cache_read_tokens);
81+
if color_enabled {
82+
format!(
83+
" (+ {} {})",
84+
"\u{1b}[1;33m".to_string() + &cached_value + "\u{1b}[0m",
85+
"\u{1b}[33mcached\u{1b}[0m"
86+
)
87+
} else {
88+
format!(" (+ {cached_value} cached)")
89+
}
90+
} else {
91+
String::new()
92+
};
93+
Some(format!(
94+
"Token usage: total={} input={}{} output={}",
95+
if color_enabled {
96+
format!("\u{1b}[1;36m{total_value}\u{1b}[0m")
97+
} else {
98+
total_value
99+
},
100+
if color_enabled {
101+
format!("\u{1b}[1;32m{input_value}\u{1b}[0m")
102+
} else {
103+
input_value
104+
},
105+
cached_suffix,
106+
if color_enabled {
107+
format!("\u{1b}[1;35m{output_value}\u{1b}[0m")
108+
} else {
109+
output_value
110+
},
111+
))
112+
}
113+
114+
fn exit_messages(exit: &devo_tui::AppExit, color_enabled: bool) -> Vec<String> {
115+
let mut lines = Vec::new();
116+
if let Some(line) = format_token_usage_line(exit, color_enabled) {
117+
lines.push(line);
118+
}
119+
if let Some(session_id) = exit.session_id {
120+
let command = format!("devo resume {session_id}");
121+
let command = if color_enabled {
122+
format!("\u{1b}[1;36m{command}\u{1b}[0m")
123+
} else {
124+
command
125+
};
126+
let prefix = if color_enabled {
127+
"\u{1b}[2mTo continue this session, run\u{1b}[0m".to_string()
128+
} else {
129+
"To continue this session, run".to_string()
130+
};
131+
lines.push(format!("{prefix} {command}"));
132+
}
133+
lines
134+
}
135+
55136
async fn run_cli() -> Result<()> {
56137
let cli = Cli::parse();
57138
let log_level = cli.log_level.map(|level| level.to_string());
@@ -62,7 +143,11 @@ async fn run_cli() -> Result<()> {
62143
// Resolve logging config early, install the process-wide file subscriber,
63144
// and keep its non-blocking writer guard alive for the command lifetime.
64145
let _logging = install_logging(&cli)?;
65-
run_agent(/*force_onboarding*/ true, log_level.as_deref()).await
146+
let exit = run_agent(/*force_onboarding*/ true, log_level.as_deref(), None).await?;
147+
for line in exit_messages(&exit, /*color_enabled*/ true) {
148+
println!("{line}");
149+
}
150+
Ok(())
66151
}
67152
Some(Command::Prompt { input }) => {
68153
maybe_print_startup_update(&cli).await;
@@ -73,6 +158,20 @@ async fn run_cli() -> Result<()> {
73158
let _logging = install_logging(&cli)?;
74159
run_doctor().await
75160
}
161+
Some(Command::Resume { session_id }) => {
162+
maybe_print_startup_update(&cli).await;
163+
let _logging = install_logging(&cli)?;
164+
let exit = run_agent(
165+
/*force_onboarding*/ false,
166+
log_level.as_deref(),
167+
Some(*session_id),
168+
)
169+
.await?;
170+
for line in exit_messages(&exit, /*color_enabled*/ true) {
171+
println!("{line}");
172+
}
173+
Ok(())
174+
}
76175
Some(Command::Server {
77176
working_root,
78177
transport,
@@ -87,7 +186,11 @@ async fn run_cli() -> Result<()> {
87186
None => {
88187
maybe_print_startup_update(&cli).await;
89188
let _logging = install_logging(&cli)?;
90-
run_agent(/*force_onboarding*/ false, log_level.as_deref()).await
189+
let exit = run_agent(/*force_onboarding*/ false, log_level.as_deref(), None).await?;
190+
for line in exit_messages(&exit, /*color_enabled*/ true) {
191+
println!("{line}");
192+
}
193+
Ok(())
91194
}
92195
}
93196
}
@@ -96,6 +199,11 @@ async fn run_cli() -> Result<()> {
96199
enum Command {
97200
/// Launch the interactive onboarding flow to configure a model provider.
98201
Onboard,
202+
/// Resume a saved interactive session by id.
203+
Resume {
204+
/// Session identifier printed by Devo at exit time.
205+
session_id: SessionId,
206+
},
99207
/// Send a single prompt to the model and print the response (non-interactive).
100208
Prompt {
101209
/// The prompt text to send to the model.
@@ -193,12 +301,15 @@ fn cli_logging_overrides(cli: &Cli) -> toml::Value {
193301
#[cfg(test)]
194302
mod tests {
195303
use clap::Parser;
304+
use devo_core::SessionId;
196305
use pretty_assertions::assert_eq;
197306
use tracing_subscriber::filter::LevelFilter;
198307

199308
use super::Cli;
200309
use super::Command;
201310
use super::cli_logging_overrides;
311+
use super::exit_messages;
312+
use super::format_token_usage_line;
202313

203314
#[test]
204315
fn cli_parses_supported_log_levels() {
@@ -328,4 +439,56 @@ mod tests {
328439
false
329440
);
330441
}
442+
443+
#[test]
444+
fn cli_parses_resume_subcommand() {
445+
let session_id = SessionId::new();
446+
let cli =
447+
Cli::try_parse_from(["devo", "resume", &session_id.to_string()]).expect("parse resume");
448+
449+
match cli.command {
450+
Some(Command::Resume { session_id: actual }) => assert_eq!(actual, session_id),
451+
other => panic!("expected resume command, got {other:?}"),
452+
}
453+
}
454+
455+
#[test]
456+
fn exit_messages_includes_usage_and_resume_hint() {
457+
let session_id = SessionId::new();
458+
let exit = devo_tui::AppExit {
459+
session_id: Some(session_id),
460+
turn_count: 1,
461+
total_input_tokens: 10,
462+
total_output_tokens: 2,
463+
total_cache_read_tokens: 5,
464+
};
465+
466+
let lines = exit_messages(&exit, /*color_enabled*/ false);
467+
assert_eq!(
468+
lines[0],
469+
"Token usage: total=12 input=5 (+ 5 cached) output=2"
470+
);
471+
assert_eq!(
472+
lines[1],
473+
format!("To continue this session, run devo resume {session_id}")
474+
);
475+
}
476+
477+
#[test]
478+
fn colorized_exit_messages_include_ansi_sequences() {
479+
let session_id = SessionId::new();
480+
let exit = devo_tui::AppExit {
481+
session_id: Some(session_id),
482+
turn_count: 1,
483+
total_input_tokens: 10,
484+
total_output_tokens: 2,
485+
total_cache_read_tokens: 5,
486+
};
487+
488+
let usage = format_token_usage_line(&exit, /*color_enabled*/ true).expect("usage line");
489+
assert!(usage.contains("\u{1b}["));
490+
491+
let lines = exit_messages(&exit, /*color_enabled*/ true);
492+
assert!(lines[1].contains("\u{1b}["));
493+
}
331494
}

0 commit comments

Comments
 (0)