Skip to content

Commit 79063c1

Browse files
authored
Merge pull request #106 from 7df-lab/dev/deep_research_0614
Add /research command for deep research
2 parents 2e59a71 + 49f65f0 commit 79063c1

228 files changed

Lines changed: 21242 additions & 2707 deletions

File tree

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: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/arg0/src/lib.rs

Lines changed: 99 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
//! }
1818
//! ```
1919
20+
use std::ffi::OsString;
2021
use std::future::Future;
2122
use std::io::ErrorKind;
2223
use std::path::Path;
@@ -27,6 +28,7 @@ use anyhow::Result;
2728

2829
/// Alias name for the server sub‑binary.
2930
const SERVER_ALIAS: &str = "devo-server";
31+
const ALIAS_SENTINEL_PREFIX: &str = "--devo-alias=";
3032

3133
/// Directory (under DEVO_HOME/tmp/arg0) where alias entries are created.
3234
const ALIAS_TEMP_ROOT: &str = "arg0";
@@ -111,7 +113,15 @@ where
111113
/// Returns the alias name if the process was invoked through one of our
112114
/// symlinks or batch scripts, or `None` for a normal `devo` invocation.
113115
fn argv0_alias() -> Option<&'static str> {
114-
let argv0 = std::env::args_os().next().unwrap_or_default();
116+
argv0_alias_from_args(std::env::args_os())
117+
}
118+
119+
fn argv0_alias_from_args<I>(args: I) -> Option<&'static str>
120+
where
121+
I: IntoIterator<Item = OsString>,
122+
{
123+
let mut args = args.into_iter();
124+
let argv0 = args.next().unwrap_or_default();
115125
let file_name = Path::new(&argv0)
116126
.file_stem()
117127
.and_then(|s| s.to_str())
@@ -123,19 +133,39 @@ fn argv0_alias() -> Option<&'static str> {
123133
}
124134

125135
// Windows batch scripts pass the intended alias via a sentinel arg.
126-
let mut args = std::env::args_os();
127-
args.next(); // skip argv[0]
128-
if let Some(sentinel) = args.next()
129-
&& let Some(s) = sentinel.to_str()
130-
&& let Some(rest) = s.strip_prefix("--devo-alias=")
131-
&& rest == SERVER_ALIAS
132-
{
136+
if args.next().as_deref().is_some_and(is_server_alias_sentinel) {
133137
return Some(SERVER_ALIAS);
134138
}
135139

136140
None
137141
}
138142

