Skip to content
Open
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
18 changes: 18 additions & 0 deletions docs/frontend-ui-audit-2026-08-11/ActivityGroups.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Frontend UI Audit — Activity Groups

Scope: `EditActivityGroup`, `TerminalActivityGroup`, and their shared event projection. This is a behavior-preserving component refactor; no rendered styles, copy, layout, focus behavior, or interaction contract changed.

| Line | Element | Verdict | Reason | Suggested change |
| --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `src/engines/ChatPanel/ChatItems/activityGroupProjection.tsx:17` | Event-item projection, intermediate running-state normalization, lazy registry rendering, and tool-usage aggregation | abstract | Edit and terminal groups previously duplicated the same presentation pipeline. One shared owner prevents loading-state and usage-badge behavior from drifting while leaving domain summaries separate. | Reuse the shared projection from both activity-group components. |
| `src/engines/ChatPanel/ChatItems/EditActivityGroup/index.tsx:111` | Edit activity stack | keep with reason | `StackedBlock`, tool icons, workstation diff tokens, and the shared usage badge already implement the design-system contracts. The edit/read and diff-stat summary is specific to edit activity. | Keep the edit summary local and continue using shared primitives. |
| `src/engines/ChatPanel/ChatItems/TerminalActivityGroup/index.tsx:140` | Terminal activity stack | keep with reason | The stack uses the same shared primitives, while terminal/MCP/wait counts and durable Work Item result cards are terminal-domain behavior. Moving them into the generic projection would leak domain rules. | Keep terminal summary and Work Item projection local. |
| `src/engines/ChatPanel/ChatItems/EditActivityGroup/index.tsx:119` | Existing summary typography and spacing | keep with reason | The existing classes compose established text and diff-stat tokens; this refactor introduces no arbitrary visual value or parallel component style. | No visual change. |

## Summary

- Fix: 0
- Keep with reason: 3
- Abstract: 1
- Sweep candidates: 0
- Accessibility: no semantic or interactive changes; `StackedBlock` retains the existing keyboard/collapse contract.
21 changes: 21 additions & 0 deletions docs/frontend-ui-audit-2026-08-11/OAuthSessionSetup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Frontend UI Audit — OAuth Session Setup

Scope: the Claude Code and Codex OAuth session-setup components plus their extracted shared shell. Provider-specific credential mapping remains outside the shared presentation/lifecycle boundary.

| Line | Element | Verdict | Reason | Suggested change |
| -------------------------------------------------------------------------- | ----------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `src/features/SessionSetup/components/OAuthSessionSetupShell.tsx:63` | OAuth idle/browser/loading/error/success presentation | abstract | Claude Code and Codex duplicated the same section layout, WebView chrome, progress indicator, overlays, alerts, and debug container. A provider-copy contract preserves localized differences without parallel JSX. | Reuse one shell and pass provider copy/test identity explicitly. |
| `src/features/SessionSetup/components/OAuthSessionSetupShell.tsx:286` | OAuth browser lifecycle | abstract | Both providers used the same open, delayed native start, success collapse, retry, parent-layout sync, and external-close transitions. One owner makes cleanup and duplicate retry policy consistent. | Keep transient browser intent in the shared shell; retain provider capture state in the existing hooks. |
| `src/features/SessionSetup/components/OAuthSessionSetupShell.tsx:123` | Refresh and close icon buttons | fix | The duplicated icon-only controls had no accessible names. The shared shell can apply localized labels once for both providers. | Add `aria-label` and `title` from existing common actions copy. |
| `src/features/SessionSetup/components/OAuthSessionSetupShell.tsx:172` | Loading and error overlays | fix | Visual states existed but did not expose status/alert semantics to assistive technology. | Mark loading as `role="status"` and errors as `role="alert"`. |
| `src/features/SessionSetup/components/OAuthSessionSetupShell.tsx:391` | Two-step progress indicator | fix | Active styling was only visual. | Expose the active item with `aria-current="step"`. |
| `src/features/SessionSetup/components/ClaudeCodeSessionSetup/index.tsx:58` | Claude Code capture and account metadata mapping | keep with reason | Claude Code owns its callback response and optional organization metadata; generalizing this mapping would weaken provider types. | Keep mapping in the provider adapter and pass normalized capture state to the shell. |
| `src/features/SessionSetup/components/CodexSessionSetup/index.tsx:41` | Codex capture and required token mapping | keep with reason | Codex requires refresh and ID tokens and supports initial auto-start. Those are provider contract differences, not presentation variants. | Keep mapping and auto-start input in the provider adapter. |

## Summary

- Fix: 3
- Keep with reason: 2
- Abstract: 2
- Sweep candidates: 0
- Visual behavior: unchanged; accessibility semantics are additive.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use std::path::{Path, PathBuf};

