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.38"
version = "0.0.39"
description = "UiPath Developer Console"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
38 changes: 33 additions & 5 deletions src/uipath/dev/server/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ async def run_async(self) -> None:
self.port = self._find_free_port(self.host, self.port)
app = self.create_app()

base_url = f"http://{self.host}:{self.port}"
self._print_banner(base_url)

if self.open_browser:
threading.Thread(
target=self._deferred_open_browser,
Expand All @@ -111,7 +114,7 @@ async def run_async(self) -> None:
app,
host=self.host,
port=self.port,
log_level="info",
log_level="warning",
)
server = uvicorn.Server(config)
await server.serve()
Expand All @@ -129,7 +132,7 @@ def run(self) -> None:
try:
asyncio.run(self.run_async())
except KeyboardInterrupt:
logger.info("Server stopped.")
pass

# ------------------------------------------------------------------
# Internal callbacks
Expand Down Expand Up @@ -173,9 +176,34 @@ def _find_free_port(host: str, start_port: int, max_attempts: int = 100) -> int:
f"Could not find a free port in range {start_port}-{start_port + max_attempts - 1}"
)

@staticmethod
def _print_banner(base_url: str) -> None:
"""Print a welcome banner to the console."""
import sys

# Use emojis only if stdout supports unicode (not Windows cp1252)
try:
"\U0001f916".encode(sys.stdout.encoding or "utf-8")
server_icon, docs_icon = "\U0001f916", "\U0001f4da"
except (UnicodeEncodeError, LookupError):
server_icon, docs_icon = ">>", ">>"

banner = (
"\n"
" _ _ _ ____ _ _ ____\n"
"| | | (_) _ \\ __ _| |_| |__ | _ \\ _____ __\n"
"| | | | | |_) / _` | __| '_ \\ | | | |/ _ \\ \\ / /\n"
"| |_| | | __/ (_| | |_| | | | | |_| | __/\\ V /\n"
" \\___/|_|_| \\__,_|\\__|_| |_| |____/ \\___| \\_/\n"
"\n"
f" {server_icon} Server: {base_url}\n"
f" {docs_icon} Docs: https://uipath.github.io/uipath-python/\n"
"\n"
" This server is designed for development and testing.\n"
)
print(banner)

def _deferred_open_browser(self) -> None:
"""Open the browser after a short delay to let uvicorn bind."""
time.sleep(1.5)
url = f"http://{self.host}:{self.port}"
logger.info("Opening browser at %s", url)
webbrowser.open(url)
webbrowser.open(f"http://{self.host}:{self.port}")
53 changes: 35 additions & 18 deletions src/uipath/dev/server/frontend/src/components/chat/ChatPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import type { WsClient } from "../../api/websocket";
import { useRunStore } from "../../store/useRunStore";
import ChatMessage from "./ChatMessage";
Expand Down Expand Up @@ -42,12 +42,15 @@ export default function ChatPanel({ messages, runId, runStatus, ws }: Props) {
return map;
}, [messages]);

const [showScrollTop, setShowScrollTop] = useState(false);

