Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/cli/IMPLEMENTATION_STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
**Date:** 2026-07-30
**Desktop base:** v0.6.4
**IsanAgent dependency:** git `altaidevorg/isanagent` `main`
(oneshot host API merged in [`isanagent#101`](https://github.com/altaidevorg/isanagent/pull/101); see `docs/cli/isanagent-oneshot-api.md`)
(oneshot: [`isanagent#101`](https://github.com/altaidevorg/isanagent/pull/101); ACP agent: [`isanagent#102`](https://github.com/altaidevorg/isanagent/pull/102) / `7018685`)

## Existing

Expand All @@ -15,6 +15,7 @@
| Configuration precedence | Verified | `altai-core::config` tests |
| Event envelope schema v1 | Verified | `altai-core::event` + JSONL emitter tests |
| `altai agent` host config + supported TUI start | Verified | dry-run + `start_host` path |
| `altai acp` Agent Client Protocol over stdio | Verified | `HostConfig.acp_mode` + clap dry-run; distinct from `altai serve` |
| ALTAI terminal theme (dark/light/auto/no-color) | Verified | palette resolve + host `theme` + TUI Theme roles |
| Responsive layout (80 / 100 / 120+) | Verified | `LayoutDensity` + wide secondary split + width-fit tests |
| Dense status header (workspace/model/permission/session) | Verified | title/status width-fit snapshots |
Expand Down
11 changes: 11 additions & 0 deletions docs/cli/TEST.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,14 @@
- Background / notification bus traffic is **not** journaled — matches Desktop
(`is_system_event` UI-only), not a CLI gap.
- Unit tests: 14 `journal_sink` cases including foreign-run warning ignore.

## ACP agent mode — 2026-07-30

- Bumped IsanAgent lock to `7018685` (ACP agent support from
[`isanagent#102`](https://github.com/altaidevorg/isanagent/pull/102)).
- `host_adapter::acp_host_config` sets `HostConfig.acp_mode = true`.
- New `altai acp` command starts IsanAgent as an ACP JSON-RPC server on
stdio (for Zed and other ACP clients). Distinct from `altai serve --stdio`
(ALTAI agent-host protocol).
- Clap dry-run preview includes `kind: "acp"` and `host.acp_mode`.
- Unit tests: host adapter ACP flag + clap contract parse.
10 changes: 5 additions & 5 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions src-tauri/crates/altai-cli/src/host_adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ pub fn oneshot_host_config(
host
}

/// Build an ACP (Agent Client Protocol) host for `altai acp`.
///
/// Stdin/stdout become the ACP JSON-RPC transport; the interactive terminal is
/// disabled. This is distinct from `altai serve --stdio`, which speaks ALTAI's
/// own agent-host protocol.
pub fn acp_host_config(workspace: &WorkspacePaths) -> isanagent::host::HostConfig {
let mut host = host_config_for_workspace(workspace);
host.acp_mode = true;
host
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -50,6 +61,7 @@ mod tests {
);
assert_eq!(config.sandbox, Some(PathBuf::from("/project")));
assert!(config.oneshot_prompt.is_none());
assert!(!config.acp_mode);
}

#[test]
Expand All @@ -61,5 +73,18 @@ mod tests {
let config = oneshot_host_config(&workspace, "summarize".into(), None);
assert_eq!(config.oneshot_prompt.as_deref(), Some("summarize"));
assert!(!config.line_mode);
assert!(!config.acp_mode);
}

#[test]
fn acp_config_enables_protocol_mode_without_oneshot() {
let workspace = WorkspacePaths {
root: PathBuf::from("/project"),
isanagent_state: PathBuf::from("/project/.isanagent"),
};
let config = acp_host_config(&workspace);
assert!(config.acp_mode);
assert!(config.oneshot_prompt.is_none());
assert_eq!(config.sandbox, Some(PathBuf::from("/project")));
}
}
93 changes: 92 additions & 1 deletion src-tauri/crates/altai-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ mod serve;
name = "altai-cli",
version,
about = "ALTAI terminal product",
long_about = "The ALTAI terminal product. Use `altai agent` for the interactive TUI and `altai run` for one-shot headless sessions."
long_about = "The ALTAI terminal product. Use `altai agent` for the interactive TUI, `altai run` for one-shot headless sessions, and `altai acp` for Agent Client Protocol (ACP) over stdio."
)]
struct Cli {
#[command(subcommand)]
Expand All @@ -27,6 +27,8 @@ enum Commands {
Agent(AgentArgs),
/// Run one prompt without starting an interactive terminal session.
Run(RunArgs),
/// Speak the Agent Client Protocol (ACP) over stdio for editors such as Zed.
Acp(AcpArgs),
/// Start the machine-facing ALTAI agent-host stdio protocol.
Serve(ServeArgs),
/// Print release and dependency information.
Expand Down Expand Up @@ -236,6 +238,17 @@ struct AgentArgs {
options: AgentOptions,
}

#[derive(Debug, Args)]
struct AcpArgs {
/// Workspace path. Defaults to the current directory.
path: Option<PathBuf>,
/// Describe the resolved ACP host without starting it.
#[arg(long)]
dry_run: bool,
#[command(flatten)]
options: AgentOptions,
}

#[derive(Debug, Args)]
struct RunArgs {
/// Workspace path. Defaults to the current directory.
Expand Down Expand Up @@ -382,6 +395,7 @@ fn run() -> Result<(), CliError> {
}
Some(Commands::Agent(args)) => agent(args),
Some(Commands::Run(args)) => run_prompt(args),
Some(Commands::Acp(args)) => acp(args),
Some(Commands::Serve(args)) => serve_command(args),
Some(Commands::Version { verbose }) => print_version(verbose),
Some(Commands::Completion { shell }) => {
Expand Down Expand Up @@ -706,6 +720,58 @@ fn agent(args: AgentArgs) -> Result<(), CliError> {
print_preview(value)
}

fn acp(args: AcpArgs) -> Result<(), CliError> {
let workspace =
resolve_command_workspace(args.options.workspace.as_deref(), args.path.as_deref())?;
let appearance = resolve_cli_theme(args.options.theme);
let mut host = host_adapter::acp_host_config(&workspace);
host.model = args.options.model.clone();
host.fallback_model = args.options.fallback_model.clone();
host.permission = args.options.permission.as_ref().map(host_permission_mode);
host.no_color = appearance == altai_core::EffectiveTerminalAppearance::NoColor;
host.theme = host_theme_mode(appearance);
host.resume = args.options.resume.clone();
host.files = args.options.files.clone();
apply_compaction_overrides(&mut host, &args.options);

if !args.dry_run {
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.map_err(|error| {
CliError::Message(format!("could not start the host runtime: {error}"))
})?;
return runtime
.block_on(isanagent::host::start_host(host))
.map_err(|error| {
CliError::Message(format!("IsanAgent ACP host exited with an error: {error}"))
});
}

let value = serde_json::json!({
"kind": "acp",
"protocol": "agent-client-protocol",
"transport": "stdio",
"workspace": workspace.root,
"isanagent_state": workspace.isanagent_state,
"host": {
"state": host.workspace,
"config": host.config,
"sandbox": host.sandbox,
"acp_mode": host.acp_mode,
},
"model": args.options.model,
"fallback_model": args.options.fallback_model,
"permission": args.options.permission.as_ref().map(PermissionMode::as_str),
"theme": args.options.theme.as_str(),
"effective_theme": appearance.as_str(),
"resume": args.options.resume,
"files": args.options.files,
"compaction": resolved_compaction_preview(&args.options),
});
print_preview(value)
}

fn apply_compaction_overrides(host: &mut isanagent::host::HostConfig, options: &AgentOptions) {
apply_compaction_fields(
host,
Expand Down Expand Up @@ -1221,6 +1287,31 @@ mod tests {
);
}

#[test]
fn acp_contract_parses_and_defaults_to_protocol_mode() {
let cli = Cli::try_parse_from([
"altai-cli",
"acp",
".",
"--model",
"anthropic/claude-sonnet-4-6",
"--permission",
"plan",
"--dry-run",
])
.expect("acp contract should parse");

let Some(Commands::Acp(args)) = cli.command else {
panic!("acp command should parse");
};
assert!(args.dry_run);
assert_eq!(args.options.permission, Some(PermissionMode::Plan));
assert_eq!(
args.options.model.as_deref(),
Some("anthropic/claude-sonnet-4-6")
);
}

#[test]
fn plain_agent_contract_can_start_the_embedded_tui() {
let cli = Cli::try_parse_from(["altai-cli", "agent", "."])
Expand Down
Loading