diff --git a/pyproject.toml b/pyproject.toml
index b33bfdf..4408913 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "uipath-dev"
-version = "0.0.60"
+version = "0.0.61"
description = "UiPath Developer Console"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
@@ -10,6 +10,8 @@ dependencies = [
"pyperclip>=1.11.0, <2.0.0",
"fastapi>=0.128.8",
"uvicorn[standard]>=0.40.0",
+ "uipath>=2.10.0, <2.11.0",
+ "openai",
]
classifiers = [
"Intended Audience :: Developers",
diff --git a/src/uipath/dev/models/eval_data.py b/src/uipath/dev/models/eval_data.py
new file mode 100644
index 0000000..8fda331
--- /dev/null
+++ b/src/uipath/dev/models/eval_data.py
@@ -0,0 +1,132 @@
+"""Data models for evaluation runs."""
+
+from __future__ import annotations
+
+import uuid
+from dataclasses import dataclass, field
+from datetime import datetime, timezone
+from typing import Any
+
+
+@dataclass
+class EvalSetInfo:
+ """Summary of a discovered evaluation set."""
+
+ id: str
+ name: str
+ eval_count: int
+ evaluator_ids: list[str]
+
+
+@dataclass
+class EvalItemResult:
+ """Result of evaluating a single item."""
+
+ name: str
+ inputs: dict[str, Any] = field(default_factory=dict)
+ expected_output: Any = None
+ scores: dict[str, float] = field(default_factory=dict)
+ overall_score: float = 0.0
+ output: Any = None
+ justifications: dict[str, str] = field(default_factory=dict)
+ duration_ms: float | None = None
+ status: str = "pending" # pending | running | completed | failed
+ error: str | None = None
+ traces: list[dict[str, Any]] = field(default_factory=list)
+
+
+@dataclass
+class EvalRunState:
+ """Full state of an eval run."""
+
+ id: str = field(default_factory=lambda: str(uuid.uuid4()))
+ eval_set_id: str = ""
+ eval_set_name: str = ""
+ status: str = "pending" # pending | running | completed | failed
+ progress_completed: int = 0
+ progress_total: int = 0
+ overall_score: float | None = None
+ evaluator_scores: dict[str, float] = field(default_factory=dict)
+ results: list[EvalItemResult] = field(default_factory=list)
+ start_time: datetime | None = None
+ end_time: datetime | None = None
+
+ def to_summary(self) -> dict[str, Any]:
+ """Serialize to summary dict (no per-item results)."""
+ return {
+ "id": self.id,
+ "eval_set_id": self.eval_set_id,
+ "eval_set_name": self.eval_set_name,
+ "status": self.status,
+ "progress_completed": self.progress_completed,
+ "progress_total": self.progress_total,
+ "overall_score": self.overall_score,
+ "evaluator_scores": self.evaluator_scores,
+ "start_time": self.start_time.isoformat() if self.start_time else None,
+ "end_time": self.end_time.isoformat() if self.end_time else None,
+ }
+
+ def to_detail(self) -> dict[str, Any]:
+ """Serialize to detail dict (includes per-item results)."""
+ base = self.to_summary()
+ base["results"] = [
+ {
+ "name": r.name,
+ "inputs": r.inputs,
+ "expected_output": r.expected_output,
+ "scores": r.scores,
+ "overall_score": r.overall_score,
+ "output": str(r.output)
+ if isinstance(r.output, Exception)
+ else r.output,
+ "justifications": r.justifications,
+ "duration_ms": r.duration_ms,
+ "status": r.status,
+ "error": r.error,
+ "traces": r.traces,
+ }
+ for r in self.results
+ ]
+ return base
+
+ def start(self) -> None:
+ """Mark run as started."""
+ self.status = "running"
+ self.start_time = datetime.now(timezone.utc)
+
+ def complete(self) -> None:
+ """Mark run as completed, computing final scores."""
+ self.status = "completed"
+ self.end_time = datetime.now(timezone.utc)
+ self._compute_scores()
+
+ def fail(self) -> None:
+ """Mark run as failed."""
+ self.status = "failed"
+ self.end_time = datetime.now(timezone.utc)
+
+ def _compute_scores(self) -> None:
+ """Compute overall and per-evaluator scores from item results."""
+ scored = [r for r in self.results if r.status in ("completed", "failed")]
+ if not scored:
+ self.overall_score = 0.0
+ return
+
+ # Per-evaluator averages (failed items count as 0 for each evaluator)
+ evaluator_totals: dict[str, list[float]] = {}
+ for r in scored:
+ for ev_id, score in r.scores.items():
+ evaluator_totals.setdefault(ev_id, []).append(score)
+
+ failed = [r for r in scored if r.status == "failed"]
+ for ev_id in evaluator_totals:
+ for _ in failed:
+ evaluator_totals[ev_id].append(0.0)
+
+ self.evaluator_scores = {
+ ev_id: sum(scores) / len(scores)
+ for ev_id, scores in evaluator_totals.items()
+ }
+
+ # Overall = average of item overall_scores
+ self.overall_score = sum(r.overall_score for r in scored) / len(scored)
diff --git a/src/uipath/dev/server/__init__.py b/src/uipath/dev/server/__init__.py
index 2e3ba31..ad80bc7 100644
--- a/src/uipath/dev/server/__init__.py
+++ b/src/uipath/dev/server/__init__.py
@@ -24,9 +24,13 @@
StateData,
TraceData,
)
+from uipath.dev.models.eval_data import EvalItemResult, EvalRunState
from uipath.dev.models.execution import ExecutionRun
from uipath.dev.server.debug_bridge import WebDebugBridge
+from uipath.dev.services.agent_service import AgentService
+from uipath.dev.services.eval_service import EvalService
from uipath.dev.services.run_service import RunService
+from uipath.dev.services.skill_service import SkillService
logger = logging.getLogger(__name__)
@@ -86,6 +90,27 @@ def __init__(
on_run_removed=self.connection_manager.remove_run_subscriptions,
)
+ self.eval_service = EvalService(
+ runtime_factory=self.runtime_factory,
+ trace_manager=self.trace_manager,
+ on_eval_run_created=self._on_eval_run_created,
+ on_eval_run_progress=self._on_eval_run_progress,
+ on_eval_run_completed=self._on_eval_run_completed,
+ )
+
+ self.skill_service = SkillService()
+
+ self.agent_service = AgentService(
+ skill_service=self.skill_service,
+ on_status=self._on_agent_status,
+ on_text=self._on_agent_text,
+ on_plan=self._on_agent_plan,
+ on_tool_use=self._on_agent_tool_use,
+ on_tool_result=self._on_agent_tool_result,
+ on_tool_approval=self._on_agent_tool_approval,
+ on_error=self._on_agent_error,
+ )
+
def create_app(self) -> Any:
"""Create and return a FastAPI application."""
from uipath.dev.server.app import create_app
@@ -111,9 +136,8 @@ async def run_async(self) -> None:
daemon=True,
).start()
- # Start file watcher if factory_creator is available
- if self.factory_creator is not None:
- self._start_watcher()
+ # Start file watcher for editor auto-refresh and factory hot-reload
+ self._start_watcher()
config = uvicorn.Config(
app,
@@ -180,11 +204,11 @@ async def reload_factory(self) -> None:
def _start_watcher(self) -> None:
"""Start the file watcher background task."""
- from uipath.dev.server.watcher import watch_python_files
+ from uipath.dev.server.watcher import watch_project_files
self._watcher_stop = asyncio.Event()
self._watcher_task = asyncio.create_task(
- watch_python_files(
+ watch_project_files(
on_change=self._on_files_changed,
stop_event=self._watcher_stop,
)
@@ -200,8 +224,24 @@ def _stop_watcher(self) -> None:
def _on_files_changed(self, changed_files: list[str]) -> None:
"""Handle file change events from the watcher."""
- self.reload_pending = True
- self.connection_manager.broadcast_reload(changed_files)
+ # Convert to relative paths with forward slashes for frontend
+ cwd = os.getcwd()
+ relative_files = []
+ for f in changed_files:
+ try:
+ relative_files.append(os.path.relpath(f, cwd).replace("\\", "/"))
+ except ValueError:
+ continue # different drive on Windows
+
+ # Broadcast files.changed for editor auto-refresh
+ if relative_files:
+ self.connection_manager.broadcast_files_changed(relative_files)
+
+ # Factory hot-reload for Python files only
+ py_files = [f for f in changed_files if f.endswith((".py", ".pyx"))]
+ if py_files and self.factory_creator is not None:
+ self.reload_pending = True
+ self.connection_manager.broadcast_reload(py_files)
# ------------------------------------------------------------------
# Internal callbacks
@@ -231,6 +271,68 @@ def _on_state(self, state_data: StateData) -> None:
"""Broadcast state transition to subscribed WebSocket clients."""
self.connection_manager.broadcast_state(state_data)
+ def _on_eval_run_created(self, run: EvalRunState) -> None:
+ """Broadcast eval run created to all connected clients."""
+ self.connection_manager.broadcast_eval_run_created(run)
+
+ def _on_eval_run_progress(
+ self,
+ run_id: str,
+ completed: int,
+ total: int,
+ item_result: EvalItemResult | None,
+ ) -> None:
+ """Broadcast eval run progress to all connected clients."""
+ self.connection_manager.broadcast_eval_run_progress(
+ run_id, completed, total, item_result
+ )
+
+ def _on_eval_run_completed(self, run: EvalRunState) -> None:
+ """Broadcast eval run completed to all connected clients."""
+ self.connection_manager.broadcast_eval_run_completed(run)
+
+ def _on_agent_status(self, session_id: str, status: str) -> None:
+ """Broadcast agent status to all connected clients."""
+ self.connection_manager.broadcast_agent_status(session_id, status)
+
+ def _on_agent_text(self, session_id: str, content: str, done: bool) -> None:
+ """Broadcast agent text to all connected clients."""
+ self.connection_manager.broadcast_agent_text(session_id, content, done)
+
+ def _on_agent_plan(self, session_id: str, items: list[dict[str, str]]) -> None:
+ """Broadcast agent plan to all connected clients."""
+ self.connection_manager.broadcast_agent_plan(session_id, items)
+
+ def _on_agent_tool_use(
+ self, session_id: str, tool: str, args: dict[str, Any]
+ ) -> None:
+ """Broadcast agent tool use to all connected clients."""
+ self.connection_manager.broadcast_agent_tool_use(session_id, tool, args)
+
+ def _on_agent_tool_result(
+ self, session_id: str, tool: str, result: str, is_error: bool
+ ) -> None:
+ """Broadcast agent tool result to all connected clients."""
+ self.connection_manager.broadcast_agent_tool_result(
+ session_id, tool, result, is_error
+ )
+
+ def _on_agent_tool_approval(
+ self,
+ session_id: str,
+ tool_call_id: str,
+ tool: str,
+ args: dict[str, Any],
+ ) -> None:
+ """Broadcast agent tool approval request to all connected clients."""
+ self.connection_manager.broadcast_agent_tool_approval(
+ session_id, tool_call_id, tool, args
+ )
+
+ def _on_agent_error(self, session_id: str, message: str) -> None:
+ """Broadcast agent error to all connected clients."""
+ self.connection_manager.broadcast_agent_error(session_id, message)
+
@staticmethod
def _find_free_port(host: str, start_port: int, max_attempts: int = 100) -> int:
"""Find a free port starting from *start_port*.
diff --git a/src/uipath/dev/server/app.py b/src/uipath/dev/server/app.py
index d608338..9b2f8e8 100644
--- a/src/uipath/dev/server/app.py
+++ b/src/uipath/dev/server/app.py
@@ -149,7 +149,11 @@ async def _config():
return {"auth_enabled": auth_enabled, **_user_project}
# Register routes
+ from uipath.dev.server.routes.agent import router as agent_router
from uipath.dev.server.routes.entrypoints import router as entrypoints_router
+ from uipath.dev.server.routes.evals import router as evals_router
+ from uipath.dev.server.routes.evaluators import router as evaluators_router
+ from uipath.dev.server.routes.files import router as files_router
from uipath.dev.server.routes.graph import router as graph_router
from uipath.dev.server.routes.reload import router as reload_router
from uipath.dev.server.routes.runs import router as runs_router
@@ -166,6 +170,10 @@ async def _config():
app.include_router(runs_router, prefix="/api")
app.include_router(graph_router, prefix="/api")
app.include_router(reload_router, prefix="/api")
+ app.include_router(evaluators_router, prefix="/api")
+ app.include_router(evals_router, prefix="/api")
+ app.include_router(agent_router, prefix="/api")
+ app.include_router(files_router, prefix="/api")
app.include_router(ws_router)
# Auto-build frontend if source is available and build is stale
diff --git a/src/uipath/dev/server/frontend/package-lock.json b/src/uipath/dev/server/frontend/package-lock.json
index 30693ab..32e99fe 100644
--- a/src/uipath/dev/server/frontend/package-lock.json
+++ b/src/uipath/dev/server/frontend/package-lock.json
@@ -8,6 +8,7 @@
"name": "uipath-dev-frontend",
"version": "0.1.0",
"dependencies": {
+ "@monaco-editor/react": "^4.7.0",
"elkjs": "^0.11.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
@@ -801,6 +802,29 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@monaco-editor/loader": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz",
+ "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==",
+ "license": "MIT",
+ "dependencies": {
+ "state-local": "^1.0.6"
+ }
+ },
+ "node_modules/@monaco-editor/react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz",
+ "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==",
+ "license": "MIT",
+ "dependencies": {
+ "@monaco-editor/loader": "^1.5.0"
+ },
+ "peerDependencies": {
+ "monaco-editor": ">= 0.25.0 < 1",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/@reactflow/background": {
"version": "11.3.14",
"resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz",
@@ -2071,6 +2095,14 @@
"@types/react": "^19.2.0"
}
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT",
+ "optional": true,
+ "peer": true
+ },
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -2425,6 +2457,16 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/dompurify": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
+ "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "peer": true,
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.286",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz",
@@ -3091,6 +3133,19 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/marked": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
+ "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
"node_modules/mdast-util-find-and-replace": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
@@ -3924,6 +3979,17 @@
],
"license": "MIT"
},
+ "node_modules/monaco-editor": {
+ "version": "0.55.1",
+ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
+ "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "dompurify": "3.2.7",
+ "marked": "14.0.0"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -4280,6 +4346,12 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/state-local": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
+ "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
+ "license": "MIT"
+ },
"node_modules/stringify-entities": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
diff --git a/src/uipath/dev/server/frontend/package.json b/src/uipath/dev/server/frontend/package.json
index 9638678..a4cea6a 100644
--- a/src/uipath/dev/server/frontend/package.json
+++ b/src/uipath/dev/server/frontend/package.json
@@ -16,6 +16,7 @@
"reactflow": "^11.11.4",
"rehype-highlight": "^7.0.2",
"remark-gfm": "^4.0.1",
+ "@monaco-editor/react": "^4.7.0",
"zustand": "^5.0.0"
},
"devDependencies": {
diff --git a/src/uipath/dev/server/frontend/src/App.tsx b/src/uipath/dev/server/frontend/src/App.tsx
index 8249df3..af87a67 100644
--- a/src/uipath/dev/server/frontend/src/App.tsx
+++ b/src/uipath/dev/server/frontend/src/App.tsx
@@ -6,18 +6,38 @@ import { useWebSocket } from "./store/useWebSocket";
import { listRuns, listEntrypoints, getRun } from "./api/client";
import type { RunDetail } from "./types/run";
import { useHashRoute } from "./hooks/useHashRoute";
+import type { Section } from "./hooks/useHashRoute";
import { useIsMobile } from "./hooks/useIsMobile";
-import Sidebar from "./components/layout/Sidebar";
+import ActivityBar from "./components/layout/ActivityBar";
+import DebugSidebar from "./components/layout/DebugSidebar";
import StatusBar from "./components/layout/StatusBar";
import NewRunPanel from "./components/runs/NewRunPanel";
import SetupView from "./components/runs/SetupView";
import RunDetailsPanel from "./components/runs/RunDetailsPanel";
import ReloadToast from "./components/shared/ReloadToast";
+import ToastContainer from "./components/shared/ToastContainer";
+import { useEvalStore } from "./store/useEvalStore";
+import { listEvalSets, listEvaluators, listEvalRuns, listLocalEvaluators } from "./api/eval-client";
+import EvalsSidebar from "./components/evals/EvalsSidebar";
+import EvalSetDetail from "./components/evals/EvalSetDetail";
+import EvalRunResults from "./components/evals/EvalRunResults";
+import CreateEvalSetView from "./components/evals/CreateEvalSetView";
+import EvaluatorsSidebar from "./components/evaluators/EvaluatorsSidebar";
+import EvaluatorsView from "./components/evaluators/EvaluatorDetail";
+import CreateEvaluatorView from "./components/evaluators/CreateEvaluatorView";
+import ExplorerSidebar from "./components/explorer/ExplorerSidebar";
+import FileEditor from "./components/explorer/FileEditor";
+import AgentChatSidebar from "./components/agent/AgentChatSidebar";
+import { useExplorerStore } from "./store/useExplorerStore";
export default function App() {
const ws = useWebSocket();
const isMobile = useIsMobile();
const [sidebarOpen, setSidebarOpen] = useState(false);
+ const [sidebarWidth, setSidebarWidth] = useState(248);
+ const [isDraggingSidebar, setIsDraggingSidebar] = useState(false);
+ const [agentWidth, setAgentWidth] = useState(380);
+ const [isDraggingAgent, setIsDraggingAgent] = useState(false);
const {
runs,
selectedRunId,
@@ -33,14 +53,31 @@ export default function App() {
setActiveNode,
removeActiveNode,
} = useRunStore();
- const { view, runId: routeRunId, setupEntrypoint, setupMode, navigate } = useHashRoute();
+ const {
+ section,
+ view,
+ runId: routeRunId,
+ setupEntrypoint,
+ setupMode,
+ evalCreating,
+ evalSetId,
+ evalRunId,
+ evalRunItemName,
+ evaluatorCreateType,
+ evaluatorId,
+ evaluatorFilter,
+ explorerFile,
+ navigate,
+ } = useHashRoute();
+
+ const { setEvalSets, setEvaluators, setLocalEvaluators, setEvalRuns } = useEvalStore();
// Sync route runId → store selection
useEffect(() => {
- if (view === "details" && routeRunId && routeRunId !== selectedRunId) {
+ if (section === "debug" && view === "details" && routeRunId && routeRunId !== selectedRunId) {
selectRun(routeRunId);
}
- }, [view, routeRunId, selectedRunId, selectRun]);
+ }, [section, view, routeRunId, selectedRunId, selectRun]);
// Load existing runs, entrypoints, auth status, and config on mount
const initAuth = useAuthStore((s) => s.init);
@@ -54,6 +91,49 @@ export default function App() {
initConfig();
}, [setRuns, setEntrypoints, initAuth, initConfig]);
+ // Load eval data when switching to evals/evaluators section
+ useEffect(() => {
+ if (section === "evals") {
+ listEvalSets().then((sets) => setEvalSets(sets)).catch(console.error);
+ listEvalRuns().then((runs) => setEvalRuns(runs)).catch(console.error);
+ }
+ if (section === "evals" || section === "evaluators") {
+ listEvaluators().then((evs) => setEvaluators(evs)).catch(console.error);
+ listLocalEvaluators().then((evs) => setLocalEvaluators(evs)).catch(console.error);
+ }
+ }, [section, setEvalSets, setEvaluators, setLocalEvaluators, setEvalRuns]);
+
+ // Auto-select latest run or first eval set when navigating to evals with no selection
+ const evalSets = useEvalStore((s) => s.evalSets);
+ const evalRuns = useEvalStore((s) => s.evalRuns);
+ useEffect(() => {
+ if (section !== "evals" || evalCreating || evalSetId || evalRunId) return;
+ // Pick latest run by start_time
+ const runs = Object.values(evalRuns).sort(
+ (a, b) => new Date(b.start_time ?? 0).getTime() - new Date(a.start_time ?? 0).getTime(),
+ );
+ if (runs.length > 0) {
+ navigate(`#/evals/runs/${runs[0].id}`);
+ return;
+ }
+ // Fallback: first eval set
+ const sets = Object.values(evalSets);
+ if (sets.length > 0) {
+ navigate(`#/evals/sets/${sets[0].id}`);
+ }
+ }, [section, evalCreating, evalSetId, evalRunId, evalRuns, evalSets, navigate]);
+
+ // Keyboard shortcuts
+ useEffect(() => {
+ const onKeyDown = (e: KeyboardEvent) => {
+ if (e.key === "Escape" && sidebarOpen) {
+ setSidebarOpen(false);
+ }
+ };
+ window.addEventListener("keydown", onKeyDown);
+ return () => window.removeEventListener("keydown", onKeyDown);
+ }, [sidebarOpen]);
+
const selectedRun = selectedRunId ? runs[selectedRunId] : null;
// Shared helper: apply a full run detail response to the store
@@ -169,70 +249,288 @@ export default function App() {
}, [selectedRunId, selectedRun?.status, applyRunDetail]);
const handleRunCreated = (runId: string) => {
- navigate(`#/runs/${runId}/traces`);
+ navigate(`#/debug/runs/${runId}/traces`);
selectRun(runId);
setSidebarOpen(false);
};
const handleSelectRun = (runId: string) => {
- navigate(`#/runs/${runId}/traces`);
+ navigate(`#/debug/runs/${runId}/traces`);
selectRun(runId);
setSidebarOpen(false);
};
const handleNewRun = () => {
- navigate("#/new");
+ navigate("#/debug/new");
setSidebarOpen(false);
};
- return (
-
-
- {/* Mobile hamburger button */}
- {isMobile && !sidebarOpen && (
-
- )}
-
{
+ if (s === "debug") navigate("#/debug/new");
+ else if (s === "evals") navigate("#/evals");
+ else if (s === "evaluators") navigate("#/evaluators");
+ else if (s === "explorer") navigate("#/explorer");
+ };
+
+ // --- Sidebar col resize ---
+ const onSidebarResizeStart = useCallback((e: React.MouseEvent | React.TouchEvent) => {
+ e.preventDefault();
+ setIsDraggingSidebar(true);
+
+ const startX = "touches" in e ? e.touches[0].clientX : e.clientX;
+ const startW = sidebarWidth;
+
+ const onMove = (ev: MouseEvent | TouchEvent) => {
+ const clientX = "touches" in ev ? ev.touches[0].clientX : ev.clientX;
+ const newW = Math.max(200, Math.min(480, startW + (clientX - startX)));
+ setSidebarWidth(newW);
+ };
+
+ const onUp = () => {
+ setIsDraggingSidebar(false);
+ document.removeEventListener("mousemove", onMove);
+ document.removeEventListener("mouseup", onUp);
+ document.removeEventListener("touchmove", onMove);
+ document.removeEventListener("touchend", onUp);
+ document.body.style.cursor = "";
+ document.body.style.userSelect = "";
+ };
+
+ document.body.style.cursor = "col-resize";
+ document.body.style.userSelect = "none";
+ document.addEventListener("mousemove", onMove);
+ document.addEventListener("mouseup", onUp);
+ document.addEventListener("touchmove", onMove, { passive: false });
+ document.addEventListener("touchend", onUp);
+ }, [sidebarWidth]);
+
+ // --- Agent panel col resize ---
+ const onAgentResizeStart = useCallback((e: React.MouseEvent | React.TouchEvent) => {
+ e.preventDefault();
+ setIsDraggingAgent(true);
+
+ const startX = "touches" in e ? e.touches[0].clientX : e.clientX;
+ const startW = agentWidth;
+
+ const onMove = (ev: MouseEvent | TouchEvent) => {
+ const clientX = "touches" in ev ? ev.touches[0].clientX : ev.clientX;
+ // Dragging left increases width (panel is on the right)
+ const newW = Math.max(280, Math.min(500, startW - (clientX - startX)));
+ setAgentWidth(newW);
+ };
+
+ const onUp = () => {
+ setIsDraggingAgent(false);
+ document.removeEventListener("mousemove", onMove);
+ document.removeEventListener("mouseup", onUp);
+ document.removeEventListener("touchmove", onMove);
+ document.removeEventListener("touchend", onUp);
+ document.body.style.cursor = "";
+ document.body.style.userSelect = "";
+ };
+
+ document.body.style.cursor = "col-resize";
+ document.body.style.userSelect = "none";
+ document.addEventListener("mousemove", onMove);
+ document.addEventListener("mouseup", onUp);
+ document.addEventListener("touchmove", onMove, { passive: false });
+ document.addEventListener("touchend", onUp);
+ }, [agentWidth]);
+
+ const explorerTabs = useExplorerStore((s) => s.openTabs);
+
+ // --- Render main content based on section ---
+ const renderMainContent = () => {
+ if (section === "explorer") {
+ if (explorerTabs.length > 0 || explorerFile) return ;
+ return (
+
+ Select a file to view
+
+ );
+ }
+
+ if (section === "evals") {
+ if (evalCreating) return ;
+ if (evalRunId) return ;
+ if (evalSetId) return ;
+ return ;
+ }
+
+ if (section === "evaluators") {
+ if (evaluatorCreateType) {
+ return ;
+ }
+ return ;
+ }
+
+ // Debug section
+ if (view === "new") {
+ return ;
+ }
+ if (view === "setup" && setupEntrypoint && setupMode) {
+ return (
+ setSidebarOpen(false)}
/>
-
- {view === "new" ? (
-
- ) : view === "setup" && setupEntrypoint && setupMode ? (
-
- ) : selectedRun ? (
-
- ) : (
-
- Select a run or create a new one
+ );
+ }
+ if (selectedRun) {
+ return
;
+ }
+ return (
+
+ Select a run or create a new one
+
+ );
+ };
+
+ // --- Mobile layout ---
+ if (isMobile) {
+ return (
+
+
+ {!sidebarOpen && (
+
+ )}
+ {sidebarOpen && (
+ <>
+
setSidebarOpen(false)}
+ />
+
+ >
+ )}
+
+ {renderMainContent()}
+
+
+
+
+
+
+ );
+ }
+
+ // --- Desktop layout ---
+ return (
+
+
+ {/* Left aside: shared header + ActivityBar + section sidebar */}
+
+
+
+
+ {renderMainContent()}
+
+ {section === "explorer" && (
+ <>
+
+
+ >
)}
+
);
}
diff --git a/src/uipath/dev/server/frontend/src/api/agent-client.ts b/src/uipath/dev/server/frontend/src/api/agent-client.ts
new file mode 100644
index 0000000..318b247
--- /dev/null
+++ b/src/uipath/dev/server/frontend/src/api/agent-client.ts
@@ -0,0 +1,20 @@
+import type { AgentModel, AgentSkill } from "../types/agent";
+
+const BASE = "/api";
+
+export async function listAgentModels(): Promise
{
+ const res = await fetch(`${BASE}/agent/models`);
+ if (!res.ok) {
+ if (res.status === 401) return [];
+ throw new Error(`HTTP ${res.status}`);
+ }
+ return res.json();
+}
+
+export async function listAgentSkills(): Promise {
+ const res = await fetch(`${BASE}/agent/skills`);
+ if (!res.ok) {
+ throw new Error(`HTTP ${res.status}`);
+ }
+ return res.json();
+}
diff --git a/src/uipath/dev/server/frontend/src/api/eval-client.ts b/src/uipath/dev/server/frontend/src/api/eval-client.ts
new file mode 100644
index 0000000..eb6baa3
--- /dev/null
+++ b/src/uipath/dev/server/frontend/src/api/eval-client.ts
@@ -0,0 +1,126 @@
+import type { EvaluatorInfo, LocalEvaluator, EvalSetSummary, EvalSetDetail, EvalItem, EvalRunSummary, EvalRunDetail } from "../types/eval";
+
+const BASE = "/api";
+
+async function fetchJson(url: string, options?: RequestInit): Promise {
+ const res = await fetch(url, options);
+ if (!res.ok) {
+ let errorDetail;
+ try {
+ const body = await res.json();
+ errorDetail = body.detail || res.statusText;
+ } catch {
+ errorDetail = res.statusText;
+ }
+ const error = new Error(`HTTP ${res.status}`);
+ (error as any).detail = errorDetail;
+ (error as any).status = res.status;
+ throw error;
+ }
+ return res.json();
+}
+
+export async function listEvaluators(): Promise {
+ return fetchJson(`${BASE}/evaluators`);
+}
+
+export async function listEvalSets(): Promise {
+ return fetchJson(`${BASE}/eval-sets`);
+}
+
+export async function createEvalSet(body: {
+ name: string;
+ evaluator_refs: string[];
+}): Promise {
+ return fetchJson(`${BASE}/eval-sets`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(body),
+ });
+}
+
+export async function addEvalItem(
+ evalSetId: string,
+ item: {
+ name: string;
+ inputs: Record;
+ expected_output: unknown;
+ evaluation_criterias?: Record>;
+ },
+): Promise {
+ return fetchJson(`${BASE}/eval-sets/${encodeURIComponent(evalSetId)}/items`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(item),
+ });
+}
+
+export async function deleteEvalItem(
+ evalSetId: string,
+ itemName: string,
+): Promise {
+ await fetchJson(`${BASE}/eval-sets/${encodeURIComponent(evalSetId)}/items/${encodeURIComponent(itemName)}`, {
+ method: "DELETE",
+ });
+}
+
+export async function getEvalSet(id: string): Promise {
+ return fetchJson(`${BASE}/eval-sets/${encodeURIComponent(id)}`);
+}
+
+export async function startEvalRun(evalSetId: string): Promise {
+ return fetchJson(`${BASE}/eval-sets/${encodeURIComponent(evalSetId)}/runs`, {
+ method: "POST",
+ });
+}
+
+export async function listEvalRuns(): Promise {
+ return fetchJson(`${BASE}/eval-runs`);
+}
+
+export async function getEvalRun(id: string): Promise {
+ return fetchJson(`${BASE}/eval-runs/${encodeURIComponent(id)}`);
+}
+
+export async function listLocalEvaluators(): Promise {
+ return fetchJson(`${BASE}/local-evaluators`);
+}
+
+export async function createLocalEvaluator(body: {
+ name: string;
+ description: string;
+ evaluator_type_id: string;
+ config: Record;
+}): Promise {
+ return fetchJson(`${BASE}/local-evaluators`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(body),
+ });
+}
+
+export async function updateEvalSetEvaluators(
+ evalSetId: string,
+ evaluatorRefs: string[],
+): Promise {
+ return fetchJson(`${BASE}/eval-sets/${encodeURIComponent(evalSetId)}/evaluators`, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ evaluator_refs: evaluatorRefs }),
+ });
+}
+
+export async function updateLocalEvaluator(
+ id: string,
+ body: {
+ description?: string;
+ evaluator_type_id?: string;
+ config?: Record;
+ },
+): Promise {
+ return fetchJson(`${BASE}/local-evaluators/${encodeURIComponent(id)}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(body),
+ });
+}
diff --git a/src/uipath/dev/server/frontend/src/api/explorer-client.ts b/src/uipath/dev/server/frontend/src/api/explorer-client.ts
new file mode 100644
index 0000000..e178222
--- /dev/null
+++ b/src/uipath/dev/server/frontend/src/api/explorer-client.ts
@@ -0,0 +1,37 @@
+import type { FileEntry, FileContent } from "../types/explorer";
+
+const BASE = "/api";
+
+async function fetchJson(url: string, options?: RequestInit): Promise {
+ const res = await fetch(url, options);
+ if (!res.ok) {
+ let errorDetail;
+ try {
+ const body = await res.json();
+ errorDetail = body.detail || res.statusText;
+ } catch {
+ errorDetail = res.statusText;
+ }
+ const error = new Error(`HTTP ${res.status}`);
+ (error as any).detail = errorDetail;
+ (error as any).status = res.status;
+ throw error;
+ }
+ return res.json();
+}
+
+export async function listDirectory(path: string): Promise {
+ return fetchJson(`${BASE}/files/tree?path=${encodeURIComponent(path)}`);
+}
+
+export async function readFile(path: string): Promise {
+ return fetchJson(`${BASE}/files/content?path=${encodeURIComponent(path)}`);
+}
+
+export async function saveFile(path: string, content: string): Promise {
+ await fetchJson(`${BASE}/files/content?path=${encodeURIComponent(path)}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ content }),
+ });
+}
diff --git a/src/uipath/dev/server/frontend/src/api/websocket.ts b/src/uipath/dev/server/frontend/src/api/websocket.ts
index 64bc1a7..b0012fe 100644
--- a/src/uipath/dev/server/frontend/src/api/websocket.ts
+++ b/src/uipath/dev/server/frontend/src/api/websocket.ts
@@ -123,4 +123,25 @@ export class WsClient {
setBreakpoints(runId: string, breakpoints: string[]): void {
this.send("debug.set_breakpoints", { run_id: runId, breakpoints });
}
+
+ sendAgentMessage(text: string, model: string, sessionId?: string | null, skillIds?: string[]): void {
+ this.send("agent.message", {
+ text,
+ model,
+ session_id: sessionId ?? undefined,
+ skill_ids: skillIds && skillIds.length > 0 ? skillIds : undefined,
+ });
+ }
+
+ sendAgentStop(sessionId: string): void {
+ this.send("agent.stop", { session_id: sessionId });
+ }
+
+ sendToolApproval(sessionId: string, toolCallId: string, approved: boolean): void {
+ this.send("agent.tool_response", {
+ session_id: sessionId,
+ tool_call_id: toolCallId,
+ approved,
+ });
+ }
}
diff --git a/src/uipath/dev/server/frontend/src/components/agent/AgentChatSidebar.tsx b/src/uipath/dev/server/frontend/src/components/agent/AgentChatSidebar.tsx
new file mode 100644
index 0000000..863546b
--- /dev/null
+++ b/src/uipath/dev/server/frontend/src/components/agent/AgentChatSidebar.tsx
@@ -0,0 +1,388 @@
+import { useCallback, useEffect, useRef, useState } from "react";
+import { useAgentStore } from "../../store/useAgentStore";
+import { useAuthStore } from "../../store/useAuthStore";
+import { listAgentModels, listAgentSkills } from "../../api/agent-client";
+import { getWs } from "../../store/useWebSocket";
+import AgentMessageComponent from "./AgentMessage";
+import type { AgentSkill } from "../../types/agent";
+
+export default function AgentChatSidebar() {
+ const ws = useRef(getWs()).current;
+ const [input, setInput] = useState("");
+ const scrollRef = useRef(null);
+ const stickToBottom = useRef(true);
+
+ const authEnabled = useAuthStore((s) => s.enabled);
+ const authStatus = useAuthStore((s) => s.status);
+ const isAuthenticated = !authEnabled || authStatus === "authenticated";
+
+ const {
+ sessionId,
+ status,
+ messages,
+ models,
+ selectedModel,
+ modelsLoading,
+ skills,
+ selectedSkillIds,
+ skillsLoading,
+ setModels,
+ setSelectedModel,
+ setModelsLoading,
+ setSkills,
+ setSelectedSkillIds,
+ toggleSkill,
+ setSkillsLoading,
+ addUserMessage,
+ clearSession,
+ } = useAgentStore();
+
+ // Load models on mount if authenticated
+ useEffect(() => {
+ if (!isAuthenticated) return;
+ if (models.length > 0) return;
+ setModelsLoading(true);
+ listAgentModels()
+ .then((m) => {
+ setModels(m);
+ if (m.length > 0 && !selectedModel) {
+ const claude = m.find((x) => x.model_name.includes("claude"));
+ setSelectedModel(claude ? claude.model_name : m[0].model_name);
+ }
+ })
+ .catch(console.error)
+ .finally(() => setModelsLoading(false));
+ }, [isAuthenticated, models.length, selectedModel, setModels, setSelectedModel, setModelsLoading]);
+
+ // Load skills on mount
+ useEffect(() => {
+ if (skills.length > 0) return;
+ setSkillsLoading(true);
+ listAgentSkills()
+ .then((s) => {
+ setSkills(s);
+ setSelectedSkillIds(s.map((sk) => sk.id));
+ })
+ .catch(console.error)
+ .finally(() => setSkillsLoading(false));
+ }, [skills.length, setSkills, setSelectedSkillIds, setSkillsLoading]);
+
+ const [showScrollTop, setShowScrollTop] = useState(false);
+
+ const handleScroll = () => {
+ const el = scrollRef.current;
+ if (!el) return;
+ const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 40;
+ stickToBottom.current = atBottom;
+ setShowScrollTop(el.scrollTop > 100);
+ };
+
+ // Auto-scroll on any message content change (streaming tokens)
+ useEffect(() => {
+ if (stickToBottom.current && scrollRef.current) {
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
+ }
+ });
+
+ const isBusy = status === "thinking" || status === "executing" || status === "planning";
+
+ const handleSend = useCallback(() => {
+ const text = input.trim();
+ if (!text || !selectedModel || isBusy) return;
+ stickToBottom.current = true;
+ addUserMessage(text);
+ ws.sendAgentMessage(text, selectedModel, sessionId, selectedSkillIds);
+ setInput("");
+ }, [input, selectedModel, isBusy, sessionId, selectedSkillIds, addUserMessage, ws]);
+
+ const handleStop = useCallback(() => {
+ if (sessionId) ws.sendAgentStop(sessionId);
+ }, [sessionId, ws]);
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ handleSend();
+ }
+ };
+
+ const canSend = !isBusy && !!selectedModel && input.trim().length > 0;
+
+ // Auth gate
+ if (!isAuthenticated) {
+ return (
+
+
+
+
+
+
Sign in to use Agent
+
Authentication is required to access the coding agent.
+
+
+
+ );
+ }
+
+ return (
+
+
0}
+ isBusy={isBusy}
+ />
+
+ {/* Messages */}
+
+
+ {messages.length === 0 && (
+
+ No messages yet
+
+ )}
+ {messages.map((msg) => (
+
+ ))}
+ {isBusy && (
+
+
+
+
+ {status === "thinking" ? "Thinking..." : status === "executing" ? "Executing..." : "Planning..."}
+
+
+
+ )}
+
+ {showScrollTop && (
+
+ )}
+
+
+ {/* Input — matches ChatInput from debug view */}
+
+ setInput(e.target.value)}
+ onKeyDown={handleKeyDown}
+ disabled={isBusy || !selectedModel}
+ placeholder={isBusy ? "Waiting for response..." : "Message..."}
+ className="flex-1 bg-transparent text-sm py-1 disabled:opacity-40 placeholder:text-[var(--text-muted)]"
+ style={{ color: "var(--text-primary)" }}
+ />
+
+
+
+ );
+}
+
+function Header({
+ selectedModel,
+ models,
+ modelsLoading,
+ onModelChange,
+ skills,
+ selectedSkillIds,
+ skillsLoading,
+ onToggleSkill,
+ onClear,
+ onStop,
+ hasMessages,
+ isBusy,
+}: {
+ selectedModel: string | null;
+ models: { model_name: string; vendor: string | null }[];
+ modelsLoading: boolean;
+ onModelChange: (model: string) => void;
+ skills: AgentSkill[];
+ selectedSkillIds: string[];
+ skillsLoading: boolean;
+ onToggleSkill: (id: string) => void;
+ onClear: () => void;
+ onStop: () => void;
+ hasMessages: boolean;
+ isBusy: boolean;
+}) {
+ const [skillsOpen, setSkillsOpen] = useState(false);
+ const dropdownRef = useRef(null);
+
+ // Close dropdown when clicking outside
+ useEffect(() => {
+ if (!skillsOpen) return;
+ const handler = (e: MouseEvent) => {
+ if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
+ setSkillsOpen(false);
+ }
+ };
+ document.addEventListener("mousedown", handler);
+ return () => document.removeEventListener("mousedown", handler);
+ }, [skillsOpen]);
+
+ return (
+
+
+ Agent
+
+
+ {/* Skills dropdown */}
+ {!skillsLoading && skills.length > 0 && (
+
+
+ {skillsOpen && (
+
+ {skills.map((s) => (
+
+ ))}
+
+ )}
+
+ )}
+ {isBusy && (
+
+ )}
+ {hasMessages && !isBusy && (
+
+ )}
+
+ );
+}
diff --git a/src/uipath/dev/server/frontend/src/components/agent/AgentMessage.tsx b/src/uipath/dev/server/frontend/src/components/agent/AgentMessage.tsx
new file mode 100644
index 0000000..a389e57
--- /dev/null
+++ b/src/uipath/dev/server/frontend/src/components/agent/AgentMessage.tsx
@@ -0,0 +1,247 @@
+import { useState } from "react";
+import Markdown from "react-markdown";
+import rehypeHighlight from "rehype-highlight";
+import remarkGfm from "remark-gfm";
+import type { AgentMessage as AgentMessageType, AgentToolCall } from "../../types/agent";
+import { useAgentStore } from "../../store/useAgentStore";
+import { getWs } from "../../store/useWebSocket";
+
+interface Props {
+ message: AgentMessageType;
+}
+
+const ROLE_CONFIG: Record = {
+ user: { label: "You", color: "var(--info)" },
+ assistant: { label: "AI", color: "var(--success)" },
+ tool: { label: "Tool", color: "var(--warning)" },
+ plan: { label: "Plan", color: "var(--accent)" },
+};
+
+function PlanCard({ message }: Props) {
+ const items = message.planItems ?? [];
+ return (
+
+
+
+ {items.map((item, i) => (
+
+ {item.status === "completed" ? (
+
+ ) : item.status === "in_progress" ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+ {item.title}
+
+
+ ))}
+
+
+ );
+}
+
+function SingleToolCall({ tc }: { tc: AgentToolCall }) {
+ const isPending = tc.status === "pending";
+ const isDenied = tc.status === "denied";
+ const [expanded, setExpanded] = useState(false);
+ const hasResult = tc.result !== undefined;
+
+ const handleApproval = (approved: boolean) => {
+ if (!tc.tool_call_id) return;
+ const sessionId = useAgentStore.getState().sessionId;
+ if (!sessionId) return;
+ useAgentStore.getState().resolveToolApproval(tc.tool_call_id, approved);
+ getWs().sendToolApproval(sessionId, tc.tool_call_id, approved);
+ };
+
+ /* ── Pending: card layout matching ChatInterrupt ── */
+ if (isPending) {
+ return (
+
+
+
+ Action Required
+
+
+ {tc.tool}
+
+
+
+ {tc.args != null && (
+
+ {JSON.stringify(tc.args, null, 2)}
+
+ )}
+
+
+
+
+
+
+ );
+ }
+
+ /* ── Resolved / completed: compact inline style ── */
+ const statusColor = isDenied
+ ? "var(--error)"
+ : hasResult
+ ? tc.is_error
+ ? "var(--error)"
+ : "var(--success)"
+ : "var(--text-muted)";
+
+ const statusIcon = isDenied ? "\u2717" : hasResult ? (tc.is_error ? "\u2717" : "\u2713") : "\u2022";
+
+ return (
+
+
+
+
+ {expanded && (
+
+
+
Arguments
+
+ {JSON.stringify(tc.args, null, 2)}
+
+
+ {hasResult && (
+
+
+ {tc.is_error ? "Error" : "Result"}
+
+
+ {tc.result}
+
+
+ )}
+
+ )}
+
+ );
+}
+
+function ToolCard({ message }: Props) {
+ const calls = message.toolCalls ?? (message.toolCall ? [message.toolCall] : []);
+ if (calls.length === 0) return null;
+
+ return (
+
+
+
+
+ {calls.length === 1 ? "Tool" : `Tools (${calls.length})`}
+
+
+
+ {calls.map((tc, i) => (
+
+ ))}
+
+
+ );
+}
+
+export default function AgentMessageComponent({ message }: Props) {
+ if (message.role === "plan") return ;
+ if (message.role === "tool") return ;
+
+ const roleKey = message.role === "user" ? "user" : "assistant";
+ const role = ROLE_CONFIG[roleKey];
+
+ return (
+
+
+ {message.content && (
+ message.role === "user" ? (
+
+ {message.content}
+
+ ) : (
+
+ {message.content}
+
+ )
+ )}
+
+ );
+}
diff --git a/src/uipath/dev/server/frontend/src/components/chat/ChatInput.tsx b/src/uipath/dev/server/frontend/src/components/chat/ChatInput.tsx
index 55a3d72..95ec9e5 100644
--- a/src/uipath/dev/server/frontend/src/components/chat/ChatInput.tsx
+++ b/src/uipath/dev/server/frontend/src/components/chat/ChatInput.tsx
@@ -36,13 +36,14 @@ export default function ChatInput({ onSend, disabled, placeholder }: Props) {
onKeyDown={handleKeyDown}
disabled={disabled}
placeholder={placeholder ?? "Message..."}
- className="flex-1 bg-transparent text-sm py-1 focus:outline-none disabled:opacity-40 placeholder:text-[var(--text-muted)]"
+ className="flex-1 bg-transparent text-sm py-1 disabled:opacity-40 placeholder:text-[var(--text-muted)]"
style={{ color: "var(--text-primary)" }}
/>