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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-dev"
version = "0.0.69"
version = "0.0.71"
description = "UiPath Developer Console"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
37 changes: 33 additions & 4 deletions src/uipath/dev/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,31 @@ async def reload_factory(self) -> None:
self.reload_pending = False
logger.debug("Factory reloaded successfully")

async def _auto_reload(self, py_files: list[str]) -> None:
"""Auto-reload factory when Python files change.

If runs are active, broadcast reload event so the frontend shows a
manual-reload prompt instead.
"""
active = [
r
for r in self.run_service.runs.values()
if r.status in ("pending", "running")
]
if active:
logger.debug("Runs in progress — deferring reload to user")
self.reload_pending = True
self.connection_manager.broadcast_reload(py_files)
return

try:
await self.reload_factory()
self.connection_manager.broadcast_reload(py_files, reloaded=True)
except Exception:
logger.warning("Auto-reload failed", exc_info=True)
self.reload_pending = True
self.connection_manager.broadcast_reload(py_files)

def _start_watcher(self) -> None:
"""Start the file watcher background task."""
from uipath.dev.server.watcher import watch_project_files
Expand Down Expand Up @@ -231,11 +256,15 @@ def _on_files_changed(self, changed_files: list[str]) -> None:
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"))]
# Factory hot-reload for Python files only (use normalized relative paths)
py_files = [f for f in relative_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)
loop = self.connection_manager._get_loop()

def _schedule_reload(files: list[str] = py_files) -> None:
asyncio.ensure_future(self._auto_reload(files))

loop.call_soon_threadsafe(_schedule_reload)

# ------------------------------------------------------------------
# Internal callbacks
Expand Down
9 changes: 8 additions & 1 deletion src/uipath/dev/server/frontend/src/api/agent-client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { AgentModel, AgentSessionState, AgentSkill } from "../types/agent";
import type { AgentModel, AgentRawState, AgentSessionState, AgentSkill } from "../types/agent";

const BASE = "/api";

Expand Down Expand Up @@ -26,6 +26,13 @@ export async function getAgentSessionState(sessionId: string): Promise<AgentSess
return res.json();
}

export async function getAgentSessionRawState(sessionId: string): Promise<AgentRawState | null> {
const res = await fetch(`${BASE}/agent/session/${sessionId}/raw-state`);
if (res.status === 404) return null;
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
}

