You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Supersedes#1416 (closed). Companion to #1415 (CLI integration bugs) and #1417 (manager→subagent CLI). This issue covers the UI layer only — no overlap.
Overview
PraisonAI has four UI entry points (praisonai ui, ui chat, ui code, ui agents). Three of them have zero wiring to the external-agent integrations that live in praisonai/integrations/*; the fourth (ui code) has a duplicate subprocess reimplementation of Claude Code that bypasses ClaudeCodeIntegration. This issue consolidates the gap analysis and proposes a single DRY fix.
And a CLI-level flag handler at src/praisonai/praisonai/cli/features/external_agents.py:134-179 (ExternalAgentsHandler.apply_to_agent_config) that already knows how to inject an integration's .as_tool() into an agent's tools list. Reuse this — don't duplicate.
This is a direct duplicate of ClaudeCodeIntegration.execute() at src/praisonai/praisonai/integrations/claude_code.py. Replacing it with ClaudeCodeIntegration(...).as_tool() removes ~50 lines AND automatically picks up any fixes from issue #1415 (stderr surfacing, env-var model config, etc.).
ui_chat/default_app.py bypasses PraisonAI Agent — no tools, no memory, no integrations possible
ui_chat/default_app.py:42-71
New default UI can never use external agents
🔴 Critical
G2
ui/chat.py Agent has no external-agent tools and no switch in ChatSettings
ui/chat.py:465-478
Users cannot delegate to Claude/Gemini/Codex/Cursor from chat UI
🔴 Critical
G3
ui/code.py has Claude only, missing Gemini/Codex/Cursor
ui/code.py:494-512
Partial solution; 3 of 4 integrations unreachable
🟠 High
G4
ui/code.py reimplements Claude via subprocess — duplicates ClaudeCodeIntegration
ui/code.py:250-290
Bugs fixed in #1415 will not propagate; ~50 LOC duplication
🟠 High
G5
ui/agents.py (AgentTeam flow) has no external-agent switches
ui/agents.py
Multi-agent team cannot include external CLIs as specialized subagents
🟡 Medium
G6
No shared UI helper for "installed external agents + switches" — each UI file must duplicate
all UI files
Future drift; DRY violation waiting to happen
🟡 Medium
G7
Settings persistence: claude_code_enabled is stored in session DB (ui/code.py:483) but other agents will need the same pattern
database_config.py
Each new agent needs schema change unless generalized
🟢 Low
Feature Gaps (per entry point)
Feature
ui
ui chat
ui code
ui agents
Agent wiring
❌
✅
✅
✅
Claude switch
❌
❌
⚠️ (subprocess dup)
❌
Gemini switch
❌
❌
❌
❌
Codex switch
❌
❌
❌
❌
Cursor switch
❌
❌
❌
❌
Availability gate (hide if not installed)
❌
❌
❌
❌
@mention routing (@claude, @gemini, etc.)
❌
❌
❌
❌
Proposed Implementation — DRY + Minimal
Phase 1 — Shared helper (new file, ~80 LOC)
New file: src/praisonai/praisonai/ui/_external_agents.py
"""Shared helpers for wiring external agents into any UI entry point.Single source of truth for:- Listing installed external agents (lazy, cached)- Rendering Chainlit Switch widgets / aiui settings entries- Building the tools list from enabled agents"""fromfunctoolsimportlru_cachefromtypingimportAny, Dict, List# Map of UI toggle id → (integration class path, pretty label)EXTERNAL_AGENTS: Dict[str, Dict[str, str]] = {
"claude_enabled": {"module": "claude_code", "cls": "ClaudeCodeIntegration",
"label": "Claude Code (coding, file edits)", "cli": "claude"},
"gemini_enabled": {"module": "gemini_cli", "cls": "GeminiCLIIntegration",
"label": "Gemini CLI (analysis, search)", "cli": "gemini"},
"codex_enabled": {"module": "codex_cli", "cls": "CodexCLIIntegration",
"label": "Codex CLI (refactoring)", "cli": "codex"},
"cursor_enabled": {"module": "cursor_cli", "cls": "CursorCLIIntegration",
"label": "Cursor CLI (IDE tasks)", "cli": "cursor-agent"},
}
@lru_cache(maxsize=1)definstalled_external_agents() ->List[str]:
"""Return toggle ids of external agents whose CLI is on PATH."""importshutilreturn [toggle_idfortoggle_id, metainEXTERNAL_AGENTS.items()
ifshutil.which(meta["cli"])]
defexternal_agent_tools(settings: Dict[str, Any], workspace: str=".") ->list:
"""Build tools list from settings dict of toggle_id → bool."""importimportlibtools= []
fortoggle_id, enabledinsettings.items():
ifnotenabledortoggle_idnotinEXTERNAL_AGENTS:
continuemeta=EXTERNAL_AGENTS[toggle_id]
mod=importlib.import_module(f"praisonai.integrations.{meta['module']}")
integration=getattr(mod, meta["cls"])(workspace=workspace)
ifintegration.is_available:
tools.append(integration.as_tool())
returntoolsdefchainlit_switches(current_settings: Dict[str, bool]):
"""Return Chainlit Switch widgets for installed external agents only."""fromchainlit.input_widgetimportSwitchreturn [
Switch(id=toggle_id, label=EXTERNAL_AGENTS[toggle_id]["label"],
initial=current_settings.get(toggle_id, False))
fortoggle_idininstalled_external_agents()
]
Phase 2 — Wire into each UI entry point
ui/chat.py
# at top: from praisonai.ui._external_agents import chainlit_switches, external_agent_tools# in @cl.on_chat_start (around line 465):settings=cl.ChatSettings([
TextInput(id="model_name", ...),
Switch(id="tools_enabled", ...),
*chainlit_switches(load_external_agent_settings()), # ← NEW
])
# in _get_or_create_agent (around line 420):tools=_get_interactive_tools() +external_agent_tools(
session_settings, workspace=os.environ.get("PRAISONAI_WORKSPACE", ".")
)
ui/code.py
# REMOVE duplicate subprocess function claude_code_tool (lines 249-290)# REPLACE with shared helper:frompraisonai.ui._external_agentsimportchainlit_switches, external_agent_tools# in @cl.on_chat_start (around line 502-511):settings=cl.ChatSettings([
TextInput(...),
Switch(id="tools_enabled", ...),
*chainlit_switches(load_external_agent_settings()), # replaces single claude switch
])
# in _get_or_create_agent: use external_agent_tools() instead of manual claude_code_tool append
ui_chat/default_app.py (clean chat)
# REPLACE direct openai.AsyncOpenAI call with PraisonAI Agent + external toolsfrompraisonaiagentsimportAgentfrompraisonai.ui._external_agentsimportexternal_agent_tools, installed_external_agents_agent=None@aiui.settingsasyncdefon_settings(new_settings):
global_agent_agent=None# invalidate cache on changedef_get_agent(settings):
global_agentif_agentisNone:
_agent=Agent(
name="PraisonAI",
instructions="You are a helpful assistant. Delegate coding/analysis tasks to external subagents when available.",
llm=os.getenv("MODEL_NAME", "gpt-4o-mini"),
tools=external_agent_tools(settings),
)
return_agent@aiui.replyasyncdefon_message(message: str, settings: dict=None):
agent=_get_agent(settingsor {})
result=awaitagent.achat(str(message))
forchunkinstr(result).split(" "):
awaitaiui.stream_token(chunk+" ")
ui/agents.py
Add the same chainlit_switches to the ChatSettings and pass external_agent_tools(...) into each Agent constructor in the AgentTeam.
In each UI's message handler, pre-process the message: if starts with @claude, @gemini, @codex, or @cursor, strip the mention and temporarily override the Agent's instructions to "Always call {cli}_tool". Default behavior (no mention) delegates automatically based on instructions.
Replace direct OpenAI call with Agent + external tools; add @aiui.settings handler
~30
src/praisonai/praisonai/ui/chat.py
Inject chainlit_switches + external_agent_tools
~10
src/praisonai/praisonai/ui/code.py
Delete duplicate claude_code_tool (lines 250-290); use shared helper; add 3 new switches
~-50 / +15
src/praisonai/praisonai/ui/agents.py
Add switches + inject into AgentTeam agents
~10
Net change: ≈ −20 LOC (shared helper eliminates more duplication than it adds).
Technical Considerations
Lazy imports: installed_external_agents() only uses shutil.which; external_agent_tools() imports the integration module lazily per toggle. No module-level heavy imports.
Performance: @lru_cache(maxsize=1) on availability list → constant-time after first call. UI cold-start impact: <5 ms.
Hide unavailable: chainlit_switches() returns widgets ONLY for installed CLIs → no misleading toggles.
Multi-agent safety: Each Chainlit session gets its own Agent instance (already done); external integrations are stateless.
Settings persistence: Reuse existing save_setting / load_setting helpers in ui/chat.py:save_setting_with_retry and ui/code.py; add one row per toggle id. No schema migration needed.
Backward compat: claude_code_enabled legacy key must keep working — alias to claude_enabled in the helper.
Acceptance Criteria
praisonai ui shows installed external agents as toggles in settings (auto-hide uninstalled)
praisonai ui chat shows installed external agents as Chainlit switches
praisonai ui code switches now offer Claude + Gemini + Codex + Cursor (when installed)
praisonai ui agents AgentTeam members can opt into external agents via switches
Duplicate claude_code_tool in ui/code.py:249-290removed — replaced by ClaudeCodeIntegration.as_tool()
Toggling an external agent on, sending a message, and verifying via logs that {cli}_tool was invoked
praisonai ui cold-start does not regress (±500 ms of baseline)
claude_code_enabled legacy setting key still works (backward compat)
Feature: External agents (claude/gemini/codex/cursor) as subagents across all
praisonai uientry points@claude please implement.
Overview
PraisonAI has four UI entry points (
praisonai ui,ui chat,ui code,ui agents). Three of them have zero wiring to the external-agent integrations that live inpraisonai/integrations/*; the fourth (ui code) has a duplicate subprocess reimplementation of Claude Code that bypassesClaudeCodeIntegration. This issue consolidates the gap analysis and proposes a single DRY fix.Background
praisonai/integrations/already provides working, production-grade wrappers:src/praisonai/praisonai/integrations/claude_code.py.is_available.as_tool()src/praisonai/praisonai/integrations/gemini_cli.pysrc/praisonai/praisonai/integrations/codex_cli.pysrc/praisonai/praisonai/integrations/cursor_cli.pyAnd a CLI-level flag handler at
src/praisonai/praisonai/cli/features/external_agents.py:134-179(ExternalAgentsHandler.apply_to_agent_config) that already knows how to inject an integration's.as_tool()into an agent's tools list. Reuse this — don't duplicate.Architecture Analysis — Current Implementation
Entry-point matrix
praisonai ui(new clean chat)src/praisonai/praisonai/ui_chat/default_app.py:1-77openai.AsyncOpenAIdirectlyAgententirelypraisonai ui chatsrc/praisonai/praisonai/ui/chat.py:403-448Agent(name="PraisonAI Assistant", …)ChatSettingspraisonai ui codesrc/praisonai/praisonai/ui/code.py:417-473Agent(name="PraisonAI Code Assistant", …)praisonai ui agentssrc/praisonai/praisonai/ui/agents.pypraisonai ui realtimesrc/praisonai/praisonai/ui/realtime.pypraisonai ui gradioThe DRY violation in
ui/code.pyThis is a direct duplicate of
ClaudeCodeIntegration.execute()atsrc/praisonai/praisonai/integrations/claude_code.py. Replacing it withClaudeCodeIntegration(...).as_tool()removes ~50 lines AND automatically picks up any fixes from issue #1415 (stderr surfacing, env-var model config, etc.).Grep evidence
Zero references outside the integrations module.
Gap Analysis
Critical Gaps
ui_chat/default_app.pybypasses PraisonAIAgent— no tools, no memory, no integrations possibleui_chat/default_app.py:42-71ui/chat.pyAgent has no external-agent tools and no switch inChatSettingsui/chat.py:465-478ui/code.pyhas Claude only, missing Gemini/Codex/Cursorui/code.py:494-512ui/code.pyreimplements Claude via subprocess — duplicatesClaudeCodeIntegrationui/code.py:250-290ui/agents.py(AgentTeam flow) has no external-agent switchesui/agents.pyclaude_code_enabledis stored in session DB (ui/code.py:483) but other agents will need the same patterndatabase_config.pyFeature Gaps (per entry point)
uiui chatui codeui agents@claude,@gemini, etc.)Proposed Implementation — DRY + Minimal
Phase 1 — Shared helper (new file, ~80 LOC)
New file:
src/praisonai/praisonai/ui/_external_agents.pyPhase 2 — Wire into each UI entry point
ui/chat.pyui/code.pyui_chat/default_app.py(clean chat)ui/agents.pyAdd the same
chainlit_switchesto theChatSettingsand passexternal_agent_tools(...)into each Agent constructor in the AgentTeam.Phase 3 — Optional @mention routing (nice-to-have)
In each UI's message handler, pre-process the message: if starts with
@claude,@gemini,@codex, or@cursor, strip the mention and temporarily override the Agent's instructions to "Always call {cli}_tool". Default behavior (no mention) delegates automatically based on instructions.Files to Create / Modify
New files
src/praisonai/praisonai/ui/_external_agents.pysrc/praisonai/tests/integration/test_ui_external_agents.pyModified files
src/praisonai/praisonai/ui_chat/default_app.py@aiui.settingshandlersrc/praisonai/praisonai/ui/chat.pychainlit_switches+external_agent_toolssrc/praisonai/praisonai/ui/code.pyclaude_code_tool(lines 250-290); use shared helper; add 3 new switchessrc/praisonai/praisonai/ui/agents.pyNet change: ≈ −20 LOC (shared helper eliminates more duplication than it adds).
Technical Considerations
installed_external_agents()only usesshutil.which;external_agent_tools()imports the integration module lazily per toggle. No module-level heavy imports.@lru_cache(maxsize=1)on availability list → constant-time after first call. UI cold-start impact: <5 ms.chainlit_switches()returns widgets ONLY for installed CLIs → no misleading toggles.ClaudeCodeIntegrationinstead of duplicate subprocess,ui/code.pyautomatically inherits Bug: --external-agent silently returns empty for gemini/codex/cursor (4 root causes) #1415 fixes (stderr surfacing, env-var models,--skip-git-repo-check, etc.).save_setting/load_settinghelpers inui/chat.py:save_setting_with_retryandui/code.py; add one row per toggle id. No schema migration needed.claude_code_enabledlegacy key must keep working — alias toclaude_enabledin the helper.Acceptance Criteria
praisonai uishows installed external agents as toggles in settings (auto-hide uninstalled)praisonai ui chatshows installed external agents as Chainlit switchespraisonai ui codeswitches now offer Claude + Gemini + Codex + Cursor (when installed)praisonai ui agentsAgentTeam members can opt into external agents via switchesclaude_code_toolinui/code.py:249-290removed — replaced byClaudeCodeIntegration.as_tool(){cli}_toolwas invokedpraisonai uicold-start does not regress (±500 ms of baseline)claude_code_enabledlegacy setting key still works (backward compat)tests/integration/test_ui_external_agents.pycover: availability gating, settings → tools mapping, backward-compat keyVerification Commands
Principles (from AGENTS.md)
praisonaiagents(core SDK); all changes inpraisonaiwrapper