diff --git a/.rules/api.md b/.rules/api.md index e83b9764..2d73a51b 100644 --- a/.rules/api.md +++ b/.rules/api.md @@ -5,5 +5,6 @@ - Software / updates: `https://software.cortex.foundation`. - The harness is a client. Chat/Code turns, tools, computers, plugins, and snapshots go through that API. Do not embed a second model provider. - Code turns: `POST /v1/code/sessions/{id}/turns` with `{ message, mode: "chat"|"code" }`. Plan/ask are TUI/harness locks, not API modes. +- Code runtime: **Cloud is the shipped default** for the TUI and `cortex exec` (Designer Q9, `CLI_100_CHROME_LOCK_SIGNED`). This PC and SSH are explicit opt-in (`CORTEX_COMPUTER`, or `CORTEX_SSH_HOST` / `CORTEX_SSH_TARGET`) and may ship in 0.1.x. They require an already connected Code session; do not substitute Cloud. - When the API is unreachable, the product fails closed with a product-facing error. Do not fall back to a local mock model. - Environment overrides (`CORTEX_API_URL`, `CORTEX_API_KEY`, `CORTEX_AUTH_TOKEN`) are for operators and tests. Defaults must stay production Cortex URLs. diff --git a/CHANGELOG.md b/CHANGELOG.md index b6c37348..892442f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased ### Changed +- Code turns default to the **Cloud** runtime for the TUI and `cortex exec` (Designer Q9 / `CLI_100_CHROME_LOCK_SIGNED`). This PC and SSH are explicit opt-in in 0.1.x (`CORTEX_COMPUTER` or `CORTEX_SSH_HOST`) and refuse a fresh session with product copy instead of blocking every first turn. - README `docs/media/intro.gif` sits on a photographed green forest desktop (not teal blobs): Terminal chrome, a pointer that walks titlebar → composer → slash / model → Shell, and the signed lock TUI. Local CLI only — no Cortex Cloud handoff in the banner story. ## 0.1.9 diff --git a/docs/configuration/env.md b/docs/configuration/env.md index 0318beb1..73221c47 100644 --- a/docs/configuration/env.md +++ b/docs/configuration/env.md @@ -31,6 +31,19 @@ See [Data locations](data-locations.md) for the defaults these override. Interactive use should prefer `cortex login`, which stores the session in the OS keyring. See [Signing in](../reference/login.md). +## Code runtime + +The TUI and `cortex exec` create a **Cloud** Code session unless you explicitly +select This PC or SSH. That is the shipped default (Designer Q9). A fresh +install can complete a turn without extra configuration. This PC and SSH are +opt-in and may ship in 0.1.x. + +| Variable | Effect | +|----------|--------| +| `CORTEX_COMPUTER` | Where tools run. Unset (or `cloud`) uses Cloud. Set `this_pc` (aliases: `this-pc`, `local`, `paired`, `connected`) or `ssh` to select those runtimes. This PC and SSH require an already connected Code session; Cortex will not substitute Cloud. | +| `CORTEX_SSH_HOST` | SSH target. When set (or `CORTEX_SSH_TARGET`), the runtime is SSH — same connected-session rule as `CORTEX_COMPUTER=ssh`. | +| `CORTEX_SSH_TARGET` | Alias of `CORTEX_SSH_HOST`. | + ## Model selection | Variable | Effect | diff --git a/docs/guides/exec.md b/docs/guides/exec.md index ab279717..53d175ea 100644 --- a/docs/guides/exec.md +++ b/docs/guides/exec.md @@ -8,7 +8,9 @@ Cortex has two non-interactive entry points: | `cortex run` | A single request from your own shell. Streams a formatted answer, can continue a session, can share the result. | Both work without a terminal, so they are safe in pipelines where the -[TUI](tui.md) refuses to start. +[TUI](tui.md) refuses to start. Turns use the **Cloud** Code runtime unless you +set `CORTEX_COMPUTER` to `this_pc` or `ssh` (see +[Environment variables](../configuration/env.md)). ## `cortex exec` diff --git a/docs/guides/getting-started.md b/docs/guides/getting-started.md index 3dad0d92..3be6e4e0 100644 --- a/docs/guides/getting-started.md +++ b/docs/guides/getting-started.md @@ -157,9 +157,12 @@ cortex You get the session view from the recording on the [docs index](../README.md): a timeline, a composer at the bottom, and a status line showing the current mode -and autonomy level. The welcome card shows the working directory and -**Computer** (`This PC` when you started in a workspace, `Cloud` or `SSH` when -those are configured). Type what you want changed and press `Enter`. +and autonomy level. The TUI and `cortex exec` run on the **Cloud** Code runtime +by default (Designer Q9), so a fresh install can complete a turn without extra +configuration. To run tools on This PC or over SSH, set `CORTEX_COMPUTER` (see +[Environment variables](../configuration/env.md)). Those runtimes are explicit +opt-in in 0.1.x and need an already connected Code session; Cortex will not +substitute Cloud. Type what you want changed and press `Enter`. Turns go to the Code session API (`POST /v1/code/sessions/{id}/turns`) with streaming tokens and first-class tool rows. Press `Esc` to cancel a turn that diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 9dbfecaf..7f476bc8 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -31,6 +31,13 @@ Work through: will tell you. 4. Are you signed in? `cortex whoami`. +## "This PC and SSH Code execution require an already connected Code session" + +You selected This PC or SSH (`CORTEX_COMPUTER` or `CORTEX_SSH_HOST`) without a +connected Code session. Those runtimes are never created on the fly, and Cloud +is not substituted. Either resume a session that already has a host, or unset +`CORTEX_COMPUTER` to use Cloud. See [Environment variables](configuration/env.md). + ## The TUI will not start Cortex needs a terminal on both stdin and stdout. In a pipeline, a CI job, or diff --git a/src/cortex-cli/tests/exec_runtime.rs b/src/cortex-cli/tests/exec_runtime.rs index f678f07a..7d8d2dbc 100644 --- a/src/cortex-cli/tests/exec_runtime.rs +++ b/src/cortex-cli/tests/exec_runtime.rs @@ -33,6 +33,9 @@ impl Run { .env("CORTEX_HOME", self.home.path()) .env("CORTEX_API_KEY", "offline-fixture") .env("CORTEX_API_URL", UNREACHABLE) + .env_remove("CORTEX_COMPUTER") + .env_remove("CORTEX_SSH_HOST") + .env_remove("CORTEX_SSH_TARGET") .env("RUST_LOG", "off") .current_dir(self.home.path()); command @@ -64,6 +67,56 @@ fn frames(stdout: &[u8]) -> Vec { .collect() } +#[test] +fn default_runtime_reaches_the_api_instead_of_refusing_this_pc() { + let run = Run::new(); + let output = run.exec(&["--output-format", "json", "--timeout", "20", "hello"], ""); + assert!(!output.status.success()); + let result: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + let error = result["error"].as_str().unwrap_or_default(); + assert!( + !error.contains("already connected Code session"), + "unset CORTEX_COMPUTER must not refuse as This PC: {result}" + ); + assert_eq!( + result["num_turns"], 1, + "the Cloud default must start a turn instead of refusing locally: {result}" + ); + assert!( + error.contains("temporarily unavailable") + || error.contains("cortex login") + || error.contains("CORTEX_API_KEY"), + "default Cloud path fails closed after attempting a session, never This PC: {result}" + ); +} + +#[test] +fn this_pc_without_a_session_refuses_with_product_copy() { + let run = Run::new(); + let mut command = run.command(&["--output-format", "json", "--timeout", "20", "hello"]); + command.env("CORTEX_COMPUTER", "this_pc"); + let mut child = command + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + child.stdin.take().unwrap().write_all(b"").unwrap(); + let output = child.wait_with_output().unwrap(); + assert!(!output.status.success()); + let result: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + let error = result["error"].as_str().unwrap_or_default(); + assert!( + error.contains("This PC") && error.contains("already connected Code session"), + "This PC without a session must use product copy: {result}" + ); + assert!( + error.contains("CORTEX_COMPUTER"), + "the refuse path must name the explicit override: {result}" + ); + assert!(!error.to_lowercase().contains("reqwest"), "{result}"); +} + #[test] fn an_unreachable_service_fails_the_run_and_reports_an_error_result_not_a_success() { let run = Run::new(); diff --git a/src/cortex-engine/src/client/code_agent.rs b/src/cortex-engine/src/client/code_agent.rs index 1570589a..90444d85 100644 --- a/src/cortex-engine/src/client/code_agent.rs +++ b/src/cortex-engine/src/client/code_agent.rs @@ -56,62 +56,7 @@ pub fn normalize_api_base(url: &str) -> String { /// Guest-cookie token prefix stored in the keyring / env. pub const GUEST_TOKEN_PREFIX: &str = "gt:"; -/// Where tools run for this CLI session. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum ComputerKind { - /// Local workspace passed to the CLI (This PC). - #[default] - ThisPc, - /// Cloud runtime (Firecracker VM when the API provisions one). - Cloud, - /// SSH remote, when `CORTEX_SSH_HOST` is set. - Ssh, -} - -impl ComputerKind { - /// Detect from the environment. A workspace path (cwd) means This PC - /// unless the operator forces cloud or sets an SSH target. - pub fn detect() -> Self { - if std::env::var("CORTEX_SSH_HOST") - .ok() - .filter(|s| !s.is_empty()) - .is_some() - || std::env::var("CORTEX_SSH_TARGET") - .ok() - .filter(|s| !s.is_empty()) - .is_some() - { - return Self::Ssh; - } - match std::env::var("CORTEX_COMPUTER") - .unwrap_or_default() - .to_ascii_lowercase() - .as_str() - { - "cloud" => Self::Cloud, - "ssh" => Self::Ssh, - _ => Self::ThisPc, - } - } - - /// Product label for the TUI. - pub fn label(self) -> &'static str { - match self { - Self::ThisPc => "This PC", - Self::Cloud => "Cloud", - Self::Ssh => "SSH", - } - } - - pub fn as_str(self) -> &'static str { - match self { - Self::ThisPc => "this_pc", - Self::Cloud => "cloud", - Self::Ssh => "ssh", - } - } -} +pub use super::computer::{ComputerKind, DISCONNECTED_RUNTIME}; /// Per-turn context the TUI sets before `complete()`. #[derive(Debug, Clone, Default)] @@ -533,9 +478,7 @@ impl CodeAgentClient { } let ctx = self.turn_context(); if ctx.computer != ComputerKind::Cloud { - return Err(CortexError::InvalidInput( - "Local and SSH Code execution require an already connected Code session. Connect a host and resume that session, or explicitly select Cloud. No runtime was substituted.".into() - )); + return Err(CortexError::InvalidInput(DISCONNECTED_RUNTIME.into())); } Ok(self .create_session_with(CreateCodeSession { @@ -955,14 +898,6 @@ mod tests { assert_eq!(GUEST_TOKEN_PREFIX, "gt:"); } - #[test] - fn computer_kind_labels() { - assert_eq!(ComputerKind::ThisPc.label(), "This PC"); - assert_eq!(ComputerKind::Cloud.label(), "Cloud"); - assert_eq!(ComputerKind::Ssh.label(), "SSH"); - assert_eq!(ComputerKind::ThisPc.as_str(), "this_pc"); - } - #[test] fn parses_tool_start_with_arguments() { let raw = r#"{"type":"tool_start","invocation_id":"tci_2","tool_name":"Bash","arguments":{"command":"ls"}}"#; diff --git a/src/cortex-engine/src/client/computer.rs b/src/cortex-engine/src/client/computer.rs new file mode 100644 index 00000000..8008b964 --- /dev/null +++ b/src/cortex-engine/src/client/computer.rs @@ -0,0 +1,191 @@ +//! Code runtime selection. Cloud is the shipped TUI + exec default. + +use serde::{Deserialize, Serialize}; + +/// This PC and SSH need a bound host session. Cloud is never substituted. +pub const DISCONNECTED_RUNTIME: &str = "This PC and SSH Code execution require an already connected Code session. Connect a host and resume that session, or leave CORTEX_COMPUTER unset to use Cloud. No runtime was substituted."; + +/// Where tools run for this CLI session. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ComputerKind { + /// Local workspace passed to the CLI (This PC). Explicit via `CORTEX_COMPUTER`. + ThisPc, + /// Cloud runtime (Firecracker VM when the API provisions one). Default. + #[default] + Cloud, + /// SSH remote, when `CORTEX_COMPUTER=ssh` or `CORTEX_SSH_HOST` is set. + Ssh, +} + +impl ComputerKind { + /// Detect from the environment. Cloud is the default Code runtime so a + /// fresh install can complete a turn. This PC and SSH are explicit. + pub fn detect() -> Self { + Self::from_env( + nonempty_env("CORTEX_SSH_HOST").as_deref(), + nonempty_env("CORTEX_SSH_TARGET").as_deref(), + nonempty_env("CORTEX_COMPUTER").as_deref(), + ) + } + + /// Resolve the Code runtime from explicit environment values. + /// + /// `CORTEX_SSH_HOST` / `CORTEX_SSH_TARGET` select SSH. `CORTEX_COMPUTER` + /// selects This PC or SSH. Unset, `cloud`, and unknown values stay Cloud. + pub fn from_env( + ssh_host: Option<&str>, + ssh_target: Option<&str>, + computer: Option<&str>, + ) -> Self { + if nonempty(ssh_host) || nonempty(ssh_target) { + return Self::Ssh; + } + let normalized = computer + .unwrap_or("") + .trim() + .to_ascii_lowercase() + .replace(['-', ' '], "_"); + match normalized.as_str() { + "this_pc" | "thispc" | "local" | "paired" | "connected" => Self::ThisPc, + "ssh" => Self::Ssh, + _ => Self::Cloud, + } + } + + /// Product label for the TUI. + pub fn label(self) -> &'static str { + match self { + Self::ThisPc => "This PC", + Self::Cloud => "Cloud", + Self::Ssh => "SSH", + } + } + + pub fn as_str(self) -> &'static str { + match self { + Self::ThisPc => "this_pc", + Self::Cloud => "cloud", + Self::Ssh => "ssh", + } + } +} + +fn nonempty(value: Option<&str>) -> bool { + value.is_some_and(|s| !s.is_empty()) +} + +fn nonempty_env(name: &str) -> Option { + std::env::var(name).ok().filter(|s| !s.is_empty()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::client::{CodeAgentClient, CodeTurnContext}; + + #[test] + fn computer_kind_labels() { + assert_eq!(ComputerKind::ThisPc.label(), "This PC"); + assert_eq!(ComputerKind::Cloud.label(), "Cloud"); + assert_eq!(ComputerKind::Ssh.label(), "SSH"); + assert_eq!(ComputerKind::ThisPc.as_str(), "this_pc"); + assert_eq!(ComputerKind::default(), ComputerKind::Cloud); + assert_eq!(CodeTurnContext::default().computer, ComputerKind::Cloud); + } + + #[test] + fn computer_kind_from_env_defaults_to_cloud() { + assert_eq!( + ComputerKind::from_env(None, None, None), + ComputerKind::Cloud + ); + assert_eq!( + ComputerKind::from_env(None, None, Some("")), + ComputerKind::Cloud + ); + assert_eq!( + ComputerKind::from_env(None, None, Some("cloud")), + ComputerKind::Cloud + ); + assert_eq!( + ComputerKind::from_env(None, None, Some("unknown")), + ComputerKind::Cloud + ); + assert_eq!( + ComputerKind::from_env(None, None, Some("this_pc")), + ComputerKind::ThisPc + ); + assert_eq!( + ComputerKind::from_env(None, None, Some("this-pc")), + ComputerKind::ThisPc + ); + assert_eq!( + ComputerKind::from_env(None, None, Some("local")), + ComputerKind::ThisPc + ); + assert_eq!( + ComputerKind::from_env(None, None, Some("ssh")), + ComputerKind::Ssh + ); + assert_eq!( + ComputerKind::from_env(Some("box.example"), None, Some("cloud")), + ComputerKind::Ssh + ); + } + + #[test] + #[serial_test::serial] + fn detect_without_cortex_computer_is_cloud() { + let mut fixture = crate::testing::TestFixture::new(); + fixture.unset_env("CORTEX_COMPUTER"); + fixture.unset_env("CORTEX_SSH_HOST"); + fixture.unset_env("CORTEX_SSH_TARGET"); + assert_eq!(ComputerKind::detect(), ComputerKind::Cloud); + fixture.set_env("CORTEX_COMPUTER", "this_pc"); + assert_eq!(ComputerKind::detect(), ComputerKind::ThisPc); + } + + #[tokio::test] + async fn this_pc_without_session_refuses_with_product_copy() { + let client = CodeAgentClient::new( + Some("http://127.0.0.1:1".into()), + Some("contract-fixture-not-a-credential".into()), + ); + client.set_turn_context(CodeTurnContext { + computer: ComputerKind::ThisPc, + ..Default::default() + }); + let err = client + .ensure_session() + .await + .expect_err("This PC must refuse"); + let msg = err.user_friendly_message(); + assert!( + msg.contains(DISCONNECTED_RUNTIME), + "expected product copy, got {msg}" + ); + assert!(msg.contains("This PC"), "{msg}"); + assert!(!msg.to_lowercase().contains("reqwest"), "{msg}"); + assert!(!msg.contains("127.0.0.1"), "{msg}"); + } + + #[tokio::test] + async fn ssh_without_session_refuses_with_product_copy() { + let client = CodeAgentClient::new( + Some("http://127.0.0.1:1".into()), + Some("contract-fixture-not-a-credential".into()), + ); + client.set_turn_context(CodeTurnContext { + computer: ComputerKind::Ssh, + ..Default::default() + }); + let err = client.ensure_session().await.expect_err("SSH must refuse"); + let msg = err.user_friendly_message(); + assert!( + msg.contains(DISCONNECTED_RUNTIME), + "expected product copy, got {msg}" + ); + assert!(msg.contains("SSH"), "{msg}"); + } +} diff --git a/src/cortex-engine/src/client/mod.rs b/src/cortex-engine/src/client/mod.rs index 455b5268..e751297b 100644 --- a/src/cortex-engine/src/client/mod.rs +++ b/src/cortex-engine/src/client/mod.rs @@ -4,14 +4,15 @@ //! All LLM requests go through the Cortex backend with OAuth authentication. mod code_agent; +mod computer; mod cortex; pub mod runtime_contract; pub mod types; pub use code_agent::{ CodeAgentClient, CodeHost, CodeHostPairing, CodeMessage, CodeSession, CodeTurnContext, - CodeTurnEvent, CodeTurnMode, ComputerKind, CreateCodeSession, GUEST_TOKEN_PREFIX, GuestSession, - cached_code_session_id, + CodeTurnEvent, CodeTurnMode, ComputerKind, CreateCodeSession, DISCONNECTED_RUNTIME, + GUEST_TOKEN_PREFIX, GuestSession, cached_code_session_id, }; pub use cortex::{CortexClient, CortexModel, PricingInfo}; pub use types::*; diff --git a/src/cortex-engine/tests/runtime_contract_client.rs b/src/cortex-engine/tests/runtime_contract_client.rs index fc459492..4eb9c5a1 100644 --- a/src/cortex-engine/tests/runtime_contract_client.rs +++ b/src/cortex-engine/tests/runtime_contract_client.rs @@ -4,8 +4,8 @@ use cortex_engine::client::runtime_contract::{ INCOMPLETE_STREAM, LOCAL_TOOLS_UNSUPPORTED, code_message, }; use cortex_engine::client::{ - CodeTurnContext, CodeTurnMode, CompletionRequest, ComputerKind, ContentPart, CortexClient, - FinishReason, Message, MessageContent, ModelClient, ResponseEvent, + CodeAgentClient, CodeTurnContext, CodeTurnMode, CompletionRequest, ComputerKind, ContentPart, + CortexClient, FinishReason, Message, MessageContent, ModelClient, ResponseEvent, }; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio_stream::StreamExt; @@ -118,6 +118,30 @@ fn request() -> CompletionRequest { } } +#[tokio::test] +#[serial_test::serial] +async fn default_detect_ensures_a_cloud_session() { + let mut fixture = cortex_engine::testing::TestFixture::new(); + fixture.unset_env("CORTEX_COMPUTER"); + fixture.unset_env("CORTEX_SSH_HOST"); + fixture.unset_env("CORTEX_SSH_TARGET"); + + let (url, peer) = peer(vec![creation()]).await; + let client = CodeAgentClient::new(Some(url), Some("contract-fixture-not-a-credential".into())); + assert_eq!( + client.turn_context().computer, + ComputerKind::Cloud, + "unset CORTEX_COMPUTER must default to Cloud" + ); + let id = client + .ensure_session() + .await + .expect("Cloud can create a session"); + assert_eq!(id, "session_fixture"); + let bodies = peer.await.unwrap(); + assert_eq!(bodies[0]["runtime"], "cloud"); +} + #[tokio::test] async fn runtime_contract_remote_tools_are_observations_even_with_arguments() { let (url, peer) = peer(vec![creation(), turn(concat!( diff --git a/src/cortex-tui/src/runner/event_loop/runtime_contract_streaming_tests.rs b/src/cortex-tui/src/runner/event_loop/runtime_contract_streaming_tests.rs index b99b5827..39e3fd3e 100644 --- a/src/cortex-tui/src/runner/event_loop/runtime_contract_streaming_tests.rs +++ b/src/cortex-tui/src/runner/event_loop/runtime_contract_streaming_tests.rs @@ -1,4 +1,5 @@ use super::*; +use super::{CodeTurnMode, ComputerKind, tui_code_turn_context}; use crate::app::AppState; use crate::views::MinimalSessionView; use cortex_engine::client::runtime_contract::INCOMPLETE_STREAM; @@ -204,3 +205,23 @@ async fn runtime_contract_forwarder_cancels_silent_initial_request() { ); assert_eq!(cancels.load(Ordering::SeqCst), 1); } + +#[test] +fn first_submit_uses_cloud_as_the_shipped_tui_default() { + let ctx = tui_code_turn_context(false); + assert_eq!(ctx.computer, ComputerKind::detect()); + assert_eq!(ctx.turn_mode, Some(CodeTurnMode::Code)); + assert_eq!( + ComputerKind::from_env(None, None, None), + ComputerKind::Cloud, + "Designer Q9 / CLI_100_CHROME_LOCK_SIGNED: Cloud is the TUI+exec default" + ); + assert_eq!( + ComputerKind::from_env(None, None, Some("this_pc")), + ComputerKind::ThisPc + ); + assert_eq!( + tui_code_turn_context(true).turn_mode, + Some(CodeTurnMode::Chat) + ); +} diff --git a/src/cortex-tui/src/runner/event_loop/streaming.rs b/src/cortex-tui/src/runner/event_loop/streaming.rs index f2f57adb..65016bb5 100644 --- a/src/cortex-tui/src/runner/event_loop/streaming.rs +++ b/src/cortex-tui/src/runner/event_loop/streaming.rs @@ -14,7 +14,8 @@ use crate::session::StoredToolCall; use crate::views::tool_call::ToolStatus; use cortex_engine::client::{ - CompletionRequest, Message, ResponseEvent, ToolDefinition as ClientToolDefinition, + CodeTurnContext, CodeTurnMode, CompletionRequest, ComputerKind, Message, ResponseEvent, + ToolDefinition as ClientToolDefinition, }; use cortex_engine::streaming::StreamEvent; @@ -94,6 +95,29 @@ pub(super) fn classify_stream_error(error: &str) -> StreamErrorKind { StreamErrorKind::Actionable } +/// Shared TUI + exec product rule: Cloud unless This PC/SSH is explicit. +fn tui_code_turn_context(plan_or_spec: bool) -> CodeTurnContext { + CodeTurnContext { + workspace: std::env::current_dir() + .ok() + .map(|p| p.display().to_string()), + computer: ComputerKind::detect(), + turn_mode: Some(if plan_or_spec { + CodeTurnMode::Chat + } else { + CodeTurnMode::Code + }), + ssh_target: std::env::var("CORTEX_SSH_HOST") + .ok() + .filter(|s| !s.is_empty()) + .or_else(|| { + std::env::var("CORTEX_SSH_TARGET") + .ok() + .filter(|s| !s.is_empty()) + }), + } +} + impl EventLoop { /// Handles message submission using the new provider system. /// @@ -226,23 +250,11 @@ impl EventLoop { } if let Some(ref c) = client { - let computer = cortex_engine::client::ComputerKind::detect(); - let turn_mode = if self.app_state.is_plan_mode() || self.app_state.is_spec_mode() { + let plan_or_spec = self.app_state.is_plan_mode() || self.app_state.is_spec_mode(); + if plan_or_spec { cortex_engine::harness::enter_spec_mode(); - cortex_engine::client::CodeTurnMode::Chat - } else { - cortex_engine::client::CodeTurnMode::Code - }; - c.configure_code_turn(cortex_engine::client::CodeTurnContext { - workspace: std::env::current_dir() - .ok() - .map(|p| p.display().to_string()), - computer, - turn_mode: Some(turn_mode), - ssh_target: std::env::var("CORTEX_SSH_HOST") - .ok() - .or_else(|| std::env::var("CORTEX_SSH_TARGET").ok()), - }); + } + c.configure_code_turn(tui_code_turn_context(plan_or_spec)); } // Create channel for streaming events @@ -741,22 +753,8 @@ impl EventLoop { } if let Some(ref c) = client { - let computer = cortex_engine::client::ComputerKind::detect(); - let turn_mode = if self.app_state.is_plan_mode() || self.app_state.is_spec_mode() { - cortex_engine::client::CodeTurnMode::Chat - } else { - cortex_engine::client::CodeTurnMode::Code - }; - c.configure_code_turn(cortex_engine::client::CodeTurnContext { - workspace: std::env::current_dir() - .ok() - .map(|p| p.display().to_string()), - computer, - turn_mode: Some(turn_mode), - ssh_target: std::env::var("CORTEX_SSH_HOST") - .ok() - .or_else(|| std::env::var("CORTEX_SSH_TARGET").ok()), - }); + let plan_or_spec = self.app_state.is_plan_mode() || self.app_state.is_spec_mode(); + c.configure_code_turn(tui_code_turn_context(plan_or_spec)); } // Create channel for streaming events