export async function listAgentSkills(): Promise<AgentSkill[]> {
const res = await fetch(`${BASE}/agent/skills`);
if (!res.ok) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useAgentStore } from "../../store/useAgentStore";
import { useAuthStore } from "../../store/useAuthStore";
import { useExplorerStore } from "../../store/useExplorerStore";
import { getAgentSessionState, listAgentModels, listAgentSkills } from "../../api/agent-client";
import { getWs } from "../../store/useWebSocket";
import AgentMessageComponent from "./AgentMessage";
Expand Down Expand Up @@ -412,6 +413,18 @@ function Header({
)}
</div>
)}
{hasMessages && (
<button
onClick={() => useExplorerStore.getState().openTab("__agent_state__")}
className="shrink-0 text-[11px] font-semibold px-2 py-1 rounded cursor-pointer"
style={{ background: "transparent", border: "1px solid var(--border)", color: "var(--text-muted)" }}
title="Open agent trace inspector"
onMouseEnter={(e) => { e.currentTarget.style.background = "var(--bg-hover)"; e.currentTarget.style.color = "var(--text-primary)"; }}
onMouseLeave={(e) => { e.currentTarget.style.background = "transparent"; e.currentTarget.style.color = "var(--text-muted)"; }}
>
Trace
</button>
)}
{isBusy && (
<button
onClick={onStop}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { AgentMessage as AgentMessageType, AgentToolCall } from "../../type
import { useAgentStore } from "../../store/useAgentStore";
import { getWs } from "../../store/useWebSocket";
import { getAgentSessionDiagnostics } from "../../api/agent-client";
import { DiffView } from "./DiffView";

interface Props {
message: AgentMessageType;
Expand Down Expand Up @@ -81,6 +82,15 @@ function PlanCard({ message }: Props) {
);
}

function isEditFileDiff(tc: AgentToolCall): { path?: string; old_string: string; new_string: string } | null {
if (tc.tool !== "edit_file" || !tc.args) return null;
const a = tc.args as Record<string, unknown>;
if (typeof a.old_string === "string" && typeof a.new_string === "string") {
return { path: a.file_path as string | undefined, old_string: a.old_string, new_string: a.new_string };
}
return null;
}

function ToolApprovalCard({ tc }: { tc: AgentToolCall }) {
const handleApproval = (approved: boolean) => {
if (!tc.tool_call_id) return;
Expand All @@ -90,6 +100,8 @@ function ToolApprovalCard({ tc }: { tc: AgentToolCall }) {
getWs().sendToolApproval(sessionId, tc.tool_call_id, approved);
};

const diff = isEditFileDiff(tc);

return (
<div
className="rounded-lg overflow-hidden"
Expand All @@ -111,16 +123,25 @@ function ToolApprovalCard({ tc }: { tc: AgentToolCall }) {
>
{tc.tool}
</span>
{diff?.path && (
<span className="text-[11px] font-mono truncate" style={{ color: "var(--text-muted)" }}>
{diff.path}
</span>
)}
</div>

{tc.args != null && (
{diff ? (
<div className="px-3 py-2" style={{ background: "var(--bg-secondary)" }}>
<DiffView path={diff.path} oldStr={diff.old_string} newStr={diff.new_string} />
</div>
) : tc.args != null ? (
<pre
className="px-3 py-2 text-[11px] font-mono whitespace-pre-wrap break-words overflow-y-auto leading-normal"
style={{ background: "var(--bg-secondary)", color: "var(--text-secondary)", maxHeight: 200 }}
>
{JSON.stringify(tc.args, null, 2)}
</pre>
)}
) : null}

<div
className="flex items-center gap-2 px-3 py-2"
Expand Down Expand Up @@ -188,6 +209,7 @@ function ToolChip({ tc, active, onClick }: { tc: AgentToolCall; active: boolean;
function ToolDetailPanel({ tc }: { tc: AgentToolCall }) {
const hasResult = tc.result !== undefined;
const hasArgs = tc.args != null && Object.keys(tc.args).length > 0;
const diff = isEditFileDiff(tc);

return (
<div
Expand All @@ -202,16 +224,23 @@ function ToolDetailPanel({ tc }: { tc: AgentToolCall }) {
<span className="text-[11px] font-mono font-semibold" style={{ color: "var(--text-primary)" }}>
{tc.tool}
</span>
{diff?.path && (
<span className="text-[11px] font-mono truncate" style={{ color: "var(--text-muted)" }}>{diff.path}</span>
)}
{tc.is_error && (
<span className="text-[10px] font-semibold px-1.5 py-0.5 rounded" style={{ background: "color-mix(in srgb, var(--error) 15%, transparent)", color: "var(--error)" }}>
Error
</span>
)}
</div>

{/* Args + Result side by side (or stacked) */}
{/* Args + Result stacked */}
<div className="flex flex-col gap-0">
{hasArgs && (
{diff ? (
<div className="px-3 py-2" style={{ borderBottom: hasResult ? "1px solid var(--border)" : "none" }}>
<DiffView path={diff.path} oldStr={diff.old_string} newStr={diff.new_string} />
</div>
) : hasArgs ? (
<div style={{ borderBottom: hasResult ? "1px solid var(--border)" : "none" }}>
<div className="px-3 pt-1.5 pb-0.5">
<span className="text-[10px] uppercase tracking-wider font-semibold" style={{ color: "var(--text-muted)" }}>Input</span>
Expand All @@ -220,7 +249,7 @@ function ToolDetailPanel({ tc }: { tc: AgentToolCall }) {
{JSON.stringify(tc.args, null, 2)}
</pre>
</div>
)}
) : null}
{hasResult && (
<div>
<div className="px-3 pt-1.5 pb-0.5">
Expand Down
Loading