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.61"
version = "0.0.62"
description = "UiPath Developer Console"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,27 @@ export default function AgentChatSidebar() {

const isBusy = status === "thinking" || status === "executing" || status === "planning";

const textareaRef = useRef<HTMLTextAreaElement>(null);

const resetTextareaHeight = () => {
const ta = textareaRef.current;
if (!ta) return;
ta.style.height = "auto";
ta.style.height = Math.min(ta.scrollHeight, 200) + "px";
};

const handleSend = useCallback(() => {
const text = input.trim();
if (!text || !selectedModel || isBusy) return;
stickToBottom.current = true;
addUserMessage(text);
ws.sendAgentMessage(text, selectedModel, sessionId, selectedSkillIds);
setInput("");
// Reset textarea height after send
requestAnimationFrame(() => {
const ta = textareaRef.current;
if (ta) ta.style.height = "auto";
});
}, [input, selectedModel, isBusy, sessionId, selectedSkillIds, addUserMessage, ws]);

const handleStop = useCallback(() => {
Expand Down Expand Up @@ -164,9 +178,15 @@ export default function AgentChatSidebar() {
className="h-full overflow-y-auto px-3 py-2 space-y-0.5"
>
{messages.length === 0 && (
<p className="text-[var(--text-muted)] text-sm text-center py-6">
No messages yet
</p>
<div className="flex flex-col items-center justify-center py-10 px-4 gap-3" style={{ color: "var(--text-muted)" }}>
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
</svg>
<div className="text-center space-y-1.5">
<p className="text-sm font-medium" style={{ color: "var(--text-secondary)" }}>Ask the agent to help you code</p>
<p className="text-xs leading-relaxed">Create agents, functions, evaluations,<br />or ask questions about your project.</p>
</div>
</div>
)}
{messages.map((msg) => (
<AgentMessageComponent key={msg.id} message={msg} />
Expand Down Expand Up @@ -196,24 +216,29 @@ export default function AgentChatSidebar() {
)}
</div>

{/* Input — matches ChatInput from debug view */}
{/* Input */}
<div
className="flex items-center gap-2 px-3 py-2 border-t"
className="flex items-end gap-2 px-3 py-2 border-t"
style={{ borderColor: "var(--border)" }}
>
<input
<textarea
ref={textareaRef}
value={input}
onChange={(e) => setInput(e.target.value)}
onChange={(e) => {
setInput(e.target.value);
resetTextareaHeight();
}}
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)" }}
rows={2}
className="flex-1 bg-transparent text-sm py-1 disabled:opacity-40 placeholder:text-[var(--text-muted)] resize-none"
style={{ color: "var(--text-primary)", maxHeight: 200, overflow: "auto" }}
/>
<button
onClick={handleSend}
disabled={!canSend}
className="text-xs font-semibold px-3 py-1.5 rounded transition-colors cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed"
className="text-xs font-semibold px-3 py-1.5 rounded transition-colors cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed shrink-0"
aria-label="Send message"
style={{
color: canSend ? "var(--accent)" : "var(--text-muted)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,17 @@ function SingleToolCall({ tc }: { tc: AgentToolCall }) {
);
}

const VISIBLE_TOOL_CALLS = 3;

function ToolCard({ message }: Props) {
const calls = message.toolCalls ?? (message.toolCall ? [message.toolCall] : []);
const [showAll, setShowAll] = useState(false);
if (calls.length === 0) return null;

const hiddenCount = calls.length - VISIBLE_TOOL_CALLS;
const shouldCollapse = hiddenCount > 0 && !showAll;
const visibleCalls = shouldCollapse ? calls.slice(-VISIBLE_TOOL_CALLS) : calls;

return (
<div className="py-1.5">
<div className="flex items-center gap-1.5 mb-0.5">
Expand All @@ -202,8 +209,24 @@ function ToolCard({ message }: Props) {
</span>
</div>
<div className="space-y-1">
{calls.map((tc, i) => (
<SingleToolCall key={i} tc={tc} />
{shouldCollapse && (
<button
onClick={() => setShowAll(true)}
className="ml-2.5 inline-flex items-center gap-1 text-[11px] font-mono px-2 py-1 rounded cursor-pointer hover:brightness-125"
style={{
background: "var(--bg-primary)",
border: "1px solid var(--border)",
color: "var(--text-muted)",
}}
>
{hiddenCount} more tool {hiddenCount === 1 ? "call" : "calls"}
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" style={{ marginLeft: 2 }}>
<path d="M6 9l6 6 6-6" />
</svg>
</button>
)}
{visibleCalls.map((tc, i) => (
<SingleToolCall key={shouldCollapse ? i + hiddenCount : i} tc={tc} />
))}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default function ReloadToast() {
<div className="fixed top-4 left-1/2 -translate-x-1/2 z-50 flex items-center justify-between px-5 py-2.5 rounded-lg shadow-lg min-w-[400px]"
style={{ background: "var(--bg-secondary)", border: "1px solid var(--bg-tertiary)" }}>
<span className="text-sm" style={{ color: "var(--text-secondary)" }}>
Files changed reload to apply
Files changed, reload to apply
</span>
<div className="flex items-center gap-2">
<button
Expand Down
17 changes: 16 additions & 1 deletion src/uipath/dev/server/frontend/src/store/useWebSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useRunStore } from "./useRunStore";
import { useEvalStore } from "./useEvalStore";
import { useAgentStore } from "./useAgentStore";
import { useExplorerStore } from "./useExplorerStore";
import { readFile } from "../api/explorer-client";
import { readFile, listDirectory } from "../api/explorer-client";
import type { RunSummary, TraceSpan, LogEntry, InterruptEvent } from "../types/run";
import type { EvalRunSummary, EvalItemResult } from "../types/eval";
import type { AgentPlanItem, AgentStatus } from "../types/agent";
Expand Down Expand Up @@ -72,6 +72,7 @@ export function useWebSocket() {
const changedFiles = msg.payload.files as string[];
const changedSet = new Set(changedFiles);
const explorer = useExplorerStore.getState();
// Refresh open tab contents
for (const tab of explorer.openTabs) {
if (explorer.dirty[tab] || !changedSet.has(tab)) continue;
readFile(tab).then((fc) => {
Expand All @@ -81,6 +82,20 @@ export function useWebSocket() {
s.setFileContent(tab, fc);
}).catch(() => {});
}
// Refresh directory listings for already-loaded parent dirs
const dirsToRefresh = new Set<string>();
for (const filePath of changedFiles) {
const lastSlash = filePath.lastIndexOf("/");
const parentDir = lastSlash === -1 ? "" : filePath.substring(0, lastSlash);
if (parentDir in explorer.children) {
dirsToRefresh.add(parentDir);
}
}
for (const dir of dirsToRefresh) {
listDirectory(dir)
.then((entries) => useExplorerStore.getState().setChildren(dir, entries))
.catch(() => {});
}
break;
}
// Eval events
Expand Down
5 changes: 4 additions & 1 deletion src/uipath/dev/server/routes/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,5 +191,8 @@ async def save_file(path: str, body: SaveFileBody) -> dict[str, str]:
"""Save file content. Creates parent dirs if needed."""
target = _resolve_safe(path)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(body.content, encoding="utf-8")
# Write bytes directly to avoid Python text-mode converting \n → \r\n on
# Windows, which would add extra carriage returns on every save since
# Monaco normalizes line endings to \n.
target.write_bytes(body.content.encode("utf-8"))
return {"status": "ok"}

Large diffs are not rendered by default.

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion src/uipath/dev/server/static/assets/index-BhpA3bEW.css

This file was deleted.

1 change: 1 addition & 0 deletions src/uipath/dev/server/static/assets/index-DKf_uUe0.css

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/uipath/dev/server/static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>UiPath Developer Console</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<script type="module" crossorigin src="/assets/index-Gpw0SLbu.js"></script>
<script type="module" crossorigin src="/assets/index-B2xfJE6O.js"></script>
<link rel="modulepreload" crossorigin href="/assets/vendor-react-BN_uQvcy.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-reactflow-BP_V7ttx.js">
<link rel="stylesheet" crossorigin href="/assets/vendor-reactflow-B5DZHykP.css">
<link rel="stylesheet" crossorigin href="/assets/index-BhpA3bEW.css">
<link rel="stylesheet" crossorigin href="/assets/index-DKf_uUe0.css">
</head>
<body>
<div id="root"></div>
Expand Down
121 changes: 102 additions & 19 deletions src/uipath/dev/services/agent_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
import logging
import os
import re
import signal
import subprocess
import sys
import uuid
from collections.abc import Callable
from dataclasses import dataclass, field
Expand All @@ -27,6 +29,10 @@
BASH_TIMEOUT = 30
TOOLS_REQUIRING_APPROVAL = {"write_file", "edit_file", "bash"}

# Matches standard ANSI CSI sequences, OSC sequences (e.g. hyperlinks), and
# carriage returns used by spinners to overwrite lines.
_ANSI_RE = re.compile(r"\x1b\][^\x1b]*(?:\x1b\\|\x07)|\x1b\[[0-9;]*[A-Za-z]|\r")

# ---------------------------------------------------------------------------
# System prompt
# ---------------------------------------------------------------------------
Expand All @@ -35,7 +41,14 @@
You are a senior Python developer specializing in UiPath coded agents and automations. \
You help users build, debug, test, and improve Python agents using the UiPath SDK.

## Core Principles
## Critical Rules

### You Have a Shell — USE IT
- You have direct access to the user's terminal via the `bash` tool.
- NEVER tell the user to run commands themselves. ALWAYS use `bash` to execute commands directly.
- This includes: `uv run`, `uv sync`, `uipath init`, `uipath run`, `uipath eval`, `uipath pack`, `uipath publish`, and any other CLI commands.
- If something needs to be run, run it. Don't explain how to run it.
- The user is already authenticated — NEVER ask them to authenticate or run `uipath auth`.

### Read Before Writing
- NEVER suggest code changes without first reading the relevant source files.
Expand All @@ -60,15 +73,37 @@
1. **Plan first**: Call `update_plan` with your steps before doing anything else.
2. **Explore**: Use `glob` and `grep` to find relevant files. Read them with `read_file`.
3. **Implement**: Use `edit_file` for targeted changes, `write_file` only for new files.
4. **Verify**: Read back edited files to confirm correctness. Run tests or linters via `bash` when appropriate.
5. **Update progress**: Mark plan steps as "completed" as you go, add new steps if scope expands.
6. **Summarize**: When done, briefly state what you changed and why.
4. **Execute**: Use `bash` to run commands — installations, tests, linters, builds, `uv run` commands, etc. \
When the user asks you to run something, always use the `bash` tool to execute it. Never tell the user to run commands themselves — you have a shell, use it.
5. **Verify**: Read back edited files to confirm correctness. Run tests or linters via `bash`.
6. **Update progress**: Mark plan steps as "completed" as you go, add new steps if scope expands.
7. **Summarize**: When done, briefly state what you changed and why.

## Full Build Cycle

When building or modifying an agent or function, always complete the full cycle — don't stop at writing code:

1. **Write the code** — implement the agent/function with Input/Output models and `@traced()` main function.
2. **Generate entry points** — run `uv run uipath init` via `bash`.
3. **Write evaluations** — create BOTH:
- **Evaluator files** in `evaluations/evaluators/` (e.g. `exact-match.json`, `json-similarity.json`) — these must exist first.
- **Eval set files** in `evaluations/eval-sets/` with test cases that reference the evaluator IDs.
4. **Run the evaluations** — execute `uv run uipath eval` via `bash` and report results.
5. **Pack & publish** — when the user asks to deploy, run via `bash`:
- `uv run uipath pack` — create a .nupkg package
- `uv run uipath publish -w` — publish to personal workspace (default)
- `uv run uipath publish -t` — publish to a specific tenant folder

Never stop after just writing code. If you create an agent/function, you must also create evaluations and run them. \
If you create eval sets, you must also create the evaluator files they reference — otherwise the eval run will fail. \
If the user asks you to publish or deploy, use `bash` to run the commands — don't tell them to do it. \
Default to publishing to personal workspace (`-w`) unless the user specifies a tenant folder.

## Tools
- `read_file` — Read file contents (always use before editing)
- `write_file` — Create or overwrite a file
- `edit_file` — Surgical string replacement (old_string must be unique)
- `bash` — Execute a shell command (timeout: 30s)
- `bash` — Execute a shell command (timeout: 30s). USE THIS to run commands for the user — never tell the user to run commands manually when you can run them yourself.
- `glob` — Find files matching a pattern (e.g. `**/*.py`)
- `grep` — Search file contents with regex
- `update_plan` — Create or update your task plan
Expand Down Expand Up @@ -147,14 +182,19 @@
"type": "function",
"function": {
"name": "bash",
"description": "Execute a shell command and return stdout/stderr. Timeout: 30s.",
"description": "Execute a shell command and return stdout/stderr. Timeout: 30s. "
"For commands that prompt for input, provide the expected input via the stdin parameter.",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The shell command to execute.",
}
},
"stdin": {
"type": "string",
"description": "Optional input to feed to the command's stdin. Use this for commands that prompt for input (e.g. confirmations, selections). Use newlines to separate multiple inputs.",
},
},
"required": ["command"],
},
Expand Down Expand Up @@ -322,24 +362,69 @@ def _tool_edit_file(args: dict[str, Any]) -> str:
return "Edit applied successfully"


def _kill_process_tree(proc: subprocess.Popen[str]) -> None:
"""Kill a process and all its children."""
try:
if sys.platform == "win32":
# taskkill /T kills the entire process tree on Windows
subprocess.run(
["taskkill", "/F", "/T", "/PID", str(proc.pid)],
capture_output=True,
)
else:
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
except (ProcessLookupError, OSError):
pass
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()


def _tool_bash(args: dict[str, Any]) -> str:
command = args["command"]
stdin_input = args.get("stdin")
try:
result = subprocess.run(
command,
env = os.environ.copy()
env["PYTHONUTF8"] = "1"
env["NO_COLOR"] = "1"
env["TERM"] = "dumb"
popen_kwargs: dict[str, Any] = dict(
shell=True,
capture_output=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
timeout=BASH_TIMEOUT,
encoding="utf-8",
errors="replace",
cwd=str(_PROJECT_ROOT),
env=env,
)
# DEVNULL prevents hanging on interactive prompts when no input given.
# PIPE is used only when the agent explicitly provides stdin input.
if stdin_input is not None:
popen_kwargs["stdin"] = subprocess.PIPE
else:
popen_kwargs["stdin"] = subprocess.DEVNULL
# On Unix, create a new process group so we can kill the whole tree.
if sys.platform != "win32":
popen_kwargs["start_new_session"] = True

proc = subprocess.Popen(command, **popen_kwargs)
try:
stdout, stderr = proc.communicate(input=stdin_input, timeout=BASH_TIMEOUT)
except subprocess.TimeoutExpired:
_kill_process_tree(proc)
return f"Error: command timed out after {BASH_TIMEOUT}s"

output = ""
if result.stdout:
output += result.stdout
if result.stderr:
output += ("\n" if output else "") + result.stderr
if result.returncode != 0:
output += f"\n[exit code: {result.returncode}]"
if stdout:
output += stdout
if stderr:
output += ("\n" if output else "") + stderr
# Strip ANSI escape sequences and spinner artifacts
output = _ANSI_RE.sub("", output)
if proc.returncode != 0:
output += f"\n[exit code: {proc.returncode}]"
if not output:
output = "[no output]"
if len(output) > MAX_OUTPUT_CHARS:
Expand All @@ -348,8 +433,6 @@ def _tool_bash(args: dict[str, Any]) -> str:
+ f"\n... [truncated at {MAX_OUTPUT_CHARS} chars]"
)
return output
except subprocess.TimeoutExpired:
return f"Error: command timed out after {BASH_TIMEOUT}s"
except Exception as e:
return f"Error: {e}"

Expand Down
Loading