143+
fn is_server_alias_sentinel(arg: &std::ffi::OsStr) -> bool {
144+
arg.to_str()
145+
.and_then(|s| s.strip_prefix(ALIAS_SENTINEL_PREFIX))
146+
.is_some_and(|rest| rest == SERVER_ALIAS)
147+
}
148+
149+
fn server_dispatch_args() -> Vec<OsString> {
150+
server_dispatch_args_from(std::env::args_os())
151+
}
152+
153+
fn server_dispatch_args_from<I>(args: I) -> Vec<OsString>
154+
where
155+
I: IntoIterator<Item = OsString>,
156+
{
157+
let mut args = args.into_iter();
158+
let mut filtered = vec![args.next().unwrap_or_else(|| OsString::from(SERVER_ALIAS))];
159+
160+
if let Some(first_arg) = args.next()
161+
&& !is_server_alias_sentinel(&first_arg)
162+
{
163+
filtered.push(first_arg);
164+
}
165+
filtered.extend(args);
166+
filtered
167+
}
168+
139169
/// Build a multi‑thread Tokio runtime.
140170
fn build_runtime() -> Result<tokio::runtime::Runtime> {
141171
let mut builder = tokio::runtime::Builder::new_multi_thread();
@@ -150,7 +180,7 @@ fn build_runtime() -> Result<tokio::runtime::Runtime> {
150180
/// consumed by `argv0_alias()` so the remaining args are the server's own.
151181
async fn run_server_dispatch() {
152182
use clap::Parser;
153-
let args = devo_server::ServerProcessArgs::parse();
183+
let args = devo_server::ServerProcessArgs::parse_from(server_dispatch_args());
154184
let home_dir = match devo_util_paths::find_devo_home() {
155185
Ok(d) => d,
156186
Err(err) => {
@@ -208,7 +238,13 @@ where
208238
I: IntoIterator<Item = std::result::Result<(String, String), dotenvy::Error>>,
209239
{
210240
for (key, value) in iter.into_iter().flatten() {
211-
if !key.to_ascii_uppercase().starts_with(ILLEGAL_ENV_VAR_PREFIX) {
241+
// Avoid allocating an uppercased copy for every dotenv key; the
242+
// reserved prefix is ASCII, so byte-wise case folding is equivalent.
243+
let has_illegal_prefix = key
244+
.as_bytes()
245+
.get(..ILLEGAL_ENV_VAR_PREFIX.len())
246+
.is_some_and(|prefix| prefix.eq_ignore_ascii_case(ILLEGAL_ENV_VAR_PREFIX.as_bytes()));
247+
if !has_illegal_prefix {
212248
// SAFETY: called before any threads are spawned.
213249
unsafe { std::env::set_var(&key, &value) };
214250
}
@@ -384,6 +420,7 @@ struct PathGuard {
384420

385421
#[cfg(test)]
386422
mod tests {
423+
use std::ffi::OsString;
387424
use std::fs;
388425
use std::io::ErrorKind;
389426
use std::path::Path;
@@ -409,6 +446,58 @@ mod tests {
409446
assert_eq!(super::argv0_alias(), None);
410447
}
411448

449+
#[test]
450+
fn argv0_alias_detects_batch_sentinel() {
451+
let args = vec![
452+
OsString::from("devo.exe"),
453+
OsString::from("--devo-alias=devo-server"),
454+
OsString::from("--transport"),
455+
OsString::from("stdio"),
456+
];
457+
458+
assert_eq!(
459+
super::argv0_alias_from_args(args),
460+
Some(super::SERVER_ALIAS)
461+
);
462+
}
463+
464+
#[test]
465+
fn server_dispatch_args_remove_batch_alias_sentinel() {
466+
let args = vec![
467+
OsString::from("devo.exe"),
468+
OsString::from("--devo-alias=devo-server"),
469+
OsString::from("--transport"),
470+
OsString::from("stdio"),
471+
];
472+
473+
assert_eq!(
474+
super::server_dispatch_args_from(args),
475+
vec![
476+
OsString::from("devo.exe"),
477+
OsString::from("--transport"),
478+
OsString::from("stdio")
479+
]
480+
);
481+
}
482+
483+
#[test]
484+
fn server_dispatch_args_keep_direct_alias_args() {
485+
let args = vec![
486+
OsString::from("devo-server"),
487+
OsString::from("--transport"),
488+
OsString::from("stdio"),
489+
];
490+
491+
assert_eq!(
492+
super::server_dispatch_args_from(args),
493+
vec![
494+
OsString::from("devo-server"),
495+
OsString::from("--transport"),
496+
OsString::from("stdio")
497+
]
498+
);
499+
}
500+
412501
#[test]
413502
fn lock_unavailable_detection_handles_would_block() {
414503
assert_eq!(

crates/cli/src/doctor_command.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
//! `devo doctor` health checks for local configuration and provider readiness.
2+
//!
3+
//! This command is intentionally diagnostic rather than mutating: it reports
4+
//! toolchain, config, provider-resolution, and model-catalog state so users can
5+
//! fix setup issues before launching the interactive runtime.
6+
17
use anyhow::Result;
28
use devo_core::AppConfigLoader;
39
use devo_core::FileSystemAppConfigLoader;
@@ -16,8 +22,8 @@ pub(crate) async fn run_doctor() -> Result<()> {
1622
let rustc = Command::new("rustc").arg("--version").output();
1723
match rustc {
1824
Ok(output) => {
19-
let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
20-
println!(" {}", version);
25+
let version = String::from_utf8_lossy(&output.stdout);
26+
println!(" {}", version.trim());
2127
}
2228
Err(e) => {
2329
println!(" {} rustc not found: {}", "✗".red(), e);
@@ -78,7 +84,7 @@ pub(crate) async fn run_doctor() -> Result<()> {
7884
println!(" model: {}", resolved.model);
7985
println!(
8086
" base_url: {}",
81-
resolved.base_url.unwrap_or("default".into())
87+
resolved.base_url.as_deref().unwrap_or("default")
8288
);
8389
println!(" wire_api: {:?}", resolved.wire_api);
8490
if resolved.api_key.is_some() {

crates/cli/src/main.rs

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
//! User-facing CLI entrypoint for interactive, prompt, diagnostic, upgrade,
2+
//! and hidden server process modes.
3+
//!
4+
//! This binary owns command-line parsing, startup update checks, logging
5+
//! bootstrap, and final exit messages. Long-lived runtime behavior is delegated
6+
//! to the server, TUI, and core crates so CLI changes stay focused on process
7+
//! orchestration and display.
8+
19
use anyhow::Result;
210
use clap::Parser;
311
use clap::Subcommand;
@@ -62,14 +70,16 @@ fn main() -> Result<()> {
6270

6371
fn format_with_separators(value: usize) -> String {
6472
let digits = value.to_string();
65-
let mut out = String::new();
66-
for (index, ch) in digits.chars().rev().enumerate() {
67-
if index > 0 && index % 3 == 0 {
73+
let separator_count = digits.len().saturating_sub(1) / 3;
74+
let first_group_len = digits.len() - separator_count * 3;
75+
let mut out = String::with_capacity(digits.len() + separator_count);
76+
for (index, ch) in digits.chars().enumerate() {
77+
if index > 0 && index >= first_group_len && (index - first_group_len).is_multiple_of(3) {
6878
out.push(',');
6979
}
7080
out.push(ch);
7181
}
72-
out.chars().rev().collect()
82+
out
7383
}
7484

7585
fn format_token_usage_line(exit: &devo_tui::AppExit, color_enabled: bool) -> Option<String> {
@@ -86,11 +96,7 @@ fn format_token_usage_line(exit: &devo_tui::AppExit, color_enabled: bool) -> Opt
8696
let cached_suffix = if exit.total_cache_read_tokens > 0 {
8797
let cached_value = format_with_separators(exit.total_cache_read_tokens);
8898
if color_enabled {
89-
format!(
90-
" (+ {} {})",
91-
"\u{1b}[1;33m".to_string() + &cached_value + "\u{1b}[0m",
92-
"\u{1b}[33mcached\u{1b}[0m"
93-
)
99+
format!(" (+ \u{1b}[1;33m{cached_value}\u{1b}[0m \u{1b}[33mcached\u{1b}[0m)")
94100
} else {
95101
format!(" (+ {cached_value} cached)")
96102
}
@@ -131,9 +137,9 @@ fn exit_messages(exit: &devo_tui::AppExit, color_enabled: bool) -> Vec<String> {
131137
command
132138
};
133139
let prefix = if color_enabled {
134-
"\u{1b}[2mTo continue this session, run\u{1b}[0m".to_string()
140+
"\u{1b}[2mTo continue this session, run\u{1b}[0m"
135141
} else {
136-
"To continue this session, run".to_string()
142+
"To continue this session, run"
137143
};
138144
lines.push(format!("{prefix} {command}"));
139145
}

0 commit comments

Comments
 (0)