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
61 changes: 58 additions & 3 deletions crates/diffcore-core/src/llm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,40 @@ pub(crate) fn resolve_cli_executable(binary: &str) -> Option<PathBuf> {

fn find_binary_in_path(binary: &str) -> Option<PathBuf> {
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<PathBuf> {
Expand All @@ -200,6 +231,30 @@ fn candidate_cli_paths(binary: &str) -> Vec<PathBuf> {
}
}

// 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));
}
Expand Down
18 changes: 16 additions & 2 deletions crates/diffcore-tauri/ui/src/components/FlowGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -399,9 +401,15 @@ export default function FlowGraph({ edges, files, onNodeClick, replayNodeId }: F

const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
const [fullscreen, setFullscreen] = useState(false);
const [nodes, setNodes] = useState<Node[]>(initialNodes);
const containerRef = useRef<HTMLDivElement>(null);
const reactFlowInstance = useRef<any>(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;

Expand All @@ -419,15 +427,20 @@ 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,
selected: node.id === activeNodeId,
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) => {
Expand Down Expand Up @@ -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; }}
Expand Down