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.65"
version = "0.0.66"
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 @@ -13,6 +13,7 @@ function FileTreeNode({ path, name, type, depth }: {
const isExpanded = useExplorerStore((s) => !!s.expanded[path]);
const isLoading = useExplorerStore((s) => !!s.loadingDirs[path]);
const isDirty = useExplorerStore((s) => !!s.dirty[path]);
const isAgentChanged = useExplorerStore((s) => !!s.agentChangedFiles[path]);
const selectedFile = useExplorerStore((s) => s.selectedFile);
const { setChildren, toggleExpanded, setLoadingDir, openTab } = useExplorerStore();
const { navigate } = useHashRoute();
Expand Down Expand Up @@ -40,7 +41,7 @@ function FileTreeNode({ path, name, type, depth }: {
<>
<button
onClick={handleClick}
className="w-full text-left flex items-center gap-1 py-[3px] text-[13px] cursor-pointer transition-colors group"
className={`w-full text-left flex items-center gap-1 py-[3px] text-[13px] cursor-pointer transition-colors group${isAgentChanged ? " agent-changed-file" : ""}`}
style={{
paddingLeft: `${12 + depth * 16}px`,
paddingRight: "8px",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useCallback, useRef } from "react";
import Editor, { type OnMount, type BeforeMount } from "@monaco-editor/react";
import Editor, { DiffEditor, type OnMount, type BeforeMount } from "@monaco-editor/react";
import { useExplorerStore } from "../../store/useExplorerStore";
import { useTheme } from "../../store/useTheme";
import { useHashRoute } from "../../hooks/useHashRoute";
Expand Down Expand Up @@ -95,7 +95,9 @@ export default function FileEditor() {
const buffer = useExplorerStore((s) => (filePath ? s.buffers[filePath] : undefined));
const loadingFile = useExplorerStore((s) => s.loadingFile);
const dirty = useExplorerStore((s) => s.dirty);
const { setFileContent, updateBuffer, markClean, setLoadingFile, openTab, closeTab } =
const diffView = useExplorerStore((s) => s.diffView);
const isAgentChanged = useExplorerStore((s) => (filePath ? !!s.agentChangedFiles[filePath] : false));
const { setFileContent, updateBuffer, markClean, setLoadingFile, openTab, closeTab, setDiffView } =
useExplorerStore();
const { navigate } = useHashRoute();
const theme = useTheme((s) => s.theme);
Expand Down Expand Up @@ -319,6 +321,8 @@ export default function FileEditor() {
);
}

const showDiff = diffView && diffView.path === filePath;

return (
<div className="flex flex-col h-full">
{tabBar}
Expand All @@ -336,6 +340,14 @@ export default function FileEditor() {
</span>
)}
<span style={{ color: "var(--text-muted)" }}>{formatSize(fileContent.size)}</span>
{isAgentChanged && (
<span
className="px-1.5 py-0.5 rounded text-[10px] font-medium"
style={{ background: "color-mix(in srgb, var(--info) 20%, transparent)", color: "var(--info)" }}
>
Agent modified
</span>
)}
<div className="flex-1" />
{isDirty && (
<span className="text-[10px] font-medium" style={{ color: "var(--accent)" }}>
Expand All @@ -354,27 +366,69 @@ export default function FileEditor() {
Save
</button>
</div>
{/* Editor */}
{/* Diff banner */}
{showDiff && (
<div
className="h-8 flex items-center px-3 gap-2 text-xs shrink-0 border-b"
style={{ borderColor: "var(--border)", background: "color-mix(in srgb, var(--info) 10%, var(--bg-secondary))" }}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--info)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="16" x2="12" y2="12" />
<line x1="12" y1="8" x2="12.01" y2="8" />
</svg>
<span style={{ color: "var(--info)" }}>Agent modified this file</span>
<div className="flex-1" />
<button
onClick={() => setDiffView(null)}
className="px-2 py-0.5 rounded text-[11px] font-medium cursor-pointer transition-colors"
style={{ background: "var(--bg-hover)", color: "var(--text-secondary)", border: "none" }}
>
Dismiss
</button>
</div>
)}
{/* Editor or DiffEditor */}
<div className="flex-1 overflow-hidden">
<Editor
key={filePath}
language={fileContent.language ?? "plaintext"}
theme={theme === "dark" ? "uipath-dark" : "uipath-light"}
value={buffer ?? fileContent.content ?? ""}
onChange={handleChange}
beforeMount={handleBeforeMount}
onMount={handleEditorMount}
options={{
minimap: { enabled: false },
fontSize: 13,
lineNumbersMinChars: 4,
scrollBeyondLastLine: false,
wordWrap: "on",
automaticLayout: true,
tabSize: 2,
renderWhitespace: "selection",
}}
/>
{showDiff ? (
<DiffEditor
key={`diff-${filePath}`}
original={diffView.original}
modified={diffView.modified}
language={diffView.language ?? "plaintext"}
theme={theme === "dark" ? "uipath-dark" : "uipath-light"}
beforeMount={handleBeforeMount}
options={{
readOnly: true,
minimap: { enabled: false },
fontSize: 13,
lineNumbersMinChars: 4,
scrollBeyondLastLine: false,
automaticLayout: true,
renderSideBySide: true,
}}
/>
) : (
<Editor
key={filePath}
language={fileContent.language ?? "plaintext"}
theme={theme === "dark" ? "uipath-dark" : "uipath-light"}
value={buffer ?? fileContent.content ?? ""}
onChange={handleChange}
beforeMount={handleBeforeMount}
onMount={handleEditorMount}
options={{
minimap: { enabled: false },
fontSize: 13,
lineNumbersMinChars: 4,
scrollBeyondLastLine: false,
wordWrap: "on",
automaticLayout: true,
tabSize: 2,
renderWhitespace: "selection",
}}
/>
)}
</div>
</div>
);
Expand Down
13 changes: 12 additions & 1 deletion src/uipath/dev/server/frontend/src/store/useAgentStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ function nextId() {
interface AgentStore {
sessionId: string | null;
status: AgentStatus;
_lastActiveAt: number | null;
messages: AgentMessage[];
plan: AgentPlanItem[];
activeQuestion: AgentQuestion | null;
Expand Down Expand Up @@ -45,6 +46,7 @@ interface AgentStore {
export const useAgentStore = create<AgentStore>((set) => ({
sessionId: null,
status: "idle",
_lastActiveAt: null,
messages: [],
plan: [],
activeQuestion: null,
Expand All @@ -55,7 +57,15 @@ export const useAgentStore = create<AgentStore>((set) => ({
selectedSkillIds: [],
skillsLoading: false,

setStatus: (status) => set({ status }),
setStatus: (status) =>
set((state) => {
const wasActive = state.status === "thinking" || state.status === "planning" || state.status === "executing" || state.status === "awaiting_approval";
const isActive = status === "thinking" || status === "planning" || status === "executing" || status === "awaiting_approval";
return {
status,
_lastActiveAt: wasActive && !isActive ? Date.now() : state._lastActiveAt,
};
}),

addUserMessage: (text) =>
set((state) => ({
Expand Down Expand Up @@ -277,6 +287,7 @@ export const useAgentStore = create<AgentStore>((set) => ({
set({
sessionId: null,
status: "idle",
_lastActiveAt: null,
messages: [],
plan: [],
activeQuestion: null,
Expand Down
36 changes: 36 additions & 0 deletions src/uipath/dev/server/frontend/src/store/useExplorerStore.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { create } from "zustand";
import type { FileEntry, FileContent } from "../types/explorer";

interface DiffView {
path: string;
original: string;
modified: string;
language: string | null;
}

interface ExplorerStore {
children: Record<string, FileEntry[]>;
expanded: Record<string, boolean>;
Expand All @@ -11,6 +18,8 @@ interface ExplorerStore {
buffers: Record<string, string>;
loadingDirs: Record<string, boolean>;
loadingFile: boolean;
agentChangedFiles: Record<string, number>;
diffView: DiffView | null;

setChildren: (path: string, entries: FileEntry[]) => void;
toggleExpanded: (path: string) => void;
Expand All @@ -22,6 +31,10 @@ interface ExplorerStore {
markClean: (path: string) => void;
setLoadingDir: (path: string, loading: boolean) => void;
setLoadingFile: (loading: boolean) => void;
markAgentChanged: (path: string) => void;
clearAgentChanged: (path: string) => void;
setDiffView: (diff: DiffView | null) => void;
expandPath: (filePath: string) => void;
}

export const useExplorerStore = create<ExplorerStore>((set) => ({
Expand All @@ -34,6 +47,8 @@ export const useExplorerStore = create<ExplorerStore>((set) => ({
buffers: {},
loadingDirs: {},
loadingFile: false,
agentChangedFiles: {},
diffView: null,

setChildren: (path, entries) =>
set((state) => ({ children: { ...state.children, [path]: entries } })),
Expand Down Expand Up @@ -86,4 +101,25 @@ export const useExplorerStore = create<ExplorerStore>((set) => ({
set((state) => ({ loadingDirs: { ...state.loadingDirs, [path]: loading } })),

setLoadingFile: (loading) => set({ loadingFile: loading }),

markAgentChanged: (path) =>
set((state) => ({ agentChangedFiles: { ...state.agentChangedFiles, [path]: Date.now() } })),

clearAgentChanged: (path) =>
set((state) => {
const { [path]: _, ...rest } = state.agentChangedFiles;
return { agentChangedFiles: rest };
}),

setDiffView: (diff) => set({ diffView: diff }),

expandPath: (filePath) =>
set((state) => {
const parts = filePath.split("/");
const expanded = { ...state.expanded };
for (let i = 1; i < parts.length; i++) {
expanded[parts.slice(0, i).join("/")] = true;
}
return { expanded };
}),
}));
98 changes: 88 additions & 10 deletions src/uipath/dev/server/frontend/src/store/useWebSocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,95 @@ 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) => {
const s = useExplorerStore.getState();
if (s.dirty[tab]) return;
if (s.fileCache[tab]?.content === fc.content) return;
s.setFileContent(tab, fc);
}).catch(() => {});
const agentState = useAgentStore.getState();
const agentStatus = agentState.status;
const agentIsActive = agentStatus === "thinking" || agentStatus === "planning" || agentStatus === "executing" || agentStatus === "awaiting_approval";
// Grace period: treat changes within 3s of agent finishing as agent-driven
// (file watcher debounce can delay files.changed past agent.status:done)
const recentlyActive = !agentIsActive && agentState._lastActiveAt != null && (Date.now() - agentState._lastActiveAt) < 3000;

// Filter out directory paths — file watcher reports both files and dirs.
// A path with a dot in the last segment is likely a file; no dot = likely a directory.
// Also skip paths already known as directories in the tree.
const fileChanges = changedFiles.filter((p) => {
if (p in explorer.children) return false; // known directory
const lastSegment = p.split("/").pop() ?? "";
return lastSegment.includes(".");
});

console.log("[files.changed]", { all: changedFiles, files: fileChanges, agentStatus, agentIsActive, recentlyActive });

if (agentIsActive || recentlyActive) {
// Agent is running (or just finished) — treat file changes as agent-driven
// We track diff candidate across async calls; first file to resolve wins
let diffShown = false;

for (const filePath of fileChanges) {
// Skip dirty files (user is editing)
if (explorer.dirty[filePath]) continue;

// Snapshot old content before fetching new (for diff)
const oldContent = explorer.fileCache[filePath]?.content ?? null;
const oldLanguage = explorer.fileCache[filePath]?.language ?? null;

// Fetch new content — this also validates the file still exists
readFile(filePath).then((fc) => {
const s = useExplorerStore.getState();
if (s.dirty[filePath]) return;
s.setFileContent(filePath, fc);

// Expand tree to reveal the file (but don't auto-open a new tab)
s.expandPath(filePath);

// Load unloaded parent directories so tree reveals the file
const parts = filePath.split("/");
for (let j = 1; j < parts.length; j++) {
const dir = parts.slice(0, j).join("/");
if (!(dir in useExplorerStore.getState().children)) {
listDirectory(dir)
.then((entries) => useExplorerStore.getState().setChildren(dir, entries))
.catch(() => {});
}
}

// Show diff for the first eligible file (only if already open in a tab)
const isOpenInTab = useExplorerStore.getState().openTabs.includes(filePath);
if (!diffShown && isOpenInTab && oldContent !== null && fc.content !== null && oldContent !== fc.content) {
diffShown = true;
useExplorerStore.getState().setSelectedFile(filePath);
s.setDiffView({ path: filePath, original: oldContent, modified: fc.content, language: oldLanguage });
setTimeout(() => {
const cur = useExplorerStore.getState().diffView;
if (cur && cur.path === filePath && cur.original === oldContent) {
useExplorerStore.getState().setDiffView(null);
}
}, 5000);
}

s.markAgentChanged(filePath);
setTimeout(() => useExplorerStore.getState().clearAgentChanged(filePath), 10000);
}).catch(() => {
// File doesn't exist (deleted) — close tab if it was open
const s = useExplorerStore.getState();
if (s.openTabs.includes(filePath)) {
s.closeTab(filePath);
}
});
}
} else {
// No agent running — regular refresh logic
for (const tab of explorer.openTabs) {
if (explorer.dirty[tab] || !changedSet.has(tab)) continue;
readFile(tab).then((fc) => {
const s = useExplorerStore.getState();
if (s.dirty[tab]) return;
if (s.fileCache[tab]?.content === fc.content) return;
s.setFileContent(tab, fc);
}).catch(() => {});
}
}
// Refresh directory listings for already-loaded parent dirs

// Refresh directory listings for already-loaded parent dirs (always)
const dirsToRefresh = new Set<string>();
for (const filePath of changedFiles) {
const lastSlash = filePath.lastIndexOf("/");
Expand Down
10 changes: 10 additions & 0 deletions src/uipath/dev/server/frontend/src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -343,3 +343,13 @@ select option {
[data-theme="light"] .chat-markdown .hljs-bullet { color: #735c0f }
[data-theme="light"] .chat-markdown .hljs-addition { color: #22863a; background-color: #f0fff4 }
[data-theme="light"] .chat-markdown .hljs-deletion { color: #b31d28; background-color: #ffeef0 }

/* Agent file change pulse animation (uses box-shadow to avoid inline style conflicts) */
@keyframes agent-changed-pulse {
0%, 100% { box-shadow: inset 0 0 0 50px color-mix(in srgb, var(--success) 20%, transparent); }
50% { box-shadow: none; }
}

.agent-changed-file {
animation: agent-changed-pulse 2s ease-in-out 3;
}
Loading