use super::detect::detect_all;
use super::mcp_config::load_external_mcp_config;
use super::types::{
frontmatter_declares_readonly, readonly_excluded_tool_names, DetectedItem, ImportItemReport,
ImportReport, ImportSelection, ImportStatus, ItemKind, SourceScope,
Expand Down Expand Up @@ -368,50 +369,6 @@ fn copy_dir_recursive(from: &Path, to: &Path) -> Result<(), String> {
// MCP import
// ============================================================

fn load_external_mcp_config(path: &Path) -> Result<McpConfigFile, String> {
let raw = std::fs::read_to_string(path)
.map_err(|err| format!("Failed to read MCP config {}: {}", path.display(), err))?;
let mut value: serde_json::Value = serde_json::from_str(&raw)
.map_err(|err| format!("Failed to parse MCP config {}: {}", path.display(), err))?;
let Some(servers) = value
.get_mut("mcpServers")
.and_then(|entry| entry.as_object_mut())
else {
return Ok(McpConfigFile::default());
};

for server in servers.values_mut() {
let Some(server_obj) = server.as_object_mut() else {
continue;
};
if !server_obj.contains_key("type") {
let inferred = if server_obj.contains_key("url") {
"streamableHttp"
} else {
"stdio"
};
server_obj.insert(
"type".to_string(),
serde_json::Value::String(inferred.to_string()),
);
}
if server_obj.get("type").and_then(|entry| entry.as_str()) == Some("http") {
server_obj.insert(
"type".to_string(),
serde_json::Value::String("streamableHttp".to_string()),
);
}
}

serde_json::from_value(value).map_err(|err| {
format!(
"Failed to parse MCP server entries {}: {}",
path.display(),
err
)
})
}

fn apply_mcp_import(
selection: &ImportSelection,
target_repo_path: Option<&Path>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@

use std::path::Path;

use super::super::mcp_config::load_external_mcp_config;
use super::super::types::{DetectedItem, ItemKind, ItemPreview, SourceAgent, SourceScope};
use super::helpers::{home_dir, orgii_mcp_exists, path_has_denied_ancestor, MAX_ITEMS_PER_BATCH};
use crate::specialization::mcp::config::{McpConfigFile, McpTransportType};
use crate::specialization::mcp::config::McpTransportType;

pub(super) fn detect_mcp_servers(repo_path: Option<&Path>) -> Vec<DetectedItem> {
let mut out = Vec::new();
Expand Down Expand Up @@ -70,50 +71,6 @@ pub(super) fn detect_mcp_servers(repo_path: Option<&Path>) -> Vec<DetectedItem>
out
}

fn load_external_mcp_config(path: &Path) -> Result<McpConfigFile, String> {
let raw = std::fs::read_to_string(path)
.map_err(|err| format!("Failed to read MCP config {}: {}", path.display(), err))?;
let mut value: serde_json::Value = serde_json::from_str(&raw)
.map_err(|err| format!("Failed to parse MCP config {}: {}", path.display(), err))?;
let Some(servers) = value
.get_mut("mcpServers")
.and_then(|entry| entry.as_object_mut())
else {
return Ok(McpConfigFile::default());
};

for server in servers.values_mut() {
let Some(server_obj) = server.as_object_mut() else {
continue;
};
if !server_obj.contains_key("type") {
let inferred = if server_obj.contains_key("url") {
"streamableHttp"
} else {
"stdio"
};
server_obj.insert(
"type".to_string(),
serde_json::Value::String(inferred.to_string()),
);
}
if server_obj.get("type").and_then(|entry| entry.as_str()) == Some("http") {
server_obj.insert(
"type".to_string(),
serde_json::Value::String("streamableHttp".to_string()),
);
}
}

serde_json::from_value(value).map_err(|err| {
format!(
"Failed to parse MCP server entries {}: {}",
path.display(),
err
)
})
}

fn scan_mcp_config_file(
path: &Path,
source_agent: SourceAgent,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use std::path::Path;

use crate::specialization::mcp::config::McpConfigFile;

/// Load an MCP config authored by another agent and normalize the transport
/// spellings that ORGII accepts before deserializing it into the canonical
/// config model.
pub(super) fn load_external_mcp_config(path: &Path) -> Result<McpConfigFile, String> {
let raw = std::fs::read_to_string(path)
.map_err(|err| format!("Failed to read MCP config {}: {}", path.display(), err))?;
let mut value: serde_json::Value = serde_json::from_str(&raw)
.map_err(|err| format!("Failed to parse MCP config {}: {}", path.display(), err))?;
let Some(servers) = value
.get_mut("mcpServers")
.and_then(|entry| entry.as_object_mut())
else {
return Ok(McpConfigFile::default());
};

for server in servers.values_mut() {
let Some(server_obj) = server.as_object_mut() else {
continue;
};
if !server_obj.contains_key("type") {
let inferred = if server_obj.contains_key("url") {
"streamableHttp"
} else {
"stdio"
};
server_obj.insert(
"type".to_string(),
serde_json::Value::String(inferred.to_string()),
);
}
if server_obj.get("type").and_then(|entry| entry.as_str()) == Some("http") {
server_obj.insert(
"type".to_string(),
serde_json::Value::String("streamableHttp".to_string()),
);
}
}

serde_json::from_value(value).map_err(|err| {
format!(
"Failed to parse MCP server entries {}: {}",
path.display(),
err
)
})
}

#[cfg(test)]
mod tests {
use super::*;
use crate::specialization::mcp::config::McpTransportType;
use tempfile::TempDir;

#[test]
fn normalizes_external_transport_variants() {
let temp = TempDir::new().expect("create temp dir");
let path = temp.path().join("mcp.json");
std::fs::write(
&path,
r#"{
"mcpServers": {
"implicit-stdio": { "command": "server" },
"implicit-http": { "url": "https://example.com/mcp" },
"legacy-http": { "type": "http", "url": "https://example.com/legacy" },
"explicit-sse": { "type": "sse", "url": "https://example.com/sse" }
}
}"#,
)
.expect("write config");

let config = load_external_mcp_config(&path).expect("load config");

assert_eq!(
config.mcp_servers["implicit-stdio"].transport_type,
McpTransportType::Stdio
);
assert_eq!(
config.mcp_servers["implicit-http"].transport_type,
McpTransportType::StreamableHttp
);
assert_eq!(
config.mcp_servers["legacy-http"].transport_type,
McpTransportType::StreamableHttp
);
assert_eq!(
config.mcp_servers["explicit-sse"].transport_type,
McpTransportType::Sse
);
}

#[test]
fn treats_missing_mcp_servers_as_empty() {
let temp = TempDir::new().expect("create temp dir");
let path = temp.path().join("mcp.json");
std::fs::write(&path, r#"{ "other": true }"#).expect("write config");

let config = load_external_mcp_config(&path).expect("load config");

assert!(config.mcp_servers.is_empty());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
pub mod commands;
pub mod detect;
mod mcp_config;
pub mod types;

// Wildcard re-export needed: `#[tauri::command]` generates hidden
Expand Down
53 changes: 53 additions & 0 deletions src-tauri/crates/lsp/src/command_detection.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use std::process::Command;

/// Check whether a command-line tool is available on the system PATH.
///
/// Forward PATH explicitly so app startup code that augments the process
/// environment is reflected consistently across every LSP discovery surface.
pub fn command_exists(command_name: &str) -> bool {
let current_path = std::env::var_os("PATH");

#[cfg(unix)]
let mut command = {
let mut command = Command::new("which");
command.arg(command_name);
command
};

#[cfg(windows)]
let mut command = {
let mut command = Command::new("where");
command.arg(command_name);
command
};

if let Some(path) = current_path {
command.env("PATH", path);
}
app_platform::hide_console(&mut command);
command
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn finds_a_platform_shell() {
#[cfg(unix)]
assert!(command_exists("sh"));

#[cfg(windows)]
assert!(command_exists("cmd"));
}

#[test]
fn rejects_a_missing_command() {
assert!(!command_exists(
"orgii-command-detection-test-definitely-missing"
));
}
}
28 changes: 1 addition & 27 deletions src-tauri/crates/lsp/src/commands/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
//!
//! Tauri commands for detecting installed language servers and lint tools.

use std::process::Command;

use super::cache;
use crate::lint_tools::LintToolInfo;
use crate::server_defs::{servers, servers_for_language_id};
Expand Down Expand Up @@ -55,31 +53,7 @@ pub const LANGUAGE_DISPLAY_NAMES: &[(&str, &str)] = &[
("zig", "Zig"),
];

/// Check if a command exists in PATH
pub fn command_exists(cmd: &str) -> bool {
// Explicitly forward PATH so the login-shell-augmented PATH is visible.
let current_path = std::env::var("PATH").unwrap_or_default();
#[cfg(unix)]
{
Command::new("which")
.arg(cmd)
.env("PATH", &current_path)
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
#[cfg(windows)]
{
let mut command = Command::new("where");
command.arg(cmd).env("PATH", &current_path);
// Suppress console window on Windows.
app_platform::hide_console(&mut command);
command
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
}
pub use crate::command_detection::command_exists;

/// Check if uninstall is supported based on install hint
fn is_uninstall_supported(install_hint: &str) -> bool {
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/crates/lsp/src/commands/package_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
//! Utilities for detecting installed package managers and extracting
//! package names from install hints.
use super::discovery::command_exists;
use crate::command_detection::command_exists;

#[cfg(test)]
#[path = "tests/package_manager_tests.rs"]
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/crates/lsp/src/install_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use std::path::{Path, PathBuf};
use std::process::Stdio;
use tokio::process::Command;

use super::commands::discovery::command_exists;
use super::command_detection::command_exists;
use super::commands::package_manager::detect_package_manager;
use app_paths::lsp_bin_dir;

Expand Down
1 change: 1 addition & 0 deletions src-tauri/crates/lsp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
pub mod broadcast;
pub mod codec;
mod command_detection;
pub mod commands;
pub mod config;
pub mod eslint;
Expand Down
Loading
Loading