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
47 changes: 27 additions & 20 deletions app/onboarding/interfaces/steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,28 +522,35 @@ def get_options(self) -> List[StepOption]:
try:
from app.tui.mcp_settings import list_mcp_servers
servers = list_mcp_servers()

# Create a lookup by name
server_lookup = {s["name"]: s for s in servers}

# Return only recommended servers that exist in config
options = []
for name, (icon, requires_setup) in self.RECOMMENDED_SERVERS.items():
if name in server_lookup:
server = server_lookup[name]
label = server["name"].replace("-", " ").replace(" mcp", "").title()
options.append(StepOption(
value=server["name"],
label=label,
description=server.get("description", f"MCP server: {server['name']}"),
default=server.get("enabled", False),
icon=icon,
requires_setup=requires_setup
))
return options
except ImportError:
except Exception:
# If MCP config is completely broken, show nothing rather than
# crashing the wizard — the user can configure later in Settings.
return []

# Create a lookup by name
server_lookup = {s["name"]: s for s in servers}

# Return only recommended servers that exist in config
options = []
for name, (icon, requires_setup) in self.RECOMMENDED_SERVERS.items():
if name in server_lookup:
server = server_lookup[name]
label = server["name"].replace("-", " ").replace(" mcp", "").title()
# Append platform warning to description when server paths
# are incompatible with the current OS
desc = server.get("description", f"MCP server: {server['name']}")
if server.get("platform_blocked"):
label += " (⚠ Windows-only — requires setup on this OS)"
options.append(StepOption(
value=server["name"],
label=label,
description=desc,
default=server.get("enabled", False),
icon=icon,
requires_setup=requires_setup,
))
return options

def validate(self, value: Any) -> tuple[bool, Optional[str]]:
# Value should be a list of server names
if not isinstance(value, list):
Expand Down
40 changes: 38 additions & 2 deletions app/tui/mcp_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from __future__ import annotations

import json
import sys
from pathlib import Path
from typing import Dict, List, Optional, Any

Expand All @@ -13,6 +14,23 @@
MCP_CONFIG_PATH = APP_CONFIG_PATH / "mcp_config.json"


def _is_windows_path(path: str) -> bool:
"""Check if a path uses Windows drive-letter syntax (e.g. C:/...)."""
return bool(path) and len(path) >= 2 and path[0].isalpha() and path[1] == ":"


def _path_usable_on_current_platform(command: str, args: list) -> bool:
"""Return False if command/args reference paths not valid on this OS."""
if sys.platform == "win32":
return True
if _is_windows_path(command):
return False
for arg in args or []:
if _is_windows_path(arg):
return False
return True


def load_mcp_config() -> MCPConfig:
"""Load MCP configuration from file."""
try:
Expand All @@ -34,10 +52,27 @@ def save_mcp_config(config: MCPConfig) -> bool:


def list_mcp_servers() -> List[Dict[str, Any]]:
"""Get list of configured MCP servers with their status."""
config = load_mcp_config()
"""Get list of configured MCP servers with their status.

Servers with platform-incompatible paths (e.g. Windows paths on macOS)
are annotated with a ``platform_blocked`` flag so the UI can explain why
they cannot be started.
"""
try:
config = load_mcp_config()
except Exception as exc:
logger.error(f"Failed to load MCP config: {exc}")
return []
servers = []
for server in config.mcp_servers:
platform_blocked = not _path_usable_on_current_platform(
server.command or "", getattr(server, "args", []) or []
)
if platform_blocked:
logger.debug(
"MCP server %s has platform-specific paths — skipping on %s",
server.name, sys.platform,
)
servers.append({
"name": server.name,
"description": server.description,
Expand All @@ -46,6 +81,7 @@ def list_mcp_servers() -> List[Dict[str, Any]]:
"command": server.command,
"action_set": server.resolved_action_set_name,
"env": server.env,
"platform_blocked": platform_blocked,
})
return servers

Expand Down
17 changes: 17 additions & 0 deletions app/tui/onboarding/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,11 @@ class OnboardingWizardScreen(Screen):

CSS = ONBOARDING_CSS

BINDINGS = [
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am still not able to call Ctrl + S in the terminal to skip steps. Will have to look deeper.

("ctrl+s", "skip_step", "Skip"),
("escape", "cancel", "Cancel"),
]

def __init__(self, handler: "TUIHardOnboarding"):
super().__init__()
self._handler = handler
Expand Down Expand Up @@ -695,7 +700,19 @@ def _complete(self) -> None:
self._handler.on_complete(cancelled=False)
self.app.pop_screen()

def action_skip_step(self) -> None:
"""Skip the current optional step (Ctrl+S)."""
step = self._handler.get_step(self._current_step)
if not step.required:
self._skip_step()

def action_cancel(self) -> None:
"""Handle Escape key to cancel wizard."""
self._handler.on_complete(cancelled=True)
self.app.pop_screen()

def action_focus_nav(self) -> None:
"""Focus the navigation bar (Tab)."""
nav = self.query_one("#nav-actions")
if hasattr(nav, 'focus'):
nav.focus()
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,21 @@ export function OnboardingPage() {
}, [onboardingStep, selectedValue, textValue, orModel, proxiedVia, ollamaUrl, formValues, submitOnboardingStep])

const handleSkip = useCallback(() => skipOnboardingStep(), [skipOnboardingStep])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does not overwrite the browser default save

// Ctrl+S to skip optional steps (matches TUI behavior)
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
if (onboardingStep && !onboardingStep.required) {
e.preventDefault()
skipOnboardingStep()
}
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [onboardingStep, skipOnboardingStep])

const handleBack = useCallback(() => goBackOnboardingStep(), [goBackOnboardingStep])

const isMultiSelect = onboardingStep?.name === 'mcp' || onboardingStep?.name === 'skills'
Expand Down