// Track whether user has scrolled away from bottom
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)
Expand All @@ -72,24 +75,38 @@ export default function ChatPanel({ messages, runId, runStatus, ws }: Props) {

return (
<div className="flex flex-col h-full">
<div
ref={scrollRef}
onScroll={handleScroll}
className="flex-1 overflow-y-auto px-3 py-2 space-y-0.5"
>
{messages.length === 0 && (
<p className="text-[var(--text-muted)] text-xs text-center py-6">
No messages yet
</p>
<div className="relative flex-1 overflow-hidden">
<div
ref={scrollRef}
onScroll={handleScroll}
className="h-full overflow-y-auto px-3 py-2 space-y-0.5"
>
{messages.length === 0 && (
<p className="text-[var(--text-muted)] text-xs text-center py-6">
No messages yet
</p>
)}
{messages.map((msg) => (
<ChatMessage
key={msg.message_id}
message={msg}
toolCallIndices={toolCallIndicesMap.get(msg.message_id)}
onToolCallClick={(name, idx) => setFocusedSpan({ name, index: idx })}
/>
))}
</div>
{showScrollTop && (
<button
onClick={() => scrollRef.current?.scrollTo({ top: 0, behavior: "smooth" })}
className="absolute top-2 right-3 w-6 h-6 flex items-center justify-center rounded-full cursor-pointer transition-opacity opacity-70 hover:opacity-100"
style={{ background: "var(--bg-tertiary)", color: "var(--text-primary)" }}
title="Scroll to top"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="18 15 12 9 6 15" />
</svg>
</button>
)}
{messages.map((msg) => (
<ChatMessage
key={msg.message_id}
message={msg}
toolCallIndices={toolCallIndicesMap.get(msg.message_id)}
onToolCallClick={(name, idx) => setFocusedSpan({ name, index: idx })}
/>
))}
</div>
<ChatInput
onSend={handleSend}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default function DebugControls({ runId, status, ws, breakpointNode }: Pro

return (
<div
className="flex items-center gap-1 px-4 border-b shrink-0 h-[33px]"
className="flex items-center gap-1 px-4 py-2.5 border-b shrink-0"
style={{ borderColor: "var(--border)", background: "var(--bg-secondary)" }}
>
<span className="text-[10px] uppercase tracking-wider font-semibold mr-1" style={{ color: "var(--text-muted)" }}>
Expand Down
84 changes: 53 additions & 31 deletions src/uipath/dev/server/frontend/src/components/logs/LogPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import type { LogEntry } from "../../types/run";

const LEVEL_STYLES: Record<string, { color: string; bg: string; border: string }> = {
Expand All @@ -17,12 +17,20 @@ interface Props {
}

export default function LogPanel({ logs }: Props) {
const scrollRef = useRef<HTMLDivElement>(null);
const bottomRef = useRef<HTMLDivElement>(null);
const [showScrollTop, setShowScrollTop] = useState(false);

useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
}, [logs.length]);

const handleScroll = () => {
const el = scrollRef.current;
if (!el) return;
setShowScrollTop(el.scrollTop > 100);
};

if (logs.length === 0) {
return (
<div className="h-full flex items-center justify-center">
Expand All @@ -32,38 +40,52 @@ export default function LogPanel({ logs }: Props) {
}

return (
<div className="h-full overflow-y-auto font-mono text-xs">
{logs.map((log, i) => {
const time = new Date(log.timestamp).toLocaleTimeString(undefined, {
hour12: false,
});
const levelKey = log.level.toUpperCase();
const levelShort = levelKey.slice(0, 4);
const style = LEVEL_STYLES[levelKey] ?? DEFAULT_STYLE;
const isEven = i % 2 === 0;
<div className="h-full relative">
<div ref={scrollRef} onScroll={handleScroll} className="h-full overflow-y-auto font-mono text-xs">
{logs.map((log, i) => {
const time = new Date(log.timestamp).toLocaleTimeString(undefined, {
hour12: false,
});
const levelKey = log.level.toUpperCase();
const levelShort = levelKey.slice(0, 4);
const style = LEVEL_STYLES[levelKey] ?? DEFAULT_STYLE;
const isEven = i % 2 === 0;

return (
<div
key={i}
className="flex gap-3 px-3 py-1.5"
style={{
background: isEven ? "var(--bg-primary)" : "var(--bg-secondary)",
}}
>
<span className="text-[var(--text-muted)] shrink-0">{time}</span>
<span
className="shrink-0 self-start px-1.5 py-0.5 rounded text-[10px] font-semibold leading-none inline-flex items-center"
style={{ color: style.color, background: style.bg }}
return (
<div
key={i}
className="flex gap-3 px-3 py-1.5"
style={{
background: isEven ? "var(--bg-primary)" : "var(--bg-secondary)",
}}
>
{levelShort}
</span>
<span className="text-[var(--text-primary)] whitespace-pre-wrap break-all">
{log.message}
</span>
</div>
);
})}
<div ref={bottomRef} />
<span className="text-[var(--text-muted)] shrink-0">{time}</span>
<span
className="shrink-0 self-start px-1.5 py-0.5 rounded text-[10px] font-semibold leading-none inline-flex items-center"
style={{ color: style.color, background: style.bg }}
>
{levelShort}
</span>
<span className="text-[var(--text-primary)] whitespace-pre-wrap break-all">
{log.message}
</span>
</div>
);
})}
<div ref={bottomRef} />
</div>
{showScrollTop && (
<button
onClick={() => scrollRef.current?.scrollTo({ top: 0, behavior: "smooth" })}
className="absolute top-2 right-3 w-6 h-6 flex items-center justify-center rounded-full cursor-pointer transition-opacity opacity-70 hover:opacity-100"
style={{ background: "var(--bg-tertiary)", color: "var(--text-primary)" }}
title="Scroll to top"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="18 15 12 9 6 15" />
</svg>
</button>
)}
</div>
);
}
55 changes: 27 additions & 28 deletions src/uipath/dev/server/frontend/src/components/runs/NewRunPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,38 +36,37 @@ export default function NewRunPanel() {
</span>
</div>
<p className="text-xs" style={{ color: "var(--text-muted)" }}>
Select an entrypoint and choose a mode
{entrypoints.length > 1 ? "Select an entrypoint and choose a mode" : "Choose a mode"}
</p>
</div>

{/* Entrypoint */}
<div className="mb-8">
<label
className="block text-[10px] uppercase tracking-wider font-semibold mb-2"
style={{ color: "var(--text-muted)" }}
>
Entrypoint
</label>
<select
value={selectedEp}
onChange={(e) => setSelectedEp(e.target.value)}
className="w-full rounded-md px-3 py-1.5 text-xs font-mono cursor-pointer appearance-auto"
style={{
background: "var(--bg-secondary)",
border: "1px solid var(--border)",
color: "var(--text-primary)",
}}
>
{entrypoints.length === 0 && (
<option value="">Loading...</option>
)}
{entrypoints.map((ep) => (
<option key={ep} value={ep}>
{ep}
</option>
))}
</select>
</div>
{entrypoints.length > 1 && (
<div className="mb-8">
<label
className="block text-[10px] uppercase tracking-wider font-semibold mb-2"
style={{ color: "var(--text-muted)" }}
>
Entrypoint
</label>
<select
value={selectedEp}
onChange={(e) => setSelectedEp(e.target.value)}
className="w-full rounded-md px-3 py-1.5 text-xs font-mono cursor-pointer appearance-auto"
style={{
background: "var(--bg-secondary)",
border: "1px solid var(--border)",
color: "var(--text-primary)",
}}
>
{entrypoints.map((ep) => (
<option key={ep} value={ep}>
{ep}
</option>
))}
</select>
</div>
)}

{/* Mode cards */}
<div className="grid grid-cols-2 gap-4">
Expand Down
Loading