diff --git a/crates/diffcore-core/src/llm/mod.rs b/crates/diffcore-core/src/llm/mod.rs index c9f8d20..7ec295b 100644 --- a/crates/diffcore-core/src/llm/mod.rs +++ b/crates/diffcore-core/src/llm/mod.rs @@ -179,9 +179,40 @@ pub(crate) fn resolve_cli_executable(binary: &str) -> Option { fn find_binary_in_path(binary: &str) -> Option { let path_var = std::env::var_os("PATH")?; - std::env::split_paths(&path_var) - .map(|entry| entry.join(binary)) - .find(|path| is_executable_file(path)) + + #[cfg(not(windows))] + { + std::env::split_paths(&path_var) + .map(|entry| entry.join(binary)) + .find(|path| is_executable_file(path)) + } + + #[cfg(windows)] + { + let pathext = std::env::var("PATHEXT") + .unwrap_or_else(|_| ".EXE;.CMD;.BAT;.COM;.PS1".to_string()); + for entry in std::env::split_paths(&path_var) { + // Try bare name first + let bare = entry.join(binary); + if is_executable_file(&bare) { + return Some(bare); + } + // Then try with each PATHEXT extension + for ext in pathext.split(';') { + let ext = ext.trim(); + if ext.is_empty() { continue; } + let with_ext = entry.join(format!("{binary}{ext}")); + if is_executable_file(&with_ext) { + return Some(with_ext); + } + let lower = entry.join(format!("{binary}{}", ext.to_lowercase())); + if is_executable_file(&lower) { + return Some(lower); + } + } + } + None + } } fn candidate_cli_paths(binary: &str) -> Vec { @@ -200,6 +231,30 @@ fn candidate_cli_paths(binary: &str) -> Vec { } } + // Windows: USERPROFILE as home fallback, plus npm global locations + #[cfg(windows)] + { + if let Some(profile) = std::env::var_os("USERPROFILE") { + let home = PathBuf::from(profile); + let pathext = std::env::var("PATHEXT") + .unwrap_or_else(|_| ".EXE;.CMD;.BAT;.COM;.PS1".to_string()); + let exts: Vec<&str> = pathext.split(';').map(str::trim).filter(|e| !e.is_empty()).collect(); + for rel in [".npm-global/bin", ".npm/bin", ".local/bin", ".cargo/bin", "bin"] { + for ext in &exts { + candidates.push(home.join(rel).join(format!("{binary}{ext}"))); + candidates.push(home.join(rel).join(format!("{binary}{}", ext.to_lowercase()))); + } + } + } + for env in ["APPDATA", "LOCALAPPDATA"] { + if let Some(base) = std::env::var_os(env) { + let base = PathBuf::from(base); + candidates.push(base.join("npm").join(format!("{binary}.cmd"))); + candidates.push(base.join("npm").join(format!("{binary}.exe"))); + } + } + } + for prefix in ["/opt/homebrew/bin", "/usr/local/bin", "/usr/bin"] { candidates.push(PathBuf::from(prefix).join(binary)); } diff --git a/crates/diffcore-tauri/ui/src/components/FlowGraph.tsx b/crates/diffcore-tauri/ui/src/components/FlowGraph.tsx index a0f307a..97e4831 100644 --- a/crates/diffcore-tauri/ui/src/components/FlowGraph.tsx +++ b/crates/diffcore-tauri/ui/src/components/FlowGraph.tsx @@ -9,10 +9,12 @@ import { type Node, type Edge, type NodeMouseHandler, + type NodeChange, type EdgeProps, Position, getBezierPath, MarkerType, + applyNodeChanges, } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; import dagre from "@dagrejs/dagre"; @@ -399,9 +401,15 @@ export default function FlowGraph({ edges, files, onNodeClick, replayNodeId }: F const [selectedNodeId, setSelectedNodeId] = useState(null); const [fullscreen, setFullscreen] = useState(false); + const [nodes, setNodes] = useState(initialNodes); const containerRef = useRef(null); const reactFlowInstance = useRef(null); + // Sync nodes when the layout changes (new analysis / group selection) + useEffect(() => { + setNodes(initialNodes); + }, [initialNodes]); + // The "active" node is either the replay node (if replaying) or the user-clicked node const activeNodeId = replayNodeId ?? selectedNodeId; @@ -419,7 +427,7 @@ export default function FlowGraph({ edges, files, onNodeClick, replayNodeId }: F // Track active node for visual highlight + replay glow const processedNodes = useMemo(() => { - return initialNodes.map((node) => ({ + return nodes.map((node) => ({ ...node, data: { ...node.data, @@ -427,7 +435,12 @@ export default function FlowGraph({ edges, files, onNodeClick, replayNodeId }: F replayActive: replayNodeId != null && node.id === replayNodeId, }, })); - }, [initialNodes, activeNodeId, replayNodeId]); + }, [nodes, activeNodeId, replayNodeId]); + + // Handle node position changes from dragging + const onNodesChange = useCallback((changes: NodeChange[]) => { + setNodes((nds) => applyNodeChanges(changes, nds)); + }, []); const handleNodeClick: NodeMouseHandler = useCallback( (_event, node) => { @@ -501,6 +514,7 @@ export default function FlowGraph({ edges, files, onNodeClick, replayNodeId }: F edges={processedEdges} nodeTypes={nodeTypes} edgeTypes={edgeTypes} + onNodesChange={onNodesChange} onNodeClick={handleNodeClick} onPaneClick={handlePaneClick} onInit={(instance: any) => { reactFlowInstance.current = instance; }}