This release represents a comprehensive transformation of the codebase from Codex to LLMX, enhanced with LiteLLM integration to support 100+ LLM providers through a unified API. ## Major Changes ### Phase 1: Repository & Infrastructure Setup - Established new repository structure and branching strategy - Created comprehensive project documentation (CLAUDE.md, LITELLM-SETUP.md) - Set up development environment and tooling configuration ### Phase 2: Rust Workspace Transformation - Renamed all Rust crates from `codex-*` to `llmx-*` (30+ crates) - Updated package names, binary names, and workspace members - Renamed core modules: codex.rs → llmx.rs, codex_delegate.rs → llmx_delegate.rs - Updated all internal references, imports, and type names - Renamed directories: codex-rs/ → llmx-rs/, codex-backend-openapi-models/ → llmx-backend-openapi-models/ - Fixed all Rust compilation errors after mass rename ### Phase 3: LiteLLM Integration - Integrated LiteLLM for multi-provider LLM support (Anthropic, OpenAI, Azure, Google AI, AWS Bedrock, etc.) - Implemented OpenAI-compatible Chat Completions API support - Added model family detection and provider-specific handling - Updated authentication to support LiteLLM API keys - Renamed environment variables: OPENAI_BASE_URL → LLMX_BASE_URL - Added LLMX_API_KEY for unified authentication - Enhanced error handling for Chat Completions API responses - Implemented fallback mechanisms between Responses API and Chat Completions API ### Phase 4: TypeScript/Node.js Components - Renamed npm package: @codex/codex-cli → @valknar/llmx - Updated TypeScript SDK to use new LLMX APIs and endpoints - Fixed all TypeScript compilation and linting errors - Updated SDK tests to support both API backends - Enhanced mock server to handle multiple API formats - Updated build scripts for cross-platform packaging ### Phase 5: Configuration & Documentation - Updated all configuration files to use LLMX naming - Rewrote README and documentation for LLMX branding - Updated config paths: ~/.codex/ → ~/.llmx/ - Added comprehensive LiteLLM setup guide - Updated all user-facing strings and help text - Created release plan and migration documentation ### Phase 6: Testing & Validation - Fixed all Rust tests for new naming scheme - Updated snapshot tests in TUI (36 frame files) - Fixed authentication storage tests - Updated Chat Completions payload and SSE tests - Fixed SDK tests for new API endpoints - Ensured compatibility with Claude Sonnet 4.5 model - Fixed test environment variables (LLMX_API_KEY, LLMX_BASE_URL) ### Phase 7: Build & Release Pipeline - Updated GitHub Actions workflows for LLMX binary names - Fixed rust-release.yml to reference llmx-rs/ instead of codex-rs/ - Updated CI/CD pipelines for new package names - Made Apple code signing optional in release workflow - Enhanced npm packaging resilience for partial platform builds - Added Windows sandbox support to workspace - Updated dotslash configuration for new binary names ### Phase 8: Final Polish - Renamed all assets (.github images, labels, templates) - Updated VSCode and DevContainer configurations - Fixed all clippy warnings and formatting issues - Applied cargo fmt and prettier formatting across codebase - Updated issue templates and pull request templates - Fixed all remaining UI text references ## Technical Details **Breaking Changes:** - Binary name changed from `codex` to `llmx` - Config directory changed from `~/.codex/` to `~/.llmx/` - Environment variables renamed (CODEX_* → LLMX_*) - npm package renamed to `@valknar/llmx` **New Features:** - Support for 100+ LLM providers via LiteLLM - Unified authentication with LLMX_API_KEY - Enhanced model provider detection and handling - Improved error handling and fallback mechanisms **Files Changed:** - 578 files modified across Rust, TypeScript, and documentation - 30+ Rust crates renamed and updated - Complete rebrand of UI, CLI, and documentation - All tests updated and passing **Dependencies:** - Updated Cargo.lock with new package names - Updated npm dependencies in llmx-cli - Enhanced OpenAPI models for LLMX backend This release establishes LLMX as a standalone project with comprehensive LiteLLM integration, maintaining full backward compatibility with existing functionality while opening support for a wide ecosystem of LLM providers. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Sebastian Krüger <support@pivoine.art>
226 lines
7.1 KiB
Rust
226 lines
7.1 KiB
Rust
use std::path::PathBuf;
|
|
|
|
use crossterm::event::KeyCode;
|
|
use crossterm::event::KeyEvent;
|
|
use crossterm::event::KeyEventKind;
|
|
use llmx_core::config::set_project_trusted;
|
|
use llmx_core::git_info::resolve_root_git_project_for_trust;
|
|
use ratatui::buffer::Buffer;
|
|
use ratatui::layout::Rect;
|
|
use ratatui::style::Stylize;
|
|
use ratatui::text::Line;
|
|
use ratatui::widgets::Paragraph;
|
|
use ratatui::widgets::WidgetRef;
|
|
use ratatui::widgets::Wrap;
|
|
|
|
use crate::key_hint;
|
|
use crate::onboarding::onboarding_screen::KeyboardHandler;
|
|
use crate::onboarding::onboarding_screen::StepStateProvider;
|
|
use crate::render::Insets;
|
|
use crate::render::renderable::ColumnRenderable;
|
|
use crate::render::renderable::Renderable;
|
|
use crate::render::renderable::RenderableExt as _;
|
|
use crate::selection_list::selection_option_row;
|
|
|
|
use super::onboarding_screen::StepState;
|
|
pub(crate) struct TrustDirectoryWidget {
|
|
pub llmx_home: PathBuf,
|
|
pub cwd: PathBuf,
|
|
pub is_git_repo: bool,
|
|
pub selection: Option<TrustDirectorySelection>,
|
|
pub highlighted: TrustDirectorySelection,
|
|
pub error: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum TrustDirectorySelection {
|
|
Trust,
|
|
DontTrust,
|
|
}
|
|
|
|
impl WidgetRef for &TrustDirectoryWidget {
|
|
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
|
let mut column = ColumnRenderable::new();
|
|
|
|
column.push(Line::from(vec![
|
|
"> ".into(),
|
|
"You are running LLMX in ".bold(),
|
|
self.cwd.to_string_lossy().to_string().into(),
|
|
]));
|
|
column.push("");
|
|
|
|
let guidance = if self.is_git_repo {
|
|
"Since this folder is version controlled, you may wish to allow LLMX to work in this folder without asking for approval."
|
|
} else {
|
|
"Since this folder is not version controlled, we recommend requiring approval of all edits and commands."
|
|
};
|
|
|
|
column.push(
|
|
Paragraph::new(guidance.to_string())
|
|
.wrap(Wrap { trim: true })
|
|
.inset(Insets::tlbr(0, 2, 0, 0)),
|
|
);
|
|
column.push("");
|
|
|
|
let mut options: Vec<(&str, TrustDirectorySelection)> = Vec::new();
|
|
if self.is_git_repo {
|
|
options.push((
|
|
"Yes, allow LLMX to work in this folder without asking for approval",
|
|
TrustDirectorySelection::Trust,
|
|
));
|
|
options.push((
|
|
"No, ask me to approve edits and commands",
|
|
TrustDirectorySelection::DontTrust,
|
|
));
|
|
} else {
|
|
options.push((
|
|
"Allow LLMX to work in this folder without asking for approval",
|
|
TrustDirectorySelection::Trust,
|
|
));
|
|
options.push((
|
|
"Require approval of edits and commands",
|
|
TrustDirectorySelection::DontTrust,
|
|
));
|
|
}
|
|
|
|
for (idx, (text, selection)) in options.iter().enumerate() {
|
|
column.push(selection_option_row(
|
|
idx,
|
|
text.to_string(),
|
|
self.highlighted == *selection,
|
|
));
|
|
}
|
|
|
|
column.push("");
|
|
|
|
if let Some(error) = &self.error {
|
|
column.push(
|
|
Paragraph::new(error.to_string())
|
|
.red()
|
|
.wrap(Wrap { trim: true })
|
|
.inset(Insets::tlbr(0, 2, 0, 0)),
|
|
);
|
|
column.push("");
|
|
}
|
|
|
|
column.push(
|
|
Line::from(vec![
|
|
"Press ".dim(),
|
|
key_hint::plain(KeyCode::Enter).into(),
|
|
" to continue".dim(),
|
|
])
|
|
.inset(Insets::tlbr(0, 2, 0, 0)),
|
|
);
|
|
|
|
column.render(area, buf);
|
|
}
|
|
}
|
|
|
|
impl KeyboardHandler for TrustDirectoryWidget {
|
|
fn handle_key_event(&mut self, key_event: KeyEvent) {
|
|
if key_event.kind == KeyEventKind::Release {
|
|
return;
|
|
}
|
|
|
|
match key_event.code {
|
|
KeyCode::Up | KeyCode::Char('k') => {
|
|
self.highlighted = TrustDirectorySelection::Trust;
|
|
}
|
|
KeyCode::Down | KeyCode::Char('j') => {
|
|
self.highlighted = TrustDirectorySelection::DontTrust;
|
|
}
|
|
KeyCode::Char('1') | KeyCode::Char('y') => self.handle_trust(),
|
|
KeyCode::Char('2') | KeyCode::Char('n') => self.handle_dont_trust(),
|
|
KeyCode::Enter => match self.highlighted {
|
|
TrustDirectorySelection::Trust => self.handle_trust(),
|
|
TrustDirectorySelection::DontTrust => self.handle_dont_trust(),
|
|
},
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl StepStateProvider for TrustDirectoryWidget {
|
|
fn get_step_state(&self) -> StepState {
|
|
match self.selection {
|
|
Some(_) => StepState::Complete,
|
|
None => StepState::InProgress,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TrustDirectoryWidget {
|
|
fn handle_trust(&mut self) {
|
|
let target =
|
|
resolve_root_git_project_for_trust(&self.cwd).unwrap_or_else(|| self.cwd.clone());
|
|
if let Err(e) = set_project_trusted(&self.llmx_home, &target) {
|
|
tracing::error!("Failed to set project trusted: {e:?}");
|
|
self.error = Some(format!("Failed to set trust for {}: {e}", target.display()));
|
|
}
|
|
|
|
self.selection = Some(TrustDirectorySelection::Trust);
|
|
}
|
|
|
|
fn handle_dont_trust(&mut self) {
|
|
self.highlighted = TrustDirectorySelection::DontTrust;
|
|
self.selection = Some(TrustDirectorySelection::DontTrust);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use crate::test_backend::VT100Backend;
|
|
|
|
use super::*;
|
|
use crossterm::event::KeyCode;
|
|
use crossterm::event::KeyEvent;
|
|
use crossterm::event::KeyEventKind;
|
|
use crossterm::event::KeyModifiers;
|
|
use pretty_assertions::assert_eq;
|
|
use ratatui::Terminal;
|
|
|
|
use std::path::PathBuf;
|
|
|
|
#[test]
|
|
fn release_event_does_not_change_selection() {
|
|
let mut widget = TrustDirectoryWidget {
|
|
llmx_home: PathBuf::from("."),
|
|
cwd: PathBuf::from("."),
|
|
is_git_repo: false,
|
|
selection: None,
|
|
highlighted: TrustDirectorySelection::DontTrust,
|
|
error: None,
|
|
};
|
|
|
|
let release = KeyEvent {
|
|
kind: KeyEventKind::Release,
|
|
..KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)
|
|
};
|
|
widget.handle_key_event(release);
|
|
assert_eq!(widget.selection, None);
|
|
|
|
let press = KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE);
|
|
widget.handle_key_event(press);
|
|
assert_eq!(widget.selection, Some(TrustDirectorySelection::DontTrust));
|
|
}
|
|
|
|
#[test]
|
|
fn renders_snapshot_for_git_repo() {
|
|
let widget = TrustDirectoryWidget {
|
|
llmx_home: PathBuf::from("."),
|
|
cwd: PathBuf::from("/workspace/project"),
|
|
is_git_repo: true,
|
|
selection: None,
|
|
highlighted: TrustDirectorySelection::Trust,
|
|
error: None,
|
|
};
|
|
|
|
let mut terminal = Terminal::new(VT100Backend::new(70, 14)).expect("terminal");
|
|
terminal
|
|
.draw(|f| (&widget).render_ref(f.area(), f.buffer_mut()))
|
|
.expect("draw");
|
|
|
|
insta::assert_snapshot!(terminal.backend());
|
|
}
|
|
}
|